react-three-game 0.0.100 → 0.0.101

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 (29) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js +1 -0
  3. package/dist/tools/assetviewer/page.js +70 -58
  4. package/dist/tools/dragdrop/DragDropLoader.d.ts +3 -0
  5. package/dist/tools/dragdrop/DragDropLoader.js +183 -44
  6. package/dist/tools/dragdrop/index.d.ts +1 -1
  7. package/dist/tools/dragdrop/index.js +1 -1
  8. package/dist/tools/dragdrop/modelLoader.js +2 -0
  9. package/dist/tools/prefabeditor/EditorUI.js +7 -8
  10. package/dist/tools/prefabeditor/PrefabEditor.d.ts +3 -0
  11. package/dist/tools/prefabeditor/PrefabEditor.js +28 -11
  12. package/dist/tools/prefabeditor/PrefabRoot.d.ts +2 -0
  13. package/dist/tools/prefabeditor/PrefabRoot.js +51 -35
  14. package/dist/tools/prefabeditor/components/BufferGeometryComponent.js +20 -0
  15. package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +5 -0
  16. package/dist/tools/prefabeditor/components/ComponentRegistry.js +31 -0
  17. package/dist/tools/prefabeditor/components/DataComponent.js +1 -0
  18. package/dist/tools/prefabeditor/components/EnvironmentComponent.js +1 -0
  19. package/dist/tools/prefabeditor/components/GeometryComponent.js +1 -0
  20. package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +1 -0
  21. package/dist/tools/prefabeditor/components/MaterialComponent.js +89 -52
  22. package/dist/tools/prefabeditor/components/ModelComponent.js +45 -3
  23. package/dist/tools/prefabeditor/components/SpriteComponent.js +1 -0
  24. package/dist/tools/prefabeditor/components/TransformComponent.js +1 -0
  25. package/dist/tools/prefabeditor/modelPrefab.d.ts +16 -0
  26. package/dist/tools/prefabeditor/modelPrefab.js +180 -0
  27. package/dist/tools/prefabeditor/prefabStore.d.ts +1 -0
  28. package/dist/tools/prefabeditor/prefabStore.js +75 -42
  29. package/package.json +1 -1
@@ -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, 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,26 +38,6 @@ 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]);
48
- }
49
- else {
50
- clonedTexture.wrapS = clonedTexture.wrapT = ClampToEdgeWrapping;
51
- clonedTexture.repeat.set(1, 1);
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;
60
- }
61
41
  export function useMaterialOverrides() {
62
42
  return useContext(MaterialOverridesContext);
63
43
  }
@@ -80,6 +60,11 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "", }) {
80
60
  const isStandardMaterial = materialType === 'standard';
81
61
  const isSpriteMaterial = materialType === 'sprite';
82
62
  const fields = [
63
+ {
64
+ name: 'attach',
65
+ type: 'string',
66
+ label: 'Attach',
67
+ },
83
68
  {
84
69
  name: 'materialType',
85
70
  type: 'select',
@@ -208,39 +193,85 @@ function MaterialComponentView({ properties: rawProps }) {
208
193
  const normalScaleProp = materialSource.normalScale;
209
194
  const normalMapTexture = normalMapTextureName ? getTexture(normalMapTextureName) : undefined;
210
195
  // 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"]);
196
+ 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
197
  const resolvedSide = sideProp ? (_d = SIDE_MAP[sideProp]) !== null && _d !== void 0 ? _d : FrontSide : FrontSide;
213
198
  const resolvedMinFilter = (_e = MIN_FILTER_MAP[minFilter]) !== null && _e !== void 0 ? _e : LinearMipmapLinearFilter;
214
199
  const resolvedMagFilter = (_f = MAG_FILTER_MAP[magFilter]) !== null && _f !== void 0 ? _f : LinearFilter;
215
200
  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
201
  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]);
202
+ return texture ? texture.clone() : undefined;
203
+ }, [texture]);
204
+ useEffect(() => {
205
+ return () => finalTexture === null || finalTexture === void 0 ? void 0 : finalTexture.dispose();
206
+ }, [finalTexture]);
207
+ useEffect(() => {
208
+ var _a, _b;
209
+ if (!finalTexture)
210
+ return;
211
+ if (repeat) {
212
+ finalTexture.wrapS = finalTexture.wrapT = RepeatWrapping;
213
+ if (repeatCount) {
214
+ finalTexture.repeat.set(repeatCount[0], repeatCount[1]);
215
+ }
216
+ }
217
+ else {
218
+ finalTexture.wrapS = finalTexture.wrapT = ClampToEdgeWrapping;
219
+ finalTexture.repeat.set(1, 1);
220
+ }
221
+ finalTexture.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);
222
+ finalTexture.colorSpace = SRGBColorSpace;
223
+ finalTexture.generateMipmaps = generateMipmaps;
224
+ finalTexture.minFilter = resolvedMinFilter;
225
+ finalTexture.magFilter = resolvedMagFilter;
226
+ finalTexture.needsUpdate = true;
227
+ }, [
228
+ finalTexture,
229
+ repeat,
230
+ repeatCount === null || repeatCount === void 0 ? void 0 : repeatCount[0],
231
+ repeatCount === null || repeatCount === void 0 ? void 0 : repeatCount[1],
232
+ offset === null || offset === void 0 ? void 0 : offset[0],
233
+ offset === null || offset === void 0 ? void 0 : offset[1],
234
+ generateMipmaps,
235
+ resolvedMinFilter,
236
+ resolvedMagFilter
237
+ ]);
230
238
  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]);
