nuxt-cornerstone 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +933 -0
- package/dist/module.d.mts +22 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +168 -0
- package/dist/runtime/annotation-json.d.ts +76 -0
- package/dist/runtime/annotation-json.js +176 -0
- package/dist/runtime/components/CornerstoneViewport.d.vue.ts +57 -0
- package/dist/runtime/components/CornerstoneViewport.vue +227 -0
- package/dist/runtime/components/CornerstoneViewport.vue.d.ts +57 -0
- package/dist/runtime/composables/useAnnotationReport.d.ts +28 -0
- package/dist/runtime/composables/useAnnotationReport.js +66 -0
- package/dist/runtime/composables/useCinePlayer.d.ts +52 -0
- package/dist/runtime/composables/useCinePlayer.js +95 -0
- package/dist/runtime/composables/useCornerstone.d.ts +17 -0
- package/dist/runtime/composables/useCornerstone.js +30 -0
- package/dist/runtime/composables/useCornerstoneI18n.d.ts +24 -0
- package/dist/runtime/composables/useCornerstoneI18n.js +32 -0
- package/dist/runtime/composables/useCornerstoneTools.d.ts +21 -0
- package/dist/runtime/composables/useCornerstoneTools.js +105 -0
- package/dist/runtime/composables/useDicomAnnotations.d.ts +76 -0
- package/dist/runtime/composables/useDicomAnnotations.js +220 -0
- package/dist/runtime/composables/useDicomFiles.d.ts +87 -0
- package/dist/runtime/composables/useDicomFiles.js +234 -0
- package/dist/runtime/composables/useDicomGuard.d.ts +24 -0
- package/dist/runtime/composables/useDicomGuard.js +30 -0
- package/dist/runtime/composables/useDicomStudy.d.ts +122 -0
- package/dist/runtime/composables/useDicomStudy.js +222 -0
- package/dist/runtime/composables/useImagePrefetch.d.ts +78 -0
- package/dist/runtime/composables/useImagePrefetch.js +119 -0
- package/dist/runtime/composables/useMeasurements.d.ts +51 -0
- package/dist/runtime/composables/useMeasurements.js +138 -0
- package/dist/runtime/composables/useRenderingEngine.d.ts +6 -0
- package/dist/runtime/composables/useRenderingEngine.js +46 -0
- package/dist/runtime/composables/useStackCine.d.ts +50 -0
- package/dist/runtime/composables/useStackCine.js +62 -0
- package/dist/runtime/composables/useViewerShortcuts.d.ts +94 -0
- package/dist/runtime/composables/useViewerShortcuts.js +115 -0
- package/dist/runtime/cornerstone.d.ts +21 -0
- package/dist/runtime/cornerstone.js +100 -0
- package/dist/runtime/dicom-instances.d.ts +32 -0
- package/dist/runtime/dicom-instances.js +15 -0
- package/dist/runtime/dicom-zip.d.ts +95 -0
- package/dist/runtime/dicom-zip.js +149 -0
- package/dist/runtime/i18n/detect.d.ts +37 -0
- package/dist/runtime/i18n/detect.js +37 -0
- package/dist/runtime/i18n/index.d.ts +61 -0
- package/dist/runtime/i18n/index.js +145 -0
- package/dist/runtime/i18n/messages.d.ts +122 -0
- package/dist/runtime/i18n/messages.js +147 -0
- package/dist/runtime/plugin.client.d.ts +13 -0
- package/dist/runtime/plugin.client.js +31 -0
- package/dist/runtime/plugin.i18n.d.ts +12 -0
- package/dist/runtime/plugin.i18n.js +15 -0
- package/dist/runtime/types.d.ts +150 -0
- package/dist/runtime/types.js +10 -0
- package/dist/types.d.mts +29 -0
- package/package.json +76 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export interface PrefetchOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Images decoded at once. Default: `4`.
|
|
4
|
+
*
|
|
5
|
+
* Decoding happens in the web worker pool, so this is really a ceiling on
|
|
6
|
+
* how much of that pool the prefetch is allowed to hold. Too high and the
|
|
7
|
+
* slice the user just scrolled to queues behind twenty they have not asked
|
|
8
|
+
* for yet.
|
|
9
|
+
*/
|
|
10
|
+
concurrency?: number;
|
|
11
|
+
/**
|
|
12
|
+
* Index to start from. Default: `0`.
|
|
13
|
+
*
|
|
14
|
+
* Pass the slice that is on screen: with the default `order`, loading fans
|
|
15
|
+
* out from there, so scrolling either way finds images already decoded.
|
|
16
|
+
*/
|
|
17
|
+
from?: number;
|
|
18
|
+
/**
|
|
19
|
+
* - `'outward'` (default) alternates either side of {@link PrefetchOptions.from}.
|
|
20
|
+
* - `'forward'` runs from `from` to the end of the stack, which is the order
|
|
21
|
+
* cine playback consumes them in.
|
|
22
|
+
*/
|
|
23
|
+
order?: 'outward' | 'forward';
|
|
24
|
+
/** Passed to Cornerstone's loader. Lower runs sooner. Default: `0`. */
|
|
25
|
+
priority?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Passed to Cornerstone's loader, as `Enums.RequestType`. Default:
|
|
28
|
+
* `'prefetch'`, which is the class the request pool serves after anything
|
|
29
|
+
* the user is waiting on.
|
|
30
|
+
*/
|
|
31
|
+
requestType?: string;
|
|
32
|
+
/** Called after each image settles. */
|
|
33
|
+
onProgress?: (progress: PrefetchProgress) => void;
|
|
34
|
+
}
|
|
35
|
+
export interface PrefetchProgress {
|
|
36
|
+
/** Images in the cache — decoded by this run, or already there when it began. */
|
|
37
|
+
loaded: number;
|
|
38
|
+
/** Images that could not be decoded. */
|
|
39
|
+
failed: number;
|
|
40
|
+
/** Images this run was asked for. */
|
|
41
|
+
total: number;
|
|
42
|
+
}
|
|
43
|
+
export interface PrefetchResult extends PrefetchProgress {
|
|
44
|
+
/** True when {@link useImagePrefetch.cancel} or a newer run stopped this one. */
|
|
45
|
+
cancelled: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* True when the image cache ran out of room and the rest of the stack was
|
|
48
|
+
* left alone. What was decoded stays decoded; raise the ceiling with
|
|
49
|
+
* `cache.setMaxCacheSize()` if a whole study has to fit.
|
|
50
|
+
*/
|
|
51
|
+
cacheFull: boolean;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Decode a stack into Cornerstone's image cache ahead of time.
|
|
55
|
+
*
|
|
56
|
+
* A stack viewport loads each slice the moment it is shown, which is right for
|
|
57
|
+
* scrolling and wrong for anything that moves on its own: at 15 frames a
|
|
58
|
+
* second the decode never keeps up and playback stutters through whatever
|
|
59
|
+
* happens to be cached. Preparing the stack first turns every later frame
|
|
60
|
+
* change into a cache hit.
|
|
61
|
+
*
|
|
62
|
+
* State is per call, so a component that prepares two stacks tracks them with
|
|
63
|
+
* two instances. One instance runs one prepare at a time: a second call cancels
|
|
64
|
+
* the first, which is what you want when the user switches series mid-load.
|
|
65
|
+
*/
|
|
66
|
+
export declare function useImagePrefetch(): {
|
|
67
|
+
loaded: import("vue").Ref<number, number>;
|
|
68
|
+
failed: import("vue").Ref<number, number>;
|
|
69
|
+
total: import("vue").Ref<number, number>;
|
|
70
|
+
pending: import("vue").Ref<boolean, boolean>;
|
|
71
|
+
progress: import("vue").ComputedRef<number>;
|
|
72
|
+
complete: import("vue").ComputedRef<boolean>;
|
|
73
|
+
cacheFull: import("vue").Ref<boolean, boolean>;
|
|
74
|
+
prepare: (imageIds: string[], options?: PrefetchOptions) => Promise<PrefetchResult>;
|
|
75
|
+
cancel: () => void;
|
|
76
|
+
reset: () => void;
|
|
77
|
+
isPrepared: (imageId: string) => boolean;
|
|
78
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { computed, getCurrentScope, onScopeDispose, ref } from "vue";
|
|
2
|
+
import { ensureCornerstone, getLoadedCornerstone } from "../cornerstone.js";
|
|
3
|
+
const DEFAULT_CONCURRENCY = 4;
|
|
4
|
+
export function useImagePrefetch() {
|
|
5
|
+
const loaded = ref(0);
|
|
6
|
+
const failed = ref(0);
|
|
7
|
+
const total = ref(0);
|
|
8
|
+
const pending = ref(false);
|
|
9
|
+
const cacheFull = ref(false);
|
|
10
|
+
const progress = computed(() => {
|
|
11
|
+
if (total.value === 0) return 1;
|
|
12
|
+
return (loaded.value + failed.value) / total.value;
|
|
13
|
+
});
|
|
14
|
+
const complete = computed(() => !pending.value && loaded.value + failed.value >= total.value);
|
|
15
|
+
let run = null;
|
|
16
|
+
async function prepare(imageIds, options = {}) {
|
|
17
|
+
const {
|
|
18
|
+
concurrency = DEFAULT_CONCURRENCY,
|
|
19
|
+
from = 0,
|
|
20
|
+
order = "outward",
|
|
21
|
+
priority = 0,
|
|
22
|
+
onProgress
|
|
23
|
+
} = options;
|
|
24
|
+
cancel();
|
|
25
|
+
const { core } = await ensureCornerstone();
|
|
26
|
+
const { cache, imageLoader, Enums } = core;
|
|
27
|
+
const requestType = options.requestType ?? Enums.RequestType.Prefetch;
|
|
28
|
+
const current = { cancelled: false, cacheFull: false, inFlight: /* @__PURE__ */ new Set() };
|
|
29
|
+
run = current;
|
|
30
|
+
const queue = orderImageIds(imageIds, from, order);
|
|
31
|
+
loaded.value = 0;
|
|
32
|
+
failed.value = 0;
|
|
33
|
+
total.value = queue.length;
|
|
34
|
+
cacheFull.value = false;
|
|
35
|
+
pending.value = queue.length > 0;
|
|
36
|
+
function report() {
|
|
37
|
+
onProgress?.({ loaded: loaded.value, failed: failed.value, total: total.value });
|
|
38
|
+
}
|
|
39
|
+
report();
|
|
40
|
+
let cursor = 0;
|
|
41
|
+
async function worker() {
|
|
42
|
+
while (cursor < queue.length) {
|
|
43
|
+
if (current.cancelled) return;
|
|
44
|
+
const imageId = queue[cursor++];
|
|
45
|
+
if (cache.isLoaded(imageId)) {
|
|
46
|
+
loaded.value += 1;
|
|
47
|
+
report();
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (cache.getBytesAvailable() <= 0) {
|
|
51
|
+
current.cacheFull = true;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
current.inFlight.add(imageId);
|
|
55
|
+
try {
|
|
56
|
+
await imageLoader.loadAndCacheImage(imageId, { priority, requestType });
|
|
57
|
+
loaded.value += 1;
|
|
58
|
+
} catch {
|
|
59
|
+
if (!current.cancelled) failed.value += 1;
|
|
60
|
+
} finally {
|
|
61
|
+
current.inFlight.delete(imageId);
|
|
62
|
+
}
|
|
63
|
+
report();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
await Promise.all(
|
|
67
|
+
Array.from({ length: Math.min(Math.max(1, concurrency), queue.length) }, () => worker())
|
|
68
|
+
);
|
|
69
|
+
if (run === current) {
|
|
70
|
+
run = null;
|
|
71
|
+
pending.value = false;
|
|
72
|
+
cacheFull.value = current.cacheFull;
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
loaded: loaded.value,
|
|
76
|
+
failed: failed.value,
|
|
77
|
+
total: total.value,
|
|
78
|
+
cancelled: current.cancelled,
|
|
79
|
+
cacheFull: current.cacheFull
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function cancel() {
|
|
83
|
+
const current = run;
|
|
84
|
+
if (!current) return;
|
|
85
|
+
current.cancelled = true;
|
|
86
|
+
run = null;
|
|
87
|
+
pending.value = false;
|
|
88
|
+
if (current.inFlight.size > 0) {
|
|
89
|
+
getLoadedCornerstone()?.core.imageLoader.cancelLoadImages([...current.inFlight]);
|
|
90
|
+
current.inFlight.clear();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function reset() {
|
|
94
|
+
cancel();
|
|
95
|
+
loaded.value = 0;
|
|
96
|
+
failed.value = 0;
|
|
97
|
+
total.value = 0;
|
|
98
|
+
cacheFull.value = false;
|
|
99
|
+
}
|
|
100
|
+
function isPrepared(imageId) {
|
|
101
|
+
return getLoadedCornerstone()?.core.cache.isLoaded(imageId) ?? false;
|
|
102
|
+
}
|
|
103
|
+
if (getCurrentScope()) onScopeDispose(cancel);
|
|
104
|
+
return { loaded, failed, total, pending, progress, complete, cacheFull, prepare, cancel, reset, isPrepared };
|
|
105
|
+
}
|
|
106
|
+
function orderImageIds(imageIds, from, order) {
|
|
107
|
+
const count = imageIds.length;
|
|
108
|
+
if (count === 0) return [];
|
|
109
|
+
const start = Math.min(Math.max(Math.trunc(from) || 0, 0), count - 1);
|
|
110
|
+
if (order === "forward") return imageIds.slice(start).concat(imageIds.slice(0, start));
|
|
111
|
+
const queue = [imageIds[start]];
|
|
112
|
+
for (let step = 1; queue.length < count; step++) {
|
|
113
|
+
const after = start + step;
|
|
114
|
+
const before = start - step;
|
|
115
|
+
if (after < count) queue.push(imageIds[after]);
|
|
116
|
+
if (before >= 0) queue.push(imageIds[before]);
|
|
117
|
+
}
|
|
118
|
+
return queue;
|
|
119
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { MaybeRefOrGetter, Ref } from 'vue';
|
|
2
|
+
import type { StackViewport } from '../types.js';
|
|
3
|
+
/** One measurement the user drew, as the delete controls see it. */
|
|
4
|
+
export interface Measurement {
|
|
5
|
+
uid: string;
|
|
6
|
+
/** The `toolName` that drew it — `'Length'`, `'RectangleROI'`, and so on. */
|
|
7
|
+
toolName: string;
|
|
8
|
+
/** The slice it is pinned to, or `null` when it is not pinned to one. */
|
|
9
|
+
imageId: string | null;
|
|
10
|
+
selected: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface MeasurementsOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Tool names whose annotations are left alone, neither counted nor deleted.
|
|
15
|
+
*
|
|
16
|
+
* The overlay tool that {@link useDicomAnnotations} draws imported boxes
|
|
17
|
+
* with is always in this set: those boxes belong to a report rather than to
|
|
18
|
+
* the reader, and `useAnnotationReport().reset()` is what takes them away.
|
|
19
|
+
* Locked annotations are skipped for the same reason, whatever drew them.
|
|
20
|
+
*/
|
|
21
|
+
keep?: string[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Deleting the measurements the user drew.
|
|
25
|
+
*
|
|
26
|
+
* Cornerstone gives every annotation tool a way to *draw* and no way to
|
|
27
|
+
* un-draw: a stray Length or a mis-clicked Probe stays on the slice for as
|
|
28
|
+
* long as the study is open. This is the other half — the counts a toolbar
|
|
29
|
+
* needs to decide what to offer, and the three removals worth offering:
|
|
30
|
+
* whatever is selected, everything on the slice on screen, and everything in
|
|
31
|
+
* the stack.
|
|
32
|
+
*
|
|
33
|
+
* Only the reader's own work is in scope. Boxes that came out of a report are
|
|
34
|
+
* excluded by tool name and anything locked is excluded outright, so a viewer
|
|
35
|
+
* can put a delete button next to an imported overlay without the button being
|
|
36
|
+
* able to erase it.
|
|
37
|
+
*
|
|
38
|
+
* Counts follow the annotation state rather than being recomputed by the
|
|
39
|
+
* caller: they move when the user draws, deletes, selects or scrolls.
|
|
40
|
+
*/
|
|
41
|
+
export declare function useMeasurements(source: MaybeRefOrGetter<StackViewport | null | undefined>, options?: MeasurementsOptions): {
|
|
42
|
+
list: () => Measurement[];
|
|
43
|
+
refresh: () => void;
|
|
44
|
+
remove: (uid: string) => boolean;
|
|
45
|
+
deleteSelected: () => number;
|
|
46
|
+
deleteOnSlice: () => number;
|
|
47
|
+
deleteAll: () => number;
|
|
48
|
+
total: Ref<number, number>;
|
|
49
|
+
onSlice: Ref<number, number>;
|
|
50
|
+
selected: Ref<number, number>;
|
|
51
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { getCurrentScope, onScopeDispose, ref, toValue, watch } from "vue";
|
|
2
|
+
import { getLoadedCornerstone } from "../cornerstone.js";
|
|
3
|
+
import { OVERLAY_TOOL_NAME } from "./useDicomAnnotations.js";
|
|
4
|
+
export function useMeasurements(source, options = {}) {
|
|
5
|
+
const kept = /* @__PURE__ */ new Set([OVERLAY_TOOL_NAME, ...options.keep ?? []]);
|
|
6
|
+
const total = ref(0);
|
|
7
|
+
const onSlice = ref(0);
|
|
8
|
+
const selected = ref(0);
|
|
9
|
+
let libs = null;
|
|
10
|
+
let listening = null;
|
|
11
|
+
let subscribed = false;
|
|
12
|
+
function annotationEvents() {
|
|
13
|
+
const { Events } = libs.tools.Enums;
|
|
14
|
+
return [
|
|
15
|
+
Events.ANNOTATION_ADDED,
|
|
16
|
+
Events.ANNOTATION_REMOVED,
|
|
17
|
+
Events.ANNOTATION_SELECTION_CHANGE,
|
|
18
|
+
Events.ANNOTATION_LOCK_CHANGE
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
function list() {
|
|
22
|
+
const viewport = toValue(source);
|
|
23
|
+
if (!libs || !viewport) return [];
|
|
24
|
+
const { tools } = libs;
|
|
25
|
+
const imageIds = new Set(viewport.getImageIds());
|
|
26
|
+
const frameOfReferenceUid = viewport.getFrameOfReferenceUID();
|
|
27
|
+
const found = [];
|
|
28
|
+
for (const annotation of tools.annotation.state.getAllAnnotations()) {
|
|
29
|
+
const uid = annotation.annotationUID;
|
|
30
|
+
const metadata = annotation.metadata;
|
|
31
|
+
const toolName = metadata?.toolName;
|
|
32
|
+
if (!uid || !toolName || kept.has(toolName)) continue;
|
|
33
|
+
if (tools.annotation.locking.isAnnotationLocked(uid)) continue;
|
|
34
|
+
const imageId = metadata.referencedImageId ?? null;
|
|
35
|
+
const belongs = imageId ? imageIds.has(imageId) : metadata.FrameOfReferenceUID === frameOfReferenceUid;
|
|
36
|
+
if (!belongs) continue;
|
|
37
|
+
found.push({
|
|
38
|
+
uid,
|
|
39
|
+
toolName,
|
|
40
|
+
imageId,
|
|
41
|
+
selected: tools.annotation.selection.isAnnotationSelected(uid)
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return found;
|
|
45
|
+
}
|
|
46
|
+
function refresh() {
|
|
47
|
+
const measurements = list();
|
|
48
|
+
const current = toValue(source)?.getCurrentImageId() ?? null;
|
|
49
|
+
total.value = measurements.length;
|
|
50
|
+
onSlice.value = current ? measurements.filter((entry) => entry.imageId === current).length : 0;
|
|
51
|
+
selected.value = measurements.filter((entry) => entry.selected).length;
|
|
52
|
+
}
|
|
53
|
+
function render() {
|
|
54
|
+
const viewport = toValue(source);
|
|
55
|
+
if (viewport) libs?.tools.utilities.triggerAnnotationRenderForViewportIds([viewport.id]);
|
|
56
|
+
}
|
|
57
|
+
function removeMany(uids) {
|
|
58
|
+
if (!libs || uids.length === 0) return 0;
|
|
59
|
+
const { tools } = libs;
|
|
60
|
+
for (const uid of uids) {
|
|
61
|
+
if (tools.annotation.selection.isAnnotationSelected(uid)) {
|
|
62
|
+
tools.annotation.selection.deselectAnnotation(uid);
|
|
63
|
+
}
|
|
64
|
+
tools.annotation.state.removeAnnotation(uid);
|
|
65
|
+
}
|
|
66
|
+
refresh();
|
|
67
|
+
render();
|
|
68
|
+
return uids.length;
|
|
69
|
+
}
|
|
70
|
+
function remove(uid) {
|
|
71
|
+
return removeMany(list().filter((entry) => entry.uid === uid).map((entry) => entry.uid)) > 0;
|
|
72
|
+
}
|
|
73
|
+
function deleteSelected() {
|
|
74
|
+
return removeMany(list().filter((entry) => entry.selected).map((entry) => entry.uid));
|
|
75
|
+
}
|
|
76
|
+
function deleteOnSlice() {
|
|
77
|
+
const current = toValue(source)?.getCurrentImageId();
|
|
78
|
+
if (!current) return 0;
|
|
79
|
+
return removeMany(list().filter((entry) => entry.imageId === current).map((entry) => entry.uid));
|
|
80
|
+
}
|
|
81
|
+
function deleteAll() {
|
|
82
|
+
return removeMany(list().map((entry) => entry.uid));
|
|
83
|
+
}
|
|
84
|
+
function subscribe() {
|
|
85
|
+
if (subscribed || !libs) return;
|
|
86
|
+
for (const event of annotationEvents()) {
|
|
87
|
+
libs.core.eventTarget.addEventListener(event, refresh);
|
|
88
|
+
}
|
|
89
|
+
subscribed = true;
|
|
90
|
+
}
|
|
91
|
+
function unsubscribe() {
|
|
92
|
+
if (!subscribed || !libs) return;
|
|
93
|
+
for (const event of annotationEvents()) {
|
|
94
|
+
libs.core.eventTarget.removeEventListener(event, refresh);
|
|
95
|
+
}
|
|
96
|
+
subscribed = false;
|
|
97
|
+
}
|
|
98
|
+
function watchElement(viewport) {
|
|
99
|
+
const element = viewport.element;
|
|
100
|
+
if (listening === element) return;
|
|
101
|
+
stopWatching();
|
|
102
|
+
element.addEventListener(libs.core.Enums.Events.STACK_NEW_IMAGE, refresh);
|
|
103
|
+
listening = element;
|
|
104
|
+
}
|
|
105
|
+
function stopWatching() {
|
|
106
|
+
if (!listening || !libs) return;
|
|
107
|
+
listening.removeEventListener(libs.core.Enums.Events.STACK_NEW_IMAGE, refresh);
|
|
108
|
+
listening = null;
|
|
109
|
+
}
|
|
110
|
+
watch(() => toValue(source), (viewport) => {
|
|
111
|
+
libs ??= getLoadedCornerstone();
|
|
112
|
+
if (!libs || !viewport) {
|
|
113
|
+
stopWatching();
|
|
114
|
+
refresh();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
subscribe();
|
|
118
|
+
watchElement(viewport);
|
|
119
|
+
refresh();
|
|
120
|
+
}, { immediate: true });
|
|
121
|
+
if (getCurrentScope()) {
|
|
122
|
+
onScopeDispose(() => {
|
|
123
|
+
unsubscribe();
|
|
124
|
+
stopWatching();
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
list,
|
|
129
|
+
refresh,
|
|
130
|
+
remove,
|
|
131
|
+
deleteSelected,
|
|
132
|
+
deleteOnSlice,
|
|
133
|
+
deleteAll,
|
|
134
|
+
total,
|
|
135
|
+
onSlice,
|
|
136
|
+
selected
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Types as CoreTypes } from '@cornerstonejs/core';
|
|
2
|
+
export declare function useRenderingEngine(): {
|
|
3
|
+
acquire: (id?: string) => Promise<CoreTypes.IRenderingEngine>;
|
|
4
|
+
release: (id?: string) => void;
|
|
5
|
+
get: (id?: string) => CoreTypes.IRenderingEngine | null;
|
|
6
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ensureCornerstone, getCornerstoneOptions } from "../cornerstone.js";
|
|
2
|
+
const hot = import.meta.hot;
|
|
3
|
+
const state = hot ? hot.data.cornerstoneEngines ??= {} : {};
|
|
4
|
+
state.engines ??= /* @__PURE__ */ new Map();
|
|
5
|
+
export function useRenderingEngine() {
|
|
6
|
+
async function acquire(id) {
|
|
7
|
+
const { core } = await ensureCornerstone();
|
|
8
|
+
const engineId = id ?? getCornerstoneOptions().renderingEngineId;
|
|
9
|
+
const engines = state.engines;
|
|
10
|
+
let entry = engines.get(engineId);
|
|
11
|
+
if (entry && !core.getRenderingEngine(engineId)) {
|
|
12
|
+
engines.delete(engineId);
|
|
13
|
+
entry = void 0;
|
|
14
|
+
}
|
|
15
|
+
if (!entry) {
|
|
16
|
+
const existing = core.getRenderingEngine(engineId);
|
|
17
|
+
entry = {
|
|
18
|
+
engine: existing ?? new core.RenderingEngine(engineId),
|
|
19
|
+
refs: 0,
|
|
20
|
+
owned: !existing
|
|
21
|
+
};
|
|
22
|
+
engines.set(engineId, entry);
|
|
23
|
+
}
|
|
24
|
+
entry.refs += 1;
|
|
25
|
+
return entry.engine;
|
|
26
|
+
}
|
|
27
|
+
function release(id) {
|
|
28
|
+
const engineId = id ?? getCornerstoneOptions().renderingEngineId;
|
|
29
|
+
const engines = state.engines;
|
|
30
|
+
const entry = engines.get(engineId);
|
|
31
|
+
if (!entry) return;
|
|
32
|
+
entry.refs = Math.max(0, entry.refs - 1);
|
|
33
|
+
if (entry.refs > 0) return;
|
|
34
|
+
engines.delete(engineId);
|
|
35
|
+
if (!entry.owned) return;
|
|
36
|
+
try {
|
|
37
|
+
entry.engine.destroy();
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function get(id) {
|
|
42
|
+
const engineId = id ?? getCornerstoneOptions().renderingEngineId;
|
|
43
|
+
return state.engines.get(engineId)?.engine ?? null;
|
|
44
|
+
}
|
|
45
|
+
return { acquire, release, get };
|
|
46
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Ref } from 'vue';
|
|
2
|
+
/**
|
|
3
|
+
* A reasonable set of frame rates for a transport to offer.
|
|
4
|
+
*
|
|
5
|
+
* 15 is the default because it is fast enough to read as motion and slow
|
|
6
|
+
* enough that a stack coming off the network can nearly keep up; the ones
|
|
7
|
+
* either side cover walking a stack slowly and playing a short cardiac loop at
|
|
8
|
+
* something close to its acquired rate.
|
|
9
|
+
*/
|
|
10
|
+
export declare const FRAME_RATES: readonly [5, 10, 15, 24, 30];
|
|
11
|
+
/**
|
|
12
|
+
* How much of the stack has to be decoded before play is offered.
|
|
13
|
+
*
|
|
14
|
+
* Waiting for all of it is more than playback needs: the film starts at the
|
|
15
|
+
* first image and the prefetch runs the same way, so half a stack is already
|
|
16
|
+
* a comfortable head start and the rest lands while the first half plays.
|
|
17
|
+
* Waiting for none of it is the stutter the prefetch exists to avoid.
|
|
18
|
+
*/
|
|
19
|
+
export declare const PLAY_READY_RATIO = 0.5;
|
|
20
|
+
export interface StackCineOptions {
|
|
21
|
+
/** Frames per second to start at. Default: `15`. */
|
|
22
|
+
frameRate?: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Cine playback for the stack on screen, and the prefetch that makes it
|
|
26
|
+
* watchable.
|
|
27
|
+
*
|
|
28
|
+
* {@link useCinePlayer} and {@link useImagePrefetch} are deliberately separate
|
|
29
|
+
* primitives, but an application almost always wants them together: a stack
|
|
30
|
+
* viewport loads each slice as it is shown, so playing a stack nobody has
|
|
31
|
+
* prepared runs at the speed of the decoder rather than at the frame rate that
|
|
32
|
+
* was asked for. Here preparing is not something the user has to think of
|
|
33
|
+
* first — a stack starts preparing as soon as it is loaded, and pressing play
|
|
34
|
+
* starts it too if for any reason it is not already done.
|
|
35
|
+
*/
|
|
36
|
+
export declare function useStackCine(imageIds: Ref<string[]>, imageIndex: Ref<number>, options?: StackCineOptions): {
|
|
37
|
+
playing: Ref<boolean, boolean>;
|
|
38
|
+
canPlay: import("vue").ComputedRef<boolean>;
|
|
39
|
+
frameRate: import("vue").WritableComputedRef<number, number>;
|
|
40
|
+
loop: Ref<boolean, boolean>;
|
|
41
|
+
preparing: Ref<boolean, boolean>;
|
|
42
|
+
prepared: import("vue").ComputedRef<boolean>;
|
|
43
|
+
buffering: import("vue").ComputedRef<boolean>;
|
|
44
|
+
percent: import("vue").ComputedRef<number>;
|
|
45
|
+
statusText: import("vue").ComputedRef<string | null>;
|
|
46
|
+
play: () => void;
|
|
47
|
+
pause: () => void;
|
|
48
|
+
toggle: () => void;
|
|
49
|
+
prepare: () => Promise<import("./useImagePrefetch.js").PrefetchResult>;
|
|
50
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { computed, watch } from "vue";
|
|
2
|
+
import { t } from "../i18n/index.js";
|
|
3
|
+
import { useCinePlayer } from "./useCinePlayer.js";
|
|
4
|
+
import { useImagePrefetch } from "./useImagePrefetch.js";
|
|
5
|
+
export const FRAME_RATES = [5, 10, 15, 24, 30];
|
|
6
|
+
export const PLAY_READY_RATIO = 0.5;
|
|
7
|
+
export function useStackCine(imageIds, imageIndex, options = {}) {
|
|
8
|
+
const prefetch = useImagePrefetch();
|
|
9
|
+
const cine = useCinePlayer(imageIndex, () => imageIds.value.length, {
|
|
10
|
+
frameRate: options.frameRate ?? 15
|
|
11
|
+
});
|
|
12
|
+
const percent = computed(() => Math.round(prefetch.progress.value * 100));
|
|
13
|
+
const prepared = computed(
|
|
14
|
+
() => prefetch.total.value > 0 && !prefetch.pending.value && !prefetch.cacheFull.value && prefetch.loaded.value + prefetch.failed.value >= prefetch.total.value
|
|
15
|
+
);
|
|
16
|
+
const buffering = computed(
|
|
17
|
+
() => prefetch.pending.value && prefetch.progress.value < PLAY_READY_RATIO
|
|
18
|
+
);
|
|
19
|
+
const statusText = computed(() => {
|
|
20
|
+
if (prefetch.pending.value) return t("cine.preparing", { percent: percent.value });
|
|
21
|
+
if (prefetch.total.value === 0) return null;
|
|
22
|
+
if (prefetch.cacheFull.value) return t("cine.cacheFull", { count: prefetch.loaded.value });
|
|
23
|
+
if (prefetch.failed.value > 0) {
|
|
24
|
+
return t("cine.preparedWithFailures", {
|
|
25
|
+
count: prefetch.loaded.value,
|
|
26
|
+
failed: prefetch.failed.value
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
if (prepared.value) return t("cine.prepared", { count: prefetch.loaded.value });
|
|
30
|
+
return null;
|
|
31
|
+
});
|
|
32
|
+
function prepare() {
|
|
33
|
+
return prefetch.prepare(imageIds.value, { from: imageIndex.value, order: "outward" });
|
|
34
|
+
}
|
|
35
|
+
function toggle() {
|
|
36
|
+
if (!cine.playing.value && buffering.value) return;
|
|
37
|
+
if (!cine.playing.value && !prepared.value && !prefetch.pending.value) {
|
|
38
|
+
prefetch.prepare(imageIds.value, { from: imageIndex.value, order: "forward" });
|
|
39
|
+
}
|
|
40
|
+
cine.toggle();
|
|
41
|
+
}
|
|
42
|
+
watch(imageIds, (ids) => {
|
|
43
|
+
cine.pause();
|
|
44
|
+
prefetch.reset();
|
|
45
|
+
if (ids.length > 1) prefetch.prepare(ids, { from: imageIndex.value, order: "forward" });
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
playing: cine.playing,
|
|
49
|
+
canPlay: cine.canPlay,
|
|
50
|
+
frameRate: cine.frameRate,
|
|
51
|
+
loop: cine.loop,
|
|
52
|
+
preparing: prefetch.pending,
|
|
53
|
+
prepared,
|
|
54
|
+
buffering,
|
|
55
|
+
percent,
|
|
56
|
+
statusText,
|
|
57
|
+
play: cine.play,
|
|
58
|
+
pause: cine.pause,
|
|
59
|
+
toggle,
|
|
60
|
+
prepare
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** A tool the keyboard can select, by its `@cornerstonejs/tools` class name. */
|
|
2
|
+
export interface ToolShortcut {
|
|
3
|
+
className: string;
|
|
4
|
+
/** A single letter, matched at its QWERTY position — see below. */
|
|
5
|
+
shortcut: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The letter bindings, following the OHIF viewer's defaults where it has them:
|
|
9
|
+
* `w`, `p` and `z` for window/level, pan and zoom. OHIF binds no keys for
|
|
10
|
+
* annotation tools, so the rest are conventional rather than specified — `m`
|
|
11
|
+
* for measure because `l` is rotate-left in OHIF, and `b` for the box because
|
|
12
|
+
* `r` is rotate-right.
|
|
13
|
+
*
|
|
14
|
+
* Pass your own list to {@link useViewerShortcuts} when your toolbar offers a
|
|
15
|
+
* different set; a toolbar and its keymap should read from one table, so that
|
|
16
|
+
* the key a button advertises is the key that works.
|
|
17
|
+
*/
|
|
18
|
+
export declare const DEFAULT_TOOL_SHORTCUTS: ToolShortcut[];
|
|
19
|
+
export interface ViewerShortcutHandlers {
|
|
20
|
+
/** False while nothing is loaded, so the keys stay inert on an empty viewer. */
|
|
21
|
+
isEnabled: () => boolean;
|
|
22
|
+
/** Move through the stack, positive forwards. */
|
|
23
|
+
step: (delta: number) => void;
|
|
24
|
+
first: () => void;
|
|
25
|
+
last: () => void;
|
|
26
|
+
/** Move through the archive's series, positive forwards. */
|
|
27
|
+
stepSeries: (delta: number) => void;
|
|
28
|
+
setTool: (className: string) => void;
|
|
29
|
+
resetViewport: () => void;
|
|
30
|
+
/** Start or stop cine playback. */
|
|
31
|
+
togglePlay: () => void;
|
|
32
|
+
/** Show the application's own list of these bindings. */
|
|
33
|
+
toggleHelp: () => void;
|
|
34
|
+
/**
|
|
35
|
+
* Delete the measurements the user has selected. Optional: a viewer with no
|
|
36
|
+
* annotation tools has nothing to delete, and leaving it out keeps Delete
|
|
37
|
+
* and Backspace with the browser.
|
|
38
|
+
*/
|
|
39
|
+
deleteMeasurement?: () => void;
|
|
40
|
+
}
|
|
41
|
+
export interface ViewerShortcutOptions {
|
|
42
|
+
/** Defaults to {@link DEFAULT_TOOL_SHORTCUTS}. */
|
|
43
|
+
tools?: ToolShortcut[];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Keyboard control for a viewer, following the OHIF viewer's default keymap
|
|
47
|
+
* — the closest thing this corner of the world has to a standard, and the one
|
|
48
|
+
* anyone arriving from a PACS will try first.
|
|
49
|
+
*
|
|
50
|
+
* Bindings are matched on `event.code`, the physical key, rather than
|
|
51
|
+
* `event.key`, the character it produces. This is not a detail: `event.key`
|
|
52
|
+
* depends on the operating system's keyboard layout, so on a Persian layout
|
|
53
|
+
* the W key reports `'ش'` and on a Russian one `'ц'`, and every letter binding
|
|
54
|
+
* silently stops working — which is exactly the kind of "sometimes it works"
|
|
55
|
+
* that is hard to pin down, because it follows the layout rather than the app.
|
|
56
|
+
* `event.code` names the key's position instead, so `W` is the key marked W on
|
|
57
|
+
* the keyboard whatever it types, and the letters in the tooltips stay true.
|
|
58
|
+
* The trade-off is Dvorak and other remapped layouts, where the keys keep
|
|
59
|
+
* their QWERTY positions; component libraries make the same choice.
|
|
60
|
+
*
|
|
61
|
+
* The horizontal arrows are deliberately unbound: OHIF gives them to moving
|
|
62
|
+
* between viewports, and a single-viewport layout has nowhere to go. That also
|
|
63
|
+
* sidesteps the question of which arrow means "forward" when the chrome is
|
|
64
|
+
* right-to-left.
|
|
65
|
+
*
|
|
66
|
+
* Space is the one place this parts company with OHIF, which resets the
|
|
67
|
+
* viewport with it. Space means play/pause everywhere a person has ever used
|
|
68
|
+
* a media player, and a stack has a film to play, so it goes to cine and the
|
|
69
|
+
* reset moves to `R`. `R` is free here — it is rotate-right in OHIF, and there
|
|
70
|
+
* is no rotate tool in this module's default set.
|
|
71
|
+
*
|
|
72
|
+
* Delete and Backspace remove the selected measurements, when the caller
|
|
73
|
+
* supplies `deleteMeasurement`. Both, because the key a reader reaches for is
|
|
74
|
+
* whichever one their keyboard has.
|
|
75
|
+
*/
|
|
76
|
+
export declare function useViewerShortcuts(handlers: ViewerShortcutHandlers, options?: ViewerShortcutOptions): void;
|
|
77
|
+
/**
|
|
78
|
+
* Drop focus from a button that was clicked with a pointer.
|
|
79
|
+
*
|
|
80
|
+
* A clicked button keeps focus afterwards, and a focused button owns the
|
|
81
|
+
* spacebar — the browser activates it rather than letting the key through. So
|
|
82
|
+
* picking a tool from a toolbar with the mouse and then pressing Space appears
|
|
83
|
+
* to do nothing at all: the key is quietly re-pressing the tool button that is
|
|
84
|
+
* still focused, and never reaches the viewer's play/pause binding.
|
|
85
|
+
*
|
|
86
|
+
* `event.detail` is the click count, and it is `0` for a click the browser
|
|
87
|
+
* synthesised from Enter or Space on a focused control. Blurring only when it
|
|
88
|
+
* is non-zero therefore drops focus for mouse and touch, where nobody is
|
|
89
|
+
* following it, and leaves it exactly where it was for anyone driving the page
|
|
90
|
+
* from the keyboard — who needs it to tab onwards from.
|
|
91
|
+
*
|
|
92
|
+
* Attach it once to a toolbar's root and let the clicks bubble to it.
|
|
93
|
+
*/
|
|
94
|
+
export declare function releaseFocus(event: MouseEvent): void;
|