react-three-game 0.0.65 → 0.0.67

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 (38) hide show
  1. package/LICENSE +2 -660
  2. package/README.md +164 -91
  3. package/dist/index.d.ts +5 -3
  4. package/dist/index.js +3 -2
  5. package/dist/shared/GameCanvas.js +1 -1
  6. package/dist/tools/assetviewer/page.d.ts +13 -2
  7. package/dist/tools/assetviewer/page.js +61 -7
  8. package/dist/tools/dragdrop/index.d.ts +1 -1
  9. package/dist/tools/dragdrop/modelLoader.d.ts +2 -0
  10. package/dist/tools/prefabeditor/EditorContext.d.ts +2 -2
  11. package/dist/tools/prefabeditor/EditorTree.js +17 -3
  12. package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +3 -1
  13. package/dist/tools/prefabeditor/EditorTreeMenus.js +7 -8
  14. package/dist/tools/prefabeditor/EditorUI.js +3 -7
  15. package/dist/tools/prefabeditor/GameEvents.d.ts +14 -1
  16. package/dist/tools/prefabeditor/GameEvents.js +2 -1
  17. package/dist/tools/prefabeditor/InstanceProvider.d.ts +4 -0
  18. package/dist/tools/prefabeditor/InstanceProvider.js +44 -12
  19. package/dist/tools/prefabeditor/PrefabEditor.js +77 -16
  20. package/dist/tools/prefabeditor/PrefabRoot.d.ts +9 -6
  21. package/dist/tools/prefabeditor/PrefabRoot.js +52 -126
  22. package/dist/tools/prefabeditor/components/CameraComponent.js +1 -1
  23. package/dist/tools/prefabeditor/components/ClickComponent.d.ts +3 -0
  24. package/dist/tools/prefabeditor/components/ClickComponent.js +45 -0
  25. package/dist/tools/prefabeditor/components/ComponentRegistry.js +0 -3
  26. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +3 -3
  27. package/dist/tools/prefabeditor/components/Input.d.ts +5 -2
  28. package/dist/tools/prefabeditor/components/Input.js +71 -38
  29. package/dist/tools/prefabeditor/components/MaterialComponent.js +4 -69
  30. package/dist/tools/prefabeditor/components/ModelComponent.js +5 -80
  31. package/dist/tools/prefabeditor/components/PhysicsComponent.d.ts +2 -0
  32. package/dist/tools/prefabeditor/components/PhysicsComponent.js +77 -10
  33. package/dist/tools/prefabeditor/components/SpotLightComponent.js +9 -7
  34. package/dist/tools/prefabeditor/components/index.js +2 -0
  35. package/dist/tools/prefabeditor/types.d.ts +1 -0
  36. package/dist/tools/prefabeditor/utils.d.ts +7 -1
  37. package/dist/tools/prefabeditor/utils.js +34 -1
  38. package/package.json +1 -1
@@ -10,81 +10,16 @@ var __rest = (this && this.__rest) || function (s, e) {
10
10
  return t;
11
11
  };
12
12
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
- import { SingleTextureViewer, TextureListViewer } from '../../assetviewer/page';
14
13
  import { extend } from '@react-three/fiber';
15
- import { useEffect, useLayoutEffect, useRef, useState } from 'react';
16
- import { createPortal } from 'react-dom';
17
- import { FieldRenderer, Input } from './Input';
18
- import { colors } from '../styles';
14
+ import { FieldRenderer, Label, NumberInput } from './Input';
19
15
  import { useMemo } from 'react';
20
16
  import { MeshBasicNodeMaterial, MeshStandardNodeMaterial } from 'three/webgpu';
17
+ import { TexturePicker } from '../../assetviewer/page';
21
18
  import { RepeatWrapping, ClampToEdgeWrapping, SRGBColorSpace, LinearSRGBColorSpace, Vector2, NearestFilter, LinearFilter, NearestMipmapNearestFilter, NearestMipmapLinearFilter, LinearMipmapNearestFilter, LinearMipmapLinearFilter, FrontSide, BackSide, DoubleSide, } from 'three';
22
- const PICKER_POPUP_WIDTH = 260;
23
- const PICKER_POPUP_HEIGHT = 360;
24
19
  extend({
25
20
  MeshBasicNodeMaterial,
26
21
  MeshStandardNodeMaterial,
27
22
  });
