@tscircuit/hypergraph 0.0.17 → 0.0.19
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/index.d.ts +173 -1
- package/dist/index.js +839 -0
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -276,6 +276,7 @@ declare class HyperGraphSolver<RegionType extends Region = Region, RegionPortTyp
|
|
|
276
276
|
rippingEnabled?: boolean;
|
|
277
277
|
ripCost?: number;
|
|
278
278
|
};
|
|
279
|
+
getSolverName(): string;
|
|
279
280
|
graph: HyperGraph;
|
|
280
281
|
connections: Connection[];
|
|
281
282
|
candidateQueue: PriorityQueue<Candidate>;
|
|
@@ -379,6 +380,7 @@ declare const JUMPER_GRAPH_SOLVER_DEFAULTS: {
|
|
|
379
380
|
greedyMultiplier: number;
|
|
380
381
|
};
|
|
381
382
|
declare class JumperGraphSolver extends HyperGraphSolver<JRegion, JPort> {
|
|
383
|
+
getSolverName(): string;
|
|
382
384
|
UNIT_OF_COST: string;
|
|
383
385
|
portUsagePenalty: number;
|
|
384
386
|
portUsagePenaltySq: number;
|
|
@@ -423,4 +425,174 @@ declare const rotateGraph90Degrees: (graph: JumperGraph) => JumperGraph;
|
|
|
423
425
|
|
|
424
426
|
declare const calculateGraphBounds: (regions: JRegion[]) => Bounds;
|
|
425
427
|
|
|
426
|
-
|
|
428
|
+
type RegionRef = {
|
|
429
|
+
id: string;
|
|
430
|
+
};
|
|
431
|
+
type PortSpread = {
|
|
432
|
+
kind: "midpoint";
|
|
433
|
+
} | {
|
|
434
|
+
kind: "even";
|
|
435
|
+
inset?: "half" | number;
|
|
436
|
+
} | {
|
|
437
|
+
kind: "fractions";
|
|
438
|
+
ts: number[];
|
|
439
|
+
};
|
|
440
|
+
type ConnectOpts = {
|
|
441
|
+
idPrefix?: string;
|
|
442
|
+
tolerance?: number;
|
|
443
|
+
};
|
|
444
|
+
type ValidateOpts = {
|
|
445
|
+
tolerance?: number;
|
|
446
|
+
allowNonBoundaryPorts?: boolean;
|
|
447
|
+
};
|
|
448
|
+
type BuildOpts = {
|
|
449
|
+
validate?: boolean;
|
|
450
|
+
tolerance?: number;
|
|
451
|
+
};
|
|
452
|
+
type SharedBoundary = {
|
|
453
|
+
axis: "vertical" | "horizontal";
|
|
454
|
+
position: number;
|
|
455
|
+
min: number;
|
|
456
|
+
max: number;
|
|
457
|
+
};
|
|
458
|
+
type RegionData = {
|
|
459
|
+
id: string;
|
|
460
|
+
bounds: Bounds | null;
|
|
461
|
+
center: {
|
|
462
|
+
x: number;
|
|
463
|
+
y: number;
|
|
464
|
+
} | null;
|
|
465
|
+
width: number | null;
|
|
466
|
+
height: number | null;
|
|
467
|
+
anchor: "center" | "min";
|
|
468
|
+
isPad: boolean;
|
|
469
|
+
isThroughJumper: boolean;
|
|
470
|
+
isConnectionRegion: boolean;
|
|
471
|
+
meta: Record<string, unknown>;
|
|
472
|
+
};
|
|
473
|
+
type PortData = {
|
|
474
|
+
id: string;
|
|
475
|
+
region1Id: string;
|
|
476
|
+
region2Id: string;
|
|
477
|
+
x: number;
|
|
478
|
+
y: number;
|
|
479
|
+
};
|
|
480
|
+
declare class TopologyError extends Error {
|
|
481
|
+
details?: {
|
|
482
|
+
regionIds?: string[];
|
|
483
|
+
relationship?: string;
|
|
484
|
+
suggestion?: string;
|
|
485
|
+
} | undefined;
|
|
486
|
+
constructor(message: string, details?: {
|
|
487
|
+
regionIds?: string[];
|
|
488
|
+
relationship?: string;
|
|
489
|
+
suggestion?: string;
|
|
490
|
+
} | undefined);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
declare class RegionBuilder implements RegionRef {
|
|
494
|
+
private data;
|
|
495
|
+
constructor(id: string);
|
|
496
|
+
get id(): string;
|
|
497
|
+
rect(b: Bounds): this;
|
|
498
|
+
center(x: number, y: number): this;
|
|
499
|
+
size(w: number, h: number, anchor?: "center" | "min"): this;
|
|
500
|
+
pad(isPad?: boolean): this;
|
|
501
|
+
throughJumper(isTJ?: boolean): this;
|
|
502
|
+
connectionRegion(isConn?: boolean): this;
|
|
503
|
+
meta(extra: Record<string, unknown>): this;
|
|
504
|
+
getData(): RegionData;
|
|
505
|
+
ref(): RegionRef;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
type AddPortFn$1 = (port: PortData) => void;
|
|
509
|
+
type PrefixIdFn = (id: string) => string;
|
|
510
|
+
declare class ConnectBuilder {
|
|
511
|
+
private region1Id;
|
|
512
|
+
private region2Id;
|
|
513
|
+
private bounds1;
|
|
514
|
+
private bounds2;
|
|
515
|
+
private tolerance;
|
|
516
|
+
private prefix;
|
|
517
|
+
private prefixIdFn;
|
|
518
|
+
private addPortFn;
|
|
519
|
+
private portIds;
|
|
520
|
+
private boundary;
|
|
521
|
+
private prefixWasExplicitlySet;
|
|
522
|
+
constructor(region1Id: string, region2Id: string, bounds1: Bounds, bounds2: Bounds, addPortFn: AddPortFn$1, prefixIdFn: PrefixIdFn, opts?: {
|
|
523
|
+
idPrefix?: string;
|
|
524
|
+
tolerance?: number;
|
|
525
|
+
});
|
|
526
|
+
private ensureBoundary;
|
|
527
|
+
idPrefix(prefix: string): this;
|
|
528
|
+
private getFullPrefix;
|
|
529
|
+
ports(count: number, spread?: PortSpread): this;
|
|
530
|
+
at(t: number): this;
|
|
531
|
+
atXY(p: {
|
|
532
|
+
x: number;
|
|
533
|
+
y: number;
|
|
534
|
+
}): this;
|
|
535
|
+
getPortIds(): string[];
|
|
536
|
+
finalizeIfNeeded(): void;
|
|
537
|
+
hasPortsAdded(): boolean;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
type ResolveRegionFn = (ref: RegionRef | string) => {
|
|
541
|
+
id: string;
|
|
542
|
+
bounds: Bounds;
|
|
543
|
+
};
|
|
544
|
+
type AddPortFn = (port: PortData) => void;
|
|
545
|
+
declare class PortBuilder {
|
|
546
|
+
private portId;
|
|
547
|
+
private region1Id;
|
|
548
|
+
private region2Id;
|
|
549
|
+
private position;
|
|
550
|
+
private tValue;
|
|
551
|
+
private resolveRegionFn;
|
|
552
|
+
private addPortFn;
|
|
553
|
+
private tolerance;
|
|
554
|
+
constructor(id: string, resolveRegionFn: ResolveRegionFn, addPortFn: AddPortFn, tolerance?: number);
|
|
555
|
+
id(id: string): this;
|
|
556
|
+
between(a: RegionRef | string, b: RegionRef | string): this;
|
|
557
|
+
onSharedEdge(t?: number): this;
|
|
558
|
+
at(p: {
|
|
559
|
+
x: number;
|
|
560
|
+
y: number;
|
|
561
|
+
}): this;
|
|
562
|
+
done(): void;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
declare class Topology {
|
|
566
|
+
private regions;
|
|
567
|
+
private ports;
|
|
568
|
+
private portIds;
|
|
569
|
+
private pendingConnections;
|
|
570
|
+
private scopePrefix;
|
|
571
|
+
private defaultTolerance;
|
|
572
|
+
private prefixId;
|
|
573
|
+
private generateRegionId;
|
|
574
|
+
private generatePortId;
|
|
575
|
+
region(id?: string): RegionBuilder;
|
|
576
|
+
getRegion(id: string): RegionRef;
|
|
577
|
+
private resolveRegion;
|
|
578
|
+
private addPort;
|
|
579
|
+
connect(a: RegionRef | string, b: RegionRef | string, opts?: ConnectOpts): ConnectBuilder;
|
|
580
|
+
port(id?: string): PortBuilder;
|
|
581
|
+
scope(prefix: string, fn: (t: Topology) => void): void;
|
|
582
|
+
validate(opts?: ValidateOpts): void;
|
|
583
|
+
toJumperGraph(opts?: BuildOpts): JumperGraph;
|
|
584
|
+
merge(graph: JumperGraph, opts?: {
|
|
585
|
+
prefix?: string;
|
|
586
|
+
transform?: (p: {
|
|
587
|
+
x: number;
|
|
588
|
+
y: number;
|
|
589
|
+
}) => {
|
|
590
|
+
x: number;
|
|
591
|
+
y: number;
|
|
592
|
+
};
|
|
593
|
+
}): void;
|
|
594
|
+
getRegionIds(): string[];
|
|
595
|
+
getPortIds(): string[];
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export { type Bounds, type BuildOpts, type Candidate, ConnectBuilder, type ConnectOpts, type Connection, type ConnectionId, type GScore, type GraphEdgeId, type HyperGraph, HyperGraphSolver, type JPort, type JRegion, JUMPER_GRAPH_SOLVER_DEFAULTS, type JumperGraph, JumperGraphSolver, type JumperGraphWithConnections, type NetworkId, type PortAssignment, PortBuilder, type PortData, type PortId, type PortSpread, type Region, RegionBuilder, type RegionData, type RegionId, type RegionPort, type RegionPortAssignment, type RegionRef, type SerializedConnection, type SerializedGraphPort, type SerializedGraphRegion, type SerializedHyperGraph, type SerializedRegionPortAssignment, type SharedBoundary, type SolvedRoute, Topology, TopologyError, type ValidateOpts, type XYConnection, applyTransformToGraph, calculateGraphBounds, convertConnectionsToSerializedConnections, convertHyperGraphToSerializedHyperGraph, createGraphWithConnectionsFromBaseGraph, generateJumperGrid, generateJumperX4Grid, rotateGraph90Degrees };
|
package/dist/index.js
CHANGED
|
@@ -1829,6 +1829,9 @@ var HyperGraphSolver = class extends BaseSolver {
|
|
|
1829
1829
|
this.candidateQueue = new PriorityQueue();
|
|
1830
1830
|
this.beginNewConnection();
|
|
1831
1831
|
}
|
|
1832
|
+
getSolverName() {
|
|
1833
|
+
return "HyperGraphSolver";
|
|
1834
|
+
}
|
|
1832
1835
|
graph;
|
|
1833
1836
|
connections;
|
|
1834
1837
|
candidateQueue;
|
|
@@ -2486,6 +2489,9 @@ var JUMPER_GRAPH_SOLVER_DEFAULTS = {
|
|
|
2486
2489
|
greedyMultiplier: 0.5518001238069296
|
|
2487
2490
|
};
|
|
2488
2491
|
var JumperGraphSolver = class extends HyperGraphSolver {
|
|
2492
|
+
getSolverName() {
|
|
2493
|
+
return "JumperGraphSolver";
|
|
2494
|
+
}
|
|
2489
2495
|
UNIT_OF_COST = "hops";
|
|
2490
2496
|
portUsagePenalty = JUMPER_GRAPH_SOLVER_DEFAULTS.portUsagePenalty;
|
|
2491
2497
|
portUsagePenaltySq = JUMPER_GRAPH_SOLVER_DEFAULTS.portUsagePenaltySq;
|
|
@@ -2567,10 +2573,843 @@ var JumperGraphSolver = class extends HyperGraphSolver {
|
|
|
2567
2573
|
return visualizeJumperGraphSolver(this);
|
|
2568
2574
|
}
|
|
2569
2575
|
};
|
|
2576
|
+
|
|
2577
|
+
// lib/topology/types.ts
|
|
2578
|
+
var TopologyError = class extends Error {
|
|
2579
|
+
constructor(message, details) {
|
|
2580
|
+
super(message);
|
|
2581
|
+
this.details = details;
|
|
2582
|
+
this.name = "TopologyError";
|
|
2583
|
+
}
|
|
2584
|
+
};
|
|
2585
|
+
|
|
2586
|
+
// lib/topology/RegionBuilder.ts
|
|
2587
|
+
var RegionBuilder = class {
|
|
2588
|
+
data;
|
|
2589
|
+
constructor(id) {
|
|
2590
|
+
this.data = {
|
|
2591
|
+
id,
|
|
2592
|
+
bounds: null,
|
|
2593
|
+
center: null,
|
|
2594
|
+
width: null,
|
|
2595
|
+
height: null,
|
|
2596
|
+
anchor: "center",
|
|
2597
|
+
isPad: false,
|
|
2598
|
+
isThroughJumper: false,
|
|
2599
|
+
isConnectionRegion: false,
|
|
2600
|
+
meta: {}
|
|
2601
|
+
};
|
|
2602
|
+
}
|
|
2603
|
+
get id() {
|
|
2604
|
+
return this.data.id;
|
|
2605
|
+
}
|
|
2606
|
+
// Geometry methods
|
|
2607
|
+
rect(b) {
|
|
2608
|
+
this.data.bounds = { ...b };
|
|
2609
|
+
this.data.center = null;
|
|
2610
|
+
this.data.width = null;
|
|
2611
|
+
this.data.height = null;
|
|
2612
|
+
return this;
|
|
2613
|
+
}
|
|
2614
|
+
center(x, y) {
|
|
2615
|
+
this.data.center = { x, y };
|
|
2616
|
+
this.data.bounds = null;
|
|
2617
|
+
return this;
|
|
2618
|
+
}
|
|
2619
|
+
size(w, h, anchor = "center") {
|
|
2620
|
+
if (w <= 0 || h <= 0) {
|
|
2621
|
+
throw new TopologyError(
|
|
2622
|
+
`Region "${this.data.id}" has invalid size: width (${w}) and height (${h}) must be positive`,
|
|
2623
|
+
{
|
|
2624
|
+
regionIds: [this.data.id],
|
|
2625
|
+
suggestion: "Use positive values for width and height"
|
|
2626
|
+
}
|
|
2627
|
+
);
|
|
2628
|
+
}
|
|
2629
|
+
this.data.width = w;
|
|
2630
|
+
this.data.height = h;
|
|
2631
|
+
this.data.anchor = anchor;
|
|
2632
|
+
this.data.bounds = null;
|
|
2633
|
+
return this;
|
|
2634
|
+
}
|
|
2635
|
+
// Semantic methods
|
|
2636
|
+
pad(isPad = true) {
|
|
2637
|
+
this.data.isPad = isPad;
|
|
2638
|
+
return this;
|
|
2639
|
+
}
|
|
2640
|
+
throughJumper(isTJ = true) {
|
|
2641
|
+
this.data.isThroughJumper = isTJ;
|
|
2642
|
+
return this;
|
|
2643
|
+
}
|
|
2644
|
+
connectionRegion(isConn = true) {
|
|
2645
|
+
this.data.isConnectionRegion = isConn;
|
|
2646
|
+
return this;
|
|
2647
|
+
}
|
|
2648
|
+
meta(extra) {
|
|
2649
|
+
this.data.meta = { ...this.data.meta, ...extra };
|
|
2650
|
+
return this;
|
|
2651
|
+
}
|
|
2652
|
+
// Get the internal data (used by Topology)
|
|
2653
|
+
getData() {
|
|
2654
|
+
return this.data;
|
|
2655
|
+
}
|
|
2656
|
+
// Get a reference to this region
|
|
2657
|
+
ref() {
|
|
2658
|
+
return { id: this.data.id };
|
|
2659
|
+
}
|
|
2660
|
+
};
|
|
2661
|
+
|
|
2662
|
+
// lib/topology/utils.ts
|
|
2663
|
+
var DEFAULT_TOLERANCE = 1e-3;
|
|
2664
|
+
function computeBoundsFromRegionData(data) {
|
|
2665
|
+
if (data.bounds) {
|
|
2666
|
+
return data.bounds;
|
|
2667
|
+
}
|
|
2668
|
+
if (data.center && data.width !== null && data.height !== null) {
|
|
2669
|
+
const halfW = data.width / 2;
|
|
2670
|
+
const halfH = data.height / 2;
|
|
2671
|
+
if (data.anchor === "center") {
|
|
2672
|
+
return {
|
|
2673
|
+
minX: data.center.x - halfW,
|
|
2674
|
+
maxX: data.center.x + halfW,
|
|
2675
|
+
minY: data.center.y - halfH,
|
|
2676
|
+
maxY: data.center.y + halfH
|
|
2677
|
+
};
|
|
2678
|
+
} else {
|
|
2679
|
+
return {
|
|
2680
|
+
minX: data.center.x,
|
|
2681
|
+
maxX: data.center.x + data.width,
|
|
2682
|
+
minY: data.center.y,
|
|
2683
|
+
maxY: data.center.y + data.height
|
|
2684
|
+
};
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
throw new TopologyError(`Region "${data.id}" has incomplete geometry`, {
|
|
2688
|
+
regionIds: [data.id],
|
|
2689
|
+
suggestion: "Use .rect() or .center().size() to define the region geometry"
|
|
2690
|
+
});
|
|
2691
|
+
}
|
|
2692
|
+
function computeCenterFromBounds(bounds) {
|
|
2693
|
+
return {
|
|
2694
|
+
x: (bounds.minX + bounds.maxX) / 2,
|
|
2695
|
+
y: (bounds.minY + bounds.maxY) / 2
|
|
2696
|
+
};
|
|
2697
|
+
}
|
|
2698
|
+
function validateBounds(bounds, regionId) {
|
|
2699
|
+
if (bounds.minX >= bounds.maxX) {
|
|
2700
|
+
throw new TopologyError(
|
|
2701
|
+
`Region "${regionId}" has invalid bounds: minX (${bounds.minX}) >= maxX (${bounds.maxX})`,
|
|
2702
|
+
{
|
|
2703
|
+
regionIds: [regionId],
|
|
2704
|
+
suggestion: "Ensure minX < maxX"
|
|
2705
|
+
}
|
|
2706
|
+
);
|
|
2707
|
+
}
|
|
2708
|
+
if (bounds.minY >= bounds.maxY) {
|
|
2709
|
+
throw new TopologyError(
|
|
2710
|
+
`Region "${regionId}" has invalid bounds: minY (${bounds.minY}) >= maxY (${bounds.maxY})`,
|
|
2711
|
+
{
|
|
2712
|
+
regionIds: [regionId],
|
|
2713
|
+
suggestion: "Ensure minY < maxY"
|
|
2714
|
+
}
|
|
2715
|
+
);
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
function findSharedBoundary(boundsA, boundsB, regionIdA, regionIdB, tolerance = DEFAULT_TOLERANCE) {
|
|
2719
|
+
const verticalOverlapMin = Math.max(boundsA.minY, boundsB.minY);
|
|
2720
|
+
const verticalOverlapMax = Math.min(boundsA.maxY, boundsB.maxY);
|
|
2721
|
+
const verticalOverlapLength = verticalOverlapMax - verticalOverlapMin;
|
|
2722
|
+
const horizontalOverlapMin = Math.max(boundsA.minX, boundsB.minX);
|
|
2723
|
+
const horizontalOverlapMax = Math.min(boundsA.maxX, boundsB.maxX);
|
|
2724
|
+
const horizontalOverlapLength = horizontalOverlapMax - horizontalOverlapMin;
|
|
2725
|
+
if (Math.abs(boundsA.maxX - boundsB.minX) < tolerance && verticalOverlapLength > tolerance) {
|
|
2726
|
+
return {
|
|
2727
|
+
axis: "vertical",
|
|
2728
|
+
position: boundsA.maxX,
|
|
2729
|
+
min: verticalOverlapMin,
|
|
2730
|
+
max: verticalOverlapMax
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2733
|
+
if (Math.abs(boundsA.minX - boundsB.maxX) < tolerance && verticalOverlapLength > tolerance) {
|
|
2734
|
+
return {
|
|
2735
|
+
axis: "vertical",
|
|
2736
|
+
position: boundsA.minX,
|
|
2737
|
+
min: verticalOverlapMin,
|
|
2738
|
+
max: verticalOverlapMax
|
|
2739
|
+
};
|
|
2740
|
+
}
|
|
2741
|
+
if (Math.abs(boundsA.maxY - boundsB.minY) < tolerance && horizontalOverlapLength > tolerance) {
|
|
2742
|
+
return {
|
|
2743
|
+
axis: "horizontal",
|
|
2744
|
+
position: boundsA.maxY,
|
|
2745
|
+
min: horizontalOverlapMin,
|
|
2746
|
+
max: horizontalOverlapMax
|
|
2747
|
+
};
|
|
2748
|
+
}
|
|
2749
|
+
if (Math.abs(boundsA.minY - boundsB.maxY) < tolerance && horizontalOverlapLength > tolerance) {
|
|
2750
|
+
return {
|
|
2751
|
+
axis: "horizontal",
|
|
2752
|
+
position: boundsA.minY,
|
|
2753
|
+
min: horizontalOverlapMin,
|
|
2754
|
+
max: horizontalOverlapMax
|
|
2755
|
+
};
|
|
2756
|
+
}
|
|
2757
|
+
const relationship = describeRelationship(boundsA, boundsB);
|
|
2758
|
+
throw new TopologyError(
|
|
2759
|
+
`Regions "${regionIdA}" and "${regionIdB}" do not share a boundary`,
|
|
2760
|
+
{
|
|
2761
|
+
regionIds: [regionIdA, regionIdB],
|
|
2762
|
+
relationship,
|
|
2763
|
+
suggestion: "Use .port().between().at() for non-boundary ports, or check region positions"
|
|
2764
|
+
}
|
|
2765
|
+
);
|
|
2766
|
+
}
|
|
2767
|
+
function describeRelationship(boundsA, boundsB) {
|
|
2768
|
+
const centerA = computeCenterFromBounds(boundsA);
|
|
2769
|
+
const centerB = computeCenterFromBounds(boundsB);
|
|
2770
|
+
const dx = centerB.x - centerA.x;
|
|
2771
|
+
const dy = centerB.y - centerA.y;
|
|
2772
|
+
let direction;
|
|
2773
|
+
if (Math.abs(dx) > Math.abs(dy)) {
|
|
2774
|
+
direction = dx > 0 ? "to the right of" : "to the left of";
|
|
2775
|
+
} else {
|
|
2776
|
+
direction = dy > 0 ? "above" : "below";
|
|
2777
|
+
}
|
|
2778
|
+
const overlapsX = boundsA.maxX > boundsB.minX && boundsB.maxX > boundsA.minX;
|
|
2779
|
+
const overlapsY = boundsA.maxY > boundsB.minY && boundsB.maxY > boundsA.minY;
|
|
2780
|
+
if (overlapsX && overlapsY) {
|
|
2781
|
+
return `Regions overlap (A center: ${centerA.x.toFixed(2)}, ${centerA.y.toFixed(2)}; B center: ${centerB.x.toFixed(2)}, ${centerB.y.toFixed(2)})`;
|
|
2782
|
+
}
|
|
2783
|
+
const touchesAtCornerX = Math.abs(boundsA.maxX - boundsB.minX) < 1e-3 || Math.abs(boundsA.minX - boundsB.maxX) < 1e-3;
|
|
2784
|
+
const touchesAtCornerY = Math.abs(boundsA.maxY - boundsB.minY) < 1e-3 || Math.abs(boundsA.minY - boundsB.maxY) < 1e-3;
|
|
2785
|
+
if (touchesAtCornerX && touchesAtCornerY) {
|
|
2786
|
+
return `Regions touch only at a corner (B is ${direction} A)`;
|
|
2787
|
+
}
|
|
2788
|
+
return `B is ${direction} A with no shared edge`;
|
|
2789
|
+
}
|
|
2790
|
+
function computePortPositionOnBoundary(boundary, t) {
|
|
2791
|
+
const pos = boundary.min + t * (boundary.max - boundary.min);
|
|
2792
|
+
if (boundary.axis === "vertical") {
|
|
2793
|
+
return { x: boundary.position, y: pos };
|
|
2794
|
+
} else {
|
|
2795
|
+
return { x: pos, y: boundary.position };
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
function computeEvenPortPositions(boundary, count, inset = "half") {
|
|
2799
|
+
const positions = [];
|
|
2800
|
+
const length = boundary.max - boundary.min;
|
|
2801
|
+
for (let i = 0; i < count; i++) {
|
|
2802
|
+
let t;
|
|
2803
|
+
if (inset === "half") {
|
|
2804
|
+
t = (i + 0.5) / count;
|
|
2805
|
+
} else {
|
|
2806
|
+
const insetRatio = inset / length;
|
|
2807
|
+
const usableRange = 1 - 2 * insetRatio;
|
|
2808
|
+
if (count === 1) {
|
|
2809
|
+
t = 0.5;
|
|
2810
|
+
} else {
|
|
2811
|
+
t = insetRatio + i / (count - 1) * usableRange;
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
positions.push(computePortPositionOnBoundary(boundary, t));
|
|
2815
|
+
}
|
|
2816
|
+
return positions;
|
|
2817
|
+
}
|
|
2818
|
+
function pointOnBoundary(point, bounds, tolerance = DEFAULT_TOLERANCE) {
|
|
2819
|
+
const onVerticalEdge = (Math.abs(point.x - bounds.minX) < tolerance || Math.abs(point.x - bounds.maxX) < tolerance) && point.y >= bounds.minY - tolerance && point.y <= bounds.maxY + tolerance;
|
|
2820
|
+
const onHorizontalEdge = (Math.abs(point.y - bounds.minY) < tolerance || Math.abs(point.y - bounds.maxY) < tolerance) && point.x >= bounds.minX - tolerance && point.x <= bounds.maxX + tolerance;
|
|
2821
|
+
return onVerticalEdge || onHorizontalEdge;
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
// lib/topology/ConnectBuilder.ts
|
|
2825
|
+
var ConnectBuilder = class {
|
|
2826
|
+
region1Id;
|
|
2827
|
+
region2Id;
|
|
2828
|
+
bounds1;
|
|
2829
|
+
bounds2;
|
|
2830
|
+
tolerance;
|
|
2831
|
+
prefix;
|
|
2832
|
+
prefixIdFn;
|
|
2833
|
+
addPortFn;
|
|
2834
|
+
portIds = [];
|
|
2835
|
+
boundary = null;
|
|
2836
|
+
prefixWasExplicitlySet = false;
|
|
2837
|
+
constructor(region1Id, region2Id, bounds1, bounds2, addPortFn, prefixIdFn, opts) {
|
|
2838
|
+
this.region1Id = region1Id;
|
|
2839
|
+
this.region2Id = region2Id;
|
|
2840
|
+
this.bounds1 = bounds1;
|
|
2841
|
+
this.bounds2 = bounds2;
|
|
2842
|
+
this.addPortFn = addPortFn;
|
|
2843
|
+
this.prefixIdFn = prefixIdFn;
|
|
2844
|
+
this.tolerance = opts?.tolerance ?? 1e-3;
|
|
2845
|
+
this.prefix = opts?.idPrefix ?? `${region1Id.split(":").pop()}-${region2Id.split(":").pop()}`;
|
|
2846
|
+
}
|
|
2847
|
+
ensureBoundary() {
|
|
2848
|
+
if (!this.boundary) {
|
|
2849
|
+
this.boundary = findSharedBoundary(
|
|
2850
|
+
this.bounds1,
|
|
2851
|
+
this.bounds2,
|
|
2852
|
+
this.region1Id,
|
|
2853
|
+
this.region2Id,
|
|
2854
|
+
this.tolerance
|
|
2855
|
+
);
|
|
2856
|
+
}
|
|
2857
|
+
return this.boundary;
|
|
2858
|
+
}
|
|
2859
|
+
idPrefix(prefix) {
|
|
2860
|
+
this.prefix = prefix;
|
|
2861
|
+
this.prefixWasExplicitlySet = true;
|
|
2862
|
+
return this;
|
|
2863
|
+
}
|
|
2864
|
+
getFullPrefix() {
|
|
2865
|
+
if (this.prefixWasExplicitlySet) {
|
|
2866
|
+
return this.prefixIdFn(this.prefix);
|
|
2867
|
+
}
|
|
2868
|
+
return this.prefixIdFn(this.prefix);
|
|
2869
|
+
}
|
|
2870
|
+
ports(count, spread) {
|
|
2871
|
+
if (count <= 0) {
|
|
2872
|
+
throw new TopologyError(
|
|
2873
|
+
`Invalid port count: ${count}. Must be at least 1`,
|
|
2874
|
+
{
|
|
2875
|
+
regionIds: [this.region1Id, this.region2Id],
|
|
2876
|
+
suggestion: "Use a positive integer for port count"
|
|
2877
|
+
}
|
|
2878
|
+
);
|
|
2879
|
+
}
|
|
2880
|
+
const boundary = this.ensureBoundary();
|
|
2881
|
+
let positions;
|
|
2882
|
+
if (!spread || spread.kind === "midpoint") {
|
|
2883
|
+
if (count === 1) {
|
|
2884
|
+
positions = [computePortPositionOnBoundary(boundary, 0.5)];
|
|
2885
|
+
} else {
|
|
2886
|
+
positions = computeEvenPortPositions(boundary, count, "half");
|
|
2887
|
+
}
|
|
2888
|
+
} else if (spread.kind === "even") {
|
|
2889
|
+
positions = computeEvenPortPositions(boundary, count, spread.inset);
|
|
2890
|
+
} else if (spread.kind === "fractions") {
|
|
2891
|
+
if (spread.ts.length !== count) {
|
|
2892
|
+
throw new TopologyError(
|
|
2893
|
+
`Port count (${count}) doesn't match fractions array length (${spread.ts.length})`,
|
|
2894
|
+
{
|
|
2895
|
+
regionIds: [this.region1Id, this.region2Id],
|
|
2896
|
+
suggestion: "Ensure fractions array has exactly `count` elements"
|
|
2897
|
+
}
|
|
2898
|
+
);
|
|
2899
|
+
}
|
|
2900
|
+
positions = spread.ts.map((t) => {
|
|
2901
|
+
if (t < 0 || t > 1) {
|
|
2902
|
+
throw new TopologyError(
|
|
2903
|
+
`Fraction value ${t} is out of range [0, 1]`,
|
|
2904
|
+
{
|
|
2905
|
+
regionIds: [this.region1Id, this.region2Id],
|
|
2906
|
+
suggestion: "Use values between 0 and 1 for fractions"
|
|
2907
|
+
}
|
|
2908
|
+
);
|
|
2909
|
+
}
|
|
2910
|
+
return computePortPositionOnBoundary(boundary, t);
|
|
2911
|
+
});
|
|
2912
|
+
} else {
|
|
2913
|
+
throw new TopologyError(`Unknown spread kind`, {
|
|
2914
|
+
regionIds: [this.region1Id, this.region2Id]
|
|
2915
|
+
});
|
|
2916
|
+
}
|
|
2917
|
+
const fullPrefix = this.getFullPrefix();
|
|
2918
|
+
for (let i = 0; i < positions.length; i++) {
|
|
2919
|
+
const portId = count === 1 ? fullPrefix : `${fullPrefix}:${i}`;
|
|
2920
|
+
this.addPortFn({
|
|
2921
|
+
id: portId,
|
|
2922
|
+
region1Id: this.region1Id,
|
|
2923
|
+
region2Id: this.region2Id,
|
|
2924
|
+
x: positions[i].x,
|
|
2925
|
+
y: positions[i].y
|
|
2926
|
+
});
|
|
2927
|
+
this.portIds.push(portId);
|
|
2928
|
+
}
|
|
2929
|
+
return this;
|
|
2930
|
+
}
|
|
2931
|
+
at(t) {
|
|
2932
|
+
if (t < 0 || t > 1) {
|
|
2933
|
+
throw new TopologyError(`Position t=${t} is out of range [0, 1]`, {
|
|
2934
|
+
regionIds: [this.region1Id, this.region2Id],
|
|
2935
|
+
suggestion: "Use a value between 0 and 1"
|
|
2936
|
+
});
|
|
2937
|
+
}
|
|
2938
|
+
const boundary = this.ensureBoundary();
|
|
2939
|
+
const position = computePortPositionOnBoundary(boundary, t);
|
|
2940
|
+
const fullPrefix = this.getFullPrefix();
|
|
2941
|
+
this.addPortFn({
|
|
2942
|
+
id: fullPrefix,
|
|
2943
|
+
region1Id: this.region1Id,
|
|
2944
|
+
region2Id: this.region2Id,
|
|
2945
|
+
x: position.x,
|
|
2946
|
+
y: position.y
|
|
2947
|
+
});
|
|
2948
|
+
this.portIds.push(fullPrefix);
|
|
2949
|
+
return this;
|
|
2950
|
+
}
|
|
2951
|
+
atXY(p) {
|
|
2952
|
+
const fullPrefix = this.getFullPrefix();
|
|
2953
|
+
this.addPortFn({
|
|
2954
|
+
id: fullPrefix,
|
|
2955
|
+
region1Id: this.region1Id,
|
|
2956
|
+
region2Id: this.region2Id,
|
|
2957
|
+
x: p.x,
|
|
2958
|
+
y: p.y
|
|
2959
|
+
});
|
|
2960
|
+
this.portIds.push(fullPrefix);
|
|
2961
|
+
return this;
|
|
2962
|
+
}
|
|
2963
|
+
getPortIds() {
|
|
2964
|
+
if (this.portIds.length === 0) {
|
|
2965
|
+
this.ports(1);
|
|
2966
|
+
}
|
|
2967
|
+
return [...this.portIds];
|
|
2968
|
+
}
|
|
2969
|
+
// Called by Topology.toJumperGraph() to ensure default ports are created
|
|
2970
|
+
finalizeIfNeeded() {
|
|
2971
|
+
if (this.portIds.length === 0) {
|
|
2972
|
+
this.ports(1);
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
// Check if ports have already been added
|
|
2976
|
+
hasPortsAdded() {
|
|
2977
|
+
return this.portIds.length > 0;
|
|
2978
|
+
}
|
|
2979
|
+
};
|
|
2980
|
+
|
|
2981
|
+
// lib/topology/PortBuilder.ts
|
|
2982
|
+
var PortBuilder = class {
|
|
2983
|
+
portId;
|
|
2984
|
+
region1Id = null;
|
|
2985
|
+
region2Id = null;
|
|
2986
|
+
position = null;
|
|
2987
|
+
tValue = null;
|
|
2988
|
+
resolveRegionFn;
|
|
2989
|
+
addPortFn;
|
|
2990
|
+
tolerance;
|
|
2991
|
+
constructor(id, resolveRegionFn, addPortFn, tolerance = 1e-3) {
|
|
2992
|
+
this.portId = id;
|
|
2993
|
+
this.resolveRegionFn = resolveRegionFn;
|
|
2994
|
+
this.addPortFn = addPortFn;
|
|
2995
|
+
this.tolerance = tolerance;
|
|
2996
|
+
}
|
|
2997
|
+
id(id) {
|
|
2998
|
+
this.portId = id;
|
|
2999
|
+
return this;
|
|
3000
|
+
}
|
|
3001
|
+
between(a, b) {
|
|
3002
|
+
this.region1Id = typeof a === "string" ? a : a.id;
|
|
3003
|
+
this.region2Id = typeof b === "string" ? b : b.id;
|
|
3004
|
+
return this;
|
|
3005
|
+
}
|
|
3006
|
+
onSharedEdge(t = 0.5) {
|
|
3007
|
+
if (t < 0 || t > 1) {
|
|
3008
|
+
throw new TopologyError(`Position t=${t} is out of range [0, 1]`, {
|
|
3009
|
+
suggestion: "Use a value between 0 and 1"
|
|
3010
|
+
});
|
|
3011
|
+
}
|
|
3012
|
+
this.tValue = t;
|
|
3013
|
+
this.position = null;
|
|
3014
|
+
return this;
|
|
3015
|
+
}
|
|
3016
|
+
at(p) {
|
|
3017
|
+
this.position = { x: p.x, y: p.y };
|
|
3018
|
+
this.tValue = null;
|
|
3019
|
+
return this;
|
|
3020
|
+
}
|
|
3021
|
+
done() {
|
|
3022
|
+
if (!this.region1Id || !this.region2Id) {
|
|
3023
|
+
throw new TopologyError(
|
|
3024
|
+
`Port "${this.portId}" must be between two regions`,
|
|
3025
|
+
{
|
|
3026
|
+
suggestion: "Use .between(regionA, regionB) before .done()"
|
|
3027
|
+
}
|
|
3028
|
+
);
|
|
3029
|
+
}
|
|
3030
|
+
const region1 = this.resolveRegionFn(this.region1Id);
|
|
3031
|
+
const region2 = this.resolveRegionFn(this.region2Id);
|
|
3032
|
+
let finalPosition;
|
|
3033
|
+
if (this.position) {
|
|
3034
|
+
finalPosition = this.position;
|
|
3035
|
+
const onBoundary1 = pointOnBoundary(
|
|
3036
|
+
this.position,
|
|
3037
|
+
region1.bounds,
|
|
3038
|
+
this.tolerance
|
|
3039
|
+
);
|
|
3040
|
+
const onBoundary2 = pointOnBoundary(
|
|
3041
|
+
this.position,
|
|
3042
|
+
region2.bounds,
|
|
3043
|
+
this.tolerance
|
|
3044
|
+
);
|
|
3045
|
+
if (!onBoundary1 && !onBoundary2) {
|
|
3046
|
+
}
|
|
3047
|
+
} else {
|
|
3048
|
+
const t = this.tValue ?? 0.5;
|
|
3049
|
+
try {
|
|
3050
|
+
const boundary = findSharedBoundary(
|
|
3051
|
+
region1.bounds,
|
|
3052
|
+
region2.bounds,
|
|
3053
|
+
this.region1Id,
|
|
3054
|
+
this.region2Id,
|
|
3055
|
+
this.tolerance
|
|
3056
|
+
);
|
|
3057
|
+
finalPosition = computePortPositionOnBoundary(boundary, t);
|
|
3058
|
+
} catch (e) {
|
|
3059
|
+
if (e instanceof TopologyError && this.position === null) {
|
|
3060
|
+
throw new TopologyError(
|
|
3061
|
+
`Port "${this.portId}": ${e.message}. No explicit position provided.`,
|
|
3062
|
+
{
|
|
3063
|
+
...e.details,
|
|
3064
|
+
suggestion: "Use .at({ x, y }) to specify position for non-adjacent regions"
|
|
3065
|
+
}
|
|
3066
|
+
);
|
|
3067
|
+
}
|
|
3068
|
+
throw e;
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
this.addPortFn({
|
|
3072
|
+
id: this.portId,
|
|
3073
|
+
region1Id: this.region1Id,
|
|
3074
|
+
region2Id: this.region2Id,
|
|
3075
|
+
x: finalPosition.x,
|
|
3076
|
+
y: finalPosition.y
|
|
3077
|
+
});
|
|
3078
|
+
}
|
|
3079
|
+
};
|
|
3080
|
+
|
|
3081
|
+
// lib/topology/Topology.ts
|
|
3082
|
+
var Topology = class {
|
|
3083
|
+
regions = /* @__PURE__ */ new Map();
|
|
3084
|
+
ports = [];
|
|
3085
|
+
portIds = /* @__PURE__ */ new Set();
|
|
3086
|
+
pendingConnections = [];
|
|
3087
|
+
scopePrefix = "";
|
|
3088
|
+
defaultTolerance = 1e-3;
|
|
3089
|
+
prefixId(id) {
|
|
3090
|
+
return this.scopePrefix ? `${this.scopePrefix}:${id}` : id;
|
|
3091
|
+
}
|
|
3092
|
+
generateRegionId() {
|
|
3093
|
+
let counter = 0;
|
|
3094
|
+
while (true) {
|
|
3095
|
+
const id = this.prefixId(`region_${counter}`);
|
|
3096
|
+
if (!this.regions.has(id)) {
|
|
3097
|
+
return id;
|
|
3098
|
+
}
|
|
3099
|
+
counter++;
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
generatePortId() {
|
|
3103
|
+
let counter = 0;
|
|
3104
|
+
while (true) {
|
|
3105
|
+
const id = this.prefixId(`port_${counter}`);
|
|
3106
|
+
if (!this.portIds.has(id)) {
|
|
3107
|
+
return id;
|
|
3108
|
+
}
|
|
3109
|
+
counter++;
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
region(id) {
|
|
3113
|
+
const fullId = id ? this.prefixId(id) : this.generateRegionId();
|
|
3114
|
+
if (this.regions.has(fullId)) {
|
|
3115
|
+
throw new TopologyError(`Region "${fullId}" already exists`, {
|
|
3116
|
+
regionIds: [fullId],
|
|
3117
|
+
suggestion: "Use a unique region ID or omit to auto-generate"
|
|
3118
|
+
});
|
|
3119
|
+
}
|
|
3120
|
+
const builder = new RegionBuilder(fullId);
|
|
3121
|
+
this.regions.set(fullId, builder);
|
|
3122
|
+
return builder;
|
|
3123
|
+
}
|
|
3124
|
+
getRegion(id) {
|
|
3125
|
+
const fullId = this.prefixId(id);
|
|
3126
|
+
const region = this.regions.get(fullId);
|
|
3127
|
+
if (!region) {
|
|
3128
|
+
throw new TopologyError(`Region "${fullId}" not found`, {
|
|
3129
|
+
regionIds: [fullId],
|
|
3130
|
+
suggestion: "Create the region first with .region(id)"
|
|
3131
|
+
});
|
|
3132
|
+
}
|
|
3133
|
+
return region.ref();
|
|
3134
|
+
}
|
|
3135
|
+
resolveRegion(ref) {
|
|
3136
|
+
const id = typeof ref === "string" ? this.prefixId(ref) : ref.id;
|
|
3137
|
+
const region = this.regions.get(id);
|
|
3138
|
+
if (!region) {
|
|
3139
|
+
throw new TopologyError(`Region "${id}" not found`, {
|
|
3140
|
+
regionIds: [id],
|
|
3141
|
+
suggestion: "Create the region first with .region(id)"
|
|
3142
|
+
});
|
|
3143
|
+
}
|
|
3144
|
+
const data = region.getData();
|
|
3145
|
+
const bounds = computeBoundsFromRegionData(data);
|
|
3146
|
+
return { id, bounds };
|
|
3147
|
+
}
|
|
3148
|
+
addPort(port) {
|
|
3149
|
+
if (this.portIds.has(port.id)) {
|
|
3150
|
+
throw new TopologyError(`Port "${port.id}" already exists`, {
|
|
3151
|
+
suggestion: "Use a unique port ID"
|
|
3152
|
+
});
|
|
3153
|
+
}
|
|
3154
|
+
this.portIds.add(port.id);
|
|
3155
|
+
this.ports.push(port);
|
|
3156
|
+
}
|
|
3157
|
+
connect(a, b, opts) {
|
|
3158
|
+
const region1 = this.resolveRegion(a);
|
|
3159
|
+
const region2 = this.resolveRegion(b);
|
|
3160
|
+
const currentScopePrefix = this.scopePrefix;
|
|
3161
|
+
const builder = new ConnectBuilder(
|
|
3162
|
+
region1.id,
|
|
3163
|
+
region2.id,
|
|
3164
|
+
region1.bounds,
|
|
3165
|
+
region2.bounds,
|
|
3166
|
+
(port) => this.addPort(port),
|
|
3167
|
+
(id) => currentScopePrefix ? `${currentScopePrefix}:${id}` : id,
|
|
3168
|
+
{
|
|
3169
|
+
idPrefix: opts?.idPrefix,
|
|
3170
|
+
tolerance: opts?.tolerance ?? this.defaultTolerance
|
|
3171
|
+
}
|
|
3172
|
+
);
|
|
3173
|
+
this.pendingConnections.push(builder);
|
|
3174
|
+
return builder;
|
|
3175
|
+
}
|
|
3176
|
+
port(id) {
|
|
3177
|
+
const fullId = id ? this.prefixId(id) : this.generatePortId();
|
|
3178
|
+
return new PortBuilder(
|
|
3179
|
+
fullId,
|
|
3180
|
+
(ref) => this.resolveRegion(ref),
|
|
3181
|
+
(port) => this.addPort(port),
|
|
3182
|
+
this.defaultTolerance
|
|
3183
|
+
);
|
|
3184
|
+
}
|
|
3185
|
+
scope(prefix, fn) {
|
|
3186
|
+
const previousPrefix = this.scopePrefix;
|
|
3187
|
+
this.scopePrefix = previousPrefix ? `${previousPrefix}:${prefix}` : prefix;
|
|
3188
|
+
try {
|
|
3189
|
+
fn(this);
|
|
3190
|
+
} finally {
|
|
3191
|
+
this.scopePrefix = previousPrefix;
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
validate(opts) {
|
|
3195
|
+
const tolerance = opts?.tolerance ?? this.defaultTolerance;
|
|
3196
|
+
const allowNonBoundaryPorts = opts?.allowNonBoundaryPorts ?? true;
|
|
3197
|
+
const errors = [];
|
|
3198
|
+
for (const [id, builder] of this.regions) {
|
|
3199
|
+
const data = builder.getData();
|
|
3200
|
+
try {
|
|
3201
|
+
const bounds = computeBoundsFromRegionData(data);
|
|
3202
|
+
validateBounds(bounds, id);
|
|
3203
|
+
} catch (e) {
|
|
3204
|
+
if (e instanceof TopologyError) {
|
|
3205
|
+
errors.push(e.message);
|
|
3206
|
+
} else {
|
|
3207
|
+
throw e;
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
for (const port of this.ports) {
|
|
3212
|
+
if (!this.regions.has(port.region1Id)) {
|
|
3213
|
+
errors.push(
|
|
3214
|
+
`Port "${port.id}" references non-existent region "${port.region1Id}"`
|
|
3215
|
+
);
|
|
3216
|
+
}
|
|
3217
|
+
if (!this.regions.has(port.region2Id)) {
|
|
3218
|
+
errors.push(
|
|
3219
|
+
`Port "${port.id}" references non-existent region "${port.region2Id}"`
|
|
3220
|
+
);
|
|
3221
|
+
}
|
|
3222
|
+
if (this.regions.has(port.region1Id) && this.regions.has(port.region2Id)) {
|
|
3223
|
+
const region1 = this.resolveRegion(port.region1Id);
|
|
3224
|
+
const region2 = this.resolveRegion(port.region2Id);
|
|
3225
|
+
const onBoundary1 = pointOnBoundary(
|
|
3226
|
+
{ x: port.x, y: port.y },
|
|
3227
|
+
region1.bounds,
|
|
3228
|
+
tolerance
|
|
3229
|
+
);
|
|
3230
|
+
const onBoundary2 = pointOnBoundary(
|
|
3231
|
+
{ x: port.x, y: port.y },
|
|
3232
|
+
region2.bounds,
|
|
3233
|
+
tolerance
|
|
3234
|
+
);
|
|
3235
|
+
if (!allowNonBoundaryPorts && !onBoundary1 && !onBoundary2) {
|
|
3236
|
+
errors.push(
|
|
3237
|
+
`Port "${port.id}" at (${port.x}, ${port.y}) is not on the boundary of either region "${port.region1Id}" or "${port.region2Id}"`
|
|
3238
|
+
);
|
|
3239
|
+
}
|
|
3240
|
+
try {
|
|
3241
|
+
findSharedBoundary(
|
|
3242
|
+
region1.bounds,
|
|
3243
|
+
region2.bounds,
|
|
3244
|
+
port.region1Id,
|
|
3245
|
+
port.region2Id,
|
|
3246
|
+
tolerance
|
|
3247
|
+
);
|
|
3248
|
+
} catch {
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
if (errors.length > 0) {
|
|
3253
|
+
throw new TopologyError(
|
|
3254
|
+
`Topology validation failed with ${errors.length} error(s):
|
|
3255
|
+
${errors.map((e) => ` - ${e}`).join("\n")}`
|
|
3256
|
+
);
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
toJumperGraph(opts) {
|
|
3260
|
+
for (const connection of this.pendingConnections) {
|
|
3261
|
+
connection.finalizeIfNeeded();
|
|
3262
|
+
}
|
|
3263
|
+
this.pendingConnections = [];
|
|
3264
|
+
if (opts?.validate !== false) {
|
|
3265
|
+
this.validate({ tolerance: opts?.tolerance });
|
|
3266
|
+
}
|
|
3267
|
+
const jregions = /* @__PURE__ */ new Map();
|
|
3268
|
+
const regionsList = [];
|
|
3269
|
+
for (const [id, builder] of this.regions) {
|
|
3270
|
+
const data = builder.getData();
|
|
3271
|
+
const bounds = computeBoundsFromRegionData(data);
|
|
3272
|
+
const center = computeCenterFromBounds(bounds);
|
|
3273
|
+
const jregion = {
|
|
3274
|
+
regionId: id,
|
|
3275
|
+
ports: [],
|
|
3276
|
+
d: {
|
|
3277
|
+
bounds,
|
|
3278
|
+
center,
|
|
3279
|
+
isPad: data.isPad,
|
|
3280
|
+
...data.isThroughJumper && { isThroughJumper: true },
|
|
3281
|
+
...data.isConnectionRegion && { isConnectionRegion: true },
|
|
3282
|
+
...data.meta
|
|
3283
|
+
}
|
|
3284
|
+
};
|
|
3285
|
+
jregions.set(id, jregion);
|
|
3286
|
+
regionsList.push(jregion);
|
|
3287
|
+
}
|
|
3288
|
+
const portsList = [];
|
|
3289
|
+
for (const portData of this.ports) {
|
|
3290
|
+
const region1 = jregions.get(portData.region1Id);
|
|
3291
|
+
const region2 = jregions.get(portData.region2Id);
|
|
3292
|
+
if (!region1 || !region2) {
|
|
3293
|
+
throw new TopologyError(
|
|
3294
|
+
`Port "${portData.id}" references non-existent region`,
|
|
3295
|
+
{
|
|
3296
|
+
regionIds: [portData.region1Id, portData.region2Id]
|
|
3297
|
+
}
|
|
3298
|
+
);
|
|
3299
|
+
}
|
|
3300
|
+
const jport = {
|
|
3301
|
+
portId: portData.id,
|
|
3302
|
+
region1,
|
|
3303
|
+
region2,
|
|
3304
|
+
d: {
|
|
3305
|
+
x: portData.x,
|
|
3306
|
+
y: portData.y
|
|
3307
|
+
}
|
|
3308
|
+
};
|
|
3309
|
+
region1.ports.push(jport);
|
|
3310
|
+
region2.ports.push(jport);
|
|
3311
|
+
portsList.push(jport);
|
|
3312
|
+
}
|
|
3313
|
+
return {
|
|
3314
|
+
regions: regionsList,
|
|
3315
|
+
ports: portsList
|
|
3316
|
+
};
|
|
3317
|
+
}
|
|
3318
|
+
// Utility method to merge an existing JumperGraph
|
|
3319
|
+
merge(graph, opts) {
|
|
3320
|
+
const prefix = opts?.prefix ?? "";
|
|
3321
|
+
const transform2 = opts?.transform ?? ((p) => p);
|
|
3322
|
+
const regionIdMap = /* @__PURE__ */ new Map();
|
|
3323
|
+
for (const region of graph.regions) {
|
|
3324
|
+
const newId = prefix ? `${prefix}:${region.regionId}` : region.regionId;
|
|
3325
|
+
if (this.regions.has(newId)) {
|
|
3326
|
+
throw new TopologyError(
|
|
3327
|
+
`Region "${newId}" already exists during merge`,
|
|
3328
|
+
{
|
|
3329
|
+
regionIds: [newId]
|
|
3330
|
+
}
|
|
3331
|
+
);
|
|
3332
|
+
}
|
|
3333
|
+
regionIdMap.set(region.regionId, newId);
|
|
3334
|
+
const builder = new RegionBuilder(newId);
|
|
3335
|
+
const transformedBounds = {
|
|
3336
|
+
minX: Math.min(
|
|
3337
|
+
transform2({ x: region.d.bounds.minX, y: region.d.bounds.minY }).x,
|
|
3338
|
+
transform2({ x: region.d.bounds.maxX, y: region.d.bounds.maxY }).x
|
|
3339
|
+
),
|
|
3340
|
+
maxX: Math.max(
|
|
3341
|
+
transform2({ x: region.d.bounds.minX, y: region.d.bounds.minY }).x,
|
|
3342
|
+
transform2({ x: region.d.bounds.maxX, y: region.d.bounds.maxY }).x
|
|
3343
|
+
),
|
|
3344
|
+
minY: Math.min(
|
|
3345
|
+
transform2({ x: region.d.bounds.minX, y: region.d.bounds.minY }).y,
|
|
3346
|
+
transform2({ x: region.d.bounds.maxX, y: region.d.bounds.maxY }).y
|
|
3347
|
+
),
|
|
3348
|
+
maxY: Math.max(
|
|
3349
|
+
transform2({ x: region.d.bounds.minX, y: region.d.bounds.minY }).y,
|
|
3350
|
+
transform2({ x: region.d.bounds.maxX, y: region.d.bounds.maxY }).y
|
|
3351
|
+
)
|
|
3352
|
+
};
|
|
3353
|
+
builder.rect(transformedBounds);
|
|
3354
|
+
if (region.d.isPad) builder.pad();
|
|
3355
|
+
if (region.d.isThroughJumper) builder.throughJumper();
|
|
3356
|
+
if (region.d.isConnectionRegion) builder.connectionRegion();
|
|
3357
|
+
const {
|
|
3358
|
+
bounds,
|
|
3359
|
+
center,
|
|
3360
|
+
isPad,
|
|
3361
|
+
isThroughJumper,
|
|
3362
|
+
isConnectionRegion,
|
|
3363
|
+
...rest
|
|
3364
|
+
} = region.d;
|
|
3365
|
+
if (Object.keys(rest).length > 0) {
|
|
3366
|
+
builder.meta(rest);
|
|
3367
|
+
}
|
|
3368
|
+
this.regions.set(newId, builder);
|
|
3369
|
+
}
|
|
3370
|
+
for (const port of graph.ports) {
|
|
3371
|
+
const newId = prefix ? `${prefix}:${port.portId}` : port.portId;
|
|
3372
|
+
if (this.portIds.has(newId)) {
|
|
3373
|
+
throw new TopologyError(`Port "${newId}" already exists during merge`, {
|
|
3374
|
+
suggestion: "Use a different prefix"
|
|
3375
|
+
});
|
|
3376
|
+
}
|
|
3377
|
+
const newRegion1Id = regionIdMap.get(port.region1.regionId);
|
|
3378
|
+
const newRegion2Id = regionIdMap.get(port.region2.regionId);
|
|
3379
|
+
if (!newRegion1Id || !newRegion2Id) {
|
|
3380
|
+
throw new TopologyError(
|
|
3381
|
+
`Port "${port.portId}" references region not in the merged graph`
|
|
3382
|
+
);
|
|
3383
|
+
}
|
|
3384
|
+
const transformedPos = transform2({ x: port.d.x, y: port.d.y });
|
|
3385
|
+
this.portIds.add(newId);
|
|
3386
|
+
this.ports.push({
|
|
3387
|
+
id: newId,
|
|
3388
|
+
region1Id: newRegion1Id,
|
|
3389
|
+
region2Id: newRegion2Id,
|
|
3390
|
+
x: transformedPos.x,
|
|
3391
|
+
y: transformedPos.y
|
|
3392
|
+
});
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
3395
|
+
// Get all region IDs (useful for debugging)
|
|
3396
|
+
getRegionIds() {
|
|
3397
|
+
return Array.from(this.regions.keys());
|
|
3398
|
+
}
|
|
3399
|
+
// Get all port IDs (useful for debugging)
|
|
3400
|
+
getPortIds() {
|
|
3401
|
+
return Array.from(this.portIds);
|
|
3402
|
+
}
|
|
3403
|
+
};
|
|
2570
3404
|
export {
|
|
3405
|
+
ConnectBuilder,
|
|
2571
3406
|
HyperGraphSolver,
|
|
2572
3407
|
JUMPER_GRAPH_SOLVER_DEFAULTS,
|
|
2573
3408
|
JumperGraphSolver,
|
|
3409
|
+
PortBuilder,
|
|
3410
|
+
RegionBuilder,
|
|
3411
|
+
Topology,
|
|
3412
|
+
TopologyError,
|
|
2574
3413
|
applyTransformToGraph,
|
|
2575
3414
|
calculateGraphBounds,
|
|
2576
3415
|
convertConnectionsToSerializedConnections,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tscircuit/hypergraph",
|
|
3
3
|
"main": "dist/index.js",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.19",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"start": "cosmos",
|
|
@@ -29,6 +29,6 @@
|
|
|
29
29
|
"typescript": "^5"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@tscircuit/solver-utils": "^0.0.
|
|
32
|
+
"@tscircuit/solver-utils": "^0.0.14"
|
|
33
33
|
}
|
|
34
34
|
}
|