react-three-game 0.0.14 → 0.0.16

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.
@@ -4,6 +4,46 @@ import EditorTree from './EditorTree';
4
4
  import { getAllComponents } from './components/ComponentRegistry';
5
5
  function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transformMode, setTransformMode, basePath }) {
6
6
  const [isInspectorCollapsed, setIsInspectorCollapsed] = useState(false);
7
+ const ui = {
8
+ panel: {
9
+ position: 'absolute',
10
+ top: 8,
11
+ right: 8,
12
+ zIndex: 20,
13
+ width: 260,
14
+ background: 'rgba(0,0,0,0.55)',
15
+ color: 'rgba(255,255,255,0.9)',
16
+ border: '1px solid rgba(255,255,255,0.12)',
17
+ borderRadius: 6,
18
+ overflow: 'hidden',
19
+ backdropFilter: 'blur(6px)',
20
+ WebkitBackdropFilter: 'blur(6px)',
21
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
22
+ fontSize: 11,
23
+ lineHeight: 1.2,
24
+ },
25
+ header: {
26
+ padding: '4px 6px',
27
+ display: 'flex',
28
+ alignItems: 'center',
29
+ justifyContent: 'space-between',
30
+ cursor: 'pointer',
31
+ background: 'rgba(255,255,255,0.05)',
32
+ borderBottom: '1px solid rgba(255,255,255,0.10)',
33
+ textTransform: 'uppercase',
34
+ letterSpacing: '0.08em',
35
+ fontSize: 10,
36
+ color: 'rgba(255,255,255,0.7)',
37
+ userSelect: 'none',
38
+ WebkitUserSelect: 'none',
39
+ },
40
+ left: {
41
+ position: 'absolute',
42
+ top: 8,
43
+ left: 8,
44
+ zIndex: 20,
45
+ },
46
+ };
7
47
  const updateNode = (updater) => {
8
48
  if (!prefabData || !setPrefabData || !selectedId)
9
49
  return;
@@ -24,12 +64,112 @@ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transf
24
64
  };
25
65
  const selectedNode = selectedId && prefabData ? findNode(prefabData.root, selectedId) : null;
26
66
  // if (!selectedNode) return null;
27
- return _jsxs(_Fragment, { children: [_jsxs("div", { style: { position: 'absolute', top: "0.5rem", right: "0.5rem", zIndex: 20, backgroundColor: "rgba(0,0,0,0.7)", backdropFilter: "blur(4px)", color: "white", border: "1px solid rgba(0,255,255,0.3)" }, children: [_jsxs("div", { className: "px-1.5 py-1 font-mono text-[10px] bg-cyan-500/10 border-b border-cyan-500/30 sticky top-0 uppercase tracking-wider text-cyan-400/80 cursor-pointer hover:bg-cyan-500/20 flex items-center justify-between", onClick: () => setIsInspectorCollapsed(!isInspectorCollapsed), children: [_jsx("span", { children: "Inspector" }), _jsx("span", { className: "text-[8px]", children: isInspectorCollapsed ? '◀' : '▶' })] }), !isInspectorCollapsed && selectedNode && (_jsx(NodeInspector, { node: selectedNode, updateNode: updateNode, deleteNode: deleteNode, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath }))] }), _jsx("div", { style: { position: 'absolute', top: "0.5rem", left: "0.5rem", zIndex: 20 }, children: _jsx(EditorTree, { prefabData: prefabData, setPrefabData: setPrefabData, selectedId: selectedId, setSelectedId: setSelectedId }) })] });
67
+ return _jsxs(_Fragment, { children: [_jsxs("div", { style: ui.panel, children: [_jsxs("div", { style: ui.header, onClick: () => setIsInspectorCollapsed(!isInspectorCollapsed), onPointerEnter: (e) => {
68
+ e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
69
+ }, onPointerLeave: (e) => {
70
+ e.currentTarget.style.background = 'rgba(255,255,255,0.05)';
71
+ }, children: [_jsx("span", { children: "Inspector" }), _jsx("span", { style: { fontSize: 10, opacity: 0.8 }, children: isInspectorCollapsed ? '◀' : '▶' })] }), !isInspectorCollapsed && selectedNode && (_jsx(NodeInspector, { node: selectedNode, updateNode: updateNode, deleteNode: deleteNode, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath }))] }), _jsx("div", { style: ui.left, children: _jsx(EditorTree, { prefabData: prefabData, setPrefabData: setPrefabData, selectedId: selectedId, setSelectedId: setSelectedId }) })] });
28
72
  }
