Docs · Using the kit
Put a device in your own page.
Portrayal compiles each device into ordinary files: an SVG per face,
and JSON indexes beside them. Everything below runs here, live, and the
code beside each result is the file that just ran it. This page documents
@portrayal/kit 0.51.0.
Start here
A compiled device is one SVG per face (<device>.<config>.<view>.svg)
plus devices.json, components.json and a
<device>.configs.json and a <device>.source.json per device. The kit,
@portrayal/kit, is plain ES modules with no build step; three.js
is needed only for 3D. A face that several configurations draw identically is
written once, so find a configuration's face with faceFile(index, config, view)
from @portrayal/kit/dist rather than building its name; each device's
default configuration is also at <device>.<view>.svg. The recipes
below use the UfiSpace S9600-72XC.
Beside every face, under the same name, is
<device>.<config>.<view>.elements.json: the
face's elements as data, each with its path, class, part reference, box
and connection points, so you can read a face's tree without parsing or
laying out the SVG. It is read from the SVG beside it and adds nothing;
its format is in Portrayal's
format-stability.md.
It comes with a build of library/dist/ (see
Getting the files); portrayal.dev doesn't serve it.
The drawing is the API
No kit at all: fetch a face, insert it, and address its parts by
data-path. A lamp is lit by a state class the drawing
already styles; a click tells you which part it hit.
Color it from your data
A port is an element like any other, so your monitoring can paint it. Here the numbers are a table in the file; in a dashboard they are whatever your telemetry returns.
Read the device from the drawing
Each device publishes its source manifest once, as
<device>.source.json: the device, its chassis in
millimeters, its configurations. Every face names that file by its
SHA-256 in <metadata>, so a drawing can be checked
against the source it was drawn from. And devices.json
lists everything in the library.
Seat an optic or a cable end
A cage is empty until you say what's in it. Name the part for the cage, and for an optic its two LC bores; the kit draws each part's own artwork into the face and refuses anything the cage doesn't accept.
In 3D
The same files, as a model you can turn over. The viewer takes the same
paths and lamp states as the drawing (setStates; a state on a
switched outlet reaches the lamp bound to it, as it does in 2D), the same
text on parts (setFields), and exports GLB and USDZ
(download). createViewer(el, {ears: true}) or
setEars(true) adds the generic rack ears described below, and a
GLB taken then carries them. It needs three.js; the page's import map says
where that lives.
Rack ears
A published face has no rack ears: a device is drawn between its ear
folds. @portrayal/kit/ears2d draws the generic L-bracket over the
face you already hold, so a page can offer the ears as a choice: off by
default in a device view, on in a rack. earPlan(configs, frontWidth)
is the plan the 3D viewer builds from, and it is null where the device
gets none: a front as wide as the rack (482.6 mm), whose ears are built into
the face and drawn already, such as a blanking panel; a device that is not
rack-mounted; or ears stated behind. The ear is silver
(#c8cacc) unless the device states chassis.ears.color.
drawEars(svg, plan, view) grows the viewBox to hold them and
clearEars puts it back. The overlay is a picture, not a part: it has
no data-path and takes no clicks, and an export of the live drawing
carries it.
An outlet and its lamp
A switched PDU's outlet declares the states on and
off, and the lamp that shows them is a part beside it, bound to
the outlet with for:. Set a state on the outlet's path and it is
shown on its lamp. @portrayal/kit/states has the one rule every
place that applies a state uses: boundLamps(svg) maps each outlet's
path to its lamps, and expandStates(map, bindings) turns a map keyed
by outlets into one that reaches their lamps too (a lamp stated in the map
itself keeps its own). A marks document, the Explorer's state chips and
viewer.setStates all apply it, so 2D and 3D agree.
off can be lit: offIsSet(el, lamps) says whether an
off chip sets state-off (an outlet, so its lamp shows the
color its device declares for off, or a power outlet with no lamp, which
is dimmed) or clears the element, as it does on a plain lamp. The module
also reads a lamp's vocabulary (statesOfEl) and whether a state
paints anything (paints).
A display is a window the drawing shows, and what it shows is a rack file's to say.
@portrayal/kit/displays has the rules: a display of a part says its
kind (pixel, character or segment), and
contentRefusal(display, content) says why {text: [lines]},
{digits: "04"} or {image: dataUrl} does not fit it. One painter,
paintDisplays(root, displays), draws the text, picture or digits for the 2D
drawing and for the 3D texture alike (shell.setDisplays,
viewer.setDisplays); loadDisplayImages reads a library picture
first. A segment display's digits are the state- class of each cell.
Annotate and export
Describe what to mark, not how to draw it: a document of selectors,
colors, lamp states and labels. A state on a switched outlet lights the
lamp bound to it as well. The same document previews on the page,
exports as an SVG you can drop into Word (toSvg), or as a PNG
or GIF (toPng, toGif), and fits in a share URL
(encode).
The Explorer shell
If you want the whole explorer rather than parts of it, mount the shell. It styles the page it lands in, so it runs here in a frame of its own. The Explorer is the full example, with 3D.
One load holds the stage at a time. When a newer
loadDevice or loadStage overtakes one you
awaited, yours writes nothing and rejects with an error that
isSuperseded(err) recognizes: another load replaced it,
which is not a failure. Annotate treats it
that way.
A part that slides
A device may declare a part that slides, such as a DIN rail panel on
slotted brackets (since 0.19.0). The compiled drawing says what moves: the
root of every face carries data-adjustments, and each member
node data-moves-with and data-moves-by. The FS
DINRAIL2U and DINRAIL4U declare one, rail-setback, how far
the rail panel stands behind the ears.
A position is a field. It is held at the path of the
adjustment's carrier, under the adjustment's id, in the map every other
field is in, so a fields= link carries it and the 3D viewer
takes it with setFields. A value is a number in mm or the
name of a stop; what is kept is the number, rounded to 0.1 mm. A value
that is not a position is not applied, and setFields answers
why in a sentence (it returns {refused: [...]} for every call
since 0.19.0). An empty value puts the part back where the drawing was
built.
The control (since 0.20.0). positionControl(shell,
opts) in positions is the control
the Explorer mounts above its
tree: for each adjustment of the device on screen, a slider over the
range in steps of 0.1 mm, a number box, a button for each named stop and
a reset. It writes through shell.setFields and holds no rule
of its own. A view that cannot show the motion names the views that do.
The page says which stage is showing by emitting mode on the
shell; in 3D each position is a scene to rebuild, so the value is written
when the slider is let go.
import {positionControl} from '@portrayal/kit/positions';
// shell: the shell mounted above
positionControl(shell, {threeD: () => true}); // {el, refresh}; opts.before: where it goes
shell.emit('mode', '3d'); // the page says which stage is showing
shell.setFields('rail-panel', {'rail-setback': 'rear'}); // a stop, or a number in mm
// ?fields=rail-panel~rail-setback~271.2
shell.setFields('rail-panel', {'rail-setback': '300'});
// {refused: [{path, key, value, reason: 'rail-setback on DINRAIL2U takes 51.2 to 271.2 mm. 300 is outside it.'}]}
A rack, by commands
The rack/ modules are the Rack Builder's
rules without its page. They read no DOM and fetch nothing but the
rack catalog (rack.json) and the cable types, so a rack
is checked the same way in a browser, in node and by an agent. Each is
imported on its own, as @portrayal/kit/rack/<module>.
createRackEditor({doc, chassisOf}) holds a rack document and
takes every edit as a named command through apply:
place, move, patch, remove,
attach, detach, frame, rename,
dcim, fit, field, the side.*,
zerou.*, cable.* and bundle.* commands. A batch
lands whole or is refused with the reason; each batch is one step for
undo and redo, and on('change') hears every
one. COMMANDS in rack/commands names each command's
arguments as JSON Schema, which is what an agent reads.
preview answers what a batch would do without doing it.
To read a rack back, describe(rack, ctx) is the whole rack as
text (or one stretch of a long one, with {section, offset, limit}),
and inspect(rack, id, ctx) is one device, cable, part beside
the rack or bundle as data. selectCables, freeUs,
fitsAt and catalog answer what a command needs to know first.
Beside the rack
A part that stands beside the rack and takes no rack unit, such as a
vertical cable manager, is an entry of rack.zeroU, not an item:
zerou.place {ref, at, ru} stands it at an upright (left or
right on a two-post, left-front to right-rear on a
four-post), and zerou.update and zerou.remove move and remove
it. The gutter beside that upright then runs through it, so a routed
length is measured through the duct. A zero-U PDU carries no lane: the
lane beside it runs just outboard of it, and a cable is taken round it. A finger bracket narrower than the
opening can stand on one rail: side.place and side.set.
Routes and ring marks
resolveRoute gives the waypoints a cable follows;
routePath(rack, cable, ctx) is that route in millimeters, each
D-ring expanded to where the cable enters and leaves it, and it is what
routedLength, fill and the capacity checks measure. Each ring is
decided once, there. ringMarks(rack, cable, ctx) hands those
decisions to a drawing, one per waypoint, so the drawing passes through
each ring the way it was measured. A mark's sense is along the rack's
axes (x right as seen from the front, y up); a drawing whose axis runs
the other way turns it with orientMarks, and a path drawn from
its other end with reverseMarks:
import {resolveRoute, ringMarks, orientMarks, reverseMarks} from '@portrayal/kit/rack/route';
import {routed2d} from '@portrayal/kit/rack/route-path';
const marks = ringMarks(rack, cable, ctx); // parallel to resolveRoute(...).waypoints
const front = orientMarks(marks, {y: -1}); // an SVG front elevation: y runs down
const rear = orientMarks(marks, {x: -1, y: -1}); // the rear, seen mirrored
const back = reverseMarks(front); // the same path, drawn from end b
const d = routed2d(points, 4, front); // an SVG path through each ring, straight
The ctx comes from the drawings: guidesOf(itemId), the
rings, ducts and pass-throughs on a part, and portX(end), a port's
x in millimeters from the rack's center line; with chassisOf from
rack/catalog, and, best, diameterOf from
loadCableTypes(dist), each cable's own diameter. The recipe above
uses a plain one, which routes by the gutters alone. A page that draws
the plugs gives plugReachOf(end, cable) as well, and the cable
types' typeOf and bendOf (below).
Build a new context when what it reads changes. Since 0.17.0 the kit keeps what it decides for a cable (the side of a route, where its turns are given room) per rack, context and cable, because every reader of a path asks for it again and the search is the costly part. A context changed in place can therefore give a path placed for what it read before. When a device moves, a cable's type changes, a port is swapped or a plug's reach is read, make a new context object and route with that; freeze it if you want a stray write to throw.
A ring holds a cord that only reaches into it. A route that
would enter and leave a ring by one face is a hook, not drawn through the
ring and reported by ringFindings. Since 0.12.0 there is one
exception: when the ring stands right past the port next to it (its near
face no further from that port than the ring's depth plus the cable's
diameter, the diameter counted at most up to the depth), the cord reaches
in to be held. That ring is a pass with held: true on the path's
ring, it is counted, and it is not a finding. Its mark is a pass like any
other, so a drawing needs nothing new for it.
The automatic route runs a patch whose two ends leave through one manager along that manager, port to port, through the rings between the two ports, and never out to a gutter and back; with no ring between, through the ring nearest the middle. Any other route takes the gutter chosen from both ends: the side both ports stand on, or, when they stand on opposite sides, the side whose path is the shorter. Routed lengths change in 0.12.0 wherever that applies.
Solid bodies
Every body in the rack is solid to a route (since 0.11.0).
solidsOf(rack, ctx) in rack/solids places each one in rack
millimeters: a box device is its envelope; a sheet part, such as a lacer
panel, is the plates its rack.json entry derives
(solids: its tray floor, webs and ears), with the pass-throughs
that cut them; a zero-U PDU is its envelope. A cable crosses a body only
through a ring, a duct's finger gap, or a pass-through its diameter fits.
Where a straight leg would cross one, routePath adds detour points
(at: 'detour', listed in path.detours): over the body's
near edge, a tray's front edge first, else round its end, else by a side
lane. They count in the length and are never stored, so a drawing that
joins only the waypoints is shorter than what was measured, and goes
through the tray; draw the detours too. Beside a zero-U PDU the lane runs
outboard of it: place lanes with laneXAt(rack, lane, ru, chassisOf).
A leg the rules cannot clear is left as drawn and reported:
bodyFindings(rack, ctx, nameOf) gives each as
{kind: 'crosses-body', cable, item, part, between, at, text}, beside
ringFindings. It warns and never refuses. inspect of a cable
lists them as route.crosses, describe adds a
Findings: line when given the route context, and
cableScheduleRows(..., {bodies}) writes each sentence into its
cable's notes:
import {routePath, bodyFindings, laneXAt} from '@portrayal/kit/rack/route';
import {solidsOf} from '@portrayal/kit/rack/solids';
import {loadCableTypes} from '@portrayal/kit/rack/cable-types';
const {diameterOf} = await loadCableTypes(dist);
const route = {chassisOf, guidesOf, portX, diameterOf};
const path = routePath(rack, cable, route); // points include {at: 'detour'}
const bodies = solidsOf(rack, route); // [{item, part, box: {x0, x1, y0, y1, z0, z1}}]
for (const f of bodyFindings(rack, route)) console.warn(f.text);
Reach, resting and solid rings
The plug's reach (since 0.13.0). A cable leaves the far
end of its plug, not the port face, so each path starts and ends at a
reach point (at: 'reach', with its end):
the port's point moved straight out of its face by the plug's reach. The
page gives the reach as ctx.plugReachOf(end, cable), in mm, read
off the seated plug (the z of relief.js cablePoints, an
optic's standing-out included); without it the kit takes
PLUG_REACH by media (27.6 mm for LC fibre, 39.4 for copper, 64.8
for a DAC or an AOC). A drawing that starts its tube there and runs its
first leg from there follows the measured leg, round whatever it has to
clear.
Trays and resting (since 0.14.0). A tray is a pathway with
a floor: traysOf(rack, ctx) in rack/solids places each one,
and a route names it by its id as it names a ring. A cable through a ring
on a tray lies on the ring's sill, its radius above the opening's lowest
inside edge, at the side nearer the rail; along a tray it lies on the floor
(at: 'tray'); and every leg nothing holds is a free span that
hangs by its family's drape and lands on any surface below it
(at: 'rest'). Give ctx.typeOf and ctx.bendOf from
loadCableTypes, so a span hangs by its own type and no tighter than
its installed bend radius. path.rests, and inspect's
route.rests, list what a cable lies on: a ring's sill, a tray (its
face and role) or a body a span came to rest on.
Rings are solid (since 0.15.0). Where a ring's contract says
what of its loop is solid, solidsOf places its legs, bar, seat and
hook as ring/<id>/<part> with the other solids. A cable
passes a ring close by on purpose, so a ring's part is met by the cable's
tube (the part grown by its radius), where any other body is met by the
center line. Every pass through a ring whose opening is placed starts and
ends at an approach point on the ring's run, clear of its
band (at: 'approach'), so a detour or a leg from behind or below
ends there and the cable enters through the opening. A drawing that
keeps its tube out of the solids should take a ring's part grown by the
tube's radius too.
import {routePath} from '@portrayal/kit/rack/route';
import {loadCableTypes} from '@portrayal/kit/rack/cable-types';
const {diameterOf, typeOf, bendOf} = await loadCableTypes(dist);
const route = {chassisOf, guidesOf, portX, portY, diameterOf, typeOf, bendOf,
plugReachOf: (end, cable) => reachOfPlugSeatedAt(end)}; // mm, or null for the default
const path = routePath(rack, cable, route);
// path.points: a, reach, [lead | approach, entry, exit, approach | tray | rest | detour | lane]..., reach, b
// path.rests: [{kind: 'ring', item, via} | {kind: 'tray', item, via, face, role} | {kind: 'body', item, part}]
The end allowance and room for the bends
The end allowance (since 0.16.0). A routed length counts,
beside the path, what is inside the port at each end, by the cable's
media. END_ALLOWANCE_BY_MEDIA is that table in meters an end
(its name since 0.18.0), and since rack file version 4 it holds the part
of the plug inside the port and nothing else: 0.0131 for LC fiber, 0.0095
for copper and a cable with no media, 0 for a DAC and 0.0524 for an AOC.
endAllowance(cable) is that figure and routePath
returns it as allowance. The maker's short tolerance and the
0.1 m an end of dressing the table and the allowance once held are named
on their own now, shortTolerance and END_PLAY_M
(below): a page that took the dressing as
endAllowance less the table gets 0.
Room for the bends (since 0.17.0). The path is the cable,
so its corners are its bends, and routePath places its points
for the cable's own installed bend radius (ctx.bendOf(cable),
else its media's). Where the route turns onto a ring's run the approach
point stands the bend radius from the band; a cable whose port stands
behind that point is led square to it through a lead point
(at: 'lead', with the ring's item and via),
and one whose first turn needs more leg than its plug gives runs straight
on out of it to a lead point with its end. A detour stands two
bend radii from each end (detour's room option), and
a free span hangs only as deep as the corners beside it have room for.
Draw a lead point as any other point of the path. Routed lengths grew in
0.17.0 on every routed cable that turns at a ring, goes round a body or
hangs.
What the kit's rules find no room for is still routed, and is said. The
path's bends lists each corner short of the radius, as
{kind: 'tight-bend', cable, point, between, at, angle_deg, legs_mm,
room_mm, need_mm, short_mm}, and
bendFindings(rack, ctx, nameOf) gives them for the whole rack
with a sentence, as bodyFindings does for crossings. It warns
and never refuses. at is the corner in rack millimeters, so a
page can mark it where it is. The measure is cornersOf(points,
{share: 'need'}) in rack/route-path: the room a corner's
legs leave it, a leg between two corners shared by what each turn uses of
it. Since 0.21.0 inspect of a cable carries them as
route.bends, describe counts them in its
Findings: line, and cableScheduleRows takes them
as bends for a cable's notes. Since 0.27.0 two turns on
one short leg are one finding, with apart_mm and
turns: [{point, at, angle_deg}, {point, at, angle_deg}]; its
at is the middle of the leg between them, so a page that marks
each corner marks turns[].at. Its sentence reads “turns
twice within 22.8 mm” and is longer than a single bend's, so do not
clip it at a fixed length. A finding for a lead put into a ring too close
for it also carries advice (one sentence) and
tooClose, and its text is the bend alone.
The ring is chosen by room (since 0.27.0). Of the rings on a lead's way to its gutter the first that the lead can turn into within its cable's bend radius is taken, and one too close is passed over; a patch along one manager takes the rings between its ports the same way. Where no ring leaves room the nearest stands and its finding says so. Routed lengths on a saved rack can move: a cable whose automatic route takes another ring is measured differently the next time a page measures it, and its stock size can change. An entered length and a route made by hand are not touched.
A span in front of other ports (since 0.21.0). With no
horizontal manager a cable runs from its lane to its port across the faces
between. routePath returns covers, each port such
a span passes in front of, and coverFindings(rack, ctx, nameOf)
gives one finding a cable, with the ports by device and the advice to add a
horizontal cable manager. It warns and never refuses. A context that
supplies portBoxesOf(itemId), every port of a device as
[{path, view, x, y, w?, h?}] in the frame portX and
portY use, has every port judged; without it only the ports
that hold a cable are, and the sentence says so. Since 0.21.0 a cable is
also laid from both ends and one lay kept, so a reader on the context
(plugReachOf, trayFaceOf, bendOf,
diameterOf) can be handed the same cable with a
and b exchanged: key what it answers on the end, not on
which of the two the cable calls a.
import {routePath, pathLength, endAllowance, END_ALLOWANCE_BY_MEDIA, bendFindings} from '@portrayal/kit/rack/route';
import {cornersOf} from '@portrayal/kit/rack/route-path';
const path = routePath(rack, cable, route); // route: a context built for the rack as it is now
const length = pathLength(path); // {value, need, measured}: the stock size, the need, and the need plus the end play
path.allowance === endAllowance(cable); // END_ALLOWANCE_BY_MEDIA[cable.media]: what is inside the port
for (const b of path.bends) console.warn(b.cable, b.at, b.room_mm, b.need_mm);
for (const f of bendFindings(rack, route, id => labelOf(id))) console.warn(f.text);
const corners = cornersOf(path.points, {share: 'need'}); // [{angle_deg, legs_mm, room_mm}]
Rack file version 4
The rack file is version 4. A version 1, 2 or 3 file opens
as it was and is saved as version 4; a reader older than this refuses a
version 4 file. Its schema is
/schemas/v3/rack.schema.json.
The reader keeps a key it does not know at every level of the file.
Half units. A position (ru on an item) is a
whole unit or a half, counted bottom-up from 1: 12 is the bottom of U12
and 12.5 its middle. rack/fit owns what follows from one:
unitsOf(chassis), a device's height in halves;
spanOf(item, chassisOf), the half-open [lo, hi) it
fills; itemBox(rack, item, chassisOf), {y0, y1, h, lo,
hi} in mm above the bottom of the rails, the body centered in its
span, always, which is where a drawing stands it; and
spanText, the span in words ("the lower half of U7").
heightOf, the u of itemsWithU and
freeUs answer in halves, and positionOf and
uText in rack/model give "U12.5". span,
the closed whole-unit pair, is removed. A device of a whole number of
units is refused at a half position, in a sentence: its ears meet no hole
there. holesIn(lo, hi) in rack/rails gives the
rail holes an ear between two heights can use, for a page that draws
ears.
The lengths of a cable, named (rack/route). The
need is the path, what is inside the two ports and the maker's
short tolerance (needOf, shortTolerance); the
stock is the length bought; the spare is stock less
need; the end play is the part of the spare that stays in the
curves into the ports, END_PLAY_M (0.1 m) an end; and what is
left is to store. spareOf(rack, cable, ctx) gives all of them
in meters. pathLength answers need beside
value and measured, and
stockLength(need) still chooses a cord up to
BREAK_TOL_M (10 mm) short of full end play. A cable keeps
length.need where it kept measured.
Where the spare is (rack/slack). A cable takes
slack, a list of where its spare is stored, in a tray it rests
on or at an end (cable.slack, cable.slack.clear).
slackOf resolves it against the route, storedText
words it, slackFindings says what is wrong
(spare-unplaced, spare-short,
spare-over, spare-off-route),
spareLine is one line for a rack, and
spareExports(rack, ctx) is what the exports take:
cableScheduleRows fills its spare and
stored columns from {spare} and its last column,
straps, from {straps: strapTexts(rack, ctx)}.
Spare is recorded and listed, not drawn, and the kit places none itself.
The frame and what stands in it. A frame takes
startU, the number of its first unit, and setback;
an item takes setback, roll (90, 180 or 270
degrees clockwise as seen from its face) and heldBy.
setbackOf(rack, item, chassisOf) is how far a faceplate stands
behind its rail: the item's, else the device's default ear position, else
the frame's, else flush. facePoint turns a point of a face as
drawn into the rack, through face, turned and
roll. frameNotices(rack, chassisOf) gives a sentence
for each key the kit keeps and does not yet act on.
States and readings (rack/states). An item
and a part beside the rack take states and
readings (state.set, readings.set).
statesFor(entry, facts) is the {path: state} map
to hand a drawing, an outlet's state carried to its lamps;
readingsFor is {source, at, badges}; and
stateNotices(rack, ctx) names what is kept and not applied.
A zero-U PDU hangs on brackets with zerou.hang, and
hangNotices in rack/zero-u says what is wrong
with one. A page calls the notice functions once it has loaded the parts
lists; the editor cannot, since it holds none.
import {itemBox, spanOf, spanText, unitsOf, fits} from '@portrayal/kit/rack/fit';
import {holesIn} from '@portrayal/kit/rack/rails';
import {spareOf, END_PLAY_M} from '@portrayal/kit/rack/route';
import {spareExports} from '@portrayal/kit/rack/slack';
const upper = {...item, ru: 12.5}; // a half-unit panel in the upper half of U12
spanOf(upper, chassisOf); // [12.5, 13)
const box = itemBox(rack, upper, chassisOf); // {y0, y1, h, lo, hi} in mm above the bottom of the rails
holesIn(box.lo, box.hi); // the one rail hole that half holds
fits(rack, {ref: 'a-1u-switch', face: 'front', ru: 12.5}, chassisOf).reason;
// "... is 1U; its ears meet the rail holes only at a whole unit, and U12.5 is a half unit."
const s = spareOf(rack, cable, route); // {need_m, stock_m, spare_m, end_play_m, to_store_m, pinned}
const spare = spareExports(rack, route); // {byCable, line, bom}: hand it to the exports as {spare}
The rack modules
| module | what it does |
|---|---|
rack/catalog | loadCatalog(dist): the rack catalog and the chassisOf(ref) lookup everything else takes; loadSlots(dist, refs): the bays, cages and parts fit and field check against. |
rack/model | The rack file: newDoc, parseDoc, serialize, and edits that return a new rack (withItem, withFrame, ...). Files are version 4 since 0.22.0; older ones are read as they were and saved as version 4. positionOf, uText, firstU, rollOf, holderOf. |
rack/editor | createRackEditor: a rack document edited by commands, with undo, redo and change events. |
rack/commands | Every edit as a named, validated command (COMMANDS), and apply for a batch. |
rack/history | Undo and redo as snapshots, as the editor keeps them. |
rack/queries | Reading a rack: describe, inspect, selectCables, fitsAt, freeUs, catalog, freePorts, suggestMedia, looseEnds. |
rack/rails | The rails: the rack unit (44.45 mm), the opening, and where a device sits between them. |
rack/fit | Whether a device fits at a unit, on a face and on one rail; whether a zero-U part fits beside the rack (fitsZeroU); how a shrink trims a rack. |
rack/zero-u | The parts beside the rack: where each stands, as words (whereText) and as x (zeroUX), and each placeable entry (zeroUEntries). |
rack/managers | Cable managers: the placement of one onto the device behind it, and moving one. |
rack/cable-rules | Which two ports may be joined, withCable, media and lengths. Since 0.30.0 a power cord, a DC feed or a ground wire can land (cableLandsOn: a part of class port, or a connector slot that accepts one; media power and ground; an end may state its plug). A page that offers power cabling supplies ctx.drawnAt first, or an outlet-to-outlet cord between two devices gets no warning. Since 0.26.0 a manager's stated count is compared only with the cables at least as thick as the kind it counts (capacity.media in rack.json; a catalogue built before it is compared with cables of 6 mm or more), and portLabel words a port. |
rack/route | resolveRoute, routePath (with each plug's reach point, each ring's approach points, each lead point, a tray's stretch and each free span's rest, and rests, bends and allowance), trayOf, PLUG_REACH, endAllowance, END_ALLOWANCE_BY_MEDIA, routedLength, pathLength, ringMarks, orientMarks, reverseMarks, laneXAt, pathway fill, ringFindings, bodyFindings and bendFindings. |
rack/solids | The solid bodies a route may not pass through: solidsOf (since 0.15.0 with a ring's legs, bar, seat and hook, ring/<id>/<part>), legCrossings, detour (since 0.17.0 with room, far enough out for the bends at each end), CLEAR, the clearance a detour keeps, and traysOf, the trays a cable lies on. |
rack/route-path | The 2D path through the rings (routed2d) and the geometry under it (throughRings, and cornersOf, the room each corner of a path leaves a bend). |
rack/cable-geometry | The 3D path (routePoints3d), cable and plug colors, and the lead and sag figures the drawings share. |
rack/export-data | The rack as rows: bomRows, cableScheduleRows (with a bundle column, and each body a route crosses in its cable's notes), the bundles as the exports read them (bundleExports, bundleNotes, strapBomRows, strapBomNotes), the device-import data, and the drawing sheet's title block (titleLines, which names the host it was drawn with as source). |
rack/dcim-rules | What a NetBox or Nautobot import needs of a rack: cable types, cable and module import rows (a NetBox cable in a bundle names it first in its description), and the import kit's README (kitReadme; a host passes its own source, where its DCIM settings are as settingsAt, and the script's scriptUrl). |
rack/slack | Where a cable's spare is stored: slackOf, storedText, slackFindings, spareLine, spareBomLine, and spareExports, which the exports take. |
rack/displays | What a display in the rack shows: displaysOf (the displays of a placed device or part), displaysFor (the {path: content} map to hand a drawing), displayRefusal, displayNotices and displayList; display.set sets or clears one. |
rack/states | What an entry is set to: statesFor (the map to hand a drawing), readingsFor and stateNotices. |
rack/validate | validate(schema, value), the small JSON Schema validator the commands use, and same. |
Bundles
Cables that share part of their route can be combed into a bundle and
held with hook-and-loop straps. bundle.create, bundle.add,
bundle.peel, bundle.update and bundle.remove make
and change them, each one undo step. Given the routing context
({ctx: {route}}), bundle.create works the trunk out from
where its cables run together (deriveTrunk), refusing a fork, a
loop or groups that share nothing, by name; a member's own route then
follows the trunk (followTrunk).
bundleCheck is the size check: the bundle's diameter,
sqrt(sum(d²) / 0.8), against the smaller of 63.5 mm and each
pathway's opening. It warns and never refuses. Each cable's diameter
comes from its type: loadCableTypes(dist) reads
cable-types.json and returns diameterOf, bendOf (the
installed minimum bend radius) and typeOf. straps places the
straps along the trunk, kept off rings.
Straps are placed on the members' routed paths (since 0.28.0).
A trunk used to be measured along the straight line from one waypoint to the
next; the members do not run there, so a strap's {segment, t}
named a place off the cables. straps and trunkLength
now read the members' paths: t is the share of the segment's
routed length, and each strap carries its own place, point
(rack millimeters), dir (the way the trunk runs there) and
on (the cable whose path the point is on). The lengths and the
counts follow, so a saved rack's strap lines move. Where no path
can be read, straps() and bundleExports say so with
straight, or few where a minority of the riders have
one, and the length and count are an estimate.
A one-waypoint trunk is a tie (since 0.51.0): cables that share
one ring and nothing else are held by one strap round the pack at the ring,
of kind: 'tie', its width from the members' diameters (19 mm, or 25
mm for a pack 25 mm across or more). trunkLength gives
{metres: 0, tie: true}, and bundleExports gives
length_m: null with tie and tie_at.
With fewer than two cables riding there is no strap.
Given bendOf as well, the check also looks at the bend: at each
corner of the trunk, and at each pathway that states a radius, against the
largest bend radius among the members there, so one fiber makes a bundle as
strict as fiber. A corner's room is worked out from its legs and marked
estimated. A miss is a warning, never a refusal. A member with no radius is
listed as unchecked in the check's notes, never passed. inspect
gives a bundle's bend as {radius_mm, by, unchecked, points,
violations}; it is null only without the routes.
cornersOf and bendCheck are exported for a page that
draws them (cornersOf lives in rack/route-path since
0.17.0, and rack/bundles still exports it). A cable outside a
bundle, and a member's lead to its port, are not checked here: each
cable's own path is, by routePath's bends and
bendFindings.
For the exports, bundleExports(rack, ctx) in
rack/export-data measures every bundle once: its members, length,
strap count, size and bend. Pass it to the cable schedule as
cableScheduleRows(rack, ends, items, routes, {bundles}), whose notes
then carry a line per bundle (bundleNotes); its bundle
column names each cable's bundle. strapBomRows(bundles) is the bill
of materials' one hook-and-loop strap line, every bundle's straps summed, and
strapBomNotes says which bundles' straps could not be counted.
Getting the files
Two things: the kit's modules, and the compiled files they read.
The kit is on npm as
@portrayal/kit.
three.js is a peer dependency, needed only for 3D:
npm install @portrayal/kit three@0.161
This page documents kit 0.51.0, which is
not on npm yet: npm's latest is 0.2.0.
The rack and bundle modules, and everything else added since 0.2.0, are
in the unpublished version only, so npm install gets a kit
without them, and the CDN map below returns 404s. Until 0.51.0 is
published, use the files from a
Portrayal checkout's
kit/ folder, or this site's own vendored copy
(vendor/*.js and vendor/rack/*.js), and point the
import map at wherever you serve them.
With a bundler, import @portrayal/kit/viewer3d and the
rest by name; the package's exports resolve them. Without one, the
modules are plain ES modules with no build step, so an import map
pointing at a CDN is enough. List each module you use, pinned to a
version:
<script type="importmap">
{ "imports": {
"@portrayal/kit/dist": "https://cdn.jsdelivr.net/npm/@portrayal/kit@0.51.0/dist.js",
"@portrayal/kit/swap": "https://cdn.jsdelivr.net/npm/@portrayal/kit@0.51.0/swap.js",
"@portrayal/kit/viewer3d": "https://cdn.jsdelivr.net/npm/@portrayal/kit@0.51.0/viewer3d.js",
"@portrayal/kit/marks": "https://cdn.jsdelivr.net/npm/@portrayal/kit@0.51.0/marks.js",
"@portrayal/kit/shell": "https://cdn.jsdelivr.net/npm/@portrayal/kit@0.51.0/shell.js",
"@portrayal/kit/ears2d": "https://cdn.jsdelivr.net/npm/@portrayal/kit@0.51.0/ears2d.js",
"@portrayal/kit/states": "https://cdn.jsdelivr.net/npm/@portrayal/kit@0.51.0/states.js",
"@portrayal/kit/positions": "https://cdn.jsdelivr.net/npm/@portrayal/kit@0.51.0/positions.js",
"@portrayal/kit/rack/catalog": "https://cdn.jsdelivr.net/npm/@portrayal/kit@0.51.0/rack/catalog.js",
"@portrayal/kit/rack/fit": "https://cdn.jsdelivr.net/npm/@portrayal/kit@0.51.0/rack/fit.js",
"@portrayal/kit/rack/rails": "https://cdn.jsdelivr.net/npm/@portrayal/kit@0.51.0/rack/rails.js",
"@portrayal/kit/rack/slack": "https://cdn.jsdelivr.net/npm/@portrayal/kit@0.51.0/rack/slack.js",
"@portrayal/kit/rack/model": "https://cdn.jsdelivr.net/npm/@portrayal/kit@0.51.0/rack/model.js",
"@portrayal/kit/rack/editor": "https://cdn.jsdelivr.net/npm/@portrayal/kit@0.51.0/rack/editor.js",
"@portrayal/kit/rack/queries": "https://cdn.jsdelivr.net/npm/@portrayal/kit@0.51.0/rack/queries.js",
"@portrayal/kit/rack/route": "https://cdn.jsdelivr.net/npm/@portrayal/kit@0.51.0/rack/route.js",
"@portrayal/kit/rack/route-path": "https://cdn.jsdelivr.net/npm/@portrayal/kit@0.51.0/rack/route-path.js",
"@portrayal/kit/rack/solids": "https://cdn.jsdelivr.net/npm/@portrayal/kit@0.51.0/rack/solids.js",
"@portrayal/kit/rack/bundles": "https://cdn.jsdelivr.net/npm/@portrayal/kit@0.51.0/rack/bundles.js",
"@portrayal/kit/rack/export-data": "https://cdn.jsdelivr.net/npm/@portrayal/kit@0.51.0/rack/export-data.js",
"@portrayal/kit/rack/cable-types": "https://cdn.jsdelivr.net/npm/@portrayal/kit@0.51.0/rack/cable-types.js",
"three": "https://cdn.jsdelivr.net/npm/three@0.161.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.161.0/examples/jsm/"
} }
</script>
The 2D recipes don't need three.js at all.
The compiled files are built, not downloaded. Clone
Portrayal and run
./build.sh, which writes every device into
library/dist/. The files are static, so serve that folder
beside your page and set DIST in each recipe to where it is.
The compiled files are not on npm yet. Publishing them through a registry, split per device, is roc-ops/Portrayal #526 and #528; this section will change when that lands.