@sametalmdr/word-editor 0.2.0-alpha → 0.3.0-alpha

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 (82) hide show
  1. package/CHANGELOG.md +108 -70
  2. package/LICENSE +21 -21
  3. package/README.md +286 -247
  4. package/dist/canvas/box-clipboard.js +238 -238
  5. package/dist/canvas/box-selection.js +80 -80
  6. package/dist/canvas/canvas-editor.js +502 -502
  7. package/dist/canvas/controller.js +717 -717
  8. package/dist/canvas/geometry.js +131 -131
  9. package/dist/canvas/gesture.d.ts +0 -1
  10. package/dist/canvas/gesture.js +289 -289
  11. package/dist/canvas/handles.js +212 -212
  12. package/dist/canvas/menu.js +82 -82
  13. package/dist/canvas/mode-controller.js +81 -81
  14. package/dist/canvas/nudge.js +108 -108
  15. package/dist/canvas/overlay.js +149 -149
  16. package/dist/canvas/snap.js +132 -132
  17. package/dist/canvas/viewport.js +112 -112
  18. package/dist/canvas/z-order.js +171 -171
  19. package/dist/commands/alignment.js +150 -150
  20. package/dist/commands/box.js +272 -272
  21. package/dist/commands/clipboard.js +93 -93
  22. package/dist/commands/graphemes.js +112 -112
  23. package/dist/commands/marks.js +153 -153
  24. package/dist/commands/paragraph-props.js +176 -176
  25. package/dist/commands/size.js +368 -368
  26. package/dist/commands/text.js +291 -291
  27. package/dist/docx.d.ts +1 -0
  28. package/dist/docx.js +15 -13
  29. package/dist/editor/editing-host.js +434 -434
  30. package/dist/element.js +41 -41
  31. package/dist/fonts/OFL-Carlito.txt +94 -0
  32. package/dist/fonts/calibri-metrics.d.ts +27 -0
  33. package/dist/fonts/calibri-metrics.js +88 -0
  34. package/dist/history/history.js +433 -433
  35. package/dist/index.js +63 -63
  36. package/dist/input/beforeinput.js +224 -224
  37. package/dist/input/clipboard.js +107 -107
  38. package/dist/input/composition.js +341 -341
  39. package/dist/input/keymap.js +54 -54
  40. package/dist/io/ooxml/custgeom.js +72 -72
  41. package/dist/io/ooxml/drawingml.js +232 -232
  42. package/dist/io/ooxml/edits.js +67 -67
  43. package/dist/io/ooxml/export-splice.js +880 -880
  44. package/dist/io/ooxml/export.js +365 -365
  45. package/dist/io/ooxml/fill.d.ts +93 -8
  46. package/dist/io/ooxml/fill.js +618 -562
  47. package/dist/io/ooxml/fit.d.ts +252 -0
  48. package/dist/io/ooxml/fit.js +598 -0
  49. package/dist/io/ooxml/import-gate.js +200 -200
  50. package/dist/io/ooxml/import.js +691 -691
  51. package/dist/io/ooxml/inflate.js +126 -126
  52. package/dist/io/ooxml/media.js +87 -87
  53. package/dist/io/ooxml/ns.d.ts +2 -0
  54. package/dist/io/ooxml/ns.js +26 -21
  55. package/dist/io/ooxml/numbering.js +290 -290
  56. package/dist/io/ooxml/opc-write.js +171 -171
  57. package/dist/io/ooxml/provenance.js +93 -93
  58. package/dist/io/ooxml/recipe.js +708 -708
  59. package/dist/io/ooxml/relationships.js +59 -59
  60. package/dist/io/ooxml/run-fonts.d.ts +89 -0
  61. package/dist/io/ooxml/run-fonts.js +378 -0
  62. package/dist/io/ooxml/slots.d.ts +4 -0
  63. package/dist/io/ooxml/slots.js +582 -581
  64. package/dist/io/ooxml/styles-resolve.js +257 -257
  65. package/dist/io/ooxml/units.js +43 -43
  66. package/dist/io/ooxml/value.js +120 -120
  67. package/dist/io/ooxml/xml-scan.js +616 -616
  68. package/dist/io/ooxml/zip-container.js +228 -228
  69. package/dist/io/ooxml/zip-reject.js +106 -106
  70. package/dist/measure/placement.js +83 -83
  71. package/dist/measure/report.js +75 -75
  72. package/dist/model/ids.js +70 -70
  73. package/dist/model/normalize.js +146 -146
  74. package/dist/model/position.js +283 -283
  75. package/dist/model/transaction.js +209 -209
  76. package/dist/platform/capabilities.js +25 -25
  77. package/dist/react/index.js +21 -21
  78. package/dist/selection/controller.js +315 -315
  79. package/dist/selection/dom-map.js +334 -334
  80. package/dist/state/editor-state.js +109 -109
  81. package/dist/styles.css +66 -66
  82. package/package.json +94 -93
