react-three-game 0.0.65 → 0.0.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/LICENSE +2 -660
  2. package/README.md +164 -91
  3. package/dist/index.d.ts +4 -2
  4. package/dist/index.js +2 -1
  5. package/dist/tools/assetviewer/page.js +4 -4
  6. package/dist/tools/prefabeditor/EditorContext.d.ts +2 -2
  7. package/dist/tools/prefabeditor/EditorTree.js +17 -3
  8. package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +3 -1
  9. package/dist/tools/prefabeditor/EditorTreeMenus.js +7 -8
  10. package/dist/tools/prefabeditor/EditorUI.js +3 -7
  11. package/dist/tools/prefabeditor/GameEvents.d.ts +14 -1
  12. package/dist/tools/prefabeditor/GameEvents.js +2 -1
  13. package/dist/tools/prefabeditor/InstanceProvider.d.ts +4 -0
  14. package/dist/tools/prefabeditor/InstanceProvider.js +44 -12
  15. package/dist/tools/prefabeditor/PrefabEditor.js +77 -16
  16. package/dist/tools/prefabeditor/PrefabRoot.d.ts +3 -1
  17. package/dist/tools/prefabeditor/PrefabRoot.js +36 -119
  18. package/dist/tools/prefabeditor/components/CameraComponent.js +1 -1
  19. package/dist/tools/prefabeditor/components/ClickComponent.d.ts +3 -0
  20. package/dist/tools/prefabeditor/components/ClickComponent.js +45 -0
  21. package/dist/tools/prefabeditor/components/ComponentRegistry.js +0 -3
  22. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +3 -3
  23. package/dist/tools/prefabeditor/components/Input.d.ts +5 -2
  24. package/dist/tools/prefabeditor/components/Input.js +71 -38
  25. package/dist/tools/prefabeditor/components/MaterialComponent.js +3 -3
  26. package/dist/tools/prefabeditor/components/ModelComponent.js +2 -2
  27. package/dist/tools/prefabeditor/components/PhysicsComponent.d.ts +2 -0
  28. package/dist/tools/prefabeditor/components/PhysicsComponent.js +77 -10
  29. package/dist/tools/prefabeditor/components/index.js +2 -0
  30. package/dist/tools/prefabeditor/types.d.ts +1 -0
  31. package/dist/tools/prefabeditor/utils.d.ts +7 -1
  32. package/dist/tools/prefabeditor/utils.js +34 -1
  33. package/package.json +1 -1
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useRef } from 'react';
3
+ import { gameEvents } from '../GameEvents';
4
+ import { FieldGroup } from './Input';
5
+ function ClickComponentEditor() {
6
+ return (_jsx(FieldGroup, { children: _jsx("div", { style: { fontSize: 12, opacity: 0.8 }, children: "Emits a click game event in play mode when this entity is clicked." }) }));
7
+ }
8
+ function ClickComponentView({ children, editMode, nodeId }) {
9
+ const clickValid = useRef(false);
10
+ const emitClick = (event) => {
11
+ if (!nodeId)
12
+ return;
13
+ gameEvents.emit('click', {
14
+ sourceEntityId: nodeId,
15
+ point: [event.point.x, event.point.y, event.point.z],
16
+ button: event.button,
17
+ altKey: event.nativeEvent.altKey,
18
+ ctrlKey: event.nativeEvent.ctrlKey,
19
+ metaKey: event.nativeEvent.metaKey,
20
+ shiftKey: event.nativeEvent.shiftKey,
21
+ });
22
+ };
23
+ if (editMode) {
24
+ return _jsx(_Fragment, { children: children });
25
+ }
26
+ return (_jsx("group", { onPointerDown: (event) => {
27
+ event.stopPropagation();
28
+ clickValid.current = true;
29
+ }, onPointerMove: () => {
30
+ clickValid.current = false;
31
+ }, onPointerUp: (event) => {
32
+ if (!clickValid.current)
33
+ return;
34
+ event.stopPropagation();
35
+ emitClick(event);
36
+ clickValid.current = false;
37
+ }, children: children }));
38
+ }
39
+ const ClickComponent = {
40
+ name: 'Click',
41
+ Editor: ClickComponentEditor,
42
+ View: ClickComponentView,
43
+ defaultProperties: {},
44
+ };
45
+ export default ClickComponent;
@@ -1,8 +1,5 @@
1
1
  const REGISTRY = {};
