anatomy
Build crafted, highly detailed, interactive isometric SVG figures that explain a technical idea as a real physical object, with precise solid parts, four-tone shading, painter's order, a framed card with live readout, and calm eased motion—for docs, landing pages, and interactive explainers (framework-free or React/Next.js, with optional in-perspective WebGL).
Install
npx skills add https://github.com/wheresryan22/anatomy --skill anatomyKit API
Everything is in kit/iso-kit.ts, with the same API in kit/iso-kit.mjs. World coordinates are [x, y, z], with x and y on the floor and z up, in units of about 1 px. Screen coordinates are [sx, sy] in the SVG viewBox. Paths come back as SVG d strings, rounded to 0.1.
Contents
- Types
- Camera
- Solids
- Lines on faces
- Parts library
- Moving things
- SVG strings (framework-free)
- Card and page
- React components
- Round parts on any axis (
kit/lathe.mjs) - Pipes, tubes and cables (
kit/tube.mjs) - The audit (
kit/audit.mjs) - The WebGL layer (
kit/gl.mjs) - Tools
- Turning groups (
kit/turn.mjs,turn-build.mjs,turn-audit.mjs)
1. Types
2. Camera
3. Solids
All solids are prisms: a floor outline extruded straight up.
What a SolidPaths contains:
fill: paper-coloured, it hides whatever is behind.shades[0..3]: visible sides, bucketed by how they face the light.top: the top face, lighter.crease: where top meets the visible sides.bevel: optional.outline: the silhouette.
solidSvg and <Solid> paint them in that order.
4. Lines on faces
5. Parts library
6. Moving things
Rotations about z can't be done with a screen transform: rebuild the rotating paths for each frame or step. That is cheap for a needle, a pointer or a small part.
7. SVG strings (framework-free)
8. Card and page
9. React components
From kit/react/draw.tsx, which is server-safe:
<Solid paths tone crease halo lit flat className inner />(inneris line markup placed under the outline, as forsolidSvg)<Line d tone dotted dashed flow free /><Wire from to via tone dotted dashed flow ends size /><Dots points size tone pulse />(a point may carry opacity as[x, y, alpha], as fordotsSvg)<FaceText transform size tone anchor>text</FaceText><IsoFigure width height label>…svg children…</IsoFigure><Plate fig title hint readout keys caption>…figure…</Plate>
From kit/react/live.tsx, which is "use client":
useReadout()returns{ anchor, say }. Putanchoron your root element;say(text)writes the card's readout.useLoop(tick: (dt, calm) => keepGoing, running)runs a frame loop whilerunningand whiletickreturns true. It returnskick()to restart it.follow(current, target, dt, seconds, calm)is exponential easing.spring(state, target, dt, stiffness, damping, calm)is a sub-stepped spring. It returns true when settled.useEased(target, seconds)returns an eased number as React state. Fine for small figures.useInView(ref),useStillness(),asksForStillness().Stageis the wrapperIsoFigureuses. It addsdata-inviewfor CSS animations.
10. Round parts on any axis (kit/lathe.mjs)
kit/iso-kit.mjs builds prisms that stand on the floor. kit/lathe.mjs turns a profile around any axis, for bells, chambers, flanges, valve bodies, pump volutes, pipe fittings, actuators lying at an angle, and connectors on a vertical face. It returns the same SolidPaths as slabOf, so solidSvg paints them. Import it as import * as G from "../../kit/lathe.mjs".
Frames. A frame is { o, a, u, v }: an origin, the unit axis, and two unit radial directions. A point on the part is (s, r, angle): s along the axis from o, r from the axis, and angle from u toward v.
Solids. A meridian is a list of [s, r] points along the axis. Add a third element ([s, r, 1]) to mark a smooth ring, so no crease line is drawn there. Use that for curved profiles such as a bell or a dome. A meridian with any smooth vertex is shaded smoothly on its own (smooth defaults to "auto", which is softOf(meridian)); pass smooth: false to opt out, true or { rows } to force it on a profile without smooth vertices, and { slope } or { slope, radius } for an analytic curve.
Smooth shading. Without it every facet row gets one flat tone, and a curved profile shows a staircase of tone blocks (the first Raptor bell and throat did, and the reviewer saw it at once). With smooth the side of each meridian segment is shaded from an interpolated normal: the tone boundaries become the true iso-light curves, sampled at rows levels and emitted as one polygon per tone region, so they run as clean curves across the whole part.
smooth: trueinterpolates each run of smooth vertices ([s, r, 1]) with a monotone cubic Hermite curve (Fritsch–Carlson slopes: zero where the profile turns, a weighted harmonic mean elsewhere), and uses that one curve for the normals, the tone sheets, the silhouette and the hull, resampled until it is within 0.2 px of the chord. Shading and outline then describe the same shape, and the normal can only change sign where the profile does. Use it for any profile with smooth vertices: domes, throats, cones, valve bulges, a flame cone. (A natural spline overshoots near a peak; on a valve bulge it zig-zagged the tone bands across the body.)smooth: { slope }takes the normal from an analyticdr/ds, so a body cut into bands shares one normal field and the tone curves cross the band joints without a step. With aslopealone the profile points are joined straight, so sample the analyticr(s)densely (20–30 knots on a throat), and the silhouette of a waisted body uses the sameslope, so the outline follows the true curve too.smooth: { slope, radius }also takes the surface from an analyticr(s): the tone sheets, the hull and the silhouette all followradiusbetween the knots, so two knots are enough for a band. This is whatbandsOfpasses to every band.- Hand-typed knots are not a curve. The monotone Hermite is only C1: its
dr/dsis a piecewise quadratic that plateaus at every knot and overshoots between them, so tone bands stall and then lurch, a staircase in scallops. The Raptor throat ([128, 31, 1], [134, 27.6, 1], …) showed it at 5×. Any curved body larger than a fitting gets an analytic profile andsmooth: { slope }: the throat isr = √(24.2² + (c·(s − 146))²), the flame coner = 48·(1 − x)^0.8closed tor = 0at the tip, the valve bulge an ellipse, the gimbal puck18.4 − a·|s − 297.5|⁴. The audit'swobblelists every profile smoothed without aslopewhosedr/dsturns between its ends. rows(default 6) is the starting sampling along each segment. Rows are then split until every tone boundary is withinerror(0.35 px) of a straight chord, and wherever the number of bands changes down todepth(8) levels or tofineness(0.1 px) on screen, whichever comes first, so a band that appears or disappears is born at zero width in a sliver instead of ending on a straight cut. Each tone is then emitted as one polygon for as long as its own number of regions holds, across every meridian segment and smooth knot of the body, and only that tone is bridged where its count changes; a band that comes and goes no longer cuts the other tones into rows. That made a small turned ball about half the size and removed the row seams. Visibility uses the drawn facet's normal, so the tones always reach the outline; every tone polygon is wound the same way, so overlapping pieces of one tone never cancel into a hole.- The result is marked
seamless(seesolidSvg). Flat caps (rings wheresdoesn't change) keep their facet shading.
Lines and dots on round surfaces. These draw only what faces the viewer. slope is dr/ds of the surface at that ring, so a line on a cone or bell hides where the real surface turns away.
Paths in 3D, for pipes and cables.
Camera and vectors.
viewOf(P)is the unit direction to the viewer, sodot3(p, viewOf(P))equalsdepthOf(p, P).lightOf(P)is the kit's light.scaleOf(P)is the screen units per world unit.toneOf(normal, P)andbandOf(score)give the four-tone bucket.hullOf(points)andrunsOf(flags)are the hull and run helpers the module uses itself.- The vector helpers are
dot3,add3,sub3,mul3,len3,unit3,cross3andlerp3.
A long curved body in bands. Cut it with bandsOf so each band depth-sorts against the pipes that wrap it. Every band shares the body's steps, its normal field and its surface, so tone curves cross every joint without a step, and the outline comes from sidesOf as one curve instead of a chord per band that kinks at every joint. The Raptor bell:
Don't build bands by hand from separate bandOfProfile calls on two-knot chords with their own Hermite fits: each band then has its own end slopes, its tones step at every joint, and its chord outline kinks.
Known traps.
- Four-tone quantisation follows the slope of each facet row when a body isn't smoothed. Mark every curved knot smooth (
[s, r, 1]) and the shading follows on its own; a profile of hard knots that still describes a curve needssmooth: true. UsebandsOfon a body cut into bands. - Smooth shading is heavier. A small turned ball with
smoothcosts about 9 KB;sphereOfcosts under 3 KB. UsesphereOffor balls and keeprowslow on small parts.
11. Pipes, tubes and cables (kit/tube.mjs)
The fills for .tb-body, .tb-shine and .tb-shade are part of ISO_CSS (and kit/iso.css), so tubeSvg works on any kit page with no extra CSS. Override them in page CSS only to restyle tubes, as the Raptor does for selected parts (.it[data-on] .tb-body).
Depth-sorting tubes. A pipe runs between parts at many depths, so draw each piece at its own depth: put it in your item list with key G.dot3(piece.mid, G.viewOf(P)), next to the solids it passes. Use short pieces (maxLength 6–18) where a pipe weaves between neighbours, and long ones (400) for a ring that lies flat on the floor. To split a pipe between SVG layers, test piece.mid. The ripple tank's motor cable goes three ways: pieces over the bridge (piece.mid[2] > Z.top - 1 && piece.mid[1] < IN.y + IN.d - 10) to the front SVG before the bar, the rest above the rim to the front SVG after the near caps, and the run down the leg to the control box to the middle SVG. Its lamp cable sends the pieces that hang over the base edge (piece.mid[1] < BASE.y - 0.2) to the back SVG. In a figure as dense as the Raptor, don't tune these keys by eye: record the pieces and let orderKeys (section 12) settle them.
Anything that wraps a pipe splits it. A collar, band, bellows disc, clamp or valve body is wider than the pipe it sits on. If the pipe runs through it as one chunk, the chunk is both behind and in front of the collar and no key can order them: the collar either hides the pipe's near side or the pipe paints over the collar's face. Gap the pipe at the collar (gaps: [[at - t/2, at + t/2]]): the far piece ends at the collar's back face, the near piece starts at its front face, and both order cleanly. The Raptor's pipe() does this for its own end flanges, bands, bellows and valve, and cable() for the harness clamps via crossingsOf.
The Raptor's pipe() helper is a good pattern:
filletthe control points with a bend of 2.6 × r (at least 3). A valve or bellows needs a straight leg at least as long as its body plus both fillet cuts (r_bend · tan(turn / 2)each); put it on a bend and its ends cut into the pipe.- Work out the collars first (end flanges, bands, bellows discs, the valve body) and pass their spans as
gaps. tubePiecesthe route, recording each chunk with itspointsandradii, and each drawn run as a tube for the audit.- Draw the collars, owned by the pipe (
owner), so the audit doesn't count a pipe's own flange as a collision.
cable() adds a terminal dot at an end that stays with its own assembly and a connector shell (shells: [start, end]) at an end that lands on another one, so every cable still ends on something when the figure comes apart. Both helpers take free: [start, end] for an end that comes off its mount when the figure is apart, and fillet with { P, tube: r } (see fillet). A line that must leave a port facing the viewer starts from an elbow (a ball on the boss, rim on sphere at both joints, the line leaving it toward its next point), so the turn happens inside a fitting. pipe() puts each end flange's back face on the route end and can end a line in a coupling (a union nut at a quick-disconnect) instead of a flange.
12. The audit (kit/audit.mjs)
The one-line hook. Make a recorder, let every drawing helper put its items and record its shapes into it, settle the keys, and end the build with auditOrExit:
node build.mjs --audit then prints the report and exits non-zero on any failing line; without the flag auditOrExit returns at once. A figure that doesn't come apart passes no order and every explode check reduces to the rest pose.
A detailed figure fails in ways the eye misses until a reviewer zooms in: a pipe or cable that passes through something, at rest or in any frame of the explode, a flange that floats off its mount, a part that hangs in the air or is sunk into its neighbour, a route that doubles back or pinches to a point on screen, a bend or end that lands on another line's outline, a cable end that dangles when the figure comes apart, and two things drawn in the wrong order where they overlap. The audit measures all of them from the real geometry. On the Raptor (478 items, 1,119 depth states, about 4,400 replayed and prefix frames for clearance) the keys take about a minute on every build and a full --audit about three minutes in all, most of it the per-frame clearance.
Shapes. Record a shape for everything you draw, in world units, as you build it:
meta carries name (used in reports, and matched by prefix: "nozzle.bell*"), owner (a pipe's own flanges, bands and valve), touch (names a tube may touch on purpose, such as the clamps round a harness), bundle (cables in one harness may lie side by side, and a clamp carries its harness's bundle), joined (runs of one gapped pipe), limp (a cable: it holds nothing up), anchor: false (a clamp: an end may not count it as its mount), fitting (a clamp or collar whose centre crowding checks against other lines), seated (a pad or rim-on-sphere joint sunk into its host by design), cut (a shape or list of shapes subtracted from this one: a saddle pad is a disc cut by its host) and curve (the smoothed profile of a turned part with no analytic slope, for wobble). Tube runs also carry tips (the route's true end points, or null for an end that merges into a body, like a volute's start), tipAxes, tipRims (the flange or connector radius at each end), free (ends that are meant to come off their mount apart), offset and total (where the run sits on its route). Every shape carries the move, ride and piece of the item that draws it, so it can be posed in any explode state.
States. An explode state is a number (every assembly at that share) or { label, shares }, one share per assembly. uniformStates(10) gives 0, 0.1 … 1; they are not frames anyone sees, so they only feed the key solver. springStates(order, { stagger, stiffness, damping, substeps, fps = 60, settle = 1e-3, stride = 1, reversals = [] }) replays the live page's own springs with their stagger, opening from rest and closing from apart, one state per frame at fps until every spring is within settle of its target (about 1.2 s each way on the Raptor, so the slow tail of every collapse is checked too). stride: 4 keeps every fourth frame, for the depth raster. reversals adds the paths a quick hover makes: open, then close again after that many seconds (and the reverse), with the page's own scheduling: on a reversal only the assemblies whose target changes are rescheduled, staggered among themselves, so one that has just left comes straight back and one still waiting simply stays home. reversalsOf(order, stagger) gives a reversal 8 ms either side of every stagger boundary: that is where the bad windows are (a reversal 15 ms before the next start left the hot-gas duct flying out into a methane pump still parked at home), and three hand-picked times missed every one. prefixStates(order, { shares = [0.25, 0.5, 0.75, 1] }) adds the static states behind the prefix rule: the first k − 1 assemblies fully out, the k-th at each share, the rest home. Shares along the stagger are always non-increasing (earlier assemblies lead, opening, closing and reversing), so every reachable frame lies between these. A 40 ms sample missed whole pass-throughs: between two samples the nozzle travelled 15 units, more than twice the widest pipe. shareOf(item, state) reads an item's share from its ride (or piece), and offsetOf(item, state) is its move times that share.
overlapsOf(items, P, { step = 0.25, tolerance = 0.3, least = 0.25, states = [0], skip }) finds every pair of drawn items whose screen footprints overlap in each state and, on a grid of step viewBox units over the overlap, ray-casts both shapes along the camera ray. A cell counts when one surface is more than tolerance nearer. Each side's flagged cells are closed (one dilate, one erode, so an aliased sliver becomes solid), eroded once (so the hairline along a real contact drops out), and the largest connected piece is measured in px²; a side is "in front" when that area is at least least. At the old 0.5 step with a 4-neighbour core count, a whole flange sunk into a pump housing and every cable crossing (a 0.5 cable is under three cells wide) read as noise. Each item is ray-cast once, at home; in any state its raster is that one shifted on screen by its offset (rounded to the grid) and in depth by offset · V, which is exact for a translation. Pair results are cached by the pair's relative shift, so pairs that move together are measured once. On the Raptor, 1,119 states (uniform, every fourth frame of every replayed path, prefix states) cost less than the old 61. skip(a, b) excludes pairs, such as neighbouring chunks of one pipe.
orderKeys(items, overlaps, { gap = 0.02 }) turns every one-sided pair into a rule in that pair's state: keyFront − keyBack ≥ gap + shareBack · liftBack − shareFront · liftFront. It is linear in the keys, so one longest-path relaxation settles all states at once and only ever moves an item later. A loop that can't be met drops its weakest rule (dropped). Pairs in front of each other in different places are cycles.
depthOrder(items, P, { overlaps, solved, rest, allow }) checks the final order in every state. At rest (rest, default the states 0 and 1) and in every replayed spring frame, every wrong pair is a problem: a clean misorder, an interlock and a pair no key of the form k + lift · share can hold all show on screen. In the uniform states only a clean misorder counts; interlocks there are listed as passing. allow takes [patternA, patternB] pairs to excuse explicitly.
clearances(tubes, solids, { ratio = 0.5, least = 0.2, state, only, mounts }) samples every tube and lists every place it comes closer than max(least, ratio · r) to a solid or another tube. Near a run's flat end it measures the end ring, not a sphere that would overhang the cut. Only an end's own mount (the shape its rim sits on, see mountsOf), that mount's pipe, and the fittings of a line that shares the mount or lands on this tube are excused, and only within r + gap + 1 of the end; near an end anything else may still not be entered by more than 0.1. Pass state to pose everything (the excuse holds only while tube and mount are within 0.05 of where they sit together) and only: "through" to list penetrations alone. A tube's distance to another tube is the true capped-cylinder distance, so a run that ends flat under a flange doesn't grow a phantom round cap that the next run seems to enter.
mountsOf(tubes, solids), terminals(tubes, solids) find, for every tube end, the nearest anchored shape to its end rim (12 points at the flange or connector radius) and list every end whose rim isn't within 0.15 of it: a flange standing off its port, a flat flange on a curved body (the rim gap), a cable that stops beside the bundle or inside a clamp.
looseEnds(tubes, solids, { state = 1 }) lists every tube end that, in that explode state, touches no solid of another owner that moves with it. Its own flange, coupling or connector shell does not count (with it, the check could never fail: 25 Raptor ends rested only on their own fitting). Ends marked free are reported separately as a deliberate list; everything else fails.
supports(solids, tubes, { ground, touch = 0.15, sunk = 0.5 }) builds a contact graph of solids and rigid tubes and lists every solid not connected to the ground (default: names starting stand.), plus every pair of solids sunk more than sunk into each other unless they share an owner or one is seated on the other (named in its cut). A seated pad is excused against its host only: the Raptor's igniter flange sunk 0.5 into the hot-gas pad of another assembly hid behind a blanket seated flag. hanging lists rigid tubes that connect to nothing held, and fails too.
solidClearances(solids, { state, sunk = 0.5 }) poses the solids and, for every pair that sits at a different offset in that state, tests each one's surface points against the other's sdf; anything more than sunk inside fails (a seated pad and its host are excused). Run it at state 1 and in every replayed and prefix state: a housing, pad or flange of one assembly sweeping through another's body is invisible to the tube checks.
seats(solids) lists every seated shape cut to a host that moves with another assembly. Apart, its cut base is on show: the hot-gas duct's saddle, cut to the methane pump's turbine and ring, flew out as a jagged shard with a pointed tail. A saddle belongs to its host's assembly; the moving line meets it on its flat face.
selfOverlaps(items, P, { fold = 0.25, least = 0.5 }) checks each drawn tube chunk against itself: where the screen centreline turns on a radius under the tube's screen half-width, the inner edge of the ribbon runs backwards and the single-chunk outline crosses itself (a knot with the hidden leg's edges showing through). It fails when that fold is at least least px and the spot isn't covered by another item. A leg that points at the viewer and turns within a radius or two of a port is the usual cause.
wobble(solids, { samples = 20 }) samples dr/ds of every recorded curve at 20 points per segment and lists the s values where it turns between the run's ends.
folds(routes, P, { fold = 140, turn = 110, pinch = 2, items }) lists every route ({ name, points, r }) whose screen direction turns more than fold degrees within four radii while its real turn is under turn (the cusp of a pipe that runs away from the viewer and then drops), and every bend that turns more than 90° on screen round a radius under pinch × the tube's screen radius, measured over spans of 1.5, 3 and 6 radii (a fillet seen edge-on: the V of a leg that points at the viewer and then climbs). A pinch within three tube widths of an end is a stub seen end-on and doesn't count. With items, only bends you can see count: each is ray-cast against every drawn shape.
crowding(routes, P, { gap = 1, bend = 30, links, items, allow, state, clear = 2 }) lists every place where a route's bend (a 3D turn over bend degrees within ±2 radii) or end lands on another route's outline on screen: the outlines closer than gap px or overlapping, with both lines visible there (ray-cast against items; a route's own flanges, shells and clamps count as the route). Those read as a joint or a line threaded through another, whatever the true depth. A bend is never excused by overlapping deeply; an end only when the other line is straight there and the end's tip itself is clear of it. Every fitting (a harness clamp) is checked too: a clamp whose outline lands within clear px of another line reads as that line growing out of the clamp, as the Raptor's harness clamp did on the oxygen-to-preburner line in the apart pose. links (from linksOf(tubes, mounts)) excuses lines that really meet: one mounted on the other, both on one mount. state poses routes and items in an explode state, so the apart pose is checked too (routes carry move, ride, piece). allow takes [patternA, patternB] pairs you have looked at and judged clear.
slenderOf(solids, { ratio = 4, least = 0.3 }) turns every slender recorded solid (a turned part, prism or box longer than ratio × its radius or half-width: tie rods, piston rods, posts, rails, handles) into a pseudo-route, which audit passes to crowding as rods. A tube's bend or end that lands on one fails like one landing on another tube (crowding: hose.B end × cyl.tie-rod (rod)), unless the tube is mounted on it or on something linked to it. Rods are only ever the line landed on: their own ends aren't checked.
coverage(items, P, { least = 0.6, pad = 1, allow }) compares, for every item with an svg string, the screen box of what it draws (markupBox(svg): every path, circle, line, rect and polyline, through transforms and clip-paths, without halos, text and defs) with the screen box of its recorded shapes (footprintOf(shape, cameraBasis(P))), and lists every item whose shapes cover less than least of its drawing: stand-in shapes: rod.rod its shapes cover 4% of its drawing. A sliver recorded for a long rod, or one shape recorded for an item that draws three parts, makes every other check blind to the rest. audit takes cover: [names] to excuse items whose drawing really reaches past their body; look at each first.
report(result) formats everything; failures(result) counts what should fail the build.
The Raptor wires it like this (examples/raptor-engine/build.mjs): its put, tubeItems and record are thin wrappers over R.put, R.route and R.solid, and the end of the build is
The live page reads the same STAGGER and SPRING from its data, so the audit and the page can't drift apart. Every helper (turned, band, flangeAt, boss, padOn, boxAt, valveOn, pipe, cable, clampAt, strut, the actuators) records its shape as it puts its item, and the static stand drawn in the back layer (deck, flame cone, deluge ring and feed, columns, splices) is recorded as fixed shapes owned by stand.frame, so an explode vector that drove a part into the stand would fail too. Nothing escapes the check.
13. The WebGL layer (kit/gl.mjs)
| GLSL_TURN | Opt-in GLSL for 3D figures, prepended by the figure to its fragment: uGroupX/Y/Z/T, toGroup(p) (world → the group's rest-world coordinates), toWorld(g), dirToGroup(d), dirToWorld(d), and the cover masks uCoverEdge[60], uCoverCount[6], coverOf(vb, soft) (1 inside a part painted after the effect). It defines toWorld: don't prepend it to a fragment that declares its own. The chunk uses 70 uniform vectors. |
| glslPose(Name, prefix = "uName") | The same four functions for any group: toName, fromName, dirToName, dirFromName over prefixX/Y/Z/T, so one shader follows several groups. Feed it TURN.poseUniforms(controller, group, prefix). |
A value for fixed, set or draw can be a number (float), an array of length 2–4 (vec2–vec4), or an array or Float32Array longer than 4, which goes to the uniform array of the declared type: uniform3fv for uniform vec3 name[N] (cover edges, spark positions), uniform2fv/uniform4fv likewise, and uniform1fv for uniform float name[N]. There are no ints, matrices or samplers through these calls; a sampler2D defaults to texture unit 0, so bind a texture with layer.gl (the ripple tank's history texture). Names that aren't in uniforms or fixed, and uniforms the compiler optimised away, are skipped.
smooth, burst and settle are plain JS: import them from kit/gl.mjs in a module, or get them for free on a page that inlines glScript(). references/webgl.md has the full contract, a minimal page and the craft rules.
14. Tools
15. Turning groups (kit/turn.mjs, turn-build.mjs, turn-audit.mjs)
The optional 3D mode. references/3d.md is the guide; this is the reference.
turning(P, { recorder, scale = 1 }) → T
details: seams, ribs: { s0, s1, count, phase, twist, fade, seams, tone }, bolts: [{ s, r, count, phase, size, tone, fade }], dots: [{ at, normal, size, tone }], rings: [{ at, normal, r, tone }], rules: [{ points, tone, free }], bevel. Each constructor takes its own subset in its own frame (a ball's s runs along its first flat's normal; a prism takes dots, seams and bevel only): the table in 3d.md, section 4. Materials: gold, chrome, steel, gunmetal, rubber, brass, copper.
separate(A, B, hints) and gjk(A, B, warm) return { n, d, margin } for two point sets (the build's plane solver).