react-three-game 0.0.36 → 0.0.38

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 (34) hide show
  1. package/dist/index.d.ts +5 -3
  2. package/dist/index.js +5 -5
  3. package/dist/tools/prefabeditor/EditorContext.d.ts +11 -0
  4. package/dist/tools/prefabeditor/EditorContext.js +9 -0
  5. package/dist/tools/prefabeditor/EditorTree.d.ts +1 -3
  6. package/dist/tools/prefabeditor/EditorTree.js +38 -3
  7. package/dist/tools/prefabeditor/EditorUI.d.ts +1 -5
  8. package/dist/tools/prefabeditor/EditorUI.js +4 -2
  9. package/dist/tools/prefabeditor/ExportHelper.d.ts +7 -0
  10. package/dist/tools/prefabeditor/ExportHelper.js +55 -0
  11. package/dist/tools/prefabeditor/InstanceProvider.d.ts +1 -0
  12. package/dist/tools/prefabeditor/InstanceProvider.js +51 -7
  13. package/dist/tools/prefabeditor/PrefabEditor.d.ts +10 -2
  14. package/dist/tools/prefabeditor/PrefabEditor.js +60 -53
  15. package/dist/tools/prefabeditor/PrefabRoot.d.ts +7 -2
  16. package/dist/tools/prefabeditor/PrefabRoot.js +78 -49
  17. package/dist/tools/prefabeditor/components/Input.d.ts +2 -1
  18. package/dist/tools/prefabeditor/components/Input.js +9 -3
  19. package/dist/tools/prefabeditor/components/PhysicsComponent.js +8 -7
  20. package/dist/tools/prefabeditor/components/TransformComponent.js +11 -3
  21. package/dist/tools/prefabeditor/utils.d.ts +7 -1
  22. package/dist/tools/prefabeditor/utils.js +41 -0
  23. package/package.json +1 -1
  24. package/src/index.ts +12 -12
  25. package/src/tools/prefabeditor/EditorContext.tsx +20 -0
  26. package/src/tools/prefabeditor/EditorTree.tsx +83 -22
  27. package/src/tools/prefabeditor/EditorUI.tsx +2 -10
  28. package/src/tools/prefabeditor/InstanceProvider.tsx +60 -4
  29. package/src/tools/prefabeditor/PrefabEditor.tsx +80 -51
  30. package/src/tools/prefabeditor/PrefabRoot.tsx +181 -69
  31. package/src/tools/prefabeditor/components/Input.tsx +11 -3
  32. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +20 -7
  33. package/src/tools/prefabeditor/components/TransformComponent.tsx +25 -4
  34. package/src/tools/prefabeditor/utils.ts +43 -1
@@ -4,17 +4,14 @@ import EditorTree from './EditorTree';
4
4
  import { getAllComponents } from './components/ComponentRegistry';
5
5
  import { base, inspector } from './styles';
6
6
  import { findNode, updateNode, deleteNode } from './utils';
7
+ import { useEditorContext } from './EditorContext';
7
8
 
