react-three-game 0.0.42 → 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 (33) 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/shared/GameCanvas.js +1 -1
  5. package/dist/tools/assetviewer/page.js +2 -2
  6. package/dist/tools/prefabeditor/EditorUI.js +3 -5
  7. package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +0 -2
  8. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +27 -27
  9. package/dist/tools/prefabeditor/components/GeometryComponent.js +40 -21
  10. package/dist/tools/prefabeditor/components/Input.d.ts +78 -1
  11. package/dist/tools/prefabeditor/components/Input.js +65 -0
  12. package/dist/tools/prefabeditor/components/MaterialComponent.js +57 -26
  13. package/dist/tools/prefabeditor/components/ModelComponent.js +17 -8
  14. package/dist/tools/prefabeditor/components/PhysicsComponent.js +25 -14
  15. package/dist/tools/prefabeditor/components/SpotLightComponent.js +10 -21
  16. package/dist/tools/prefabeditor/components/TransformComponent.js +27 -19
  17. package/dist/tools/prefabeditor/page.js +1 -1
  18. package/package.json +1 -1
  19. package/src/index.ts +28 -10
  20. package/src/shared/GameCanvas.tsx +1 -1
  21. package/src/tools/assetviewer/page.tsx +3 -3
  22. package/src/tools/prefabeditor/EditorUI.tsx +0 -10
  23. package/src/tools/prefabeditor/components/ComponentRegistry.ts +3 -5
  24. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +72 -76
  25. package/src/tools/prefabeditor/components/GeometryComponent.tsx +55 -38
  26. package/src/tools/prefabeditor/components/Input.tsx +299 -0
  27. package/src/tools/prefabeditor/components/MaterialComponent.tsx +97 -140
  28. package/src/tools/prefabeditor/components/ModelComponent.tsx +62 -41
  29. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +29 -33
  30. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +17 -65
  31. package/src/tools/prefabeditor/components/TransformComponent.tsx +83 -56
  32. package/src/tools/prefabeditor/page.tsx +1 -1
  33. package/dist/index.umd.js +0 -4622
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { ModelListViewer, SingleModelViewer } from '../../assetviewer/page';
3
3
  import { useEffect, useState, useMemo } from 'react';
