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
@@ -17,7 +17,7 @@ import { PrefabEditorMode, PrefabRoot } from "./PrefabRoot";
17
17
  import EditorUI from "./EditorUI";
18
18
  import { base, toolbar } from "./styles";
19
19
  import { computeParentWorldMatrix, decompose, exportGLB as exportGLBFile, exportGLBData, focusCameraOnObject, regenerateIds } from "./utils";
20
- import { loadFiles } from "../dragdrop";
20
+ import { loadDroppedAssets } from "../dragdrop";
21
21
  import { denormalizePrefab, createImageNode, createModelNode, createNode } from './prefab';
22
22
  import { createPrefabStore, PrefabStoreProvider } from "./prefabStore";
23
23
  function isObjectAttachedToRoot(root, object) {
@@ -31,6 +31,19 @@ function isObjectAttachedToRoot(root, object) {
31
31
  }
32
32
  return false;
33
33
  }
34
+ export function isAbsoluteAssetPath(path) {
35
+ return (path.startsWith("data:") ||
36
+ path.startsWith("http://") ||
37
+ path.startsWith("https://"));
38
+ }
39
+ export function resolvePrefabAssetPath(basePath, file) {
40
+ if (isAbsoluteAssetPath(file))
41
+ return file;
42
+ return file.startsWith("/") ? `${basePath}${file}` : `${basePath}/${file}`;
43
+ }
44
+ export function getPrefabAssetRef(assetRef, folder) {
45
+ return isAbsoluteAssetPath(assetRef) ? assetRef : `${folder}/${assetRef}`;
46
+ }
34
47
  function SelectionHelper({ object }) {
35
48
  const objectRef = useRef(null);
36
49
  objectRef.current = object;
@@ -91,6 +104,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, mode: initialMode =
91
104
  const getRoot = useCallback(() => { var _a, _b; return (_b = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.root) !== null && _b !== void 0 ? _b : null; }, []);
92
105
  const getObject = useCallback((nodeId) => { var _a, _b; return (_b = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.getObject(nodeId)) !== null && _b !== void 0 ? _b : null; }, []);
93
106
  const getHandle = useCallback((nodeId, kind) => { var _a, _b; return (_b = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.getHandle(nodeId, kind)) !== null && _b !== void 0 ? _b : null; }, []);
107
+ const getModel = useCallback((path) => { var _a, _b; return (_b = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.getModel(path)) !== null && _b !== void 0 ? _b : null; }, []);
94
108
  const scheduleHistory = useCallback((nextPrefab) => {
95
109
  if (historyTimeoutRef.current) {
96
110
  clearTimeout(historyTimeoutRef.current);
@@ -124,6 +138,9 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, mode: initialMode =
124
138
  const update = useCallback((id, fn) => {
125
139
  mutate(s => s.updateNode(id, fn));
126
140
  }, [mutate]);
141
+ const replaceNode = useCallback((id, node) => {
142
+ mutate(s => s.replaceNode(id, node));
143
+ }, [mutate]);
127
144
  const remove = useCallback((id) => {
128
145
  mutate(s => s.deleteNode(id));
129
146
  }, [mutate]);
@@ -320,21 +337,19 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, mode: initialMode =
320
337
  e.stopPropagation();
321
338
  }
322
339
  function handleDrop(e) {
323
- var _a;
324
340
  e.preventDefault();
325
341
  e.stopPropagation();
326
- const files = ((_a = e.dataTransfer) === null || _a === void 0 ? void 0 : _a.files) ? Array.from(e.dataTransfer.files) : [];
327
342
  const scene = prefabRootRef.current;
328
- void loadFiles(files, {
329
- onModelLoaded: (model, filename) => {
330
- const path = `models/${filename}`;
343
+ void loadDroppedAssets(e.dataTransfer, {
344
+ onModelLoaded: (model, filename, file) => {
345
+ const path = getPrefabAssetRef(filename, 'models');
331
346
  scene === null || scene === void 0 ? void 0 : scene.addModel(path, model);
332
- add(createModelNode(path, filename.replace(/\.[^.]+$/, '')));
347
+ add(createModelNode(path, file.name.replace(/\.[^.]+$/, '')));
333
348
  },
334
- onTextureLoaded: (texture, filename) => {
335
- const path = `textures/${filename}`;
349
+ onTextureLoaded: (texture, filename, file) => {
350
+ const path = getPrefabAssetRef(filename, 'textures');
336
351
  scene === null || scene === void 0 ? void 0 : scene.addTexture(path, texture);
337
- add(createImageNode(path, filename.replace(/\.[^.]+$/, '')));
352
+ add(createImageNode(path, file.name.replace(/\.[^.]+$/, '')));
338
353
  },
339
354
  onLoadError: error => {
340
355
  console.error('Drop asset error:', error);
@@ -356,8 +371,10 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, mode: initialMode =
356
371
  get: getNode,
357
372
  getObject,
358
373
  getHandle,
374
+ getModel,
359
375
  add,
360
376
  update,
377
+ replaceNode,
361
378
  remove,
362
379
  duplicate,
363
380
  move,
@@ -365,7 +382,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, mode: initialMode =
365
382
  addModel: (path, model) => { var _a; return (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.addModel(path, model); },
366
383
  addTexture: (path, texture) => { var _a; return (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.addTexture(path, texture); },
367
384
  addSound: (path, sound) => { var _a; return (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.addSound(path, sound); },
368
- }), [add, duplicate, getHandle, getNode, getObject, getRoot, mode, move, remove, replace, update]);
385
+ }), [add, duplicate, getHandle, getModel, getNode, getObject, getRoot, mode, move, remove, replace, replaceNode, update]);
369
386
  const editorRefValue = useMemo(() => (Object.assign(Object.assign({}, sceneValue), { save: getPrefab, load: loadPrefab, undo,
370
387
  redo, screenshot: handleScreenshot, exportGLB: handleExportGLB, exportGLBData: handleExportGLBData, clearSelection })), [clearSelection, getPrefab, handleExportGLB, handleExportGLBData, handleScreenshot, loadPrefab, redo, sceneValue, undo]);
371
388
  useImperativeHandle(ref, () => editorRefValue, [editorRefValue]);
@@ -15,8 +15,10 @@ export interface Scene {
15
15
  get(id: string): GameObjectType | null;
16
16
  getObject(id: string): Object3D | null;
17
17
  getHandle<T = unknown>(id: string, kind: string): T | null;
18
+ getModel(path: string): Object3D | null;
18
19
  add(node: GameObjectType, parentId?: string): GameObjectType;
19
20
  update(id: string, fn: (node: PrefabNode) => PrefabNode): void;
21
+ replaceNode(id: string, node: GameObjectType): void;
20
22
  remove(id: string): void;
21
23
  duplicate(id: string): string | null;
22
24
  move(draggedId: string, targetId: string, position: "before" | "inside"): void;
@@ -31,14 +31,16 @@ const EMPTY_TEXTURES = {};
31
31
  const EMPTY_SOUNDS = {};
32
32
  const EMPTY_NODE_COMPONENTS = {
33
33
  geometry: undefined,
34
- material: undefined,
35
- model: undefined,
34
+ materials: [],
35
+ models: [],
36
36
  sprite: undefined,
37
37
  clickEventName: null,
38
38
  composition: [],
39
39
  };
40
40
  /** Resolve a relative or absolute asset file path against a base path. */
41
41
  function resolveAssetPath(basePath, file) {
42
+ if (file.startsWith("data:"))
43
+ return file;
42
44
  if (file.startsWith("http://") || file.startsWith("https://"))
43
45
  return file;
44
46
  return file.startsWith("/") ? `${basePath}${file}` : `${basePath}/${file}`;
@@ -101,6 +103,9 @@ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSel
101
103
  const availableModels = useMemo(() => (Object.assign(Object.assign({}, models), injectedModels)), [models, injectedModels]);
102
104
  const availableTextures = useMemo(() => (Object.assign(Object.assign({}, textures), injectedTextures)), [textures, injectedTextures]);
103
105
  const availableSounds = useMemo(() => (Object.assign(Object.assign({}, sounds), injectedSounds)), [sounds, injectedSounds]);
106
+ const getModel = useCallback((path) => { var _a; return (_a = availableModels[path]) !== null && _a !== void 0 ? _a : null; }, [availableModels]);
107
+ const getTexture = useCallback((path) => { var _a; return (_a = availableTextures[path]) !== null && _a !== void 0 ? _a : null; }, [availableTextures]);
108
+ const getSound = useCallback((path) => { var _a; return (_a = availableSounds[path]) !== null && _a !== void 0 ? _a : null; }, [availableSounds]);
104
109
  const getObject = useCallback((id) => {
105
110
  var _a;
106
111
  return (_a = objectRefs.current[id]) !== null && _a !== void 0 ? _a : null;
@@ -139,12 +144,14 @@ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSel
139
144
  get: getNode,
140
145
  getObject,
141
146
  getHandle,
147
+ getModel,
142
148
  add: (node, parentId) => {
143
149
  const state = resolvedStore.getState();
144
150
  state.addChild(parentId !== null && parentId !== void 0 ? parentId : state.rootId, node);
145
151
  return node;
146
152
  },
147
153
  update: (id, fn) => resolvedStore.getState().updateNode(id, fn),
154
+ replaceNode: (id, node) => resolvedStore.getState().replaceNode(id, node),
148
155
  remove: (id) => resolvedStore.getState().deleteNode(id),
149
156
  duplicate: (id) => resolvedStore.getState().duplicateNode(id),
150
157
  move: (draggedId, targetId, position) => resolvedStore.getState().moveNode(draggedId, targetId, position),
@@ -155,7 +162,7 @@ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSel
155
162
  soundManager.setBuffer(path, sound);
156
163
  setInjectedSounds(prev => (Object.assign(Object.assign({}, prev), { [path]: sound })));
157
164
  },
158
- }), [editMode, getHandle, getNode, getObject, resolvedStore, rootId]);
165
+ }), [editMode, getHandle, getModel, getNode, getObject, resolvedStore, rootId]);
159
166
  useImperativeHandle(ref, () => sceneValue, [sceneValue]);
160
167
  const registerRef = useCallback((id, obj) => {
161
168
  objectRefs.current[id] = obj;
@@ -234,11 +241,11 @@ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSel
234
241
  registerHandle,
235
242
  getHandle,
236
243
  getObject,
237
- getModel: (path) => { var _a; return (_a = availableModels[path]) !== null && _a !== void 0 ? _a : null; },
238
- getTexture: (path) => { var _a; return (_a = availableTextures[path]) !== null && _a !== void 0 ? _a : null; },
239
- getSound: (path) => { var _a; return (_a = availableSounds[path]) !== null && _a !== void 0 ? _a : null; },
244
+ getModel,
245
+ getTexture,
246
+ getSound,
240
247
  getAssetRevision: () => `${Object.keys(availableTextures).sort().join('|')}::${Object.keys(availableModels).sort().join('|')}`,
241
- }), [registerHandle, getHandle, getObject, availableModels, availableTextures, availableSounds]);
248
+ }), [registerHandle, getHandle, getObject, getModel, getTexture, getSound, availableModels, availableTextures]);
242
249
  const handleNodeClick = useCallback((event, nodeId, fallbackObject) => {
243
250
  const node = resolvedStore.getState().nodesById[nodeId];
244
251
  if (!node)
@@ -266,8 +273,8 @@ function analyzeNodeComponents(node) {
266
273
  var _a, _b, _c, _d;
267
274
  let bufferGeometry;
268
275
  let geometry;
269
- let material;
270
- let model;
276
+ const materials = [];
277
+ const models = [];
271
278
  let sprite;
272
279
  const composition = [];
273
280
  for (const [key, component] of Object.entries((_a = node.components) !== null && _a !== void 0 ? _a : {})) {
@@ -283,10 +290,10 @@ function analyzeNodeComponents(node) {
283
290
  geometry = component;
284
291
  break;
285
292
  case "Material":
286
- material = component;
293
+ materials.push({ key, component });
287
294
  break;
288
295
  case "Model":
289
- model = component;
296
+ models.push({ key, component });
290
297
  break;
291
298
  case "Sprite":
292
299
  sprite = component;
@@ -306,10 +313,10 @@ function analyzeNodeComponents(node) {
306
313
  }
307
314
  return {
308
315
  geometry: bufferGeometry !== null && bufferGeometry !== void 0 ? bufferGeometry : geometry,
309
- material,
310
- model,
316
+ materials,
317
+ models,
311
318
  sprite,
312
- clickEventName: (_d = (_c = (_b = getClickEventName(bufferGeometry)) !== null && _b !== void 0 ? _b : getClickEventName(geometry)) !== null && _c !== void 0 ? _c : getClickEventName(model)) !== null && _d !== void 0 ? _d : getClickEventName(sprite),
319
+ clickEventName: (_d = (_c = (_b = getClickEventName(bufferGeometry)) !== null && _b !== void 0 ? _b : getClickEventName(geometry)) !== null && _c !== void 0 ? _c : models.map(({ component }) => getClickEventName(component)).find(Boolean)) !== null && _d !== void 0 ? _d : getClickEventName(sprite),
313
320
  composition,
314
321
  };
315
322
  }
@@ -361,7 +368,7 @@ function InstancedNode({ nodeId, parentMatrix = IDENTITY, editMode, registerRef,
361
368
  const analyzedComponents = useMemo(() => gameObject ? analyzeNodeComponents(gameObject) : EMPTY_NODE_COMPONENTS, [gameObject]);
362
369
  const localTransform = getNodeTransformProps(gameObject);
363
370
  const isLocked = Boolean(gameObject === null || gameObject === void 0 ? void 0 : gameObject.locked);
364
- const modelUrl = (_b = (_a = analyzedComponents.model) === null || _a === void 0 ? void 0 : _a.properties) === null || _b === void 0 ? void 0 : _b.filename;
371
+ const modelUrl = (_b = (_a = analyzedComponents.models[0]) === null || _a === void 0 ? void 0 : _a.component.properties) === null || _b === void 0 ? void 0 : _b.filename;
365
372
  const instances = useMemo(() => buildRepeatedInstances(gameObject, parentMatrix, modelUrl), [gameObject, modelUrl, parentMatrix]);
366
373
  const groupRef = useRef(null);
367
374
  const handleGroupRef = useCallback((object) => {
@@ -498,35 +505,47 @@ function getNodeTransformProps(node) {
498
505
  };
499
506
  }
500
507
  function renderNodeContent(analyzedComponents, loadedModels, primaryClickHandlers, childNodes) {
501
- var _a, _b, _c, _d;
508
+ var _a, _b, _c, _d, _e, _f;
502
509
  const geometry = analyzedComponents.geometry;
503
- const model = analyzedComponents.model;
504
- const material = analyzedComponents.material;
510
+ const models = analyzedComponents.models;
511
+ const materials = analyzedComponents.materials;
512
+ const primaryMaterial = (_a = materials[0]) === null || _a === void 0 ? void 0 : _a.component;
505
513
  const sprite = analyzedComponents.sprite;
506
- const shapeKind = (sprite === null || sprite === void 0 ? void 0 : sprite.type) ? 'sprite' : (geometry === null || geometry === void 0 ? void 0 : geometry.type) ? 'mesh' : (model === null || model === void 0 ? void 0 : model.type) ? 'model' : 'none';
514
+ const shapeKind = (sprite === null || sprite === void 0 ? void 0 : sprite.type) ? 'sprite' : (geometry === null || geometry === void 0 ? void 0 : geometry.type) ? 'mesh' : models.length > 0 ? 'model' : 'none';
507
515
  let materialContent = null;
508
516
  switch (shapeKind) {
509
517
  case 'sprite': {
510
- const materialDef = (material === null || material === void 0 ? void 0 : material.type) ? getComponentDef(material.type) : undefined;
511
- if ((material === null || material === void 0 ? void 0 : material.properties) && (materialDef === null || materialDef === void 0 ? void 0 : materialDef.View)) {
512
- const materialIsSprite = material.properties.materialType === 'sprite';
513
- materialContent = (_jsx(materialDef.View, { properties: Object.assign(Object.assign({}, material.properties), { materialType: 'sprite', transparent: materialIsSprite ? material.properties.transparent : true, depthTest: materialIsSprite ? material.properties.depthTest : false, depthWrite: materialIsSprite ? material.properties.depthWrite : false }) }, "material"));
518
+ const materialDef = (primaryMaterial === null || primaryMaterial === void 0 ? void 0 : primaryMaterial.type) ? getComponentDef(primaryMaterial.type) : undefined;
519
+ if ((primaryMaterial === null || primaryMaterial === void 0 ? void 0 : primaryMaterial.properties) && (materialDef === null || materialDef === void 0 ? void 0 : materialDef.View)) {
520
+ const materialIsSprite = primaryMaterial.properties.materialType === 'sprite';
521
+ materialContent = (_jsx(materialDef.View, { properties: Object.assign(Object.assign({}, primaryMaterial.properties), { materialType: 'sprite', attach: 'material', transparent: materialIsSprite ? primaryMaterial.properties.transparent : true, depthTest: materialIsSprite ? primaryMaterial.properties.depthTest : false, depthWrite: materialIsSprite ? primaryMaterial.properties.depthWrite : false }) }, (_c = (_b = materials[0]) === null || _b === void 0 ? void 0 : _b.key) !== null && _c !== void 0 ? _c : 'material'));
514
522
  }
515
523
  break;
516
524
  }
517
525
  case 'mesh': {
518
- const materialDef = (material === null || material === void 0 ? void 0 : material.type) ? getComponentDef(material.type) : undefined;
519
- if ((material === null || material === void 0 ? void 0 : material.properties) && (materialDef === null || materialDef === void 0 ? void 0 : materialDef.View)) {
520
- materialContent = _jsx(materialDef.View, { properties: material.properties }, "material");
521
- }
526
+ materialContent = materials.map(({ key, component }) => {
527
+ const materialDef = component.type ? getComponentDef(component.type) : undefined;
528
+ if (!component.properties || !(materialDef === null || materialDef === void 0 ? void 0 : materialDef.View))
529
+ return null;
530
+ return _jsx(materialDef.View, { properties: component.properties }, key);
531
+ });
522
532
  break;
523
533
  }
524
534
  }
525
535
  let primaryContent = null;
526
536
  let contentChildren = childNodes;
537
+ const modelContent = models.map(({ key, component }) => {
538
+ var _a;
539
+ if (!component.type || ((_a = component.properties) === null || _a === void 0 ? void 0 : _a.instanced) || !isNodeReady(component, loadedModels))
540
+ return null;
541
+ const modelDef = getComponentDef(component.type);
542
+ if (!(modelDef === null || modelDef === void 0 ? void 0 : modelDef.View))
543
+ return null;
544
+ return _jsx(modelDef.View, { properties: component.properties }, key);
545
+ });
527
546
  switch (shapeKind) {
528
547
  case 'sprite': {
529
- primaryContent = (_jsxs("sprite", Object.assign({ center: (_b = (_a = sprite === null || sprite === void 0 ? void 0 : sprite.properties) === null || _a === void 0 ? void 0 : _a.center) !== null && _b !== void 0 ? _b : [0.5, 0.5] }, primaryClickHandlers, { children: [materialContent, childNodes] })));
548
+ primaryContent = (_jsxs("sprite", Object.assign({ center: (_e = (_d = sprite === null || sprite === void 0 ? void 0 : sprite.properties) === null || _d === void 0 ? void 0 : _d.center) !== null && _e !== void 0 ? _e : [0.5, 0.5] }, primaryClickHandlers, { children: [materialContent, childNodes] })));
530
549
  contentChildren = null;
531
550
  break;
532
551
  }
@@ -535,22 +554,19 @@ function renderNodeContent(analyzedComponents, loadedModels, primaryClickHandler
535
554
  if (!(geometry === null || geometry === void 0 ? void 0 : geometry.properties) || !(geometryDef === null || geometryDef === void 0 ? void 0 : geometryDef.View))
536
555
  break;
537
556
  const GeometryView = geometryDef.View;
538
- const geometryProperties = (_c = geometry.properties) !== null && _c !== void 0 ? _c : {};
557
+ const geometryProperties = (_f = geometry.properties) !== null && _f !== void 0 ? _f : {};
539
558
  const visible = geometryProperties.visible !== false;
540
559
  primaryContent = (_jsxs("mesh", Object.assign({ visible: visible, castShadow: visible && geometryProperties.castShadow !== false, receiveShadow: visible && geometryProperties.receiveShadow !== false }, primaryClickHandlers, { children: [_jsx(GeometryView, { properties: geometry.properties }), materialContent] })));
541
560
  break;
542
561
  }
543
562
  case 'model': {
544
- if (!(model === null || model === void 0 ? void 0 : model.type) || ((_d = model.properties) === null || _d === void 0 ? void 0 : _d.instanced) || !isNodeReady(model, loadedModels))
545
- break;
546
- const modelDef = getComponentDef(model.type);
547
- if (!(modelDef === null || modelDef === void 0 ? void 0 : modelDef.View))
548
- break;
549
- const modelContent = _jsx(modelDef.View, { properties: model.properties });
550
563
  primaryContent = primaryClickHandlers ? _jsx("group", Object.assign({}, primaryClickHandlers, { children: modelContent })) : modelContent;
551
564
  break;
552
565
  }
553
566
  }
567
+ if (shapeKind !== 'model' && modelContent.some(Boolean)) {
568
+ primaryContent = _jsxs(_Fragment, { children: [primaryContent, modelContent] });
569
+ }
554
570
  let content = _jsxs(_Fragment, { children: [primaryContent, contentChildren] });
555
571
  for (const { key, View, properties } of analyzedComponents.composition) {
556
572
  content = (_jsx(View, { properties: properties, children: content }, key));
@@ -15,6 +15,18 @@ const DEFAULT_TRIANGLE_UVS = [
15
15
  function isFiniteNumberArray(value) {
16
16
  return Array.isArray(value) && value.every(entry => typeof entry === 'number' && Number.isFinite(entry));
17
17
  }
18
+ function isGeometryGroupArray(value) {
19
+ return Array.isArray(value) && value.every(group => {
20
+ if (!group || typeof group !== 'object' || Array.isArray(group))
21
+ return false;
22
+ const entry = group;
23
+ return typeof entry.start === 'number'
24
+ && Number.isFinite(entry.start)
25
+ && typeof entry.count === 'number'
26
+ && Number.isFinite(entry.count)
27
+ && (entry.materialIndex === undefined || typeof entry.materialIndex === 'number');
28
+ });
29
+ }
18
30
  function normalizeNumberArray(value, fallback) {
19
31
  return isFiniteNumberArray(value) ? value : fallback;
20
32
  }
@@ -62,7 +74,13 @@ function BufferGeometryComponentView({ properties }) {
62
74
  const indexArray = getIndexArray(indices);
63
75
  const hasNormals = normals.length >= 3 && normals.length % 3 === 0;
64
76
  const hasUvs = uvs.length >= 2 && uvs.length % 2 === 0;
77
+ const groups = isGeometryGroupArray(properties.groups) ? properties.groups : [];
65
78
  return (_jsxs("bufferGeometry", { onUpdate: (geometry) => {
79
+ geometry.clearGroups();
80
+ groups.forEach(group => {
81
+ var _a;
82
+ geometry.addGroup(group.start, group.count, (_a = group.materialIndex) !== null && _a !== void 0 ? _a : 0);
83
+ });
66
84
  if (properties.computeVertexNormals !== false && !hasNormals) {
67
85
  geometry.computeVertexNormals();
68
86
  }
@@ -72,6 +90,7 @@ function BufferGeometryComponentView({ properties }) {
72
90
  }
73
91
  const BufferGeometryComponent = {
74
92
  name: 'BufferGeometry',
93
+ disableSiblingComposition: 'geometry',
75
94
  Editor: BufferGeometryComponentEditor,
76
95
  View: BufferGeometryComponentView,
77
96
  defaultProperties: {
@@ -79,6 +98,7 @@ const BufferGeometryComponent = {
79
98
  indices: DEFAULT_TRIANGLE_INDICES,
80
99
  normals: [],
81
100
  uvs: DEFAULT_TRIANGLE_UVS,
101
+ groups: [],
82
102
  computeVertexNormals: true,
83
103
  emitClickEvent: false,
84
104
  clickEventName: '',
@@ -21,6 +21,8 @@ export interface ComponentViewProps<P = Record<string, unknown>> {
21
21
  }
22
22
  export interface Component {
23
23
  name: string;
24
+ /** Set when this component occupies a single slot on a node. Use a string to share a slot across component types. */
25
+ disableSiblingComposition?: boolean | string;
24
26
  Editor: FC<{
25
27
  node?: GameObject;
26
28
  component: ComponentData;
@@ -35,4 +37,7 @@ export interface Component {
35
37
  export declare function registerComponent(component: Component): void;
36
38
  export declare function getComponentDef(name: string): Component | undefined;
37
39
  export declare function getAllComponentDefs(): Record<string, Component>;
40
+ export declare function getSiblingCompositionSlot(componentName: string, disableSiblingComposition: boolean | string | undefined): string | null;
41
+ export declare function canAddComponentToNode(node: GameObject, component: Component | undefined, allComponents?: Record<string, Component>): boolean;
42
+ export declare function getNextComponentKey(node: GameObject, componentName: string): string;
38
43
  export declare function getComponentAssetRefs(componentType: string, properties: Record<string, unknown>): AssetRef[];
@@ -14,6 +14,37 @@ export function getComponentDef(name) {
14
14
  export function getAllComponentDefs() {
15
15
  return Object.assign({}, REGISTRY);
16
16
  }
17
+ export function getSiblingCompositionSlot(componentName, disableSiblingComposition) {
18
+ if (!disableSiblingComposition)
19
+ return null;
20
+ return typeof disableSiblingComposition === "string" ? disableSiblingComposition : componentName;
21
+ }
22
+ export function canAddComponentToNode(node, component, allComponents = REGISTRY) {
23
+ var _a;
24
+ if (!component)
25
+ return false;
26
+ const slot = getSiblingCompositionSlot(component.name, component.disableSiblingComposition);
27
+ if (!slot)
28
+ return true;
29
+ return !Object.values((_a = node.components) !== null && _a !== void 0 ? _a : {}).some(entry => {
30
+ if (!(entry === null || entry === void 0 ? void 0 : entry.type))
31
+ return false;
32
+ const sibling = allComponents[entry.type];
33
+ return getSiblingCompositionSlot(entry.type, sibling === null || sibling === void 0 ? void 0 : sibling.disableSiblingComposition) === slot;
34
+ });
35
+ }
36
+ export function getNextComponentKey(node, componentName) {
37
+ var _a;
38
+ const baseKey = componentName.toLowerCase();
39
+ const existingKeys = new Set(Object.keys((_a = node.components) !== null && _a !== void 0 ? _a : {}));
40
+ let nextKey = baseKey;
41
+ let index = 1;
42
+ while (existingKeys.has(nextKey)) {
43
+ nextKey = `${baseKey}_${index}`;
44
+ index += 1;
45
+ }
46
+ return nextKey;
47
+ }
17
48
  export function getComponentAssetRefs(componentType, properties) {
18
49
  var _a, _b;
19
50
  const component = REGISTRY[componentType];
@@ -68,6 +68,7 @@ function DataComponentEditor({ component, onUpdate }) {
68
68
  }
69
69
  const DataComponent = {
70
70
  name: 'Data',
71
+ disableSiblingComposition: true,
71
72
  Editor: DataComponentEditor,
72
73
  defaultProperties: {
73
74
  data: {},
@@ -10,6 +10,7 @@ function EnvironmentView({ properties, children, }) {
10
10
  }
11
11
  const EnvironmentComponent = {
12
12
  name: 'Environment',
13
+ disableSiblingComposition: true,
13
14
  Editor: ({ component, onUpdate }) => (_jsxs(FieldGroup, { children: [_jsx(NumberField, { name: "intensity", label: "Intensity", values: component.properties, onChange: onUpdate, min: 0, step: 0.1, fallback: 1 }), _jsx(NumberField, { name: "resolution", label: "Resolution", values: component.properties, onChange: onUpdate, min: 64, step: 64, fallback: 256 })] })),
14
15
  View: EnvironmentView,
15
16
  defaultProperties: {},
@@ -82,6 +82,7 @@ function GeometryComponentView({ properties, children }) {
82
82
  }
83
83
  const GeometryComponent = {
84
84
  name: 'Geometry',
85
+ disableSiblingComposition: 'geometry',
85
86
  Editor: GeometryComponentEditor,
86
87
  View: GeometryComponentView,
87
88
  defaultProperties: {
@@ -11,6 +11,7 @@ declare module '@react-three/fiber' {
11
11
  }
12
12
  }
13
13
  export interface MaterialProps extends Omit<MeshStandardMaterialProperties & MeshBasicMaterialProperties, 'args' | 'normalScale' | 'side'> {
14
+ attach?: string;
14
15
  materialType?: 'standard' | 'basic' | 'sprite';
15
16
  transmission?: number;
16
17
  thickness?: number;