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,5 @@
1
1
  import { Component } from "./ComponentRegistry";
2
- import { FieldRenderer, FieldDefinition } from "./Input";
2
+ import { ColorField, FieldGroup, NumberField, SelectField, StringField } from "./Input";
3
3
  import { Text } from 'three-text/three/react';
4
4
  import { useRef, useState, useCallback } from 'react';
5
5
  import { BufferGeometry, Mesh } from "three";
@@ -14,63 +14,64 @@ function TextComponentEditor({
14
14
  component: any;
15
15
  onUpdate: (newProps: any) => void;
16
16
  }) {
17
- const fields: FieldDefinition[] = [
18
- {
19
- name: 'text',
20
- type: 'string',
21
- label: 'Text',
22
- placeholder: 'Enter text...',
23
- },
24
- {
25
- name: 'color',
26
- type: 'color',
27
- label: 'Color',
28
- },
29
- {
30
- name: 'font',
31
- type: 'string',
32
- label: 'Font',
33
- placeholder: '/fonts/NotoSans-Regular.ttf',
34
- },
35
- {
36
- name: 'size',
37
- type: 'number',
38
- label: 'Size',
39
- min: 0.01,
40
- step: 0.1,
41
- },
42
- {
43
- name: 'depth',
44
- type: 'number',
45
- label: 'Depth',
46
- min: 0,
47
- step: 0.1,
48
- },
49
- {
50
- name: 'width',
51
- type: 'number',
52
- label: 'Width',
53
- min: 0,
54
- step: 0.5,
55
- },
56
- {
57
- name: 'align',
58
- type: 'select',
59
- label: 'Align',
60
- options: [
61
- { value: 'left', label: 'Left' },
62
- { value: 'center', label: 'Center' },
63
- { value: 'right', label: 'Right' },
64
- ],
65
- },
66
- ];
67
-
68
17
  return (
69
- <FieldRenderer
70
- fields={fields}
71
- values={component.properties}
72
- onChange={onUpdate}
73
- />
18
+ <FieldGroup>
19
+ <StringField
20
+ name="text"
21
+ label="Text"
22
+ values={component.properties}
23
+ onChange={onUpdate}
24
+ placeholder="Enter text..."
25
+ />
26
+ <ColorField
27
+ name="color"
28
+ label="Color"
29
+ values={component.properties}
30
+ onChange={onUpdate}
31
+ />
32
+ <StringField
33
+ name="font"
34
+ label="Font"
35
+ values={component.properties}
36
+ onChange={onUpdate}
37
+ placeholder="/fonts/NotoSans-Regular.ttf"
38
+ />
39
+ <NumberField
40
+ name="size"
41
+ label="Size"
42
+ values={component.properties}
43
+ onChange={onUpdate}
44
+ min={0.01}
45
+ step={0.1}
46
+ />
47
+ <NumberField
48
+ name="depth"
49
+ label="Depth"
50
+ values={component.properties}
51
+ onChange={onUpdate}
52
+ min={0}
53
+ step={0.1}
54
+ />
55
+ <NumberField
56
+ name="width"
57
+ label="Width"
58
+ values={component.properties}
59
+ onChange={onUpdate}
60
+ min={0}
61
+ step={0.5}
62
+ />
63
+ <SelectField
64
+ name="align"
65
+ label="Align"
66
+ values={component.properties}
67
+ onChange={onUpdate}
68
+ options={[
69
+ { value: 'left', label: 'Left' },
70
+ { value: 'center', label: 'Center' },
71
+ { value: 'right', label: 'Right' },
72
+ ]}
73
+ />
74
+ </FieldGroup>
74
75
  );
75
76
  }
76
77
 
@@ -1,5 +1,5 @@
1
1
  import { Component } from "./ComponentRegistry";
2
- import { FieldRenderer, FieldDefinition, Label } from "./Input";
2
+ import { Label, Vector3Field, Vector3Input } from "./Input";
3
3
  import { useEditorContext } from "../EditorContext";
4
4
  import { colors } from "../styles";
5
5
 
@@ -78,17 +78,41 @@ function TransformModeSelector({
78
78
  );
79
79
  }
80
80
 
