react-three-game 0.0.64 → 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 +6 -6
- 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 +14 -0
- package/dist/tools/prefabeditor/InstanceProvider.js +198 -34
- package/dist/tools/prefabeditor/PrefabEditor.js +77 -16
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +3 -1
- package/dist/tools/prefabeditor/PrefabRoot.js +88 -120
- 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 +95 -16
- 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 +40 -2
- package/package.json +1 -1
|
@@ -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' },
|
|
@@ -1,10 +1,76 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { ModelListViewer, SingleModelViewer } from '../../assetviewer/page';
|
|
3
|
-
import { useEffect, useLayoutEffect, useState, useMemo, useRef } from 'react';
|
|
3
|
+
import { useContext, useEffect, useLayoutEffect, useState, useMemo, useRef } from 'react';
|
|
4
4
|
import { createPortal } from 'react-dom';
|
|
5
|
-
import {
|
|
5
|
+
import { BooleanField, FieldGroup, Label, NumberInput, SelectInput } from './Input';
|
|
6
|
+
import { EditorContext } from '../EditorContext';
|
|
7
|
+
import { DEFAULT_REPEAT_AXES, getRepeatAxesFromModelProperties, normalizeRepeatAxes } from '../InstanceProvider';
|
|
8
|
+
import { colors } from '../styles';
|
|
6
9
|
const PICKER_POPUP_WIDTH = 260;
|
|
7
10
|
const PICKER_POPUP_HEIGHT = 360;
|
|
11
|
+
const AXIS_OPTIONS = [
|
|
12
|
+
{ value: 'x', label: 'X' },
|
|
13
|
+
{ value: 'y', label: 'Y' },
|
|
14
|
+
{ value: 'z', label: 'Z' },
|
|
15
|
+
];
|
|
16
|
+
function quantize(value, step) {
|
|
17
|
+
if (!Number.isFinite(value))
|
|
18
|
+
return 0;
|
|
19
|
+
if (!Number.isFinite(step) || step <= 0)
|
|
20
|
+
return value;
|
|
21
|
+
return Math.round(value / step) * step;
|
|
22
|
+
}
|
|
23
|
+
function RepeatAxisEditor({ axes, onChange, positionSnap, }) {
|
|
24
|
+
const addAxis = () => {
|
|
25
|
+
const used = new Set(axes.map(axis => axis.axis));
|
|
26
|
+
const nextAxis = AXIS_OPTIONS.find(option => !used.has(option.value));
|
|
27
|
+
if (!nextAxis)
|
|
28
|
+
return;
|
|
29
|
+
onChange([...axes, { axis: nextAxis.value, count: 1, offset: 1 }]);
|
|
30
|
+
};
|
|
31
|
+
const updateAxis = (index, patch) => {
|
|
32
|
+
const nextAxes = axes.map((axis, axisIndex) => axisIndex === index ? Object.assign(Object.assign({}, axis), patch) : axis);
|
|
33
|
+
onChange(normalizeRepeatAxes(nextAxes));
|
|
34
|
+
};
|
|
35
|
+
const removeAxis = (index) => {
|
|
36
|
+
onChange(axes.filter((_, axisIndex) => axisIndex !== index));
|
|
37
|
+
};
|
|
38
|
+
const canAddAxis = axes.length < AXIS_OPTIONS.length;
|
|
39
|
+
return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 8 }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' }, children: [_jsx(Label, { children: "Repeat Axes" }), _jsx("button", { type: "button", onClick: addAxis, disabled: !canAddAxis, style: {
|
|
40
|
+
width: 22,
|
|
41
|
+
height: 22,
|
|
42
|
+
borderRadius: 3,
|
|
43
|
+
border: `1px solid ${canAddAxis ? colors.accentBorder : colors.border}`,
|
|
44
|
+
background: canAddAxis ? colors.accentBg : colors.bgSurface,
|
|
45
|
+
color: canAddAxis ? colors.accent : colors.textMuted,
|
|
46
|
+
cursor: canAddAxis ? 'pointer' : 'not-allowed',
|
|
47
|
+
fontSize: 14,
|
|
48
|
+
lineHeight: 1,
|
|
49
|
+
padding: 0,
|
|
50
|
+
}, title: canAddAxis ? 'Add repeat axis' : 'All axes already in use', children: "+" })] }), axes.map((axisConfig, index) => {
|
|
51
|
+
const usedByOthers = new Set(axes.filter((_, axisIndex) => axisIndex !== index).map(axis => axis.axis));
|
|
52
|
+
const axisOptions = AXIS_OPTIONS.filter(option => option.value === axisConfig.axis || !usedByOthers.has(option.value));
|
|
53
|
+
return (_jsxs("div", { style: {
|
|
54
|
+
display: 'flex',
|
|
55
|
+
flexDirection: 'column',
|
|
56
|
+
gap: 6,
|
|
57
|
+
padding: 8,
|
|
58
|
+
border: `1px solid ${colors.border}`,
|
|
59
|
+
borderRadius: 4,
|
|
60
|
+
background: colors.bgSurface,
|
|
61
|
+
}, children: [_jsxs("div", { style: { display: 'flex', gap: 6, alignItems: 'end' }, children: [_jsx("div", { style: { flex: 1, minWidth: 0 }, children: _jsx(SelectInput, { label: "Axis", value: axisConfig.axis, onChange: (axis) => updateAxis(index, { axis: axis }), options: axisOptions }) }), index > 0 ? (_jsx("button", { type: "button", onClick: () => removeAxis(index), style: {
|
|
62
|
+
height: 24,
|
|
63
|
+
width: 28,
|
|
64
|
+
borderRadius: 3,
|
|
65
|
+
border: `1px solid ${colors.border}`,
|
|
66
|
+
background: colors.bgInput,
|
|
67
|
+
color: colors.text,
|
|
68
|
+
cursor: 'pointer',
|
|
69
|
+
padding: 0,
|
|
70
|
+
flexShrink: 0,
|
|
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
|
+
})] }));
|
|
73
|
+
}
|
|
8
74
|
function ModelPicker({ value, onChange, basePath, nodeId }) {
|
|
9
75
|
const [modelFiles, setModelFiles] = useState([]);
|
|
10
76
|
const [showPicker, setShowPicker] = useState(false);
|
|
@@ -57,21 +123,32 @@ function ModelPicker({ value, onChange, basePath, nodeId }) {
|
|
|
57
123
|
onChange(filename);
|
|
58
124
|
setShowPicker(false);
|
|
59
125
|
};
|
|
60
|
-
return (_jsxs("div", { style: { maxHeight:
|
|
61
|
-
|
|
62
|
-
|
|
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)] }));
|
|
63
145
|
}
|
|
64
146
|
function ModelComponentEditor({ component, node, onUpdate, basePath = "" }) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
render: ({ value, onChange }) => (_jsx(ModelPicker, { value: value, onChange: onChange, basePath: basePath, nodeId: node === null || node === void 0 ? void 0 : node.id })),
|
|
71
|
-
},
|
|
72
|
-
{ name: 'instanced', type: 'boolean', label: 'Instanced' },
|
|
73
|
-
];
|
|
74
|
-
return (_jsx(FieldRenderer, { fields: fields, values: component.properties, onChange: onUpdate }));
|
|
147
|
+
var _a;
|
|
148
|
+
const editorContext = useContext(EditorContext);
|
|
149
|
+
const positionSnap = (_a = editorContext === null || editorContext === void 0 ? void 0 : editorContext.positionSnap) !== null && _a !== void 0 ? _a : 0.5;
|
|
150
|
+
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 }))] }))] }));
|
|
75
152
|
}
|
|
76
153
|
// View for Model component
|
|
77
154
|
function ModelComponentView({ properties, loadedModels, children }) {
|
|
@@ -103,7 +180,9 @@ const ModelComponent = {
|
|
|
103
180
|
nonComposable: true,
|
|
104
181
|
defaultProperties: {
|
|
105
182
|
filename: '',
|
|
106
|
-
instanced: false
|
|
183
|
+
instanced: false,
|
|
184
|
+
repeat: false,
|
|
185
|
+
repeatAxes: DEFAULT_REPEAT_AXES
|
|
107
186
|
}
|
|
108
187
|
};
|
|
109
188
|
export default ModelComponent;
|
|
@@ -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;
|
|
@@ -255,7 +288,12 @@ export function createModelNode(filename, name) {
|
|
|
255
288
|
},
|
|
256
289
|
model: {
|
|
257
290
|
type: 'Model',
|
|
258
|
-
properties: {
|
|
291
|
+
properties: {
|
|
292
|
+
filename,
|
|
293
|
+
instanced: false,
|
|
294
|
+
repeat: false,
|
|
295
|
+
repeatAxes: [{ axis: 'x', count: 1, offset: 1 }]
|
|
296
|
+
}
|
|
259
297
|
}
|
|
260
298
|
}
|
|
261
299
|
};
|