Header menu logo issie

Making sheets from a program

A design sheet can be written as data — a list of components and a list of logical connections — and turned into a .dgm (or an .ldgm library component) that Issie opens and a person can read. No geometry is written by hand and no part of Issie has to be running.

Two modules, split by what they depend on:

src/Renderer/Common/SheetDescription.fs

the description: plain data, no draw block, no Fable

src/Renderer/DrawBlock/SheetLayout.fs

realisation: port resolution, placement, output

A sheet

open SheetDescription
open SheetDescription.Operators      // see the warning about ==> below

describeSheet "adder" [
    comp "A"   (Input1(4, None))
    comp "B"   (Input1(4, None))
    comp "ADD" (NbitsAdderNoCinCout 4)
    comp "S"   (Output 4)
] [
    "A"       ==> "ADD/P"
    "B"       ==> "ADD/Q"
    "ADD/SUM" ==> "S"
]

A component's name is its label unless compLabelled gives a different one. A port is written COMPONENT/PORT, where PORT is matched case- and space-insensitively against the names Issie gives that component type and otherwise read as an index — the index is not a convenience, since gates, inputs, outputs, wire labels, constants, bus selects and splitters have no port names at all. PORT may be left off when the component has exactly one port in the direction wanted. From is always the driver.

Declaration order fixes the sheet's port order. CanvasExtractor.getOrderedCompLabels sorts a sheet's I/O by position, and that order becomes the signature every Custom instance is checked against. The layout places Inputs and Outputs in declaration order for exactly this reason.

==> is opt-in because it is also Fable.Core.JsInterop's object-literal operator and FsCheck's implication. Open SheetDescription.Operators only in a file that opens neither; connect is the same function and is always safe.

Parameters

describeSheet "adder" comps conns
|> withParam "W" 6 "width of the two operands, in bits"
|> withSlot "A"   (IO "A")  "W"
|> withSlot "ADD" Buswidth  "W"
|> withSlot "S"   (IO "S")  "W-1"

Expressions are read by ParameterTypes.parseExpression — the properties-pane parser — so they mean here what they mean typed into a properties box. The generated sheet carries both the resolved integer in the component and the expression in ParameterDefinitions, which is what Issie itself writes.

Four things are refused rather than silently skipped: a slot naming an undeclared parameter, an expression that will not parse, an expression that will not evaluate, and a component that has no such slot. That last one is ComponentSlots.slotApplies, and it is why a GateN or a MergeN cannot be parameterised at all — their integer is an input count, and a parameter records a value, not a change of shape. A SplitN is the mixed case: its number of outputs is a shape, but the width and bit position of a given output are values, so SplitNWidth i/SplitNLSB i work for the outputs that exist and are refused past the end. A memory has two slots rather than one, since neither of its widths is "the" width: withSlot "ROM" MemoryAddressWidth "clog2(WORDS)" and withSlot "ROM" MemoryWordWidth "W". Its contents are not parameterisable and are never resized — data that no longer fits the widths a design gives it is reported when that design is simulated.

Layout

Inputs go in a left column and outputs in a right column, in declaration order. Everything else is placed by recursive bisection: build the graph with edge weights counting connections, split into roughly equal halves minimising cut weight, alternate the split axis, and place leaves a component apart on the 30px grid Issie snaps to.

Wires are written with no vertices. BusWireUpdate.LoadConnections checks whether each saved end vertex is near the actual port and routes from scratch when it is not, so opening the sheet runs smartAutoroute on every wire and then the global separation pass. Wire creation is the draw block's, deferred to load — there is nothing to route at build time and nothing that could route it, since that needs a populated SymbolT.Model.

Component ids are <sheet>-<name> rather than uuids. Ids must be unique per project, which the sheet prefix gives, and readable ids make a generated file possible to read and diff.

Output, from .NET

SheetLayout.toCanvasState  sheet                              // Result<CanvasState, string>
SheetLayout.saveSheet      folder sheet                       // one .dgm
SheetLayout.saveProject    folder [sheet1; sheet2]            // .dgm per sheet, plus the .dprj
SheetLayout.saveLibraryComponent libPath description deps sheet   // .ldgm, plus its dependencies

All four run under plain .NET — dotnet run, a script, a test — with no Electron and no Issie process. Three things had to be true for that, and now are:

Each side therefore reads what the other writes, in both directions. SheetDescriptionTests.fs writes an .ldgm this way and reads it back, with nothing running.

Verifying a generated sheet

npm run test covers port resolution, error messages, non-overlap, I/O order, block ordering, simulation and a save/reload round trip. For anything about how it looks, open it and use scripts/inspect-canvas.js — see inspectingTheCanvas.md.

union case Option.None: Option<'T>

Type something to start searching.