react-three-game 0.0.60 → 0.0.61

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 (58) hide show
  1. package/package.json +9 -3
  2. package/.gitattributes +0 -2
  3. package/.github/copilot-instructions.md +0 -83
  4. package/.github/workflows/nextjs.yml +0 -99
  5. package/.gitmodules +0 -3
  6. package/assets/architecture.png +0 -0
  7. package/assets/editor.gif +0 -0
  8. package/assets/favicon.ico +0 -0
  9. package/assets/react-three-game-logo.png +0 -0
  10. package/dist/tools/dragdrop/page.d.ts +0 -1
  11. package/dist/tools/dragdrop/page.js +0 -11
  12. package/dist/tools/prefabeditor/EntityEvents.d.ts +0 -54
  13. package/dist/tools/prefabeditor/EntityEvents.js +0 -85
  14. package/dist/tools/prefabeditor/page.d.ts +0 -1
  15. package/dist/tools/prefabeditor/page.js +0 -5
  16. package/react-three-game-skill/.gitattributes +0 -2
  17. package/react-three-game-skill/README.md +0 -7
  18. package/react-three-game-skill/react-three-game/SKILL.md +0 -514
  19. package/react-three-game-skill/react-three-game/rules/ADVANCED_PHYSICS.md +0 -472
  20. package/react-three-game-skill/react-three-game/rules/LIGHTING.md +0 -6
  21. package/src/helpers/SoundManager.ts +0 -130
  22. package/src/helpers/index.ts +0 -91
  23. package/src/index.ts +0 -59
  24. package/src/shared/ContactShadow.tsx +0 -74
  25. package/src/shared/GameCanvas.tsx +0 -52
  26. package/src/tools/assetviewer/page.tsx +0 -425
  27. package/src/tools/dragdrop/DragDropLoader.tsx +0 -159
  28. package/src/tools/dragdrop/index.ts +0 -4
  29. package/src/tools/dragdrop/modelLoader.ts +0 -204
  30. package/src/tools/dragdrop/page.tsx +0 -45
  31. package/src/tools/prefabeditor/Dropdown.tsx +0 -112
  32. package/src/tools/prefabeditor/EditorContext.tsx +0 -25
  33. package/src/tools/prefabeditor/EditorTree.tsx +0 -452
  34. package/src/tools/prefabeditor/EditorTreeMenus.tsx +0 -307
  35. package/src/tools/prefabeditor/EditorUI.tsx +0 -204
  36. package/src/tools/prefabeditor/EventSystem.tsx +0 -36
  37. package/src/tools/prefabeditor/GameEvents.ts +0 -191
  38. package/src/tools/prefabeditor/InstanceProvider.tsx +0 -466
  39. package/src/tools/prefabeditor/PrefabEditor.tsx +0 -256
  40. package/src/tools/prefabeditor/PrefabRoot.tsx +0 -767
  41. package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +0 -34
  42. package/src/tools/prefabeditor/components/CameraComponent.tsx +0 -117
  43. package/src/tools/prefabeditor/components/ComponentRegistry.ts +0 -40
  44. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +0 -210
  45. package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +0 -47
  46. package/src/tools/prefabeditor/components/GeometryComponent.tsx +0 -133
  47. package/src/tools/prefabeditor/components/Input.tsx +0 -820
  48. package/src/tools/prefabeditor/components/MaterialComponent.tsx +0 -431
  49. package/src/tools/prefabeditor/components/ModelComponent.tsx +0 -176
  50. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +0 -188
  51. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +0 -109
  52. package/src/tools/prefabeditor/components/TextComponent.tsx +0 -137
  53. package/src/tools/prefabeditor/components/TransformComponent.tsx +0 -173
  54. package/src/tools/prefabeditor/components/index.ts +0 -26
  55. package/src/tools/prefabeditor/page.tsx +0 -10
  56. package/src/tools/prefabeditor/styles.ts +0 -235
  57. package/src/tools/prefabeditor/types.ts +0 -20
  58. package/src/tools/prefabeditor/utils.ts +0 -312
