Blender files

Edit a linked part in its own .blend file; keep library paths relative and inside this repository.

FileOwnership
key.blendKeycap in three variants on one notch and footprint: the keycap, with a dished top; the anchor keycap, the keycap's finished geometry plus Key-Cap-Anchor-Bar; the hump keycap, 1.8 mm taller with a domed top. The modules seat on each key the cap build.toml gives it (keys): the anchor keycap on the home keys (LYNXware.toml home), the hump keycap on the keys it lists, the keycap on the others. Also the master key cutout
finger-module.blendFinger module covers (left, and its right twin), key grid, envelope, mounting screws, both finger boards placed, a keycap on every key; links key and references
thumb-module.blendThumb module covers (left, and its right twin), the translucent LED plug, envelope, mounting screws, both thumb boards placed, a keycap on every key and a plug in every LED window; the joystick thumb's cover, dome and cap (the joystick thumb); links key and references
lid.blendLid in four variants, one per kind of bottom slot, all built from Lid-Plate (the lid without feet, bottom module or battery), and the first three each with and without the battery (the battery and the power switch): plain with rubber pads (no bottom module), gyro with the GY-521's pocket and pads, mouse with the sensor breakout's pocket, a window under the lens and PTFE skates (it slides the hand on the desk; mechanical/associations.toml bottom), and the v3 ADNS-5050 module's mouse lid, its own holder with the board snapped into a pocket, and skates, always without the battery; left, and their right twins; the bottom modules themselves, which have no printed part of their own: the holder, each module's cuts and the breakout where it sits (the bottom module); its envelope and casing/lid screw system, a foot's pocket over every screw holding its head (the lid's feet); the plate outline is drawn by hand, and the envelope, the screw places and the casing's lower edge are written from it (the casing rim), and so is the space the casing leaves the holders (the lid's space); links references
casing.blendCasing (left, and its right twin) and slot placement, flush with its modules, with the slots shown where it cuts them (the casing's slots) and the lid where it sits (the lid's space); links the finger and thumb module files and lid
references.blendThe boards' KiCad geometry, one collection per board, and measured, drawn or missing purchased modules and screws, with provenance/status tags
scene/cat.blendThe Assembly assembled: places the fitted parts (./lx mech assemble); retains Logo, the logo artwork Logo-Cut, which cuts no part yet

The inserted part owns its shape and fasteners; the receiving part owns placement. Envelopes live beside the variants whose shape defines them. Links flow from key and references to modules to casing to scene; modules do not link one another. Do not make linked geometry local or bake copies into source parts. Export evaluated copies for slicing (exports).

associations.toml owns cross-domain relationships; dimensions.json owns fit requirements and unresolved dimensional conflicts. Presentation and visibility are authored in scene/; generated renders are not sources. Names put the side first (namings). A module's parts carry its stem from LYNXware.toml: left-finger-module-21-keys-cover, …-board (the imported board), …-board-placement, …-keycaps-placement, …-led-plugs-placement; other side-specific parts and containers read left-casing-body, right-finger-module. Shared helpers without a side keep their names (Finger-Key-Grid, Finger-Envelope). The generated module lookup lists each module's file and parts. Older names and the rename commits: namings.

Collections

A file shows its sets at the top level; the file already says what they belong to. First come the parts, the left ones, their right twins, then the rest: left-lid-plain, left-lid-gyro, left-lid-mouse, left-lid-mouse-adns5050, left-lid-plain-no-battery, …, right-lid-plain, …, Thumb-LED-Plug. A part set holds only its printed part (left-finger-module: the cover). What is placed under the part from other sets sits beside it in its imports set, <set>-imports: a module's board, the keycaps and LED plugs its cover takes, the mouse lid's sensor breakout, the screws each part takes and the gyro's washers (left-finger-module-imports, left-lid-mouse-imports; place_boards.py, place_cover_parts.py, ./lx mech cover-parts; place_screws.py, ./lx mech screws). The scene places both. Everything else sits in sets named <Prefix>-<What>, whose Outliner color says what their objects do to the part:

