react-three-game 0.0.28 → 0.0.30

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 (26) hide show
  1. package/README.md +18 -0
  2. package/assets/architecture.png +0 -0
  3. package/dist/tools/prefabeditor/EditorTree.js +30 -36
  4. package/dist/tools/prefabeditor/EditorUI.js +2 -1
  5. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +1 -1
  6. package/dist/tools/prefabeditor/components/GeometryComponent.js +1 -1
  7. package/dist/tools/prefabeditor/components/MaterialComponent.js +2 -2
  8. package/dist/tools/prefabeditor/components/ModelComponent.js +1 -1
  9. package/dist/tools/prefabeditor/components/PhysicsComponent.js +3 -3
  10. package/dist/tools/prefabeditor/components/SpotLightComponent.js +1 -1
  11. package/dist/tools/prefabeditor/components/TransformComponent.js +64 -9
  12. package/dist/tools/prefabeditor/types.d.ts +1 -0
  13. package/dist/tools/prefabeditor/utils.d.ts +1 -0
  14. package/dist/tools/prefabeditor/utils.js +20 -2
  15. package/package.json +1 -1
  16. package/src/tools/prefabeditor/EditorTree.tsx +49 -40
  17. package/src/tools/prefabeditor/EditorUI.tsx +4 -2
  18. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +35 -35
  19. package/src/tools/prefabeditor/components/GeometryComponent.tsx +2 -2
  20. package/src/tools/prefabeditor/components/MaterialComponent.tsx +19 -19
  21. package/src/tools/prefabeditor/components/ModelComponent.tsx +6 -6
  22. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +6 -6
  23. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +15 -15
  24. package/src/tools/prefabeditor/components/TransformComponent.tsx +125 -30
  25. package/src/tools/prefabeditor/types.ts +1 -0
  26. package/src/tools/prefabeditor/utils.ts +30 -0
