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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Environment } from '@react-three/drei';
|
|
2
|
+
import { Component } from './ComponentRegistry';
|
|
3
|
+
import { FieldGroup, NumberField } from './Input';
|
|
4
|
+
import { Object3D, Texture } from 'three';
|
|
5
|
+
|
|
6
|
+
function EnvironmentView({
|
|
7
|
+
properties,
|
|
8
|
+
children,
|
|
9
|
+
editMode,
|
|
10
|
+
loadedTextures,
|
|
11
|
+
loadedModels,
|
|
12
|
+
}: {
|
|
13
|
+
properties: any;
|
|
14
|
+
children?: React.ReactNode;
|
|
15
|
+
editMode?: boolean;
|
|
16
|
+
loadedTextures?: Record<string, Texture>;
|
|
17
|
+
loadedModels?: Record<string, Object3D>;
|
|
18
|
+
}) {
|
|
19
|
+
const { intensity = 1, resolution = 256 } = properties;
|
|
20
|
+
const assetRevision = `${Object.keys(loadedTextures ?? {}).sort().join('|')}::${Object.keys(loadedModels ?? {}).sort().join('|')}`;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Environment
|
|
24
|
+
key={assetRevision}
|
|
25
|
+
background={true}
|
|
26
|
+
environmentIntensity={intensity}
|
|
27
|
+
resolution={resolution}
|
|
28
|
+
frames={editMode ? undefined : 1}
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
</Environment>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const EnvironmentComponent: Component = {
|
|
36
|
+
name: 'Environment',
|
|
37
|
+
Editor: ({ component, onUpdate }) => (
|
|
38
|
+
<FieldGroup>
|
|
39
|
+
<NumberField name="intensity" label="Intensity" values={component.properties} onChange={onUpdate} min={0} step={0.1} fallback={1} />
|
|
40
|
+
<NumberField name="resolution" label="Resolution" values={component.properties} onChange={onUpdate} min={64} step={64} fallback={256} />
|
|
41
|
+
</FieldGroup>
|
|
42
|
+
),
|
|
43
|
+
View: EnvironmentView,
|
|
44
|
+
defaultProperties: {},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export default EnvironmentComponent;
|
|
@@ -1,28 +1,49 @@
|
|
|
1
1
|
import { Component } from "./ComponentRegistry";
|
|
2
|
-
import {
|
|
2
|
+
import { FieldGroup, NumberField, SelectField } from "./Input";
|
|
3
3
|
|
|
4
4
|
const GEOMETRY_ARGS: Record<string, {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
fields: Array<{
|
|
6
|
+
name: string;
|
|
7
|
+
label: string;
|
|
8
|
+
defaultValue: number;
|
|
9
|
+
min?: number;
|
|
10
|
+
step?: number;
|
|
11
|
+
}>;
|
|
7
12
|
}> = {
|
|
8
13
|
box: {
|
|
9
|
-
|
|
10
|
-
|
|
14
|
+
fields: [
|
|
15
|
+
{ name: 'width', label: 'Width', defaultValue: 1, min: 0.01, step: 0.1 },
|
|
16
|
+
{ name: 'height', label: 'Height', defaultValue: 1, min: 0.01, step: 0.1 },
|
|
17
|
+
{ name: 'depth', label: 'Depth', defaultValue: 1, min: 0.01, step: 0.1 },
|
|
18
|
+
],
|
|
11
19
|
},
|
|
12
20
|
sphere: {
|
|
13
|
-
|
|
14
|
-
|
|
21
|
+
fields: [
|
|
22
|
+
{ name: 'radius', label: 'Radius', defaultValue: 1, min: 0.01, step: 0.1 },
|
|
23
|
+
{ name: 'widthSegments', label: 'Width Segments', defaultValue: 32, min: 3, step: 1 },
|
|
24
|
+
{ name: 'heightSegments', label: 'Height Segments', defaultValue: 16, min: 2, step: 1 },
|
|
25
|
+
],
|
|
15
26
|
},
|
|
16
27
|
plane: {
|
|
17
|
-
|
|
18
|
-
|
|
28
|
+
fields: [
|
|
29
|
+
{ name: 'width', label: 'Width', defaultValue: 1, min: 0.01, step: 0.1 },
|
|
30
|
+
{ name: 'height', label: 'Height', defaultValue: 1, min: 0.01, step: 0.1 },
|
|
31
|
+
],
|
|
19
32
|
},
|
|
20
33
|
cylinder: {
|
|
21
|
-
|
|
22
|
-
|
|
34
|
+
fields: [
|
|
35
|
+
{ name: 'radiusTop', label: 'Radius Top', defaultValue: 1, min: 0.01, step: 0.1 },
|
|
36
|
+
{ name: 'radiusBottom', label: 'Radius Bottom', defaultValue: 1, min: 0.01, step: 0.1 },
|
|
37
|
+
{ name: 'height', label: 'Height', defaultValue: 1, min: 0.01, step: 0.1 },
|
|
38
|
+
{ name: 'radialSegments', label: 'Radial Segments', defaultValue: 32, min: 3, step: 1 },
|
|
39
|
+
],
|
|
23
40
|
},
|
|
24
41
|
};
|
|
25
42
|
|
|
43
|
+
function getDefaultArgs(geometryType: string) {
|
|
44
|
+
return (GEOMETRY_ARGS[geometryType]?.fields ?? []).map(field => field.defaultValue);
|
|
45
|
+
}
|
|
46
|
+
|
|
26
47
|
function GeometryComponentEditor({
|
|
27
48
|
component,
|
|
28
49
|
onUpdate,
|
|
@@ -30,67 +51,52 @@ function GeometryComponentEditor({
|
|
|
30
51
|
component: any;
|
|
31
52
|
onUpdate: (newProps: any) => void;
|
|
32
53
|
}) {
|
|
33
|
-
const
|
|
34
|
-
const schema = GEOMETRY_ARGS[geometryType];
|
|
35
|
-
|
|
36
|
-
const fields: FieldDefinition[] = [
|
|
37
|
-
{
|
|
38
|
-
name: 'geometryType',
|
|
39
|
-
type: 'select',
|
|
40
|
-
label: 'Type',
|
|
41
|
-
options: [
|
|
42
|
-
{ value: 'box', label: 'Box' },
|
|
43
|
-
{ value: 'sphere', label: 'Sphere' },
|
|
44
|
-
{ value: 'plane', label: 'Plane' },
|
|
45
|
-
{ value: 'cylinder', label: 'Cylinder' },
|
|
46
|
-
],
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
name: 'args',
|
|
50
|
-
type: 'custom',
|
|
51
|
-
label: '',
|
|
52
|
-
render: ({ values, onChangeMultiple }) => {
|
|
53
|
-
const currentType = values.geometryType;
|
|
54
|
-
const currentSchema = GEOMETRY_ARGS[currentType];
|
|
55
|
-
const currentArgs = values.args || currentSchema.defaults;
|
|
56
|
-
|
|
57
|
-
return (
|
|
58
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
|
59
|
-
{currentSchema.labels.map((label, i) => (
|
|
60
|
-
<Input
|
|
61
|
-
key={label}
|
|
62
|
-
label={label}
|
|
63
|
-
value={currentArgs[i] ?? currentSchema.defaults[i]}
|
|
64
|
-
step={0.1}
|
|
65
|
-
min={0.01}
|
|
66
|
-
onChange={value => {
|
|
67
|
-
const next = [...currentArgs];
|
|
68
|
-
next[i] = value;
|
|
69
|
-
onChangeMultiple({ args: next });
|
|
70
|
-
}}
|
|
71
|
-
/>
|
|
72
|
-
))}
|
|
73
|
-
</div>
|
|
74
|
-
);
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
];
|
|
54
|
+
const geometryType = component.properties.geometryType ?? 'box';
|
|
55
|
+
const schema = GEOMETRY_ARGS[geometryType] ?? GEOMETRY_ARGS.box;
|
|
56
|
+
const args = component.properties.args ?? getDefaultArgs(geometryType);
|
|
78
57
|
|
|
79
58
|
// Handle geometry type change to reset args
|
|
80
59
|
const handleChange = (newValues: Record<string, any>) => {
|
|
81
60
|
if ('geometryType' in newValues && newValues.geometryType !== geometryType) {
|
|
82
|
-
onUpdate({ geometryType: newValues.geometryType, args:
|
|
61
|
+
onUpdate({ geometryType: newValues.geometryType, args: getDefaultArgs(newValues.geometryType) });
|
|
83
62
|
} else {
|
|
84
63
|
onUpdate(newValues);
|
|
85
64
|
}
|
|
86
65
|
};
|
|
87
66
|
|
|
67
|
+
const updateArg = (index: number, value: number) => {
|
|
68
|
+
const next = [...args];
|
|
69
|
+
next[index] = value;
|
|
70
|
+
onUpdate({ args: next });
|
|
71
|
+
};
|
|
72
|
+
|
|
88
73
|
return (
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
74
|
+
<FieldGroup>
|
|
75
|
+
<SelectField
|
|
76
|
+
name="geometryType"
|
|
77
|
+
label="Type"
|
|
78
|
+
values={component.properties}
|
|
79
|
+
onChange={handleChange}
|
|
80
|
+
options={[
|
|
81
|
+
{ value: 'box', label: 'Box' },
|
|
82
|
+
{ value: 'sphere', label: 'Sphere' },
|
|
83
|
+
{ value: 'plane', label: 'Plane' },
|
|
84
|
+
{ value: 'cylinder', label: 'Cylinder' },
|
|
85
|
+
]}
|
|
86
|
+
/>
|
|
87
|
+
{schema.fields.map((field, index) => (
|
|
88
|
+
<NumberField
|
|
89
|
+
key={field.name}
|
|
90
|
+
name={field.name}
|
|
91
|
+
label={field.label}
|
|
92
|
+
values={{ [field.name]: args[index] ?? field.defaultValue }}
|
|
93
|
+
onChange={(next) => updateArg(index, next[field.name])}
|
|
94
|
+
fallback={field.defaultValue}
|
|
95
|
+
min={field.min}
|
|
96
|
+
step={field.step}
|
|
97
|
+
/>
|
|
98
|
+
))}
|
|
99
|
+
</FieldGroup>
|
|
94
100
|
);
|
|
95
101
|
}
|
|
96
102
|
|
|
@@ -120,7 +126,7 @@ const GeometryComponent: Component = {
|
|
|
120
126
|
nonComposable: true,
|
|
121
127
|
defaultProperties: {
|
|
122
128
|
geometryType: 'box',
|
|
123
|
-
args:
|
|
129
|
+
args: getDefaultArgs('box'),
|
|
124
130
|
}
|
|
125
131
|
};
|
|
126
132
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { colors } from '../styles';
|
|
2
3
|
|
|
3
4
|
// ============================================================================
|
|
4
5
|
// Field Definition Types
|
|
@@ -61,32 +62,57 @@ export type FieldDefinition =
|
|
|
61
62
|
| CustomFieldDefinition;
|
|
62
63
|
|
|
63
64
|
// ============================================================================
|
|
64
|
-
// Shared Styles
|
|
65
|
+
// Shared Styles (derived from shared color tokens)
|
|
65
66
|
// ============================================================================
|
|
66
67
|
|
|
67
|
-
// Shared styles
|
|
68
68
|
const styles = {
|
|
69
69
|
input: {
|
|
70
70
|
width: '80px',
|
|
71
|
-
backgroundColor:
|
|
72
|
-
border:
|
|
73
|
-
padding: '
|
|
74
|
-
fontSize: '
|
|
75
|
-
color:
|
|
71
|
+
backgroundColor: colors.bgInput,
|
|
72
|
+
border: `1px solid ${colors.border}`,
|
|
73
|
+
padding: '3px 6px',
|
|
74
|
+
fontSize: '11px',
|
|
75
|
+
color: colors.text,
|
|
76
76
|
fontFamily: 'monospace',
|
|
77
77
|
outline: 'none',
|
|
78
78
|
textAlign: 'right',
|
|
79
|
+
borderRadius: 3,
|
|
79
80
|
} as React.CSSProperties,
|
|
80
81
|
label: {
|
|
81
82
|
display: 'block',
|
|
82
|
-
fontSize: '
|
|
83
|
-
color:
|
|
83
|
+
fontSize: '10px',
|
|
84
|
+
color: colors.textMuted,
|
|
84
85
|
textTransform: 'uppercase',
|
|
85
86
|
letterSpacing: '0.05em',
|
|
86
87
|
marginBottom: 2,
|
|
88
|
+
fontWeight: 500,
|
|
87
89
|
} as React.CSSProperties,
|
|
88
90
|
};
|
|
89
91
|
|
|
92
|
+
function getNumericStep(step: string | number | undefined, fallback: number) {
|
|
93
|
+
if (typeof step === 'number' && Number.isFinite(step) && step > 0) return step;
|
|
94
|
+
|
|
95
|
+
if (typeof step === 'string') {
|
|
96
|
+
const parsed = parseFloat(step);
|
|
97
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return fallback;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getStepPrecision(step: number) {
|
|
104
|
+
if (!Number.isFinite(step) || step <= 0) return 3;
|
|
105
|
+
|
|
106
|
+
const stepString = step.toString();
|
|
107
|
+
if (stepString.includes('e-')) {
|
|
108
|
+
const exponent = stepString.split('e-')[1];
|
|
109
|
+
return exponent ? parseInt(exponent, 10) : 3;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const decimal = stepString.split('.')[1];
|
|
113
|
+
return decimal?.length ?? 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
90
116
|
interface InputProps {
|
|
91
117
|
value: number;
|
|
92
118
|
onChange: (value: number) => void;
|
|
@@ -127,15 +153,12 @@ export function Input({ value, onChange, step, min, max, style, label }: InputPr
|
|
|
127
153
|
} | null>(null);
|
|
128
154
|
|
|
129
155
|
const startScrub = (e: React.PointerEvent) => {
|
|
130
|
-
if (!label) return;
|
|
131
|
-
e.preventDefault();
|
|
132
|
-
|
|
133
156
|
dragState.current = {
|
|
134
157
|
startX: e.clientX,
|
|
135
158
|
startValue: value
|
|
136
159
|
};
|
|
137
160
|
|
|
138
|
-
|
|
161
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
139
162
|
document.body.style.cursor = "ew-resize";
|
|
140
163
|
};
|
|
141
164
|
|
|
@@ -144,18 +167,20 @@ export function Input({ value, onChange, step, min, max, style, label }: InputPr
|
|
|
144
167
|
|
|
145
168
|
const { startX, startValue } = dragState.current;
|
|
146
169
|
const dx = e.clientX - startX;
|
|
170
|
+
const baseStep = getNumericStep(step, 0.1);
|
|
171
|
+
let scrubStep = baseStep;
|
|
172
|
+
if (e.shiftKey) scrubStep /= 10;
|
|
173
|
+
if (e.altKey) scrubStep *= 10;
|
|
147
174
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
let nextValue = startValue + dx * speed;
|
|
175
|
+
const precision = getStepPrecision(scrubStep);
|
|
176
|
+
const deltaSteps = Math.round(dx / 8);
|
|
177
|
+
let nextValue = startValue + deltaSteps * scrubStep;
|
|
153
178
|
|
|
154
179
|
// Apply min/max constraints
|
|
155
180
|
if (min !== undefined && nextValue < min) nextValue = min;
|
|
156
181
|
if (max !== undefined && nextValue > max) nextValue = max;
|
|
157
182
|
|
|
158
|
-
setDraft(nextValue.toFixed(
|
|
183
|
+
setDraft(nextValue.toFixed(precision));
|
|
159
184
|
onChange(nextValue);
|
|
160
185
|
};
|
|
161
186
|
|
|
@@ -164,7 +189,7 @@ export function Input({ value, onChange, step, min, max, style, label }: InputPr
|
|
|
164
189
|
|
|
165
190
|
dragState.current = null;
|
|
166
191
|
document.body.style.cursor = "";
|
|
167
|
-
|
|
192
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
168
193
|
};
|
|
169
194
|
|
|
170
195
|
if (label) {
|
|
@@ -178,14 +203,10 @@ export function Input({ value, onChange, step, min, max, style, label }: InputPr
|
|
|
178
203
|
style={{
|
|
179
204
|
...styles.label,
|
|
180
205
|
marginBottom: 0,
|
|
181
|
-
cursor: 'ew-resize',
|
|
182
206
|
userSelect: 'none',
|
|
183
207
|
flex: '0 0 auto',
|
|
184
208
|
minWidth: 20,
|
|
185
209
|
}}
|
|
186
|
-
onPointerDown={startScrub}
|
|
187
|
-
onPointerMove={onScrubMove}
|
|
188
|
-
onPointerUp={endScrub}
|
|
189
210
|
>
|
|
190
211
|
{label}
|
|
191
212
|
</span>
|
|
@@ -202,7 +223,10 @@ export function Input({ value, onChange, step, min, max, style, label }: InputPr
|
|
|
202
223
|
step={step}
|
|
203
224
|
min={min}
|
|
204
225
|
max={max}
|
|
205
|
-
style={{ ...styles.input, ...style }}
|
|
226
|
+
style={{ ...styles.input, cursor: 'ew-resize', ...style }}
|
|
227
|
+
onPointerDown={startScrub}
|
|
228
|
+
onPointerMove={onScrubMove}
|
|
229
|
+
onPointerUp={endScrub}
|
|
206
230
|
/>
|
|
207
231
|
</div>
|
|
208
232
|
);
|
|
@@ -222,7 +246,10 @@ export function Input({ value, onChange, step, min, max, style, label }: InputPr
|
|
|
222
246
|
step={step}
|
|
223
247
|
min={min}
|
|
224
248
|
max={max}
|
|
225
|
-
style={{ ...styles.input, ...style }}
|
|
249
|
+
style={{ ...styles.input, cursor: 'ew-resize', ...style }}
|
|
250
|
+
onPointerDown={startScrub}
|
|
251
|
+
onPointerMove={onScrubMove}
|
|
252
|
+
onPointerUp={endScrub}
|
|
226
253
|
/>
|
|
227
254
|
);
|
|
228
255
|
}
|
|
@@ -235,12 +262,14 @@ export function Vector3Input({
|
|
|
235
262
|
label,
|
|
236
263
|
value,
|
|
237
264
|
onChange,
|
|
238
|
-
snap
|
|
265
|
+
snap,
|
|
266
|
+
labelExtra
|
|
239
267
|
}: {
|
|
240
268
|
label: string;
|
|
241
269
|
value: [number, number, number];
|
|
242
270
|
onChange: (v: [number, number, number]) => void;
|
|
243
271
|
snap?: number;
|
|
272
|
+
labelExtra?: React.ReactNode;
|
|
244
273
|
}) {
|
|
245
274
|
const snapValue = (num: number) => {
|
|
246
275
|
if (!snap) return num;
|
|
@@ -272,15 +301,13 @@ export function Vector3Input({
|
|
|
272
301
|
};
|
|
273
302
|
|
|
274
303
|
const startScrub = (e: React.PointerEvent, index: number) => {
|
|
275
|
-
e.preventDefault();
|
|
276
|
-
|
|
277
304
|
dragState.current = {
|
|
278
305
|
index,
|
|
279
306
|
startX: e.clientX,
|
|
280
307
|
startValue: value[index]
|
|
281
308
|
};
|
|
282
309
|
|
|
283
|
-
|
|
310
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
284
311
|
document.body.style.cursor = "ew-resize";
|
|
285
312
|
};
|
|
286
313
|
|
|
@@ -313,18 +340,21 @@ export function Vector3Input({
|
|
|
313
340
|
|
|
314
341
|
dragState.current = null;
|
|
315
342
|
document.body.style.cursor = "";
|
|
316
|
-
|
|
343
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
317
344
|
};
|
|
318
345
|
|
|
319
346
|
const axes = [
|
|
320
|
-
{ key: "x", color: '
|
|
321
|
-
{ key: "y", color: '
|
|
322
|
-
{ key: "z", color: '
|
|
347
|
+
{ key: "x", color: '#e06c75', index: 0 },
|
|
348
|
+
{ key: "y", color: '#98c379', index: 1 },
|
|
349
|
+
{ key: "z", color: '#61afef', index: 2 }
|
|
323
350
|
] as const;
|
|
324
351
|
|
|
325
352
|
return (
|
|
326
353
|
<div style={{ marginBottom: 8 }}>
|
|
327
|
-
<
|
|
354
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 }}>
|
|
355
|
+
<label style={{ ...styles.label, marginBottom: 0 }}>{label}</label>
|
|
356
|
+
{labelExtra}
|
|
357
|
+
</div>
|
|
328
358
|
<div style={{ display: 'flex', gap: 4 }}>
|
|
329
359
|
{axes.map(({ key, color, index }) => (
|
|
330
360
|
<div
|
|
@@ -334,25 +364,25 @@ export function Vector3Input({
|
|
|
334
364
|
display: 'flex',
|
|
335
365
|
alignItems: 'center',
|
|
336
366
|
gap: 4,
|
|
337
|
-
backgroundColor:
|
|
338
|
-
border:
|
|
339
|
-
borderRadius:
|
|
367
|
+
backgroundColor: colors.bgInput,
|
|
368
|
+
border: `1px solid ${colors.border}`,
|
|
369
|
+
borderRadius: 3,
|
|
340
370
|
padding: '4px 6px',
|
|
341
|
-
minHeight:
|
|
371
|
+
minHeight: 28,
|
|
372
|
+
cursor: 'ew-resize',
|
|
342
373
|
}}
|
|
374
|
+
onPointerDown={e => startScrub(e, index)}
|
|
375
|
+
onPointerMove={onScrubMove}
|
|
376
|
+
onPointerUp={endScrub}
|
|
343
377
|
>
|
|
344
378
|
<span
|
|
345
379
|
style={{
|
|
346
|
-
fontSize:
|
|
347
|
-
fontWeight:
|
|
380
|
+
fontSize: 11,
|
|
381
|
+
fontWeight: 600,
|
|
348
382
|
color,
|
|
349
383
|
width: 12,
|
|
350
|
-
cursor: 'ew-resize',
|
|
351
384
|
userSelect: 'none',
|
|
352
385
|
}}
|
|
353
|
-
onPointerDown={e => startScrub(e, index)}
|
|
354
|
-
onPointerMove={onScrubMove}
|
|
355
|
-
onPointerUp={endScrub}
|
|
356
386
|
>
|
|
357
387
|
{key.toUpperCase()}
|
|
358
388
|
</span>
|
|
@@ -361,12 +391,13 @@ export function Vector3Input({
|
|
|
361
391
|
flex: 1,
|
|
362
392
|
backgroundColor: 'transparent',
|
|
363
393
|
border: 'none',
|
|
364
|
-
fontSize:
|
|
365
|
-
color:
|
|
394
|
+
fontSize: 11,
|
|
395
|
+
color: colors.text,
|
|
366
396
|
fontFamily: 'monospace',
|
|
367
397
|
outline: 'none',
|
|
368
398
|
width: '100%',
|
|
369
399
|
minWidth: 0,
|
|
400
|
+
cursor: 'inherit',
|
|
370
401
|
}}
|
|
371
402
|
type="text"
|
|
372
403
|
value={draft[index]}
|
|
@@ -411,11 +442,11 @@ export function ColorInput({
|
|
|
411
442
|
style={{
|
|
412
443
|
height: 32,
|
|
413
444
|
width: 48,
|
|
414
|
-
backgroundColor:
|
|
415
|
-
border:
|
|
416
|
-
borderRadius:
|
|
445
|
+
backgroundColor: colors.bgInput,
|
|
446
|
+
border: `1px solid ${colors.border}`,
|
|
447
|
+
borderRadius: 3,
|
|
417
448
|
cursor: 'pointer',
|
|
418
|
-
padding:
|
|
449
|
+
padding: 2,
|
|
419
450
|
flexShrink: 0,
|
|
420
451
|
}}
|
|
421
452
|
value={value}
|
|
@@ -474,8 +505,7 @@ export function BooleanInput({
|
|
|
474
505
|
style={{
|
|
475
506
|
height: 16,
|
|
476
507
|
width: 16,
|
|
477
|
-
|
|
478
|
-
border: '1px solid rgba(34, 211, 238, 0.3)',
|
|
508
|
+
accentColor: colors.accent,
|
|
479
509
|
cursor: 'pointer',
|
|
480
510
|
}}
|
|
481
511
|
checked={value}
|
|
@@ -514,6 +544,170 @@ export function SelectInput({
|
|
|
514
544
|
);
|
|
515
545
|
}
|
|
516
546
|
|
|
547
|
+
interface BoundFieldProps {
|
|
548
|
+
name: string;
|
|
549
|
+
values: Record<string, any>;
|
|
550
|
+
onChange: (values: Record<string, any>) => void;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
interface BoundNumberFieldProps extends BoundFieldProps {
|
|
554
|
+
label: string;
|
|
555
|
+
fallback?: number;
|
|
556
|
+
step?: string | number;
|
|
557
|
+
min?: number;
|
|
558
|
+
max?: number;
|
|
559
|
+
style?: React.CSSProperties;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
interface BoundStringFieldProps extends BoundFieldProps {
|
|
563
|
+
label: string;
|
|
564
|
+
fallback?: string;
|
|
565
|
+
placeholder?: string;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
interface BoundColorFieldProps extends BoundFieldProps {
|
|
569
|
+
label: string;
|
|
570
|
+
fallback?: string;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
interface BoundBooleanFieldProps extends BoundFieldProps {
|
|
574
|
+
label: string;
|
|
575
|
+
fallback?: boolean;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
interface BoundSelectFieldProps extends BoundFieldProps {
|
|
579
|
+
label: string;
|
|
580
|
+
fallback?: string;
|
|
581
|
+
options: { value: string; label: string }[];
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
interface BoundVector3FieldProps extends BoundFieldProps {
|
|
585
|
+
label: string;
|
|
586
|
+
fallback?: [number, number, number];
|
|
587
|
+
snap?: number;
|
|
588
|
+
labelExtra?: React.ReactNode;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function bindFieldChange(name: string, onChange: (values: Record<string, any>) => void) {
|
|
592
|
+
return (value: any) => onChange({ [name]: value });
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
export function FieldGroup({ children }: { children: React.ReactNode }) {
|
|
596
|
+
return <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>{children}</div>;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
export function NumberField({
|
|
600
|
+
name,
|
|
601
|
+
label,
|
|
602
|
+
values,
|
|
603
|
+
onChange,
|
|
604
|
+
fallback = 0,
|
|
605
|
+
step,
|
|
606
|
+
min,
|
|
607
|
+
max,
|
|
608
|
+
style,
|
|
609
|
+
}: BoundNumberFieldProps) {
|
|
610
|
+
return (
|
|
611
|
+
<Input
|
|
612
|
+
label={label}
|
|
613
|
+
value={values[name] ?? fallback}
|
|
614
|
+
onChange={bindFieldChange(name, onChange)}
|
|
615
|
+
step={step}
|
|
616
|
+
min={min}
|
|
617
|
+
max={max}
|
|
618
|
+
style={style}
|
|
619
|
+
/>
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export function StringField({
|
|
624
|
+
name,
|
|
625
|
+
label,
|
|
626
|
+
values,
|
|
627
|
+
onChange,
|
|
628
|
+
fallback = '',
|
|
629
|
+
placeholder,
|
|
630
|
+
}: BoundStringFieldProps) {
|
|
631
|
+
return (
|
|
632
|
+
<StringInput
|
|
633
|
+
label={label}
|
|
634
|
+
value={values[name] ?? fallback}
|
|
635
|
+
onChange={bindFieldChange(name, onChange)}
|
|
636
|
+
placeholder={placeholder}
|
|
637
|
+
/>
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
export function ColorField({
|
|
642
|
+
name,
|
|
643
|
+
label,
|
|
644
|
+
values,
|
|
645
|
+
onChange,
|
|
646
|
+
fallback = '#ffffff',
|
|
647
|
+
}: BoundColorFieldProps) {
|
|
648
|
+
return (
|
|
649
|
+
<ColorInput
|
|
650
|
+
label={label}
|
|
651
|
+
value={values[name] ?? fallback}
|
|
652
|
+
onChange={bindFieldChange(name, onChange)}
|
|
653
|
+
/>
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
export function BooleanField({
|
|
658
|
+
name,
|
|
659
|
+
label,
|
|
660
|
+
values,
|
|
661
|
+
onChange,
|
|
662
|
+
fallback = false,
|
|
663
|
+
}: BoundBooleanFieldProps) {
|
|
664
|
+
return (
|
|
665
|
+
<BooleanInput
|
|
666
|
+
label={label}
|
|
667
|
+
value={values[name] ?? fallback}
|
|
668
|
+
onChange={bindFieldChange(name, onChange)}
|
|
669
|
+
/>
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
export function SelectField({
|
|
674
|
+
name,
|
|
675
|
+
label,
|
|
676
|
+
values,
|
|
677
|
+
onChange,
|
|
678
|
+
fallback,
|
|
679
|
+
options,
|
|
680
|
+
}: BoundSelectFieldProps) {
|
|
681
|
+
return (
|
|
682
|
+
<SelectInput
|
|
683
|
+
label={label}
|
|
684
|
+
value={values[name] ?? fallback ?? options[0]?.value ?? ''}
|
|
685
|
+
onChange={bindFieldChange(name, onChange)}
|
|
686
|
+
options={options}
|
|
687
|
+
/>
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
export function Vector3Field({
|
|
692
|
+
name,
|
|
693
|
+
label,
|
|
694
|
+
values,
|
|
695
|
+
onChange,
|
|
696
|
+
fallback = [0, 0, 0],
|
|
697
|
+
snap,
|
|
698
|
+
labelExtra,
|
|
699
|
+
}: BoundVector3FieldProps) {
|
|
700
|
+
return (
|
|
701
|
+
<Vector3Input
|
|
702
|
+
label={label}
|
|
703
|
+
value={values[name] ?? fallback}
|
|
704
|
+
onChange={bindFieldChange(name, onChange)}
|
|
705
|
+
snap={snap}
|
|
706
|
+
labelExtra={labelExtra}
|
|
707
|
+
/>
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
|
|
517
711
|
// ============================================================================
|
|
518
712
|
// Field Renderer - Schema-driven UI generation
|
|
519
713
|
// ============================================================================
|