react-three-game 0.0.100 → 0.0.102

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +2 -1
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +1 -0
  4. package/dist/plugins/crashcat/CrashcatPhysicsComponent.d.ts +3 -0
  5. package/dist/plugins/crashcat/CrashcatPhysicsComponent.js +352 -0
  6. package/dist/plugins/crashcat/CrashcatRuntime.d.ts +27 -0
  7. package/dist/plugins/crashcat/CrashcatRuntime.js +154 -0
  8. package/dist/plugins/crashcat/index.d.ts +2 -0
  9. package/dist/plugins/crashcat/index.js +2 -0
  10. package/dist/plugins/index.d.ts +1 -0
  11. package/dist/plugins/index.js +1 -0
  12. package/dist/tools/assetviewer/page.js +95 -46
  13. package/dist/tools/dragdrop/DragDropLoader.d.ts +3 -0
  14. package/dist/tools/dragdrop/DragDropLoader.js +183 -44
  15. package/dist/tools/dragdrop/index.d.ts +1 -1
  16. package/dist/tools/dragdrop/index.js +1 -1
  17. package/dist/tools/dragdrop/modelLoader.js +2 -0
  18. package/dist/tools/prefabeditor/EditorUI.js +7 -8
  19. package/dist/tools/prefabeditor/PrefabEditor.d.ts +3 -0
  20. package/dist/tools/prefabeditor/PrefabEditor.js +28 -11
  21. package/dist/tools/prefabeditor/PrefabRoot.d.ts +2 -0
  22. package/dist/tools/prefabeditor/PrefabRoot.js +51 -35
  23. package/dist/tools/prefabeditor/components/BufferGeometryComponent.js +20 -0
  24. package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +5 -0
  25. package/dist/tools/prefabeditor/components/ComponentRegistry.js +31 -0
  26. package/dist/tools/prefabeditor/components/DataComponent.js +1 -0
  27. package/dist/tools/prefabeditor/components/EnvironmentComponent.js +1 -0
  28. package/dist/tools/prefabeditor/components/GeometryComponent.js +1 -0
  29. package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +1 -0
  30. package/dist/tools/prefabeditor/components/MaterialComponent.js +85 -57
  31. package/dist/tools/prefabeditor/components/ModelComponent.js +45 -3
  32. package/dist/tools/prefabeditor/components/SpriteComponent.js +1 -0
  33. package/dist/tools/prefabeditor/components/TransformComponent.js +1 -0
  34. package/dist/tools/prefabeditor/modelPrefab.d.ts +16 -0
  35. package/dist/tools/prefabeditor/modelPrefab.js +180 -0
  36. package/dist/tools/prefabeditor/prefabStore.d.ts +1 -0
  37. package/dist/tools/prefabeditor/prefabStore.js +75 -42
  38. package/package.json +27 -2
@@ -10,7 +10,7 @@ var __rest = (this && this.__rest) || function (s, e) {
10
10
  return t;
11
11
  };
12
12
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
13
- import { createContext, useContext, useMemo, useRef } from 'react';
13
+ import { createContext, useContext, useEffect, useLayoutEffect, useMemo, useRef } from 'react';
14
14
  import { extend } from '@react-three/fiber';
15
15
  import { useFrame } from '@react-three/fiber';
16
16
  import { assetRef, assetRefs } from './ComponentRegistry';
@@ -38,25 +38,53 @@ const MAG_FILTER_MAP = {
38
38
  NearestFilter,
39
39
  LinearFilter,
40
40
  };
