med-viewer-sdk 0.1.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 (87) hide show
  1. package/README.md +253 -0
  2. package/dist/adapters/vue/MedViewer.d.ts +17 -0
  3. package/dist/adapters/vue/index.d.ts +2 -0
  4. package/dist/core/AnnoAnnotator.d.ts +15 -0
  5. package/dist/core/BaseAnnotator.d.ts +33 -0
  6. package/dist/core/ColorAdjustPlugin.d.ts +29 -0
  7. package/dist/core/Coords.d.ts +6 -0
  8. package/dist/core/Engine.d.ts +57 -0
  9. package/dist/core/KonvaAnnotator.d.ts +36 -0
  10. package/dist/core/Scalebar.d.ts +42 -0
  11. package/dist/core/SelectionPlugin.d.ts +102 -0
  12. package/dist/core/Toolbar.d.ts +32 -0
  13. package/dist/med-viewer-sdk.d.ts +6 -0
  14. package/dist/med-viewer-sdk.mjs +14248 -0
  15. package/dist/med-viewer-sdk.umd.js +2 -0
  16. package/dist/style.css +1 -0
  17. package/package.json +34 -0
  18. package/src/adapters/vue/MedViewer.ts +37 -0
  19. package/src/adapters/vue/index.ts +4 -0
  20. package/src/assets/icons/button_grouphover.png +0 -0
  21. package/src/assets/icons/button_hover.png +0 -0
  22. package/src/assets/icons/button_pressed.png +0 -0
  23. package/src/assets/icons/button_rest.png +0 -0
  24. package/src/assets/icons/flip_grouphover.png +0 -0
  25. package/src/assets/icons/flip_hover.png +0 -0
  26. package/src/assets/icons/flip_pressed.png +0 -0
  27. package/src/assets/icons/flip_rest.png +0 -0
  28. package/src/assets/icons/fullpage_grouphover.png +0 -0
  29. package/src/assets/icons/fullpage_hover.png +0 -0
  30. package/src/assets/icons/fullpage_pressed.png +0 -0
  31. package/src/assets/icons/fullpage_rest.png +0 -0
  32. package/src/assets/icons/home_grouphover.png +0 -0
  33. package/src/assets/icons/home_hover.png +0 -0
  34. package/src/assets/icons/home_pressed.png +0 -0
  35. package/src/assets/icons/home_rest.png +0 -0
  36. package/src/assets/icons/next_grouphover.png +0 -0
  37. package/src/assets/icons/next_hover.png +0 -0
  38. package/src/assets/icons/next_pressed.png +0 -0
  39. package/src/assets/icons/next_rest.png +0 -0
  40. package/src/assets/icons/previous_grouphover.png +0 -0
  41. package/src/assets/icons/previous_hover.png +0 -0
  42. package/src/assets/icons/previous_pressed.png +0 -0
  43. package/src/assets/icons/previous_rest.png +0 -0
  44. package/src/assets/icons/rotateleft_grouphover.png +0 -0
  45. package/src/assets/icons/rotateleft_hover.png +0 -0
  46. package/src/assets/icons/rotateleft_pressed.png +0 -0
  47. package/src/assets/icons/rotateleft_rest.png +0 -0
  48. package/src/assets/icons/rotateright_grouphover.png +0 -0
  49. package/src/assets/icons/rotateright_hover.png +0 -0
  50. package/src/assets/icons/rotateright_pressed.png +0 -0
  51. package/src/assets/icons/rotateright_rest.png +0 -0
  52. package/src/assets/icons/selection_cancel_grouphover.png +0 -0
  53. package/src/assets/icons/selection_cancel_hover.png +0 -0
  54. package/src/assets/icons/selection_cancel_pressed.png +0 -0
  55. package/src/assets/icons/selection_cancel_rest.png +0 -0
  56. package/src/assets/icons/selection_confirm_grouphover.png +0 -0
  57. package/src/assets/icons/selection_confirm_hover.png +0 -0
  58. package/src/assets/icons/selection_confirm_pressed.png +0 -0
  59. package/src/assets/icons/selection_confirm_rest.png +0 -0
  60. package/src/assets/icons/selection_grouphover.png +0 -0
  61. package/src/assets/icons/selection_hover.png +0 -0
  62. package/src/assets/icons/selection_pressed.png +0 -0
  63. package/src/assets/icons/selection_rest.png +0 -0
  64. package/src/assets/icons/tool_anno.png +0 -0
  65. package/src/assets/icons/tool_selection.png +0 -0
  66. package/src/assets/icons/zoomin_grouphover.png +0 -0
  67. package/src/assets/icons/zoomin_hover.png +0 -0
  68. package/src/assets/icons/zoomin_pressed.png +0 -0
  69. package/src/assets/icons/zoomin_rest.png +0 -0
  70. package/src/assets/icons/zoomout_grouphover.png +0 -0
  71. package/src/assets/icons/zoomout_hover.png +0 -0
  72. package/src/assets/icons/zoomout_pressed.png +0 -0
  73. package/src/assets/icons/zoomout_rest.png +0 -0
  74. package/src/core/AnnoAnnotator.ts +102 -0
  75. package/src/core/BaseAnnotator.ts +43 -0
  76. package/src/core/ColorAdjustPlugin.ts +256 -0
  77. package/src/core/Coords.ts +9 -0
  78. package/src/core/Engine.ts +246 -0
  79. package/src/core/KonvaAnnotator.ts +185 -0
  80. package/src/core/Scalebar.ts +87 -0
  81. package/src/core/SelectionPlugin.ts +252 -0
  82. package/src/core/Toolbar.ts +370 -0
  83. package/src/index.ts +21 -0
  84. package/src/plugins/ShapeLabelsFormatter.js +435 -0
  85. package/src/plugins/openseadragon-scalebar.js +592 -0
  86. package/src/plugins/openseadragon-selection.js +657 -0
  87. package/src/types/type.d.ts +9 -0
