dsh-figmark 1.0.0 → 1.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.
- package/PUBLISHING.md +17 -2
- package/lib/client.js +240 -90
- package/lib/client.pdf.js +3 -2
- package/lib/index.js +20 -0
- package/market/index.json +4 -5
- package/package.json +1 -2
package/PUBLISHING.md
CHANGED
|
@@ -17,9 +17,10 @@ node -e "console.log(Math.floor(Date.now()/1000))"
|
|
|
17
17
|
# 2) 先干跑一次,确认包内容
|
|
18
18
|
npm pack --dry-run
|
|
19
19
|
|
|
20
|
-
# 3) 发布到 npm
|
|
20
|
+
# 3) 发布到 npm(两个坑见文末)
|
|
21
21
|
npm login # 只需一次
|
|
22
|
-
|
|
22
|
+
env -u http_proxy -u https_proxy -u all_proxy -u NODE_USE_ENV_PROXY \
|
|
23
|
+
NO_PROXY='*' no_proxy='*' npm publish --access public
|
|
23
24
|
|
|
24
25
|
# npm 还没发布时:索引里 versions[0].source 用 git 类型(见下),
|
|
25
26
|
# 钉到当前 commit 即可让别人装上源码版本。npm 发布之后把 source 换回
|
|
@@ -77,3 +78,17 @@ const {parseMarketIndex}=require('/Applications/DeepSeek Harness.app/Contents/Re
|
|
|
77
78
|
1. **侧栏 → 插件 → 实验性插件 → 更多 → 自定义插件市场**(启用)
|
|
78
79
|
2. 发现页 → 添加索引 → 粘贴 raw 地址
|
|
79
80
|
3. 搜 `figmark` → 安装 → **Cmd+Q 完全退出重开**
|
|
81
|
+
|
|
82
|
+
## 两个真实的坑
|
|
83
|
+
|
|
84
|
+
**代理会吞掉 tarball 上传。** 本机 `http_proxy` 指向本地代理时,`npm publish` 会连续报
|
|
85
|
+
`PUT ... failed with 503` 并静默重试,看起来像 npm 挂了,其实是几 MB 的包体没传上去。
|
|
86
|
+
发布这一步必须 `env -u http_proxy -u https_proxy -u all_proxy -u NODE_USE_ENV_PROXY`
|
|
87
|
+
清掉代理变量,`~/.npmrc` 里的 `noproxy=` 对 npm 自己的 dispatcher 不生效。
|
|
88
|
+
|
|
89
|
+
**Granular Token 默认不让发布。** 账号开了 2FA 时,未勾选 "Bypass 2FA" 的 Granular Token
|
|
90
|
+
会得到 `403 Two-factor authentication or granular access token with bypass 2fa enabled is required`。
|
|
91
|
+
两个选择:在 token 页勾上 Bypass 2FA,或生成 Classic Token 时选 **Automation** 类型(自带绕过)。
|
|
92
|
+
|
|
93
|
+
**发布中途失败会留下 `0.0.0-stage` 占位版本。** 直接重跑 `npm publish` 即可,占位会被替换;
|
|
94
|
+
不用手动清。
|
package/lib/client.js
CHANGED
|
@@ -25,6 +25,8 @@ window.__ModuleLoader__.load({
|
|
|
25
25
|
redo: "重做",
|
|
26
26
|
clear: "清空",
|
|
27
27
|
detail: "这个元素的详细说明",
|
|
28
|
+
addComment: "添加评论…",
|
|
29
|
+
weight: "线条粗细",
|
|
28
30
|
detailPlaceholder: "这一处具体要改什么(例如「这个标号应指向滑套 34」)",
|
|
29
31
|
summary: "总体目标",
|
|
30
32
|
summaryPlaceholder: "可选:这张图整体要往哪个方向改(智能体会连同下面逐条说明一起读)",
|
|
@@ -110,6 +112,8 @@ window.__ModuleLoader__.load({
|
|
|
110
112
|
redo: "Redo",
|
|
111
113
|
clear: "Clear",
|
|
112
114
|
detail: "What this element needs",
|
|
115
|
+
addComment: "Add a comment…",
|
|
116
|
+
weight: "Line weight",
|
|
113
117
|
detailPlaceholder: "What should change here (for example \"this numeral should point at the sleeve 34\")",
|
|
114
118
|
summary: "Overall goal",
|
|
115
119
|
summaryPlaceholder: "Optional: what this figure should become overall (the agent reads it with the marks below)",
|
|
@@ -253,6 +257,26 @@ window.__ModuleLoader__.load({
|
|
|
253
257
|
}
|
|
254
258
|
//#endregion
|
|
255
259
|
//#region src/shared/annotation.ts
|
|
260
|
+
/** Thinnest stroke a mark can be drawn at, as a multiple of the default width. */
|
|
261
|
+
const WEIGHT_MIN = .5;
|
|
262
|
+
/**
|
|
263
|
+
* Hold a stroke weight inside the range the painter can draw.
|
|
264
|
+
*
|
|
265
|
+
* @param value - the weight to hold.
|
|
266
|
+
* @returns the value, pulled to the nearest end of the range.
|
|
267
|
+
*/
|
|
268
|
+
function clampWeight(value) {
|
|
269
|
+
return Math.min(4, Math.max(WEIGHT_MIN, value));
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Whether a value is a stroke weight this plugin would write.
|
|
273
|
+
*
|
|
274
|
+
* @param value - the value to test.
|
|
275
|
+
* @returns true when it is one.
|
|
276
|
+
*/
|
|
277
|
+
function isWeight(value) {
|
|
278
|
+
return typeof value === "number" && Number.isFinite(value) && value >= .5 && value <= 4;
|
|
279
|
+
}
|
|
256
280
|
/**
|
|
257
281
|
* Suffix naming one page's files inside a paged document's name.
|
|
258
282
|
* @param page - 1-based page, or undefined for a single-surface figure.
|
|
@@ -991,6 +1015,20 @@ window.__ModuleLoader__.load({
|
|
|
991
1015
|
//#region src/client/render.ts
|
|
992
1016
|
/** Stroke width of every mark, in figure pixels. */
|
|
993
1017
|
const STROKE_WIDTH = 2.5;
|
|
1018
|
+
/**
|
|
1019
|
+
* The stroke width one mark is drawn at.
|
|
1020
|
+
*
|
|
1021
|
+
* A mark's weight is a multiple of {@link STROKE_WIDTH}, so the default is the
|
|
1022
|
+
* width marks have always had and a heavier mark is the same drawing with a
|
|
1023
|
+
* thicker pen. The arrow's barbs are weighed by the same multiple, because an
|
|
1024
|
+
* arrow drawn with a thick pen has a thick head.
|
|
1025
|
+
*
|
|
1026
|
+
* @param mark - the mark to weigh.
|
|
1027
|
+
* @returns its stroke width in figure pixels.
|
|
1028
|
+
*/
|
|
1029
|
+
function strokeWidthOf(mark) {
|
|
1030
|
+
return STROKE_WIDTH * (mark.weight ?? 1);
|
|
1031
|
+
}
|
|
994
1032
|
/** Arrow head length, in figure pixels. */
|
|
995
1033
|
const ARROW_HEAD = 14;
|
|
996
1034
|
/** Half-angle of the arrow head, in radians. */
|
|
@@ -1073,12 +1111,13 @@ window.__ModuleLoader__.load({
|
|
|
1073
1111
|
* @param points - the arrow's tail and tip.
|
|
1074
1112
|
* @returns tail, tip and both barbs; the stored points alone when it is half drawn.
|
|
1075
1113
|
*/
|
|
1076
|
-
function arrowBarbs(points) {
|
|
1114
|
+
function arrowBarbs(points, weight = 1) {
|
|
1077
1115
|
const [first, second] = points;
|
|
1078
1116
|
if (first === void 0 || second === void 0) return points;
|
|
1079
1117
|
const angle = Math.atan2(second[1] - first[1], second[0] - first[0]);
|
|
1080
|
-
const
|
|
1081
|
-
const
|
|
1118
|
+
const head = ARROW_HEAD * weight;
|
|
1119
|
+
const left = [second[0] - head * Math.cos(angle - ARROW_SPREAD), second[1] - head * Math.sin(angle - ARROW_SPREAD)];
|
|
1120
|
+
const right = [second[0] - head * Math.cos(angle + ARROW_SPREAD), second[1] - head * Math.sin(angle + ARROW_SPREAD)];
|
|
1082
1121
|
return [
|
|
1083
1122
|
...points,
|
|
1084
1123
|
left,
|
|
@@ -1096,7 +1135,7 @@ window.__ModuleLoader__.load({
|
|
|
1096
1135
|
switch (mark.kind) {
|
|
1097
1136
|
case "arrow": {
|
|
1098
1137
|
if (second === void 0) return void 0;
|
|
1099
|
-
const [left, right] = arrowBarbs(mark.points).slice(2);
|
|
1138
|
+
const [left, right] = arrowBarbs(mark.points, mark.weight ?? 1).slice(2);
|
|
1100
1139
|
return `M ${n(first[0])} ${n(first[1])} L ${n(second[0])} ${n(second[1])} M ${n(left[0])} ${n(left[1])} L ${n(second[0])} ${n(second[1])} L ${n(right[0])} ${n(right[1])}`;
|
|
1101
1140
|
}
|
|
1102
1141
|
case "rect": {
|
|
@@ -1206,12 +1245,12 @@ window.__ModuleLoader__.load({
|
|
|
1206
1245
|
*/
|
|
1207
1246
|
function markOutline(mark) {
|
|
1208
1247
|
if (mark.kind === "point") return pointMarkBox(mark);
|
|
1209
|
-
const geometry = mark.kind === "arrow" ? arrowBarbs(mark.points) : mark.points;
|
|
1248
|
+
const geometry = mark.kind === "arrow" ? arrowBarbs(mark.points, mark.weight ?? 1) : mark.points;
|
|
1210
1249
|
const straight = pointsBox(geometry);
|
|
1211
1250
|
if (straight === void 0) return void 0;
|
|
1212
1251
|
const radians = markRadians(mark);
|
|
1213
1252
|
const box = radians === 0 ? straight : turnedBox(geometry, straight, radians);
|
|
1214
|
-
const grow =
|
|
1253
|
+
const grow = strokeWidthOf(mark) / 2;
|
|
1215
1254
|
return {
|
|
1216
1255
|
x: box.x - grow,
|
|
1217
1256
|
y: box.y - grow,
|
|
@@ -1318,6 +1357,7 @@ window.__ModuleLoader__.load({
|
|
|
1318
1357
|
*/
|
|
1319
1358
|
function Canvas(props) {
|
|
1320
1359
|
const { width, height, marks, tool, color, containerRef, anchoring } = props;
|
|
1360
|
+
const weight = props.weight ?? 1;
|
|
1321
1361
|
/**
|
|
1322
1362
|
* Each mark's number, by id.
|
|
1323
1363
|
*
|
|
@@ -1567,7 +1607,8 @@ window.__ModuleLoader__.load({
|
|
|
1567
1607
|
id: nextMarkId(),
|
|
1568
1608
|
kind: tool,
|
|
1569
1609
|
color,
|
|
1570
|
-
points: seed
|
|
1610
|
+
points: seed,
|
|
1611
|
+
weight
|
|
1571
1612
|
});
|
|
1572
1613
|
};
|
|
1573
1614
|
const move = (event) => {
|
|
@@ -1814,7 +1855,7 @@ window.__ModuleLoader__.load({
|
|
|
1814
1855
|
d: path,
|
|
1815
1856
|
fill: "none",
|
|
1816
1857
|
stroke: "#ffffff",
|
|
1817
|
-
strokeWidth: 5,
|
|
1858
|
+
strokeWidth: strokeWidthOf(shown) + 2.5,
|
|
1818
1859
|
strokeLinecap: "round",
|
|
1819
1860
|
strokeLinejoin: "round"
|
|
1820
1861
|
}),
|
|
@@ -1822,7 +1863,7 @@ window.__ModuleLoader__.load({
|
|
|
1822
1863
|
d: path,
|
|
1823
1864
|
fill: "none",
|
|
1824
1865
|
stroke: shown.color,
|
|
1825
|
-
strokeWidth:
|
|
1866
|
+
strokeWidth: strokeWidthOf(shown),
|
|
1826
1867
|
strokeLinecap: "round",
|
|
1827
1868
|
strokeLinejoin: "round"
|
|
1828
1869
|
}),
|
|
@@ -2449,7 +2490,7 @@ window.__ModuleLoader__.load({
|
|
|
2449
2490
|
function markSvg(mark, number) {
|
|
2450
2491
|
const path = markPathData(mark);
|
|
2451
2492
|
const parts = [];
|
|
2452
|
-
if (path !== void 0) parts.push(`<path d="${path}" fill="none" stroke="#ffffff" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>`, `<path d="${path}" fill="none" stroke="${mark.color}" stroke-width="${
|
|
2493
|
+
if (path !== void 0) parts.push(`<path d="${path}" fill="none" stroke="#ffffff" stroke-width="${strokeWidthOf(mark) + 2.5}" stroke-linecap="round" stroke-linejoin="round"/>`, `<path d="${path}" fill="none" stroke="${mark.color}" stroke-width="${strokeWidthOf(mark)}" stroke-linecap="round" stroke-linejoin="round"/>`);
|
|
2453
2494
|
const pin = mark.kind === "point" ? pinPathData(mark) : void 0;
|
|
2454
2495
|
const centre = mark.kind === "point" ? pinNumberCentre(mark) : void 0;
|
|
2455
2496
|
if (pin !== void 0 && centre !== void 0) {
|
|
@@ -2787,6 +2828,22 @@ window.__ModuleLoader__.load({
|
|
|
2787
2828
|
}
|
|
2788
2829
|
//#endregion
|
|
2789
2830
|
//#region src/client/preferences.ts
|
|
2831
|
+
/**
|
|
2832
|
+
* The drawing choices a reader expects to still be there after a reload.
|
|
2833
|
+
*
|
|
2834
|
+
* Marks and their sentences belong to the annotation and live in its sidecar. The
|
|
2835
|
+
* colour the reader is drawing in, and how large the numbers are, are a different
|
|
2836
|
+
* kind of thing: they describe how the *next* mark should look, not what any mark
|
|
2837
|
+
* already means. Keeping them in a sidecar would tie them to a figure that has one —
|
|
2838
|
+
* a reader who picks a colour, draws nothing, and reloads would find the colour gone,
|
|
2839
|
+
* because a figure with no annotation has no sidecar to remember it in.
|
|
2840
|
+
*
|
|
2841
|
+
* So they go in browser storage, which is scoped to the reader rather than to the
|
|
2842
|
+
* figure, and which exists before the first mark does. An annotation that records
|
|
2843
|
+
* its own number style still wins over this: the picture the agent reads has to match
|
|
2844
|
+
* the annotation, not the reader's current preference.
|
|
2845
|
+
* @module dsh-figmark/client/preferences
|
|
2846
|
+
*/
|
|
2790
2847
|
/** The storage key. Namespaced so it cannot collide with the shell's own keys. */
|
|
2791
2848
|
const PREFERENCES_KEY = "dsh-figmark:drawing";
|
|
2792
2849
|
/** A colour this plugin would have written: six hex digits. */
|
|
@@ -2842,6 +2899,7 @@ window.__ModuleLoader__.load({
|
|
|
2842
2899
|
const bar = spotOf(record["bar"]);
|
|
2843
2900
|
return {
|
|
2844
2901
|
...typeof color === "string" && HEX_COLOR.test(color) ? { color } : {},
|
|
2902
|
+
...isWeight(record["weight"]) ? { weight: record["weight"] } : {},
|
|
2845
2903
|
...palette === void 0 ? {} : { palette },
|
|
2846
2904
|
...bar === void 0 ? {} : { bar }
|
|
2847
2905
|
};
|
|
@@ -2868,10 +2926,11 @@ window.__ModuleLoader__.load({
|
|
|
2868
2926
|
*/
|
|
2869
2927
|
function rememberPreferences(preferences, store = browserStore()) {
|
|
2870
2928
|
if (store === void 0) return;
|
|
2871
|
-
const { color, palette, bar } = preferences;
|
|
2929
|
+
const { color, weight, palette, bar } = preferences;
|
|
2872
2930
|
try {
|
|
2873
2931
|
store.setItem(PREFERENCES_KEY, JSON.stringify({
|
|
2874
2932
|
...color === void 0 ? {} : { color },
|
|
2933
|
+
...weight === void 0 ? {} : { weight: clampWeight(weight) },
|
|
2875
2934
|
...palette === void 0 ? {} : { palette: {
|
|
2876
2935
|
x: palette.x,
|
|
2877
2936
|
y: palette.y
|
|
@@ -3061,6 +3120,41 @@ window.__ModuleLoader__.load({
|
|
|
3061
3120
|
.da-palette .da-tool .da-tool-icon{height:auto}
|
|
3062
3121
|
.da-palette .da-tool .da-tool-icon svg{width:16px;height:16px;stroke-width:1.9}
|
|
3063
3122
|
|
|
3123
|
+
/* --- the weight rail ----------------------------------------------------- */
|
|
3124
|
+
/* A vertical rail along the left edge of the figure. Idle it is a hairline: the
|
|
3125
|
+
whole control is 6px wide, so it reads as a rule at the edge of the drawing
|
|
3126
|
+
rather than as a panel competing with it. Crossing it opens it out to a track
|
|
3127
|
+
with a knob, which is where the width can be read and dragged. A horizontal
|
|
3128
|
+
range input is rotated upright inside the pill, because a range is the one
|
|
3129
|
+
control the browser already knows how to drag with every pointer. */
|
|
3130
|
+
.da-weight{position:absolute;z-index:7;left:6px;top:50%;transform:translateY(-50%);
|
|
3131
|
+
display:flex;align-items:center;justify-content:center;width:6px;height:132px;
|
|
3132
|
+
border-radius:999px;background:var(--dsw-alias-border-l2,rgba(127,127,127,.22));
|
|
3133
|
+
cursor:ns-resize;transition:width .16s ease,background .16s ease,box-shadow .16s ease}
|
|
3134
|
+
.da-weight:hover,.da-weight:focus-within{width:26px;background:var(--dsw-alias-bg-layer-2,#fff);
|
|
3135
|
+
box-shadow:0 5px 18px rgba(20,40,80,.16),0 0 0 0.5px var(--dsw-alias-border-l2,currentColor)}
|
|
3136
|
+
/* The track: a hairline down the middle of the pill, only wide enough to see. */
|
|
3137
|
+
.da-weight::before{content:"";position:absolute;left:50%;top:11px;bottom:11px;width:2px;
|
|
3138
|
+
transform:translateX(-50%);border-radius:1px;background:var(--dsw-alias-border-l3,rgba(127,127,127,.34))}
|
|
3139
|
+
/* The filled part of the track, drawn from the bottom up. */
|
|
3140
|
+
.da-weight-fill{position:absolute;left:50%;bottom:11px;width:2px;transform:translateX(-50%);
|
|
3141
|
+
border-radius:1px;background:var(--dsw-alias-brand-primary,#176df2);opacity:0;transition:opacity .16s ease}
|
|
3142
|
+
.da-weight:hover .da-weight-fill,.da-weight:focus-within .da-weight-fill{opacity:1}
|
|
3143
|
+
/* The knob sits on the value. It is the only part that shows at rest, so the rail
|
|
3144
|
+
reads as a slider waiting to be touched rather than a bare rule. */
|
|
3145
|
+
.da-weight-knob{position:absolute;left:50%;width:9px;height:9px;border-radius:50%;
|
|
3146
|
+
transform:translateX(-50%);background:var(--dsw-alias-bg-layer-2,#fff);
|
|
3147
|
+
box-shadow:0 1px 4px rgba(20,40,80,.32),0 0 0 1px var(--dsw-alias-border-l3,currentColor);
|
|
3148
|
+
transition:width .16s ease,height .16s ease,background .16s ease}
|
|
3149
|
+
.da-weight:hover .da-weight-knob,.da-weight:focus-within .da-weight-knob{
|
|
3150
|
+
width:13px;height:13px;background:var(--dsw-alias-brand-primary,#176df2);
|
|
3151
|
+
box-shadow:0 2px 7px rgba(20,40,80,.34)}
|
|
3152
|
+
/* The browser's own range, rotated upright and made transparent: it keeps the
|
|
3153
|
+
dragging, the arrow keys, the step and the accessible name, while the rail above
|
|
3154
|
+
does the drawing. */
|
|
3155
|
+
.da-weight-input{position:absolute;left:50%;top:50%;width:112px;height:26px;margin:0;opacity:0;
|
|
3156
|
+
transform:translate(-50%,-50%) rotate(-90deg);cursor:ns-resize;appearance:none;background:transparent}
|
|
3157
|
+
|
|
3064
3158
|
/* --- the floating bar ---------------------------------------------------- */
|
|
3065
3159
|
/* The same object as the tool palette: a dark rounded strip over the figure, which
|
|
3066
3160
|
the reader can drag clear of whatever they are looking at. It carries the records
|
|
@@ -3219,9 +3313,31 @@ window.__ModuleLoader__.load({
|
|
|
3219
3313
|
.da-float-record{width:min(330px,calc(100% - 22px))}
|
|
3220
3314
|
.da-float-list{max-height:min(58%,400px)}
|
|
3221
3315
|
.da-float-list .da-marks{overflow:auto;min-height:0}
|
|
3222
|
-
/* The detail
|
|
3223
|
-
come from the mark it belongs to.
|
|
3224
|
-
|
|
3316
|
+
/* The detail field is placed by arithmetic, not by the stylesheet: its left and top
|
|
3317
|
+
come from the mark it belongs to. It is a pill rather than a card — no head, no
|
|
3318
|
+
border between the field and the panel — so writing about a mark costs one line
|
|
3319
|
+
over the figure instead of a box that covers it. */
|
|
3320
|
+
.da-float-detail{width:300px;gap:0;padding:4px;border-radius:22px;
|
|
3321
|
+
flex-direction:row;align-items:flex-end;
|
|
3322
|
+
box-shadow:0 8px 26px rgba(20,40,80,.2),0 0 0 1px rgba(20,40,80,.04)}
|
|
3323
|
+
.da-float .da-comment{flex:1 1 auto;min-width:0;box-sizing:border-box;border:0;background:transparent;
|
|
3324
|
+
color:inherit;font:inherit;font-size:13px;line-height:1.55;padding:7px 10px;resize:none;
|
|
3325
|
+
/* One line until the sentence needs more: the pill is a place to type, not a
|
|
3326
|
+
panel, and the rule for the record cards' textareas sets a 74px floor. */
|
|
3327
|
+
min-height:0;height:33px;max-height:120px;overflow-y:auto;outline:none;field-sizing:content}
|
|
3328
|
+
.da-float .da-comment::placeholder{color:var(--dsw-alias-label-tertiary,currentColor);opacity:.85}
|
|
3329
|
+
/* The hand-off appears only once the field says something: an empty pill is a
|
|
3330
|
+
place to type, not a decision to make. */
|
|
3331
|
+
.da-comment-send{display:flex;align-items:center;gap:4px;flex:0 0 auto;padding-bottom:3px;padding-right:3px}
|
|
3332
|
+
.da-comment-send-btn{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;
|
|
3333
|
+
border:0;border-radius:50%;background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));
|
|
3334
|
+
color:var(--dsw-alias-label-secondary,currentColor);cursor:pointer;padding:0}
|
|
3335
|
+
.da-comment-send-btn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.2));
|
|
3336
|
+
color:var(--dsw-alias-label-primary,currentColor)}
|
|
3337
|
+
.da-comment-send-btn:disabled{opacity:.45;cursor:default}
|
|
3338
|
+
.da-comment-send-btn svg{width:15px;height:15px}
|
|
3339
|
+
.da-comment-send-primary{background:var(--dsw-alias-brand-primary,#176df2);color:#fff}
|
|
3340
|
+
.da-comment-send-primary:hover{background:var(--dsw-alias-brand-primary,#176df2);color:#fff;filter:brightness(1.08)}
|
|
3225
3341
|
.da-float textarea{width:100%;box-sizing:border-box;min-height:74px;max-height:200px;resize:vertical;border:0.5px solid var(--dsw-alias-border-l3,currentColor);border-radius:9px;background:var(--dsw-alias-bg-layer-2,transparent);color:inherit;font:inherit;font-size:12px;padding:8px 10px;line-height:1.65}
|
|
3226
3342
|
.da-float textarea:focus{outline:none;border-color:var(--dsw-alias-brand-primary,#176df2)}
|
|
3227
3343
|
.da-btn-count{display:inline-flex;align-items:center;justify-content:center;min-width:16px;height:16px;
|
|
@@ -3587,27 +3703,6 @@ window.__ModuleLoader__.load({
|
|
|
3587
3703
|
});
|
|
3588
3704
|
}
|
|
3589
3705
|
/**
|
|
3590
|
-
* The small pictures the panel's furniture wears.
|
|
3591
|
-
*
|
|
3592
|
-
* Each is a 14px line drawing on the same 16-unit grid as the tool icons, so the
|
|
3593
|
-
* chrome reads as one set rather than as a crate of borrowed glyphs.
|
|
3594
|
-
*/
|
|
3595
|
-
function NoteIcon() {
|
|
3596
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
3597
|
-
className: "da-ico",
|
|
3598
|
-
viewBox: "0 0 16 16",
|
|
3599
|
-
"aria-hidden": "true",
|
|
3600
|
-
focusable: "false",
|
|
3601
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
3602
|
-
x: "2.5",
|
|
3603
|
-
y: "2.5",
|
|
3604
|
-
width: "11",
|
|
3605
|
-
height: "11",
|
|
3606
|
-
rx: "2"
|
|
3607
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M 5.5 6 h 5 M 5.5 9 h 3.5" })]
|
|
3608
|
-
});
|
|
3609
|
-
}
|
|
3610
|
-
/**
|
|
3611
3706
|
* The overall goal's mark: an arrow that has hit a target.
|
|
3612
3707
|
*
|
|
3613
3708
|
* The record this heads is not a note about one place on the figure — it is what the
|
|
@@ -3842,6 +3937,7 @@ window.__ModuleLoader__.load({
|
|
|
3842
3937
|
*/
|
|
3843
3938
|
const remembered = (0, react.useMemo)(() => readPreferences(), []);
|
|
3844
3939
|
const [color, setColor] = (0, react.useState)(remembered.color ?? COLORS[0]);
|
|
3940
|
+
const [weight, setWeight] = (0, react.useState)(remembered.weight ?? 1);
|
|
3845
3941
|
const [marks, setMarks] = (0, react.useState)(() => [...props.draft?.marks ?? []]);
|
|
3846
3942
|
const [past, setPast] = (0, react.useState)([]);
|
|
3847
3943
|
const [future, setFuture] = (0, react.useState)([]);
|
|
@@ -3998,13 +4094,15 @@ window.__ModuleLoader__.load({
|
|
|
3998
4094
|
(0, react.useEffect)(() => {
|
|
3999
4095
|
rememberPreferences({
|
|
4000
4096
|
color,
|
|
4097
|
+
weight,
|
|
4001
4098
|
...palette === void 0 ? {} : { palette },
|
|
4002
4099
|
...barSpot === void 0 ? {} : { bar: barSpot }
|
|
4003
4100
|
});
|
|
4004
4101
|
}, [
|
|
4005
4102
|
barSpot,
|
|
4006
4103
|
color,
|
|
4007
|
-
palette
|
|
4104
|
+
palette,
|
|
4105
|
+
weight
|
|
4008
4106
|
]);
|
|
4009
4107
|
/**
|
|
4010
4108
|
* The bar's box, when the reader has moved it off the bottom edge.
|
|
@@ -4283,11 +4381,12 @@ window.__ModuleLoader__.load({
|
|
|
4283
4381
|
* exchanged; so is everything inside a record, which is where the writing happens.
|
|
4284
4382
|
*/
|
|
4285
4383
|
(0, react.useEffect)(() => {
|
|
4286
|
-
if (openPanel === null) return;
|
|
4287
4384
|
const onDown = (event) => {
|
|
4288
4385
|
const target = event.target;
|
|
4289
|
-
if (target instanceof Element && (target.closest(".da-float") !== null || target.closest(".da-style-row") !== null || barRef.current?.contains(target) === true)) return;
|
|
4290
|
-
setOpenPanel(null);
|
|
4386
|
+
if (target instanceof Element && (target.closest(".da-float") !== null || target.closest(".da-style-row") !== null || target.closest(".da-palette") !== null || target.closest(".da-weight") !== null || barRef.current?.contains(target) === true)) return;
|
|
4387
|
+
if (openPanel !== null) setOpenPanel(null);
|
|
4388
|
+
if (stageRef.current?.contains(target) === true) return;
|
|
4389
|
+
setSelectedId(null);
|
|
4291
4390
|
};
|
|
4292
4391
|
document.addEventListener("pointerdown", onDown, true);
|
|
4293
4392
|
return () => {
|
|
@@ -4526,22 +4625,15 @@ window.__ModuleLoader__.load({
|
|
|
4526
4625
|
* so a recolour can be taken back like any other edit.
|
|
4527
4626
|
* @param value - the CSS hex string of the chosen swatch.
|
|
4528
4627
|
*/
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
if (!repainted.some((mark, index) => mark !== marks[index])) return;
|
|
4539
|
-
commit(repainted);
|
|
4540
|
-
}, [
|
|
4541
|
-
commit,
|
|
4542
|
-
marks,
|
|
4543
|
-
selectedId
|
|
4544
|
-
]);
|
|
4628
|
+
/**
|
|
4629
|
+
* Change one attribute of the selected mark, or of the next stroke when nothing
|
|
4630
|
+
* is selected.
|
|
4631
|
+
*
|
|
4632
|
+
* A control that is already at the mark's value leaves the list identical, so a
|
|
4633
|
+
* press that changes nothing records no undo step — whether the mark is still
|
|
4634
|
+
* there or was removed while it was selected.
|
|
4635
|
+
* @param patch - the attribute to change.
|
|
4636
|
+
*/
|
|
4545
4637
|
const undo = (0, react.useCallback)(() => {
|
|
4546
4638
|
setPast((history) => {
|
|
4547
4639
|
const previous = history[history.length - 1];
|
|
@@ -4659,7 +4751,7 @@ window.__ModuleLoader__.load({
|
|
|
4659
4751
|
const exportScale = Math.min(2, Math.max(1, 2400 / Math.max(target.width, target.height)));
|
|
4660
4752
|
return await rasterizePng(svg, target.width, target.height, exportScale);
|
|
4661
4753
|
}, [marks]);
|
|
4662
|
-
const buildDocument = (0, react.useCallback)((target) => {
|
|
4754
|
+
const buildDocument = (0, react.useCallback)((target, scope = marks) => {
|
|
4663
4755
|
if (loaded === null) return null;
|
|
4664
4756
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4665
4757
|
return {
|
|
@@ -4674,7 +4766,7 @@ window.__ModuleLoader__.load({
|
|
|
4674
4766
|
},
|
|
4675
4767
|
createdAt: createdAt ?? now,
|
|
4676
4768
|
updatedAt: now,
|
|
4677
|
-
marks,
|
|
4769
|
+
marks: scope,
|
|
4678
4770
|
...summary.trim() === "" ? {} : { summary: summary.trim() }
|
|
4679
4771
|
};
|
|
4680
4772
|
}, [
|
|
@@ -4777,15 +4869,16 @@ window.__ModuleLoader__.load({
|
|
|
4777
4869
|
* would make the reader clear it twice.
|
|
4778
4870
|
* @param target - who receives the annotation.
|
|
4779
4871
|
*/
|
|
4780
|
-
const run = (0, react.useCallback)(async (target) => {
|
|
4781
|
-
|
|
4872
|
+
const run = (0, react.useCallback)(async (target, only) => {
|
|
4873
|
+
const scope = only === void 0 ? marks : [only];
|
|
4874
|
+
if (scope.length === 0 || size === null || size.height <= 0) return;
|
|
4782
4875
|
setBusy("sending");
|
|
4783
4876
|
setStatus({
|
|
4784
4877
|
tone: "info",
|
|
4785
4878
|
text: t(target === "agent" ? "sending" : "composing")
|
|
4786
4879
|
});
|
|
4787
4880
|
try {
|
|
4788
|
-
const document_ = buildDocument(size);
|
|
4881
|
+
const document_ = buildDocument(size, scope);
|
|
4789
4882
|
if (document_ === null) throw new Error(t("loadFailed"));
|
|
4790
4883
|
const review = layer === null || !needsReviewImage(document_) ? void 0 : await exportPng(layer);
|
|
4791
4884
|
const saved = await saveAnnotation(address, document_, review);
|
|
@@ -4846,7 +4939,7 @@ window.__ModuleLoader__.load({
|
|
|
4846
4939
|
discard,
|
|
4847
4940
|
exportPng,
|
|
4848
4941
|
layer,
|
|
4849
|
-
marks
|
|
4942
|
+
marks,
|
|
4850
4943
|
props.conversation,
|
|
4851
4944
|
props.localeId,
|
|
4852
4945
|
props.sessions,
|
|
@@ -4880,6 +4973,37 @@ window.__ModuleLoader__.load({
|
|
|
4880
4973
|
* the next stroke, which is where the remembered preference comes in.
|
|
4881
4974
|
*/
|
|
4882
4975
|
const shownColor = selected === null ? color : selected.color;
|
|
4976
|
+
const shownWeight = selected === null ? weight : selected.weight ?? 1;
|
|
4977
|
+
/**
|
|
4978
|
+
* Change one attribute of the selected mark, or of the next stroke when nothing
|
|
4979
|
+
* is selected.
|
|
4980
|
+
*
|
|
4981
|
+
* A control already at the mark's value leaves the list untouched, so a press
|
|
4982
|
+
* that changes nothing records no undo step.
|
|
4983
|
+
* @param patch - the attribute to change.
|
|
4984
|
+
*/
|
|
4985
|
+
const repaintSelected = (mark, patch) => {
|
|
4986
|
+
if (Object.keys(patch).every((key) => mark[key] === patch[key])) return;
|
|
4987
|
+
commit(marks.map((entry) => entry.id === mark.id ? {
|
|
4988
|
+
...entry,
|
|
4989
|
+
...patch
|
|
4990
|
+
} : entry));
|
|
4991
|
+
};
|
|
4992
|
+
const chooseColor = (value) => {
|
|
4993
|
+
if (selected === null) {
|
|
4994
|
+
setColor(value);
|
|
4995
|
+
return;
|
|
4996
|
+
}
|
|
4997
|
+
repaintSelected(selected, { color: value });
|
|
4998
|
+
};
|
|
4999
|
+
const chooseWeight = (value) => {
|
|
5000
|
+
const held = clampWeight(value);
|
|
5001
|
+
if (selected === null) {
|
|
5002
|
+
setWeight(held);
|
|
5003
|
+
return;
|
|
5004
|
+
}
|
|
5005
|
+
repaintSelected(selected, { weight: held });
|
|
5006
|
+
};
|
|
4883
5007
|
const surfaceStyle = size === null ? {} : {
|
|
4884
5008
|
width: size.width * scale,
|
|
4885
5009
|
height: size.height * scale
|
|
@@ -4955,6 +5079,7 @@ window.__ModuleLoader__.load({
|
|
|
4955
5079
|
marks,
|
|
4956
5080
|
tool,
|
|
4957
5081
|
color,
|
|
5082
|
+
weight,
|
|
4958
5083
|
containerRef: figureHostRef,
|
|
4959
5084
|
anchoring: isVector,
|
|
4960
5085
|
anchorAtFigurePoint: figure?.kind === "html" ? anchorAtFigurePoint : void 0,
|
|
@@ -5122,42 +5247,67 @@ window.__ModuleLoader__.load({
|
|
|
5122
5247
|
left: detailAt.left,
|
|
5123
5248
|
top: detailAt.top
|
|
5124
5249
|
},
|
|
5125
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5250
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
5251
|
+
ref: detailRef,
|
|
5252
|
+
className: "da-comment",
|
|
5253
|
+
value: selected.detail ?? "",
|
|
5254
|
+
placeholder: selected.detail === void 0 || selected.detail.trim() === "" ? t("addComment") : t("detailPlaceholder"),
|
|
5255
|
+
rows: 1,
|
|
5256
|
+
onChange: (event) => {
|
|
5257
|
+
setMarkDetail(selected.id, event.target.value);
|
|
5258
|
+
}
|
|
5259
|
+
}), selected.detail === void 0 || selected.detail.trim() === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5260
|
+
className: "da-comment-send",
|
|
5261
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5262
|
+
type: "button",
|
|
5263
|
+
className: "da-comment-send-btn da-comment-send-primary",
|
|
5264
|
+
disabled: busy !== null,
|
|
5265
|
+
"aria-label": t("sendToAgent"),
|
|
5266
|
+
title: t("sendToAgent"),
|
|
5267
|
+
onClick: () => {
|
|
5268
|
+
run("agent", selected);
|
|
5269
|
+
},
|
|
5270
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SendIcon, {})
|
|
5137
5271
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5138
5272
|
type: "button",
|
|
5139
|
-
className: "da-
|
|
5140
|
-
|
|
5141
|
-
|
|
5273
|
+
className: "da-comment-send-btn",
|
|
5274
|
+
disabled: busy !== null,
|
|
5275
|
+
"aria-label": t("sendToComposer"),
|
|
5276
|
+
title: t("sendToComposer"),
|
|
5142
5277
|
onClick: () => {
|
|
5143
|
-
|
|
5278
|
+
run("composer", selected);
|
|
5144
5279
|
},
|
|
5145
|
-
children:
|
|
5280
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChatIcon, {})
|
|
5146
5281
|
})]
|
|
5147
|
-
})
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5282
|
+
})]
|
|
5283
|
+
}),
|
|
5284
|
+
tool === "point" || selected?.kind === "point" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
5285
|
+
className: "da-weight",
|
|
5286
|
+
title: t("weight"),
|
|
5287
|
+
children: [
|
|
5288
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
5289
|
+
type: "range",
|
|
5290
|
+
className: "da-weight-input",
|
|
5291
|
+
"aria-label": t("weight"),
|
|
5292
|
+
min: WEIGHT_MIN,
|
|
5293
|
+
max: 4,
|
|
5294
|
+
step: .1,
|
|
5295
|
+
value: shownWeight,
|
|
5153
5296
|
onChange: (event) => {
|
|
5154
|
-
|
|
5297
|
+
chooseWeight(Number(event.target.value));
|
|
5155
5298
|
}
|
|
5156
|
-
}),
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5299
|
+
}),
|
|
5300
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5301
|
+
className: "da-weight-fill",
|
|
5302
|
+
"aria-hidden": true,
|
|
5303
|
+
style: { height: `${(shownWeight - WEIGHT_MIN) / (4 - WEIGHT_MIN) * 100}%` }
|
|
5304
|
+
}),
|
|
5305
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5306
|
+
className: "da-weight-knob",
|
|
5307
|
+
"aria-hidden": true,
|
|
5308
|
+
style: { bottom: `calc(${(shownWeight - WEIGHT_MIN) / (4 - WEIGHT_MIN) * 100}% - 5px)` }
|
|
5309
|
+
})
|
|
5310
|
+
]
|
|
5161
5311
|
}),
|
|
5162
5312
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5163
5313
|
className: "da-palette",
|
package/lib/client.pdf.js
CHANGED
|
@@ -25867,10 +25867,11 @@ while (prev === _win[++scan] && prev === _win[++scan] && prev === _win[++scan] &
|
|
|
25867
25867
|
if (mark.kind === "rect" || mark.kind === "ellipse") {
|
|
25868
25868
|
if (turned) points = turnedRing(mark.kind, mark.points, centre, radians);
|
|
25869
25869
|
} else if (spins) points = mark.points.map((point) => turnAround(point, centre, radians));
|
|
25870
|
+
const weight = mark.weight ?? 1;
|
|
25870
25871
|
return {
|
|
25871
25872
|
points: points.map((point) => toUserSpace(transform, point)),
|
|
25872
|
-
width: STROKE_WIDTH / scale,
|
|
25873
|
-
head: ARROW_HEAD / scale,
|
|
25873
|
+
width: STROKE_WIDTH * weight / scale,
|
|
25874
|
+
head: ARROW_HEAD * weight / scale,
|
|
25874
25875
|
turned
|
|
25875
25876
|
};
|
|
25876
25877
|
}
|
package/lib/index.js
CHANGED
|
@@ -260,6 +260,19 @@ function annotationTargetsFigure(document, figurePath) {
|
|
|
260
260
|
return basename(document.figure.path).toLowerCase() === basename(figurePath).toLowerCase();
|
|
261
261
|
}
|
|
262
262
|
//#endregion
|
|
263
|
+
//#region src/shared/annotation.ts
|
|
264
|
+
/** Thinnest stroke a mark can be drawn at, as a multiple of the default width. */
|
|
265
|
+
const WEIGHT_MIN = .5;
|
|
266
|
+
/**
|
|
267
|
+
* Hold a stroke weight inside the range the painter can draw.
|
|
268
|
+
*
|
|
269
|
+
* @param value - the weight to hold.
|
|
270
|
+
* @returns the value, pulled to the nearest end of the range.
|
|
271
|
+
*/
|
|
272
|
+
function clampWeight(value) {
|
|
273
|
+
return Math.min(4, Math.max(WEIGHT_MIN, value));
|
|
274
|
+
}
|
|
275
|
+
//#endregion
|
|
263
276
|
//#region src/host/store.ts
|
|
264
277
|
/**
|
|
265
278
|
* Reading and writing one figure's annotation sidecar.
|
|
@@ -332,6 +345,11 @@ function readAnchor(value) {
|
|
|
332
345
|
* @returns the angle in degrees, or undefined when absent.
|
|
333
346
|
* @throws {Error} when the field is present but is not a usable angle.
|
|
334
347
|
*/
|
|
348
|
+
function readWeight(value) {
|
|
349
|
+
if (value === void 0) return void 0;
|
|
350
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error("annotation mark weight must be a finite multiple of the default width");
|
|
351
|
+
return clampWeight(value);
|
|
352
|
+
}
|
|
335
353
|
function readRotation(value) {
|
|
336
354
|
if (value === void 0) return void 0;
|
|
337
355
|
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error("annotation mark rotation must be a finite number of degrees");
|
|
@@ -377,6 +395,7 @@ function readMark(value, index, figure) {
|
|
|
377
395
|
if (page !== void 0 && figure.pageCount !== void 0 && page > figure.pageCount) throw new Error(`annotation mark ${index} is on page ${page} of a ${figure.pageCount}-page document`);
|
|
378
396
|
const anchor = readAnchor(raw["anchor"]);
|
|
379
397
|
const rotation = readRotation(raw["rotation"]);
|
|
398
|
+
const weight = readWeight(raw["weight"]);
|
|
380
399
|
return {
|
|
381
400
|
id: raw["id"],
|
|
382
401
|
kind,
|
|
@@ -385,6 +404,7 @@ function readMark(value, index, figure) {
|
|
|
385
404
|
...typeof raw["detail"] === "string" ? { detail: raw["detail"] } : {},
|
|
386
405
|
...page === void 0 ? {} : { page },
|
|
387
406
|
...rotation === void 0 ? {} : { rotation },
|
|
407
|
+
...weight === void 0 ? {} : { weight },
|
|
388
408
|
...anchor === void 0 ? {} : { anchor }
|
|
389
409
|
};
|
|
390
410
|
}
|
package/market/index.json
CHANGED
|
@@ -93,12 +93,11 @@
|
|
|
93
93
|
},
|
|
94
94
|
"versions": [
|
|
95
95
|
{
|
|
96
|
-
"version": "1.
|
|
97
|
-
"publishTimestamp":
|
|
96
|
+
"version": "1.1.0",
|
|
97
|
+
"publishTimestamp": 1792725959,
|
|
98
98
|
"source": {
|
|
99
|
-
"type": "
|
|
100
|
-
"
|
|
101
|
-
"commit": "75b0d80331e1bcd12cf431644dd50c3d8fa1083f"
|
|
99
|
+
"type": "npm",
|
|
100
|
+
"registry": "https://registry.npmjs.org"
|
|
102
101
|
},
|
|
103
102
|
"engines": {
|
|
104
103
|
"dsh": ">=0.2.1-rc.2"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-figmark",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "DSH 插件:在右侧栏预览里对科研配图、SVG、PDF、HTML 直接圈画批注,标注写进侧车文件并可一键交给智能体改图源",
|
|
6
6
|
"license": "MIT",
|
|
@@ -79,7 +79,6 @@
|
|
|
79
79
|
},
|
|
80
80
|
"packageManager": "pnpm@11.7.0",
|
|
81
81
|
"scripts": {
|
|
82
|
-
"prepare": "pnpm run build",
|
|
83
82
|
"build": "node -e \"require('node:fs').rmSync('lib',{recursive:true,force:true})\" && tsc --noEmit -p tsconfig.json && tsdown",
|
|
84
83
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
85
84
|
"lint": "oxlint . --type-aware --deny-warnings --ignore-pattern 'tests/gates/fixtures/**'",
|