react-three-game 0.0.44 → 0.0.46

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 (41) hide show
  1. package/README.md +38 -4
  2. package/assets/architecture.png +0 -0
  3. package/dist/index.d.ts +7 -6
  4. package/dist/index.js +9 -6
  5. package/dist/tools/assetviewer/page.js +1 -1
  6. package/dist/tools/prefabeditor/EditorUI.js +3 -5
  7. package/dist/tools/prefabeditor/PrefabRoot.js +8 -3
  8. package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +2 -2
  9. package/dist/tools/prefabeditor/components/ComponentRegistry.js +5 -0
  10. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +27 -27
  11. package/dist/tools/prefabeditor/components/GeometryComponent.js +41 -21
  12. package/dist/tools/prefabeditor/components/Input.d.ts +78 -1
  13. package/dist/tools/prefabeditor/components/Input.js +65 -0
  14. package/dist/tools/prefabeditor/components/MaterialComponent.js +60 -28
  15. package/dist/tools/prefabeditor/components/ModelComponent.js +18 -8
  16. package/dist/tools/prefabeditor/components/PhysicsComponent.js +26 -14
  17. package/dist/tools/prefabeditor/components/SpotLightComponent.js +10 -21
  18. package/dist/tools/prefabeditor/components/TextComponent.d.ts +3 -0
  19. package/dist/tools/prefabeditor/components/TextComponent.js +103 -0
  20. package/dist/tools/prefabeditor/components/TransformComponent.js +28 -19
  21. package/dist/tools/prefabeditor/components/index.js +3 -1
  22. package/dist/tools/prefabeditor/page.js +1 -1
  23. package/package.json +3 -2
  24. package/skill/SKILL.md +491 -0
  25. package/skill/package.json +17 -0
  26. package/src/index.ts +28 -10
  27. package/src/tools/assetviewer/page.tsx +2 -0
  28. package/src/tools/prefabeditor/EditorUI.tsx +0 -10
  29. package/src/tools/prefabeditor/PrefabRoot.tsx +11 -2
  30. package/src/tools/prefabeditor/components/ComponentRegistry.ts +11 -5
  31. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +72 -76
  32. package/src/tools/prefabeditor/components/GeometryComponent.tsx +56 -38
  33. package/src/tools/prefabeditor/components/Input.tsx +299 -0
  34. package/src/tools/prefabeditor/components/MaterialComponent.tsx +98 -142
  35. package/src/tools/prefabeditor/components/ModelComponent.tsx +63 -41
  36. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +30 -33
  37. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +17 -65
  38. package/src/tools/prefabeditor/components/TextComponent.tsx +136 -0
  39. package/src/tools/prefabeditor/components/TransformComponent.tsx +84 -56
  40. package/src/tools/prefabeditor/components/index.ts +3 -1
  41. package/src/tools/prefabeditor/page.tsx +1 -1