29
73
  function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransformMode, basePath }) {
30
74
  const ALL_COMPONENTS = getAllComponents();
31
75
  const allComponentKeys = Object.keys(ALL_COMPONENTS);
32
76
  const [addComponentType, setAddComponentType] = useState(allComponentKeys[0]);
77
+ const s = {
78
+ root: {
79
+ display: 'flex',
80
+ flexDirection: 'column',
81
+ gap: 6,
82
+ padding: 6,
83
+ maxHeight: '80vh',
84
+ overflowY: 'auto',
85
+ },
86
+ section: {
87
+ paddingBottom: 6,
88
+ borderBottom: '1px solid rgba(255,255,255,0.10)',
89
+ },
90
+ label: {
91
+ display: 'block',
92
+ fontSize: 10,
93
+ opacity: 0.7,
94
+ textTransform: 'uppercase',
95
+ letterSpacing: '0.08em',
96
+ marginBottom: 4,
97
+ },
98
+ input: {
99
+ width: '100%',
100
+ background: 'rgba(255,255,255,0.06)',
101
+ border: '1px solid rgba(255,255,255,0.14)',
102
+ borderRadius: 4,
103
+ padding: '4px 6px',
104
+ color: 'rgba(255,255,255,0.92)',
105
+ font: 'inherit',
106
+ outline: 'none',
107
+ },
108
+ row: {
109
+ display: 'flex',
110
+ alignItems: 'center',
111
+ justifyContent: 'space-between',
112
+ gap: 8,
113
+ },
114
+ button: {
115
+ padding: '2px 6px',
116
+ background: 'transparent',
117
+ color: 'rgba(255,255,255,0.9)',
118
+ border: '1px solid rgba(255,255,255,0.14)',
119
+ borderRadius: 4,
120
+ cursor: 'pointer',
121
+ font: 'inherit',
122
+ },
123
+ buttonActive: {
124
+ background: 'rgba(255,255,255,0.10)',
125
+ },
126
+ smallDanger: {
127
+ background: 'transparent',
128
+ border: 'none',
129
+ cursor: 'pointer',
130
+ color: 'rgba(255,120,120,0.95)',
131
+ font: 'inherit',
132
+ padding: '2px 4px',
133
+ },
134
+ componentHeader: {
135
+ display: 'flex',
136
+ alignItems: 'center',
137
+ justifyContent: 'space-between',
138
+ padding: '4px 0',
139
+ borderBottom: '1px solid rgba(255,255,255,0.08)',
140
+ marginBottom: 4,
141
+ },
142
+ componentTitle: {
143
+ fontSize: 10,
144
+ textTransform: 'uppercase',
145
+ letterSpacing: '0.08em',
146
+ opacity: 0.8,
147
+ },
148
+ select: {
149
+ flex: 1,
150
+ background: 'rgba(255,255,255,0.06)',
151
+ border: '1px solid rgba(255,255,255,0.14)',
152
+ borderRadius: 4,
153
+ padding: '4px 6px',
154
+ color: 'rgba(255,255,255,0.92)',
155
+ font: 'inherit',
156
+ outline: 'none',
157
+ },
158
+ addButton: {
159
+ width: 28,
160
+ padding: '4px 0',
161
+ background: 'rgba(255,255,255,0.08)',
162
+ color: 'rgba(255,255,255,0.92)',
163
+ border: '1px solid rgba(255,255,255,0.14)',
164
+ borderRadius: 4,
165
+ cursor: 'pointer',
166
+ font: 'inherit',
167
+ },
168
+ disabled: {
169
+ opacity: 0.35,
170
+ cursor: 'not-allowed',
171
+ },
172
+ };
33
173
  const componentKeys = Object.keys(node.components || {}).join(',');
34
174
  useEffect(() => {
35
175
  // Components stored on nodes use lowercase keys (e.g. 'geometry'),
@@ -39,19 +179,25 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
39
179
  setAddComponentType(available[0] || "");
40
180
  }
41
181
  }, [componentKeys, addComponentType, node.components, allComponentKeys]);
