react-three-game 0.0.59 → 0.0.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tools/dragdrop/DragDropLoader.d.ts +8 -8
- package/dist/tools/dragdrop/DragDropLoader.js +33 -15
- package/dist/tools/dragdrop/index.d.ts +3 -3
- package/dist/tools/dragdrop/index.js +1 -1
- package/dist/tools/dragdrop/modelLoader.d.ts +10 -1
- package/dist/tools/dragdrop/modelLoader.js +39 -0
- package/dist/tools/prefabeditor/PrefabEditor.js +17 -26
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +1 -1
- package/dist/tools/prefabeditor/PrefabRoot.js +2 -8
- 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 -136
- package/src/tools/dragdrop/index.ts +0 -4
- package/src/tools/dragdrop/modelLoader.ts +0 -145
- 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 -262
- package/src/tools/prefabeditor/PrefabRoot.tsx +0 -773
- 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,431 +0,0 @@
|
|
|
1
|
-
import { SingleTextureViewer, TextureListViewer } from '../../assetviewer/page';
|
|
2
|
-
import { extend } from '@react-three/fiber';
|
|
3
|
-
import type { ThreeElement } from '@react-three/fiber';
|
|
4
|
-
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
5
|
-
import { createPortal } from 'react-dom';
|
|
6
|
-
import { Component } from './ComponentRegistry';
|
|
7
|
-
import { FieldRenderer, FieldDefinition, Input } from './Input';
|
|
8
|
-
import { colors } from '../styles';
|
|
9
|
-
import { useMemo } from 'react';
|
|
10
|
-
import { MeshBasicNodeMaterial, MeshStandardNodeMaterial } from 'three/webgpu';
|
|
11
|
-
import {
|
|
12
|
-
RepeatWrapping,
|
|
13
|
-
ClampToEdgeWrapping,
|
|
14
|
-
SRGBColorSpace,
|
|
15
|
-
LinearSRGBColorSpace,
|
|
16
|
-
Texture,
|
|
17
|
-
Vector2,
|
|
18
|
-
NearestFilter,
|
|
19
|
-
LinearFilter,
|
|
20
|
-
NearestMipmapNearestFilter,
|
|
21
|
-
NearestMipmapLinearFilter,
|
|
22
|
-
LinearMipmapNearestFilter,
|
|
23
|
-
LinearMipmapLinearFilter,
|
|
24
|
-
MinificationTextureFilter,
|
|
25
|
-
MagnificationTextureFilter,
|
|
26
|
-
MeshBasicMaterialProperties,
|
|
27
|
-
MeshStandardMaterialProperties,
|
|
28
|
-
FrontSide,
|
|
29
|
-
BackSide,
|
|
30
|
-
DoubleSide,
|
|
31
|
-
} from 'three';
|
|
32
|
-
|
|
33
|
-
declare module '@react-three/fiber' {
|
|
34
|
-
interface ThreeElements {
|
|
35
|
-
meshBasicNodeMaterial: ThreeElement<typeof MeshBasicNodeMaterial>;
|
|
36
|
-
meshStandardNodeMaterial: ThreeElement<typeof MeshStandardNodeMaterial>;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface MaterialProps extends Omit<MeshStandardMaterialProperties & MeshBasicMaterialProperties, 'args' | 'normalScale'> {
|
|
41
|
-
materialType?: 'standard' | 'basic';
|
|
42
|
-
transmission?: number;
|
|
43
|
-
thickness?: number;
|
|
44
|
-
ior?: number;
|
|
45
|
-
texture?: string;
|
|
46
|
-
repeat?: boolean;
|
|
47
|
-
repeatCount?: [number, number];
|
|
48
|
-
generateMipmaps?: boolean;
|
|
49
|
-
minFilter?: string;
|
|
50
|
-
magFilter?: string;
|
|
51
|
-
normalMapTexture?: string;
|
|
52
|
-
normalScale?: [number, number];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const PICKER_POPUP_WIDTH = 260;
|
|
56
|
-
const PICKER_POPUP_HEIGHT = 360;
|
|
57
|
-
extend({
|
|
58
|
-
MeshBasicNodeMaterial,
|
|
59
|
-
MeshStandardNodeMaterial,
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
function TexturePicker({
|
|
63
|
-
value,
|
|
64
|
-
onChange,
|
|
65
|
-
basePath
|
|
66
|
-
}: {
|
|
67
|
-
value: string | undefined;
|
|
68
|
-
onChange: (v: string) => void;
|
|
69
|
-
basePath: string;
|
|
70
|
-
}) {
|
|
71
|
-
const [textureFiles, setTextureFiles] = useState<string[]>([]);
|
|
72
|
-
const [showPicker, setShowPicker] = useState(false);
|
|
73
|
-
const [popupStyle, setPopupStyle] = useState<React.CSSProperties | null>(null);
|
|
74
|
-
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
75
|
-
|
|
76
|
-
useEffect(() => {
|
|
77
|
-
fetch(`${basePath}/textures/manifest.json`)
|
|
78
|
-
.then(r => r.json())
|
|
79
|
-
.then(data => setTextureFiles(Array.isArray(data) ? data : data.files || []))
|
|
80
|
-
.catch(console.error);
|
|
81
|
-
}, [basePath]);
|
|
82
|
-
|
|
83
|
-
useLayoutEffect(() => {
|
|
84
|
-
if (!showPicker || !triggerRef.current || typeof window === 'undefined') return;
|
|
85
|
-
|
|
86
|
-
const updatePosition = () => {
|
|
87
|
-
const rect = triggerRef.current?.getBoundingClientRect();
|
|
88
|
-
if (!rect) return;
|
|
89
|
-
|
|
90
|
-
const preferredLeft = rect.left - PICKER_POPUP_WIDTH - 8;
|
|
91
|
-
const fallbackLeft = rect.right + 8;
|
|
92
|
-
const fitsLeft = preferredLeft >= 8;
|
|
93
|
-
const left = fitsLeft ? preferredLeft : Math.min(fallbackLeft, window.innerWidth - PICKER_POPUP_WIDTH - 8);
|
|
94
|
-
const top = Math.min(Math.max(8, rect.top), window.innerHeight - PICKER_POPUP_HEIGHT - 8);
|
|
95
|
-
|
|
96
|
-
setPopupStyle({
|
|
97
|
-
position: 'fixed',
|
|
98
|
-
left,
|
|
99
|
-
top,
|
|
100
|
-
background: colors.bg,
|
|
101
|
-
padding: 12,
|
|
102
|
-
border: `1px solid ${colors.border}`,
|
|
103
|
-
borderRadius: 6,
|
|
104
|
-
width: PICKER_POPUP_WIDTH,
|
|
105
|
-
height: PICKER_POPUP_HEIGHT,
|
|
106
|
-
overflow: 'hidden',
|
|
107
|
-
zIndex: 1000,
|
|
108
|
-
boxShadow: '0 4px 16px rgba(0,0,0,0.6)',
|
|
109
|
-
});
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
updatePosition();
|
|
113
|
-
window.addEventListener('resize', updatePosition);
|
|
114
|
-
window.addEventListener('scroll', updatePosition, true);
|
|
115
|
-
|
|
116
|
-
return () => {
|
|
117
|
-
window.removeEventListener('resize', updatePosition);
|
|
118
|
-
window.removeEventListener('scroll', updatePosition, true);
|
|
119
|
-
};
|
|
120
|
-
}, [showPicker]);
|
|
121
|
-
|
|
122
|
-
// Only show 3D preview for server-hosted textures (starting with / or http)
|
|
123
|
-
const canPreview = value && (value.startsWith('/') || value.startsWith('http'));
|
|
124
|
-
|
|
125
|
-
return (
|
|
126
|
-
<div style={{ maxHeight: 128, overflow: 'visible', position: 'relative', display: 'flex', alignItems: 'center' }}>
|
|
127
|
-
{canPreview
|
|
128
|
-
? <SingleTextureViewer file={value} basePath={basePath} />
|
|
129
|
-
: value
|
|
130
|
-
? <span style={{ fontSize: 10, opacity: 0.6, maxWidth: 100, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{value}</span>
|
|
131
|
-
: null
|
|
132
|
-
}
|
|
133
|
-
<button
|
|
134
|
-
ref={triggerRef}
|
|
135
|
-
onClick={() => setShowPicker(!showPicker)}
|
|
136
|
-
style={{ padding: '4px 8px', backgroundColor: colors.bgLight, color: 'inherit', fontSize: 10, cursor: 'pointer', border: `1px solid ${colors.border}`, borderRadius: 3, marginTop: 4 }}
|
|
137
|
-
>
|
|
138
|
-
{showPicker ? 'Cancel' : 'Change'}
|
|
139
|
-
</button>
|
|
140
|
-
<button
|
|
141
|
-
onClick={() => {
|
|
142
|
-
onChange(undefined as any);
|
|
143
|
-
}}
|
|
144
|
-
style={{ padding: '4px 8px', backgroundColor: colors.bgLight, color: 'inherit', fontSize: 10, cursor: 'pointer', border: `1px solid ${colors.border}`, borderRadius: 3, marginTop: 4, marginLeft: 4 }}
|
|
145
|
-
>
|
|
146
|
-
Clear
|
|
147
|
-
</button>
|
|
148
|
-
{showPicker && popupStyle && typeof document !== 'undefined' && createPortal(
|
|
149
|
-
<div style={popupStyle} onMouseLeave={() => setShowPicker(false)}>
|
|
150
|
-
<TextureListViewer
|
|
151
|
-
files={textureFiles}
|
|
152
|
-
selected={value || undefined}
|
|
153
|
-
onSelect={(file) => {
|
|
154
|
-
onChange(file);
|
|
155
|
-
setShowPicker(false);
|
|
156
|
-
}}
|
|
157
|
-
basePath={basePath}
|
|
158
|
-
/>
|
|
159
|
-
</div>,
|
|
160
|
-
document.body
|
|
161
|
-
)}
|
|
162
|
-
</div>
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function MaterialComponentEditor({ component, onUpdate, basePath = "" }: { component: any; onUpdate: (newComp: any) => void; basePath?: string }) {
|
|
167
|
-
const materialType = component.properties.materialType ?? 'standard';
|
|
168
|
-
const hasTexture = !!component.properties.texture;
|
|
169
|
-
const hasRepeat = component.properties.repeat;
|
|
170
|
-
const isStandardMaterial = materialType === 'standard';
|
|
171
|
-
|
|
172
|
-
const fields: FieldDefinition[] = [
|
|
173
|
-
{
|
|
174
|
-
name: 'materialType',
|
|
175
|
-
type: 'select',
|
|
176
|
-
label: 'Material Type',
|
|
177
|
-
options: [
|
|
178
|
-
{ value: 'standard', label: 'Standard' },
|
|
179
|
-
{ value: 'basic', label: 'Basic' },
|
|
180
|
-
],
|
|
181
|
-
},
|
|
182
|
-
{ name: 'color', type: 'color', label: 'Color' },
|
|
183
|
-
{ name: 'toneMapped', type: 'boolean', label: 'Tone Mapped' },
|
|
184
|
-
{ name: 'wireframe', type: 'boolean', label: 'Wireframe' },
|
|
185
|
-
{ name: 'transparent', type: 'boolean', label: 'Transparent' },
|
|
186
|
-
{ name: 'opacity', type: 'number', label: 'Opacity', min: 0, max: 1, step: 0.01 },
|
|
187
|
-
...(isStandardMaterial ? [
|
|
188
|
-
{ name: 'metalness', type: 'number', label: 'Metalness', min: 0, max: 1, step: 0.01 },
|
|
189
|
-
{ name: 'roughness', type: 'number', label: 'Roughness', min: 0, max: 1, step: 0.01 },
|
|
190
|
-
{ name: 'transmission', type: 'number', label: 'Transmission', min: 0, max: 1, step: 0.01 },
|
|
191
|
-
{ name: 'thickness', type: 'number', label: 'Thickness', min: 0, step: 0.1 },
|
|
192
|
-
{ name: 'ior', type: 'number', label: 'IOR (Index of Refraction)', min: 1, max: 2.333, step: 0.01 },
|
|
193
|
-
] as FieldDefinition[] : []),
|
|
194
|
-
{
|
|
195
|
-
name: 'side',
|
|
196
|
-
type: 'select',
|
|
197
|
-
label: 'Side',
|
|
198
|
-
options: [
|
|
199
|
-
{ value: 'FrontSide', label: 'Front' },
|
|
200
|
-
{ value: 'BackSide', label: 'Back' },
|
|
201
|
-
{ value: 'DoubleSide', label: 'Double' },
|
|
202
|
-
],
|
|
203
|
-
} as FieldDefinition,
|
|
204
|
-
{
|
|
205
|
-
name: 'texture',
|
|
206
|
-
type: 'custom',
|
|
207
|
-
label: 'Texture File',
|
|
208
|
-
render: ({ value, onChange }) => (
|
|
209
|
-
<TexturePicker value={value} onChange={onChange} basePath={basePath} />
|
|
210
|
-
),
|
|
211
|
-
},
|
|
212
|
-
// Conditional texture settings
|
|
213
|
-
...(hasTexture ? [
|
|
214
|
-
{ name: 'repeat', type: 'boolean', label: 'Repeat Texture' } as FieldDefinition,
|
|
215
|
-
...(hasRepeat ? [{
|
|
216
|
-
name: 'repeatCount',
|
|
217
|
-
type: 'custom',
|
|
218
|
-
label: 'Repeat (X, Y)',
|
|
219
|
-
render: ({ value, onChange }: { value: [number, number] | undefined; onChange: (v: [number, number]) => void }) => (
|
|
220
|
-
<div style={{ display: 'flex', gap: 2 }}>
|
|
221
|
-
<Input
|
|
222
|
-
label="X"
|
|
223
|
-
value={value?.[0] ?? 1}
|
|
224
|
-
onChange={v => onChange([v, value?.[1] ?? 1])}
|
|
225
|
-
min={0.01}
|
|
226
|
-
max={100}
|
|
227
|
-
step={0.1}
|
|
228
|
-
/>
|
|
229
|
-
<Input
|
|
230
|
-
label="Y"
|
|
231
|
-
value={value?.[1] ?? 1}
|
|
232
|
-
onChange={v => onChange([value?.[0] ?? 1, v])}
|
|
233
|
-
min={0.01}
|
|
234
|
-
max={100}
|
|
235
|
-
step={0.1}
|
|
236
|
-
/>
|
|
237
|
-
</div>
|
|
238
|
-
),
|
|
239
|
-
} as FieldDefinition] : []),
|
|
240
|
-
{
|
|
241
|
-
name: 'normalMapTexture',
|
|
242
|
-
type: 'custom',
|
|
243
|
-
label: 'Normal Map',
|
|
244
|
-
render: ({ value, onChange }) => (
|
|
245
|
-
<TexturePicker value={value} onChange={onChange} basePath={basePath} />
|
|
246
|
-
),
|
|
247
|
-
} as FieldDefinition,
|
|
248
|
-
...(component.properties.normalMapTexture ? [{
|
|
249
|
-
name: 'normalScale',
|
|
250
|
-
type: 'custom',
|
|
251
|
-
label: 'Normal Scale (X, Y)',
|
|
252
|
-
render: ({ value, onChange }: { value: [number, number] | undefined; onChange: (v: [number, number]) => void }) => (
|
|
253
|
-
<div style={{ display: 'flex', gap: 2 }}>
|
|
254
|
-
<Input
|
|
255
|
-
label="X"
|
|
256
|
-
value={value?.[0] ?? 1}
|
|
257
|
-
onChange={v => onChange([v, value?.[1] ?? 1])}
|
|
258
|
-
min={0}
|
|
259
|
-
max={5}
|
|
260
|
-
step={0.01}
|
|
261
|
-
/>
|
|
262
|
-
<Input
|
|
263
|
-
label="Y"
|
|
264
|
-
value={value?.[1] ?? 1}
|
|
265
|
-
onChange={v => onChange([value?.[0] ?? 1, v])}
|
|
266
|
-
min={0}
|
|
267
|
-
max={5}
|
|
268
|
-
step={0.01}
|
|
269
|
-
/>
|
|
270
|
-
</div>
|
|
271
|
-
),
|
|
272
|
-
} as FieldDefinition] : []),
|
|
273
|
-
{ name: 'generateMipmaps', type: 'boolean', label: 'Generate Mipmaps' } as FieldDefinition,
|
|
274
|
-
{
|
|
275
|
-
name: 'minFilter',
|
|
276
|
-
type: 'select',
|
|
277
|
-
label: 'Min Filter',
|
|
278
|
-
options: [
|
|
279
|
-
{ value: 'NearestFilter', label: 'Nearest' },
|
|
280
|
-
{ value: 'NearestMipmapNearestFilter', label: 'Nearest Mipmap Nearest' },
|
|
281
|
-
{ value: 'NearestMipmapLinearFilter', label: 'Nearest Mipmap Linear' },
|
|
282
|
-
{ value: 'LinearFilter', label: 'Linear' },
|
|
283
|
-
{ value: 'LinearMipmapNearestFilter', label: 'Linear Mipmap Nearest' },
|
|
284
|
-
{ value: 'LinearMipmapLinearFilter', label: 'Linear Mipmap Linear (Default)' },
|
|
285
|
-
],
|
|
286
|
-
} as FieldDefinition,
|
|
287
|
-
{
|
|
288
|
-
name: 'magFilter',
|
|
289
|
-
type: 'select',
|
|
290
|
-
label: 'Mag Filter',
|
|
291
|
-
options: [
|
|
292
|
-
{ value: 'NearestFilter', label: 'Nearest' },
|
|
293
|
-
{ value: 'LinearFilter', label: 'Linear (Default)' },
|
|
294
|
-
],
|
|
295
|
-
} as FieldDefinition,
|
|
296
|
-
] : []),
|
|
297
|
-
];
|
|
298
|
-
|
|
299
|
-
return (
|
|
300
|
-
<FieldRenderer
|
|
301
|
-
fields={fields}
|
|
302
|
-
values={component.properties}
|
|
303
|
-
onChange={onUpdate}
|
|
304
|
-
/>
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// View for Material component
|
|
309
|
-
function MaterialComponentView({ properties, loadedTextures }: { properties: MaterialProps, loadedTextures?: Record<string, Texture> }) {
|
|
310
|
-
const materialType = properties?.materialType ?? 'standard';
|
|
311
|
-
const textureName = properties?.texture;
|
|
312
|
-
const repeat = properties?.repeat;
|
|
313
|
-
const repeatCount = properties?.repeatCount;
|
|
314
|
-
const generateMipmaps = properties?.generateMipmaps !== false;
|
|
315
|
-
const minFilter = properties?.minFilter || 'LinearMipmapLinearFilter';
|
|
316
|
-
const magFilter = properties?.magFilter || 'LinearFilter';
|
|
317
|
-
const texture = textureName && loadedTextures ? loadedTextures[textureName] : undefined;
|
|
318
|
-
|
|
319
|
-
const normalMapTextureName = properties?.normalMapTexture;
|
|
320
|
-
const normalScaleProp = properties?.normalScale;
|
|
321
|
-
const normalMapTexture = normalMapTextureName && loadedTextures ? loadedTextures[normalMapTextureName] : undefined;
|
|
322
|
-
const materialSource: MaterialProps = properties ?? {};
|
|
323
|
-
|
|
324
|
-
// Destructure all material props and separate custom texture handling props
|
|
325
|
-
const {
|
|
326
|
-
texture: _texture,
|
|
327
|
-
repeat: _repeat,
|
|
328
|
-
repeatCount: _repeatCount,
|
|
329
|
-
generateMipmaps: _generateMipmaps,
|
|
330
|
-
minFilter: _minFilter,
|
|
331
|
-
magFilter: _magFilter,
|
|
332
|
-
map: _map,
|
|
333
|
-
materialType: _materialType,
|
|
334
|
-
normalMapTexture: _normalMapTexture,
|
|
335
|
-
normalScale: _normalScale,
|
|
336
|
-
normalMap: _normalMap,
|
|
337
|
-
side: sideProp,
|
|
338
|
-
...materialProps
|
|
339
|
-
} = materialSource;
|
|
340
|
-
|
|
341
|
-
const sideMap: Record<string, any> = { FrontSide, BackSide, DoubleSide };
|
|
342
|
-
const resolvedSide = sideProp ? (sideMap[sideProp as unknown as string] ?? FrontSide) : FrontSide;
|
|
343
|
-
|
|
344
|
-
const minFilterMap: Record<string, MinificationTextureFilter> = {
|
|
345
|
-
NearestFilter,
|
|
346
|
-
LinearFilter,
|
|
347
|
-
NearestMipmapNearestFilter,
|
|
348
|
-
NearestMipmapLinearFilter,
|
|
349
|
-
LinearMipmapNearestFilter,
|
|
350
|
-
LinearMipmapLinearFilter
|
|
351
|
-
};
|
|
352
|
-
|
|
353
|
-
const magFilterMap: Record<string, MagnificationTextureFilter> = {
|
|
354
|
-
NearestFilter,
|
|
355
|
-
LinearFilter
|
|
356
|
-
};
|
|
357
|
-
|
|
358
|
-
const finalTexture = useMemo(() => {
|
|
359
|
-
if (!texture) return undefined;
|
|
360
|
-
const t = texture.clone();
|
|
361
|
-
if (repeat) {
|
|
362
|
-
t.wrapS = t.wrapT = RepeatWrapping;
|
|
363
|
-
if (repeatCount) t.repeat.set(repeatCount[0], repeatCount[1]);
|
|
364
|
-
} else {
|
|
365
|
-
t.wrapS = t.wrapT = ClampToEdgeWrapping;
|
|
366
|
-
t.repeat.set(1, 1);
|
|
367
|
-
}
|
|
368
|
-
t.colorSpace = SRGBColorSpace;
|
|
369
|
-
t.generateMipmaps = generateMipmaps;
|
|
370
|
-
t.minFilter = minFilterMap[minFilter] ?? LinearMipmapLinearFilter;
|
|
371
|
-
t.magFilter = magFilterMap[magFilter] ?? LinearFilter;
|
|
372
|
-
t.needsUpdate = true;
|
|
373
|
-
return t;
|
|
374
|
-
}, [texture, repeat, repeatCount?.[0], repeatCount?.[1], generateMipmaps, minFilter, magFilter]);
|
|
375
|
-
|
|
376
|
-
const finalNormalMap = useMemo(() => {
|
|
377
|
-
if (!normalMapTexture) return undefined;
|
|
378
|
-
const t = normalMapTexture.clone();
|
|
379
|
-
t.colorSpace = LinearSRGBColorSpace;
|
|
380
|
-
t.needsUpdate = true;
|
|
381
|
-
return t;
|
|
382
|
-
}, [normalMapTexture]);
|
|
383
|
-
|
|
384
|
-
const normalScaleVec = useMemo(() => {
|
|
385
|
-
if (!finalNormalMap) return undefined;
|
|
386
|
-
return new Vector2(normalScaleProp?.[0] ?? 1, normalScaleProp?.[1] ?? 1);
|
|
387
|
-
}, [finalNormalMap, normalScaleProp?.[0], normalScaleProp?.[1]]);
|
|
388
|
-
|
|
389
|
-
if (!properties) {
|
|
390
|
-
return <meshStandardNodeMaterial color="red" wireframe />;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const materialKey = `${finalTexture?.uuid ?? 'no-texture'}:${materialProps.transparent ? 'transparent' : 'opaque'}`;
|
|
394
|
-
const sharedProps = {
|
|
395
|
-
map: finalTexture,
|
|
396
|
-
side: resolvedSide,
|
|
397
|
-
...materialProps,
|
|
398
|
-
};
|
|
399
|
-
|
|
400
|
-
if (materialType === 'basic') {
|
|
401
|
-
return <meshBasicNodeMaterial key={materialKey} {...sharedProps} />;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
return (
|
|
405
|
-
<meshStandardNodeMaterial
|
|
406
|
-
key={materialKey}
|
|
407
|
-
{...sharedProps}
|
|
408
|
-
normalMap={finalNormalMap}
|
|
409
|
-
normalScale={normalScaleVec}
|
|
410
|
-
/>
|
|
411
|
-
);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
const MaterialComponent: Component = {
|
|
415
|
-
name: 'Material',
|
|
416
|
-
Editor: MaterialComponentEditor,
|
|
417
|
-
View: MaterialComponentView,
|
|
418
|
-
nonComposable: true,
|
|
419
|
-
defaultProperties: {
|
|
420
|
-
materialType: 'standard',
|
|
421
|
-
color: '#ffffff',
|
|
422
|
-
toneMapped: true,
|
|
423
|
-
wireframe: false,
|
|
424
|
-
transparent: false,
|
|
425
|
-
opacity: 1,
|
|
426
|
-
metalness: 0,
|
|
427
|
-
roughness: 1
|
|
428
|
-
}
|
|
429
|
-
};
|
|
430
|
-
|
|
431
|
-
export default MaterialComponent;
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import { ModelListViewer, SingleModelViewer } from '../../assetviewer/page';
|
|
2
|
-
import { useEffect, useLayoutEffect, useState, useMemo, useRef } from 'react';
|
|
3
|
-
import { createPortal } from 'react-dom';
|
|
4
|
-
import { Component } from './ComponentRegistry';
|
|
5
|
-
import { FieldRenderer, FieldDefinition } from './Input';
|
|
6
|
-
import { GameObject } from '../types';
|
|
7
|
-
|
|
8
|
-
const PICKER_POPUP_WIDTH = 260;
|
|
9
|
-
const PICKER_POPUP_HEIGHT = 360;
|
|
10
|
-
|
|
11
|
-
function ModelPicker({
|
|
12
|
-
value,
|
|
13
|
-
onChange,
|
|
14
|
-
basePath,
|
|
15
|
-
nodeId
|
|
16
|
-
}: {
|
|
17
|
-
value: string | undefined;
|
|
18
|
-
onChange: (v: string) => void;
|
|
19
|
-
basePath: string;
|
|
20
|
-
nodeId?: string;
|
|
21
|
-
}) {
|
|
22
|
-
const [modelFiles, setModelFiles] = useState<string[]>([]);
|
|
23
|
-
const [showPicker, setShowPicker] = useState(false);
|
|
24
|
-
const [popupStyle, setPopupStyle] = useState<React.CSSProperties | null>(null);
|
|
25
|
-
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
26
|
-
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
fetch(`${basePath}/models/manifest.json`)
|
|
29
|
-
.then(r => r.json())
|
|
30
|
-
.then(data => setModelFiles(Array.isArray(data) ? data : data.files || []))
|
|
31
|
-
.catch(console.error);
|
|
32
|
-
}, [basePath]);
|
|
33
|
-
|
|
34
|
-
useLayoutEffect(() => {
|
|
35
|
-
if (!showPicker || !triggerRef.current || typeof window === 'undefined') return;
|
|
36
|
-
|
|
37
|
-
const updatePosition = () => {
|
|
38
|
-
const rect = triggerRef.current?.getBoundingClientRect();
|
|
39
|
-
if (!rect) return;
|
|
40
|
-
|
|
41
|
-
const preferredLeft = rect.left - PICKER_POPUP_WIDTH - 8;
|
|
42
|
-
const fallbackLeft = rect.right + 8;
|
|
43
|
-
const fitsLeft = preferredLeft >= 8;
|
|
44
|
-
const left = fitsLeft ? preferredLeft : Math.min(fallbackLeft, window.innerWidth - PICKER_POPUP_WIDTH - 8);
|
|
45
|
-
const top = Math.min(Math.max(8, rect.top), window.innerHeight - PICKER_POPUP_HEIGHT - 8);
|
|
46
|
-
|
|
47
|
-
setPopupStyle({
|
|
48
|
-
position: 'fixed',
|
|
49
|
-
left,
|
|
50
|
-
top,
|
|
51
|
-
background: 'rgba(0,0,0,0.9)',
|
|
52
|
-
padding: 12,
|
|
53
|
-
border: '1px solid rgba(34, 211, 238, 0.3)',
|
|
54
|
-
borderRadius: 6,
|
|
55
|
-
width: PICKER_POPUP_WIDTH,
|
|
56
|
-
height: PICKER_POPUP_HEIGHT,
|
|
57
|
-
overflow: 'hidden',
|
|
58
|
-
zIndex: 1000,
|
|
59
|
-
boxShadow: '0 4px 16px rgba(0,0,0,0.6)',
|
|
60
|
-
});
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
updatePosition();
|
|
64
|
-
window.addEventListener('resize', updatePosition);
|
|
65
|
-
window.addEventListener('scroll', updatePosition, true);
|
|
66
|
-
|
|
67
|
-
return () => {
|
|
68
|
-
window.removeEventListener('resize', updatePosition);
|
|
69
|
-
window.removeEventListener('scroll', updatePosition, true);
|
|
70
|
-
};
|
|
71
|
-
}, [showPicker]);
|
|
72
|
-
|
|
73
|
-
const handleModelSelect = (file: string) => {
|
|
74
|
-
const filename = file.startsWith('/') ? file.slice(1) : file;
|
|
75
|
-
onChange(filename);
|
|
76
|
-
setShowPicker(false);
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
return (
|
|
80
|
-
<div style={{ maxHeight: 128, overflow: 'visible', position: 'relative', display: 'flex', alignItems: 'center' }}>
|
|
81
|
-
<SingleModelViewer file={value ? `/${value}` : undefined} basePath={basePath} />
|
|
82
|
-
<button
|
|
83
|
-
ref={triggerRef}
|
|
84
|
-
onClick={() => setShowPicker(!showPicker)}
|
|
85
|
-
style={{ padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)', marginTop: 4 }}
|
|
86
|
-
>
|
|
87
|
-
{showPicker ? 'Cancel' : 'Change'}
|
|
88
|
-
</button>
|
|
89
|
-
<button
|
|
90
|
-
onClick={() => {
|
|
91
|
-
onChange(undefined as any);
|
|
92
|
-
}}
|
|
93
|
-
style={{ padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)', marginTop: 4, marginLeft: 4 }}
|
|
94
|
-
>
|
|
95
|
-
Clear
|
|
96
|
-
</button>
|
|
97
|
-
{showPicker && popupStyle && typeof document !== 'undefined' && createPortal(
|
|
98
|
-
<div style={popupStyle} onMouseLeave={() => setShowPicker(false)}>
|
|
99
|
-
<ModelListViewer
|
|
100
|
-
key={nodeId}
|
|
101
|
-
files={modelFiles}
|
|
102
|
-
selected={value ? `/${value}` : undefined}
|
|
103
|
-
onSelect={handleModelSelect}
|
|
104
|
-
basePath={basePath}
|
|
105
|
-
/>
|
|
106
|
-
</div>,
|
|
107
|
-
document.body
|
|
108
|
-
)}
|
|
109
|
-
</div>
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function ModelComponentEditor({ component, node, onUpdate, basePath = "" }: { component: any; node?: GameObject; onUpdate: (newComp: any) => void; basePath?: string }) {
|
|
114
|
-
const fields: FieldDefinition[] = [
|
|
115
|
-
{
|
|
116
|
-
name: 'filename',
|
|
117
|
-
type: 'custom',
|
|
118
|
-
label: 'Model File',
|
|
119
|
-
render: ({ value, onChange }) => (
|
|
120
|
-
<ModelPicker
|
|
121
|
-
value={value}
|
|
122
|
-
onChange={onChange}
|
|
123
|
-
basePath={basePath}
|
|
124
|
-
nodeId={node?.id}
|
|
125
|
-
/>
|
|
126
|
-
),
|
|
127
|
-
},
|
|
128
|
-
{ name: 'instanced', type: 'boolean', label: 'Instanced' },
|
|
129
|
-
];
|
|
130
|
-
|
|
131
|
-
return (
|
|
132
|
-
<FieldRenderer
|
|
133
|
-
fields={fields}
|
|
134
|
-
values={component.properties}
|
|
135
|
-
onChange={onUpdate}
|
|
136
|
-
/>
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// View for Model component
|
|
141
|
-
function ModelComponentView({ properties, loadedModels, children }: { properties: any, loadedModels?: Record<string, any>, children?: React.ReactNode }) {
|
|
142
|
-
// Instanced models are handled elsewhere (GameInstance), so only render non-instanced here
|
|
143
|
-
if (!properties.filename || properties.instanced) return <>{children}</>;
|
|
144
|
-
|
|
145
|
-
const sourceModel = loadedModels?.[properties.filename];
|
|
146
|
-
|
|
147
|
-
// Clone model once and set up shadows - memoized to avoid cloning on every render
|
|
148
|
-
const clonedModel = useMemo(() => {
|
|
149
|
-
if (!sourceModel) return null;
|
|
150
|
-
const clone = sourceModel.clone();
|
|
151
|
-
clone.traverse((obj: any) => {
|
|
152
|
-
if (obj.isMesh) {
|
|
153
|
-
obj.castShadow = true;
|
|
154
|
-
obj.receiveShadow = true;
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
return clone;
|
|
158
|
-
}, [sourceModel]);
|
|
159
|
-
|
|
160
|
-
if (!clonedModel) return <>{children}</>;
|
|
161
|
-
|
|
162
|
-
return <primitive object={clonedModel}>{children}</primitive>;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const ModelComponent: Component = {
|
|
166
|
-
name: 'Model',
|
|
167
|
-
Editor: ModelComponentEditor,
|
|
168
|
-
View: ModelComponentView,
|
|
169
|
-
nonComposable: true,
|
|
170
|
-
defaultProperties: {
|
|
171
|
-
filename: '',
|
|
172
|
-
instanced: false
|
|
173
|
-
}
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
export default ModelComponent;
|