81
+ const snapLockBtnStyle: React.CSSProperties = {
82
+ background: 'none',
83
+ border: 'none',
84
+ cursor: 'pointer',
85
+ padding: '0 2px',
86
+ fontSize: 12,
87
+ lineHeight: 1,
88
+ color: colors.textMuted,
89
+ };
90
+
91
+ function SnapLockButton({ locked, onToggle, title }: { locked: boolean; onToggle: () => void; title: string }) {
92
+ return (
93
+ <button style={snapLockBtnStyle} onClick={onToggle} title={title}>
94
+ {locked ? '🔒' : '🔓'}
95
+ </button>
96
+ );
97
+ }
98
+
81
99
  function TransformComponentEditor({ component, onUpdate }: {
82
100
  component: any;
83
101
  onUpdate: (newComp: any) => void;
84
102
  }) {
85
- const { transformMode, setTransformMode, snapResolution, setSnapResolution } = useEditorContext();
103
+ const {
104
+ transformMode,
105
+ setTransformMode,
106
+ snapResolution,
107
+ setSnapResolution,
108
+ positionSnap,
109
+ setPositionSnap,
110
+ rotationSnap,
111
+ setRotationSnap
112
+ } = useEditorContext();
86
113
 
87
- const fields: FieldDefinition[] = [
88
- { name: 'position', type: 'vector3', label: 'Position', snap: snapResolution },
89
- { name: 'rotation', type: 'vector3', label: 'Rotation', snap: snapResolution },
90
- { name: 'scale', type: 'vector3', label: 'Scale', snap: snapResolution },
91
- ];
114
+ const positionSnapped = positionSnap > 0;
115
+ const rotationSnapped = rotationSnap > 0;
92
116
 
93
117
  return (
94
118
  <div style={{ display: 'flex', flexDirection: 'column' }}>
@@ -98,10 +122,38 @@ function TransformComponentEditor({ component, onUpdate }: {
98
122
  snapResolution={snapResolution}
99
123
  setSnapResolution={setSnapResolution}
100
124
  />
101
- <FieldRenderer
102
- fields={fields}
125
+ <Vector3Input
126
+ label="Position"
127
+ value={component.properties.position ?? [0, 0, 0]}
128
+ onChange={v => onUpdate({ position: v })}
129
+ snap={positionSnap}
130
+ labelExtra={
131
+ <SnapLockButton
132
+ locked={positionSnapped}
133
+ onToggle={() => setPositionSnap(positionSnapped ? 0 : 0.5)}
134
+ title={positionSnapped ? `Snap ON (0.5) — click to disable` : `Snap OFF — click to enable (0.5)`}
135
+ />
136
+ }
137
+ />
138
+ <Vector3Input
139
+ label="Rotation"
140
+ value={component.properties.rotation ?? [0, 0, 0]}
141
+ onChange={v => onUpdate({ rotation: v })}
142
+ snap={rotationSnap}
143
+ labelExtra={
144
+ <SnapLockButton
145
+ locked={rotationSnapped}
146
+ onToggle={() => setRotationSnap(rotationSnapped ? 0 : Math.PI / 4)}
147
+ title={rotationSnapped ? `Snap ON (π/4) — click to disable` : `Snap OFF — click to enable (π/4)`}
148
+ />
149
+ }
150
+ />
151
+ <Vector3Field
152
+ name="scale"
153
+ label="Scale"
103
154
  values={component.properties}
104
155
  onChange={onUpdate}
156
+ fallback={[1, 1, 1]}
105
157
  />
106
158
  </div>
107
159
  );
@@ -7,6 +7,8 @@ import DirectionalLightComponent from './DirectionalLightComponent';
7
7
  import AmbientLightComponent from './AmbientLightComponent';
8
8
  import ModelComponent from './ModelComponent';
9
9
  import TextComponent from './TextComponent';
10
+ import EnvironmentComponent from './EnvironmentComponent';
11
+ import CameraComponent from './CameraComponent';
10
12
 
11
13
  export default [
12
14
  GeometryComponent,
@@ -17,6 +19,8 @@ export default [
17
19
  DirectionalLightComponent,
18
20
  AmbientLightComponent,
19
21
  ModelComponent,
20
- TextComponent
22
+ TextComponent,
23
+ EnvironmentComponent,
24
+ CameraComponent,
21
25
  ];
22
26
 
@@ -115,6 +115,8 @@ export const inspector = {
115
115
  padding: 8,
116
116
  maxHeight: '80vh',
117
117
  overflowY: 'auto' as const,
118
+ overflowX: 'hidden' as const,
119
+ boxSizing: 'border-box' as const,
118
120
  display: 'flex',
119
121
  flexDirection: 'column' as const,
120
122
  gap: 8,
@@ -156,7 +158,9 @@ export const menu = {
156
158
  container: {
157
159
  position: 'fixed' as const,
158
160
  zIndex: 50,
159
- minWidth: 140,
161
+ minWidth: 'auto',
162
+ width: 'max-content',
163
+ maxWidth: 'min(240px, calc(100vw - 16px))',
160
164
  background: colors.bgSurface,
161
165
  border: `1px solid ${colors.border}`,
162
166
  borderRadius: 4,
@@ -171,6 +175,7 @@ export const menu = {
171
175
  border: 'none',
172
176
  color: colors.text,
173
177
  fontSize: fonts.size,
178
+ whiteSpace: 'nowrap' as const,
174
179
  cursor: 'pointer',
175
180
  outline: 'none',
176
181
  } as React.CSSProperties,
@@ -183,8 +188,7 @@ export const toolbar = {
183
188
  panel: {
184
189
  position: 'absolute' as const,
185
190
  top: 8,
186
- left: '50%',
187
- transform: 'translateX(-50%)',
191
+ left: '240px',
188
192
  display: 'flex',
189
193
  gap: 6,
190
194
  padding: '4px 6px',
@@ -1,6 +1,6 @@
1
1
  import { GameObject, Prefab } from "./types";
2
2
  import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js';
3
- import { Object3D } from 'three';
3
+ import { Box3, Object3D, PerspectiveCamera, Quaternion, Vector3 } from 'three';
4
4
 
5
5
  export interface ExportGLBOptions {
6
6
  filename?: string;
@@ -9,10 +9,26 @@ export interface ExportGLBOptions {
9
9
  onError?: (error: any) => void;
10
10
  }
11
11
 
12
- /** Save a prefab as JSON file */
13
- export function saveJson(data: Prefab, filename: string) {
12
+ /** Save a prefab as JSON file, showing a Save As dialog when supported */
13
+ export async function saveJson(data: Prefab, filename: string) {
14
+ const json = JSON.stringify(data, null, 2);
15
+ if ('showSaveFilePicker' in window) {
16
+ try {
17
+ const handle = await (window as any).showSaveFilePicker({
18
+ suggestedName: `${filename || 'prefab'}.json`,
19
+ types: [{ description: 'JSON', accept: { 'application/json': ['.json'] } }],
20
+ });
21
+ const writable = await handle.createWritable();
22
+ await writable.write(json);
23
+ await writable.close();
24
+ return;
25
+ } catch (e: any) {
26
+ if (e?.name === 'AbortError') return; // user cancelled
27
+ }
28
+ }
29
+ // Fallback for browsers without File System Access API
14
30
  const a = document.createElement('a');
15
- a.href = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data, null, 2));
31
+ a.href = "data:text/json;charset=utf-8," + encodeURIComponent(json);
16
32
  a.download = `${filename || 'prefab'}.json`;
17
33
  a.click();
18
34
  }
@@ -102,6 +118,41 @@ export async function exportGLBData(sceneRoot: Object3D): Promise<ArrayBuffer> {
102
118
  return result as ArrayBuffer;
103
119
  }
104
120
 
121
+ export function focusCameraOnObject(
122
+ object: Object3D,
123
+ camera: Object3D,
124
+ target: Vector3,
125
+ update?: () => void,
126
+ ) {
127
+ const bounds = new Box3().setFromObject(object);
128
+ const center = new Vector3();
129
+ const size = new Vector3();
130
+ const quaternion = new Quaternion();
131
+ object.getWorldQuaternion(quaternion);
132
+
133
+ if (bounds.isEmpty()) {
134
+ object.getWorldPosition(center);
135
+ size.setScalar(1);
136
+ } else {
137
+ bounds.getCenter(center);
138
+ bounds.getSize(size);
139
+ }
140
+
141
+ const radius = Math.max(size.length() * 0.5, 1);
142
+ const forward = new Vector3(0, 0, 1).applyQuaternion(quaternion).normalize();
143
+ const worldUp = new Vector3(0, 1, 0);
144
+ const elevatedDirection = forward.clone().addScaledVector(worldUp, 0.65).normalize();
145
+ const distance = camera instanceof PerspectiveCamera
146
+ ? Math.max(radius / Math.tan((camera.fov * Math.PI) / 360) * 1.8, radius * 3.5)
147
+ : radius * 4.5;
148
+ const nextPosition = center.clone().add(elevatedDirection.multiplyScalar(distance));
149
+
150
+ camera.position.copy(nextPosition);
151
+ camera.lookAt(center);
152
+ target.copy(center);
153
+ update?.();
154
+ }
155
+
105
156
  /** Find a node by ID in the tree */
106
157
  export function findNode(root: GameObject, id: string): GameObject | null {
107
158
  if (root.id === id) return root;