41
- function cloneConfiguredTexture({ texture, repeat, repeatCount, offset, colorSpace, generateMipmaps, minFilter, magFilter, }) {
42
- var _a, _b;
43
- const clonedTexture = texture.clone();
44
- if (repeat) {
45
- clonedTexture.wrapS = clonedTexture.wrapT = RepeatWrapping;
46
- if (repeatCount)
47
- clonedTexture.repeat.set(repeatCount[0], repeatCount[1]);
41
+ function configureTexture(texture, options) {
42
+ var _a, _b, _c, _d, _e, _f, _g, _h;
43
+ if (!texture)
44
+ return;
45
+ if (options.repeat) {
46
+ texture.wrapS = texture.wrapT = RepeatWrapping;
47
+ texture.repeat.set((_b = (_a = options.repeatCount) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : 1, (_d = (_c = options.repeatCount) === null || _c === void 0 ? void 0 : _c[1]) !== null && _d !== void 0 ? _d : 1);
48
48
  }
49
49
  else {
50
- clonedTexture.wrapS = clonedTexture.wrapT = ClampToEdgeWrapping;
51
- clonedTexture.repeat.set(1, 1);
50
+ texture.wrapS = texture.wrapT = ClampToEdgeWrapping;
51
+ texture.repeat.set(1, 1);
52
52
  }
53
- clonedTexture.offset.set((_a = offset === null || offset === void 0 ? void 0 : offset[0]) !== null && _a !== void 0 ? _a : 0, (_b = offset === null || offset === void 0 ? void 0 : offset[1]) !== null && _b !== void 0 ? _b : 0);
54
- clonedTexture.colorSpace = colorSpace;
55
- clonedTexture.generateMipmaps = generateMipmaps;
56
- clonedTexture.minFilter = minFilter;
57
- clonedTexture.magFilter = magFilter;
58
- clonedTexture.needsUpdate = true;
59
- return clonedTexture;
53
+ texture.offset.set((_f = (_e = options.offset) === null || _e === void 0 ? void 0 : _e[0]) !== null && _f !== void 0 ? _f : 0, (_h = (_g = options.offset) === null || _g === void 0 ? void 0 : _g[1]) !== null && _h !== void 0 ? _h : 0);
54
+ texture.colorSpace = options.colorSpace;
55
+ texture.generateMipmaps = options.generateMipmaps;
56
+ texture.minFilter = options.minFilter;
57
+ texture.magFilter = options.magFilter;
58
+ texture.needsUpdate = true;
59
+ }
60
+ function cloneConfiguredTexture(texture, options) {
61
+ if (!texture)
62
+ return undefined;
63
+ const nextTexture = texture.clone();
64
+ configureTexture(nextTexture, options);
65
+ return nextTexture;
66
+ }
67
+ function useConfiguredTexture(texture, options) {
68
+ var _a, _b, _c, _d;
69
+ const configuredTexture = useMemo(() => cloneConfiguredTexture(texture, options), [texture]);
70
+ useEffect(() => {
71
+ return () => configuredTexture === null || configuredTexture === void 0 ? void 0 : configuredTexture.dispose();
72
+ }, [configuredTexture]);
73
+ useLayoutEffect(() => {
74
+ configureTexture(configuredTexture, options);
75
+ }, [
76
+ configuredTexture,
77
+ options.colorSpace,
78
+ options.repeat,
79
+ (_a = options.repeatCount) === null || _a === void 0 ? void 0 : _a[0],
80
+ (_b = options.repeatCount) === null || _b === void 0 ? void 0 : _b[1],
81
+ (_c = options.offset) === null || _c === void 0 ? void 0 : _c[0],
82
+ (_d = options.offset) === null || _d === void 0 ? void 0 : _d[1],
83
+ options.generateMipmaps,
84
+ options.minFilter,
85
+ options.magFilter,
86
+ ]);
87
+ return configuredTexture;
60
88
  }
61
89
  export function useMaterialOverrides() {
62
90
  return useContext(MaterialOverridesContext);
@@ -72,14 +100,20 @@ extend({
72
100
  SpriteNodeMaterial,
73
101
  });
74
102
  function MaterialComponentEditor({ component, onUpdate, basePath = "", }) {
75
- var _a;
103
+ var _a, _b, _c, _d;
76
104
  const materialType = (_a = component.properties.materialType) !== null && _a !== void 0 ? _a : 'standard';
77
105
  const hasTexture = !!component.properties.texture;
78
106
  const hasRepeat = component.properties.repeat;
79
107
  const animateOffset = component.properties.animateOffset;
80
108
  const isStandardMaterial = materialType === 'standard';
81
109
  const isSpriteMaterial = materialType === 'sprite';
110
+ const editorValues = Object.assign(Object.assign({}, component.properties), { generateMipmaps: (_b = component.properties.generateMipmaps) !== null && _b !== void 0 ? _b : true, minFilter: (_c = component.properties.minFilter) !== null && _c !== void 0 ? _c : 'LinearMipmapLinearFilter', magFilter: (_d = component.properties.magFilter) !== null && _d !== void 0 ? _d : 'LinearFilter' });
82
111
  const fields = [
112
+ {
113
+ name: 'attach',
114
+ type: 'string',
115
+ label: 'Attach',
116
+ },
83
117
  {
84
118
  name: 'materialType',
85
119
  type: 'select',
@@ -166,12 +200,12 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "", }) {
166
200
  type: 'select',
167
201
  label: 'Min Filter',
168
202
  options: [
203
+ { value: 'LinearMipmapLinearFilter', label: 'Linear Mipmap Linear (Default)' },
204
+ { value: 'LinearFilter', label: 'Linear' },
205
+ { value: 'LinearMipmapNearestFilter', label: 'Linear Mipmap Nearest' },
169
206
  { value: 'NearestFilter', label: 'Nearest' },
170
207
  { value: 'NearestMipmapNearestFilter', label: 'Nearest Mipmap Nearest' },
171
208
  { value: 'NearestMipmapLinearFilter', label: 'Nearest Mipmap Linear' },
172
- { value: 'LinearFilter', label: 'Linear' },
173
- { value: 'LinearMipmapNearestFilter', label: 'Linear Mipmap Nearest' },
174
- { value: 'LinearMipmapLinearFilter', label: 'Linear Mipmap Linear (Default)' },
175
209
  ],
176
210
  },
177
211
  {
@@ -179,17 +213,17 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "", }) {
179
213
  type: 'select',
180
214
  label: 'Mag Filter',
181
215
  options: [
182
- { value: 'NearestFilter', label: 'Nearest' },
183
216
  { value: 'LinearFilter', label: 'Linear (Default)' },
217
+ { value: 'NearestFilter', label: 'Nearest' },
184
218
  ],
185
219
  },
186
220
  ] : []),
187
221
  ];
188
- return (_jsx(FieldRenderer, { fields: fields, values: component.properties, onChange: onUpdate }));
222
+ return (_jsx(FieldRenderer, { fields: fields, values: editorValues, onChange: onUpdate }));
189
223
  }
