@kiberon-labs/behave-graph-scene 1.0.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/.storybook/main.ts +18 -0
  2. package/.storybook/preview.ts +16 -0
  3. package/.storybook/vscode.css +822 -0
  4. package/.turbo/turbo-build.log +4 -3
  5. package/CHANGELOG.md +84 -0
  6. package/README.md +1 -1
  7. package/dist/Abstractions/Drivers/DummyScene.d.ts +47 -3
  8. package/dist/Abstractions/Drivers/DummyScene.d.ts.map +1 -1
  9. package/dist/Abstractions/Drivers/DummyScene.js +57 -0
  10. package/dist/Abstractions/Drivers/DummyScene.js.map +1 -1
  11. package/dist/Abstractions/IScene.d.ts +63 -4
  12. package/dist/Abstractions/IScene.d.ts.map +1 -1
  13. package/dist/Abstractions/IScene.js +19 -0
  14. package/dist/Abstractions/IScene.js.map +1 -0
  15. package/dist/Nodes/Actions/AddLight.d.ts +21 -0
  16. package/dist/Nodes/Actions/AddLight.d.ts.map +1 -0
  17. package/dist/Nodes/Actions/AddLight.js +38 -0
  18. package/dist/Nodes/Actions/AddLight.js.map +1 -0
  19. package/dist/Nodes/Actions/CloneMesh.d.ts +16 -0
  20. package/dist/Nodes/Actions/CloneMesh.d.ts.map +1 -0
  21. package/dist/Nodes/Actions/CloneMesh.js +28 -0
  22. package/dist/Nodes/Actions/CloneMesh.js.map +1 -0
  23. package/dist/Nodes/Actions/CreateMesh.d.ts +22 -0
  24. package/dist/Nodes/Actions/CreateMesh.d.ts.map +1 -0
  25. package/dist/Nodes/Actions/CreateMesh.js +44 -0
  26. package/dist/Nodes/Actions/CreateMesh.js.map +1 -0
  27. package/dist/Nodes/Actions/DeleteMesh.d.ts +15 -0
  28. package/dist/Nodes/Actions/DeleteMesh.d.ts.map +1 -0
  29. package/dist/Nodes/Actions/DeleteMesh.js +27 -0
  30. package/dist/Nodes/Actions/DeleteMesh.js.map +1 -0
  31. package/dist/Nodes/Actions/EaseSceneProperty.d.ts +1 -1
  32. package/dist/Nodes/Actions/EaseSceneProperty.d.ts.map +1 -1
  33. package/dist/Nodes/Actions/EaseSceneProperty.js +2 -2
  34. package/dist/Nodes/Actions/EaseSceneProperty.js.map +1 -1
  35. package/dist/Nodes/Actions/LookAt.d.ts +16 -0
  36. package/dist/Nodes/Actions/LookAt.d.ts.map +1 -0
  37. package/dist/Nodes/Actions/LookAt.js +35 -0
  38. package/dist/Nodes/Actions/LookAt.js.map +1 -0
  39. package/dist/Nodes/Actions/MoveTowards.d.ts +21 -0
  40. package/dist/Nodes/Actions/MoveTowards.d.ts.map +1 -0
  41. package/dist/Nodes/Actions/MoveTowards.js +46 -0
  42. package/dist/Nodes/Actions/MoveTowards.js.map +1 -0
  43. package/dist/Nodes/Actions/RemoveLight.d.ts +15 -0
  44. package/dist/Nodes/Actions/RemoveLight.d.ts.map +1 -0
  45. package/dist/Nodes/Actions/RemoveLight.js +27 -0
  46. package/dist/Nodes/Actions/RemoveLight.js.map +1 -0
  47. package/dist/Nodes/Actions/SetLightProperty.d.ts +25 -0
  48. package/dist/Nodes/Actions/SetLightProperty.d.ts.map +1 -0
  49. package/dist/Nodes/Actions/SetLightProperty.js +64 -0
  50. package/dist/Nodes/Actions/SetLightProperty.js.map +1 -0
  51. package/dist/Nodes/Actions/SetMaterialProperty.d.ts +25 -0
  52. package/dist/Nodes/Actions/SetMaterialProperty.d.ts.map +1 -0
  53. package/dist/Nodes/Actions/SetMaterialProperty.js +69 -0
  54. package/dist/Nodes/Actions/SetMaterialProperty.js.map +1 -0
  55. package/dist/Nodes/Actions/SetMeshPosition.d.ts +18 -0
  56. package/dist/Nodes/Actions/SetMeshPosition.d.ts.map +1 -0
  57. package/dist/Nodes/Actions/SetMeshPosition.js +34 -0
  58. package/dist/Nodes/Actions/SetMeshPosition.js.map +1 -0
  59. package/dist/Nodes/Actions/SetMeshVisible.d.ts +16 -0
  60. package/dist/Nodes/Actions/SetMeshVisible.d.ts.map +1 -0
  61. package/dist/Nodes/Actions/SetMeshVisible.js +28 -0
  62. package/dist/Nodes/Actions/SetMeshVisible.js.map +1 -0
  63. package/dist/Nodes/Actions/SetSceneProperty.d.ts +12 -1
  64. package/dist/Nodes/Actions/SetSceneProperty.d.ts.map +1 -1
  65. package/dist/Nodes/Actions/SetSceneProperty.js +1 -1
  66. package/dist/Nodes/Actions/SetSceneProperty.js.map +1 -1
  67. package/dist/Nodes/Events/OnAnyMeshClicked.d.ts +13 -0
  68. package/dist/Nodes/Events/OnAnyMeshClicked.d.ts.map +1 -0
  69. package/dist/Nodes/Events/OnAnyMeshClicked.js +34 -0
  70. package/dist/Nodes/Events/OnAnyMeshClicked.js.map +1 -0
  71. package/dist/Nodes/Events/OnSceneChanged.d.ts +12 -0
  72. package/dist/Nodes/Events/OnSceneChanged.d.ts.map +1 -0
  73. package/dist/Nodes/Events/OnSceneChanged.js +28 -0
  74. package/dist/Nodes/Events/OnSceneChanged.js.map +1 -0
  75. package/dist/Nodes/Events/OnSceneNodeClick.d.ts +14 -1
  76. package/dist/Nodes/Events/OnSceneNodeClick.d.ts.map +1 -1
  77. package/dist/Nodes/Events/OnSceneNodeClick.js +1 -1
  78. package/dist/Nodes/Events/OnSceneNodeClick.js.map +1 -1
  79. package/dist/Nodes/Logic/ColorNodes.d.ts +17 -13
  80. package/dist/Nodes/Logic/ColorNodes.d.ts.map +1 -1
  81. package/dist/Nodes/Logic/EulerNodes.d.ts +16 -12
  82. package/dist/Nodes/Logic/EulerNodes.d.ts.map +1 -1
  83. package/dist/Nodes/Logic/Mat3Nodes.d.ts +26 -23
  84. package/dist/Nodes/Logic/Mat3Nodes.d.ts.map +1 -1
  85. package/dist/Nodes/Logic/Mat3Nodes.js +0 -14
  86. package/dist/Nodes/Logic/Mat3Nodes.js.map +1 -1
  87. package/dist/Nodes/Logic/Mat4Nodes.d.ts +32 -28
  88. package/dist/Nodes/Logic/Mat4Nodes.d.ts.map +1 -1
  89. package/dist/Nodes/Logic/QuatNodes.d.ts +22 -18
  90. package/dist/Nodes/Logic/QuatNodes.d.ts.map +1 -1
  91. package/dist/Nodes/Logic/Vec2Nodes.d.ts +16 -12
  92. package/dist/Nodes/Logic/Vec2Nodes.d.ts.map +1 -1
  93. package/dist/Nodes/Logic/Vec3Nodes.d.ts +17 -13
  94. package/dist/Nodes/Logic/Vec3Nodes.d.ts.map +1 -1
  95. package/dist/Nodes/Logic/Vec4Nodes.d.ts +16 -12
  96. package/dist/Nodes/Logic/Vec4Nodes.d.ts.map +1 -1
  97. package/dist/Nodes/Queries/GetDistanceBetween.d.ts +18 -0
  98. package/dist/Nodes/Queries/GetDistanceBetween.d.ts.map +1 -0
  99. package/dist/Nodes/Queries/GetDistanceBetween.js +30 -0
  100. package/dist/Nodes/Queries/GetDistanceBetween.js.map +1 -0
  101. package/dist/Nodes/Queries/GetLightProperty.d.ts +23 -0
  102. package/dist/Nodes/Queries/GetLightProperty.d.ts.map +1 -0
  103. package/dist/Nodes/Queries/GetLightProperty.js +68 -0
  104. package/dist/Nodes/Queries/GetLightProperty.js.map +1 -0
  105. package/dist/Nodes/Queries/GetMaterialProperty.d.ts +23 -0
  106. package/dist/Nodes/Queries/GetMaterialProperty.d.ts.map +1 -0
  107. package/dist/Nodes/Queries/GetMaterialProperty.js +69 -0
  108. package/dist/Nodes/Queries/GetMaterialProperty.js.map +1 -0
  109. package/dist/Nodes/Queries/GetMeshPosition.d.ts +16 -0
  110. package/dist/Nodes/Queries/GetMeshPosition.d.ts.map +1 -0
  111. package/dist/Nodes/Queries/GetMeshPosition.js +29 -0
  112. package/dist/Nodes/Queries/GetMeshPosition.js.map +1 -0
  113. package/dist/Nodes/Queries/GetSceneProperty.d.ts +10 -1
  114. package/dist/Nodes/Queries/GetSceneProperty.d.ts.map +1 -1
  115. package/dist/Nodes/Queries/GetSceneProperty.js +2 -2
  116. package/dist/Nodes/Queries/GetSceneProperty.js.map +1 -1
  117. package/dist/Values/Internal/Mat3.d.ts +1 -1
  118. package/dist/Values/Internal/Mat3.d.ts.map +1 -1
  119. package/dist/Values/Internal/Mat3.js +1 -3
  120. package/dist/Values/Internal/Mat3.js.map +1 -1
  121. package/dist/Values/Internal/Mat4.d.ts.map +1 -1
  122. package/dist/Values/Internal/Mat4.js +1 -3
  123. package/dist/Values/Internal/Mat4.js.map +1 -1
  124. package/dist/Values/Internal/Vec3.d.ts +1 -1
  125. package/dist/Values/Internal/Vec3.d.ts.map +1 -1
  126. package/dist/Values/Internal/Vec3.js +0 -2
  127. package/dist/Values/Internal/Vec3.js.map +1 -1
  128. package/dist/Values/Internal/Vec4.d.ts.map +1 -1
  129. package/dist/Values/Internal/Vec4.js +0 -1
  130. package/dist/Values/Internal/Vec4.js.map +1 -1
  131. package/dist/_virtual/rolldown_runtime.js +23 -1
  132. package/dist/behave-graph.manifest.json +6082 -0
  133. package/dist/buildScene.d.ts.map +1 -1
  134. package/dist/buildScene.js +24 -3
  135. package/dist/buildScene.js.map +1 -1
  136. package/dist/index.d.ts +29 -3
  137. package/dist/index.d.ts.map +1 -0
  138. package/dist/index.js +22 -4
  139. package/dist/manifest.source.d.ts +7 -0
  140. package/dist/manifest.source.d.ts.map +1 -0
  141. package/dist/manifest.source.js +54 -0
  142. package/dist/manifest.source.js.map +1 -0
  143. package/dist/node_modules/.pnpm/react@19.2.3/node_modules/react/cjs/react-jsx-runtime.development.js +207 -0
  144. package/dist/node_modules/.pnpm/react@19.2.3/node_modules/react/cjs/react-jsx-runtime.development.js.map +1 -0
  145. package/dist/node_modules/.pnpm/react@19.2.3/node_modules/react/cjs/react-jsx-runtime.production.js +40 -0
  146. package/dist/node_modules/.pnpm/react@19.2.3/node_modules/react/cjs/react-jsx-runtime.production.js.map +1 -0
  147. package/dist/node_modules/.pnpm/react@19.2.3/node_modules/react/cjs/react.development.js +766 -0
  148. package/dist/node_modules/.pnpm/react@19.2.3/node_modules/react/cjs/react.development.js.map +1 -0
  149. package/dist/node_modules/.pnpm/react@19.2.3/node_modules/react/cjs/react.production.js +367 -0
  150. package/dist/node_modules/.pnpm/react@19.2.3/node_modules/react/cjs/react.production.js.map +1 -0
  151. package/dist/node_modules/.pnpm/react@19.2.3/node_modules/react/index.js +15 -0
  152. package/dist/node_modules/.pnpm/react@19.2.3/node_modules/react/index.js.map +1 -0
  153. package/dist/node_modules/.pnpm/react@19.2.3/node_modules/react/jsx-runtime.js +15 -0
  154. package/dist/node_modules/.pnpm/react@19.2.3/node_modules/react/jsx-runtime.js.map +1 -0
  155. package/dist/packages/nodes/scene/package.js +7 -0
  156. package/dist/packages/nodes/scene/package.js.map +1 -0
  157. package/dist/registerSceneProfile.d.ts +3 -3
  158. package/dist/registerSceneProfile.d.ts.map +1 -1
  159. package/dist/registerSceneProfile.js +35 -1
  160. package/dist/registerSceneProfile.js.map +1 -1
  161. package/dist/ui/controls/vec3.d.ts +9 -0
  162. package/dist/ui/controls/vec3.d.ts.map +1 -0
  163. package/dist/ui/controls/vec3.js +99 -0
  164. package/dist/ui/controls/vec3.js.map +1 -0
  165. package/package.json +28 -9
  166. package/src/Abstractions/Drivers/DummyScene.ts +110 -2
  167. package/src/Abstractions/IScene.ts +74 -3
  168. package/src/Nodes/Actions/AddLight.ts +46 -0
  169. package/src/Nodes/Actions/CloneMesh.ts +31 -0
  170. package/src/Nodes/Actions/CreateMesh.ts +47 -0
  171. package/src/Nodes/Actions/DeleteMesh.ts +29 -0
  172. package/src/Nodes/Actions/EaseSceneProperty.ts +6 -2
  173. package/src/Nodes/Actions/LookAt.ts +34 -0
  174. package/src/Nodes/Actions/MoveTowards.ts +55 -0
  175. package/src/Nodes/Actions/RemoveLight.ts +29 -0
  176. package/src/Nodes/Actions/SetLightProperty.ts +60 -0
  177. package/src/Nodes/Actions/SetMaterialProperty.ts +62 -0
  178. package/src/Nodes/Actions/SetMeshPosition.ts +37 -0
  179. package/src/Nodes/Actions/SetMeshVisible.ts +31 -0
  180. package/src/Nodes/Actions/SetSceneProperty.ts +3 -5
  181. package/src/Nodes/Events/OnAnyMeshClicked.ts +48 -0
  182. package/src/Nodes/Events/OnSceneChanged.ts +43 -0
  183. package/src/Nodes/Events/OnSceneNodeClick.ts +3 -3
  184. package/src/Nodes/Logic/Mat3Nodes.ts +0 -10
  185. package/src/Nodes/Queries/GetDistanceBetween.ts +37 -0
  186. package/src/Nodes/Queries/GetLightProperty.ts +53 -0
  187. package/src/Nodes/Queries/GetMaterialProperty.ts +55 -0
  188. package/src/Nodes/Queries/GetMeshPosition.ts +32 -0
  189. package/src/Nodes/Queries/GetSceneProperty.ts +4 -5
  190. package/src/Values/Internal/Mat3.ts +3 -3
  191. package/src/Values/Internal/Mat4.ts +5 -4
  192. package/src/Values/Internal/Vec3.ts +5 -4
  193. package/src/Values/Internal/Vec4.ts +3 -2
  194. package/src/buildScene.ts +36 -2
  195. package/src/index.ts +26 -2
  196. package/src/manifest.source.ts +61 -0
  197. package/src/registerSceneProfile.ts +41 -4
  198. package/src/ui/controls/vec3.tsx +69 -0
  199. package/stories/click.stories.tsx +112 -0
  200. package/stories/components/DemoScene.ts +610 -0
  201. package/stories/components/SceneViewer.tsx +204 -0
  202. package/stories/components/SceneViewerPanel.tsx +41 -0
  203. package/stories/data/clickDemo.json +94 -0
  204. package/stories/data/rotate.json +402 -0
  205. package/stories/index.stories.tsx +90 -0
  206. package/stories/plugin/sceneViewerPlugin.tsx +88 -0
  207. package/tests/manifest.test.ts +65 -0
  208. package/tests/readSceneGraphs.test.ts +8 -1
  209. package/tests/registerSceneProfile.test.ts +6 -5
  210. package/tsconfig.json +18 -11
  211. package/tsdown.config.ts +5 -1
  212. package/vite.config.js +7 -0
  213. package/src/Values/Internal/Mat2.ts +0 -214
  214. package/src/loadScene.ts +0 -81
