react-three-game 0.0.16 → 0.0.17

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.
@@ -188,7 +188,7 @@ function renderCoreNode(gameObject, ctx, parentMatrix) {
188
188
  const geometryDef = geometry ? getComponent('Geometry') : undefined;
189
189
  const materialDef = material ? getComponent('Material') : undefined;
190
190
  const isModelAvailable = !!(modelComp && modelComp.properties && modelComp.properties.filename && ctx.loadedModels[modelComp.properties.filename]);
191
- // Generic component views (exclude geometry/material/model)
191
+ // Generic component views (exclude geometry/material/model/transform/physics)
192
192
  const contextProps = {
193
193
  loadedModels: ctx.loadedModels,
194
194
  loadedTextures: ctx.loadedTextures,
@@ -197,29 +197,48 @@ function renderCoreNode(gameObject, ctx, parentMatrix) {
197
197
  parentMatrix,
198
198
  registerRef: ctx.registerRef,
199
199
  };
200
- const allComponentViews = gameObject.components
201
- ? Object.entries(gameObject.components)
200
+ // Separate wrapper components (that accept children) from leaf components
201
+ const wrapperComponents = [];
202
+ const leafComponents = [];
203
+ if (gameObject.components) {
204
+ Object.entries(gameObject.components)
202
205
  .filter(([key]) => key !== 'geometry' && key !== 'material' && key !== 'model' && key !== 'transform' && key !== 'physics')
203
- .map(([key, comp]) => {
206
+ .forEach(([key, comp]) => {
204
207
  if (!comp || !comp.type)
205
- return null;
208
+ return;
206
209
  const def = getComponent(comp.type);
207
210
  if (!def || !def.View)
208
- return null;
209
- return _jsx(def.View, Object.assign({ properties: comp.properties }, contextProps), key);
210
- })
211
- : null;
211
+ return;
212
+ // Check if the component View accepts children by checking function signature
213
+ // Components that wrap content should accept children prop
214
+ const viewString = def.View.toString();
215
+ if (viewString.includes('children')) {
216
+ wrapperComponents.push({ key, View: def.View, properties: comp.properties });
217
+ }
218
+ else {
219
+ leafComponents.push(_jsx(def.View, Object.assign({ properties: comp.properties }, contextProps), key));
220
+ }
221
+ });
222
+ }
223
+ // Build the core content (model or mesh)
224
+ let coreContent;
212
225
  // If we have a model (non-instanced) render it as a primitive with material override
213
226
  if (isModelAvailable) {
214
227
  const modelObj = ctx.loadedModels[modelComp.properties.filename].clone();
215
- return (_jsxs("primitive", { object: modelObj, children: [material && materialDef && materialDef.View && (_jsx(materialDef.View, { properties: material.properties, loadedTextures: ctx.loadedTextures, isSelected: ctx.selectedId === gameObject.id, editMode: ctx.editMode, parentMatrix: parentMatrix, registerRef: ctx.registerRef }, "material")), allComponentViews] }));
228
+ coreContent = (_jsxs("primitive", { object: modelObj, children: [material && materialDef && materialDef.View && (_jsx(materialDef.View, { properties: material.properties, loadedTextures: ctx.loadedTextures, isSelected: ctx.selectedId === gameObject.id, editMode: ctx.editMode, parentMatrix: parentMatrix, registerRef: ctx.registerRef }, "material")), leafComponents] }));
229
+ }
230
+ else if (geometry && geometryDef && geometryDef.View) {
231
+ // Otherwise, if geometry present, render a mesh
232
+ coreContent = (_jsxs("mesh", { children: [_jsx(geometryDef.View, Object.assign({ properties: geometry.properties }, contextProps), "geometry"), material && materialDef && materialDef.View && (_jsx(materialDef.View, { properties: material.properties, loadedTextures: ctx.loadedTextures, isSelected: ctx.selectedId === gameObject.id, editMode: ctx.editMode, parentMatrix: parentMatrix, registerRef: ctx.registerRef }, "material")), leafComponents] }));
216
233
  }
217
- // Otherwise, if geometry present, render a mesh
218
- if (geometry && geometryDef && geometryDef.View) {
219
- return (_jsxs("mesh", { children: [_jsx(geometryDef.View, Object.assign({ properties: geometry.properties }, contextProps), "geometry"), material && materialDef && materialDef.View && (_jsx(materialDef.View, { properties: material.properties, loadedTextures: ctx.loadedTextures, isSelected: ctx.selectedId === gameObject.id, editMode: ctx.editMode, parentMatrix: parentMatrix, registerRef: ctx.registerRef }, "material")), allComponentViews] }));
234
+ else {
235
+ // No geometry or model, just render leaf components
236
+ coreContent = _jsx(_Fragment, { children: leafComponents });
220
237
  }
221
- // Default: render other component views (no geometry/model)
222
- return _jsx(_Fragment, { children: allComponentViews });
238
+ // Wrap core content with wrapper components (in order)
239
+ return wrapperComponents.reduce((content, { key, View, properties }) => {
240
+ return _jsx(View, Object.assign({ properties: properties }, contextProps, { children: content }), key);
241
+ }, coreContent);
223
242
  }
224
243
  // Helper: wrap core content with physics component when necessary
225
244
  function wrapPhysicsIfNeeded(gameObject, content, ctx) {
@@ -5,6 +5,8 @@ export interface Component {
5
5
  component: any;
6
6
  onUpdate: (newComp: any) => void;
7
7
  basePath?: string;
8
+ transformMode?: "translate" | "rotate" | "scale";
9
+ setTransformMode?: (m: "translate" | "rotate" | "scale") => void;
8
10
  }>;
9
11
  defaultProperties: any;
10
12
  View?: FC<any>;
@@ -0,0 +1,3 @@
1
+ import { Component } from "./ComponentRegistry";
2
+ declare const RotatorComponent: Component;
3
+ export default RotatorComponent;
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useFrame } from "@react-three/fiber";
3
+ import { useRef } from "react";
4
+ function RotatorComponentEditor({ component, onUpdate }) {
5
+ var _a, _b;
6
+ const props = {
7
+ speed: (_a = component.properties.speed) !== null && _a !== void 0 ? _a : 1.0,
8
+ axis: (_b = component.properties.axis) !== null && _b !== void 0 ? _b : 'y'
9
+ };
10
+ 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: "Rotation Speed" }), _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.speed, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { speed: parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Rotation Axis" }), _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: props.axis, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { axis: e.target.value })), children: [_jsx("option", { value: "x", children: "X" }), _jsx("option", { value: "y", children: "Y" }), _jsx("option", { value: "z", children: "Z" })] })] })] });
11
+ }
12
+ // The view component for Rotator
13
+ function RotatorView({ properties, children }) {
14
+ var _a, _b;
15
+ const groupRef = useRef(null);
16
+ const speed = (_a = properties.speed) !== null && _a !== void 0 ? _a : 1.0;
17
+ const axis = (_b = properties.axis) !== null && _b !== void 0 ? _b : 'y';
18
+ useFrame((state, delta) => {
19
+ if (groupRef.current) {
20
+ if (axis === 'x') {
21
+ groupRef.current.rotation.x += delta * speed;
22
+ }
23
+ else if (axis === 'y') {
24
+ groupRef.current.rotation.y += delta * speed;
25
+ }
26
+ else if (axis === 'z') {
27
+ groupRef.current.rotation.z += delta * speed;
28
+ }
29
+ }
30
+ });
31
+ return (_jsx("group", { ref: groupRef, children: children }));
32
+ }
33
+ const RotatorComponent = {
34
+ name: 'Rotator',
35
+ Editor: RotatorComponentEditor,
36
+ View: RotatorView,
37
+ defaultProperties: {
38
+ speed: 1.0,
39
+ axis: 'y'
40
+ }
41
+ };
42
+ export default RotatorComponent;
@@ -1,6 +1,26 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- function TransformComponentEditor({ component, onUpdate }) {
3
- return _jsxs("div", { className: "flex flex-col", children: [_jsx(Vector3Input, { label: "Position", value: component.properties.position, onChange: v => onUpdate({ position: v }) }), _jsx(Vector3Input, { label: "Rotation", value: component.properties.rotation, onChange: v => onUpdate({ rotation: v }) }), _jsx(Vector3Input, { label: "Scale", value: component.properties.scale, onChange: v => onUpdate({ scale: v }) })] });
2
+ function TransformComponentEditor({ component, onUpdate, transformMode, setTransformMode }) {
3
+ const s = {
4
+ button: {
5
+ padding: '2px 6px',
6
+ background: 'transparent',
7
+ color: 'rgba(255,255,255,0.9)',
8
+ border: '1px solid rgba(255,255,255,0.14)',
9
+ borderRadius: 4,
10
+ cursor: 'pointer',
11
+ font: 'inherit',
12
+ },
13
+ buttonActive: {
14
+ background: 'rgba(255,255,255,0.10)',
15
+ },
16
+ };
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
+ if (transformMode !== mode)
19
+ e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
20
+ }, onPointerLeave: (e) => {
21
+ if (transformMode !== mode)
22
+ e.currentTarget.style.background = 'transparent';
23
+ }, children: mode }, mode))) })] })), _jsx(Vector3Input, { label: "Position", value: component.properties.position, onChange: v => onUpdate({ position: v }) }), _jsx(Vector3Input, { label: "Rotation", value: component.properties.rotation, onChange: v => onUpdate({ rotation: v }) }), _jsx(Vector3Input, { label: "Scale", value: component.properties.scale, onChange: v => onUpdate({ scale: v }) })] });
4
24
  }
