react-three-game 0.0.42 → 0.0.45

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 (33) hide show
  1. package/README.md +38 -4
  2. package/dist/index.d.ts +7 -6
  3. package/dist/index.js +9 -6
  4. package/dist/shared/GameCanvas.js +1 -1
  5. package/dist/tools/assetviewer/page.js +2 -2
  6. package/dist/tools/prefabeditor/EditorUI.js +3 -5
  7. package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +0 -2
  8. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +27 -27
  9. package/dist/tools/prefabeditor/components/GeometryComponent.js +40 -21
  10. package/dist/tools/prefabeditor/components/Input.d.ts +78 -1
  11. package/dist/tools/prefabeditor/components/Input.js +65 -0
  12. package/dist/tools/prefabeditor/components/MaterialComponent.js +57 -26
  13. package/dist/tools/prefabeditor/components/ModelComponent.js +17 -8
  14. package/dist/tools/prefabeditor/components/PhysicsComponent.js +25 -14
  15. package/dist/tools/prefabeditor/components/SpotLightComponent.js +10 -21
  16. package/dist/tools/prefabeditor/components/TransformComponent.js +27 -19
  17. package/dist/tools/prefabeditor/page.js +1 -1
  18. package/package.json +1 -1
  19. package/src/index.ts +28 -10
  20. package/src/shared/GameCanvas.tsx +1 -1
  21. package/src/tools/assetviewer/page.tsx +3 -3
  22. package/src/tools/prefabeditor/EditorUI.tsx +0 -10
  23. package/src/tools/prefabeditor/components/ComponentRegistry.ts +3 -5
  24. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +72 -76
  25. package/src/tools/prefabeditor/components/GeometryComponent.tsx +55 -38
  26. package/src/tools/prefabeditor/components/Input.tsx +299 -0
  27. package/src/tools/prefabeditor/components/MaterialComponent.tsx +97 -140
  28. package/src/tools/prefabeditor/components/ModelComponent.tsx +62 -41
  29. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +29 -33
  30. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +17 -65
  31. package/src/tools/prefabeditor/components/TransformComponent.tsx +83 -56
  32. package/src/tools/prefabeditor/page.tsx +1 -1
  33. package/dist/index.umd.js +0 -4622
@@ -1,10 +1,20 @@
1
1
  import { ModelListViewer, SingleModelViewer } from '../../assetviewer/page';
2
2
  import { useEffect, useState, useMemo } from 'react';
3
3
  import { Component } from './ComponentRegistry';
4
- import { Label } from './Input';
4
+ import { FieldRenderer, FieldDefinition } from './Input';
5
5
  import { GameObject } from '../types';
6
6
 