28
- function TexturePicker({ value, onChange, basePath }) {
29
- const [textureFiles, setTextureFiles] = useState([]);
30
- const [showPicker, setShowPicker] = useState(false);
31
- const [popupStyle, setPopupStyle] = useState(null);
32
- const triggerRef = useRef(null);
33
- useEffect(() => {
34
- fetch(`${basePath}/textures/manifest.json`)
35
- .then(r => r.json())
36
- .then(data => setTextureFiles(Array.isArray(data) ? data : data.files || []))
37
- .catch(console.error);
38
- }, [basePath]);
39
- useLayoutEffect(() => {
40
- if (!showPicker || !triggerRef.current || typeof window === 'undefined')
41
- return;
42
- const updatePosition = () => {
43
- var _a;
44
- const rect = (_a = triggerRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
45
- if (!rect)
46
- return;
47
- const preferredLeft = rect.left - PICKER_POPUP_WIDTH - 8;
48
- const fallbackLeft = rect.right + 8;
49
- const fitsLeft = preferredLeft >= 8;
50
- const left = fitsLeft ? preferredLeft : Math.min(fallbackLeft, window.innerWidth - PICKER_POPUP_WIDTH - 8);
51
- const top = Math.min(Math.max(8, rect.top), window.innerHeight - PICKER_POPUP_HEIGHT - 8);
52
- setPopupStyle({
53
- position: 'fixed',
54
- left,
55
- top,
56
- background: colors.bg,
57
- padding: 12,
58
- border: `1px solid ${colors.border}`,
59
- borderRadius: 6,
60
- width: PICKER_POPUP_WIDTH,
61
- height: PICKER_POPUP_HEIGHT,
62
- overflow: 'hidden',
63
- zIndex: 1000,
64
- boxShadow: '0 4px 16px rgba(0,0,0,0.6)',
65
- });
66
- };
67
- updatePosition();
68
- window.addEventListener('resize', updatePosition);
69
- window.addEventListener('scroll', updatePosition, true);
70
- return () => {
71
- window.removeEventListener('resize', updatePosition);
72
- window.removeEventListener('scroll', updatePosition, true);
73
- };
74
- }, [showPicker]);
75
- // Only show 3D preview for server-hosted textures (starting with / or http)
76
- const canPreview = value && (value.startsWith('/') || value.startsWith('http'));
77
- return (_jsxs("div", { style: { maxHeight: 128, overflow: 'visible', position: 'relative', display: 'flex', alignItems: 'center' }, children: [canPreview
78
- ? _jsx(SingleTextureViewer, { file: value, basePath: basePath })
79
- : value
80
- ? _jsx("span", { style: { fontSize: 10, opacity: 0.6, maxWidth: 100, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: value })
81
- : null, _jsx("button", { ref: triggerRef, onClick: () => setShowPicker(!showPicker), style: { padding: '4px 8px', backgroundColor: colors.bgLight, color: 'inherit', fontSize: 10, cursor: 'pointer', border: `1px solid ${colors.border}`, borderRadius: 3, marginTop: 4 }, children: showPicker ? 'Cancel' : 'Change' }), _jsx("button", { onClick: () => {
82
- onChange(undefined);
83
- }, style: { padding: '4px 8px', backgroundColor: colors.bgLight, color: 'inherit', fontSize: 10, cursor: 'pointer', border: `1px solid ${colors.border}`, borderRadius: 3, marginTop: 4, marginLeft: 4 }, children: "Clear" }), showPicker && popupStyle && typeof document !== 'undefined' && createPortal(_jsx("div", { style: popupStyle, onMouseLeave: () => setShowPicker(false), children: _jsx(TextureListViewer, { files: textureFiles, selected: value || undefined, onSelect: (file) => {
84
- onChange(file);
85
- setShowPicker(false);
86
- }, basePath: basePath }) }), document.body)] }));
87
- }
88
23
  function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
89
24
  var _a;
90
25
  const materialType = (_a = component.properties.materialType) !== null && _a !== void 0 ? _a : 'standard';
@@ -138,7 +73,7 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
138
73
  label: 'Repeat (X, Y)',
139
74
  render: ({ value, onChange }) => {
140
75
  var _a, _b;
141
- return (_jsxs("div", { style: { display: 'flex', gap: 2 }, children: [_jsx(Input, { label: "X", value: (_a = value === null || value === void 0 ? void 0 : value[0]) !== null && _a !== void 0 ? _a : 1, onChange: v => { var _a; return onChange([v, (_a = value === null || value === void 0 ? void 0 : value[1]) !== null && _a !== void 0 ? _a : 1]); }, min: 0.01, max: 100, step: 0.1 }), _jsx(Input, { label: "Y", value: (_b = value === null || value === void 0 ? void 0 : value[1]) !== null && _b !== void 0 ? _b : 1, onChange: v => { var _a; return onChange([(_a = value === null || value === void 0 ? void 0 : value[0]) !== null && _a !== void 0 ? _a : 1, v]); }, min: 0.01, max: 100, step: 0.1 })] }));
76
+ return (_jsxs("div", { style: { display: 'flex', gap: 2 }, children: [_jsxs("div", { style: { flex: 1 }, children: [_jsx(Label, { children: "X" }), _jsx(NumberInput, { value: (_a = value === null || value === void 0 ? void 0 : value[0]) !== null && _a !== void 0 ? _a : 1, onChange: v => { var _a; return onChange([v, (_a = value === null || value === void 0 ? void 0 : value[1]) !== null && _a !== void 0 ? _a : 1]); }, min: 0.01, max: 100, step: 0.1, style: { width: '100%', minWidth: 0, boxSizing: 'border-box' } })] }), _jsxs("div", { style: { flex: 1 }, children: [_jsx(Label, { children: "Y" }), _jsx(NumberInput, { value: (_b = value === null || value === void 0 ? void 0 : value[1]) !== null && _b !== void 0 ? _b : 1, onChange: v => { var _a; return onChange([(_a = value === null || value === void 0 ? void 0 : value[0]) !== null && _a !== void 0 ? _a : 1, v]); }, min: 0.01, max: 100, step: 0.1, style: { width: '100%', minWidth: 0, boxSizing: 'border-box' } })] })] }));
142
77
  },
143
78
  }] : []),
