react-three-game 0.0.55 → 0.0.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/shared/ContactShadow.d.ts +8 -0
- package/dist/shared/ContactShadow.js +32 -0
- package/dist/shared/GameCanvas.js +1 -3
- package/dist/tools/assetviewer/page.js +36 -15
- package/dist/tools/dragdrop/DragDropLoader.js +17 -40
- package/dist/tools/dragdrop/modelLoader.d.ts +5 -0
- package/dist/tools/dragdrop/modelLoader.js +39 -0
- package/dist/tools/prefabeditor/Dropdown.d.ts +15 -0
- package/dist/tools/prefabeditor/Dropdown.js +82 -0
- package/dist/tools/prefabeditor/EditorContext.d.ts +5 -0
- package/dist/tools/prefabeditor/EditorTree.js +139 -70
- package/dist/tools/prefabeditor/EditorUI.js +5 -10
- package/dist/tools/prefabeditor/PrefabEditor.d.ts +1 -0
- package/dist/tools/prefabeditor/PrefabEditor.js +70 -3
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +3 -0
- package/dist/tools/prefabeditor/PrefabRoot.js +136 -35
- package/dist/tools/prefabeditor/components/AmbientLightComponent.js +3 -7
- package/dist/tools/prefabeditor/components/CameraComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/CameraComponent.js +25 -0
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +2 -2
- package/dist/tools/prefabeditor/components/EnvironmentComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/EnvironmentComponent.js +15 -0
- package/dist/tools/prefabeditor/components/GeometryComponent.js +46 -46
- package/dist/tools/prefabeditor/components/Input.d.ts +51 -1
- package/dist/tools/prefabeditor/components/Input.js +100 -47
- package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +8 -2
- package/dist/tools/prefabeditor/components/MaterialComponent.js +129 -14
- package/dist/tools/prefabeditor/components/ModelComponent.js +44 -3
- package/dist/tools/prefabeditor/components/PhysicsComponent.js +16 -81
- package/dist/tools/prefabeditor/components/SpotLightComponent.js +6 -11
- package/dist/tools/prefabeditor/components/TextComponent.js +7 -53
- package/dist/tools/prefabeditor/components/TransformComponent.js +31 -19
- package/dist/tools/prefabeditor/components/index.js +5 -1
- package/dist/tools/prefabeditor/styles.d.ts +17 -4
- package/dist/tools/prefabeditor/styles.js +69 -32
- package/dist/tools/prefabeditor/utils.d.ts +8 -3
- package/dist/tools/prefabeditor/utils.js +92 -6
- package/package.json +1 -1
- package/react-three-game-skill/react-three-game/rules/LIGHTING.md +6 -0
- package/src/index.ts +7 -0
- package/src/shared/ContactShadow.tsx +74 -0
- package/src/shared/GameCanvas.tsx +0 -3
- package/src/tools/assetviewer/page.tsx +78 -46
- package/src/tools/dragdrop/DragDropLoader.tsx +7 -39
- package/src/tools/dragdrop/modelLoader.ts +36 -0
- package/src/tools/prefabeditor/Dropdown.tsx +112 -0
- package/src/tools/prefabeditor/EditorContext.tsx +5 -0
- package/src/tools/prefabeditor/EditorTree.tsx +237 -115
- package/src/tools/prefabeditor/EditorUI.tsx +6 -11
- package/src/tools/prefabeditor/PrefabEditor.tsx +77 -5
- package/src/tools/prefabeditor/PrefabRoot.tsx +228 -59
- package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +5 -11
- package/src/tools/prefabeditor/components/CameraComponent.tsx +80 -0
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +2 -2
- package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +47 -0
- package/src/tools/prefabeditor/components/GeometryComponent.tsx +69 -63
- package/src/tools/prefabeditor/components/Input.tsx +247 -53
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +191 -20
- package/src/tools/prefabeditor/components/ModelComponent.tsx +52 -5
- package/src/tools/prefabeditor/components/PhysicsComponent.tsx +44 -85
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +14 -16
- package/src/tools/prefabeditor/components/TextComponent.tsx +58 -57
- package/src/tools/prefabeditor/components/TransformComponent.tsx +78 -20
- package/src/tools/prefabeditor/components/index.ts +5 -1
- package/src/tools/prefabeditor/styles.ts +71 -32
- package/src/tools/prefabeditor/utils.ts +96 -5
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { SingleTextureViewer, TextureListViewer } from '../../assetviewer/page';
|
|
2
|
-
import { useEffect, useState } from 'react';
|
|
2
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
+
import { createPortal } from 'react-dom';
|
|
3
4
|
import { Component } from './ComponentRegistry';
|
|
4
5
|
import { FieldRenderer, FieldDefinition, Input } from './Input';
|
|
6
|
+
import { colors } from '../styles';
|
|
5
7
|
import { useMemo } from 'react';
|
|
6
8
|
import {
|
|
7
9
|
RepeatWrapping,
|
|
8
10
|
ClampToEdgeWrapping,
|
|
9
11
|
SRGBColorSpace,
|
|
12
|
+
LinearSRGBColorSpace,
|
|
10
13
|
Texture,
|
|
14
|
+
Vector2,
|
|
11
15
|
NearestFilter,
|
|
12
16
|
LinearFilter,
|
|
13
17
|
NearestMipmapNearestFilter,
|
|
@@ -16,18 +20,31 @@ import {
|
|
|
16
20
|
LinearMipmapLinearFilter,
|
|
17
21
|
MinificationTextureFilter,
|
|
18
22
|
MagnificationTextureFilter,
|
|
19
|
-
|
|
23
|
+
MeshBasicMaterialProperties,
|
|
24
|
+
MeshStandardMaterialProperties,
|
|
25
|
+
FrontSide,
|
|
26
|
+
BackSide,
|
|
27
|
+
DoubleSide,
|
|
20
28
|
} from 'three';
|
|
21
29
|
|
|
22
|
-
export interface MaterialProps extends Omit<MeshStandardMaterialProperties, 'args'> {
|
|
30
|
+
export interface MaterialProps extends Omit<MeshStandardMaterialProperties & MeshBasicMaterialProperties, 'args' | 'normalScale'> {
|
|
31
|
+
materialType?: 'standard' | 'basic';
|
|
32
|
+
transmission?: number;
|
|
33
|
+
thickness?: number;
|
|
34
|
+
ior?: number;
|
|
23
35
|
texture?: string;
|
|
24
36
|
repeat?: boolean;
|
|
25
37
|
repeatCount?: [number, number];
|
|
26
38
|
generateMipmaps?: boolean;
|
|
27
39
|
minFilter?: string;
|
|
28
40
|
magFilter?: string;
|
|
41
|
+
normalMapTexture?: string;
|
|
42
|
+
normalScale?: [number, number];
|
|
29
43
|
}
|
|
30
44
|
|
|
45
|
+
const PICKER_POPUP_WIDTH = 260;
|
|
46
|
+
const PICKER_POPUP_HEIGHT = 360;
|
|
47
|
+
|
|
31
48
|
function TexturePicker({
|
|
32
49
|
value,
|
|
33
50
|
onChange,
|
|
@@ -39,6 +56,8 @@ function TexturePicker({
|
|
|
39
56
|
}) {
|
|
40
57
|
const [textureFiles, setTextureFiles] = useState<string[]>([]);
|
|
41
58
|
const [showPicker, setShowPicker] = useState(false);
|
|
59
|
+
const [popupStyle, setPopupStyle] = useState<React.CSSProperties | null>(null);
|
|
60
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
42
61
|
|
|
43
62
|
useEffect(() => {
|
|
44
63
|
fetch(`${basePath}/textures/manifest.json`)
|
|
@@ -47,12 +66,60 @@ function TexturePicker({
|
|
|
47
66
|
.catch(console.error);
|
|
48
67
|
}, [basePath]);
|
|
49
68
|
|
|
69
|
+
useLayoutEffect(() => {
|
|
70
|
+
if (!showPicker || !triggerRef.current || typeof window === 'undefined') return;
|
|
71
|
+
|
|
72
|
+
const updatePosition = () => {
|
|
73
|
+
const rect = triggerRef.current?.getBoundingClientRect();
|
|
74
|
+
if (!rect) return;
|
|
75
|
+
|
|
76
|
+
const preferredLeft = rect.left - PICKER_POPUP_WIDTH - 8;
|
|
77
|
+
const fallbackLeft = rect.right + 8;
|
|
78
|
+
const fitsLeft = preferredLeft >= 8;
|
|
79
|
+
const left = fitsLeft ? preferredLeft : Math.min(fallbackLeft, window.innerWidth - PICKER_POPUP_WIDTH - 8);
|
|
80
|
+
const top = Math.min(Math.max(8, rect.top), window.innerHeight - PICKER_POPUP_HEIGHT - 8);
|
|
81
|
+
|
|
82
|
+
setPopupStyle({
|
|
83
|
+
position: 'fixed',
|
|
84
|
+
left,
|
|
85
|
+
top,
|
|
86
|
+
background: colors.bg,
|
|
87
|
+
padding: 12,
|
|
88
|
+
border: `1px solid ${colors.border}`,
|
|
89
|
+
borderRadius: 6,
|
|
90
|
+
width: PICKER_POPUP_WIDTH,
|
|
91
|
+
height: PICKER_POPUP_HEIGHT,
|
|
92
|
+
overflow: 'hidden',
|
|
93
|
+
zIndex: 1000,
|
|
94
|
+
boxShadow: '0 4px 16px rgba(0,0,0,0.6)',
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
updatePosition();
|
|
99
|
+
window.addEventListener('resize', updatePosition);
|
|
100
|
+
window.addEventListener('scroll', updatePosition, true);
|
|
101
|
+
|
|
102
|
+
return () => {
|
|
103
|
+
window.removeEventListener('resize', updatePosition);
|
|
104
|
+
window.removeEventListener('scroll', updatePosition, true);
|
|
105
|
+
};
|
|
106
|
+
}, [showPicker]);
|
|
107
|
+
|
|
108
|
+
// Only show 3D preview for server-hosted textures (starting with / or http)
|
|
109
|
+
const canPreview = value && (value.startsWith('/') || value.startsWith('http'));
|
|
110
|
+
|
|
50
111
|
return (
|
|
51
|
-
<div style={{ maxHeight: 128,
|
|
52
|
-
|
|
112
|
+
<div style={{ maxHeight: 128, overflow: 'visible', position: 'relative', display: 'flex', alignItems: 'center' }}>
|
|
113
|
+
{canPreview
|
|
114
|
+
? <SingleTextureViewer file={value} basePath={basePath} />
|
|
115
|
+
: value
|
|
116
|
+
? <span style={{ fontSize: 10, opacity: 0.6, maxWidth: 100, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{value}</span>
|
|
117
|
+
: null
|
|
118
|
+
}
|
|
53
119
|
<button
|
|
120
|
+
ref={triggerRef}
|
|
54
121
|
onClick={() => setShowPicker(!showPicker)}
|
|
55
|
-
style={{ padding: '4px 8px', backgroundColor:
|
|
122
|
+
style={{ padding: '4px 8px', backgroundColor: colors.bgLight, color: 'inherit', fontSize: 10, cursor: 'pointer', border: `1px solid ${colors.border}`, borderRadius: 3, marginTop: 4 }}
|
|
56
123
|
>
|
|
57
124
|
{showPicker ? 'Cancel' : 'Change'}
|
|
58
125
|
</button>
|
|
@@ -60,12 +127,12 @@ function TexturePicker({
|
|
|
60
127
|
onClick={() => {
|
|
61
128
|
onChange(undefined as any);
|
|
62
129
|
}}
|
|
63
|
-
style={{ padding: '4px 8px', backgroundColor:
|
|
130
|
+
style={{ padding: '4px 8px', backgroundColor: colors.bgLight, color: 'inherit', fontSize: 10, cursor: 'pointer', border: `1px solid ${colors.border}`, borderRadius: 3, marginTop: 4, marginLeft: 4 }}
|
|
64
131
|
>
|
|
65
132
|
Clear
|
|
66
133
|
</button>
|
|
67
|
-
{showPicker && (
|
|
68
|
-
<div style={{
|
|
134
|
+
{showPicker && popupStyle && typeof document !== 'undefined' && createPortal(
|
|
135
|
+
<div style={popupStyle} onMouseLeave={() => setShowPicker(false)}>
|
|
69
136
|
<TextureListViewer
|
|
70
137
|
files={textureFiles}
|
|
71
138
|
selected={value || undefined}
|
|
@@ -75,26 +142,51 @@ function TexturePicker({
|
|
|
75
142
|
}}
|
|
76
143
|
basePath={basePath}
|
|
77
144
|
/>
|
|
78
|
-
</div
|
|
145
|
+
</div>,
|
|
146
|
+
document.body
|
|
79
147
|
)}
|
|
80
148
|
</div>
|
|
81
149
|
);
|
|
82
150
|
}
|
|
83
151
|
|
|
84
152
|
function MaterialComponentEditor({ component, onUpdate, basePath = "" }: { component: any; onUpdate: (newComp: any) => void; basePath?: string }) {
|
|
153
|
+
const materialType = component.properties.materialType ?? 'standard';
|
|
85
154
|
const hasTexture = !!component.properties.texture;
|
|
86
155
|
const hasRepeat = component.properties.repeat;
|
|
156
|
+
const isStandardMaterial = materialType === 'standard';
|
|
87
157
|
|
|
88
158
|
const fields: FieldDefinition[] = [
|
|
159
|
+
{
|
|
160
|
+
name: 'materialType',
|
|
161
|
+
type: 'select',
|
|
162
|
+
label: 'Material Type',
|
|
163
|
+
options: [
|
|
164
|
+
{ value: 'standard', label: 'Standard' },
|
|
165
|
+
{ value: 'basic', label: 'Basic' },
|
|
166
|
+
],
|
|
167
|
+
},
|
|
89
168
|
{ name: 'color', type: 'color', label: 'Color' },
|
|
169
|
+
{ name: 'toneMapped', type: 'boolean', label: 'Tone Mapped' },
|
|
90
170
|
{ name: 'wireframe', type: 'boolean', label: 'Wireframe' },
|
|
91
171
|
{ name: 'transparent', type: 'boolean', label: 'Transparent' },
|
|
92
172
|
{ name: 'opacity', type: 'number', label: 'Opacity', min: 0, max: 1, step: 0.01 },
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
173
|
+
...(isStandardMaterial ? [
|
|
174
|
+
{ name: 'metalness', type: 'number', label: 'Metalness', min: 0, max: 1, step: 0.01 },
|
|
175
|
+
{ name: 'roughness', type: 'number', label: 'Roughness', min: 0, max: 1, step: 0.01 },
|
|
176
|
+
{ name: 'transmission', type: 'number', label: 'Transmission', min: 0, max: 1, step: 0.01 },
|
|
177
|
+
{ name: 'thickness', type: 'number', label: 'Thickness', min: 0, step: 0.1 },
|
|
178
|
+
{ name: 'ior', type: 'number', label: 'IOR (Index of Refraction)', min: 1, max: 2.333, step: 0.01 },
|
|
179
|
+
] as FieldDefinition[] : []),
|
|
180
|
+
{
|
|
181
|
+
name: 'side',
|
|
182
|
+
type: 'select',
|
|
183
|
+
label: 'Side',
|
|
184
|
+
options: [
|
|
185
|
+
{ value: 'FrontSide', label: 'Front' },
|
|
186
|
+
{ value: 'BackSide', label: 'Back' },
|
|
187
|
+
{ value: 'DoubleSide', label: 'Double' },
|
|
188
|
+
],
|
|
189
|
+
} as FieldDefinition,
|
|
98
190
|
{
|
|
99
191
|
name: 'texture',
|
|
100
192
|
type: 'custom',
|
|
@@ -131,6 +223,39 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }: { compo
|
|
|
131
223
|
</div>
|
|
132
224
|
),
|
|
133
225
|
} as FieldDefinition] : []),
|
|
226
|
+
{
|
|
227
|
+
name: 'normalMapTexture',
|
|
228
|
+
type: 'custom',
|
|
229
|
+
label: 'Normal Map',
|
|
230
|
+
render: ({ value, onChange }) => (
|
|
231
|
+
<TexturePicker value={value} onChange={onChange} basePath={basePath} />
|
|
232
|
+
),
|
|
233
|
+
} as FieldDefinition,
|
|
234
|
+
...(component.properties.normalMapTexture ? [{
|
|
235
|
+
name: 'normalScale',
|
|
236
|
+
type: 'custom',
|
|
237
|
+
label: 'Normal Scale (X, Y)',
|
|
238
|
+
render: ({ value, onChange }: { value: [number, number] | undefined; onChange: (v: [number, number]) => void }) => (
|
|
239
|
+
<div style={{ display: 'flex', gap: 2 }}>
|
|
240
|
+
<Input
|
|
241
|
+
label="X"
|
|
242
|
+
value={value?.[0] ?? 1}
|
|
243
|
+
onChange={v => onChange([v, value?.[1] ?? 1])}
|
|
244
|
+
min={0}
|
|
245
|
+
max={5}
|
|
246
|
+
step={0.01}
|
|
247
|
+
/>
|
|
248
|
+
<Input
|
|
249
|
+
label="Y"
|
|
250
|
+
value={value?.[1] ?? 1}
|
|
251
|
+
onChange={v => onChange([value?.[0] ?? 1, v])}
|
|
252
|
+
min={0}
|
|
253
|
+
max={5}
|
|
254
|
+
step={0.01}
|
|
255
|
+
/>
|
|
256
|
+
</div>
|
|
257
|
+
),
|
|
258
|
+
} as FieldDefinition] : []),
|
|
134
259
|
{ name: 'generateMipmaps', type: 'boolean', label: 'Generate Mipmaps' } as FieldDefinition,
|
|
135
260
|
{
|
|
136
261
|
name: 'minFilter',
|
|
@@ -168,6 +293,7 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }: { compo
|
|
|
168
293
|
|
|
169
294
|
// View for Material component
|
|
170
295
|
function MaterialComponentView({ properties, loadedTextures }: { properties: MaterialProps, loadedTextures?: Record<string, Texture> }) {
|
|
296
|
+
const materialType = properties?.materialType ?? 'standard';
|
|
171
297
|
const textureName = properties?.texture;
|
|
172
298
|
const repeat = properties?.repeat;
|
|
173
299
|
const repeatCount = properties?.repeatCount;
|
|
@@ -176,6 +302,11 @@ function MaterialComponentView({ properties, loadedTextures }: { properties: Mat
|
|
|
176
302
|
const magFilter = properties?.magFilter || 'LinearFilter';
|
|
177
303
|
const texture = textureName && loadedTextures ? loadedTextures[textureName] : undefined;
|
|
178
304
|
|
|
305
|
+
const normalMapTextureName = properties?.normalMapTexture;
|
|
306
|
+
const normalScaleProp = properties?.normalScale;
|
|
307
|
+
const normalMapTexture = normalMapTextureName && loadedTextures ? loadedTextures[normalMapTextureName] : undefined;
|
|
308
|
+
const materialSource: MaterialProps = properties ?? {};
|
|
309
|
+
|
|
179
310
|
// Destructure all material props and separate custom texture handling props
|
|
180
311
|
const {
|
|
181
312
|
texture: _texture,
|
|
@@ -184,9 +315,22 @@ function MaterialComponentView({ properties, loadedTextures }: { properties: Mat
|
|
|
184
315
|
generateMipmaps: _generateMipmaps,
|
|
185
316
|
minFilter: _minFilter,
|
|
186
317
|
magFilter: _magFilter,
|
|
187
|
-
map: _map,
|
|
318
|
+
map: _map,
|
|
319
|
+
materialType: _materialType,
|
|
320
|
+
normalMapTexture: _normalMapTexture,
|
|
321
|
+
normalScale: _normalScale,
|
|
322
|
+
normalMap: _normalMap,
|
|
323
|
+
side: sideProp,
|
|
324
|
+
metalness: _metalness,
|
|
325
|
+
roughness: _roughness,
|
|
326
|
+
transmission: _transmission,
|
|
327
|
+
thickness: _thickness,
|
|
328
|
+
ior: _ior,
|
|
188
329
|
...materialProps
|
|
189
|
-
} =
|
|
330
|
+
} = materialSource;
|
|
331
|
+
|
|
332
|
+
const sideMap: Record<string, any> = { FrontSide, BackSide, DoubleSide };
|
|
333
|
+
const resolvedSide = sideProp ? (sideMap[sideProp as unknown as string] ?? FrontSide) : FrontSide;
|
|
190
334
|
|
|
191
335
|
const minFilterMap: Record<string, MinificationTextureFilter> = {
|
|
192
336
|
NearestFilter,
|
|
@@ -220,15 +364,40 @@ function MaterialComponentView({ properties, loadedTextures }: { properties: Mat
|
|
|
220
364
|
return t;
|
|
221
365
|
}, [texture, repeat, repeatCount?.[0], repeatCount?.[1], generateMipmaps, minFilter, magFilter]);
|
|
222
366
|
|
|
367
|
+
const finalNormalMap = useMemo(() => {
|
|
368
|
+
if (!normalMapTexture) return undefined;
|
|
369
|
+
const t = normalMapTexture.clone();
|
|
370
|
+
t.colorSpace = LinearSRGBColorSpace;
|
|
371
|
+
t.needsUpdate = true;
|
|
372
|
+
return t;
|
|
373
|
+
}, [normalMapTexture]);
|
|
374
|
+
|
|
375
|
+
const normalScaleVec = useMemo(() => {
|
|
376
|
+
if (!finalNormalMap) return undefined;
|
|
377
|
+
return new Vector2(normalScaleProp?.[0] ?? 1, normalScaleProp?.[1] ?? 1);
|
|
378
|
+
}, [finalNormalMap, normalScaleProp?.[0], normalScaleProp?.[1]]);
|
|
379
|
+
|
|
223
380
|
if (!properties) {
|
|
224
381
|
return <meshStandardMaterial color="red" wireframe />;
|
|
225
382
|
}
|
|
226
383
|
|
|
384
|
+
const materialKey = finalTexture?.uuid ?? 'no-texture';
|
|
385
|
+
const sharedProps = {
|
|
386
|
+
map: finalTexture,
|
|
387
|
+
side: resolvedSide,
|
|
388
|
+
...materialProps,
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
if (materialType === 'basic') {
|
|
392
|
+
return <meshBasicMaterial key={materialKey} {...sharedProps} />;
|
|
393
|
+
}
|
|
394
|
+
|
|
227
395
|
return (
|
|
228
396
|
<meshStandardMaterial
|
|
229
|
-
key={
|
|
230
|
-
|
|
231
|
-
{
|
|
397
|
+
key={materialKey}
|
|
398
|
+
{...sharedProps}
|
|
399
|
+
normalMap={finalNormalMap}
|
|
400
|
+
normalScale={normalScaleVec}
|
|
232
401
|
/>
|
|
233
402
|
);
|
|
234
403
|
}
|
|
@@ -239,7 +408,9 @@ const MaterialComponent: Component = {
|
|
|
239
408
|
View: MaterialComponentView,
|
|
240
409
|
nonComposable: true,
|
|
241
410
|
defaultProperties: {
|
|
411
|
+
materialType: 'standard',
|
|
242
412
|
color: '#ffffff',
|
|
413
|
+
toneMapped: true,
|
|
243
414
|
wireframe: false,
|
|
244
415
|
transparent: false,
|
|
245
416
|
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);
|
|
@@ -32,9 +77,10 @@ function ModelPicker({
|
|
|
32
77
|
};
|
|
33
78
|
|
|
34
79
|
return (
|
|
35
|
-
<div style={{ maxHeight: 128,
|
|
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
|
|
|
@@ -1,27 +1,23 @@
|
|
|
1
1
|
import { Component } from "./ComponentRegistry";
|
|
2
2
|
import { useRef, useEffect } from "react";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
{ name: 'color', type: 'color', label: 'Color' },
|
|
7
|
-
{ name: 'intensity', type: 'number', label: 'Intensity', step: 0.1, min: 0 },
|
|
8
|
-
{ name: 'angle', type: 'number', label: 'Angle', step: 0.1, min: 0, max: Math.PI },
|
|
9
|
-
{ name: 'penumbra', type: 'number', label: 'Penumbra', step: 0.1, min: 0, max: 1 },
|
|
10
|
-
{ name: 'distance', type: 'number', label: 'Distance', step: 1, min: 0 },
|
|
11
|
-
{ name: 'castShadow', type: 'boolean', label: 'Cast Shadow' },
|
|
12
|
-
];
|
|
3
|
+
import { BooleanField, ColorField, FieldGroup, NumberField } from "./Input";
|
|
4
|
+
import { useHelper } from "@react-three/drei";
|
|
5
|
+
import { SpotLightHelper } from "three";
|
|
13
6
|
|
|
14
7
|
function SpotLightComponentEditor({ component, onUpdate }: { component: any; onUpdate: (newComp: any) => void }) {
|
|
15
8
|
return (
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
values={component.properties}
|
|
19
|
-
onChange={onUpdate}
|
|
20
|
-
|
|
9
|
+
<FieldGroup>
|
|
10
|
+
<ColorField name="color" label="Color" values={component.properties} onChange={onUpdate} />
|
|
11
|
+
<NumberField name="intensity" label="Intensity" values={component.properties} onChange={onUpdate} min={0} step={0.1} fallback={1} />
|
|
12
|
+
<NumberField name="angle" label="Angle" values={component.properties} onChange={onUpdate} min={0} max={Math.PI} step={0.05} fallback={Math.PI / 6} />
|
|
13
|
+
<NumberField name="penumbra" label="Penumbra" values={component.properties} onChange={onUpdate} min={0} max={1} step={0.05} fallback={0.5} />
|
|
14
|
+
<NumberField name="distance" label="Distance" values={component.properties} onChange={onUpdate} min={0} step={1} fallback={100} />
|
|
15
|
+
<BooleanField name="castShadow" label="Cast Shadow" values={component.properties} onChange={onUpdate} fallback={true} />
|
|
16
|
+
</FieldGroup>
|
|
21
17
|
);
|
|
22
18
|
}
|
|
23
19
|
|
|
24
|
-
function SpotLightView({ properties, editMode }: { properties: any; editMode?: boolean }) {
|
|
20
|
+
function SpotLightView({ properties, editMode, isSelected }: { properties: any; editMode?: boolean; isSelected?: boolean }) {
|
|
25
21
|
const color = properties.color ?? '#ffffff';
|
|
26
22
|
const intensity = properties.intensity ?? 1.0;
|
|
27
23
|
const angle = properties.angle ?? Math.PI / 6;
|
|
@@ -32,6 +28,8 @@ function SpotLightView({ properties, editMode }: { properties: any; editMode?: b
|
|
|
32
28
|
const spotLightRef = useRef<any>(null);
|
|
33
29
|
const targetRef = useRef<any>(null);
|
|
34
30
|
|
|
31
|
+
useHelper(editMode && isSelected ? spotLightRef : null, SpotLightHelper, color);
|
|
32
|
+
|
|
35
33
|
useEffect(() => {
|
|
36
34
|
if (spotLightRef.current && targetRef.current) {
|
|
37
35
|
spotLightRef.current.target = targetRef.current;
|