@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 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
- export { type Candidate, 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, type PortId, type Region, type RegionId, type RegionPort, type RegionPortAssignment, type SerializedConnection, type SerializedGraphPort, type SerializedGraphRegion, type SerializedHyperGraph, type SerializedRegionPortAssignment, type SolvedRoute, type XYConnection, applyTransformToGraph, calculateGraphBounds, convertConnectionsToSerializedConnections, convertHyperGraphToSerializedHyperGraph, createGraphWithConnectionsFromBaseGraph, generateJumperGrid, generateJumperX4Grid, rotateGraph90Degrees };
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.17",
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.13"
32
+ "@tscircuit/solver-utils": "^0.0.14"
33
33
  }
34
34
  }