2
2
  export function registerComponent(component) {
3
- if (REGISTRY[component.name]) {
4
- throw new Error(`Component with name ${component.name} already registered.`);
5
- }
6
3
  REGISTRY[component.name] = component;
7
4
  }
8
5
  export function getComponent(name) {
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useRef, useEffect, useMemo, useState } from "react";
3
3
  import { useFrame } from "@react-three/fiber";
4
4
  import { CameraHelper, Vector3 } from "three";
5
- import { FieldRenderer, Input } from "./Input";
5
+ import { FieldRenderer, NumberInput } from "./Input";
6
6
  const smallLabel = { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 };
7
7
  const directionalLightDefaults = {
8
8
  color: '#ffffff',
@@ -28,7 +28,7 @@ const directionalLightFields = [
28
28
  label: 'Shadow Camera',
29
29
  render: ({ values, onChangeMultiple }) => {
30
30
  var _a, _b, _c, _d, _e, _f;
31
- return (_jsxs("div", { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4 }, children: [_jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Near" }), _jsx(Input, { step: 0.1, value: (_a = values.shadowCameraNear) !== null && _a !== void 0 ? _a : 0.1, onChange: v => onChangeMultiple({ shadowCameraNear: v }) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Far" }), _jsx(Input, { step: 1, value: (_b = values.shadowCameraFar) !== null && _b !== void 0 ? _b : 100, onChange: v => onChangeMultiple({ shadowCameraFar: v }) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Top" }), _jsx(Input, { step: 1, value: (_c = values.shadowCameraTop) !== null && _c !== void 0 ? _c : 30, onChange: v => onChangeMultiple({ shadowCameraTop: v }) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Bottom" }), _jsx(Input, { step: 1, value: (_d = values.shadowCameraBottom) !== null && _d !== void 0 ? _d : -30, onChange: v => onChangeMultiple({ shadowCameraBottom: v }) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Left" }), _jsx(Input, { step: 1, value: (_e = values.shadowCameraLeft) !== null && _e !== void 0 ? _e : -30, onChange: v => onChangeMultiple({ shadowCameraLeft: v }) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Right" }), _jsx(Input, { step: 1, value: (_f = values.shadowCameraRight) !== null && _f !== void 0 ? _f : 30, onChange: v => onChangeMultiple({ shadowCameraRight: v }) })] })] }));
31
+ return (_jsxs("div", { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4 }, children: [_jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Near" }), _jsx(NumberInput, { step: 0.1, value: (_a = values.shadowCameraNear) !== null && _a !== void 0 ? _a : 0.1, onChange: v => onChangeMultiple({ shadowCameraNear: v }) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Far" }), _jsx(NumberInput, { step: 1, value: (_b = values.shadowCameraFar) !== null && _b !== void 0 ? _b : 100, onChange: v => onChangeMultiple({ shadowCameraFar: v }) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Top" }), _jsx(NumberInput, { step: 1, value: (_c = values.shadowCameraTop) !== null && _c !== void 0 ? _c : 30, onChange: v => onChangeMultiple({ shadowCameraTop: v }) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Bottom" }), _jsx(NumberInput, { step: 1, value: (_d = values.shadowCameraBottom) !== null && _d !== void 0 ? _d : -30, onChange: v => onChangeMultiple({ shadowCameraBottom: v }) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Left" }), _jsx(NumberInput, { step: 1, value: (_e = values.shadowCameraLeft) !== null && _e !== void 0 ? _e : -30, onChange: v => onChangeMultiple({ shadowCameraLeft: v }) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Right" }), _jsx(NumberInput, { step: 1, value: (_f = values.shadowCameraRight) !== null && _f !== void 0 ? _f : 30, onChange: v => onChangeMultiple({ shadowCameraRight: v }) })] })] }));
32
32
  },
