@tscircuit/hypergraph 0.0.17 → 0.0.18

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