4
- import { Label } from './Input';
5
- function ModelComponentEditor({ component, node, onUpdate, basePath = "" }) {
4
+ import { FieldRenderer } from './Input';
5
+ function ModelPicker({ value, onChange, basePath, nodeId }) {
6
6
  const [modelFiles, setModelFiles] = useState([]);
7
7
  const [showPicker, setShowPicker] = useState(false);
8
8
  useEffect(() => {
@@ -13,14 +13,23 @@ function ModelComponentEditor({ component, node, onUpdate, basePath = "" }) {
13
13
  .catch(console.error);
14
14
  }, [basePath]);
15
15
  const handleModelSelect = (file) => {
16
- // Remove leading slash for prefab compatibility
17
16
  const filename = file.startsWith('/') ? file.slice(1) : file;
18
- onUpdate({ 'filename': filename });
17
+ onChange(filename);
18
+ setShowPicker(false);
19
19
  };
20
- return _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 4 }, children: [_jsxs("div", { children: [_jsx(Label, { children: "Model File" }), _jsxs("div", { style: { maxHeight: 128, overflowY: 'auto', position: 'relative', display: 'flex', alignItems: 'center' }, children: [_jsx(SingleModelViewer, { file: component.properties.filename ? `/${component.properties.filename}` : 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(ModelListViewer, { files: modelFiles, selected: component.properties.filename ? `/${component.properties.filename}` : undefined, onSelect: (file) => {
21
- handleModelSelect(file);
22
- setShowPicker(false);
23
- }, basePath: basePath }, node === null || node === void 0 ? void 0 : node.id) }))] })] }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4 }, children: [_jsx("input", { type: "checkbox", id: "instanced-checkbox", checked: component.properties.instanced || false, onChange: e => onUpdate({ instanced: e.target.checked }), style: { width: 12, height: 12 } }), _jsx("label", { htmlFor: "instanced-checkbox", style: { fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)' }, children: "Instanced" })] })] });
20
+ return (_jsxs("div", { style: { maxHeight: 128, overflowY: 'auto', position: 'relative', display: 'flex', alignItems: 'center' }, children: [_jsx(SingleModelViewer, { file: value ? `/${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(ModelListViewer, { files: modelFiles, selected: value ? `/${value}` : undefined, onSelect: handleModelSelect, basePath: basePath }, nodeId) }))] }));
21
+ }
22
+ function ModelComponentEditor({ component, node, onUpdate, basePath = "" }) {
23
+ const fields = [
24
+ {
25
+ name: 'filename',
26
+ type: 'custom',
27
+ label: 'Model File',
28
+ render: ({ value, onChange }) => (_jsx(ModelPicker, { value: value, onChange: onChange, basePath: basePath, nodeId: node === null || node === void 0 ? void 0 : node.id })),
29
+ },
30
+ { name: 'instanced', type: 'boolean', label: 'Instanced' },
31
+ ];
32
+ return (_jsx(FieldRenderer, { fields: fields, values: component.properties, onChange: onUpdate }));
24
33
  }
25
34
  // View for Model component
26
35
  function ModelComponentView({ properties, loadedModels, children }) {
@@ -1,19 +1,30 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { RigidBody } from "@react-three/rapier";
3
- import { Label } from "./Input";
3
+ import { FieldRenderer } from "./Input";
4
+ const physicsFields = [
5
+ {
6
+ name: 'type',
7
+ type: 'select',
8
+ label: 'Type',
9
+ options: [
10
+ { value: 'dynamic', label: 'Dynamic' },
11
+ { value: 'fixed', label: 'Fixed' },
12
+ ],
13
+ },
14
+ {
15
+ name: 'collider',
16
+ type: 'select',
17
+ label: 'Collider',
18
+ options: [
19
+ { value: 'hull', label: 'Hull (convex)' },
20
+ { value: 'trimesh', label: 'Trimesh (exact)' },
21
+ { value: 'cuboid', label: 'Cuboid (box)' },
22
+ { value: 'ball', label: 'Ball (sphere)' },
23
+ ],
24
+ },
25
+ ];
4
26
  function PhysicsComponentEditor({ component, onUpdate }) {
5
- const { type = 'dynamic', collider = 'hull' } = component.properties;
6
- const selectStyle = {
7
- width: '100%',
8
- backgroundColor: 'rgba(0, 0, 0, 0.4)',
9
- border: '1px solid rgba(34, 211, 238, 0.3)',
10
- padding: '2px 4px',
11
- fontSize: '10px',
12
- color: 'rgba(165, 243, 252, 1)',
13
- fontFamily: 'monospace',
14
- outline: 'none',
15
- };
16
- return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 8 }, children: [_jsxs("div", { children: [_jsx(Label, { children: "Type" }), _jsxs("select", { style: selectStyle, value: type, onChange: e => onUpdate({ type: e.target.value }), children: [_jsx("option", { value: "dynamic", children: "Dynamic" }), _jsx("option", { value: "fixed", children: "Fixed" })] })] }), _jsxs("div", { children: [_jsx(Label, { children: "Collider" }), _jsxs("select", { style: selectStyle, value: collider, onChange: e => onUpdate({ collider: e.target.value }), children: [_jsx("option", { value: "hull", children: "Hull (convex)" }), _jsx("option", { value: "trimesh", children: "Trimesh (exact)" }), _jsx("option", { value: "cuboid", children: "Cuboid (box)" }), _jsx("option", { value: "ball", children: "Ball (sphere)" })] })] })] }));
27
+ return (_jsx(FieldRenderer, { fields: physicsFields, values: component.properties, onChange: onUpdate }));
17
28
  }
18
29
  function PhysicsComponentView({ properties, children, position, rotation, scale, editMode }) {
19
30
  const colliders = properties.collider || (properties.type === 'fixed' ? 'trimesh' : 'hull');
@@ -1,27 +1,16 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useRef, useEffect } from "react";
3
- import { Input, Label } from "./Input";
3
+ import { FieldRenderer } from "./Input";
4
+ const spotLightFields = [
5
+ { name: 'color', type: 'color', label: 'Color' },
6
+ { name: 'intensity', type: 'number', label: 'Intensity', step: 0.1, min: 0 },
7
+ { name: 'angle', type: 'number', label: 'Angle', step: 0.1, min: 0, max: Math.PI },
8
+ { name: 'penumbra', type: 'number', label: 'Penumbra', step: 0.1, min: 0, max: 1 },
9
+ { name: 'distance', type: 'number', label: 'Distance', step: 1, min: 0 },
10
+ { name: 'castShadow', type: 'boolean', label: 'Cast Shadow' },
11
+ ];
4
12
  function SpotLightComponentEditor({ component, onUpdate }) {
5
- var _a, _b, _c, _d, _e, _f;
6
- const props = {
7
- color: (_a = component.properties.color) !== null && _a !== void 0 ? _a : '#ffffff',
8
- intensity: (_b = component.properties.intensity) !== null && _b !== void 0 ? _b : 1.0,
9
- angle: (_c = component.properties.angle) !== null && _c !== void 0 ? _c : Math.PI / 6,
10
- penumbra: (_d = component.properties.penumbra) !== null && _d !== void 0 ? _d : 0.5,
11
- distance: (_e = component.properties.distance) !== null && _e !== void 0 ? _e : 100,
12
- castShadow: (_f = component.properties.castShadow) !== null && _f !== void 0 ? _f : true
13
- };
14
- const textInputStyle = {
15
- flex: 1,
16
- backgroundColor: 'rgba(0, 0, 0, 0.4)',
17
- border: '1px solid rgba(34, 211, 238, 0.3)',
18
- padding: '2px 4px',
19
- fontSize: '10px',
20
- color: 'rgba(165, 243, 252, 1)',
21
- fontFamily: 'monospace',
22
- outline: 'none',
23
- };
24
- 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: "Angle" }), _jsx(Input, { step: "0.1", min: 0, max: Math.PI, value: props.angle, onChange: value => onUpdate(Object.assign(Object.assign({}, component.properties), { angle: value })) })] }), _jsxs("div", { children: [_jsx(Label, { children: "Penumbra" }), _jsx(Input, { step: "0.1", min: 0, max: 1, value: props.penumbra, onChange: value => onUpdate(Object.assign(Object.assign({}, component.properties), { penumbra: value })) })] }), _jsxs("div", { children: [_jsx(Label, { children: "Distance" }), _jsx(Input, { step: "1", min: 0, value: props.distance, onChange: value => onUpdate(Object.assign(Object.assign({}, component.properties), { distance: 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 })) })] })] });
13
+ return (_jsx(FieldRenderer, { fields: spotLightFields, values: component.properties, onChange: onUpdate }));
25
14
  }
26
15
  function SpotLightView({ properties, editMode }) {
27
16
  var _a, _b, _c, _d, _e, _f;
@@ -1,5 +1,5 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
- import { Vector3Input, Label } from "./Input";
2
+ import { FieldRenderer, Label } from "./Input";
3
3
  import { useEditorContext } from "../EditorContext";
4
4
  const buttonStyle = {
5
5
  padding: '2px 6px',
@@ -11,24 +11,32 @@ const buttonStyle = {
11
11
  font: 'inherit',
12
12
  flex: 1,
13
13
  };
14
- function TransformComponentEditor({ component, onUpdate, transformMode, setTransformMode }) {
15
- const { snapResolution, setSnapResolution } = useEditorContext();
16
- return _jsxs("div", { style: { display: 'flex', flexDirection: 'column' }, children: [transformMode && setTransformMode && (_jsxs("div", { style: { marginBottom: 8 }, children: [_jsxs(Label, { children: ["Transform Mode ", snapResolution > 0 && `(Snap: ${snapResolution})`] }), _jsx("div", { style: { display: 'flex', gap: 6 }, children: ["translate", "rotate", "scale"].map(mode => {
17
- const isActive = transformMode === mode;
18
- return (_jsx("button", { onClick: () => setTransformMode(mode), style: Object.assign(Object.assign({}, buttonStyle), { background: isActive ? 'rgba(255,255,255,0.10)' : 'transparent' }), onPointerEnter: (e) => {
19
- if (!isActive)
20
- e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
21
- }, onPointerLeave: (e) => {
22
- if (!isActive)
23
- e.currentTarget.style.background = 'transparent';
24
- }, children: mode }, mode));
25
- }) }), _jsx("div", { style: { marginTop: 6 }, children: _jsxs("button", { onClick: () => setSnapResolution(snapResolution > 0 ? 0 : 0.1), style: Object.assign(Object.assign({}, buttonStyle), { background: snapResolution > 0 ? 'rgba(255,255,255,0.10)' : 'transparent', width: '100%' }), onPointerEnter: (e) => {
26
- if (snapResolution === 0)
27
- e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
28
- }, onPointerLeave: (e) => {
29
- if (snapResolution === 0)
30
- e.currentTarget.style.background = 'transparent';
31
- }, children: ["Snap: ", snapResolution > 0 ? `ON (${snapResolution})` : 'OFF'] }) })] })), _jsx(Vector3Input, { label: "Position", value: component.properties.position, onChange: v => onUpdate({ position: v }), snap: snapResolution }), _jsx(Vector3Input, { label: "Rotation", value: component.properties.rotation, onChange: v => onUpdate({ rotation: v }), snap: snapResolution }), _jsx(Vector3Input, { label: "Scale", value: component.properties.scale, onChange: v => onUpdate({ scale: v }), snap: snapResolution })] });
14
+ function TransformModeSelector({ transformMode, setTransformMode, snapResolution, setSnapResolution }) {
15
+ return (_jsxs("div", { style: { marginBottom: 8 }, children: [_jsxs(Label, { children: ["Transform Mode ", snapResolution > 0 && `(Snap: ${snapResolution})`] }), _jsx("div", { style: { display: 'flex', gap: 6 }, children: ["translate", "rotate", "scale"].map(mode => {
16
+ const isActive = transformMode === mode;
17
+ return (_jsx("button", { onClick: () => setTransformMode(mode), style: Object.assign(Object.assign({}, buttonStyle), { background: isActive ? 'rgba(255,255,255,0.10)' : 'transparent' }), onPointerEnter: (e) => {
18
+ if (!isActive)
19
+ e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
20
+ }, onPointerLeave: (e) => {
21
+ if (!isActive)
22
+ e.currentTarget.style.background = 'transparent';
23
+ }, children: mode }, mode));
24
+ }) }), _jsx("div", { style: { marginTop: 6 }, children: _jsxs("button", { onClick: () => setSnapResolution(snapResolution > 0 ? 0 : 0.1), style: Object.assign(Object.assign({}, buttonStyle), { background: snapResolution > 0 ? 'rgba(255,255,255,0.10)' : 'transparent', width: '100%' }), onPointerEnter: (e) => {
25
+ if (snapResolution === 0)
26
+ e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
27
+ }, onPointerLeave: (e) => {
28
+ if (snapResolution === 0)
29
+ e.currentTarget.style.background = 'transparent';
30
+ }, children: ["Snap: ", snapResolution > 0 ? `ON (${snapResolution})` : 'OFF'] }) })] }));
31
+ }
32
+ function TransformComponentEditor({ component, onUpdate }) {
33
+ const { transformMode, setTransformMode, snapResolution, setSnapResolution } = useEditorContext();
34
+ const fields = [
35
+ { name: 'position', type: 'vector3', label: 'Position', snap: snapResolution },
36
+ { name: 'rotation', type: 'vector3', label: 'Rotation', snap: snapResolution },
37
+ { name: 'scale', type: 'vector3', label: 'Scale', snap: snapResolution },
38
+ ];
39
+ return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column' }, children: [_jsx(TransformModeSelector, { transformMode: transformMode, setTransformMode: setTransformMode, snapResolution: snapResolution, setSnapResolution: setSnapResolution }), _jsx(FieldRenderer, { fields: fields, values: component.properties, onChange: onUpdate })] }));
32
40
  }
33
41
  const TransformComponent = {
34
42
  name: 'Transform',
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import PrefabEditor from "./PrefabEditor";
3
3
  export default function PrefabEditorPage() {
4
- return _jsx("div", { className: "w-screen h-screen", children: _jsx(PrefabEditor, { children: _jsx("directionalLight", { position: [5, 10, 7.5], intensity: 1, castShadow: true }) }) });
4
+ return _jsx("div", { style: { width: '100%', height: '100%' }, children: _jsx(PrefabEditor, { children: _jsx("directionalLight", { position: [5, 10, 7.5], intensity: 1, castShadow: true }) }) });
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-three-game",
3
- "version": "0.0.42",
3
+ "version": "0.0.45",
4
4
  "description": "Batteries included React Three Fiber game engine",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
package/src/index.ts CHANGED
@@ -1,16 +1,38 @@
1
- // Core Components
1
+ // Core
2
2
  export { default as GameCanvas } from './shared/GameCanvas';
3
3
 
4
- // Prefab Editor
4
+ // Helpers
5
+ export * from './helpers';
6
+ export { sound as soundManager } from './helpers/SoundManager';
7
+
8
+ // Prefab Editor - Components
5
9
  export { default as PrefabEditor } from './tools/prefabeditor/PrefabEditor';
6
- export type { PrefabEditorRef } from './tools/prefabeditor/PrefabEditor';
7
10
  export { default as PrefabRoot } from './tools/prefabeditor/PrefabRoot';
8
- export type { PrefabRootRef } from './tools/prefabeditor/PrefabRoot';
11
+
12
+ // Prefab Editor - Component Registry
9
13
  export { registerComponent } from './tools/prefabeditor/components/ComponentRegistry';
14
+
15
+ // Prefab Editor - Input Components
16
+ export {
17
+ FieldRenderer,
18
+ Input,
19
+ Label,
20
+ Vector3Input,
21
+ ColorInput,
22
+ StringInput,
23
+ BooleanInput,
24
+ SelectInput,
25
+ } from './tools/prefabeditor/components/Input';
26
+
27
+ // Prefab Editor - Styles & Utils
28
+ export * from './tools/prefabeditor/utils';
29
+
30
+ // Prefab Editor - Types
31
+ export type { PrefabEditorRef } from './tools/prefabeditor/PrefabEditor';
32
+ export type { PrefabRootRef } from './tools/prefabeditor/PrefabRoot';
10
33
  export type { Component } from './tools/prefabeditor/components/ComponentRegistry';
34
+ export type { FieldDefinition, FieldType } from './tools/prefabeditor/components/Input';
11
35
  export type { Prefab, GameObject, ComponentData } from './tools/prefabeditor/types';
12
- export * as editorStyles from './tools/prefabeditor/styles';
13
- export * from './tools/prefabeditor/utils';
14
36
 
15
37
  // Asset Tools
16
38
  export { DragDropLoader } from './tools/dragdrop/DragDropLoader';
@@ -20,7 +42,3 @@ export {
20
42
  SoundListViewer,
21
43
  SharedCanvas,
22
44
  } from './tools/assetviewer/page';
23
- export { sound as soundManager } from './helpers/SoundManager';
24
-
25
- // Helpers
26
- export * from './helpers';
@@ -24,7 +24,7 @@ export default function GameCanvas({ loader = false, children, glConfig, ...prop
24
24
 
25
25
  return <>
26
26
  <Canvas
27
- style={{ touchAction: 'none' }}
27
+ style={{ touchAction: 'none', userSelect: 'none' }}
28
28
  shadows={{ type: PCFShadowMap, }}
29
29
  frameloop={frameloop}
30
30
  gl={async ({ canvas }) => {
@@ -269,9 +269,9 @@ function ModelCard({ file, onSelect, basePath = "" }: { file: string; onSelect:
269
269
  <View style={{ width: '100%', height: '100%' }}>
270
270
  <PerspectiveCamera makeDefault position={[0, 1, 3]} fov={50} />
271
271
  <Suspense fallback={null}>
272
- <Stage intensity={0.5} environment="city">
273
- <ModelPreview url={fullPath} onError={() => setError(true)} />
274
- </Stage>
272
+ <ambientLight intensity={1} />
273
+ <pointLight position={[5, 5, 5]} intensity={0.5} />
274
+ <ModelPreview url={fullPath} onError={() => setError(true)} />
275
275
  <OrbitControls enableZoom={false} />
276
276
  </Suspense>
277
277
  </View>
@@ -4,7 +4,6 @@ import EditorTree from './EditorTree';
4
4
  import { getAllComponents } from './components/ComponentRegistry';
5
5
  import { base, inspector } from './styles';
6
6
  import { findNode, updateNode, deleteNode } from './utils';
7
- import { useEditorContext } from './EditorContext';
8
7
 
9
8
  function EditorUI({
10
9
  prefabData,
@@ -28,7 +27,6 @@ function EditorUI({
28
27
  canRedo?: boolean;
29
28
  }) {
30
29
  const [collapsed, setCollapsed] = useState(false);
31
- const { transformMode, setTransformMode } = useEditorContext();
32
30
 
33
31
  const updateNodeHandler = (updater: (n: GameObjectType) => GameObjectType) => {
34
32
  if (!prefabData || !setPrefabData || !selectedId) return;
@@ -63,8 +61,6 @@ function EditorUI({
63
61
  node={selectedNode}
64
62
  updateNode={updateNodeHandler}
65
63
  deleteNode={deleteNodeHandler}
66
- transformMode={transformMode}
67
- setTransformMode={setTransformMode}
68
64
  basePath={basePath}
69
65
  />
70
66
  )}
@@ -89,15 +85,11 @@ function NodeInspector({
89
85
  node,
90
86
  updateNode,
91
87
  deleteNode,
92
- transformMode,
93
- setTransformMode,
94
88
  basePath
95
89
  }: {
96
90
  node: GameObjectType;
97
91
  updateNode: (updater: (n: GameObjectType) => GameObjectType) => void;
98
92
  deleteNode: () => void;
99
- transformMode: "translate" | "rotate" | "scale";
100
- setTransformMode: (m: "translate" | "rotate" | "scale") => void;
101
93
  basePath?: string;
102
94
  }) {
103
95
  const ALL_COMPONENTS = getAllComponents();
@@ -170,8 +162,6 @@ function NodeInspector({
170
162
  }
171
163
  }))}
172
164
  basePath={basePath}
173
- transformMode={transformMode}
174
- setTransformMode={setTransformMode}
175
165
  />
176
166
  )}
177
167
  </div>
@@ -3,13 +3,11 @@ import { ComponentData, GameObject } from "../types";
3
3
 
4
4
  export interface Component {
5
5
  name: string;
6
- Editor: FC<{
6
+ Editor: FC<{
7
7
  node?: GameObject;
8
- component: ComponentData;
9
- onUpdate: (newComp: any) => void;
8
+ component: ComponentData;
9
+ onUpdate: (newComp: any) => void;
10
10
  basePath?: string;
11
- transformMode?: "translate" | "rotate" | "scale";
12
- setTransformMode?: (m: "translate" | "rotate" | "scale") => void;
13
11
  }>;
14
12
  defaultProperties: any;
15
13
  // Allow View to accept extra props for special cases (like material)
@@ -2,86 +2,82 @@ import { Component } from "./ComponentRegistry";
2
2
  import { useRef, useEffect } from "react";
3
3
  import { useFrame } from "@react-three/fiber";
4
4
  import { DirectionalLight, Object3D, Vector3 } from "three";
5
- import { Input, Label } from "./Input";
5
+ import { FieldRenderer, FieldDefinition, Input } from "./Input";
6
6
 
7
- function DirectionalLightComponentEditor({ component, onUpdate }: { component: any; onUpdate: (newComp: any) => void }) {
8
- const props = {
9
- color: component.properties.color ?? '#ffffff',
10
- intensity: component.properties.intensity ?? 1.0,
11
- castShadow: component.properties.castShadow ?? true,
12
- shadowMapSize: component.properties.shadowMapSize ?? 1024,
13
- shadowCameraNear: component.properties.shadowCameraNear ?? 0.1,
14
- shadowCameraFar: component.properties.shadowCameraFar ?? 100,
15
- shadowCameraTop: component.properties.shadowCameraTop ?? 30,
16
- shadowCameraBottom: component.properties.shadowCameraBottom ?? -30,
17
- shadowCameraLeft: component.properties.shadowCameraLeft ?? -30,
18
- shadowCameraRight: component.properties.shadowCameraRight ?? 30,
19
- targetOffset: component.properties.targetOffset ?? [0, -5, 0]
20
- };
21
-
22
- const textInputStyle = {
23
- flex: 1,
24
- backgroundColor: 'rgba(0, 0, 0, 0.4)',
25
- border: '1px solid rgba(34, 211, 238, 0.3)',
26
- padding: '2px 4px',
27
- fontSize: '10px',
28
- color: 'rgba(165, 243, 252, 1)',
29
- fontFamily: 'monospace',
30
- outline: 'none',
31
- };
32
-
33
- const smallLabel = { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 };
7
+ const smallLabel = { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 } as const;
34
8
 
35
- return <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
36
- <div>
37
- <Label>Color</Label>
38
- <div style={{ display: 'flex', gap: 2 }}>
39
- <input
40
- type="color"
41
- style={{ height: 20, width: 20, backgroundColor: 'transparent', border: 'none', cursor: 'pointer' }}
42
- value={props.color}
43
- onChange={e => onUpdate({ ...component.properties, color: e.target.value })}
44
- />
45
- <input type="text" style={textInputStyle} value={props.color} onChange={e => onUpdate({ ...component.properties, color: e.target.value })} />
46
- </div>
47
- </div>
48
- <div>
49
- <Label>Intensity</Label>
50
- <Input step="0.1" value={props.intensity} onChange={value => onUpdate({ ...component.properties, intensity: value })} />
51
- </div>
52
- <div>
53
- <Label>Cast Shadow</Label>
54
- <input
55
- type="checkbox"
56
- style={{ height: 16, width: 16, backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', cursor: 'pointer' }}
57
- checked={props.castShadow}
58
- onChange={e => onUpdate({ ...component.properties, castShadow: e.target.checked })}
59
- />
60
- </div>
61
- <div>
62
- <Label>Shadow Map Size</Label>
63
- <Input step="256" value={props.shadowMapSize} onChange={value => onUpdate({ ...component.properties, shadowMapSize: value })} />
64
- </div>
65
- <div style={{ borderTop: '1px solid rgba(34, 211, 238, 0.2)', paddingTop: 8, marginTop: 8 }}>
66
- <label style={{ display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 4 }}>Shadow Camera</label>
9
+ const directionalLightFields: FieldDefinition[] = [
10
+ { name: 'color', type: 'color', label: 'Color' },
11
+ { name: 'intensity', type: 'number', label: 'Intensity', step: 0.1, min: 0 },
12
+ { name: 'castShadow', type: 'boolean', label: 'Cast Shadow' },
13
+ { name: 'shadowMapSize', type: 'number', label: 'Shadow Map Size', step: 256, min: 256 },
14
+ {
15
+ name: '_shadowCamera',
16
+ type: 'custom',
17
+ label: 'Shadow Camera',
18
+ render: ({ values, onChangeMultiple }) => (
67
19
  <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4 }}>
68
- <div><label style={smallLabel}>Near</label><Input step="0.1" value={props.shadowCameraNear} onChange={value => onUpdate({ ...component.properties, shadowCameraNear: value })} /></div>
69
- <div><label style={smallLabel}>Far</label><Input step="1" value={props.shadowCameraFar} onChange={value => onUpdate({ ...component.properties, shadowCameraFar: value })} /></div>
70
- <div><label style={smallLabel}>Top</label><Input step="1" value={props.shadowCameraTop} onChange={value => onUpdate({ ...component.properties, shadowCameraTop: value })} /></div>
71
- <div><label style={smallLabel}>Bottom</label><Input step="1" value={props.shadowCameraBottom} onChange={value => onUpdate({ ...component.properties, shadowCameraBottom: value })} /></div>
72
- <div><label style={smallLabel}>Left</label><Input step="1" value={props.shadowCameraLeft} onChange={value => onUpdate({ ...component.properties, shadowCameraLeft: value })} /></div>
73
- <div><label style={smallLabel}>Right</label><Input step="1" value={props.shadowCameraRight} onChange={value => onUpdate({ ...component.properties, shadowCameraRight: value })} /></div>
20
+ <div>
21
+ <label style={smallLabel}>Near</label>
22
+ <Input step={0.1} value={values.shadowCameraNear ?? 0.1} onChange={v => onChangeMultiple({ shadowCameraNear: v })} />
23
+ </div>
24
+ <div>
25
+ <label style={smallLabel}>Far</label>
26
+ <Input step={1} value={values.shadowCameraFar ?? 100} onChange={v => onChangeMultiple({ shadowCameraFar: v })} />
27
+ </div>
28
+ <div>
29
+ <label style={smallLabel}>Top</label>
30
+ <Input step={1} value={values.shadowCameraTop ?? 30} onChange={v => onChangeMultiple({ shadowCameraTop: v })} />
31
+ </div>
32
+ <div>
33
+ <label style={smallLabel}>Bottom</label>
34
+ <Input step={1} value={values.shadowCameraBottom ?? -30} onChange={v => onChangeMultiple({ shadowCameraBottom: v })} />
35
+ </div>
36
+ <div>
37
+ <label style={smallLabel}>Left</label>
38
+ <Input step={1} value={values.shadowCameraLeft ?? -30} onChange={v => onChangeMultiple({ shadowCameraLeft: v })} />
39
+ </div>
40
+ <div>
41
+ <label style={smallLabel}>Right</label>
42
+ <Input step={1} value={values.shadowCameraRight ?? 30} onChange={v => onChangeMultiple({ shadowCameraRight: v })} />
43
+ </div>
74
44
  </div>
75
- </div>
76
- <div style={{ borderTop: '1px solid rgba(34, 211, 238, 0.2)', paddingTop: 8, marginTop: 8 }}>
77
- <label style={{ display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 4 }}>Target Offset</label>
78
- <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 4 }}>
79
- <div><label style={smallLabel}>X</label><Input step="0.5" value={props.targetOffset[0]} onChange={value => onUpdate({ ...component.properties, targetOffset: [value, props.targetOffset[1], props.targetOffset[2]] })} /></div>
80
- <div><label style={smallLabel}>Y</label><Input step="0.5" value={props.targetOffset[1]} onChange={value => onUpdate({ ...component.properties, targetOffset: [props.targetOffset[0], value, props.targetOffset[2]] })} /></div>
81
- <div><label style={smallLabel}>Z</label><Input step="0.5" value={props.targetOffset[2]} onChange={value => onUpdate({ ...component.properties, targetOffset: [props.targetOffset[0], props.targetOffset[1], value] })} /></div>
82
- </div>
83
- </div>
84
- </div>;
45
+ ),
46
+ },
47
+ {
48
+ name: 'targetOffset',
49
+ type: 'custom',
50
+ label: 'Target Offset',
51
+ render: ({ value, onChange }) => {
52
+ const offset = value ?? [0, -5, 0];
53
+ return (
54
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 4 }}>
55
+ <div>
56
+ <label style={smallLabel}>X</label>
57
+ <Input step={0.5} value={offset[0]} onChange={v => onChange([v, offset[1], offset[2]])} />
58
+ </div>
59
+ <div>
60
+ <label style={smallLabel}>Y</label>
61
+ <Input step={0.5} value={offset[1]} onChange={v => onChange([offset[0], v, offset[2]])} />
62
+ </div>
63
+ <div>
64
+ <label style={smallLabel}>Z</label>
65
+ <Input step={0.5} value={offset[2]} onChange={v => onChange([offset[0], offset[1], v])} />
66
+ </div>
67
+ </div>
68
+ );
69
+ },
70
+ },
71
+ ];
72
+
73
+ function DirectionalLightComponentEditor({ component, onUpdate }: { component: any; onUpdate: (newComp: any) => void }) {
74
+ return (
75
+ <FieldRenderer
76
+ fields={directionalLightFields}
77
+ values={component.properties}
78
+ onChange={onUpdate}
79
+ />
80
+ );
85
81
  }
86
82
 
87
83
  function DirectionalLightView({ properties, editMode }: { properties: any; editMode?: boolean }) {
@@ -1,5 +1,5 @@
1
1
  import { Component } from "./ComponentRegistry";
2
- import { Input, Label } from "./Input";
2
+ import { FieldRenderer, FieldDefinition, Input, Label } from "./Input";
3
3
 
4
4
  const GEOMETRY_ARGS: Record<string, {
5
5
  labels: string[];
@@ -29,46 +29,63 @@ function GeometryComponentEditor({
29
29
  const { geometryType, args = [] } = component.properties;
30
30
  const schema = GEOMETRY_ARGS[geometryType];
31
31
 
32
- const selectStyle = {
33
- width: '100%',
34
- backgroundColor: 'rgba(0, 0, 0, 0.4)',
35
- border: '1px solid rgba(34, 211, 238, 0.3)',
36
- padding: '2px 4px',
37
- fontSize: '10px',
38
- color: 'rgba(165, 243, 252, 1)',
39
- fontFamily: 'monospace',
40
- outline: 'none',
32
+ const fields: FieldDefinition[] = [
33
+ {
34
+ name: 'geometryType',
35
+ type: 'select',
36
+ label: 'Type',
37
+ options: [
38
+ { value: 'box', label: 'Box' },
39
+ { value: 'sphere', label: 'Sphere' },
40
+ { value: 'plane', label: 'Plane' },
41
+ ],
42
+ },
43
+ {
44
+ name: 'args',
45
+ type: 'custom',
46
+ label: '',
47
+ render: ({ values, onChangeMultiple }) => {
48
+ const currentType = values.geometryType;
49
+ const currentSchema = GEOMETRY_ARGS[currentType];
50
+ const currentArgs = values.args || currentSchema.defaults;
51
+
52
+ return (
53
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
54
+ {currentSchema.labels.map((label, i) => (
55
+ <div key={label}>
56
+ <Label>{label}</Label>
57
+ <Input
58
+ value={currentArgs[i] ?? currentSchema.defaults[i]}
59
+ step={0.1}
60
+ onChange={value => {
61
+ const next = [...currentArgs];
62
+ next[i] = value;
63
+ onChangeMultiple({ args: next });
64
+ }}
65
+ />
66
+ </div>
67
+ ))}
68
+ </div>
69
+ );
70
+ },
71
+ },
72
+ ];
73
+
74
+ // Handle geometry type change to reset args
75
+ const handleChange = (newValues: Record<string, any>) => {
76
+ if ('geometryType' in newValues && newValues.geometryType !== geometryType) {
77
+ onUpdate({ geometryType: newValues.geometryType, args: GEOMETRY_ARGS[newValues.geometryType].defaults });
78
+ } else {
79
+ onUpdate(newValues);
80
+ }
41
81
  };
42
82
 
43
83
  return (
44
- <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
45
- <div>
46
- <Label>Type</Label>
47
- <select style={selectStyle} value={geometryType} onChange={e => {
48
- const type = e.target.value;
49
- onUpdate({ geometryType: type, args: GEOMETRY_ARGS[type].defaults });
50
- }}>
51
- <option value="box">Box</option>
52
- <option value="sphere">Sphere</option>
53
- <option value="plane">Plane</option>
54
- </select>
55
- </div>
56
-
57
- {schema.labels.map((label, i) => (
58
- <div key={label}>
59
- <Label>{label}</Label>
60
- <Input
61
- value={args[i] ?? schema.defaults[i]}
62
- step="0.1"
63
- onChange={value => {
64
- const next = [...args];
65
- next[i] = value;
66
- onUpdate({ args: next });
67
- }}
68
- />
69
- </div>
70
- ))}
71
- </div>
84
+ <FieldRenderer
85
+ fields={fields}
86
+ values={component.properties}
87
+ onChange={handleChange}
88
+ />
72
89
  );
73
90
  }
74
91