Skip to content

figure-geometry.js — the body, and the lines between

One definition of the human figure the cross-effector network is drawn on, and of the arcs that join its nodes. A plain script that hangs a single object off window; no build step and no module system.

Two things draw this body and they must agree to the pixel, because what you arrange in the wizard is what the dashboard draws:

consumer gets the file from
the dashboard's network tab (network.js, beside it) vendor/dims-tabs/, copied wholesale by dims-case
the wizard's step 4 diagram (builder.js) the DIMS checkout, served at /vendor/figure-geometry.js

The two figures on this page are drawn by this file, published into the site from packages/dims-tabs/ and run in your browser — so they are the geometry this release ships, not a drawing of it.

To move a body part, change it here. The number you change moves the node in the wizard immediately, and in a built study at its next dims-case sync — a study keeps its own vendored copy until then, which is what vendoring is for.

The coordinate space

Everything is in viewBox units, so it scales with the picture rather than with the window. The network tab's viewBox is 0 0 1000 560.

constant value is
VIEW_W 1000 the diagram's viewBox width
NODE_R 26 node radius in the wizard
TAB_NODE_R 12 node radius on the dashboard
HEAD_Y, HEAD_R 85, 30 head centre and radius
SHOULDER_Y, SHOULDER_DX 150, 60 shoulder line
TORSO_Y 235 torso node
HIP_Y, HIP_DX 330, 32 hip line
HAND_Y, HAND_DX 300, 100 hand height and offset from the centre line
FOOT_Y, FOOT_DX 545, 35 foot height and offset

The two node radii are not an inconsistency. The circle does two different jobs. In the wizard it is a drop target: it has to be comfortably clickable, and an empty one has to read as a place where something could go. On the dashboard it is a dot at the end of a line, and the line is the measurement — the heaviest edge the tab draws is 16 px, which a 52 px circle would swallow whole.

Places

SPOTS — six places something can sit. dx is measured from the figure's centre line, so a spot is positioned relative to whichever person it belongs to.

part label dx y
head head 0 85
righthand right hand −100 300
lefthand left hand +100 300
torso torso 0 235
hip hip 0 330
foot foot 0 545

ALIASES — { nose: 'head', hand: 'lefthand' }. Eight accepted names, six places: two are older spellings for a place that already exists. Written once here rather than inline in one consumer and mapped in the other.

BODY_TOKENS — the eight names ordered so that a compound name is tested before any name it contains: lefthand and righthand come before hand, and nose before head is irrelevant but harmless. (It is not strictly longest-first — righthand follows the shorter lefthand — and it does not need to be; what matters is that no token is swallowed by a substring of itself.) This is how a study written before effectors existed still lands its measures on a body: the part is read out of what is left of a measure's name once the group prefix is stripped, so teacher_righthandspeed becomes righthandspeed and matches righthand.

Functions

spotOf(part) the real place a part name refers to, following the aliases
bodyPart(name) the part a measure's name mentions, or null. Case-insensitive substring match against BODY_TOKENS
spot(part) the SPOTS record for a part name, following aliases, or null
positions(cx) every part name — aliases included — as {x, y} on a figure centred at cx
personCx(index, total) where the nth of total figures is centred: VIEW_W × (index+1) / (total+1), so people are evenly spaced with margins at both ends
appendFigure(svg, {cx, color, el, opacity}) draws one translucent body. opacity defaults to 0.3

positions derives the alias entries from SPOTS rather than listing them, so an alias and the place it points at cannot drift apart.

appendFigure takes el, the caller's element-maker — (name, attrs) → SVG element — because the tab and the wizard each already have one and neither should have to adopt the other's.

Each arm stays on its own side — check the numbers, not the names

The code pairs the right shoulder with lefthand, and the left shoulder with righthand. That reads as though the arms cross. They do not.

The figure faces the viewer, so the person's left side is drawn on the viewer's right. Both the spot names and the shoulder offsets follow that convention, and the coordinates settle it — on a figure centred at 500:

segment from to
right shoulder → lefthand x = 560 x = 600
left shoulder → righthand x = 440 x = 400

Each segment stays on its own side of the centre line. Nothing crosses.

What would cross is pairing them by name — left shoulder to lefthand — which reaches across the chest and draws the two arms as an X. That is a mistake the file made once and now avoids; the pairing you see is the fix, not the bug.

The fan

Every edge is drawn as a quadratic curve bowed perpendicular to its chord, and no two edges by the same amount.

Straight lines between collinear nodes are the same line: three within-body edges drew one thick bar with no way to tell one from three. Lines that merely share an endpoint are nearly as bad — every cross-body edge crosses the middle and they arrive at a node as a single smear. Fanning the whole set is what separates them.

constant value is
BOW_FLOOR 22 even a lone edge curves this much
BOW_STEP 30 spacing between neighbours in the fan
BOW_MAX 240 the widest arc, so a large fan stays in frame
bowRanks(count) one rank per edge, symmetrical about zero — [-1, 0, 1] for three, [-1.5, -0.5, 0.5, 1.5] for four
bowStep(count) BOW_STEP until the outermost arc would leave the picture, then whatever fits
edgePath(p1, p2, rank, step) the SVG path: M … Q …, offset along the unit perpendicular

bowStep is why a study with twenty edges gets a tighter fan rather than arcs sweeping off the top of the viewBox. The floor is signed with the rank so the fan stays symmetrical about the chord, and it exists so that even rank 0 curves — two nodes joined by a single straight line read as structure rather than as a measurement.

The fan lives here rather than in the tab because the wizard draws the same lines, and a request for an analysis should be the same shape as the result that comes back.

Where it lives

packages/dims-tabs/figure-geometry.js.

It must be loaded before network.js — the scaffold's index.html sets that order. When it is missing the network tab reports it rather than throwing, so the tab still registers and says what is wrong.