package/README.md CHANGED
@@ -72,21 +72,29 @@ interface GameObject {
72
72
  ## Custom Components
73
73
 
74
74
  ```tsx
75
- import { Component, registerComponent } from 'react-three-game';
75
+ import { Component, registerComponent, FieldRenderer, FieldDefinition } from 'react-three-game';
76
76
  import { useFrame } from '@react-three/fiber';
77
77
 
78
+ const rotatorFields: FieldDefinition[] = [
79
+ { name: 'speed', type: 'number', label: 'Speed', step: 0.1 },
80
+ { name: 'axis', type: 'select', label: 'Axis', options: [
81
+ { value: 'x', label: 'X' },
82
+ { value: 'y', label: 'Y' },
83
+ { value: 'z', label: 'Z' },
84
+ ]},
85
+ ];
86
+
78
87
  const Rotator: Component = {
79
88
  name: 'Rotator',
80
89
  Editor: ({ component, onUpdate }) => (
81
- <input type="number" value={component.properties.speed}
82
- onChange={e => onUpdate({ speed: +e.target.value })} />
90
+ <FieldRenderer fields={rotatorFields} values={component.properties} onChange={onUpdate} />
83
91
  ),
84
92
  View: ({ properties, children }) => {
85
93
  const ref = useRef<Group>(null);
86
94
  useFrame((_, dt) => { ref.current!.rotation.y += dt * properties.speed });
87
95
  return <group ref={ref}>{children}</group>;
88
96
  },
89
- defaultProperties: { speed: 1 }
97
+ defaultProperties: { speed: 1, axis: 'y' }
90
98
  };
91
99
 
92
100
  registerComponent(Rotator); // before rendering PrefabEditor
@@ -94,6 +102,32 @@ registerComponent(Rotator); // before rendering PrefabEditor
94
102
 
95
103
  **Wrapper** components accept `children` (animations, controllers). **Leaf** components don't (lights, particles).
96
104
 
105
+ ### Schema-Driven Field Types
106
+
107
+ The `FieldRenderer` component auto-generates editor UI from a field schema:
108
+
109
+ | Type | Description | Options |
110
+ |------|-------------|---------|
111
+ | `vector3` | X/Y/Z inputs with drag-to-scrub | `snap?: number` |
112
+ | `number` | Numeric input | `min?`, `max?`, `step?` |
113
+ | `string` | Text input | `placeholder?` |
114
+ | `color` | Color picker + hex input | — |
115
+ | `boolean` | Checkbox | — |
116
+ | `select` | Dropdown | `options: { value, label }[]` |
117
+ | `custom` | Render function for one-off UI | `render: (props) => ReactNode` |
118
+
119
+ ```tsx
120
+ // Custom field example for complex one-off UI
121
+ {
122
+ name: 'gradient',
123
+ type: 'custom',
124
+ label: 'Gradient',
125
+ render: ({ value, onChange, values, onChangeMultiple }) => (
126
+ <GradientPicker value={value} onChange={onChange} />
127
+ ),
128
+ }
129
+ ```
130
+
97
131
  ## Visual Editor
98
132
 
99
133
  ```jsx
Binary file
package/dist/index.d.ts CHANGED
@@ -1,14 +1,15 @@
1
1
  export { default as GameCanvas } from './shared/GameCanvas';
2
+ export * from './helpers';
3
+ export { sound as soundManager } from './helpers/SoundManager';
2
4
  export { default as PrefabEditor } from './tools/prefabeditor/PrefabEditor';
3
- export type { PrefabEditorRef } from './tools/prefabeditor/PrefabEditor';
4
5
  export { default as PrefabRoot } from './tools/prefabeditor/PrefabRoot';
5
- export type { PrefabRootRef } from './tools/prefabeditor/PrefabRoot';
6
6
  export { registerComponent } from './tools/prefabeditor/components/ComponentRegistry';
7
+ export { FieldRenderer, Input, Label, Vector3Input, ColorInput, StringInput, BooleanInput, SelectInput, } from './tools/prefabeditor/components/Input';
8
+ export * from './tools/prefabeditor/utils';
9
+ export type { PrefabEditorRef } from './tools/prefabeditor/PrefabEditor';
10
+ export type { PrefabRootRef } from './tools/prefabeditor/PrefabRoot';
7
11
  export type { Component } from './tools/prefabeditor/components/ComponentRegistry';
12
+ export type { FieldDefinition, FieldType } from './tools/prefabeditor/components/Input';
8
13
  export type { Prefab, GameObject, ComponentData } from './tools/prefabeditor/types';
9
- export * as editorStyles from './tools/prefabeditor/styles';
10
- export * from './tools/prefabeditor/utils';
11
14
  export { DragDropLoader } from './tools/dragdrop/DragDropLoader';
12
15
  export { TextureListViewer, ModelListViewer, SoundListViewer, SharedCanvas, } from './tools/assetviewer/page';
13
- export { sound as soundManager } from './helpers/SoundManager';
14
- export * from './helpers';
package/dist/index.js CHANGED
@@ -1,14 +1,17 @@
1
- // Core Components
1
+ // Core
2
2
  export { default as GameCanvas } from './shared/GameCanvas';
3
- // Prefab Editor
3
+ // Helpers
4
+ export * from './helpers';
5
+ export { sound as soundManager } from './helpers/SoundManager';
6
+ // Prefab Editor - Components
4
7
  export { default as PrefabEditor } from './tools/prefabeditor/PrefabEditor';
5
8
  export { default as PrefabRoot } from './tools/prefabeditor/PrefabRoot';
9
+ // Prefab Editor - Component Registry
6
10
  export { registerComponent } from './tools/prefabeditor/components/ComponentRegistry';
7
- export * as editorStyles from './tools/prefabeditor/styles';
11
+ // Prefab Editor - Input Components
12
+ export { FieldRenderer, Input, Label, Vector3Input, ColorInput, StringInput, BooleanInput, SelectInput, } from './tools/prefabeditor/components/Input';
13
+ // Prefab Editor - Styles & Utils
8
14
  export * from './tools/prefabeditor/utils';
9
15
  // Asset Tools
10
16
  export { DragDropLoader } from './tools/dragdrop/DragDropLoader';
11
17
  export { TextureListViewer, ModelListViewer, SoundListViewer, SharedCanvas, } from './tools/assetviewer/page';
12
- export { sound as soundManager } from './helpers/SoundManager';
13
- // Helpers
14
- export * from './helpers';
@@ -103,7 +103,7 @@ function ModelCard({ file, onSelect, basePath = "" }) {
103
103
  if (error) {
104
104
  return (_jsx("div", { ref: ref, style: { aspectRatio: '1 / 1', backgroundColor: '#374151', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }, onClick: () => onSelect(file), children: _jsx("div", { style: styles.errorIcon, children: "\u2717" }) }));
105
105
  }
106
- return (_jsxs("div", { ref: ref, style: { maxWidth: 60, aspectRatio: '1 / 1', backgroundColor: '#111827', cursor: 'pointer', display: 'flex', flexDirection: 'column' }, onClick: () => onSelect(file), children: [_jsx("div", { style: styles.flexFillRelative, children: isInView ? (_jsxs(View, { style: { width: '100%', height: '100%' }, children: [_jsx(PerspectiveCamera, { makeDefault: true, position: [0, 1, 3], fov: 50 }), _jsxs(Suspense, { fallback: null, children: [_jsx(ModelPreview, { url: fullPath, onError: () => setError(true) }), _jsx(OrbitControls, { enableZoom: false })] })] })) : null }), _jsx("div", { style: { backgroundColor: 'rgba(0,0,0,0.6)', fontSize: 10, padding: '0 4px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center' }, children: file.split('/').pop() })] }));
106
+ return (_jsxs("div", { ref: ref, style: { maxWidth: 60, aspectRatio: '1 / 1', backgroundColor: '#111827', cursor: 'pointer', display: 'flex', flexDirection: 'column' }, onClick: () => onSelect(file), children: [_jsx("div", { style: styles.flexFillRelative, children: isInView ? (_jsxs(View, { style: { width: '100%', height: '100%' }, children: [_jsx(PerspectiveCamera, { makeDefault: true, position: [0, 1, 3], fov: 50 }), _jsxs(Suspense, { fallback: null, children: [_jsx("ambientLight", { intensity: 1 }), _jsx("pointLight", { position: [5, 5, 5], intensity: 0.5 }), _jsx(ModelPreview, { url: fullPath, onError: () => setError(true) }), _jsx(OrbitControls, { enableZoom: false })] })] })) : null }), _jsx("div", { style: { backgroundColor: 'rgba(0,0,0,0.6)', fontSize: 10, padding: '0 4px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center' }, children: file.split('/').pop() })] }));
107
107
  }
108
108
  function ModelPreview({ url, onError }) {
109
109
  const [model, setModel] = useState(null);
@@ -15,10 +15,8 @@ import EditorTree from './EditorTree';
15
15
  import { getAllComponents } from './components/ComponentRegistry';
16
16
  import { base, inspector } from './styles';
17
17
  import { findNode, updateNode, deleteNode } from './utils';
18
- import { useEditorContext } from './EditorContext';
19
18
  function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, basePath, onUndo, onRedo, canUndo, canRedo }) {
20
19
  const [collapsed, setCollapsed] = useState(false);
21
- const { transformMode, setTransformMode } = useEditorContext();
22
20
  const updateNodeHandler = (updater) => {
23
21
  if (!prefabData || !setPrefabData || !selectedId)
24
22
  return;
@@ -36,9 +34,9 @@ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, basePa
36
34
  .prefab-scroll::-webkit-scrollbar-track { background: transparent; }
37
35
  .prefab-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.06); border-radius: 8px; }