33
33
  },
34
34
  {
@@ -37,7 +37,7 @@ const directionalLightFields = [
37
37
  label: 'Target Offset',
38
38
  render: ({ value, onChange }) => {
39
39
  const offset = value !== null && value !== void 0 ? value : [0, -5, 0];
40
- return (_jsxs("div", { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 4 }, children: [_jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "X" }), _jsx(Input, { step: 0.5, value: offset[0], onChange: v => onChange([v, offset[1], offset[2]]) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Y" }), _jsx(Input, { step: 0.5, value: offset[1], onChange: v => onChange([offset[0], v, offset[2]]) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Z" }), _jsx(Input, { step: 0.5, value: offset[2], onChange: v => onChange([offset[0], offset[1], v]) })] })] }));
40
+ return (_jsxs("div", { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 4 }, children: [_jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "X" }), _jsx(NumberInput, { step: 0.5, value: offset[0], onChange: v => onChange([v, offset[1], offset[2]]) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Y" }), _jsx(NumberInput, { step: 0.5, value: offset[1], onChange: v => onChange([offset[0], v, offset[2]]) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Z" }), _jsx(NumberInput, { step: 0.5, value: offset[2], onChange: v => onChange([offset[0], offset[1], v]) })] })] }));
41
41
  },
42
42
  },
43
43
  ];
@@ -52,12 +52,15 @@ interface InputProps {
52
52
  min?: number;
53
53
  max?: number;
54
54
  style?: React.CSSProperties;
55
- label?: string;
56
55
  }
57
- export declare function Input({ value, onChange, step, min, max, style, label }: InputProps): import("react/jsx-runtime").JSX.Element;
56
+ export declare function NumberInput({ value, onChange, step, min, max, style }: InputProps): import("react/jsx-runtime").JSX.Element;
58
57
  export declare function Label({ children }: {
59
58
  children: React.ReactNode;
60
59
  }): import("react/jsx-runtime").JSX.Element;