239
+ return normalMapTexture ? normalMapTexture.clone() : undefined;
240
+ }, [normalMapTexture]);
241
+ useEffect(() => {
242
+ return () => finalNormalMap === null || finalNormalMap === void 0 ? void 0 : finalNormalMap.dispose();
243
+ }, [finalNormalMap]);
244
+ useEffect(() => {
245
+ var _a, _b;
246
+ if (!finalNormalMap)
247
+ return;
248
+ if (repeat) {
249
+ finalNormalMap.wrapS = finalNormalMap.wrapT = RepeatWrapping;
250
+ if (repeatCount) {
251
+ finalNormalMap.repeat.set(repeatCount[0], repeatCount[1]);
252
+ }
253
+ }
254
+ else {
255
+ finalNormalMap.wrapS = finalNormalMap.wrapT = ClampToEdgeWrapping;
256
+ finalNormalMap.repeat.set(1, 1);
257
+ }
258
+ finalNormalMap.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);
259
+ finalNormalMap.colorSpace = NoColorSpace;
260
+ finalNormalMap.generateMipmaps = generateMipmaps;
261
+ finalNormalMap.minFilter = resolvedMinFilter;
262
+ finalNormalMap.magFilter = resolvedMagFilter;
263
+ finalNormalMap.needsUpdate = true;
264
+ }, [
265
+ finalNormalMap,
266
+ repeat,
267
+ repeatCount === null || repeatCount === void 0 ? void 0 : repeatCount[0],
268
+ repeatCount === null || repeatCount === void 0 ? void 0 : repeatCount[1],
269
+ offset === null || offset === void 0 ? void 0 : offset[0],
270
+ offset === null || offset === void 0 ? void 0 : offset[1],
271
+ generateMipmaps,
272
+ resolvedMinFilter,
273
+ resolvedMagFilter
274
+ ]);
244
275
  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
276
  useFrame((_, delta) => {
246
277
  var _a, _b;
@@ -259,22 +290,28 @@ function MaterialComponentView({ properties: rawProps }) {
259
290
  const sharedProps = Object.assign(Object.assign({ map: finalTexture !== null && finalTexture !== void 0 ? finalTexture : null, side: resolvedSide, onUpdate: (material) => {
260
291
  material.needsUpdate = true;
261
292
  } }, materialProps), overrides);
293
+ const materialKey = [
294
+ materialType,
295
+ textureName !== null && textureName !== void 0 ? textureName : 'no-texture',
296
+ normalMapTextureName !== null && normalMapTextureName !== void 0 ? normalMapTextureName : 'no-normal',
297
+ ].join('|');
262
298
  if (materialType === 'basic') {
263
- return _jsx("meshBasicNodeMaterial", Object.assign({ attach: "material" }, sharedProps));
299
+ return _jsx("meshBasicNodeMaterial", Object.assign({ attach: attach !== null && attach !== void 0 ? attach : 'material' }, sharedProps), materialKey);
264
300
  }
265
301
  if (materialType === 'sprite') {
266
302
  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 => {
303
+ return (_jsx("spriteNodeMaterial", Object.assign({ attach: attach !== null && attach !== void 0 ? 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 => {
268
304
  material.needsUpdate = true;
269
- } }, overrides, { rotation: rotation !== null && rotation !== void 0 ? rotation : 0, sizeAttenuation: sizeAttenuation !== null && sizeAttenuation !== void 0 ? sizeAttenuation : true })));
305
+ } }, overrides, { rotation: rotation !== null && rotation !== void 0 ? rotation : 0, sizeAttenuation: sizeAttenuation !== null && sizeAttenuation !== void 0 ? sizeAttenuation : true }), materialKey));
270
306
  }
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 })));
307
+ return (_jsx("meshStandardNodeMaterial", Object.assign({ attach: attach !== null && attach !== void 0 ? 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 }), materialKey));
272
308
  }