@@ -1,767 +0,0 @@
1
- import { MapControls, TransformControls, useHelper } from "@react-three/drei";
2
- import { forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useRef, useState } from "react";
3
- import { BoxHelper, Euler, Group, Matrix4, Object3D, Quaternion, SRGBColorSpace, Texture, TextureLoader, Vector3, } from "three";
4
- import { ThreeEvent } from "@react-three/fiber";
5
-
6
- import { Prefab, GameObject as GameObjectType } from "./types";
7
- import { getComponent, registerComponent, getNonComposableKeys } from "./components/ComponentRegistry";
8
- import components from "./components";
9
- import { loadModel } from "../dragdrop";
10
- import { GameInstance, GameInstanceProvider, useInstanceCheck } from "./InstanceProvider";
11
- import { focusCameraOnObject, updateNode } from "./utils";
12
- import { PhysicsProps } from "./components/PhysicsComponent";
13
- import { EditorContext } from "./EditorContext";
14
-
15
- // Dynamic type to avoid requiring @react-three/rapier when not using physics
16
- type RapierRigidBody = any;
17
-
18
- components.forEach(registerComponent);
19
-
20
- const IDENTITY = new Matrix4();
21
-
22
- export interface PrefabRootRef {
23
- root: Group | null;
24
- rigidBodyRefs: Map<string, any>; // RigidBody refs only populated when using physics
25
- injectModel: (filename: string, model: Object3D) => void;
26
- injectTexture: (filename: string, texture: Texture) => void;
27
- focusNode: (nodeId: string) => void;
28
- }
29
-
30
- export const PrefabRoot = forwardRef<PrefabRootRef, {
31
- editMode?: boolean;
32
- data: Prefab;
33
- onPrefabChange?: (data: Prefab) => void;
34
- selectedId?: string | null;
35
- onSelect?: (id: string | null) => void;
36
- onClick?: (event: ThreeEvent<PointerEvent>, entity: GameObjectType) => void;
37
- basePath?: string;
38
- }>(({ editMode, data, onPrefabChange, selectedId, onSelect, onClick, basePath = "" }, ref) => {
39
-
40
- // optional editor context
41
- const editorContext = useContext(EditorContext);
42
- const transformMode = editorContext?.transformMode ?? "translate";
43
- const snapResolution = editorContext?.snapResolution ?? 0;
44
- const positionSnap = editorContext?.positionSnap ?? 0.5;
45
- const rotationSnap = editorContext?.rotationSnap ?? Math.PI / 4;
46
-
47
- // prefab root state
48
- const [models, setModels] = useState<Record<string, Object3D>>({});
49
- const [textures, setTextures] = useState<Record<string, Texture>>({});
50
- const loading = useRef(new Set<string>());
51
- const failedTextures = useRef(new Set<string>());
52
- const objectRefs = useRef<Record<string, Object3D | null>>({});
53
- const rigidBodyRefs = useRef<Map<string, RapierRigidBody | null>>(new Map());
54
- const [selectedObject, setSelectedObject] = useState<Object3D | null>(null);
55
- const rootRef = useRef<Group>(null);
56
- const controlsRef = useRef<any>(null);
57
-
58
- const injectModel = useCallback((filename: string, model: Object3D) => {
59
- setModels(m => ({ ...m, [filename]: model }));
60
- }, []);
61
-
62
- const injectTexture = useCallback((filename: string, texture: Texture) => {
63
- loading.current.add(filename);
64
- setTextures(t => ({ ...t, [filename]: texture }));
65
- }, []);
66
-
67
- useImperativeHandle(ref, () => ({
68
- root: rootRef.current,
69
- rigidBodyRefs: rigidBodyRefs.current,
70
- injectModel,
71
- injectTexture,
72
- focusNode: (nodeId: string) => {
73
- const object = objectRefs.current[nodeId];
74
- const controls = controlsRef.current;
75
- const camera = controls?.object;
76
-
77
- if (!object || !controls || !camera) return;
78
-
79
- focusCameraOnObject(object, camera, controls.target, () => controls.update?.());
80
- }
81
- }), [injectModel, injectTexture]);
82
-
83
- const registerRef = useCallback((id: string, obj: Object3D | null) => {
84
- objectRefs.current[id] = obj;
85
- if (id === selectedId) setSelectedObject(obj);
86
- }, [selectedId]);
87
-
88
- const registerRigidBodyRef = useCallback((id: string, rb: any) => {
89
- rigidBodyRefs.current.set(id, rb);
90
- }, []);
91
-
92
- useEffect(() => {
93
- const originalError = console.error;
94
- console.error = (...args: any[]) => {
95
- if (typeof args[0] === 'string' && args[0].includes('TransformControls') && args[0].includes('scene graph')) return;
96
- originalError.apply(console, args);
97
- };
98
- return () => { console.error = originalError; };
99
- }, []);
100
-
101
- useEffect(() => {
102
- setSelectedObject(selectedId ? objectRefs.current[selectedId] ?? null : null);
103
- }, [selectedId]);
104
-
105
- const onTransformChange = () => {
106
- if (!selectedId || !onPrefabChange) return;
107
-
108
- const obj = objectRefs.current[selectedId];
109
- if (!obj) return;
110
-
111
- const parentWorld = computeParentWorldMatrix(data.root, selectedId);
112
- const local = parentWorld.clone().invert().multiply(obj.matrixWorld);
113
-
114
- const { position, rotation, scale } = decompose(local);
115
-
116
- const root = updateNode(data.root, selectedId, node => ({
117
- ...node,
118
- components: {
119
- ...node.components,
120
- transform: {
121
- type: "Transform",
122
- properties: { position, rotation, scale },
123
- },
124
- },
125
- }));
126
-
127
- onPrefabChange({ ...data, root });
128
- };
129
-
130
- useEffect(() => {
131
- const modelsToLoad = new Set<string>();
132
- const texturesToLoad = new Set<string>();
133
-
134
- walk(data.root, node => {
135
- node.components?.model?.properties?.filename &&
136
- modelsToLoad.add(node.components.model.properties.filename);
137
- node.components?.material?.properties?.texture &&
138
- texturesToLoad.add(node.components.material.properties.texture);
139
- node.components?.material?.properties?.normalMapTexture &&
140
- texturesToLoad.add(node.components.material.properties.normalMapTexture);
141
- });
142
-
143
- modelsToLoad.forEach(async file => {
144
- if (models[file] || loading.current.has(file)) return;
145
- loading.current.add(file);
146
- const path =
147
- file.startsWith("/")
148
- ? `${basePath}${file}`
149
- : `${basePath}/${file}`;
150
-
151
- const res = await loadModel(path);
152
- const model = res.model;
153
-
154
- if (res.success && model) {
155
- setModels(m => ({ ...m, [file]: model }));
156
- }
157
- });
158
-
159
- const loader = new TextureLoader();
160
- texturesToLoad.forEach(file => {
161
- if (textures[file] || loading.current.has(file) || failedTextures.current.has(file)) return;
162
- loading.current.add(file);
163
-
164
- // Handle full URLs (http/https) or regular paths
165
- const path = file.startsWith("http://") || file.startsWith("https://")
166
- ? file
167
- : file.startsWith("/")
168
- ? `${basePath}${file}`
169
- : `${basePath}/${file}`;
170
-
171
- loader.load(path, tex => {
172
- tex.colorSpace = SRGBColorSpace;
173
- setTextures(t => ({ ...t, [file]: tex }));
174
- }, undefined, (err) => {
175
- console.warn(`Failed to load texture: ${path}`, err);
176
- loading.current.delete(file);
177
- failedTextures.current.add(file);
178
- });
179
- });
180
- }, [data, models, textures]);
181
-
182
- return (
183
- <group ref={rootRef}>
184
- <GameInstanceProvider
185
- models={models}
186
- selectedId={selectedId}
187
- editMode={editMode}
188
- onSelect={editMode ? onSelect : undefined}
189
- registerRef={registerRef}
190
- >
191
- <GameObjectRenderer
192
- gameObject={data.root}
193
- selectedId={selectedId}
194
- onSelect={editMode ? onSelect : undefined}
195
- onClick={onClick}
196
- registerRef={registerRef}
197
- registerRigidBodyRef={registerRigidBodyRef}
198
- loadedModels={models}
199
- loadedTextures={textures}
200
- editMode={editMode}
201
- parentMatrix={IDENTITY}
202
- />
203
- </GameInstanceProvider>
204
-
205
- {editMode && (
206
- <>
207
- <MapControls ref={controlsRef} makeDefault />
208
- {selectedObject && (
209
- <TransformControls
210
- key={`transform-${transformMode}-${positionSnap}-${rotationSnap}-${snapResolution}`}
211
- object={selectedObject}
212
- mode={transformMode}
213
- space="local"
214
- onObjectChange={onTransformChange}
215
- translationSnap={positionSnap > 0 ? positionSnap : undefined}
216
- rotationSnap={rotationSnap > 0 ? rotationSnap : undefined}
217
- scaleSnap={snapResolution > 0 ? snapResolution : undefined}
218
- />
219
- )}
220
- </>
221
- )}
222
- </group>
223
- );
224
- });
225
-
226
- export function GameObjectRenderer(props: RendererProps) {
227
- const node = props.gameObject;
228
- if (!node || node.disabled) return null;
229
-
230
- const isInstanced = node.components?.model?.properties?.instanced;
231
- const prevInstancedRef = useRef<boolean | undefined>(undefined);
232
- const [isTransitioning, setIsTransitioning] = useState(false);
233
-
234
- useEffect(() => {
235
- if (prevInstancedRef.current !== undefined && prevInstancedRef.current !== isInstanced) {
236
- setIsTransitioning(true);
237
- const timer = setTimeout(() => setIsTransitioning(false), 100);
238
- return () => clearTimeout(timer);
239
- }
240
- prevInstancedRef.current = isInstanced;
241
- }, [isInstanced]);
242
-
243
- if (isTransitioning) return null;
244
-
245
- const key = `${node.id}_${isInstanced ? 'instanced' : 'standard'}`;
246
- return isInstanced
247
- ? <InstancedNode key={key} {...props} />
248
- : <StandardNode key={key} {...props} />;
249
- }
250
-
251
- function isPhysicsProps(v: any): v is PhysicsProps {
252
- return v?.type === "fixed" || v?.type === "dynamic";
253
- }
254
-
255
- function InstancedNode({ gameObject, parentMatrix = IDENTITY, editMode, registerRef, selectedId: _selectedId, onSelect, onClick }: RendererProps) {
256
- const world = parentMatrix.clone().multiply(compose(gameObject));
257
- const { position: worldPosition, rotation: worldRotation, scale: worldScale } = decompose(world);
258
- const localTransform = getNodeTransformProps(gameObject);
259
-
260
- const physicsProps = isPhysicsProps(
261
- gameObject.components?.physics?.properties
262
- )
263
- ? gameObject.components?.physics?.properties
264
- : undefined;
265
-
266
- const groupRef = useRef<Group>(null);
267
- const clickValid = useRef(false);
268
-
269
- useEffect(() => {
270
- if (editMode) {
271
- registerRef(gameObject.id, groupRef.current);
272
- return () => registerRef(gameObject.id, null);
273
- }
274
- }, [gameObject.id, registerRef, editMode]);
275
-
276
- const modelUrl = gameObject.components?.model?.properties?.filename;
277
-
278
- if (editMode) {
279
- return (
280
- <>
281
- <group
282
- ref={groupRef}
283
- position={localTransform.position}
284
- rotation={localTransform.rotation}
285
- scale={localTransform.scale}
286
- onPointerDown={(e) => { e.stopPropagation(); clickValid.current = true; }}
287
- onPointerMove={() => { clickValid.current = false; }}
288
- onPointerUp={(e) => {
289
- if (clickValid.current) {
290
- e.stopPropagation();
291
- onSelect?.(gameObject.id);
292
- onClick?.(e, gameObject);
293
- }
294
- clickValid.current = false;
295
- }}
296
- >
297
- <mesh visible={false}>
298
- <boxGeometry args={[0.01, 0.01, 0.01]} />
299
- </mesh>
300
- </group>
301
- <GameInstance
302
- id={gameObject.id}
303
- modelUrl={modelUrl}
304
- position={worldPosition}
305
- rotation={worldRotation}
306
- scale={worldScale}
307
- physics={physicsProps}
308
- />
309
- </>
310
- );
311
- }
312
-
313
- return (
314
- <GameInstance
315
- id={gameObject.id}
316
- modelUrl={gameObject.components?.model?.properties?.filename}
317
- position={worldPosition}
318
- rotation={worldRotation}
319
- scale={worldScale}
320
- physics={physicsProps}
321
- />
322
- );
323
- }
324
-
325
- function StandardNode({
326
- gameObject,
327
- selectedId,
328
- onSelect,
329
- onClick,
330
- registerRef,
331
- registerRigidBodyRef,
332
- loadedModels,
333
- loadedTextures,
334
- editMode,
335
- parentMatrix = IDENTITY,
336
- }: RendererProps) {
337
-
338
- const groupRef = useRef<Object3D | null>(null);
339
- const helperRef = useRef<Object3D | null>(null);
340
- const clickValid = useRef(false);
341
- const isSelected = selectedId === gameObject.id;
342
- const stillInstanced = useInstanceCheck(gameObject.id);
343
-
344
- useHelper(
345
- editMode && isSelected ? helperRef as React.RefObject<Object3D> : null,
346
- BoxHelper,
347
- "cyan"
348
- );
349
-
350
- useEffect(() => {
351
- registerRef(gameObject.id, groupRef.current);
352
- return () => registerRef(gameObject.id, null);
353
- }, [gameObject.id, registerRef]);
354
-
355
- const world = parentMatrix.clone().multiply(compose(gameObject));
356
-
357
- const onDown = (e: ThreeEvent<PointerEvent>) => {
358
- e.stopPropagation();
359
- clickValid.current = true;
360
- };
361
-
362
- const onUp = (e: ThreeEvent<PointerEvent>) => {
363
- if (clickValid.current) {
364
- e.stopPropagation();
365
- onSelect?.(gameObject.id);
366
- onClick?.(e, gameObject);
367
- }
368
- clickValid.current = false;
369
- };
370
-
371
- const physics = gameObject.components?.physics;
372
- const ready = !gameObject.components?.model ||
373
- loadedModels[gameObject.components.model.properties.filename];
374
- const hasPhysics = physics && ready && !stillInstanced;
375
- const transform = getNodeTransformProps(gameObject);
376
- const physicsDef = hasPhysics ? getComponent("Physics") : null;
377
- const isInstanced = gameObject.components?.model?.properties?.instanced;
378
- const physicsKey = `physics_${gameObject.id}_${isInstanced ? 'instanced' : 'standard'}`;
379
- const renderCtx = createRenderContext(loadedModels, loadedTextures, editMode, selectedId, registerRef);
380
- const childNodes = getChildHostComponents(gameObject).length > 0
381
- ? renderHostedChildren(gameObject, renderCtx, world)
382
- : renderSceneChildren(gameObject, world, {
383
- selectedId,
384
- onSelect,
385
- onClick,
386
- registerRef,
387
- registerRigidBodyRef,
388
- loadedModels,
389
- loadedTextures,
390
- editMode,
391
- });
392
-
393
- const inner = (
394
- <group
395
- onPointerDown={editMode ? onDown : undefined}
396
- onPointerMove={editMode ? () => (clickValid.current = false) : undefined}
397
- onPointerUp={editMode ? onUp : undefined}
398
- >
399
- {renderCompositionNode(gameObject, renderCtx, parentMatrix, childNodes)}
400
- </group>
401
- );
402
-
403
- if (editMode) {
404
- return (
405
- <>
406
- <group
407
- ref={groupRef}
408
- position={transform.position}
409
- rotation={transform.rotation}
410
- scale={transform.scale}
411
- >
412
- <mesh visible={false}>
413
- <boxGeometry args={[0.01, 0.01, 0.01]} />
414
- </mesh>
415
- </group>
416
- <group
417
- ref={helperRef}
418
- position={transform.position}
419
- rotation={transform.rotation}
420
- scale={transform.scale}
421
- >
422
- {inner}
423
- </group>
424
- {hasPhysics && physicsDef?.View ? (
425
- <physicsDef.View
426
- key={physicsKey}
427
- properties={physics.properties}
428
- position={transform.position}
429
- rotation={transform.rotation}
430
- scale={transform.scale}
431
- editMode={editMode}
432
- nodeId={gameObject.id}
433
- registerRigidBodyRef={registerRigidBodyRef}
434
- >{inner}</physicsDef.View>
435
- ) : null}
436
- </>
437
- );
438
- }
439
-
440
- if (hasPhysics && physicsDef?.View) {
441
- return (
442
- <physicsDef.View
443
- key={physicsKey}
444
- properties={physics.properties}
445
- position={transform.position}
446
- rotation={transform.rotation}
447
- scale={transform.scale}
448
- editMode={editMode}
449
- nodeId={gameObject.id}
450
- registerRigidBodyRef={registerRigidBodyRef}
451
- >{inner}</physicsDef.View>
452
- );
453
- }
454
-
455
- return (
456
- <group
457
- ref={groupRef}
458
- position={transform.position}
459
- rotation={transform.rotation}
460
- scale={transform.scale}
461
- onPointerDown={onDown}
462
- onPointerMove={() => (clickValid.current = false)}
463
- onPointerUp={onUp}
464
- >
465
- {inner}
466
- </group>
467
- );
468
- }
469
-
470
- interface RendererProps {
471
- gameObject: GameObjectType;
472
- selectedId?: string | null;
473
- onSelect?: (id: string) => void;
474
- onClick?: (event: ThreeEvent<PointerEvent>, entity: GameObjectType) => void;
475
- registerRef: (id: string, obj: Object3D | null) => void;
476
- registerRigidBodyRef: (id: string, rb: any) => void;
477
- loadedModels: Record<string, Object3D>;
478
- loadedTextures: Record<string, Texture>;
479
- editMode?: boolean;
480
- parentMatrix?: Matrix4;
481
- }
482
-
483
- const CHILD_HOST_COMPONENT_TYPES = new Set(["Environment"]);
484
-
485
- function isChildHostType(type: string) {
486
- return CHILD_HOST_COMPONENT_TYPES.has(type);
487
- }
488
-
489
- function getChildHostComponents(gameObject: GameObjectType) {
490
- return Object.entries(gameObject.components ?? {}).flatMap(([key, comp]) => {
491
- if (!comp?.type || !isChildHostType(comp.type)) return [];
492
-
493
- const def = getComponent(comp.type);
494
- if (!def?.View) return [];
495
-
496
- return { key, View: def.View, properties: comp.properties };
497
- });
498
- }
499
-
500
- interface RenderContext {
501
- loadedModels: Record<string, Object3D>;
502
- loadedTextures: Record<string, Texture>;
503
- editMode?: boolean;
504
- selectedId?: string | null;
505
- registerRef: (id: string, obj: Object3D | null) => void;
506
- }
507
-
508
- interface RuntimeChildRendererProps {
509
- selectedId?: string | null;
510
- onSelect?: (id: string) => void;
511
- onClick?: (event: ThreeEvent<PointerEvent>, entity: GameObjectType) => void;
512
- registerRef: (id: string, obj: Object3D | null) => void;
513
- registerRigidBodyRef: (id: string, rb: any) => void;
514
- loadedModels: Record<string, Object3D>;
515
- loadedTextures: Record<string, Texture>;
516
- editMode?: boolean;
517
- }
518
-
519
- function createRenderContext(
520
- loadedModels: Record<string, Object3D>,
521
- loadedTextures: Record<string, Texture>,
522
- editMode: boolean | undefined,
523
- selectedId: string | null | undefined,
524
- registerRef: (id: string, obj: Object3D | null) => void,
525
- ): RenderContext {
526
- return { loadedModels, loadedTextures, editMode, selectedId, registerRef };
527
- }
528
-
529
- function renderSceneChildren(
530
- gameObject: GameObjectType,
531
- parentMatrix: Matrix4,
532
- props: RuntimeChildRendererProps,
533
- ) {
534
- return gameObject.children?.map(child =>
535
- <GameObjectRenderer
536
- key={child.id}
537
- gameObject={child}
538
- selectedId={props.selectedId}
539
- onSelect={props.onSelect}
540
- onClick={props.onClick}
541
- registerRef={props.registerRef}
542
- registerRigidBodyRef={props.registerRigidBodyRef}
543
- loadedModels={props.loadedModels}
544
- loadedTextures={props.loadedTextures}
545
- editMode={props.editMode}
546
- parentMatrix={parentMatrix}
547
- />
548
- );
549
- }
550
-
551
- function walk(node: GameObjectType, fn: (n: GameObjectType) => void) {
552
- fn(node);
553
- node.children?.forEach(c => walk(c, fn));
554
- }
555
-
556
- function compose(node?: GameObjectType | null) {
557
- const { position, rotation, scale } = getNodeTransformProps(node);
558
- return new Matrix4().compose(
559
- new Vector3(...position),
560
- new Quaternion().setFromEuler(new Euler(...rotation)),
561
- new Vector3(...scale)
562
- );
563
- }
564
-
565
- function decompose(m: Matrix4) {
566
- const p = new Vector3(), q = new Quaternion(), s = new Vector3();
567
- m.decompose(p, q, s);
568
- const e = new Euler().setFromQuaternion(q);
569
- return {
570
- position: [p.x, p.y, p.z] as [number, number, number],
571
- rotation: [e.x, e.y, e.z] as [number, number, number],
572
- scale: [s.x, s.y, s.z] as [number, number, number],
573
- };
574
- }
575
-
576
- function getNodeTransformProps(node?: GameObjectType | null) {
577
- const t = node?.components?.transform?.properties;
578
- return {
579
- position: t?.position ?? [0, 0, 0],
580
- rotation: t?.rotation ?? [0, 0, 0],
581
- scale: t?.scale ?? [1, 1, 1],
582
- };
583
- }
584
-
585
- function computeParentWorldMatrix(root: GameObjectType, targetId: string): Matrix4 {
586
- let result: Matrix4 | null = null;
587
-
588
- const visit = (node: GameObjectType, parent: Matrix4) => {
589
- if (node.id === targetId) {
590
- result = parent.clone();
591
- return;
592
- }
593
- const world = parent.clone().multiply(compose(node));
594
- node.children?.forEach(c => !result && visit(c, world));
595
- };
596
-
597
- visit(root, IDENTITY);
598
- return result ?? IDENTITY;
599
- }
600
-
601
- function renderCompositionSubtree(
602
- gameObject: GameObjectType,
603
- ctx: RenderContext,
604
- parentMatrix = IDENTITY
605
- ): React.ReactNode {
606
- if (!gameObject || gameObject.disabled) return null;
607
-
608
- const transform = getNodeTransformProps(gameObject);
609
- const world = parentMatrix.clone().multiply(compose(gameObject));
610
- const childNodes = renderHostedChildren(gameObject, ctx, world);
611
-
612
- return (
613
- <group
614
- key={gameObject.id}
615
- position={transform.position}
616
- rotation={transform.rotation}
617
- scale={transform.scale}
618
- >
619
- {renderCompositionNode(gameObject, ctx, parentMatrix, childNodes)}
620
- </group>
621
- );
622
- }
623
-
624
- function renderHostedChildren(
625
- gameObject: GameObjectType,
626
- ctx: RenderContext,
627
- parentMatrix: Matrix4,
628
- ) {
629
- return gameObject.children?.map(child =>
630
- renderCompositionSubtree(child, ctx, parentMatrix)
631
- );
632
- }
633
-
634
- function renderCompositionNode(
635
- gameObject: GameObjectType,
636
- ctx: RenderContext,
637
- parentMatrix?: Matrix4,
638
- childNodes?: React.ReactNode
639
- ) {
640
- const ownContent = renderNodeOwnContent(gameObject, ctx, parentMatrix);
641
- const siblingContent = renderNodeSiblingComponents(gameObject, ctx, parentMatrix);
642
- const subtree = <>{ownContent}{siblingContent}{childNodes}</>;
643
-
644
- return wrapWithChildHosts(gameObject, ctx, parentMatrix, subtree);
645
- }
646
-
647
- function renderNodeOwnContent(
648
- gameObject: GameObjectType,
649
- ctx: RenderContext,
650
- parentMatrix?: Matrix4
651
- ) {
652
- const geometry = gameObject.components?.geometry;
653
- const material = gameObject.components?.material;
654
- const model = gameObject.components?.model;
655
- const text = gameObject.components?.text;
656
-
657
- const geometryDef = geometry && getComponent("Geometry");
658
- const materialDef = material && getComponent("Material");
659
- const modelDef = model && getComponent("Model");
660
- const textDef = text && getComponent("Text");
661
-
662
- const contextProps = {
663
- loadedModels: ctx.loadedModels,
664
- loadedTextures: ctx.loadedTextures,
665
- editMode: ctx.editMode,
666
- isSelected: ctx.selectedId === gameObject.id,
667
- nodeId: gameObject.id,
668
- parentMatrix,
669
- registerRef: ctx.registerRef,
670
- };
671
-
672
- let core: React.ReactNode;
673
-
674
- if (model && modelDef?.View) {
675
- core = (
676
- <modelDef.View properties={model.properties} {...contextProps}>
677
- {material && materialDef?.View && (
678
- <materialDef.View
679
- key="material"
680
- properties={material.properties}
681
- {...contextProps}
682
- />
683
- )}
684
- </modelDef.View>
685
- );
686
- } else if (geometry && geometryDef?.View) {
687
- core = (
688
- <mesh castShadow receiveShadow>
689
- <geometryDef.View properties={geometry.properties} {...contextProps} />
690
- {material && materialDef?.View && (
691
- <materialDef.View
692
- key="material"
693
- properties={material.properties}
694
- {...contextProps}
695
- />
696
- )}
697
- </mesh>
698
- );
699
- } else if (text && textDef?.View) {
700
- core = (
701
- <>
702
- <textDef.View properties={text.properties} {...contextProps} />
703
- </>
704
- );
705
- } else {
706
- core = null;
707
- }
708
-
709
- return core;
710
- }
711
-
712
- function renderNodeSiblingComponents(
713
- gameObject: GameObjectType,
714
- ctx: RenderContext,
715
- parentMatrix?: Matrix4
716
- ) {
717
- const contextProps = {
718
- loadedModels: ctx.loadedModels,
719
- loadedTextures: ctx.loadedTextures,
720
- editMode: ctx.editMode,
721
- isSelected: ctx.selectedId === gameObject.id,
722
- nodeId: gameObject.id,
723
- parentMatrix,
724
- registerRef: ctx.registerRef,
725
- };
726
-
727
- return Object.entries(gameObject.components ?? {})
728
- .filter(([key]) => !getNonComposableKeys().includes(key))
729
- .flatMap(([key, comp]) => {
730
- if (!comp?.type || isChildHostType(comp.type)) return [];
731
-
732
- const def = getComponent(comp.type);
733
- if (!def?.View) return [];
734
-
735
- return <def.View key={key} properties={comp.properties} {...contextProps} />;
736
- });
737
- }
738
-
739
- function wrapWithChildHosts(
740
- gameObject: GameObjectType,
741
- ctx: RenderContext,
742
- parentMatrix: Matrix4 | undefined,
743
- subtree: React.ReactNode
744
- ) {
745
- const contextProps = {
746
- loadedModels: ctx.loadedModels,
747
- loadedTextures: ctx.loadedTextures,
748
- editMode: ctx.editMode,
749
- isSelected: ctx.selectedId === gameObject.id,
750
- nodeId: gameObject.id,
751
- parentMatrix,
752
- registerRef: ctx.registerRef,
753
- };
754
-
755
- const childHosts = getChildHostComponents(gameObject);
756
-
757
- return childHosts.reduce(
758
- (acc, { key, View, properties }) => (
759
- <View key={key} properties={properties} {...contextProps}>
760
- {acc}
761
- </View>
762
- ),
763
- subtree
764
- );
765
- }
766
-
767
- export default PrefabRoot;