ColorFunctionShown by default
blueparts: left, right twins, variantsyes
redcutsno
greenunionsno
violetcontracts other files are built against (envelopes, Key-Cutout, Lid-Screws)yes
yellowhelpers: placement empties, origins, templatesno
orangereferences: context only, shaping no part, shown to see where things are and how they come together: a part's placed imports (left-finger-module-imports), boards and drawn references, the lid's space (Lid-Space), the lid on the casing (Casing-Lid-Preview); also the sets objects are linked into (Casing-linked), whose objects keep their own file's coloryes
browntest printsno
pinkunused: no modifier uses it, kept until decidedno
nonea feature group mixing functions (Lid-Switch, Thumb-LED)no

The part files draw every face in the color of the set it comes from (./lx mech colors, colors.py): a finished part shows its body blue, what a cut removed red, what an envelope or Key-Cutout cut violet, what a union added green, and the right twins the same grayed. The printed parts a module's cover takes, keycaps and LED plugs, draw one gray in the module file: their placements instance a <source>-print stand-in that realizes the part in that gray (Cover-Print), while the file modelling them paints them as parts. The paint is the color attribute function, which booleans and geometry nodes carry onto the result; materials stay what renders and exports show. An object in an untagged set is painted by how the booleans use it, or, when parts only take its geometry (Lid-Plate), like those parts. After adding an object, run ./lx mech colors; ./lx mech check fails until then, and also when a part set holds anything but meshes.

Each part file says what its colors mean where it opens: the set Legend, last at the top level, lies flat (read from above) left of the parts, a swatch per color with its meaning. ./lx mech colors draws it from colors.py's LEGEND_LINES, the same in every file; it cannot be selected, and nothing places or exports it. Hide it with its eye. The scene draws materials and has none.

A set a boolean takes as its collection operand holds exactly that operand, children included, so nest nothing else in it. Other files link the part sets and what the violet sets hold by name.

Booleans use the Manifold solver, modifiers and Mesh Boolean nodes alike: 10–20 times faster than Exact, so parts stay live while edited, and its results are closed. A new Boolean modifier starts on Exact; switch it. Manifold needs closed operands: given one with an open edge it skips the cut, with only a warning on the modifier ("non-manifold geometry"), and ./lx mech check fails on the part's faces or volume. Faces not flat are split along a diagonal each solver picks itself, so where that shapes a part, triangulate the operand (Casing-Volume-Cut).

The scene

scene/cat.blend holds no part geometry. ./lx mech assemble (assemble.py) places, per hand, what LYNXware.toml fits: the casing, the lid the bottom slot and the battery ask for (associations.toml bottom, battery) with the bottom module it holds, the finger and thumb modules with their boards, the battery and its power switch where the side has one, the main board, and once the tower. Each placement records whose it is (device, side), which the website's nodes carry. Each placement is an empty that instances a part set, and a second one its imports set where it has one, so the part is evaluated in its own file with every cutter and then moved as a whole. The receiving part owns the place: the casing's Casing-Finger-Slot-Pos and Casing-Thumb-Slot-Pos (where it cuts the module openings), the lid's Lid-Board-Pos, Lid-Switch-Pos and Lid-Battery-Pos; a right module sits at the left slot's mirror image. The keycaps and LED plugs come with their module: each cap sits in its key cutout at key.blend's modelled height, which is not yet where it rests on its switch (the keycap pocket, open issues). After moving a slot, the board or the switch, or changing the Assembly, run ./lx mech assemble; ./lx mech check fails until then. What the website gets of the scene, one node per selectable part and each view per hand, is scene_roots.py's. The scene's sets are grouped as the explosion views move them (views.json); visibility.json records its flags.

The main board, the power switch and the tower

No maker publishes a model we may redistribute, so all three are drawn (status = "drawn" in references.blend) from the published outline and the product photos, in their real colors as materials: recognizable, not fit-approved. Where a part sits and how big it is beyond the outline is read off photos and usual packages (ref_shapes.py holds the shapes and the palette).

