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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +933 -0
  3. package/dist/module.d.mts +22 -0
  4. package/dist/module.json +12 -0
  5. package/dist/module.mjs +168 -0
  6. package/dist/runtime/annotation-json.d.ts +76 -0
  7. package/dist/runtime/annotation-json.js +176 -0
  8. package/dist/runtime/components/CornerstoneViewport.d.vue.ts +57 -0
  9. package/dist/runtime/components/CornerstoneViewport.vue +227 -0
  10. package/dist/runtime/components/CornerstoneViewport.vue.d.ts +57 -0
  11. package/dist/runtime/composables/useAnnotationReport.d.ts +28 -0
  12. package/dist/runtime/composables/useAnnotationReport.js +66 -0
  13. package/dist/runtime/composables/useCinePlayer.d.ts +52 -0
  14. package/dist/runtime/composables/useCinePlayer.js +95 -0
  15. package/dist/runtime/composables/useCornerstone.d.ts +17 -0
  16. package/dist/runtime/composables/useCornerstone.js +30 -0
  17. package/dist/runtime/composables/useCornerstoneI18n.d.ts +24 -0
  18. package/dist/runtime/composables/useCornerstoneI18n.js +32 -0
  19. package/dist/runtime/composables/useCornerstoneTools.d.ts +21 -0
  20. package/dist/runtime/composables/useCornerstoneTools.js +105 -0
  21. package/dist/runtime/composables/useDicomAnnotations.d.ts +76 -0
  22. package/dist/runtime/composables/useDicomAnnotations.js +220 -0
  23. package/dist/runtime/composables/useDicomFiles.d.ts +87 -0
  24. package/dist/runtime/composables/useDicomFiles.js +234 -0
  25. package/dist/runtime/composables/useDicomGuard.d.ts +24 -0
  26. package/dist/runtime/composables/useDicomGuard.js +30 -0
  27. package/dist/runtime/composables/useDicomStudy.d.ts +122 -0
  28. package/dist/runtime/composables/useDicomStudy.js +222 -0
  29. package/dist/runtime/composables/useImagePrefetch.d.ts +78 -0
  30. package/dist/runtime/composables/useImagePrefetch.js +119 -0
  31. package/dist/runtime/composables/useMeasurements.d.ts +51 -0
  32. package/dist/runtime/composables/useMeasurements.js +138 -0
  33. package/dist/runtime/composables/useRenderingEngine.d.ts +6 -0
  34. package/dist/runtime/composables/useRenderingEngine.js +46 -0
  35. package/dist/runtime/composables/useStackCine.d.ts +50 -0
  36. package/dist/runtime/composables/useStackCine.js +62 -0
  37. package/dist/runtime/composables/useViewerShortcuts.d.ts +94 -0
  38. package/dist/runtime/composables/useViewerShortcuts.js +115 -0
  39. package/dist/runtime/cornerstone.d.ts +21 -0
  40. package/dist/runtime/cornerstone.js +100 -0
  41. package/dist/runtime/dicom-instances.d.ts +32 -0
  42. package/dist/runtime/dicom-instances.js +15 -0
  43. package/dist/runtime/dicom-zip.d.ts +95 -0
  44. package/dist/runtime/dicom-zip.js +149 -0
  45. package/dist/runtime/i18n/detect.d.ts +37 -0
  46. package/dist/runtime/i18n/detect.js +37 -0
  47. package/dist/runtime/i18n/index.d.ts +61 -0
  48. package/dist/runtime/i18n/index.js +145 -0
  49. package/dist/runtime/i18n/messages.d.ts +122 -0
  50. package/dist/runtime/i18n/messages.js +147 -0
  51. package/dist/runtime/plugin.client.d.ts +13 -0
  52. package/dist/runtime/plugin.client.js +31 -0
  53. package/dist/runtime/plugin.i18n.d.ts +12 -0
  54. package/dist/runtime/plugin.i18n.js +15 -0
  55. package/dist/runtime/types.d.ts +150 -0
  56. package/dist/runtime/types.js +10 -0
  57. package/dist/types.d.mts +29 -0
  58. package/package.json +76 -0
