react-three-game 0.0.64 → 0.0.65
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/dist/tools/assetviewer/page.js +3 -3
- package/dist/tools/prefabeditor/InstanceProvider.d.ts +10 -0
- package/dist/tools/prefabeditor/InstanceProvider.js +158 -26
- package/dist/tools/prefabeditor/PrefabRoot.js +59 -8
- package/dist/tools/prefabeditor/components/ModelComponent.js +95 -16
- package/dist/tools/prefabeditor/utils.js +6 -1
- package/package.json +1 -1
|
@@ -114,14 +114,14 @@ function TextureSphere({ url, onError }) {
|
|
|
114
114
|
export function ModelListViewer({ files, selected, onSelect, basePath = "" }) {
|
|
115
115
|
return (_jsxs("div", { style: { position: 'relative', width: '100%', height: '100%' }, children: [_jsx("div", { style: { width: '100%', height: '100%', overflowY: 'auto', overflowX: 'hidden', paddingRight: 4 }, children: _jsx(AssetListViewer, { files: files, selected: selected, onSelect: onSelect, renderCard: (file, onSelectHandler) => (_jsx(ModelCard, { file: file, basePath: basePath, onSelect: onSelectHandler })) }) }), _jsx(SharedCanvas, {})] }));
|
|
116
116
|
}
|
|
117
|
-
function ModelCard({ file, onSelect, basePath = "" }) {
|
|
117
|
+
function ModelCard({ file, onSelect, basePath = "", size = 60, }) {
|
|
118
118
|
const [error, setError] = useState(false);
|
|
119
119
|
const { ref, isInView } = useInView();
|
|
120
120
|
const fullPath = basePath ? `/${basePath}${file}` : file;
|
|
121
121
|
if (error) {
|
|
122
122
|
return (_jsx("div", { ref: ref, style: { aspectRatio: '1 / 1', backgroundColor: '#374151', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }, onClick: () => onSelect(file), children: _jsx("div", { style: styles.errorIcon, children: "\u2717" }) }));
|
|
123
123
|
}
|
|
124
|
-
return (_jsxs("div", { ref: ref, style: {
|
|
124
|
+
return (_jsxs("div", { ref: ref, style: { width: size, aspectRatio: '1 / 1', backgroundColor: '#111827', color: '#f9fafb', cursor: 'pointer', display: 'flex', flexDirection: 'column' }, onClick: () => onSelect(file), children: [_jsx("div", { style: styles.flexFillRelative, children: isInView ? (_jsxs(View, { style: { width: '100%', height: '100%' }, children: [_jsx(PerspectiveCamera, { makeDefault: true, position: [0, 1, 3], fov: 50 }), _jsxs(Suspense, { fallback: null, children: [_jsx("ambientLight", { intensity: 1 }), _jsx("pointLight", { position: [5, 5, 5], intensity: 0.5 }), _jsx(ModelPreview, { url: fullPath, onError: () => setError(true) }), _jsx(OrbitControls, { enableZoom: false })] })] })) : null }), _jsx("div", { style: styles.bottomLabel, children: file.split('/').pop() })] }));
|
|
125
125
|
}
|
|
126
126
|
function ModelPreview({ url, onError }) {
|
|
127
127
|
const [model, setModel] = useState(null);
|
|
@@ -164,7 +164,7 @@ export function SingleTextureViewer({ file, basePath = "" }) {
|
|
|
164
164
|
export function SingleModelViewer({ file, basePath = "" }) {
|
|
165
165
|
if (!file)
|
|
166
166
|
return null;
|
|
167
|
-
return (_jsxs(_Fragment, { children: [_jsx(ModelCard, { file: file, basePath: basePath, onSelect: () => { } }), _jsx(SharedCanvas, {})] }));
|
|
167
|
+
return (_jsxs(_Fragment, { children: [_jsx(ModelCard, { file: file, basePath: basePath, onSelect: () => { }, size: 112 }), _jsx(SharedCanvas, {})] }));
|
|
168
168
|
}
|
|
169
169
|
export function SingleSoundViewer({ file, basePath = "" }) {
|
|
170
170
|
if (!file)
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Object3D, Group } from "three";
|
|
3
3
|
import { PhysicsProps } from "./components/PhysicsComponent";
|
|
4
|
+
export type RepeatAxisConfig = {
|
|
5
|
+
axis: 'x' | 'y' | 'z';
|
|
6
|
+
count: number;
|
|
7
|
+
offset: number;
|
|
8
|
+
};
|
|
9
|
+
export declare const DEFAULT_REPEAT_AXES: RepeatAxisConfig[];
|
|
10
|
+
export declare function normalizeRepeatAxes(value: unknown): RepeatAxisConfig[];
|
|
11
|
+
export declare function getRepeatAxesFromModelProperties(properties: Record<string, any>): RepeatAxisConfig[];
|
|
4
12
|
export type InstanceData = {
|
|
5
13
|
id: string;
|
|
14
|
+
sourceId: string;
|
|
6
15
|
position: [number, number, number];
|
|
7
16
|
rotation: [number, number, number];
|
|
8
17
|
scale: [number, number, number];
|
|
@@ -22,6 +31,7 @@ export declare function GameInstanceProvider({ children, models, onSelect, regis
|
|
|
22
31
|
export declare function useInstanceCheck(id: string): boolean;
|
|
23
32
|
export declare const GameInstance: React.ForwardRefExoticComponent<{
|
|
24
33
|
id: string;
|
|
34
|
+
sourceId?: string;
|
|
25
35
|
modelUrl: string;
|
|
26
36
|
position: [number, number, number];
|
|
27
37
|
rotation: [number, number, number];
|
|
@@ -1,8 +1,73 @@
|
|
|
1
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
2
|
+
var t = {};
|
|
3
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
4
|
+
t[p] = s[p];
|
|
5
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
6
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
7
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
8
|
+
t[p[i]] = s[p[i]];
|
|
9
|
+
}
|
|
10
|
+
return t;
|
|
11
|
+
};
|
|
1
12
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
13
|
import React, { createContext, useContext, useMemo, useRef, useState, useEffect, useCallback } from "react";
|
|
3
14
|
import { Merged, useHelper } from '@react-three/drei';
|
|
4
15
|
import { InstancedRigidBodies } from "@react-three/rapier";
|
|
16
|
+
import { ActiveCollisionTypes } from "@dimforge/rapier3d-compat";
|
|
5
17
|
import { Mesh, Matrix4, Vector3, Quaternion, Euler, BoxHelper } from "three";
|
|
18
|
+
import { gameEvents, getEntityIdFromRigidBody } from "./GameEvents";
|
|
19
|
+
export const DEFAULT_REPEAT_AXES = [{ axis: 'x', count: 1, offset: 1 }];
|
|
20
|
+
export function normalizeRepeatAxes(value) {
|
|
21
|
+
if (!Array.isArray(value)) {
|
|
22
|
+
return DEFAULT_REPEAT_AXES;
|
|
23
|
+
}
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
const normalized = value.flatMap((entry) => {
|
|
26
|
+
if (!entry || typeof entry !== 'object')
|
|
27
|
+
return [];
|
|
28
|
+
const axisValue = entry.axis;
|
|
29
|
+
if (axisValue !== 'x' && axisValue !== 'y' && axisValue !== 'z')
|
|
30
|
+
return [];
|
|
31
|
+
if (seen.has(axisValue))
|
|
32
|
+
return [];
|
|
33
|
+
seen.add(axisValue);
|
|
34
|
+
const countValue = Number(entry.count);
|
|
35
|
+
const offsetValue = Number(entry.offset);
|
|
36
|
+
return [{
|
|
37
|
+
axis: axisValue,
|
|
38
|
+
count: Number.isFinite(countValue) ? Math.max(1, Math.floor(countValue)) : 1,
|
|
39
|
+
offset: Number.isFinite(offsetValue) ? offsetValue : 1,
|
|
40
|
+
}];
|
|
41
|
+
});
|
|
42
|
+
return normalized.length > 0 ? normalized : DEFAULT_REPEAT_AXES;
|
|
43
|
+
}
|
|
44
|
+
function toVector3Tuple(value, fallback) {
|
|
45
|
+
if (!Array.isArray(value) || value.length !== 3)
|
|
46
|
+
return fallback;
|
|
47
|
+
return value.map((entry, index) => {
|
|
48
|
+
const next = typeof entry === 'number' ? entry : Number(entry);
|
|
49
|
+
return Number.isFinite(next) ? next : fallback[index];
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
export function getRepeatAxesFromModelProperties(properties) {
|
|
53
|
+
var _a;
|
|
54
|
+
if (Array.isArray(properties.repeatAxes)) {
|
|
55
|
+
return normalizeRepeatAxes(properties.repeatAxes);
|
|
56
|
+
}
|
|
57
|
+
const repeatCount = toVector3Tuple(properties.repeatCount, [1, 1, 1]).map(value => Math.max(1, Math.floor(value)));
|
|
58
|
+
const repeatOffset = toVector3Tuple(properties.repeatOffset, [1, 1, 1]);
|
|
59
|
+
const legacyAxes = [];
|
|
60
|
+
if ((_a = properties.repeatX) !== null && _a !== void 0 ? _a : true) {
|
|
61
|
+
legacyAxes.push({ axis: 'x', count: repeatCount[0], offset: repeatOffset[0] });
|
|
62
|
+
}
|
|
63
|
+
if (properties.repeatY) {
|
|
64
|
+
legacyAxes.push({ axis: 'y', count: repeatCount[1], offset: repeatOffset[1] });
|
|
65
|
+
}
|
|
66
|
+
if (properties.repeatZ) {
|
|
67
|
+
legacyAxes.push({ axis: 'z', count: repeatCount[2], offset: repeatOffset[2] });
|
|
68
|
+
}
|
|
69
|
+
return legacyAxes.length > 0 ? legacyAxes : DEFAULT_REPEAT_AXES;
|
|
70
|
+
}
|
|
6
71
|
// Helper functions for comparison
|
|
7
72
|
function arrayEquals(a, b) {
|
|
8
73
|
if (a === b)
|
|
@@ -15,14 +80,63 @@ function arrayEquals(a, b) {
|
|
|
15
80
|
}
|
|
16
81
|
return true;
|
|
17
82
|
}
|
|
83
|
+
function stableSerialize(value) {
|
|
84
|
+
if (Array.isArray(value)) {
|
|
85
|
+
return `[${value.map(stableSerialize).join(',')}]`;
|
|
86
|
+
}
|
|
87
|
+
if (value && typeof value === 'object') {
|
|
88
|
+
const entries = Object.entries(value)
|
|
89
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
90
|
+
.map(([key, entry]) => `${key}:${stableSerialize(entry)}`);
|
|
91
|
+
return `{${entries.join(',')}}`;
|
|
92
|
+
}
|
|
93
|
+
return JSON.stringify(value);
|
|
94
|
+
}
|
|
95
|
+
function getPhysicsSignature(physics) {
|
|
96
|
+
return physics ? stableSerialize(physics) : 'none';
|
|
97
|
+
}
|
|
98
|
+
function hasPhysics(instance) {
|
|
99
|
+
return Boolean(instance.physics);
|
|
100
|
+
}
|
|
101
|
+
function getColliderType(physics) {
|
|
102
|
+
return physics.colliders || (physics.type === 'fixed' ? 'trimesh' : 'hull');
|
|
103
|
+
}
|
|
104
|
+
function emitSensorEnter(sourceId, payload) {
|
|
105
|
+
gameEvents.emit('sensor:enter', {
|
|
106
|
+
sourceEntityId: sourceId,
|
|
107
|
+
targetEntityId: getEntityIdFromRigidBody(payload.other.rigidBody),
|
|
108
|
+
targetRigidBody: payload.other.rigidBody,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function emitSensorExit(sourceId, payload) {
|
|
112
|
+
gameEvents.emit('sensor:exit', {
|
|
113
|
+
sourceEntityId: sourceId,
|
|
114
|
+
targetEntityId: getEntityIdFromRigidBody(payload.other.rigidBody),
|
|
115
|
+
targetRigidBody: payload.other.rigidBody,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function emitCollisionEnter(sourceId, payload) {
|
|
119
|
+
gameEvents.emit('collision:enter', {
|
|
120
|
+
sourceEntityId: sourceId,
|
|
121
|
+
targetEntityId: getEntityIdFromRigidBody(payload.other.rigidBody),
|
|
122
|
+
targetRigidBody: payload.other.rigidBody,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
function emitCollisionExit(sourceId, payload) {
|
|
126
|
+
gameEvents.emit('collision:exit', {
|
|
127
|
+
sourceEntityId: sourceId,
|
|
128
|
+
targetEntityId: getEntityIdFromRigidBody(payload.other.rigidBody),
|
|
129
|
+
targetRigidBody: payload.other.rigidBody,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
18
132
|
function instanceEquals(a, b) {
|
|
19
|
-
var _a, _b;
|
|
20
133
|
return a.id === b.id &&
|
|
134
|
+
a.sourceId === b.sourceId &&
|
|
21
135
|
a.meshPath === b.meshPath &&
|
|
22
136
|
arrayEquals(a.position, b.position) &&
|
|
23
137
|
arrayEquals(a.rotation, b.rotation) &&
|
|
24
138
|
arrayEquals(a.scale, b.scale) &&
|
|
25
|
-
(
|
|
139
|
+
getPhysicsSignature(a.physics) === getPhysicsSignature(b.physics);
|
|
26
140
|
}
|
|
27
141
|
const GameInstanceContext = createContext(null);
|
|
28
142
|
export function GameInstanceProvider({ children, models, onSelect, registerRef, selectedId, editMode }) {
|
|
@@ -51,7 +165,7 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef,
|
|
|
51
165
|
});
|
|
52
166
|
}, []);
|
|
53
167
|
const hasInstance = useCallback((id) => {
|
|
54
|
-
return instances.some(i => i.id === id);
|
|
168
|
+
return instances.some(i => i.id === id || i.sourceId === id);
|
|
55
169
|
}, [instances]);
|
|
56
170
|
// Flatten all model meshes once (models → flat mesh parts)
|
|
57
171
|
// Note: Geometry is cloned with baked transforms for instancing
|
|
@@ -82,17 +196,18 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef,
|
|
|
82
196
|
Object.values(flatMeshes).forEach(mesh => mesh.geometry.dispose());
|
|
83
197
|
};
|
|
84
198
|
}, [flatMeshes]);
|
|
85
|
-
// Group instances by meshPath
|
|
199
|
+
// Group instances by meshPath and physics presence for batch rendering.
|
|
86
200
|
const grouped = useMemo(() => {
|
|
87
|
-
var _a;
|
|
88
201
|
const groups = {};
|
|
89
202
|
for (const inst of instances) {
|
|
90
|
-
const
|
|
91
|
-
const key = `${inst.meshPath}__${type}`;
|
|
203
|
+
const key = `${inst.meshPath}__${inst.physics ? 'physics' : 'visual'}`;
|
|
92
204
|
if (!groups[key])
|
|
93
|
-
groups[key] = {
|
|
205
|
+
groups[key] = { hasPhysics: Boolean(inst.physics), instances: [] };
|
|
94
206
|
groups[key].instances.push(inst);
|
|
95
207
|
}
|
|
208
|
+
Object.values(groups).forEach(group => {
|
|
209
|
+
group.instances.sort((a, b) => a.id.localeCompare(b.id));
|
|
210
|
+
});
|
|
96
211
|
return groups;
|
|
97
212
|
}, [instances]);
|
|
98
213
|
return (_jsxs(GameInstanceContext.Provider, { value: {
|
|
@@ -103,7 +218,7 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef,
|
|
|
103
218
|
modelParts,
|
|
104
219
|
hasInstance
|
|
105
220
|
}, children: [children, Object.entries(grouped).map(([key, group]) => {
|
|
106
|
-
if (group.
|
|
221
|
+
if (!group.hasPhysics)
|
|
107
222
|
return null;
|
|
108
223
|
const modelKey = group.instances[0].meshPath;
|
|
109
224
|
const partCount = modelParts[modelKey] || 0;
|
|
@@ -111,7 +226,7 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef,
|
|
|
111
226
|
return null;
|
|
112
227
|
return (_jsx(InstancedRigidGroup, { group: group, modelKey: modelKey, partCount: partCount, flatMeshes: flatMeshes, onSelect: onSelect, editMode: editMode }, key));
|
|
113
228
|
}), Object.entries(grouped).map(([key, group]) => {
|
|
114
|
-
if (group.
|
|
229
|
+
if (group.hasPhysics)
|
|
115
230
|
return null;
|
|
116
231
|
const modelKey = group.instances[0].meshPath;
|
|
117
232
|
const partCount = modelParts[modelKey] || 0;
|
|
@@ -130,12 +245,10 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef,
|
|
|
130
245
|
function InstancedRigidGroup({ group, modelKey, partCount, flatMeshes, onSelect, editMode }) {
|
|
131
246
|
const meshRefs = useRef([]);
|
|
132
247
|
const rigidBodiesRef = useRef(null);
|
|
133
|
-
const instances = useMemo(() => group.instances.map(inst =>
|
|
134
|
-
|
|
135
|
-
position: inst.position,
|
|
136
|
-
|
|
137
|
-
scale: inst.scale,
|
|
138
|
-
})), [group.instances]);
|
|
248
|
+
const instances = useMemo(() => group.instances.filter(hasPhysics).map(inst => {
|
|
249
|
+
const _a = inst.physics, { activeCollisionTypes: _activeCollisionTypes, colliders: _colliders, userData } = _a, rigidBodyProps = __rest(_a, ["activeCollisionTypes", "colliders", "userData"]);
|
|
250
|
+
return Object.assign(Object.assign({ key: inst.id, position: inst.position, rotation: inst.rotation, scale: inst.scale }, rigidBodyProps), { colliders: getColliderType(inst.physics), userData: Object.assign(Object.assign({}, userData), { entityId: inst.sourceId }), onIntersectionEnter: (payload) => emitSensorEnter(inst.sourceId, payload), onIntersectionExit: (payload) => emitSensorExit(inst.sourceId, payload), onCollisionEnter: (payload) => emitCollisionEnter(inst.sourceId, payload), onCollisionExit: (payload) => emitCollisionExit(inst.sourceId, payload) });
|
|
251
|
+
}), [group.instances]);
|
|
139
252
|
// Apply scale to visual meshes (InstancedRigidBodies only scales colliders, not visuals)
|
|
140
253
|
useEffect(() => {
|
|
141
254
|
const matrix = new Matrix4();
|
|
@@ -161,7 +274,7 @@ function InstancedRigidGroup({ group, modelKey, partCount, flatMeshes, onSelect,
|
|
|
161
274
|
try {
|
|
162
275
|
group.instances.forEach((inst, i) => {
|
|
163
276
|
var _a;
|
|
164
|
-
const body = (_a = rigidBodiesRef.current) === null || _a === void 0 ? void 0 : _a
|
|
277
|
+
const body = (_a = rigidBodiesRef.current) === null || _a === void 0 ? void 0 : _a[i];
|
|
165
278
|
if (body && body.setTranslation && body.setRotation) {
|
|
166
279
|
pos.set(...inst.position);
|
|
167
280
|
euler.set(...inst.rotation);
|
|
@@ -177,7 +290,22 @@ function InstancedRigidGroup({ group, modelKey, partCount, flatMeshes, onSelect,
|
|
|
177
290
|
}
|
|
178
291
|
}
|
|
179
292
|
}, [group.instances]);
|
|
180
|
-
|
|
293
|
+
useEffect(() => {
|
|
294
|
+
group.instances.forEach((inst, i) => {
|
|
295
|
+
var _a, _b;
|
|
296
|
+
if (!inst.physics || inst.physics.activeCollisionTypes !== 'all')
|
|
297
|
+
return;
|
|
298
|
+
const body = (_a = rigidBodiesRef.current) === null || _a === void 0 ? void 0 : _a[i];
|
|
299
|
+
if (!body || !body.numColliders || !body.collider)
|
|
300
|
+
return;
|
|
301
|
+
for (let colliderIndex = 0; colliderIndex < body.numColliders(); colliderIndex++) {
|
|
302
|
+
const collider = body.collider(colliderIndex);
|
|
303
|
+
(_b = collider.setActiveCollisionTypes) === null || _b === void 0 ? void 0 : _b.call(collider, ActiveCollisionTypes.DEFAULT |
|
|
304
|
+
ActiveCollisionTypes.KINEMATIC_FIXED |
|
|
305
|
+
ActiveCollisionTypes.KINEMATIC_KINEMATIC);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
}, [group.instances]);
|
|
181
309
|
// Handle click on instanced mesh in edit mode
|
|
182
310
|
const handleClick = (e) => {
|
|
183
311
|
if (!editMode || !onSelect)
|
|
@@ -186,12 +314,12 @@ function InstancedRigidGroup({ group, modelKey, partCount, flatMeshes, onSelect,
|
|
|
186
314
|
// Get the instance index from the intersection
|
|
187
315
|
const instanceId = e.instanceId;
|
|
188
316
|
if (instanceId !== undefined && group.instances[instanceId]) {
|
|
189
|
-
onSelect(group.instances[instanceId].
|
|
317
|
+
onSelect(group.instances[instanceId].sourceId);
|
|
190
318
|
}
|
|
191
319
|
};
|
|
192
320
|
// Add key to force remount when instance count changes significantly (helps with cleanup)
|
|
193
|
-
const rigidBodyKey = `rb_${modelKey}_${group.
|
|
194
|
-
return (_jsx(InstancedRigidBodies, { ref: rigidBodiesRef, instances: instances,
|
|
321
|
+
const rigidBodyKey = `rb_${modelKey}_${group.instances.map(inst => `${inst.id}:${getPhysicsSignature(inst.physics)}`).join('|')}`;
|
|
322
|
+
return (_jsx(InstancedRigidBodies, { ref: rigidBodiesRef, instances: instances, children: Array.from({ length: partCount }).map((_, i) => {
|
|
195
323
|
const mesh = flatMeshes[`${modelKey}__${i}`];
|
|
196
324
|
if (!mesh)
|
|
197
325
|
return null;
|
|
@@ -208,16 +336,19 @@ function NonPhysicsInstancedGroup({ modelKey, group, partCount, instancesMap, on
|
|
|
208
336
|
function InstanceGroupItem({ instance, InstanceComponents, onSelect, registerRef, selectedId, editMode }) {
|
|
209
337
|
const clickValid = useRef(false);
|
|
210
338
|
const groupRef = useRef(null);
|
|
211
|
-
const isSelected = selectedId === instance.id;
|
|
339
|
+
const isSelected = selectedId === instance.id || selectedId === instance.sourceId;
|
|
212
340
|
// Use BoxHelper when object is selected in edit mode
|
|
213
341
|
useHelper(editMode && isSelected ? groupRef : null, BoxHelper, 'cyan');
|
|
214
342
|
useEffect(() => {
|
|
343
|
+
if (editMode)
|
|
344
|
+
return;
|
|
215
345
|
registerRef === null || registerRef === void 0 ? void 0 : registerRef(instance.id, groupRef.current);
|
|
216
|
-
|
|
346
|
+
return () => registerRef === null || registerRef === void 0 ? void 0 : registerRef(instance.id, null);
|
|
347
|
+
}, [editMode, instance.id, registerRef]);
|
|
217
348
|
return (_jsx("group", { ref: groupRef, position: instance.position, rotation: instance.rotation, scale: instance.scale, onPointerDown: (e) => { e.stopPropagation(); clickValid.current = true; }, onPointerMove: () => { clickValid.current = false; }, onPointerUp: (e) => {
|
|
218
349
|
if (clickValid.current) {
|
|
219
350
|
e.stopPropagation();
|
|
220
|
-
onSelect === null || onSelect === void 0 ? void 0 : onSelect(instance.
|
|
351
|
+
onSelect === null || onSelect === void 0 ? void 0 : onSelect(instance.sourceId);
|
|
221
352
|
}
|
|
222
353
|
clickValid.current = false;
|
|
223
354
|
}, children: InstanceComponents.map((Instance, i) => _jsx(Instance, {}, i)) }));
|
|
@@ -229,18 +360,19 @@ export function useInstanceCheck(id) {
|
|
|
229
360
|
return (_a = ctx === null || ctx === void 0 ? void 0 : ctx.hasInstance(id)) !== null && _a !== void 0 ? _a : false;
|
|
230
361
|
}
|
|
231
362
|
// GameInstance component: registers an instance for batch rendering (renders nothing itself)
|
|
232
|
-
export const GameInstance = React.forwardRef(({ id, modelUrl, position, rotation, scale, physics = undefined, }, ref) => {
|
|
363
|
+
export const GameInstance = React.forwardRef(({ id, sourceId, modelUrl, position, rotation, scale, physics = undefined, }, ref) => {
|
|
233
364
|
const ctx = useContext(GameInstanceContext);
|
|
234
365
|
const addInstance = ctx === null || ctx === void 0 ? void 0 : ctx.addInstance;
|
|
235
366
|
const removeInstance = ctx === null || ctx === void 0 ? void 0 : ctx.removeInstance;
|
|
236
367
|
const instance = useMemo(() => ({
|
|
237
368
|
id,
|
|
369
|
+
sourceId: sourceId !== null && sourceId !== void 0 ? sourceId : id,
|
|
238
370
|
meshPath: modelUrl,
|
|
239
371
|
position,
|
|
240
372
|
rotation,
|
|
241
373
|
scale,
|
|
242
374
|
physics,
|
|
243
|
-
}), [id, modelUrl, JSON.stringify(position), JSON.stringify(rotation), JSON.stringify(scale),
|
|
375
|
+
}), [id, sourceId, modelUrl, JSON.stringify(position), JSON.stringify(rotation), JSON.stringify(scale), getPhysicsSignature(physics)]);
|
|
244
376
|
useEffect(() => {
|
|
245
377
|
if (!addInstance || !removeInstance)
|
|
246
378
|
return;
|
|
@@ -14,7 +14,7 @@ import { BoxHelper, Euler, Matrix4, Quaternion, SRGBColorSpace, TextureLoader, V
|
|
|
14
14
|
import { getComponent, registerComponent, getNonComposableKeys } from "./components/ComponentRegistry";
|
|
15
15
|
import components from "./components";
|
|
16
16
|
import { loadModel } from "../dragdrop";
|
|
17
|
-
import { GameInstance, GameInstanceProvider, useInstanceCheck } from "./InstanceProvider";
|
|
17
|
+
import { GameInstance, GameInstanceProvider, getRepeatAxesFromModelProperties, useInstanceCheck } from "./InstanceProvider";
|
|
18
18
|
import { focusCameraOnObject, updateNode } from "./utils";
|
|
19
19
|
import { EditorContext } from "./EditorContext";
|
|
20
20
|
components.forEach(registerComponent);
|
|
@@ -159,16 +159,16 @@ export function GameObjectRenderer(props) {
|
|
|
159
159
|
: _jsx(StandardNode, Object.assign({}, props), key);
|
|
160
160
|
}
|
|
161
161
|
function isPhysicsProps(v) {
|
|
162
|
-
return (v === null || v === void 0 ? void 0 : v.type) === "fixed" || (v === null || v === void 0 ? void 0 : v.type) === "dynamic";
|
|
162
|
+
return (v === null || v === void 0 ? void 0 : v.type) === "fixed" || (v === null || v === void 0 ? void 0 : v.type) === "dynamic" || (v === null || v === void 0 ? void 0 : v.type) === "kinematicPosition" || (v === null || v === void 0 ? void 0 : v.type) === "kinematicVelocity";
|
|
163
163
|
}
|
|
164
164
|
function InstancedNode({ gameObject, parentMatrix = IDENTITY, editMode, registerRef, selectedId: _selectedId, onSelect, onClick }) {
|
|
165
|
-
var _a, _b, _c, _d, _e, _f, _g
|
|
166
|
-
const world = parentMatrix.clone().multiply(compose(gameObject));
|
|
167
|
-
const { position: worldPosition, rotation: worldRotation, scale: worldScale } = decompose(world);
|
|
165
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
168
166
|
const localTransform = getNodeTransformProps(gameObject);
|
|
169
167
|
const physicsProps = isPhysicsProps((_b = (_a = gameObject.components) === null || _a === void 0 ? void 0 : _a.physics) === null || _b === void 0 ? void 0 : _b.properties)
|
|
170
168
|
? (_d = (_c = gameObject.components) === null || _c === void 0 ? void 0 : _c.physics) === null || _d === void 0 ? void 0 : _d.properties
|
|
171
169
|
: undefined;
|
|
170
|
+
const modelUrl = (_g = (_f = (_e = gameObject.components) === null || _e === void 0 ? void 0 : _e.model) === null || _f === void 0 ? void 0 : _f.properties) === null || _g === void 0 ? void 0 : _g.filename;
|
|
171
|
+
const instances = useMemo(() => buildRepeatedInstances(gameObject, parentMatrix, modelUrl, physicsProps), [gameObject, modelUrl, parentMatrix, physicsProps]);
|
|
172
172
|
const groupRef = useRef(null);
|
|
173
173
|
const clickValid = useRef(false);
|
|
174
174
|
useEffect(() => {
|
|
@@ -177,7 +177,6 @@ function InstancedNode({ gameObject, parentMatrix = IDENTITY, editMode, register
|
|
|
177
177
|
return () => registerRef(gameObject.id, null);
|
|
178
178
|
}
|
|
179
179
|
}, [gameObject.id, registerRef, editMode]);
|
|
180
|
-
const modelUrl = (_g = (_f = (_e = gameObject.components) === null || _e === void 0 ? void 0 : _e.model) === null || _f === void 0 ? void 0 : _f.properties) === null || _g === void 0 ? void 0 : _g.filename;
|
|
181
180
|
if (editMode) {
|
|
182
181
|
return (_jsxs(_Fragment, { children: [_jsx("group", { ref: groupRef, position: localTransform.position, rotation: localTransform.rotation, scale: localTransform.scale, onPointerDown: (e) => { e.stopPropagation(); clickValid.current = true; }, onPointerMove: () => { clickValid.current = false; }, onPointerUp: (e) => {
|
|
183
182
|
if (clickValid.current) {
|
|
@@ -186,9 +185,9 @@ function InstancedNode({ gameObject, parentMatrix = IDENTITY, editMode, register
|
|
|
186
185
|
onClick === null || onClick === void 0 ? void 0 : onClick(e, gameObject);
|
|
187
186
|
}
|
|
188
187
|
clickValid.current = false;
|
|
189
|
-
}, children: _jsx("mesh", { visible: false, children: _jsx("boxGeometry", { args: [0.01, 0.01, 0.01] }) }) }), _jsx(GameInstance, { id: gameObject.id, modelUrl: modelUrl, position:
|
|
188
|
+
}, children: _jsx("mesh", { visible: false, children: _jsx("boxGeometry", { args: [0.01, 0.01, 0.01] }) }) }), instances.map(instance => (_jsx(GameInstance, { id: instance.id, sourceId: gameObject.id, modelUrl: instance.modelUrl, position: instance.position, rotation: instance.rotation, scale: instance.scale, physics: instance.physics }, instance.id)))] }));
|
|
190
189
|
}
|
|
191
|
-
return (_jsx(
|
|
190
|
+
return (_jsx(_Fragment, { children: instances.map(instance => (_jsx(GameInstance, { id: instance.id, sourceId: gameObject.id, modelUrl: instance.modelUrl, position: instance.position, rotation: instance.rotation, scale: instance.scale, physics: instance.physics }, instance.id))) }));
|
|
192
191
|
}
|
|
193
192
|
function StandardNode({ gameObject, selectedId, onSelect, onClick, registerRef, registerRigidBodyRef, loadedModels, loadedTextures, editMode, parentMatrix = IDENTITY, }) {
|
|
194
193
|
var _a, _b, _c, _d, _e;
|
|
@@ -276,6 +275,58 @@ function compose(node) {
|
|
|
276
275
|
const { position, rotation, scale } = getNodeTransformProps(node);
|
|
277
276
|
return new Matrix4().compose(new Vector3(...position), new Quaternion().setFromEuler(new Euler(...rotation)), new Vector3(...scale));
|
|
278
277
|
}
|
|
278
|
+
function getModelRepeatSettings(node) {
|
|
279
|
+
var _a, _b, _c;
|
|
280
|
+
const properties = (_c = (_b = (_a = node === null || node === void 0 ? void 0 : node.components) === null || _a === void 0 ? void 0 : _a.model) === null || _b === void 0 ? void 0 : _b.properties) !== null && _c !== void 0 ? _c : {};
|
|
281
|
+
return {
|
|
282
|
+
repeat: Boolean(properties.repeat),
|
|
283
|
+
repeatAxes: getRepeatAxesFromModelProperties(properties),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function buildRepeatedInstances(gameObject, parentMatrix, modelUrl, physics) {
|
|
287
|
+
if (!modelUrl)
|
|
288
|
+
return [];
|
|
289
|
+
const transform = getNodeTransformProps(gameObject);
|
|
290
|
+
const repeat = getModelRepeatSettings(gameObject);
|
|
291
|
+
const counts = [1, 1, 1];
|
|
292
|
+
const offsets = [0, 0, 0];
|
|
293
|
+
if (repeat.repeat) {
|
|
294
|
+
for (const entry of repeat.repeatAxes) {
|
|
295
|
+
const axisIndex = entry.axis === 'x' ? 0 : entry.axis === 'y' ? 1 : 2;
|
|
296
|
+
counts[axisIndex] = entry.count;
|
|
297
|
+
offsets[axisIndex] = entry.offset;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const baseTranslation = new Matrix4().makeTranslation(transform.position[0], transform.position[1], transform.position[2]);
|
|
301
|
+
const baseRotation = new Matrix4().makeRotationFromEuler(new Euler(...transform.rotation));
|
|
302
|
+
const baseScale = new Matrix4().makeScale(transform.scale[0], transform.scale[1], transform.scale[2]);
|
|
303
|
+
const offsetMatrix = new Matrix4();
|
|
304
|
+
const worldMatrix = new Matrix4();
|
|
305
|
+
const instances = [];
|
|
306
|
+
for (let x = 0; x < counts[0]; x++) {
|
|
307
|
+
for (let y = 0; y < counts[1]; y++) {
|
|
308
|
+
for (let z = 0; z < counts[2]; z++) {
|
|
309
|
+
offsetMatrix.makeTranslation(x * offsets[0], y * offsets[1], z * offsets[2]);
|
|
310
|
+
worldMatrix.copy(parentMatrix)
|
|
311
|
+
.multiply(baseTranslation)
|
|
312
|
+
.multiply(baseRotation)
|
|
313
|
+
.multiply(offsetMatrix)
|
|
314
|
+
.multiply(baseScale);
|
|
315
|
+
const { position, rotation, scale } = decompose(worldMatrix);
|
|
316
|
+
const isBaseInstance = x === 0 && y === 0 && z === 0;
|
|
317
|
+
instances.push({
|
|
318
|
+
id: isBaseInstance ? gameObject.id : `${gameObject.id}__repeat_${x}_${y}_${z}`,
|
|
319
|
+
modelUrl,
|
|
320
|
+
position,
|
|
321
|
+
rotation,
|
|
322
|
+
scale,
|
|
323
|
+
physics,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return instances;
|
|
329
|
+
}
|
|
279
330
|
function decompose(m) {
|
|
280
331
|
const p = new Vector3(), q = new Quaternion(), s = new Vector3();
|
|
281
332
|
m.decompose(p, q, s);
|
|
@@ -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, Input, Label, 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: [_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}`));
|
|
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;
|
|
@@ -255,7 +255,12 @@ export function createModelNode(filename, name) {
|
|
|
255
255
|
},
|
|
256
256
|
model: {
|
|
257
257
|
type: 'Model',
|
|
258
|
-
properties: {
|
|
258
|
+
properties: {
|
|
259
|
+
filename,
|
|
260
|
+
instanced: false,
|
|
261
|
+
repeat: false,
|
|
262
|
+
repeatAxes: [{ axis: 'x', count: 1, offset: 1 }]
|
|
263
|
+
}
|
|
259
264
|
}
|
|
260
265
|
}
|
|
261
266
|
};
|