Add BIM section plane tool
This commit is contained in:
parent
4638acc04b
commit
e343412468
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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: ["", "", ""],
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@
|
|||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="./dcViewer.css?v=17">
|
||||
<link rel="stylesheet" href="./dcViewer.css?v=18">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
|
|
@ -107,6 +107,7 @@
|
|||
<section class="viewer-wrapper">
|
||||
<canvas id="viewerCanvas"></canvas>
|
||||
<canvas id="navCube"></canvas>
|
||||
<canvas id="sectionPlanesOverview" class="section-overview" width="150" height="150"></canvas>
|
||||
<div class="drop-hint" id="dropZone">
|
||||
<div class="drop-center">
|
||||
<div class="drop-plus">+</div>
|
||||
|
|
@ -292,6 +293,14 @@
|
|||
<path d="m13 7 2 2"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="tb-btn" data-action="section" title="Разрез" aria-label="Разрез">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M4.5 7.25 12 3.5l7.5 3.75L12 11 4.5 7.25Z"></path>
|
||||
<path d="M4.5 16.75 12 20.5l7.5-3.75"></path>
|
||||
<path d="M12 11v9.5"></path>
|
||||
<path d="M6.25 4.5 17.75 19.5"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="tb-btn" data-action="projection" title="Перспектива / аксонометрия">A</button>
|
||||
<button class="tb-btn" data-action="share" title="Поделиться моделью" aria-label="Поделиться моделью">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
|
|
@ -308,9 +317,38 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div id="sectionToolbar" class="section-toolbar hidden" aria-label="Управление разрезом">
|
||||
<button class="section-tool-btn" data-section-action="camera" type="button" title="По камере" aria-label="По камере">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M4.5 12s2.75-5 7.5-5 7.5 5 7.5 5-2.75 5-7.5 5-7.5-5-7.5-5Z"></path>
|
||||
<path d="M12 14.75A2.75 2.75 0 1 0 12 9.25a2.75 2.75 0 0 0 0 5.5Z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="section-tool-btn section-axis-btn" data-section-action="x" type="button" title="Ось X" aria-label="Ось X">X</button>
|
||||
<button class="section-tool-btn section-axis-btn" data-section-action="y" type="button" title="Ось Y" aria-label="Ось Y">Y</button>
|
||||
<button class="section-tool-btn section-axis-btn" data-section-action="z" type="button" title="Ось Z" aria-label="Ось Z">Z</button>
|
||||
<button class="section-tool-btn" data-section-action="flip" type="button" title="Инвертировать" aria-label="Инвертировать">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M7.5 7.5h8a3.5 3.5 0 0 1 0 7h-7"></path>
|
||||
<path d="m10.5 4.5-3 3 3 3"></path>
|
||||
<path d="m13.5 19.5 3-3-3-3"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="section-tool-btn" data-section-action="clear" type="button" title="Удалить разрез" aria-label="Удалить разрез">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M5 7h14"></path>
|
||||
<path d="M9 7V5h6v2"></path>
|
||||
<path d="M8 10v8"></path>
|
||||
<path d="M12 10v8"></path>
|
||||
<path d="M16 10v8"></path>
|
||||
<path d="M7 7l1 13h8l1-13"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<input id="fileInput" class="hidden" type="file"
|
||||
accept=".xkt,.glb,.gltf,.bim,.las,.laz,.obj,.stl">
|
||||
|
||||
<script type="module" src="./dcViewer.js?v=22"></script>
|
||||
<script type="module" src="./dcViewer.js?v=23"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Reference in New Issue