@altnovatrail/canvas 0.0.0-stage → 0.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.
Files changed (75) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +59 -2
  3. package/lib/core/boxes.d.ts +54 -0
  4. package/lib/core/boxes.js +116 -0
  5. package/lib/core/clock.d.ts +25 -0
  6. package/lib/core/clock.js +39 -0
  7. package/lib/core/crop.d.ts +87 -0
  8. package/lib/core/crop.js +227 -0
  9. package/lib/core/eraser.d.ts +42 -0
  10. package/lib/core/eraser.js +127 -0
  11. package/lib/core/example.d.ts +66 -0
  12. package/lib/core/example.js +116 -0
  13. package/lib/core/fields.d.ts +29 -0
  14. package/lib/core/fields.js +78 -0
  15. package/lib/core/geometry.d.ts +38 -0
  16. package/lib/core/geometry.js +107 -0
  17. package/lib/core/grid.d.ts +30 -0
  18. package/lib/core/grid.js +47 -0
  19. package/lib/core/groups.d.ts +55 -0
  20. package/lib/core/groups.js +172 -0
  21. package/lib/core/hands.d.ts +120 -0
  22. package/lib/core/hands.js +273 -0
  23. package/lib/core/history.d.ts +42 -0
  24. package/lib/core/history.js +228 -0
  25. package/lib/core/index.d.ts +12 -0
  26. package/lib/core/index.js +15 -0
  27. package/lib/core/ink.d.ts +1 -0
  28. package/lib/core/ink.js +5 -0
  29. package/lib/core/manifest.d.ts +22 -0
  30. package/lib/core/manifest.js +50 -0
  31. package/lib/core/marks.d.ts +145 -0
  32. package/lib/core/marks.js +454 -0
  33. package/lib/core/outline.d.ts +22 -0
  34. package/lib/core/outline.js +50 -0
  35. package/lib/core/page.d.ts +62 -0
  36. package/lib/core/page.js +309 -0
  37. package/lib/core/png.d.ts +13 -0
  38. package/lib/core/png.js +231 -0
  39. package/lib/core/raster.d.ts +14 -0
  40. package/lib/core/raster.js +72 -0
  41. package/lib/core/recorder.d.ts +73 -0
  42. package/lib/core/recorder.js +127 -0
  43. package/lib/core/screen.d.ts +35 -0
  44. package/lib/core/screen.js +36 -0
  45. package/lib/core/sets.d.ts +10 -0
  46. package/lib/core/sets.js +31 -0
  47. package/lib/core/sheet.d.ts +9 -0
  48. package/lib/core/sheet.js +6 -0
  49. package/lib/core/submission.d.ts +41 -0
  50. package/lib/core/submission.js +63 -0
  51. package/lib/core/task.d.ts +214 -0
  52. package/lib/core/task.js +343 -0
  53. package/lib/core/widths.d.ts +57 -0
  54. package/lib/core/widths.js +218 -0
  55. package/lib/core/work.d.ts +77 -0
  56. package/lib/core/work.js +293 -0
  57. package/lib/element/attributes.d.ts +11 -0
  58. package/lib/element/attributes.js +22 -0
  59. package/lib/element/index.d.ts +219 -0
  60. package/lib/element/index.js +1898 -0
  61. package/lib/element/labels.d.ts +41 -0
  62. package/lib/element/labels.js +74 -0
  63. package/lib/element/parts.d.ts +63 -0
  64. package/lib/element/parts.js +191 -0
  65. package/lib/element/pictures.d.ts +45 -0
  66. package/lib/element/pictures.js +167 -0
  67. package/lib/element/probe.d.ts +33 -0
  68. package/lib/element/probe.js +18 -0
  69. package/lib/element/session.d.ts +3 -0
  70. package/lib/element/session.js +7 -0
  71. package/lib/element/statement.d.ts +35 -0
  72. package/lib/element/statement.js +116 -0
  73. package/lib/element/tools.d.ts +64 -0
  74. package/lib/element/tools.js +254 -0
  75. package/package.json +41 -4