5
25
  const TransformComponent = {
6
26
  name: 'Transform',
@@ -18,5 +38,10 @@ export function Vector3Input({ label, value, onChange }) {
18
38
  newValue[index] = parseFloat(val) || 0;
19
39
  onChange(newValue);
20
40
  };
21
- return _jsxs("div", { className: "mb-1", children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: label }), _jsxs("div", { className: "flex gap-0.5", children: [_jsxs("div", { className: "relative flex-1", children: [_jsx("span", { className: "absolute left-0.5 top-0 text-[8px] text-red-400/80 font-mono", children: "X" }), _jsx("input", { className: "w-full bg-black/40 border border-cyan-500/30 pl-3 pr-0.5 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", type: "number", step: "0.1", value: value[0], onChange: e => handleChange(0, e.target.value) })] }), _jsxs("div", { className: "relative flex-1", children: [_jsx("span", { className: "absolute left-0.5 top-0 text-[8px] text-green-400/80 font-mono", children: "Y" }), _jsx("input", { className: "w-full bg-black/40 border border-cyan-500/30 pl-3 pr-0.5 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", type: "number", step: "0.1", value: value[1], onChange: e => handleChange(1, e.target.value) })] }), _jsxs("div", { className: "relative flex-1", children: [_jsx("span", { className: "absolute left-0.5 top-0 text-[8px] text-blue-400/80 font-mono", children: "Z" }), _jsx("input", { className: "w-full bg-black/40 border border-cyan-500/30 pl-3 pr-0.5 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", type: "number", step: "0.1", value: value[2], onChange: e => handleChange(2, e.target.value) })] })] })] });
41
+ const axes = [
42
+ { key: 'x', color: 'red', index: 0 },
43
+ { key: 'y', color: 'green', index: 1 },
44
+ { key: 'z', color: 'blue', index: 2 }
45
+ ];
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))) })] });
22
47
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-three-game",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
4
4
  "description": "Batteries included React Three Fiber game engine",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -17,12 +17,12 @@
