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.
Files changed (59) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +1 -1
  3. package/dist/shared/GameCanvas.js +1 -3
  4. package/dist/tools/assetviewer/page.js +35 -14
  5. package/dist/tools/prefabeditor/Dropdown.d.ts +15 -0
  6. package/dist/tools/prefabeditor/Dropdown.js +82 -0
  7. package/dist/tools/prefabeditor/EditorContext.d.ts +5 -0
  8. package/dist/tools/prefabeditor/EditorTree.js +138 -56
  9. package/dist/tools/prefabeditor/EditorUI.js +1 -1
  10. package/dist/tools/prefabeditor/PrefabEditor.d.ts +1 -0
  11. package/dist/tools/prefabeditor/PrefabEditor.js +13 -2
  12. package/dist/tools/prefabeditor/PrefabRoot.d.ts +1 -0
  13. package/dist/tools/prefabeditor/PrefabRoot.js +120 -34
  14. package/dist/tools/prefabeditor/components/AmbientLightComponent.js +3 -7
  15. package/dist/tools/prefabeditor/components/CameraComponent.d.ts +3 -0
  16. package/dist/tools/prefabeditor/components/CameraComponent.js +25 -0
  17. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +2 -2
  18. package/dist/tools/prefabeditor/components/EnvironmentComponent.d.ts +3 -0
  19. package/dist/tools/prefabeditor/components/EnvironmentComponent.js +15 -0
  20. package/dist/tools/prefabeditor/components/GeometryComponent.js +46 -46
  21. package/dist/tools/prefabeditor/components/Input.d.ts +51 -1
  22. package/dist/tools/prefabeditor/components/Input.js +73 -21
  23. package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +8 -2
  24. package/dist/tools/prefabeditor/components/MaterialComponent.js +122 -14
  25. package/dist/tools/prefabeditor/components/ModelComponent.js +44 -3
  26. package/dist/tools/prefabeditor/components/PhysicsComponent.js +16 -81
  27. package/dist/tools/prefabeditor/components/SpotLightComponent.js +4 -12
  28. package/dist/tools/prefabeditor/components/TextComponent.js +7 -53
  29. package/dist/tools/prefabeditor/components/TransformComponent.js +18 -8
  30. package/dist/tools/prefabeditor/components/index.js +5 -1
  31. package/dist/tools/prefabeditor/styles.d.ts +5 -2
  32. package/dist/tools/prefabeditor/styles.js +7 -3
  33. package/dist/tools/prefabeditor/utils.d.ts +4 -3
  34. package/dist/tools/prefabeditor/utils.js +53 -5
  35. package/package.json +1 -1
  36. package/src/index.ts +7 -0
  37. package/src/shared/GameCanvas.tsx +0 -3
  38. package/src/tools/assetviewer/page.tsx +77 -45
  39. package/src/tools/prefabeditor/Dropdown.tsx +112 -0
  40. package/src/tools/prefabeditor/EditorContext.tsx +5 -0
  41. package/src/tools/prefabeditor/EditorTree.tsx +234 -101
  42. package/src/tools/prefabeditor/EditorUI.tsx +1 -1
  43. package/src/tools/prefabeditor/PrefabEditor.tsx +17 -4
  44. package/src/tools/prefabeditor/PrefabRoot.tsx +208 -58
  45. package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +5 -11
  46. package/src/tools/prefabeditor/components/CameraComponent.tsx +80 -0
  47. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +2 -2
  48. package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +47 -0
  49. package/src/tools/prefabeditor/components/GeometryComponent.tsx +69 -63
  50. package/src/tools/prefabeditor/components/Input.tsx +220 -27
  51. package/src/tools/prefabeditor/components/MaterialComponent.tsx +178 -16
  52. package/src/tools/prefabeditor/components/ModelComponent.tsx +51 -4
  53. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +44 -85
  54. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +11 -17
  55. package/src/tools/prefabeditor/components/TextComponent.tsx +58 -57
  56. package/src/tools/prefabeditor/components/TransformComponent.tsx +61 -9
  57. package/src/tools/prefabeditor/components/index.ts +5 -1
  58. package/src/tools/prefabeditor/styles.ts +7 -3
  59. 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
- MeshStandardMaterialProperties
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={{ position: 'fixed', right: 60, top: 60, transform: 'translate(-100%,0%)', background: colors.bg, padding: 16, border: `1px solid ${colors.border}`, borderRadius: 4, maxHeight: '80vh', overflowY: 'auto', overflowX: 'hidden', width: 220, zIndex: 1000, boxShadow: '0 4px 16px rgba(0,0,0,0.6)' }}>
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
- { name: 'metalness', type: 'number', label: 'Metalness', min: 0, max: 1, step: 0.01 },
103
- { name: 'roughness', type: 'number', label: 'Roughness', min: 0, max: 1, step: 0.01 },
104
- { name: 'transmission', type: 'number', label: 'Transmission', min: 0, max: 1, step: 0.01 },
105
- { name: 'thickness', type: 'number', label: 'Thickness', min: 0, step: 0.1 },
106
- { name: 'ior', type: 'number', label: 'IOR (Index of Refraction)', min: 1, max: 2.333, step: 0.01 },
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, // Filter out map since we set it explicitly
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
- } = properties || {};
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={finalTexture?.uuid ?? 'no-texture'}
239
- map={finalTexture}
240
- {...materialProps}
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={{ position: 'fixed', right: 60, top: 60, transform: 'translate(-100%,0%)', background: 'rgba(0,0,0,0.9)', padding: 16, border: '1px solid rgba(34, 211, 238, 0.3)', maxHeight: '80vh', overflowY: 'auto', overflowX: 'hidden', width: 220, zIndex: 1000 }}>
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 { FieldRenderer, FieldDefinition } from "./Input";
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
- <FieldRenderer
96
- fields={physicsFields}
97
- values={component.properties}
98
- onChange={onUpdate}
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 { FieldRenderer, FieldDefinition } from "./Input";
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
- <FieldRenderer
19
- fields={spotLightFields}
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) {