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,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
|
|
|
@@ -89,6 +89,30 @@ const styles = {
|
|
|
89
89
|
} as React.CSSProperties,
|
|
90
90
|
};
|
|
91
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
|
+
|
|
92
116
|
interface InputProps {
|
|
93
117
|
value: number;
|
|
94
118
|
onChange: (value: number) => void;
|
|
@@ -129,15 +153,12 @@ export function Input({ value, onChange, step, min, max, style, label }: InputPr
|
|
|
129
153
|
} | null>(null);
|
|
130
154
|
|
|
131
155
|
const startScrub = (e: React.PointerEvent) => {
|
|
132
|
-
if (!label) return;
|
|
133
|
-
e.preventDefault();
|
|
134
|
-
|
|
135
156
|
dragState.current = {
|
|
136
157
|
startX: e.clientX,
|
|
137
158
|
startValue: value
|
|
138
159
|
};
|
|
139
160
|
|
|
140
|
-
|
|
161
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
141
162
|
document.body.style.cursor = "ew-resize";
|
|
142
163
|
};
|
|
143
164
|
|
|
@@ -146,18 +167,20 @@ export function Input({ value, onChange, step, min, max, style, label }: InputPr
|
|
|
146
167
|
|
|
147
168
|
const { startX, startValue } = dragState.current;
|
|
148
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;
|
|
149
174
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
let nextValue = startValue + dx * speed;
|
|
175
|
+
const precision = getStepPrecision(scrubStep);
|
|
176
|
+
const deltaSteps = Math.round(dx / 8);
|
|
177
|
+
let nextValue = startValue + deltaSteps * scrubStep;
|
|
155
178
|
|
|
156
179
|
// Apply min/max constraints
|
|
157
180
|
if (min !== undefined && nextValue < min) nextValue = min;
|
|
158
181
|
if (max !== undefined && nextValue > max) nextValue = max;
|
|
159
182
|
|
|
160
|
-
setDraft(nextValue.toFixed(
|
|
183
|
+
setDraft(nextValue.toFixed(precision));
|
|
161
184
|
onChange(nextValue);
|
|
162
185
|
};
|
|
163
186
|
|
|
@@ -166,7 +189,7 @@ export function Input({ value, onChange, step, min, max, style, label }: InputPr
|
|
|
166
189
|
|
|
167
190
|
dragState.current = null;
|
|
168
191
|
document.body.style.cursor = "";
|
|
169
|
-
|
|
192
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
170
193
|
};
|
|
171
194
|
|
|
172
195
|
if (label) {
|
|
@@ -180,14 +203,10 @@ export function Input({ value, onChange, step, min, max, style, label }: InputPr
|
|
|
180
203
|
style={{
|
|
181
204
|
...styles.label,
|
|
182
205
|
marginBottom: 0,
|
|
183
|
-
cursor: 'ew-resize',
|
|
184
206
|
userSelect: 'none',
|
|
185
207
|
flex: '0 0 auto',
|
|
186
208
|
minWidth: 20,
|
|
187
209
|
}}
|
|
188
|
-
onPointerDown={startScrub}
|
|
189
|
-
onPointerMove={onScrubMove}
|
|
190
|
-
onPointerUp={endScrub}
|
|
191
210
|
>
|
|
192
211
|
{label}
|
|
193
212
|
</span>
|
|
@@ -204,7 +223,10 @@ export function Input({ value, onChange, step, min, max, style, label }: InputPr
|
|
|
204
223
|
step={step}
|
|
205
224
|
min={min}
|
|
206
225
|
max={max}
|
|
207
|
-
style={{ ...styles.input, ...style }}
|
|
226
|
+
style={{ ...styles.input, cursor: 'ew-resize', ...style }}
|
|
227
|
+
onPointerDown={startScrub}
|
|
228
|
+
onPointerMove={onScrubMove}
|
|
229
|
+
onPointerUp={endScrub}
|
|
208
230
|
/>
|
|
209
231
|
</div>
|
|
210
232
|
);
|
|
@@ -224,7 +246,10 @@ export function Input({ value, onChange, step, min, max, style, label }: InputPr
|
|
|
224
246
|
step={step}
|
|
225
247
|
min={min}
|
|
226
248
|
max={max}
|
|
227
|
-
style={{ ...styles.input, ...style }}
|
|
249
|
+
style={{ ...styles.input, cursor: 'ew-resize', ...style }}
|
|
250
|
+
onPointerDown={startScrub}
|
|
251
|
+
onPointerMove={onScrubMove}
|
|
252
|
+
onPointerUp={endScrub}
|
|
228
253
|
/>
|
|
229
254
|
);
|
|
230
255
|
}
|
|
@@ -237,12 +262,14 @@ export function Vector3Input({
|
|
|
237
262
|
label,
|
|
238
263
|
value,
|
|
239
264
|
onChange,
|
|
240
|
-
snap
|
|
265
|
+
snap,
|
|
266
|
+
labelExtra
|
|
241
267
|
}: {
|
|
242
268
|
label: string;
|
|
243
269
|
value: [number, number, number];
|
|
244
270
|
onChange: (v: [number, number, number]) => void;
|
|
245
271
|
snap?: number;
|
|
272
|
+
labelExtra?: React.ReactNode;
|
|
246
273
|
}) {
|
|
247
274
|
const snapValue = (num: number) => {
|
|
248
275
|
if (!snap) return num;
|
|
@@ -274,15 +301,13 @@ export function Vector3Input({
|
|
|
274
301
|
};
|
|
275
302
|
|
|
276
303
|
const startScrub = (e: React.PointerEvent, index: number) => {
|
|
277
|
-
e.preventDefault();
|
|
278
|
-
|
|
279
304
|
dragState.current = {
|
|
280
305
|
index,
|
|
281
306
|
startX: e.clientX,
|
|
282
307
|
startValue: value[index]
|
|
283
308
|
};
|
|
284
309
|
|
|
285
|
-
|
|
310
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
286
311
|
document.body.style.cursor = "ew-resize";
|
|
287
312
|
};
|
|
288
313
|
|
|
@@ -315,7 +340,7 @@ export function Vector3Input({
|
|
|
315
340
|
|
|
316
341
|
dragState.current = null;
|
|
317
342
|
document.body.style.cursor = "";
|
|
318
|
-
|
|
343
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
319
344
|
};
|
|
320
345
|
|
|
321
346
|
const axes = [
|
|
@@ -326,7 +351,10 @@ export function Vector3Input({
|
|
|
326
351
|
|
|
327
352
|
return (
|
|
328
353
|
<div style={{ marginBottom: 8 }}>
|
|
329
|
-
<
|
|
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>
|
|
330
358
|
<div style={{ display: 'flex', gap: 4 }}>
|
|
331
359
|
{axes.map(({ key, color, index }) => (
|
|
332
360
|
<div
|
|
@@ -341,7 +369,11 @@ export function Vector3Input({
|
|
|
341
369
|
borderRadius: 3,
|
|
342
370
|
padding: '4px 6px',
|
|
343
371
|
minHeight: 28,
|
|
372
|
+
cursor: 'ew-resize',
|
|
344
373
|
}}
|
|
374
|
+
onPointerDown={e => startScrub(e, index)}
|
|
375
|
+
onPointerMove={onScrubMove}
|
|
376
|
+
onPointerUp={endScrub}
|
|
345
377
|
>
|
|
346
378
|
<span
|
|
347
379
|
style={{
|
|
@@ -349,12 +381,8 @@ export function Vector3Input({
|
|
|
349
381
|
fontWeight: 600,
|
|
350
382
|
color,
|
|
351
383
|
width: 12,
|
|
352
|
-
cursor: 'ew-resize',
|
|
353
384
|
userSelect: 'none',
|
|
354
385
|
}}
|
|
355
|
-
onPointerDown={e => startScrub(e, index)}
|
|
356
|
-
onPointerMove={onScrubMove}
|
|
357
|
-
onPointerUp={endScrub}
|
|
358
386
|
>
|
|
359
387
|
{key.toUpperCase()}
|
|
360
388
|
</span>
|
|
@@ -369,6 +397,7 @@ export function Vector3Input({
|
|
|
369
397
|
outline: 'none',
|
|
370
398
|
width: '100%',
|
|
371
399
|
minWidth: 0,
|
|
400
|
+
cursor: 'inherit',
|
|
372
401
|
}}
|
|
373
402
|
type="text"
|
|
374
403
|
value={draft[index]}
|
|
@@ -515,6 +544,170 @@ export function SelectInput({
|
|
|
515
544
|
);
|
|
516
545
|
}
|
|
517
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
|
+
|
|
518
711
|
// ============================================================================
|
|
519
712
|
// Field Renderer - Schema-driven UI generation
|
|
520
713
|
// ============================================================================
|