17
17
  "type": "module",
18
18
  "workspaces": ["docs"],
19
19
  "peerDependencies": {
20
- "@react-three/fiber": "^9.0.0",
21
- "@react-three/drei": "^10.0.0",
22
- "@react-three/rapier": "^2.0.0",
23
- "react": "^18.0.0 || ^19.0.0",
24
- "react-dom": "^18.0.0 || ^19.0.0",
25
- "three": "^0.181.0"
20
+ "@react-three/fiber": ">=9.0.0",
21
+ "@react-three/drei": ">=10.0.0",
22
+ "@react-three/rapier": ">=2.0.0",
23
+ "react": ">=18.0.0",
24
+ "react-dom": ">=18.0.0",
25
+ "three": ">=0.181.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@react-three/drei": "^10.7.7",
package/src/index.ts CHANGED
@@ -10,6 +10,10 @@ export {
10
10
  SharedCanvas,
11
11
  } from './tools/assetviewer/page';
12
12
 
13
+ // Component Registry
14
+ export { registerComponent } from './tools/prefabeditor/components/ComponentRegistry';
15
+ export type { Component } from './tools/prefabeditor/components/ComponentRegistry';
16
+
13
17
  // Helpers
14
18
  export * from './helpers';
15
19
 
@@ -74,6 +74,17 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
74
74
  overflow: "hidden",
75
75
  textOverflow: "ellipsis",
76
76
  },