144
79
  {
@@ -153,7 +88,7 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
153
88
  label: 'Normal Scale (X, Y)',
154
89
  render: ({ value, onChange }) => {
155
90
  var _a, _b;
156
- return (_jsxs("div", { style: { display: 'flex', gap: 2 }, children: [_jsx(Input, { label: "X", value: (_a = value === null || value === void 0 ? void 0 : value[0]) !== null && _a !== void 0 ? _a : 1, onChange: v => { var _a; return onChange([v, (_a = value === null || value === void 0 ? void 0 : value[1]) !== null && _a !== void 0 ? _a : 1]); }, min: 0, max: 5, step: 0.01 }), _jsx(Input, { label: "Y", value: (_b = value === null || value === void 0 ? void 0 : value[1]) !== null && _b !== void 0 ? _b : 1, onChange: v => { var _a; return onChange([(_a = value === null || value === void 0 ? void 0 : value[0]) !== null && _a !== void 0 ? _a : 1, v]); }, min: 0, max: 5, step: 0.01 })] }));
91
+ return (_jsxs("div", { style: { display: 'flex', gap: 2 }, children: [_jsxs("div", { style: { flex: 1 }, children: [_jsx(Label, { children: "X" }), _jsx(NumberInput, { value: (_a = value === null || value === void 0 ? void 0 : value[0]) !== null && _a !== void 0 ? _a : 1, onChange: v => { var _a; return onChange([v, (_a = value === null || value === void 0 ? void 0 : value[1]) !== null && _a !== void 0 ? _a : 1]); }, min: 0, max: 5, step: 0.01, style: { width: '100%', minWidth: 0, boxSizing: 'border-box' } })] }), _jsxs("div", { style: { flex: 1 }, children: [_jsx(Label, { children: "Y" }), _jsx(NumberInput, { value: (_b = value === null || value === void 0 ? void 0 : value[1]) !== null && _b !== void 0 ? _b : 1, onChange: v => { var _a; return onChange([(_a = value === null || value === void 0 ? void 0 : value[0]) !== null && _a !== void 0 ? _a : 1, v]); }, min: 0, max: 5, step: 0.01, style: { width: '100%', minWidth: 0, boxSizing: 'border-box' } })] })] }));
157
92
  },
158
93
  }] : []),
159
94
  { name: 'generateMipmaps', type: 'boolean', label: 'Generate Mipmaps' },
@@ -1,13 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { ModelListViewer, SingleModelViewer } from '../../assetviewer/page';
3
- import { useContext, useEffect, useLayoutEffect, useState, useMemo, useRef } from 'react';
4
- import { createPortal } from 'react-dom';
5
- import { BooleanField, FieldGroup, Input, Label, SelectInput } from './Input';
2
+ import { ModelPicker } from '../../assetviewer/page';
3
+ import { useContext, useMemo } from 'react';
4
+ import { BooleanField, FieldGroup, Label, NumberInput, SelectInput } from './Input';
6
5
  import { EditorContext } from '../EditorContext';
7
6
  import { DEFAULT_REPEAT_AXES, getRepeatAxesFromModelProperties, normalizeRepeatAxes } from '../InstanceProvider';
8
7
  import { colors } from '../styles';