package/README.md CHANGED
@@ -7,6 +7,7 @@ npm i react-three-game @react-three/fiber @react-three/rapier three
7
7
  ```
8
8
 
9
9
  ![Prefab Editor](assets/editor.gif)
10
+ ![Architecture](assets/architecture.png)
10
11
 
11
12
  ## Usage
12
13
 
@@ -132,3 +133,20 @@ npm run release # build + publish
132
133
  ---
133
134
 
134
135
  React 19 · Three.js WebGPU · TypeScript 5 · Rapier WASM · MIT License
136
+
137
+ ## Manifest generation script
138
+
139
+ A small helper script is included to auto-generate asset manifests from the `public` folder. See `docs/generate-manifests.sh`.
140
+
141
+ - What it does: searches `public/models` for `.glb`/`.fbx`, `public/textures` for `.jpg`/`.png`, and `public/sound` for `.mp3`/`.wav`, then writes JSON arrays to `public/models/manifest.json`, `public/textures/manifest.json`, and `public/sound/manifest.json`. These manifest files are used top populate the Asset Viewer in the the Editor.
142
+ - How to run:
143
+
144
+ 1. Make it executable (once):
145
+
146
+ chmod +x docs/generate-manifests.sh
147
+
148
+ 2. Run the script from the repo root (zsh/bash):
149
+
150
+ ./docs/generate-manifests.sh
151
+
152
+ The script is intentionally simple and portable (uses `find`/`sed`). If you need different file types or output formatting, edit `docs/generate-manifests.sh`.
Binary file
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useState } from 'react';
3
3
  import { getComponent } from './components/ComponentRegistry';
4
4
  import { base, tree, menu } from './styles';
5
- import { findNode, findParent, deleteNode, cloneNode } from './utils';
5
+ import { findNode, findParent, deleteNode, cloneNode, updateNodeById } from './utils';
6
6
  export default function EditorTree({ prefabData, setPrefabData, selectedId, setSelectedId }) {
7
7
  const [contextMenu, setContextMenu] = useState(null);
8
8
  const [draggedId, setDraggedId] = useState(null);
@@ -28,6 +28,7 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
28
28
  var _a;
29
29
  const newNode = {
30
30
  id: crypto.randomUUID(),
31
+ name: "New Node",
31
32
  components: {
32
33
  transform: {
33
34
  type: "Transform",
@@ -35,30 +36,25 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
35
36
  }
36
37
  }
37
38
  };
38
- setPrefabData(prev => {
39
- const newRoot = JSON.parse(JSON.stringify(prev.root));
40
- const parent = findNode(newRoot, parentId);
41
- if (parent) {
42
- parent.children = parent.children || [];
43
- parent.children.push(newNode);
44
- }
45
- return Object.assign(Object.assign({}, prev), { root: newRoot });
46
- });
39
+ setPrefabData(prev => (Object.assign(Object.assign({}, prev), { root: updateNodeById(prev.root, parentId, parent => {
40
+ var _a;
41
+ return (Object.assign(Object.assign({}, parent), { children: [...((_a = parent.children) !== null && _a !== void 0 ? _a : []), newNode] }));
42
+ }) })));
47
43
  setContextMenu(null);
48
44
  };
49
45
  const handleDuplicate = (nodeId) => {
50
46
  if (nodeId === prefabData.root.id)
51
47
  return;
52
48
  setPrefabData(prev => {
53
- const newRoot = JSON.parse(JSON.stringify(prev.root));
54
- const parent = findParent(newRoot, nodeId);
55
- const node = findNode(newRoot, nodeId);
56
- if (parent && node) {
57
- const clone = cloneNode(node);
58
- parent.children = parent.children || [];
59
- parent.children.push(clone);
60
- }
61
- return Object.assign(Object.assign({}, prev), { root: newRoot });
49
+ const node = findNode(prev.root, nodeId);
50
+ const parent = findParent(prev.root, nodeId);
51
+ if (!node || !parent)
52
+ return prev;
53
+ const clone = cloneNode(node);
54
+ return Object.assign(Object.assign({}, prev), { root: updateNodeById(prev.root, parent.id, p => {
55
+ var _a;
56
+ return (Object.assign(Object.assign({}, p), { children: [...((_a = p.children) !== null && _a !== void 0 ? _a : []), clone] }));
57
+ }) });
62
58
  });
63
59
  setContextMenu(null);
64
60
  };
@@ -92,28 +88,26 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
92
88
  return;
93
89
  e.preventDefault();
94
90
  setPrefabData(prev => {
95
- var _a;
96
- const newRoot = JSON.parse(JSON.stringify(prev.root));
97
- const draggedNode = findNode(newRoot, draggedId);
98
- if (draggedNode && findNode(draggedNode, targetId))
99
- return prev;
100
- const parent = findParent(newRoot, draggedId);
101
- if (!parent)
91
+ const draggedNode = findNode(prev.root, draggedId);
92
+ const oldParent = findParent(prev.root, draggedId);
93
+ if (!draggedNode || !oldParent)
102
94
  return prev;
103
- const nodeToMove = (_a = parent.children) === null || _a === void 0 ? void 0 : _a.find(c => c.id === draggedId);
104
- if (!nodeToMove)
95
+ // Prevent dropping into own subtree
96
+ if (findNode(draggedNode, targetId))
105
97
  return prev;
106
- parent.children = parent.children.filter(c => c.id !== draggedId);
107
- const target = findNode(newRoot, targetId);
108
- if (target) {
109
- target.children = target.children || [];
110
- target.children.push(nodeToMove);
111
- }
112
- return Object.assign(Object.assign({}, prev), { root: newRoot });
98
+ // 1. Remove from old parent
99
+ let root = updateNodeById(prev.root, oldParent.id, p => (Object.assign(Object.assign({}, p), { children: p.children.filter(c => c.id !== draggedId) })));
100
+ // 2. Add to new parent
101
+ root = updateNodeById(root, targetId, t => {
102
+ var _a;
103
+ return (Object.assign(Object.assign({}, t), { children: [...((_a = t.children) !== null && _a !== void 0 ? _a : []), draggedNode] }));
104
+ });
105
+ return Object.assign(Object.assign({}, prev), { root });
113
106
  });
114
107
  setDraggedId(null);
115
108
  };
116
109
  const renderNode = (node, depth = 0) => {
110
+ var _a;
117
111
  if (!node)
118
112
  return null;
119
113
  const isSelected = node.id === selectedId;
@@ -126,7 +120,7 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
126
120
  marginRight: 4,
127
121
  cursor: 'pointer',
128
122
  visibility: hasChildren ? 'visible' : 'hidden'
129
- }, onClick: (e) => hasChildren && toggleCollapse(e, node.id), children: isCollapsed ? '▶' : '▼' }), !isRoot && _jsx("span", { style: { marginRight: 4, opacity: 0.4 }, children: "\u22EE\u22EE" }), _jsx("span", { style: { overflow: 'hidden', textOverflow: 'ellipsis' }, children: node.id })] }), !isCollapsed && node.children && node.children.map(child => renderNode(child, depth + 1))] }, node.id));
123
+ }, onClick: (e) => hasChildren && toggleCollapse(e, node.id), children: isCollapsed ? '▶' : '▼' }), !isRoot && _jsx("span", { style: { marginRight: 4, opacity: 0.4 }, children: "\u22EE\u22EE" }), _jsx("span", { style: { overflow: 'hidden', textOverflow: 'ellipsis' }, children: (_a = node.name) !== null && _a !== void 0 ? _a : node.id })] }), !isCollapsed && node.children && node.children.map(child => renderNode(child, depth + 1))] }, node.id));
130
124
  };
131
125
  return (_jsxs(_Fragment, { children: [_jsxs("div", { style: Object.assign(Object.assign({}, tree.panel), { width: collapsed ? 'auto' : 224 }), onClick: () => setContextMenu(null), children: [_jsxs("div", { style: base.header, onClick: () => setCollapsed(!collapsed), children: [_jsx("span", { children: "Scene" }), _jsx("span", { children: collapsed ? '▶' : '◀' })] }), !collapsed && _jsx("div", { style: tree.scroll, children: renderNode(prefabData.root) })] }), contextMenu && (_jsxs("div", { style: Object.assign(Object.assign({}, menu.container), { top: contextMenu.y, left: contextMenu.x }), onClick: (e) => e.stopPropagation(), onPointerLeave: () => setContextMenu(null), children: [_jsx("button", { style: menu.item, onClick: () => handleAddChild(contextMenu.nodeId), children: "Add Child" }), contextMenu.nodeId !== prefabData.root.id && (_jsxs(_Fragment, { children: [_jsx("button", { style: menu.item, onClick: () => handleDuplicate(contextMenu.nodeId), children: "Duplicate" }), _jsx("button", { style: Object.assign(Object.assign({}, menu.item), menu.danger), onClick: () => handleDelete(contextMenu.nodeId), children: "Delete" })] }))] }))] }));
132
126
  }
@@ -32,6 +32,7 @@ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transf
32
32
  return _jsxs(_Fragment, { children: [_jsxs("div", { style: inspector.panel, children: [_jsxs("div", { style: base.header, onClick: () => setCollapsed(!collapsed), children: [_jsx("span", { children: "Inspector" }), _jsx("span", { children: collapsed ? '◀' : '▼' })] }), !collapsed && selectedNode && (_jsx(NodeInspector, { node: selectedNode, updateNode: updateNodeHandler, deleteNode: deleteNodeHandler, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath }))] }), _jsx("div", { style: { position: 'absolute', top: 8, left: 8, zIndex: 20 }, children: _jsx(EditorTree, { prefabData: prefabData, setPrefabData: setPrefabData, selectedId: selectedId, setSelectedId: setSelectedId }) })] });
33
33
  }
34
34
  function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransformMode, basePath }) {
35
+ var _a;
35
36
  const ALL_COMPONENTS = getAllComponents();
36
37
  const allKeys = Object.keys(ALL_COMPONENTS);
37
38
  const available = allKeys.filter(k => { var _a; return !((_a = node.components) === null || _a === void 0 ? void 0 : _a[k.toLowerCase()]); });
@@ -41,7 +42,7 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
41
42
  if (!newAvailable.includes(addType))
42
43
  setAddType(newAvailable[0] || "");
43
44
  }, [Object.keys(node.components || {}).join(',')]);
44
- return _jsxs("div", { style: inspector.content, children: [_jsxs("div", { style: base.section, children: [_jsx("div", { style: base.label, children: "Node ID" }), _jsx("input", { style: base.input, value: node.id, onChange: e => updateNode(n => (Object.assign(Object.assign({}, n), { id: e.target.value }))) })] }), _jsxs("div", { style: base.section, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }, children: [_jsx("div", { style: base.label, children: "Components" }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), base.btnDanger), onClick: deleteNode, children: "Delete Node" })] }), node.components && Object.entries(node.components).map(([key, comp]) => {
45
+ return _jsxs("div", { style: inspector.content, children: [_jsxs("div", { style: base.section, children: [_jsx("div", { style: base.label, children: "Node ID" }), _jsx("input", { style: base.input, value: (_a = node.name) !== null && _a !== void 0 ? _a : "", onChange: e => updateNode(n => (Object.assign(Object.assign({}, n), { name: e.target.value }))) })] }), _jsxs("div", { style: base.section, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }, children: [_jsx("div", { style: base.label, children: "Components" }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), base.btnDanger), onClick: deleteNode, children: "Delete Node" })] }), node.components && Object.entries(node.components).map(([key, comp]) => {
45
46
  if (!comp)
46
47
  return null;
47
48
  const def = ALL_COMPONENTS[comp.type];
@@ -17,7 +17,7 @@ function DirectionalLightComponentEditor({ component, onUpdate }) {
17
17
  shadowCameraRight: (_k = component.properties.shadowCameraRight) !== null && _k !== void 0 ? _k : 30,
18
18
  targetOffset: (_l = component.properties.targetOffset) !== null && _l !== void 0 ? _l : [0, -5, 0]
19
19
  };
20
- return _jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Color" }), _jsxs("div", { className: "flex gap-0.5", children: [_jsx("input", { type: "color", className: "h-5 w-5 bg-transparent border-none cursor-pointer", value: props.color, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'color': e.target.value })) }), _jsx("input", { type: "text", className: "flex-1 bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.color, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'color': e.target.value })) })] })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Intensity" }), _jsx("input", { type: "number", step: "0.1", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.intensity, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'intensity': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Cast Shadow" }), _jsx("input", { type: "checkbox", className: "h-4 w-4 bg-black/40 border border-cyan-500/30 cursor-pointer", checked: props.castShadow, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'castShadow': e.target.checked })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Shadow Map Size" }), _jsx("input", { type: "number", step: "256", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.shadowMapSize, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowMapSize': parseFloat(e.target.value) })) })] }), _jsxs("div", { className: "border-t border-cyan-500/20 pt-2 mt-2", children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-1", children: "Shadow Camera" }), _jsxs("div", { className: "grid grid-cols-2 gap-1", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "Near" }), _jsx("input", { type: "number", step: "0.1", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.shadowCameraNear, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraNear': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "Far" }), _jsx("input", { type: "number", step: "1", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.shadowCameraFar, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraFar': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "Top" }), _jsx("input", { type: "number", step: "1", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.shadowCameraTop, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraTop': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "Bottom" }), _jsx("input", { type: "number", step: "1", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.shadowCameraBottom, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraBottom': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "Left" }), _jsx("input", { type: "number", step: "1", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.shadowCameraLeft, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraLeft': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "Right" }), _jsx("input", { type: "number", step: "1", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.shadowCameraRight, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraRight': parseFloat(e.target.value) })) })] })] })] }), _jsxs("div", { className: "border-t border-cyan-500/20 pt-2 mt-2", children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-1", children: "Target Offset" }), _jsxs("div", { className: "grid grid-cols-3 gap-1", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "X" }), _jsx("input", { type: "number", step: "0.5", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.targetOffset[0], onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'targetOffset': [parseFloat(e.target.value), props.targetOffset[1], props.targetOffset[2]] })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "Y" }), _jsx("input", { type: "number", step: "0.5", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.targetOffset[1], onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'targetOffset': [props.targetOffset[0], parseFloat(e.target.value), props.targetOffset[2]] })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "Z" }), _jsx("input", { type: "number", step: "0.5", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.targetOffset[2], onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'targetOffset': [props.targetOffset[0], props.targetOffset[1], parseFloat(e.target.value)] })) })] })] })] })] });
20
+ return _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 8 }, children: [_jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Color" }), _jsxs("div", { style: { display: 'flex', gap: 2 }, children: [_jsx("input", { type: "color", style: { height: 20, width: 20, backgroundColor: 'transparent', border: 'none', cursor: 'pointer' }, value: props.color, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'color': e.target.value })) }), _jsx("input", { type: "text", style: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.color, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'color': e.target.value })) })] })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Intensity" }), _jsx("input", { type: "number", step: "0.1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.intensity, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'intensity': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Cast Shadow" }), _jsx("input", { type: "checkbox", style: { height: 16, width: 16, backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', cursor: 'pointer' }, checked: props.castShadow, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'castShadow': e.target.checked })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Shadow Map Size" }), _jsx("input", { type: "number", step: "256", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.shadowMapSize, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowMapSize': parseFloat(e.target.value) })) })] }), _jsxs("div", { style: { borderTop: '1px solid rgba(34, 211, 238, 0.2)', paddingTop: 8, marginTop: 8 }, children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 4 }, children: "Shadow Camera" }), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4 }, children: [_jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "Near" }), _jsx("input", { type: "number", step: "0.1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.shadowCameraNear, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraNear': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "Far" }), _jsx("input", { type: "number", step: "1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.shadowCameraFar, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraFar': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "Top" }), _jsx("input", { type: "number", step: "1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.shadowCameraTop, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraTop': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "Bottom" }), _jsx("input", { type: "number", step: "1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.shadowCameraBottom, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraBottom': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "Left" }), _jsx("input", { type: "number", step: "1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.shadowCameraLeft, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraLeft': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "Right" }), _jsx("input", { type: "number", step: "1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.shadowCameraRight, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraRight': parseFloat(e.target.value) })) })] })] })] }), _jsxs("div", { style: { borderTop: '1px solid rgba(34, 211, 238, 0.2)', paddingTop: 8, marginTop: 8 }, children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 4 }, children: "Target Offset" }), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 4 }, children: [_jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "X" }), _jsx("input", { type: "number", step: "0.5", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.targetOffset[0], onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'targetOffset': [parseFloat(e.target.value), props.targetOffset[1], props.targetOffset[2]] })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "Y" }), _jsx("input", { type: "number", step: "0.5", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.targetOffset[1], onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'targetOffset': [props.targetOffset[0], parseFloat(e.target.value), props.targetOffset[2]] })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "Z" }), _jsx("input", { type: "number", step: "0.5", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.targetOffset[2], onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'targetOffset': [props.targetOffset[0], props.targetOffset[1], parseFloat(e.target.value)] })) })] })] })] })] });
21
21
  }
22
22
  function DirectionalLightView({ properties, editMode }) {
23
23
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  function GeometryComponentEditor({ component, onUpdate }) {
3
- return _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Type" }), _jsxs("select", { className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: component.properties.geometryType, onChange: e => onUpdate({ geometryType: e.target.value }), children: [_jsx("option", { value: "box", children: "Box" }), _jsx("option", { value: "sphere", children: "Sphere" }), _jsx("option", { value: "plane", children: "Plane" })] })] });
3
+ return _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Type" }), _jsxs("select", { style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: component.properties.geometryType, onChange: e => onUpdate({ geometryType: e.target.value }), children: [_jsx("option", { value: "box", children: "Box" }), _jsx("option", { value: "sphere", children: "Sphere" }), _jsx("option", { value: "plane", children: "Plane" })] })] });
4
4
  }
5
5
  // View for Geometry component
6
6
  function GeometryComponentView({ properties, children }) {
@@ -11,11 +11,11 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
11
11
  .then(data => setTextureFiles(Array.isArray(data) ? data : data.files || []))
12
12
  .catch(console.error);
13
13
  }, [basePath]);
14
- return (_jsxs("div", { className: "flex flex-col", children: [_jsxs("div", { className: "mb-1", children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Color" }), _jsxs("div", { className: "flex gap-0.5", children: [_jsx("input", { type: "color", className: "h-5 w-5 bg-transparent border-none cursor-pointer", value: component.properties.color, onChange: e => onUpdate({ 'color': e.target.value }) }), _jsx("input", { type: "text", className: "flex-1 bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: component.properties.color, onChange: e => onUpdate({ 'color': e.target.value }) })] })] }), _jsxs("div", { className: "flex items-center gap-1 mb-1", children: [_jsx("input", { type: "checkbox", className: "w-3 h-3", checked: component.properties.wireframe || false, onChange: e => onUpdate({ 'wireframe': e.target.checked }) }), _jsx("label", { className: "text-[9px] text-cyan-400/60", children: "Wireframe" })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Texture" }), _jsx("div", { className: "max-h-32 overflow-y-auto", children: _jsx(TextureListViewer, { files: textureFiles, selected: component.properties.texture || undefined, onSelect: (file) => onUpdate({ 'texture': file }), basePath: basePath }) })] }), component.properties.texture && (_jsxs("div", { className: "border-t border-cyan-500/20 pt-1 mt-1", children: [_jsxs("div", { className: "flex items-center gap-1 mb-1", children: [_jsx("input", { type: "checkbox", className: "w-3 h-3", checked: component.properties.repeat || false, onChange: e => onUpdate({ 'repeat': e.target.checked }) }), _jsx("label", { className: "text-[9px] text-cyan-400/60", children: "Repeat Texture" })] }), component.properties.repeat && (_jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Repeat (X, Y)" }), _jsxs("div", { className: "flex gap-0.5", children: [_jsx("input", { type: "number", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: (_b = (_a = component.properties.repeatCount) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : 1, onChange: e => {
14
+ return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column' }, children: [_jsxs("div", { style: { marginBottom: 4 }, children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Color" }), _jsxs("div", { style: { display: 'flex', gap: 2 }, children: [_jsx("input", { type: "color", style: { height: 20, width: 20, backgroundColor: 'transparent', border: 'none', cursor: 'pointer' }, value: component.properties.color, onChange: e => onUpdate({ 'color': e.target.value }) }), _jsx("input", { type: "text", style: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: component.properties.color, onChange: e => onUpdate({ 'color': e.target.value }) })] })] }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4, marginBottom: 4 }, children: [_jsx("input", { type: "checkbox", style: { width: 12, height: 12 }, checked: component.properties.wireframe || false, onChange: e => onUpdate({ 'wireframe': e.target.checked }) }), _jsx("label", { style: { fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)' }, children: "Wireframe" })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Texture" }), _jsx("div", { style: { maxHeight: 128, overflowY: 'auto' }, children: _jsx(TextureListViewer, { files: textureFiles, selected: component.properties.texture || undefined, onSelect: (file) => onUpdate({ 'texture': file }), basePath: basePath }) })] }), component.properties.texture && (_jsxs("div", { style: { borderTop: '1px solid rgba(34, 211, 238, 0.2)', paddingTop: 4, marginTop: 4 }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4, marginBottom: 4 }, children: [_jsx("input", { type: "checkbox", style: { width: 12, height: 12 }, checked: component.properties.repeat || false, onChange: e => onUpdate({ 'repeat': e.target.checked }) }), _jsx("label", { style: { fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)' }, children: "Repeat Texture" })] }), component.properties.repeat && (_jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Repeat (X, Y)" }), _jsxs("div", { style: { display: 'flex', gap: 2 }, children: [_jsx("input", { type: "number", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: (_b = (_a = component.properties.repeatCount) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : 1, onChange: e => {
15
15
  var _a, _b;
16
16
  const y = (_b = (_a = component.properties.repeatCount) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : 1;
17
17
  onUpdate({ 'repeatCount': [parseFloat(e.target.value), y] });
18
- } }), _jsx("input", { type: "number", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: (_d = (_c = component.properties.repeatCount) === null || _c === void 0 ? void 0 : _c[1]) !== null && _d !== void 0 ? _d : 1, onChange: e => {
18
+ } }), _jsx("input", { type: "number", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: (_d = (_c = component.properties.repeatCount) === null || _c === void 0 ? void 0 : _c[1]) !== null && _d !== void 0 ? _d : 1, onChange: e => {
19
19
  var _a, _b;
20
20
  const x = (_b = (_a = component.properties.repeatCount) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : 1;
21
21
  onUpdate({ 'repeatCount': [x, parseFloat(e.target.value)] });
@@ -15,7 +15,7 @@ function ModelComponentEditor({ component, onUpdate, basePath = "" }) {
15
15
  const filename = file.startsWith('/') ? file.slice(1) : file;
16
16
  onUpdate({ 'filename': filename });
17
17
  };
18
- return _jsxs("div", { children: [_jsxs("div", { className: "mb-1", children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Model" }), _jsx("div", { className: "max-h-32 overflow-y-auto", children: _jsx(ModelListViewer, { files: modelFiles, selected: component.properties.filename ? `/${component.properties.filename}` : undefined, onSelect: handleModelSelect, basePath: basePath }) })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("input", { type: "checkbox", id: "instanced-checkbox", checked: component.properties.instanced || false, onChange: e => onUpdate({ 'instanced': e.target.checked }), className: "w-3 h-3" }), _jsx("label", { htmlFor: "instanced-checkbox", className: "text-[9px] text-cyan-400/60", children: "Instanced" })] })] });
18
+ return _jsxs("div", { children: [_jsxs("div", { style: { marginBottom: 4 }, children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Model" }), _jsx("div", { style: { maxHeight: 128, overflowY: 'auto' }, children: _jsx(ModelListViewer, { files: modelFiles, selected: component.properties.filename ? `/${component.properties.filename}` : undefined, onSelect: handleModelSelect, basePath: basePath }) })] }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4 }, children: [_jsx("input", { type: "checkbox", id: "instanced-checkbox", checked: component.properties.instanced || false, onChange: e => onUpdate({ 'instanced': e.target.checked }), style: { width: 12, height: 12 } }), _jsx("label", { htmlFor: "instanced-checkbox", style: { fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)' }, children: "Instanced" })] })] });
19
19
  }
20
20
  // View for Model component
21
21
  function ModelComponentView({ properties, loadedModels, children }) {
@@ -1,10 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { RigidBody } from "@react-three/rapier";
3
- const selectClass = "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50";
4
- const labelClass = "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5";
3
+ const selectClass = { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' };
4
+ const labelClass = { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 };
5
5
  function PhysicsComponentEditor({ component, onUpdate }) {
6
6
  const { type = 'dynamic', collider = 'hull' } = component.properties;
7
- return (_jsxs("div", { children: [_jsx("label", { className: labelClass, children: "Type" }), _jsxs("select", { className: selectClass, value: type, onChange: e => onUpdate({ type: e.target.value }), children: [_jsx("option", { value: "dynamic", children: "Dynamic" }), _jsx("option", { value: "fixed", children: "Fixed" })] }), _jsx("label", { className: `${labelClass} mt-2`, children: "Collider" }), _jsxs("select", { className: selectClass, value: collider, onChange: e => onUpdate({ collider: e.target.value }), children: [_jsx("option", { value: "hull", children: "Hull (convex)" }), _jsx("option", { value: "trimesh", children: "Trimesh (exact)" }), _jsx("option", { value: "cuboid", children: "Cuboid (box)" }), _jsx("option", { value: "ball", children: "Ball (sphere)" })] })] }));
7
+ return (_jsxs("div", { children: [_jsx("label", { style: labelClass, children: "Type" }), _jsxs("select", { style: selectClass, value: type, onChange: e => onUpdate({ type: e.target.value }), children: [_jsx("option", { value: "dynamic", children: "Dynamic" }), _jsx("option", { value: "fixed", children: "Fixed" })] }), _jsx("label", { style: Object.assign(Object.assign({}, labelClass), { marginTop: 8 }), children: "Collider" }), _jsxs("select", { style: selectClass, value: collider, onChange: e => onUpdate({ collider: e.target.value }), children: [_jsx("option", { value: "hull", children: "Hull (convex)" }), _jsx("option", { value: "trimesh", children: "Trimesh (exact)" }), _jsx("option", { value: "cuboid", children: "Cuboid (box)" }), _jsx("option", { value: "ball", children: "Ball (sphere)" })] })] }));
8
8
  }
9
9
  function PhysicsComponentView({ properties, editMode, children }) {
10
10
  if (editMode)
@@ -10,7 +10,7 @@ function SpotLightComponentEditor({ component, onUpdate }) {
10
10
  distance: (_e = component.properties.distance) !== null && _e !== void 0 ? _e : 100,
11
11
  castShadow: (_f = component.properties.castShadow) !== null && _f !== void 0 ? _f : true
12
12
  };
13
- return _jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Color" }), _jsxs("div", { className: "flex gap-0.5", children: [_jsx("input", { type: "color", className: "h-5 w-5 bg-transparent border-none cursor-pointer", value: props.color, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'color': e.target.value })) }), _jsx("input", { type: "text", className: "flex-1 bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.color, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'color': e.target.value })) })] })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Intensity" }), _jsx("input", { type: "number", step: "0.1", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.intensity, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'intensity': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Angle" }), _jsx("input", { type: "number", step: "0.1", min: "0", max: Math.PI, className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.angle, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'angle': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Penumbra" }), _jsx("input", { type: "number", step: "0.1", min: "0", max: "1", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.penumbra, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'penumbra': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Distance" }), _jsx("input", { type: "number", step: "1", min: "0", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.distance, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'distance': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Cast Shadow" }), _jsx("input", { type: "checkbox", className: "h-4 w-4 bg-black/40 border border-cyan-500/30 cursor-pointer", checked: props.castShadow, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'castShadow': e.target.checked })) })] })] });
13
+ return _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 8 }, children: [_jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Color" }), _jsxs("div", { style: { display: 'flex', gap: 2 }, children: [_jsx("input", { type: "color", style: { height: 20, width: 20, backgroundColor: 'transparent', border: 'none', cursor: 'pointer' }, value: props.color, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'color': e.target.value })) }), _jsx("input", { type: "text", style: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.color, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'color': e.target.value })) })] })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Intensity" }), _jsx("input", { type: "number", step: "0.1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.intensity, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'intensity': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Angle" }), _jsx("input", { type: "number", step: "0.1", min: "0", max: Math.PI, style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.angle, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'angle': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Penumbra" }), _jsx("input", { type: "number", step: "0.1", min: "0", max: "1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.penumbra, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'penumbra': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Distance" }), _jsx("input", { type: "number", step: "1", min: "0", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.distance, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'distance': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Cast Shadow" }), _jsx("input", { type: "checkbox", style: { height: 16, width: 16, backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', cursor: 'pointer' }, checked: props.castShadow, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'castShadow': e.target.checked })) })] })] });
14
14
  }
15
15
  function SpotLightView({ properties, editMode }) {
16
16
  var _a, _b, _c, _d, _e, _f;
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
2
3
  function TransformComponentEditor({ component, onUpdate, transformMode, setTransformMode }) {
3
4
  const s = {
4
5
  button: {
@@ -14,7 +15,7 @@ function TransformComponentEditor({ component, onUpdate, transformMode, setTrans
14
15
  background: 'rgba(255,255,255,0.10)',
15
16
  },
16
17
  };
17
- return _jsxs("div", { className: "flex flex-col", children: [transformMode && setTransformMode && (_jsxs("div", { className: "mb-2", children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-1", children: "Transform Mode" }), _jsx("div", { style: { display: 'flex', gap: 6 }, children: ["translate", "rotate", "scale"].map(mode => (_jsx("button", { onClick: () => setTransformMode(mode), style: Object.assign(Object.assign(Object.assign({}, s.button), { flex: 1 }), (transformMode === mode ? s.buttonActive : {})), onPointerEnter: (e) => {
18
+ return _jsxs("div", { style: { display: 'flex', flexDirection: 'column' }, children: [transformMode && setTransformMode && (_jsxs("div", { style: { marginBottom: 8 }, children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 4 }, children: "Transform Mode" }), _jsx("div", { style: { display: 'flex', gap: 6 }, children: ["translate", "rotate", "scale"].map(mode => (_jsx("button", { onClick: () => setTransformMode(mode), style: Object.assign(Object.assign(Object.assign({}, s.button), { flex: 1 }), (transformMode === mode ? s.buttonActive : {})), onPointerEnter: (e) => {
18
19
  if (transformMode !== mode)
19
20
  e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
20
21
  }, onPointerLeave: (e) => {
@@ -33,15 +34,69 @@ const TransformComponent = {
33
34
  };
34
35
  export default TransformComponent;
35
36
  export function Vector3Input({ label, value, onChange }) {
36
- const handleChange = (index, val) => {
37
- const newValue = [...value];
38
- newValue[index] = parseFloat(val) || 0;
39
- onChange(newValue);
37
+ const [draft, setDraft] = useState(() => value.map(v => v.toString()));
38
+ // Sync external changes (gizmo, undo, etc.)
39
+ useEffect(() => {
40
+ setDraft(value.map(v => v.toString()));
41
+ }, [value[0], value[1], value[2]]);
42
+ const dragState = useRef(null);
43
+ const commit = (index) => {
44
+ const num = parseFloat(draft[index]);
45
+ if (Number.isFinite(num)) {
46
+ const next = [...value];
47
+ next[index] = num;
48
+ onChange(next);
49
+ }
50
+ };
51
+ const startScrub = (e, index) => {
52
+ e.preventDefault();
53
+ dragState.current = {
54
+ index,
55
+ startX: e.clientX,
56
+ startValue: value[index]
57
+ };
58
+ e.target.setPointerCapture(e.pointerId);
59
+ document.body.style.cursor = "ew-resize";
60
+ };
61
+ const onScrubMove = (e) => {
62
+ if (!dragState.current)
63
+ return;
64
+ const { index, startX, startValue } = dragState.current;
65
+ const dx = e.clientX - startX;
66
+ let speed = 0.02;
67
+ if (e.shiftKey)
68
+ speed *= 0.1; // fine
69
+ if (e.altKey)
70
+ speed *= 5; // coarse
71
+ const nextValue = startValue + dx * speed;
72
+ const next = [...value];
73
+ next[index] = nextValue;
74
+ setDraft(d => {
75
+ const copy = [...d];
76
+ copy[index] = nextValue.toFixed(3);
77
+ return copy;
78
+ });
79
+ onChange(next);
80
+ };
81
+ const endScrub = (e) => {
82
+ if (!dragState.current)
83
+ return;
84
+ dragState.current = null;
85
+ document.body.style.cursor = "";
86
+ e.target.releasePointerCapture(e.pointerId);
40
87
  };
41
88
  const axes = [
42
- { key: 'x', color: 'red', index: 0 },
43
- { key: 'y', color: 'green', index: 1 },
44
- { key: 'z', color: 'blue', index: 2 }
89
+ { key: "x", color: "red", index: 0 },
90
+ { key: "y", color: "green", index: 1 },
91
+ { key: "z", color: "blue", index: 2 }
45
92
  ];
46
- return _jsxs("div", { className: "mb-2", children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-1", children: label }), _jsx("div", { className: "flex gap-1", children: axes.map(({ key, color, index }) => (_jsxs("div", { className: "flex-1 flex items-center gap-1 bg-black/30 border border-cyan-500/20 rounded px-1.5 py-1 min-h-[32px]", children: [_jsx("span", { className: `text-xs font-bold text-${color}-400 w-3`, children: key.toUpperCase() }), _jsx("input", { className: "flex-1 bg-transparent text-xs text-cyan-200 font-mono outline-none w-full min-w-0", type: "number", step: "0.1", value: value[index].toFixed(2), onChange: e => handleChange(index, e.target.value), onFocus: e => e.target.select() })] }, key))) })] });
93
+ return (_jsxs("div", { style: { marginBottom: 8 }, children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 4 }, children: label }), _jsx("div", { style: { display: 'flex', gap: 4 }, children: axes.map(({ key, color, index }) => (_jsxs("div", { style: { flex: 1, display: 'flex', alignItems: 'center', gap: 4, backgroundColor: 'rgba(0, 0, 0, 0.3)', border: '1px solid rgba(34, 211, 238, 0.2)', borderRadius: 4, padding: '4px 6px', minHeight: 32 }, children: [_jsx("span", { style: { fontSize: '12px', fontWeight: 'bold', color: color === 'red' ? 'rgba(248, 113, 113, 1)' : color === 'green' ? 'rgba(134, 239, 172, 1)' : 'rgba(96, 165, 250, 1)', width: 12, cursor: 'ew-resize', userSelect: 'none' }, onPointerDown: e => startScrub(e, index), onPointerMove: onScrubMove, onPointerUp: endScrub, children: key.toUpperCase() }), _jsx("input", { style: { flex: 1, backgroundColor: 'transparent', fontSize: '12px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none', width: '100%', minWidth: 0 }, type: "text", value: draft[index], onChange: e => {
94
+ const next = [...draft];
95
+ next[index] = e.target.value;
96
+ setDraft(next);
97
+ }, onBlur: () => commit(index), onKeyDown: e => {
98
+ if (e.key === "Enter") {
99
+ e.target.blur();
100
+ }
101
+ } })] }, key))) })] }));
47
102
  }
@@ -5,6 +5,7 @@ export interface Prefab {
5
5
  }
6
6
  export interface GameObject {
7
7
  id: string;
8
+ name?: string;
8
9
  disabled?: boolean;
9
10
  hidden?: boolean;
10
11
  children?: GameObject[];
@@ -17,3 +17,4 @@ export declare function deleteNode(root: GameObject, id: string): GameObject | n
17
17
  export declare function cloneNode(node: GameObject): GameObject;
18
18
  /** Get component data from a node */
19
19
  export declare function getComponent<T = any>(node: GameObject, type: string): T | undefined;
20
+ export declare function updateNodeById(root: GameObject, id: string, updater: (node: GameObject) => GameObject): GameObject;
@@ -61,8 +61,8 @@ export function deleteNode(root, id) {
61
61
  }
62
62
  /** Deep clone a node with new IDs */
63
63
  export function cloneNode(node) {
64
- var _a;
65
- return Object.assign(Object.assign({}, node), { id: crypto.randomUUID(), children: (_a = node.children) === null || _a === void 0 ? void 0 : _a.map(cloneNode) });
64
+ var _a, _b;
65
+ return Object.assign(Object.assign({}, node), { id: crypto.randomUUID(), name: `${(_a = node.name) !== null && _a !== void 0 ? _a : "Node"} Copy`, children: (_b = node.children) === null || _b === void 0 ? void 0 : _b.map(cloneNode) });
66
66
  }
67
67
  /** Get component data from a node */
68
68
  export function getComponent(node, type) {
@@ -70,3 +70,21 @@ export function getComponent(node, type) {
70
70
  const comp = Object.values((_a = node.components) !== null && _a !== void 0 ? _a : {}).find(c => (c === null || c === void 0 ? void 0 : c.type) === type);
71
71
  return comp === null || comp === void 0 ? void 0 : comp.properties;
72
72
  }
73
+ export function updateNodeById(root, id, updater) {
74
+ if (root.id === id) {
75
+ return updater(root);
76
+ }
77
+ if (!root.children) {
78
+ return root;
79
+ }
80
+ let didChange = false;
81
+ const newChildren = root.children.map(child => {
82
+ const updated = updateNodeById(child, id, updater);
83
+ if (updated !== child)
84
+ didChange = true;
85
+ return updated;
86
+ });
87
+ if (!didChange)
88
+ return root;
89
+ return Object.assign(Object.assign({}, root), { children: newChildren });
90
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-three-game",
3
- "version": "0.0.28",
3
+ "version": "0.0.30",
4
4
  "description": "Batteries included React Three Fiber game engine",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",