diff --git a/frontend/dcViewer.js b/frontend/dcViewer.js index 39f3e7c..a5fac30 100644 --- a/frontend/dcViewer.js +++ b/frontend/dcViewer.js @@ -2301,6 +2301,7 @@ const getShareableModelPayload = () => { src, type: normalizeType(null, src) || entry.meta?.artifactType || entry.type, versionId: entry.meta?.versionId || entry.meta?.conversion?.versionId || null, + viewerSettings: buildModelViewerSettings(), versions: Array.isArray(entry.meta?.versions) ? entry.meta.versions : [], }; }; @@ -4077,6 +4078,37 @@ const applyStoredTransformsToModel = (modelId) => { 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 title = (node?.title || node?.text || node?.name || "").trim(); const firstObject = (node && collectObjectIds(node)[0]) || null; @@ -5532,9 +5564,7 @@ const snapshotVec = (v) => { return null; }; -const buildProjectSnapshot = async (name) => { - const models = []; - +const buildActiveTransformsSnapshot = () => { const transformsCopy = safeClone(transformStore); activeModels.forEach(({ model }) => { if (!model || !model.id) return; @@ -5547,6 +5577,18 @@ const buildProjectSnapshot = async (name) => { }; } }); + 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) { const model = entry.model; @@ -5637,10 +5679,7 @@ const buildProjectSnapshot = async (name) => { ids: lastSelection.ids || [], modelId: lastSelection.modelId || null, }, - models: activeModels.map((entry) => ({ - id: entry.model?.id, - visible: entry.model?.visible !== false - })), + models: buildActiveModelStateSnapshot(), transforms: transformsCopy } : {}; @@ -5704,6 +5743,8 @@ const buildModelViewerSettings = () => ({ lightingMode: activeLightingMode, navigationAxis: activeNavigationAxis, zoomSpeed: activeZoomSpeed, + models: buildActiveModelStateSnapshot(), + transforms: buildActiveTransformsSnapshot(), lod: (() => { const entry = getPrimaryModelEntry(); const selected = getSelectedLodRecord(entry); @@ -5885,7 +5926,10 @@ const applyViewerState = (state) => { if (state.transforms && typeof state.transforms === "object") { Object.keys(transformStore).forEach((k) => delete transformStore[k]); Object.assign(transformStore, safeClone(state.transforms)); - Object.keys(sceneModels).forEach((mid) => applyStoredTransformsToModel(mid)); + Object.keys(sceneModels).forEach((mid) => { + ensureLoadedModelTransformAlias(mid, { viewerState: state }); + applyStoredTransformsToModel(mid); + }); } if (Array.isArray(state.models)) { state.models.forEach((m) => { @@ -6445,6 +6489,7 @@ const initViewer = async () => { } sceneModels[id] = currentModel; modelTypes[id] = inferredType; + ensureLoadedModelTransformAlias(id, modelSettings); currentModel.on("loaded", () => { reportViewerEvent("model-loaded", { @@ -6455,8 +6500,14 @@ const initViewer = async () => { objectCount: Object.keys(viewer.scene.objects || {}).length, src: srcToLoad, }); - setTimeout(() => applyStoredTransformsToModel(id), 0); - setTimeout(() => applyStoredTransformsToModel(id), 60); + setTimeout(() => { + ensureLoadedModelTransformAlias(id, modelSettings); + applyStoredTransformsToModel(id); + }, 0); + setTimeout(() => { + ensureLoadedModelTransformAlias(id, modelSettings); + applyStoredTransformsToModel(id); + }, 60); const hasModelCamera = !!modelSettings?.viewerState?.camera; if (!isLoadingSavedProject && !hasModelCamera) { viewer.cameraFlight.flyTo({ aabb: viewer.scene.getAABB({}) }); @@ -6520,6 +6571,7 @@ const initViewer = async () => { refreshModelLods({ silent: true }).catch(() => {}); // если есть сохранённые трансформы для модели — применим + ensureLoadedModelTransformAlias(id, modelSettings); applyStoredTransformsToModel(id); return currentModel; } catch (e) { @@ -6600,6 +6652,9 @@ const initViewer = async () => { clearShareStartupPending(); 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 = () => { if (params.has("displayMode")) { activeDisplayMode = params.get("displayMode") || "source"; @@ -6637,11 +6692,11 @@ const initViewer = async () => { selectedLod: sharedViewer?.selectedLod || null, versions: Array.isArray(sharedViewer?.versions) ? sharedViewer.versions : [], shareToken: startupSharePayload?.share?.token || getShareTokenFromPath(), - viewerSettings: null, + viewerSettings: sharedViewerSettings, }; const applyFetchedStartupSettings = (settingsResponse) => { - const modelSettings = settingsResponse?.viewerSettings || null; + const modelSettings = sharedViewerSettings || settingsResponse?.viewerSettings || null; settingsSrc = settingsResponse?.sourceSrc || settingsSrc; reportViewerEvent("startup-settings-loaded", { settingsSrc, @@ -6683,7 +6738,9 @@ const initViewer = async () => { if (params.has("edges")) { startupMeta.viewerSettings = { + ...(startupMeta.viewerSettings || {}), viewerState: { + ...(startupMeta.viewerSettings?.viewerState || {}), edges: params.get("edges") !== "false" } }; @@ -6694,7 +6751,7 @@ const initViewer = async () => { name, type, settingsSrc, - hasViewerSettings: false, + hasViewerSettings: !!sharedViewerSettings, }); loadModel({ type, diff --git a/server/index.js b/server/index.js index 764822d..fb2d296 100644 --- a/server/index.js +++ b/server/index.js @@ -1973,6 +1973,9 @@ const handleCreateShare = async (req, res) => { const name = typeof payload?.name === "string" && payload.name.trim() ? payload.name.trim().slice(0, 240) : path.basename(src); + const viewerSettings = payload?.viewerSettings && typeof payload.viewerSettings === "object" && !Array.isArray(payload.viewerSettings) + ? payload.viewerSettings + : null; const share = { token, access: "public-readonly", @@ -1984,6 +1987,7 @@ const handleCreateShare = async (req, res) => { projectId: identity.projectId, versionId: typeof payload?.versionId === "string" ? payload.versionId : null, selectedLod: stringOrNull(payload?.selectedLod), + viewerSettings, createdAt: now, createdBy: session?.user ? { id: session.user.id || null, @@ -2073,6 +2077,7 @@ const handleGetShare = async (req, res, token) => { projectId: share.projectId || null, versionId: share.versionId || versionHistory?.currentVersionId || null, selectedLod: share.selectedLod || null, + viewerSettings: share.viewerSettings || null, versions: versionHistory?.versions || [] }, share: {