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.
- package/LICENSE +2 -660
- package/README.md +164 -91
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/tools/assetviewer/page.js +4 -4
- package/dist/tools/prefabeditor/EditorContext.d.ts +2 -2
- package/dist/tools/prefabeditor/EditorTree.js +17 -3
- package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +3 -1
- package/dist/tools/prefabeditor/EditorTreeMenus.js +7 -8
- package/dist/tools/prefabeditor/EditorUI.js +3 -7
- package/dist/tools/prefabeditor/GameEvents.d.ts +14 -1
- package/dist/tools/prefabeditor/GameEvents.js +2 -1
- package/dist/tools/prefabeditor/InstanceProvider.d.ts +4 -0
- package/dist/tools/prefabeditor/InstanceProvider.js +44 -12
- package/dist/tools/prefabeditor/PrefabEditor.js +77 -16
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +3 -1
- package/dist/tools/prefabeditor/PrefabRoot.js +36 -119
- package/dist/tools/prefabeditor/components/CameraComponent.js +1 -1
- package/dist/tools/prefabeditor/components/ClickComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/ClickComponent.js +45 -0
- package/dist/tools/prefabeditor/components/ComponentRegistry.js +0 -3
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +3 -3
- package/dist/tools/prefabeditor/components/Input.d.ts +5 -2
- package/dist/tools/prefabeditor/components/Input.js +71 -38
- package/dist/tools/prefabeditor/components/MaterialComponent.js +3 -3
- package/dist/tools/prefabeditor/components/ModelComponent.js +2 -2
- package/dist/tools/prefabeditor/components/PhysicsComponent.d.ts +2 -0
- package/dist/tools/prefabeditor/components/PhysicsComponent.js +77 -10
- package/dist/tools/prefabeditor/components/index.js +2 -0
- package/dist/tools/prefabeditor/types.d.ts +1 -0
- package/dist/tools/prefabeditor/utils.d.ts +7 -1
- package/dist/tools/prefabeditor/utils.js +34 -1
- 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,
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
|
166
|
+
}, [value]);
|
|
140
167
|
const dragState = useRef(null);
|
|
141
168
|
const commit = (index) => {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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.
|
|
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: "
|
|
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(
|
|
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(
|
|
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,
|
|
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(
|
|
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(
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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: {
|
|
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
|
];
|
|
@@ -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;
|