@fps-games/editor 0.1.7-beta.4 → 0.1.8

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 (112) hide show
  1. package/dist/agent-bridge/capabilities/assets.d.ts.map +1 -1
  2. package/dist/agent-bridge/capabilities/assets.js +12 -7
  3. package/dist/agent-bridge/capabilities/assets.js.map +1 -1
  4. package/dist/agent-bridge/capabilities/host-lifecycle.d.ts +1 -1
  5. package/dist/agent-bridge/capabilities/host-lifecycle.d.ts.map +1 -1
  6. package/dist/agent-bridge/capabilities/host-lifecycle.js +4 -2
  7. package/dist/agent-bridge/capabilities/host-lifecycle.js.map +1 -1
  8. package/dist/agent-bridge/capabilities/prefabs.js +50 -0
  9. package/dist/agent-bridge/capabilities/prefabs.js.map +1 -1
  10. package/dist/agent-bridge/capability-sources.d.ts.map +1 -1
  11. package/dist/agent-bridge/capability-sources.js +4 -1
  12. package/dist/agent-bridge/capability-sources.js.map +1 -1
  13. package/dist/build-info.json +15 -15
  14. package/dist/index.d.ts +5 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +7 -0
  17. package/dist/index.js.map +1 -1
  18. package/dist/local-editor-harness.d.ts +4 -0
  19. package/dist/local-editor-harness.d.ts.map +1 -1
  20. package/dist/local-editor-harness.js +145 -53
  21. package/dist/local-editor-harness.js.map +1 -1
  22. package/dist/playable-local-editor-host.d.ts +5 -1
  23. package/dist/playable-local-editor-host.d.ts.map +1 -1
  24. package/dist/playable-local-editor-host.js +74 -1
  25. package/dist/playable-local-editor-host.js.map +1 -1
  26. package/node_modules/@fps-games/babylon-renderer/package.json +1 -1
  27. package/node_modules/@fps-games/editor-babylon/dist/inspector-host.d.ts +2 -0
  28. package/node_modules/@fps-games/editor-babylon/dist/inspector-host.d.ts.map +1 -1
  29. package/node_modules/@fps-games/editor-babylon/dist/inspector-host.js +1 -0
  30. package/node_modules/@fps-games/editor-babylon/dist/inspector-host.js.map +1 -1
  31. package/node_modules/@fps-games/editor-babylon/dist/material-property-adapter.d.ts +6 -2
  32. package/node_modules/@fps-games/editor-babylon/dist/material-property-adapter.d.ts.map +1 -1
  33. package/node_modules/@fps-games/editor-babylon/dist/material-property-adapter.js +94 -24
  34. package/node_modules/@fps-games/editor-babylon/dist/material-property-adapter.js.map +1 -1
  35. package/node_modules/@fps-games/editor-babylon/dist/material-slot-source-resolver.d.ts +6 -2
  36. package/node_modules/@fps-games/editor-babylon/dist/material-slot-source-resolver.d.ts.map +1 -1
  37. package/node_modules/@fps-games/editor-babylon/dist/material-slot-source-resolver.js +7 -0
  38. package/node_modules/@fps-games/editor-babylon/dist/material-slot-source-resolver.js.map +1 -1
  39. package/node_modules/@fps-games/editor-babylon/dist/monitor.d.ts +2 -0
  40. package/node_modules/@fps-games/editor-babylon/dist/monitor.d.ts.map +1 -1
  41. package/node_modules/@fps-games/editor-babylon/dist/monitor.js +2 -0
  42. package/node_modules/@fps-games/editor-babylon/dist/monitor.js.map +1 -1
  43. package/node_modules/@fps-games/editor-babylon/dist/projection.d.ts +19 -0
  44. package/node_modules/@fps-games/editor-babylon/dist/projection.d.ts.map +1 -1
  45. package/node_modules/@fps-games/editor-babylon/dist/projection.js +711 -35
  46. package/node_modules/@fps-games/editor-babylon/dist/projection.js.map +1 -1
  47. package/node_modules/@fps-games/editor-babylon/package.json +5 -5
  48. package/node_modules/@fps-games/editor-browser/dist/index.d.ts +2 -0
  49. package/node_modules/@fps-games/editor-browser/dist/index.d.ts.map +1 -1
  50. package/node_modules/@fps-games/editor-browser/dist/index.js +1 -0
  51. package/node_modules/@fps-games/editor-browser/dist/index.js.map +1 -1
  52. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-modal-dialog.d.ts +26 -0
  53. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-modal-dialog.d.ts.map +1 -0
  54. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-modal-dialog.js +187 -0
  55. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-modal-dialog.js.map +1 -0
  56. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.d.ts.map +1 -1
  57. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.js +102 -2
  58. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.js.map +1 -1
  59. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts +3 -0
  60. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts.map +1 -1
  61. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts.map +1 -1
  62. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js +15 -0
  63. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js.map +1 -1
  64. package/node_modules/@fps-games/editor-browser/package.json +3 -3
  65. package/node_modules/@fps-games/editor-core/package.json +2 -2
  66. package/node_modules/@fps-games/editor-forge-play/package.json +2 -2
  67. package/node_modules/@fps-games/editor-playable-sdk/dist/asset-database/metadata.js +198 -53
  68. package/node_modules/@fps-games/editor-playable-sdk/dist/asset-database/metadata.js.map +1 -1
  69. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/forge-play-types.d.ts +4 -1
  70. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/forge-play-types.d.ts.map +1 -1
  71. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/forge-play.d.ts.map +1 -1
  72. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/forge-play.js +89 -9
  73. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/forge-play.js.map +1 -1
  74. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/host-controller.d.ts +3 -1
  75. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/host-controller.d.ts.map +1 -1
  76. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/host-controller.js +59 -5
  77. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/host-controller.js.map +1 -1
  78. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/index.d.ts +1 -1
  79. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/index.d.ts.map +1 -1
  80. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/index.js.map +1 -1
  81. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/lifecycle.d.ts.map +1 -1
  82. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/lifecycle.js +59 -1
  83. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/lifecycle.js.map +1 -1
  84. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/types.d.ts +25 -0
  85. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/types.d.ts.map +1 -1
  86. package/node_modules/@fps-games/editor-playable-sdk/dist/material-binding/binding.d.ts.map +1 -1
  87. package/node_modules/@fps-games/editor-playable-sdk/dist/material-binding/binding.js +29 -0
  88. package/node_modules/@fps-games/editor-playable-sdk/dist/material-binding/binding.js.map +1 -1
  89. package/node_modules/@fps-games/editor-playable-sdk/dist/runtime-preview/nodes.d.ts.map +1 -1
  90. package/node_modules/@fps-games/editor-playable-sdk/dist/runtime-preview/nodes.js +34 -1
  91. package/node_modules/@fps-games/editor-playable-sdk/dist/runtime-preview/nodes.js.map +1 -1
  92. package/node_modules/@fps-games/editor-playable-sdk/dist/scene-compiler/compiler.js +6 -2
  93. package/node_modules/@fps-games/editor-playable-sdk/dist/scene-compiler/compiler.js.map +1 -1
  94. package/node_modules/@fps-games/editor-playable-sdk/dist/scene-source/driver.d.ts.map +1 -1
  95. package/node_modules/@fps-games/editor-playable-sdk/dist/scene-source/driver.js +57 -3
  96. package/node_modules/@fps-games/editor-playable-sdk/dist/scene-source/driver.js.map +1 -1
  97. package/node_modules/@fps-games/editor-playable-sdk/dist/scene-source/types.d.ts +30 -0
  98. package/node_modules/@fps-games/editor-playable-sdk/dist/scene-source/types.d.ts.map +1 -1
  99. package/node_modules/@fps-games/editor-playable-sdk/dist/session/patches.d.ts +1 -0
  100. package/node_modules/@fps-games/editor-playable-sdk/dist/session/patches.d.ts.map +1 -1
  101. package/node_modules/@fps-games/editor-playable-sdk/dist/session/patches.js +1 -0
  102. package/node_modules/@fps-games/editor-playable-sdk/dist/session/patches.js.map +1 -1
  103. package/node_modules/@fps-games/editor-playable-sdk/package.json +3 -3
  104. package/node_modules/@fps-games/editor-protocol/dist/agent-bridge.d.ts +13 -0
  105. package/node_modules/@fps-games/editor-protocol/dist/agent-bridge.d.ts.map +1 -1
  106. package/node_modules/@fps-games/editor-protocol/dist/agent-bridge.js +29 -0
  107. package/node_modules/@fps-games/editor-protocol/dist/agent-bridge.js.map +1 -1
  108. package/node_modules/@fps-games/editor-protocol/dist/index.d.ts +6 -1
  109. package/node_modules/@fps-games/editor-protocol/dist/index.d.ts.map +1 -1
  110. package/node_modules/@fps-games/editor-protocol/dist/index.js.map +1 -1
  111. package/node_modules/@fps-games/editor-protocol/package.json +1 -1
  112. package/package.json +8 -8
