react-three-game 0.0.67 → 0.0.69

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 (35) hide show
  1. package/README.md +109 -304
  2. package/dist/index.d.ts +15 -8
  3. package/dist/index.js +11 -8
  4. package/dist/shared/GameCanvas.d.ts +1 -2
  5. package/dist/tools/prefabeditor/EditorContext.d.ts +2 -2
  6. package/dist/tools/prefabeditor/EditorTree.d.ts +6 -6
  7. package/dist/tools/prefabeditor/EditorTree.js +92 -142
  8. package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +4 -11
  9. package/dist/tools/prefabeditor/EditorTreeMenus.js +16 -25
  10. package/dist/tools/prefabeditor/EditorUI.d.ts +5 -5
  11. package/dist/tools/prefabeditor/EditorUI.js +14 -11
  12. package/dist/tools/prefabeditor/GameEvents.d.ts +0 -30
  13. package/dist/tools/prefabeditor/GameEvents.js +0 -7
  14. package/dist/tools/prefabeditor/PrefabEditor.d.ts +12 -13
  15. package/dist/tools/prefabeditor/PrefabEditor.js +168 -138
  16. package/dist/tools/prefabeditor/PrefabRoot.d.ts +8 -5
  17. package/dist/tools/prefabeditor/PrefabRoot.js +141 -123
  18. package/dist/tools/prefabeditor/components/AmbientLightComponent.js +3 -3
  19. package/dist/tools/prefabeditor/components/CameraComponent.js +2 -2
  20. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +2 -2
  21. package/dist/tools/prefabeditor/components/ModelComponent.js +0 -1
  22. package/dist/tools/prefabeditor/components/SpotLightComponent.js +2 -2
  23. package/dist/tools/prefabeditor/components/TextComponent.js +2 -3
  24. package/dist/tools/prefabeditor/components/TransformComponent.js +9 -14
  25. package/dist/tools/prefabeditor/prefabStore.d.ts +42 -0
  26. package/dist/tools/prefabeditor/prefabStore.js +347 -0
  27. package/dist/tools/prefabeditor/sceneApi.d.ts +44 -0
  28. package/dist/tools/prefabeditor/sceneApi.js +161 -0
  29. package/dist/tools/prefabeditor/styles.d.ts +2 -1
  30. package/dist/tools/prefabeditor/styles.js +2 -12
  31. package/dist/tools/prefabeditor/utils.d.ts +15 -36
  32. package/dist/tools/prefabeditor/utils.js +36 -162
  33. package/package.json +4 -3
  34. package/dist/tools/prefabeditor/EventSystem.d.ts +0 -7
  35. package/dist/tools/prefabeditor/EventSystem.js +0 -23
@@ -7,19 +7,33 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
+ var __rest = (this && this.__rest) || function (s, e) {
11
+ var t = {};
12
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
13
+ t[p] = s[p];
14
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
15
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
16
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
17
+ t[p[i]] = s[p[i]];
18
+ }
19
+ return t;
20
+ };
10
21
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
11
22
  import { useHelper } from "@react-three/drei";
12
23
  import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
13
24
  import { BoxHelper, Euler, Matrix4, Quaternion, Vector3, } from "three";
25
+ import { useStore } from "zustand";
14
26
  import { getComponent, registerComponent } from "./components/ComponentRegistry";
15
27
  import components from "./components";
16
28
  import { loadModel, loadTexture } from "../dragdrop";
17
29
  import { GameInstance, GameInstanceProvider, getRepeatAxesFromModelProperties, useInstanceCheck } from "./InstanceProvider";
18
30
  import { decompose } from "./utils";
31
+ import { createPrefabStore, PrefabStoreProvider, prefabStoreToPrefab, usePrefabChildIds, usePrefabNode, usePrefabRootId } from "./prefabStore";
19
32
  components.forEach(registerComponent);
20
33
  const IDENTITY = new Matrix4();
