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.
Files changed (64) hide show
  1. package/README.md +16 -3
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/shared/GameCanvas.js +1 -3
  5. package/dist/tools/assetviewer/page.js +35 -14
  6. package/dist/tools/prefabeditor/Dropdown.d.ts +15 -0
  7. package/dist/tools/prefabeditor/Dropdown.js +82 -0
  8. package/dist/tools/prefabeditor/EditorContext.d.ts +5 -0
  9. package/dist/tools/prefabeditor/EditorTree.js +149 -91
  10. package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +33 -0
  11. package/dist/tools/prefabeditor/EditorTreeMenus.js +136 -0
  12. package/dist/tools/prefabeditor/EditorUI.js +1 -1
  13. package/dist/tools/prefabeditor/PrefabEditor.d.ts +1 -0
  14. package/dist/tools/prefabeditor/PrefabEditor.js +13 -2
  15. package/dist/tools/prefabeditor/PrefabRoot.d.ts +1 -0
  16. package/dist/tools/prefabeditor/PrefabRoot.js +120 -34
  17. package/dist/tools/prefabeditor/components/AmbientLightComponent.js +3 -7
  18. package/dist/tools/prefabeditor/components/CameraComponent.d.ts +3 -0
  19. package/dist/tools/prefabeditor/components/CameraComponent.js +45 -0
  20. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +50 -24
  21. package/dist/tools/prefabeditor/components/EnvironmentComponent.d.ts +3 -0
  22. package/dist/tools/prefabeditor/components/EnvironmentComponent.js +15 -0
  23. package/dist/tools/prefabeditor/components/GeometryComponent.js +46 -46
  24. package/dist/tools/prefabeditor/components/Input.d.ts +51 -1
  25. package/dist/tools/prefabeditor/components/Input.js +73 -21
  26. package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +16 -2
  27. package/dist/tools/prefabeditor/components/MaterialComponent.js +129 -15
  28. package/dist/tools/prefabeditor/components/ModelComponent.js +44 -3
  29. package/dist/tools/prefabeditor/components/PhysicsComponent.js +16 -81
  30. package/dist/tools/prefabeditor/components/SpotLightComponent.js +36 -23
  31. package/dist/tools/prefabeditor/components/TextComponent.js +7 -53
  32. package/dist/tools/prefabeditor/components/TransformComponent.js +18 -8
  33. package/dist/tools/prefabeditor/components/index.js +5 -1
  34. package/dist/tools/prefabeditor/styles.d.ts +5 -2
  35. package/dist/tools/prefabeditor/styles.js +7 -3
  36. package/dist/tools/prefabeditor/utils.d.ts +4 -3
  37. package/dist/tools/prefabeditor/utils.js +53 -5
  38. package/package.json +1 -1
  39. package/react-three-game-skill/react-three-game/SKILL.md +4 -1
  40. package/src/index.ts +7 -0
  41. package/src/shared/GameCanvas.tsx +0 -3
  42. package/src/tools/assetviewer/page.tsx +77 -45
  43. package/src/tools/prefabeditor/Dropdown.tsx +112 -0
  44. package/src/tools/prefabeditor/EditorContext.tsx +5 -0
  45. package/src/tools/prefabeditor/EditorTree.tsx +242 -178
  46. package/src/tools/prefabeditor/EditorTreeMenus.tsx +307 -0
  47. package/src/tools/prefabeditor/EditorUI.tsx +1 -1
  48. package/src/tools/prefabeditor/PrefabEditor.tsx +17 -4
  49. package/src/tools/prefabeditor/PrefabRoot.tsx +208 -58
  50. package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +5 -11
  51. package/src/tools/prefabeditor/components/CameraComponent.tsx +117 -0
  52. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +61 -30
  53. package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +47 -0
  54. package/src/tools/prefabeditor/components/GeometryComponent.tsx +69 -63
  55. package/src/tools/prefabeditor/components/Input.tsx +220 -27
  56. package/src/tools/prefabeditor/components/MaterialComponent.tsx +189 -18
  57. package/src/tools/prefabeditor/components/ModelComponent.tsx +51 -4
  58. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +44 -85
  59. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +52 -27
  60. package/src/tools/prefabeditor/components/TextComponent.tsx +58 -57
  61. package/src/tools/prefabeditor/components/TransformComponent.tsx +61 -9
  62. package/src/tools/prefabeditor/components/index.ts +5 -1
  63. package/src/tools/prefabeditor/styles.ts +7 -3
  64. 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 { FieldRenderer } from "./Input";
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(FieldRenderer, { fields: physicsFields, values: component.properties, onChange: onUpdate }));
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 { FieldRenderer } from "./Input";
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
- const spotLightFields = [
7
- { name: 'color', type: 'color', label: 'Color' },
8
- { name: 'intensity', type: 'number', label: 'Intensity', step: 0.1, min: 0 },
9
- { name: 'angle', type: 'number', label: 'Angle', step: 0.1, min: 0, max: Math.PI },
10
- { name: 'penumbra', type: 'number', label: 'Penumbra', step: 0.1, min: 0, max: 1 },
11
- { name: 'distance', type: 'number', label: 'Distance', step: 1, min: 0 },
12
- { name: 'castShadow', type: 'boolean', label: 'Cast Shadow' },
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
- return (_jsx(FieldRenderer, { fields: spotLightFields, values: component.properties, onChange: onUpdate }));
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
- var _a, _b, _c, _d, _e, _f;
19
- const color = (_a = properties.color) !== null && _a !== void 0 ? _a : '#ffffff';
20
- const intensity = (_b = properties.intensity) !== null && _b !== void 0 ? _b : 1.0;
21
- const angle = (_c = properties.angle) !== null && _c !== void 0 ? _c : Math.PI / 6;
22
- const penumbra = (_d = properties.penumbra) !== null && _d !== void 0 ? _d : 0.5;
23
- const distance = (_e = properties.distance) !== null && _e !== void 0 ? _e : 100;
24
- const castShadow = (_f = properties.castShadow) !== null && _f !== void 0 ? _f : true;
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
- useHelper(editMode ? spotLightRef : null, SpotLightHelper, color);
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
- 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 }), _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 })] })] }))] }));
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 { FieldRenderer } from "./Input";
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
- const fields = [
9
- {
10
- name: 'text',
11
- type: 'string',
12
- label: 'Text',
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 { FieldRenderer, Label } from "./Input";
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
- const { transformMode, setTransformMode, snapResolution, setSnapResolution } = useEditorContext();
36
- const fields = [
37
- { name: 'position', type: 'vector3', label: 'Position', snap: snapResolution },
38
- { name: 'rotation', type: 'vector3', label: 'Rotation', snap: snapResolution },
39
- { name: 'scale', type: 'vector3', label: 'Scale', snap: snapResolution },
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: number;
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: 140,
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: '50%',
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
- /** Save a prefab as JSON file */
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
- const a = document.createElement('a');
14
- a.href = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data, null, 2));
15
- a.download = `${filename || 'prefab'}.json`;
16
- a.click();
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-three-game",
3
- "version": "0.0.56",
3
+ "version": "0.0.58",
4
4
  "description": "Batteries included React Three Fiber game engine",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -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
@@ -40,9 +40,6 @@ export default function GameCanvas({ loader = false, children, glConfig, ...prop
40
40
  });
41
41
  return renderer
42
42
  }}
43
- camera={{
44
- position: [0, 1, 5],
45
- }}
46
43
  {...props}
47
44
  >
48
45
  <Suspense>