dsh-figmark 1.0.1 → 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/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 left = [second[0] - ARROW_HEAD * Math.cos(angle - ARROW_SPREAD), second[1] - ARROW_HEAD * Math.sin(angle - ARROW_SPREAD)];
1081
- const right = [second[0] - ARROW_HEAD * Math.cos(angle + ARROW_SPREAD), second[1] - ARROW_HEAD * Math.sin(angle + ARROW_SPREAD)];
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 = STROKE_WIDTH / 2;
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: STROKE_WIDTH,
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="${STROKE_WIDTH}" stroke-linecap="round" stroke-linejoin="round"/>`);
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 card is placed by arithmetic, not by the stylesheet: its left and top
3223
- come from the mark it belongs to. */
3224
- .da-float-detail{width:252px}
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
- const chooseColor = (0, react.useCallback)((value) => {
4530
- if (selectedId === null) {
4531
- setColor(value);
4532
- return;
4533
- }
4534
- const repainted = marks.map((mark) => mark.id === selectedId && mark.color !== value ? {
4535
- ...mark,
4536
- color: value
4537
- } : mark);
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
- if (marks.length === 0 || size === null || size.height <= 0) return;
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.length,
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.jsxs)("div", {
5126
- className: "da-float-head",
5127
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5128
- className: "da-card-title",
5129
- children: [
5130
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NoteIcon, {}),
5131
- t("detail"),
5132
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5133
- className: "da-chip",
5134
- children: t(TOOL_COPY[selected.kind])
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-btn da-btn-quiet",
5140
- "aria-label": t("close"),
5141
- title: t("close"),
5273
+ className: "da-comment-send-btn",
5274
+ disabled: busy !== null,
5275
+ "aria-label": t("sendToComposer"),
5276
+ title: t("sendToComposer"),
5142
5277
  onClick: () => {
5143
- setSelectedId(null);
5278
+ run("composer", selected);
5144
5279
  },
5145
- children: "✕"
5280
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChatIcon, {})
5146
5281
  })]
5147
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5148
- className: "da-field",
5149
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
5150
- ref: detailRef,
5151
- value: selected.detail ?? "",
5152
- placeholder: t("detailPlaceholder"),
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
- setMarkDetail(selected.id, event.target.value);
5297
+ chooseWeight(Number(event.target.value));
5155
5298
  }
5156
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5157
- className: "da-count",
5158
- children: [(selected.detail ?? "").length, "/500"]
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,8 +93,8 @@
93
93
  },
94
94
  "versions": [
95
95
  {
96
- "version": "1.0.1",
97
- "publishTimestamp": 1792640347,
96
+ "version": "1.1.0",
97
+ "publishTimestamp": 1792725959,
98
98
  "source": {
99
99
  "type": "npm",
100
100
  "registry": "https://registry.npmjs.org"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-figmark",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "private": false,
5
5
  "description": "DSH 插件:在右侧栏预览里对科研配图、SVG、PDF、HTML 直接圈画批注,标注写进侧车文件并可一键交给智能体改图源",
6
6
  "license": "MIT",