electron-snapora 1.0.1
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/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.es.md +79 -0
- package/README.ja.md +79 -0
- package/README.ko.md +79 -0
- package/README.md +392 -0
- package/README.zh-CN.md +79 -0
- package/dist/core/index.cjs +184 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +97 -0
- package/dist/core/index.d.ts +97 -0
- package/dist/core/index.mjs +150 -0
- package/dist/core/index.mjs.map +1 -0
- package/dist/main/index.cjs +1637 -0
- package/dist/main/index.cjs.map +1 -0
- package/dist/main/index.d.cts +283 -0
- package/dist/main/index.d.ts +283 -0
- package/dist/main/index.mjs +1599 -0
- package/dist/main/index.mjs.map +1 -0
- package/dist/messages-DYY0MpGh.d.ts +76 -0
- package/dist/messages-ifub9bae.d.cts +76 -0
- package/dist/overlay/assets/index-7dVRGn83.css +1 -0
- package/dist/overlay/assets/index-CJf5iC1U.js +2 -0
- package/dist/overlay/assets/index-CJf5iC1U.js.map +1 -0
- package/dist/overlay/index.html +316 -0
- package/dist/overlay/preload.cjs +97 -0
- package/dist/overlay/preload.cjs.map +1 -0
- package/dist/overlay/preload.d.cts +16 -0
- package/dist/overlay/preload.d.ts +16 -0
- package/dist/overlay/preload.mjs +80 -0
- package/dist/overlay/preload.mjs.map +1 -0
- package/dist/preload/auto.cjs +24 -0
- package/dist/preload/auto.cjs.map +1 -0
- package/dist/preload/auto.d.cts +2 -0
- package/dist/preload/auto.d.ts +2 -0
- package/dist/preload/auto.mjs +22 -0
- package/dist/preload/auto.mjs.map +1 -0
- package/dist/preload/index.cjs +46 -0
- package/dist/preload/index.cjs.map +1 -0
- package/dist/preload/index.d.cts +18 -0
- package/dist/preload/index.d.ts +18 -0
- package/dist/preload/index.mjs +19 -0
- package/dist/preload/index.mjs.map +1 -0
- package/dist/types/index.cjs +19 -0
- package/dist/types/index.cjs.map +1 -0
- package/dist/types/index.d.cts +92 -0
- package/dist/types/index.d.ts +92 -0
- package/dist/types/index.mjs +1 -0
- package/dist/types/index.mjs.map +1 -0
- package/package.json +158 -0
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# electron-snapora — Electron 截图与标注工具
|
|
2
|
+
|
|
3
|
+
[English](https://github.com/electron-tools/electron-snapora/blob/main/README.md) | 简体中文 | [日本語](https://github.com/electron-tools/electron-snapora/blob/main/README.ja.md) | [한국어](https://github.com/electron-tools/electron-snapora/blob/main/README.ko.md) | [Español](https://github.com/electron-tools/electron-snapora/blob/main/README.es.md)
|
|
4
|
+
|
|
5
|
+
为 Electron 应用增加区域截图、交互式选区、图片标注、剪贴板复制和 PNG 保存能力。
|
|
6
|
+
|
|
7
|
+
## 功能
|
|
8
|
+
|
|
9
|
+
- 矩形区域截图和交互式截图层。
|
|
10
|
+
- 矩形、椭圆、箭头、画笔、文字和马赛克标注。
|
|
11
|
+
- 撤销、重做、剪贴板复制和原生 PNG 保存。
|
|
12
|
+
- 支持 TypeScript、ESM 和 CommonJS。
|
|
13
|
+
- 默认不依赖原生扩展,也不需要安装后编译。
|
|
14
|
+
|
|
15
|
+
## 快速开始
|
|
16
|
+
|
|
17
|
+
环境要求:Node.js 20 或更高版本、Electron `>=42 <44`。
|
|
18
|
+
|
|
19
|
+
### 1. 安装
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install electron-snapora
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
请将它保留在应用的生产环境 `dependencies` 中。
|
|
26
|
+
|
|
27
|
+
### 2. 配置主进程
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { app, BrowserWindow, ipcMain } from 'electron';
|
|
31
|
+
import { setupElectronSnapora } from 'electron-snapora/main';
|
|
32
|
+
|
|
33
|
+
app.whenReady().then(() => {
|
|
34
|
+
const snapora = setupElectronSnapora({ ipcMain });
|
|
35
|
+
const mainWindow = new BrowserWindow({
|
|
36
|
+
webPreferences: {
|
|
37
|
+
preload: snapora.preloadPath,
|
|
38
|
+
contextIsolation: true,
|
|
39
|
+
sandbox: true,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
mainWindow.loadFile('index.html');
|
|
44
|
+
app.once('before-quit', snapora.unregister);
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 3. 在 Renderer 中截图
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
const result = await window.electronSnapora.capture({ display: 'cursor' });
|
|
52
|
+
|
|
53
|
+
if (result.status === 'completed') {
|
|
54
|
+
console.log(result.data, result.bounds, result.output);
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`result.data` 是 PNG 字节数据。用户取消时返回 `cancelled`,处理失败时返回 `failed`。
|
|
59
|
+
|
|
60
|
+
## 已有 Preload 的应用
|
|
61
|
+
|
|
62
|
+
在应用自己的 Preload 中暴露 API,并保持该 Preload 由宿主构建工具打包:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { contextBridge, ipcRenderer } from 'electron';
|
|
66
|
+
import { exposeScreenshotApi } from 'electron-snapora/preload';
|
|
67
|
+
|
|
68
|
+
exposeScreenshotApi({ contextBridge, ipcRenderer });
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 打包说明
|
|
72
|
+
|
|
73
|
+
打包 Electron 主进程时,请将 `electron-snapora` 保持为 external,并确保它位于生产环境依赖中,这样 Overlay HTML、CSS 和 Preload 文件才会随应用一起安装。
|
|
74
|
+
|
|
75
|
+
主题、本地化、IPC 来源校验、并发队列、诊断和各类打包器配置请参阅[英文完整文档](https://github.com/electron-tools/electron-snapora/blob/main/README.md)。
|
|
76
|
+
|
|
77
|
+
仓库:[github.com/electron-tools/electron-snapora](https://github.com/electron-tools/electron-snapora)
|
|
78
|
+
|
|
79
|
+
许可证:[MIT](https://github.com/electron-tools/electron-snapora/blob/main/LICENSE)
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/core.ts
|
|
21
|
+
var core_exports = {};
|
|
22
|
+
__export(core_exports, {
|
|
23
|
+
AddElementCommand: () => AddElementCommand,
|
|
24
|
+
CommandStack: () => CommandStack,
|
|
25
|
+
RemoveElementCommand: () => RemoveElementCommand,
|
|
26
|
+
UpdateElementCommand: () => UpdateElementCommand,
|
|
27
|
+
clampRect: () => clampRect,
|
|
28
|
+
isRectValid: () => isRectValid,
|
|
29
|
+
normalizeRect: () => normalizeRect,
|
|
30
|
+
viewportPointToImagePoint: () => viewportPointToImagePoint
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(core_exports);
|
|
33
|
+
|
|
34
|
+
// src/core/geometry/rect.ts
|
|
35
|
+
function normalizeRect(start, end) {
|
|
36
|
+
return {
|
|
37
|
+
x: Math.min(start.x, end.x),
|
|
38
|
+
y: Math.min(start.y, end.y),
|
|
39
|
+
width: Math.abs(end.x - start.x),
|
|
40
|
+
height: Math.abs(end.y - start.y)
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function clampRect(rect, bounds) {
|
|
44
|
+
const left = Math.max(bounds.x, rect.x);
|
|
45
|
+
const top = Math.max(bounds.y, rect.y);
|
|
46
|
+
const right = Math.min(bounds.x + bounds.width, rect.x + rect.width);
|
|
47
|
+
const bottom = Math.min(bounds.y + bounds.height, rect.y + rect.height);
|
|
48
|
+
return {
|
|
49
|
+
x: left,
|
|
50
|
+
y: top,
|
|
51
|
+
width: Math.max(0, right - left),
|
|
52
|
+
height: Math.max(0, bottom - top)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function isRectValid(rect, minimumSize = 1) {
|
|
56
|
+
return rect.width >= minimumSize && rect.height >= minimumSize;
|
|
57
|
+
}
|
|
58
|
+
function viewportPointToImagePoint(point, viewportSize, imageSize) {
|
|
59
|
+
if (viewportSize.width <= 0 || viewportSize.height <= 0) {
|
|
60
|
+
throw new RangeError("Viewport dimensions must be greater than zero.");
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
x: point.x * (imageSize.width / viewportSize.width),
|
|
64
|
+
y: point.y * (imageSize.height / viewportSize.height)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/core/history/command-stack.ts
|
|
69
|
+
var CommandStack = class {
|
|
70
|
+
#undoStack = [];
|
|
71
|
+
#redoStack = [];
|
|
72
|
+
execute(command, document) {
|
|
73
|
+
const nextDocument = command.execute(document);
|
|
74
|
+
this.#undoStack.push(command);
|
|
75
|
+
this.#redoStack.length = 0;
|
|
76
|
+
return nextDocument;
|
|
77
|
+
}
|
|
78
|
+
undo(document) {
|
|
79
|
+
const command = this.#undoStack.pop();
|
|
80
|
+
if (!command) return document;
|
|
81
|
+
const previousDocument = command.undo(document);
|
|
82
|
+
this.#redoStack.push(command);
|
|
83
|
+
return previousDocument;
|
|
84
|
+
}
|
|
85
|
+
redo(document) {
|
|
86
|
+
const command = this.#redoStack.pop();
|
|
87
|
+
if (!command) return document;
|
|
88
|
+
const nextDocument = command.execute(document);
|
|
89
|
+
this.#undoStack.push(command);
|
|
90
|
+
return nextDocument;
|
|
91
|
+
}
|
|
92
|
+
clear() {
|
|
93
|
+
this.#undoStack.length = 0;
|
|
94
|
+
this.#redoStack.length = 0;
|
|
95
|
+
}
|
|
96
|
+
get canUndo() {
|
|
97
|
+
return this.#undoStack.length > 0;
|
|
98
|
+
}
|
|
99
|
+
get canRedo() {
|
|
100
|
+
return this.#redoStack.length > 0;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// src/core/history/document-commands.ts
|
|
105
|
+
var AddElementCommand = class {
|
|
106
|
+
#element;
|
|
107
|
+
constructor(element) {
|
|
108
|
+
this.#element = element;
|
|
109
|
+
}
|
|
110
|
+
execute(document) {
|
|
111
|
+
if (document.elements.some((element) => element.id === this.#element.id)) {
|
|
112
|
+
return document;
|
|
113
|
+
}
|
|
114
|
+
return { ...document, elements: [...document.elements, this.#element] };
|
|
115
|
+
}
|
|
116
|
+
undo(document) {
|
|
117
|
+
return {
|
|
118
|
+
...document,
|
|
119
|
+
elements: document.elements.filter((element) => element.id !== this.#element.id)
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
var UpdateElementCommand = class {
|
|
124
|
+
#before;
|
|
125
|
+
#after;
|
|
126
|
+
constructor(before, after) {
|
|
127
|
+
if (before.id !== after.id) {
|
|
128
|
+
throw new Error("An annotation update must preserve the element id.");
|
|
129
|
+
}
|
|
130
|
+
this.#before = before;
|
|
131
|
+
this.#after = after;
|
|
132
|
+
}
|
|
133
|
+
execute(document) {
|
|
134
|
+
return replaceElement(document, this.#after);
|
|
135
|
+
}
|
|
136
|
+
undo(document) {
|
|
137
|
+
return replaceElement(document, this.#before);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
var RemoveElementCommand = class {
|
|
141
|
+
#element;
|
|
142
|
+
#index;
|
|
143
|
+
constructor(element, index) {
|
|
144
|
+
this.#element = element;
|
|
145
|
+
this.#index = index;
|
|
146
|
+
}
|
|
147
|
+
execute(document) {
|
|
148
|
+
return {
|
|
149
|
+
...document,
|
|
150
|
+
elements: document.elements.filter((element) => element.id !== this.#element.id)
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
undo(document) {
|
|
154
|
+
if (document.elements.some((element) => element.id === this.#element.id)) {
|
|
155
|
+
return document;
|
|
156
|
+
}
|
|
157
|
+
const elements = [...document.elements];
|
|
158
|
+
elements.splice(Math.min(this.#index, elements.length), 0, this.#element);
|
|
159
|
+
return { ...document, elements };
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
function replaceElement(document, replacement) {
|
|
163
|
+
let found = false;
|
|
164
|
+
const elements = document.elements.map((element) => {
|
|
165
|
+
if (element.id !== replacement.id) {
|
|
166
|
+
return element;
|
|
167
|
+
}
|
|
168
|
+
found = true;
|
|
169
|
+
return replacement;
|
|
170
|
+
});
|
|
171
|
+
return found ? { ...document, elements } : document;
|
|
172
|
+
}
|
|
173
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
174
|
+
0 && (module.exports = {
|
|
175
|
+
AddElementCommand,
|
|
176
|
+
CommandStack,
|
|
177
|
+
RemoveElementCommand,
|
|
178
|
+
UpdateElementCommand,
|
|
179
|
+
clampRect,
|
|
180
|
+
isRectValid,
|
|
181
|
+
normalizeRect,
|
|
182
|
+
viewportPointToImagePoint
|
|
183
|
+
});
|
|
184
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core.ts","../../src/core/geometry/rect.ts","../../src/core/history/command-stack.ts","../../src/core/history/document-commands.ts"],"sourcesContent":["export * from './core/index.js';\n","export interface Point {\n x: number;\n y: number;\n}\n\nexport interface Size {\n width: number;\n height: number;\n}\n\nexport type Rect = Point & Size;\n\nexport function normalizeRect(start: Point, end: Point): Rect {\n return {\n x: Math.min(start.x, end.x),\n y: Math.min(start.y, end.y),\n width: Math.abs(end.x - start.x),\n height: Math.abs(end.y - start.y),\n };\n}\n\nexport function clampRect(rect: Rect, bounds: Rect): Rect {\n const left = Math.max(bounds.x, rect.x);\n const top = Math.max(bounds.y, rect.y);\n const right = Math.min(bounds.x + bounds.width, rect.x + rect.width);\n const bottom = Math.min(bounds.y + bounds.height, rect.y + rect.height);\n\n return {\n x: left,\n y: top,\n width: Math.max(0, right - left),\n height: Math.max(0, bottom - top),\n };\n}\n\nexport function isRectValid(rect: Rect, minimumSize = 1): boolean {\n return rect.width >= minimumSize && rect.height >= minimumSize;\n}\n\nexport function viewportPointToImagePoint(\n point: Point,\n viewportSize: Size,\n imageSize: Size\n): Point {\n if (viewportSize.width <= 0 || viewportSize.height <= 0) {\n throw new RangeError('Viewport dimensions must be greater than zero.');\n }\n\n return {\n x: point.x * (imageSize.width / viewportSize.width),\n y: point.y * (imageSize.height / viewportSize.height),\n };\n}\n","export interface ScreenshotCommand<TDocument> {\n execute(document: TDocument): TDocument;\n undo(document: TDocument): TDocument;\n}\n\nexport class CommandStack<TDocument> {\n readonly #undoStack: ScreenshotCommand<TDocument>[] = [];\n readonly #redoStack: ScreenshotCommand<TDocument>[] = [];\n\n execute(command: ScreenshotCommand<TDocument>, document: TDocument): TDocument {\n const nextDocument = command.execute(document);\n this.#undoStack.push(command);\n this.#redoStack.length = 0;\n return nextDocument;\n }\n\n undo(document: TDocument): TDocument {\n const command = this.#undoStack.pop();\n if (!command) return document;\n\n const previousDocument = command.undo(document);\n this.#redoStack.push(command);\n return previousDocument;\n }\n\n redo(document: TDocument): TDocument {\n const command = this.#redoStack.pop();\n if (!command) return document;\n\n const nextDocument = command.execute(document);\n this.#undoStack.push(command);\n return nextDocument;\n }\n\n clear(): void {\n this.#undoStack.length = 0;\n this.#redoStack.length = 0;\n }\n\n get canUndo(): boolean {\n return this.#undoStack.length > 0;\n }\n\n get canRedo(): boolean {\n return this.#redoStack.length > 0;\n }\n}\n","import type { AnnotationElement, ScreenshotDocument } from '../model/document.js';\nimport type { ScreenshotCommand } from './command-stack.js';\n\nexport class AddElementCommand implements ScreenshotCommand<ScreenshotDocument> {\n readonly #element: AnnotationElement;\n\n constructor(element: AnnotationElement) {\n this.#element = element;\n }\n\n execute(document: ScreenshotDocument): ScreenshotDocument {\n if (document.elements.some((element) => element.id === this.#element.id)) {\n return document;\n }\n\n return { ...document, elements: [...document.elements, this.#element] };\n }\n\n undo(document: ScreenshotDocument): ScreenshotDocument {\n return {\n ...document,\n elements: document.elements.filter((element) => element.id !== this.#element.id),\n };\n }\n}\n\nexport class UpdateElementCommand implements ScreenshotCommand<ScreenshotDocument> {\n readonly #before: AnnotationElement;\n readonly #after: AnnotationElement;\n\n constructor(before: AnnotationElement, after: AnnotationElement) {\n if (before.id !== after.id) {\n throw new Error('An annotation update must preserve the element id.');\n }\n this.#before = before;\n this.#after = after;\n }\n\n execute(document: ScreenshotDocument): ScreenshotDocument {\n return replaceElement(document, this.#after);\n }\n\n undo(document: ScreenshotDocument): ScreenshotDocument {\n return replaceElement(document, this.#before);\n }\n}\n\nexport class RemoveElementCommand implements ScreenshotCommand<ScreenshotDocument> {\n readonly #element: AnnotationElement;\n readonly #index: number;\n\n constructor(element: AnnotationElement, index: number) {\n this.#element = element;\n this.#index = index;\n }\n\n execute(document: ScreenshotDocument): ScreenshotDocument {\n return {\n ...document,\n elements: document.elements.filter((element) => element.id !== this.#element.id),\n };\n }\n\n undo(document: ScreenshotDocument): ScreenshotDocument {\n if (document.elements.some((element) => element.id === this.#element.id)) {\n return document;\n }\n\n const elements = [...document.elements];\n elements.splice(Math.min(this.#index, elements.length), 0, this.#element);\n return { ...document, elements };\n }\n}\n\nfunction replaceElement(\n document: ScreenshotDocument,\n replacement: AnnotationElement\n): ScreenshotDocument {\n let found = false;\n const elements = document.elements.map((element) => {\n if (element.id !== replacement.id) {\n return element;\n }\n found = true;\n return replacement;\n });\n\n return found ? { ...document, elements } : document;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
interface Point {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
}
|
|
5
|
+
interface Size {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}
|
|
9
|
+
type Rect = Point & Size;
|
|
10
|
+
declare function normalizeRect(start: Point, end: Point): Rect;
|
|
11
|
+
declare function clampRect(rect: Rect, bounds: Rect): Rect;
|
|
12
|
+
declare function isRectValid(rect: Rect, minimumSize?: number): boolean;
|
|
13
|
+
declare function viewportPointToImagePoint(point: Point, viewportSize: Size, imageSize: Size): Point;
|
|
14
|
+
|
|
15
|
+
interface AnnotationElementBase {
|
|
16
|
+
id: string;
|
|
17
|
+
zIndex: number;
|
|
18
|
+
createdAt: number;
|
|
19
|
+
color: string;
|
|
20
|
+
}
|
|
21
|
+
interface RectangleElement extends AnnotationElementBase {
|
|
22
|
+
type: 'rectangle';
|
|
23
|
+
bounds: Rect;
|
|
24
|
+
lineWidth: number;
|
|
25
|
+
}
|
|
26
|
+
interface EllipseElement extends AnnotationElementBase {
|
|
27
|
+
type: 'ellipse';
|
|
28
|
+
bounds: Rect;
|
|
29
|
+
lineWidth: number;
|
|
30
|
+
}
|
|
31
|
+
interface ArrowElement extends AnnotationElementBase {
|
|
32
|
+
type: 'arrow';
|
|
33
|
+
start: Point;
|
|
34
|
+
end: Point;
|
|
35
|
+
lineWidth: number;
|
|
36
|
+
}
|
|
37
|
+
interface BrushElement extends AnnotationElementBase {
|
|
38
|
+
type: 'brush';
|
|
39
|
+
points: Point[];
|
|
40
|
+
lineWidth: number;
|
|
41
|
+
}
|
|
42
|
+
interface TextLayoutMetrics {
|
|
43
|
+
width: number;
|
|
44
|
+
ascent: number;
|
|
45
|
+
descent: number;
|
|
46
|
+
}
|
|
47
|
+
interface TextElement extends AnnotationElementBase {
|
|
48
|
+
type: 'text';
|
|
49
|
+
position: Point;
|
|
50
|
+
value: string;
|
|
51
|
+
fontSize: number;
|
|
52
|
+
metrics: TextLayoutMetrics;
|
|
53
|
+
}
|
|
54
|
+
interface MosaicElement extends AnnotationElementBase {
|
|
55
|
+
type: 'mosaic';
|
|
56
|
+
bounds: Rect;
|
|
57
|
+
}
|
|
58
|
+
type AnnotationElement = RectangleElement | EllipseElement | ArrowElement | BrushElement | TextElement | MosaicElement;
|
|
59
|
+
interface ScreenshotDocument {
|
|
60
|
+
selection: Rect;
|
|
61
|
+
elements: AnnotationElement[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface ScreenshotCommand<TDocument> {
|
|
65
|
+
execute(document: TDocument): TDocument;
|
|
66
|
+
undo(document: TDocument): TDocument;
|
|
67
|
+
}
|
|
68
|
+
declare class CommandStack<TDocument> {
|
|
69
|
+
#private;
|
|
70
|
+
execute(command: ScreenshotCommand<TDocument>, document: TDocument): TDocument;
|
|
71
|
+
undo(document: TDocument): TDocument;
|
|
72
|
+
redo(document: TDocument): TDocument;
|
|
73
|
+
clear(): void;
|
|
74
|
+
get canUndo(): boolean;
|
|
75
|
+
get canRedo(): boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
declare class AddElementCommand implements ScreenshotCommand<ScreenshotDocument> {
|
|
79
|
+
#private;
|
|
80
|
+
constructor(element: AnnotationElement);
|
|
81
|
+
execute(document: ScreenshotDocument): ScreenshotDocument;
|
|
82
|
+
undo(document: ScreenshotDocument): ScreenshotDocument;
|
|
83
|
+
}
|
|
84
|
+
declare class UpdateElementCommand implements ScreenshotCommand<ScreenshotDocument> {
|
|
85
|
+
#private;
|
|
86
|
+
constructor(before: AnnotationElement, after: AnnotationElement);
|
|
87
|
+
execute(document: ScreenshotDocument): ScreenshotDocument;
|
|
88
|
+
undo(document: ScreenshotDocument): ScreenshotDocument;
|
|
89
|
+
}
|
|
90
|
+
declare class RemoveElementCommand implements ScreenshotCommand<ScreenshotDocument> {
|
|
91
|
+
#private;
|
|
92
|
+
constructor(element: AnnotationElement, index: number);
|
|
93
|
+
execute(document: ScreenshotDocument): ScreenshotDocument;
|
|
94
|
+
undo(document: ScreenshotDocument): ScreenshotDocument;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export { AddElementCommand, type AnnotationElement, type ArrowElement, type BrushElement, CommandStack, type EllipseElement, type MosaicElement, type Point, type Rect, type RectangleElement, RemoveElementCommand, type ScreenshotCommand, type ScreenshotDocument, type Size, type TextElement, type TextLayoutMetrics, UpdateElementCommand, clampRect, isRectValid, normalizeRect, viewportPointToImagePoint };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
interface Point {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
}
|
|
5
|
+
interface Size {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}
|
|
9
|
+
type Rect = Point & Size;
|
|
10
|
+
declare function normalizeRect(start: Point, end: Point): Rect;
|
|
11
|
+
declare function clampRect(rect: Rect, bounds: Rect): Rect;
|
|
12
|
+
declare function isRectValid(rect: Rect, minimumSize?: number): boolean;
|
|
13
|
+
declare function viewportPointToImagePoint(point: Point, viewportSize: Size, imageSize: Size): Point;
|
|
14
|
+
|
|
15
|
+
interface AnnotationElementBase {
|
|
16
|
+
id: string;
|
|
17
|
+
zIndex: number;
|
|
18
|
+
createdAt: number;
|
|
19
|
+
color: string;
|
|
20
|
+
}
|
|
21
|
+
interface RectangleElement extends AnnotationElementBase {
|
|
22
|
+
type: 'rectangle';
|
|
23
|
+
bounds: Rect;
|
|
24
|
+
lineWidth: number;
|
|
25
|
+
}
|
|
26
|
+
interface EllipseElement extends AnnotationElementBase {
|
|
27
|
+
type: 'ellipse';
|
|
28
|
+
bounds: Rect;
|
|
29
|
+
lineWidth: number;
|
|
30
|
+
}
|
|
31
|
+
interface ArrowElement extends AnnotationElementBase {
|
|
32
|
+
type: 'arrow';
|
|
33
|
+
start: Point;
|
|
34
|
+
end: Point;
|
|
35
|
+
lineWidth: number;
|
|
36
|
+
}
|
|
37
|
+
interface BrushElement extends AnnotationElementBase {
|
|
38
|
+
type: 'brush';
|
|
39
|
+
points: Point[];
|
|
40
|
+
lineWidth: number;
|
|
41
|
+
}
|
|
42
|
+
interface TextLayoutMetrics {
|
|
43
|
+
width: number;
|
|
44
|
+
ascent: number;
|
|
45
|
+
descent: number;
|
|
46
|
+
}
|
|
47
|
+
interface TextElement extends AnnotationElementBase {
|
|
48
|
+
type: 'text';
|
|
49
|
+
position: Point;
|
|
50
|
+
value: string;
|
|
51
|
+
fontSize: number;
|
|
52
|
+
metrics: TextLayoutMetrics;
|
|
53
|
+
}
|
|
54
|
+
interface MosaicElement extends AnnotationElementBase {
|
|
55
|
+
type: 'mosaic';
|
|
56
|
+
bounds: Rect;
|
|
57
|
+
}
|
|
58
|
+
type AnnotationElement = RectangleElement | EllipseElement | ArrowElement | BrushElement | TextElement | MosaicElement;
|
|
59
|
+
interface ScreenshotDocument {
|
|
60
|
+
selection: Rect;
|
|
61
|
+
elements: AnnotationElement[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface ScreenshotCommand<TDocument> {
|
|
65
|
+
execute(document: TDocument): TDocument;
|
|
66
|
+
undo(document: TDocument): TDocument;
|
|
67
|
+
}
|
|
68
|
+
declare class CommandStack<TDocument> {
|
|
69
|
+
#private;
|
|
70
|
+
execute(command: ScreenshotCommand<TDocument>, document: TDocument): TDocument;
|
|
71
|
+
undo(document: TDocument): TDocument;
|
|
72
|
+
redo(document: TDocument): TDocument;
|
|
73
|
+
clear(): void;
|
|
74
|
+
get canUndo(): boolean;
|
|
75
|
+
get canRedo(): boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
declare class AddElementCommand implements ScreenshotCommand<ScreenshotDocument> {
|
|
79
|
+
#private;
|
|
80
|
+
constructor(element: AnnotationElement);
|
|
81
|
+
execute(document: ScreenshotDocument): ScreenshotDocument;
|
|
82
|
+
undo(document: ScreenshotDocument): ScreenshotDocument;
|
|
83
|
+
}
|
|
84
|
+
declare class UpdateElementCommand implements ScreenshotCommand<ScreenshotDocument> {
|
|
85
|
+
#private;
|
|
86
|
+
constructor(before: AnnotationElement, after: AnnotationElement);
|
|
87
|
+
execute(document: ScreenshotDocument): ScreenshotDocument;
|
|
88
|
+
undo(document: ScreenshotDocument): ScreenshotDocument;
|
|
89
|
+
}
|
|
90
|
+
declare class RemoveElementCommand implements ScreenshotCommand<ScreenshotDocument> {
|
|
91
|
+
#private;
|
|
92
|
+
constructor(element: AnnotationElement, index: number);
|
|
93
|
+
execute(document: ScreenshotDocument): ScreenshotDocument;
|
|
94
|
+
undo(document: ScreenshotDocument): ScreenshotDocument;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export { AddElementCommand, type AnnotationElement, type ArrowElement, type BrushElement, CommandStack, type EllipseElement, type MosaicElement, type Point, type Rect, type RectangleElement, RemoveElementCommand, type ScreenshotCommand, type ScreenshotDocument, type Size, type TextElement, type TextLayoutMetrics, UpdateElementCommand, clampRect, isRectValid, normalizeRect, viewportPointToImagePoint };
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// src/core/geometry/rect.ts
|
|
2
|
+
function normalizeRect(start, end) {
|
|
3
|
+
return {
|
|
4
|
+
x: Math.min(start.x, end.x),
|
|
5
|
+
y: Math.min(start.y, end.y),
|
|
6
|
+
width: Math.abs(end.x - start.x),
|
|
7
|
+
height: Math.abs(end.y - start.y)
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function clampRect(rect, bounds) {
|
|
11
|
+
const left = Math.max(bounds.x, rect.x);
|
|
12
|
+
const top = Math.max(bounds.y, rect.y);
|
|
13
|
+
const right = Math.min(bounds.x + bounds.width, rect.x + rect.width);
|
|
14
|
+
const bottom = Math.min(bounds.y + bounds.height, rect.y + rect.height);
|
|
15
|
+
return {
|
|
16
|
+
x: left,
|
|
17
|
+
y: top,
|
|
18
|
+
width: Math.max(0, right - left),
|
|
19
|
+
height: Math.max(0, bottom - top)
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function isRectValid(rect, minimumSize = 1) {
|
|
23
|
+
return rect.width >= minimumSize && rect.height >= minimumSize;
|
|
24
|
+
}
|
|
25
|
+
function viewportPointToImagePoint(point, viewportSize, imageSize) {
|
|
26
|
+
if (viewportSize.width <= 0 || viewportSize.height <= 0) {
|
|
27
|
+
throw new RangeError("Viewport dimensions must be greater than zero.");
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
x: point.x * (imageSize.width / viewportSize.width),
|
|
31
|
+
y: point.y * (imageSize.height / viewportSize.height)
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/core/history/command-stack.ts
|
|
36
|
+
var CommandStack = class {
|
|
37
|
+
#undoStack = [];
|
|
38
|
+
#redoStack = [];
|
|
39
|
+
execute(command, document) {
|
|
40
|
+
const nextDocument = command.execute(document);
|
|
41
|
+
this.#undoStack.push(command);
|
|
42
|
+
this.#redoStack.length = 0;
|
|
43
|
+
return nextDocument;
|
|
44
|
+
}
|
|
45
|
+
undo(document) {
|
|
46
|
+
const command = this.#undoStack.pop();
|
|
47
|
+
if (!command) return document;
|
|
48
|
+
const previousDocument = command.undo(document);
|
|
49
|
+
this.#redoStack.push(command);
|
|
50
|
+
return previousDocument;
|
|
51
|
+
}
|
|
52
|
+
redo(document) {
|
|
53
|
+
const command = this.#redoStack.pop();
|
|
54
|
+
if (!command) return document;
|
|
55
|
+
const nextDocument = command.execute(document);
|
|
56
|
+
this.#undoStack.push(command);
|
|
57
|
+
return nextDocument;
|
|
58
|
+
}
|
|
59
|
+
clear() {
|
|
60
|
+
this.#undoStack.length = 0;
|
|
61
|
+
this.#redoStack.length = 0;
|
|
62
|
+
}
|
|
63
|
+
get canUndo() {
|
|
64
|
+
return this.#undoStack.length > 0;
|
|
65
|
+
}
|
|
66
|
+
get canRedo() {
|
|
67
|
+
return this.#redoStack.length > 0;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// src/core/history/document-commands.ts
|
|
72
|
+
var AddElementCommand = class {
|
|
73
|
+
#element;
|
|
74
|
+
constructor(element) {
|
|
75
|
+
this.#element = element;
|
|
76
|
+
}
|
|
77
|
+
execute(document) {
|
|
78
|
+
if (document.elements.some((element) => element.id === this.#element.id)) {
|
|
79
|
+
return document;
|
|
80
|
+
}
|
|
81
|
+
return { ...document, elements: [...document.elements, this.#element] };
|
|
82
|
+
}
|
|
83
|
+
undo(document) {
|
|
84
|
+
return {
|
|
85
|
+
...document,
|
|
86
|
+
elements: document.elements.filter((element) => element.id !== this.#element.id)
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
var UpdateElementCommand = class {
|
|
91
|
+
#before;
|
|
92
|
+
#after;
|
|
93
|
+
constructor(before, after) {
|
|
94
|
+
if (before.id !== after.id) {
|
|
95
|
+
throw new Error("An annotation update must preserve the element id.");
|
|
96
|
+
}
|
|
97
|
+
this.#before = before;
|
|
98
|
+
this.#after = after;
|
|
99
|
+
}
|
|
100
|
+
execute(document) {
|
|
101
|
+
return replaceElement(document, this.#after);
|
|
102
|
+
}
|
|
103
|
+
undo(document) {
|
|
104
|
+
return replaceElement(document, this.#before);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
var RemoveElementCommand = class {
|
|
108
|
+
#element;
|
|
109
|
+
#index;
|
|
110
|
+
constructor(element, index) {
|
|
111
|
+
this.#element = element;
|
|
112
|
+
this.#index = index;
|
|
113
|
+
}
|
|
114
|
+
execute(document) {
|
|
115
|
+
return {
|
|
116
|
+
...document,
|
|
117
|
+
elements: document.elements.filter((element) => element.id !== this.#element.id)
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
undo(document) {
|
|
121
|
+
if (document.elements.some((element) => element.id === this.#element.id)) {
|
|
122
|
+
return document;
|
|
123
|
+
}
|
|
124
|
+
const elements = [...document.elements];
|
|
125
|
+
elements.splice(Math.min(this.#index, elements.length), 0, this.#element);
|
|
126
|
+
return { ...document, elements };
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
function replaceElement(document, replacement) {
|
|
130
|
+
let found = false;
|
|
131
|
+
const elements = document.elements.map((element) => {
|
|
132
|
+
if (element.id !== replacement.id) {
|
|
133
|
+
return element;
|
|
134
|
+
}
|
|
135
|
+
found = true;
|
|
136
|
+
return replacement;
|
|
137
|
+
});
|
|
138
|
+
return found ? { ...document, elements } : document;
|
|
139
|
+
}
|
|
140
|
+
export {
|
|
141
|
+
AddElementCommand,
|
|
142
|
+
CommandStack,
|
|
143
|
+
RemoveElementCommand,
|
|
144
|
+
UpdateElementCommand,
|
|
145
|
+
clampRect,
|
|
146
|
+
isRectValid,
|
|
147
|
+
normalizeRect,
|
|
148
|
+
viewportPointToImagePoint
|
|
149
|
+
};
|
|
150
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core/geometry/rect.ts","../../src/core/history/command-stack.ts","../../src/core/history/document-commands.ts"],"sourcesContent":["export interface Point {\n x: number;\n y: number;\n}\n\nexport interface Size {\n width: number;\n height: number;\n}\n\nexport type Rect = Point & Size;\n\nexport function normalizeRect(start: Point, end: Point): Rect {\n return {\n x: Math.min(start.x, end.x),\n y: Math.min(start.y, end.y),\n width: Math.abs(end.x - start.x),\n height: Math.abs(end.y - start.y),\n };\n}\n\nexport function clampRect(rect: Rect, bounds: Rect): Rect {\n const left = Math.max(bounds.x, rect.x);\n const top = Math.max(bounds.y, rect.y);\n const right = Math.min(bounds.x + bounds.width, rect.x + rect.width);\n const bottom = Math.min(bounds.y + bounds.height, rect.y + rect.height);\n\n return {\n x: left,\n y: top,\n width: Math.max(0, right - left),\n height: Math.max(0, bottom - top),\n };\n}\n\nexport function isRectValid(rect: Rect, minimumSize = 1): boolean {\n return rect.width >= minimumSize && rect.height >= minimumSize;\n}\n\nexport function viewportPointToImagePoint(\n point: Point,\n viewportSize: Size,\n imageSize: Size\n): Point {\n if (viewportSize.width <= 0 || viewportSize.height <= 0) {\n throw new RangeError('Viewport dimensions must be greater than zero.');\n }\n\n return {\n x: point.x * (imageSize.width / viewportSize.width),\n y: point.y * (imageSize.height / viewportSize.height),\n };\n}\n","export interface ScreenshotCommand<TDocument> {\n execute(document: TDocument): TDocument;\n undo(document: TDocument): TDocument;\n}\n\nexport class CommandStack<TDocument> {\n readonly #undoStack: ScreenshotCommand<TDocument>[] = [];\n readonly #redoStack: ScreenshotCommand<TDocument>[] = [];\n\n execute(command: ScreenshotCommand<TDocument>, document: TDocument): TDocument {\n const nextDocument = command.execute(document);\n this.#undoStack.push(command);\n this.#redoStack.length = 0;\n return nextDocument;\n }\n\n undo(document: TDocument): TDocument {\n const command = this.#undoStack.pop();\n if (!command) return document;\n\n const previousDocument = command.undo(document);\n this.#redoStack.push(command);\n return previousDocument;\n }\n\n redo(document: TDocument): TDocument {\n const command = this.#redoStack.pop();\n if (!command) return document;\n\n const nextDocument = command.execute(document);\n this.#undoStack.push(command);\n return nextDocument;\n }\n\n clear(): void {\n this.#undoStack.length = 0;\n this.#redoStack.length = 0;\n }\n\n get canUndo(): boolean {\n return this.#undoStack.length > 0;\n }\n\n get canRedo(): boolean {\n return this.#redoStack.length > 0;\n }\n}\n","import type { AnnotationElement, ScreenshotDocument } from '../model/document.js';\nimport type { ScreenshotCommand } from './command-stack.js';\n\nexport class AddElementCommand implements ScreenshotCommand<ScreenshotDocument> {\n readonly #element: AnnotationElement;\n\n constructor(element: AnnotationElement) {\n this.#element = element;\n }\n\n execute(document: ScreenshotDocument): ScreenshotDocument {\n if (document.elements.some((element) => element.id === this.#element.id)) {\n return document;\n }\n\n return { ...document, elements: [...document.elements, this.#element] };\n }\n\n undo(document: ScreenshotDocument): ScreenshotDocument {\n return {\n ...document,\n elements: document.elements.filter((element) => element.id !== this.#element.id),\n };\n }\n}\n\nexport class UpdateElementCommand implements ScreenshotCommand<ScreenshotDocument> {\n readonly #before: AnnotationElement;\n readonly #after: AnnotationElement;\n\n constructor(before: AnnotationElement, after: AnnotationElement) {\n if (before.id !== after.id) {\n throw new Error('An annotation update must preserve the element id.');\n }\n this.#before = before;\n this.#after = after;\n }\n\n execute(document: ScreenshotDocument): ScreenshotDocument {\n return replaceElement(document, this.#after);\n }\n\n undo(document: ScreenshotDocument): ScreenshotDocument {\n return replaceElement(document, this.#before);\n }\n}\n\nexport class RemoveElementCommand implements ScreenshotCommand<ScreenshotDocument> {\n readonly #element: AnnotationElement;\n readonly #index: number;\n\n constructor(element: AnnotationElement, index: number) {\n this.#element = element;\n this.#index = index;\n }\n\n execute(document: ScreenshotDocument): ScreenshotDocument {\n return {\n ...document,\n elements: document.elements.filter((element) => element.id !== this.#element.id),\n };\n }\n\n undo(document: ScreenshotDocument): ScreenshotDocument {\n if (document.elements.some((element) => element.id === this.#element.id)) {\n return document;\n }\n\n const elements = [...document.elements];\n elements.splice(Math.min(this.#index, elements.length), 0, this.#element);\n return { ...document, elements };\n }\n}\n\nfunction replaceElement(\n document: ScreenshotDocument,\n replacement: AnnotationElement\n): ScreenshotDocument {\n let found = false;\n const elements = document.elements.map((element) => {\n if (element.id !== replacement.id) {\n return element;\n }\n found = true;\n return replacement;\n });\n\n return found ? { ...document, elements } : document;\n}\n"],"mappings":"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","names":[]}
|