react-three-game 0.0.56 → 0.0.58
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 +16 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/shared/GameCanvas.js +1 -3
- package/dist/tools/assetviewer/page.js +35 -14
- package/dist/tools/prefabeditor/Dropdown.d.ts +15 -0
- package/dist/tools/prefabeditor/Dropdown.js +82 -0
- package/dist/tools/prefabeditor/EditorContext.d.ts +5 -0
- package/dist/tools/prefabeditor/EditorTree.js +149 -91
- package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +33 -0
- package/dist/tools/prefabeditor/EditorTreeMenus.js +136 -0
- package/dist/tools/prefabeditor/EditorUI.js +1 -1
- package/dist/tools/prefabeditor/PrefabEditor.d.ts +1 -0
- package/dist/tools/prefabeditor/PrefabEditor.js +13 -2
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +1 -0
- package/dist/tools/prefabeditor/PrefabRoot.js +120 -34
- package/dist/tools/prefabeditor/components/AmbientLightComponent.js +3 -7
- package/dist/tools/prefabeditor/components/CameraComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/CameraComponent.js +45 -0
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +50 -24
- package/dist/tools/prefabeditor/components/EnvironmentComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/EnvironmentComponent.js +15 -0
- package/dist/tools/prefabeditor/components/GeometryComponent.js +46 -46
- package/dist/tools/prefabeditor/components/Input.d.ts +51 -1
- package/dist/tools/prefabeditor/components/Input.js +73 -21
- package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +16 -2
- package/dist/tools/prefabeditor/components/MaterialComponent.js +129 -15
- package/dist/tools/prefabeditor/components/ModelComponent.js +44 -3
- package/dist/tools/prefabeditor/components/PhysicsComponent.js +16 -81
- package/dist/tools/prefabeditor/components/SpotLightComponent.js +36 -23
- package/dist/tools/prefabeditor/components/TextComponent.js +7 -53
- package/dist/tools/prefabeditor/components/TransformComponent.js +18 -8
- package/dist/tools/prefabeditor/components/index.js +5 -1
- package/dist/tools/prefabeditor/styles.d.ts +5 -2
- package/dist/tools/prefabeditor/styles.js +7 -3
- package/dist/tools/prefabeditor/utils.d.ts +4 -3
- package/dist/tools/prefabeditor/utils.js +53 -5
- package/package.json +1 -1
- package/react-three-game-skill/react-three-game/SKILL.md +4 -1
- package/src/index.ts +7 -0
- package/src/shared/GameCanvas.tsx +0 -3
- package/src/tools/assetviewer/page.tsx +77 -45
- package/src/tools/prefabeditor/Dropdown.tsx +112 -0
- package/src/tools/prefabeditor/EditorContext.tsx +5 -0
- package/src/tools/prefabeditor/EditorTree.tsx +242 -178
- package/src/tools/prefabeditor/EditorTreeMenus.tsx +307 -0
- package/src/tools/prefabeditor/EditorUI.tsx +1 -1
- package/src/tools/prefabeditor/PrefabEditor.tsx +17 -4
- package/src/tools/prefabeditor/PrefabRoot.tsx +208 -58
- package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +5 -11
- package/src/tools/prefabeditor/components/CameraComponent.tsx +117 -0
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +61 -30
- package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +47 -0
- package/src/tools/prefabeditor/components/GeometryComponent.tsx +69 -63
- package/src/tools/prefabeditor/components/Input.tsx +220 -27
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +189 -18
- package/src/tools/prefabeditor/components/ModelComponent.tsx +51 -4
- package/src/tools/prefabeditor/components/PhysicsComponent.tsx +44 -85
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +52 -27
- package/src/tools/prefabeditor/components/TextComponent.tsx +58 -57
- package/src/tools/prefabeditor/components/TransformComponent.tsx +61 -9
- package/src/tools/prefabeditor/components/index.ts +5 -1
- package/src/tools/prefabeditor/styles.ts +7 -3
- package/src/tools/prefabeditor/utils.ts +55 -4
|
@@ -9,91 +9,26 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
9
9
|
}
|
|
10
10
|
return t;
|
|
11
11
|
};
|
|
12
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
12
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
13
13
|
import { RigidBody, useRapier } from "@react-three/rapier";
|
|
14
14
|
import { useRef, useEffect, useCallback } from 'react';
|
|
15
|
-
import {
|
|
15
|
+
import { BooleanField, FieldGroup, NumberField, SelectField } from "./Input";
|
|
16
16
|
import { gameEvents, getEntityIdFromRigidBody } from "../GameEvents";
|
|
17
|
-
const physicsFields = [
|
|
18
|
-
{
|
|
19
|
-
name: 'type',
|
|
20
|
-
type: 'select',
|
|
21
|
-
label: 'Type',
|
|
22
|
-
options: [
|
|
23
|
-
{ value: 'dynamic', label: 'Dynamic' },
|
|
24
|
-
{ value: 'fixed', label: 'Fixed' },
|
|
25
|
-
{ value: 'kinematicPosition', label: 'Kinematic Position' },
|
|
26
|
-
{ value: 'kinematicVelocity', label: 'Kinematic Velocity' },
|
|
27
|
-
],
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
name: 'colliders',
|
|
31
|
-
type: 'select',
|
|
32
|
-
label: 'Collider',
|
|
33
|
-
options: [
|
|
34
|
-
{ value: 'hull', label: 'Hull (convex)' },
|
|
35
|
-
{ value: 'trimesh', label: 'Trimesh (exact)' },
|
|
36
|
-
{ value: 'cuboid', label: 'Cuboid (box)' },
|
|
37
|
-
{ value: 'ball', label: 'Ball (sphere)' },
|
|
38
|
-
],
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
name: 'mass',
|
|
42
|
-
type: 'number',
|
|
43
|
-
label: 'Mass',
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
name: 'restitution',
|
|
47
|
-
type: 'number',
|
|
48
|
-
label: 'Restitution (Bounciness)',
|
|
49
|
-
min: 0,
|
|
50
|
-
max: 1,
|
|
51
|
-
step: 0.1,
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
name: 'friction',
|
|
55
|
-
type: 'number',
|
|
56
|
-
label: 'Friction',
|
|
57
|
-
min: 0,
|
|
58
|
-
step: 0.1,
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
name: 'linearDamping',
|
|
62
|
-
type: 'number',
|
|
63
|
-
label: 'Linear Damping',
|
|
64
|
-
min: 0,
|
|
65
|
-
step: 0.1,
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
name: 'angularDamping',
|
|
69
|
-
type: 'number',
|
|
70
|
-
label: 'Angular Damping',
|
|
71
|
-
min: 0,
|
|
72
|
-
step: 0.1,
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
name: 'gravityScale',
|
|
76
|
-
type: 'number',
|
|
77
|
-
label: 'Gravity Scale',
|
|
78
|
-
step: 0.1,
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
name: 'sensor',
|
|
82
|
-
type: 'boolean',
|
|
83
|
-
label: 'Sensor (Trigger Only)',
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
name: 'activeCollisionTypes',
|
|
87
|
-
type: 'select',
|
|
88
|
-
label: 'Collision Detection',
|
|
89
|
-
options: [
|
|
90
|
-
{ value: '', label: 'Default (Dynamic only)' },
|
|
91
|
-
{ value: 'all', label: 'All (includes kinematic & fixed)' },
|
|
92
|
-
],
|
|
93
|
-
},
|
|
94
|
-
];
|
|
95
17
|
function PhysicsComponentEditor({ component, onUpdate }) {
|
|
96
|
-
return (_jsx(
|
|
18
|
+
return (_jsxs(FieldGroup, { children: [_jsx(SelectField, { name: "type", label: "Type", values: component.properties, onChange: onUpdate, options: [
|
|
19
|
+
{ value: 'dynamic', label: 'Dynamic' },
|
|
20
|
+
{ value: 'fixed', label: 'Fixed' },
|
|
21
|
+
{ value: 'kinematicPosition', label: 'Kinematic Position' },
|
|
22
|
+
{ value: 'kinematicVelocity', label: 'Kinematic Velocity' },
|
|
23
|
+
] }), _jsx(SelectField, { name: "colliders", label: "Collider", values: component.properties, onChange: onUpdate, options: [
|
|
24
|
+
{ value: 'hull', label: 'Hull (convex)' },
|
|
25
|
+
{ value: 'trimesh', label: 'Trimesh (exact)' },
|
|
26
|
+
{ value: 'cuboid', label: 'Cuboid (box)' },
|
|
27
|
+
{ value: 'ball', label: 'Ball (sphere)' },
|
|
28
|
+
] }), _jsx(NumberField, { name: "mass", label: "Mass", values: component.properties, onChange: onUpdate, fallback: 1, step: 0.1, min: 0 }), _jsx(NumberField, { name: "restitution", label: "Restitution (Bounciness)", values: component.properties, onChange: onUpdate, fallback: 0, min: 0, max: 1, step: 0.1 }), _jsx(NumberField, { name: "friction", label: "Friction", values: component.properties, onChange: onUpdate, fallback: 0.5, min: 0, step: 0.1 }), _jsx(NumberField, { name: "linearDamping", label: "Linear Damping", values: component.properties, onChange: onUpdate, fallback: 0, min: 0, step: 0.1 }), _jsx(NumberField, { name: "angularDamping", label: "Angular Damping", values: component.properties, onChange: onUpdate, fallback: 0, min: 0, step: 0.1 }), _jsx(NumberField, { name: "gravityScale", label: "Gravity Scale", values: component.properties, onChange: onUpdate, fallback: 1, step: 0.1 }), _jsx(BooleanField, { name: "sensor", label: "Sensor (Trigger Only)", values: component.properties, onChange: onUpdate, fallback: false }), _jsx(SelectField, { name: "activeCollisionTypes", label: "Collision Detection", values: component.properties, onChange: onUpdate, options: [
|
|
29
|
+
{ value: '', label: 'Default (Dynamic only)' },
|
|
30
|
+
{ value: 'all', label: 'All (includes kinematic & fixed)' },
|
|
31
|
+
] })] }));
|
|
97
32
|
}
|
|
98
33
|
function PhysicsComponentView({ properties, children, position, rotation, scale, editMode, nodeId, registerRigidBodyRef }) {
|
|
99
34
|
const { type, colliders, sensor, activeCollisionTypes } = properties, otherProps = __rest(properties, ["type", "colliders", "sensor", "activeCollisionTypes"]);
|
|
@@ -1,41 +1,54 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useRef, useEffect } from "react";
|
|
3
|
-
import {
|
|
4
|
-
import { useHelper } from "@react-three/drei";
|
|
2
|
+
import { useRef, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { BooleanField, ColorField, FieldGroup, NumberField } from "./Input";
|
|
5
4
|
import { SpotLightHelper } from "three";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
import { useFrame } from "@react-three/fiber";
|
|
6
|
+
const spotLightDefaults = {
|
|
7
|
+
color: '#ffffff',
|
|
8
|
+
intensity: 1,
|
|
9
|
+
angle: Math.PI / 6,
|
|
10
|
+
penumbra: 0.5,
|
|
11
|
+
distance: 100,
|
|
12
|
+
castShadow: true,
|
|
13
|
+
};
|
|
14
14
|
function SpotLightComponentEditor({ component, onUpdate }) {
|
|
15
|
-
|
|
15
|
+
const values = Object.assign(Object.assign({}, spotLightDefaults), component.properties);
|
|
16
|
+
return (_jsxs(FieldGroup, { children: [_jsx(ColorField, { name: "color", label: "Color", values: values, onChange: onUpdate }), _jsx(NumberField, { name: "intensity", label: "Intensity", values: values, onChange: onUpdate, min: 0, step: 0.1, fallback: 1 }), _jsx(NumberField, { name: "angle", label: "Angle", values: values, onChange: onUpdate, min: 0, max: Math.PI, step: 0.05, fallback: Math.PI / 6 }), _jsx(NumberField, { name: "penumbra", label: "Penumbra", values: values, onChange: onUpdate, min: 0, max: 1, step: 0.05, fallback: 0.5 }), _jsx(NumberField, { name: "distance", label: "Distance", values: values, onChange: onUpdate, min: 0, step: 1, fallback: 100 }), _jsx(BooleanField, { name: "castShadow", label: "Cast Shadow", values: values, onChange: onUpdate, fallback: true })] }));
|
|
16
17
|
}
|
|
17
|
-
function SpotLightView({ properties, editMode }) {
|
|
18
|
-
|
|
19
|
-
const color =
|
|
20
|
-
const intensity =
|
|
21
|
-
const angle =
|
|
22
|
-
const penumbra =
|
|
23
|
-
const distance =
|
|
24
|
-
const castShadow =
|
|
18
|
+
function SpotLightView({ properties, editMode, isSelected }) {
|
|
19
|
+
const merged = Object.assign(Object.assign({}, spotLightDefaults), properties);
|
|
20
|
+
const color = merged.color;
|
|
21
|
+
const intensity = merged.intensity;
|
|
22
|
+
const angle = merged.angle;
|
|
23
|
+
const penumbra = merged.penumbra;
|
|
24
|
+
const distance = merged.distance;
|
|
25
|
+
const castShadow = merged.castShadow;
|
|
25
26
|
const spotLightRef = useRef(null);
|
|
26
27
|
const targetRef = useRef(null);
|
|
27
|
-
|
|
28
|
+
const [spotLight, setSpotLight] = useState(null);
|
|
29
|
+
const spotLightHelper = useMemo(() => spotLight ? new SpotLightHelper(spotLight, color) : null, [spotLight, color]);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
return () => {
|
|
32
|
+
spotLightHelper === null || spotLightHelper === void 0 ? void 0 : spotLightHelper.dispose();
|
|
33
|
+
};
|
|
34
|
+
}, [spotLightHelper]);
|
|
28
35
|
useEffect(() => {
|
|
29
36
|
if (spotLightRef.current && targetRef.current) {
|
|
30
37
|
spotLightRef.current.target = targetRef.current;
|
|
38
|
+
setSpotLight(spotLightRef.current);
|
|
31
39
|
}
|
|
32
40
|
}, []);
|
|
33
|
-
|
|
41
|
+
useFrame(() => {
|
|
42
|
+
if (spotLightHelper && editMode && isSelected) {
|
|
43
|
+
spotLightHelper.update();
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return (_jsxs(_Fragment, { children: [_jsx("spotLight", { ref: spotLightRef, color: color, intensity: intensity, angle: angle, penumbra: penumbra, distance: distance, castShadow: castShadow, "shadow-mapSize-width": 1024, "shadow-mapSize-height": 1024, "shadow-bias": -0.0001, "shadow-normalBias": 0.02 }), editMode && isSelected && spotLightHelper && (_jsx("primitive", { object: spotLightHelper })), _jsx("object3D", { ref: targetRef, position: [0, -5, 0] }), editMode && (_jsxs(_Fragment, { children: [_jsxs("mesh", { children: [_jsx("sphereGeometry", { args: [0.2, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true })] }), _jsxs("mesh", { position: [0, -5, 0], children: [_jsx("sphereGeometry", { args: [0.15, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true, opacity: 0.5, transparent: true })] })] }))] }));
|
|
34
47
|
}
|
|
35
48
|
const SpotLightComponent = {
|
|
36
49
|
name: 'SpotLight',
|
|
37
50
|
Editor: SpotLightComponentEditor,
|
|
38
51
|
View: SpotLightView,
|
|
39
|
-
defaultProperties:
|
|
52
|
+
defaultProperties: spotLightDefaults
|
|
40
53
|
};
|
|
41
54
|
export default SpotLightComponent;
|
|
@@ -1,61 +1,15 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ColorField, FieldGroup, NumberField, SelectField, StringField } from "./Input";
|
|
3
3
|
import { Text } from 'three-text/three/react';
|
|
4
4
|
import { useRef, useState, useCallback } from 'react';
|
|
5
5
|
// Initialize HarfBuzz path for font shaping
|
|
6
6
|
Text.setHarfBuzzPath('/fonts/hb.wasm');
|
|
7
7
|
function TextComponentEditor({ component, onUpdate, }) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
placeholder: 'Enter text...',
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
name: 'color',
|
|
17
|
-
type: 'color',
|
|
18
|
-
label: 'Color',
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
name: 'font',
|
|
22
|
-
type: 'string',
|
|
23
|
-
label: 'Font',
|
|
24
|
-
placeholder: '/fonts/NotoSans-Regular.ttf',
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
name: 'size',
|
|
28
|
-
type: 'number',
|
|
29
|
-
label: 'Size',
|
|
30
|
-
min: 0.01,
|
|
31
|
-
step: 0.1,
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
name: 'depth',
|
|
35
|
-
type: 'number',
|
|
36
|
-
label: 'Depth',
|
|
37
|
-
min: 0,
|
|
38
|
-
step: 0.1,
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
name: 'width',
|
|
42
|
-
type: 'number',
|
|
43
|
-
label: 'Width',
|
|
44
|
-
min: 0,
|
|
45
|
-
step: 0.5,
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
name: 'align',
|
|
49
|
-
type: 'select',
|
|
50
|
-
label: 'Align',
|
|
51
|
-
options: [
|
|
52
|
-
{ value: 'left', label: 'Left' },
|
|
53
|
-
{ value: 'center', label: 'Center' },
|
|
54
|
-
{ value: 'right', label: 'Right' },
|
|
55
|
-
],
|
|
56
|
-
},
|
|
57
|
-
];
|
|
58
|
-
return (_jsx(FieldRenderer, { fields: fields, values: component.properties, onChange: onUpdate }));
|
|
8
|
+
return (_jsxs(FieldGroup, { children: [_jsx(StringField, { name: "text", label: "Text", values: component.properties, onChange: onUpdate, placeholder: "Enter text..." }), _jsx(ColorField, { name: "color", label: "Color", values: component.properties, onChange: onUpdate }), _jsx(StringField, { name: "font", label: "Font", values: component.properties, onChange: onUpdate, placeholder: "/fonts/NotoSans-Regular.ttf" }), _jsx(NumberField, { name: "size", label: "Size", values: component.properties, onChange: onUpdate, min: 0.01, step: 0.1 }), _jsx(NumberField, { name: "depth", label: "Depth", values: component.properties, onChange: onUpdate, min: 0, step: 0.1 }), _jsx(NumberField, { name: "width", label: "Width", values: component.properties, onChange: onUpdate, min: 0, step: 0.5 }), _jsx(SelectField, { name: "align", label: "Align", values: component.properties, onChange: onUpdate, options: [
|
|
9
|
+
{ value: 'left', label: 'Left' },
|
|
10
|
+
{ value: 'center', label: 'Center' },
|
|
11
|
+
{ value: 'right', label: 'Right' },
|
|
12
|
+
] })] }));
|
|
59
13
|
}
|
|
60
14
|
function TextComponentView({ properties }) {
|
|
61
15
|
const { text = '', font, size, depth, width, align, color } = properties;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { Label, Vector3Field, Vector3Input } from "./Input";
|
|
3
3
|
import { useEditorContext } from "../EditorContext";
|
|
4
4
|
import { colors } from "../styles";
|
|
5
5
|
const buttonStyle = {
|
|
@@ -31,14 +31,24 @@ function TransformModeSelector({ transformMode, setTransformMode, snapResolution
|
|
|
31
31
|
e.currentTarget.style.background = colors.bgSurface;
|
|
32
32
|
}, children: ["Snap: ", snapResolution > 0 ? `ON (${snapResolution})` : 'OFF'] }) })] }));
|
|
33
33
|
}
|
|
34
|
+
const snapLockBtnStyle = {
|
|
35
|
+
background: 'none',
|
|
36
|
+
border: 'none',
|
|
37
|
+
cursor: 'pointer',
|
|
38
|
+
padding: '0 2px',
|
|
39
|
+
fontSize: 12,
|
|
40
|
+
lineHeight: 1,
|
|
41
|
+
color: colors.textMuted,
|
|
42
|
+
};
|
|
43
|
+
function SnapLockButton({ locked, onToggle, title }) {
|
|
44
|
+
return (_jsx("button", { style: snapLockBtnStyle, onClick: onToggle, title: title, children: locked ? '🔒' : '🔓' }));
|
|
45
|
+
}
|
|
34
46
|
function TransformComponentEditor({ component, onUpdate }) {
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
];
|
|
41
|
-
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 })] }));
|
|
47
|
+
var _a, _b;
|
|
48
|
+
const { transformMode, setTransformMode, snapResolution, setSnapResolution, positionSnap, setPositionSnap, rotationSnap, setRotationSnap } = useEditorContext();
|
|
49
|
+
const positionSnapped = positionSnap > 0;
|
|
50
|
+
const rotationSnapped = rotationSnap > 0;
|
|
51
|
+
return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column' }, children: [_jsx(TransformModeSelector, { transformMode: transformMode, setTransformMode: setTransformMode, snapResolution: snapResolution, setSnapResolution: setSnapResolution }), _jsx(Vector3Input, { label: "Position", value: (_a = component.properties.position) !== null && _a !== void 0 ? _a : [0, 0, 0], onChange: v => onUpdate({ position: v }), snap: positionSnap, labelExtra: _jsx(SnapLockButton, { locked: positionSnapped, onToggle: () => setPositionSnap(positionSnapped ? 0 : 0.5), title: positionSnapped ? `Snap ON (0.5) — click to disable` : `Snap OFF — click to enable (0.5)` }) }), _jsx(Vector3Input, { label: "Rotation", value: (_b = component.properties.rotation) !== null && _b !== void 0 ? _b : [0, 0, 0], onChange: v => onUpdate({ rotation: v }), snap: rotationSnap, labelExtra: _jsx(SnapLockButton, { locked: rotationSnapped, onToggle: () => setRotationSnap(rotationSnapped ? 0 : Math.PI / 4), title: rotationSnapped ? `Snap ON (π/4) — click to disable` : `Snap OFF — click to enable (π/4)` }) }), _jsx(Vector3Field, { name: "scale", label: "Scale", values: component.properties, onChange: onUpdate, fallback: [1, 1, 1] })] }));
|
|
42
52
|
}
|
|
43
53
|
const TransformComponent = {
|
|
44
54
|
name: 'Transform',
|
|
@@ -7,6 +7,8 @@ import DirectionalLightComponent from './DirectionalLightComponent';
|
|
|
7
7
|
import AmbientLightComponent from './AmbientLightComponent';
|
|
8
8
|
import ModelComponent from './ModelComponent';
|
|
9
9
|
import TextComponent from './TextComponent';
|
|
10
|
+
import EnvironmentComponent from './EnvironmentComponent';
|
|
11
|
+
import CameraComponent from './CameraComponent';
|
|
10
12
|
export default [
|
|
11
13
|
GeometryComponent,
|
|
12
14
|
TransformComponent,
|
|
@@ -16,5 +18,7 @@ export default [
|
|
|
16
18
|
DirectionalLightComponent,
|
|
17
19
|
AmbientLightComponent,
|
|
18
20
|
ModelComponent,
|
|
19
|
-
TextComponent
|
|
21
|
+
TextComponent,
|
|
22
|
+
EnvironmentComponent,
|
|
23
|
+
CameraComponent,
|
|
20
24
|
];
|
|
@@ -896,6 +896,8 @@ export declare const inspector: {
|
|
|
896
896
|
padding: number;
|
|
897
897
|
maxHeight: string;
|
|
898
898
|
overflowY: "auto";
|
|
899
|
+
overflowX: "hidden";
|
|
900
|
+
boxSizing: "border-box";
|
|
899
901
|
display: string;
|
|
900
902
|
flexDirection: "column";
|
|
901
903
|
gap: number;
|
|
@@ -1772,7 +1774,9 @@ export declare const menu: {
|
|
|
1772
1774
|
container: {
|
|
1773
1775
|
position: "fixed";
|
|
1774
1776
|
zIndex: number;
|
|
1775
|
-
minWidth:
|
|
1777
|
+
minWidth: string;
|
|
1778
|
+
width: string;
|
|
1779
|
+
maxWidth: string;
|
|
1776
1780
|
background: string;
|
|
1777
1781
|
border: string;
|
|
1778
1782
|
borderRadius: number;
|
|
@@ -1789,7 +1793,6 @@ export declare const toolbar: {
|
|
|
1789
1793
|
position: "absolute";
|
|
1790
1794
|
top: number;
|
|
1791
1795
|
left: string;
|
|
1792
|
-
transform: string;
|
|
1793
1796
|
display: string;
|
|
1794
1797
|
gap: number;
|
|
1795
1798
|
padding: string;
|
|
@@ -97,6 +97,8 @@ export const inspector = {
|
|
|
97
97
|
padding: 8,
|
|
98
98
|
maxHeight: '80vh',
|
|
99
99
|
overflowY: 'auto',
|
|
100
|
+
overflowX: 'hidden',
|
|
101
|
+
boxSizing: 'border-box',
|
|
100
102
|
display: 'flex',
|
|
101
103
|
flexDirection: 'column',
|
|
102
104
|
gap: 8,
|
|
@@ -130,7 +132,9 @@ export const menu = {
|
|
|
130
132
|
container: {
|
|
131
133
|
position: 'fixed',
|
|
132
134
|
zIndex: 50,
|
|
133
|
-
minWidth:
|
|
135
|
+
minWidth: 'auto',
|
|
136
|
+
width: 'max-content',
|
|
137
|
+
maxWidth: 'min(240px, calc(100vw - 16px))',
|
|
134
138
|
background: colors.bgSurface,
|
|
135
139
|
border: `1px solid ${colors.border}`,
|
|
136
140
|
borderRadius: 4,
|
|
@@ -145,6 +149,7 @@ export const menu = {
|
|
|
145
149
|
border: 'none',
|
|
146
150
|
color: colors.text,
|
|
147
151
|
fontSize: fonts.size,
|
|
152
|
+
whiteSpace: 'nowrap',
|
|
148
153
|
cursor: 'pointer',
|
|
149
154
|
outline: 'none',
|
|
150
155
|
},
|
|
@@ -156,8 +161,7 @@ export const toolbar = {
|
|
|
156
161
|
panel: {
|
|
157
162
|
position: 'absolute',
|
|
158
163
|
top: 8,
|
|
159
|
-
left: '
|
|
160
|
-
transform: 'translateX(-50%)',
|
|
164
|
+
left: '240px',
|
|
161
165
|
display: 'flex',
|
|
162
166
|
gap: 6,
|
|
163
167
|
padding: '4px 6px',
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { GameObject, Prefab } from "./types";
|
|
2
|
-
import { Object3D } from 'three';
|
|
2
|
+
import { Object3D, Vector3 } from 'three';
|
|
3
3
|
export interface ExportGLBOptions {
|
|
4
4
|
filename?: string;
|
|
5
5
|
binary?: boolean;
|
|
6
6
|
onComplete?: (result: ArrayBuffer | object) => void;
|
|
7
7
|
onError?: (error: any) => void;
|
|
8
8
|
}
|
|
9
|
-
/** Save a prefab as JSON file */
|
|
10
|
-
export declare function saveJson(data: Prefab, filename: string): void
|
|
9
|
+
/** Save a prefab as JSON file, showing a Save As dialog when supported */
|
|
10
|
+
export declare function saveJson(data: Prefab, filename: string): Promise<void>;
|
|
11
11
|
/** Load a prefab from JSON file */
|
|
12
12
|
export declare function loadJson(): Promise<Prefab | undefined>;
|
|
13
13
|
/**
|
|
@@ -23,6 +23,7 @@ export declare function exportGLB(sceneRoot: Object3D, options?: ExportGLBOption
|
|
|
23
23
|
* @returns Promise that resolves with the GLB data as ArrayBuffer
|
|
24
24
|
*/
|
|
25
25
|
export declare function exportGLBData(sceneRoot: Object3D): Promise<ArrayBuffer>;
|
|
26
|
+
export declare function focusCameraOnObject(object: Object3D, camera: Object3D, target: Vector3, update?: () => void): void;
|
|
26
27
|
/** Find a node by ID in the tree */
|
|
27
28
|
export declare function findNode(root: GameObject, id: string): GameObject | null;
|
|
28
29
|
/** Find the parent of a node by ID */
|
|
@@ -8,12 +8,33 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
10
|
import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js';
|
|
11
|
-
|
|
11
|
+
import { Box3, PerspectiveCamera, Quaternion, Vector3 } from 'three';
|
|
12
|
+
/** Save a prefab as JSON file, showing a Save As dialog when supported */
|
|
12
13
|
export function saveJson(data, filename) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
15
|
+
const json = JSON.stringify(data, null, 2);
|
|
16
|
+
if ('showSaveFilePicker' in window) {
|
|
17
|
+
try {
|
|
18
|
+
const handle = yield window.showSaveFilePicker({
|
|
19
|
+
suggestedName: `${filename || 'prefab'}.json`,
|
|
20
|
+
types: [{ description: 'JSON', accept: { 'application/json': ['.json'] } }],
|
|
21
|
+
});
|
|
22
|
+
const writable = yield handle.createWritable();
|
|
23
|
+
yield writable.write(json);
|
|
24
|
+
yield writable.close();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
if ((e === null || e === void 0 ? void 0 : e.name) === 'AbortError')
|
|
29
|
+
return; // user cancelled
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Fallback for browsers without File System Access API
|
|
33
|
+
const a = document.createElement('a');
|
|
34
|
+
a.href = "data:text/json;charset=utf-8," + encodeURIComponent(json);
|
|
35
|
+
a.download = `${filename || 'prefab'}.json`;
|
|
36
|
+
a.click();
|
|
37
|
+
});
|
|
17
38
|
}
|
|
18
39
|
/** Load a prefab from JSON file */
|
|
19
40
|
export function loadJson() {
|
|
@@ -85,6 +106,33 @@ export function exportGLBData(sceneRoot) {
|
|
|
85
106
|
return result;
|
|
86
107
|
});
|
|
87
108
|
}
|
|
109
|
+
export function focusCameraOnObject(object, camera, target, update) {
|
|
110
|
+
const bounds = new Box3().setFromObject(object);
|
|
111
|
+
const center = new Vector3();
|
|
112
|
+
const size = new Vector3();
|
|
113
|
+
const quaternion = new Quaternion();
|
|
114
|
+
object.getWorldQuaternion(quaternion);
|
|
115
|
+
if (bounds.isEmpty()) {
|
|
116
|
+
object.getWorldPosition(center);
|
|
117
|
+
size.setScalar(1);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
bounds.getCenter(center);
|
|
121
|
+
bounds.getSize(size);
|
|
122
|
+
}
|
|
123
|
+
const radius = Math.max(size.length() * 0.5, 1);
|
|
124
|
+
const forward = new Vector3(0, 0, 1).applyQuaternion(quaternion).normalize();
|
|
125
|
+
const worldUp = new Vector3(0, 1, 0);
|
|
126
|
+
const elevatedDirection = forward.clone().addScaledVector(worldUp, 0.65).normalize();
|
|
127
|
+
const distance = camera instanceof PerspectiveCamera
|
|
128
|
+
? Math.max(radius / Math.tan((camera.fov * Math.PI) / 360) * 1.8, radius * 3.5)
|
|
129
|
+
: radius * 4.5;
|
|
130
|
+
const nextPosition = center.clone().add(elevatedDirection.multiplyScalar(distance));
|
|
131
|
+
camera.position.copy(nextPosition);
|
|
132
|
+
camera.lookAt(center);
|
|
133
|
+
target.copy(center);
|
|
134
|
+
update === null || update === void 0 ? void 0 : update();
|
|
135
|
+
}
|
|
88
136
|
/** Find a node by ID in the tree */
|
|
89
137
|
export function findNode(root, id) {
|
|
90
138
|
var _a;
|
package/package.json
CHANGED
|
@@ -81,12 +81,15 @@ Every game object follows this schema:
|
|
|
81
81
|
```typescript
|
|
82
82
|
interface GameObject {
|
|
83
83
|
id: string;
|
|
84
|
+
name?: string;
|
|
84
85
|
disabled?: boolean;
|
|
85
86
|
components?: Record<string, { type: string; properties: any }>;
|
|
86
87
|
children?: GameObject[];
|
|
87
88
|
}
|
|
88
89
|
```
|
|
89
90
|
|
|
91
|
+
`disabled` is the canonical visibility toggle. Transforms are local to the parent node.
|
|
92
|
+
|
|
90
93
|
### Prefab JSON Format
|
|
91
94
|
|
|
92
95
|
Scenes are defined as JSON prefabs with a root node containing children:
|
|
@@ -178,7 +181,7 @@ import { GameCanvas, PrefabRoot } from 'react-three-game';
|
|
|
178
181
|
</GameCanvas>
|
|
179
182
|
```
|
|
180
183
|
|
|
181
|
-
**PrefabEditor**: Managed scene with editor UI and play/pause controls for physics. Full authoring tool for level design and prototyping. Includes canvas, physics, transform gizmos, and inspector. Physics only runs in play mode. Can pass R3F components as children.
|
|
184
|
+
**PrefabEditor**: Managed scene with editor UI and play/pause controls for physics. Full authoring tool for level design and prototyping. Includes canvas, physics, transform gizmos, and inspector. Physics only runs in play mode. Can pass R3F components as children. Editor actions live under `Menu > File`, and exports under `Menu > Export`.
|
|
182
185
|
|
|
183
186
|
```jsx
|
|
184
187
|
import { PrefabEditor } from 'react-three-game';
|
package/src/index.ts
CHANGED
|
@@ -15,13 +15,20 @@ export { registerComponent } from './tools/prefabeditor/components/ComponentRegi
|
|
|
15
15
|
// Prefab Editor - Input Components
|
|
16
16
|
export {
|
|
17
17
|
FieldRenderer,
|
|
18
|
+
FieldGroup,
|
|
18
19
|
Input,
|
|
19
20
|
Label,
|
|
20
21
|
Vector3Input,
|
|
22
|
+
Vector3Field,
|
|
23
|
+
NumberField,
|
|
21
24
|
ColorInput,
|
|
25
|
+
ColorField,
|
|
22
26
|
StringInput,
|
|
27
|
+
StringField,
|
|
23
28
|
BooleanInput,
|
|
29
|
+
BooleanField,
|
|
24
30
|
SelectInput,
|
|
31
|
+
SelectField,
|
|
25
32
|
} from './tools/prefabeditor/components/Input';
|
|
26
33
|
|
|
27
34
|
// Prefab Editor - Styles & Utils
|