42
- return _jsxs("div", { className: "flex flex-col gap-1 text-[11px] max-w-[250px] max-h-[80vh] overflow-y-auto", children: [_jsx("div", { className: "border-b border-cyan-500/20 pb-1 px-1.5 pt-1", children: _jsx("input", { className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[11px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: node.id, onChange: e => updateNode(n => (Object.assign(Object.assign({}, n), { id: e.target.value }))) }) }), _jsxs("div", { className: "flex justify-between items-center px-1.5 py-0.5 border-b border-cyan-500/20", children: [_jsx("label", { className: "text-[10px] font-mono text-cyan-400/80 uppercase tracking-wider", children: "Components" }), _jsx("button", { onClick: deleteNode, className: "text-[10px] text-red-400/80 hover:text-red-400", children: "\u2715" })] }), _jsxs("div", { className: "px-1.5 py-1 border-b border-cyan-500/20", children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Mode" }), _jsx("div", { className: "flex gap-0.5", children: ["translate", "rotate", "scale"].map(mode => (_jsx("button", { onClick: () => setTransformMode(mode), className: `flex-1 px-1 py-0.5 text-[10px] font-mono border ${transformMode === mode ? 'bg-cyan-500/30 border-cyan-400/50 text-cyan-200' : 'bg-black/30 border-cyan-500/20 text-cyan-400/60 hover:border-cyan-400/30'}`, children: mode[0].toUpperCase() }, mode))) })] }), node.components && Object.entries(node.components).map(([key, comp]) => {
182
+ return _jsxs("div", { style: s.root, children: [_jsx("div", { style: s.section, children: _jsx("input", { style: s.input, value: node.id, onChange: e => updateNode(n => (Object.assign(Object.assign({}, n), { id: e.target.value }))) }) }), _jsxs("div", { style: Object.assign(Object.assign(Object.assign({}, s.row), s.section), { paddingBottom: 6 }), children: [_jsx("label", { style: Object.assign(Object.assign({}, s.label), { marginBottom: 0 }), children: "Components" }), _jsx("button", { onClick: deleteNode, style: s.smallDanger, title: "Delete node", children: "\u2715" })] }), _jsxs("div", { style: s.section, children: [_jsx("label", { style: s.label, children: "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 : null)), onPointerEnter: (e) => {
183
+ if (transformMode !== mode)
184
+ e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
185
+ }, onPointerLeave: (e) => {
186
+ if (transformMode !== mode)
187
+ e.currentTarget.style.background = 'transparent';
188
+ }, children: mode[0].toUpperCase() }, mode))) })] }), node.components && Object.entries(node.components).map(([key, comp]) => {
43
189
  if (!comp)
44
190
  return null;
45
191
  const componentDef = ALL_COMPONENTS[comp.type];
46
192
  if (!componentDef)
47
- return _jsxs("div", { className: "px-1 py-0.5 text-red-400 text-[10px]", children: ["Unknown component type: ", comp.type, _jsx("textarea", { defaultValue: JSON.stringify(comp) })] }, key);
193
+ return _jsxs("div", { style: { padding: '4px 0', color: 'rgba(255,120,120,0.95)', fontSize: 11 }, children: ["Unknown component type: ", comp.type, _jsx("textarea", { defaultValue: JSON.stringify(comp) })] }, key);
48
194
  const EditorComp = componentDef.Editor;
49
- return (_jsxs("div", { className: 'px-1', children: [_jsxs("div", { className: "flex justify-between items-center py-0.5 border-b border-cyan-500/20 bg-cyan-500/5", children: [_jsx("span", { className: "font-mono text-[10px] text-cyan-300 uppercase", children: key }), _jsx("button", { onClick: () => updateNode(n => {
195
+ return (_jsxs("div", { style: { padding: '0 2px' }, children: [_jsxs("div", { style: s.componentHeader, children: [_jsx("span", { style: s.componentTitle, children: key }), _jsx("button", { onClick: () => updateNode(n => {
50
196
  const components = Object.assign({}, n.components);
51
197
  delete components[key];
52
198
  return Object.assign(Object.assign({}, n), { components });
53
- }), className: "text-[9px] text-red-400/60 hover:text-red-400", children: "\u2715" })] }), EditorComp ? (_jsx(EditorComp, { component: comp, onUpdate: (newProps) => updateNode(n => (Object.assign(Object.assign({}, n), { components: Object.assign(Object.assign({}, n.components), { [key]: Object.assign(Object.assign({}, comp), { properties: Object.assign(Object.assign({}, comp.properties), newProps) }) }) }))), basePath: basePath })) : null] }, key));
54
- }), _jsxs("div", { className: "px-1.5 py-1 border-t border-cyan-500/20", children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Add Component" }), _jsxs("div", { className: "flex gap-0.5", children: [_jsx("select", { className: "bg-black/40 border border-cyan-500/30 px-1 py-0.5 flex-1 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: addComponentType, onChange: e => setAddComponentType(e.target.value), children: allComponentKeys.filter(k => { var _a; return !((_a = node.components) === null || _a === void 0 ? void 0 : _a[k.toLowerCase()]); }).map(k => (_jsx("option", { value: k, children: k }, k))) }), _jsx("button", { className: "bg-cyan-500/20 hover:bg-cyan-500/30 border border-cyan-500/30 px-2 py-0.5 text-[10px] text-cyan-300 font-mono disabled:opacity-30", disabled: !addComponentType, onClick: () => {
199
+ }), style: s.smallDanger, title: "Remove component", children: "\u2715" })] }), EditorComp ? (_jsx(EditorComp, { component: comp, onUpdate: (newProps) => updateNode(n => (Object.assign(Object.assign({}, n), { components: Object.assign(Object.assign({}, n.components), { [key]: Object.assign(Object.assign({}, comp), { properties: Object.assign(Object.assign({}, comp.properties), newProps) }) }) }))), basePath: basePath })) : null] }, key));
200
+ }), _jsxs("div", { style: Object.assign(Object.assign({}, s.section), { borderBottom: 'none', paddingBottom: 0 }), children: [_jsx("label", { style: s.label, children: "Add Component" }), _jsxs("div", { style: { display: 'flex', gap: 6 }, children: [_jsx("select", { style: s.select, value: addComponentType, onChange: e => setAddComponentType(e.target.value), children: allComponentKeys.filter(k => { var _a; return !((_a = node.components) === null || _a === void 0 ? void 0 : _a[k.toLowerCase()]); }).map(k => (_jsx("option", { value: k, children: k }, k))) }), _jsx("button", { style: Object.assign(Object.assign({}, s.addButton), (!addComponentType ? s.disabled : null)), disabled: !addComponentType, onClick: () => {
55
201
  var _a;
56
202
  if (!addComponentType)
57
203
  return;
@@ -60,6 +206,14 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
60
206
  const key = addComponentType.toLowerCase();
61
207
  updateNode(n => (Object.assign(Object.assign({}, n), { components: Object.assign(Object.assign({}, n.components), { [key]: { type: def.name, properties: def.defaultProperties } }) })));
62
208
  }
209
+ }, onPointerEnter: (e) => {
210
+ if (!addComponentType)
211
+ return;
212
+ e.currentTarget.style.background = 'rgba(255,255,255,0.12)';
213
+ }, onPointerLeave: (e) => {
214
+ if (!addComponentType)
215
+ return;
216
+ e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
63
217
  }, children: "+" })] })] })] });
64
218
  }
