astro-archify 0.3.4 → 0.3.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/README.md +70 -4
- package/package.json +6 -3
- package/vendor/archify/NOTICE.md +8 -5
- package/vendor/archify/assets/template.html +1 -1
- package/vendor/archify/renderers/architecture/render-architecture.mjs +1 -12
- package/vendor/archify/renderers/dataflow/render-dataflow.mjs +16 -15
- package/vendor/archify/renderers/lifecycle/render-lifecycle.mjs +1 -10
- package/vendor/archify/renderers/sequence/render-sequence.mjs +1 -16
- package/vendor/archify/renderers/shared/cli.mjs +1 -3
- package/vendor/archify/renderers/shared/diagnostics.mjs +17 -6
- package/vendor/archify/renderers/shared/generated-validators.mjs +2 -2
- package/vendor/archify/renderers/shared/geometry.mjs +129 -40
- package/vendor/archify/renderers/workflow/render-workflow.mjs +21 -735
- package/vendor/archify/renderers/workflow/workflow-compiler.mjs +4400 -0
- package/vendor/archify/renderers/workflow/workflow-migration-geometry.mjs +144 -0
|
@@ -344,7 +344,6 @@ export function cleanFlowProblems({
|
|
|
344
344
|
diagramType,
|
|
345
345
|
relationCollection,
|
|
346
346
|
obstacleKind,
|
|
347
|
-
profile,
|
|
348
347
|
clearance = 2,
|
|
349
348
|
routeHint = 'adjust fromSide/toSide, set route/via or channel coordinates, or move the obstacle'
|
|
350
349
|
}) {
|
|
@@ -399,6 +398,85 @@ export function cleanFlowProblems({
|
|
|
399
398
|
return problems;
|
|
400
399
|
}
|
|
401
400
|
|
|
401
|
+
// Build a read-only analysis copy of a polyline with straight-through
|
|
402
|
+
// waypoints removed. A waypoint on a forward-collinear run is not a visual
|
|
403
|
+
// endpoint, so treating it as one would hide a proper X that lands exactly on
|
|
404
|
+
// that waypoint. Reversals and real bends stay split: their shared point can
|
|
405
|
+
// still be an authored touch rather than a crossing. The source points are
|
|
406
|
+
// retained on every merged segment so diagnostics can name the authored
|
|
407
|
+
// segment that contains a hit without changing rendered/receipt geometry.
|
|
408
|
+
export function forwardCollinearAnalysisSegments(points) {
|
|
409
|
+
const segments = [];
|
|
410
|
+
for (let segmentIndex = 0; segmentIndex < asArray(points).length - 1; segmentIndex += 1) {
|
|
411
|
+
const authoredStart = points[segmentIndex];
|
|
412
|
+
const authoredEnd = points[segmentIndex + 1];
|
|
413
|
+
const start = Array.isArray(authoredStart) ? [...authoredStart] : authoredStart;
|
|
414
|
+
const end = Array.isArray(authoredEnd) ? [...authoredEnd] : authoredEnd;
|
|
415
|
+
const sourceSegment = { start, end, segmentIndex };
|
|
416
|
+
const previous = segments.at(-1);
|
|
417
|
+
if (previous && segmentsContinueForward(previous.start, previous.end, start, end)) {
|
|
418
|
+
previous.end = end;
|
|
419
|
+
previous.sourceSegments.push(sourceSegment);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
segments.push({
|
|
423
|
+
start,
|
|
424
|
+
end,
|
|
425
|
+
segmentIndex,
|
|
426
|
+
sourceSegments: [sourceSegment],
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
return segments;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export function sourceSegmentIndexAtPoint(segment, point) {
|
|
433
|
+
const source = asArray(segment?.sourceSegments).find(({ start, end }) => (
|
|
434
|
+
pointLiesOnSegment(point, start, end)
|
|
435
|
+
));
|
|
436
|
+
return source?.segmentIndex ?? segment?.segmentIndex ?? 0;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function authoredAnalysisSegments(points) {
|
|
440
|
+
return asArray(points).slice(0, -1).map((start, segmentIndex) => ({
|
|
441
|
+
start,
|
|
442
|
+
end: points[segmentIndex + 1],
|
|
443
|
+
segmentIndex,
|
|
444
|
+
sourceSegments: [{ start, end: points[segmentIndex + 1], segmentIndex }],
|
|
445
|
+
}));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function segmentsContinueForward(firstStart, firstEnd, secondStart, secondEnd) {
|
|
449
|
+
if (![firstStart, firstEnd, secondStart, secondEnd].every((point) => (
|
|
450
|
+
Array.isArray(point) && point.length === 2 && isFinitePoint(...point)
|
|
451
|
+
))) return false;
|
|
452
|
+
const epsilon = 0.0001;
|
|
453
|
+
if (Math.abs(firstEnd[0] - secondStart[0]) > epsilon
|
|
454
|
+
|| Math.abs(firstEnd[1] - secondStart[1]) > epsilon) return false;
|
|
455
|
+
const firstVector = [firstEnd[0] - firstStart[0], firstEnd[1] - firstStart[1]];
|
|
456
|
+
const secondVector = [secondEnd[0] - secondStart[0], secondEnd[1] - secondStart[1]];
|
|
457
|
+
const firstLength = Math.hypot(...firstVector);
|
|
458
|
+
const secondLength = Math.hypot(...secondVector);
|
|
459
|
+
if (firstLength <= epsilon || secondLength <= epsilon) return false;
|
|
460
|
+
const cross = firstVector[0] * secondVector[1] - firstVector[1] * secondVector[0];
|
|
461
|
+
if (Math.abs(cross) > epsilon) return false;
|
|
462
|
+
const dot = firstVector[0] * secondVector[0] + firstVector[1] * secondVector[1];
|
|
463
|
+
return dot > epsilon;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function pointLiesOnSegment(point, start, end) {
|
|
467
|
+
if (![point, start, end].every((candidate) => (
|
|
468
|
+
Array.isArray(candidate) && candidate.length === 2 && isFinitePoint(...candidate)
|
|
469
|
+
))) return false;
|
|
470
|
+
const epsilon = 0.0001;
|
|
471
|
+
const length = Math.hypot(end[0] - start[0], end[1] - start[1]);
|
|
472
|
+
if (length <= epsilon) return Math.hypot(point[0] - start[0], point[1] - start[1]) <= epsilon;
|
|
473
|
+
if (Math.abs(crossProduct(start, end, point)) > epsilon * length) return false;
|
|
474
|
+
return point[0] >= Math.min(start[0], end[0]) - epsilon
|
|
475
|
+
&& point[0] <= Math.max(start[0], end[0]) + epsilon
|
|
476
|
+
&& point[1] >= Math.min(start[1], end[1]) - epsilon
|
|
477
|
+
&& point[1] <= Math.max(start[1], end[1]) + epsilon;
|
|
478
|
+
}
|
|
479
|
+
|
|
402
480
|
// Reject only a proper interior X between relationships that share no semantic
|
|
403
481
|
// endpoint. Endpoint touches, branch/merge ports, and collinear shared
|
|
404
482
|
// corridors are intentionally outside this contract because geometry alone
|
|
@@ -410,17 +488,24 @@ export function cleanCrossingProblems({
|
|
|
410
488
|
diagramType,
|
|
411
489
|
relationCollection,
|
|
412
490
|
profile = 'standard',
|
|
491
|
+
profileIsAuthoritative = false,
|
|
492
|
+
mergeForwardCollinearWaypoints = false,
|
|
413
493
|
routeHint = 'adjust route/via or channel coordinates so the relationships use separate corridors'
|
|
414
494
|
}) {
|
|
415
|
-
|
|
416
|
-
const activeProfile = requestedProfile === 'showcase' ? 'showcase' : 'standard';
|
|
417
|
-
if (activeProfile !== 'showcase') return [];
|
|
495
|
+
if (qualityProfileForGate(profile, profileIsAuthoritative) !== 'showcase') return [];
|
|
418
496
|
const routed = asArray(relations).map((relation, index) => {
|
|
419
497
|
if (!relation || !endpointIds.has(relation.from) || !endpointIds.has(relation.to)) return null;
|
|
420
498
|
const points = pathFor(relation)?.points;
|
|
421
499
|
if (!Array.isArray(points) || points.length < 2) return null;
|
|
422
500
|
if (!points.every((point) => Array.isArray(point) && point.length === 2 && isFinitePoint(...point))) return null;
|
|
423
|
-
return {
|
|
501
|
+
return {
|
|
502
|
+
relation,
|
|
503
|
+
index,
|
|
504
|
+
points,
|
|
505
|
+
analysisSegments: mergeForwardCollinearWaypoints
|
|
506
|
+
? forwardCollinearAnalysisSegments(points)
|
|
507
|
+
: authoredAnalysisSegments(points),
|
|
508
|
+
};
|
|
424
509
|
}).filter(Boolean);
|
|
425
510
|
const problems = [];
|
|
426
511
|
|
|
@@ -431,16 +516,21 @@ export function cleanCrossingProblems({
|
|
|
431
516
|
if ([left.relation.from, left.relation.to].some((id) => id === right.relation.from || id === right.relation.to)) continue;
|
|
432
517
|
|
|
433
518
|
let hit = null;
|
|
434
|
-
for (
|
|
435
|
-
|
|
519
|
+
for (const leftSegment of left.analysisSegments) {
|
|
520
|
+
if (hit) break;
|
|
521
|
+
for (const rightSegment of right.analysisSegments) {
|
|
436
522
|
const point = properSegmentIntersection(
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
523
|
+
leftSegment.start,
|
|
524
|
+
leftSegment.end,
|
|
525
|
+
rightSegment.start,
|
|
526
|
+
rightSegment.end
|
|
441
527
|
);
|
|
442
528
|
if (point) {
|
|
443
|
-
hit = {
|
|
529
|
+
hit = {
|
|
530
|
+
point,
|
|
531
|
+
leftSegment: sourceSegmentIndexAtPoint(leftSegment, point),
|
|
532
|
+
rightSegment: sourceSegmentIndexAtPoint(rightSegment, point),
|
|
533
|
+
};
|
|
444
534
|
break;
|
|
445
535
|
}
|
|
446
536
|
}
|
|
@@ -536,16 +626,12 @@ export function cleanAmbiguousCorridorProblems({
|
|
|
536
626
|
diagramType,
|
|
537
627
|
relationCollection,
|
|
538
628
|
profile = 'standard',
|
|
629
|
+
profileIsAuthoritative = false,
|
|
539
630
|
routeHint = 'adjust route/via or channel coordinates so the relationships use separate corridors',
|
|
540
631
|
minOverlapPx = 8,
|
|
541
632
|
}) {
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
const routedRelations = asArray(relations).map((relation, relationIndex) => {
|
|
545
|
-
if (!relation || typeof relation.from !== 'string' || typeof relation.to !== 'string') return null;
|
|
546
|
-
if (endpointIds && (!endpointIds.has(relation.from) || !endpointIds.has(relation.to))) return null;
|
|
547
|
-
return { relation, relationIndex, points: pathFor(relation)?.points };
|
|
548
|
-
}).filter(Boolean);
|
|
633
|
+
if (qualityProfileForGate(profile, profileIsAuthoritative) !== 'showcase') return [];
|
|
634
|
+
const routedRelations = collectEligibleRoutedRelations({ relations, endpointIds, pathFor });
|
|
549
635
|
|
|
550
636
|
return collectAmbiguousCorridors({ routedRelations, minOverlapPx }).map((hit) => {
|
|
551
637
|
const describe = ({ relation, relationIndex }) => {
|
|
@@ -634,14 +720,11 @@ export function cleanBorderRunProblems({
|
|
|
634
720
|
diagramType,
|
|
635
721
|
relationCollection,
|
|
636
722
|
profile,
|
|
723
|
+
profileIsAuthoritative = false,
|
|
637
724
|
routeHint = 'adjust route/via or channel coordinates so the relationship crosses the frame perpendicularly through a clear opening'
|
|
638
725
|
}) {
|
|
639
|
-
if (!
|
|
640
|
-
const routedRelations =
|
|
641
|
-
if (!relation || typeof relation.from !== 'string' || typeof relation.to !== 'string') return null;
|
|
642
|
-
if (endpointIds && (!endpointIds.has(relation.from) || !endpointIds.has(relation.to))) return null;
|
|
643
|
-
return { relation, relationIndex, points: pathFor(relation)?.points };
|
|
644
|
-
}).filter(Boolean);
|
|
726
|
+
if (!qualityProfileForGate(profile, profileIsAuthoritative)) return [];
|
|
727
|
+
const routedRelations = collectEligibleRoutedRelations({ relations, endpointIds, pathFor });
|
|
645
728
|
return collectBorderRuns({ routedRelations, frames }).map((hit) => {
|
|
646
729
|
const relation = hit.relation || {};
|
|
647
730
|
const relationId = relation.id ? ` id "${relation.id}"` : '';
|
|
@@ -779,17 +862,13 @@ export function cleanRouteRhythmProblems({
|
|
|
779
862
|
diagramType,
|
|
780
863
|
relationCollection,
|
|
781
864
|
profile,
|
|
865
|
+
profileIsAuthoritative = false,
|
|
782
866
|
routeHint = 'move the channel/via point to remove the cramped turn or give the route more corridor space',
|
|
783
867
|
interiorSegmentPx = 16,
|
|
784
868
|
microSegmentPx = 8,
|
|
785
869
|
}) {
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
const routedRelations = asArray(relations).map((relation, relationIndex) => {
|
|
789
|
-
if (!relation || typeof relation.from !== 'string' || typeof relation.to !== 'string') return null;
|
|
790
|
-
if (endpointIds && (!endpointIds.has(relation.from) || !endpointIds.has(relation.to))) return null;
|
|
791
|
-
return { relation, relationIndex, points: pathFor(relation)?.points };
|
|
792
|
-
}).filter(Boolean);
|
|
870
|
+
if (qualityProfileForGate(profile, profileIsAuthoritative) !== 'showcase') return [];
|
|
871
|
+
const routedRelations = collectEligibleRoutedRelations({ relations, endpointIds, pathFor });
|
|
793
872
|
return collectRouteRhythmIssues({ routedRelations, interiorSegmentPx, microSegmentPx }).map((hit) => {
|
|
794
873
|
const relation = hit.relation || {};
|
|
795
874
|
const relationId = relation.id ? ` id "${relation.id}"` : '';
|
|
@@ -827,16 +906,12 @@ export function cleanLabelRouteClearanceProblems({
|
|
|
827
906
|
diagramType,
|
|
828
907
|
relationCollection,
|
|
829
908
|
profile,
|
|
909
|
+
profileIsAuthoritative = false,
|
|
830
910
|
threshold = 4,
|
|
831
911
|
routeHint = 'adjust labelAt, labelDx, labelDy, or labelSegment; otherwise adjust the other relationship route/via/channel',
|
|
832
912
|
}) {
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
const routedRelations = asArray(relations).map((relation, relationIndex) => {
|
|
836
|
-
if (!relation || typeof relation.from !== 'string' || typeof relation.to !== 'string') return null;
|
|
837
|
-
if (endpointIds && (!endpointIds.has(relation.from) || !endpointIds.has(relation.to))) return null;
|
|
838
|
-
return { relation, relationIndex, points: pathFor(relation)?.points };
|
|
839
|
-
}).filter(Boolean);
|
|
913
|
+
if (qualityProfileForGate(profile, profileIsAuthoritative) !== 'showcase') return [];
|
|
914
|
+
const routedRelations = collectEligibleRoutedRelations({ relations, endpointIds, pathFor });
|
|
840
915
|
return collectLabelRouteClearance({ labels, routedRelations, threshold }).map((hit) => {
|
|
841
916
|
const describe = (relation, relationIndex) => {
|
|
842
917
|
const relationId = relation?.id ? ` id "${relation.id}"` : '';
|
|
@@ -868,6 +943,20 @@ export function cleanLabelRouteClearanceProblems({
|
|
|
868
943
|
});
|
|
869
944
|
}
|
|
870
945
|
|
|
946
|
+
function qualityProfileForGate(profile, profileIsAuthoritative) {
|
|
947
|
+
return profileIsAuthoritative
|
|
948
|
+
? profile
|
|
949
|
+
: process.env.ARCHIFY_QUALITY_PROFILE || profile;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function collectEligibleRoutedRelations({ relations, endpointIds, pathFor }) {
|
|
953
|
+
return asArray(relations).map((relation, relationIndex) => {
|
|
954
|
+
if (!relation || typeof relation.from !== 'string' || typeof relation.to !== 'string') return null;
|
|
955
|
+
if (endpointIds && (!endpointIds.has(relation.from) || !endpointIds.has(relation.to))) return null;
|
|
956
|
+
return { relation, relationIndex, points: pathFor(relation)?.points };
|
|
957
|
+
}).filter(Boolean);
|
|
958
|
+
}
|
|
959
|
+
|
|
871
960
|
function segmentPosition(index, segmentCount) {
|
|
872
961
|
if (index === 0) return 'source-stub';
|
|
873
962
|
if (index === segmentCount - 1) return 'target-stub';
|
|
@@ -1319,7 +1408,7 @@ export function suggestLabelObstacleFix(labelRect, lx, ly, obstacle, obstacleKin
|
|
|
1319
1408
|
export function suggestLabelPairFix(a, b) {
|
|
1320
1409
|
return [
|
|
1321
1410
|
` "${a.label}" ${formatRect(a)}; "${b.label}" ${formatRect(b)}`,
|
|
1322
|
-
' Suggested fix:
|
|
1411
|
+
' Suggested fix: adjust labelDx/labelDy/labelSegment, or route one relationship through a separate corridor',
|
|
1323
1412
|
].join('\n');
|
|
1324
1413
|
}
|
|
1325
1414
|
|