@formicoidea/labre-core 0.36.0 → 0.38.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_pkgs/affine-block-frame/frame-manager.d.ts +31 -0
- package/dist/_pkgs/affine-block-frame/frame-manager.js +62 -2
- package/dist/_pkgs/affine-block-frame/frame-tool.js +6 -2
- package/dist/_pkgs/affine-block-surface/extensions/audit.js +12 -34
- package/dist/_pkgs/affine-block-surface/extensions/reading-widget.d.ts +25 -13
- package/dist/_pkgs/affine-block-surface/extensions/reading-widget.js +58 -24
- package/dist/_pkgs/affine-block-surface/extensions/reading.d.ts +83 -7
- package/dist/_pkgs/affine-block-surface/extensions/reading.js +51 -42
- package/dist/_pkgs/affine-block-surface/extensions/validation.d.ts +321 -16
- package/dist/_pkgs/affine-block-surface/extensions/validation.js +945 -62
- package/dist/_pkgs/affine-block-surface/framework-background/facts.d.ts +24 -0
- package/dist/_pkgs/affine-block-surface/framework-background/facts.js +31 -0
- package/dist/_pkgs/affine-block-surface/framework-background/index.d.ts +1 -0
- package/dist/_pkgs/affine-block-surface/framework-background/index.js +1 -0
- package/dist/_pkgs/affine-block-surface/framework-background/stacking.d.ts +131 -0
- package/dist/_pkgs/affine-block-surface/framework-background/stacking.js +184 -0
- package/dist/_pkgs/affine-block-surface/utils/sort.d.ts +15 -1
- package/dist/_pkgs/affine-block-surface/utils/sort.js +30 -31
- package/dist/_pkgs/affine-block-surface/view.js +5 -0
- package/dist/_pkgs/affine-gfx-group/element-renderer/utils.js +3 -1
- package/dist/_pkgs/affine-gfx-template/index.d.ts +1 -0
- package/dist/_pkgs/affine-gfx-template/index.js +1 -0
- package/dist/_pkgs/affine-gfx-template/make-snapshot.d.ts +16 -2
- package/dist/_pkgs/affine-gfx-template/make-snapshot.js +43 -4
- package/dist/_pkgs/affine-gfx-template/services/template-middlewares.js +1 -0
- package/dist/_pkgs/affine-gfx-template/services/template.d.ts +6 -0
- package/dist/_pkgs/affine-gfx-template/services/template.js +6 -0
- package/dist/_pkgs/affine-gfx-template/snapshot-from-action.d.ts +20 -0
- package/dist/_pkgs/affine-gfx-template/snapshot-from-action.js +205 -0
- package/dist/_pkgs/affine-gfx-template/toolbar/template-panel.js +4 -0
- package/dist/_pkgs/affine-gfx-template/toolbar/template-type.d.ts +13 -0
- package/dist/_pkgs/affine-inline-link/link-node/configs/toolbar.d.ts +11 -17
- package/dist/_pkgs/affine-inline-link/link-node/configs/toolbar.js +55 -14
- package/dist/_pkgs/affine-model/elements/cynefin/cynefin.d.ts +9 -20
- package/dist/_pkgs/affine-model/elements/cynefin/cynefin.js +7 -28
- package/dist/_pkgs/affine-model/elements/edgy/board.d.ts +9 -21
- package/dist/_pkgs/affine-model/elements/edgy/board.js +8 -29
- package/dist/_pkgs/affine-model/elements/edgy/facets.d.ts +14 -28
- package/dist/_pkgs/affine-model/elements/edgy/facets.js +12 -36
- package/dist/_pkgs/affine-model/elements/estuarine/estuarine.d.ts +9 -19
- package/dist/_pkgs/affine-model/elements/estuarine/estuarine.js +7 -27
- package/dist/_pkgs/affine-shared/services/link-preview-service/link-preview-service.d.ts +28 -0
- package/dist/_pkgs/affine-shared/services/link-preview-service/link-preview-service.js +20 -3
- package/dist/translations.js +8 -2
- package/package.json +1 -1
|
@@ -61,6 +61,30 @@ export interface BackgroundAxisFact {
|
|
|
61
61
|
*/
|
|
62
62
|
transitionsVisibleProp?: string;
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* The frame that WHOLLY CONTAINS `bound`, or `null` — the one membership test
|
|
66
|
+
* every reader of a framework board uses (PF2.4).
|
|
67
|
+
*
|
|
68
|
+
* An element belongs to a board (a Wardley map, a BPMN pool, a C4 board) only
|
|
69
|
+
* when it is ENTIRELY inside the board's element bound. Not its centre: a node
|
|
70
|
+
* hanging over the edge of a map is a node the author has not finished putting
|
|
71
|
+
* on it, and the validation engine already judges it that way — reading or
|
|
72
|
+
* auditing it as a member would give two answers about one node.
|
|
73
|
+
*
|
|
74
|
+
* The asymmetry with the INNER regions is deliberate: a zone, an evolution
|
|
75
|
+
* stage, a BPMN lane keeps the centre test, because a band is meant to be
|
|
76
|
+
* straddled and a board is not.
|
|
77
|
+
*
|
|
78
|
+
* `Bound.contains` is inclusive on all four edges, so an element whose bound
|
|
79
|
+
* EQUALS the board's is a member — the same answer `containingBackground` gives
|
|
80
|
+
* inside the engine, which is the whole point of having one function.
|
|
81
|
+
*
|
|
82
|
+
* Ties go to the smaller id, never to the order a `Y.Map` was rebuilt in: two
|
|
83
|
+
* overlapping maps must attribute an artefact the same way on every reload.
|
|
84
|
+
*/
|
|
85
|
+
export declare function containingFrame<T extends {
|
|
86
|
+
id: string;
|
|
87
|
+
}>(bound: Bound, candidates: Iterable<T>, boundOf: (candidate: T) => Bound): T | null;
|
|
64
88
|
/** The declared axes, as facts. Empty for a background that declares none. */
|
|
65
89
|
export declare function backgroundAxisFacts(def: FrameworkBackgroundDef): readonly BackgroundAxisFact[];
|
|
66
90
|
/** The declared axis with this id, as a fact. */
|
|
@@ -29,6 +29,37 @@ const FORWARD = {
|
|
|
29
29
|
Object.freeze(FORWARD.horizontal);
|
|
30
30
|
Object.freeze(FORWARD.vertical);
|
|
31
31
|
Object.freeze(FORWARD);
|
|
32
|
+
/**
|
|
33
|
+
* The frame that WHOLLY CONTAINS `bound`, or `null` — the one membership test
|
|
34
|
+
* every reader of a framework board uses (PF2.4).
|
|
35
|
+
*
|
|
36
|
+
* An element belongs to a board (a Wardley map, a BPMN pool, a C4 board) only
|
|
37
|
+
* when it is ENTIRELY inside the board's element bound. Not its centre: a node
|
|
38
|
+
* hanging over the edge of a map is a node the author has not finished putting
|
|
39
|
+
* on it, and the validation engine already judges it that way — reading or
|
|
40
|
+
* auditing it as a member would give two answers about one node.
|
|
41
|
+
*
|
|
42
|
+
* The asymmetry with the INNER regions is deliberate: a zone, an evolution
|
|
43
|
+
* stage, a BPMN lane keeps the centre test, because a band is meant to be
|
|
44
|
+
* straddled and a board is not.
|
|
45
|
+
*
|
|
46
|
+
* `Bound.contains` is inclusive on all four edges, so an element whose bound
|
|
47
|
+
* EQUALS the board's is a member — the same answer `containingBackground` gives
|
|
48
|
+
* inside the engine, which is the whole point of having one function.
|
|
49
|
+
*
|
|
50
|
+
* Ties go to the smaller id, never to the order a `Y.Map` was rebuilt in: two
|
|
51
|
+
* overlapping maps must attribute an artefact the same way on every reload.
|
|
52
|
+
*/
|
|
53
|
+
export function containingFrame(bound, candidates, boundOf) {
|
|
54
|
+
let found = null;
|
|
55
|
+
for (const candidate of candidates) {
|
|
56
|
+
if (!boundOf(candidate).contains(bound))
|
|
57
|
+
continue;
|
|
58
|
+
if (found === null || candidate.id < found.id)
|
|
59
|
+
found = candidate;
|
|
60
|
+
}
|
|
61
|
+
return found;
|
|
62
|
+
}
|
|
32
63
|
/** The declared axes, as facts. Empty for a background that declares none. */
|
|
33
64
|
export function backgroundAxisFacts(def) {
|
|
34
65
|
return (def.axes ?? []).map(axis => ({
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Bound } from '../../global/gfx/index.js';
|
|
2
|
+
import { type GfxController, GfxExtension, type GfxModel } from '../../std/gfx/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* One already-placed element, as {@link indexOverBackgrounds} needs to read it.
|
|
5
|
+
*
|
|
6
|
+
* Three fields and no model, so the placement can be reasoned about — and
|
|
7
|
+
* tested — without a document: what it is, where it is, and how deep. Blocks
|
|
8
|
+
* and canvas elements both reduce to this, which is what lets a frame (a
|
|
9
|
+
* block) and a Wardley zone (a canvas element) share one answer.
|
|
10
|
+
*/
|
|
11
|
+
export interface StackedElement {
|
|
12
|
+
/** Fractional index. Sorts lexicographically, which IS the paint order. */
|
|
13
|
+
index: string;
|
|
14
|
+
/** Serialized box. */
|
|
15
|
+
xywh: string;
|
|
16
|
+
/** Whether this is a framework BACKGROUND — a map, a pool, a C4 board… */
|
|
17
|
+
isBackground: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* How deep something drawn ON a framework board goes: just ABOVE the
|
|
21
|
+
* backgrounds it covers, and below everything else. `null` means the back of
|
|
22
|
+
* the surface.
|
|
23
|
+
*
|
|
24
|
+
* "The back" alone was the first answer and it was wrong, as the recette of
|
|
25
|
+
* #213 found: a Wardley map is a framework BACKGROUND — an opaque one — so an
|
|
26
|
+
* element sent behind the whole surface went behind the map and vanished. What
|
|
27
|
+
* it must be under is the artefacts it groups; what it must be over is the
|
|
28
|
+
* canvas they are drawn on. Those are two different depths, and only the first
|
|
29
|
+
* one is "the back".
|
|
30
|
+
*
|
|
31
|
+
* So: find the topmost background the box actually OVERLAPS — the map it was
|
|
32
|
+
* drawn on, and not some other framework's board parked elsewhere on the same
|
|
33
|
+
* canvas — and mint a key between it and whatever sits directly above it. With
|
|
34
|
+
* no background under it at all there is nothing to clear, and the back of the
|
|
35
|
+
* surface is right again.
|
|
36
|
+
*
|
|
37
|
+
* Two callers want exactly this depth, for the same reason. A Wardley zone is
|
|
38
|
+
* a wash over the components it names, so it is lowered — but not past the map
|
|
39
|
+
* (#213). A frame is a container that renders behind everything it owns, so it
|
|
40
|
+
* too is lowered — and it too vanished under the map. Same problem, same
|
|
41
|
+
* answer, one function.
|
|
42
|
+
*
|
|
43
|
+
* `siblings` need not be sorted: fractional indexes sort lexicographically, so
|
|
44
|
+
* this sorts them itself and reads the paint order straight off the strings.
|
|
45
|
+
*/
|
|
46
|
+
export declare function indexOverBackgrounds(siblings: readonly StackedElement[], box: Bound): string | null;
|
|
47
|
+
/**
|
|
48
|
+
* Where a top-level element belongs in the stack, once the rule "a framework
|
|
49
|
+
* background is a FLOOR, never a lid" is applied. `null` means "leave it
|
|
50
|
+
* alone".
|
|
51
|
+
*
|
|
52
|
+
* ## The rule
|
|
53
|
+
*
|
|
54
|
+
* A background is the canvas the user draws ON. It must therefore stay under
|
|
55
|
+
* everything that overlaps it, and boards must still be able to overlap each
|
|
56
|
+
* other — a board dropped on a board is a cross-reading, not a mistake. Two
|
|
57
|
+
* cases, and they are the same sentence read from either side:
|
|
58
|
+
*
|
|
59
|
+
* 1. `element` is NOT a background — a shape, a node, a foreign framework's
|
|
60
|
+
* artefact. If a background it overlaps sits ABOVE it, it has been buried:
|
|
61
|
+
* raise it just above that background ({@link indexOverBackgrounds}), which
|
|
62
|
+
* is still below every artefact already drawn there. Otherwise nothing.
|
|
63
|
+
* 2. `element` IS a background. If another background it overlaps sits above
|
|
64
|
+
* it, the same raise applies — superposed boards stack in the order they
|
|
65
|
+
* were placed, each still under its own artefacts. Otherwise, if it covers
|
|
66
|
+
* an artefact it overlaps, it is acting as a lid: lower it to just above
|
|
67
|
+
* the floor it lies on, or to the back of the surface when it lies on bare
|
|
68
|
+
* canvas. Otherwise nothing.
|
|
69
|
+
*
|
|
70
|
+
* `siblings` are the OTHER top-level elements of the surface — canvas elements
|
|
71
|
+
* and blocks alike, since the surface paints both by index — with `element`
|
|
72
|
+
* itself removed. Nested elements are left out by the caller: a group's child
|
|
73
|
+
* is ordered by its ancestor's index, so moving it would change nothing.
|
|
74
|
+
*
|
|
75
|
+
* ## Idempotence, which is what makes undo safe
|
|
76
|
+
*
|
|
77
|
+
* The function never returns the index the element already has, so a second
|
|
78
|
+
* call about the same element on an unchanged board answers `null`: the rule
|
|
79
|
+
* has a fixed point and reaches it in one step. It answers about the element
|
|
80
|
+
* the user just placed or moved, and about that one only — which is what makes
|
|
81
|
+
* two superposed boards settle in the order the gestures happened rather than
|
|
82
|
+
* in some order the surface would have to arbitrate.
|
|
83
|
+
*
|
|
84
|
+
* That is the whole undo/redo argument, and it restates the frame manager's
|
|
85
|
+
* (`_watchFrameMoved`, #223): an index restored by undo is one the rule had
|
|
86
|
+
* already accepted when it was written, so replaying the rule over a restored
|
|
87
|
+
* state writes nothing — no corrupted depth, and no extra entry pushed onto
|
|
88
|
+
* the history stack.
|
|
89
|
+
*/
|
|
90
|
+
export declare function stackingIndexFor(element: StackedElement, siblings: readonly StackedElement[]): string | null;
|
|
91
|
+
/**
|
|
92
|
+
* The top-level elements of a surface as {@link StackedElement}s — blocks as
|
|
93
|
+
* well as canvas elements, since the surface paints both by index — minus the
|
|
94
|
+
* one being placed.
|
|
95
|
+
*
|
|
96
|
+
* Read off `gfx.layer.layers` because that is the list the canvas draws from,
|
|
97
|
+
* and shared because both callers of the depth rules need exactly this list:
|
|
98
|
+
* the frame manager (#223) and {@link BackgroundStackingExtension}.
|
|
99
|
+
*/
|
|
100
|
+
export declare function stackedElementsOf(gfx: GfxController, exclude?: GfxModel): StackedElement[];
|
|
101
|
+
/**
|
|
102
|
+
* Keeps {@link stackingIndexFor}'s rule true on a live surface: a framework
|
|
103
|
+
* background is a floor, never a lid — whatever framework it belongs to, and
|
|
104
|
+
* whichever of the two elements arrived first.
|
|
105
|
+
*
|
|
106
|
+
* ENGINE behaviour, not tooling, so it is registered unconditionally: a board
|
|
107
|
+
* whose framework flag is off still has to sit under what the user drops on it
|
|
108
|
+
* (`docs/adr/0009`).
|
|
109
|
+
*
|
|
110
|
+
* Two gestures produce a wrong depth, and they are the two this listens for:
|
|
111
|
+
* an element CREATED (every creation site mints the top of the stack, so a
|
|
112
|
+
* board drawn last covers what it was drawn around) and an element MOVED (onto
|
|
113
|
+
* a board that is above it, or over free elements it then hides). Anything
|
|
114
|
+
* else — a resize, a colour, a rename — cannot change who covers whom.
|
|
115
|
+
*
|
|
116
|
+
* Frames are BLOCKS: their own two gestures are already handled, at creation
|
|
117
|
+
* by `frameIndexAt` and on a move by `_watchFrameMoved` (#223), so this never
|
|
118
|
+
* writes a frame's index. They are read as siblings all the same — a board
|
|
119
|
+
* lowered under a frame must go under the frame.
|
|
120
|
+
*
|
|
121
|
+
* A remote change is ignored: the client that made it ran this same rule and
|
|
122
|
+
* synced the index it decided on. Applying it again here would be a second
|
|
123
|
+
* writer for one gesture.
|
|
124
|
+
*/
|
|
125
|
+
export declare class BackgroundStackingExtension extends GfxExtension {
|
|
126
|
+
static key: string;
|
|
127
|
+
private readonly _disposable;
|
|
128
|
+
constructor(gfx: GfxController);
|
|
129
|
+
private _restack;
|
|
130
|
+
unmounted(): void;
|
|
131
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { FrameworkBackgroundElementModel } from '../../affine-model/index.js';
|
|
2
|
+
import { DisposableGroup } from '../../global/disposable/index.js';
|
|
3
|
+
import { Bound } from '../../global/gfx/index.js';
|
|
4
|
+
import { generateKeyBetween, GfxExtension, } from '../../std/gfx/index.js';
|
|
5
|
+
/**
|
|
6
|
+
* How deep something drawn ON a framework board goes: just ABOVE the
|
|
7
|
+
* backgrounds it covers, and below everything else. `null` means the back of
|
|
8
|
+
* the surface.
|
|
9
|
+
*
|
|
10
|
+
* "The back" alone was the first answer and it was wrong, as the recette of
|
|
11
|
+
* #213 found: a Wardley map is a framework BACKGROUND — an opaque one — so an
|
|
12
|
+
* element sent behind the whole surface went behind the map and vanished. What
|
|
13
|
+
* it must be under is the artefacts it groups; what it must be over is the
|
|
14
|
+
* canvas they are drawn on. Those are two different depths, and only the first
|
|
15
|
+
* one is "the back".
|
|
16
|
+
*
|
|
17
|
+
* So: find the topmost background the box actually OVERLAPS — the map it was
|
|
18
|
+
* drawn on, and not some other framework's board parked elsewhere on the same
|
|
19
|
+
* canvas — and mint a key between it and whatever sits directly above it. With
|
|
20
|
+
* no background under it at all there is nothing to clear, and the back of the
|
|
21
|
+
* surface is right again.
|
|
22
|
+
*
|
|
23
|
+
* Two callers want exactly this depth, for the same reason. A Wardley zone is
|
|
24
|
+
* a wash over the components it names, so it is lowered — but not past the map
|
|
25
|
+
* (#213). A frame is a container that renders behind everything it owns, so it
|
|
26
|
+
* too is lowered — and it too vanished under the map. Same problem, same
|
|
27
|
+
* answer, one function.
|
|
28
|
+
*
|
|
29
|
+
* `siblings` need not be sorted: fractional indexes sort lexicographically, so
|
|
30
|
+
* this sorts them itself and reads the paint order straight off the strings.
|
|
31
|
+
*/
|
|
32
|
+
export function indexOverBackgrounds(siblings, box) {
|
|
33
|
+
const stack = [...siblings].sort((a, b) => a.index < b.index ? -1 : a.index > b.index ? 1 : 0);
|
|
34
|
+
// The LAST match, which is the topmost: a board with two maps on it gets the
|
|
35
|
+
// element above the one it is actually drawn over, whichever was drawn first.
|
|
36
|
+
const under = stack.reduce((found, element, at) => element.isBackground &&
|
|
37
|
+
Bound.deserialize(element.xywh).isOverlapWithBound(box)
|
|
38
|
+
? at
|
|
39
|
+
: found, -1);
|
|
40
|
+
if (under < 0)
|
|
41
|
+
return null;
|
|
42
|
+
// `null` for the upper bound when the background is the topmost element
|
|
43
|
+
// there is — `generateKeyBetween` reads that as "append after".
|
|
44
|
+
return generateKeyBetween(stack[under].index, stack[under + 1]?.index ?? null);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Where a top-level element belongs in the stack, once the rule "a framework
|
|
48
|
+
* background is a FLOOR, never a lid" is applied. `null` means "leave it
|
|
49
|
+
* alone".
|
|
50
|
+
*
|
|
51
|
+
* ## The rule
|
|
52
|
+
*
|
|
53
|
+
* A background is the canvas the user draws ON. It must therefore stay under
|
|
54
|
+
* everything that overlaps it, and boards must still be able to overlap each
|
|
55
|
+
* other — a board dropped on a board is a cross-reading, not a mistake. Two
|
|
56
|
+
* cases, and they are the same sentence read from either side:
|
|
57
|
+
*
|
|
58
|
+
* 1. `element` is NOT a background — a shape, a node, a foreign framework's
|
|
59
|
+
* artefact. If a background it overlaps sits ABOVE it, it has been buried:
|
|
60
|
+
* raise it just above that background ({@link indexOverBackgrounds}), which
|
|
61
|
+
* is still below every artefact already drawn there. Otherwise nothing.
|
|
62
|
+
* 2. `element` IS a background. If another background it overlaps sits above
|
|
63
|
+
* it, the same raise applies — superposed boards stack in the order they
|
|
64
|
+
* were placed, each still under its own artefacts. Otherwise, if it covers
|
|
65
|
+
* an artefact it overlaps, it is acting as a lid: lower it to just above
|
|
66
|
+
* the floor it lies on, or to the back of the surface when it lies on bare
|
|
67
|
+
* canvas. Otherwise nothing.
|
|
68
|
+
*
|
|
69
|
+
* `siblings` are the OTHER top-level elements of the surface — canvas elements
|
|
70
|
+
* and blocks alike, since the surface paints both by index — with `element`
|
|
71
|
+
* itself removed. Nested elements are left out by the caller: a group's child
|
|
72
|
+
* is ordered by its ancestor's index, so moving it would change nothing.
|
|
73
|
+
*
|
|
74
|
+
* ## Idempotence, which is what makes undo safe
|
|
75
|
+
*
|
|
76
|
+
* The function never returns the index the element already has, so a second
|
|
77
|
+
* call about the same element on an unchanged board answers `null`: the rule
|
|
78
|
+
* has a fixed point and reaches it in one step. It answers about the element
|
|
79
|
+
* the user just placed or moved, and about that one only — which is what makes
|
|
80
|
+
* two superposed boards settle in the order the gestures happened rather than
|
|
81
|
+
* in some order the surface would have to arbitrate.
|
|
82
|
+
*
|
|
83
|
+
* That is the whole undo/redo argument, and it restates the frame manager's
|
|
84
|
+
* (`_watchFrameMoved`, #223): an index restored by undo is one the rule had
|
|
85
|
+
* already accepted when it was written, so replaying the rule over a restored
|
|
86
|
+
* state writes nothing — no corrupted depth, and no extra entry pushed onto
|
|
87
|
+
* the history stack.
|
|
88
|
+
*/
|
|
89
|
+
export function stackingIndexFor(element, siblings) {
|
|
90
|
+
const box = Bound.deserialize(element.xywh);
|
|
91
|
+
const overlapping = siblings.filter(sibling => Bound.deserialize(sibling.xywh).isOverlapWithBound(box));
|
|
92
|
+
const buried = overlapping.some(sibling => sibling.isBackground && sibling.index > element.index);
|
|
93
|
+
const lidding = element.isBackground &&
|
|
94
|
+
overlapping.some(sibling => !sibling.isBackground && sibling.index < element.index);
|
|
95
|
+
const next = buried
|
|
96
|
+
? indexOverBackgrounds(siblings, box)
|
|
97
|
+
: lidding
|
|
98
|
+
? // Down to the floor it lies on, or to the back of the surface when
|
|
99
|
+
// there is none — the depth `createWardleyArea` mints by hand.
|
|
100
|
+
(indexOverBackgrounds(siblings, box) ?? backOf(siblings))
|
|
101
|
+
: null;
|
|
102
|
+
return next === element.index ? null : next;
|
|
103
|
+
}
|
|
104
|
+
/** A key below every sibling: the back of the surface. */
|
|
105
|
+
function backOf(siblings) {
|
|
106
|
+
const lowest = siblings.reduce((min, sibling) => min === null || sibling.index < min ? sibling.index : min, null);
|
|
107
|
+
return lowest === null ? null : generateKeyBetween(null, lowest);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The top-level elements of a surface as {@link StackedElement}s — blocks as
|
|
111
|
+
* well as canvas elements, since the surface paints both by index — minus the
|
|
112
|
+
* one being placed.
|
|
113
|
+
*
|
|
114
|
+
* Read off `gfx.layer.layers` because that is the list the canvas draws from,
|
|
115
|
+
* and shared because both callers of the depth rules need exactly this list:
|
|
116
|
+
* the frame manager (#223) and {@link BackgroundStackingExtension}.
|
|
117
|
+
*/
|
|
118
|
+
export function stackedElementsOf(gfx, exclude) {
|
|
119
|
+
return gfx.layer.layers.reduce((all, layer) => all.concat(layer.elements
|
|
120
|
+
.filter(element => element.group === null && element !== exclude)
|
|
121
|
+
.map(element => ({
|
|
122
|
+
index: element.index,
|
|
123
|
+
xywh: element.xywh,
|
|
124
|
+
isBackground: element instanceof FrameworkBackgroundElementModel,
|
|
125
|
+
}))), []);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Keeps {@link stackingIndexFor}'s rule true on a live surface: a framework
|
|
129
|
+
* background is a floor, never a lid — whatever framework it belongs to, and
|
|
130
|
+
* whichever of the two elements arrived first.
|
|
131
|
+
*
|
|
132
|
+
* ENGINE behaviour, not tooling, so it is registered unconditionally: a board
|
|
133
|
+
* whose framework flag is off still has to sit under what the user drops on it
|
|
134
|
+
* (`docs/adr/0009`).
|
|
135
|
+
*
|
|
136
|
+
* Two gestures produce a wrong depth, and they are the two this listens for:
|
|
137
|
+
* an element CREATED (every creation site mints the top of the stack, so a
|
|
138
|
+
* board drawn last covers what it was drawn around) and an element MOVED (onto
|
|
139
|
+
* a board that is above it, or over free elements it then hides). Anything
|
|
140
|
+
* else — a resize, a colour, a rename — cannot change who covers whom.
|
|
141
|
+
*
|
|
142
|
+
* Frames are BLOCKS: their own two gestures are already handled, at creation
|
|
143
|
+
* by `frameIndexAt` and on a move by `_watchFrameMoved` (#223), so this never
|
|
144
|
+
* writes a frame's index. They are read as siblings all the same — a board
|
|
145
|
+
* lowered under a frame must go under the frame.
|
|
146
|
+
*
|
|
147
|
+
* A remote change is ignored: the client that made it ran this same rule and
|
|
148
|
+
* synced the index it decided on. Applying it again here would be a second
|
|
149
|
+
* writer for one gesture.
|
|
150
|
+
*/
|
|
151
|
+
export class BackgroundStackingExtension extends GfxExtension {
|
|
152
|
+
static { this.key = 'background-stacking'; }
|
|
153
|
+
constructor(gfx) {
|
|
154
|
+
super(gfx);
|
|
155
|
+
this._disposable = new DisposableGroup();
|
|
156
|
+
const surface = gfx.surface;
|
|
157
|
+
if (!surface)
|
|
158
|
+
return;
|
|
159
|
+
this._disposable.add(surface.elementAdded.subscribe(({ id, local }) => {
|
|
160
|
+
if (local)
|
|
161
|
+
this._restack(surface.getElementById(id));
|
|
162
|
+
}));
|
|
163
|
+
this._disposable.add(surface.elementUpdated.subscribe(({ id, props, local }) => {
|
|
164
|
+
if (local && props['xywh'])
|
|
165
|
+
this._restack(surface.getElementById(id));
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
_restack(model) {
|
|
169
|
+
// A nested element is ordered by its ancestor's index — `compare` reads
|
|
170
|
+
// that and ignores the child's own, so restacking it would change nothing.
|
|
171
|
+
if (!model || model.group !== null)
|
|
172
|
+
return;
|
|
173
|
+
const index = stackingIndexFor({
|
|
174
|
+
index: model.index,
|
|
175
|
+
xywh: model.xywh,
|
|
176
|
+
isBackground: model instanceof FrameworkBackgroundElementModel,
|
|
177
|
+
}, stackedElementsOf(this.gfx, model));
|
|
178
|
+
if (index !== null)
|
|
179
|
+
this.gfx.updateElement(model, { index });
|
|
180
|
+
}
|
|
181
|
+
unmounted() {
|
|
182
|
+
this._disposable.dispose();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -2,6 +2,20 @@ export declare function loadingSort<T extends {
|
|
|
2
2
|
id: string;
|
|
3
3
|
deps: string[];
|
|
4
4
|
}>(elements: T[]): T[];
|
|
5
|
+
/**
|
|
6
|
+
* Paint order of two serialized elements, group-aware to ANY depth.
|
|
7
|
+
*
|
|
8
|
+
* `groupIndexMap` maps a child id to its parent group's `{ id, index }`. An
|
|
9
|
+
* element's position is the chain of indexes from its outermost group down to
|
|
10
|
+
* itself, and two chains compare lexicographically — the same order the live
|
|
11
|
+
* layer's `compare` derives from the group tree. A group is a strict prefix of
|
|
12
|
+
* its children's chains, so it sorts before them.
|
|
13
|
+
*
|
|
14
|
+
* It used to look ONE level up only, which made the comparator cyclic on a
|
|
15
|
+
* group inside a group (a leaf of the outer group, a leaf of the inner one and
|
|
16
|
+
* the inner group itself had no consistent order) — and `Array.sort` on a
|
|
17
|
+
* cyclic comparator returns whatever it likes.
|
|
18
|
+
*/
|
|
5
19
|
export declare function sortIndex(a: {
|
|
6
20
|
id: string;
|
|
7
21
|
index: string;
|
|
@@ -11,4 +25,4 @@ export declare function sortIndex(a: {
|
|
|
11
25
|
}, groupIndexMap: Map<string, {
|
|
12
26
|
id: string;
|
|
13
27
|
index: string;
|
|
14
|
-
}>):
|
|
28
|
+
}>): number;
|
|
@@ -38,37 +38,36 @@ export function loadingSort(elements) {
|
|
|
38
38
|
}
|
|
39
39
|
return sortedOrder.map(id => map.get(id));
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Paint order of two serialized elements, group-aware to ANY depth.
|
|
43
|
+
*
|
|
44
|
+
* `groupIndexMap` maps a child id to its parent group's `{ id, index }`. An
|
|
45
|
+
* element's position is the chain of indexes from its outermost group down to
|
|
46
|
+
* itself, and two chains compare lexicographically — the same order the live
|
|
47
|
+
* layer's `compare` derives from the group tree. A group is a strict prefix of
|
|
48
|
+
* its children's chains, so it sorts before them.
|
|
49
|
+
*
|
|
50
|
+
* It used to look ONE level up only, which made the comparator cyclic on a
|
|
51
|
+
* group inside a group (a leaf of the outer group, a leaf of the inner one and
|
|
52
|
+
* the inner group itself had no consistent order) — and `Array.sort` on a
|
|
53
|
+
* cyclic comparator returns whatever it likes.
|
|
54
|
+
*/
|
|
41
55
|
export function sortIndex(a, b, groupIndexMap) {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
? 1
|
|
58
|
-
: aGroupIndex.index === b.index
|
|
59
|
-
? 0
|
|
60
|
-
: aGroupIndex.index > b.index
|
|
61
|
-
? 1
|
|
62
|
-
: -1;
|
|
63
|
-
}
|
|
64
|
-
if (bGroupIndex) {
|
|
65
|
-
return a.id === bGroupIndex.id
|
|
66
|
-
? -1
|
|
67
|
-
: a.index === bGroupIndex.index
|
|
68
|
-
? 0
|
|
69
|
-
: a.index > bGroupIndex.index
|
|
70
|
-
? 1
|
|
71
|
-
: -1;
|
|
56
|
+
const chainOf = (el) => {
|
|
57
|
+
const chain = [el.index];
|
|
58
|
+
const seen = new Set([el.id]);
|
|
59
|
+
for (let group = groupIndexMap.get(el.id); group && !seen.has(group.id); group = groupIndexMap.get(group.id)) {
|
|
60
|
+
seen.add(group.id);
|
|
61
|
+
chain.unshift(group.index);
|
|
62
|
+
}
|
|
63
|
+
return chain;
|
|
64
|
+
};
|
|
65
|
+
const ca = chainOf(a);
|
|
66
|
+
const cb = chainOf(b);
|
|
67
|
+
const depth = Math.min(ca.length, cb.length);
|
|
68
|
+
for (let i = 0; i < depth; i++) {
|
|
69
|
+
if (ca[i] !== cb[i])
|
|
70
|
+
return ca[i] > cb[i] ? 1 : -1;
|
|
72
71
|
}
|
|
73
|
-
return
|
|
72
|
+
return ca.length - cb.length;
|
|
74
73
|
}
|
|
@@ -4,6 +4,7 @@ import { literal } from 'lit/static-html.js';
|
|
|
4
4
|
import { effects } from './effects.js';
|
|
5
5
|
import { auditCommands, EdgelessCRUDExtension, EdgelessLegacySlotExtension, EditPropsMiddlewareBuilder, mapQualityCommands, mapQualityWidget, ReadingManager, readingProposalWidget, readingToolbarExtension, SpotlightManager, ValidationManager, validationExceptionToolbarExtension, ValidationOverlay, violationDetailWidget, } from './extensions/index.js';
|
|
6
6
|
import { ExportManagerExtension } from './extensions/export-manager/export-manager.js';
|
|
7
|
+
import { BackgroundStackingExtension } from './framework-background/stacking.js';
|
|
7
8
|
import { ElementHighlightOverlay } from './renderer/element-highlight-overlay.js';
|
|
8
9
|
import { DefaultTool } from './tool/default-tool.js';
|
|
9
10
|
export class SurfaceViewExtension extends ViewExtensionProvider {
|
|
@@ -31,6 +32,10 @@ export class SurfaceViewExtension extends ViewExtensionProvider {
|
|
|
31
32
|
// too — embedded read-only windows are the API's primary consumer — and
|
|
32
33
|
// paints nothing until a host asks for a highlight.
|
|
33
34
|
context.register(ElementHighlightOverlay);
|
|
35
|
+
// "A framework background is a floor, never a lid" — engine behaviour,
|
|
36
|
+
// so it is registered unconditionally, like the frame manager it mirrors
|
|
37
|
+
// (`docs/adr/0009`: a flag gates tooling, never what a board does).
|
|
38
|
+
context.register(BackgroundStackingExtension);
|
|
34
39
|
// No-op until a framework registers a SpotlightHostExtension.
|
|
35
40
|
context.register(SpotlightManager);
|
|
36
41
|
// No-op until a framework registers a ValidationRuleExtension — which
|
|
@@ -3,7 +3,9 @@ import { FontWeight } from '../../affine-model/index.js';
|
|
|
3
3
|
import { Bound } from '../../global/gfx/index.js';
|
|
4
4
|
import { GROUP_TITLE_FONT, GROUP_TITLE_FONT_SIZE, GROUP_TITLE_OFFSET, GROUP_TITLE_PADDING, } from './consts.js';
|
|
5
5
|
export function titleRenderParams(group, zoom) {
|
|
6
|
-
|
|
6
|
+
// A group loaded without a title (older documents, hand-written templates)
|
|
7
|
+
// must not take the whole canvas down with it.
|
|
8
|
+
let text = (group.title?.toString() ?? '').trim();
|
|
7
9
|
const font = getGroupTitleFont(zoom);
|
|
8
10
|
const lineWidth = getLineWidth(text, font);
|
|
9
11
|
const lineHeight = getLineHeight(GROUP_TITLE_FONT, GROUP_TITLE_FONT_SIZE / zoom, 'normal');
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './contribute.js';
|
|
2
2
|
export * from './make-snapshot.js';
|
|
3
3
|
export * from './services/template.js';
|
|
4
|
+
export * from './snapshot-from-action.js';
|
|
4
5
|
export * from './template-tool.js';
|
|
5
6
|
export * from './toolbar/senior-tool.js';
|
|
6
7
|
export * from './toolbar/template-panel.js';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './contribute.js';
|
|
2
2
|
export * from './make-snapshot.js';
|
|
3
3
|
export * from './services/template.js';
|
|
4
|
+
export * from './snapshot-from-action.js';
|
|
4
5
|
export * from './template-tool.js';
|
|
5
6
|
export * from './toolbar/senior-tool.js';
|
|
6
7
|
export * from './toolbar/template-panel.js';
|
|
@@ -12,11 +12,25 @@ export declare function surfaceText(text: string): {
|
|
|
12
12
|
insert: string;
|
|
13
13
|
}[];
|
|
14
14
|
};
|
|
15
|
+
/**
|
|
16
|
+
* Serialize a plain record into the surface `Y.Map` JSON form used inside
|
|
17
|
+
* snapshots — a group's `children`, a mindmap's node table. Same seam as
|
|
18
|
+
* {@link surfaceText}: what `SurfaceBlockTransformer.toSnapshot` emits.
|
|
19
|
+
*/
|
|
20
|
+
export declare function surfaceYMap(json: Record<string, unknown>): {
|
|
21
|
+
"affine:surface:ymap": boolean;
|
|
22
|
+
json: Record<string, unknown>;
|
|
23
|
+
};
|
|
15
24
|
/**
|
|
16
25
|
* Wrap a surface-elements map into the minimal `page → surface` DocSnapshot a
|
|
17
26
|
* template's `content` expects. Block/element ids are remapped on insert by the
|
|
18
|
-
* template job's `replaceIdMiddleware
|
|
19
|
-
*
|
|
27
|
+
* template job's `replaceIdMiddleware`.
|
|
28
|
+
*
|
|
29
|
+
* Z-order indices are regenerated on insert too, but IN THE SNAPSHOT'S OWN
|
|
30
|
+
* ORDER: `createRegenerateIndexMiddleware` sorts the elements by their `index`
|
|
31
|
+
* (group-aware) and only then mints fresh keys — and it skips any element that
|
|
32
|
+
* carries no `index` at all. So a missing `index` is filled in here, in key
|
|
33
|
+
* (insertion) order, which is the order a creation action added the elements.
|
|
20
34
|
*/
|
|
21
35
|
export declare function makeTemplateSnapshot(elements: SurfaceElementsJSON, title?: string): {
|
|
22
36
|
type: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SURFACE_TEXT_UNIQ_IDENTIFIER } from '../std/gfx/index.js';
|
|
1
|
+
import { generateKeyBetween, SURFACE_TEXT_UNIQ_IDENTIFIER, SURFACE_YMAP_UNIQ_IDENTIFIER, } from '../std/gfx/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* Serialize a plain string into the surface `Y.Text` JSON form used inside
|
|
4
4
|
* snapshots (the shape inner-text / label field). Mirrors what
|
|
@@ -11,13 +11,52 @@ export function surfaceText(text) {
|
|
|
11
11
|
delta: text ? [{ insert: text }] : [],
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Serialize a plain record into the surface `Y.Map` JSON form used inside
|
|
16
|
+
* snapshots — a group's `children`, a mindmap's node table. Same seam as
|
|
17
|
+
* {@link surfaceText}: what `SurfaceBlockTransformer.toSnapshot` emits.
|
|
18
|
+
*/
|
|
19
|
+
export function surfaceYMap(json) {
|
|
20
|
+
return {
|
|
21
|
+
[SURFACE_YMAP_UNIQ_IDENTIFIER]: true,
|
|
22
|
+
json,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
14
25
|
/**
|
|
15
26
|
* Wrap a surface-elements map into the minimal `page → surface` DocSnapshot a
|
|
16
27
|
* template's `content` expects. Block/element ids are remapped on insert by the
|
|
17
|
-
* template job's `replaceIdMiddleware
|
|
18
|
-
*
|
|
28
|
+
* template job's `replaceIdMiddleware`.
|
|
29
|
+
*
|
|
30
|
+
* Z-order indices are regenerated on insert too, but IN THE SNAPSHOT'S OWN
|
|
31
|
+
* ORDER: `createRegenerateIndexMiddleware` sorts the elements by their `index`
|
|
32
|
+
* (group-aware) and only then mints fresh keys — and it skips any element that
|
|
33
|
+
* carries no `index` at all. So a missing `index` is filled in here, in key
|
|
34
|
+
* (insertion) order, which is the order a creation action added the elements.
|
|
19
35
|
*/
|
|
20
36
|
export function makeTemplateSnapshot(elements, title = 'Template') {
|
|
37
|
+
const indexed = {};
|
|
38
|
+
let last = null;
|
|
39
|
+
for (const [id, element] of Object.entries(elements)) {
|
|
40
|
+
const copy = { ...element };
|
|
41
|
+
// The middleware keys its fresh indexes by the element's OWN `id` field,
|
|
42
|
+
// not by the map key: a hand-written element without one would collect
|
|
43
|
+
// every other element's key under `undefined` and land at one shared depth.
|
|
44
|
+
if (copy['id'] === undefined)
|
|
45
|
+
copy['id'] = id;
|
|
46
|
+
// A group's title is a Y.Text the model initialises at creation and reads
|
|
47
|
+
// back unguarded when it paints (`group.title.toString()`): a group written
|
|
48
|
+
// without one lands with no key at all, and the renderer threw on every
|
|
49
|
+
// frame — the inserted map showed its background and nothing else
|
|
50
|
+
// (recette of 09/09/2026).
|
|
51
|
+
if (copy['type'] === 'group' && copy['title'] === undefined) {
|
|
52
|
+
copy['title'] = surfaceText('');
|
|
53
|
+
}
|
|
54
|
+
if (copy['index'] === undefined) {
|
|
55
|
+
last = generateKeyBetween(last, null);
|
|
56
|
+
copy['index'] = last;
|
|
57
|
+
}
|
|
58
|
+
indexed[id] = copy;
|
|
59
|
+
}
|
|
21
60
|
return {
|
|
22
61
|
type: 'page',
|
|
23
62
|
meta: {
|
|
@@ -41,7 +80,7 @@ export function makeTemplateSnapshot(elements, title = 'Template') {
|
|
|
41
80
|
type: 'block',
|
|
42
81
|
id: 'block:template-surface',
|
|
43
82
|
flavour: 'affine:surface',
|
|
44
|
-
props: { elements },
|
|
83
|
+
props: { elements: indexed },
|
|
45
84
|
children: [],
|
|
46
85
|
},
|
|
47
86
|
],
|