dsh-figmark 1.0.1 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +637 -208
- package/lib/client.pdf.js +22 -16
- package/lib/index.js +20 -0
- package/market/index.json +2 -2
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -25,6 +25,9 @@ window.__ModuleLoader__.load({
|
|
|
25
25
|
redo: "重做",
|
|
26
26
|
clear: "清空",
|
|
27
27
|
detail: "这个元素的详细说明",
|
|
28
|
+
addComment: "添加评论…",
|
|
29
|
+
pinSize: "标记大小",
|
|
30
|
+
weight: "线条粗细",
|
|
28
31
|
detailPlaceholder: "这一处具体要改什么(例如「这个标号应指向滑套 34」)",
|
|
29
32
|
summary: "总体目标",
|
|
30
33
|
summaryPlaceholder: "可选:这张图整体要往哪个方向改(智能体会连同下面逐条说明一起读)",
|
|
@@ -110,6 +113,9 @@ window.__ModuleLoader__.load({
|
|
|
110
113
|
redo: "Redo",
|
|
111
114
|
clear: "Clear",
|
|
112
115
|
detail: "What this element needs",
|
|
116
|
+
addComment: "Add a comment…",
|
|
117
|
+
pinSize: "Marker size",
|
|
118
|
+
weight: "Line weight",
|
|
113
119
|
detailPlaceholder: "What should change here (for example \"this numeral should point at the sleeve 34\")",
|
|
114
120
|
summary: "Overall goal",
|
|
115
121
|
summaryPlaceholder: "Optional: what this figure should become overall (the agent reads it with the marks below)",
|
|
@@ -253,6 +259,26 @@ window.__ModuleLoader__.load({
|
|
|
253
259
|
}
|
|
254
260
|
//#endregion
|
|
255
261
|
//#region src/shared/annotation.ts
|
|
262
|
+
/** Thinnest stroke a mark can be drawn at, as a multiple of the default width. */
|
|
263
|
+
const WEIGHT_MIN = .5;
|
|
264
|
+
/**
|
|
265
|
+
* Hold a stroke weight inside the range the painter can draw.
|
|
266
|
+
*
|
|
267
|
+
* @param value - the weight to hold.
|
|
268
|
+
* @returns the value, pulled to the nearest end of the range.
|
|
269
|
+
*/
|
|
270
|
+
function clampWeight(value) {
|
|
271
|
+
return Math.min(4, Math.max(WEIGHT_MIN, value));
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Whether a value is a stroke weight this plugin would write.
|
|
275
|
+
*
|
|
276
|
+
* @param value - the value to test.
|
|
277
|
+
* @returns true when it is one.
|
|
278
|
+
*/
|
|
279
|
+
function isWeight(value) {
|
|
280
|
+
return typeof value === "number" && Number.isFinite(value) && value >= .5 && value <= 4;
|
|
281
|
+
}
|
|
256
282
|
/**
|
|
257
283
|
* Suffix naming one page's files inside a paged document's name.
|
|
258
284
|
* @param page - 1-based page, or undefined for a single-surface figure.
|
|
@@ -989,13 +1015,46 @@ window.__ModuleLoader__.load({
|
|
|
989
1015
|
}
|
|
990
1016
|
//#endregion
|
|
991
1017
|
//#region src/client/render.ts
|
|
992
|
-
/** Stroke width of
|
|
1018
|
+
/** Stroke width of a mark drawn at weight 1 on a reference-sized figure, in pixels. */
|
|
993
1019
|
const STROKE_WIDTH = 2.5;
|
|
1020
|
+
/**
|
|
1021
|
+
* The stroke width one mark is drawn at.
|
|
1022
|
+
*
|
|
1023
|
+
* A mark's weight is a multiple of {@link STROKE_WIDTH}, so the default is the
|
|
1024
|
+
* width marks have always had and a heavier mark is the same drawing with a
|
|
1025
|
+
* thicker pen. The arrow's barbs are weighed by the same multiple, because an
|
|
1026
|
+
* arrow drawn with a thick pen has a thick head.
|
|
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
|
+
*
|
|
1031
|
+
* @param mark - the mark to weigh.
|
|
1032
|
+
* @param unit - the figure's size multiple, 1 for a reference-sized figure.
|
|
1033
|
+
* @returns its stroke width in figure pixels.
|
|
1034
|
+
*/
|
|
1035
|
+
function strokeWidthOf(mark, unit = 1) {
|
|
1036
|
+
return STROKE_WIDTH * (mark.weight ?? 1) * unit;
|
|
1037
|
+
}
|
|
994
1038
|
/** Arrow head length, in figure pixels. */
|
|
995
1039
|
const ARROW_HEAD = 14;
|
|
996
1040
|
/** Half-angle of the arrow head, in radians. */
|
|
997
1041
|
const ARROW_SPREAD = Math.PI / 7;
|
|
998
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
|
+
/**
|
|
999
1058
|
* How far a pin's head sits above its coordinate, as a multiple of its half-width.
|
|
1000
1059
|
*
|
|
1001
1060
|
* A map pin puts its *tip* on the coordinate it names, so the head stands clear of
|
|
@@ -1073,12 +1132,13 @@ window.__ModuleLoader__.load({
|
|
|
1073
1132
|
* @param points - the arrow's tail and tip.
|
|
1074
1133
|
* @returns tail, tip and both barbs; the stored points alone when it is half drawn.
|
|
1075
1134
|
*/
|
|
1076
|
-
function arrowBarbs(points) {
|
|
1135
|
+
function arrowBarbs(points, weight = 1, unit = 1) {
|
|
1077
1136
|
const [first, second] = points;
|
|
1078
1137
|
if (first === void 0 || second === void 0) return points;
|
|
1079
1138
|
const angle = Math.atan2(second[1] - first[1], second[0] - first[0]);
|
|
1080
|
-
const
|
|
1081
|
-
const
|
|
1139
|
+
const head = ARROW_HEAD * weight * unit;
|
|
1140
|
+
const left = [second[0] - head * Math.cos(angle - ARROW_SPREAD), second[1] - head * Math.sin(angle - ARROW_SPREAD)];
|
|
1141
|
+
const right = [second[0] - head * Math.cos(angle + ARROW_SPREAD), second[1] - head * Math.sin(angle + ARROW_SPREAD)];
|
|
1082
1142
|
return [
|
|
1083
1143
|
...points,
|
|
1084
1144
|
left,
|
|
@@ -1096,7 +1156,7 @@ window.__ModuleLoader__.load({
|
|
|
1096
1156
|
switch (mark.kind) {
|
|
1097
1157
|
case "arrow": {
|
|
1098
1158
|
if (second === void 0) return void 0;
|
|
1099
|
-
const [left, right] = arrowBarbs(mark.points).slice(2);
|
|
1159
|
+
const [left, right] = arrowBarbs(mark.points, mark.weight ?? 1).slice(2);
|
|
1100
1160
|
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
1161
|
}
|
|
1102
1162
|
case "rect": {
|
|
@@ -1131,10 +1191,10 @@ window.__ModuleLoader__.load({
|
|
|
1131
1191
|
* @param mark - the point mark.
|
|
1132
1192
|
* @returns the marker's path, number centre and box, or undefined with no coordinate.
|
|
1133
1193
|
*/
|
|
1134
|
-
function pinGeometry(mark) {
|
|
1194
|
+
function pinGeometry(mark, unit = 1) {
|
|
1135
1195
|
const tip = mark.points[0];
|
|
1136
1196
|
if (tip === void 0) return void 0;
|
|
1137
|
-
const radius =
|
|
1197
|
+
const radius = pinRadiusOf(mark, unit);
|
|
1138
1198
|
const lift = radius * PIN_LIFT;
|
|
1139
1199
|
const [tipX, tipY] = tip;
|
|
1140
1200
|
const up = tipY - lift - radius >= 0;
|
|
@@ -1168,16 +1228,16 @@ window.__ModuleLoader__.load({
|
|
|
1168
1228
|
* @param mark - the point mark.
|
|
1169
1229
|
* @returns path data, or undefined when the mark carries no coordinate.
|
|
1170
1230
|
*/
|
|
1171
|
-
function pinPathData(mark) {
|
|
1172
|
-
return pinGeometry(mark)?.path;
|
|
1231
|
+
function pinPathData(mark, unit = 1) {
|
|
1232
|
+
return pinGeometry(mark, unit)?.path;
|
|
1173
1233
|
}
|
|
1174
1234
|
/**
|
|
1175
1235
|
* Where the number inside a `point` mark's marker sits.
|
|
1176
1236
|
* @param mark - the point mark.
|
|
1177
1237
|
* @returns the number's centre, or undefined when the mark carries no coordinate.
|
|
1178
1238
|
*/
|
|
1179
|
-
function pinNumberCentre(mark) {
|
|
1180
|
-
return pinGeometry(mark)?.number;
|
|
1239
|
+
function pinNumberCentre(mark, unit = 1) {
|
|
1240
|
+
return pinGeometry(mark, unit)?.number;
|
|
1181
1241
|
}
|
|
1182
1242
|
/**
|
|
1183
1243
|
* The box a `point` mark's pin covers.
|
|
@@ -1190,8 +1250,8 @@ window.__ModuleLoader__.load({
|
|
|
1190
1250
|
* @param mark - the point mark.
|
|
1191
1251
|
* @returns the pin's box, or undefined when the mark carries no point.
|
|
1192
1252
|
*/
|
|
1193
|
-
function pointMarkBox(mark) {
|
|
1194
|
-
return pinGeometry(mark)?.box;
|
|
1253
|
+
function pointMarkBox(mark, unit = 1) {
|
|
1254
|
+
return pinGeometry(mark, unit)?.box;
|
|
1195
1255
|
}
|
|
1196
1256
|
/**
|
|
1197
1257
|
* The box a mark covers once it is drawn, rotation included.
|
|
@@ -1204,14 +1264,14 @@ window.__ModuleLoader__.load({
|
|
|
1204
1264
|
* @param mark - the mark to measure.
|
|
1205
1265
|
* @returns the box, or undefined when the mark carries no coordinate.
|
|
1206
1266
|
*/
|
|
1207
|
-
function markOutline(mark) {
|
|
1208
|
-
if (mark.kind === "point") return pointMarkBox(mark);
|
|
1209
|
-
const geometry = mark.kind === "arrow" ? arrowBarbs(mark.points) : mark.points;
|
|
1267
|
+
function markOutline(mark, unit = 1) {
|
|
1268
|
+
if (mark.kind === "point") return pointMarkBox(mark, unit);
|
|
1269
|
+
const geometry = mark.kind === "arrow" ? arrowBarbs(mark.points, mark.weight ?? 1, unit) : mark.points;
|
|
1210
1270
|
const straight = pointsBox(geometry);
|
|
1211
1271
|
if (straight === void 0) return void 0;
|
|
1212
1272
|
const radians = markRadians(mark);
|
|
1213
1273
|
const box = radians === 0 ? straight : turnedBox(geometry, straight, radians);
|
|
1214
|
-
const grow =
|
|
1274
|
+
const grow = strokeWidthOf(mark, unit) / 2;
|
|
1215
1275
|
return {
|
|
1216
1276
|
x: box.x - grow,
|
|
1217
1277
|
y: box.y - grow,
|
|
@@ -1285,6 +1345,42 @@ window.__ModuleLoader__.load({
|
|
|
1285
1345
|
return `rotate(${n(radians * 180 / Math.PI)} ${n(cx)} ${n(cy)})`;
|
|
1286
1346
|
}
|
|
1287
1347
|
//#endregion
|
|
1348
|
+
//#region src/client/scale.ts
|
|
1349
|
+
/**
|
|
1350
|
+
* How big the marks should be for the figure they are drawn on.
|
|
1351
|
+
*
|
|
1352
|
+
* A stroke is not a physical thing: 2.5 pixels is a bold line on a 300px thumbnail
|
|
1353
|
+
* and a hairline on a 4000px scan. Marks are comparisons the reader makes against
|
|
1354
|
+
* the line work underneath them, so they have to be sized in the same currency as
|
|
1355
|
+
* the figure — a fraction of it — rather than in the figure's pixels.
|
|
1356
|
+
*
|
|
1357
|
+
* The reference edge is the size a science figure is usually exported at. An
|
|
1358
|
+
* ordinary figure then gets the unit it always had, and only the outliers (a
|
|
1359
|
+
* thumbnail, a poster) are scaled. The range is clamped rather than proportional
|
|
1360
|
+
* because a 20000px figure would otherwise get strokes 40px wide, which reads as a
|
|
1361
|
+
* highlighter rather than a line.
|
|
1362
|
+
* @module dsh-figmark/client/scale
|
|
1363
|
+
*/
|
|
1364
|
+
/** The figure edge, in pixels, that gets the unit it always had. */
|
|
1365
|
+
const REFERENCE_EDGE = 1200;
|
|
1366
|
+
/** Smallest multiple of the base sizes, for figures far below the reference. */
|
|
1367
|
+
const MIN_UNIT = .75;
|
|
1368
|
+
/**
|
|
1369
|
+
* The size everything drawn on a figure is multiplied by.
|
|
1370
|
+
*
|
|
1371
|
+
* Measured against the figure's longer edge: that is the dimension a reader judges
|
|
1372
|
+
* a stroke against, and using the area would shrink a tall thin panel's marks for a
|
|
1373
|
+
* reason the eye does not see.
|
|
1374
|
+
* @param width - the figure's intrinsic width, in figure pixels.
|
|
1375
|
+
* @param height - the figure's intrinsic height, in figure pixels.
|
|
1376
|
+
* @returns the multiple of the base sizes, held inside {@link MIN_UNIT} and {@link MAX_UNIT}.
|
|
1377
|
+
*/
|
|
1378
|
+
function figureUnit(width, height) {
|
|
1379
|
+
const edge = Math.max(width, height);
|
|
1380
|
+
if (!Number.isFinite(edge) || edge <= 0) return 1;
|
|
1381
|
+
return Math.min(3, Math.max(MIN_UNIT, edge / REFERENCE_EDGE));
|
|
1382
|
+
}
|
|
1383
|
+
//#endregion
|
|
1288
1384
|
//#region src/client/Canvas.tsx
|
|
1289
1385
|
/**
|
|
1290
1386
|
* The drawing surface.
|
|
@@ -1295,6 +1391,19 @@ window.__ModuleLoader__.load({
|
|
|
1295
1391
|
* means the same thing at any zoom or pane width.
|
|
1296
1392
|
* @module dsh-figmark/client/Canvas
|
|
1297
1393
|
*/
|
|
1394
|
+
/**
|
|
1395
|
+
* The `weight` field of a new mark, or nothing when it is the default.
|
|
1396
|
+
*
|
|
1397
|
+
* Absent means the default throughout this plugin, so writing `weight: 1` would put
|
|
1398
|
+
* a field into every mark that says nothing — sidecars would grow a key for every
|
|
1399
|
+
* mark ever drawn, and every comparison of two documents would see a difference
|
|
1400
|
+
* where there is none.
|
|
1401
|
+
* @param weight - the weight the next stroke is drawn at.
|
|
1402
|
+
* @returns the field, or an empty object for the default.
|
|
1403
|
+
*/
|
|
1404
|
+
function weightField(weight) {
|
|
1405
|
+
return weight === 1 ? {} : { weight };
|
|
1406
|
+
}
|
|
1298
1407
|
/** Hit width of an invisible selection stroke, in figure pixels. */
|
|
1299
1408
|
const HIT_WIDTH = 14;
|
|
1300
1409
|
/** Minimum distance between recorded freehand samples, in figure pixels. */
|
|
@@ -1318,6 +1427,8 @@ window.__ModuleLoader__.load({
|
|
|
1318
1427
|
*/
|
|
1319
1428
|
function Canvas(props) {
|
|
1320
1429
|
const { width, height, marks, tool, color, containerRef, anchoring } = props;
|
|
1430
|
+
const [originX, originY] = props.origin ?? [0, 0];
|
|
1431
|
+
const weight = props.weight ?? 1;
|
|
1321
1432
|
/**
|
|
1322
1433
|
* Each mark's number, by id.
|
|
1323
1434
|
*
|
|
@@ -1326,6 +1437,14 @@ window.__ModuleLoader__.load({
|
|
|
1326
1437
|
* otherwise.
|
|
1327
1438
|
*/
|
|
1328
1439
|
const numbers = new Map(marks.map((mark, index) => [mark.id, props.numbering?.get(mark.id) ?? index + 1]));
|
|
1440
|
+
/**
|
|
1441
|
+
* How big everything is drawn here, as a multiple of the base sizes.
|
|
1442
|
+
*
|
|
1443
|
+
* Derived from the surface rather than passed in: every mark on this canvas shares
|
|
1444
|
+
* one figure, so one number decides the pen for all of them, and a caller that
|
|
1445
|
+
* forgot to thread it through could not make a mark disagree with its hit area.
|
|
1446
|
+
*/
|
|
1447
|
+
const unit = figureUnit(width, height);
|
|
1329
1448
|
const [draft, setDraft] = (0, react.useState)(null);
|
|
1330
1449
|
const [press, setPress] = (0, react.useState)(null);
|
|
1331
1450
|
const drawing = (0, react.useRef)(false);
|
|
@@ -1370,7 +1489,7 @@ window.__ModuleLoader__.load({
|
|
|
1370
1489
|
const rect = event.currentTarget.getBoundingClientRect();
|
|
1371
1490
|
const scaleX = rect.width === 0 ? 1 : width / rect.width;
|
|
1372
1491
|
const scaleY = rect.height === 0 ? 1 : height / rect.height;
|
|
1373
|
-
return [(event.clientX - rect.left) * scaleX, (event.clientY - rect.top) * scaleY];
|
|
1492
|
+
return [(event.clientX - rect.left) * scaleX - originX, (event.clientY - rect.top) * scaleY - originY];
|
|
1374
1493
|
};
|
|
1375
1494
|
/**
|
|
1376
1495
|
* Describe what the mark landed on.
|
|
@@ -1552,6 +1671,7 @@ window.__ModuleLoader__.load({
|
|
|
1552
1671
|
kind: "point",
|
|
1553
1672
|
color,
|
|
1554
1673
|
points: [point],
|
|
1674
|
+
...weightField(weight),
|
|
1555
1675
|
...anchor === void 0 ? {} : { anchor }
|
|
1556
1676
|
};
|
|
1557
1677
|
props.onAdd(mark);
|
|
@@ -1567,7 +1687,8 @@ window.__ModuleLoader__.load({
|
|
|
1567
1687
|
id: nextMarkId(),
|
|
1568
1688
|
kind: tool,
|
|
1569
1689
|
color,
|
|
1570
|
-
points: seed
|
|
1690
|
+
points: seed,
|
|
1691
|
+
...weightField(weight)
|
|
1571
1692
|
});
|
|
1572
1693
|
};
|
|
1573
1694
|
const move = (event) => {
|
|
@@ -1593,10 +1714,18 @@ window.__ModuleLoader__.load({
|
|
|
1593
1714
|
...press,
|
|
1594
1715
|
snap: event.shiftKey
|
|
1595
1716
|
};
|
|
1717
|
+
const edit = editFor(held, point, dx, dy);
|
|
1596
1718
|
setPress({
|
|
1597
1719
|
...held,
|
|
1598
|
-
edit
|
|
1720
|
+
edit
|
|
1599
1721
|
});
|
|
1722
|
+
const base = props.marks.find((candidate) => candidate.id === press.id);
|
|
1723
|
+
/* v8 ignore next -- a press always begins on a mark that is still in the list. */
|
|
1724
|
+
if (base === void 0) return;
|
|
1725
|
+
const live = { ...base };
|
|
1726
|
+
if (edit.points !== void 0) live.points = edit.points;
|
|
1727
|
+
if (edit.rotation !== void 0) live.rotation = edit.rotation;
|
|
1728
|
+
props.onLiveEdit?.(live);
|
|
1600
1729
|
return;
|
|
1601
1730
|
}
|
|
1602
1731
|
if (!drawing.current || draft === null) return;
|
|
@@ -1624,6 +1753,7 @@ window.__ModuleLoader__.load({
|
|
|
1624
1753
|
if (press !== null) {
|
|
1625
1754
|
const done = press.edit;
|
|
1626
1755
|
setPress(null);
|
|
1756
|
+
props.onLiveEdit?.(null);
|
|
1627
1757
|
if (done === null) return;
|
|
1628
1758
|
if (press.handle === void 0) {
|
|
1629
1759
|
/* v8 ignore next -- a move always carries the geometry it moved to. */
|
|
@@ -1713,8 +1843,8 @@ window.__ModuleLoader__.load({
|
|
|
1713
1843
|
* compared to a washed-out sticker.
|
|
1714
1844
|
*/
|
|
1715
1845
|
const renderPoint = (mark) => {
|
|
1716
|
-
const pin = pinPathData(mark);
|
|
1717
|
-
const centre = pinNumberCentre(mark);
|
|
1846
|
+
const pin = pinPathData(mark, unit);
|
|
1847
|
+
const centre = pinNumberCentre(mark, unit);
|
|
1718
1848
|
/* v8 ignore next -- a point mark carries exactly the one coordinate it was placed at. */
|
|
1719
1849
|
if (pin === void 0 || centre === void 0) return null;
|
|
1720
1850
|
const number = numbers.get(mark.id);
|
|
@@ -1723,21 +1853,21 @@ window.__ModuleLoader__.load({
|
|
|
1723
1853
|
d: pin,
|
|
1724
1854
|
fill: "#ffffff",
|
|
1725
1855
|
stroke: "#ffffff",
|
|
1726
|
-
strokeWidth: 4.5,
|
|
1856
|
+
strokeWidth: 4.5 * unit,
|
|
1727
1857
|
strokeLinejoin: "round"
|
|
1728
1858
|
}),
|
|
1729
1859
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1730
1860
|
d: pin,
|
|
1731
1861
|
fill: mark.color,
|
|
1732
1862
|
stroke: "#ffffff",
|
|
1733
|
-
strokeWidth: 1.5,
|
|
1863
|
+
strokeWidth: 1.5 * unit,
|
|
1734
1864
|
strokeLinejoin: "round"
|
|
1735
1865
|
}),
|
|
1736
1866
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
|
|
1737
1867
|
x: centre[0],
|
|
1738
1868
|
y: centre[1],
|
|
1739
1869
|
fontFamily: PIN_FONT_FAMILY,
|
|
1740
|
-
fontSize: 11,
|
|
1870
|
+
fontSize: 11 * unit,
|
|
1741
1871
|
fontWeight: 700,
|
|
1742
1872
|
fill: PIN_NUMBER_COLOR,
|
|
1743
1873
|
textAnchor: "middle",
|
|
@@ -1755,19 +1885,19 @@ window.__ModuleLoader__.load({
|
|
|
1755
1885
|
* handles that would do nothing.
|
|
1756
1886
|
*/
|
|
1757
1887
|
const renderPointFrame = (shown) => {
|
|
1758
|
-
const box = pointMarkBox(shown);
|
|
1888
|
+
const box = pointMarkBox(shown, unit);
|
|
1759
1889
|
/* v8 ignore next -- a point mark carries exactly the one coordinate it was placed at. */
|
|
1760
1890
|
if (box === void 0) return null;
|
|
1761
1891
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
1762
|
-
x: box.x - 3,
|
|
1763
|
-
y: box.y - 3,
|
|
1764
|
-
width: box.width + 6,
|
|
1765
|
-
height: box.height + 6,
|
|
1766
|
-
rx: 3,
|
|
1892
|
+
x: box.x - 3 * unit,
|
|
1893
|
+
y: box.y - 3 * unit,
|
|
1894
|
+
width: box.width + 6 * unit,
|
|
1895
|
+
height: box.height + 6 * unit,
|
|
1896
|
+
rx: 3 * unit,
|
|
1767
1897
|
fill: "none",
|
|
1768
1898
|
stroke: "#4c8dff",
|
|
1769
|
-
strokeWidth:
|
|
1770
|
-
strokeDasharray:
|
|
1899
|
+
strokeWidth: unit,
|
|
1900
|
+
strokeDasharray: `${3 * unit} ${2 * unit}`,
|
|
1771
1901
|
style: { pointerEvents: "none" }
|
|
1772
1902
|
});
|
|
1773
1903
|
};
|
|
@@ -1777,7 +1907,7 @@ window.__ModuleLoader__.load({
|
|
|
1777
1907
|
const selected = props.selectedId === mark.id;
|
|
1778
1908
|
const transform = markTransform(shown);
|
|
1779
1909
|
const isPoint = mark.kind === "point";
|
|
1780
|
-
const pin = isPoint ? pinPathData(shown) : void 0;
|
|
1910
|
+
const pin = isPoint ? pinPathData(shown, unit) : void 0;
|
|
1781
1911
|
/**
|
|
1782
1912
|
* What a press on the drawn shape starts.
|
|
1783
1913
|
*
|
|
@@ -1789,7 +1919,7 @@ window.__ModuleLoader__.load({
|
|
|
1789
1919
|
d: pin,
|
|
1790
1920
|
fill: "transparent",
|
|
1791
1921
|
stroke: "transparent",
|
|
1792
|
-
strokeWidth: HIT_WIDTH,
|
|
1922
|
+
strokeWidth: HIT_WIDTH * unit,
|
|
1793
1923
|
style: {
|
|
1794
1924
|
pointerEvents: "all",
|
|
1795
1925
|
cursor: "pointer"
|
|
@@ -1814,7 +1944,7 @@ window.__ModuleLoader__.load({
|
|
|
1814
1944
|
d: path,
|
|
1815
1945
|
fill: "none",
|
|
1816
1946
|
stroke: "#ffffff",
|
|
1817
|
-
strokeWidth: 5,
|
|
1947
|
+
strokeWidth: strokeWidthOf(shown, unit) + 2.5 * unit,
|
|
1818
1948
|
strokeLinecap: "round",
|
|
1819
1949
|
strokeLinejoin: "round"
|
|
1820
1950
|
}),
|
|
@@ -1822,7 +1952,7 @@ window.__ModuleLoader__.load({
|
|
|
1822
1952
|
d: path,
|
|
1823
1953
|
fill: "none",
|
|
1824
1954
|
stroke: shown.color,
|
|
1825
|
-
strokeWidth:
|
|
1955
|
+
strokeWidth: strokeWidthOf(shown, unit),
|
|
1826
1956
|
strokeLinecap: "round",
|
|
1827
1957
|
strokeLinejoin: "round"
|
|
1828
1958
|
}),
|
|
@@ -1830,7 +1960,7 @@ window.__ModuleLoader__.load({
|
|
|
1830
1960
|
d: path,
|
|
1831
1961
|
fill: "none",
|
|
1832
1962
|
stroke: "transparent",
|
|
1833
|
-
strokeWidth: HIT_WIDTH,
|
|
1963
|
+
strokeWidth: HIT_WIDTH * unit,
|
|
1834
1964
|
style: {
|
|
1835
1965
|
pointerEvents: "stroke",
|
|
1836
1966
|
cursor: "pointer"
|
|
@@ -1851,9 +1981,13 @@ window.__ModuleLoader__.load({
|
|
|
1851
1981
|
]
|
|
1852
1982
|
}, mark.id);
|
|
1853
1983
|
};
|
|
1854
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.
|
|
1984
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1855
1985
|
className: "da-overlay",
|
|
1856
1986
|
"data-da-overlay": "",
|
|
1987
|
+
style: {
|
|
1988
|
+
left: originX,
|
|
1989
|
+
top: originY
|
|
1990
|
+
},
|
|
1857
1991
|
width,
|
|
1858
1992
|
height,
|
|
1859
1993
|
viewBox: `0 0 ${width} ${height}`,
|
|
@@ -1875,7 +2009,10 @@ window.__ModuleLoader__.load({
|
|
|
1875
2009
|
}
|
|
1876
2010
|
props.onSelect(null);
|
|
1877
2011
|
},
|
|
1878
|
-
children:
|
|
2012
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", {
|
|
2013
|
+
transform: `translate(${originX} ${originY})`,
|
|
2014
|
+
children: [marks.map((mark) => renderMark(mark, false)), draft === null ? null : renderMark(draft, true)]
|
|
2015
|
+
})
|
|
1879
2016
|
});
|
|
1880
2017
|
}
|
|
1881
2018
|
/** A difference that never changes, for a body that has no edit yet. */
|
|
@@ -2437,6 +2574,44 @@ window.__ModuleLoader__.load({
|
|
|
2437
2574
|
const existing = shell.snapshot.draft.trim();
|
|
2438
2575
|
shell.setDraft(existing === "" ? message : `${existing}\n\n${message}`);
|
|
2439
2576
|
}
|
|
2577
|
+
/**
|
|
2578
|
+
* The box the whole drawing occupies: the figure, plus wherever the marks reach.
|
|
2579
|
+
*
|
|
2580
|
+
* A figure with every mark inside it gets exactly its own box back, which is the
|
|
2581
|
+
* case the overlay has always drawn; the growth only happens for marks that leave
|
|
2582
|
+
* it. The margin is applied only on sides that actually grew, so an ordinary figure
|
|
2583
|
+
* is not padded on all four sides for nothing.
|
|
2584
|
+
* @param width - the figure's width, in figure pixels.
|
|
2585
|
+
* @param height - the figure's height, in figure pixels.
|
|
2586
|
+
* @param marks - the marks drawn on it.
|
|
2587
|
+
* @param unit - the figure's size multiple, so outlines are measured as drawn.
|
|
2588
|
+
* @returns the box the drawing occupies, in figure coordinates.
|
|
2589
|
+
*/
|
|
2590
|
+
function plotBounds(width, height, marks, unit = 1) {
|
|
2591
|
+
let left = 0;
|
|
2592
|
+
let top = 0;
|
|
2593
|
+
let right = width;
|
|
2594
|
+
let bottom = height;
|
|
2595
|
+
for (const mark of marks) {
|
|
2596
|
+
const box = markOutline(mark, unit);
|
|
2597
|
+
/* v8 ignore next -- a mark with no coordinate covers nothing to grow for. */
|
|
2598
|
+
if (box === void 0) continue;
|
|
2599
|
+
left = Math.min(left, box.x);
|
|
2600
|
+
top = Math.min(top, box.y);
|
|
2601
|
+
right = Math.max(right, box.x + box.width);
|
|
2602
|
+
bottom = Math.max(bottom, box.y + box.height);
|
|
2603
|
+
}
|
|
2604
|
+
const padLeft = left < 0 ? 12 : 0;
|
|
2605
|
+
const padTop = top < 0 ? 12 : 0;
|
|
2606
|
+
const padRight = right > width ? 12 : 0;
|
|
2607
|
+
const padBottom = bottom > height ? 12 : 0;
|
|
2608
|
+
return {
|
|
2609
|
+
x: left - padLeft,
|
|
2610
|
+
y: top - padTop,
|
|
2611
|
+
width: right + padRight - (left - padLeft),
|
|
2612
|
+
height: bottom + padBottom - (top - padTop)
|
|
2613
|
+
};
|
|
2614
|
+
}
|
|
2440
2615
|
//#endregion
|
|
2441
2616
|
//#region src/client/export.ts
|
|
2442
2617
|
/**
|
|
@@ -2444,17 +2619,19 @@ window.__ModuleLoader__.load({
|
|
|
2444
2619
|
*
|
|
2445
2620
|
* @param mark - the mark to draw.
|
|
2446
2621
|
* @param number - the mark's number in the list, when it carries one.
|
|
2622
|
+
* @param unit - the figure's size multiple, so the export is drawn at the same
|
|
2623
|
+
* weight the reader saw.
|
|
2447
2624
|
* @returns the mark's elements.
|
|
2448
2625
|
*/
|
|
2449
|
-
function markSvg(mark, number) {
|
|
2626
|
+
function markSvg(mark, number, unit) {
|
|
2450
2627
|
const path = markPathData(mark);
|
|
2451
2628
|
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="${
|
|
2453
|
-
const pin = mark.kind === "point" ? pinPathData(mark) : void 0;
|
|
2454
|
-
const centre = mark.kind === "point" ? pinNumberCentre(mark) : void 0;
|
|
2629
|
+
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"/>`);
|
|
2630
|
+
const pin = mark.kind === "point" ? pinPathData(mark, unit) : void 0;
|
|
2631
|
+
const centre = mark.kind === "point" ? pinNumberCentre(mark, unit) : void 0;
|
|
2455
2632
|
if (pin !== void 0 && centre !== void 0) {
|
|
2456
|
-
parts.push(`<path d="${pin}" fill="${mark.color}" stroke="#ffffff" stroke-width="2" stroke-linejoin="round"/>`);
|
|
2457
|
-
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>`);
|
|
2633
|
+
parts.push(`<path d="${pin}" fill="${mark.color}" stroke="#ffffff" stroke-width="${2 * unit}" stroke-linejoin="round"/>`);
|
|
2634
|
+
if (number !== void 0) parts.push(`<text x="${centre[0]}" y="${centre[1]}" font-family="${PIN_FONT_FAMILY}" font-size="${11 * unit}" font-weight="700" fill="${PIN_NUMBER_COLOR}" text-anchor="middle" dominant-baseline="central">${number}</text>`);
|
|
2458
2635
|
}
|
|
2459
2636
|
return parts.join("");
|
|
2460
2637
|
}
|
|
@@ -2490,7 +2667,27 @@ window.__ModuleLoader__.load({
|
|
|
2490
2667
|
*/
|
|
2491
2668
|
function composeReviewSvg(layer, marks, numbering) {
|
|
2492
2669
|
const numbers = new Map(marks.map((mark, index) => [mark.id, numbering?.get(mark.id) ?? index + 1]));
|
|
2493
|
-
|
|
2670
|
+
const unit = figureUnit(layer.width, layer.height);
|
|
2671
|
+
const paper = plotBounds(layer.width, layer.height, marks, unit);
|
|
2672
|
+
const shift = `translate(${-paper.x} ${-paper.y})`;
|
|
2673
|
+
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>";
|
|
2674
|
+
}
|
|
2675
|
+
/**
|
|
2676
|
+
* The size of the picture {@link composeReviewSvg} builds.
|
|
2677
|
+
*
|
|
2678
|
+
* A caller has to know it before rasterizing: an SVG rendered at the figure's size
|
|
2679
|
+
* would crop away exactly the marks that made the paper grow.
|
|
2680
|
+
* @param layer - the figure to place underneath.
|
|
2681
|
+
* @param marks - marks in draw order.
|
|
2682
|
+
* @param unit - the figure's size multiple.
|
|
2683
|
+
* @returns the paper's width and height, in figure pixels.
|
|
2684
|
+
*/
|
|
2685
|
+
function reviewSize(layer, marks, unit = figureUnit(layer.width, layer.height)) {
|
|
2686
|
+
const paper = plotBounds(layer.width, layer.height, marks, unit);
|
|
2687
|
+
return {
|
|
2688
|
+
width: paper.width,
|
|
2689
|
+
height: paper.height
|
|
2690
|
+
};
|
|
2494
2691
|
}
|
|
2495
2692
|
/**
|
|
2496
2693
|
* Rasterization scale for one review image.
|
|
@@ -2787,6 +2984,22 @@ window.__ModuleLoader__.load({
|
|
|
2787
2984
|
}
|
|
2788
2985
|
//#endregion
|
|
2789
2986
|
//#region src/client/preferences.ts
|
|
2987
|
+
/**
|
|
2988
|
+
* The drawing choices a reader expects to still be there after a reload.
|
|
2989
|
+
*
|
|
2990
|
+
* Marks and their sentences belong to the annotation and live in its sidecar. The
|
|
2991
|
+
* colour the reader is drawing in, and how large the numbers are, are a different
|
|
2992
|
+
* kind of thing: they describe how the *next* mark should look, not what any mark
|
|
2993
|
+
* already means. Keeping them in a sidecar would tie them to a figure that has one —
|
|
2994
|
+
* a reader who picks a colour, draws nothing, and reloads would find the colour gone,
|
|
2995
|
+
* because a figure with no annotation has no sidecar to remember it in.
|
|
2996
|
+
*
|
|
2997
|
+
* So they go in browser storage, which is scoped to the reader rather than to the
|
|
2998
|
+
* figure, and which exists before the first mark does. An annotation that records
|
|
2999
|
+
* its own number style still wins over this: the picture the agent reads has to match
|
|
3000
|
+
* the annotation, not the reader's current preference.
|
|
3001
|
+
* @module dsh-figmark/client/preferences
|
|
3002
|
+
*/
|
|
2790
3003
|
/** The storage key. Namespaced so it cannot collide with the shell's own keys. */
|
|
2791
3004
|
const PREFERENCES_KEY = "dsh-figmark:drawing";
|
|
2792
3005
|
/** A colour this plugin would have written: six hex digits. */
|
|
@@ -2842,6 +3055,7 @@ window.__ModuleLoader__.load({
|
|
|
2842
3055
|
const bar = spotOf(record["bar"]);
|
|
2843
3056
|
return {
|
|
2844
3057
|
...typeof color === "string" && HEX_COLOR.test(color) ? { color } : {},
|
|
3058
|
+
...isWeight(record["weight"]) ? { weight: record["weight"] } : {},
|
|
2845
3059
|
...palette === void 0 ? {} : { palette },
|
|
2846
3060
|
...bar === void 0 ? {} : { bar }
|
|
2847
3061
|
};
|
|
@@ -2868,10 +3082,11 @@ window.__ModuleLoader__.load({
|
|
|
2868
3082
|
*/
|
|
2869
3083
|
function rememberPreferences(preferences, store = browserStore()) {
|
|
2870
3084
|
if (store === void 0) return;
|
|
2871
|
-
const { color, palette, bar } = preferences;
|
|
3085
|
+
const { color, weight, palette, bar } = preferences;
|
|
2872
3086
|
try {
|
|
2873
3087
|
store.setItem(PREFERENCES_KEY, JSON.stringify({
|
|
2874
3088
|
...color === void 0 ? {} : { color },
|
|
3089
|
+
...weight === void 0 ? {} : { weight: clampWeight(weight) },
|
|
2875
3090
|
...palette === void 0 ? {} : { palette: {
|
|
2876
3091
|
x: palette.x,
|
|
2877
3092
|
y: palette.y
|
|
@@ -3061,6 +3276,55 @@ window.__ModuleLoader__.load({
|
|
|
3061
3276
|
.da-palette .da-tool .da-tool-icon{height:auto}
|
|
3062
3277
|
.da-palette .da-tool .da-tool-icon svg{width:16px;height:16px;stroke-width:1.9}
|
|
3063
3278
|
|
|
3279
|
+
/* --- the size rail ------------------------------------------------------- */
|
|
3280
|
+
/* A vertical rail along the left edge of the figure: how heavy the pen is, and how
|
|
3281
|
+
big a point mark's pin is. Idle it is a hairline, so it costs the figure nothing;
|
|
3282
|
+
crossing it opens it out to a track. The track is *tapered* — a thin line at the
|
|
3283
|
+
bottom and a broad one at the top — because the control's whole subject is
|
|
3284
|
+
thickness, and a wedge says which way is heavier without reading a number.
|
|
3285
|
+
A horizontal range input is rotated upright inside the pill: it keeps dragging,
|
|
3286
|
+
arrow keys, the step and the accessible name, while the rail above does the
|
|
3287
|
+
drawing. The drawn parts never take the pointer, or grabbing the knob would grab
|
|
3288
|
+
the knob instead of the slider. */
|
|
3289
|
+
.da-weight{position:absolute;z-index:7;left:6px;top:50%;transform:translateY(-50%);
|
|
3290
|
+
display:flex;align-items:center;justify-content:center;width:7px;height:200px;
|
|
3291
|
+
border-radius:999px;background:var(--dsw-alias-border-l2,rgba(127,127,127,.22));
|
|
3292
|
+
cursor:ns-resize;transition:width .16s ease,background .16s ease,box-shadow .16s ease}
|
|
3293
|
+
.da-weight:hover,.da-weight:focus-within{width:30px;background:var(--dsw-alias-bg-layer-2,#fff);
|
|
3294
|
+
box-shadow:0 5px 18px rgba(20,40,80,.16),0 0 0 0.5px var(--dsw-alias-border-l2,currentColor)}
|
|
3295
|
+
/* The wedge: a hairline at the bottom, the heaviest width at the top. Drawn as a
|
|
3296
|
+
clipped gradient box so it costs one element and no SVG. */
|
|
3297
|
+
.da-weight::before{content:"";position:absolute;left:50%;top:14px;bottom:14px;width:11px;
|
|
3298
|
+
transform:translateX(-50%);opacity:0;
|
|
3299
|
+
background:linear-gradient(to top,var(--dsw-alias-border-l3,rgba(127,127,127,.34)),
|
|
3300
|
+
var(--dsw-alias-border-l4,rgba(127,127,127,.5)));
|
|
3301
|
+
clip-path:polygon(calc(50% - 0.5px) 100%,calc(50% + 0.5px) 100%,
|
|
3302
|
+
calc(50% + 5.5px) 0,calc(50% - 5.5px) 0);
|
|
3303
|
+
transition:opacity .16s ease}
|
|
3304
|
+
.da-weight:hover::before,.da-weight:focus-within::before{opacity:1}
|
|
3305
|
+
/* The part already chosen, drawn from the bottom up in the brand colour. */
|
|
3306
|
+
.da-weight-fill{position:absolute;left:50%;bottom:14px;width:11px;transform:translateX(-50%);
|
|
3307
|
+
opacity:0;pointer-events:none;transition:opacity .16s ease;
|
|
3308
|
+
background:var(--dsw-alias-brand-primary,#176df2);
|
|
3309
|
+
clip-path:polygon(calc(50% - 0.5px) 100%,calc(50% + 0.5px) 100%,
|
|
3310
|
+
calc(50% + 5.5px) 0,calc(50% - 5.5px) 0)}
|
|
3311
|
+
.da-weight:hover .da-weight-fill,.da-weight:focus-within .da-weight-fill{opacity:1}
|
|
3312
|
+
/* The knob sits on the value: the only part that shows at rest, so the rail reads as
|
|
3313
|
+
a slider waiting to be touched rather than a bare rule. */
|
|
3314
|
+
.da-weight-knob{position:absolute;left:50%;width:11px;height:11px;border-radius:50%;
|
|
3315
|
+
transform:translateX(-50%);pointer-events:none;
|
|
3316
|
+
background:var(--dsw-alias-bg-layer-2,#fff);
|
|
3317
|
+
box-shadow:0 1px 4px rgba(20,40,80,.32),0 0 0 1px var(--dsw-alias-border-l3,currentColor);
|
|
3318
|
+
transition:width .16s ease,height .16s ease,background .16s ease}
|
|
3319
|
+
.da-weight:hover .da-weight-knob,.da-weight:focus-within .da-weight-knob{
|
|
3320
|
+
width:15px;height:15px;background:var(--dsw-alias-brand-primary,#176df2);
|
|
3321
|
+
box-shadow:0 2px 7px rgba(20,40,80,.34)}
|
|
3322
|
+
/* The browser's own range, rotated upright and made transparent. It spans the whole
|
|
3323
|
+
rail so a press anywhere on it lands on the slider — including on the knob, which
|
|
3324
|
+
is why the knob is pointer-transparent. */
|
|
3325
|
+
.da-weight-input{position:absolute;left:50%;top:50%;width:200px;height:30px;margin:0;opacity:0;
|
|
3326
|
+
transform:translate(-50%,-50%) rotate(-90deg);cursor:ns-resize;appearance:none;background:transparent}
|
|
3327
|
+
|
|
3064
3328
|
/* --- the floating bar ---------------------------------------------------- */
|
|
3065
3329
|
/* The same object as the tool palette: a dark rounded strip over the figure, which
|
|
3066
3330
|
the reader can drag clear of whatever they are looking at. It carries the records
|
|
@@ -3149,6 +3413,10 @@ window.__ModuleLoader__.load({
|
|
|
3149
3413
|
needs to see where the drawing stops. */
|
|
3150
3414
|
.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)}
|
|
3151
3415
|
.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))}
|
|
3416
|
+
/* The paper: the figure plus the margin a mark may have needed. It is the white sheet
|
|
3417
|
+
the drawing sits on, so a mark that reaches past the figure lands on paper rather
|
|
3418
|
+
than on the panel behind it. */
|
|
3419
|
+
.da-paper{background:#fff}
|
|
3152
3420
|
.da-figure{display:block;user-select:none;-webkit-user-drag:none}
|
|
3153
3421
|
/* The table is a thing you can push. The hand only shows under the tools that
|
|
3154
3422
|
leave a drag free to move the view: under a drawing tool the same drag draws. */
|
|
@@ -3159,7 +3427,7 @@ window.__ModuleLoader__.load({
|
|
|
3159
3427
|
/* The rendered document takes the figure's place; the overlay above it owns every
|
|
3160
3428
|
pointer event, which is also what keeps a link inside it from navigating. */
|
|
3161
3429
|
.da-html{display:block;border:0;background:#fff;pointer-events:none}
|
|
3162
|
-
.da-overlay{position:absolute;
|
|
3430
|
+
.da-overlay{position:absolute;touch-action:none}
|
|
3163
3431
|
.da-overlay[data-mode="view"]{pointer-events:none}
|
|
3164
3432
|
.da-overlay[data-tool="select"]{cursor:default}
|
|
3165
3433
|
.da-overlay:not([data-tool="select"]){cursor:crosshair}
|
|
@@ -3219,9 +3487,36 @@ window.__ModuleLoader__.load({
|
|
|
3219
3487
|
.da-float-record{width:min(330px,calc(100% - 22px))}
|
|
3220
3488
|
.da-float-list{max-height:min(58%,400px)}
|
|
3221
3489
|
.da-float-list .da-marks{overflow:auto;min-height:0}
|
|
3222
|
-
/* The detail
|
|
3223
|
-
come from the mark it belongs to.
|
|
3224
|
-
|
|
3490
|
+
/* The detail field is placed by arithmetic, not by the stylesheet: its left and top
|
|
3491
|
+
come from the mark it belongs to. It is a pill rather than a card — no head, no
|
|
3492
|
+
border between the field and the panel — so writing about a mark costs one line
|
|
3493
|
+
over the figure instead of a box that covers it. */
|
|
3494
|
+
.da-float-detail{width:300px;gap:0;padding:4px;border-radius:22px;
|
|
3495
|
+
flex-direction:row;align-items:flex-end;
|
|
3496
|
+
box-shadow:0 8px 26px rgba(20,40,80,.2),0 0 0 1px rgba(20,40,80,.04)}
|
|
3497
|
+
.da-float .da-comment{flex:1 1 auto;min-width:0;box-sizing:border-box;border:0;background:transparent;
|
|
3498
|
+
color:inherit;font:inherit;font-size:13px;line-height:1.55;padding:7px 10px;resize:none;
|
|
3499
|
+
/* One line until the sentence needs more, then as many as it takes: the pill is a
|
|
3500
|
+
place to type, and a sentence read through a slot two lines tall is a sentence
|
|
3501
|
+
the reader has to scroll to finish. The rule for the record cards' textareas sets
|
|
3502
|
+
a 74px floor, hence the explicit reset. The scrollbar is hidden because it is not
|
|
3503
|
+
a scroll view — wheel and keys still work once the sentence outgrows the pane. */
|
|
3504
|
+
min-height:0;height:auto;max-height:min(180px,42vh);overflow-y:auto;outline:none;
|
|
3505
|
+
field-sizing:content;scrollbar-width:none}
|
|
3506
|
+
.da-float .da-comment::-webkit-scrollbar{width:0;height:0}
|
|
3507
|
+
.da-float .da-comment::placeholder{color:var(--dsw-alias-label-tertiary,currentColor);opacity:.85}
|
|
3508
|
+
/* The hand-off appears only once the field says something: an empty pill is a
|
|
3509
|
+
place to type, not a decision to make. */
|
|
3510
|
+
.da-comment-send{display:flex;align-items:center;gap:4px;flex:0 0 auto;padding-bottom:3px;padding-right:3px}
|
|
3511
|
+
.da-comment-send-btn{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;
|
|
3512
|
+
border:0;border-radius:50%;background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));
|
|
3513
|
+
color:var(--dsw-alias-label-secondary,currentColor);cursor:pointer;padding:0}
|
|
3514
|
+
.da-comment-send-btn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.2));
|
|
3515
|
+
color:var(--dsw-alias-label-primary,currentColor)}
|
|
3516
|
+
.da-comment-send-btn:disabled{opacity:.45;cursor:default}
|
|
3517
|
+
.da-comment-send-btn svg{width:15px;height:15px}
|
|
3518
|
+
.da-comment-send-primary{background:var(--dsw-alias-brand-primary,#176df2);color:#fff}
|
|
3519
|
+
.da-comment-send-primary:hover{background:var(--dsw-alias-brand-primary,#176df2);color:#fff;filter:brightness(1.08)}
|
|
3225
3520
|
.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
3521
|
.da-float textarea:focus{outline:none;border-color:var(--dsw-alias-brand-primary,#176df2)}
|
|
3227
3522
|
.da-btn-count{display:inline-flex;align-items:center;justify-content:center;min-width:16px;height:16px;
|
|
@@ -3428,13 +3723,34 @@ window.__ModuleLoader__.load({
|
|
|
3428
3723
|
* @param size - the card's size, in CSS pixels.
|
|
3429
3724
|
* @returns the card's top-left corner, in the panel's coordinates.
|
|
3430
3725
|
*/
|
|
3726
|
+
/**
|
|
3727
|
+
* The size to place a card by: the card's own, or a stand-in before it is measured.
|
|
3728
|
+
*
|
|
3729
|
+
* A comment pill grows with the sentence in it, so a placement that assumed one line
|
|
3730
|
+
* would push a long comment off the bottom of the panel. The element is measured once
|
|
3731
|
+
* it has been laid out; before that — the first frame of a selection, and in any
|
|
3732
|
+
* environment that reports zero boxes — the caller's estimate is the only size there
|
|
3733
|
+
* is, and the next pass corrects it.
|
|
3734
|
+
* @param element - the laid-out card, when there is one.
|
|
3735
|
+
* @param fallback - the size to use when the card has not been measured.
|
|
3736
|
+
* @returns the width and height to place by.
|
|
3737
|
+
*/
|
|
3738
|
+
function measuredSize(element, fallback) {
|
|
3739
|
+
if (element === null || element.offsetHeight === 0) return fallback;
|
|
3740
|
+
return {
|
|
3741
|
+
width: element.offsetWidth,
|
|
3742
|
+
height: element.offsetHeight
|
|
3743
|
+
};
|
|
3744
|
+
}
|
|
3431
3745
|
function placeCard(anchor, panelWidth, panelHeight, size = {
|
|
3432
|
-
width:
|
|
3433
|
-
height:
|
|
3746
|
+
width: 300,
|
|
3747
|
+
height: 48
|
|
3434
3748
|
}) {
|
|
3435
3749
|
const below = anchor.y + anchor.height + 10;
|
|
3436
3750
|
const above = anchor.y - size.height - 10;
|
|
3437
|
-
const
|
|
3751
|
+
const roomBelow = panelHeight - 8 - below;
|
|
3752
|
+
const roomAbove = anchor.y - 10 - 8;
|
|
3753
|
+
const top = roomBelow >= size.height ? below : roomAbove >= size.height ? above : Math.max(8, roomAbove > roomBelow ? above : below);
|
|
3438
3754
|
const widest = Math.max(8, panelWidth - size.width - 8);
|
|
3439
3755
|
return {
|
|
3440
3756
|
left: Math.min(Math.max(8, anchor.x), widest),
|
|
@@ -3524,6 +3840,20 @@ window.__ModuleLoader__.load({
|
|
|
3524
3840
|
label: "toolPoint"
|
|
3525
3841
|
}
|
|
3526
3842
|
];
|
|
3843
|
+
/**
|
|
3844
|
+
* The tools that draw something with a size the rail can set.
|
|
3845
|
+
*
|
|
3846
|
+
* `select` is not among them: it draws nothing, so with it armed and no mark
|
|
3847
|
+
* selected the rail has no subject. `point` is, because a pin is sized by the same
|
|
3848
|
+
* number — the rail asks "how big is this mark" either way.
|
|
3849
|
+
*/
|
|
3850
|
+
const WEIGHT_TOOLS = /* @__PURE__ */ new Set([
|
|
3851
|
+
"arrow",
|
|
3852
|
+
"rect",
|
|
3853
|
+
"ellipse",
|
|
3854
|
+
"pen",
|
|
3855
|
+
"point"
|
|
3856
|
+
]);
|
|
3527
3857
|
/** The copy key naming the shape each kind of mark is, for the list's second line. */
|
|
3528
3858
|
const TOOL_COPY = {
|
|
3529
3859
|
arrow: "toolArrow",
|
|
@@ -3587,27 +3917,6 @@ window.__ModuleLoader__.load({
|
|
|
3587
3917
|
});
|
|
3588
3918
|
}
|
|
3589
3919
|
/**
|
|
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
3920
|
* The overall goal's mark: an arrow that has hit a target.
|
|
3612
3921
|
*
|
|
3613
3922
|
* The record this heads is not a note about one place on the figure — it is what the
|
|
@@ -3842,6 +4151,7 @@ window.__ModuleLoader__.load({
|
|
|
3842
4151
|
*/
|
|
3843
4152
|
const remembered = (0, react.useMemo)(() => readPreferences(), []);
|
|
3844
4153
|
const [color, setColor] = (0, react.useState)(remembered.color ?? COLORS[0]);
|
|
4154
|
+
const [weight, setWeight] = (0, react.useState)(remembered.weight ?? 1);
|
|
3845
4155
|
const [marks, setMarks] = (0, react.useState)(() => [...props.draft?.marks ?? []]);
|
|
3846
4156
|
const [past, setPast] = (0, react.useState)([]);
|
|
3847
4157
|
const [future, setFuture] = (0, react.useState)([]);
|
|
@@ -3892,6 +4202,14 @@ window.__ModuleLoader__.load({
|
|
|
3892
4202
|
*/
|
|
3893
4203
|
const [detailAt, setDetailAt] = (0, react.useState)(null);
|
|
3894
4204
|
/**
|
|
4205
|
+
* The selected mark as a drag is currently shaping it.
|
|
4206
|
+
*
|
|
4207
|
+
* Kept beside the stored marks rather than in them: the gesture is not committed
|
|
4208
|
+
* until it is released, so writing it into the list would make every frame an
|
|
4209
|
+
* undoable edit. It exists only so the pill can follow the hand.
|
|
4210
|
+
*/
|
|
4211
|
+
const [liveMark, setLiveMark] = (0, react.useState)(null);
|
|
4212
|
+
/**
|
|
3895
4213
|
* Shut the style panel when the reader picks up a tool.
|
|
3896
4214
|
*
|
|
3897
4215
|
* Arming a tool is the moment the panel has done its job; leaving it open over
|
|
@@ -3998,13 +4316,15 @@ window.__ModuleLoader__.load({
|
|
|
3998
4316
|
(0, react.useEffect)(() => {
|
|
3999
4317
|
rememberPreferences({
|
|
4000
4318
|
color,
|
|
4319
|
+
weight,
|
|
4001
4320
|
...palette === void 0 ? {} : { palette },
|
|
4002
4321
|
...barSpot === void 0 ? {} : { bar: barSpot }
|
|
4003
4322
|
});
|
|
4004
4323
|
}, [
|
|
4005
4324
|
barSpot,
|
|
4006
4325
|
color,
|
|
4007
|
-
palette
|
|
4326
|
+
palette,
|
|
4327
|
+
weight
|
|
4008
4328
|
]);
|
|
4009
4329
|
/**
|
|
4010
4330
|
* The bar's box, when the reader has moved it off the bottom edge.
|
|
@@ -4077,6 +4397,23 @@ window.__ModuleLoader__.load({
|
|
|
4077
4397
|
rasterSize
|
|
4078
4398
|
]);
|
|
4079
4399
|
/**
|
|
4400
|
+
* How big everything drawn is, as a multiple of the base sizes.
|
|
4401
|
+
*
|
|
4402
|
+
* One number for the whole surface: the pen, the pins and the review image all
|
|
4403
|
+
* have to agree, and a stroke is only bold or thin relative to the figure it sits
|
|
4404
|
+
* on. A 4000px scan and a 300px thumbnail get the same-looking mark this way.
|
|
4405
|
+
*/
|
|
4406
|
+
const unit = size === null ? 1 : figureUnit(size.width, size.height);
|
|
4407
|
+
/**
|
|
4408
|
+
* The paper the drawing needs: the figure, plus anywhere the marks reach past it.
|
|
4409
|
+
*
|
|
4410
|
+
* A reader boxing the margin a plot clips, or flagging a spot just outside the
|
|
4411
|
+
* axes, is drawing something the figure does not contain. The paper grows to hold
|
|
4412
|
+
* it rather than the mark being clipped — and because the surface is white, a mark
|
|
4413
|
+
* out there lands on paper instead of on nothing.
|
|
4414
|
+
*/
|
|
4415
|
+
const paper = size === null ? null : plotBounds(size.width, size.height, marks, unit);
|
|
4416
|
+
/**
|
|
4080
4417
|
* Everything a background write needs, read at write time.
|
|
4081
4418
|
*
|
|
4082
4419
|
* A write runs from a timer that outlives the render that scheduled it, so it has
|
|
@@ -4283,11 +4620,12 @@ window.__ModuleLoader__.load({
|
|
|
4283
4620
|
* exchanged; so is everything inside a record, which is where the writing happens.
|
|
4284
4621
|
*/
|
|
4285
4622
|
(0, react.useEffect)(() => {
|
|
4286
|
-
if (openPanel === null) return;
|
|
4287
4623
|
const onDown = (event) => {
|
|
4288
4624
|
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);
|
|
4625
|
+
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;
|
|
4626
|
+
if (openPanel !== null) setOpenPanel(null);
|
|
4627
|
+
if (stageRef.current?.contains(target) === true) return;
|
|
4628
|
+
setSelectedId(null);
|
|
4291
4629
|
};
|
|
4292
4630
|
document.addEventListener("pointerdown", onDown, true);
|
|
4293
4631
|
return () => {
|
|
@@ -4307,31 +4645,38 @@ window.__ModuleLoader__.load({
|
|
|
4307
4645
|
const root = rootRef.current;
|
|
4308
4646
|
const host = figureRef.current;
|
|
4309
4647
|
const mark = marks.find((candidate) => candidate.id === selectedId);
|
|
4310
|
-
const
|
|
4648
|
+
const drawn = liveMark !== null && liveMark.id === selectedId ? liveMark : mark;
|
|
4649
|
+
const box = drawn === void 0 ? void 0 : markOutline(drawn, unit);
|
|
4311
4650
|
if (root === null || host === null || box === void 0 || size === null) {
|
|
4312
4651
|
setDetailAt(null);
|
|
4313
4652
|
return;
|
|
4314
4653
|
}
|
|
4315
4654
|
const panel = root.getBoundingClientRect();
|
|
4316
|
-
const
|
|
4655
|
+
const measured = host.getBoundingClientRect();
|
|
4656
|
+
const pillSize = measuredSize(detailRef.current, {
|
|
4657
|
+
width: 300,
|
|
4658
|
+
height: 48
|
|
4659
|
+
});
|
|
4317
4660
|
const at = placeCard(scaledBox(box, {
|
|
4318
|
-
x:
|
|
4319
|
-
y:
|
|
4320
|
-
width:
|
|
4321
|
-
height:
|
|
4661
|
+
x: measured.left,
|
|
4662
|
+
y: measured.top,
|
|
4663
|
+
width: measured.width,
|
|
4664
|
+
height: measured.height
|
|
4322
4665
|
}, {
|
|
4323
4666
|
x: panel.left,
|
|
4324
4667
|
y: panel.top,
|
|
4325
4668
|
width: panel.width,
|
|
4326
4669
|
height: panel.height
|
|
4327
|
-
}, size.width), panel.width, panel.height);
|
|
4670
|
+
}, size.width), panel.width, panel.height, pillSize);
|
|
4328
4671
|
setDetailAt((previous) => previous !== null && previous.left === at.left && previous.top === at.top ? previous : at);
|
|
4329
4672
|
}, [
|
|
4673
|
+
htmlHeight,
|
|
4674
|
+
liveMark,
|
|
4330
4675
|
marks,
|
|
4331
|
-
selectedId,
|
|
4332
4676
|
scale,
|
|
4677
|
+
selectedId,
|
|
4333
4678
|
size,
|
|
4334
|
-
|
|
4679
|
+
unit
|
|
4335
4680
|
]);
|
|
4336
4681
|
/**
|
|
4337
4682
|
* The factor currently drawn, kept beside the state that will hold it.
|
|
@@ -4526,22 +4871,15 @@ window.__ModuleLoader__.load({
|
|
|
4526
4871
|
* so a recolour can be taken back like any other edit.
|
|
4527
4872
|
* @param value - the CSS hex string of the chosen swatch.
|
|
4528
4873
|
*/
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
if (!repainted.some((mark, index) => mark !== marks[index])) return;
|
|
4539
|
-
commit(repainted);
|
|
4540
|
-
}, [
|
|
4541
|
-
commit,
|
|
4542
|
-
marks,
|
|
4543
|
-
selectedId
|
|
4544
|
-
]);
|
|
4874
|
+
/**
|
|
4875
|
+
* Change one attribute of the selected mark, or of the next stroke when nothing
|
|
4876
|
+
* is selected.
|
|
4877
|
+
*
|
|
4878
|
+
* A control that is already at the mark's value leaves the list identical, so a
|
|
4879
|
+
* press that changes nothing records no undo step — whether the mark is still
|
|
4880
|
+
* there or was removed while it was selected.
|
|
4881
|
+
* @param patch - the attribute to change.
|
|
4882
|
+
*/
|
|
4545
4883
|
const undo = (0, react.useCallback)(() => {
|
|
4546
4884
|
setPast((history) => {
|
|
4547
4885
|
const previous = history[history.length - 1];
|
|
@@ -4656,10 +4994,11 @@ window.__ModuleLoader__.load({
|
|
|
4656
4994
|
]);
|
|
4657
4995
|
const exportPng = (0, react.useCallback)(async (target) => {
|
|
4658
4996
|
const svg = composeReviewSvg(target, marks);
|
|
4659
|
-
const
|
|
4660
|
-
|
|
4997
|
+
const paper = reviewSize(target, marks);
|
|
4998
|
+
const exportScale = Math.min(2, Math.max(1, 2400 / Math.max(paper.width, paper.height)));
|
|
4999
|
+
return await rasterizePng(svg, paper.width, paper.height, exportScale);
|
|
4661
5000
|
}, [marks]);
|
|
4662
|
-
const buildDocument = (0, react.useCallback)((target) => {
|
|
5001
|
+
const buildDocument = (0, react.useCallback)((target, scope = marks) => {
|
|
4663
5002
|
if (loaded === null) return null;
|
|
4664
5003
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4665
5004
|
return {
|
|
@@ -4674,7 +5013,7 @@ window.__ModuleLoader__.load({
|
|
|
4674
5013
|
},
|
|
4675
5014
|
createdAt: createdAt ?? now,
|
|
4676
5015
|
updatedAt: now,
|
|
4677
|
-
marks,
|
|
5016
|
+
marks: scope,
|
|
4678
5017
|
...summary.trim() === "" ? {} : { summary: summary.trim() }
|
|
4679
5018
|
};
|
|
4680
5019
|
}, [
|
|
@@ -4777,15 +5116,16 @@ window.__ModuleLoader__.load({
|
|
|
4777
5116
|
* would make the reader clear it twice.
|
|
4778
5117
|
* @param target - who receives the annotation.
|
|
4779
5118
|
*/
|
|
4780
|
-
const run = (0, react.useCallback)(async (target) => {
|
|
4781
|
-
|
|
5119
|
+
const run = (0, react.useCallback)(async (target, only) => {
|
|
5120
|
+
const scope = only === void 0 ? marks : [only];
|
|
5121
|
+
if (scope.length === 0 || size === null || size.height <= 0) return;
|
|
4782
5122
|
setBusy("sending");
|
|
4783
5123
|
setStatus({
|
|
4784
5124
|
tone: "info",
|
|
4785
5125
|
text: t(target === "agent" ? "sending" : "composing")
|
|
4786
5126
|
});
|
|
4787
5127
|
try {
|
|
4788
|
-
const document_ = buildDocument(size);
|
|
5128
|
+
const document_ = buildDocument(size, scope);
|
|
4789
5129
|
if (document_ === null) throw new Error(t("loadFailed"));
|
|
4790
5130
|
const review = layer === null || !needsReviewImage(document_) ? void 0 : await exportPng(layer);
|
|
4791
5131
|
const saved = await saveAnnotation(address, document_, review);
|
|
@@ -4846,7 +5186,7 @@ window.__ModuleLoader__.load({
|
|
|
4846
5186
|
discard,
|
|
4847
5187
|
exportPng,
|
|
4848
5188
|
layer,
|
|
4849
|
-
marks
|
|
5189
|
+
marks,
|
|
4850
5190
|
props.conversation,
|
|
4851
5191
|
props.localeId,
|
|
4852
5192
|
props.sessions,
|
|
@@ -4880,16 +5220,72 @@ window.__ModuleLoader__.load({
|
|
|
4880
5220
|
* the next stroke, which is where the remembered preference comes in.
|
|
4881
5221
|
*/
|
|
4882
5222
|
const shownColor = selected === null ? color : selected.color;
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
5223
|
+
/**
|
|
5224
|
+
* What the size rail currently means, or null when it means nothing.
|
|
5225
|
+
*
|
|
5226
|
+
* It sizes one thing: the stroke of the mark the reader is working on, or the pen
|
|
5227
|
+
* of the next stroke. With nothing selected and no drawing tool out there is no
|
|
5228
|
+
* such thing, so the rail stands down instead of offering a control for a decision
|
|
5229
|
+
* nobody is making. A point mark borrows the same number for its pin, so the rail
|
|
5230
|
+
* stays when one is selected — it is the mark's size, whichever way it is drawn.
|
|
5231
|
+
*/
|
|
5232
|
+
const shownWeight = selected === null ? WEIGHT_TOOLS.has(tool) ? weight : null : selected.weight ?? 1;
|
|
5233
|
+
/**
|
|
5234
|
+
* Change one attribute of the selected mark, or of the next stroke when nothing
|
|
5235
|
+
* is selected.
|
|
5236
|
+
*
|
|
5237
|
+
* A control already at the mark's value leaves the list untouched, so a press
|
|
5238
|
+
* that changes nothing records no undo step.
|
|
5239
|
+
* @param patch - the attribute to change.
|
|
5240
|
+
*/
|
|
5241
|
+
const repaintSelected = (mark, patch) => {
|
|
5242
|
+
if (Object.keys(patch).every((key) => mark[key] === patch[key])) return;
|
|
5243
|
+
commit(marks.map((entry) => entry.id === mark.id ? {
|
|
5244
|
+
...entry,
|
|
5245
|
+
...patch
|
|
5246
|
+
} : entry));
|
|
5247
|
+
};
|
|
5248
|
+
const chooseColor = (value) => {
|
|
5249
|
+
if (selected === null) {
|
|
5250
|
+
setColor(value);
|
|
5251
|
+
return;
|
|
5252
|
+
}
|
|
5253
|
+
repaintSelected(selected, { color: value });
|
|
4886
5254
|
};
|
|
4887
|
-
const
|
|
4888
|
-
|
|
4889
|
-
|
|
5255
|
+
const chooseWeight = (value) => {
|
|
5256
|
+
const held = clampWeight(value);
|
|
5257
|
+
if (selected === null) {
|
|
5258
|
+
setWeight(held);
|
|
5259
|
+
return;
|
|
5260
|
+
}
|
|
5261
|
+
repaintSelected(selected, { weight: held });
|
|
5262
|
+
};
|
|
5263
|
+
const surfaceStyle = paper === null ? {} : {
|
|
5264
|
+
width: paper.width * scale,
|
|
5265
|
+
height: paper.height * scale
|
|
5266
|
+
};
|
|
5267
|
+
/**
|
|
5268
|
+
* The paper at its unscaled size, with the figure placed at its own offset on it.
|
|
5269
|
+
*
|
|
5270
|
+
* The whole paper is scaled by one transform rather than each part scaling itself,
|
|
5271
|
+
* so the figure and the overlay cannot drift apart at a fractional factor — which
|
|
5272
|
+
* at 60% is exactly what a mark drawn on a line work and a mark drawn on paper
|
|
5273
|
+
* would do.
|
|
5274
|
+
*/
|
|
5275
|
+
const paperStyle = paper === null ? {} : {
|
|
5276
|
+
position: "relative",
|
|
5277
|
+
width: paper.width,
|
|
5278
|
+
height: paper.height,
|
|
4890
5279
|
transform: `scale(${scale})`,
|
|
4891
5280
|
transformOrigin: "top left"
|
|
4892
5281
|
};
|
|
5282
|
+
const figureStyle = paper === null ? {} : {
|
|
5283
|
+
position: "absolute",
|
|
5284
|
+
left: -paper.x,
|
|
5285
|
+
top: -paper.y,
|
|
5286
|
+
width: size?.width,
|
|
5287
|
+
height: size?.height
|
|
5288
|
+
};
|
|
4893
5289
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4894
5290
|
className: "da-root",
|
|
4895
5291
|
ref: rootRef,
|
|
@@ -4918,63 +5314,67 @@ window.__ModuleLoader__.load({
|
|
|
4918
5314
|
className: "da-surface",
|
|
4919
5315
|
ref: surfaceRef,
|
|
4920
5316
|
style: surfaceStyle,
|
|
4921
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.
|
|
4922
|
-
|
|
4923
|
-
style:
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
5317
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5318
|
+
className: "da-paper",
|
|
5319
|
+
style: paperStyle,
|
|
5320
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5321
|
+
ref: figureRef,
|
|
5322
|
+
style: figureStyle,
|
|
5323
|
+
children: [figure?.kind === "svg" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5324
|
+
ref: figureHostRef,
|
|
5325
|
+
dangerouslySetInnerHTML: { __html: figure.markup }
|
|
5326
|
+
}) : figure?.kind === "raster" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
5327
|
+
className: "da-figure",
|
|
5328
|
+
src: figure.objectUrl,
|
|
5329
|
+
width: size?.width,
|
|
5330
|
+
height: size?.height,
|
|
5331
|
+
alt: "",
|
|
5332
|
+
onLoad: (event) => {
|
|
5333
|
+
const image = event.currentTarget;
|
|
5334
|
+
setFileRasterSize({
|
|
5335
|
+
width: image.naturalWidth,
|
|
5336
|
+
height: image.naturalHeight
|
|
5337
|
+
});
|
|
5338
|
+
}
|
|
5339
|
+
}) : figure?.kind === "html" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("iframe", {
|
|
5340
|
+
ref: frameRef,
|
|
5341
|
+
className: "da-html",
|
|
5342
|
+
title: t("htmlFrame"),
|
|
5343
|
+
sandbox: "allow-same-origin",
|
|
5344
|
+
style: {
|
|
5345
|
+
width: figure.width,
|
|
5346
|
+
height: htmlHeight
|
|
5347
|
+
}
|
|
5348
|
+
}) : null, paper === null || size === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Canvas, {
|
|
5349
|
+
width: paper.width,
|
|
5350
|
+
height: paper.height,
|
|
5351
|
+
origin: [-paper.x, -paper.y],
|
|
5352
|
+
marks,
|
|
5353
|
+
tool,
|
|
5354
|
+
color,
|
|
5355
|
+
weight,
|
|
5356
|
+
containerRef: figureHostRef,
|
|
5357
|
+
anchoring: isVector,
|
|
5358
|
+
anchorAtFigurePoint: figure?.kind === "html" ? anchorAtFigurePoint : void 0,
|
|
5359
|
+
selectedId,
|
|
5360
|
+
onSelect: setSelectedId,
|
|
5361
|
+
onAdd: addMark,
|
|
5362
|
+
onMove: moveMark,
|
|
5363
|
+
onUpdate: updateMark,
|
|
5364
|
+
onRemove: removeMark,
|
|
5365
|
+
onPan: panBy,
|
|
5366
|
+
onPanStart: () => {
|
|
5367
|
+
setPanning(true);
|
|
5368
|
+
},
|
|
5369
|
+
onPanEnd: () => {
|
|
5370
|
+
setPanning(false);
|
|
5371
|
+
},
|
|
5372
|
+
onEdit: editMark,
|
|
5373
|
+
onDrawn: finishDrawing,
|
|
5374
|
+
onLiveEdit: setLiveMark,
|
|
5375
|
+
numbering: props.numbering
|
|
5376
|
+
})]
|
|
5377
|
+
})
|
|
4978
5378
|
})
|
|
4979
5379
|
})
|
|
4980
5380
|
}),
|
|
@@ -5122,42 +5522,67 @@ window.__ModuleLoader__.load({
|
|
|
5122
5522
|
left: detailAt.left,
|
|
5123
5523
|
top: detailAt.top
|
|
5124
5524
|
},
|
|
5125
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5525
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
5526
|
+
ref: detailRef,
|
|
5527
|
+
className: "da-comment",
|
|
5528
|
+
value: selected.detail ?? "",
|
|
5529
|
+
placeholder: selected.detail === void 0 || selected.detail.trim() === "" ? t("addComment") : t("detailPlaceholder"),
|
|
5530
|
+
rows: 1,
|
|
5531
|
+
onChange: (event) => {
|
|
5532
|
+
setMarkDetail(selected.id, event.target.value);
|
|
5533
|
+
}
|
|
5534
|
+
}), selected.detail === void 0 || selected.detail.trim() === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5535
|
+
className: "da-comment-send",
|
|
5536
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5537
|
+
type: "button",
|
|
5538
|
+
className: "da-comment-send-btn da-comment-send-primary",
|
|
5539
|
+
disabled: busy !== null,
|
|
5540
|
+
"aria-label": t("sendToAgent"),
|
|
5541
|
+
title: t("sendToAgent"),
|
|
5542
|
+
onClick: () => {
|
|
5543
|
+
run("agent", selected);
|
|
5544
|
+
},
|
|
5545
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SendIcon, {})
|
|
5137
5546
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5138
5547
|
type: "button",
|
|
5139
|
-
className: "da-
|
|
5140
|
-
|
|
5141
|
-
|
|
5548
|
+
className: "da-comment-send-btn",
|
|
5549
|
+
disabled: busy !== null,
|
|
5550
|
+
"aria-label": t("sendToComposer"),
|
|
5551
|
+
title: t("sendToComposer"),
|
|
5142
5552
|
onClick: () => {
|
|
5143
|
-
|
|
5553
|
+
run("composer", selected);
|
|
5144
5554
|
},
|
|
5145
|
-
children:
|
|
5555
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChatIcon, {})
|
|
5146
5556
|
})]
|
|
5147
|
-
})
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5557
|
+
})]
|
|
5558
|
+
}),
|
|
5559
|
+
shownWeight === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
5560
|
+
className: "da-weight",
|
|
5561
|
+
title: t(selected?.kind === "point" || tool === "point" ? "pinSize" : "weight"),
|
|
5562
|
+
children: [
|
|
5563
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
5564
|
+
type: "range",
|
|
5565
|
+
className: "da-weight-input",
|
|
5566
|
+
"aria-label": t(selected?.kind === "point" || tool === "point" ? "pinSize" : "weight"),
|
|
5567
|
+
min: WEIGHT_MIN,
|
|
5568
|
+
max: 4,
|
|
5569
|
+
step: .1,
|
|
5570
|
+
value: shownWeight,
|
|
5153
5571
|
onChange: (event) => {
|
|
5154
|
-
|
|
5572
|
+
chooseWeight(Number(event.target.value));
|
|
5155
5573
|
}
|
|
5156
|
-
}),
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5574
|
+
}),
|
|
5575
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5576
|
+
className: "da-weight-fill",
|
|
5577
|
+
"aria-hidden": true,
|
|
5578
|
+
style: { height: `${(shownWeight - WEIGHT_MIN) / (4 - WEIGHT_MIN) * 100}%` }
|
|
5579
|
+
}),
|
|
5580
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5581
|
+
className: "da-weight-knob",
|
|
5582
|
+
"aria-hidden": true,
|
|
5583
|
+
style: { bottom: `calc(${(shownWeight - WEIGHT_MIN) / (4 - WEIGHT_MIN) * 100}% - 5.5px)` }
|
|
5584
|
+
})
|
|
5585
|
+
]
|
|
5161
5586
|
}),
|
|
5162
5587
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5163
5588
|
className: "da-palette",
|
|
@@ -5494,15 +5919,18 @@ window.__ModuleLoader__.load({
|
|
|
5494
5919
|
const images = [];
|
|
5495
5920
|
const numbering = new Map(document.marks.map((mark, index) => [mark.id, index + 1]));
|
|
5496
5921
|
for (const page of pages) {
|
|
5497
|
-
const
|
|
5922
|
+
const layer = {
|
|
5498
5923
|
kind: "raster",
|
|
5499
5924
|
dataUrl: page.dataUrl,
|
|
5500
5925
|
width: page.width,
|
|
5501
5926
|
height: page.height
|
|
5502
|
-
}
|
|
5927
|
+
};
|
|
5928
|
+
const marks = document.marks.filter((mark) => mark.page === page.page);
|
|
5929
|
+
const svg = composeReviewSvg(layer, marks, numbering);
|
|
5930
|
+
const paper = reviewSize(layer, marks);
|
|
5503
5931
|
images.push({
|
|
5504
5932
|
page: page.page,
|
|
5505
|
-
image: await rasterizePng(svg,
|
|
5933
|
+
image: await rasterizePng(svg, paper.width, paper.height, reviewScale(paper.width, paper.height))
|
|
5506
5934
|
});
|
|
5507
5935
|
}
|
|
5508
5936
|
return images;
|
|
@@ -6028,6 +6456,7 @@ window.__ModuleLoader__.load({
|
|
|
6028
6456
|
createAutosave,
|
|
6029
6457
|
annotationSignature,
|
|
6030
6458
|
autosaveDelayMs: 600,
|
|
6459
|
+
figureUnit,
|
|
6031
6460
|
seat: createDocumentSeat({
|
|
6032
6461
|
sessions: ctx.get("sessions"),
|
|
6033
6462
|
conversation: ctx.get("conversation"),
|