9
- const PICKER_POPUP_WIDTH = 260;
10
- const PICKER_POPUP_HEIGHT = 360;
11
8
  const AXIS_OPTIONS = [
12
9
  { value: 'x', label: 'X' },
13
10
  { value: 'y', label: 'Y' },
@@ -68,87 +65,15 @@ function RepeatAxisEditor({ axes, onChange, positionSnap, }) {
68
65
  cursor: 'pointer',
69
66
  padding: 0,
70
67
  flexShrink: 0,
71
- }, title: "Remove repeat axis", children: "\u00D7" })) : null] }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 6 }, children: [_jsx(Input, { label: "Count", value: axisConfig.count, onChange: (count) => updateAxis(index, { count: Math.max(1, Math.floor(count)) }), step: 1, min: 1, style: { width: '100%', minWidth: 0, boxSizing: 'border-box' } }), _jsx(Input, { label: "Offset", value: axisConfig.offset, onChange: (offset) => updateAxis(index, { offset: quantize(offset, positionSnap) }), step: positionSnap > 0 ? positionSnap : 0.1, style: { width: '100%', minWidth: 0, boxSizing: 'border-box' } })] })] }, `${axisConfig.axis}-${index}`));
68
+ }, title: "Remove repeat axis", children: "\u00D7" })) : null] }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 6 }, children: [_jsxs("div", { children: [_jsx(Label, { children: "Count" }), _jsx(NumberInput, { value: axisConfig.count, onChange: (count) => updateAxis(index, { count: Math.max(1, Math.floor(count)) }), step: 1, min: 1, style: { width: '100%', minWidth: 0, boxSizing: 'border-box' } })] }), _jsxs("div", { children: [_jsx(Label, { children: "Offset" }), _jsx(NumberInput, { value: axisConfig.offset, onChange: (offset) => updateAxis(index, { offset: quantize(offset, positionSnap) }), step: positionSnap > 0 ? positionSnap : 0.1, style: { width: '100%', minWidth: 0, boxSizing: 'border-box' } })] })] })] }, `${axisConfig.axis}-${index}`));
72
69
  })] }));
73
70
  }
74
- function ModelPicker({ value, onChange, basePath, nodeId }) {
75
- const [modelFiles, setModelFiles] = useState([]);
76
- const [showPicker, setShowPicker] = useState(false);
77
- const [popupStyle, setPopupStyle] = useState(null);
78
- const triggerRef = useRef(null);
79
- useEffect(() => {
80
- fetch(`${basePath}/models/manifest.json`)
81
- .then(r => r.json())
82
- .then(data => setModelFiles(Array.isArray(data) ? data : data.files || []))
83
- .catch(console.error);
84
- }, [basePath]);
85
- useLayoutEffect(() => {
86
- if (!showPicker || !triggerRef.current || typeof window === 'undefined')
87
- return;
88
- const updatePosition = () => {
89
- var _a;
90
- const rect = (_a = triggerRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
91
- if (!rect)
92
- return;
93
- const preferredLeft = rect.left - PICKER_POPUP_WIDTH - 8;
94
- const fallbackLeft = rect.right + 8;
95
- const fitsLeft = preferredLeft >= 8;
96
- const left = fitsLeft ? preferredLeft : Math.min(fallbackLeft, window.innerWidth - PICKER_POPUP_WIDTH - 8);
97
- const top = Math.min(Math.max(8, rect.top), window.innerHeight - PICKER_POPUP_HEIGHT - 8);
98
- setPopupStyle({
99
- position: 'fixed',
100
- left,
101
- top,
102
- background: 'rgba(0,0,0,0.9)',
103
- padding: 12,
104
- border: '1px solid rgba(34, 211, 238, 0.3)',
105
- borderRadius: 6,
106
- width: PICKER_POPUP_WIDTH,
107
- height: PICKER_POPUP_HEIGHT,
108
- overflow: 'hidden',
109
- zIndex: 1000,
110
- boxShadow: '0 4px 16px rgba(0,0,0,0.6)',
111
- });
112
- };
113
- updatePosition();
114
- window.addEventListener('resize', updatePosition);
115
- window.addEventListener('scroll', updatePosition, true);
116
- return () => {
117
- window.removeEventListener('resize', updatePosition);
118
- window.removeEventListener('scroll', updatePosition, true);
119
- };
120
- }, [showPicker]);
121
- const handleModelSelect = (file) => {
122
- const filename = file.startsWith('/') ? file.slice(1) : file;
123
- onChange(filename);
124
- setShowPicker(false);
125
- };
126
- return (_jsxs("div", { style: { maxHeight: 160, overflow: 'visible', position: 'relative', display: 'flex', gap: 8, alignItems: 'center', justifyContent: 'center' }, children: [_jsx("div", { style: { flex: '0 0 auto' }, children: _jsx(SingleModelViewer, { file: value ? `/${value}` : undefined, basePath: basePath }) }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 6, flex: '0 0 84px', minWidth: 84, justifyContent: 'flex-end' }, children: [_jsx("button", { ref: triggerRef, onClick: () => setShowPicker(!showPicker), style: {
127
- width: '100%',
128
- padding: '6px 8px',
129
- backgroundColor: '#1f2937',
130
- color: 'inherit',
131
- fontSize: 10,
132
- cursor: 'pointer',
133
- border: '1px solid rgba(34, 211, 238, 0.3)',
134
- }, children: showPicker ? 'Cancel' : 'Change' }), _jsx("button", { onClick: () => {
135
- onChange(undefined);
136
- }, style: {
137
- width: '100%',
138
- padding: '6px 8px',
139
- backgroundColor: '#1f2937',
140
- color: 'inherit',
141
- fontSize: 10,
142
- cursor: 'pointer',
143
- border: '1px solid rgba(34, 211, 238, 0.3)',
144
- }, children: "Clear" })] }), showPicker && popupStyle && typeof document !== 'undefined' && createPortal(_jsx("div", { style: popupStyle, onMouseLeave: () => setShowPicker(false), children: _jsx(ModelListViewer, { files: modelFiles, selected: value ? `/${value}` : undefined, onSelect: handleModelSelect, basePath: basePath }, nodeId) }), document.body)] }));
145
- }
146
71
  function ModelComponentEditor({ component, node, onUpdate, basePath = "" }) {
147
72
  var _a;
148
73
  const editorContext = useContext(EditorContext);
149
74
  const positionSnap = (_a = editorContext === null || editorContext === void 0 ? void 0 : editorContext.positionSnap) !== null && _a !== void 0 ? _a : 0.5;
150
75
  const repeatAxes = getRepeatAxesFromModelProperties(component.properties);
151
- return (_jsxs(FieldGroup, { children: [_jsx(ModelPicker, { value: component.properties.filename, onChange: (filename) => onUpdate({ filename }), basePath: basePath, nodeId: node === null || node === void 0 ? void 0 : node.id }), _jsx(BooleanField, { name: "instanced", label: "Instanced", values: component.properties, onChange: onUpdate, fallback: false }), component.properties.instanced && (_jsxs(_Fragment, { children: [_jsx(BooleanField, { name: "repeat", label: "Repeat", values: component.properties, onChange: onUpdate, fallback: false }), component.properties.repeat && (_jsx(RepeatAxisEditor, { axes: repeatAxes, onChange: (nextAxes) => onUpdate({ repeatAxes: nextAxes }), positionSnap: positionSnap }))] }))] }));
76
+ return (_jsxs(FieldGroup, { children: [_jsx(ModelPicker, { value: component.properties.filename, onChange: (filename) => onUpdate({ filename }), basePath: basePath, pickerKey: node === null || node === void 0 ? void 0 : node.id }), _jsx(BooleanField, { name: "instanced", label: "Instanced", values: component.properties, onChange: onUpdate, fallback: false }), component.properties.instanced && (_jsxs(_Fragment, { children: [_jsx(BooleanField, { name: "repeat", label: "Repeat", values: component.properties, onChange: onUpdate, fallback: false }), component.properties.repeat && (_jsx(RepeatAxisEditor, { axes: repeatAxes, onChange: (nextAxes) => onUpdate({ repeatAxes: nextAxes }), positionSnap: positionSnap }))] }))] }));
152
77
  }