21
- export const PrefabRoot = forwardRef(({ editMode, data, selectedId, onSelect, onClick, onSelectedObjectChange, onFocusNode, basePath = "", injectedModels = {}, injectedTextures = {} }, ref) => {
22
- // prefab root state
34
+ const EMPTY_MODELS = {};
35
+ const EMPTY_TEXTURES = {};
36
+ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSelect, onClick, onSelectedObjectChange, onFocusNode, basePath = "", injectedModels = EMPTY_MODELS, injectedTextures = EMPTY_TEXTURES }, ref) => {
23
37
  const [models, setModels] = useState({});
24
38
  const [textures, setTextures] = useState({});
25
39
  const loading = useRef(new Set());
@@ -27,6 +41,9 @@ export const PrefabRoot = forwardRef(({ editMode, data, selectedId, onSelect, on
27
41
  const objectRefs = useRef({});
28
42
  const rigidBodyRefs = useRef(new Map());
29
43
  const rootRef = useRef(null);
44
+ const [internalStore] = useState(() => { var _a; return createPrefabStore(data !== null && data !== void 0 ? data : prefabStoreToPrefab((_a = store === null || store === void 0 ? void 0 : store.getState()) !== null && _a !== void 0 ? _a : missingStoreState())); });
45
+ const prefabStore = store !== null && store !== void 0 ? store : internalStore;
46
+ const assetManifestKey = useStore(prefabStore, state => state.assetManifestKey);
30
47
  const availableModels = useMemo(() => (Object.assign(Object.assign({}, models), injectedModels)), [models, injectedModels]);
31
48
  const availableTextures = useMemo(() => (Object.assign(Object.assign({}, textures), injectedTextures)), [textures, injectedTextures]);
32
49
  useImperativeHandle(ref, () => ({
@@ -54,68 +71,78 @@ export const PrefabRoot = forwardRef(({ editMode, data, selectedId, onSelect, on
54
71
  return () => { console.error = originalError; };
55
72
  }, []);
56
73
  useEffect(() => {
57
- const modelsToLoad = new Set();
58
- const texturesToLoad = new Set();
59
- walk(data.root, node => {
60
- var _a;
61
- Object.values((_a = node.components) !== null && _a !== void 0 ? _a : {}).forEach(component => {
62
- var _a, _b, _c, _d;
63
- if (!(component === null || component === void 0 ? void 0 : component.type))
64
- return;
65
- if (component.type === 'Model' && ((_a = component.properties) === null || _a === void 0 ? void 0 : _a.filename)) {
66
- modelsToLoad.add(component.properties.filename);
67
- }
68
- if (component.type === 'Material') {
69
- ((_b = component.properties) === null || _b === void 0 ? void 0 : _b.texture) && texturesToLoad.add(component.properties.texture);
70
- ((_c = component.properties) === null || _c === void 0 ? void 0 : _c.normalMapTexture) && texturesToLoad.add(component.properties.normalMapTexture);
71
- }
72
- if (component.type === 'SpotLight' && ((_d = component.properties) === null || _d === void 0 ? void 0 : _d.map)) {
73
- texturesToLoad.add(component.properties.map);
74
- }
74
+ if (!store && data) {
75
+ prefabStore.getState().replacePrefab(data);
76
+ }
77
+ }, [data, prefabStore, store]);
78
+ useEffect(() => {
79
+ const syncAssets = (snapshot = prefabStore.getState()) => {
80
+ const modelsToLoad = new Set();
81
+ const texturesToLoad = new Set();
82
+ Object.values(snapshot.nodesById).forEach(node => {
83
+ var _a;
84
+ Object.values((_a = node.components) !== null && _a !== void 0 ? _a : {}).forEach(component => {
85
+ var _a, _b, _c, _d;
86
+ if (!(component === null || component === void 0 ? void 0 : component.type))
87
+ return;
88
+ if (component.type === 'Model' && ((_a = component.properties) === null || _a === void 0 ? void 0 : _a.filename)) {
89
+ modelsToLoad.add(component.properties.filename);
90
+ }
91
+ if (component.type === 'Material') {
92
+ ((_b = component.properties) === null || _b === void 0 ? void 0 : _b.texture) && texturesToLoad.add(component.properties.texture);
93
+ ((_c = component.properties) === null || _c === void 0 ? void 0 : _c.normalMapTexture) && texturesToLoad.add(component.properties.normalMapTexture);
94
+ }
95
+ if (component.type === 'SpotLight' && ((_d = component.properties) === null || _d === void 0 ? void 0 : _d.map)) {
96
+ texturesToLoad.add(component.properties.map);
97
+ }
98
+ });
75
99
  });
76
- });
77
- modelsToLoad.forEach((file) => __awaiter(void 0, void 0, void 0, function* () {
78
- if (availableModels[file] || loading.current.has(file))
79
- return;
80
- loading.current.add(file);
81
- const path = file.startsWith("/")
82
- ? `${basePath}${file}`
83
- : `${basePath}/${file}`;
84
- const res = yield loadModel(path);
85
- const model = res.model;
86
- if (res.success && model) {
87
- setModels(m => (Object.assign(Object.assign({}, m), { [file]: model })));
88
- }
89
- }));
90
- texturesToLoad.forEach(file => {
91
- if (availableTextures[file] || loading.current.has(file) || failedTextures.current.has(file))
92
- return;
93
- loading.current.add(file);
94
- // Handle full URLs (http/https) or regular paths
95
- const path = file.startsWith("http://") || file.startsWith("https://")
96
- ? file
97
- : file.startsWith("/")
100
+ modelsToLoad.forEach((file) => __awaiter(void 0, void 0, void 0, function* () {
101
+ if (models[file] || injectedModels[file] || loading.current.has(file))
102
+ return;
103
+ loading.current.add(file);
104
+ const path = file.startsWith("/")
98
105
  ? `${basePath}${file}`
99
106
  : `${basePath}/${file}`;
100
- void loadTexture(path).then(result => {
101
- if (result.success && result.texture) {
102
- setTextures(t => (Object.assign(Object.assign({}, t), { [file]: result.texture })));
103
- return;
107
+ const res = yield loadModel(path);
108
+ const model = res.model;
109
+ if (res.success && model) {
110
+ setModels(m => (Object.assign(Object.assign({}, m), { [file]: model })));
104
111
  }
105
- console.warn(`Failed to load texture: ${path}`, result.error);
106
- loading.current.delete(file);
107
- failedTextures.current.add(file);
112
+ }));
113
+ texturesToLoad.forEach(file => {
114
+ if (textures[file] || injectedTextures[file] || loading.current.has(file) || failedTextures.current.has(file))
115
+ return;
116
+ loading.current.add(file);
117
+ // Handle full URLs (http/https) or regular paths
118
+ const path = file.startsWith("http://") || file.startsWith("https://")
119
+ ? file
120
+ : file.startsWith("/")
121
+ ? `${basePath}${file}`
122
+ : `${basePath}/${file}`;
123
+ void loadTexture(path).then(result => {
124
+ if (result.success && result.texture) {
125
+ setTextures(t => (Object.assign(Object.assign({}, t), { [file]: result.texture })));
126
+ return;
127
+ }
128
+ console.warn(`Failed to load texture: ${path}`, result.error);
129
+ loading.current.delete(file);
130
+ failedTextures.current.add(file);
131
+ });
108
132
  });
109
- });
110
- }, [data, availableModels, availableTextures, basePath]);
111
- return (_jsx("group", { ref: rootRef, children: _jsx(GameInstanceProvider, { models: availableModels, selectedId: selectedId, editMode: editMode, onSelect: editMode ? onSelect : undefined, registerRef: registerRef, children: _jsx(GameObjectRenderer, { gameObject: data.root, selectedId: selectedId, onSelect: editMode ? onSelect : undefined, onClick: onClick, registerRef: registerRef, registerRigidBodyRef: registerRigidBodyRef, loadedModels: availableModels, loadedTextures: availableTextures, editMode: editMode, parentMatrix: IDENTITY }) }) }));
133
+ };
134
+ syncAssets();
135
+ }, [prefabStore, assetManifestKey, basePath, injectedModels, injectedTextures]);
136
+ return (_jsx(PrefabStoreProvider, { store: prefabStore, children: _jsx("group", { ref: rootRef, children: _jsx(GameInstanceProvider, { models: availableModels, selectedId: selectedId, editMode: editMode, onSelect: editMode ? onSelect : undefined, registerRef: registerRef, children: _jsx(StoreRootNode, { selectedId: selectedId, onSelect: editMode ? onSelect : undefined, onClick: onClick, registerRef: registerRef, registerRigidBodyRef: registerRigidBodyRef, loadedModels: availableModels, loadedTextures: availableTextures, editMode: editMode, parentMatrix: IDENTITY }) }) }) }));
112
137
  });
138
+ function StoreRootNode(props) {
139
+ const rootId = usePrefabRootId();
140
+ return _jsx(GameObjectRenderer, Object.assign({}, props, { nodeId: rootId }));
141
+ }
113
142
  export function GameObjectRenderer(props) {
114
143
  var _a, _b, _c;
115
- const node = props.gameObject;
116
- if (!node || node.disabled)
117
- return null;
118
- const isInstanced = (_c = (_b = (_a = node.components) === null || _a === void 0 ? void 0 : _a.model) === null || _b === void 0 ? void 0 : _b.properties) === null || _c === void 0 ? void 0 : _c.instanced;
144
+ const node = usePrefabNode(props.nodeId);
145
+ const isInstanced = (_c = (_b = (_a = node === null || node === void 0 ? void 0 : node.components) === null || _a === void 0 ? void 0 : _a.model) === null || _b === void 0 ? void 0 : _b.properties) === null || _c === void 0 ? void 0 : _c.instanced;
119
146
  const prevInstancedRef = useRef(undefined);
120
147
  const [isTransitioning, setIsTransitioning] = useState(false);
121
148
  useEffect(() => {
@@ -126,9 +153,9 @@ export function GameObjectRenderer(props) {
126
153
  }
127
154
  prevInstancedRef.current = isInstanced;
128
155
  }, [isInstanced]);
129
- if (isTransitioning)
156
+ if (!node || node.disabled || isTransitioning)
130
157
  return null;
131
- const key = `${node.id}_${isInstanced ? 'instanced' : 'standard'}`;
158
+ const key = `${props.nodeId}_${isInstanced ? 'instanced' : 'standard'}`;
132
159
  return isInstanced
133
160
  ? _jsx(InstancedNode, Object.assign({}, props), key)
134
161
  : _jsx(StandardNode, Object.assign({}, props), key);
@@ -136,8 +163,11 @@ export function GameObjectRenderer(props) {
136
163
  function isPhysicsProps(v) {
137
164
  return (v === null || v === void 0 ? void 0 : v.type) === "fixed" || (v === null || v === void 0 ? void 0 : v.type) === "dynamic" || (v === null || v === void 0 ? void 0 : v.type) === "kinematicPosition" || (v === null || v === void 0 ? void 0 : v.type) === "kinematicVelocity";
138
165
  }
139
- function InstancedNode({ gameObject, parentMatrix = IDENTITY, editMode, registerRef, selectedId: _selectedId, onSelect, onClick }) {
166
+ function InstancedNode({ nodeId, parentMatrix = IDENTITY, editMode, registerRef, onSelect, onClick }) {
140
167
  var _a, _b, _c, _d, _e, _f, _g, _h;
168
+ const gameObject = usePrefabNode(nodeId);
169
+ if (!gameObject)
170
+ return null;
141
171
  const localTransform = getNodeTransformProps(gameObject);
142
172
  const isLocked = Boolean(gameObject.locked);
143
173
  const clickable = Object.values((_a = gameObject.components) !== null && _a !== void 0 ? _a : {}).some(component => (component === null || component === void 0 ? void 0 : component.type) === 'Click');
@@ -150,15 +180,15 @@ function InstancedNode({ gameObject, parentMatrix = IDENTITY, editMode, register
150
180
  const clickValid = useRef(false);
151
181
  useEffect(() => {
152
182
  if (editMode) {
153
- registerRef(gameObject.id, groupRef.current);
154
- return () => registerRef(gameObject.id, null);
183
+ registerRef(nodeId, groupRef.current);
184
+ return () => registerRef(nodeId, null);
155
185
  }
156
- }, [gameObject.id, registerRef, editMode]);
186
+ }, [nodeId, registerRef, editMode]);
157
187
  if (editMode) {
158
188
  return (_jsxs(_Fragment, { children: [_jsx("group", { ref: groupRef, position: localTransform.position, rotation: localTransform.rotation, scale: localTransform.scale, onPointerDown: isLocked ? undefined : (e) => { e.stopPropagation(); clickValid.current = true; }, onPointerMove: isLocked ? undefined : () => { clickValid.current = false; }, onPointerUp: isLocked ? undefined : (e) => {
159
189
  if (clickValid.current) {
160
190
  e.stopPropagation();
161
- onSelect === null || onSelect === void 0 ? void 0 : onSelect(gameObject.id);
191
+ onSelect === null || onSelect === void 0 ? void 0 : onSelect(nodeId);
162
192
  onClick === null || onClick === void 0 ? void 0 : onClick(e, gameObject);
163
193
  }
164
194
  clickValid.current = false;
@@ -166,19 +196,23 @@ function InstancedNode({ gameObject, parentMatrix = IDENTITY, editMode, register
166
196
  }
167
197
  return (_jsx(_Fragment, { children: instances.map(instance => (_jsx(GameInstance, { id: instance.id, sourceId: gameObject.id, clickable: clickable, modelUrl: instance.modelUrl, position: instance.position, rotation: instance.rotation, scale: instance.scale, locked: isLocked, physics: instance.physics }, instance.id))) }));
168
198
  }
169
- function StandardNode({ gameObject, selectedId, onSelect, onClick, registerRef, registerRigidBodyRef, loadedModels, loadedTextures, editMode, parentMatrix = IDENTITY, }) {
199
+ function StandardNode({ nodeId, selectedId, onSelect, onClick, registerRef, registerRigidBodyRef, loadedModels, loadedTextures, editMode, parentMatrix = IDENTITY, }) {
170
200
  var _a, _b, _c, _d, _e;
201
+ const gameObject = usePrefabNode(nodeId);
202
+ const childIds = usePrefabChildIds(nodeId);
203
+ const isSelected = selectedId === nodeId;
204
+ if (!gameObject)
205
+ return null;
171
206
  const groupRef = useRef(null);
172
207
  const helperRef = useRef(null);
173
208
  const clickValid = useRef(false);
174
209
  const isLocked = Boolean(gameObject.locked);
175
- const isSelected = selectedId === gameObject.id;
176
- const stillInstanced = useInstanceCheck(gameObject.id);
210
+ const stillInstanced = useInstanceCheck(nodeId);
177
211
  useHelper(editMode && isSelected ? helperRef : null, BoxHelper, "cyan");
178
212
  useEffect(() => {
179
- registerRef(gameObject.id, groupRef.current);
180
- return () => registerRef(gameObject.id, null);
181
- }, [gameObject.id, registerRef]);
213
+ registerRef(nodeId, groupRef.current);
214
+ return () => registerRef(nodeId, null);
215
+ }, [nodeId, registerRef]);
182
216
  const world = parentMatrix.clone().multiply(compose(gameObject));
183
217
  const onDown = (e) => {
184
218
  e.stopPropagation();
@@ -187,7 +221,7 @@ function StandardNode({ gameObject, selectedId, onSelect, onClick, registerRef,
187
221
  const onUp = (e) => {
188
222
  if (clickValid.current) {
189
223
  e.stopPropagation();
190
- onSelect === null || onSelect === void 0 ? void 0 : onSelect(gameObject.id);
224
+ onSelect === null || onSelect === void 0 ? void 0 : onSelect(nodeId);
191
225
  onClick === null || onClick === void 0 ? void 0 : onClick(e, gameObject);
192
226
  }
193
227
  clickValid.current = false;
@@ -199,51 +233,35 @@ function StandardNode({ gameObject, selectedId, onSelect, onClick, registerRef,
199
233
  const transform = getNodeTransformProps(gameObject);
200
234
  const physicsDef = hasPhysics ? getComponent("Physics") : null;
201
235
  const isInstanced = (_e = (_d = (_c = gameObject.components) === null || _c === void 0 ? void 0 : _c.model) === null || _d === void 0 ? void 0 : _d.properties) === null || _e === void 0 ? void 0 : _e.instanced;
202
- const physicsKey = `physics_${gameObject.id}_${isInstanced ? 'instanced' : 'standard'}`;
203
- const renderCtx = createRenderContext(loadedModels, loadedTextures, editMode, selectedId, registerRef);
236
+ const physicsKey = `physics_${nodeId}_${isInstanced ? 'instanced' : 'standard'}`;
237
+ const renderCtx = { loadedModels, loadedTextures, editMode, registerRef };
204
238
  const childNodes = getChildHostComponents(gameObject).length > 0
205
- ? renderHostedChildren(gameObject, renderCtx, world)
206
- : renderSceneChildren(gameObject, world, {
207
- selectedId,
208
- onSelect,
209
- onClick,
210
- registerRef,
211
- registerRigidBodyRef,
212
- loadedModels,
213
- loadedTextures,
214
- editMode,
215
- });
216
- const inner = (_jsx("group", { onPointerDown: editMode && !isLocked ? onDown : undefined, onPointerMove: editMode && !isLocked ? () => (clickValid.current = false) : undefined, onPointerUp: editMode && !isLocked ? onUp : undefined, children: renderCompositionNode(gameObject, renderCtx, parentMatrix, childNodes) }));
239
+ ? _jsx(CompositionChildren, { childIds: childIds, selectedId: selectedId, ctx: renderCtx, parentMatrix: world })
240
+ : _jsx(ChildNodes, { childIds: childIds, parentMatrix: world, selectedId: selectedId, onSelect: onSelect, onClick: onClick, registerRef: registerRef, registerRigidBodyRef: registerRigidBodyRef, loadedModels: loadedModels, loadedTextures: loadedTextures, editMode: editMode });
241
+ const inner = (_jsx("group", { onPointerDown: editMode && !isLocked ? onDown : undefined, onPointerMove: editMode && !isLocked ? () => (clickValid.current = false) : undefined, onPointerUp: editMode && !isLocked ? onUp : undefined, children: renderCompositionNode(gameObject, renderCtx, isSelected, parentMatrix, childNodes) }));
217
242
  if (editMode) {
218
- return (_jsxs(_Fragment, { children: [_jsx("group", { ref: groupRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: _jsx("mesh", { visible: false, children: _jsx("boxGeometry", { args: [0.01, 0.01, 0.01] }) }) }), _jsx("group", { ref: helperRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: inner }), hasPhysics && (physicsDef === null || physicsDef === void 0 ? void 0 : physicsDef.View) ? (_jsx(physicsDef.View, { properties: physics.properties, position: transform.position, rotation: transform.rotation, scale: transform.scale, editMode: editMode, nodeId: gameObject.id, registerRigidBodyRef: registerRigidBodyRef, children: inner }, physicsKey)) : null] }));
243
+ return (_jsxs(_Fragment, { children: [_jsx("group", { ref: groupRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: _jsx("mesh", { visible: false, children: _jsx("boxGeometry", { args: [0.01, 0.01, 0.01] }) }) }), _jsx("group", { ref: helperRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: inner }), hasPhysics && (physicsDef === null || physicsDef === void 0 ? void 0 : physicsDef.View) ? (_jsx(physicsDef.View, { properties: physics.properties, position: transform.position, rotation: transform.rotation, scale: transform.scale, editMode: editMode, nodeId: nodeId, registerRigidBodyRef: registerRigidBodyRef, children: inner }, physicsKey)) : null] }));
219
244
  }
220
245
  if (hasPhysics && (physicsDef === null || physicsDef === void 0 ? void 0 : physicsDef.View)) {
221
- return (_jsx(physicsDef.View, { properties: physics.properties, position: transform.position, rotation: transform.rotation, scale: transform.scale, editMode: editMode, nodeId: gameObject.id, registerRigidBodyRef: registerRigidBodyRef, children: inner }, physicsKey));
246
+ return (_jsx(physicsDef.View, { properties: physics.properties, position: transform.position, rotation: transform.rotation, scale: transform.scale, editMode: editMode, nodeId: nodeId, registerRigidBodyRef: registerRigidBodyRef, children: inner }, physicsKey));
222
247
  }
223
248
  return (_jsx("group", { ref: groupRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: inner }));
224
249
  }
250
+ const LEAF_COMPONENT_TYPES = new Set(["Geometry", "Material", "Physics"]);
225
251
  function getChildHostComponents(gameObject) {
226
252
  var _a;
227
253
  return Object.entries((_a = gameObject.components) !== null && _a !== void 0 ? _a : {}).flatMap(([key, comp]) => {
228
254
  if (!(comp === null || comp === void 0 ? void 0 : comp.type))
229
255
  return [];
230
256
  const def = getComponent(comp.type);
231
- if (!(def === null || def === void 0 ? void 0 : def.View) || def.nonComposable)
257
+ if (!(def === null || def === void 0 ? void 0 : def.View) || LEAF_COMPONENT_TYPES.has(comp.type))
232
258
  return [];
233
259
  return { key, View: def.View, properties: comp.properties };
234
260
  });
235
261
  }
236
- function createRenderContext(loadedModels, loadedTextures, editMode, selectedId, registerRef) {
237
- return { loadedModels, loadedTextures, editMode, selectedId, registerRef };
238
- }
239
- function renderSceneChildren(gameObject, parentMatrix, props) {
240
- var _a;
241
- return (_a = gameObject.children) === null || _a === void 0 ? void 0 : _a.map(child => _jsx(GameObjectRenderer, { gameObject: child, selectedId: props.selectedId, onSelect: props.onSelect, onClick: props.onClick, registerRef: props.registerRef, registerRigidBodyRef: props.registerRigidBodyRef, loadedModels: props.loadedModels, loadedTextures: props.loadedTextures, editMode: props.editMode, parentMatrix: parentMatrix }, child.id));
242
- }
243
- function walk(node, fn) {
244
- var _a;
245
- fn(node);
246
- (_a = node.children) === null || _a === void 0 ? void 0 : _a.forEach(c => walk(c, fn));
262
+ function ChildNodes(_a) {
263
+ var { childIds, parentMatrix } = _a, props = __rest(_a, ["childIds", "parentMatrix"]);
264
+ return childIds.map(childId => _jsx(GameObjectRenderer, Object.assign({ nodeId: childId, parentMatrix: parentMatrix }, props), childId));
247
265
  }
248
266
  function compose(node) {
249
267
  const { position, rotation, scale } = getNodeTransformProps(node);
@@ -310,62 +328,59 @@ function getNodeTransformProps(node) {
310
328
  scale: (_e = t === null || t === void 0 ? void 0 : t.scale) !== null && _e !== void 0 ? _e : [1, 1, 1],
311
329
  };
312
330
  }
313
- function renderCompositionSubtree(gameObject, ctx, parentMatrix = IDENTITY) {
331
+ function renderCompositionSubtree(gameObject, ctx, isSelected, childIds, parentMatrix = IDENTITY) {
314
332
  if (!gameObject || gameObject.disabled)
315
333
  return null;
316
334
  const transform = getNodeTransformProps(gameObject);
317
335
  const world = parentMatrix.clone().multiply(compose(gameObject));
318
- const childNodes = renderHostedChildren(gameObject, ctx, world);
319
- return (_jsx("group", { position: transform.position, rotation: transform.rotation, scale: transform.scale, children: renderCompositionNode(gameObject, ctx, parentMatrix, childNodes) }, gameObject.id));
336
+ const childNodes = _jsx(CompositionChildren, { childIds: childIds, ctx: ctx, parentMatrix: world });
337
+ return (_jsx("group", { position: transform.position, rotation: transform.rotation, scale: transform.scale, children: renderCompositionNode(gameObject, ctx, isSelected, parentMatrix, childNodes) }, gameObject.id));
320
338
  }
321
- function renderHostedChildren(gameObject, ctx, parentMatrix) {
322
- var _a;
323
- return (_a = gameObject.children) === null || _a === void 0 ? void 0 : _a.map(child => renderCompositionSubtree(child, ctx, parentMatrix));
339
+ function CompositionChildren({ childIds, selectedId, ctx, parentMatrix, }) {
340
+ return childIds.map(childId => (_jsx(CompositionSubtree, { nodeId: childId, selectedId: selectedId, ctx: ctx, parentMatrix: parentMatrix }, childId)));
341
+ }
342
+ function CompositionSubtree({ nodeId, selectedId, ctx, parentMatrix, }) {
343
+ const gameObject = usePrefabNode(nodeId);
344
+ const childIds = usePrefabChildIds(nodeId);
345
+ const isSelected = selectedId === nodeId;
346
+ if (!gameObject)
347
+ return null;
348
+ return renderCompositionSubtree(gameObject, ctx, isSelected, childIds, parentMatrix);
324
349
  }
325
- function renderCompositionNode(gameObject, ctx, parentMatrix, childNodes) {
326
- const ownContent = renderNodeOwnContent(gameObject, ctx, parentMatrix);
327
- return wrapWithChildHosts(gameObject, ctx, parentMatrix, _jsxs(_Fragment, { children: [ownContent, childNodes] }));
350
+ function renderCompositionNode(gameObject, ctx, isSelected, parentMatrix, childNodes) {
351
+ const ownContent = renderNodeOwnContent(gameObject, ctx, isSelected, parentMatrix);
352
+ return wrapWithChildHosts(gameObject, ctx, isSelected, parentMatrix, _jsxs(_Fragment, { children: [ownContent, childNodes] }));
328
353
  }
329
- function renderNodeOwnContent(gameObject, ctx, parentMatrix) {
330
- var _a, _b, _c, _d;
354
+ function renderNodeOwnContent(gameObject, ctx, isSelected, parentMatrix) {
355
+ var _a, _b;
331
356
  const geometry = (_a = gameObject.components) === null || _a === void 0 ? void 0 : _a.geometry;
332
357
  const material = (_b = gameObject.components) === null || _b === void 0 ? void 0 : _b.material;
333
- const model = (_c = gameObject.components) === null || _c === void 0 ? void 0 : _c.model;
334
- const text = (_d = gameObject.components) === null || _d === void 0 ? void 0 : _d.text;
335
358
  const geometryDef = geometry && getComponent("Geometry");
336
359
  const materialDef = material && getComponent("Material");
337
- const modelDef = model && getComponent("Model");
338
- const textDef = text && getComponent("Text");
339
360
  const contextProps = {
340
361
  loadedModels: ctx.loadedModels,
341
362
  loadedTextures: ctx.loadedTextures,
342
363
  editMode: ctx.editMode,
343
- isSelected: ctx.selectedId === gameObject.id,
364
+ isSelected,
344
365
  nodeId: gameObject.id,
345
366
  parentMatrix,
346
367
  registerRef: ctx.registerRef,
347
368
  };
348
369
  let core;
349
- if (model && (modelDef === null || modelDef === void 0 ? void 0 : modelDef.View)) {
350
- core = (_jsx(modelDef.View, Object.assign({ properties: model.properties }, contextProps, { children: material && (materialDef === null || materialDef === void 0 ? void 0 : materialDef.View) && (_jsx(materialDef.View, Object.assign({ properties: material.properties }, contextProps), "material")) })));
351
- }
352
- else if (geometry && (geometryDef === null || geometryDef === void 0 ? void 0 : geometryDef.View)) {
370
+ if (geometry && (geometryDef === null || geometryDef === void 0 ? void 0 : geometryDef.View)) {
353
371
  core = (_jsxs("mesh", { castShadow: true, receiveShadow: true, children: [_jsx(geometryDef.View, Object.assign({ properties: geometry.properties }, contextProps)), material && (materialDef === null || materialDef === void 0 ? void 0 : materialDef.View) && (_jsx(materialDef.View, Object.assign({ properties: material.properties }, contextProps), "material"))] }));
354
372
  }
355
- else if (text && (textDef === null || textDef === void 0 ? void 0 : textDef.View)) {
356
- core = (_jsx(_Fragment, { children: _jsx(textDef.View, Object.assign({ properties: text.properties }, contextProps)) }));
357
- }
358
373
  else {
359
374
  core = null;
360
375
  }
361
376
  return core;
362
377
  }
363
- function wrapWithChildHosts(gameObject, ctx, parentMatrix, subtree) {
378
+ function wrapWithChildHosts(gameObject, ctx, isSelected, parentMatrix, subtree) {
364
379
  const contextProps = {
365
380
  loadedModels: ctx.loadedModels,
366
381
  loadedTextures: ctx.loadedTextures,
367
382
  editMode: ctx.editMode,
368
- isSelected: ctx.selectedId === gameObject.id,
383
+ isSelected,
369
384
  nodeId: gameObject.id,
370
385
  parentMatrix,
371
386
  registerRef: ctx.registerRef,
@@ -374,3 +389,6 @@ function wrapWithChildHosts(gameObject, ctx, parentMatrix, subtree) {
374
389
  return childHosts.reduce((acc, { key, View, properties }) => (_jsx(View, Object.assign({ properties: properties }, contextProps, { children: acc }), key)), subtree);
375
390
  }
376
391
  export default PrefabRoot;
392
+ function missingStoreState() {
393
+ throw new Error("PrefabRoot requires either data or store");
394
+ }
@@ -1,11 +1,11 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { ColorField, FieldGroup, NumberField } from "./Input";
3
3
  function AmbientLightComponentEditor({ component, onUpdate, }) {
4
4
  return (_jsxs(FieldGroup, { children: [_jsx(ColorField, { name: "color", label: "Color", values: component.properties, onChange: onUpdate }), _jsx(NumberField, { name: "intensity", label: "Intensity", values: component.properties, onChange: onUpdate, min: 0, step: 0.1, fallback: 1 })] }));
5
5
  }
6
- function AmbientLightComponentView({ properties }) {
6
+ function AmbientLightComponentView({ properties, children }) {
7
7
  const { color = '#ffffff', intensity = 1 } = properties;
8
- return _jsx("ambientLight", { color: color, intensity: intensity });
8
+ return (_jsxs(_Fragment, { children: [_jsx("ambientLight", { color: color, intensity: intensity }), children] }));
9
9
  }
10
10
  const AmbientLightComponent = {
11
11
  name: 'AmbientLight',
@@ -14,7 +14,7 @@ function CameraComponentEditor({ component, onUpdate }) {
14
14
  const values = Object.assign(Object.assign({}, cameraDefaults), component.properties);
15
15
  return (_jsxs(FieldGroup, { children: [_jsx(NumberField, { name: "fov", label: "FOV", values: values, onChange: onUpdate, fallback: 50, min: 1, max: 179, step: 1 }), _jsx(NumberField, { name: "near", label: "Near", values: values, onChange: onUpdate, fallback: 0.1, min: 0.001, step: 0.1 }), _jsx(NumberField, { name: "zoom", label: "Zoom", values: values, onChange: onUpdate, fallback: 1, min: 0.01, step: 0.1 }), _jsx(NumberField, { name: "far", label: "Far", values: values, onChange: onUpdate, fallback: 1000, min: 0.1, step: 1 })] }));
16
16
  }
17
- function CameraComponentView({ properties, editMode, isSelected }) {
17
+ function CameraComponentView({ properties, children, editMode, isSelected }) {
18
18
  const merged = Object.assign(Object.assign({}, cameraDefaults), properties);
19
19
  const fov = merged.fov;
20
20
  const near = merged.near;
@@ -34,7 +34,7 @@ function CameraComponentView({ properties, editMode, isSelected }) {
34
34
  cameraHelper.update();
35
35
  }
36
36
  });
37
- return (_jsxs(_Fragment, { children: [_jsx(PerspectiveCamera, { ref: (instance) => setCamera(instance), makeDefault: !editMode, fov: fov, near: near, zoom: zoom, far: far }), editMode && isSelected && cameraHelper && (_jsx("primitive", { object: cameraHelper })), editMode ? (_jsxs("group", { children: [_jsxs("mesh", { children: [_jsx("boxGeometry", { args: [0.3, 0.3, 0.5] }), _jsx("meshBasicMaterial", { color: '#22d3ee', wireframe: true })] }), _jsxs("mesh", { position: [0, 0, -0.25], rotation: [Math.PI / 2, 0, 0], children: [_jsx("coneGeometry", { args: [0.08, 0.16, 16] }), _jsx("meshBasicMaterial", { color: '#22d3ee', wireframe: true })] })] })) : null] }));
37
+ return (_jsxs(_Fragment, { children: [_jsx(PerspectiveCamera, { ref: (instance) => setCamera(instance), makeDefault: !editMode, fov: fov, near: near, zoom: zoom, far: far }), editMode && isSelected && cameraHelper && (_jsx("primitive", { object: cameraHelper })), editMode ? (_jsxs("group", { children: [_jsxs("mesh", { children: [_jsx("boxGeometry", { args: [0.3, 0.3, 0.5] }), _jsx("meshBasicMaterial", { color: '#22d3ee', wireframe: true })] }), _jsxs("mesh", { position: [0, 0, -0.25], rotation: [Math.PI / 2, 0, 0], children: [_jsx("coneGeometry", { args: [0.08, 0.16, 16] }), _jsx("meshBasicMaterial", { color: '#22d3ee', wireframe: true })] })] })) : null, children] }));
38
38
  }
39
39
  const CameraComponent = {
40
40
  name: 'Camera',
@@ -48,7 +48,7 @@ function DirectionalLightComponentEditor({ component, onUpdate }) {
48
48
  : directionalLightFields.filter(field => field.name !== '_shadowCamera');
49
49
  return (_jsx(FieldRenderer, { fields: fields, values: values, onChange: onUpdate }));
50
50
  }
51
- function DirectionalLightView({ properties, editMode, isSelected }) {
51
+ function DirectionalLightView({ properties, children, editMode, isSelected }) {
52
52
  const merged = Object.assign(Object.assign({}, directionalLightDefaults), properties);
53
53
  const color = merged.color;
54
54
  const intensity = merged.intensity;
@@ -93,7 +93,7 @@ function DirectionalLightView({ properties, editMode, isSelected }) {
93
93
  new Vector3(targetOffset[0], targetOffset[1], targetOffset[2])
94
94
  ];
95
95
  geo.setFromPoints(points);
96
- } }), _jsx("lineBasicMaterial", { color: color, opacity: 0.6, transparent: true })] })] }))] }));
96
+ } }), _jsx("lineBasicMaterial", { color: color, opacity: 0.6, transparent: true })] })] })), children] }));
97
97
  }
98
98
  const DirectionalLightComponent = {
99
99
  name: 'DirectionalLight',
@@ -102,7 +102,6 @@ const ModelComponent = {
102
102
  name: 'Model',
103
103
  Editor: ModelComponentEditor,
104
104
  View: ModelComponentView,
105
- nonComposable: true,
106
105
  defaultProperties: {
107
106
  filename: '',
108
107
  instanced: false,
@@ -16,7 +16,7 @@ function SpotLightComponentEditor({ component, onUpdate, basePath = "" }) {
16
16
  const values = Object.assign(Object.assign({}, spotLightDefaults), component.properties);
17
17
  return (_jsxs(FieldGroup, { children: [_jsx(ColorField, { name: "color", label: "Color", values: values, onChange: onUpdate }), _jsx(NumberField, { name: "intensity", label: "Intensity", values: values, onChange: onUpdate, min: 0, step: 1, fallback: 200 }), _jsx(NumberField, { name: "angle", label: "Angle", values: values, onChange: onUpdate, min: 0, max: Math.PI, step: 0.05, fallback: 0.5 }), _jsx(NumberField, { name: "penumbra", label: "Penumbra", values: values, onChange: onUpdate, min: 0, max: 1, step: 0.05, fallback: 0.5 }), _jsx(NumberField, { name: "distance", label: "Distance", values: values, onChange: onUpdate, min: 0, step: 1, fallback: 100 }), _jsx(BooleanField, { name: "castShadow", label: "Cast Shadow", values: values, onChange: onUpdate, fallback: true }), _jsxs("div", { children: [_jsx(Label, { children: "Texture Map" }), _jsx(TexturePicker, { value: values.map, onChange: (map) => onUpdate({ map }), basePath: basePath })] })] }));
18
18
  }
19
- function SpotLightView({ properties, editMode, isSelected, loadedTextures }) {
19
+ function SpotLightView({ properties, children, editMode, isSelected, loadedTextures }) {
20
20
  const merged = Object.assign(Object.assign({}, spotLightDefaults), properties);
21
21
  const color = merged.color;
22
22
  const intensity = merged.intensity;
@@ -45,7 +45,7 @@ function SpotLightView({ properties, editMode, isSelected, loadedTextures }) {
45
45
  spotLightHelper.update();
46
46
  }
47
47
  });
48
- return (_jsxs(_Fragment, { children: [_jsx("spotLight", { ref: spotLightRef, color: color, intensity: intensity, angle: angle, penumbra: penumbra, distance: distance, map: textureMap, castShadow: castShadow, "shadow-mapSize-width": 1024, "shadow-mapSize-height": 1024, "shadow-bias": -0.0001, "shadow-normalBias": 0.02 }), editMode && isSelected && spotLightHelper && (_jsx("primitive", { object: spotLightHelper })), _jsx("object3D", { ref: targetRef, position: [0, -5, 0] }), editMode && (_jsxs(_Fragment, { children: [_jsxs("mesh", { children: [_jsx("sphereGeometry", { args: [0.2, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true })] }), _jsxs("mesh", { position: [0, -5, 0], children: [_jsx("sphereGeometry", { args: [0.15, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true, opacity: 0.5, transparent: true })] })] }))] }));
48
+ return (_jsxs(_Fragment, { children: [_jsx("spotLight", { ref: spotLightRef, color: color, intensity: intensity, angle: angle, penumbra: penumbra, distance: distance, map: textureMap, castShadow: castShadow, "shadow-mapSize-width": 1024, "shadow-mapSize-height": 1024, "shadow-bias": -0.0001, "shadow-normalBias": 0.02 }), editMode && isSelected && spotLightHelper && (_jsx("primitive", { object: spotLightHelper })), _jsx("object3D", { ref: targetRef, position: [0, -5, 0] }), editMode && (_jsxs(_Fragment, { children: [_jsxs("mesh", { children: [_jsx("sphereGeometry", { args: [0.2, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true })] }), _jsxs("mesh", { position: [0, -5, 0], children: [_jsx("sphereGeometry", { args: [0.15, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true, opacity: 0.5, transparent: true })] })] })), children] }));
49
49
  }
50
50
  const SpotLightComponent = {
51
51
  name: 'SpotLight',
@@ -11,7 +11,7 @@ function TextComponentEditor({ component, onUpdate, }) {
11
11
  { value: 'right', label: 'Right' },
12
12
  ] })] }));
13
13
  }
14
- function TextComponentView({ properties }) {
14
+ function TextComponentView({ properties, children }) {
15
15
  const { text = '', font, size, depth, width, align, color } = properties;
16
16
  const textContent = String(text || '');
17
17
  const meshRef = useRef(null);
@@ -37,13 +37,12 @@ function TextComponentView({ properties }) {
37
37
  }, [align]);
38
38
  if (!textContent)
39
39
  return null;
40
- return (_jsx("group", { position: offset, children: _jsx(Text, { ref: meshRef, font: font, size: size, depth: depth, layout: { align, width }, color: color, onLoad: handleLoad, children: textContent }) }));
40
+ return (_jsxs("group", { position: offset, children: [_jsx(Text, { ref: meshRef, font: font, size: size, depth: depth, layout: { align, width }, color: color, onLoad: handleLoad, children: textContent }), children] }));
41
41
  }
42
42
  const TextComponent = {
43
43
  name: 'Text',
44
44
  Editor: TextComponentEditor,
45
45
  View: TextComponentView,
46
- nonComposable: true,
47
46
  defaultProperties: {
48
47
  text: 'Hello World',
49
48
  color: '#888888',
@@ -1,5 +1,5 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
- import { Label, Vector3Field, Vector3Input } from "./Input";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Label, Vector3Input } from "./Input";
3
3
  import { useEditorContext } from "../EditorContext";
4
4
  import { colors } from "../styles";
5
5
  const buttonStyle = {
@@ -13,8 +13,8 @@ const buttonStyle = {
13
13
  fontSize: 11,
14
14
  flex: 1,
15
15
  };
16
- function TransformModeSelector({ transformMode, setTransformMode, snapResolution, setSnapResolution }) {
17
- return (_jsxs("div", { style: { marginBottom: 8 }, children: [_jsxs(Label, { children: ["Transform Mode ", snapResolution > 0 && `(Snap: ${snapResolution})`] }), _jsx("div", { style: { display: 'flex', gap: 6 }, children: ["translate", "rotate", "scale"].map(mode => {
16
+ function TransformModeSelector({ transformMode, setTransformMode }) {
17
+ return (_jsxs("div", { style: { marginBottom: 8 }, children: [_jsx(Label, { children: "Transform Mode" }), _jsx("div", { style: { display: 'flex', gap: 6 }, children: ["translate", "rotate", "scale"].map(mode => {
18
18
  const isActive = transformMode === mode;
19
19
  return (_jsx("button", { onClick: () => setTransformMode(mode), style: Object.assign(Object.assign({}, buttonStyle), { background: isActive ? colors.accentBg : colors.bgSurface, borderColor: isActive ? colors.accentBorder : colors.border, color: isActive ? colors.accent : colors.text }), onPointerEnter: (e) => {
20
20
  if (!isActive)
@@ -23,13 +23,7 @@ function TransformModeSelector({ transformMode, setTransformMode, snapResolution
23
23
  if (!isActive)
24
24
  e.currentTarget.style.background = colors.bgSurface;
25
25
  }, children: mode }, mode));
26
- }) }), _jsx("div", { style: { marginTop: 6 }, children: _jsxs("button", { onClick: () => setSnapResolution(snapResolution > 0 ? 0 : 0.1), style: Object.assign(Object.assign({}, buttonStyle), { background: snapResolution > 0 ? colors.accentBg : colors.bgSurface, borderColor: snapResolution > 0 ? colors.accentBorder : colors.border, color: snapResolution > 0 ? colors.accent : colors.text, width: '100%' }), onPointerEnter: (e) => {
27
- if (snapResolution === 0)
28
- e.currentTarget.style.background = colors.bgHover;
29
- }, onPointerLeave: (e) => {
30
- if (snapResolution === 0)
31
- e.currentTarget.style.background = colors.bgSurface;
32
- }, children: ["Snap: ", snapResolution > 0 ? `ON (${snapResolution})` : 'OFF'] }) })] }));
26
+ }) })] }));
33
27
  }
34
28
  const snapLockBtnStyle = {
35
29
  background: 'none',
@@ -44,11 +38,12 @@ function SnapLockButton({ locked, onToggle, title }) {
44
38
  return (_jsx("button", { style: snapLockBtnStyle, onClick: onToggle, title: title, children: locked ? '🔒' : '🔓' }));
45
39
  }
46
40
  function TransformComponentEditor({ component, onUpdate }) {
47
- var _a, _b;
48
- const { transformMode, setTransformMode, snapResolution, setSnapResolution, positionSnap, setPositionSnap, rotationSnap, setRotationSnap } = useEditorContext();
41
+ var _a, _b, _c;
42
+ const { transformMode, setTransformMode, scaleSnap, setScaleSnap, positionSnap, setPositionSnap, rotationSnap, setRotationSnap } = useEditorContext();
43
+ const scaleSnapped = scaleSnap > 0;
49
44
  const positionSnapped = positionSnap > 0;
50
45
  const rotationSnapped = rotationSnap > 0;
51
- return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column' }, children: [_jsx(TransformModeSelector, { transformMode: transformMode, setTransformMode: setTransformMode, snapResolution: snapResolution, setSnapResolution: setSnapResolution }), _jsx(Vector3Input, { label: "Position", value: (_a = component.properties.position) !== null && _a !== void 0 ? _a : [0, 0, 0], onChange: v => onUpdate({ position: v }), snap: positionSnap, labelExtra: _jsx(SnapLockButton, { locked: positionSnapped, onToggle: () => setPositionSnap(positionSnapped ? 0 : 0.5), title: positionSnapped ? `Snap ON (0.5) — click to disable` : `Snap OFF — click to enable (0.5)` }) }), _jsx(Vector3Input, { label: "Rotation", value: (_b = component.properties.rotation) !== null && _b !== void 0 ? _b : [0, 0, 0], onChange: v => onUpdate({ rotation: v }), snap: rotationSnap, labelExtra: _jsx(SnapLockButton, { locked: rotationSnapped, onToggle: () => setRotationSnap(rotationSnapped ? 0 : Math.PI / 4), title: rotationSnapped ? `Snap ON (π/4) — click to disable` : `Snap OFF — click to enable (π/4)` }) }), _jsx(Vector3Field, { name: "scale", label: "Scale", values: component.properties, onChange: onUpdate, fallback: [1, 1, 1] })] }));
46
+ return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column' }, children: [_jsx(TransformModeSelector, { transformMode: transformMode, setTransformMode: setTransformMode }), _jsx(Vector3Input, { label: "Position", value: (_a = component.properties.position) !== null && _a !== void 0 ? _a : [0, 0, 0], onChange: v => onUpdate({ position: v }), snap: positionSnap, labelExtra: _jsx(SnapLockButton, { locked: positionSnapped, onToggle: () => setPositionSnap(positionSnapped ? 0 : 0.5), title: positionSnapped ? `Snap ON (0.5) — click to disable` : `Snap OFF — click to enable (0.5)` }) }), _jsx(Vector3Input, { label: "Rotation", value: (_b = component.properties.rotation) !== null && _b !== void 0 ? _b : [0, 0, 0], onChange: v => onUpdate({ rotation: v }), snap: rotationSnap, labelExtra: _jsx(SnapLockButton, { locked: rotationSnapped, onToggle: () => setRotationSnap(rotationSnapped ? 0 : Math.PI / 4), title: rotationSnapped ? `Snap ON (π/4) — click to disable` : `Snap OFF — click to enable (π/4)` }) }), _jsx(Vector3Input, { label: "Scale", value: (_c = component.properties.scale) !== null && _c !== void 0 ? _c : [1, 1, 1], onChange: v => onUpdate({ scale: v }), snap: scaleSnap, labelExtra: _jsx(SnapLockButton, { locked: scaleSnapped, onToggle: () => setScaleSnap(scaleSnapped ? 0 : 0.1), title: scaleSnapped ? `Snap ON (0.1) — click to disable` : `Snap OFF — click to enable (0.1)` }) })] }));
52
47
  }
53
48
  const TransformComponent = {
54
49
  name: 'Transform',