38
36
  .prefab-scroll { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.06) transparent; }
39
- ` }), _jsxs("div", { style: inspector.panel, children: [_jsxs("div", { style: base.header, onClick: () => setCollapsed(!collapsed), children: [_jsx("span", { children: "Inspector" }), _jsx("span", { children: collapsed ? '◀' : '▼' })] }), !collapsed && selectedNode && (_jsx(NodeInspector, { node: selectedNode, updateNode: updateNodeHandler, deleteNode: deleteNodeHandler, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath }))] }), _jsx("div", { style: { position: 'absolute', top: 8, left: 8, zIndex: 20 }, children: _jsx(EditorTree, { prefabData: prefabData, setPrefabData: setPrefabData, selectedId: selectedId, setSelectedId: setSelectedId, onUndo: onUndo, onRedo: onRedo, canUndo: canUndo, canRedo: canRedo }) })] });
37
+ ` }), _jsxs("div", { style: inspector.panel, children: [_jsxs("div", { style: base.header, onClick: () => setCollapsed(!collapsed), children: [_jsx("span", { children: "Inspector" }), _jsx("span", { children: collapsed ? '◀' : '▼' })] }), !collapsed && selectedNode && (_jsx(NodeInspector, { node: selectedNode, updateNode: updateNodeHandler, deleteNode: deleteNodeHandler, basePath: basePath }))] }), _jsx("div", { style: { position: 'absolute', top: 8, left: 8, zIndex: 20 }, children: _jsx(EditorTree, { prefabData: prefabData, setPrefabData: setPrefabData, selectedId: selectedId, setSelectedId: setSelectedId, onUndo: onUndo, onRedo: onRedo, canUndo: canUndo, canRedo: canRedo }) })] });
40
38
  }
