@uipath/maestro-builder-sdk 6.66.4 → 6.66.6
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/merge.d.ts +4 -2
- package/dist/merge.js +8 -5
- package/dist/node-placement.d.ts +43 -13
- package/dist/node-placement.js +487 -58
- package/dist/nodes/agent/flow/node-type.js +2 -2
- package/dist/serialize.js +19 -5
- package/package.json +2 -2
package/dist/merge.d.ts
CHANGED
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* what a faithful round-trip of the original produces. When a node in
|
|
19
19
|
* `edited` is byte-equal to its `baseline` counterpart, the user did NOT
|
|
20
20
|
* touch it, so the merge keeps the ORIGINAL node verbatim (its real type,
|
|
21
|
-
* inputs, layout — even for a placeholder
|
|
21
|
+
* inputs, layout — even for a placeholder; its position moves only to make
|
|
22
|
+
* room for an added node, see node-placement.ts). Only nodes the user actually
|
|
22
23
|
* changed (or added) are taken from `edited`, and a changed node takes only
|
|
23
24
|
* the fields that differ from the baseline (see withEdits).
|
|
24
25
|
* - Without a `baseline`, the merge is two-way: it takes `edited`'s graph and
|
|
@@ -27,7 +28,8 @@
|
|
|
27
28
|
*
|
|
28
29
|
* Layout, `metadata`, `solutionId`, `projectId`, `runtime` and `layout` are
|
|
29
30
|
* preserved from the original, apart from the `layout.nodes` entries of
|
|
30
|
-
* removed nodes (dropped) and added nodes
|
|
31
|
+
* removed nodes (dropped) and added nodes, and the positions and container
|
|
32
|
+
* sizes placement moves to make room for them (node-placement.ts); `subflows` and
|
|
31
33
|
* edges (with their ids) come from `edited`; root `variables` merge as a
|
|
32
34
|
* delta per entry, like nodes (merge-variables.ts); `definitions` and
|
|
33
35
|
* `bindings` are unioned
|
package/dist/merge.js
CHANGED
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* what a faithful round-trip of the original produces. When a node in
|
|
19
19
|
* `edited` is byte-equal to its `baseline` counterpart, the user did NOT
|
|
20
20
|
* touch it, so the merge keeps the ORIGINAL node verbatim (its real type,
|
|
21
|
-
* inputs, layout — even for a placeholder
|
|
21
|
+
* inputs, layout — even for a placeholder; its position moves only to make
|
|
22
|
+
* room for an added node, see node-placement.ts). Only nodes the user actually
|
|
22
23
|
* changed (or added) are taken from `edited`, and a changed node takes only
|
|
23
24
|
* the fields that differ from the baseline (see withEdits).
|
|
24
25
|
* - Without a `baseline`, the merge is two-way: it takes `edited`'s graph and
|
|
@@ -27,7 +28,8 @@
|
|
|
27
28
|
*
|
|
28
29
|
* Layout, `metadata`, `solutionId`, `projectId`, `runtime` and `layout` are
|
|
29
30
|
* preserved from the original, apart from the `layout.nodes` entries of
|
|
30
|
-
* removed nodes (dropped) and added nodes
|
|
31
|
+
* removed nodes (dropped) and added nodes, and the positions and container
|
|
32
|
+
* sizes placement moves to make room for them (node-placement.ts); `subflows` and
|
|
31
33
|
* edges (with their ids) come from `edited`; root `variables` merge as a
|
|
32
34
|
* delta per entry, like nodes (merge-variables.ts); `definitions` and
|
|
33
35
|
* `bindings` are unioned
|
|
@@ -432,8 +434,9 @@ function mergeFlowFindings(original, edited, options) {
|
|
|
432
434
|
// ── Placement: a node the edit ADDED has no original position. Unless the
|
|
433
435
|
// author set one, it has compile's placeholder, an emission-index slot
|
|
434
436
|
// that can land on an original node. Put it next to its predecessor,
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
+
// moving the nodes downstream of it and growing its containers to make
|
|
438
|
+
// room, and store every added node's position where the original stores
|
|
439
|
+
// positions — see node-placement.ts. Root scope only: a
|
|
437
440
|
// subflow is taken from `edited` whole, layout included. ──
|
|
438
441
|
placeAddedNodes(finalNodes, result.edges, positionStoreOf(original) && layoutNodesOf(result), (id) => !origNodes.has(id));
|
|
439
442
|
// ── Definitions: the (type, version) pairs used by EVERY node in the file —
|
|
@@ -458,7 +461,7 @@ function mergeFlowFindings(original, edited, options) {
|
|
|
458
461
|
// metadata / solutionId / projectId / layout / runtime were carried by the
|
|
459
462
|
// initial clone of `original` and are intentionally left untouched, apart
|
|
460
463
|
// from the `layout.nodes` entries of removed nodes (dropped above) and added
|
|
461
|
-
// nodes
|
|
464
|
+
// nodes, and the positions and container sizes placement moves to make room.
|
|
462
465
|
//
|
|
463
466
|
// The declared file-format `version` follows the same delta rule the nodes
|
|
464
467
|
// do. The original's wins by default, because a compile of an edited source
|
package/dist/node-placement.d.ts
CHANGED
|
@@ -6,15 +6,34 @@
|
|
|
6
6
|
* `x = 256 + 288 × index, y = 144` (`+160` inside a container). Merge replaces
|
|
7
7
|
* that placeholder on each node the edit added:
|
|
8
8
|
*
|
|
9
|
-
* -
|
|
9
|
+
* - right of its rightmost positioned predecessor in the same container, past
|
|
10
|
+
* its width, on that predecessor's row; when the node leads into another
|
|
11
|
+
* lane (its first successor's row does not meet the predecessor's) and that
|
|
12
|
+
* successor is no join another lane already feeds, on that lane's row; when
|
|
13
|
+
* its direct successor is tucked under the predecessor (a context index
|
|
14
|
+
* under its agent), in that successor's spot, pushing it a slot;
|
|
10
15
|
* - else, when the incoming edge comes from the node's own container (the first
|
|
11
16
|
* node of a loop body): at the body origin, inside the container's padding;
|
|
12
|
-
* - else, one slot left of a positioned successor in the same container
|
|
13
|
-
*
|
|
17
|
+
* - else, one slot left of a positioned successor in the same container, or in
|
|
18
|
+
* its place when that is taken.
|
|
19
|
+
*
|
|
20
|
+
* A new branch whose spot holds another new branch out of the same
|
|
21
|
+
* predecessor takes the next row down instead of pushing it. A sticky note
|
|
22
|
+
* moves with the node it covers most (by id on a tie); one over no node stays.
|
|
23
|
+
*
|
|
24
|
+
* Added nodes are placed in waves by id, each node once every predecessor it
|
|
25
|
+
* has in its container is placed, so the result does not depend on the order
|
|
26
|
+
* of the node or edge arrays, and a join lands right of all its branches.
|
|
27
|
+
*
|
|
28
|
+
* When the spot is taken, or a successor would sit left of the new node, the
|
|
29
|
+
* nodes downstream of the new one, the ones it lands on, and whatever they
|
|
30
|
+
* would land on move right to make room; when one of them has to stay, the
|
|
31
|
+
* new node takes the nearest free spot instead. Each container that gained or
|
|
32
|
+
* moved a child then grows to fit it, and pushes what is past its old edges.
|
|
14
33
|
*
|
|
15
34
|
* Example: the edit inserts `auditLog` between `extractInvoice (448,144)` and
|
|
16
|
-
* `routeInvoice (640,144)`.
|
|
17
|
-
*
|
|
35
|
+
* `routeInvoice (640,144)`. `auditLog` takes `(640,144)`; `routeInvoice` and
|
|
36
|
+
* `end` move 192 right.
|
|
18
37
|
*
|
|
19
38
|
* A child's position is relative to its container, as on the canvas.
|
|
20
39
|
*
|
|
@@ -66,21 +85,32 @@ interface PlacedNode {
|
|
|
66
85
|
[k: string]: unknown;
|
|
67
86
|
}
|
|
68
87
|
/**
|
|
69
|
-
* Give each node the edit added a position next to its predecessor, in place
|
|
88
|
+
* Give each node the edit added a position next to its predecessor, in place,
|
|
89
|
+
* making room for it.
|
|
70
90
|
*
|
|
71
91
|
* A node is placed when it is not in the original and its position is a
|
|
72
|
-
* compile placeholder
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* the
|
|
76
|
-
*
|
|
77
|
-
*
|
|
92
|
+
* compile placeholder; the module header lists where it goes. When the spot is
|
|
93
|
+
* taken, or one of its first positioned successors would sit left of its
|
|
94
|
+
* right edge, the nodes past the insertion point that it lands on, and every
|
|
95
|
+
* node downstream of the new one, move right by the room it needs; so does
|
|
96
|
+
* whatever a moved node would then land on, and its downstream. Downstream
|
|
97
|
+
* reaches through added nodes not placed yet. When that would move a node that
|
|
98
|
+
* has to stay (an author position, an added node that can never be anchored),
|
|
99
|
+
* nothing moves and the node takes the nearest free spot. Then each container
|
|
100
|
+
* that gained or moved a child grows as far as those children need, keeping
|
|
101
|
+
* its padding, and pushes the siblings past its old right or bottom edge the
|
|
102
|
+
* same way, up to the root, keeping at least the row gap to its new edge.
|
|
103
|
+
*
|
|
104
|
+
* An author `.layout()` position is kept when a node is placed, but a growing
|
|
105
|
+
* container moves one in its way or downstream of it past its old edge (it
|
|
106
|
+
* was chosen against the old size), so growth never covers a node.
|
|
78
107
|
*
|
|
79
108
|
* Where the result is written follows the file: when the original stores
|
|
80
109
|
* positions in `layout.nodes` (every canvas-saved file does), every added
|
|
81
110
|
* node's `node.ui`, placed or not, sticky notes included, replaces its
|
|
82
111
|
* `layout.nodes` entry, so the node has one position; otherwise
|
|
83
|
-
* `node.ui.position` is updated.
|
|
112
|
+
* `node.ui.position` is updated. A moved or grown node keeps each field
|
|
113
|
+
* where it already is.
|
|
84
114
|
*
|
|
85
115
|
* @param nodes - The merged root nodes (mutated).
|
|
86
116
|
* @param edges - The merged root edges.
|
package/dist/node-placement.js
CHANGED
|
@@ -6,15 +6,34 @@
|
|
|
6
6
|
* `x = 256 + 288 × index, y = 144` (`+160` inside a container). Merge replaces
|
|
7
7
|
* that placeholder on each node the edit added:
|
|
8
8
|
*
|
|
9
|
-
* -
|
|
9
|
+
* - right of its rightmost positioned predecessor in the same container, past
|
|
10
|
+
* its width, on that predecessor's row; when the node leads into another
|
|
11
|
+
* lane (its first successor's row does not meet the predecessor's) and that
|
|
12
|
+
* successor is no join another lane already feeds, on that lane's row; when
|
|
13
|
+
* its direct successor is tucked under the predecessor (a context index
|
|
14
|
+
* under its agent), in that successor's spot, pushing it a slot;
|
|
10
15
|
* - else, when the incoming edge comes from the node's own container (the first
|
|
11
16
|
* node of a loop body): at the body origin, inside the container's padding;
|
|
12
|
-
* - else, one slot left of a positioned successor in the same container
|
|
13
|
-
*
|
|
17
|
+
* - else, one slot left of a positioned successor in the same container, or in
|
|
18
|
+
* its place when that is taken.
|
|
19
|
+
*
|
|
20
|
+
* A new branch whose spot holds another new branch out of the same
|
|
21
|
+
* predecessor takes the next row down instead of pushing it. A sticky note
|
|
22
|
+
* moves with the node it covers most (by id on a tie); one over no node stays.
|
|
23
|
+
*
|
|
24
|
+
* Added nodes are placed in waves by id, each node once every predecessor it
|
|
25
|
+
* has in its container is placed, so the result does not depend on the order
|
|
26
|
+
* of the node or edge arrays, and a join lands right of all its branches.
|
|
27
|
+
*
|
|
28
|
+
* When the spot is taken, or a successor would sit left of the new node, the
|
|
29
|
+
* nodes downstream of the new one, the ones it lands on, and whatever they
|
|
30
|
+
* would land on move right to make room; when one of them has to stay, the
|
|
31
|
+
* new node takes the nearest free spot instead. Each container that gained or
|
|
32
|
+
* moved a child then grows to fit it, and pushes what is past its old edges.
|
|
14
33
|
*
|
|
15
34
|
* Example: the edit inserts `auditLog` between `extractInvoice (448,144)` and
|
|
16
|
-
* `routeInvoice (640,144)`.
|
|
17
|
-
*
|
|
35
|
+
* `routeInvoice (640,144)`. `auditLog` takes `(640,144)`; `routeInvoice` and
|
|
36
|
+
* `end` move 192 right.
|
|
18
37
|
*
|
|
19
38
|
* A child's position is relative to its container, as on the canvas.
|
|
20
39
|
*
|
|
@@ -37,6 +56,10 @@ const PLACEHOLDER = { x0: 256, dx: 288, y0: 144, nestedDy: 160 };
|
|
|
37
56
|
const DEFAULT_SIZE = 96;
|
|
38
57
|
/** One slot to the right of a node: its width plus an equal gap, the spacing of a canvas row. */
|
|
39
58
|
const SLOT_DX = 192;
|
|
59
|
+
/** The space between two nodes in a canvas row. */
|
|
60
|
+
const GAP = SLOT_DX - DEFAULT_SIZE;
|
|
61
|
+
/** The box a container with no size gets (flow-core's container default). */
|
|
62
|
+
const CONTAINER_SIZE = { width: 560, height: 320 };
|
|
40
63
|
/** flow-core `findFreePosition` defaults. */
|
|
41
64
|
const PADDING = 24;
|
|
42
65
|
const STEP = 40;
|
|
@@ -69,6 +92,8 @@ export function isPlaceholderPosition(p) {
|
|
|
69
92
|
}
|
|
70
93
|
const overlaps = (a, b) => !(a.x + a.width + PADDING <= b.x || b.x + b.width + PADDING <= a.x
|
|
71
94
|
|| a.y + a.height + PADDING <= b.y || b.y + b.height + PADDING <= a.y);
|
|
95
|
+
/** Whether two boxes share a row: their vertical extents meet, padding included. */
|
|
96
|
+
const rowsMeet = (a, b) => a.y < b.y + b.height + PADDING && b.y < a.y + a.height + PADDING;
|
|
72
97
|
/** flow-core `findFreePosition`: the candidate, else the first free spot on rings around it. */
|
|
73
98
|
function findFreePosition(candidate, size, others) {
|
|
74
99
|
const free = (p) => !others.some((o) => overlaps({ ...p, ...size }, o));
|
|
@@ -98,22 +123,41 @@ function moveAddedUi(nodes, layoutNodes, isAdded) {
|
|
|
98
123
|
delete n.ui;
|
|
99
124
|
}
|
|
100
125
|
}
|
|
126
|
+
/** What a push does when a moved node would land on one it may not move. */
|
|
127
|
+
var Blocked;
|
|
128
|
+
(function (Blocked) {
|
|
129
|
+
/** The push is called off: nothing moves. */
|
|
130
|
+
Blocked[Blocked["Refuse"] = 0] = "Refuse";
|
|
131
|
+
/** That node moves too. */
|
|
132
|
+
Blocked[Blocked["Push"] = 1] = "Push";
|
|
133
|
+
})(Blocked || (Blocked = {}));
|
|
101
134
|
/**
|
|
102
|
-
* Give each node the edit added a position next to its predecessor, in place
|
|
135
|
+
* Give each node the edit added a position next to its predecessor, in place,
|
|
136
|
+
* making room for it.
|
|
103
137
|
*
|
|
104
138
|
* A node is placed when it is not in the original and its position is a
|
|
105
|
-
* compile placeholder
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* the
|
|
109
|
-
*
|
|
110
|
-
*
|
|
139
|
+
* compile placeholder; the module header lists where it goes. When the spot is
|
|
140
|
+
* taken, or one of its first positioned successors would sit left of its
|
|
141
|
+
* right edge, the nodes past the insertion point that it lands on, and every
|
|
142
|
+
* node downstream of the new one, move right by the room it needs; so does
|
|
143
|
+
* whatever a moved node would then land on, and its downstream. Downstream
|
|
144
|
+
* reaches through added nodes not placed yet. When that would move a node that
|
|
145
|
+
* has to stay (an author position, an added node that can never be anchored),
|
|
146
|
+
* nothing moves and the node takes the nearest free spot. Then each container
|
|
147
|
+
* that gained or moved a child grows as far as those children need, keeping
|
|
148
|
+
* its padding, and pushes the siblings past its old right or bottom edge the
|
|
149
|
+
* same way, up to the root, keeping at least the row gap to its new edge.
|
|
150
|
+
*
|
|
151
|
+
* An author `.layout()` position is kept when a node is placed, but a growing
|
|
152
|
+
* container moves one in its way or downstream of it past its old edge (it
|
|
153
|
+
* was chosen against the old size), so growth never covers a node.
|
|
111
154
|
*
|
|
112
155
|
* Where the result is written follows the file: when the original stores
|
|
113
156
|
* positions in `layout.nodes` (every canvas-saved file does), every added
|
|
114
157
|
* node's `node.ui`, placed or not, sticky notes included, replaces its
|
|
115
158
|
* `layout.nodes` entry, so the node has one position; otherwise
|
|
116
|
-
* `node.ui.position` is updated.
|
|
159
|
+
* `node.ui.position` is updated. A moved or grown node keeps each field
|
|
160
|
+
* where it already is.
|
|
117
161
|
*
|
|
118
162
|
* @param nodes - The merged root nodes (mutated).
|
|
119
163
|
* @param edges - The merged root edges.
|
|
@@ -123,56 +167,441 @@ function moveAddedUi(nodes, layoutNodes, isAdded) {
|
|
|
123
167
|
export function placeAddedNodes(nodes, edges, layoutNodes, isAdded) {
|
|
124
168
|
if (layoutNodes)
|
|
125
169
|
moveAddedUi(nodes, layoutNodes, isAdded);
|
|
170
|
+
const s = sceneOf(nodes, edges, layoutNodes, isAdded);
|
|
171
|
+
const before = new Map([...s.containers].map((id) => [id, bodyExtent(s, id, (o) => !isAdded(o.id))]));
|
|
172
|
+
placeInWaves(s);
|
|
173
|
+
fitContainers(s, before);
|
|
174
|
+
}
|
|
175
|
+
function sceneOf(nodes, edges, layoutNodes, isAdded) {
|
|
126
176
|
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
127
|
-
const
|
|
128
|
-
|
|
177
|
+
const outOf = new Map();
|
|
178
|
+
const inOf = new Map();
|
|
179
|
+
for (const e of edges) {
|
|
180
|
+
if (!outOf.has(e.sourceNodeId))
|
|
181
|
+
outOf.set(e.sourceNodeId, []);
|
|
182
|
+
if (!inOf.has(e.targetNodeId))
|
|
183
|
+
inOf.set(e.targetNodeId, []);
|
|
184
|
+
outOf.get(e.sourceNodeId).push(e.targetNodeId);
|
|
185
|
+
inOf.get(e.targetNodeId).push(e.sourceNodeId);
|
|
186
|
+
}
|
|
187
|
+
const s = {
|
|
188
|
+
nodes,
|
|
189
|
+
byId,
|
|
190
|
+
layoutNodes,
|
|
191
|
+
isAdded,
|
|
192
|
+
containers: new Set(nodes.flatMap((n) => (n.parentId !== undefined && byId.has(n.parentId) ? [n.parentId] : []))),
|
|
193
|
+
outOf,
|
|
194
|
+
inOf,
|
|
195
|
+
pending: new Set(),
|
|
196
|
+
placed: new Set(),
|
|
197
|
+
anchored: new Set(),
|
|
198
|
+
toFit: new Set(),
|
|
199
|
+
notesOf: new Map(),
|
|
200
|
+
};
|
|
201
|
+
for (const n of nodes) {
|
|
202
|
+
if (isAdded(n.id) && n.type !== STICKY_NOTE_TYPE && isPlaceholderPosition(uiOf(s, n)?.position))
|
|
203
|
+
s.pending.add(n.id);
|
|
204
|
+
}
|
|
205
|
+
// A pending node no edge path links to a positioned one in its container
|
|
206
|
+
// (or to the container's entry) is never placed: it keeps its placeholder,
|
|
207
|
+
// and nothing may land on it.
|
|
208
|
+
const reach = [...nodes.filter((n) => isPositioned(s, n.id)).map((n) => n.id),
|
|
209
|
+
...[...s.pending].filter((id) => enteredFromContainer(s, id))];
|
|
210
|
+
for (const id of reach)
|
|
211
|
+
s.anchored.add(id);
|
|
212
|
+
for (let i = 0; i < reach.length; i++) {
|
|
213
|
+
const scope = byId.get(reach[i])?.parentId;
|
|
214
|
+
for (const t of [...(outOf.get(reach[i]) ?? []), ...(inOf.get(reach[i]) ?? [])]) {
|
|
215
|
+
if (s.anchored.has(t) || !s.pending.has(t) || byId.get(t)?.parentId !== scope)
|
|
216
|
+
continue;
|
|
217
|
+
s.anchored.add(t);
|
|
218
|
+
reach.push(t);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
assignNotes(s);
|
|
222
|
+
return s;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Give each sticky note to the positioned node in its container it covers
|
|
226
|
+
* most, by id on a tie, so the note moves with it; a note over no node stays.
|
|
227
|
+
*/
|
|
228
|
+
function assignNotes(s) {
|
|
229
|
+
const area = (a, b) => Math.max(0, Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x)) * Math.max(0, Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y));
|
|
230
|
+
for (const note of s.nodes) {
|
|
231
|
+
if (note.type !== STICKY_NOTE_TYPE || !hasPosition(s, note))
|
|
232
|
+
continue;
|
|
233
|
+
const r = rectOf(s, note);
|
|
234
|
+
const [owner] = positionedIn(s, note.parentId)
|
|
235
|
+
.map((o) => ({ id: o.id, covered: area(r, rectOf(s, o)) }))
|
|
236
|
+
.filter(({ covered }) => covered > 0)
|
|
237
|
+
.sort((a, b) => b.covered - a.covered || a.id.localeCompare(b.id));
|
|
238
|
+
if (!owner)
|
|
239
|
+
continue;
|
|
240
|
+
if (!s.notesOf.has(owner.id))
|
|
241
|
+
s.notesOf.set(owner.id, []);
|
|
242
|
+
s.notesOf.get(owner.id).push(note.id);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// ── Reading and writing positions ──
|
|
246
|
+
function uiOf(s, n) {
|
|
247
|
+
const entry = s.layoutNodes?.[n.id];
|
|
248
|
+
const own = n.ui;
|
|
249
|
+
return entry ? { ...own, ...entry } : own;
|
|
250
|
+
}
|
|
251
|
+
function sizeOf(s, n) {
|
|
252
|
+
const fallback = s.containers.has(n.id) ? CONTAINER_SIZE : { width: DEFAULT_SIZE, height: DEFAULT_SIZE };
|
|
253
|
+
return { width: uiOf(s, n)?.size?.width ?? fallback.width, height: uiOf(s, n)?.size?.height ?? fallback.height };
|
|
254
|
+
}
|
|
255
|
+
const rectOf = (s, n) => ({ ...uiOf(s, n).position, ...sizeOf(s, n) });
|
|
256
|
+
/** Write each field where it already lives; a new one goes where the file stores positions. */
|
|
257
|
+
function write(s, n, patch) {
|
|
258
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
129
259
|
const own = n.ui;
|
|
130
|
-
|
|
260
|
+
const entry = s.layoutNodes?.[n.id];
|
|
261
|
+
if (s.layoutNodes && (entry?.[key] !== undefined || own?.[key] === undefined))
|
|
262
|
+
s.layoutNodes[n.id] = { ...entry, [key]: value };
|
|
263
|
+
else
|
|
264
|
+
n.ui = { ...own, [key]: value };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/** Move `ids`, and the sticky notes they carry, by `by`. */
|
|
268
|
+
function shift(s, ids, by) {
|
|
269
|
+
for (const id of [...ids, ...[...ids].flatMap((i) => s.notesOf.get(i) ?? [])]) {
|
|
270
|
+
const o = s.byId.get(id);
|
|
271
|
+
const p = uiOf(s, o).position;
|
|
272
|
+
write(s, o, { position: { x: p.x + by.x, y: p.y + by.y } });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// ── What may move ──
|
|
276
|
+
const hasPosition = (s, o) => uiOf(s, o)?.position !== undefined;
|
|
277
|
+
function isPositioned(s, id) {
|
|
278
|
+
const n = s.byId.get(id);
|
|
279
|
+
return n !== undefined && !s.pending.has(id) && hasPosition(s, n);
|
|
280
|
+
}
|
|
281
|
+
/** Positioned, and not an author `.layout()` position: those stay where the author put them. */
|
|
282
|
+
const isMovable = (s, n) => isPositioned(s, n.id) && (!s.isAdded(n.id) || s.placed.has(n.id));
|
|
283
|
+
/** Whether an edge enters `id` from its own container, which is in the scope: the first node of a body. */
|
|
284
|
+
function enteredFromContainer(s, id) {
|
|
285
|
+
const parent = s.byId.get(id)?.parentId;
|
|
286
|
+
return parent !== undefined && s.byId.has(parent) && (s.inOf.get(id) ?? []).includes(parent);
|
|
287
|
+
}
|
|
288
|
+
/** A pending node that will never be placed: it keeps its placeholder. */
|
|
289
|
+
const isFloating = (s, o) => s.pending.has(o.id) && !s.anchored.has(o.id);
|
|
290
|
+
/** The nodes in `scope` a node may not overlap: positioned ones and floating placeholders. Pending ones are placed later, around them. */
|
|
291
|
+
const obstaclesIn = (s, scope, except) => s.nodes.filter((o) => o.id !== except && o.parentId === scope && o.type !== STICKY_NOTE_TYPE && hasPosition(s, o)
|
|
292
|
+
&& (!s.pending.has(o.id) || isFloating(s, o)));
|
|
293
|
+
const positionedIn = (s, scope) => obstaclesIn(s, scope, '').filter((o) => isPositioned(s, o.id));
|
|
294
|
+
/** Every node an edge path from `from` reaches inside its container, through pending ones. */
|
|
295
|
+
function downstreamOf(s, from) {
|
|
296
|
+
const scope = s.byId.get(from)?.parentId;
|
|
297
|
+
const seen = new Set();
|
|
298
|
+
const queue = [from];
|
|
299
|
+
for (let i = 0; i < queue.length; i++) {
|
|
300
|
+
for (const t of s.outOf.get(queue[i]) ?? []) {
|
|
301
|
+
if (t === from || seen.has(t) || !s.byId.has(t) || s.byId.get(t).parentId !== scope)
|
|
302
|
+
continue;
|
|
303
|
+
seen.add(t);
|
|
304
|
+
queue.push(t);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return seen;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* The first positioned nodes `from` reaches in its container, through pending
|
|
311
|
+
* ones, each with how many pending ones lie on the way.
|
|
312
|
+
*/
|
|
313
|
+
function frontierOf(s, from) {
|
|
314
|
+
const scope = s.byId.get(from)?.parentId;
|
|
315
|
+
const found = new Map();
|
|
316
|
+
const hops = new Map([[from, 0]]);
|
|
317
|
+
const queue = [from];
|
|
318
|
+
for (let i = 0; i < queue.length; i++) {
|
|
319
|
+
for (const t of s.outOf.get(queue[i]) ?? []) {
|
|
320
|
+
if (t === from || hops.has(t) || !s.byId.has(t) || s.byId.get(t).parentId !== scope)
|
|
321
|
+
continue;
|
|
322
|
+
const h = hops.get(queue[i]) + (queue[i] === from ? 0 : 1);
|
|
323
|
+
hops.set(t, h);
|
|
324
|
+
if (s.pending.has(t))
|
|
325
|
+
queue.push(t);
|
|
326
|
+
else if (isPositioned(s, t))
|
|
327
|
+
found.set(t, h);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return found;
|
|
331
|
+
}
|
|
332
|
+
// ── Pushing ──
|
|
333
|
+
/** The cell span of the x-grid that indexes boxes for collision checks. */
|
|
334
|
+
const GRID = 1024;
|
|
335
|
+
/** A box spanning more cells than this is checked against every moved box instead of indexed. */
|
|
336
|
+
const MAX_CELLS = 64;
|
|
337
|
+
/** Boxes indexed by the x-grid cells they span, so a lookup only checks boxes near it. */
|
|
338
|
+
function gridOf(s, boxes) {
|
|
339
|
+
const cells = new Map();
|
|
340
|
+
const wide = [];
|
|
341
|
+
const cell = (x) => Math.floor(x / GRID);
|
|
342
|
+
for (const o of boxes) {
|
|
343
|
+
const r = rectOf(s, o);
|
|
344
|
+
const first = cell(r.x - PADDING);
|
|
345
|
+
const last = cell(r.x + r.width + PADDING);
|
|
346
|
+
if (last - first >= MAX_CELLS) {
|
|
347
|
+
wide.push(o);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
for (let c = first; c <= last; c++) {
|
|
351
|
+
if (!cells.has(c))
|
|
352
|
+
cells.set(c, []);
|
|
353
|
+
cells.get(c).push(o);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return (r) => {
|
|
357
|
+
const near = new Set(wide);
|
|
358
|
+
for (let c = cell(r.x - PADDING); c <= cell(r.x + r.width + PADDING); c++)
|
|
359
|
+
for (const o of cells.get(c) ?? [])
|
|
360
|
+
near.add(o);
|
|
361
|
+
return [...near];
|
|
131
362
|
};
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* What `opts.seed` carries along: the seed, the nodes downstream of a moved
|
|
366
|
+
* one that `opts.mayMove` allows (through pending ones, which do not move),
|
|
367
|
+
* and whatever a moved node would land on, with its downstream. Undefined when
|
|
368
|
+
* a moved node lands on one it may not move and `opts.blocked` refuses.
|
|
369
|
+
*/
|
|
370
|
+
function spread(s, opts) {
|
|
371
|
+
if (opts.seed.length === 0)
|
|
372
|
+
return new Set();
|
|
373
|
+
const scope = opts.seed[0].parentId;
|
|
374
|
+
const nearby = gridOf(s, obstaclesIn(s, scope, opts.origin));
|
|
375
|
+
const moved = new Set();
|
|
376
|
+
const seen = new Set([opts.origin]);
|
|
377
|
+
const queue = [];
|
|
378
|
+
const visit = (id, moves) => {
|
|
379
|
+
if (seen.has(id))
|
|
380
|
+
return;
|
|
381
|
+
seen.add(id);
|
|
382
|
+
if (moves)
|
|
383
|
+
moved.add(id);
|
|
384
|
+
queue.push(id);
|
|
140
385
|
};
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
386
|
+
for (const o of opts.seed)
|
|
387
|
+
visit(o.id, true);
|
|
388
|
+
for (let i = 0; i < queue.length; i++) {
|
|
389
|
+
const id = queue[i];
|
|
390
|
+
for (const t of s.outOf.get(id) ?? []) {
|
|
391
|
+
const d = s.byId.get(t);
|
|
392
|
+
if (!d || d.parentId !== scope || seen.has(t))
|
|
393
|
+
continue;
|
|
394
|
+
if (s.pending.has(t))
|
|
395
|
+
visit(t, false);
|
|
396
|
+
else if (isPositioned(s, t) && opts.mayMove(d))
|
|
397
|
+
visit(t, true);
|
|
398
|
+
}
|
|
399
|
+
if (!moved.has(id))
|
|
400
|
+
continue;
|
|
401
|
+
const r = rectOf(s, s.byId.get(id));
|
|
402
|
+
const box = { ...r, x: r.x + opts.by.x, y: r.y + opts.by.y };
|
|
403
|
+
if (opts.blocked === Blocked.Refuse && opts.keepClear && overlaps(box, opts.keepClear))
|
|
404
|
+
return undefined;
|
|
405
|
+
for (const other of nearby(box)) {
|
|
406
|
+
if (moved.has(other.id) || !overlaps(box, rectOf(s, other)))
|
|
161
407
|
continue;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
408
|
+
if (opts.blocked === Blocked.Refuse && !opts.mayMove(other))
|
|
409
|
+
return undefined;
|
|
410
|
+
visit(other.id, true);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return moved;
|
|
414
|
+
}
|
|
415
|
+
// ── Placing ──
|
|
416
|
+
/** Place in waves, by id, until no pending node has an anchor left. */
|
|
417
|
+
function placeInWaves(s) {
|
|
418
|
+
for (let wave = nextWave(s); wave.length > 0; wave = nextWave(s)) {
|
|
419
|
+
for (const id of wave)
|
|
420
|
+
placeNode(s, id);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* The pending nodes to place next: those with a positioned predecessor (or
|
|
425
|
+
* their container's entry) and no predecessor in their container still to be
|
|
426
|
+
* placed; when the rest are in a cycle, those with a positioned predecessor;
|
|
427
|
+
* else those with a positioned successor.
|
|
428
|
+
*/
|
|
429
|
+
function nextWave(s) {
|
|
430
|
+
const pending = [...s.pending].sort();
|
|
431
|
+
const scopeOfId = (id) => s.byId.get(id).parentId;
|
|
432
|
+
const positionedNear = (id, other) => s.byId.get(other)?.parentId === scopeOfId(id) && isPositioned(s, other);
|
|
433
|
+
const fromBehind = (id) => enteredFromContainer(s, id) || (s.inOf.get(id) ?? []).some((p) => positionedNear(id, p));
|
|
434
|
+
const predsPlaced = (id) => (s.inOf.get(id) ?? []).every((p) => p === id || !s.anchored.has(p) || !s.pending.has(p) || s.byId.get(p)?.parentId !== scopeOfId(id));
|
|
435
|
+
const behind = pending.filter(fromBehind);
|
|
436
|
+
const settled = behind.filter(predsPlaced);
|
|
437
|
+
if (settled.length > 0)
|
|
438
|
+
return settled;
|
|
439
|
+
if (behind.length > 0)
|
|
440
|
+
return behind;
|
|
441
|
+
return pending.filter((id) => (s.outOf.get(id) ?? []).some((t) => positionedNear(id, t)));
|
|
442
|
+
}
|
|
443
|
+
/** Where a node goes before making room, and which nodes count as past it. */
|
|
444
|
+
function candidateOf(s, n) {
|
|
445
|
+
const inScope = (other) => s.byId.get(other)?.parentId === n.parentId && isPositioned(s, other);
|
|
446
|
+
const rightEdge = (id) => rectOf(s, s.byId.get(id)).x + rectOf(s, s.byId.get(id)).width;
|
|
447
|
+
const [pred] = (s.inOf.get(n.id) ?? []).filter(inScope).sort((a, b) => rightEdge(b) - rightEdge(a) || a.localeCompare(b));
|
|
448
|
+
if (pred !== undefined) {
|
|
449
|
+
const anchor = rectOf(s, s.byId.get(pred));
|
|
450
|
+
const [lead] = [...frontierOf(s, n.id)].filter(([t]) => t !== pred)
|
|
451
|
+
.map(([t, hops]) => ({ t, hops, r: rectOf(s, s.byId.get(t)) }))
|
|
452
|
+
.sort((a, b) => a.hops - b.hops || a.r.x - b.r.x || a.t.localeCompare(b.t));
|
|
453
|
+
// A successor tucked under its predecessor (a context index under its agent): take its spot and push it a slot.
|
|
454
|
+
if (lead && lead.hops === 0 && lead.r.x >= anchor.x && lead.r.x < anchor.x + anchor.width) {
|
|
455
|
+
return { at: { x: lead.r.x, y: lead.r.y }, past: (o) => o.id !== pred && rectOf(s, o).x >= lead.r.x, pred };
|
|
456
|
+
}
|
|
457
|
+
// A join another lane already feeds keeps the node in its predecessor's lane.
|
|
458
|
+
const joins = lead !== undefined && (s.inOf.get(lead.t) ?? [])
|
|
459
|
+
.some((p) => p !== pred && s.byId.get(p)?.parentId === n.parentId && isPositioned(s, p));
|
|
460
|
+
const y = lead && !joins && !rowsMeet(anchor, lead.r) ? lead.r.y : anchor.y;
|
|
461
|
+
// At the predecessor's x counts as past (a context index under its agent); the predecessor does not.
|
|
462
|
+
return { at: { x: anchor.x + anchor.width + GAP, y }, past: (o) => o.id !== pred && rectOf(s, o).x >= anchor.x, pred };
|
|
463
|
+
}
|
|
464
|
+
if (enteredFromContainer(s, n.id))
|
|
465
|
+
return { at: { ...BODY_ORIGIN }, past: () => true };
|
|
466
|
+
const [succ] = (s.outOf.get(n.id) ?? []).filter(inScope)
|
|
467
|
+
.sort((a, b) => rectOf(s, s.byId.get(a)).x - rectOf(s, s.byId.get(b)).x || a.localeCompare(b));
|
|
468
|
+
const at = rectOf(s, s.byId.get(succ));
|
|
469
|
+
const left = { x: at.x - SLOT_DX, y: at.y };
|
|
470
|
+
if (!obstaclesIn(s, n.parentId, n.id).some((o) => overlaps({ ...left, ...sizeOf(s, n) }, rectOf(s, o))))
|
|
471
|
+
return { at: left };
|
|
472
|
+
return { at: { x: at.x, y: at.y }, past: (o) => rectOf(s, o).x >= at.x };
|
|
473
|
+
}
|
|
474
|
+
/** The new branches already placed out of `pred`, other than the node's own downstream. */
|
|
475
|
+
function siblingsOf(s, pred, downstream) {
|
|
476
|
+
if (pred === undefined)
|
|
477
|
+
return new Set();
|
|
478
|
+
return new Set((s.outOf.get(pred) ?? []).filter((t) => s.placed.has(t) && !downstream.has(t)));
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Move `at` down, row by row, while everything a box of `size` there would
|
|
482
|
+
* overlap is a sibling, in one sweep: only obstacles in its column can be
|
|
483
|
+
* hit, and the rows only go down.
|
|
484
|
+
*/
|
|
485
|
+
function stackBelowSiblings(s, at, size, obstacles, siblings) {
|
|
486
|
+
if (siblings.size === 0)
|
|
487
|
+
return;
|
|
488
|
+
const column = obstacles.map((o) => ({ id: o.id, r: rectOf(s, o) }))
|
|
489
|
+
.filter(({ r }) => r.x < at.x + size.width + PADDING && at.x < r.x + r.width + PADDING)
|
|
490
|
+
.sort((a, b) => a.r.y - b.r.y);
|
|
491
|
+
let row = [];
|
|
492
|
+
for (let next = 0;;) {
|
|
493
|
+
while (next < column.length && column[next].r.y < at.y + size.height + PADDING)
|
|
494
|
+
row.push(column[next++]);
|
|
495
|
+
row = row.filter(({ r }) => r.y + r.height + PADDING > at.y);
|
|
496
|
+
if (row.length === 0 || !row.every(({ id }) => siblings.has(id)))
|
|
497
|
+
return;
|
|
498
|
+
at.y = Math.max(...row.map(({ r }) => r.y + r.height)) + GAP;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
function placeNode(s, id) {
|
|
502
|
+
const n = s.byId.get(id);
|
|
503
|
+
const { at, past, pred } = candidateOf(s, n);
|
|
504
|
+
const obstacles = obstaclesIn(s, n.parentId, id);
|
|
505
|
+
// An added sibling (another new branch out of the same predecessor) on the spot: take the next row down instead of pushing it.
|
|
506
|
+
const downstream = downstreamOf(s, id);
|
|
507
|
+
stackBelowSiblings(s, at, sizeOf(s, n), obstacles, siblingsOf(s, pred, downstream));
|
|
508
|
+
const box = { ...at, ...sizeOf(s, n) };
|
|
509
|
+
const hits = obstacles.filter((o) => overlaps(box, rectOf(s, o)));
|
|
510
|
+
const freeSpot = () => findFreePosition(at, sizeOf(s, n), obstacles.map((o) => rectOf(s, o)));
|
|
511
|
+
let position = at;
|
|
512
|
+
if (past) {
|
|
513
|
+
const pushable = (o) => isMovable(s, o) && past(o);
|
|
514
|
+
const roomFor = (o) => box.x + box.width + GAP - rectOf(s, o).x;
|
|
515
|
+
const after = [...downstream].map((d) => s.byId.get(d)).filter((o) => isPositioned(s, o.id) && pushable(o));
|
|
516
|
+
const rowNeed = [...hits, ...after].filter((o) => pushable(o) && rowsMeet(box, rectOf(s, o))).map(roomFor);
|
|
517
|
+
const successorNeed = [...frontierOf(s, id).keys()].map((t) => s.byId.get(t)).filter(pushable).map(roomFor);
|
|
518
|
+
const by = { x: Math.max(0, ...rowNeed, ...successorNeed), y: 0 };
|
|
519
|
+
const moved = hits.every(pushable)
|
|
520
|
+
? spread(s, { origin: id, seed: [...hits, ...after], by, mayMove: pushable, blocked: Blocked.Refuse, keepClear: box })
|
|
521
|
+
: undefined;
|
|
522
|
+
if (moved)
|
|
523
|
+
shift(s, moved, by);
|
|
524
|
+
else if (hits.length > 0)
|
|
525
|
+
position = freeSpot();
|
|
526
|
+
}
|
|
527
|
+
else if (hits.length > 0) {
|
|
528
|
+
position = freeSpot();
|
|
529
|
+
}
|
|
530
|
+
write(s, n, { position });
|
|
531
|
+
s.pending.delete(id);
|
|
532
|
+
s.placed.add(id);
|
|
533
|
+
if (n.parentId !== undefined && s.byId.has(n.parentId))
|
|
534
|
+
s.toFit.add(n.parentId);
|
|
535
|
+
}
|
|
536
|
+
// ── Growing containers ──
|
|
537
|
+
/** The right and bottom edge of a container's body, over the children `which` keeps. */
|
|
538
|
+
function bodyExtent(s, id, which) {
|
|
539
|
+
const body = positionedIn(s, id).filter(which).map((o) => rectOf(s, o));
|
|
540
|
+
if (body.length === 0)
|
|
541
|
+
return undefined;
|
|
542
|
+
return { right: Math.max(...body.map((r) => r.x + r.width)), bottom: Math.max(...body.map((r) => r.y + r.height)) };
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Grow each container that gained or moved a child, innermost first (by id on
|
|
546
|
+
* a tie), by as much as its body grew past the padding it had (the body
|
|
547
|
+
* origin's, for a body that was empty). A grown container pushes every
|
|
548
|
+
* sibling in its way or downstream of it past its old edges, an author
|
|
549
|
+
* position or placeholder included (it was chosen against the old size), and
|
|
550
|
+
* keeps at least the row gap to its new right edge. Its own container is
|
|
551
|
+
* fitted next.
|
|
552
|
+
*/
|
|
553
|
+
function fitContainers(s, before) {
|
|
554
|
+
const fitted = new Set();
|
|
555
|
+
for (let id = deepest(s.toFit, s.byId); id !== undefined; id = deepest(s.toFit, s.byId)) {
|
|
556
|
+
s.toFit.delete(id);
|
|
557
|
+
fitted.add(id);
|
|
558
|
+
const c = s.byId.get(id);
|
|
559
|
+
const now = bodyExtent(s, id, () => true);
|
|
560
|
+
if (!now)
|
|
561
|
+
continue;
|
|
562
|
+
const old = sizeOf(s, c);
|
|
563
|
+
const was = before.get(id);
|
|
564
|
+
const padRight = was ? Math.min(BODY_ORIGIN.x, old.width - was.right) : BODY_ORIGIN.x;
|
|
565
|
+
const padBottom = was ? Math.min(BODY_ORIGIN.y, old.height - was.bottom) : BODY_ORIGIN.y;
|
|
566
|
+
const width = Math.max(old.width, now.right + padRight);
|
|
567
|
+
const height = Math.max(old.height, now.bottom + padBottom);
|
|
568
|
+
if (width === old.width && height === old.height)
|
|
569
|
+
continue;
|
|
570
|
+
write(s, c, { size: { ...uiOf(s, c)?.size, width, height } });
|
|
571
|
+
if (c.parentId !== undefined && s.byId.has(c.parentId) && !fitted.has(c.parentId))
|
|
572
|
+
s.toFit.add(c.parentId);
|
|
573
|
+
if (!hasPosition(s, c))
|
|
574
|
+
continue;
|
|
575
|
+
const at = rectOf(s, c);
|
|
576
|
+
const grown = { ...at, width, height };
|
|
577
|
+
const siblings = obstaclesIn(s, c.parentId, id);
|
|
578
|
+
const downstream = downstreamOf(s, id);
|
|
579
|
+
if (width > old.width) {
|
|
580
|
+
const right = siblings.filter((o) => rectOf(s, o).x >= at.x + old.width && (downstream.has(o.id) || overlaps(grown, rectOf(s, o))));
|
|
581
|
+
const gapNeed = right.filter((o) => rowsMeet(grown, rectOf(s, o))).map((o) => at.x + width + GAP - rectOf(s, o).x);
|
|
582
|
+
const by = { x: Math.max(width - old.width, ...gapNeed), y: 0 };
|
|
583
|
+
const pastRight = (o) => hasPosition(s, o) && rectOf(s, o).x >= at.x + old.width;
|
|
584
|
+
const moved = spread(s, { origin: id, seed: right, by, mayMove: pastRight, blocked: Blocked.Push });
|
|
585
|
+
if (moved)
|
|
586
|
+
shift(s, moved, by);
|
|
587
|
+
}
|
|
588
|
+
if (height > old.height) {
|
|
589
|
+
const under = siblings.filter((o) => rectOf(s, o).y >= at.y + old.height && overlaps(grown, rectOf(s, o)));
|
|
590
|
+
const by = { x: 0, y: height - old.height };
|
|
591
|
+
const pastBottom = (o) => hasPosition(s, o) && rectOf(s, o).y >= at.y + old.height;
|
|
592
|
+
const moved = spread(s, { origin: id, seed: under, by, mayMove: pastBottom, blocked: Blocked.Push });
|
|
593
|
+
if (moved)
|
|
594
|
+
shift(s, moved, by);
|
|
176
595
|
}
|
|
177
596
|
}
|
|
178
597
|
}
|
|
598
|
+
/** The id in `ids` nested in the most containers, by id on a tie; a parent cycle stops counting at the node count. */
|
|
599
|
+
function deepest(ids, byId) {
|
|
600
|
+
const depthOf = (id) => {
|
|
601
|
+
let depth = 0;
|
|
602
|
+
for (let p = byId.get(id)?.parentId; p !== undefined && byId.has(p) && depth < byId.size; p = byId.get(p)?.parentId)
|
|
603
|
+
depth += 1;
|
|
604
|
+
return depth;
|
|
605
|
+
};
|
|
606
|
+
return [...ids].sort((a, b) => depthOf(b) - depthOf(a) || a.localeCompare(b))[0];
|
|
607
|
+
}
|
|
@@ -114,7 +114,7 @@ function serializeVoiceAgent(s, ctx) {
|
|
|
114
114
|
const json = voiceAgentJson({
|
|
115
115
|
systemPrompt: s.systemPrompt, plan, voice: s.voice, maxIterations: s.maxIterations, source, name: ctx.step,
|
|
116
116
|
});
|
|
117
|
-
ctx.sidecar({ path: `${source}/agent.json`, json });
|
|
117
|
+
ctx.sidecar({ path: `${source}/agent.json`, json }, s.source !== undefined ? `voice agent step "${ctx.step}"` : undefined);
|
|
118
118
|
if (plan.refs.length > 0)
|
|
119
119
|
ctx.typeAgentInputs(json.inputSchema, plan.refs);
|
|
120
120
|
// The resource model lives on the definition (its `model.source: true` is
|
|
@@ -169,7 +169,7 @@ function serializeConversationalAgent(s, ctx) {
|
|
|
169
169
|
maxTokenPerResponse: s.maxTokenPerResponse, maxIterations: s.maxIterations,
|
|
170
170
|
guardrails: s.guardrails, source, name: ctx.step,
|
|
171
171
|
}),
|
|
172
|
-
});
|
|
172
|
+
}, s.source !== undefined ? `conversational agent step "${ctx.step}"` : undefined);
|
|
173
173
|
// A SHELL node, like the inline agent's (#927): exactly the keys
|
|
174
174
|
// `toAgentShellInputs` keeps. The system prompt, the model and the run
|
|
175
175
|
// settings live only in the sidecar (`metadata.isConversational` there is the
|
package/dist/serialize.js
CHANGED
|
@@ -292,6 +292,13 @@ export function serializeWithSidecars(built, opts = {}) {
|
|
|
292
292
|
const flow = serialize(built, { ...opts, sidecars });
|
|
293
293
|
return { flow, sidecars };
|
|
294
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* A sidecar path as the file system sees it on macOS (default APFS) and
|
|
297
|
+
* Windows, which ignore case: two paths with one key are one file there.
|
|
298
|
+
*/
|
|
299
|
+
function sidecarKey(path) {
|
|
300
|
+
return path.toLowerCase();
|
|
301
|
+
}
|
|
295
302
|
/**
|
|
296
303
|
* `variables.nodes` — one entry per node OUTPUT, derived from the nodes we just
|
|
297
304
|
* emitted.
|
|
@@ -1815,7 +1822,7 @@ function serializeFlow(built, opts) {
|
|
|
1815
1822
|
* gets them and one that does not is unaffected.
|
|
1816
1823
|
*/
|
|
1817
1824
|
const sidecars = opts.sidecars ?? [];
|
|
1818
|
-
/** The sidecar paths an explicit `source` chose → who chose it (see `addSidecar`). */
|
|
1825
|
+
/** The sidecar paths an explicit `source` chose (by {@link sidecarKey}) → who chose it (see `addSidecar`). */
|
|
1819
1826
|
const explicitSidecars = new Map();
|
|
1820
1827
|
/**
|
|
1821
1828
|
* Add one sidecar file. An agent or a resource whose explicit `source` names
|
|
@@ -1825,19 +1832,26 @@ function serializeFlow(built, opts) {
|
|
|
1825
1832
|
* naming both; `owner` is the one whose `source` is explicit, as the refusal
|
|
1826
1833
|
* names it (`inline agent step "answer"'s context index "SOP"`). The same file
|
|
1827
1834
|
* twice (a subflow body emitted for two callers) is not a clash.
|
|
1835
|
+
*
|
|
1836
|
+
* Paths are compared by {@link sidecarKey}, ignoring case: on macOS (default
|
|
1837
|
+
* APFS) and Windows, `D84E2C17-…/agent.json` and `d84e2c17-…/agent.json` are
|
|
1838
|
+
* one file (#1278).
|
|
1828
1839
|
*/
|
|
1829
1840
|
const addSidecar = (file, owner) => {
|
|
1830
|
-
const
|
|
1831
|
-
const
|
|
1841
|
+
const key = sidecarKey(file.path);
|
|
1842
|
+
const earlier = sidecars.find((f) => sidecarKey(f.path) === key);
|
|
1843
|
+
const earlierOwner = explicitSidecars.get(key);
|
|
1832
1844
|
if (earlier !== undefined && JSON.stringify(earlier.json) !== JSON.stringify(file.json)
|
|
1833
1845
|
&& (owner !== undefined || earlierOwner !== undefined)) {
|
|
1834
1846
|
const names = [earlierOwner ?? 'another agent or resource', owner ?? 'another agent or resource'];
|
|
1835
|
-
|
|
1847
|
+
const where = earlier.path === file.path ? `"${file.path}"`
|
|
1848
|
+
: `"${earlier.path}" and "${file.path}" (one file on macOS and Windows, which ignore case)`;
|
|
1849
|
+
throw new CompileRefusal('INLINE_AGENT_RESOURCE_SOURCE_DUP', `${names[0].charAt(0).toUpperCase()}${names[0].slice(1)} and ${names[1]} would both write ${where} with `
|
|
1836
1850
|
+ 'different content, so the second file would replace the first. Give each its own `source`, or omit '
|
|
1837
1851
|
+ '`source` to derive one.');
|
|
1838
1852
|
}
|
|
1839
1853
|
if (owner !== undefined && earlierOwner === undefined)
|
|
1840
|
-
explicitSidecars.set(
|
|
1854
|
+
explicitSidecars.set(key, owner);
|
|
1841
1855
|
sidecars.push(file);
|
|
1842
1856
|
};
|
|
1843
1857
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uipath/maestro-builder-sdk",
|
|
3
|
-
"version": "6.66.
|
|
3
|
+
"version": "6.66.6",
|
|
4
4
|
"description": "Build UiPath Flow, Case, and BPMN artifacts by writing TypeScript.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://docs.uipath.com/maestro",
|
|
@@ -100,5 +100,5 @@
|
|
|
100
100
|
"@types/node": "^22.7.0",
|
|
101
101
|
"esbuild": "^0.28.1"
|
|
102
102
|
},
|
|
103
|
-
"gitref": "
|
|
103
|
+
"gitref": "108478742e5fc426dc26ba07020d85a3fdbcaa66"
|
|
104
104
|
}
|