77
+ dragHandle: {
78
+ width: 14,
79
+ height: 14,
80
+ display: "flex",
81
+ alignItems: "center",
82
+ justifyContent: "center",
83
+ marginRight: 4,
84
+ opacity: 0.4,
85
+ cursor: "grab",
86
+ fontSize: 10,
87
+ },
77
88
  contextMenu: {
78
89
  position: "fixed",
79
90
  zIndex: 50,
@@ -179,40 +190,38 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
179
190
 
180
191
  // Drag and Drop
181
192
  const handleDragStart = (e: React.DragEvent, id: string) => {
182
- e.stopPropagation();
183
193
  if (id === prefabData.root.id) {
184
- e.preventDefault(); // Cannot drag root
194
+ e.preventDefault();
185
195
  return;
186
196
  }
187
- setDraggedId(id);
188
197
  e.dataTransfer.effectAllowed = "move";
198
+ e.dataTransfer.setData("text/plain", id);
199
+ setDraggedId(id);
200
+ };
201
+
202
+ const handleDragEnd = () => {
203
+ setDraggedId(null);
189
204
  };
190
205
 
191
206
  const handleDragOver = (e: React.DragEvent, targetId: string) => {
192
- e.preventDefault();
193
- e.stopPropagation();
194
207
  if (!draggedId || draggedId === targetId) return;
195
-
196
- // Check for cycles: target cannot be a descendant of dragged node
197
208
  const draggedNode = findNode(prefabData.root, draggedId);
198
209
  if (draggedNode && findNode(draggedNode, targetId)) return;
199
210
 
211
+ e.preventDefault();
200
212
  e.dataTransfer.dropEffect = "move";
201
213
  };
202
214
 
203
215
  const handleDrop = (e: React.DragEvent, targetId: string) => {
204
- e.preventDefault();
205
- e.stopPropagation();
206
216
  if (!draggedId || draggedId === targetId) return;
207
217
 
218
+ e.preventDefault();
219
+
208
220
  setPrefabData(prev => {
209
221
  const newRoot = JSON.parse(JSON.stringify(prev.root));
222
+ const draggedNode = findNode(newRoot, draggedId);
223
+ if (draggedNode && findNode(draggedNode, targetId)) return prev;
210
224
 
211
- // Check cycle again on the fresh tree
212
- const draggedNodeRef = findNode(newRoot, draggedId);
213
- if (draggedNodeRef && findNode(draggedNodeRef, targetId)) return prev;
214
-
215
- // Remove from old parent
216
225
  const parent = findParent(newRoot, draggedId);
217
226
  if (!parent) return prev;
218
227
 
@@ -221,7 +230,6 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
221
230
 
222
231
  parent.children = parent.children!.filter(c => c.id !== draggedId);
223
232
 
224
- // Add to new parent
225
233
  const target = findNode(newRoot, targetId);
226
234
  if (target) {
227
235
  target.children = target.children || [];
@@ -247,11 +255,13 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
247
255
  ...styles.row,
248
256
  ...(isSelected ? styles.rowSelected : null),
249
257
  paddingLeft: `${depth * 10 + 6}px`,
258
+ cursor: node.id !== prefabData.root.id ? "grab" : "pointer",
250
259
  }}
260
+ draggable={node.id !== prefabData.root.id}
251
261
  onClick={(e) => { e.stopPropagation(); setSelectedId(node.id); }}
252
262
  onContextMenu={(e) => handleContextMenu(e, node.id)}
253
- draggable={node.id !== prefabData.root.id}
254
263
  onDragStart={(e) => handleDragStart(e, node.id)}
264
+ onDragEnd={handleDragEnd}
255
265
  onDragOver={(e) => handleDragOver(e, node.id)}
256
266
  onDrop={(e) => handleDrop(e, node.id)}
257
267
  onPointerEnter={(e) => {
@@ -276,6 +286,19 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
276
286
  >
277
287
  {isCollapsed ? '▶' : '▼'}
278
288
  </span>
289
+ {node.id !== prefabData.root.id && (
290
+ <span
291
+ style={styles.dragHandle}
292
+ onPointerEnter={(e) => {
293
+ (e.currentTarget as HTMLSpanElement).style.opacity = "0.9";
294
+ }}
295
+ onPointerLeave={(e) => {
296
+ (e.currentTarget as HTMLSpanElement).style.opacity = "0.4";
297
+ }}
298
+ >
299
+ ⋮⋮
300
+ </span>
301
+ )}
279
302
  <span style={styles.idText}>
280
303
  {node.id}
281
304
  </span>
@@ -256,33 +256,6 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
256
256
  </button>
257
257
  </div>
258
258
 
259
- <div style={s.section}>
260
- <label style={s.label}>Mode</label>
261
- <div style={{ display: 'flex', gap: 6 }}>
262
- {["translate", "rotate", "scale"].map(mode => (
263
- <button
264
- key={mode}
265
- onClick={() => setTransformMode(mode as any)}
266
- style={{
267
- ...s.button,
268
- flex: 1,
269
- ...(transformMode === mode ? s.buttonActive : null),
270
- }}
271
- onPointerEnter={(e) => {
272
- if (transformMode !== mode) (e.currentTarget as HTMLButtonElement).style.background = 'rgba(255,255,255,0.08)';
273
- }}
274
- onPointerLeave={(e) => {
275
- if (transformMode !== mode) (e.currentTarget as HTMLButtonElement).style.background = 'transparent';
276
- }}
277
- >
278
- {mode[0].toUpperCase()}
279
- </button>
280
- ))}
281
- </div>
282
- </div>
283
-
284
- {/* Components (legacy renderer removed) */}
285
-
286
259
  {node.components && Object.entries(node.components).map(([key, comp]: [string, any]) => {
287
260
  if (!comp) return null;
288
261
  const componentDef = ALL_COMPONENTS[comp.type];
@@ -322,6 +295,8 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
322
295
  }
323
296
  }))}
324
297
  basePath={basePath}
298
+ transformMode={transformMode}
299
+ setTransformMode={setTransformMode}
325
300
  />
326
301
  ) : null}
327
302
  </div>