@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.
- package/CHANGELOG.md +108 -70
- package/LICENSE +21 -21
- package/README.md +286 -247
- package/dist/canvas/box-clipboard.js +238 -238
- package/dist/canvas/box-selection.js +80 -80
- package/dist/canvas/canvas-editor.js +502 -502
- package/dist/canvas/controller.js +717 -717
- package/dist/canvas/geometry.js +131 -131
- package/dist/canvas/gesture.d.ts +0 -1
- package/dist/canvas/gesture.js +289 -289
- package/dist/canvas/handles.js +212 -212
- package/dist/canvas/menu.js +82 -82
- package/dist/canvas/mode-controller.js +81 -81
- package/dist/canvas/nudge.js +108 -108
- package/dist/canvas/overlay.js +149 -149
- package/dist/canvas/snap.js +132 -132
- package/dist/canvas/viewport.js +112 -112
- package/dist/canvas/z-order.js +171 -171
- package/dist/commands/alignment.js +150 -150
- package/dist/commands/box.js +272 -272
- package/dist/commands/clipboard.js +93 -93
- package/dist/commands/graphemes.js +112 -112
- package/dist/commands/marks.js +153 -153
- package/dist/commands/paragraph-props.js +176 -176
- package/dist/commands/size.js +368 -368
- package/dist/commands/text.js +291 -291
- package/dist/docx.d.ts +1 -0
- package/dist/docx.js +15 -13
- package/dist/editor/editing-host.js +434 -434
- package/dist/element.js +41 -41
- package/dist/fonts/OFL-Carlito.txt +94 -0
- package/dist/fonts/calibri-metrics.d.ts +27 -0
- package/dist/fonts/calibri-metrics.js +88 -0
- package/dist/history/history.js +433 -433
- package/dist/index.js +63 -63
- package/dist/input/beforeinput.js +224 -224
- package/dist/input/clipboard.js +107 -107
- package/dist/input/composition.js +341 -341
- package/dist/input/keymap.js +54 -54
- package/dist/io/ooxml/custgeom.js +72 -72
- package/dist/io/ooxml/drawingml.js +232 -232
- package/dist/io/ooxml/edits.js +67 -67
- package/dist/io/ooxml/export-splice.js +880 -880
- package/dist/io/ooxml/export.js +365 -365
- package/dist/io/ooxml/fill.d.ts +93 -8
- package/dist/io/ooxml/fill.js +618 -562
- package/dist/io/ooxml/fit.d.ts +252 -0
- package/dist/io/ooxml/fit.js +598 -0
- package/dist/io/ooxml/import-gate.js +200 -200
- package/dist/io/ooxml/import.js +691 -691
- package/dist/io/ooxml/inflate.js +126 -126
- package/dist/io/ooxml/media.js +87 -87
- package/dist/io/ooxml/ns.d.ts +2 -0
- package/dist/io/ooxml/ns.js +26 -21
- package/dist/io/ooxml/numbering.js +290 -290
- package/dist/io/ooxml/opc-write.js +171 -171
- package/dist/io/ooxml/provenance.js +93 -93
- package/dist/io/ooxml/recipe.js +708 -708
- package/dist/io/ooxml/relationships.js +59 -59
- package/dist/io/ooxml/run-fonts.d.ts +89 -0
- package/dist/io/ooxml/run-fonts.js +378 -0
- package/dist/io/ooxml/slots.d.ts +4 -0
- package/dist/io/ooxml/slots.js +582 -581
- package/dist/io/ooxml/styles-resolve.js +257 -257
- package/dist/io/ooxml/units.js +43 -43
- package/dist/io/ooxml/value.js +120 -120
- package/dist/io/ooxml/xml-scan.js +616 -616
- package/dist/io/ooxml/zip-container.js +228 -228
- package/dist/io/ooxml/zip-reject.js +106 -106
- package/dist/measure/placement.js +83 -83
- package/dist/measure/report.js +75 -75
- package/dist/model/ids.js +70 -70
- package/dist/model/normalize.js +146 -146
- package/dist/model/position.js +283 -283
- package/dist/model/transaction.js +209 -209
- package/dist/platform/capabilities.js +25 -25
- package/dist/react/index.js +21 -21
- package/dist/selection/controller.js +315 -315
- package/dist/selection/dom-map.js +334 -334
- package/dist/state/editor-state.js +109 -109
- package/dist/styles.css +66 -66
- package/package.json +94 -93
package/dist/canvas/viewport.js
CHANGED
|
@@ -1,112 +1,112 @@
|
|
|
1
|
-
// Pure px (client/viewport) <-> pt (page/model) bridge for the canvas
|
|
2
|
-
// interaction layer (ADR-0018 §9/Karar 9, §9.1).
|
|
3
|
-
//
|
|
4
|
-
// The renderer never computes a scale factor of its own — `syncPages`
|
|
5
|
-
// (src/render/renderer.js) writes raw `pt` CSS strings and lets the browser
|
|
6
|
-
// do px conversion (measured, not assumed: no ptToPx/getBoundingClientRect
|
|
7
|
-
// call exists anywhere in src/ today). This module is the FIRST place that
|
|
8
|
-
// conversion is named, and it takes an already-measured `PageRect` as
|
|
9
|
-
// input rather than measuring the DOM itself — invariant #7 ("the
|
|
10
|
-
// browser's DOM is not the data model") applied to geometry: the real
|
|
11
|
-
// `getBoundingClientRect()` call is a controller-level concern (a later
|
|
12
|
-
// phase's `readPageMetrics()`), enjected in, never called from here.
|
|
13
|
-
//
|
|
14
|
-
// No src/, DOM, or history import — this module is pure data in, data out.
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* @typedef {object} PageRect
|
|
18
|
-
* @property {number} left client px
|
|
19
|
-
* @property {number} top client px
|
|
20
|
-
* @property {number} width client px
|
|
21
|
-
* @property {number} height client px
|
|
22
|
-
* @property {number} scale client px per pt
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* @param {PageRect} pageRect
|
|
27
|
-
* @returns {number}
|
|
28
|
-
*/
|
|
29
|
-
export function pageScale(pageRect) {
|
|
30
|
-
return pageRect.scale;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* A client-space point, converted to page-space pt relative to the page's
|
|
35
|
-
* own top-left corner. The only place a pointer event's clientX/clientY
|
|
36
|
-
* becomes a pt coordinate hit-test.js can use.
|
|
37
|
-
*
|
|
38
|
-
* @param {PageRect} pageRect
|
|
39
|
-
* @param {number} clientX
|
|
40
|
-
* @param {number} clientY
|
|
41
|
-
* @returns {{x: number, y: number}}
|
|
42
|
-
*/
|
|
43
|
-
export function clientToPagePoint(pageRect, clientX, clientY) {
|
|
44
|
-
return {
|
|
45
|
-
x: (clientX - pageRect.left) / pageRect.scale,
|
|
46
|
-
y: (clientY - pageRect.top) / pageRect.scale,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* A page-space pt rect (BoxLayout/geometry.js's `Rect` shape), converted to
|
|
52
|
-
* a client-space px rect — the inverse of `clientToPagePoint`, for drawing
|
|
53
|
-
* a tutamak/overlay (a later phase) over a real box.
|
|
54
|
-
*
|
|
55
|
-
* @param {PageRect} pageRect
|
|
56
|
-
* @param {import("./geometry.js").Rect} rect
|
|
57
|
-
* @returns {{left: number, top: number, width: number, height: number}}
|
|
58
|
-
*/
|
|
59
|
-
export function pageToClientRect(pageRect, rect) {
|
|
60
|
-
return {
|
|
61
|
-
left: pageRect.left + rect.x * pageRect.scale,
|
|
62
|
-
top: pageRect.top + rect.y * pageRect.scale,
|
|
63
|
-
width: rect.cx * pageRect.scale,
|
|
64
|
-
height: rect.cy * pageRect.scale,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Which page (1-based, matching `BoxLayout.page`'s convention) a client
|
|
70
|
-
* point falls on, or null if it lands on none of them — the space between
|
|
71
|
-
* pages, or above/below the whole stack. `pageRects[0]` is page 1,
|
|
72
|
-
* `pageRects[1]` is page 2, and so on: `PageRect` itself carries no page
|
|
73
|
-
* number (it is exactly the five numbers a controller measures for ONE
|
|
74
|
-
* page), so the array's own order carries that instead.
|
|
75
|
-
*
|
|
76
|
-
* @param {PageRect[]} pageRects
|
|
77
|
-
* @param {number} clientX
|
|
78
|
-
* @param {number} clientY
|
|
79
|
-
* @returns {number | null}
|
|
80
|
-
*/
|
|
81
|
-
export function pageAtClientPoint(pageRects, clientX, clientY) {
|
|
82
|
-
for (let i = 0; i < pageRects.length; i += 1) {
|
|
83
|
-
const rect = pageRects[i];
|
|
84
|
-
if (
|
|
85
|
-
clientX >= rect.left &&
|
|
86
|
-
clientX <= rect.left + rect.width &&
|
|
87
|
-
clientY >= rect.top &&
|
|
88
|
-
clientY <= rect.top + rect.height
|
|
89
|
-
) {
|
|
90
|
-
return i + 1;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Observation only, not a rule (ADR-0018 §10.2): whether a page's measured
|
|
98
|
-
* client-px footprint scales its width and height by the SAME factor
|
|
99
|
-
* relative to the page's own pt size. `PageRect.scale` is a single number
|
|
100
|
-
* because every other function here assumes uniform scaling; this is the
|
|
101
|
-
* one place that assumption gets checked rather than silently trusted.
|
|
102
|
-
* Zero means no skew. No consumer reads this value for real behavior, and
|
|
103
|
-
* no mutation is registered for it — see the file header of
|
|
104
|
-
* `test/canvas/viewport.test.js`.
|
|
105
|
-
*
|
|
106
|
-
* @param {{width: number, height: number}} pageRectPx raw client px measurement
|
|
107
|
-
* @param {{width: number, height: number}} pagePt the page's own pt size (Doc.page)
|
|
108
|
-
* @returns {number}
|
|
109
|
-
*/
|
|
110
|
-
export function pageScaleSkew(pageRectPx, pagePt) {
|
|
111
|
-
return pageRectPx.width / pagePt.width - pageRectPx.height / pagePt.height;
|
|
112
|
-
}
|
|
1
|
+
// Pure px (client/viewport) <-> pt (page/model) bridge for the canvas
|
|
2
|
+
// interaction layer (ADR-0018 §9/Karar 9, §9.1).
|
|
3
|
+
//
|
|
4
|
+
// The renderer never computes a scale factor of its own — `syncPages`
|
|
5
|
+
// (src/render/renderer.js) writes raw `pt` CSS strings and lets the browser
|
|
6
|
+
// do px conversion (measured, not assumed: no ptToPx/getBoundingClientRect
|
|
7
|
+
// call exists anywhere in src/ today). This module is the FIRST place that
|
|
8
|
+
// conversion is named, and it takes an already-measured `PageRect` as
|
|
9
|
+
// input rather than measuring the DOM itself — invariant #7 ("the
|
|
10
|
+
// browser's DOM is not the data model") applied to geometry: the real
|
|
11
|
+
// `getBoundingClientRect()` call is a controller-level concern (a later
|
|
12
|
+
// phase's `readPageMetrics()`), enjected in, never called from here.
|
|
13
|
+
//
|
|
14
|
+
// No src/, DOM, or history import — this module is pure data in, data out.
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {object} PageRect
|
|
18
|
+
* @property {number} left client px
|
|
19
|
+
* @property {number} top client px
|
|
20
|
+
* @property {number} width client px
|
|
21
|
+
* @property {number} height client px
|
|
22
|
+
* @property {number} scale client px per pt
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {PageRect} pageRect
|
|
27
|
+
* @returns {number}
|
|
28
|
+
*/
|
|
29
|
+
export function pageScale(pageRect) {
|
|
30
|
+
return pageRect.scale;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A client-space point, converted to page-space pt relative to the page's
|
|
35
|
+
* own top-left corner. The only place a pointer event's clientX/clientY
|
|
36
|
+
* becomes a pt coordinate hit-test.js can use.
|
|
37
|
+
*
|
|
38
|
+
* @param {PageRect} pageRect
|
|
39
|
+
* @param {number} clientX
|
|
40
|
+
* @param {number} clientY
|
|
41
|
+
* @returns {{x: number, y: number}}
|
|
42
|
+
*/
|
|
43
|
+
export function clientToPagePoint(pageRect, clientX, clientY) {
|
|
44
|
+
return {
|
|
45
|
+
x: (clientX - pageRect.left) / pageRect.scale,
|
|
46
|
+
y: (clientY - pageRect.top) / pageRect.scale,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A page-space pt rect (BoxLayout/geometry.js's `Rect` shape), converted to
|
|
52
|
+
* a client-space px rect — the inverse of `clientToPagePoint`, for drawing
|
|
53
|
+
* a tutamak/overlay (a later phase) over a real box.
|
|
54
|
+
*
|
|
55
|
+
* @param {PageRect} pageRect
|
|
56
|
+
* @param {import("./geometry.js").Rect} rect
|
|
57
|
+
* @returns {{left: number, top: number, width: number, height: number}}
|
|
58
|
+
*/
|
|
59
|
+
export function pageToClientRect(pageRect, rect) {
|
|
60
|
+
return {
|
|
61
|
+
left: pageRect.left + rect.x * pageRect.scale,
|
|
62
|
+
top: pageRect.top + rect.y * pageRect.scale,
|
|
63
|
+
width: rect.cx * pageRect.scale,
|
|
64
|
+
height: rect.cy * pageRect.scale,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Which page (1-based, matching `BoxLayout.page`'s convention) a client
|
|
70
|
+
* point falls on, or null if it lands on none of them — the space between
|
|
71
|
+
* pages, or above/below the whole stack. `pageRects[0]` is page 1,
|
|
72
|
+
* `pageRects[1]` is page 2, and so on: `PageRect` itself carries no page
|
|
73
|
+
* number (it is exactly the five numbers a controller measures for ONE
|
|
74
|
+
* page), so the array's own order carries that instead.
|
|
75
|
+
*
|
|
76
|
+
* @param {PageRect[]} pageRects
|
|
77
|
+
* @param {number} clientX
|
|
78
|
+
* @param {number} clientY
|
|
79
|
+
* @returns {number | null}
|
|
80
|
+
*/
|
|
81
|
+
export function pageAtClientPoint(pageRects, clientX, clientY) {
|
|
82
|
+
for (let i = 0; i < pageRects.length; i += 1) {
|
|
83
|
+
const rect = pageRects[i];
|
|
84
|
+
if (
|
|
85
|
+
clientX >= rect.left &&
|
|
86
|
+
clientX <= rect.left + rect.width &&
|
|
87
|
+
clientY >= rect.top &&
|
|
88
|
+
clientY <= rect.top + rect.height
|
|
89
|
+
) {
|
|
90
|
+
return i + 1;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Observation only, not a rule (ADR-0018 §10.2): whether a page's measured
|
|
98
|
+
* client-px footprint scales its width and height by the SAME factor
|
|
99
|
+
* relative to the page's own pt size. `PageRect.scale` is a single number
|
|
100
|
+
* because every other function here assumes uniform scaling; this is the
|
|
101
|
+
* one place that assumption gets checked rather than silently trusted.
|
|
102
|
+
* Zero means no skew. No consumer reads this value for real behavior, and
|
|
103
|
+
* no mutation is registered for it — see the file header of
|
|
104
|
+
* `test/canvas/viewport.test.js`.
|
|
105
|
+
*
|
|
106
|
+
* @param {{width: number, height: number}} pageRectPx raw client px measurement
|
|
107
|
+
* @param {{width: number, height: number}} pagePt the page's own pt size (Doc.page)
|
|
108
|
+
* @returns {number}
|
|
109
|
+
*/
|
|
110
|
+
export function pageScaleSkew(pageRectPx, pagePt) {
|
|
111
|
+
return pageRectPx.width / pagePt.width - pageRectPx.height / pagePt.height;
|
|
112
|
+
}
|
package/dist/canvas/z-order.js
CHANGED
|
@@ -1,171 +1,171 @@
|
|
|
1
|
-
// Which z values a "bring to front / send to back / step forward / step
|
|
2
|
-
// backward" command should write (ADR-0018 §8, §9/Karar 9). Pure planning —
|
|
3
|
-
// the caller feeds the returned entries straight to `setBoxZOrder`
|
|
4
|
-
// (src/commands/box.js). No src/, DOM, or history import.
|
|
5
|
-
//
|
|
6
|
-
// Reference set is CLOSED (§8, measured): boxes UNION decorations.
|
|
7
|
-
// `Renderer.writeLayout`/`syncDecorations` write `zIndex` onto the SAME
|
|
8
|
-
// page element's children, so a "bring to front" that ignored decorations
|
|
9
|
-
// could leave a box BEHIND a decoration that is already above it.
|
|
10
|
-
// `pageOccupants` is called with no `kind` filter for the same reason
|
|
11
|
-
// snap.js does not filter it — the "is this a box or a decoration"
|
|
12
|
-
// question stays in ONE file (page.js).
|
|
13
|
-
//
|
|
14
|
-
// z is a SPARSE integer, never renumbered: reassigning every occupant on a
|
|
15
|
-
// page for a single "bring to front" would turn one user gesture into a
|
|
16
|
-
// step touching every box on the page. Decorations' own z never appears in
|
|
17
|
-
// the returned entries (ADR-0015: it never changes) — they are read here
|
|
18
|
-
// only as part of the max/min reference set.
|
|
19
|
-
//
|
|
20
|
-
// `selectedIds` is assumed to name only boxes, never decorations — a
|
|
21
|
-
// decoration cannot be selected in the first place (ADR-0015: not a
|
|
22
|
-
// hit-test target), so this is a caller invariant, not something this file
|
|
23
|
-
// re-validates.
|
|
24
|
-
|
|
25
|
-
import { pageOccupants } from "./page.js";
|
|
26
|
-
import { stackOrder } from "./hit-test.js";
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* @typedef {"front" | "back" | "forward" | "backward"} ZOrderOp
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* @param {number} minZ
|
|
34
|
-
* @returns {void}
|
|
35
|
-
*/
|
|
36
|
-
export function requireMinZ(minZ) {
|
|
37
|
-
if (typeof minZ !== "number" || !Number.isFinite(minZ)) {
|
|
38
|
-
throw new TypeError(`zOrderPlan: minZ must be a finite number, got ${JSON.stringify(minZ)}`);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* `front`/`back` for a WHOLE selection at once: every selected occupant
|
|
44
|
-
* gets its OWN new z, consecutive, positioned entirely above (front) or
|
|
45
|
-
* below (back) the current max/min — never a single shared value. A shared
|
|
46
|
-
* value would leave the group's mutual order to whichever occupant happens
|
|
47
|
-
* to have the smaller `index` (its unrelated `doc.content` position),
|
|
48
|
-
* silently discarding the group's actual prior stacking. Values are
|
|
49
|
-
* assigned in the group's own current bottom-to-top order, so the occupant
|
|
50
|
-
* that was already highest among the selection is still the highest once
|
|
51
|
-
* the whole group lands above (or below) everything else.
|
|
52
|
-
*
|
|
53
|
-
* @param {import("./page.js").PageOccupant[]} occupants the WHOLE page's reference set (boxes + decorations)
|
|
54
|
-
* @param {import("./page.js").PageOccupant[]} ordered `occupants`, `stackOrder`'d (top-to-bottom)
|
|
55
|
-
* @param {Set<import("../model/node.js").NodeId>} selected
|
|
56
|
-
* @param {"front" | "back"} end
|
|
57
|
-
* @param {number} minZ
|
|
58
|
-
* @returns {import("../commands/box.js").BoxZOrderEntry[]}
|
|
59
|
-
*/
|
|
60
|
-
function edgePlan(occupants, ordered, selected, end, minZ) {
|
|
61
|
-
const selectedBottomToTop = ordered.filter((occupant) => selected.has(occupant.id)).reverse();
|
|
62
|
-
const k = selectedBottomToTop.length;
|
|
63
|
-
if (k === 0) {
|
|
64
|
-
return [];
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const zs = occupants.map((occupant) => occupant.z);
|
|
68
|
-
if (end === "front") {
|
|
69
|
-
const base = Math.max(...zs) + 1;
|
|
70
|
-
return selectedBottomToTop.map((occupant, i) => ({ containerId: occupant.id, z: base + i }));
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// back — the group's lowest member would land at min(zs) - k; ADR-0018
|
|
74
|
-
// §4.3b's floor-clamp pattern (CANV14) applied to z instead of cx/cy:
|
|
75
|
-
// never let that go below minZ, shifting the WHOLE group up together so
|
|
76
|
-
// it stays a contiguous, distinct block instead of several members
|
|
77
|
-
// collapsing onto the same clamped value.
|
|
78
|
-
const base = Math.max(minZ, Math.min(...zs) - k);
|
|
79
|
-
return selectedBottomToTop.map((occupant, i) => ({ containerId: occupant.id, z: base + i }));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* `forward`/`backward` for a WHOLE selection: each selected occupant swaps
|
|
84
|
-
* z with its immediate neighbor in the CURRENT stacking order — `step: -1`
|
|
85
|
-
* moves toward the front (swap with the occupant just above), `step: +1`
|
|
86
|
-
* moves toward the back (swap with the occupant just below). An occupant
|
|
87
|
-
* already at that edge of the page has no neighbor to swap with and is
|
|
88
|
-
* left out of the result entirely.
|
|
89
|
-
*
|
|
90
|
-
* When two selected occupants are immediately adjacent to EACH OTHER, the
|
|
91
|
-
* one further from the edge sits out this step rather than swapping with
|
|
92
|
-
* its selected neighbor — trading two selected occupants' z values would
|
|
93
|
-
* not change their relative order at all, and a swap needs a genuine
|
|
94
|
-
* (non-selected) neighbor to make progress against. A block of N
|
|
95
|
-
* consecutive selected occupants therefore advances one member per call
|
|
96
|
-
* rather than shifting as a unit. Two DIFFERENT selected occupants can
|
|
97
|
-
* never target the same neighbor position in one call (each position has a
|
|
98
|
-
* unique -1/+1 partner), so this never produces two conflicting entries
|
|
99
|
-
* for the same id.
|
|
100
|
-
*
|
|
101
|
-
* A DECORATION never becomes a swap partner: its own z must never appear
|
|
102
|
-
* in the returned entries (ADR-0015 — no step ever targets one; `box.js`'s
|
|
103
|
-
* `setBoxZOrder` would in fact silently no-op the WHOLE batch if it did,
|
|
104
|
-
* since a decoration has no `doc.layout` entry). When the immediate
|
|
105
|
-
* neighbor in `step`'s direction is a decoration, the search keeps walking
|
|
106
|
-
* past it to the nearest actual BOX — decorations are fixed scenery a box
|
|
107
|
-
* steps over, not something a step swaps with. A box's z crossing a
|
|
108
|
-
* decoration's is itself enough to change their relative order; the
|
|
109
|
-
* decoration's own z does not need to (and must not) move for that.
|
|
110
|
-
*
|
|
111
|
-
* @param {import("./page.js").PageOccupant[]} ordered `stackOrder`'d, top-to-bottom
|
|
112
|
-
* @param {Set<import("../model/node.js").NodeId>} selected
|
|
113
|
-
* @param {-1 | 1} step
|
|
114
|
-
* @returns {import("../commands/box.js").BoxZOrderEntry[]}
|
|
115
|
-
*/
|
|
116
|
-
function swapPlan(ordered, selected, step) {
|
|
117
|
-
/** @type {import("../commands/box.js").BoxZOrderEntry[]} */
|
|
118
|
-
const entries = [];
|
|
119
|
-
for (let position = 0; position < ordered.length; position += 1) {
|
|
120
|
-
const occupant = ordered[position];
|
|
121
|
-
if (!selected.has(occupant.id)) {
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
let partnerPosition = position + step;
|
|
125
|
-
while (partnerPosition >= 0 && partnerPosition < ordered.length && ordered[partnerPosition].kind === "decoration") {
|
|
126
|
-
partnerPosition += step;
|
|
127
|
-
}
|
|
128
|
-
if (partnerPosition < 0 || partnerPosition >= ordered.length) {
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
const partner = ordered[partnerPosition];
|
|
132
|
-
if (selected.has(partner.id)) {
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
entries.push({ containerId: occupant.id, z: partner.z });
|
|
136
|
-
entries.push({ containerId: partner.id, z: occupant.z });
|
|
137
|
-
}
|
|
138
|
-
return entries;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* The `BoxZOrderEntry[]` a z-order command should write, ready for
|
|
143
|
-
* `setBoxZOrder` (`src/commands/box.js`) — never renumbers an occupant this
|
|
144
|
-
* op did not target.
|
|
145
|
-
*
|
|
146
|
-
* @param {import("../model/node.js").Doc} doc
|
|
147
|
-
* @param {number} page
|
|
148
|
-
* @param {import("../model/node.js").NodeId[]} selectedIds
|
|
149
|
-
* @param {ZOrderOp} op
|
|
150
|
-
* @param {number} minZ pt — injected, no default (ADR-0018 §8, unmeasured: only `back`'s floor is decided open; front/forward/backward do not consult it)
|
|
151
|
-
* @returns {import("../commands/box.js").BoxZOrderEntry[]}
|
|
152
|
-
*/
|
|
153
|
-
export function zOrderPlan(doc, page, selectedIds, op, minZ) {
|
|
154
|
-
requireMinZ(minZ);
|
|
155
|
-
const occupants = pageOccupants(doc, page);
|
|
156
|
-
const selected = new Set(selectedIds);
|
|
157
|
-
const ordered = stackOrder(occupants);
|
|
158
|
-
|
|
159
|
-
switch (op) {
|
|
160
|
-
case "front":
|
|
161
|
-
return edgePlan(occupants, ordered, selected, "front", minZ);
|
|
162
|
-
case "back":
|
|
163
|
-
return edgePlan(occupants, ordered, selected, "back", minZ);
|
|
164
|
-
case "forward":
|
|
165
|
-
return swapPlan(ordered, selected, -1);
|
|
166
|
-
case "backward":
|
|
167
|
-
return swapPlan(ordered, selected, 1);
|
|
168
|
-
default:
|
|
169
|
-
throw new TypeError(`zOrderPlan: unknown op ${JSON.stringify(op)}`);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
1
|
+
// Which z values a "bring to front / send to back / step forward / step
|
|
2
|
+
// backward" command should write (ADR-0018 §8, §9/Karar 9). Pure planning —
|
|
3
|
+
// the caller feeds the returned entries straight to `setBoxZOrder`
|
|
4
|
+
// (src/commands/box.js). No src/, DOM, or history import.
|
|
5
|
+
//
|
|
6
|
+
// Reference set is CLOSED (§8, measured): boxes UNION decorations.
|
|
7
|
+
// `Renderer.writeLayout`/`syncDecorations` write `zIndex` onto the SAME
|
|
8
|
+
// page element's children, so a "bring to front" that ignored decorations
|
|
9
|
+
// could leave a box BEHIND a decoration that is already above it.
|
|
10
|
+
// `pageOccupants` is called with no `kind` filter for the same reason
|
|
11
|
+
// snap.js does not filter it — the "is this a box or a decoration"
|
|
12
|
+
// question stays in ONE file (page.js).
|
|
13
|
+
//
|
|
14
|
+
// z is a SPARSE integer, never renumbered: reassigning every occupant on a
|
|
15
|
+
// page for a single "bring to front" would turn one user gesture into a
|
|
16
|
+
// step touching every box on the page. Decorations' own z never appears in
|
|
17
|
+
// the returned entries (ADR-0015: it never changes) — they are read here
|
|
18
|
+
// only as part of the max/min reference set.
|
|
19
|
+
//
|
|
20
|
+
// `selectedIds` is assumed to name only boxes, never decorations — a
|
|
21
|
+
// decoration cannot be selected in the first place (ADR-0015: not a
|
|
22
|
+
// hit-test target), so this is a caller invariant, not something this file
|
|
23
|
+
// re-validates.
|
|
24
|
+
|
|
25
|
+
import { pageOccupants } from "./page.js";
|
|
26
|
+
import { stackOrder } from "./hit-test.js";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {"front" | "back" | "forward" | "backward"} ZOrderOp
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {number} minZ
|
|
34
|
+
* @returns {void}
|
|
35
|
+
*/
|
|
36
|
+
export function requireMinZ(minZ) {
|
|
37
|
+
if (typeof minZ !== "number" || !Number.isFinite(minZ)) {
|
|
38
|
+
throw new TypeError(`zOrderPlan: minZ must be a finite number, got ${JSON.stringify(minZ)}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* `front`/`back` for a WHOLE selection at once: every selected occupant
|
|
44
|
+
* gets its OWN new z, consecutive, positioned entirely above (front) or
|
|
45
|
+
* below (back) the current max/min — never a single shared value. A shared
|
|
46
|
+
* value would leave the group's mutual order to whichever occupant happens
|
|
47
|
+
* to have the smaller `index` (its unrelated `doc.content` position),
|
|
48
|
+
* silently discarding the group's actual prior stacking. Values are
|
|
49
|
+
* assigned in the group's own current bottom-to-top order, so the occupant
|
|
50
|
+
* that was already highest among the selection is still the highest once
|
|
51
|
+
* the whole group lands above (or below) everything else.
|
|
52
|
+
*
|
|
53
|
+
* @param {import("./page.js").PageOccupant[]} occupants the WHOLE page's reference set (boxes + decorations)
|
|
54
|
+
* @param {import("./page.js").PageOccupant[]} ordered `occupants`, `stackOrder`'d (top-to-bottom)
|
|
55
|
+
* @param {Set<import("../model/node.js").NodeId>} selected
|
|
56
|
+
* @param {"front" | "back"} end
|
|
57
|
+
* @param {number} minZ
|
|
58
|
+
* @returns {import("../commands/box.js").BoxZOrderEntry[]}
|
|
59
|
+
*/
|
|
60
|
+
function edgePlan(occupants, ordered, selected, end, minZ) {
|
|
61
|
+
const selectedBottomToTop = ordered.filter((occupant) => selected.has(occupant.id)).reverse();
|
|
62
|
+
const k = selectedBottomToTop.length;
|
|
63
|
+
if (k === 0) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const zs = occupants.map((occupant) => occupant.z);
|
|
68
|
+
if (end === "front") {
|
|
69
|
+
const base = Math.max(...zs) + 1;
|
|
70
|
+
return selectedBottomToTop.map((occupant, i) => ({ containerId: occupant.id, z: base + i }));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// back — the group's lowest member would land at min(zs) - k; ADR-0018
|
|
74
|
+
// §4.3b's floor-clamp pattern (CANV14) applied to z instead of cx/cy:
|
|
75
|
+
// never let that go below minZ, shifting the WHOLE group up together so
|
|
76
|
+
// it stays a contiguous, distinct block instead of several members
|
|
77
|
+
// collapsing onto the same clamped value.
|
|
78
|
+
const base = Math.max(minZ, Math.min(...zs) - k);
|
|
79
|
+
return selectedBottomToTop.map((occupant, i) => ({ containerId: occupant.id, z: base + i }));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* `forward`/`backward` for a WHOLE selection: each selected occupant swaps
|
|
84
|
+
* z with its immediate neighbor in the CURRENT stacking order — `step: -1`
|
|
85
|
+
* moves toward the front (swap with the occupant just above), `step: +1`
|
|
86
|
+
* moves toward the back (swap with the occupant just below). An occupant
|
|
87
|
+
* already at that edge of the page has no neighbor to swap with and is
|
|
88
|
+
* left out of the result entirely.
|
|
89
|
+
*
|
|
90
|
+
* When two selected occupants are immediately adjacent to EACH OTHER, the
|
|
91
|
+
* one further from the edge sits out this step rather than swapping with
|
|
92
|
+
* its selected neighbor — trading two selected occupants' z values would
|
|
93
|
+
* not change their relative order at all, and a swap needs a genuine
|
|
94
|
+
* (non-selected) neighbor to make progress against. A block of N
|
|
95
|
+
* consecutive selected occupants therefore advances one member per call
|
|
96
|
+
* rather than shifting as a unit. Two DIFFERENT selected occupants can
|
|
97
|
+
* never target the same neighbor position in one call (each position has a
|
|
98
|
+
* unique -1/+1 partner), so this never produces two conflicting entries
|
|
99
|
+
* for the same id.
|
|
100
|
+
*
|
|
101
|
+
* A DECORATION never becomes a swap partner: its own z must never appear
|
|
102
|
+
* in the returned entries (ADR-0015 — no step ever targets one; `box.js`'s
|
|
103
|
+
* `setBoxZOrder` would in fact silently no-op the WHOLE batch if it did,
|
|
104
|
+
* since a decoration has no `doc.layout` entry). When the immediate
|
|
105
|
+
* neighbor in `step`'s direction is a decoration, the search keeps walking
|
|
106
|
+
* past it to the nearest actual BOX — decorations are fixed scenery a box
|
|
107
|
+
* steps over, not something a step swaps with. A box's z crossing a
|
|
108
|
+
* decoration's is itself enough to change their relative order; the
|
|
109
|
+
* decoration's own z does not need to (and must not) move for that.
|
|
110
|
+
*
|
|
111
|
+
* @param {import("./page.js").PageOccupant[]} ordered `stackOrder`'d, top-to-bottom
|
|
112
|
+
* @param {Set<import("../model/node.js").NodeId>} selected
|
|
113
|
+
* @param {-1 | 1} step
|
|
114
|
+
* @returns {import("../commands/box.js").BoxZOrderEntry[]}
|
|
115
|
+
*/
|
|
116
|
+
function swapPlan(ordered, selected, step) {
|
|
117
|
+
/** @type {import("../commands/box.js").BoxZOrderEntry[]} */
|
|
118
|
+
const entries = [];
|
|
119
|
+
for (let position = 0; position < ordered.length; position += 1) {
|
|
120
|
+
const occupant = ordered[position];
|
|
121
|
+
if (!selected.has(occupant.id)) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
let partnerPosition = position + step;
|
|
125
|
+
while (partnerPosition >= 0 && partnerPosition < ordered.length && ordered[partnerPosition].kind === "decoration") {
|
|
126
|
+
partnerPosition += step;
|
|
127
|
+
}
|
|
128
|
+
if (partnerPosition < 0 || partnerPosition >= ordered.length) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const partner = ordered[partnerPosition];
|
|
132
|
+
if (selected.has(partner.id)) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
entries.push({ containerId: occupant.id, z: partner.z });
|
|
136
|
+
entries.push({ containerId: partner.id, z: occupant.z });
|
|
137
|
+
}
|
|
138
|
+
return entries;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The `BoxZOrderEntry[]` a z-order command should write, ready for
|
|
143
|
+
* `setBoxZOrder` (`src/commands/box.js`) — never renumbers an occupant this
|
|
144
|
+
* op did not target.
|
|
145
|
+
*
|
|
146
|
+
* @param {import("../model/node.js").Doc} doc
|
|
147
|
+
* @param {number} page
|
|
148
|
+
* @param {import("../model/node.js").NodeId[]} selectedIds
|
|
149
|
+
* @param {ZOrderOp} op
|
|
150
|
+
* @param {number} minZ pt — injected, no default (ADR-0018 §8, unmeasured: only `back`'s floor is decided open; front/forward/backward do not consult it)
|
|
151
|
+
* @returns {import("../commands/box.js").BoxZOrderEntry[]}
|
|
152
|
+
*/
|
|
153
|
+
export function zOrderPlan(doc, page, selectedIds, op, minZ) {
|
|
154
|
+
requireMinZ(minZ);
|
|
155
|
+
const occupants = pageOccupants(doc, page);
|
|
156
|
+
const selected = new Set(selectedIds);
|
|
157
|
+
const ordered = stackOrder(occupants);
|
|
158
|
+
|
|
159
|
+
switch (op) {
|
|
160
|
+
case "front":
|
|
161
|
+
return edgePlan(occupants, ordered, selected, "front", minZ);
|
|
162
|
+
case "back":
|
|
163
|
+
return edgePlan(occupants, ordered, selected, "back", minZ);
|
|
164
|
+
case "forward":
|
|
165
|
+
return swapPlan(ordered, selected, -1);
|
|
166
|
+
case "backward":
|
|
167
|
+
return swapPlan(ordered, selected, 1);
|
|
168
|
+
default:
|
|
169
|
+
throw new TypeError(`zOrderPlan: unknown op ${JSON.stringify(op)}`);
|
|
170
|
+
}
|
|
171
|
+
}
|