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
@@ -11,74 +11,74 @@ function SpotLightComponentEditor({ component, onUpdate }: { component: any; onU
11
11
  castShadow: component.properties.castShadow ?? true
12
12
  };
13
13
 
14
- return <div className="flex flex-col gap-2">
14
+ return <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
15
15
  <div>
16
- <label className="block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5">Color</label>
17
- <div className="flex gap-0.5">
16
+ <label style={{ display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }}>Color</label>
17
+ <div style={{ display: 'flex', gap: 2 }}>
18
18
  <input
19
19
  type="color"
20
- className="h-5 w-5 bg-transparent border-none cursor-pointer"
20
+ style={{ height: 20, width: 20, backgroundColor: 'transparent', border: 'none', cursor: 'pointer' }}
21
21
  value={props.color}
22
22
  onChange={e => onUpdate({ ...component.properties, 'color': e.target.value })}
23
23
  />
24
24
  <input
25
25
  type="text"
26
- 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"
26
+ 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' }}
27
27
  value={props.color}
28
28
  onChange={e => onUpdate({ ...component.properties, 'color': e.target.value })}
29
29
  />
30
30
  </div>
31
31
  </div>
32
32
  <div>
33
- <label className="block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5">Intensity</label>
33
+ <label style={{ display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }}>Intensity</label>
34
34
  <input
35
35
  type="number"
36
36
  step="0.1"
37
- 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"
37
+ 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' }}
38
38
  value={props.intensity}
39
39
  onChange={e => onUpdate({ ...component.properties, 'intensity': parseFloat(e.target.value) })}
40
40
  />
41
41
  </div>
42
42
  <div>
43
- <label className="block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5">Angle</label>
43
+ <label style={{ display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }}>Angle</label>
44
44
  <input
45
45
  type="number"
46
46
  step="0.1"
47
47
  min="0"
48
48
  max={Math.PI}
49
- 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"
49
+ 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' }}
50
50
  value={props.angle}
51
51
  onChange={e => onUpdate({ ...component.properties, 'angle': parseFloat(e.target.value) })}
52
52
  />
53
53
  </div>
54
54
  <div>
55
- <label className="block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5">Penumbra</label>
55
+ <label style={{ display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }}>Penumbra</label>
56
56
  <input
57
57
  type="number"
58
58
  step="0.1"
59
59
  min="0"
60
60
  max="1"
61
- 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"
61
+ 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' }}
62
62
  value={props.penumbra}
63
63
  onChange={e => onUpdate({ ...component.properties, 'penumbra': parseFloat(e.target.value) })}
64
64
  />
65
65
  </div>
66
66
  <div>
67
- <label className="block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5">Distance</label>
67
+ <label style={{ display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }}>Distance</label>
68
68
  <input
69
69
  type="number"
70
70
  step="1"
71
71
  min="0"
72
- 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"
72
+ 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' }}
73
73
  value={props.distance}
74
74
  onChange={e => onUpdate({ ...component.properties, 'distance': parseFloat(e.target.value) })}
75
75
  />
76
76
  </div>
77
77
  <div>
78
- <label className="block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5">Cast Shadow</label>
78
+ <label style={{ display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }}>Cast Shadow</label>
79
79
  <input
80
80
  type="checkbox"
81
- className="h-4 w-4 bg-black/40 border border-cyan-500/30 cursor-pointer"
81
+ style={{ height: 16, width: 16, backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', cursor: 'pointer' }}
82
82
  checked={props.castShadow}
83
83
  onChange={e => onUpdate({ ...component.properties, 'castShadow': e.target.checked })}
84
84
  />
@@ -1,4 +1,4 @@
1
-
1
+ import { useEffect, useRef, useState } from "react";
2
2
  import { Component } from "./ComponentRegistry";
3
3
 
4
4
  function TransformComponentEditor({ component, onUpdate, transformMode, setTransformMode }: {
@@ -22,10 +22,10 @@ function TransformComponentEditor({ component, onUpdate, transformMode, setTrans
22
22
  },
23
23
  };
24
24
 
25
- return <div className="flex flex-col">
25
+ return <div style={{ display: 'flex', flexDirection: 'column' }}>
26
26
  {transformMode && setTransformMode && (
27
- <div className="mb-2">
28
- <label className="block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-1">Transform Mode</label>
27
+ <div style={{ marginBottom: 8 }}>
28
+ <label style={{ display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 4 }}>Transform Mode</label>
29
29
  <div style={{ display: 'flex', gap: 6 }}>
30
30
  {["translate", "rotate", "scale"].map(mode => (
31
31
  <button
@@ -67,36 +67,131 @@ const TransformComponent: Component = {
67
67
 
68
68
  export default TransformComponent;
69
69
 
70
+ export function Vector3Input({
71
+ label,
72
+ value,
73
+ onChange
74
+ }: {
75
+ label: string;
76
+ value: [number, number, number];
77
+ onChange: (v: [number, number, number]) => void;
78
+ }) {
79
+ const [draft, setDraft] = useState<[string, string, string]>(
80
+ () => value.map(v => v.toString()) as any
81
+ );
82
+
83
+ // Sync external changes (gizmo, undo, etc.)
84
+ useEffect(() => {
85
+ setDraft(value.map(v => v.toString()) as any);
86
+ }, [value[0], value[1], value[2]]);
87
+
88
+ const dragState = useRef<{
89
+ index: number;
90
+ startX: number;
91
+ startValue: number;
92
+ } | null>(null);
93
+
94
+ const commit = (index: number) => {
95
+ const num = parseFloat(draft[index]);
96
+ if (Number.isFinite(num)) {
97
+ const next = [...value] as [number, number, number];
98
+ next[index] = num;
99
+ onChange(next);
100
+ }
101
+ };
102
+
103
+ const startScrub = (e: React.PointerEvent, index: number) => {
104
+ e.preventDefault();
105
+
106
+ dragState.current = {
107
+ index,
108
+ startX: e.clientX,
109
+ startValue: value[index]
110
+ };
111
+
112
+ (e.target as HTMLElement).setPointerCapture(e.pointerId);
113
+ document.body.style.cursor = "ew-resize";
114
+ };
115
+
116
+ const onScrubMove = (e: React.PointerEvent) => {
117
+ if (!dragState.current) return;
118
+
119
+ const { index, startX, startValue } = dragState.current;
120
+ const dx = e.clientX - startX;
121
+
122
+ let speed = 0.02;
123
+ if (e.shiftKey) speed *= 0.1; // fine
124
+ if (e.altKey) speed *= 5; // coarse
70
125
 
71
- export function Vector3Input({ label, value, onChange }: { label: string, value: [number, number, number], onChange: (v: [number, number, number]) => void }) {
72
- const handleChange = (index: number, val: string) => {
73
- const newValue = [...value] as [number, number, number];
74
- newValue[index] = parseFloat(val) || 0;
75
- onChange(newValue);
126
+ const nextValue = startValue + dx * speed;
127
+ const next = [...value] as [number, number, number];
128
+ next[index] = nextValue;
129
+
130
+ setDraft(d => {
131
+ const copy = [...d] as any;
132
+ copy[index] = nextValue.toFixed(3);
133
+ return copy;
134
+ });
135
+
136
+ onChange(next);
137
+ };
138
+
139
+ const endScrub = (e: React.PointerEvent) => {
140
+ if (!dragState.current) return;
141
+
142
+ dragState.current = null;
143
+ document.body.style.cursor = "";
144
+ (e.target as HTMLElement).releasePointerCapture(e.pointerId);
76
145
  };
77
146
 
78
147
  const axes = [
79
- { key: 'x', color: 'red', index: 0 },
80
- { key: 'y', color: 'green', index: 1 },
81
- { key: 'z', color: 'blue', index: 2 }
148
+ { key: "x", color: "red", index: 0 },
149
+ { key: "y", color: "green", index: 1 },
150
+ { key: "z", color: "blue", index: 2 }
82
151
  ] as const;
83
152
 
84
- return <div className="mb-2">
85
- <label className="block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-1">{label}</label>
86
- <div className="flex gap-1">
87
- {axes.map(({ key, color, index }) => (
88
- <div key={key} 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]">
89
- <span className={`text-xs font-bold text-${color}-400 w-3`}>{key.toUpperCase()}</span>
90
- <input
91
- className="flex-1 bg-transparent text-xs text-cyan-200 font-mono outline-none w-full min-w-0"
92
- type="number"
93
- step="0.1"
94
- value={value[index].toFixed(2)}
95
- onChange={e => handleChange(index, e.target.value)}
96
- onFocus={e => e.target.select()}
97
- />
98
- </div>
99
- ))}
153
+ return (
154
+ <div style={{ marginBottom: 8 }}>
155
+ <label style={{ display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 4 }}>
156
+ {label}
157
+ </label>
158
+
159
+ <div style={{ display: 'flex', gap: 4 }}>
160
+ {axes.map(({ key, color, index }) => (
161
+ <div
162
+ key={key}
163
+ 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 }}
164
+ >
165
+ {/* SCRUB HANDLE */}
166
+ <span
167
+ 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' }}
168
+ onPointerDown={e => startScrub(e, index)}
169
+ onPointerMove={onScrubMove}
170
+ onPointerUp={endScrub}
171
+ >
172
+ {key.toUpperCase()}
173
+ </span>
174
+
175
+ {/* TEXT INPUT */}
176
+ <input
177
+ style={{ flex: 1, backgroundColor: 'transparent', fontSize: '12px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none', width: '100%', minWidth: 0 }}
178
+ type="text"
179
+ value={draft[index]}
180
+ onChange={e => {
181
+ const next = [...draft] as any;
182
+ next[index] = e.target.value;
183
+ setDraft(next);
184
+ }}
185
+ onBlur={() => commit(index)}
186
+ onKeyDown={e => {
187
+ if (e.key === "Enter") {
188
+ (e.target as HTMLInputElement).blur();
189
+ }
190
+ }}
191
+ />
192
+ </div>
193
+ ))}
194
+ </div>
100
195
  </div>
101
- </div>
102
- }
196
+ );
197
+ }
@@ -6,6 +6,7 @@ export interface Prefab {
6
6
 
7
7
  export interface GameObject {
8
8
  id: string;
9
+ name?: string;
9
10
  disabled?: boolean;
10
11
  hidden?: boolean;
11
12
  children?: GameObject[];
@@ -69,6 +69,7 @@ export function cloneNode(node: GameObject): GameObject {
69
69
  return {
70
70
  ...node,
71
71
  id: crypto.randomUUID(),
72
+ name: `${node.name ?? "Node"} Copy`,
72
73
  children: node.children?.map(cloneNode)
73
74
  };
74
75
  }
@@ -78,3 +79,32 @@ export function getComponent<T = any>(node: GameObject, type: string): T | undef
78
79
  const comp = Object.values(node.components ?? {}).find(c => c?.type === type);
79
80
  return comp?.properties as T | undefined;
80
81
  }
82
+
83
+ export function updateNodeById(
84
+ root: GameObject,
85
+ id: string,
86
+ updater: (node: GameObject) => GameObject
87
+ ): GameObject {
88
+ if (root.id === id) {
89
+ return updater(root);
90
+ }
91
+
92
+ if (!root.children) {
93
+ return root;
94
+ }
95
+
96
+ let didChange = false;
97
+
98
+ const newChildren = root.children.map(child => {
99
+ const updated = updateNodeById(child, id, updater);
100
+ if (updated !== child) didChange = true;
101
+ return updated;
102
+ });
103
+
104
+ if (!didChange) return root;
105
+
106
+ return {
107
+ ...root,
108
+ children: newChildren
109
+ };
110
+ }