From e343412468a43c7bde6ea4435b0c4543d1421c49 Mon Sep 17 00:00:00 2001 From: CODEX Date: Thu, 18 Jun 2026 21:53:18 +0300 Subject: [PATCH] Add BIM section plane tool --- frontend/dcViewer.css | 91 +++++++++++++++++++ frontend/dcViewer.js | 197 +++++++++++++++++++++++++++++++++++++++++- frontend/index.html | 42 ++++++++- 3 files changed, 327 insertions(+), 3 deletions(-) diff --git a/frontend/dcViewer.css b/frontend/dcViewer.css index b6edbfe..0f8e02b 100644 --- a/frontend/dcViewer.css +++ b/frontend/dcViewer.css @@ -1911,11 +1911,102 @@ header { background: var(--toolbar-bg-active); } +.tb-btn.disabled, +.tb-btn:disabled { + cursor: default; + opacity: 0.45; + transform: none; + box-shadow: none; +} + body[data-viewer-mode="guest"] .bottom-toolbar, +body[data-viewer-mode="guest"] .section-toolbar, +body[data-viewer-mode="guest"] .section-overview, body[data-viewer-mode="guest"] .inspector-restore-tray { display: none !important; } +.section-toolbar { + position: fixed; + left: calc(12px + var(--tb-size, 52px) + 10px); + bottom: 12px; + z-index: 16; + display: flex; + align-items: center; + gap: 6px; + padding: 6px; + border: 1px solid var(--toolbar-border); + border-radius: var(--tb-radius, 12px); + background: color-mix(in srgb, var(--toolbar-bg) 88%, transparent); + box-shadow: 0 18px 34px rgba(0, 0, 0, 0.28); + backdrop-filter: blur(18px) saturate(124%); + -webkit-backdrop-filter: blur(18px) saturate(124%); +} + +.section-toolbar.hidden { + display: none; +} + +.section-tool-btn { + width: 38px; + height: 38px; + display: inline-grid; + place-items: center; + border: 1px solid transparent; + border-radius: calc(var(--tb-radius, 12px) - 4px); + background: transparent; + color: var(--text); + font-size: 13px; + font-weight: 700; + line-height: 1; + cursor: pointer; + transition: background 120ms ease, border-color 120ms ease, transform 100ms ease; +} + +.section-tool-btn:hover, +.section-tool-btn.active { + border-color: var(--toolbar-outline, rgba(76, 224, 210, 0.7)); + background: var(--toolbar-bg-active); + color: var(--modal-btn-primary-contrast, #15170f); +} + +.section-tool-btn:active { + transform: translateY(1px); +} + +.section-tool-btn svg { + display: block; + width: 21px; + height: 21px; + fill: none; + stroke: currentColor; + stroke-width: 1.8; + stroke-linecap: round; + stroke-linejoin: round; +} + +.section-axis-btn { + letter-spacing: 0; +} + +.section-overview { + position: fixed; + left: calc(12px + var(--tb-size, 52px) + 10px); + bottom: 66px; + width: 150px; + height: 150px; + z-index: 14; + display: none; + border: 1px solid var(--toolbar-border); + border-radius: 12px; + background: color-mix(in srgb, var(--toolbar-bg) 84%, transparent); + box-shadow: 0 18px 34px rgba(0, 0, 0, 0.3); +} + +body[data-section-mode="active"] .section-overview { + display: block; +} + .templates-panel { position: fixed; left: var(--templates-left, 72px); diff --git a/frontend/dcViewer.js b/frontend/dcViewer.js index 6aa4a55..0dc9798 100644 --- a/frontend/dcViewer.js +++ b/frontend/dcViewer.js @@ -39,6 +39,9 @@ const measureButton = document.querySelector('[data-action="measure"]'); const settingsButton = document.querySelector('[data-action="settings"]'); const templatesButton = document.querySelector('[data-action="templates"]'); const shareButton = document.querySelector('[data-action="share"]'); +const sectionButton = document.querySelector('[data-action="section"]'); +const sectionToolbar = document.getElementById("sectionToolbar"); +const sectionOverviewCanvas = document.getElementById("sectionPlanesOverview"); const projectionToggle = document.querySelector('[data-action="projection"]'); const treeContainer = document.getElementById("treeContainer"); const displayModeControl = document.getElementById("displayModeControl"); @@ -250,6 +253,11 @@ let measurePlugin = null; let measureControl = null; let measureActive = false; let activeMeasurementId = null; +let sectionPlanesPlugin = null; +let activeSectionPlaneId = null; +let sectionPlaneCounter = 0; +let sectionModeActive = false; +let activeSectionAxis = null; let objectContextModal = null; let mathUtil = null; let selectionState = null; // { ids, modelId, pivotCenter, baseTransforms: Map, transform } @@ -454,6 +462,9 @@ const applyRuntimeMode = () => { if (dropZone) { dropZone.style.pointerEvents = isGuestMode() ? "none" : ""; } + if (sectionToolbar && isGuestMode()) { + sectionToolbar.classList.add("hidden"); + } }; const initializeRuntimeMode = async () => { @@ -1171,6 +1182,155 @@ const clearMeasurements = () => { closeMeasurementModal(); }; +const getSectionPlaneIds = () => Object.keys(sectionPlanesPlugin?.sectionPlanes || {}); + +const getActiveSectionPlane = () => { + if (!sectionPlanesPlugin) return null; + if (activeSectionPlaneId && sectionPlanesPlugin.sectionPlanes?.[activeSectionPlaneId]) { + return sectionPlanesPlugin.sectionPlanes[activeSectionPlaneId]; + } + const firstId = getSectionPlaneIds()[0] || null; + activeSectionPlaneId = firstId; + return firstId ? sectionPlanesPlugin.sectionPlanes[firstId] : null; +}; + +const setAllSectionPlanesActive = (active) => { + getSectionPlaneIds().forEach((id) => { + const sectionPlane = sectionPlanesPlugin?.sectionPlanes?.[id]; + if (sectionPlane) { + sectionPlane.active = !!active; + } + }); +}; + +const getSectionCenter = () => { + const sceneAABB = viewer?.scene?.aabb; + const aabb = Array.isArray(sceneAABB) || ArrayBuffer.isView(sceneAABB) + ? Array.from(sceneAABB) + : null; + const center = centerFromAABB(aabb); + return center.every((value) => Number.isFinite(value)) ? center : [0, 0, 0]; +}; + +const getCameraSectionDir = () => { + const eye = snapshotVec(viewer?.camera?.eye) || [0, 0, 1]; + const look = snapshotVec(viewer?.camera?.look) || [0, 0, 0]; + return normalizeVec3([ + look[0] - eye[0], + look[1] - eye[1], + look[2] - eye[2], + ], [0, 0, -1]); +}; + +const setSectionAxisActive = (axis) => { + activeSectionAxis = axis || null; + sectionToolbar?.querySelectorAll("[data-section-action]").forEach((btn) => { + btn.classList.toggle("active", !!axis && btn.dataset.sectionAction === axis); + }); +}; + +const setSectionModeActive = (active) => { + sectionModeActive = !!active && !!sectionPlanesPlugin && getSectionPlaneIds().length > 0 && !isGuestMode(); + sectionButton?.classList.toggle("active", sectionModeActive); + sectionToolbar?.classList.toggle("hidden", !sectionModeActive); + document.body.dataset.sectionMode = sectionModeActive ? "active" : ""; + sectionPlanesPlugin?.setOverviewVisible?.(sectionModeActive); + if (!sectionModeActive) { + setSectionAxisActive(null); + sectionPlanesPlugin?.hideControl?.(); + } +}; + +const updateSectionControls = () => { + const available = !!sectionPlanesPlugin && !isGuestMode() && activeModels.length > 0; + if (sectionButton) { + sectionButton.disabled = !available; + sectionButton.classList.toggle("disabled", !available); + sectionButton.classList.toggle("active", available && sectionModeActive); + } + if (!available) { + sectionToolbar?.classList.add("hidden"); + document.body.dataset.sectionMode = ""; + sectionPlanesPlugin?.setOverviewVisible?.(false); + } +}; + +const createOrUpdateSectionPlane = (axis = "camera") => { + if (!sectionPlanesPlugin) { + showError("Разрез недоступен в текущей сборке SDK"); + return null; + } + if (!activeModels.length) { + showError("Сначала загрузите модель"); + return null; + } + + setMeasureActive(false); + const dirs = { + camera: getCameraSectionDir(), + x: [1, 0, 0], + y: [0, 1, 0], + z: [0, 0, 1], + }; + const dir = dirs[axis] || dirs.camera; + const pos = getSectionCenter(); + let sectionPlane = getActiveSectionPlane(); + + if (!sectionPlane) { + const id = `sectionPlane-${++sectionPlaneCounter}`; + sectionPlane = sectionPlanesPlugin.createSectionPlane({ id, pos, dir }); + activeSectionPlaneId = sectionPlane.id || id; + } else { + sectionPlane.pos = pos; + sectionPlane.dir = dir; + sectionPlane.active = true; + activeSectionPlaneId = sectionPlane.id || activeSectionPlaneId; + } + + sectionPlanesPlugin.showControl(activeSectionPlaneId); + setSectionModeActive(true); + setSectionAxisActive(axis); + setStatus("Разрез включён"); + rearmCameraControl(); + return sectionPlane; +}; + +const clearSectionPlanes = () => { + sectionPlanesPlugin?.clear?.(); + activeSectionPlaneId = null; + setSectionModeActive(false); +}; + +const flipSectionPlanes = () => { + if (!getActiveSectionPlane()) { + createOrUpdateSectionPlane("camera"); + return; + } + setAllSectionPlanesActive(true); + sectionPlanesPlugin?.flipSectionPlanes?.(); + sectionPlanesPlugin?.showControl?.(activeSectionPlaneId); + setSectionModeActive(true); + setStatus("Разрез инвертирован"); +}; + +const toggleSectionPlanes = () => { + if (sectionModeActive) { + setAllSectionPlanesActive(false); + setSectionModeActive(false); + setStatus("Разрез выключен"); + return; + } + if (getSectionPlaneIds().length > 0) { + setAllSectionPlanesActive(true); + activeSectionPlaneId = activeSectionPlaneId || getSectionPlaneIds()[0]; + sectionPlanesPlugin?.showControl?.(activeSectionPlaneId); + setSectionModeActive(true); + setStatus("Разрез включён"); + return; + } + createOrUpdateSectionPlane("camera"); +}; + const rebuildNavCube = (sdk) => { if (!navCubeCanvas || !viewer) return; const cubeColor = cssValue("--cube-base", "#7b3fff"); @@ -1257,6 +1417,7 @@ const updatePanelsVisibility = () => { projectNameInput.value = ""; } } + updateSectionControls(); applyRuntimeMode(); }; @@ -1990,6 +2151,7 @@ const destroyAllModels = ({ preserveMeta = false } = {}) => { gizmo?.clearTarget(); treeView?.unShowNode?.(); clearMeasurements(); + clearSectionPlanes(); selectionState = null; lastSelection = { ids: [], modelId: null }; activeTransformTarget = null; @@ -2852,7 +3014,7 @@ const initViewer = async () => { const sdk = await import(SDK_URL); sdkRef = sdk; - const { Viewer, NavCubePlugin, DistanceMeasurementsPlugin, DistanceMeasurementsMouseControl } = sdk; + const { Viewer, NavCubePlugin, DistanceMeasurementsPlugin, DistanceMeasurementsMouseControl, SectionPlanesPlugin } = sdk; mathUtil = sdk.math; if (headerEl && homeButton && !homeButton.querySelector(".ue-logo")) { const logoEl = createLogo(); @@ -2931,6 +3093,22 @@ const initViewer = async () => { } } + if (SectionPlanesPlugin && sectionOverviewCanvas) { + sectionPlanesPlugin = new SectionPlanesPlugin(viewer, { + overviewCanvasId: "sectionPlanesOverview", + overviewVisible: false, + }); + sectionPlanesPlugin.setOverviewVisible(false); + setSectionModeActive(false); + } else { + console.warn("SectionPlanesPlugin is not available in the loaded SDK build"); + if (sectionButton) { + sectionButton.classList.add("disabled"); + sectionButton.disabled = true; + sectionButton.title = "Разрез недоступен"; + } + } + if (DistanceMeasurementsPlugin && DistanceMeasurementsMouseControl) { measurePlugin = new DistanceMeasurementsPlugin(viewer, { defaultAxisVisible: false, @@ -4308,6 +4486,8 @@ const loadProjectSnapshot = async (project) => { } else if (action === "measure") { if (!measurePlugin || !measureControl) return; setMeasureActive(!measureActive); + } else if (action === "section") { + toggleSectionPlanes(); } else if (action === "projection") { toggleProjection(); } else if (action === "share") { @@ -4318,6 +4498,21 @@ const loadProjectSnapshot = async (project) => { }); } + sectionToolbar?.addEventListener("click", (e) => { + if (isGuestMode()) return; + const btn = e.target.closest("[data-section-action]"); + if (!btn) return; + const action = btn.dataset.sectionAction; + if (action === "camera" || action === "x" || action === "y" || action === "z") { + createOrUpdateSectionPlane(action); + } else if (action === "flip") { + flipSectionPlanes(); + } else if (action === "clear") { + clearSectionPlanes(); + setStatus("Разрез выключен"); + } + }); + function updateTransformInputs(transform) { const empty = { position: ["", "", ""], diff --git a/frontend/index.html b/frontend/index.html index e4d44b7..c2061fe 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -89,7 +89,7 @@ - +
@@ -107,6 +107,7 @@
+
+
@@ -292,6 +293,14 @@ +
+ + - +