Files
willtools-demo/WillTools.extension/tests/test_pa_sketch_extents.py
T
will 25592fa6c2
ci / test (push) Successful in 6m14s
WillTools 2026-09-09 -- clean snapshot of the development repo for review
History-free snapshot of the private development repository at 77a82eb.
Code, docs and the public catalogue only: no plant or job knowledge
layers, no client, job or producer names (tools/scrub.py clean against
the widened term list; a second word-level grep clean). The intro deck is
left out because its slide images cannot be checked for job drawings.
2026-09-09 08:35:15 -05:00

475 lines
20 KiB
Python

# -*- coding: utf-8 -*-
"""Host tests for `vibe.pa_sketch_extents` -- the two drawing functions
(`outline()` / `section()`) the Concrete Extents page's new `drawing` blocks
name via `module: 'pa_sketch_extents'` (see `pa_extents.py`'s "WHY THE TWO
DRAWINGS ARE NOW STACKED" docstring note for the 2026-09-04 owner ruling this
exists to satisfy). Loaded the way `test_panelsketch.py` is -- `conftest.py`
registers a bare `vibe` package pointed at `lib/vibe` so this imports on
plain host CPython with no Revit and no `pyrevit` on the path.
What this suite checks, per the task brief: `outline()` never returns an
empty/blank canvas even when geometry is missing; the cut-line tick + label
move with `values['_cut_frac']`; every primitive from both functions stays
inside the canvas box; `section()` removes an opening's gap only when the
cut station falls inside that opening's along-length range; a Reveal cuts a
partial-depth notch instead; a Solid Zone removes nothing; a Sandwich
section draws its wythe/insulation layers; the printed net area matches the
gross area minus what was actually drawn as removed; and neither function
raises or mutates `values`.
"""
import copy
from vibe import panelsketch
from vibe import pa_sketch_extents as S
# --------------------------------------------------------------- utilities
def _values(**over):
base = {'length_ft': '24', 'width_ft': '8', 'thickness_in': '8',
'mark': 'PNL101', 'design_number': 'PNL1'}
base.update(over)
return base
def _rows(primitives, kind, role=None):
return [p for p in primitives
if p['kind'] == kind and (role is None or p.get('role') == role)]
def _within_box(primitives, width_px, height_px, tol=1e-6):
for p in primitives:
if p['kind'] == 'rect':
assert -tol <= p['x'] <= width_px + tol, p
assert -tol <= p['y'] <= height_px + tol, p
assert -tol <= p['x'] + p['w'] <= width_px + tol, p
assert -tol <= p['y'] + p['h'] <= height_px + tol, p
elif p['kind'] == 'line':
for kx, ky in (('x1', 'y1'), ('x2', 'y2')):
assert -tol <= p[kx] <= width_px + tol, p
assert -tol <= p[ky] <= height_px + tol, p
else:
if 'x' in p:
assert -tol <= p['x'] <= width_px + tol, p
if 'y' in p:
assert -tol <= p['y'] <= height_px + tol, p
def _tick_x(primitives):
ticks = _rows(primitives, 'line', 'cutline-tick')
assert len(ticks) == 2
xs = set()
for t in ticks:
assert abs(t['x1'] - t['x2']) < 1e-9 # a tick is vertical
xs.add(round(t['x1'], 6))
assert len(xs) == 1, 'top and bottom ticks should share one x'
return ticks[0]['x1']
_OPENING_ROW = {'type': 'Opening', 'pos_along': 10, 'pos_along_from': 'start',
'length': 4, 'pos_vert': 2, 'height': 3, 'label': 'DOOR'}
# =========================================================== outline() ===
def test_outline_missing_geometry_is_a_note_not_a_blank_canvas():
primitives, problems = S.outline(_values(length_ft=''), 400, 300)
assert primitives, 'the canvas must never come back empty'
assert any(p['kind'] == 'text' for p in primitives)
assert problems
def test_outline_draws_the_panel_and_readable_overall_labels():
primitives, _problems = S.outline(
_values(length_ft='40', width_ft='8'), 800, 300)
rects = _rows(primitives, 'rect', 'panel')
assert len(rects) == 1
texts = _rows(primitives, 'text', 'dimension')
l_text = [t for t in texts if t['text'].startswith('L = ')]
w_text = [t for t in texts if t['text'].startswith('W = ')]
assert l_text and l_text[0]['font_size'] >= 11
assert w_text and w_text[0]['font_size'] >= 11
def test_outline_draws_cutline_tick_and_station_label():
primitives, _problems = S.outline(
_values(length_ft='40', width_ft='8', _cut_frac='0.25'), 800, 300)
labels = _rows(primitives, 'text', 'cutline-label')
assert len(labels) == 1
assert labels[0]['text'] == 'cut @ 10.0 ft'
assert labels[0]['font_size'] >= 11
def test_outline_cut_tick_moves_with_cut_frac():
values_lo = _values(length_ft='40', width_ft='8', _cut_frac='0.2')
values_hi = _values(length_ft='40', width_ft='8', _cut_frac='0.8')
prims_lo, _ = S.outline(values_lo, 800, 300)
prims_hi, _ = S.outline(values_hi, 800, 300)
x_lo = _tick_x(prims_lo)
x_hi = _tick_x(prims_hi)
assert x_hi > x_lo
def test_outline_cut_frac_defaults_to_midpoint():
primitives, _problems = S.outline(
_values(length_ft='40', width_ft='8'), 800, 300)
labels = _rows(primitives, 'text', 'cutline-label')
assert labels[0]['text'] == 'cut @ 20.0 ft'
def test_outline_cut_frac_out_of_range_clamps():
lo_prims, _ = S.outline(
_values(length_ft='40', width_ft='8', _cut_frac='-3'), 800, 300)
hi_prims, _ = S.outline(
_values(length_ft='40', width_ft='8', _cut_frac='9'), 800, 300)
assert _rows(lo_prims, 'text', 'cutline-label')[0]['text'] == 'cut @ 0.0 ft'
assert _rows(hi_prims, 'text', 'cutline-label')[0]['text'] == 'cut @ 40.0 ft'
def test_outline_draws_openings_to_scale():
values = _values(length_ft='40', width_ft='10',
openings_rows=[dict(_OPENING_ROW)])
primitives, _problems = S.outline(values, 800, 300)
assert len(_rows(primitives, 'rect', 'opening')) == 1
def test_outline_stays_inside_box():
values = _values(length_ft='40', width_ft='10',
openings_rows=[dict(_OPENING_ROW)])
for w, h in ((800, 300), (60, 60), (20, 15)):
primitives, _problems = S.outline(values, w, h)
_within_box(primitives, w, h)
def test_outline_never_mutates_values():
values = _values(length_ft='40', width_ft='10', _cut_frac='0.3',
openings_rows=[dict(_OPENING_ROW)])
before = copy.deepcopy(values)
S.outline(values, 800, 300)
assert values == before
def test_outline_never_raises_on_garbage():
garbage = {'length_ft': object(), 'width_ft': [1, 2], '_cut_frac': {},
'openings_rows': 'not-a-list'}
primitives, problems = S.outline(garbage, 800, 300)
assert isinstance(primitives, list) and primitives
assert isinstance(problems, list)
def test_outline_degrades_when_panelsketch_unavailable(monkeypatch):
# `outline()` no longer needs `panelsketch` for the panel rectangle, the
# L/W labels or the cut marker (see the module docstring's "WHY
# `outline()` NO LONGER DELEGATES" note) -- only openings/reveals/solid
# zones and the title are lost. So the picture still draws; only the
# openings problem is what names `panelsketch` as missing.
monkeypatch.setattr(S, 'panelsketch', None)
primitives, problems = S.outline(
_values(length_ft='40', width_ft='8'), 800, 300)
assert _rows(primitives, 'rect', 'panel')
assert any('panelsketch' in p for p in problems)
def test_outline_degrades_when_openings_row_parsing_raises(monkeypatch):
# The panel rectangle and its labels must survive even if the
# `panelsketch` openings machinery this module reuses blows up --
# never take the whole outline down for a row-parsing bug.
def _boom(values, length_ft, width_ft, thickness_ft):
raise RuntimeError('boom')
monkeypatch.setattr(panelsketch, '_openings_rows', _boom)
values = _values(length_ft='40', width_ft='8',
openings_rows=[dict(_OPENING_ROW)])
primitives, problems = S.outline(values, 800, 300)
assert _rows(primitives, 'rect', 'panel')
assert any('boom' in p for p in problems)
def test_outline_panel_spans_at_least_85pct_of_canvas_width():
# The headless-render finding this rewrite exists to fix: the panel
# rectangle must fill (almost) the whole canvas width, never letterboxed
# down to roughly half of it. Uses the real target dimensions from the
# owner's render (37.58 x 8.375 ft x 8 in) at a realistic canvas size.
values = _values(length_ft='37.58', width_ft='8.375', thickness_in='8')
primitives, _problems = S.outline(values, 1100, 300)
panel = _rows(primitives, 'rect', 'panel')[0]
assert panel['w'] >= 0.85 * 1100
def test_outline_cut_and_l_labels_stay_off_the_cut_tick():
values = _values(length_ft='37.58', width_ft='8.375', thickness_in='8')
primitives, _problems = S.outline(values, 1100, 300)
tick_x = _tick_x(primitives)
cut_label = _rows(primitives, 'text', 'cutline-label')[0]
l_label = [t for t in _rows(primitives, 'text', 'dimension')
if t['text'].startswith('L = ')][0]
assert abs(cut_label['x'] - tick_x) > 12.0
assert abs(l_label['x'] - tick_x) > 12.0
def test_outline_opening_rect_scales_with_length():
row = dict(_OPENING_ROW) # pos_along=10, length=4, from 'start'
short = S.outline(_values(length_ft='20', width_ft='10',
openings_rows=[row]), 1000, 300)[0]
long_ = S.outline(_values(length_ft='40', width_ft='10',
openings_rows=[row]), 1000, 300)[0]
w_short = _rows(short, 'rect', 'opening')[0]['w']
w_long = _rows(long_, 'rect', 'opening')[0]['w']
# the same absolute 4 ft opening is a bigger FRACTION of a 20 ft panel
# than of a 40 ft one, so it must draw roughly half as wide in pixels.
assert w_long < w_short
assert abs(w_long - w_short / 2.0) < 1.0
def test_outline_geometry_matches_the_drawn_panel_rect():
values = _values(length_ft='37.58', width_ft='8.375', thickness_in='8')
for w, h in ((1100, 300), (800, 260), (600, 220)):
primitives, _problems = S.outline(values, w, h)
panel = _rows(primitives, 'rect', 'panel')[0]
geo = S.outline_geometry(values, w, h)
assert abs(panel['x'] - geo['x0']) < 1e-6
assert abs(panel['y'] - geo['y0']) < 1e-6
assert abs(panel['x'] + panel['w'] - geo['x1']) < 1e-6
assert abs(panel['y'] + panel['h'] - geo['y1']) < 1e-6
def test_outline_geometry_cut_tick_matches_the_geometry_helper():
values = _values(length_ft='37.58', width_ft='8.375', _cut_frac='0.3')
primitives, _problems = S.outline(values, 1100, 300)
tick_x = _tick_x(primitives)
geo = S.outline_geometry(values, 1100, 300)
expected = geo['x0'] + 0.3 * (geo['x1'] - geo['x0'])
assert abs(tick_x - expected) < 1e-6
def _all_ys(primitives):
ys = []
for p in primitives:
if p['kind'] == 'rect':
ys.append(p['y'])
ys.append(p['y'] + p['h'])
elif p['kind'] == 'line':
ys.append(p['y1'])
ys.append(p['y2'])
elif 'y' in p:
ys.append(p['y'])
return ys
def test_outline_every_primitive_has_4px_clearance_top_and_bottom():
# Live-render finding, 2026-09-04: the panel's bottom edge was pinned to
# the canvas bottom and the L label clipped off entirely -- both because
# nothing reserved room outside the panel rectangle. Checked at several
# realistic canvas sizes (the container's real height is
# `width_px * aspect`, `aspect` from `pa_extents` -- see
# `_OUTLINE_LABEL_ROWS_ASPECT`), not the degenerate tiny-canvas sizes
# `test_outline_stays_inside_box` exercises (where 4px clearance is not
# geometrically possible at all).
values = _values(length_ft='37.58', width_ft='8.375', thickness_in='8',
openings_rows=[dict(_OPENING_ROW)])
for width_px in (600, 900, 1100):
height_px = width_px * 0.4232 # width_ft/length_ft (~0.2228,
# clamped through [0.12, 0.5]) + 0.10
primitives, _problems = S.outline(values, width_px, height_px)
for y in _all_ys(primitives):
assert -1e-6 <= y <= height_px + 1e-6, (y, height_px)
assert y >= 4.0 - 1e-6, 'too close to the top: {0}'.format(y)
assert y <= height_px - 4.0 + 1e-6, \
'too close to the bottom: {0}'.format(y)
def test_outline_l_label_sits_below_the_panel_rect():
values = _values(length_ft='37.58', width_ft='8.375', thickness_in='8')
primitives, _problems = S.outline(values, 1100, 1100 * 0.4232)
panel = _rows(primitives, 'rect', 'panel')[0]
l_label = [t for t in _rows(primitives, 'text', 'dimension')
if t['text'].startswith('L = ')][0]
assert l_label['y'] > panel['y'] + panel['h']
def test_outline_geometry_never_raises_and_always_returns_a_box():
geo = S.outline_geometry(
{'length_ft': object(), 'width_ft': None}, 'nope', [])
for key in ('x0', 'x1', 'y0', 'y1'):
assert key in geo
assert geo['x1'] >= geo['x0']
assert geo['y1'] >= geo['y0']
# =========================================================== section() ===
def test_section_missing_width_or_thickness_is_a_note_not_a_blank_canvas():
primitives, problems = S.section(
_values(width_ft='', thickness_in=''), 400, 300)
assert primitives and primitives[0]['kind'] == 'text'
assert any('Width' in p for p in problems)
assert any('Thickness' in p for p in problems)
def test_section_draws_the_gross_rectangle_and_labels():
primitives, _problems = S.section(
_values(width_ft='8.375', thickness_in='8', length_ft='24'),
600, 260)
rects = _rows(primitives, 'rect', 'panel-section')
assert len(rects) == 1
t_text = [t['text'] for t in _rows(primitives, 'text', 'dimension')
if t['text'].startswith('t = ')]
w_text = [t['text'] for t in _rows(primitives, 'text', 'dimension')
if t['text'].startswith('W = ')]
assert t_text == ['t = 8.00 in']
assert w_text == ['W = 8.38 ft']
def test_section_net_area_equals_gross_when_no_openings():
primitives, _problems = S.section(
_values(width_ft='8', thickness_in='8', length_ft='24',
openings_rows=[]),
600, 260)
net = _rows(primitives, 'text', 'annotation')
gross_in2 = 8.0 * 12.0 * 8.0
assert len(net) == 1
assert '{0:.1f}'.format(gross_in2) in net[0]['text']
def test_section_opening_inside_along_range_removes_a_gap_and_area():
# station = 0.5 * 24 = 12 ft, inside the opening's [10, 14] along range.
values = _values(width_ft='10', thickness_in='8', length_ft='24',
_cut_frac='0.5',
openings_rows=[dict(_OPENING_ROW)])
primitives, _problems = S.section(values, 600, 260)
gaps = _rows(primitives, 'rect', 'opening')
assert len(gaps) == 1
net_text = _rows(primitives, 'text', 'annotation')[0]['text']
gross_in2 = 10.0 * 12.0 * 8.0
removed_in2 = 3.0 * 12.0 * 8.0 # pos_vert=2..height=3 -> [2,5] * full t
expected_net = gross_in2 - removed_in2
assert '{0:.1f}'.format(expected_net) in net_text
def test_section_opening_outside_along_range_draws_nothing():
# station = 0.05 * 24 = 1.2 ft, outside the opening's [10, 14] range.
values = _values(width_ft='10', thickness_in='8', length_ft='24',
_cut_frac='0.05',
openings_rows=[dict(_OPENING_ROW)])
primitives, _problems = S.section(values, 600, 260)
assert not _rows(primitives, 'rect', 'opening')
net_text = _rows(primitives, 'text', 'annotation')[0]['text']
gross_in2 = 10.0 * 12.0 * 8.0
assert '{0:.1f}'.format(gross_in2) in net_text
def test_section_reveal_cuts_a_partial_depth_notch():
# width=2/12 ft (2 in) -- a reveal deeper than the 8 in thickness would
# be nonsensical, and internal units are decimal feet throughout this
# codebase (`CLAUDE.md` sec.3), the same as `pos_along` / `length` above.
row = {'type': 'Reveal', 'pos_along': 10, 'pos_along_from': 'start',
'length': 4, 'pos_vert': 2, 'height': 3, 'pos_cross': 0,
'pos_cross_from': 'left', 'width': 2.0 / 12.0, 'label': 'REVEAL1'}
values = _values(width_ft='10', thickness_in='8', length_ft='24',
_cut_frac='0.5', openings_rows=[row])
primitives, _problems = S.section(values, 600, 260)
reveals = _rows(primitives, 'rect', 'reveal')
assert len(reveals) == 1
panel = _rows(primitives, 'rect', 'panel-section')[0]
# a reveal 2 in deep out of an 8 in thickness must be shorter than the
# full panel-section height, and still sit inside it.
assert reveals[0]['h'] < panel['h']
assert reveals[0]['y'] >= panel['y'] - 1e-6
net_text = _rows(primitives, 'text', 'annotation')[0]['text']
gross_in2 = 10.0 * 12.0 * 8.0
removed_in2 = 3.0 * 12.0 * 2.0 # vert extent (ft->in) * cross depth, in
expected_net = gross_in2 - removed_in2
assert '{0:.1f}'.format(expected_net) in net_text
def test_section_solid_zone_at_station_removes_nothing():
row = {'type': 'Solid Zone', 'pos_along': 10, 'pos_along_from': 'start',
'length': 4, 'pos_vert': 2, 'height': 3, 'label': 'SZ1'}
values = _values(width_ft='10', thickness_in='8', length_ft='24',
_cut_frac='0.5', openings_rows=[row])
primitives, problems = S.section(values, 600, 260)
assert not _rows(primitives, 'rect', 'opening')
assert not _rows(primitives, 'rect', 'reveal')
net_text = _rows(primitives, 'text', 'annotation')[0]['text']
gross_in2 = 10.0 * 12.0 * 8.0
assert '{0:.1f}'.format(gross_in2) in net_text
assert any('Solid Zone' in p for p in problems)
def test_section_length_missing_draws_no_openings_but_reports_why():
values = _values(width_ft='10', thickness_in='8', length_ft='',
openings_rows=[dict(_OPENING_ROW)])
primitives, problems = S.section(values, 600, 260)
assert not _rows(primitives, 'rect', 'opening')
assert any('Length' in p for p in problems)
station = _rows(primitives, 'text', 'cutline-label')
assert station and 'not entered' in station[0]['text']
def test_section_sandwich_draws_wythe_and_insulation_layers():
values = _values(width_ft='8', thickness_in='8', length_ft='24',
section_type='Sandwich', wythe_top_in='2',
insulation_in='2', wythe_bottom_in='4')
primitives, problems = S.section(values, 600, 260)
assert len(_rows(primitives, 'rect', 'wythe')) == 2
assert len(_rows(primitives, 'rect', 'insulation')) == 1
assert not any('do not add up' in p for p in problems)
def test_section_sandwich_missing_layers_falls_back_to_one_outline():
values = _values(width_ft='8', thickness_in='8', length_ft='24',
section_type='Sandwich')
primitives, problems = S.section(values, 600, 260)
assert not _rows(primitives, 'rect', 'wythe')
assert any('not all entered' in p for p in problems)
def test_section_openings_rows_as_plain_text_is_reported_not_guessed():
values = _values(width_ft='8', thickness_in='8', length_ft='24',
openings='2 openings near the middle')
primitives, problems = S.section(values, 600, 260)
assert not _rows(primitives, 'rect', 'opening')
assert any('plain text' in p for p in problems)
def test_section_stays_inside_box():
values = _values(width_ft='10', thickness_in='8', length_ft='24',
_cut_frac='0.5', openings_rows=[dict(_OPENING_ROW)])
for w, h in ((600, 260), (60, 60), (20, 15)):
primitives, _problems = S.section(values, w, h)
_within_box(primitives, w, h)
def test_section_never_mutates_values():
values = _values(width_ft='10', thickness_in='8', length_ft='24',
_cut_frac='0.5', openings_rows=[dict(_OPENING_ROW)])
before = copy.deepcopy(values)
S.section(values, 600, 260)
assert values == before
def test_section_never_raises_on_garbage():
garbage = {'width_ft': object(), 'thickness_in': [1], 'length_ft': {},
'_cut_frac': 'nope', 'openings_rows': 'not-a-list'}
primitives, problems = S.section(garbage, 600, 260)
assert isinstance(primitives, list) and primitives
assert isinstance(problems, list)
def test_section_degrades_when_panelsketch_unavailable(monkeypatch):
monkeypatch.setattr(S, 'panelsketch', None)
primitives, problems = S.section(_values(), 400, 300)
assert primitives and primitives[0]['kind'] == 'text'
assert problems
# --------------------------------------------------------- _cut_fraction
def test_cut_fraction_defaults_and_clamps():
assert S._cut_fraction({}) == 0.5
assert S._cut_fraction({'_cut_frac': None}) == 0.5
assert S._cut_fraction({'_cut_frac': 'garbage'}) == 0.5
assert S._cut_fraction({'_cut_frac': '-1'}) == 0.0
assert S._cut_fraction({'_cut_frac': '2'}) == 1.0
assert S._cut_fraction({'_cut_frac': '0.37'}) == 0.37
assert S._cut_fraction({'_cut_frac': 0.6}) == 0.6