import { core } from "../core.js"; import { showToast, toastHelper } from "../viewer-utils.js"; import { t } from "../i18n-utils.js"; import THREE from "../init.js"; export function attachMaterialsEditor(Viewer) { Object.assign(Viewer, { openMaterialsFolder(materialUuid = null) { this.buildMaterialsDialog(); if (!this.materialsDialog) return; if (!this.materialsEditorObject || !this.materialsList?.length) { showToast(t("gui.selectByMaterial", "select by material"), "warning"); return; } this.syncMaterialsDialogSelectionOptions(); const nextUuid = materialUuid && materialUuid !== "" ? materialUuid : this.selectedMaterialUuid || this.materialsList[0]?.uuid || ""; if (nextUuid) { this.selectMaterialInEditor(this.materialsEditorObject, nextUuid); } this.updateMaterialsDialogBounds(); this.materialsDialog.hidden = false; this.syncMaterialsDialogFields(); this.closeActionMenu(); this.materialsDialogSelect?.focus(); }, destroyMaterialGuiControls() { if (this.materialGuiControls) { Object.values(this.materialGuiControls).forEach((controller) => { if (controller?.destroy) { controller.destroy(); } }); this.materialGuiControls = null; } }, destroyMaterialSelectionController() { this.materialsEditorObject = null; this.selectedMaterialUuid = null; this.materialsList = []; if (this.materialsDialogSelect) { this.materialsDialogSelect.innerHTML = ""; } this.syncMaterialsDialogFields(); }, getMaterialByUuid(object, uuid) { if (!object || !uuid) return null; let found = null; object.traverse((child) => { if ( child.isMesh && child.material ) { const materials = Array.isArray(child.material) ? child.material : [child.material]; materials.forEach((material) => { if (material?.isMaterial && material.uuid === uuid) { found = material; } }); } }); return found; }, normalizeMaterialColorValue(value, fallback = "#ffffff") { if (!value) return fallback; if (typeof value === "string") { if (value.startsWith("0x")) { return `#${value.slice(2).padStart(6, "0")}`; } return value.startsWith("#") ? value : `#${value}`; } if (typeof value.getHexString === "function") { return `#${value.getHexString()}`; } return fallback; }, syncMaterialsDialogSelectionOptions() { if (!this.materialsDialogSelect) return; const currentValue = this.selectedMaterialUuid || ""; this.materialsDialogSelect.innerHTML = ""; const placeholder = document.createElement("option"); placeholder.value = ""; placeholder.textContent = t("gui.selectByMaterial", "select by material"); this.materialsDialogSelect.appendChild(placeholder); this.materialsList.forEach((item) => { const option = document.createElement("option"); option.value = item.uuid; option.textContent = item.label; this.materialsDialogSelect.appendChild(option); }); this.materialsDialogSelect.value = currentValue; }, updateMaterialsDialogLabels() { if (!this.materialsDialog) return; const title = this.materialsDialog.querySelector("#materialsDialogTitle"); const closeButton = this.materialsDialog.querySelector(".materials-dialog__close"); const fieldLabels = Array.from( this.materialsDialog.querySelectorAll(".materials-dialog__field > span") ); const hint = this.materialsDialogInputs?.hint; const emptyState = this.materialsDialogInputs?.emptyState; if (title) { title.textContent = t("gui.materials", "Materials"); } if (closeButton) { closeButton.setAttribute("aria-label", t("gui.materials", "Materials")); } if (fieldLabels.length > 0) { fieldLabels[0].textContent = t("gui.editMaterial", "Edit material"); } if (fieldLabels.length > 1) { fieldLabels[1].textContent = t("gui.color", "Color"); } if (fieldLabels.length > 2) { fieldLabels[2].textContent = t("gui.emissive", "Emissive"); } if (fieldLabels.length > 3) { fieldLabels[3].textContent = t("gui.intensity", "Intensity"); } if (fieldLabels.length > 4) { fieldLabels[4].textContent = t("gui.metalness", "Metalness"); } if (this.materialsDialogSelect) { const placeholderOption = this.materialsDialogSelect.querySelector('option[value=""]'); if (placeholderOption) { placeholderOption.textContent = t("gui.selectByMaterial", "select by material"); } } if (hint) { hint.textContent = t("gui.selectByMaterial", "select by material"); } if (emptyState) { emptyState.textContent = t("gui.selectByMaterial", "select by material"); } }, syncMaterialsDialogFields() { if (!this.materialsDialogInputs) return; const material = this.materialsEditorObject && this.selectedMaterialUuid ? this.getMaterialByUuid(this.materialsEditorObject, this.selectedMaterialUuid) : null; const hasMaterial = Boolean(material); const { color, emissiveColor, emissive, metalness, emptyState, hint, } = this.materialsDialogInputs; if (this.materialsDialogSelect) { this.materialsDialogSelect.disabled = this.materialsList.length === 0; this.materialsDialogSelect.value = this.selectedMaterialUuid || ""; } [color, emissiveColor, emissive, metalness].forEach((input) => { if (input) input.disabled = !hasMaterial; }); if (!hasMaterial) { if (color) color.value = "#ffffff"; if (emissiveColor) emissiveColor.value = "#000000"; if (emissive) emissive.value = "0"; if (metalness) metalness.value = "0"; if (emptyState) emptyState.hidden = this.materialsList.length > 0; if (hint) hint.textContent = t("gui.selectByMaterial", "select by material"); return; } if (color) color.value = this.normalizeMaterialColorValue(material.color, "#ffffff"); if (emissiveColor) emissiveColor.value = this.normalizeMaterialColorValue(material.emissive, "#000000"); if (emissive) emissive.value = String(material.emissiveIntensity ?? 0); if (metalness) metalness.value = String(material.metalness ?? 0); if (emptyState) emptyState.hidden = true; if (hint) { hint.textContent = this.materialsList.find((item) => item.uuid === material.uuid)?.label || material.uuid; } }, selectMaterialInEditor(object, value) { if (!object) return; if (!value) { this.destroyMaterialGuiControls(); this.selectedMaterialUuid = null; this.refreshMaterialsToolbarMenu(); this.syncMaterialsDialogFields(); return; } this.destroyMaterialGuiControls(); const material = this.getMaterialByUuid(object, value); if (!material) { this.selectedMaterialUuid = null; this.refreshMaterialsToolbarMenu(); this.syncMaterialsDialogFields(); return; } core.materialProperties.color = this.normalizeMaterialColorValue(material.color, "#ffffff"); core.materialProperties.emissiveColor = this.normalizeMaterialColorValue(material.emissive, "#000000"); core.materialProperties.emissive = material.emissiveIntensity ?? 0; core.materialProperties.metalness = material.metalness ?? 0; this.selectedMaterialUuid = value; this.syncMaterialsDialogSelectionOptions(); this.refreshMaterialsToolbarMenu(); this.syncMaterialsDialogFields(); }, initializeMaterialsEditor(object) { if (!object) return; this.destroyMaterialGuiControls(); this.destroyMaterialSelectionController(); const materials = new Map(); const registerMaterial = (material) => { if (!material || !material.isMaterial || !material.uuid) return; if (!materials.has(material.uuid)) { materials.set(material.uuid, material); } }; const gatherMaterials = (mesh) => { if (!mesh.material) return; if (Array.isArray(mesh.material)) { mesh.material.forEach(registerMaterial); } else { registerMaterial(mesh.material); } }; if (object.isMesh) { gatherMaterials(object); } object.traverse((child) => { if (child.isMesh) { gatherMaterials(child); } }); const options = { [t("gui.selectByMaterial", "select by material")]: "", }; this.materialsList = []; materials.forEach((material, uuid) => { const label = material.name?.trim() ? material.name : uuid; options[label] = uuid; this.materialsList.push({ uuid, label }); }); core.materialsPropertiesText["Edit material"] = ""; this.materialsEditorObject = object; this.refreshMaterialsToolbarMenu(); this.syncMaterialsDialogSelectionOptions(); this.syncMaterialsDialogFields(); }, refreshMaterialsToolbarMenu() { if (!this.materialsSubmenu) return; this.materialsSubmenu.innerHTML = ""; const list = this.materialsList?.length ? this.materialsList : []; if (!list.length) { const emptyButton = document.createElement("button"); emptyButton.type = "button"; emptyButton.className = "viewer-editor-tool viewer-editor-tool_submenu-button viewer-editor-tool_submenu-control viewer-editor-tool_submenu-materials"; emptyButton.disabled = true; const label = t("gui.selectByMaterial", "Select by material"); const text = document.createElement("span"); text.textContent = label; emptyButton.appendChild(text); this.materialsSubmenu.appendChild(emptyButton); return; } list.forEach((item) => { const subButton = document.createElement("button"); subButton.type = "button"; subButton.className = "viewer-editor-tool viewer-editor-tool_submenu-button viewer-editor-tool_submenu-control viewer-editor-tool_submenu-materials"; if (item.uuid === this.selectedMaterialUuid) { subButton.classList.add("is-active"); } subButton.dataset.materialUuid = item.uuid; subButton.setAttribute("title", item.label); subButton.setAttribute("aria-label", item.label); const iconSpan = document.createElement("span"); iconSpan.className = "viewer-editor-tool_icon"; iconSpan.setAttribute("aria-hidden", "true"); iconSpan.innerHTML = this.getEditorToolbarIcon("color"); subButton.appendChild(iconSpan); const labelSpan = document.createElement("span"); labelSpan.className = "viewer-editor-tool_label"; labelSpan.textContent = item.label; subButton.appendChild(labelSpan); this.bindEventListener(subButton, "click", (event) => { event.stopPropagation(); this.openMaterialsFolder(item.uuid); }); this.materialsSubmenu.appendChild(subButton); }); }, buildMaterialsDialog() { if (!core.container || this.materialsDialog) return; const dialog = document.createElement("div"); dialog.id = "materialsDialog"; dialog.className = "materials-dialog"; dialog.hidden = true; dialog.innerHTML = `
${t("gui.selectByMaterial", "select by material")}
${t("gui.selectByMaterial", "select by material")}