Add a squaring-semantic normalization profile with voltage bands for squares and nodes, an indigo wire color in dark mode, and themed labels and block outlines. Replace interpunct separators with bars.
- commit
- 8792e678d57b5d8a74514bddcefea954f70db5c9
- parent
- 8847c5d8e8bea5a0a9ff055adeecc50d2cee218c
- author
- Dami <dami.etoile@gmail.com>
| M | apps/ | 2 | +- |
| M | apps/ | 2 | +- |
| M | apps/ | 2 | +- |
| M | apps/ | 4 | +- |
| M | apps/ | 22 | ++++++---- |
| M | renderers/ | 2 | +- |
| M | renderers/ | 2 | +- |
| M | renderers/ | 2 | +- |
| M | renderers/ | 63 | +++++++++++++--------------- |
| M | renderers/ | 4 | +- |
| M | shared/ | 68 | ++++++++++++++++++++++++++++-- |
@@ -334,7 +334,7 @@ export const diagramTypes = [
334334 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 `#`.'],335335 language: 'text',336336 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.',337+ styleSummary: 'Squaring colors each square and node by voltage from blue to red and scales wire thickness by current. Light and dark appearances use their own square, node, and wire palettes, and compound blocks are outlined with a dashed line.',338338 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.'],339339 rendererOptions: [],340340 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.'],@@ -8,7 +8,7 @@ sidebar_label: Squaring
88 99 # Style Squaring1010 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.11+Squaring colors each square and node by voltage from blue to red and scales wire thickness by current. Light and dark appearances use their own square, node, and wire palettes, and compound blocks are outlined with a dashed line.1212 1313 Start with the [general presentation settings](/style/presentation). Edit shared or raw presentation in Details, and keep renderer-specific styling in Source.1414 @@ -2557,7 +2557,7 @@ sidebar_label: Squaring
25572557 25582558 # Style Squaring25592559 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.2560+Squaring colors each square and node by voltage from blue to red and scales wire thickness by current. Light and dark appearances use their own square, node, and wire palettes, and compound blocks are outlined with a dashed line.25612561 25622562 Start with the [general presentation settings](/style/presentation). Edit shared or raw presentation in Details, and keep renderer-specific styling in Source.25632563 @@ -142,7 +142,7 @@ document.querySelector('#app').innerHTML = `
142142 <span class="preview-toolbar-divider" aria-hidden="true"></span>143143 <div class="preview-appearance-controls" aria-label="Preview appearance">144144 <button id="preview-raw" type="button" aria-pressed="false" title="Show the renderer's original appearance">Raw</button>145- <button id="preview-theme-toggle" type="button" data-preview-theme="light" aria-pressed="false" aria-label="Preview theme: Light" title="Preview theme: Light · click for dark">145+ <button id="preview-theme-toggle" type="button" data-preview-theme="light" aria-pressed="false" aria-label="Preview theme: Light" title="Preview theme: Light. Click for Dark">146146 <svg class="preview-control-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">147147 <circle cx="12" cy="12" r="8"/>148148 <path class="preview-theme-fill" d="M12 4a8 8 0 0 1 0 16Z"/>@@ -806,7 +806,7 @@ function syncPreviewAppearanceControls() {
806806 previewThemeButton.dataset.previewTheme = previewTheme807807 previewThemeButton.setAttribute('aria-pressed', String(!raw && previewTheme === 'dark'))808808 previewThemeButton.setAttribute('aria-label', `Preview theme: ${previewTheme === 'dark' ? 'Dark' : 'Light'}`)809- previewThemeButton.title = `Preview theme: ${previewTheme === 'dark' ? 'Dark' : 'Light'} · click for ${oppositeTheme}`809+ previewThemeButton.title = `Preview theme: ${previewTheme === 'dark' ? 'Dark' : 'Light'}. Click for ${oppositeTheme}`810810 previewThemeButton.disabled = raw || !supportedPreviewAppearances.has(811811 `${oppositeTheme}-${previewTransparent ? 'transparent' : 'framed'}`,812812 )@@ -181,20 +181,24 @@ test('applies D2, PlantUML, Svgbob, and neutral SVG family roles', () => {
181181 }182182 })183183 184-test('normalizes squaring ink and canvas while preserving voltage colors', () => {184+test('normalizes squaring voltage bands, wires, and labels with dedicated palettes', () => {185185 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',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 class="squaring-square-fill" data-band="7" x="4" y="4" width="20" height="20" fill="#f87171" stroke="#1e293b"></rect><text class="squaring-square-label" x="14" y="14" fill="#0f172a">2</text></g><path class="squaring-wire-path" d="M0 0L1 1" fill="none" stroke="#1e293b"></path><circle class="squaring-node-dot" data-band="0" cx="1" cy="1" r="5" fill="#2563eb"></circle><rect class="squaring-block" x="1" y="1" width="2" height="2" fill="none" stroke="#db2777"></rect><text class="squaring-summary" fill="#666666">Order 3</text></svg>',187+ metadata, 'squaring', 'diagramzip-squaring@2',188188 )189189 190- assert.match(squaring, /data-dz-profile="neutral-svg-semantic-2"/)190+ assert.match(squaring, /data-dz-profile="squaring-semantic-1"/)191191 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>/)192+ assert.match(squaring, /class="squaring-square-fill"[^>]*data-dz-fill="squaring-square-7"[^>]*data-dz-stroke="squaring-edge"/)193+ assert.match(squaring, /class="squaring-square-label"[^>]*data-dz-fill="squaring-label"/)194+ assert.match(squaring, /class="squaring-wire-path"[^>]*data-dz-stroke="squaring-wire"/)195+ assert.match(squaring, /class="squaring-node-dot"[^>]*data-dz-fill="squaring-node-0"/)196+ assert.match(squaring, /class="squaring-block"[^>]*data-dz-stroke="squaring-block"/)196197 assert.match(squaring, /class="squaring-summary"[^>]*data-dz-fill="ink-muted"/)197- assert.match(materializeSvg(squaring, 'dark-transparent'), /data-dz-appearance="dark-transparent"/)198+ const dark = materializeSvg(squaring, 'dark-transparent')199+ assert.match(dark, /:root\[data-dz-profile="squaring-semantic-1"\]\[data-dz-appearance\^="dark-"\]\{[^}]*--dz-sq-square-7:#dc2626/)200+ assert.match(dark, /--dz-sq-wire:#a5b4fc/)201+ assert.match(dark, /\[data-dz-fill="squaring-square-7"\]\{fill:var\(--dz-sq-square-7\)!important\}/)198202 })199203 200204 test('normalizes TRN table structure and relationship zones', () => {@@ -98,7 +98,7 @@ const versions: Record<EngineId, string> = {
9898 nwdiag: 'nwdiag@3.0.0/python-worker-1',9999 packetdiag: 'packetdiag@3.0.0/python-worker-1',100100 rackdiag: 'rackdiag@3.0.0/python-worker-1',101- squaring: 'diagramzip-squaring@1',101+ squaring: 'diagramzip-squaring@2',102102 graphviz: 'graphviz@15.1.1/edge-wasm-2',103103 dbml: 'dbml@1.0.31+graphviz@15.1.1/edge-wasm-2',104104 pikchr: 'pikchr@85e65b9686/edge-wasm-1',@@ -5,6 +5,6 @@
55 "compatibility_date": "2026-08-20",66 "compatibility_flags": ["nodejs_compat"],77 "observability": { "enabled": true, "logs": { "head_sampling_rate": 1 } },8- "vars": { "RENDERER_BUILD": "diagramzip-squaring-1" },8+ "vars": { "RENDERER_BUILD": "diagramzip-squaring-2" },99 "routes": [{ "pattern": "squaring.render.diagram.zip", "custom_domain": true }]1010 }@@ -5,7 +5,7 @@ import { buildSquaring } from '../languages/squaring-model'
55 import { renderSquaring } from '../languages/squaring-renderer'66 import { edgeFailure, edgeResult } from './types'77 8-const VERSION = 'diagramzip-squaring@1'8+const VERSION = 'diagramzip-squaring@2'99 1010 function validateOptions(options: Record<string, string>): void {1111 for (const name of Object.keys(options)) {@@ -10,9 +10,13 @@ const TARGET_SIZE = 480
1010 const FONT_FAMILY = 'Arial,Helvetica,sans-serif'1111 const INK = '#111827'1212 const MUTED = '#666666'13-const WIRE = '#111827'14-const OVERLAY_WIRE = '#1e293b'13+const WIRE = '#1e293b'1514 const BLOCK = '#db2777'15+export const BAND_COUNT = 816+// Raw appearance uses the light palette; themed appearances swap these through the17+// squaring-semantic normalization profile in shared/svg.18+export const LIGHT_SQUARE_BANDS = ['#93c5fd', '#b3d1fb', '#cddaf2', '#e2e4e8', '#f1d6d2', '#fbbfba', '#fca5a5', '#f87171'] as const19+export const LIGHT_NODE_BANDS = ['#2563eb', '#3b6fe0', '#4f74c8', '#6b7280', '#b45454', '#d13b3b', '#dc2626', '#b91c1c'] as const1620 1721 interface Panel {1822 x: number@@ -29,18 +33,9 @@ function round(value: number): string {
2933 return String(Math.round(value * 100) / 100)3034 }3135 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 {36+function band(t: number): number {3937 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)38+ return Math.min(BAND_COUNT - 1, Math.floor(clamped * BAND_COUNT))4439 }4540 4641 function squareSvg(square: SquaringSquare, model: SquaringModel, panel: Panel, unit: number, labels: boolean, overlay: boolean): string {@@ -48,14 +43,15 @@ function squareSvg(square: SquaringSquare, model: SquaringModel, panel: Panel, u
4843 const y = panel.y + square.y * unit4944 const size = square.side * unit5045 const topVoltage = model.nodes[square.top]?.voltage ?? model.height51- const fill = voltageColor((topVoltage - square.side / 2) / model.height, true)46+ const level = band((topVoltage - square.side / 2) / model.height)47+ const fill = LIGHT_SQUARE_BANDS[level] ?? LIGHT_SQUARE_BANDS[0]5248 const fontSize = Math.min(16, size * 0.42)5349 // In the overlay the wire runs down the middle of the square, so the label moves beside it.5450 const labelX = overlay ? x + size / 2 + 6 : x + size / 25551 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>`52+ ? `<text x="${round(labelX)}" y="${round(y + size / 2)}" text-anchor="${overlay ? 'start' : 'middle'}" dominant-baseline="central" class="squaring-square-label" fill="${INK}" font-size="${round(fontSize)}" font-family="${FONT_FAMILY}">${square.side}</text>`5753 : ''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>`54+ 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 class="squaring-square-fill" data-band="${level}" x="${round(x)}" y="${round(y)}" width="${round(size)}" height="${round(size)}" fill="${fill}" stroke="${INK}" stroke-width="1"/>${label}</g>`5955 }6056 6157 function blocksSvg(model: SquaringModel, panel: Panel, unit: number): string {@@ -69,8 +65,8 @@ function nodeCenter(node: SquaringNode, panel: Panel, unit: number): { x: number
6965 return { x: panel.x + ((node.x0 + node.x1) / 2) * unit, y: panel.y + node.y * unit }7066 }7167 72-function wireSvg(square: SquaringSquare, model: SquaringModel, panel: Panel, unit: number, maxSide: number, showCurrent: boolean, overlay: boolean): string {73- const stroke = overlay ? OVERLAY_WIRE : WIRE68+function wireSvg(square: SquaringSquare, model: SquaringModel, panel: Panel, unit: number, maxSide: number, showCurrent: boolean): string {69+ const stroke = WIRE7470 const top = model.nodes[square.top]7571 const bottom = model.nodes[square.bottom]7672 if (top === undefined || bottom === undefined) return ''@@ -92,19 +88,20 @@ function wireSvg(square: SquaringSquare, model: SquaringModel, panel: Panel, uni
9288 ].join(' ')9389 const midY = (a.y + b.y) / 29490 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}"/>`91+ ? `<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" class="squaring-wire-arrow" fill="${stroke}"/>`9692 : ''9793 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>`94+ ? `<text x="${round(cx + 6)}" y="${round(midY - 7)}" text-anchor="start" dominant-baseline="central" class="squaring-wire-label" fill="${MUTED}" font-size="10" font-family="${FONT_FAMILY}">${square.side}</text>`9995 : ''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>`96+ 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 class="squaring-wire-path" d="${path}" fill="none" stroke="${stroke}" stroke-width="${round(width)}" stroke-linecap="round" stroke-linejoin="round"/>${arrow}${label}</g>`10197 }10298 10399 function nodeSvg(node: SquaringNode, model: SquaringModel, panel: Panel, unit: number, rung: boolean, showVoltage: boolean): string {104100 const center = nodeCenter(node, panel, unit)105- const color = voltageColor(node.voltage / model.height, false)101+ const level = band(node.voltage / model.height)102+ const color = LIGHT_NODE_BANDS[level] ?? LIGHT_NODE_BANDS[0]106103 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"/>`104+ ? `<line class="squaring-node-rung" data-band="${level}" 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"/>`108105 : ''109106 const pole = node.index === model.positive ? '+' : node.index === model.negative ? '−' : ''110107 // Wires leave every node horizontally, so labels sit above or below the rung instead of on it.@@ -112,9 +109,9 @@ function nodeSvg(node: SquaringNode, model: SquaringModel, panel: Panel, unit: n
112109 const labelY = node.index === model.negative ? center.y + 13 : center.y - 10113110 const anchor = pole === '' ? 'start' : 'middle'114111 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>`112+ ? `<text x="${round(labelX)}" y="${round(labelY)}" text-anchor="${anchor}" dominant-baseline="central" class="squaring-node-label" fill="${INK}" font-size="11" font-weight="700" font-family="${FONT_FAMILY}">${pole === '' ? '' : `${pole} `}${node.voltage}</text>`116113 : ''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>`114+ 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 class="squaring-node-dot" data-band="${level}" cx="${round(center.x)}" cy="${round(center.y)}" r="5.5" fill="${color}"/>${label}</g>`118115 }119116 120117 function batterySvg(model: SquaringModel, panel: Panel, unit: number): string {@@ -126,11 +123,11 @@ function batterySvg(model: SquaringModel, panel: Panel, unit: number): string {
126123 const gx = panel.x - BATTERY_GUTTER / 2 - 4127124 const midY = (a.y + b.y) / 2128125 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>` +126+ `<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)}" class="squaring-wire-path" fill="none" stroke="${WIRE}" stroke-width="1.5" stroke-dasharray="4 3"/>` +127+ `<line class="squaring-wire-path" x1="${round(gx - 9)}" y1="${round(midY - 4)}" x2="${round(gx + 9)}" y2="${round(midY - 4)}" stroke="${WIRE}" stroke-width="2.5"/>` +128+ `<line class="squaring-wire-path" x1="${round(gx - 4)}" y1="${round(midY + 4)}" x2="${round(gx + 4)}" y2="${round(midY + 4)}" stroke="${WIRE}" stroke-width="2.5"/>` +129+ `<text class="squaring-node-label" 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>` +130+ `<text class="squaring-node-label" 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>` +134131 `</g>`135132 }136133 @@ -139,7 +136,7 @@ function panelTitle(text: string, panel: Panel): string {
139136 }140137 141138 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"/>`139+ return `<rect class="squaring-frame" x="${round(panel.x)}" y="${round(panel.y)}" width="${round(panel.width)}" height="${round(panel.height)}" fill="none" stroke="${INK}" stroke-width="1.5"/>`143140 }144141 145142 function rectanglePanel(model: SquaringModel, panel: Panel, unit: number, labels: boolean, overlay = false): string {@@ -149,7 +146,7 @@ function rectanglePanel(model: SquaringModel, panel: Panel, unit: number, labels
149146 150147 function circuitPanel(model: SquaringModel, panel: Panel, unit: number, overlay: boolean, labels: boolean): string {151148 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('')149+ const wires = model.squares.map(square => wireSvg(square, model, panel, unit, maxSide, labels && !overlay)).join('')153150 const nodes = model.nodes.map(node => nodeSvg(node, model, panel, unit, overlay, labels && !overlay)).join('')154151 return `<g class="squaring-circuit" data-overlay="${overlay}">${batterySvg(model, panel, unit)}${wires}${nodes}</g>`155152 }@@ -168,7 +165,7 @@ export function describeSquaring(model: SquaringModel): string {
168165 const negative = model.nodes[model.negative]?.id ?? ''169166 parts.push(`battery ${positive} → ${negative}`)170167 }171- return parts.join(' · ')168+ return parts.join(' | ')172169 }173170 174171 export function renderSquaring(document: SquaringDocument, model: SquaringModel): string {@@ -133,7 +133,7 @@ describe('squaring renderer', () => {
133133 expect(result.body).toContain('class="squaring-wire" data-from="top" data-to="n2" data-current="18"')134134 expect(result.body).toContain('class="squaring-node" data-node-id="n1" data-voltage="17"')135135 expect(result.body).toContain('class="squaring-battery"')136- expect(result.body).toContain('Order 9 · 33 × 32 · simple · perfect')136+ expect(result.body).toContain('Order 9 | 33 × 32 | simple | perfect')137137 expect(result.body).not.toContain('class="squaring-block"')138138 })139139 @@ -141,7 +141,7 @@ describe('squaring renderer', () => {
141141 const overlay = await render(compound)142142 expect(overlay.body).toContain('data-view="overlay"')143143 expect(overlay.body).toContain('class="squaring-block" data-squares="9"')144- expect(overlay.body).toContain('compound (1 block outlined) · perfect')144+ expect(overlay.body).toContain('compound (1 block outlined) | perfect')145145 expect(overlay.body).toContain('data-overlay="true"')146146 147147 const plain = await render('.view rectangle\n.labels none\nrectangle 3 x 2\n2 1 1')@@ -93,6 +93,12 @@ const TRN_NORMALIZATION = capability(
9393 ['Four relationship-zone branch colors adapt to the selected appearance and adjacent zones never share a color.'],9494 )9595 96+const SQUARING_NORMALIZATION = capability(97+ 'squaring-semantic-1',98+ 'semantic',99+ ['Voltage bands for squares and nodes, wires, and block outlines use dedicated light and dark palettes.'],100+)101+96102 const PROFILE_BY_ENGINE = Object.freeze({97103 graphviz: GRAPHVIZ_NORMALIZATION,98104 dbml: GRAPHVIZ_NORMALIZATION,@@ -110,7 +116,7 @@ const PROFILE_BY_ENGINE = Object.freeze({
110116 nwdiag: NEUTRAL_SVG_NORMALIZATION,111117 packetdiag: NEUTRAL_SVG_NORMALIZATION,112118 rackdiag: NEUTRAL_SVG_NORMALIZATION,113- squaring: NEUTRAL_SVG_NORMALIZATION,119+ squaring: SQUARING_NORMALIZATION,114120 bytefield: NEUTRAL_SVG_NORMALIZATION,115121 mermaid: NEUTRAL_SVG_NORMALIZATION,116122 bpmn: NEUTRAL_SVG_NORMALIZATION,@@ -145,7 +151,7 @@ const VERSION_PATTERNS = Object.freeze({
145151 nwdiag: /(?:nwdiag@)?3\.0\.0/,146152 packetdiag: /(?:packetdiag@)?3\.0\.0/,147153 rackdiag: /(?:rackdiag@)?3\.0\.0/,148- squaring: /diagramzip-squaring@1/,154+ squaring: /diagramzip-squaring@2/,149155 bytefield: /bytefield-svg@1\.11\.0/,150156 mermaid: /mermaid@11\.17\.0/,151157 bpmn: /(?:bpmn-js@18\.25\.1|diagramzip-bpmn-svg@1)/,@@ -711,7 +717,7 @@ function resolveSiblingTextContrast(root) {
711717 })712718 if (owner?.role.startsWith('accent-')) child.attributes.set('data-dz-fill', 'on-accent')713719 else if (owner?.role.startsWith('surface-')) child.attributes.set('data-dz-fill', 'ink')714- else if (owner !== null && owner !== undefined) child.attributes.delete('data-dz-fill')720+ else if (owner !== null && owner !== undefined && !(child.attributes.get('data-dz-fill') ?? '').startsWith('squaring-')) child.attributes.delete('data-dz-fill')715721 }716722 }717723 for (const child of node.children) visit(child)@@ -719,6 +725,31 @@ function resolveSiblingTextContrast(root) {
719725 visit(root)720726 }721727 728+function applySquaringProfile(root) {729+ applyNeutralSvgProfile(root, 'squaring')730+ const visit = node => {731+ if (node.type === 'text') return732+ const classes = classNames(node)733+ const band = node.attributes.get('data-band') ?? '0'734+ if (classes.has('squaring-square-fill')) {735+ node.attributes.set('data-dz-fill', `squaring-square-${band}`)736+ node.attributes.set('data-dz-stroke', 'squaring-edge')737+ }738+ if (classes.has('squaring-node-dot')) node.attributes.set('data-dz-fill', `squaring-node-${band}`)739+ if (classes.has('squaring-node-rung')) node.attributes.set('data-dz-stroke', `squaring-node-${band}`)740+ if (classes.has('squaring-square-label')) node.attributes.set('data-dz-fill', 'squaring-label')741+ if (classes.has('squaring-wire-path')) node.attributes.set('data-dz-stroke', 'squaring-wire')742+ if (classes.has('squaring-wire-arrow')) node.attributes.set('data-dz-fill', 'squaring-wire')743+ if (classes.has('squaring-wire-label') || classes.has('squaring-summary')) node.attributes.set('data-dz-fill', 'ink-muted')744+ if (classes.has('squaring-node-label') || classes.has('squaring-title')) node.attributes.set('data-dz-fill', 'ink')745+ if (classes.has('squaring-panel-title')) node.attributes.set('data-dz-fill', 'ink-muted')746+ if (classes.has('squaring-frame')) node.attributes.set('data-dz-stroke', 'line-muted')747+ if (classes.has('squaring-block')) node.attributes.set('data-dz-stroke', 'squaring-block')748+ for (const child of node.children) visit(child)749+ }750+ visit(root)751+}752+722753 function applyTrnProfile(root) {723754 const visit = (node, inGrid = false) => {724755 if (node.type === 'text') return@@ -769,6 +800,7 @@ function applyProfile(root, capability, engine) {
769800 }770801 else if (capability.profile === 'authored-neutral-semantic-1') applyNeutralSvgProfile(root, engine)771802 else if (capability.profile === 'trn-semantic-4') applyTrnProfile(root)803+ else if (capability.profile === 'squaring-semantic-1') applySquaringProfile(root)772804 if (capability.conformance !== 'presentation-only') resolveSiblingTextContrast(root)773805 }774806 @@ -1009,6 +1041,30 @@ const TRN_LIGHT_BRANCH_PALETTE = Object.freeze({
10091041 accent1: '#dbeafe', accent2: '#fef3c7', accent3: '#ede9fe', accent4: '#ccfbf1', onAccent: '#0f172a',10101042 })10111043 1044+const SQUARING_LIGHT_PALETTE = Object.freeze({1045+ squares: ['#93c5fd', '#b3d1fb', '#cddaf2', '#e2e4e8', '#f1d6d2', '#fbbfba', '#fca5a5', '#f87171'],1046+ nodes: ['#2563eb', '#3b6fe0', '#4f74c8', '#6b7280', '#b45454', '#d13b3b', '#dc2626', '#b91c1c'],1047+ edge: '#1e293b', label: '#0f172a', wire: '#1e293b', block: '#db2777',1048+})1049+const SQUARING_DARK_PALETTE = Object.freeze({1050+ squares: ['#1e40af', '#2d55b8', '#3f5f9f', '#4f5b73', '#7f414d', '#a23a3f', '#c12f33', '#dc2626'],1051+ nodes: ['#60a5fa', '#7cb0f5', '#93b7e8', '#a8b0c4', '#d6a3a3', '#f38f8f', '#f87171', '#ef4444'],1052+ edge: '#0b1220', label: '#f8fafc', wire: '#a5b4fc', block: '#f472b6',1053+})1054+1055+function squaringPaletteVariables(palette) {1056+ const bands = palette.squares.map((value, index) => `--dz-sq-square-${index}:${value};--dz-sq-node-${index}:${palette.nodes[index]};`).join('')1057+ return `${bands}--dz-sq-edge:${palette.edge};--dz-sq-label:${palette.label};--dz-sq-wire:${palette.wire};--dz-sq-block:${palette.block};`1058+}1059+1060+function squaringRoleCss(themed) {1061+ const scope = `${themed}[data-dz-profile="squaring-semantic-1"]`1062+ const bands = SQUARING_LIGHT_PALETTE.squares.map((_, index) =>1063+ `${scope} [data-dz-fill="squaring-square-${index}"]{fill:var(--dz-sq-square-${index})!important}${scope} [data-dz-fill="squaring-node-${index}"]{fill:var(--dz-sq-node-${index})!important}${scope} [data-dz-stroke="squaring-node-${index}"]{stroke:var(--dz-sq-node-${index})!important}`,1064+ ).join('')1065+ return `${bands}${scope} [data-dz-stroke="squaring-edge"]{stroke:var(--dz-sq-edge)!important}${scope} [data-dz-fill="squaring-label"]{fill:var(--dz-sq-label)!important}${scope} [data-dz-stroke="squaring-wire"]{stroke:var(--dz-sq-wire)!important}${scope} [data-dz-fill="squaring-wire"]{fill:var(--dz-sq-wire)!important}${scope} [data-dz-stroke="squaring-block"]{stroke:var(--dz-sq-block)!important}`1066+}1067+10121068 function paletteVariables(palette) {10131069 return `--dz-canvas:${palette.canvas};--dz-surface-1:${palette.surface1};--dz-surface-2:${palette.surface2};--dz-surface-3:${palette.surface3};--dz-ink:${palette.ink};--dz-ink-muted:${palette.inkMuted};--dz-line:${palette.line};--dz-line-muted:${palette.lineMuted};--dz-accent-1:${palette.accent1};--dz-accent-2:${palette.accent2};--dz-accent-3:${palette.accent3};--dz-accent-4:${palette.accent4};--dz-on-accent:${palette.onAccent};--dz-frame:${palette.frame};`10141070 }@@ -1025,10 +1081,14 @@ function materializerCss() {
10251081 const trnLight = ':root[data-dz-profile="trn-semantic-4"][data-dz-appearance^="light-"]'10261082 const trnAutomatic = ':root[data-dz-profile="trn-semantic-4"][data-dz-appearance^="auto-"]'10271083 const variables = `${light}{${paletteVariables(LIGHT_PALETTE)}}${dark}{${paletteVariables(DARK_PALETTE)}}${automatic}{${paletteVariables(LIGHT_PALETTE)}}@media(prefers-color-scheme:dark){${automatic}{${paletteVariables(DARK_PALETTE)}}}`1084+ const squaringLight = ':root[data-dz-profile="squaring-semantic-1"][data-dz-appearance^="light-"]'1085+ const squaringDark = ':root[data-dz-profile="squaring-semantic-1"][data-dz-appearance^="dark-"]'1086+ const squaringAutomatic = ':root[data-dz-profile="squaring-semantic-1"][data-dz-appearance^="auto-"]'1087+ const squaringVariables = `${squaringLight}{${squaringPaletteVariables(SQUARING_LIGHT_PALETTE)}}${squaringDark}{${squaringPaletteVariables(SQUARING_DARK_PALETTE)}}${squaringAutomatic}{${squaringPaletteVariables(SQUARING_LIGHT_PALETTE)}}@media(prefers-color-scheme:dark){${squaringAutomatic}{${squaringPaletteVariables(SQUARING_DARK_PALETTE)}}}`10281088 const trnVariables = `${trnLight}{${branchPaletteVariables(TRN_LIGHT_BRANCH_PALETTE)}}${trnAutomatic}{${branchPaletteVariables(TRN_LIGHT_BRANCH_PALETTE)}}@media(prefers-color-scheme:dark){${trnAutomatic}{${branchPaletteVariables(DARK_PALETTE)}}}`10291089 const materializerVisibility = `:root[data-dz-appearance="raw"] [data-dz-owned="materializer"],:root[data-dz-appearance$="-transparent"] [data-dz-owned="materializer"]{display:none!important}`10301090 const roles = `${themed} [data-dz-role="canvas"]:not([data-dz-owned="materializer"]){display:none!important}${themed} [data-dz-fill="none"]{fill:none!important}${themed} [data-dz-fill="surface-1"]{fill:var(--dz-surface-1)!important}${themed} [data-dz-fill="surface-2"]{fill:var(--dz-surface-2)!important}${themed} [data-dz-fill="surface-3"]{fill:var(--dz-surface-3)!important}${themed} [data-dz-fill="ink"]{fill:var(--dz-ink)!important;color:var(--dz-ink)!important}${themed} [data-dz-fill="ink-muted"]{fill:var(--dz-ink-muted)!important;color:var(--dz-ink-muted)!important}${themed} [data-dz-fill="line"]{fill:var(--dz-line)!important}${themed} [data-dz-fill="accent-1"]{fill:var(--dz-accent-1)!important}${themed} [data-dz-fill="accent-2"]{fill:var(--dz-accent-2)!important}${themed} [data-dz-fill="accent-3"]{fill:var(--dz-accent-3)!important}${themed} [data-dz-fill="accent-4"]{fill:var(--dz-accent-4)!important}${themed} [data-dz-fill="on-accent"]{fill:var(--dz-on-accent)!important;color:var(--dz-on-accent)!important}${themed} [data-dz-stroke="line"]{stroke:var(--dz-line)!important}${themed} [data-dz-stroke="line-muted"]{stroke:var(--dz-line-muted)!important}${themed} [data-dz-stroke="accent-1"]{stroke:var(--dz-accent-1)!important}${themed} [data-dz-stroke="accent-2"]{stroke:var(--dz-accent-2)!important}${themed} [data-dz-stroke="accent-3"]{stroke:var(--dz-accent-3)!important}${themed} [data-dz-stroke="accent-4"]{stroke:var(--dz-accent-4)!important}`1031- return `${variables}${trnVariables}${materializerVisibility}${roles}`1091+ return `${variables}${trnVariables}${squaringVariables}${materializerVisibility}${roles}${squaringRoleCss(themed)}`10321092 }10331093 10341094 function canonicalBounds(root) {