@mark1russell7/layout-contract 0.0.0-stage → 0.2.2
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/LICENSE +21 -0
- package/README.md +46 -2
- package/build/index.d.ts +442 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +613 -0
- package/build/index.js.map +1 -0
- package/package.json +57 -3
- package/src/geometry.ts +109 -0
- package/src/index.ts +9 -0
- package/src/input.ts +144 -0
- package/src/measures.ts +88 -0
- package/src/output.ts +58 -0
- package/src/record.ts +83 -0
- package/src/rules.ts +241 -0
- package/src/strategy.ts +20 -0
- package/src/tolerance.ts +25 -0
- package/src/turn.ts +67 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/geometry.ts","../src/tolerance.ts","../src/input.ts","../src/turn.ts","../src/output.ts","../src/record.ts","../src/rules.ts","../src/measures.ts"],"sourcesContent":["/** A side of a box. The y axis points down, so `top` has the smallest y. */\nexport type Side = \"top\" | \"right\" | \"bottom\" | \"left\";\n\n/** The sides in clockwise order, from the top. */\nexport const SIDES: readonly Side[] = [\"top\", \"right\", \"bottom\", \"left\"];\n\nexport interface Point {\n readonly x: number;\n readonly y: number;\n}\n\n/** A rectangle: its top-left corner and its size. */\nexport interface Rect {\n readonly x: number;\n readonly y: number;\n readonly w: number;\n readonly h: number;\n}\n\n/** A value for each side of a box. */\nexport interface Sides {\n readonly top: number;\n readonly right: number;\n readonly bottom: number;\n readonly left: number;\n}\n\n/**\n * A region. The forms and the rules are those of `docs/FORMAT.md` of `packing`.\n * A ring of a polygon closes without a copy of its first point.\n */\nexport type Region =\n | { readonly kind: \"rect\"; readonly x: number; readonly y: number; readonly w: number; readonly h: number }\n | {\n readonly kind: \"polygon\";\n readonly points: readonly (readonly [number, number])[];\n readonly holes?: readonly (readonly (readonly [number, number])[])[];\n }\n | { readonly kind: \"circle\"; readonly cx: number; readonly cy: number; readonly r: number }\n | { readonly kind: \"ellipse\"; readonly cx: number; readonly cy: number; readonly rx: number; readonly ry: number };\n\n/** A margin: one value for the four sides, or one value for each side. */\nexport type Margin = number | Sides;\n\n/** This function gives the value of each side of a margin. No margin gives 0 on each side. */\nexport function marginSides(margin: Margin | undefined): Sides {\n if (margin === undefined) return { top: 0, right: 0, bottom: 0, left: 0 };\n if (typeof margin === \"number\") return { top: margin, right: margin, bottom: margin, left: margin };\n return margin;\n}\n\n/** This function gives the rectangle made larger by the margin on each side. */\nexport function grow(rect: Rect, margin: Margin | undefined): Rect {\n const m = marginSides(margin);\n return { x: rect.x - m.left, y: rect.y - m.top, w: rect.w + m.left + m.right, h: rect.h + m.top + m.bottom };\n}\n\n/** This function gives the bounding box of a region. */\nexport function boundsOf(region: Region): Rect {\n switch (region.kind) {\n case \"rect\":\n return { x: region.x, y: region.y, w: region.w, h: region.h };\n case \"circle\":\n return { x: region.cx - region.r, y: region.cy - region.r, w: 2 * region.r, h: 2 * region.r };\n case \"ellipse\":\n return { x: region.cx - region.rx, y: region.cy - region.ry, w: 2 * region.rx, h: 2 * region.ry };\n case \"polygon\": {\n let x0 = Infinity;\n let y0 = Infinity;\n let x1 = -Infinity;\n let y1 = -Infinity;\n for (const [x, y] of region.points) {\n x0 = Math.min(x0, x);\n y0 = Math.min(y0, y);\n x1 = Math.max(x1, x);\n y1 = Math.max(y1, y);\n }\n return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 };\n }\n }\n}\n\n/** This function gives the region moved by `dx` and `dy`. */\nexport function translate(region: Region, dx: number, dy: number): Region {\n switch (region.kind) {\n case \"rect\":\n return { ...region, x: region.x + dx, y: region.y + dy };\n case \"circle\":\n case \"ellipse\":\n return { ...region, cx: region.cx + dx, cy: region.cy + dy };\n case \"polygon\": {\n const move = (ring: readonly (readonly [number, number])[]): [number, number][] => ring.map(([x, y]) => [x + dx, y + dy]);\n return region.holes === undefined\n ? { kind: \"polygon\", points: move(region.points) }\n : { kind: \"polygon\", points: move(region.points), holes: region.holes.map(move) };\n }\n }\n}\n\n/**\n * This function gives the overlap of the insides of two rectangles on each axis.\n * A value of 0 or less on an axis is no overlap. Two rectangles with a common edge have an overlap of 0.\n */\nexport function overlapOf(a: Rect, b: Rect): { readonly x: number; readonly y: number } {\n return {\n x: Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x),\n y: Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y),\n };\n}\n","/** The epsilon for the float compares in an engine. */\nexport const EPS = 1e-6;\n\n/**\n * The epsilon for the float compares on a written file. A file rounds each number to 3 decimals,\n * so a difference of two values can move by up to 0.001.\n */\nexport const RECORD_EPS = 1e-3;\n\n/**\n * The tolerance of each hard rule, in layout units. A record writes the tolerance that its run used,\n * in its field `tolerance`, next to `hard`.\n */\nexport interface Tolerance {\n /** Two boxes with their margins break `no-overlap` when their insides overlap by more than this on each axis. */\n readonly overlap: number;\n /** A route breaks `no-pass-through` when a part longer than this is inside a box made this much smaller. */\n readonly through: number;\n}\n\n/**\n * The default tolerance of each rule. The packer is exact, so `overlap` is one unit of the rounding.\n * Routes are sampled curves that follow the margin line, so `through` is larger.\n */\nexport const DEFAULT_TOLERANCE: Tolerance = Object.freeze({ overlap: 0.001, through: 0.5 });\n","import type { Margin, Region, Side } from \"./geometry.ts\";\n\n/**\n * A port of a box: a place where edges go into the box.\n * With `x` and `y`, the port is fixed on the border of the box, relative to its top-left corner.\n * Without them, the router places the port on its `side`. Without a side, the router picks the side.\n */\nexport interface Port {\n readonly id: string;\n readonly side?: Side;\n readonly x?: number;\n readonly y?: number;\n}\n\n/** A box of an input. In the packer, its name is item. */\nexport interface LayoutBox {\n readonly id: string;\n readonly w: number;\n readonly h: number;\n /** The packer writes the position. The router reads it. */\n readonly x?: number;\n readonly y?: number;\n /** A locked box keeps `x` and `y`. The packer reads it as an obstacle. */\n readonly locked?: boolean;\n /** The reading order. Without it, the order of the array is the rank. */\n readonly rank?: number;\n readonly parent?: string;\n readonly group?: string;\n /** A region relative to the origin of the box. Without it, the box is the rectangle `w` by `h`. */\n readonly shape?: Region;\n readonly ports?: readonly Port[];\n /** A ring around the box. No other box enters it. Only a route from a port of the box enters it, at a right angle. */\n readonly margin?: Margin;\n /** The packer can turn the box by 90 degrees. The output has the turned box and ports. */\n readonly turnable?: boolean;\n /** The free data of the node. The grouping rule `keys` of `highways` reads `data.group`. */\n readonly data?: Readonly<Record<string, unknown>>;\n}\n\n/** The packer name of a box. It is the same type. */\nexport type Item = LayoutBox;\n\n/** An edge between two boxes, with optional ports. */\nexport interface LayoutEdge {\n readonly id: string;\n readonly source: string;\n readonly target: string;\n readonly sourcePort?: string;\n readonly targetPort?: string;\n readonly data?: Readonly<Record<string, unknown>>;\n}\n\n/** The container. `fixed` tells which sides of the size stay. */\nexport interface LayoutContainer {\n readonly w?: number;\n readonly h?: number;\n readonly fixed: \"size\" | \"width\" | \"height\" | \"none\";\n /** The target aspect: the width divided by the height. The default is 1. */\n readonly aspect?: number;\n /** The weight of the aspect error in the objective. 0 gives the least area. The default is 1. */\n readonly aspectWeight?: number;\n /** The shape. Without it, the container is the rectangle from 0, 0 to `w`, `h`. */\n readonly shape?: Region;\n /** The space between the border and the boxes with their margins. */\n readonly inset?: number;\n}\n\n/** An area that boxes and routes avoid. Without `move`, it stays. With `move`, the packer can move it for `cost` times the distance. */\nexport interface Obstacle {\n readonly id: string;\n readonly region: Region;\n readonly move?: { readonly cost: number };\n}\n\n/** The generator of an input. The same name, version, parameters and seed give the same file, byte for byte. */\nexport interface LayoutGenerator {\n readonly name: string;\n readonly version: number;\n readonly params: Readonly<Record<string, unknown>>;\n readonly seed: number;\n}\n\n/** The cell of an input in the taxonomy: one value for each dimension. The dimensions are in `docs/FORMAT.md` of `packing`. */\nexport interface LayoutCell {\n readonly containerSize?: \"fixed\" | \"freeHeight\" | \"freeWidth\" | \"freeAspect\" | \"freeArea\";\n readonly containerShape?: \"rect\" | \"convex\" | \"polygon\" | \"circle\" | \"ellipse\" | \"holes\";\n readonly obstacles?: \"none\" | \"center\" | \"scattered\" | \"wall\" | \"corridors\" | \"touching\" | \"edge\";\n readonly sizes?: \"identical\" | \"uniform\" | \"heavyTail\" | \"bimodal\" | \"extremeAspect\" | \"nearIdentical\";\n readonly count?: \"1\" | \"10\" | \"100\" | \"1k\" | \"10k\";\n readonly mode?: \"offline\" | \"online\" | \"incremental\";\n readonly edgeDensity?: \"sparse\" | \"medium\" | \"dense\";\n readonly edgeShape?: \"random\" | \"clustered\" | \"hubChain\" | \"longRange\" | \"tree\" | \"bundled\";\n readonly groups?: \"none\" | \"flat\" | \"nested\";\n readonly ports?: \"free\" | \"sides\" | \"fixed\";\n}\n\n/** An input of the corpus: the format `layout-input/1`. */\nexport interface LayoutInput {\n readonly schema: \"layout-input/1\";\n readonly id: string;\n readonly set?: string;\n readonly family?: string;\n readonly seed?: number;\n /** The origin: `made-up`, `published:<set>` or `known-best:<construction>`. */\n readonly source?: string;\n /** The first 16 hex characters of the SHA-256 of the canonical file without this field. */\n readonly hash?: string;\n /** The generator of the input. `null` or no value tells that a person made the input. */\n readonly generator?: LayoutGenerator | null;\n readonly cell?: LayoutCell;\n /** The place of the input in a sequence of changes. */\n readonly step?: number;\n /** A router input can have no container. */\n readonly container?: LayoutContainer;\n /** The free space between two boxes with their margins. Routes use it. */\n readonly gap?: number;\n readonly boxes: readonly LayoutBox[];\n readonly edges?: readonly LayoutEdge[];\n readonly obstacles?: readonly Obstacle[];\n /**\n * A variant names its base input and its change.\n * With `packed`, the variant is the packed output of its base input: the router routes the positions of that pack.\n */\n readonly base?: string;\n readonly change?: \"add\" | \"remove\" | \"resize\" | \"reorder\" | \"packed\";\n}\n\n/** The input of a packer: a `layout-input/1` file without the edges. */\nexport interface PackInput {\n readonly container: LayoutContainer;\n readonly items: readonly Item[];\n readonly obstacles?: readonly Obstacle[];\n readonly gap?: number;\n}\n\n/** This function gives the packer input of a file. An input without a container gets `{ fixed: \"none\" }`, so the packer picks the size. */\nexport function toPackInput(input: LayoutInput): PackInput {\n return {\n container: input.container ?? { fixed: \"none\" },\n items: input.boxes,\n ...(input.obstacles === undefined ? {} : { obstacles: input.obstacles }),\n ...(input.gap === undefined ? {} : { gap: input.gap }),\n };\n}\n","import { SIDES, marginSides, type Margin, type Region, type Side } from \"./geometry.ts\";\nimport type { LayoutBox, Port } from \"./input.ts\";\n\n/*\n * The turn: 90 degrees clockwise, with the y axis down. The turned box has `w` and `h` swapped,\n * and its top-left corner stays at the position of the box. A point (px, py) of a box of height h\n * goes to (h - py, px). Thus the top-left corner goes to the top-right corner, and each side moves\n * one step clockwise: top to right, right to bottom, bottom to left, left to top.\n */\n\n/** This function gives the side after a turn of 90 degrees clockwise. */\nexport function turnSide(side: Side): Side {\n return SIDES[(SIDES.indexOf(side) + 1) % 4]!;\n}\n\n/** This function gives a point of a box of height `h` after the turn. */\nexport function turnPoint(x: number, y: number, h: number): [number, number] {\n return [h - y, x];\n}\n\n/** This function gives a port of a box of height `h` after the turn. Each value of the port turns, also a port with only `x` or only `y`. */\nexport function turnPort(port: Port, h: number): Port {\n const { side, x, y, ...rest } = port;\n return {\n ...rest,\n ...(side === undefined ? {} : { side: turnSide(side) }),\n ...(y === undefined ? {} : { x: h - y }),\n ...(x === undefined ? {} : { y: x }),\n };\n}\n\n/** This function gives a margin after the turn: the value of each side moves one side clockwise. */\nexport function turnMargin(margin: Margin): Margin {\n if (typeof margin === \"number\") return margin;\n const m = marginSides(margin);\n return { top: m.left, right: m.top, bottom: m.right, left: m.bottom };\n}\n\n/** This function gives a region relative to the origin of a box of height `h` after the turn. */\nexport function turnRegion(region: Region, h: number): Region {\n switch (region.kind) {\n case \"rect\":\n return { kind: \"rect\", x: h - region.y - region.h, y: region.x, w: region.h, h: region.w };\n case \"circle\":\n return { ...region, cx: h - region.cy, cy: region.cx };\n case \"ellipse\":\n return { kind: \"ellipse\", cx: h - region.cy, cy: region.cx, rx: region.ry, ry: region.rx };\n case \"polygon\": {\n const turn = (ring: readonly (readonly [number, number])[]): [number, number][] => ring.map(([x, y]) => turnPoint(x, y, h));\n return region.holes === undefined\n ? { kind: \"polygon\", points: turn(region.points) }\n : { kind: \"polygon\", points: turn(region.points), holes: region.holes.map(turn) };\n }\n }\n}\n\n/** This function gives a box after the turn: the size, the ports, the margin and the shape turn. The position stays. */\nexport function turnBox(box: LayoutBox): LayoutBox {\n return {\n ...box,\n w: box.h,\n h: box.w,\n ...(box.ports === undefined ? {} : { ports: box.ports.map((p) => turnPort(p, box.h)) }),\n ...(box.margin === undefined ? {} : { margin: turnMargin(box.margin) }),\n ...(box.shape === undefined ? {} : { shape: turnRegion(box.shape, box.h) }),\n };\n}\n","import { boundsOf, translate } from \"./geometry.ts\";\nimport type { LayoutInput } from \"./input.ts\";\nimport { turnBox } from \"./turn.ts\";\n\n/** The position of one item. `turn` is 90 when the packer turned a turnable item. */\nexport interface Placement {\n readonly id: string;\n readonly x: number;\n readonly y: number;\n readonly turn?: 0 | 90;\n}\n\n/** The facts of one stage of a strategy, for the records and the site. */\nexport interface StageTrace {\n readonly id: string;\n readonly ms?: number;\n readonly measures?: Readonly<Record<string, number>>;\n}\n\n/** The output of a packer. It does not change the input. */\nexport interface PackOutput {\n readonly placements: readonly Placement[];\n /** The ids of the items that the packer did not place. */\n readonly overflow: readonly string[];\n /** The size of the container after the pack. For a free size, the size that the search found. */\n readonly size: { readonly w: number; readonly h: number };\n /** The new top-left corner of the bounding box of each obstacle that moved. */\n readonly moved: readonly { readonly id: string; readonly x: number; readonly y: number }[];\n readonly trace?: readonly StageTrace[];\n}\n\n/**\n * This function gives the input with the output of a packer applied. Each placed box gets its position.\n * A turned box gets its turned size, ports, margin and shape. Each moved obstacle moves.\n * The container gets the size of the output. The router reads this result. It does not read `turn`.\n */\nexport function applyPack(input: LayoutInput, output: PackOutput): LayoutInput {\n const at = new Map(output.placements.map((p) => [p.id, p]));\n const moved = new Map(output.moved.map((m) => [m.id, m]));\n const boxes = input.boxes.map((box) => {\n const p = at.get(box.id);\n if (p === undefined) return box;\n const turned = p.turn === 90 ? turnBox(box) : box;\n return { ...turned, x: p.x, y: p.y };\n });\n const obstacles = input.obstacles?.map((o) => {\n const m = moved.get(o.id);\n if (m === undefined) return o;\n const b = boundsOf(o.region);\n return { ...o, region: translate(o.region, m.x - b.x, m.y - b.y) };\n });\n return {\n ...input,\n container: { ...(input.container ?? { fixed: \"none\" }), w: output.size.w, h: output.size.h },\n boxes,\n ...(obstacles === undefined ? {} : { obstacles }),\n };\n}\n","import type { Tolerance } from \"./tolerance.ts\";\nimport type { LayoutInput } from \"./input.ts\";\n\n/** The stage of a record: the packer, the router, or both together. */\nexport type Stage = \"pack\" | \"route\" | \"joint\";\n\n/** One run of one strategy on one input and seed: the format `layout-record/1`. A run writes one line of JSON Lines. */\nexport interface LayoutRecord {\n readonly schema: \"layout-record/1\";\n readonly stage: Stage;\n readonly step: string;\n readonly strategy: string;\n /** The options in full, with the defaults. */\n readonly options: Readonly<Record<string, unknown>>;\n readonly corpus: string;\n /** The id of the input. */\n readonly input: string;\n /** The `hash` of the input file. */\n readonly inputHash: string;\n readonly set: string;\n readonly seed: number;\n readonly nodes: number;\n readonly edges: number;\n /** The value of each measure, by its name: `pack.*`, `route.*` or `joint.*`. */\n readonly measures: Readonly<Record<string, number>>;\n /** The count of the breaks of each hard rule. A good run has 0 for each rule. */\n readonly hard: Readonly<Record<string, number>>;\n /** The tolerance of each hard rule of the run. */\n readonly tolerance?: Readonly<Partial<Tolerance>>;\n /** `true` when two runs with the same seed give the same positions. */\n readonly deterministic: boolean;\n /** The id of the prediction that was written before the run. */\n readonly prediction?: string;\n /** The median of `msRuns`, or the time of the one run. */\n readonly ms: number;\n /** The time of each timed repeat, in milliseconds. */\n readonly msRuns?: readonly number[];\n /**\n * The first 16 hex characters of the SHA-256 of the canonical output of the stage.\n * The packer hashes its placements by the order of the ids. The router hashes its routes by the order of the edge ids.\n */\n readonly outputHash?: string;\n /** The facts of the computer of the run. */\n readonly env?: RunEnv;\n /** The reason when the strategy did not run, for example an input over its `maxNodes`. Then the record has no measures. */\n readonly skipped?: string;\n readonly commit?: string;\n}\n\n/**\n * The values that a strategy supports, for each dimension of a taxonomy.\n * The dimension names are in `docs/FORMAT.md` of `packing` and `docs/LAYOUT-RECORDS.md` of `highways`.\n */\nexport type Supports = Readonly<Record<string, readonly string[]>>;\n\n/** The facts of the computer of a run. */\nexport interface RunEnv {\n readonly node: string;\n readonly os: string;\n readonly arch: string;\n /** The name of the runner, for example `ubuntu-latest` or `local`. */\n readonly runner: string;\n /** The model of the processor. */\n readonly cpu: string;\n /** The count of the logical processors. */\n readonly cpus: number;\n}\n\n/** The facts of a run that the input does not give. */\nexport type RunFacts = Omit<LayoutRecord, \"schema\" | \"input\" | \"inputHash\" | \"set\" | \"nodes\" | \"edges\">;\n\n/** This function gives the record of a run. The input gives its id, its hash, its set and the counts of the nodes and the edges. */\nexport function toLayoutRecord(input: LayoutInput, run: RunFacts): LayoutRecord {\n return {\n schema: \"layout-record/1\",\n ...run,\n input: input.id,\n inputHash: input.hash ?? \"\",\n set: input.set ?? \"\",\n nodes: input.boxes.length,\n edges: input.edges?.length ?? 0,\n };\n}\n","import { boundsOf, grow, overlapOf, type Rect, type Region } from \"./geometry.ts\";\nimport type { LayoutBox, LayoutInput, Obstacle } from \"./input.ts\";\nimport { applyPack, type PackOutput } from \"./output.ts\";\nimport { DEFAULT_TOLERANCE, RECORD_EPS } from \"./tolerance.ts\";\n\n/** This function gives the area of a placed box: its shape or its rectangle, at its position. A box without a position gives `undefined`. */\nexport function placedRect(box: LayoutBox): Rect | undefined {\n if (box.x === undefined || box.y === undefined) return undefined;\n if (box.shape === undefined) return { x: box.x, y: box.y, w: box.w, h: box.h };\n const b = boundsOf(box.shape);\n return { x: box.x + b.x, y: box.y + b.y, w: b.w, h: b.h };\n}\n\n/**\n * This function counts the pairs of placed boxes whose insides, with their margins, overlap by more than `tolerance` on each axis.\n * A box with a shape counts as its bounding box. That area is larger, thus the count is safe.\n */\nexport function countOverlaps(boxes: readonly LayoutBox[], tolerance: number = DEFAULT_TOLERANCE.overlap): number {\n const rects: Rect[] = [];\n for (const box of boxes) {\n const r = placedRect(box);\n if (r !== undefined) rects.push(grow(r, box.margin));\n }\n rects.sort((a, b) => a.x - b.x);\n let count = 0;\n for (let i = 0; i < rects.length; i++) {\n const a = rects[i]!;\n for (let j = i + 1; j < rects.length; j++) {\n const b = rects[j]!;\n if (b.x >= a.x + a.w - tolerance) break;\n const o = overlapOf(a, b);\n if (o.x > tolerance && o.y > tolerance) count++;\n }\n }\n return count;\n}\n\n/**\n * This function counts the placed boxes that go out of the container.\n * Each box with its margin must stay inside the container made smaller by `inset`. Only the fixed sides of the size count. A shaped container counts as its bounding box.\n * An input without a container gives 0.\n */\nexport function countOutside(input: LayoutInput, tolerance: number = DEFAULT_TOLERANCE.overlap): number {\n const c = input.container;\n if (c === undefined) return 0;\n const area = c.shape === undefined ? { x: 0, y: 0, w: c.w ?? Infinity, h: c.h ?? Infinity } : boundsOf(c.shape);\n const inset = c.inset ?? 0;\n const fixW = c.fixed === \"size\" || c.fixed === \"width\";\n const fixH = c.fixed === \"size\" || c.fixed === \"height\";\n let count = 0;\n for (const box of input.boxes) {\n const r0 = placedRect(box);\n if (r0 === undefined) continue;\n const r = grow(r0, box.margin);\n const outX = r.x < area.x + inset - tolerance || r.x + r.w > area.x + area.w - inset + tolerance;\n const outY = r.y < area.y + inset - tolerance || r.y + r.h > area.y + area.h - inset + tolerance;\n if ((fixW && outX) || (fixH && outY)) count++;\n }\n return count;\n}\n\n/** This function counts the fixed ports that are not on the border of their box, with the epsilon of a written file. A port without a side does not count. */\nexport function countPortsOffBorder(boxes: readonly LayoutBox[], eps: number = RECORD_EPS): number {\n let count = 0;\n for (const box of boxes) {\n for (const p of box.ports ?? []) {\n if (p.x === undefined || p.y === undefined || p.side === undefined) continue;\n const along = p.side === \"top\" || p.side === \"bottom\" ? p.x : p.y;\n const length = p.side === \"top\" || p.side === \"bottom\" ? box.w : box.h;\n const across = { top: p.y, bottom: p.y - box.h, left: p.x, right: p.x - box.w }[p.side];\n if (Math.abs(across) > eps || along < -eps || along > length + eps) count++;\n }\n }\n return count;\n}\n\n/** The count of the breaks of each hard rule of a pack. */\nexport interface PackHard {\n readonly \"no-overlap\": number;\n readonly \"in-container\": number;\n readonly \"locked-kept\": number;\n readonly complete: number;\n}\n\n/** This function tells if the inside of `region` and the inside of `rect` have a common point. */\nexport function regionHitsRect(region: Region, rect: Rect): boolean {\n if (rect.w <= 0 || rect.h <= 0) return false;\n switch (region.kind) {\n case \"rect\": {\n const o = overlapOf(region, rect);\n return o.x > 0 && o.y > 0;\n }\n case \"circle\":\n return circleHitsRect(region.cx, region.cy, region.r, region.r, rect);\n case \"ellipse\":\n return circleHitsRect(region.cx, region.cy, region.rx, region.ry, rect);\n case \"polygon\": {\n const rings = [region.points, ...(region.holes ?? [])];\n const inside = (x: number, y: number): boolean => x > rect.x && x < rect.x + rect.w && y > rect.y && y < rect.y + rect.h;\n if (region.points.some(([x, y]) => inside(x, y))) return true;\n for (const ring of rings) {\n for (let i = 0; i < ring.length; i++) {\n const a = ring[i]!;\n const b = ring[(i + 1) % ring.length]!;\n const m = clipMid(a[0], a[1], b[0], b[1], rect);\n if (m !== undefined && inside(m[0], m[1])) return true;\n }\n }\n return pointInRings(rect.x + rect.w / 2, rect.y + rect.h / 2, rings);\n }\n }\n}\n\n/** An ellipse with the radii `rx` and `ry` scales to a unit circle. The rectangle scales with it. */\nfunction circleHitsRect(cx: number, cy: number, rx: number, ry: number, rect: Rect): boolean {\n if (rx <= 0 || ry <= 0) return false;\n const x0 = (rect.x - cx) / rx;\n const x1 = (rect.x + rect.w - cx) / rx;\n const y0 = (rect.y - cy) / ry;\n const y1 = (rect.y + rect.h - cy) / ry;\n const dx = Math.max(x0, 0, -x1);\n const dy = Math.max(y0, 0, -y1);\n return dx * dx + dy * dy < 1;\n}\n\n/** This function gives the middle of the part of a segment inside a rectangle, or `undefined` when that part is a point or empty. */\nfunction clipMid(ax: number, ay: number, bx: number, by: number, r: Rect): readonly [number, number] | undefined {\n let t0 = 0;\n let t1 = 1;\n const dx = bx - ax;\n const dy = by - ay;\n const edges: readonly (readonly [number, number])[] = [\n [-dx, ax - r.x],\n [dx, r.x + r.w - ax],\n [-dy, ay - r.y],\n [dy, r.y + r.h - ay],\n ];\n for (const [p, q] of edges) {\n if (p === 0) {\n if (q < 0) return undefined;\n } else if (p < 0) {\n t0 = Math.max(t0, q / p);\n } else {\n t1 = Math.min(t1, q / p);\n }\n }\n if (t1 <= t0) return undefined;\n const t = (t0 + t1) / 2;\n return [ax + t * dx, ay + t * dy];\n}\n\n/** The even-odd rule over the outer ring and the holes. */\nfunction pointInRings(x: number, y: number, rings: readonly (readonly (readonly [number, number])[])[]): boolean {\n let inside = false;\n for (const ring of rings) {\n for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {\n const [xi, yi] = ring[i]!;\n const [xj, yj] = ring[j]!;\n if (yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) inside = !inside;\n }\n }\n return inside;\n}\n\n/**\n * This function counts the pairs of a placed box and an obstacle that overlap by more than `tolerance`.\n * The box counts with its margin. A box with a shape counts as its bounding box.\n */\nexport function countObstacleHits(\n boxes: readonly LayoutBox[],\n obstacles: readonly Obstacle[] | undefined,\n tolerance: number = DEFAULT_TOLERANCE.overlap,\n): number {\n if (obstacles === undefined || obstacles.length === 0) return 0;\n let count = 0;\n for (const box of boxes) {\n const r0 = placedRect(box);\n if (r0 === undefined) continue;\n const r = grow(r0, box.margin);\n const small = { x: r.x + tolerance, y: r.y + tolerance, w: r.w - 2 * tolerance, h: r.h - 2 * tolerance };\n for (const o of obstacles) if (regionHitsRect(o.region, small)) count++;\n }\n return count;\n}\n\n/**\n * This function gives the count of each hard rule of the output of a packer.\n * It applies the output with `applyPack`. Only the placed boxes and the locked boxes count for the overlaps and the container.\n * `no-overlap` counts the pairs of boxes and the pairs of a box and an obstacle.\n * A locked box without a placement, or with a move of more than `RECORD_EPS`, breaks `locked-kept`.\n * A locked box without a position in the input is a bad input, not a break. `checkInput` finds it.\n * Another box without a placement that is not in `overflow` breaks `complete`.\n */\nexport function packHard(input: LayoutInput, output: PackOutput, tolerance: number = DEFAULT_TOLERANCE.overlap): PackHard {\n const after = applyPack(input, output);\n const placed = new Set(output.placements.map((p) => p.id));\n const out = new Set(output.overflow);\n const was = new Map(input.boxes.map((b) => [b.id, b]));\n let lockedMoved = 0;\n let incomplete = 0;\n const kept: LayoutBox[] = [];\n for (const box of after.boxes) {\n const old = was.get(box.id);\n if (old?.locked === true) {\n if (old.x === undefined || old.y === undefined) continue;\n const moved = Math.abs(old.x - (box.x ?? old.x)) > RECORD_EPS || Math.abs(old.y - (box.y ?? old.y)) > RECORD_EPS;\n if (!placed.has(box.id) || moved) lockedMoved++;\n kept.push(box);\n } else if (placed.has(box.id)) {\n kept.push(box);\n } else if (!out.has(box.id)) {\n incomplete++;\n }\n }\n return {\n \"no-overlap\": countOverlaps(kept, tolerance) + countObstacleHits(kept, after.obstacles, tolerance),\n \"in-container\": countOutside({ ...after, boxes: kept }, tolerance),\n \"locked-kept\": lockedMoved,\n complete: incomplete,\n };\n}\n\n/**\n * This function gives the problems of an input file. An empty list tells that the file is good.\n * A problem is a bad input, not a break of a hard rule. Thus an engine refuses the input.\n */\nexport function checkInput(input: LayoutInput): readonly string[] {\n const problems: string[] = [];\n const ids = new Set<string>();\n for (const box of input.boxes) {\n if (ids.has(box.id)) problems.push(`box ${box.id}: the id is not unique`);\n ids.add(box.id);\n if (!(box.w > 0 && box.h > 0 && Number.isFinite(box.w) && Number.isFinite(box.h))) problems.push(`box ${box.id}: the size is not positive and finite`);\n if (box.locked === true && (box.x === undefined || box.y === undefined)) problems.push(`box ${box.id}: a locked box has no position`);\n }\n if (countPortsOffBorder(input.boxes) > 0) problems.push(\"a fixed port is not on the border of its box\");\n for (const edge of input.edges ?? []) {\n if (!ids.has(edge.source) || !ids.has(edge.target)) problems.push(`edge ${edge.id}: a box of the edge is not in the input`);\n }\n return problems;\n}\n","import { RECORD_EPS } from \"./tolerance.ts\";\n\n/** The direction of a measure: the side of the better values. */\nexport type Direction = \"lower\" | \"higher\";\n\n/**\n * The facts of one measure: its unit, its direction and its range.\n * `floor` and `tol` are the parameters of the floor rule of the promotion rule (`docs/PROMOTION.md` of `highways`).\n * Each engine exports the specs of its own measures. This package exports the specs of the `joint.*` measures.\n */\nexport interface MeasureSpec<Id extends string = string> {\n readonly id: Id;\n readonly unit: string;\n readonly direction: Direction;\n /** The lowest value. */\n readonly min: number;\n /** The highest value. The value `Infinity` tells that the measure has no upper limit. */\n readonly max: number;\n /** The family of the measure in the measure note of its engine. */\n readonly family?: string;\n /** The best value. Without it, the measure has no floor. */\n readonly floor?: number;\n /** The absolute tolerance at the floor. */\n readonly tol?: number;\n /**\n * The smallest change that counts as better or worse. A number is an absolute change. `{ rel }` is a share of the larger of the two values.\n * `docs/JOINT-MEASURES.md` gives the reason of each joint value.\n */\n readonly minEffect?: number | { readonly rel: number };\n /** The role of the measure in the joint selector. */\n readonly role?: \"guard\" | \"change-guard\" | \"diagnostic\" | \"time\";\n}\n\n/** The id of a joint measure. `docs/JOINT-MEASURES.md` gives the definitions. */\nexport type JointMeasureId =\n | \"joint.fill\"\n | \"joint.aspectError\"\n | \"joint.spill\"\n | \"joint.straight.crossings\"\n | \"joint.straight.length\"\n | \"joint.stability.mean\"\n | \"joint.stability.moved\"\n | \"joint.proxy.rank\"\n | \"joint.ms\";\n\n/** The specs of the joint measures, in the order of `docs/JOINT-MEASURES.md`. */\nexport const JOINT_MEASURES: readonly MeasureSpec<JointMeasureId>[] = Object.freeze([\n { id: \"joint.fill\", minEffect: 0.001, unit: \"0 to 1\", direction: \"higher\", min: 0, max: 1, floor: 1, role: \"guard\" },\n { id: \"joint.aspectError\", minEffect: 0.01, unit: \"natural log\", direction: \"lower\", min: 0, max: Infinity, floor: 0, tol: 0.01, role: \"guard\" },\n { id: \"joint.spill\", minEffect: 0.001, unit: \"0 to 1\", direction: \"lower\", min: 0, max: 1, floor: 0, tol: 0.001, role: \"guard\" },\n { id: \"joint.straight.crossings\", minEffect: 0, unit: \"count\", direction: \"lower\", min: 0, max: Infinity, floor: 0, tol: 0, role: \"guard\" },\n { id: \"joint.straight.length\", minEffect: 0.01, unit: \"S for each edge\", direction: \"lower\", min: 0, max: Infinity, floor: 0, tol: 0.01, role: \"guard\" },\n { id: \"joint.stability.mean\", minEffect: 0.005, unit: \"W\", direction: \"lower\", min: 0, max: Infinity, floor: 0, tol: 0.005, role: \"change-guard\" },\n { id: \"joint.stability.moved\", minEffect: 0.02, unit: \"0 to 1\", direction: \"lower\", min: 0, max: 1, floor: 0, tol: 0.02, role: \"change-guard\" },\n { id: \"joint.proxy.rank\", unit: \"-1 to 1\", direction: \"higher\", min: -1, max: 1, role: \"diagnostic\" },\n { id: \"joint.ms\", unit: \"milliseconds\", direction: \"lower\", min: 0, max: Infinity, role: \"time\" },\n] as const);\n\n/** The threshold of `joint.stability.moved`, in widths `W` of the base. */\nexport const JOINT_MOVED_MIN = 0.01;\n\n/** The result of a compare of a candidate with a baseline on one measure. A tie is not better. */\nexport type Verdict = \"better\" | \"worse\" | \"tie\";\n\n/**\n * This function gives the smallest change of a measure that counts between the values `a` and `b`.\n * The value is `max(RECORD_EPS, minEffect)`, or `max(RECORD_EPS, rel * max(|a|, |b|))` for a relative `minEffect`.\n * The larger value makes the threshold the same after a swap of `a` and `b`.\n */\nexport function minEffectOf(spec: Pick<MeasureSpec, \"minEffect\">, a = 0, b = 0): number {\n const m = spec.minEffect ?? 0;\n const t = typeof m === \"number\" ? m : m.rel * Math.max(Math.abs(a), Math.abs(b));\n return Math.max(RECORD_EPS, Number.isFinite(t) ? t : 0);\n}\n\n/**\n * This function compares the value `candidate` with the value `baseline` of one measure on one input.\n * A change of at most `minEffectOf(spec, candidate, baseline)` is a tie. The test is symmetric: a swap of the two values swaps better and worse.\n * Two equal values are a tie, also two infinite values. A value `NaN` is an error.\n */\nexport function compareMeasure(candidate: number, baseline: number, spec: Pick<MeasureSpec, \"direction\" | \"minEffect\">): Verdict {\n if (Number.isNaN(candidate) || Number.isNaN(baseline)) throw new RangeError(\"compareMeasure: a value is NaN\");\n if (candidate === baseline) return \"tie\";\n const delta = candidate - baseline;\n if (!(Math.abs(delta) > minEffectOf(spec, candidate, baseline))) return \"tie\";\n const better = spec.direction === \"lower\" ? delta < 0 : delta > 0;\n return better ? \"better\" : \"worse\";\n}\n"],"mappings":";;AAIA,MAAa,QAAyB;CAAC;CAAO;CAAS;CAAU;AAAM;;AAyCvE,SAAgB,YAAY,QAAmC;CAC7D,IAAI,WAAW,KAAA,GAAW,OAAO;EAAE,KAAK;EAAG,OAAO;EAAG,QAAQ;EAAG,MAAM;CAAE;CACxE,IAAI,OAAO,WAAW,UAAU,OAAO;EAAE,KAAK;EAAQ,OAAO;EAAQ,QAAQ;EAAQ,MAAM;CAAO;CAClG,OAAO;AACT;;AAGA,SAAgB,KAAK,MAAY,QAAkC;CACjE,MAAM,IAAI,YAAY,MAAM;CAC5B,OAAO;EAAE,GAAG,KAAK,IAAI,EAAE;EAAM,GAAG,KAAK,IAAI,EAAE;EAAK,GAAG,KAAK,IAAI,EAAE,OAAO,EAAE;EAAO,GAAG,KAAK,IAAI,EAAE,MAAM,EAAE;CAAO;AAC7G;;AAGA,SAAgB,SAAS,QAAsB;CAC7C,QAAQ,OAAO,MAAf;EACE,KAAK,QACH,OAAO;GAAE,GAAG,OAAO;GAAG,GAAG,OAAO;GAAG,GAAG,OAAO;GAAG,GAAG,OAAO;EAAE;EAC9D,KAAK,UACH,OAAO;GAAE,GAAG,OAAO,KAAK,OAAO;GAAG,GAAG,OAAO,KAAK,OAAO;GAAG,GAAG,IAAI,OAAO;GAAG,GAAG,IAAI,OAAO;EAAE;EAC9F,KAAK,WACH,OAAO;GAAE,GAAG,OAAO,KAAK,OAAO;GAAI,GAAG,OAAO,KAAK,OAAO;GAAI,GAAG,IAAI,OAAO;GAAI,GAAG,IAAI,OAAO;EAAG;EAClG,KAAK,WAAW;GACd,IAAI,KAAK;GACT,IAAI,KAAK;GACT,IAAI,KAAK;GACT,IAAI,KAAK;GACT,KAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ;IAClC,KAAK,KAAK,IAAI,IAAI,CAAC;IACnB,KAAK,KAAK,IAAI,IAAI,CAAC;IACnB,KAAK,KAAK,IAAI,IAAI,CAAC;IACnB,KAAK,KAAK,IAAI,IAAI,CAAC;GACrB;GACA,OAAO;IAAE,GAAG;IAAI,GAAG;IAAI,GAAG,KAAK;IAAI,GAAG,KAAK;GAAG;EAChD;CACF;AACF;;AAGA,SAAgB,UAAU,QAAgB,IAAY,IAAoB;CACxE,QAAQ,OAAO,MAAf;EACE,KAAK,QACH,OAAO;GAAE,GAAG;GAAQ,GAAG,OAAO,IAAI;GAAI,GAAG,OAAO,IAAI;EAAG;EACzD,KAAK;EACL,KAAK,WACH,OAAO;GAAE,GAAG;GAAQ,IAAI,OAAO,KAAK;GAAI,IAAI,OAAO,KAAK;EAAG;EAC7D,KAAK,WAAW;GACd,MAAM,QAAQ,SAAqE,KAAK,KAAK,CAAC,GAAG,OAAO,CAAC,IAAI,IAAI,IAAI,EAAE,CAAC;GACxH,OAAO,OAAO,UAAU,KAAA,IACpB;IAAE,MAAM;IAAW,QAAQ,KAAK,OAAO,MAAM;GAAE,IAC/C;IAAE,MAAM;IAAW,QAAQ,KAAK,OAAO,MAAM;IAAG,OAAO,OAAO,MAAM,IAAI,IAAI;GAAE;EACpF;CACF;AACF;;;;;AAMA,SAAgB,UAAU,GAAS,GAAqD;CACtF,OAAO;EACL,GAAG,KAAK,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,IAAI,KAAK,IAAI,EAAE,GAAG,EAAE,CAAC;EACrD,GAAG,KAAK,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,IAAI,KAAK,IAAI,EAAE,GAAG,EAAE,CAAC;CACvD;AACF;;;AC3GA,MAAa,MAAM;;;;;AAMnB,MAAa,aAAa;;;;;AAiB1B,MAAa,oBAA+B,OAAO,OAAO;CAAE,SAAS;CAAO,SAAS;AAAI,CAAC;;;ACgH1F,SAAgB,YAAY,OAA+B;CACzD,OAAO;EACL,WAAW,MAAM,aAAa,EAAE,OAAO,OAAO;EAC9C,OAAO,MAAM;EACb,GAAI,MAAM,cAAc,KAAA,IAAY,CAAC,IAAI,EAAE,WAAW,MAAM,UAAU;EACtE,GAAI,MAAM,QAAQ,KAAA,IAAY,CAAC,IAAI,EAAE,KAAK,MAAM,IAAI;CACtD;AACF;;;ACpIA,SAAgB,SAAS,MAAkB;CACzC,OAAO,OAAO,MAAM,QAAQ,IAAI,IAAI,KAAK;AAC3C;;AAGA,SAAgB,UAAU,GAAW,GAAW,GAA6B;CAC3E,OAAO,CAAC,IAAI,GAAG,CAAC;AAClB;;AAGA,SAAgB,SAAS,MAAY,GAAiB;CACpD,MAAM,EAAE,MAAM,GAAG,GAAG,GAAG,SAAS;CAChC,OAAO;EACL,GAAG;EACH,GAAI,SAAS,KAAA,IAAY,CAAC,IAAI,EAAE,MAAM,SAAS,IAAI,EAAE;EACrD,GAAI,MAAM,KAAA,IAAY,CAAC,IAAI,EAAE,GAAG,IAAI,EAAE;EACtC,GAAI,MAAM,KAAA,IAAY,CAAC,IAAI,EAAE,GAAG,EAAE;CACpC;AACF;;AAGA,SAAgB,WAAW,QAAwB;CACjD,IAAI,OAAO,WAAW,UAAU,OAAO;CACvC,MAAM,IAAI,YAAY,MAAM;CAC5B,OAAO;EAAE,KAAK,EAAE;EAAM,OAAO,EAAE;EAAK,QAAQ,EAAE;EAAO,MAAM,EAAE;CAAO;AACtE;;AAGA,SAAgB,WAAW,QAAgB,GAAmB;CAC5D,QAAQ,OAAO,MAAf;EACE,KAAK,QACH,OAAO;GAAE,MAAM;GAAQ,GAAG,IAAI,OAAO,IAAI,OAAO;GAAG,GAAG,OAAO;GAAG,GAAG,OAAO;GAAG,GAAG,OAAO;EAAE;EAC3F,KAAK,UACH,OAAO;GAAE,GAAG;GAAQ,IAAI,IAAI,OAAO;GAAI,IAAI,OAAO;EAAG;EACvD,KAAK,WACH,OAAO;GAAE,MAAM;GAAW,IAAI,IAAI,OAAO;GAAI,IAAI,OAAO;GAAI,IAAI,OAAO;GAAI,IAAI,OAAO;EAAG;EAC3F,KAAK,WAAW;GACd,MAAM,QAAQ,SAAqE,KAAK,KAAK,CAAC,GAAG,OAAO,UAAU,GAAG,GAAG,CAAC,CAAC;GAC1H,OAAO,OAAO,UAAU,KAAA,IACpB;IAAE,MAAM;IAAW,QAAQ,KAAK,OAAO,MAAM;GAAE,IAC/C;IAAE,MAAM;IAAW,QAAQ,KAAK,OAAO,MAAM;IAAG,OAAO,OAAO,MAAM,IAAI,IAAI;GAAE;EACpF;CACF;AACF;;AAGA,SAAgB,QAAQ,KAA2B;CACjD,OAAO;EACL,GAAG;EACH,GAAG,IAAI;EACP,GAAG,IAAI;EACP,GAAI,IAAI,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO,IAAI,MAAM,KAAK,MAAM,SAAS,GAAG,IAAI,CAAC,CAAC,EAAE;EACrF,GAAI,IAAI,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,QAAQ,WAAW,IAAI,MAAM,EAAE;EACrE,GAAI,IAAI,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO,WAAW,IAAI,OAAO,IAAI,CAAC,EAAE;CAC3E;AACF;;;;;;;AC9BA,SAAgB,UAAU,OAAoB,QAAiC;CAC7E,MAAM,KAAK,IAAI,IAAI,OAAO,WAAW,KAAK,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;CAC1D,MAAM,QAAQ,IAAI,IAAI,OAAO,MAAM,KAAK,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;CACxD,MAAM,QAAQ,MAAM,MAAM,KAAK,QAAQ;EACrC,MAAM,IAAI,GAAG,IAAI,IAAI,EAAE;EACvB,IAAI,MAAM,KAAA,GAAW,OAAO;EAE5B,OAAO;GAAE,GADM,EAAE,SAAS,KAAK,QAAQ,GAAG,IAAI;GAC1B,GAAG,EAAE;GAAG,GAAG,EAAE;EAAE;CACrC,CAAC;CACD,MAAM,YAAY,MAAM,WAAW,KAAK,MAAM;EAC5C,MAAM,IAAI,MAAM,IAAI,EAAE,EAAE;EACxB,IAAI,MAAM,KAAA,GAAW,OAAO;EAC5B,MAAM,IAAI,SAAS,EAAE,MAAM;EAC3B,OAAO;GAAE,GAAG;GAAG,QAAQ,UAAU,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;EAAE;CACnE,CAAC;CACD,OAAO;EACL,GAAG;EACH,WAAW;GAAE,GAAI,MAAM,aAAa,EAAE,OAAO,OAAO;GAAI,GAAG,OAAO,KAAK;GAAG,GAAG,OAAO,KAAK;EAAE;EAC3F;EACA,GAAI,cAAc,KAAA,IAAY,CAAC,IAAI,EAAE,UAAU;CACjD;AACF;;;ACeA,SAAgB,eAAe,OAAoB,KAA6B;CAC9E,OAAO;EACL,QAAQ;EACR,GAAG;EACH,OAAO,MAAM;EACb,WAAW,MAAM,QAAQ;EACzB,KAAK,MAAM,OAAO;EAClB,OAAO,MAAM,MAAM;EACnB,OAAO,MAAM,OAAO,UAAU;CAChC;AACF;;;AC5EA,SAAgB,WAAW,KAAkC;CAC3D,IAAI,IAAI,MAAM,KAAA,KAAa,IAAI,MAAM,KAAA,GAAW,OAAO,KAAA;CACvD,IAAI,IAAI,UAAU,KAAA,GAAW,OAAO;EAAE,GAAG,IAAI;EAAG,GAAG,IAAI;EAAG,GAAG,IAAI;EAAG,GAAG,IAAI;CAAE;CAC7E,MAAM,IAAI,SAAS,IAAI,KAAK;CAC5B,OAAO;EAAE,GAAG,IAAI,IAAI,EAAE;EAAG,GAAG,IAAI,IAAI,EAAE;EAAG,GAAG,EAAE;EAAG,GAAG,EAAE;CAAE;AAC1D;;;;;AAMA,SAAgB,cAAc,OAA6B,YAAoB,kBAAkB,SAAiB;CAChH,MAAM,QAAgB,CAAC;CACvB,KAAK,MAAM,OAAO,OAAO;EACvB,MAAM,IAAI,WAAW,GAAG;EACxB,IAAI,MAAM,KAAA,GAAW,MAAM,KAAK,KAAK,GAAG,IAAI,MAAM,CAAC;CACrD;CACA,MAAM,MAAM,GAAG,MAAM,EAAE,IAAI,EAAE,CAAC;CAC9B,IAAI,QAAQ;CACZ,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;EACrC,MAAM,IAAI,MAAM;EAChB,KAAK,IAAI,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;GACzC,MAAM,IAAI,MAAM;GAChB,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,WAAW;GAClC,MAAM,IAAI,UAAU,GAAG,CAAC;GACxB,IAAI,EAAE,IAAI,aAAa,EAAE,IAAI,WAAW;EAC1C;CACF;CACA,OAAO;AACT;;;;;;AAOA,SAAgB,aAAa,OAAoB,YAAoB,kBAAkB,SAAiB;CACtG,MAAM,IAAI,MAAM;CAChB,IAAI,MAAM,KAAA,GAAW,OAAO;CAC5B,MAAM,OAAO,EAAE,UAAU,KAAA,IAAY;EAAE,GAAG;EAAG,GAAG;EAAG,GAAG,EAAE,KAAK;EAAU,GAAG,EAAE,KAAK;CAAS,IAAI,SAAS,EAAE,KAAK;CAC9G,MAAM,QAAQ,EAAE,SAAS;CACzB,MAAM,OAAO,EAAE,UAAU,UAAU,EAAE,UAAU;CAC/C,MAAM,OAAO,EAAE,UAAU,UAAU,EAAE,UAAU;CAC/C,IAAI,QAAQ;CACZ,KAAK,MAAM,OAAO,MAAM,OAAO;EAC7B,MAAM,KAAK,WAAW,GAAG;EACzB,IAAI,OAAO,KAAA,GAAW;EACtB,MAAM,IAAI,KAAK,IAAI,IAAI,MAAM;EAC7B,MAAM,OAAO,EAAE,IAAI,KAAK,IAAI,QAAQ,aAAa,EAAE,IAAI,EAAE,IAAI,KAAK,IAAI,KAAK,IAAI,QAAQ;EACvF,MAAM,OAAO,EAAE,IAAI,KAAK,IAAI,QAAQ,aAAa,EAAE,IAAI,EAAE,IAAI,KAAK,IAAI,KAAK,IAAI,QAAQ;EACvF,IAAK,QAAQ,QAAU,QAAQ,MAAO;CACxC;CACA,OAAO;AACT;;AAGA,SAAgB,oBAAoB,OAA6B,MAAc,YAAoB;CACjG,IAAI,QAAQ;CACZ,KAAK,MAAM,OAAO,OAChB,KAAK,MAAM,KAAK,IAAI,SAAS,CAAC,GAAG;EAC/B,IAAI,EAAE,MAAM,KAAA,KAAa,EAAE,MAAM,KAAA,KAAa,EAAE,SAAS,KAAA,GAAW;EACpE,MAAM,QAAQ,EAAE,SAAS,SAAS,EAAE,SAAS,WAAW,EAAE,IAAI,EAAE;EAChE,MAAM,SAAS,EAAE,SAAS,SAAS,EAAE,SAAS,WAAW,IAAI,IAAI,IAAI;EACrE,MAAM,SAAS;GAAE,KAAK,EAAE;GAAG,QAAQ,EAAE,IAAI,IAAI;GAAG,MAAM,EAAE;GAAG,OAAO,EAAE,IAAI,IAAI;EAAE,EAAE,EAAE;EAClF,IAAI,KAAK,IAAI,MAAM,IAAI,OAAO,QAAQ,CAAC,OAAO,QAAQ,SAAS,KAAK;CACtE;CAEF,OAAO;AACT;;AAWA,SAAgB,eAAe,QAAgB,MAAqB;CAClE,IAAI,KAAK,KAAK,KAAK,KAAK,KAAK,GAAG,OAAO;CACvC,QAAQ,OAAO,MAAf;EACE,KAAK,QAAQ;GACX,MAAM,IAAI,UAAU,QAAQ,IAAI;GAChC,OAAO,EAAE,IAAI,KAAK,EAAE,IAAI;EAC1B;EACA,KAAK,UACH,OAAO,eAAe,OAAO,IAAI,OAAO,IAAI,OAAO,GAAG,OAAO,GAAG,IAAI;EACtE,KAAK,WACH,OAAO,eAAe,OAAO,IAAI,OAAO,IAAI,OAAO,IAAI,OAAO,IAAI,IAAI;EACxE,KAAK,WAAW;GACd,MAAM,QAAQ,CAAC,OAAO,QAAQ,GAAI,OAAO,SAAS,CAAC,CAAE;GACrD,MAAM,UAAU,GAAW,MAAuB,IAAI,KAAK,KAAK,IAAI,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,IAAI,KAAK;GACvH,IAAI,OAAO,OAAO,MAAM,CAAC,GAAG,OAAO,OAAO,GAAG,CAAC,CAAC,GAAG,OAAO;GACzD,KAAK,MAAM,QAAQ,OACjB,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;IACpC,MAAM,IAAI,KAAK;IACf,MAAM,IAAI,MAAM,IAAI,KAAK,KAAK;IAC9B,MAAM,IAAI,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,IAAI;IAC9C,IAAI,MAAM,KAAA,KAAa,OAAO,EAAE,IAAI,EAAE,EAAE,GAAG,OAAO;GACpD;GAEF,OAAO,aAAa,KAAK,IAAI,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,IAAI,GAAG,KAAK;EACrE;CACF;AACF;;AAGA,SAAS,eAAe,IAAY,IAAY,IAAY,IAAY,MAAqB;CAC3F,IAAI,MAAM,KAAK,MAAM,GAAG,OAAO;CAC/B,MAAM,MAAM,KAAK,IAAI,MAAM;CAC3B,MAAM,MAAM,KAAK,IAAI,KAAK,IAAI,MAAM;CACpC,MAAM,MAAM,KAAK,IAAI,MAAM;CAC3B,MAAM,MAAM,KAAK,IAAI,KAAK,IAAI,MAAM;CACpC,MAAM,KAAK,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE;CAC9B,MAAM,KAAK,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE;CAC9B,OAAO,KAAK,KAAK,KAAK,KAAK;AAC7B;;AAGA,SAAS,QAAQ,IAAY,IAAY,IAAY,IAAY,GAAgD;CAC/G,IAAI,KAAK;CACT,IAAI,KAAK;CACT,MAAM,KAAK,KAAK;CAChB,MAAM,KAAK,KAAK;CAChB,MAAM,QAAgD;EACpD,CAAC,CAAC,IAAI,KAAK,EAAE,CAAC;EACd,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE;EACnB,CAAC,CAAC,IAAI,KAAK,EAAE,CAAC;EACd,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE;CACrB;CACA,KAAK,MAAM,CAAC,GAAG,MAAM,OACnB,IAAI,MAAM,GACJ;MAAA,IAAI,GAAG,OAAO,KAAA;CAAA,OACb,IAAI,IAAI,GACb,KAAK,KAAK,IAAI,IAAI,IAAI,CAAC;MAEvB,KAAK,KAAK,IAAI,IAAI,IAAI,CAAC;CAG3B,IAAI,MAAM,IAAI,OAAO,KAAA;CACrB,MAAM,KAAK,KAAK,MAAM;CACtB,OAAO,CAAC,KAAK,IAAI,IAAI,KAAK,IAAI,EAAE;AAClC;;AAGA,SAAS,aAAa,GAAW,GAAW,OAAqE;CAC/G,IAAI,SAAS;CACb,KAAK,MAAM,QAAQ,OACjB,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,SAAS,GAAG,IAAI,KAAK,QAAQ,IAAI,KAAK;EAC7D,MAAM,CAAC,IAAI,MAAM,KAAK;EACtB,MAAM,CAAC,IAAI,MAAM,KAAK;EACtB,IAAI,KAAK,MAAM,KAAK,KAAK,KAAM,KAAK,OAAO,IAAI,OAAQ,KAAK,MAAM,IAAI,SAAS,CAAC;CAClF;CAEF,OAAO;AACT;;;;;AAMA,SAAgB,kBACd,OACA,WACA,YAAoB,kBAAkB,SAC9B;CACR,IAAI,cAAc,KAAA,KAAa,UAAU,WAAW,GAAG,OAAO;CAC9D,IAAI,QAAQ;CACZ,KAAK,MAAM,OAAO,OAAO;EACvB,MAAM,KAAK,WAAW,GAAG;EACzB,IAAI,OAAO,KAAA,GAAW;EACtB,MAAM,IAAI,KAAK,IAAI,IAAI,MAAM;EAC7B,MAAM,QAAQ;GAAE,GAAG,EAAE,IAAI;GAAW,GAAG,EAAE,IAAI;GAAW,GAAG,EAAE,IAAI,IAAI;GAAW,GAAG,EAAE,IAAI,IAAI;EAAU;EACvG,KAAK,MAAM,KAAK,WAAW,IAAI,eAAe,EAAE,QAAQ,KAAK,GAAG;CAClE;CACA,OAAO;AACT;;;;;;;;;AAUA,SAAgB,SAAS,OAAoB,QAAoB,YAAoB,kBAAkB,SAAmB;CACxH,MAAM,QAAQ,UAAU,OAAO,MAAM;CACrC,MAAM,SAAS,IAAI,IAAI,OAAO,WAAW,KAAK,MAAM,EAAE,EAAE,CAAC;CACzD,MAAM,MAAM,IAAI,IAAI,OAAO,QAAQ;CACnC,MAAM,MAAM,IAAI,IAAI,MAAM,MAAM,KAAK,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;CACrD,IAAI,cAAc;CAClB,IAAI,aAAa;CACjB,MAAM,OAAoB,CAAC;CAC3B,KAAK,MAAM,OAAO,MAAM,OAAO;EAC7B,MAAM,MAAM,IAAI,IAAI,IAAI,EAAE;EAC1B,IAAI,KAAK,WAAW,MAAM;GACxB,IAAI,IAAI,MAAM,KAAA,KAAa,IAAI,MAAM,KAAA,GAAW;GAChD,MAAM,QAAQ,KAAK,IAAI,IAAI,KAAK,IAAI,KAAK,IAAI,EAAE,IAAA,QAAkB,KAAK,IAAI,IAAI,KAAK,IAAI,KAAK,IAAI,EAAE,IAAA;GAClG,IAAI,CAAC,OAAO,IAAI,IAAI,EAAE,KAAK,OAAO;GAClC,KAAK,KAAK,GAAG;EACf,OAAO,IAAI,OAAO,IAAI,IAAI,EAAE,GAC1B,KAAK,KAAK,GAAG;OACR,IAAI,CAAC,IAAI,IAAI,IAAI,EAAE,GACxB;CAEJ;CACA,OAAO;EACL,cAAc,cAAc,MAAM,SAAS,IAAI,kBAAkB,MAAM,MAAM,WAAW,SAAS;EACjG,gBAAgB,aAAa;GAAE,GAAG;GAAO,OAAO;EAAK,GAAG,SAAS;EACjE,eAAe;EACf,UAAU;CACZ;AACF;;;;;AAMA,SAAgB,WAAW,OAAuC;CAChE,MAAM,WAAqB,CAAC;CAC5B,MAAM,sBAAM,IAAI,IAAY;CAC5B,KAAK,MAAM,OAAO,MAAM,OAAO;EAC7B,IAAI,IAAI,IAAI,IAAI,EAAE,GAAG,SAAS,KAAK,OAAO,IAAI,GAAG,uBAAuB;EACxE,IAAI,IAAI,IAAI,EAAE;EACd,IAAI,EAAE,IAAI,IAAI,KAAK,IAAI,IAAI,KAAK,OAAO,SAAS,IAAI,CAAC,KAAK,OAAO,SAAS,IAAI,CAAC,IAAI,SAAS,KAAK,OAAO,IAAI,GAAG,sCAAsC;EACrJ,IAAI,IAAI,WAAW,SAAS,IAAI,MAAM,KAAA,KAAa,IAAI,MAAM,KAAA,IAAY,SAAS,KAAK,OAAO,IAAI,GAAG,+BAA+B;CACtI;CACA,IAAI,oBAAoB,MAAM,KAAK,IAAI,GAAG,SAAS,KAAK,8CAA8C;CACtG,KAAK,MAAM,QAAQ,MAAM,SAAS,CAAC,GACjC,IAAI,CAAC,IAAI,IAAI,KAAK,MAAM,KAAK,CAAC,IAAI,IAAI,KAAK,MAAM,GAAG,SAAS,KAAK,QAAQ,KAAK,GAAG,wCAAwC;CAE5H,OAAO;AACT;;;AClMA,MAAa,iBAAyD,OAAO,OAAO;CAClF;EAAE,IAAI;EAAc,WAAW;EAAO,MAAM;EAAU,WAAW;EAAU,KAAK;EAAG,KAAK;EAAG,OAAO;EAAG,MAAM;CAAQ;CACnH;EAAE,IAAI;EAAqB,WAAW;EAAM,MAAM;EAAe,WAAW;EAAS,KAAK;EAAG,KAAK;EAAU,OAAO;EAAG,KAAK;EAAM,MAAM;CAAQ;CAC/I;EAAE,IAAI;EAAe,WAAW;EAAO,MAAM;EAAU,WAAW;EAAS,KAAK;EAAG,KAAK;EAAG,OAAO;EAAG,KAAK;EAAO,MAAM;CAAQ;CAC/H;EAAE,IAAI;EAA4B,WAAW;EAAG,MAAM;EAAS,WAAW;EAAS,KAAK;EAAG,KAAK;EAAU,OAAO;EAAG,KAAK;EAAG,MAAM;CAAQ;CAC1I;EAAE,IAAI;EAAyB,WAAW;EAAM,MAAM;EAAmB,WAAW;EAAS,KAAK;EAAG,KAAK;EAAU,OAAO;EAAG,KAAK;EAAM,MAAM;CAAQ;CACvJ;EAAE,IAAI;EAAwB,WAAW;EAAO,MAAM;EAAK,WAAW;EAAS,KAAK;EAAG,KAAK;EAAU,OAAO;EAAG,KAAK;EAAO,MAAM;CAAe;CACjJ;EAAE,IAAI;EAAyB,WAAW;EAAM,MAAM;EAAU,WAAW;EAAS,KAAK;EAAG,KAAK;EAAG,OAAO;EAAG,KAAK;EAAM,MAAM;CAAe;CAC9I;EAAE,IAAI;EAAoB,MAAM;EAAW,WAAW;EAAU,KAAK;EAAI,KAAK;EAAG,MAAM;CAAa;CACpG;EAAE,IAAI;EAAY,MAAM;EAAgB,WAAW;EAAS,KAAK;EAAG,KAAK;EAAU,MAAM;CAAO;AAClG,CAAU;;AAGV,MAAa,kBAAkB;;;;;;AAU/B,SAAgB,YAAY,MAAsC,IAAI,GAAG,IAAI,GAAW;CACtF,MAAM,IAAI,KAAK,aAAa;CAC5B,MAAM,IAAI,OAAO,MAAM,WAAW,IAAI,EAAE,MAAM,KAAK,IAAI,KAAK,IAAI,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC;CAC/E,OAAO,KAAK,IAAI,YAAY,OAAO,SAAS,CAAC,IAAI,IAAI,CAAC;AACxD;;;;;;AAOA,SAAgB,eAAe,WAAmB,UAAkB,MAA6D;CAC/H,IAAI,OAAO,MAAM,SAAS,KAAK,OAAO,MAAM,QAAQ,GAAG,MAAM,IAAI,WAAW,gCAAgC;CAC5G,IAAI,cAAc,UAAU,OAAO;CACnC,MAAM,QAAQ,YAAY;CAC1B,IAAI,EAAE,KAAK,IAAI,KAAK,IAAI,YAAY,MAAM,WAAW,QAAQ,IAAI,OAAO;CAExE,QADe,KAAK,cAAc,UAAU,QAAQ,IAAI,QAAQ,KAChD,WAAW;AAC7B"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,60 @@
|
|
|
1
1
|
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/package",
|
|
2
3
|
"name": "@mark1russell7/layout-contract",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
4
|
+
"version": "0.2.2",
|
|
5
|
+
"description": "The shared contract of the packing and highways layout engines: the input and record formats, the turn rule, margins, tolerances and hard rules.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"layout",
|
|
8
|
+
"packing",
|
|
9
|
+
"edge-routing",
|
|
10
|
+
"graph-layout",
|
|
11
|
+
"contract",
|
|
12
|
+
"typescript"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/mark1russell7/layout-contract#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/mark1russell7/layout-contract/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/mark1russell7/layout-contract.git",
|
|
21
|
+
"directory": "packages/layout-contract"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "Mark Russell (https://github.com/mark1russell7)",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"main": "./build/index.js",
|
|
28
|
+
"types": "./build/index.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./build/index.d.ts",
|
|
32
|
+
"default": "./build/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./package.json": "./package.json"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"build",
|
|
38
|
+
"src",
|
|
39
|
+
"!src/**/*.test.ts"
|
|
40
|
+
],
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=22.12"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@mark1russell7/cue": "^1.0.2",
|
|
49
|
+
"@types/node": "^26.6.4",
|
|
50
|
+
"typescript": "^7.0.2",
|
|
51
|
+
"vitest": "^5.0.3",
|
|
52
|
+
"tsdown": "^0.23.1"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "tsdown",
|
|
56
|
+
"test": "vitest run",
|
|
57
|
+
"test:watch": "vitest",
|
|
58
|
+
"typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.test.json"
|
|
59
|
+
}
|
|
6
60
|
}
|
package/src/geometry.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/** A side of a box. The y axis points down, so `top` has the smallest y. */
|
|
2
|
+
export type Side = "top" | "right" | "bottom" | "left";
|
|
3
|
+
|
|
4
|
+
/** The sides in clockwise order, from the top. */
|
|
5
|
+
export const SIDES: readonly Side[] = ["top", "right", "bottom", "left"];
|
|
6
|
+
|
|
7
|
+
export interface Point {
|
|
8
|
+
readonly x: number;
|
|
9
|
+
readonly y: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** A rectangle: its top-left corner and its size. */
|
|
13
|
+
export interface Rect {
|
|
14
|
+
readonly x: number;
|
|
15
|
+
readonly y: number;
|
|
16
|
+
readonly w: number;
|
|
17
|
+
readonly h: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A value for each side of a box. */
|
|
21
|
+
export interface Sides {
|
|
22
|
+
readonly top: number;
|
|
23
|
+
readonly right: number;
|
|
24
|
+
readonly bottom: number;
|
|
25
|
+
readonly left: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A region. The forms and the rules are those of `docs/FORMAT.md` of `packing`.
|
|
30
|
+
* A ring of a polygon closes without a copy of its first point.
|
|
31
|
+
*/
|
|
32
|
+
export type Region =
|
|
33
|
+
| { readonly kind: "rect"; readonly x: number; readonly y: number; readonly w: number; readonly h: number }
|
|
34
|
+
| {
|
|
35
|
+
readonly kind: "polygon";
|
|
36
|
+
readonly points: readonly (readonly [number, number])[];
|
|
37
|
+
readonly holes?: readonly (readonly (readonly [number, number])[])[];
|
|
38
|
+
}
|
|
39
|
+
| { readonly kind: "circle"; readonly cx: number; readonly cy: number; readonly r: number }
|
|
40
|
+
| { readonly kind: "ellipse"; readonly cx: number; readonly cy: number; readonly rx: number; readonly ry: number };
|
|
41
|
+
|
|
42
|
+
/** A margin: one value for the four sides, or one value for each side. */
|
|
43
|
+
export type Margin = number | Sides;
|
|
44
|
+
|
|
45
|
+
/** This function gives the value of each side of a margin. No margin gives 0 on each side. */
|
|
46
|
+
export function marginSides(margin: Margin | undefined): Sides {
|
|
47
|
+
if (margin === undefined) return { top: 0, right: 0, bottom: 0, left: 0 };
|
|
48
|
+
if (typeof margin === "number") return { top: margin, right: margin, bottom: margin, left: margin };
|
|
49
|
+
return margin;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** This function gives the rectangle made larger by the margin on each side. */
|
|
53
|
+
export function grow(rect: Rect, margin: Margin | undefined): Rect {
|
|
54
|
+
const m = marginSides(margin);
|
|
55
|
+
return { x: rect.x - m.left, y: rect.y - m.top, w: rect.w + m.left + m.right, h: rect.h + m.top + m.bottom };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** This function gives the bounding box of a region. */
|
|
59
|
+
export function boundsOf(region: Region): Rect {
|
|
60
|
+
switch (region.kind) {
|
|
61
|
+
case "rect":
|
|
62
|
+
return { x: region.x, y: region.y, w: region.w, h: region.h };
|
|
63
|
+
case "circle":
|
|
64
|
+
return { x: region.cx - region.r, y: region.cy - region.r, w: 2 * region.r, h: 2 * region.r };
|
|
65
|
+
case "ellipse":
|
|
66
|
+
return { x: region.cx - region.rx, y: region.cy - region.ry, w: 2 * region.rx, h: 2 * region.ry };
|
|
67
|
+
case "polygon": {
|
|
68
|
+
let x0 = Infinity;
|
|
69
|
+
let y0 = Infinity;
|
|
70
|
+
let x1 = -Infinity;
|
|
71
|
+
let y1 = -Infinity;
|
|
72
|
+
for (const [x, y] of region.points) {
|
|
73
|
+
x0 = Math.min(x0, x);
|
|
74
|
+
y0 = Math.min(y0, y);
|
|
75
|
+
x1 = Math.max(x1, x);
|
|
76
|
+
y1 = Math.max(y1, y);
|
|
77
|
+
}
|
|
78
|
+
return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** This function gives the region moved by `dx` and `dy`. */
|
|
84
|
+
export function translate(region: Region, dx: number, dy: number): Region {
|
|
85
|
+
switch (region.kind) {
|
|
86
|
+
case "rect":
|
|
87
|
+
return { ...region, x: region.x + dx, y: region.y + dy };
|
|
88
|
+
case "circle":
|
|
89
|
+
case "ellipse":
|
|
90
|
+
return { ...region, cx: region.cx + dx, cy: region.cy + dy };
|
|
91
|
+
case "polygon": {
|
|
92
|
+
const move = (ring: readonly (readonly [number, number])[]): [number, number][] => ring.map(([x, y]) => [x + dx, y + dy]);
|
|
93
|
+
return region.holes === undefined
|
|
94
|
+
? { kind: "polygon", points: move(region.points) }
|
|
95
|
+
: { kind: "polygon", points: move(region.points), holes: region.holes.map(move) };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* This function gives the overlap of the insides of two rectangles on each axis.
|
|
102
|
+
* A value of 0 or less on an axis is no overlap. Two rectangles with a common edge have an overlap of 0.
|
|
103
|
+
*/
|
|
104
|
+
export function overlapOf(a: Rect, b: Rect): { readonly x: number; readonly y: number } {
|
|
105
|
+
return {
|
|
106
|
+
x: Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x),
|
|
107
|
+
y: Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y),
|
|
108
|
+
};
|
|
109
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./geometry.ts";
|
|
2
|
+
export * from "./tolerance.ts";
|
|
3
|
+
export * from "./input.ts";
|
|
4
|
+
export * from "./output.ts";
|
|
5
|
+
export * from "./record.ts";
|
|
6
|
+
export * from "./rules.ts";
|
|
7
|
+
export * from "./turn.ts";
|
|
8
|
+
export * from "./measures.ts";
|
|
9
|
+
export * from "./strategy.ts";
|
package/src/input.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { Margin, Region, Side } from "./geometry.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A port of a box: a place where edges go into the box.
|
|
5
|
+
* With `x` and `y`, the port is fixed on the border of the box, relative to its top-left corner.
|
|
6
|
+
* Without them, the router places the port on its `side`. Without a side, the router picks the side.
|
|
7
|
+
*/
|
|
8
|
+
export interface Port {
|
|
9
|
+
readonly id: string;
|
|
10
|
+
readonly side?: Side;
|
|
11
|
+
readonly x?: number;
|
|
12
|
+
readonly y?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** A box of an input. In the packer, its name is item. */
|
|
16
|
+
export interface LayoutBox {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly w: number;
|
|
19
|
+
readonly h: number;
|
|
20
|
+
/** The packer writes the position. The router reads it. */
|
|
21
|
+
readonly x?: number;
|
|
22
|
+
readonly y?: number;
|
|
23
|
+
/** A locked box keeps `x` and `y`. The packer reads it as an obstacle. */
|
|
24
|
+
readonly locked?: boolean;
|
|
25
|
+
/** The reading order. Without it, the order of the array is the rank. */
|
|
26
|
+
readonly rank?: number;
|
|
27
|
+
readonly parent?: string;
|
|
28
|
+
readonly group?: string;
|
|
29
|
+
/** A region relative to the origin of the box. Without it, the box is the rectangle `w` by `h`. */
|
|
30
|
+
readonly shape?: Region;
|
|
31
|
+
readonly ports?: readonly Port[];
|
|
32
|
+
/** A ring around the box. No other box enters it. Only a route from a port of the box enters it, at a right angle. */
|
|
33
|
+
readonly margin?: Margin;
|
|
34
|
+
/** The packer can turn the box by 90 degrees. The output has the turned box and ports. */
|
|
35
|
+
readonly turnable?: boolean;
|
|
36
|
+
/** The free data of the node. The grouping rule `keys` of `highways` reads `data.group`. */
|
|
37
|
+
readonly data?: Readonly<Record<string, unknown>>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The packer name of a box. It is the same type. */
|
|
41
|
+
export type Item = LayoutBox;
|
|
42
|
+
|
|
43
|
+
/** An edge between two boxes, with optional ports. */
|
|
44
|
+
export interface LayoutEdge {
|
|
45
|
+
readonly id: string;
|
|
46
|
+
readonly source: string;
|
|
47
|
+
readonly target: string;
|
|
48
|
+
readonly sourcePort?: string;
|
|
49
|
+
readonly targetPort?: string;
|
|
50
|
+
readonly data?: Readonly<Record<string, unknown>>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The container. `fixed` tells which sides of the size stay. */
|
|
54
|
+
export interface LayoutContainer {
|
|
55
|
+
readonly w?: number;
|
|
56
|
+
readonly h?: number;
|
|
57
|
+
readonly fixed: "size" | "width" | "height" | "none";
|
|
58
|
+
/** The target aspect: the width divided by the height. The default is 1. */
|
|
59
|
+
readonly aspect?: number;
|
|
60
|
+
/** The weight of the aspect error in the objective. 0 gives the least area. The default is 1. */
|
|
61
|
+
readonly aspectWeight?: number;
|
|
62
|
+
/** The shape. Without it, the container is the rectangle from 0, 0 to `w`, `h`. */
|
|
63
|
+
readonly shape?: Region;
|
|
64
|
+
/** The space between the border and the boxes with their margins. */
|
|
65
|
+
readonly inset?: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** An area that boxes and routes avoid. Without `move`, it stays. With `move`, the packer can move it for `cost` times the distance. */
|
|
69
|
+
export interface Obstacle {
|
|
70
|
+
readonly id: string;
|
|
71
|
+
readonly region: Region;
|
|
72
|
+
readonly move?: { readonly cost: number };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The generator of an input. The same name, version, parameters and seed give the same file, byte for byte. */
|
|
76
|
+
export interface LayoutGenerator {
|
|
77
|
+
readonly name: string;
|
|
78
|
+
readonly version: number;
|
|
79
|
+
readonly params: Readonly<Record<string, unknown>>;
|
|
80
|
+
readonly seed: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The cell of an input in the taxonomy: one value for each dimension. The dimensions are in `docs/FORMAT.md` of `packing`. */
|
|
84
|
+
export interface LayoutCell {
|
|
85
|
+
readonly containerSize?: "fixed" | "freeHeight" | "freeWidth" | "freeAspect" | "freeArea";
|
|
86
|
+
readonly containerShape?: "rect" | "convex" | "polygon" | "circle" | "ellipse" | "holes";
|
|
87
|
+
readonly obstacles?: "none" | "center" | "scattered" | "wall" | "corridors" | "touching" | "edge";
|
|
88
|
+
readonly sizes?: "identical" | "uniform" | "heavyTail" | "bimodal" | "extremeAspect" | "nearIdentical";
|
|
89
|
+
readonly count?: "1" | "10" | "100" | "1k" | "10k";
|
|
90
|
+
readonly mode?: "offline" | "online" | "incremental";
|
|
91
|
+
readonly edgeDensity?: "sparse" | "medium" | "dense";
|
|
92
|
+
readonly edgeShape?: "random" | "clustered" | "hubChain" | "longRange" | "tree" | "bundled";
|
|
93
|
+
readonly groups?: "none" | "flat" | "nested";
|
|
94
|
+
readonly ports?: "free" | "sides" | "fixed";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** An input of the corpus: the format `layout-input/1`. */
|
|
98
|
+
export interface LayoutInput {
|
|
99
|
+
readonly schema: "layout-input/1";
|
|
100
|
+
readonly id: string;
|
|
101
|
+
readonly set?: string;
|
|
102
|
+
readonly family?: string;
|
|
103
|
+
readonly seed?: number;
|
|
104
|
+
/** The origin: `made-up`, `published:<set>` or `known-best:<construction>`. */
|
|
105
|
+
readonly source?: string;
|
|
106
|
+
/** The first 16 hex characters of the SHA-256 of the canonical file without this field. */
|
|
107
|
+
readonly hash?: string;
|
|
108
|
+
/** The generator of the input. `null` or no value tells that a person made the input. */
|
|
109
|
+
readonly generator?: LayoutGenerator | null;
|
|
110
|
+
readonly cell?: LayoutCell;
|
|
111
|
+
/** The place of the input in a sequence of changes. */
|
|
112
|
+
readonly step?: number;
|
|
113
|
+
/** A router input can have no container. */
|
|
114
|
+
readonly container?: LayoutContainer;
|
|
115
|
+
/** The free space between two boxes with their margins. Routes use it. */
|
|
116
|
+
readonly gap?: number;
|
|
117
|
+
readonly boxes: readonly LayoutBox[];
|
|
118
|
+
readonly edges?: readonly LayoutEdge[];
|
|
119
|
+
readonly obstacles?: readonly Obstacle[];
|
|
120
|
+
/**
|
|
121
|
+
* A variant names its base input and its change.
|
|
122
|
+
* With `packed`, the variant is the packed output of its base input: the router routes the positions of that pack.
|
|
123
|
+
*/
|
|
124
|
+
readonly base?: string;
|
|
125
|
+
readonly change?: "add" | "remove" | "resize" | "reorder" | "packed";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The input of a packer: a `layout-input/1` file without the edges. */
|
|
129
|
+
export interface PackInput {
|
|
130
|
+
readonly container: LayoutContainer;
|
|
131
|
+
readonly items: readonly Item[];
|
|
132
|
+
readonly obstacles?: readonly Obstacle[];
|
|
133
|
+
readonly gap?: number;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** This function gives the packer input of a file. An input without a container gets `{ fixed: "none" }`, so the packer picks the size. */
|
|
137
|
+
export function toPackInput(input: LayoutInput): PackInput {
|
|
138
|
+
return {
|
|
139
|
+
container: input.container ?? { fixed: "none" },
|
|
140
|
+
items: input.boxes,
|
|
141
|
+
...(input.obstacles === undefined ? {} : { obstacles: input.obstacles }),
|
|
142
|
+
...(input.gap === undefined ? {} : { gap: input.gap }),
|
|
143
|
+
};
|
|
144
|
+
}
|
package/src/measures.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { RECORD_EPS } from "./tolerance.ts";
|
|
2
|
+
|
|
3
|
+
/** The direction of a measure: the side of the better values. */
|
|
4
|
+
export type Direction = "lower" | "higher";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The facts of one measure: its unit, its direction and its range.
|
|
8
|
+
* `floor` and `tol` are the parameters of the floor rule of the promotion rule (`docs/PROMOTION.md` of `highways`).
|
|
9
|
+
* Each engine exports the specs of its own measures. This package exports the specs of the `joint.*` measures.
|
|
10
|
+
*/
|
|
11
|
+
export interface MeasureSpec<Id extends string = string> {
|
|
12
|
+
readonly id: Id;
|
|
13
|
+
readonly unit: string;
|
|
14
|
+
readonly direction: Direction;
|
|
15
|
+
/** The lowest value. */
|
|
16
|
+
readonly min: number;
|
|
17
|
+
/** The highest value. The value `Infinity` tells that the measure has no upper limit. */
|
|
18
|
+
readonly max: number;
|
|
19
|
+
/** The family of the measure in the measure note of its engine. */
|
|
20
|
+
readonly family?: string;
|
|
21
|
+
/** The best value. Without it, the measure has no floor. */
|
|
22
|
+
readonly floor?: number;
|
|
23
|
+
/** The absolute tolerance at the floor. */
|
|
24
|
+
readonly tol?: number;
|
|
25
|
+
/**
|
|
26
|
+
* The smallest change that counts as better or worse. A number is an absolute change. `{ rel }` is a share of the larger of the two values.
|
|
27
|
+
* `docs/JOINT-MEASURES.md` gives the reason of each joint value.
|
|
28
|
+
*/
|
|
29
|
+
readonly minEffect?: number | { readonly rel: number };
|
|
30
|
+
/** The role of the measure in the joint selector. */
|
|
31
|
+
readonly role?: "guard" | "change-guard" | "diagnostic" | "time";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The id of a joint measure. `docs/JOINT-MEASURES.md` gives the definitions. */
|
|
35
|
+
export type JointMeasureId =
|
|
36
|
+
| "joint.fill"
|
|
37
|
+
| "joint.aspectError"
|
|
38
|
+
| "joint.spill"
|
|
39
|
+
| "joint.straight.crossings"
|
|
40
|
+
| "joint.straight.length"
|
|
41
|
+
| "joint.stability.mean"
|
|
42
|
+
| "joint.stability.moved"
|
|
43
|
+
| "joint.proxy.rank"
|
|
44
|
+
| "joint.ms";
|
|
45
|
+
|
|
46
|
+
/** The specs of the joint measures, in the order of `docs/JOINT-MEASURES.md`. */
|
|
47
|
+
export const JOINT_MEASURES: readonly MeasureSpec<JointMeasureId>[] = Object.freeze([
|
|
48
|
+
{ id: "joint.fill", minEffect: 0.001, unit: "0 to 1", direction: "higher", min: 0, max: 1, floor: 1, role: "guard" },
|
|
49
|
+
{ id: "joint.aspectError", minEffect: 0.01, unit: "natural log", direction: "lower", min: 0, max: Infinity, floor: 0, tol: 0.01, role: "guard" },
|
|
50
|
+
{ id: "joint.spill", minEffect: 0.001, unit: "0 to 1", direction: "lower", min: 0, max: 1, floor: 0, tol: 0.001, role: "guard" },
|
|
51
|
+
{ id: "joint.straight.crossings", minEffect: 0, unit: "count", direction: "lower", min: 0, max: Infinity, floor: 0, tol: 0, role: "guard" },
|
|
52
|
+
{ id: "joint.straight.length", minEffect: 0.01, unit: "S for each edge", direction: "lower", min: 0, max: Infinity, floor: 0, tol: 0.01, role: "guard" },
|
|
53
|
+
{ id: "joint.stability.mean", minEffect: 0.005, unit: "W", direction: "lower", min: 0, max: Infinity, floor: 0, tol: 0.005, role: "change-guard" },
|
|
54
|
+
{ id: "joint.stability.moved", minEffect: 0.02, unit: "0 to 1", direction: "lower", min: 0, max: 1, floor: 0, tol: 0.02, role: "change-guard" },
|
|
55
|
+
{ id: "joint.proxy.rank", unit: "-1 to 1", direction: "higher", min: -1, max: 1, role: "diagnostic" },
|
|
56
|
+
{ id: "joint.ms", unit: "milliseconds", direction: "lower", min: 0, max: Infinity, role: "time" },
|
|
57
|
+
] as const);
|
|
58
|
+
|
|
59
|
+
/** The threshold of `joint.stability.moved`, in widths `W` of the base. */
|
|
60
|
+
export const JOINT_MOVED_MIN = 0.01;
|
|
61
|
+
|
|
62
|
+
/** The result of a compare of a candidate with a baseline on one measure. A tie is not better. */
|
|
63
|
+
export type Verdict = "better" | "worse" | "tie";
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* This function gives the smallest change of a measure that counts between the values `a` and `b`.
|
|
67
|
+
* The value is `max(RECORD_EPS, minEffect)`, or `max(RECORD_EPS, rel * max(|a|, |b|))` for a relative `minEffect`.
|
|
68
|
+
* The larger value makes the threshold the same after a swap of `a` and `b`.
|
|
69
|
+
*/
|
|
70
|
+
export function minEffectOf(spec: Pick<MeasureSpec, "minEffect">, a = 0, b = 0): number {
|
|
71
|
+
const m = spec.minEffect ?? 0;
|
|
72
|
+
const t = typeof m === "number" ? m : m.rel * Math.max(Math.abs(a), Math.abs(b));
|
|
73
|
+
return Math.max(RECORD_EPS, Number.isFinite(t) ? t : 0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* This function compares the value `candidate` with the value `baseline` of one measure on one input.
|
|
78
|
+
* A change of at most `minEffectOf(spec, candidate, baseline)` is a tie. The test is symmetric: a swap of the two values swaps better and worse.
|
|
79
|
+
* Two equal values are a tie, also two infinite values. A value `NaN` is an error.
|
|
80
|
+
*/
|
|
81
|
+
export function compareMeasure(candidate: number, baseline: number, spec: Pick<MeasureSpec, "direction" | "minEffect">): Verdict {
|
|
82
|
+
if (Number.isNaN(candidate) || Number.isNaN(baseline)) throw new RangeError("compareMeasure: a value is NaN");
|
|
83
|
+
if (candidate === baseline) return "tie";
|
|
84
|
+
const delta = candidate - baseline;
|
|
85
|
+
if (!(Math.abs(delta) > minEffectOf(spec, candidate, baseline))) return "tie";
|
|
86
|
+
const better = spec.direction === "lower" ? delta < 0 : delta > 0;
|
|
87
|
+
return better ? "better" : "worse";
|
|
88
|
+
}
|
package/src/output.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { boundsOf, translate } from "./geometry.ts";
|
|
2
|
+
import type { LayoutInput } from "./input.ts";
|
|
3
|
+
import { turnBox } from "./turn.ts";
|
|
4
|
+
|
|
5
|
+
/** The position of one item. `turn` is 90 when the packer turned a turnable item. */
|
|
6
|
+
export interface Placement {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly x: number;
|
|
9
|
+
readonly y: number;
|
|
10
|
+
readonly turn?: 0 | 90;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The facts of one stage of a strategy, for the records and the site. */
|
|
14
|
+
export interface StageTrace {
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly ms?: number;
|
|
17
|
+
readonly measures?: Readonly<Record<string, number>>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The output of a packer. It does not change the input. */
|
|
21
|
+
export interface PackOutput {
|
|
22
|
+
readonly placements: readonly Placement[];
|
|
23
|
+
/** The ids of the items that the packer did not place. */
|
|
24
|
+
readonly overflow: readonly string[];
|
|
25
|
+
/** The size of the container after the pack. For a free size, the size that the search found. */
|
|
26
|
+
readonly size: { readonly w: number; readonly h: number };
|
|
27
|
+
/** The new top-left corner of the bounding box of each obstacle that moved. */
|
|
28
|
+
readonly moved: readonly { readonly id: string; readonly x: number; readonly y: number }[];
|
|
29
|
+
readonly trace?: readonly StageTrace[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* This function gives the input with the output of a packer applied. Each placed box gets its position.
|
|
34
|
+
* A turned box gets its turned size, ports, margin and shape. Each moved obstacle moves.
|
|
35
|
+
* The container gets the size of the output. The router reads this result. It does not read `turn`.
|
|
36
|
+
*/
|
|
37
|
+
export function applyPack(input: LayoutInput, output: PackOutput): LayoutInput {
|
|
38
|
+
const at = new Map(output.placements.map((p) => [p.id, p]));
|
|
39
|
+
const moved = new Map(output.moved.map((m) => [m.id, m]));
|
|
40
|
+
const boxes = input.boxes.map((box) => {
|
|
41
|
+
const p = at.get(box.id);
|
|
42
|
+
if (p === undefined) return box;
|
|
43
|
+
const turned = p.turn === 90 ? turnBox(box) : box;
|
|
44
|
+
return { ...turned, x: p.x, y: p.y };
|
|
45
|
+
});
|
|
46
|
+
const obstacles = input.obstacles?.map((o) => {
|
|
47
|
+
const m = moved.get(o.id);
|
|
48
|
+
if (m === undefined) return o;
|
|
49
|
+
const b = boundsOf(o.region);
|
|
50
|
+
return { ...o, region: translate(o.region, m.x - b.x, m.y - b.y) };
|
|
51
|
+
});
|
|
52
|
+
return {
|
|
53
|
+
...input,
|
|
54
|
+
container: { ...(input.container ?? { fixed: "none" }), w: output.size.w, h: output.size.h },
|
|
55
|
+
boxes,
|
|
56
|
+
...(obstacles === undefined ? {} : { obstacles }),
|
|
57
|
+
};
|
|
58
|
+
}
|