@@ -6,6 +6,7 @@ import { normalizeProjectionOwnerNodePath, readProjectionSourceNodeSegment, } fr
6
6
  import { createMaterialSlotOwnerPathMatchKey, isMaterialSlotOwnerPathMatch, } from './material-slot-source-resolver.js';
7
7
  const LARGE_SELECTION_EDGE_LIMIT = 200;
8
8
  const PROJECTION_LOCAL_MATRIX_METADATA_KEY = 'editorLocalTransformMatrix';
9
+ const MIN_SELECTION_BOUNDS_PROXY_SIZE = 0.002;
9
10
  const ASSET_MESH_SELECTION_SEPARATOR = '::assetMesh::';
10
11
  function requireBabylonCtor(value, name) {
11
12
  if (!value)
@@ -209,6 +210,7 @@ export function createBabylonEditorProjection(options) {
209
210
  nodeId: node.id,
210
211
  root,
211
212
  outlineMeshes: [],
213
+ selectionMeshes: [],
212
214
  animationGroups: [],
213
215
  runtimeObjects: [],
214
216
  };
@@ -626,7 +628,7 @@ function getProjectionSelectionBounds(projections, nodeIds) {
626
628
  let min = null;
627
629
  let max = null;
628
630
  for (const nodeId of nodeIds) {
629
- const bounds = readProjectionBounds(projections.get(nodeId)?.root);
631
+ const bounds = readProjectionBounds(resolveProjectionSelectionBoundsTarget(projections, nodeId));
630
632
  if (!bounds)
631
633
  continue;
632
634
  min = min
@@ -662,6 +664,32 @@ function getProjectionSelectionBounds(projections, nodeIds) {
662
664
  },
663
665
  };
664
666
  }
667
+ function resolveProjectionSelectionBoundsTarget(projections, nodeId) {
668
+ const projection = projections.get(nodeId);
669
+ if (projection?.root)
670
+ return projection.root;
671
+ for (const candidateProjection of projections.values()) {
672
+ const selectionTarget = findProjectionSelectionMeshByNodeId(candidateProjection, nodeId);
673
+ if (selectionTarget)
674
+ return selectionTarget;
675
+ }
676
+ return null;
677
+ }
678
+ function findProjectionSelectionMeshByNodeId(projection, nodeId) {
679
+ for (const mesh of projection.selectionMeshes ?? []) {
680
+ if (readProjectionMeshSelectionNodeId(mesh) === nodeId)
681
+ return mesh;
682
+ }
683
+ for (const mesh of projection.outlineMeshes) {
684
+ if (readProjectionMeshSelectionNodeId(mesh) === nodeId)
685
+ return mesh;
686
+ }
687
+ return null;
688
+ }
689
+ function readProjectionMeshSelectionNodeId(mesh) {
690
+ const nodeId = mesh?.metadata?.editorProjection?.nodeId;
691
+ return typeof nodeId === 'string' && nodeId.length > 0 ? nodeId : null;
692
+ }
665
693
  function readProjectionBounds(root) {
666
694
  if (!root)
667
695
  return null;
@@ -810,20 +838,59 @@ function attachImportedProjectionResult(options, node, asset, projection, result
810
838
  },
811
839
  };
812
840
  }