65
219
  function findNode(root, id) {
@@ -21,8 +21,6 @@ const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }) =>
21
21
  "name": "New Prefab",
22
22
  "root": {
23
23
  "id": "root",
24
- "enabled": true,
25
- "visible": true,
26
24
  "components": {
27
25
  "transform": {
28
26
  "type": "Transform",
@@ -52,11 +50,65 @@ const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }) =>
52
50
  };
53
51
  return _jsxs(_Fragment, { children: [_jsx(GameCanvas, { children: _jsxs(Physics, { paused: editMode, children: [_jsx("ambientLight", { intensity: 1.5 }), _jsx("gridHelper", { args: [10, 10], position: [0, -1, 0] }), _jsx(PrefabRoot, { data: loadedPrefab, ref: prefabRef,
54
52
  // props for edit mode
55
- editMode: editMode, onPrefabChange: updatePrefab, selectedId: selectedId, onSelect: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath }), children] }) }), _jsxs("div", { style: { position: "absolute", top: "0.5rem", left: "50%", transform: "translateX(-50%)" }, className: "bg-black/70 backdrop-blur-sm border border-cyan-500/30 px-2 py-1 flex items-center gap-1", children: [_jsx("button", { className: "px-1 py-0.5 text-[10px] font-mono text-cyan-300 hover:bg-cyan-500/20 border border-cyan-500/30", onClick: () => setEditMode(!editMode), children: editMode ? "▶" : "⏸" }), _jsx("span", { className: "text-cyan-500/30 text-[10px]", children: "|" }), _jsx("button", { className: "px-1 py-0.5 text-[10px] font-mono text-cyan-300 hover:bg-cyan-500/20 border border-cyan-500/30", onClick: () => __awaiter(void 0, void 0, void 0, function* () {
53
+ editMode: editMode, onPrefabChange: updatePrefab, selectedId: selectedId, onSelect: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath }), children] }) }), _jsxs("div", { style: {
54
+ position: "absolute",
55
+ top: 8,
56
+ left: "50%",
57
+ transform: "translateX(-50%)",
58
+ display: "flex",
59
+ alignItems: "center",
60
+ gap: 6,
61
+ padding: "2px 4px",
62
+ background: "rgba(0,0,0,0.55)",
63
+ border: "1px solid rgba(255,255,255,0.12)",
64
+ borderRadius: 4,
65
+ color: "rgba(255,255,255,0.9)",
66
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
67
+ fontSize: 11,
68
+ lineHeight: 1,
69
+ WebkitUserSelect: "none",
70
+ userSelect: "none",
71
+ }, children: [_jsx("button", { style: {
72
+ padding: "2px 6px",
73
+ font: "inherit",
74
+ background: "transparent",
75
+ color: "inherit",
76
+ border: "1px solid rgba(255,255,255,0.18)",
77
+ borderRadius: 3,
78
+ cursor: "pointer",
79
+ }, onClick: () => setEditMode(!editMode), onPointerEnter: (e) => {
80
+ e.currentTarget.style.background = "rgba(255,255,255,0.08)";
81
+ }, onPointerLeave: (e) => {
82
+ e.currentTarget.style.background = "transparent";
83
+ }, children: editMode ? "▶" : "⏸" }), _jsx("span", { style: { opacity: 0.35 }, children: "|" }), _jsx("button", { style: {
84
+ padding: "2px 6px",
85
+ font: "inherit",
86
+ background: "transparent",
87
+ color: "inherit",
88
+ border: "1px solid rgba(255,255,255,0.18)",
89
+ borderRadius: 3,
90
+ cursor: "pointer",
91
+ }, onClick: () => __awaiter(void 0, void 0, void 0, function* () {
56
92
  const prefab = yield loadJson();
57
93
  if (prefab)
58
94
  setLoadedPrefab(prefab);
59
- }), children: "\uD83D\uDCE5" }), _jsx("button", { className: "px-1 py-0.5 text-[10px] font-mono text-cyan-300 hover:bg-cyan-500/20 border border-cyan-500/30", onClick: () => saveJson(loadedPrefab, "prefab"), children: "\uD83D\uDCBE" })] }), editMode && _jsx(EditorUI, { prefabData: loadedPrefab, setPrefabData: updatePrefab, selectedId: selectedId, setSelectedId: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath })] });
95
+ }), onPointerEnter: (e) => {
96
+ e.currentTarget.style.background = "rgba(255,255,255,0.08)";
97
+ }, onPointerLeave: (e) => {
98
+ e.currentTarget.style.background = "transparent";
99
+ }, children: "\uD83D\uDCE5" }), _jsx("button", { style: {
100
+ padding: "2px 6px",
101
+ font: "inherit",
102
+ background: "transparent",
103
+ color: "inherit",
104
+ border: "1px solid rgba(255,255,255,0.18)",
105
+ borderRadius: 3,
106
+ cursor: "pointer",
107
+ }, onClick: () => saveJson(loadedPrefab, "prefab"), onPointerEnter: (e) => {
108
+ e.currentTarget.style.background = "rgba(255,255,255,0.08)";
109
+ }, onPointerLeave: (e) => {
110
+ e.currentTarget.style.background = "transparent";
111
+ }, children: "\uD83D\uDCBE" })] }), editMode && _jsx(EditorUI, { prefabData: loadedPrefab, setPrefabData: updatePrefab, selectedId: selectedId, setSelectedId: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath })] });
60
112
  };
