@kubex/zinc 1.1.94 → 1.1.96
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/custom-elements.json +653 -89
- package/dist/vscode.html-custom-data.json +73 -18
- package/dist/web-types.json +145 -35
- package/dist/zn.d.ts +204 -39
- package/dist/zn.min.css +1 -1
- package/dist/zn.min.js +374 -308
- package/docs/pages/components/flow-builder.md +17 -1
- package/docs/pages/components/page-builder.md +31 -0
- package/docs/pages/components/slash-menu.md +132 -6
- package/docs/pages/components/textarea.md +16 -0
- package/package.json +1 -1
- package/scss/_root.scss +7 -1
- package/src/components/button/button.scss +5 -2
- package/src/components/flow-builder/flow-builder.component.ts +16 -1
- package/src/components/flow-builder/flow-builder.test.ts +206 -1
- package/src/components/flow-builder/flow-geometry.test.ts +102 -0
- package/src/components/flow-builder/flow.types.ts +82 -15
- package/src/components/flow-builder/modules/flow-canvas/flow-canvas.component.ts +163 -108
- package/src/components/flow-builder/modules/flow-step/flow-step.component.ts +2 -0
- package/src/components/inline-edit/inline-edit.component.ts +6 -1
- package/src/components/input/input.component.ts +12 -2
- package/src/components/page/page.scss +7 -2
- package/src/components/page-builder/modules/page-section-card/page-section-card.component.ts +16 -9
- package/src/components/page-builder/modules/page-section-card/page-section-card.scss +13 -0
- package/src/components/page-builder/modules/page-section-card/page-section-card.test.ts +9 -0
- package/src/components/page-builder/page-builder.component.ts +62 -4
- package/src/components/page-builder/page-builder.test.ts +94 -0
- package/src/components/remarkd-editor/remarkd-editor.component.ts +227 -12
- package/src/components/remarkd-editor/remarkd-editor.scss +81 -0
- package/src/components/remarkd-editor/remarkd-editor.test.ts +258 -0
- package/src/components/settings-container/settings-container.scss +2 -1
- package/src/components/slash-item/slash-item.component.ts +1 -1
- package/src/components/slash-menu/slash-menu-items.ts +48 -0
- package/src/components/slash-menu/slash-menu.component.ts +134 -27
- package/src/components/slash-menu/slash-menu.scss +90 -12
- package/src/components/slash-menu/slash-menu.test.ts +107 -0
- package/src/components/textarea/textarea.component.ts +12 -2
- package/src/components/textarea/textarea.test.ts +2 -2
- package/src/components/translations/translations.component.ts +5 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BRANCH_SPREAD,
|
|
3
|
+
branchDropXs,
|
|
4
|
+
branchPillTop,
|
|
5
|
+
BUS_OFFSET,
|
|
6
|
+
type FlowConnection,
|
|
7
|
+
type FlowNodeInstance,
|
|
8
|
+
type FlowNodeType,
|
|
9
|
+
NODE_HEIGHT,
|
|
10
|
+
NODE_WIDTH,
|
|
11
|
+
} from './flow.types';
|
|
12
|
+
import {expect} from '@open-wc/testing';
|
|
13
|
+
|
|
14
|
+
const TRIPLE: FlowNodeType = {
|
|
15
|
+
type: 'step',
|
|
16
|
+
label: 'Step',
|
|
17
|
+
group: 'action',
|
|
18
|
+
outputs: [
|
|
19
|
+
{id: 'success', label: 'Success'},
|
|
20
|
+
{id: 'failure', label: 'Failure'},
|
|
21
|
+
{id: 'skip', label: 'Skip'},
|
|
22
|
+
],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const typeOf = () => TRIPLE;
|
|
26
|
+
|
|
27
|
+
const node = (id: string, x: number, y = 0): FlowNodeInstance => ({id, type: 'step', x, y, data: {}});
|
|
28
|
+
|
|
29
|
+
const wire = (from: string, port: string, to = 'child'): FlowConnection =>
|
|
30
|
+
({id: `c-${from}-${port}`, source: {node: from, port}, target: {node: to, port: 'in'}});
|
|
31
|
+
|
|
32
|
+
const centreOf = (n: FlowNodeInstance) => n.x + NODE_WIDTH / 2;
|
|
33
|
+
|
|
34
|
+
describe('flow branch geometry', () => {
|
|
35
|
+
it('should keep a fully wired fan on the classic spread', () => {
|
|
36
|
+
const source = node('n1', 400);
|
|
37
|
+
const xs = branchDropXs(source, typeOf, [
|
|
38
|
+
wire('n1', 'success', 'a'), wire('n1', 'failure', 'b'), wire('n1', 'skip', 'c'),
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
expect(xs).to.deep.equal([
|
|
42
|
+
centreOf(source) - BRANCH_SPREAD,
|
|
43
|
+
centreOf(source),
|
|
44
|
+
centreOf(source) + BRANCH_SPREAD,
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should run a lone wired branch straight down, whatever else the step declares', () => {
|
|
49
|
+
const source = node('n1', 400);
|
|
50
|
+
const xs = branchDropXs(source, typeOf, [wire('n1', 'success')]);
|
|
51
|
+
|
|
52
|
+
// Success is wired, so it keeps the node's centre line and its child sits
|
|
53
|
+
// directly below; the two spare branches tuck in beside it.
|
|
54
|
+
expect(xs[0]).to.equal(centreOf(source));
|
|
55
|
+
expect(xs[1]).to.be.greaterThan(xs[0]);
|
|
56
|
+
expect(xs[2]).to.be.greaterThan(xs[1]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should give a spare branch its pill\'s width, not a whole child lane', () => {
|
|
60
|
+
const source = node('n1', 400);
|
|
61
|
+
const wired = branchDropXs(source, typeOf, [
|
|
62
|
+
wire('n1', 'success', 'a'), wire('n1', 'failure', 'b'), wire('n1', 'skip', 'c'),
|
|
63
|
+
]);
|
|
64
|
+
const spare = branchDropXs(source, typeOf, [wire('n1', 'success')]);
|
|
65
|
+
|
|
66
|
+
// Same three branches, two of them unused: the fan collapses to well under
|
|
67
|
+
// half of what three child lanes claim.
|
|
68
|
+
expect(spare[2] - spare[0]).to.be.lessThan((wired[2] - wired[0]) / 2 + 1);
|
|
69
|
+
expect(spare[1] - spare[0]).to.be.lessThan(BRANCH_SPREAD);
|
|
70
|
+
expect(spare[2] - spare[1]).to.be.lessThan(BRANCH_SPREAD);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should centre two wired branches on the node with a spare branch declared', () => {
|
|
74
|
+
const source = node('n1', 400);
|
|
75
|
+
const xs = branchDropXs(source, typeOf, [wire('n1', 'success', 'a'), wire('n1', 'failure', 'b')]);
|
|
76
|
+
|
|
77
|
+
expect((xs[0] + xs[1]) / 2).to.equal(centreOf(source));
|
|
78
|
+
expect(xs[1] - xs[0]).to.equal(BRANCH_SPREAD);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should centre a pill along a short wire to the next row', () => {
|
|
82
|
+
const source = node('n1', 0, 0);
|
|
83
|
+
const child = node('n2', 0, NODE_HEIGHT + 160);
|
|
84
|
+
|
|
85
|
+
const top = branchPillTop(source, 40, child);
|
|
86
|
+
expect(top).to.be.greaterThan(source.y + NODE_HEIGHT);
|
|
87
|
+
expect(top).to.be.lessThan(child.y);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should keep a pill under its own node when the wire skips past a row', () => {
|
|
91
|
+
const source = node('n1', 0, 0);
|
|
92
|
+
const near = node('n2', 0, NODE_HEIGHT + 160);
|
|
93
|
+
const far = node('n3', 0, NODE_HEIGHT + 800);
|
|
94
|
+
|
|
95
|
+
// A long branch must not park its pill halfway down, in among the rows it
|
|
96
|
+
// passes — that is what stacked two nodes' pills on top of each other. It
|
|
97
|
+
// sits on the bus, leaving the gap below clear for the wire to route.
|
|
98
|
+
const top = branchPillTop(source, 40, far);
|
|
99
|
+
expect(top).to.equal(source.y + NODE_HEIGHT + BUS_OFFSET);
|
|
100
|
+
expect(top).to.be.lessThan(near.y);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -125,6 +125,14 @@ export interface FlowNodeType {
|
|
|
125
125
|
inputs?: FlowPort[];
|
|
126
126
|
/** Output ports. Defaults to a single unlabelled output. e.g. TRUE/FALSE for a split. */
|
|
127
127
|
outputs?: FlowPort[];
|
|
128
|
+
/**
|
|
129
|
+
* The type's branches are the only ones it has: the canvas offers no delete
|
|
130
|
+
* on their pills, and clicking a fully-wired node's output adds nothing.
|
|
131
|
+
* For steps whose branches mirror fixed fields in the consumer's own model
|
|
132
|
+
* (a success / failure / skip triple, say), where an extra or missing branch
|
|
133
|
+
* has nowhere to be stored.
|
|
134
|
+
*/
|
|
135
|
+
fixedOutputs?: boolean;
|
|
128
136
|
/** Initial `data` for a freshly placed node. */
|
|
129
137
|
defaultData?: Record<string, unknown>;
|
|
130
138
|
/**
|
|
@@ -230,6 +238,18 @@ export const NODE_HEIGHT = 60;
|
|
|
230
238
|
*/
|
|
231
239
|
export const BRANCH_SPREAD = NODE_WIDTH + 80;
|
|
232
240
|
|
|
241
|
+
/** Clearance between two branches that both carry a child card. */
|
|
242
|
+
const BRANCH_CARD_GAP = BRANCH_SPREAD - NODE_WIDTH;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Clearance beside a branch with nothing under it but its own pill. A pill is a
|
|
246
|
+
* chip, not a card, so its neighbours can sit far closer than two child lanes.
|
|
247
|
+
*/
|
|
248
|
+
const BRANCH_PILL_GAP = 40;
|
|
249
|
+
|
|
250
|
+
/** Half-width claimed by an open branch with no pill — room for its "+" target. */
|
|
251
|
+
const OPEN_BRANCH_MIN_HALF = 20;
|
|
252
|
+
|
|
233
253
|
// Branch geometry below a node: outputs fork from a stem onto a horizontal bus,
|
|
234
254
|
// and labelled outputs drop from it into a name pill.
|
|
235
255
|
export const BUS_OFFSET = 40;
|
|
@@ -243,14 +263,25 @@ export const PILL_LINE_HEIGHT = 20;
|
|
|
243
263
|
const PILL_APPROACH = 20;
|
|
244
264
|
/** The least wire kept above a sole output's pill in a tight gap. */
|
|
245
265
|
const PILL_MIN_STUB = 20;
|
|
266
|
+
/**
|
|
267
|
+
* The longest run a pill will centre itself along. A branch reaching past the
|
|
268
|
+
* next row would otherwise park its pill halfway down, in among nodes it has
|
|
269
|
+
* nothing to do with; beyond this it keeps the fixed drop under its own node.
|
|
270
|
+
*/
|
|
271
|
+
const PILL_CENTRE_MAX_RUN = 240;
|
|
246
272
|
|
|
247
273
|
/**
|
|
248
|
-
* Canvas y of a branch pill's top edge. A pill on a wire to a child
|
|
249
|
-
* centred along the run from its node's bottom to the child's top —
|
|
250
|
-
* above and below
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
274
|
+
* Canvas y of a branch pill's top edge. A pill on a wire to a child in the next
|
|
275
|
+
* row down is centred along the run from its node's bottom to the child's top —
|
|
276
|
+
* equal wire above and below. A sole output's wire is a straight stem with no
|
|
277
|
+
* bus to respect, so its pill may rise above the bus line to stay centred; fan
|
|
278
|
+
* branches stop at the bus.
|
|
279
|
+
*
|
|
280
|
+
* A branch reaching past the next row sits right under the bus instead: it
|
|
281
|
+
* reads as belonging to the node it hangs from rather than to whatever it flies
|
|
282
|
+
* over, and it leaves the whole gap below free for the wire to route around
|
|
283
|
+
* what is in the way. Open branches and loop/side wires keep the fixed drop
|
|
284
|
+
* below the bus, where their "+" has room.
|
|
254
285
|
*/
|
|
255
286
|
export function branchPillTop(
|
|
256
287
|
node: Pick<FlowNodeInstance, 'y'>,
|
|
@@ -261,6 +292,7 @@ export function branchPillTop(
|
|
|
261
292
|
const bottom = node.y + NODE_HEIGHT;
|
|
262
293
|
const busY = bottom + BUS_OFFSET;
|
|
263
294
|
if (child && child.y >= bottom) {
|
|
295
|
+
if (child.y - bottom > PILL_CENTRE_MAX_RUN) return busY;
|
|
264
296
|
const minTop = soleOutput ? bottom + PILL_MIN_STUB : busY;
|
|
265
297
|
const centered = Math.round((bottom + child.y) / 2 - pillH / 2);
|
|
266
298
|
return Math.max(minTop, Math.min(centered, child.y - PILL_APPROACH - pillH));
|
|
@@ -321,17 +353,52 @@ function rectsOverlap(a: Rect, b: Rect): boolean {
|
|
|
321
353
|
export type FlowTypeOf = (type: string) => FlowNodeType | undefined;
|
|
322
354
|
|
|
323
355
|
/**
|
|
324
|
-
*
|
|
325
|
-
* the
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
356
|
+
* Half the room a branch needs either side of its drop: a wired branch has to
|
|
357
|
+
* clear the child card hanging under it, an open one only its own pill.
|
|
358
|
+
*/
|
|
359
|
+
function branchHalfWidth(port: FlowPort, wired: boolean): number {
|
|
360
|
+
if (wired) return NODE_WIDTH / 2;
|
|
361
|
+
return port.label ? pillSize(port.label).w / 2 : OPEN_BRANCH_MIN_HALF;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Canvas x for each of a node's branch drops. Neighbours are spaced by what
|
|
366
|
+
* they actually occupy — two child lanes keep the full spread, while a branch
|
|
367
|
+
* with nothing wired to it needs only its pill — and the run is then shifted so
|
|
368
|
+
* the *wired* branches stay centred on the node. A step that declares branches
|
|
369
|
+
* it is not using therefore still runs straight down, instead of fanning empty
|
|
370
|
+
* lanes across the canvas; and a fully wired fan is spaced exactly as before.
|
|
371
|
+
*
|
|
372
|
+
* Pills never shift sideways to chase their child — the wire into a pill is
|
|
373
|
+
* always a straight vertical, and any lateral offset to the child is taken up
|
|
374
|
+
* by the elbow below the pill (which still enters the child from straight
|
|
375
|
+
* above).
|
|
329
376
|
*/
|
|
330
|
-
export function branchDropXs(
|
|
377
|
+
export function branchDropXs(
|
|
378
|
+
node: FlowNodeInstance,
|
|
379
|
+
typeOf: FlowTypeOf,
|
|
380
|
+
connections: FlowConnection[] = []
|
|
381
|
+
): number[] {
|
|
331
382
|
const outputs = nodeOutputs(node, typeOf(node.type));
|
|
332
383
|
const cx = node.x + NODE_WIDTH / 2;
|
|
333
|
-
return outputs.map((
|
|
334
|
-
|
|
384
|
+
if (outputs.length < 2) return outputs.map(() => cx);
|
|
385
|
+
|
|
386
|
+
const wired = outputs.map(p => connections.some(c => c.source.node === node.id && c.source.port === p.id));
|
|
387
|
+
|
|
388
|
+
const offsets = [0];
|
|
389
|
+
for (let i = 1; i < outputs.length; i++) {
|
|
390
|
+
const clearance = wired[i - 1] && wired[i] ? BRANCH_CARD_GAP : BRANCH_PILL_GAP;
|
|
391
|
+
const gap = branchHalfWidth(outputs[i - 1], wired[i - 1]) + branchHalfWidth(outputs[i], wired[i]) + clearance;
|
|
392
|
+
offsets.push(offsets[i - 1] + snapToGrid(gap));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Anchor on the branches carrying children, so wiring or unwiring a spare
|
|
396
|
+
// branch never drags the cards below the others sideways.
|
|
397
|
+
const anchored = offsets.filter((_, i) => wired[i]);
|
|
398
|
+
const span = anchored.length ? anchored : offsets;
|
|
399
|
+
const anchor = (Math.min(...span) + Math.max(...span)) / 2;
|
|
400
|
+
|
|
401
|
+
return offsets.map(o => Math.round(cx + o - anchor));
|
|
335
402
|
}
|
|
336
403
|
|
|
337
404
|
/**
|
|
@@ -346,7 +413,7 @@ export function nodeObstacles(
|
|
|
346
413
|
): Rect[] {
|
|
347
414
|
const rects: Rect[] = [{x: node.x, y: node.y, w: NODE_WIDTH, h: NODE_HEIGHT, m: CARD_MARGIN}];
|
|
348
415
|
const ports = nodeOutputs(node, typeOf(node.type));
|
|
349
|
-
const xs = branchDropXs(node, typeOf);
|
|
416
|
+
const xs = branchDropXs(node, typeOf, connections);
|
|
350
417
|
ports.forEach((port, i) => {
|
|
351
418
|
if (!port.label) return;
|
|
352
419
|
const size = pillSize(port.label);
|
|
@@ -46,6 +46,8 @@ const ADD_POINT_OFFSET = 40;
|
|
|
46
46
|
// input, and how far a detour clears a node card when routing around it. All
|
|
47
47
|
// whole grid units so wire runs sit on grid lines.
|
|
48
48
|
const WIRE_STUB = 20;
|
|
49
|
+
/** Strip above a card where incoming arrows land; foreign wires keep out of it. */
|
|
50
|
+
const APPROACH_ZONE = 48;
|
|
49
51
|
const WIRE_APPROACH = 20;
|
|
50
52
|
// Half the wire "+"'s footprint.
|
|
51
53
|
const WIRE_PLUS_RADIUS = 10;
|
|
@@ -312,6 +314,9 @@ export default class ZnFlowCanvas extends ZincElement {
|
|
|
312
314
|
const open = nodeOutputs(node, this._typeFor(node)).find(
|
|
313
315
|
p => !this.connections.some(c => c.source.node === nodeId && c.source.port === p.id)
|
|
314
316
|
);
|
|
317
|
+
// A fixed-output node has no spare branch to start once all of its own are
|
|
318
|
+
// wired — wire one of those instead, from its pill.
|
|
319
|
+
if (!open && this._typeFor(node)?.fixedOutputs) return;
|
|
315
320
|
this._startLink(nodeId, open?.id ?? NEW_OUTPUT_PORT);
|
|
316
321
|
};
|
|
317
322
|
|
|
@@ -531,7 +536,7 @@ export default class ZnFlowCanvas extends ZincElement {
|
|
|
531
536
|
// Drops always sit at the natural fan position under the source, so the
|
|
532
537
|
// arrow into a pill is a straight vertical; the wire below the pill takes
|
|
533
538
|
// up any lateral offset to the child.
|
|
534
|
-
const dropXs = branchDropXs(node, t => this.registry?.get(t));
|
|
539
|
+
const dropXs = branchDropXs(node, t => this.registry?.get(t), this.connections);
|
|
535
540
|
const branches = outputs.map((port, i) => {
|
|
536
541
|
const conn = this.connections.find(c => c.source.node === node.id && c.source.port === port.id);
|
|
537
542
|
const child = conn ? this.nodes.find(n => n.id === conn.target.node) : undefined;
|
|
@@ -590,14 +595,19 @@ export default class ZnFlowCanvas extends ZincElement {
|
|
|
590
595
|
}
|
|
591
596
|
|
|
592
597
|
/**
|
|
593
|
-
* Whether an orthogonal segment passes through any node card (with margin)
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
*
|
|
598
|
+
* Whether an orthogonal segment passes through any node card (with margin).
|
|
599
|
+
* With `keepApproachClear`, the strip above each card - where incoming arrows
|
|
600
|
+
* land - counts as occupied too: a foreign wire running just over a card's
|
|
601
|
+
* input port reads as connecting to it.
|
|
597
602
|
*/
|
|
598
|
-
private _segmentBlocked(
|
|
603
|
+
private _segmentBlocked(
|
|
604
|
+
a: { x: number; y: number },
|
|
605
|
+
b: { x: number; y: number },
|
|
606
|
+
ignore: Set<string>,
|
|
607
|
+
keepApproachClear = true
|
|
608
|
+
): boolean {
|
|
599
609
|
const M = 8;
|
|
600
|
-
const
|
|
610
|
+
const approach = keepApproachClear ? APPROACH_ZONE : M;
|
|
601
611
|
const minX = Math.min(a.x, b.x);
|
|
602
612
|
const maxX = Math.max(a.x, b.x);
|
|
603
613
|
const minY = Math.min(a.y, b.y);
|
|
@@ -605,13 +615,17 @@ export default class ZnFlowCanvas extends ZincElement {
|
|
|
605
615
|
return this.nodes.some(n =>
|
|
606
616
|
!ignore.has(n.id)
|
|
607
617
|
&& minX < n.x + NODE_WIDTH + M && maxX > n.x - M
|
|
608
|
-
&& minY < n.y + NODE_HEIGHT + M && maxY > n.y -
|
|
618
|
+
&& minY < n.y + NODE_HEIGHT + M && maxY > n.y - approach
|
|
609
619
|
);
|
|
610
620
|
}
|
|
611
621
|
|
|
612
|
-
private _routeClear(
|
|
622
|
+
private _routeClear(
|
|
623
|
+
points: { x: number; y: number }[],
|
|
624
|
+
ignore: Set<string>,
|
|
625
|
+
keepApproachClear = true
|
|
626
|
+
): boolean {
|
|
613
627
|
for (let i = 0; i < points.length - 1; i++) {
|
|
614
|
-
if (this._segmentBlocked(points[i], points[i + 1], ignore)) return false;
|
|
628
|
+
if (this._segmentBlocked(points[i], points[i + 1], ignore, keepApproachClear)) return false;
|
|
615
629
|
}
|
|
616
630
|
return true;
|
|
617
631
|
}
|
|
@@ -619,8 +633,13 @@ export default class ZnFlowCanvas extends ZincElement {
|
|
|
619
633
|
/**
|
|
620
634
|
* Orthogonal waypoints from a branch exit to a child's input. The wire always
|
|
621
635
|
* enters the input from above (arrow pointing down), and never passes through
|
|
622
|
-
* a node card
|
|
623
|
-
*
|
|
636
|
+
* a node card.
|
|
637
|
+
*
|
|
638
|
+
* The search runs twice: first keeping clear of the strip above every card as
|
|
639
|
+
* well as the cards themselves, then - if the corridor is genuinely too tight
|
|
640
|
+
* for that - clear of the cards alone. Only when nothing at all fits does the
|
|
641
|
+
* wire fall back to the direct line, so a wire drawn over a card now means
|
|
642
|
+
* there was no way around it rather than that the search gave up.
|
|
624
643
|
*/
|
|
625
644
|
private _routePoints(
|
|
626
645
|
from: { x: number; y: number },
|
|
@@ -632,101 +651,134 @@ export default class ZnFlowCanvas extends ZincElement {
|
|
|
632
651
|
) {
|
|
633
652
|
const {x: tx, y: ty} = this._inputAnchor(child, targetPort);
|
|
634
653
|
const ignore = new Set(sourceId ? [child.id, sourceId] : [child.id]);
|
|
654
|
+
const candidates = ty - WIRE_APPROACH >= from.y
|
|
655
|
+
? this._forwardRoutes(from, tx, ty, midOffset, ignore)
|
|
656
|
+
: this._aroundRoutes(from, tx, ty, ignore, loop);
|
|
635
657
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
658
|
+
for (const keepApproachClear of [true, false]) {
|
|
659
|
+
for (const route of candidates) {
|
|
660
|
+
if (this._routeClear(route, ignore, keepApproachClear)) return route;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return candidates[0];
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Candidate routes to a child below, tidiest first: the straight drop or the
|
|
668
|
+
* single elbow, then the same elbow on neighbouring lines, then out around
|
|
669
|
+
* whatever sits between the two (the shape a loop-back uses - far more
|
|
670
|
+
* readable than threading a corridor when a wire skips a row it has nothing
|
|
671
|
+
* to do with), and finally a side-step that jogs out and back.
|
|
672
|
+
*/
|
|
673
|
+
private _forwardRoutes(
|
|
674
|
+
from: { x: number; y: number },
|
|
675
|
+
tx: number,
|
|
676
|
+
ty: number,
|
|
677
|
+
midOffset: number,
|
|
678
|
+
ignore: Set<string>
|
|
679
|
+
): { x: number; y: number }[][] {
|
|
680
|
+
const routes: { x: number; y: number }[][] = [];
|
|
681
|
+
|
|
682
|
+
// The exact midpoint, not grid-snapped: both endpoints sit on the grid, so
|
|
683
|
+
// the midpoint is always a half-tile multiple — snapping it to full tiles
|
|
684
|
+
// made the two verticals grow alternately as a node was dragged.
|
|
685
|
+
const base = Math.round((from.y + ty) / 2) + midOffset;
|
|
686
|
+
// The horizontal run stays within the middle half of the gap, so the drop
|
|
687
|
+
// and the final approach grow in proportion to the distance — lane offsets
|
|
688
|
+
// can't shove the run right up against either end.
|
|
689
|
+
const span = ty - from.y;
|
|
690
|
+
const lo = Math.min(ty - 12, Math.ceil((from.y + span / 4) / GRID_SIZE) * GRID_SIZE);
|
|
691
|
+
const hi = Math.max(lo, Math.floor((ty - span / 4) / GRID_SIZE) * GRID_SIZE);
|
|
692
|
+
const clampY = (v: number) => Math.max(lo, Math.min(hi, v));
|
|
693
|
+
|
|
694
|
+
if (tx === from.x) {
|
|
695
|
+
routes.push([from, {x: tx, y: ty}]);
|
|
696
|
+
} else {
|
|
697
|
+
for (let step = 0; step <= 20; step++) {
|
|
698
|
+
for (const cand of step === 0 ? [base] : [base + step * GRID_SIZE, base - step * GRID_SIZE]) {
|
|
699
|
+
const my = clampY(cand);
|
|
700
|
+
if (my !== cand && step > 0) continue; // out of range
|
|
701
|
+
routes.push([from, {x: from.x, y: my}, {x: tx, y: my}, {x: tx, y: ty}]);
|
|
664
702
|
}
|
|
665
703
|
}
|
|
704
|
+
}
|
|
666
705
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
}
|
|
706
|
+
routes.push(...this._aroundRoutes(from, tx, ty, ignore, false));
|
|
707
|
+
|
|
708
|
+
// Side-step: drop to m1, jog sideways to sx, descend, and approach the
|
|
709
|
+
// input from above. The drop and the final approach share one run-in depth
|
|
710
|
+
// (equal lengths by construction); scan that depth and the jog.
|
|
711
|
+
const half = Math.floor(span / 2 / 10) * 10;
|
|
712
|
+
const baseDepth = Math.max(10, Math.round(span / 4 / 10) * 10);
|
|
713
|
+
for (let dStep = 0; dStep <= 12; dStep++) {
|
|
714
|
+
for (const depth of dStep === 0 ? [baseDepth] : [baseDepth + dStep * 10, baseDepth - dStep * 10]) {
|
|
715
|
+
if (depth < 10 || depth > half) continue;
|
|
716
|
+
const m1 = from.y + depth;
|
|
717
|
+
const m2 = ty - depth;
|
|
718
|
+
if (m2 < m1) continue;
|
|
719
|
+
for (let sStep = 1; sStep <= 14; sStep++) {
|
|
720
|
+
for (const dir of [1, -1]) {
|
|
721
|
+
const sx = from.x + dir * sStep * GRID_SIZE;
|
|
722
|
+
routes.push([
|
|
723
|
+
from,
|
|
724
|
+
{x: from.x, y: m1},
|
|
725
|
+
{x: sx, y: m1},
|
|
726
|
+
{x: sx, y: m2},
|
|
727
|
+
{x: tx, y: m2},
|
|
728
|
+
{x: tx, y: ty},
|
|
729
|
+
]);
|
|
692
730
|
}
|
|
693
731
|
}
|
|
694
732
|
}
|
|
695
|
-
return fallback!;
|
|
696
733
|
}
|
|
697
734
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
735
|
+
return routes;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Routes that leave the exit, run down the outside of everything they span
|
|
740
|
+
* vertically, and come back in above the input. Loop-backs always travel this
|
|
741
|
+
* way rather than squeezing through corridors inside the flow; a forward wire
|
|
742
|
+
* uses it when the corridor between it and its child is occupied.
|
|
743
|
+
*/
|
|
744
|
+
private _aroundRoutes(
|
|
745
|
+
from: { x: number; y: number },
|
|
746
|
+
tx: number,
|
|
747
|
+
ty: number,
|
|
748
|
+
ignore: Set<string>,
|
|
749
|
+
loop: boolean
|
|
750
|
+
): { x: number; y: number }[][] {
|
|
701
751
|
const stubY = from.y + WIRE_STUB;
|
|
702
752
|
const overY = ty - WIRE_APPROACH;
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
753
|
+
const spanned = this.nodes.filter(
|
|
754
|
+
n => !ignore.has(n.id) && n.y + NODE_HEIGHT > Math.min(stubY, overY) - GRID_SIZE
|
|
755
|
+
&& n.y < Math.max(stubY, overY) + GRID_SIZE
|
|
756
|
+
);
|
|
757
|
+
if (!spanned.length) return [];
|
|
758
|
+
|
|
759
|
+
const rightEdge = Math.max(...spanned.map(n => n.x + NODE_WIDTH)) + WIRE_DETOUR;
|
|
760
|
+
const leftEdge = Math.min(...spanned.map(n => n.x)) - WIRE_DETOUR;
|
|
761
|
+
const cost = (x: number) => Math.abs(x - from.x) + Math.abs(x - tx);
|
|
762
|
+
// A loop weighs both outsides; a forward wire hugs the side its child is
|
|
763
|
+
// already on, so the detour reads as a lean rather than a lap of the flow.
|
|
764
|
+
const preferRight = loop ? cost(rightEdge) <= cost(leftEdge) : tx >= (leftEdge + rightEdge) / 2;
|
|
765
|
+
|
|
766
|
+
const routes: { x: number; y: number }[][] = [];
|
|
767
|
+
for (const dir of preferRight ? [1, -1] : [-1, 1]) {
|
|
768
|
+
const edge = dir > 0 ? rightEdge : leftEdge;
|
|
769
|
+
for (let step = 0; step <= 20; step++) {
|
|
770
|
+
const dx = edge + dir * step * GRID_SIZE;
|
|
771
|
+
routes.push([
|
|
772
|
+
from,
|
|
773
|
+
{x: from.x, y: stubY},
|
|
774
|
+
{x: dx, y: stubY},
|
|
775
|
+
{x: dx, y: overY},
|
|
776
|
+
{x: tx, y: overY},
|
|
777
|
+
{x: tx, y: ty},
|
|
778
|
+
]);
|
|
779
|
+
}
|
|
728
780
|
}
|
|
729
|
-
return
|
|
781
|
+
return routes;
|
|
730
782
|
}
|
|
731
783
|
|
|
732
784
|
private static _pathFrom(points: { x: number; y: number }[]): string {
|
|
@@ -1018,7 +1070,7 @@ export default class ZnFlowCanvas extends ZincElement {
|
|
|
1018
1070
|
const loop = loops.has(b.conn);
|
|
1019
1071
|
const pts = this._routePoints({x: b.x, y: b.exitY}, b.child, b.conn.target.port, offset, node.id, loop);
|
|
1020
1072
|
paths.push(svg`<path
|
|
1021
|
-
class="wire ${loop ? 'wire--loop' : ''}"
|
|
1073
|
+
class="wire wire--link ${loop ? 'wire--loop' : ''}"
|
|
1022
1074
|
d="${ZnFlowCanvas._pathFrom(pts)}"
|
|
1023
1075
|
marker-end="url(#${loop ? 'flow-arrow-loop' : 'flow-arrow'})"
|
|
1024
1076
|
></path>`);
|
|
@@ -1075,10 +1127,11 @@ export default class ZnFlowCanvas extends ZincElement {
|
|
|
1075
1127
|
const loops = loopConnections(this.nodes, this.connections);
|
|
1076
1128
|
const items: {
|
|
1077
1129
|
key: string; nodeId: string; portId: string; label: string; x: number; top: number; height: number;
|
|
1078
|
-
loop: boolean; open: boolean;
|
|
1130
|
+
loop: boolean; open: boolean; fixed: boolean;
|
|
1079
1131
|
}[] = [];
|
|
1080
1132
|
for (const node of this.nodes) {
|
|
1081
1133
|
const {branches} = this._outputLayout(node);
|
|
1134
|
+
const fixed = !!this._typeFor(node)?.fixedOutputs;
|
|
1082
1135
|
for (const b of branches) {
|
|
1083
1136
|
if (b.pillTop === null) continue;
|
|
1084
1137
|
items.push({
|
|
@@ -1091,6 +1144,7 @@ export default class ZnFlowCanvas extends ZincElement {
|
|
|
1091
1144
|
height: b.pillH,
|
|
1092
1145
|
loop: !!b.conn && loops.has(b.conn),
|
|
1093
1146
|
open: !b.conn,
|
|
1147
|
+
fixed,
|
|
1094
1148
|
});
|
|
1095
1149
|
}
|
|
1096
1150
|
}
|
|
@@ -1111,17 +1165,18 @@ export default class ZnFlowCanvas extends ZincElement {
|
|
|
1111
1165
|
this._emit('flow-branch-pick', {nodeId: i.nodeId, port: i.portId});
|
|
1112
1166
|
}}"
|
|
1113
1167
|
>${i.label}</button>
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
e
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1168
|
+
${i.fixed ? '' : html`
|
|
1169
|
+
<button
|
|
1170
|
+
class="branch-pill-delete"
|
|
1171
|
+
title="Delete branch"
|
|
1172
|
+
@pointerdown="${(e: PointerEvent) => e.button === 0 && e.stopPropagation()}"
|
|
1173
|
+
@click="${(e: Event) => {
|
|
1174
|
+
e.stopPropagation();
|
|
1175
|
+
this._emit('flow-branch-delete', {nodeId: i.nodeId, port: i.portId});
|
|
1176
|
+
}}"
|
|
1177
|
+
>
|
|
1178
|
+
<zn-icon src="x@lu" size="14"></zn-icon>
|
|
1179
|
+
</button>`}
|
|
1125
1180
|
${i.open ? html`
|
|
1126
1181
|
<span
|
|
1127
1182
|
class="branch-pill-port"
|
|
@@ -42,6 +42,8 @@ export default class ZnFlowStep extends ZincElement {
|
|
|
42
42
|
@property() inputs: string;
|
|
43
43
|
/** JSON array of outputs (`"a"` or `{"id","label"}`), e.g. `'[{"id":"true","label":"TRUE"}]'`. Omit for one default output. */
|
|
44
44
|
@property() outputs: string;
|
|
45
|
+
/** The declared outputs are the only branches this step has — none can be deleted or added on the canvas. */
|
|
46
|
+
@property({attribute: 'fixed-outputs', type: Boolean}) fixedOutputs = false;
|
|
45
47
|
|
|
46
48
|
connectedCallback() {
|
|
47
49
|
super.connectedCallback();
|