{# Trajectory analyze: path-order mutate, coords/volumes API, z-path save, GIF lock helpers #} function applyManualAnchorPathOrderPreview(order) { var session = typeof activeTrajectorySession === "function" ? activeTrajectorySession() : null; var orderedRows = null; if (order === "preserve") { pendingOriginalCatalogIndexReorder = false; orderedRows = applyOriginalCatalogIndexPathOrderNow({ skipChrome: true }) || reorderManualPathToOriginalCatalogIndexOrder(); } else if (order === "heuristic") { reorderManualPathPointsForDirectPath(); buildManualPickerRows(); // Prefer the interleaved tour just written to anchors — do not re-derive // catalog-then-custom order from selection helpers. if (anchorIndicesActive && anchorIndicesActive.length >= 2) { orderedRows = anchorIndicesActive.slice(); } } if (session) { var rows = orderedRows && orderedRows.length >= 2 ? orderedRows : visitOrderAnchorPlotRows(); session.setVisitOrderPath(order, rows || []); } else if (orderedRows && orderedRows.length >= 2 && typeof reorderManualSelectionFromAnchorRows === "function") { if (order !== "preserve") reorderManualSelectionFromAnchorRows(orderedRows); anchorIndicesActive = orderedRows.slice(); trajPlotRows = orderedRows.slice(); var xyPath = trajXYFromPlotRows(orderedRows); if (xyPath && xyPath.length === orderedRows.length) { anchorTrajXY = xyPath; } } syncManualAnchorsPreview({ skipVolumeSync: true }); if (session && typeof syncManualVolumeSliderPreview === "function") { syncManualVolumeSliderPreview({ fromSession: true }); } else if (trajectoryVolumeDisplayReorderPending) { finishAnchorPathOrderVolumePreserveIfPending(); } } function requestExactAnchorPathOrder() { if (!anchorPathOrderEnabled()) return; if (anchorPathOrderActive === "exact" && !pendingAnchorPathOrder) return; beginAnchorPathOrderVolumePreserve(); pendingAnchorPathOrder = "exact"; syncAnchorPathOrderUI(); setTrajStatus("Computing exact anchor path order…", false); if (isManualTraversalMode()) { if (manualInterpolationArmed()) { // Keep interiors; re-run interp after the server reorders anchors. syncManualSelectionToAnchors({ forceInterp: true, directLine: typeof manualDirectLineDensifyActive === "function" && manualDirectLineDensifyActive(), snapVolume: !!manualParticleSnapPathActive }); } else { // Catalog-only path: permute the current waypoints only (like Inexact / // Original order). Do not fetch an interpolated latent polyline. fetchExactAnchorPermutation(); } return; } // Particle-set path in direct-trace: Exact permutation then local rebuild // restores catalog marker XY (coords fetch would snap to plot particles). if (typeof particleSetDrivenDirectPath === "function" && particleSetDrivenDirectPath()) { fetchExactAnchorPermutation(); return; } // Direct-trace mode with file/typed anchors: coords fetch with n_points=0 // remains a pure anchor permutation after the server-side short-circuit. fetchTrajectoryCoords({ anchorPathOrderChange: true }); } function requestHeuristicAnchorPathOrder() { if (!anchorPathOrderEnabled()) return; if (anchorPathOrderActive === "heuristic" && !pendingAnchorPathOrder) return; beginAnchorPathOrderVolumePreserve(); pendingAnchorPathOrder = "heuristic"; syncAnchorPathOrderUI(); setTrajStatus("Computing anchor path order…", false); if (isManualTraversalMode()) { if (manualInterpolationArmed()) { syncManualSelectionToAnchors({ forceInterp: true, directLine: typeof manualDirectLineDensifyActive === "function" && manualDirectLineDensifyActive(), snapVolume: !!manualParticleSnapPathActive }); } else { applyManualAnchorPathOrderPreview("heuristic"); finishAnchorPathOrderRequest("heuristic"); setTrajStatus("", false); } return; } // Particle-set path: local non-crossing tour + rebuildFromSelection keeps // catalog marker XY (choose-waypoints parity). Server coords fetch replaces // those coordinates with nearest-particle plot positions. if (typeof particleSetDrivenDirectPath === "function" && particleSetDrivenDirectPath()) { applyManualAnchorPathOrderPreview("heuristic"); finishAnchorPathOrderRequest("heuristic"); setTrajStatus("", false); return; } fetchTrajectoryCoords({ anchorPathOrderChange: true }); } function requestPreserveAnchorPathOrder() { if (!anchorPathOrderEnabled()) return; // Always re-apply native PC / kmeans index order, even when already on // Original order (e.g. after Exact/Inexact or Reverse left a tour). beginAnchorPathOrderVolumePreserve(); pendingAnchorPathOrder = "preserve"; syncAnchorPathOrderUI(); if (isManualTraversalMode()) { if (manualInterpolationArmed()) { applyOriginalCatalogIndexPathOrderNow(); syncManualSelectionToAnchors({ preserveOrder: true, forceInterp: true, directLine: typeof manualDirectLineDensifyActive === "function" && manualDirectLineDensifyActive(), snapVolume: !!manualParticleSnapPathActive }); finishAnchorPathOrderRequest("preserve"); } else { applyManualAnchorPathOrderPreview("preserve"); finishAnchorPathOrderRequest("preserve"); } return; } // Particle-set path in direct-trace: Original order locally (marker XY). if (typeof particleSetDrivenDirectPath === "function" && particleSetDrivenDirectPath()) { applyManualAnchorPathOrderPreview("preserve"); finishAnchorPathOrderRequest("preserve"); return; } fetchTrajectoryCoords({ anchorPathOrderChange: true }); } function syncTraversalModeLabels() { var xlab = axisDisplayName(sx.value); var ylab = axisDisplayName(sy.value); if (trajModeDirectLabel) { trajModeDirectLabel.innerHTML = "Tracing trajectory path directly
in " + xlab + "/" + ylab + " space"; } if (trajScatterInterpDirectLabel) { trajScatterInterpDirectLabel.innerHTML = "Use direct interpolation
in " + xlab + "/" + ylab + " space"; } } function syncTraversalSettingsText() { var hasAnchors = hasAnchorIndices(); var particleSetDirect = typeof particleSetDrivenDirectPath === "function" && particleSetDrivenDirectPath(); syncTraversalModeLabels(); syncTraversalAltButtons(); syncPointsSelectorRange(); if (trajPointsRowEl) { // Choose-waypoints hides Points; particle-set paths in direct-trace match. trajPointsRowEl.style.display = (isManualTraversalMode() || particleSetDirect) ? "none" : "flex"; } if (!trajPointsLabelEl) return; if (!hasAnchors || isGraphTraversalActive() || isManualTraversalMode() || particleSetDirect) { trajPointsLabelEl.textContent = "Points"; return; } trajPointsLabelEl.textContent = "Interpolation points between anchors"; } function currentVolumeCount() { return resolvedTrajectoryVolumeCount(); } function volumeNavDisplayCount() { if (isManualTraversalMode()) { if (manualDirectSnapActive() && anchorTrajXY && anchorTrajXY.length >= 2) { return anchorTrajXY.length; } if (manualTrajectoryVolumeIds.length >= 2) { return manualTrajectoryVolumeIds.length; } if (manualSelectedVolIds.length >= 2) { return manualSelectedVolIds.length; } } if (directEndpointPathConfigured() && !hasGeneratedTrajectoryVolumes()) { return currentVolumeCount(); } if (anchorKmeansVolumeIds.length >= 2) return anchorKmeansVolumeIds.length; return currentVolumeCount(); } function updateSaveVolumesButtonLabel() { if (!btnSaveVolumesEl) return; var saveLabel; if (isManualTraversalMode() || analyzeVolumeCatalogFullyCoversPath()) { saveLabel = "Save volumes"; } else { var n = generatedTrajectoryVolumeCount > 0 ? generatedTrajectoryVolumeCount : (trajectoryVolumesToGenerateCount() || currentVolumeCount()); saveLabel = "Save " + n + " volumes"; } if (typeof CryoDashboardIcons !== "undefined" && typeof CryoDashboardIcons.setSaveButtonLabel === "function") { CryoDashboardIcons.setSaveButtonLabel(btnSaveVolumesEl, saveLabel); } else { btnSaveVolumesEl.textContent = saveLabel; } } function updateGenerateVolumesButtonLabel() { if (!btnGenerateVolumesEl) return; var busyDecode = trajectoryDecodeJobInFlight(); var busyRender = trajectoryChimeraxJobInFlight() || !!pendingCombinedRenderAfterDecode; if (busyDecode && pendingCombinedRenderAfterDecode) { btnGenerateVolumesEl.textContent = "Decoding and\nrendering\u2026"; } else if (busyDecode) { var busyDecodeN = activeVolumeJobTotal("decode"); btnGenerateVolumesEl.textContent = busyDecodeN === 1 ? "Decoding\nvolume\u2026" : "Decoding\nvolumes\u2026"; } else if (pendingCombinedRenderAfterDecode || trajectoryChimeraxJobInFlight()) { var busyRenderN = activeVolumeJobTotal("chimerax"); btnGenerateVolumesEl.textContent = busyRenderN === 1 ? "Rendering\nvolume\u2026" : "Rendering\nvolumes\u2026"; } else if (busyRender) { var busyRenderN2 = activeVolumeJobTotal("chimerax"); btnGenerateVolumesEl.textContent = busyRenderN2 === 1 ? "Rendering\nvolume\u2026" : "Rendering\nvolumes\u2026"; } else { var debts = typeof trajectoryDecodeRenderDebts === "function" ? trajectoryDecodeRenderDebts() : null; var decodeN = debts ? debts.decode : trajectoryVolumesToGenerateCount(); if (decodeN == null || !Number.isFinite(decodeN)) decodeN = 0; var renderN = debts ? debts.render : (typeof trajectoryVolumesOutstandingRenderCount === "function" ? trajectoryVolumesOutstandingRenderCount() : trajectoryVolumesToRenderCount()); if (renderN == null || !Number.isFinite(renderN)) renderN = 0; // Label shows outstanding debt. Do not zero decodeN when the action is // disabled — that made Reverse appear to change Decode/Render text. // Keep render debt visible so VTK/2D with matching decode+render counts // still read "Decode and render N volumes" (decode still hydrates the viewer). btnGenerateVolumesEl.textContent = buildDecodeRenderVolumesButtonLabel(decodeN, renderN); } updateSaveVolumesButtonLabel(); if (!updateGenerateVolumesButtonLabel._fromVisibilitySync) { syncGenerateVolumesButtonVisibility(); } else if (btnGenerateVolumesEl) { btnGenerateVolumesEl.disabled = !decodeRenderVolumesActionEnabled(); } } /** * Combined Decode/Render control: decode outstanding volumes, then render * outstanding ChimeraX images as one action (no intermediate "Render N"). */ function requestDecodeAndRenderVolumes() { reconcileChimeraxRenderingFlag(); if (trajectoryVolumeActionBusy()) return; if (!decodeRenderVolumesActionEnabled()) return; var debts = typeof trajectoryDecodeRenderDebts === "function" ? trajectoryDecodeRenderDebts() : null; var decodeN = debts ? debts.decode : trajectoryVolumesToGenerateCount(); if (decodeN == null || !Number.isFinite(decodeN)) decodeN = 0; var renderN = debts ? debts.render : (typeof trajectoryVolumesOutstandingRenderCount === "function" ? trajectoryVolumesOutstandingRenderCount() : trajectoryVolumesToRenderCount()); if (renderN == null || !Number.isFinite(renderN)) renderN = 0; var canDecode = generateVolumesActionEnabled() && decodeN > 0; // Interactive backends only decode; ChimeraX PNG batches stay ChimeraX-mode. var wantRender = renderN > 0 && !(typeof isInteractiveVolumeBackend === "function" && isInteractiveVolumeBackend()); function startCombinedRender() { if (!pendingCombinedRenderAfterDecode) return; updateGenerateVolumesButtonLabel(); if (!(trajectoryVolumesToRenderCount() > 0 || (typeof trajectoryVolumesOutstandingRenderCount === "function" && trajectoryVolumesOutstandingRenderCount() > 0) || pendingCombinedRenderBatchTotal > 0)) { clearPendingCombinedRenderAfterDecode(); updateGenerateVolumesButtonLabel(); return; } if (!requestManualWaypointVolumeRerender({ force: true })) { clearPendingCombinedRenderAfterDecode(); updateGenerateVolumesButtonLabel(); if (typeof setTrajStatus === "function") { setTrajStatus("Could not start ChimeraX rendering for the selected volumes.", false); } } } if (canDecode) { if (wantRender) armPendingCombinedRenderAfterDecode(renderN); else clearPendingCombinedRenderAfterDecode(); updateGenerateVolumesButtonLabel(); var decodePromise = generateTrajectory(); if (decodePromise && typeof decodePromise.then === "function") { decodePromise.then(function(res) { if (res && res.ok === false) { clearPendingCombinedRenderAfterDecode(); updateGenerateVolumesButtonLabel(); return; } // Primary kick-off is onDecodeComplete; this is a backup if decode // resolved without scheduling render (e.g. nothing left to decode). setTimeout(function() { if (!pendingCombinedRenderAfterDecode) return; if (trajectoryChimeraxJobInFlight()) return; startCombinedRender(); }, 0); }).catch(function() { clearPendingCombinedRenderAfterDecode(); updateGenerateVolumesButtonLabel(); }); } else { // Early exit from generateTrajectory (nothing decoded) — still render. if (pendingCombinedRenderAfterDecode) startCombinedRender(); else updateGenerateVolumesButtonLabel(); } return; } if (wantRender) { armPendingCombinedRenderAfterDecode(renderN); updateGenerateVolumesButtonLabel(); if (!requestManualWaypointVolumeRerender({ force: true })) { clearPendingCombinedRenderAfterDecode(); updateGenerateVolumesButtonLabel(); if (typeof setTrajStatus === "function") { setTrajStatus("Could not start ChimeraX rendering for the selected volumes.", false); } } } } function isTrajDecodeRenderChromeReady() { return !!trajDecodeRenderChromeReady; } function maybeMarkTrajDecodeRenderChromeReady() { if (trajDecodeRenderChromeReady) return false; // Wait for the first blocking scatter paint to finish. if (!trajScatterRenderingEverCompleted) return false; // Direct-trace / waypoint pages need the analyze catalog before endpoint // / PC1 debt counts are meaningful. if (!manualCatalogLoaded) return false; trajDecodeRenderChromeReady = true; reconcileChimeraxRenderingFlag(); if (typeof updateGenerateVolumesButtonLabel === "function") { updateGenerateVolumesButtonLabel(); } return true; } function setGeneratedVolumesState(cacheId, nGenerated, opts) { opts = opts || {}; var nextCacheId = String(cacheId || ""); var clearingGenerated = !nextCacheId && !!lastTrajectoryVolumeCacheId; if (clearingGenerated && typeof stashActiveChimeraxRenderingsToHeap === "function") { // Stash while cache / slot identities are still valid for heap keys. stashActiveChimeraxRenderingsToHeap(); } lastTrajectoryVolumeCacheId = nextCacheId; generatedTrajectoryVolumeCount = Number.isFinite(nGenerated) ? Math.max(0, nGenerated) : 0; // Partial decode after a path move must keep stale flags until Render // finishes; clearTrajectoryVolumeInvalidationForPayload handles slots // once ChimeraX frames land. Full decode clears decode/render debt only — // never detachedAt, or freely moved endpoints lose traj-vol tick labels. if (lastTrajectoryVolumeCacheId && !opts.partialDecode) { if (directTraceUi) { if (typeof directTraceUi.clearStale === "function") directTraceUi.clearStale(); if (typeof directTraceUi.clearDragInvalidated === "function") { directTraceUi.clearDragInvalidated(); } if (typeof bindDirectTraceInvalidationAliases === "function") { bindDirectTraceInvalidationAliases(); } } else { trajVolumeStaleAt = {}; dragVolumeInvalidated = {}; } } syncTrajGifRecordButton(); syncTrajReverseButton(); if (!lastTrajectoryVolumeCacheId) { lastTrajectoryVolumeCacheSlotIndices = null; if (volumesDisplayReady() && volumePayload()) { updateGenerateVolumesButtonLabel(); syncTrajVolBackendChrome(); applyManualAnalyzeVolumeDisplay( isManualTraversalMode() ? preferredManualVolumeBackend() : currentVolumeRenderBackend() ); syncDirectModeVolumePendingOverlay(); return; } resetVtkNavigationCamera(); lastVolumePayload = null; if (trajVolColumn) trajVolColumn.innerHTML = ""; if (trajVolDisplay && !trajectoryChimeraxJobInFlight()) { trajVolDisplay.loadPayload({ volumes: [], images: [] }); if (trajVolDisplay.expandedBelow) trajVolDisplay.setExpandedBelow(false); } closeTrajVolPopoutModal(); syncTrajVolBackendChrome(); } updateGenerateVolumesButtonLabel(); syncDirectModeVolumePendingOverlay(); } function scheduleTrajectoryOverlayRefresh() { function attempt(n) { if (!gd || !gd.data || !gd.data.length) { if (n < 120) requestAnimationFrame(function() { attempt(n + 1); }); return; } if (isManualTraversalMode() && manualInterpolatedPathExpansionArmed()) { if (!_manualInterpolatedScatterPathReady()) return; } else if (!anchorTrajXY || anchorTrajXY.length < 2) { return; } redrawTrajectoryOverlay(); scheduleTrajGlyphOverlaySync(); } attempt(0); } function redrawTrajectoryOverlay() { if (!gd || !gd.data || !gd.data.length) return; if (!isManualTraversalMode() && typeof ensureWaypointPathXYForOverlay === "function") { ensureWaypointPathXYForOverlay(); } var pathXY = null; var expected = (isManualTraversalMode() && manualInterpolatedPathExpansionArmed()) ? expectedManualInterpolatedPointCount() : 0; if (manualInterpolatedPathExpansionArmed()) { pathXY = activeManualTrajectoryXY(); } else if (isManualTraversalMode() && manualTrajectoryHasInteriorSamples()) { pathXY = activeManualTrajectoryXY(); } if ((!pathXY || pathXY.length < 2) && typeof activeTrajectoryPathXY === "function") { var pathClassXY = activeTrajectoryPathXY(); if (pathClassXY && (!expected || pathClassXY.length >= expected)) pathXY = pathClassXY; } if ((!pathXY || pathXY.length < 2) && anchorTrajXY && anchorTrajXY.length >= 2) { if (!expected || anchorTrajXY.length >= expected) pathXY = anchorTrajXY; } if (pathXY && pathXY.length >= 2 && (anchorIndicesActive || isManualTraversalMode())) { drawAnchorPolyline(pathXY); return; } if (isManualTraversalMode()) { removeOverlayTraces(); return; } var editablePathXY = typeof activeTrajectoryPathXY === "function" ? activeTrajectoryPathXY() : null; if (!hasAnchorIndices() && editablePathXY && editablePathXY.length >= 2) { drawEditableTrajectory(editablePathXY); return; } if (!hasAnchorIndices() && editableTrajXY && editableTrajXY.length >= 2) { drawEditableTrajectory(editableTrajXY); return; } if (!startXY || !endXY) return; drawLineAndEndpoints(startXY[0], startXY[1], endXY[0], endXY[1]); } function clearTrajectoryZPanel() { zPanelRequestId++; lastZPathTxt = null; if (trajZNums) trajZNums.textContent = ""; if (trajZPre) trajZPre.textContent = ""; if (trajZPanel) trajZPanel.hidden = true; } function zTrajToPathTxt(zTraj) { if (!zTraj || !zTraj.length) return ""; return zTraj.map(function(row) { var vals = Array.isArray(row) ? row : [row]; return vals.map(function(v) { var x = Number(v); return Number.isFinite(x) ? String(x) : String(v); }).join(" "); }).join("\n") + "\n"; } function catalogVolumeKindForId(volId) { if (volId == null || volId === "") return null; var key = String(volId); var marker = manualMarkersByVolId && manualMarkersByVolId[key]; if (marker && marker.kind) return String(marker.kind); var entry = manualCatalogById && manualCatalogById[key]; if (entry && entry.kind) return String(entry.kind); if (/^pc[0-9]+:/i.test(key)) return "pc"; if (/^kmeans:/i.test(key)) return "kmeans"; return null; } function lerpLatentZRows(zA, zB, t) { var a = zA || []; var b = zB || []; var n = Math.max(a.length, b.length); var out = new Array(n); for (var i = 0; i < n; i++) { var va = Number(a[i]); var vb = Number(b[i]); if (!Number.isFinite(va) && !Number.isFinite(vb)) out[i] = 0; else if (!Number.isFinite(va)) out[i] = vb; else if (!Number.isFinite(vb)) out[i] = va; else out[i] = va + t * (vb - va); } return out; } /** * Direct-line densify of PC catalog anchors: expand marker.z along the same * spacing as buildLinearInterpolatedPathXY. Labels stay on anchor slots only. */ function buildDensifiedPcCatalogZPayload() { if (!manualSelectedVolIds || manualSelectedVolIds.length < 2) return null; var anchorIds = manualSelectedVolIds.map(String); var anchorZ = []; var anchorLabels = []; for (var ai = 0; ai < anchorIds.length; ai++) { if (catalogVolumeKindForId(anchorIds[ai]) !== "pc") return null; var marker = manualMarkersByVolId && manualMarkersByVolId[anchorIds[ai]]; if (!marker || !marker.z || !marker.z.length) return null; anchorZ.push(marker.z.map(Number)); anchorLabels.push(marker.label ? String(marker.label) : anchorIds[ai]); } var nInterp = typeof readManualSnapNPoints === "function" ? readManualSnapNPoints() : 0; if (!Number.isFinite(nInterp) || nInterp < 1) return null; var densifyLive = !!( (typeof manualInterpolationArmed === "function" && manualInterpolationArmed()) || (typeof manualVolumeSnapActive !== "undefined" && manualVolumeSnapActive) || (typeof manualParticleSnapPathActive !== "undefined" && manualParticleSnapPathActive) || (typeof graphTraversalActive !== "undefined" && graphTraversalActive) || (manualActiveCustomPlotRows && manualActiveCustomPlotRows.length) ); var expected = typeof expectedManualSnapPointCount === "function" ? expectedManualSnapPointCount(anchorIds.length, nInterp) : 0; var pathXY = null; if (anchorTrajXY && anchorTrajXY.length >= 2) pathXY = anchorTrajXY; else if (typeof activeTrajectoryPathXY === "function") pathXY = activeTrajectoryPathXY(); var pathLen = pathXY && pathXY.length ? pathXY.length : (typeof lastLatentTrajectoryPointCount === "number" ? lastLatentTrajectoryPointCount : 0); // After Other deselection the path collapses to catalog anchors — never keep // a densified latent-z listing once interiors / interp are gone. if (!densifyLive || !expected || pathLen < expected || pathLen <= anchorIds.length) { return null; } if (pathLen !== expected) { // Prefer the live densified polyline length when it matches expansion. if (!(pathXY && pathXY.length > anchorIds.length)) return null; expected = pathXY.length; // Infer step from anchors/path: path = (nA-1)*step + 1 var stepGuess = (expected - 1) / (anchorIds.length - 1); if (!Number.isFinite(stepGuess) || stepGuess < 1 || Math.abs(stepGuess - Math.round(stepGuess)) > 1e-6) { return null; } nInterp = Math.round(stepGuess) - 1; } var step = nInterp + 1; var zTraj = []; var trajXy = []; var labels = []; var particleIndices = []; for (var i = 0; i < expected; i++) { var a0 = Math.floor(i / step); if (a0 >= anchorIds.length - 1) a0 = anchorIds.length - 2; var local = i - a0 * step; var t = step <= 0 ? 0 : local / step; if (local === 0) { zTraj.push(anchorZ[a0].slice()); labels.push(anchorLabels[a0]); } else if (a0 === anchorIds.length - 2 && local === step) { zTraj.push(anchorZ[a0 + 1].slice()); labels.push(anchorLabels[a0 + 1]); } else { zTraj.push(lerpLatentZRows(anchorZ[a0], anchorZ[a0 + 1], t)); labels.push(null); } particleIndices.push(null); if (pathXY && pathXY[i] && pathXY[i].length >= 2) { trajXy.push([Number(pathXY[i][0]), Number(pathXY[i][1])]); } else { trajXy.push(null); } } // Final sample is always the last anchor (matches buildLinearInterpolatedPathXY). zTraj[expected - 1] = anchorZ[anchorIds.length - 1].slice(); labels[expected - 1] = anchorLabels[anchorIds.length - 1]; return { ok: true, z_traj: zTraj, z_path_txt: zTrajToPathTxt(zTraj), traj_xy: trajXy, traj_point_labels: labels, traj_particle_indices: particleIndices, xcol: sx && sx.value ? sx.value : undefined, ycol: sy && sy.value ? sy.value : undefined }; } /** * PC analyze volumes are latent-space samples (z_values.txt), not dataset * particles. Nearest plot_row is only a scatter fallback — never treat it as * a particle identity in the latent-z panel. */ function scrubNonParticleIdentityInZPayload(j, volIds) { if (!j) return j; volIds = volIds || ( typeof manualVolumeDisplayIdsForCurrentPath === "function" ? manualVolumeDisplayIdsForCurrentPath() : null ); if (!volIds || !volIds.length) return j; var zLenIn = j.z_traj ? j.z_traj.length : 0; // Compact PC catalog ids must not be packed onto a longer densified path. if (zLenIn > volIds.length) { var densified = buildDensifiedPcCatalogZPayload(); if (densified) return densified; return j; } var zTraj = (j.z_traj && j.z_traj.length) ? j.z_traj.map(function(row) { return Array.isArray(row) ? row.slice() : row; }) : []; var pidx = Array.isArray(j.traj_particle_indices) ? j.traj_particle_indices.slice() : []; var labels = Array.isArray(j.traj_point_labels) ? j.traj_point_labels.slice() : []; var changed = false; var n = Math.max(volIds.length, zTraj.length, pidx.length); for (var i = 0; i < n; i++) { var id = i < volIds.length ? volIds[i] : null; if (id == null || id === "") continue; var kind = catalogVolumeKindForId(id); if (kind !== "pc") continue; changed = true; while (pidx.length <= i) pidx.push(null); pidx[i] = null; var marker = manualMarkersByVolId && manualMarkersByVolId[String(id)]; while (labels.length <= i) labels.push(null); if (marker && marker.label) labels[i] = String(marker.label); else if (!labels[i]) labels[i] = String(id); if (marker && marker.z && marker.z.length) { while (zTraj.length <= i) zTraj.push(null); zTraj[i] = marker.z.map(Number); } } if (!changed) return j; var out = Object.assign({}, j, { z_traj: zTraj, traj_particle_indices: pidx, traj_point_labels: labels }); if (zTraj.length && zTraj.every(function(row) { return row && row.length; })) { out.z_path_txt = zTrajToPathTxt(zTraj); } return out; } /** * Build latent-z payload from analyze-catalog markers when every path slot has * true latent z (PC volumes). Returns null when any slot needs particle z. */ function buildCatalogTrajectoryZPayload() { var densified = buildDensifiedPcCatalogZPayload(); if (densified) return densified; if (typeof manualVolumeDisplayIdsForCurrentPath !== "function" && !(manualSelectedVolIds && manualSelectedVolIds.length >= 2)) { return null; } var ids = typeof manualVolumeDisplayIdsForCurrentPath === "function" ? manualVolumeDisplayIdsForCurrentPath() : manualSelectedVolIds.map(String); if (!ids || ids.length < 2) return null; var pathXY = null; if (typeof activeTrajectoryPathXY === "function") pathXY = activeTrajectoryPathXY(); if ((!pathXY || pathXY.length !== ids.length) && anchorTrajXY && anchorTrajXY.length === ids.length) { pathXY = anchorTrajXY; } var pathLen = pathXY && pathXY.length ? pathXY.length : (anchorTrajXY && anchorTrajXY.length ? anchorTrajXY.length : (typeof lastLatentTrajectoryPointCount === "number" ? lastLatentTrajectoryPointCount : 0)); var zTraj = []; var trajXy = []; var labels = []; var particleIndices = []; var anyPc = false; var rejectReason = null; if (pathLen && pathLen > ids.length) { rejectReason = "densified-path-without-builder"; } for (var i = 0; !rejectReason && i < ids.length; i++) { var id = ids[i]; if (id == null || id === "") { rejectReason = "empty-id@" + i; break; } var key = String(id); var marker = manualMarkersByVolId && manualMarkersByVolId[key]; var kind = catalogVolumeKindForId(key); if (kind === "pc") { anyPc = true; if (!marker || !marker.z || !marker.z.length) { rejectReason = "pc-missing-z@" + key; break; } zTraj.push(marker.z.map(Number)); particleIndices.push(null); labels.push(marker.label ? String(marker.label) : key); } else { // Kmeans / Other / densified interiors need particle or interpolated z. rejectReason = "non-pc@" + key; break; } var xy = null; if (pathXY && pathXY[i] && pathXY[i].length >= 2) { xy = [Number(pathXY[i][0]), Number(pathXY[i][1])]; } else if (typeof scatterXYForVolumeId === "function") { xy = scatterXYForVolumeId(key); } else if (marker && marker.xy && marker.xy.length >= 2) { xy = [Number(marker.xy[0]), Number(marker.xy[1])]; } trajXy.push(xy && Number.isFinite(xy[0]) && Number.isFinite(xy[1]) ? xy : null); } if (rejectReason) return null; if (!anyPc) return null; return { ok: true, z_traj: zTraj, z_path_txt: zTrajToPathTxt(zTraj), traj_xy: trajXy, traj_point_labels: labels, traj_particle_indices: particleIndices, xcol: sx && sx.value ? sx.value : undefined, ycol: sy && sy.value ? sy.value : undefined }; } function applyTrajectoryZPanelFromPayload(j) { var densifiedPc = buildDensifiedPcCatalogZPayload(); if (densifiedPc) j = densifiedPc; else j = scrubNonParticleIdentityInZPayload(j); if (j && j.z_path_txt && trajZPre && trajZPanel) { lastZPathTxt = j.z_path_txt; var hasZTraj = j.z_traj && j.z_traj.length; trajZPre.textContent = hasZTraj ? formatZPathDisplayFromZTraj(j.z_traj) : j.z_path_txt; if (trajZNums) trajZNums.textContent = hasZTraj ? formatZPathGutterFromPayload(j) : ""; trajZPanel.hidden = false; return true; } clearTrajectoryZPanel(); return false; } /** * Show latent-z chrome as soon as a path exists (choose-waypoints preview does * not go through fetchTrajectoryCoords). Content fills in via async refresh. */ function revealTrajectoryZPanelChrome() { if (trajZPanel) trajZPanel.hidden = false; } /** * Refresh latent z for the live path without rewriting geometry. * PC catalog paths use marker.z (not nearest-particle plot rows). */ function refreshTrajectoryZPanel() { if (!trajZPanel || !trajZPre) return; var catalogPayload = buildCatalogTrajectoryZPayload(); if (catalogPayload) { zPanelRequestId++; revealTrajectoryZPanelChrome(); applyTrajectoryZPanelFromPayload(catalogPayload); return; } var rows = (trajPlotRows && trajPlotRows.length >= 2) ? trajPlotRows : ((anchorIndicesActive && anchorIndicesActive.length >= 2) ? anchorIndicesActive : null); var pathXY = null; if (typeof activeTrajectoryPathXY === "function") { pathXY = activeTrajectoryPathXY(); } if ((!pathXY || pathXY.length < 2) && anchorTrajXY && anchorTrajXY.length >= 2) { pathXY = anchorTrajXY; } if ((!pathXY || pathXY.length < 2) && editableTrajXY && editableTrajXY.length >= 2) { pathXY = editableTrajXY; } var myId = ++zPanelRequestId; var body = null; if (rows && rows.length >= 2) { revealTrajectoryZPanelChrome(); body = Object.assign({ anchor_indices: rows, mode: "direct", n_points: 0, x: sx.value, y: sy.value, anchor_path_order: "preserve" }, trajColorPayloadFields()); if (pathXY && pathXY.length >= 2) { body.traj_xy = pathXY; } } else if (startXY && endXY) { revealTrajectoryZPanelChrome(); body = Object.assign({ mode: trajectoryMode === "nearest" ? "nearest" : "direct", start: startXY, end: endXY, x: sx.value, y: sy.value, n_points: Math.max( 2, currentNPoints(), (pathXY && pathXY.length) || 0 ) }, trajColorPayloadFields()); if (pathXY && pathXY.length >= 2) { body.traj_xy = pathXY; } } else { clearTrajectoryZPanel(); return; } fetch("{{ url_for('api_trajectory_coords') }}", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }) .then(function(r) { return r.json().then(function(j) { return { ok: r.ok, j: j }; }); }) .then(function(res) { if (myId !== zPanelRequestId) return; if (!res.ok || !res.j) return; applyTrajectoryZPanelFromPayload(res.j); }) .catch(function() { if (myId !== zPanelRequestId) return; }); } function clearVolumeMontage() { volumeGeneration++; stopDecodeProgressPoll(); coordsRequestId++; if (trajVolColumn) trajVolColumn.innerHTML = ""; setGeneratedVolumesState("", 0); editableTrajXY = null; clearTrajectoryZPanel(); if (gd && gd.data && gd.data.length > 3) { try { Plotly.deleteTraces(gd, [3]); } catch (e) {} } } /** Drop numbered trajectory markers without touching thumbnails or in-flight decode. */ function removeTrajectorySampleMarkersOnly() { if (gd && gd.data && gd.data.length > 3) { try { Plotly.deleteTraces(gd, [3]); } catch (e) {} } } var dragRestyleRaf = 0; var editableDragRaf = 0; function cancelDragRestyleRaf() { if (dragRestyleRaf) { cancelAnimationFrame(dragRestyleRaf); dragRestyleRaf = 0; } } /** Update line + endpoint traces (indices 1 and 2) without touching sample markers. */ function restyleTrajectoryLineOnly() { if (!gd || !gd.data || gd.data.length < 2) return; try { var xs; var ys; if (editableTrajXY && editableTrajXY.length >= 2) { xs = [editableTrajXY.map(function(p) { return p[0]; })]; ys = [editableTrajXY.map(function(p) { return p[1]; })]; } else if (startXY && endXY) { xs = [[startXY[0], endXY[0]]]; ys = [[startXY[1], endXY[1]]]; } else { return; } Plotly.restyle(gd, { x: xs, y: ys }, [1]); syncTrajGlyphOverlay(); } catch (e) { console.error(e); } } function scheduleTrajectoryRestyle() { if (dragRestyleRaf) return; dragRestyleRaf = requestAnimationFrame(function() { dragRestyleRaf = 0; restyleTrajectoryLineOnly(); }); } function cancelEditableDragRaf() { if (editableDragRaf) { cancelAnimationFrame(editableDragRaf); editableDragRaf = 0; } } function restyleEditableTrajectoryFromState() { if (!gd || !gd.data || !editableTrajXY || editableTrajXY.length < 2) return; redrawTrajectoryOverlay(); } function scheduleEditableTrajectoryRestyle() { if (editableDragRaf) return; editableDragRaf = requestAnimationFrame(function() { editableDragRaf = 0; restyleTrajectoryLineOnly(); }); } function fmtCoord(v) { if (v == null) return "?"; var a = Math.abs(v); if (a === 0) return "0"; if (a >= 100) return v.toFixed(0); if (a >= 1) return v.toFixed(1); return v.toPrecision(3); } function fmtXY(xy) { if (!xy) return "(?, ?)"; return "(" + fmtCoord(xy[0]) + ", " + fmtCoord(xy[1]) + ")"; } function formatZPathDisplayFromZTraj(zTraj) { if (!zTraj || !zTraj.length) return ""; var nCols = Array.isArray(zTraj[0]) ? zTraj[0].length : 0; var zHead = []; for (var j = 0; j < nCols; j++) zHead.push("z" + j); var body = zTraj.map(function(row) { return row.map(function(v) { var x = Number(v); if (!Number.isFinite(x)) return String(v); return x.toFixed(3); }).join(" "); }).join("\n"); return (zHead.length ? zHead.join(" ") + "\n" : "") + body; } function formatZPathGutterFromPayload(j) { if (!j) return ""; var traj_xy = (j.traj_xy && j.traj_xy.length) ? j.traj_xy : []; var pidx = (j.traj_particle_indices && j.traj_particle_indices.length) ? j.traj_particle_indices : []; var labels = (j.traj_point_labels && j.traj_point_labels.length) ? j.traj_point_labels : []; var n = 0; if (j.z_traj && j.z_traj.length) n = j.z_traj.length; else if (traj_xy.length) n = traj_xy.length; if (!n) return ""; var nDigits = String(n).length; var anyParticle = false; var anyLabel = false; for (var pi = 0; pi < n; pi++) { if (pidx[pi] != null && pidx[pi] !== "") anyParticle = true; if (labels[pi] != null && String(labels[pi]) !== "") anyLabel = true; } // PC / latent samples are not dataset particles — prefer volume labels. var idHeader = "id"; if (anyLabel && !anyParticle) idHeader = "volume"; else if (anyParticle && !anyLabel) idHeader = "particle"; else if (anyParticle && anyLabel) idHeader = "id"; var lines = []; var xLabel = j.xcol || "x"; var yLabel = j.ycol || "y"; lines.push("pt " + idHeader + " (" + xLabel + ", " + yLabel + ")"); for (var i = 0; i < n; i++) { var idxTxt = String(i + 1); while (idxTxt.length < nDigits) idxTxt = " " + idxTxt; var idTxt = "—"; if (labels[i] != null && String(labels[i]) !== "") { idTxt = String(labels[i]); } else if (pidx[i] != null && pidx[i] !== "") { idTxt = "#" + String(pidx[i]); } var xy = traj_xy[i]; var xyTxt = "(?, ?)"; if (xy && xy.length >= 2) { xyTxt = "(" + fmtCoord(xy[0]) + ", " + fmtCoord(xy[1]) + ")"; } lines.push(idxTxt + " " + idTxt + " " + xyTxt); } return lines.join("\n"); } function axisDisplayName(col) { return covariateDisplayMap[col] || col; } function applyVolumeCellAnnotations(entries, j) { var mode = (j && j.mode) || trajectoryMode; var traj_xy = (j && j.traj_xy) || []; var xcol = (j && j.xcol) || sx.value; var ycol = (j && j.ycol) || sy.value; var xdn = axisDisplayName(xcol); var ydn = axisDisplayName(ycol); for (var i = 0; i < entries.length; i++) { var ent = entries[i]; var xy = traj_xy[i]; ent.inset.classList.remove("cryo-traj-vol-inset--show"); ent.insetImg.src = ""; if (!xy || xy.length < 2) { ent.meta.textContent = ""; continue; } if ((mode === "nearest" || mode === "graph") && j.traj_particle_indices && j.traj_particle_indices[i] != null) { var pidx = j.traj_particle_indices[i]; var row = j.traj_rows && j.traj_rows[i] != null ? j.traj_rows[i] : ""; ent.meta.textContent = "#" + pidx + (row !== "" ? " · row " + row : "") + "\n" + xdn + ": " + fmtCoord(xy[0]) + "\n" + ydn + ": " + fmtCoord(xy[1]); var thumb = j.particle_thumbs && j.particle_thumbs[i]; if (thumb) { ent.insetImg.src = "data:image/jpeg;base64," + thumb; ent.inset.classList.add("cryo-traj-vol-inset--show"); } } else { ent.meta.textContent = xdn + ": " + fmtCoord(xy[0]) + "\n" + ydn + ": " + fmtCoord(xy[1]); } } } function syncScatterControlsAlignment() { if (!scatterControlsCard) return; /* Scrollable left menu: pin controls to the top — vertical centring shifts marginTop when colour cells grow and makes inner scroll jump. */ scatterControlsCard.style.marginTop = "0px"; } if (plotlyStackEl && window.CryoResizeObserverUtils) { window.CryoResizeObserverUtils.observeResize([plotlyStackEl], function() { window.requestAnimationFrame(function() { syncScatterControlsAlignment(); applyTrajScatterLegendPosition(); if (!gd || !gd.data || !gd.data.length) return; try { Plotly.Plots.resize(gd); } catch (e) {} }); }); } /* ── Convert a DOM click on the plot to data coordinates ── */ function plotClickToData(evt, clamp) { var dragLayer = gd.querySelector(".nsewdrag"); if (!dragLayer) return null; var rect = dragLayer.getBoundingClientRect(); var fracX = (evt.clientX - rect.left) / rect.width; var fracY = (evt.clientY - rect.top) / rect.height; if (clamp) { fracX = Math.max(0, Math.min(1, fracX)); fracY = Math.max(0, Math.min(1, fracY)); } else if (fracX < 0 || fracX > 1 || fracY < 0 || fracY > 1) { return null; } var fl = gd._fullLayout; if (!fl || !fl.xaxis || !fl.yaxis) return null; var xr = fl.xaxis.range; var yr = fl.yaxis.range; var dataX = xr[0] + fracX * (xr[1] - xr[0]); var dataY = yr[1] - fracY * (yr[1] - yr[0]); return [dataX, dataY]; } function dataToPixelXY(dataX, dataY) { var pt = dataToPlotStackXY(dataX, dataY); if (!pt || !trajGlyphOverlayEl) return null; var stackRect = trajGlyphOverlayEl.parentElement.getBoundingClientRect(); return [stackRect.left + pt[0], stackRect.top + pt[1]]; } function dataToPlotStackXY(dataX, dataY) { var fl = gd._fullLayout; if (!fl || !fl.xaxis || !fl.yaxis || !trajGlyphOverlayEl) return null; var plotArea = gd.querySelector(".nsewdrag"); if (!plotArea) return null; var plotRect = plotArea.getBoundingClientRect(); var stackRect = trajGlyphOverlayEl.parentElement.getBoundingClientRect(); var xr = (trajGlyphPanAxisRanges && trajGlyphPanAxisRanges.xr) || fl.xaxis.range; var yr = (trajGlyphPanAxisRanges && trajGlyphPanAxisRanges.yr) || fl.yaxis.range; if (!xr || xr.length < 2 || !yr || yr.length < 2) return null; if (xr[1] === xr[0] || yr[1] === yr[0]) return null; var fracX = (dataX - xr[0]) / (xr[1] - xr[0]); var fracY = (yr[1] - dataY) / (yr[1] - yr[0]); return [ plotRect.left - stackRect.left + fracX * plotRect.width, plotRect.top - stackRect.top + fracY * plotRect.height ]; } function pixelDist(ax, ay, bx, by) { var ddx = ax - bx, ddy = ay - by; return Math.sqrt(ddx * ddx + ddy * ddy); } var DRAG_HANDLE_PX = 18; function restyleLineAndHandles() { if (!gd || !gd.data || gd.data.length < 3) return; try { if (gd.data.length > 3) Plotly.deleteTraces(gd, [3]); } catch (e) {} restyleTrajectoryLineOnly(); } function setTrajectoryModesLockedForAnchors(on) { // Keep signature for call sites; mode buttons now remain active with anchors loaded. syncTraversalModeButtonStates(); } function directTraversalAllowedForAxes(xc, yc) { function zCol(c) { return /^z[0-9]+$/.test(c); } function pcCol(c) { return /^PC[0-9]+$/.test(c); } return (zCol(xc) || pcCol(xc)) && (zCol(yc) || pcCol(yc)); } function syncTraversalModeButtonStates() { syncManualModeChrome(); if (isManualTraversalMode()) { if (trajModeManualRadio) trajModeManualRadio.checked = true; if (trajModeDirectRadio) trajModeDirectRadio.checked = false; syncTraversalSettingsText(); return; } if (hasAnchorIndices()) { trajectoryMode = "direct"; if (trajModeDirectRadio) { trajModeDirectRadio.disabled = false; trajModeDirectRadio.checked = true; } if (trajModeManualRadio) trajModeManualRadio.checked = false; syncTraversalSettingsText(); return; } // Direct-trace mode itself stays available for any plot axes. Only the // "direct interpolation in space" sub-mode requires PC*/z* axes; otherwise // force snap-to-nearest immediately and lock the interp radios. var allowDirect = directTraversalAllowedForAxes(sx.value, sy.value); var forcedNearest = false; if (!allowDirect && trajectoryMode === "direct") { trajectoryMode = "nearest"; lastScatterTraversalMode = "nearest"; forcedNearest = true; } if (trajModeDirectRadio) { trajModeDirectRadio.disabled = false; trajModeDirectRadio.checked = true; } if (trajModeManualRadio) trajModeManualRadio.checked = false; // Sync interp chrome after any forced nearest switch so radios match state. syncTraversalSettingsText(); if (forcedNearest && startXY && endXY) { fetchTrajectoryCoords(); } } function syncAnchorIndicesUI() { syncManualModeChrome(); syncAnchorPathOrderUI(); if (isManualTraversalMode()) { if (btnAnchorRemove) { btnAnchorRemove.classList.remove("cryo-traj-remove--visible"); btnAnchorRemove.setAttribute("aria-hidden", "true"); } return; } var on = typeof shouldShowRemoveIndicesButton === "function" ? shouldShowRemoveIndicesButton() : !!(anchorIndicesActive && anchorIndicesActive.length >= 2); if (on) { closeFileBrowser(); } if (btnAnchorRemove) { btnAnchorRemove.classList.toggle("cryo-traj-remove--visible", !!on); btnAnchorRemove.setAttribute("aria-hidden", on ? "false" : "true"); } } function clearAnchorMode() { var session = typeof activeTrajectorySession === "function" ? activeTrajectorySession() : null; if (session && typeof session.clear === "function") { session.clear(); } clearInteractiveAnchorState(); clearManualAnalyzeVolumeDisplay(); manualSelectedVolIds = []; manualActiveCustomPlotRows = []; // Reset trajectory points returns to the default PC1 path — drop Other / // random registry so visit-order leftovers cannot rematerialize. manualCustomPlotRows = []; manualCustomPlotRowsDisplayOrder = []; manualBootstrapDone = false; manualBootstrapPending = false; buildManualPickerRows(); updateGenerateVolumesButtonLabel(); closeFileBrowser(); } function onTrajectoryIndicesLoaded(res, loadOpts) { loadOpts = loadOpts || {}; var preserveDirectLineXY = !!loadOpts.preserveDirectLineXY || (typeof manualDirectLineDensifyActive === "function" && manualDirectLineDensifyActive()); var preservedDirectXY = preserveDirectLineXY && typeof snapshotManualDirectLinePathXY === "function" ? snapshotManualDirectLinePathXY() : null; if (isManualTraversalMode() && (manualVolumeSnapActive || manualParticleSnapPathActive)) { manualVolumeSnapActive = true; graphTraversalActive = false; // Keep particle-snap flag as-is (direct-line leaves it false). } if (!preserveDirectLineXY) { res = ensureManualParticleSnapTrajectory(res); } if (isManualTraversalMode()) { syncManualAnchorIndicesToVolIdOrder(); } else if (res.anchor_indices && res.anchor_indices.length >= 2) { anchorIndicesActive = res.anchor_indices.slice(); } if (preservedDirectXY) { restoreManualDirectLinePathXY(preservedDirectXY); } else { anchorTrajXY = (res.traj_xy && res.traj_xy.length) ? res.traj_xy.map(function(p) { return [Number(p[0]), Number(p[1])]; }) : null; if (isManualTraversalMode() && res.traj_rows && res.traj_rows.length >= 2) { trajPlotRows = res.traj_rows.map(function(r) { return Number(r); }); if (res.traj_xy && res.traj_xy.length === trajPlotRows.length) { anchorTrajXY = res.traj_xy.map(function(p) { return [Number(p[0]), Number(p[1])]; }); } else { var rebuilt = trajXYFromPlotRows(trajPlotRows); if (rebuilt && rebuilt.length === trajPlotRows.length) { anchorTrajXY = rebuilt; } } } } manualInterpTrajectoryCount = anchorTrajXY ? anchorTrajXY.length : 0; lastLatentTrajectoryPointCount = manualInterpTrajectoryCount; editableTrajXY = null; // Densify→Other promotion owns the full waypoint path. applyCoordsPayload // must not collapse anchors back to the pre-densify server list, and // restoreManualDirectLinePathXY must not re-mark the path as densified. var densifyPromotedToOther = false; if (isManualTraversalMode()) { refreshManualCustomPlotRowsDisplayOrderFromTrajectory(); // Promote densify/snap samples to Other waypoints before any sparse // catalog volume chrome can invent densified slot layouts. if (manualInterpolationArmed() || (anchorTrajXY && manualSnapAnchorRowsInOrder().length >= 2 && anchorTrajXY.length > manualSnapAnchorRowsInOrder().length) || (trajPlotRows && manualSnapAnchorRowsInOrder().length >= 2 && trajPlotRows.length > manualSnapAnchorRowsInOrder().length)) { densifyPromotedToOther = !!promoteDensifiedPathAsOtherWaypoints( trajPlotRows && trajPlotRows.length >= 2 ? trajPlotRows.slice() : null, anchorTrajXY && anchorTrajXY.length >= 2 ? anchorTrajXY.map(function(p) { return p.slice(); }) : null ); } } if (res && (res.mode === "direct" || res.mode === "graph")) { if (isManualTraversalMode()) { if (manualParticleSnapPathActive) { graphTraversalActive = false; manualVolumeSnapActive = true; } else if (manualVolumeSnapActive) { graphTraversalActive = false; } else { graphTraversalActive = res.mode === "graph"; } syncTraversalAltButtons(); } else if (!isManualTraversalMode()) { trajectoryMode = res.mode === "graph" ? "direct" : res.mode; } } if (nPointsEl) nPointsEl.disabled = false; setTrajectoryModesLockedForAnchors(true); syncPointsSelectorRange(); if (!shouldPreserveAnalyzeVolumeDisplayOnCoordsRefresh() && !isManualTraversalMode()) { setGeneratedVolumesState("", 0); } updateGenerateVolumesButtonLabel(); syncAnchorIndicesUI(); applyTrajectoryCoordsPayload(res, { preserveDirectLineXY: preserveDirectLineXY, preserveWaypointGeometry: densifyPromotedToOther }); if (!densifyPromotedToOther && preservedDirectXY && preservedDirectXY.length >= 2) { // Belt-and-suspenders: applyCoordsPayload may still have rewritten geometry. restoreManualDirectLinePathXY(preservedDirectXY); } if (isManualTraversalMode()) { syncManualInterpolatedScatterVisuals({ immediate: true }); scheduleManualScatterTrajectorySync(); if (manualInterpolatedCatalogActive() && typeof syncSessionVolumesToPath === "function") { syncSessionVolumesToPath({ seedCatalog: true }); } } else { redrawTrajectoryOverlay(); scheduleTrajGlyphOverlaySync(); scheduleTrajectoryOverlayRefresh(); } if (res.kmeans_volume_ids && res.kmeans_volume_ids.length >= 2) { anchorKmeansVolumeIds = res.kmeans_volume_ids.slice(); } else if (!isManualTraversalMode()) { anchorKmeansVolumeIds = manualSelectedVolIds.length >= 2 ? manualSelectedVolIds.slice() : []; } if (isManualTraversalMode()) { if (res.analyze_volume_ids && res.analyze_volume_ids.length >= 2 && !manualParticleSnapPathActive && !manualVolumeSnapActive) { manualTrajectoryVolumeIds = res.analyze_volume_ids.slice(); } else if (!isGraphTraversalActive() && currentNPoints() === 0 && !manualParticleSnapPathActive && !manualVolumeSnapActive) { manualTrajectoryVolumeIds = manualSelectedVolIds.slice(); } else { manualTrajectoryVolumeIds = []; } var volumesReorderedOnly = finishAnchorPathOrderVolumePreserveIfPending(); if (!volumesReorderedOnly) { if (pendingDecodedVolumeRemap) { var remapSnap = pendingDecodedVolumeRemap; pendingDecodedVolumeRemap = null; remapDecodedVolumesToPlotRows(remapSnap.rows, remapSnap.payload); } else if (manualInterpolatedCatalogActive()) { if (manualInterpolatedVolumeReversePending) { manualInterpolatedVolumeReversePending = false; if (hasGeneratedTrajectoryVolumes() && lastVolumePayload) { applyVolumePayloadDisplay(lastVolumePayload, currentVolumeRenderBackend()); syncTrajVolBackendChrome(); syncDirectModeVolumePendingOverlay(); } else { applyManualInterpolatedVolumeSliderDisplay(); } } else if (!trajectoryVolumeDisplayReorderPending) { syncManualInterpolatedVolumeCatalogForCount(); } } else if (manualAnalyzeVolumeIdsMismatch()) { syncManualVolumeViewerForWaypointSelection(); } else if (densifyPromotedToOther) { // promoteDensifiedPathAsOtherWaypoints already rematched Session slots // onto the expanded waypoint path. Re-applying the compact PC1 catalog // here index-stamps {decoded:true} onto new interiors and under-counts // Decode vs Render (e.g. Decode 4 / render 9 after PC1×10 densify). if (typeof syncManualVolumeScatterHighlight === "function") { syncManualVolumeScatterHighlight(); } if (typeof updateGenerateVolumesButtonLabel === "function") { updateGenerateVolumesButtonLabel(); } } else { refreshPreservedAnalyzeVolumeDisplay(); } } else { pendingDecodedVolumeRemap = null; syncManualVolumeScatterHighlight(); scheduleTrajGlyphOverlaySync(); } if (manualInterpolatedCatalogActive()) { setManualInterpolatedTrajectoryStatus(); } else if (!trajectoryChimeraxJobInFlight()) { setTrajStatus("Manual selection ready.", false); } trajectoryVolumeDisplayReorderPending = false; ingestManualCustomPlotRowsFromTrajectoryResponse(res, { preserveDirectLineXY: preserveDirectLineXY }); if (densifyPromotedToOther && typeof scheduleManualParticleSetPickerRefresh === "function") { scheduleManualParticleSetPickerRefresh(); } snapVolumeFocusToActiveTick(lastDisplayedVolumeFocusIndex); } else if (anchorKmeansVolumeIds.length >= 2 || manualSelectedVolIds.length >= 2) { if (finishAnchorPathOrderVolumePreserveIfPending()) { syncManualVolumeScatterHighlight(); scheduleTrajGlyphOverlaySync(); setTrajStatus("Anchor path and volumes ready.", false); } else if (refreshPreservedAnalyzeVolumeDisplay()) { setTrajStatus("Anchor path and volumes ready.", false); } else if (deferManualWaypointVolumeRerender()) { syncTrajectoryVolumeChrome(); syncTrajVolBackendChrome(); prepareDeferredCatalogVolumeChrome(); updateGenerateVolumesButtonLabel(); } else { syncTrajectoryVolumeChrome(); syncTrajVolBackendChrome(); syncAnalyzeVolumeDisplay(); } } else { finishAnchorPathOrderVolumePreserveIfPending(); onTrajectoryPathCoordsReady(); } syncTrajReverseButton(); } function makeLinearEditablePath(firstPt, lastPt, nPoints) { if (!window.CryoTrajectoryPath || typeof CryoTrajectoryPath.linearXYFromEndpoints !== "function") { return null; } return CryoTrajectoryPath.linearXYFromEndpoints( firstPt, lastPt, Math.max(2, parseInt(nPoints, 10) || DEFAULT_DIRECT_TRAJ_N_POINTS) ); } function currentTrajectoryPathXY(fallbackCount) { if (editableTrajXY && editableTrajXY.length >= 2) { return editableTrajXY.map(function(p) { return [Number(p[0]), Number(p[1])]; }); } if (hasAnchorIndices() && anchorTrajXY && anchorTrajXY.length >= 2) { return anchorTrajXY.map(function(p) { return [Number(p[0]), Number(p[1])]; }); } var endpoints = currentTrajectoryEndpoints(); if (endpoints) { var n = Math.max(2, parseInt(fallbackCount, 10) || currentVolumeCount()); return makeLinearEditablePath(endpoints.start, endpoints.end, n); } return null; } function resampleTrajectoryPath(path, newCount) { if (!path || path.length < 2) return null; newCount = Math.max(2, parseInt(newCount, 10) || DEFAULT_DIRECT_TRAJ_N_POINTS); if (path.length === newCount) { return path.map(function(p) { return [Number(p[0]), Number(p[1])]; }); } var segLens = [0]; var total = 0; for (var i = 1; i < path.length; i++) { var dx = Number(path[i][0]) - Number(path[i - 1][0]); var dy = Number(path[i][1]) - Number(path[i - 1][1]); total += Math.hypot(dx, dy); segLens.push(total); } if (!(total > 0)) { return makeLinearEditablePath(path[0], path[path.length - 1], newCount); } var out = []; for (var j = 0; j < newCount; j++) { var target = (newCount <= 1) ? 0 : (j / (newCount - 1)) * total; var seg = 0; while (seg < segLens.length - 1 && segLens[seg + 1] < target) seg++; if (seg >= path.length - 1) { out.push([Number(path[path.length - 1][0]), Number(path[path.length - 1][1])]); continue; } var segStart = segLens[seg]; var segEnd = segLens[seg + 1]; var t = segEnd > segStart ? (target - segStart) / (segEnd - segStart) : 0; var x0 = Number(path[seg][0]), y0 = Number(path[seg][1]); var x1 = Number(path[seg + 1][0]), y1 = Number(path[seg + 1][1]); out.push([x0 + t * (x1 - x0), y0 + t * (y1 - y0)]); } return out; } function applyResampledTrajectoryPath(newCount) { var path = currentTrajectoryPathXY(newCount); if (!path || path.length < 2) return false; // Stamp latent XY / path_t onto VolumeState *and* legacy payloads before the // polyline length changes so densify can remap decoded interiors by geometry. if (editableTrajXY && editableTrajXY.length >= 2) { stampTrajectoryVolumeGeometryOntoPath(editableTrajXY); } var resampled = resampleTrajectoryPath(path, newCount); if (!resampled || resampled.length < 2) return false; editableTrajXY = resampled; startXY = resampled[0].slice(); endXY = resampled[resampled.length - 1].slice(); manualInterpTrajectoryCount = resampled.length; lastLatentTrajectoryPointCount = resampled.length; // Stale nearest particle rows must not keep slider ticks at the old count. if (trajectoryMode !== "nearest" || !trajPlotRows || trajPlotRows.length !== resampled.length) { trajPlotRows = null; } // Keep path object's editable polyline in lockstep with page state so // getVolumeSlotIds / sampleCount see the new length immediately. var trajPath = typeof activeTrajectoryPath === "function" ? activeTrajectoryPath() : null; if (trajPath && typeof trajPath.setEditableXY === "function") { trajPath.setEditableXY(resampled); } if (trajPath && typeof trajPath.setInterpolatedCount === "function") { trajPath.setInterpolatedCount(resampled.length); } return true; } function currentTrajectoryEndpoints() { if (editableTrajXY && editableTrajXY.length >= 2) { return { start: editableTrajXY[0].slice(), end: editableTrajXY[editableTrajXY.length - 1].slice() }; } if (startXY && endXY) { return { start: startXY.slice(), end: endXY.slice() }; } return null; } function resetDirectTraversalFromCurrentEndpoints() { var endpoints = currentTrajectoryEndpoints(); if (!endpoints) return false; var path = activeTrajectoryPath(); if (path && typeof path.setEndpoints === "function" && typeof path.buildLinearPath === "function") { path.setEndpoints(endpoints.start, endpoints.end); return !!path.buildLinearPath(currentNPoints()); } var linearPath = makeLinearEditablePath( endpoints.start, endpoints.end, currentNPoints() ); if (!linearPath || linearPath.length < 2) return false; startXY = linearPath[0].slice(); endXY = linearPath[linearPath.length - 1].slice(); editableTrajXY = linearPath; return true; } function preservedManualPathPointCount() { if (anchorTrajXY && anchorTrajXY.length >= 2) return anchorTrajXY.length; if (trajPlotRows && trajPlotRows.length >= 2) return trajPlotRows.length; if (manualInterpTrajectoryCount >= 2) return manualInterpTrajectoryCount; var vc = currentVolumeCount(); return vc >= 2 ? vc : 0; } function applyPreservedScatterDirectPath(endpoints, pathPointCount, endpointVolumeIds) { if (!endpoints || !endpoints.start || !endpoints.end) return false; var n = Math.max(2, parseInt(pathPointCount, 10) || 0); if (nPointsEl) { nPointsEl.value = String(n); syncPointsSelectorRange(); } startXY = endpoints.start.slice(); endXY = endpoints.end.slice(); if (!resetDirectTraversalFromCurrentEndpoints()) return false; if (endpointVolumeIds && endpointVolumeIds.length >= 2) { directEndpointVolumeIds = endpointVolumeIds.slice(); } return true; } /* ── Mode toggle (also refreshes latent z via coords API) ── */ function setMode(mode) { var prevMode = trajectoryMode; if (mode === "manual") { enterManualMode(prevMode); return; } if (mode !== "direct") return; if (isManualTraversalMode()) { // Direct-trace opens on direct interpolation when both axes are PC*/z*; // otherwise snap-to-nearest is mandatory (interpolation disabled). var enterMode = directTraversalAllowedForAxes(sx.value, sy.value) ? "direct" : "nearest"; leaveManualForScatterMode(enterMode); return; } if (trajModeDirectRadio) trajModeDirectRadio.checked = true; if (trajModeManualRadio) trajModeManualRadio.checked = false; syncTraversalAltButtons(); } document.querySelectorAll('input[name="traj-mode"]').forEach(function(radio) { radio.addEventListener("change", function() { if (!radio.checked) return; setMode(radio.value); }); }); document.querySelectorAll('input[name="traj-scatter-interp"]').forEach(function(radio) { radio.addEventListener("change", function() { if (!radio.checked) return; setScatterInterpolationMode(radio.value); }); }); if (btnTrajManualVolume) { btnTrajManualVolume.addEventListener("click", function() { if (typeof trajectoryAddPointsBusy === "function" && trajectoryAddPointsBusy()) return; setManualInterpolationMode("volume"); }); } if (btnTrajManualGraph) { btnTrajManualGraph.addEventListener("click", function() { if (typeof trajectoryAddPointsBusy === "function" && trajectoryAddPointsBusy()) return; setManualInterpolationMode("graph"); }); } if (btnGenerateVolumesEl) { btnGenerateVolumesEl.addEventListener("click", function() { reconcileChimeraxRenderingFlag(); if (trajectoryVolumeActionBusy()) return; if (!decodeRenderVolumesActionEnabled()) return; requestDecodeAndRenderVolumes(); }); } if (btnSaveVolumesEl) { btnSaveVolumesEl.addEventListener("click", function() { if (btnSaveVolumesEl.disabled) return; saveTrajectoryVolumes(); }); } if (btnSaveZpathAs) { btnSaveZpathAs.addEventListener("click", function() { if (!lastZPathTxt) return; if (fbSaveName) fbSaveName.value = "z-path.txt"; openFileBrowser("", "save-zpath"); }); } if (btnSaveZpathWorkdir) { btnSaveZpathWorkdir.addEventListener("click", function() { if (!lastZPathTxt) return; fetch("{{ url_for('api_trajectory_save_zpath') }}", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ z_path_txt: lastZPathTxt }) }) .then(function(r) { return r.json().then(function(j) { return { ok: r.ok, j: j }; }); }) .then(function(res) { if (!res.ok) { setTrajStatus(res.j.error || "Could not save z-path.txt.", false); return; } var p = res.j.path || (expWorkdir ? expWorkdir + "/z-path.txt" : "z-path.txt"); setTrajStatus("Saved " + p, false); }) .catch(function(err) { console.error(err); setTrajStatus("Save failed.", false); }); }); } function postTrajectoryIndices(url, statusMsg, errPrefix) { closeFileBrowser(); resetAnchorPathOrderToDefault(); setTrajStatus(statusMsg, false); if (isManualTraversalMode()) manualCustomPlotRowsRegisterPending = true; var anchorMode = currentAnchorTraversalMode(); var nPointsReq = requestedAnchorNPoints(anchorMode); fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(Object.assign({ x: sx.value, y: sy.value, mode: anchorMode, n_points: nPointsReq, max_neighbors: currentMaxNeighbors(), avg_neighbors: currentAvgNeighbors() }, trajAnchorPathOrderFields())) }) .then(function(r) { return r.json().then(function(j) { return { ok: r.ok, j: j }; }); }) .then(function(res) { if (!res.ok) { manualCustomPlotRowsRegisterPending = false; setTrajStatus(res.j.error || (errPrefix + " failed."), false); return; } onTrajectoryIndicesLoaded(res.j); }) .catch(function(err) { manualCustomPlotRowsRegisterPending = false; console.error(err); setTrajStatus(errPrefix + " request failed.", false); }); } function existingTrajectoryPlotRowsForAppend() { var seen = {}; var out = []; function pushRows(rows) { (rows || []).forEach(function(row) { row = Number(row); if (!Number.isFinite(row) || row < 0 || seen[row]) return; seen[row] = true; out.push(row); }); } if (isManualTraversalMode() || directTraceTraversalMode()) { if (typeof manualCatalogSelectedPlotRows === "function") { pushRows(manualCatalogSelectedPlotRows()); } pushRows(manualActiveCustomPlotRows); } if (typeof directTraceTraversalMode === "function" && directTraceTraversalMode()) { pushRows(directTraceLivePlotRowsForAppend()); } pushRows(anchorIndicesActive); if (trajPlotRows && trajPlotRows.length) { pushRows(trajPlotRows); } return out; } function directTraceLivePlotRowsForAppend() { if (typeof directTraceTraversalMode !== "function" || !directTraceTraversalMode()) { return []; } var path = typeof activeTrajectoryPath === "function" ? activeTrajectoryPath() : null; if (!path || path.kind !== "direct" || typeof path._livePathRowsAndXY !== "function") { return []; } var live = path._livePathRowsAndXY(); if (live && live.rows && live.rows.length >= 2) return live.rows.slice(); return []; } function finishRandomPlotRowAppendUi(opts) { opts = opts || {}; if (typeof expandManualVolumeDisplayAfterWaypointAppend === "function") { expandManualVolumeDisplayAfterWaypointAppend({ priorSnapshot: opts.priorSnapshot || null }); } else if (opts.session && typeof opts.session.syncVolumeCache === "function") { opts.session.syncVolumeCache(); } syncTraversalAltButtons(); syncAnchorPathOrderUI(); if (typeof syncTrajReverseButton === "function") syncTrajReverseButton(); if (typeof updateGenerateVolumesButtonLabel === "function") { updateGenerateVolumesButtonLabel(); } redrawTrajectoryOverlay(); if (typeof scheduleTrajGlyphOverlaySync === "function") { scheduleTrajGlyphOverlaySync(); } // Session append registers Other with skipPickerRebuild; refresh here for // choose-waypoints, direct-trace, and snap-to-nearest densify paths. if (typeof scheduleManualParticleSetPickerRefresh === "function") { scheduleManualParticleSetPickerRefresh(); } else if (typeof buildManualPickerRows === "function" && (isManualTraversalMode() || (typeof directTraceTraversalMode === "function" && directTraceTraversalMode()) || (manualCustomPlotRows && manualCustomPlotRows.length > 0))) { requestAnimationFrame(function() { buildManualPickerRows(); }); } } function appendRandomPlotRowsToTrajectory(addedRows) { addedRows = (addedRows || []).map(Number).filter(function(row) { return Number.isFinite(row) && row >= 0; }); if (!addedRows.length) return false; if (typeof ensureExistingWaypointsTrackedBeforeRandomAppend === "function") { ensureExistingWaypointsTrackedBeforeRandomAppend(); } resetAnchorPathOrderToDefault(); var selectionDriven = isManualTraversalMode() || directTraceTraversalMode(); var preAppendVolumeSnapshot = null; if (selectionDriven && typeof expandManualVolumeDisplayAfterWaypointAppend === "function") { var snapImgs = []; if (volumePayload() && Array.isArray(volumeImages())) { snapImgs = volumeImages().slice(); } else if (trajVolDisplay && Array.isArray(trajVolDisplay.chimeraxImages)) { snapImgs = trajVolDisplay.chimeraxImages.slice(); } preAppendVolumeSnapshot = { ids: (volumeDisplayIds() || []).slice(), vols: (volumeSlots() || []).slice(), imgs: snapImgs }; } if (selectionDriven) { var session = typeof activeTrajectorySession === "function" ? activeTrajectorySession() : null; if (!session && typeof ensureTrajectorySession === "function") { session = ensureTrajectorySession(); } if (session) { // Silent append: mutation already rebuilds waypoints + aligns slots. // Seed Session from the compact catalog first so alignToIds rematches // PC volumes and only Other slots stay undecoded (Decode 10, not 20). if (isManualTraversalMode() && typeof resetManualInterpolationArmed === "function") { resetManualInterpolationArmed(); } var appended = session.appendPlotRows(addedRows, { silent: true }); if (!appended || !appended.ok) return false; // Safety net: if rebuild still left geometry empty, repair from overrides // then fall back to a server coords refresh. if ((!anchorTrajXY || anchorTrajXY.length < 2) && anchorIndicesActive && anchorIndicesActive.length >= 2) { var repaired = typeof manualPathXYForOrderedAnchorRows === "function" ? manualPathXYForOrderedAnchorRows(anchorIndicesActive) : (typeof trajXYFromPlotRows === "function" ? trajXYFromPlotRows(anchorIndicesActive) : null); if (repaired && repaired.length === anchorIndicesActive.length) { anchorTrajXY = repaired; } } finishRandomPlotRowAppendUi({ priorSnapshot: preAppendVolumeSnapshot, session: session }); if ((!anchorTrajXY || anchorTrajXY.length < 2) && typeof fetchTrajectoryCoords === "function") { fetchTrajectoryCoords({ preserveGeneratedVolumes: true }); } return true; } if (directTraceTraversalMode() && typeof mergeManualCustomPlotRows === "function") { var directPath = typeof activeTrajectoryPath === "function" ? activeTrajectoryPath() : null; if (directPath && typeof directPath.appendParticleRows === "function") { mergeManualCustomPlotRows(addedRows, { activate: true, activateNewOnly: true, skipPickerRebuild: true }); var directResult = directPath.appendParticleRows(addedRows); if (directResult && directResult.ok) { finishRandomPlotRowAppendUi({ priorSnapshot: preAppendVolumeSnapshot }); return true; } if (directResult && directResult.useRebuild && typeof directPath.rebuildFromSelection === "function") { directPath.rebuildFromSelection({ force: true }); finishRandomPlotRowAppendUi({ priorSnapshot: preAppendVolumeSnapshot }); return true; } } } } if (directTraceTraversalMode()) { return false; } var existing = existingTrajectoryPlotRowsForAppend(); var combined = existing.slice(); var seen = {}; for (var ei = 0; ei < combined.length; ei++) seen[combined[ei]] = true; for (var ai = 0; ai < addedRows.length; ai++) { if (seen[addedRows[ai]]) continue; seen[addedRows[ai]] = true; combined.push(addedRows[ai]); } if (combined.length < 2) { setTrajStatus("Need at least two indices for a trajectory.", false); return false; } anchorIndicesActive = combined.slice(); anchorTrajXY = null; editableTrajXY = null; trajPlotRows = null; if (nPointsEl) nPointsEl.disabled = false; setTrajectoryModesLockedForAnchors(true); updateGenerateVolumesButtonLabel(); syncAnchorIndicesUI(); fetchTrajectoryCoords(); return true; } function addRandomIndicesToTrajectory() { if (typeof trajectoryAddPointsBusy === "function" && trajectoryAddPointsBusy()) return; closeFileBrowser(); var existing = existingTrajectoryPlotRowsForAppend(); setTrajStatus("Sampling random indices\u2026", false); var body = { count: 10, exclude_indices: existing }; if (sx && sx.value) body.x = sx.value; if (sy && sy.value) body.y = sy.value; fetch("{{ url_for('api_trajectory_random_indices') }}", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }) .then(function(r) { return r.json().then(function(j) { return { ok: r.ok, j: j }; }); }) .then(function(res) { if (!res.ok) { setTrajStatus((res.j && res.j.error) || "Random indices failed.", false); return; } var added = (res.j.indices || res.j.anchor_indices || []).map(Number).filter(function(row) { return Number.isFinite(row) && row >= 0 && existing.indexOf(row) < 0; }); if (!added.length) { setTrajStatus("No unused particle indices available to add.", false); return; } // Cache server XY for indices that may be absent from the scatter subsample // so rebuildFromSelection / path overlays can resolve coordinates. if (res.j.xy && res.j.xy.length === (res.j.indices || res.j.anchor_indices || []).length) { var rawIdx = (res.j.indices || res.j.anchor_indices || []).map(Number); if (typeof rememberPlotRowXYList === "function") { rememberPlotRowXYList(rawIdx, res.j.xy); } } else if (res.j.xy && res.j.xy.length === added.length && typeof rememberPlotRowXYList === "function") { rememberPlotRowXYList(added, res.j.xy); } if (!appendRandomPlotRowsToTrajectory(added)) return; var noun = added.length === 1 ? "index" : "indices"; setTrajStatus("Added " + added.length + " random " + noun + " to the trajectory.", false); }) .catch(function(err) { console.error(err); setTrajStatus("Random indices request failed.", false); }); } function closeTrajAnchorManualModal() { if (!trajAnchorManualModal) return; if (window.CryoDashModal) { CryoDashModal.close(trajAnchorManualModal, { restoreFocus: btnAnchorManualOpen }); } else { trajAnchorManualModal.hidden = true; trajAnchorManualModal.setAttribute("aria-hidden", "true"); document.body.classList.remove("cryo-explorer-save-modal-open"); if (btnAnchorManualOpen) { try { btnAnchorManualOpen.focus(); } catch (eFocus) { /* ignore */ } } } } function openTrajAnchorManualModal() { if (!trajAnchorManualModal) return; if (window.CryoDashModal) { CryoDashModal.open(trajAnchorManualModal, { focusEl: anchorIndicesManualEl }); } else { trajAnchorManualModal.hidden = false; trajAnchorManualModal.setAttribute("aria-hidden", "false"); document.body.classList.add("cryo-explorer-save-modal-open"); if (anchorIndicesManualEl) { requestAnimationFrame(function() { try { anchorIndicesManualEl.focus(); if (typeof anchorIndicesManualEl.select === "function") { anchorIndicesManualEl.select(); } } catch (eFocus) { /* ignore */ } }); } } } function parseManualAnchorIndices(raw) { var txt = String(raw || "").trim(); if (!txt) return []; txt = txt.replace(/[\[\]\(\)]/g, " "); var toks = txt.split(/[\s,;]+/).filter(Boolean); var out = []; for (var i = 0; i < toks.length; i++) { var t = toks[i]; if (!/^-?[0-9]+$/.test(t)) return null; out.push(parseInt(t, 10)); } return out; } function loadManualAnchorIndices() { closeFileBrowser(); var parsed = parseManualAnchorIndices(anchorIndicesManualEl && anchorIndicesManualEl.value); if (!parsed || parsed.length < 2) { setTrajStatus("Enter at least two integer anchor indices (comma or space separated).", false); return; } resetAnchorPathOrderToDefault(); anchorIndicesActive = parsed.slice(); anchorTrajXY = null; if (isManualTraversalMode()) { registerManualCustomPlotRows(parsed.slice(), true); } if (nPointsEl) nPointsEl.disabled = false; if (manualSnapNPointsEl) manualSnapNPointsEl.disabled = false; if (manualGraphNPointsEl) manualGraphNPointsEl.disabled = false; if (manualSnapNPointsEl && currentAnchorTraversalMode() === "direct") { manualSnapNPointsEl.value = String(DEFAULT_MANUAL_TRAJ_N_POINTS); } setTrajectoryModesLockedForAnchors(true); updateGenerateVolumesButtonLabel(); syncAnchorIndicesUI(); closeTrajAnchorManualModal(); fetchTrajectoryCoords(); } function canRecordTrajectoryGif() { if (gifSourceChimeraxImageCount() >= 2) return true; if (hasGeneratedTrajectoryVolumes()) { return generatedTrajectoryVolumeCount >= 2 || currentVolumeCount() >= 2; } return false; } function gifSourceChimeraxImageCount() { var payloads = [ activeVolumePayload(), lastVolumePayload, volumePayload() ]; for (var i = 0; i < payloads.length; i++) { if (!payloads[i]) continue; var n = chimeraxImagesFromPayload(payloads[i]).length; if (n) return n; } return 0; } function canIncludeScatterInTrajectoryGif() { return !!(gd && gd.data && gd.data.length && gd._fullLayout); } function syncTrajGifRecordButton() { if (!btnRecordTrajGif) return; var ok = canRecordTrajectoryGif(); btnRecordTrajGif.disabled = !ok; btnRecordTrajGif.title = ok ? "Assemble volume frames along the trajectory into an animated GIF." : "Select and render at least two trajectory volumes first (e.g. PC1, PC2, …)."; } var trajectoryVolumeDisplayReorderPending = false; function reverseTrajectoryPathPoints(path) { return path.slice().reverse().map(function(p) { return [Number(p[0]), Number(p[1])]; }); } function volumeDisplaySlotCount(payload) { var n = 0; if (payload) { var expected = payload.expected_volume_count != null ? Math.floor(Number(payload.expected_volume_count)) : (payload.expectedVolumeCount != null ? Math.floor(Number(payload.expectedVolumeCount)) : 0); if (Number.isFinite(expected) && expected > 0) n = Math.max(n, expected); if (Array.isArray(payload.images)) n = Math.max(n, payload.images.length); if (Array.isArray(payload.volumes)) n = Math.max(n, payload.volumes.length); } if (trajVolDisplay && trajVolDisplay.expectedVolumeCount != null) { var fromDisplay = Math.floor(Number(trajVolDisplay.expectedVolumeCount)); if (Number.isFinite(fromDisplay) && fromDisplay > 0) n = Math.max(n, fromDisplay); } if (trajVolDisplay && typeof trajVolDisplay._volumeNavCount === "function") { var navCount = Math.floor(Number(trajVolDisplay._volumeNavCount())); if (Number.isFinite(navCount) && navCount > 0) n = Math.max(n, navCount); } var pathN = latentTrajectoryPointCount(); if (Number.isFinite(pathN) && pathN > 0) n = Math.max(n, pathN); var curN = currentVolumeCount(); if (Number.isFinite(curN) && curN > 0) n = Math.max(n, curN); return n; } function reverseTrajectoryVolumeStaleAt(n) { n = Math.max(0, Math.floor(Number(n))); if (!n) return; remapTrajectoryVolumeStaleAt(reversePermutation(n)); } function remapTrajectoryVolumeStaleAt(perm) { if (!perm || perm.length < 2) return; if (directTraceUi && typeof CryoDirectTraceUiState !== "undefined" && CryoDirectTraceUiState.remapIndexFlags) { var remapped = CryoDirectTraceUiState.remapIndexFlags(directTraceUi.staleAt(), perm); replaceTrajVolumeStaleMap(remapped); return; } var n = perm.length; var next = {}; for (var key in trajVolumeStaleAt) { if (!trajVolumeStaleAt[key]) continue; var src = Math.floor(Number(key)); if (!Number.isFinite(src) || src < 0 || src >= n) continue; for (var di = 0; di < n; di++) { if (perm[di] === src) { next[di] = true; break; } } } if (typeof replaceTrajVolumeStaleMap === "function") { replaceTrajVolumeStaleMap(next); } else { trajVolumeStaleAt = next; } } function remapCacheSlotIndices(indices, perm) { if (!Array.isArray(indices) || !indices.length || !perm || !perm.length) return indices; var n = perm.length; return indices.map(function(idx) { idx = Math.floor(Number(idx)); if (!Number.isFinite(idx) || idx < 0 || idx >= n) return idx; for (var di = 0; di < n; di++) { if (perm[di] === idx) return di; } return idx; }); } function permutationFromListReorder(oldList, newList) { if (!oldList || !newList || oldList.length !== newList.length || oldList.length < 2) return null; var n = oldList.length; var key = function(v) { return String(v); }; var oldSorted = oldList.map(key).sort().join("\u0000"); var newSorted = newList.map(key).sort().join("\u0000"); if (oldSorted !== newSorted) return null; var used = {}; var perm = new Array(n); for (var i = 0; i < n; i++) { var want = key(newList[i]); var src = -1; for (var j = 0; j < n; j++) { if (used[j]) continue; if (key(oldList[j]) === want) { src = j; break; } } if (src < 0) return null; used[src] = true; perm[i] = src; } return perm; } function snapshotVolumeDisplayPayload(payload) { if (!payload) return null; return { volumes: payload.volumes ? payload.volumes.slice() : [], images: payload.images ? payload.images.slice() : [], ids: payload.ids ? payload.ids.slice() : [], expectedVolumeCount: payload.expectedVolumeCount != null ? payload.expectedVolumeCount : payload.expected_volume_count }; } function volumeDisplaySlotContentKey(payload, idx) { if (!payload) return ""; if (payload.ids && payload.ids[idx]) return "id:" + String(payload.ids[idx]); if (payload.images && payload.images[idx]) { var img = typeof normalizeChimeraxImageB64 === "function" ? normalizeChimeraxImageB64(payload.images[idx]) : payload.images[idx]; if (img) return "img:" + String(img).slice(0, 48); } if (payload.volumes && payload.volumes[idx] && payload.volumes[idx].volume_b64) { return "vol:" + String(payload.volumes[idx].volume_b64).slice(0, 48); } return ""; } function permutationFromDisplayPayloadReorder(oldPayload, newPayload) { var n = volumeDisplaySlotCount(oldPayload); if (n < 2 || n !== volumeDisplaySlotCount(newPayload)) return null; var perm = permutationFromNonemptyDisplaySlots(oldPayload, newPayload); if (perm) return perm; var oldKeys = new Array(n); var newKeys = new Array(n); for (var i = 0; i < n; i++) { oldKeys[i] = volumeDisplaySlotContentKey(oldPayload, i); newKeys[i] = volumeDisplaySlotContentKey(newPayload, i); } return permutationFromListReorder(oldKeys, newKeys); } function permutationFromNonemptyDisplaySlots(oldPayload, newPayload) { var n = volumeDisplaySlotCount(oldPayload); if (n < 2 || n !== volumeDisplaySlotCount(newPayload)) return null; var oldEntries = []; var newEntries = []; for (var i = 0; i < n; i++) { var oldKey = volumeDisplaySlotContentKey(oldPayload, i); if (oldKey) oldEntries.push({ key: oldKey, idx: i }); var newKey = volumeDisplaySlotContentKey(newPayload, i); if (newKey) newEntries.push({ key: newKey, idx: i }); } if (oldEntries.length < 2 || oldEntries.length !== newEntries.length) return null; oldEntries.sort(function(a, b) { return a.key < b.key ? -1 : (a.key > b.key ? 1 : 0); }); newEntries.sort(function(a, b) { return a.key < b.key ? -1 : (a.key > b.key ? 1 : 0); }); for (var k = 0; k < oldEntries.length; k++) { if (oldEntries[k].key !== newEntries[k].key) return null; } var perm = new Array(n); for (var j = 0; j < n; j++) perm[j] = j; for (var m = 0; m < oldEntries.length; m++) { perm[newEntries[m].idx] = oldEntries[m].idx; } return perm; } function mapFocusIndexForPermutation(perm, prevFocus) { if (!perm || !perm.length) return null; if (!Number.isFinite(Number(prevFocus)) || prevFocus < 0 || prevFocus >= perm.length) return null; for (var fi = 0; fi < perm.length; fi++) { if (perm[fi] === prevFocus) return fi; } return null; } function shouldPreserveVolumesOnPathOrderChange() { if (hasGeneratedTrajectoryVolumes()) return true; if (typeof trajectoryHasDisplayableVolumes === "function" && trajectoryHasDisplayableVolumes()) { return true; } return analyzeVolumeCatalogDisplayReady(); } function shouldPreserveTrajectoryVolumesOnCoordsFetch(opts) { opts = opts || {}; if (opts.preserveGeneratedVolumes) return true; if (opts.anchorPathOrderChange && shouldPreserveVolumesOnPathOrderChange()) return true; return false; } var anchorPathOrderVolumeSnapshot = null; function beginAnchorPathOrderVolumePreserve() { anchorPathOrderVolumeSnapshot = null; if (!shouldPreserveVolumesOnPathOrderChange()) return; trajectoryVolumeDisplayReorderPending = true; anchorPathOrderVolumeSnapshot = { volIds: isManualTraversalMode() ? manualSelectedVolIds.slice() : [], anchorRows: anchorIndicesActive ? anchorIndicesActive.slice() : [], kmeansVolIds: anchorKmeansVolumeIds.slice(), displayPayload: snapshotVolumeDisplayPayload(lastVolumePayload || volumePayload()) }; } function reorderVolumePayloadInMemory(perm) { if (!perm || perm.length < 2) return false; var displayPayload = lastVolumePayload || (typeof volumePayload === "function" ? volumePayload() : null) || volumePayload(); var focusN = volumeDisplaySlotCount(displayPayload); if (focusN < 2 || perm.length !== focusN) return false; if (!hasGeneratedTrajectoryVolumes() && !analyzeVolumeCatalogDisplayReady() && !(typeof trajectoryHasDisplayableVolumes === "function" && trajectoryHasDisplayableVolumes())) { return false; } var prevFocus = currentVolumeFocusIndex(); var vs = typeof volumeState === "function" ? volumeState() : null; if (vs && vs.slotCount() === focusN && typeof vs.permute === "function") { vs.permute(perm); if (typeof syncVolumeCacheFromSession === "function") { syncVolumeCacheFromSession(vs.snapshot()); } if (hasGeneratedTrajectoryVolumes() || lastVolumePayload) { assignLastVolumePayloadFromSession(vs.toPayload()); } displayPayload = lastVolumePayload || vs.toPayload(); } else { if (lastVolumePayload) { lastVolumePayload = mergeVolumePayload(reorderVolumePayloadFields(lastVolumePayload, perm)); displayPayload = lastVolumePayload; } var curPayload = volumePayload(); if (curPayload && typeof commitVolumePayload === "function") { var reordered = reorderVolumePayloadFields(curPayload, perm); if (volumeSlots().length === focusN) { reordered.volumes = applyArrayPermutation( ensureSparseSlotArray(volumeSlots(), focusN), perm ); } if (volumeDisplayIds().length === focusN) { reordered.ids = applyArrayPermutation( ensureSparseSlotArray(volumeDisplayIds(), focusN), perm ); } commitVolumePayload(reordered); if (!displayPayload) displayPayload = volumePayload(); } } if (Array.isArray(lastTrajectoryVolumeCacheSlotIndices) && lastTrajectoryVolumeCacheSlotIndices.length) { lastTrajectoryVolumeCacheSlotIndices = remapCacheSlotIndices( lastTrajectoryVolumeCacheSlotIndices, perm ); } remapTrajectoryVolumeStaleAt(perm); applyVolumeDisplayReorder(perm, displayPayload, prevFocus); return true; } function reorderManualInterpolatedVolumesForAnchorPermutation(snap) { if (!snap || !volumePayload() || !manualInterpolatedCatalogActive()) return false; var oldIds = snap.volIds; var newIds = manualSelectedVolIds; if (oldIds.length < 2 || oldIds.length !== newIds.length) return false; var anchorPerm = permutationFromListReorder(oldIds, newIds); if (!anchorPerm) return false; var nAnchors = oldIds.length; var nPoints = currentNPoints(); var pathLen = volumeDisplaySlotCount(snap.displayPayload || volumePayload()); var snapPayload = snap.displayPayload || volumePayload(); var compact = compactManualAnchorVolumesFromSnapshot(snapPayload, nAnchors, nPoints, pathLen); var newCompactVols = applyArrayPermutation( ensureSparseSlotArray(compact.volumes, nAnchors), anchorPerm ); var newCompactImgs = applyArrayPermutation( ensureSparseSlotArray(compact.images, nAnchors), anchorPerm ); var seeded; if (pathLen > nAnchors) { seeded = seedManualSnappedPathDisplaySlots( newCompactVols, newCompactImgs, nAnchors, nPoints, pathLen ); } else { seeded = seedManualInterpolatedDisplaySlots(newCompactVols, newCompactImgs, nAnchors, nPoints); } var nextCount = seeded.expectedVolumeCount || seeded.volumes.length; if (pathLen >= 2 && nextCount !== pathLen) return false; var targetPayload = { volumes: seeded.volumes, images: seeded.images, ids: seeded.ids && seeded.ids.length === nextCount ? seeded.ids.slice() : buildExpandedManualVolumeDisplayIds(nAnchors, nPoints), expectedVolumeCount: nextCount }; commitVolumePayload(targetPayload, { clearStale: true }); applyManualInterpolatedVolumeSliderDisplay(); return true; } function completeAnchorPathOrderVolumePreserve() { var snap = anchorPathOrderVolumeSnapshot; anchorPathOrderVolumeSnapshot = null; if (!trajectoryVolumeDisplayReorderPending) return false; var skipFullRerender = !!snap; var applied = false; if (isManualTraversalMode() && snap && snap.volIds.length >= 2) { var volPerm = permutationFromListReorder(snap.volIds, manualSelectedVolIds); if (volPerm) { var compactDisplayN = volumeDisplaySlotCount(lastVolumePayload || volumePayload()); if (compactDisplayN === volPerm.length) { applied = reorderVolumePayloadInMemory(volPerm) || applied; } } } if (!applied && snap && snap.kmeansVolIds.length >= 2 && anchorKmeansVolumeIds.length === snap.kmeansVolIds.length) { var kmPerm = permutationFromListReorder(snap.kmeansVolIds, anchorKmeansVolumeIds); if (kmPerm && volumeDisplayIds().length === kmPerm.length) { applied = reorderVolumePayloadInMemory(kmPerm) || applied; } } if (!applied && snap && snap.anchorRows.length >= 2 && anchorIndicesActive && snap.anchorRows.length === anchorIndicesActive.length) { var rowPerm = permutationFromListReorder(snap.anchorRows, anchorIndicesActive); if (rowPerm && rowPerm.length === volumeDisplaySlotCount(lastVolumePayload || volumePayload())) { applied = reorderVolumePayloadInMemory(rowPerm) || applied; } } if (!applied && snap && manualInterpolatedCatalogActive()) { applied = reorderManualInterpolatedVolumesForAnchorPermutation(snap) || applied; } if (!applied && snap && snap.displayPayload) { var payloadPerm = permutationFromDisplayPayloadReorder( snap.displayPayload, lastVolumePayload || volumePayload() ); if (payloadPerm) applied = reorderVolumePayloadInMemory(payloadPerm) || applied; } trajectoryVolumeDisplayReorderPending = false; return skipFullRerender || applied; } function finishAnchorPathOrderVolumePreserveIfPending() { if (!trajectoryVolumeDisplayReorderPending) return false; return completeAnchorPathOrderVolumePreserve(); } function reverseCacheSlotIndices(indices, n) { if (!Array.isArray(indices) || !indices.length) return indices; n = Math.max(0, Math.floor(Number(n))); if (!n) return indices.slice(); return indices.map(function(idx) { idx = Math.floor(Number(idx)); if (!Number.isFinite(idx) || idx < 0 || idx >= n) return idx; return n - 1 - idx; }); } function reorderVolumePayloadFields(payload, perm) { if (!payload || !perm || perm.length < 2) return payload; var n = perm.length; var out = Object.assign({}, payload); if (out.volumes) { out.volumes = applyArrayPermutation(ensureSparseSlotArray(out.volumes, n), perm).map(function(vol, i) { if (!vol || typeof vol !== "object") return vol; return Object.assign({}, vol, { index: i }); }); } if (out.images) { out.images = applyArrayPermutation(ensureSparseSlotArray(out.images, n), perm); } if (out.ids) { out.ids = applyArrayPermutation(ensureSparseSlotArray(out.ids, n), perm); } if (out.slots) { out.slots = applyArrayPermutation(ensureSparseSlotArray(out.slots, n), perm); } if (out.traj_rows) { out.traj_rows = applyArrayPermutation(ensureSparseSlotArray(out.traj_rows, n), perm); } if (out.traj_xy) { out.traj_xy = applyArrayPermutation(ensureSparseSlotArray(out.traj_xy, n), perm); } if (out.traj_marker_colors) { out.traj_marker_colors = applyArrayPermutation( ensureSparseSlotArray(out.traj_marker_colors, n), perm ); } if (out.expected_volume_count != null) out.expected_volume_count = n; if (out.expectedVolumeCount != null) out.expectedVolumeCount = n; return out; } function reverseVolumePayloadFields(payload) { var n = volumeDisplaySlotCount(payload); if (n < 2) return payload; return reorderVolumePayloadFields(payload, reversePermutation(n)); } function volumeCountFromPayload(payload) { if (!payload) return 0; var imgs = chimeraxImagesFromPayload(payload); if (imgs.length) return imgs.length; if (payload.volumes && payload.volumes.length) return payload.volumes.length; return 0; } function applyManualInterpolatedSeededVolumePayload(seeded, compactVols, pathLen) { if (!seeded) return false; pathLen = Math.max(0, Math.floor(Number(pathLen))); var nextCount = seeded.expectedVolumeCount || seeded.volumes.length || pathLen; var nAnchors = manualCatalogAnchorVolIdCount(); var ids = seeded.ids && seeded.ids.length === nextCount ? seeded.ids.slice() : buildExpandedManualVolumeDisplayIds(nAnchors, currentNPoints()); var ready = (compactVols && loadedManualVolumeCount(compactVols) > 0) || seeded.volumes.some(function(v) { return !!v; }) || seeded.images.some(function(img) { return !!img; }); commitVolumePayload({ volumes: seeded.volumes.slice(), ids: ids, images: seeded.images.slice(), expectedVolumeCount: nextCount }, { ready: ready, clearStale: true }); applyManualInterpolatedVolumeSliderDisplay(); syncTrajVolBackendChrome(); syncDirectModeVolumePendingOverlay(); return true; } function compactManualAnchorVolumesFromSnapshot(snapPayload, nAnchors, nPoints, pathLen) { var vols = (snapPayload && snapPayload.volumes) || []; var imgs = (snapPayload && snapPayload.images) || []; nAnchors = Math.max(0, Math.floor(Number(nAnchors))); nPoints = Math.max(0, parseInt(nPoints, 10) || 0); pathLen = Math.max(0, Math.floor(Number(pathLen))); var formulaN = nAnchors < 2 ? 0 : ((nAnchors - 1) * (nPoints + 1) + 1); if (pathLen > nAnchors && (vols.length === pathLen || (imgs && imgs.length === pathLen) || vols.length === formulaN || (imgs && imgs.length === formulaN))) { var slots = manualAnchorSlotsOnInterpolatedPath(nAnchors, nPoints); var cv = []; var ci = []; for (var ai = 0; ai < slots.length && ai < nAnchors; ai++) { var slot = slots[ai]; cv.push(slot >= 0 && slot < vols.length ? vols[slot] : null); ci.push(imgs && slot >= 0 && slot < imgs.length ? imgs[slot] : null); } return { volumes: cv, images: ci }; } if (vols.length === nAnchors) { var compactImgs = (imgs || []).slice(0, nAnchors); if (imgs && imgs.length > nAnchors) { compactImgs = compactManualAnchorVolumesAndImages([], imgs, nAnchors, nPoints, pathLen).images; } return { volumes: vols.slice(), images: compactImgs }; } return compactManualAnchorVolumesAndImages(vols, imgs, nAnchors, nPoints, pathLen); } function canReverseTrajectory() { // Choose-waypoints: selection alone is enough (path preview may lag bootstrap). if (isManualTraversalMode()) return manualSelectedVolIds.length >= 2; if (hasAnchorIndices()) return true; if (editableTrajXY && editableTrajXY.length >= 2) return true; if (anchorTrajXY && anchorTrajXY.length >= 2) return true; if (trajPlotRows && trajPlotRows.length >= 2) return true; if (startXY && endXY) return true; if (typeof latentTrajectoryPointCount === "function" && latentTrajectoryPointCount() >= 2) { return true; } if (hasGeneratedTrajectoryVolumes() && generatedTrajectoryVolumeCount >= 2) return true; return false; } function syncTrajReverseButton() { if (!btnReverseTraj) return; var pathBusy = typeof trajectoryPathRecalcBusy === "function" && trajectoryPathRecalcBusy(); var ok = canReverseTrajectory() && !pathBusy; btnReverseTraj.disabled = !ok; if (pathBusy && canReverseTrajectory()) { btnReverseTraj.title = "Unavailable while generating trajectory\u2026"; } else if (ok) { btnReverseTraj.title = "Reverse traversal order (start \u2194 end and volume frame order)."; } else { btnReverseTraj.title = "Set up a trajectory with at least two points or volumes first."; } } function reverseTrajectory() { if (!canReverseTrajectory()) return; var preserveVolumes = hasGeneratedTrajectoryVolumes(); var session = typeof activeTrajectorySession === "function" ? activeTrajectorySession() : null; if (isManualTraversalMode() && manualSelectedVolIds.length >= 2) { if (!session) { setTrajStatus("Trajectory session unavailable; reload the page and try again.", false); syncTrajReverseButton(); return; } var preserveInterpolatedCatalog = manualInterpolatedCatalogActive() && !hasGeneratedTrajectoryVolumes(); if (preserveInterpolatedCatalog) { beginAnchorPathOrderVolumePreserve(); } session.reverse(); refreshManualCustomPlotRowsDisplayOrderFromTrajectory(); buildManualPickerRows(); if (manualInterpolationArmed()) { syncManualSelectionToAnchors({ preserveOrder: true, forceInterp: true, directLine: typeof manualDirectLineDensifyActive === "function" && manualDirectLineDensifyActive(), snapVolume: !!manualParticleSnapPathActive }); } else { if (preserveInterpolatedCatalog) { syncManualInterpolatedScatterVisuals({ immediate: true }); syncManualVolumeScatterHighlight(); } redrawTrajectoryOverlay(); if (typeof scheduleTrajGlyphOverlaySync === "function") { scheduleTrajGlyphOverlaySync(); } if (trajVolDisplay) { trajVolDisplay.setChimeraxRendering(false); if (typeof trajVolDisplay._syncVolumeNavChrome === "function") { trajVolDisplay._syncVolumeNavChrome(); } } updateGenerateVolumesButtonLabel(); } syncTrajReverseButton(); return; } if (session) { var reversePathN = typeof latentTrajectoryPointCount === "function" ? latentTrajectoryPointCount() : 0; if (reversePathN < 2 && editableTrajXY && editableTrajXY.length >= 2) { reversePathN = editableTrajXY.length; } session.reverse(); // Path polyline and volume slots reverse together; restamp so path_t / // traj_xy match the new indices before any chrome sync remaps by geometry. if (editableTrajXY && editableTrajXY.length >= 2 && typeof stampTrajectoryVolumeGeometryOntoPath === "function") { stampTrajectoryVolumeGeometryOntoPath(editableTrajXY); } if (typeof reverseDirectTraceInvalidationMaps === "function" && isScatterDirectOrNearestMode() && reversePathN >= 2) { reverseDirectTraceInvalidationMaps(reversePathN); } else if (typeof reverseTrajectoryVolumeStaleAt === "function" && reversePathN >= 2) { reverseTrajectoryVolumeStaleAt(reversePathN); } refreshManualCustomPlotRowsDisplayOrderFromTrajectory(); redrawTrajectoryOverlay(); if (typeof syncDirectTraceVolumeChromeAfterPathMutation === "function" && isScatterDirectOrNearestMode()) { syncDirectTraceVolumeChromeAfterPathMutation({ forceExpand: true, redraw: false }); } if (hasAnchorIndices() || (startXY && endXY) || (editableTrajXY && editableTrajXY.length >= 2)) { fetchTrajectoryCoords({ preserveGeneratedVolumes: preserveVolumes }); } syncTrajReverseButton(); return; } setTrajStatus("Trajectory session unavailable; reload the page and try again.", false); syncTrajReverseButton(); } function syncTrajGifScatterOption() { if (!trajGifIncludeScatterEl) return; var ok = canIncludeScatterInTrajectoryGif(); trajGifIncludeScatterEl.disabled = !ok; if (trajGifIncludeScatterWrap) { trajGifIncludeScatterWrap.classList.toggle("is-disabled", !ok); } if (!ok) trajGifIncludeScatterEl.checked = false; } function gifDataUrlToBareB64(dataUrl) { var i = String(dataUrl || "").indexOf(","); return i >= 0 ? dataUrl.slice(i + 1) : String(dataUrl || ""); } function waitGifPaintFrames(n) { return new Promise(function(resolve) { var c = 0; function tick() { c++; if (c >= n) resolve(); else requestAnimationFrame(tick); } requestAnimationFrame(tick); }); } function waitTrajScatterHighlightPaint() { syncManualVolumeScatterHighlight(); syncTrajGlyphOverlay(); if (!gd || !gd.data || !gd.data[0] || gd.data[0].type !== "scattergl") { return waitGifPaintFrames(4); } return new Promise(function(resolve) { requestAnimationFrame(function() { requestAnimationFrame(function() { var dm = gd.layout && gd.layout.dragmode != null ? gd.layout.dragmode : "pan"; var done = function() { waitGifPaintFrames(3).then(resolve); }; try { var p = Plotly.relayout(gd, { dragmode: false }); if (p && typeof p.then === "function") { p.then(function() { return Plotly.relayout(gd, { dragmode: dm }); }) .then(done) .catch(done); } else { try { Plotly.relayout(gd, { dragmode: dm }); } catch (e2) { /* ignore */ } done(); } } catch (e) { done(); } }); }); }); } function trajGifVolumeDisplayRegionRect() { var viewport = document.getElementById("vslice-viewport"); var scroll = viewport && viewport.closest(".cryo-vslice-scroll"); var shell = document.getElementById("traj-vol-aside-shell"); var candidates = [ viewport, scroll, shell, trajVolDisplay && trajVolDisplay.viewportEl ]; for (var i = 0; i < candidates.length; i++) { var el = candidates[i]; if (!el) continue; var w = el.offsetWidth; var h = el.offsetHeight; if (w >= 32 && h >= 32) return { w: w, h: h }; } return { w: 480, h: 480 }; } function clearTrajGifScatterCaptureLocks() { trajGifLockedScatterSize = null; trajGifPlotStackMap = null; } function clearTrajGifVolumeAlignmentLocks() { trajGifLockedVolumeFrameBounds = null; trajGifLockedVolumeContentScale = null; }