190
224
  // View for Material component
191
225
  function MaterialComponentView({ properties: rawProps }) {
192
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
226
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v;
193
227
  const { getTexture } = useAssetRuntime();
194
228
  const properties = rawProps;
195
229
  const materialSource = properties !== null && properties !== void 0 ? properties : {};
@@ -208,39 +242,21 @@ function MaterialComponentView({ properties: rawProps }) {
208
242
  const normalScaleProp = materialSource.normalScale;
209
243
  const normalMapTexture = normalMapTextureName ? getTexture(normalMapTextureName) : undefined;
210
244
  // Destructure all material props and separate custom texture handling props
211
- const { texture: _texture, offset: _offset, repeat: _repeat, repeatCount: _repeatCount, animateOffset: _animateOffset, offsetSpeed: _offsetSpeed, generateMipmaps: _generateMipmaps, minFilter: _minFilter, magFilter: _magFilter, map: _map, materialType: _materialType, normalMapTexture: _normalMapTexture, normalScale: _normalScale, normalMap: _normalMap, rotation, sizeAttenuation, side: sideProp } = materialSource, materialProps = __rest(materialSource, ["texture", "offset", "repeat", "repeatCount", "animateOffset", "offsetSpeed", "generateMipmaps", "minFilter", "magFilter", "map", "materialType", "normalMapTexture", "normalScale", "normalMap", "rotation", "sizeAttenuation", "side"]);
245
+ const { texture: _texture, offset: _offset, repeat: _repeat, repeatCount: _repeatCount, animateOffset: _animateOffset, offsetSpeed: _offsetSpeed, generateMipmaps: _generateMipmaps, minFilter: _minFilter, magFilter: _magFilter, map: _map, materialType: _materialType, attach, normalMapTexture: _normalMapTexture, normalScale: _normalScale, normalMap: _normalMap, rotation, sizeAttenuation, side: sideProp } = materialSource, materialProps = __rest(materialSource, ["texture", "offset", "repeat", "repeatCount", "animateOffset", "offsetSpeed", "generateMipmaps", "minFilter", "magFilter", "map", "materialType", "attach", "normalMapTexture", "normalScale", "normalMap", "rotation", "sizeAttenuation", "side"]);
212
246
  const resolvedSide = sideProp ? (_d = SIDE_MAP[sideProp]) !== null && _d !== void 0 ? _d : FrontSide : FrontSide;
213
247
  const resolvedMinFilter = (_e = MIN_FILTER_MAP[minFilter]) !== null && _e !== void 0 ? _e : LinearMipmapLinearFilter;
214
248
  const resolvedMagFilter = (_f = MAG_FILTER_MAP[magFilter]) !== null && _f !== void 0 ? _f : LinearFilter;
215
249
  const animatedOffsetRef = useRef([(_g = offset === null || offset === void 0 ? void 0 : offset[0]) !== null && _g !== void 0 ? _g : 0, (_h = offset === null || offset === void 0 ? void 0 : offset[1]) !== null && _h !== void 0 ? _h : 0]);
216
- const finalTexture = useMemo(() => {
217
- if (!texture)
218
- return undefined;
219
- return cloneConfiguredTexture({
220
- texture,
221
- repeat,
222
- repeatCount,
223
- offset,
224
- colorSpace: SRGBColorSpace,
225
- generateMipmaps,
226
- minFilter: resolvedMinFilter,
227
- magFilter: resolvedMagFilter,
228
- });
229
- }, [texture, repeat, repeatCount === null || repeatCount === void 0 ? void 0 : repeatCount[0], repeatCount === null || repeatCount === void 0 ? void 0 : repeatCount[1], offset === null || offset === void 0 ? void 0 : offset[0], offset === null || offset === void 0 ? void 0 : offset[1], generateMipmaps, resolvedMinFilter, resolvedMagFilter]);
230
- const finalNormalMap = useMemo(() => {
231
- if (!normalMapTexture)
232
- return undefined;
233
- return cloneConfiguredTexture({
234
- texture: normalMapTexture,
235
- repeat,
236
- repeatCount,
237
- offset,
238
- colorSpace: NoColorSpace,
239
- generateMipmaps,
240
- minFilter: resolvedMinFilter,
241
- magFilter: resolvedMagFilter,
242
- });
243
- }, [normalMapTexture, repeat, repeatCount === null || repeatCount === void 0 ? void 0 : repeatCount[0], repeatCount === null || repeatCount === void 0 ? void 0 : repeatCount[1], offset === null || offset === void 0 ? void 0 : offset[0], offset === null || offset === void 0 ? void 0 : offset[1], generateMipmaps, resolvedMinFilter, resolvedMagFilter]);
250
+ const textureConfig = {
251
+ repeat,
252
+ repeatCount,
253
+ offset,
254
+ generateMipmaps,
255
+ minFilter: resolvedMinFilter,
256
+ magFilter: resolvedMagFilter,
257
+ };
258
+ const finalTexture = useConfiguredTexture(texture, Object.assign(Object.assign({}, textureConfig), { colorSpace: SRGBColorSpace }));
259
+ const finalNormalMap = useConfiguredTexture(normalMapTexture, Object.assign(Object.assign({}, textureConfig), { colorSpace: NoColorSpace }));
244
260
  animatedOffsetRef.current = [(_j = offset === null || offset === void 0 ? void 0 : offset[0]) !== null && _j !== void 0 ? _j : 0, (_k = offset === null || offset === void 0 ? void 0 : offset[1]) !== null && _k !== void 0 ? _k : 0];
245
261
  useFrame((_, delta) => {
246
262
  var _a, _b;
@@ -259,22 +275,34 @@ function MaterialComponentView({ properties: rawProps }) {
259
275
  const sharedProps = Object.assign(Object.assign({ map: finalTexture !== null && finalTexture !== void 0 ? finalTexture : null, side: resolvedSide, onUpdate: (material) => {
260
276
  material.needsUpdate = true;
261
277
  } }, materialProps), overrides);
278
+ const materialKey = [
279
+ materialType,
280
+ textureName !== null && textureName !== void 0 ? textureName : 'no-texture',
281
+ normalMapTextureName !== null && normalMapTextureName !== void 0 ? normalMapTextureName : 'no-normal',
282
+ repeat ? 'repeat' : 'clamp',
283
+ (_l = repeatCount === null || repeatCount === void 0 ? void 0 : repeatCount[0]) !== null && _l !== void 0 ? _l : 1,
284
+ (_m = repeatCount === null || repeatCount === void 0 ? void 0 : repeatCount[1]) !== null && _m !== void 0 ? _m : 1,
285
+ generateMipmaps ? 'mips' : 'no-mips',
286
+ minFilter,
287
+ magFilter,
288
+ ].join('|');
262
289
  if (materialType === 'basic') {
263
- return _jsx("meshBasicNodeMaterial", Object.assign({ attach: "material" }, sharedProps));
290
+ return _jsx("meshBasicNodeMaterial", Object.assign({ attach: attach !== null && attach !== void 0 ? attach : 'material' }, sharedProps), materialKey);
264
291
  }
265
292
  if (materialType === 'sprite') {
266
293
  const spriteTransparent = materialSource.transparent !== false;
267
- return (_jsx("spriteNodeMaterial", Object.assign({ attach: "material", map: finalTexture !== null && finalTexture !== void 0 ? finalTexture : null, color: (_l = materialSource.color) !== null && _l !== void 0 ? _l : '#ffffff', opacity: (_m = materialSource.opacity) !== null && _m !== void 0 ? _m : 1, transparent: spriteTransparent, alphaTest: (_o = materialSource.alphaTest) !== null && _o !== void 0 ? _o : 0, depthTest: (_p = materialSource.depthTest) !== null && _p !== void 0 ? _p : false, depthWrite: (_q = materialSource.depthWrite) !== null && _q !== void 0 ? _q : false, toneMapped: (_r = materialSource.toneMapped) !== null && _r !== void 0 ? _r : true, onUpdate: material => {
294
+ return (_jsx("spriteNodeMaterial", Object.assign({ attach: attach !== null && attach !== void 0 ? attach : 'material', map: finalTexture !== null && finalTexture !== void 0 ? finalTexture : null, color: (_o = materialSource.color) !== null && _o !== void 0 ? _o : '#ffffff', opacity: (_p = materialSource.opacity) !== null && _p !== void 0 ? _p : 1, transparent: spriteTransparent, alphaTest: (_q = materialSource.alphaTest) !== null && _q !== void 0 ? _q : 0, depthTest: (_r = materialSource.depthTest) !== null && _r !== void 0 ? _r : false, depthWrite: (_s = materialSource.depthWrite) !== null && _s !== void 0 ? _s : false, toneMapped: (_t = materialSource.toneMapped) !== null && _t !== void 0 ? _t : true, onUpdate: material => {
268
295
  material.needsUpdate = true;
269
- } }, overrides, { rotation: rotation !== null && rotation !== void 0 ? rotation : 0, sizeAttenuation: sizeAttenuation !== null && sizeAttenuation !== void 0 ? sizeAttenuation : true })));
296
+ } }, overrides, { rotation: rotation !== null && rotation !== void 0 ? rotation : 0, sizeAttenuation: sizeAttenuation !== null && sizeAttenuation !== void 0 ? sizeAttenuation : true }), materialKey));
270
297
  }
271
- return (_jsx("meshStandardNodeMaterial", Object.assign({ attach: "material" }, sharedProps, { normalMap: finalNormalMap !== null && finalNormalMap !== void 0 ? finalNormalMap : null, normalScale: finalNormalMap ? [(_s = normalScaleProp === null || normalScaleProp === void 0 ? void 0 : normalScaleProp[0]) !== null && _s !== void 0 ? _s : 1, (_t = normalScaleProp === null || normalScaleProp === void 0 ? void 0 : normalScaleProp[1]) !== null && _t !== void 0 ? _t : 1] : undefined })));
298
+ return (_jsx("meshStandardNodeMaterial", Object.assign({ attach: attach !== null && attach !== void 0 ? attach : 'material' }, sharedProps, { normalMap: finalNormalMap !== null && finalNormalMap !== void 0 ? finalNormalMap : null, normalScale: finalNormalMap ? [(_u = normalScaleProp === null || normalScaleProp === void 0 ? void 0 : normalScaleProp[0]) !== null && _u !== void 0 ? _u : 1, (_v = normalScaleProp === null || normalScaleProp === void 0 ? void 0 : normalScaleProp[1]) !== null && _v !== void 0 ? _v : 1] : undefined }), materialKey));
272
299
  }
273
300
  const MaterialComponent = {
274
301
  name: 'Material',
275
302
  Editor: MaterialComponentEditor,
276
303
  View: MaterialComponentView,
277
304
  defaultProperties: {
305
+ attach: 'material',
278
306
  materialType: 'standard',
279
307
  color: '#ffffff',
280
308
  toneMapped: true,
@@ -5,9 +5,10 @@ import { Mesh } from 'three';
5
5
  import { assetRef, assetRefs } from './ComponentRegistry';
6
6
  import { BooleanField, FieldGroup, Label, ListEditor, NumberInput, SelectInput, StringField } from './Input';
7
7
  import { useAssetRuntime } from '../assetRuntime';
8
- import { useEditorContext } from '../PrefabEditor';
8
+ import { useEditorContext, useEditorRef } from '../PrefabEditor';
9
9
  import { getRepeatAxesFromModelProperties, normalizeRepeatAxes } from '../InstanceProvider';
10
- import { colors, ui } from '../styles';
10
+ import { base, colors, ui } from '../styles';
11
+ import { decomposeModelToPrefabNodes } from '../modelPrefab';
11
12
  const AXIS_OPTIONS = [
12
13
  { value: 'x', label: 'X' },
13
14
  { value: 'y', label: 'Y' },
@@ -42,8 +43,49 @@ function RepeatAxisEditor({ axes, onChange, positionSnap, }) {
42
43
  }
43
44
  function ModelComponentEditor({ component, node, onUpdate, basePath = "" }) {
44
45
  const { positionSnap } = useEditorContext();
46
+ const editor = useEditorRef();
45
47
  const repeatAxes = getRepeatAxesFromModelProperties(component.properties);
46
- return (_jsxs(FieldGroup, { children: [_jsx(ModelPicker, { value: component.properties.filename, onChange: (filename) => onUpdate({ filename }), basePath: basePath, pickerKey: node === null || node === void 0 ? void 0 : node.id }), _jsx(BooleanField, { name: "instanced", label: "Instanced", values: component.properties, onChange: onUpdate, fallback: false }), !component.properties.instanced ? (_jsxs(_Fragment, { children: [_jsx(BooleanField, { name: "emitClickEvent", label: "Emit Click Event", values: component.properties, onChange: onUpdate, fallback: false }), component.properties.emitClickEvent ? (_jsx(StringField, { name: "clickEventName", label: "Click Event Name", values: component.properties, onChange: onUpdate, placeholder: "node:click" })) : null] })) : null, 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 }))] }))] }));
48
+ const filename = component.properties.filename;
49
+ const canDecompose = Boolean(node && filename);
50
+ const handleDecompose = () => {
51
+ var _a, _b, _c, _d;
52
+ if (!node || !filename)
53
+ return;
54
+ const model = editor.getModel(filename);
55
+ if (!model) {
56
+ console.warn(`Model is not loaded yet: ${filename}`);
57
+ return;
58
+ }
59
+ const textureRefs = new Map();
60
+ const decomposed = decomposeModelToPrefabNodes(model, {
61
+ idPrefix: node.id,
62
+ getTexturePath: (texture, usage) => {
63
+ const key = `embedded/${node.id}/${usage}/${texture.uuid}`;
64
+ textureRefs.set(key, texture);
65
+ return key;
66
+ },
67
+ });
68
+ textureRefs.forEach((texture, path) => {
69
+ editor.addTexture(path, texture);
70
+ });
71
+ const preservedComponents = Object.entries((_a = node.components) !== null && _a !== void 0 ? _a : {}).reduce((result, [key, entry]) => {
72
+ if (!(entry === null || entry === void 0 ? void 0 : entry.type))
73
+ return result;
74
+ if (entry.type === 'Model' || entry.type === 'Geometry' || entry.type === 'BufferGeometry' || entry.type === 'Material') {
75
+ return result;
76
+ }
77
+ result[key] = entry;
78
+ return result;
79
+ }, {});
80
+ const decomposedComponents = Object.entries((_b = decomposed.components) !== null && _b !== void 0 ? _b : {}).reduce((result, [key, entry]) => {
81
+ if (!(entry === null || entry === void 0 ? void 0 : entry.type) || entry.type === 'Transform')
82
+ return result;
83
+ result[key] = entry;
84
+ return result;
85
+ }, {});
86
+ editor.replaceNode(node.id, Object.assign(Object.assign({}, node), { name: (_c = node.name) !== null && _c !== void 0 ? _c : decomposed.name, components: Object.assign(Object.assign({}, preservedComponents), decomposedComponents), children: (_d = decomposed.children) !== null && _d !== void 0 ? _d : [] }));
87
+ };
88
+ return (_jsxs(FieldGroup, { children: [_jsx(ModelPicker, { value: filename, onChange: (filename) => onUpdate({ filename }), basePath: basePath, pickerKey: node === null || node === void 0 ? void 0 : node.id }), _jsx("button", { type: "button", style: Object.assign(Object.assign({}, base.btn), { width: '100%' }), onClick: handleDecompose, disabled: !canDecompose, title: canDecompose ? 'Replace this model node with editable geometry and material nodes' : 'Choose a model before decomposing', children: "Decompose Model" }), _jsx(BooleanField, { name: "instanced", label: "Instanced", values: component.properties, onChange: onUpdate, fallback: false }), !component.properties.instanced ? (_jsxs(_Fragment, { children: [_jsx(BooleanField, { name: "emitClickEvent", label: "Emit Click Event", values: component.properties, onChange: onUpdate, fallback: false }), component.properties.emitClickEvent ? (_jsx(StringField, { name: "clickEventName", label: "Click Event Name", values: component.properties, onChange: onUpdate, placeholder: "node:click" })) : null] })) : null, 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 }))] }))] }));
47
89
  }
