react-three-game 0.0.55 → 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/ContactShadow.d.ts +8 -0
- package/dist/shared/ContactShadow.js +32 -0
- package/dist/shared/GameCanvas.js +1 -3
- package/dist/tools/assetviewer/page.js +36 -15
- package/dist/tools/dragdrop/DragDropLoader.js +17 -40
- package/dist/tools/dragdrop/modelLoader.d.ts +5 -0
- package/dist/tools/dragdrop/modelLoader.js +39 -0
- 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 +139 -70
- package/dist/tools/prefabeditor/EditorUI.js +5 -10
- package/dist/tools/prefabeditor/PrefabEditor.d.ts +1 -0
- package/dist/tools/prefabeditor/PrefabEditor.js +70 -3
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +3 -0
- package/dist/tools/prefabeditor/PrefabRoot.js +136 -35
- 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 +100 -47
- package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +8 -2
- package/dist/tools/prefabeditor/components/MaterialComponent.js +129 -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 +6 -11
- package/dist/tools/prefabeditor/components/TextComponent.js +7 -53
- package/dist/tools/prefabeditor/components/TransformComponent.js +31 -19
- package/dist/tools/prefabeditor/components/index.js +5 -1
- package/dist/tools/prefabeditor/styles.d.ts +17 -4
- package/dist/tools/prefabeditor/styles.js +69 -32
- package/dist/tools/prefabeditor/utils.d.ts +8 -3
- package/dist/tools/prefabeditor/utils.js +92 -6
- package/package.json +1 -1
- package/react-three-game-skill/react-three-game/rules/LIGHTING.md +6 -0
- package/src/index.ts +7 -0
- package/src/shared/ContactShadow.tsx +74 -0
- package/src/shared/GameCanvas.tsx +0 -3
- package/src/tools/assetviewer/page.tsx +78 -46
- package/src/tools/dragdrop/DragDropLoader.tsx +7 -39
- package/src/tools/dragdrop/modelLoader.ts +36 -0
- package/src/tools/prefabeditor/Dropdown.tsx +112 -0
- package/src/tools/prefabeditor/EditorContext.tsx +5 -0
- package/src/tools/prefabeditor/EditorTree.tsx +237 -115
- package/src/tools/prefabeditor/EditorUI.tsx +6 -11
- package/src/tools/prefabeditor/PrefabEditor.tsx +77 -5
- package/src/tools/prefabeditor/PrefabRoot.tsx +228 -59
- 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 +247 -53
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +191 -20
- package/src/tools/prefabeditor/components/ModelComponent.tsx +52 -5
- package/src/tools/prefabeditor/components/PhysicsComponent.tsx +44 -85
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +14 -16
- package/src/tools/prefabeditor/components/TextComponent.tsx +58 -57
- package/src/tools/prefabeditor/components/TransformComponent.tsx +78 -20
- package/src/tools/prefabeditor/components/index.ts +5 -1
- package/src/tools/prefabeditor/styles.ts +71 -32
- package/src/tools/prefabeditor/utils.ts +96 -5
|
@@ -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,15 +1,17 @@
|
|
|
1
1
|
import { Component } from "./ComponentRegistry";
|
|
2
|
-
import {
|
|
2
|
+
import { Label, Vector3Field, Vector3Input } from "./Input";
|
|
3
3
|
import { useEditorContext } from "../EditorContext";
|
|
4
|
+
import { colors } from "../styles";
|
|
4
5
|
|
|
5
6
|
const buttonStyle = {
|
|
6
|
-
padding: '
|
|
7
|
-
background:
|
|
8
|
-
color:
|
|
9
|
-
border:
|
|
10
|
-
borderRadius:
|
|
7
|
+
padding: '4px 8px',
|
|
8
|
+
background: colors.bgSurface,
|
|
9
|
+
color: colors.text,
|
|
10
|
+
border: `1px solid ${colors.border}`,
|
|
11
|
+
borderRadius: 3,
|
|
11
12
|
cursor: 'pointer',
|
|
12
13
|
font: 'inherit',
|
|
14
|
+
fontSize: 11,
|
|
13
15
|
flex: 1,
|
|
14
16
|
};
|
|
15
17
|
|
|
@@ -36,13 +38,15 @@ function TransformModeSelector({
|
|
|
36
38
|
onClick={() => setTransformMode(mode as any)}
|
|
37
39
|
style={{
|
|
38
40
|
...buttonStyle,
|
|
39
|
-
background: isActive ?
|
|
41
|
+
background: isActive ? colors.accentBg : colors.bgSurface,
|
|
42
|
+
borderColor: isActive ? colors.accentBorder : colors.border,
|
|
43
|
+
color: isActive ? colors.accent : colors.text,
|
|
40
44
|
}}
|
|
41
45
|
onPointerEnter={(e) => {
|
|
42
|
-
if (!isActive) e.currentTarget.style.background =
|
|
46
|
+
if (!isActive) e.currentTarget.style.background = colors.bgHover;
|
|
43
47
|
}}
|
|
44
48
|
onPointerLeave={(e) => {
|
|
45
|
-
if (!isActive) e.currentTarget.style.background =
|
|
49
|
+
if (!isActive) e.currentTarget.style.background = colors.bgSurface;
|
|
46
50
|
}}
|
|
47
51
|
>
|
|
48
52
|
{mode}
|
|
@@ -55,14 +59,16 @@ function TransformModeSelector({
|
|
|
55
59
|
onClick={() => setSnapResolution(snapResolution > 0 ? 0 : 0.1)}
|
|
56
60
|
style={{
|
|
57
61
|
...buttonStyle,
|
|
58
|
-
background: snapResolution > 0 ?
|
|
62
|
+
background: snapResolution > 0 ? colors.accentBg : colors.bgSurface,
|
|
63
|
+
borderColor: snapResolution > 0 ? colors.accentBorder : colors.border,
|
|
64
|
+
color: snapResolution > 0 ? colors.accent : colors.text,
|
|
59
65
|
width: '100%',
|
|
60
66
|
}}
|
|
61
67
|
onPointerEnter={(e) => {
|
|
62
|
-
if (snapResolution === 0) e.currentTarget.style.background =
|
|
68
|
+
if (snapResolution === 0) e.currentTarget.style.background = colors.bgHover;
|
|
63
69
|
}}
|
|
64
70
|
onPointerLeave={(e) => {
|
|
65
|
-
if (snapResolution === 0) e.currentTarget.style.background =
|
|
71
|
+
if (snapResolution === 0) e.currentTarget.style.background = colors.bgSurface;
|
|
66
72
|
}}
|
|
67
73
|
>
|
|
68
74
|
Snap: {snapResolution > 0 ? `ON (${snapResolution})` : 'OFF'}
|
|
@@ -72,17 +78,41 @@ function TransformModeSelector({
|
|
|
72
78
|
);
|
|
73
79
|
}
|
|
74
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
|
+
|
|
75
99
|
function TransformComponentEditor({ component, onUpdate }: {
|
|
76
100
|
component: any;
|
|
77
101
|
onUpdate: (newComp: any) => void;
|
|
78
102
|
}) {
|
|
79
|
-
const {
|
|
103
|
+
const {
|
|
104
|
+
transformMode,
|
|
105
|
+
setTransformMode,
|
|
106
|
+
snapResolution,
|
|
107
|
+
setSnapResolution,
|
|
108
|
+
positionSnap,
|
|
109
|
+
setPositionSnap,
|
|
110
|
+
rotationSnap,
|
|
111
|
+
setRotationSnap
|
|
112
|
+
} = useEditorContext();
|
|
80
113
|
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
{ name: 'rotation', type: 'vector3', label: 'Rotation', snap: snapResolution },
|
|
84
|
-
{ name: 'scale', type: 'vector3', label: 'Scale', snap: snapResolution },
|
|
85
|
-
];
|
|
114
|
+
const positionSnapped = positionSnap > 0;
|
|
115
|
+
const rotationSnapped = rotationSnap > 0;
|
|
86
116
|
|
|
87
117
|
return (
|
|
88
118
|
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
|
@@ -92,10 +122,38 @@ function TransformComponentEditor({ component, onUpdate }: {
|
|
|
92
122
|
snapResolution={snapResolution}
|
|
93
123
|
setSnapResolution={setSnapResolution}
|
|
94
124
|
/>
|
|
95
|
-
<
|
|
96
|
-
|
|
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"
|
|
97
154
|
values={component.properties}
|
|
98
155
|
onChange={onUpdate}
|
|
156
|
+
fallback={[1, 1, 1]}
|
|
99
157
|
/>
|
|
100
158
|
</div>
|
|
101
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
|
|
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
// Shared editor styles - single source of truth for all prefab editor UI
|
|
2
2
|
|
|
3
3
|
export const colors = {
|
|
4
|
-
bg: '
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
4
|
+
bg: '#1e1e1e',
|
|
5
|
+
bgSurface: '#252526',
|
|
6
|
+
bgLight: '#2d2d2d',
|
|
7
|
+
bgHover: '#2a2d2e',
|
|
8
|
+
bgInput: '#1a1a1a',
|
|
9
|
+
border: '#3c3c3c',
|
|
10
|
+
borderLight: '#333333',
|
|
11
|
+
borderFaint: '#2a2a2a',
|
|
12
|
+
text: '#cccccc',
|
|
13
|
+
textMuted: '#999999',
|
|
14
|
+
textDim: '#666666',
|
|
15
|
+
accent: '#4c9eff',
|
|
16
|
+
accentBg: 'rgba(76, 158, 255, 0.12)',
|
|
17
|
+
accentBorder: 'rgba(76, 158, 255, 0.4)',
|
|
18
|
+
danger: '#f44747',
|
|
19
|
+
dangerBg: 'rgba(244, 71, 71, 0.12)',
|
|
20
|
+
dangerBorder: 'rgba(244, 71, 71, 0.35)',
|
|
15
21
|
};
|
|
16
22
|
|
|
17
23
|
export const fonts = {
|
|
18
|
-
family: 'system-ui, sans-serif',
|
|
24
|
+
family: 'system-ui, -apple-system, sans-serif',
|
|
19
25
|
size: 11,
|
|
20
26
|
sizeSm: 10,
|
|
21
27
|
};
|
|
@@ -27,13 +33,13 @@ export const base = {
|
|
|
27
33
|
color: colors.text,
|
|
28
34
|
border: `1px solid ${colors.border}`,
|
|
29
35
|
borderRadius: 4,
|
|
30
|
-
backdropFilter: 'blur(8px)',
|
|
31
36
|
fontFamily: fonts.family,
|
|
32
37
|
fontSize: fonts.size,
|
|
38
|
+
boxShadow: '0 2px 8px rgba(0,0,0,0.4)',
|
|
33
39
|
} as React.CSSProperties,
|
|
34
40
|
|
|
35
41
|
header: {
|
|
36
|
-
padding: '
|
|
42
|
+
padding: '7px 10px',
|
|
37
43
|
display: 'flex',
|
|
38
44
|
alignItems: 'center',
|
|
39
45
|
justifyContent: 'space-between',
|
|
@@ -41,24 +47,25 @@ export const base = {
|
|
|
41
47
|
background: colors.bgLight,
|
|
42
48
|
borderBottom: `1px solid ${colors.borderLight}`,
|
|
43
49
|
fontSize: fonts.size,
|
|
44
|
-
fontWeight:
|
|
50
|
+
fontWeight: 600,
|
|
45
51
|
textTransform: 'uppercase',
|
|
46
|
-
letterSpacing: 0.
|
|
52
|
+
letterSpacing: 0.8,
|
|
53
|
+
color: colors.text,
|
|
47
54
|
} as React.CSSProperties,
|
|
48
55
|
|
|
49
56
|
input: {
|
|
50
57
|
width: '100%',
|
|
51
|
-
background: colors.
|
|
58
|
+
background: colors.bgInput,
|
|
52
59
|
border: `1px solid ${colors.border}`,
|
|
53
60
|
borderRadius: 3,
|
|
54
|
-
padding: '
|
|
61
|
+
padding: '5px 8px',
|
|
55
62
|
color: colors.text,
|
|
56
63
|
fontSize: fonts.size,
|
|
57
64
|
outline: 'none',
|
|
58
65
|
} as React.CSSProperties,
|
|
59
66
|
|
|
60
67
|
btn: {
|
|
61
|
-
background: colors.
|
|
68
|
+
background: colors.bgLight,
|
|
62
69
|
border: `1px solid ${colors.border}`,
|
|
63
70
|
borderRadius: 3,
|
|
64
71
|
padding: '4px 8px',
|
|
@@ -76,10 +83,11 @@ export const base = {
|
|
|
76
83
|
|
|
77
84
|
label: {
|
|
78
85
|
fontSize: fonts.sizeSm,
|
|
79
|
-
|
|
86
|
+
color: colors.textMuted,
|
|
80
87
|
marginBottom: 4,
|
|
81
88
|
textTransform: 'uppercase',
|
|
82
89
|
letterSpacing: 0.5,
|
|
90
|
+
fontWeight: 500,
|
|
83
91
|
} as React.CSSProperties,
|
|
84
92
|
|
|
85
93
|
row: {
|
|
@@ -107,6 +115,8 @@ export const inspector = {
|
|
|
107
115
|
padding: 8,
|
|
108
116
|
maxHeight: '80vh',
|
|
109
117
|
overflowY: 'auto' as const,
|
|
118
|
+
overflowX: 'hidden' as const,
|
|
119
|
+
boxSizing: 'border-box' as const,
|
|
110
120
|
display: 'flex',
|
|
111
121
|
flexDirection: 'column' as const,
|
|
112
122
|
gap: 8,
|
|
@@ -125,18 +135,22 @@ export const tree = {
|
|
|
125
135
|
overflowY: 'auto' as const,
|
|
126
136
|
padding: 4,
|
|
127
137
|
scrollbarWidth: 'thin' as const,
|
|
128
|
-
scrollbarColor:
|
|
138
|
+
scrollbarColor: `${colors.bgLight} transparent`,
|
|
129
139
|
} as React.CSSProperties,
|
|
130
140
|
row: {
|
|
131
141
|
display: 'flex',
|
|
132
142
|
alignItems: 'center',
|
|
133
143
|
padding: '3px 6px',
|
|
134
|
-
|
|
144
|
+
borderBottomWidth: 1,
|
|
145
|
+
borderBottomStyle: 'solid',
|
|
146
|
+
borderBottomColor: colors.borderFaint,
|
|
135
147
|
cursor: 'pointer',
|
|
136
148
|
whiteSpace: 'nowrap' as const,
|
|
149
|
+
borderRadius: 2,
|
|
137
150
|
} as React.CSSProperties,
|
|
138
151
|
selected: {
|
|
139
|
-
background:
|
|
152
|
+
background: colors.accentBg,
|
|
153
|
+
borderBottomColor: colors.accentBorder,
|
|
140
154
|
},
|
|
141
155
|
};
|
|
142
156
|
|
|
@@ -144,22 +158,24 @@ export const menu = {
|
|
|
144
158
|
container: {
|
|
145
159
|
position: 'fixed' as const,
|
|
146
160
|
zIndex: 50,
|
|
147
|
-
minWidth:
|
|
148
|
-
|
|
161
|
+
minWidth: 'auto',
|
|
162
|
+
width: 'max-content',
|
|
163
|
+
maxWidth: 'min(240px, calc(100vw - 16px))',
|
|
164
|
+
background: colors.bgSurface,
|
|
149
165
|
border: `1px solid ${colors.border}`,
|
|
150
166
|
borderRadius: 4,
|
|
151
167
|
overflow: 'hidden',
|
|
152
|
-
boxShadow: '0
|
|
153
|
-
backdropFilter: 'blur(8px)',
|
|
168
|
+
boxShadow: '0 4px 16px rgba(0,0,0,0.6)',
|
|
154
169
|
},
|
|
155
170
|
item: {
|
|
156
171
|
width: '100%',
|
|
157
172
|
textAlign: 'left' as const,
|
|
158
|
-
padding: '
|
|
173
|
+
padding: '7px 12px',
|
|
159
174
|
background: 'transparent',
|
|
160
175
|
border: 'none',
|
|
161
176
|
color: colors.text,
|
|
162
177
|
fontSize: fonts.size,
|
|
178
|
+
whiteSpace: 'nowrap' as const,
|
|
163
179
|
cursor: 'pointer',
|
|
164
180
|
outline: 'none',
|
|
165
181
|
} as React.CSSProperties,
|
|
@@ -172,8 +188,7 @@ export const toolbar = {
|
|
|
172
188
|
panel: {
|
|
173
189
|
position: 'absolute' as const,
|
|
174
190
|
top: 8,
|
|
175
|
-
left: '
|
|
176
|
-
transform: 'translateX(-50%)',
|
|
191
|
+
left: '240px',
|
|
177
192
|
display: 'flex',
|
|
178
193
|
gap: 6,
|
|
179
194
|
padding: '4px 6px',
|
|
@@ -183,14 +198,38 @@ export const toolbar = {
|
|
|
183
198
|
color: colors.text,
|
|
184
199
|
fontFamily: fonts.family,
|
|
185
200
|
fontSize: fonts.size,
|
|
186
|
-
|
|
201
|
+
boxShadow: '0 2px 8px rgba(0,0,0,0.4)',
|
|
187
202
|
},
|
|
188
203
|
divider: {
|
|
189
204
|
width: 1,
|
|
190
|
-
background:
|
|
205
|
+
background: colors.borderLight,
|
|
191
206
|
},
|
|
192
207
|
disabled: {
|
|
193
208
|
opacity: 0.4,
|
|
194
209
|
cursor: 'not-allowed',
|
|
195
210
|
},
|
|
196
211
|
};
|
|
212
|
+
|
|
213
|
+
// Shared scrollbar CSS (inject via <style> tag since CSS can't be bundled)
|
|
214
|
+
export const scrollbarCSS = `
|
|
215
|
+
.prefab-scroll::-webkit-scrollbar,
|
|
216
|
+
.tree-scroll::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
217
|
+
.prefab-scroll::-webkit-scrollbar-track,
|
|
218
|
+
.tree-scroll::-webkit-scrollbar-track { background: transparent; }
|
|
219
|
+
.prefab-scroll::-webkit-scrollbar-thumb,
|
|
220
|
+
.tree-scroll::-webkit-scrollbar-thumb { background: ${colors.border}; border-radius: 3px; }
|
|
221
|
+
.prefab-scroll::-webkit-scrollbar-thumb:hover,
|
|
222
|
+
.tree-scroll::-webkit-scrollbar-thumb:hover { background: #555; }
|
|
223
|
+
.prefab-scroll { scrollbar-width: thin; scrollbar-color: ${colors.border} transparent; }
|
|
224
|
+
`;
|
|
225
|
+
|
|
226
|
+
// Reusable component card style for inspector sections
|
|
227
|
+
export const componentCard = {
|
|
228
|
+
container: {
|
|
229
|
+
marginBottom: 8,
|
|
230
|
+
backgroundColor: colors.bgSurface,
|
|
231
|
+
padding: 8,
|
|
232
|
+
borderRadius: 4,
|
|
233
|
+
border: `1px solid ${colors.border}`,
|
|
234
|
+
} as React.CSSProperties,
|
|
235
|
+
};
|
|
@@ -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;
|
|
@@ -171,7 +222,7 @@ export function cloneNode(node: GameObject): GameObject {
|
|
|
171
222
|
return {
|
|
172
223
|
...node,
|
|
173
224
|
id: crypto.randomUUID(),
|
|
174
|
-
name: `${node.name ??
|
|
225
|
+
name: `${node.name ?? node.id} Copy`,
|
|
175
226
|
children: node.children?.map(cloneNode)
|
|
176
227
|
};
|
|
177
228
|
}
|
|
@@ -219,3 +270,43 @@ export function updateNodeById(
|
|
|
219
270
|
children: newChildren
|
|
220
271
|
};
|
|
221
272
|
}
|
|
273
|
+
|
|
274
|
+
/** Create a GameObject node for a 3D model file */
|
|
275
|
+
export function createModelNode(filename: string, name?: string): GameObject {
|
|
276
|
+
return {
|
|
277
|
+
id: crypto.randomUUID(),
|
|
278
|
+
name: name ?? filename.replace(/^.*[\/]/, '').replace(/\.[^.]+$/, ''),
|
|
279
|
+
components: {
|
|
280
|
+
transform: {
|
|
281
|
+
type: 'Transform',
|
|
282
|
+
properties: { position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1] }
|
|
283
|
+
},
|
|
284
|
+
model: {
|
|
285
|
+
type: 'Model',
|
|
286
|
+
properties: { filename, instanced: false }
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Create a GameObject node for an image as a textured plane */
|
|
293
|
+
export function createImageNode(texturePath: string, name?: string): GameObject {
|
|
294
|
+
return {
|
|
295
|
+
id: crypto.randomUUID(),
|
|
296
|
+
name: name ?? texturePath.replace(/^.*[\/]/, '').replace(/\.[^.]+$/, ''),
|
|
297
|
+
components: {
|
|
298
|
+
transform: {
|
|
299
|
+
type: 'Transform',
|
|
300
|
+
properties: { position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1] }
|
|
301
|
+
},
|
|
302
|
+
geometry: {
|
|
303
|
+
type: 'Geometry',
|
|
304
|
+
properties: { geometryType: 'plane', args: [1, 1] }
|
|
305
|
+
},
|
|
306
|
+
material: {
|
|
307
|
+
type: 'Material',
|
|
308
|
+
properties: { color: '#ffffff', texture: texturePath }
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
}
|