d8cb0dc06d
Web tool for designing nickel/copper busbars over cylindrical-cell battery packs (21700, 18650) in hex holders. Flask + build123d backend exports STEP/DXF/SVG; vanilla JS frontend with live preview, multi-project SQLite persistence, snapshot history. Deploy scripts in deploy/ (proxmox-lxc.sh, install.sh, update.sh).
168 lines
5.1 KiB
Python
168 lines
5.1 KiB
Python
"""Sanity tests for busbar_export — exercise STEP/DXF/SVG writers end-to-end."""
|
||
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||
|
||
from busbar_export import to_step, to_dxf, to_svg
|
||
|
||
|
||
def _payload():
|
||
return {
|
||
"extrude": False,
|
||
"busbars": [
|
||
{
|
||
"name": "P1",
|
||
"color": "#ff8800",
|
||
"strip_width": 8.0,
|
||
"pad_radius": 7.0,
|
||
"hole_radius": 5.0,
|
||
"cells": [
|
||
{"id": 1, "x": 0.0, "y": 0.0},
|
||
{"id": 2, "x": 22.8, "y": 0.0},
|
||
{"id": 3, "x": 45.6, "y": 0.0},
|
||
],
|
||
}
|
||
],
|
||
}
|
||
|
||
|
||
def test_step_export_is_iso_10303():
|
||
data = to_step(_payload())
|
||
text = data[:200].decode("ascii", errors="ignore")
|
||
assert "ISO-10303-21" in text, f"STEP header missing, got: {text!r}"
|
||
assert len(data) > 500
|
||
|
||
|
||
def test_dxf_export_is_nonempty():
|
||
data = to_dxf(_payload())
|
||
assert b"SECTION" in data[:2000]
|
||
|
||
|
||
def test_svg_export_is_svg():
|
||
data = to_svg(_payload())
|
||
assert b"<svg" in data[:2000]
|
||
|
||
|
||
def test_extruded_step_includes_solid():
|
||
payload = _payload()
|
||
payload["extrude"] = True
|
||
payload["thickness"] = 0.2
|
||
data = to_step(payload)
|
||
assert b"MANIFOLD_SOLID_BREP" in data or b"CLOSED_SHELL" in data
|
||
|
||
|
||
def test_panel_5p_row_is_stadium():
|
||
"""5 cells in a row → collinear hull → stadium-shaped panel."""
|
||
payload = {
|
||
"busbars": [{
|
||
"name": "5P",
|
||
"shape": "panel",
|
||
"pad_radius": 10.0,
|
||
"hole_radius": 5.0,
|
||
"cells": [{"id": i + 1, "x": i * 22.8, "y": 0.0} for i in range(5)],
|
||
}]
|
||
}
|
||
data = to_step(payload)
|
||
assert b"ISO-10303-21" in data[:200]
|
||
assert len(data) > 500
|
||
|
||
|
||
def test_panel_grid_uses_convex_hull():
|
||
"""4-cell square → 4-vertex hull → rounded rectangle."""
|
||
payload = {
|
||
"busbars": [{
|
||
"name": "4P",
|
||
"shape": "panel",
|
||
"pad_radius": 10.0,
|
||
"hole_radius": 5.0,
|
||
"cells": [
|
||
{"id": 1, "x": 0.0, "y": 0.0},
|
||
{"id": 2, "x": 22.8, "y": 0.0},
|
||
{"id": 3, "x": 22.8, "y": 19.746},
|
||
{"id": 4, "x": 0.0, "y": 19.746},
|
||
],
|
||
}]
|
||
}
|
||
data = to_step(payload)
|
||
assert b"ISO-10303-21" in data[:200]
|
||
|
||
|
||
def test_panel_single_cell_is_disc_with_hole():
|
||
payload = {
|
||
"busbars": [{
|
||
"name": "1P",
|
||
"shape": "panel",
|
||
"pad_radius": 10.0,
|
||
"hole_radius": 5.0,
|
||
"cells": [{"id": 1, "x": 0.0, "y": 0.0}],
|
||
}]
|
||
}
|
||
data = to_step(payload)
|
||
assert b"ISO-10303-21" in data[:200]
|
||
|
||
|
||
def test_wire_shape_still_works():
|
||
payload = {
|
||
"busbars": [{
|
||
"name": "wire",
|
||
"shape": "wire",
|
||
"strip_width": 6.0,
|
||
"pad_radius": 7.0,
|
||
"hole_radius": 5.0,
|
||
"cells": [{"x": 0, "y": 0}, {"x": 22.8, "y": 0}],
|
||
}]
|
||
}
|
||
data = to_step(payload)
|
||
assert b"ISO-10303-21" in data[:200]
|
||
|
||
|
||
def test_panel_L_shape_has_no_diagonal_bridge():
|
||
"""L-shape (7 cells in column + 2 cells across top) must not bridge across
|
||
non-selected cells. The convex-hull approach would have, but neighbor-edge
|
||
must not — verify by checking that the area of the resulting face is close
|
||
to the sum of stadium-chain segments, not the area of the L's bounding box.
|
||
"""
|
||
from busbar_export import parse_payload, busbar_sketch
|
||
cells = [{"x": 0, "y": i * 19.75} for i in range(7)]
|
||
cells += [{"x": 22.8, "y": 6 * 19.75}, {"x": 45.6, "y": 6 * 19.75}]
|
||
payload = {"busbars": [{
|
||
"name": "L", "shape": "panel",
|
||
"pad_radius": 10.0, "hole_radius": 6.0,
|
||
"hole_shape": "cross", "slit_width": 1.8,
|
||
"cells": cells,
|
||
}]}
|
||
busbars, *_ = parse_payload(payload)
|
||
face = busbar_sketch(busbars[0])
|
||
area = face.area
|
||
|
||
# Pad disc area = π·10² ≈ 314 per cell, 9 cells → 2826 max if all disjoint.
|
||
# Bridges add some, holes subtract some. The L's bounding box is
|
||
# 65.6 × 138.25 ≈ 9070 — convex hull would be at least ~4500. Neighbor
|
||
# chain should be well under 3500.
|
||
assert 2000 < area < 3500, f"area {area} suggests convex-hull bridging"
|
||
|
||
|
||
def test_cross_slit_punches_through():
|
||
"""Cross slit must remove area from the panel (vs. no holes baseline)."""
|
||
from busbar_export import parse_payload, busbar_sketch
|
||
base = {
|
||
"busbars": [{
|
||
"name": "P", "shape": "panel", "pad_radius": 10.0,
|
||
"hole_radius": 6.0, "hole_shape": "cross", "slit_width": 1.8,
|
||
"cells": [{"x": 0, "y": 0}, {"x": 22.8, "y": 0}],
|
||
}]
|
||
}
|
||
with_cross, *_ = parse_payload(base)
|
||
area_cross = busbar_sketch(with_cross[0]).area
|
||
|
||
# Same payload, tiny hole — should be nearly the same area as un-punched.
|
||
base["busbars"][0]["hole_radius"] = 0.1
|
||
base["busbars"][0]["slit_width"] = 0.1
|
||
tiny, *_ = parse_payload(base)
|
||
area_tiny = busbar_sketch(tiny[0]).area
|
||
|
||
assert area_tiny > area_cross + 50, \
|
||
f"cross didn't punch much: tiny={area_tiny:.1f}, cross={area_cross:.1f}"
|