The cat's main board is a BPI-Leaf-S3 with its header pins soldered pointing up (main_board_ref.py, References-Main-Board). Its outline, holes and pins are the KiCad part's (footprints.toml BPI-Leaf-S3, through wires.py), and the scene export fails when a pin the wiring harness wires is not drawn where that part has it. The scene seats it in the lid's board holder: antenna end against the holder's end wall, on the floor of the grooves it slides in, USB-C at the open end. assemble.py reads the seat off Lid-Board-Groove-Cut, so moving the holder moves the board. It sits as low as its USB cable allows: the casing keeps 3 mm under the cable's opening, over the lid seat (dimensions.json main_board; its underside 6.3 mm over the lid's face, 7.9 mm lower since the bottom module moved beside it). Drawn this way, its pin tails touch the holder's floor (0.05 mm) and its header strips the lip above the grooves (0.1 mm); the jumper wires on the pins are not modelled. Its battery socket (CN1) is drawn where the wiring harness puts it ([main_board.battery]), open toward the side the plug enters, and the scene export anchors the battery wires at its contacts.

The power switch is the SS-12F15-G4 slide switch on the battery's + lead (power_switch_ref.py, References-Power-Switch; its docstring separates the sourced dimensions from the eyeballed ones). Its pins are at the harness's pitch. The lid's Lid-Switch-Pos seats it in the holder beside the main board's left edge, about its middle and parallel to it, the holder merged with the board holder's rail (since 2026-10-06): its plate on the floor of the pocket Lid-Switch-Cut cuts, actuator down through the lid's floor, pins up, pin 1's end towards the main board's socket. The holder and the cut are the helper's children, so they move with it; the cut is symmetric, so the helper owns which way pin 1 points (turning it half a turn about its z changes no geometry), and assemble.py fails when the cut leaves the helper's centre, floor or long axis. The right switch is the left one turned about its length, which keeps pin 1 at the socket's end. The battery (References-Battery) is the hand-measured Lid-Battery-ref, the size authority, drawn in its own frame (the middle of its underside on the origin, +y toward its leads; the lid's Lid-Battery-Pos seats it, its two holders the helper's children, since 2026-10-08), with where its lead leaves the cell, eyeballed (battery_lead_ref.py): red and black stubs whose ends are empties naming the lead's conductor, which the scene export anchors the battery's wires at.

The tower is a LILYGO T-Dongle-S3 (LYNXware.toml [tower]), which ships in its own clear shell: LYNX prints no enclosure for it. It is drawn as it is plugged in, without its cap (tower_ref.py, References-Tower). With a BPI-Leaf-S3 tower ([tower] board), the scene lays the main board's set where the dongle would lie, USB-C to the right.

The battery and the power switch

A cat may run without a battery, from its USB cable to the computer or a USB power supply (build.toml [choice.battery]); it then fits no power switch either. So each lid but the ADNS-5050's comes twice: built on Lid-Plate-Battery (Lid-Plate with the switch's holder, its pocket and window through the floor, and the battery's two holders), or as -no-battery on Lid-Plate itself, the floor closed where the window would be and nothing to hold what is not fitted. The ADNS-5050's lid is only the latter: that sensor runs from the main board's 5 V pin, which only USB powers (build.toml rule battery-not-with-usb-only-sensor). Lid-Plate-Battery takes Lid-Plate's finished geometry (Geometry-From) and adds them after the screws: the screws neither reach the switch nor the holders, so the battery lids kept every vertex; only a few faces not quite flat split along other diagonals. A lid without a battery shares its sibling's imports: one placement of a bottom module's breakout and its screws sits in both lids' imports sets. ./lx mech check fails when a lid's lid plate starts from another plate than its battery says (check_associations.py).

The casing rim