41
- function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransformMode, basePath }) {
39
+ function NodeInspector({ node, updateNode, deleteNode, basePath }) {
42
40
  var _a;
43
41
  const ALL_COMPONENTS = getAllComponents();
44
42
  const allKeys = Object.keys(ALL_COMPONENTS);
@@ -58,7 +56,7 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
58
56
  return (_jsxs("div", { style: { marginBottom: 8 }, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }, children: [_jsx("div", { style: { fontSize: 11, fontWeight: 500 }, children: key }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px' }), title: "Remove Component", onClick: () => updateNode(n => {
59
57
  const _a = n.components || {}, _b = key, _ = _a[_b], rest = __rest(_a, [typeof _b === "symbol" ? _b : _b + ""]);
60
58
  return Object.assign(Object.assign({}, n), { components: rest });
61
- }), children: "\u2715" })] }), def.Editor && (_jsx(def.Editor, { component: comp, node: node, onUpdate: (newProps) => updateNode(n => (Object.assign(Object.assign({}, n), { components: Object.assign(Object.assign({}, n.components), { [key]: Object.assign(Object.assign({}, comp), { properties: Object.assign(Object.assign({}, comp.properties), newProps) }) }) }))), basePath: basePath, transformMode: transformMode, setTransformMode: setTransformMode }))] }, key));
59
+ }), children: "\u2715" })] }), def.Editor && (_jsx(def.Editor, { component: comp, node: node, onUpdate: (newProps) => updateNode(n => (Object.assign(Object.assign({}, n), { components: Object.assign(Object.assign({}, n.components), { [key]: Object.assign(Object.assign({}, comp), { properties: Object.assign(Object.assign({}, comp.properties), newProps) }) }) }))), basePath: basePath }))] }, key));
62
60
  })] }), available.length > 0 && (_jsx("div", { children: _jsxs("div", { style: base.row, children: [_jsx("select", { style: Object.assign(Object.assign({}, base.input), { flex: 1 }), value: addType, onChange: e => setAddType(e.target.value), children: available.map(k => _jsx("option", { value: k, children: k }, k)) }), _jsx("button", { style: base.btn, disabled: !addType, onClick: () => {
63
61
  if (!addType)
64
62
  return;
@@ -11,7 +11,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
11
11
  import { MapControls, TransformControls, useHelper } from "@react-three/drei";
12
12
  import { forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useRef, useState } from "react";
13
13
  import { BoxHelper, Euler, Matrix4, Quaternion, SRGBColorSpace, TextureLoader, Vector3, } from "three";
14
- import { getComponent, registerComponent } from "./components/ComponentRegistry";
14
+ import { getComponent, registerComponent, getNonComposableKeys } from "./components/ComponentRegistry";
15
15
  import components from "./components";
16
16
  import { loadModel } from "../dragdrop/modelLoader";
17
17
  import { GameInstance, GameInstanceProvider, useInstanceCheck } from "./InstanceProvider";
@@ -245,13 +245,15 @@ function computeParentWorldMatrix(root, targetId) {
245
245
  return result !== null && result !== void 0 ? result : IDENTITY;
246
246
  }
247
247
  function renderCoreNode(gameObject, ctx, parentMatrix) {
248
- var _a, _b, _c;
248
+ var _a, _b, _c, _d;
249
249
  const geometry = (_a = gameObject.components) === null || _a === void 0 ? void 0 : _a.geometry;
250
250
  const material = (_b = gameObject.components) === null || _b === void 0 ? void 0 : _b.material;
251
251
  const model = (_c = gameObject.components) === null || _c === void 0 ? void 0 : _c.model;
252
+ const text = (_d = gameObject.components) === null || _d === void 0 ? void 0 : _d.text;
252
253
  const geometryDef = geometry && getComponent("Geometry");
253
254
  const materialDef = material && getComponent("Material");
254
255
  const modelDef = model && getComponent("Model");
256
+ const textDef = text && getComponent("Text");
255
257
  const contextProps = {
256
258
  loadedModels: ctx.loadedModels,
257
259
  loadedTextures: ctx.loadedTextures,
@@ -263,7 +265,7 @@ function renderCoreNode(gameObject, ctx, parentMatrix) {
263
265
  const leaves = [];
264
266
  if (gameObject.components) {
265
267
  Object.entries(gameObject.components)
266
- .filter(([k]) => !["geometry", "material", "model", "transform", "physics"].includes(k))
268
+ .filter(([k]) => !getNonComposableKeys().includes(k))
267
269
  .forEach(([key, comp]) => {
268
270
  if (!(comp === null || comp === void 0 ? void 0 : comp.type))
269
271
  return;
@@ -285,6 +287,9 @@ function renderCoreNode(gameObject, ctx, parentMatrix) {
285
287
  else if (geometry && (geometryDef === null || geometryDef === void 0 ? void 0 : geometryDef.View)) {
286
288
  core = (_jsxs("mesh", { castShadow: true, receiveShadow: true, children: [_jsx(geometryDef.View, Object.assign({ properties: geometry.properties }, contextProps)), material && (materialDef === null || materialDef === void 0 ? void 0 : materialDef.View) && (_jsx(materialDef.View, Object.assign({ properties: material.properties }, contextProps), "material")), leaves] }));
287
289
  }
290
+ else if (text && (textDef === null || textDef === void 0 ? void 0 : textDef.View)) {
291
+ core = (_jsxs(_Fragment, { children: [_jsx(textDef.View, Object.assign({ properties: text.properties }, contextProps)), leaves] }));
292
+ }
288
293
  else {
289
294
  core = _jsx(_Fragment, { children: leaves });
290
295
  }
@@ -7,12 +7,12 @@ export interface Component {
7
7
  component: ComponentData;
8
8
  onUpdate: (newComp: any) => void;
9
9
  basePath?: string;
10
- transformMode?: "translate" | "rotate" | "scale";
11
- setTransformMode?: (m: "translate" | "rotate" | "scale") => void;
12
10
  }>;
13
11
  defaultProperties: any;
14
12
  View?: FC<any>;
13
+ nonComposable?: boolean;
15
14
  }
16
15
  export declare function registerComponent(component: Component): void;
17
16
  export declare function getComponent(name: string): Component | undefined;
18
17
  export declare function getAllComponents(): Record<string, Component>;
18
+ export declare function getNonComposableKeys(): string[];
@@ -11,3 +11,8 @@ export function getComponent(name) {
11
11
  export function getAllComponents() {
12
12
  return Object.assign({}, REGISTRY);
13
13
  }
14
+ export function getNonComposableKeys() {
15
+ return Object.values(REGISTRY)
16
+ .filter(c => c.nonComposable)
17
+ .map(c => c.name.toLowerCase());
18
+ }
@@ -2,34 +2,34 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useRef, useEffect } from "react";
3
3
  import { useFrame } from "@react-three/fiber";
4
4
  import { Vector3 } from "three";
5
- import { Input, Label } from "./Input";
5
+ import { FieldRenderer, Input } from "./Input";
6
+ const smallLabel = { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 };
7
+ const directionalLightFields = [
8
+ { name: 'color', type: 'color', label: 'Color' },
9
+ { name: 'intensity', type: 'number', label: 'Intensity', step: 0.1, min: 0 },
10
+ { name: 'castShadow', type: 'boolean', label: 'Cast Shadow' },
11
+ { name: 'shadowMapSize', type: 'number', label: 'Shadow Map Size', step: 256, min: 256 },
12
+ {
13
+ name: '_shadowCamera',
14
+ type: 'custom',
15
+ label: 'Shadow Camera',
16
+ render: ({ values, onChangeMultiple }) => {
17
+ var _a, _b, _c, _d, _e, _f;
18
+ return (_jsxs("div", { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4 }, children: [_jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Near" }), _jsx(Input, { step: 0.1, value: (_a = values.shadowCameraNear) !== null && _a !== void 0 ? _a : 0.1, onChange: v => onChangeMultiple({ shadowCameraNear: v }) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Far" }), _jsx(Input, { step: 1, value: (_b = values.shadowCameraFar) !== null && _b !== void 0 ? _b : 100, onChange: v => onChangeMultiple({ shadowCameraFar: v }) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Top" }), _jsx(Input, { step: 1, value: (_c = values.shadowCameraTop) !== null && _c !== void 0 ? _c : 30, onChange: v => onChangeMultiple({ shadowCameraTop: v }) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Bottom" }), _jsx(Input, { step: 1, value: (_d = values.shadowCameraBottom) !== null && _d !== void 0 ? _d : -30, onChange: v => onChangeMultiple({ shadowCameraBottom: v }) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Left" }), _jsx(Input, { step: 1, value: (_e = values.shadowCameraLeft) !== null && _e !== void 0 ? _e : -30, onChange: v => onChangeMultiple({ shadowCameraLeft: v }) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Right" }), _jsx(Input, { step: 1, value: (_f = values.shadowCameraRight) !== null && _f !== void 0 ? _f : 30, onChange: v => onChangeMultiple({ shadowCameraRight: v }) })] })] }));
19
+ },
20
+ },
21
+ {
22
+ name: 'targetOffset',
23
+ type: 'custom',
24
+ label: 'Target Offset',
25
+ render: ({ value, onChange }) => {
26
+ const offset = value !== null && value !== void 0 ? value : [0, -5, 0];
27
+ return (_jsxs("div", { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 4 }, children: [_jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "X" }), _jsx(Input, { step: 0.5, value: offset[0], onChange: v => onChange([v, offset[1], offset[2]]) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Y" }), _jsx(Input, { step: 0.5, value: offset[1], onChange: v => onChange([offset[0], v, offset[2]]) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Z" }), _jsx(Input, { step: 0.5, value: offset[2], onChange: v => onChange([offset[0], offset[1], v]) })] })] }));
28
+ },
29
+ },
30
+ ];
6
31
  function DirectionalLightComponentEditor({ component, onUpdate }) {
7
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
8
- const props = {
9
- color: (_a = component.properties.color) !== null && _a !== void 0 ? _a : '#ffffff',
10
- intensity: (_b = component.properties.intensity) !== null && _b !== void 0 ? _b : 1.0,
11
- castShadow: (_c = component.properties.castShadow) !== null && _c !== void 0 ? _c : true,
12
- shadowMapSize: (_d = component.properties.shadowMapSize) !== null && _d !== void 0 ? _d : 1024,
13
- shadowCameraNear: (_e = component.properties.shadowCameraNear) !== null && _e !== void 0 ? _e : 0.1,
14
- shadowCameraFar: (_f = component.properties.shadowCameraFar) !== null && _f !== void 0 ? _f : 100,
15
- shadowCameraTop: (_g = component.properties.shadowCameraTop) !== null && _g !== void 0 ? _g : 30,
16
- shadowCameraBottom: (_h = component.properties.shadowCameraBottom) !== null && _h !== void 0 ? _h : -30,
17
- shadowCameraLeft: (_j = component.properties.shadowCameraLeft) !== null && _j !== void 0 ? _j : -30,
18
- shadowCameraRight: (_k = component.properties.shadowCameraRight) !== null && _k !== void 0 ? _k : 30,
19
- targetOffset: (_l = component.properties.targetOffset) !== null && _l !== void 0 ? _l : [0, -5, 0]
20
- };
21
- const textInputStyle = {
22
- flex: 1,
23
- backgroundColor: 'rgba(0, 0, 0, 0.4)',
24
- border: '1px solid rgba(34, 211, 238, 0.3)',
25
- padding: '2px 4px',
26
- fontSize: '10px',
27
- color: 'rgba(165, 243, 252, 1)',
28
- fontFamily: 'monospace',
29
- outline: 'none',
30
- };
31
- const smallLabel = { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 };
32
- return _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 8 }, children: [_jsxs("div", { children: [_jsx(Label, { children: "Color" }), _jsxs("div", { style: { display: 'flex', gap: 2 }, children: [_jsx("input", { type: "color", style: { height: 20, width: 20, backgroundColor: 'transparent', border: 'none', cursor: 'pointer' }, value: props.color, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { color: e.target.value })) }), _jsx("input", { type: "text", style: textInputStyle, value: props.color, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { color: e.target.value })) })] })] }), _jsxs("div", { children: [_jsx(Label, { children: "Intensity" }), _jsx(Input, { step: "0.1", value: props.intensity, onChange: value => onUpdate(Object.assign(Object.assign({}, component.properties), { intensity: value })) })] }), _jsxs("div", { children: [_jsx(Label, { children: "Cast Shadow" }), _jsx("input", { type: "checkbox", style: { height: 16, width: 16, backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', cursor: 'pointer' }, checked: props.castShadow, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { castShadow: e.target.checked })) })] }), _jsxs("div", { children: [_jsx(Label, { children: "Shadow Map Size" }), _jsx(Input, { step: "256", value: props.shadowMapSize, onChange: value => onUpdate(Object.assign(Object.assign({}, component.properties), { shadowMapSize: value })) })] }), _jsxs("div", { style: { borderTop: '1px solid rgba(34, 211, 238, 0.2)', paddingTop: 8, marginTop: 8 }, children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 4 }, children: "Shadow Camera" }), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4 }, children: [_jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Near" }), _jsx(Input, { step: "0.1", value: props.shadowCameraNear, onChange: value => onUpdate(Object.assign(Object.assign({}, component.properties), { shadowCameraNear: value })) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Far" }), _jsx(Input, { step: "1", value: props.shadowCameraFar, onChange: value => onUpdate(Object.assign(Object.assign({}, component.properties), { shadowCameraFar: value })) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Top" }), _jsx(Input, { step: "1", value: props.shadowCameraTop, onChange: value => onUpdate(Object.assign(Object.assign({}, component.properties), { shadowCameraTop: value })) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Bottom" }), _jsx(Input, { step: "1", value: props.shadowCameraBottom, onChange: value => onUpdate(Object.assign(Object.assign({}, component.properties), { shadowCameraBottom: value })) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Left" }), _jsx(Input, { step: "1", value: props.shadowCameraLeft, onChange: value => onUpdate(Object.assign(Object.assign({}, component.properties), { shadowCameraLeft: value })) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Right" }), _jsx(Input, { step: "1", value: props.shadowCameraRight, onChange: value => onUpdate(Object.assign(Object.assign({}, component.properties), { shadowCameraRight: value })) })] })] })] }), _jsxs("div", { style: { borderTop: '1px solid rgba(34, 211, 238, 0.2)', paddingTop: 8, marginTop: 8 }, children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 4 }, children: "Target Offset" }), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 4 }, children: [_jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "X" }), _jsx(Input, { step: "0.5", value: props.targetOffset[0], onChange: value => onUpdate(Object.assign(Object.assign({}, component.properties), { targetOffset: [value, props.targetOffset[1], props.targetOffset[2]] })) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Y" }), _jsx(Input, { step: "0.5", value: props.targetOffset[1], onChange: value => onUpdate(Object.assign(Object.assign({}, component.properties), { targetOffset: [props.targetOffset[0], value, props.targetOffset[2]] })) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Z" }), _jsx(Input, { step: "0.5", value: props.targetOffset[2], onChange: value => onUpdate(Object.assign(Object.assign({}, component.properties), { targetOffset: [props.targetOffset[0], props.targetOffset[1], value] })) })] })] })] })] });
32
+ return (_jsx(FieldRenderer, { fields: directionalLightFields, values: component.properties, onChange: onUpdate }));
33
33
  }
34
34
  function DirectionalLightView({ properties, editMode }) {
35
35
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Input, Label } from "./Input";
2
+ import { FieldRenderer, Input, Label } from "./Input";
3
3
  const GEOMETRY_ARGS = {
4
4
  box: {
5
5
  labels: ["Width", "Height", "Depth"],
@@ -17,27 +17,46 @@ const GEOMETRY_ARGS = {
17
17
  function GeometryComponentEditor({ component, onUpdate, }) {
18
18
  const { geometryType, args = [] } = component.properties;
19
19
  const schema = GEOMETRY_ARGS[geometryType];
20
- const selectStyle = {
21
- width: '100%',
22
- backgroundColor: 'rgba(0, 0, 0, 0.4)',
23
- border: '1px solid rgba(34, 211, 238, 0.3)',
24
- padding: '2px 4px',
25
- fontSize: '10px',
26
- color: 'rgba(165, 243, 252, 1)',
27
- fontFamily: 'monospace',
28
- outline: 'none',
20
+ const fields = [
21
+ {
22
+ name: 'geometryType',
23
+ type: 'select',
24
+ label: 'Type',
25
+ options: [
26
+ { value: 'box', label: 'Box' },
27
+ { value: 'sphere', label: 'Sphere' },
28
+ { value: 'plane', label: 'Plane' },
29
+ ],
30
+ },
31
+ {
32
+ name: 'args',
33
+ type: 'custom',
34
+ label: '',
35
+ render: ({ values, onChangeMultiple }) => {
36
+ const currentType = values.geometryType;
37
+ const currentSchema = GEOMETRY_ARGS[currentType];
38
+ const currentArgs = values.args || currentSchema.defaults;
39
+ return (_jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: 8 }, children: currentSchema.labels.map((label, i) => {
40
+ var _a;
41
+ return (_jsxs("div", { children: [_jsx(Label, { children: label }), _jsx(Input, { value: (_a = currentArgs[i]) !== null && _a !== void 0 ? _a : currentSchema.defaults[i], step: 0.1, onChange: value => {
42
+ const next = [...currentArgs];
43
+ next[i] = value;
44
+ onChangeMultiple({ args: next });
45
+ } })] }, label));
46
+ }) }));
47
+ },
48
+ },
49
+ ];
50
+ // Handle geometry type change to reset args
51
+ const handleChange = (newValues) => {
52
+ if ('geometryType' in newValues && newValues.geometryType !== geometryType) {
53
+ onUpdate({ geometryType: newValues.geometryType, args: GEOMETRY_ARGS[newValues.geometryType].defaults });
54
+ }
55
+ else {
56
+ onUpdate(newValues);
57
+ }
29
58
  };
30
- return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 8 }, children: [_jsxs("div", { children: [_jsx(Label, { children: "Type" }), _jsxs("select", { style: selectStyle, value: geometryType, onChange: e => {
31
- const type = e.target.value;
32
- onUpdate({ geometryType: type, args: GEOMETRY_ARGS[type].defaults });
33
- }, children: [_jsx("option", { value: "box", children: "Box" }), _jsx("option", { value: "sphere", children: "Sphere" }), _jsx("option", { value: "plane", children: "Plane" })] })] }), schema.labels.map((label, i) => {
34
- var _a;
35
- return (_jsxs("div", { children: [_jsx(Label, { children: label }), _jsx(Input, { value: (_a = args[i]) !== null && _a !== void 0 ? _a : schema.defaults[i], step: "0.1", onChange: value => {
36
- const next = [...args];
37
- next[i] = value;
38
- onUpdate({ args: next });
39
- } })] }, label));
40
- })] }));
59
+ return (_jsx(FieldRenderer, { fields: fields, values: component.properties, onChange: handleChange }));
41
60
  }
42
61
  // View for Geometry component
43
62
  function GeometryComponentView({ properties, children }) {
@@ -58,6 +77,7 @@ const GeometryComponent = {
58
77
  name: 'Geometry',
59
78
  Editor: GeometryComponentEditor,
60
79
  View: GeometryComponentView,
80
+ nonComposable: true,
61
81
  defaultProperties: {
62
82
  geometryType: 'box',
63
83
  args: GEOMETRY_ARGS.box.defaults,
@@ -1,4 +1,50 @@
1
1
  import React from 'react';
2
+ export type FieldType = 'vector3' | 'number' | 'string' | 'color' | 'boolean' | 'select';
3
+ interface BaseFieldDefinition {
4
+ name: string;
5
+ label: string;
6
+ }
7
+ interface Vector3FieldDefinition extends BaseFieldDefinition {
8
+ type: 'vector3';
9
+ snap?: number;
10
+ }
11
+ interface NumberFieldDefinition extends BaseFieldDefinition {
12
+ type: 'number';
13
+ min?: number;
14
+ max?: number;
15
+ step?: number;
16
+ }
17
+ interface StringFieldDefinition extends BaseFieldDefinition {
18
+ type: 'string';
19
+ placeholder?: string;
20
+ }
21
+ interface ColorFieldDefinition extends BaseFieldDefinition {
22
+ type: 'color';
23
+ }
24
+ interface BooleanFieldDefinition extends BaseFieldDefinition {
25
+ type: 'boolean';
26
+ }
27
+ interface SelectFieldDefinition extends BaseFieldDefinition {
28
+ type: 'select';
29
+ options: {
30
+ value: string;
31
+ label: string;
32
+ }[];
33
+ }
34
+ interface CustomFieldDefinition extends BaseFieldDefinition {
35
+ type: 'custom';
36
+ render: (props: {
37
+ value: any;
38
+ onChange: (value: any) => void;
39
+ values: Record<string, any>;
40
+ onChangeMultiple: (values: Record<string, any>) => void;
41
+ }) => React.ReactNode;
42
+ }
43
+ export type FieldDefinition = Vector3FieldDefinition | NumberFieldDefinition | StringFieldDefinition | ColorFieldDefinition | BooleanFieldDefinition | SelectFieldDefinition | CustomFieldDefinition;
44
+ declare const styles: {
45
+ input: React.CSSProperties;
46
+ label: React.CSSProperties;
47
+ };
2
48
  interface InputProps {
3
49
  value: number;
4
50
  onChange: (value: number) => void;
@@ -17,4 +63,35 @@ export declare function Vector3Input({ label, value, onChange, snap }: {
17
63
  onChange: (v: [number, number, number]) => void;
18
64
  snap?: number;
19
65
  }): import("react/jsx-runtime").JSX.Element;
20
- export {};
66
+ export declare function ColorInput({ label, value, onChange }: {
67
+ label?: string;
68
+ value: string;
69
+ onChange: (value: string) => void;
70
+ }): import("react/jsx-runtime").JSX.Element;
71
+ export declare function StringInput({ label, value, onChange, placeholder }: {
72
+ label?: string;
73
+ value: string;
74
+ onChange: (value: string) => void;
75
+ placeholder?: string;
76
+ }): import("react/jsx-runtime").JSX.Element;
77
+ export declare function BooleanInput({ label, value, onChange }: {
78
+ label?: string;
79
+ value: boolean;
80
+ onChange: (value: boolean) => void;
81
+ }): import("react/jsx-runtime").JSX.Element;
82
+ export declare function SelectInput({ label, value, onChange, options }: {
83
+ label?: string;
84
+ value: string;
85
+ onChange: (value: string) => void;
86
+ options: {
87
+ value: string;
88
+ label: string;
89
+ }[];
90
+ }): import("react/jsx-runtime").JSX.Element;
91
+ interface FieldRendererProps {
92
+ fields: FieldDefinition[];
93
+ values: Record<string, any>;
94
+ onChange: (values: Record<string, any>) => void;
95
+ }
96
+ export declare function FieldRenderer({ fields, values, onChange }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
97
+ export { styles };
@@ -1,5 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from 'react';
3
+ // ============================================================================
4
+ // Shared Styles
5
+ // ============================================================================
3
6
  // Shared styles
4
7
  const styles = {
5
8
  input: {
@@ -127,3 +130,65 @@ export function Vector3Input({ label, value, onChange, snap }) {
127
130
  }
128
131
  } })] }, key))) })] }));
