Add an edge renderer unit that draws a squared rectangle beside its Smith diagram. Bouwkamp codes are placed and converted into the circuit; planar networks with a battery are solved exactly with Kirchhoff and Ohm and arranged back into squares. The caption reports simple and perfect status and compound blocks are outlined. Register the engine across the catalog, editor, normalization profiles, smoke scripts, docs, and the diagramming skill.
- commit
- 5ae47fd59098b97a92099e95f99aec98be43cff7
- parent
- 7eea6d2be8a8c770788227bb550290899188a9db
- author
- Dami <dami.etoile@gmail.com>
@@ -323,6 +323,29 @@ export const diagramTypes = [
323323 rendererOptions: [],324324 upstream: 'https://pikchr.org/home/doc/trunk/doc/userman.md',325325 },326+ {327+ id: 'squaring',328+ label: 'Squaring',329+ fullName: 'Squared rectangles and Smith diagrams',330+ category: 'Graphs and networks',331+ format: 'Squaring text',332+ summary: 'Squaring draws a squared rectangle, which is a rectangle tiled by squares, next to its Smith diagram. In that circuit every horizontal segment is a node and every square is a unit resistor whose current equals its side.',333+ use: 'Use Squaring to explore squared rectangles and squared squares, or to check whether a dissection is simple and perfect. It can also build a squaring backwards from a planar network once you choose a battery edge.',334+ syntax: ['Optionally begin with `.view rectangle`, `.view circuit`, `.view overlay`, or `.view both`; the default is `both`.', 'Add `.labels none` to hide side lengths, voltages, and currents.', 'Add `title` followed by text to name the diagram.', 'For a known dissection, write `rectangle <width> x <height>` and then the square sides in Bouwkamp order; parentheses and commas are ignored.', 'Bouwkamp order places each square at the leftmost point of the highest unfilled segment. Squares that share a top edge are listed left to right.', 'For a network, write `battery <positive> <negative>` and then wires as `wire a b` or chains such as `a - b - c`.', 'Every wire has resistance one; the renderer solves Kirchhoff and Ohm exactly, scales the voltages so all sides are whole numbers, and arranges the squares.', 'The battery replaces one edge of the graph, so do not list that edge as a wire. A wire parallel to the battery becomes a full-height square.', 'The network form needs a planar network in which the battery closes a face; polyhedral (3-connected planar) networks give a simple squaring.', 'The caption reports the order, size, whether the squaring is simple (no smaller rectangle of squares) and perfect (no repeated side), and the battery.', 'Compound blocks are outlined; node color runs from blue at the negative pole to red at the positive pole.', 'Start comments with `#`.'],335+ language: 'text',336+ example: ['.view both', 'title Order 9 simple perfect squared rectangle', '', 'rectangle 33 x 32', 'squares (18 15) (7 8) (14 4) (10 1) (9)'],337+ styleSummary: 'Squaring tints each square by the voltage at its top edge, colors nodes from blue to red by voltage, and scales wire thickness by current. Compound blocks are outlined with a dashed line.',338+ sourceStyle: ['Use `.view rectangle` for the dissection alone and `.view circuit` for the Smith diagram alone.', 'Use `.view overlay` to draw the circuit on top of the squares, which shows how each segment collapses into a node.', 'Use `.labels none` for large dissections where numbers would not fit.', 'Choose a different battery wire in a network to obtain a different squaring from the same graph.', 'Reorder the square list, not the geometry, to change a Bouwkamp dissection.'],339+ rendererOptions: [],340+ limitations: ['Networks are arranged by search, so very large or non-planar networks are rejected instead of drawn.', 'A wire whose two nodes settle at the same voltage carries no current and is rejected; choose another battery.', 'Node positions in the Smith diagram follow the midpoints of the horizontal segments rather than a spring layout.'],341+ origin: {342+ creator: 'Brooks, Smith, Stone, and Tutte',343+ site: 'The dissection of rectangles into squares (Duke Mathematical Journal, 1940)',344+ siteUrl: 'https://doi.org/10.1215/S0012-7094-40-00718-9',345+ note: 'Diagram.zip implements a text notation and SVG renderer for the squared-rectangle and Smith-diagram correspondence that the four Trinity students discovered.',346+ },347+ upstream: 'https://www.squaring.net/',348+ },326349 {327350 id: 'structurizr',328351 label: 'Structurizr',@@ -0,0 +1,68 @@
1+---2+id: create-squaring3+slug: /create/types/squaring4+title: Squaring5+description: Create a Squaring diagram in diagram.zip.6+sidebar_label: Squaring7+---8+9+import DiagramExample from '@site/src/components/DiagramExample';10+11+# Squaring12+13+Squaring draws a squared rectangle, which is a rectangle tiled by squares, next to its Smith diagram. In that circuit every horizontal segment is a node and every square is a unit resistor whose current equals its side.14+15+## Use this type16+17+Use Squaring to explore squared rectangles and squared squares, or to check whether a dissection is simple and perfect. It can also build a squaring backwards from a planar network once you choose a battery edge.18+19+## Source format20+21+The source format is **Squaring text**. Enter the source in the diagram.zip editor.22+23+## Syntax essentials24+25+- Optionally begin with `.view rectangle`, `.view circuit`, `.view overlay`, or `.view both`; the default is `both`.26+- Add `.labels none` to hide side lengths, voltages, and currents.27+- Add `title` followed by text to name the diagram.28+- For a known dissection, write `rectangle <width> x <height>` and then the square sides in Bouwkamp order; parentheses and commas are ignored.29+- Bouwkamp order places each square at the leftmost point of the highest unfilled segment. Squares that share a top edge are listed left to right.30+- For a network, write `battery <positive> <negative>` and then wires as `wire a b` or chains such as `a - b - c`.31+- Every wire has resistance one; the renderer solves Kirchhoff and Ohm exactly, scales the voltages so all sides are whole numbers, and arranges the squares.32+- The battery replaces one edge of the graph, so do not list that edge as a wire. A wire parallel to the battery becomes a full-height square.33+- The network form needs a planar network in which the battery closes a face; polyhedral (3-connected planar) networks give a simple squaring.34+- The caption reports the order, size, whether the squaring is simple (no smaller rectangle of squares) and perfect (no repeated side), and the battery.35+- Compound blocks are outlined; node color runs from blue at the negative pole to red at the positive pole.36+- Start comments with `#`.37+38+### Example39+40+```text41+.view both42+title Order 9 simple perfect squared rectangle43+44+rectangle 33 x 3245+squares (18 15) (7 8) (14 4) (10 1) (9)46+```47+48+<DiagramExample engine="squaring" label="Squaring" sourceUrl="/examples/squaring.json" />49+50+## Origin51+52+Squaring originates with [Brooks, Smith, Stone, and Tutte](https://doi.org/10.1215/S0012-7094-40-00718-9) at [The dissection of rectangles into squares (Duke Mathematical Journal, 1940)](https://doi.org/10.1215/S0012-7094-40-00718-9). Diagram.zip implements a text notation and SVG renderer for the squared-rectangle and Smith-diagram correspondence that the four Trinity students discovered.53+54+## Limitations55+56+- Networks are arranged by search, so very large or non-planar networks are rejected instead of drawn.57+- A wire whose two nodes settle at the same voltage carries no current and is rejected; choose another battery.58+- Node positions in the Smith diagram follow the midpoints of the horizontal segments rather than a spring layout.59+60+## Related pages61+62+- [Style Squaring](/style/types/squaring)63+- [General presentation settings](/style/presentation)64+- [Open Squaring in the editor](https://diagram.zip/?type=squaring)65+66+## Upstream reference67+68+[Squaring documentation](https://www.squaring.net/)@@ -0,0 +1,33 @@
1+---2+id: style-squaring3+slug: /style/types/squaring4+title: Squaring5+description: Style a Squaring diagram in diagram.zip.6+sidebar_label: Squaring7+---8+9+# Style Squaring10+11+Squaring tints each square by the voltage at its top edge, colors nodes from blue to red by voltage, and scales wire thickness by current. Compound blocks are outlined with a dashed line.12+13+Start with the [general presentation settings](/style/presentation). Edit shared or raw presentation in Details, and keep renderer-specific styling in Source.14+15+## Source controls16+17+- Use `.view rectangle` for the dissection alone and `.view circuit` for the Smith diagram alone.18+- Use `.view overlay` to draw the circuit on top of the squares, which shows how each segment collapses into a node.19+- Use `.labels none` for large dissections where numbers would not fit.20+- Choose a different battery wire in a network to obtain a different squaring from the same graph.21+- Reorder the square list, not the geometry, to change a Bouwkamp dissection.22+## Limitations23+24+- Networks are arranged by search, so very large or non-planar networks are rejected instead of drawn.25+- A wire whose two nodes settle at the same voltage carries no current and is rejected; choose another battery.26+- Node positions in the Smith diagram follow the midpoints of the horizontal segments rather than a spring layout.27+28+## Related pages29+30+- [Create a Squaring diagram](/create/types/squaring)31+- [General presentation settings](/style/presentation)32+- [SVG normalization and version contracts](/style/svg-normalization)33+- [Open Squaring in the editor](https://diagram.zip/?type=squaring)@@ -253,6 +253,25 @@
253253 "editor": "https://diagram.zip/?type=pikchr",254254 "example": "/examples/pikchr.json"255255 },256+ {257+ "id": "squaring",258+ "label": "Squaring",259+ "fullName": "Squared rectangles and Smith diagrams",260+ "category": "Graphs and networks",261+ "format": "Squaring text",262+ "language": "text",263+ "upstream": "https://www.squaring.net/",264+ "create": "/create/types/squaring",265+ "style": "/style/types/squaring",266+ "editor": "https://diagram.zip/?type=squaring",267+ "example": "/examples/squaring.json",268+ "origin": {269+ "creator": "Brooks, Smith, Stone, and Tutte",270+ "site": "The dissection of rectangles into squares (Duke Mathematical Journal, 1940)",271+ "siteUrl": "https://doi.org/10.1215/S0012-7094-40-00718-9",272+ "note": "Diagram.zip implements a text notation and SVG renderer for the squared-rectangle and Smith-diagram correspondence that the four Trinity students discovered."273+ }274+ },256275 {257276 "id": "structurizr",258277 "label": "Structurizr",@@ -2482,6 +2482,107 @@ Start with the [general presentation settings](/style/presentation). Edit shared
24822482 24832483 ---24842484 2485+---2486+id: create-squaring2487+slug: /create/types/squaring2488+title: Squaring2489+description: Create a Squaring diagram in diagram.zip.2490+sidebar_label: Squaring2491+---2492+2493+# Squaring2494+2495+Squaring draws a squared rectangle, which is a rectangle tiled by squares, next to its Smith diagram. In that circuit every horizontal segment is a node and every square is a unit resistor whose current equals its side.2496+2497+## Use this type2498+2499+Use Squaring to explore squared rectangles and squared squares, or to check whether a dissection is simple and perfect. It can also build a squaring backwards from a planar network once you choose a battery edge.2500+2501+## Source format2502+2503+The source format is **Squaring text**. Enter the source in the diagram.zip editor.2504+2505+## Syntax essentials2506+2507+- Optionally begin with `.view rectangle`, `.view circuit`, `.view overlay`, or `.view both`; the default is `both`.2508+- Add `.labels none` to hide side lengths, voltages, and currents.2509+- Add `title` followed by text to name the diagram.2510+- For a known dissection, write `rectangle <width> x <height>` and then the square sides in Bouwkamp order; parentheses and commas are ignored.2511+- Bouwkamp order places each square at the leftmost point of the highest unfilled segment. Squares that share a top edge are listed left to right.2512+- For a network, write `battery <positive> <negative>` and then wires as `wire a b` or chains such as `a - b - c`.2513+- Every wire has resistance one; the renderer solves Kirchhoff and Ohm exactly, scales the voltages so all sides are whole numbers, and arranges the squares.2514+- The battery replaces one edge of the graph, so do not list that edge as a wire. A wire parallel to the battery becomes a full-height square.2515+- The network form needs a planar network in which the battery closes a face; polyhedral (3-connected planar) networks give a simple squaring.2516+- The caption reports the order, size, whether the squaring is simple (no smaller rectangle of squares) and perfect (no repeated side), and the battery.2517+- Compound blocks are outlined; node color runs from blue at the negative pole to red at the positive pole.2518+- Start comments with `#`.2519+2520+### Example2521+2522+```text2523+.view both2524+title Order 9 simple perfect squared rectangle2525+2526+rectangle 33 x 322527+squares (18 15) (7 8) (14 4) (10 1) (9)2528+```2529+2530+## Origin2531+2532+Squaring originates with [Brooks, Smith, Stone, and Tutte](https://doi.org/10.1215/S0012-7094-40-00718-9) at [The dissection of rectangles into squares (Duke Mathematical Journal, 1940)](https://doi.org/10.1215/S0012-7094-40-00718-9). Diagram.zip implements a text notation and SVG renderer for the squared-rectangle and Smith-diagram correspondence that the four Trinity students discovered.2533+2534+## Limitations2535+2536+- Networks are arranged by search, so very large or non-planar networks are rejected instead of drawn.2537+- A wire whose two nodes settle at the same voltage carries no current and is rejected; choose another battery.2538+- Node positions in the Smith diagram follow the midpoints of the horizontal segments rather than a spring layout.2539+2540+## Related pages2541+2542+- [Style Squaring](/style/types/squaring)2543+- [General presentation settings](/style/presentation)2544+- [Open Squaring in the editor](https://diagram.zip/?type=squaring)2545+2546+## Upstream reference2547+2548+[Squaring documentation](https://www.squaring.net/)2549+2550+---2551+id: style-squaring2552+slug: /style/types/squaring2553+title: Squaring2554+description: Style a Squaring diagram in diagram.zip.2555+sidebar_label: Squaring2556+---2557+2558+# Style Squaring2559+2560+Squaring tints each square by the voltage at its top edge, colors nodes from blue to red by voltage, and scales wire thickness by current. Compound blocks are outlined with a dashed line.2561+2562+Start with the [general presentation settings](/style/presentation). Edit shared or raw presentation in Details, and keep renderer-specific styling in Source.2563+2564+## Source controls2565+2566+- Use `.view rectangle` for the dissection alone and `.view circuit` for the Smith diagram alone.2567+- Use `.view overlay` to draw the circuit on top of the squares, which shows how each segment collapses into a node.2568+- Use `.labels none` for large dissections where numbers would not fit.2569+- Choose a different battery wire in a network to obtain a different squaring from the same graph.2570+- Reorder the square list, not the geometry, to change a Bouwkamp dissection.2571+## Limitations2572+2573+- Networks are arranged by search, so very large or non-planar networks are rejected instead of drawn.2574+- A wire whose two nodes settle at the same voltage carries no current and is rejected; choose another battery.2575+- Node positions in the Smith diagram follow the midpoints of the horizontal segments rather than a spring layout.2576+2577+## Related pages2578+2579+- [Create a Squaring diagram](/create/types/squaring)2580+- [General presentation settings](/style/presentation)2581+- [SVG normalization and version contracts](/style/svg-normalization)2582+- [Open Squaring in the editor](https://diagram.zip/?type=squaring)2583+2584+---2585+24852586 ---24862587 id: create-structurizr24872588 slug: /create/types/structurizr@@ -85,6 +85,7 @@
8585 - [GoAT syntax](https://docs.diagram.zip/create/types/goat/): Create a GoAT diagram. [Style](https://docs.diagram.zip/style/types/goat/).8686 - [Nomnoml syntax](https://docs.diagram.zip/create/types/nomnoml/): Create a Nomnoml diagram. [Style](https://docs.diagram.zip/style/types/nomnoml/).8787 - [Pikchr syntax](https://docs.diagram.zip/create/types/pikchr/): Create a Pikchr diagram. [Style](https://docs.diagram.zip/style/types/pikchr/).88+- [Squaring syntax](https://docs.diagram.zip/create/types/squaring/): Squaring stands for Squared rectangles and Smith diagrams. Create a Squaring diagram. [Style](https://docs.diagram.zip/style/types/squaring/). Origin: [Brooks, Smith, Stone, and Tutte at The dissection of rectangles into squares (Duke Mathematical Journal, 1940)](https://doi.org/10.1215/S0012-7094-40-00718-9).8889 - [Structurizr syntax](https://docs.diagram.zip/create/types/structurizr/): Create a Structurizr diagram. [Style](https://docs.diagram.zip/style/types/structurizr/).8990 - [Svgbob syntax](https://docs.diagram.zip/create/types/svgbob/): Create a Svgbob diagram. [Style](https://docs.diagram.zip/style/types/svgbob/).9091 - [Symbolator syntax](https://docs.diagram.zip/create/types/symbolator/): Create a Symbolator diagram. [Style](https://docs.diagram.zip/style/types/symbolator/).@@ -28,6 +28,8 @@ Common choices include Mermaid for compact general diagrams, PlantUML for UML-or
2828 2929 Use Tabular Recipe Notation (`trn`) for recipes, crafting trees, and assembly transformations where ingredient quantities feed short actions and meaningful outcomes. Put `.layout combined` or `.layout individual` in the TRN source. TRN originates with [Michael Chu at Cooking for Engineers](https://www.cookingforengineers.com/); diagram.zip provides its own text DSL and SVG renderer for that tabular recipe concept. For OCR transcription, preserve the printed quantities and wording, and put uncertain readings in `#` comments instead of guessing.3030 31+Use Squaring (`squaring`) for squared rectangles, squared squares, and their Smith diagrams: the electric-circuit view in which every horizontal segment is a node and every square is a unit resistor whose current is the square's side. Write a known dissection as `rectangle <width> x <height>` followed by the square sides in Bouwkamp order, or build one backwards from a planar network with `battery <positive> <negative>` and wires such as `a - b - c`. The caption reports whether the result is simple (no smaller rectangle of squares) and perfect (no repeated side). The correspondence comes from [Brooks, Smith, Stone, and Tutte](https://doi.org/10.1215/S0012-7094-40-00718-9); diagram.zip provides its own notation and SVG renderer for it.32+3133 The complete renderer set and syntax links are in [llms.txt](https://docs.diagram.zip/llms.txt) and [diagram-types.json](https://docs.diagram.zip/diagram-types.json). Semantic routing metadata is in [diagram-skills.json](https://docs.diagram.zip/diagram-skills.json).3234 3335 Do not infer standards support from a similarly named renderer mode. Confirm the renderer's output and interchange fidelity.@@ -11,6 +11,7 @@ import nwdiag from '../../../examples/diagrams/network.diag?raw'
1111 import packetdiag from '../../../examples/diagrams/packet.diag?raw'1212 import pikchr from '../../../examples/diagrams/diamond.pikchr?raw'1313 import rackdiag from '../../../examples/diagrams/rack.diag?raw'14+import squaring from '../../../examples/diagrams/order-nine.squaring?raw'1415 import svgbob from '../../../examples/diagrams/cloud.bob?raw'1516 import symbolator from '../../../examples/diagrams/component.sv?raw'1617 import tikz from '../../../examples/diagrams/tikz.tex?raw'@@ -176,6 +177,7 @@ Rel(bob, tandem, "Completes tasks")
176177 goat,177178 nomnoml,178179 pikchr,180+ squaring,179181 svgbob,180182 symbolator,181183 tikz,@@ -2,7 +2,7 @@ const ICON_KIND_BY_TYPE = Object.freeze({
22 plantuml: 'sequence', mermaid: 'flow', seqdiag: 'sequence', actdiag: 'flow', blockdiag: 'flow', bpmn: 'flow', trn: 'flow',33 c4plantuml: 'layers', structurizr: 'layers', d2: 'graph', graphviz: 'graph', nomnoml: 'classes', umlet: 'classes',44 dbml: 'database', erd: 'database', vega: 'chart', vegalite: 'chart',5- nwdiag: 'network', packetdiag: 'packet', rackdiag: 'rack', bytefield: 'packet', wavedrom: 'wave', symbolator: 'chip', wireviz: 'cable',5+ nwdiag: 'network', packetdiag: 'packet', rackdiag: 'rack', bytefield: 'packet', wavedrom: 'wave', symbolator: 'chip', wireviz: 'cable', squaring: 'squares',66 excalidraw: 'drawing', diagramsnet: 'canvas', ditaa: 'terminal', goat: 'terminal', svgbob: 'terminal', pikchr: 'drawing', tikz: 'typeset',77 })88 @@ -23,6 +23,7 @@ const ICON_PATHS = Object.freeze({
2323 drawing: '<path d="m11 45 4-14L40 6l10 10-25 25-14 4Z"/><path d="m16 30 10 10M36 10l10 10M11 45l9-3-6-6-3 9Z"/>',2424 canvas: '<rect x="7" y="10" width="46" height="39" rx="3"/><circle cx="19" cy="23" r="5"/><path d="m10 43 13-12 8 7 8-10 11 15M43 16h4M45 14v4"/>',2525 terminal: '<rect x="6" y="10" width="48" height="40" rx="4"/><path d="m14 22 7 6-7 6M27 36h14"/>',26+ squares: '<rect x="7" y="7" width="26" height="26"/><rect x="33" y="7" width="20" height="20"/><rect x="33" y="27" width="14" height="14"/><rect x="47" y="27" width="6" height="6"/><rect x="47" y="33" width="6" height="6"/><rect x="33" y="41" width="12" height="12"/><rect x="45" y="39" width="8" height="8"/><rect x="7" y="33" width="20" height="20"/><rect x="27" y="33" width="6" height="6"/><rect x="27" y="39" width="6" height="6"/><rect x="27" y="45" width="6" height="8"/><rect x="45" y="47" width="8" height="6"/>',2627 typeset: '<path d="M13 12h34M30 12v36M21 48h18M14 21h11M35 39h11"/><path d="m42 21 5 5-5 5"/>',2728 })2829 @@ -20,7 +20,7 @@ const GROUP_DEFINITIONS = [
2020 id: 'data-charts',2121 label: 'Data & charts',2222 description: 'Data models, plots, and technical visualizations.',23- types: ['dbml', 'erd', 'vega', 'vegalite'],23+ types: ['dbml', 'erd', 'vega', 'vegalite', 'squaring'],2424 },2525 {2626 id: 'network-hardware',@@ -48,6 +48,7 @@ const DESCRIPTIONS = {
4848 nwdiag: 'Network topology diagrams.',4949 packetdiag: 'Packet and protocol layout diagrams.',5050 rackdiag: 'Rack and infrastructure diagrams.',51+ squaring: 'Squared rectangles and their Smith diagram circuits.',5152 bpmn: 'Business process model and notation diagrams.',5253 bytefield: 'Structured byte and protocol field diagrams.',5354 dbml: 'Database schemas from DBML.',@@ -77,7 +78,7 @@ const EXTENSIONS = {
7778 bytefield: ['.bf'], dbml: ['.dbml'], diagramsnet: ['.drawio'], ditaa: ['.ditaa'],7879 erd: ['.erd'], excalidraw: ['.excalidraw'], goat: ['.goat'], nomnoml: ['.nomnoml'],7980 pikchr: ['.pikchr'], structurizr: ['.dsl'], svgbob: ['.bob'], symbolator: ['.sv'],80- tikz: ['.tex'], trn: ['.trn'], umlet: ['.uxf'], vega: ['.json'], vegalite: ['.json'], wavedrom: ['.json'],81+ tikz: ['.tex'], trn: ['.trn'], squaring: ['.squaring'], umlet: ['.uxf'], vega: ['.json'], vegalite: ['.json'], wavedrom: ['.json'],8182 wireviz: ['.yaml', '.yml'],8283 }8384 @@ -11,6 +11,7 @@ export const HTTP_RENDERER_UNITS = Object.freeze({
1111 nwdiag: 'https://nwdiag.render.diagram.zip/v1/svg',1212 packetdiag: 'https://packetdiag.render.diagram.zip/v1/svg',1313 rackdiag: 'https://rackdiag.render.diagram.zip/v1/svg',14+ squaring: 'https://squaring.render.diagram.zip/v1/svg',1415 bytefield: 'https://bytefield.render.diagram.zip/v1/svg',1516 dbml: 'https://dbml.render.diagram.zip/v1/svg',1617 ditaa: 'https://ditaa.render.diagram.zip/v1/svg',@@ -29,7 +29,7 @@ test('routes every remaining known type to the generic diagram language', () =>
2929 .map(({ id }) => id)3030 .filter(type => !specialized.has(type))3131 32- assert.equal(diagramTypes.length, 31)32+ assert.equal(diagramTypes.length, 32)3333 assert.ok(genericTypes.length > 0)3434 for (const type of genericTypes) {3535 assert.equal(editorLanguageFor(type), GENERIC_LANGUAGE_ID)@@ -10,7 +10,7 @@ import {
1010 1111 test('launcher groups cover every diagram type exactly once in the source order', () => {1212 const groupedIds = launcherCatalogGroups.flatMap(group => group.items.map(item => item.id))13- assert.equal(launcherCatalog.length, 31)13+ assert.equal(launcherCatalog.length, 32)1414 assert.equal(groupedIds.length, diagramTypes.length)1515 assert.equal(new Set(groupedIds).size, groupedIds.length)1616 assert.deepEqual(new Set(groupedIds), new Set(diagramTypes.map(type => type.id)))@@ -18,7 +18,7 @@ test('launcher groups cover every diagram type exactly once in the source order'
1818 assert.deepEqual(launcherCatalogGroups.map(group => [group.label, group.items.length]), [1919 ['Flow & sequence', 7],2020 ['Architecture', 6],21- ['Data & charts', 4],21+ ['Data & charts', 5],2222 ['Network & hardware', 7],2323 ['Drawing & ASCII', 7],2424 ])@@ -4,7 +4,7 @@ import { diagramTypes } from '../src/diagram-types.js'
44 import { HTTP_RENDERER_UNITS, httpRendererUnitFor } from '../src/renderer-units.js'55 66 test('gives each HTTP renderer its own subdomain', () => {7- assert.equal(Object.keys(HTTP_RENDERER_UNITS).length, 31)7+ assert.equal(Object.keys(HTTP_RENDERER_UNITS).length, 32)88 for (const [engine, endpoint] of Object.entries(HTTP_RENDERER_UNITS)) {99 assert.equal(new URL(endpoint).hostname, `${engine}.render.diagram.zip`)1010 assert.equal(new URL(endpoint).pathname, '/v1/svg')@@ -28,6 +28,6 @@ test('does not invent an endpoint for an unknown renderer', () => {
2828 2929 test('assigns exactly one final renderer path to every diagram type', () => {3030 const http = Object.keys(HTTP_RENDERER_UNITS)31- assert.equal(http.length, 31)31+ assert.equal(http.length, 32)3232 assert.deepEqual(new Set(http), new Set(diagramTypes.map(({ id }) => id)))3333 })@@ -181,6 +181,22 @@ test('applies D2, PlantUML, Svgbob, and neutral SVG family roles', () => {
181181 }182182 })183183 184+test('normalizes squaring ink and canvas while preserving voltage colors', () => {185+ const squaring = canonicalizeSvg(186+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 40"><rect class="squaring-canvas" x="0" y="0" width="80" height="40" fill="#ffffff"></rect><g class="squaring-square" data-side="2"><rect x="4" y="4" width="20" height="20" fill="rgb(254,226,226)" stroke="#111827" stroke-width="1"></rect><text x="14" y="14" fill="#111827">2</text></g><path class="squaring-wire" d="M0 0L1 1" fill="none" stroke="#111827"></path><text class="squaring-summary" fill="#666666">Order 3</text></svg>',187+ metadata, 'squaring', 'diagramzip-squaring@1',188+ )189+190+ assert.match(squaring, /data-dz-profile="neutral-svg-semantic-2"/)191+ assert.match(squaring, /class="squaring-canvas"[^>]*data-dz-role="canvas"/)192+ assert.match(squaring, /fill="rgb\(254,226,226\)"[^>]*data-dz-stroke="line"/)193+ assert.doesNotMatch(squaring, /fill="rgb\(254,226,226\)"[^>]*data-dz-fill=/)194+ // The side label sits on a preserved tint, so it keeps its authored ink instead of a themed role.195+ assert.match(squaring, /<text x="14" y="14" fill="#111827">2<\/text>/)196+ assert.match(squaring, /class="squaring-summary"[^>]*data-dz-fill="ink-muted"/)197+ assert.match(materializeSvg(squaring, 'dark-transparent'), /data-dz-appearance="dark-transparent"/)198+})199+184200 test('normalizes TRN table structure and relationship zones', () => {185201 const trn = canonicalizeSvg(186202 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 40"><rect class="trn-canvas" width="80" height="40" fill="#fbf8dc"></rect><text class="trn-title" fill="#111827">Bread</text><rect class="trn-table-surface" y="10" width="80" height="30" fill="white"></rect><path class="trn-zone-shape trn-branch-4" fill="white" stroke="#65a268"></path><text class="trn-operation-label" fill="#111827">mix</text></svg>',@@ -0,0 +1,12 @@
1+# Build a squared rectangle backwards from a polyhedral graph.2+# This is the skeleton of the tetragonal anti-wedge: six corners, ten edges.3+# One edge becomes the battery, every other edge becomes a wire with4+# resistance one, and the current through each wire is the side of a square.5+.view both6+title Tetragonal anti-wedge as a Smith diagram7+8+battery a e9+a - b - c - d - a10+a - f - e11+b - f12+c - e - d@@ -0,0 +1,9 @@
1+# The smallest simple perfect squared rectangle: nine squares, no two alike,2+# and no smaller group of squares forming its own rectangle.3+# Squares are listed in Bouwkamp order: each one goes to the leftmost point4+# of the highest unfilled segment.5+.view both6+title Order 9 simple perfect squared rectangle7+8+rectangle 33 x 329+squares (18 15) (7 8) (14 4) (10 1) (9)@@ -11,7 +11,7 @@ This directory contains the complete Cloudflare renderer plane for diagram.zip.
1111 | `shared/` | Engine identities shared by the catalog and runtime units |1212 | `scripts/` | Cross-runtime deployment and smoke-test orchestration |1313 14-All 31 catalog engines have a final renderer path. No renderer uses the Fly.io application. No renderer can use an origin fallback.14+All 32 catalog engines have a final renderer path. No renderer uses the Fly.io application. No renderer can use an origin fallback.1515 1616 Each engine has a hostname:1717 @@ -37,7 +37,7 @@ npm --prefix edge run deploy:dry-run
3737 3838 Run `npm --prefix edge run smoke:renderers` after all units are deployed. This command checks one repository fixture or capability response for each catalog engine. It checks coverage and SVG structure. It does not compare pixels.3939 40-Run `npm --prefix edge run smoke:catalog` against a local catalog Worker. This command checks the health route, the 31-engine catalog, and the absence of the old render proxy.40+Run `npm --prefix edge run smoke:catalog` against a local catalog Worker. This command checks the health route, the 32-engine catalog, and the absence of the old render proxy.4141 4242 ## HTTP unit contract4343 @@ -79,12 +79,12 @@ Some units translate source into another engine. The pipeline header lists each
7979 8080 | Runtime | Count | Engines |8181 | --- | ---: | --- |82-| Worker JavaScript | 10 | BPMN, Bytefield, Excalidraw, Nomnoml, Structurizr, TRN, UMLet, Vega, Vega-Lite, WaveDrom |82+| Worker JavaScript | 11 | BPMN, Bytefield, Excalidraw, Nomnoml, Squaring, Structurizr, TRN, UMLet, Vega, Vega-Lite, WaveDrom |8383 | Worker Python | 8 | BlockDiag, SeqDiag, ActDiag, NwDiag, PacketDiag, RackDiag, Symbolator, WireViz |8484 | Worker WebAssembly | 11 | PlantUML, C4 PlantUML, GraphViz, DBML, ERD, D2, Ditaa, GoAT, Pikchr, Svgbob, TikZ |8585 | Browser Run | 2 | Mermaid, diagrams.net |8686 87-The total is 31 of 31 engines. The Fly.io Java rendering image is not part of this renderer plane.87+The total is 32 of 32 engines. The Fly.io Java rendering image is not part of this renderer plane.8888 8989 ## Output policy9090 @@ -16,12 +16,12 @@ if (health.ok !== true) throw new Error('The health response is invalid.')
1616 1717 if (!catalogResponse.ok) throw new Error(`Catalog check returned HTTP ${catalogResponse.status}.`)1818 const catalog = await catalogResponse.json()19-if (catalog.format !== 'svg' || !Array.isArray(catalog.engines) || catalog.engines.length !== 31) {20- throw new Error('The catalog response does not contain all 31 SVG engines.')19+if (catalog.format !== 'svg' || !Array.isArray(catalog.engines) || catalog.engines.length !== 32) {20+ throw new Error('The catalog response does not contain all 32 SVG engines.')2121 }2222 2323 if (removedRenderResponse.status !== 404) {2424 throw new Error(`The removed render proxy returned HTTP ${removedRenderResponse.status}.`)2525 }2626 27-console.log('The catalog service is healthy. It lists 31 engines and has no render proxy.')27+console.log('The catalog service is healthy. It lists 32 engines and has no render proxy.')@@ -22,6 +22,7 @@ const targetRuntime: Record<EngineId, EngineRuntime> = {
2222 nwdiag: 'edge-python',2323 packetdiag: 'edge-python',2424 rackdiag: 'edge-python',25+ squaring: 'edge-js',2526 bpmn: 'edge-js',2627 bytefield: 'edge-js',2728 dbml: 'edge-wasm',@@ -62,6 +63,7 @@ const activeRuntime: Record<EngineId, EngineRuntime> = {
6263 nwdiag: 'edge-python',6364 packetdiag: 'edge-python',6465 rackdiag: 'edge-python',66+ squaring: 'edge-js',6567 graphviz: 'edge-wasm',6668 dbml: 'edge-wasm',6769 pikchr: 'edge-wasm',@@ -96,6 +98,7 @@ const versions: Record<EngineId, string> = {
9698 nwdiag: 'nwdiag@3.0.0/python-worker-1',9799 packetdiag: 'packetdiag@3.0.0/python-worker-1',98100 rackdiag: 'rackdiag@3.0.0/python-worker-1',101+ squaring: 'diagramzip-squaring@1',99102 graphviz: 'graphviz@15.1.1/edge-wasm-2',100103 dbml: 'dbml@1.0.31+graphviz@15.1.1/edge-wasm-2',101104 pikchr: 'pikchr@85e65b9686/edge-wasm-1',@@ -213,6 +216,9 @@ const losses: Partial<Record<EngineId, readonly string[]>> = {
213216 'External files, package downloads, shell escape, hyperlinks, and external resources are unavailable.',214217 'Typography and SVG details may differ from the browser TikZJax unit.',215218 ],219+ squaring: [220+ 'Networks are arranged by search, so very large or non-planar networks are rejected instead of drawn.',221+ ],216222 trn: [217223 'The source selects one layout; the SVG does not embed an interactive layout toggle.',218224 ],@@ -4,8 +4,8 @@ import { ENGINE_IDS } from '../../shared/engines'
44 55 describe('engine catalog', () => {66 it('covers every catalog engine exactly once', () => {7- expect(ENGINE_CATALOG).toHaveLength(31)8- expect(new Set(ENGINE_CATALOG.map(engine => engine.id)).size).toBe(31)7+ expect(ENGINE_CATALOG).toHaveLength(32)8+ expect(new Set(ENGINE_CATALOG.map(engine => engine.id)).size).toBe(32)99 expect(ENGINE_CATALOG.map(engine => engine.id)).toEqual(ENGINE_IDS)1010 })1111 @@ -16,6 +16,6 @@ describe('catalog service boundary', () => {
1616 it('publishes the complete catalog', async () => {1717 const response = await worker.fetch(new Request('https://diagram.zip/render/v1/catalog'))1818 expect(response.status).toBe(200)19- await expect(response.json()).resolves.toMatchObject({ format: 'svg', engines: { length: 31 } })19+ await expect(response.json()).resolves.toMatchObject({ format: 'svg', engines: { length: 32 } })2020 })2121 })@@ -0,0 +1,10 @@
1+{2+ "$schema": "../../node_modules/wrangler/config-schema.json",3+ "name": "diagramzip-render-squaring",4+ "main": "../../src/units/squaring.ts",5+ "compatibility_date": "2026-08-20",6+ "compatibility_flags": ["nodejs_compat"],7+ "observability": { "enabled": true, "logs": { "head_sampling_rate": 1 } },8+ "vars": { "RENDERER_BUILD": "diagramzip-squaring-1" },9+ "routes": [{ "pattern": "squaring.render.diagram.zip", "custom_domain": true }]10+}@@ -6,6 +6,7 @@ import { erdAdapter } from './erd'
66 import { graphvizAdapter } from './graphviz'77 import { goatAdapter } from './goat'88 import { nomnomlAdapter } from './nomnoml'9+import { squaringAdapter } from './squaring'910 import { svgbobAdapter } from './svgbob'1011 import { trnAdapter } from './trn'1112 import { vegaAdapter } from './vega'@@ -21,6 +22,7 @@ const adapters: Record<EdgeEngineId, RendererAdapter> = {
2122 graphviz: graphvizAdapter,2223 goat: goatAdapter,2324 nomnoml: nomnomlAdapter,25+ squaring: squaringAdapter,2426 svgbob: svgbobAdapter,2527 trn: trnAdapter,2628 vega: vegaAdapter,@@ -0,0 +1,30 @@
1+import type { RendererAdapter } from '../runtime/types'2+import { RenderError } from '../runtime/errors'3+import { parseSquaring } from '../languages/squaring'4+import { buildSquaring } from '../languages/squaring-model'5+import { renderSquaring } from '../languages/squaring-renderer'6+import { edgeFailure, edgeResult } from './types'7+8+const VERSION = 'diagramzip-squaring@1'9+10+function validateOptions(options: Record<string, string>): void {11+ for (const name of Object.keys(options)) {12+ throw new RenderError(400, 'unsupported_options', `Unsupported squaring option: ${name}.`)13+ }14+}15+16+export const squaringAdapter: RendererAdapter = {17+ id: 'squaring',18+ runtime: 'edge-js',19+ version: VERSION,20+ render(request, signal) {21+ if (signal.aborted) throw signal.reason22+ try {23+ validateOptions(request.options)24+ const document = parseSquaring(request.source)25+ return Promise.resolve(edgeResult('squaring', VERSION, renderSquaring(document, buildSquaring(document))))26+ } catch (error) {27+ return edgeFailure('squaring', error)28+ }29+ },30+}@@ -1,7 +1,7 @@
11 import { RenderError } from '../runtime/errors'22 import type { EngineId, RenderResult } from '../runtime/types'33 4-export type EdgeEngineId = 'bytefield' | 'dbml' | 'ditaa' | 'erd' | 'goat' | 'graphviz' | 'nomnoml' | 'svgbob' | 'trn' | 'vega' | 'vegalite' | 'wavedrom'4+export type EdgeEngineId = 'bytefield' | 'dbml' | 'ditaa' | 'erd' | 'goat' | 'graphviz' | 'nomnoml' | 'squaring' | 'svgbob' | 'trn' | 'vega' | 'vegalite' | 'wavedrom'55 66 export function edgeResult(id: EngineId, version: string, body: string, runtime: 'edge-js' | 'edge-wasm' = 'edge-js'): RenderResult {77 if (typeof body !== 'string' || body.trim() === '') {@@ -0,0 +1,443 @@
1+import { RenderError } from '../runtime/errors'2+import type { SquaringDocument, SquaringNetworkSource, SquaringRectangleSource } from './squaring'3+4+export interface SquaringSquare {5+ index: number6+ x: number7+ y: number8+ side: number9+ top: number10+ bottom: number11+}12+13+export interface SquaringNode {14+ id: string15+ index: number16+ y: number17+ x0: number18+ x1: number19+ voltage: number20+}21+22+export interface SquaringBlock {23+ x: number24+ y: number25+ width: number26+ height: number27+ squares: number28+}29+30+export interface SquaringModel {31+ form: 'rectangle' | 'network'32+ width: number33+ height: number34+ squares: SquaringSquare[]35+ nodes: SquaringNode[]36+ positive: number37+ negative: number38+ order: number39+ perfect: boolean40+ repeatedSides: number[]41+ simple: boolean42+ blocks: SquaringBlock[]43+}44+45+const SEARCH_BUDGET = 250_00046+const MAXIMUM_ORDER = 2_00047+48+function sourceError(line: number, message: string): never {49+ throw new RenderError(422, 'invalid_source', `Line ${line}: ${message}`)50+}51+52+interface Segment {53+ x0: number54+ x1: number55+ depth: number56+ node: number57+}58+59+function mergeSkyline(segments: Segment[]): Segment[] {60+ const merged: Segment[] = []61+ for (const segment of segments) {62+ const previous = merged.at(-1)63+ if (previous !== undefined && previous.depth === segment.depth && previous.node === segment.node && previous.x1 === segment.x0) {64+ previous.x1 = segment.x165+ } else {66+ merged.push({ ...segment })67+ }68+ }69+ return merged70+}71+72+function lowestSegment(skyline: Segment[]): number {73+ let best = 074+ for (let index = 1; index < skyline.length; index += 1) {75+ if ((skyline[index]?.depth ?? Infinity) < (skyline[best]?.depth ?? Infinity)) best = index76+ }77+ return best78+}79+80+// Bouwkamp placement: every square goes to the leftmost point of the highest81+// unfilled horizontal segment, in source order.82+function placeBouwkamp(rectangle: SquaringRectangleSource): Array<{ x: number; y: number; side: number }> {83+ const { width, height, sides, line } = rectangle84+ if (sides.length > MAXIMUM_ORDER) sourceError(line, `At most ${MAXIMUM_ORDER} squares are supported.`)85+ const area = sides.reduce((sum, side) => sum + side * side, 0)86+ if (area !== width * height) {87+ sourceError(line, `The squares cover an area of ${area}, but a ${width} × ${height} rectangle has area ${width * height}.`)88+ }89+ let skyline: Segment[] = [{ x0: 0, x1: width, depth: 0, node: 0 }]90+ const placed: Array<{ x: number; y: number; side: number }> = []91+ sides.forEach((side, index) => {92+ const at = lowestSegment(skyline)93+ const segment = skyline[at]94+ if (segment === undefined) sourceError(line, 'The rectangle is already full.')95+ if (segment.x0 + side > segment.x1 || segment.depth + side > height) {96+ sourceError(line, `Square ${index + 1} (side ${side}) does not fit at column ${segment.x0}, row ${segment.depth}; only ${segment.x1 - segment.x0} × ${height - segment.depth} is free there.`)97+ }98+ placed.push({ x: segment.x0, y: segment.depth, side })99+ const replacement: Segment[] = [{ x0: segment.x0, x1: segment.x0 + side, depth: segment.depth + side, node: 0 }]100+ if (segment.x0 + side < segment.x1) replacement.push({ x0: segment.x0 + side, x1: segment.x1, depth: segment.depth, node: 0 })101+ skyline = mergeSkyline([...skyline.slice(0, at), ...replacement, ...skyline.slice(at + 1)])102+ })103+ return placed104+}105+106+// Every maximal horizontal segment of the dissection is a node of the Smith107+// diagram; its voltage is the height of the segment above the bottom edge.108+function nodesFromGeometry(squares: Array<{ x: number; y: number; side: number }>, width: number, height: number): {109+ nodes: SquaringNode[]110+ topOf: number[]111+ bottomOf: number[]112+} {113+ const intervals = new Map<number, Array<[number, number]>>()114+ const add = (y: number, x0: number, x1: number): void => {115+ const list = intervals.get(y) ?? []116+ list.push([x0, x1])117+ intervals.set(y, list)118+ }119+ add(0, 0, width)120+ add(height, 0, width)121+ for (const square of squares) {122+ add(square.y, square.x, square.x + square.side)123+ add(square.y + square.side, square.x, square.x + square.side)124+ }125+ const nodes: SquaringNode[] = []126+ for (const y of [...intervals.keys()].sort((a, b) => a - b)) {127+ const list = (intervals.get(y) ?? []).sort((a, b) => a[0] - b[0])128+ let current: [number, number] | null = null129+ const flush = (): void => {130+ if (current === null) return131+ nodes.push({ id: '', index: nodes.length, y, x0: current[0], x1: current[1], voltage: height - y })132+ current = null133+ }134+ for (const [x0, x1] of list) {135+ if (current !== null && x0 <= current[1]) current[1] = Math.max(current[1], x1)136+ else {137+ flush()138+ current = [x0, x1]139+ }140+ }141+ flush()142+ }143+ nodes.forEach((node, index) => {144+ node.id = index === 0 ? 'top' : index === nodes.length - 1 ? 'bottom' : `n${index}`145+ })146+ const find = (y: number, x: number): number => {147+ const node = nodes.find(candidate => candidate.y === y && candidate.x0 <= x && x < candidate.x1)148+ if (node === undefined) throw new RenderError(422, 'invalid_source', `No horizontal segment contains column ${x} at row ${y}.`)149+ return node.index150+ }151+ return {152+ nodes,153+ topOf: squares.map(square => find(square.y, square.x)),154+ bottomOf: squares.map(square => find(square.y + square.side, square.x)),155+ }156+}157+158+// Exact rational arithmetic keeps the Kirchhoff solution integral after scaling.159+type Fraction = { n: bigint; d: bigint }160+161+function gcd(a: bigint, b: bigint): bigint {162+ let x = a < 0n ? -a : a163+ let y = b < 0n ? -b : b164+ while (y !== 0n) [x, y] = [y, x % y]165+ return x166+}167+168+function fraction(n: bigint, d = 1n): Fraction {169+ if (d === 0n) throw new Error('Division by zero.')170+ const sign = d < 0n ? -1n : 1n171+ const divisor = gcd(n, d) || 1n172+ return { n: (sign * n) / divisor, d: (sign * d) / divisor }173+}174+175+const add = (a: Fraction, b: Fraction): Fraction => fraction(a.n * b.d + b.n * a.d, a.d * b.d)176+const sub = (a: Fraction, b: Fraction): Fraction => fraction(a.n * b.d - b.n * a.d, a.d * b.d)177+const mul = (a: Fraction, b: Fraction): Fraction => fraction(a.n * b.n, a.d * b.d)178+const div = (a: Fraction, b: Fraction): Fraction => fraction(a.n * b.d, a.d * b.n)179+180+interface SolvedNetwork {181+ voltages: number[]182+ currents: number[]183+}184+185+function solveNetwork(network: SquaringNetworkSource): SolvedNetwork {186+ const { nodes, wires, positive, negative, line } = network187+ const indexOf = new Map(nodes.map((id, index) => [id, index]))188+ const positiveIndex = indexOf.get(positive) ?? 0189+ const negativeIndex = indexOf.get(negative) ?? 0190+ const interior = nodes.map((_, index) => index).filter(index => index !== positiveIndex && index !== negativeIndex)191+ const column = new Map(interior.map((node, position) => [node, position]))192+ const size = interior.length193+194+ const matrix: Fraction[][] = interior.map(() => interior.map(() => fraction(0n)))195+ const rhs: Fraction[] = interior.map(() => fraction(0n))196+ for (const wire of wires) {197+ const a = indexOf.get(wire.from) ?? 0198+ const b = indexOf.get(wire.to) ?? 0199+ for (const [self, other] of [[a, b], [b, a]] as const) {200+ const row = column.get(self)201+ if (row === undefined) continue202+ const rowValues = matrix[row]203+ if (rowValues === undefined) continue204+ rowValues[row] = add(rowValues[row] ?? fraction(0n), fraction(1n))205+ const otherColumn = column.get(other)206+ if (otherColumn !== undefined) rowValues[otherColumn] = sub(rowValues[otherColumn] ?? fraction(0n), fraction(1n))207+ else if (other === positiveIndex) rhs[row] = add(rhs[row] ?? fraction(0n), fraction(1n))208+ }209+ }210+211+ // Gaussian elimination with exact fractions.212+ for (let pivot = 0; pivot < size; pivot += 1) {213+ let pivotRow = pivot214+ while (pivotRow < size && (matrix[pivotRow]?.[pivot]?.n ?? 0n) === 0n) pivotRow += 1215+ if (pivotRow === size) {216+ sourceError(line, 'The network is not connected: every node must have a wire path to both battery nodes.')217+ }218+ if (pivotRow !== pivot) {219+ ;[matrix[pivot], matrix[pivotRow]] = [matrix[pivotRow] as Fraction[], matrix[pivot] as Fraction[]]220+ ;[rhs[pivot], rhs[pivotRow]] = [rhs[pivotRow] as Fraction, rhs[pivot] as Fraction]221+ }222+ const pivotValues = matrix[pivot] as Fraction[]223+ const pivotValue = pivotValues[pivot] as Fraction224+ for (let row = 0; row < size; row += 1) {225+ if (row === pivot) continue226+ const rowValues = matrix[row] as Fraction[]227+ const factor = div(rowValues[pivot] as Fraction, pivotValue)228+ if (factor.n === 0n) continue229+ for (let col = pivot; col < size; col += 1) {230+ rowValues[col] = sub(rowValues[col] as Fraction, mul(factor, pivotValues[col] as Fraction))231+ }232+ rhs[row] = sub(rhs[row] as Fraction, mul(factor, rhs[pivot] as Fraction))233+ }234+ }235+236+ const solution: Fraction[] = nodes.map(() => fraction(0n))237+ solution[positiveIndex] = fraction(1n)238+ interior.forEach((node, position) => {239+ solution[node] = div(rhs[position] as Fraction, (matrix[position] as Fraction[])[position] as Fraction)240+ })241+242+ let scale = 1n243+ for (const value of solution) scale = (scale * value.d) / gcd(scale, value.d)244+ const integral = solution.map(value => (value.n * scale) / value.d)245+ const currentValues = wires.map(wire => (integral[indexOf.get(wire.from) ?? 0] ?? 0n) - (integral[indexOf.get(wire.to) ?? 0] ?? 0n))246+ let reduction = 0n247+ for (const current of currentValues) reduction = gcd(reduction, current)248+ if (reduction === 0n) sourceError(line, 'No current flows through the network.')249+ const toNumber = (value: bigint): number => {250+ const reduced = value / reduction251+ if (reduced > BigInt(Number.MAX_SAFE_INTEGER) || reduced < -BigInt(Number.MAX_SAFE_INTEGER)) {252+ sourceError(line, 'The network produces square sides that are too large to draw.')253+ }254+ return Number(reduced)255+ }256+ return { voltages: integral.map(toNumber), currents: currentValues.map(toNumber) }257+}258+259+interface OrientedWire {260+ index: number261+ from: number262+ to: number263+ side: number264+}265+266+function distinctPermutations<T>(items: T[], key: (item: T) => string): T[][] {267+ const sorted = [...items].sort((a, b) => key(a).localeCompare(key(b)))268+ const results: T[][] = []269+ const used = sorted.map(() => false)270+ const current: T[] = []271+ const walk = (): void => {272+ if (current.length === sorted.length) {273+ results.push([...current])274+ return275+ }276+ let previous: string | null = null277+ sorted.forEach((item, index) => {278+ if (used[index]) return279+ const itemKey = key(item)280+ if (itemKey === previous) return281+ previous = itemKey282+ used[index] = true283+ current.push(item)284+ walk()285+ current.pop()286+ used[index] = false287+ })288+ }289+ walk()290+ return results291+}292+293+// Rebuild the dissection from the circuit: fill the rectangle top-down, and at294+// every horizontal segment try each left-to-right order of the squares that295+// hang from it. Polyhedral networks admit one arrangement up to mirroring.296+function arrangeNetwork(network: SquaringNetworkSource, solved: SolvedNetwork): { squares: SquaringSquare[]; nodes: SquaringNode[]; positive: number; negative: number } {297+ const { nodes, wires, positive, negative, line } = network298+ const indexOf = new Map(nodes.map((id, index) => [id, index]))299+ const positiveIndex = indexOf.get(positive) ?? 0300+ const negativeIndex = indexOf.get(negative) ?? 0301+ const oriented: OrientedWire[] = wires.map((wire, index) => {302+ const a = indexOf.get(wire.from) ?? 0303+ const b = indexOf.get(wire.to) ?? 0304+ const current = solved.currents[index] ?? 0305+ if (current === 0) {306+ sourceError(wire.line, `Wire ${wire.from} - ${wire.to} carries no current because both nodes settle at the same voltage, so it cannot become a square. Choose a different battery.`)307+ }308+ return current > 0 ? { index, from: a, to: b, side: current } : { index, from: b, to: a, side: -current }309+ })310+ const outgoing = nodes.map<OrientedWire[]>(() => [])311+ const inflow = nodes.map(() => 0)312+ for (const wire of oriented) {313+ outgoing[wire.from]?.push(wire)314+ inflow[wire.to] = (inflow[wire.to] ?? 0) + wire.side315+ }316+ const width = outgoing[positiveIndex]?.reduce((sum, wire) => sum + wire.side, 0) ?? 0317+ const height = solved.voltages[positiveIndex] ?? 0318+ const placement = new Map<number, { x: number; y: number }>()319+ let budget = SEARCH_BUDGET320+321+ const search = (skyline: Segment[]): boolean => {322+ budget -= 1323+ if (budget < 0) sourceError(line, 'The network has too many possible arrangements to explore. Choose a battery on the outer face of a polyhedral network.')324+ const at = lowestSegment(skyline)325+ const segment = skyline[at]326+ if (segment === undefined) return false327+ if (segment.depth >= height) return true328+ const node = segment.node329+ if (node === negativeIndex) return false330+ let start = at331+ while (start > 0 && skyline[start - 1]?.depth === segment.depth && skyline[start - 1]?.node === node) start -= 1332+ let end = at333+ while (end + 1 < skyline.length && skyline[end + 1]?.depth === segment.depth && skyline[end + 1]?.node === node) end += 1334+ const x0 = skyline[start]?.x0 ?? 0335+ const x1 = skyline[end]?.x1 ?? 0336+ const hanging = outgoing[node] ?? []337+ const outflow = hanging.reduce((sum, wire) => sum + wire.side, 0)338+ if (x1 - x0 !== outflow) return false339+ for (const order of distinctPermutations(hanging, wire => `${wire.side}:${wire.to}`)) {340+ const replacement: Segment[] = []341+ let x = x0342+ for (const wire of order) {343+ placement.set(wire.index, { x, y: segment.depth })344+ replacement.push({ x0: x, x1: x + wire.side, depth: segment.depth + wire.side, node: wire.to })345+ x += wire.side346+ }347+ if (search(mergeSkyline([...skyline.slice(0, start), ...replacement, ...skyline.slice(end + 1)]))) return true348+ for (const wire of order) placement.delete(wire.index)349+ }350+ return false351+ }352+353+ if (!search([{ x0: 0, x1: width, depth: 0, node: positiveIndex }])) {354+ sourceError(line, `The network cannot be arranged into a squared rectangle with the battery between ${positive} and ${negative}. Check that the wires form a planar network and try another battery.`)355+ }356+357+ const squares: SquaringSquare[] = oriented.map(wire => {358+ const at = placement.get(wire.index) ?? { x: 0, y: 0 }359+ return { index: wire.index, x: at.x, y: at.y, side: wire.side, top: wire.from, bottom: wire.to }360+ })361+ const x0 = nodes.map(() => Infinity)362+ for (const square of squares) x0[square.bottom] = Math.min(x0[square.bottom] ?? Infinity, square.x)363+ x0[positiveIndex] = 0364+ const modelNodes: SquaringNode[] = nodes.map((id, index) => {365+ const voltage = solved.voltages[index] ?? 0366+ const left = x0[index] === Infinity ? 0 : (x0[index] ?? 0)367+ const span = index === positiveIndex ? width : inflow[index] ?? 0368+ return { id, index, y: height - voltage, x0: left, x1: left + span, voltage }369+ })370+ return { squares, nodes: modelNodes, positive: positiveIndex, negative: negativeIndex }371+}372+373+// A compound block is a proper sub-rectangle tiled by at least two squares.374+function findBlocks(squares: SquaringSquare[], width: number, height: number): SquaringBlock[] {375+ const xs = [...new Set(squares.flatMap(square => [square.x, square.x + square.side]))].sort((a, b) => a - b)376+ const ys = [...new Set(squares.flatMap(square => [square.y, square.y + square.side]))].sort((a, b) => a - b)377+ const blocks: SquaringBlock[] = []378+ for (let left = 0; left < xs.length; left += 1) {379+ const x0 = xs[left] ?? 0380+ for (let right = left + 1; right < xs.length; right += 1) {381+ const x1 = xs[right] ?? 0382+ const overlapping = squares.filter(square => square.x < x1 && square.x + square.side > x0)383+ const straddlers = overlapping.filter(square => square.x < x0 || square.x + square.side > x1)384+ const inside = overlapping.filter(square => square.x >= x0 && square.x + square.side <= x1)385+ const clean = ys.filter(y => !overlapping.some(square => square.y < y && square.y + square.side > y))386+ for (const y0 of clean) {387+ let limit = height388+ for (const square of straddlers) {389+ if (square.y + square.side > y0) limit = Math.min(limit, square.y)390+ }391+ for (const y1 of clean) {392+ if (y1 <= y0) continue393+ if (y1 > limit) break394+ if (x0 === 0 && x1 === width && y0 === 0 && y1 === height) continue395+ const count = inside.filter(square => square.y >= y0 && square.y + square.side <= y1).length396+ if (count >= 2) blocks.push({ x: x0, y: y0, width: x1 - x0, height: y1 - y0, squares: count })397+ }398+ }399+ }400+ }401+ const contains = (outer: SquaringBlock, inner: SquaringBlock): boolean =>402+ outer !== inner && outer.x <= inner.x && outer.y <= inner.y && outer.x + outer.width >= inner.x + inner.width && outer.y + outer.height >= inner.y + inner.height403+ return blocks.filter(block => !blocks.some(other => contains(other, block)))404+}405+406+function analyse(form: 'rectangle' | 'network', width: number, height: number, squares: SquaringSquare[], nodes: SquaringNode[], positive: number, negative: number): SquaringModel {407+ const counts = new Map<number, number>()408+ for (const square of squares) counts.set(square.side, (counts.get(square.side) ?? 0) + 1)409+ const repeatedSides = [...counts.entries()].filter(([, count]) => count > 1).map(([side]) => side).sort((a, b) => a - b)410+ const blocks = findBlocks(squares, width, height)411+ return {412+ form,413+ width,414+ height,415+ squares,416+ nodes,417+ positive,418+ negative,419+ order: squares.length,420+ perfect: repeatedSides.length === 0,421+ repeatedSides,422+ simple: blocks.length === 0,423+ blocks,424+ }425+}426+427+export function buildSquaring(document: SquaringDocument): SquaringModel {428+ if (document.rectangle !== null) {429+ const rectangle = document.rectangle430+ const placed = placeBouwkamp(rectangle)431+ const { nodes, topOf, bottomOf } = nodesFromGeometry(placed, rectangle.width, rectangle.height)432+ const squares = placed.map((square, index) => ({ index, ...square, top: topOf[index] ?? 0, bottom: bottomOf[index] ?? 0 }))433+ return analyse('rectangle', rectangle.width, rectangle.height, squares, nodes, 0, nodes.length - 1)434+ }435+ if (document.network === null) throw new RenderError(422, 'invalid_source', 'The squaring source is empty.')436+ const network = document.network437+ if (network.wires.length > MAXIMUM_ORDER) sourceError(network.line, `At most ${MAXIMUM_ORDER} wires are supported.`)438+ const solved = solveNetwork(network)439+ const arranged = arrangeNetwork(network, solved)440+ const width = arranged.nodes[arranged.positive]?.x1 ?? 0441+ const height = arranged.nodes[arranged.positive]?.voltage ?? 0442+ return analyse('network', width, height, arranged.squares, arranged.nodes, arranged.positive, arranged.negative)443+}@@ -0,0 +1,216 @@
1+import type { SquaringDocument } from './squaring'2+import type { SquaringModel, SquaringNode, SquaringSquare } from './squaring-model'3+4+const MARGIN = 165+const PANEL_GAP = 446+const PANEL_TITLE_HEIGHT = 267+const CAPTION_HEIGHT = 448+const BATTERY_GUTTER = 409+const TARGET_SIZE = 48010+const FONT_FAMILY = 'Arial,Helvetica,sans-serif'11+const INK = '#111827'12+const MUTED = '#666666'13+const WIRE = '#111827'14+const OVERLAY_WIRE = '#1e293b'15+const BLOCK = '#db2777'16+17+interface Panel {18+ x: number19+ y: number20+ width: number21+ height: number22+}23+24+function escapeXml(value: string): string {25+ return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"')26+}27+28+function round(value: number): string {29+ return String(Math.round(value * 100) / 100)30+}31+32+function mix(a: [number, number, number], b: [number, number, number], t: number): string {33+ const channel = (index: 0 | 1 | 2): number => Math.round(a[index] + (b[index] - a[index]) * t)34+ return `rgb(${channel(0)},${channel(1)},${channel(2)})`35+}36+37+// Voltage runs from the negative pole (blue) through neutral to the positive pole (red).38+function voltageColor(t: number, tint: boolean): string {39+ const clamped = Math.min(1, Math.max(0, t))40+ const blue: [number, number, number] = tint ? [219, 234, 254] : [37, 99, 235]41+ const neutral: [number, number, number] = tint ? [241, 245, 249] : [148, 163, 184]42+ const red: [number, number, number] = tint ? [254, 226, 226] : [220, 38, 38]43+ return clamped < 0.5 ? mix(blue, neutral, clamped * 2) : mix(neutral, red, (clamped - 0.5) * 2)44+}45+46+function squareSvg(square: SquaringSquare, model: SquaringModel, panel: Panel, unit: number, labels: boolean, overlay: boolean): string {47+ const x = panel.x + square.x * unit48+ const y = panel.y + square.y * unit49+ const size = square.side * unit50+ const topVoltage = model.nodes[square.top]?.voltage ?? model.height51+ const fill = voltageColor((topVoltage - square.side / 2) / model.height, true)52+ const fontSize = Math.min(16, size * 0.42)53+ // In the overlay the wire runs down the middle of the square, so the label moves beside it.54+ const labelX = overlay ? x + size / 2 + 6 : x + size / 255+ const label = labels && size >= 1056+ ? `<text x="${round(labelX)}" y="${round(y + size / 2)}" text-anchor="${overlay ? 'start' : 'middle'}" dominant-baseline="central" fill="${INK}" font-size="${round(fontSize)}" font-family="${FONT_FAMILY}">${square.side}</text>`57+ : ''58+ return `<g class="squaring-square" data-index="${square.index}" data-side="${square.side}" data-x="${square.x}" data-y="${square.y}"><title>Square ${square.side} at (${square.x}, ${square.y})</title><rect x="${round(x)}" y="${round(y)}" width="${round(size)}" height="${round(size)}" fill="${fill}" stroke="${INK}" stroke-width="1"/>${label}</g>`59+}60+61+function blocksSvg(model: SquaringModel, panel: Panel, unit: number): string {62+ return model.blocks.map(block => {63+ const inset = 264+ return `<rect class="squaring-block" data-squares="${block.squares}" x="${round(panel.x + block.x * unit + inset)}" y="${round(panel.y + block.y * unit + inset)}" width="${round(block.width * unit - inset * 2)}" height="${round(block.height * unit - inset * 2)}" fill="none" stroke="${BLOCK}" stroke-width="2.5" stroke-dasharray="7 4"><title>Compound block ${block.width} × ${block.height} of ${block.squares} squares</title></rect>`65+ }).join('')66+}67+68+function nodeCenter(node: SquaringNode, panel: Panel, unit: number): { x: number; y: number } {69+ return { x: panel.x + ((node.x0 + node.x1) / 2) * unit, y: panel.y + node.y * unit }70+}71+72+function wireSvg(square: SquaringSquare, model: SquaringModel, panel: Panel, unit: number, maxSide: number, showCurrent: boolean, overlay: boolean): string {73+ const stroke = overlay ? OVERLAY_WIRE : WIRE74+ const top = model.nodes[square.top]75+ const bottom = model.nodes[square.bottom]76+ if (top === undefined || bottom === undefined) return ''77+ const a = nodeCenter(top, panel, unit)78+ const b = nodeCenter(bottom, panel, unit)79+ const cx = panel.x + (square.x + square.side / 2) * unit80+ const size = square.side * unit81+ const radius = Math.max(0, Math.min(8, size / 3, Math.abs(cx - a.x) / 2, Math.abs(cx - b.x) / 2))82+ const width = 1 + 2.4 * (square.side / maxSide)83+ const towardsA = a.x < cx ? -1 : 184+ const towardsB = b.x < cx ? -1 : 185+ const path = [86+ `M${round(a.x)} ${round(a.y)}`,87+ `L${round(cx + towardsA * radius)} ${round(a.y)}`,88+ `Q${round(cx)} ${round(a.y)} ${round(cx)} ${round(a.y + radius)}`,89+ `L${round(cx)} ${round(b.y - radius)}`,90+ `Q${round(cx)} ${round(b.y)} ${round(cx + towardsB * radius)} ${round(b.y)}`,91+ `L${round(b.x)} ${round(b.y)}`,92+ ].join(' ')93+ const midY = (a.y + b.y) / 294+ const arrow = size >= 1495+ ? `<path d="M${round(cx - 3.5)} ${round(midY - 3)} L${round(cx + 3.5)} ${round(midY - 3)} L${round(cx)} ${round(midY + 3.5)} Z" fill="${stroke}"/>`96+ : ''97+ const label = showCurrent && size >= 1498+ ? `<text x="${round(cx + 6)}" y="${round(midY - 7)}" text-anchor="start" dominant-baseline="central" fill="${MUTED}" font-size="10" font-family="${FONT_FAMILY}">${square.side}</text>`99+ : ''100+ return `<g class="squaring-wire" data-from="${escapeXml(top.id)}" data-to="${escapeXml(bottom.id)}" data-current="${square.side}"><title>Wire ${escapeXml(top.id)} → ${escapeXml(bottom.id)} carries ${square.side}</title><path d="${path}" fill="none" stroke="${stroke}" stroke-width="${round(width)}" stroke-linecap="round" stroke-linejoin="round"/>${arrow}${label}</g>`101+}102+103+function nodeSvg(node: SquaringNode, model: SquaringModel, panel: Panel, unit: number, rung: boolean, showVoltage: boolean): string {104+ const center = nodeCenter(node, panel, unit)105+ const color = voltageColor(node.voltage / model.height, false)106+ const rungLine = rung107+ ? `<line x1="${round(panel.x + node.x0 * unit)}" y1="${round(center.y)}" x2="${round(panel.x + node.x1 * unit)}" y2="${round(center.y)}" stroke="${color}" stroke-width="3" stroke-linecap="round"/>`108+ : ''109+ const pole = node.index === model.positive ? '+' : node.index === model.negative ? '−' : ''110+ // Wires leave every node horizontally, so labels sit above or below the rung instead of on it.111+ const labelX = pole === '' ? center.x + 7 : center.x112+ const labelY = node.index === model.negative ? center.y + 13 : center.y - 10113+ const anchor = pole === '' ? 'start' : 'middle'114+ const label = showVoltage115+ ? `<text x="${round(labelX)}" y="${round(labelY)}" text-anchor="${anchor}" dominant-baseline="central" fill="${INK}" font-size="11" font-weight="700" font-family="${FONT_FAMILY}">${pole === '' ? '' : `${pole} `}${node.voltage}</text>`116+ : ''117+ return `<g class="squaring-node" data-node-id="${escapeXml(node.id)}" data-voltage="${node.voltage}"><title>Node ${escapeXml(node.id)} at ${node.voltage}</title>${rungLine}<circle cx="${round(center.x)}" cy="${round(center.y)}" r="5" fill="${color}" stroke="white" stroke-width="1.5"/>${label}</g>`118+}119+120+function batterySvg(model: SquaringModel, panel: Panel, unit: number): string {121+ const positive = model.nodes[model.positive]122+ const negative = model.nodes[model.negative]123+ if (positive === undefined || negative === undefined) return ''124+ const a = nodeCenter(positive, panel, unit)125+ const b = nodeCenter(negative, panel, unit)126+ const gx = panel.x - BATTERY_GUTTER / 2 - 4127+ const midY = (a.y + b.y) / 2128+ return `<g class="squaring-battery"><title>Battery: ${escapeXml(positive.id)} is the positive pole at ${positive.voltage}, ${escapeXml(negative.id)} is the negative pole at 0</title>` +129+ `<path d="M${round(a.x)} ${round(a.y)} L${round(gx)} ${round(a.y)} L${round(gx)} ${round(midY - 7)} M${round(gx)} ${round(midY + 7)} L${round(gx)} ${round(b.y)} L${round(b.x)} ${round(b.y)}" fill="none" stroke="${WIRE}" stroke-width="1.5" stroke-dasharray="4 3"/>` +130+ `<line x1="${round(gx - 9)}" y1="${round(midY - 4)}" x2="${round(gx + 9)}" y2="${round(midY - 4)}" stroke="${WIRE}" stroke-width="2.5"/>` +131+ `<line x1="${round(gx - 4)}" y1="${round(midY + 4)}" x2="${round(gx + 4)}" y2="${round(midY + 4)}" stroke="${WIRE}" stroke-width="2.5"/>` +132+ `<text x="${round(gx - 12)}" y="${round(midY - 8)}" text-anchor="end" dominant-baseline="central" fill="${INK}" font-size="12" font-weight="700" font-family="${FONT_FAMILY}">+</text>` +133+ `<text x="${round(gx - 12)}" y="${round(midY + 9)}" text-anchor="end" dominant-baseline="central" fill="${INK}" font-size="12" font-weight="700" font-family="${FONT_FAMILY}">−</text>` +134+ `</g>`135+}136+137+function panelTitle(text: string, panel: Panel): string {138+ return `<text class="squaring-panel-title" x="${round(panel.x)}" y="${round(panel.y - 9)}" fill="${MUTED}" font-size="12" font-weight="700" font-family="${FONT_FAMILY}">${escapeXml(text)}</text>`139+}140+141+function frame(panel: Panel): string {142+ return `<rect x="${round(panel.x)}" y="${round(panel.y)}" width="${round(panel.width)}" height="${round(panel.height)}" fill="none" stroke="${INK}" stroke-width="1.5"/>`143+}144+145+function rectanglePanel(model: SquaringModel, panel: Panel, unit: number, labels: boolean, overlay = false): string {146+ const squares = model.squares.map(square => squareSvg(square, model, panel, unit, labels, overlay)).join('')147+ return `<g class="squaring-rectangle">${squares}${blocksSvg(model, panel, unit)}${frame(panel)}</g>`148+}149+150+function circuitPanel(model: SquaringModel, panel: Panel, unit: number, overlay: boolean, labels: boolean): string {151+ const maxSide = Math.max(...model.squares.map(square => square.side))152+ const wires = model.squares.map(square => wireSvg(square, model, panel, unit, maxSide, labels && !overlay, overlay)).join('')153+ const nodes = model.nodes.map(node => nodeSvg(node, model, panel, unit, overlay, labels && !overlay)).join('')154+ return `<g class="squaring-circuit" data-overlay="${overlay}">${batterySvg(model, panel, unit)}${wires}${nodes}</g>`155+}156+157+function defaultTitle(document: SquaringDocument, model: SquaringModel): string {158+ if (document.title !== '') return document.title159+ return model.width === model.height ? `Squared square ${model.width} × ${model.height}` : `Squared rectangle ${model.width} × ${model.height}`160+}161+162+export function describeSquaring(model: SquaringModel): string {163+ const parts = [`Order ${model.order}`, `${model.width} × ${model.height}`]164+ parts.push(model.simple ? 'simple' : `compound (${model.blocks.length} ${model.blocks.length === 1 ? 'block' : 'blocks'} outlined)`)165+ parts.push(model.perfect ? 'perfect' : `imperfect (${model.repeatedSides.length} repeated ${model.repeatedSides.length === 1 ? 'side' : 'sides'}: ${model.repeatedSides.join(', ')})`)166+ if (model.form === 'network') {167+ const positive = model.nodes[model.positive]?.id ?? ''168+ const negative = model.nodes[model.negative]?.id ?? ''169+ parts.push(`battery ${positive} → ${negative}`)170+ }171+ return parts.join(' · ')172+}173+174+export function renderSquaring(document: SquaringDocument, model: SquaringModel): string {175+ const unit = Math.min(60, Math.max(0.5, TARGET_SIZE / Math.max(model.width, model.height)))176+ const labels = document.labels === 'sides'177+ const view = document.view178+ const panelWidth = model.width * unit179+ const panelHeight = model.height * unit180+ const top = MARGIN + PANEL_TITLE_HEIGHT181+ const panels: string[] = []182+ let x = MARGIN183+184+ if (view === 'rectangle' || view === 'both') {185+ const panel = { x, y: top, width: panelWidth, height: panelHeight }186+ panels.push(panelTitle('Squared rectangle', panel), rectanglePanel(model, panel, unit, labels))187+ x += panelWidth + PANEL_GAP188+ }189+ if (view === 'circuit' || view === 'both') {190+ x += BATTERY_GUTTER191+ const panel = { x, y: top, width: panelWidth, height: panelHeight }192+ panels.push(panelTitle('Smith diagram', panel), circuitPanel(model, panel, unit, false, labels))193+ x += panelWidth194+ }195+ if (view === 'overlay') {196+ x += BATTERY_GUTTER197+ const panel = { x, y: top, width: panelWidth, height: panelHeight }198+ panels.push(panelTitle('Smith diagram over the squared rectangle', panel), rectanglePanel(model, panel, unit, labels, true), circuitPanel(model, panel, unit, true, labels))199+ x += panelWidth200+ }201+202+ const width = Math.max(x + MARGIN, 320)203+ const height = top + panelHeight + CAPTION_HEIGHT + MARGIN204+ const title = defaultTitle(document, model)205+ const summary = describeSquaring(model)206+ const captionY = top + panelHeight + 20207+208+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${round(width)} ${round(height)}" width="${round(width)}" height="${round(height)}" class="squaring-diagram" data-view="${view}" data-form="${model.form}" data-order="${model.order}" data-width="${model.width}" data-height="${model.height}" data-simple="${model.simple}" data-perfect="${model.perfect}">209+ <title>${escapeXml(title)}</title>210+ <desc>${escapeXml(summary)}. Each square is a wire carrying current equal to its side; each horizontal segment is a node whose voltage is its height above the bottom edge.</desc>211+ <rect class="squaring-canvas" x="0" y="0" width="${round(width)}" height="${round(height)}" fill="#ffffff"/>212+ ${panels.join('\n ')}213+ <text class="squaring-title" x="${MARGIN}" y="${round(captionY)}" fill="${INK}" font-size="14" font-weight="700" font-family="${FONT_FAMILY}">${escapeXml(title)}</text>214+ <text class="squaring-summary" x="${MARGIN}" y="${round(captionY + 17)}" fill="${MUTED}" font-size="12" font-family="${FONT_FAMILY}">${escapeXml(summary)}</text>215+</svg>`216+}@@ -0,0 +1,201 @@
1+import { RenderError } from '../runtime/errors'2+3+export type SquaringView = 'rectangle' | 'circuit' | 'overlay' | 'both'4+export type SquaringLabels = 'sides' | 'none'5+6+export interface SquaringWire {7+ from: string8+ to: string9+ line: number10+}11+12+export interface SquaringRectangleSource {13+ width: number14+ height: number15+ sides: number[]16+ line: number17+}18+19+export interface SquaringNetworkSource {20+ positive: string21+ negative: string22+ nodes: string[]23+ wires: SquaringWire[]24+ line: number25+}26+27+export interface SquaringDocument {28+ view: SquaringView29+ labels: SquaringLabels30+ title: string31+ rectangle: SquaringRectangleSource | null32+ network: SquaringNetworkSource | null33+}34+35+const IDENTIFIER = /^[A-Za-z0-9_]+$/36+const CHAIN = /^[A-Za-z0-9_]+(?:\s*-+\s*[A-Za-z0-9_]+)+$/37+const RECTANGLE = /^rectangle\s+(\d+)\s*(?:x|×|by)\s*(\d+)$/i38+const NUMBER_LIST = /^[\d\s(),]+$/39+const VIEWS: readonly SquaringView[] = ['rectangle', 'circuit', 'overlay', 'both']40+const LABELS: readonly SquaringLabels[] = ['sides', 'none']41+42+function sourceError(line: number, message: string): never {43+ throw new RenderError(422, 'invalid_source', `Line ${line}: ${message}`)44+}45+46+function stripComment(raw: string): string {47+ let quoted = false48+ for (let index = 0; index < raw.length; index += 1) {49+ const character = raw[index]50+ if (character === '"') quoted = !quoted51+ else if (character === '#' && !quoted) return raw.slice(0, index)52+ }53+ return raw54+}55+56+function identifier(value: string, line: number, role: string): string {57+ if (!IDENTIFIER.test(value)) sourceError(line, `${role} "${value}" must use letters, digits, or underscores.`)58+ return value59+}60+61+function positiveInteger(value: string, line: number, role: string): number {62+ const number = Number(value)63+ if (!Number.isSafeInteger(number) || number <= 0) sourceError(line, `${role} must be a positive whole number, not "${value}".`)64+ return number65+}66+67+function parseSides(text: string, line: number): number[] {68+ const tokens = text.replaceAll(/[(),]/g, ' ').trim().split(/\s+/).filter(token => token !== '')69+ return tokens.map(token => positiveInteger(token, line, 'Each square side'))70+}71+72+export function parseSquaring(source: string): SquaringDocument {73+ let view: SquaringView | null = null74+ let labels: SquaringLabels | null = null75+ let title = ''76+ let rectangle: SquaringRectangleSource | null = null77+ let battery: { positive: string; negative: string; line: number } | null = null78+ const wires: SquaringWire[] = []79+ const nodes: string[] = []80+ let firstNetworkLine = 081+82+ const rememberNode = (id: string): void => {83+ if (!nodes.includes(id)) nodes.push(id)84+ }85+ const addWire = (from: string, to: string, line: number): void => {86+ if (from === to) sourceError(line, `A wire cannot connect node "${from}" to itself.`)87+ if (rectangle !== null) sourceError(line, 'A squaring is either a rectangle with squares or a network with a battery, not both.')88+ if (firstNetworkLine === 0) firstNetworkLine = line89+ rememberNode(from)90+ rememberNode(to)91+ wires.push({ from, to, line })92+ }93+94+ const lines = source.split(/\r?\n/)95+ for (let index = 0; index < lines.length; index += 1) {96+ const line = index + 197+ const text = stripComment(lines[index] ?? '').trim()98+ if (text === '') continue99+100+ if (text.startsWith('.')) {101+ const [directive = '', value = '', ...rest] = text.slice(1).split(/\s+/)102+ if (rest.length > 0) sourceError(line, `The .${directive} directive takes one value.`)103+ if (directive === 'view') {104+ if (view !== null) sourceError(line, 'The .view directive may appear only once.')105+ if (!VIEWS.includes(value as SquaringView)) sourceError(line, `Unknown view "${value}". Use rectangle, circuit, overlay, or both.`)106+ view = value as SquaringView107+ } else if (directive === 'labels') {108+ if (labels !== null) sourceError(line, 'The .labels directive may appear only once.')109+ if (!LABELS.includes(value as SquaringLabels)) sourceError(line, `Unknown labels value "${value}". Use sides or none.`)110+ labels = value as SquaringLabels111+ } else {112+ sourceError(line, `Unknown directive ".${directive}". Use .view or .labels.`)113+ }114+ continue115+ }116+117+ const [keyword = '', ...restTokens] = text.split(/\s+/)118+ const rest = text.slice(keyword.length).trim()119+120+ if (keyword === 'title') {121+ if (rest === '') sourceError(line, 'A title needs text after the keyword.')122+ title = rest.startsWith('"') && rest.endsWith('"') && rest.length >= 2 ? rest.slice(1, -1) : rest123+ continue124+ }125+126+ const rectangleMatch = RECTANGLE.exec(text)127+ if (rectangleMatch !== null) {128+ if (rectangle !== null) sourceError(line, 'Only one rectangle may be declared.')129+ if (wires.length > 0 || battery !== null) sourceError(line, 'A squaring is either a rectangle with squares or a network with a battery, not both.')130+ rectangle = {131+ width: positiveInteger(rectangleMatch[1] ?? '', line, 'The rectangle width'),132+ height: positiveInteger(rectangleMatch[2] ?? '', line, 'The rectangle height'),133+ sides: [],134+ line,135+ }136+ continue137+ }138+139+ if (keyword === 'squares' || NUMBER_LIST.test(text)) {140+ if (rectangle === null) sourceError(line, 'Declare the rectangle with "rectangle <width> x <height>" before listing squares.')141+ const sides = parseSides(keyword === 'squares' ? rest : text, line)142+ if (sides.length === 0) sourceError(line, 'List at least one square side.')143+ rectangle.sides.push(...sides)144+ continue145+ }146+147+ if (keyword === 'battery') {148+ if (battery !== null) sourceError(line, 'Only one battery may be declared.')149+ if (rectangle !== null) sourceError(line, 'A squaring is either a rectangle with squares or a network with a battery, not both.')150+ if (restTokens.length !== 2) sourceError(line, 'Write the battery as "battery <positive node> <negative node>".')151+ const positive = identifier(restTokens[0] ?? '', line, 'The positive node')152+ const negative = identifier(restTokens[1] ?? '', line, 'The negative node')153+ if (positive === negative) sourceError(line, 'The battery needs two different nodes.')154+ if (firstNetworkLine === 0) firstNetworkLine = line155+ rememberNode(positive)156+ rememberNode(negative)157+ battery = { positive, negative, line }158+ continue159+ }160+161+ if (keyword === 'wire') {162+ if (restTokens.length !== 2) sourceError(line, 'Write a wire as "wire <node> <node>" or "<node> - <node>".')163+ addWire(identifier(restTokens[0] ?? '', line, 'A node'), identifier(restTokens[1] ?? '', line, 'A node'), line)164+ continue165+ }166+167+ if (CHAIN.test(text)) {168+ const chain = text.split(/\s*-+\s*/).map(node => identifier(node, line, 'A node'))169+ for (let position = 1; position < chain.length; position += 1) {170+ addWire(chain[position - 1] ?? '', chain[position] ?? '', line)171+ }172+ continue173+ }174+175+ sourceError(line, `Unrecognized line "${text}". Expected a rectangle, squares, battery, wire, title, or directive.`)176+ }177+178+ if (rectangle === null && battery === null && wires.length === 0) {179+ sourceError(1, 'Declare a rectangle with its squares, or a network with a battery and wires.')180+ }181+182+ if (rectangle !== null) {183+ if (rectangle.sides.length < 2) sourceError(rectangle.line, 'A squared rectangle needs at least two squares.')184+ return { view: view ?? 'both', labels: labels ?? 'sides', title, rectangle, network: null }185+ }186+187+ if (battery === null) sourceError(firstNetworkLine, 'A network needs a battery: "battery <positive node> <negative node>".')188+ if (wires.length === 0) sourceError(battery.line, 'A network needs at least one wire.')189+ const connectedToWire = new Set(wires.flatMap(wire => [wire.from, wire.to]))190+ for (const pole of [battery.positive, battery.negative]) {191+ if (!connectedToWire.has(pole)) sourceError(battery.line, `Battery node "${pole}" must be connected to at least one wire.`)192+ }193+194+ return {195+ view: view ?? 'both',196+ labels: labels ?? 'sides',197+ title,198+ rectangle: null,199+ network: { positive: battery.positive, negative: battery.negative, nodes, wires, line: firstNetworkLine },200+ }201+}@@ -0,0 +1,9 @@
1+import { squaringAdapter } from '../adapters/squaring'2+import { createRendererUnit } from '../runtime/unit'3+4+export default createRendererUnit({5+ id: 'squaring',6+ kind: 'render',7+ adapter: squaringAdapter,8+ knownLosses: ['Networks are arranged by search, so very large or non-planar networks are rejected instead of drawn.'],9+})@@ -8,6 +8,7 @@ const fixtures = {
88 erd: '[Person]\n*name\n\n[Place]\n*id\n\nPerson *--1 Place',99 nomnoml: '[Alice]->[Bob]',1010 bytefield: '[{:bits 8 :name "field"}]',11+ squaring: 'rectangle 3 x 2\nsquares 2 1 1',1112 trn: 'ingredient flour\noutcome bread {\n + flour 500 g\n -> bake\n}',1213 vega: '{"width":100,"height":50,"marks":[{"type":"rect","encode":{"enter":{"x":{"value":0},"y":{"value":0},"width":{"value":100},"height":{"value":50}}}}]}',1314 vegalite: '{"data":{"values":[{"x":"A","y":2}]},"mark":"bar","encoding":{"x":{"field":"x"},"y":{"field":"y","type":"quantitative"}}}',@@ -0,0 +1,162 @@
1+import { describe, expect, it } from 'vitest'2+import orderNine from '../../../examples/diagrams/order-nine.squaring?raw'3+import antiWedge from '../../../examples/diagrams/anti-wedge.squaring?raw'4+import compound from '../../../examples/diagrams/compound.squaring?raw'5+import { squaringAdapter } from '../src/adapters/squaring'6+import { parseSquaring } from '../src/languages/squaring'7+import { buildSquaring } from '../src/languages/squaring-model'8+import type { RenderRequest } from '../src/runtime/types'9+10+function request(source: string, options: Record<string, string> = {}): RenderRequest {11+ return {12+ engine: 'squaring',13+ source,14+ format: 'svg',15+ options,16+ metadata: { title: '', description: '' },17+ presentation: { background: '', padding: 0, frame: false },18+ }19+}20+21+const render = (source: string, options: Record<string, string> = {}) => squaringAdapter.render(request(source, options), new AbortController().signal)22+23+describe('squaring language', () => {24+ it('parses a Bouwkamp rectangle with grouped, bare, and commented square lists', () => {25+ const document = parseSquaring(orderNine)26+27+ expect(document.view).toBe('both')28+ expect(document.labels).toBe('sides')29+ expect(document.title).toBe('Order 9 simple perfect squared rectangle')30+ expect(document.rectangle).toMatchObject({ width: 33, height: 32, sides: [18, 15, 7, 8, 14, 4, 10, 1, 9] })31+ expect(document.network).toBeNull()32+ expect(parseSquaring('rectangle 3x2\n2, 1, 1 # trailing comment').rectangle?.sides).toEqual([2, 1, 1])33+ })34+35+ it('parses a network with a battery, wire lines, and chains', () => {36+ const document = parseSquaring(antiWedge)37+38+ expect(document.rectangle).toBeNull()39+ expect(document.network?.positive).toBe('a')40+ expect(document.network?.negative).toBe('e')41+ expect(document.network?.nodes).toEqual(['a', 'e', 'b', 'c', 'd', 'f'])42+ expect(document.network?.wires).toHaveLength(9)43+ expect(parseSquaring('battery p n\nwire p n').network?.wires).toEqual([{ from: 'p', to: 'n', line: 2 }])44+ })45+46+ it('rejects mixed, incomplete, and malformed sources with line numbers', () => {47+ expect(() => parseSquaring('squares 1 2')).toThrow('Line 1: Declare the rectangle')48+ expect(() => parseSquaring('rectangle 3 x 2\nsquares 2 1 1\nbattery a b')).toThrow('Line 3: A squaring is either a rectangle')49+ expect(() => parseSquaring('a - b')).toThrow('Line 1: A network needs a battery')50+ expect(() => parseSquaring('battery a b\nc - d')).toThrow('Battery node "a" must be connected')51+ expect(() => parseSquaring('.view sideways\nrectangle 3 x 2\n2 1 1')).toThrow('Line 1: Unknown view "sideways"')52+ expect(() => parseSquaring('rectangle 3 x 2\n2 1 1\nbogus line')).toThrow('Line 3: Unrecognized line')53+ expect(() => parseSquaring('rectangle 3 x 2\n2')).toThrow('at least two squares')54+ })55+})56+57+describe('squaring model', () => {58+ it('places Bouwkamp squares and derives the Smith diagram from horizontal segments', () => {59+ const model = buildSquaring(parseSquaring(orderNine))60+61+ expect(model).toMatchObject({ form: 'rectangle', width: 33, height: 32, order: 9, simple: true, perfect: true, repeatedSides: [], blocks: [] })62+ expect(model.squares.map(square => [square.side, square.x, square.y])).toEqual([63+ [18, 0, 0], [15, 18, 0], [7, 18, 15], [8, 25, 15], [14, 0, 18], [4, 14, 18], [10, 14, 22], [1, 24, 22], [9, 24, 23],64+ ])65+ expect(model.nodes.map(node => [node.id, node.voltage, node.x0, node.x1])).toEqual([66+ ['top', 32, 0, 33], ['n1', 17, 18, 33], ['n2', 14, 0, 18], ['n3', 10, 14, 25], ['n4', 9, 24, 33], ['bottom', 0, 0, 33],67+ ])68+ // Kirchhoff: current into every interior node equals current out of it.69+ for (const node of model.nodes.slice(1, -1)) {70+ const inflow = model.squares.filter(square => square.bottom === node.index).reduce((sum, square) => sum + square.side, 0)71+ const outflow = model.squares.filter(square => square.top === node.index).reduce((sum, square) => sum + square.side, 0)72+ expect(inflow).toBe(outflow)73+ }74+ })75+76+ it('solves a polyhedral network into an integer squared rectangle', () => {77+ const model = buildSquaring(parseSquaring(antiWedge))78+79+ expect(model).toMatchObject({ form: 'network', width: 15, height: 11, order: 9, simple: true, perfect: false, repeatedSides: [1, 4, 5, 6] })80+ expect(model.nodes.find(node => node.id === 'a')?.voltage).toBe(11)81+ expect(model.nodes.find(node => node.id === 'e')?.voltage).toBe(0)82+ expect(model.squares.reduce((sum, square) => sum + square.side * square.side, 0)).toBe(15 * 11)83+ // Ohm with unit resistance: every square's side is the voltage drop across it.84+ for (const square of model.squares) {85+ expect((model.nodes[square.top]?.voltage ?? 0) - (model.nodes[square.bottom]?.voltage ?? 0)).toBe(square.side)86+ }87+ // The squares tile the rectangle without overlaps.88+ for (const first of model.squares) {89+ for (const second of model.squares) {90+ if (first === second) continue91+ const overlap = first.x < second.x + second.side && second.x < first.x + first.side && first.y < second.y + second.side && second.y < first.y + first.side92+ expect(overlap).toBe(false)93+ }94+ }95+ })96+97+ it('finds compound blocks and repeated sides', () => {98+ const model = buildSquaring(parseSquaring(compound))99+100+ expect(model).toMatchObject({ order: 10, simple: false, perfect: true })101+ expect(model.blocks).toEqual([{ x: 32, y: 0, width: 33, height: 32, squares: 9 }])102+103+ const quilt = buildSquaring(parseSquaring('rectangle 3 x 2\n2 1 1'))104+ expect(quilt.simple).toBe(false)105+ expect(quilt.blocks).toEqual([{ x: 2, y: 0, width: 1, height: 2, squares: 2 }])106+ expect(quilt.repeatedSides).toEqual([1])107+ })108+109+ it('explains why a rectangle or a network cannot become a squaring', () => {110+ expect(() => buildSquaring(parseSquaring('rectangle 33 x 32\n18 15 7 8 14 4 10 1 8'))).toThrow('area of 1039')111+ expect(() => buildSquaring(parseSquaring('rectangle 33 x 32\n15 18 7 8 14 4 10 1 9'))).toThrow('Square 7 (side 10) does not fit at column 0, row 22; only 7 × 10 is free there.')112+ expect(() => buildSquaring(parseSquaring('battery a b\na - b\nc - d'))).toThrow('not connected')113+ expect(() => buildSquaring(parseSquaring('battery a c\na - b - c\na - d - c\nb - d'))).toThrow('carries no current')114+ // A network containing K3,3 is not planar, so no arrangement exists.115+ expect(() => buildSquaring(parseSquaring('battery n1 n4\nn1 - n5\nn1 - n6\nn2 - n4\nn2 - n5\nn2 - n6\nn3 - n4\nn3 - n5\nn3 - n6\nn2 - n5'))).toThrow('cannot be arranged')116+ // A wire parallel to the battery becomes a full-height square beside a compound remainder.117+ const cube = buildSquaring(parseSquaring('battery a b\na - b - c - d - a\ne - f - g - h - e\na - e\nb - f\nc - g\nd - h'))118+ expect(cube).toMatchObject({ width: 24, height: 14, order: 12, simple: false })119+ expect(cube.squares[0]).toMatchObject({ side: 14, x: 0, y: 0 })120+ })121+})122+123+describe('squaring renderer', () => {124+ it('renders the rectangle and the Smith diagram side by side', async () => {125+ const result = await render(orderNine)126+127+ expect(result.engineVersion).toBe('diagramzip-squaring@1')128+ expect(result.body).toContain('<title>Order 9 simple perfect squared rectangle</title>')129+ expect(result.body).toContain('data-view="both" data-form="rectangle" data-order="9" data-width="33" data-height="32" data-simple="true" data-perfect="true"')130+ expect(result.body.match(/class="squaring-square"/g)).toHaveLength(9)131+ expect(result.body.match(/class="squaring-wire"/g)).toHaveLength(9)132+ expect(result.body.match(/class="squaring-node"/g)).toHaveLength(6)133+ expect(result.body).toContain('class="squaring-wire" data-from="top" data-to="n2" data-current="18"')134+ expect(result.body).toContain('class="squaring-node" data-node-id="n1" data-voltage="17"')135+ expect(result.body).toContain('class="squaring-battery"')136+ expect(result.body).toContain('Order 9 · 33 × 32 · simple · perfect')137+ expect(result.body).not.toContain('class="squaring-block"')138+ })139+140+ it('outlines compound blocks in the overlay view and honours label and view directives', async () => {141+ const overlay = await render(compound)142+ expect(overlay.body).toContain('data-view="overlay"')143+ expect(overlay.body).toContain('class="squaring-block" data-squares="9"')144+ expect(overlay.body).toContain('compound (1 block outlined) · perfect')145+ expect(overlay.body).toContain('data-overlay="true"')146+147+ const plain = await render('.view rectangle\n.labels none\nrectangle 3 x 2\n2 1 1')148+ expect(plain.body).toContain('data-view="rectangle"')149+ expect(plain.body).not.toContain('class="squaring-wire"')150+ expect(plain.body).not.toContain('<text x=')151+ expect(plain.body).toContain('<title>Squared rectangle 3 × 2</title>')152+153+ const circuit = await render(antiWedge.replace('.view both', '.view circuit'))154+ expect(circuit.body).toContain('data-view="circuit"')155+ expect(circuit.body).not.toContain('class="squaring-square"')156+ expect(circuit.body).toContain('battery a → e')157+ })158+159+ it('rejects renderer options', () => {160+ expect(() => render(orderNine, { scale: '2' })).toThrow('Unsupported squaring option: scale.')161+ })162+})@@ -12,6 +12,7 @@ export const ENGINE_IDS = [
1212 'nwdiag',1313 'packetdiag',1414 'rackdiag',15+ 'squaring',1516 'bpmn',1617 'bytefield',1718 'dbml',@@ -110,6 +110,7 @@ const PROFILE_BY_ENGINE = Object.freeze({
110110 nwdiag: NEUTRAL_SVG_NORMALIZATION,111111 packetdiag: NEUTRAL_SVG_NORMALIZATION,112112 rackdiag: NEUTRAL_SVG_NORMALIZATION,113+ squaring: NEUTRAL_SVG_NORMALIZATION,113114 bytefield: NEUTRAL_SVG_NORMALIZATION,114115 mermaid: NEUTRAL_SVG_NORMALIZATION,115116 bpmn: NEUTRAL_SVG_NORMALIZATION,@@ -144,6 +145,7 @@ const VERSION_PATTERNS = Object.freeze({
144145 nwdiag: /(?:nwdiag@)?3\.0\.0/,145146 packetdiag: /(?:packetdiag@)?3\.0\.0/,146147 rackdiag: /(?:rackdiag@)?3\.0\.0/,148+ squaring: /diagramzip-squaring@1/,147149 bytefield: /bytefield-svg@1\.11\.0/,148150 mermaid: /mermaid@11\.17\.0/,149151 bpmn: /(?:bpmn-js@18\.25\.1|diagramzip-bpmn-svg@1)/,@@ -28,6 +28,8 @@ Common choices include Mermaid for compact general diagrams, PlantUML for UML-or
2828 2929 Use Tabular Recipe Notation (`trn`) for recipes, crafting trees, and assembly transformations where ingredient quantities feed short actions and meaningful outcomes. Put `.layout combined` or `.layout individual` in the TRN source. TRN originates with [Michael Chu at Cooking for Engineers](https://www.cookingforengineers.com/); diagram.zip provides its own text DSL and SVG renderer for that tabular recipe concept. For OCR transcription, preserve the printed quantities and wording, and put uncertain readings in `#` comments instead of guessing.3030 31+Use Squaring (`squaring`) for squared rectangles, squared squares, and their Smith diagrams: the electric-circuit view in which every horizontal segment is a node and every square is a unit resistor whose current is the square's side. Write a known dissection as `rectangle <width> x <height>` followed by the square sides in Bouwkamp order, or build one backwards from a planar network with `battery <positive> <negative>` and wires such as `a - b - c`. The caption reports whether the result is simple (no smaller rectangle of squares) and perfect (no repeated side). The correspondence comes from [Brooks, Smith, Stone, and Tutte](https://doi.org/10.1215/S0012-7094-40-00718-9); diagram.zip provides its own notation and SVG renderer for it.32+3133 The complete renderer set and syntax links are in [llms.txt](https://docs.diagram.zip/llms.txt) and [diagram-types.json](https://docs.diagram.zip/diagram-types.json). Semantic routing metadata is in [diagram-skills.json](https://docs.diagram.zip/diagram-skills.json).3234 3335 Do not infer standards support from a similarly named renderer mode. Confirm the renderer's output and interchange fidelity.