153
78
  // View for Model component
154
79
  function ModelComponentView({ properties, loadedModels, children }) {
@@ -2,6 +2,8 @@ import type { RigidBodyOptions } from "@react-three/rapier";
2
2
  import { Component } from "./ComponentRegistry";
3
3
  export type PhysicsProps = RigidBodyOptions & {
4
4
  activeCollisionTypes?: 'all' | undefined;
5
+ linearVelocity?: [number, number, number];
6
+ angularVelocity?: [number, number, number];
5
7
  };
6
8
  declare const PhysicsComponent: Component;
7
9
  export default PhysicsComponent;
@@ -12,8 +12,50 @@ var __rest = (this && this.__rest) || function (s, e) {
12
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 { BooleanField, FieldGroup, NumberField, SelectField } from "./Input";
15
+ import { BooleanField, FieldGroup, NumberField, SelectField, Vector3Field } from "./Input";
16
16
  import { gameEvents, getEntityIdFromRigidBody } from "../GameEvents";
17
+ import { colors } from "../styles";
18
+ const enabledAxesFallback = [true, true, true];
19
+ function LockedAxisField({ label, values, onChange, }) {
20
+ const enabledTranslations = Array.isArray(values.enabledTranslations)
21
+ ? values.enabledTranslations
22
+ : enabledAxesFallback;
23
+ const axisLabels = ['X', 'Y', 'Z'];
24
+ const toggleAxisLock = (index) => {
25
+ const nextEnabledTranslations = [...enabledTranslations];
26
+ nextEnabledTranslations[index] = !nextEnabledTranslations[index];
27
+ onChange({ enabledTranslations: nextEnabledTranslations });
28
+ };
29
+ return (_jsxs("div", { children: [_jsxs("div", { style: {
30
+ display: 'flex',
31
+ alignItems: 'center',
32
+ justifyContent: 'space-between',
33
+ marginBottom: 4,
34
+ }, children: [_jsx("span", { style: {
35
+ display: 'block',
36
+ fontSize: '10px',
37
+ color: colors.textMuted,
38
+ textTransform: 'uppercase',
39
+ letterSpacing: '0.05em',
40
+ fontWeight: 500,
41
+ }, children: label }), _jsx("span", { style: {
42
+ fontSize: '10px',
43
+ color: colors.textDim,
44
+ }, children: "Active means locked" })] }), _jsx("div", { style: { display: 'flex', gap: 4 }, children: axisLabels.map((axisLabel, index) => {
45
+ const isLocked = !enabledTranslations[index];
46
+ return (_jsx("button", { type: "button", onClick: () => toggleAxisLock(index), style: {
47
+ flex: 1,
48
+ backgroundColor: isLocked ? colors.dangerBg : colors.bgInput,
49
+ border: `1px solid ${isLocked ? colors.dangerBorder : colors.border}`,
50
+ borderRadius: 3,
51
+ padding: '6px 8px',
52
+ color: isLocked ? colors.danger : colors.textMuted,
53
+ fontSize: '11px',
54
+ fontFamily: 'monospace',
55
+ cursor: 'pointer',
56
+ }, children: axisLabel }, axisLabel));
57
+ }) })] }));
58
+ }
17
59
  function PhysicsComponentEditor({ component, onUpdate }) {
18
60
  return (_jsxs(FieldGroup, { children: [_jsx(SelectField, { name: "type", label: "Type", values: component.properties, onChange: onUpdate, options: [
19
61
  { value: 'dynamic', label: 'Dynamic' },
@@ -25,15 +67,20 @@ function PhysicsComponentEditor({ component, onUpdate }) {
25
67
  { value: 'trimesh', label: 'Trimesh (exact)' },
26
68
  { value: 'cuboid', label: 'Cuboid (box)' },
27
69
  { 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: [
70
+ ] }), _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(Vector3Field, { name: "linearVelocity", label: "Linear Velocity", values: component.properties, onChange: onUpdate, fallback: [0, 0, 0] }), _jsx(Vector3Field, { name: "angularVelocity", label: "Angular Velocity", values: component.properties, onChange: onUpdate, fallback: [0, 0, 0] }), _jsx(LockedAxisField, { label: "Lock Movement", values: component.properties, onChange: onUpdate }), _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
71
  { value: '', label: 'Default (Dynamic only)' },
30
72
  { value: 'all', label: 'All (includes kinematic & fixed)' },
31
73
  ] })] }));
32
74
  }
33
75
  function PhysicsComponentView({ properties, children, position, rotation, scale, editMode, nodeId, registerRigidBodyRef }) {
34
- const { type, colliders, sensor, activeCollisionTypes } = properties, otherProps = __rest(properties, ["type", "colliders", "sensor", "activeCollisionTypes"]);
76
+ const { type, colliders, sensor, activeCollisionTypes, linearVelocity = [0, 0, 0], angularVelocity = [0, 0, 0], enabledTranslations = enabledAxesFallback } = properties, otherProps = __rest(properties, ["type", "colliders", "sensor", "activeCollisionTypes", "linearVelocity", "angularVelocity", "enabledTranslations"]);
35
77
  const colliderType = colliders || (type === 'fixed' ? 'trimesh' : 'hull');
36
78
  const rigidBodyRef = useRef(null);
79
+ const linearVelocityKey = linearVelocity.join(',');
80
+ const angularVelocityKey = angularVelocity.join(',');
81
+ const rbKey = editMode
82
+ ? `${type || 'dynamic'}_${colliderType}_${position === null || position === void 0 ? void 0 : position.join(',')}_${rotation === null || rotation === void 0 ? void 0 : rotation.join(',')}`
83
+ : `${type || 'dynamic'}_${colliderType}`;
37
84
  // Try to get rapier context - will be null if not inside <Physics>
38
85
  let rapier = null;
39
86
  try {
@@ -67,6 +114,25 @@ function PhysicsComponentView({ properties, children, position, rotation, scale,
67
114
  }
68
115
  }
69
116
  }, [activeCollisionTypes, rapier, type, colliders]);
117
+ // Seed authored velocities when the body instance changes or the authored values change.
118
+ useEffect(() => {
119
+ if (!rigidBodyRef.current)
120
+ return;
121
+ rigidBodyRef.current.setLinvel({
122
+ x: linearVelocity[0],
123
+ y: linearVelocity[1],
124
+ z: linearVelocity[2],
125
+ }, true);
126
+ }, [rbKey, linearVelocityKey]);
127
+ useEffect(() => {
128
+ if (!rigidBodyRef.current)
129
+ return;
130
+ rigidBodyRef.current.setAngvel({
131
+ x: angularVelocity[0],
132
+ y: angularVelocity[1],
133
+ z: angularVelocity[2],
134
+ }, true);
135
+ }, [rbKey, angularVelocityKey]);
70
136
  // Event handlers for physics interactions
71
137
  const handleIntersectionEnter = useCallback((payload) => {
72
138
  if (!nodeId)
@@ -104,18 +170,19 @@ function PhysicsComponentView({ properties, children, position, rotation, scale,
104
170
  targetRigidBody: payload.other.rigidBody,
105
171
  });
106
172
  }, [nodeId]);
107
- // In edit mode, include position/rotation in key to force remount when transform changes
108
- // This ensures the RigidBody debug visualization updates even when physics is paused
109
- const rbKey = editMode
110
- ? `${type || 'dynamic'}_${colliderType}_${position === null || position === void 0 ? void 0 : position.join(',')}_${rotation === null || rotation === void 0 ? void 0 : rotation.join(',')}`
111
- : `${type || 'dynamic'}_${colliderType}`;
112
- return (_jsx(RigidBody, Object.assign({ ref: rigidBodyRef, type: type, colliders: colliderType, position: position, rotation: rotation, scale: scale, sensor: sensor, userData: { entityId: nodeId }, onIntersectionEnter: handleIntersectionEnter, onIntersectionExit: handleIntersectionExit, onCollisionEnter: handleCollisionEnter, onCollisionExit: handleCollisionExit }, otherProps, { children: children }), rbKey));
173
+ return (_jsx(RigidBody, Object.assign({ ref: rigidBodyRef, type: type, colliders: colliderType, position: position, rotation: rotation, scale: scale, sensor: sensor, enabledTranslations: enabledTranslations, userData: { entityId: nodeId }, onIntersectionEnter: handleIntersectionEnter, onIntersectionExit: handleIntersectionExit, onCollisionEnter: handleCollisionEnter, onCollisionExit: handleCollisionExit }, otherProps, { children: children }), rbKey));
113
174
  }
114
175
  const PhysicsComponent = {
115
176
  name: 'Physics',
116
177
  Editor: PhysicsComponentEditor,
117
178
  View: PhysicsComponentView,
118
179
  nonComposable: true,
119
- defaultProperties: { type: 'dynamic', colliders: 'hull' }
180
+ defaultProperties: {
181
+ type: 'dynamic',
182
+ colliders: 'hull',
183
+ linearVelocity: [0, 0, 0],
184
+ angularVelocity: [0, 0, 0],
185
+ enabledTranslations: [true, true, true],
186
+ }
120
187
  };
121
188
  export default PhysicsComponent;
@@ -1,21 +1,22 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useRef, useEffect, useMemo, useState } from "react";
3
- import { BooleanField, ColorField, FieldGroup, NumberField } from "./Input";
3
+ import { BooleanField, ColorField, FieldGroup, Label, NumberField } from "./Input";
4
4
  import { SpotLightHelper } from "three";
