Skip to content

v2.0.5

Choose a tag to compare

@github-actions github-actions released this 20 Sep 05:45
· 1390 commits to main since this release

vajra-sculpt — a character as hand-placed field terms, print-closed

  • Added: a contribution gate that reads fields[].terms (stats.contribution, per term
    { exposure, protrusion, blend, buried, absorbed }). The fields monomer never had one: the
    contribution and jut-shortfall checks that keep a workbench monomer from being swallowed by its
    neighbours are blind to a term list, so the failure they exist to catch — a mass buried inside its
    host, contributing no silhouette — was invisible until a render was looked at. Two failures are
    reported separately because the fixes are opposite. BURIED: the term's own surface lies inside the
    union of the others, so it breaks no silhouette and costs bytes and sampling time while drawing
    nothing — move it out. ABSORBED: the term IS proud of its neighbours, but by less than its own
    blend radius, so the smooth-union bulge swallows what little it clears — raise the protrusion or
    lower the blend. Method is point-in-solid against the union, which is exact here because every term
    already IS a signed distance function: each term is surfaced alone on a coarse net and every face
    centroid is tested against every other term. No AABBs — a bounding box lies about a taper, which is
    how the monomer check first got this wrong. Advisory, never gating; pure and deterministic.
    Retro-run against a 23-term first-pass head (97 ms) it named four real defects with no render, and
    cleared two ears that were in fact unreadable. That miss is the useful result, because it separates
    the classes: a geometric gate finds CALIBRATION defects (buried, absorbed) with no eyes, and is
    blind to IDENTIFICATION defects (the wrong KIND of mass), which are fixed by a better reference and
    not by a better check.
  • Added: attachments on a figure recipe — any workbench recipe mounted on any named landmark,
    scaled to the figure and riding the pose: { recipe, at, t, size, fit, anchor, offset, rotate, align }.
    A held shield was already possible and was welded to one prop and one landmark; a character with a
    helmet, a spear and greaves needs the same move at four more places. This is what lets a character's
    BODY come from the figure family — parametric, posable, nothing sculpted — while its GEAR comes from
    the object lane, where manufactured parts belong. The existing hold path is untouched. A mounted
    recipe carries any monomer but drapes (cloth needs the garment path's two-sided shading) and
    reliefs (carved lettering reads a font from disk, and the figure renderer is reachable from the
    browser bundle, where there is no disk); both remain workbench-path monomers.
  • Added: a connectivity readout on every workbench recipe (stats.components). Closure asks whether
    every edge has a partner; a detached horn tip, a spear that never reached the hand and a crest fin short
    of the helmet all pass it, because each stray island is its own closed shell. Multi-body is not itself a
    defect — superposition is the construction method and a loose part is ordinarily its own solid — so the
    count is reported as a fact and only a declared bodies: N that the measurement contradicts warns.
    Method: parity voxelization along +x then a 6-connected flood fill, run over the WHOLE baked face
    list, so it crosses the field-hand / lathe-spear seam that no per-monomer check can see. It uses the
    NONZERO WINDING rule and not even-odd, and that is the whole point: superposition is the house
    construction method, so two separately closed shells that OVERLAP are the ordinary case, and parity
    fills their two rinds, leaves the overlap hollow and reports the pieces disconnected (measured: a
    shield read 11 bodies, a spear 4; winding puts both at 1). The sign is free — a triangle's det in
    the y–z projection is exactly the x-component of its normal. Two more things the build taught: a
    grid-aligned sample line lands on the shared edge of a dual mesh every time and doubles the crossing
    count, so lines are nudged off-lattice and the barycentric test is half-open; and the count is run at
    two resolutions, because a junction that only GRAZES merges at one grid and splits at the other — the
    instability IS the finding, and it is the same tangency Manifold's union cannot resolve. The gate also
    inherits the geometry's resolution ceiling, so each piece reports across (cells at its thinnest) and
    a split involving a ≤2-cell piece carries a caveat instead of an assertion. A slender member has to be
    verified on its own bounds, which is the same h = longest / cells argument that governs the sculpt.
  • Added: figure-cluster — one scale authority and one identity lock over a character's roles.
    A character built as a body plus mounted gear is already a CLUSTER of independently authored recipes.
    Composing them by hand worked and showed exactly what was missing: the gear took four placement passes
    and came out oversized, because attachments[].size is an ABSOLUTE STAND number and nothing says what
    STAND is — measured at headTop, the default armature stands 0.93 and a chibi 0.52. That happened to
    one author with the whole thing in view; fan the roles out and it happens once per agent, in different
    directions, with nothing to reconcile them. The frame holds four things nothing else did: the unit
    (measureFigure poses the armature and reports its height, so scale is MEASURED rather than guessed),
    the identity lock (≤5 named traits in one sentence, required, riding the minted recipe so a later
    pass restates the same character instead of inventing a neighbouring one), the interface (each role
    declares its mount as validated data instead of a tuned literal), and the ledger (every role's own
    closure / contribution / connectivity gates on its OWN bounds — the only scale at which a slender member
    is resolvable at all). The rule that makes the scale trap impossible rather than merely documented:
    a role declares its size as a FRACTION of the figure, never as an absolute — mount.size is a
    validation error and mount.span is required. This is the object protocol's no-absolute-z discipline
    applied to scale: a spear is 1.19 figures long and a shield 0.34 across, and those are true of a chibi,
    an adult and a brute alike, so a role authored once ports to every cast and two agents cannot disagree
    about what 0.43 means. One unit was not enough — a chibi's head is a far larger share of its body than
    an adult's and a helmet is a fraction of the HEAD — so mount.spanOf: 'figure' | 'head' says which
    level a role answers to; offsets stay figure-relative whatever the span measures against, because an
    offset is a position on the body and positions belong to the body's frame even when a size belongs to a
    part's. Verified by resolving a hand-tuned three-role character to byte-identical absolutes, then
    rendering the same spec, with no number changed, on an adult and a brute: the unit re-measures, every
    span re-multiplies, and the gear arrives at the same proportions. resolveCluster returns a
    kind: 'figure' manifest that mint_solid stores plus the gate ledger; it is a build-path module, and
    there is no cluster dial on the tool surface yet.

limb-mass — the arm and the leg in relation to the torso's weight

  • Added: armWidth and thighWidth — the upper arm's and the thigh's LATERAL breadth, which
    no dial could reach. bicep scales the anterior and posterior upper-arm lobes and quad scales
    the thigh's front lobe, but every one of those lobes adds into the limb's FORE/AFT radius and the
    lateral radius was bare baseR: measured, bicep: 2 moved the upper arm's width by ×1.000 and
    its depth by ×1.363, and quad: 2 moved the thigh's width by ×1.000. A heavy arm and a lean arm
    were the same width head-on and only the profile ever moved. (forearm and calf never had the
    problem — they ride the ring radius itself.) armWidth eases along the lobe profile so the
    deltoid and elbow caps still cover the ends; thighWidth is localized to the thigh band and
    leaves the lower leg to calf. bicep and quad are untouched, so every stored recipe renders
    byte-identically.
  • Added: tricep, hamstring and adductor — the lobes where gained weight actually sits.
    One dial drove both the bicep and the tricep, and the thigh's posterior and medial lobes had no
    dial at all, so the only thing a limb could do as it filled was grow a bigger bicep and a bigger
    quad. Muscle reads anterior and lateral; weight reads posterior and medial. adductor is what
    closes a heavy leg at the top. The fat↔muscle axis is now a choice rather than a side effect:
    { bicep: 1.5, armWidth: 1.2 } and { tricep: 1.5, armWidth: 1.35 } are different shapes at the
    same circumference.
  • Added: deltoid and elbowCap — the joint caps, which had no dial and never saw weight,
    so segments could inflate while the joints they hang from held still. Each sizes its mass and
    leaves its centre on the sex pole, so a cap grows once rather than scaling its radius and sliding
    its centre away too. (The hip already had hipFlare.)
  • Fixed: proto.weight landed on the trunk and stopped there. Across weight 1 → 2 the bust ran
    ×1.371 and the waist ×1.721 while the upper arm managed ×1.209 and the thigh ×1.191, so
    upperArm/bust fell 0.388 → 0.342 and thigh/hip fell 0.504 → 0.474 on both poles — a heavier
    figure read as a heavier TRUNK on the same limbs. It was not a matter of turning the limb gains
    up: there was no lateral axis for them to gain on, and raising bicep or quad builds muscle
    shape rather than fat shape. With the axes above in place the gains were SOLVED against the ratio
    rather than chosen, and both now hold within +5 / −0 % across weight 1 → 2 on both poles. The
    wrist and the ankle still fall away, which is correct — weight is proximal, and distal is bone.
  • Changed: figure-torso.test.js's weight-distribution pin. It asserted that the waist, hip and
    bust each outgain the wrist, thigh and upper arm, and that blanket ordering WAS the defect — it
    held only because the proximal limbs could not keep up by construction. What the dial promises,
    and what is now pinned, is the trunk↔EXTREMITY separation: the middle gains most, the hand and
    the foot barely move, and the proximal limbs sit between. The hip is the least responsive trunk
    landmark and no longer outgains the thigh; the anthropometry says a thigh gains slightly faster
    than a hip, and that is pinned instead.
  • Added: forearmDrop and calfDrop — WHERE the distal belly sits along its own segment
    (0 = canonical). The distal limb had the opposite gap to the proximal one: forearm and calf
    ride the ring radius itself, so they always scaled both axes and never needed a width dial. What
    they could not say is where the mass sits — the forearm's crest was pinned at 0.400 of the segment
    and the calf's at 0.488 (against a knee trough at 0.442) for every dial and every weight, so a
    long low calf tapering into the ankle and a short high one were the same calf at different
    volumes. That is the axis a calf is actually read on. calfDrop travels DOWN freely (crest
    measured at 0.488 / 0.512 / 0.535 / 0.581 / 0.605) and is bounded going UP by the knee: a gastroc
    does not sit above the joint, so negative values concentrate the mass and read as a shorter,
    tighter calf rather than raising it.
  • Added: wristGirth and ankleGirth — the terminal joints' own thickness. wristTaper
    FLATTENS the wrist (it is the flipper dial) and only ever moved girth as a side effect, and the
    ankle answered to nothing at all: calf: 2 moved the measured ankle by 1.3 %. Each is localized
    to its own end and stays out of the belly above it (wristGirth: 1.5 reaches the forearm belly by
    3.8 %, ankleGirth: 1.5 leaves the calf crest untouched).
  • Measured, and deliberately NOT re-gained: weight already reaches the distal limb. Unlike the
    proximal pass, no new gain was wired. forearm and calf already carry gains, and at weight: 2
    the wrist measures ×1.151 and the ankle ×1.148 against roughly ×1.12 and ×1.18 lean-to-obese —
    inside the reference's own slop, so chasing them would be over-fitting a remembered table. The
    distal gap was expressive, not relational, and the four dials above are absent from WEIGHT_GAIN
    on purpose. Pinned as such.
  • Found, not fixed (pinned): bodyGirths does not report the hip monotonically below weight 1
    — 123.9 cm at 0.6, 118.2 at 0.85, back up to 121.4 at 1.0, so a leaner figure can measure a wider
    hip than a slightly-lean one. It is the chart and not the body: the trunk's own lower geometry is
    strictly monotone across the same range, and the hip landmark does not migrate there either
    (v holds at 0.900–0.903). Left alone because every cut-and-sewn garment reads that chart. It is
    why the new ratio gate starts at weight 1 — below that it would be measuring this.

figure-cast — limb proportions as a dial, cast off the locked armature

  • Added: cast on mint_solid { kind: 'figure' } — rest segment-length multipliers
    (upperArm, forearm, thigh, shank, lumbar, thoracic, neck, skull) plus
    shoulderSpan / hipSpan, and the groups arm / leg / limb / torso. They apply at the
    vajra level, before anything is fleshed: a length dial re-places a distal node along its own rest
    direction, a span dial translates a limb subtree, and the figure re-seats on the floor. The joint
    graph, the subtrees and the joint LIMITS are untouched — a cast moves rest lengths, never a
    joint's range. This is what makes a figure long-limbed, stubby, broad-shouldered, an ape-index
    brute or a mascot; proto remains girth and proto.height remains overall size.
  • Changed: the flesh, the balance IK, the gait, the spine warp, the garments and the exported
    rig all read the cast.
    articulate / applyPose take an optional rest armature (the signature
    articulateTransforms already had); the 2-bone ground IK solves on the cast's own bone lengths;
    the walk converts its absolute ground stride against the cast's leg, so a long-legged figure
    strides instead of mincing; the spine warp's rest curve and the arm's anchor height come from the
    cast; the cut-and-sewn wardrobe follows because the body is its own tape measure.
  • Added: named cast presets (heroic, brute, lithe, stout, child, chibi, and
    canonical), each a starter to nudge rather than a fixed look. They resolve BY VALUE at mint —
    the stored manifest carries the numbers plus from — so re-tuning a preset can never change an
    already-minted figure, and the recipe has something to read and edit.
  • Changed: buildPosedFigure's positional tail is now an options object
    ({ fluffs, hold, hair, cast, fluffQuality, weld }), and every render path reads the body half of
    a manifest through one reader instead of six argument lists.
  • Fixed: an emote minted from a stored figure kept its garment but silently dropped its
    fluffs
    , so emoting a stylized figure produced an anatomical one. It now carries the whole
    body — fluffs and cast — as its comment always claimed.
  • Added: cast.shoulderDrop — the girdle's CARRIAGE, in degrees rather than as a multiplier.
    The armature puts the neck root and both shoulders at the same height, so the clavicle line and
    the shoulder yoke run dead level out to the acromion: anatomically a permanent shrug, and the
    reason a neutral figure reads stiff and high through the girdle. + declines that line, − rides it
    into a shrug. The arm translates with the acromion instead of rotating with it, so the humerus
    keeps its rest hang and every cached rest bone direction stays valid. The canonical rest is
    unchanged (still level); declining it by default is blocked on the trunk chart's cap taking its
    footprint from the shoulder LINE rather than from whichever hull band is highest — past ~5° of
    drop the deltoids leave that band, the footprint collapses and a shoulder-seamed garment is
    misplaced. Pinned in figure-cast.test.js; shells and side-seam garments are unaffected.
  • Changed: cast.shoulderSpan scales the clavicle as a VECTOR, not in x alone, so a broad
    shoulder keeps its shoulder ANGLE instead of flattening toward level. The angle is
    shoulderDrop's business.
  • Added: proto.chestDepth and proto.pelvisDepth — the torso's fore/aft, which nothing could
    reach. chestWidth has always scaled the ribcage's whole cross-section, breadth and depth by the
    same multiplier, so a deep-chested or slab-flat trunk was not a setting; these trim the depth
    alone on top of it (chestWidth: 1.4 with chestDepth: 0.71 is broad AND flat). The waist needs
    no dial: its depth is the interpolation of the two and follows them. chestWidth's own behaviour
    is unchanged, so every stored recipe renders byte-identically.
  • Added: proto.weight — one number for how heavy a figure reads, distributed across the
    region dials rather than scaling them evenly (which is what stockiness already did). The
    abdomen takes the most and takes it forward, the hips and thighs next, the waist un-tucks, the
    V-taper flattens, the wrist barely moves: at weight: 1.6, waist ×1.24 against wrist ×1.09. It
    is a global gain, so the operator's own region dials multiply on top, and it is relative to the
    frame because every gain is a multiplier. weight: 1 is byte-identical on both poles. Distinct
    from pose.weight, which is which foot the figure stands over.
  • Fixed: a heavy figure grew spheres bolted to a torso. Three faults compounded in the side
    profile. The trunk is authored back-anchored (cy = bez + ap·ct puts the spine at the back), so
    every millimetre a depth dial added travelled FORWARD — at weight 2 the front moved +1.24 and the
    back −0.20; depth dials now grow the section about its own centre. Weight's depth gain was nearly
    double its width gain, building a deep narrow slab; width now leads. And the belly and the seat
    are separate stacks that each grow twice over — radius and centre — with nothing bounding how
    proud of the torso they could stand, so seatMass now holds every superposed mass to the
    proportion of the trunk it has at canonical (the seat had reached 80 % of trunk depth against a
    canonical 55 %). Self-limiting, so a hand-set gluteSize: 3 is bounded too.
  • Fixed: a concave belt across a heavy figure's waist. The trunk's belly swell peaked below the
    waist and fell away before the ribcage picked up, leaving a groove exactly where a heavy body
    should be fullest — the lateral outline sat 0.091 below its own convex hull at weight 1.6 and
    0.166 at weight 2. The swell is re-centred and widened (uu 0.42, spread 0.42) so it reaches the
    ribs: 0.021 and 0.068. The waist's DEPTH pinch also now rides waistTuck exactly as its width
    pinch always has, so a relaxing tuck opens the groove instead of holding it at full strength.
    A lean figure keeps its waist — that narrowing is a waist, not a defect. Canonical unchanged.
  • Added: proto.bellyFill — the trunk's OWN swell around the navel, and what weight now
    drives instead of inflating the dantien ellipsoid. A heavy middle is one continuous silhouette
    rather than a ball overlapping a torso; dantien remains the distinct mass for when that is
    wanted. Canonical at 0.
  • Added the STOMACH and HIP axes: proto.bellyDepth, bellyDrop, gluteRear, hipFlare.
    dantien and gluteSize scale their mass uniformly, so a forward gut, a high or low belly, a
    shelf buttock and the hip's own flesh were all unreachable at any setting. bellyDepth grows the
    belly forward with the back edge pinned; bellyDrop slides it along its own height (0 =
    canonical); gluteRear projects the buttock rearward without widening it, multiplying the sex
    pole's rear scale rather than replacing it; hipFlare gives the hip cap the dial every other
    body mass already had. All default to canonical, so stored recipes render byte-identically.
  • Removed: manifest.proportions (the chibi rig, which only ever reached a fluffs body and
    was never writable by any mint tool). The cast is its general form; a stored proportions
    migrates to the equivalent cast dials on read, so nothing re-mints.

the head grows its features — the ear, the side wall, the nose tip, the eye

  • Added: the ear is a feature instead of a bulge. It was one flattened lobe at the tragion,
    smooth-unioned at the face's 0.019·s blend — ~17 mm of softening over a 4 mm lobe, so it added
    7 mm of head width and no shape at all, and the lateral turntable showed a bare curve from
    temple to jaw. It is now a helix rim of round cones from the root, over the top, down the back
    to the lobule, thickened by two pads and closed by a thin pinna plate, in its own blend group
    behind its own bounding gate
    — the same treatment the nose already needed. The rim's outer
    edge spans the canon band, subnasale to glabella.
  • Added: the zygomatic arch and a light masseter. The side of the face fell into a trough
    between the cheekbone and the ear — 3.7 mm real on the bare skull, which the new ear deepened
    to 4.5 mm by raising the wall behind it. The arch bridges cheekbone to ear canal and the
    masseter rounds the jaw's side below it. It costs nothing in the head's width band (both sit
    under the ear's peak) and it made the ear render cleaner: filling the valley stopped the
    latitude rays plunging into it, so the worst ring-to-ring step on the ear fell from 6.0 to 5.5 mm.
  • Changed: the nose tip domes and reads as a forward pyramid. The tip was a sphere whose
    radius of curvature (~12 mm real) dwarfed the feature it was meant to be, so it rendered as a
    flat facet. It is now an ellipsoid plus an infratip lobule, with a dorsal keel carrying one
    continuous ridge from the bridge to the tip. The tip gained ~4 mm of projection and got
    narrower, not wider: all the volume went into projection and height, none into width.
  • Added: the eye. A globe in the orbit under two C-wave lids, unioned after the orbit
    subtraction (before it, the socket cut simply erased the globe) with that cut trimmed to suit.
    At the eye a latitude ring is 5.9 mm real, so a 10 mm palpebral fissure spans 1.7 rings and
    cannot be a modelled gap — it is a shadow step between two lid ridges, the same answer the
    lip line arrived at. The eye reads on form and shading; it carries no colour.
  • Added: earSize and eyeSize head knobs. earSize grows the ear in its own plane and its
    rim thickness but deliberately not its standoff, because the head's width is ear-driven and
    the width band is the guard on the whole skull. eyeSize grows the globe and the fissure
    together — a bigger eyeball behind the same aperture only buries itself.

one machine, one answer — the cap tessellator and the rounded-rect profile

  • Fixed: a carved cap with two or more counters could fill one of them. Bridging a hole into an
    outline leaves the ring touching itself at the bridge, and the ear clipper could cut straight
    through one of those contact points — emitting an inverted triangle and covering a counter twice,
    so a letter like B came out with a solid bowl. triangulateRings now spots the inverted
    triangle and re-clips that ring with the bridge contacts guarded, keeping whichever result
    actually matches the ring's area. Caps that were already correct are untouched, byte for byte.
  • Fixed: a rounded-rect extrude profile now emits the same bytes on Apple silicon and on x86.
    roundedRectPath asked Math.cos/Math.sin for each of the four corner arcs by absolute angle,
    and V8's argument reduction past π/2 is not bit-identical across CPU targets — one face normal
    came out a ULP apart on the two architectures. It now takes a single quadrant and turns it by
    exact sign swaps. A recipe regenerates to the same bytes wherever it is read, which is the point.
  • Changed: the cap tessellator's regression pin no longer depends on the machine's fonts. The
    glyph tests resolve whatever font is installed — Arial Black on macOS, DejaVu on the Linux CI
    image — so the two B bugs above only ever reproduced on CI. The failing contours are frozen in
    triangulate.dejavu-b.fixture.js and pinned everywhere.

readme — worked examples instead of a manifesto

  • Changed: the repo README is now five concrete examples (a mug, a living room, a city, a
    walkable world to a Godot export, a snowman), each with the sentence you say, the tool that
    fires, and the recipe that gets stored, followed by the export table and what stays on the
    machine. The long-form catalog moves unchanged to docs/tour.md.
  • Changed: the one-line self-description is "a 3D compiler for agents" — a compiler, not a
    generator: the recipe is the source, kernels compile it back byte-for-byte, emitters are the
    targets, renders are derived. Said in one voice across the README, the npm README and
    package.json description, server.json, glama.json, the plugin manifests, the
    SERVER_INSTRUCTIONS preamble, the get_substrate one-breath answer and studio opener,
    docs/AGENT-REFERENCE.md, and the installer's chatbot note. "3D factory" survives only in
    code comments and the changelog history.
  • Fixed: LICENSE is the verbatim Apache-2.0 text (two clauses had been paraphrased, so
    GitHub reported the license as "Other"); the copyright line moves to a new NOTICE file.
  • Removed: six scratch scripts (control/relief-test2.mjs and the
    underscore-prefixed prototypes under control/scripts/) that carried absolute home paths;
    scripts/_*.mjs is now gitignored.

terms — the open-source commitment and a possible mojulo cloud

  • Changed: TERMS.md, README, SECURITY.md, docs/responsibility-model.md, and the get_substrate
    facts
    now state the stance in one voice: the open-source mojulo is and stays Apache-2.0 with no
    telemetry, ever. That commitment is about the software, not a bar on a hosted offering — a
    separately offered mojulo cloud on standard services may be explored if there is demand, as its
    own opt-in product that changes nothing about the local install. "There is no hosted service"
    lines become present-tense ("today") where they read as a permanent ruling-out.

dungeon-rock-texture — a surface tile per cave surface, multiply-lit over the traced bake

  • Added: texture in the dungeon style bible. style.texture: '<key>' | { floor?, wall?, ceiling?, scale? }, a per-chamber texture override, and style.tunnel.texture / per-tunnel
    texture (tunnels inherit the style's wall key: a cave is one rock). Keys are surface-textures
    tiles (rock-cave, rock-granite, rock-sandstone, or a defineRockTile custom tone); an
    unknown key refuses the mint like an unknown material does. Every shell face gets texture,
    textureLit, and a per-corner uv — walls along the ring by unwrapped azimuth × radius (no
    stretch, no ±π seam inside a face), floor and ceiling by world XY, tunnels by their dominant
    plane — so the World and the .glb draw tile × baked fire light. This is the texel-frequency
    stage a 6th-gen cave frame has and the bare bake lacked. padTrianglesForWorld now pads a fan
    triangle's uv with its corners. Absent texture ⇒ no face carries a key, byte-identical.
    Manual: the dungeon view-vocab card. Tests: dungeon-texture.test.js.

figure-head — a structured head, male and female

  • Changed: the protoform's head is one closed form built from anatomy. It was two overlapping
    open tubes: a three-bead vajra egg and a tapered "face mask" sleeve pushed forward of it. They
    z-fought where they crossed, the mask's lower rim read as a visor, and because a ring stack is
    capped at neither end the crown and the underside were holes about 5.5 cm and 7.5 cm across at a
    170 cm stature (the crown scoop was visible from behind; the underside hid behind the neck).
    figure-head.js now builds the head as a signed-distance field of named anatomical primitives
    (braincase, frontal, occiput, brow, subtracted orbits, zygomatics, maxilla, nasal, lips,
    mandible, ramus, chin, submandibular floor, ears) smooth-unioned into one skin, the way the
    welded animal skull is, and surfaces it by LATITUDE rings from a centre inside the braincase,
    so both poles close by construction. The faceMask stack is gone; headEgg keeps its id and
    its ring currency, so hair, hats, garments, the skin seam and the spine warp read it unchanged.
  • Changed: the head has a sex. Male and female heads were byte-identical. DIMORPH carries a
    head pole (brow, jaw width and drop, chin point and size, forehead slope, cranial roundness,
    nose, cheek fullness, size, neck girth); the female basis is a rounder cranium, a vertical
    forehead, a smooth brow, a narrower jaw, a pointed chin, a smaller nose, fuller cheeks and a
    thinner neck. proto gains per-region multipliers on top: browRidge, jawWidth, chinPoint,
    noseSize, noseWidth, noseDroop, cheekbone, cheek, foreheadSlope, neckGirth (1 = canonical, absent = same bytes).
    The nose is articulated and seated AT the face plane, projecting past it — a narrow bony bridge
    from the nasion dip to a slightly proud mid-dorsum, a cartilage dorsum to a supratip break, a tip
    ball hanging below the dorsum line (noseDroop, the hook), two alae lower and wider than the tip
    (noseWidth), a nostril bar, and a flat columella back to the subnasale so the nose has an underside.
    The nose blends within itself far tighter than it blends into the face, with a shallow alar crease
    either side of the tip, so the two alae and the tip read as three lobes and not one bell — and the
    head's rings are sampled twice as densely at the face as at the nape (44 around), because at a
    uniform 36 the whole nose base was one or two samples wide. The philtrum is a shallow recess and
    the nose joins the face tighter than the face blends with itself, so the underside of the nose
    stands over air instead of filleting into the lip. The lips have mass: an upper lip rising to a
    cupid's bow — both lips are built on one M-shaped mouth path from inner cheek to inner cheek, the
    upper lip's bottom edge tracing the bow and overhanging the lower lip so the line reads as its
    shadow — a fuller lower lip that rides the jaw and sits just under the upper lip's overhang, and a
    sulcus under the lower lip; the mandible rests closed-up against the maxilla with the chin brought
    up and forward under the lips, so the profile does not read as an overbite (bone only — the lips
    rest where they belong and follow the open dial).
    cheek is the soft cheek under the cheekbone — the apple of the cheek, a pad standing proud of
    the maxilla, and a jowl blend into the jaw — so the face rounds out in the ¾ view instead of
    reading as a skull or as sullen; the female basis is fuller.
    The buccal plane is lateral and thin (the face beside the mouth is the maxilla's own curve),
    the lips join the face at a wider fillet than
    the nose does, so the upper lip's wings run out into the inner cheek instead of standing as a
    ridge with a trench either side, and the cheek is one convex mass whose inner lower edge runs in
    to the base of the nose with no concave section between them. The philtrum groove sits in the
    skin (it had stayed at the maxilla when the lips moved forward, a pocket inside the head that
    every latitude ring through the mouth zigzagged over), and the lips run out into the cheek at
    their corners instead of ending in a wall.
    headScale still scales the whole skull about the neck join.
  • Added: pose.face — the jaw and the mouth, structured in the field, not the armature.
    face: { jaw, mouth, brow }: jaw (degrees, 0–30) rotates the jaw group about a hinge through
    the condyles so the chin swings down and back and the smooth union re-fuses it into the cheeks;
    mouth (0–1, default follows the jaw) subtracts a lip slot; brow (−1..1) lowers or raises the
    brow ridge. No new armature node, so the 17-node manji, LIMITS, the packed rig and the VRM map
    are untouched, and emotes/keyframes drive face through the same phase→dof contract as every
    other dial. Stated limit: the skinned GLB bakes rest geometry over rigid bones and the head is
    one bone, so jaw motion shows in SVG, GIF and world frames, not in the skinned export.
  • Added: the head-study camera. renderFigureToSvg takes crop: 'head' (the head, the neck
    and whatever sits on the head) and elev (degrees), mirroring the animal path's crop and
    animalCamera, so a human head can be checked top-down and from under. /head-study renders
    the eight skull-study angles for both sexes, filled and wireframe, cropped to the head.
  • Added: head rows on the tailor's tape. bodyGirths reports head (the widest ring's girth,
    the hat size), head_height and head_width in cm at stature, read off the head stack.
  • Changed: wigs and hats average the skull radius over height, not over rings. The head's rings
    are latitudes now, dense and tiny at the poles; a per-ring mean read the dome about a quarter
    small. The trapezoid rule over the radius-by-height profile is the same number for rings spaced
    evenly along the axis. A curtain wig's apex also clears the crown by about 2 cm instead of
    grazing it: the old crown was an open ring, so a dome that touched it showed nothing; the closed
    skull poked through.
  • Re-pins: the three figure-absent.char.test.js hashes and the world kinds that place figures
    (world-scene.kinds.test.js.snap) — the flesh moved; the absent-channel promise is unchanged.

toon-shading — cel bands and ink outlines as a manifest dial

  • toon on any 3D manifest (toon: true = { bands: 3, ink: true }; also read at
    scene.toon). bands quantizes the baked Lambert term into N tones in
    vexar.js (makeLight({ bands }), withBands, bandFactor;
    the final key+fill factor is banded, so the shadow hemisphere steps too). Because every
    renderer draws the baked fills unlit, the bands show identically in the SVG still, the CSS-3D
    scene, the orbit World, the GLB's COLOR_0, and the Godot / Unity / Unreal / Blender packs. A
    material's own cel still wins per part; FLAT_LIGHT (unshaded export) ignores the dial.
  • Threaded through resolveWorldScene (ctx.toon, payload.toon) into the workbench,
    assembler, manji-tree, figure, animal, edifice, fractal-city and room assemblers, and through
    resolveLighting for the CSS-3D room paths. mint_solid { kind:'workbench', spec.toon }
    stores it; other kinds take it via update_sketch (set /toon). Absent → byte-identical.
  • toon.ink in the World — a new pre-runtime setup channel
    (channels/toon-ink.js, provides __mojInk): per
    eligible group the emitter packs the opaque non-studio faces with their authored outward
    normals (ink: { pos, nrm }), and the page draws an inverted-hull silhouette (welded normals,
    winding made to agree with them, BackSide) plus EdgesGeometry crease lines; the fills take
    a polygon offset. ink: { color, width, crease } tunes it. Rooms' shell: groups, studio,
    water, decal, glow, wireframe and translucent faces are excluded. Zero bytes when absent; one
    new toon-ink fixture pinned in the char net.
  • Live ink — every ink group owns its hull + line materials and registers by group name in
    window.__mojInk (tint(name, '#hex'), width(name, k) — the hull push is a vertex-shader
    uniform, so width is live — reset(name), set(on)); hull/line meshes carry
    userData.ink. Rig figures: the controllable rig builder inks every part (hull + creases
    parented under the bone mesh, so they pose with it and survive loadout/livery rebuilds) when
    the world carries toon.ink; parts have no authored normals, so triangles orient off the
    part centroid. fx verbs: a standing state { ink: '#hex', pulse?: true } and an
    inkFlash gesture ({ gesture: 'inkFlash', color } on a bus binding) recolour an entity's
    outline on the deterministic step clock; spliced only when an fx spec uses them, so every
    existing fx world is byte-identical. Two more fixtures pinned (toon-ink-controllable,
    toon-ink-fx).
  • Docs: docs/scene-css3d-lighting.md (authoring dials), the workbench and figure vocab cards.