diff --git a/frontend/dcViewer.css b/frontend/dcViewer.css index f2a1bf4..95639ff 100644 --- a/frontend/dcViewer.css +++ b/frontend/dcViewer.css @@ -128,13 +128,15 @@ body { margin: 0; min-height: 100vh; font-family: "Manrope", "Segoe UI", system-ui, -apple-system, sans-serif; - background: radial-gradient(circle at 20% 20%, rgba(76, 224, 210, 0.08), transparent 28%), - radial-gradient(circle at 82% 22%, rgba(116, 240, 229, 0.08), transparent 25%), - var(--bg-outer); + background: var(--bg-outer); color: var(--text); overflow: hidden; } +body.app-starting { + background: linear-gradient(180deg, var(--loading-top), var(--loading-bottom)); +} + html, body, .viewer-wrapper, @@ -339,6 +341,25 @@ html.is-embedded-viewer #navCube { display: none; } +body.app-starting header, +body.app-starting #dropZone, +body.app-starting #status, +body.app-starting #bottomToolbar, +body.app-starting #sectionToolbar, +body.app-starting #navCube, +body.app-starting .side-panel, +body.app-starting .comments-panel, +body.app-starting .templates-panel, +body.app-starting .settings-panel { + opacity: 0 !important; + pointer-events: none !important; + visibility: hidden !important; +} + +body.app-starting #loader.hidden { + display: block; +} + .loader__figure, .loader { position: absolute; diff --git a/frontend/dcViewer.js b/frontend/dcViewer.js index a5fac30..9bed55f 100644 --- a/frontend/dcViewer.js +++ b/frontend/dcViewer.js @@ -29,6 +29,26 @@ const detectEmbeddedViewer = () => { const embeddedViewer = detectEmbeddedViewer(); document.documentElement.classList.toggle("is-embedded-viewer", embeddedViewer); +let appShellSetupComplete = false; +let appStartupContentReady = false; + +const revealAppShell = ({ force = false } = {}) => { + if (!force && (!appShellSetupComplete || !appStartupContentReady)) return; + appShellSetupComplete = true; + appStartupContentReady = true; + const preloadHidden = document.getElementById("preload-hidden"); + if (preloadHidden) { + preloadHidden.remove(); + } + document.body?.classList.remove("app-starting"); + document.documentElement.classList.add("app-ready"); +}; + +const markStartupContentReady = () => { + appStartupContentReady = true; + revealAppShell(); +}; + const dropZone = document.getElementById("dropZone"); const viewerWrapper = document.querySelector(".viewer-wrapper"); const headerEl = document.querySelector("header"); @@ -5967,12 +5987,6 @@ const initViewer = async () => { setupNodeDCSessionSync(); setupNodeDCSessionWatcher(); - const preloadHidden = document.getElementById("preload-hidden"); - if (preloadHidden) { - preloadHidden.remove(); - } - document.body.style.opacity = "1"; - const sdk = await import(SDK_URL); sdkRef = sdk; const { Viewer, NavCubePlugin, DistanceMeasurementsPlugin, DistanceMeasurementsMouseControl, SectionPlanesPlugin } = sdk; @@ -6417,7 +6431,7 @@ const initViewer = async () => { const loadModel = (options) => { const debugOptions = options || {}; try { - const { type, url, name = "", replace = true, id: forcedId, meta, edges, preserveMeta = false } = options; + const { type, url, name = "", replace = true, id: forcedId, meta, edges, preserveMeta = false, startupReveal = false } = options; const modelSettings = meta?.viewerSettings; const normalizedType = normalizeType(type, typeof url === "string" ? url : ""); const inferredType = normalizedType || normalizeType(guessTypeFromName(typeof url === "string" ? url : "")); @@ -6517,6 +6531,9 @@ const initViewer = async () => { } setStatus(`Загружено: ${name || inferredType}`); dropZone.classList.add("hidden"); + if (startupReveal) { + markStartupContentReady(); + } setLoading(false); if (inferredType === "las") { snapshotPointCloudColors(id); @@ -6553,6 +6570,9 @@ const initViewer = async () => { src: srcToLoad, }, "error"); showError(`Ошибка загрузки: ${err}`); + if (startupReveal) { + markStartupContentReady(); + } setLoading(false); updateNavCubeVisibility(); updatePanelsVisibility(); @@ -6583,6 +6603,9 @@ const initViewer = async () => { error: e?.message || String(e), }, "error"); showError(e.message || "Ошибка загрузки"); + if (debugOptions.startupReveal) { + markStartupContentReady(); + } setLoading(false); return null; } @@ -6630,9 +6653,10 @@ const initViewer = async () => { nameParam: sharedViewer?.name || params.get("name"), }); if (!src) { - reportViewerEvent("startup-no-src", { search: window.location.search }, "error"); + reportViewerEvent("startup-no-src", { search: window.location.search }); clearShareStartupPending(); setLoading(false); + markStartupContentReady(); return; } @@ -6643,6 +6667,7 @@ const initViewer = async () => { showError("Не удалось определить формат модели из URL"); clearShareStartupPending(); setLoading(false); + markStartupContentReady(); return; } @@ -6753,14 +6778,18 @@ const initViewer = async () => { settingsSrc, hasViewerSettings: !!sharedViewerSettings, }); - loadModel({ + const startupModel = loadModel({ type, url: src, name, replace: params.get("replace") !== "false", edges: params.has("edges") ? params.get("edges") !== "false" : undefined, meta: startupMeta, + startupReveal: true, }); + if (!startupModel) { + markStartupContentReady(); + } }; const startupModelLoadPromise = loadStartupModelFromQuery().catch((err) => { @@ -6770,6 +6799,7 @@ const initViewer = async () => { stack: err?.stack, }, "error"); showError(err.message || "Не удалось загрузить модель из URL"); + markStartupContentReady(); }); const loadProjectSnapshot = async (project) => { @@ -7744,6 +7774,8 @@ const loadProjectSnapshot = async (project) => { }); } + appShellSetupComplete = true; + revealAppShell(); void startupModelLoadPromise; }; @@ -7754,6 +7786,7 @@ const loadProjectSnapshot = async (project) => { stack: err?.stack, }, "error"); showError(err?.message || "Не удалось инициализировать viewer"); + revealAppShell({ force: true }); }); }); diff --git a/frontend/index.html b/frontend/index.html index b7b9859..7f9587b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -8,7 +8,31 @@ - + @@ -231,9 +255,9 @@ - + -
+