@@ -1,149 +1,149 @@
1
- // The resize-handle / snap-guide overlay: a SEPARATE layer, never a child of
2
- // a page element (ADR-0018 §4.1/Karar 9). `handlePlacements`/
3
- // `guidePlacements` are pure (page-pt geometry -> client-px placements);
4
- // `Overlay` is the one class in this module that touches the DOM, and the
5
- // ONLY DOM writer in `src/canvas/` besides `CanvasController`.
6
- //
7
- // Measured constraint, not a style choice: `Renderer.decorationCounts`
8
- // assumes decorations occupy a FIXED PREFIX of every page element's own
9
- // child list, and `placeContainers` does index arithmetic against that
10
- // assumption (`src/render/renderer.js`'s `placeContainers`). Inserting a
11
- // handle/guide element as a page's child would desync that prefix on the
12
- // very next render — `Overlay` therefore writes into its OWN container
13
- // element, a sibling of the pages, never into one.
14
- //
15
- // `document` is INJECTED, never read from the global — `ssr-safety.test.js`
16
- // imports every file under `src/` with no `window`/`document` in scope, so
17
- // this is not a style preference, it is the only way this file can be
18
- // imported at all.
19
-
20
- import { handleRects } from "./handles.js";
21
- import { pageToClientRect, pageScale } from "./viewport.js";
22
-
23
- /**
24
- * @typedef {object} HandlePlacement
25
- * @property {string} name one of `ALL_HANDLES` (handles.js)
26
- * @property {{left: number, top: number, width: number, height: number}} rect client px
27
- */
28
-
29
- /**
30
- * @typedef {object} GuidePlacement
31
- * @property {"x" | "y"} axis
32
- * @property {number} position client px — an "x" guide is a vertical line at this left offset, a "y" guide a horizontal line at this top offset
33
- */
34
-
35
- /**
36
- * Every resize handle a box currently offers, converted from page-pt
37
- * (`handles.js`'s `handleRects`) to client-px — the one bridge between the
38
- * canvas layer's own geometry and an actual positioned `<div>`.
39
- *
40
- * @param {import("../model/node.js").BoxLayout} layout
41
- * @param {import("./handles.js").HandleMetrics} metrics
42
- * @param {import("./viewport.js").PageRect} pageRect
43
- * @returns {HandlePlacement[]}
44
- */
45
- export function handlePlacements(layout, metrics, pageRect) {
46
- return handleRects(layout, metrics).map((handle) => ({
47
- name: handle.name,
48
- rect: pageToClientRect(pageRect, handle.rect),
49
- }));
50
- }
51
-
52
- /**
53
- * `snapAdjustment`'s guides (page-pt line positions, `snap.js`), converted
54
- * to client-px — the overlay draws each as a full-length line across
55
- * `pageRect`, so only the ONE offset perpendicular to the line matters.
56
- *
57
- * @param {import("./snap.js").SnapGuide[]} guides
58
- * @param {import("./viewport.js").PageRect} pageRect
59
- * @returns {GuidePlacement[]}
60
- */
61
- export function guidePlacements(guides, pageRect) {
62
- const scale = pageScale(pageRect);
63
- return guides.map((guide) => ({
64
- axis: guide.axis,
65
- position: guide.axis === "x" ? pageRect.left + guide.position * scale : pageRect.top + guide.position * scale,
66
- }));
67
- }
68
-
69
- /**
70
- * The one DOM writer for handles and snap guides. Lives in its own
71
- * `container` element (a sibling of the page elements `Renderer` owns —
72
- * never a child of one, see this file's header). Handle elements are
73
- * REUSED across renders (matched by `name`) so a drag does not thrash the
74
- * DOM every frame; guide elements are cheap and simply rebuilt each call,
75
- * since they exist only while a drag is actively snapping.
76
- */
77
- export class Overlay {
78
- /**
79
- * @param {import("../render/dom.js").DocumentLike} documentTarget injected, never the global
80
- * @param {any} container an element OUTSIDE every page — this class never touches a page element
81
- */
82
- constructor(documentTarget, container) {
83
- this.document = documentTarget;
84
- this.container = container;
85
- /** @type {Map<string, any>} */
86
- this.handleElements = new Map();
87
- /** @type {any[]} */
88
- this.guideElements = [];
89
- }
90
-
91
- /**
92
- * @param {HandlePlacement[]} placements
93
- * @returns {void}
94
- */
95
- syncHandles(placements) {
96
- const seen = new Set();
97
- for (const placement of placements) {
98
- seen.add(placement.name);
99
- let el = this.handleElements.get(placement.name);
100
- if (el === undefined) {
101
- el = this.document.createElement("div");
102
- el.className = "word-editor-box-handle";
103
- el.dataset.handle = placement.name;
104
- this.container.appendChild(el);
105
- this.handleElements.set(placement.name, el);
106
- }
107
- el.style.left = `${placement.rect.left}px`;
108
- el.style.top = `${placement.rect.top}px`;
109
- el.style.width = `${placement.rect.width}px`;
110
- el.style.height = `${placement.rect.height}px`;
111
- }
112
- // A handle that no longer applies (e.g. the box degraded a kademe, or
113
- // the selection changed) is removed rather than hidden — no handle
114
- // this class ever creates should linger once `placements` stops naming it.
115
- for (const [name, el] of this.handleElements) {
116
- if (!seen.has(name)) {
117
- this.container.removeChild(el);
118
- this.handleElements.delete(name);
119
- }
120
- }
121
- }
122
-
123
- /**
124
- * @param {GuidePlacement[]} placements
125
- * @returns {void}
126
- */
127
- syncGuides(placements) {
128
- for (const el of this.guideElements) {
129
- this.container.removeChild(el);
130
- }
131
- this.guideElements = placements.map((placement) => {
132
- const el = this.document.createElement("div");
133
- el.className = `word-editor-snap-guide word-editor-snap-guide--${placement.axis}`;
134
- if (placement.axis === "x") {
135
- el.style.left = `${placement.position}px`;
136
- } else {
137
- el.style.top = `${placement.position}px`;
138
- }
139
- this.container.appendChild(el);
140
- return el;
141
- });
142
- }
143
-
144
- /** Removes every handle and guide element this instance owns. @returns {void} */
145
- clear() {
146
- this.syncHandles([]);
147
- this.syncGuides([]);
148
- }
149
- }
1
+ // The resize-handle / snap-guide overlay: a SEPARATE layer, never a child of
2
+ // a page element (ADR-0018 §4.1/Karar 9). `handlePlacements`/
3
+ // `guidePlacements` are pure (page-pt geometry -> client-px placements);
4
+ // `Overlay` is the one class in this module that touches the DOM, and the
5
+ // ONLY DOM writer in `src/canvas/` besides `CanvasController`.
6
+ //
7
+ // Measured constraint, not a style choice: `Renderer.decorationCounts`
8
+ // assumes decorations occupy a FIXED PREFIX of every page element's own
9
+ // child list, and `placeContainers` does index arithmetic against that
10
+ // assumption (`src/render/renderer.js`'s `placeContainers`). Inserting a
11
+ // handle/guide element as a page's child would desync that prefix on the
12
+ // very next render — `Overlay` therefore writes into its OWN container
13
+ // element, a sibling of the pages, never into one.
14
+ //
15
+ // `document` is INJECTED, never read from the global — `ssr-safety.test.js`
16
+ // imports every file under `src/` with no `window`/`document` in scope, so
17
+ // this is not a style preference, it is the only way this file can be
18
+ // imported at all.
19
+
20
+ import { handleRects } from "./handles.js";
21
+ import { pageToClientRect, pageScale } from "./viewport.js";
22
+
23
+ /**
24
+ * @typedef {object} HandlePlacement
25
+ * @property {string} name one of `ALL_HANDLES` (handles.js)
26
+ * @property {{left: number, top: number, width: number, height: number}} rect client px
27
+ */
28
+
29
+ /**
30
+ * @typedef {object} GuidePlacement
31
+ * @property {"x" | "y"} axis
32
+ * @property {number} position client px — an "x" guide is a vertical line at this left offset, a "y" guide a horizontal line at this top offset
33
+ */
34
+
35
+ /**
36
+ * Every resize handle a box currently offers, converted from page-pt
37
+ * (`handles.js`'s `handleRects`) to client-px — the one bridge between the
38
+ * canvas layer's own geometry and an actual positioned `<div>`.
39
+ *
40
+ * @param {import("../model/node.js").BoxLayout} layout
41
+ * @param {import("./handles.js").HandleMetrics} metrics
42
+ * @param {import("./viewport.js").PageRect} pageRect
43
+ * @returns {HandlePlacement[]}
44
+ */
45
+ export function handlePlacements(layout, metrics, pageRect) {
46
+ return handleRects(layout, metrics).map((handle) => ({
47
+ name: handle.name,
48
+ rect: pageToClientRect(pageRect, handle.rect),
49
+ }));
50
+ }
51
+
52
+ /**
53
+ * `snapAdjustment`'s guides (page-pt line positions, `snap.js`), converted
54
+ * to client-px — the overlay draws each as a full-length line across
55
+ * `pageRect`, so only the ONE offset perpendicular to the line matters.
56
+ *
57
+ * @param {import("./snap.js").SnapGuide[]} guides
58
+ * @param {import("./viewport.js").PageRect} pageRect
59
+ * @returns {GuidePlacement[]}
60
+ */
61
+ export function guidePlacements(guides, pageRect) {
62
+ const scale = pageScale(pageRect);
63
+ return guides.map((guide) => ({
64
+ axis: guide.axis,
65
+ position: guide.axis === "x" ? pageRect.left + guide.position * scale : pageRect.top + guide.position * scale,
66
+ }));
67
+ }
68
+
69
+ /**
70
+ * The one DOM writer for handles and snap guides. Lives in its own
71
+ * `container` element (a sibling of the page elements `Renderer` owns —
72
+ * never a child of one, see this file's header). Handle elements are
73
+ * REUSED across renders (matched by `name`) so a drag does not thrash the
74
+ * DOM every frame; guide elements are cheap and simply rebuilt each call,
75
+ * since they exist only while a drag is actively snapping.
76
+ */
77
+ export class Overlay {
78
+ /**
79
+ * @param {import("../render/dom.js").DocumentLike} documentTarget injected, never the global
80
+ * @param {any} container an element OUTSIDE every page — this class never touches a page element
81
+ */
82
+ constructor(documentTarget, container) {
83
+ this.document = documentTarget;
84
+ this.container = container;
85
+ /** @type {Map<string, any>} */
86
+ this.handleElements = new Map();
87
+ /** @type {any[]} */
88
+ this.guideElements = [];
89
+ }
90
+
91
+ /**
92
+ * @param {HandlePlacement[]} placements
93
+ * @returns {void}
94
+ */
95
+ syncHandles(placements) {
96
+ const seen = new Set();
97
+ for (const placement of placements) {
98
+ seen.add(placement.name);
99
+ let el = this.handleElements.get(placement.name);
100
+ if (el === undefined) {
101
+ el = this.document.createElement("div");
102
+ el.className = "word-editor-box-handle";
103
+ el.dataset.handle = placement.name;
104
+ this.container.appendChild(el);
105
+ this.handleElements.set(placement.name, el);
106
+ }
107
+ el.style.left = `${placement.rect.left}px`;
108
+ el.style.top = `${placement.rect.top}px`;
109
+ el.style.width = `${placement.rect.width}px`;
110
+ el.style.height = `${placement.rect.height}px`;
111
+ }
112
+ // A handle that no longer applies (e.g. the box degraded a kademe, or
113
+ // the selection changed) is removed rather than hidden — no handle
114
+ // this class ever creates should linger once `placements` stops naming it.
115
+ for (const [name, el] of this.handleElements) {
116
+ if (!seen.has(name)) {
117
+ this.container.removeChild(el);
118
+ this.handleElements.delete(name);
119
+ }
120
+ }
121
+ }
122
+
123
+ /**
124
+ * @param {GuidePlacement[]} placements
125
+ * @returns {void}
126
+ */
127
+ syncGuides(placements) {
128
+ for (const el of this.guideElements) {
129
+ this.container.removeChild(el);
130
+ }
131
+ this.guideElements = placements.map((placement) => {
132
+ const el = this.document.createElement("div");
133
+ el.className = `word-editor-snap-guide word-editor-snap-guide--${placement.axis}`;
134
+ if (placement.axis === "x") {
135
+ el.style.left = `${placement.position}px`;
136
+ } else {
137
+ el.style.top = `${placement.position}px`;
138
+ }
139
+ this.container.appendChild(el);
140
+ return el;
141
+ });
142
+ }
143
+
144
+ /** Removes every handle and guide element this instance owns. @returns {void} */
145
+ clear() {
146
+ this.syncHandles([]);
147
+ this.syncGuides([]);
148
+ }
149
+ }
@@ -1,132 +1,132 @@
1
- // Which occupants a drag can snap against, and by how much to correct a
2
- // proposed drop position toward the nearest matching edge (ADR-0018 §6,
3
- // §9/Karar 9). No src/, DOM, or history import — pure data in, data out.
4
- //
5
- // Target set is CLOSED (ADR-0015, ADR-0018 §6.1): editable boxes UNION
6
- // decorations. `pageOccupants` is called with NO `kind` filter here — the
7
- // same "one definition of is-this-a-box-or-a-decoration" file hit-test.js
8
- // filters TO boxes only, this file deliberately does not. A decoration
9
- // cannot be selected or moved, but it is a legitimate snap target — being a
10
- // target is not the same as being selectable.
11
- //
12
- // `rectLines` (geometry.js) is the ONLY place the six snap lines are
13
- // computed — this file never recomputes left/centerX/right/top/centerY/
14
- // bottom itself (ADR-0018 §9's "TEK tanım" rule).
15
-
16
- import { pageOccupants } from "./page.js";
17
- import { rectLines } from "./geometry.js";
18
-
19
- /**
20
- * @typedef {object} SnapGuide
21
- * @property {"x" | "y"} axis
22
- * @property {number} position pt — where the guide line runs, at the matched target edge
23
- */
24
-
25
- /**
26
- * @typedef {object} SnapResult
27
- * @property {number} dx pt — add to the drag's own raw delta on this axis; 0 when nothing snapped
28
- * @property {number} dy pt
29
- * @property {SnapGuide[]} guides 0, 1, or 2 entries — one per axis that actually snapped
30
- */
31
-
32
- /**
33
- * Every occupant on `page` that a drag may snap against, except the box(es)
34
- * currently being dragged. No `kind` filter — a decoration is excluded from
35
- * SELECTION (ADR-0015) but not from this set (ADR-0018 §6.1's closed
36
- * target-set decision: "hedef olmak ≠ seçilebilmek").
37
- *
38
- * @param {import("../model/node.js").Doc} doc
39
- * @param {number} page
40
- * @param {import("../model/node.js").NodeId[]} [excludeIds] the box(es) being dragged — never snap a box against itself
41
- * @returns {import("./page.js").PageOccupant[]}
42
- */
43
- export function snapTargets(doc, page, excludeIds = []) {
44
- const exclude = new Set(excludeIds);
45
- return pageOccupants(doc, page).filter((occupant) => !exclude.has(occupant.id));
46
- }
47
-
48
- /**
49
- * @param {number} threshold
50
- * @returns {void}
51
- */
52
- export function requireSnapThreshold(threshold) {
53
- if (typeof threshold !== "number" || !Number.isFinite(threshold)) {
54
- throw new TypeError(`snapAdjustment: threshold must be a finite number, got ${JSON.stringify(threshold)}`);
55
- }
56
- }
57
-
58
- /**
59
- * The single closest (targetLine, draggedLine) pair on one axis, within
60
- * `threshold`. No priority between targets (ADR-0018 §6.1) — every line of
61
- * every target is a candidate on equal footing; the smallest distance wins,
62
- * a tie broken by the smaller `NodeId` (determinism, not meaning).
63
- *
64
- * @param {number[]} draggedLines this axis's 3 candidate lines on the dragged rect
65
- * @param {import("./page.js").PageOccupant[]} targets
66
- * @param {(lines: import("./geometry.js").SnapLines) => number[]} axisLinesOf
67
- * @param {number} threshold
68
- * @returns {{delta: number, position: number} | null}
69
- */
70
- function bestAxisMatch(draggedLines, targets, axisLinesOf, threshold) {
71
- /** @type {{delta: number, distance: number, position: number, targetId: number} | null} */
72
- let best = null;
73
- for (const target of targets) {
74
- const targetLines = axisLinesOf(rectLines(target.rect));
75
- for (const targetLine of targetLines) {
76
- for (const draggedLine of draggedLines) {
77
- const delta = targetLine - draggedLine;
78
- const distance = Math.abs(delta);
79
- if (distance > threshold) {
80
- continue;
81
- }
82
- const better =
83
- best === null || distance < best.distance || (distance === best.distance && target.id < best.targetId);
84
- if (better) {
85
- best = { delta, distance, position: targetLine, targetId: target.id };
86
- }
87
- }
88
- }
89
- }
90
- return best === null ? null : { delta: best.delta, position: best.position };
91
- }
92
-
93
- /**
94
- * The correction to ADD to a drag's own raw {dx, dy} so the dragged rect's
95
- * nearest edge lands exactly on the nearest matching target edge, on EACH
96
- * axis independently (ADR-0018 §6.2 — x may snap while y does not, or vice
97
- * versa). `draggedRect` is the box's PROPOSED rect for this frame (its
98
- * anchor already moved by the drag's own raw delta), not the anchor itself.
99
- *
100
- * @param {import("./geometry.js").Rect} draggedRect
101
- * @param {import("./page.js").PageOccupant[]} targets from `snapTargets`
102
- * @param {number} threshold pt — injected, no default (ADR-0018 §6.3, Ö4 unmeasured)
103
- * @returns {SnapResult}
104
- */
105
- export function snapAdjustment(draggedRect, targets, threshold) {
106
- requireSnapThreshold(threshold);
107
- const dragged = rectLines(draggedRect);
108
-
109
- const xMatch = bestAxisMatch(
110
- [dragged.left, dragged.centerX, dragged.right],
111
- targets,
112
- (lines) => [lines.left, lines.centerX, lines.right],
113
- threshold,
114
- );
115
- const yMatch = bestAxisMatch(
116
- [dragged.top, dragged.centerY, dragged.bottom],
117
- targets,
118
- (lines) => [lines.top, lines.centerY, lines.bottom],
119
- threshold,
120
- );
121
-
122
- /** @type {SnapGuide[]} */
123
- const guides = [];
124
- if (xMatch !== null) {
125
- guides.push({ axis: "x", position: xMatch.position });
126
- }
127
- if (yMatch !== null) {
128
- guides.push({ axis: "y", position: yMatch.position });
129
- }
130
-
131
- return { dx: xMatch === null ? 0 : xMatch.delta, dy: yMatch === null ? 0 : yMatch.delta, guides };
132
- }
1
+ // Which occupants a drag can snap against, and by how much to correct a
2
+ // proposed drop position toward the nearest matching edge (ADR-0018 §6,
3
+ // §9/Karar 9). No src/, DOM, or history import — pure data in, data out.
4
+ //
5
+ // Target set is CLOSED (ADR-0015, ADR-0018 §6.1): editable boxes UNION
6
+ // decorations. `pageOccupants` is called with NO `kind` filter here — the
7
+ // same "one definition of is-this-a-box-or-a-decoration" file hit-test.js
8
+ // filters TO boxes only, this file deliberately does not. A decoration
9
+ // cannot be selected or moved, but it is a legitimate snap target — being a
10
+ // target is not the same as being selectable.
11
+ //
12
+ // `rectLines` (geometry.js) is the ONLY place the six snap lines are
13
+ // computed — this file never recomputes left/centerX/right/top/centerY/
14
+ // bottom itself (ADR-0018 §9's "TEK tanım" rule).
15
+
16
+ import { pageOccupants } from "./page.js";
17
+ import { rectLines } from "./geometry.js";
18
+
19
+ /**
20
+ * @typedef {object} SnapGuide
21
+ * @property {"x" | "y"} axis
22
+ * @property {number} position pt — where the guide line runs, at the matched target edge
23
+ */
24
+
25
+ /**
26
+ * @typedef {object} SnapResult
27
+ * @property {number} dx pt — add to the drag's own raw delta on this axis; 0 when nothing snapped
28
+ * @property {number} dy pt
29
+ * @property {SnapGuide[]} guides 0, 1, or 2 entries — one per axis that actually snapped
30
+ */
31
+
32
+ /**
33
+ * Every occupant on `page` that a drag may snap against, except the box(es)
34
+ * currently being dragged. No `kind` filter — a decoration is excluded from
35
+ * SELECTION (ADR-0015) but not from this set (ADR-0018 §6.1's closed
36
+ * target-set decision: "hedef olmak ≠ seçilebilmek").
37
+ *
38
+ * @param {import("../model/node.js").Doc} doc
39
+ * @param {number} page
40
+ * @param {import("../model/node.js").NodeId[]} [excludeIds] the box(es) being dragged — never snap a box against itself
41
+ * @returns {import("./page.js").PageOccupant[]}
42
+ */
43
+ export function snapTargets(doc, page, excludeIds = []) {
44
+ const exclude = new Set(excludeIds);
45
+ return pageOccupants(doc, page).filter((occupant) => !exclude.has(occupant.id));
46
+ }
47
+
48
+ /**
49
+ * @param {number} threshold
50
+ * @returns {void}
51
+ */
52
+ export function requireSnapThreshold(threshold) {
53
+ if (typeof threshold !== "number" || !Number.isFinite(threshold)) {
54
+ throw new TypeError(`snapAdjustment: threshold must be a finite number, got ${JSON.stringify(threshold)}`);
55
+ }
56
+ }
57
+
58
+ /**
59
+ * The single closest (targetLine, draggedLine) pair on one axis, within
60
+ * `threshold`. No priority between targets (ADR-0018 §6.1) — every line of
61
+ * every target is a candidate on equal footing; the smallest distance wins,
62
+ * a tie broken by the smaller `NodeId` (determinism, not meaning).
63
+ *
64
+ * @param {number[]} draggedLines this axis's 3 candidate lines on the dragged rect
65
+ * @param {import("./page.js").PageOccupant[]} targets
66
+ * @param {(lines: import("./geometry.js").SnapLines) => number[]} axisLinesOf
67
+ * @param {number} threshold
68
+ * @returns {{delta: number, position: number} | null}
69
+ */
70
+ function bestAxisMatch(draggedLines, targets, axisLinesOf, threshold) {
71
+ /** @type {{delta: number, distance: number, position: number, targetId: number} | null} */
72
+ let best = null;
73
+ for (const target of targets) {
74
+ const targetLines = axisLinesOf(rectLines(target.rect));
75
+ for (const targetLine of targetLines) {
76
+ for (const draggedLine of draggedLines) {
77
+ const delta = targetLine - draggedLine;
78
+ const distance = Math.abs(delta);
79
+ if (distance > threshold) {
80
+ continue;
81
+ }
82
+ const better =
83
+ best === null || distance < best.distance || (distance === best.distance && target.id < best.targetId);
84
+ if (better) {
85
+ best = { delta, distance, position: targetLine, targetId: target.id };
86
+ }
87
+ }
88
+ }
89
+ }
90
+ return best === null ? null : { delta: best.delta, position: best.position };
91
+ }
92
+
93
+ /**
94
+ * The correction to ADD to a drag's own raw {dx, dy} so the dragged rect's
95
+ * nearest edge lands exactly on the nearest matching target edge, on EACH
96
+ * axis independently (ADR-0018 §6.2 — x may snap while y does not, or vice
97
+ * versa). `draggedRect` is the box's PROPOSED rect for this frame (its
98
+ * anchor already moved by the drag's own raw delta), not the anchor itself.
99
+ *
100
+ * @param {import("./geometry.js").Rect} draggedRect
101
+ * @param {import("./page.js").PageOccupant[]} targets from `snapTargets`
102
+ * @param {number} threshold pt — injected, no default (ADR-0018 §6.3, Ö4 unmeasured)
103
+ * @returns {SnapResult}
104
+ */
105
+ export function snapAdjustment(draggedRect, targets, threshold) {
106
+ requireSnapThreshold(threshold);
107
+ const dragged = rectLines(draggedRect);
108
+
109
+ const xMatch = bestAxisMatch(
110
+ [dragged.left, dragged.centerX, dragged.right],
111
+ targets,
112
+ (lines) => [lines.left, lines.centerX, lines.right],
113
+ threshold,
114
+ );
115
+ const yMatch = bestAxisMatch(
116
+ [dragged.top, dragged.centerY, dragged.bottom],
117
+ targets,
118
+ (lines) => [lines.top, lines.centerY, lines.bottom],
119
+ threshold,
120
+ );
121
+
122
+ /** @type {SnapGuide[]} */
123
+ const guides = [];
124
+ if (xMatch !== null) {
125
+ guides.push({ axis: "x", position: xMatch.position });
126
+ }
127
+ if (yMatch !== null) {
128
+ guides.push({ axis: "y", position: yMatch.position });
129
+ }
130
+
131
+ return { dx: xMatch === null ? 0 : xMatch.delta, dy: yMatch === null ? 0 : yMatch.delta, guides };
132
+ }