60
+ export declare function FieldRow({ label, children, }: {
61
+ label: string;
62
+ children: React.ReactNode;
63
+ }): import("react/jsx-runtime").JSX.Element;
61
64
  export declare function Vector3Input({ label, value, onChange, snap, labelExtra }: {
62
65
  label: string;
63
66
  value: [number, number, number];
@@ -49,23 +49,59 @@ function getStepPrecision(step) {
49
49
  const decimal = stepString.split('.')[1];
50
50
  return (_a = decimal === null || decimal === void 0 ? void 0 : decimal.length) !== null && _a !== void 0 ? _a : 0;
51
51
  }
52
- export function Input({ value, onChange, step, min, max, style, label }) {
52
+ function clampNumber(value, min, max) {
53
+ if (min !== undefined && value < min)
54
+ return min;
55
+ if (max !== undefined && value > max)
56
+ return max;
57
+ return value;
58
+ }
59
+ function normalizeNumber(value, step, min, max) {
60
+ const clampedValue = clampNumber(value, min, max);
61
+ const normalizedStep = getNumericStep(step, 0);
62
+ if (!Number.isFinite(normalizedStep) || normalizedStep <= 0)
63
+ return clampedValue;
64
+ const precision = getStepPrecision(normalizedStep);
65
+ const stepBase = min !== null && min !== void 0 ? min : 0;
66
+ const steppedValue = stepBase + Math.round((clampedValue - stepBase) / normalizedStep) * normalizedStep;
67
+ return Number(steppedValue.toFixed(precision));
68
+ }
69
+ function isIncompleteNumber(value) {
70
+ return value === '' || value === '-' || value === '.' || value === '-.';
71
+ }
72
+ export function NumberInput({ value, onChange, step, min, max, style }) {
53
73
  const [draft, setDraft] = useState(() => value.toString());
74
+ const [isFocused, setIsFocused] = useState(false);
54
75
  useEffect(() => {
55
- setDraft(value.toString());
56
- }, [value]);
76
+ if (!isFocused) {
77
+ setDraft(value.toString());
78
+ }
79
+ }, [value, isFocused]);
57
80
  const handleChange = (e) => {
58
81
  const inputValue = e.target.value;
59
82
  setDraft(inputValue);
60
- const num = parseFloat(inputValue);
83
+ if (isIncompleteNumber(inputValue))
84
+ return;
85
+ const num = Number(inputValue);
61
86
  if (Number.isFinite(num)) {
62
- onChange(num);
87
+ onChange(clampNumber(num, min, max));
63
88
  }
64
89
  };
65
90
  const handleBlur = () => {
66
- const num = parseFloat(draft);
91
+ setIsFocused(false);
92
+ if (isIncompleteNumber(draft)) {
93
+ setDraft(value.toString());
94
+ return;
95
+ }
96
+ const num = Number(draft);
67
97
  if (!Number.isFinite(num)) {
68
98
  setDraft(value.toString());
99
+ return;
100
+ }
101
+ const normalized = normalizeNumber(num, step, min, max);
102
+ setDraft(normalized.toString());
103
+ if (normalized !== value) {
104
+ onChange(normalized);
69
105
  }
70
106
  };
71
107
  const dragState = useRef(null);
@@ -88,15 +124,10 @@ export function Input({ value, onChange, step, min, max, style, label }) {
88
124
  scrubStep /= 10;
89
125
  if (e.altKey)
90
126
  scrubStep *= 10;
91
- const precision = getStepPrecision(scrubStep);
92
127
  const deltaSteps = Math.round(dx / 8);
93
- let nextValue = startValue + deltaSteps * scrubStep;
94
- // Apply min/max constraints
95
- if (min !== undefined && nextValue < min)
96
- nextValue = min;
97
- if (max !== undefined && nextValue > max)
98
- nextValue = max;
99
- setDraft(nextValue.toFixed(precision));
128
+ const rawValue = startValue + deltaSteps * scrubStep;
129
+ const nextValue = normalizeNumber(rawValue, scrubStep, min, max);
130
+ setDraft(nextValue.toString());
100
131
  onChange(nextValue);
101
132
  };
102
133
  const endScrub = (e) => {
@@ -106,26 +137,23 @@ export function Input({ value, onChange, step, min, max, style, label }) {
106
137
  document.body.style.cursor = "";
107
138
  e.currentTarget.releasePointerCapture(e.pointerId);
108
139
  };
109
- if (label) {
110
- return (_jsxs("div", { style: {
111
- display: 'flex',
112
- alignItems: 'center',
113
- justifyContent: 'space-between',
114
- }, children: [_jsx("span", { style: Object.assign(Object.assign({}, styles.label), { marginBottom: 0, userSelect: 'none', flex: '0 0 auto', minWidth: 20 }), children: label }), _jsx("input", { type: "text", value: draft, onChange: handleChange, onBlur: handleBlur, onKeyDown: e => {
115
- if (e.key === 'Enter') {
116
- e.target.blur();
117
- }
118
- }, step: step, min: min, max: max, style: Object.assign(Object.assign(Object.assign({}, styles.input), { cursor: 'ew-resize' }), style), onPointerDown: startScrub, onPointerMove: onScrubMove, onPointerUp: endScrub })] }));
119
- }
120
- return (_jsx("input", { type: "text", value: draft, onChange: handleChange, onBlur: handleBlur, onKeyDown: e => {
140
+ return (_jsx("input", { type: "number", inputMode: "decimal", value: draft, onChange: handleChange, onFocus: () => setIsFocused(true), onBlur: handleBlur, onKeyDown: e => {
121
141
  if (e.key === 'Enter') {
122
142
  e.target.blur();
123
143
  }
124
- }, step: step, min: min, max: max, style: Object.assign(Object.assign(Object.assign({}, styles.input), { cursor: 'ew-resize' }), style), onPointerDown: startScrub, onPointerMove: onScrubMove, onPointerUp: endScrub }));
144
+ }, step: step !== null && step !== void 0 ? step : 'any', min: min, max: max, style: Object.assign(Object.assign(Object.assign({}, styles.input), { cursor: 'ew-resize' }), style), onPointerDown: startScrub, onPointerMove: onScrubMove, onPointerUp: endScrub }));
125
145
  }
126
146
  export function Label({ children }) {
127
147
  return _jsx("label", { style: styles.label, children: children });
128
148
  }
149
+ export function FieldRow({ label, children, }) {
150
+ return (_jsxs("div", { style: {
151
+ display: 'flex',
152
+ alignItems: 'center',
153
+ justifyContent: 'space-between',
154
+ gap: 8,
155
+ }, children: [_jsx("span", { style: Object.assign(Object.assign({}, styles.label), { marginBottom: 0, userSelect: 'none', flex: '0 0 auto', minWidth: 20 }), children: label }), children] }));
156
+ }
129
157
  export function Vector3Input({ label, value, onChange, snap, labelExtra }) {
130
158
  const snapValue = (num) => {
131
159
  if (!snap)
@@ -133,18 +161,23 @@ export function Vector3Input({ label, value, onChange, snap, labelExtra }) {
133
161
  return Math.round(num / snap) * snap;
134
162
  };
135
163
  const [draft, setDraft] = useState(() => value.map(v => v.toString()));
136
- // Sync external changes (gizmo, undo, etc.)
137
164
  useEffect(() => {
138
165
  setDraft(value.map(v => v.toString()));
139
- }, [value[0], value[1], value[2]]);
166
+ }, [value]);
140
167
  const dragState = useRef(null);
141
168
  const commit = (index) => {
142
- const num = parseFloat(draft[index]);
143
- if (Number.isFinite(num)) {
144
- const next = [...value];
145
- next[index] = snapValue(num);
146
- onChange(next);
169
+ if (isIncompleteNumber(draft[index])) {
170
+ setDraft(value.map(v => v.toString()));
171
+ return;
147
172
  }
173
+ const num = Number(draft[index]);
174
+ if (!Number.isFinite(num)) {
175
+ setDraft(value.map(v => v.toString()));
176
+ return;
177
+ }
178
+ const next = [...value];
179
+ next[index] = snapValue(num);
180
+ onChange(next);
148
181
  };
149
182
  const startScrub = (e, index) => {
150
183
  dragState.current = {
@@ -171,7 +204,7 @@ export function Vector3Input({ label, value, onChange, snap, labelExtra }) {
171
204
  next[index] = nextValue;
172
205
  setDraft(d => {
173
206
  const copy = [...d];
174
- copy[index] = nextValue.toFixed(3);
207
+ copy[index] = nextValue.toString();
175
208
  return copy;
176
209
  });
177
210
  onChange(next);
@@ -216,7 +249,7 @@ export function Vector3Input({ label, value, onChange, snap, labelExtra }) {
216
249
  width: '100%',
217
250
  minWidth: 0,
218
251
  cursor: 'inherit',
219
- }, type: "text", value: draft[index], onChange: e => {
252
+ }, type: "number", inputMode: "decimal", step: snap !== null && snap !== void 0 ? snap : 'any', value: draft[index], onChange: e => {
220
253
  const next = [...draft];
221
254
  next[index] = e.target.value;
222
255
  setDraft(next);
@@ -263,7 +296,7 @@ export function FieldGroup({ children }) {
263
296
  }
264
297
  export function NumberField({ name, label, values, onChange, fallback = 0, step, min, max, style, }) {
265
298
  var _a;
266
- return (_jsx(Input, { label: label, value: (_a = values[name]) !== null && _a !== void 0 ? _a : fallback, onChange: bindFieldChange(name, onChange), step: step, min: min, max: max, style: style }));
299
+ return (_jsx(FieldRow, { label: label, children: _jsx(NumberInput, { value: (_a = values[name]) !== null && _a !== void 0 ? _a : fallback, onChange: bindFieldChange(name, onChange), step: step, min: min, max: max, style: style }) }));
267
300
  }
268
301
  export function StringField({ name, label, values, onChange, fallback = '', placeholder, }) {
269
302
  var _a;
@@ -296,7 +329,7 @@ export function FieldRenderer({ fields, values, onChange }) {
296
329
  case 'vector3':
297
330
  return (_jsx(Vector3Input, { label: field.label, value: value !== null && value !== void 0 ? value : [0, 0, 0], onChange: v => updateField(field.name, v), snap: field.snap }, field.name));
298
331
  case 'number':
299
- return (_jsx(Input, { label: field.label, value: value !== null && value !== void 0 ? value : 0, onChange: v => updateField(field.name, v), min: field.min, max: field.max, step: field.step }, field.name));
332
+ return (_jsx(FieldRow, { label: field.label, children: _jsx(NumberInput, { value: value !== null && value !== void 0 ? value : 0, onChange: v => updateField(field.name, v), min: field.min, max: field.max, step: field.step }) }, field.name));
300
333
  case 'string':
301
334
  return (_jsx(StringInput, { label: field.label, value: value !== null && value !== void 0 ? value : '', onChange: v => updateField(field.name, v), placeholder: field.placeholder }, field.name));
302
335
  case 'color':
@@ -14,7 +14,7 @@ import { SingleTextureViewer, TextureListViewer } from '../../assetviewer/page';
14
14
  import { extend } from '@react-three/fiber';
15
15
  import { useEffect, useLayoutEffect, useRef, useState } from 'react';
16
16
  import { createPortal } from 'react-dom';
17
- import { FieldRenderer, Input } from './Input';
17
+ import { FieldRenderer, Label, NumberInput } from './Input';
18
18
  import { colors } from '../styles';
19
19
  import { useMemo } from 'react';
20
20
  import { MeshBasicNodeMaterial, MeshStandardNodeMaterial } from 'three/webgpu';
@@ -138,7 +138,7 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
138
138
  label: 'Repeat (X, Y)',
139
139
  render: ({ value, onChange }) => {
140
140
  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 })] }));
141
+ 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
142
  },
143
143
  }] : []),
