react-three-game 0.0.44 → 0.0.45

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 (30) hide show
  1. package/README.md +38 -4
  2. package/dist/index.d.ts +7 -6
  3. package/dist/index.js +9 -6
  4. package/dist/tools/assetviewer/page.js +1 -1
  5. package/dist/tools/prefabeditor/EditorUI.js +3 -5
  6. package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +0 -2
  7. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +27 -27
  8. package/dist/tools/prefabeditor/components/GeometryComponent.js +40 -21
  9. package/dist/tools/prefabeditor/components/Input.d.ts +78 -1
  10. package/dist/tools/prefabeditor/components/Input.js +65 -0
  11. package/dist/tools/prefabeditor/components/MaterialComponent.js +57 -26
  12. package/dist/tools/prefabeditor/components/ModelComponent.js +17 -8
  13. package/dist/tools/prefabeditor/components/PhysicsComponent.js +25 -14
  14. package/dist/tools/prefabeditor/components/SpotLightComponent.js +10 -21
  15. package/dist/tools/prefabeditor/components/TransformComponent.js +27 -19
  16. package/dist/tools/prefabeditor/page.js +1 -1
  17. package/package.json +1 -1
  18. package/src/index.ts +28 -10
  19. package/src/tools/assetviewer/page.tsx +2 -0
  20. package/src/tools/prefabeditor/EditorUI.tsx +0 -10
  21. package/src/tools/prefabeditor/components/ComponentRegistry.ts +3 -5
  22. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +72 -76
  23. package/src/tools/prefabeditor/components/GeometryComponent.tsx +55 -38
  24. package/src/tools/prefabeditor/components/Input.tsx +299 -0
  25. package/src/tools/prefabeditor/components/MaterialComponent.tsx +97 -140
  26. package/src/tools/prefabeditor/components/ModelComponent.tsx +62 -41
  27. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +29 -33
  28. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +17 -65
  29. package/src/tools/prefabeditor/components/TransformComponent.tsx +83 -56
  30. 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
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;
@@ -7,8 +7,6 @@ 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>;
@@ -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 }) {
@@ -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 };
@@ -1,11 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { SingleTextureViewer, TextureListViewer } from '../../assetviewer/page';
3
3
  import { useEffect, useState } from 'react';
4
- import { Input, Label } from './Input';
4
+ import { FieldRenderer, Input } from './Input';
5
5
  import { useMemo } from 'react';
6
6
  import { DoubleSide, RepeatWrapping, ClampToEdgeWrapping, SRGBColorSpace, NearestFilter, LinearFilter, NearestMipmapNearestFilter, NearestMipmapLinearFilter, LinearMipmapNearestFilter, LinearMipmapLinearFilter } from 'three';