@@ -0,0 +1,72 @@
1
+ import { tenths } from "./geometry.js";
2
+ import { encodePNG } from "./png.js";
3
+ /**
4
+ * The pixels of a crop (R25): grey, a byte each, row by row from the top, white 255 and the ink 0,
5
+ * of the strokes the crop was made of. Every segment of a stroke is a line as wide all along, with
6
+ * round ends, and a stroke of one point a dot as wide as the line: a pixel takes the share of it
7
+ * that the line covers, along a ramp a pixel wide at the line's edge, and the darkest of what
8
+ * covers it. The places are the crop's own whole numbers, from the points in tenths of a unit, as
9
+ * a sample keeps them (R09). Only adding, subtracting, multiplying, dividing and Math.sqrt go into
10
+ * a pixel, which ECMAScript rounds alike in every engine, so the bytes are the same in each.
11
+ */
12
+ export function rasterOf(strokes, crop) {
13
+ const { width, height, drawn } = crop;
14
+ const [top, left] = crop.margins;
15
+ const { box, median: unit } = crop.exact;
16
+ const gray = new Uint8Array(width * height).fill(255);
17
+ const reach = Math.max(drawn, 12) / 24;
18
+ // A whole number over the median: one division, as the crop's own numbers are.
19
+ const spot = ({ x, y }) => ({
20
+ x: ((tenths(x) - box.minX) * drawn + left * unit) / unit,
21
+ y: ((tenths(y) - box.minY) * drawn + top * unit) / unit,
22
+ });
23
+ for (const { points } of strokes) {
24
+ const spots = points.map(spot);
25
+ const [first] = spots;
26
+ if (spots.length === 1 && first !== undefined)
27
+ paint(gray, width, height, first, first, reach);
28
+ for (let k = 1; k < spots.length; k++) {
29
+ const [a, b] = [spots[k - 1], spots[k]];
30
+ if (a !== undefined && b !== undefined)
31
+ paint(gray, width, height, a, b, reach);
32
+ }
33
+ }
34
+ return gray;
35
+ }
36
+ /** The PNG of a crop: its pixels, encoded (R25). */
37
+ export function pictureOf(strokes, crop) {
38
+ return encodePNG(rasterOf(strokes, crop), crop.width, crop.height);
39
+ }
40
+ /**
41
+ * One segment's ink on the pixels near it. Its ends go in one order whichever way it was written,
42
+ * so a stroke drawn back to front gives the same bytes.
43
+ */
44
+ function paint(gray, width, height, from, to, reach) {
45
+ const forward = from.x < to.x || (from.x === to.x && from.y <= to.y);
46
+ const a = forward ? from : to;
47
+ const b = forward ? to : from;
48
+ const dx = b.x - a.x;
49
+ const dy = b.y - a.y;
50
+ const length = dx * dx + dy * dy;
51
+ const firstColumn = Math.max(0, Math.floor(Math.min(a.x, b.x) - reach - 1));
52
+ const lastColumn = Math.min(width - 1, Math.ceil(Math.max(a.x, b.x) + reach + 1));
53
+ const firstRow = Math.max(0, Math.floor(Math.min(a.y, b.y) - reach - 1));
54
+ const lastRow = Math.min(height - 1, Math.ceil(Math.max(a.y, b.y) + reach + 1));
55
+ for (let row = firstRow; row <= lastRow; row++) {
56
+ const cy = row + 0.5;
57
+ for (let column = firstColumn; column <= lastColumn; column++) {
58
+ const cx = column + 0.5;
59
+ // The point of the segment nearest the pixel's centre, as a share of the way from a to b.
60
+ const along = length === 0 ? 0 : Math.min(1, Math.max(0, ((cx - a.x) * dx + (cy - a.y) * dy) / length));
61
+ const ex = cx - (a.x + along * dx);
62
+ const ey = cy - (a.y + along * dy);
63
+ const cover = reach + 0.5 - Math.sqrt(ex * ex + ey * ey);
64
+ if (cover <= 0)
65
+ continue;
66
+ const value = Math.round(255 * (1 - Math.min(1, cover)));
67
+ const at = row * width + column;
68
+ if (value < (gray[at] ?? 255))
69
+ gray[at] = value;
70
+ }
71
+ }
72
+ }
@@ -0,0 +1,73 @@
1
+ import { Clock } from "./clock.js";
2
+ import type { Stroke } from "./ink.js";
3
+ /**
4
+ * What the recorder takes of a pointer event, and of each event coalesced into it. The DOM-free
5
+ * part sees no DOM (R12), so the element hands it this much, the place already in sheet units.
6
+ */
7
+ export interface PointerSample {
8
+ /** The event's `pointerId`. */
9
+ readonly pointerId: number;
10
+ /** The event's `pointerType`, as it came. */
11
+ readonly kind: string;
12
+ /** The event's `button`, the button whose state changed: 0 is the primary, -1 none. */
13
+ readonly button: number;
14
+ /** The event's `buttons`, the buttons held: bit 1 is the primary. */
15
+ readonly buttons: number;
16
+ /** Where, in sheet units, clamped to the sheet. */
17
+ readonly x: number;
18
+ readonly y: number;
19
+ /** When: the event's `timeStamp`, in milliseconds. */
20
+ readonly time: number;
21
+ /** The event's `pressure`, 0 to 1. */
22
+ readonly pressure: number;
23
+ }
24
+ /**
25
+ * How close two recorded points of a stroke may come, in sheet units: 0.1, the precision of a
26
+ * sample file (R09). A point nearer than that to the last one recorded adds nothing the file would
27
+ * keep (R18).
28
+ */
29
+ export declare const MIN_STEP = 0.1;
30
+ /** `button` of the primary button: a pen's tip, a finger, a mouse's left button. */
31
+ export declare const PRIMARY = 0;
32
+ /** The primary button's bit in `buttons`. */
33
+ export declare const PRIMARY_HELD = 1;
34
+ /**
35
+ * The strokes of one sheet, from the pointer's samples (R18; architecture 3.1 in
36
+ * docs/architecture.md). One pointer writes at a time, with its primary button: a second pointer,
37
+ * and any other button, write nothing while a stroke is under way. A finished stroke goes to the
38
+ * handler the recorder is made with — onto the work on the sheet (R19). Its points take their time
39
+ * from the sheet's clock, which the sheet's other actions keep too (R22).
40
+ */
41
+ export declare class Recorder {
42
+ #private;
43
+ constructor(finished: (stroke: Stroke) => void, clock?: Clock);
44
+ /** The stroke being written, or nothing between strokes. */
45
+ get live(): Stroke | undefined;
46
+ /** The `pointerId` of the pointer writing, or nothing between strokes. */
47
+ get pointer(): number | undefined;
48
+ /** `pointerdown`: a stroke begins here, if none is under way and the primary button went down. */
49
+ down(sample: PointerSample): boolean;
50
+ /**
51
+ * `pointermove`, once for each coalesced event: the stroke goes on here. A move without the
52
+ * primary button held ends the stroke where it was, its `pointerup` lost.
53
+ */
54
+ move(sample: PointerSample): boolean;
55
+ /**
56
+ * `pointerup`: the stroke ends where and when the pointer left, however near the last point,
57
+ * so that the stroke keeps a pause before it. Its pressure is the last move's: `pointerup`'s
58
+ * is 0 by definition, the buttons no longer pressed, rather than measured.
59
+ */
60
+ up(sample: PointerSample): boolean;
61
+ /** `pointercancel`: the device took the touch for an accident, and the stroke goes (R05). */
62
+ cancel(pointerId: number): boolean;
63
+ /**
64
+ * The stroke ends where the pointer last was, its `pointerup` never to come: the pointer's
65
+ * capture was lost, or the sheet left the page. Any pointer's stroke, unless one is named.
66
+ */
67
+ end(pointerId?: number): boolean;
68
+ /**
69
+ * The sheet has taken another history of writing (R27): the stroke under way goes, as on
70
+ * `pointercancel` — its ID and its times are the last sheet's — and the next stroke is `nextId`.
71
+ */
72
+ restart(nextId: number): void;
73
+ }
@@ -0,0 +1,127 @@
1
+ import { Clock } from "./clock.js";
2
+ /**
3
+ * How close two recorded points of a stroke may come, in sheet units: 0.1, the precision of a
4
+ * sample file (R09). A point nearer than that to the last one recorded adds nothing the file would
5
+ * keep (R18).
6
+ */
7
+ export const MIN_STEP = 0.1;
8
+ /** `button` of the primary button: a pen's tip, a finger, a mouse's left button. */
9
+ export const PRIMARY = 0;
10
+ /** The primary button's bit in `buttons`. */
11
+ export const PRIMARY_HELD = 1;
12
+ /**
13
+ * The strokes of one sheet, from the pointer's samples (R18; architecture 3.1 in
14
+ * docs/architecture.md). One pointer writes at a time, with its primary button: a second pointer,
15
+ * and any other button, write nothing while a stroke is under way. A finished stroke goes to the
16
+ * handler the recorder is made with — onto the work on the sheet (R19). Its points take their time
17
+ * from the sheet's clock, which the sheet's other actions keep too (R22).
18
+ */
19
+ export class Recorder {
20
+ #finished;
21
+ #clock;
22
+ #live;
23
+ #nextId = 1;
24
+ constructor(finished, clock = new Clock()) {
25
+ this.#finished = finished;
26
+ this.#clock = clock;
27
+ }
28
+ /** The stroke being written, or nothing between strokes. */
29
+ get live() {
30
+ return this.#live;
31
+ }
32
+ /** The `pointerId` of the pointer writing, or nothing between strokes. */
33
+ get pointer() {
34
+ return this.#live?.pointerId;
35
+ }
36
+ /** `pointerdown`: a stroke begins here, if none is under way and the primary button went down. */
37
+ down(sample) {
38
+ if (this.#live !== undefined || sample.button !== PRIMARY)
39
+ return false;
40
+ const point = this.#point(sample, sample.pressure);
41
+ this.#live = {
42
+ id: this.#nextId++,
43
+ kind: sample.kind,
44
+ points: [point],
45
+ pointerId: sample.pointerId,
46
+ latest: point,
47
+ };
48
+ return true;
49
+ }
50
+ /**
51
+ * `pointermove`, once for each coalesced event: the stroke goes on here. A move without the
52
+ * primary button held ends the stroke where it was, its `pointerup` lost.
53
+ */
54
+ move(sample) {
55
+ const live = this.#live;
56
+ if (live === undefined || sample.pointerId !== live.pointerId)
57
+ return false;
58
+ if ((sample.buttons & PRIMARY_HELD) === 0)
59
+ return this.#finish();
60
+ const point = this.#point(sample, sample.pressure);
61
+ live.latest = point;
62
+ const last = live.points[live.points.length - 1];
63
+ if (last !== undefined && Math.hypot(point.x - last.x, point.y - last.y) < MIN_STEP) {
64
+ return false;
65
+ }
66
+ live.points.push(point);
67
+ return true;
68
+ }
69
+ /**
70
+ * `pointerup`: the stroke ends where and when the pointer left, however near the last point,
71
+ * so that the stroke keeps a pause before it. Its pressure is the last move's: `pointerup`'s
72
+ * is 0 by definition, the buttons no longer pressed, rather than measured.
73
+ */
74
+ up(sample) {
75
+ const live = this.#live;
76
+ if (live === undefined || sample.pointerId !== live.pointerId)
77
+ return false;
78
+ live.latest = this.#point(sample, live.latest.p);
79
+ return this.#finish();
80
+ }
81
+ /** `pointercancel`: the device took the touch for an accident, and the stroke goes (R05). */
82
+ cancel(pointerId) {
83
+ if (this.#live?.pointerId !== pointerId)
84
+ return false;
85
+ this.#live = undefined;
86
+ return true;
87
+ }
88
+ /**
89
+ * The stroke ends where the pointer last was, its `pointerup` never to come: the pointer's
90
+ * capture was lost, or the sheet left the page. Any pointer's stroke, unless one is named.
91
+ */
92
+ end(pointerId) {
93
+ if (this.#live === undefined)
94
+ return false;
95
+ if (pointerId !== undefined && this.#live.pointerId !== pointerId)
96
+ return false;
97
+ return this.#finish();
98
+ }
99
+ /**
100
+ * The sheet has taken another history of writing (R27): the stroke under way goes, as on
101
+ * `pointercancel` — its ID and its times are the last sheet's — and the next stroke is `nextId`.
102
+ */
103
+ restart(nextId) {
104
+ this.#live = undefined;
105
+ this.#nextId = nextId;
106
+ }
107
+ #finish() {
108
+ const live = this.#live;
109
+ if (live === undefined)
110
+ return false;
111
+ if (live.latest !== live.points[live.points.length - 1])
112
+ live.points.push(live.latest);
113
+ // The stroke is no longer under way by the time the handler has it.
114
+ this.#live = undefined;
115
+ this.#finished({ id: live.id, kind: live.kind, points: live.points });
116
+ return true;
117
+ }
118
+ /**
119
+ * A point of the sample, its time the sheet's and never going back. Its pressure is kept to 0–1,
120
+ * the range Pointer Events gives it: an event a script makes may say more, and the sheet would
121
+ * write a history it does not take back (R27).
122
+ */
123
+ #point(sample, pressure) {
124
+ const p = Math.min(Math.max(pressure, 0), 1);
125
+ return { x: sample.x, y: sample.y, t: this.#clock.at(sample.time), p };
126
+ }
127
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Where the sheet is on the screen, one view for every layer (R06; architecture 3.2 in
3
+ * docs/architecture.md): `k` CSS pixels to a logical unit, for the sheet fits the width of its
4
+ * area, and `vy`, the top of the sheet's visible part, in units.
5
+ */
6
+ export interface View {
7
+ readonly k: number;
8
+ readonly vy: number;
9
+ }
10
+ /** A point, on the sheet in logical units or on the screen in CSS pixels. */
11
+ export interface Point {
12
+ readonly x: number;
13
+ readonly y: number;
14
+ }
15
+ /**
16
+ * The most CSS pixels to a unit: on a wide screen the sheet is at most 1000 CSS pixels wide, a
17
+ * pixel to a unit as on the picture of the work, and stands in the middle (R23).
18
+ */
19
+ export declare const K_MAX = 1;
20
+ /** `k` of a sheet `width` CSS pixels wide as it is on the screen: k = A / W. */
21
+ export declare function scaleFor(width: number): number;
22
+ /**
23
+ * `k` for a sheet laid out in an area `width` CSS pixels wide: it fits the width, up to `K_MAX`.
24
+ * This is the layout's `k`, before any transform the page puts on the element; input takes `k`
25
+ * from the sheet's rectangle on the screen, by `scaleFor`.
26
+ */
27
+ export declare function fitScale(width: number): number;
28
+ /**
29
+ * The point of the sheet under a point of the screen, given in CSS pixels from the top left corner
30
+ * of the sheet's visible part. A point beyond the sheet is clamped to its edge, as in spec 4.2 of
31
+ * canvas-api: a pointer captured by the sheet goes on writing on its edge.
32
+ */
33
+ export declare function toSheet(view: View, x: number, y: number): Point;
34
+ /** The point of the screen over a point of the sheet, from the top left corner of its visible part. */
35
+ export declare function toScreen(view: View, x: number, y: number): Point;
@@ -0,0 +1,36 @@
1
+ import { SHEET } from "./sheet.js";
2
+ /**
3
+ * The most CSS pixels to a unit: on a wide screen the sheet is at most 1000 CSS pixels wide, a
4
+ * pixel to a unit as on the picture of the work, and stands in the middle (R23).
5
+ */
6
+ export const K_MAX = 1;
7
+ /** `k` of a sheet `width` CSS pixels wide as it is on the screen: k = A / W. */
8
+ export function scaleFor(width) {
9
+ return width / SHEET.width;
10
+ }
11
+ /**
12
+ * `k` for a sheet laid out in an area `width` CSS pixels wide: it fits the width, up to `K_MAX`.
13
+ * This is the layout's `k`, before any transform the page puts on the element; input takes `k`
14
+ * from the sheet's rectangle on the screen, by `scaleFor`.
15
+ */
16
+ export function fitScale(width) {
17
+ return Math.min(width / SHEET.width, K_MAX);
18
+ }
19
+ /**
20
+ * The point of the sheet under a point of the screen, given in CSS pixels from the top left corner
21
+ * of the sheet's visible part. A point beyond the sheet is clamped to its edge, as in spec 4.2 of
22
+ * canvas-api: a pointer captured by the sheet goes on writing on its edge.
23
+ */
24
+ export function toSheet(view, x, y) {
25
+ return {
26
+ x: clamp(x / view.k, 0, SHEET.width),
27
+ y: clamp(y / view.k + view.vy, 0, SHEET.height),
28
+ };
29
+ }
30
+ /** The point of the screen over a point of the sheet, from the top left corner of its visible part. */
31
+ export function toScreen(view, x, y) {
32
+ return { x: x * view.k, y: (y - view.vy) * view.k };
33
+ }
34
+ function clamp(value, low, high) {
35
+ return Math.min(Math.max(value, low), high);
36
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The sets that strokes join into, pair by pair: union-find, each set known by its root, which is
3
+ * its smallest member. The groups grow in them (R24), and the crop's bundles (R25).
4
+ */
5
+ export declare class Sets {
6
+ #private;
7
+ constructor(size: number);
8
+ root(at: number): number;
9
+ join(a: number, b: number): void;
10
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The sets that strokes join into, pair by pair: union-find, each set known by its root, which is
3
+ * its smallest member. The groups grow in them (R24), and the crop's bundles (R25).
4
+ */
5
+ export class Sets {
6
+ #parent;
7
+ constructor(size) {
8
+ this.#parent = Array.from({ length: size }, (_, at) => at);
9
+ }
10
+ root(at) {
11
+ let root = at;
12
+ while (this.#parent[root] !== root)
13
+ root = this.#parent[root] ?? root;
14
+ // Each piece on the way points at the root itself from now on.
15
+ for (let next = at; this.#parent[next] !== root;) {
16
+ const parent = this.#parent[next] ?? root;
17
+ this.#parent[next] = root;
18
+ next = parent;
19
+ }
20
+ return root;
21
+ }
22
+ join(a, b) {
23
+ const rootA = this.root(a);
24
+ const rootB = this.root(b);
25
+ // The smaller root stays: which pair comes first changes no root.
26
+ if (rootA < rootB)
27
+ this.#parent[rootB] = rootA;
28
+ else if (rootB < rootA)
29
+ this.#parent[rootA] = rootB;
30
+ }
31
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The handwriting sheet in logical units: 1000 wide and 1400 tall, the origin at its top left
3
+ * corner and y pointing down, one scale on both axes (R06). Every layer of the sheet counts in
4
+ * these units, whatever the device's screen.
5
+ */
6
+ export declare const SHEET: Readonly<{
7
+ readonly width: 1000;
8
+ readonly height: 1400;
9
+ }>;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The handwriting sheet in logical units: 1000 wide and 1400 tall, the origin at its top left
3
+ * corner and y pointing down, one scale on both axes (R06). Every layer of the sheet counts in
4
+ * these units, whatever the device's screen.
5
+ */
6
+ export const SHEET = Object.freeze({ width: 1000, height: 1400 });
@@ -0,0 +1,41 @@
1
+ import { type Cell } from "./boxes.js";
2
+ import type { Stroke } from "./ink.js";
3
+ import { type SheetSubmission, type WorkbookSubmission } from "./marks.js";
4
+ /** The work of a sheet for checking (R27): the PNG of its crop, and the context of the submission. */
5
+ export interface WorkForCheck {
6
+ readonly png: Uint8Array<ArrayBuffer>;
7
+ readonly submission: SheetSubmission;
8
+ }
9
+ /** The crop of one filled box of a workbook (R38): its item and its slot, from 1, and its PNG. */
10
+ export interface CellForCheck {
11
+ readonly item: number;
12
+ readonly slot: number;
13
+ readonly png: Uint8Array<ArrayBuffer>;
14
+ }
15
+ /**
16
+ * The work of a workbook for checking (R34, R38): the crop of each filled box, in the task's order,
17
+ * and the context of the submission.
18
+ */
19
+ export interface CellsForCheck {
20
+ readonly cells: readonly CellForCheck[];
21
+ readonly submission: WorkbookSubmission;
22
+ }
23
+ /**
24
+ * The work of a sheet for checking, as `exportWork()` gives it (R07, R27): of the strokes the sheet
25
+ * shows, their groups by the sheet's rule (R24), their crop with its digits `digit` px high and its
26
+ * PNG (R25), and the context of the submission — the crop's numbers that place the tutor's marks,
27
+ * the groups, the table «label → stroke IDs», and their digest (R26, R27) — frozen through. A sheet
28
+ * without strokes has no work to check, and gives null (H3).
29
+ */
30
+ export declare function workForCheck(strokes: readonly Stroke[], digit: number): WorkForCheck | null;
31
+ /**
32
+ * The work of a workbook for checking, as `exportWork()` gives it (R34, R38): of the strokes the
33
+ * page shows, the table of the boxes they fill by the rule of boxes (R37), and the crop of each
34
+ * filled box by R25 — the box's strokes alone, one group of them, its digits `digit` px high —
35
+ * with its item and its slot, in the task's order, and the context of the submission: the table and
36
+ * its digest. The draft goes nowhere. A page with no box filled has no work to check, and gives
37
+ * null, though it holds draft. The crops are frozen, not their bytes, which no freezing holds; the
38
+ * context is frozen through. One crop of every box's strokes, which spec may choose (В45 in
39
+ * RUN.md), is `croppedOf` of the table's strokes with the table's rows for groups.
40
+ */
41
+ export declare function cellsForCheck(strokes: readonly Stroke[], cells: readonly Cell[], digit: number): CellsForCheck | null;
@@ -0,0 +1,63 @@
1
+ import { tableOf } from "./boxes.js";
2
+ import { cropOf } from "./crop.js";
3
+ import { groupsOf } from "./groups.js";
4
+ import { digestOf, workbookDigestOf, } from "./marks.js";
5
+ import { pictureOf } from "./raster.js";
6
+ /**
7
+ * The work of a sheet for checking, as `exportWork()` gives it (R07, R27): of the strokes the sheet
8
+ * shows, their groups by the sheet's rule (R24), their crop with its digits `digit` px high and its
9
+ * PNG (R25), and the context of the submission — the crop's numbers that place the tutor's marks,
10
+ * the groups, the table «label → stroke IDs», and their digest (R26, R27) — frozen through. A sheet
11
+ * without strokes has no work to check, and gives null (H3).
12
+ */
13
+ export function workForCheck(strokes, digit) {
14
+ const groups = groupsOf(strokes);
15
+ const cropped = croppedOf(strokes, groups, digit);
16
+ if (cropped === null)
17
+ return null;
18
+ const { drawn, width, height, margins, sheetBox, frames, exact } = cropped.crop;
19
+ const numbers = { drawn, width, height, margins, sheetBox, frames, exact };
20
+ const submission = { crop: numbers, groups, digest: digestOf(strokes, numbers, groups) };
21
+ return Object.freeze({ png: cropped.png, submission: frozen(submission) });
22
+ }
23
+ /**
24
+ * The work of a workbook for checking, as `exportWork()` gives it (R34, R38): of the strokes the
25
+ * page shows, the table of the boxes they fill by the rule of boxes (R37), and the crop of each
26
+ * filled box by R25 — the box's strokes alone, one group of them, its digits `digit` px high —
27
+ * with its item and its slot, in the task's order, and the context of the submission: the table and
28
+ * its digest. The draft goes nowhere. A page with no box filled has no work to check, and gives
29
+ * null, though it holds draft. The crops are frozen, not their bytes, which no freezing holds; the
30
+ * context is frozen through. One crop of every box's strokes, which spec may choose (В45 in
31
+ * RUN.md), is `croppedOf` of the table's strokes with the table's rows for groups.
32
+ */
33
+ export function cellsForCheck(strokes, cells, digit) {
34
+ const boxes = tableOf(strokes, cells);
35
+ if (boxes.length === 0)
36
+ return null;
37
+ const byId = new Map(strokes.map((stroke) => [stroke.id, stroke]));
38
+ const own = (ids) => ids.flatMap((id) => byId.get(id) ?? []);
39
+ const crops = boxes.map(({ item, slot, ids }) => {
40
+ // A row of the table holds a stroke at least, and a crop of strokes is never null.
41
+ const cropped = croppedOf(own(ids), [{ label: "A", ids }], digit);
42
+ if (cropped === null)
43
+ throw new Error(`the box ${item}.${slot} has strokes and no crop`);
44
+ return Object.freeze({ item, slot, png: cropped.png });
45
+ });
46
+ const sent = own(boxes.flatMap(({ ids }) => ids));
47
+ const submission = { boxes, digest: workbookDigestOf(sent, boxes, cells) };
48
+ return Object.freeze({ cells: Object.freeze(crops), submission: frozen(submission) });
49
+ }
50
+ /** The crop of strokes in their groups, its digits `digit` px high, and its PNG (R25). */
51
+ function croppedOf(strokes, groups, digit) {
52
+ const crop = cropOf(strokes, groups, digit);
53
+ return crop === null ? null : { crop, png: pictureOf(strokes, crop) };
54
+ }
55
+ /** A value of plain objects and arrays, frozen through. */
56
+ function frozen(value) {
57
+ if (typeof value === "object" && value !== null && !Object.isFrozen(value)) {
58
+ for (const each of Object.values(value))
59
+ frozen(each);
60
+ Object.freeze(value);
61
+ }
62
+ return value;
63
+ }