5
5
  import { useFrame } from "@react-three/fiber";
6
+ import { TexturePicker } from "../../assetviewer/page";
6
7
  const spotLightDefaults = {
7
8
  color: '#ffffff',
8
- intensity: 1,
9
- angle: Math.PI / 6,
9
+ intensity: 200,
10
+ angle: 0.5,
10
11
  penumbra: 0.5,
11
12
  distance: 100,
12
13
  castShadow: true,
13
14
  };
14
- function SpotLightComponentEditor({ component, onUpdate }) {
15
+ function SpotLightComponentEditor({ component, onUpdate, basePath = "" }) {
15
16
  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 })] }));
17
+ 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: 1, fallback: 200 }), _jsx(NumberField, { name: "angle", label: "Angle", values: values, onChange: onUpdate, min: 0, max: Math.PI, step: 0.05, fallback: 0.5 }), _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 }), _jsxs("div", { children: [_jsx(Label, { children: "Texture Map" }), _jsx(TexturePicker, { value: values.map, onChange: (map) => onUpdate({ map }), basePath: basePath })] })] }));
17
18
  }
18
- function SpotLightView({ properties, editMode, isSelected }) {
19
+ function SpotLightView({ properties, editMode, isSelected, loadedTextures }) {
19
20
  const merged = Object.assign(Object.assign({}, spotLightDefaults), properties);
20
21
  const color = merged.color;
21
22
  const intensity = merged.intensity;
@@ -23,6 +24,7 @@ function SpotLightView({ properties, editMode, isSelected }) {
23
24
  const penumbra = merged.penumbra;
24
25
  const distance = merged.distance;
25
26
  const castShadow = merged.castShadow;
27
+ const textureMap = merged.map && loadedTextures ? loadedTextures[merged.map] : undefined;
26
28
  const spotLightRef = useRef(null);
27
29
  const targetRef = useRef(null);
28
30
  const [spotLight, setSpotLight] = useState(null);
@@ -43,7 +45,7 @@ function SpotLightView({ properties, editMode, isSelected }) {
43
45
  spotLightHelper.update();
44
46
  }
45
47
  });
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 })] })] }))] }));
48
+ return (_jsxs(_Fragment, { children: [_jsx("spotLight", { ref: spotLightRef, color: color, intensity: intensity, angle: angle, penumbra: penumbra, distance: distance, map: textureMap, 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 })] })] }))] }));
47
49
  }
