diff --git a/frontend/assets/favicon/apple-touch-icon.png b/frontend/assets/favicon/apple-touch-icon.png new file mode 100644 index 0000000..5078c01 Binary files /dev/null and b/frontend/assets/favicon/apple-touch-icon.png differ diff --git a/frontend/assets/favicon/favicon.ico b/frontend/assets/favicon/favicon.ico new file mode 100644 index 0000000..973f3b2 Binary files /dev/null and b/frontend/assets/favicon/favicon.ico differ diff --git a/frontend/assets/favicon/icon-192.png b/frontend/assets/favicon/icon-192.png new file mode 100644 index 0000000..d553a58 Binary files /dev/null and b/frontend/assets/favicon/icon-192.png differ diff --git a/frontend/assets/favicon/icon-adaptive.svg b/frontend/assets/favicon/icon-adaptive.svg new file mode 100644 index 0000000..fee636f --- /dev/null +++ b/frontend/assets/favicon/icon-adaptive.svg @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/frontend/assets/favicon/manifest.webmanifest.json b/frontend/assets/favicon/manifest.webmanifest.json new file mode 100644 index 0000000..f7256e8 --- /dev/null +++ b/frontend/assets/favicon/manifest.webmanifest.json @@ -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" + } + ] +} diff --git a/frontend/dcViewer.css b/frontend/dcViewer.css index c7c3012..b5f8709 100644 --- a/frontend/dcViewer.css +++ b/frontend/dcViewer.css @@ -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; } diff --git a/frontend/dcViewer.js b/frontend/dcViewer.js index 3a3c2e3..f009d5a 100644 --- a/frontend/dcViewer.js +++ b/frontend/dcViewer.js @@ -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; diff --git a/frontend/favicon.ico b/frontend/favicon.ico new file mode 100644 index 0000000..973f3b2 Binary files /dev/null and b/frontend/favicon.ico differ diff --git a/frontend/index.html b/frontend/index.html index d4d290d..de4e331 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,35 +4,40 @@ BIM DC Viewer - + + + + - + + diff --git a/frontend/src/ui/settingsMenu.js b/frontend/src/ui/settingsMenu.js index 6b8849a..9d0471b 100644 --- a/frontend/src/ui/settingsMenu.js +++ b/frontend/src/ui/settingsMenu.js @@ -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)); diff --git a/frontend/src/ui/toolbar.js b/frontend/src/ui/toolbar.js index 6abe865..b59a17b 100644 --- a/frontend/src/ui/toolbar.js +++ b/frontend/src/ui/toolbar.js @@ -1,7 +1,7 @@ const toolbarConfig = { minSize: 25, - maxSize: 56, - lensCount: 7, + maxSize: 87, + lensCount: 5, autoHide: false, size: 25, gap: 10, diff --git a/frontend/theme.js b/frontend/theme.js index b67a9df..10a8bd4 100644 --- a/frontend/theme.js +++ b/frontend/theme.js @@ -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,