react-three-game 0.0.59 → 0.0.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/tools/dragdrop/DragDropLoader.d.ts +8 -8
  2. package/dist/tools/dragdrop/DragDropLoader.js +33 -15
  3. package/dist/tools/dragdrop/index.d.ts +3 -3
  4. package/dist/tools/dragdrop/index.js +1 -1
  5. package/dist/tools/dragdrop/modelLoader.d.ts +10 -1
  6. package/dist/tools/dragdrop/modelLoader.js +39 -0
  7. package/dist/tools/prefabeditor/PrefabEditor.js +17 -26
  8. package/dist/tools/prefabeditor/PrefabRoot.d.ts +1 -1
  9. package/dist/tools/prefabeditor/PrefabRoot.js +2 -8
  10. package/package.json +9 -3
  11. package/.gitattributes +0 -2
  12. package/.github/copilot-instructions.md +0 -83
  13. package/.github/workflows/nextjs.yml +0 -99
  14. package/.gitmodules +0 -3
  15. package/assets/architecture.png +0 -0
  16. package/assets/editor.gif +0 -0
  17. package/assets/favicon.ico +0 -0
  18. package/assets/react-three-game-logo.png +0 -0
  19. package/dist/tools/dragdrop/page.d.ts +0 -1
  20. package/dist/tools/dragdrop/page.js +0 -11
  21. package/dist/tools/prefabeditor/EntityEvents.d.ts +0 -54
  22. package/dist/tools/prefabeditor/EntityEvents.js +0 -85
  23. package/dist/tools/prefabeditor/page.d.ts +0 -1
  24. package/dist/tools/prefabeditor/page.js +0 -5
  25. package/react-three-game-skill/.gitattributes +0 -2
  26. package/react-three-game-skill/README.md +0 -7
  27. package/react-three-game-skill/react-three-game/SKILL.md +0 -514
  28. package/react-three-game-skill/react-three-game/rules/ADVANCED_PHYSICS.md +0 -472
  29. package/react-three-game-skill/react-three-game/rules/LIGHTING.md +0 -6
  30. package/src/helpers/SoundManager.ts +0 -130
  31. package/src/helpers/index.ts +0 -91
  32. package/src/index.ts +0 -59
  33. package/src/shared/ContactShadow.tsx +0 -74
  34. package/src/shared/GameCanvas.tsx +0 -52
  35. package/src/tools/assetviewer/page.tsx +0 -425
  36. package/src/tools/dragdrop/DragDropLoader.tsx +0 -136
  37. package/src/tools/dragdrop/index.ts +0 -4
  38. package/src/tools/dragdrop/modelLoader.ts +0 -145
  39. package/src/tools/dragdrop/page.tsx +0 -45
  40. package/src/tools/prefabeditor/Dropdown.tsx +0 -112
  41. package/src/tools/prefabeditor/EditorContext.tsx +0 -25
  42. package/src/tools/prefabeditor/EditorTree.tsx +0 -452
  43. package/src/tools/prefabeditor/EditorTreeMenus.tsx +0 -307
  44. package/src/tools/prefabeditor/EditorUI.tsx +0 -204
  45. package/src/tools/prefabeditor/EventSystem.tsx +0 -36
  46. package/src/tools/prefabeditor/GameEvents.ts +0 -191
  47. package/src/tools/prefabeditor/InstanceProvider.tsx +0 -466
  48. package/src/tools/prefabeditor/PrefabEditor.tsx +0 -262
  49. package/src/tools/prefabeditor/PrefabRoot.tsx +0 -773
  50. package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +0 -34
  51. package/src/tools/prefabeditor/components/CameraComponent.tsx +0 -117
  52. package/src/tools/prefabeditor/components/ComponentRegistry.ts +0 -40
  53. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +0 -210
  54. package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +0 -47
  55. package/src/tools/prefabeditor/components/GeometryComponent.tsx +0 -133
  56. package/src/tools/prefabeditor/components/Input.tsx +0 -820
  57. package/src/tools/prefabeditor/components/MaterialComponent.tsx +0 -431
  58. package/src/tools/prefabeditor/components/ModelComponent.tsx +0 -176
  59. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +0 -188
  60. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +0 -109
  61. package/src/tools/prefabeditor/components/TextComponent.tsx +0 -137
  62. package/src/tools/prefabeditor/components/TransformComponent.tsx +0 -173
  63. package/src/tools/prefabeditor/components/index.ts +0 -26
  64. package/src/tools/prefabeditor/page.tsx +0 -10
  65. package/src/tools/prefabeditor/styles.ts +0 -235
  66. package/src/tools/prefabeditor/types.ts +0 -20
  67. package/src/tools/prefabeditor/utils.ts +0 -312