7
- function ModelComponentEditor({ component, node, onUpdate, basePath = "" }: { component: any; node?: GameObject; onUpdate: (newComp: any) => void; basePath?: string }) {
7
+ function ModelPicker({
8
+ value,
9
+ onChange,
10
+ basePath,
11
+ nodeId
12
+ }: {
13
+ value: string | undefined;
14
+ onChange: (v: string) => void;
15
+ basePath: string;
16
+ nodeId?: string;
17
+ }) {
8
18
  const [modelFiles, setModelFiles] = useState<string[]>([]);
9
19
  const [showPicker, setShowPicker] = useState(false);
10
20
 
@@ -17,49 +27,60 @@ function ModelComponentEditor({ component, node, onUpdate, basePath = "" }: { co
17
27
  }, [basePath]);
18
28
 
19
29
  const handleModelSelect = (file: string) => {
20
- // Remove leading slash for prefab compatibility
21
30
  const filename = file.startsWith('/') ? file.slice(1) : file;
22
- onUpdate({ 'filename': filename });
31
+ onChange(filename);
32
+ setShowPicker(false);
23
33
  };
24
34
 
25
- return <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
26
- <div>
27
- <Label>Model File</Label>
28
- <div style={{ maxHeight: 128, overflowY: 'auto', position: 'relative', display: 'flex', alignItems: 'center' }}>
29
- <SingleModelViewer file={component.properties.filename ? `/${component.properties.filename}` : undefined} basePath={basePath} />
30
- <button
31
- onClick={() => setShowPicker(!showPicker)}
32
- style={{ padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)', marginTop: 4 }}
33
- >
34
- {showPicker ? 'Hide' : 'Change'}
35
- </button>
36
- {showPicker && (
37
- <div style={{ position: 'fixed', left: '-10px', top: '50%', transform: 'translate(-100%, -50%)', background: 'rgba(0,0,0,0.9)', padding: 16, border: '1px solid rgba(34, 211, 238, 0.3)', maxHeight: '80vh', overflowY: 'auto', zIndex: 1000 }}>
38
- <ModelListViewer
39
- key={node?.id}
40
- files={modelFiles}
41
- selected={component.properties.filename ? `/${component.properties.filename}` : undefined}
42
- onSelect={(file) => {
43
- handleModelSelect(file);
44
- setShowPicker(false);
45
- }}
46
- basePath={basePath}
47
- />
48
- </div>
49
- )}
50
- </div>
51
- </div>
52
- <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
53
- <input
54
- type="checkbox"
55
- id="instanced-checkbox"
56
- checked={component.properties.instanced || false}
57
- onChange={e => onUpdate({ instanced: e.target.checked })}
58
- style={{ width: 12, height: 12 }}
59
- />
60
- <label htmlFor="instanced-checkbox" style={{ fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)' }}>Instanced</label>
35
+ return (
36
+ <div style={{ maxHeight: 128, overflowY: 'auto', position: 'relative', display: 'flex', alignItems: 'center' }}>
37
+ <SingleModelViewer file={value ? `/${value}` : undefined} basePath={basePath} />
38
+ <button
39
+ onClick={() => setShowPicker(!showPicker)}
40
+ style={{ padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)', marginTop: 4 }}
41
+ >
42
+ {showPicker ? 'Hide' : 'Change'}
43
+ </button>
44
+ {showPicker && (
45
+ <div style={{ position: 'fixed', left: '-10px', top: '50%', transform: 'translate(-100%, -50%)', background: 'rgba(0,0,0,0.9)', padding: 16, border: '1px solid rgba(34, 211, 238, 0.3)', maxHeight: '80vh', overflowY: 'auto', zIndex: 1000 }}>
46
+ <ModelListViewer
47
+ key={nodeId}
48
+ files={modelFiles}
49
+ selected={value ? `/${value}` : undefined}
50
+ onSelect={handleModelSelect}
51
+ basePath={basePath}
52
+ />
53
+ </div>
54
+ )}
61
55
  </div>
62
- </div>;
56
+ );
57
+ }
58
+
59
+ function ModelComponentEditor({ component, node, onUpdate, basePath = "" }: { component: any; node?: GameObject; onUpdate: (newComp: any) => void; basePath?: string }) {
60
+ const fields: FieldDefinition[] = [
61
+ {
62
+ name: 'filename',
63
+ type: 'custom',
64
+ label: 'Model File',
65
+ render: ({ value, onChange }) => (
66
+ <ModelPicker
67
+ value={value}
68
+ onChange={onChange}
69
+ basePath={basePath}
70
+ nodeId={node?.id}
71
+ />
72
+ ),
73
+ },
74
+ { name: 'instanced', type: 'boolean', label: 'Instanced' },
75
+ ];
76
+
77
+ return (
78
+ <FieldRenderer
79
+ fields={fields}
80
+ values={component.properties}
81
+ onChange={onUpdate}
82
+ />
83
+ );
63
84
  }
64
85
 
65
86
  // View for Model component
@@ -2,7 +2,7 @@ import { RigidBody, RapierRigidBody } from "@react-three/rapier";
2
2
  import type { ReactNode } from 'react';
3
3
  import { useEffect, useRef } from 'react';
4
4
  import { Component } from "./ComponentRegistry";
5
- import { Label } from "./Input";
5
+ import { FieldRenderer, FieldDefinition } from "./Input";
6
6
  import { Quaternion, Euler } from 'three';
7
7
 
8
8
  export interface PhysicsProps {
@@ -13,40 +13,36 @@ export interface PhysicsProps {
13
13
  friction?: number;
14
14
  }
15
15
 
16
- function PhysicsComponentEditor({ component, onUpdate }: { component: { properties: { type?: 'dynamic' | 'fixed'; collider?: string;[k: string]: any } }; onUpdate: (props: Partial<Record<string, any>>) => void }) {
17
- const { type = 'dynamic', collider = 'hull' } = component.properties;
18
-
19
- const selectStyle = {
20
- width: '100%',
21
- backgroundColor: 'rgba(0, 0, 0, 0.4)',
22
- border: '1px solid rgba(34, 211, 238, 0.3)',
23
- padding: '2px 4px',
24
- fontSize: '10px',
25
- color: 'rgba(165, 243, 252, 1)',
26
- fontFamily: 'monospace',
27
- outline: 'none',
28
- };
16
+ const physicsFields: FieldDefinition[] = [
17
+ {
18
+ name: 'type',
19
+ type: 'select',
20
+ label: 'Type',
21
+ options: [
22
+ { value: 'dynamic', label: 'Dynamic' },
23
+ { value: 'fixed', label: 'Fixed' },
24
+ ],
25
+ },
26
+ {
27
+ name: 'collider',
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
+ ];
29
38
 
39
+ function PhysicsComponentEditor({ component, onUpdate }: { component: { properties: { type?: 'dynamic' | 'fixed'; collider?: string;[k: string]: any } }; onUpdate: (props: Partial<Record<string, any>>) => void }) {
30
40
  return (
31
- <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
32
- <div>
33
- <Label>Type</Label>
34
- <select style={selectStyle} value={type} onChange={e => onUpdate({ type: e.target.value })}>
35
- <option value="dynamic">Dynamic</option>
36
- <option value="fixed">Fixed</option>
37
- </select>
38
- </div>
39
-
40
- <div>
41
- <Label>Collider</Label>
42
- <select style={selectStyle} value={collider} onChange={e => onUpdate({ collider: e.target.value })}>
43
- <option value="hull">Hull (convex)</option>
44
- <option value="trimesh">Trimesh (exact)</option>
45
- <option value="cuboid">Cuboid (box)</option>
46
- <option value="ball">Ball (sphere)</option>
47
- </select>
48
- </div>
49
- </div>
41
+ <FieldRenderer
42
+ fields={physicsFields}
43
+ values={component.properties}
44
+ onChange={onUpdate}
45
+ />
50
46
  );
51
47
  }
52
48
 
@@ -1,72 +1,24 @@
1
1
  import { Component } from "./ComponentRegistry";
2
2
  import { useRef, useEffect } from "react";
3
- import { Input, Label } from "./Input";
3
+ import { FieldRenderer, FieldDefinition } from "./Input";
4
4
 
5
- function SpotLightComponentEditor({ component, onUpdate }: { component: any; onUpdate: (newComp: any) => void }) {
6
- const props = {
7
- color: component.properties.color ?? '#ffffff',
8
- intensity: component.properties.intensity ?? 1.0,
9
- angle: component.properties.angle ?? Math.PI / 6,
10
- penumbra: component.properties.penumbra ?? 0.5,
11
- distance: component.properties.distance ?? 100,
12
- castShadow: component.properties.castShadow ?? true
13
- };
14
-
15
- const textInputStyle = {
16
- flex: 1,
17
- backgroundColor: 'rgba(0, 0, 0, 0.4)',
18
- border: '1px solid rgba(34, 211, 238, 0.3)',
19
- padding: '2px 4px',
20
- fontSize: '10px',
21
- color: 'rgba(165, 243, 252, 1)',
22
- fontFamily: 'monospace',
23
- outline: 'none',
24
- };
5
+ const spotLightFields: FieldDefinition[] = [
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
+ ];
25
13
 
26
- return <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
27
- <div>
28
- <Label>Color</Label>
29
- <div style={{ display: 'flex', gap: 2 }}>
30
- <input
31
- type="color"
32
- style={{ height: 20, width: 20, backgroundColor: 'transparent', border: 'none', cursor: 'pointer' }}
33
- value={props.color}
34
- onChange={e => onUpdate({ ...component.properties, color: e.target.value })}
35
- />
36
- <input
37
- type="text"
38
- style={textInputStyle}
39
- value={props.color}
40
- onChange={e => onUpdate({ ...component.properties, color: e.target.value })}
41
- />
42
- </div>
43
- </div>
44
- <div>
45
- <Label>Intensity</Label>
46
- <Input step="0.1" value={props.intensity} onChange={value => onUpdate({ ...component.properties, intensity: value })} />
47
- </div>
48
- <div>
49
- <Label>Angle</Label>
50
- <Input step="0.1" min={0} max={Math.PI} value={props.angle} onChange={value => onUpdate({ ...component.properties, angle: value })} />
51
- </div>
52
- <div>
53
- <Label>Penumbra</Label>
54
- <Input step="0.1" min={0} max={1} value={props.penumbra} onChange={value => onUpdate({ ...component.properties, penumbra: value })} />
55
- </div>
56
- <div>
57
- <Label>Distance</Label>
58
- <Input step="1" min={0} value={props.distance} onChange={value => onUpdate({ ...component.properties, distance: value })} />
59
- </div>
60
- <div>
61
- <Label>Cast Shadow</Label>
62
- <input
63
- type="checkbox"
64
- style={{ height: 16, width: 16, backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', cursor: 'pointer' }}
65
- checked={props.castShadow}
66
- onChange={e => onUpdate({ ...component.properties, castShadow: e.target.checked })}
67
- />
68
- </div>
69
- </div>;
14
+ function SpotLightComponentEditor({ component, onUpdate }: { component: any; onUpdate: (newComp: any) => void }) {
15
+ return (
16
+ <FieldRenderer
17
+ fields={spotLightFields}
18
+ values={component.properties}
19
+ onChange={onUpdate}
20
+ />
21
+ );
70
22
  }
71
23
 
72
24
  function SpotLightView({ properties, editMode }: { properties: any; editMode?: boolean }) {
@@ -1,5 +1,5 @@
1
1
  import { Component } from "./ComponentRegistry";
2
- import { Vector3Input, Label } from "./Input";
2
+ import { FieldRenderer, FieldDefinition, Label } from "./Input";
3
3
  import { useEditorContext } from "../EditorContext";
4
4
 
5
5
  const buttonStyle = {
@@ -13,65 +13,92 @@ const buttonStyle = {
13
13
  flex: 1,
14
14
  };
15
15
 
16
- function TransformComponentEditor({ component, onUpdate, transformMode, setTransformMode }: {
16
+ function TransformModeSelector({
17
+ transformMode,
18
+ setTransformMode,
19
+ snapResolution,
20
+ setSnapResolution
21
+ }: {
22
+ transformMode: "translate" | "rotate" | "scale";
23
+ setTransformMode: (m: "translate" | "rotate" | "scale") => void;
24
+ snapResolution: number;
25
+ setSnapResolution: (v: number) => void;
26
+ }) {
27
+ return (
28
+ <div style={{ marginBottom: 8 }}>
29
+ <Label>Transform Mode {snapResolution > 0 && `(Snap: ${snapResolution})`}</Label>
30
+ <div style={{ display: 'flex', gap: 6 }}>
31
+ {["translate", "rotate", "scale"].map(mode => {
32
+ const isActive = transformMode === mode;
33
+ return (
34
+ <button
35
+ key={mode}
36
+ onClick={() => setTransformMode(mode as any)}
37
+ style={{
38
+ ...buttonStyle,
39
+ background: isActive ? 'rgba(255,255,255,0.10)' : 'transparent',
40
+ }}
41
+ onPointerEnter={(e) => {
42
+ if (!isActive) e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
43
+ }}
44
+ onPointerLeave={(e) => {
45
+ if (!isActive) e.currentTarget.style.background = 'transparent';
46
+ }}
47
+ >
48
+ {mode}
49
+ </button>
50
+ );
51
+ })}
52
+ </div>
53
+ <div style={{ marginTop: 6 }}>
54
+ <button
55
+ onClick={() => setSnapResolution(snapResolution > 0 ? 0 : 0.1)}
56
+ style={{
57
+ ...buttonStyle,
58
+ background: snapResolution > 0 ? 'rgba(255,255,255,0.10)' : 'transparent',
59
+ width: '100%',
60
+ }}
61
+ onPointerEnter={(e) => {
62
+ if (snapResolution === 0) e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
63
+ }}
64
+ onPointerLeave={(e) => {
65
+ if (snapResolution === 0) e.currentTarget.style.background = 'transparent';
66
+ }}
67
+ >
68
+ Snap: {snapResolution > 0 ? `ON (${snapResolution})` : 'OFF'}
69
+ </button>
70
+ </div>
71
+ </div>
72
+ );
73
+ }
74
+
75
+ function TransformComponentEditor({ component, onUpdate }: {
17
76
  component: any;
18
77
  onUpdate: (newComp: any) => void;
19
- transformMode?: "translate" | "rotate" | "scale";
20
- setTransformMode?: (m: "translate" | "rotate" | "scale") => void;
21
78
  }) {
22
- const { snapResolution, setSnapResolution } = useEditorContext();
79
+ const { transformMode, setTransformMode, snapResolution, setSnapResolution } = useEditorContext();
23
80
 
24
- return <div style={{ display: 'flex', flexDirection: 'column' }}>
25
- {transformMode && setTransformMode && (
26
- <div style={{ marginBottom: 8 }}>
27
- <Label>Transform Mode {snapResolution > 0 && `(Snap: ${snapResolution})`}</Label>
28
- <div style={{ display: 'flex', gap: 6 }}>
29
- {["translate", "rotate", "scale"].map(mode => {
30
- const isActive = transformMode === mode;
31
- return (
32
- <button
33
- key={mode}
34
- onClick={() => setTransformMode(mode as any)}
35
- style={{
36
- ...buttonStyle,
37
- background: isActive ? 'rgba(255,255,255,0.10)' : 'transparent',
38
- }}
39
- onPointerEnter={(e) => {
40
- if (!isActive) e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
41
- }}
42
- onPointerLeave={(e) => {
43
- if (!isActive) e.currentTarget.style.background = 'transparent';
44
- }}
45
- >
46
- {mode}
47
- </button>
48
- );
49
- })}
50
- </div>
51
- <div style={{ marginTop: 6 }}>
52
- <button
53
- onClick={() => setSnapResolution(snapResolution > 0 ? 0 : 0.1)}
54
- style={{
55
- ...buttonStyle,
56
- background: snapResolution > 0 ? 'rgba(255,255,255,0.10)' : 'transparent',
57
- width: '100%',
58
- }}
59
- onPointerEnter={(e) => {
60
- if (snapResolution === 0) e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
61
- }}
62
- onPointerLeave={(e) => {
63
- if (snapResolution === 0) e.currentTarget.style.background = 'transparent';
64
- }}
65
- >
66
- Snap: {snapResolution > 0 ? `ON (${snapResolution})` : 'OFF'}
67
- </button>
68
- </div>
69
- </div>
70
- )}
71
- <Vector3Input label="Position" value={component.properties.position} onChange={v => onUpdate({ position: v })} snap={snapResolution} />
72
- <Vector3Input label="Rotation" value={component.properties.rotation} onChange={v => onUpdate({ rotation: v })} snap={snapResolution} />
73
- <Vector3Input label="Scale" value={component.properties.scale} onChange={v => onUpdate({ scale: v })} snap={snapResolution} />
74
- </div>;
81
+ const fields: FieldDefinition[] = [
82
+ { name: 'position', type: 'vector3', label: 'Position', snap: snapResolution },
83
+ { name: 'rotation', type: 'vector3', label: 'Rotation', snap: snapResolution },
84
+ { name: 'scale', type: 'vector3', label: 'Scale', snap: snapResolution },
85
+ ];
86
+
87
+ return (
88
+ <div style={{ display: 'flex', flexDirection: 'column' }}>
89
+ <TransformModeSelector
90
+ transformMode={transformMode}
91
+ setTransformMode={setTransformMode}
92
+ snapResolution={snapResolution}
93
+ setSnapResolution={setSnapResolution}
94
+ />
95
+ <FieldRenderer
96
+ fields={fields}
97
+ values={component.properties}
98
+ onChange={onUpdate}
99
+ />
100
+ </div>
101
+ );
75
102
  }
76
103
 
77
104
  const TransformComponent: Component = {
@@ -2,7 +2,7 @@ import PrefabEditor from "./PrefabEditor";
2
2
 
3
3
 
4
4
  export default function PrefabEditorPage() {
5
- return <div className="w-screen h-screen">
5
+ return <div style={{ width: '100%', height: '100%' }}>
6
6
  <PrefabEditor>
7
7
  <directionalLight position={[5, 10, 7.5]} intensity={1} castShadow />
8
8
  </PrefabEditor>