61
113
  const saveJson = (data, filename) => {
62
114
  const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data, null, 2));
@@ -49,13 +49,6 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
49
49
  setSelectedObject(null);
50
50
  }
51
51
  }, [selectedId]);
52
- // const [transformMode, setTransformMode] = useState<"translate" | "rotate" | "scale">("translate"); // Removed local state
53
- const updateNode = (updater) => {
54
- if (!selectedId || !onPrefabChange)
55
- return;
56
- const newRoot = updatePrefabNode(data.root, selectedId, updater);
57
- onPrefabChange(Object.assign(Object.assign({}, data), { root: newRoot }));
58
- };
59
52
  const onTransformChange = () => {
60
53
  if (!selectedId || !onPrefabChange)
61
54
  return;
@@ -158,7 +151,7 @@ function GameObjectRenderer({ gameObject, selectedId, onSelect, registerRef, loa
158
151
  }
159
152
  clickValid.current = false;
160
153
  };
161
- if (!gameObject.enabled || !gameObject.visible)
154
+ if (gameObject.disabled === true || gameObject.hidden === true)
162
155
  return null;
163
156
  // --- 2. If instanced, short-circuit to a tiny clean branch ---
164
157
  const isInstanced = !!((_c = (_b = (_a = gameObject.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);
@@ -170,8 +163,6 @@ function GameObjectRenderer({ gameObject, selectedId, onSelect, registerRef, loa
170
163
  // --- 5. Render children (always relative transforms) ---
171
164
  const children = ((_d = gameObject.children) !== null && _d !== void 0 ? _d : []).map((child) => (_jsx(GameObjectRenderer, { gameObject: child, selectedId: selectedId, onSelect: onSelect, registerRef: registerRef, loadedModels: loadedModels, loadedTextures: loadedTextures, editMode: editMode, parentMatrix: worldMatrix }, child.id)));
172
165
  // --- 4. Wrap with physics if needed ---
173
- // Only wrap the core content (geometry/model), not children
174
- // Children should be siblings, not inside the physics body
175
166
  const physicsWrapped = wrapPhysicsIfNeeded(gameObject, core, ctx);
176
167
  // --- 6. Final group wrapper ---
177
168
  return (_jsxs("group", { ref: (el) => registerRef(gameObject.id, el), position: transformProps.position, rotation: transformProps.rotation, scale: transformProps.scale, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, children: [physicsWrapped, children] }));
@@ -1,6 +1,6 @@
1
1
  export interface Prefab {
2
- id: string;
3
- name: string;
2
+ id?: string;
3
+ name?: string;
4
4
  description?: string;
5
5
  author?: string;
6
6
  version?: string;
@@ -12,8 +12,8 @@ export interface Prefab {
12
12
  }
13
13
  export interface GameObject {
14
14
  id: string;
15
- enabled: boolean;
16
- visible: boolean;
15
+ disabled?: boolean;
16
+ hidden?: boolean;
17
17
  ref?: any;
18
18
  children?: GameObject[];
19
19
  components?: {
@@ -1 +1,2 @@
1
+ // import { ThreeElements } from "@react-three/fiber"
1
2
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-three-game",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "description": "Batteries included React Three Fiber game engine",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -15,8 +15,11 @@
15
15
  "author": "prnth",
16
16
  "license": "VPL",
17
17
  "type": "module",
18
+ "workspaces": ["docs"],
18
19
  "peerDependencies": {
19
20
  "@react-three/fiber": "^9.0.0",
21
+ "@react-three/drei": "^10.0.0",
22
+ "@react-three/rapier": "^2.0.0",
20
23
  "react": "^18.0.0 || ^19.0.0",
21
24
  "react-dom": "^18.0.0 || ^19.0.0",
22
25
  "three": "^0.181.0"
@@ -0,0 +1,95 @@
1
+ import type { GameObject } from "../tools/prefabeditor/types";
2
+
3
+ export type Vec3 = [number, number, number];
4
+
5
+ export interface GroundOptions {
6
+ /** GameObject id. Defaults to "ground". */
7
+ id?: string;
8
+
9
+ /** Plane size. Defaults to 50. */
10
+ size?: number;
11
+
12
+ /** Transform overrides. */
13
+ position?: Vec3;
14
+ rotation?: Vec3;
15
+ scale?: Vec3;
16
+
17
+ /** Material overrides. */
18
+ color?: string;
19
+ texture?: string;
20
+ /** When true, set repeat wrapping. Defaults to true if texture is provided. */
21
+ repeat?: boolean;
22
+ /** Texture repeat counts when repeat=true. Defaults to [25,25]. */
23
+ repeatCount?: [number, number];
24
+
25
+ /** Physics body type. Defaults to "fixed". */
26
+ physicsType?: "fixed" | "dynamic" | "kinematic";
27
+
28
+ /** Set true to hide the node. */
29
+ hidden?: boolean;
30
+ /** Set true to disable the node. */
31
+ disabled?: boolean;
32
+ }
33
+
34
+ /**
35
+ * Create a ready-to-use plane ground GameObject.
36
+ *
37
+ * Designed to reduce prefab boilerplate:
38
+ * - Transform (rotated to lie flat)
39
+ * - Geometry (plane)
40
+ * - Material (optional texture + repeat)
41
+ * - Physics (fixed by default)
42
+ */
43
+ export function ground(options: GroundOptions = {}): GameObject {
44
+ const {
45
+ id = "ground",
46
+ size = 50,
47
+ position = [0, 0, 0],
48
+ rotation = [-Math.PI / 2, 0, 0],
49
+ scale = [1, 1, 1],
50
+ color = "white",
51
+ texture,
52
+ repeat = texture ? true : false,
53
+ repeatCount = [25, 25],
54
+ physicsType = "fixed",
55
+ hidden = false,
56
+ disabled = false,
57
+ } = options;
58
+
59
+ return {
60
+ id,
61
+ disabled,
62
+ hidden,
63
+ components: {
64
+ transform: {
65
+ type: "Transform",
66
+ properties: {
67
+ position,
68
+ rotation,
69
+ scale,
70
+ },
71
+ },
72
+ geometry: {
73
+ type: "Geometry",
74
+ properties: {
75
+ geometryType: "plane",
76
+ args: [size, size],
77
+ },
78
+ },
79
+ material: {
80
+ type: "Material",
81
+ properties: {
82
+ color,
83
+ ...(texture ? { texture } : {}),
84
+ ...(repeat ? { repeat: true, repeatCount } : {}),
85
+ },
86
+ },
87
+ physics: {
88
+ type: "Physics",
89
+ properties: {
90
+ type: physicsType,
91
+ },
92
+ },
93
+ },
94
+ };
95
+ }
package/src/index.ts CHANGED
@@ -10,5 +10,8 @@ export {
10
10
  SharedCanvas,
11
11
  } from './tools/assetviewer/page';
12
12
 
13
+ // Helpers
14
+ export * from './helpers';
15
+
13
16
  // Types
14
17
  export type { Prefab, GameObject } from './tools/prefabeditor/types';
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import { Canvas, extend } from "@react-three/fiber";
2
4
  import { WebGPURenderer, MeshBasicNodeMaterial, MeshStandardNodeMaterial, SpriteNodeMaterial, PCFShadowMap } from "three/webgpu";
3
5
  import { Suspense, useState } from "react";
@@ -16,10 +18,10 @@ extend({
16
18
 
17
19
  export default function GameCanvas({ loader = false, children, ...props }: { loader?: boolean, children: React.ReactNode, props?: WebGPURendererParameters }) {
18
20
  const [frameloop, setFrameloop] = useState<"never" | "always">("never");
19
- const [loading, setLoading] = useState(true);
20
21
 
21
22
  return <>
22
23
  <Canvas
24
+ style={{ touchAction: 'none' }}
23
25
  shadows={{ type: PCFShadowMap, }}
24
26
  frameloop={frameloop}
25
27
  gl={async ({ canvas }) => {
@@ -42,7 +44,8 @@ export default function GameCanvas({ loader = false, children, ...props }: { loa
42
44
  <Suspense>
43
45
  {children}
44
46
  </Suspense>
47
+
48
+ {loader ? <Loader /> : null}
45
49
  </Canvas>
46
- {loader ? <Loader /> : null}
47
50
  </>;
48
51
  }
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  // DragDropLoader.tsx
2
3
  import { useEffect, ChangeEvent } from "react";
3
4
  import { DRACOLoader, FBXLoader, GLTFLoader } from "three/examples/jsm/Addons.js";