diff --git a/lib/JumperGraphSolver/jumper-types.ts b/lib/JumperGraphSolver/jumper-types.ts index ed2376f..2ac66fe 100644 --- a/lib/JumperGraphSolver/jumper-types.ts +++ b/lib/JumperGraphSolver/jumper-types.ts @@ -5,6 +5,7 @@ export interface JRegion extends Region { d: { bounds: Bounds center: { x: number; y: number } + polygon?: { x: number; y: number }[] isPad: boolean isThroughJumper?: boolean isConnectionRegion?: boolean diff --git a/lib/JumperGraphSolver/visualizeJumperGraph.ts b/lib/JumperGraphSolver/visualizeJumperGraph.ts index eaaff10..70f9fa7 100644 --- a/lib/JumperGraphSolver/visualizeJumperGraph.ts +++ b/lib/JumperGraphSolver/visualizeJumperGraph.ts @@ -21,12 +21,14 @@ export const visualizeJumperGraph = ( points: [], rects: [], texts: [], + polygons: [], coordinateSystem: "cartesian", } as Required - // Draw regions as rectangles + // Draw regions as rectangles or polygons for (const region of graph.regions) { - const { bounds, isPad, isThroughJumper, isConnectionRegion } = region.d + const { bounds, isPad, isThroughJumper, isConnectionRegion, polygon } = + region.d const centerX = (bounds.minX + bounds.maxX) / 2 const centerY = (bounds.minY + bounds.maxY) / 2 const width = bounds.maxX - bounds.minX @@ -43,12 +45,22 @@ export const visualizeJumperGraph = ( fill = "rgba(200, 200, 255, 0.1)" // blue for other regions } - graphics.rects.push({ - center: { x: centerX, y: centerY }, - width: width - 0.1, - height: height - 0.1, - fill, - }) + if (polygon && polygon.length >= 3) { + const points = polygon + graphics.polygons.push({ + points, + fill, + stroke: "rgba(128, 128, 128, 0.5)", + strokeWidth: 0.03, + }) + } else { + graphics.rects.push({ + center: { x: centerX, y: centerY }, + width: width - 0.1, + height: height - 0.1, + fill, + }) + } } // Draw ports as small circles with labels diff --git a/lib/topology/RegionBuilder.ts b/lib/topology/RegionBuilder.ts index 0df5d8f..02e9ac6 100644 --- a/lib/topology/RegionBuilder.ts +++ b/lib/topology/RegionBuilder.ts @@ -8,6 +8,7 @@ export class RegionBuilder implements RegionRef { this.data = { id, bounds: null, + polygon: null, center: null, width: null, height: null, @@ -27,6 +28,7 @@ export class RegionBuilder implements RegionRef { rect(b: Bounds): this { this.data.bounds = { ...b } + this.data.polygon = null // Clear center/size if rect is used this.data.center = null this.data.width = null @@ -34,10 +36,42 @@ export class RegionBuilder implements RegionRef { return this } + polygon(points: { x: number; y: number }[]): this { + if (points.length < 3) { + throw new TopologyError( + `Region "${this.data.id}" has invalid polygon: at least 3 points required`, + { + regionIds: [this.data.id], + suggestion: "Provide at least three polygon vertices", + }, + ) + } + + for (const point of points) { + if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) { + throw new TopologyError( + `Region "${this.data.id}" has invalid polygon point`, + { + regionIds: [this.data.id], + suggestion: "Use finite numeric x/y values", + }, + ) + } + } + + this.data.polygon = points.map((p) => ({ x: p.x, y: p.y })) + this.data.bounds = null + this.data.center = null + this.data.width = null + this.data.height = null + return this + } + center(x: number, y: number): this { this.data.center = { x, y } // Clear bounds if center/size approach is used this.data.bounds = null + this.data.polygon = null return this } @@ -56,6 +90,7 @@ export class RegionBuilder implements RegionRef { this.data.anchor = anchor // Clear bounds if center/size approach is used this.data.bounds = null + this.data.polygon = null return this } diff --git a/lib/topology/Topology.ts b/lib/topology/Topology.ts index 9cbabd4..591e780 100644 --- a/lib/topology/Topology.ts +++ b/lib/topology/Topology.ts @@ -270,6 +270,7 @@ export class Topology { bounds, center, isPad: data.isPad, + ...(data.polygon && { polygon: data.polygon }), ...(data.isThroughJumper && { isThroughJumper: true }), ...(data.isConnectionRegion && { isConnectionRegion: true }), ...data.meta, diff --git a/lib/topology/types.ts b/lib/topology/types.ts index b94df2d..0be6ef4 100644 --- a/lib/topology/types.ts +++ b/lib/topology/types.ts @@ -1,9 +1,9 @@ +import type { Bounds } from "../JumperGraphSolver/Bounds" import type { - JRegion, JPort, + JRegion, JumperGraph, } from "../JumperGraphSolver/jumper-types" -import type { Bounds } from "../JumperGraphSolver/Bounds" export type { Bounds, JRegion, JPort, JumperGraph } @@ -39,6 +39,7 @@ export type SharedBoundary = { export type RegionData = { id: string bounds: Bounds | null + polygon: { x: number; y: number }[] | null center: { x: number; y: number } | null width: number | null height: number | null diff --git a/lib/topology/utils.ts b/lib/topology/utils.ts index 575e2da..73568f5 100644 --- a/lib/topology/utils.ts +++ b/lib/topology/utils.ts @@ -8,6 +8,23 @@ export function computeBoundsFromRegionData(data: RegionData): Bounds { return data.bounds } + if (data.polygon && data.polygon.length > 0) { + let minX = data.polygon[0].x + let maxX = data.polygon[0].x + let minY = data.polygon[0].y + let maxY = data.polygon[0].y + + for (let i = 1; i < data.polygon.length; i++) { + const point = data.polygon[i] + minX = Math.min(minX, point.x) + maxX = Math.max(maxX, point.x) + minY = Math.min(minY, point.y) + maxY = Math.max(maxY, point.y) + } + + return { minX, maxX, minY, maxY } + } + if (data.center && data.width !== null && data.height !== null) { const halfW = data.width / 2 const halfH = data.height / 2 diff --git a/package.json b/package.json index 88ec9ae..3c1c23a 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "@tscircuit/math-utils": "^0.0.29", "@types/bun": "latest", "bun-match-svg": "^0.0.15", - "graphics-debug": "^0.0.76", + "graphics-debug": "^0.0.83", "react-cosmos": "^7.1.0", "react-cosmos-plugin-vite": "^7.1.0", "transformation-matrix": "^3.1.0", diff --git a/tests/topology/__snapshots__/topology19.snap.svg b/tests/topology/__snapshots__/topology19.snap.svg new file mode 100644 index 0000000..d3a7443 --- /dev/null +++ b/tests/topology/__snapshots__/topology19.snap.svg @@ -0,0 +1,44 @@ + \ No newline at end of file diff --git a/tests/topology/topology19.test.ts b/tests/topology/topology19.test.ts new file mode 100644 index 0000000..701d721 --- /dev/null +++ b/tests/topology/topology19.test.ts @@ -0,0 +1,36 @@ +import { expect, test } from "bun:test" +import { getSvgFromGraphicsObject } from "graphics-debug" +import { visualizeJumperGraph } from "lib/JumperGraphSolver/visualizeJumperGraph" +import { Topology } from "lib/topology" + +test("topology19 - connect polygon regions", () => { + const topo = new Topology() + + const A = topo.region("A").polygon([ + { x: 1, y: 2 }, + { x: 1.5, y: 0.866 }, + { x: 2.5, y: 0.866 }, + { x: 3, y: 2 }, + { x: 2.5, y: 3.134 }, + { x: 1.5, y: 3.134 }, + ]) + const B = topo.region("B").polygon([ + { x: 3, y: 2 }, + { x: 3.5, y: 0.866 }, + { x: 4.5, y: 0.866 }, + { x: 5, y: 2 }, + { x: 4.5, y: 3.134 }, + { x: 3.5, y: 3.134 }, + ]) + + topo.connect(A, B) + + const graph = topo.toJumperGraph() + + expect(graph.regions.map((r) => r.regionId).sort()).toEqual(["A", "B"]) + expect(graph.ports[0].portId).toBe("A-B") + + expect( + getSvgFromGraphicsObject(visualizeJumperGraph(graph)), + ).toMatchSvgSnapshot(import.meta.path) +})