react-three-game 0.0.56 → 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/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 +138 -56
- 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 +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 +73 -21
- package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +8 -2
- package/dist/tools/prefabeditor/components/MaterialComponent.js +122 -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 +4 -12
- 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/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 +234 -101
- 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 +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 +220 -27
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +178 -16
- 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 +11 -17
- 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,5 +1,6 @@
|
|
|
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';
|
|
5
6
|
import { colors } from '../styles';
|
|
@@ -8,7 +9,9 @@ import {
|
|
|
8
9
|
RepeatWrapping,
|
|
9
10
|
ClampToEdgeWrapping,
|
|
10
11
|
SRGBColorSpace,
|
|
12
|
+
LinearSRGBColorSpace,
|
|
11
13
|
Texture,
|
|
14
|
+
Vector2,
|
|
12
15
|
NearestFilter,
|
|
13
16
|
LinearFilter,
|
|
14
17
|
NearestMipmapNearestFilter,
|
|
@@ -17,18 +20,31 @@ import {
|
|
|
17
20
|
LinearMipmapLinearFilter,
|
|
18
21
|
MinificationTextureFilter,
|
|
19
22
|
MagnificationTextureFilter,
|
|
20
|
-
|
|
23
|
+
MeshBasicMaterialProperties,
|
|
24
|
+
MeshStandardMaterialProperties,
|
|
25
|
+
FrontSide,
|
|
26
|
+
BackSide,
|
|
27
|
+
DoubleSide,
|
|
21
28
|
} from 'three';
|
|
22
29
|
|
|
23
|
-
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;
|
|
24
35
|
texture?: string;
|
|
25
36
|
repeat?: boolean;
|
|
26
37
|
repeatCount?: [number, number];
|
|
27
38
|
generateMipmaps?: boolean;
|
|
28
39
|
minFilter?: string;
|
|
29
40
|
magFilter?: string;
|
|
41
|
+
normalMapTexture?: string;
|
|
42
|
+
normalScale?: [number, number];
|
|
30
43
|
}
|
|
31
44
|
|
|
45
|
+
const PICKER_POPUP_WIDTH = 260;
|
|
46
|
+
const PICKER_POPUP_HEIGHT = 360;
|
|
47
|
+
|
|
32
48
|
function TexturePicker({
|
|
33
49
|
value,
|
|
34
50
|
onChange,
|
|
@@ -40,6 +56,8 @@ function TexturePicker({
|
|
|
40
56
|
}) {
|
|
41
57
|
const [textureFiles, setTextureFiles] = useState<string[]>([]);
|
|
42
58
|
const [showPicker, setShowPicker] = useState(false);
|
|
59
|
+
const [popupStyle, setPopupStyle] = useState<React.CSSProperties | null>(null);
|
|
60
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
43
61
|
|
|
44
62
|
useEffect(() => {
|
|
45
63
|
fetch(`${basePath}/textures/manifest.json`)
|
|
@@ -48,6 +66,45 @@ function TexturePicker({
|
|
|
48
66
|
.catch(console.error);
|
|
49
67
|
}, [basePath]);
|
|
50
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
|
+
|
|
51
108
|
// Only show 3D preview for server-hosted textures (starting with / or http)
|
|
52
109
|
const canPreview = value && (value.startsWith('/') || value.startsWith('http'));
|
|
53
110
|
|
|
@@ -60,6 +117,7 @@ function TexturePicker({
|
|
|
60
117
|
: null
|
|
61
118
|
}
|
|
62
119
|
<button
|
|
120
|
+
ref={triggerRef}
|
|
63
121
|
onClick={() => setShowPicker(!showPicker)}
|
|
64
122
|
style={{ padding: '4px 8px', backgroundColor: colors.bgLight, color: 'inherit', fontSize: 10, cursor: 'pointer', border: `1px solid ${colors.border}`, borderRadius: 3, marginTop: 4 }}
|
|
65
123
|
>
|
|
@@ -73,8 +131,8 @@ function TexturePicker({
|
|
|
73
131
|
>
|
|
74
132
|
Clear
|
|
75
133
|
</button>
|
|
76
|
-
{showPicker && (
|
|
77
|
-
<div style={{
|
|
134
|
+
{showPicker && popupStyle && typeof document !== 'undefined' && createPortal(
|
|
135
|
+
<div style={popupStyle} onMouseLeave={() => setShowPicker(false)}>
|
|
78
136
|
<TextureListViewer
|
|
79
137
|
files={textureFiles}
|
|
80
138
|
selected={value || undefined}
|
|
@@ -84,26 +142,51 @@ function TexturePicker({
|
|
|
84
142
|
}}
|
|
85
143
|
basePath={basePath}
|
|
86
144
|
/>
|
|
87
|
-
</div
|
|
145
|
+
</div>,
|
|
146
|
+
document.body
|
|
88
147
|
)}
|
|
89
148
|
</div>
|
|
90
149
|
);
|
|
91
150
|
}
|
|
92
151
|
|
|
93
152
|
function MaterialComponentEditor({ component, onUpdate, basePath = "" }: { component: any; onUpdate: (newComp: any) => void; basePath?: string }) {
|
|
153
|
+
const materialType = component.properties.materialType ?? 'standard';
|
|
94
154
|
const hasTexture = !!component.properties.texture;
|
|
95
155
|
const hasRepeat = component.properties.repeat;
|
|
156
|
+
const isStandardMaterial = materialType === 'standard';
|
|
96
157
|
|
|
97
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
|
+
},
|
|
98
168
|
{ name: 'color', type: 'color', label: 'Color' },
|
|
169
|
+
{ name: 'toneMapped', type: 'boolean', label: 'Tone Mapped' },
|
|
99
170
|
{ name: 'wireframe', type: 'boolean', label: 'Wireframe' },
|
|
100
171
|
{ name: 'transparent', type: 'boolean', label: 'Transparent' },
|
|
101
172
|
{ name: 'opacity', type: 'number', label: 'Opacity', min: 0, max: 1, step: 0.01 },
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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,
|
|
107
190
|
{
|
|
108
191
|
name: 'texture',
|
|
109
192
|
type: 'custom',
|
|
@@ -140,6 +223,39 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }: { compo
|
|
|
140
223
|
</div>
|
|
141
224
|
),
|
|
142
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] : []),
|
|
143
259
|
{ name: 'generateMipmaps', type: 'boolean', label: 'Generate Mipmaps' } as FieldDefinition,
|
|
144
260
|
{
|
|
145
261
|
name: 'minFilter',
|
|
@@ -177,6 +293,7 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }: { compo
|
|
|
177
293
|
|
|
178
294
|
// View for Material component
|
|
179
295
|
function MaterialComponentView({ properties, loadedTextures }: { properties: MaterialProps, loadedTextures?: Record<string, Texture> }) {
|
|
296
|
+
const materialType = properties?.materialType ?? 'standard';
|
|
180
297
|
const textureName = properties?.texture;
|
|
181
298
|
const repeat = properties?.repeat;
|
|
182
299
|
const repeatCount = properties?.repeatCount;
|
|
@@ -185,6 +302,11 @@ function MaterialComponentView({ properties, loadedTextures }: { properties: Mat
|
|
|
185
302
|
const magFilter = properties?.magFilter || 'LinearFilter';
|
|
186
303
|
const texture = textureName && loadedTextures ? loadedTextures[textureName] : undefined;
|
|
187
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
|
+
|
|
188
310
|
// Destructure all material props and separate custom texture handling props
|
|
189
311
|
const {
|
|
190
312
|
texture: _texture,
|
|
@@ -193,9 +315,22 @@ function MaterialComponentView({ properties, loadedTextures }: { properties: Mat
|
|
|
193
315
|
generateMipmaps: _generateMipmaps,
|
|
194
316
|
minFilter: _minFilter,
|
|
195
317
|
magFilter: _magFilter,
|
|
196
|
-
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,
|
|
197
329
|
...materialProps
|
|
198
|
-
} =
|
|
330
|
+
} = materialSource;
|
|
331
|
+
|
|
332
|
+
const sideMap: Record<string, any> = { FrontSide, BackSide, DoubleSide };
|
|
333
|
+
const resolvedSide = sideProp ? (sideMap[sideProp as unknown as string] ?? FrontSide) : FrontSide;
|
|
199
334
|
|
|
200
335
|
const minFilterMap: Record<string, MinificationTextureFilter> = {
|
|
201
336
|
NearestFilter,
|
|
@@ -229,15 +364,40 @@ function MaterialComponentView({ properties, loadedTextures }: { properties: Mat
|
|
|
229
364
|
return t;
|
|
230
365
|
}, [texture, repeat, repeatCount?.[0], repeatCount?.[1], generateMipmaps, minFilter, magFilter]);
|
|
231
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
|
+
|
|
232
380
|
if (!properties) {
|
|
233
381
|
return <meshStandardMaterial color="red" wireframe />;
|
|
234
382
|
}
|
|
235
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
|
+
|
|
236
395
|
return (
|
|
237
396
|
<meshStandardMaterial
|
|
238
|
-
key={
|
|
239
|
-
|
|
240
|
-
{
|
|
397
|
+
key={materialKey}
|
|
398
|
+
{...sharedProps}
|
|
399
|
+
normalMap={finalNormalMap}
|
|
400
|
+
normalScale={normalScaleVec}
|
|
241
401
|
/>
|
|
242
402
|
);
|
|
243
403
|
}
|
|
@@ -248,7 +408,9 @@ const MaterialComponent: Component = {
|
|
|
248
408
|
View: MaterialComponentView,
|
|
249
409
|
nonComposable: true,
|
|
250
410
|
defaultProperties: {
|
|
411
|
+
materialType: 'standard',
|
|
251
412
|
color: '#ffffff',
|
|
413
|
+
toneMapped: true,
|
|
252
414
|
wireframe: false,
|
|
253
415
|
transparent: false,
|
|
254
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);
|
|
@@ -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
|
|
|
@@ -1,29 +1,23 @@
|
|
|
1
1
|
import { Component } from "./ComponentRegistry";
|
|
2
2
|
import { useRef, useEffect } from "react";
|
|
3
|
-
import {
|
|
3
|
+
import { BooleanField, ColorField, FieldGroup, NumberField } from "./Input";
|
|
4
4
|
import { useHelper } from "@react-three/drei";
|
|
5
5
|
import { SpotLightHelper } from "three";
|
|
6
6
|
|
|
7
|
-
const spotLightFields: FieldDefinition[] = [
|
|
8
|
-
{ name: 'color', type: 'color', label: 'Color' },
|
|
9
|
-
{ name: 'intensity', type: 'number', label: 'Intensity', step: 0.1, min: 0 },
|
|
10
|
-
{ name: 'angle', type: 'number', label: 'Angle', step: 0.1, min: 0, max: Math.PI },
|
|
11
|
-
{ name: 'penumbra', type: 'number', label: 'Penumbra', step: 0.1, min: 0, max: 1 },
|
|
12
|
-
{ name: 'distance', type: 'number', label: 'Distance', step: 1, min: 0 },
|
|
13
|
-
{ name: 'castShadow', type: 'boolean', label: 'Cast Shadow' },
|
|
14
|
-
];
|
|
15
|
-
|
|
16
7
|
function SpotLightComponentEditor({ component, onUpdate }: { component: any; onUpdate: (newComp: any) => void }) {
|
|
17
8
|
return (
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
values={component.properties}
|
|
21
|
-
onChange={onUpdate}
|
|
22
|
-
|
|
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>
|
|
23
17
|
);
|
|
24
18
|
}
|
|
25
19
|
|
|
26
|
-
function SpotLightView({ properties, editMode }: { properties: any; editMode?: boolean }) {
|
|
20
|
+
function SpotLightView({ properties, editMode, isSelected }: { properties: any; editMode?: boolean; isSelected?: boolean }) {
|
|
27
21
|
const color = properties.color ?? '#ffffff';
|
|
28
22
|
const intensity = properties.intensity ?? 1.0;
|
|
29
23
|
const angle = properties.angle ?? Math.PI / 6;
|
|
@@ -34,7 +28,7 @@ function SpotLightView({ properties, editMode }: { properties: any; editMode?: b
|
|
|
34
28
|
const spotLightRef = useRef<any>(null);
|
|
35
29
|
const targetRef = useRef<any>(null);
|
|
36
30
|
|
|
37
|
-
useHelper(editMode ? spotLightRef : null, SpotLightHelper, color);
|
|
31
|
+
useHelper(editMode && isSelected ? spotLightRef : null, SpotLightHelper, color);
|
|
38
32
|
|
|
39
33
|
useEffect(() => {
|
|
40
34
|
if (spotLightRef.current && targetRef.current) {
|