841
+ const materialSlots = readProjectionAssetMaterialSlots(asset, node.id);
842
+ const ownerNodePathByMesh = new Map();
843
+ const ownerMeshCountByKey = new Map();
844
+ for (const mesh of result.meshes ?? []) {
845
+ const ownerNodePath = buildProjectionOwnerNodePath(mesh, modelRoot, node.id);
846
+ if (!ownerNodePath)
847
+ continue;
848
+ ownerNodePathByMesh.set(mesh, ownerNodePath);
849
+ const ownerKey = createMaterialSlotOwnerPathMatchKey(ownerNodePath, { projectionNodeId: node.id });
850
+ ownerMeshCountByKey.set(ownerKey, (ownerMeshCountByKey.get(ownerKey) ?? 0) + 1);
851
+ }
852
+ const splitPrimitiveOrdinalByOwnerKey = new Map();
853
+ const primitiveBoundsProxyOwnerKeys = new Set();
813
854
  for (const mesh of result.meshes ?? []) {
814
855
  mesh.isPickable = true;
815
856
  applyProjectionRenderingToMesh(node, mesh);
816
- const ownerNodePath = buildProjectionOwnerNodePath(mesh, modelRoot, node.id);
857
+ const ownerNodePath = ownerNodePathByMesh.get(mesh) ?? null;
817
858
  if (ownerNodePath) {
859
+ const ownerKey = createMaterialSlotOwnerPathMatchKey(ownerNodePath, { projectionNodeId: node.id });
860
+ const ownerMeshCount = ownerMeshCountByKey.get(ownerKey) ?? 0;
861
+ const splitPrimitiveSlot = resolveProjectionSplitPrimitiveMeshSelectionSlot(mesh, materialSlots, ownerNodePath, node.id, ownerMeshCount, splitPrimitiveOrdinalByOwnerKey);
862
+ const primitiveSelectionMeshes = createProjectionAssetMeshSelectionProxies(options, node, projection, mesh, ownerNodePath, materialSlots);
863
+ const primitiveBoundsSelectionMeshes = !splitPrimitiveSlot
864
+ && primitiveSelectionMeshes.length === 0
865
+ && !primitiveBoundsProxyOwnerKeys.has(ownerKey)
866
+ ? createProjectionPrimitiveBoundsSelectionProxies(options, node, projection, mesh, ownerNodePath, materialSlots, ownerMeshCount)
867
+ : [];
868
+ if (primitiveBoundsSelectionMeshes.length > 0)
869
+ primitiveBoundsProxyOwnerKeys.add(ownerKey);
870
+ const usesSelectionMeshes = primitiveSelectionMeshes.length > 0
871
+ || primitiveBoundsSelectionMeshes.length > 0
872
+ || primitiveBoundsProxyOwnerKeys.has(ownerKey);
818
873
  const selectionKey = resolveProjectionAssetMeshSelectionKey(asset, ownerNodePath, node.id);
819
874
  mesh.metadata = {
820
875
  ...(mesh.metadata ?? {}),
821
876
  editorProjection: {
822
- nodeId: createAssetMeshProjectionNodeId(node.id, selectionKey),
877
+ nodeId: splitPrimitiveSlot
878
+ ? createAssetMeshProjectionNodeId(node.id, splitPrimitiveSlot.key)
879
+ : usesSelectionMeshes
880
+ ? node.id
881
+ : createAssetMeshProjectionNodeId(node.id, selectionKey),
823
882
  rootNodeId: node.id,
824
883
  sourceId: asset.sourceId,
825
884
  ownerNodePath,
826
- ...(selectionKey !== ownerNodePath ? { slotSelectionKey: selectionKey } : {}),
885
+ ...(splitPrimitiveSlot
886
+ ? {
887
+ slotSelectionKey: splitPrimitiveSlot.key,
888
+ primitiveIndex: splitPrimitiveSlot.primitiveIndex,
889
+ }
890
+ : {}),
891
+ ...(!splitPrimitiveSlot && !usesSelectionMeshes && selectionKey !== ownerNodePath
892
+ ? { slotSelectionKey: selectionKey }
893
+ : {}),
827
894
  runtimeKind: 'assetMesh',
828
895
  ...createProjectionShadowMetadata(node),
829
896
  },
@@ -1230,30 +1297,104 @@ function applyProjectionArtistMaterialProfilesToMeshes(options, node, modelRoot,
1230
1297
  if (hasRootProfile) {
1231
1298
  for (const mesh of meshes) {
1232
1299
  const material = mesh?.material;
1233
- if (material)
1234
- materials.add(material);
1235
- }
1236
- for (const material of materials) {
1300
+ const meshOwnerNodePath = buildProjectionOwnerNodePath(mesh, modelRoot, node.id);
1301
+ if (!material) {
1302
+ emitProjectionMaterialProfileDiagnostic(options, {
1303
+ projectionNodeId: node.id,
1304
+ source: 'rootProfile',
1305
+ applied: false,
1306
+ matchKind: 'root',
1307
+ targetMaterialStatus: 'missingMaterial',
1308
+ meshOwnerNodePath,
1309
+ warningMessage: 'Root material profile was not applied because an imported mesh has no target material.',
1310
+ });
1311
+ continue;
1312
+ }
1313
+ if (materials.has(material))
1314
+ continue;
1315
+ materials.add(material);
1237
1316
  applyProjectionArtistMaterialProfile(options, material, node.artistMaterialProfile);
1317
+ emitProjectionMaterialProfileDiagnostic(options, {
1318
+ projectionNodeId: node.id,
1319
+ source: 'rootProfile',
1320
+ applied: true,
1321
+ matchKind: 'root',
1322
+ targetMaterialStatus: 'material',
1323
+ meshOwnerNodePath,
1324
+ targetMaterialName: readProjectionMaterialName(material),
1325
+ targetMaterialType: readProjectionMaterialType(material),
1326
+ });
1327
+ }
1328
+ if (materials.size === 0 && meshes.length === 0) {
1329
+ emitProjectionMaterialProfileDiagnostic(options, {
1330
+ projectionNodeId: node.id,
1331
+ source: 'rootProfile',
1332
+ applied: false,
1333
+ matchKind: 'root',
1334
+ targetMaterialStatus: 'missingMaterial',
1335
+ meshOwnerNodePath: null,
1336
+ warningMessage: 'Root material profile was not applied because the imported model produced no meshes.',
1337
+ });
1238
1338
  }
1239
1339
  }
1240
1340
  if (!hasSlotProfiles)
1241
1341
  return clonedMaterials;
1242
1342
  const materialCounts = countProjectionMeshMaterials(meshes);
1343
+ const assetMaterialSlots = node.asset ? readProjectionAssetMaterialSlots(node.asset, node.id) : [];
1344
+ const unmatchedSlotBindings = new Set(slotBindings);
1243
1345
  for (const mesh of meshes) {
1244
- const material = mesh?.material;
1245
- if (!material)
1246
- continue;
1247
1346
  const ownerNodePath = buildProjectionOwnerNodePath(mesh, modelRoot, node.id);
1248
- const slotBinding = resolveProjectionSlotBinding(slotBindings, ownerNodePath, node.id);
1249
- if (!slotBinding)
1250
- continue;
1251
- const slotMaterial = detachProjectionSlotMaterial(mesh, materialCounts, ownerNodePath);
1252
- if (!slotMaterial)
1253
- continue;
1254
- if (slotMaterial !== material)
1255
- clonedMaterials.push(slotMaterial);
1256
- applyProjectionArtistMaterialProfile(options, slotMaterial, slotBinding.profile);
1347
+ const meshSlotBindingMatches = resolveProjectionSlotBindingMatches(slotBindings, ownerNodePath, node.id);
1348
+ for (const { binding: slotBinding, matchKind } of meshSlotBindingMatches) {
1349
+ unmatchedSlotBindings.delete(slotBinding);
1350
+ if (!mesh?.material) {
1351
+ emitProjectionSlotMaterialProfileDiagnostic(options, node.id, slotBinding, {
1352
+ applied: false,
1353
+ matchKind,
1354
+ meshOwnerNodePath: ownerNodePath,
1355
+ targetMaterialStatus: 'missingMaterial',
1356
+ warningMessage: 'Slot material profile was not applied because the matched mesh has no target material.',
1357
+ });
1358
+ continue;
1359
+ }
1360
+ const resolution = resolveProjectionSlotMaterialTarget(mesh, materialCounts, slotBinding.slot, ownerNodePath, {
1361
+ projectionNodeId: node.id,
1362
+ primitiveSingleMaterialFallbackAllowed: isProjectionSinglePrimitiveMaterialSlot(slotBinding.slot, assetMaterialSlots),
1363
+ });
1364
+ const targetMaterial = resolution.target?.material ?? mesh.material;
1365
+ if (!resolution.target) {
1366
+ emitProjectionSlotMaterialProfileDiagnostic(options, node.id, slotBinding, {
1367
+ applied: false,
1368
+ matchKind,
1369
+ meshOwnerNodePath: ownerNodePath,
1370
+ targetMaterialStatus: resolution.targetMaterialStatus,
1371
+ targetMaterialName: readProjectionMaterialName(targetMaterial),
1372
+ targetMaterialType: readProjectionMaterialType(targetMaterial),
1373
+ warningMessage: resolution.warningMessage,
1374
+ });
1375
+ continue;
1376
+ }
1377
+ const target = resolution.target;
1378
+ clonedMaterials.push(...target.clonedMaterials);
1379
+ applyProjectionArtistMaterialProfile(options, target.material, slotBinding.profile);
1380
+ emitProjectionSlotMaterialProfileDiagnostic(options, node.id, slotBinding, {
1381
+ applied: true,
1382
+ matchKind,
1383
+ meshOwnerNodePath: ownerNodePath,
1384
+ targetMaterialStatus: resolution.targetMaterialStatus,
1385
+ targetMaterialName: readProjectionMaterialName(target.material),
1386
+ targetMaterialType: readProjectionMaterialType(target.material),
1387
+ });
1388
+ }
1389
+ }
1390
+ for (const slotBinding of unmatchedSlotBindings) {
1391
+ emitProjectionSlotMaterialProfileDiagnostic(options, node.id, slotBinding, {
1392
+ applied: false,
1393
+ matchKind: 'none',
1394
+ meshOwnerNodePath: null,
1395
+ targetMaterialStatus: 'missingMeshOwner',
1396
+ warningMessage: 'Slot material profile was not applied because no imported mesh matched its owner path.',
1397
+ });
1257
1398
  }
1258
1399
  return clonedMaterials;
1259
1400
  }
@@ -1265,6 +1406,40 @@ function applyProjectionArtistMaterialProfile(options, material, profile) {
1265
1406
  resolveTextureUrl: options.resolveTextureUrl,
1266
1407
  });
1267
1408
  }
1409
+ function emitProjectionSlotMaterialProfileDiagnostic(options, projectionNodeId, slotBinding, diagnostic) {
1410
+ emitProjectionMaterialProfileDiagnostic(options, {
1411
+ projectionNodeId,
1412
+ source: slotBinding.source,
1413
+ slotId: slotBinding.slot.slotId,
1414
+ ownerNodePath: slotBinding.slot.ownerNodePath,
1415
+ primitiveIndex: slotBinding.slot.primitiveIndex,
1416
+ ...diagnostic,
1417
+ });
1418
+ }
1419
+ function emitProjectionMaterialProfileDiagnostic(options, diagnostic) {
1420
+ options.onMaterialProfileDiagnostic?.(diagnostic);
1421
+ if (!diagnostic.warningMessage)
1422
+ return;
1423
+ options.logger?.warn?.('[BabylonEditorProjection] Material profile diagnostic warning', {
1424
+ projectionNodeId: diagnostic.projectionNodeId,
1425
+ source: diagnostic.source,
1426
+ slotId: diagnostic.slotId,
1427
+ ownerNodePath: diagnostic.ownerNodePath,
1428
+ meshOwnerNodePath: diagnostic.meshOwnerNodePath,
1429
+ matchKind: diagnostic.matchKind,
1430
+ targetMaterialStatus: diagnostic.targetMaterialStatus,
1431
+ warningMessage: diagnostic.warningMessage,
1432
+ });
1433
+ }
1434
+ function readProjectionMaterialName(material) {
1435
+ const name = material?.name;
1436
+ return typeof name === 'string' && name.trim() ? name.trim() : undefined;
1437
+ }
1438
+ function readProjectionMaterialType(material) {
1439
+ const getClassName = material?.getClassName;
1440
+ const className = typeof getClassName === 'function' ? getClassName.call(material) : null;
1441
+ return typeof className === 'string' && className.trim() ? className.trim() : className;
1442
+ }
1268
1443
  function createProjectionMaterialSlotBindings(node) {
1269
1444
  const bindings = [];
1270
1445
  const occupiedOwnerPaths = new Set();
@@ -1273,14 +1448,14 @@ function createProjectionMaterialSlotBindings(node) {
1273
1448
  const slot = createProjectionMaterialSlotDescriptor(binding?.slot, node.id);
1274
1449
  if (!profile || !slot)
1275
1450
  continue;
1276
- bindings.push({ slot, profile });
1451
+ bindings.push({ slot, profile, source: 'slotBinding' });
1277
1452
  occupiedOwnerPaths.add(slot.matchKey);
1278
1453
  }
1279
1454
  for (const [ownerNodePath, profile] of Object.entries(node.artistMaterialSlotProfiles ?? {})) {
1280
1455
  const slot = createProjectionMaterialSlotDescriptor({ ownerNodePath }, node.id);
1281
1456
  if (!profile || !slot || occupiedOwnerPaths.has(slot.matchKey))
1282
1457
  continue;
1283
- bindings.push({ slot, profile });
1458
+ bindings.push({ slot, profile, source: 'legacySlotProfile' });
1284
1459
  occupiedOwnerPaths.add(slot.matchKey);
1285
1460
  }
1286
1461
  return bindings;
@@ -1309,6 +1484,8 @@ function createProjectionMaterialSlotDescriptor(value, projectionNodeId) {
1309
1484
  const materialNames = Array.isArray(value.materialNames)
1310
1485
  ? value.materialNames.filter((item) => typeof item === 'string' && !!item.trim()).map(item => item.trim())
1311
1486
  : undefined;
1487
+ const triangleCount = Number.isFinite(value.triangleCount) ? value.triangleCount : undefined;
1488
+ const boundingBox = readProjectionMaterialSlotBoundingBox(value.boundingBox);
1312
1489
  return {
1313
1490
  ...(slotId ? { slotId } : {}),
1314
1491
  ownerNodePath,
@@ -1323,27 +1500,58 @@ function createProjectionMaterialSlotDescriptor(value, projectionNodeId) {
1323
1500
  : {}),
1324
1501
  ...(materialName ? { materialName } : {}),
1325
1502
  ...(materialNames && materialNames.length > 0 ? { materialNames } : {}),
1503
+ ...(triangleCount != null ? { triangleCount } : {}),
1504
+ ...(boundingBox ? { boundingBox } : {}),
1326
1505
  key: slotId || ownerNodePath,
1327
1506
  normalizedOwnerNodePath: ownerNodePath,
1328
1507
  matchKey: createMaterialSlotOwnerPathMatchKey(ownerNodePath, { projectionNodeId }),
1329
1508
  };
1330
1509
  }
1331
- function resolveProjectionSlotBinding(slotBindings, ownerNodePath, projectionNodeId) {
1510
+ function readProjectionMaterialSlotBoundingBox(value) {
1511
+ if (!value || typeof value !== 'object' || Array.isArray(value))
1512
+ return undefined;
1513
+ const record = value;
1514
+ const min = readProjectionMaterialSlotBoundsTuple(record.min);
1515
+ const max = readProjectionMaterialSlotBoundsTuple(record.max);
1516
+ if (!min && !max)
1517
+ return undefined;
1518
+ return {
1519
+ ...(min ? { min } : {}),
1520
+ ...(max ? { max } : {}),
1521
+ };
1522
+ }
1523
+ function readProjectionMaterialSlotBoundsTuple(value) {
1524
+ if (!Array.isArray(value) || value.length < 3)
1525
+ return undefined;
1526
+ const tuple = value.slice(0, 3).map(item => Number(item));
1527
+ if (!tuple.every(item => Number.isFinite(item)))
1528
+ return undefined;
1529
+ return tuple;
1530
+ }
1531
+ function resolveProjectionSlotBindingMatches(slotBindings, ownerNodePath, projectionNodeId) {
1332
1532
  if (ownerNodePath == null)
1333
- return null;
1533
+ return [];
1334
1534
  const normalized = normalizeProjectionOwnerNodePath(ownerNodePath, { projectionNodeId });
1335
1535
  const normalizedMatchKey = createMaterialSlotOwnerPathMatchKey(normalized, { projectionNodeId });
1336
- let descendantMatch = null;
1536
+ const exactMatches = [];
1537
+ const descendantMatches = [];
1538
+ let descendantMatchLength = -1;
1337
1539
  for (const binding of slotBindings) {
1338
- if (binding.slot.matchKey === normalizedMatchKey)
1339
- return binding;
1540
+ if (binding.slot.matchKey === normalizedMatchKey) {
1541
+ exactMatches.push({ binding, matchKind: 'exact' });
1542
+ continue;
1543
+ }
1340
1544
  if (isMaterialSlotOwnerPathMatch(normalized, binding.slot.normalizedOwnerNodePath, { projectionNodeId })) {
1341
- if (!descendantMatch || binding.slot.matchKey.length > descendantMatch.slot.matchKey.length) {
1342
- descendantMatch = binding;
1545
+ if (binding.slot.matchKey.length > descendantMatchLength) {
1546
+ descendantMatches.length = 0;
1547
+ descendantMatchLength = binding.slot.matchKey.length;
1548
+ }
1549
+ if (binding.slot.matchKey.length === descendantMatchLength) {
1550
+ descendantMatches.push({ binding, matchKind: 'descendant' });
1343
1551
  }
1344
1552
  }
1345
1553
  }
1346
- return descendantMatch;
1554
+ return exactMatches.length > 0 ? exactMatches : descendantMatches;
1347
1555
  }
1348
1556
  function buildProjectionOwnerNodePath(node, rootNode, projectionNodeId) {
1349
1557
  if (!node || !rootNode || node === rootNode)
@@ -1390,6 +1598,363 @@ function resolveProjectionAssetMeshSelectionKey(asset, ownerNodePath, projection
1390
1598
  }
1391
1599
  return bestSlot?.key ?? ownerNodePath;
1392
1600
  }
1601
+ function resolveProjectionAssetMeshSelectionSlots(materialSlots, ownerNodePath, projectionNodeId) {
1602
+ const normalizedOwnerPath = normalizeProjectionOwnerNodePath(ownerNodePath, { projectionNodeId });
1603
+ const normalizedMatchKey = createMaterialSlotOwnerPathMatchKey(normalizedOwnerPath, { projectionNodeId });
1604
+ const exactMatches = [];
1605
+ const descendantMatches = [];
1606
+ let descendantMatchLength = -1;
1607
+ for (const slot of materialSlots) {
1608
+ if (slot.matchKey === normalizedMatchKey) {
1609
+ exactMatches.push(slot);
1610
+ continue;
1611
+ }
1612
+ if (isMaterialSlotOwnerPathMatch(normalizedOwnerPath, slot.normalizedOwnerNodePath, { projectionNodeId })) {
1613
+ if (slot.matchKey.length > descendantMatchLength) {
1614
+ descendantMatches.length = 0;
1615
+ descendantMatchLength = slot.matchKey.length;
1616
+ }
1617
+ if (slot.matchKey.length === descendantMatchLength)
1618
+ descendantMatches.push(slot);
1619
+ }
1620
+ }
1621
+ return exactMatches.length > 0 ? exactMatches : descendantMatches;
1622
+ }
1623
+ function createProjectionAssetMeshSelectionProxies(options, node, projection, mesh, ownerNodePath, materialSlots) {
1624
+ const primitiveSlots = resolveProjectionAssetMeshSelectionSlots(materialSlots, ownerNodePath, node.id)
1625
+ .filter(slot => Number.isInteger(slot.primitiveIndex));
1626
+ if (primitiveSlots.length <= 1)
1627
+ return [];
1628
+ const subMeshes = Array.isArray(mesh?.subMeshes) ? mesh.subMeshes : [];
1629
+ if (subMeshes.length <= 1)
1630
+ return [];
1631
+ const proxies = [];
1632
+ const createdNodeIds = new Set();
1633
+ for (const slot of primitiveSlots) {
1634
+ const primitiveIndex = slot.primitiveIndex;
1635
+ if (primitiveIndex == null || primitiveIndex < 0)
1636
+ continue;
1637
+ const subMesh = subMeshes[primitiveIndex];
1638
+ if (!subMesh)
1639
+ continue;
1640
+ const selectionNodeId = createAssetMeshProjectionNodeId(node.id, slot.key);
1641
+ if (createdNodeIds.has(selectionNodeId))
1642
+ continue;
1643
+ const proxy = createProjectionSubMeshSelectionProxy(options, node, mesh, subMesh, {
1644
+ selectionNodeId,
1645
+ ownerNodePath,
1646
+ slotSelectionKey: slot.key,
1647
+ primitiveIndex,
1648
+ });
1649
+ if (!proxy)
1650
+ continue;
1651
+ createdNodeIds.add(selectionNodeId);
1652
+ proxies.push(proxy);
1653
+ projection.selectionMeshes = projection.selectionMeshes ?? [];
1654
+ projection.selectionMeshes.push(proxy);
1655
+ if (proxy.material)
1656
+ projection.runtimeObjects.push(proxy.material);
1657
+ }
1658
+ return proxies;
1659
+ }
1660
+ function resolveProjectionSplitPrimitiveMeshSelectionSlot(mesh, materialSlots, ownerNodePath, projectionNodeId, ownerMeshCount, ordinalByOwnerKey) {
1661
+ const subMeshes = Array.isArray(mesh?.subMeshes) ? mesh.subMeshes : [];
1662
+ if (subMeshes.length > 1)
1663
+ return null;
1664
+ if ((mesh?.getTotalVertices?.() ?? 0) <= 0)
1665
+ return null;
1666
+ const primitiveSlots = resolveProjectionAssetMeshSelectionSlots(materialSlots, ownerNodePath, projectionNodeId)
1667
+ .filter((slot) => (Number.isInteger(slot.primitiveIndex)))
1668
+ .sort((left, right) => left.primitiveIndex - right.primitiveIndex);
1669
+ if (primitiveSlots.length <= 1)
1670
+ return null;
1671
+ if (ownerMeshCount < primitiveSlots.length)
1672
+ return null;
1673
+ const ownerKey = createMaterialSlotOwnerPathMatchKey(ownerNodePath, { projectionNodeId });
1674
+ const ordinal = ordinalByOwnerKey.get(ownerKey) ?? 0;
1675
+ ordinalByOwnerKey.set(ownerKey, ordinal + 1);
1676
+ const namedSlot = primitiveSlots.find(slot => isProjectionRuntimeSplitPrimitiveOwnerNode(mesh, slot));
1677
+ if (namedSlot)
1678
+ return namedSlot;
1679
+ return primitiveSlots.find(slot => slot.primitiveIndex === ordinal) ?? primitiveSlots[ordinal] ?? null;
1680
+ }
1681
+ function createProjectionPrimitiveBoundsSelectionProxies(options, node, projection, mesh, ownerNodePath, materialSlots, ownerMeshCount) {
1682
+ const subMeshes = Array.isArray(mesh?.subMeshes) ? mesh.subMeshes : [];
1683
+ if (subMeshes.length > 1)
1684
+ return [];
1685
+ const primitiveSlots = resolveProjectionAssetMeshSelectionSlots(materialSlots, ownerNodePath, node.id)
1686
+ .filter(slot => Number.isInteger(slot.primitiveIndex) && !!slot.boundingBox)
1687
+ .sort((left, right) => (left.primitiveIndex ?? 0) - (right.primitiveIndex ?? 0));
1688
+ if (primitiveSlots.length <= 1)
1689
+ return [];
1690
+ if (ownerMeshCount >= primitiveSlots.length)
1691
+ return [];
1692
+ const proxies = [];
1693
+ const createdNodeIds = new Set();
1694
+ for (const slot of primitiveSlots) {
1695
+ const primitiveIndex = slot.primitiveIndex;
1696
+ if (primitiveIndex == null || primitiveIndex < 0)
1697
+ continue;
1698
+ const selectionNodeId = createAssetMeshProjectionNodeId(node.id, slot.key);
1699
+ if (createdNodeIds.has(selectionNodeId))
1700
+ continue;
1701
+ const proxy = createProjectionPrimitiveBoundsSelectionProxy(options, node, mesh, slot, {
1702
+ selectionNodeId,
1703
+ ownerNodePath,
1704
+ slotSelectionKey: slot.key,
1705
+ primitiveIndex,
1706
+ });
1707
+ if (!proxy)
1708
+ continue;
1709
+ createdNodeIds.add(selectionNodeId);
1710
+ proxies.push(proxy);
1711
+ projection.selectionMeshes = projection.selectionMeshes ?? [];
1712
+ projection.selectionMeshes.push(proxy);
1713
+ if (proxy.material)
1714
+ projection.runtimeObjects.push(proxy.material);
1715
+ }
1716
+ return proxies;
1717
+ }
1718
+ function createProjectionPrimitiveBoundsSelectionProxy(options, node, sourceMesh, slot, metadata) {
1719
+ const { babylon, scene } = options;
1720
+ const MeshBuilder = babylon.MeshBuilder;
1721
+ const StandardMaterial = babylon.StandardMaterial;
1722
+ if (!MeshBuilder?.CreateBox || !StandardMaterial)
1723
+ return null;
1724
+ const bounds = readProjectionPrimitiveSlotBounds(slot);
1725
+ if (!bounds)
1726
+ return null;
1727
+ const size = {
1728
+ x: Math.max(Math.abs(bounds.max.x - bounds.min.x), MIN_SELECTION_BOUNDS_PROXY_SIZE),
1729
+ y: Math.max(Math.abs(bounds.max.y - bounds.min.y), MIN_SELECTION_BOUNDS_PROXY_SIZE),
1730
+ z: Math.max(Math.abs(bounds.max.z - bounds.min.z), MIN_SELECTION_BOUNDS_PROXY_SIZE),
1731
+ };
1732
+ const center = {
1733
+ x: (bounds.min.x + bounds.max.x) / 2,
1734
+ y: (bounds.min.y + bounds.max.y) / 2,
1735
+ z: (bounds.min.z + bounds.max.z) / 2,
1736
+ };
1737
+ const proxy = MeshBuilder.CreateBox(`${sourceMesh.name ?? node.id}.selectionBoundsProxy.${sanitizeProjectionImportSnapshotId(metadata.slotSelectionKey)}`, {
1738
+ width: size.x,
1739
+ height: size.y,
1740
+ depth: size.z,
1741
+ }, scene);
1742
+ if (!proxy)
1743
+ return null;
1744
+ proxy.parent = sourceMesh;
1745
+ proxy.position.x = center.x;
1746
+ proxy.position.y = center.y;
1747
+ proxy.position.z = center.z;
1748
+ proxy.isPickable = false;
1749
+ proxy.metadata = {
1750
+ ...(proxy.metadata ?? {}),
1751
+ disablePlanarShadow: true,
1752
+ disableBlobShadow: true,
1753
+ disableStaticProjectedShadow: true,
1754
+ editorProjectionSelectionProxy: true,
1755
+ editorProjectionSelectionBoundsProxy: true,
1756
+ editorProjection: {
1757
+ nodeId: metadata.selectionNodeId,
1758
+ rootNodeId: node.id,
1759
+ sourceId: node.asset?.sourceId,
1760
+ ownerNodePath: metadata.ownerNodePath,
1761
+ slotSelectionKey: metadata.slotSelectionKey,
1762
+ primitiveIndex: metadata.primitiveIndex,
1763
+ runtimeKind: 'assetMeshSelectionProxy',
1764
+ ...createProjectionShadowMetadata(node),
1765
+ },
1766
+ };
1767
+ const material = new StandardMaterial(`${proxy.name}.material`, scene);
1768
+ material.alpha = 0;
1769
+ material.disableLighting = true;
1770
+ material.backFaceCulling = false;
1771
+ if ('disableColorWrite' in material) {
1772
+ material.disableColorWrite = true;
1773
+ }
1774
+ proxy.material = material;
1775
+ return proxy;
1776
+ }
1777
+ function readProjectionPrimitiveSlotBounds(slot) {
1778
+ const min = readProjectionMaterialSlotBoundsVec3(slot.boundingBox?.min);
1779
+ const max = readProjectionMaterialSlotBoundsVec3(slot.boundingBox?.max);
1780
+ if (!min || !max)
1781
+ return null;
1782
+ return { min, max };
1783
+ }
1784
+ function readProjectionMaterialSlotBoundsVec3(value) {
1785
+ if (!Array.isArray(value) || value.length < 3)
1786
+ return null;
1787
+ const [x, y, z] = value.map(item => Number(item));
1788
+ if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z))
1789
+ return null;
1790
+ return { x, y, z };
1791
+ }
1792
+ function isProjectionRuntimeSplitPrimitiveOwnerNode(ownerNode, slot) {
1793
+ if (slot.primitiveIndex == null)
1794
+ return false;
1795
+ const primitiveNames = new Set();
1796
+ for (const baseName of [
1797
+ slot.ownerNodePath,
1798
+ readProjectionOwnerPathLastSegment(slot.ownerNodePath),
1799
+ stripProjectionPrimitiveLabelSuffix(readProjectionSlotLabel(slot)),
1800
+ readProjectionSlotLabel(slot),
1801
+ ]) {
1802
+ addProjectionRuntimeOwnerName(primitiveNames, appendProjectionPrimitiveOwnerSuffix(baseName, slot.primitiveIndex));
1803
+ }
1804
+ return collectProjectionRuntimeNodeMatchNames(ownerNode).some(name => primitiveNames.has(name));
1805
+ }
1806
+ function collectProjectionRuntimeNodeMatchNames(node) {
1807
+ const rawNames = [
1808
+ typeof node?.name === 'string' ? node.name : null,
1809
+ typeof node?.id === 'string' ? node.id : null,
1810
+ readProjectionRuntimeNodeDisplayName(node),
1811
+ readProjectionSourceNodeSegment(node),
1812
+ ];
1813
+ const names = new Set();
1814
+ for (const rawName of rawNames) {
1815
+ addProjectionRuntimeOwnerName(names, rawName);
1816
+ for (const tailName of collectProjectionRuntimeOwnerTailNames(rawName)) {
1817
+ addProjectionRuntimeOwnerName(names, tailName);
1818
+ }
1819
+ }
1820
+ return [...names];
1821
+ }
1822
+ function readProjectionRuntimeNodeDisplayName(node) {
1823
+ const name = typeof node?.name === 'string' ? node.name.trim() : '';
1824
+ const id = typeof node?.id === 'string' ? node.id.trim() : '';
1825
+ if (name && id && name !== id)
1826
+ return `${name} (${id})`;
1827
+ return name || id || null;
1828
+ }
1829
+ function collectProjectionRuntimeOwnerTailNames(value) {
1830
+ const raw = typeof value === 'string' ? value.trim() : '';
1831
+ if (!raw)
1832
+ return [];
1833
+ const names = new Set();
1834
+ const withoutClonePrefix = raw.replace(/^Clone of\s+/i, '').trim();
1835
+ for (const candidate of [raw, withoutClonePrefix]) {
1836
+ const colonTail = candidate.includes(':') ? candidate.split(':').pop() : '';
1837
+ const dotTail = candidate.includes('.') ? candidate.split('.').pop() : '';
1838
+ if (colonTail)
1839
+ names.add(colonTail);
1840
+ if (dotTail)
1841
+ names.add(dotTail);
1842
+ }
1843
+ return [...names];
1844
+ }
1845
+ function readProjectionOwnerPathLastSegment(ownerNodePath) {
1846
+ const segments = ownerNodePath.split('/').map(segment => segment.trim()).filter(Boolean);
1847
+ return segments.length > 0 ? segments[segments.length - 1] ?? null : null;
1848
+ }
1849
+ function stripProjectionPrimitiveLabelSuffix(value) {
1850
+ if (typeof value !== 'string')
1851
+ return null;
1852
+ const stripped = value.replace(/\s*\/\s*Primitive\s+\d+\s*$/i, '').trim();
1853
+ return stripped || null;
1854
+ }
1855
+ function appendProjectionPrimitiveOwnerSuffix(value, primitiveIndex) {
1856
+ if (typeof value !== 'string')
1857
+ return null;
1858
+ const baseName = stripProjectionPrimitiveLabelSuffix(value) ?? value.trim();
1859
+ return baseName ? `${baseName}_primitive${primitiveIndex}` : null;
1860
+ }
1861
+ function readProjectionSlotLabel(slot) {
1862
+ const label = typeof slot.label === 'string' ? slot.label.trim() : '';
1863
+ if (label)
1864
+ return label;
1865
+ const materialName = typeof slot.materialName === 'string' ? slot.materialName.trim() : '';
1866
+ return materialName || null;
1867
+ }
1868
+ function addProjectionRuntimeOwnerName(names, value) {
1869
+ const normalized = normalizeProjectionRuntimeOwnerName(value);
1870
+ if (normalized)
1871
+ names.add(normalized);
1872
+ }
1873
+ function normalizeProjectionRuntimeOwnerName(value) {
1874
+ const trimmed = typeof value === 'string' ? value.trim() : '';
1875
+ if (!trimmed)
1876
+ return null;
1877
+ return createMaterialSlotOwnerPathMatchKey(trimmed);
1878
+ }
1879
+ function createProjectionSubMeshSelectionProxy(options, node, sourceMesh, subMesh, metadata) {
1880
+ const { babylon, scene } = options;
1881
+ const Mesh = babylon.Mesh;
1882
+ const VertexData = babylon.VertexData;
1883
+ const VertexBuffer = babylon.VertexBuffer;
1884
+ const StandardMaterial = babylon.StandardMaterial;
1885
+ if (!Mesh || !VertexData || !VertexBuffer || !StandardMaterial)
1886
+ return null;
1887
+ const positions = sourceMesh.getVerticesData?.(VertexBuffer.PositionKind, true, true);
1888
+ if (!positions || positions.length < 9)
1889
+ return null;
1890
+ const sourceIndices = sourceMesh.getIndices?.(true, true) ?? null;
1891
+ const subIndices = readProjectionSubMeshIndices(sourceIndices, subMesh);
1892
+ if (subIndices.length < 3)
1893
+ return null;
1894
+ const indexMap = new Map();
1895
+ const proxyPositions = [];
1896
+ const proxyIndices = [];
1897
+ for (const sourceIndex of subIndices) {
1898
+ const vertexIndex = Number(sourceIndex);
1899
+ if (!Number.isInteger(vertexIndex) || vertexIndex < 0)
1900
+ continue;
1901
+ let mappedIndex = indexMap.get(vertexIndex);
1902
+ if (mappedIndex == null) {
1903
+ const positionIndex = vertexIndex * 3;
1904
+ if (positionIndex + 2 >= positions.length)
1905
+ continue;
1906
+ mappedIndex = indexMap.size;
1907
+ indexMap.set(vertexIndex, mappedIndex);
1908
+ proxyPositions.push(Number(positions[positionIndex] ?? 0), Number(positions[positionIndex + 1] ?? 0), Number(positions[positionIndex + 2] ?? 0));
1909
+ }
1910
+ proxyIndices.push(mappedIndex);
1911
+ }
1912
+ if (proxyPositions.length < 9 || proxyIndices.length < 3)
1913
+ return null;
1914
+ const proxy = new Mesh(`${sourceMesh.name ?? node.id}.selectionProxy.${sanitizeProjectionImportSnapshotId(metadata.slotSelectionKey)}`, scene);
1915
+ proxy.parent = sourceMesh;
1916
+ proxy.isPickable = false;
1917
+ proxy.metadata = {
1918
+ ...(proxy.metadata ?? {}),
1919
+ disablePlanarShadow: true,
1920
+ disableBlobShadow: true,
1921
+ disableStaticProjectedShadow: true,
1922
+ editorProjectionSelectionProxy: true,
1923
+ editorProjection: {
1924
+ nodeId: metadata.selectionNodeId,
1925
+ rootNodeId: node.id,
1926
+ sourceId: node.asset?.sourceId,
1927
+ ownerNodePath: metadata.ownerNodePath,
1928
+ slotSelectionKey: metadata.slotSelectionKey,
1929
+ primitiveIndex: metadata.primitiveIndex,
1930
+ runtimeKind: 'assetMeshSelectionProxy',
1931
+ ...createProjectionShadowMetadata(node),
1932
+ },
1933
+ };
1934
+ const vertexData = new VertexData();
1935
+ vertexData.positions = proxyPositions;
1936
+ vertexData.indices = proxyIndices;
1937
+ vertexData.applyToMesh(proxy, false);
1938
+ const material = new StandardMaterial(`${proxy.name}.material`, scene);
1939
+ material.alpha = 0;
1940
+ material.disableLighting = true;
1941
+ material.backFaceCulling = false;
1942
+ if ('disableColorWrite' in material) {
1943
+ material.disableColorWrite = true;
1944
+ }
1945
+ proxy.material = material;
1946
+ return proxy;
1947
+ }
1948
+ function readProjectionSubMeshIndices(sourceIndices, subMesh) {
1949
+ const indexStart = Number.isInteger(subMesh?.indexStart) ? subMesh.indexStart : 0;
1950
+ const indexCount = Number.isInteger(subMesh?.indexCount) ? subMesh.indexCount : 0;
1951
+ if (sourceIndices && indexCount > 0) {
1952
+ return Array.from(sourceIndices).slice(indexStart, indexStart + indexCount);
1953
+ }
1954
+ const verticesStart = Number.isInteger(subMesh?.verticesStart) ? subMesh.verticesStart : 0;
1955
+ const verticesCount = Number.isInteger(subMesh?.verticesCount) ? subMesh.verticesCount : 0;
1956
+ return Array.from({ length: Math.max(0, verticesCount) }, (_, index) => verticesStart + index);
1957
+ }
1393
1958
  function readProjectionAssetMaterialSlots(asset, projectionNodeId) {
1394
1959
  const rawSlots = asset.metadata?.materialSlots;
1395
1960
  if (!Array.isArray(rawSlots))
@@ -1427,6 +1992,20 @@ function countProjectionMeshMaterials(meshes) {
1427
1992
  }
1428
1993
  return counts;
1429
1994
  }
1995
+ function isProjectionSinglePrimitiveMaterialSlot(slot, materialSlots) {
1996
+ if (slot.primitiveIndex !== 0 || materialSlots.length === 0)
1997
+ return false;
1998
+ const matchingPrimitiveSlots = materialSlots.filter(candidate => (candidate.primitiveIndex != null
1999
+ && candidate.matchKey === slot.matchKey
2000
+ && isProjectionMaterialSlotSameMesh(candidate, slot)));
2001
+ return matchingPrimitiveSlots.length === 1 && matchingPrimitiveSlots[0]?.primitiveIndex === 0;
2002
+ }
2003
+ function isProjectionMaterialSlotSameMesh(left, right) {
2004
+ if (left.meshIndex != null || right.meshIndex != null) {
2005
+ return left.meshIndex === right.meshIndex;
2006
+ }
2007
+ return true;
2008
+ }
1430
2009
  function detachProjectionSlotMaterial(mesh, materialCounts, ownerNodePath) {
1431
2010
  const material = mesh?.material;
1432
2011
  if (!material || typeof material !== 'object')
@@ -1445,6 +2024,94 @@ function detachProjectionSlotMaterial(mesh, materialCounts, ownerNodePath) {
1445
2024
  materialCounts.set(clone, 1);
1446
2025
  return clone;
1447
2026
  }
2027
+ function resolveProjectionSlotMaterialTarget(mesh, materialCounts, slot, ownerNodePath, options) {
2028
+ const material = mesh?.material;
2029
+ if (!material) {
2030
+ return {
2031
+ target: null,
2032
+ targetMaterialStatus: 'missingMaterial',
2033
+ warningMessage: 'Slot material profile was not applied because the matched mesh has no target material.',
2034
+ };
2035
+ }
2036
+ if (slot.primitiveIndex == null) {
2037
+ const slotMaterial = detachProjectionSlotMaterial(mesh, materialCounts, ownerNodePath);
2038
+ if (!slotMaterial) {
2039
+ return {
2040
+ target: null,
2041
+ targetMaterialStatus: 'missingTargetMaterial',
2042
+ warningMessage: 'Slot material profile was not applied because the mesh material could not be detached.',
2043
+ };
2044
+ }
2045
+ return {
2046
+ target: {
2047
+ material: slotMaterial,
2048
+ clonedMaterials: slotMaterial !== material ? [slotMaterial] : [],
2049
+ },
2050
+ targetMaterialStatus: 'material',
2051
+ };
2052
+ }
2053
+ if (!Array.isArray(material.subMaterials)) {
2054
+ const isSplitPrimitiveOwner = isProjectionRuntimeSplitPrimitiveOwnerNode(mesh, slot);
2055
+ if (!isSplitPrimitiveOwner && (slot.primitiveIndex !== 0 || !options.primitiveSingleMaterialFallbackAllowed)) {
2056
+ return {
2057
+ target: null,
2058
+ targetMaterialStatus: 'missingSubMaterial',
2059
+ warningMessage: 'Primitive slot material profile was not applied because the target mesh does not expose a matching subMaterial.',
2060
+ };
2061
+ }
2062
+ const slotMaterial = detachProjectionSlotMaterial(mesh, materialCounts, ownerNodePath);
2063
+ if (!slotMaterial) {
2064
+ return {
2065
+ target: null,
2066
+ targetMaterialStatus: 'missingTargetMaterial',
2067
+ warningMessage: 'Primitive slot material profile was not applied because the fallback mesh material could not be detached.',
2068
+ };
2069
+ }
2070
+ return {
2071
+ target: {
2072
+ material: slotMaterial,
2073
+ clonedMaterials: slotMaterial !== material ? [slotMaterial] : [],
2074
+ },
2075
+ targetMaterialStatus: 'primitiveFallbackMaterial',
2076
+ };
2077
+ }
2078
+ const slotMaterial = detachProjectionSlotMaterial(mesh, materialCounts, ownerNodePath);
2079
+ if (!slotMaterial || !Array.isArray(slotMaterial.subMaterials)) {
2080
+ return {
2081
+ target: null,
2082
+ targetMaterialStatus: 'missingTargetMaterial',
2083
+ warningMessage: 'Primitive slot material profile was not applied because the multi-material target could not be detached.',
2084
+ };
2085
+ }
2086
+ const subMaterials = slotMaterial.subMaterials;
2087
+ const subMaterial = subMaterials[slot.primitiveIndex];
2088
+ if (!subMaterial || typeof subMaterial !== 'object' || typeof subMaterial.clone !== 'function') {
2089
+ return {
2090
+ target: null,
2091
+ targetMaterialStatus: 'missingSubMaterial',
2092
+ warningMessage: 'Primitive slot material profile was not applied because the target subMaterial is missing or cannot be cloned.',
2093
+ };
2094
+ }
2095
+ const clonedSubMaterial = subMaterial.clone(createProjectionSlotSubMaterialName(subMaterial, ownerNodePath, slot.primitiveIndex));
2096
+ if (!clonedSubMaterial) {
2097
+ return {
2098
+ target: null,
2099
+ targetMaterialStatus: 'missingSubMaterial',
2100
+ warningMessage: 'Primitive slot material profile was not applied because the target subMaterial clone failed.',
2101
+ };
2102
+ }
2103
+ subMaterials[slot.primitiveIndex] = clonedSubMaterial;
2104
+ return {
2105
+ target: {
2106
+ material: clonedSubMaterial,
2107
+ clonedMaterials: [
2108
+ ...(slotMaterial !== material ? [slotMaterial] : []),
2109
+ clonedSubMaterial,
2110
+ ],
2111
+ },
2112
+ targetMaterialStatus: 'subMaterial',
2113
+ };
2114
+ }
1448
2115
  function cloneProjectionSubMaterials(clone, source, ownerNodePath) {
1449
2116
  if (!Array.isArray(clone?.subMaterials) || !Array.isArray(source?.subMaterials))
1450
2117
  return;
@@ -2064,17 +2731,26 @@ function syncProjectionSelection(babylon, projections, selection) {
2064
2731
  for (const [nodeId, projection] of projections) {
2065
2732
  const rootSelected = selectedIds.has(nodeId);
2066
2733
  const rootActive = selection.activeId === nodeId;
2734
+ const proxyNodeIds = new Set((projection.selectionMeshes ?? [])
2735
+ .map(readProjectionMeshSelectionNodeId)
2736
+ .filter((id) => !!id));
2067
2737
  let projectionSelected = false;
2068
2738
  for (const mesh of projection.outlineMeshes) {
2069
- const meshNodeId = typeof mesh?.metadata?.editorProjection?.nodeId === 'string'
2070
- ? mesh.metadata.editorProjection.nodeId
2071
- : nodeId;
2072
- const selected = rootSelected || selectedIds.has(meshNodeId);
2739
+ const meshNodeId = readProjectionMeshSelectionNodeId(mesh) ?? nodeId;
2740
+ const selected = rootSelected || (!proxyNodeIds.has(meshNodeId) && selectedIds.has(meshNodeId));
2073
2741
  const active = rootActive || selection.activeId === meshNodeId;
2074
2742
  if (selected)
2075
2743
  projectionSelected = true;
2076
2744
  syncProjectionMeshSelection(mesh, selected, active, Color4);
2077
2745
  }
2746
+ for (const mesh of projection.selectionMeshes ?? []) {
2747
+ const meshNodeId = readProjectionMeshSelectionNodeId(mesh) ?? nodeId;
2748
+ const selected = selectedIds.has(meshNodeId);
2749
+ const active = selection.activeId === meshNodeId;
2750
+ if (selected)
2751
+ projectionSelected = true;
2752
+ syncProjectionMeshSelection(mesh, selected, active, Color4);
2753
+ }
2078
2754
  if (projectionSelected) {
2079
2755
  projection.selectionEdgesPrewarmCancel?.();
2080
2756
  projection.selectionEdgesPrewarmCancel = null;
@@ -2099,7 +2775,7 @@ function syncProjectionMeshSelection(mesh, selected, active, Color4) {
2099
2775
  }
2100
2776
  }
2101
2777
  function prewarmProjectionSelectionEdges(projection) {
2102
- for (const mesh of projection.outlineMeshes) {
2778
+ for (const mesh of [...projection.outlineMeshes, ...(projection.selectionMeshes ?? [])]) {
2103
2779
  if (mesh?.metadata?.editorProjectionSelectionEdgesPrewarmed)
2104
2780
  continue;
2105
2781
  if (mesh?.metadata?.editorProjectionSelectionEdgesSelected)