Scroll case config reference¶
Scroll cases are configured with a TOML file passed to scrollcase build -c.
Every key is optional; anything left out comes from the config's style
preset. Print a style's complete config:
uv run scrollcase defaults --style educelab.v1 > my-scroll.toml
Unknown keys are an error, so typos don't silently fall back to defaults. Combinations that can't be built are also rejected when the config loads (see Valid combinations). Lengths are in millimeters and angles in degrees.
Styles¶
style picks a preset of defaults for every key below. Your own values
override the preset key by key, so [shell] type = "solid" keeps the rest of
the preset's [shell] settings, including its marker rings.
| Style | Description |
|---|---|
educelab.v1 |
The default: honeycomb shell with floor and lid, flat split, Generic Mount Disc, stand. The former OpenSCAD generator's design. |
villa.2026-10 |
ScrollPrize/villa's case at commit 7d6a82c (2026-10-01): no shell, curved split ending in round posts, square bolted caps with upstream's label layout, kinematic mount disc, 3 mm margins, no stand. |
Components mix freely within the valid combinations, e.g. villa's caps and kinematic disc around a honeycomb shell:
style = "educelab.v1"
[split]
type = "curve"
[ends]
type = "caps"
[mount]
type = "kinematic"
[stand]
enabled = false
villa.2026-10 reproduces upstream's case bodies exactly; see the
comparison. The known differences are all in
the mesh stage: the scroll fit uses a tighter tolerance, the wide-axis
rotation normalizes its vector before taking the angle, voxel_size is
absolute (upstream uses 0.4% of the mesh diagonal), and "denoise"
smoothing uses smoothing_amount (upstream ignores its strength setting).
Released styles never change, so a config rebuilds the same case for as
long as it names the same style. A design update becomes a new version
(e.g. educelab.v2). Omitting style means educelab.v1, permanently.
Case coordinates. The case axis is +Z and the bottom of the shell is Z=0; the mount disc sits below it, from Z=-12.5 to 0. The halves split on the XZ plane: the left half (with the mount disc) is on the -Y side and the right half is on the +Y side.
Top level¶
| Key | Default | Description |
|---|---|---|
style |
"educelab.v1" |
Preset to resolve the rest of the config against. See Styles. |
name |
"Scroll Case" |
Output file prefix: <name>-L.stl, -R.stl, -Stand.stl, -Scroll.stl. Overridden by --name. |
lining_offset |
2.0 |
Clearance between the scroll surface and the inside of the lining. Also the gap above and below the scroll. |
wall_thickness |
2.0 |
Thickness of the lining wall, shell, lid, and half of the divider. The floor is max(2, wall_thickness). |
bottom_buffer |
5.0 |
Gap between the floor and the bottom of the lining. |
top_buffer |
5.0 |
Gap between the top of the lining and the lid. |
internal_gap |
3.0 |
Radial gap between the lining and the inside of the shell. |
overhang_removal |
true |
Extrudes each half's cavity toward the split so the scroll can be lowered straight in. Turn off only to inspect the raw lining. |
voxel_size |
0.4 |
Resolution of the lining offsets and overhang removal. Smaller is more faithful but slower and uses more memory; the PHercParis scrolls peaked at 3.5–5 GB at 0.4. |
[scroll]¶
The input mesh and how it is prepared and placed.
| Key | Default | Description |
|---|---|---|
mesh |
none | Scroll mesh (PLY, STL, or OBJ). A relative path is resolved against the config file. Overridden by the positional mesh argument. If unset, a generic cylinder is used. |
scale |
1.0 |
Multiplies the mesh coordinates. Use it if the mesh isn't in millimeters. |
auto_align |
true |
Fits the smallest cylinder that encloses the scroll, stands the scroll up along +Z with its bottom at Z=0, and turns its widest direction into the split plane. With false, the mesh keeps its own orientation (its Z axis becomes the case axis) but is still centered on the axis with its bottom at Z=0. |
rotate |
[0, 0, 0] |
Extra rotation in degrees about the X, then Y, then Z axes, applied after alignment. The scroll is then re-centered on the case axis, so only the orientation changes. Z spins the scroll about the case axis, e.g. [0, 0, 90] puts the widest direction across the split instead of in it. X/Y tilt it; the case grows to enclose the tilted scroll. |
translate |
[0, 0, 0] |
Offset applied after re-centering. An X/Y offset deliberately moves the scroll off the case axis, and the case grows to enclose it. Z has no effect, because the scroll is always placed lining_offset above the cavity floor. |
decimate_max_error |
0.05 |
How far, at most, the simplified input mesh may deviate from the original. |
smoothing |
"none" |
Simplifies a very detailed lining: "denoise" or "shrink_expand". The smoothed result is merged with the original, so smoothing never cuts into the scroll. |
smoothing_amount |
2.0 |
For "denoise", the smoothing strength (meshlib gamma). For "shrink_expand", the shrink/expand distance in mm; larger values round off more detail. |
generic_diameter |
76.0 |
Diameter of the generic cylinder used when mesh is unset. |
generic_height |
155.0 |
Height of the generic cylinder used when mesh is unset. |
[split]¶
The surface the two halves separate along. Every split is a profile y = f(x) extruded along Z, so from anywhere in a half the scroll can move straight along Y to the split; overhang removal relies on this.
| Key | Default | Description |
|---|---|---|
type |
"plane" |
"plane": the XZ plane. "curve": ScrollPrize/villa's S-shaped split, made of two arcs, spanning the whole case out to the middle of the shell wall. A curved seam avoids a flat interface lined up with the beam, which shows up as a streak in CT. |
amplitude |
unset | How far the S bulges from the XZ plane. Unset means 0.2 × (span + wall_thickness), as upstream, where the span is the radius at the middle of the shell wall. Too large an amplitude is an error. |
flip |
false |
Mirrors the S. |
With a curved split the divider follows the curve at constant thickness, and nubs move along Y to sit on it.
[shell]¶
The outer cylinder around the lining, including its floor and lid.
| Key | Default | Description |
|---|---|---|
type |
"honeycomb" |
"honeycomb" (hexagonal cutouts), "solid", or "none". With "none" the case is just the lining, divider, and ends, as upstream; the divider then ends in round posts at the cavity edge. |
open_top |
false |
Cuts away the upper shell on both sides, leaving an open cradle around the lining. |
marker_rings |
true |
Raised rings at the cavity bottom, scroll bottom, scroll middle, scroll top, and cavity top, on a honeycomb or solid shell. Ignored with type = "none", since there is no shell to put them on, so switching a preset to no shell doesn't also require turning them off. |
[shell.honeycomb]¶
Hexagonal cutouts, used when shell.type = "honeycomb". The floor, lid,
marker ring bands, and divider stay solid.
| Key | Default | Description |
|---|---|---|
hole_edges |
6 |
Number of sides on each hole. |
columns |
12 |
Holes around the circumference. Alternate rows are offset by half a column. |
spacing |
1.5 |
Width of the strips between holes. |
[ends]¶
What closes the top and bottom of the case.
| Key | Default | Description |
|---|---|---|
type |
"shell" |
"shell": the shell's own floor and lid. "caps": villa's square end caps, sized to the larger of the mount disc and the case (plus one wall_thickness on each side around a shell). Bolts through tabs on both sides clamp the halves together; the bottom cap has counterbored mounting holes and an engraved arrow; the top cap carries the label. |
cap_height |
10.0 |
Cap thickness. Also the width of the bolt tabs. |
corner_fillet |
6.25 |
Radius of the caps' corners. |
bolt_hole_diameter |
5.0 |
Clamping bolt clearance hole (M4, loose). |
bolt_counterbore_diameter |
8.0 |
Bolt head counterbore, on the +Y face of each tab. |
bolt_counterbore_depth |
2.0 |
|
nut_diameter |
9.0 |
Hex nut pocket, across corners, on the -Y face of each tab. |
nut_depth |
3.5 |
|
mount_hole_spacing |
50.0 |
Mounting holes in the bottom cap at (±spacing, ±spacing). The build fails if the counterbores don't fit on the cap, e.g. on a small case with no mount disc. |
mount_hole_diameter |
6.8 |
Mounting bolt clearance hole (M6). |
mount_counterbore_diameter |
10.5 |
Counterbored from the inside face of the bottom cap. |
mount_counterbore_depth |
5.0 |
The cap_*, bolt_*, nut_*, and mount_* keys only apply to caps.
[mount]¶
The mount that attaches the left half to the scanner.
| Key | Default | Description |
|---|---|---|
type |
"generic-112.5" |
"generic-112.5" or "generic-65": the Generic Mount Disc of that diameter, matching OpenSCAD/Generic Mount Disc*.scad, with an orientation notch on its rim. "kinematic": villa's 112.5 × 10 mm disc with three V-slots for a kinematic mount; the right half's side of its top is lowered 0.5 mm so the right half doesn't bind on it. "none": no mount. |
[nubs]¶
Alignment nubs on the left half's split face, with matching sockets on the
right half. Place them on the divider, outside the cavity and inside the
case: the build fails, naming the nub, if one isn't. scrollcase build
prints the lining and shell diameters. On a curved split each nub sits at
(x, f(x), z), still pointing along Y, with its base sunk into the divider so
it stays attached where the surface slopes.
| Key | Default | Description |
|---|---|---|
positions |
[] |
[x, z] pairs, e.g. [[-44, 20], [44, 150]]. The nub is placed at X = -x, as in the old OpenSCAD generator. Odd-numbered entries (the 2nd, 4th, …) are rotated 45° into diamonds. |
size |
4.0 |
Width and height of each nub. |
depth |
1.5 |
How far each nub sticks out from the split face. |
margin |
0.5 |
Clearance on every side of the socket. |
[escape_holes]¶
Four holes per half: two through the floor and two through the lid, placed at
±angle from the X axis in the gap between the lining and the shell. On the
left half, the floor holes go through the mount disc as well.
| Key | Default | Description |
|---|---|---|
enabled |
false |
Adds the holes. |
offset |
1.5 |
Moves the holes outward from halfway between the lining and the shell. Holes that would be tangent to a shell surface are nudged inward by 0.1 mm. |
diameter |
4.0 |
Hole diameter. |
angle |
15.0 |
Angle of each hole from the X axis, in degrees. |
[label]¶
With shell ends, the label is engraved on the bottom of the mount disc (left
half) and the bottom of the shell (right half), mirrored so it reads
correctly from below. It is also engraved on the stand's back plate, on the
face toward the case. With caps it goes on the top cap in upstream's layout:
line1 once on each half, line2, and a generated <lining D> x <cavity H>
line.
| Key | Default | Description |
|---|---|---|
line1 |
"" |
First line. Empty lines are skipped. |
line2 |
"" |
Second line, placed 1.5 × height below the first. |
height |
5.0 |
Font size. |
depth |
0.5 |
Engraving depth. |
font |
"Arial Rounded MT Bold" |
System font name. If the font isn't installed, Arial is used instead. OpenCASCADE's warning about this is hidden when the build succeeds. |
[stand]¶
Cradle that holds the left half upright on its mount disc during assembly.
| Key | Default | Description |
|---|---|---|
enabled |
true |
Builds <name>-Stand.stl. --parts defaults to left,right, plus stand when this is on. |
length_scale |
1.0 |
Stand length as a fraction of the case height. The top cradle moves with it. |
Valid combinations¶
These are checked when the config loads, and the error names the keys involved.
| Rule | Why |
|---|---|
ends.type = "shell" requires a shell |
The floor and lid are part of the shell. |
shell.open_top requires ends.type = "shell" |
It cuts away the shell's lid. |
escape_holes require a shell |
They sit in the gap between the lining and the shell. |
stand.enabled requires a generic-* mount and ends.type = "shell" |
The cradle is shaped around the Generic Mount Disc, its notch, and the round shell. |
How the dimensions stack up¶
From the fitted scroll radius r and height h:
cavity diameter = 2 (r + lining_offset)
lining diameter = cavity diameter + 2 wall_thickness
shell inner diam. = lining diameter + 2 internal_gap
case diameter = shell inner diam. + 2 wall_thickness
case height = floor + bottom_buffer
+ wall_thickness + (h + 2 lining_offset) + wall_thickness
+ top_buffer + lid
The floor is max(2, wall_thickness) and the lid is wall_thickness; with
caps both are cap_height. Without a shell, the case diameter is the
lining diameter.
scrollcase build prints the scroll, lining, shell, and case sizes.