144
144
  {
@@ -153,7 +153,7 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
153
153
  label: 'Normal Scale (X, Y)',
154
154
  render: ({ value, onChange }) => {
155
155
  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 })] }));
156
+ 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
157
  },
158
158
  }] : []),
159
159
  { name: 'generateMipmaps', type: 'boolean', label: 'Generate Mipmaps' },
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { ModelListViewer, SingleModelViewer } from '../../assetviewer/page';
3
3
  import { useContext, useEffect, useLayoutEffect, useState, useMemo, useRef } from 'react';
4
4
  import { createPortal } from 'react-dom';
5
- import { BooleanField, FieldGroup, Input, Label, SelectInput } from './Input';
5
+ import { BooleanField, FieldGroup, Label, NumberInput, SelectInput } from './Input';
6
6
  import { EditorContext } from '../EditorContext';
7
7
  import { DEFAULT_REPEAT_AXES, getRepeatAxesFromModelProperties, normalizeRepeatAxes } from '../InstanceProvider';
8
8
  import { colors } from '../styles';
@@ -68,7 +68,7 @@ function RepeatAxisEditor({ axes, onChange, positionSnap, }) {
68
68
  cursor: 'pointer',
69
69
  padding: 0,
70
70
  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}`));
71
+ }, 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
72
  })] }));
73
73
  }
74
74
  function ModelPicker({ value, onChange, basePath, nodeId }) {
@@ -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;
@@ -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.66",
4
4
  "description": "high performance 3D game engine for React",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",