react-three-game 0.0.60 → 0.0.62
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 +56 -0
- package/dist/index.d.ts +1 -1
- package/dist/shared/GameCanvas.d.ts +2 -1
- package/dist/shared/GameCanvas.js +7 -2
- package/dist/tools/prefabeditor/PrefabEditor.d.ts +18 -4
- package/dist/tools/prefabeditor/PrefabEditor.js +90 -36
- package/dist/tools/prefabeditor/utils.d.ts +2 -0
- package/dist/tools/prefabeditor/utils.js +15 -0
- package/package.json +9 -3
- package/.gitattributes +0 -2
- package/.github/copilot-instructions.md +0 -83
- package/.github/workflows/nextjs.yml +0 -99
- package/.gitmodules +0 -3
- package/assets/architecture.png +0 -0
- package/assets/editor.gif +0 -0
- package/assets/favicon.ico +0 -0
- package/assets/react-three-game-logo.png +0 -0
- package/dist/tools/dragdrop/page.d.ts +0 -1
- package/dist/tools/dragdrop/page.js +0 -11
- package/dist/tools/prefabeditor/EntityEvents.d.ts +0 -54
- package/dist/tools/prefabeditor/EntityEvents.js +0 -85
- package/dist/tools/prefabeditor/page.d.ts +0 -1
- package/dist/tools/prefabeditor/page.js +0 -5
- package/react-three-game-skill/.gitattributes +0 -2
- package/react-three-game-skill/README.md +0 -7
- package/react-three-game-skill/react-three-game/SKILL.md +0 -514
- package/react-three-game-skill/react-three-game/rules/ADVANCED_PHYSICS.md +0 -472
- package/react-three-game-skill/react-three-game/rules/LIGHTING.md +0 -6
- package/src/helpers/SoundManager.ts +0 -130
- package/src/helpers/index.ts +0 -91
- package/src/index.ts +0 -59
- package/src/shared/ContactShadow.tsx +0 -74
- package/src/shared/GameCanvas.tsx +0 -52
- package/src/tools/assetviewer/page.tsx +0 -425
- package/src/tools/dragdrop/DragDropLoader.tsx +0 -159
- package/src/tools/dragdrop/index.ts +0 -4
- package/src/tools/dragdrop/modelLoader.ts +0 -204
- package/src/tools/dragdrop/page.tsx +0 -45
- package/src/tools/prefabeditor/Dropdown.tsx +0 -112
- package/src/tools/prefabeditor/EditorContext.tsx +0 -25
- package/src/tools/prefabeditor/EditorTree.tsx +0 -452
- package/src/tools/prefabeditor/EditorTreeMenus.tsx +0 -307
- package/src/tools/prefabeditor/EditorUI.tsx +0 -204
- package/src/tools/prefabeditor/EventSystem.tsx +0 -36
- package/src/tools/prefabeditor/GameEvents.ts +0 -191
- package/src/tools/prefabeditor/InstanceProvider.tsx +0 -466
- package/src/tools/prefabeditor/PrefabEditor.tsx +0 -256
- package/src/tools/prefabeditor/PrefabRoot.tsx +0 -767
- package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +0 -34
- package/src/tools/prefabeditor/components/CameraComponent.tsx +0 -117
- package/src/tools/prefabeditor/components/ComponentRegistry.ts +0 -40
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +0 -210
- package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +0 -47
- package/src/tools/prefabeditor/components/GeometryComponent.tsx +0 -133
- package/src/tools/prefabeditor/components/Input.tsx +0 -820
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +0 -431
- package/src/tools/prefabeditor/components/ModelComponent.tsx +0 -176
- package/src/tools/prefabeditor/components/PhysicsComponent.tsx +0 -188
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +0 -109
- package/src/tools/prefabeditor/components/TextComponent.tsx +0 -137
- package/src/tools/prefabeditor/components/TransformComponent.tsx +0 -173
- package/src/tools/prefabeditor/components/index.ts +0 -26
- package/src/tools/prefabeditor/page.tsx +0 -10
- package/src/tools/prefabeditor/styles.ts +0 -235
- package/src/tools/prefabeditor/types.ts +0 -20
- package/src/tools/prefabeditor/utils.ts +0 -312
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
// Shared editor styles - single source of truth for all prefab editor UI
|
|
2
|
-
|
|
3
|
-
export const colors = {
|
|
4
|
-
bg: '#1e1e1e',
|
|
5
|
-
bgSurface: '#252526',
|
|
6
|
-
bgLight: '#2d2d2d',
|
|
7
|
-
bgHover: '#2a2d2e',
|
|
8
|
-
bgInput: '#1a1a1a',
|
|
9
|
-
border: '#3c3c3c',
|
|
10
|
-
borderLight: '#333333',
|
|
11
|
-
borderFaint: '#2a2a2a',
|
|
12
|
-
text: '#cccccc',
|
|
13
|
-
textMuted: '#999999',
|
|
14
|
-
textDim: '#666666',
|
|
15
|
-
accent: '#4c9eff',
|
|
16
|
-
accentBg: 'rgba(76, 158, 255, 0.12)',
|
|
17
|
-
accentBorder: 'rgba(76, 158, 255, 0.4)',
|
|
18
|
-
danger: '#f44747',
|
|
19
|
-
dangerBg: 'rgba(244, 71, 71, 0.12)',
|
|
20
|
-
dangerBorder: 'rgba(244, 71, 71, 0.35)',
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export const fonts = {
|
|
24
|
-
family: 'system-ui, -apple-system, sans-serif',
|
|
25
|
-
size: 11,
|
|
26
|
-
sizeSm: 10,
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
// Base component styles
|
|
30
|
-
export const base = {
|
|
31
|
-
panel: {
|
|
32
|
-
background: colors.bg,
|
|
33
|
-
color: colors.text,
|
|
34
|
-
border: `1px solid ${colors.border}`,
|
|
35
|
-
borderRadius: 4,
|
|
36
|
-
fontFamily: fonts.family,
|
|
37
|
-
fontSize: fonts.size,
|
|
38
|
-
boxShadow: '0 2px 8px rgba(0,0,0,0.4)',
|
|
39
|
-
} as React.CSSProperties,
|
|
40
|
-
|
|
41
|
-
header: {
|
|
42
|
-
padding: '7px 10px',
|
|
43
|
-
display: 'flex',
|
|
44
|
-
alignItems: 'center',
|
|
45
|
-
justifyContent: 'space-between',
|
|
46
|
-
cursor: 'pointer',
|
|
47
|
-
background: colors.bgLight,
|
|
48
|
-
borderBottom: `1px solid ${colors.borderLight}`,
|
|
49
|
-
fontSize: fonts.size,
|
|
50
|
-
fontWeight: 600,
|
|
51
|
-
textTransform: 'uppercase',
|
|
52
|
-
letterSpacing: 0.8,
|
|
53
|
-
color: colors.text,
|
|
54
|
-
} as React.CSSProperties,
|
|
55
|
-
|
|
56
|
-
input: {
|
|
57
|
-
width: '100%',
|
|
58
|
-
background: colors.bgInput,
|
|
59
|
-
border: `1px solid ${colors.border}`,
|
|
60
|
-
borderRadius: 3,
|
|
61
|
-
padding: '5px 8px',
|
|
62
|
-
color: colors.text,
|
|
63
|
-
fontSize: fonts.size,
|
|
64
|
-
outline: 'none',
|
|
65
|
-
} as React.CSSProperties,
|
|
66
|
-
|
|
67
|
-
btn: {
|
|
68
|
-
background: colors.bgLight,
|
|
69
|
-
border: `1px solid ${colors.border}`,
|
|
70
|
-
borderRadius: 3,
|
|
71
|
-
padding: '4px 8px',
|
|
72
|
-
color: colors.text,
|
|
73
|
-
fontSize: fonts.size,
|
|
74
|
-
cursor: 'pointer',
|
|
75
|
-
outline: 'none',
|
|
76
|
-
} as React.CSSProperties,
|
|
77
|
-
|
|
78
|
-
btnDanger: {
|
|
79
|
-
background: colors.dangerBg,
|
|
80
|
-
borderColor: colors.dangerBorder,
|
|
81
|
-
color: colors.danger,
|
|
82
|
-
} as React.CSSProperties,
|
|
83
|
-
|
|
84
|
-
label: {
|
|
85
|
-
fontSize: fonts.sizeSm,
|
|
86
|
-
color: colors.textMuted,
|
|
87
|
-
marginBottom: 4,
|
|
88
|
-
textTransform: 'uppercase',
|
|
89
|
-
letterSpacing: 0.5,
|
|
90
|
-
fontWeight: 500,
|
|
91
|
-
} as React.CSSProperties,
|
|
92
|
-
|
|
93
|
-
row: {
|
|
94
|
-
display: 'flex',
|
|
95
|
-
gap: 6,
|
|
96
|
-
} as React.CSSProperties,
|
|
97
|
-
|
|
98
|
-
section: {
|
|
99
|
-
paddingBottom: 8,
|
|
100
|
-
borderBottom: `1px solid ${colors.borderLight}`,
|
|
101
|
-
} as React.CSSProperties,
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
// Specific panel styles
|
|
105
|
-
export const inspector = {
|
|
106
|
-
panel: {
|
|
107
|
-
...base.panel,
|
|
108
|
-
position: 'absolute' as const,
|
|
109
|
-
top: 8,
|
|
110
|
-
right: 8,
|
|
111
|
-
zIndex: 20,
|
|
112
|
-
width: 260,
|
|
113
|
-
},
|
|
114
|
-
content: {
|
|
115
|
-
padding: 8,
|
|
116
|
-
maxHeight: '80vh',
|
|
117
|
-
overflowY: 'auto' as const,
|
|
118
|
-
overflowX: 'hidden' as const,
|
|
119
|
-
boxSizing: 'border-box' as const,
|
|
120
|
-
display: 'flex',
|
|
121
|
-
flexDirection: 'column' as const,
|
|
122
|
-
gap: 8,
|
|
123
|
-
},
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
export const tree = {
|
|
127
|
-
panel: {
|
|
128
|
-
...base.panel,
|
|
129
|
-
maxHeight: '85vh',
|
|
130
|
-
display: 'flex',
|
|
131
|
-
flexDirection: 'column' as const,
|
|
132
|
-
userSelect: 'none' as const,
|
|
133
|
-
},
|
|
134
|
-
scroll: {
|
|
135
|
-
overflowY: 'auto' as const,
|
|
136
|
-
padding: 4,
|
|
137
|
-
scrollbarWidth: 'thin' as const,
|
|
138
|
-
scrollbarColor: `${colors.bgLight} transparent`,
|
|
139
|
-
} as React.CSSProperties,
|
|
140
|
-
row: {
|
|
141
|
-
display: 'flex',
|
|
142
|
-
alignItems: 'center',
|
|
143
|
-
padding: '3px 6px',
|
|
144
|
-
borderBottomWidth: 1,
|
|
145
|
-
borderBottomStyle: 'solid',
|
|
146
|
-
borderBottomColor: colors.borderFaint,
|
|
147
|
-
cursor: 'pointer',
|
|
148
|
-
whiteSpace: 'nowrap' as const,
|
|
149
|
-
borderRadius: 2,
|
|
150
|
-
} as React.CSSProperties,
|
|
151
|
-
selected: {
|
|
152
|
-
background: colors.accentBg,
|
|
153
|
-
borderBottomColor: colors.accentBorder,
|
|
154
|
-
},
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
export const menu = {
|
|
158
|
-
container: {
|
|
159
|
-
position: 'fixed' as const,
|
|
160
|
-
zIndex: 50,
|
|
161
|
-
minWidth: 'auto',
|
|
162
|
-
width: 'max-content',
|
|
163
|
-
maxWidth: 'min(240px, calc(100vw - 16px))',
|
|
164
|
-
background: colors.bgSurface,
|
|
165
|
-
border: `1px solid ${colors.border}`,
|
|
166
|
-
borderRadius: 4,
|
|
167
|
-
overflow: 'hidden',
|
|
168
|
-
boxShadow: '0 4px 16px rgba(0,0,0,0.6)',
|
|
169
|
-
},
|
|
170
|
-
item: {
|
|
171
|
-
width: '100%',
|
|
172
|
-
textAlign: 'left' as const,
|
|
173
|
-
padding: '7px 12px',
|
|
174
|
-
background: 'transparent',
|
|
175
|
-
border: 'none',
|
|
176
|
-
color: colors.text,
|
|
177
|
-
fontSize: fonts.size,
|
|
178
|
-
whiteSpace: 'nowrap' as const,
|
|
179
|
-
cursor: 'pointer',
|
|
180
|
-
outline: 'none',
|
|
181
|
-
} as React.CSSProperties,
|
|
182
|
-
danger: {
|
|
183
|
-
color: colors.danger,
|
|
184
|
-
},
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
export const toolbar = {
|
|
188
|
-
panel: {
|
|
189
|
-
position: 'absolute' as const,
|
|
190
|
-
top: 8,
|
|
191
|
-
left: '240px',
|
|
192
|
-
display: 'flex',
|
|
193
|
-
gap: 6,
|
|
194
|
-
padding: '4px 6px',
|
|
195
|
-
background: colors.bg,
|
|
196
|
-
border: `1px solid ${colors.border}`,
|
|
197
|
-
borderRadius: 4,
|
|
198
|
-
color: colors.text,
|
|
199
|
-
fontFamily: fonts.family,
|
|
200
|
-
fontSize: fonts.size,
|
|
201
|
-
boxShadow: '0 2px 8px rgba(0,0,0,0.4)',
|
|
202
|
-
},
|
|
203
|
-
divider: {
|
|
204
|
-
width: 1,
|
|
205
|
-
background: colors.borderLight,
|
|
206
|
-
},
|
|
207
|
-
disabled: {
|
|
208
|
-
opacity: 0.4,
|
|
209
|
-
cursor: 'not-allowed',
|
|
210
|
-
},
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
// Shared scrollbar CSS (inject via <style> tag since CSS can't be bundled)
|
|
214
|
-
export const scrollbarCSS = `
|
|
215
|
-
.prefab-scroll::-webkit-scrollbar,
|
|
216
|
-
.tree-scroll::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
217
|
-
.prefab-scroll::-webkit-scrollbar-track,
|
|
218
|
-
.tree-scroll::-webkit-scrollbar-track { background: transparent; }
|
|
219
|
-
.prefab-scroll::-webkit-scrollbar-thumb,
|
|
220
|
-
.tree-scroll::-webkit-scrollbar-thumb { background: ${colors.border}; border-radius: 3px; }
|
|
221
|
-
.prefab-scroll::-webkit-scrollbar-thumb:hover,
|
|
222
|
-
.tree-scroll::-webkit-scrollbar-thumb:hover { background: #555; }
|
|
223
|
-
.prefab-scroll { scrollbar-width: thin; scrollbar-color: ${colors.border} transparent; }
|
|
224
|
-
`;
|
|
225
|
-
|
|
226
|
-
// Reusable component card style for inspector sections
|
|
227
|
-
export const componentCard = {
|
|
228
|
-
container: {
|
|
229
|
-
marginBottom: 8,
|
|
230
|
-
backgroundColor: colors.bgSurface,
|
|
231
|
-
padding: 8,
|
|
232
|
-
borderRadius: 4,
|
|
233
|
-
border: `1px solid ${colors.border}`,
|
|
234
|
-
} as React.CSSProperties,
|
|
235
|
-
};
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
export interface Prefab {
|
|
2
|
-
id?: string;
|
|
3
|
-
name?: string;
|
|
4
|
-
root: GameObject;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export interface GameObject {
|
|
8
|
-
id: string;
|
|
9
|
-
name?: string;
|
|
10
|
-
disabled?: boolean;
|
|
11
|
-
children?: GameObject[];
|
|
12
|
-
components?: {
|
|
13
|
-
[key: string]: ComponentData | undefined;
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface ComponentData {
|
|
18
|
-
type: string;
|
|
19
|
-
properties: Record<string, any>;
|
|
20
|
-
}
|
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
import { GameObject, Prefab } from "./types";
|
|
2
|
-
import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js';
|
|
3
|
-
import { Box3, Object3D, PerspectiveCamera, Quaternion, Vector3 } from 'three';
|
|
4
|
-
|
|
5
|
-
export interface ExportGLBOptions {
|
|
6
|
-
filename?: string;
|
|
7
|
-
binary?: boolean;
|
|
8
|
-
onComplete?: (result: ArrayBuffer | object) => void;
|
|
9
|
-
onError?: (error: any) => void;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/** Save a prefab as JSON file, showing a Save As dialog when supported */
|
|
13
|
-
export async function saveJson(data: Prefab, filename: string) {
|
|
14
|
-
const json = JSON.stringify(data, null, 2);
|
|
15
|
-
if ('showSaveFilePicker' in window) {
|
|
16
|
-
try {
|
|
17
|
-
const handle = await (window as any).showSaveFilePicker({
|
|
18
|
-
suggestedName: `${filename || 'prefab'}.json`,
|
|
19
|
-
types: [{ description: 'JSON', accept: { 'application/json': ['.json'] } }],
|
|
20
|
-
});
|
|
21
|
-
const writable = await handle.createWritable();
|
|
22
|
-
await writable.write(json);
|
|
23
|
-
await writable.close();
|
|
24
|
-
return;
|
|
25
|
-
} catch (e: any) {
|
|
26
|
-
if (e?.name === 'AbortError') return; // user cancelled
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
// Fallback for browsers without File System Access API
|
|
30
|
-
const a = document.createElement('a');
|
|
31
|
-
a.href = "data:text/json;charset=utf-8," + encodeURIComponent(json);
|
|
32
|
-
a.download = `${filename || 'prefab'}.json`;
|
|
33
|
-
a.click();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Load a prefab from JSON file */
|
|
37
|
-
export function loadJson(): Promise<Prefab | undefined> {
|
|
38
|
-
return new Promise(resolve => {
|
|
39
|
-
const input = document.createElement('input');
|
|
40
|
-
input.type = 'file';
|
|
41
|
-
input.accept = '.json,application/json';
|
|
42
|
-
input.onchange = e => {
|
|
43
|
-
const file = (e.target as HTMLInputElement).files?.[0];
|
|
44
|
-
if (!file) return resolve(undefined);
|
|
45
|
-
const reader = new FileReader();
|
|
46
|
-
reader.onload = e => {
|
|
47
|
-
try {
|
|
48
|
-
const text = e.target?.result;
|
|
49
|
-
if (typeof text === 'string') resolve(JSON.parse(text) as Prefab);
|
|
50
|
-
} catch (err) {
|
|
51
|
-
console.error('Error parsing prefab JSON:', err);
|
|
52
|
-
resolve(undefined);
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
reader.readAsText(file);
|
|
56
|
-
};
|
|
57
|
-
input.click();
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Export a Three.js scene or object to GLB format
|
|
63
|
-
* @param sceneRoot - The Three.js Object3D to export
|
|
64
|
-
* @param options - Export options
|
|
65
|
-
* @returns Promise that resolves when export is complete
|
|
66
|
-
*/
|
|
67
|
-
export function exportGLB(
|
|
68
|
-
sceneRoot: Object3D,
|
|
69
|
-
options: ExportGLBOptions = {}
|
|
70
|
-
): Promise<ArrayBuffer | object> {
|
|
71
|
-
const {
|
|
72
|
-
filename = 'scene.glb',
|
|
73
|
-
binary = true,
|
|
74
|
-
onComplete,
|
|
75
|
-
onError
|
|
76
|
-
} = options;
|
|
77
|
-
|
|
78
|
-
return new Promise((resolve, reject) => {
|
|
79
|
-
const exporter = new GLTFExporter();
|
|
80
|
-
|
|
81
|
-
exporter.parse(
|
|
82
|
-
sceneRoot,
|
|
83
|
-
(result) => {
|
|
84
|
-
onComplete?.(result);
|
|
85
|
-
resolve(result);
|
|
86
|
-
|
|
87
|
-
// Trigger download if filename is provided
|
|
88
|
-
if (filename) {
|
|
89
|
-
const blob = new Blob(
|
|
90
|
-
[result as ArrayBuffer],
|
|
91
|
-
{ type: binary ? 'application/octet-stream' : 'application/json' }
|
|
92
|
-
);
|
|
93
|
-
const url = URL.createObjectURL(blob);
|
|
94
|
-
const a = document.createElement('a');
|
|
95
|
-
a.href = url;
|
|
96
|
-
a.download = filename;
|
|
97
|
-
a.click();
|
|
98
|
-
URL.revokeObjectURL(url);
|
|
99
|
-
}
|
|
100
|
-
},
|
|
101
|
-
(error) => {
|
|
102
|
-
console.error('Error exporting GLB:', error);
|
|
103
|
-
onError?.(error);
|
|
104
|
-
reject(error);
|
|
105
|
-
},
|
|
106
|
-
{ binary }
|
|
107
|
-
);
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Export a Three.js scene to GLB and return the ArrayBuffer without downloading
|
|
113
|
-
* @param sceneRoot - The Three.js Object3D to export
|
|
114
|
-
* @returns Promise that resolves with the GLB data as ArrayBuffer
|
|
115
|
-
*/
|
|
116
|
-
export async function exportGLBData(sceneRoot: Object3D): Promise<ArrayBuffer> {
|
|
117
|
-
const result = await exportGLB(sceneRoot, { filename: '', binary: true });
|
|
118
|
-
return result as ArrayBuffer;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export function focusCameraOnObject(
|
|
122
|
-
object: Object3D,
|
|
123
|
-
camera: Object3D,
|
|
124
|
-
target: Vector3,
|
|
125
|
-
update?: () => void,
|
|
126
|
-
) {
|
|
127
|
-
const bounds = new Box3().setFromObject(object);
|
|
128
|
-
const center = new Vector3();
|
|
129
|
-
const size = new Vector3();
|
|
130
|
-
const quaternion = new Quaternion();
|
|
131
|
-
object.getWorldQuaternion(quaternion);
|
|
132
|
-
|
|
133
|
-
if (bounds.isEmpty()) {
|
|
134
|
-
object.getWorldPosition(center);
|
|
135
|
-
size.setScalar(1);
|
|
136
|
-
} else {
|
|
137
|
-
bounds.getCenter(center);
|
|
138
|
-
bounds.getSize(size);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const radius = Math.max(size.length() * 0.5, 1);
|
|
142
|
-
const forward = new Vector3(0, 0, 1).applyQuaternion(quaternion).normalize();
|
|
143
|
-
const worldUp = new Vector3(0, 1, 0);
|
|
144
|
-
const elevatedDirection = forward.clone().addScaledVector(worldUp, 0.65).normalize();
|
|
145
|
-
const distance = camera instanceof PerspectiveCamera
|
|
146
|
-
? Math.max(radius / Math.tan((camera.fov * Math.PI) / 360) * 1.8, radius * 3.5)
|
|
147
|
-
: radius * 4.5;
|
|
148
|
-
const nextPosition = center.clone().add(elevatedDirection.multiplyScalar(distance));
|
|
149
|
-
|
|
150
|
-
camera.position.copy(nextPosition);
|
|
151
|
-
camera.lookAt(center);
|
|
152
|
-
target.copy(center);
|
|
153
|
-
update?.();
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** Find a node by ID in the tree */
|
|
157
|
-
export function findNode(root: GameObject, id: string): GameObject | null {
|
|
158
|
-
if (root.id === id) return root;
|
|
159
|
-
for (const child of root.children ?? []) {
|
|
160
|
-
const found = findNode(child, id);
|
|
161
|
-
if (found) return found;
|
|
162
|
-
}
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** Find the parent of a node by ID */
|
|
167
|
-
export function findParent(root: GameObject, id: string): GameObject | null {
|
|
168
|
-
for (const child of root.children ?? []) {
|
|
169
|
-
if (child.id === id) return root;
|
|
170
|
-
const found = findParent(child, id);
|
|
171
|
-
if (found) return found;
|
|
172
|
-
}
|
|
173
|
-
return null;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/** Find all nodes matching a predicate */
|
|
177
|
-
export function findAll(root: GameObject, predicate: (node: GameObject) => boolean): GameObject[] {
|
|
178
|
-
const results: GameObject[] = [];
|
|
179
|
-
if (predicate(root)) results.push(root);
|
|
180
|
-
for (const child of root.children ?? []) {
|
|
181
|
-
results.push(...findAll(child, predicate));
|
|
182
|
-
}
|
|
183
|
-
return results;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/** Find all nodes that have a specific component type */
|
|
187
|
-
export function findByComponent(root: GameObject, componentType: string): GameObject[] {
|
|
188
|
-
return findAll(root, node =>
|
|
189
|
-
Object.values(node.components ?? {}).some(c => c?.type === componentType)
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/** Get a flattened list of all nodes */
|
|
194
|
-
export function flatten(root: GameObject): GameObject[] {
|
|
195
|
-
return findAll(root, () => true);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/** Immutably update a node by ID */
|
|
199
|
-
export function updateNode(root: GameObject, id: string, update: (node: GameObject) => GameObject): GameObject {
|
|
200
|
-
if (root.id === id) return update(root);
|
|
201
|
-
if (!root.children) return root;
|
|
202
|
-
return {
|
|
203
|
-
...root,
|
|
204
|
-
children: root.children.map(child => updateNode(child, id, update))
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/** Immutably delete a node by ID */
|
|
209
|
-
export function deleteNode(root: GameObject, id: string): GameObject | null {
|
|
210
|
-
if (root.id === id) return null;
|
|
211
|
-
if (!root.children) return root;
|
|
212
|
-
return {
|
|
213
|
-
...root,
|
|
214
|
-
children: root.children
|
|
215
|
-
.map(child => deleteNode(child, id))
|
|
216
|
-
.filter((child): child is GameObject => child !== null)
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/** Deep clone a node with new IDs */
|
|
221
|
-
export function cloneNode(node: GameObject): GameObject {
|
|
222
|
-
return {
|
|
223
|
-
...node,
|
|
224
|
-
id: crypto.randomUUID(),
|
|
225
|
-
name: `${node.name ?? node.id} Copy`,
|
|
226
|
-
children: node.children?.map(cloneNode)
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/** Recursively update all IDs in a node tree */
|
|
231
|
-
export function regenerateIds(node: GameObject): GameObject {
|
|
232
|
-
return {
|
|
233
|
-
...node,
|
|
234
|
-
id: crypto.randomUUID(),
|
|
235
|
-
children: node.children?.map(regenerateIds)
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/** Get component data from a node */
|
|
240
|
-
export function getComponent<T = any>(node: GameObject, type: string): T | undefined {
|
|
241
|
-
const comp = Object.values(node.components ?? {}).find(c => c?.type === type);
|
|
242
|
-
return comp?.properties as T | undefined;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
export function updateNodeById(
|
|
246
|
-
root: GameObject,
|
|
247
|
-
id: string,
|
|
248
|
-
updater: (node: GameObject) => GameObject
|
|
249
|
-
): GameObject {
|
|
250
|
-
if (root.id === id) {
|
|
251
|
-
return updater(root);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
if (!root.children) {
|
|
255
|
-
return root;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
let didChange = false;
|
|
259
|
-
|
|
260
|
-
const newChildren = root.children.map(child => {
|
|
261
|
-
const updated = updateNodeById(child, id, updater);
|
|
262
|
-
if (updated !== child) didChange = true;
|
|
263
|
-
return updated;
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
if (!didChange) return root;
|
|
267
|
-
|
|
268
|
-
return {
|
|
269
|
-
...root,
|
|
270
|
-
children: newChildren
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/** Create a GameObject node for a 3D model file */
|
|
275
|
-
export function createModelNode(filename: string, name?: string): GameObject {
|
|
276
|
-
return {
|
|
277
|
-
id: crypto.randomUUID(),
|
|
278
|
-
name: name ?? filename.replace(/^.*[\/]/, '').replace(/\.[^.]+$/, ''),
|
|
279
|
-
components: {
|
|
280
|
-
transform: {
|
|
281
|
-
type: 'Transform',
|
|
282
|
-
properties: { position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1] }
|
|
283
|
-
},
|
|
284
|
-
model: {
|
|
285
|
-
type: 'Model',
|
|
286
|
-
properties: { filename, instanced: false }
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/** Create a GameObject node for an image as a textured plane */
|
|
293
|
-
export function createImageNode(texturePath: string, name?: string): GameObject {
|
|
294
|
-
return {
|
|
295
|
-
id: crypto.randomUUID(),
|
|
296
|
-
name: name ?? texturePath.replace(/^.*[\/]/, '').replace(/\.[^.]+$/, ''),
|
|
297
|
-
components: {
|
|
298
|
-
transform: {
|
|
299
|
-
type: 'Transform',
|
|
300
|
-
properties: { position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1] }
|
|
301
|
-
},
|
|
302
|
-
geometry: {
|
|
303
|
-
type: 'Geometry',
|
|
304
|
-
properties: { geometryType: 'plane', args: [1, 1] }
|
|
305
|
-
},
|
|
306
|
-
material: {
|
|
307
|
-
type: 'Material',
|
|
308
|
-
properties: { color: '#ffffff', texture: texturePath }
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
};
|
|
312
|
-
}
|