react-three-game 0.0.56 → 0.0.58
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.
- package/README.md +16 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/shared/GameCanvas.js +1 -3
- package/dist/tools/assetviewer/page.js +35 -14
- package/dist/tools/prefabeditor/Dropdown.d.ts +15 -0
- package/dist/tools/prefabeditor/Dropdown.js +82 -0
- package/dist/tools/prefabeditor/EditorContext.d.ts +5 -0
- package/dist/tools/prefabeditor/EditorTree.js +149 -91
- package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +33 -0
- package/dist/tools/prefabeditor/EditorTreeMenus.js +136 -0
- package/dist/tools/prefabeditor/EditorUI.js +1 -1
- package/dist/tools/prefabeditor/PrefabEditor.d.ts +1 -0
- package/dist/tools/prefabeditor/PrefabEditor.js +13 -2
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +1 -0
- package/dist/tools/prefabeditor/PrefabRoot.js +120 -34
- package/dist/tools/prefabeditor/components/AmbientLightComponent.js +3 -7
- package/dist/tools/prefabeditor/components/CameraComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/CameraComponent.js +45 -0
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +50 -24
- package/dist/tools/prefabeditor/components/EnvironmentComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/EnvironmentComponent.js +15 -0
- package/dist/tools/prefabeditor/components/GeometryComponent.js +46 -46
- package/dist/tools/prefabeditor/components/Input.d.ts +51 -1
- package/dist/tools/prefabeditor/components/Input.js +73 -21
- package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +16 -2
- package/dist/tools/prefabeditor/components/MaterialComponent.js +129 -15
- package/dist/tools/prefabeditor/components/ModelComponent.js +44 -3
- package/dist/tools/prefabeditor/components/PhysicsComponent.js +16 -81
- package/dist/tools/prefabeditor/components/SpotLightComponent.js +36 -23
- package/dist/tools/prefabeditor/components/TextComponent.js +7 -53
- package/dist/tools/prefabeditor/components/TransformComponent.js +18 -8
- package/dist/tools/prefabeditor/components/index.js +5 -1
- package/dist/tools/prefabeditor/styles.d.ts +5 -2
- package/dist/tools/prefabeditor/styles.js +7 -3
- package/dist/tools/prefabeditor/utils.d.ts +4 -3
- package/dist/tools/prefabeditor/utils.js +53 -5
- package/package.json +1 -1
- package/react-three-game-skill/react-three-game/SKILL.md +4 -1
- package/src/index.ts +7 -0
- package/src/shared/GameCanvas.tsx +0 -3
- package/src/tools/assetviewer/page.tsx +77 -45
- package/src/tools/prefabeditor/Dropdown.tsx +112 -0
- package/src/tools/prefabeditor/EditorContext.tsx +5 -0
- package/src/tools/prefabeditor/EditorTree.tsx +242 -178
- package/src/tools/prefabeditor/EditorTreeMenus.tsx +307 -0
- package/src/tools/prefabeditor/EditorUI.tsx +1 -1
- package/src/tools/prefabeditor/PrefabEditor.tsx +17 -4
- package/src/tools/prefabeditor/PrefabRoot.tsx +208 -58
- package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +5 -11
- package/src/tools/prefabeditor/components/CameraComponent.tsx +117 -0
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +61 -30
- package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +47 -0
- package/src/tools/prefabeditor/components/GeometryComponent.tsx +69 -63
- package/src/tools/prefabeditor/components/Input.tsx +220 -27
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +189 -18
- package/src/tools/prefabeditor/components/ModelComponent.tsx +51 -4
- package/src/tools/prefabeditor/components/PhysicsComponent.tsx +44 -85
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +52 -27
- package/src/tools/prefabeditor/components/TextComponent.tsx +58 -57
- package/src/tools/prefabeditor/components/TransformComponent.tsx +61 -9
- package/src/tools/prefabeditor/components/index.ts +5 -1
- package/src/tools/prefabeditor/styles.ts +7 -3
- package/src/tools/prefabeditor/utils.ts +55 -4
|
@@ -8,7 +8,7 @@ import { getComponent, registerComponent, getNonComposableKeys } from "./compone
|
|
|
8
8
|
import components from "./components";
|
|
9
9
|
import { loadModel } from "../dragdrop/modelLoader";
|
|
10
10
|
import { GameInstance, GameInstanceProvider, useInstanceCheck } from "./InstanceProvider";
|
|
11
|
-
import { updateNode } from "./utils";
|
|
11
|
+
import { focusCameraOnObject, updateNode } from "./utils";
|
|
12
12
|
import { PhysicsProps } from "./components/PhysicsComponent";
|
|
13
13
|
import { EditorContext } from "./EditorContext";
|
|
14
14
|
|
|
@@ -24,6 +24,7 @@ export interface PrefabRootRef {
|
|
|
24
24
|
rigidBodyRefs: Map<string, any>; // RigidBody refs only populated when using physics
|
|
25
25
|
injectModel: (filename: string, model: Object3D) => void;
|
|
26
26
|
injectTexture: (filename: string, file: File) => void;
|
|
27
|
+
focusNode: (nodeId: string) => void;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export const PrefabRoot = forwardRef<PrefabRootRef, {
|
|
@@ -40,15 +41,19 @@ export const PrefabRoot = forwardRef<PrefabRootRef, {
|
|
|
40
41
|
const editorContext = useContext(EditorContext);
|
|
41
42
|
const transformMode = editorContext?.transformMode ?? "translate";
|
|
42
43
|
const snapResolution = editorContext?.snapResolution ?? 0;
|
|
44
|
+
const positionSnap = editorContext?.positionSnap ?? 0.5;
|
|
45
|
+
const rotationSnap = editorContext?.rotationSnap ?? Math.PI / 4;
|
|
43
46
|
|
|
44
47
|
// prefab root state
|
|
45
48
|
const [models, setModels] = useState<Record<string, Object3D>>({});
|
|
46
49
|
const [textures, setTextures] = useState<Record<string, Texture>>({});
|
|
47
50
|
const loading = useRef(new Set<string>());
|
|
51
|
+
const failedTextures = useRef(new Set<string>());
|
|
48
52
|
const objectRefs = useRef<Record<string, Object3D | null>>({});
|
|
49
53
|
const rigidBodyRefs = useRef<Map<string, RapierRigidBody | null>>(new Map());
|
|
50
54
|
const [selectedObject, setSelectedObject] = useState<Object3D | null>(null);
|
|
51
55
|
const rootRef = useRef<Group>(null);
|
|
56
|
+
const controlsRef = useRef<any>(null);
|
|
52
57
|
|
|
53
58
|
const injectModel = useCallback((filename: string, model: Object3D) => {
|
|
54
59
|
setModels(m => ({ ...m, [filename]: model }));
|
|
@@ -69,7 +74,16 @@ export const PrefabRoot = forwardRef<PrefabRootRef, {
|
|
|
69
74
|
root: rootRef.current,
|
|
70
75
|
rigidBodyRefs: rigidBodyRefs.current,
|
|
71
76
|
injectModel,
|
|
72
|
-
injectTexture
|
|
77
|
+
injectTexture,
|
|
78
|
+
focusNode: (nodeId: string) => {
|
|
79
|
+
const object = objectRefs.current[nodeId];
|
|
80
|
+
const controls = controlsRef.current;
|
|
81
|
+
const camera = controls?.object;
|
|
82
|
+
|
|
83
|
+
if (!object || !controls || !camera) return;
|
|
84
|
+
|
|
85
|
+
focusCameraOnObject(object, camera, controls.target, () => controls.update?.());
|
|
86
|
+
}
|
|
73
87
|
}), [injectModel, injectTexture]);
|
|
74
88
|
|
|
75
89
|
const registerRef = useCallback((id: string, obj: Object3D | null) => {
|
|
@@ -128,6 +142,8 @@ export const PrefabRoot = forwardRef<PrefabRootRef, {
|
|
|
128
142
|
modelsToLoad.add(node.components.model.properties.filename);
|
|
129
143
|
node.components?.material?.properties?.texture &&
|
|
130
144
|
texturesToLoad.add(node.components.material.properties.texture);
|
|
145
|
+
node.components?.material?.properties?.normalMapTexture &&
|
|
146
|
+
texturesToLoad.add(node.components.material.properties.normalMapTexture);
|
|
131
147
|
});
|
|
132
148
|
|
|
133
149
|
modelsToLoad.forEach(async file => {
|
|
@@ -145,7 +161,7 @@ export const PrefabRoot = forwardRef<PrefabRootRef, {
|
|
|
145
161
|
|
|
146
162
|
const loader = new TextureLoader();
|
|
147
163
|
texturesToLoad.forEach(file => {
|
|
148
|
-
if (textures[file] || loading.current.has(file)) return;
|
|
164
|
+
if (textures[file] || loading.current.has(file) || failedTextures.current.has(file)) return;
|
|
149
165
|
loading.current.add(file);
|
|
150
166
|
|
|
151
167
|
// Handle full URLs (http/https) or regular paths
|
|
@@ -159,8 +175,9 @@ export const PrefabRoot = forwardRef<PrefabRootRef, {
|
|
|
159
175
|
tex.colorSpace = SRGBColorSpace;
|
|
160
176
|
setTextures(t => ({ ...t, [file]: tex }));
|
|
161
177
|
}, undefined, (err) => {
|
|
162
|
-
console.
|
|
178
|
+
console.warn(`Failed to load texture: ${path}`, err);
|
|
163
179
|
loading.current.delete(file);
|
|
180
|
+
failedTextures.current.add(file);
|
|
164
181
|
});
|
|
165
182
|
});
|
|
166
183
|
}, [data, models, textures]);
|
|
@@ -190,16 +207,16 @@ export const PrefabRoot = forwardRef<PrefabRootRef, {
|
|
|
190
207
|
|
|
191
208
|
{editMode && (
|
|
192
209
|
<>
|
|
193
|
-
<MapControls makeDefault />
|
|
210
|
+
<MapControls ref={controlsRef} makeDefault />
|
|
194
211
|
{selectedObject && (
|
|
195
212
|
<TransformControls
|
|
196
|
-
key={`transform-${snapResolution}`}
|
|
213
|
+
key={`transform-${transformMode}-${positionSnap}-${rotationSnap}-${snapResolution}`}
|
|
197
214
|
object={selectedObject}
|
|
198
215
|
mode={transformMode}
|
|
199
216
|
space="local"
|
|
200
217
|
onObjectChange={onTransformChange}
|
|
201
|
-
translationSnap={
|
|
202
|
-
rotationSnap={
|
|
218
|
+
translationSnap={positionSnap > 0 ? positionSnap : undefined}
|
|
219
|
+
rotationSnap={rotationSnap > 0 ? rotationSnap : undefined}
|
|
203
220
|
scaleSnap={snapResolution > 0 ? snapResolution : undefined}
|
|
204
221
|
/>
|
|
205
222
|
)}
|
|
@@ -362,6 +379,19 @@ function StandardNode({
|
|
|
362
379
|
const physicsDef = hasPhysics ? getComponent("Physics") : null;
|
|
363
380
|
const isInstanced = gameObject.components?.model?.properties?.instanced;
|
|
364
381
|
const physicsKey = `physics_${gameObject.id}_${isInstanced ? 'instanced' : 'standard'}`;
|
|
382
|
+
const renderCtx = createRenderContext(loadedModels, loadedTextures, editMode, selectedId, registerRef);
|
|
383
|
+
const childNodes = getChildHostComponents(gameObject).length > 0
|
|
384
|
+
? renderHostedChildren(gameObject, renderCtx, world)
|
|
385
|
+
: renderSceneChildren(gameObject, world, {
|
|
386
|
+
selectedId,
|
|
387
|
+
onSelect,
|
|
388
|
+
onClick,
|
|
389
|
+
registerRef,
|
|
390
|
+
registerRigidBodyRef,
|
|
391
|
+
loadedModels,
|
|
392
|
+
loadedTextures,
|
|
393
|
+
editMode,
|
|
394
|
+
});
|
|
365
395
|
|
|
366
396
|
const inner = (
|
|
367
397
|
<group
|
|
@@ -369,22 +399,7 @@ function StandardNode({
|
|
|
369
399
|
onPointerMove={editMode ? () => (clickValid.current = false) : undefined}
|
|
370
400
|
onPointerUp={editMode ? onUp : undefined}
|
|
371
401
|
>
|
|
372
|
-
{
|
|
373
|
-
{gameObject.children?.map(child => (
|
|
374
|
-
<GameObjectRenderer
|
|
375
|
-
key={child.id}
|
|
376
|
-
gameObject={child}
|
|
377
|
-
selectedId={selectedId}
|
|
378
|
-
onSelect={onSelect}
|
|
379
|
-
onClick={onClick}
|
|
380
|
-
registerRef={registerRef}
|
|
381
|
-
registerRigidBodyRef={registerRigidBodyRef}
|
|
382
|
-
loadedModels={loadedModels}
|
|
383
|
-
loadedTextures={loadedTextures}
|
|
384
|
-
editMode={editMode}
|
|
385
|
-
parentMatrix={world}
|
|
386
|
-
/>
|
|
387
|
-
))}
|
|
402
|
+
{renderCompositionNode(gameObject, renderCtx, parentMatrix, childNodes)}
|
|
388
403
|
</group>
|
|
389
404
|
);
|
|
390
405
|
|
|
@@ -468,6 +483,74 @@ interface RendererProps {
|
|
|
468
483
|
parentMatrix?: Matrix4;
|
|
469
484
|
}
|
|
470
485
|
|
|
486
|
+
const CHILD_HOST_COMPONENT_TYPES = new Set(["Environment"]);
|
|
487
|
+
|
|
488
|
+
function isChildHostType(type: string) {
|
|
489
|
+
return CHILD_HOST_COMPONENT_TYPES.has(type);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function getChildHostComponents(gameObject: GameObjectType) {
|
|
493
|
+
return Object.entries(gameObject.components ?? {}).flatMap(([key, comp]) => {
|
|
494
|
+
if (!comp?.type || !isChildHostType(comp.type)) return [];
|
|
495
|
+
|
|
496
|
+
const def = getComponent(comp.type);
|
|
497
|
+
if (!def?.View) return [];
|
|
498
|
+
|
|
499
|
+
return { key, View: def.View, properties: comp.properties };
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
interface RenderContext {
|
|
504
|
+
loadedModels: Record<string, Object3D>;
|
|
505
|
+
loadedTextures: Record<string, Texture>;
|
|
506
|
+
editMode?: boolean;
|
|
507
|
+
selectedId?: string | null;
|
|
508
|
+
registerRef: (id: string, obj: Object3D | null) => void;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
interface RuntimeChildRendererProps {
|
|
512
|
+
selectedId?: string | null;
|
|
513
|
+
onSelect?: (id: string) => void;
|
|
514
|
+
onClick?: (event: ThreeEvent<PointerEvent>, entity: GameObjectType) => void;
|
|
515
|
+
registerRef: (id: string, obj: Object3D | null) => void;
|
|
516
|
+
registerRigidBodyRef: (id: string, rb: any) => void;
|
|
517
|
+
loadedModels: Record<string, Object3D>;
|
|
518
|
+
loadedTextures: Record<string, Texture>;
|
|
519
|
+
editMode?: boolean;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function createRenderContext(
|
|
523
|
+
loadedModels: Record<string, Object3D>,
|
|
524
|
+
loadedTextures: Record<string, Texture>,
|
|
525
|
+
editMode: boolean | undefined,
|
|
526
|
+
selectedId: string | null | undefined,
|
|
527
|
+
registerRef: (id: string, obj: Object3D | null) => void,
|
|
528
|
+
): RenderContext {
|
|
529
|
+
return { loadedModels, loadedTextures, editMode, selectedId, registerRef };
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function renderSceneChildren(
|
|
533
|
+
gameObject: GameObjectType,
|
|
534
|
+
parentMatrix: Matrix4,
|
|
535
|
+
props: RuntimeChildRendererProps,
|
|
536
|
+
) {
|
|
537
|
+
return gameObject.children?.map(child =>
|
|
538
|
+
<GameObjectRenderer
|
|
539
|
+
key={child.id}
|
|
540
|
+
gameObject={child}
|
|
541
|
+
selectedId={props.selectedId}
|
|
542
|
+
onSelect={props.onSelect}
|
|
543
|
+
onClick={props.onClick}
|
|
544
|
+
registerRef={props.registerRef}
|
|
545
|
+
registerRigidBodyRef={props.registerRigidBodyRef}
|
|
546
|
+
loadedModels={props.loadedModels}
|
|
547
|
+
loadedTextures={props.loadedTextures}
|
|
548
|
+
editMode={props.editMode}
|
|
549
|
+
parentMatrix={parentMatrix}
|
|
550
|
+
/>
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
|
|
471
554
|
function walk(node: GameObjectType, fn: (n: GameObjectType) => void) {
|
|
472
555
|
fn(node);
|
|
473
556
|
node.children?.forEach(c => walk(c, fn));
|
|
@@ -518,14 +601,55 @@ function computeParentWorldMatrix(root: GameObjectType, targetId: string): Matri
|
|
|
518
601
|
return result ?? IDENTITY;
|
|
519
602
|
}
|
|
520
603
|
|
|
521
|
-
function
|
|
604
|
+
function renderCompositionSubtree(
|
|
605
|
+
gameObject: GameObjectType,
|
|
606
|
+
ctx: RenderContext,
|
|
607
|
+
parentMatrix = IDENTITY
|
|
608
|
+
): React.ReactNode {
|
|
609
|
+
if (!gameObject || gameObject.disabled) return null;
|
|
610
|
+
|
|
611
|
+
const transform = getNodeTransformProps(gameObject);
|
|
612
|
+
const world = parentMatrix.clone().multiply(compose(gameObject));
|
|
613
|
+
const childNodes = renderHostedChildren(gameObject, ctx, world);
|
|
614
|
+
|
|
615
|
+
return (
|
|
616
|
+
<group
|
|
617
|
+
key={gameObject.id}
|
|
618
|
+
position={transform.position}
|
|
619
|
+
rotation={transform.rotation}
|
|
620
|
+
scale={transform.scale}
|
|
621
|
+
>
|
|
622
|
+
{renderCompositionNode(gameObject, ctx, parentMatrix, childNodes)}
|
|
623
|
+
</group>
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function renderHostedChildren(
|
|
628
|
+
gameObject: GameObjectType,
|
|
629
|
+
ctx: RenderContext,
|
|
630
|
+
parentMatrix: Matrix4,
|
|
631
|
+
) {
|
|
632
|
+
return gameObject.children?.map(child =>
|
|
633
|
+
renderCompositionSubtree(child, ctx, parentMatrix)
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function renderCompositionNode(
|
|
522
638
|
gameObject: GameObjectType,
|
|
523
|
-
ctx:
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
639
|
+
ctx: RenderContext,
|
|
640
|
+
parentMatrix?: Matrix4,
|
|
641
|
+
childNodes?: React.ReactNode
|
|
642
|
+
) {
|
|
643
|
+
const ownContent = renderNodeOwnContent(gameObject, ctx, parentMatrix);
|
|
644
|
+
const siblingContent = renderNodeSiblingComponents(gameObject, ctx, parentMatrix);
|
|
645
|
+
const subtree = <>{ownContent}{siblingContent}{childNodes}</>;
|
|
646
|
+
|
|
647
|
+
return wrapWithChildHosts(gameObject, ctx, parentMatrix, subtree);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function renderNodeOwnContent(
|
|
651
|
+
gameObject: GameObjectType,
|
|
652
|
+
ctx: RenderContext,
|
|
529
653
|
parentMatrix?: Matrix4
|
|
530
654
|
) {
|
|
531
655
|
const geometry = gameObject.components?.geometry;
|
|
@@ -542,31 +666,12 @@ function renderCoreNode(
|
|
|
542
666
|
loadedModels: ctx.loadedModels,
|
|
543
667
|
loadedTextures: ctx.loadedTextures,
|
|
544
668
|
editMode: ctx.editMode,
|
|
669
|
+
isSelected: ctx.selectedId === gameObject.id,
|
|
670
|
+
nodeId: gameObject.id,
|
|
545
671
|
parentMatrix,
|
|
546
672
|
registerRef: ctx.registerRef,
|
|
547
673
|
};
|
|
548
674
|
|
|
549
|
-
const wrappers: Array<{ key: string; View: any; properties: any }> = [];
|
|
550
|
-
const leaves: React.ReactNode[] = [];
|
|
551
|
-
|
|
552
|
-
if (gameObject.components) {
|
|
553
|
-
Object.entries(gameObject.components)
|
|
554
|
-
.filter(([k]) => !getNonComposableKeys().includes(k))
|
|
555
|
-
.forEach(([key, comp]) => {
|
|
556
|
-
if (!comp?.type) return;
|
|
557
|
-
const def = getComponent(comp.type);
|
|
558
|
-
if (!def?.View) return;
|
|
559
|
-
|
|
560
|
-
if (def.View.toString().includes("children")) {
|
|
561
|
-
wrappers.push({ key, View: def.View, properties: comp.properties });
|
|
562
|
-
} else {
|
|
563
|
-
leaves.push(
|
|
564
|
-
<def.View key={key} properties={comp.properties} {...contextProps} />
|
|
565
|
-
);
|
|
566
|
-
}
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
|
|
570
675
|
let core: React.ReactNode;
|
|
571
676
|
|
|
572
677
|
if (model && modelDef?.View) {
|
|
@@ -579,7 +684,6 @@ function renderCoreNode(
|
|
|
579
684
|
{...contextProps}
|
|
580
685
|
/>
|
|
581
686
|
)}
|
|
582
|
-
{leaves}
|
|
583
687
|
</modelDef.View>
|
|
584
688
|
);
|
|
585
689
|
} else if (geometry && geometryDef?.View) {
|
|
@@ -593,27 +697,73 @@ function renderCoreNode(
|
|
|
593
697
|
{...contextProps}
|
|
594
698
|
/>
|
|
595
699
|
)}
|
|
596
|
-
{leaves}
|
|
597
700
|
</mesh>
|
|
598
701
|
);
|
|
599
702
|
} else if (text && textDef?.View) {
|
|
600
703
|
core = (
|
|
601
704
|
<>
|
|
602
705
|
<textDef.View properties={text.properties} {...contextProps} />
|
|
603
|
-
{leaves}
|
|
604
706
|
</>
|
|
605
707
|
);
|
|
606
708
|
} else {
|
|
607
|
-
core =
|
|
709
|
+
core = null;
|
|
608
710
|
}
|
|
609
711
|
|
|
610
|
-
return
|
|
712
|
+
return core;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function renderNodeSiblingComponents(
|
|
716
|
+
gameObject: GameObjectType,
|
|
717
|
+
ctx: RenderContext,
|
|
718
|
+
parentMatrix?: Matrix4
|
|
719
|
+
) {
|
|
720
|
+
const contextProps = {
|
|
721
|
+
loadedModels: ctx.loadedModels,
|
|
722
|
+
loadedTextures: ctx.loadedTextures,
|
|
723
|
+
editMode: ctx.editMode,
|
|
724
|
+
isSelected: ctx.selectedId === gameObject.id,
|
|
725
|
+
nodeId: gameObject.id,
|
|
726
|
+
parentMatrix,
|
|
727
|
+
registerRef: ctx.registerRef,
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
return Object.entries(gameObject.components ?? {})
|
|
731
|
+
.filter(([key]) => !getNonComposableKeys().includes(key))
|
|
732
|
+
.flatMap(([key, comp]) => {
|
|
733
|
+
if (!comp?.type || isChildHostType(comp.type)) return [];
|
|
734
|
+
|
|
735
|
+
const def = getComponent(comp.type);
|
|
736
|
+
if (!def?.View) return [];
|
|
737
|
+
|
|
738
|
+
return <def.View key={key} properties={comp.properties} {...contextProps} />;
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function wrapWithChildHosts(
|
|
743
|
+
gameObject: GameObjectType,
|
|
744
|
+
ctx: RenderContext,
|
|
745
|
+
parentMatrix: Matrix4 | undefined,
|
|
746
|
+
subtree: React.ReactNode
|
|
747
|
+
) {
|
|
748
|
+
const contextProps = {
|
|
749
|
+
loadedModels: ctx.loadedModels,
|
|
750
|
+
loadedTextures: ctx.loadedTextures,
|
|
751
|
+
editMode: ctx.editMode,
|
|
752
|
+
isSelected: ctx.selectedId === gameObject.id,
|
|
753
|
+
nodeId: gameObject.id,
|
|
754
|
+
parentMatrix,
|
|
755
|
+
registerRef: ctx.registerRef,
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
const childHosts = getChildHostComponents(gameObject);
|
|
759
|
+
|
|
760
|
+
return childHosts.reduce(
|
|
611
761
|
(acc, { key, View, properties }) => (
|
|
612
762
|
<View key={key} properties={properties} {...contextProps}>
|
|
613
763
|
{acc}
|
|
614
764
|
</View>
|
|
615
765
|
),
|
|
616
|
-
|
|
766
|
+
subtree
|
|
617
767
|
);
|
|
618
768
|
}
|
|
619
769
|
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import { Component } from "./ComponentRegistry";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
const ambientLightFields: FieldDefinition[] = [
|
|
5
|
-
{ name: 'color', type: 'color', label: 'Color' },
|
|
6
|
-
{ name: 'intensity', type: 'number', label: 'Intensity', step: 0.1, min: 0 },
|
|
7
|
-
];
|
|
2
|
+
import { ColorField, FieldGroup, NumberField } from "./Input";
|
|
8
3
|
|
|
9
4
|
function AmbientLightComponentEditor({
|
|
10
5
|
component,
|
|
@@ -14,11 +9,10 @@ function AmbientLightComponentEditor({
|
|
|
14
9
|
onUpdate: (newProps: any) => void;
|
|
15
10
|
}) {
|
|
16
11
|
return (
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
values={component.properties}
|
|
20
|
-
|
|
21
|
-
/>
|
|
12
|
+
<FieldGroup>
|
|
13
|
+
<ColorField name="color" label="Color" values={component.properties} onChange={onUpdate} />
|
|
14
|
+
<NumberField name="intensity" label="Intensity" values={component.properties} onChange={onUpdate} min={0} step={0.1} fallback={1} />
|
|
15
|
+
</FieldGroup>
|
|
22
16
|
);
|
|
23
17
|
}
|
|
24
18
|
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { PerspectiveCamera } from '@react-three/drei';
|
|
2
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { CameraHelper, PerspectiveCamera as ThreePerspectiveCamera } from 'three';
|
|
4
|
+
import { useFrame } from '@react-three/fiber';
|
|
5
|
+
import { Component } from './ComponentRegistry';
|
|
6
|
+
import { FieldGroup, NumberField } from './Input';
|
|
7
|
+
|
|
8
|
+
const cameraDefaults = {
|
|
9
|
+
fov: 50,
|
|
10
|
+
near: 0.1,
|
|
11
|
+
zoom: 1,
|
|
12
|
+
far: 1000,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function CameraComponentEditor({ component, onUpdate }: { component: any; onUpdate: (newComp: any) => void }) {
|
|
16
|
+
const values = { ...cameraDefaults, ...component.properties };
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<FieldGroup>
|
|
20
|
+
<NumberField
|
|
21
|
+
name="fov"
|
|
22
|
+
label="FOV"
|
|
23
|
+
values={values}
|
|
24
|
+
onChange={onUpdate}
|
|
25
|
+
fallback={50}
|
|
26
|
+
min={1}
|
|
27
|
+
max={179}
|
|
28
|
+
step={1}
|
|
29
|
+
/>
|
|
30
|
+
<NumberField
|
|
31
|
+
name="near"
|
|
32
|
+
label="Near"
|
|
33
|
+
values={values}
|
|
34
|
+
onChange={onUpdate}
|
|
35
|
+
fallback={0.1}
|
|
36
|
+
min={0.001}
|
|
37
|
+
step={0.1}
|
|
38
|
+
/>
|
|
39
|
+
<NumberField
|
|
40
|
+
name="zoom"
|
|
41
|
+
label="Zoom"
|
|
42
|
+
values={values}
|
|
43
|
+
onChange={onUpdate}
|
|
44
|
+
fallback={1}
|
|
45
|
+
min={0.01}
|
|
46
|
+
step={0.1}
|
|
47
|
+
/>
|
|
48
|
+
<NumberField
|
|
49
|
+
name="far"
|
|
50
|
+
label="Far"
|
|
51
|
+
values={values}
|
|
52
|
+
onChange={onUpdate}
|
|
53
|
+
fallback={1000}
|
|
54
|
+
min={0.1}
|
|
55
|
+
step={1}
|
|
56
|
+
/>
|
|
57
|
+
</FieldGroup>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function CameraComponentView({ properties, editMode, isSelected }: { properties: any; editMode?: boolean; isSelected?: boolean }) {
|
|
62
|
+
const merged = { ...cameraDefaults, ...properties };
|
|
63
|
+
const fov = merged.fov;
|
|
64
|
+
const near = merged.near;
|
|
65
|
+
const zoom = merged.zoom;
|
|
66
|
+
const far = merged.far;
|
|
67
|
+
const [camera, setCamera] = useState<ThreePerspectiveCamera | null>(null);
|
|
68
|
+
const cameraHelper = useMemo(
|
|
69
|
+
() => camera ? new CameraHelper(camera) : null,
|
|
70
|
+
[camera]
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
return () => {
|
|
75
|
+
cameraHelper?.dispose();
|
|
76
|
+
};
|
|
77
|
+
}, [cameraHelper]);
|
|
78
|
+
|
|
79
|
+
useFrame(() => {
|
|
80
|
+
if (camera && cameraHelper && editMode && isSelected) {
|
|
81
|
+
camera.updateProjectionMatrix();
|
|
82
|
+
camera.updateMatrixWorld();
|
|
83
|
+
cameraHelper.update();
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<>
|
|
89
|
+
<PerspectiveCamera
|
|
90
|
+
ref={(instance) => setCamera(instance)}
|
|
91
|
+
makeDefault={!editMode}
|
|
92
|
+
fov={fov}
|
|
93
|
+
near={near}
|
|
94
|
+
zoom={zoom}
|
|
95
|
+
far={far}
|
|
96
|
+
/>
|
|
97
|
+
{editMode && isSelected && cameraHelper && (
|
|
98
|
+
<primitive object={cameraHelper} />
|
|
99
|
+
)}
|
|
100
|
+
{editMode && !isSelected ? (
|
|
101
|
+
<mesh>
|
|
102
|
+
<boxGeometry args={[0.34, 0.22, 0.18]} />
|
|
103
|
+
<meshBasicMaterial color="#22d3ee" wireframe />
|
|
104
|
+
</mesh>
|
|
105
|
+
) : null}
|
|
106
|
+
</>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const CameraComponent: Component = {
|
|
111
|
+
name: 'Camera',
|
|
112
|
+
Editor: CameraComponentEditor,
|
|
113
|
+
View: CameraComponentView,
|
|
114
|
+
defaultProperties: cameraDefaults,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export default CameraComponent;
|
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import { Component } from "./ComponentRegistry";
|
|
2
|
-
import { useRef, useEffect } from "react";
|
|
2
|
+
import { useRef, useEffect, useMemo, useState } from "react";
|
|
3
3
|
import { useFrame } from "@react-three/fiber";
|
|
4
|
-
import { DirectionalLight, Object3D, Vector3 } from "three";
|
|
4
|
+
import { CameraHelper, DirectionalLight, Object3D, OrthographicCamera, Vector3 } from "three";
|
|
5
5
|
import { FieldRenderer, FieldDefinition, Input } from "./Input";
|
|
6
6
|
|
|
7
7
|
const smallLabel = { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 } as const;
|
|
8
8
|
|
|
9
|
+
const directionalLightDefaults = {
|
|
10
|
+
color: '#ffffff',
|
|
11
|
+
intensity: 1,
|
|
12
|
+
castShadow: true,
|
|
13
|
+
shadowMapSize: 1024,
|
|
14
|
+
shadowCameraNear: 0.1,
|
|
15
|
+
shadowCameraFar: 100,
|
|
16
|
+
shadowCameraTop: 30,
|
|
17
|
+
shadowCameraBottom: -30,
|
|
18
|
+
shadowCameraLeft: -30,
|
|
19
|
+
shadowCameraRight: 30,
|
|
20
|
+
targetOffset: [0, -5, 0],
|
|
21
|
+
};
|
|
22
|
+
|
|
9
23
|
const directionalLightFields: FieldDefinition[] = [
|
|
10
24
|
{ name: 'color', type: 'color', label: 'Color' },
|
|
11
25
|
{ name: 'intensity', type: 'number', label: 'Intensity', step: 0.1, min: 0 },
|
|
@@ -71,51 +85,66 @@ const directionalLightFields: FieldDefinition[] = [
|
|
|
71
85
|
];
|
|
72
86
|
|
|
73
87
|
function DirectionalLightComponentEditor({ component, onUpdate }: { component: any; onUpdate: (newComp: any) => void }) {
|
|
88
|
+
const values = { ...directionalLightDefaults, ...component.properties };
|
|
89
|
+
const fields = values.castShadow
|
|
90
|
+
? directionalLightFields
|
|
91
|
+
: directionalLightFields.filter(field => field.name !== '_shadowCamera');
|
|
92
|
+
|
|
74
93
|
return (
|
|
75
94
|
<FieldRenderer
|
|
76
|
-
fields={
|
|
77
|
-
values={
|
|
95
|
+
fields={fields}
|
|
96
|
+
values={values}
|
|
78
97
|
onChange={onUpdate}
|
|
79
98
|
/>
|
|
80
99
|
);
|
|
81
100
|
}
|
|
82
101
|
|
|
83
|
-
function DirectionalLightView({ properties, editMode }: { properties: any; editMode?: boolean }) {
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
const
|
|
102
|
+
function DirectionalLightView({ properties, editMode, isSelected }: { properties: any; editMode?: boolean; isSelected?: boolean }) {
|
|
103
|
+
const merged = { ...directionalLightDefaults, ...properties };
|
|
104
|
+
const color = merged.color;
|
|
105
|
+
const intensity = merged.intensity;
|
|
106
|
+
const castShadow = merged.castShadow;
|
|
107
|
+
const shadowMapSize = merged.shadowMapSize;
|
|
108
|
+
const shadowCameraNear = merged.shadowCameraNear;
|
|
109
|
+
const shadowCameraFar = merged.shadowCameraFar;
|
|
110
|
+
const shadowCameraTop = merged.shadowCameraTop;
|
|
111
|
+
const shadowCameraBottom = merged.shadowCameraBottom;
|
|
112
|
+
const shadowCameraLeft = merged.shadowCameraLeft;
|
|
113
|
+
const shadowCameraRight = merged.shadowCameraRight;
|
|
114
|
+
const targetOffset = merged.targetOffset;
|
|
95
115
|
|
|
96
116
|
const directionalLightRef = useRef<DirectionalLight>(null);
|
|
97
117
|
const targetRef = useRef<Object3D>(null);
|
|
118
|
+
const [shadowCamera, setShadowCamera] = useState<OrthographicCamera | null>(null);
|
|
119
|
+
const shadowCameraHelper = useMemo(
|
|
120
|
+
() => shadowCamera ? new CameraHelper(shadowCamera) : null,
|
|
121
|
+
[shadowCamera]
|
|
122
|
+
);
|
|
98
123
|
|
|
99
|
-
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
return () => {
|
|
126
|
+
shadowCameraHelper?.dispose();
|
|
127
|
+
};
|
|
128
|
+
}, [shadowCameraHelper]);
|
|
129
|
+
|
|
130
|
+
// Use a local target object so node transforms rotate the light direction naturally.
|
|
100
131
|
useEffect(() => {
|
|
101
132
|
if (directionalLightRef.current && targetRef.current) {
|
|
102
133
|
directionalLightRef.current.target = targetRef.current;
|
|
134
|
+
setShadowCamera(directionalLightRef.current.shadow.camera);
|
|
103
135
|
}
|
|
104
136
|
}, []);
|
|
105
137
|
|
|
106
|
-
// Update target world position based on light position + offset
|
|
107
138
|
useFrame(() => {
|
|
108
139
|
if (!directionalLightRef.current || !targetRef.current) return;
|
|
109
140
|
|
|
110
|
-
|
|
111
|
-
directionalLightRef.current.getWorldPosition(lightWorldPos);
|
|
141
|
+
directionalLightRef.current.target.updateMatrixWorld();
|
|
112
142
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
);
|
|
143
|
+
if (shadowCamera && shadowCameraHelper && castShadow) {
|
|
144
|
+
shadowCamera.updateProjectionMatrix();
|
|
145
|
+
shadowCamera.updateMatrixWorld();
|
|
146
|
+
shadowCameraHelper.update();
|
|
147
|
+
}
|
|
119
148
|
});
|
|
120
149
|
|
|
121
150
|
return (
|
|
@@ -136,9 +165,11 @@ function DirectionalLightView({ properties, editMode }: { properties: any; editM
|
|
|
136
165
|
shadow-bias={-0.001}
|
|
137
166
|
shadow-normalBias={0.02}
|
|
138
167
|
/>
|
|
139
|
-
{
|
|
140
|
-
|
|
141
|
-
|
|
168
|
+
<object3D ref={targetRef} position={targetOffset as [number, number, number]} />
|
|
169
|
+
{editMode && isSelected && castShadow && shadowCameraHelper && (
|
|
170
|
+
<primitive object={shadowCameraHelper} />
|
|
171
|
+
)}
|
|
172
|
+
{editMode && isSelected && (
|
|
142
173
|
<>
|
|
143
174
|
{/* Light source indicator */}
|
|
144
175
|
<mesh>
|
|
@@ -173,7 +204,7 @@ const DirectionalLightComponent: Component = {
|
|
|
173
204
|
name: 'DirectionalLight',
|
|
174
205
|
Editor: DirectionalLightComponentEditor,
|
|
175
206
|
View: DirectionalLightView,
|
|
176
|
-
defaultProperties:
|
|
207
|
+
defaultProperties: directionalLightDefaults
|
|
177
208
|
};
|
|
178
209
|
|
|
179
210
|
export default DirectionalLightComponent;
|