holder: expose all 59 scad parameters auto-extracted from the .scad source
The bundled hex_cell.scad has many more knobs than the dozen we used to expose (box clearances, cap/box wall, vertical stacking, busbar template, etc). Maintaining a hand-curated PARAMS list in Python next to a SCAD file that already documents every variable inline was always going to drift. Sync + auto-extract approach: - scad/hex_cell.scad: replace with the upstream master file from Albert Phan's Hex-Cell-Holder fork (adds the BUSBAR TEMPLATE section and the "busbar template" part option). - holder.py: PARAMS now built at import time by _scan_scad(), which regexes top-level `name = literal; // help` lines into Param entries up until the // END OF CONFIGURATION marker. Scan handles bool / number / string literals; derived expressions and helper variables are skipped automatically. - Manual maps stay small and explicit: _SELECT_OPTIONS for the few string-enum params (part, pack_style, box_style, template_outline, template_hole_style, etc.), _GROUP_RULES for UI sectioning, and _NUMBER_HINTS for sensible min/max/step on the most-tweaked numbers. UI: - holder-app.js: extra GROUP_ORDER / GROUP_LABELS for the new sections (cap, box, insulator, bolts, wires, stacking, template, advanced). Group titles are now click-to-collapse; the advanced groups start collapsed so the form isn't a wall of inputs on load. - holder.css: caret marker on the group title, smooth rotate on collapse, hides the body via .collapsed class. Net effect: every variable in the .scad — including the new busbar template knobs — is editable from the page, with helpful comments copied straight from the .scad source.
This commit is contained in:
@@ -62,46 +62,142 @@ class Param:
|
||||
help: str | None = None
|
||||
|
||||
|
||||
PARAMS: list[Param] = [
|
||||
# Pack geometry
|
||||
Param("part", "Part", "select", "holder", group="part",
|
||||
options=["holder", "cap", "box lid", "box bottom", "insulator"],
|
||||
help="Which piece to generate."),
|
||||
Param("pack_style", "Pack style", "select", "rect", group="part",
|
||||
options=["rect", "para", "tria"],
|
||||
help="Cell arrangement."),
|
||||
Param("wire_style", "Wire style", "select", "strip", group="part",
|
||||
options=["strip", "bus"],
|
||||
help="strip = nickel strips between cells; bus = bus wires between rows."),
|
||||
# ----- automated scan of the bundled .scad ----------------------------------
|
||||
# We extract top-level `name = literal; // help` lines into Param entries.
|
||||
# Lines whose RHS isn't a constant number / bool / string (e.g. derived
|
||||
# expressions like `box_total_height = get_mock_pack_height() + ...`) are
|
||||
# skipped — those would still take their default value from the .scad itself.
|
||||
|
||||
# Cell
|
||||
Param("cell_dia", "Cell diameter (mm)", "number", 21.2, group="cell",
|
||||
min=10, max=40, step=0.1, help="21.2 for 21700, 18.4 for 18650, 26.5 for 26650."),
|
||||
Param("cell_height", "Cell height (mm)", "number", 70.0, group="cell",
|
||||
min=30, max=200, step=1),
|
||||
Param("wall", "Wall thickness (mm)", "number", 0.8, group="cell",
|
||||
min=0.4, max=3.0, step=0.1,
|
||||
help="Wall around one cell; spacing between cells is 2× this."),
|
||||
# Param categories — used both for grouping in the UI and for picking the
|
||||
# select-typed params (those with a fixed option set). Keep keys in scad
|
||||
# variable-name space; group order/labels are the rendering hint.
|
||||
_SELECT_OPTIONS: dict[str, list[str]] = {
|
||||
"part": ["holder", "cap", "box lid", "box bottom",
|
||||
"wire clamp", "insulator",
|
||||
"vertical box section", "busbar template"],
|
||||
"part_type": ["normal", "mirrored", "both", "assembled"],
|
||||
"pack_style": ["rect", "para", "tria"],
|
||||
"wire_style": ["strip", "bus"],
|
||||
"box_style": ["bolt", "ziptie", "both"],
|
||||
"template_outline": ["rect", "hull"],
|
||||
"template_hole_style": ["engrave", "through", "center"],
|
||||
}
|
||||
|
||||
# Pack size
|
||||
Param("num_rows", "Rows", "number", 6, group="size", min=1, max=40, step=1),
|
||||
Param("num_cols", "Columns", "number", 12, group="size", min=1, max=40, step=1),
|
||||
|
||||
# Holder
|
||||
Param("holder_height", "Holder height (mm)", "number", 10.0, group="holder",
|
||||
min=4, max=30, step=0.5),
|
||||
Param("slot_height", "Slot height (mm)", "number", 3.0, group="holder",
|
||||
min=0, max=10, step=0.5,
|
||||
help="Height of all wire slots (set 0 for no slots)."),
|
||||
Param("col_slot_width", "Column slot width (mm)", "number", 8.0, group="holder",
|
||||
min=0, max=20, step=0.5),
|
||||
Param("row_slot_width", "Row slot width (mm)", "number", 8.0, group="holder",
|
||||
min=0, max=20, step=0.5),
|
||||
Param("cell_top_overlap", "Cell top overlap (mm)","number", 3.0, group="holder",
|
||||
min=0, max=10, step=0.5,
|
||||
help="Opening dia = cell_dia − 2 × this. Welding-window size."),
|
||||
# How to group each scad var into UI sections. First match wins; "advanced"
|
||||
# catches the rest.
|
||||
_GROUP_RULES: list[tuple[str, list[str]]] = [
|
||||
("part", ["part", "part_type", "pack_style", "wire_style", "box_style"]),
|
||||
("cell", ["cell_dia", "cell_height", "wall"]),
|
||||
("size", ["num_rows", "num_cols"]),
|
||||
("holder", ["holder_height", "slot_height", "col_slot_width",
|
||||
"row_slot_width", "cell_top_overlap"]),
|
||||
("cap", ["cap_wall", "cap_clearance"]),
|
||||
("box", ["box_lip", "wire_clamp_add", "box_wall", "box_clearance",
|
||||
"bms_clearance", "box_bottom_clearance",
|
||||
"box_wire_side_clearance", "box_nonwire_side_clearance"]),
|
||||
("insulator",["insulator_as_support", "support_z_gap", "insulator_tolerance"]),
|
||||
("bolts", ["bolt_dia", "bolt_head_dia", "bolt_head_thickness",
|
||||
"ziptie_width", "ziptie_thickness", "bolt_dia_clearance",
|
||||
"wire_clamp_bolt_dia"]),
|
||||
("wires", ["wire_diameter", "clamp_factor", "wire_hole_width",
|
||||
"wire_hole_length", "wire_top_wall", "clamp_plate_height"]),
|
||||
("stacking", ["stacking_pins", "stacking_pin_dia", "stacking_pin_alt_style",
|
||||
"stacking_bolts", "stacking_bolt_dia", "num_pack_stacks",
|
||||
"stacking_pins_tolerance"]),
|
||||
("template", ["template_2d", "template_outline", "template_thickness",
|
||||
"template_margin", "template_mark_dia", "template_hole_style",
|
||||
"template_line_width", "template_engrave_depth",
|
||||
"template_center_mark", "template_center_mark_dia"]),
|
||||
]
|
||||
|
||||
# Tighter numeric input hints (min/max/step) for selected params. Anything
|
||||
# not listed here gets a free-form number input.
|
||||
_NUMBER_HINTS: dict[str, dict[str, float]] = {
|
||||
"cell_dia": {"min": 10, "max": 40, "step": 0.1},
|
||||
"cell_height": {"min": 30, "max": 200, "step": 1},
|
||||
"wall": {"min": 0.2, "max": 3, "step": 0.1},
|
||||
"num_rows": {"min": 1, "max": 50, "step": 1},
|
||||
"num_cols": {"min": 1, "max": 50, "step": 1},
|
||||
"holder_height": {"min": 4, "max": 30, "step": 0.5},
|
||||
"slot_height": {"min": 0, "max": 10, "step": 0.5},
|
||||
"col_slot_width": {"min": 0, "max": 20, "step": 0.5},
|
||||
"row_slot_width": {"min": 0, "max": 20, "step": 0.5},
|
||||
"cell_top_overlap": {"min": 0, "max": 10, "step": 0.5},
|
||||
"cap_wall": {"min": 0.4, "max": 5, "step": 0.1},
|
||||
"box_wall": {"min": 0.4, "max": 10, "step": 0.1},
|
||||
"num_pack_stacks": {"min": 1, "max": 10, "step": 1},
|
||||
}
|
||||
|
||||
_LITERAL_RE = re.compile(
|
||||
r"^\s*([a-z_][a-z0-9_]*)\s*=\s*"
|
||||
r"(?P<rhs>true|false|\"[^\"]*\"|[-+]?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?)\s*;"
|
||||
r"(?:\s*//\s*(?P<comment>.*))?\s*$"
|
||||
)
|
||||
|
||||
|
||||
def _group_for(name: str) -> str:
|
||||
for grp, names in _GROUP_RULES:
|
||||
if name in names:
|
||||
return grp
|
||||
return "advanced"
|
||||
|
||||
|
||||
def _pretty_label(name: str) -> str:
|
||||
return name.replace("_", " ").capitalize()
|
||||
|
||||
|
||||
def _scan_scad(path: Path) -> list[Param]:
|
||||
out: list[Param] = []
|
||||
seen: set[str] = set()
|
||||
if not path.is_file():
|
||||
return out
|
||||
for raw in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||
# Stop at the explicit end-of-config marker so we don't pick up
|
||||
# internal helpers (hextra, spacing, etc.).
|
||||
if "END OF CONFIGURATION" in raw or "NON-Configurable" in raw:
|
||||
break
|
||||
m = _LITERAL_RE.match(raw)
|
||||
if not m:
|
||||
continue
|
||||
name = m.group(1)
|
||||
if name in seen:
|
||||
continue
|
||||
rhs = m.group("rhs")
|
||||
help_text = (m.group("comment") or "").strip() or None
|
||||
|
||||
if rhs in ("true", "false"):
|
||||
kind, default = "bool", (rhs == "true")
|
||||
elif rhs.startswith('"'):
|
||||
literal = rhs[1:-1]
|
||||
if name in _SELECT_OPTIONS:
|
||||
kind, default = "select", literal
|
||||
else:
|
||||
# Plain free-text strings aren't worth a form field.
|
||||
continue
|
||||
else:
|
||||
try:
|
||||
num = float(rhs)
|
||||
default = int(num) if num.is_integer() else num
|
||||
kind = "number"
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
p = Param(
|
||||
name=name, label=_pretty_label(name), kind=kind,
|
||||
default=default, group=_group_for(name), help=help_text,
|
||||
)
|
||||
if kind == "select":
|
||||
p.options = _SELECT_OPTIONS[name]
|
||||
if kind == "number" and name in _NUMBER_HINTS:
|
||||
h = _NUMBER_HINTS[name]
|
||||
p.min, p.max, p.step = h.get("min"), h.get("max"), h.get("step")
|
||||
out.append(p)
|
||||
seen.add(name)
|
||||
return out
|
||||
|
||||
|
||||
PARAMS: list[Param] = _scan_scad(SCAD_FILE)
|
||||
|
||||
|
||||
def default_params() -> dict[str, Any]:
|
||||
return {p.name: p.default for p in PARAMS}
|
||||
|
||||
Reference in New Issue
Block a user