react-three-game 0.0.56 → 0.0.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/shared/GameCanvas.js +1 -3
- package/dist/tools/assetviewer/page.js +35 -14
- package/dist/tools/prefabeditor/Dropdown.d.ts +15 -0
- package/dist/tools/prefabeditor/Dropdown.js +82 -0
- package/dist/tools/prefabeditor/EditorContext.d.ts +5 -0
- package/dist/tools/prefabeditor/EditorTree.js +149 -91
- package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +33 -0
- package/dist/tools/prefabeditor/EditorTreeMenus.js +136 -0
- package/dist/tools/prefabeditor/EditorUI.js +1 -1
- package/dist/tools/prefabeditor/PrefabEditor.d.ts +1 -0
- package/dist/tools/prefabeditor/PrefabEditor.js +13 -2
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +1 -0
- package/dist/tools/prefabeditor/PrefabRoot.js +120 -34
- package/dist/tools/prefabeditor/components/AmbientLightComponent.js +3 -7
- package/dist/tools/prefabeditor/components/CameraComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/CameraComponent.js +45 -0
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +50 -24
- package/dist/tools/prefabeditor/components/EnvironmentComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/EnvironmentComponent.js +15 -0
- package/dist/tools/prefabeditor/components/GeometryComponent.js +46 -46
- package/dist/tools/prefabeditor/components/Input.d.ts +51 -1
- package/dist/tools/prefabeditor/components/Input.js +73 -21
- package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +16 -2
- package/dist/tools/prefabeditor/components/MaterialComponent.js +129 -15
- package/dist/tools/prefabeditor/components/ModelComponent.js +44 -3
- package/dist/tools/prefabeditor/components/PhysicsComponent.js +16 -81
- package/dist/tools/prefabeditor/components/SpotLightComponent.js +36 -23
- package/dist/tools/prefabeditor/components/TextComponent.js +7 -53
- package/dist/tools/prefabeditor/components/TransformComponent.js +18 -8
- package/dist/tools/prefabeditor/components/index.js +5 -1
- package/dist/tools/prefabeditor/styles.d.ts +5 -2
- package/dist/tools/prefabeditor/styles.js +7 -3
- package/dist/tools/prefabeditor/utils.d.ts +4 -3
- package/dist/tools/prefabeditor/utils.js +53 -5
- package/package.json +1 -1
- package/react-three-game-skill/react-three-game/SKILL.md +4 -1
- package/src/index.ts +7 -0
- package/src/shared/GameCanvas.tsx +0 -3
- package/src/tools/assetviewer/page.tsx +77 -45
- package/src/tools/prefabeditor/Dropdown.tsx +112 -0
- package/src/tools/prefabeditor/EditorContext.tsx +5 -0
- package/src/tools/prefabeditor/EditorTree.tsx +242 -178
- package/src/tools/prefabeditor/EditorTreeMenus.tsx +307 -0
- package/src/tools/prefabeditor/EditorUI.tsx +1 -1
- package/src/tools/prefabeditor/PrefabEditor.tsx +17 -4
- package/src/tools/prefabeditor/PrefabRoot.tsx +208 -58
- package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +5 -11
- package/src/tools/prefabeditor/components/CameraComponent.tsx +117 -0
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +61 -30
- package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +47 -0
- package/src/tools/prefabeditor/components/GeometryComponent.tsx +69 -63
- package/src/tools/prefabeditor/components/Input.tsx +220 -27
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +189 -18
- package/src/tools/prefabeditor/components/ModelComponent.tsx +51 -4
- package/src/tools/prefabeditor/components/PhysicsComponent.tsx +44 -85
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +52 -27
- package/src/tools/prefabeditor/components/TextComponent.tsx +58 -57
- package/src/tools/prefabeditor/components/TransformComponent.tsx +61 -9
- package/src/tools/prefabeditor/components/index.ts +5 -1
- package/src/tools/prefabeditor/styles.ts +7 -3
- package/src/tools/prefabeditor/utils.ts +55 -4
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { SingleTextureViewer, TextureListViewer } from '../../assetviewer/page';
|
|
2
|
-
import {
|
|
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';
|
|
3
6
|
import { Component } from './ComponentRegistry';
|
|
4
7
|
import { FieldRenderer, FieldDefinition, Input } from './Input';
|
|
5
8
|
import { colors } from '../styles';
|
|
6
9
|
import { useMemo } from 'react';
|
|
10
|
+
import { MeshBasicNodeMaterial, MeshStandardNodeMaterial } from 'three/webgpu';
|
|
7
11
|
import {
|
|
8
12
|
RepeatWrapping,
|
|
9
13
|
ClampToEdgeWrapping,
|
|
10
14
|
SRGBColorSpace,
|
|
15
|
+
LinearSRGBColorSpace,
|
|
11
16
|
Texture,
|
|
17
|
+
Vector2,
|
|
12
18
|
NearestFilter,
|
|
13
19
|
LinearFilter,
|
|
14
20
|
NearestMipmapNearestFilter,
|
|
@@ -17,18 +23,42 @@ import {
|
|
|
17
23
|
LinearMipmapLinearFilter,
|
|
18
24
|
MinificationTextureFilter,
|
|
19
25
|
MagnificationTextureFilter,
|
|
20
|
-
|
|
26
|
+
MeshBasicMaterialProperties,
|
|
27
|
+
MeshStandardMaterialProperties,
|
|
28
|
+
FrontSide,
|
|
29
|
+
BackSide,
|
|
30
|
+
DoubleSide,
|
|
21
31
|
} from 'three';
|
|
22
32
|
|
|
23
|
-
|
|
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;
|
|
24
45
|
texture?: string;
|
|
25
46
|
repeat?: boolean;
|
|
26
47
|
repeatCount?: [number, number];
|
|
27
48
|
generateMipmaps?: boolean;
|
|
28
49
|
minFilter?: string;
|
|
29
50
|
magFilter?: string;
|
|
51
|
+
normalMapTexture?: string;
|
|
52
|
+
normalScale?: [number, number];
|
|
30
53
|
}
|
|
31
54
|
|
|
55
|
+
const PICKER_POPUP_WIDTH = 260;
|
|
56
|
+
const PICKER_POPUP_HEIGHT = 360;
|
|
57
|
+
extend({
|
|
58
|
+
MeshBasicNodeMaterial,
|
|
59
|
+
MeshStandardNodeMaterial,
|
|
60
|
+
});
|
|
61
|
+
|
|
32
62
|
function TexturePicker({
|
|
33
63
|
value,
|
|
34
64
|
onChange,
|
|
@@ -40,6 +70,8 @@ function TexturePicker({
|
|
|
40
70
|
}) {
|
|
41
71
|
const [textureFiles, setTextureFiles] = useState<string[]>([]);
|
|
42
72
|
const [showPicker, setShowPicker] = useState(false);
|
|
73
|
+
const [popupStyle, setPopupStyle] = useState<React.CSSProperties | null>(null);
|
|
74
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
43
75
|
|
|
44
76
|
useEffect(() => {
|
|
45
77
|
fetch(`${basePath}/textures/manifest.json`)
|
|
@@ -48,6 +80,45 @@ function TexturePicker({
|
|
|
48
80
|
.catch(console.error);
|
|
49
81
|
}, [basePath]);
|
|
50
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
|
+
|
|
51
122
|
// Only show 3D preview for server-hosted textures (starting with / or http)
|
|
52
123
|
const canPreview = value && (value.startsWith('/') || value.startsWith('http'));
|
|
53
124
|
|
|
@@ -60,6 +131,7 @@ function TexturePicker({
|
|
|
60
131
|
: null
|
|
61
132
|
}
|
|
62
133
|
<button
|
|
134
|
+
ref={triggerRef}
|
|
63
135
|
onClick={() => setShowPicker(!showPicker)}
|
|
64
136
|
style={{ padding: '4px 8px', backgroundColor: colors.bgLight, color: 'inherit', fontSize: 10, cursor: 'pointer', border: `1px solid ${colors.border}`, borderRadius: 3, marginTop: 4 }}
|
|
65
137
|
>
|
|
@@ -73,8 +145,8 @@ function TexturePicker({
|
|
|
73
145
|
>
|
|
74
146
|
Clear
|
|
75
147
|
</button>
|
|
76
|
-
{showPicker && (
|
|
77
|
-
<div style={{
|
|
148
|
+
{showPicker && popupStyle && typeof document !== 'undefined' && createPortal(
|
|
149
|
+
<div style={popupStyle} onMouseLeave={() => setShowPicker(false)}>
|
|
78
150
|
<TextureListViewer
|
|
79
151
|
files={textureFiles}
|
|
80
152
|
selected={value || undefined}
|
|
@@ -84,26 +156,51 @@ function TexturePicker({
|
|
|
84
156
|
}}
|
|
85
157
|
basePath={basePath}
|
|
86
158
|
/>
|
|
87
|
-
</div
|
|
159
|
+
</div>,
|
|
160
|
+
document.body
|
|
88
161
|
)}
|
|
89
162
|
</div>
|
|
90
163
|
);
|
|
91
164
|
}
|
|
92
165
|
|
|
93
166
|
function MaterialComponentEditor({ component, onUpdate, basePath = "" }: { component: any; onUpdate: (newComp: any) => void; basePath?: string }) {
|
|
167
|
+
const materialType = component.properties.materialType ?? 'standard';
|
|
94
168
|
const hasTexture = !!component.properties.texture;
|
|
95
169
|
const hasRepeat = component.properties.repeat;
|
|
170
|
+
const isStandardMaterial = materialType === 'standard';
|
|
96
171
|
|
|
97
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
|
+
},
|
|
98
182
|
{ name: 'color', type: 'color', label: 'Color' },
|
|
183
|
+
{ name: 'toneMapped', type: 'boolean', label: 'Tone Mapped' },
|
|
99
184
|
{ name: 'wireframe', type: 'boolean', label: 'Wireframe' },
|
|
100
185
|
{ name: 'transparent', type: 'boolean', label: 'Transparent' },
|
|
101
186
|
{ name: 'opacity', type: 'number', label: 'Opacity', min: 0, max: 1, step: 0.01 },
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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,
|
|
107
204
|
{
|
|
108
205
|
name: 'texture',
|
|
109
206
|
type: 'custom',
|
|
@@ -140,6 +237,39 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }: { compo
|
|
|
140
237
|
</div>
|
|
141
238
|
),
|
|
142
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] : []),
|
|
143
273
|
{ name: 'generateMipmaps', type: 'boolean', label: 'Generate Mipmaps' } as FieldDefinition,
|
|
144
274
|
{
|
|
145
275
|
name: 'minFilter',
|
|
@@ -177,6 +307,7 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }: { compo
|
|
|
177
307
|
|
|
178
308
|
// View for Material component
|
|
179
309
|
function MaterialComponentView({ properties, loadedTextures }: { properties: MaterialProps, loadedTextures?: Record<string, Texture> }) {
|
|
310
|
+
const materialType = properties?.materialType ?? 'standard';
|
|
180
311
|
const textureName = properties?.texture;
|
|
181
312
|
const repeat = properties?.repeat;
|
|
182
313
|
const repeatCount = properties?.repeatCount;
|
|
@@ -185,6 +316,11 @@ function MaterialComponentView({ properties, loadedTextures }: { properties: Mat
|
|
|
185
316
|
const magFilter = properties?.magFilter || 'LinearFilter';
|
|
186
317
|
const texture = textureName && loadedTextures ? loadedTextures[textureName] : undefined;
|
|
187
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
|
+
|
|
188
324
|
// Destructure all material props and separate custom texture handling props
|
|
189
325
|
const {
|
|
190
326
|
texture: _texture,
|
|
@@ -193,9 +329,17 @@ function MaterialComponentView({ properties, loadedTextures }: { properties: Mat
|
|
|
193
329
|
generateMipmaps: _generateMipmaps,
|
|
194
330
|
minFilter: _minFilter,
|
|
195
331
|
magFilter: _magFilter,
|
|
196
|
-
map: _map,
|
|
332
|
+
map: _map,
|
|
333
|
+
materialType: _materialType,
|
|
334
|
+
normalMapTexture: _normalMapTexture,
|
|
335
|
+
normalScale: _normalScale,
|
|
336
|
+
normalMap: _normalMap,
|
|
337
|
+
side: sideProp,
|
|
197
338
|
...materialProps
|
|
198
|
-
} =
|
|
339
|
+
} = materialSource;
|
|
340
|
+
|
|
341
|
+
const sideMap: Record<string, any> = { FrontSide, BackSide, DoubleSide };
|
|
342
|
+
const resolvedSide = sideProp ? (sideMap[sideProp as unknown as string] ?? FrontSide) : FrontSide;
|
|
199
343
|
|
|
200
344
|
const minFilterMap: Record<string, MinificationTextureFilter> = {
|
|
201
345
|
NearestFilter,
|
|
@@ -229,15 +373,40 @@ function MaterialComponentView({ properties, loadedTextures }: { properties: Mat
|
|
|
229
373
|
return t;
|
|
230
374
|
}, [texture, repeat, repeatCount?.[0], repeatCount?.[1], generateMipmaps, minFilter, magFilter]);
|
|
231
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
|
+
|
|
232
389
|
if (!properties) {
|
|
233
|
-
return <
|
|
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} />;
|
|
234
402
|
}
|
|
235
403
|
|
|
236
404
|
return (
|
|
237
|
-
<
|
|
238
|
-
key={
|
|
239
|
-
|
|
240
|
-
{
|
|
405
|
+
<meshStandardNodeMaterial
|
|
406
|
+
key={materialKey}
|
|
407
|
+
{...sharedProps}
|
|
408
|
+
normalMap={finalNormalMap}
|
|
409
|
+
normalScale={normalScaleVec}
|
|
241
410
|
/>
|
|
242
411
|
);
|
|
243
412
|
}
|
|
@@ -248,7 +417,9 @@ const MaterialComponent: Component = {
|
|
|
248
417
|
View: MaterialComponentView,
|
|
249
418
|
nonComposable: true,
|
|
250
419
|
defaultProperties: {
|
|
420
|
+
materialType: 'standard',
|
|
251
421
|
color: '#ffffff',
|
|
422
|
+
toneMapped: true,
|
|
252
423
|
wireframe: false,
|
|
253
424
|
transparent: false,
|
|
254
425
|
opacity: 1,
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { ModelListViewer, SingleModelViewer } from '../../assetviewer/page';
|
|
2
|
-
import { useEffect, useState, useMemo } from 'react';
|
|
2
|
+
import { useEffect, useLayoutEffect, useState, useMemo, useRef } from 'react';
|
|
3
|
+
import { createPortal } from 'react-dom';
|
|
3
4
|
import { Component } from './ComponentRegistry';
|
|
4
5
|
import { FieldRenderer, FieldDefinition } from './Input';
|
|
5
6
|
import { GameObject } from '../types';
|
|
6
7
|
|
|
8
|
+
const PICKER_POPUP_WIDTH = 260;
|
|
9
|
+
const PICKER_POPUP_HEIGHT = 360;
|
|
10
|
+
|
|
7
11
|
function ModelPicker({
|
|
8
12
|
value,
|
|
9
13
|
onChange,
|
|
@@ -17,6 +21,8 @@ function ModelPicker({
|
|
|
17
21
|
}) {
|
|
18
22
|
const [modelFiles, setModelFiles] = useState<string[]>([]);
|
|
19
23
|
const [showPicker, setShowPicker] = useState(false);
|
|
24
|
+
const [popupStyle, setPopupStyle] = useState<React.CSSProperties | null>(null);
|
|
25
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
20
26
|
|
|
21
27
|
useEffect(() => {
|
|
22
28
|
fetch(`${basePath}/models/manifest.json`)
|
|
@@ -25,6 +31,45 @@ function ModelPicker({
|
|
|
25
31
|
.catch(console.error);
|
|
26
32
|
}, [basePath]);
|
|
27
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
|
+
|
|
28
73
|
const handleModelSelect = (file: string) => {
|
|
29
74
|
const filename = file.startsWith('/') ? file.slice(1) : file;
|
|
30
75
|
onChange(filename);
|
|
@@ -35,6 +80,7 @@ function ModelPicker({
|
|
|
35
80
|
<div style={{ maxHeight: 128, overflow: 'visible', position: 'relative', display: 'flex', alignItems: 'center' }}>
|
|
36
81
|
<SingleModelViewer file={value ? `/${value}` : undefined} basePath={basePath} />
|
|
37
82
|
<button
|
|
83
|
+
ref={triggerRef}
|
|
38
84
|
onClick={() => setShowPicker(!showPicker)}
|
|
39
85
|
style={{ padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)', marginTop: 4 }}
|
|
40
86
|
>
|
|
@@ -48,8 +94,8 @@ function ModelPicker({
|
|
|
48
94
|
>
|
|
49
95
|
Clear
|
|
50
96
|
</button>
|
|
51
|
-
{showPicker && (
|
|
52
|
-
<div style={{
|
|
97
|
+
{showPicker && popupStyle && typeof document !== 'undefined' && createPortal(
|
|
98
|
+
<div style={popupStyle} onMouseLeave={() => setShowPicker(false)}>
|
|
53
99
|
<ModelListViewer
|
|
54
100
|
key={nodeId}
|
|
55
101
|
files={modelFiles}
|
|
@@ -57,7 +103,8 @@ function ModelPicker({
|
|
|
57
103
|
onSelect={handleModelSelect}
|
|
58
104
|
basePath={basePath}
|
|
59
105
|
/>
|
|
60
|
-
</div
|
|
106
|
+
</div>,
|
|
107
|
+
document.body
|
|
61
108
|
)}
|
|
62
109
|
</div>
|
|
63
110
|
);
|
|
@@ -3,7 +3,7 @@ import type { RigidBodyOptions, CollisionPayload, IntersectionEnterPayload, Inte
|
|
|
3
3
|
import type { ReactNode } from 'react';
|
|
4
4
|
import { useRef, useEffect, useCallback } from 'react';
|
|
5
5
|
import { Component } from "./ComponentRegistry";
|
|
6
|
-
import {
|
|
6
|
+
import { BooleanField, FieldGroup, NumberField, SelectField } from "./Input";
|
|
7
7
|
import { ComponentData } from "../types";
|
|
8
8
|
import { gameEvents, getEntityIdFromRigidBody } from "../GameEvents";
|
|
9
9
|
|
|
@@ -11,92 +11,51 @@ export type PhysicsProps = RigidBodyOptions & {
|
|
|
11
11
|
activeCollisionTypes?: 'all' | undefined;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
const physicsFields: FieldDefinition[] = [
|
|
15
|
-
{
|
|
16
|
-
name: 'type',
|
|
17
|
-
type: 'select',
|
|
18
|
-
label: 'Type',
|
|
19
|
-
options: [
|
|
20
|
-
{ value: 'dynamic', label: 'Dynamic' },
|
|
21
|
-
{ value: 'fixed', label: 'Fixed' },
|
|
22
|
-
{ value: 'kinematicPosition', label: 'Kinematic Position' },
|
|
23
|
-
{ value: 'kinematicVelocity', label: 'Kinematic Velocity' },
|
|
24
|
-
],
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
name: 'colliders',
|
|
28
|
-
type: 'select',
|
|
29
|
-
label: 'Collider',
|
|
30
|
-
options: [
|
|
31
|
-
{ value: 'hull', label: 'Hull (convex)' },
|
|
32
|
-
{ value: 'trimesh', label: 'Trimesh (exact)' },
|
|
33
|
-
{ value: 'cuboid', label: 'Cuboid (box)' },
|
|
34
|
-
{ value: 'ball', label: 'Ball (sphere)' },
|
|
35
|
-
],
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
name: 'mass',
|
|
39
|
-
type: 'number',
|
|
40
|
-
label: 'Mass',
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
name: 'restitution',
|
|
44
|
-
type: 'number',
|
|
45
|
-
label: 'Restitution (Bounciness)',
|
|
46
|
-
min: 0,
|
|
47
|
-
max: 1,
|
|
48
|
-
step: 0.1,
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
name: 'friction',
|
|
52
|
-
type: 'number',
|
|
53
|
-
label: 'Friction',
|
|
54
|
-
min: 0,
|
|
55
|
-
step: 0.1,
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
name: 'linearDamping',
|
|
59
|
-
type: 'number',
|
|
60
|
-
label: 'Linear Damping',
|
|
61
|
-
min: 0,
|
|
62
|
-
step: 0.1,
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
name: 'angularDamping',
|
|
66
|
-
type: 'number',
|
|
67
|
-
label: 'Angular Damping',
|
|
68
|
-
min: 0,
|
|
69
|
-
step: 0.1,
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
name: 'gravityScale',
|
|
73
|
-
type: 'number',
|
|
74
|
-
label: 'Gravity Scale',
|
|
75
|
-
step: 0.1,
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
name: 'sensor',
|
|
79
|
-
type: 'boolean',
|
|
80
|
-
label: 'Sensor (Trigger Only)',
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
name: 'activeCollisionTypes',
|
|
84
|
-
type: 'select',
|
|
85
|
-
label: 'Collision Detection',
|
|
86
|
-
options: [
|
|
87
|
-
{ value: '', label: 'Default (Dynamic only)' },
|
|
88
|
-
{ value: 'all', label: 'All (includes kinematic & fixed)' },
|
|
89
|
-
],
|
|
90
|
-
},
|
|
91
|
-
];
|
|
92
|
-
|
|
93
14
|
function PhysicsComponentEditor({ component, onUpdate }: { component: ComponentData; onUpdate: (newComp: any) => void }) {
|
|
94
15
|
return (
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
16
|
+
<FieldGroup>
|
|
17
|
+
<SelectField
|
|
18
|
+
name="type"
|
|
19
|
+
label="Type"
|
|
20
|
+
values={component.properties}
|
|
21
|
+
onChange={onUpdate}
|
|
22
|
+
options={[
|
|
23
|
+
{ value: 'dynamic', label: 'Dynamic' },
|
|
24
|
+
{ value: 'fixed', label: 'Fixed' },
|
|
25
|
+
{ value: 'kinematicPosition', label: 'Kinematic Position' },
|
|
26
|
+
{ value: 'kinematicVelocity', label: 'Kinematic Velocity' },
|
|
27
|
+
]}
|
|
28
|
+
/>
|
|
29
|
+
<SelectField
|
|
30
|
+
name="colliders"
|
|
31
|
+
label="Collider"
|
|
32
|
+
values={component.properties}
|
|
33
|
+
onChange={onUpdate}
|
|
34
|
+
options={[
|
|
35
|
+
{ value: 'hull', label: 'Hull (convex)' },
|
|
36
|
+
{ value: 'trimesh', label: 'Trimesh (exact)' },
|
|
37
|
+
{ value: 'cuboid', label: 'Cuboid (box)' },
|
|
38
|
+
{ value: 'ball', label: 'Ball (sphere)' },
|
|
39
|
+
]}
|
|
40
|
+
/>
|
|
41
|
+
<NumberField name="mass" label="Mass" values={component.properties} onChange={onUpdate} fallback={1} step={0.1} min={0} />
|
|
42
|
+
<NumberField name="restitution" label="Restitution (Bounciness)" values={component.properties} onChange={onUpdate} fallback={0} min={0} max={1} step={0.1} />
|
|
43
|
+
<NumberField name="friction" label="Friction" values={component.properties} onChange={onUpdate} fallback={0.5} min={0} step={0.1} />
|
|
44
|
+
<NumberField name="linearDamping" label="Linear Damping" values={component.properties} onChange={onUpdate} fallback={0} min={0} step={0.1} />
|
|
45
|
+
<NumberField name="angularDamping" label="Angular Damping" values={component.properties} onChange={onUpdate} fallback={0} min={0} step={0.1} />
|
|
46
|
+
<NumberField name="gravityScale" label="Gravity Scale" values={component.properties} onChange={onUpdate} fallback={1} step={0.1} />
|
|
47
|
+
<BooleanField name="sensor" label="Sensor (Trigger Only)" values={component.properties} onChange={onUpdate} fallback={false} />
|
|
48
|
+
<SelectField
|
|
49
|
+
name="activeCollisionTypes"
|
|
50
|
+
label="Collision Detection"
|
|
51
|
+
values={component.properties}
|
|
52
|
+
onChange={onUpdate}
|
|
53
|
+
options={[
|
|
54
|
+
{ value: '', label: 'Default (Dynamic only)' },
|
|
55
|
+
{ value: 'all', label: 'All (includes kinematic & fixed)' },
|
|
56
|
+
]}
|
|
57
|
+
/>
|
|
58
|
+
</FieldGroup>
|
|
100
59
|
);
|
|
101
60
|
}
|
|
102
61
|
|