@@ -0,0 +1,610 @@
1
+ import {
2
+ Scene,
3
+ Mesh,
4
+ BoxGeometry,
5
+ MeshPhongMaterial,
6
+ TorusKnotGeometry,
7
+ SphereGeometry,
8
+ CylinderGeometry,
9
+ PlaneGeometry,
10
+ ConeGeometry,
11
+ TorusGeometry,
12
+ PointLight,
13
+ DirectionalLight,
14
+ SpotLight,
15
+ AmbientLight,
16
+ type Light,
17
+ Vector3,
18
+ Color
19
+ } from 'three';
20
+ import { EventEmitter } from '@kiberon-labs/behave-graph';
21
+ import type { IScene } from '@/Abstractions/IScene';
22
+ import type { ChoiceJSON } from '@kiberon-labs/behave-graph';
23
+
24
+ // Copies whichever of x/y/z are present on the incoming value onto a target
25
+ // Vector3/Euler-like object, leaving unspecified axes untouched.
26
+ function assignVectorComponents(
27
+ target: { x: number; y: number; z: number },
28
+ value: any
29
+ ): void {
30
+ for (const axis of ['x', 'y', 'z'] as const) {
31
+ if (value[axis] !== undefined) target[axis] = value[axis];
32
+ }
33
+ }
34
+
35
+ export class DemoScene implements IScene {
36
+ public readonly scene: Scene;
37
+ public readonly onSceneChanged = new EventEmitter<void>();
38
+
39
+ private objects: Map<string, Mesh>;
40
+ private lights: Map<string, Light>;
41
+ private clickListeners: Map<string, Set<(jsonPath: string) => void>>;
42
+ private anyMeshClickListeners: Set<(meshName: string) => void>;
43
+ private sceneChangedListeners: Set<() => void>;
44
+
45
+ constructor() {
46
+ this.scene = new Scene();
47
+ this.objects = new Map();
48
+ this.lights = new Map();
49
+ this.clickListeners = new Map();
50
+ this.anyMeshClickListeners = new Set();
51
+ this.sceneChangedListeners = new Set();
52
+
53
+ this.initializeScene();
54
+ }
55
+
56
+ private initializeScene() {
57
+ // Create cube
58
+ const cubeGeometry = new BoxGeometry(2, 2, 2);
59
+ const cubeMaterial = new MeshPhongMaterial({
60
+ color: 0x2194ce,
61
+ shininess: 100
62
+ });
63
+ const cube = new Mesh(cubeGeometry, cubeMaterial);
64
+ cube.position.set(0, 1, 0);
65
+ cube.name = 'cube';
66
+ this.scene.add(cube);
67
+ this.objects.set('cube', cube);
68
+
69
+ // Create torus knot
70
+ const torusGeometry = new TorusKnotGeometry(0.8, 0.3, 100, 16);
71
+ const torusMaterial = new MeshPhongMaterial({
72
+ color: 0xce2194,
73
+ shininess: 100
74
+ });
75
+ const torus = new Mesh(torusGeometry, torusMaterial);
76
+ torus.position.set(4, 2, 2);
77
+ torus.name = 'torus';
78
+ this.scene.add(torus);
79
+ this.objects.set('torus', torus);
80
+
81
+ // Create sphere
82
+ const sphereGeometry = new SphereGeometry(1, 32, 32);
83
+ const sphereMaterial = new MeshPhongMaterial({
84
+ color: 0x21ce94,
85
+ shininess: 100
86
+ });
87
+ const sphere = new Mesh(sphereGeometry, sphereMaterial);
88
+ sphere.position.set(-4, 1, -2);
89
+ sphere.name = 'sphere';
90
+ this.scene.add(sphere);
91
+ this.objects.set('sphere', sphere);
92
+
93
+ // Create cylinder
94
+ const cylinderGeometry = new CylinderGeometry(0.7, 0.7, 3, 32);
95
+ const cylinderMaterial = new MeshPhongMaterial({
96
+ color: 0xce9421,
97
+ shininess: 100
98
+ });
99
+ const cylinder = new Mesh(cylinderGeometry, cylinderMaterial);
100
+ cylinder.position.set(-2, 1.5, 3);
101
+ cylinder.name = 'cylinder';
102
+ this.scene.add(cylinder);
103
+ this.objects.set('cylinder', cylinder);
104
+
105
+ // Create a default directional light
106
+ const defaultLight = new DirectionalLight(0xffffff, 1);
107
+ defaultLight.position.set(5, 10, 7);
108
+ defaultLight.name = 'defaultLight';
109
+ this.scene.add(defaultLight);
110
+ this.lights.set('defaultLight', defaultLight);
111
+
112
+ // Create a default ambient light
113
+ const ambientLight = new AmbientLight(0x404040, 0.5);
114
+ ambientLight.name = 'ambientLight';
115
+ this.scene.add(ambientLight);
116
+ this.lights.set('ambientLight', ambientLight);
117
+ }
118
+
119
+ getProperty(jsonPath: string): any {
120
+ const parts = jsonPath.split('/');
121
+ const objectName = parts[0]!;
122
+ const propertyName = parts[1];
123
+
124
+ const obj = this.objects.get(objectName);
125
+ if (!obj) return undefined;
126
+
127
+ switch (propertyName) {
128
+ case 'position':
129
+ return { x: obj.position.x, y: obj.position.y, z: obj.position.z };
130
+ case 'rotation':
131
+ return { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z };
132
+ case 'scale':
133
+ return { x: obj.scale.x, y: obj.scale.y, z: obj.scale.z };
134
+ case 'visible':
135
+ return obj.visible;
136
+ case 'color':
137
+ if (obj.material instanceof MeshPhongMaterial) {
138
+ const color = obj.material.color;
139
+ return { r: color.r, g: color.g, b: color.b };
140
+ }
141
+ return undefined;
142
+ default:
143
+ return undefined;
144
+ }
145
+ }
146
+
147
+ setProperty(jsonPath: string, valueTypeName: string, value: any): void {
148
+ const parts = jsonPath.split('/');
149
+ const objectName = parts[0]!;
150
+ const propertyName = parts[1];
151
+
152
+ const obj = this.objects.get(objectName);
153
+ if (!obj) return;
154
+
155
+ switch (propertyName) {
156
+ case 'position':
157
+ assignVectorComponents(obj.position, value);
158
+ break;
159
+ case 'rotation':
160
+ assignVectorComponents(obj.rotation, value);
161
+ break;
162
+ case 'scale':
163
+ assignVectorComponents(obj.scale, value);
164
+ break;
165
+ case 'visible':
166
+ obj.visible = Boolean(value);
167
+ break;
168
+ case 'color':
169
+ if (obj.material instanceof MeshPhongMaterial) {
170
+ obj.material.color.setRGB(value.x || 0, value.y || 0, value.z || 0);
171
+ }
172
+ break;
173
+ }
174
+
175
+ this.onSceneChanged.emit();
176
+ this.sceneChangedListeners.forEach((listener) => listener());
177
+ }
178
+
179
+ addOnClickedListener(
180
+ jsonPath: string,
181
+ callback: (jsonPath: string) => void
182
+ ): void {
183
+ if (!this.clickListeners.has(jsonPath)) {
184
+ this.clickListeners.set(jsonPath, new Set());
185
+ }
186
+ this.clickListeners.get(jsonPath)!.add(callback);
187
+ }
188
+
189
+ removeOnClickedListener(
190
+ jsonPath: string,
191
+ callback: (jsonPath: string) => void
192
+ ): void {
193
+ const listeners = this.clickListeners.get(jsonPath);
194
+ if (listeners) {
195
+ listeners.delete(callback);
196
+ }
197
+ }
198
+
199
+ getRaycastableProperties(): ChoiceJSON {
200
+ const choices: ChoiceJSON = [];
201
+ this.objects.forEach((obj, name) => {
202
+ choices.push({
203
+ text: name,
204
+ value: name
205
+ });
206
+ });
207
+ return choices;
208
+ }
209
+
210
+ getProperties(valueFilter?: string): ChoiceJSON {
211
+ const choices: ChoiceJSON = [];
212
+ this.objects.forEach((obj, name) => {
213
+ if (valueFilter == 'euler' || valueFilter == 'vec3') {
214
+ choices.push({ text: `${name}/position`, value: `${name}/position` });
215
+ choices.push({ text: `${name}/rotation`, value: `${name}/rotation` });
216
+ choices.push({ text: `${name}/scale`, value: `${name}/scale` });
217
+ }
218
+ if (valueFilter === 'boolean') {
219
+ choices.push({ text: `${name}/visible`, value: `${name}/visible` });
220
+ }
221
+ if (valueFilter === 'color') {
222
+ choices.push({ text: `${name}/color`, value: `${name}/color` });
223
+ }
224
+ });
225
+ return choices;
226
+ }
227
+
228
+ addOnSceneChangedListener(listener: () => void): void {
229
+ this.sceneChangedListeners.add(listener);
230
+ }
231
+
232
+ removeOnSceneChangedListener(listener: () => void): void {
233
+ this.sceneChangedListeners.delete(listener);
234
+ }
235
+
236
+ // Trigger a click event (for testing)
237
+ triggerClick(jsonPath: string): void {
238
+ const listeners = this.clickListeners.get(jsonPath);
239
+ if (listeners) {
240
+ listeners.forEach((callback) => callback(jsonPath));
241
+ }
242
+ }
243
+
244
+ // --- mesh lifecycle ---
245
+
246
+ private createGeometry(
247
+ geometryType: string,
248
+ size: { x: number; y: number; z: number }
249
+ ) {
250
+ switch (geometryType) {
251
+ case 'box':
252
+ return new BoxGeometry(size.x, size.y, size.z);
253
+ case 'sphere':
254
+ return new SphereGeometry(size.x / 2, 32, 32);
255
+ case 'cylinder':
256
+ return new CylinderGeometry(size.x / 2, size.x / 2, size.y, 32);
257
+ case 'torus':
258
+ return new TorusGeometry(size.x / 2, size.y / 4, 16, 48);
259
+ case 'plane':
260
+ return new PlaneGeometry(size.x, size.y);
261
+ case 'cone':
262
+ return new ConeGeometry(size.x / 2, size.y, 32);
263
+ default:
264
+ return new BoxGeometry(size.x, size.y, size.z);
265
+ }
266
+ }
267
+
268
+ createMesh(
269
+ name: string,
270
+ geometryType: string,
271
+ size: { x: number; y: number; z: number }
272
+ ): void {
273
+ // Remove existing mesh with the same name
274
+ if (this.objects.has(name)) {
275
+ this.deleteMesh(name);
276
+ }
277
+
278
+ const geometry = this.createGeometry(geometryType, size);
279
+ const material = new MeshPhongMaterial({
280
+ color: 0xcccccc,
281
+ shininess: 100
282
+ });
283
+ const mesh = new Mesh(geometry, material);
284
+ mesh.name = name;
285
+ this.scene.add(mesh);
286
+ this.objects.set(name, mesh);
287
+
288
+ this.onSceneChanged.emit();
289
+ this.sceneChangedListeners.forEach((listener) => listener());
290
+ }
291
+
292
+ deleteMesh(name: string): void {
293
+ const obj = this.objects.get(name);
294
+ if (!obj) return;
295
+
296
+ this.scene.remove(obj);
297
+ obj.geometry.dispose();
298
+ if (obj.material instanceof MeshPhongMaterial) {
299
+ obj.material.dispose();
300
+ }
301
+ this.objects.delete(name);
302
+
303
+ this.onSceneChanged.emit();
304
+ this.sceneChangedListeners.forEach((listener) => listener());
305
+ }
306
+
307
+ getMeshNames(): ChoiceJSON {
308
+ const choices: ChoiceJSON = [];
309
+ this.objects.forEach((_obj, name) => {
310
+ choices.push({ text: name, value: name });
311
+ });
312
+ return choices;
313
+ }
314
+
315
+ // --- lighting ---
316
+
317
+ private createLight(
318
+ lightType: string,
319
+ color: { r: number; g: number; b: number },
320
+ intensity: number
321
+ ): Light {
322
+ const threeColor = new Color(color.r, color.g, color.b);
323
+
324
+ switch (lightType) {
325
+ case 'point': {
326
+ const light = new PointLight(threeColor, intensity);
327
+ light.position.set(0, 5, 0);
328
+ return light;
329
+ }
330
+ case 'directional': {
331
+ const light = new DirectionalLight(threeColor, intensity);
332
+ light.position.set(5, 10, 7);
333
+ return light;
334
+ }
335
+ case 'spot': {
336
+ const light = new SpotLight(threeColor, intensity);
337
+ light.position.set(0, 10, 0);
338
+ return light;
339
+ }
340
+ case 'ambient':
341
+ return new AmbientLight(threeColor, intensity);
342
+ default:
343
+ return new PointLight(threeColor, intensity);
344
+ }
345
+ }
346
+
347
+ addLight(
348
+ name: string,
349
+ lightType: string,
350
+ color: { r: number; g: number; b: number },
351
+ intensity: number
352
+ ): void {
353
+ // Remove existing light with the same name
354
+ if (this.lights.has(name)) {
355
+ this.removeLight(name);
356
+ }
357
+
358
+ const light = this.createLight(lightType, color, intensity);
359
+ light.name = name;
360
+ this.scene.add(light);
361
+ this.lights.set(name, light);
362
+
363
+ this.onSceneChanged.emit();
364
+ this.sceneChangedListeners.forEach((listener) => listener());
365
+ }
366
+
367
+ removeLight(name: string): void {
368
+ const light = this.lights.get(name);
369
+ if (!light) return;
370
+
371
+ this.scene.remove(light);
372
+ light.dispose();
373
+ this.lights.delete(name);
374
+
375
+ this.onSceneChanged.emit();
376
+ this.sceneChangedListeners.forEach((listener) => listener());
377
+ }
378
+
379
+ setLightProperty(name: string, property: string, value: unknown): void {
380
+ const light = this.lights.get(name);
381
+ if (!light) return;
382
+
383
+ switch (property) {
384
+ case 'color': {
385
+ const c = value as { r: number; g: number; b: number };
386
+ light.color.setRGB(c.r, c.g, c.b);
387
+ break;
388
+ }
389
+ case 'intensity':
390
+ light.intensity = value as number;
391
+ break;
392
+ case 'position': {
393
+ const p = value as { x: number; y: number; z: number };
394
+ light.position.set(p.x, p.y, p.z);
395
+ break;
396
+ }
397
+ }
398
+
399
+ this.onSceneChanged.emit();
400
+ this.sceneChangedListeners.forEach((listener) => listener());
401
+ }
402
+
403
+ getLightProperty(name: string, property: string): unknown {
404
+ const light = this.lights.get(name);
405
+ if (!light) return undefined;
406
+
407
+ switch (property) {
408
+ case 'color':
409
+ return {
410
+ r: light.color.r,
411
+ g: light.color.g,
412
+ b: light.color.b
413
+ };
414
+ case 'intensity':
415
+ return light.intensity;
416
+ case 'position':
417
+ return {
418
+ x: light.position.x,
419
+ y: light.position.y,
420
+ z: light.position.z
421
+ };
422
+ default:
423
+ return undefined;
424
+ }
425
+ }
426
+
427
+ getLightNames(): ChoiceJSON {
428
+ const choices: ChoiceJSON = [];
429
+ this.lights.forEach((_light, name) => {
430
+ choices.push({ text: name, value: name });
431
+ });
432
+ return choices;
433
+ }
434
+
435
+ // --- material ---
436
+
437
+ setMaterialProperty(
438
+ meshName: string,
439
+ property: string,
440
+ value: unknown
441
+ ): void {
442
+ const obj = this.objects.get(meshName);
443
+ if (!obj) return;
444
+ if (!(obj.material instanceof MeshPhongMaterial)) return;
445
+
446
+ const mat = obj.material;
447
+
448
+ switch (property) {
449
+ case 'color': {
450
+ const c = value as { r: number; g: number; b: number };
451
+ mat.color.setRGB(c.r, c.g, c.b);
452
+ break;
453
+ }
454
+ case 'opacity':
455
+ mat.opacity = value as number;
456
+ mat.transparent = mat.opacity < 1;
457
+ break;
458
+ case 'visible':
459
+ mat.visible = value as boolean;
460
+ break;
461
+ case 'wireframe':
462
+ mat.wireframe = value as boolean;
463
+ break;
464
+ }
465
+
466
+ mat.needsUpdate = true;
467
+ this.onSceneChanged.emit();
468
+ this.sceneChangedListeners.forEach((listener) => listener());
469
+ }
470
+
471
+ getMaterialProperty(meshName: string, property: string): unknown {
472
+ const obj = this.objects.get(meshName);
473
+ if (!obj) return undefined;
474
+ if (!(obj.material instanceof MeshPhongMaterial)) return undefined;
475
+
476
+ const mat = obj.material;
477
+
478
+ switch (property) {
479
+ case 'color':
480
+ return { r: mat.color.r, g: mat.color.g, b: mat.color.b };
481
+ case 'opacity':
482
+ return mat.opacity;
483
+ case 'visible':
484
+ return mat.visible;
485
+ case 'wireframe':
486
+ return mat.wireframe;
487
+ default:
488
+ return undefined;
489
+ }
490
+ }
491
+
492
+ // --- global click ---
493
+
494
+ addOnAnyMeshClickedListener(callback: (meshName: string) => void): void {
495
+ console.log(
496
+ '[DemoScene] addOnAnyMeshClickedListener , registering listener, total:',
497
+ this.anyMeshClickListeners.size + 1
498
+ );
499
+ this.anyMeshClickListeners.add(callback);
500
+ }
501
+
502
+ removeOnAnyMeshClickedListener(callback: (meshName: string) => void): void {
503
+ this.anyMeshClickListeners.delete(callback);
504
+ }
505
+
506
+ // Trigger the "any mesh clicked" event (for testing / raycaster hookup)
507
+ triggerAnyMeshClick(meshName: string): void {
508
+ console.log(
509
+ '[DemoScene] triggerAnyMeshClick:',
510
+ meshName,
511
+ ', listeners:',
512
+ this.anyMeshClickListeners.size
513
+ );
514
+ this.anyMeshClickListeners.forEach((cb) => cb(meshName));
515
+ // Also fire per-mesh listeners for backwards compat
516
+ this.triggerClick(meshName);
517
+ }
518
+
519
+ // --- spatial helpers ---
520
+
521
+ getMeshPosition(
522
+ meshName: string
523
+ ): { x: number; y: number; z: number } | undefined {
524
+ const obj = this.objects.get(meshName);
525
+ if (!obj) return undefined;
526
+ return {
527
+ x: obj.position.x,
528
+ y: obj.position.y,
529
+ z: obj.position.z
530
+ };
531
+ }
532
+
533
+ setMeshPosition(
534
+ meshName: string,
535
+ position: { x: number; y: number; z: number }
536
+ ): void {
537
+ const obj = this.objects.get(meshName);
538
+ if (!obj) return;
539
+ obj.position.set(position.x, position.y, position.z);
540
+ this.onSceneChanged.emit();
541
+ this.sceneChangedListeners.forEach((listener) => listener());
542
+ }
543
+
544
+ getDistanceBetween(meshA: string, meshB: string): number {
545
+ const a = this.objects.get(meshA);
546
+ const b = this.objects.get(meshB);
547
+ if (!a || !b) return 0;
548
+ return a.position.distanceTo(b.position);
549
+ }
550
+
551
+ lookAt(meshName: string, target: { x: number; y: number; z: number }): void {
552
+ const obj = this.objects.get(meshName);
553
+ if (!obj) return;
554
+ obj.lookAt(target.x, target.y, target.z);
555
+ this.onSceneChanged.emit();
556
+ this.sceneChangedListeners.forEach((listener) => listener());
557
+ }
558
+
559
+ moveTowards(
560
+ meshName: string,
561
+ target: { x: number; y: number; z: number },
562
+ speed: number,
563
+ deltaSeconds: number
564
+ ): boolean {
565
+ const obj = this.objects.get(meshName);
566
+ if (!obj) return true;
567
+
568
+ const targetVec = new Vector3(target.x, target.y, target.z);
569
+ const direction = targetVec.clone().sub(obj.position);
570
+ const distance = direction.length();
571
+ const step = speed * deltaSeconds;
572
+
573
+ if (distance <= step) {
574
+ // Arrived
575
+ obj.position.copy(targetVec);
576
+ this.onSceneChanged.emit();
577
+ this.sceneChangedListeners.forEach((listener) => listener());
578
+ return true;
579
+ }
580
+
581
+ direction.normalize().multiplyScalar(step);
582
+ obj.position.add(direction);
583
+ this.onSceneChanged.emit();
584
+ this.sceneChangedListeners.forEach((listener) => listener());
585
+ return false;
586
+ }
587
+
588
+ // --- mesh utilities ---
589
+
590
+ cloneMesh(sourceName: string, newName: string): void {
591
+ const source = this.objects.get(sourceName);
592
+ if (!source) return;
593
+
594
+ const cloned = source.clone();
595
+ cloned.name = newName;
596
+ this.scene.add(cloned);
597
+ this.objects.set(newName, cloned);
598
+
599
+ this.onSceneChanged.emit();
600
+ this.sceneChangedListeners.forEach((listener) => listener());
601
+ }
602
+
603
+ setMeshVisible(meshName: string, visible: boolean): void {
604
+ const obj = this.objects.get(meshName);
605
+ if (!obj) return;
606
+ obj.visible = visible;
607
+ this.onSceneChanged.emit();
608
+ this.sceneChangedListeners.forEach((listener) => listener());
609
+ }
610
+ }