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.
Files changed (64) hide show
  1. package/README.md +16 -3
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/shared/GameCanvas.js +1 -3
  5. package/dist/tools/assetviewer/page.js +35 -14
  6. package/dist/tools/prefabeditor/Dropdown.d.ts +15 -0
  7. package/dist/tools/prefabeditor/Dropdown.js +82 -0
  8. package/dist/tools/prefabeditor/EditorContext.d.ts +5 -0
  9. package/dist/tools/prefabeditor/EditorTree.js +149 -91
  10. package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +33 -0
  11. package/dist/tools/prefabeditor/EditorTreeMenus.js +136 -0
  12. package/dist/tools/prefabeditor/EditorUI.js +1 -1
  13. package/dist/tools/prefabeditor/PrefabEditor.d.ts +1 -0
  14. package/dist/tools/prefabeditor/PrefabEditor.js +13 -2
  15. package/dist/tools/prefabeditor/PrefabRoot.d.ts +1 -0
  16. package/dist/tools/prefabeditor/PrefabRoot.js +120 -34
  17. package/dist/tools/prefabeditor/components/AmbientLightComponent.js +3 -7
  18. package/dist/tools/prefabeditor/components/CameraComponent.d.ts +3 -0
  19. package/dist/tools/prefabeditor/components/CameraComponent.js +45 -0
  20. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +50 -24
  21. package/dist/tools/prefabeditor/components/EnvironmentComponent.d.ts +3 -0
  22. package/dist/tools/prefabeditor/components/EnvironmentComponent.js +15 -0
  23. package/dist/tools/prefabeditor/components/GeometryComponent.js +46 -46
  24. package/dist/tools/prefabeditor/components/Input.d.ts +51 -1
  25. package/dist/tools/prefabeditor/components/Input.js +73 -21
  26. package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +16 -2
  27. package/dist/tools/prefabeditor/components/MaterialComponent.js +129 -15
  28. package/dist/tools/prefabeditor/components/ModelComponent.js +44 -3
  29. package/dist/tools/prefabeditor/components/PhysicsComponent.js +16 -81
  30. package/dist/tools/prefabeditor/components/SpotLightComponent.js +36 -23
  31. package/dist/tools/prefabeditor/components/TextComponent.js +7 -53
  32. package/dist/tools/prefabeditor/components/TransformComponent.js +18 -8
  33. package/dist/tools/prefabeditor/components/index.js +5 -1
  34. package/dist/tools/prefabeditor/styles.d.ts +5 -2
  35. package/dist/tools/prefabeditor/styles.js +7 -3
  36. package/dist/tools/prefabeditor/utils.d.ts +4 -3
  37. package/dist/tools/prefabeditor/utils.js +53 -5
  38. package/package.json +1 -1
  39. package/react-three-game-skill/react-three-game/SKILL.md +4 -1
  40. package/src/index.ts +7 -0
  41. package/src/shared/GameCanvas.tsx +0 -3
  42. package/src/tools/assetviewer/page.tsx +77 -45
  43. package/src/tools/prefabeditor/Dropdown.tsx +112 -0
  44. package/src/tools/prefabeditor/EditorContext.tsx +5 -0
  45. package/src/tools/prefabeditor/EditorTree.tsx +242 -178
  46. package/src/tools/prefabeditor/EditorTreeMenus.tsx +307 -0
  47. package/src/tools/prefabeditor/EditorUI.tsx +1 -1
  48. package/src/tools/prefabeditor/PrefabEditor.tsx +17 -4
  49. package/src/tools/prefabeditor/PrefabRoot.tsx +208 -58
  50. package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +5 -11
  51. package/src/tools/prefabeditor/components/CameraComponent.tsx +117 -0
  52. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +61 -30
  53. package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +47 -0
  54. package/src/tools/prefabeditor/components/GeometryComponent.tsx +69 -63
  55. package/src/tools/prefabeditor/components/Input.tsx +220 -27
  56. package/src/tools/prefabeditor/components/MaterialComponent.tsx +189 -18
  57. package/src/tools/prefabeditor/components/ModelComponent.tsx +51 -4
  58. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +44 -85
  59. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +52 -27
  60. package/src/tools/prefabeditor/components/TextComponent.tsx +58 -57
  61. package/src/tools/prefabeditor/components/TransformComponent.tsx +61 -9
  62. package/src/tools/prefabeditor/components/index.ts +5 -1
  63. package/src/tools/prefabeditor/styles.ts +7 -3
  64. package/src/tools/prefabeditor/utils.ts +55 -4
@@ -1,14 +1,20 @@
1
1
  import { SingleTextureViewer, TextureListViewer } from '../../assetviewer/page';
2
- import { useEffect, useState } from 'react';
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
- MeshStandardMaterialProperties
26
+ MeshBasicMaterialProperties,
27
+ MeshStandardMaterialProperties,
28
+ FrontSide,
29
+ BackSide,
30
+ DoubleSide,
21
31
  } from 'three';
22
32
 
23
- export interface MaterialProps extends Omit<MeshStandardMaterialProperties, 'args'> {
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={{ 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)' }}>
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
- { 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 },
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, // Filter out map since we set it explicitly
332
+ map: _map,
333
+ materialType: _materialType,
334
+ normalMapTexture: _normalMapTexture,
335
+ normalScale: _normalScale,
336
+ normalMap: _normalMap,
337
+ side: sideProp,
197
338
  ...materialProps
198
- } = properties || {};
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 <meshStandardMaterial color="red" wireframe />;
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
- <meshStandardMaterial
238
- key={finalTexture?.uuid ?? 'no-texture'}
239
- map={finalTexture}
240
- {...materialProps}
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={{ 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