react-three-game 0.0.55 → 0.0.57
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/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/shared/ContactShadow.d.ts +8 -0
- package/dist/shared/ContactShadow.js +32 -0
- package/dist/shared/GameCanvas.js +1 -3
- package/dist/tools/assetviewer/page.js +36 -15
- package/dist/tools/dragdrop/DragDropLoader.js +17 -40
- package/dist/tools/dragdrop/modelLoader.d.ts +5 -0
- package/dist/tools/dragdrop/modelLoader.js +39 -0
- 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 +139 -70
- package/dist/tools/prefabeditor/EditorUI.js +5 -10
- package/dist/tools/prefabeditor/PrefabEditor.d.ts +1 -0
- package/dist/tools/prefabeditor/PrefabEditor.js +70 -3
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +3 -0
- package/dist/tools/prefabeditor/PrefabRoot.js +136 -35
- 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 +25 -0
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +2 -2
- 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 +100 -47
- package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +8 -2
- package/dist/tools/prefabeditor/components/MaterialComponent.js +129 -14
- 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 +6 -11
- package/dist/tools/prefabeditor/components/TextComponent.js +7 -53
- package/dist/tools/prefabeditor/components/TransformComponent.js +31 -19
- package/dist/tools/prefabeditor/components/index.js +5 -1
- package/dist/tools/prefabeditor/styles.d.ts +17 -4
- package/dist/tools/prefabeditor/styles.js +69 -32
- package/dist/tools/prefabeditor/utils.d.ts +8 -3
- package/dist/tools/prefabeditor/utils.js +92 -6
- package/package.json +1 -1
- package/react-three-game-skill/react-three-game/rules/LIGHTING.md +6 -0
- package/src/index.ts +7 -0
- package/src/shared/ContactShadow.tsx +74 -0
- package/src/shared/GameCanvas.tsx +0 -3
- package/src/tools/assetviewer/page.tsx +78 -46
- package/src/tools/dragdrop/DragDropLoader.tsx +7 -39
- package/src/tools/dragdrop/modelLoader.ts +36 -0
- package/src/tools/prefabeditor/Dropdown.tsx +112 -0
- package/src/tools/prefabeditor/EditorContext.tsx +5 -0
- package/src/tools/prefabeditor/EditorTree.tsx +237 -115
- package/src/tools/prefabeditor/EditorUI.tsx +6 -11
- package/src/tools/prefabeditor/PrefabEditor.tsx +77 -5
- package/src/tools/prefabeditor/PrefabRoot.tsx +228 -59
- package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +5 -11
- package/src/tools/prefabeditor/components/CameraComponent.tsx +80 -0
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +2 -2
- 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 +247 -53
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +191 -20
- package/src/tools/prefabeditor/components/ModelComponent.tsx +52 -5
- package/src/tools/prefabeditor/components/PhysicsComponent.tsx +44 -85
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +14 -16
- package/src/tools/prefabeditor/components/TextComponent.tsx +58 -57
- package/src/tools/prefabeditor/components/TransformComponent.tsx +78 -20
- package/src/tools/prefabeditor/components/index.ts +5 -1
- package/src/tools/prefabeditor/styles.ts +71 -32
- package/src/tools/prefabeditor/utils.ts +96 -5
|
@@ -6,7 +6,8 @@ import { Physics } from "@react-three/rapier";
|
|
|
6
6
|
import EditorUI from "./EditorUI";
|
|
7
7
|
import { base, toolbar } from "./styles";
|
|
8
8
|
import { EditorContext } from "./EditorContext";
|
|
9
|
-
import { exportGLB } from "./utils";
|
|
9
|
+
import { exportGLB, createModelNode, createImageNode } from "./utils";
|
|
10
|
+
import { parseModelFromFile } from "../dragdrop/modelLoader";
|
|
10
11
|
|
|
11
12
|
export interface PrefabEditorRef {
|
|
12
13
|
screenshot: () => void;
|
|
@@ -35,13 +36,16 @@ const PrefabEditor = forwardRef<PrefabEditorRef, {
|
|
|
35
36
|
initialPrefab?: Prefab;
|
|
36
37
|
physics?: boolean;
|
|
37
38
|
onPrefabChange?: (prefab: Prefab) => void;
|
|
39
|
+
uiPlugins?: React.ReactNode[] | React.ReactNode;
|
|
38
40
|
children?: React.ReactNode;
|
|
39
|
-
}>(({ basePath, initialPrefab, physics = true, onPrefabChange, children }, ref) => {
|
|
41
|
+
}>(({ basePath, initialPrefab, physics = true, onPrefabChange, uiPlugins, children }, ref) => {
|
|
40
42
|
const [editMode, setEditMode] = useState(true);
|
|
41
43
|
const [loadedPrefab, setLoadedPrefab] = useState<Prefab>(initialPrefab ?? DEFAULT_PREFAB);
|
|
42
44
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
43
45
|
const [transformMode, setTransformMode] = useState<"translate" | "rotate" | "scale">("translate");
|
|
44
46
|
const [snapResolution, setSnapResolution] = useState(0);
|
|
47
|
+
const [positionSnap, setPositionSnap] = useState(0.5);
|
|
48
|
+
const [rotationSnap, setRotationSnap] = useState(Math.PI / 4);
|
|
45
49
|
const [history, setHistory] = useState<Prefab[]>([loadedPrefab]);
|
|
46
50
|
const [historyIndex, setHistoryIndex] = useState(0);
|
|
47
51
|
const throttleRef = useRef<NodeJS.Timeout | null>(null);
|
|
@@ -120,11 +124,73 @@ const PrefabEditor = forwardRef<PrefabEditorRef, {
|
|
|
120
124
|
});
|
|
121
125
|
};
|
|
122
126
|
|
|
127
|
+
const handleFocusNode = (nodeId: string) => {
|
|
128
|
+
prefabRootRef.current?.focusNode(nodeId);
|
|
129
|
+
};
|
|
130
|
+
|
|
123
131
|
useEffect(() => {
|
|
124
132
|
const canvas = document.querySelector('canvas');
|
|
125
133
|
if (canvas) canvasRef.current = canvas;
|
|
126
134
|
}, []);
|
|
127
135
|
|
|
136
|
+
// --- Drag & drop files to add nodes ---
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
const IMAGE_EXTS = ['png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp', 'svg'];
|
|
139
|
+
const MODEL_EXTS = ['glb', 'gltf', 'fbx'];
|
|
140
|
+
|
|
141
|
+
function handleDragOver(e: DragEvent) {
|
|
142
|
+
e.preventDefault();
|
|
143
|
+
e.stopPropagation();
|
|
144
|
+
}
|
|
145
|
+
function handleDrop(e: DragEvent) {
|
|
146
|
+
e.preventDefault();
|
|
147
|
+
e.stopPropagation();
|
|
148
|
+
const files = e.dataTransfer?.files ? Array.from(e.dataTransfer.files) : [];
|
|
149
|
+
files.forEach(file => {
|
|
150
|
+
const ext = file.name.split('.').pop()?.toLowerCase();
|
|
151
|
+
if (!ext) return;
|
|
152
|
+
|
|
153
|
+
const baseName = file.name.replace(/\.[^.]+$/, '');
|
|
154
|
+
|
|
155
|
+
if (MODEL_EXTS.includes(ext)) {
|
|
156
|
+
const modelPath = `models/${file.name}`;
|
|
157
|
+
const newNode = createModelNode(modelPath, baseName);
|
|
158
|
+
|
|
159
|
+
updatePrefab(prev => ({
|
|
160
|
+
...prev,
|
|
161
|
+
root: { ...prev.root, children: [...(prev.root.children ?? []), newNode] }
|
|
162
|
+
}));
|
|
163
|
+
|
|
164
|
+
parseModelFromFile(file).then(result => {
|
|
165
|
+
if (result.success && result.model) {
|
|
166
|
+
prefabRootRef.current?.injectModel(modelPath, result.model);
|
|
167
|
+
} else {
|
|
168
|
+
console.error('Drop parse error:', result.error);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
} else if (IMAGE_EXTS.includes(ext)) {
|
|
172
|
+
const texturePath = `textures/${file.name}`;
|
|
173
|
+
const newNode = createImageNode(texturePath, baseName);
|
|
174
|
+
|
|
175
|
+
updatePrefab(prev => ({
|
|
176
|
+
...prev,
|
|
177
|
+
root: { ...prev.root, children: [...(prev.root.children ?? []), newNode] }
|
|
178
|
+
}));
|
|
179
|
+
|
|
180
|
+
// Inject a blob URL texture so it renders immediately
|
|
181
|
+
prefabRootRef.current?.injectTexture(texturePath, file);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
window.addEventListener('dragover', handleDragOver);
|
|
187
|
+
window.addEventListener('drop', handleDrop);
|
|
188
|
+
return () => {
|
|
189
|
+
window.removeEventListener('dragover', handleDragOver);
|
|
190
|
+
window.removeEventListener('drop', handleDrop);
|
|
191
|
+
};
|
|
192
|
+
}, [loadedPrefab]);
|
|
193
|
+
|
|
128
194
|
useImperativeHandle(ref, () => ({
|
|
129
195
|
screenshot: handleScreenshot,
|
|
130
196
|
exportGLB: handleExportGLB,
|
|
@@ -155,10 +221,15 @@ const PrefabEditor = forwardRef<PrefabEditorRef, {
|
|
|
155
221
|
setTransformMode,
|
|
156
222
|
snapResolution,
|
|
157
223
|
setSnapResolution,
|
|
224
|
+
positionSnap,
|
|
225
|
+
setPositionSnap,
|
|
226
|
+
rotationSnap,
|
|
227
|
+
setRotationSnap,
|
|
228
|
+
onFocusNode: handleFocusNode,
|
|
158
229
|
onScreenshot: handleScreenshot,
|
|
159
230
|
onExportGLB: handleExportGLB
|
|
160
231
|
}}>
|
|
161
|
-
<GameCanvas>
|
|
232
|
+
<GameCanvas camera={{ position: [0, 5, 15] }}>
|
|
162
233
|
{physics ? (
|
|
163
234
|
<Physics debug={editMode} paused={editMode}>
|
|
164
235
|
{content}
|
|
@@ -170,8 +241,9 @@ const PrefabEditor = forwardRef<PrefabEditorRef, {
|
|
|
170
241
|
<button style={base.btn} onClick={() => setEditMode(!editMode)}>
|
|
171
242
|
{editMode ? "▶" : "⏸"}
|
|
172
243
|
</button>
|
|
244
|
+
{uiPlugins}
|
|
173
245
|
</div>
|
|
174
|
-
|
|
246
|
+
<EditorUI
|
|
175
247
|
prefabData={loadedPrefab}
|
|
176
248
|
setPrefabData={updatePrefab}
|
|
177
249
|
selectedId={selectedId}
|
|
@@ -181,7 +253,7 @@ const PrefabEditor = forwardRef<PrefabEditorRef, {
|
|
|
181
253
|
onRedo={redo}
|
|
182
254
|
canUndo={historyIndex > 0}
|
|
183
255
|
canRedo={historyIndex < history.length - 1}
|
|
184
|
-
/>
|
|
256
|
+
/>
|
|
185
257
|
</EditorContext.Provider>
|
|
186
258
|
});
|
|
187
259
|
|
|
@@ -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
|
|
|
@@ -22,6 +22,9 @@ const IDENTITY = new Matrix4();
|
|
|
22
22
|
export interface PrefabRootRef {
|
|
23
23
|
root: Group | null;
|
|
24
24
|
rigidBodyRefs: Map<string, any>; // RigidBody refs only populated when using physics
|
|
25
|
+
injectModel: (filename: string, model: Object3D) => void;
|
|
26
|
+
injectTexture: (filename: string, file: File) => void;
|
|
27
|
+
focusNode: (nodeId: string) => void;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
export const PrefabRoot = forwardRef<PrefabRootRef, {
|
|
@@ -38,20 +41,50 @@ export const PrefabRoot = forwardRef<PrefabRootRef, {
|
|
|
38
41
|
const editorContext = useContext(EditorContext);
|
|
39
42
|
const transformMode = editorContext?.transformMode ?? "translate";
|
|
40
43
|
const snapResolution = editorContext?.snapResolution ?? 0;
|
|
44
|
+
const positionSnap = editorContext?.positionSnap ?? 0.5;
|
|
45
|
+
const rotationSnap = editorContext?.rotationSnap ?? Math.PI / 4;
|
|
41
46
|
|
|
42
47
|
// prefab root state
|
|
43
48
|
const [models, setModels] = useState<Record<string, Object3D>>({});
|
|
44
49
|
const [textures, setTextures] = useState<Record<string, Texture>>({});
|
|
45
50
|
const loading = useRef(new Set<string>());
|
|
51
|
+
const failedTextures = useRef(new Set<string>());
|
|
46
52
|
const objectRefs = useRef<Record<string, Object3D | null>>({});
|
|
47
53
|
const rigidBodyRefs = useRef<Map<string, RapierRigidBody | null>>(new Map());
|
|
48
54
|
const [selectedObject, setSelectedObject] = useState<Object3D | null>(null);
|
|
49
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, file: File) => {
|
|
63
|
+
loading.current.add(filename);
|
|
64
|
+
const url = URL.createObjectURL(file);
|
|
65
|
+
const loader = new TextureLoader();
|
|
66
|
+
loader.load(url, tex => {
|
|
67
|
+
tex.colorSpace = SRGBColorSpace;
|
|
68
|
+
setTextures(t => ({ ...t, [filename]: tex }));
|
|
69
|
+
URL.revokeObjectURL(url);
|
|
70
|
+
}, undefined, () => URL.revokeObjectURL(url));
|
|
71
|
+
}, []);
|
|
50
72
|
|
|
51
73
|
useImperativeHandle(ref, () => ({
|
|
52
74
|
root: rootRef.current,
|
|
53
|
-
rigidBodyRefs: rigidBodyRefs.current
|
|
54
|
-
|
|
75
|
+
rigidBodyRefs: rigidBodyRefs.current,
|
|
76
|
+
injectModel,
|
|
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
|
+
}
|
|
87
|
+
}), [injectModel, injectTexture]);
|
|
55
88
|
|
|
56
89
|
const registerRef = useCallback((id: string, obj: Object3D | null) => {
|
|
57
90
|
objectRefs.current[id] = obj;
|
|
@@ -109,6 +142,8 @@ export const PrefabRoot = forwardRef<PrefabRootRef, {
|
|
|
109
142
|
modelsToLoad.add(node.components.model.properties.filename);
|
|
110
143
|
node.components?.material?.properties?.texture &&
|
|
111
144
|
texturesToLoad.add(node.components.material.properties.texture);
|
|
145
|
+
node.components?.material?.properties?.normalMapTexture &&
|
|
146
|
+
texturesToLoad.add(node.components.material.properties.normalMapTexture);
|
|
112
147
|
});
|
|
113
148
|
|
|
114
149
|
modelsToLoad.forEach(async file => {
|
|
@@ -126,7 +161,7 @@ export const PrefabRoot = forwardRef<PrefabRootRef, {
|
|
|
126
161
|
|
|
127
162
|
const loader = new TextureLoader();
|
|
128
163
|
texturesToLoad.forEach(file => {
|
|
129
|
-
if (textures[file] || loading.current.has(file)) return;
|
|
164
|
+
if (textures[file] || loading.current.has(file) || failedTextures.current.has(file)) return;
|
|
130
165
|
loading.current.add(file);
|
|
131
166
|
|
|
132
167
|
// Handle full URLs (http/https) or regular paths
|
|
@@ -140,8 +175,9 @@ export const PrefabRoot = forwardRef<PrefabRootRef, {
|
|
|
140
175
|
tex.colorSpace = SRGBColorSpace;
|
|
141
176
|
setTextures(t => ({ ...t, [file]: tex }));
|
|
142
177
|
}, undefined, (err) => {
|
|
143
|
-
console.
|
|
178
|
+
console.warn(`Failed to load texture: ${path}`, err);
|
|
144
179
|
loading.current.delete(file);
|
|
180
|
+
failedTextures.current.add(file);
|
|
145
181
|
});
|
|
146
182
|
});
|
|
147
183
|
}, [data, models, textures]);
|
|
@@ -171,16 +207,16 @@ export const PrefabRoot = forwardRef<PrefabRootRef, {
|
|
|
171
207
|
|
|
172
208
|
{editMode && (
|
|
173
209
|
<>
|
|
174
|
-
<MapControls makeDefault />
|
|
210
|
+
<MapControls ref={controlsRef} makeDefault />
|
|
175
211
|
{selectedObject && (
|
|
176
212
|
<TransformControls
|
|
177
|
-
key={`transform-${snapResolution}`}
|
|
213
|
+
key={`transform-${transformMode}-${positionSnap}-${rotationSnap}-${snapResolution}`}
|
|
178
214
|
object={selectedObject}
|
|
179
215
|
mode={transformMode}
|
|
180
216
|
space="local"
|
|
181
217
|
onObjectChange={onTransformChange}
|
|
182
|
-
translationSnap={
|
|
183
|
-
rotationSnap={
|
|
218
|
+
translationSnap={positionSnap > 0 ? positionSnap : undefined}
|
|
219
|
+
rotationSnap={rotationSnap > 0 ? rotationSnap : undefined}
|
|
184
220
|
scaleSnap={snapResolution > 0 ? snapResolution : undefined}
|
|
185
221
|
/>
|
|
186
222
|
)}
|
|
@@ -343,6 +379,19 @@ function StandardNode({
|
|
|
343
379
|
const physicsDef = hasPhysics ? getComponent("Physics") : null;
|
|
344
380
|
const isInstanced = gameObject.components?.model?.properties?.instanced;
|
|
345
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
|
+
});
|
|
346
395
|
|
|
347
396
|
const inner = (
|
|
348
397
|
<group
|
|
@@ -350,22 +399,7 @@ function StandardNode({
|
|
|
350
399
|
onPointerMove={editMode ? () => (clickValid.current = false) : undefined}
|
|
351
400
|
onPointerUp={editMode ? onUp : undefined}
|
|
352
401
|
>
|
|
353
|
-
{
|
|
354
|
-
{gameObject.children?.map(child => (
|
|
355
|
-
<GameObjectRenderer
|
|
356
|
-
key={child.id}
|
|
357
|
-
gameObject={child}
|
|
358
|
-
selectedId={selectedId}
|
|
359
|
-
onSelect={onSelect}
|
|
360
|
-
onClick={onClick}
|
|
361
|
-
registerRef={registerRef}
|
|
362
|
-
registerRigidBodyRef={registerRigidBodyRef}
|
|
363
|
-
loadedModels={loadedModels}
|
|
364
|
-
loadedTextures={loadedTextures}
|
|
365
|
-
editMode={editMode}
|
|
366
|
-
parentMatrix={world}
|
|
367
|
-
/>
|
|
368
|
-
))}
|
|
402
|
+
{renderCompositionNode(gameObject, renderCtx, parentMatrix, childNodes)}
|
|
369
403
|
</group>
|
|
370
404
|
);
|
|
371
405
|
|
|
@@ -449,6 +483,74 @@ interface RendererProps {
|
|
|
449
483
|
parentMatrix?: Matrix4;
|
|
450
484
|
}
|
|
451
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
|
+
|
|
452
554
|
function walk(node: GameObjectType, fn: (n: GameObjectType) => void) {
|
|
453
555
|
fn(node);
|
|
454
556
|
node.children?.forEach(c => walk(c, fn));
|
|
@@ -499,14 +601,55 @@ function computeParentWorldMatrix(root: GameObjectType, targetId: string): Matri
|
|
|
499
601
|
return result ?? IDENTITY;
|
|
500
602
|
}
|
|
501
603
|
|
|
502
|
-
function
|
|
604
|
+
function renderCompositionSubtree(
|
|
503
605
|
gameObject: GameObjectType,
|
|
504
|
-
ctx:
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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(
|
|
638
|
+
gameObject: GameObjectType,
|
|
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,
|
|
510
653
|
parentMatrix?: Matrix4
|
|
511
654
|
) {
|
|
512
655
|
const geometry = gameObject.components?.geometry;
|
|
@@ -523,31 +666,12 @@ function renderCoreNode(
|
|
|
523
666
|
loadedModels: ctx.loadedModels,
|
|
524
667
|
loadedTextures: ctx.loadedTextures,
|
|
525
668
|
editMode: ctx.editMode,
|
|
669
|
+
isSelected: ctx.selectedId === gameObject.id,
|
|
670
|
+
nodeId: gameObject.id,
|
|
526
671
|
parentMatrix,
|
|
527
672
|
registerRef: ctx.registerRef,
|
|
528
673
|
};
|
|
529
674
|
|
|
530
|
-
const wrappers: Array<{ key: string; View: any; properties: any }> = [];
|
|
531
|
-
const leaves: React.ReactNode[] = [];
|
|
532
|
-
|
|
533
|
-
if (gameObject.components) {
|
|
534
|
-
Object.entries(gameObject.components)
|
|
535
|
-
.filter(([k]) => !getNonComposableKeys().includes(k))
|
|
536
|
-
.forEach(([key, comp]) => {
|
|
537
|
-
if (!comp?.type) return;
|
|
538
|
-
const def = getComponent(comp.type);
|
|
539
|
-
if (!def?.View) return;
|
|
540
|
-
|
|
541
|
-
if (def.View.toString().includes("children")) {
|
|
542
|
-
wrappers.push({ key, View: def.View, properties: comp.properties });
|
|
543
|
-
} else {
|
|
544
|
-
leaves.push(
|
|
545
|
-
<def.View key={key} properties={comp.properties} {...contextProps} />
|
|
546
|
-
);
|
|
547
|
-
}
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
|
|
551
675
|
let core: React.ReactNode;
|
|
552
676
|
|
|
553
677
|
if (model && modelDef?.View) {
|
|
@@ -560,7 +684,6 @@ function renderCoreNode(
|
|
|
560
684
|
{...contextProps}
|
|
561
685
|
/>
|
|
562
686
|
)}
|
|
563
|
-
{leaves}
|
|
564
687
|
</modelDef.View>
|
|
565
688
|
);
|
|
566
689
|
} else if (geometry && geometryDef?.View) {
|
|
@@ -574,27 +697,73 @@ function renderCoreNode(
|
|
|
574
697
|
{...contextProps}
|
|
575
698
|
/>
|
|
576
699
|
)}
|
|
577
|
-
{leaves}
|
|
578
700
|
</mesh>
|
|
579
701
|
);
|
|
580
702
|
} else if (text && textDef?.View) {
|
|
581
703
|
core = (
|
|
582
704
|
<>
|
|
583
705
|
<textDef.View properties={text.properties} {...contextProps} />
|
|
584
|
-
{leaves}
|
|
585
706
|
</>
|
|
586
707
|
);
|
|
587
708
|
} else {
|
|
588
|
-
core =
|
|
709
|
+
core = null;
|
|
589
710
|
}
|
|
590
711
|
|
|
591
|
-
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(
|
|
592
761
|
(acc, { key, View, properties }) => (
|
|
593
762
|
<View key={key} properties={properties} {...contextProps}>
|
|
594
763
|
{acc}
|
|
595
764
|
</View>
|
|
596
765
|
),
|
|
597
|
-
|
|
766
|
+
subtree
|
|
598
767
|
);
|
|
599
768
|
}
|
|
600
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,80 @@
|
|
|
1
|
+
import { PerspectiveCamera, useHelper } from '@react-three/drei';
|
|
2
|
+
import { useRef } from 'react';
|
|
3
|
+
import { CameraHelper, PerspectiveCamera as ThreePerspectiveCamera } from 'three';
|
|
4
|
+
import { Component } from './ComponentRegistry';
|
|
5
|
+
import { FieldGroup, NumberField } from './Input';
|
|
6
|
+
|
|
7
|
+
function CameraComponentEditor({ component, onUpdate }: { component: any; onUpdate: (newComp: any) => void }) {
|
|
8
|
+
return (
|
|
9
|
+
<FieldGroup>
|
|
10
|
+
<NumberField
|
|
11
|
+
name="fov"
|
|
12
|
+
label="FOV"
|
|
13
|
+
values={component.properties}
|
|
14
|
+
onChange={onUpdate}
|
|
15
|
+
fallback={50}
|
|
16
|
+
min={1}
|
|
17
|
+
max={179}
|
|
18
|
+
step={1}
|
|
19
|
+
/>
|
|
20
|
+
<NumberField
|
|
21
|
+
name="near"
|
|
22
|
+
label="Near"
|
|
23
|
+
values={component.properties}
|
|
24
|
+
onChange={onUpdate}
|
|
25
|
+
fallback={0.1}
|
|
26
|
+
min={0.001}
|
|
27
|
+
step={0.1}
|
|
28
|
+
/>
|
|
29
|
+
<NumberField
|
|
30
|
+
name="zoom"
|
|
31
|
+
label="Zoom"
|
|
32
|
+
values={component.properties}
|
|
33
|
+
onChange={onUpdate}
|
|
34
|
+
fallback={1}
|
|
35
|
+
min={0.01}
|
|
36
|
+
step={0.1}
|
|
37
|
+
/>
|
|
38
|
+
<NumberField
|
|
39
|
+
name="far"
|
|
40
|
+
label="Far"
|
|
41
|
+
values={component.properties}
|
|
42
|
+
onChange={onUpdate}
|
|
43
|
+
fallback={1000}
|
|
44
|
+
min={0.1}
|
|
45
|
+
step={1}
|
|
46
|
+
/>
|
|
47
|
+
</FieldGroup>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function CameraComponentView({ properties, editMode, isSelected }: { properties: any; editMode?: boolean; isSelected?: boolean }) {
|
|
52
|
+
const fov = properties?.fov ?? 50;
|
|
53
|
+
const near = properties?.near ?? 0.1;
|
|
54
|
+
const zoom = properties?.zoom ?? 1;
|
|
55
|
+
const far = properties?.far ?? 1000;
|
|
56
|
+
const cameraRef = useRef<ThreePerspectiveCamera>(null);
|
|
57
|
+
|
|
58
|
+
useHelper(editMode && isSelected ? (cameraRef as React.RefObject<ThreePerspectiveCamera>) : null, CameraHelper);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<>
|
|
62
|
+
<PerspectiveCamera ref={cameraRef} makeDefault={!editMode} fov={fov} near={near} zoom={zoom} far={far} />
|
|
63
|
+
{editMode && !isSelected ? (
|
|
64
|
+
<mesh>
|
|
65
|
+
<boxGeometry args={[0.34, 0.22, 0.18]} />
|
|
66
|
+
<meshBasicMaterial color="#22d3ee" wireframe />
|
|
67
|
+
</mesh>
|
|
68
|
+
) : null}
|
|
69
|
+
</>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const CameraComponent: Component = {
|
|
74
|
+
name: 'Camera',
|
|
75
|
+
Editor: CameraComponentEditor,
|
|
76
|
+
View: CameraComponentView,
|
|
77
|
+
defaultProperties: {},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export default CameraComponent;
|
|
@@ -80,7 +80,7 @@ function DirectionalLightComponentEditor({ component, onUpdate }: { component: a
|
|
|
80
80
|
);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
function DirectionalLightView({ properties, editMode }: { properties: any; editMode?: boolean }) {
|
|
83
|
+
function DirectionalLightView({ properties, editMode, isSelected }: { properties: any; editMode?: boolean; isSelected?: boolean }) {
|
|
84
84
|
const color = properties.color ?? '#ffffff';
|
|
85
85
|
const intensity = properties.intensity ?? 1.0;
|
|
86
86
|
const castShadow = properties.castShadow ?? true;
|
|
@@ -138,7 +138,7 @@ function DirectionalLightView({ properties, editMode }: { properties: any; editM
|
|
|
138
138
|
/>
|
|
139
139
|
{/* Target object - rendered declaratively in scene graph */}
|
|
140
140
|
<object3D ref={targetRef} />
|
|
141
|
-
{editMode && (
|
|
141
|
+
{editMode && isSelected && (
|
|
142
142
|
<>
|
|
143
143
|
{/* Light source indicator */}
|
|
144
144
|
<mesh>
|