48
90
  // View for Model component
49
91
  function ModelComponentView({ properties, children }) {
@@ -17,6 +17,7 @@ function SpriteComponentEditor({ component, onUpdate, }) {
17
17
  }
18
18
  const SpriteComponent = {
19
19
  name: 'Sprite',
20
+ disableSiblingComposition: true,
20
21
  Editor: SpriteComponentEditor,
21
22
  defaultProperties: {
22
23
  center: [0.5, 0.5],
@@ -48,6 +48,7 @@ function TransformComponentEditor({ component, onUpdate }) {
48
48
  }
49
49
  const TransformComponent = {
50
50
  name: 'Transform',
51
+ disableSiblingComposition: true,
51
52
  Editor: TransformComponentEditor,
52
53
  defaultProperties: {}
53
54
  };
@@ -0,0 +1,16 @@
1
+ import { Object3D } from 'three';
2
+ import type { Texture } from 'three';
3
+ import type { GameObject } from './types';
4
+ export interface DecomposeModelOptions {
5
+ /** Prefix used for generated prefab node ids. Defaults to "model". */
6
+ idPrefix?: string;
7
+ /** Include invisible Three objects in the generated prefab tree. */
8
+ includeInvisible?: boolean;
9
+ /** Return a serializable texture ref for embedded or externally loaded textures. */
10
+ getTexturePath?: (texture: Texture, usage: 'map' | 'normalMap') => string | null | undefined;
11
+ }
12
+ /**
13
+ * Converts a live Three object hierarchy into prefab JSON nodes made from
14
+ * Transform, BufferGeometry, and Material components.
15
+ */
16
+ export declare function decomposeModelToPrefabNodes(object: Object3D, options?: DecomposeModelOptions): GameObject;
@@ -0,0 +1,180 @@
1
+ import { BackSide, DoubleSide, Mesh, } from 'three';
2
+ function createId(prefix) {
3
+ return `${prefix}-${crypto.randomUUID()}`;
4
+ }
5
+ function toArrayAttribute(attribute, itemSize, vertexIndices) {
6
+ if (!attribute || attribute.itemSize < itemSize)
7
+ return undefined;
8
+ const values = [];
9
+ for (const vertexIndex of vertexIndices) {
10
+ for (let axis = 0; axis < itemSize; axis += 1) {
11
+ values.push(attribute.getComponent(vertexIndex, axis));
12
+ }
13
+ }
14
+ return values;
15
+ }
16
+ function getSequentialVertexIndices(count) {
17
+ return Array.from({ length: count }, (_, index) => index);
18
+ }
19
+ function serializeGeometry(geometry) {
20
+ var _a, _b, _c, _d;
21
+ const position = geometry.getAttribute('position');
22
+ const normal = geometry.getAttribute('normal');
23
+ const uv = geometry.getAttribute('uv');
24
+ const vertexIndices = getSequentialVertexIndices((_a = position === null || position === void 0 ? void 0 : position.count) !== null && _a !== void 0 ? _a : 0);
25
+ const index = geometry.index;
26
+ const indices = [];
27
+ if (index) {
28
+ for (let offset = 0; offset < index.count; offset += 1) {
29
+ indices.push(index.getX(offset));
30
+ }
31
+ }
32
+ else {
33
+ indices.push(...vertexIndices);
34
+ }
35
+ const positions = (_b = toArrayAttribute(position, 3, vertexIndices)) !== null && _b !== void 0 ? _b : [];
36
+ const normals = (_c = toArrayAttribute(normal, 3, vertexIndices)) !== null && _c !== void 0 ? _c : [];
37
+ const uvs = (_d = toArrayAttribute(uv, 2, vertexIndices)) !== null && _d !== void 0 ? _d : [];
38
+ const groups = geometry.groups.map(group => {
39
+ var _a;
40
+ return ({
41
+ start: group.start,
42
+ count: group.count,
43
+ materialIndex: (_a = group.materialIndex) !== null && _a !== void 0 ? _a : 0,
44
+ });
45
+ });
46
+ return {
47
+ positions,
48
+ indices,
49
+ normals,
50
+ uvs,
51
+ groups,
52
+ computeVertexNormals: normals.length === 0,
53
+ };
54
+ }
55
+ function getSideName(side) {
56
+ if (side === BackSide)
57
+ return 'BackSide';
58
+ if (side === DoubleSide)
59
+ return 'DoubleSide';
60
+ return 'FrontSide';
61
+ }
62
+ function getMaterialColor(material) {
63
+ var _a, _b;
64
+ const maybeColor = material;
65
+ return (_b = (_a = maybeColor.color) === null || _a === void 0 ? void 0 : _a.getStyle) === null || _b === void 0 ? void 0 : _b.call(_a);
66
+ }
67
+ function getTextureImagePath(texture) {
68
+ var _a;
69
+ const image = texture === null || texture === void 0 ? void 0 : texture.image;
70
+ const path = (_a = image === null || image === void 0 ? void 0 : image.currentSrc) !== null && _a !== void 0 ? _a : image === null || image === void 0 ? void 0 : image.src;
71
+ if (typeof path === 'string' && path.trim() && !path.startsWith('blob:')) {
72
+ return path;
73
+ }
74
+ return getTextureImageDataUrl(image);
75
+ }
76
+ function getTextureImageDataUrl(image) {
77
+ if (!image
78
+ || typeof document === 'undefined'
79
+ || typeof image.width !== 'number'
80
+ || typeof image.height !== 'number'
81
+ || image.width <= 0
82
+ || image.height <= 0) {
83
+ return undefined;
84
+ }
85
+ try {
86
+ const canvas = document.createElement('canvas');
87
+ canvas.width = image.width;
88
+ canvas.height = image.height;
89
+ const context = canvas.getContext('2d');
90
+ if (!context)
91
+ return undefined;
92
+ context.drawImage(image, 0, 0);
93
+ return canvas.toDataURL('image/png');
94
+ }
95
+ catch (_a) {
96
+ return undefined;
97
+ }
98
+ }
99
+ function getTexturePath(texture, usage, options) {
100
+ var _a, _b;
101
+ if (!texture)
102
+ return undefined;
103
+ return (_b = (_a = getTextureImagePath(texture)) !== null && _a !== void 0 ? _a : options.getTexturePath(texture, usage)) !== null && _b !== void 0 ? _b : undefined;
104
+ }
105
+ function serializeMaterial(material, attach, options) {
106
+ var _a, _b, _c, _d, _e, _f, _g;
107
+ const source = material;
108
+ const materialType = 'metalness' in source || 'roughness' in source ? 'standard' : 'basic';
109
+ const texture = getTexturePath(source.map, 'map', options);
110
+ const normalMapTexture = getTexturePath(source.normalMap, 'normalMap', options);
111
+ const normalScale = (_b = (_a = source.normalScale) === null || _a === void 0 ? void 0 : _a.toArray) === null || _b === void 0 ? void 0 : _b.call(_a);
112
+ return {
113
+ type: 'Material',
114
+ properties: Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ attach,
115
+ materialType, color: (_c = getMaterialColor(material)) !== null && _c !== void 0 ? _c : '#ffffff' }, (texture ? { texture } : null)), (normalMapTexture ? { normalMapTexture } : null)), (normalScale ? { normalScale } : null)), { opacity: material.opacity, transparent: material.transparent, side: getSideName(material.side), wireframe: (_d = source.wireframe) !== null && _d !== void 0 ? _d : false, toneMapped: (_e = source.toneMapped) !== null && _e !== void 0 ? _e : true }), (materialType === 'standard' ? {
116
+ metalness: (_f = source.metalness) !== null && _f !== void 0 ? _f : 0,
117
+ roughness: (_g = source.roughness) !== null && _g !== void 0 ? _g : 1,
118
+ } : null)),
119
+ };
120
+ }
121
+ function getMeshParts(mesh) {
122
+ const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
123
+ return materials.map((material, index) => {
124
+ return {
125
+ key: materials.length > 1 ? `material_${index}` : 'material',
126
+ material,
127
+ attach: materials.length > 1 ? `material-${index}` : 'material',
128
+ };
129
+ }).filter(part => part.material);
130
+ }
131
+ function createTransformComponent(object) {
132
+ return {
133
+ type: 'Transform',
134
+ properties: {
135
+ position: object.position.toArray(),
136
+ rotation: [object.rotation.x, object.rotation.y, object.rotation.z],
137
+ scale: object.scale.toArray(),
138
+ },
139
+ };
140
+ }
141
+ function createNode(object, idPrefix, children = [], components = {}) {
142
+ return {
143
+ id: createId(idPrefix),
144
+ name: object.name || object.type,
145
+ hidden: object.visible === false ? true : undefined,
146
+ components: Object.assign({ transform: createTransformComponent(object) }, components),
147
+ children,
148
+ };
149
+ }
150
+ function decomposeObject(object, options) {
151
+ if (!options.includeInvisible && !object.visible)
152
+ return null;
153
+ const childNodes = object.children
154
+ .map(child => decomposeObject(child, options))
155
+ .filter((child) => child != null);
156
+ if (!(object instanceof Mesh)) {
157
+ return createNode(object, options.idPrefix, childNodes);
158
+ }
159
+ const parts = getMeshParts(object);
160
+ const materialComponents = parts.reduce((result, part) => {
161
+ result[part.key] = serializeMaterial(part.material, part.attach, options);
162
+ return result;
163
+ }, {});
164
+ return createNode(object, options.idPrefix, childNodes, Object.assign({ geometry: {
165
+ type: 'BufferGeometry',
166
+ properties: Object.assign(Object.assign({}, serializeGeometry(object.geometry)), { visible: object.visible, castShadow: object.castShadow, receiveShadow: object.receiveShadow }),
167
+ } }, materialComponents));
168
+ }
169
+ /**
170
+ * Converts a live Three object hierarchy into prefab JSON nodes made from
171
+ * Transform, BufferGeometry, and Material components.
172
+ */
173
+ export function decomposeModelToPrefabNodes(object, options = {}) {
174
+ var _a, _b, _c, _d, _e;
175
+ return (_d = decomposeObject(object, {
176
+ idPrefix: (_a = options.idPrefix) !== null && _a !== void 0 ? _a : 'model',
177
+ includeInvisible: (_b = options.includeInvisible) !== null && _b !== void 0 ? _b : false,
178
+ getTexturePath: (_c = options.getTexturePath) !== null && _c !== void 0 ? _c : (() => undefined),
179
+ })) !== null && _d !== void 0 ? _d : createNode(object, (_e = options.idPrefix) !== null && _e !== void 0 ? _e : 'model');
180
+ }
@@ -5,6 +5,7 @@ import { denormalizePrefab, PrefabState, PrefabNodeRecord } from "./prefab";
5
5
  export interface PrefabStoreState extends PrefabState {
6
6
  replacePrefab: (prefab: Prefab) => void;
7
7
  updateNode: (id: string, update: (node: PrefabNodeRecord) => PrefabNodeRecord) => void;
8
+ replaceNode: (id: string, node: GameObject) => void;
8
9
  addChild: (parentId: string, node: GameObject) => void;
9
10
  deleteNode: (id: string) => void;
10
11
  duplicateNode: (id: string) => string | null;