The ring of casing around the lid is 2.1 mm wide all round where the slicer cuts the first layer, 0.1 mm over the bed, so Bambu Studio lays 4 walls there (dimensions.json casing_rim). Its P2S profiles print the first layer in 0.5 mm lines and shrink it by 0.15 mm of elephant-foot compensation: the ring of 4 × 0.4 mm it had before printed 3 lines. The lid owns that shape since 2026-10-10; the casing follows it. Draw the outline on Lid-Plate's base mesh, which every lid variant is built from: move its side faces, keep its thickness. ./lx mech lid-fit (lid_fit.py, geometry in rim.py) cuts the plate halfway up, grows it by the lid's clearance (0.08 mm a side, 0.12 until the first print moved too much) into the seat, and writes into lid.blend Lid-Envelope (the seat, and above the plate its step, reaching over the lid's screw bosses) and each Lid-Screw-N-Pos: a fixed distance inside the seat, facing across the rim, where it already sits along it, with its foot (the lid's feet). The seat grown by the rim is where the casing's outer surface must cut the first layer; the fairing below puts it there. To move a screw, move its empty along the rim and run it again. There are eight (fastener.casing_lid_screws count), placed since 2026-10-08 under the skates as Stas arranged them for the mouse lid; each pad sits over its screw as well. Two of them sit where the casing leaves little room: screws 5 and 6 beside the finger module's opening, which trims 2.8 and 0.7 mm³ off their casing bosses (FINDINGs: the opening wins, the holes keep 0.7 and 0.9 mm), and screw 8 under the board's stopper at the USB end, threading into it, its its hole as long as the others', its top about 0.27 mm under the cable's opening. A screw whose bosses or hole reach into the board, the battery, the switch, the bottom module, a holder or a module's envelope is a FINDING; the casing's USB cuts and the stopper it does not see, so keep a screw moved near them clear by eye. A finished casing in more than one piece fails: a casing boss reaches 0.6 mm past its old outer face along its slope, since on the waist the casing's cavity thins the wall and screw 3's boss once missed it. After reshaping the lid plate or the casing run it; ./lx mech check fails until then, and whenever the finished casings' rim leaves 2.1 mm there. Only that height is exact: the casing's lower edge flares out, so the ring widens above it.

lid-fit fairs that edge first (rim_fair.py, casing_rim.fair). The casing is drawn as a tilted cage, so the bed cuts its faces at a slant: the outline on the bed rippled at the cage's spacing and the flare jumped along the rim. Casing-Rim-Fair, the modifier after Casing-Slot-Conform, moves the surface sideways onto the lid's outline, the flare evened along the rim (its mean kept, so the wall still leans out as drawn): wholly up to 0.5 mm over the bed, fading to nothing at 3 mm, the surface as drawn above. lid-fit measures the shifts on the casing as drawn and stores them in Casing-Rim-Fair-Field (Casing-Helpers, hidden), so the cage stays yours: reshape it, then run lid-fit. The casing's Subdivision is level 3 for it: at level 2 the chords of its facets near the bed rippled the edge by themselves.

The casing's Triangulate right after Subdivision belongs to the rim: a subdivided surface's quads are not flat, and without it every boolean after them splits them along its own diagonal, moving the printed edge by up to 0.06 mm from what the tool measured.