7
- function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
8
- var _a, _b, _c, _d;
7
+ function TexturePicker({ value, onChange, basePath }) {
9
8
  const [textureFiles, setTextureFiles] = useState([]);
10
9
  const [showPicker, setShowPicker] = useState(false);
11
10
  useEffect(() => {
@@ -15,30 +14,62 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
15
14
  .then(data => setTextureFiles(Array.isArray(data) ? data : data.files || []))
16
15
  .catch(console.error);
17
16
  }, [basePath]);
18
- const textInputStyle = {
19
- flex: 1,
20
- backgroundColor: 'rgba(0, 0, 0, 0.4)',
21
- border: '1px solid rgba(34, 211, 238, 0.3)',
22
- padding: '2px 4px',
23
- fontSize: '10px',
24
- color: 'rgba(165, 243, 252, 1)',
25
- fontFamily: 'monospace',
26
- outline: 'none',
27
- };
28
- return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 4 }, 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: component.properties.color, onChange: e => onUpdate({ color: e.target.value }) }), _jsx("input", { type: "text", style: textInputStyle, value: component.properties.color, onChange: e => onUpdate({ color: e.target.value }) })] })] }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4 }, children: [_jsx("input", { type: "checkbox", style: { width: 12, height: 12 }, checked: component.properties.wireframe || false, onChange: e => onUpdate({ wireframe: e.target.checked }) }), _jsx("label", { style: { fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)' }, children: "Wireframe" })] }), _jsxs("div", { children: [_jsx(Label, { children: "Texture File" }), _jsxs("div", { style: { maxHeight: 128, overflowY: 'auto', position: 'relative', display: 'flex', alignItems: 'center' }, children: [_jsx(SingleTextureViewer, { file: component.properties.texture || undefined, basePath: basePath }), _jsx("button", { onClick: () => setShowPicker(!showPicker), style: { padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)', marginTop: 4 }, children: showPicker ? 'Hide' : 'Change' }), showPicker && (_jsx("div", { style: { position: 'fixed', left: '-10px', top: '50%', transform: 'translate(-100%, -50%)', background: 'rgba(0,0,0,0.9)', padding: 16, border: '1px solid rgba(34, 211, 238, 0.3)', maxHeight: '80vh', overflowY: 'auto', zIndex: 1000 }, children: _jsx(TextureListViewer, { files: textureFiles, selected: component.properties.texture || undefined, onSelect: (file) => {
29
- onUpdate({ texture: file });
30
- setShowPicker(false);
31
- }, basePath: basePath }) }))] })] }), component.properties.texture && (_jsxs("div", { style: { borderTop: '1px solid rgba(34, 211, 238, 0.2)', paddingTop: 4, marginTop: 4 }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4, marginBottom: 4 }, children: [_jsx("input", { type: "checkbox", style: { width: 12, height: 12 }, checked: component.properties.repeat || false, onChange: e => onUpdate({ repeat: e.target.checked }) }), _jsx("label", { style: { fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)' }, children: "Repeat Texture" })] }), component.properties.repeat && (_jsxs("div", { children: [_jsx(Label, { children: "Repeat (X, Y)" }), _jsxs("div", { style: { display: 'flex', gap: 2 }, children: [_jsx(Input, { value: (_b = (_a = component.properties.repeatCount) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : 1, onChange: value => {
32
- var _a, _b;
33
- const y = (_b = (_a = component.properties.repeatCount) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : 1;
34
- onUpdate({ repeatCount: [value, y] });
35
- } }), _jsx(Input, { value: (_d = (_c = component.properties.repeatCount) === null || _c === void 0 ? void 0 : _c[1]) !== null && _d !== void 0 ? _d : 1, onChange: value => {
36
- var _a, _b;
37
- const x = (_b = (_a = component.properties.repeatCount) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : 1;
38
- onUpdate({ repeatCount: [x, value] });
39
- } })] })] })), _jsxs("div", { style: { marginTop: 4 }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4, marginBottom: 4 }, children: [_jsx("input", { type: "checkbox", style: { width: 12, height: 12 }, checked: component.properties.generateMipmaps !== false, onChange: e => onUpdate({ generateMipmaps: e.target.checked }) }), _jsx("label", { style: { fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)' }, children: "Generate Mipmaps" })] }), _jsxs("div", { children: [_jsx(Label, { children: "Min Filter" }), _jsxs("select", { style: Object.assign(Object.assign({}, textInputStyle), { width: '100%', cursor: 'pointer' }), value: component.properties.minFilter || 'LinearMipmapLinearFilter', onChange: e => onUpdate({ minFilter: e.target.value }), children: [_jsx("option", { value: "NearestFilter", children: "Nearest" }), _jsx("option", { value: "NearestMipmapNearestFilter", children: "Nearest Mipmap Nearest" }), _jsx("option", { value: "NearestMipmapLinearFilter", children: "Nearest Mipmap Linear" }), _jsx("option", { value: "LinearFilter", children: "Linear" }), _jsx("option", { value: "LinearMipmapNearestFilter", children: "Linear Mipmap Nearest" }), _jsx("option", { value: "LinearMipmapLinearFilter", children: "Linear Mipmap Linear (Default)" })] })] }), _jsxs("div", { style: { marginTop: 4 }, children: [_jsx(Label, { children: "Mag Filter" }), _jsxs("select", { style: Object.assign(Object.assign({}, textInputStyle), { width: '100%', cursor: 'pointer' }), value: component.properties.magFilter || 'LinearFilter', onChange: e => onUpdate({ magFilter: e.target.value }), children: [_jsx("option", { value: "NearestFilter", children: "Nearest" }), _jsx("option", { value: "LinearFilter", children: "Linear (Default)" })] })] })] })] }))] }));
17
+ return (_jsxs("div", { style: { maxHeight: 128, overflowY: 'auto', position: 'relative', display: 'flex', alignItems: 'center' }, children: [_jsx(SingleTextureViewer, { file: value || undefined, basePath: basePath }), _jsx("button", { onClick: () => setShowPicker(!showPicker), style: { padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)', marginTop: 4 }, children: showPicker ? 'Hide' : 'Change' }), showPicker && (_jsx("div", { style: { position: 'fixed', left: '-10px', top: '50%', transform: 'translate(-100%, -50%)', background: 'rgba(0,0,0,0.9)', padding: 16, border: '1px solid rgba(34, 211, 238, 0.3)', maxHeight: '80vh', overflowY: 'auto', zIndex: 1000 }, children: _jsx(TextureListViewer, { files: textureFiles, selected: value || undefined, onSelect: (file) => {
18
+ onChange(file);
19
+ setShowPicker(false);
20
+ }, basePath: basePath }) }))] }));
21
+ }
22
+ function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
23
+ const hasTexture = !!component.properties.texture;
24
+ const hasRepeat = component.properties.repeat;
25
+ const fields = [
26
+ { name: 'color', type: 'color', label: 'Color' },
27
+ { name: 'wireframe', type: 'boolean', label: 'Wireframe' },
28
+ {
29
+ name: 'texture',
30
+ type: 'custom',
31
+ label: 'Texture File',
32
+ render: ({ value, onChange }) => (_jsx(TexturePicker, { value: value, onChange: onChange, basePath: basePath })),
33
+ },
34
+ // Conditional texture settings
35
+ ...(hasTexture ? [
36
+ { name: 'repeat', type: 'boolean', label: 'Repeat Texture' },
37
+ ...(hasRepeat ? [{
38
+ name: 'repeatCount',
39
+ type: 'custom',
40
+ label: 'Repeat (X, Y)',
41
+ render: ({ value, onChange }) => {
42
+ var _a, _b;
43
+ return (_jsxs("div", { style: { display: 'flex', gap: 2 }, children: [_jsx(Input, { value: (_a = value === null || value === void 0 ? void 0 : value[0]) !== null && _a !== void 0 ? _a : 1, onChange: v => { var _a; return onChange([v, (_a = value === null || value === void 0 ? void 0 : value[1]) !== null && _a !== void 0 ? _a : 1]); } }), _jsx(Input, { value: (_b = value === null || value === void 0 ? void 0 : value[1]) !== null && _b !== void 0 ? _b : 1, onChange: v => { var _a; return onChange([(_a = value === null || value === void 0 ? void 0 : value[0]) !== null && _a !== void 0 ? _a : 1, v]); } })] }));
44
+ },
45
+ }] : []),
46
+ { name: 'generateMipmaps', type: 'boolean', label: 'Generate Mipmaps' },
47
+ {
48
+ name: 'minFilter',
49
+ type: 'select',
50
+ label: 'Min Filter',
51
+ options: [
52
+ { value: 'NearestFilter', label: 'Nearest' },
53
+ { value: 'NearestMipmapNearestFilter', label: 'Nearest Mipmap Nearest' },
54
+ { value: 'NearestMipmapLinearFilter', label: 'Nearest Mipmap Linear' },
55
+ { value: 'LinearFilter', label: 'Linear' },
56
+ { value: 'LinearMipmapNearestFilter', label: 'Linear Mipmap Nearest' },
57
+ { value: 'LinearMipmapLinearFilter', label: 'Linear Mipmap Linear (Default)' },
58
+ ],
59
+ },
60
+ {
61
+ name: 'magFilter',
62
+ type: 'select',
63
+ label: 'Mag Filter',
64
+ options: [
65
+ { value: 'NearestFilter', label: 'Nearest' },
66
+ { value: 'LinearFilter', label: 'Linear (Default)' },
67
+ ],
68
+ },
69
+ ] : []),
70
+ ];
71
+ return (_jsx(FieldRenderer, { fields: fields, values: component.properties, onChange: onUpdate }));
40
72
  }
41
- ;
42
73
  // View for Material component
43
74
  function MaterialComponentView({ properties, loadedTextures }) {
44
75
  var _a;