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

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 (98) hide show
  1. package/dist/build-info.json +15 -15
  2. package/dist/index.d.ts +5 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +7 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/local-editor-harness.d.ts +4 -0
  7. package/dist/local-editor-harness.d.ts.map +1 -1
  8. package/dist/local-editor-harness.js +113 -51
  9. package/dist/local-editor-harness.js.map +1 -1
  10. package/dist/local-editor-performance-stats.d.ts.map +1 -1
  11. package/dist/local-editor-performance-stats.js +7 -0
  12. package/dist/local-editor-performance-stats.js.map +1 -1
  13. package/dist/playable-local-editor-host.d.ts +5 -1
  14. package/dist/playable-local-editor-host.d.ts.map +1 -1
  15. package/dist/playable-local-editor-host.js +75 -1
  16. package/dist/playable-local-editor-host.js.map +1 -1
  17. package/node_modules/@fps-games/babylon-renderer/package.json +1 -1
  18. package/node_modules/@fps-games/editor-babylon/dist/inspector-host.d.ts +2 -0
  19. package/node_modules/@fps-games/editor-babylon/dist/inspector-host.d.ts.map +1 -1
  20. package/node_modules/@fps-games/editor-babylon/dist/inspector-host.js +1 -0
  21. package/node_modules/@fps-games/editor-babylon/dist/inspector-host.js.map +1 -1
  22. package/node_modules/@fps-games/editor-babylon/dist/material-property-adapter.d.ts +6 -2
  23. package/node_modules/@fps-games/editor-babylon/dist/material-property-adapter.d.ts.map +1 -1
  24. package/node_modules/@fps-games/editor-babylon/dist/material-property-adapter.js +94 -24
  25. package/node_modules/@fps-games/editor-babylon/dist/material-property-adapter.js.map +1 -1
  26. package/node_modules/@fps-games/editor-babylon/dist/material-slot-source-resolver.d.ts +6 -2
  27. package/node_modules/@fps-games/editor-babylon/dist/material-slot-source-resolver.d.ts.map +1 -1
  28. package/node_modules/@fps-games/editor-babylon/dist/material-slot-source-resolver.js +7 -0
  29. package/node_modules/@fps-games/editor-babylon/dist/material-slot-source-resolver.js.map +1 -1
  30. package/node_modules/@fps-games/editor-babylon/dist/monitor.d.ts +2 -0
  31. package/node_modules/@fps-games/editor-babylon/dist/monitor.d.ts.map +1 -1
  32. package/node_modules/@fps-games/editor-babylon/dist/monitor.js +2 -0
  33. package/node_modules/@fps-games/editor-babylon/dist/monitor.js.map +1 -1
  34. package/node_modules/@fps-games/editor-babylon/dist/performance-monitor.d.ts +14 -0
  35. package/node_modules/@fps-games/editor-babylon/dist/performance-monitor.d.ts.map +1 -1
  36. package/node_modules/@fps-games/editor-babylon/dist/performance-monitor.js +121 -2
  37. package/node_modules/@fps-games/editor-babylon/dist/performance-monitor.js.map +1 -1
  38. package/node_modules/@fps-games/editor-babylon/dist/projection-asset-container-importer.js +1 -5
  39. package/node_modules/@fps-games/editor-babylon/dist/projection-asset-container-importer.js.map +1 -1
  40. package/node_modules/@fps-games/editor-babylon/dist/projection.d.ts +18 -0
  41. package/node_modules/@fps-games/editor-babylon/dist/projection.d.ts.map +1 -1
  42. package/node_modules/@fps-games/editor-babylon/dist/projection.js +367 -52
  43. package/node_modules/@fps-games/editor-babylon/dist/projection.js.map +1 -1
  44. package/node_modules/@fps-games/editor-babylon/dist/shadow-preview-controller.d.ts.map +1 -1
  45. package/node_modules/@fps-games/editor-babylon/dist/shadow-preview-controller.js +96 -35
  46. package/node_modules/@fps-games/editor-babylon/dist/shadow-preview-controller.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 +3 -1
  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-types.d.ts +17 -0
  57. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts.map +1 -1
  58. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts.map +1 -1
  59. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js +84 -0
  60. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js.map +1 -1
  61. package/node_modules/@fps-games/editor-browser/package.json +3 -3
  62. package/node_modules/@fps-games/editor-core/package.json +2 -2
  63. package/node_modules/@fps-games/editor-forge-play/package.json +2 -2
  64. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/forge-play-types.d.ts +4 -1
  65. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/forge-play-types.d.ts.map +1 -1
  66. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/forge-play.d.ts.map +1 -1
  67. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/forge-play.js +89 -9
  68. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/forge-play.js.map +1 -1
  69. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/host-controller.d.ts +4 -1
  70. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/host-controller.d.ts.map +1 -1
  71. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/host-controller.js +60 -5
  72. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/host-controller.js.map +1 -1
  73. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/index.d.ts +1 -1
  74. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/index.d.ts.map +1 -1
  75. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/index.js.map +1 -1
  76. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/lifecycle.d.ts.map +1 -1
  77. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/lifecycle.js +62 -2
  78. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/lifecycle.js.map +1 -1
  79. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/types.d.ts +27 -0
  80. package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/types.d.ts.map +1 -1
  81. package/node_modules/@fps-games/editor-playable-sdk/dist/scene-source/driver.d.ts.map +1 -1
  82. package/node_modules/@fps-games/editor-playable-sdk/dist/scene-source/driver.js +57 -3
  83. package/node_modules/@fps-games/editor-playable-sdk/dist/scene-source/driver.js.map +1 -1
  84. package/node_modules/@fps-games/editor-playable-sdk/dist/scene-source/types.d.ts +30 -0
  85. package/node_modules/@fps-games/editor-playable-sdk/dist/scene-source/types.d.ts.map +1 -1
  86. package/node_modules/@fps-games/editor-playable-sdk/dist/session/hierarchy.d.ts.map +1 -1
  87. package/node_modules/@fps-games/editor-playable-sdk/dist/session/hierarchy.js +18 -2
  88. package/node_modules/@fps-games/editor-playable-sdk/dist/session/hierarchy.js.map +1 -1
  89. package/node_modules/@fps-games/editor-playable-sdk/dist/session/patches.d.ts +1 -0
  90. package/node_modules/@fps-games/editor-playable-sdk/dist/session/patches.d.ts.map +1 -1
  91. package/node_modules/@fps-games/editor-playable-sdk/dist/session/patches.js +1 -0
  92. package/node_modules/@fps-games/editor-playable-sdk/dist/session/patches.js.map +1 -1
  93. package/node_modules/@fps-games/editor-playable-sdk/package.json +3 -3
  94. package/node_modules/@fps-games/editor-protocol/dist/index.d.ts +1 -1
  95. package/node_modules/@fps-games/editor-protocol/dist/index.d.ts.map +1 -1
  96. package/node_modules/@fps-games/editor-protocol/dist/index.js.map +1 -1
  97. package/node_modules/@fps-games/editor-protocol/package.json +1 -1
  98. package/package.json +8 -8
