react-three-game 0.0.16 → 0.0.18
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 +88 -113
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/tools/prefabeditor/EditorTree.js +27 -15
- package/dist/tools/prefabeditor/EditorUI.js +2 -8
- package/dist/tools/prefabeditor/InstanceProvider.d.ts +4 -4
- package/dist/tools/prefabeditor/InstanceProvider.js +21 -13
- package/dist/tools/prefabeditor/PrefabEditor.js +128 -59
- package/dist/tools/prefabeditor/PrefabRoot.js +51 -33
- package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +2 -0
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +114 -0
- package/dist/tools/prefabeditor/components/ModelComponent.js +12 -4
- package/dist/tools/prefabeditor/components/RotatorComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/RotatorComponent.js +42 -0
- package/dist/tools/prefabeditor/components/SpotLightComponent.js +10 -5
- package/dist/tools/prefabeditor/components/TransformComponent.js +28 -3
- package/dist/tools/prefabeditor/components/index.js +2 -0
- package/dist/tools/prefabeditor/hooks/useModelLoader.d.ts +10 -0
- package/dist/tools/prefabeditor/hooks/useModelLoader.js +40 -0
- package/package.json +8 -8
- package/src/index.ts +4 -0
- package/src/tools/prefabeditor/EditorTree.tsx +39 -16
- package/src/tools/prefabeditor/EditorUI.tsx +2 -27
- package/src/tools/prefabeditor/InstanceProvider.tsx +43 -32
- package/src/tools/prefabeditor/PrefabEditor.tsx +202 -86
- package/src/tools/prefabeditor/PrefabRoot.tsx +62 -54
- package/src/tools/prefabeditor/components/ComponentRegistry.ts +7 -1
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +332 -0
- package/src/tools/prefabeditor/components/ModelComponent.tsx +14 -4
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +27 -7
- package/src/tools/prefabeditor/components/TransformComponent.tsx +69 -16
- package/src/tools/prefabeditor/components/index.ts +2 -0
|
@@ -67,92 +67,12 @@ const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }: { b
|
|
|
67
67
|
</Physics>
|
|
68
68
|
</GameCanvas>
|
|
69
69
|
|
|
70
|
-
<
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
display: "flex",
|
|
77
|
-
alignItems: "center",
|
|
78
|
-
gap: 6,
|
|
79
|
-
padding: "2px 4px",
|
|
80
|
-
background: "rgba(0,0,0,0.55)",
|
|
81
|
-
border: "1px solid rgba(255,255,255,0.12)",
|
|
82
|
-
borderRadius: 4,
|
|
83
|
-
color: "rgba(255,255,255,0.9)",
|
|
84
|
-
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
|
85
|
-
fontSize: 11,
|
|
86
|
-
lineHeight: 1,
|
|
87
|
-
WebkitUserSelect: "none",
|
|
88
|
-
userSelect: "none",
|
|
89
|
-
}}
|
|
90
|
-
>
|
|
91
|
-
<button
|
|
92
|
-
style={{
|
|
93
|
-
padding: "2px 6px",
|
|
94
|
-
font: "inherit",
|
|
95
|
-
background: "transparent",
|
|
96
|
-
color: "inherit",
|
|
97
|
-
border: "1px solid rgba(255,255,255,0.18)",
|
|
98
|
-
borderRadius: 3,
|
|
99
|
-
cursor: "pointer",
|
|
100
|
-
}}
|
|
101
|
-
onClick={() => setEditMode(!editMode)}
|
|
102
|
-
onPointerEnter={(e) => {
|
|
103
|
-
(e.currentTarget as HTMLButtonElement).style.background = "rgba(255,255,255,0.08)";
|
|
104
|
-
}}
|
|
105
|
-
onPointerLeave={(e) => {
|
|
106
|
-
(e.currentTarget as HTMLButtonElement).style.background = "transparent";
|
|
107
|
-
}}
|
|
108
|
-
>
|
|
109
|
-
{editMode ? "▶" : "⏸"}
|
|
110
|
-
</button>
|
|
111
|
-
<span style={{ opacity: 0.35 }}>|</span>
|
|
112
|
-
<button
|
|
113
|
-
style={{
|
|
114
|
-
padding: "2px 6px",
|
|
115
|
-
font: "inherit",
|
|
116
|
-
background: "transparent",
|
|
117
|
-
color: "inherit",
|
|
118
|
-
border: "1px solid rgba(255,255,255,0.18)",
|
|
119
|
-
borderRadius: 3,
|
|
120
|
-
cursor: "pointer",
|
|
121
|
-
}}
|
|
122
|
-
onClick={async () => {
|
|
123
|
-
const prefab = await loadJson();
|
|
124
|
-
if (prefab) setLoadedPrefab(prefab);
|
|
125
|
-
}}
|
|
126
|
-
onPointerEnter={(e) => {
|
|
127
|
-
(e.currentTarget as HTMLButtonElement).style.background = "rgba(255,255,255,0.08)";
|
|
128
|
-
}}
|
|
129
|
-
onPointerLeave={(e) => {
|
|
130
|
-
(e.currentTarget as HTMLButtonElement).style.background = "transparent";
|
|
131
|
-
}}
|
|
132
|
-
>
|
|
133
|
-
📥
|
|
134
|
-
</button>
|
|
135
|
-
<button
|
|
136
|
-
style={{
|
|
137
|
-
padding: "2px 6px",
|
|
138
|
-
font: "inherit",
|
|
139
|
-
background: "transparent",
|
|
140
|
-
color: "inherit",
|
|
141
|
-
border: "1px solid rgba(255,255,255,0.18)",
|
|
142
|
-
borderRadius: 3,
|
|
143
|
-
cursor: "pointer",
|
|
144
|
-
}}
|
|
145
|
-
onClick={() => saveJson(loadedPrefab, "prefab")}
|
|
146
|
-
onPointerEnter={(e) => {
|
|
147
|
-
(e.currentTarget as HTMLButtonElement).style.background = "rgba(255,255,255,0.08)";
|
|
148
|
-
}}
|
|
149
|
-
onPointerLeave={(e) => {
|
|
150
|
-
(e.currentTarget as HTMLButtonElement).style.background = "transparent";
|
|
151
|
-
}}
|
|
152
|
-
>
|
|
153
|
-
💾
|
|
154
|
-
</button>
|
|
155
|
-
</div>
|
|
70
|
+
<SaveDataPanel
|
|
71
|
+
currentData={loadedPrefab}
|
|
72
|
+
onDataChange={updatePrefab}
|
|
73
|
+
editMode={editMode}
|
|
74
|
+
onEditModeChange={setEditMode}
|
|
75
|
+
/>
|
|
156
76
|
{editMode && <EditorUI
|
|
157
77
|
prefabData={loadedPrefab}
|
|
158
78
|
setPrefabData={updatePrefab}
|
|
@@ -165,6 +85,202 @@ const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }: { b
|
|
|
165
85
|
</>
|
|
166
86
|
}
|
|
167
87
|
|
|
88
|
+
const SaveDataPanel = ({
|
|
89
|
+
currentData,
|
|
90
|
+
onDataChange,
|
|
91
|
+
editMode,
|
|
92
|
+
onEditModeChange
|
|
93
|
+
}: {
|
|
94
|
+
currentData: Prefab;
|
|
95
|
+
onDataChange: (data: Prefab) => void;
|
|
96
|
+
editMode: boolean;
|
|
97
|
+
onEditModeChange: (mode: boolean) => void;
|
|
98
|
+
}) => {
|
|
99
|
+
const [history, setHistory] = useState<Prefab[]>([currentData]);
|
|
100
|
+
const [historyIndex, setHistoryIndex] = useState(0);
|
|
101
|
+
const throttleTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
102
|
+
const lastSavedDataRef = useRef<string>(JSON.stringify(currentData));
|
|
103
|
+
|
|
104
|
+
// Define undo/redo handlers
|
|
105
|
+
const handleUndo = () => {
|
|
106
|
+
if (historyIndex > 0) {
|
|
107
|
+
const newIndex = historyIndex - 1;
|
|
108
|
+
setHistoryIndex(newIndex);
|
|
109
|
+
lastSavedDataRef.current = JSON.stringify(history[newIndex]);
|
|
110
|
+
onDataChange(history[newIndex]);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const handleRedo = () => {
|
|
115
|
+
if (historyIndex < history.length - 1) {
|
|
116
|
+
const newIndex = historyIndex + 1;
|
|
117
|
+
setHistoryIndex(newIndex);
|
|
118
|
+
lastSavedDataRef.current = JSON.stringify(history[newIndex]);
|
|
119
|
+
onDataChange(history[newIndex]);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Keyboard shortcuts for undo/redo
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
126
|
+
// Undo: Ctrl+Z (Cmd+Z on Mac)
|
|
127
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
|
|
128
|
+
e.preventDefault();
|
|
129
|
+
handleUndo();
|
|
130
|
+
}
|
|
131
|
+
// Redo: Ctrl+Shift+Z or Ctrl+Y (Cmd+Shift+Z or Cmd+Y on Mac)
|
|
132
|
+
else if ((e.ctrlKey || e.metaKey) && (e.shiftKey && e.key === 'z' || e.key === 'y')) {
|
|
133
|
+
e.preventDefault();
|
|
134
|
+
handleRedo();
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
139
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
140
|
+
}, [historyIndex, history]);
|
|
141
|
+
|
|
142
|
+
// Throttled history update when currentData changes
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
const currentDataStr = JSON.stringify(currentData);
|
|
145
|
+
|
|
146
|
+
// Skip if data hasn't actually changed
|
|
147
|
+
if (currentDataStr === lastSavedDataRef.current) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Clear existing throttle timeout
|
|
152
|
+
if (throttleTimeoutRef.current) {
|
|
153
|
+
clearTimeout(throttleTimeoutRef.current);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Set new throttled update
|
|
157
|
+
throttleTimeoutRef.current = setTimeout(() => {
|
|
158
|
+
lastSavedDataRef.current = currentDataStr;
|
|
159
|
+
|
|
160
|
+
setHistory(prev => {
|
|
161
|
+
// Slice history at current index (discard future states)
|
|
162
|
+
const newHistory = prev.slice(0, historyIndex + 1);
|
|
163
|
+
// Add new state
|
|
164
|
+
newHistory.push(currentData);
|
|
165
|
+
// Limit history size to 50 states
|
|
166
|
+
if (newHistory.length > 50) {
|
|
167
|
+
newHistory.shift();
|
|
168
|
+
return newHistory;
|
|
169
|
+
}
|
|
170
|
+
return newHistory;
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
setHistoryIndex(prev => {
|
|
174
|
+
const newHistory = history.slice(0, prev + 1);
|
|
175
|
+
newHistory.push(currentData);
|
|
176
|
+
return Math.min(newHistory.length - 1, 49);
|
|
177
|
+
});
|
|
178
|
+
}, 500); // 500ms throttle
|
|
179
|
+
|
|
180
|
+
return () => {
|
|
181
|
+
if (throttleTimeoutRef.current) {
|
|
182
|
+
clearTimeout(throttleTimeoutRef.current);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
}, [currentData, historyIndex, history]);
|
|
186
|
+
|
|
187
|
+
const handleLoad = async () => {
|
|
188
|
+
const prefab = await loadJson();
|
|
189
|
+
if (prefab) {
|
|
190
|
+
onDataChange(prefab);
|
|
191
|
+
// Reset history when loading new file
|
|
192
|
+
setHistory([prefab]);
|
|
193
|
+
setHistoryIndex(0);
|
|
194
|
+
lastSavedDataRef.current = JSON.stringify(prefab);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const canUndo = historyIndex > 0;
|
|
199
|
+
const canRedo = historyIndex < history.length - 1;
|
|
200
|
+
|
|
201
|
+
return <div style={{
|
|
202
|
+
position: "absolute",
|
|
203
|
+
top: 8,
|
|
204
|
+
left: "50%",
|
|
205
|
+
transform: "translateX(-50%)",
|
|
206
|
+
display: "flex",
|
|
207
|
+
alignItems: "center",
|
|
208
|
+
gap: 6,
|
|
209
|
+
padding: "2px 4px",
|
|
210
|
+
background: "rgba(0,0,0,0.55)",
|
|
211
|
+
border: "1px solid rgba(255,255,255,0.12)",
|
|
212
|
+
borderRadius: 4,
|
|
213
|
+
color: "rgba(255,255,255,0.9)",
|
|
214
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
|
215
|
+
fontSize: 11,
|
|
216
|
+
lineHeight: 1,
|
|
217
|
+
WebkitUserSelect: "none",
|
|
218
|
+
userSelect: "none",
|
|
219
|
+
}}>
|
|
220
|
+
<PanelButton onClick={() => onEditModeChange(!editMode)}>
|
|
221
|
+
{editMode ? "▶" : "⏸"}
|
|
222
|
+
</PanelButton>
|
|
223
|
+
|
|
224
|
+
<span style={{ opacity: 0.35 }}>|</span>
|
|
225
|
+
|
|
226
|
+
<PanelButton onClick={handleUndo} disabled={!canUndo} title="Undo (Ctrl+Z)">
|
|
227
|
+
↶
|
|
228
|
+
</PanelButton>
|
|
229
|
+
|
|
230
|
+
<PanelButton onClick={handleRedo} disabled={!canRedo} title="Redo (Ctrl+Shift+Z)">
|
|
231
|
+
↷
|
|
232
|
+
</PanelButton>
|
|
233
|
+
|
|
234
|
+
<span style={{ opacity: 0.35 }}>|</span>
|
|
235
|
+
|
|
236
|
+
<PanelButton onClick={handleLoad} title="Load JSON">
|
|
237
|
+
📥
|
|
238
|
+
</PanelButton>
|
|
239
|
+
|
|
240
|
+
<PanelButton onClick={() => saveJson(currentData, "prefab")} title="Save JSON">
|
|
241
|
+
💾
|
|
242
|
+
</PanelButton>
|
|
243
|
+
</div>;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const PanelButton = ({
|
|
247
|
+
onClick,
|
|
248
|
+
disabled,
|
|
249
|
+
title,
|
|
250
|
+
children
|
|
251
|
+
}: {
|
|
252
|
+
onClick: () => void;
|
|
253
|
+
disabled?: boolean;
|
|
254
|
+
title?: string;
|
|
255
|
+
children: React.ReactNode;
|
|
256
|
+
}) => {
|
|
257
|
+
return <button
|
|
258
|
+
style={{
|
|
259
|
+
padding: "2px 6px",
|
|
260
|
+
font: "inherit",
|
|
261
|
+
background: "transparent",
|
|
262
|
+
color: disabled ? "rgba(255,255,255,0.3)" : "inherit",
|
|
263
|
+
border: "1px solid rgba(255,255,255,0.18)",
|
|
264
|
+
borderRadius: 3,
|
|
265
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
266
|
+
opacity: disabled ? 0.5 : 1,
|
|
267
|
+
}}
|
|
268
|
+
onClick={onClick}
|
|
269
|
+
disabled={disabled}
|
|
270
|
+
title={title}
|
|
271
|
+
onPointerEnter={(e) => {
|
|
272
|
+
if (!disabled) {
|
|
273
|
+
(e.currentTarget as HTMLButtonElement).style.background = "rgba(255,255,255,0.08)";
|
|
274
|
+
}
|
|
275
|
+
}}
|
|
276
|
+
onPointerLeave={(e) => {
|
|
277
|
+
(e.currentTarget as HTMLButtonElement).style.background = "transparent";
|
|
278
|
+
}}
|
|
279
|
+
>
|
|
280
|
+
{children}
|
|
281
|
+
</button>;
|
|
282
|
+
};
|
|
283
|
+
|
|
168
284
|
const saveJson = (data: any, filename: string) => {
|
|
169
285
|
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data, null, 2));
|
|
170
286
|
const downloadAnchorNode = document.createElement('a');
|
|
@@ -203,11 +203,12 @@ function GameObjectRenderer({
|
|
|
203
203
|
|
|
204
204
|
// Early return if gameObject is null or undefined
|
|
205
205
|
if (!gameObject) return null;
|
|
206
|
+
if (gameObject.disabled === true || gameObject.hidden === true) return null;
|
|
206
207
|
|
|
207
|
-
// Build
|
|
208
|
+
// Build context object for passing to helper functions
|
|
208
209
|
const ctx = { gameObject, selectedId, onSelect, registerRef, loadedModels, loadedTextures, editMode };
|
|
209
210
|
|
|
210
|
-
// --- 1.
|
|
211
|
+
// --- 1. Compute transforms (local + world) ---
|
|
211
212
|
const transformProps = getNodeTransformProps(gameObject);
|
|
212
213
|
const localMatrix = new Matrix4().compose(
|
|
213
214
|
new Vector3(...transformProps.position),
|
|
@@ -216,7 +217,7 @@ function GameObjectRenderer({
|
|
|
216
217
|
);
|
|
217
218
|
const worldMatrix = parentMatrix.clone().multiply(localMatrix);
|
|
218
219
|
|
|
219
|
-
//
|
|
220
|
+
// --- 2. Handle selection interaction (edit mode only) ---
|
|
220
221
|
const clickValid = useRef(false);
|
|
221
222
|
const handlePointerDown = (e: ThreeEvent<PointerEvent>) => {
|
|
222
223
|
e.stopPropagation();
|
|
@@ -233,18 +234,19 @@ function GameObjectRenderer({
|
|
|
233
234
|
clickValid.current = false;
|
|
234
235
|
};
|
|
235
236
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
// --- 2. If instanced, short-circuit to a tiny clean branch ---
|
|
237
|
+
// --- 3. If instanced model, short-circuit to GameInstance (terminal node) ---
|
|
239
238
|
const isInstanced = !!gameObject.components?.model?.properties?.instanced;
|
|
240
239
|
if (isInstanced) {
|
|
241
240
|
return renderInstancedNode(gameObject, worldMatrix, ctx);
|
|
242
241
|
}
|
|
243
242
|
|
|
244
|
-
// ---
|
|
243
|
+
// --- 4. Render core content using component system ---
|
|
245
244
|
const core = renderCoreNode(gameObject, ctx, parentMatrix);
|
|
246
245
|
|
|
247
|
-
// --- 5.
|
|
246
|
+
// --- 5. Wrap with physics if needed (except in edit mode) ---
|
|
247
|
+
const physicsWrapped = wrapPhysicsIfNeeded(gameObject, core, ctx);
|
|
248
|
+
|
|
249
|
+
// --- 6. Render children recursively (always relative transforms) ---
|
|
248
250
|
const children = (gameObject.children ?? []).map((child) => (
|
|
249
251
|
<GameObjectRenderer
|
|
250
252
|
key={child.id}
|
|
@@ -259,10 +261,7 @@ function GameObjectRenderer({
|
|
|
259
261
|
/>
|
|
260
262
|
));
|
|
261
263
|
|
|
262
|
-
// ---
|
|
263
|
-
const physicsWrapped = wrapPhysicsIfNeeded(gameObject, core, ctx);
|
|
264
|
-
|
|
265
|
-
// --- 6. Final group wrapper ---
|
|
264
|
+
// --- 7. Final group wrapper with local transform ---
|
|
266
265
|
return (
|
|
267
266
|
<group
|
|
268
267
|
ref={(el) => registerRef(gameObject.id, el)}
|
|
@@ -300,18 +299,17 @@ function renderInstancedNode(gameObject: GameObjectType, worldMatrix: Matrix4, c
|
|
|
300
299
|
);
|
|
301
300
|
}
|
|
302
301
|
|
|
303
|
-
// Helper: render main
|
|
302
|
+
// Helper: render main content for a non-instanced node using the component system
|
|
304
303
|
function renderCoreNode(gameObject: GameObjectType, ctx: any, parentMatrix: Matrix4 | undefined) {
|
|
305
304
|
const geometry = gameObject.components?.geometry;
|
|
306
305
|
const material = gameObject.components?.material;
|
|
307
|
-
const
|
|
306
|
+
const model = gameObject.components?.model;
|
|
308
307
|
|
|
309
308
|
const geometryDef = geometry ? getComponent('Geometry') : undefined;
|
|
310
309
|
const materialDef = material ? getComponent('Material') : undefined;
|
|
310
|
+
const modelDef = model ? getComponent('Model') : undefined;
|
|
311
311
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
// Generic component views (exclude geometry/material/model)
|
|
312
|
+
// Context props for all component Views
|
|
315
313
|
const contextProps = {
|
|
316
314
|
loadedModels: ctx.loadedModels,
|
|
317
315
|
loadedTextures: ctx.loadedTextures,
|
|
@@ -320,61 +318,71 @@ function renderCoreNode(gameObject: GameObjectType, ctx: any, parentMatrix: Matr
|
|
|
320
318
|
parentMatrix,
|
|
321
319
|
registerRef: ctx.registerRef,
|
|
322
320
|
};
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
321
|
+
|
|
322
|
+
// Collect wrapper and leaf components (excluding transform/physics which are handled separately)
|
|
323
|
+
const wrapperComponents: Array<{ key: string; View: any; properties: any }> = [];
|
|
324
|
+
const leafComponents: React.ReactNode[] = [];
|
|
325
|
+
|
|
326
|
+
if (gameObject.components) {
|
|
327
|
+
Object.entries(gameObject.components)
|
|
328
|
+
.filter(([key]) => !['geometry', 'material', 'model', 'transform', 'physics'].includes(key))
|
|
329
|
+
.forEach(([key, comp]) => {
|
|
330
|
+
if (!comp || !comp.type) return;
|
|
328
331
|
const def = getComponent(comp.type);
|
|
329
|
-
if (!def || !def.View) return
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
332
|
+
if (!def || !def.View) return;
|
|
333
|
+
|
|
334
|
+
// Components that accept children are wrappers, others are leaves
|
|
335
|
+
const viewString = def.View.toString();
|
|
336
|
+
if (viewString.includes('children')) {
|
|
337
|
+
wrapperComponents.push({ key, View: def.View, properties: comp.properties });
|
|
338
|
+
} else {
|
|
339
|
+
leafComponents.push(<def.View key={key} properties={comp.properties} {...contextProps} />);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Build core content based on what components exist
|
|
345
|
+
let coreContent: React.ReactNode;
|
|
346
|
+
|
|
347
|
+
// Priority: Model > Geometry + Material > Empty
|
|
348
|
+
if (model && modelDef && modelDef.View) {
|
|
349
|
+
// Model component wraps its children (including material override)
|
|
350
|
+
coreContent = (
|
|
351
|
+
<modelDef.View properties={model.properties} {...contextProps}>
|
|
339
352
|
{material && materialDef && materialDef.View && (
|
|
340
353
|
<materialDef.View
|
|
341
354
|
key="material"
|
|
342
355
|
properties={material.properties}
|
|
343
|
-
|
|
344
|
-
isSelected={ctx.selectedId === gameObject.id}
|
|
345
|
-
editMode={ctx.editMode}
|
|
346
|
-
parentMatrix={parentMatrix}
|
|
347
|
-
registerRef={ctx.registerRef}
|
|
356
|
+
{...contextProps}
|
|
348
357
|
/>
|
|
349
358
|
)}
|
|
350
|
-
{
|
|
351
|
-
</
|
|
359
|
+
{leafComponents}
|
|
360
|
+
</modelDef.View>
|
|
352
361
|
);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
<mesh>
|
|
359
|
-
<geometryDef.View key="geometry" properties={geometry.properties} {...contextProps} />
|
|
362
|
+
} else if (geometry && geometryDef && geometryDef.View) {
|
|
363
|
+
// Geometry + Material = mesh
|
|
364
|
+
coreContent = (
|
|
365
|
+
<mesh castShadow receiveShadow>
|
|
366
|
+
<geometryDef.View properties={geometry.properties} {...contextProps} />
|
|
360
367
|
{material && materialDef && materialDef.View && (
|
|
361
368
|
<materialDef.View
|
|
362
369
|
key="material"
|
|
363
370
|
properties={material.properties}
|
|
364
|
-
|
|
365
|
-
isSelected={ctx.selectedId === gameObject.id}
|
|
366
|
-
editMode={ctx.editMode}
|
|
367
|
-
parentMatrix={parentMatrix}
|
|
368
|
-
registerRef={ctx.registerRef}
|
|
371
|
+
{...contextProps}
|
|
369
372
|
/>
|
|
370
373
|
)}
|
|
371
|
-
{
|
|
374
|
+
{leafComponents}
|
|
372
375
|
</mesh>
|
|
373
376
|
);
|
|
377
|
+
} else {
|
|
378
|
+
// No visual component - just render leaves
|
|
379
|
+
coreContent = <>{leafComponents}</>;
|
|
374
380
|
}
|
|
375
381
|
|
|
376
|
-
//
|
|
377
|
-
return
|
|
382
|
+
// Wrap core content with wrapper components (in order)
|
|
383
|
+
return wrapperComponents.reduce((content, { key, View, properties }) => {
|
|
384
|
+
return <View key={key} properties={properties} {...contextProps}>{content}</View>;
|
|
385
|
+
}, coreContent);
|
|
378
386
|
}
|
|
379
387
|
|
|
380
388
|
// Helper: wrap core content with physics component when necessary
|
|
@@ -2,7 +2,13 @@ import { FC } from "react";
|
|
|
2
2
|
|
|
3
3
|
export interface Component {
|
|
4
4
|
name: string;
|
|
5
|
-
Editor: FC<{
|
|
5
|
+
Editor: FC<{
|
|
6
|
+
component: any;
|
|
7
|
+
onUpdate: (newComp: any) => void;
|
|
8
|
+
basePath?: string;
|
|
9
|
+
transformMode?: "translate" | "rotate" | "scale";
|
|
10
|
+
setTransformMode?: (m: "translate" | "rotate" | "scale") => void;
|
|
11
|
+
}>;
|
|
6
12
|
defaultProperties: any;
|
|
7
13
|
// Allow View to accept extra props for special cases (like material)
|
|
8
14
|
View?: FC<any>;
|