Compare commits
No commits in common. "1a78dd0dc814586713eb2a8a9c2bf0e5da7927c7" and "abf899fefd0f9f8f23561b9083cb3f8843ecb528" have entirely different histories.
1a78dd0dc8
...
abf899fefd
|
|
@ -128,15 +128,13 @@ body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
font-family: "Manrope", "Segoe UI", system-ui, -apple-system, sans-serif;
|
font-family: "Manrope", "Segoe UI", system-ui, -apple-system, sans-serif;
|
||||||
background: var(--bg-outer);
|
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);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.app-starting {
|
|
||||||
background: linear-gradient(180deg, var(--loading-top), var(--loading-bottom));
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body,
|
body,
|
||||||
.viewer-wrapper,
|
.viewer-wrapper,
|
||||||
|
|
@ -341,25 +339,6 @@ html.is-embedded-viewer #navCube {
|
||||||
display: none;
|
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__figure,
|
||||||
.loader {
|
.loader {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
||||||
|
|
@ -29,26 +29,6 @@ const detectEmbeddedViewer = () => {
|
||||||
const embeddedViewer = detectEmbeddedViewer();
|
const embeddedViewer = detectEmbeddedViewer();
|
||||||
document.documentElement.classList.toggle("is-embedded-viewer", embeddedViewer);
|
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 dropZone = document.getElementById("dropZone");
|
||||||
const viewerWrapper = document.querySelector(".viewer-wrapper");
|
const viewerWrapper = document.querySelector(".viewer-wrapper");
|
||||||
const headerEl = document.querySelector("header");
|
const headerEl = document.querySelector("header");
|
||||||
|
|
@ -2321,7 +2301,6 @@ const getShareableModelPayload = () => {
|
||||||
src,
|
src,
|
||||||
type: normalizeType(null, src) || entry.meta?.artifactType || entry.type,
|
type: normalizeType(null, src) || entry.meta?.artifactType || entry.type,
|
||||||
versionId: entry.meta?.versionId || entry.meta?.conversion?.versionId || null,
|
versionId: entry.meta?.versionId || entry.meta?.conversion?.versionId || null,
|
||||||
viewerSettings: buildModelViewerSettings(),
|
|
||||||
versions: Array.isArray(entry.meta?.versions) ? entry.meta.versions : [],
|
versions: Array.isArray(entry.meta?.versions) ? entry.meta.versions : [],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -4098,37 +4077,6 @@ const applyStoredTransformsToModel = (modelId) => {
|
||||||
return applied;
|
return applied;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getViewerSettingsTransformMap = (settings) => {
|
|
||||||
const transforms = settings?.viewerState?.transforms;
|
|
||||||
return transforms && typeof transforms === "object" && !Array.isArray(transforms)
|
|
||||||
? transforms
|
|
||||||
: null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ensureLoadedModelTransformAlias = (modelId, settings = null) => {
|
|
||||||
if (!modelId) return false;
|
|
||||||
const currentTransforms = transformStore[modelId];
|
|
||||||
if (currentTransforms && Object.keys(currentTransforms).length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const transforms = getViewerSettingsTransformMap(settings) || transformStore;
|
|
||||||
const preferredIds = Array.isArray(settings?.viewerState?.models)
|
|
||||||
? settings.viewerState.models.map((item) => item?.id).filter(Boolean)
|
|
||||||
: [];
|
|
||||||
const candidateIds = [...preferredIds, ...Object.keys(transforms || {})]
|
|
||||||
.filter((id, index, items) => id && id !== modelId && items.indexOf(id) === index);
|
|
||||||
|
|
||||||
const sourceId = candidateIds.find((id) => {
|
|
||||||
const value = transformStore[id] || transforms?.[id];
|
|
||||||
return value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length;
|
|
||||||
});
|
|
||||||
if (!sourceId) return false;
|
|
||||||
|
|
||||||
transformStore[modelId] = safeClone(transformStore[sourceId] || transforms[sourceId]);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getModelIdFromNode = (node) => {
|
const getModelIdFromNode = (node) => {
|
||||||
const title = (node?.title || node?.text || node?.name || "").trim();
|
const title = (node?.title || node?.text || node?.name || "").trim();
|
||||||
const firstObject = (node && collectObjectIds(node)[0]) || null;
|
const firstObject = (node && collectObjectIds(node)[0]) || null;
|
||||||
|
|
@ -5584,7 +5532,9 @@ const snapshotVec = (v) => {
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildActiveTransformsSnapshot = () => {
|
const buildProjectSnapshot = async (name) => {
|
||||||
|
const models = [];
|
||||||
|
|
||||||
const transformsCopy = safeClone(transformStore);
|
const transformsCopy = safeClone(transformStore);
|
||||||
activeModels.forEach(({ model }) => {
|
activeModels.forEach(({ model }) => {
|
||||||
if (!model || !model.id) return;
|
if (!model || !model.id) return;
|
||||||
|
|
@ -5597,18 +5547,6 @@ const buildActiveTransformsSnapshot = () => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return transformsCopy;
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildActiveModelStateSnapshot = () => activeModels.map((entry) => ({
|
|
||||||
id: entry.model?.id,
|
|
||||||
visible: entry.model?.visible !== false
|
|
||||||
}));
|
|
||||||
|
|
||||||
const buildProjectSnapshot = async (name) => {
|
|
||||||
const models = [];
|
|
||||||
|
|
||||||
const transformsCopy = buildActiveTransformsSnapshot();
|
|
||||||
|
|
||||||
for (const entry of activeModels) {
|
for (const entry of activeModels) {
|
||||||
const model = entry.model;
|
const model = entry.model;
|
||||||
|
|
@ -5699,7 +5637,10 @@ const buildProjectSnapshot = async (name) => {
|
||||||
ids: lastSelection.ids || [],
|
ids: lastSelection.ids || [],
|
||||||
modelId: lastSelection.modelId || null,
|
modelId: lastSelection.modelId || null,
|
||||||
},
|
},
|
||||||
models: buildActiveModelStateSnapshot(),
|
models: activeModels.map((entry) => ({
|
||||||
|
id: entry.model?.id,
|
||||||
|
visible: entry.model?.visible !== false
|
||||||
|
})),
|
||||||
transforms: transformsCopy
|
transforms: transformsCopy
|
||||||
} : {};
|
} : {};
|
||||||
|
|
||||||
|
|
@ -5763,8 +5704,6 @@ const buildModelViewerSettings = () => ({
|
||||||
lightingMode: activeLightingMode,
|
lightingMode: activeLightingMode,
|
||||||
navigationAxis: activeNavigationAxis,
|
navigationAxis: activeNavigationAxis,
|
||||||
zoomSpeed: activeZoomSpeed,
|
zoomSpeed: activeZoomSpeed,
|
||||||
models: buildActiveModelStateSnapshot(),
|
|
||||||
transforms: buildActiveTransformsSnapshot(),
|
|
||||||
lod: (() => {
|
lod: (() => {
|
||||||
const entry = getPrimaryModelEntry();
|
const entry = getPrimaryModelEntry();
|
||||||
const selected = getSelectedLodRecord(entry);
|
const selected = getSelectedLodRecord(entry);
|
||||||
|
|
@ -5946,10 +5885,7 @@ const applyViewerState = (state) => {
|
||||||
if (state.transforms && typeof state.transforms === "object") {
|
if (state.transforms && typeof state.transforms === "object") {
|
||||||
Object.keys(transformStore).forEach((k) => delete transformStore[k]);
|
Object.keys(transformStore).forEach((k) => delete transformStore[k]);
|
||||||
Object.assign(transformStore, safeClone(state.transforms));
|
Object.assign(transformStore, safeClone(state.transforms));
|
||||||
Object.keys(sceneModels).forEach((mid) => {
|
Object.keys(sceneModels).forEach((mid) => applyStoredTransformsToModel(mid));
|
||||||
ensureLoadedModelTransformAlias(mid, { viewerState: state });
|
|
||||||
applyStoredTransformsToModel(mid);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (Array.isArray(state.models)) {
|
if (Array.isArray(state.models)) {
|
||||||
state.models.forEach((m) => {
|
state.models.forEach((m) => {
|
||||||
|
|
@ -5987,6 +5923,12 @@ const initViewer = async () => {
|
||||||
setupNodeDCSessionSync();
|
setupNodeDCSessionSync();
|
||||||
setupNodeDCSessionWatcher();
|
setupNodeDCSessionWatcher();
|
||||||
|
|
||||||
|
const preloadHidden = document.getElementById("preload-hidden");
|
||||||
|
if (preloadHidden) {
|
||||||
|
preloadHidden.remove();
|
||||||
|
}
|
||||||
|
document.body.style.opacity = "1";
|
||||||
|
|
||||||
const sdk = await import(SDK_URL);
|
const sdk = await import(SDK_URL);
|
||||||
sdkRef = sdk;
|
sdkRef = sdk;
|
||||||
const { Viewer, NavCubePlugin, DistanceMeasurementsPlugin, DistanceMeasurementsMouseControl, SectionPlanesPlugin } = sdk;
|
const { Viewer, NavCubePlugin, DistanceMeasurementsPlugin, DistanceMeasurementsMouseControl, SectionPlanesPlugin } = sdk;
|
||||||
|
|
@ -6431,7 +6373,7 @@ const initViewer = async () => {
|
||||||
const loadModel = (options) => {
|
const loadModel = (options) => {
|
||||||
const debugOptions = options || {};
|
const debugOptions = options || {};
|
||||||
try {
|
try {
|
||||||
const { type, url, name = "", replace = true, id: forcedId, meta, edges, preserveMeta = false, startupReveal = false } = options;
|
const { type, url, name = "", replace = true, id: forcedId, meta, edges, preserveMeta = false } = options;
|
||||||
const modelSettings = meta?.viewerSettings;
|
const modelSettings = meta?.viewerSettings;
|
||||||
const normalizedType = normalizeType(type, typeof url === "string" ? url : "");
|
const normalizedType = normalizeType(type, typeof url === "string" ? url : "");
|
||||||
const inferredType = normalizedType || normalizeType(guessTypeFromName(typeof url === "string" ? url : ""));
|
const inferredType = normalizedType || normalizeType(guessTypeFromName(typeof url === "string" ? url : ""));
|
||||||
|
|
@ -6503,7 +6445,6 @@ const initViewer = async () => {
|
||||||
}
|
}
|
||||||
sceneModels[id] = currentModel;
|
sceneModels[id] = currentModel;
|
||||||
modelTypes[id] = inferredType;
|
modelTypes[id] = inferredType;
|
||||||
ensureLoadedModelTransformAlias(id, modelSettings);
|
|
||||||
|
|
||||||
currentModel.on("loaded", () => {
|
currentModel.on("loaded", () => {
|
||||||
reportViewerEvent("model-loaded", {
|
reportViewerEvent("model-loaded", {
|
||||||
|
|
@ -6514,14 +6455,8 @@ const initViewer = async () => {
|
||||||
objectCount: Object.keys(viewer.scene.objects || {}).length,
|
objectCount: Object.keys(viewer.scene.objects || {}).length,
|
||||||
src: srcToLoad,
|
src: srcToLoad,
|
||||||
});
|
});
|
||||||
setTimeout(() => {
|
setTimeout(() => applyStoredTransformsToModel(id), 0);
|
||||||
ensureLoadedModelTransformAlias(id, modelSettings);
|
setTimeout(() => applyStoredTransformsToModel(id), 60);
|
||||||
applyStoredTransformsToModel(id);
|
|
||||||
}, 0);
|
|
||||||
setTimeout(() => {
|
|
||||||
ensureLoadedModelTransformAlias(id, modelSettings);
|
|
||||||
applyStoredTransformsToModel(id);
|
|
||||||
}, 60);
|
|
||||||
const hasModelCamera = !!modelSettings?.viewerState?.camera;
|
const hasModelCamera = !!modelSettings?.viewerState?.camera;
|
||||||
if (!isLoadingSavedProject && !hasModelCamera) {
|
if (!isLoadingSavedProject && !hasModelCamera) {
|
||||||
viewer.cameraFlight.flyTo({ aabb: viewer.scene.getAABB({}) });
|
viewer.cameraFlight.flyTo({ aabb: viewer.scene.getAABB({}) });
|
||||||
|
|
@ -6531,9 +6466,6 @@ const initViewer = async () => {
|
||||||
}
|
}
|
||||||
setStatus(`Загружено: ${name || inferredType}`);
|
setStatus(`Загружено: ${name || inferredType}`);
|
||||||
dropZone.classList.add("hidden");
|
dropZone.classList.add("hidden");
|
||||||
if (startupReveal) {
|
|
||||||
markStartupContentReady();
|
|
||||||
}
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
if (inferredType === "las") {
|
if (inferredType === "las") {
|
||||||
snapshotPointCloudColors(id);
|
snapshotPointCloudColors(id);
|
||||||
|
|
@ -6570,9 +6502,6 @@ const initViewer = async () => {
|
||||||
src: srcToLoad,
|
src: srcToLoad,
|
||||||
}, "error");
|
}, "error");
|
||||||
showError(`Ошибка загрузки: ${err}`);
|
showError(`Ошибка загрузки: ${err}`);
|
||||||
if (startupReveal) {
|
|
||||||
markStartupContentReady();
|
|
||||||
}
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
updateNavCubeVisibility();
|
updateNavCubeVisibility();
|
||||||
updatePanelsVisibility();
|
updatePanelsVisibility();
|
||||||
|
|
@ -6591,7 +6520,6 @@ const initViewer = async () => {
|
||||||
refreshModelLods({ silent: true }).catch(() => {});
|
refreshModelLods({ silent: true }).catch(() => {});
|
||||||
|
|
||||||
// если есть сохранённые трансформы для модели — применим
|
// если есть сохранённые трансформы для модели — применим
|
||||||
ensureLoadedModelTransformAlias(id, modelSettings);
|
|
||||||
applyStoredTransformsToModel(id);
|
applyStoredTransformsToModel(id);
|
||||||
return currentModel;
|
return currentModel;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -6603,9 +6531,6 @@ const initViewer = async () => {
|
||||||
error: e?.message || String(e),
|
error: e?.message || String(e),
|
||||||
}, "error");
|
}, "error");
|
||||||
showError(e.message || "Ошибка загрузки");
|
showError(e.message || "Ошибка загрузки");
|
||||||
if (debugOptions.startupReveal) {
|
|
||||||
markStartupContentReady();
|
|
||||||
}
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -6653,10 +6578,9 @@ const initViewer = async () => {
|
||||||
nameParam: sharedViewer?.name || params.get("name"),
|
nameParam: sharedViewer?.name || params.get("name"),
|
||||||
});
|
});
|
||||||
if (!src) {
|
if (!src) {
|
||||||
reportViewerEvent("startup-no-src", { search: window.location.search });
|
reportViewerEvent("startup-no-src", { search: window.location.search }, "error");
|
||||||
clearShareStartupPending();
|
clearShareStartupPending();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
markStartupContentReady();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -6667,7 +6591,6 @@ const initViewer = async () => {
|
||||||
showError("Не удалось определить формат модели из URL");
|
showError("Не удалось определить формат модели из URL");
|
||||||
clearShareStartupPending();
|
clearShareStartupPending();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
markStartupContentReady();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -6677,9 +6600,6 @@ const initViewer = async () => {
|
||||||
clearShareStartupPending();
|
clearShareStartupPending();
|
||||||
|
|
||||||
let settingsSrc = sharedViewer?.settingsSrc || params.get("settingsSrc") || params.get("sourceSrc") || params.get("originalSrc") || src;
|
let settingsSrc = sharedViewer?.settingsSrc || params.get("settingsSrc") || params.get("sourceSrc") || params.get("originalSrc") || src;
|
||||||
const sharedViewerSettings = sharedViewer?.viewerSettings && typeof sharedViewer.viewerSettings === "object"
|
|
||||||
? sharedViewer.viewerSettings
|
|
||||||
: null;
|
|
||||||
const applyQueryViewerOverrides = () => {
|
const applyQueryViewerOverrides = () => {
|
||||||
if (params.has("displayMode")) {
|
if (params.has("displayMode")) {
|
||||||
activeDisplayMode = params.get("displayMode") || "source";
|
activeDisplayMode = params.get("displayMode") || "source";
|
||||||
|
|
@ -6717,11 +6637,11 @@ const initViewer = async () => {
|
||||||
selectedLod: sharedViewer?.selectedLod || null,
|
selectedLod: sharedViewer?.selectedLod || null,
|
||||||
versions: Array.isArray(sharedViewer?.versions) ? sharedViewer.versions : [],
|
versions: Array.isArray(sharedViewer?.versions) ? sharedViewer.versions : [],
|
||||||
shareToken: startupSharePayload?.share?.token || getShareTokenFromPath(),
|
shareToken: startupSharePayload?.share?.token || getShareTokenFromPath(),
|
||||||
viewerSettings: sharedViewerSettings,
|
viewerSettings: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyFetchedStartupSettings = (settingsResponse) => {
|
const applyFetchedStartupSettings = (settingsResponse) => {
|
||||||
const modelSettings = sharedViewerSettings || settingsResponse?.viewerSettings || null;
|
const modelSettings = settingsResponse?.viewerSettings || null;
|
||||||
settingsSrc = settingsResponse?.sourceSrc || settingsSrc;
|
settingsSrc = settingsResponse?.sourceSrc || settingsSrc;
|
||||||
reportViewerEvent("startup-settings-loaded", {
|
reportViewerEvent("startup-settings-loaded", {
|
||||||
settingsSrc,
|
settingsSrc,
|
||||||
|
|
@ -6763,9 +6683,7 @@ const initViewer = async () => {
|
||||||
|
|
||||||
if (params.has("edges")) {
|
if (params.has("edges")) {
|
||||||
startupMeta.viewerSettings = {
|
startupMeta.viewerSettings = {
|
||||||
...(startupMeta.viewerSettings || {}),
|
|
||||||
viewerState: {
|
viewerState: {
|
||||||
...(startupMeta.viewerSettings?.viewerState || {}),
|
|
||||||
edges: params.get("edges") !== "false"
|
edges: params.get("edges") !== "false"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -6776,20 +6694,16 @@ const initViewer = async () => {
|
||||||
name,
|
name,
|
||||||
type,
|
type,
|
||||||
settingsSrc,
|
settingsSrc,
|
||||||
hasViewerSettings: !!sharedViewerSettings,
|
hasViewerSettings: false,
|
||||||
});
|
});
|
||||||
const startupModel = loadModel({
|
loadModel({
|
||||||
type,
|
type,
|
||||||
url: src,
|
url: src,
|
||||||
name,
|
name,
|
||||||
replace: params.get("replace") !== "false",
|
replace: params.get("replace") !== "false",
|
||||||
edges: params.has("edges") ? params.get("edges") !== "false" : undefined,
|
edges: params.has("edges") ? params.get("edges") !== "false" : undefined,
|
||||||
meta: startupMeta,
|
meta: startupMeta,
|
||||||
startupReveal: true,
|
|
||||||
});
|
});
|
||||||
if (!startupModel) {
|
|
||||||
markStartupContentReady();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const startupModelLoadPromise = loadStartupModelFromQuery().catch((err) => {
|
const startupModelLoadPromise = loadStartupModelFromQuery().catch((err) => {
|
||||||
|
|
@ -6799,7 +6713,6 @@ const initViewer = async () => {
|
||||||
stack: err?.stack,
|
stack: err?.stack,
|
||||||
}, "error");
|
}, "error");
|
||||||
showError(err.message || "Не удалось загрузить модель из URL");
|
showError(err.message || "Не удалось загрузить модель из URL");
|
||||||
markStartupContentReady();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const loadProjectSnapshot = async (project) => {
|
const loadProjectSnapshot = async (project) => {
|
||||||
|
|
@ -7774,8 +7687,6 @@ const loadProjectSnapshot = async (project) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
appShellSetupComplete = true;
|
|
||||||
revealAppShell();
|
|
||||||
void startupModelLoadPromise;
|
void startupModelLoadPromise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -7786,7 +7697,6 @@ const loadProjectSnapshot = async (project) => {
|
||||||
stack: err?.stack,
|
stack: err?.stack,
|
||||||
}, "error");
|
}, "error");
|
||||||
showError(err?.message || "Не удалось инициализировать viewer");
|
showError(err?.message || "Не удалось инициализировать viewer");
|
||||||
revealAppShell({ force: true });
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,31 +8,7 @@
|
||||||
<link rel="alternate icon" href="/assets/favicon/favicon.ico">
|
<link rel="alternate icon" href="/assets/favicon/favicon.ico">
|
||||||
<link rel="apple-touch-icon" href="/assets/favicon/apple-touch-icon.png">
|
<link rel="apple-touch-icon" href="/assets/favicon/apple-touch-icon.png">
|
||||||
<link rel="manifest" href="/assets/favicon/manifest.webmanifest.json">
|
<link rel="manifest" href="/assets/favicon/manifest.webmanifest.json">
|
||||||
<style id="preload-hidden">
|
<style id="preload-hidden">body{opacity:0;}</style>
|
||||||
html,
|
|
||||||
body {
|
|
||||||
background: #1c1c1c;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
display: block !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Manrope:wght@500;600;700&display=swap">
|
||||||
|
|
@ -255,9 +231,9 @@
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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 href="https://fonts.googleapis.com/css2?family=Manrope:wght@500;600;700&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="/dcViewer.css?v=42">
|
<link rel="stylesheet" href="/dcViewer.css?v=41">
|
||||||
</head>
|
</head>
|
||||||
<body class="app-starting">
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<button class="logo-btn" id="homeButton" type="button" title="Открыть NODE.DC Hub"></button>
|
<button class="logo-btn" id="homeButton" type="button" title="Открыть NODE.DC Hub"></button>
|
||||||
<div class="toolbar"></div>
|
<div class="toolbar"></div>
|
||||||
|
|
@ -726,6 +702,6 @@
|
||||||
<input id="fileInput" class="hidden" type="file"
|
<input id="fileInput" class="hidden" type="file"
|
||||||
accept=".xkt,.glb,.gltf,.bim,.las,.laz,.obj,.stl">
|
accept=".xkt,.glb,.gltf,.bim,.las,.laz,.obj,.stl">
|
||||||
|
|
||||||
<script type="module" src="/dcViewer.js?v=64"></script>
|
<script type="module" src="/dcViewer.js?v=63"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
126
server/index.js
126
server/index.js
|
|
@ -1940,121 +1940,6 @@ const handleLogout = (req, res, {redirect = true} = {}) => {
|
||||||
res.end("<!doctype html><html><head><meta charset='utf-8'></head><body>NODE.DC BIM session closed.</body></html>");
|
res.end("<!doctype html><html><head><meta charset='utf-8'></head><body>NODE.DC BIM session closed.</body></html>");
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeViewerSettings = (value) => (
|
|
||||||
value && typeof value === "object" && !Array.isArray(value)
|
|
||||||
? value
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const viewerSettingsHaveTransforms = (settings) => {
|
|
||||||
const transforms = settings?.viewerState?.transforms;
|
|
||||||
return !!(
|
|
||||||
transforms &&
|
|
||||||
typeof transforms === "object" &&
|
|
||||||
!Array.isArray(transforms) &&
|
|
||||||
Object.keys(transforms).length
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const modelRecordMatchesShare = (model, share) => {
|
|
||||||
const meta = model?.meta && typeof model.meta === "object" ? model.meta : {};
|
|
||||||
if (share?.assetId && meta.assetId === share.assetId) {
|
|
||||||
if (share.projectId && meta.projectId && meta.projectId !== share.projectId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (share.versionId && meta.versionId && meta.versionId !== share.versionId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const haystack = [
|
|
||||||
model?.src,
|
|
||||||
meta.sourceSrc,
|
|
||||||
meta.settingsSrc,
|
|
||||||
meta.artifactSrc
|
|
||||||
].filter(Boolean).join("\n");
|
|
||||||
if (!haystack || !share?.assetId || !haystack.includes(share.assetId)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return !share.versionId || haystack.includes(share.versionId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const readProjectViewerSettingsForShare = async (share) => {
|
|
||||||
if (!share?.assetId) return null;
|
|
||||||
let entries = [];
|
|
||||||
try {
|
|
||||||
entries = await fs.readdir(DATA_DIR, {withFileTypes: true});
|
|
||||||
} catch (_err) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let best = null;
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (!entry.isFile() || !entry.name.endsWith(".json") || entry.name === "index.json") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const projectPath = path.join(DATA_DIR, entry.name);
|
|
||||||
const project = await readJSONFile(projectPath).catch(() => null);
|
|
||||||
if (!project || !Array.isArray(project.models)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!project.models.some((model) => modelRecordMatchesShare(model, share))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const settings = normalizeViewerSettings({
|
|
||||||
version: 1,
|
|
||||||
updatedAt: project.updatedAt || project.createdAt || nowIso(),
|
|
||||||
viewerState: project.viewerState || {},
|
|
||||||
design: project.design || null
|
|
||||||
});
|
|
||||||
if (!viewerSettingsHaveTransforms(settings)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const updatedAtMs = Date.parse(settings.updatedAt || "") || 0;
|
|
||||||
if (!best || updatedAtMs >= best.updatedAtMs) {
|
|
||||||
best = {settings, updatedAtMs};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return best?.settings || null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const readViewerSettingsForModelSrc = async (src) => {
|
|
||||||
const manifestRef = await findManifestForModelSrc(src).catch(() => null);
|
|
||||||
if (!manifestRef || !existsSync(manifestRef.manifestPath)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const manifest = await readJSONFile(manifestRef.manifestPath);
|
|
||||||
return normalizeViewerSettings(manifest?.viewerSettings);
|
|
||||||
};
|
|
||||||
|
|
||||||
const mergeViewerSettingsTransforms = (base, fallback) => {
|
|
||||||
const normalizedBase = normalizeViewerSettings(base);
|
|
||||||
const normalizedFallback = normalizeViewerSettings(fallback);
|
|
||||||
if (!normalizedBase || viewerSettingsHaveTransforms(normalizedBase) || !viewerSettingsHaveTransforms(normalizedFallback)) {
|
|
||||||
return normalizedBase || normalizedFallback || null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...normalizedBase,
|
|
||||||
viewerState: {
|
|
||||||
...(normalizedBase.viewerState || {}),
|
|
||||||
transforms: normalizedFallback.viewerState.transforms,
|
|
||||||
models: normalizedFallback.viewerState.models || normalizedBase.viewerState?.models || []
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const resolveShareViewerSettings = async (share) => {
|
|
||||||
const shareSettings = normalizeViewerSettings(share?.viewerSettings);
|
|
||||||
const manifestSettings = await readViewerSettingsForModelSrc(share?.settingsSrc || share?.src);
|
|
||||||
const base = shareSettings || manifestSettings;
|
|
||||||
if (viewerSettingsHaveTransforms(base)) {
|
|
||||||
return base;
|
|
||||||
}
|
|
||||||
const projectSettings = await readProjectViewerSettingsForShare(share);
|
|
||||||
return mergeViewerSettingsTransforms(base, projectSettings);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreateShare = async (req, res) => {
|
const handleCreateShare = async (req, res) => {
|
||||||
const session = await getValidatedBimSession(req, res);
|
const session = await getValidatedBimSession(req, res);
|
||||||
if (BIM_AUTH_REQUIRED && !session) {
|
if (BIM_AUTH_REQUIRED && !session) {
|
||||||
|
|
@ -2088,14 +1973,6 @@ const handleCreateShare = async (req, res) => {
|
||||||
const name = typeof payload?.name === "string" && payload.name.trim()
|
const name = typeof payload?.name === "string" && payload.name.trim()
|
||||||
? payload.name.trim().slice(0, 240)
|
? payload.name.trim().slice(0, 240)
|
||||||
: path.basename(src);
|
: path.basename(src);
|
||||||
const viewerSettings = await resolveShareViewerSettings({
|
|
||||||
...identity,
|
|
||||||
src,
|
|
||||||
settingsSrc,
|
|
||||||
versionId: typeof payload?.versionId === "string" ? payload.versionId : null,
|
|
||||||
selectedLod: stringOrNull(payload?.selectedLod),
|
|
||||||
viewerSettings: normalizeViewerSettings(payload?.viewerSettings)
|
|
||||||
});
|
|
||||||
const share = {
|
const share = {
|
||||||
token,
|
token,
|
||||||
access: "public-readonly",
|
access: "public-readonly",
|
||||||
|
|
@ -2107,7 +1984,6 @@ const handleCreateShare = async (req, res) => {
|
||||||
projectId: identity.projectId,
|
projectId: identity.projectId,
|
||||||
versionId: typeof payload?.versionId === "string" ? payload.versionId : null,
|
versionId: typeof payload?.versionId === "string" ? payload.versionId : null,
|
||||||
selectedLod: stringOrNull(payload?.selectedLod),
|
selectedLod: stringOrNull(payload?.selectedLod),
|
||||||
viewerSettings,
|
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
createdBy: session?.user ? {
|
createdBy: session?.user ? {
|
||||||
id: session.user.id || null,
|
id: session.user.id || null,
|
||||||
|
|
@ -2178,7 +2054,6 @@ const handleGetShare = async (req, res, token) => {
|
||||||
projectId: share.projectId,
|
projectId: share.projectId,
|
||||||
assetId: share.assetId
|
assetId: share.assetId
|
||||||
}).catch(() => null);
|
}).catch(() => null);
|
||||||
const viewerSettings = await resolveShareViewerSettings(share);
|
|
||||||
sendJSON(res, 200, {
|
sendJSON(res, 200, {
|
||||||
ok: true,
|
ok: true,
|
||||||
mode: authenticated ? "standard" : "guest",
|
mode: authenticated ? "standard" : "guest",
|
||||||
|
|
@ -2198,7 +2073,6 @@ const handleGetShare = async (req, res, token) => {
|
||||||
projectId: share.projectId || null,
|
projectId: share.projectId || null,
|
||||||
versionId: share.versionId || versionHistory?.currentVersionId || null,
|
versionId: share.versionId || versionHistory?.currentVersionId || null,
|
||||||
selectedLod: share.selectedLod || null,
|
selectedLod: share.selectedLod || null,
|
||||||
viewerSettings,
|
|
||||||
versions: versionHistory?.versions || []
|
versions: versionHistory?.versions || []
|
||||||
},
|
},
|
||||||
share: {
|
share: {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue