diff --git a/server/index.js b/server/index.js index 9659c4f..5bc1dc9 100644 --- a/server/index.js +++ b/server/index.js @@ -1946,6 +1946,79 @@ const normalizeViewerSettings = (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)) { @@ -1955,6 +2028,33 @@ const readViewerSettingsForModelSrc = async (src) => { 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 session = await getValidatedBimSession(req, res); if (BIM_AUTH_REQUIRED && !session) { @@ -1988,8 +2088,14 @@ 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 = normalizeViewerSettings(payload?.viewerSettings) - || await readViewerSettingsForModelSrc(settingsSrc || 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 = { token, access: "public-readonly", @@ -2072,8 +2178,7 @@ const handleGetShare = async (req, res, token) => { projectId: share.projectId, assetId: share.assetId }).catch(() => null); - const viewerSettings = normalizeViewerSettings(share.viewerSettings) - || await readViewerSettingsForModelSrc(share.settingsSrc || share.src); + const viewerSettings = await resolveShareViewerSettings(share); sendJSON(res, 200, { ok: true, mode: authenticated ? "standard" : "guest",