@@ -1,820 +0,0 @@
1
- import React, { useEffect, useRef, useState } from 'react';
2
- import { colors } from '../styles';
3
-
4
- // ============================================================================
5
- // Field Definition Types
6
- // ============================================================================
7
-
8
- export type FieldType = 'vector3' | 'number' | 'string' | 'color' | 'boolean' | 'select';
9
-
10
- interface BaseFieldDefinition {
11
- name: string;
12
- label: string;
13
- }
14
-
15
- interface Vector3FieldDefinition extends BaseFieldDefinition {
16
- type: 'vector3';
17
- snap?: number;
18
- }
19
-
20
- interface NumberFieldDefinition extends BaseFieldDefinition {
21
- type: 'number';
22
- min?: number;
23
- max?: number;
24
- step?: number;
25
- }
26
-
27
- interface StringFieldDefinition extends BaseFieldDefinition {
28
- type: 'string';
29
- placeholder?: string;
30
- }
31
-
32
- interface ColorFieldDefinition extends BaseFieldDefinition {
33
- type: 'color';
34
- }
35
-
36
- interface BooleanFieldDefinition extends BaseFieldDefinition {
37
- type: 'boolean';
38
- }
39
-
40
- interface SelectFieldDefinition extends BaseFieldDefinition {
41
- type: 'select';
42
- options: { value: string; label: string }[];
43
- }
44
-
45
- interface CustomFieldDefinition extends BaseFieldDefinition {
46
- type: 'custom';
47
- render: (props: {
48
- value: any;
49
- onChange: (value: any) => void;
50
- values: Record<string, any>;
51
- onChangeMultiple: (values: Record<string, any>) => void;
52
- }) => React.ReactNode;
53
- }
54
-
55
- export type FieldDefinition =
56
- | Vector3FieldDefinition
57
- | NumberFieldDefinition
58
- | StringFieldDefinition
59
- | ColorFieldDefinition
60
- | BooleanFieldDefinition
61
- | SelectFieldDefinition
62
- | CustomFieldDefinition;
63
-
64
- // ============================================================================
65
- // Shared Styles (derived from shared color tokens)
66
- // ============================================================================
67
-
68
- const styles = {
69
- input: {
70
- width: '80px',
71
- backgroundColor: colors.bgInput,
72
- border: `1px solid ${colors.border}`,
73
- padding: '3px 6px',
74
- fontSize: '11px',
75
- color: colors.text,
76
- fontFamily: 'monospace',
77
- outline: 'none',
78
- textAlign: 'right',
79
- borderRadius: 3,
80
- } as React.CSSProperties,
81
- label: {
82
- display: 'block',
83
- fontSize: '10px',
84
- color: colors.textMuted,
85
- textTransform: 'uppercase',
86
- letterSpacing: '0.05em',
87
- marginBottom: 2,
88
- fontWeight: 500,
89
- } as React.CSSProperties,
90
- };
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
-
116
- interface InputProps {
117
- value: number;
118
- onChange: (value: number) => void;
119
- step?: string | number;
120
- min?: number;
121
- max?: number;
122
- style?: React.CSSProperties;
123
- label?: string;
124
- }
125
-
126
- export function Input({ value, onChange, step, min, max, style, label }: InputProps) {
127
- const [draft, setDraft] = useState<string>(() => value.toString());
128
-
129
- useEffect(() => {
130
- setDraft(value.toString());
131
- }, [value]);
132
-
133
- const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
134
- const inputValue = e.target.value;
135
- setDraft(inputValue);
136
-
137
- const num = parseFloat(inputValue);
138
- if (Number.isFinite(num)) {
139
- onChange(num);
140
- }
141
- };
142
-
143
- const handleBlur = () => {
144
- const num = parseFloat(draft);
145
- if (!Number.isFinite(num)) {
146
- setDraft(value.toString());
147
- }
148
- };
149
-
150
- const dragState = useRef<{
151
- startX: number;
152
- startValue: number;
153
- } | null>(null);
154
-
155
- const startScrub = (e: React.PointerEvent) => {
156
- dragState.current = {
157
- startX: e.clientX,
158
- startValue: value
159
- };
160
-
161
- e.currentTarget.setPointerCapture(e.pointerId);
162
- document.body.style.cursor = "ew-resize";
163
- };
164
-
165
- const onScrubMove = (e: React.PointerEvent) => {
166
- if (!dragState.current) return;
167
-
168
- const { startX, startValue } = dragState.current;
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;
174
-
175
- const precision = getStepPrecision(scrubStep);
176
- const deltaSteps = Math.round(dx / 8);
177
- let nextValue = startValue + deltaSteps * scrubStep;
178
-
179
- // Apply min/max constraints
180
- if (min !== undefined && nextValue < min) nextValue = min;
181
- if (max !== undefined && nextValue > max) nextValue = max;
182
-
183
- setDraft(nextValue.toFixed(precision));
184
- onChange(nextValue);
185
- };
186
-
187
- const endScrub = (e: React.PointerEvent) => {
188
- if (!dragState.current) return;
189
-
190
- dragState.current = null;
191
- document.body.style.cursor = "";
192
- e.currentTarget.releasePointerCapture(e.pointerId);
193
- };
194
-
195
- if (label) {
196
- return (
197
- <div style={{
198
- display: 'flex',
199
- alignItems: 'center',
200
- justifyContent: 'space-between',
201
- }}>
202
- <span
203
- style={{
204
- ...styles.label,
205
- marginBottom: 0,
206
- userSelect: 'none',
207
- flex: '0 0 auto',
208
- minWidth: 20,
209
- }}
210
- >
211
- {label}
212
- </span>
213
- <input
214
- type="text"
215
- value={draft}
216
- onChange={handleChange}
217
- onBlur={handleBlur}
218
- onKeyDown={e => {
219
- if (e.key === 'Enter') {
220
- (e.target as HTMLInputElement).blur();
221
- }
222
- }}
223
- step={step}
224
- min={min}
225
- max={max}
226
- style={{ ...styles.input, cursor: 'ew-resize', ...style }}
227
- onPointerDown={startScrub}
228
- onPointerMove={onScrubMove}
229
- onPointerUp={endScrub}
230
- />
231
- </div>
232
- );
233
- }
234
-
235
- return (
236
- <input
237
- type="text"
238
- value={draft}
239
- onChange={handleChange}
240
- onBlur={handleBlur}
241
- onKeyDown={e => {
242
- if (e.key === 'Enter') {
243
- (e.target as HTMLInputElement).blur();
244
- }
245
- }}
246
- step={step}
247
- min={min}
248
- max={max}
249
- style={{ ...styles.input, cursor: 'ew-resize', ...style }}
250
- onPointerDown={startScrub}
251
- onPointerMove={onScrubMove}
252
- onPointerUp={endScrub}
253
- />
254
- );
255
- }
256
-
257
- export function Label({ children }: { children: React.ReactNode }) {
258
- return <label style={styles.label}>{children}</label>;
259
- }
260
-
261
- export function Vector3Input({
262
- label,
263
- value,
264
- onChange,
265
- snap,
266
- labelExtra
267
- }: {
268
- label: string;
269
- value: [number, number, number];
270
- onChange: (v: [number, number, number]) => void;
271
- snap?: number;
272
- labelExtra?: React.ReactNode;
273
- }) {
274
- const snapValue = (num: number) => {
275
- if (!snap) return num;
276
- return Math.round(num / snap) * snap;
277
- };
278
-
279
- const [draft, setDraft] = useState<[string, string, string]>(
280
- () => value.map(v => v.toString()) as any
281
- );
282
-
283
- // Sync external changes (gizmo, undo, etc.)
284
- useEffect(() => {
285
- setDraft(value.map(v => v.toString()) as any);
286
- }, [value[0], value[1], value[2]]);
287
-
288
- const dragState = useRef<{
289
- index: number;
290
- startX: number;
291
- startValue: number;
292
- } | null>(null);
293
-
294
- const commit = (index: number) => {
295
- const num = parseFloat(draft[index]);
296
- if (Number.isFinite(num)) {
297
- const next = [...value] as [number, number, number];
298
- next[index] = snapValue(num);
299
- onChange(next);
300
- }
301
- };
302
-
303
- const startScrub = (e: React.PointerEvent, index: number) => {
304
- dragState.current = {
305
- index,
306
- startX: e.clientX,
307
- startValue: value[index]
308
- };
309
-
310
- e.currentTarget.setPointerCapture(e.pointerId);
311
- document.body.style.cursor = "ew-resize";
312
- };
313
-
314
- const onScrubMove = (e: React.PointerEvent) => {
315
- if (!dragState.current) return;
316
-
317
- const { index, startX, startValue } = dragState.current;
318
- const dx = e.clientX - startX;
319
-
320
- let speed = 0.02;
321
- if (e.shiftKey) speed *= 0.1; // fine
322
- if (e.altKey) speed *= 5; // coarse
323
-
324
- const rawValue = startValue + dx * speed;
325
- const nextValue = snapValue(rawValue);
326
- const next = [...value] as [number, number, number];
327
- next[index] = nextValue;
328
-
329
- setDraft(d => {
330
- const copy = [...d] as any;
331
- copy[index] = nextValue.toFixed(3);
332
- return copy;
333
- });
334
-
335
- onChange(next);
336
- };
337
-
338
- const endScrub = (e: React.PointerEvent) => {
339
- if (!dragState.current) return;
340
-
341
- dragState.current = null;
342
- document.body.style.cursor = "";
343
- e.currentTarget.releasePointerCapture(e.pointerId);
344
- };
345
-
346
- const axes = [
347
- { key: "x", color: '#e06c75', index: 0 },
348
- { key: "y", color: '#98c379', index: 1 },
349
- { key: "z", color: '#61afef', index: 2 }
350
- ] as const;
351
-
352
- return (
353
- <div style={{ marginBottom: 8 }}>
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>
358
- <div style={{ display: 'flex', gap: 4 }}>
359
- {axes.map(({ key, color, index }) => (
360
- <div
361
- key={key}
362
- style={{
363
- flex: 1,
364
- display: 'flex',
365
- alignItems: 'center',
366
- gap: 4,
367
- backgroundColor: colors.bgInput,
368
- border: `1px solid ${colors.border}`,
369
- borderRadius: 3,
370
- padding: '4px 6px',
371
- minHeight: 28,
372
- cursor: 'ew-resize',
373
- }}
374
- onPointerDown={e => startScrub(e, index)}
375
- onPointerMove={onScrubMove}
376
- onPointerUp={endScrub}
377
- >
378
- <span
379
- style={{
380
- fontSize: 11,
381
- fontWeight: 600,
382
- color,
383
- width: 12,
384
- userSelect: 'none',
385
- }}
386
- >
387
- {key.toUpperCase()}
388
- </span>
389
- <input
390
- style={{
391
- flex: 1,
392
- backgroundColor: 'transparent',
393
- border: 'none',
394
- fontSize: 11,
395
- color: colors.text,
396
- fontFamily: 'monospace',
397
- outline: 'none',
398
- width: '100%',
399
- minWidth: 0,
400
- cursor: 'inherit',
401
- }}
402
- type="text"
403
- value={draft[index]}
404
- onChange={e => {
405
- const next = [...draft] as any;
406
- next[index] = e.target.value;
407
- setDraft(next);
408
- }}
409
- onBlur={() => commit(index)}
410
- onKeyDown={e => {
411
- if (e.key === "Enter") {
412
- (e.target as HTMLInputElement).blur();
413
- }
414
- }}
415
- />
416
- </div>
417
- ))}
418
- </div>
419
- </div>
420
- );
421
- }
422
-
423
- // ============================================================================
424
- // Additional Input Components
425
- // ============================================================================
426
-
427
- export function ColorInput({
428
- label,
429
- value,
430
- onChange
431
- }: {
432
- label?: string;
433
- value: string;
434
- onChange: (value: string) => void;
435
- }) {
436
- return (
437
- <div>
438
- {label && <Label>{label}</Label>}
439
- <div style={{ display: 'flex', gap: 4, justifyContent: 'space-between' }}>
440
- <input
441
- type="color"
442
- style={{
443
- height: 32,
444
- width: 48,
445
- backgroundColor: colors.bgInput,
446
- border: `1px solid ${colors.border}`,
447
- borderRadius: 3,
448
- cursor: 'pointer',
449
- padding: 2,
450
- flexShrink: 0,
451
- }}
452
- value={value}
453
- onChange={e => onChange(e.target.value)}
454
- />
455
- <input
456
- type="text"
457
- style={{ ...styles.input, }}
458
- value={value}
459
- onChange={e => onChange(e.target.value)}
460
- />
461
- </div>
462
- </div>
463
- );
464
- }
465
-
466
- export function StringInput({
467
- label,
468
- value,
469
- onChange,
470
- placeholder
471
- }: {
472
- label?: string;
473
- value: string;
474
- onChange: (value: string) => void;
475
- placeholder?: string;
476
- }) {
477
- return (
478
- <div>
479
- {label && <Label>{label}</Label>}
480
- <input
481
- type="text"
482
- style={styles.input}
483
- value={value}
484
- onChange={e => onChange(e.target.value)}
485
- placeholder={placeholder}
486
- />
487
- </div>
488
- );
489
- }
490
-
491
- export function BooleanInput({
492
- label,
493
- value,
494
- onChange
495
- }: {
496
- label?: string;
497
- value: boolean;
498
- onChange: (value: boolean) => void;
499
- }) {
500
- return (
501
- <div style={{ display: 'flex', justifyContent: 'space-between' }}>
502
- {label && <Label>{label}</Label>}
503
- <input
504
- type="checkbox"
505
- style={{
506
- height: 16,
507
- width: 16,
508
- accentColor: colors.accent,
509
- cursor: 'pointer',
510
- }}
511
- checked={value}
512
- onChange={e => onChange(e.target.checked)}
513
- />
514
- </div>
515
- );
516
- }
517
-
518
- export function SelectInput({
519
- label,
520
- value,
521
- onChange,
522
- options
523
- }: {
524
- label?: string;
525
- value: string;
526
- onChange: (value: string) => void;
527
- options: { value: string; label: string }[];
528
- }) {
529
- return (
530
- <div style={{ display: 'flex', justifyContent: 'space-between' }}>
531
- {label && <Label>{label}</Label>}
532
- <select
533
- style={styles.input as React.CSSProperties}
534
- value={value}
535
- onChange={e => onChange(e.target.value)}
536
- >
537
- {options.map(opt => (
538
- <option key={opt.value} value={opt.value}>
539
- {opt.label}
540
- </option>
541
- ))}
542
- </select>
543
- </div>
544
- );
545
- }
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
-
711
- // ============================================================================
712
- // Field Renderer - Schema-driven UI generation
713
- // ============================================================================
714
-
715
- interface FieldRendererProps {
716
- fields: FieldDefinition[];
717
- values: Record<string, any>;
718
- onChange: (values: Record<string, any>) => void;
719
- }
720
-
721
- export function FieldRenderer({ fields, values, onChange }: FieldRendererProps) {
722
- const updateField = (name: string, value: any) => {
723
- onChange({ [name]: value });
724
- };
725
-
726
- return (
727
- <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
728
- {fields.map(field => {
729
- const value = values[field.name];
730
-
731
- switch (field.type) {
732
- case 'vector3':
733
- return (
734
- <Vector3Input
735
- key={field.name}
736
- label={field.label}
737
- value={value ?? [0, 0, 0]}
738
- onChange={v => updateField(field.name, v)}
739
- snap={field.snap}
740
- />
741
- );
742
-
743
- case 'number':
744
- return (
745
- <Input
746
- key={field.name}
747
- label={field.label}
748
- value={value ?? 0}
749
- onChange={v => updateField(field.name, v)}
750
- min={field.min}
751
- max={field.max}
752
- step={field.step}
753
- />
754
- );
755
-
756
- case 'string':
757
- return (
758
- <StringInput
759
- key={field.name}
760
- label={field.label}
761
- value={value ?? ''}
762
- onChange={v => updateField(field.name, v)}
763
- placeholder={field.placeholder}
764
- />
765
- );
766
-
767
- case 'color':
768
- return (
769
- <ColorInput
770
- key={field.name}
771
- label={field.label}
772
- value={value ?? '#ffffff'}
773
- onChange={v => updateField(field.name, v)}
774
- />
775
- );
776
-
777
- case 'boolean':
778
- return (
779
- <BooleanInput
780
- key={field.name}
781
- label={field.label}
782
- value={value ?? false}
783
- onChange={v => updateField(field.name, v)}
784
- />
785
- );
786
-
787
- case 'select':
788
- return (
789
- <SelectInput
790
- key={field.name}
791
- label={field.label}
792
- value={value ?? field.options[0]?.value ?? ''}
793
- onChange={v => updateField(field.name, v)}
794
- options={field.options}
795
- />
796
- );
797
-
798
- case 'custom':
799
- return (
800
- <div key={field.name}>
801
- {field.label && <Label>{field.label}</Label>}
802
- {field.render({
803
- value,
804
- onChange: v => updateField(field.name, v),
805
- values,
806
- onChangeMultiple: onChange,
807
- })}
808
- </div>
809
- );
810
-
811
- default:
812
- return null;
813
- }
814
- })}
815
- </div>
816
- );
817
- }
818
-
819
- // Export styles for use in custom field renderers
820
- export { styles };