From 8385c08ec7fc15e60fc936375cd877262e020077 Mon Sep 17 00:00:00 2001 From: plum <40649341+plum-k@users.noreply.github.com> Date: Wed, 8 Apr 2026 17:25:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(all):=20=E6=95=B0=E6=8D=AE=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF=E8=BF=81=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/editor/src/http/api/dataSet.ts | 53 ++ packages/editor/src/language/zh-CN-en-US.ts | 19 + .../src/views/editor/layouts/Sidebar.vue | 3 + .../layouts/sidebar/SidebarDataComponent.vue | 598 ++++++++++++++++ .../sdk/lib/core/tools/DataBindingManager.ts | 645 ++++++++++++++++++ packages/sdk/lib/core/tools/index.ts | 1 + packages/sdk/lib/core/viewer/Viewer.ts | 8 + packages/sdk/types/app/Project.d.ts | 119 ++++ 8 files changed, 1446 insertions(+) create mode 100644 packages/editor/src/http/api/dataSet.ts create mode 100644 packages/editor/src/views/editor/layouts/sidebar/SidebarDataComponent.vue create mode 100644 packages/sdk/lib/core/tools/DataBindingManager.ts diff --git a/packages/editor/src/http/api/dataSet.ts b/packages/editor/src/http/api/dataSet.ts new file mode 100644 index 0000000..6004b71 --- /dev/null +++ b/packages/editor/src/http/api/dataSet.ts @@ -0,0 +1,53 @@ +import {request} from "@/http/request"; + +export interface DataSetQueryParams { + page?: number; + pageSize?: number; + limit?: number; + offset?: number; + name?: string; + type?: string; + groupId?: IDataSet.Item["groupId"]; +} + +export interface DataSetPayload { + id?: IDataSet.Item["id"]; + name: string; + groupId: IDataSet.Item["groupId"]; + type: "API" | "SQL" | "JSON" | string; + method?: IDataSet.Item["method"]; + api?: string; + dataSourceId?: string; + sql?: string; + json?: string; +} + +export interface DataSetExecutePayload { + params?: any[]; + query?: Record; + body?: Record; +} + +export function fetchDataSetPage(params: DataSetQueryParams) { + return request.get>(`/data-set/page`, {params}); +} + +export function fetchDataSetDetail(id: IDataSet.Item["id"]) { + return request.get(`/data-set/${id}`); +} + +export function fetchCreateDataSet(data: DataSetPayload) { + return request.post(`/data-set`, data); +} + +export function fetchUpdateDataSet(data: DataSetPayload) { + return request.put(`/data-set`, data); +} + +export function fetchDeleteDataSet(id: IDataSet.Item["id"]) { + return request.delete(`/data-set/${id}`, {}); +} + +export function fetchExecuteDataSet(id: IDataSet.Item["id"], data?: DataSetExecutePayload) { + return request.post(`/data-set/${id}/execute`, data); +} diff --git a/packages/editor/src/language/zh-CN-en-US.ts b/packages/editor/src/language/zh-CN-en-US.ts index 0ec4bb1..d4865c9 100644 --- a/packages/editor/src/language/zh-CN-en-US.ts +++ b/packages/editor/src/language/zh-CN-en-US.ts @@ -32,6 +32,25 @@ export default { "Data source name":"数据源名称", "Data source type":"数据源类型", "Connection string":"连接字符串", + "Data component": "数据组件", + dataComponent: { + "Data set": "数据集", + "Select data set": "选择数据集", + "Data filter": "数据过滤器", + "Enable filter": "启用过滤", + "Disabled filter": "禁用过滤", + "Popup editor": "弹出编辑", + "Filter editor": "过滤器编辑", + "Auto refresh": "自动刷新", + "Seconds once": "秒一次", + "Apply to model": "应用于模型", + "Transition": "过渡时间", + "Milliseconds": "毫秒", + "Data result": "数据结果", + "Result editor": "结果编辑", + "Manual refresh": "手动刷新", + "Result error": "结果错误", + }, "Username":"用户名", "Password":"密码", "Test the connection":"测试连接", diff --git a/packages/editor/src/views/editor/layouts/Sidebar.vue b/packages/editor/src/views/editor/layouts/Sidebar.vue index 3bc3f3a..2192753 100644 --- a/packages/editor/src/views/editor/layouts/Sidebar.vue +++ b/packages/editor/src/views/editor/layouts/Sidebar.vue @@ -14,6 +14,7 @@ import { MagicWandFilled, CloudSnow, HeatMap, + DataBase, Opacity, ImageReference, LocationHeart, @@ -45,6 +46,7 @@ import SidebarHeatmap from "./sidebar/SidebarHeatmap.vue"; import SidebarPath from "./sidebar/SidebarPath.vue"; import SidebarUIPanel from "./sidebar/SidebarUIPanel.vue"; import SidebarWaterPool from "./sidebar/SidebarWaterPool.vue"; +import SidebarDataComponent from "./sidebar/SidebarDataComponent.vue"; const tabsInstRef = ref(null); const tabs = ref>([]); @@ -74,6 +76,7 @@ function setTabs(object){ { name: "geometry", icon: { text: 'Geometry',color:"#6287D1", component: markRaw(GroupObjects) }, component: markRaw(SidebarGeometry) }, { name: "material", icon: { text: 'Material',color:"#6287D1", component: markRaw(Opacity) }, component: markRaw(SidebarMaterial) }, { name: "animations", icon: { text: 'Animations',color:"#06AF88", component: markRaw(Draw) }, component: markRaw(SidebarAnimations) }, + { name: "dataComponent", icon: { text: 'Data component', color:"#0bbc14", component: markRaw(DataBase) }, component: markRaw(SidebarDataComponent) }, { name: "script", icon: { text: 'Script',color:"#06AF88", component: markRaw(Script) }, component: markRaw(SidebarScript) }, ] diff --git a/packages/editor/src/views/editor/layouts/sidebar/SidebarDataComponent.vue b/packages/editor/src/views/editor/layouts/sidebar/SidebarDataComponent.vue new file mode 100644 index 0000000..102f7b9 --- /dev/null +++ b/packages/editor/src/views/editor/layouts/sidebar/SidebarDataComponent.vue @@ -0,0 +1,598 @@ + + + + + diff --git a/packages/sdk/lib/core/tools/DataBindingManager.ts b/packages/sdk/lib/core/tools/DataBindingManager.ts new file mode 100644 index 0000000..c67f252 --- /dev/null +++ b/packages/sdk/lib/core/tools/DataBindingManager.ts @@ -0,0 +1,645 @@ +import * as THREE from "three"; +import { Easing, Tween } from "three/examples/jsm/libs/tween.module.js"; +import App from "@/core/app/App"; + +export interface AstralMaterialFormat { + color?: string; + opacity?: number; + transparent?: boolean; + metalness?: number; + roughness?: number; + emissive?: string; + emissiveIntensity?: number; +} + +export type AstralAnimationBehavior = "play" | "pause" | "stop"; + +export interface AstralAnimationCommand { + name: string; + behavior: AstralAnimationBehavior; + duration?: number; +} + +export interface AstralDataFormat { + position?: [number, number, number]; + rotation?: [number, number, number]; + scale?: [number, number, number]; + material?: AstralMaterialFormat; + visible?: boolean; + animations?: AstralAnimationCommand[]; +} + +export interface DataBindingResult { + changed: boolean; + errors: string[]; +} + +export interface DataBindingOptions { + transition?: number; + onUpdate?: (object: THREE.Object3D) => void; +} + +export interface DataComponentConfig { + dataSetId?: unknown; + filterEnabled?: boolean; + filterBody?: string; + autoRefreshEnabled?: boolean; + autoRefreshInterval?: number; + applyToModel?: boolean; + transition?: number; +} + +export interface DataComponentEntry { + config?: DataComponentConfig; + data?: unknown; +} + +export interface SetConfigOptions { + index?: number; +} + +export interface SetDataOptions { + index?: number; + applyToModel?: boolean; + transition?: number; + onUpdate?: (object: THREE.Object3D) => void; +} + +type Vector3Tuple = [number, number, number]; + +const animationBehaviors = new Set(["play", "pause", "stop"]); +const activeTweens = new WeakMap>>>(); +const pendingUpdates = new WeakMap(); +function isPlainObject(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function parseVector3(value: unknown, field: string, errors: string[]): Vector3Tuple | null { + if (!Array.isArray(value) || value.length !== 3) { + errors.push(`${field} must be an array of 3 numbers`); + return null; + } + const parsed = value.map(item => Number(item)) as number[]; + if (parsed.some(item => !Number.isFinite(item))) { + errors.push(`${field} must contain finite numbers`); + return null; + } + return [parsed[0], parsed[1], parsed[2]]; +} + +function clamp(value: number, min: number, max: number) { + if (!Number.isFinite(value)) return null; + return Math.min(max, Math.max(min, value)); +} + +function normalizeTransition(value: number | undefined) { + const numeric = Number(value); + if (!Number.isFinite(numeric) || numeric <= 0) return 0; + return numeric; +} + +function normalizeIndex(value: number | undefined) { + const index = Number(value); + if (!Number.isFinite(index) || index < 0) return 0; + return Math.floor(index); +} + +function ensureDataComponentEntry(object: THREE.Object3D, index: number): DataComponentEntry { + const list = Array.isArray((object as any).dataComponent) ? ((object as any).dataComponent as DataComponentEntry[]) : []; + if (!Array.isArray((object as any).dataComponent)) { + (object as any).dataComponent = list; + } + let entry = list[index]; + if (!entry || typeof entry !== "object") { + entry = { config: {}, data: undefined }; + list[index] = entry; + } else if (!entry.config || typeof entry.config !== "object") { + entry.config = {}; + } + return entry; +} + +function scheduleObjectUpdate(object: THREE.Object3D, onUpdate?: (object: THREE.Object3D) => void) { + if (!onUpdate) return; + if (pendingUpdates.get(object)) return; + pendingUpdates.set(object, true); + if (typeof requestAnimationFrame === "function") { + requestAnimationFrame(() => { + pendingUpdates.delete(object); + onUpdate(object); + }); + } else { + pendingUpdates.delete(object); + onUpdate(object); + } +} + +function getTweenBucket(object: THREE.Object3D, key: string) { + let map = activeTweens.get(object); + if (!map) { + map = new Map(); + activeTweens.set(object, map); + } + let bucket = map.get(key); + if (!bucket) { + bucket = new Set(); + map.set(key, bucket); + } + return bucket; +} + +function trackTween(object: THREE.Object3D, key: string, tween: Tween) { + const bucket = getTweenBucket(object, key); + bucket.add(tween); + const cleanup = () => { + const map = activeTweens.get(object); + const currentBucket = map?.get(key); + if (!currentBucket) return; + currentBucket.delete(tween); + if (currentBucket.size === 0) { + map?.delete(key); + } + if (map && map.size === 0) { + activeTweens.delete(object); + } + }; + tween.onStop(cleanup); + tween.onComplete(cleanup); +} + +function stopTweensForKey(object: THREE.Object3D, key: string) { + const map = activeTweens.get(object); + const bucket = map?.get(key); + if (!bucket) return; + bucket.forEach(tween => tween.stop()); + map?.delete(key); + if (map && map.size === 0) { + activeTweens.delete(object); + } +} + +function tweenVector3( + object: THREE.Object3D, + key: string, + target: THREE.Vector3, + value: Vector3Tuple, + duration: number, + onUpdate?: (object: THREE.Object3D) => void +) { + if (target.x === value[0] && target.y === value[1] && target.z === value[2]) { + return false; + } + const state = { x: target.x, y: target.y, z: target.z }; + const tween = new Tween(state) + .to({ x: value[0], y: value[1], z: value[2] }, duration) + .easing(Easing.Linear.None) + .onUpdate(() => { + target.set(state.x, state.y, state.z); + scheduleObjectUpdate(object, onUpdate); + }); + trackTween(object, key, tween); + tween.start(); + return true; +} + +function tweenEuler( + object: THREE.Object3D, + key: string, + target: THREE.Euler, + value: Vector3Tuple, + duration: number, + onUpdate?: (object: THREE.Object3D) => void +) { + if (target.x === value[0] && target.y === value[1] && target.z === value[2]) { + return false; + } + const state = { x: target.x, y: target.y, z: target.z }; + const tween = new Tween(state) + .to({ x: value[0], y: value[1], z: value[2] }, duration) + .easing(Easing.Linear.None) + .onUpdate(() => { + target.set(state.x, state.y, state.z); + scheduleObjectUpdate(object, onUpdate); + }); + trackTween(object, key, tween); + tween.start(); + return true; +} + +function tweenNumber( + object: THREE.Object3D, + key: string, + target: Record, + prop: string, + value: number, + duration: number, + onUpdate?: (object: THREE.Object3D) => void +) { + if (Number(target[prop]) === value) { + return false; + } + const state = { value: Number(target[prop]) }; + const tween = new Tween(state) + .to({ value }, duration) + .easing(Easing.Linear.None) + .onUpdate(() => { + target[prop] = state.value; + scheduleObjectUpdate(object, onUpdate); + }); + trackTween(object, key, tween); + tween.start(); + return true; +} + +function tweenColor( + object: THREE.Object3D, + key: string, + target: THREE.Color, + value: THREE.Color, + duration: number, + onUpdate?: (object: THREE.Object3D) => void +) { + if (target.equals(value)) { + return false; + } + const state = { r: target.r, g: target.g, b: target.b }; + const tween = new Tween(state) + .to({ r: value.r, g: value.g, b: value.b }, duration) + .easing(Easing.Linear.None) + .onUpdate(() => { + target.setRGB(state.r, state.g, state.b); + scheduleObjectUpdate(object, onUpdate); + }); + trackTween(object, key, tween); + tween.start(); + return true; +} + +function updateVector3(target: THREE.Vector3, value: Vector3Tuple) { + if (target.x === value[0] && target.y === value[1] && target.z === value[2]) { + return false; + } + target.set(value[0], value[1], value[2]); + return true; +} + +function updateEuler(target: THREE.Euler, value: Vector3Tuple) { + if (target.x === value[0] && target.y === value[1] && target.z === value[2]) { + return false; + } + target.set(value[0], value[1], value[2]); + return true; +} + +function updateMaterial( + object: THREE.Object3D, + material: THREE.Material, + data: AstralMaterialFormat, + errors: string[], + transition: number, + onUpdate?: (object: THREE.Object3D) => void +) { + let changed = false; + const duration = normalizeTransition(transition); + const mat = material as any; + const materialKey = material.uuid || "material"; + + if (data.color !== undefined && mat.color && typeof mat.color.set === "function") { + let nextColor: THREE.Color | null = null; + try { + nextColor = new THREE.Color(); + nextColor.set(data.color); + } catch (error) { + errors.push(`material.color invalid: ${String((error as Error).message || error)}`); + } + if (nextColor) { + const key = `material:${materialKey}:color`; + stopTweensForKey(object, key); + if (duration > 0) { + changed = tweenColor(object, key, mat.color, nextColor, duration, onUpdate) || changed; + } else if (typeof mat.color.equals !== "function" || !mat.color.equals(nextColor)) { + mat.color.copy(nextColor); + changed = true; + } + } + } + + if (data.emissive !== undefined && mat.emissive && typeof mat.emissive.set === "function") { + let nextEmissive: THREE.Color | null = null; + try { + nextEmissive = new THREE.Color(); + nextEmissive.set(data.emissive); + } catch (error) { + errors.push(`material.emissive invalid: ${String((error as Error).message || error)}`); + } + if (nextEmissive) { + const key = `material:${materialKey}:emissive`; + stopTweensForKey(object, key); + if (duration > 0) { + changed = tweenColor(object, key, mat.emissive, nextEmissive, duration, onUpdate) || changed; + } else if (typeof mat.emissive.equals !== "function" || !mat.emissive.equals(nextEmissive)) { + mat.emissive.copy(nextEmissive); + changed = true; + } + } + } + + if (data.opacity !== undefined && "opacity" in mat) { + const next = clamp(Number(data.opacity), 0, 1); + if (next === null) { + errors.push("material.opacity must be a number between 0 and 1"); + } else { + const key = `material:${materialKey}:opacity`; + stopTweensForKey(object, key); + if (duration > 0) { + changed = tweenNumber(object, key, mat, "opacity", next, duration, onUpdate) || changed; + } else if (mat.opacity !== next) { + mat.opacity = next; + changed = true; + } + } + } + + if (data.transparent !== undefined && "transparent" in mat) { + const next = Boolean(data.transparent); + const key = `material:${materialKey}:transparent`; + stopTweensForKey(object, key); + if (mat.transparent !== next) { + mat.transparent = next; + changed = true; + scheduleObjectUpdate(object, onUpdate); + } + } + + if (data.metalness !== undefined && "metalness" in mat) { + const next = clamp(Number(data.metalness), 0, 1); + if (next === null) { + errors.push("material.metalness must be a number between 0 and 1"); + } else { + const key = `material:${materialKey}:metalness`; + stopTweensForKey(object, key); + if (duration > 0) { + changed = tweenNumber(object, key, mat, "metalness", next, duration, onUpdate) || changed; + } else if (mat.metalness !== next) { + mat.metalness = next; + changed = true; + } + } + } + + if (data.roughness !== undefined && "roughness" in mat) { + const next = clamp(Number(data.roughness), 0, 1); + if (next === null) { + errors.push("material.roughness must be a number between 0 and 1"); + } else { + const key = `material:${materialKey}:roughness`; + stopTweensForKey(object, key); + if (duration > 0) { + changed = tweenNumber(object, key, mat, "roughness", next, duration, onUpdate) || changed; + } else if (mat.roughness !== next) { + mat.roughness = next; + changed = true; + } + } + } + + if (data.emissiveIntensity !== undefined && "emissiveIntensity" in mat) { + const next = clamp(Number(data.emissiveIntensity), 0, 1); + if (next === null) { + errors.push("material.emissiveIntensity must be a number between 0 and 1"); + } else { + const key = `material:${materialKey}:emissiveIntensity`; + stopTweensForKey(object, key); + if (duration > 0) { + changed = tweenNumber(object, key, mat, "emissiveIntensity", next, duration, onUpdate) || changed; + } else if (mat.emissiveIntensity !== next) { + mat.emissiveIntensity = next; + changed = true; + } + } + } + + if (changed && "needsUpdate" in mat) { + mat.needsUpdate = true; + } + + if (duration === 0 && changed) { + scheduleObjectUpdate(object, onUpdate); + } + + return changed; +} + +function getAnimationActionByName(object: THREE.Object3D, name: string) { + const animations = (object as any)?.animations; + if (!Array.isArray(animations)) return null; + + for (const animation of animations) { + if (!animation || typeof (animation as any).getClip !== "function") continue; + + const clip = (animation as THREE.AnimationAction).getClip?.(); + if (!clip || clip.name !== name) continue; + + if (typeof (animation as any).play === "function") { + return animation as THREE.AnimationAction; + } + } + + return null; +} + +function applyAnimations(object: THREE.Object3D, commands: AstralAnimationCommand[], errors: string[]) { + let changed = false; + + commands.forEach((command, index) => { + if (!isPlainObject(command)) { + errors.push(`animations[${index}] must be an object`); + return; + } + + const name = typeof command.name === "string" ? command.name.trim() : ""; + if (!name) { + errors.push(`animations[${index}].name is required`); + return; + } + + const behaviorValue = typeof command.behavior === "string" ? command.behavior.trim().toLowerCase() : ""; + if (!animationBehaviors.has(behaviorValue as AstralAnimationBehavior)) { + errors.push(`animations[${index}].behavior must be play | pause | stop`); + return; + } + + const action = getAnimationActionByName(object, name); + if (!action) return; + + if (command.duration !== undefined) { + const duration = Number(command.duration); + if (!Number.isFinite(duration) || duration <= 0) { + errors.push(`animations[${index}].duration must be a positive number`); + return; + } + if (typeof action.setDuration === "function") { + action.setDuration(duration); + } + } + + switch (behaviorValue) { + case "play": + action.play(); + action.paused = false; + break; + case "pause": + action.paused = true; + break; + case "stop": + action.stop(); + action.paused = false; + break; + } + + changed = true; + }); + + return changed; +} + +export class DataBindingManager { + static setConfig(object: THREE.Object3D, config: DataComponentConfig, options: SetConfigOptions = {}) { + const index = normalizeIndex(options.index); + const entry = ensureDataComponentEntry(object, index); + entry.config = config && typeof config === "object" ? { ...config } : {}; + return entry; + } + + static setData(object: THREE.Object3D, data: unknown, options: SetDataOptions = {}): DataBindingResult { + const index = normalizeIndex(options.index); + const entry = ensureDataComponentEntry(object, index); + entry.data = data; + + const config = entry.config || {}; + if (App.viewer) { + App.viewer.dispatchEvent({ + type: "bindDataChange", + object, + data, + config, + index, + }); + } + + const shouldApply = options.applyToModel ?? Boolean(config.applyToModel); + if (!shouldApply) { + return { changed: false, errors: [] }; + } + + const transition = + options.transition !== undefined ? options.transition : Number.isFinite(Number(config.transition)) ? Number(config.transition) : undefined; + + return DataBindingManager.applyDataToObject(object, data, { + transition, + onUpdate: options.onUpdate, + }); + } + + static applyDataToObject(object: THREE.Object3D, data: unknown, options: DataBindingOptions = {}): DataBindingResult { + const errors: string[] = []; + let changed = false; + + if (!isPlainObject(data)) { + return { changed: false, errors: ["data must be an object"] }; + } + + const payload = data as AstralDataFormat; + const transition = normalizeTransition(options.transition); + const onUpdate = options.onUpdate; + + if (payload.position !== undefined) { + const next = parseVector3(payload.position, "position", errors); + if (next) { + const key = "position"; + stopTweensForKey(object, key); + if (transition > 0) { + changed = tweenVector3(object, key, object.position, next, transition, onUpdate) || changed; + } else { + changed = updateVector3(object.position, next) || changed; + if (changed) { + scheduleObjectUpdate(object, onUpdate); + } + } + } + } + + if (payload.rotation !== undefined) { + const next = parseVector3(payload.rotation, "rotation", errors); + if (next) { + const key = "rotation"; + stopTweensForKey(object, key); + if (transition > 0) { + changed = tweenEuler(object, key, object.rotation, next, transition, onUpdate) || changed; + } else { + changed = updateEuler(object.rotation, next) || changed; + if (changed) { + scheduleObjectUpdate(object, onUpdate); + } + } + } + } + + if (payload.scale !== undefined) { + const next = parseVector3(payload.scale, "scale", errors); + if (next) { + const key = "scale"; + stopTweensForKey(object, key); + if (transition > 0) { + changed = tweenVector3(object, key, object.scale, next, transition, onUpdate) || changed; + } else { + changed = updateVector3(object.scale, next) || changed; + if (changed) { + scheduleObjectUpdate(object, onUpdate); + } + } + } + } + + if (payload.visible !== undefined) { + const next = Boolean(payload.visible); + const key = "visible"; + stopTweensForKey(object, key); + if (object.visible !== next) { + object.visible = next; + changed = true; + scheduleObjectUpdate(object, onUpdate); + } + } + + if (payload.material && isPlainObject(payload.material)) { + const targetMaterial = (object as any).material; + if (Array.isArray(targetMaterial)) { + targetMaterial.forEach((mat: THREE.Material) => { + changed = + updateMaterial(object, mat, payload.material as AstralMaterialFormat, errors, transition, onUpdate) || changed; + }); + } else if (targetMaterial) { + changed = + updateMaterial( + object, + targetMaterial as THREE.Material, + payload.material as AstralMaterialFormat, + errors, + transition, + onUpdate + ) || changed; + } + } + + if (payload.animations !== undefined) { + if (!Array.isArray(payload.animations)) { + errors.push("animations must be an array"); + } else { + changed = applyAnimations(object, payload.animations, errors) || changed; + } + } + + return { changed, errors }; + } +} diff --git a/packages/sdk/lib/core/tools/index.ts b/packages/sdk/lib/core/tools/index.ts index e452b7a..93cbcd6 100644 --- a/packages/sdk/lib/core/tools/index.ts +++ b/packages/sdk/lib/core/tools/index.ts @@ -5,3 +5,4 @@ export {ClippedEdgesBox} from "./ClippedEdgesBox"; export {Measure,MeasureMode} from "./Measure"; export {Export} from "./Export"; export {ModelExplode} from "./ModelExplode"; +export {DataBindingManager} from "./DataBindingManager"; diff --git a/packages/sdk/lib/core/viewer/Viewer.ts b/packages/sdk/lib/core/viewer/Viewer.ts index 41cf0f5..e8fcde1 100644 --- a/packages/sdk/lib/core/viewer/Viewer.ts +++ b/packages/sdk/lib/core/viewer/Viewer.ts @@ -61,6 +61,14 @@ export interface ViewerEventMap { // 模型双击事件 onDoubleClick: { intersect: THREE.Intersection, object: THREE.Object3D }; + // 数据组件绑定数据更新 + bindDataChange: { + object: THREE.Object3D; + data: unknown; + config: unknown; + index: number; + }; + // 键盘按下事件(全局) onKeyDown: { event: KeyboardEvent }; diff --git a/packages/sdk/types/app/Project.d.ts b/packages/sdk/types/app/Project.d.ts index 7982bc3..558c808 100644 --- a/packages/sdk/types/app/Project.d.ts +++ b/packages/sdk/types/app/Project.d.ts @@ -25,6 +25,39 @@ declare namespace IAppProject { interface Effect { enabled: boolean; + ToneMapping: { + mode: "LINEAR" | "REINHARD" | "REINHARD2" | "OPTIMIZED_CINEON" | "ACES_FILMIC" | "AGX" | "NEUTRAL"; + exposure: number; + blendFunction: string; + }; + SMAA: { + enabled: boolean; + preset: string; + }; + SSAO: { + enabled: boolean; + blendFunction: string; + samples: number; + rings: number; + radius: number; + intensity: number; + bias: number; + fade: number; + luminanceInfluence: number; + minRadiusScale: number; + depthAwareUpsampling: boolean; + resolutionScale: number; + distanceThreshold: number; + distanceFalloff: number; + rangeThreshold: number; + rangeFalloff: number; + worldDistanceThreshold: number | null; + worldDistanceFalloff: number | null; + worldProximityThreshold: number | null; + worldProximityFalloff: number | null; + colorEnabled: boolean; + color: string; + }; Outline: { enabled: boolean; edgeStrength: number; @@ -34,6 +67,12 @@ declare namespace IAppProject { usePatternTexture: boolean; visibleEdgeColor: string; hiddenEdgeColor: string; + pulseSpeed?: number; + xRay?: boolean; + blur?: boolean; + kernelSize?: number; + multisampling?: number; + blendFunction?: string; }; FXAA: { enabled: boolean; @@ -43,18 +82,98 @@ declare namespace IAppProject { threshold: number; strength: number; radius: number; + intensity?: number; + luminanceThreshold?: number; + luminanceSmoothing?: number; + levels?: number; + mipmapBlur?: boolean; + blendFunction?: string; }; Bokeh: { enabled: boolean; focus: number; aperture: number; maxblur: number; + focusDistance?: number; + focusRange?: number; + bokehScale?: number; + resolutionScale?: number; }; Pixelate: { enabled: boolean; pixelSize: number; normalEdgeStrength: number; depthEdgeStrength: number; + granularity?: number; + }; + TiltShift?: { + enabled: boolean; + offset: number; + rotation: number; + focusArea: number; + feather: number; + blendFunction: string; + }; + Scanline?: { + enabled: boolean; + density: number; + scrollSpeed: number; + blendFunction: string; + }; + BrightnessContrast?: { + enabled: boolean; + brightness: number; + contrast: number; + blendFunction: string; + }; + ChromaticAberration?: { + enabled: boolean; + offset: { x: number; y: number }; + radialModulation: boolean; + modulationOffset: number; + blendFunction: string; + }; + ColorDepth?: { + enabled: boolean; + bits: number; + blendFunction: string; + }; + Glitch?: { + enabled: boolean; + chromaticAberrationOffset: { x: number; y: number } | null; + delay: { min: number; max: number }; + duration: { min: number; max: number }; + strength: { min: number; max: number }; + mode: "SPORADIC" | "CONSTANT_MILD" | "CONSTANT_WILD"; + ratio: number; + blendFunction: string; + }; + HueSaturation?: { + enabled: boolean; + hue: number; + saturation: number; + blendFunction: string; + }; + LensDistortion?: { + enabled: boolean; + distortion: { x: number; y: number }; + principalPoint: { x: number; y: number }; + focalLength: { x: number; y: number }; + skew: number; + }; + ShockWave?: { + enabled: boolean; + amplitude: number; + waveSize: number; + speed: number; + maxRadius: number; + clickTrigger: boolean; + }; + Vignette?: { + enabled: boolean; + offset: number; + darkness: number; + blendFunction: string; }; Halftone: { enabled: boolean;