Refine BIM section controls and styling

This commit is contained in:
CODEX 2026-06-18 22:09:55 +03:00
parent e343412468
commit 84b28ba12c
6 changed files with 294 additions and 46 deletions

View File

@ -51,6 +51,16 @@
--measure-label: #c4ff67;
--measure-label-text: #292929;
--measure-dot: #c4ff67;
--section-axis-x: #ff2b2b;
--section-axis-y: #35ff4f;
--section-axis-z: #285bff;
--section-gizmo-diameter: 1;
--section-plane-fill: #606060;
--section-plane-alpha: 0.32;
--section-plane-edge: #0d0d0d;
--section-plane-edge-alpha: 0.8;
--section-toolbar-left: calc(12px + var(--tb-size, 25px) + var(--tb-gap, 10px));
--section-toolbar-bottom: 123px;
--nodedc-glass-control-bg: var(--modal-element);
--nodedc-glass-control-hover: color-mix(in srgb, var(--modal-element) 86%, white 8%);
--nodedc-glass-control-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.01);
@ -1928,19 +1938,20 @@ body[data-viewer-mode="guest"] .inspector-restore-tray {
.section-toolbar {
position: fixed;
left: calc(12px + var(--tb-size, 52px) + 10px);
bottom: 12px;
left: var(--section-toolbar-left);
bottom: var(--section-toolbar-bottom);
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%);
gap: var(--tb-gap, 10px);
height: var(--tb-size, 25px);
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
.section-toolbar.hidden {
@ -1948,26 +1959,30 @@ body[data-viewer-mode="guest"] .inspector-restore-tray {
}
.section-tool-btn {
width: 38px;
height: 38px;
width: var(--tb-size, 25px);
height: var(--tb-size, 25px);
display: inline-grid;
place-items: center;
border: 1px solid transparent;
border-radius: calc(var(--tb-radius, 12px) - 4px);
background: transparent;
border: 1px solid var(--toolbar-border);
border-radius: var(--tb-radius, 7px);
background: var(--toolbar-bg);
color: var(--text);
font-size: 13px;
font-size: var(--tb-icon-size, 13px);
font-weight: 700;
line-height: 1;
cursor: pointer;
transition: background 120ms ease, border-color 120ms ease, transform 100ms ease;
padding: var(--tb-icon-pad, 5px);
transition: transform 120ms ease, border-color 120ms ease, box-shadow 120ms ease, background 120ms ease;
}
.section-tool-btn:hover,
.section-tool-btn.active {
.section-tool-btn:hover {
transform: translateY(-1px);
border-color: var(--toolbar-outline, rgba(76, 224, 210, 0.7));
box-shadow: 0 10px 20px color-mix(in srgb, var(--toolbar-outline, rgba(76, 224, 210, 0.7)) 30%, transparent);
}
.section-tool-btn.active {
background: var(--toolbar-bg-active);
color: var(--modal-btn-primary-contrast, #15170f);
}
.section-tool-btn:active {
@ -1976,8 +1991,8 @@ body[data-viewer-mode="guest"] .inspector-restore-tray {
.section-tool-btn svg {
display: block;
width: 21px;
height: 21px;
width: var(--tb-svg-size, 24px);
height: var(--tb-svg-size, 24px);
fill: none;
stroke: currentColor;
stroke-width: 1.8;
@ -1990,21 +2005,14 @@ body[data-viewer-mode="guest"] .inspector-restore-tray {
}
.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 {
width: var(--tb-size, 25px);
height: var(--tb-size, 25px);
flex: 0 0 var(--tb-size, 25px);
display: block;
border: 1px solid var(--toolbar-border);
border-radius: var(--tb-radius, 7px);
background: var(--toolbar-bg);
box-shadow: none;
}
.templates-panel {

View File

@ -1,10 +1,10 @@
const SDK_VERSION = "2.6.78"; // соответствует package.json
const SDK_URL = "./lib/dc-camera-context.es.js";
const SDK_URL = "./lib/dc-camera-context.es.js?v=2";
import { createLogo } from "./src/ui/logo.js";
import { toolbarConfig, applyToolbarVars } from "./src/ui/toolbar.js";
import { createTemplatesMenu } from "./src/ui/templatesMenu.js?v=2";
import { createSettingsMenu } from "./src/ui/settingsMenu.js?v=5";
import { createSettingsMenu } from "./src/ui/settingsMenu.js?v=6";
import { createMeasurementModal } from "./src/ui/measurementModal.js";
import { createGlassSelect } from "./src/ui/glassSelect.js";
import { createSliderControl } from "./src/ui/controls/slider.js";
@ -943,6 +943,12 @@ const cssValue = (name, fallback = "") => {
return (v && v.trim()) || fallback;
};
const clampNumber = (value, min, max, fallback) => {
const num = Number(value);
if (!Number.isFinite(num)) return fallback;
return Math.max(min, Math.min(max, num));
};
const hexToRgb01 = (hex) => {
if (!hex) return [1, 1, 1];
const clean = hex.trim();
@ -998,6 +1004,56 @@ const applyMeasureTheme = () => {
});
};
const sectionPlaneThemeFromCSS = () => ({
axes: {
x: hexToRgb01(cssValue("--section-axis-x", "#ff2b2b")),
y: hexToRgb01(cssValue("--section-axis-y", "#35ff4f")),
z: hexToRgb01(cssValue("--section-axis-z", "#285bff")),
},
gizmoDiameter: clampNumber(parseFloat(cssValue("--section-gizmo-diameter", "1")), 0.5, 1.8, 1),
planeFillColor: hexToRgb01(cssValue("--section-plane-fill", "#606060")),
planeFillAlpha: clampNumber(parseFloat(cssValue("--section-plane-alpha", "0.32")), 0, 1, 0.32),
planeEdgeColor: hexToRgb01(cssValue("--section-plane-edge", "#0d0d0d")),
planeEdgeAlpha: clampNumber(parseFloat(cssValue("--section-plane-edge-alpha", "0.8")), 0, 1, 0.8),
});
const applySectionPlaneTheme = () => {
if (!sectionPlanesPlugin) return;
const theme = sectionPlaneThemeFromCSS();
Object.values(sectionPlanesPlugin._controls || {}).forEach((control) => {
control?.setStyle?.(theme);
});
Object.values(sectionPlanesPlugin._overview?._planes || {}).forEach((plane) => {
const mesh = plane?._mesh;
if (!mesh) return;
if (mesh.material) {
mesh.material.diffuse = theme.planeFillColor;
mesh.material.emissive = theme.planeFillColor;
mesh.material.alpha = theme.planeFillAlpha;
mesh.material.alphaMode = theme.planeFillAlpha < 1 ? "blend" : "opaque";
}
if (mesh.edgeMaterial) {
mesh.edgeMaterial.edgeColor = theme.planeEdgeColor;
mesh.edgeMaterial.edgeAlpha = theme.planeEdgeAlpha;
}
if (mesh.highlightMaterial) {
mesh.highlightMaterial.fillColor = theme.planeFillColor;
mesh.highlightMaterial.fillAlpha = theme.planeFillAlpha;
mesh.highlightMaterial.edgeColor = theme.planeEdgeColor;
mesh.highlightMaterial.edgeAlpha = theme.planeEdgeAlpha;
}
if (mesh.selectedMaterial) {
mesh.selectedMaterial.fillColor = theme.planeFillColor;
mesh.selectedMaterial.fillAlpha = theme.planeFillAlpha;
mesh.selectedMaterial.edgeColor = theme.planeEdgeColor;
mesh.selectedMaterial.edgeAlpha = theme.planeEdgeAlpha;
}
mesh.opacity = theme.planeFillAlpha;
});
viewer?.scene?.glRedraw?.();
sectionPlanesPlugin._overview?._scene?.glRedraw?.();
};
const applyHighlightTheme = () => {
if (!viewer?.scene?.highlightMaterial) return;
const hexToRGB = (hex) => {
@ -1229,8 +1285,21 @@ const setSectionAxisActive = (axis) => {
});
};
const positionSectionToolbar = () => {
if (!sectionButton || !sectionToolbar) return;
const rect = sectionButton.getBoundingClientRect();
if (!rect.width || !rect.height) return;
const gap = parseFloat(cssValue("--tb-gap", "10")) || 10;
const root = document.documentElement;
root.style.setProperty("--section-toolbar-left", `${Math.round(rect.right + gap)}px`);
root.style.setProperty("--section-toolbar-bottom", `${Math.round(Math.max(8, window.innerHeight - rect.bottom))}px`);
};
const setSectionModeActive = (active) => {
sectionModeActive = !!active && !!sectionPlanesPlugin && getSectionPlaneIds().length > 0 && !isGuestMode();
if (sectionModeActive) {
positionSectionToolbar();
}
sectionButton?.classList.toggle("active", sectionModeActive);
sectionToolbar?.classList.toggle("hidden", !sectionModeActive);
document.body.dataset.sectionMode = sectionModeActive ? "active" : "";
@ -1287,6 +1356,7 @@ const createOrUpdateSectionPlane = (axis = "camera") => {
activeSectionPlaneId = sectionPlane.id || activeSectionPlaneId;
}
applySectionPlaneTheme();
sectionPlanesPlugin.showControl(activeSectionPlaneId);
setSectionModeActive(true);
setSectionAxisActive(axis);
@ -1569,6 +1639,11 @@ window.addEventListener("pointerup", scheduleCameraRearm, true);
window.addEventListener("mouseup", scheduleCameraRearm, true);
window.addEventListener("touchend", scheduleCameraRearm, true);
window.addEventListener("touchcancel", scheduleCameraRearm, true);
window.addEventListener("resize", () => {
if (sectionModeActive) {
positionSectionToolbar();
}
});
document.addEventListener("visibilitychange", () => {
if (!document.hidden) {
scheduleCameraRearm();
@ -3099,6 +3174,7 @@ const initViewer = async () => {
overviewVisible: false,
});
sectionPlanesPlugin.setOverviewVisible(false);
applySectionPlaneTheme();
setSectionModeActive(false);
} else {
console.warn("SectionPlanesPlugin is not available in the loaded SDK build");
@ -3971,6 +4047,9 @@ const loadProjectSnapshot = async (project) => {
if (key && key.startsWith("measure")) {
applyMeasureTheme();
}
if (key && key.startsWith("section")) {
applySectionPlaneTheme();
}
focusCanvasAndControls();
},
onShow: () => {
@ -3986,6 +4065,7 @@ const loadProjectSnapshot = async (project) => {
rebuildNavCube(sdk);
applyHighlightTheme();
applyMeasureTheme();
applySectionPlaneTheme();
if (displayModeControl && displayModeSelectHost) {
displayModeSelect = createGlassSelect({
host: displayModeSelectHost,

View File

@ -81,6 +81,14 @@
set("--measure-label", s.measureLabel);
set("--measure-label-text", s.measureLabelText);
set("--measure-dot", s.measureDot);
set("--section-axis-x", s.sectionAxisX);
set("--section-axis-y", s.sectionAxisY);
set("--section-axis-z", s.sectionAxisZ);
set("--section-gizmo-diameter", s.sectionGizmoDiameter);
set("--section-plane-fill", s.sectionPlaneFill);
set("--section-plane-alpha", s.sectionPlaneAlpha);
set("--section-plane-edge", s.sectionPlaneEdge);
set("--section-plane-edge-alpha", s.sectionPlaneEdgeAlpha);
} catch (e) {
// ignore corrupted settings
}
@ -89,7 +97,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=18">
<link rel="stylesheet" href="./dcViewer.css?v=19">
</head>
<body>
<header>
@ -107,7 +115,6 @@
<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>
@ -318,6 +325,7 @@
</div>
<div id="sectionToolbar" class="section-toolbar hidden" aria-label="Управление разрезом">
<canvas id="sectionPlanesOverview" class="section-overview" width="80" height="80"></canvas>
<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>
@ -349,6 +357,6 @@
<input id="fileInput" class="hidden" type="file"
accept=".xkt,.glb,.gltf,.bim,.las,.laz,.obj,.stl">
<script type="module" src="./dcViewer.js?v=23"></script>
<script type="module" src="./dcViewer.js?v=24"></script>
</body>
</html>

View File

@ -126510,6 +126510,9 @@ class TransformControl {
scale: [5, 5, 5],
isObject: false
});
this._rootNode = rootNode;
this._styleScale = 1;
this._baseRootScale = 5;
const pos = math.vec3();
this._setPosition = (function() {
@ -126831,6 +126834,16 @@ class TransformControl {
return {
setPositionActive: (a) => { positionActive = a; updatePositionHandle(); },
setRotationActive: (a) => { rotationActive = a; updateRotationHandle(); },
setColor: (color) => {
if (!color) {
return;
}
material.diffuse = color;
material.emissive = color;
if (hoop.highlightMaterial) {
hoop.highlightMaterial.fillColor = color;
}
},
set visible(v) {
visible = v;
updatePositionHandle();
@ -126868,13 +126881,16 @@ class TransformControl {
{ // Keep gizmo screen size constant
let lastDist = -1;
const setRootNodeScale = size => {
if (rootNode.scale[0] !== size) {
rootNode.scale = [size, size, size];
this._baseRootScale = size;
const styledSize = size * (this._styleScale || 1);
if (rootNode.scale[0] !== styledSize) {
rootNode.scale = [styledSize, styledSize, styledSize];
if (this._handlers && this._handlers.onScreenScale) {
this._handlers.onScreenScale(rootNode.scale);
}
}
};
this._applyRootScale = () => setRootNodeScale(this._baseRootScale || rootNode.scale[0] || 5);
const onSceneTick = scene.on("tick", () => {
const camera = scene.camera;
const dist = Math.abs(math.distVec3(camera.eye, pos));
@ -127030,6 +127046,19 @@ class TransformControl {
this.__destroy();
}
setStyle(style = {}) {
if (style.gizmoDiameter !== undefined) {
this._styleScale = style.gizmoDiameter;
this._applyRootScale?.();
}
if (style.axes) {
this._displayMeshes.x?.setColor?.(style.axes.x);
this._displayMeshes.y?.setColor?.(style.axes.y);
this._displayMeshes.z?.setColor?.(style.axes.z);
}
this._rootNode?.scene?.glRedraw?.();
}
/**
* Called to assign this Control to Handlers.
* Call with a null or undefined value to disconnect the Control from whatever Handlers it was assigned to.
@ -127703,7 +127732,7 @@ class SectionPlanesPlugin extends Plugin {
const planeRoot = (function() {
const rootNode = new Node$1(scene, { isObject: false });
rootNode.addChild(new Mesh(rootNode, { // plane
const planeMesh = rootNode.addChild(new Mesh(rootNode, { // plane
geometry: new ReadableGeometry(rootNode, {
primitive: "triangles",
positions: [
@ -127737,7 +127766,7 @@ class SectionPlanesPlugin extends Plugin {
isObject: false
}));
rootNode.addChild(new Mesh(rootNode, { // Visible frame
const frameMesh = rootNode.addChild(new Mesh(rootNode, { // Visible frame
geometry: new ReadableGeometry(rootNode, buildTorusGeometry({
center: [0, 0, 0],
radius: 1.7,
@ -127769,6 +127798,38 @@ class SectionPlanesPlugin extends Plugin {
isObject: false
}));
const setPhongMaterial = (material, color, alpha) => {
if (!material) {
return;
}
if (color) {
material.diffuse = color;
material.emissive = color;
}
if (alpha !== undefined) {
material.alpha = alpha;
material.alphaMode = alpha < 1 ? "blend" : "opaque";
}
};
const setEmphasisMaterial = (material, fillColor, fillAlpha, edgeColor, edgeAlpha) => {
if (!material) {
return;
}
if (fillColor) {
material.fillColor = fillColor;
}
if (fillAlpha !== undefined) {
material.fillAlpha = fillAlpha;
}
if (edgeColor) {
material.edgeColor = edgeColor;
}
if (edgeAlpha !== undefined) {
material.edgeAlpha = edgeAlpha;
}
};
return {
setPosition: (function() {
const origin = math.vec3();
@ -127781,7 +127842,26 @@ class SectionPlanesPlugin extends Plugin {
})(),
setQuaternion: q => { rootNode.quaternion = q; },
setScale: s => { rootNode.scale = s; },
setVisible: v => { rootNode.visible = v; }
setVisible: v => { rootNode.visible = v; },
setStyle: (style = {}) => {
setPhongMaterial(planeMesh.material, style.planeFillColor, style.planeFillAlpha);
setEmphasisMaterial(
planeMesh.ghostMaterial,
style.planeFillColor,
style.planeFillAlpha,
style.planeEdgeColor,
style.planeEdgeAlpha
);
planeMesh.opacity = style.planeFillAlpha;
setPhongMaterial(frameMesh.material, style.planeEdgeColor, style.planeEdgeAlpha);
setEmphasisMaterial(
frameMesh.highlightMaterial,
style.planeEdgeColor,
style.planeEdgeAlpha,
style.planeEdgeColor,
style.planeEdgeAlpha
);
}
};
})();
let unbindSectionPlane = () => { };
@ -127803,6 +127883,10 @@ class SectionPlanesPlugin extends Plugin {
},
setCulled: c => { culled = c; updateVisible(); },
setVisible: v => { visible = v; updateVisible(); },
setStyle: style => {
planeRoot.setStyle?.(style);
ctrl.setStyle?.(style);
},
_setSectionPlane: sectionPlane => {
unbindSectionPlane();
if (sectionPlane) {

View File

@ -49,6 +49,14 @@ const defaultSettings = {
measureLabel: "#c4ff67",
measureLabelText: "#292929",
measureDot: "#c4ff67",
sectionAxisX: "#ff2b2b",
sectionAxisY: "#35ff4f",
sectionAxisZ: "#285bff",
sectionGizmoDiameter: 1,
sectionPlaneFill: "#606060",
sectionPlaneAlpha: 0.32,
sectionPlaneEdge: "#0d0d0d",
sectionPlaneEdgeAlpha: 0.8,
};
const STORAGE_KEY = "bimdc-settings-v3";
@ -109,6 +117,14 @@ const applySettings = (s) => {
root.style.setProperty("--measure-label", s.measureLabel);
root.style.setProperty("--measure-label-text", s.measureLabelText);
root.style.setProperty("--measure-dot", s.measureDot);
root.style.setProperty("--section-axis-x", s.sectionAxisX);
root.style.setProperty("--section-axis-y", s.sectionAxisY);
root.style.setProperty("--section-axis-z", s.sectionAxisZ);
root.style.setProperty("--section-gizmo-diameter", s.sectionGizmoDiameter);
root.style.setProperty("--section-plane-fill", s.sectionPlaneFill);
root.style.setProperty("--section-plane-alpha", s.sectionPlaneAlpha);
root.style.setProperty("--section-plane-edge", s.sectionPlaneEdge);
root.style.setProperty("--section-plane-edge-alpha", s.sectionPlaneEdgeAlpha);
// подсветка объектов
root.style.setProperty("--highlight-color", s.highlight);
};
@ -352,6 +368,50 @@ export function createSettingsMenu({ panel, controls = {}, onChange = null, onSh
});
panel.appendChild(navCube);
const sectionCut = section("Разрез / сечение");
addColor(sectionCut, "Ось X", "sectionAxisX");
addColor(sectionCut, "Ось Y", "sectionAxisY");
addColor(sectionCut, "Ось Z", "sectionAxisZ");
addSettingsSlider({
parent: sectionCut,
label: "Диаметр гизмо",
value: Math.round(Math.max(0.5, Math.min(1.8, Number(state.sectionGizmoDiameter ?? 1))) * 100),
min: 50,
max: 180,
step: 5,
formatValue: (val) => `${Math.round(val)}%`,
onChange: (val) => {
setVal("sectionGizmoDiameter", Math.max(0.5, Math.min(1.8, Number(val) / 100)));
},
});
addColor(sectionCut, "Плоскость", "sectionPlaneFill");
addSettingsSlider({
parent: sectionCut,
label: "Прозрачность плоскости",
value: Math.round(Math.max(0, Math.min(1, Number(state.sectionPlaneAlpha ?? 0.32))) * 100),
min: 0,
max: 100,
step: 5,
formatValue: (val) => `${Math.round(val)}%`,
onChange: (val) => {
setVal("sectionPlaneAlpha", Math.max(0, Math.min(1, Number(val) / 100)));
},
});
addColor(sectionCut, "Обводка", "sectionPlaneEdge");
addSettingsSlider({
parent: sectionCut,
label: "Прозрачность обводки",
value: Math.round(Math.max(0, Math.min(1, Number(state.sectionPlaneEdgeAlpha ?? 0.8))) * 100),
min: 0,
max: 100,
step: 5,
formatValue: (val) => `${Math.round(val)}%`,
onChange: (val) => {
setVal("sectionPlaneEdgeAlpha", Math.max(0, Math.min(1, Number(val) / 100)));
},
});
panel.appendChild(sectionCut);
const measure = section("Линейка");
addColor(measure, "Линия/бордер", "measureLine");
addColor(measure, "Плашка", "measureLabel");

View File

@ -48,6 +48,14 @@ const PROJECT_THEME = {
measureLabel: "#c4ff67",
measureLabelText: "#292929",
measureDot: "#c4ff67",
sectionAxisX: "#ff2b2b",
sectionAxisY: "#35ff4f",
sectionAxisZ: "#285bff",
sectionGizmoDiameter: 1,
sectionPlaneFill: "#606060",
sectionPlaneAlpha: 0.32,
sectionPlaneEdge: "#0d0d0d",
sectionPlaneEdgeAlpha: 0.8,
};
if (typeof window !== "undefined") {