8
9
  function EditorUI({
9
10
  prefabData,
10
11
  setPrefabData,
11
12
  selectedId,
12
13
  setSelectedId,
13
- transformMode,
14
- setTransformMode,
15
14
  basePath,
16
- onSave,
17
- onLoad,
18
15
  onUndo,
19
16
  onRedo,
20
17
  canUndo,
@@ -24,17 +21,14 @@ function EditorUI({
24
21
  setPrefabData?: Dispatch<SetStateAction<Prefab>>;
25
22
  selectedId: string | null;
26
23
  setSelectedId: Dispatch<SetStateAction<string | null>>;
27
- transformMode: "translate" | "rotate" | "scale";
28
- setTransformMode: (m: "translate" | "rotate" | "scale") => void;
29
24
  basePath?: string;
30
- onSave?: () => void;
31
- onLoad?: () => void;
32
25
  onUndo?: () => void;
33
26
  onRedo?: () => void;
34
27
  canUndo?: boolean;
35
28
  canRedo?: boolean;
36
29
  }) {
37
30
  const [collapsed, setCollapsed] = useState(false);
31
+ const { transformMode, setTransformMode } = useEditorContext();
38
32
 
39
33
  const updateNodeHandler = (updater: (n: GameObjectType) => GameObjectType) => {
40
34
  if (!prefabData || !setPrefabData || !selectedId) return;
@@ -81,8 +75,6 @@ function EditorUI({
81
75
  setPrefabData={setPrefabData}
82
76
  selectedId={selectedId}
83
77
  setSelectedId={setSelectedId}
84
- onSave={onSave}
85
- onLoad={onLoad}
86
78
  onUndo={onUndo}
87
79
  onRedo={onRedo}
88
80
  canUndo={canUndo}
@@ -40,6 +40,7 @@ type GameInstanceContextType = {
40
40
  instances: InstanceData[];
41
41
  meshes: Record<string, Mesh>;
42
42
  modelParts?: Record<string, number>;
43
+ hasInstance: (id: string) => boolean;
43
44
  };
44
45
  const GameInstanceContext = createContext<GameInstanceContextType | null>(null);
45
46
 
@@ -84,6 +85,10 @@ export function GameInstanceProvider({
84
85
  });
85
86
  }, []);
86
87
 
88
+ const hasInstance = useCallback((id: string) => {
89
+ return instances.some(i => i.id === id);
90
+ }, [instances]);
91
+
87
92
  // Flatten all model meshes once (models → flat mesh parts)
88
93
  // Note: Geometry is cloned with baked transforms for instancing
89
94
  const { flatMeshes, modelParts } = useMemo(() => {
@@ -138,7 +143,8 @@ export function GameInstanceProvider({
138
143
  removeInstance,
139
144
  instances,
140
145
  meshes: flatMeshes,
141
- modelParts
146
+ modelParts,
147
+ hasInstance
142
148
  }}
143
149
  >
144
150
  {/* Render normal prefab hierarchy (non-instanced objects) */}
@@ -158,6 +164,8 @@ export function GameInstanceProvider({
158
164
  modelKey={modelKey}
159
165
  partCount={partCount}
160
166
  flatMeshes={flatMeshes}
167
+ onSelect={onSelect}
168
+ editMode={editMode}
161
169
  />
162
170
  );
163
171
  })}
@@ -208,14 +216,19 @@ function InstancedRigidGroup({
208
216
  group,
209
217
  modelKey,
210
218
  partCount,
211
- flatMeshes
219
+ flatMeshes,
220
+ onSelect,
221
+ editMode
212
222
  }: {
213
223
  group: { physicsType: string, instances: InstanceData[] },
214
224
  modelKey: string,
215
225
  partCount: number,
216
- flatMeshes: Record<string, Mesh>
226
+ flatMeshes: Record<string, Mesh>,
227
+ onSelect?: (id: string | null) => void,
228
+ editMode?: boolean
217
229
  }) {
218
230
  const meshRefs = useRef<(InstancedMesh | null)[]>([]);
231
+ const rigidBodiesRef = useRef<any>(null);
219
232
 
220
233
  const instances = useMemo(
221
234
  () => group.instances.map(inst => ({
@@ -248,12 +261,48 @@ function InstancedRigidGroup({
248
261
  });
249
262
  mesh.instanceMatrix.needsUpdate = true;
250
263
  });
264
+
265
+ // Update rigid body positions when instances change
266
+ if (rigidBodiesRef.current) {
267
+ try {
268
+ group.instances.forEach((inst, i) => {
269
+ const body = rigidBodiesRef.current?.at(i);
270
+ if (body && body.setTranslation && body.setRotation) {
271
+ pos.set(...inst.position);
272
+ euler.set(...inst.rotation);
273
+ quat.setFromEuler(euler);
274
+ body.setTranslation(pos, false);
275
+ body.setRotation(quat, false);
276
+ }
277
+ });
278
+ } catch (error) {
279
+ // Ignore errors when switching between instanced/non-instanced states
280
+ console.warn('Failed to update rigidbody positions:', error);
281
+ }
282
+ }
251
283
  }, [group.instances]);
252
284
 
253
285
  const colliders = group.physicsType === 'fixed' ? 'trimesh' : 'hull';
254
286
 
287
+ // Handle click on instanced mesh in edit mode
288
+ const handleClick = (e: any) => {
289
+ if (!editMode || !onSelect) return;
290
+ e.stopPropagation();
291
+
292
+ // Get the instance index from the intersection
293
+ const instanceId = e.instanceId;
294
+ if (instanceId !== undefined && group.instances[instanceId]) {
295
+ onSelect(group.instances[instanceId].id);
296
+ }
297
+ };
298
+
299
+ // Add key to force remount when instance count changes significantly (helps with cleanup)
300
+ const rigidBodyKey = `rb_${modelKey}_${group.physicsType}_${group.instances.length}`;
301
+
255
302
  return (
256
303
  <InstancedRigidBodies
304
+ key={rigidBodyKey}
305
+ ref={rigidBodiesRef}
257
306
  instances={instances}
258
307
  colliders={colliders}
259
308
  type={group.physicsType as 'dynamic' | 'fixed'}
@@ -269,6 +318,7 @@ function InstancedRigidGroup({
269
318
  castShadow
270
319
  receiveShadow
271
320
  frustumCulled={false}
321
+ onClick={editMode ? handleClick : undefined}
272
322
  />
273
323
  );
274
324
  })}
@@ -368,6 +418,12 @@ function InstanceGroupItem({
368
418
  }
369
419
 
370
420
 
421
+ // Hook to check if an instance exists
422
+ export function useInstanceCheck(id: string): boolean {
423
+ const ctx = useContext(GameInstanceContext);
424
+ return ctx?.hasInstance(id) ?? false;
425
+ }
426
+
371
427
  // GameInstance component: registers an instance for batch rendering (renders nothing itself)
372
428
  export const GameInstance = React.forwardRef<Group, {
373
429
  id: string;
@@ -395,7 +451,7 @@ export const GameInstance = React.forwardRef<Group, {
395
451
  rotation,
396
452
  scale,
397
453
  physics,
398
- }), [id, modelUrl, position, rotation, scale, physics]);
454
+ }), [id, modelUrl, JSON.stringify(position), JSON.stringify(rotation), JSON.stringify(scale), JSON.stringify(physics)]);
399
455
 
400
456
  useEffect(() => {
401
457
  if (!addInstance || !removeInstance) return;
@@ -1,12 +1,23 @@
1
1
  "use client";
2
2
 
3
3
  import GameCanvas from "../../shared/GameCanvas";
4
- import { useState, useRef, useEffect } from "react";
4
+ import { useState, useRef, useEffect, forwardRef, useImperativeHandle } from "react";
5
5
  import { Prefab } from "./types";
6
- import PrefabRoot from "./PrefabRoot";
6
+ import PrefabRoot, { PrefabRootRef } from "./PrefabRoot";
7
7
  import { Physics } from "@react-three/rapier";
8
8
  import EditorUI from "./EditorUI";
9
9
  import { base, toolbar } from "./styles";
10
+ import { EditorContext } from "./EditorContext";
11
+ import { GLTFExporter } from "three/examples/jsm/exporters/GLTFExporter.js";
12
+ import { Group } from "three";
13
+
14
+ export interface PrefabEditorRef {
15
+ screenshot: () => void;
16
+ exportGLB: () => void;
17
+ prefab: Prefab;
18
+ setPrefab: (prefab: Prefab) => void;
19
+ rootRef: React.RefObject<PrefabRootRef | null>;
20
+ }
10
21
 
11
22
  const DEFAULT_PREFAB: Prefab = {
12
23
  id: "prefab-default",
@@ -22,20 +33,23 @@ const DEFAULT_PREFAB: Prefab = {
22
33
  }
23
34
  };
24
35
 
25
- const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }: {
36
+ const PrefabEditor = forwardRef<PrefabEditorRef, {
26
37
  basePath?: string;
27
38
  initialPrefab?: Prefab;
28
39
  onPrefabChange?: (prefab: Prefab) => void;
29
40
  children?: React.ReactNode;
30
- }) => {
41
+ }>(({ basePath, initialPrefab, onPrefabChange, children }, ref) => {
31
42
  const [editMode, setEditMode] = useState(true);
32
43
  const [loadedPrefab, setLoadedPrefab] = useState<Prefab>(initialPrefab ?? DEFAULT_PREFAB);
33
44
  const [selectedId, setSelectedId] = useState<string | null>(null);
34
45
  const [transformMode, setTransformMode] = useState<"translate" | "rotate" | "scale">("translate");
46
+ const [snapResolution, setSnapResolution] = useState(0);
35
47
  const [history, setHistory] = useState<Prefab[]>([loadedPrefab]);
36
48
  const [historyIndex, setHistoryIndex] = useState(0);
37
49
  const throttleRef = useRef<NodeJS.Timeout | null>(null);
38
50
  const lastDataRef = useRef(JSON.stringify(loadedPrefab));
51
+ const prefabRootRef = useRef<PrefabRootRef>(null);
52
+ const canvasRef = useRef<HTMLCanvasElement | null>(null);
39
53
 
40
54
  useEffect(() => {
41
55
  if (initialPrefab) setLoadedPrefab(initialPrefab);
@@ -84,29 +98,76 @@ const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }: {
84
98
  return () => { if (throttleRef.current) clearTimeout(throttleRef.current); };
85
99
  }, [loadedPrefab]);
86
100
 
87
- const handleLoad = async () => {
88
- const prefab = await loadJson();
89
- if (prefab) {
90
- setLoadedPrefab(prefab);
91
- onPrefabChange?.(prefab);
92
- setHistory([prefab]);
93
- setHistoryIndex(0);
94
- lastDataRef.current = JSON.stringify(prefab);
95
- }
101
+ const handleScreenshot = () => {
102
+ const canvas = canvasRef.current;
103
+ if (!canvas) return;
104
+
105
+ canvas.toBlob((blob) => {
106
+ if (!blob) return;
107
+ const url = URL.createObjectURL(blob);
108
+ const a = document.createElement('a');
109
+ a.href = url;
110
+ a.download = `${loadedPrefab.name || 'screenshot'}.png`;
111
+ a.click();
112
+ URL.revokeObjectURL(url);
113
+ });
114
+ };
115
+
116
+ const handleExportGLB = () => {
117
+ const sceneRoot = prefabRootRef.current?.root;
118
+ if (!sceneRoot) return;
119
+
120
+ const exporter = new GLTFExporter();
121
+ exporter.parse(
122
+ sceneRoot,
123
+ (result) => {
124
+ const blob = new Blob([result as ArrayBuffer], { type: 'application/octet-stream' });
125
+ const url = URL.createObjectURL(blob);
126
+ const a = document.createElement('a');
127
+ a.href = url;
128
+ a.download = `${loadedPrefab.name || 'scene'}.glb`;
129
+ a.click();
130
+ URL.revokeObjectURL(url);
131
+ },
132
+ (error) => {
133
+ console.error('Error exporting GLB:', error);
134
+ },
135
+ { binary: true }
136
+ );
96
137
  };
97
138
 
98
- return <>
139
+ useEffect(() => {
140
+ const canvas = document.querySelector('canvas');
141
+ if (canvas) canvasRef.current = canvas;
142
+ }, []);
143
+
144
+ useImperativeHandle(ref, () => ({
145
+ screenshot: handleScreenshot,
146
+ exportGLB: handleExportGLB,
147
+ prefab: loadedPrefab,
148
+ setPrefab: setLoadedPrefab,
149
+ rootRef: prefabRootRef
150
+ }), [loadedPrefab]);
151
+
152
+ return <EditorContext.Provider value={{
153
+ transformMode,
154
+ setTransformMode,
155
+ snapResolution,
156
+ setSnapResolution,
157
+ onScreenshot: handleScreenshot,
158
+ onExportGLB: handleExportGLB
159
+ }}>
99
160
  <GameCanvas>
100
- <Physics paused={editMode}>
161
+ <Physics debug={editMode} paused={editMode}>
101
162
  <ambientLight intensity={1.5} />
102
163
  <gridHelper args={[10, 10]} position={[0, -1, 0]} />
103
164
  <PrefabRoot
165
+ ref={prefabRootRef}
104
166
  data={loadedPrefab}
105
167
  editMode={editMode}
106
168
  onPrefabChange={updatePrefab}
107
169
  selectedId={selectedId}
108
170
  onSelect={setSelectedId}
109
- transformMode={transformMode}
110
171
  basePath={basePath}
111
172
  />
112
173
  {children}
@@ -123,47 +184,15 @@ const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }: {
123
184
  setPrefabData={updatePrefab}
124
185
  selectedId={selectedId}
125
186
  setSelectedId={setSelectedId}
126
- transformMode={transformMode}
127
- setTransformMode={setTransformMode}
128
187
  basePath={basePath}
129
- onSave={() => saveJson(loadedPrefab, "prefab")}
130
- onLoad={handleLoad}
131
188
  onUndo={undo}
132
189
  onRedo={redo}
133
190
  canUndo={historyIndex > 0}
134
191
  canRedo={historyIndex < history.length - 1}
135
192
  />}
136
- </>
137
- }
138
-
139
-
140
- const saveJson = (data: Prefab, filename: string) => {
141
- const a = document.createElement('a');
142
- a.href = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data, null, 2));
143
- a.download = `${filename || 'prefab'}.json`;
144
- a.click();
145
- };
146
-
147
- const loadJson = () => new Promise<Prefab | undefined>(resolve => {
148
- const input = document.createElement('input');
149
- input.type = 'file';
150
- input.accept = '.json,application/json';
151
- input.onchange = e => {
152
- const file = (e.target as HTMLInputElement).files?.[0];
153
- if (!file) return resolve(undefined);
154
- const reader = new FileReader();
155
- reader.onload = e => {
156
- try {
157
- const text = e.target?.result;
158
- if (typeof text === 'string') resolve(JSON.parse(text) as Prefab);
159
- } catch (err) {
160
- console.error('Error parsing prefab JSON:', err);
161
- resolve(undefined);
162
- }
163
- };
164
- reader.readAsText(file);
165
- };
166
- input.click();
193
+ </EditorContext.Provider>
167
194
  });
168
195
 
196
+ PrefabEditor.displayName = "PrefabEditor";
197
+
169
198
  export default PrefabEditor;