273
309
  const MaterialComponent = {
274
310
  name: 'Material',
275
311
  Editor: MaterialComponentEditor,
276
312
  View: MaterialComponentView,
277
313
  defaultProperties: {
314
+ attach: 'material',
278
315
  materialType: 'standard',
279
316
  color: '#ffffff',
280
317
  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;
@@ -5,6 +5,45 @@ import { createStore } from "zustand/vanilla";
5
5
  import { collectAssetRefsForIds, collectSubtreeAssetRefs, collectSubtreeIds, cloneSubtree, createPrefabPatch, denormalizePrefab, insertSubtree, isDescendant, normalizePrefab, updateAssetRefsForNodeChange, } from "./prefab";
6
6
  const PrefabStoreContext = createContext(null);
7
7
  const EMPTY_CHILD_IDS = [];
8
+ function addAssetRefs(assetRefCounts, refs) {
9
+ refs.forEach(ref => {
10
+ var _a;
11
+ assetRefCounts[ref] = ((_a = assetRefCounts[ref]) !== null && _a !== void 0 ? _a : 0) + 1;
12
+ });
13
+ }
14
+ function removeAssetRefs(assetRefCounts, refs) {
15
+ refs.forEach(ref => {
16
+ var _a;
17
+ const nextCount = ((_a = assetRefCounts[ref]) !== null && _a !== void 0 ? _a : 0) - 1;
18
+ if (nextCount > 0) {
19
+ assetRefCounts[ref] = nextCount;
20
+ return;
21
+ }
22
+ delete assetRefCounts[ref];
23
+ });
24
+ }
25
+ function cloneGraphState(state) {
26
+ return {
27
+ nodesById: Object.assign({}, state.nodesById),
28
+ childIdsById: Object.assign({}, state.childIdsById),
29
+ parentIdById: Object.assign({}, state.parentIdById),
30
+ assetRefCounts: Object.assign({}, state.assetRefCounts),
31
+ };
32
+ }
33
+ function removeSubtreeFromGraph(id, state, next) {
34
+ const ids = collectSubtreeIds(id, state.childIdsById);
35
+ removeAssetRefs(next.assetRefCounts, collectAssetRefsForIds(ids, state.nodesById));
36
+ ids.forEach(nodeId => {
37
+ delete next.nodesById[nodeId];
38
+ delete next.childIdsById[nodeId];
39
+ delete next.parentIdById[nodeId];
40
+ });
41
+ return ids;
42
+ }
43
+ function insertSubtreeIntoGraph(node, parentId, next) {
44
+ insertSubtree(node, parentId, next.nodesById, next.childIdsById, next.parentIdById);
45
+ addAssetRefs(next.assetRefCounts, collectSubtreeAssetRefs(node));
46
+ }
8
47
  export function PrefabStoreProvider({ store, children, }) {
9
48
  return createElement(PrefabStoreContext.Provider, { value: store }, children);
10
49
  }
@@ -42,26 +81,40 @@ export function createPrefabStore(prefab) {
42
81
  set(createPrefabPatch(state, {
43
82
  nodesById: Object.assign(Object.assign({}, state.nodesById), { [id]: nextNode }),
44
83
  }, nextAssetRefCounts));
84
+ }, replaceNode: (id, node) => {
85
+ var _a;
86
+ const state = get();
87
+ if (!state.nodesById[id])
88
+ return;
89
+ const parentId = state.parentIdById[id];
90
+ const next = cloneGraphState(state);
91
+ removeSubtreeFromGraph(id, state, next);
92
+ insertSubtreeIntoGraph(node, parentId, next);
93
+ const patch = {
94
+ nodesById: next.nodesById,
95
+ childIdsById: next.childIdsById,
96
+ parentIdById: next.parentIdById,
97
+ };
98
+ if (id === state.rootId) {
99
+ patch.rootId = node.id;
100
+ }
101
+ else if (parentId) {
102
+ next.childIdsById[parentId] = ((_a = next.childIdsById[parentId]) !== null && _a !== void 0 ? _a : []).map(childId => childId === id ? node.id : childId);
103
+ }
104
+ set(createPrefabPatch(state, patch, next.assetRefCounts));
45
105
  }, addChild: (parentId, node) => {
46
106
  var _a;
47
107
  const state = get();
48
108
  if (!state.nodesById[parentId])
49
109
  return;
50
- const nextNodesById = Object.assign({}, state.nodesById);
51
- const nextChildIdsById = Object.assign({}, state.childIdsById);
52
- const nextParentIdById = Object.assign({}, state.parentIdById);
53
- const nextAssetRefCounts = Object.assign({}, state.assetRefCounts);
54
- insertSubtree(node, parentId, nextNodesById, nextChildIdsById, nextParentIdById);
55
- nextChildIdsById[parentId] = [...((_a = nextChildIdsById[parentId]) !== null && _a !== void 0 ? _a : []), node.id];
56
- collectSubtreeAssetRefs(node).forEach(ref => {
57
- var _a;
58
- nextAssetRefCounts[ref] = ((_a = nextAssetRefCounts[ref]) !== null && _a !== void 0 ? _a : 0) + 1;
59
- });
110
+ const next = cloneGraphState(state);
111
+ insertSubtreeIntoGraph(node, parentId, next);
112
+ next.childIdsById[parentId] = [...((_a = next.childIdsById[parentId]) !== null && _a !== void 0 ? _a : []), node.id];
60
113
  set(createPrefabPatch(state, {
61
- nodesById: nextNodesById,
62
- childIdsById: nextChildIdsById,
63
- parentIdById: nextParentIdById,
64
- }, nextAssetRefCounts));
114
+ nodesById: next.nodesById,
115
+ childIdsById: next.childIdsById,
116
+ parentIdById: next.parentIdById,
117
+ }, next.assetRefCounts));
65
118
  }, deleteNode: (id) => {
66
119
  var _a;
67
120
  const state = get();
@@ -70,31 +123,14 @@ export function createPrefabStore(prefab) {
70
123
  const parentId = state.parentIdById[id];
71
124
  if (!parentId)
72
125
  return;
73
- const idsToDelete = collectSubtreeIds(id, state.childIdsById);
74
- const nextNodesById = Object.assign({}, state.nodesById);
75
- const nextChildIdsById = Object.assign({}, state.childIdsById);
76
- const nextParentIdById = Object.assign({}, state.parentIdById);
77
- const nextAssetRefCounts = Object.assign({}, state.assetRefCounts);
78
- collectAssetRefsForIds(idsToDelete, state.nodesById).forEach(ref => {
79
- var _a;
80
- const nextCount = ((_a = nextAssetRefCounts[ref]) !== null && _a !== void 0 ? _a : 0) - 1;
81
- if (nextCount > 0) {
82
- nextAssetRefCounts[ref] = nextCount;
83
- return;
84
- }
85
- delete nextAssetRefCounts[ref];
86
- });
87
- idsToDelete.forEach(nodeId => {
88
- delete nextNodesById[nodeId];
89
- delete nextChildIdsById[nodeId];
90
- delete nextParentIdById[nodeId];
91
- });
92
- nextChildIdsById[parentId] = ((_a = nextChildIdsById[parentId]) !== null && _a !== void 0 ? _a : []).filter(childId => childId !== id);
126
+ const next = cloneGraphState(state);
127
+ removeSubtreeFromGraph(id, state, next);
128
+ next.childIdsById[parentId] = ((_a = next.childIdsById[parentId]) !== null && _a !== void 0 ? _a : []).filter(childId => childId !== id);
93
129
  set(createPrefabPatch(state, {
94
- nodesById: nextNodesById,
95
- childIdsById: nextChildIdsById,
96
- parentIdById: nextParentIdById,
97
- }, nextAssetRefCounts));
130
+ nodesById: next.nodesById,
131
+ childIdsById: next.childIdsById,
132
+ parentIdById: next.parentIdById,
133
+ }, next.assetRefCounts));
98
134
  }, duplicateNode: (id) => {
99
135
  var _a;
100
136
  const state = get();
@@ -119,10 +155,7 @@ export function createPrefabStore(prefab) {
119
155
  }
120
156
  nextChildIdsById[parentId] = siblings;
121
157
  const nextAssetRefCounts = Object.assign({}, state.assetRefCounts);
122
- collectAssetRefsForIds(collectSubtreeIds(id, state.childIdsById), state.nodesById).forEach(ref => {
123
- var _a;
124
- nextAssetRefCounts[ref] = ((_a = nextAssetRefCounts[ref]) !== null && _a !== void 0 ? _a : 0) + 1;
125
- });
158
+ addAssetRefs(nextAssetRefCounts, collectAssetRefsForIds(collectSubtreeIds(id, state.childIdsById), state.nodesById));
126
159
  set(createPrefabPatch(state, {
127
160
  nodesById: nextNodesById,
128
161
  childIdsById: nextChildIdsById,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-three-game",
3
- "version": "0.0.100",
3
+ "version": "0.0.101",
4
4
  "description": "high performance 3D game engine built in React",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",