NODEDC_BIM_VIEWER/frontend/index.html

642 lines
36 KiB
HTML

<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>BIM DC Viewer</title>
<link rel="icon" href="./assets/logo.svg">
<style id="preload-hidden">body{opacity:0;}</style>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Manrope:wght@500;600;700&display=swap">
<script src="./theme.js"></script>
<script>
// Apply persisted theme ASAP to avoid initial flash of defaults
(function () {
const STORAGE_KEY = "bimdc-settings-v3";
const LEGACY_KEYS = ["bimdc-settings", "bimdc-settings-v2"];
try {
LEGACY_KEYS.forEach((k) => localStorage.removeItem(k));
const project = window.__BIMDC_THEME__ || {};
const raw = localStorage.getItem(STORAGE_KEY);
let stored = null;
try {
stored = raw ? JSON.parse(raw) : null;
} catch (e) {
stored = null;
}
const storedVersion = stored && stored.themeVersion;
const projectVersion = project && project.themeVersion;
if (!stored || storedVersion !== projectVersion) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(project));
stored = { ...project };
}
const s = { ...(project || {}), ...(stored || {}) };
if (!Object.keys(s).length) return;
const root = document.documentElement;
const set = (k, v) => v !== undefined && root.style.setProperty(k, v);
const parseColor = (value) => {
if (typeof value !== "string") return null;
const color = value.trim();
const hex = color.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
if (hex) {
const raw = hex[1].length === 3
? hex[1].split("").map((c) => c + c).join("")
: hex[1];
return [
parseInt(raw.slice(0, 2), 16),
parseInt(raw.slice(2, 4), 16),
parseInt(raw.slice(4, 6), 16),
];
}
const rgb = color.match(/^rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)/i);
if (!rgb) return null;
return rgb.slice(1, 4).map((channel) => Math.max(0, Math.min(255, Number(channel))));
};
const readableTextColor = (value) => {
const rgb = parseColor(value);
if (!rgb || rgb.some((channel) => !Number.isFinite(channel))) return "#15170f";
const toLinear = (channel) => {
const value = channel / 255;
return value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
};
const lum = 0.2126 * toLinear(rgb[0]) + 0.7152 * toLinear(rgb[1]) + 0.0722 * toLinear(rgb[2]);
return (lum + 0.05) / 0.05 >= 1.05 / (lum + 0.05) ? "#15170f" : "#f5f5fa";
};
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
const colorParts = (value, fallback) => {
const parsed = parseColor(value);
if (parsed && parsed.every((channel) => Number.isFinite(channel))) return parsed;
const parsedFallback = parseColor(fallback);
if (parsedFallback && parsedFallback.every((channel) => Number.isFinite(channel))) return parsedFallback;
return [28, 28, 28];
};
const alphaValue = (value, fallback) => {
const numeric = Number(value);
return Number.isFinite(numeric) ? clamp(numeric, 0, 1) : fallback;
};
const rgba = (parts, alpha) => {
const [r, g, b] = parts.map((channel) => Math.round(clamp(channel, 0, 255)));
return `rgba(${r}, ${g}, ${b}, ${clamp(alpha, 0, 1).toFixed(3)})`;
};
const applyGlassSettings = () => {
const panelColor = colorParts(s.modalBg, "#1c1c1c");
const focusColor = colorParts(s.modalFocus, "#292929");
const controlColor = colorParts(s.modalElement, s.modalFocus || "#262626");
const outlineColor = colorParts(s.modalBorder, s.modalElementOutline || "#1c1c1c");
const controlOutlineColor = colorParts(s.modalElementOutline, s.modalBorder || "#262626");
const modalBgAlpha = alphaValue(s.modalBgAlpha, 1);
const modalFocusAlpha = alphaValue(s.modalFocusAlpha, 0.35);
const panelAlpha = clamp(0.24 + modalBgAlpha * 0.48, 0.32, 0.88);
const panelStrongAlpha = clamp(panelAlpha + 0.1, 0.42, 0.92);
const panelSoftAlpha = clamp(panelAlpha - 0.16, 0.2, 0.72);
const sectionAlpha = clamp(modalFocusAlpha * 0.6, 0.04, 0.42);
const controlAlpha = clamp(0.12 + modalFocusAlpha * 0.9, 0.16, 0.66);
const controlHoverAlpha = clamp(controlAlpha + 0.08, 0.22, 0.74);
const outlineAlpha = clamp(0.055 + modalFocusAlpha * 0.18, 0.065, 0.2);
const controlOutlineAlpha = clamp(0.045 + modalFocusAlpha * 0.12, 0.055, 0.16);
const panelBg = rgba(panelColor, panelAlpha);
const panelBgStrong = rgba(panelColor, panelStrongAlpha);
const panelBgSoft = rgba(panelColor, panelSoftAlpha);
const sectionBg = rgba(focusColor, sectionAlpha);
const outline = rgba(outlineColor, outlineAlpha);
const outlineBright = rgba(outlineColor, clamp(outlineAlpha + 0.05, 0.08, 0.26));
const outlineMid = rgba(outlineColor, clamp(outlineAlpha + 0.015, 0.07, 0.22));
const outlineDim = rgba(outlineColor, clamp(outlineAlpha * 0.62, 0.04, 0.16));
const controlBg = rgba(controlColor, controlAlpha);
const controlHover = rgba(focusColor, controlHoverAlpha);
const controlShadow = rgba(controlOutlineColor, controlOutlineAlpha);
set("--nodedc-glass-panel-bg", panelBg);
set("--nodedc-glass-panel-bg-strong", panelBgStrong);
set("--nodedc-glass-panel-bg-soft", panelBgSoft);
set("--nodedc-glass-panel-surface", `linear-gradient(180deg, rgba(255, 255, 255, 0.052) 0%, rgba(255, 255, 255, 0.014) 100%), ${panelBg}`);
set("--nodedc-glass-panel-surface-strong", `linear-gradient(180deg, rgba(255, 255, 255, 0.06) 0%, rgba(255, 255, 255, 0.016) 100%), ${panelBgStrong}`);
set("--nodedc-glass-section-bg", `linear-gradient(180deg, rgba(255, 255, 255, 0.038) 0%, rgba(255, 255, 255, 0.01) 100%), ${sectionBg}`);
set("--nodedc-glass-outline", outline);
set("--nodedc-glass-outline-gradient", `linear-gradient(180deg, ${outlineBright} 0%, ${outlineMid} 56%, ${outlineDim} 100%)`);
set("--nodedc-glass-panel-shadow", `inset 0 1px 0 ${outlineDim}, 0 22px 72px rgba(0, 0, 0, 0.42)`);
set("--nodedc-glass-control-bg", `linear-gradient(180deg, rgba(255, 255, 255, 0.052) 0%, rgba(255, 255, 255, 0.018) 100%), ${controlBg}`);
set("--nodedc-glass-control-hover", `linear-gradient(180deg, rgba(255, 255, 255, 0.075) 0%, rgba(255, 255, 255, 0.026) 100%), ${controlHover}`);
set("--nodedc-glass-control-shadow", `inset 0 1px 0 ${controlShadow}`);
set("--nodedc-header-dropdown-bg", panelBgStrong);
set("--nodedc-header-dropdown-item-bg", controlBg);
set("--nodedc-header-dropdown-item-hover-bg", controlHover);
set("--nodedc-header-dropdown-item-active-bg", s.toolbarBgActive);
set("--nodedc-header-dropdown-item-active-color", s.toolbarBgActive ? readableTextColor(s.toolbarBgActive) : undefined);
};
set("--bg-outer", s.bgOuter);
set("--canvas-top", s.canvasTop);
set("--canvas-bottom", s.canvasBottom);
set("--loading-top", s.loadingTop);
set("--loading-bottom", s.loadingBottom);
set("--accent", s.accent);
set("--accent-2", s.accent2);
set("--template-fill", s.templateFill);
set("--template-outline", s.templateOutline);
set("--template-outline-hover", s.templateOutlineHover);
set("--template-glow-strength", `${s.templateGlow}%`);
set("--toolbar-bg", s.toolbarBg);
set("--toolbar-bg-active", s.toolbarBgActive);
set("--toolbar-bg-active-contrast", s.toolbarBgActive ? readableTextColor(s.toolbarBgActive) : undefined);
set("--toolbar-border", s.toolbarBorder);
set("--toolbar-outline", s.toolbarOutline);
set("--modal-bg", s.modalBg);
set("--modal-bg-alpha", s.modalBgAlpha !== undefined ? `${s.modalBgAlpha * 100}%` : undefined);
set("--modal-focus", s.modalFocus);
set("--modal-focus-alpha", s.modalFocusAlpha !== undefined ? `${s.modalFocusAlpha * 100}%` : undefined);
set("--modal-border", s.modalBorder);
set("--modal-element", s.modalElement);
set("--modal-element-outline", s.modalElementOutline);
set("--modal-radius", s.modalRadius);
applyGlassSettings();
set("--project-btn-fill", s.projectBtnFill);
set("--project-btn-border", s.projectBtnBorder);
set("--project-btn-hover", s.projectBtnHover);
set("--modal-btn-primary", s.modalBtnPrimary);
set("--modal-btn-primary-contrast", s.modalBtnPrimary ? readableTextColor(s.modalBtnPrimary) : undefined);
set("--modal-btn-secondary", s.modalBtnSecondary);
set("--loader-color", s.loaderColor);
set("--border", s.border);
set("--text", s.text);
set("--panel", s.panel);
set("--panel-2", s.panel2);
set("--highlight-color", s.highlight);
set("--cube-text", s.navCubeText || s.cubeText);
set("--cube-edges", s.navCubeBase || s.cubeEdges);
set("--cube-base", s.navCubeBase || s.cubeEdges);
set("--cube-hover", s.navCubeHover);
set("--cube-edge-color", s.navCubeEdge);
set("--cube-edge-alpha", s.navCubeEdgeAlpha);
set("--plus-color", s.plus);
set("--measure-line", s.measureLine);
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-gizmo-thickness", s.sectionGizmoThickness);
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);
set("--comment-target-active", s.commentTargetActiveColor);
set("--comment-target-active-alpha", s.commentTargetActiveAlpha);
set("--comment-target-active-size", s.commentTargetActiveSize !== undefined ? `${s.commentTargetActiveSize}px` : undefined);
set("--comment-target-passive", s.commentTargetPassiveColor);
set("--comment-target-passive-alpha", s.commentTargetPassiveAlpha);
set("--comment-target-passive-size", s.commentTargetPassiveSize !== undefined ? `${s.commentTargetPassiveSize}px` : undefined);
} catch (e) {
// ignore corrupted settings
}
}());
</script>
<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=32">
</head>
<body>
<header>
<button class="logo-btn" id="homeButton" title="Вернуться к старту"></button>
<div class="toolbar"></div>
<div class="hidden-controls" style="display:none">
<input type="checkbox" id="toggleEdges" checked>
<input type="checkbox" id="toggleTransparent" checked>
<input type="checkbox" id="addMode" checked>
<button id="fitView"></button>
</div>
</header>
<main class="main">
<section class="viewer-wrapper">
<canvas id="viewerCanvas"></canvas>
<canvas id="navCube"></canvas>
<div class="drop-hint" id="dropZone">
<div class="drop-center">
<div class="drop-plus">+</div>
<div class="drop-formats">
<div class="muted">IFC · BIM · LAS · LAZ</div>
<div class="muted">GLB · GLTF · OBJ · STL</div>
</div>
</div>
</div>
<div class="status-bar" id="status">
<span class="dot"></span>
<span id="statusText">Готов к загрузке</span>
</div>
<div class="loader hidden" id="loader">
<div class="loader__figure"></div>
<p class="loader__label">DC</p>
</div>
</section>
<aside class="side-panel" id="viewerInspector">
<div class="inspector-panel-header">
<h3>Инспектор</h3>
<button
class="inspector-collapse-btn"
id="inspectorCollapseButton"
type="button"
title="Свернуть инспектор"
aria-label="Свернуть инспектор"
>
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
<path d="M4.25 4.25 11.75 11.75M11.75 4.25 4.25 11.75" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path>
</svg>
</button>
</div>
<div class="inspector-panel-body">
<div class="panel-section inspector-main-section">
<div id="treeContainer" class="tree"></div>
<div class="inspector-controls">
<div class="inspector-control-row inspector-control-row--half" id="displayModeControl">
<div class="inspector-control-label">Режим отображения</div>
<div class="inspector-select-host" id="displayModeSelect"></div>
</div>
<div class="inspector-control-row inspector-control-row--half" id="lightingModeControl">
<div class="inspector-control-label">Свет</div>
<div class="inspector-select-host" id="lightingModeSelect"></div>
</div>
<div class="inspector-control-row inspector-control-row--full" id="navigationAxisControl">
<div class="inspector-control-label">Ось навигации</div>
<div class="inspector-select-host" id="navigationAxisSelect"></div>
</div>
<div class="inspector-control-row inspector-control-row--full inspector-control-row--slider" id="zoomSpeedControl"></div>
</div>
</div>
<div class="panel-section" id="projectNameSection">
<h3>Название проекта</h3>
<input type="text" id="projectNameInput" class="project-name-input" placeholder="Проект" autocomplete="off">
<div class="error-text inline-error" id="projectNameError"></div>
</div>
<div class="panel-section">
<h3>Глобальные координаты</h3>
<div class="muted" style="margin-bottom:6px;">Центр выделения (мировые)</div>
<div class="transform-grid">
<label>X <input type="text" id="globalX"></label>
<label>Y <input type="text" id="globalY"></label>
<label>Z <input type="text" id="globalZ"></label>
</div>
</div>
<div class="panel-section">
<h3>Трансформации</h3>
<div class="muted" style="margin-bottom:6px;">Перемещение</div>
<div class="transform-grid">
<label>X <input type="text" id="posX"></label>
<label>Y <input type="text" id="posY"></label>
<label>Z <input type="text" id="posZ"></label>
</div>
<div class="muted" style="margin:8px 0 6px;">Вращение (deg)</div>
<div class="transform-grid">
<label>X <input type="text" id="rotX"></label>
<label>Y <input type="text" id="rotY"></label>
<label>Z <input type="text" id="rotZ"></label>
</div>
<div class="muted" style="margin:8px 0 6px;">Масштаб</div>
<div class="transform-grid">
<label>X <input type="text" id="scaleX"></label>
<label>Y <input type="text" id="scaleY"></label>
<label>Z <input type="text" id="scaleZ"></label>
</div>
<small id="transformHint" class="muted">Выберите объект в инспекторе или сцене, чтобы редактировать.</small>
</div>
<div class="panel-section">
<h3>Журнал</h3>
<div id="logList" class="log"></div>
</div>
<div class="panel-section" id="saveProjectSection">
<button class="secondary-btn model-settings-btn" id="saveModelSettingsButton" type="button">Сохранить настройки</button>
<button class="primary-btn" id="saveProjectButton" type="button">Сохранить проект</button>
<button class="secondary-btn danger-btn" id="deleteProjectButton" type="button">Удалить проект</button>
</div>
</div>
</aside>
<div class="inspector-restore-tray" id="inspectorRestoreTray" hidden>
<button class="inspector-restore-button" id="inspectorRestoreButton" type="button">Инспектор</button>
</div>
<aside class="comments-panel hidden" id="commentsPanel">
<div class="comments-panel-header" id="commentsPanelHeader">
<h3>Комментарии</h3>
<button
class="inspector-collapse-btn"
id="commentsCollapseButton"
type="button"
title="Опустить комментарии"
aria-label="Опустить комментарии"
>
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
<path d="M4 6.25 8 10.25 12 6.25" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
</button>
</div>
<div class="comments-panel-body">
<div id="commentsList" class="comments-list"></div>
</div>
</aside>
<div class="comments-restore-tray" id="commentsRestoreTray" hidden>
<button class="inspector-restore-button" id="commentsRestoreButton" type="button">Комментарии</button>
</div>
<div id="commentTargetsLayer" class="comment-targets-layer hidden"></div>
</main>
<div id="templatesPanel" class="templates-panel hidden"></div>
<div id="settingsPanel" class="settings-panel hidden"></div>
<div id="commentContextMenu" class="nodedc-dropdown-surface bim-context-menu hidden" role="menu" aria-label="Действия по модели">
<button class="nodedc-dropdown-option bim-context-menu__item" data-comment-menu-action="create" type="button" role="menuitem">
<span>Комментарий</span>
</button>
</div>
<div id="measureModalBackdrop" class="modal-backdrop hidden"></div>
<div id="measureModal" class="templates-panel measurement-modal hidden"></div>
<div id="objectModalBackdrop" class="modal-backdrop hidden"></div>
<div id="objectModal" class="templates-panel measurement-modal hidden"></div>
<div id="commentModalBackdrop" class="modal-backdrop hidden"></div>
<div id="commentModal" class="comment-card-modal ops-comment-detail hidden" role="dialog" aria-modal="true" aria-label="Комментарий к модели">
<div class="comment-card-modal__header" id="commentModalHeader">
<div class="ops-detail-actions">
<button class="ops-detail-icon-btn" type="button" aria-label="Уведомления" title="Уведомления">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M18 8a6 6 0 0 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9"></path>
<path d="M10 21h4"></path>
</svg>
</button>
<button class="ops-detail-icon-btn" id="commentModalExpandButton" type="button" aria-label="Развернуть" title="Развернуть">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M14 5h5v5M19 5l-7 7M10 19H5v-5M5 19l7-7"></path>
</svg>
</button>
<button class="ops-detail-icon-btn modal-close" id="commentModalClose" type="button" aria-label="Закрыть" title="Закрыть">
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
<path d="M4.25 4.25 11.75 11.75M11.75 4.25 4.25 11.75" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path>
</svg>
</button>
</div>
</div>
<div class="comment-card-modal__body">
<section class="ops-detail-hero">
<div class="comment-card-modal__subtitle" id="commentModalSubtitle">BIM-COMMENT</div>
<input class="ops-detail-title-input" type="text" id="commentTitleInput" autocomplete="off" placeholder="Новый комментарий">
<textarea class="ops-detail-description-input" id="commentBodyInput" rows="2" placeholder="Нажмите, чтобы добавить описание"></textarea>
<div class="ops-detail-meta-row">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M3 12a9 9 0 1 0 3-6.7M3 4v6h6"></path>
<path d="M12 7v5l3 2"></path>
</svg>
<span id="commentModalEditedMeta">Последнее редактирование: только что</span>
</div>
<div class="ops-detail-pills">
<button class="ops-detail-pill" id="commentAddSubitemButton" type="button">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 3 4 7l8 4 8-4-8-4ZM4 12l8 4 8-4M4 17l8 4 8-4"></path>
</svg>
<span>Добавить подэлемент</span>
</button>
</div>
<div id="commentSubitemMenu" class="nodedc-dropdown-surface bim-subitem-menu hidden" role="menu" aria-label="Тип подэлемента">
<button class="nodedc-dropdown-option" data-comment-subitem-kind="text" type="button" role="menuitem">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M4 6h16M4 12h10M4 18h12"></path>
</svg>
<span>Создать текстовый блок</span>
</button>
<button class="nodedc-dropdown-option" data-comment-subitem-kind="checker" type="button" role="menuitem">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M9 11l2 2 4-5"></path>
<path d="M4 5h16M4 12h3M4 19h16"></path>
</svg>
<span>Создать чекер</span>
</button>
</div>
</section>
<section class="ops-comment-subitems hidden" id="commentSubitemsSection">
<div class="ops-detail-section-title">Подэлементы</div>
<div class="ops-comment-subitems__list" id="commentSubitemsList"></div>
</section>
<section class="comment-attachments ops-detail-dropzone" id="commentDropzone">
<div class="ops-detail-dropzone__icon">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 16V7"></path>
<path d="m8 11 4-4 4 4"></path>
<path d="M20 16.5a4.5 4.5 0 0 0-8.7-1.6A3.5 3.5 0 1 0 6.5 19H18a2 2 0 0 0 2-2.5Z"></path>
</svg>
</div>
<div class="ops-detail-dropzone__copy">
<strong>Перетащите файлы сюда или нажмите для выбора</strong>
<span>Любой формат, несколько файлов за раз, до 50 МБ на файл</span>
</div>
<button type="button" class="comment-attach-button" id="commentAttachButton">Выбрать</button>
<input type="file" id="commentAttachmentInput" multiple hidden>
<div class="comment-attachment-grid" id="commentAttachmentGrid"></div>
</section>
<section class="comment-thread ops-detail-activity">
<div class="ops-detail-section-head">
<div class="comment-thread__title">Активность</div>
<div class="ops-detail-section-actions">
<button class="ops-detail-mini-btn" type="button" aria-label="Сортировка" title="Сортировка">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M4 6h12M4 12h8M4 18h4M18 8v10M15 15l3 3 3-3"></path>
</svg>
</button>
<button class="ops-detail-mini-btn" type="button" aria-label="Фильтр" title="Фильтр">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M4 6h16M7 12h10M10 18h4"></path>
</svg>
</button>
</div>
</div>
<label class="ops-activity-composer">
<span>Добавить комментарий</span>
<textarea id="commentReplyInput" rows="5" placeholder="Добавить комментарий"></textarea>
<div class="ops-activity-composer__toolbar">
<button type="button" class="ops-detail-mini-btn" id="commentReplyAttachButton" aria-label="Прикрепить файл" title="Прикрепить файл">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="m21.4 11.6-8.5 8.5a6 6 0 0 1-8.5-8.5l8.5-8.5a4 4 0 0 1 5.7 5.7l-8.5 8.5a2 2 0 0 1-2.8-2.8l7.8-7.8"></path>
</svg>
</button>
<button type="button" class="ops-detail-mini-btn" aria-label="Реакция" title="Реакция">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Z"></path>
<path d="M8 14s1.5 2 4 2 4-2 4-2M9 9h.01M15 9h.01"></path>
</svg>
</button>
<button type="button" class="ops-activity-send" id="commentApplyInlineButton" aria-label="Отправить комментарий" title="Отправить комментарий">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 19V5"></path>
<path d="m5 12 7-7 7 7"></path>
</svg>
</button>
<input type="file" id="commentReplyAttachmentInput" multiple hidden>
</div>
</label>
<div class="comment-attachment-grid comment-attachment-grid--reply" id="commentReplyAttachmentGrid"></div>
<div class="comment-thread__list" id="commentThreadList"></div>
</section>
</div>
<div class="comment-card-modal__footer">
<div class="error-text comment-card-modal__footer-error" id="commentModalError" hidden></div>
<button class="secondary-btn" id="commentCancelButton" type="button">Отменить</button>
<button class="primary-btn" id="commentApplyButton" type="button">Применить</button>
<button class="primary-btn" id="commentSaveButton" type="button">Сохранить</button>
</div>
</div>
<div id="saveProjectBackdrop" class="modal-backdrop hidden"></div>
<div id="saveProjectModal" class="save-project-modal hidden">
<div class="modal-header">
<div class="modal-title">Сохранить проект</div>
<button class="modal-close inspector-collapse-btn" id="saveProjectClose" aria-label="Закрыть">
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
<path d="M4.25 4.25 11.75 11.75M11.75 4.25 4.25 11.75" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path>
</svg>
</button>
</div>
<label for="saveProjectName" class="input-label">Имя проекта</label>
<input type="text" id="saveProjectName" placeholder="Новый проект" autocomplete="off">
<div class="input-hint" id="saveProjectHint">Введите имя, чтобы сохранить проект в хранилище</div>
<div class="modal-actions">
<button class="secondary-btn" id="saveProjectCancel" type="button">Отмена</button>
<button class="primary-btn" id="saveProjectConfirm" type="button" disabled>Сохранить</button>
<button class="secondary-btn" id="saveProjectDuplicate" type="button" disabled>Сохранить как новый</button>
</div>
<div class="error-text" id="saveProjectError"></div>
</div>
<div id="deleteProjectBackdrop" class="modal-backdrop hidden"></div>
<div id="deleteProjectModal" class="save-project-modal hidden">
<div class="modal-header">
<div class="modal-title">Удалить проект</div>
<button class="modal-close inspector-collapse-btn" id="deleteProjectClose" aria-label="Закрыть">
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
<path d="M4.25 4.25 11.75 11.75M11.75 4.25 4.25 11.75" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path>
</svg>
</button>
</div>
<div class="input-hint">Вы уверены, что хотите удалить проект?</div>
<div class="modal-actions">
<button class="secondary-btn" id="deleteProjectCancel" type="button">Отмена</button>
<button class="primary-btn danger-btn" id="deleteProjectConfirm" type="button">Удалить</button>
</div>
<div class="error-text" id="deleteProjectError"></div>
</div>
<div id="shareModalBackdrop" class="modal-backdrop hidden"></div>
<div id="shareModal" class="save-project-modal hidden">
<div class="modal-header">
<div class="modal-title">Ссылка на модель</div>
<button class="modal-close inspector-collapse-btn" id="shareModalClose" aria-label="Закрыть">
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
<path d="M4.25 4.25 11.75 11.75M11.75 4.25 4.25 11.75" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path>
</svg>
</button>
</div>
<input type="text" id="shareLinkInput" readonly autocomplete="off">
<div class="modal-actions">
<button class="secondary-btn" id="shareModalCancel" type="button">Закрыть</button>
<button class="primary-btn" id="shareCopyButton" type="button" disabled>Скопировать</button>
</div>
<div class="error-text" id="shareError"></div>
</div>
<div id="bottomToolbar" class="bottom-toolbar">
<button class="tb-btn" data-action="settings" title="Настройки" aria-label="Настройки">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 15.5A3.5 3.5 0 1 0 12 8a3.5 3.5 0 0 0 0 7.5Z"></path>
<path d="M19.4 15a1.7 1.7 0 0 0 .34 1.88l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06A1.7 1.7 0 0 0 15 19.37a1.7 1.7 0 0 0-1 .56V20a2 2 0 0 1-4 0v-.08a1.7 1.7 0 0 0-1-.56 1.7 1.7 0 0 0-1.88.34l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.7 1.7 0 0 0 4.63 15a1.7 1.7 0 0 0-.56-1H4a2 2 0 0 1 0-4h.08a1.7 1.7 0 0 0 .56-1 1.7 1.7 0 0 0-.34-1.88l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.7 1.7 0 0 0 9 4.63a1.7 1.7 0 0 0 1-.56V4a2 2 0 0 1 4 0v.08a1.7 1.7 0 0 0 1 .56 1.7 1.7 0 0 0 1.88-.34l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.7 1.7 0 0 0 19.37 9c.18.38.38.73.56 1H20a2 2 0 0 1 0 4h-.08c-.18.27-.37.62-.52 1Z"></path>
</svg>
</button>
<button class="tb-btn" data-action="templates" title="Примеры" aria-label="Примеры">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M3.5 6.5h6l2 2h9v8.75a2.25 2.25 0 0 1-2.25 2.25H5.75A2.25 2.25 0 0 1 3.5 17.25V6.5Z"></path>
<path d="M3.5 10h17"></path>
</svg>
</button>
<button class="tb-btn" data-action="measure" title="Линейка" aria-label="Линейка">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M4.5 15.5 15.5 4.5l4 4-11 11-4-4Z"></path>
<path d="m8 12 2 2"></path>
<path d="m10.5 9.5 1.5 1.5"></path>
<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="comments" title="Комментарии" aria-label="Комментарии">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M6.5 5.5h11a2.5 2.5 0 0 1 2.5 2.5v6.25a2.5 2.5 0 0 1-2.5 2.5H12l-4.3 3.1v-3.1H6.5A2.5 2.5 0 0 1 4 14.25V8a2.5 2.5 0 0 1 2.5-2.5Z"></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">
<path d="M12 14.5V4.75"></path>
<path d="m8.25 8.5 3.75-3.75 3.75 3.75"></path>
<path d="M7.25 11.25H6.5a2 2 0 0 0-2 2v4.25a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2v-4.25a2 2 0 0 0-2-2h-.75"></path>
</svg>
</button>
<button class="tb-btn" data-action="add" title="Добавить файл" aria-label="Добавить файл">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 5v14"></path>
<path d="M5 12h14"></path>
</svg>
</button>
</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 16 16" aria-hidden="true">
<path d="M1.7 8s2.35-4 6.3-4 6.3 4 6.3 4-2.35 4-6.3 4-6.3-4-6.3-4Z"></path>
<circle cx="8" cy="8" r="2.15"></circle>
</svg>
</button>
<button class="section-tool-btn section-axis-btn" data-section-action="x" type="button" title="Ось X" aria-label="Ось X">
<svg class="section-letter-icon" viewBox="0 0 16 16" aria-hidden="true">
<text x="8" y="8" text-anchor="middle" dominant-baseline="central">X</text>
</svg>
</button>
<button class="section-tool-btn section-axis-btn" data-section-action="y" type="button" title="Ось Y" aria-label="Ось Y">
<svg class="section-letter-icon" viewBox="0 0 16 16" aria-hidden="true">
<text x="8" y="8" text-anchor="middle" dominant-baseline="central">Y</text>
</svg>
</button>
<button class="section-tool-btn section-axis-btn" data-section-action="z" type="button" title="Ось Z" aria-label="Ось Z">
<svg class="section-letter-icon" viewBox="0 0 16 16" aria-hidden="true">
<text x="8" y="8" text-anchor="middle" dominant-baseline="central">Z</text>
</svg>
</button>
<button class="section-tool-btn" data-section-action="flip" type="button" title="Инвертировать" aria-label="Инвертировать">
<svg viewBox="0 0 16 16" aria-hidden="true">
<path d="M5.5 4.7h4.4a3.05 3.05 0 0 1 0 6.1H5.6"></path>
<path d="M6.7 2.9 4.9 4.7l1.8 1.8"></path>
<path d="m9.3 13.1 1.8-1.8-1.8-1.8"></path>
</svg>
</button>
<button class="section-tool-btn" data-section-action="clear" type="button" title="Удалить разрез" aria-label="Удалить разрез">
<svg viewBox="0 0 16 16" aria-hidden="true">
<path d="M3.2 4.5h9.6"></path>
<path d="M6.2 4.5V3.1h3.6v1.4"></path>
<path d="m4.4 4.5.55 8.4h6.1l.55-8.4"></path>
<path d="M6.65 6.5v4.3"></path>
<path d="M9.35 6.5v4.3"></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=45"></script>
</body>
</html>