129
132
  }
133
+ // ============================================================================
134
+ // Additional Input Components
135
+ // ============================================================================
136
+ export function ColorInput({ label, value, onChange }) {
137
+ return (_jsxs("div", { children: [label && _jsx(Label, { children: label }), _jsxs("div", { style: { display: 'flex', gap: 2 }, children: [_jsx("input", { type: "color", style: {
138
+ height: 20,
139
+ width: 20,
140
+ backgroundColor: 'transparent',
141
+ border: 'none',
142
+ cursor: 'pointer',
143
+ padding: 0,
144
+ }, value: value, onChange: e => onChange(e.target.value) }), _jsx("input", { type: "text", style: Object.assign(Object.assign({}, styles.input), { flex: 1 }), value: value, onChange: e => onChange(e.target.value) })] })] }));
145
+ }
146
+ export function StringInput({ label, value, onChange, placeholder }) {
147
+ return (_jsxs("div", { children: [label && _jsx(Label, { children: label }), _jsx("input", { type: "text", style: styles.input, value: value, onChange: e => onChange(e.target.value), placeholder: placeholder })] }));
148
+ }
149
+ export function BooleanInput({ label, value, onChange }) {
150
+ return (_jsxs("div", { children: [label && _jsx(Label, { children: label }), _jsx("input", { type: "checkbox", style: {
151
+ height: 16,
152
+ width: 16,
153
+ backgroundColor: 'rgba(0, 0, 0, 0.4)',
154
+ border: '1px solid rgba(34, 211, 238, 0.3)',
155
+ cursor: 'pointer',
156
+ }, checked: value, onChange: e => onChange(e.target.checked) })] }));
157
+ }
158
+ export function SelectInput({ label, value, onChange, options }) {
159
+ return (_jsxs("div", { children: [label && _jsx(Label, { children: label }), _jsx("select", { style: styles.input, value: value, onChange: e => onChange(e.target.value), children: options.map(opt => (_jsx("option", { value: opt.value, children: opt.label }, opt.value))) })] }));
160
+ }
161
+ export function FieldRenderer({ fields, values, onChange }) {
162
+ const updateField = (name, value) => {
163
+ onChange({ [name]: value });
164
+ };
165
+ return (_jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: 8 }, children: fields.map(field => {
166
+ var _a, _b;
167
+ const value = values[field.name];
168
+ switch (field.type) {
169
+ case 'vector3':
170
+ return (_jsx(Vector3Input, { label: field.label, value: value !== null && value !== void 0 ? value : [0, 0, 0], onChange: v => updateField(field.name, v), snap: field.snap }, field.name));
171
+ case 'number':
172
+ return (_jsxs("div", { children: [_jsx(Label, { children: field.label }), _jsx(Input, { value: value !== null && value !== void 0 ? value : 0, onChange: v => updateField(field.name, v), min: field.min, max: field.max, step: field.step })] }, field.name));
173
+ case 'string':
174
+ return (_jsx(StringInput, { label: field.label, value: value !== null && value !== void 0 ? value : '', onChange: v => updateField(field.name, v), placeholder: field.placeholder }, field.name));
175
+ case 'color':
176
+ return (_jsx(ColorInput, { label: field.label, value: value !== null && value !== void 0 ? value : '#ffffff', onChange: v => updateField(field.name, v) }, field.name));
177
+ case 'boolean':
178
+ return (_jsx(BooleanInput, { label: field.label, value: value !== null && value !== void 0 ? value : false, onChange: v => updateField(field.name, v) }, field.name));
179
+ case 'select':
180
+ return (_jsx(SelectInput, { label: field.label, value: (_b = value !== null && value !== void 0 ? value : (_a = field.options[0]) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : '', onChange: v => updateField(field.name, v), options: field.options }, field.name));
181
+ case 'custom':
182
+ return (_jsxs("div", { children: [field.label && _jsx(Label, { children: field.label }), field.render({
183
+ value,
184
+ onChange: v => updateField(field.name, v),
185
+ values,
186
+ onChangeMultiple: onChange,
187
+ })] }, field.name));
188
+ default:
189
+ return null;
190
+ }
191
+ }) }));
192
+ }
193
+ // Export styles for use in custom field renderers
194
+ export { styles };