@fps-games/editor 0.1.7-beta.4 → 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.
- package/dist/agent-bridge/capabilities/assets.d.ts.map +1 -1
- package/dist/agent-bridge/capabilities/assets.js +12 -7
- package/dist/agent-bridge/capabilities/assets.js.map +1 -1
- package/dist/agent-bridge/capabilities/host-lifecycle.d.ts +1 -1
- package/dist/agent-bridge/capabilities/host-lifecycle.d.ts.map +1 -1
- package/dist/agent-bridge/capabilities/host-lifecycle.js +4 -2
- package/dist/agent-bridge/capabilities/host-lifecycle.js.map +1 -1
- package/dist/agent-bridge/capabilities/prefabs.js +50 -0
- package/dist/agent-bridge/capabilities/prefabs.js.map +1 -1
- package/dist/agent-bridge/capability-sources.d.ts.map +1 -1
- package/dist/agent-bridge/capability-sources.js +4 -1
- package/dist/agent-bridge/capability-sources.js.map +1 -1
- package/dist/build-info.json +15 -15
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/local-editor-harness.d.ts +4 -0
- package/dist/local-editor-harness.d.ts.map +1 -1
- package/dist/local-editor-harness.js +146 -54
- package/dist/local-editor-harness.js.map +1 -1
- package/dist/local-editor-performance-stats.d.ts.map +1 -1
- package/dist/local-editor-performance-stats.js +7 -0
- package/dist/local-editor-performance-stats.js.map +1 -1
- package/dist/playable-local-editor-host.d.ts +5 -1
- package/dist/playable-local-editor-host.d.ts.map +1 -1
- package/dist/playable-local-editor-host.js +75 -1
- package/dist/playable-local-editor-host.js.map +1 -1
- package/node_modules/@fps-games/babylon-renderer/package.json +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/inspector-host.d.ts +2 -0
- package/node_modules/@fps-games/editor-babylon/dist/inspector-host.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/inspector-host.js +1 -0
- package/node_modules/@fps-games/editor-babylon/dist/inspector-host.js.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/material-property-adapter.d.ts +6 -2
- package/node_modules/@fps-games/editor-babylon/dist/material-property-adapter.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/material-property-adapter.js +94 -24
- package/node_modules/@fps-games/editor-babylon/dist/material-property-adapter.js.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/material-slot-source-resolver.d.ts +6 -2
- package/node_modules/@fps-games/editor-babylon/dist/material-slot-source-resolver.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/material-slot-source-resolver.js +7 -0
- package/node_modules/@fps-games/editor-babylon/dist/material-slot-source-resolver.js.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/monitor.d.ts +2 -0
- package/node_modules/@fps-games/editor-babylon/dist/monitor.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/monitor.js +2 -0
- package/node_modules/@fps-games/editor-babylon/dist/monitor.js.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/performance-monitor.d.ts +14 -0
- package/node_modules/@fps-games/editor-babylon/dist/performance-monitor.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/performance-monitor.js +121 -2
- package/node_modules/@fps-games/editor-babylon/dist/performance-monitor.js.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/projection-asset-container-importer.js +1 -5
- package/node_modules/@fps-games/editor-babylon/dist/projection-asset-container-importer.js.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/projection.d.ts +19 -0
- package/node_modules/@fps-games/editor-babylon/dist/projection.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/projection.js +882 -43
- package/node_modules/@fps-games/editor-babylon/dist/projection.js.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/shadow-preview-controller.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/shadow-preview-controller.js +96 -35
- package/node_modules/@fps-games/editor-babylon/dist/shadow-preview-controller.js.map +1 -1
- package/node_modules/@fps-games/editor-babylon/package.json +5 -5
- package/node_modules/@fps-games/editor-browser/dist/index.d.ts +3 -1
- package/node_modules/@fps-games/editor-browser/dist/index.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/index.js +1 -0
- package/node_modules/@fps-games/editor-browser/dist/index.js.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-modal-dialog.d.ts +26 -0
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-modal-dialog.d.ts.map +1 -0
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-modal-dialog.js +187 -0
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-modal-dialog.js.map +1 -0
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.js +102 -2
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.js.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts +17 -0
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js +84 -0
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js.map +1 -1
- package/node_modules/@fps-games/editor-browser/package.json +3 -3
- package/node_modules/@fps-games/editor-core/package.json +2 -2
- package/node_modules/@fps-games/editor-forge-play/package.json +2 -2
- package/node_modules/@fps-games/editor-playable-sdk/dist/asset-database/metadata.js +198 -53
- package/node_modules/@fps-games/editor-playable-sdk/dist/asset-database/metadata.js.map +1 -1
- package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/forge-play-types.d.ts +4 -1
- package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/forge-play-types.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/forge-play.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/forge-play.js +89 -9
- package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/forge-play.js.map +1 -1
- package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/host-controller.d.ts +4 -1
- package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/host-controller.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/host-controller.js +60 -5
- package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/host-controller.js.map +1 -1
- package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/index.d.ts +1 -1
- package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/index.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/index.js.map +1 -1
- package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/lifecycle.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/lifecycle.js +62 -2
- package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/lifecycle.js.map +1 -1
- package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/types.d.ts +27 -0
- package/node_modules/@fps-games/editor-playable-sdk/dist/local-harness/types.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-playable-sdk/dist/material-binding/binding.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-playable-sdk/dist/material-binding/binding.js +29 -0
- package/node_modules/@fps-games/editor-playable-sdk/dist/material-binding/binding.js.map +1 -1
- package/node_modules/@fps-games/editor-playable-sdk/dist/runtime-preview/nodes.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-playable-sdk/dist/runtime-preview/nodes.js +34 -1
- package/node_modules/@fps-games/editor-playable-sdk/dist/runtime-preview/nodes.js.map +1 -1
- package/node_modules/@fps-games/editor-playable-sdk/dist/scene-compiler/compiler.js +6 -2
- package/node_modules/@fps-games/editor-playable-sdk/dist/scene-compiler/compiler.js.map +1 -1
- package/node_modules/@fps-games/editor-playable-sdk/dist/scene-source/driver.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-playable-sdk/dist/scene-source/driver.js +57 -3
- package/node_modules/@fps-games/editor-playable-sdk/dist/scene-source/driver.js.map +1 -1
- package/node_modules/@fps-games/editor-playable-sdk/dist/scene-source/types.d.ts +30 -0
- package/node_modules/@fps-games/editor-playable-sdk/dist/scene-source/types.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-playable-sdk/dist/session/hierarchy.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-playable-sdk/dist/session/hierarchy.js +18 -2
- package/node_modules/@fps-games/editor-playable-sdk/dist/session/hierarchy.js.map +1 -1
- package/node_modules/@fps-games/editor-playable-sdk/dist/session/patches.d.ts +1 -0
- package/node_modules/@fps-games/editor-playable-sdk/dist/session/patches.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-playable-sdk/dist/session/patches.js +1 -0
- package/node_modules/@fps-games/editor-playable-sdk/dist/session/patches.js.map +1 -1
- package/node_modules/@fps-games/editor-playable-sdk/package.json +3 -3
- package/node_modules/@fps-games/editor-protocol/dist/agent-bridge.d.ts +13 -0
- package/node_modules/@fps-games/editor-protocol/dist/agent-bridge.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-protocol/dist/agent-bridge.js +29 -0
- package/node_modules/@fps-games/editor-protocol/dist/agent-bridge.js.map +1 -1
- package/node_modules/@fps-games/editor-protocol/dist/index.d.ts +6 -1
- package/node_modules/@fps-games/editor-protocol/dist/index.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-protocol/dist/index.js.map +1 -1
- package/node_modules/@fps-games/editor-protocol/package.json +1 -1
- 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
|
|
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;
|
|
@@ -790,6 +818,7 @@ function attachImportedProjectionResult(options, node, asset, projection, result
|
|
|
790
818
|
},
|
|
791
819
|
};
|
|
792
820
|
applyAssetDefaultsTransform(babylon, modelRoot, asset.transform);
|
|
821
|
+
collapseZeroVertexProjectionWrapperMeshes(babylon, scene, result);
|
|
793
822
|
const importedNodes = [
|
|
794
823
|
...(result.transformNodes ?? []),
|
|
795
824
|
...(result.meshes ?? []),
|
|
@@ -810,20 +839,59 @@ function attachImportedProjectionResult(options, node, asset, projection, result
|
|
|
810
839
|
},
|
|
811
840
|
};
|
|
812
841
|
}
|
|
842
|
+
const materialSlots = readProjectionAssetMaterialSlots(asset, node.id);
|
|
843
|
+
const ownerNodePathByMesh = new Map();
|
|
844
|
+
const ownerMeshCountByKey = new Map();
|
|
845
|
+
for (const mesh of result.meshes ?? []) {
|
|
846
|
+
const ownerNodePath = buildProjectionOwnerNodePath(mesh, modelRoot, node.id);
|
|
847
|
+
if (!ownerNodePath)
|
|
848
|
+
continue;
|
|
849
|
+
ownerNodePathByMesh.set(mesh, ownerNodePath);
|
|
850
|
+
const ownerKey = createMaterialSlotOwnerPathMatchKey(ownerNodePath, { projectionNodeId: node.id });
|
|
851
|
+
ownerMeshCountByKey.set(ownerKey, (ownerMeshCountByKey.get(ownerKey) ?? 0) + 1);
|
|
852
|
+
}
|
|
853
|
+
const splitPrimitiveOrdinalByOwnerKey = new Map();
|
|
854
|
+
const primitiveBoundsProxyOwnerKeys = new Set();
|
|
813
855
|
for (const mesh of result.meshes ?? []) {
|
|
814
856
|
mesh.isPickable = true;
|
|
815
857
|
applyProjectionRenderingToMesh(node, mesh);
|
|
816
|
-
const ownerNodePath =
|
|
858
|
+
const ownerNodePath = ownerNodePathByMesh.get(mesh) ?? null;
|
|
817
859
|
if (ownerNodePath) {
|
|
860
|
+
const ownerKey = createMaterialSlotOwnerPathMatchKey(ownerNodePath, { projectionNodeId: node.id });
|
|
861
|
+
const ownerMeshCount = ownerMeshCountByKey.get(ownerKey) ?? 0;
|
|
862
|
+
const splitPrimitiveSlot = resolveProjectionSplitPrimitiveMeshSelectionSlot(mesh, materialSlots, ownerNodePath, node.id, ownerMeshCount, splitPrimitiveOrdinalByOwnerKey);
|
|
863
|
+
const primitiveSelectionMeshes = createProjectionAssetMeshSelectionProxies(options, node, projection, mesh, ownerNodePath, materialSlots);
|
|
864
|
+
const primitiveBoundsSelectionMeshes = !splitPrimitiveSlot
|
|
865
|
+
&& primitiveSelectionMeshes.length === 0
|
|
866
|
+
&& !primitiveBoundsProxyOwnerKeys.has(ownerKey)
|
|
867
|
+
? createProjectionPrimitiveBoundsSelectionProxies(options, node, projection, mesh, ownerNodePath, materialSlots, ownerMeshCount)
|
|
868
|
+
: [];
|
|
869
|
+
if (primitiveBoundsSelectionMeshes.length > 0)
|
|
870
|
+
primitiveBoundsProxyOwnerKeys.add(ownerKey);
|
|
871
|
+
const usesSelectionMeshes = primitiveSelectionMeshes.length > 0
|
|
872
|
+
|| primitiveBoundsSelectionMeshes.length > 0
|
|
873
|
+
|| primitiveBoundsProxyOwnerKeys.has(ownerKey);
|
|
818
874
|
const selectionKey = resolveProjectionAssetMeshSelectionKey(asset, ownerNodePath, node.id);
|
|
819
875
|
mesh.metadata = {
|
|
820
876
|
...(mesh.metadata ?? {}),
|
|
821
877
|
editorProjection: {
|
|
822
|
-
nodeId:
|
|
878
|
+
nodeId: splitPrimitiveSlot
|
|
879
|
+
? createAssetMeshProjectionNodeId(node.id, splitPrimitiveSlot.key)
|
|
880
|
+
: usesSelectionMeshes
|
|
881
|
+
? node.id
|
|
882
|
+
: createAssetMeshProjectionNodeId(node.id, selectionKey),
|
|
823
883
|
rootNodeId: node.id,
|
|
824
884
|
sourceId: asset.sourceId,
|
|
825
885
|
ownerNodePath,
|
|
826
|
-
...(
|
|
886
|
+
...(splitPrimitiveSlot
|
|
887
|
+
? {
|
|
888
|
+
slotSelectionKey: splitPrimitiveSlot.key,
|
|
889
|
+
primitiveIndex: splitPrimitiveSlot.primitiveIndex,
|
|
890
|
+
}
|
|
891
|
+
: {}),
|
|
892
|
+
...(!splitPrimitiveSlot && !usesSelectionMeshes && selectionKey !== ownerNodePath
|
|
893
|
+
? { slotSelectionKey: selectionKey }
|
|
894
|
+
: {}),
|
|
827
895
|
runtimeKind: 'assetMesh',
|
|
828
896
|
...createProjectionShadowMetadata(node),
|
|
829
897
|
},
|
|
@@ -843,6 +911,141 @@ function attachImportedProjectionResult(options, node, asset, projection, result
|
|
|
843
911
|
throw error;
|
|
844
912
|
}
|
|
845
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
|
+
}
|
|
846
1049
|
function createProjectionImportStructureSnapshot(node, asset, modelRoot, result) {
|
|
847
1050
|
const runtimeNodes = collectProjectionImportRuntimeNodes(modelRoot, result);
|
|
848
1051
|
const nodeIdByRuntimeNode = new Map();
|
|
@@ -1229,31 +1432,105 @@ function applyProjectionArtistMaterialProfilesToMeshes(options, node, modelRoot,
|
|
|
1229
1432
|
const materials = new Set();
|
|
1230
1433
|
if (hasRootProfile) {
|
|
1231
1434
|
for (const mesh of meshes) {
|
|
1232
|
-
const material = mesh
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
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);
|
|
1237
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
|
+
});
|
|
1238
1474
|
}
|
|
1239
1475
|
}
|
|
1240
1476
|
if (!hasSlotProfiles)
|
|
1241
1477
|
return clonedMaterials;
|
|
1242
1478
|
const materialCounts = countProjectionMeshMaterials(meshes);
|
|
1479
|
+
const assetMaterialSlots = node.asset ? readProjectionAssetMaterialSlots(node.asset, node.id) : [];
|
|
1480
|
+
const unmatchedSlotBindings = new Set(slotBindings);
|
|
1243
1481
|
for (const mesh of meshes) {
|
|
1244
|
-
const material = mesh?.material;
|
|
1245
|
-
if (!material)
|
|
1246
|
-
continue;
|
|
1247
1482
|
const ownerNodePath = buildProjectionOwnerNodePath(mesh, modelRoot, node.id);
|
|
1248
|
-
const
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
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,
|
|
1498
|
+
primitiveSingleMaterialFallbackAllowed: isProjectionSinglePrimitiveMaterialSlot(slotBinding.slot, assetMaterialSlots),
|
|
1499
|
+
});
|
|
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
|
+
});
|
|
1511
|
+
continue;
|
|
1512
|
+
}
|
|
1513
|
+
const target = resolution.target;
|
|
1514
|
+
clonedMaterials.push(...target.clonedMaterials);
|
|
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
|
+
});
|
|
1524
|
+
}
|
|
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
|
+
});
|
|
1257
1534
|
}
|
|
1258
1535
|
return clonedMaterials;
|
|
1259
1536
|
}
|
|
@@ -1265,6 +1542,40 @@ function applyProjectionArtistMaterialProfile(options, material, profile) {
|
|
|
1265
1542
|
resolveTextureUrl: options.resolveTextureUrl,
|
|
1266
1543
|
});
|
|
1267
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
|
+
}
|
|
1268
1579
|
function createProjectionMaterialSlotBindings(node) {
|
|
1269
1580
|
const bindings = [];
|
|
1270
1581
|
const occupiedOwnerPaths = new Set();
|
|
@@ -1273,14 +1584,14 @@ function createProjectionMaterialSlotBindings(node) {
|
|
|
1273
1584
|
const slot = createProjectionMaterialSlotDescriptor(binding?.slot, node.id);
|
|
1274
1585
|
if (!profile || !slot)
|
|
1275
1586
|
continue;
|
|
1276
|
-
bindings.push({ slot, profile });
|
|
1587
|
+
bindings.push({ slot, profile, source: 'slotBinding' });
|
|
1277
1588
|
occupiedOwnerPaths.add(slot.matchKey);
|
|
1278
1589
|
}
|
|
1279
1590
|
for (const [ownerNodePath, profile] of Object.entries(node.artistMaterialSlotProfiles ?? {})) {
|
|
1280
1591
|
const slot = createProjectionMaterialSlotDescriptor({ ownerNodePath }, node.id);
|
|
1281
1592
|
if (!profile || !slot || occupiedOwnerPaths.has(slot.matchKey))
|
|
1282
1593
|
continue;
|
|
1283
|
-
bindings.push({ slot, profile });
|
|
1594
|
+
bindings.push({ slot, profile, source: 'legacySlotProfile' });
|
|
1284
1595
|
occupiedOwnerPaths.add(slot.matchKey);
|
|
1285
1596
|
}
|
|
1286
1597
|
return bindings;
|
|
@@ -1309,6 +1620,8 @@ function createProjectionMaterialSlotDescriptor(value, projectionNodeId) {
|
|
|
1309
1620
|
const materialNames = Array.isArray(value.materialNames)
|
|
1310
1621
|
? value.materialNames.filter((item) => typeof item === 'string' && !!item.trim()).map(item => item.trim())
|
|
1311
1622
|
: undefined;
|
|
1623
|
+
const triangleCount = Number.isFinite(value.triangleCount) ? value.triangleCount : undefined;
|
|
1624
|
+
const boundingBox = readProjectionMaterialSlotBoundingBox(value.boundingBox);
|
|
1312
1625
|
return {
|
|
1313
1626
|
...(slotId ? { slotId } : {}),
|
|
1314
1627
|
ownerNodePath,
|
|
@@ -1323,27 +1636,58 @@ function createProjectionMaterialSlotDescriptor(value, projectionNodeId) {
|
|
|
1323
1636
|
: {}),
|
|
1324
1637
|
...(materialName ? { materialName } : {}),
|
|
1325
1638
|
...(materialNames && materialNames.length > 0 ? { materialNames } : {}),
|
|
1639
|
+
...(triangleCount != null ? { triangleCount } : {}),
|
|
1640
|
+
...(boundingBox ? { boundingBox } : {}),
|
|
1326
1641
|
key: slotId || ownerNodePath,
|
|
1327
1642
|
normalizedOwnerNodePath: ownerNodePath,
|
|
1328
1643
|
matchKey: createMaterialSlotOwnerPathMatchKey(ownerNodePath, { projectionNodeId }),
|
|
1329
1644
|
};
|
|
1330
1645
|
}
|
|
1331
|
-
function
|
|
1646
|
+
function readProjectionMaterialSlotBoundingBox(value) {
|
|
1647
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
1648
|
+
return undefined;
|
|
1649
|
+
const record = value;
|
|
1650
|
+
const min = readProjectionMaterialSlotBoundsTuple(record.min);
|
|
1651
|
+
const max = readProjectionMaterialSlotBoundsTuple(record.max);
|
|
1652
|
+
if (!min && !max)
|
|
1653
|
+
return undefined;
|
|
1654
|
+
return {
|
|
1655
|
+
...(min ? { min } : {}),
|
|
1656
|
+
...(max ? { max } : {}),
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
function readProjectionMaterialSlotBoundsTuple(value) {
|
|
1660
|
+
if (!Array.isArray(value) || value.length < 3)
|
|
1661
|
+
return undefined;
|
|
1662
|
+
const tuple = value.slice(0, 3).map(item => Number(item));
|
|
1663
|
+
if (!tuple.every(item => Number.isFinite(item)))
|
|
1664
|
+
return undefined;
|
|
1665
|
+
return tuple;
|
|
1666
|
+
}
|
|
1667
|
+
function resolveProjectionSlotBindingMatches(slotBindings, ownerNodePath, projectionNodeId) {
|
|
1332
1668
|
if (ownerNodePath == null)
|
|
1333
|
-
return
|
|
1669
|
+
return [];
|
|
1334
1670
|
const normalized = normalizeProjectionOwnerNodePath(ownerNodePath, { projectionNodeId });
|
|
1335
1671
|
const normalizedMatchKey = createMaterialSlotOwnerPathMatchKey(normalized, { projectionNodeId });
|
|
1336
|
-
|
|
1672
|
+
const exactMatches = [];
|
|
1673
|
+
const descendantMatches = [];
|
|
1674
|
+
let descendantMatchLength = -1;
|
|
1337
1675
|
for (const binding of slotBindings) {
|
|
1338
|
-
if (binding.slot.matchKey === normalizedMatchKey)
|
|
1339
|
-
|
|
1676
|
+
if (binding.slot.matchKey === normalizedMatchKey) {
|
|
1677
|
+
exactMatches.push({ binding, matchKind: 'exact' });
|
|
1678
|
+
continue;
|
|
1679
|
+
}
|
|
1340
1680
|
if (isMaterialSlotOwnerPathMatch(normalized, binding.slot.normalizedOwnerNodePath, { projectionNodeId })) {
|
|
1341
|
-
if (
|
|
1342
|
-
|
|
1681
|
+
if (binding.slot.matchKey.length > descendantMatchLength) {
|
|
1682
|
+
descendantMatches.length = 0;
|
|
1683
|
+
descendantMatchLength = binding.slot.matchKey.length;
|
|
1684
|
+
}
|
|
1685
|
+
if (binding.slot.matchKey.length === descendantMatchLength) {
|
|
1686
|
+
descendantMatches.push({ binding, matchKind: 'descendant' });
|
|
1343
1687
|
}
|
|
1344
1688
|
}
|
|
1345
1689
|
}
|
|
1346
|
-
return
|
|
1690
|
+
return exactMatches.length > 0 ? exactMatches : descendantMatches;
|
|
1347
1691
|
}
|
|
1348
1692
|
function buildProjectionOwnerNodePath(node, rootNode, projectionNodeId) {
|
|
1349
1693
|
if (!node || !rootNode || node === rootNode)
|
|
@@ -1390,6 +1734,363 @@ function resolveProjectionAssetMeshSelectionKey(asset, ownerNodePath, projection
|
|
|
1390
1734
|
}
|
|
1391
1735
|
return bestSlot?.key ?? ownerNodePath;
|
|
1392
1736
|
}
|
|
1737
|
+
function resolveProjectionAssetMeshSelectionSlots(materialSlots, ownerNodePath, projectionNodeId) {
|
|
1738
|
+
const normalizedOwnerPath = normalizeProjectionOwnerNodePath(ownerNodePath, { projectionNodeId });
|
|
1739
|
+
const normalizedMatchKey = createMaterialSlotOwnerPathMatchKey(normalizedOwnerPath, { projectionNodeId });
|
|
1740
|
+
const exactMatches = [];
|
|
1741
|
+
const descendantMatches = [];
|
|
1742
|
+
let descendantMatchLength = -1;
|
|
1743
|
+
for (const slot of materialSlots) {
|
|
1744
|
+
if (slot.matchKey === normalizedMatchKey) {
|
|
1745
|
+
exactMatches.push(slot);
|
|
1746
|
+
continue;
|
|
1747
|
+
}
|
|
1748
|
+
if (isMaterialSlotOwnerPathMatch(normalizedOwnerPath, slot.normalizedOwnerNodePath, { projectionNodeId })) {
|
|
1749
|
+
if (slot.matchKey.length > descendantMatchLength) {
|
|
1750
|
+
descendantMatches.length = 0;
|
|
1751
|
+
descendantMatchLength = slot.matchKey.length;
|
|
1752
|
+
}
|
|
1753
|
+
if (slot.matchKey.length === descendantMatchLength)
|
|
1754
|
+
descendantMatches.push(slot);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
return exactMatches.length > 0 ? exactMatches : descendantMatches;
|
|
1758
|
+
}
|
|
1759
|
+
function createProjectionAssetMeshSelectionProxies(options, node, projection, mesh, ownerNodePath, materialSlots) {
|
|
1760
|
+
const primitiveSlots = resolveProjectionAssetMeshSelectionSlots(materialSlots, ownerNodePath, node.id)
|
|
1761
|
+
.filter(slot => Number.isInteger(slot.primitiveIndex));
|
|
1762
|
+
if (primitiveSlots.length <= 1)
|
|
1763
|
+
return [];
|
|
1764
|
+
const subMeshes = Array.isArray(mesh?.subMeshes) ? mesh.subMeshes : [];
|
|
1765
|
+
if (subMeshes.length <= 1)
|
|
1766
|
+
return [];
|
|
1767
|
+
const proxies = [];
|
|
1768
|
+
const createdNodeIds = new Set();
|
|
1769
|
+
for (const slot of primitiveSlots) {
|
|
1770
|
+
const primitiveIndex = slot.primitiveIndex;
|
|
1771
|
+
if (primitiveIndex == null || primitiveIndex < 0)
|
|
1772
|
+
continue;
|
|
1773
|
+
const subMesh = subMeshes[primitiveIndex];
|
|
1774
|
+
if (!subMesh)
|
|
1775
|
+
continue;
|
|
1776
|
+
const selectionNodeId = createAssetMeshProjectionNodeId(node.id, slot.key);
|
|
1777
|
+
if (createdNodeIds.has(selectionNodeId))
|
|
1778
|
+
continue;
|
|
1779
|
+
const proxy = createProjectionSubMeshSelectionProxy(options, node, mesh, subMesh, {
|
|
1780
|
+
selectionNodeId,
|
|
1781
|
+
ownerNodePath,
|
|
1782
|
+
slotSelectionKey: slot.key,
|
|
1783
|
+
primitiveIndex,
|
|
1784
|
+
});
|
|
1785
|
+
if (!proxy)
|
|
1786
|
+
continue;
|
|
1787
|
+
createdNodeIds.add(selectionNodeId);
|
|
1788
|
+
proxies.push(proxy);
|
|
1789
|
+
projection.selectionMeshes = projection.selectionMeshes ?? [];
|
|
1790
|
+
projection.selectionMeshes.push(proxy);
|
|
1791
|
+
if (proxy.material)
|
|
1792
|
+
projection.runtimeObjects.push(proxy.material);
|
|
1793
|
+
}
|
|
1794
|
+
return proxies;
|
|
1795
|
+
}
|
|
1796
|
+
function resolveProjectionSplitPrimitiveMeshSelectionSlot(mesh, materialSlots, ownerNodePath, projectionNodeId, ownerMeshCount, ordinalByOwnerKey) {
|
|
1797
|
+
const subMeshes = Array.isArray(mesh?.subMeshes) ? mesh.subMeshes : [];
|
|
1798
|
+
if (subMeshes.length > 1)
|
|
1799
|
+
return null;
|
|
1800
|
+
if ((mesh?.getTotalVertices?.() ?? 0) <= 0)
|
|
1801
|
+
return null;
|
|
1802
|
+
const primitiveSlots = resolveProjectionAssetMeshSelectionSlots(materialSlots, ownerNodePath, projectionNodeId)
|
|
1803
|
+
.filter((slot) => (Number.isInteger(slot.primitiveIndex)))
|
|
1804
|
+
.sort((left, right) => left.primitiveIndex - right.primitiveIndex);
|
|
1805
|
+
if (primitiveSlots.length <= 1)
|
|
1806
|
+
return null;
|
|
1807
|
+
if (ownerMeshCount < primitiveSlots.length)
|
|
1808
|
+
return null;
|
|
1809
|
+
const ownerKey = createMaterialSlotOwnerPathMatchKey(ownerNodePath, { projectionNodeId });
|
|
1810
|
+
const ordinal = ordinalByOwnerKey.get(ownerKey) ?? 0;
|
|
1811
|
+
ordinalByOwnerKey.set(ownerKey, ordinal + 1);
|
|
1812
|
+
const namedSlot = primitiveSlots.find(slot => isProjectionRuntimeSplitPrimitiveOwnerNode(mesh, slot));
|
|
1813
|
+
if (namedSlot)
|
|
1814
|
+
return namedSlot;
|
|
1815
|
+
return primitiveSlots.find(slot => slot.primitiveIndex === ordinal) ?? primitiveSlots[ordinal] ?? null;
|
|
1816
|
+
}
|
|
1817
|
+
function createProjectionPrimitiveBoundsSelectionProxies(options, node, projection, mesh, ownerNodePath, materialSlots, ownerMeshCount) {
|
|
1818
|
+
const subMeshes = Array.isArray(mesh?.subMeshes) ? mesh.subMeshes : [];
|
|
1819
|
+
if (subMeshes.length > 1)
|
|
1820
|
+
return [];
|
|
1821
|
+
const primitiveSlots = resolveProjectionAssetMeshSelectionSlots(materialSlots, ownerNodePath, node.id)
|
|
1822
|
+
.filter(slot => Number.isInteger(slot.primitiveIndex) && !!slot.boundingBox)
|
|
1823
|
+
.sort((left, right) => (left.primitiveIndex ?? 0) - (right.primitiveIndex ?? 0));
|
|
1824
|
+
if (primitiveSlots.length <= 1)
|
|
1825
|
+
return [];
|
|
1826
|
+
if (ownerMeshCount >= primitiveSlots.length)
|
|
1827
|
+
return [];
|
|
1828
|
+
const proxies = [];
|
|
1829
|
+
const createdNodeIds = new Set();
|
|
1830
|
+
for (const slot of primitiveSlots) {
|
|
1831
|
+
const primitiveIndex = slot.primitiveIndex;
|
|
1832
|
+
if (primitiveIndex == null || primitiveIndex < 0)
|
|
1833
|
+
continue;
|
|
1834
|
+
const selectionNodeId = createAssetMeshProjectionNodeId(node.id, slot.key);
|
|
1835
|
+
if (createdNodeIds.has(selectionNodeId))
|
|
1836
|
+
continue;
|
|
1837
|
+
const proxy = createProjectionPrimitiveBoundsSelectionProxy(options, node, mesh, slot, {
|
|
1838
|
+
selectionNodeId,
|
|
1839
|
+
ownerNodePath,
|
|
1840
|
+
slotSelectionKey: slot.key,
|
|
1841
|
+
primitiveIndex,
|
|
1842
|
+
});
|
|
1843
|
+
if (!proxy)
|
|
1844
|
+
continue;
|
|
1845
|
+
createdNodeIds.add(selectionNodeId);
|
|
1846
|
+
proxies.push(proxy);
|
|
1847
|
+
projection.selectionMeshes = projection.selectionMeshes ?? [];
|
|
1848
|
+
projection.selectionMeshes.push(proxy);
|
|
1849
|
+
if (proxy.material)
|
|
1850
|
+
projection.runtimeObjects.push(proxy.material);
|
|
1851
|
+
}
|
|
1852
|
+
return proxies;
|
|
1853
|
+
}
|
|
1854
|
+
function createProjectionPrimitiveBoundsSelectionProxy(options, node, sourceMesh, slot, metadata) {
|
|
1855
|
+
const { babylon, scene } = options;
|
|
1856
|
+
const MeshBuilder = babylon.MeshBuilder;
|
|
1857
|
+
const StandardMaterial = babylon.StandardMaterial;
|
|
1858
|
+
if (!MeshBuilder?.CreateBox || !StandardMaterial)
|
|
1859
|
+
return null;
|
|
1860
|
+
const bounds = readProjectionPrimitiveSlotBounds(slot);
|
|
1861
|
+
if (!bounds)
|
|
1862
|
+
return null;
|
|
1863
|
+
const size = {
|
|
1864
|
+
x: Math.max(Math.abs(bounds.max.x - bounds.min.x), MIN_SELECTION_BOUNDS_PROXY_SIZE),
|
|
1865
|
+
y: Math.max(Math.abs(bounds.max.y - bounds.min.y), MIN_SELECTION_BOUNDS_PROXY_SIZE),
|
|
1866
|
+
z: Math.max(Math.abs(bounds.max.z - bounds.min.z), MIN_SELECTION_BOUNDS_PROXY_SIZE),
|
|
1867
|
+
};
|
|
1868
|
+
const center = {
|
|
1869
|
+
x: (bounds.min.x + bounds.max.x) / 2,
|
|
1870
|
+
y: (bounds.min.y + bounds.max.y) / 2,
|
|
1871
|
+
z: (bounds.min.z + bounds.max.z) / 2,
|
|
1872
|
+
};
|
|
1873
|
+
const proxy = MeshBuilder.CreateBox(`${sourceMesh.name ?? node.id}.selectionBoundsProxy.${sanitizeProjectionImportSnapshotId(metadata.slotSelectionKey)}`, {
|
|
1874
|
+
width: size.x,
|
|
1875
|
+
height: size.y,
|
|
1876
|
+
depth: size.z,
|
|
1877
|
+
}, scene);
|
|
1878
|
+
if (!proxy)
|
|
1879
|
+
return null;
|
|
1880
|
+
proxy.parent = sourceMesh;
|
|
1881
|
+
proxy.position.x = center.x;
|
|
1882
|
+
proxy.position.y = center.y;
|
|
1883
|
+
proxy.position.z = center.z;
|
|
1884
|
+
proxy.isPickable = false;
|
|
1885
|
+
proxy.metadata = {
|
|
1886
|
+
...(proxy.metadata ?? {}),
|
|
1887
|
+
disablePlanarShadow: true,
|
|
1888
|
+
disableBlobShadow: true,
|
|
1889
|
+
disableStaticProjectedShadow: true,
|
|
1890
|
+
editorProjectionSelectionProxy: true,
|
|
1891
|
+
editorProjectionSelectionBoundsProxy: true,
|
|
1892
|
+
editorProjection: {
|
|
1893
|
+
nodeId: metadata.selectionNodeId,
|
|
1894
|
+
rootNodeId: node.id,
|
|
1895
|
+
sourceId: node.asset?.sourceId,
|
|
1896
|
+
ownerNodePath: metadata.ownerNodePath,
|
|
1897
|
+
slotSelectionKey: metadata.slotSelectionKey,
|
|
1898
|
+
primitiveIndex: metadata.primitiveIndex,
|
|
1899
|
+
runtimeKind: 'assetMeshSelectionProxy',
|
|
1900
|
+
...createProjectionShadowMetadata(node),
|
|
1901
|
+
},
|
|
1902
|
+
};
|
|
1903
|
+
const material = new StandardMaterial(`${proxy.name}.material`, scene);
|
|
1904
|
+
material.alpha = 0;
|
|
1905
|
+
material.disableLighting = true;
|
|
1906
|
+
material.backFaceCulling = false;
|
|
1907
|
+
if ('disableColorWrite' in material) {
|
|
1908
|
+
material.disableColorWrite = true;
|
|
1909
|
+
}
|
|
1910
|
+
proxy.material = material;
|
|
1911
|
+
return proxy;
|
|
1912
|
+
}
|
|
1913
|
+
function readProjectionPrimitiveSlotBounds(slot) {
|
|
1914
|
+
const min = readProjectionMaterialSlotBoundsVec3(slot.boundingBox?.min);
|
|
1915
|
+
const max = readProjectionMaterialSlotBoundsVec3(slot.boundingBox?.max);
|
|
1916
|
+
if (!min || !max)
|
|
1917
|
+
return null;
|
|
1918
|
+
return { min, max };
|
|
1919
|
+
}
|
|
1920
|
+
function readProjectionMaterialSlotBoundsVec3(value) {
|
|
1921
|
+
if (!Array.isArray(value) || value.length < 3)
|
|
1922
|
+
return null;
|
|
1923
|
+
const [x, y, z] = value.map(item => Number(item));
|
|
1924
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z))
|
|
1925
|
+
return null;
|
|
1926
|
+
return { x, y, z };
|
|
1927
|
+
}
|
|
1928
|
+
function isProjectionRuntimeSplitPrimitiveOwnerNode(ownerNode, slot) {
|
|
1929
|
+
if (slot.primitiveIndex == null)
|
|
1930
|
+
return false;
|
|
1931
|
+
const primitiveNames = new Set();
|
|
1932
|
+
for (const baseName of [
|
|
1933
|
+
slot.ownerNodePath,
|
|
1934
|
+
readProjectionOwnerPathLastSegment(slot.ownerNodePath),
|
|
1935
|
+
stripProjectionPrimitiveLabelSuffix(readProjectionSlotLabel(slot)),
|
|
1936
|
+
readProjectionSlotLabel(slot),
|
|
1937
|
+
]) {
|
|
1938
|
+
addProjectionRuntimeOwnerName(primitiveNames, appendProjectionPrimitiveOwnerSuffix(baseName, slot.primitiveIndex));
|
|
1939
|
+
}
|
|
1940
|
+
return collectProjectionRuntimeNodeMatchNames(ownerNode).some(name => primitiveNames.has(name));
|
|
1941
|
+
}
|
|
1942
|
+
function collectProjectionRuntimeNodeMatchNames(node) {
|
|
1943
|
+
const rawNames = [
|
|
1944
|
+
typeof node?.name === 'string' ? node.name : null,
|
|
1945
|
+
typeof node?.id === 'string' ? node.id : null,
|
|
1946
|
+
readProjectionRuntimeNodeDisplayName(node),
|
|
1947
|
+
readProjectionSourceNodeSegment(node),
|
|
1948
|
+
];
|
|
1949
|
+
const names = new Set();
|
|
1950
|
+
for (const rawName of rawNames) {
|
|
1951
|
+
addProjectionRuntimeOwnerName(names, rawName);
|
|
1952
|
+
for (const tailName of collectProjectionRuntimeOwnerTailNames(rawName)) {
|
|
1953
|
+
addProjectionRuntimeOwnerName(names, tailName);
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
return [...names];
|
|
1957
|
+
}
|
|
1958
|
+
function readProjectionRuntimeNodeDisplayName(node) {
|
|
1959
|
+
const name = typeof node?.name === 'string' ? node.name.trim() : '';
|
|
1960
|
+
const id = typeof node?.id === 'string' ? node.id.trim() : '';
|
|
1961
|
+
if (name && id && name !== id)
|
|
1962
|
+
return `${name} (${id})`;
|
|
1963
|
+
return name || id || null;
|
|
1964
|
+
}
|
|
1965
|
+
function collectProjectionRuntimeOwnerTailNames(value) {
|
|
1966
|
+
const raw = typeof value === 'string' ? value.trim() : '';
|
|
1967
|
+
if (!raw)
|
|
1968
|
+
return [];
|
|
1969
|
+
const names = new Set();
|
|
1970
|
+
const withoutClonePrefix = raw.replace(/^Clone of\s+/i, '').trim();
|
|
1971
|
+
for (const candidate of [raw, withoutClonePrefix]) {
|
|
1972
|
+
const colonTail = candidate.includes(':') ? candidate.split(':').pop() : '';
|
|
1973
|
+
const dotTail = candidate.includes('.') ? candidate.split('.').pop() : '';
|
|
1974
|
+
if (colonTail)
|
|
1975
|
+
names.add(colonTail);
|
|
1976
|
+
if (dotTail)
|
|
1977
|
+
names.add(dotTail);
|
|
1978
|
+
}
|
|
1979
|
+
return [...names];
|
|
1980
|
+
}
|
|
1981
|
+
function readProjectionOwnerPathLastSegment(ownerNodePath) {
|
|
1982
|
+
const segments = ownerNodePath.split('/').map(segment => segment.trim()).filter(Boolean);
|
|
1983
|
+
return segments.length > 0 ? segments[segments.length - 1] ?? null : null;
|
|
1984
|
+
}
|
|
1985
|
+
function stripProjectionPrimitiveLabelSuffix(value) {
|
|
1986
|
+
if (typeof value !== 'string')
|
|
1987
|
+
return null;
|
|
1988
|
+
const stripped = value.replace(/\s*\/\s*Primitive\s+\d+\s*$/i, '').trim();
|
|
1989
|
+
return stripped || null;
|
|
1990
|
+
}
|
|
1991
|
+
function appendProjectionPrimitiveOwnerSuffix(value, primitiveIndex) {
|
|
1992
|
+
if (typeof value !== 'string')
|
|
1993
|
+
return null;
|
|
1994
|
+
const baseName = stripProjectionPrimitiveLabelSuffix(value) ?? value.trim();
|
|
1995
|
+
return baseName ? `${baseName}_primitive${primitiveIndex}` : null;
|
|
1996
|
+
}
|
|
1997
|
+
function readProjectionSlotLabel(slot) {
|
|
1998
|
+
const label = typeof slot.label === 'string' ? slot.label.trim() : '';
|
|
1999
|
+
if (label)
|
|
2000
|
+
return label;
|
|
2001
|
+
const materialName = typeof slot.materialName === 'string' ? slot.materialName.trim() : '';
|
|
2002
|
+
return materialName || null;
|
|
2003
|
+
}
|
|
2004
|
+
function addProjectionRuntimeOwnerName(names, value) {
|
|
2005
|
+
const normalized = normalizeProjectionRuntimeOwnerName(value);
|
|
2006
|
+
if (normalized)
|
|
2007
|
+
names.add(normalized);
|
|
2008
|
+
}
|
|
2009
|
+
function normalizeProjectionRuntimeOwnerName(value) {
|
|
2010
|
+
const trimmed = typeof value === 'string' ? value.trim() : '';
|
|
2011
|
+
if (!trimmed)
|
|
2012
|
+
return null;
|
|
2013
|
+
return createMaterialSlotOwnerPathMatchKey(trimmed);
|
|
2014
|
+
}
|
|
2015
|
+
function createProjectionSubMeshSelectionProxy(options, node, sourceMesh, subMesh, metadata) {
|
|
2016
|
+
const { babylon, scene } = options;
|
|
2017
|
+
const Mesh = babylon.Mesh;
|
|
2018
|
+
const VertexData = babylon.VertexData;
|
|
2019
|
+
const VertexBuffer = babylon.VertexBuffer;
|
|
2020
|
+
const StandardMaterial = babylon.StandardMaterial;
|
|
2021
|
+
if (!Mesh || !VertexData || !VertexBuffer || !StandardMaterial)
|
|
2022
|
+
return null;
|
|
2023
|
+
const positions = sourceMesh.getVerticesData?.(VertexBuffer.PositionKind, true, true);
|
|
2024
|
+
if (!positions || positions.length < 9)
|
|
2025
|
+
return null;
|
|
2026
|
+
const sourceIndices = sourceMesh.getIndices?.(true, true) ?? null;
|
|
2027
|
+
const subIndices = readProjectionSubMeshIndices(sourceIndices, subMesh);
|
|
2028
|
+
if (subIndices.length < 3)
|
|
2029
|
+
return null;
|
|
2030
|
+
const indexMap = new Map();
|
|
2031
|
+
const proxyPositions = [];
|
|
2032
|
+
const proxyIndices = [];
|
|
2033
|
+
for (const sourceIndex of subIndices) {
|
|
2034
|
+
const vertexIndex = Number(sourceIndex);
|
|
2035
|
+
if (!Number.isInteger(vertexIndex) || vertexIndex < 0)
|
|
2036
|
+
continue;
|
|
2037
|
+
let mappedIndex = indexMap.get(vertexIndex);
|
|
2038
|
+
if (mappedIndex == null) {
|
|
2039
|
+
const positionIndex = vertexIndex * 3;
|
|
2040
|
+
if (positionIndex + 2 >= positions.length)
|
|
2041
|
+
continue;
|
|
2042
|
+
mappedIndex = indexMap.size;
|
|
2043
|
+
indexMap.set(vertexIndex, mappedIndex);
|
|
2044
|
+
proxyPositions.push(Number(positions[positionIndex] ?? 0), Number(positions[positionIndex + 1] ?? 0), Number(positions[positionIndex + 2] ?? 0));
|
|
2045
|
+
}
|
|
2046
|
+
proxyIndices.push(mappedIndex);
|
|
2047
|
+
}
|
|
2048
|
+
if (proxyPositions.length < 9 || proxyIndices.length < 3)
|
|
2049
|
+
return null;
|
|
2050
|
+
const proxy = new Mesh(`${sourceMesh.name ?? node.id}.selectionProxy.${sanitizeProjectionImportSnapshotId(metadata.slotSelectionKey)}`, scene);
|
|
2051
|
+
proxy.parent = sourceMesh;
|
|
2052
|
+
proxy.isPickable = false;
|
|
2053
|
+
proxy.metadata = {
|
|
2054
|
+
...(proxy.metadata ?? {}),
|
|
2055
|
+
disablePlanarShadow: true,
|
|
2056
|
+
disableBlobShadow: true,
|
|
2057
|
+
disableStaticProjectedShadow: true,
|
|
2058
|
+
editorProjectionSelectionProxy: true,
|
|
2059
|
+
editorProjection: {
|
|
2060
|
+
nodeId: metadata.selectionNodeId,
|
|
2061
|
+
rootNodeId: node.id,
|
|
2062
|
+
sourceId: node.asset?.sourceId,
|
|
2063
|
+
ownerNodePath: metadata.ownerNodePath,
|
|
2064
|
+
slotSelectionKey: metadata.slotSelectionKey,
|
|
2065
|
+
primitiveIndex: metadata.primitiveIndex,
|
|
2066
|
+
runtimeKind: 'assetMeshSelectionProxy',
|
|
2067
|
+
...createProjectionShadowMetadata(node),
|
|
2068
|
+
},
|
|
2069
|
+
};
|
|
2070
|
+
const vertexData = new VertexData();
|
|
2071
|
+
vertexData.positions = proxyPositions;
|
|
2072
|
+
vertexData.indices = proxyIndices;
|
|
2073
|
+
vertexData.applyToMesh(proxy, false);
|
|
2074
|
+
const material = new StandardMaterial(`${proxy.name}.material`, scene);
|
|
2075
|
+
material.alpha = 0;
|
|
2076
|
+
material.disableLighting = true;
|
|
2077
|
+
material.backFaceCulling = false;
|
|
2078
|
+
if ('disableColorWrite' in material) {
|
|
2079
|
+
material.disableColorWrite = true;
|
|
2080
|
+
}
|
|
2081
|
+
proxy.material = material;
|
|
2082
|
+
return proxy;
|
|
2083
|
+
}
|
|
2084
|
+
function readProjectionSubMeshIndices(sourceIndices, subMesh) {
|
|
2085
|
+
const indexStart = Number.isInteger(subMesh?.indexStart) ? subMesh.indexStart : 0;
|
|
2086
|
+
const indexCount = Number.isInteger(subMesh?.indexCount) ? subMesh.indexCount : 0;
|
|
2087
|
+
if (sourceIndices && indexCount > 0) {
|
|
2088
|
+
return Array.from(sourceIndices).slice(indexStart, indexStart + indexCount);
|
|
2089
|
+
}
|
|
2090
|
+
const verticesStart = Number.isInteger(subMesh?.verticesStart) ? subMesh.verticesStart : 0;
|
|
2091
|
+
const verticesCount = Number.isInteger(subMesh?.verticesCount) ? subMesh.verticesCount : 0;
|
|
2092
|
+
return Array.from({ length: Math.max(0, verticesCount) }, (_, index) => verticesStart + index);
|
|
2093
|
+
}
|
|
1393
2094
|
function readProjectionAssetMaterialSlots(asset, projectionNodeId) {
|
|
1394
2095
|
const rawSlots = asset.metadata?.materialSlots;
|
|
1395
2096
|
if (!Array.isArray(rawSlots))
|
|
@@ -1420,30 +2121,159 @@ function readProjectionAssetMaterialSlotString(value) {
|
|
|
1420
2121
|
function countProjectionMeshMaterials(meshes) {
|
|
1421
2122
|
const counts = new Map();
|
|
1422
2123
|
for (const mesh of meshes) {
|
|
1423
|
-
const material = mesh
|
|
2124
|
+
const material = readProjectionWritableMeshMaterial(mesh);
|
|
1424
2125
|
if (!material)
|
|
1425
2126
|
continue;
|
|
1426
2127
|
counts.set(material, (counts.get(material) ?? 0) + 1);
|
|
1427
2128
|
}
|
|
1428
2129
|
return counts;
|
|
1429
2130
|
}
|
|
2131
|
+
function isProjectionSinglePrimitiveMaterialSlot(slot, materialSlots) {
|
|
2132
|
+
if (slot.primitiveIndex !== 0 || materialSlots.length === 0)
|
|
2133
|
+
return false;
|
|
2134
|
+
const matchingPrimitiveSlots = materialSlots.filter(candidate => (candidate.primitiveIndex != null
|
|
2135
|
+
&& candidate.matchKey === slot.matchKey
|
|
2136
|
+
&& isProjectionMaterialSlotSameMesh(candidate, slot)));
|
|
2137
|
+
return matchingPrimitiveSlots.length === 1 && matchingPrimitiveSlots[0]?.primitiveIndex === 0;
|
|
2138
|
+
}
|
|
2139
|
+
function isProjectionMaterialSlotSameMesh(left, right) {
|
|
2140
|
+
if (left.meshIndex != null || right.meshIndex != null) {
|
|
2141
|
+
return left.meshIndex === right.meshIndex;
|
|
2142
|
+
}
|
|
2143
|
+
return true;
|
|
2144
|
+
}
|
|
1430
2145
|
function detachProjectionSlotMaterial(mesh, materialCounts, ownerNodePath) {
|
|
1431
|
-
const
|
|
1432
|
-
|
|
1433
|
-
|
|
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
|
+
}
|
|
1434
2154
|
const materialUseCount = materialCounts.get(material) ?? 0;
|
|
1435
2155
|
if (materialUseCount <= 1)
|
|
1436
|
-
return material;
|
|
2156
|
+
return { material, clonedMaterials: [], projectionOwnsMaterialLifecycle };
|
|
1437
2157
|
const clone = typeof material.clone === 'function'
|
|
1438
2158
|
? material.clone(createProjectionSlotMaterialName(material, ownerNodePath))
|
|
1439
2159
|
: null;
|
|
1440
2160
|
if (!clone)
|
|
1441
2161
|
return null;
|
|
1442
2162
|
cloneProjectionSubMaterials(clone, material, ownerNodePath);
|
|
1443
|
-
|
|
2163
|
+
materialOwner.material = clone;
|
|
1444
2164
|
materialCounts.set(material, materialUseCount - 1);
|
|
1445
2165
|
materialCounts.set(clone, 1);
|
|
1446
|
-
return
|
|
2166
|
+
return {
|
|
2167
|
+
material: clone,
|
|
2168
|
+
clonedMaterials: projectionOwnsMaterialLifecycle ? [clone] : [],
|
|
2169
|
+
projectionOwnsMaterialLifecycle,
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
function resolveProjectionSlotMaterialTarget(mesh, materialCounts, slot, ownerNodePath, options) {
|
|
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
|
+
}
|
|
2181
|
+
if (slot.primitiveIndex == 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
|
+
}
|
|
2190
|
+
return {
|
|
2191
|
+
target: {
|
|
2192
|
+
material: detachResult.material,
|
|
2193
|
+
clonedMaterials: detachResult.clonedMaterials,
|
|
2194
|
+
},
|
|
2195
|
+
targetMaterialStatus: 'material',
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2198
|
+
if (!Array.isArray(material.subMaterials)) {
|
|
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)) {
|
|
2226
|
+
return {
|
|
2227
|
+
target: null,
|
|
2228
|
+
targetMaterialStatus: 'missingTargetMaterial',
|
|
2229
|
+
warningMessage: 'Primitive slot material profile was not applied because the multi-material target could not be detached.',
|
|
2230
|
+
};
|
|
2231
|
+
}
|
|
2232
|
+
const subMaterials = slotMaterial.subMaterials;
|
|
2233
|
+
const subMaterial = subMaterials[slot.primitiveIndex];
|
|
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
|
+
}
|
|
2241
|
+
const clonedSubMaterial = subMaterial.clone(createProjectionSlotSubMaterialName(subMaterial, ownerNodePath, slot.primitiveIndex));
|
|
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
|
+
}
|
|
2249
|
+
subMaterials[slot.primitiveIndex] = clonedSubMaterial;
|
|
2250
|
+
return {
|
|
2251
|
+
target: {
|
|
2252
|
+
material: clonedSubMaterial,
|
|
2253
|
+
clonedMaterials: [
|
|
2254
|
+
...detachResult.clonedMaterials,
|
|
2255
|
+
...(detachResult.projectionOwnsMaterialLifecycle ? [clonedSubMaterial] : []),
|
|
2256
|
+
],
|
|
2257
|
+
},
|
|
2258
|
+
targetMaterialStatus: 'subMaterial',
|
|
2259
|
+
};
|
|
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);
|
|
1447
2277
|
}
|
|
1448
2278
|
function cloneProjectionSubMaterials(clone, source, ownerNodePath) {
|
|
1449
2279
|
if (!Array.isArray(clone?.subMaterials) || !Array.isArray(source?.subMaterials))
|
|
@@ -2064,17 +2894,26 @@ function syncProjectionSelection(babylon, projections, selection) {
|
|
|
2064
2894
|
for (const [nodeId, projection] of projections) {
|
|
2065
2895
|
const rootSelected = selectedIds.has(nodeId);
|
|
2066
2896
|
const rootActive = selection.activeId === nodeId;
|
|
2897
|
+
const proxyNodeIds = new Set((projection.selectionMeshes ?? [])
|
|
2898
|
+
.map(readProjectionMeshSelectionNodeId)
|
|
2899
|
+
.filter((id) => !!id));
|
|
2067
2900
|
let projectionSelected = false;
|
|
2068
2901
|
for (const mesh of projection.outlineMeshes) {
|
|
2069
|
-
const meshNodeId =
|
|
2070
|
-
|
|
2071
|
-
: nodeId;
|
|
2072
|
-
const selected = rootSelected || selectedIds.has(meshNodeId);
|
|
2902
|
+
const meshNodeId = readProjectionMeshSelectionNodeId(mesh) ?? nodeId;
|
|
2903
|
+
const selected = rootSelected || (!proxyNodeIds.has(meshNodeId) && selectedIds.has(meshNodeId));
|
|
2073
2904
|
const active = rootActive || selection.activeId === meshNodeId;
|
|
2074
2905
|
if (selected)
|
|
2075
2906
|
projectionSelected = true;
|
|
2076
2907
|
syncProjectionMeshSelection(mesh, selected, active, Color4);
|
|
2077
2908
|
}
|
|
2909
|
+
for (const mesh of projection.selectionMeshes ?? []) {
|
|
2910
|
+
const meshNodeId = readProjectionMeshSelectionNodeId(mesh) ?? nodeId;
|
|
2911
|
+
const selected = selectedIds.has(meshNodeId);
|
|
2912
|
+
const active = selection.activeId === meshNodeId;
|
|
2913
|
+
if (selected)
|
|
2914
|
+
projectionSelected = true;
|
|
2915
|
+
syncProjectionMeshSelection(mesh, selected, active, Color4);
|
|
2916
|
+
}
|
|
2078
2917
|
if (projectionSelected) {
|
|
2079
2918
|
projection.selectionEdgesPrewarmCancel?.();
|
|
2080
2919
|
projection.selectionEdgesPrewarmCancel = null;
|
|
@@ -2099,7 +2938,7 @@ function syncProjectionMeshSelection(mesh, selected, active, Color4) {
|
|
|
2099
2938
|
}
|
|
2100
2939
|
}
|
|
2101
2940
|
function prewarmProjectionSelectionEdges(projection) {
|
|
2102
|
-
for (const mesh of projection.outlineMeshes) {
|
|
2941
|
+
for (const mesh of [...projection.outlineMeshes, ...(projection.selectionMeshes ?? [])]) {
|
|
2103
2942
|
if (mesh?.metadata?.editorProjectionSelectionEdgesPrewarmed)
|
|
2104
2943
|
continue;
|
|
2105
2944
|
if (mesh?.metadata?.editorProjectionSelectionEdgesSelected)
|