@@ -818,6 +818,7 @@ function attachImportedProjectionResult(options, node, asset, projection, result
818
818
  },
819
819
  };
820
820
  applyAssetDefaultsTransform(babylon, modelRoot, asset.transform);
821
+ collapseZeroVertexProjectionWrapperMeshes(babylon, scene, result);
821
822
  const importedNodes = [
822
823
  ...(result.transformNodes ?? []),
823
824
  ...(result.meshes ?? []),
@@ -910,6 +911,141 @@ function attachImportedProjectionResult(options, node, asset, projection, result
910
911
  throw error;
911
912
  }
912
913
  }
914
+ function collapseZeroVertexProjectionWrapperMeshes(babylon, scene, result) {
915
+ const meshes = result.meshes ?? [];
916
+ if (meshes.length === 0)
917
+ return;
918
+ const TransformNode = babylon.TransformNode;
919
+ if (!TransformNode)
920
+ return;
921
+ const sourceMeshes = collectProjectionSourceMeshReferences(meshes);
922
+ const keptMeshes = [];
923
+ const replacementTransformNodes = [];
924
+ for (const mesh of meshes) {
925
+ if (!canCollapseProjectionWrapperMesh(mesh, sourceMeshes)) {
926
+ keptMeshes.push(mesh);
927
+ continue;
928
+ }
929
+ const replacement = createProjectionWrapperTransformNode(TransformNode, scene, mesh);
930
+ if (!replacement) {
931
+ keptMeshes.push(mesh);
932
+ continue;
933
+ }
934
+ for (const child of readProjectionDirectChildren(mesh)) {
935
+ child.parent = replacement;
936
+ }
937
+ mesh.dispose?.(true, false);
938
+ replacementTransformNodes.push(replacement);
939
+ }
940
+ if (replacementTransformNodes.length === 0)
941
+ return;
942
+ result.meshes = keptMeshes;
943
+ result.transformNodes = [
944
+ ...(result.transformNodes ?? []),
945
+ ...replacementTransformNodes,
946
+ ];
947
+ }
948
+ function collectProjectionSourceMeshReferences(meshes) {
949
+ const sourceMeshes = new Set();
950
+ for (const mesh of meshes) {
951
+ const sourceMesh = mesh?.sourceMesh;
952
+ if (sourceMesh)
953
+ sourceMeshes.add(sourceMesh);
954
+ }
955
+ return sourceMeshes;
956
+ }
957
+ function canCollapseProjectionWrapperMesh(mesh, sourceMeshes) {
958
+ if (!mesh || mesh.getClassName?.() !== 'Mesh')
959
+ return false;
960
+ if (sourceMeshes.has(mesh))
961
+ return false;
962
+ if (mesh.sourceMesh)
963
+ return false;
964
+ const vertices = mesh.getTotalVertices?.();
965
+ if (vertices !== 0)
966
+ return false;
967
+ if (Array.isArray(mesh.instances) && mesh.instances.length > 0)
968
+ return false;
969
+ if (mesh.skeleton || mesh.morphTargetManager)
970
+ return false;
971
+ if (Array.isArray(mesh.subMeshes) && mesh.subMeshes.length > 0)
972
+ return false;
973
+ if (mesh.material)
974
+ return false;
975
+ const projection = readProjectionObject(mesh.metadata?.editorProjection);
976
+ const runtimeKind = projection?.runtimeKind;
977
+ if (runtimeKind === 'assetMeshSelectionProxy'
978
+ || runtimeKind === 'primitive'
979
+ || runtimeKind === 'camera'
980
+ || runtimeKind === 'light'
981
+ || projection?.primitiveShape != null
982
+ || projection?.helper != null
983
+ || projection?.disableStaticProjectedShadow === false) {
984
+ return false;
985
+ }
986
+ return true;
987
+ }
988
+ function createProjectionWrapperTransformNode(TransformNode, scene, mesh) {
989
+ const name = typeof mesh.name === 'string' ? mesh.name : typeof mesh.id === 'string' ? mesh.id : 'projectionWrapper';
990
+ const replacement = new TransformNode(name, scene);
991
+ if (typeof mesh.id === 'string')
992
+ replacement.id = mesh.id;
993
+ if (typeof mesh.name === 'string')
994
+ replacement.name = mesh.name;
995
+ replacement.parent = mesh.parent ?? null;
996
+ if (mesh.metadata && typeof mesh.metadata === 'object') {
997
+ replacement.metadata = { ...mesh.metadata };
998
+ }
999
+ if (mesh.reservedDataStore && typeof mesh.reservedDataStore === 'object') {
1000
+ replacement.reservedDataStore = { ...mesh.reservedDataStore };
1001
+ }
1002
+ if (Array.isArray(mesh.animations))
1003
+ replacement.animations = [...mesh.animations];
1004
+ copyProjectionWrapperTransform(mesh, replacement);
1005
+ if (typeof mesh.isEnabled === 'function' && typeof replacement.setEnabled === 'function') {
1006
+ replacement.setEnabled(mesh.isEnabled());
1007
+ }
1008
+ if (typeof mesh.state === 'string')
1009
+ replacement.state = mesh.state;
1010
+ if (typeof mesh.layerMask === 'number')
1011
+ replacement.layerMask = mesh.layerMask;
1012
+ return replacement;
1013
+ }
1014
+ function copyProjectionWrapperTransform(source, target) {
1015
+ copyBabylonVector3(source.position, target.position);
1016
+ copyBabylonVector3(source.rotation, target.rotation);
1017
+ copyBabylonVector3(source.scaling, target.scaling);
1018
+ if (source.rotationQuaternion?.clone) {
1019
+ target.rotationQuaternion = source.rotationQuaternion.clone();
1020
+ }
1021
+ else {
1022
+ target.rotationQuaternion = null;
1023
+ }
1024
+ const pivotMatrix = source._pivotMatrix?.clone?.() ?? source.getPivotMatrix?.()?.clone?.();
1025
+ if (pivotMatrix) {
1026
+ if (source._usePivotMatrix && typeof target.setPreTransformMatrix === 'function') {
1027
+ target.setPreTransformMatrix(pivotMatrix.clone?.() ?? pivotMatrix);
1028
+ }
1029
+ else if (typeof target.setPivotMatrix === 'function') {
1030
+ target.setPivotMatrix(pivotMatrix.clone?.() ?? pivotMatrix, false);
1031
+ }
1032
+ }
1033
+ }
1034
+ function copyBabylonVector3(source, target) {
1035
+ if (!source || !target)
1036
+ return;
1037
+ if (typeof target.copyFrom === 'function') {
1038
+ target.copyFrom(source);
1039
+ return;
1040
+ }
1041
+ target.x = source.x;
1042
+ target.y = source.y;
1043
+ target.z = source.z;
1044
+ }
1045
+ function readProjectionDirectChildren(node) {
1046
+ const children = node?.getChildren?.((candidate) => candidate?.parent === node, true);
1047
+ return Array.isArray(children) ? children.filter(child => child?.parent === node) : [];
1048
+ }
913
1049
  function createProjectionImportStructureSnapshot(node, asset, modelRoot, result) {
914
1050
  const runtimeNodes = collectProjectionImportRuntimeNodes(modelRoot, result);
915
1051
  const nodeIdByRuntimeNode = new Map();
@@ -1296,34 +1432,106 @@ function applyProjectionArtistMaterialProfilesToMeshes(options, node, modelRoot,
1296
1432
  const materials = new Set();
1297
1433
  if (hasRootProfile) {
1298
1434
  for (const mesh of meshes) {
1299
- const material = mesh?.material;
1300
- if (material)
1301
- materials.add(material);
1302
- }
1303
- for (const material of materials) {
1435
+ const material = readProjectionWritableMeshMaterial(mesh);
1436
+ const meshOwnerNodePath = buildProjectionOwnerNodePath(mesh, modelRoot, node.id);
1437
+ if (!material) {
1438
+ emitProjectionMaterialProfileDiagnostic(options, {
1439
+ projectionNodeId: node.id,
1440
+ source: 'rootProfile',
1441
+ applied: false,
1442
+ matchKind: 'root',
1443
+ targetMaterialStatus: 'missingMaterial',
1444
+ meshOwnerNodePath,
1445
+ warningMessage: 'Root material profile was not applied because an imported mesh has no target material.',
1446
+ });
1447
+ continue;
1448
+ }
1449
+ if (materials.has(material))
1450
+ continue;
1451
+ materials.add(material);
1304
1452
  applyProjectionArtistMaterialProfile(options, material, node.artistMaterialProfile);
1453
+ emitProjectionMaterialProfileDiagnostic(options, {
1454
+ projectionNodeId: node.id,
1455
+ source: 'rootProfile',
1456
+ applied: true,
1457
+ matchKind: 'root',
1458
+ targetMaterialStatus: 'material',
1459
+ meshOwnerNodePath,
1460
+ targetMaterialName: readProjectionMaterialName(material),
1461
+ targetMaterialType: readProjectionMaterialType(material),
1462
+ });
1463
+ }
1464
+ if (materials.size === 0 && meshes.length === 0) {
1465
+ emitProjectionMaterialProfileDiagnostic(options, {
1466
+ projectionNodeId: node.id,
1467
+ source: 'rootProfile',
1468
+ applied: false,
1469
+ matchKind: 'root',
1470
+ targetMaterialStatus: 'missingMaterial',
1471
+ meshOwnerNodePath: null,
1472
+ warningMessage: 'Root material profile was not applied because the imported model produced no meshes.',
1473
+ });
1305
1474
  }
1306
1475
  }
1307
1476
  if (!hasSlotProfiles)
1308
1477
  return clonedMaterials;
1309
1478
  const materialCounts = countProjectionMeshMaterials(meshes);
1310
1479
  const assetMaterialSlots = node.asset ? readProjectionAssetMaterialSlots(node.asset, node.id) : [];
1480
+ const unmatchedSlotBindings = new Set(slotBindings);
1311
1481
  for (const mesh of meshes) {
1312
- const material = mesh?.material;
1313
- if (!material)
1314
- continue;
1315
1482
  const ownerNodePath = buildProjectionOwnerNodePath(mesh, modelRoot, node.id);
1316
- const meshSlotBindings = resolveProjectionSlotBindings(slotBindings, ownerNodePath, node.id);
1317
- for (const slotBinding of meshSlotBindings) {
1318
- const target = resolveProjectionSlotMaterialTarget(mesh, materialCounts, slotBinding.slot, ownerNodePath, {
1483
+ const meshSlotBindingMatches = resolveProjectionSlotBindingMatches(slotBindings, ownerNodePath, node.id);
1484
+ for (const { binding: slotBinding, matchKind } of meshSlotBindingMatches) {
1485
+ unmatchedSlotBindings.delete(slotBinding);
1486
+ if (!readProjectionWritableMeshMaterial(mesh)) {
1487
+ emitProjectionSlotMaterialProfileDiagnostic(options, node.id, slotBinding, {
1488
+ applied: false,
1489
+ matchKind,
1490
+ meshOwnerNodePath: ownerNodePath,
1491
+ targetMaterialStatus: 'missingMaterial',
1492
+ warningMessage: 'Slot material profile was not applied because the matched mesh has no target material.',
1493
+ });
1494
+ continue;
1495
+ }
1496
+ const resolution = resolveProjectionSlotMaterialTarget(mesh, materialCounts, slotBinding.slot, ownerNodePath, {
1497
+ projectionNodeId: node.id,
1319
1498
  primitiveSingleMaterialFallbackAllowed: isProjectionSinglePrimitiveMaterialSlot(slotBinding.slot, assetMaterialSlots),
1320
1499
  });
1321
- if (!target)
1500
+ const targetMaterial = resolution.target?.material ?? readProjectionWritableMeshMaterial(mesh);
1501
+ if (!resolution.target) {
1502
+ emitProjectionSlotMaterialProfileDiagnostic(options, node.id, slotBinding, {
1503
+ applied: false,
1504
+ matchKind,
1505
+ meshOwnerNodePath: ownerNodePath,
1506
+ targetMaterialStatus: resolution.targetMaterialStatus,
1507
+ targetMaterialName: readProjectionMaterialName(targetMaterial),
1508
+ targetMaterialType: readProjectionMaterialType(targetMaterial),
1509
+ warningMessage: resolution.warningMessage,
1510
+ });
1322
1511
  continue;
1512
+ }
1513
+ const target = resolution.target;
1323
1514
  clonedMaterials.push(...target.clonedMaterials);
1324
1515
  applyProjectionArtistMaterialProfile(options, target.material, slotBinding.profile);
1516
+ emitProjectionSlotMaterialProfileDiagnostic(options, node.id, slotBinding, {
1517
+ applied: true,
1518
+ matchKind,
1519
+ meshOwnerNodePath: ownerNodePath,
1520
+ targetMaterialStatus: resolution.targetMaterialStatus,
1521
+ targetMaterialName: readProjectionMaterialName(target.material),
1522
+ targetMaterialType: readProjectionMaterialType(target.material),
1523
+ });
1325
1524
  }
1326
1525
  }
1526
+ for (const slotBinding of unmatchedSlotBindings) {
1527
+ emitProjectionSlotMaterialProfileDiagnostic(options, node.id, slotBinding, {
1528
+ applied: false,
1529
+ matchKind: 'none',
1530
+ meshOwnerNodePath: null,
1531
+ targetMaterialStatus: 'missingMeshOwner',
1532
+ warningMessage: 'Slot material profile was not applied because no imported mesh matched its owner path.',
1533
+ });
1534
+ }
1327
1535
  return clonedMaterials;
1328
1536
  }
1329
1537
  function applyProjectionArtistMaterialProfile(options, material, profile) {
@@ -1334,6 +1542,40 @@ function applyProjectionArtistMaterialProfile(options, material, profile) {
1334
1542
  resolveTextureUrl: options.resolveTextureUrl,
1335
1543
  });
1336
1544
  }
1545
+ function emitProjectionSlotMaterialProfileDiagnostic(options, projectionNodeId, slotBinding, diagnostic) {
1546
+ emitProjectionMaterialProfileDiagnostic(options, {
1547
+ projectionNodeId,
1548
+ source: slotBinding.source,
1549
+ slotId: slotBinding.slot.slotId,
1550
+ ownerNodePath: slotBinding.slot.ownerNodePath,
1551
+ primitiveIndex: slotBinding.slot.primitiveIndex,
1552
+ ...diagnostic,
1553
+ });
1554
+ }
1555
+ function emitProjectionMaterialProfileDiagnostic(options, diagnostic) {
1556
+ options.onMaterialProfileDiagnostic?.(diagnostic);
1557
+ if (!diagnostic.warningMessage)
1558
+ return;
1559
+ options.logger?.warn?.('[BabylonEditorProjection] Material profile diagnostic warning', {
1560
+ projectionNodeId: diagnostic.projectionNodeId,
1561
+ source: diagnostic.source,
1562
+ slotId: diagnostic.slotId,
1563
+ ownerNodePath: diagnostic.ownerNodePath,
1564
+ meshOwnerNodePath: diagnostic.meshOwnerNodePath,
1565
+ matchKind: diagnostic.matchKind,
1566
+ targetMaterialStatus: diagnostic.targetMaterialStatus,
1567
+ warningMessage: diagnostic.warningMessage,
1568
+ });
1569
+ }
1570
+ function readProjectionMaterialName(material) {
1571
+ const name = material?.name;
1572
+ return typeof name === 'string' && name.trim() ? name.trim() : undefined;
1573
+ }
1574
+ function readProjectionMaterialType(material) {
1575
+ const getClassName = material?.getClassName;
1576
+ const className = typeof getClassName === 'function' ? getClassName.call(material) : null;
1577
+ return typeof className === 'string' && className.trim() ? className.trim() : className;
1578
+ }
1337
1579
  function createProjectionMaterialSlotBindings(node) {
1338
1580
  const bindings = [];
1339
1581
  const occupiedOwnerPaths = new Set();
@@ -1342,14 +1584,14 @@ function createProjectionMaterialSlotBindings(node) {
1342
1584
  const slot = createProjectionMaterialSlotDescriptor(binding?.slot, node.id);
1343
1585
  if (!profile || !slot)
1344
1586
  continue;
1345
- bindings.push({ slot, profile });
1587
+ bindings.push({ slot, profile, source: 'slotBinding' });
1346
1588
  occupiedOwnerPaths.add(slot.matchKey);
1347
1589
  }
1348
1590
  for (const [ownerNodePath, profile] of Object.entries(node.artistMaterialSlotProfiles ?? {})) {
1349
1591
  const slot = createProjectionMaterialSlotDescriptor({ ownerNodePath }, node.id);
1350
1592
  if (!profile || !slot || occupiedOwnerPaths.has(slot.matchKey))
1351
1593
  continue;
1352
- bindings.push({ slot, profile });
1594
+ bindings.push({ slot, profile, source: 'legacySlotProfile' });
1353
1595
  occupiedOwnerPaths.add(slot.matchKey);
1354
1596
  }
1355
1597
  return bindings;
@@ -1422,7 +1664,7 @@ function readProjectionMaterialSlotBoundsTuple(value) {
1422
1664
  return undefined;
1423
1665
  return tuple;
1424
1666
  }
1425
- function resolveProjectionSlotBindings(slotBindings, ownerNodePath, projectionNodeId) {
1667
+ function resolveProjectionSlotBindingMatches(slotBindings, ownerNodePath, projectionNodeId) {
1426
1668
  if (ownerNodePath == null)
1427
1669
  return [];
1428
1670
  const normalized = normalizeProjectionOwnerNodePath(ownerNodePath, { projectionNodeId });
@@ -1432,7 +1674,7 @@ function resolveProjectionSlotBindings(slotBindings, ownerNodePath, projectionNo
1432
1674
  let descendantMatchLength = -1;
1433
1675
  for (const binding of slotBindings) {
1434
1676
  if (binding.slot.matchKey === normalizedMatchKey) {
1435
- exactMatches.push(binding);
1677
+ exactMatches.push({ binding, matchKind: 'exact' });
1436
1678
  continue;
1437
1679
  }
1438
1680
  if (isMaterialSlotOwnerPathMatch(normalized, binding.slot.normalizedOwnerNodePath, { projectionNodeId })) {
@@ -1440,8 +1682,9 @@ function resolveProjectionSlotBindings(slotBindings, ownerNodePath, projectionNo
1440
1682
  descendantMatches.length = 0;
1441
1683
  descendantMatchLength = binding.slot.matchKey.length;
1442
1684
  }
1443
- if (binding.slot.matchKey.length === descendantMatchLength)
1444
- descendantMatches.push(binding);
1685
+ if (binding.slot.matchKey.length === descendantMatchLength) {
1686
+ descendantMatches.push({ binding, matchKind: 'descendant' });
1687
+ }
1445
1688
  }
1446
1689
  }
1447
1690
  return exactMatches.length > 0 ? exactMatches : descendantMatches;
@@ -1878,7 +2121,7 @@ function readProjectionAssetMaterialSlotString(value) {
1878
2121
  function countProjectionMeshMaterials(meshes) {
1879
2122
  const counts = new Map();
1880
2123
  for (const mesh of meshes) {
1881
- const material = mesh?.material;
2124
+ const material = readProjectionWritableMeshMaterial(mesh);
1882
2125
  if (!material)
1883
2126
  continue;
1884
2127
  counts.set(material, (counts.get(material) ?? 0) + 1);
@@ -1900,66 +2143,138 @@ function isProjectionMaterialSlotSameMesh(left, right) {
1900
2143
  return true;
1901
2144
  }
1902
2145
  function detachProjectionSlotMaterial(mesh, materialCounts, ownerNodePath) {
1903
- const material = mesh?.material;
1904
- if (!material || typeof material !== 'object')
1905
- return material;
2146
+ const materialOwner = resolveProjectionWritableMaterialOwner(mesh);
2147
+ const material = materialOwner?.material;
2148
+ if (!material)
2149
+ return null;
2150
+ const projectionOwnsMaterialLifecycle = materialOwner === mesh;
2151
+ if (typeof material !== 'object') {
2152
+ return { material, clonedMaterials: [], projectionOwnsMaterialLifecycle };
2153
+ }
1906
2154
  const materialUseCount = materialCounts.get(material) ?? 0;
1907
2155
  if (materialUseCount <= 1)
1908
- return material;
2156
+ return { material, clonedMaterials: [], projectionOwnsMaterialLifecycle };
1909
2157
  const clone = typeof material.clone === 'function'
1910
2158
  ? material.clone(createProjectionSlotMaterialName(material, ownerNodePath))
1911
2159
  : null;
1912
2160
  if (!clone)
1913
2161
  return null;
1914
2162
  cloneProjectionSubMaterials(clone, material, ownerNodePath);
1915
- mesh.material = clone;
2163
+ materialOwner.material = clone;
1916
2164
  materialCounts.set(material, materialUseCount - 1);
1917
2165
  materialCounts.set(clone, 1);
1918
- return clone;
2166
+ return {
2167
+ material: clone,
2168
+ clonedMaterials: projectionOwnsMaterialLifecycle ? [clone] : [],
2169
+ projectionOwnsMaterialLifecycle,
2170
+ };
1919
2171
  }
1920
2172
  function resolveProjectionSlotMaterialTarget(mesh, materialCounts, slot, ownerNodePath, options) {
1921
- const material = mesh?.material;
1922
- if (!material)
1923
- return null;
2173
+ const material = readProjectionWritableMeshMaterial(mesh);
2174
+ if (!material) {
2175
+ return {
2176
+ target: null,
2177
+ targetMaterialStatus: 'missingMaterial',
2178
+ warningMessage: 'Slot material profile was not applied because the matched mesh has no target material.',
2179
+ };
2180
+ }
1924
2181
  if (slot.primitiveIndex == null) {
1925
- const slotMaterial = detachProjectionSlotMaterial(mesh, materialCounts, ownerNodePath);
1926
- if (!slotMaterial)
1927
- return null;
2182
+ const detachResult = detachProjectionSlotMaterial(mesh, materialCounts, ownerNodePath);
2183
+ if (!detachResult) {
2184
+ return {
2185
+ target: null,
2186
+ targetMaterialStatus: 'missingTargetMaterial',
2187
+ warningMessage: 'Slot material profile was not applied because the mesh material could not be detached.',
2188
+ };
2189
+ }
1928
2190
  return {
1929
- material: slotMaterial,
1930
- clonedMaterials: slotMaterial !== material ? [slotMaterial] : [],
2191
+ target: {
2192
+ material: detachResult.material,
2193
+ clonedMaterials: detachResult.clonedMaterials,
2194
+ },
2195
+ targetMaterialStatus: 'material',
1931
2196
  };
1932
2197
  }
1933
2198
  if (!Array.isArray(material.subMaterials)) {
1934
- if (slot.primitiveIndex !== 0 || !options.primitiveSingleMaterialFallbackAllowed)
1935
- return null;
1936
- const slotMaterial = detachProjectionSlotMaterial(mesh, materialCounts, ownerNodePath);
1937
- if (!slotMaterial)
1938
- return null;
2199
+ const isSplitPrimitiveOwner = isProjectionRuntimeSplitPrimitiveOwnerNode(mesh, slot);
2200
+ if (!isSplitPrimitiveOwner && (slot.primitiveIndex !== 0 || !options.primitiveSingleMaterialFallbackAllowed)) {
2201
+ return {
2202
+ target: null,
2203
+ targetMaterialStatus: 'missingSubMaterial',
2204
+ warningMessage: 'Primitive slot material profile was not applied because the target mesh does not expose a matching subMaterial.',
2205
+ };
2206
+ }
2207
+ const detachResult = detachProjectionSlotMaterial(mesh, materialCounts, ownerNodePath);
2208
+ if (!detachResult) {
2209
+ return {
2210
+ target: null,
2211
+ targetMaterialStatus: 'missingTargetMaterial',
2212
+ warningMessage: 'Primitive slot material profile was not applied because the fallback mesh material could not be detached.',
2213
+ };
2214
+ }
2215
+ return {
2216
+ target: {
2217
+ material: detachResult.material,
2218
+ clonedMaterials: detachResult.clonedMaterials,
2219
+ },
2220
+ targetMaterialStatus: 'primitiveFallbackMaterial',
2221
+ };
2222
+ }
2223
+ const detachResult = detachProjectionSlotMaterial(mesh, materialCounts, ownerNodePath);
2224
+ const slotMaterial = detachResult?.material;
2225
+ if (!detachResult || !Array.isArray(slotMaterial.subMaterials)) {
1939
2226
  return {
1940
- material: slotMaterial,
1941
- clonedMaterials: slotMaterial !== material ? [slotMaterial] : [],
2227
+ target: null,
2228
+ targetMaterialStatus: 'missingTargetMaterial',
2229
+ warningMessage: 'Primitive slot material profile was not applied because the multi-material target could not be detached.',
1942
2230
  };
1943
2231
  }
1944
- const slotMaterial = detachProjectionSlotMaterial(mesh, materialCounts, ownerNodePath);
1945
- if (!slotMaterial || !Array.isArray(slotMaterial.subMaterials))
1946
- return null;
1947
2232
  const subMaterials = slotMaterial.subMaterials;
1948
2233
  const subMaterial = subMaterials[slot.primitiveIndex];
1949
- if (!subMaterial || typeof subMaterial !== 'object' || typeof subMaterial.clone !== 'function')
1950
- return null;
2234
+ if (!subMaterial || typeof subMaterial !== 'object' || typeof subMaterial.clone !== 'function') {
2235
+ return {
2236
+ target: null,
2237
+ targetMaterialStatus: 'missingSubMaterial',
2238
+ warningMessage: 'Primitive slot material profile was not applied because the target subMaterial is missing or cannot be cloned.',
2239
+ };
2240
+ }
1951
2241
  const clonedSubMaterial = subMaterial.clone(createProjectionSlotSubMaterialName(subMaterial, ownerNodePath, slot.primitiveIndex));
1952
- if (!clonedSubMaterial)
1953
- return null;
2242
+ if (!clonedSubMaterial) {
2243
+ return {
2244
+ target: null,
2245
+ targetMaterialStatus: 'missingSubMaterial',
2246
+ warningMessage: 'Primitive slot material profile was not applied because the target subMaterial clone failed.',
2247
+ };
2248
+ }
1954
2249
  subMaterials[slot.primitiveIndex] = clonedSubMaterial;
1955
2250
  return {
1956
- material: clonedSubMaterial,
1957
- clonedMaterials: [
1958
- ...(slotMaterial !== material ? [slotMaterial] : []),
1959
- clonedSubMaterial,
1960
- ],
2251
+ target: {
2252
+ material: clonedSubMaterial,
2253
+ clonedMaterials: [
2254
+ ...detachResult.clonedMaterials,
2255
+ ...(detachResult.projectionOwnsMaterialLifecycle ? [clonedSubMaterial] : []),
2256
+ ],
2257
+ },
2258
+ targetMaterialStatus: 'subMaterial',
1961
2259
  };
1962
2260
  }
2261
+ function readProjectionWritableMeshMaterial(mesh) {
2262
+ return resolveProjectionWritableMaterialOwner(mesh)?.material ?? null;
2263
+ }
2264
+ function resolveProjectionWritableMaterialOwner(mesh) {
2265
+ if (!mesh || typeof mesh !== 'object')
2266
+ return null;
2267
+ if (isProjectionInstancedMesh(mesh) && mesh.sourceMesh)
2268
+ return mesh.sourceMesh;
2269
+ return mesh;
2270
+ }
2271
+ function isProjectionInstancedMesh(mesh) {
2272
+ if (!mesh || typeof mesh !== 'object')
2273
+ return false;
2274
+ const className = typeof mesh.getClassName === 'function' ? mesh.getClassName() : undefined;
2275
+ return className === 'InstancedMesh'
2276
+ || (mesh.constructor?.name === 'InstancedMesh' && !!mesh.sourceMesh);
2277
+ }
1963
2278
  function cloneProjectionSubMaterials(clone, source, ownerNodePath) {
1964
2279
  if (!Array.isArray(clone?.subMaterials) || !Array.isArray(source?.subMaterials))
1965
2280
  return;