From 60fdff461c95d9dd6e2ea775908dbfa04197ee5c Mon Sep 17 00:00:00 2001 From: Alexander Serdyukov Date: Mon, 15 Jun 2026 22:48:27 +0400 Subject: [PATCH 1/9] Lazy file picker, responsive slider, resolution restricting, intra scf/ctg context in ruler, , OSD settings --- src/BinMousePosition.js | 148 ++++++---- src/app/core/controls/RulerControl.ts | 37 +++ .../mapmanagers/HiCViewAndLayersManager.ts | 90 +++++- src/app/core/net/api/RequestManager.ts | 9 + src/app/core/net/api/request.ts | 11 + src/app/core/net/api/response.ts | 4 +- src/app/core/net/dto/requestDTO.ts | 12 + src/app/core/net/dto/responseDTO.ts | 4 +- src/app/stores/uiSettingsStore.ts | 73 ++++- .../components/common/FileSelectionTable.vue | 30 +- .../common/FileSelectionTableTypes.ts | 2 + .../sidebar/ColorPickerRectangle.vue | 1 + .../sidebar/VisualziationSettingsEditor.vue | 54 +++- .../components/upper_ribbon/HeaderRibbon.vue | 13 + .../components/upper_ribbon/NavigationBar.vue | 231 ++++++++++++++- .../upper_ribbon/UniversalFileSelector.vue | 274 ++++++++++++------ 16 files changed, 832 insertions(+), 161 deletions(-) diff --git a/src/BinMousePosition.js b/src/BinMousePosition.js index 083c7a1..03f2411 100644 --- a/src/BinMousePosition.js +++ b/src/BinMousePosition.js @@ -28,6 +28,35 @@ import { identityTransform, } from "ol/proj"; import TileLayer from "ol/layer/Tile"; +import { useUiSettingsStore } from "@/app/stores/uiSettingsStore"; + +function getOsdSettings() { + try { + return useUiSettingsStore(); + } catch { + return null; + } +} + +function overlayStyle(position) { + const location = + position === "bottom-left" + ? "left: 12px; bottom: 12px;" + : "right: 12px; top: 12px;"; + return [ + "display: block", + "position: absolute", + location, + "max-width: min(48rem, calc(100vw - 3rem))", + "padding: 14px 16px", + "background: rgba(0, 0, 0, 0.35)", + "border: 1px solid black", + "border-radius: 12px", + "color: white", + "text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000", + "pointer-events: none", + ].join("; "); +} export default class BinMousePosition extends MousePosition { constructor(opt_options) { @@ -133,6 +162,24 @@ export default class BinMousePosition extends MousePosition { this.getDimensionHolderForDescriptor(guidanceDescriptor); const bpResolution = guidanceDescriptor.bpResolution; const pixelResolution = guidanceDescriptor.pixelResolution; + const osdSettings = getOsdSettings(); + if (osdSettings && !osdSettings.osdOverlayVisible) { + html = ""; + if (!this.renderedHTML_ || html !== this.renderedHTML_) { + this.element.innerHTML = html; + this.renderedHTML_ = html; + } + return; + } + const fields = osdSettings?.osdOverlayFields ?? {}; + const fieldOrder = osdSettings?.osdOverlayFieldOrder ?? []; + const osdLines = {}; + const fieldEnabled = (field) => fields[field] !== false; + const appendLine = (field, text) => { + if (fieldEnabled(field)) { + osdLines[field] = text; + } + }; const fixed_coordinates = mapCoordinate.map((c) => Math.ceil(c / pixelResolution) ); @@ -145,12 +192,14 @@ export default class BinMousePosition extends MousePosition { bpResolution ); - html = - '
'; - html += - "Global projection coordinate: " + coordinate.map(Math.floor); - html = html + "<"; - html = html + "br/>"; + this.element.style.position = "absolute"; + this.element.style.inset = "0"; + this.element.style.pointerEvents = "none"; + html = `
`; + appendLine( + "global", + "Global projection coordinate: " + coordinate.map(Math.floor) + ); // html += // "Center coordinate: " + map.getView().getCenter().map(Math.floor); @@ -158,23 +207,21 @@ export default class BinMousePosition extends MousePosition { // html = html + "br/>"; if (fixed_coordinates) { - html = html + "Bin resolution: 1:" + bpResolution; - html = html + "<"; - html = html + "br/>"; + appendLine("resolution", "Bin resolution: 1:" + bpResolution); if ( Number.isFinite(hoveredBpResolution) && hoveredBpResolution !== bpResolution ) { - html = - html + "Hovered tile resolution: 1:" + hoveredBpResolution; - html = html + "<"; - html = html + "br/>"; + appendLine( + "resolution", + "Hovered tile resolution: 1:" + hoveredBpResolution + ); } if (guidanceDescriptor.sourceName) { - html = - html + "Guidance source: " + guidanceDescriptor.sourceName; - html = html + "<"; - html = html + "br/>"; + appendLine( + "source", + "Guidance source: " + guidanceDescriptor.sourceName + ); } if (this.layersManager?.getVisibleSourceResolutionDescriptors) { const visible = @@ -188,18 +235,19 @@ export default class BinMousePosition extends MousePosition { : null, ].filter(Boolean); if (details.length > 0) { - html = - html + "Visible source resolutions: " + details.join("; "); - html = html + "<"; - html = html + "br/>"; + appendLine( + "visibleResolutions", + "Visible source resolutions: " + details.join("; ") + ); } } - html = - html + + appendLine( + "pixels", "Position: px1=" + int_coordinates_px[0] + " px2=" + - int_coordinates_px[1]; + int_coordinates_px[1] + ); } if (dimensionHolder) { @@ -207,16 +255,13 @@ export default class BinMousePosition extends MousePosition { int_coordinates_px, bpResolution ); - html = html + "<"; - html = html + "br/>"; - html = - html + + appendLine( + "bins", "Position: bin1=" + int_coordinates_bins[0] + " bin2=" + - int_coordinates_bins[1]; - html = html + "<"; - html = html + "br/>"; + int_coordinates_bins[1] + ); const bp1 = dimensionHolder.getStartBpOfPx( int_coordinates_px[0], bpResolution @@ -233,10 +278,8 @@ export default class BinMousePosition extends MousePosition { int_coordinates_px[1], bpResolution ); - html = html + "Position: bp1=" + bp1 + " bp2=" + bp2; - html = html + "<"; - html = html + "br/>"; - html = html + "Contigs: ctg1=" + ctg1 + " ctg2=" + ctg2; + appendLine("basePairs", "Position: bp1=" + bp1 + " bp2=" + bp2); + appendLine("contigs", "Contigs: ctg1=" + ctg1 + " ctg2=" + ctg2); if (dimensionHolder.getContigLocusByPx) { const locus1 = dimensionHolder.getContigLocusByPx( int_coordinates_px[0], @@ -246,39 +289,44 @@ export default class BinMousePosition extends MousePosition { int_coordinates_px[1], bpResolution ); - html = html + "<"; - html = html + "br/>"; - html = - html + + appendLine( + "inContig", "In-contig bp: ctg1=+" + locus1.inContigBp + " ctg2=+" + - locus2.inContigBp; + locus2.inContigBp + ); } if (this.scaffold_holder?.getScaffoldLocusByBp) { const scaffold1 = this.scaffold_holder.getScaffoldLocusByBp(bp1); const scaffold2 = this.scaffold_holder.getScaffoldLocusByBp(bp2); - html = html + "<"; - html = html + "br/>"; - html = - html + + appendLine( + "scaffolds", "Scaffolds: scf1=" + (scaffold1 ? scaffold1.scaffoldName : "unscaffolded") + " scf2=" + - (scaffold2 ? scaffold2.scaffoldName : "unscaffolded"); - html = html + "<"; - html = html + "br/>"; - html = - html + + (scaffold2 ? scaffold2.scaffoldName : "unscaffolded") + ); + appendLine( + "inScaffold", "In-scaffold bp: scf1=" + (scaffold1 ? "+" + scaffold1.inScaffoldBp : "n/a") + " scf2=" + - (scaffold2 ? "+" + scaffold2.inScaffoldBp : "n/a"); + (scaffold2 ? "+" + scaffold2.inScaffoldBp : "n/a") + ); } } + const orderedFields = [ + ...fieldOrder, + ...Object.keys(osdLines).filter((field) => !fieldOrder.includes(field)), + ]; + const orderedLines = orderedFields + .map((field) => osdLines[field]) + .filter(Boolean); + html += orderedLines.join("
"); html += "
"; } catch (error) { console.warn("Unable to update map mouse position overlay", error); diff --git a/src/app/core/controls/RulerControl.ts b/src/app/core/controls/RulerControl.ts index 7e4a5c0..eca40d1 100644 --- a/src/app/core/controls/RulerControl.ts +++ b/src/app/core/controls/RulerControl.ts @@ -49,6 +49,8 @@ interface RulerTick { mapPx: number; bp: number; label: string; + contigLabel?: string; + scaffoldLabel?: string; major: boolean; boundary?: "start" | "end"; } @@ -558,6 +560,8 @@ class RulerControl extends Control { currentAnchor = anchor; } const delta = Math.max(0, Math.round(bp - currentAnchor)); + const contig = this.contigDimensionHolder.getContigLocusByBp(bp); + const scaffold = this.mapManager.scaffoldHolder.getScaffoldLocusByBp(bp); return { screen, mapPx, @@ -568,6 +572,13 @@ class RulerControl extends Control { major || delta <= 0 ? this.formatBpLabel(anchor, 0) : `+${this.formatBpLabel(delta, 0)}`, + contigLabel: major + ? `ctg +${this.formatBpLabel(contig.inContigBp, 0)}` + : undefined, + scaffoldLabel: + major && scaffold + ? `scf +${this.formatBpLabel(scaffold.inScaffoldBp, 0)}` + : undefined, }; }); } @@ -648,6 +659,19 @@ class RulerControl extends Control { true, false ); + if (tick.major && (tick.contigLabel || tick.scaffoldLabel)) { + this.drawRotatedText( + [tick.contigLabel, tick.scaffoldLabel].filter(Boolean).join(" / "), + textX, + Math.max(22, coord[1] - tickLength + 10), + context, + 0, + "9px sans-serif", + textAlign, + true, + false + ); + } return; } @@ -663,6 +687,19 @@ class RulerControl extends Control { true, false ); + if (tick.major && (tick.contigLabel || tick.scaffoldLabel)) { + this.drawRotatedText( + [tick.contigLabel, tick.scaffoldLabel].filter(Boolean).join(" / "), + Math.max(2, coord[0] - tickLength - 4), + this.clamp(textY + 12, 12, this.canvas.height - 3), + context, + 0, + "9px sans-serif", + "right", + true, + false + ); + } } private formatBpLabel(bp: number, precision: number): string { diff --git a/src/app/core/mapmanagers/HiCViewAndLayersManager.ts b/src/app/core/mapmanagers/HiCViewAndLayersManager.ts index 54c274d..e684d46 100644 --- a/src/app/core/mapmanagers/HiCViewAndLayersManager.ts +++ b/src/app/core/mapmanagers/HiCViewAndLayersManager.ts @@ -161,6 +161,7 @@ class HiCViewAndLayersManager { private secondaryResolutionSet?: SourceResolutionDescriptorSet; private wheelZoomInteraction?: ContigMouseWheelZoom; private coordinateBaseBp: number; + private enabledBpResolutions?: Set; public selectionCollections: { readonly selectedContigFeatures: Collection>; @@ -428,10 +429,74 @@ class HiCViewAndLayersManager { resolutions: number[]; pixelResolutionSet: number[]; } { - return getNavigationResolutionModelForSets( - this.primaryResolutionSet, - this.secondaryResolutionSet + const descriptors = [ + ...this.getEnabledResolutionTuples(this.primaryResolutionSet), + ...(this.secondaryResolutionSet + ? this.getEnabledResolutionTuples(this.secondaryResolutionSet) + : []), + ].sort((a, b) => a.pixelResolution - b.pixelResolution); + const byPixelResolution = new Map(); + for (const descriptor of descriptors) { + if (!byPixelResolution.has(descriptor.pixelResolution)) { + byPixelResolution.set(descriptor.pixelResolution, descriptor); + } + } + const uniqueDescriptors = [...byPixelResolution.values()].sort( + (a, b) => a.pixelResolution - b.pixelResolution + ); + if (uniqueDescriptors.length === 0) { + return getNavigationResolutionModelForSets( + this.primaryResolutionSet, + this.secondaryResolutionSet + ); + } + return { + resolutions: uniqueDescriptors.map((descriptor) => descriptor.bpResolution), + pixelResolutionSet: uniqueDescriptors.map( + (descriptor) => descriptor.pixelResolution + ), + }; + } + + public getAllNavigationBpResolutions(): number[] { + return Array.from( + new Set([ + ...this.primaryResolutionSet.resolutions, + ...(this.secondaryResolutionSet?.resolutions ?? []), + ]) + ).sort((a, b) => a - b); + } + + public getEnabledBpResolutions(): number[] { + const all = this.getAllNavigationBpResolutions(); + if (!this.enabledBpResolutions || this.enabledBpResolutions.size === 0) { + return all; + } + return all.filter((resolution) => this.enabledBpResolutions?.has(resolution)); + } + + public setEnabledBpResolutions(resolutions: number[]): void { + const valid = new Set(this.getAllNavigationBpResolutions()); + const next = resolutions + .map((resolution) => Number(resolution)) + .filter((resolution) => Number.isFinite(resolution) && valid.has(resolution)); + this.enabledBpResolutions = next.length > 0 ? new Set(next) : undefined; + this.updateWheelZoomResolutionModel(); + this.updateProjectionAndViewExtent(1); + this.updateCurrentHiCViewState(); + this.mapManager.getMap().changed(); + } + + private getEnabledResolutionTuples( + set: SourceResolutionDescriptorSet + ): LayerResolutionDescriptor[] { + if (!this.enabledBpResolutions || this.enabledBpResolutions.size === 0) { + return set.resolutionTuples; + } + const filtered = set.resolutionTuples.filter((tuple) => + this.enabledBpResolutions?.has(tuple.bpResolution) ); + return filtered.length > 0 ? filtered : set.resolutionTuples; } public getVectorResolutionTuples(): LayerResolutionDescriptor[] { @@ -989,7 +1054,24 @@ class HiCViewAndLayersManager { if (!set) { throw new Error(`No resolutions available for ${sourceName}`); } - return getResolutionDescriptorForViewResolution(set, viewResolution); + const tuples = this.getEnabledResolutionTuples(set); + if (tuples.length === set.resolutionTuples.length) { + return getResolutionDescriptorForViewResolution(set, viewResolution); + } + let descriptor = tuples[0]; + const sorted = [...tuples].sort( + (a, b) => + a.layerResolutionBorders.minResolutionInclusive - + b.layerResolutionBorders.minResolutionInclusive + ); + for (const tuple of sorted) { + if (tuple.layerResolutionBorders.minResolutionInclusive <= viewResolution) { + descriptor = tuple; + } else { + break; + } + } + return descriptor; } public getVisibleSourceResolutionDescriptors(): { diff --git a/src/app/core/net/api/RequestManager.ts b/src/app/core/net/api/RequestManager.ts index 6bb846d..51dc49a 100644 --- a/src/app/core/net/api/RequestManager.ts +++ b/src/app/core/net/api/RequestManager.ts @@ -56,6 +56,7 @@ import { ListAGPFilesRequest, ListCoolerFilesRequest, ListConvertibleMatrixFilesRequest, + ListDirectoryRequest, ListFilesDetailedRequest, ListFASTAFilesRequest, ListFilesRequest, @@ -542,6 +543,14 @@ class RequestManager { ); } + public async listDirectory(directory = ""): Promise { + return this.sendRequest(new ListDirectoryRequest({ directory })) + .then((response) => response.data as Record[]) + .then((items) => + items.map((item) => new FileEntryResponseDTO(item).toEntity()) + ); + } + public async listCoolers(): Promise { const response = await this.sendRequest(new ListCoolerFilesRequest()); return response.data as string[]; diff --git a/src/app/core/net/api/request.ts b/src/app/core/net/api/request.ts index 1a90bfd..c9d180d 100644 --- a/src/app/core/net/api/request.ts +++ b/src/app/core/net/api/request.ts @@ -61,6 +61,16 @@ class ListFilesDetailedRequest implements HiCTAPIRequest { requestPath = "/list_files_detailed"; } +class ListDirectoryRequest implements HiCTAPIRequest { + requestPath = "/list_directory"; + + public constructor( + public readonly options: { + readonly directory: string; + } + ) {} +} + class ListFASTAFilesRequest implements HiCTAPIRequest { requestPath = "/list_fasta_files"; } @@ -730,6 +740,7 @@ export { OpenFileRequest, ListFilesRequest, ListFilesDetailedRequest, + ListDirectoryRequest, GroupContigsIntoScaffoldRequest, UngroupContigsFromScaffoldRequest, ReverseSelectionRangeRequest, diff --git a/src/app/core/net/api/response.ts b/src/app/core/net/api/response.ts index deb66d5..6d60124 100644 --- a/src/app/core/net/api/response.ts +++ b/src/app/core/net/api/response.ts @@ -260,7 +260,9 @@ class FileEntryResponse { public readonly name: string, public readonly sizeBytes: number, public readonly modifiedAtMs: number, - public readonly extension: string + public readonly extension: string, + public readonly type: "file" | "directory" = "file", + public readonly symbolicLink: boolean = false ) {} } diff --git a/src/app/core/net/dto/requestDTO.ts b/src/app/core/net/dto/requestDTO.ts index c6ff1aa..034bd2b 100644 --- a/src/app/core/net/dto/requestDTO.ts +++ b/src/app/core/net/dto/requestDTO.ts @@ -22,6 +22,7 @@ import { ListFilesRequest, ListFilesDetailedRequest, + ListDirectoryRequest, OpenFileRequest, CloseFileRequest, AttachSessionRequest, @@ -172,6 +173,8 @@ abstract class HiCTAPIRequestDTO< return new ListFilesRequestDTO(entity); case entity instanceof ListFilesDetailedRequest: return new ListFilesDetailedRequestDTO(entity); + case entity instanceof ListDirectoryRequest: + return new ListDirectoryRequestDTO(entity as ListDirectoryRequest); case entity instanceof ListCoolerFilesRequest: return new ListCoolerFilesRequestDTO(entity); case entity instanceof ListConvertibleMatrixFilesRequest: @@ -779,6 +782,14 @@ class ListFilesDetailedRequestDTO extends HiCTAPIRequestDTO { + toDTO(): Record { + return { + directory: this.entity.options.directory, + }; + } +} + class ListCoolerFilesRequestDTO extends HiCTAPIRequestDTO { toDTO(): Record { return {}; @@ -1030,6 +1041,7 @@ export { OpenFileRequestDTO, ListFilesRequestDTO, ListFilesDetailedRequestDTO, + ListDirectoryRequestDTO, CloseFileRequestDTO, AttachSessionRequestDTO, ResolveMatrixSourceRequestDTO, diff --git a/src/app/core/net/dto/responseDTO.ts b/src/app/core/net/dto/responseDTO.ts index e32625e..d2a9a63 100644 --- a/src/app/core/net/dto/responseDTO.ts +++ b/src/app/core/net/dto/responseDTO.ts @@ -370,7 +370,9 @@ class FileEntryResponseDTO extends InboundDTO { (this.json["name"] as string) ?? "", (this.json["sizeBytes"] as number) ?? -1, (this.json["modifiedAtMs"] as number) ?? 0, - (this.json["extension"] as string) ?? "" + (this.json["extension"] as string) ?? "", + ((this.json["type"] as string) === "directory" ? "directory" : "file"), + Boolean(this.json["symbolicLink"] ?? false) ); } } diff --git a/src/app/stores/uiSettingsStore.ts b/src/app/stores/uiSettingsStore.ts index ddffbaf..9a5f1e6 100644 --- a/src/app/stores/uiSettingsStore.ts +++ b/src/app/stores/uiSettingsStore.ts @@ -1,12 +1,77 @@ import { defineStore } from "pinia"; -import { ref } from "vue"; +import { ref, watch } from "vue"; + +const readStored = (key: string, fallback: T): T => { + try { + const raw = window.localStorage.getItem(key); + return raw == null ? fallback : (JSON.parse(raw) as T); + } catch { + return fallback; + } +}; + +const persistRef = (key: string, value: { value: T }) => { + watch( + value, + (next) => { + try { + window.localStorage.setItem(key, JSON.stringify(next)); + } catch { + // Ignore storage failures; UI settings are non-critical. + } + }, + { deep: true } + ); +}; export const useUiSettingsStore = defineStore("uiSettings", () => { const customZoomSliderEnabled = ref(false); const binaryTileTransportEnabled = ref(false); - const fileSelectorMode = ref<"explorer" | "tree">("explorer"); + const fileSelectorMode = ref<"explorer" | "tree">( + readStored("hict.ui.fileSelectorMode", "explorer") + ); const inheritTrackBackgroundFromMap = ref(true); const trackBackgroundColor = ref("rgba(244,247,251,0.98)"); + const osdOverlayVisible = ref(readStored("hict.ui.osd.visible", true)); + const osdOverlayPosition = ref<"top-right" | "bottom-left">( + readStored("hict.ui.osd.position", "top-right") + ); + const osdOverlayFields = ref>( + readStored("hict.ui.osd.fields", { + global: true, + resolution: true, + source: true, + visibleResolutions: true, + pixels: true, + bins: true, + basePairs: true, + contigs: true, + inContig: true, + scaffolds: true, + inScaffold: true, + }) + ); + const osdOverlayFieldOrder = ref( + readStored("hict.ui.osd.fieldOrder", [ + "global", + "resolution", + "source", + "visibleResolutions", + "pixels", + "bins", + "basePairs", + "contigs", + "inContig", + "scaffolds", + "inScaffold", + ]) + ); + + persistRef("hict.ui.fileSelectorMode", fileSelectorMode); + persistRef("hict.ui.osd.visible", osdOverlayVisible); + persistRef("hict.ui.osd.position", osdOverlayPosition); + persistRef("hict.ui.osd.fields", osdOverlayFields); + persistRef("hict.ui.osd.fieldOrder", osdOverlayFieldOrder); return { customZoomSliderEnabled, @@ -14,5 +79,9 @@ export const useUiSettingsStore = defineStore("uiSettings", () => { fileSelectorMode, inheritTrackBackgroundFromMap, trackBackgroundColor, + osdOverlayVisible, + osdOverlayPosition, + osdOverlayFields, + osdOverlayFieldOrder, }; }); diff --git a/src/app/ui/components/common/FileSelectionTable.vue b/src/app/ui/components/common/FileSelectionTable.vue index c414088..8dab452 100644 --- a/src/app/ui/components/common/FileSelectionTable.vue +++ b/src/app/ui/components/common/FileSelectionTable.vue @@ -48,6 +48,7 @@ :aria-label="`Select ${data.name}`" :checked="isSelected(data.path)" class="form-check-input file-row-checkbox" + :disabled="data.type === 'directory'" type="checkbox" @click.stop="selectPath(data.path, $event)" @keydown.space.stop.prevent="selectPath(data.path)" @@ -59,6 +60,7 @@ {{ data.name }} + link @@ -136,6 +138,8 @@ type FileSelectionTableRow = { iconClass: string; statusLabel: string; statusClass: string; + type: "file" | "directory"; + symbolicLink: boolean; }; const emit = defineEmits<{ @@ -178,6 +182,7 @@ const tableRows = computed(() => const path = entry.path; const name = entry.name && entry.name.trim().length > 0 ? entry.name : basename(path); const extension = entry.extension ?? extensionOf(path); + const type = entry.type ?? "file"; return { path, name, @@ -186,9 +191,11 @@ const tableRows = computed(() => sizeLabel: formatBytes(entry.sizeBytes ?? -1), modifiedLabel: formatTimestamp(entry.modifiedAtMs ?? 0), extension, - iconClass: entry.iconClass ?? iconClassForPath(path, extension), + iconClass: entry.iconClass ?? iconClassForPath(path, extension, type), statusLabel: props.statusLabel?.(path) ?? "", statusClass: props.statusClass?.(path) ?? "", + type, + symbolicLink: Boolean(entry.symbolicLink), }; }) ); @@ -207,6 +214,11 @@ const isSelected = (path: string): boolean => : path === props.selectedPath; const selectPath = (path: string, event?: MouseEvent): void => { + const row = tableRows.value.find((candidate) => candidate.path === path); + if (row?.type === "directory") { + emit("activate", path); + return; + } if (props.multiSelect) { togglePath(path, event); return; @@ -290,7 +302,10 @@ const extensionOf = (path: string): string => { return dot >= 0 ? name.slice(dot + 1).toLowerCase() : ""; }; -const iconClassForPath = (path: string, extension: string): string => { +const iconClassForPath = (path: string, extension: string, type: "file" | "directory" = "file"): string => { + if (type === "directory") { + return "pi pi-fw pi-folder"; + } const normalized = path.toLowerCase(); if (normalized.endsWith(".hict") || normalized.endsWith(".hict.hdf5") || normalized.endsWith(".hdf5")) { return "pi pi-fw pi-map"; @@ -407,6 +422,17 @@ const formatTimestamp = (timestampMs: number): string => { text-overflow: ellipsis; } +.file-link-badge { + border: 1px solid #94a3b8; + border-radius: 0.25rem; + color: #475569; + font-size: 0.68rem; + font-weight: 700; + line-height: 1; + padding: 0.08rem 0.25rem; + text-transform: uppercase; +} + .file-path-cell { display: block; max-width: 100%; diff --git a/src/app/ui/components/common/FileSelectionTableTypes.ts b/src/app/ui/components/common/FileSelectionTableTypes.ts index e1e16c2..8a60689 100644 --- a/src/app/ui/components/common/FileSelectionTableTypes.ts +++ b/src/app/ui/components/common/FileSelectionTableTypes.ts @@ -29,4 +29,6 @@ export type FileSelectionTableEntry = { modifiedAtMs?: number; extension?: string; iconClass?: string; + type?: "file" | "directory"; + symbolicLink?: boolean; }; diff --git a/src/app/ui/components/sidebar/ColorPickerRectangle.vue b/src/app/ui/components/sidebar/ColorPickerRectangle.vue index 3c41044..356cc7a 100644 --- a/src/app/ui/components/sidebar/ColorPickerRectangle.vue +++ b/src/app/ui/components/sidebar/ColorPickerRectangle.vue @@ -95,6 +95,7 @@ onMounted(() => { legacyCSS: true, }); colorSelectorStyleObject.value["background"] = currentColor.value.RGBA; + emit("onColorChanged", currentColor.value as ColorTranslator); }, onDone: function (color) { currentColor.value = new ColorTranslator(color.rgbaString as string, { diff --git a/src/app/ui/components/sidebar/VisualziationSettingsEditor.vue b/src/app/ui/components/sidebar/VisualziationSettingsEditor.vue index 3fad100..cd450da 100644 --- a/src/app/ui/components/sidebar/VisualziationSettingsEditor.vue +++ b/src/app/ui/components/sidebar/VisualziationSettingsEditor.vue @@ -152,7 +152,7 @@ From 101d16169e0b1302868df15e36292ed6ac439568 Mon Sep 17 00:00:00 2001 From: Alexander Serdyukov Date: Tue, 16 Jun 2026 01:28:50 +0400 Subject: [PATCH 2/9] Hot-fixed freeze on map open --- .../sidebar/ColorPickerRectangle.vue | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/app/ui/components/sidebar/ColorPickerRectangle.vue b/src/app/ui/components/sidebar/ColorPickerRectangle.vue index 356cc7a..f052099 100644 --- a/src/app/ui/components/sidebar/ColorPickerRectangle.vue +++ b/src/app/ui/components/sidebar/ColorPickerRectangle.vue @@ -58,6 +58,7 @@ const currentColor = ref(props.getDefaultColor()); const vPicker: Ref = ref(null); const overlayRoot: Ref = ref(null); const pickerOpen = ref(false); +let suppressPickerEvents = false; watch( () => props.getDefaultColor(), @@ -72,7 +73,14 @@ watch( // currentColor.value = nc.replace(re, `rgba($1,$2,$3,${alpha})`); currentColor.value = nc; colorSelectorStyleObject.value["background"] = currentColor.value.RGBA; - picker.value?.setColor(currentColor.value.RGBA, false); + if (picker.value) { + suppressPickerEvents = true; + try { + picker.value.setColor(currentColor.value.RGBA, false); + } finally { + suppressPickerEvents = false; + } + } // console.log("Picker rectangle: new color", currentColor.value); } } @@ -95,14 +103,18 @@ onMounted(() => { legacyCSS: true, }); colorSelectorStyleObject.value["background"] = currentColor.value.RGBA; - emit("onColorChanged", currentColor.value as ColorTranslator); + if (pickerOpen.value && !suppressPickerEvents) { + emit("onColorChanged", currentColor.value as ColorTranslator); + } }, onDone: function (color) { currentColor.value = new ColorTranslator(color.rgbaString as string, { legacyCSS: true, }); colorSelectorStyleObject.value["background"] = currentColor.value.RGBA; - emit("onColorChanged", currentColor.value as ColorTranslator); + if (pickerOpen.value && !suppressPickerEvents) { + emit("onColorChanged", currentColor.value as ColorTranslator); + } closePicker(); }, popup: false, From f88a0b6c59570175db2ca7c89cd74241c045de89 Mon Sep 17 00:00:00 2001 From: Alexander Serdyukov Date: Tue, 16 Jun 2026 02:31:46 +0400 Subject: [PATCH 3/9] OSD config fixes, buttons+icons fixes, adding export matrix modal, defaulting to using natives, fixed .. navifation in the explorer view --- src/app/core/controls/RulerControl.ts | 47 +++- .../mapmanagers/HiCViewAndLayersManager.ts | 81 ++++-- src/app/core/net/api/request.ts | 1 + src/app/core/net/dto/requestDTO.ts | 1 + src/app/stores/uiSettingsStore.ts | 5 + .../components/common/FileSelectionTable.vue | 12 +- .../sidebar/VisualziationSettingsEditor.vue | 6 +- .../components/upper_ribbon/HeaderRibbon.vue | 48 +++- .../upper_ribbon/MatrixExportModal.vue | 251 ++++++++++++++++++ .../components/upper_ribbon/NavigationBar.vue | 167 ++++++++---- .../upper_ribbon/UniversalFileSelector.vue | 34 ++- 11 files changed, 543 insertions(+), 110 deletions(-) create mode 100644 src/app/ui/components/upper_ribbon/MatrixExportModal.vue diff --git a/src/app/core/controls/RulerControl.ts b/src/app/core/controls/RulerControl.ts index eca40d1..7c5590f 100644 --- a/src/app/core/controls/RulerControl.ts +++ b/src/app/core/controls/RulerControl.ts @@ -32,6 +32,7 @@ import { transform } from "ol/proj"; import { storeToRefs } from "pinia"; import { useStyleStore } from "@/app/stores/styleStore"; import { useVisualizationOptionsStore } from "@/app/stores/visualizationOptionsStore"; +import { useUiSettingsStore } from "@/app/stores/uiSettingsStore"; import { Ref } from "vue"; import Colormap from "../visualization/colormap/Colormap"; import { ColorTranslator } from "colortranslator"; @@ -64,6 +65,7 @@ class RulerControl extends Control { protected readonly mapBackgroundColor: Ref; protected readonly colormap: Ref; + protected readonly rulerCoordinateMode: Ref<"global" | "contig" | "scaffold">; public readonly canvasSize: number[]; public readonly direction: "vertical" | "horizontal"; @@ -139,9 +141,12 @@ class RulerControl extends Control { const { colormap } = storeToRefs(visualizationOptionsStore); const stylesStore = useStyleStore(); const { mapBackgroundColor } = storeToRefs(stylesStore); + const uiSettingsStore = useUiSettingsStore(); + const { rulerCoordinateMode } = storeToRefs(uiSettingsStore); this.colormap = colormap; this.mapBackgroundColor = mapBackgroundColor as Ref; + this.rulerCoordinateMode = rulerCoordinateMode; this.canvasSize = canvasSize; this.canvas.addEventListener("mousemove", (event) => @@ -546,22 +551,30 @@ class RulerControl extends Control { options.resolutionDescriptor.bpResolution ) ); - const maxBp = Math.max(...bpValues, 0); + const maxBp = Math.max( + ...bpValues.map((bp) => this.coordinateValueForMode(bp)), + 0 + ); const labelUnit = this.absoluteLabelUnit(maxBp); - let currentAnchor = this.roundDownToUnit(bpValues[0] ?? 0, labelUnit); + let currentAnchor = this.roundDownToUnit( + this.coordinateValueForMode(bpValues[0] ?? 0), + labelUnit + ); return screens.map((screen, index) => { const bp = bpValues[index] ?? 0; const mapPx = mapPxValues[index] ?? 0; - const anchor = this.roundDownToUnit(bp, labelUnit); + const coordinateValue = this.coordinateValueForMode(bp); + const anchor = this.roundDownToUnit(coordinateValue, labelUnit); const boundary = index === 0 ? "start" : index === screens.length - 1 ? "end" : undefined; const major = boundary !== undefined || anchor !== currentAnchor; if (major) { currentAnchor = anchor; } - const delta = Math.max(0, Math.round(bp - currentAnchor)); + const delta = Math.max(0, Math.round(coordinateValue - currentAnchor)); const contig = this.contigDimensionHolder.getContigLocusByBp(bp); const scaffold = this.mapManager.scaffoldHolder.getScaffoldLocusByBp(bp); + const prefix = this.coordinatePrefixForMode(); return { screen, mapPx, @@ -570,19 +583,39 @@ class RulerControl extends Control { boundary, label: major || delta <= 0 - ? this.formatBpLabel(anchor, 0) + ? `${prefix}${this.formatBpLabel(anchor, 0)}` : `+${this.formatBpLabel(delta, 0)}`, - contigLabel: major + contigLabel: major && this.rulerCoordinateMode.value === "global" ? `ctg +${this.formatBpLabel(contig.inContigBp, 0)}` : undefined, scaffoldLabel: - major && scaffold + major && scaffold && this.rulerCoordinateMode.value === "global" ? `scf +${this.formatBpLabel(scaffold.inScaffoldBp, 0)}` : undefined, }; }); } + private coordinateValueForMode(bp: number): number { + if (this.rulerCoordinateMode.value === "contig") { + return this.contigDimensionHolder.getContigLocusByBp(bp).inContigBp; + } + if (this.rulerCoordinateMode.value === "scaffold") { + return this.mapManager.scaffoldHolder.getScaffoldLocusByBp(bp)?.inScaffoldBp ?? bp; + } + return bp; + } + + private coordinatePrefixForMode(): string { + if (this.rulerCoordinateMode.value === "contig") { + return "ctg "; + } + if (this.rulerCoordinateMode.value === "scaffold") { + return "scf "; + } + return ""; + } + private screenPositionToMapPx( screenPosition: number, mapStartScreen: number, diff --git a/src/app/core/mapmanagers/HiCViewAndLayersManager.ts b/src/app/core/mapmanagers/HiCViewAndLayersManager.ts index e684d46..e522ff9 100644 --- a/src/app/core/mapmanagers/HiCViewAndLayersManager.ts +++ b/src/app/core/mapmanagers/HiCViewAndLayersManager.ts @@ -162,6 +162,8 @@ class HiCViewAndLayersManager { private wheelZoomInteraction?: ContigMouseWheelZoom; private coordinateBaseBp: number; private enabledBpResolutions?: Set; + private rulerRenderCallback: (() => void) | null = null; + private readonly pendingFeatureStyleRefreshHandles = new Set(); public selectionCollections: { readonly selectedContigFeatures: Collection>; @@ -913,14 +915,6 @@ class HiCViewAndLayersManager { track: Track2DSymmetric, layersCollection: Layer[] = this.layersHolder.track2DLayers ) { - console.log( - "Adding track: ", - track, - " is it Track2DSymmetric: ", - track instanceof Track2DSymmetric, - " is it ContigBordersTrack2D: ", - track instanceof ContigBordersTrack2D - ); this.getVectorResolutionTuples().forEach( ({ bpResolution, pixelResolution }) => { const vectorSource = new VectorSource(); @@ -1333,6 +1327,7 @@ class HiCViewAndLayersManager { rulerV.render({ map } as never); }); }; + this.rulerRenderCallback = scheduleRulerRender; map.on("moveend", scheduleRulerRender); view.on("change:center", scheduleRulerRender); view.on("change:resolution", scheduleRulerRender); @@ -1344,11 +1339,20 @@ class HiCViewAndLayersManager { } } + public scheduleRulerRender(): void { + this.rulerRenderCallback?.(); + } + public dispose(): void { if (!this.mapManager.getMap()) { return; } try { + for (const handle of this.pendingFeatureStyleRefreshHandles) { + window.clearTimeout(handle); + } + this.pendingFeatureStyleRefreshHandles.clear(); + this.rulerRenderCallback = null; this.mapManager.getMap().getControls().clear(); const hRuler = document.getElementById("horizontal-ruler-div"); if (hRuler) { @@ -1674,18 +1678,63 @@ class HiCViewAndLayersManager { layers: Layer[], trackTypes: Set ): void { - for (const features of track.features.values()) { - for (const feature of features) { - if (trackTypes.has(String(feature.get("trackType")))) { - feature.setStyle(track.style); + for (const layer of layers) { + const source = layer.getSource() as VectorSource | undefined; + if (!source) { + continue; + } + if (layer.getVisible()) { + for (const feature of source.getFeatures()) { + if (trackTypes.has(String(feature.get("trackType")))) { + feature.setStyle(track.style); + } } + source.changed(); + layer.changed(); + } else { + layer.set(HiCViewAndLayersManager.VECTOR_SOURCE_DIRTY_FLAG, true); } } - for (const layer of layers) { - layer.getSource()?.changed(); - layer.changed(); - } this.mapManager.getMap().changed(); + this.scheduleBackgroundGeneratedFeatureStyleRefresh(track, trackTypes); + } + + private scheduleBackgroundGeneratedFeatureStyleRefresh( + track: Track2DSymmetric, + trackTypes: Set + ): void { + const resolutionFeatureGroups = Array.from(track.features.values()); + let groupIndex = 0; + let featureIndex = 0; + let handle: number | undefined; + const processChunk = () => { + if (handle !== undefined) { + this.pendingFeatureStyleRefreshHandles.delete(handle); + handle = undefined; + } + const deadline = performance.now() + 8; + while (groupIndex < resolutionFeatureGroups.length && performance.now() < deadline) { + const features = resolutionFeatureGroups[groupIndex]; + while (featureIndex < features.length && performance.now() < deadline) { + const feature = features[featureIndex]; + if (trackTypes.has(String(feature.get("trackType")))) { + feature.setStyle(track.style); + } + featureIndex += 1; + } + if (featureIndex >= features.length) { + groupIndex += 1; + featureIndex = 0; + } + } + if (groupIndex < resolutionFeatureGroups.length) { + handle = window.setTimeout(processChunk, 0); + this.pendingFeatureStyleRefreshHandles.add(handle); + } + }; + + handle = window.setTimeout(processChunk, 0); + this.pendingFeatureStyleRefreshHandles.add(handle); } private isVectorLayerDirty(layer: Layer | undefined): boolean { diff --git a/src/app/core/net/api/request.ts b/src/app/core/net/api/request.ts index c9d180d..ad5d4b6 100644 --- a/src/app/core/net/api/request.ts +++ b/src/app/core/net/api/request.ts @@ -194,6 +194,7 @@ class StartConversionJobRequest implements HiCTAPIRequest { public readonly options: { readonly filename: string; readonly assemblyFilename?: string; + readonly useCurrentAssembly?: boolean; readonly direction?: string; readonly overwrite?: boolean; readonly resolutions?: string; diff --git a/src/app/core/net/dto/requestDTO.ts b/src/app/core/net/dto/requestDTO.ts index 034bd2b..c9e8b51 100644 --- a/src/app/core/net/dto/requestDTO.ts +++ b/src/app/core/net/dto/requestDTO.ts @@ -503,6 +503,7 @@ class StartConversionJobRequestDTO extends HiCTAPIRequestDTO(key: string, value: { value: T }) => { export const useUiSettingsStore = defineStore("uiSettings", () => { const customZoomSliderEnabled = ref(false); const binaryTileTransportEnabled = ref(false); + const rulerCoordinateMode = ref<"global" | "contig" | "scaffold">( + readStored("hict.ui.rulerCoordinateMode", "global") + ); const fileSelectorMode = ref<"explorer" | "tree">( readStored("hict.ui.fileSelectorMode", "explorer") ); @@ -68,6 +71,7 @@ export const useUiSettingsStore = defineStore("uiSettings", () => { ); persistRef("hict.ui.fileSelectorMode", fileSelectorMode); + persistRef("hict.ui.rulerCoordinateMode", rulerCoordinateMode); persistRef("hict.ui.osd.visible", osdOverlayVisible); persistRef("hict.ui.osd.position", osdOverlayPosition); persistRef("hict.ui.osd.fields", osdOverlayFields); @@ -76,6 +80,7 @@ export const useUiSettingsStore = defineStore("uiSettings", () => { return { customZoomSliderEnabled, binaryTileTransportEnabled, + rulerCoordinateMode, fileSelectorMode, inheritTrackBackgroundFromMap, trackBackgroundColor, diff --git a/src/app/ui/components/common/FileSelectionTable.vue b/src/app/ui/components/common/FileSelectionTable.vue index 8dab452..cfe8ced 100644 --- a/src/app/ui/components/common/FileSelectionTable.vue +++ b/src/app/ui/components/common/FileSelectionTable.vue @@ -273,17 +273,19 @@ const emitSelectedPaths = (selected: Set): void => { }; const onRowClick = (event: { data?: FileSelectionTableRow; originalEvent?: Event }): void => { - if (event.data?.path) { - selectPath(event.data.path, event.originalEvent as MouseEvent | undefined); + const path = event.data?.path; + if (path !== undefined) { + selectPath(path, event.originalEvent as MouseEvent | undefined); } }; const onRowDoubleClick = (event: { data?: FileSelectionTableRow }): void => { - if (event.data?.path) { + const path = event.data?.path; + if (path !== undefined) { if (!props.multiSelect) { - selectPath(event.data.path); + selectPath(path); } - emit("activate", event.data.path); + emit("activate", path); } }; diff --git a/src/app/ui/components/sidebar/VisualziationSettingsEditor.vue b/src/app/ui/components/sidebar/VisualziationSettingsEditor.vue index cd450da..a156e92 100644 --- a/src/app/ui/components/sidebar/VisualziationSettingsEditor.vue +++ b/src/app/ui/components/sidebar/VisualziationSettingsEditor.vue @@ -232,13 +232,13 @@ const lowerSliderValue = computed(() => toSliderPosition(lowerBound.value)); const upperSliderValue = computed(() => toSliderPosition(upperBound.value)); const sliderStep = computed(() => { if (sliderLogScale.value) { - return 0.001; + return 1e-12; } const span = Math.max( sliderPositionMax.value - sliderPositionMin.value, - Number.EPSILON + Number.MIN_VALUE ); - return Math.max(span / 1000, Number.EPSILON); + return Math.max(span / 1_000_000, Number.MIN_VALUE); }); const thresholdSliderVars = computed>(() => ({ "--lower-thumb-color": fromColor.value.RGBA, diff --git a/src/app/ui/components/upper_ribbon/HeaderRibbon.vue b/src/app/ui/components/upper_ribbon/HeaderRibbon.vue index aef1e0a..2832fcc 100644 --- a/src/app/ui/components/upper_ribbon/HeaderRibbon.vue +++ b/src/app/ui/components/upper_ribbon/HeaderRibbon.vue @@ -131,26 +131,37 @@
+ - -
-
@@ -750,7 +780,9 @@ const saving = ref(false); const gatewayAddress: Ref = ref("http://localhost:5000/"); const aboutOpen = ref(false); const osdSettingsOpen = ref(false); -const aboutActiveTab = ref<"about" | "attribution">("about"); +const aboutActiveTab = ref<"about" | "attribution" | "changelog">("about"); +const changelogText = ref("Changelog for this build was not detected"); +const changelogLoading = ref(false); const pendingFastaFilename = ref(null); const fastaLinkReport = ref(null); const pendingJuiceboxAssemblyFilename = ref(""); @@ -1213,6 +1245,7 @@ function openAbout(): void { aboutOpen.value = true; aboutActiveTab.value = "about"; backendVersion.value = "loading..."; + loadChangelog(); props.networkManager.requestManager .getBackendVersion() .then((v) => { @@ -1226,6 +1259,21 @@ function openAbout(): void { .catch(() => (backendVersion.value = "unknown")); } +function loadChangelog(): void { + changelogLoading.value = true; + props.networkManager.requestManager + .getChangelog() + .then((text) => { + changelogText.value = text || "Changelog for this build was not detected"; + }) + .catch(() => { + changelogText.value = "Changelog for this build was not detected"; + }) + .finally(() => { + changelogLoading.value = false; + }); +} + function onOpenFASTAFile() { pendingFastaFilename.value = null; fastaLinkReport.value = null; @@ -1706,6 +1754,54 @@ function onAssemblyAGPRequest() { gap: 4px; } +.ask-authors { + margin-top: 16px; +} + +.ask-authors h3 { + font-size: 15px; + margin: 0 0 8px; +} + +.ask-authors-grid { + display: grid; + gap: 10px; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); +} + +.ask-authors-grid article { + background: var(--hict-control-bg, rgba(248, 249, 250, 0.88)); + border: 1px solid var(--hict-surface-border, rgba(15, 23, 38, 0.12)); + border-radius: 8px; + display: grid; + gap: 3px; + padding: 10px 12px; +} + +.ask-authors-title { + font-weight: 700; +} + +.ask-authors a { + color: #0d6efd; + overflow-wrap: anywhere; + text-decoration: none; +} + +.about-changelog { + background: var(--hict-control-bg, #f8f9fa); + border: 1px solid var(--hict-surface-border, rgba(15, 23, 38, 0.12)); + border-radius: 6px; + color: var(--hict-surface-fg, #1f2937); + font-family: inherit; + font-size: 13px; + margin: 0; + max-height: 62vh; + overflow: auto; + padding: 12px; + white-space: pre-wrap; +} + .attribution-panel { display: grid; gap: 18px; From 09b8ce7c63d19823b9e6d4c8adc71763261815c8 Mon Sep 17 00:00:00 2001 From: Alexander Serdyukov Date: Tue, 16 Jun 2026 03:01:45 +0400 Subject: [PATCH 5/9] Fix resolution settings in View menu, added changelog version stamping --- .../components/upper_ribbon/NavigationBar.vue | 152 ++++++++++++------ 1 file changed, 102 insertions(+), 50 deletions(-) diff --git a/src/app/ui/components/upper_ribbon/NavigationBar.vue b/src/app/ui/components/upper_ribbon/NavigationBar.vue index 2c3bdbb..55259de 100644 --- a/src/app/ui/components/upper_ribbon/NavigationBar.vue +++ b/src/app/ui/components/upper_ribbon/NavigationBar.vue @@ -144,52 +144,13 @@ >OSD settings... -
  • + Restrict Resolutions...
  • +
    + +
    From 6bd01f6713868c324ec78adc4d79e8649e10a7a3 Mon Sep 17 00:00:00 2001 From: Alexander Serdyukov Date: Wed, 17 Jun 2026 01:46:33 +0400 Subject: [PATCH 7/9] HiCT->Cooler converter fixed --- src/app/core/net/api/request.ts | 4 +++ src/app/core/net/dto/requestDTO.ts | 4 +++ .../upper_ribbon/MatrixExportModal.vue | 32 +++++++++++++++++-- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/app/core/net/api/request.ts b/src/app/core/net/api/request.ts index ad5d4b6..120f8bd 100644 --- a/src/app/core/net/api/request.ts +++ b/src/app/core/net/api/request.ts @@ -202,6 +202,8 @@ class StartConversionJobRequest implements HiCTAPIRequest { readonly compressionAlgorithm?: string; readonly chunkSize?: number; readonly parallelism?: number; + readonly exportMode?: string; + readonly exportAllResolutions?: boolean; readonly binTableFilename?: string; readonly chromSizesFilename?: string; readonly binSize?: number; @@ -226,6 +228,8 @@ class StartBatchConversionJobsRequest implements HiCTAPIRequest { readonly compression?: number; readonly compressionAlgorithm?: string; readonly chunkSize?: number; + readonly exportMode?: string; + readonly exportAllResolutions?: boolean; readonly binTableFilename?: string; readonly chromSizesFilename?: string; readonly binSize?: number; diff --git a/src/app/core/net/dto/requestDTO.ts b/src/app/core/net/dto/requestDTO.ts index c9e8b51..3205e7b 100644 --- a/src/app/core/net/dto/requestDTO.ts +++ b/src/app/core/net/dto/requestDTO.ts @@ -511,6 +511,8 @@ class StartConversionJobRequestDTO extends HiCTAPIRequestDTO
    +
    + + +
    -
    +
    -
    +
    +
    + + +
    + + Disabled by default: only the finest resolution is exported, so you can zoomify later if needed. + +
    (null); const submitting = ref(false); const errorMessage = ref(""); @@ -289,6 +315,8 @@ async function startExport(): Promise { compression: compression.value, compressionAlgorithm: compressionAlgorithm.value, parallelism: parallelism.value, + exportMode: exportMode.value, + exportAllResolutions: exportAllResolutions.value, }) ); jobId.value = response.jobId; From 31467f121de00f6198635f84153c4b37c50bcdcf Mon Sep 17 00:00:00 2001 From: Alexander Serdyukov Date: Wed, 17 Jun 2026 03:19:43 +0400 Subject: [PATCH 8/9] Intermediate upgrades to HiCT/Cooler converters --- src/app/core/net/api/request.ts | 2 ++ src/app/core/net/dto/requestDTO.ts | 2 ++ .../ui/components/upper_ribbon/FileWizardModal.vue | 11 +++++++++++ 3 files changed, 15 insertions(+) diff --git a/src/app/core/net/api/request.ts b/src/app/core/net/api/request.ts index 120f8bd..cc35c46 100644 --- a/src/app/core/net/api/request.ts +++ b/src/app/core/net/api/request.ts @@ -204,6 +204,7 @@ class StartConversionJobRequest implements HiCTAPIRequest { readonly parallelism?: number; readonly exportMode?: string; readonly exportAllResolutions?: boolean; + readonly buildResolutionPyramid?: boolean; readonly binTableFilename?: string; readonly chromSizesFilename?: string; readonly binSize?: number; @@ -230,6 +231,7 @@ class StartBatchConversionJobsRequest implements HiCTAPIRequest { readonly chunkSize?: number; readonly exportMode?: string; readonly exportAllResolutions?: boolean; + readonly buildResolutionPyramid?: boolean; readonly binTableFilename?: string; readonly chromSizesFilename?: string; readonly binSize?: number; diff --git a/src/app/core/net/dto/requestDTO.ts b/src/app/core/net/dto/requestDTO.ts index 3205e7b..993ae15 100644 --- a/src/app/core/net/dto/requestDTO.ts +++ b/src/app/core/net/dto/requestDTO.ts @@ -513,6 +513,7 @@ class StartConversionJobRequestDTO extends HiCTAPIRequestDTO
    +
    + + +
    + Enabled by default. HiCT will zoomify and balance converted Cooler inputs before import so sparse or single-resolution files open with a complete set of map resolutions. +
    +
    {{ toolchainStatus.summary }}
    @@ -903,6 +912,7 @@ const fixableIssuePolicy = reactive>({}); const precomputeTracks = ref(true); const forceTrackPrecompute = ref(false); const dropCachesBeforeRun = ref(false); +const buildResolutionPyramid = ref(true); const blendMode = ref("OVER"); const topOpacity = ref(0.5); const bottomOpacity = ref(1.0); @@ -1759,6 +1769,7 @@ const ensureOpenedFilename = async (source: SourceDraft): Promise => { chromSizesFilename: source.chromSizesFilename || undefined, binSize: source.binSize ?? undefined, countAsFloat: source.countAsFloat || undefined, + buildResolutionPyramid: buildResolutionPyramid.value, }) ); const finishedJob = await waitForConversionJob(started.jobId); From 9129e8bbe583826c21635dcb4b06a313ef7341d3 Mon Sep 17 00:00:00 2001 From: Alexander Serdyukov Date: Wed, 17 Jun 2026 14:43:46 +0400 Subject: [PATCH 9/9] Updated changelog rendering --- .../components/upper_ribbon/NavigationBar.vue | 77 ++++++++++++++++++- src/env.d.ts | 14 ++++ 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/src/app/ui/components/upper_ribbon/NavigationBar.vue b/src/app/ui/components/upper_ribbon/NavigationBar.vue index 55259de..08364f5 100644 --- a/src/app/ui/components/upper_ribbon/NavigationBar.vue +++ b/src/app/ui/components/upper_ribbon/NavigationBar.vue @@ -679,7 +679,11 @@ Loading changelog...
    -
    {{ changelogText }}
    +
    @@ -758,6 +762,7 @@