Add GLB XKT pipeline and LOD controls
This commit is contained in:
+29
-1
@@ -1242,6 +1242,32 @@ html.is-embedded-viewer #navCube {
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.model-lod-generate-btn {
|
||||
width: 100%;
|
||||
min-height: 42px;
|
||||
background: var(--modal-btn-primary, var(--accent));
|
||||
color: var(--modal-btn-primary-contrast, #15170f);
|
||||
box-shadow: 0 8px 24px color-mix(in srgb, var(--modal-btn-primary, var(--accent)) 25%, transparent);
|
||||
}
|
||||
|
||||
.model-lod-generate-btn.is-generating,
|
||||
.model-lod-generate-btn.is-generating:disabled {
|
||||
cursor: progress;
|
||||
opacity: 1;
|
||||
animation: model-lod-generate-pulse 2.4s ease-in-out infinite;
|
||||
box-shadow: 0 8px 26px color-mix(in srgb, var(--modal-btn-primary, var(--accent)) 28%, transparent);
|
||||
}
|
||||
|
||||
@keyframes model-lod-generate-pulse {
|
||||
0%,
|
||||
100% {
|
||||
background: var(--modal-btn-primary, var(--accent));
|
||||
}
|
||||
50% {
|
||||
background: color-mix(in srgb, var(--modal-btn-primary, var(--accent)) 72%, white 28%);
|
||||
}
|
||||
}
|
||||
|
||||
.inspector-color-host {
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -3242,7 +3268,9 @@ html[data-share-startup] body:not([data-viewer-mode="standard"]) #projectNameSec
|
||||
body[data-viewer-mode="guest"] #saveProjectSection,
|
||||
html[data-share-startup] body:not([data-viewer-mode="standard"]) #saveProjectSection,
|
||||
body[data-viewer-mode="guest"] #modelVersionUploadButton,
|
||||
html[data-share-startup] body:not([data-viewer-mode="standard"]) #modelVersionUploadButton {
|
||||
html[data-share-startup] body:not([data-viewer-mode="standard"]) #modelVersionUploadButton,
|
||||
body[data-viewer-mode="guest"] #modelLodGenerateButton,
|
||||
html[data-share-startup] body:not([data-viewer-mode="standard"]) #modelLodGenerateButton {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
+330
-11
@@ -123,6 +123,9 @@ const modelVersionSelectHost = document.getElementById("modelVersionSelect");
|
||||
const modelVersionHistoryButton = document.getElementById("modelVersionHistoryButton");
|
||||
const modelVersionUploadButton = document.getElementById("modelVersionUploadButton");
|
||||
const modelVersionFileInput = document.getElementById("modelVersionFileInput");
|
||||
const modelLodControl = document.getElementById("modelLodControl");
|
||||
const modelLodSelectHost = document.getElementById("modelLodSelect");
|
||||
const modelLodGenerateButton = document.getElementById("modelLodGenerateButton");
|
||||
const navigationAxisControl = document.getElementById("navigationAxisControl");
|
||||
const navigationAxisSelectHost = document.getElementById("navigationAxisSelect");
|
||||
const zoomSpeedControl = document.getElementById("zoomSpeedControl");
|
||||
@@ -200,6 +203,7 @@ const AUTH_SESSION_API = "/api/auth/session";
|
||||
const SHARES_API = "/api/shares";
|
||||
const COMMENTS_API = "/api/comments";
|
||||
const UPLOAD_VERSIONS_API = "/api/uploads/versions";
|
||||
const UPLOAD_LODS_API = "/api/uploads/lods";
|
||||
const SHARE_AUTH_GUEST_PARAM = "ndc_bim_guest";
|
||||
const SHARE_PROMOTION_TIMEOUT_MS = 4500;
|
||||
const SHARE_PROMOTION_RETRY_MS = 15000;
|
||||
@@ -317,6 +321,9 @@ let startupSharePayload = null;
|
||||
let modelVersionHistory = null;
|
||||
let modelVersionHistoryLoading = false;
|
||||
let modelVersionUploading = false;
|
||||
let modelLodLoading = false;
|
||||
let modelLodGenerating = false;
|
||||
let modelLodPollTimer = null;
|
||||
let versionHistoryModal = null;
|
||||
let loadModelAsyncRef = null;
|
||||
let versionModelCounter = 0;
|
||||
@@ -344,6 +351,7 @@ let customDisplayColorControl = null;
|
||||
let customDisplaySaturationSlider = null;
|
||||
let lightingModeSelect = null;
|
||||
let modelVersionSelect = null;
|
||||
let modelLodSelect = null;
|
||||
let navigationAxisSelect = null;
|
||||
let zoomSpeedSlider = null;
|
||||
let defaultCameraWorldAxis = null;
|
||||
@@ -2277,7 +2285,8 @@ const getShareableModelPayload = () => {
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
const src = normalizeModelSettingsSrc(entry.meta?.artifactSrc || entry.src || entry.meta?.src);
|
||||
const selectedLodSrc = getSelectedLodViewerSrc(entry);
|
||||
const src = normalizeModelSettingsSrc(selectedLodSrc || entry.meta?.artifactSrc || entry.src || entry.meta?.src);
|
||||
const settingsSrc = getModelSettingsSrc(entry);
|
||||
if (!src) {
|
||||
return null;
|
||||
@@ -2287,9 +2296,10 @@ const getShareableModelPayload = () => {
|
||||
assetId: entry.meta?.assetId || entry.meta?.conversion?.assetId || null,
|
||||
projectId: entry.meta?.projectId || entry.meta?.conversion?.projectId || versionIdentity.projectId || null,
|
||||
name: entry.name || entry.meta?.label || src.split("/").pop() || "model",
|
||||
selectedLod: getCurrentLodIdForEntry(entry) || null,
|
||||
settingsSrc: settingsSrc || src,
|
||||
src,
|
||||
type: entry.type || normalizeType(null, src),
|
||||
type: normalizeType(null, src) || entry.meta?.artifactType || entry.type,
|
||||
versionId: entry.meta?.versionId || entry.meta?.conversion?.versionId || null,
|
||||
versions: Array.isArray(entry.meta?.versions) ? entry.meta.versions : [],
|
||||
};
|
||||
@@ -3483,6 +3493,7 @@ const updatePanelsVisibility = () => {
|
||||
if (saveModelSettingsButton) {
|
||||
saveModelSettingsButton.disabled = isGuestMode() || !hasModels || !hasModelSettingsTarget();
|
||||
}
|
||||
updateModelLodControl();
|
||||
if (deleteProjectButton) {
|
||||
deleteProjectButton.disabled = isGuestMode() || !(currentProjectMeta || lastAttemptedProjectId);
|
||||
}
|
||||
@@ -4626,7 +4637,7 @@ const formatFetchFailureMessage = (url, res, text, data) => {
|
||||
const formatApiNonJsonMessage = (url, res, text) => {
|
||||
const status = res?.status ? `HTTP ${res.status}` : "HTTP";
|
||||
if (/^\s*<!doctype html/i.test(text || "")) {
|
||||
return `BIM API на этом адресе вернул HTML вместо JSON (${status}). На localhost:8080 сейчас нет backend /api/comments.`;
|
||||
return `BIM API на этом адресе вернул HTML вместо JSON (${status}). Открой viewer через backend-профиль npm run serve, не через статический http-server.`;
|
||||
}
|
||||
return `BIM API вернул не-JSON ответ (${status}).`;
|
||||
};
|
||||
@@ -4760,6 +4771,252 @@ const getVersionViewerType = (version) => {
|
||||
return normalizeType(version?.type, src) || normalizeType(guessTypeFromName(src || version?.originalFilename || ""));
|
||||
};
|
||||
|
||||
const getLodKey = (lod) => lod?.id || lod?.artifactSrc || lod?.src || lod?.url || "lod";
|
||||
|
||||
const getLodViewerSrc = (lod) => normalizeVersionSource(lod?.src || lod?.url || lod?.artifactSrc || "");
|
||||
|
||||
const getLodViewerType = (lod) => {
|
||||
const src = getLodViewerSrc(lod);
|
||||
return normalizeType(lod?.type, src) || normalizeType(guessTypeFromName(src)) || "gltf";
|
||||
};
|
||||
|
||||
const getEntryLods = (entry = getPrimaryModelEntry()) => {
|
||||
const lods = entry?.meta?.lods;
|
||||
return Array.isArray(lods) ? lods : [];
|
||||
};
|
||||
|
||||
const getCurrentLodIdForEntry = (entry = getPrimaryModelEntry()) => {
|
||||
const meta = entry?.meta || {};
|
||||
const savedId = meta.selectedLodId ||
|
||||
meta.selectedLod ||
|
||||
meta.viewerSettings?.viewerState?.lod?.selectedId ||
|
||||
meta.viewerSettings?.viewerState?.lod?.id ||
|
||||
"";
|
||||
if (savedId) return savedId;
|
||||
const entrySrc = normalizeModelSettingsSrc(entry?.src || meta.artifactSrc || "");
|
||||
const currentLod = getEntryLods(entry).find((lod) => {
|
||||
const lodSrc = normalizeModelSettingsSrc(lod?.artifactSrc || lod?.src || lod?.url || "");
|
||||
return lodSrc && lodSrc === entrySrc;
|
||||
});
|
||||
return currentLod?.id || getEntryLods(entry)[0]?.id || "";
|
||||
};
|
||||
|
||||
const getSelectedLodRecord = (entry = getPrimaryModelEntry()) => {
|
||||
const lods = getEntryLods(entry);
|
||||
const currentId = getCurrentLodIdForEntry(entry);
|
||||
return lods.find((lod) => currentId && lod?.id === currentId) || null;
|
||||
};
|
||||
|
||||
const getSelectedLodViewerSrc = (entry = getPrimaryModelEntry()) => {
|
||||
const lod = getSelectedLodRecord(entry);
|
||||
return lod ? getLodViewerSrc(lod) : "";
|
||||
};
|
||||
|
||||
const setEntryLodPayload = (entry, payload) => {
|
||||
if (!entry || !payload) return;
|
||||
const meta = entry.meta || {};
|
||||
entry.meta = {
|
||||
...meta,
|
||||
sourceSrc: payload.sourceSrc || meta.sourceSrc || null,
|
||||
settingsSrc: payload.sourceSrc || meta.settingsSrc || null,
|
||||
artifactSrc: payload.artifactSrc || meta.artifactSrc || null,
|
||||
artifactType: payload.artifactType || meta.artifactType || null,
|
||||
xktConversion: payload.xktConversion || meta.xktConversion || null,
|
||||
lodGeneration: payload.lodGeneration || null,
|
||||
lods: Array.isArray(payload.lods) ? payload.lods : [],
|
||||
selectedLod: payload.selectedLod || meta.selectedLod || null,
|
||||
};
|
||||
};
|
||||
|
||||
const isGlbLodCandidate = (entry = getPrimaryModelEntry()) => {
|
||||
if (!entry) return false;
|
||||
const meta = entry.meta || {};
|
||||
const candidates = [
|
||||
meta.sourceFormat,
|
||||
meta.sourceSrc,
|
||||
meta.settingsSrc,
|
||||
meta.artifactSrc,
|
||||
entry.src,
|
||||
entry.name,
|
||||
].filter(Boolean);
|
||||
return candidates.some((value) => String(value).toLowerCase().includes(".glb") || String(value).toLowerCase() === "glb");
|
||||
};
|
||||
|
||||
const buildLodParams = (entry = getPrimaryModelEntry()) => {
|
||||
const params = new URLSearchParams();
|
||||
const src = getModelSettingsSrc(entry) || entry?.meta?.sourceSrc || entry?.meta?.settingsSrc || "";
|
||||
if (src) params.set("src", src);
|
||||
return params;
|
||||
};
|
||||
|
||||
const fetchModelLods = async (entry = getPrimaryModelEntry()) => {
|
||||
const params = buildLodParams(entry);
|
||||
if (!params.toString()) return null;
|
||||
return fetchJSON(`${UPLOAD_LODS_API}?${params.toString()}`);
|
||||
};
|
||||
|
||||
const formatLodOptionLabel = (lod) => {
|
||||
const label = lod?.label || lod?.id || "LOD";
|
||||
const size = formatFileSize(lod?.size);
|
||||
return size ? `${label} · ${size}` : label;
|
||||
};
|
||||
|
||||
const updateModelLodControl = () => {
|
||||
const entry = getPrimaryModelEntry();
|
||||
const lods = getEntryLods(entry);
|
||||
const generation = entry?.meta?.lodGeneration || null;
|
||||
const generationStatus = generation?.status || "";
|
||||
const hasControl = !!entry && isGlbLodCandidate(entry) && (!isGuestMode() || lods.length > 0 || modelLodLoading);
|
||||
modelLodControl?.classList.toggle("hidden", !hasControl);
|
||||
if (!hasControl) {
|
||||
modelLodGenerateButton?.classList.remove("is-generating");
|
||||
return;
|
||||
}
|
||||
|
||||
const hasReadyLods = lods.length > 0;
|
||||
modelLodSelectHost?.classList.toggle("hidden", !hasReadyLods);
|
||||
modelLodGenerateButton?.classList.toggle("hidden", hasReadyLods);
|
||||
|
||||
if (modelLodGenerateButton) {
|
||||
const busy = modelLodGenerating || modelLodLoading || generationStatus === "queued" || generationStatus === "processing";
|
||||
modelLodGenerateButton.classList.toggle("is-generating", busy);
|
||||
modelLodGenerateButton.disabled = isGuestMode() || busy;
|
||||
modelLodGenerateButton.textContent = busy
|
||||
? "Генерируем LOD..."
|
||||
: generationStatus === "failed"
|
||||
? "Повторить генерацию LOD"
|
||||
: "Сгенерировать LOD";
|
||||
}
|
||||
|
||||
if (modelLodSelect) {
|
||||
const options = hasReadyLods
|
||||
? lods.map((lod) => ({ value: getLodKey(lod), label: formatLodOptionLabel(lod) }))
|
||||
: [{ value: "loading", label: modelLodLoading ? "Загружаем LOD..." : "LOD не сгенерирован" }];
|
||||
const currentValue = getCurrentLodIdForEntry(entry) || options[0]?.value;
|
||||
modelLodSelect.setOptions(options, currentValue);
|
||||
modelLodSelect.setDisabled(!hasReadyLods);
|
||||
}
|
||||
};
|
||||
|
||||
const scheduleModelLodPoll = () => {
|
||||
if (modelLodPollTimer) {
|
||||
window.clearTimeout(modelLodPollTimer);
|
||||
}
|
||||
modelLodPollTimer = window.setTimeout(() => {
|
||||
modelLodPollTimer = null;
|
||||
refreshModelLods({ silent: true }).catch(() => {});
|
||||
}, 2500);
|
||||
};
|
||||
|
||||
const refreshModelLods = async ({ silent = false } = {}) => {
|
||||
const entry = getPrimaryModelEntry();
|
||||
if (!entry || !isGlbLodCandidate(entry)) {
|
||||
updateModelLodControl();
|
||||
return null;
|
||||
}
|
||||
modelLodLoading = true;
|
||||
updateModelLodControl();
|
||||
try {
|
||||
const payload = await fetchModelLods(entry);
|
||||
setEntryLodPayload(entry, payload);
|
||||
const status = payload?.lodGeneration?.status;
|
||||
if (status === "queued" || status === "processing") {
|
||||
scheduleModelLodPoll();
|
||||
}
|
||||
updateModelLodControl();
|
||||
return payload;
|
||||
} catch (err) {
|
||||
if (!silent) console.warn("model LOD state unavailable", err);
|
||||
updateModelLodControl();
|
||||
return null;
|
||||
} finally {
|
||||
modelLodLoading = false;
|
||||
updateModelLodControl();
|
||||
}
|
||||
};
|
||||
|
||||
const openModelLod = async (lod) => {
|
||||
if (!loadModelAsyncRef) {
|
||||
showError("Загрузчик модели еще не готов");
|
||||
return;
|
||||
}
|
||||
const viewerSrc = getLodViewerSrc(lod);
|
||||
const viewerType = getLodViewerType(lod);
|
||||
if (!viewerSrc || !viewerType) {
|
||||
setStatus("Этот LOD еще не готов к просмотру");
|
||||
return;
|
||||
}
|
||||
const previousEntry = getPrimaryModelEntry();
|
||||
const meta = {
|
||||
...(previousEntry?.meta || {}),
|
||||
artifactSrc: normalizeModelSettingsSrc(viewerSrc) || viewerSrc,
|
||||
selectedLod: lod.id || null,
|
||||
selectedLodId: lod.id || null,
|
||||
};
|
||||
if (meta.viewerSettings?.viewerState) {
|
||||
meta.viewerSettings = {
|
||||
...meta.viewerSettings,
|
||||
viewerState: {
|
||||
...meta.viewerSettings.viewerState,
|
||||
lod: {
|
||||
selectedId: lod.id || null,
|
||||
selectedSrc: normalizeModelSettingsSrc(viewerSrc) || viewerSrc,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
try {
|
||||
await loadModelAsyncRef({
|
||||
type: viewerType,
|
||||
url: viewerSrc,
|
||||
name: previousEntry?.name || meta.label || lod.label || "model",
|
||||
replace: true,
|
||||
preserveMeta: true,
|
||||
id: previousEntry?.model?.id || "model",
|
||||
meta,
|
||||
});
|
||||
updateModelLodControl();
|
||||
setStatus(`Открыт LOD: ${lod.label || lod.id || "LOD"}`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showError(err.message || "Не удалось открыть LOD модели");
|
||||
updateModelLodControl();
|
||||
}
|
||||
};
|
||||
|
||||
const generateModelLods = async () => {
|
||||
const entry = getPrimaryModelEntry();
|
||||
if (!entry || isGuestMode() || modelLodGenerating) return;
|
||||
const settingsSrc = getModelSettingsSrc(entry);
|
||||
if (!settingsSrc) {
|
||||
setStatus("LOD можно сгенерировать только для модели из BIM-хранилища");
|
||||
return;
|
||||
}
|
||||
modelLodGenerating = true;
|
||||
updateModelLodControl();
|
||||
try {
|
||||
const payload = await fetchJSON(UPLOAD_LODS_API, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
settingsSrc,
|
||||
sourceSrc: entry.meta?.sourceSrc || settingsSrc,
|
||||
artifactSrc: entry.meta?.artifactSrc || entry.src || null,
|
||||
}),
|
||||
});
|
||||
setEntryLodPayload(entry, payload);
|
||||
scheduleModelLodPoll();
|
||||
setStatus("Генерация LOD поставлена в очередь");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showError(err.message || "Не удалось запустить генерацию LOD");
|
||||
} finally {
|
||||
modelLodGenerating = false;
|
||||
updateModelLodControl();
|
||||
}
|
||||
};
|
||||
|
||||
const getCurrentVersionIdForEntry = (entry = getPrimaryModelEntry()) => {
|
||||
const meta = entry?.meta || {};
|
||||
return meta.versionId || meta.conversion?.versionId || modelVersionHistory?.currentVersionId || null;
|
||||
@@ -4898,6 +5155,8 @@ const openModelVersion = async (version) => {
|
||||
sourceSrc: normalizeModelSettingsSrc(settingsSrc) || settingsSrc,
|
||||
settingsSrc: normalizeModelSettingsSrc(settingsSrc) || settingsSrc,
|
||||
artifactSrc: normalizeModelSettingsSrc(viewerSrc) || viewerSrc,
|
||||
artifactType: viewerType,
|
||||
xktConversion: version.xktConversion || null,
|
||||
viewerSettings: null,
|
||||
};
|
||||
try {
|
||||
@@ -4905,6 +5164,14 @@ const openModelVersion = async (version) => {
|
||||
if (settings?.viewerSettings) {
|
||||
meta.viewerSettings = settings.viewerSettings;
|
||||
}
|
||||
if (settings) {
|
||||
meta.lodGeneration = settings.lodGeneration || null;
|
||||
meta.lods = Array.isArray(settings.lods) ? settings.lods : [];
|
||||
meta.selectedLod = settings.selectedLod || null;
|
||||
meta.artifactSrc = settings.artifactSrc || meta.artifactSrc;
|
||||
meta.artifactType = normalizeType(null, settings.artifactSrc) || meta.artifactType || viewerType;
|
||||
meta.xktConversion = settings.xktConversion || meta.xktConversion || null;
|
||||
}
|
||||
const previousModelId = previousEntry?.model?.id || null;
|
||||
const nextModelId = makeVersionModelId(version);
|
||||
if (previousModelId && transformStore[previousModelId]) {
|
||||
@@ -4926,6 +5193,7 @@ const openModelVersion = async (version) => {
|
||||
versions: modelVersionHistory?.versions || meta.versions || [],
|
||||
};
|
||||
updateModelVersionControl();
|
||||
updateModelLodControl();
|
||||
setStatus(`Открыта версия v${version.version || ""}`.trim());
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -5282,9 +5550,10 @@ const buildProjectSnapshot = async (name) => {
|
||||
|
||||
for (const entry of activeModels) {
|
||||
const model = entry.model;
|
||||
const modelType = normalizeType(entry.type, entry.src) || normalizeType(guessTypeFromName(entry.src)) || entry.type || null;
|
||||
const src = typeof entry.src === "string" ? entry.src : "";
|
||||
const meta = entry.meta || {};
|
||||
const preferredSrc = normalizeModelSettingsSrc(getSelectedLodViewerSrc(entry) || meta.artifactSrc || src) || src;
|
||||
const modelType = normalizeType(null, preferredSrc) || normalizeType(entry.type, src) || normalizeType(guessTypeFromName(src)) || entry.type || null;
|
||||
const versionIdentity = getModelVersionIdentity(entry);
|
||||
const baseModel = {
|
||||
id: model.id,
|
||||
@@ -5299,7 +5568,12 @@ const buildProjectSnapshot = async (name) => {
|
||||
metadataPath: meta.metadataPath || null,
|
||||
settingsSrc: getModelSettingsSrc(entry) || null,
|
||||
sourceSrc: meta.sourceSrc || meta.conversion?.sourceSrc || meta.manifest?.sourceSrc || null,
|
||||
artifactSrc: meta.artifactSrc || null
|
||||
artifactSrc: meta.artifactSrc || null,
|
||||
artifactType: meta.artifactType || normalizeType(null, meta.artifactSrc) || null,
|
||||
xktConversion: meta.xktConversion || null,
|
||||
lodGeneration: meta.lodGeneration || null,
|
||||
lods: Array.isArray(meta.lods) ? meta.lods : [],
|
||||
selectedLod: getCurrentLodIdForEntry(entry) || meta.selectedLod || null
|
||||
},
|
||||
visible: model.visible !== false
|
||||
};
|
||||
@@ -5308,12 +5582,12 @@ const buildProjectSnapshot = async (name) => {
|
||||
throw new Error("Не удалось определить тип модели для сохранения.");
|
||||
}
|
||||
|
||||
if (isRemoteSrc(src)) {
|
||||
models.push({ ...baseModel, src });
|
||||
if (isRemoteSrc(preferredSrc)) {
|
||||
models.push({ ...baseModel, src: preferredSrc });
|
||||
continue;
|
||||
}
|
||||
|
||||
const cacheKey = src || `${model.id}-${modelType}`;
|
||||
const cacheKey = preferredSrc || src || `${model.id}-${modelType}`;
|
||||
if (uploadCache.has(cacheKey)) {
|
||||
models.push({ ...baseModel, src: uploadCache.get(cacheKey) });
|
||||
continue;
|
||||
@@ -5429,7 +5703,22 @@ const buildModelViewerSettings = () => ({
|
||||
customDisplaySaturation: activeCustomDisplaySaturation,
|
||||
lightingMode: activeLightingMode,
|
||||
navigationAxis: activeNavigationAxis,
|
||||
zoomSpeed: activeZoomSpeed
|
||||
zoomSpeed: activeZoomSpeed,
|
||||
lod: (() => {
|
||||
const entry = getPrimaryModelEntry();
|
||||
const selected = getSelectedLodRecord(entry);
|
||||
const selectedSrc = getSelectedLodViewerSrc(entry);
|
||||
return {
|
||||
selectedId: selected?.id || getCurrentLodIdForEntry(entry) || null,
|
||||
selectedSrc: normalizeModelSettingsSrc(selectedSrc) || selectedSrc || null,
|
||||
levels: getEntryLods(entry).map((lod) => ({
|
||||
id: lod.id || null,
|
||||
label: lod.label || null,
|
||||
artifactSrc: normalizeModelSettingsSrc(lod.artifactSrc || lod.src || lod.url || "") || null,
|
||||
size: Number(lod.size) || 0
|
||||
}))
|
||||
};
|
||||
})()
|
||||
} : {},
|
||||
design: settingsMenu?.getState ? settingsMenu.getState() : {}
|
||||
});
|
||||
@@ -5496,6 +5785,8 @@ const fetchModelSettings = async (src) => {
|
||||
reportViewerEvent("model-settings-response", {
|
||||
src: data?.sourceSrc || normalized,
|
||||
hasViewerSettings: !!data?.viewerSettings,
|
||||
lodCount: Array.isArray(data?.lods) ? data.lods.length : 0,
|
||||
lodStatus: data?.lodGeneration?.status || null,
|
||||
});
|
||||
return data;
|
||||
};
|
||||
@@ -5530,8 +5821,12 @@ const saveCurrentModelSettings = async () => {
|
||||
...(entry.meta || {}),
|
||||
settingsSrc: saved?.sourceSrc || settingsSrc,
|
||||
sourceSrc: saved?.sourceSrc || entry.meta?.sourceSrc || null,
|
||||
lodGeneration: saved?.lodGeneration || entry.meta?.lodGeneration || null,
|
||||
lods: Array.isArray(saved?.lods) ? saved.lods : (entry.meta?.lods || []),
|
||||
selectedLod: saved?.selectedLod || entry.meta?.selectedLod || null,
|
||||
viewerSettings: saved?.viewerSettings || payload.viewerSettings
|
||||
};
|
||||
updateModelLodControl();
|
||||
setStatus("Настройки модели сохранены");
|
||||
log(`Настройки модели сохранены: ${entry.name || entry.model?.id || settingsSrc}`, false);
|
||||
} catch (err) {
|
||||
@@ -6222,6 +6517,7 @@ const initViewer = async () => {
|
||||
updateNavCubeVisibility();
|
||||
updatePanelsVisibility();
|
||||
refreshModelVersionHistory({ silent: true }).catch(() => {});
|
||||
refreshModelLods({ silent: true }).catch(() => {});
|
||||
|
||||
// если есть сохранённые трансформы для модели — применим
|
||||
applyStoredTransformsToModel(id);
|
||||
@@ -6338,6 +6634,7 @@ const initViewer = async () => {
|
||||
assetId: sharedViewer?.assetId || null,
|
||||
projectId: sharedViewer?.projectId || null,
|
||||
versionId: sharedViewer?.versionId || null,
|
||||
selectedLod: sharedViewer?.selectedLod || null,
|
||||
versions: Array.isArray(sharedViewer?.versions) ? sharedViewer.versions : [],
|
||||
shareToken: startupSharePayload?.share?.token || getShareTokenFromPath(),
|
||||
viewerSettings: null,
|
||||
@@ -6350,7 +6647,6 @@ const initViewer = async () => {
|
||||
settingsSrc,
|
||||
hasViewerSettings: !!modelSettings,
|
||||
});
|
||||
if (!modelSettings) return;
|
||||
const normalizedSettingsSrc = normalizeModelSettingsSrc(settingsSrc) || startupMeta.settingsSrc;
|
||||
startupMeta.settingsSrc = normalizedSettingsSrc;
|
||||
startupMeta.viewerSettings = modelSettings;
|
||||
@@ -6360,10 +6656,17 @@ const initViewer = async () => {
|
||||
...(entry.meta || {}),
|
||||
settingsSrc: normalizedSettingsSrc,
|
||||
sourceSrc: normalizedSettingsSrc,
|
||||
artifactSrc: startupMeta.artifactSrc,
|
||||
artifactSrc: settingsResponse?.artifactSrc || startupMeta.artifactSrc,
|
||||
artifactType: normalizeType(null, settingsResponse?.artifactSrc || startupMeta.artifactSrc) || startupMeta.artifactType || null,
|
||||
xktConversion: settingsResponse?.xktConversion || startupMeta.xktConversion || null,
|
||||
lodGeneration: settingsResponse?.lodGeneration || null,
|
||||
lods: Array.isArray(settingsResponse?.lods) ? settingsResponse.lods : [],
|
||||
selectedLod: settingsResponse?.selectedLod || startupMeta.selectedLod || null,
|
||||
viewerSettings: modelSettings,
|
||||
};
|
||||
updateModelLodControl();
|
||||
}
|
||||
if (!modelSettings) return;
|
||||
applyModelViewerSettings(modelSettings, { applyCamera: true, applyDesign: true });
|
||||
applyQueryViewerOverrides();
|
||||
};
|
||||
@@ -6734,6 +7037,19 @@ const loadProjectSnapshot = async (project) => {
|
||||
});
|
||||
updateModelVersionControl();
|
||||
}
|
||||
if (modelLodControl && modelLodSelectHost) {
|
||||
modelLodSelect = createGlassSelect({
|
||||
host: modelLodSelectHost,
|
||||
value: "loading",
|
||||
options: [{ value: "loading", label: "LOD не сгенерирован" }],
|
||||
ariaLabel: "Уровень LOD",
|
||||
onChange: (value) => {
|
||||
const lod = getEntryLods(getPrimaryModelEntry()).find((item) => getLodKey(item) === value);
|
||||
if (lod) openModelLod(lod);
|
||||
},
|
||||
});
|
||||
updateModelLodControl();
|
||||
}
|
||||
if (displayModeControl && displayModeSelectHost) {
|
||||
displayModeSelect = createGlassSelect({
|
||||
host: displayModeSelectHost,
|
||||
@@ -6841,6 +7157,9 @@ const loadProjectSnapshot = async (project) => {
|
||||
event.target.value = "";
|
||||
if (file) await uploadModelVersion(file);
|
||||
});
|
||||
modelLodGenerateButton?.addEventListener("click", () => {
|
||||
generateModelLods();
|
||||
});
|
||||
registerFloatingWindowElement("inspector", sidePanelEl);
|
||||
registerFloatingWindowElement("comments", commentsPanel);
|
||||
registerFloatingWindowElement("settings", settingsPanel);
|
||||
|
||||
+7
-2
@@ -231,7 +231,7 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<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 rel="stylesheet" href="/dcViewer.css?v=40">
|
||||
<link rel="stylesheet" href="/dcViewer.css?v=41">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
@@ -310,6 +310,11 @@
|
||||
<div class="inspector-select-host" id="modelVersionSelect"></div>
|
||||
<input id="modelVersionFileInput" class="hidden" type="file" accept=".xkt,.glb,.gltf,.bim,.las,.laz,.obj,.stl,.step,.stp">
|
||||
</div>
|
||||
<div class="inspector-control-row inspector-control-row--full hidden" id="modelLodControl">
|
||||
<div class="inspector-control-label">Уровни LOD</div>
|
||||
<button class="primary-btn model-lod-generate-btn" id="modelLodGenerateButton" type="button">Сгенерировать LOD</button>
|
||||
<div class="inspector-select-host hidden" id="modelLodSelect"></div>
|
||||
</div>
|
||||
<div class="inspector-control-row inspector-control-row--half" id="displayModeControl">
|
||||
<div class="inspector-control-label">Режим отображения</div>
|
||||
<div class="inspector-select-host" id="displayModeSelect"></div>
|
||||
@@ -697,6 +702,6 @@
|
||||
<input id="fileInput" class="hidden" type="file"
|
||||
accept=".xkt,.glb,.gltf,.bim,.las,.laz,.obj,.stl">
|
||||
|
||||
<script type="module" src="/dcViewer.js?v=61"></script>
|
||||
<script type="module" src="/dcViewer.js?v=63"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user