dsh-model-everything 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en-US.md +75 -19
- package/README.md +377 -345
- package/cordis.patch.yml +9 -0
- package/diagram/package.json +5 -0
- package/lib/client.js +3494 -0
- package/lib/diagram-diff-tool.js +90 -0
- package/lib/diagram-render-tool.js +102 -0
- package/lib/diagram-review-tool.js +99 -0
- package/lib/diagram.js +43 -0
- package/lib/gate.js +14 -12
- package/lib/presentation.js +10 -0
- package/lib/render/budget.js +99 -0
- package/lib/render/channels.js +243 -0
- package/lib/render/collapse.js +155 -0
- package/lib/render/components.js +99 -0
- package/lib/render/coords.js +288 -0
- package/lib/render/cycle-break.js +147 -0
- package/lib/render/diff.js +138 -0
- package/lib/render/dummy.js +43 -0
- package/lib/render/from-mermaid.js +235 -0
- package/lib/render/from-plantuml.js +430 -0
- package/lib/render/index.js +42 -0
- package/lib/render/layer.js +91 -0
- package/lib/render/layout.js +68 -0
- package/lib/render/model.js +17 -0
- package/lib/render/network-simplex.js +272 -0
- package/lib/render/order.js +122 -0
- package/lib/render/paint.js +891 -0
- package/lib/render/palette.js +127 -0
- package/lib/render/place.js +482 -0
- package/lib/render/route.js +225 -0
- package/lib/render/svg.js +229 -0
- package/lib/render/symbol.js +108 -0
- package/lib/render/text.js +74 -0
- package/lib/render/viewmodel.js +126 -0
- package/lib/render/wrap.js +87 -0
- package/lib/render/write.js +14 -0
- package/lib/scan.js +248 -8
- package/lib/structure-tool.js +2 -2
- package/lib/types/client.d.ts +83 -0
- package/lib/types/diagram-diff-tool.d.ts +8 -0
- package/lib/types/diagram-render-tool.d.ts +8 -0
- package/lib/types/diagram-review-tool.d.ts +8 -0
- package/lib/types/diagram.d.ts +34 -0
- package/lib/types/presentation.d.ts +7 -0
- package/lib/types/render/budget.d.ts +55 -0
- package/lib/types/render/channels.d.ts +63 -0
- package/lib/types/render/collapse.d.ts +23 -0
- package/lib/types/render/components.d.ts +57 -0
- package/lib/types/render/coords.d.ts +64 -0
- package/lib/types/render/cycle-break.d.ts +45 -0
- package/lib/types/render/diff.d.ts +44 -0
- package/lib/types/render/dummy.d.ts +32 -0
- package/lib/types/render/from-mermaid.d.ts +27 -0
- package/lib/types/render/from-plantuml.d.ts +33 -0
- package/lib/types/render/index.d.ts +38 -0
- package/lib/types/render/layer.d.ts +28 -0
- package/lib/types/render/layout.d.ts +54 -0
- package/lib/types/render/model.d.ts +73 -0
- package/lib/types/render/network-simplex.d.ts +54 -0
- package/lib/types/render/order.d.ts +27 -0
- package/lib/types/render/paint.d.ts +127 -0
- package/lib/types/render/palette.d.ts +41 -0
- package/lib/types/render/place.d.ts +55 -0
- package/lib/types/render/route.d.ts +77 -0
- package/lib/types/render/svg.d.ts +19 -0
- package/lib/types/render/symbol.d.ts +73 -0
- package/lib/types/render/text.d.ts +38 -0
- package/lib/types/render/viewmodel.d.ts +20 -0
- package/lib/types/render/wrap.d.ts +27 -0
- package/lib/types/render/write.d.ts +14 -0
- package/lib/types/scan.d.ts +10 -2
- package/lib/types/structure.d.ts +1 -0
- package/package.json +148 -132
- package/skills/model-everything/SKILL.md +454 -454
- package/skills/model-everything/references/architecture-domains.md +137 -0
- package/skills/model-everything/references/architecture-views.md +119 -0
- package/skills/model-everything-diagram/SKILL.md +84 -0
- package/src/client.ts +307 -0
- package/src/diagram-diff-tool.ts +94 -0
- package/src/diagram-render-tool.ts +108 -0
- package/src/diagram-review-tool.ts +109 -0
- package/src/diagram.ts +61 -0
- package/src/gate.ts +14 -12
- package/src/presentation.ts +12 -0
- package/src/render/budget.ts +135 -0
- package/src/render/channels.ts +309 -0
- package/src/render/collapse.ts +150 -0
- package/src/render/components.ts +126 -0
- package/src/render/coords.ts +328 -0
- package/src/render/cycle-break.ts +164 -0
- package/src/render/diff.ts +162 -0
- package/src/render/dummy.ts +65 -0
- package/src/render/from-mermaid.ts +261 -0
- package/src/render/from-plantuml.ts +469 -0
- package/src/render/index.ts +73 -0
- package/src/render/layer.ts +98 -0
- package/src/render/layout.ts +101 -0
- package/src/render/model.ts +86 -0
- package/src/render/network-simplex.ts +290 -0
- package/src/render/order.ts +139 -0
- package/src/render/paint.ts +1006 -0
- package/src/render/palette.ts +164 -0
- package/src/render/place.ts +527 -0
- package/src/render/route.ts +269 -0
- package/src/render/svg.ts +270 -0
- package/src/render/symbol.ts +137 -0
- package/src/render/text.ts +86 -0
- package/src/render/viewmodel.ts +136 -0
- package/src/render/wrap.ts +103 -0
- package/src/render/write.ts +17 -0
- package/src/scan.ts +807 -564
- package/src/structure-tool.ts +2 -2
- package/src/structure.ts +1 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text as the layout sees it: measured, broken into lines, and turned into a box.
|
|
3
|
+
*
|
|
4
|
+
* Mermaid and Graphviz both measure a label in the platform's own text engine and then lay out around that
|
|
5
|
+
* measurement - a node's size is its text plus padding, and a label too long for its maximum width is wrapped
|
|
6
|
+
* before anything is placed. There is no DOM here, so the measurement is an estimate over a per-character width
|
|
7
|
+
* table; what matters is that the *order* is the same: text first, geometry second. A renderer that fixes the box
|
|
8
|
+
* and pours text into it cannot help overflowing.
|
|
9
|
+
*
|
|
10
|
+
* Explicit breaks (`<br>`, `<br/>`, `<br />`, a real newline) are honoured as written, because a source that put
|
|
11
|
+
* one there wanted that line to end.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Average advance per character, in the drawing's units, at the 11px font the writer emits. */
|
|
15
|
+
const CHAR_WIDTH = 6.6
|
|
16
|
+
|
|
17
|
+
/** A line longer than this wraps on a space, or breaks in the middle when it has none. */
|
|
18
|
+
export const MAX_LINE_CHARS = 30
|
|
19
|
+
|
|
20
|
+
/** Height of one line of box text, and the breathing room around the block. */
|
|
21
|
+
export const LINE_HEIGHT = 15
|
|
22
|
+
export const TEXT_PADDING = 12
|
|
23
|
+
|
|
24
|
+
/** The smallest and largest box a label may produce. */
|
|
25
|
+
export const MIN_NODE_WIDTH = 88
|
|
26
|
+
export const MAX_NODE_WIDTH = 260
|
|
27
|
+
|
|
28
|
+
/** `<br>`, `<br/>`, `<br />` and a real newline all end a line. */
|
|
29
|
+
const BREAK = /<br\s*\/?>|\r?\n/gi
|
|
30
|
+
|
|
31
|
+
/** The lines a label asks for, before wrapping: an explicit break is a line end, not a space. */
|
|
32
|
+
export function explicitLines(label: string): string[] {
|
|
33
|
+
return label.split(BREAK).map((line) => line.trim())
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Wrap one line to the maximum width.
|
|
38
|
+
*
|
|
39
|
+
* A word longer than the limit is broken rather than allowed to overflow: a single token with no spaces is the
|
|
40
|
+
* common case in identifiers, and letting it run off the box is the defect this function exists to prevent.
|
|
41
|
+
*/
|
|
42
|
+
export function wrapLine(line: string, limit: number = MAX_LINE_CHARS): string[] {
|
|
43
|
+
if (line === '') return ['']
|
|
44
|
+
const out: string[] = []
|
|
45
|
+
let current = ''
|
|
46
|
+
for (const word of line.split(/\s+/).filter((part) => part !== '')) {
|
|
47
|
+
const candidate = current === '' ? word : current + ' ' + word
|
|
48
|
+
if (candidate.length <= limit) {
|
|
49
|
+
current = candidate
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
if (current !== '') out.push(current)
|
|
53
|
+
if (word.length <= limit) {
|
|
54
|
+
current = word
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
let rest = word
|
|
58
|
+
while (rest.length > limit) {
|
|
59
|
+
out.push(rest.slice(0, limit))
|
|
60
|
+
rest = rest.slice(limit)
|
|
61
|
+
}
|
|
62
|
+
current = rest
|
|
63
|
+
}
|
|
64
|
+
if (current !== '') out.push(current)
|
|
65
|
+
return out.length === 0 ? [''] : out
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface Measured {
|
|
69
|
+
lines: string[]
|
|
70
|
+
width: number
|
|
71
|
+
height: number
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The text block a label produces: its lines, and the box it needs. */
|
|
75
|
+
export function measure(label: string): Measured {
|
|
76
|
+
const lines = explicitLines(label).flatMap((line) => wrapLine(line))
|
|
77
|
+
const longest = lines.reduce((max, line) => Math.max(max, line.length), 0)
|
|
78
|
+
const width = Math.min(MAX_NODE_WIDTH, Math.max(MIN_NODE_WIDTH, longest * CHAR_WIDTH + TEXT_PADDING * 2))
|
|
79
|
+
const height = lines.length * LINE_HEIGHT + TEXT_PADDING
|
|
80
|
+
return { lines, width, height }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Rough width of a single-line piece of text, for placing an edge label without a box. */
|
|
84
|
+
export function textWidth(text: string): number {
|
|
85
|
+
return text.length * CHAR_WIDTH
|
|
86
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a tool card shows, reduced to a drawing.
|
|
3
|
+
*
|
|
4
|
+
* The input is what the host hands a settled tool view: a phase and a block whose shape differs per phase.
|
|
5
|
+
* A settled call carries the paired call's arguments as a **lazy view** (`PartialArguments`) and the result
|
|
6
|
+
* as content blocks, so nothing here reads `block.args.model`: the view is asked, and the report text is
|
|
7
|
+
* parsed for the two facts a drawing needs. Every reader is total - an absent field removes a shape rather
|
|
8
|
+
* than throwing, because the same view renders during streaming.
|
|
9
|
+
*
|
|
10
|
+
* Pure. Everything decided here is a fact a test can assert: which nodes exist, which edges, why they are
|
|
11
|
+
* coloured the way they are.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const record = (value: unknown): Record<string, unknown> | undefined =>
|
|
15
|
+
typeof value === 'object' && value !== null && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined
|
|
16
|
+
|
|
17
|
+
const array = (value: unknown): unknown[] => (Array.isArray(value) ? value : [])
|
|
18
|
+
|
|
19
|
+
const text = (value: unknown): string | undefined => (typeof value === 'string' && value !== '' ? value : undefined)
|
|
20
|
+
|
|
21
|
+
/** The report text a settled call carries, joined: a card reads words, not blocks. */
|
|
22
|
+
function reportText(content: unknown): string {
|
|
23
|
+
const parts: string[] = []
|
|
24
|
+
for (const entry of array(content)) {
|
|
25
|
+
const block = record(entry)
|
|
26
|
+
const body = text(block?.text)
|
|
27
|
+
if (body !== undefined) parts.push(body)
|
|
28
|
+
}
|
|
29
|
+
return parts.join(String.fromCharCode(10))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Two facts out of this plugin's own report format: the verdict, and the first error it named. */
|
|
33
|
+
function readReport(content: unknown): { verdict?: string; code?: string; message?: string } {
|
|
34
|
+
const body = reportText(content)
|
|
35
|
+
if (body === '') return {}
|
|
36
|
+
const verdict = /verdict=(\w+)/.exec(body)?.[1]
|
|
37
|
+
const named = /\[error\/([A-Z0-9_]+)\]\s*(.*)/.exec(body)
|
|
38
|
+
if (named !== null) return { verdict, code: named[1], message: named[2] }
|
|
39
|
+
const first = /first error:\s*([A-Z0-9_]+)/.exec(body)
|
|
40
|
+
return first === null ? { verdict } : { verdict, code: first[1] }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The `model` argument out of the host's lazy arguments view.
|
|
45
|
+
*
|
|
46
|
+
* A settled call exposes `value(key)`; a plain object (a test, or an older shape) is read directly. The
|
|
47
|
+
* view answers `undefined` until the field closes, which is exactly when a drawing should be attempted.
|
|
48
|
+
*/
|
|
49
|
+
function readModel(args: unknown): Record<string, unknown> | undefined {
|
|
50
|
+
const view = record(args)
|
|
51
|
+
if (view === undefined) return undefined
|
|
52
|
+
const read = view.value
|
|
53
|
+
if (typeof read === 'function') return record((read as (key: string) => unknown)('model'))
|
|
54
|
+
return record(view.model)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The states of a model: a machine, or nothing when the call carried none. */
|
|
58
|
+
function machineOf(model: Record<string, unknown> | undefined): { nodes: DiagramNode[]; edges: DiagramEdge[]; init?: string } | undefined {
|
|
59
|
+
if (model === undefined) return undefined
|
|
60
|
+
const states = array(model.states).map(record).filter((entry): entry is Record<string, unknown> => entry !== undefined && typeof entry.id === 'string')
|
|
61
|
+
if (states.length === 0) return undefined
|
|
62
|
+
const edges = array(model.transitions)
|
|
63
|
+
.map(record)
|
|
64
|
+
.filter((entry): entry is Record<string, unknown> => entry !== undefined && typeof entry.from === 'string' && typeof entry.to === 'string')
|
|
65
|
+
.map((entry) => ({ from: String(entry.from), to: String(entry.to), label: text(entry.event) ?? '' }))
|
|
66
|
+
return {
|
|
67
|
+
nodes: states.map((state) => ({ id: String(state.id), role: 'normal' as const })),
|
|
68
|
+
edges,
|
|
69
|
+
...(typeof model.init === 'string' ? { init: model.init } : {}),
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The caption: the verdict, then the first error's code and message when the report named one. */
|
|
74
|
+
function captionOf(report: { verdict?: string; code?: string; message?: string }): string {
|
|
75
|
+
const verdict = report.verdict ?? 'no report yet'
|
|
76
|
+
if (report.code === undefined) return verdict
|
|
77
|
+
const message = report.message ?? ''
|
|
78
|
+
return verdict + ' — ' + report.code + (message === '' ? '' : ': ' + message)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Which nodes the report blames.
|
|
83
|
+
*
|
|
84
|
+
* A finding names a state inside its message as `"x"`; matching that is a heuristic, so it only ever *adds*
|
|
85
|
+
* emphasis and never decides the verdict.
|
|
86
|
+
*/
|
|
87
|
+
function blame(nodes: DiagramNode[], report: { message?: string }): DiagramNode[] {
|
|
88
|
+
if (report.message === undefined) return nodes
|
|
89
|
+
const message = report.message
|
|
90
|
+
// Whole words only: a report does not quote the state, and a state called `do` must not match `done`.
|
|
91
|
+
const named = nodes
|
|
92
|
+
.filter((node) => {
|
|
93
|
+
const escaped = node.id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
94
|
+
return new RegExp('(?<![A-Za-z0-9_])' + escaped + '(?![A-Za-z0-9_])').test(message)
|
|
95
|
+
})
|
|
96
|
+
.map((node) => node.id)
|
|
97
|
+
if (named.length === 0) return nodes
|
|
98
|
+
return nodes.map((node) => (named.includes(node.id) ? { ...node, role: 'error' as const } : node))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
import type { Diagram, DiagramEdge, DiagramNode } from './model.js'
|
|
102
|
+
|
|
103
|
+
export interface ViewInput {
|
|
104
|
+
phase?: unknown
|
|
105
|
+
toolName?: unknown
|
|
106
|
+
/** The whole block the host hands a view; its shape depends on the phase. */
|
|
107
|
+
block?: unknown
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function viewModel(input: ViewInput): Diagram {
|
|
111
|
+
const block = record(input.block)
|
|
112
|
+
const title = text(block?.name) ?? text(input.toolName) ?? 'tool'
|
|
113
|
+
const report = readReport(block?.content)
|
|
114
|
+
// A shape that still carries a parsed report object is read from there; it is what older callers pass.
|
|
115
|
+
const parsed = record(block?.result)
|
|
116
|
+
const findings = array(parsed?.findings).map(record)
|
|
117
|
+
const firstError = findings.find((finding) => finding?.severity === 'error')
|
|
118
|
+
const merged = {
|
|
119
|
+
verdict: text(parsed?.verdict) ?? report.verdict,
|
|
120
|
+
...(report.code === undefined && typeof firstError?.code === 'string' ? { code: firstError.code } : { code: report.code }),
|
|
121
|
+
...(report.message === undefined && typeof firstError?.message === 'string' ? { message: firstError.message } : { message: report.message }),
|
|
122
|
+
}
|
|
123
|
+
const caption = captionOf(merged)
|
|
124
|
+
const machine = machineOf(readModel(block?.args))
|
|
125
|
+
if (machine === undefined) {
|
|
126
|
+
return { kind: 'none', title, nodes: [], edges: [], caption }
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
kind: 'machine',
|
|
130
|
+
title,
|
|
131
|
+
...(machine.init === undefined ? {} : { init: machine.init }),
|
|
132
|
+
nodes: blame(machine.nodes, merged),
|
|
133
|
+
edges: machine.edges,
|
|
134
|
+
caption,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wrapping: turning one endless column into a page.
|
|
3
|
+
*
|
|
4
|
+
* A chain of twelve boxes has twelve layers, and layering alone draws it as a single column taller than anything a
|
|
5
|
+
* reader can take in - a strip, not a drawing. Graphviz and ELK both answer this with a wrapping pass: cut the layer
|
|
6
|
+
* sequence into bands and stand the bands side by side, so reading order becomes "down the page, then the next
|
|
7
|
+
* column", the way text wraps.
|
|
8
|
+
*
|
|
9
|
+
* The cut is chosen, not guessed: each candidate number of bands is scored by how close the packed page gets to the
|
|
10
|
+
* aspect ratio a page and a landscape screen share, and the best one wins. A drawing that is already close to that
|
|
11
|
+
* shape is left alone - wrapping exists for the strip, not for every tall drawing.
|
|
12
|
+
*
|
|
13
|
+
* Pure: layer sizes in, band ranges and offsets out. The caller applies them.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** The aspect ratio a page and a landscape screen share. */
|
|
17
|
+
const PAGE_ASPECT = 1.6
|
|
18
|
+
|
|
19
|
+
/** A drawing shorter than this many layers is read better stacked, whatever its proportions. */
|
|
20
|
+
const MIN_LAYERS_TO_WRAP = 4
|
|
21
|
+
|
|
22
|
+
/** A drawing that is not this tall relative to its width is not a strip. */
|
|
23
|
+
const STRIP_RATIO = 2
|
|
24
|
+
|
|
25
|
+
export interface BandRange {
|
|
26
|
+
/** The first and last layer the band holds, inclusive. */
|
|
27
|
+
first: number
|
|
28
|
+
last: number
|
|
29
|
+
/** How far right the band stands, relative to the first one. */
|
|
30
|
+
offsetX: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface BandPlan {
|
|
34
|
+
bands: BandRange[]
|
|
35
|
+
width: number
|
|
36
|
+
height: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The height of a run of layers with the gap between them. */
|
|
40
|
+
function runHeight(layerHeight: number[], first: number, last: number, gap: number): number {
|
|
41
|
+
let total = 0
|
|
42
|
+
for (let index = first; index <= last; index += 1) {
|
|
43
|
+
total += layerHeight[index]
|
|
44
|
+
if (index < last) total += gap
|
|
45
|
+
}
|
|
46
|
+
return total
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The width of a run of layers: the widest layer sets it. */
|
|
50
|
+
function runWidth(layerWidth: number[], first: number, last: number): number {
|
|
51
|
+
let widest = 0
|
|
52
|
+
for (let index = first; index <= last; index += 1) widest = Math.max(widest, layerWidth[index])
|
|
53
|
+
return widest
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** How far the packed page is from the target shape, in log space so half and double count the same. */
|
|
57
|
+
function shapeError(width: number, height: number, aspect: number): number {
|
|
58
|
+
if (width <= 0 || height <= 0) return Infinity
|
|
59
|
+
const ratio = Math.max(width, height) / Math.min(width, height)
|
|
60
|
+
return Math.abs(Math.log(ratio / aspect))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function planBands(layerWidth: number[], layerHeight: number[], gap: number, aspect: number = PAGE_ASPECT): BandPlan {
|
|
64
|
+
const count = layerWidth.length
|
|
65
|
+
if (count === 0) return { bands: [], width: 0, height: 0 }
|
|
66
|
+
const single: BandPlan = {
|
|
67
|
+
bands: [{ first: 0, last: count - 1, offsetX: 0 }],
|
|
68
|
+
width: runWidth(layerWidth, 0, count - 1),
|
|
69
|
+
height: runHeight(layerHeight, 0, count - 1, gap),
|
|
70
|
+
}
|
|
71
|
+
if (count < MIN_LAYERS_TO_WRAP || single.height <= single.width * STRIP_RATIO) return single
|
|
72
|
+
|
|
73
|
+
const columnGap = gap * 2
|
|
74
|
+
let best = single
|
|
75
|
+
let bestError = shapeError(single.width, single.height, aspect)
|
|
76
|
+
for (let bands = 2; bands < count; bands += 1) {
|
|
77
|
+
const base = Math.floor(count / bands)
|
|
78
|
+
const extra = count % bands
|
|
79
|
+
const ranges: Array<{ first: number; last: number }> = []
|
|
80
|
+
let cursor = 0
|
|
81
|
+
for (let band = 0; band < bands; band += 1) {
|
|
82
|
+
const size = base + (band < extra ? 1 : 0)
|
|
83
|
+
ranges.push({ first: cursor, last: cursor + size - 1 })
|
|
84
|
+
cursor += size
|
|
85
|
+
}
|
|
86
|
+
let width = columnGap * (bands - 1)
|
|
87
|
+
let height = 0
|
|
88
|
+
for (const range of ranges) {
|
|
89
|
+
width += runWidth(layerWidth, range.first, range.last)
|
|
90
|
+
height = Math.max(height, runHeight(layerHeight, range.first, range.last, gap))
|
|
91
|
+
}
|
|
92
|
+
if (shapeError(width, height, aspect) >= bestError) continue
|
|
93
|
+
const planned: BandRange[] = []
|
|
94
|
+
let offsetX = 0
|
|
95
|
+
for (const range of ranges) {
|
|
96
|
+
planned.push({ first: range.first, last: range.last, offsetX })
|
|
97
|
+
offsetX += runWidth(layerWidth, range.first, range.last) + columnGap
|
|
98
|
+
}
|
|
99
|
+
best = { bands: planned, width, height }
|
|
100
|
+
bestError = shapeError(width, height, aspect)
|
|
101
|
+
}
|
|
102
|
+
return best
|
|
103
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writing a drawing to disk: the CLI's job, kept out of the renderer.
|
|
3
|
+
*
|
|
4
|
+
* `render/index.ts` is bundled into the page as well as run in Node, so it may not import a filesystem;
|
|
5
|
+
* everything that touches one lives here, where only the host reaches it.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { writeFileSync } from 'node:fs'
|
|
9
|
+
import { render, type RenderInput, type Rendered } from './index.js'
|
|
10
|
+
import type { Palette, PaletteOptions } from './palette.js'
|
|
11
|
+
|
|
12
|
+
/** Write the drawing where a document can link to it. Returns the path, the byte count and the render. */
|
|
13
|
+
export function writeSvg(input: RenderInput, path: string, overrides: Partial<Palette> = {}, options: PaletteOptions = {}): { path: string; bytes: number; rendered: Rendered } {
|
|
14
|
+
const rendered = render(input, overrides, options)
|
|
15
|
+
writeFileSync(path, rendered.markup, 'utf8')
|
|
16
|
+
return { path, bytes: rendered.markup.length, rendered }
|
|
17
|
+
}
|