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,5 @@
|
|
|
1
1
|
import { Component } from "./ComponentRegistry";
|
|
2
|
-
import {
|
|
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
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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 {
|
|
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 {
|
|
103
|
+
const {
|
|
104
|
+
transformMode,
|
|
105
|
+
setTransformMode,
|
|
106
|
+
snapResolution,
|
|
107
|
+
setSnapResolution,
|
|
108
|
+
positionSnap,
|
|
109
|
+
setPositionSnap,
|
|
110
|
+
rotationSnap,
|
|
111
|
+
setRotationSnap
|
|
112
|
+
} = useEditorContext();
|
|
86
113
|
|
|
87
|
-
const
|
|
88
|
-
|
|
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
|
-
<
|
|
102
|
-
|
|
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:
|
|
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: '
|
|
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(
|
|
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;
|