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,105 @@
|
|
|
1
|
+
import { ensureCornerstone, getCornerstoneOptions, getLoadedCornerstone } from "../cornerstone.js";
|
|
2
|
+
import { t } from "../i18n/index.js";
|
|
3
|
+
export function useCornerstoneTools(toolGroupId) {
|
|
4
|
+
const groupId = toolGroupId ?? getCornerstoneOptions().toolGroupId;
|
|
5
|
+
function resolveToolName(tools, tool) {
|
|
6
|
+
const exported = tools[tool];
|
|
7
|
+
return exported?.toolName ?? tool;
|
|
8
|
+
}
|
|
9
|
+
async function ensureGroup() {
|
|
10
|
+
const { tools } = await ensureCornerstone();
|
|
11
|
+
const existing = tools.ToolGroupManager.getToolGroup(groupId);
|
|
12
|
+
if (existing) return existing;
|
|
13
|
+
const group = tools.ToolGroupManager.createToolGroup(groupId);
|
|
14
|
+
if (!group) {
|
|
15
|
+
throw new Error(`[nuxt-cornerstone] ${t("error.toolGroupCreate", { groupId })}`);
|
|
16
|
+
}
|
|
17
|
+
const registered = getCornerstoneOptions().tools.register;
|
|
18
|
+
const classNames = registered === false ? [] : registered;
|
|
19
|
+
for (const className of classNames) {
|
|
20
|
+
const toolName = resolveToolName(tools, className);
|
|
21
|
+
if (!group.hasTool(toolName)) group.addTool(toolName);
|
|
22
|
+
}
|
|
23
|
+
const { MouseBindings } = tools.Enums;
|
|
24
|
+
const activate = (className, bindings) => {
|
|
25
|
+
const toolName = resolveToolName(tools, className);
|
|
26
|
+
if (group.hasTool(toolName)) group.setToolActive(toolName, { bindings });
|
|
27
|
+
};
|
|
28
|
+
activate("WindowLevelTool", [{ mouseButton: MouseBindings.Primary }]);
|
|
29
|
+
activate("ZoomTool", [{ mouseButton: MouseBindings.Secondary }]);
|
|
30
|
+
activate("PanTool", [{ mouseButton: MouseBindings.Auxiliary }]);
|
|
31
|
+
activate("StackScrollTool", [{ mouseButton: MouseBindings.Wheel }]);
|
|
32
|
+
return group;
|
|
33
|
+
}
|
|
34
|
+
async function addViewport(viewportId, renderingEngineId) {
|
|
35
|
+
const group = await ensureGroup();
|
|
36
|
+
group.addViewport(viewportId, renderingEngineId ?? getCornerstoneOptions().renderingEngineId);
|
|
37
|
+
}
|
|
38
|
+
function removeViewport(viewportId, renderingEngineId) {
|
|
39
|
+
const tools = getLoadedCornerstone()?.tools;
|
|
40
|
+
if (!tools) return;
|
|
41
|
+
const group = tools.ToolGroupManager.getToolGroup(groupId);
|
|
42
|
+
group?.removeViewports(
|
|
43
|
+
renderingEngineId ?? getCornerstoneOptions().renderingEngineId,
|
|
44
|
+
viewportId
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
async function setActive(tool, bindings) {
|
|
48
|
+
const { tools } = await ensureCornerstone();
|
|
49
|
+
const group = await ensureGroup();
|
|
50
|
+
const toolName = resolveToolName(tools, tool);
|
|
51
|
+
if (!group.hasTool(toolName)) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`[nuxt-cornerstone] ${t("error.toolNotInGroup", { tool: toolName, groupId })}`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const resolved = bindings ?? [{ mouseButton: tools.Enums.MouseBindings.Primary }];
|
|
57
|
+
const takesPrimary = resolved.some(
|
|
58
|
+
(binding) => binding.mouseButton === tools.Enums.MouseBindings.Primary
|
|
59
|
+
);
|
|
60
|
+
if (takesPrimary) {
|
|
61
|
+
const previous = group.getCurrentActivePrimaryToolName();
|
|
62
|
+
if (previous && previous !== toolName) group.setToolPassive(previous);
|
|
63
|
+
}
|
|
64
|
+
group.setToolActive(toolName, { bindings: resolved });
|
|
65
|
+
}
|
|
66
|
+
async function setPassive(tool) {
|
|
67
|
+
const { tools } = await ensureCornerstone();
|
|
68
|
+
const group = await ensureGroup();
|
|
69
|
+
group.setToolPassive(resolveToolName(tools, tool));
|
|
70
|
+
}
|
|
71
|
+
async function setEnabled(tool) {
|
|
72
|
+
const { tools } = await ensureCornerstone();
|
|
73
|
+
const group = await ensureGroup();
|
|
74
|
+
group.setToolEnabled(resolveToolName(tools, tool));
|
|
75
|
+
}
|
|
76
|
+
async function setDisabled(tool) {
|
|
77
|
+
const { tools } = await ensureCornerstone();
|
|
78
|
+
const group = await ensureGroup();
|
|
79
|
+
group.setToolDisabled(resolveToolName(tools, tool));
|
|
80
|
+
}
|
|
81
|
+
function getActiveTool() {
|
|
82
|
+
const tools = getLoadedCornerstone()?.tools;
|
|
83
|
+
if (!tools) return null;
|
|
84
|
+
return tools.ToolGroupManager.getToolGroup(groupId)?.getCurrentActivePrimaryToolName() ?? null;
|
|
85
|
+
}
|
|
86
|
+
function destroy() {
|
|
87
|
+
const tools = getLoadedCornerstone()?.tools;
|
|
88
|
+
if (!tools) return;
|
|
89
|
+
if (tools.ToolGroupManager.getToolGroup(groupId)) {
|
|
90
|
+
tools.ToolGroupManager.destroyToolGroup(groupId);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
toolGroupId: groupId,
|
|
95
|
+
ensureGroup,
|
|
96
|
+
addViewport,
|
|
97
|
+
removeViewport,
|
|
98
|
+
setActive,
|
|
99
|
+
setPassive,
|
|
100
|
+
setEnabled,
|
|
101
|
+
setDisabled,
|
|
102
|
+
getActiveTool,
|
|
103
|
+
destroy
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { ReadAnnotationsOptions, ReadAnnotationsResult } from '../annotation-json.js';
|
|
2
|
+
import type { MaybeRefOrGetter, Ref } from 'vue';
|
|
3
|
+
import type { StackViewport } from '../types.js';
|
|
4
|
+
/**
|
|
5
|
+
* A rectangle in image pixel coordinates — the space a detector or a report
|
|
6
|
+
* works in. `(x1, y1)` and `(x2, y2)` are opposite corners in either order.
|
|
7
|
+
*/
|
|
8
|
+
export interface PixelBox {
|
|
9
|
+
x1: number;
|
|
10
|
+
y1: number;
|
|
11
|
+
x2: number;
|
|
12
|
+
y2: number;
|
|
13
|
+
}
|
|
14
|
+
/** One box to draw, on the slice its SOPInstanceUID names. */
|
|
15
|
+
export interface DicomBox {
|
|
16
|
+
/** SOPInstanceUID (0008,0018) of the slice this box was drawn on. */
|
|
17
|
+
sopInstanceUid: string;
|
|
18
|
+
box: PixelBox;
|
|
19
|
+
/** Drawn beside the rectangle. Nothing is drawn when it is absent. */
|
|
20
|
+
label?: string;
|
|
21
|
+
/**
|
|
22
|
+
* A stable id of your own. Supply one when boxes arrive more than once —
|
|
23
|
+
* re-adding a box with the same id replaces it rather than stacking a second
|
|
24
|
+
* rectangle on the first. Left out, one is generated.
|
|
25
|
+
*/
|
|
26
|
+
uid?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface AddBoxesOptions {
|
|
29
|
+
/**
|
|
30
|
+
* Stroke colour, as any CSS colour. Applies to every box this composable
|
|
31
|
+
* draws, not to the user's own measurements. Default: Cornerstone's colour
|
|
32
|
+
* for locked annotations.
|
|
33
|
+
*/
|
|
34
|
+
color?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Lock the boxes so they cannot be dragged, resized or deleted. Default:
|
|
37
|
+
* `true` — these came from somewhere else and editing them here would
|
|
38
|
+
* silently diverge from the source.
|
|
39
|
+
*/
|
|
40
|
+
locked?: boolean;
|
|
41
|
+
}
|
|
42
|
+
export interface AddJsonOptions extends AddBoxesOptions, ReadAnnotationsOptions {
|
|
43
|
+
}
|
|
44
|
+
/** What {@link useDicomAnnotations.addJson} drew, and what it read to draw it. */
|
|
45
|
+
export interface AddJsonResult extends AddBoxesResult {
|
|
46
|
+
/** The reader's own account of the file. See {@link ReadAnnotationsResult}. */
|
|
47
|
+
report: ReadAnnotationsResult;
|
|
48
|
+
}
|
|
49
|
+
export interface AddBoxesResult {
|
|
50
|
+
/** Boxes drawn straight away, because their slice was already loaded. */
|
|
51
|
+
drawn: number;
|
|
52
|
+
/** Boxes held until their slice is shown. See {@link useDicomAnnotations}. */
|
|
53
|
+
deferred: number;
|
|
54
|
+
/** SOPInstanceUIDs that match nothing currently loaded. */
|
|
55
|
+
unresolved: string[];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The tool the imported boxes are drawn with.
|
|
59
|
+
*
|
|
60
|
+
* It is a RectangleROI instance under another name rather than RectangleROI
|
|
61
|
+
* itself, for two reasons. The user's own rectangles stay separately
|
|
62
|
+
* selectable and separately styled, and this instance carries its own
|
|
63
|
+
* `getTextLines`, so an imported box shows the label it arrived with instead of
|
|
64
|
+
* the area and mean a measurement would show.
|
|
65
|
+
*/
|
|
66
|
+
export declare const OVERLAY_TOOL_NAME = "DicomBoxOverlay";
|
|
67
|
+
export declare function useDicomAnnotations(source: MaybeRefOrGetter<StackViewport | null | undefined>): {
|
|
68
|
+
addBoxes: (boxes: DicomBox[], options?: AddBoxesOptions) => Promise<AddBoxesResult>;
|
|
69
|
+
addJson: (input: File | Blob | string | unknown, options?: AddJsonOptions) => Promise<AddJsonResult>;
|
|
70
|
+
clear: () => void;
|
|
71
|
+
setVisible: (next: boolean) => void;
|
|
72
|
+
drawn: Ref<number, number>;
|
|
73
|
+
pending: Ref<number, number>;
|
|
74
|
+
visible: Ref<boolean, boolean>;
|
|
75
|
+
toolName: string;
|
|
76
|
+
};
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { getCurrentScope, onScopeDispose, ref, toValue } from "vue";
|
|
2
|
+
import { readAnnotationJson } from "../annotation-json.js";
|
|
3
|
+
import { ensureCornerstone } from "../cornerstone.js";
|
|
4
|
+
import { imageIdForSopInstanceUid } from "../dicom-instances.js";
|
|
5
|
+
import { t } from "../i18n/index.js";
|
|
6
|
+
export const OVERLAY_TOOL_NAME = "DicomBoxOverlay";
|
|
7
|
+
const PARENT_TOOL_CLASS = "RectangleROITool";
|
|
8
|
+
function resolveToolClass(tools, name) {
|
|
9
|
+
const exported = tools[name];
|
|
10
|
+
return typeof exported === "function" ? exported : null;
|
|
11
|
+
}
|
|
12
|
+
export function useDicomAnnotations(source) {
|
|
13
|
+
const waiting = /* @__PURE__ */ new Map();
|
|
14
|
+
const owned = /* @__PURE__ */ new Set();
|
|
15
|
+
const drawn = ref(0);
|
|
16
|
+
const pending = ref(0);
|
|
17
|
+
const visible = ref(true);
|
|
18
|
+
let libs = null;
|
|
19
|
+
let listening = null;
|
|
20
|
+
let locked = true;
|
|
21
|
+
function requireViewport() {
|
|
22
|
+
const viewport = toValue(source);
|
|
23
|
+
if (!viewport) throw new Error(`[nuxt-cornerstone] ${t("error.annotationViewport")}`);
|
|
24
|
+
return viewport;
|
|
25
|
+
}
|
|
26
|
+
async function ensureOverlayTool(viewport, color) {
|
|
27
|
+
const { tools } = libs;
|
|
28
|
+
const group = tools.ToolGroupManager.getToolGroupForViewport(
|
|
29
|
+
viewport.id,
|
|
30
|
+
viewport.renderingEngineId
|
|
31
|
+
);
|
|
32
|
+
if (!group) {
|
|
33
|
+
throw new Error(`[nuxt-cornerstone] ${t("error.annotationToolGroup")}`);
|
|
34
|
+
}
|
|
35
|
+
if (!group.hasTool(OVERLAY_TOOL_NAME)) {
|
|
36
|
+
const ParentTool = resolveToolClass(tools, PARENT_TOOL_CLASS);
|
|
37
|
+
if (!ParentTool) {
|
|
38
|
+
throw new Error(`[nuxt-cornerstone] ${t("error.annotationTool")}`);
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
tools.addTool(ParentTool);
|
|
42
|
+
} catch {
|
|
43
|
+
}
|
|
44
|
+
group.addToolInstance(OVERLAY_TOOL_NAME, ParentTool.toolName);
|
|
45
|
+
group.setToolConfiguration(OVERLAY_TOOL_NAME, {
|
|
46
|
+
getTextLines: (data) => data.label ? [data.label] : []
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const mode = group.getToolInstance(OVERLAY_TOOL_NAME)?.mode;
|
|
50
|
+
if (mode !== tools.Enums.ToolModes.Enabled) group.setToolEnabled(OVERLAY_TOOL_NAME);
|
|
51
|
+
if (color) {
|
|
52
|
+
tools.annotation.config.style.setToolGroupToolStyles(group.id, {
|
|
53
|
+
[OVERLAY_TOOL_NAME]: { color, colorLocked: color, colorAutoGenerated: color }
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function draw(prepared, viewport) {
|
|
58
|
+
const { core, tools } = libs;
|
|
59
|
+
const { imageId } = prepared;
|
|
60
|
+
let points;
|
|
61
|
+
try {
|
|
62
|
+
const toWorld = (x, y) => core.utilities.imageToWorldCoords(imageId, [x, y]);
|
|
63
|
+
const left = Math.min(prepared.box.x1, prepared.box.x2);
|
|
64
|
+
const right = Math.max(prepared.box.x1, prepared.box.x2);
|
|
65
|
+
const top = Math.min(prepared.box.y1, prepared.box.y2);
|
|
66
|
+
const bottom = Math.max(prepared.box.y1, prepared.box.y2);
|
|
67
|
+
points = [
|
|
68
|
+
toWorld(left, top),
|
|
69
|
+
toWorld(right, top),
|
|
70
|
+
toWorld(left, bottom),
|
|
71
|
+
toWorld(right, bottom)
|
|
72
|
+
];
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const camera = viewport.getCamera();
|
|
77
|
+
const annotation = {
|
|
78
|
+
annotationUID: prepared.uid,
|
|
79
|
+
highlighted: false,
|
|
80
|
+
invalidated: true,
|
|
81
|
+
isLocked: locked,
|
|
82
|
+
isVisible: visible.value,
|
|
83
|
+
autoGenerated: true,
|
|
84
|
+
metadata: {
|
|
85
|
+
toolName: OVERLAY_TOOL_NAME,
|
|
86
|
+
// referencedImageId is what pins the box to one slice: a stack
|
|
87
|
+
// viewport shows an annotation only while that image is the one on
|
|
88
|
+
// screen.
|
|
89
|
+
referencedImageId: imageId,
|
|
90
|
+
FrameOfReferenceUID: viewport.getFrameOfReferenceUID(),
|
|
91
|
+
viewPlaneNormal: camera.viewPlaneNormal ? [...camera.viewPlaneNormal] : void 0,
|
|
92
|
+
viewUp: camera.viewUp ? [...camera.viewUp] : void 0
|
|
93
|
+
},
|
|
94
|
+
data: {
|
|
95
|
+
label: prepared.label ?? "",
|
|
96
|
+
handles: {
|
|
97
|
+
points,
|
|
98
|
+
activeHandleIndex: null,
|
|
99
|
+
textBox: { hasMoved: false }
|
|
100
|
+
},
|
|
101
|
+
cachedStats: {}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const uid = tools.annotation.state.addAnnotation(
|
|
105
|
+
annotation,
|
|
106
|
+
viewport.element
|
|
107
|
+
);
|
|
108
|
+
owned.add(uid);
|
|
109
|
+
if (locked) tools.annotation.locking.setAnnotationLocked(uid, true);
|
|
110
|
+
if (!visible.value) tools.annotation.visibility.setAnnotationVisibility(uid, false);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
function queue(prepared) {
|
|
114
|
+
const queued = waiting.get(prepared.imageId);
|
|
115
|
+
if (queued) queued.push(prepared);
|
|
116
|
+
else waiting.set(prepared.imageId, [prepared]);
|
|
117
|
+
}
|
|
118
|
+
function countWaiting() {
|
|
119
|
+
let total = 0;
|
|
120
|
+
for (const queued of waiting.values()) total += queued.length;
|
|
121
|
+
return total;
|
|
122
|
+
}
|
|
123
|
+
function flush(viewport) {
|
|
124
|
+
const imageId = viewport.getCurrentImageId();
|
|
125
|
+
if (!imageId) return;
|
|
126
|
+
const queued = waiting.get(imageId);
|
|
127
|
+
if (!queued) return;
|
|
128
|
+
waiting.delete(imageId);
|
|
129
|
+
let added = 0;
|
|
130
|
+
for (const prepared of queued) {
|
|
131
|
+
if (draw(prepared, viewport)) added += 1;
|
|
132
|
+
}
|
|
133
|
+
drawn.value = owned.size;
|
|
134
|
+
pending.value = countWaiting();
|
|
135
|
+
if (added) render(viewport);
|
|
136
|
+
if (waiting.size === 0) stopWatching();
|
|
137
|
+
}
|
|
138
|
+
function onStackNewImage() {
|
|
139
|
+
const viewport = toValue(source);
|
|
140
|
+
if (viewport) flush(viewport);
|
|
141
|
+
}
|
|
142
|
+
function watchViewport(viewport) {
|
|
143
|
+
const element = viewport.element;
|
|
144
|
+
if (listening === element) return;
|
|
145
|
+
stopWatching();
|
|
146
|
+
element.addEventListener(libs.core.Enums.Events.STACK_NEW_IMAGE, onStackNewImage);
|
|
147
|
+
listening = element;
|
|
148
|
+
}
|
|
149
|
+
function stopWatching() {
|
|
150
|
+
if (!listening || !libs) return;
|
|
151
|
+
listening.removeEventListener(libs.core.Enums.Events.STACK_NEW_IMAGE, onStackNewImage);
|
|
152
|
+
listening = null;
|
|
153
|
+
}
|
|
154
|
+
function render(viewport) {
|
|
155
|
+
libs?.tools.utilities.triggerAnnotationRenderForViewportIds([viewport.id]);
|
|
156
|
+
}
|
|
157
|
+
async function addBoxes(boxes, options = {}) {
|
|
158
|
+
libs = await ensureCornerstone();
|
|
159
|
+
const viewport = requireViewport();
|
|
160
|
+
locked = options.locked ?? true;
|
|
161
|
+
await ensureOverlayTool(viewport, options.color);
|
|
162
|
+
const unresolved = [];
|
|
163
|
+
let immediate = 0;
|
|
164
|
+
let deferred = 0;
|
|
165
|
+
for (const entry of boxes) {
|
|
166
|
+
const imageId = imageIdForSopInstanceUid(entry.sopInstanceUid);
|
|
167
|
+
if (!imageId) {
|
|
168
|
+
unresolved.push(entry.sopInstanceUid);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (entry.uid && owned.has(entry.uid)) remove(entry.uid);
|
|
172
|
+
const prepared = {
|
|
173
|
+
imageId,
|
|
174
|
+
box: entry.box,
|
|
175
|
+
label: entry.label,
|
|
176
|
+
uid: entry.uid
|
|
177
|
+
};
|
|
178
|
+
if (draw(prepared, viewport)) immediate += 1;
|
|
179
|
+
else {
|
|
180
|
+
queue(prepared);
|
|
181
|
+
deferred += 1;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (deferred > 0) watchViewport(viewport);
|
|
185
|
+
drawn.value = owned.size;
|
|
186
|
+
pending.value = countWaiting();
|
|
187
|
+
if (immediate > 0) render(viewport);
|
|
188
|
+
return { drawn: immediate, deferred, unresolved };
|
|
189
|
+
}
|
|
190
|
+
async function addJson(input, options = {}) {
|
|
191
|
+
const report = await readAnnotationJson(input, options);
|
|
192
|
+
const result = await addBoxes(report.boxes, options);
|
|
193
|
+
return { ...result, report };
|
|
194
|
+
}
|
|
195
|
+
function remove(uid) {
|
|
196
|
+
libs?.tools.annotation.state.removeAnnotation(uid);
|
|
197
|
+
owned.delete(uid);
|
|
198
|
+
}
|
|
199
|
+
function clear() {
|
|
200
|
+
for (const uid of owned) libs?.tools.annotation.state.removeAnnotation(uid);
|
|
201
|
+
owned.clear();
|
|
202
|
+
waiting.clear();
|
|
203
|
+
stopWatching();
|
|
204
|
+
drawn.value = 0;
|
|
205
|
+
pending.value = 0;
|
|
206
|
+
const viewport = toValue(source);
|
|
207
|
+
if (viewport) render(viewport);
|
|
208
|
+
}
|
|
209
|
+
function setVisible(next) {
|
|
210
|
+
visible.value = next;
|
|
211
|
+
const tools = libs?.tools;
|
|
212
|
+
if (tools) {
|
|
213
|
+
for (const uid of owned) tools.annotation.visibility.setAnnotationVisibility(uid, next);
|
|
214
|
+
}
|
|
215
|
+
const viewport = toValue(source);
|
|
216
|
+
if (viewport) render(viewport);
|
|
217
|
+
}
|
|
218
|
+
if (getCurrentScope()) onScopeDispose(stopWatching);
|
|
219
|
+
return { addBoxes, addJson, clear, setVisible, drawn, pending, visible, toolName: OVERLAY_TOOL_NAME };
|
|
220
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { imageIdForSopInstanceUid } from '../dicom-instances.js';
|
|
2
|
+
import type { SkippedEntry } from '../dicom-zip.js';
|
|
3
|
+
export type { SkippedEntry, SkipReason } from '../dicom-zip.js';
|
|
4
|
+
export interface AddFilesOptions {
|
|
5
|
+
/**
|
|
6
|
+
* How to order the resulting imageIds.
|
|
7
|
+
*
|
|
8
|
+
* - `'instanceNumber'` (default) reads DICOM tag (0020,0013) from each file
|
|
9
|
+
* and sorts on it, falling back to the filename for files that do not
|
|
10
|
+
* carry it. This is what a viewer needs: filename order and acquisition
|
|
11
|
+
* order disagree often enough to matter.
|
|
12
|
+
* - `'name'` sorts by filename with a natural (numeric-aware) collator.
|
|
13
|
+
* - `false` keeps the order the files came in.
|
|
14
|
+
*/
|
|
15
|
+
sort?: 'instanceNumber' | 'name' | false;
|
|
16
|
+
}
|
|
17
|
+
export interface AddZipOptions extends AddFilesOptions {
|
|
18
|
+
/**
|
|
19
|
+
* `'series'` (default) splits the archive by SeriesInstanceUID (0020,000E),
|
|
20
|
+
* because a study ZIP normally holds several series and stacking a sagittal
|
|
21
|
+
* T1 on top of an axial T2 is not a stack. `false` returns one group holding
|
|
22
|
+
* everything, matching {@link useDicomFiles.addFiles}.
|
|
23
|
+
*/
|
|
24
|
+
groupBy?: 'series' | false;
|
|
25
|
+
/** Ceiling on total uncompressed bytes. Default: 2 GiB. */
|
|
26
|
+
maxBytes?: number;
|
|
27
|
+
/** Called as the archive moves through its phases. See {@link ZipProgress}. */
|
|
28
|
+
onProgress?: (progress: ZipProgress) => void;
|
|
29
|
+
}
|
|
30
|
+
export interface ZipProgress {
|
|
31
|
+
/**
|
|
32
|
+
* - `'reading'` — pulling the archive into memory.
|
|
33
|
+
* - `'extracting'` — inflating; fflate reports nothing until it finishes, so
|
|
34
|
+
* `done`/`total` are both `0` here and a UI should show an indeterminate bar.
|
|
35
|
+
* - `'indexing'` — reading DICOM headers, one file at a time and countable.
|
|
36
|
+
*/
|
|
37
|
+
phase: 'reading' | 'extracting' | 'indexing';
|
|
38
|
+
done: number;
|
|
39
|
+
total: number;
|
|
40
|
+
}
|
|
41
|
+
/** One series' worth of images, ready to hand to a viewport. */
|
|
42
|
+
export interface DicomSeries {
|
|
43
|
+
/** SeriesInstanceUID (0020,000E), or a path-derived id when the tag is absent. */
|
|
44
|
+
seriesInstanceUid: string;
|
|
45
|
+
/** SeriesNumber (0020,0011). */
|
|
46
|
+
seriesNumber: number | null;
|
|
47
|
+
/** SeriesDescription (0008,103E). */
|
|
48
|
+
description: string | null;
|
|
49
|
+
/** Modality (0008,0060). */
|
|
50
|
+
modality: string | null;
|
|
51
|
+
/** A human-readable label, already falling back when tags are missing. */
|
|
52
|
+
label: string;
|
|
53
|
+
/** imageIds in display order. */
|
|
54
|
+
imageIds: string[];
|
|
55
|
+
}
|
|
56
|
+
export interface AddZipResult {
|
|
57
|
+
/** Series ordered by SeriesNumber, then description. */
|
|
58
|
+
series: DicomSeries[];
|
|
59
|
+
/** Every imageId, series order then display order within each series. */
|
|
60
|
+
imageIds: string[];
|
|
61
|
+
/** Archive members that were not loaded, and why. */
|
|
62
|
+
skipped: SkippedEntry[];
|
|
63
|
+
}
|
|
64
|
+
/** See {@link useDicomFiles.indexUrls}. */
|
|
65
|
+
export interface IndexUrlsOptions {
|
|
66
|
+
/** Files fetched at once. Default: `6`. */
|
|
67
|
+
concurrency?: number;
|
|
68
|
+
}
|
|
69
|
+
export interface IndexUrlsResult {
|
|
70
|
+
/** Files whose SOPInstanceUID was read and recorded. */
|
|
71
|
+
indexed: number;
|
|
72
|
+
/** URLs that could not be fetched, or were not readable DICOM. */
|
|
73
|
+
failed: string[];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Turn local DICOM Part 10 files into `dicomfile:` imageIds the viewport can
|
|
77
|
+
* render, unpack ZIP archives of them, and build `wadouri:` imageIds for files
|
|
78
|
+
* served over HTTP.
|
|
79
|
+
*/
|
|
80
|
+
export declare function useDicomFiles(): {
|
|
81
|
+
addFiles: (input: File[] | FileList, options?: AddFilesOptions) => Promise<string[]>;
|
|
82
|
+
addZip: (input: File | Blob | ArrayBuffer | Uint8Array, options?: AddZipOptions) => Promise<AddZipResult>;
|
|
83
|
+
toImageId: (url: string) => string;
|
|
84
|
+
indexUrls: (urls: string[], options?: IndexUrlsOptions) => Promise<IndexUrlsResult>;
|
|
85
|
+
purge: () => Promise<void>;
|
|
86
|
+
imageIdForSopInstanceUid: typeof imageIdForSopInstanceUid;
|
|
87
|
+
};
|