@@ -0,0 +1,227 @@
1
+ <script setup>
2
+ import { onBeforeUnmount, onMounted, ref, shallowRef, useId, watch } from "vue";
3
+ import { ensureCornerstone, getCornerstoneOptions } from "../cornerstone";
4
+ import { useCornerstoneTools } from "../composables/useCornerstoneTools";
5
+ import { useRenderingEngine } from "../composables/useRenderingEngine";
6
+ const props = defineProps({
7
+ imageIds: { type: Array, required: true },
8
+ viewportId: { type: String, required: false, default: void 0 },
9
+ renderingEngineId: { type: String, required: false, default: void 0 },
10
+ toolGroupId: { type: String, required: false, default: void 0 },
11
+ imageIndex: { type: Number, required: false, default: 0 },
12
+ background: { type: Array, required: false, default: () => [0, 0, 0] },
13
+ defaultTool: { type: [String, Boolean], required: false, default: "WindowLevelTool" }
14
+ });
15
+ const emit = defineEmits(["ready", "error", "imageRendered", "imageIndexChange"]);
16
+ const element = ref(null);
17
+ const viewport = shallowRef(null);
18
+ const status = ref("loading");
19
+ const error = shallowRef(null);
20
+ const engines = useRenderingEngine();
21
+ const tools = useCornerstoneTools(props.toolGroupId);
22
+ const viewportId = props.viewportId ?? `nuxt-cornerstone-${useId()}`;
23
+ const renderingEngineId = props.renderingEngineId ?? getCornerstoneOptions().renderingEngineId;
24
+ let libs = null;
25
+ let sizeObserver = null;
26
+ let resizeObserver = null;
27
+ let frame = 0;
28
+ let acquired = false;
29
+ let disposed = false;
30
+ let settingIndex = false;
31
+ let queuedIndex = null;
32
+ function fail(caught) {
33
+ const asError = caught instanceof Error ? caught : new Error(String(caught));
34
+ error.value = asError;
35
+ status.value = "error";
36
+ emit("error", asError);
37
+ }
38
+ function waitForSize(el) {
39
+ if (el.clientWidth > 0 && el.clientHeight > 0) return Promise.resolve();
40
+ return new Promise((resolve) => {
41
+ sizeObserver = new ResizeObserver(() => {
42
+ if (el.clientWidth > 0 && el.clientHeight > 0) {
43
+ sizeObserver?.disconnect();
44
+ sizeObserver = null;
45
+ resolve();
46
+ }
47
+ });
48
+ sizeObserver.observe(el);
49
+ });
50
+ }
51
+ function observeResize(el) {
52
+ resizeObserver = new ResizeObserver(() => {
53
+ cancelAnimationFrame(frame);
54
+ frame = requestAnimationFrame(() => {
55
+ engines.get(renderingEngineId)?.resize(true, true);
56
+ });
57
+ });
58
+ resizeObserver.observe(el);
59
+ }
60
+ function onImageRendered() {
61
+ emit("imageRendered");
62
+ }
63
+ function onStackNewImage() {
64
+ const index = viewport.value?.getCurrentImageIdIndex();
65
+ if (typeof index === "number") emit("imageIndexChange", index);
66
+ }
67
+ function clampIndex(index, length) {
68
+ if (length === 0) return 0;
69
+ return Math.min(Math.max(index, 0), length - 1);
70
+ }
71
+ async function applyStack(imageIds, index) {
72
+ const vp = viewport.value;
73
+ if (!vp) return;
74
+ queuedIndex = null;
75
+ if (imageIds.length === 0) {
76
+ vp.setStack([]);
77
+ vp.render();
78
+ return;
79
+ }
80
+ await vp.setStack(imageIds, clampIndex(index, imageIds.length));
81
+ vp.render();
82
+ }
83
+ async function showIndex(target) {
84
+ if (settingIndex) {
85
+ queuedIndex = target;
86
+ return;
87
+ }
88
+ settingIndex = true;
89
+ try {
90
+ let next = target;
91
+ while (next !== null && !disposed) {
92
+ const vp = viewport.value;
93
+ if (!vp) return;
94
+ const current = next;
95
+ next = null;
96
+ if (vp.getCurrentImageIdIndex() !== current) await vp.setImageIdIndex(current);
97
+ if (queuedIndex !== null) {
98
+ next = queuedIndex;
99
+ queuedIndex = null;
100
+ }
101
+ }
102
+ } catch (caught) {
103
+ if (!disposed) fail(caught);
104
+ } finally {
105
+ settingIndex = false;
106
+ }
107
+ }
108
+ onMounted(async () => {
109
+ const el = element.value;
110
+ if (!el) return;
111
+ try {
112
+ libs = await ensureCornerstone();
113
+ await waitForSize(el);
114
+ if (disposed) return;
115
+ const { Enums } = libs.core;
116
+ const engine = await engines.acquire(renderingEngineId);
117
+ acquired = true;
118
+ if (disposed) return;
119
+ engine.enableElement({
120
+ viewportId,
121
+ element: el,
122
+ type: Enums.ViewportType.STACK,
123
+ defaultOptions: { background: props.background }
124
+ });
125
+ viewport.value = engine.getViewport(viewportId);
126
+ el.addEventListener(Enums.Events.IMAGE_RENDERED, onImageRendered);
127
+ el.addEventListener(Enums.Events.STACK_NEW_IMAGE, onStackNewImage);
128
+ await tools.addViewport(viewportId, renderingEngineId);
129
+ if (props.defaultTool) await tools.setActive(props.defaultTool);
130
+ observeResize(el);
131
+ await applyStack(props.imageIds, props.imageIndex);
132
+ if (disposed) return;
133
+ status.value = "ready";
134
+ emit("ready", viewport.value);
135
+ } catch (caught) {
136
+ if (!disposed) fail(caught);
137
+ }
138
+ });
139
+ watch(
140
+ () => props.imageIds,
141
+ async (imageIds) => {
142
+ if (!viewport.value) return;
143
+ try {
144
+ await applyStack(imageIds, props.imageIndex);
145
+ } catch (caught) {
146
+ fail(caught);
147
+ }
148
+ }
149
+ );
150
+ watch(
151
+ () => props.imageIndex,
152
+ (index) => {
153
+ const vp = viewport.value;
154
+ if (!vp || props.imageIds.length === 0) return;
155
+ const target = clampIndex(index, props.imageIds.length);
156
+ if (!settingIndex && vp.getCurrentImageIdIndex() === target) return;
157
+ showIndex(target);
158
+ }
159
+ );
160
+ onBeforeUnmount(() => {
161
+ disposed = true;
162
+ cancelAnimationFrame(frame);
163
+ sizeObserver?.disconnect();
164
+ resizeObserver?.disconnect();
165
+ sizeObserver = null;
166
+ resizeObserver = null;
167
+ const el = element.value;
168
+ if (el && libs) {
169
+ el.removeEventListener(libs.core.Enums.Events.IMAGE_RENDERED, onImageRendered);
170
+ el.removeEventListener(libs.core.Enums.Events.STACK_NEW_IMAGE, onStackNewImage);
171
+ }
172
+ tools.removeViewport(viewportId, renderingEngineId);
173
+ try {
174
+ engines.get(renderingEngineId)?.disableElement(viewportId);
175
+ } catch {
176
+ }
177
+ if (acquired) engines.release(renderingEngineId);
178
+ viewport.value = null;
179
+ });
180
+ defineExpose({
181
+ /** The Cornerstone stack viewport, once mounted. */
182
+ viewport,
183
+ viewportId,
184
+ renderingEngineId,
185
+ status,
186
+ error,
187
+ setImageIndex: async (index) => {
188
+ if (!viewport.value) return;
189
+ await showIndex(clampIndex(index, props.imageIds.length));
190
+ },
191
+ resetCamera: () => {
192
+ viewport.value?.resetCamera();
193
+ viewport.value?.render();
194
+ },
195
+ getRenderingEngine: () => engines.get(renderingEngineId)
196
+ });
197
+ </script>
198
+
199
+ <template>
200
+ <!--
201
+ The layout this element needs to work at all is in the `<style>` block
202
+ below rather than in utility classes here — see the note there.
203
+
204
+ `dir="ltr"` is deliberate and should stay. In an RTL app the surrounding
205
+ chrome flips, but a DICOM image must not: left and right are facts about the
206
+ patient, and mirroring one turns a left-sided finding into a right-sided
207
+ one. It is written as a default rather than hard-coded, so an app that has
208
+ its own reason to change it still can by passing `dir` to the component.
209
+ -->
210
+ <div
211
+ ref="element"
212
+ dir="ltr"
213
+ class="nuxt-cornerstone-viewport"
214
+ :data-status="status"
215
+ @contextmenu.prevent
216
+ >
217
+ <slot
218
+ :status="status"
219
+ :error="error"
220
+ :viewport="viewport"
221
+ />
222
+ </div>
223
+ </template>
224
+
225
+ <style>
226
+ :where(.nuxt-cornerstone-viewport){height:100%;overflow:hidden;position:relative;touch-action:none;width:100%}:where(.nuxt-cornerstone-viewport)>canvas{display:block}
227
+ </style>
@@ -0,0 +1,57 @@
1
+ /**
2
+ * A Cornerstone3D stack viewport.
3
+ *
4
+ * Registered with `mode: 'client'`, so Nuxt never renders it on the server —
5
+ * WebGL, web workers and the WASM decoders have no server-side equivalent.
6
+ */
7
+ type __VLS_Props = {
8
+ /** `wadouri:` / `wadors:` / `dicomfile:` imageIds, in display order. */
9
+ imageIds: string[];
10
+ /** Defaults to a generated id, unique per component instance. */
11
+ viewportId?: string;
12
+ renderingEngineId?: string;
13
+ toolGroupId?: string;
14
+ /** Index into `imageIds`. Two-way via the `imageIndexChange` event. */
15
+ imageIndex?: number;
16
+ /** Canvas background as RGB in 0..1. */
17
+ background?: [number, number, number];
18
+ /** Tool to bind to left-drag once mounted, or `false` to leave it alone. */
19
+ defaultTool?: string | false;
20
+ };
21
+ declare var __VLS_1: {
22
+ status: "ready" | "error" | "loading";
23
+ error: Error | null;
24
+ viewport: import("@cornerstonejs/core").StackViewport | null;
25
+ };
26
+ type __VLS_Slots = {} & {
27
+ default?: (props: typeof __VLS_1) => any;
28
+ };
29
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
30
+ /** The Cornerstone stack viewport, once mounted. */
31
+ viewport: import("vue").ShallowRef<import("@cornerstonejs/core").StackViewport | null, import("@cornerstonejs/core").StackViewport | null>;
32
+ viewportId: string;
33
+ renderingEngineId: string;
34
+ status: import("vue").Ref<"ready" | "error" | "loading", "ready" | "error" | "loading">;
35
+ error: import("vue").ShallowRef<Error | null, Error | null>;
36
+ setImageIndex: (index: number) => Promise<void>;
37
+ resetCamera: () => void;
38
+ getRenderingEngine: () => import("@cornerstonejs/core").RenderingEngine | null;
39
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
40
+ ready: (viewport: import("@cornerstonejs/core").StackViewport) => any;
41
+ error: (error: Error) => any;
42
+ imageRendered: () => any;
43
+ imageIndexChange: (index: number) => any;
44
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
45
+ onReady?: ((viewport: import("@cornerstonejs/core").StackViewport) => any) | undefined;
46
+ onError?: ((error: Error) => any) | undefined;
47
+ onImageRendered?: (() => any) | undefined;
48
+ onImageIndexChange?: ((index: number) => any) | undefined;
49
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
50
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
51
+ declare const _default: typeof __VLS_export;
52
+ export default _default;
53
+ type __VLS_WithSlots<T, S> = T & {
54
+ new (): {
55
+ $slots: S;
56
+ };
57
+ };
@@ -0,0 +1,28 @@
1
+ import type { MaybeRefOrGetter } from 'vue';
2
+ import type { StackViewport } from '../types.js';
3
+ export interface AnnotationReportOptions {
4
+ /** CSS colour for the boxes. Default: amber. */
5
+ color?: string;
6
+ }
7
+ /**
8
+ * Opening a JSON annotation report as a user action.
9
+ *
10
+ * {@link useDicomAnnotations} reads the file and draws the boxes; this adds the
11
+ * part an application always ends up writing — which file is open, whether its
12
+ * boxes are showing, and what to tell the reader afterwards. The interesting
13
+ * case is a file that parsed perfectly and placed nothing, which normally means
14
+ * the report was made from a different series rather than that anything went
15
+ * wrong, and which a bare count would not explain.
16
+ */
17
+ export declare function useAnnotationReport(viewport: MaybeRefOrGetter<StackViewport | null | undefined>, options?: AnnotationReportOptions): {
18
+ open: (file: File) => Promise<void>;
19
+ toggle: () => void;
20
+ reset: () => void;
21
+ busy: import("vue").Ref<boolean, boolean>;
22
+ loaded: import("vue").Ref<boolean, boolean>;
23
+ visible: import("vue").Ref<boolean, boolean>;
24
+ summaryText: import("vue").ComputedRef<string | null>;
25
+ failure: import("vue").Ref<string | null, string | null>;
26
+ drawn: import("vue").Ref<number, number>;
27
+ pending: import("vue").Ref<number, number>;
28
+ };
@@ -0,0 +1,66 @@
1
+ import { computed, ref } from "vue";
2
+ import { t } from "../i18n/index.js";
3
+ import { useDicomAnnotations } from "./useDicomAnnotations.js";
4
+ const DEFAULT_BOX_COLOR = "rgb(251, 191, 36)";
5
+ export function useAnnotationReport(viewport, options = {}) {
6
+ const { addJson, clear, setVisible, visible, drawn, pending } = useDicomAnnotations(viewport);
7
+ const busy = ref(false);
8
+ const loaded = ref(false);
9
+ const fileName = ref(null);
10
+ const failure = ref(null);
11
+ const stats = ref(null);
12
+ async function open(file) {
13
+ busy.value = true;
14
+ failure.value = null;
15
+ try {
16
+ clear();
17
+ const result = await addJson(file, { color: options.color ?? DEFAULT_BOX_COLOR });
18
+ fileName.value = file.name;
19
+ loaded.value = true;
20
+ setVisible(true);
21
+ stats.value = {
22
+ total: result.report.boxes.length,
23
+ placed: result.drawn + result.deferred,
24
+ malformed: result.report.malformed,
25
+ series: result.report.seriesInstanceUid
26
+ };
27
+ } catch (caught) {
28
+ reset();
29
+ failure.value = t("report.failed", {
30
+ file: file.name,
31
+ message: caught instanceof Error ? caught.message : String(caught)
32
+ });
33
+ } finally {
34
+ busy.value = false;
35
+ }
36
+ }
37
+ function toggle() {
38
+ setVisible(!visible.value);
39
+ }
40
+ function reset() {
41
+ clear();
42
+ loaded.value = false;
43
+ fileName.value = null;
44
+ stats.value = null;
45
+ failure.value = null;
46
+ }
47
+ const summaryText = computed(() => {
48
+ const value = stats.value;
49
+ if (!value || !fileName.value) return null;
50
+ if (value.total === 0) return t("report.empty", { file: fileName.value });
51
+ if (value.placed === 0) {
52
+ return value.series ? t("report.unmatched", { total: value.total, series: value.series }) : t("report.unmatchedNoSeries", { total: value.total });
53
+ }
54
+ const parts = [
55
+ t("report.summary", {
56
+ file: fileName.value,
57
+ placed: value.placed,
58
+ total: value.total
59
+ })
60
+ ];
61
+ if (pending.value > 0) parts.push(t("report.waiting", { count: pending.value }));
62
+ if (value.malformed > 0) parts.push(t("report.dropped", { count: value.malformed }));
63
+ return parts.join(" ");
64
+ });
65
+ return { open, toggle, reset, busy, loaded, visible, summaryText, failure, drawn, pending };
66
+ }
@@ -0,0 +1,52 @@
1
+ import type { MaybeRefOrGetter, Ref } from 'vue';
2
+ export interface CinePlayerOptions {
3
+ /** Frames per second. Default: `15`, and clamped to {@link FRAME_RATE_RANGE}. */
4
+ frameRate?: number;
5
+ /** Start again from the other end instead of stopping. Default: `true`. */
6
+ loop?: boolean;
7
+ /**
8
+ * Reverse at each end rather than jumping back. Default: `false`.
9
+ *
10
+ * This is what a cardiac cine wants — a short loop read back and forth reads
11
+ * as motion, where the jump from last frame to first reads as a glitch.
12
+ */
13
+ bounce?: boolean;
14
+ /** `1` plays towards the last image, `-1` towards the first. Default: `1`. */
15
+ direction?: 1 | -1;
16
+ }
17
+ /** What {@link useCinePlayer} will accept as a frame rate. */
18
+ export declare const FRAME_RATE_RANGE: {
19
+ readonly min: 1;
20
+ readonly max: 60;
21
+ };
22
+ /**
23
+ * Play a stack as a film.
24
+ *
25
+ * The player owns no images and no viewport: it advances the index ref it is
26
+ * given, on a wall-clock schedule, and whatever is bound to that index follows.
27
+ * That keeps it usable with `<CornerstoneViewport v-model>`-style bindings, a
28
+ * scrubber, keyboard stepping and anything else, all moving the same value.
29
+ *
30
+ * Frames are paced with `requestAnimationFrame` rather than `setInterval`,
31
+ * which buys two things: the schedule is measured against the clock so it does
32
+ * not drift, and a backgrounded tab stops advancing instead of spending the
33
+ * battery on frames nobody is watching.
34
+ *
35
+ * Nothing here waits for an image to decode. A frame the loader has not caught
36
+ * up with is simply shown late, so a stack played straight off the network
37
+ * stutters — run {@link useImagePrefetch} over it first and playback is a
38
+ * sequence of cache hits.
39
+ */
40
+ export declare function useCinePlayer(index: Ref<number>, frameCount: MaybeRefOrGetter<number>, options?: CinePlayerOptions): {
41
+ playing: Ref<boolean, boolean>;
42
+ frameRate: import("vue").WritableComputedRef<number, number>;
43
+ loop: Ref<boolean, boolean>;
44
+ bounce: Ref<boolean, boolean>;
45
+ direction: Ref<1 | -1, 1 | -1>;
46
+ canPlay: import("vue").ComputedRef<boolean>;
47
+ play: () => void;
48
+ pause: () => void;
49
+ toggle: () => void;
50
+ setFrameRate: (value: number) => void;
51
+ setDirection: (value: 1 | -1) => void;
52
+ };
@@ -0,0 +1,95 @@
1
+ import { computed, getCurrentScope, onScopeDispose, ref, toValue, watch } from "vue";
2
+ export const FRAME_RATE_RANGE = { min: 1, max: 60 };
3
+ const DEFAULT_FRAME_RATE = 15;
4
+ export function useCinePlayer(index, frameCount, options = {}) {
5
+ const rate = ref(clampFrameRate(options.frameRate ?? DEFAULT_FRAME_RATE));
6
+ const loop = ref(options.loop ?? true);
7
+ const bounce = ref(options.bounce ?? false);
8
+ const direction = ref(options.direction ?? 1);
9
+ const playing = ref(false);
10
+ const count = computed(() => Math.max(0, Math.trunc(toValue(frameCount)) || 0));
11
+ const canPlay = computed(() => count.value > 1);
12
+ let frame = 0;
13
+ let dueAt = 0;
14
+ function tick(now) {
15
+ if (!playing.value) return;
16
+ if (now >= dueAt) {
17
+ const interval = 1e3 / rate.value;
18
+ advance();
19
+ dueAt = now - dueAt > interval ? now + interval : dueAt + interval;
20
+ }
21
+ frame = requestAnimationFrame(tick);
22
+ }
23
+ function advance() {
24
+ const last = count.value - 1;
25
+ if (last < 1) return;
26
+ let next = index.value + direction.value;
27
+ if (next > last || next < 0) {
28
+ if (bounce.value) {
29
+ direction.value = direction.value === 1 ? -1 : 1;
30
+ next = index.value + direction.value;
31
+ } else if (loop.value) {
32
+ next = direction.value === 1 ? 0 : last;
33
+ } else {
34
+ pause();
35
+ return;
36
+ }
37
+ }
38
+ index.value = Math.min(Math.max(next, 0), last);
39
+ }
40
+ function play() {
41
+ if (!import.meta.client || playing.value || !canPlay.value) return;
42
+ const last = count.value - 1;
43
+ if (!bounce.value) {
44
+ if (direction.value === 1 && index.value >= last) index.value = 0;
45
+ if (direction.value === -1 && index.value <= 0) index.value = last;
46
+ }
47
+ playing.value = true;
48
+ dueAt = performance.now() + 1e3 / rate.value;
49
+ frame = requestAnimationFrame(tick);
50
+ }
51
+ function pause() {
52
+ if (!playing.value) return;
53
+ playing.value = false;
54
+ cancelAnimationFrame(frame);
55
+ frame = 0;
56
+ }
57
+ function toggle() {
58
+ if (playing.value) pause();
59
+ else play();
60
+ }
61
+ function setFrameRate(value) {
62
+ const clamped = clampFrameRate(value);
63
+ if (clamped === rate.value) return;
64
+ rate.value = clamped;
65
+ if (playing.value) dueAt = performance.now() + 1e3 / clamped;
66
+ }
67
+ const frameRate = computed({
68
+ get: () => rate.value,
69
+ set: setFrameRate
70
+ });
71
+ function setDirection(value) {
72
+ direction.value = value;
73
+ }
74
+ watch(canPlay, (value) => {
75
+ if (!value) pause();
76
+ });
77
+ if (getCurrentScope()) onScopeDispose(pause);
78
+ return {
79
+ playing,
80
+ frameRate,
81
+ loop,
82
+ bounce,
83
+ direction,
84
+ canPlay,
85
+ play,
86
+ pause,
87
+ toggle,
88
+ setFrameRate,
89
+ setDirection
90
+ };
91
+ }
92
+ function clampFrameRate(value) {
93
+ if (!Number.isFinite(value)) return DEFAULT_FRAME_RATE;
94
+ return Math.min(Math.max(value, FRAME_RATE_RANGE.min), FRAME_RATE_RANGE.max);
95
+ }
@@ -0,0 +1,17 @@
1
+ import type { CornerstoneLibs, CornerstoneModuleOptions } from '../types.js';
2
+ /**
3
+ * Access to the initialised Cornerstone3D libraries.
4
+ *
5
+ * Calling this on the client starts initialisation if it has not started
6
+ * already; `ready` flips once `core`, the DICOM image loader and the tools are
7
+ * all up. Initialisation itself is shared process-wide, so calling this from
8
+ * ten components still initialises once.
9
+ */
10
+ export declare function useCornerstone(): {
11
+ libs: import("vue").ShallowRef<CornerstoneLibs | null, CornerstoneLibs | null>;
12
+ ready: import("vue").ComputedRef<boolean>;
13
+ error: import("vue").ShallowRef<Error | null, Error | null>;
14
+ pending: import("vue").ShallowRef<boolean, boolean>;
15
+ ensure: (overrides?: CornerstoneModuleOptions) => Promise<CornerstoneLibs>;
16
+ options: import("../types.js").ResolvedCornerstoneOptions;
17
+ };
@@ -0,0 +1,30 @@
1
+ import { computed, shallowRef } from "vue";
2
+ import {
3
+ ensureCornerstone,
4
+ getCornerstoneOptions,
5
+ getLoadedCornerstone
6
+ } from "../cornerstone.js";
7
+ export function useCornerstone() {
8
+ const libs = shallowRef(getLoadedCornerstone());
9
+ const error = shallowRef(null);
10
+ const pending = shallowRef(false);
11
+ const ready = computed(() => libs.value !== null);
12
+ async function ensure(overrides) {
13
+ pending.value = true;
14
+ try {
15
+ libs.value = await ensureCornerstone(overrides);
16
+ error.value = null;
17
+ return libs.value;
18
+ } catch (caught) {
19
+ error.value = caught instanceof Error ? caught : new Error(String(caught));
20
+ throw error.value;
21
+ } finally {
22
+ pending.value = false;
23
+ }
24
+ }
25
+ if (import.meta.client && !libs.value) {
26
+ ensure().catch(() => {
27
+ });
28
+ }
29
+ return { libs, ready, error, pending, ensure, options: getCornerstoneOptions() };
30
+ }
@@ -0,0 +1,24 @@
1
+ import { addCornerstoneMessages, formatBytes, n, setCornerstoneLocale, setCornerstoneTranslator, t } from '../i18n/index.js';
2
+ /**
3
+ * Locale, translation and number formatting for this module's strings.
4
+ *
5
+ * The locale lives in a module-scoped ref, so every viewer in the app switches
6
+ * together and `t()` re-runs inside any computed or render that read it.
7
+ *
8
+ * `dir` is for an app's own chrome. Do not apply it to the viewport: a DICOM
9
+ * image must never be mirrored, because left and right are clinical facts about
10
+ * the patient. `<CornerstoneViewport>` therefore pins itself to `dir="ltr"`.
11
+ */
12
+ export declare function useCornerstoneI18n(): {
13
+ locale: import("vue").WritableComputedRef<string, string>;
14
+ setLocale: typeof setCornerstoneLocale;
15
+ availableLocales: import("vue").ComputedRef<string[]>;
16
+ /** `'rtl'` or `'ltr'` for the active locale. */
17
+ dir: import("vue").ComputedRef<"ltr" | "rtl">;
18
+ isRtl: import("vue").ComputedRef<boolean>;
19
+ t: typeof t;
20
+ n: typeof n;
21
+ formatBytes: typeof formatBytes;
22
+ addMessages: typeof addCornerstoneMessages;
23
+ setTranslator: typeof setCornerstoneTranslator;
24
+ };
@@ -0,0 +1,32 @@
1
+ import { computed } from "vue";
2
+ import {
3
+ addCornerstoneMessages,
4
+ dirForLocale,
5
+ formatBytes,
6
+ getCornerstoneLocale,
7
+ getCornerstoneLocales,
8
+ isRtlLocale,
9
+ n,
10
+ setCornerstoneLocale,
11
+ setCornerstoneTranslator,
12
+ t
13
+ } from "../i18n/index.js";
14
+ export function useCornerstoneI18n() {
15
+ const locale = computed({
16
+ get: () => getCornerstoneLocale(),
17
+ set: (value) => setCornerstoneLocale(value)
18
+ });
19
+ return {
20
+ locale,
21
+ setLocale: setCornerstoneLocale,
22
+ availableLocales: computed(() => getCornerstoneLocales()),
23
+ /** `'rtl'` or `'ltr'` for the active locale. */
24
+ dir: computed(() => dirForLocale(locale.value)),
25
+ isRtl: computed(() => isRtlLocale(locale.value)),
26
+ t,
27
+ n,
28
+ formatBytes,
29
+ addMessages: addCornerstoneMessages,
30
+ setTranslator: setCornerstoneTranslator
31
+ };
32
+ }
@@ -0,0 +1,21 @@
1
+ import type { ToolBinding } from '../types.js';
2
+ import type { Types as ToolsTypes } from '@cornerstonejs/tools';
3
+ /**
4
+ * Tool group management.
5
+ *
6
+ * Note the two different names in play: `WindowLevelTool` is the *class* export
7
+ * from `@cornerstonejs/tools`, while `'WindowLevel'` (its static `toolName`) is
8
+ * what tool groups speak. Both are accepted here and resolved to the latter.
9
+ */
10
+ export declare function useCornerstoneTools(toolGroupId?: string): {
11
+ toolGroupId: string;
12
+ ensureGroup: () => Promise<ToolsTypes.IToolGroup>;
13
+ addViewport: (viewportId: string, renderingEngineId?: string) => Promise<void>;
14
+ removeViewport: (viewportId: string, renderingEngineId?: string) => void;
15
+ setActive: (tool: string, bindings?: ToolBinding[]) => Promise<void>;
16
+ setPassive: (tool: string) => Promise<void>;
17
+ setEnabled: (tool: string) => Promise<void>;
18
+ setDisabled: (tool: string) => Promise<void>;
19
+ getActiveTool: () => string | null;
20
+ destroy: () => void;
21
+ };