Polish BIM shared viewer controls

This commit is contained in:
CODEX 2026-06-22 23:05:15 +03:00
parent fcfa59ec98
commit f680d6bad2
12 changed files with 367 additions and 77 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -0,0 +1,13 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<style>
.icon-fav-01 { fill: #333334; } /* Тёмный цвет для светлого таб-бара */
@media (prefers-color-scheme: dark) {
.icon-fav-01 { fill: #FEFEFE; } /* Светлый цвет для тёмного таб-бара */
}
@media (prefers-color-scheme: light) {
.icon-fav-01 { fill: #000000; } /* Тёмный цвет для светлого таб-бара (для явности) */
}
</style>
<path class="icon-fav-01" d="M17.8738 15.9159L16.0002 18.9718L14.1267 15.9159H17.8738ZM23.6307 12.7864H8.36914L15.9999 25.2308L23.6307 12.7864Z"/>
<path class="icon-fav-01" d="M10.9793 19.4872L6.67269 11.5918H25.5344L21.2281 19.4872H25.0581L31.2614 8H0.738281L7.16244 19.4872H10.9793Z"/>
</svg>

After

Width:  |  Height:  |  Size: 852 B

View File

@ -0,0 +1,29 @@
{
"theme_color": "#eeeff4",
"background_color": "#eeeff4",
"display": "browser",
"scope": "/",
"start_url": "/",
"icons": [
{
"src": "icon-adaptive.svg",
"type": "image/svg+xml",
"sizes": "any"
},
{
"src": "icon-192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "icon-512.png",
"type": "image/png",
"sizes": "512x512"
},
{
"src": "apple-touch-icon.png",
"type": "image/png",
"sizes": "180x180"
}
]
}

View File

@ -1183,6 +1183,74 @@ header {
min-width: 0;
}
.inspector-color-host {
min-width: 0;
}
.inspector-color-host .env-color-field {
display: flex;
width: 100%;
min-width: 0;
height: 40px;
align-items: center;
gap: 8px;
box-sizing: border-box;
padding: 5px 10px 5px 7px;
border-radius: 14px;
background: var(--nodedc-glass-control-bg);
box-shadow: var(--nodedc-glass-control-shadow);
backdrop-filter: blur(24px) saturate(128%) brightness(1.05);
-webkit-backdrop-filter: blur(24px) saturate(128%) brightness(1.05);
}
.inspector-color-host .env-color-field__swatch {
position: relative;
width: 26px;
height: 26px;
min-height: 26px;
flex: 0 0 26px;
overflow: hidden;
border-radius: 999px;
cursor: pointer;
}
.inspector-color-host .env-color-field__preview {
position: absolute;
inset: 0;
border-radius: inherit;
}
.inspector-color-host .env-color-field__swatch input[type="color"] {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
padding: 0;
opacity: 0;
cursor: pointer;
}
.inspector-color-host .env-color-field__value {
height: 32px;
min-height: 32px;
flex: 1 1 auto;
min-width: 0;
}
.inspector-color-host .env-color-field__value input {
width: 100%;
height: 100%;
box-sizing: border-box;
border: 0;
padding: 0 2px;
background: transparent;
color: rgba(255, 255, 255, 0.66);
font-size: 12px;
font-weight: 760;
letter-spacing: 0;
text-align: center;
}
.nodedc-select {
position: relative;
min-width: 0;
@ -3102,10 +3170,11 @@ header {
box-shadow: none;
}
body[data-viewer-mode="guest"] .bottom-toolbar,
body[data-viewer-mode="guest"] .section-toolbar,
body[data-viewer-mode="guest"] .section-overview,
body[data-viewer-mode="guest"] .inspector-restore-tray {
body[data-viewer-mode="guest"] .tb-btn:not([data-action="measure"]):not([data-action="section"]):not([data-action="projection"]),
body[data-viewer-mode="guest"] #settingsPanel,
body[data-viewer-mode="guest"] #templatesPanel,
body[data-viewer-mode="guest"] #projectNameSection,
body[data-viewer-mode="guest"] #saveProjectSection {
display: none !important;
}

View File

@ -2,13 +2,14 @@ const SDK_VERSION = "2.6.78"; // соответствует package.json
const SDK_URL = "./lib/dc-camera-context.es.js?v=3";
import { createLogo } from "./src/ui/logo.js";
import { applyToolbarVars } from "./src/ui/toolbar.js?v=2";
import { applyToolbarVars } from "./src/ui/toolbar.js?v=3";
import { createTemplatesMenu } from "./src/ui/templatesMenu.js?v=3";
import { createSettingsMenu } from "./src/ui/settingsMenu.js?v=12";
import { createSettingsMenu } from "./src/ui/settingsMenu.js?v=14";
import { createMeasurementModal } from "./src/ui/measurementModal.js";
import { createGlassSelect } from "./src/ui/glassSelect.js";
import { createColorControl } from "./src/ui/controls/color.js";
import { createSliderControl } from "./src/ui/controls/slider.js";
import "./theme.js?v=2";
import "./theme.js?v=3";
import { TransformGizmo } from "./transformGizmo.js";
import { createObjectModal } from "./src/ui/objectModal.js";
@ -73,6 +74,20 @@ const commentThreadList = document.getElementById("commentThreadList");
const commentReplyInput = document.getElementById("commentReplyInput");
const commentReplyAttachButton = document.getElementById("commentReplyAttachButton");
const commentReplyAttachmentInput = document.getElementById("commentReplyAttachmentInput");
const DEFAULT_LAUNCHER_URL = "https://hub.nodedc.ru/";
const getLauncherUrl = () => {
const configured = typeof window !== "undefined" ? window.__BIMDC_LAUNCHER_URL__ : "";
const raw = typeof configured === "string" && configured.trim()
? configured.trim()
: DEFAULT_LAUNCHER_URL;
try {
return new URL(raw, window.location.href).toString();
} catch {
return DEFAULT_LAUNCHER_URL;
}
};
const commentReplyAttachmentGrid = document.getElementById("commentReplyAttachmentGrid");
const commentModalError = document.getElementById("commentModalError");
const commentCancelButton = document.getElementById("commentCancelButton");
@ -82,6 +97,9 @@ const commentApplyInlineButton = document.getElementById("commentApplyInlineButt
const treeContainer = document.getElementById("treeContainer");
const displayModeControl = document.getElementById("displayModeControl");
const displayModeSelectHost = document.getElementById("displayModeSelect");
const customDisplayColorRow = document.getElementById("customDisplayColorRow");
const customDisplayColorControlHost = document.getElementById("customDisplayColorControl");
const customDisplaySaturationControl = document.getElementById("customDisplaySaturationControl");
const lightingModeControl = document.getElementById("lightingModeControl");
const lightingModeSelectHost = document.getElementById("lightingModeSelect");
const navigationAxisControl = document.getElementById("navigationAxisControl");
@ -214,11 +232,14 @@ const DISPLAY_MODE_OPTIONS = [
{ value: "source", label: "Исходный" },
{ value: "clay", label: "Clay" },
{ value: "white", label: "Белый" },
{ value: "contrast", label: "Контраст" },
{ value: "custom", label: "Кастом" },
{ value: "edges", label: "Рёбра" },
{ value: "xray", label: "Рентген" },
];
const DEFAULT_CUSTOM_DISPLAY_COLOR = "#ffffff";
const DEFAULT_CUSTOM_DISPLAY_SATURATION = 100;
const NAVIGATION_AXIS_OPTIONS = [
{ value: "auto", label: "Авто" },
{ value: "x-up", label: "X" },
@ -229,7 +250,6 @@ const NAVIGATION_AXIS_OPTIONS = [
const LIGHTING_MODE_OPTIONS = [
{ value: "standard", label: "Стандарт" },
{ value: "soft", label: "Мягкий" },
{ value: "ambient", label: "Без солнца" },
{ value: "technical", label: "Тех." },
];
@ -253,6 +273,7 @@ const NAVIGATION_AXIS_PRESETS = {
let templatesMenu = null;
let settingsMenu = null;
let pendingDesignState = null;
let measurementModal = null;
applyPanelOffsets();
@ -265,6 +286,7 @@ const activeModels = [];
let viewerMode = "standard";
let runtimeSessionInfo = { authRequired: false, authenticated: false, canShare: true, user: null };
let startupSharePayload = null;
const GUEST_TOOLBAR_ACTIONS = new Set(["measure", "section", "projection"]);
const memoryStore = new Map(); // url -> ArrayBuffer
const blobUrls = [];
let gizmo = null;
@ -273,11 +295,15 @@ const modelTypes = {}; // modelId -> normalized type
const sceneModels = {}; // modelId -> SceneModel
const pointCloudColorSnapshots = new Map(); // modelId -> [{ layer, colors, workingColors }]
let activeDisplayMode = "source";
let activeLightingMode = "ambient";
let activeLightingMode = "technical";
let activeNavigationAxis = "auto";
let activeZoomSpeed = 20;
let activeCustomDisplayColor = DEFAULT_CUSTOM_DISPLAY_COLOR;
let activeCustomDisplaySaturation = DEFAULT_CUSTOM_DISPLAY_SATURATION;
let inspectorCollapsed = false;
let displayModeSelect = null;
let customDisplayColorControl = null;
let customDisplaySaturationSlider = null;
let lightingModeSelect = null;
let navigationAxisSelect = null;
let zoomSpeedSlider = null;
@ -344,6 +370,43 @@ const normalizeZoomSpeed = (value) => {
return Math.min(100, Math.max(5, Math.round(numeric)));
};
const isHexColor = (value) => /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(String(value || "").trim());
const normalizeCustomDisplayColor = (value) => {
const raw = String(value || "").trim();
if (!isHexColor(raw)) return DEFAULT_CUSTOM_DISPLAY_COLOR;
if (raw.length === 4) {
const [, r, g, b] = raw;
return `#${r}${r}${g}${g}${b}${b}`.toLowerCase();
}
return raw.toLowerCase();
};
const normalizeCustomDisplaySaturation = (value) => {
const numeric = Number(value);
if (!Number.isFinite(numeric)) return DEFAULT_CUSTOM_DISPLAY_SATURATION;
return Math.min(200, Math.max(0, Math.round(numeric)));
};
const customDisplayHexToRgb01 = (value) => {
const hex = normalizeCustomDisplayColor(value).slice(1);
return [
Number.parseInt(hex.slice(0, 2), 16) / 255,
Number.parseInt(hex.slice(2, 4), 16) / 255,
Number.parseInt(hex.slice(4, 6), 16) / 255,
];
};
const applyRgbSaturation = (rgb, saturation) => {
const factor = normalizeCustomDisplaySaturation(saturation) / 100;
const gray = (rgb[0] * 0.2126) + (rgb[1] * 0.7152) + (rgb[2] * 0.0722);
return rgb.map((channel) => Math.min(1, Math.max(0, gray + ((channel - gray) * factor))));
};
const getCustomDisplayColorize = () => (
applyRgbSaturation(customDisplayHexToRgb01(activeCustomDisplayColor), activeCustomDisplaySaturation)
);
const configureCameraPanMap = (control) => {
const input = viewer?.scene?.input;
if (!control || !input) return;
@ -439,16 +502,36 @@ const normalizeDisplayMode = (mode) => (
);
const normalizeLightingMode = (mode) => (
LIGHTING_MODE_OPTIONS.some((option) => option.value === mode) ? mode : "ambient"
LIGHTING_MODE_OPTIONS.some((option) => option.value === mode) ? mode : "technical"
);
const normalizeNavigationAxis = (axis) => (
NAVIGATION_AXIS_OPTIONS.some((option) => option.value === axis) ? axis : "auto"
);
const setCustomDisplaySettings = ({ color, saturation } = {}) => {
if (color !== undefined) {
activeCustomDisplayColor = normalizeCustomDisplayColor(color);
customDisplayColorControl?.setValue?.(activeCustomDisplayColor);
}
if (saturation !== undefined) {
activeCustomDisplaySaturation = normalizeCustomDisplaySaturation(saturation);
customDisplaySaturationSlider?.setValue?.(activeCustomDisplaySaturation);
}
};
const setCustomDisplayControlsState = () => {
const isCustom = normalizeDisplayMode(activeDisplayMode) === "custom";
customDisplayColorRow?.classList.toggle("hidden", !isCustom);
customDisplaySaturationControl?.classList.toggle("hidden", !isCustom);
customDisplayColorControl?.setValue?.(activeCustomDisplayColor);
customDisplaySaturationSlider?.setValue?.(activeCustomDisplaySaturation);
};
const setDisplayModeControlState = () => {
activeDisplayMode = normalizeDisplayMode(activeDisplayMode);
displayModeSelect?.setValue(activeDisplayMode);
setCustomDisplayControlsState();
};
const setLightingModeControlState = () => {
@ -519,6 +602,7 @@ const getPrimaryModelEntry = () => (
const hasModelSettingsTarget = () => !!getModelSettingsSrc(getPrimaryModelEntry());
const isGuestMode = () => viewerMode === "guest";
const isGuestToolbarAction = (action) => GUEST_TOOLBAR_ACTIONS.has(action);
const getShareTokenFromPath = () => {
const match = window.location.pathname.match(/^\/share\/([^/?#]+)/);
@ -1609,15 +1693,34 @@ const bindFooterButtonAction = (button, handler) => {
};
const applyRuntimeMode = () => {
const guest = isGuestMode();
document.body.dataset.viewerMode = viewerMode;
if (bottomToolbar) {
bottomToolbar.classList.toggle("hidden", isGuestMode());
bottomToolbar.classList.remove("hidden");
bottomToolbar.querySelectorAll(".tb-btn").forEach((btn) => {
const action = btn.dataset.action || "";
const guestHidden = guest && !isGuestToolbarAction(action);
btn.classList.toggle("hidden", guestHidden);
if (guestHidden) {
btn.disabled = true;
} else if (!guest && action !== "section" && action !== "comments" && action !== "share") {
btn.disabled = false;
}
});
}
if (settingsButton) settingsButton.disabled = guest;
if (templatesButton) templatesButton.disabled = guest;
if (measureButton && guest) measureButton.disabled = !measurePlugin || !measureControl || activeModels.length === 0;
if (projectionToggle && guest) projectionToggle.disabled = activeModels.length === 0;
if (commentsButton) {
commentsButton.classList.toggle("hidden", isGuestMode());
commentsButton.disabled = isGuestMode() || activeModels.length === 0;
commentsButton.classList.toggle("hidden", guest);
commentsButton.disabled = guest || activeModels.length === 0;
}
if (isGuestMode()) {
if (guest) {
closeSettingsPanel();
closeTemplatesPanel();
settingsPanel?.classList.add("hidden");
templatesPanel?.classList.add("hidden");
commentsModeActive = false;
commentsCollapsed = false;
commentContextTarget = null;
@ -1630,15 +1733,12 @@ const applyRuntimeMode = () => {
stopCommentTargetLoop();
}
if (shareButton) {
const canShowShare = !isGuestMode() && runtimeSessionInfo.canShare && activeModels.length > 0;
const canShowShare = !guest && runtimeSessionInfo.canShare && activeModels.length > 0;
shareButton.classList.toggle("hidden", !canShowShare);
shareButton.disabled = !canShowShare;
}
if (dropZone) {
dropZone.style.pointerEvents = isGuestMode() ? "none" : "";
}
if (sectionToolbar && isGuestMode()) {
sectionToolbar.classList.add("hidden");
dropZone.style.pointerEvents = guest ? "none" : "";
}
};
@ -1646,6 +1746,7 @@ const initializeRuntimeMode = async () => {
const shareToken = getShareTokenFromPath();
if (shareToken) {
viewerMode = "guest";
inspectorCollapsed = true;
try {
const response = await fetch(`${SHARES_API}/${encodeURIComponent(shareToken)}`, {
credentials: "include",
@ -1755,11 +1856,11 @@ const copyShareLink = async () => {
const getDisplayModePreset = (mode) => {
switch (mode) {
case "clay":
return { colorize: [0.86, 0.86, 0.82], opacity: 1, edges: false, xrayed: false };
return { colorize: [0.76, 0.35, 0.25], opacity: 1, edges: false, xrayed: false };
case "white":
return { colorize: [1, 1, 1], opacity: 1, edges: false, xrayed: false };
case "contrast":
return { colorize: [0.78, 0.82, 1], opacity: 1, edges: false, xrayed: false };
case "custom":
return { colorize: getCustomDisplayColorize(), opacity: 1, edges: false, xrayed: false };
case "edges":
return { colorize: null, opacity: 1, edges: true, xrayed: false };
case "xray":
@ -1960,6 +2061,7 @@ const applyLightingMode = ({ announce = false } = {}) => {
});
break;
case "technical":
default:
createSceneLight("ambient", { color: [1.0, 1.0, 1.0], intensity: 0.62 });
createSceneLight("dir", {
dir: [0.9, -0.45, -0.35],
@ -1981,10 +2083,6 @@ const applyLightingMode = ({ announce = false } = {}) => {
scene.sao.numSamples = 20;
}
break;
case "ambient":
default:
createSceneLight("ambient", { color: [1.0, 1.0, 1.0], intensity: 0.92 });
break;
}
scene.glRedraw?.();
@ -2706,7 +2804,7 @@ const positionSectionToolbar = () => {
};
const setSectionModeActive = (active) => {
sectionModeActive = !!active && !!sectionPlanesPlugin && getSectionPlaneIds().length > 0 && !isGuestMode();
sectionModeActive = !!active && !!sectionPlanesPlugin && getSectionPlaneIds().length > 0;
if (sectionModeActive) {
positionSectionToolbar();
}
@ -2721,7 +2819,7 @@ const setSectionModeActive = (active) => {
};
const updateSectionControls = () => {
const available = !!sectionPlanesPlugin && !isGuestMode() && activeModels.length > 0;
const available = !!sectionPlanesPlugin && activeModels.length > 0;
if (sectionButton) {
sectionButton.disabled = !available;
sectionButton.classList.toggle("disabled", !available);
@ -3386,6 +3484,7 @@ const parseInputNumber = (val) => {
};
const persistTransform = (modelId, objectId, transform) => {
if (isGuestMode()) return;
if (!modelId || !transform) return;
if (!transformStore[modelId]) transformStore[modelId] = {};
const key = objectId || "__model__";
@ -4299,6 +4398,8 @@ const buildProjectSnapshot = async (name) => {
edges: !!toggleEdges?.checked,
addMode: !!addMode?.checked,
displayMode: activeDisplayMode,
customDisplayColor: activeCustomDisplayColor,
customDisplaySaturation: activeCustomDisplaySaturation,
lightingMode: activeLightingMode,
navigationAxis: activeNavigationAxis,
zoomSpeed: activeZoomSpeed,
@ -4329,7 +4430,11 @@ const buildProjectSnapshot = async (name) => {
};
const applyDesignState = (design) => {
if (!design || !settingsMenu?.applyState) return;
if (!design || typeof design !== "object") return;
if (!settingsMenu?.applyState) {
pendingDesignState = { ...design };
return;
}
settingsMenu.applyState(design);
applyToolbarVars(settingsMenu?.getState?.());
updateToolbarDockMode();
@ -4364,6 +4469,8 @@ const buildModelViewerSettings = () => ({
transparent: !!toggleTransparent?.checked,
edges: !!toggleEdges?.checked,
displayMode: activeDisplayMode,
customDisplayColor: activeCustomDisplayColor,
customDisplaySaturation: activeCustomDisplaySaturation,
lightingMode: activeLightingMode,
navigationAxis: activeNavigationAxis,
zoomSpeed: activeZoomSpeed
@ -4394,9 +4501,18 @@ const applyModelViewerSettings = (settings, { applyCamera = true, applyDesign =
toggleEdges.checked = viewerState.edges;
suppressEdgeReload = false;
}
setCustomDisplaySettings({
color: viewerState.customDisplayColor,
saturation: viewerState.customDisplaySaturation,
});
if (typeof viewerState.displayMode === "string") {
activeDisplayMode = viewerState.displayMode;
applyDisplayMode();
} else if (viewerState.customDisplayColor !== undefined || viewerState.customDisplaySaturation !== undefined) {
setCustomDisplayControlsState();
if (normalizeDisplayMode(activeDisplayMode) === "custom") {
applyDisplayMode();
}
}
if (typeof viewerState.lightingMode === "string") {
activeLightingMode = viewerState.lightingMode;
@ -4491,9 +4607,18 @@ const applyViewerState = (state) => {
if (typeof state.addMode === "boolean" && addMode) {
addMode.checked = state.addMode;
}
setCustomDisplaySettings({
color: state.customDisplayColor,
saturation: state.customDisplaySaturation,
});
if (typeof state.displayMode === "string") {
activeDisplayMode = state.displayMode;
applyDisplayMode();
} else if (state.customDisplayColor !== undefined || state.customDisplaySaturation !== undefined) {
setCustomDisplayControlsState();
if (normalizeDisplayMode(activeDisplayMode) === "custom") {
applyDisplayMode();
}
}
if (typeof state.lightingMode === "string") {
activeLightingMode = state.lightingMode;
@ -4558,6 +4683,8 @@ const initViewer = async () => {
if (headerEl && homeButton && !homeButton.querySelector(".ue-logo")) {
const logoEl = createLogo();
homeButton.appendChild(logoEl);
homeButton.title = "Открыть NODE.DC Hub";
homeButton.setAttribute("aria-label", "Открыть NODE.DC Hub");
if (logoEl.style.width) homeButton.style.width = logoEl.style.width;
if (logoEl.style.height) homeButton.style.height = logoEl.style.height;
homeButton.style.border = "none";
@ -5192,8 +5319,15 @@ const initViewer = async () => {
activeDisplayMode = params.get("displayMode") || "source";
setDisplayModeControlState();
}
setCustomDisplaySettings({
color: params.has("customDisplayColor") ? params.get("customDisplayColor") : undefined,
saturation: params.has("customDisplaySaturation") ? params.get("customDisplaySaturation") : undefined,
});
if ((params.has("customDisplayColor") || params.has("customDisplaySaturation")) && normalizeDisplayMode(activeDisplayMode) === "custom") {
applyDisplayMode();
}
if (params.has("lightingMode") || params.has("light")) {
activeLightingMode = params.get("lightingMode") || params.get("light") || "ambient";
activeLightingMode = params.get("lightingMode") || params.get("light") || "technical";
applyLightingMode();
}
if (params.has("navigationAxis") || params.has("axis")) {
@ -5582,6 +5716,11 @@ const loadProjectSnapshot = async (project) => {
setToolbarButtonActive(settingsButton, false);
}
});
if (pendingDesignState) {
const design = pendingDesignState;
pendingDesignState = null;
applyDesignState(design);
}
// ensure persisted settings are applied to cube/highlight on first load
applyToolbarVars(settingsMenu?.getState?.());
updateToolbarDockMode();
@ -5603,6 +5742,40 @@ const loadProjectSnapshot = async (project) => {
});
setDisplayModeControlState();
}
if (customDisplayColorControlHost) {
customDisplayColorControl = createColorControl({
value: activeCustomDisplayColor,
ariaLabel: "Цвет кастомного режима",
valueLabel: "HEX цвет кастомного режима",
onChange: (value) => {
setCustomDisplaySettings({ color: value });
if (normalizeDisplayMode(activeDisplayMode) === "custom") {
applyDisplayMode();
}
},
});
customDisplayColorControlHost.replaceChildren(customDisplayColorControl);
}
if (customDisplaySaturationControl) {
customDisplaySaturationSlider = createSliderControl({
label: "Насыщенность",
value: activeCustomDisplaySaturation,
min: 0,
max: 200,
step: 1,
className: "inspector-slider",
showValue: true,
formatValue: (value) => `${Math.round(value)}%`,
onChange: (value) => {
setCustomDisplaySettings({ saturation: value });
if (normalizeDisplayMode(activeDisplayMode) === "custom") {
applyDisplayMode();
}
},
});
customDisplaySaturationControl.replaceChildren(customDisplaySaturationSlider);
}
setCustomDisplayControlsState();
if (lightingModeControl && lightingModeSelectHost) {
lightingModeSelect = createGlassSelect({
host: lightingModeSelectHost,
@ -5846,23 +6019,7 @@ const loadProjectSnapshot = async (project) => {
});
homeButton.addEventListener("click", () => {
destroyAllModels();
dropZone.classList.remove("hidden");
setStatus("Готов к загрузке");
modelCounter = 0;
setLoading(false);
memoryStore.clear();
currentSelection = null;
updateTransformInputs(null);
templatesMenu?.hide();
setMeasureActive(false);
clearMeasurements();
closeSaveModal();
closeDeleteModal();
// Примеры доступны, но сцена чистая
gizmo?.clearTarget();
updateNavCubeVisibility();
updatePanelsVisibility();
window.location.assign(getLauncherUrl());
});
if (saveProjectButton) {
@ -6190,8 +6347,9 @@ const loadProjectSnapshot = async (project) => {
bottomToolbar.addEventListener("click", (e) => {
const btn = e.target.closest(".tb-btn");
if (!btn) return;
if (isGuestMode()) return;
if (btn.disabled) return;
const action = btn.dataset.action;
if (isGuestMode() && !isGuestToolbarAction(action)) return;
if (action === "settings") {
if (btn.classList.contains("active")) {
closeSettingsPanel();
@ -6222,7 +6380,6 @@ const loadProjectSnapshot = async (project) => {
}
sectionToolbar?.addEventListener("click", (e) => {
if (isGuestMode()) return;
const btn = e.target.closest("[data-section-action]");
if (!btn) return;
const action = btn.dataset.sectionAction;

BIN
frontend/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -4,23 +4,27 @@
<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">
<link rel="icon" type="image/svg+xml" href="/assets/favicon/icon-adaptive.svg">
<link rel="alternate icon" href="/assets/favicon/favicon.ico">
<link rel="apple-touch-icon" href="/assets/favicon/apple-touch-icon.png">
<link rel="manifest" href="/assets/favicon/manifest.webmanifest.json">
<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?v=2"></script>
<script src="/theme.js?v=3"></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);
const sharedViewer = /^\/share\/[^/]+\/?$/.test(window.location.pathname || "");
let stored = null;
if (!sharedViewer) {
LEGACY_KEYS.forEach((k) => localStorage.removeItem(k));
const raw = localStorage.getItem(STORAGE_KEY);
try {
stored = raw ? JSON.parse(raw) : null;
} catch (e) {
@ -32,7 +36,8 @@
localStorage.setItem(STORAGE_KEY, JSON.stringify(project));
stored = { ...project };
}
const s = { ...(project || {}), ...(stored || {}) };
}
const s = { ...(project || {}), ...(!sharedViewer && stored ? stored : {}) };
if (!Object.keys(s).length) return;
const root = document.documentElement;
const set = (k, v) => v !== undefined && root.style.setProperty(k, v);
@ -203,11 +208,11 @@
<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=36">
<link rel="stylesheet" href="/dcViewer.css?v=37">
</head>
<body>
<header>
<button class="logo-btn" id="homeButton" title="Вернуться к старту"></button>
<button class="logo-btn" id="homeButton" type="button" title="Открыть NODE.DC Hub"></button>
<div class="toolbar"></div>
<div class="hidden-controls" style="display:none">
<input type="checkbox" id="toggleEdges" checked>
@ -263,10 +268,15 @@
<div class="inspector-control-label">Режим отображения</div>
<div class="inspector-select-host" id="displayModeSelect"></div>
</div>
<div class="inspector-control-row inspector-control-row--half hidden" id="customDisplayColorRow">
<div class="inspector-control-label">Цвет кастома</div>
<div class="inspector-color-host" id="customDisplayColorControl"></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 inspector-control-row--slider hidden" id="customDisplaySaturationControl"></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>
@ -641,6 +651,6 @@
<input id="fileInput" class="hidden" type="file"
accept=".xkt,.glb,.gltf,.bim,.las,.laz,.obj,.stl">
<script type="module" src="/dcViewer.js?v=50"></script>
<script type="module" src="/dcViewer.js?v=55"></script>
</body>
</html>

View File

@ -2,7 +2,7 @@ import { createSliderControl } from "./controls/slider.js";
import { createColorControl } from "./controls/color.js";
const defaultSettings = {
themeVersion: "v6-comments-targets",
themeVersion: "v7-toolbar-hub-icon",
bgOuter: "#0f0f0f",
canvasTop: "#1c1c1c",
canvasBottom: "#1c1c1c",
@ -25,8 +25,8 @@ const defaultSettings = {
toolbarBorder: "#292929",
toolbarOutline: "#292929",
toolbarMinSize: 25,
toolbarMaxSize: 56,
toolbarLensCount: 7,
toolbarMaxSize: 87,
toolbarLensCount: 5,
toolbarAutoHide: false,
modalBg: "#1c1c1c",
modalBgAlpha: 1,
@ -72,6 +72,14 @@ const defaultSettings = {
const STORAGE_KEY = "bimdc-settings-v3";
const LEGACY_KEYS = ["bimdc-settings", "bimdc-settings-v2"];
const isSharedViewerPath = () => (
typeof window !== "undefined" &&
/^\/share\/[^/]+\/?$/.test(window.location.pathname || "")
);
const isReadonlySettingsContext = () => (
isSharedViewerPath() ||
(typeof document !== "undefined" && document.body?.dataset?.viewerMode === "guest")
);
const projectSettings = typeof window !== "undefined" && window.__BIMDC_THEME__
? { ...defaultSettings, ...window.__BIMDC_THEME__ }
: { ...defaultSettings };
@ -278,6 +286,7 @@ const applySettings = (s) => {
};
const saveSettings = (s) => {
if (isReadonlySettingsContext()) return;
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(s));
} catch (e) {
@ -286,6 +295,9 @@ const saveSettings = (s) => {
};
const loadSettings = () => {
if (isReadonlySettingsContext()) {
return { ...projectSettings };
}
try {
// purge legacy keys so старые палитры не мешали
LEGACY_KEYS.forEach((k) => localStorage.removeItem(k));

View File

@ -1,7 +1,7 @@
const toolbarConfig = {
minSize: 25,
maxSize: 56,
lensCount: 7,
maxSize: 87,
lensCount: 5,
autoHide: false,
size: 25,
gap: 10,

View File

@ -1,7 +1,7 @@
// Базовые цвета/настройки проекта. При очистке localStorage эти значения остаются.
// Чтобы изменить палитру по умолчанию — правь этот объект и закоммить изменения.
const PROJECT_THEME = {
themeVersion: "v6-comments-targets",
themeVersion: "v7-toolbar-hub-icon",
bgOuter: "#0f0f0f",
canvasTop: "#1c1c1c",
canvasBottom: "#1c1c1c",
@ -18,8 +18,8 @@ const PROJECT_THEME = {
toolbarBorder: "#292929",
toolbarOutline: "#292929",
toolbarMinSize: 25,
toolbarMaxSize: 56,
toolbarLensCount: 7,
toolbarMaxSize: 87,
toolbarLensCount: 5,
toolbarAutoHide: false,
modalBg: "#1c1c1c",
modalBgAlpha: 1,