@@ -0,0 +1,252 @@
1
+ import OpenSeadragon from "openseadragon";
2
+ import "../plugins/openseadragon-selection.js";
3
+ import selectionRest from "@/assets/icons/selection_rest.png";
4
+ import selectionGroup from "@/assets/icons/selection_grouphover.png";
5
+ import selectionHover from "@/assets/icons/selection_hover.png";
6
+ import selectionDown from "@/assets/icons/selection_pressed.png";
7
+ import selectionConfirmRest from "@/assets/icons/selection_confirm_rest.png";
8
+ import selectionConfirmGroup from "@/assets/icons/selection_confirm_grouphover.png";
9
+ import selectionConfirmHover from "@/assets/icons/selection_confirm_hover.png";
10
+ import selectionConfirmDown from "@/assets/icons/selection_confirm_pressed.png";
11
+ import selectionCancelRest from "@/assets/icons/selection_cancel_rest.png";
12
+ import selectionCancelGroup from "@/assets/icons/selection_cancel_grouphover.png";
13
+ import selectionCancelHover from "@/assets/icons/selection_cancel_hover.png";
14
+ import selectionCancelDown from "@/assets/icons/selection_cancel_pressed.png";
15
+
16
+ // 最简单的办法:直接向 OSD 的字符串库注入翻译
17
+ OpenSeadragon.setString("Tooltips.SelectionToggle", "Toggle Selection"); // 切换选择
18
+ OpenSeadragon.setString("Tooltips.SelectionConfirm", "Confirm Selection"); // 确认选择
19
+ OpenSeadragon.setString("Tooltips.SelectionCancel", "Cancel Selection"); // 取消选择
20
+
21
+ export interface SelectionOptions {
22
+ element?: HTMLElement | null;
23
+ showSelectionControl?: boolean;
24
+ toggleButton?: HTMLElement | null;
25
+ showConfirmDenyButtons?: boolean;
26
+ styleConfirmDenyButtons?: boolean;
27
+ returnPixelCoordinates?: boolean;
28
+ keyboardShortcut?: string;
29
+ rect?: OpenSeadragon.Rect | null;
30
+ allowRotation?: boolean;
31
+ startRotated?: boolean;
32
+ startRotatedHeight?: number;
33
+ restrictToImage?: boolean;
34
+ onSelection?: (rect: OpenSeadragon.Rect) => void;
35
+ onSelectionCanceled?: () => void;
36
+ onSelectionChange?: (rect: OpenSeadragon.Rect) => void;
37
+ onSelectionToggled?: (state: { enabled: boolean }) => void;
38
+ prefixUrl?: string | null;
39
+ navImages?: {
40
+ selection: {
41
+ REST: string;
42
+ GROUP: string;
43
+ HOVER: string;
44
+ DOWN: string;
45
+ };
46
+ selectionConfirm: {
47
+ REST: string;
48
+ GROUP: string;
49
+ HOVER: string;
50
+ DOWN: string;
51
+ };
52
+ selectionCancel: {
53
+ REST: string;
54
+ GROUP: string;
55
+ HOVER: string;
56
+ DOWN: string;
57
+ };
58
+ };
59
+ borderStyle?: {
60
+ width: string;
61
+ color: string;
62
+ };
63
+ handleStyle?: {
64
+ top: string;
65
+ left: string;
66
+ width: string;
67
+ height: string;
68
+ margin: string;
69
+ background: string;
70
+ border: string;
71
+ };
72
+ cornersStyle?: {
73
+ width: string;
74
+ height: string;
75
+ background: string;
76
+ border: string;
77
+ };
78
+ }
79
+
80
+ export class SelectionPlugin {
81
+ private viewer: OpenSeadragon.Viewer;
82
+ private selection: any; // OpenSeadragonSelection instance
83
+ private options: SelectionOptions;
84
+
85
+ constructor(viewer: OpenSeadragon.Viewer, options: SelectionOptions = {}) {
86
+ this.viewer = viewer;
87
+ this.options = {
88
+ showSelectionControl: true,
89
+ showConfirmDenyButtons: true,
90
+ styleConfirmDenyButtons: true,
91
+ returnPixelCoordinates: true,
92
+ keyboardShortcut: "c",
93
+ allowRotation: true,
94
+ startRotated: false,
95
+ restrictToImage: false,
96
+ prefixUrl: "",
97
+ navImages: {
98
+ selection: {
99
+ REST: selectionRest,
100
+ GROUP: selectionGroup,
101
+ HOVER: selectionHover,
102
+ DOWN: selectionDown,
103
+ },
104
+ selectionConfirm: {
105
+ REST: selectionConfirmRest,
106
+ GROUP: selectionConfirmGroup,
107
+ HOVER: selectionConfirmHover,
108
+ DOWN: selectionConfirmDown,
109
+ },
110
+ selectionCancel: {
111
+ REST: selectionCancelRest,
112
+ GROUP: selectionCancelGroup,
113
+ HOVER: selectionCancelHover,
114
+ DOWN: selectionCancelDown,
115
+ },
116
+ },
117
+ borderStyle: {
118
+ width: "2px", // 稍微加粗,更有质感
119
+
120
+ color: "#4CAF50", // 使用经典的“激活蓝”
121
+ },
122
+
123
+ handleStyle: {
124
+ top: "50%",
125
+ left: "50%",
126
+ width: "10px", // 增大触点,方便鼠标点击
127
+ height: "10px",
128
+ margin: "-6px 0 0 -6px",
129
+ background: "#4CAF50", // 白色背景
130
+ border: "2px solid #4CAF50", // 蓝色边框
131
+ },
132
+
133
+
134
+ cornersStyle: {
135
+ width: "12px", // 角部手柄稍微比边部大一点
136
+ height: "12px",
137
+ background: "#4CAF50",
138
+ border: "2px solid #4CAF50",
139
+ },
140
+
141
+ ...options,
142
+ };
143
+
144
+ this.init();
145
+ }
146
+
147
+ private init(): void {
148
+ // 等待 viewer 初始化完成
149
+ this.viewer.addOnceHandler("open", () => {
150
+ this.setupSelection();
151
+ });
152
+ }
153
+ private setupSelection(): void {
154
+ try {
155
+ // 检查是否有 selection 插件
156
+ if (typeof (this.viewer as any).selection !== "function") {
157
+ console.warn(
158
+ "OpenSeadragonSelection plugin not found. Please include openseadragonselection.js",
159
+ );
160
+ return;
161
+ }
162
+
163
+ // 初始化 selection
164
+ this.selection = (this.viewer as any).selection(this.options);
165
+
166
+ console.log(
167
+ "[SelectionPlugin] Selection plugin initialized",
168
+ this.selection,
169
+ );
170
+ } catch (error) {
171
+ console.error(
172
+ "[SelectionPlugin] Failed to initialize selection plugin:",
173
+ error,
174
+ );
175
+ }
176
+ }
177
+
178
+ /**
179
+ * 启用选择模式
180
+ */
181
+ enable(): void {
182
+ if (this.selection) {
183
+ this.selection.enable();
184
+ }
185
+ }
186
+
187
+ /**
188
+ * 禁用选择模式
189
+ */
190
+ disable(): void {
191
+ if (this.selection) {
192
+ this.selection.disable();
193
+ }
194
+ }
195
+
196
+ /**
197
+ * 切换选择模式状态
198
+ */
199
+ toggleState(): void {
200
+ if (this.selection) {
201
+ this.selection.toggleState();
202
+ }
203
+ }
204
+
205
+ /**
206
+ * 获取当前选择区域
207
+ */
208
+ getSelection(): OpenSeadragon.Rect | null {
209
+ if (this.selection) {
210
+ return this.selection.getSelection();
211
+ }
212
+ return null;
213
+ }
214
+
215
+ /**
216
+ * 设置选择区域
217
+ */
218
+ setSelection(rect: OpenSeadragon.Rect): void {
219
+ if (this.selection) {
220
+ this.selection.setSelection(rect);
221
+ }
222
+ }
223
+
224
+ /**
225
+ * 清除选择
226
+ */
227
+ clearSelection(): void {
228
+ if (this.selection) {
229
+ this.selection.clearSelection();
230
+ }
231
+ }
232
+
233
+ /**
234
+ * 检查是否启用选择模式
235
+ */
236
+ isEnabled(): boolean {
237
+ if (this.selection) {
238
+ return this.selection.isEnabled();
239
+ }
240
+ return false;
241
+ }
242
+
243
+ /**
244
+ * 销毁插件
245
+ */
246
+ destroy(): void {
247
+ if (this.selection) {
248
+ this.selection.destroy();
249
+ this.selection = null;
250
+ }
251
+ }
252
+ }
@@ -0,0 +1,370 @@
1
+ import type { MedViewerEngine } from "./Engine";
2
+ import buttonAnno from "@/assets/icons/tool_anno.png";
3
+ import buttonSelection from "@/assets/icons/tool_selection.png";
4
+
5
+ export type ToolbarPosition =
6
+ | "TOP_LEFT" | "TOP_CENTER" | "TOP_RIGHT"
7
+ | "BOTTOM_LEFT" | "BOTTOM_CENTER" | "BOTTOM_RIGHT"
8
+ | "MIDDLE_LEFT" | "MIDDLE_RIGHT";
9
+
10
+ export interface ToolbarButton {
11
+ id: string;
12
+ label?: string;
13
+ icon?: string;
14
+ dropdownContent?: (engine: MedViewerEngine, hide: () => void) => HTMLElement;
15
+ onClick?: (engine: MedViewerEngine, hide: () => void) => void;
16
+ }
17
+
18
+ export interface ToolbarOptions {
19
+ position?: ToolbarPosition;
20
+ buttons?: ToolbarButton[];
21
+ }
22
+
23
+ /**
24
+ * 预设:标注工具下拉内容生成器
25
+ */
26
+ const createAnnoDropdownContent = (engine: MedViewerEngine, hide: () => void): HTMLElement => {
27
+ const container = document.createElement("div");
28
+ container.className = "med-toolbar-dropdown-inner";
29
+
30
+ let selectedColor = "#ff0000";
31
+ const colorSection = document.createElement("div");
32
+ colorSection.innerHTML = `<div class="med-toolbar-section-title">标注颜色</div>`;
33
+ const colorGrid = document.createElement("div");
34
+ colorGrid.className = "med-color-grid";
35
+
36
+ const colors = ["#ff0000", "#00ff00", "#0000ff", "#ffff00", "#00ffff", "#ffffff"];
37
+ colors.forEach((c) => {
38
+ const dot = document.createElement("div");
39
+ dot.className = "med-color-item";
40
+ dot.style.backgroundColor = c;
41
+ dot.onclick = () => {
42
+ selectedColor = c;
43
+ colorGrid.querySelectorAll(".med-color-item").forEach((el) => el.classList.remove("active"));
44
+ dot.classList.add("active");
45
+ };
46
+ colorGrid.appendChild(dot);
47
+ });
48
+ colorSection.appendChild(colorGrid);
49
+ container.appendChild(colorSection);
50
+
51
+ const toolSection = document.createElement("div");
52
+ toolSection.innerHTML = `<div class="med-toolbar-section-title">标注形状</div>`;
53
+ const toolGrid = document.createElement("div");
54
+ toolGrid.className = "med-tool-grid";
55
+
56
+ const tools = [
57
+ { id: "rect", label: "矩形" }, { id: "polygon", label: "多边形" },
58
+ { id: "circle", label: "圆形" }, { id: "ellipse", label: "椭圆" },
59
+ { id: "line", label: "线段" }, { id: "freehand", label: "手绘" },
60
+ ];
61
+
62
+ tools.forEach((t) => {
63
+ const btn = document.createElement("button");
64
+ btn.className = "med-tool-item";
65
+ btn.textContent = t.label;
66
+ btn.onclick = () => {
67
+ engine.setInteractionEffect("anno");
68
+ if (engine.anno) engine.anno.setTool(t.id as any, selectedColor);
69
+ hide();
70
+ };
71
+ toolGrid.appendChild(btn);
72
+ });
73
+ toolSection.appendChild(toolGrid);
74
+ container.appendChild(toolSection);
75
+
76
+ return container;
77
+ };
78
+
79
+ /**
80
+ * 默认按钮配置
81
+ */
82
+ const DEFAULT_BUTTONS: ToolbarButton[] = [
83
+ { id: "anno", icon: buttonAnno, dropdownContent: createAnnoDropdownContent, label: "标注设置" },
84
+ { id: "selection", icon: buttonSelection, label: "截图设置",
85
+ onClick: (engine: MedViewerEngine, hide: () => void) => {
86
+ engine.selection?.toggleState();
87
+ hide();
88
+ },
89
+ },
90
+ ];
91
+
92
+ const STYLE_ID = "med-toolbar-styles";
93
+
94
+ /**
95
+ * MedToolbar 主类
96
+ */
97
+ export class MedToolbar {
98
+ private engine: MedViewerEngine;
99
+ private options: ToolbarOptions;
100
+ private element: HTMLDivElement;
101
+ private dropdownElement: HTMLDivElement | null = null;
102
+ private outsideClickHandler: ((e: MouseEvent) => void) | null = null;
103
+
104
+ constructor(engine: MedViewerEngine, options: ToolbarOptions = {}) {
105
+ this.engine = engine;
106
+ this.options = options;
107
+ this.element = document.createElement("div");
108
+ this.element.className = this.getClassName();
109
+
110
+ this.injectStyles();
111
+ this.render();
112
+ this.mount();
113
+ }
114
+
115
+ public destroy(): void {
116
+ this.closeDropdown(true); // 销毁时强制立即移除
117
+ this.element.remove();
118
+ (this.engine as any) = null;
119
+ }
120
+
121
+ private render(): void {
122
+ this.element.innerHTML = "";
123
+ const defaultButtonMap = new Map(DEFAULT_BUTTONS.map((btn) => [btn.id, btn]));
124
+ const mergedButtons: ToolbarButton[] = [];
125
+
126
+ // 合并配置:用户自定义优先
127
+ if (this.options.buttons) {
128
+ this.options.buttons.forEach((userBtn) => {
129
+ const defaultBtn = defaultButtonMap.get(userBtn.id);
130
+ if (defaultBtn) {
131
+ mergedButtons.push({ ...defaultBtn, ...userBtn });
132
+ defaultButtonMap.delete(userBtn.id);
133
+ } else {
134
+ mergedButtons.push(userBtn);
135
+ }
136
+ });
137
+ }
138
+ mergedButtons.push(...Array.from(defaultButtonMap.values()));
139
+
140
+ mergedButtons.forEach((btnConfig) => {
141
+ const wrapper = document.createElement("div");
142
+ wrapper.className = "med-toolbar-item-wrapper";
143
+
144
+ const btn = document.createElement("button");
145
+ btn.className = "med-main-btn";
146
+
147
+ if (btnConfig.icon) {
148
+ const img = document.createElement("img");
149
+ img.src = btnConfig.icon;
150
+ img.alt = btnConfig.label || btnConfig.id;
151
+ btn.appendChild(img);
152
+ } else if (btnConfig.label) {
153
+ btn.textContent = btnConfig.label;
154
+ }
155
+
156
+ btn.onclick = (e) => {
157
+ e.stopPropagation();
158
+ // 如果当前点击的按钮对应的下拉框已打开,则关闭它
159
+ if (this.dropdownElement && this.dropdownElement.parentElement === wrapper) {
160
+ this.closeDropdown();
161
+ return;
162
+ }
163
+
164
+ // 先尝试关闭现有下拉框
165
+ this.closeDropdown();
166
+
167
+ if (btnConfig.onClick) {
168
+ btnConfig.onClick(this.engine, () => this.closeDropdown());
169
+ } else if (btnConfig.dropdownContent) {
170
+ this.showDropdown(wrapper, btnConfig);
171
+ }
172
+ };
173
+
174
+ wrapper.appendChild(btn);
175
+ this.element.appendChild(wrapper);
176
+ });
177
+ }
178
+
179
+ private showDropdown(parent: HTMLElement, config: ToolbarButton) {
180
+ this.dropdownElement = document.createElement("div");
181
+ this.dropdownElement.className = "med-toolbar-dropdown";
182
+
183
+ const content = config.dropdownContent!(this.engine, () => this.closeDropdown());
184
+ this.dropdownElement.appendChild(content);
185
+ parent.appendChild(this.dropdownElement);
186
+
187
+ // 1. 边界检查调整位置
188
+ this.adjustDropdownPosition();
189
+
190
+ // 2. 触发动画 (下一帧添加 show 类)
191
+ requestAnimationFrame(() => {
192
+ this.dropdownElement?.classList.add("show");
193
+ });
194
+
195
+ // 3. 点击外部关闭
196
+ this.outsideClickHandler = (e: MouseEvent) => {
197
+ if (this.dropdownElement && !this.dropdownElement.contains(e.target as Node)) {
198
+ this.closeDropdown();
199
+ }
200
+ };
201
+ setTimeout(() => document.addEventListener("click", this.outsideClickHandler!), 0);
202
+ }
203
+
204
+ private adjustDropdownPosition() {
205
+ if (!this.dropdownElement) return;
206
+
207
+ const rect = this.dropdownElement.getBoundingClientRect();
208
+ const viewportWidth = window.innerWidth;
209
+ const padding = 12;
210
+
211
+ // 检查右边缘
212
+ if (rect.right > viewportWidth) {
213
+ this.dropdownElement.style.left = "auto";
214
+ this.dropdownElement.style.right = "0";
215
+ this.dropdownElement.style.transform = "translateX(0) translateY(10px)";
216
+ this.dropdownElement.setAttribute('data-adjusted', 'true');
217
+ }
218
+ // 检查左边缘
219
+ else if (rect.left < 0) {
220
+ this.dropdownElement.style.left = "0";
221
+ this.dropdownElement.style.transform = "translateX(0) translateY(10px)";
222
+ this.dropdownElement.setAttribute('data-adjusted', 'true');
223
+ }
224
+
225
+ // 针对顶部位置调整动画起始点
226
+ const isTop = (this.options.position || "").includes("TOP");
227
+ if (isTop) {
228
+ const currentTransform = this.dropdownElement.style.transform;
229
+ this.dropdownElement.style.transform = currentTransform.replace("translateY(10px)", "translateY(-10px)");
230
+ }
231
+ }
232
+
233
+ private closeDropdown(immediate = false) {
234
+ if (this.dropdownElement) {
235
+ const el = this.dropdownElement;
236
+ this.dropdownElement = null; // 立即清除引用防止重复触发
237
+
238
+ if (immediate) {
239
+ el.remove();
240
+ } else {
241
+ el.classList.remove("show");
242
+ // 等待 CSS 过渡动画结束后移除元素
243
+ setTimeout(() => el.remove(), 200);
244
+ }
245
+ }
246
+ if (this.outsideClickHandler) {
247
+ document.removeEventListener("click", this.outsideClickHandler);
248
+ this.outsideClickHandler = null;
249
+ }
250
+ }
251
+
252
+ private mount(): void {
253
+ const container = this.engine.viewer?.element;
254
+ if (container) container.appendChild(this.element);
255
+ }
256
+
257
+ private getClassName(): string {
258
+ const pos = this.options.position || "BOTTOM_CENTER";
259
+ return `med-toolbar med-toolbar--${pos}`;
260
+ }
261
+
262
+ private injectStyles(): void {
263
+ if (document.getElementById(STYLE_ID)) return;
264
+ const style = document.createElement("style");
265
+ style.id = STYLE_ID;
266
+ style.textContent = `
267
+ .med-toolbar {
268
+ position: absolute;
269
+ display: flex;
270
+ gap: 12px;
271
+ padding: 10px;
272
+ z-index: 100;
273
+ background: rgba(24, 28, 36, 0.85);
274
+ border-radius: 12px;
275
+ backdrop-filter: blur(10px);
276
+ border: 1px solid rgba(255,255,255,0.1);
277
+ box-shadow: 0 8px 32px rgba(0,0,0,0.3);
278
+ }
279
+
280
+ /* 定位 */
281
+ .med-toolbar--TOP_LEFT { top: 18px; left: 18px; }
282
+ .med-toolbar--TOP_CENTER { top: 18px; left: 50%; transform: translateX(-50%); }
283
+ .med-toolbar--TOP_RIGHT { top: 18px; right: 18px; }
284
+ .med-toolbar--BOTTOM_LEFT { bottom: 18px; left: 18px; }
285
+ .med-toolbar--BOTTOM_CENTER { bottom: 18px; left: 50%; transform: translateX(-50%); }
286
+ .med-toolbar--BOTTOM_RIGHT { bottom: 18px; right: 18px; }
287
+ .med-toolbar--MIDDLE_LEFT { top: 50%; left: 18px; transform: translateY(-50%); flex-direction: column; }
288
+ .med-toolbar--MIDDLE_RIGHT { top: 50%; right: 18px; transform: translateY(-50%); flex-direction: column; }
289
+
290
+ .med-toolbar-item-wrapper { position: relative; }
291
+
292
+ /* 按钮及动画 */
293
+ .med-main-btn {
294
+ background: rgba(255,255,255,0.08);
295
+ color: #f2f5f8;
296
+ border: none;
297
+ padding: 8px 16px;
298
+ border-radius: 8px;
299
+ cursor: pointer;
300
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
301
+ display: flex;
302
+ align-items: center;
303
+ justify-content: center;
304
+ }
305
+ .med-main-btn:hover { background: rgba(49, 208, 170, 0.2); }
306
+ .med-main-btn:active { transform: scale(0.9); }
307
+ .med-main-btn img { width: 24px; height: 24px; }
308
+
309
+ /* 下拉框基础及进场动画 */
310
+ .med-toolbar-dropdown {
311
+ position: absolute;
312
+ bottom: calc(100% + 12px);
313
+ left: 50%;
314
+ background: #181c24;
315
+ border: 1px solid rgba(255,255,255,0.1);
316
+ border-radius: 12px;
317
+ padding: 16px;
318
+ box-shadow: 0 10px 40px rgba(0,0,0,0.5);
319
+ min-width: 220px;
320
+ z-index: 101;
321
+
322
+ /* 初始动画状态 */
323
+ opacity: 0;
324
+ pointer-events: none;
325
+ transform: translateX(-50%) translateY(10px);
326
+ transition: opacity 0.2s ease, transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
327
+ }
328
+
329
+ /* 动画显示状态 */
330
+ .med-toolbar-dropdown.show {
331
+ opacity: 1;
332
+ pointer-events: auto;
333
+ transform: translateX(-50%) translateY(0) !important;
334
+ }
335
+
336
+ /* 边界修正后的显示状态 */
337
+ .med-toolbar-dropdown[data-adjusted="true"].show {
338
+ transform: translateX(0) translateY(0) !important;
339
+ }
340
+
341
+ /* 顶部位置下拉动画修正 */
342
+ [class*="med-toolbar--TOP"] .med-toolbar-dropdown {
343
+ bottom: auto;
344
+ top: calc(100% + 12px);
345
+ transform: translateX(-50%) translateY(-10px);
346
+ }
347
+
348
+ /* 侧边位置逻辑 */
349
+ .med-toolbar--MIDDLE_LEFT .med-toolbar-dropdown {
350
+ left: calc(100% + 12px); top: 0; bottom: auto; transform: translateX(-10px);
351
+ }
352
+ .med-toolbar--MIDDLE_LEFT .med-toolbar-dropdown.show { transform: translateX(0) !important; }
353
+
354
+ .med-toolbar--MIDDLE_RIGHT .med-toolbar-dropdown {
355
+ left: auto; right: calc(100% + 12px); top: 0; bottom: auto; transform: translateX(10px);
356
+ }
357
+ .med-toolbar--MIDDLE_RIGHT .med-toolbar-dropdown.show { transform: translateX(0) !important; }
358
+
359
+ /* 内容样式 */
360
+ .med-toolbar-section-title { font-size: 11px; color: rgba(255,255,255,0.4); margin-bottom: 10px; letter-spacing: 1px; }
361
+ .med-color-grid { display: flex; gap: 10px; margin-bottom: 20px; }
362
+ .med-color-item { width: 24px; height: 24px; border-radius: 50%; cursor: pointer; border: 2px solid transparent; transition: 0.2s; }
363
+ .med-color-item.active { border-color: #fff; transform: scale(1.15); box-shadow: 0 0 10px rgba(255,255,255,0.3); }
364
+ .med-tool-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
365
+ .med-tool-item { background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.05); color: #fff; padding: 8px 4px; border-radius: 6px; cursor: pointer; font-size: 12px; transition: 0.2s; }
366
+ .med-tool-item:hover { background: rgba(49, 208, 170, 0.2); border-color: rgba(49, 208, 170, 0.4); }
367
+ `;
368
+ document.head.appendChild(style);
369
+ }
370
+ }
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { MedViewerEngine } from './core/Engine';
2
+ import { KonvaAnnotator } from './core/KonvaAnnotator';
3
+ import { AnnoAnnotator } from './core/AnnoAnnotator';
4
+ import { MedToolbar } from './core/Toolbar';
5
+ import { SelectionPlugin } from './core/SelectionPlugin';
6
+ // import { ColorAdjustPlugin } from './core/ColorAdjustPlugin';
7
+
8
+ // 1. 导出供模块化开发使用 (Vue/React/Vite)
9
+ export { MedViewerEngine, KonvaAnnotator, AnnoAnnotator, MedToolbar, SelectionPlugin }; // , ColorAdjustPlugin };
10
+
11
+ // 2. 导出供原生 HTML 全局变量使用 (window.MedViewerSDK)
12
+ if (typeof window !== 'undefined') {
13
+ (window as any).MedViewerSDK = {
14
+ MedViewerEngine,
15
+ KonvaAnnotator,
16
+ AnnoAnnotator,
17
+ MedToolbar,
18
+ SelectionPlugin,
19
+ // ColorAdjustPlugin
20
+ };
21
+ }