@altnovatrail/canvas 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,218 @@
1
+ // The width table of the workbook's layout (R35): how wide each character of a line is, in whole
2
+ // hundredths of the type size, so that the layout puts the boxes where it puts them on any device,
3
+ // whatever its fonts, and the text is fitted to its width by SVG (R34). The numbers are the
4
+ // canvas's own: each the mean of Liberation Sans and Liberation Serif, the fonts of the image the
5
+ // tests run in, rounded to a hundredth — no font's table. Which character is which comes from
6
+ // explicit ranges of code points alone, never from the engine's Unicode tables — no `\p{…}`, no
7
+ // `\s`, no `Intl.Segmenter` — for those follow each engine's version of Unicode, and the layout
8
+ // must be the same in every engine and in Node, as the crop is (R25).
9
+ /** A digit: they are all as wide, and the page asks for figures of one width (R35). */
10
+ export const DIGIT = 53;
11
+ /** A sign of an operation or a comparison: one width for all of them, the mean of theirs. */
12
+ export const SIGN = 56;
13
+ /** The signs of operations and comparisons, one width for all (R35). */
14
+ export const SIGNS = "+−×÷=<>≤≥≠±≈";
15
+ /** The space between two words, which the layout sets itself. */
16
+ export const SPACE = 26;
17
+ /** A wide character — a Chinese, Japanese or Korean one, an emoji: a square. */
18
+ export const WIDE = 100;
19
+ /** A character the table does not know, a Greek or a Hebrew letter among them. */
20
+ export const UNKNOWN = 60;
21
+ /**
22
+ * The characters of their own width: printable ASCII, the Russian alphabet and the marks a school
23
+ * page has, by width. ⋅ and ∶ the fonts lack: they take the width of · and of :. The space is
24
+ * SPACE, set by the layout.
25
+ */
26
+ export const BY_WIDTH = [
27
+ [19, "'"],
28
+ [23, "|"],
29
+ [25, "ijl"],
30
+ [26, ",."],
31
+ [28, "/:;t\\‘’∶"],
32
+ [31, "!I[]f"],
33
+ [32, "²³"],
34
+ [33, "()-`r·⋅"],
35
+ [38, '"'],
36
+ [39, "г„“”"],
37
+ [40, "°"],
38
+ [41, "{}"],
39
+ [43, "з"],
40
+ [44, "*Js"],
41
+ [45, "т"],
42
+ [46, "к"],
43
+ [47, "^czсэ"],
44
+ [49, "ь"],
45
+ [50, "?aekvxyавеухяё"],
46
+ [51, "ч"],
47
+ [53, "#$_bdghnopquор«»–"],
48
+ [54, "блнп"],
49
+ [55, "Здийц"],
50
+ [56, "~Г"],
51
+ [57, "ъ"],
52
+ [58, "FL"],
53
+ [61, "PSTZРТ"],
54
+ [62, "БКЬ"],
55
+ [64, "EЕЁ"],
56
+ [66, "Чм"],
57
+ [67, "BВЛУ"],
58
+ [68, "Дж"],
59
+ [69, "ACKRVXYАСХЭЯ"],
60
+ [70, "ы"],
61
+ [72, "&DHNUwИЙНП"],
62
+ [73, "Ц"],
63
+ [74, "ф"],
64
+ [75, "GOQОЪю"],
65
+ [78, "Ф"],
66
+ [79, "ш½¼¾⅓⅔⅛"],
67
+ [80, "щ"],
68
+ [81, "m"],
69
+ [86, "%MМ"],
70
+ [88, "Ы"],
71
+ [91, "Ж"],
72
+ [94, "W"],
73
+ [96, "Ш"],
74
+ [97, "@Щ"],
75
+ [100, "—…"],
76
+ [101, "№"],
77
+ [102, "Ю"],
78
+ ];
79
+ /**
80
+ * Code points of no width, each joining the character before it: controls, the soft hyphen,
81
+ * combining marks, the zero-width space and joiners, the marks of direction, variation selectors,
82
+ * the skin tones of emoji and tags.
83
+ */
84
+ export const ZERO = [
85
+ [0x0000, 0x0008],
86
+ [0x000e, 0x001f],
87
+ [0x007f, 0x009f],
88
+ [0x00ad, 0x00ad],
89
+ [0x0300, 0x036f],
90
+ [0x0483, 0x0489],
91
+ [0x1ab0, 0x1aff],
92
+ [0x1dc0, 0x1dff],
93
+ [0x200b, 0x200f],
94
+ [0x202a, 0x202e],
95
+ [0x2060, 0x206f],
96
+ [0x20d0, 0x20ff],
97
+ [0xfe00, 0xfe0f],
98
+ [0xfe20, 0xfe2f],
99
+ [0xfeff, 0xfeff],
100
+ [0x1f3fb, 0x1f3ff],
101
+ [0xe0000, 0xe007f],
102
+ [0xe0100, 0xe01ef],
103
+ ];
104
+ /** Code points of a square's width: Hangul, CJK, the full-width forms and emoji. */
105
+ export const WIDE_RANGES = [
106
+ [0x1100, 0x115f],
107
+ [0x2e80, 0x303e],
108
+ [0x3041, 0x33ff],
109
+ [0x3400, 0x4dbf],
110
+ [0x4e00, 0x9fff],
111
+ [0xa000, 0xa4cf],
112
+ [0xac00, 0xd7a3],
113
+ [0xf900, 0xfaff],
114
+ [0xfe30, 0xfe4f],
115
+ [0xff00, 0xff60],
116
+ [0xffe0, 0xffe6],
117
+ [0x1f000, 0x1faff],
118
+ [0x20000, 0x3fffd],
119
+ ];
120
+ /**
121
+ * The white space a line breaks at: tab, the line and paragraph ends and the space. A run of it is
122
+ * one space between two words, and none at the ends of a piece. Any other space — a no-break one
123
+ * among them — is a character of its word.
124
+ */
125
+ export const BREAKS = [0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x20, 0x85, 0x2028, 0x2029];
126
+ /** The joiner of an emoji's sequence: the next code point goes with the one before it. */
127
+ const ZWJ = 0x200d;
128
+ /** The regional indicators, two of which make a flag. */
129
+ const REGIONAL = [0x1f1e6, 0x1f1ff];
130
+ /**
131
+ * The spaces that do not break, by code point, for they do not show: the no-break space, the figure
132
+ * space, a digit wide, and the narrow no-break space.
133
+ */
134
+ export const SPACES = [
135
+ [0x00a0, 26],
136
+ [0x2007, 53],
137
+ [0x202f, 20],
138
+ ];
139
+ const OWN = new Map([
140
+ ...BY_WIDTH.flatMap(([width, characters]) => Array.from(characters, (character) => [character.codePointAt(0) ?? 0, width])),
141
+ ...SPACES,
142
+ ]);
143
+ function within(code, ranges) {
144
+ return ranges.some(([from, to]) => code >= from && code <= to);
145
+ }
146
+ /** The width of a code point, in hundredths of the type size. */
147
+ export function widthOf(code) {
148
+ if (within(code, ZERO))
149
+ return 0;
150
+ if (code >= 0x30 && code <= 0x39)
151
+ return DIGIT;
152
+ const character = String.fromCodePoint(code);
153
+ if (SIGNS.includes(character))
154
+ return SIGN;
155
+ const own = OWN.get(code);
156
+ if (own !== undefined)
157
+ return own;
158
+ return within(code, WIDE_RANGES) ? WIDE : UNKNOWN;
159
+ }
160
+ /** Whether a line may break at this code point, white space that collapses. */
161
+ export function breaks(code) {
162
+ return BREAKS.includes(code);
163
+ }
164
+ /**
165
+ * The words of a piece of text: what lies between its runs of white space. A piece of white space
166
+ * alone has none.
167
+ */
168
+ export function wordsOf(text) {
169
+ const words = [];
170
+ let word = "";
171
+ for (const character of text) {
172
+ if (breaks(character.codePointAt(0) ?? 0)) {
173
+ if (word !== "")
174
+ words.push(word);
175
+ word = "";
176
+ }
177
+ else {
178
+ word += character;
179
+ }
180
+ }
181
+ if (word !== "")
182
+ words.push(word);
183
+ return words;
184
+ }
185
+ /**
186
+ * The clusters of a word, which a word too long for a line is broken between: a code point of no
187
+ * width, and whatever follows a joiner, go with the one before them, and so does the second
188
+ * regional indicator of a flag. The code points come by `for…of`, which pairs surrogates by the
189
+ * language's own rule, not by Unicode's tables.
190
+ */
191
+ export function clustersOf(word) {
192
+ const clusters = [];
193
+ let joined = false;
194
+ let regional = 0;
195
+ for (const character of word) {
196
+ const code = character.codePointAt(0) ?? 0;
197
+ const flag = code >= REGIONAL[0] && code <= REGIONAL[1];
198
+ const last = clusters.length - 1;
199
+ if (last >= 0 && (joined || widthOf(code) === 0 || (flag && regional === 1))) {
200
+ clusters[last] += character;
201
+ regional = flag && regional === 1 ? 2 : regional;
202
+ }
203
+ else {
204
+ clusters.push(character);
205
+ regional = flag ? 1 : 0;
206
+ }
207
+ joined = code === ZWJ;
208
+ }
209
+ return clusters;
210
+ }
211
+ /** The width of a cluster: that of its first code point. */
212
+ export function clusterWidth(cluster) {
213
+ return widthOf(cluster.codePointAt(0) ?? 0);
214
+ }
215
+ /** The width of a word, in hundredths of the type size: the sum of its clusters'. */
216
+ export function wordWidth(word) {
217
+ return clustersOf(word).reduce((sum, cluster) => sum + clusterWidth(cluster), 0);
218
+ }
@@ -0,0 +1,77 @@
1
+ import { Clock } from "./clock.js";
2
+ import { type HistoryEvent } from "./history.js";
3
+ import type { Stroke } from "./ink.js";
4
+ import type { Point } from "./screen.js";
5
+ /**
6
+ * The work on the sheet: the strokes in sight, the stack of the actions that undo takes back, the
7
+ * last first (R19), and the history of writing, every action as an event at the sheet's time
8
+ * (R22). Clearing and erasing are actions too, so undo brings their strokes back. Nothing is
9
+ * redone. The times its methods take are the page's, and the sheet's clock turns one into the
10
+ * sheet's time only when the action lands: an undo that has nothing to take back is no event. A
11
+ * history given to the sheet is replayed by the same rules, each action at its own time (R27).
12
+ */
13
+ export declare class Work {
14
+ #private;
15
+ constructor(clock?: Clock);
16
+ /**
17
+ * The work a history of writing given to the sheet leaves (R27): its strokes, its stack of
18
+ * actions for undo and the history itself, in frozen objects of the work's own. The events are
19
+ * checked as `historyOf` checks them, then replayed by the work's own rules, each at its own time,
20
+ * and each must be what the work records for it: an erasing takes off strokes on the sheet, in
21
+ * the sheet's order; a clearing clears a sheet with strokes, all of them; an undo takes back the
22
+ * last action still in force and says what the stack says. A history the sheet could not have
23
+ * written is a TypeError that names the event. The clock is not asked: it goes on as the sheet
24
+ * that takes the work tells it (`Clock.resume`).
25
+ */
26
+ static restore(value: unknown, clock: Clock): Work;
27
+ /**
28
+ * The strokes on the sheet, in the order they were written. Every action gives a new array, and
29
+ * undoing a clearing gives back the very one it cleared.
30
+ */
31
+ get strokes(): readonly Stroke[];
32
+ /**
33
+ * Whether undo has an action to take back (R19): the stack is not empty. The element's Undo
34
+ * button stands unavailable while it is (R28).
35
+ */
36
+ get undoable(): boolean;
37
+ /**
38
+ * The history of writing: every action, in the order it landed, frozen through. An array once
39
+ * handed out never changes: after an action the history is a new one, made when it is next asked
40
+ * for.
41
+ */
42
+ get history(): readonly HistoryEvent[];
43
+ /**
44
+ * A stroke is written: it goes on the sheet and on the stack. The sheet gets a new array rather
45
+ * than a longer one, for an array once handed out never changes: a clearing keeps the one it
46
+ * cleared, and whoever read `strokes` keeps what they read. It lands at the sheet's latest time:
47
+ * its last point's, unless an action landed while it was under way and it ended without its
48
+ * pointerup, its last point earlier.
49
+ */
50
+ write(stroke: Stroke): void;
51
+ /**
52
+ * Takes the strokes of these IDs off the sheet, whole, in one action — one press of the eraser.
53
+ * IDs no longer on the sheet are left out; if none is on it, nothing is done.
54
+ */
55
+ erase(ids: Iterable<number>, time: number): boolean;
56
+ /**
57
+ * Takes back the last action: a written stroke goes, erased strokes come back to their places, a
58
+ * clearing brings its strokes back. On a sheet without actions nothing changes.
59
+ */
60
+ undo(time: number): boolean;
61
+ /** Takes every stroke off the sheet, in one action. Clearing an empty sheet is no action. */
62
+ clear(time: number): boolean;
63
+ /**
64
+ * The strokes on the sheet whose line comes within `reach` of the segment from `from` to `to`, in
65
+ * sheet units: the index finds those whose box is near enough, and the distance to each segment
66
+ * of their points decides (R05). A segment whose ends are one point is that point.
67
+ */
68
+ near(from: Point, to: Point, reach: number): Stroke[];
69
+ }
70
+ /** The ID the next stroke takes after a history (R27): one past the highest written, or 1. */
71
+ export declare function nextIdOf(history: readonly HistoryEvent[]): number;
72
+ /**
73
+ * Where the sheet's clock goes on after a history given to it (R27): past its last event by more
74
+ * than the pause of the groups (R24), so that the first stroke after it is no neighbour of its
75
+ * strokes; at 0 after an empty history, which is a new sheet's.
76
+ */
77
+ export declare function resumeOf(history: readonly HistoryEvent[]): number;
@@ -0,0 +1,293 @@
1
+ import RBush from "rbush";
2
+ import { Clock } from "./clock.js";
3
+ import { shown } from "./fields.js";
4
+ import { boxOf, segmentBox, segmentDistance } from "./geometry.js";
5
+ import { GROUP_RULE } from "./groups.js";
6
+ import { frozen, historyOf, sameEvent } from "./history.js";
7
+ /**
8
+ * The work on the sheet: the strokes in sight, the stack of the actions that undo takes back, the
9
+ * last first (R19), and the history of writing, every action as an event at the sheet's time
10
+ * (R22). Clearing and erasing are actions too, so undo brings their strokes back. Nothing is
11
+ * redone. The times its methods take are the page's, and the sheet's clock turns one into the
12
+ * sheet's time only when the action lands: an undo that has nothing to take back is no event. A
13
+ * history given to the sheet is replayed by the same rules, each action at its own time (R27).
14
+ */
15
+ export class Work {
16
+ #clock;
17
+ #strokes = [];
18
+ /**
19
+ * Whether a history is being replayed into the work (`restore`): a stroke written then goes onto
20
+ * the array of the sheet as it is, for no one has been handed that array yet.
21
+ */
22
+ #replaying = false;
23
+ #done = [];
24
+ /** The history's events, and the copy of them last handed out, until the next action. */
25
+ #events = [];
26
+ #history;
27
+ /** The index of the strokes on the sheet, built anew for each new array of them. */
28
+ #index = new RBush();
29
+ #indexed = this.#strokes;
30
+ #entries = new WeakMap();
31
+ constructor(clock = new Clock()) {
32
+ this.#clock = clock;
33
+ }
34
+ /**
35
+ * The work a history of writing given to the sheet leaves (R27): its strokes, its stack of
36
+ * actions for undo and the history itself, in frozen objects of the work's own. The events are
37
+ * checked as `historyOf` checks them, then replayed by the work's own rules, each at its own time,
38
+ * and each must be what the work records for it: an erasing takes off strokes on the sheet, in
39
+ * the sheet's order; a clearing clears a sheet with strokes, all of them; an undo takes back the
40
+ * last action still in force and says what the stack says. A history the sheet could not have
41
+ * written is a TypeError that names the event. The clock is not asked: it goes on as the sheet
42
+ * that takes the work tells it (`Clock.resume`).
43
+ */
44
+ static restore(value, clock) {
45
+ const work = new Work(clock);
46
+ work.#replaying = true;
47
+ for (const [n, event] of historyOf(value).entries())
48
+ work.#replay(event, `history[${n}]`);
49
+ work.#replaying = false;
50
+ // A new array for the sheet, which the index of the strokes is then built for.
51
+ work.#strokes = [...work.#strokes];
52
+ return work;
53
+ }
54
+ /**
55
+ * The strokes on the sheet, in the order they were written. Every action gives a new array, and
56
+ * undoing a clearing gives back the very one it cleared.
57
+ */
58
+ get strokes() {
59
+ return this.#strokes;
60
+ }
61
+ /**
62
+ * Whether undo has an action to take back (R19): the stack is not empty. The element's Undo
63
+ * button stands unavailable while it is (R28).
64
+ */
65
+ get undoable() {
66
+ return this.#done.length > 0;
67
+ }
68
+ /**
69
+ * The history of writing: every action, in the order it landed, frozen through. An array once
70
+ * handed out never changes: after an action the history is a new one, made when it is next asked
71
+ * for.
72
+ */
73
+ get history() {
74
+ this.#history ??= Object.freeze([...this.#events]);
75
+ return this.#history;
76
+ }
77
+ /**
78
+ * A stroke is written: it goes on the sheet and on the stack. The sheet gets a new array rather
79
+ * than a longer one, for an array once handed out never changes: a clearing keeps the one it
80
+ * cleared, and whoever read `strokes` keeps what they read. It lands at the sheet's latest time:
81
+ * its last point's, unless an action landed while it was under way and it ended without its
82
+ * pointerup, its last point earlier.
83
+ */
84
+ write(stroke) {
85
+ this.#write(stroke, Math.max(this.#clock.latest, stroke.points.at(-1)?.t ?? 0));
86
+ }
87
+ /**
88
+ * Takes the strokes of these IDs off the sheet, whole, in one action — one press of the eraser.
89
+ * IDs no longer on the sheet are left out; if none is on it, nothing is done.
90
+ */
91
+ erase(ids, time) {
92
+ return this.#erase(ids, () => this.#clock.at(time));
93
+ }
94
+ /**
95
+ * Takes back the last action: a written stroke goes, erased strokes come back to their places, a
96
+ * clearing brings its strokes back. On a sheet without actions nothing changes.
97
+ */
98
+ undo(time) {
99
+ return this.#undo(() => this.#clock.at(time));
100
+ }
101
+ /** Takes every stroke off the sheet, in one action. Clearing an empty sheet is no action. */
102
+ clear(time) {
103
+ return this.#clear(() => this.#clock.at(time));
104
+ }
105
+ /**
106
+ * The strokes on the sheet whose line comes within `reach` of the segment from `from` to `to`, in
107
+ * sheet units: the index finds those whose box is near enough, and the distance to each segment
108
+ * of their points decides (R05). A segment whose ends are one point is that point.
109
+ */
110
+ near(from, to, reach) {
111
+ const hits = [];
112
+ for (const { stroke } of this.#indexOf().search(segmentBox(from, to, reach))) {
113
+ if (lineDistance(stroke, from, to) <= reach)
114
+ hits.push(stroke);
115
+ }
116
+ return hits;
117
+ }
118
+ // The actions, each with the sheet's time it lands at, asked for only once it does.
119
+ #write(stroke, t) {
120
+ // A history replayed writes its strokes one after another, without a copy of the sheet for
121
+ // each: thousands of strokes would copy the sheet thousands of times. A clearing keeps the array
122
+ // it cleared and leaves the sheet a new one, so no array an action keeps grows by it.
123
+ if (this.#replaying)
124
+ this.#strokes.push(stroke);
125
+ else
126
+ this.#strokes = [...this.#strokes, stroke];
127
+ this.#done.push({ kind: "write", stroke });
128
+ this.#record({ kind: "write", t, stroke });
129
+ }
130
+ #erase(ids, at) {
131
+ const wanted = new Set(ids);
132
+ const erased = [];
133
+ const kept = [];
134
+ for (const [place, stroke] of this.#strokes.entries()) {
135
+ if (wanted.has(stroke.id))
136
+ erased.push({ at: place, stroke });
137
+ else
138
+ kept.push(stroke);
139
+ }
140
+ if (erased.length === 0)
141
+ return false;
142
+ this.#strokes = kept;
143
+ this.#done.push({ kind: "erase", erased });
144
+ this.#record({ kind: "erase", t: at(), ids: erased.map(({ stroke }) => stroke.id) });
145
+ return true;
146
+ }
147
+ #undo(at) {
148
+ const action = this.#done.pop();
149
+ if (action === undefined)
150
+ return false;
151
+ const t = at();
152
+ if (action.kind === "write") {
153
+ // Each undo gives back the sheet as it was after the action before, so a written stroke is
154
+ // the last on the sheet when its writing is undone.
155
+ this.#strokes = this.#strokes.slice(0, -1);
156
+ this.#record({ kind: "undo", t, removed: [action.stroke.id], restored: [] });
157
+ }
158
+ else if (action.kind === "erase") {
159
+ // The sheet is as the erasing left it: each stroke goes back to its place, the first first.
160
+ const strokes = [...this.#strokes];
161
+ for (const { at: place, stroke } of action.erased)
162
+ strokes.splice(place, 0, stroke);
163
+ this.#strokes = strokes;
164
+ const restored = action.erased.map(({ stroke }) => stroke.id);
165
+ this.#record({ kind: "undo", t, removed: [], restored });
166
+ }
167
+ else {
168
+ this.#strokes = action.strokes;
169
+ const restored = action.strokes.map((stroke) => stroke.id);
170
+ this.#record({ kind: "undo", t, removed: [], restored });
171
+ }
172
+ return true;
173
+ }
174
+ #clear(at) {
175
+ if (this.#strokes.length === 0)
176
+ return false;
177
+ const strokes = this.#strokes;
178
+ this.#done.push({ kind: "clear", strokes });
179
+ this.#strokes = [];
180
+ this.#record({ kind: "clear", t: at(), ids: strokes.map((stroke) => stroke.id) });
181
+ return true;
182
+ }
183
+ /**
184
+ * One event of a history given to the sheet, replayed at its own time: what the work records for
185
+ * it must be the event itself, or the history is not one the sheet could have written.
186
+ */
187
+ #replay(event, path) {
188
+ const at = () => event.t;
189
+ let done = true;
190
+ if (event.kind === "write")
191
+ this.#write(event.stroke, event.t);
192
+ else if (event.kind === "erase")
193
+ done = this.#erase(event.ids, at);
194
+ else if (event.kind === "clear")
195
+ done = this.#clear(at);
196
+ else
197
+ done = this.#undo(at);
198
+ const recorded = this.#events.at(-1);
199
+ if (done && recorded !== undefined && sameEvent(recorded, event))
200
+ return;
201
+ throw new TypeError(`${path}: ${what(event)}, ${done ? instead(recorded) : unable(event)}`);
202
+ }
203
+ #record(event) {
204
+ this.#events.push(frozen(event));
205
+ this.#history = undefined;
206
+ }
207
+ /** The index of the strokes on the sheet, built anew if they are a new array since it was built. */
208
+ #indexOf() {
209
+ if (this.#indexed !== this.#strokes) {
210
+ this.#index = new RBush().load(this.#strokes.map((stroke) => this.#entryOf(stroke)));
211
+ this.#indexed = this.#strokes;
212
+ }
213
+ return this.#index;
214
+ }
215
+ #entryOf(stroke) {
216
+ let entry = this.#entries.get(stroke);
217
+ if (entry === undefined) {
218
+ entry = { ...boxOf(stroke.points), stroke };
219
+ this.#entries.set(stroke, entry);
220
+ }
221
+ return entry;
222
+ }
223
+ }
224
+ /** The ID the next stroke takes after a history (R27): one past the highest written, or 1. */
225
+ export function nextIdOf(history) {
226
+ let highest = 0;
227
+ for (const event of history) {
228
+ if (event.kind === "write")
229
+ highest = Math.max(highest, event.stroke.id);
230
+ }
231
+ return highest + 1;
232
+ }
233
+ /**
234
+ * Where the sheet's clock goes on after a history given to it (R27): past its last event by more
235
+ * than the pause of the groups (R24), so that the first stroke after it is no neighbour of its
236
+ * strokes; at 0 after an empty history, which is a new sheet's.
237
+ */
238
+ export function resumeOf(history) {
239
+ const last = history.at(-1);
240
+ return last === undefined ? 0 : last.t + GROUP_RULE.pause + 1;
241
+ }
242
+ /** What an event of a history says it does, for a message. */
243
+ function what(event) {
244
+ switch (event.kind) {
245
+ case "write":
246
+ return `stroke ${event.stroke.id} written`;
247
+ case "erase":
248
+ return `strokes ${shown(event.ids)} erased`;
249
+ case "clear":
250
+ return `strokes ${shown(event.ids)} cleared`;
251
+ case "undo":
252
+ return `an undo that takes off ${shown(event.removed)} and brings back ${shown(event.restored)}`;
253
+ }
254
+ }
255
+ /** What the sheet does instead, by the rules of its stack, for a message. */
256
+ function instead(recorded) {
257
+ switch (recorded?.kind) {
258
+ case "erase":
259
+ return `where the sheet takes off ${shown(recorded.ids)}, in its order`;
260
+ case "clear":
261
+ return `where the sheet clears ${shown(recorded.ids)}`;
262
+ case "undo":
263
+ return `where the last action still in force takes off ${shown(recorded.removed)} and brings back ${shown(recorded.restored)}`;
264
+ default:
265
+ return "which the sheet does otherwise";
266
+ }
267
+ }
268
+ /** Why the sheet does nothing for an event, for a message. */
269
+ function unable(event) {
270
+ switch (event.kind) {
271
+ case "erase":
272
+ return "but none of them is on the sheet";
273
+ case "clear":
274
+ return "but the sheet is empty";
275
+ default:
276
+ return "but there is nothing to take back";
277
+ }
278
+ }
279
+ /** How near a stroke's line — the segments between its points — comes to the segment ab. */
280
+ function lineDistance(stroke, a, b) {
281
+ const [first, ...rest] = stroke.points;
282
+ if (first === undefined)
283
+ return Number.POSITIVE_INFINITY;
284
+ if (rest.length === 0)
285
+ return segmentDistance(a, b, first, first);
286
+ let nearest = Number.POSITIVE_INFINITY;
287
+ let previous = first;
288
+ for (const point of rest) {
289
+ nearest = Math.min(nearest, segmentDistance(a, b, previous, point));
290
+ previous = point;
291
+ }
292
+ return nearest;
293
+ }
@@ -0,0 +1,11 @@
1
+ /** The namespace of the SVG elements the element makes: its layers, and the row's triangle. */
2
+ export declare const SVG = "http://www.w3.org/2000/svg";
3
+ /** An SVG shape — a workbook's box, a demo's picture — set over a box in its layer's units. */
4
+ export declare function placeOver(shape: SVGElement, { left, top, width, height }: {
5
+ left: number;
6
+ top: number;
7
+ width: number;
8
+ height: number;
9
+ }): void;
10
+ /** An attribute set to a value, or taken off for null, and written only if it changes. */
11
+ export declare function write(element: Element, name: string, value: string | null): void;
@@ -0,0 +1,22 @@
1
+ // The attributes the element writes as its state changes — the row's and the template's — each
2
+ // only when its value changes, so that what has not changed tells no observer of a change, the
3
+ // namespace of the SVG it writes them on, and the place of an SVG shape of a box. This module is
4
+ // no entry point of the package.
5
+ /** The namespace of the SVG elements the element makes: its layers, and the row's triangle. */
6
+ export const SVG = "http://www.w3.org/2000/svg";
7
+ /** An SVG shape — a workbook's box, a demo's picture — set over a box in its layer's units. */
8
+ export function placeOver(shape, { left, top, width, height }) {
9
+ shape.setAttribute("x", String(left));
10
+ shape.setAttribute("y", String(top));
11
+ shape.setAttribute("width", String(width));
12
+ shape.setAttribute("height", String(height));
13
+ }
14
+ /** An attribute set to a value, or taken off for null, and written only if it changes. */
15
+ export function write(element, name, value) {
16
+ if (element.getAttribute(name) === value)
17
+ return;
18
+ if (value === null)
19
+ element.removeAttribute(name);
20
+ else
21
+ element.setAttribute(name, value);
22
+ }