48
50
  const SpotLightComponent = {
49
51
  name: 'SpotLight',
@@ -9,6 +9,7 @@ import ModelComponent from './ModelComponent';
9
9
  import TextComponent from './TextComponent';
10
10
  import EnvironmentComponent from './EnvironmentComponent';
11
11
  import CameraComponent from './CameraComponent';
12
+ import ClickComponent from './ClickComponent';
12
13
  export default [
13
14
  GeometryComponent,
14
15
  TransformComponent,
@@ -21,4 +22,5 @@ export default [
21
22
  TextComponent,
22
23
  EnvironmentComponent,
23
24
  CameraComponent,
25
+ ClickComponent,
24
26
  ];
@@ -7,6 +7,7 @@ export interface GameObject {
7
7
  id: string;
8
8
  name?: string;
9
9
  disabled?: boolean;
10
+ locked?: boolean;
10
11
  children?: GameObject[];
11
12
  components?: {
12
13
  [key: string]: ComponentData | undefined;
@@ -1,5 +1,5 @@
1
1
  import { GameObject, Prefab } from "./types";
2
- import { Object3D, Vector3 } from 'three';
2
+ import { Matrix4, Object3D, Vector3 } from 'three';
3
3
  export interface ExportGLBOptions {
4
4
  filename?: string;
5
5
  binary?: boolean;
@@ -24,6 +24,12 @@ export declare function exportGLB(sceneRoot: Object3D, options?: ExportGLBOption
24
24
  */
25
25
  export declare function exportGLBData(sceneRoot: Object3D): Promise<ArrayBuffer>;
26
26
  export declare function focusCameraOnObject(object: Object3D, camera: Object3D, target: Vector3, update?: () => void): void;
27
+ export declare function decompose(m: Matrix4): {
28
+ position: [number, number, number];
29
+ rotation: [number, number, number];
30
+ scale: [number, number, number];
31
+ };
32
+ export declare function computeParentWorldMatrix(root: GameObject, targetId: string): Matrix4;
27
33
  /** Find a node by ID in the tree */
28
34
  export declare function findNode(root: GameObject, id: string): GameObject | null;
29
35
  /** Find the parent of a node by ID */
@@ -8,7 +8,7 @@ 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
- import { Box3, PerspectiveCamera, Quaternion, Vector3 } from 'three';
11
+ import { Box3, Euler, Matrix4, PerspectiveCamera, Quaternion, Vector3 } from 'three';
12
12
  /** Save a prefab as JSON file, showing a Save As dialog when supported */
13
13
  export function saveJson(data, filename) {
14
14
  return __awaiter(this, void 0, void 0, function* () {
@@ -133,6 +133,39 @@ export function focusCameraOnObject(object, camera, target, update) {
133
133
  target.copy(center);
134
134
  update === null || update === void 0 ? void 0 : update();
135
135
  }
136
+ export function decompose(m) {
137
+ const p = new Vector3(), q = new Quaternion(), s = new Vector3();
138
+ m.decompose(p, q, s);
139
+ const e = new Euler().setFromQuaternion(q);
140
+ return {
141
+ position: [p.x, p.y, p.z],
142
+ rotation: [e.x, e.y, e.z],
143
+ scale: [s.x, s.y, s.z],
144
+ };
145
+ }
146
+ function compose(node) {
147
+ var _a, _b, _c, _d, _e;
148
+ const t = (_b = (_a = node === null || node === void 0 ? void 0 : node.components) === null || _a === void 0 ? void 0 : _a.transform) === null || _b === void 0 ? void 0 : _b.properties;
149
+ const position = (_c = t === null || t === void 0 ? void 0 : t.position) !== null && _c !== void 0 ? _c : [0, 0, 0];
150
+ const rotation = (_d = t === null || t === void 0 ? void 0 : t.rotation) !== null && _d !== void 0 ? _d : [0, 0, 0];
151
+ const scale = (_e = t === null || t === void 0 ? void 0 : t.scale) !== null && _e !== void 0 ? _e : [1, 1, 1];
152
+ return new Matrix4().compose(new Vector3(...position), new Quaternion().setFromEuler(new Euler(...rotation)), new Vector3(...scale));
153
+ }
154
+ export function computeParentWorldMatrix(root, targetId) {
155
+ const identity = new Matrix4();
156
+ let result = null;
157
+ const visit = (node, parent) => {
158
+ var _a;
159
+ if (node.id === targetId) {
160
+ result = parent.clone();
161
+ return;
162
+ }
163
+ const world = parent.clone().multiply(compose(node));
164
+ (_a = node.children) === null || _a === void 0 ? void 0 : _a.forEach(child => !result && visit(child, world));
165
+ };
166
+ visit(root, identity);
167
+ return result !== null && result !== void 0 ? result : identity;
168
+ }
136
169
  /** Find a node by ID in the tree */
137
170
  export function findNode(root, id) {
138
171
  var _a;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-three-game",
3
- "version": "0.0.65",
3
+ "version": "0.0.67",
4
4
  "description": "high performance 3D game engine for React",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",