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.
- package/README.md +38 -4
- package/dist/index.d.ts +7 -6
- package/dist/index.js +9 -6
- package/dist/shared/GameCanvas.js +1 -1
- package/dist/tools/assetviewer/page.js +2 -2
- package/dist/tools/prefabeditor/EditorUI.js +3 -5
- package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +0 -2
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +27 -27
- package/dist/tools/prefabeditor/components/GeometryComponent.js +40 -21
- package/dist/tools/prefabeditor/components/Input.d.ts +78 -1
- package/dist/tools/prefabeditor/components/Input.js +65 -0
- package/dist/tools/prefabeditor/components/MaterialComponent.js +57 -26
- package/dist/tools/prefabeditor/components/ModelComponent.js +17 -8
- package/dist/tools/prefabeditor/components/PhysicsComponent.js +25 -14
- package/dist/tools/prefabeditor/components/SpotLightComponent.js +10 -21
- package/dist/tools/prefabeditor/components/TransformComponent.js +27 -19
- package/dist/tools/prefabeditor/page.js +1 -1
- package/package.json +1 -1
- package/src/index.ts +28 -10
- package/src/shared/GameCanvas.tsx +1 -1
- package/src/tools/assetviewer/page.tsx +3 -3
- package/src/tools/prefabeditor/EditorUI.tsx +0 -10
- package/src/tools/prefabeditor/components/ComponentRegistry.ts +3 -5
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +72 -76
- package/src/tools/prefabeditor/components/GeometryComponent.tsx +55 -38
- package/src/tools/prefabeditor/components/Input.tsx +299 -0
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +97 -140
- package/src/tools/prefabeditor/components/ModelComponent.tsx +62 -41
- package/src/tools/prefabeditor/components/PhysicsComponent.tsx +29 -33
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +17 -65
- package/src/tools/prefabeditor/components/TransformComponent.tsx +83 -56
- package/src/tools/prefabeditor/page.tsx +1 -1
- 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 {
|
|
5
|
-
function
|
|
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
|
-
|
|
17
|
+
onChange(filename);
|
|
18
|
+
setShowPicker(false);
|
|
19
19
|
};
|
|
20
|
-
return
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { RigidBody } from "@react-three/rapier";
|
|
3
|
-
import {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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", {
|
|
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
package/src/index.ts
CHANGED
|
@@ -1,16 +1,38 @@
|
|
|
1
|
-
// Core
|
|
1
|
+
// Core
|
|
2
2
|
export { default as GameCanvas } from './shared/GameCanvas';
|
|
3
3
|
|
|
4
|
-
//
|
|
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
|
-
|
|
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
|
-
<
|
|
273
|
-
|
|
274
|
-
|
|
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 {
|
|
5
|
+
import { FieldRenderer, FieldDefinition, Input } from "./Input";
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
<div
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|