dsh-figmark 1.1.0 → 1.1.2

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
@@ -26,6 +26,7 @@ window.__ModuleLoader__.load({
26
26
  clear: "清空",
27
27
  detail: "这个元素的详细说明",
28
28
  addComment: "添加评论…",
29
+ pinSize: "标记大小",
29
30
  weight: "线条粗细",
30
31
  detailPlaceholder: "这一处具体要改什么(例如「这个标号应指向滑套 34」)",
31
32
  summary: "总体目标",
@@ -113,6 +114,7 @@ window.__ModuleLoader__.load({
113
114
  clear: "Clear",
114
115
  detail: "What this element needs",
115
116
  addComment: "Add a comment…",
117
+ pinSize: "Marker size",
116
118
  weight: "Line weight",
117
119
  detailPlaceholder: "What should change here (for example \"this numeral should point at the sleeve 34\")",
118
120
  summary: "Overall goal",
@@ -1013,7 +1015,7 @@ window.__ModuleLoader__.load({
1013
1015
  }
1014
1016
  //#endregion
1015
1017
  //#region src/client/render.ts
1016
- /** Stroke width of every mark, in figure pixels. */
1018
+ /** Stroke width of a mark drawn at weight 1 on a reference-sized figure, in pixels. */
1017
1019
  const STROKE_WIDTH = 2.5;
1018
1020
  /**
1019
1021
  * The stroke width one mark is drawn at.
@@ -1023,17 +1025,36 @@ window.__ModuleLoader__.load({
1023
1025
  * thicker pen. The arrow's barbs are weighed by the same multiple, because an
1024
1026
  * arrow drawn with a thick pen has a thick head.
1025
1027
  *
1028
+ * The figure's own unit multiplies everything drawn on it, so the same mark is a
1029
+ * bold line on a thumbnail and a hairline on a scan — see `./scale`.
1030
+ *
1026
1031
  * @param mark - the mark to weigh.
1032
+ * @param unit - the figure's size multiple, 1 for a reference-sized figure.
1027
1033
  * @returns its stroke width in figure pixels.
1028
1034
  */
1029
- function strokeWidthOf(mark) {
1030
- return STROKE_WIDTH * (mark.weight ?? 1);
1035
+ function strokeWidthOf(mark, unit = 1) {
1036
+ return STROKE_WIDTH * (mark.weight ?? 1) * unit;
1031
1037
  }
1032
1038
  /** Arrow head length, in figure pixels. */
1033
1039
  const ARROW_HEAD = 14;
1034
1040
  /** Half-angle of the arrow head, in radians. */
1035
1041
  const ARROW_SPREAD = Math.PI / 7;
1036
1042
  /**
1043
+ * Half-width of one pin's head, in figure pixels.
1044
+ *
1045
+ * A pin is a marker the reader compares against the line work, so it is sized like a
1046
+ * stroke: by the figure's unit and by the mark's own weight. The weight is reused
1047
+ * rather than given its own field because it already means "how big this mark is" to
1048
+ * the reader, and one rail on the left is enough — a stroke and a pin are never the
1049
+ * same mark, so the one number never has to mean two things at once.
1050
+ * @param mark - the point mark.
1051
+ * @param unit - the figure's size multiple.
1052
+ * @returns its head's half-width in figure pixels.
1053
+ */
1054
+ function pinRadiusOf(mark, unit = 1) {
1055
+ return 10 * (mark.weight ?? 1) * unit;
1056
+ }
1057
+ /**
1037
1058
  * How far a pin's head sits above its coordinate, as a multiple of its half-width.
1038
1059
  *
1039
1060
  * A map pin puts its *tip* on the coordinate it names, so the head stands clear of
@@ -1062,6 +1083,20 @@ window.__ModuleLoader__.load({
1062
1083
  * every colour the palette offers.
1063
1084
  */
1064
1085
  const PIN_NUMBER_COLOR = "#ffffff";
1086
+ /**
1087
+ * Font size of the number inside a pin, in figure pixels.
1088
+ *
1089
+ * Scaled by the mark's weight as well as the figure: the rail that makes a pin bigger
1090
+ * makes its head bigger, and a head that grows around a fixed-size numeral reads as a
1091
+ * circle with a speck in it. The number is the pin's whole payload, so it keeps the
1092
+ * same share of the head at every size.
1093
+ * @param mark - the point mark whose pin is being drawn.
1094
+ * @param unit - the figure's size multiple.
1095
+ * @returns the font size to draw the number at.
1096
+ */
1097
+ function pinFontSizeOf(mark, unit = 1) {
1098
+ return 11 * (mark.weight ?? 1) * unit;
1099
+ }
1065
1100
  /** Normalize two opposite corners into a top-left origin and positive extents. */
1066
1101
  function bounds(first, second) {
1067
1102
  return {
@@ -1111,11 +1146,11 @@ window.__ModuleLoader__.load({
1111
1146
  * @param points - the arrow's tail and tip.
1112
1147
  * @returns tail, tip and both barbs; the stored points alone when it is half drawn.
1113
1148
  */
1114
- function arrowBarbs(points, weight = 1) {
1149
+ function arrowBarbs(points, weight = 1, unit = 1) {
1115
1150
  const [first, second] = points;
1116
1151
  if (first === void 0 || second === void 0) return points;
1117
1152
  const angle = Math.atan2(second[1] - first[1], second[0] - first[0]);
1118
- const head = ARROW_HEAD * weight;
1153
+ const head = ARROW_HEAD * weight * unit;
1119
1154
  const left = [second[0] - head * Math.cos(angle - ARROW_SPREAD), second[1] - head * Math.sin(angle - ARROW_SPREAD)];
1120
1155
  const right = [second[0] - head * Math.cos(angle + ARROW_SPREAD), second[1] - head * Math.sin(angle + ARROW_SPREAD)];
1121
1156
  return [
@@ -1170,10 +1205,10 @@ window.__ModuleLoader__.load({
1170
1205
  * @param mark - the point mark.
1171
1206
  * @returns the marker's path, number centre and box, or undefined with no coordinate.
1172
1207
  */
1173
- function pinGeometry(mark) {
1208
+ function pinGeometry(mark, unit = 1) {
1174
1209
  const tip = mark.points[0];
1175
1210
  if (tip === void 0) return void 0;
1176
- const radius = 10;
1211
+ const radius = pinRadiusOf(mark, unit);
1177
1212
  const lift = radius * PIN_LIFT;
1178
1213
  const [tipX, tipY] = tip;
1179
1214
  const up = tipY - lift - radius >= 0;
@@ -1207,16 +1242,16 @@ window.__ModuleLoader__.load({
1207
1242
  * @param mark - the point mark.
1208
1243
  * @returns path data, or undefined when the mark carries no coordinate.
1209
1244
  */
1210
- function pinPathData(mark) {
1211
- return pinGeometry(mark)?.path;
1245
+ function pinPathData(mark, unit = 1) {
1246
+ return pinGeometry(mark, unit)?.path;
1212
1247
  }
1213
1248
  /**
1214
1249
  * Where the number inside a `point` mark's marker sits.
1215
1250
  * @param mark - the point mark.
1216
1251
  * @returns the number's centre, or undefined when the mark carries no coordinate.
1217
1252
  */
1218
- function pinNumberCentre(mark) {
1219
- return pinGeometry(mark)?.number;
1253
+ function pinNumberCentre(mark, unit = 1) {
1254
+ return pinGeometry(mark, unit)?.number;
1220
1255
  }
1221
1256
  /**
1222
1257
  * The box a `point` mark's pin covers.
@@ -1229,8 +1264,8 @@ window.__ModuleLoader__.load({
1229
1264
  * @param mark - the point mark.
1230
1265
  * @returns the pin's box, or undefined when the mark carries no point.
1231
1266
  */
1232
- function pointMarkBox(mark) {
1233
- return pinGeometry(mark)?.box;
1267
+ function pointMarkBox(mark, unit = 1) {
1268
+ return pinGeometry(mark, unit)?.box;
1234
1269
  }
1235
1270
  /**
1236
1271
  * The box a mark covers once it is drawn, rotation included.
@@ -1243,14 +1278,14 @@ window.__ModuleLoader__.load({
1243
1278
  * @param mark - the mark to measure.
1244
1279
  * @returns the box, or undefined when the mark carries no coordinate.
1245
1280
  */
1246
- function markOutline(mark) {
1247
- if (mark.kind === "point") return pointMarkBox(mark);
1248
- const geometry = mark.kind === "arrow" ? arrowBarbs(mark.points, mark.weight ?? 1) : mark.points;
1281
+ function markOutline(mark, unit = 1) {
1282
+ if (mark.kind === "point") return pointMarkBox(mark, unit);
1283
+ const geometry = mark.kind === "arrow" ? arrowBarbs(mark.points, mark.weight ?? 1, unit) : mark.points;
1249
1284
  const straight = pointsBox(geometry);
1250
1285
  if (straight === void 0) return void 0;
1251
1286
  const radians = markRadians(mark);
1252
1287
  const box = radians === 0 ? straight : turnedBox(geometry, straight, radians);
1253
- const grow = strokeWidthOf(mark) / 2;
1288
+ const grow = strokeWidthOf(mark, unit) / 2;
1254
1289
  return {
1255
1290
  x: box.x - grow,
1256
1291
  y: box.y - grow,
@@ -1324,6 +1359,42 @@ window.__ModuleLoader__.load({
1324
1359
  return `rotate(${n(radians * 180 / Math.PI)} ${n(cx)} ${n(cy)})`;
1325
1360
  }
1326
1361
  //#endregion
1362
+ //#region src/client/scale.ts
1363
+ /**
1364
+ * How big the marks should be for the figure they are drawn on.
1365
+ *
1366
+ * A stroke is not a physical thing: 2.5 pixels is a bold line on a 300px thumbnail
1367
+ * and a hairline on a 4000px scan. Marks are comparisons the reader makes against
1368
+ * the line work underneath them, so they have to be sized in the same currency as
1369
+ * the figure — a fraction of it — rather than in the figure's pixels.
1370
+ *
1371
+ * The reference edge is the size a science figure is usually exported at. An
1372
+ * ordinary figure then gets the unit it always had, and only the outliers (a
1373
+ * thumbnail, a poster) are scaled. The range is clamped rather than proportional
1374
+ * because a 20000px figure would otherwise get strokes 40px wide, which reads as a
1375
+ * highlighter rather than a line.
1376
+ * @module dsh-figmark/client/scale
1377
+ */
1378
+ /** The figure edge, in pixels, that gets the unit it always had. */
1379
+ const REFERENCE_EDGE = 1200;
1380
+ /** Smallest multiple of the base sizes, for figures far below the reference. */
1381
+ const MIN_UNIT = .75;
1382
+ /**
1383
+ * The size everything drawn on a figure is multiplied by.
1384
+ *
1385
+ * Measured against the figure's longer edge: that is the dimension a reader judges
1386
+ * a stroke against, and using the area would shrink a tall thin panel's marks for a
1387
+ * reason the eye does not see.
1388
+ * @param width - the figure's intrinsic width, in figure pixels.
1389
+ * @param height - the figure's intrinsic height, in figure pixels.
1390
+ * @returns the multiple of the base sizes, held inside {@link MIN_UNIT} and {@link MAX_UNIT}.
1391
+ */
1392
+ function figureUnit(width, height) {
1393
+ const edge = Math.max(width, height);
1394
+ if (!Number.isFinite(edge) || edge <= 0) return 1;
1395
+ return Math.min(3, Math.max(MIN_UNIT, edge / REFERENCE_EDGE));
1396
+ }
1397
+ //#endregion
1327
1398
  //#region src/client/Canvas.tsx
1328
1399
  /**
1329
1400
  * The drawing surface.
@@ -1334,6 +1405,19 @@ window.__ModuleLoader__.load({
1334
1405
  * means the same thing at any zoom or pane width.
1335
1406
  * @module dsh-figmark/client/Canvas
1336
1407
  */
1408
+ /**
1409
+ * The `weight` field of a new mark, or nothing when it is the default.
1410
+ *
1411
+ * Absent means the default throughout this plugin, so writing `weight: 1` would put
1412
+ * a field into every mark that says nothing — sidecars would grow a key for every
1413
+ * mark ever drawn, and every comparison of two documents would see a difference
1414
+ * where there is none.
1415
+ * @param weight - the weight the next stroke is drawn at.
1416
+ * @returns the field, or an empty object for the default.
1417
+ */
1418
+ function weightField(weight) {
1419
+ return weight === 1 ? {} : { weight };
1420
+ }
1337
1421
  /** Hit width of an invisible selection stroke, in figure pixels. */
1338
1422
  const HIT_WIDTH = 14;
1339
1423
  /** Minimum distance between recorded freehand samples, in figure pixels. */
@@ -1357,6 +1441,7 @@ window.__ModuleLoader__.load({
1357
1441
  */
1358
1442
  function Canvas(props) {
1359
1443
  const { width, height, marks, tool, color, containerRef, anchoring } = props;
1444
+ const [originX, originY] = props.origin ?? [0, 0];
1360
1445
  const weight = props.weight ?? 1;
1361
1446
  /**
1362
1447
  * Each mark's number, by id.
@@ -1366,6 +1451,14 @@ window.__ModuleLoader__.load({
1366
1451
  * otherwise.
1367
1452
  */
1368
1453
  const numbers = new Map(marks.map((mark, index) => [mark.id, props.numbering?.get(mark.id) ?? index + 1]));
1454
+ /**
1455
+ * How big everything is drawn here, as a multiple of the base sizes.
1456
+ *
1457
+ * Derived from the surface rather than passed in: every mark on this canvas shares
1458
+ * one figure, so one number decides the pen for all of them, and a caller that
1459
+ * forgot to thread it through could not make a mark disagree with its hit area.
1460
+ */
1461
+ const unit = figureUnit(width, height);
1369
1462
  const [draft, setDraft] = (0, react.useState)(null);
1370
1463
  const [press, setPress] = (0, react.useState)(null);
1371
1464
  const drawing = (0, react.useRef)(false);
@@ -1410,7 +1503,7 @@ window.__ModuleLoader__.load({
1410
1503
  const rect = event.currentTarget.getBoundingClientRect();
1411
1504
  const scaleX = rect.width === 0 ? 1 : width / rect.width;
1412
1505
  const scaleY = rect.height === 0 ? 1 : height / rect.height;
1413
- return [(event.clientX - rect.left) * scaleX, (event.clientY - rect.top) * scaleY];
1506
+ return [(event.clientX - rect.left) * scaleX - originX, (event.clientY - rect.top) * scaleY - originY];
1414
1507
  };
1415
1508
  /**
1416
1509
  * Describe what the mark landed on.
@@ -1592,6 +1685,7 @@ window.__ModuleLoader__.load({
1592
1685
  kind: "point",
1593
1686
  color,
1594
1687
  points: [point],
1688
+ ...weightField(weight),
1595
1689
  ...anchor === void 0 ? {} : { anchor }
1596
1690
  };
1597
1691
  props.onAdd(mark);
@@ -1608,7 +1702,7 @@ window.__ModuleLoader__.load({
1608
1702
  kind: tool,
1609
1703
  color,
1610
1704
  points: seed,
1611
- weight
1705
+ ...weightField(weight)
1612
1706
  });
1613
1707
  };
1614
1708
  const move = (event) => {
@@ -1634,10 +1728,18 @@ window.__ModuleLoader__.load({
1634
1728
  ...press,
1635
1729
  snap: event.shiftKey
1636
1730
  };
1731
+ const edit = editFor(held, point, dx, dy);
1637
1732
  setPress({
1638
1733
  ...held,
1639
- edit: editFor(held, point, dx, dy)
1734
+ edit
1640
1735
  });
1736
+ const base = props.marks.find((candidate) => candidate.id === press.id);
1737
+ /* v8 ignore next -- a press always begins on a mark that is still in the list. */
1738
+ if (base === void 0) return;
1739
+ const live = { ...base };
1740
+ if (edit.points !== void 0) live.points = edit.points;
1741
+ if (edit.rotation !== void 0) live.rotation = edit.rotation;
1742
+ props.onLiveEdit?.(live);
1641
1743
  return;
1642
1744
  }
1643
1745
  if (!drawing.current || draft === null) return;
@@ -1665,6 +1767,7 @@ window.__ModuleLoader__.load({
1665
1767
  if (press !== null) {
1666
1768
  const done = press.edit;
1667
1769
  setPress(null);
1770
+ props.onLiveEdit?.(null);
1668
1771
  if (done === null) return;
1669
1772
  if (press.handle === void 0) {
1670
1773
  /* v8 ignore next -- a move always carries the geometry it moved to. */
@@ -1754,8 +1857,8 @@ window.__ModuleLoader__.load({
1754
1857
  * compared to a washed-out sticker.
1755
1858
  */
1756
1859
  const renderPoint = (mark) => {
1757
- const pin = pinPathData(mark);
1758
- const centre = pinNumberCentre(mark);
1860
+ const pin = pinPathData(mark, unit);
1861
+ const centre = pinNumberCentre(mark, unit);
1759
1862
  /* v8 ignore next -- a point mark carries exactly the one coordinate it was placed at. */
1760
1863
  if (pin === void 0 || centre === void 0) return null;
1761
1864
  const number = numbers.get(mark.id);
@@ -1764,21 +1867,21 @@ window.__ModuleLoader__.load({
1764
1867
  d: pin,
1765
1868
  fill: "#ffffff",
1766
1869
  stroke: "#ffffff",
1767
- strokeWidth: 4.5,
1870
+ strokeWidth: 4.5 * unit,
1768
1871
  strokeLinejoin: "round"
1769
1872
  }),
1770
1873
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1771
1874
  d: pin,
1772
1875
  fill: mark.color,
1773
1876
  stroke: "#ffffff",
1774
- strokeWidth: 1.5,
1877
+ strokeWidth: 1.5 * unit,
1775
1878
  strokeLinejoin: "round"
1776
1879
  }),
1777
1880
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
1778
1881
  x: centre[0],
1779
1882
  y: centre[1],
1780
1883
  fontFamily: PIN_FONT_FAMILY,
1781
- fontSize: 11,
1884
+ fontSize: pinFontSizeOf(mark, unit),
1782
1885
  fontWeight: 700,
1783
1886
  fill: PIN_NUMBER_COLOR,
1784
1887
  textAnchor: "middle",
@@ -1796,19 +1899,19 @@ window.__ModuleLoader__.load({
1796
1899
  * handles that would do nothing.
1797
1900
  */
1798
1901
  const renderPointFrame = (shown) => {
1799
- const box = pointMarkBox(shown);
1902
+ const box = pointMarkBox(shown, unit);
1800
1903
  /* v8 ignore next -- a point mark carries exactly the one coordinate it was placed at. */
1801
1904
  if (box === void 0) return null;
1802
1905
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
1803
- x: box.x - 3,
1804
- y: box.y - 3,
1805
- width: box.width + 6,
1806
- height: box.height + 6,
1807
- rx: 3,
1906
+ x: box.x - 3 * unit,
1907
+ y: box.y - 3 * unit,
1908
+ width: box.width + 6 * unit,
1909
+ height: box.height + 6 * unit,
1910
+ rx: 3 * unit,
1808
1911
  fill: "none",
1809
1912
  stroke: "#4c8dff",
1810
- strokeWidth: 1,
1811
- strokeDasharray: "3 2",
1913
+ strokeWidth: unit,
1914
+ strokeDasharray: `${3 * unit} ${2 * unit}`,
1812
1915
  style: { pointerEvents: "none" }
1813
1916
  });
1814
1917
  };
@@ -1818,7 +1921,7 @@ window.__ModuleLoader__.load({
1818
1921
  const selected = props.selectedId === mark.id;
1819
1922
  const transform = markTransform(shown);
1820
1923
  const isPoint = mark.kind === "point";
1821
- const pin = isPoint ? pinPathData(shown) : void 0;
1924
+ const pin = isPoint ? pinPathData(shown, unit) : void 0;
1822
1925
  /**
1823
1926
  * What a press on the drawn shape starts.
1824
1927
  *
@@ -1830,7 +1933,7 @@ window.__ModuleLoader__.load({
1830
1933
  d: pin,
1831
1934
  fill: "transparent",
1832
1935
  stroke: "transparent",
1833
- strokeWidth: HIT_WIDTH,
1936
+ strokeWidth: HIT_WIDTH * unit,
1834
1937
  style: {
1835
1938
  pointerEvents: "all",
1836
1939
  cursor: "pointer"
@@ -1855,7 +1958,7 @@ window.__ModuleLoader__.load({
1855
1958
  d: path,
1856
1959
  fill: "none",
1857
1960
  stroke: "#ffffff",
1858
- strokeWidth: strokeWidthOf(shown) + 2.5,
1961
+ strokeWidth: strokeWidthOf(shown, unit) + 2.5 * unit,
1859
1962
  strokeLinecap: "round",
1860
1963
  strokeLinejoin: "round"
1861
1964
  }),
@@ -1863,7 +1966,7 @@ window.__ModuleLoader__.load({
1863
1966
  d: path,
1864
1967
  fill: "none",
1865
1968
  stroke: shown.color,
1866
- strokeWidth: strokeWidthOf(shown),
1969
+ strokeWidth: strokeWidthOf(shown, unit),
1867
1970
  strokeLinecap: "round",
1868
1971
  strokeLinejoin: "round"
1869
1972
  }),
@@ -1871,7 +1974,7 @@ window.__ModuleLoader__.load({
1871
1974
  d: path,
1872
1975
  fill: "none",
1873
1976
  stroke: "transparent",
1874
- strokeWidth: HIT_WIDTH,
1977
+ strokeWidth: HIT_WIDTH * unit,
1875
1978
  style: {
1876
1979
  pointerEvents: "stroke",
1877
1980
  cursor: "pointer"
@@ -1892,9 +1995,13 @@ window.__ModuleLoader__.load({
1892
1995
  ]
1893
1996
  }, mark.id);
1894
1997
  };
1895
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1998
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1896
1999
  className: "da-overlay",
1897
2000
  "data-da-overlay": "",
2001
+ style: {
2002
+ left: originX,
2003
+ top: originY
2004
+ },
1898
2005
  width,
1899
2006
  height,
1900
2007
  viewBox: `0 0 ${width} ${height}`,
@@ -1916,7 +2023,10 @@ window.__ModuleLoader__.load({
1916
2023
  }
1917
2024
  props.onSelect(null);
1918
2025
  },
1919
- children: [marks.map((mark) => renderMark(mark, false)), draft === null ? null : renderMark(draft, true)]
2026
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", {
2027
+ transform: `translate(${originX} ${originY})`,
2028
+ children: [marks.map((mark) => renderMark(mark, false)), draft === null ? null : renderMark(draft, true)]
2029
+ })
1920
2030
  });
1921
2031
  }
1922
2032
  /** A difference that never changes, for a body that has no edit yet. */
@@ -2478,6 +2588,44 @@ window.__ModuleLoader__.load({
2478
2588
  const existing = shell.snapshot.draft.trim();
2479
2589
  shell.setDraft(existing === "" ? message : `${existing}\n\n${message}`);
2480
2590
  }
2591
+ /**
2592
+ * The box the whole drawing occupies: the figure, plus wherever the marks reach.
2593
+ *
2594
+ * A figure with every mark inside it gets exactly its own box back, which is the
2595
+ * case the overlay has always drawn; the growth only happens for marks that leave
2596
+ * it. The margin is applied only on sides that actually grew, so an ordinary figure
2597
+ * is not padded on all four sides for nothing.
2598
+ * @param width - the figure's width, in figure pixels.
2599
+ * @param height - the figure's height, in figure pixels.
2600
+ * @param marks - the marks drawn on it.
2601
+ * @param unit - the figure's size multiple, so outlines are measured as drawn.
2602
+ * @returns the box the drawing occupies, in figure coordinates.
2603
+ */
2604
+ function plotBounds(width, height, marks, unit = 1) {
2605
+ let left = 0;
2606
+ let top = 0;
2607
+ let right = width;
2608
+ let bottom = height;
2609
+ for (const mark of marks) {
2610
+ const box = markOutline(mark, unit);
2611
+ /* v8 ignore next -- a mark with no coordinate covers nothing to grow for. */
2612
+ if (box === void 0) continue;
2613
+ left = Math.min(left, box.x);
2614
+ top = Math.min(top, box.y);
2615
+ right = Math.max(right, box.x + box.width);
2616
+ bottom = Math.max(bottom, box.y + box.height);
2617
+ }
2618
+ const padLeft = left < 0 ? 12 : 0;
2619
+ const padTop = top < 0 ? 12 : 0;
2620
+ const padRight = right > width ? 12 : 0;
2621
+ const padBottom = bottom > height ? 12 : 0;
2622
+ return {
2623
+ x: left - padLeft,
2624
+ y: top - padTop,
2625
+ width: right + padRight - (left - padLeft),
2626
+ height: bottom + padBottom - (top - padTop)
2627
+ };
2628
+ }
2481
2629
  //#endregion
2482
2630
  //#region src/client/export.ts
2483
2631
  /**
@@ -2485,17 +2633,19 @@ window.__ModuleLoader__.load({
2485
2633
  *
2486
2634
  * @param mark - the mark to draw.
2487
2635
  * @param number - the mark's number in the list, when it carries one.
2636
+ * @param unit - the figure's size multiple, so the export is drawn at the same
2637
+ * weight the reader saw.
2488
2638
  * @returns the mark's elements.
2489
2639
  */
2490
- function markSvg(mark, number) {
2640
+ function markSvg(mark, number, unit) {
2491
2641
  const path = markPathData(mark);
2492
2642
  const parts = [];
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"/>`);
2494
- const pin = mark.kind === "point" ? pinPathData(mark) : void 0;
2495
- const centre = mark.kind === "point" ? pinNumberCentre(mark) : void 0;
2643
+ if (path !== void 0) parts.push(`<path d="${path}" fill="none" stroke="#ffffff" stroke-width="${strokeWidthOf(mark, unit) + 2.5 * unit}" stroke-linecap="round" stroke-linejoin="round"/>`, `<path d="${path}" fill="none" stroke="${mark.color}" stroke-width="${strokeWidthOf(mark, unit)}" stroke-linecap="round" stroke-linejoin="round"/>`);
2644
+ const pin = mark.kind === "point" ? pinPathData(mark, unit) : void 0;
2645
+ const centre = mark.kind === "point" ? pinNumberCentre(mark, unit) : void 0;
2496
2646
  if (pin !== void 0 && centre !== void 0) {
2497
- parts.push(`<path d="${pin}" fill="${mark.color}" stroke="#ffffff" stroke-width="2" stroke-linejoin="round"/>`);
2498
- if (number !== void 0) parts.push(`<text x="${centre[0]}" y="${centre[1]}" font-family="${PIN_FONT_FAMILY}" font-size="11" font-weight="700" fill="${PIN_NUMBER_COLOR}" text-anchor="middle" dominant-baseline="central">${number}</text>`);
2647
+ parts.push(`<path d="${pin}" fill="${mark.color}" stroke="#ffffff" stroke-width="${2 * unit}" stroke-linejoin="round"/>`);
2648
+ if (number !== void 0) parts.push(`<text x="${centre[0]}" y="${centre[1]}" font-family="${PIN_FONT_FAMILY}" font-size="${pinFontSizeOf(mark, unit)}" font-weight="700" fill="${PIN_NUMBER_COLOR}" text-anchor="middle" dominant-baseline="central">${number}</text>`);
2499
2649
  }
2500
2650
  return parts.join("");
2501
2651
  }
@@ -2531,7 +2681,27 @@ window.__ModuleLoader__.load({
2531
2681
  */
2532
2682
  function composeReviewSvg(layer, marks, numbering) {
2533
2683
  const numbers = new Map(marks.map((mark, index) => [mark.id, numbering?.get(mark.id) ?? index + 1]));
2534
- return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${layer.width}" height="${layer.height}" viewBox="0 0 ${layer.width} ${layer.height}"><rect x="0" y="0" width="${layer.width}" height="${layer.height}" fill="#ffffff"/>` + figureSvg(layer) + marks.map((mark) => markSvg(mark, numbers.get(mark.id))).join("") + "</svg>";
2684
+ const unit = figureUnit(layer.width, layer.height);
2685
+ const paper = plotBounds(layer.width, layer.height, marks, unit);
2686
+ const shift = `translate(${-paper.x} ${-paper.y})`;
2687
+ return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${paper.width}" height="${paper.height}" viewBox="0 0 ${paper.width} ${paper.height}"><rect x="0" y="0" width="${paper.width}" height="${paper.height}" fill="#ffffff"/><g transform="${shift}">` + figureSvg(layer) + marks.map((mark) => markSvg(mark, numbers.get(mark.id), unit)).join("") + "</g></svg>";
2688
+ }
2689
+ /**
2690
+ * The size of the picture {@link composeReviewSvg} builds.
2691
+ *
2692
+ * A caller has to know it before rasterizing: an SVG rendered at the figure's size
2693
+ * would crop away exactly the marks that made the paper grow.
2694
+ * @param layer - the figure to place underneath.
2695
+ * @param marks - marks in draw order.
2696
+ * @param unit - the figure's size multiple.
2697
+ * @returns the paper's width and height, in figure pixels.
2698
+ */
2699
+ function reviewSize(layer, marks, unit = figureUnit(layer.width, layer.height)) {
2700
+ const paper = plotBounds(layer.width, layer.height, marks, unit);
2701
+ return {
2702
+ width: paper.width,
2703
+ height: paper.height
2704
+ };
2535
2705
  }
2536
2706
  /**
2537
2707
  * Rasterization scale for one review image.
@@ -3120,39 +3290,45 @@ window.__ModuleLoader__.load({
3120
3290
  .da-palette .da-tool .da-tool-icon{height:auto}
3121
3291
  .da-palette .da-tool .da-tool-icon svg{width:16px;height:16px;stroke-width:1.9}
3122
3292
 
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. */
3293
+ /* --- the size rail ------------------------------------------------------- */
3294
+ /* A vertical rail along the left edge of the figure: how heavy the pen is, and how
3295
+ big a point mark's pin is. Idle it is a hairline, so it costs the figure nothing;
3296
+ crossing it opens it out to a track. The track is *tapered* — a thin line at the
3297
+ bottom and a broad one at the top — because the control's whole subject is
3298
+ thickness, and a wedge says which way is heavier without reading a number.
3299
+ A horizontal range input is rotated upright inside the pill: it keeps dragging,
3300
+ arrow keys, the step and the accessible name, while the rail above does the
3301
+ drawing. The drawn parts never take the pointer, or grabbing the knob would grab
3302
+ the knob instead of the slider. */
3130
3303
  .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;
3304
+ display:flex;align-items:center;justify-content:center;width:7px;height:200px;
3132
3305
  border-radius:999px;background:var(--dsw-alias-border-l2,rgba(127,127,127,.22));
3133
3306
  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);
3307
+ .da-weight:hover,.da-weight:focus-within{width:30px;background:var(--dsw-alias-bg-layer-2,#fff);
3135
3308
  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);
3309
+ /* The track is an svg in the body: a hairline at the bottom that opens into a rounded
3310
+ shoulder at the top. It is not a clip-path polygon because a polygon has no rounded
3311
+ ends — the corners were what made the wedge read as a cut-out blade rather than a
3312
+ part of the panel. preserveAspectRatio="none" stretches one 12x100 shape over the
3313
+ rail's height, so the shoulders stay smooth at any length. */
3314
+ .da-weight-svg{position:absolute;left:50%;top:12px;bottom:12px;width:18px;
3315
+ transform:translateX(-50%);opacity:0;pointer-events:none;transition:opacity .16s ease}
3316
+ .da-weight:hover .da-weight-svg,.da-weight:focus-within .da-weight-svg{opacity:1}
3317
+ /* The knob sits on the value: the only part that shows at rest, so the rail reads as
3318
+ a slider waiting to be touched rather than a bare rule. */
3319
+ .da-weight-knob{position:absolute;left:50%;width:var(--da-knob-size,11px);height:var(--da-knob-size,11px);
3320
+ border-radius:50%;transform:translateX(-50%);pointer-events:none;
3321
+ background:var(--dsw-alias-bg-layer-2,#fff);
3322
+ box-shadow:0 1px 4px rgba(20,40,80,.28),0 0 0 1px var(--dsw-alias-border-l3,currentColor);
3148
3323
  transition:width .16s ease,height .16s ease,background .16s ease}
3324
+ .da-weight:hover,.da-weight:focus-within{--da-knob-size:15px}
3149
3325
  .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;
3326
+ background:var(--dsw-alias-brand-primary,#176df2);
3327
+ box-shadow:0 2px 7px rgba(20,40,80,.3)}
3328
+ /* The browser's own range, rotated upright and made transparent. It spans the whole
3329
+ rail so a press anywhere on it lands on the slider — including on the knob, which
3330
+ is why the knob is pointer-transparent. */
3331
+ .da-weight-input{position:absolute;left:50%;top:50%;width:200px;height:30px;margin:0;opacity:0;
3156
3332
  transform:translate(-50%,-50%) rotate(-90deg);cursor:ns-resize;appearance:none;background:transparent}
3157
3333
 
3158
3334
  /* --- the floating bar ---------------------------------------------------- */
@@ -3243,6 +3419,10 @@ window.__ModuleLoader__.load({
3243
3419
  needs to see where the drawing stops. */
3244
3420
  .da-stage{position:relative;display:flex;align-items:safe center;justify-content:safe center;flex:1 1 auto;overflow:auto;min-height:0;padding:14px;background:var(--dsw-alias-bg-document-preview,transparent)}
3245
3421
  .da-surface{position:relative;flex:0 0 auto;margin:0;transform-origin:top left;background:#fff;border-radius:4px;box-shadow:var(--dsw-elevation-prominent,0 1px 3px rgba(0,0,0,.14),0 8px 24px rgba(0,0,0,.08))}
3422
+ /* The paper: the figure plus the margin a mark may have needed. It is the white sheet
3423
+ the drawing sits on, so a mark that reaches past the figure lands on paper rather
3424
+ than on the panel behind it. */
3425
+ .da-paper{background:#fff}
3246
3426
  .da-figure{display:block;user-select:none;-webkit-user-drag:none}
3247
3427
  /* The table is a thing you can push. The hand only shows under the tools that
3248
3428
  leave a drag free to move the view: under a drawing tool the same drag draws. */
@@ -3253,7 +3433,7 @@ window.__ModuleLoader__.load({
3253
3433
  /* The rendered document takes the figure's place; the overlay above it owns every
3254
3434
  pointer event, which is also what keeps a link inside it from navigating. */
3255
3435
  .da-html{display:block;border:0;background:#fff;pointer-events:none}
3256
- .da-overlay{position:absolute;inset:0;touch-action:none}
3436
+ .da-overlay{position:absolute;touch-action:none}
3257
3437
  .da-overlay[data-mode="view"]{pointer-events:none}
3258
3438
  .da-overlay[data-tool="select"]{cursor:default}
3259
3439
  .da-overlay:not([data-tool="select"]){cursor:crosshair}
@@ -3317,14 +3497,19 @@ window.__ModuleLoader__.load({
3317
3497
  come from the mark it belongs to. It is a pill rather than a card — no head, no
3318
3498
  border between the field and the panel — so writing about a mark costs one line
3319
3499
  over the figure instead of a box that covers it. */
3320
- .da-float-detail{width:300px;gap:0;padding:4px;border-radius:22px;
3500
+ .da-float-detail{width:300px;max-width:calc(100% - 16px);gap:0;padding:4px;border-radius:22px;
3321
3501
  flex-direction:row;align-items:flex-end;
3322
3502
  box-shadow:0 8px 26px rgba(20,40,80,.2),0 0 0 1px rgba(20,40,80,.04)}
3323
3503
  .da-float .da-comment{flex:1 1 auto;min-width:0;box-sizing:border-box;border:0;background:transparent;
3324
3504
  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}
3505
+ /* One line until the sentence needs more, then as many as it takes: the pill is a
3506
+ place to type, and a sentence read through a slot two lines tall is a sentence
3507
+ the reader has to scroll to finish. The rule for the record cards' textareas sets
3508
+ a 74px floor, hence the explicit reset. The scrollbar is hidden because it is not
3509
+ a scroll view — wheel and keys still work once the sentence outgrows the pane. */
3510
+ min-height:0;height:auto;max-height:min(180px,42vh);overflow-y:auto;outline:none;
3511
+ field-sizing:content;scrollbar-width:none}
3512
+ .da-float .da-comment::-webkit-scrollbar{width:0;height:0}
3328
3513
  .da-float .da-comment::placeholder{color:var(--dsw-alias-label-tertiary,currentColor);opacity:.85}
3329
3514
  /* The hand-off appears only once the field says something: an empty pill is a
3330
3515
  place to type, not a decision to make. */
@@ -3544,13 +3729,34 @@ window.__ModuleLoader__.load({
3544
3729
  * @param size - the card's size, in CSS pixels.
3545
3730
  * @returns the card's top-left corner, in the panel's coordinates.
3546
3731
  */
3732
+ /**
3733
+ * The size to place a card by: the card's own, or a stand-in before it is measured.
3734
+ *
3735
+ * A comment pill grows with the sentence in it, so a placement that assumed one line
3736
+ * would push a long comment off the bottom of the panel. The element is measured once
3737
+ * it has been laid out; before that — the first frame of a selection, and in any
3738
+ * environment that reports zero boxes — the caller's estimate is the only size there
3739
+ * is, and the next pass corrects it.
3740
+ * @param element - the laid-out card, when there is one.
3741
+ * @param fallback - the size to use when the card has not been measured.
3742
+ * @returns the width and height to place by.
3743
+ */
3744
+ function measuredSize(element, fallback) {
3745
+ if (element === null || element.offsetHeight === 0) return fallback;
3746
+ return {
3747
+ width: element.offsetWidth,
3748
+ height: element.offsetHeight
3749
+ };
3750
+ }
3547
3751
  function placeCard(anchor, panelWidth, panelHeight, size = {
3548
- width: 252,
3549
- height: 196
3752
+ width: 300,
3753
+ height: 48
3550
3754
  }) {
3551
3755
  const below = anchor.y + anchor.height + 10;
3552
3756
  const above = anchor.y - size.height - 10;
3553
- const top = below + size.height <= panelHeight - 8 ? below : Math.max(8, above);
3757
+ const roomBelow = panelHeight - 8 - below;
3758
+ const roomAbove = anchor.y - 10 - 8;
3759
+ const top = roomBelow >= size.height ? below : roomAbove >= size.height ? above : Math.max(8, roomAbove > roomBelow ? above : below);
3554
3760
  const widest = Math.max(8, panelWidth - size.width - 8);
3555
3761
  return {
3556
3762
  left: Math.min(Math.max(8, anchor.x), widest),
@@ -3614,6 +3820,27 @@ window.__ModuleLoader__.load({
3614
3820
  "#2f9e44"
3615
3821
  ];
3616
3822
  /** Tools in toolbar order. */
3823
+ /**
3824
+ * The rail's track, drawn on an 18x100 grid and stretched to whatever height it gets.
3825
+ *
3826
+ * A hairline at the bottom opening into a round shoulder at the top: the control's
3827
+ * subject is thickness, so the track itself says which way is heavier. Every edge is
3828
+ * curved — straight flanks make a paper dart, and a square cap makes a cut-off blade —
3829
+ * so the profile is eased and the top is closed with an arc a little wider than the
3830
+ * shoulders. Stretching it vertically (preserveAspectRatio="none") costs nothing
3831
+ * visually: the only vertical features are straight lines, and the horizontal ones are
3832
+ * what the gradient is for.
3833
+ */
3834
+ const WEIGHT_TRACK = [
3835
+ "M 8.2 100",
3836
+ "C 8.2 78 7.9 58 6.4 38",
3837
+ "C 5.4 25 3.2 16 1.9 11",
3838
+ "A 7.1 7.1 0 0 1 16.1 11",
3839
+ "C 14.8 16 12.6 25 11.6 38",
3840
+ "C 10.1 58 9.8 78 9.8 100",
3841
+ "A 0.8 0.8 0 0 1 8.2 100",
3842
+ "Z"
3843
+ ].join(" ");
3617
3844
  const TOOLS = [
3618
3845
  {
3619
3846
  tool: "select",
@@ -3640,6 +3867,20 @@ window.__ModuleLoader__.load({
3640
3867
  label: "toolPoint"
3641
3868
  }
3642
3869
  ];
3870
+ /**
3871
+ * The tools that draw something with a size the rail can set.
3872
+ *
3873
+ * `select` is not among them: it draws nothing, so with it armed and no mark
3874
+ * selected the rail has no subject. `point` is, because a pin is sized by the same
3875
+ * number — the rail asks "how big is this mark" either way.
3876
+ */
3877
+ const WEIGHT_TOOLS = /* @__PURE__ */ new Set([
3878
+ "arrow",
3879
+ "rect",
3880
+ "ellipse",
3881
+ "pen",
3882
+ "point"
3883
+ ]);
3643
3884
  /** The copy key naming the shape each kind of mark is, for the list's second line. */
3644
3885
  const TOOL_COPY = {
3645
3886
  arrow: "toolArrow",
@@ -3849,9 +4090,27 @@ window.__ModuleLoader__.load({
3849
4090
  const isVector = mediaType === "image/svg+xml";
3850
4091
  const t = (0, react.useCallback)((key) => resolveTranslate(props, key), [props.t, props.localeId]);
3851
4092
  const sessionId = props.sessionId ?? parsedAddress?.sessionId;
4093
+ /**
4094
+ * A private prefix for the rail's gradient and clip ids.
4095
+ *
4096
+ * Two bodies can be mounted at once (a PDF lays out one per page), and an id is
4097
+ * document-wide: sharing one would make every rail's fill follow the first rail's
4098
+ * selection. Declared here, with the other hooks, because the ids are read far below
4099
+ * the early returns for a figure that failed to load.
4100
+ */
4101
+ const trackId = `da-weight-${(0, react.useId)().replaceAll(":", "")}`;
3852
4102
  const stageRef = (0, react.useRef)(null);
3853
4103
  /** The box a double clicked mark's detail is written in. */
3854
4104
  const detailRef = (0, react.useRef)(null);
4105
+ /**
4106
+ * The comment pill around that box.
4107
+ *
4108
+ * Measured rather than the textarea inside it: the field gives up width to the two
4109
+ * hand-off buttons as soon as there is something to send, so a placement computed
4110
+ * from the field's box would put the buttons past the edge of the panel — which is
4111
+ * exactly what a comment pill against the right edge used to do.
4112
+ */
4113
+ const pillRef = (0, react.useRef)(null);
3855
4114
  const barRef = (0, react.useRef)(null);
3856
4115
  const figureHostRef = (0, react.useRef)(null);
3857
4116
  const surfaceRef = (0, react.useRef)(null);
@@ -3988,6 +4247,14 @@ window.__ModuleLoader__.load({
3988
4247
  */
3989
4248
  const [detailAt, setDetailAt] = (0, react.useState)(null);
3990
4249
  /**
4250
+ * The selected mark as a drag is currently shaping it.
4251
+ *
4252
+ * Kept beside the stored marks rather than in them: the gesture is not committed
4253
+ * until it is released, so writing it into the list would make every frame an
4254
+ * undoable edit. It exists only so the pill can follow the hand.
4255
+ */
4256
+ const [liveMark, setLiveMark] = (0, react.useState)(null);
4257
+ /**
3991
4258
  * Shut the style panel when the reader picks up a tool.
3992
4259
  *
3993
4260
  * Arming a tool is the moment the panel has done its job; leaving it open over
@@ -4175,6 +4442,23 @@ window.__ModuleLoader__.load({
4175
4442
  rasterSize
4176
4443
  ]);
4177
4444
  /**
4445
+ * How big everything drawn is, as a multiple of the base sizes.
4446
+ *
4447
+ * One number for the whole surface: the pen, the pins and the review image all
4448
+ * have to agree, and a stroke is only bold or thin relative to the figure it sits
4449
+ * on. A 4000px scan and a 300px thumbnail get the same-looking mark this way.
4450
+ */
4451
+ const unit = size === null ? 1 : figureUnit(size.width, size.height);
4452
+ /**
4453
+ * The paper the drawing needs: the figure, plus anywhere the marks reach past it.
4454
+ *
4455
+ * A reader boxing the margin a plot clips, or flagging a spot just outside the
4456
+ * axes, is drawing something the figure does not contain. The paper grows to hold
4457
+ * it rather than the mark being clipped — and because the surface is white, a mark
4458
+ * out there lands on paper instead of on nothing.
4459
+ */
4460
+ const paper = size === null ? null : plotBounds(size.width, size.height, marks, unit);
4461
+ /**
4178
4462
  * Everything a background write needs, read at write time.
4179
4463
  *
4180
4464
  * A write runs from a timer that outlives the render that scheduled it, so it has
@@ -4406,31 +4690,38 @@ window.__ModuleLoader__.load({
4406
4690
  const root = rootRef.current;
4407
4691
  const host = figureRef.current;
4408
4692
  const mark = marks.find((candidate) => candidate.id === selectedId);
4409
- const box = mark === void 0 ? void 0 : markOutline(mark);
4693
+ const drawn = liveMark !== null && liveMark.id === selectedId ? liveMark : mark;
4694
+ const box = drawn === void 0 ? void 0 : markOutline(drawn, unit);
4410
4695
  if (root === null || host === null || box === void 0 || size === null) {
4411
4696
  setDetailAt(null);
4412
4697
  return;
4413
4698
  }
4414
4699
  const panel = root.getBoundingClientRect();
4415
- const drawn = host.getBoundingClientRect();
4700
+ const measured = host.getBoundingClientRect();
4701
+ const pillSize = measuredSize(pillRef.current, {
4702
+ width: 300,
4703
+ height: 48
4704
+ });
4416
4705
  const at = placeCard(scaledBox(box, {
4417
- x: drawn.left,
4418
- y: drawn.top,
4419
- width: drawn.width,
4420
- height: drawn.height
4706
+ x: measured.left,
4707
+ y: measured.top,
4708
+ width: measured.width,
4709
+ height: measured.height
4421
4710
  }, {
4422
4711
  x: panel.left,
4423
4712
  y: panel.top,
4424
4713
  width: panel.width,
4425
4714
  height: panel.height
4426
- }, size.width), panel.width, panel.height);
4715
+ }, size.width), panel.width, panel.height, pillSize);
4427
4716
  setDetailAt((previous) => previous !== null && previous.left === at.left && previous.top === at.top ? previous : at);
4428
4717
  }, [
4718
+ htmlHeight,
4719
+ liveMark,
4429
4720
  marks,
4430
- selectedId,
4431
4721
  scale,
4722
+ selectedId,
4432
4723
  size,
4433
- htmlHeight
4724
+ unit
4434
4725
  ]);
4435
4726
  /**
4436
4727
  * The factor currently drawn, kept beside the state that will hold it.
@@ -4748,8 +5039,9 @@ window.__ModuleLoader__.load({
4748
5039
  ]);
4749
5040
  const exportPng = (0, react.useCallback)(async (target) => {
4750
5041
  const svg = composeReviewSvg(target, marks);
4751
- const exportScale = Math.min(2, Math.max(1, 2400 / Math.max(target.width, target.height)));
4752
- return await rasterizePng(svg, target.width, target.height, exportScale);
5042
+ const paper = reviewSize(target, marks);
5043
+ const exportScale = Math.min(2, Math.max(1, 2400 / Math.max(paper.width, paper.height)));
5044
+ return await rasterizePng(svg, paper.width, paper.height, exportScale);
4753
5045
  }, [marks]);
4754
5046
  const buildDocument = (0, react.useCallback)((target, scope = marks) => {
4755
5047
  if (loaded === null) return null;
@@ -4973,7 +5265,23 @@ window.__ModuleLoader__.load({
4973
5265
  * the next stroke, which is where the remembered preference comes in.
4974
5266
  */
4975
5267
  const shownColor = selected === null ? color : selected.color;
4976
- const shownWeight = selected === null ? weight : selected.weight ?? 1;
5268
+ /**
5269
+ * What the size rail currently means, or null when it means nothing.
5270
+ *
5271
+ * It sizes one thing: the stroke of the mark the reader is working on, or the pen
5272
+ * of the next stroke. With nothing selected and no drawing tool out there is no
5273
+ * such thing, so the rail stands down instead of offering a control for a decision
5274
+ * nobody is making. A point mark borrows the same number for its pin, so the rail
5275
+ * stays when one is selected — it is the mark's size, whichever way it is drawn.
5276
+ */
5277
+ const shownWeight = selected === null ? WEIGHT_TOOLS.has(tool) ? weight : null : selected.weight ?? 1;
5278
+ /**
5279
+ * How far up the rail that value sits, from 0 at the lightest to 1 at the heaviest.
5280
+ *
5281
+ * One number for the fill, the knob and the input: computed three times the three
5282
+ * could round differently and the knob would sit a pixel off the fill it belongs to.
5283
+ */
5284
+ const weightFraction = shownWeight === null ? 0 : (shownWeight - WEIGHT_MIN) / (4 - WEIGHT_MIN);
4977
5285
  /**
4978
5286
  * Change one attribute of the selected mark, or of the next stroke when nothing
4979
5287
  * is selected.
@@ -5004,16 +5312,32 @@ window.__ModuleLoader__.load({
5004
5312
  }
5005
5313
  repaintSelected(selected, { weight: held });
5006
5314
  };
5007
- const surfaceStyle = size === null ? {} : {
5008
- width: size.width * scale,
5009
- height: size.height * scale
5315
+ const surfaceStyle = paper === null ? {} : {
5316
+ width: paper.width * scale,
5317
+ height: paper.height * scale
5010
5318
  };
5011
- const figureStyle = size === null ? {} : {
5012
- width: size.width,
5013
- height: size.height,
5319
+ /**
5320
+ * The paper at its unscaled size, with the figure placed at its own offset on it.
5321
+ *
5322
+ * The whole paper is scaled by one transform rather than each part scaling itself,
5323
+ * so the figure and the overlay cannot drift apart at a fractional factor — which
5324
+ * at 60% is exactly what a mark drawn on a line work and a mark drawn on paper
5325
+ * would do.
5326
+ */
5327
+ const paperStyle = paper === null ? {} : {
5328
+ position: "relative",
5329
+ width: paper.width,
5330
+ height: paper.height,
5014
5331
  transform: `scale(${scale})`,
5015
5332
  transformOrigin: "top left"
5016
5333
  };
5334
+ const figureStyle = paper === null ? {} : {
5335
+ position: "absolute",
5336
+ left: -paper.x,
5337
+ top: -paper.y,
5338
+ width: size?.width,
5339
+ height: size?.height
5340
+ };
5017
5341
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5018
5342
  className: "da-root",
5019
5343
  ref: rootRef,
@@ -5042,64 +5366,67 @@ window.__ModuleLoader__.load({
5042
5366
  className: "da-surface",
5043
5367
  ref: surfaceRef,
5044
5368
  style: surfaceStyle,
5045
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5046
- ref: figureRef,
5047
- style: {
5048
- ...figureStyle,
5049
- position: "relative"
5050
- },
5051
- children: [figure?.kind === "svg" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5052
- ref: figureHostRef,
5053
- dangerouslySetInnerHTML: { __html: figure.markup }
5054
- }) : figure?.kind === "raster" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
5055
- className: "da-figure",
5056
- src: figure.objectUrl,
5057
- width: size?.width,
5058
- height: size?.height,
5059
- alt: "",
5060
- onLoad: (event) => {
5061
- const image = event.currentTarget;
5062
- setFileRasterSize({
5063
- width: image.naturalWidth,
5064
- height: image.naturalHeight
5065
- });
5066
- }
5067
- }) : figure?.kind === "html" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("iframe", {
5068
- ref: frameRef,
5069
- className: "da-html",
5070
- title: t("htmlFrame"),
5071
- sandbox: "allow-same-origin",
5072
- style: {
5073
- width: figure.width,
5074
- height: htmlHeight
5075
- }
5076
- }) : null, size === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Canvas, {
5077
- width: size.width,
5078
- height: size.height,
5079
- marks,
5080
- tool,
5081
- color,
5082
- weight,
5083
- containerRef: figureHostRef,
5084
- anchoring: isVector,
5085
- anchorAtFigurePoint: figure?.kind === "html" ? anchorAtFigurePoint : void 0,
5086
- selectedId,
5087
- onSelect: setSelectedId,
5088
- onAdd: addMark,
5089
- onMove: moveMark,
5090
- onUpdate: updateMark,
5091
- onRemove: removeMark,
5092
- onPan: panBy,
5093
- onPanStart: () => {
5094
- setPanning(true);
5095
- },
5096
- onPanEnd: () => {
5097
- setPanning(false);
5098
- },
5099
- onEdit: editMark,
5100
- onDrawn: finishDrawing,
5101
- numbering: props.numbering
5102
- })]
5369
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5370
+ className: "da-paper",
5371
+ style: paperStyle,
5372
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5373
+ ref: figureRef,
5374
+ style: figureStyle,
5375
+ children: [figure?.kind === "svg" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5376
+ ref: figureHostRef,
5377
+ dangerouslySetInnerHTML: { __html: figure.markup }
5378
+ }) : figure?.kind === "raster" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
5379
+ className: "da-figure",
5380
+ src: figure.objectUrl,
5381
+ width: size?.width,
5382
+ height: size?.height,
5383
+ alt: "",
5384
+ onLoad: (event) => {
5385
+ const image = event.currentTarget;
5386
+ setFileRasterSize({
5387
+ width: image.naturalWidth,
5388
+ height: image.naturalHeight
5389
+ });
5390
+ }
5391
+ }) : figure?.kind === "html" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("iframe", {
5392
+ ref: frameRef,
5393
+ className: "da-html",
5394
+ title: t("htmlFrame"),
5395
+ sandbox: "allow-same-origin",
5396
+ style: {
5397
+ width: figure.width,
5398
+ height: htmlHeight
5399
+ }
5400
+ }) : null, paper === null || size === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Canvas, {
5401
+ width: paper.width,
5402
+ height: paper.height,
5403
+ origin: [-paper.x, -paper.y],
5404
+ marks,
5405
+ tool,
5406
+ color,
5407
+ weight,
5408
+ containerRef: figureHostRef,
5409
+ anchoring: isVector,
5410
+ anchorAtFigurePoint: figure?.kind === "html" ? anchorAtFigurePoint : void 0,
5411
+ selectedId,
5412
+ onSelect: setSelectedId,
5413
+ onAdd: addMark,
5414
+ onMove: moveMark,
5415
+ onUpdate: updateMark,
5416
+ onRemove: removeMark,
5417
+ onPan: panBy,
5418
+ onPanStart: () => {
5419
+ setPanning(true);
5420
+ },
5421
+ onPanEnd: () => {
5422
+ setPanning(false);
5423
+ },
5424
+ onEdit: editMark,
5425
+ onDrawn: finishDrawing,
5426
+ onLiveEdit: setLiveMark,
5427
+ numbering: props.numbering
5428
+ })]
5429
+ })
5103
5430
  })
5104
5431
  })
5105
5432
  }),
@@ -5243,6 +5570,7 @@ window.__ModuleLoader__.load({
5243
5570
  className: "da-float da-float-detail",
5244
5571
  role: "group",
5245
5572
  "aria-label": t("detail"),
5573
+ ref: pillRef,
5246
5574
  style: {
5247
5575
  left: detailAt.left,
5248
5576
  top: detailAt.top
@@ -5281,14 +5609,14 @@ window.__ModuleLoader__.load({
5281
5609
  })]
5282
5610
  })]
5283
5611
  }),
5284
- tool === "point" || selected?.kind === "point" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5612
+ shownWeight === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5285
5613
  className: "da-weight",
5286
- title: t("weight"),
5614
+ title: t(selected?.kind === "point" || tool === "point" ? "pinSize" : "weight"),
5287
5615
  children: [
5288
5616
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
5289
5617
  type: "range",
5290
5618
  className: "da-weight-input",
5291
- "aria-label": t("weight"),
5619
+ "aria-label": t(selected?.kind === "point" || tool === "point" ? "pinSize" : "weight"),
5292
5620
  min: WEIGHT_MIN,
5293
5621
  max: 4,
5294
5622
  step: .1,
@@ -5297,15 +5625,49 @@ window.__ModuleLoader__.load({
5297
5625
  chooseWeight(Number(event.target.value));
5298
5626
  }
5299
5627
  }),
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}%` }
5628
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
5629
+ className: "da-weight-svg",
5630
+ viewBox: "0 0 18 100",
5631
+ preserveAspectRatio: "none",
5632
+ "aria-hidden": "true",
5633
+ children: [
5634
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("defs", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("linearGradient", {
5635
+ id: `${trackId}-shade`,
5636
+ x1: "0",
5637
+ y1: "1",
5638
+ x2: "0",
5639
+ y2: "0",
5640
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("stop", {
5641
+ offset: "0",
5642
+ stopColor: "rgba(20,40,80,.05)"
5643
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("stop", {
5644
+ offset: "1",
5645
+ stopColor: "rgba(20,40,80,.2)"
5646
+ })]
5647
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("clipPath", {
5648
+ id: `${trackId}-cut`,
5649
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
5650
+ x: "0",
5651
+ y: 100 - weightFraction * 100,
5652
+ width: "18",
5653
+ height: weightFraction * 100
5654
+ })
5655
+ })] }),
5656
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
5657
+ d: WEIGHT_TRACK,
5658
+ fill: `url(#${trackId}-shade)`
5659
+ }),
5660
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
5661
+ d: WEIGHT_TRACK,
5662
+ fill: "var(--dsw-alias-brand-primary,#176df2)",
5663
+ clipPath: `url(#${trackId}-cut)`
5664
+ })
5665
+ ]
5304
5666
  }),
5305
5667
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5306
5668
  className: "da-weight-knob",
5307
5669
  "aria-hidden": true,
5308
- style: { bottom: `calc(${(shownWeight - WEIGHT_MIN) / (4 - WEIGHT_MIN) * 100}% - 5px)` }
5670
+ style: { bottom: `calc(12px + ${weightFraction} * (100% - 24px - var(--da-knob-size, 11px)))` }
5309
5671
  })
5310
5672
  ]
5311
5673
  }),
@@ -5644,15 +6006,18 @@ window.__ModuleLoader__.load({
5644
6006
  const images = [];
5645
6007
  const numbering = new Map(document.marks.map((mark, index) => [mark.id, index + 1]));
5646
6008
  for (const page of pages) {
5647
- const svg = composeReviewSvg({
6009
+ const layer = {
5648
6010
  kind: "raster",
5649
6011
  dataUrl: page.dataUrl,
5650
6012
  width: page.width,
5651
6013
  height: page.height
5652
- }, document.marks.filter((mark) => mark.page === page.page), numbering);
6014
+ };
6015
+ const marks = document.marks.filter((mark) => mark.page === page.page);
6016
+ const svg = composeReviewSvg(layer, marks, numbering);
6017
+ const paper = reviewSize(layer, marks);
5653
6018
  images.push({
5654
6019
  page: page.page,
5655
- image: await rasterizePng(svg, page.width, page.height, reviewScale(page.width, page.height))
6020
+ image: await rasterizePng(svg, paper.width, paper.height, reviewScale(paper.width, paper.height))
5656
6021
  });
5657
6022
  }
5658
6023
  return images;
@@ -6178,6 +6543,7 @@ window.__ModuleLoader__.load({
6178
6543
  createAutosave,
6179
6544
  annotationSignature,
6180
6545
  autosaveDelayMs: 600,
6546
+ figureUnit,
6181
6547
  seat: createDocumentSeat({
6182
6548
  sessions: ctx.get("sessions"),
6183
6549
  conversation: ctx.get("conversation"),
package/lib/client.pdf.js CHANGED
@@ -25856,7 +25856,7 @@ while (prev === _win[++scan] && prev === _win[++scan] && prev === _win[++scan] &
25856
25856
  * @param transform - `[a b c d e f]`, user space → page units.
25857
25857
  * @returns the placed geometry.
25858
25858
  */
25859
- function place(mark, transform) {
25859
+ function place(mark, transform, unit) {
25860
25860
  const scale = unitScale(transform);
25861
25861
  const radians = (mark.rotation ?? 0) * Math.PI / 180;
25862
25862
  const box = pointsBox(mark.points);
@@ -25867,7 +25867,7 @@ 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
+ const weight = (mark.weight ?? 1) * unit;
25871
25871
  return {
25872
25872
  points: points.map((point) => toUserSpace(transform, point)),
25873
25873
  width: STROKE_WIDTH * weight / scale,
@@ -25963,13 +25963,14 @@ while (prev === _win[++scan] && prev === _win[++scan] && prev === _win[++scan] &
25963
25963
  * @param rect - the box the appearance is drawn in.
25964
25964
  * @returns the content stream operators.
25965
25965
  */
25966
- function pointContent(mark, placed, rect) {
25966
+ function pointContent(mark, placed, rect, unit) {
25967
25967
  const point = placed.points[0];
25968
25968
  /* v8 ignore next -- a point mark carries exactly the one coordinate it was placed at. */
25969
25969
  if (point === void 0) return "";
25970
25970
  const [x, y] = [point[0] - rect.x, point[1] - rect.y];
25971
- const radius = POINT_RADIUS / Math.max(placed.width / STROKE_WIDTH, 1e-4);
25972
- const dot = POINT_DOT_RADIUS / Math.max(placed.width / STROKE_WIDTH, 1e-4);
25971
+ const base = Math.max(placed.width / (STROKE_WIDTH * unit), 1e-4);
25972
+ const radius = POINT_RADIUS / base;
25973
+ const dot = POINT_DOT_RADIUS / base;
25973
25974
  const ring = ellipsePath(x, y, radius, radius);
25974
25975
  const centre = ellipsePath(x, y, dot, dot);
25975
25976
  return `${colorOperators(mark.color, "RG")} ${num(placed.width)} w 1 J ${ring} S ${colorOperators(mark.color, "rg")} ${centre} f`;
@@ -26014,8 +26015,8 @@ while (prev === _win[++scan] && prev === _win[++scan] && prev === _win[++scan] &
26014
26015
  * @returns the appearance stream reference, or undefined when the mark draws
26015
26016
  * nothing a stream is needed for.
26016
26017
  */
26017
- function appearance(context, mark, placed, rect) {
26018
- const content = mark.kind === "point" ? pointContent(mark, placed, rect) : strokeContent(mark, placed, rect);
26018
+ function appearance(context, mark, placed, rect, unit) {
26019
+ const content = mark.kind === "point" ? pointContent(mark, placed, rect, unit) : strokeContent(mark, placed, rect);
26019
26020
  if (content === "") return void 0;
26020
26021
  const dictionary = import_cjs.PDFDict.withContext(context);
26021
26022
  dictionary.set(import_cjs.PDFName.of("Type"), import_cjs.PDFName.of("XObject"));
@@ -26054,11 +26055,11 @@ while (prev === _win[++scan] && prev === _win[++scan] && prev === _win[++scan] &
26054
26055
  * @param transform - `[a b c d e f]`, user space → page units.
26055
26056
  * @returns the annotation dictionary.
26056
26057
  */
26057
- function annotation(context, mark, transform) {
26058
- const placed = place(mark, transform);
26058
+ function annotation(context, mark, transform, unit) {
26059
+ const placed = place(mark, transform, unit);
26059
26060
  if (mark.points[0] === void 0 || placed.points[0] === void 0) throw new Error("an annotation mark needs at least one coordinate pair");
26060
26061
  const rect = boundsOf(drawnPoints(mark, placed), placed.width * 2);
26061
- const box = mark.kind === "point" ? boundsOf(placed.points, POINT_RADIUS * 2) : rect;
26062
+ const box = mark.kind === "point" ? boundsOf(placed.points, POINT_RADIUS * unit * 2) : rect;
26062
26063
  const dictionary = import_cjs.PDFDict.withContext(context);
26063
26064
  dictionary.set(import_cjs.PDFName.of("Type"), import_cjs.PDFName.of("Annot"));
26064
26065
  dictionary.set(import_cjs.PDFName.of("Subtype"), import_cjs.PDFName.of(annotationSubtype(mark)));
@@ -26102,7 +26103,7 @@ while (prev === _win[++scan] && prev === _win[++scan] && prev === _win[++scan] &
26102
26103
  dictionary.set(import_cjs.PDFName.of("LE"), endings);
26103
26104
  }
26104
26105
  }
26105
- const stream = appearance(context, mark, placed, box);
26106
+ const stream = appearance(context, mark, placed, box, unit);
26106
26107
  if (stream !== void 0) {
26107
26108
  const streams = import_cjs.PDFDict.withContext(context);
26108
26109
  streams.set(import_cjs.PDFName.of("N"), stream);
@@ -26150,7 +26151,10 @@ while (prev === _win[++scan] && prev === _win[++scan] && prev === _win[++scan] &
26150
26151
  const page = document.getPage(input.page - 1);
26151
26152
  if (input.marks.length === 0) continue;
26152
26153
  const annots = pageAnnots(document.context, page);
26153
- for (const mark of input.marks) annots.push(document.context.register(annotation(document.context, mark, input.transform)));
26154
+ for (const mark of input.marks) {
26155
+ const unit = input.unit ?? 1;
26156
+ annots.push(document.context.register(annotation(document.context, mark, input.transform, unit)));
26157
+ }
26154
26158
  }
26155
26159
  return await document.save();
26156
26160
  }
@@ -50274,7 +50278,7 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
50274
50278
  });
50275
50279
  try {
50276
50280
  const pages = markedPages(marks);
50277
- const annotatedPdf = await annotatePdf(bytes, await annotateInputs(pdf, pages));
50281
+ const annotatedPdf = await annotatePdf(bytes, await annotateInputs(pdf, pages, props.figureUnit));
50278
50282
  const saved = await seats.save(address, input, annotatedPdf);
50279
50283
  autosave.cancel();
50280
50284
  writtenSignature.current = signature(input);
@@ -50456,14 +50460,15 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
50456
50460
  * @param pages - the marked pages.
50457
50461
  * @returns one input per page, each with its marks and its user-space transform.
50458
50462
  */
50459
- async function annotateInputs(pdf, pages) {
50463
+ async function annotateInputs(pdf, pages, unitOf) {
50460
50464
  const inputs = [];
50461
50465
  for (const entry of pages) {
50462
50466
  const size = await pdf.size(entry.page);
50463
50467
  inputs.push({
50464
50468
  page: entry.page,
50465
50469
  transform: size.transform,
50466
- marks: entry.marks
50470
+ marks: entry.marks,
50471
+ unit: unitOf(size.width, size.height)
50467
50472
  });
50468
50473
  }
50469
50474
  return inputs;
package/market/index.json CHANGED
@@ -93,8 +93,8 @@
93
93
  },
94
94
  "versions": [
95
95
  {
96
- "version": "1.1.0",
97
- "publishTimestamp": 1792725959,
96
+ "version": "1.1.2",
97
+ "publishTimestamp": 1792731605,
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.1.0",
3
+ "version": "1.1.2",
4
4
  "private": false,
5
5
  "description": "DSH 插件:在右侧栏预览里对科研配图、SVG、PDF、HTML 直接圈画批注,标注写进侧车文件并可一键交给智能体改图源",
6
6
  "license": "MIT",