For the same reason Casing-Volume-Cut has its own Triangulate: its quads are up to 2 mm out of plane, and the solvers split them differently by 0.8 cm³ of wall. On Manifold the casing recomputes in about 0.4 s at Subdivision 3 (0.2 s at 2, where Exact took 1.8 s); in Edit Mode it shows only its surface up to the conform, and the fairing and the cuts follow on leaving it (each modifier's Edit Mode toggle), so dragging stays live.

The lid's feet

Each foot covers a casing/lid screw: its pocket (Lid-Pads, Lid-Skates) sits over the screw and holds the head in a smaller pocket, so the pad or skate stuck in it hides the screw (dimensions.json lid_feet). The lid prints on its outer face, so the pocket, the head's pocket and the screw's hole open onto the bed one inside the other, and the layer closing each lies over air with the next opening in it. feet.py draws each closing as two steps one layer high: a slot as wide as the opening above, across the opening below from wall to wall, so the strands beside it bridge it anchored at both ends; then, bridged across the slot, the square the opening above starts from. The lid prints at that layer height (build.toml).

./lx mech lid-fit writes them with the screws: Lid-Pad-N and Lid-Skate-N, a mesh each, seated on Lid-Screw-N-Pos and cut from the outline of Lid-Screw-Template-Pad or -Skate (beside the screw template: edit a foot's shape there). A pocket sits offset_mm inward of its screw, its length along the rim; where the plate's edge bends closer than edge_lines (a skate at the corner of screw 2) it turns and slides along its length, the head's pocket staying on the screw and inside it. The lid's boss is a D round the screw that walls the head's pocket. The casing prints on its bottom face, so its boss grows out of the wall over air: its underside is one slope boss_underside_deg off the bed (fastener.casing_lid_screws), and the lid boss's top follows it, under the lid clearance. ./lx mech check fails when a foot or a template differs from what the templates and lid_feet give, or when a casing boss faces the bed flatter than that slope.

The lid's space

The holders are placed in lid.blend against the space the casing leaves them: the orange set Lid-Space, a reference that shapes and prints nothing. Lid-Space-Casing is the casing's inner volume (its shell within Casing-Volume-Cut, less what the finished casing holds there: the screw bosses, the board's stopper); Lid-Space-Finger-Envelope and Lid-Space-Thumb-Envelope, in its violet Lid-Space-Envelopes (an envelope is a contract wherever it is drawn), are the module envelopes where the casing cuts their openings. All three are wireframes that cannot be selected, and copies: lid.blend cannot link the casing back, because the casing links lid.blend (a linked casing loses its seat and screws). ./lx mech lid-fit writes them with the rim, after reshaping the casing, moving a slot or changing an envelope; ./lx mech check fails until then.

Lid-Space-Main-Board shows the main board on its seat in the holder (seats.py, the scene's seat), parented to Lid-Board-Pos: moving it moves the board with its holder, live; Lid-Space-Power-Switch the power switch on Lid-Switch-Pos, Lid-Space-Battery the battery on Lid-Battery-Pos, which carries its holders, as the scene seats them; the mouse lid's imports show its breakout where the lid holds it (the bottom module). ./lx mech lid-fit and its check print a FINDING for the main board, the breakout, the power switch, the battery or a holder reaching into an envelope or out of the inner volume, for the main board, pin tails included, or the breakout reaching into the battery holders, the switch's holder or the bottom module's holder, and for the battery's cell reaching into a holder (its own too), the main board or the breakout, by the overlap's volume. They see the parts as drawn: not the jumper wires on the board's header pins nor the wires soldered to the switch's, which stand above the pins towards the modules. After moving a helper, ./lx mech assemble places the board, the switch and the battery in the scene. The holders are drawn in their helpers' frames, so a helper carries its holder along; where the casing or an envelope then cuts into one, its mesh is trimmed by hand (the board holder's ends, cut for the board's place on 2026-10-06), and a screw whose boss the USB cuts reach moves along the rim (the casing rim).

The casing's end of the board follows Lid-Board-Pos as well. Casing-Board-Stopper (green) stops the board at its USB end, and Casing-USB-Cuts (red: Casing-USB-Plug-Cut, Casing-USB-Cable-Cut) opens the wall for the plug and its cable. The casing's booleans add the stopper right after the lid cut, so the lid's envelope does not trim it: it reaches down to the seat and touches the lid near screw 8 (Stas's order since 2026-10-08, open issues); they cut the USB openings after the screw bosses and holes, so an opening wins over a boss, and the slot cuts last, so a module's opening wins over all of them. The stopper's own Triangulate keeps its hand-drawn quads, up to 0.58 mm out of plane, from leaving the casing with n-gons whose split changes its shape (as Casing-Volume-Cut's does). All three are parented to Lid-Board-Pos, linked from lid.blend. Once lid.blend is saved, casing.blend picks up a moved board on its next load (in a running Blender, reload the library). ./lx mech check fails when one of them loses that parent, and when the casing under the cable's opening is thinner than main_board.usb_wall_mm over the lid seat: the board sits too low.

The other way round, the casing is edited against the lid: casing.blend's orange Casing-Lid-Preview draws lid.blend's left-lid-mouse-body, finished, where it sits, solid and in the reference orange in place of its own paint (its node group stores it), and cannot be selected. It is the lid the left hand fits; the gyro lid differs from it only in the holder's pocket and underneath, the plain lid by no holder. Only the left one: the right casing mirrors it. It is linked, so it follows the lid as the seat and the stopper do. ./lx mech lid-fit draws it where it is missing; ./lx mech check fails when it draws another lid, sits elsewhere, or evaluates in casing.blend to another shape than lid.blend builds.

The bottom module

A bottom module has no printed part of its own: each kind of bottom slot has its own lid, and the module's breakout screws into it (dimensions.json bottom_module), so a hand prints one lid and nothing else. Which way a hand pushes the sensor is the tower's to set: it turns each hand's motion by the lid's mounting yaw plus the user's trim (firmware input_engine_rules::motion_yaw, the YL/YR keys in Vial settings), so nothing turns in the lid, and the main board sits low beside the module. bottom_module.py (./lx mech bottom-module) builds it in lid.blend, in the frame of Lid-Bottom-Module-Pos, left of the main board: its origin on the lid's face, y along the module. The gyro and mouse lids add Lid-Bottom-Module-Holder (green), a block from inside the plate up to the breakout board's top, and cut their module's set (red, in Lid-Cuts): Bottom-Gyro-Cuts, the GY-521's pocket, the board flush and 0.2 mm a side, a channel under its header's row for the pin tails and its two screw holes on the other side (one-sided, so the right lid holds it turned half round); bottom-module-mouse-sensor-pmw3389-cuts, the breakout's pocket, flush, its two screw holes, the lens's seat, the room over it up to the board (a tongue left round each screw) and the window under the lens through the plate, narrowing towards the face. The lens hangs loose on the chip's guide posts, so the lid holds it as PixArt's base plate does: its flange rests on the seat's floor, the walls round it stop it moving across, and the board on its pocket's floor holds it down. The holder and the cuts are children of the helper, so moving it moves them; each lid takes them right after lid plate in its stack.

The breakout's references (drawn from PixArt's datasheet by mouse_sensor_ref.py: the board, the chip and the lens) sit in the mouse lid's imports with the lens flange 2.4 mm over the desk, which lies under the lid's face by what the skates stand proud. The holder and every cut are symmetric about the helper's x = 0, so the right lid, the left one mirrored, holds the same breakout turned: its placement in right-lid-mouse-imports is MIRROR · helper · FLIP_X. The GY-521's references (gyro_ref.py: the board, its parts and its header, after Fritzing's part and a KiCad redraw) sit in the gyro lid's imports on the pocket's floor; the gyro's cuts read the pocket, the holes and the header's row off them. Its right placement is MIRROR · helper · FLIP_X turned half round about z, as its cuts are not symmetric. The v3 ADNS-5050 module (BM-ADNS5050) has a lid of its own, left-lid-mouse-adns5050-body and its recipe twin, which bottom_module.py makes from the mouse lid's lid plate and skates where it is missing. Its board is v3's, 35 × 25 mm without screw holes, so the lid holds it in a pocket 0.25 mm a side under a lip over the middle of each long edge, 45° under and over; each lip stands on an arm the lid's slots cut free behind it, at its ends and beside its foot, so it gives as the board snaps in. The board rests on a ledge at the datasheet's height (its top 7.45 mm over the desk), the lens's flange on a seat 2.4 mm over the desk, the room between them taking the lens's body and the leads' tails, the window under the lens round the lens's optics, its prism, the sensor's view and the light's paths between them, and nothing rising into the board's cutout (dimensions.json bottom_module adns5050). Its references (adns5050_ref.py: the board after the v3 EasyEDA board, the stack and the lens's flange and optical centre after Avago's datasheets, the rest of the discontinued lens after the v3 model) sit in its imports. The board is not its own mirror image, and its 1×6 header faces the main board in both hands, so the right lid holds it turned end for end (MIRROR · helper · its place · FLIP_Y), not across as the PMW3389's: each lid has its own holder (left-/right-bottom-module-mouse-sensor-adns5050-holder, green) and cuts (…-cuts), both drawn in the left lid's frame, the right ones mirrored with the right lid, a recipe twin that takes them in place of the left lid's (associations.toml twin_operands, sides.py). Each sits where bottom_module.py's A_PLACE puts it in the helper, clear of the power switch's pocket (1.5 mm), the battery (0.3 mm) and the main board's groove (0.13 mm), which the check measures (the lid takes neither the switch nor the battery, but the module stays where it was test-printed); lid-fit's findings see both lids' holders and boards. ./lx mech check fails when the holder, a cut, a placement or a lid's stack is stale, the lens leaves 2.2–2.6 mm over the desk or its seat's floor, the seat leaves it more than 0.15 mm to a wall, or the window lets light past the lens; and when the helper's turn and the sensor's on it no longer give bottom_module.mounting_yaw_deg, which the tower undoes (assemble.py; tools/lx/test_invariants.py holds it to the firmware). Move the helper, and both follow from that number: set it there and in the firmware.

The casing's slots

The envelopes lie at the origin in their module's frame; the casing's modifiers move them onto Casing-Finger-Slot-Pos and Casing-Thumb-Slot-Pos (casing_slots.py, ./lx mech casing-slots).

The casing is flush with its modules: Casing-Slot-Conform, right after Triangulate, moves the casing surface along each slot's axis onto the envelope's top edge, where the module's cover ends, with the whole gap within Plateau of the edge and fading out by Band, so the sculpted slope is kept and only the height near the opening changes. The sculpted casing was up to 0.4 mm off; ./lx mech check fails above 0.1 mm. The inner volume does not follow, so the wall near an opening is thicker or thinner by what the conform moved. Sculpt the casing as before; the conform follows the surface, the slot helpers and the envelopes by itself.

Casing-Slot-Cuts cuts each opening with Casing-Slot-Cutter: the envelope with its upward faces extruded by Extend out through the casing surface. The casing file draws with the same group each envelope where the cut places it: Casing-Slot-Envelopes (violet), a wireframe in front of the casing that cannot be selected, since the casing is edited against it. It takes the cut's envelopes and slots, so moving a slot helper or reshaping an envelope in its module file (saved, then reloaded) moves cut and preview together. Nothing to rerun; ./lx mech check fails when the preview stops drawing the cut's inputs. The modules themselves are seen in the assembled scene: linked into the casing file, they would load their boards and keycaps with every run on it.

The joystick thumb

left-T4J and right-T4J (four keys and a PS4 stick) are catalogued but fitted to no slot, and drawn in thumb-module.blend beside the T11 covers. The left board is placed on the screws they share and the right one is its mirror (place_boards.py). Each cover sits in its own part set, left-thumb-module-4-keys-joystick and right-…, with its imports set beside it. ./lx mech joystick (joystick.py) owns everything of it that is not the board: the cover is the left T11 cover's mesh and modifier stack with only the key cutouts its board has switches under (Thumb-Joystick-Key-Cuts), a countersunk seat for the dome with the notches its lugs drop through, and the stops that end their twist; the dome (Thumb-Joystick-Dome) and the cap (Thumb-Joystick-Cap) are their own part sets, one print for either hand. All of it is built from the stick its board places and the numbers in dimensions.json joystick: the stick's pivot, lever and housings measured off Alps' 3D model, the gate angle, the gap and the fits.

The cover prints as T11's does, standing on its tilt cut, which would lay a dome on it sideways: the gate's circle would print stair-stepped across the surface the cap slides on. So the dome prints apart, standing on its skirt, its gate made of round perimeters, its 45° inner roof needing no supports, and twists into the cover. The cap prints head down. Its neck runs against the gate at 22°, before the stick's own 23° stop, and its head's underside and the dome's top are spheres about the stick's pivot, so their gap stays the same at every tilt. ./lx mech check measures that on the finished parts (joystick.py's GATE, GAP, STICK, SEAT, LOCK and BOARD lines). After the T11 cover's modifiers, the board or the numbers change, run ./lx mech joystick and then ./lx mech cover-parts, which seats a dome and a cap on the stick (<stem>-stick-placement).

The right cover is the left one mirrored (sides.py, a live twin), with the same dome and cap, each its own mirror image. The stick itself cannot be mirrored: right-T4J carries it in left-T4J's orientation on the mirrored axis, so both hands read the same directions. The countersink is round the stick, so it fits either way. The notches lie on the other side of the lock, so on the right hand the dome locks turning counter-clockwise. joystick.py measures STICK, SEAT, LOCK and BOARD on both sticks, each in its own cover.

Left and right

Work on the left part; the right one follows. Every right part is built from its left part by sides.py, mirrored across the side plane x = 55 mm (MIRROR_X, bootstrap.py): the same plane in every file, so right parts fit each other as the left ones do, and off the left parts, so the right side sits beside them. A file shows which side is worked on: the left part keeps its color under the arrow left-edit-here, and the right twin wears the same hue grayed.

  • Live mirrors (casing, lids, thumb cover) take the left part's finished geometry, whatever changed, and need no rebuild.
  • The right ADNS-5050 lid is a recipe twin too: the left lid's modifiers with the right lid's own holder and cuts (twin_operands), which hold the board turned end for end (the bottom module); a changed modifier needs ./lx mech sides.
  • The right finger cover is not a mirror image: right-F20W has 4 middle keys and the wheel window. It shares the left cover's mesh and a copy of its modifiers with the declared differences in associations.toml (key counts from the Assembly, the wheel cut switched on). After adding or changing one of the left cover's modifiers, run ./lx mech sides; ./lx mech check fails until then.
  • Right boards are real right-hand boards, never mirrored geometry. Their placement is the left one mirrored, kept by drivers.
  • Keycaps and LED plugs are the same print in both hands, turned (MIRROR · pose · FLIP_X), which is the mirror image only of a part that is its own. place_cover_parts.py checks both are and fails once one is not: that hand would then need a mirrored print.

Cut logos and text after the mirror, on each side's part, or they read backwards.

Verify and preview

The files are authored with one Blender release, BLENDER_VERSION in bootstrap.py: the STL check compares exact bytes, and a file saved by a newer Blender may not open in an older one. ./lx mech check first fails on another major.minor and warns on another patch release (check_blender.py); moving to a new release is a deliberate change of that constant, with the files re-saved and the STLs re-exported. The tools save the files zstd-compressed: Git LFS keeps every version whole.

See Blender tools for commands. Run ./lx mech check after geometry or link edits: linked-part regression, right-hand twins, associations, reference status, the scene's placements, the scene views per hand and the build steps' nodes, the boards' fit, the keycaps and LED plugs, the joystick thumb's fit, and the STLs. Use python3 tools/blender/split_parts.py accept for propagation validation; it temporarily edits and restores geometry, so do not run it concurrently with another editor.

Geometry-node object/collection pointers do not count as Blender users. Keep linked operands pinned in their excluded linked collection or a save can drop them. The checks cover missing operands as well as file paths and dimensional regression.

Provenance

Copied from CATv4_069.blend on 2026-09-24, SHA256 67ffbf0bb69e9d134d19339d0e5c06036c54b2fa5f92ab12983286b9fb646f85. The snap-linked node groups were localized, names normalized, baked interface copies replaced with live references, and the file split. The joystick-thumb and ADNS-5050 base drafts, the slim casing and the 45 wall gauges lay in the scene's excluded Archive until 2026-09-26; the milestones and the Git history of scene/cat.blend hold them. Historical dimensions used for checks are in regression_baseline.json.