Initial import of NODEDC BIM Viewer

This commit is contained in:
CODEX
2026-06-04 13:25:42 +03:00
commit 9aaf82862f
2075 changed files with 3592575 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
# Frontend
Статический клиент переехал в директорию `frontend/`. Точка входа — `index.html` (использует модульный `dcViewer.js` и стили/ассеты из этой папки).
Быстрый запуск
- Совместно с API: `npm --prefix server start` (по умолчанию PORT=8080; если занят, сервер попробует 8081/8082/8083). Открыть http://localhost:<порт>.
- Только фронт (без API): `npx http-server frontend -p 9001` и открыть http://localhost:9001
API-запросы идут на тот же origin (`/api/projects`), поэтому при отдельном поднятии фронта без сервера API работать не будет.
Снимок проекта собирается на фронте (модели, состояние камеры/виджетов, трансформы, дизайн) и отправляется POST `/api/projects`.
+1
View File
@@ -0,0 +1 @@
<svg id="Слой_1" data-name="Слой 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 280.05 54.55"><defs><style>.cls-1{fill:#e2e1e1;}</style></defs><path class="cls-1" d="M52.8,23.61,46.92,33.76,41.05,23.61H52.8m18-10.39H23.06L46.92,54.55Z"/><polygon class="cls-1" points="31.28 33.13 18.11 10.34 75.73 10.34 62.59 33.13 74.28 33.13 93.22 0 0 0 19.61 33.13 31.28 33.13"/><path class="cls-1" d="M115,18.56V1h5.63c5.51,0,8.86,3.52,8.86,8.8s-3.35,8.81-8.86,8.81Zm2.19-2h3.44c4.28,0,6.62-2.69,6.62-6.79S124.94,3,120.66,3h-3.44Z"/><path class="cls-1" d="M131.64,9.75c0-5.33,3.29-8.95,8.4-8.95,3.9,0,6.74,2.26,7.4,5.91h-2.32a5.07,5.07,0,0,0-5.18-3.9c-3.7,0-6.06,2.87-6.06,6.94s2.26,6.94,6,6.94a5.05,5.05,0,0,0,5.26-3.89h2.31c-.65,3.64-3.57,5.91-7.55,5.91C134.81,18.71,131.64,15.14,131.64,9.75Z"/><path class="cls-1" d="M156.65,1h6.24c3.75,0,5.84,1.81,5.84,4.65A3.82,3.82,0,0,1,166,9.43a3.94,3.94,0,0,1,3.5,4.1c0,3.11-2.21,5-6.39,5h-6.42Zm2.19,2V8.59H163c2.27,0,3.55-1,3.55-2.84S165.28,2.94,163,2.94Zm0,7.62v6h4.23c2.72,0,4.15-1.13,4.15-2.94,0-2-1.53-3.07-4.15-3.07Z"/><path class="cls-1" d="M172.21,1h2.19V18.56h-2.19Z"/><path class="cls-1" d="M177.82,1h2.77l6.24,14.61L193,1h2.72V18.56h-2.14V4.67l-5.84,13.89H185.8L180,4.67V18.56h-2.14Z"/><path class="cls-1" d="M204.25,1h2.31L212,16,217.3,1h2.27l-6.34,17.61h-2.64Z"/><path class="cls-1" d="M220.94,2.23a1.33,1.33,0,1,1,2.66,0,1.33,1.33,0,1,1-2.66,0Zm.28,3.5h2.06V18.56h-2.06Z"/><path class="cls-1" d="M225.65,12.14c0-3.9,2.46-6.56,6.08-6.56s6,2.29,6.09,6.08c0,.28,0,.58,0,.89h-10v.17a3.93,3.93,0,0,0,4.05,4.15,3.52,3.52,0,0,0,3.65-2.61h2.08A5.45,5.45,0,0,1,232,18.71C228.16,18.71,225.65,16.07,225.65,12.14Zm10-1.31a3.57,3.57,0,0,0-3.85-3.44,3.81,3.81,0,0,0-3.83,3.44Z"/><path class="cls-1" d="M238.56,5.73h2.16l3.19,10.44,3.38-10.44h1.91l3.19,10.44,3.35-10.44h2.09l-4.28,12.83h-2.26L248.19,9,245,18.56h-2.26Z"/><path class="cls-1" d="M258.57,12.14c0-3.9,2.46-6.56,6.08-6.56s6,2.29,6.09,6.08c0,.28,0,.58-.05.89h-10v.17a3.93,3.93,0,0,0,4,4.15,3.52,3.52,0,0,0,3.65-2.61h2.08a5.45,5.45,0,0,1-5.58,4.45C261.08,18.71,258.57,16.07,258.57,12.14Zm10-1.31a3.57,3.57,0,0,0-3.85-3.44,3.81,3.81,0,0,0-3.83,3.44Z"/><path class="cls-1" d="M279.63,5.73v2h-1c-2.7,0-3.42,2.24-3.42,4.28v6.54h-2.07V5.73h1.79l.28,1.93a4,4,0,0,1,3.87-1.93Z"/></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

File diff suppressed because it is too large Load Diff
+3133
View File
File diff suppressed because it is too large Load Diff
+32
View File
@@ -0,0 +1,32 @@
{
"examples": [
{
"id": "ifc-xkt",
"name": "IFC / XKT пример",
"description": "Стандартная BIM-модель Schependomlaan.",
"type": "xkt",
"url": "./examples/ifc-xkt/Schependomlaan.ifc.xkt"
},
{
"id": "glb",
"name": "GLB пример",
"description": "Городской фрагмент MAP в формате glTF/GLB.",
"type": "gltf",
"url": "./examples/glb/MAP.glb"
},
{
"id": "pointcloud",
"name": "Point Cloud пример",
"description": "Облако точек Nalls Pumpkin Hill (LAZ).",
"type": "las",
"url": "./examples/pointcloud/Nalls_Pumpkin_Hill.laz"
},
{
"id": "bim",
"name": "BIM (.bim) пример",
"description": "Тестовая BIM-модель BlenderHouse в формате .bim.",
"type": "bim",
"url": "./examples/bim/BlenderHouse.bim"
}
]
}
Binary file not shown.
+32
View File
@@ -0,0 +1,32 @@
{
"examples": [
{
"id": "ifc-xkt",
"name": "IFC / XKT пример",
"description": "Стандартная BIM-модель Schependomlaan.",
"type": "xkt",
"url": "./examples/ifc-xkt/Schependomlaan.ifc.xkt"
},
{
"id": "glb",
"name": "GLB пример",
"description": "Городской фрагмент MAP в формате glTF/GLB.",
"type": "gltf",
"url": "./examples/glb/MAP.glb"
},
{
"id": "pointcloud",
"name": "Point Cloud пример",
"description": "Облако точек Nalls Pumpkin Hill (LAZ).",
"type": "las",
"url": "./examples/pointcloud/Nalls_Pumpkin_Hill.laz"
},
{
"id": "bim",
"name": "BIM (.bim) пример",
"description": "Тестовая BIM-модель BlenderHouse в формате .bim.",
"type": "bim",
"url": "./examples/bim/BlenderHouse.bim"
}
]
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
+228
View File
@@ -0,0 +1,228 @@
<!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);
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-border", s.toolbarBorder);
set("--toolbar-outline", s.toolbarOutline);
set("--modal-bg", s.modalBg);
set("--modal-focus", s.modalFocus);
set("--modal-border", s.modalBorder);
set("--modal-element", s.modalElement);
set("--modal-element-outline", s.modalElementOutline);
set("--modal-radius", s.modalRadius);
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-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);
} 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=3">
</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">
<div class="panel-section">
<h3>Инспектор</h3>
<div id="treeContainer" class="tree"></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="primary-btn" id="saveProjectButton" type="button">Сохранить проект</button>
<button class="secondary-btn danger-btn" id="deleteProjectButton" type="button">Удалить проект</button>
</div>
</aside>
</main>
<div id="templatesPanel" class="templates-panel hidden"></div>
<div id="settingsPanel" class="settings-panel hidden"></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="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" id="saveProjectClose" aria-label="Закрыть">×</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" id="deleteProjectClose" aria-label="Закрыть">×</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="bottomToolbar" class="bottom-toolbar">
<button class="tb-btn" data-action="settings" title="Настройки"></button>
<button class="tb-btn" data-action="templates" title="Примеры">📂</button>
<button class="tb-btn" data-action="measure" title="Линейка">📏</button>
<button class="tb-btn" data-action="projection" title="Перспектива / аксонометрия">A</button>
<button class="tb-btn" data-action="add" title="Добавить файл"></button>
</div>
<input id="fileInput" class="hidden" type="file"
accept=".xkt,.glb,.gltf,.bim,.las,.laz,.obj,.stl">
<script type="module" src="./dcViewer.js"></script>
</body>
</html>
File diff suppressed because one or more lines are too long
+33
View File
@@ -0,0 +1,33 @@
export function createSliderControl({ label = "", value = 0, min = 0, max = 100, step = 1, onChange }) {
const row = document.createElement("div");
row.className = "slider-row";
const lbl = document.createElement("label");
lbl.textContent = label;
lbl.className = "slider-label";
const input = document.createElement("input");
input.type = "range";
input.className = "slider-input";
input.min = min;
input.max = max;
input.step = step;
input.value = value;
const updateFill = () => {
const percent = ((input.value - input.min) / (input.max - input.min)) * 100;
input.style.setProperty("--slider-percent", `${percent}%`);
};
input.addEventListener("input", () => {
updateFill();
const v = Number(input.value);
onChange?.(v);
});
updateFill();
row.appendChild(lbl);
row.appendChild(input);
return row;
}
+24
View File
@@ -0,0 +1,24 @@
export function createLogo({
size = 160, // ширина контейнера (px)
aspect = 4, // соотношение width / height контейнера
offsetY = 0,
paddingX = 0,
imageUrl = "./assets/logo.svg",
} = {}) {
const wrapper = document.createElement("div");
wrapper.className = "ue-logo";
wrapper.style.width = `${size}px`;
wrapper.style.height = `${size / aspect}px`;
wrapper.style.marginLeft = `${paddingX}px`;
wrapper.style.marginRight = `${paddingX}px`;
wrapper.style.transform = `translateY(${offsetY}px)`;
const img = document.createElement("img");
img.src = imageUrl;
img.alt = "logo";
img.style.width = "100%";
img.style.height = "100%";
img.style.objectFit = "contain";
img.style.display = "block";
wrapper.appendChild(img);
return wrapper;
}
+90
View File
@@ -0,0 +1,90 @@
export function createMeasurementModal({ panel, backdrop, onDelete, onToggleVisibility, onClose }) {
let activeMeasurementId = null;
let currentVisible = true;
const hide = () => {
if (panel) {
panel.classList.add("hidden");
panel.style.display = "";
}
if (backdrop) {
backdrop.classList.add("hidden");
backdrop.style.display = "";
}
activeMeasurementId = null;
onClose?.();
};
const render = () => {
if (!panel) return;
panel.innerHTML = "";
const header = document.createElement("div");
header.className = "templates-header";
header.innerHTML = `<span class="templates-title">Размер</span>`;
const closeBtn = document.createElement("button");
closeBtn.className = "templates-close";
closeBtn.textContent = "×";
closeBtn.addEventListener("click", hide);
header.appendChild(closeBtn);
panel.appendChild(header);
const list = document.createElement("div");
list.className = "template-list";
panel.appendChild(list);
const deleteBtn = document.createElement("button");
deleteBtn.className = "example-card";
deleteBtn.textContent = "Удалить размер";
deleteBtn.addEventListener("click", () => {
if (!activeMeasurementId) return;
onDelete?.(activeMeasurementId);
});
list.appendChild(deleteBtn);
const toggleBtn = document.createElement("button");
toggleBtn.className = "example-card";
toggleBtn.textContent = currentVisible ? "Скрыть размер" : "Показать размер";
toggleBtn.addEventListener("click", () => {
if (!activeMeasurementId) return;
const nextVisible = !currentVisible;
onToggleVisibility?.(activeMeasurementId, nextVisible);
currentVisible = nextVisible;
toggleBtn.textContent = currentVisible ? "Скрыть размер" : "Показать размер";
});
list.appendChild(toggleBtn);
};
const show = ({ measurementId, visible = true } = {}) => {
activeMeasurementId = measurementId || null;
currentVisible = visible !== false;
render();
if (panel) {
panel.classList.remove("hidden");
panel.style.display = "grid";
}
if (backdrop) {
backdrop.classList.remove("hidden");
backdrop.style.display = "block";
}
};
const setVisibility = (visible) => {
currentVisible = !!visible;
render();
if (panel && !panel.classList.contains("hidden")) {
panel.style.display = "grid";
}
};
if (backdrop) {
backdrop.addEventListener("click", hide);
}
return {
show,
hide,
setVisibility,
};
}
+75
View File
@@ -0,0 +1,75 @@
export function createObjectModal({ panel, backdrop, onDelete, onClose }) {
let activeTarget = null;
const hide = () => {
if (panel) {
panel.classList.add("hidden");
panel.style.display = "";
}
if (backdrop) {
backdrop.classList.add("hidden");
backdrop.style.display = "";
}
activeTarget = null;
onClose?.();
};
const render = () => {
if (!panel) return;
panel.innerHTML = "";
const header = document.createElement("div");
header.className = "templates-header";
header.innerHTML = `<span class="templates-title">Объект</span>`;
const closeBtn = document.createElement("button");
closeBtn.className = "templates-close";
closeBtn.textContent = "×";
closeBtn.addEventListener("click", hide);
header.appendChild(closeBtn);
panel.appendChild(header);
const list = document.createElement("div");
list.className = "template-list";
panel.appendChild(list);
if (activeTarget?.label) {
const label = document.createElement("div");
label.className = "muted";
label.style.margin = "0 0 8px";
label.textContent = activeTarget.label;
list.appendChild(label);
}
const deleteBtn = document.createElement("button");
deleteBtn.className = "example-card danger-btn";
deleteBtn.textContent = "Удалить объект";
deleteBtn.addEventListener("click", () => {
if (!activeTarget?.modelId) return;
onDelete?.(activeTarget);
});
list.appendChild(deleteBtn);
};
const show = ({ modelId, label } = {}) => {
activeTarget = { modelId, label };
render();
if (panel) {
panel.classList.remove("hidden");
panel.style.display = "grid";
}
if (backdrop) {
backdrop.classList.remove("hidden");
backdrop.style.display = "block";
}
};
if (backdrop) {
backdrop.addEventListener("click", hide);
}
return {
show,
hide,
};
}
+401
View File
@@ -0,0 +1,401 @@
import { createSliderControl } from "./controls/slider.js";
const defaultSettings = {
themeVersion: "v3-yellow",
bgOuter: "#0f0f0f",
canvasTop: "#1c1c1c",
canvasBottom: "#1c1c1c",
loadingTop: "#1c1c1c",
loadingBottom: "#1c1c1c",
accent: "#ffea00",
accent2: "#ffea00",
templateFill: "#1c1c1c",
templateOutline: "#233044",
templateOutlineHover: "rgba(255,234,0,0.7)",
projectBtnFill: "#1c1c1c",
projectBtnBorder: "#233044",
projectBtnHover: "rgba(255,234,0,0.7)",
modalBtnPrimary: "#ffea00",
modalBtnSecondary: "#292929",
loaderColor: "#ffea00",
templateGlow: 80,
toolbarBg: "#1c1c1c",
toolbarBgActive: "#ffea00",
toolbarBorder: "#233044",
toolbarOutline: "#ffea00",
modalBg: "#1c1c1c",
modalBgAlpha: 1,
modalFocus: "#292929",
modalFocusAlpha: 1,
modalBorder: "#292929",
modalElement: "#292929",
modalElementOutline: "#2b2b2b",
modalRadius: "14px",
border: "#233044",
text: "#e6edf3",
panel: "#1c1c1c",
panel2: "#292929",
highlight: "#ffea00",
cubeText: "#242424",
cubeEdges: "#e0e7ff",
plus: "#ffea00",
navCubeBase: "#ffea00",
navCubeHover: "#ffffff",
navCubeText: "#242424",
navCubeEdge: "#e0e7ff",
navCubeEdgeAlpha: 0.6,
measureLine: "#ffea00",
measureLabel: "#ffea00",
measureLabelText: "#292929",
measureDot: "#ffea00",
};
const STORAGE_KEY = "bimdc-settings-v3";
const LEGACY_KEYS = ["bimdc-settings", "bimdc-settings-v2"];
const projectSettings = typeof window !== "undefined" && window.__BIMDC_THEME__
? { ...defaultSettings, ...window.__BIMDC_THEME__ }
: { ...defaultSettings };
const applySettings = (s) => {
const root = document.documentElement;
root.style.setProperty("--bg-outer", s.bgOuter);
root.style.setProperty("--canvas-top", s.canvasTop);
root.style.setProperty("--canvas-bottom", s.canvasBottom);
root.style.setProperty("--loading-top", s.loadingTop);
root.style.setProperty("--loading-bottom", s.loadingBottom);
root.style.setProperty("--accent", s.accent);
root.style.setProperty("--accent-2", s.accent2);
root.style.setProperty("--template-fill", s.templateFill);
root.style.setProperty("--template-outline", s.templateOutline);
root.style.setProperty("--template-outline-hover", s.templateOutlineHover);
root.style.setProperty("--project-btn-fill", s.projectBtnFill);
root.style.setProperty("--project-btn-border", s.projectBtnBorder);
root.style.setProperty("--project-btn-hover", s.projectBtnHover);
root.style.setProperty("--modal-btn-primary", s.modalBtnPrimary);
root.style.setProperty("--modal-btn-secondary", s.modalBtnSecondary);
root.style.setProperty("--loader-color", s.loaderColor);
root.style.setProperty("--template-glow-strength", `${s.templateGlow}%`);
root.style.setProperty("--toolbar-bg", s.toolbarBg);
root.style.setProperty("--toolbar-bg-active", s.toolbarBgActive);
root.style.setProperty("--toolbar-border", s.toolbarBorder);
root.style.setProperty("--toolbar-outline", s.toolbarOutline);
const bgAlphaPct = `${(s.modalBgAlpha ?? 1) * 100}%`;
const focusAlphaPct = `${(s.modalFocusAlpha ?? 1) * 100}%`;
root.style.setProperty("--modal-bg", s.modalBg);
root.style.setProperty("--modal-bg-alpha", bgAlphaPct);
root.style.setProperty("--modal-focus", s.modalFocus);
root.style.setProperty("--modal-focus-alpha", focusAlphaPct);
root.style.setProperty("--modal-border", s.modalBorder);
root.style.setProperty("--modal-element", s.modalElement);
root.style.setProperty("--modal-element-outline", s.modalElementOutline);
root.style.setProperty("--modal-radius", s.modalRadius);
root.style.setProperty("--border", s.border);
root.style.setProperty("--text", s.text);
root.style.setProperty("--panel", s.panel);
root.style.setProperty("--panel-2", s.panel2);
root.style.setProperty("--highlight-color", s.highlight);
root.style.setProperty("--cube-text", s.cubeText);
root.style.setProperty("--cube-edges", s.navCubeBase || s.cubeEdges);
root.style.setProperty("--cube-base", s.navCubeBase || s.cubeEdges);
root.style.setProperty("--cube-hover", s.navCubeHover);
root.style.setProperty("--cube-text", s.navCubeText || s.cubeText);
root.style.setProperty("--cube-edge-color", s.navCubeEdge);
root.style.setProperty("--cube-edge-alpha", s.navCubeEdgeAlpha);
root.style.setProperty("--plus-color", s.plus);
root.style.setProperty("--template-fill", s.templateFill);
root.style.setProperty("--template-outline", s.templateOutline);
root.style.setProperty("--measure-line", s.measureLine);
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("--highlight-color", s.highlight);
};
const saveSettings = (s) => {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(s));
} catch (e) {
// ignore
}
};
const loadSettings = () => {
try {
// purge legacy keys so старые палитры не мешали
LEGACY_KEYS.forEach((k) => localStorage.removeItem(k));
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) {
// зашиваем проектные настройки обратно в storage, чтобы при чистом кэше они сразу подхватывались
saveSettings(projectSettings);
return { ...projectSettings };
}
const stored = JSON.parse(raw);
if (stored.themeVersion && stored.themeVersion !== projectSettings.themeVersion) {
saveSettings(projectSettings);
return { ...projectSettings };
}
return { ...projectSettings, ...stored };
} catch (e) {
saveSettings(projectSettings);
return { ...projectSettings };
}
};
export function createSettingsMenu({ panel, controls = {}, onChange = null, onShow, onHide }) {
let state = loadSettings();
applySettings(state);
const getState = () => ({ ...state });
const applyStateExternal = (nextState = {}) => {
state = { ...state, ...nextState };
applySettings(state);
saveSettings(state);
if (typeof onChange === "function") {
onChange({ key: null, state });
}
if (panel && !panel.classList.contains("hidden")) {
render();
}
};
const hide = () => {
if (panel) {
panel.classList.add("hidden");
panel.style.display = "";
}
onHide?.();
};
const setVal = (key, val) => {
state = { ...state, [key]: val };
applySettings(state);
saveSettings(state);
if (typeof onChange === "function") {
onChange({ key, value: val, state });
}
};
const render = () => {
if (!panel) return;
panel.innerHTML = "";
const header = document.createElement("div");
header.className = "settings-header";
header.innerHTML = `<span class="settings-title">Настройки</span>`;
const closeBtn = document.createElement("button");
closeBtn.className = "settings-close";
closeBtn.textContent = "—";
closeBtn.addEventListener("click", hide);
header.appendChild(closeBtn);
panel.appendChild(header);
const section = (title) => {
const box = document.createElement("div");
box.className = "settings-section";
const t = document.createElement("div");
t.className = "section-title";
t.textContent = title;
box.appendChild(t);
return box;
};
const addColor = (parent, label, key) => {
const isHex = (val) => /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test((val || "").trim());
const row = document.createElement("div");
row.className = "color-row";
const lbl = document.createElement("label");
lbl.textContent = label;
const input = document.createElement("input");
input.type = "color";
input.value = isHex(state[key]) ? state[key] : "#ffffff";
const hex = document.createElement("input");
hex.type = "text";
hex.value = state[key];
input.addEventListener("change", (e) => {
hex.value = e.target.value;
setVal(key, e.target.value);
});
hex.addEventListener("change", (e) => {
input.value = e.target.value;
setVal(key, e.target.value);
});
row.appendChild(lbl);
row.appendChild(input);
row.appendChild(hex);
parent.appendChild(row);
};
const font = section("Фон");
addColor(font, "Фон внешний", "bgOuter");
addColor(font, "Фон холста верх", "canvasTop");
addColor(font, "Фон холста низ", "canvasBottom");
addColor(font, "Фон загрузки верх", "loadingTop");
addColor(font, "Фон загрузки низ", "loadingBottom");
panel.appendChild(font);
const buttons = section("Кнопки");
addColor(buttons, "Плюс", "plus");
addColor(buttons, "Кнопки (заливка)", "templateFill");
addColor(buttons, "Кнопки (бордер)", "templateOutline");
addColor(buttons, "Кнопки (outline hover)", "templateOutlineHover");
const brightnessSlider = createSliderControl({
label: "Яркость подсветки",
value: Number(state.templateGlow ?? 80),
min: 0,
max: 100,
step: 1,
onChange: (val) => setVal("templateGlow", val),
});
buttons.appendChild(brightnessSlider);
panel.appendChild(buttons);
const projectButtons = section("Кнопки проектов");
addColor(projectButtons, "Проекты (заливка)", "projectBtnFill");
addColor(projectButtons, "Проекты (бордер)", "projectBtnBorder");
addColor(projectButtons, "Проекты (hover)", "projectBtnHover");
panel.appendChild(projectButtons);
const modalButtons = section("Кнопки модалок");
addColor(modalButtons, "Модальная primary", "modalBtnPrimary");
addColor(modalButtons, "Модальная secondary", "modalBtnSecondary");
addColor(modalButtons, "Лоудер", "loaderColor");
panel.appendChild(modalButtons);
const toolbar = section("Toolbar");
addColor(toolbar, "Фон", "toolbarBg");
addColor(toolbar, "Актив", "toolbarBgActive");
addColor(toolbar, "Бордер", "toolbarBorder");
addColor(toolbar, "Outline", "toolbarOutline");
panel.appendChild(toolbar);
const navCube = section("NavCube");
addColor(navCube, "Грани", "navCubeBase");
addColor(navCube, "Hover", "navCubeHover");
addColor(navCube, "Текст", "navCubeText");
addColor(navCube, "Рёбра", "navCubeEdge");
const edgeAlphaRow = document.createElement("div");
edgeAlphaRow.className = "color-row";
const edgeAlphaLbl = document.createElement("label");
edgeAlphaLbl.textContent = "Прозрачность рёбер (0-1)";
const edgeAlphaInput = document.createElement("input");
edgeAlphaInput.type = "number";
edgeAlphaInput.step = "0.05";
edgeAlphaInput.min = "0";
edgeAlphaInput.max = "1";
edgeAlphaInput.value = state.navCubeEdgeAlpha;
edgeAlphaInput.addEventListener("change", () => {
const v = Math.min(1, Math.max(0, parseFloat(edgeAlphaInput.value) || 0));
edgeAlphaInput.value = v;
setVal("navCubeEdgeAlpha", v);
});
edgeAlphaRow.appendChild(edgeAlphaLbl);
edgeAlphaRow.appendChild(edgeAlphaInput);
edgeAlphaRow.appendChild(document.createElement("div"));
navCube.appendChild(edgeAlphaRow);
panel.appendChild(navCube);
const measure = section("Линейка");
addColor(measure, "Линия/бордер", "measureLine");
addColor(measure, "Плашка", "measureLabel");
addColor(measure, "Текст плашки", "measureLabelText");
addColor(measure, "Кружок", "measureDot");
panel.appendChild(measure);
const viewport = section("Viewport");
const addCheck = (label, checked, handler) => {
const row = document.createElement("div");
row.className = "checkbox-row";
const chk = document.createElement("input");
chk.type = "checkbox";
chk.checked = checked;
chk.addEventListener("change", () => handler(chk.checked));
const lbl = document.createElement("label");
lbl.textContent = label;
row.appendChild(chk);
row.appendChild(lbl);
viewport.appendChild(row);
};
if (controls.toggleEdges) addCheck("Рёбра", controls.toggleEdges.checked, (v) => { controls.toggleEdges.checked = v; controls.toggleEdges.dispatchEvent(new Event("change")); });
if (controls.toggleTransparent) addCheck("Прозрачный фон", controls.toggleTransparent.checked, (v) => { controls.toggleTransparent.checked = v; controls.toggleTransparent.dispatchEvent(new Event("change")); });
if (controls.addMode) addCheck("Добавлять в сцену", controls.addMode.checked, (v) => { controls.addMode.checked = v; });
panel.appendChild(viewport);
const modals = section("Модальные окна");
addColor(modals, "Основной фон", "modalBg");
addColor(modals, "Фокусный фон", "modalFocus");
const modalBgAlphaRow = createSliderControl({
label: "Прозрачность основного фона",
min: 0,
max: 1,
step: 0.05,
value: parseFloat(state.modalBgAlpha ?? 1),
onChange: (val) => {
const num = Math.max(0, Math.min(1, parseFloat(val)));
setVal("modalBgAlpha", num);
document.documentElement.style.setProperty("--modal-bg-alpha", `${num * 100}%`);
},
});
modals.appendChild(modalBgAlphaRow);
const modalFocusAlphaRow = createSliderControl({
label: "Прозрачность фокусного фона",
min: 0,
max: 1,
step: 0.05,
value: parseFloat(state.modalFocusAlpha ?? 1),
onChange: (val) => {
const num = Math.max(0, Math.min(1, parseFloat(val)));
setVal("modalFocusAlpha", num);
document.documentElement.style.setProperty("--modal-focus-alpha", `${num * 100}%`);
},
});
modals.appendChild(modalFocusAlphaRow);
addColor(modals, "Аутлайн", "modalBorder");
addColor(modals, "Фон элемента", "modalElement");
addColor(modals, "Аутлайн элемента", "modalElementOutline");
const radiusRow = document.createElement("div");
radiusRow.className = "color-row";
const radiusLbl = document.createElement("label");
radiusLbl.textContent = "Радиус";
const radiusInput = document.createElement("input");
radiusInput.type = "number";
radiusInput.value = parseInt(getComputedStyle(document.documentElement).getPropertyValue("--modal-radius")) || 14;
radiusInput.addEventListener("change", () => {
const val = `${radiusInput.value}px`;
document.documentElement.style.setProperty("--modal-radius", val);
setVal("modalRadius", val);
});
radiusRow.appendChild(radiusLbl);
radiusRow.appendChild(radiusInput);
radiusRow.appendChild(document.createElement("div"));
modals.appendChild(radiusRow);
panel.appendChild(modals);
const cube = section("Выделение объектов");
addColor(cube, "Подсветка объектов", "highlight");
panel.appendChild(cube);
};
const show = () => {
render();
if (panel) {
panel.classList.remove("hidden");
panel.style.display = "grid";
}
onShow?.();
};
const toggle = () => {
if (!panel) return;
if (panel.classList.contains("hidden")) {
show();
} else {
hide();
}
};
return { render, show, toggle, hide, getState, applyState: applyStateExternal };
}
+154
View File
@@ -0,0 +1,154 @@
export function createTemplatesMenu({ panel, fetchProjects, openProject, onShow, onHide }) {
let state = { templates: [], projects: [], loading: false, error: null };
const setState = (next) => {
state = { ...state, ...next };
render();
};
const renderEmpty = (parent, text) => {
const empty = document.createElement("div");
empty.className = "templates-empty";
empty.textContent = text;
parent.appendChild(empty);
};
const renderSection = (title, items, emptyText) => {
const section = document.createElement("div");
section.className = "templates-section";
const subtitle = document.createElement("div");
subtitle.className = "templates-subtitle";
subtitle.textContent = title;
section.appendChild(subtitle);
if (!items || !items.length) {
renderEmpty(section, emptyText);
} else {
items.forEach((item) => {
const card = document.createElement("button");
card.className = "example-card template-card";
card.type = "button";
card.addEventListener("click", () => openProject?.(item.id, item.type));
const titleEl = document.createElement("div");
titleEl.className = "card-title";
titleEl.textContent = item.name || item.id;
card.appendChild(titleEl);
const meta = document.createElement("div");
meta.className = "card-meta";
const typePill = document.createElement("span");
typePill.className = "card-pill";
typePill.dataset.kind = item.type || "project";
typePill.textContent = item.type === "template" ? "Шаблон" : "Проект";
meta.appendChild(typePill);
if (item.updatedAt) {
const updated = document.createElement("span");
updated.textContent = new Date(item.updatedAt).toLocaleDateString();
meta.appendChild(updated);
}
card.appendChild(meta);
section.appendChild(card);
});
}
return section;
};
const render = () => {
if (!panel) return;
panel.innerHTML = "";
const header = document.createElement("div");
header.className = "templates-header";
header.innerHTML = `<span class="templates-title">Проекты и шаблоны</span>`;
const actions = document.createElement("div");
actions.style.display = "flex";
actions.style.gap = "6px";
const refresh = document.createElement("button");
refresh.className = "templates-close";
refresh.title = "Обновить список";
refresh.textContent = "↻";
refresh.addEventListener("click", () => load());
const closeBtn = document.createElement("button");
closeBtn.className = "templates-close";
closeBtn.textContent = "×";
closeBtn.addEventListener("click", hide);
actions.appendChild(refresh);
actions.appendChild(closeBtn);
header.appendChild(actions);
panel.appendChild(header);
if (state.loading) {
const loadingEl = document.createElement("div");
loadingEl.className = "templates-empty";
loadingEl.textContent = "Загрузка списка проектов...";
panel.appendChild(loadingEl);
return;
}
if (state.error) {
const errorEl = document.createElement("div");
errorEl.className = "templates-empty";
errorEl.style.color = "var(--error)";
errorEl.textContent = state.error;
panel.appendChild(errorEl);
}
panel.appendChild(renderSection("Шаблоны", state.templates, "Нет шаблонов"));
panel.appendChild(renderSection("Мои проекты", state.projects, "Нет сохранённых проектов"));
};
const load = async () => {
if (!fetchProjects) return;
setState({ loading: true, error: null });
try {
const data = await fetchProjects();
setState({
loading: false,
templates: data?.templates || [],
projects: data?.projects || [],
error: null
});
} catch (err) {
setState({ loading: false, error: err?.message || "Не удалось загрузить проекты" });
}
};
const hide = () => {
if (panel) {
panel.classList.add("hidden");
panel.style.display = "";
}
onHide?.();
};
const show = () => {
render();
if (panel) {
panel.classList.remove("hidden");
panel.style.display = "grid";
}
load();
onShow?.();
};
const toggle = () => {
if (!panel) return;
if (panel.classList.contains("hidden")) {
show();
} else {
hide();
}
};
const setData = (data = {}) => {
setState({
templates: data.templates || [],
projects: data.projects || [],
loading: false,
error: data.error || null
});
};
return { render, show, hide, toggle, reload: load, setData };
}
+20
View File
@@ -0,0 +1,20 @@
const toolbarConfig = {
size: 25,
gap: 10,
radius: 7,
paddingY: 6,
iconPadding: 5,
iconSize: 13,
};
const applyToolbarVars = () => {
const root = document.documentElement;
root.style.setProperty("--tb-size", `${toolbarConfig.size}px`);
root.style.setProperty("--tb-gap", `${toolbarConfig.gap}px`);
root.style.setProperty("--tb-radius", `${toolbarConfig.radius}px`);
root.style.setProperty("--tb-pad-y", `${toolbarConfig.paddingY}px`);
root.style.setProperty("--tb-icon-pad", `${toolbarConfig.iconPadding}px`);
root.style.setProperty("--tb-icon-size", `${toolbarConfig.iconSize}px`);
};
export { toolbarConfig, applyToolbarVars };
+59
View File
@@ -0,0 +1,59 @@
// Базовые цвета/настройки проекта. При очистке localStorage эти значения остаются.
// Чтобы изменить палитру по умолчанию — правь этот объект и закоммить изменения.
const PROJECT_THEME = {
themeVersion: "v3-yellow",
bgOuter: "#0f0f0f",
canvasTop: "#1c1c1c",
canvasBottom: "#1c1c1c",
loadingTop: "#1c1c1c",
loadingBottom: "#1c1c1c",
accent: "#ffea00",
accent2: "#ffea00",
templateFill: "#1c1c1c",
templateOutline: "#233044",
templateOutlineHover: "rgba(255,234,0,0.7)",
templateGlow: 80,
toolbarBg: "#1c1c1c",
toolbarBgActive: "#ffea00",
toolbarBorder: "#233044",
toolbarOutline: "#ffea00",
modalBg: "#1c1c1c",
modalBgAlpha: 1,
modalFocus: "#292929",
modalFocusAlpha: 1,
modalBorder: "#292929",
modalElement: "#292929",
modalElementOutline: "#2b2b2b",
modalRadius: "14px",
projectBtnFill: "#1c1c1c",
projectBtnBorder: "#233044",
projectBtnHover: "rgba(255,234,0,0.7)",
modalBtnPrimary: "#ffea00",
modalBtnSecondary: "#292929",
border: "#233044",
text: "#e6edf3",
panel: "#1c1c1c",
panel2: "#292929",
highlight: "#ffea00",
cubeText: "#242424",
cubeEdges: "#e0e7ff",
plus: "#ffea00",
navCubeBase: "#ffea00",
navCubeHover: "#ffffff",
navCubeText: "#242424",
navCubeEdge: "#e0e7ff",
navCubeEdgeAlpha: 0.6,
measureLine: "#ffea00",
measureLabel: "#ffea00",
measureLabelText: "#292929",
measureDot: "#ffea00",
};
if (typeof window !== "undefined") {
window.__BIMDC_THEME__ = PROJECT_THEME;
}
// Support both module and browser usage without syntax errors in classic scripts
if (typeof module !== "undefined" && module.exports) {
module.exports = PROJECT_THEME;
}
+153
View File
@@ -0,0 +1,153 @@
// Минимальный плагин трансформаций: хранит выбранный объект, даёт доступ к pos/rot/scale,
// реагирует на клики по дереву. Трансформации применяются на уровне SceneModel.
export class TransformGizmo {
constructor({ viewer, sdk, treeView, transformStore, onTargetChange, onTransformChange, sceneModels }) {
this.viewer = viewer;
this.scene = viewer.scene;
this.sdk = sdk;
this.treeView = treeView;
this.transformStore = transformStore || {};
this.sceneModels = sceneModels || {};
this.onTargetChange = onTargetChange;
this.onTransformChange = onTransformChange;
this.mode = "translate"; // translate | rotate | scale
this.target = null; // { modelId, objectId }
this._bindTree();
}
setMode(next) {
if (!["translate", "rotate", "scale"].includes(next)) return;
this.mode = next;
this._log(`Режим гизмы: ${next}`);
}
clearTarget() {
if (this.target?.objectId) {
this.scene.setObjectsHighlighted([this.target.objectId], false);
}
this.scene.setObjectsHighlighted(this.scene.highlightedObjectIds || [], false);
this.target = null;
this.onTargetChange?.(null);
this._log("Выбор очищен");
}
setTarget({ modelId, objectId }) {
if (!modelId) return;
this.scene.setObjectsHighlighted(this.scene.highlightedObjectIds || [], false);
const model = this.sceneModels ? this.sceneModels[modelId] || this.scene.models[modelId] : this.scene.models[modelId];
if (!model) {
this._log(`Модель ${modelId} не найдена`, true);
return;
}
// Если указан объект
if (objectId) {
const entity = this.scene.objects[objectId];
if (!entity) {
this._log(`Объект ${objectId} не найден`, true);
} else {
const saved = this.transformStore?.[modelId]?.[objectId];
if (saved) {
if (saved.position) model.position = saved.position;
if (saved.rotation) model.rotation = saved.rotation;
if (saved.scale) model.scale = saved.scale;
}
this.onTargetChange?.({
modelId,
objectId,
transform: {
position: [...model.position],
rotation: [...model.rotation],
scale: model.scale ? [...model.scale] : [1, 1, 1],
},
});
this.scene.setObjectsHighlighted([objectId], true);
this.target = { modelId, objectId };
return;
}
}
// Если objectId нет, работаем всей моделью
const ids = Object.keys(model.objects || {});
if (ids.length) {
this.scene.setObjectsHighlighted(ids, true);
}
this.onTargetChange?.({
modelId,
objectId: null,
transform: {
position: [...model.position],
rotation: [...model.rotation],
scale: model.scale ? [...model.scale] : [1, 1, 1],
},
});
this.target = { modelId, objectId: null };
}
handleTreeNodeClick(evt) {
const node = evt?.treeViewNode || evt?.node || evt;
const objectId = node?.objectId || node?.id || node?.object?.id || node?.entityId || node?.objectIdGiven;
const modelId = node?.modelId || node?.model?.id || node?.modelID || node?.model?.modelId || node?.modelIdGiven;
if (objectId && modelId) {
this.setTarget({ modelId, objectId });
} else if (modelId) {
// нет objectId - подсветим всю модель
const model = this.scene.models[modelId];
if (model) {
const ids = Object.keys(model.objects || {});
this.scene.setObjectsHighlighted(this.scene.highlightedObjectIds || [], false);
this.scene.setObjectsHighlighted(ids, true);
this.target = { modelId, objectId: null };
}
}
}
updateTransform({ position, rotation, scale }) {
if (!this.target?.modelId) return;
const model = this.sceneModels ? this.sceneModels[this.target.modelId] || this.scene.models[this.target.modelId] : this.scene.models[this.target.modelId];
if (!model) return;
if (position) model.position = position;
if (rotation) model.rotation = rotation;
if (scale) model.scale = scale;
// сохраняем в store
if (!this.transformStore[this.target.modelId]) {
this.transformStore[this.target.modelId] = {};
}
const key = this.target.objectId || "__model__";
this.transformStore[this.target.modelId][key] = {
position: [...model.position],
rotation: [...model.rotation],
scale: model.scale ? [...model.scale] : [1, 1, 1],
};
this.onTransformChange?.({
modelId: this.target.modelId,
objectId: this.target.objectId,
transform: {
position: [...model.position],
rotation: [...model.rotation],
scale: model.scale ? [...model.scale] : [1, 1, 1],
},
});
}
_bindTree() {
if (this.treeView?.on) {
this.treeView.on("nodeTitleClicked", (e) => this.handleTreeNodeClick(e));
// подстрахуемся: некоторые деревья бросают другой эвент
this.treeView.on("nodeClicked", (e) => this.handleTreeNodeClick(e));
}
}
_log(msg, isError = false) {
if (isError) {
console.error("[TransformGizmo]", msg);
} else {
console.log("[TransformGizmo]", msg);
}
}
}