import { BoxGeometry, Group, Mesh, MeshBasicMaterial } from "three"; import { instantiateModelTemplate } from "./gltf-model-import"; const MODEL_PLACEHOLDER_COLOR = 0x89b6ff; const MODEL_SELECTION_COLOR = 0xf7d2aa; const MODEL_PREVIEW_SHELL_OPACITY = 0.5; function getLocalModelBounds(asset) { if (asset?.kind === "model" && asset.metadata.boundingBox !== null) { const boundingBox = asset.metadata.boundingBox; return { center: { x: (boundingBox.min.x + boundingBox.max.x) * 0.5, y: (boundingBox.min.y + boundingBox.max.y) * 0.5, z: (boundingBox.min.z + boundingBox.max.z) * 0.5 }, size: { x: Math.max(0.1, Math.abs(boundingBox.max.x - boundingBox.min.x)), y: Math.max(0.1, Math.abs(boundingBox.max.y - boundingBox.min.y)), z: Math.max(0.1, Math.abs(boundingBox.max.z - boundingBox.min.z)) } }; } return { center: { x: 0, y: 0, z: 0 }, size: { x: 1, y: 1, z: 1 } }; } function createWireframeBox(size, color, opacity) { return new Mesh(new BoxGeometry(size.x, size.y, size.z), new MeshBasicMaterial({ color, wireframe: true, transparent: true, opacity, depthWrite: false })); } function createWireframeMaterial(material) { const source = material; const opacity = typeof source.opacity === "number" ? source.opacity : 1; return new MeshBasicMaterial({ color: source.color?.getHex() ?? MODEL_PLACEHOLDER_COLOR, wireframe: true, transparent: source.transparent === true || opacity < 1, opacity, depthWrite: false }); } function applyWireframeMaterialPresentation(group) { group.traverse((object) => { const maybeMesh = object; if (maybeMesh.isMesh !== true) { return; } if (Array.isArray(maybeMesh.material)) { const originalMaterials = maybeMesh.material; maybeMesh.material = originalMaterials.map((material) => createWireframeMaterial(material)); for (const material of originalMaterials) { material.dispose(); } return; } const originalMaterial = maybeMesh.material; maybeMesh.material = createWireframeMaterial(originalMaterial); originalMaterial.dispose(); }); } function disposeTexture(texture, seenTextures) { if (seenTextures.has(texture)) { return; } seenTextures.add(texture); texture.dispose(); } function disposeMaterialResources(material, disposeTextures, seenTextures) { if (disposeTextures) { for (const value of Object.values(material)) { if (value === null || value === undefined) { continue; } if (Array.isArray(value)) { for (const entry of value) { if (entry !== null && typeof entry === "object" && "isTexture" in entry) { disposeTexture(entry, seenTextures); } } continue; } if (typeof value === "object" && "isTexture" in value) { disposeTexture(value, seenTextures); } } } material.dispose(); } function disposeMeshResources(object, disposeTextures, seenTextures) { const maybeMesh = object; if (maybeMesh.isMesh !== true) { return; } maybeMesh.geometry.dispose(); if (Array.isArray(maybeMesh.material)) { for (const material of maybeMesh.material) { disposeMaterialResources(material, disposeTextures, seenTextures); } } else { disposeMaterialResources(maybeMesh.material, disposeTextures, seenTextures); } } export function createModelInstanceRenderGroup(modelInstance, asset, loadedAsset, selected = false, previewShellColor, renderMode = "normal") { const bounds = getLocalModelBounds(asset); const group = new Group(); group.position.set(modelInstance.position.x, modelInstance.position.y, modelInstance.position.z); group.rotation.set((modelInstance.rotationDegrees.x * Math.PI) / 180, (modelInstance.rotationDegrees.y * Math.PI) / 180, (modelInstance.rotationDegrees.z * Math.PI) / 180); group.scale.set(modelInstance.scale.x, modelInstance.scale.y, modelInstance.scale.z); group.userData.modelInstanceId = modelInstance.id; group.userData.assetId = modelInstance.assetId; if (loadedAsset !== undefined) { const instantiatedModel = instantiateModelTemplate(loadedAsset.template); if (renderMode === "wireframe") { applyWireframeMaterialPresentation(instantiatedModel); } group.add(instantiatedModel); } else { const placeholder = createWireframeBox(bounds.size, previewShellColor ?? MODEL_PLACEHOLDER_COLOR, previewShellColor === undefined ? 0.28 : MODEL_PREVIEW_SHELL_OPACITY); placeholder.position.set(bounds.center.x, bounds.center.y, bounds.center.z); placeholder.userData.shadowIgnored = true; group.add(placeholder); } if (loadedAsset !== undefined && previewShellColor !== undefined) { const previewShell = createWireframeBox(bounds.size, previewShellColor, MODEL_PREVIEW_SHELL_OPACITY); previewShell.position.set(bounds.center.x, bounds.center.y, bounds.center.z); previewShell.userData.shadowIgnored = true; group.add(previewShell); } if (selected) { const selectionShell = createWireframeBox(bounds.size, MODEL_SELECTION_COLOR, 0.8); selectionShell.position.set(bounds.center.x, bounds.center.y, bounds.center.z); selectionShell.userData.shadowIgnored = true; group.add(selectionShell); } return group; } export function disposeModelInstance(instance) { const seenTextures = new Set(); instance.traverse((object) => { disposeMeshResources(object, false, seenTextures); }); }