Files
willtools-demo/WillTools.extension/tests/test_panelsketch.py
T
will 52d94303bc
ci / test (push) Has been cancelled
WillTools: precast shop-ticket QC and panel handling tools
Public demo snapshot of an in-house pyRevit extension for precast concrete
shop-ticket QC (37 checks / 46 checklist rows) and a standalone panel
handling solver (PCI Chapter 8).

- No client, job, plant or product data: knowledge layers ship as
  structural templates only.
- Ticket QC's model-read escalation targets a LAN model server endpoint
  (configurable), not a cloud provider.
- Built from the private development repo via tools/make_dist.py.
2026-09-03 16:33:47 -05:00

815 lines
32 KiB
Python

# -*- coding: utf-8 -*-
"""Host tests for `vibe.panelsketch` -- the panel elevation drawing geometry
behind the Panel Analysis form's picture. The WPF `Canvas` painting is the
button's job and out of scope here (see the module docstring); this pins the
primitive list `sketch()` produces: scale-true letterboxing, support
placement, the missing-input degrade, and the openings non-drawing rule.
Loaded the way the sibling `test_panelanalysisform.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.
"""
from __future__ import annotations
import copy
from vibe import panelsketch as S
def _values(**over):
base = {
'length_ft': '24',
'width_ft': '8',
'thickness_in': '8',
'supports_ft': '2, 22',
'openings': '',
'mark': 'PNL101',
'design_number': 'PNL1',
}
base.update(over)
return base
def _rect(primitives):
rects = [p for p in primitives if p['kind'] == 'rect' and p['role'] == 'panel']
assert len(rects) == 1
return rects[0]
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
# ---------------------------------------------------------------- aspect
def test_tall_panel_preserves_aspect_ratio():
primitives, problems = S.sketch(
_values(length_ft='10', width_ft='30', supports_ft='2, 8'), 400, 400)
assert problems == []
rect = _rect(primitives)
assert abs((rect['w'] / rect['h']) - (10.0 / 30.0)) < 1e-6
def test_square_panel_preserves_aspect_ratio():
primitives, problems = S.sketch(
_values(length_ft='20', width_ft='20', supports_ft='2, 18'), 500, 300)
assert problems == []
rect = _rect(primitives)
assert abs((rect['w'] / rect['h']) - 1.0) < 1e-6
def test_wide_panel_preserves_aspect_ratio():
primitives, problems = S.sketch(
_values(length_ft='40', width_ft='8'), 300, 500)
assert problems == []
rect = _rect(primitives)
assert abs((rect['w'] / rect['h']) - (40.0 / 8.0)) < 1e-6
def test_letterboxed_panel_does_not_fill_the_box_when_aspect_differs():
# a very wide panel in a tall box must not stretch to fill the height
primitives, _problems = S.sketch(
_values(length_ft='40', width_ft='8'), 200, 800)
rect = _rect(primitives)
assert rect['h'] < 800
# ------------------------------------------------------------- inside box
def test_everything_stays_inside_a_normal_box():
primitives, _problems = S.sketch(_values(), 640, 480)
_within_box(primitives, 640, 480)
def test_everything_stays_inside_a_small_box():
primitives, _problems = S.sketch(_values(), 60, 60)
_within_box(primitives, 60, 60)
def test_everything_stays_inside_a_very_small_box():
primitives, _problems = S.sketch(_values(), 20, 15)
_within_box(primitives, 20, 15)
def test_out_of_range_support_still_stays_inside_the_box():
primitives, _problems = S.sketch(
_values(supports_ft='2, 999'), 400, 300)
_within_box(primitives, 400, 300)
# --------------------------------------------------------- missing input
def test_missing_length_returns_no_primitives_and_names_the_field():
primitives, problems = S.sketch(_values(length_ft=''), 400, 300)
assert primitives == []
assert any('Length' in p for p in problems)
def test_missing_width_returns_no_primitives_and_names_the_field():
primitives, problems = S.sketch(_values(width_ft=''), 400, 300)
assert primitives == []
assert any('Width' in p for p in problems)
def test_zero_length_is_treated_as_missing():
primitives, problems = S.sketch(_values(length_ft='0'), 400, 300)
assert primitives == []
assert any('Length' in p for p in problems)
def test_negative_width_is_treated_as_missing():
primitives, problems = S.sketch(_values(width_ft='-4'), 400, 300)
assert primitives == []
assert any('Width' in p for p in problems)
def test_non_numeric_length_is_treated_as_missing():
primitives, problems = S.sketch(_values(length_ft='tall'), 400, 300)
assert primitives == []
assert any('Length' in p for p in problems)
def test_both_missing_reports_both_fields_not_just_one():
_primitives, problems = S.sketch(_values(length_ft='', width_ft=''),
400, 300)
assert any('Length' in p for p in problems)
assert any('Width' in p for p in problems)
def test_missing_thickness_does_not_block_the_drawing_but_is_noted():
primitives, problems = S.sketch(_values(thickness_in=''), 400, 300)
assert primitives != []
assert any('Thickness' in p for p in problems)
notes = [p for p in primitives if p['role'] == 'annotation']
assert any('not entered' in n['text'] for n in notes)
def test_missing_thickness_never_fakes_a_value():
primitives, _problems = S.sketch(_values(thickness_in=''), 400, 300)
notes = [p for p in primitives if p['role'] == 'annotation']
for n in notes:
assert 'in' not in n['text'].split('not entered')[0] or \
'not entered' in n['text']
# --------------------------------------------------------------- supports
def test_supports_land_at_the_true_proportional_position():
primitives, problems = S.sketch(
_values(length_ft='40', supports_ft='10, 30'), 500, 300)
assert problems == []
rect = _rect(primitives)
markers = [p for p in primitives if p['kind'] == 'support']
assert len(markers) == 2
xs = sorted(m['x'] for m in markers)
# 10/40 and 30/40 of the way along the drawn width, from the rect origin
expect_1 = rect['x'] + (10.0 / 40.0) * rect['w']
expect_2 = rect['x'] + (30.0 / 40.0) * rect['w']
assert abs(xs[0] - expect_1) < 1e-6
assert abs(xs[1] - expect_2) < 1e-6
for m in markers:
assert m['flagged'] is False
def test_support_at_each_end_is_not_flagged():
primitives, problems = S.sketch(
_values(length_ft='24', supports_ft='0, 24'), 400, 300)
assert not any('outside the panel length' in p for p in problems)
markers = [p for p in primitives if p['kind'] == 'support']
assert all(m['flagged'] is False for m in markers)
def test_out_of_range_support_is_flagged_and_still_drawn():
primitives, problems = S.sketch(
_values(length_ft='24', supports_ft='2, 40'), 400, 300)
assert any('outside the panel length' in p for p in problems)
markers = [p for p in primitives if p['kind'] == 'support']
assert len(markers) == 2
flagged = [m for m in markers if m['flagged']]
assert len(flagged) == 1
# clamped to the panel end, not dropped
rect = _rect(primitives)
assert abs(flagged[0]['x'] - (rect['x'] + rect['w'])) < 1e-6
def test_negative_support_is_flagged_and_clamped_to_the_start():
primitives, problems = S.sketch(
_values(length_ft='24', supports_ft='-5, 12'), 400, 300)
assert any('outside the panel length' in p for p in problems)
rect = _rect(primitives)
markers = [p for p in primitives if p['kind'] == 'support']
flagged = [m for m in markers if m['flagged']]
assert len(flagged) == 1
assert abs(flagged[0]['x'] - rect['x']) < 1e-6
def test_no_supports_entered_is_reported_but_still_draws_the_panel():
primitives, problems = S.sketch(_values(supports_ft=''), 400, 300)
assert primitives != []
assert any('No support positions' in p for p in problems)
assert not any(p['kind'] == 'support' for p in primitives)
def test_pre_parsed_supports_list_is_honoured_over_raw_text():
values = _values(supports_ft='999') # would be out of range if read
values['supports'] = [4.0, 20.0]
primitives, problems = S.sketch(values, 400, 300)
assert not any('999' in p for p in problems)
markers = [p for p in primitives if p['kind'] == 'support']
assert len(markers) == 2
assert all(m['flagged'] is False for m in markers)
# --------------------------------------------------------------- openings
def test_blank_openings_produces_no_note():
primitives, problems = S.sketch(_values(openings=''), 400, 300)
assert not any(p['role'] == 'openings-note' for p in primitives)
assert not any('opening' in p.lower() for p in problems)
def test_unparsed_openings_prose_produces_a_note_not_a_rectangle():
primitives, problems = S.sketch(
_values(openings='one door near the south end'), 400, 300)
notes = [p for p in primitives if p['role'] == 'openings-note']
assert len(notes) == 1
assert 'not drawn' in notes[0]['text']
# never a rectangle standing in for the opening
rects = [p for p in primitives if p['kind'] == 'rect']
assert len(rects) == 1 # only the panel itself
assert any('not drawn' in p for p in problems)
def test_openings_with_a_leading_count_is_still_only_a_note():
primitives, _problems = S.sketch(
_values(openings='2 openings, door and window'), 400, 300)
notes = [p for p in primitives if p['role'] == 'openings-note']
assert len(notes) == 1
assert '2 opening' in notes[0]['text']
rects = [p for p in primitives if p['kind'] == 'rect']
assert len(rects) == 1
# ------------------------------------------------------------- dimensions
def test_length_and_width_dimension_text_carries_value_and_units():
primitives, _problems = S.sketch(
_values(length_ft='24', width_ft='8'), 400, 300)
dim_texts = [p['text'] for p in primitives
if p['kind'] == 'text' and p['role'] == 'dimension']
assert any('24.00 ft' in t for t in dim_texts)
assert any('8.00 ft' in t for t in dim_texts)
def test_title_carries_mark_when_given():
primitives, _problems = S.sketch(_values(mark='PNL101'), 400, 300)
titles = [p for p in primitives if p['role'] == 'title']
assert len(titles) == 1
assert 'PNL101' in titles[0]['text']
def test_no_title_when_mark_and_design_number_are_blank():
primitives, _problems = S.sketch(
_values(mark='', design_number=''), 400, 300)
assert not any(p['role'] == 'title' for p in primitives)
# ------------------------------------------------------------ no mutation
def test_sketch_never_mutates_the_values_dict():
values = _values(supports_ft='2, 22')
before = copy.deepcopy(values)
S.sketch(values, 400, 300)
assert values == before
def test_sketch_never_mutates_a_values_dict_that_fails_the_gate():
values = _values(length_ft='')
before = copy.deepcopy(values)
S.sketch(values, 400, 300)
assert values == before
# ===========================================================================
# elevation() / plan() / section() / weight_diagram() -- the four new entry
# points. `sketch()` above is untouched and re-tested above; these pin the
# additive contract: scale/aspect fidelity, containment, the missing-input
# degrade, out-of-range flag-but-draw, and non-mutation, for each function.
# ===========================================================================
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)]
# --------------------------------------------------------------- elevation
def test_elevation_missing_length_or_width_returns_no_primitives():
primitives, problems = S.elevation(_values(length_ft=''), 400, 300)
assert primitives == []
assert any('Length' in p for p in problems)
primitives, problems = S.elevation(_values(width_ft=''), 400, 300)
assert primitives == []
assert any('Width' in p for p in problems)
def test_elevation_front_panel_is_scale_true():
primitives, problems = S.elevation(
_values(length_ft='10', width_ft='30', supports_ft='2, 8'), 500, 400)
assert not any('Length' in p or 'Width' in p for p in problems)
rects = _rows(primitives, 'rect', 'panel')
assert len(rects) == 1
rect = rects[0]
assert abs((rect['w'] / rect['h']) - (10.0 / 30.0)) < 1e-6
def test_elevation_side_view_matches_front_width_scale():
primitives, _problems = S.elevation(
_values(length_ft='24', width_ft='8', thickness_in='12'), 640, 480)
front = _rows(primitives, 'rect', 'panel')[0]
side = _rows(primitives, 'rect', 'panel-side')
assert len(side) == 1
side = side[0]
# same px/ft scale on the shared width_ft axis -> equal drawn heights
assert abs(side['h'] - front['h']) < 1e-6
# thickness (1 ft) drawn to the same scale as the front's width axis
scale = front['h'] / 8.0
assert abs(side['w'] - 1.0 * scale) < 1e-6
def test_elevation_missing_thickness_degrades_side_view_without_blocking_front():
primitives, problems = S.elevation(_values(thickness_in=''), 400, 300)
assert _rows(primitives, 'rect', 'panel')
assert not _rows(primitives, 'rect', 'panel-side')
assert not _rows(primitives, 'text', 'section-property')
assert not _rows(primitives, 'rect', 'panel-crosssection')
assert any('Thickness' in p and 'waiting on it' in p for p in problems)
def test_elevation_openings_rows_are_drawn_to_scale_and_placed_correctly():
values = _values(length_ft='40', width_ft='10',
openings_rows=[{'type': 'Opening', 'pos_along': 10,
'pos_along_from': 'start', 'length': 4,
'pos_vert': 2, 'height': 3,
'label': 'DOOR'}])
primitives, problems = S.elevation(values, 640, 480)
assert not any('DOOR' in p and 'outside' in p for p in problems)
openings = _rows(primitives, 'rect', 'opening')
assert len(openings) == 1
op = openings[0]
front = _rows(primitives, 'rect', 'panel')[0]
expect_x = front['x'] + (10.0 / 40.0) * front['w']
expect_w = (4.0 / 40.0) * front['w']
assert abs(op['x'] - expect_x) < 1e-6
assert abs(op['w'] - expect_w) < 1e-6
assert op['flagged'] is False
labels = _rows(primitives, 'text', 'opening-label')
assert any('DOOR' in l['text'] for l in labels)
def test_elevation_opening_position_from_end_is_resolved_correctly():
# pos_along measured from the far end -> x0 = length - pos_along - length
values = _values(length_ft='24', width_ft='8',
openings_rows=[{'type': 'Opening', 'pos_along': 2,
'pos_along_from': 'end', 'length': 4,
'pos_vert': 1, 'height': 3,
'label': 'WIN'}])
primitives, problems = S.elevation(values, 640, 480)
assert not any('outside' in p for p in problems)
op = _rows(primitives, 'rect', 'opening')[0]
front = _rows(primitives, 'rect', 'panel')[0]
# x0 = 24 - 2 - 4 = 18
expect_x = front['x'] + (18.0 / 24.0) * front['w']
assert abs(op['x'] - expect_x) < 1e-6
def test_elevation_out_of_range_opening_is_flagged_and_still_drawn():
values = _values(length_ft='24', width_ft='8',
openings_rows=[{'type': 'Opening', 'pos_along': 20,
'pos_along_from': 'start', 'length': 10,
'pos_vert': 0, 'height': 4,
'label': ''}])
primitives, problems = S.elevation(values, 400, 300)
assert any('outside the panel' in p for p in problems)
openings = _rows(primitives, 'rect', 'opening')
assert len(openings) == 1
assert openings[0]['flagged'] is True
def test_elevation_opening_entirely_outside_is_dropped_not_moved():
values = _values(length_ft='24', width_ft='8',
openings_rows=[{'type': 'Opening', 'pos_along': 100,
'pos_along_from': 'start', 'length': 2,
'pos_vert': 100, 'height': 2,
'label': ''}])
primitives, problems = S.elevation(values, 400, 300)
assert any('entirely outside' in p for p in problems)
assert not _rows(primitives, 'rect', 'opening')
def test_elevation_opening_missing_along_position_is_never_drawn():
values = _values(length_ft='24', width_ft='8',
openings_rows=[{'type': 'Opening', 'length': 4,
'pos_vert': 0, 'height': 4,
'label': 'X'}])
primitives, problems = S.elevation(values, 400, 300)
assert not _rows(primitives, 'rect', 'opening')
assert any('along-length' in p for p in problems)
def test_elevation_opening_missing_vertical_position_is_reported_not_guessed():
values = _values(length_ft='24', width_ft='8',
openings_rows=[{'type': 'Opening', 'pos_along': 2,
'pos_along_from': 'start', 'length': 4,
'label': 'X'}])
primitives, problems = S.elevation(values, 400, 300)
assert not _rows(primitives, 'rect', 'opening')
assert any('no vertical position' in p for p in problems)
def test_elevation_reveal_is_drawn_with_a_distinct_role():
values = _values(length_ft='24', width_ft='8',
openings_rows=[{'type': 'Reveal', 'pos_along': 2,
'pos_along_from': 'start', 'length': 4,
'pos_vert': 0, 'height': 8,
'label': ''}])
primitives, _problems = S.elevation(values, 400, 300)
assert _rows(primitives, 'rect', 'reveal')
assert not _rows(primitives, 'rect', 'opening')
def test_elevation_falls_back_to_openings_prose_note_when_no_rows():
values = _values(openings='one door near the south end')
primitives, problems = S.elevation(values, 400, 300)
notes = _rows(primitives, 'text', 'openings-note')
assert len(notes) == 1
assert 'not drawn' in notes[0]['text']
assert any('not drawn' in p for p in problems)
def test_elevation_stays_inside_box_various_sizes():
values = _values(
openings_rows=[{'type': 'Opening', 'pos_along': 2, 'pos_along_from': 'start',
'length': 3, 'pos_vert': 1, 'height': 3, 'label': 'X'}],
supports_ft='2, 999')
for w, h in ((640, 480), (60, 60), (20, 15)):
primitives, _problems = S.elevation(values, w, h)
_within_box(primitives, w, h)
def test_elevation_never_mutates_values():
values = _values(openings_rows=[{'type': 'Opening', 'pos_along': 2, 'pos_along_from': 'start',
'length': 3, 'pos_vert': 1, 'height': 3,
'label': 'X'}])
before = copy.deepcopy(values)
S.elevation(values, 400, 300)
assert values == before
def test_sketch_alias_untouched_by_elevation_addition():
# sketch() keeps its original contract -- no side view, no scale-true
# openings, prose-only openings note.
values = _values(openings='2 openings, door and window')
primitives, _problems = S.sketch(values, 400, 300)
assert not _rows(primitives, 'rect', 'panel-side')
assert _rows(primitives, 'text', 'openings-note')
# -------------------------------------------------------------------- plan
def test_plan_missing_length_or_width_returns_no_primitives():
primitives, problems = S.plan(_values(length_ft='', width_ft=''),
400, 300)
assert primitives == []
assert any('Length' in p for p in problems)
assert any('Width' in p for p in problems)
def test_plan_outline_is_scale_true():
primitives, _problems = S.plan(
_values(length_ft='40', width_ft='8'), 300, 500)
rect = _rows(primitives, 'rect', 'panel-plan')[0]
assert abs((rect['w'] / rect['h']) - (40.0 / 8.0)) < 1e-6
def test_plan_centroid_at_geometric_center_with_no_openings():
primitives, _problems = S.plan(
_values(length_ft='20', width_ft='10'), 400, 300)
rect = _rows(primitives, 'rect', 'panel-plan')[0]
cg = _rows(primitives, 'marker', 'centroid')[0]
assert abs(cg['x'] - (rect['x'] + rect['w'] / 2.0)) < 1e-6
assert abs(cg['y'] - (rect['y'] + rect['h'] / 2.0)) < 1e-6
def test_plan_centroid_shifts_away_from_a_large_opening():
values = _values(length_ft='20', width_ft='10',
openings_rows=[{'type': 'Opening', 'pos_along': 0,
'pos_along_from': 'start', 'length': 8,
'pos_vert': 0, 'height': 10,
'label': ''}])
primitives, _problems = S.plan(values, 400, 300)
rect = _rows(primitives, 'rect', 'panel-plan')[0]
cg = _rows(primitives, 'marker', 'centroid')[0]
# material removed from the left end -> centroid moves right of center
assert cg['x'] > rect['x'] + rect['w'] / 2.0
def test_plan_weight_missing_is_reported_and_shows_not_entered():
primitives, problems = S.plan(_values(weight_lb=''), 400, 300)
texts = [p['text'] for p in _rows(primitives, 'text', 'annotation')]
assert any('not entered' in t for t in texts)
assert any('Weight' in p for p in problems)
def test_plan_weight_present_is_printed():
primitives, _problems = S.plan(_values(weight_lb='12000'), 400, 300)
texts = [p['text'] for p in _rows(primitives, 'text', 'annotation')]
assert any('12.0 k' in t for t in texts)
def test_plan_centroid_block_has_all_three_axes():
primitives, _problems = S.plan(_values(weight_lb='12000'), 400, 300)
texts = [p['text'] for p in _rows(primitives, 'text', 'annotation')]
assert any(t.startswith('X:') for t in texts)
assert any(t.startswith('Y:') for t in texts)
assert any(t.startswith('Z:') for t in texts)
def test_plan_legend_always_present_even_with_no_lifter_data():
primitives, _problems = S.plan(_values(), 400, 300)
swatches = _rows(primitives, 'swatch', 'legend-fill')
assert len(swatches) == len(S.STAGE_FILLS)
fills = set(s['fill'] for s in swatches)
assert fills == set(S.STAGE_FILLS)
glyphs = [p['text'] for p in _rows(primitives, 'text', 'legend-glyph')]
assert any('Face Lifter' in g for g in glyphs)
assert any('Dunnage' in g for g in glyphs)
def test_plan_lifter_text_produces_a_note_never_a_marker_with_a_guessed_position():
values = _values(face_lifters='2 face lifters', edge_lifters='1 edge')
primitives, problems = S.plan(values, 400, 300)
notes = _rows(primitives, 'text', 'lifters-note')
assert len(notes) == 1
assert 'not entered' in notes[0]['text'] or 'not drawn' in notes[0]['text']
# no marker kind was invented at a fake position
markers = [p for p in primitives if p['kind'] == 'marker'
and p['role'] != 'centroid']
assert markers == []
assert any('Lifter positions are not entered' in p for p in problems)
def test_plan_no_lifter_text_produces_no_note():
primitives, _problems = S.plan(
_values(face_lifters='', edge_lifters=''), 400, 300)
assert not _rows(primitives, 'text', 'lifters-note')
def test_plan_stages_list_is_printed_as_text_when_present():
values = _values(stages=['Stripping', 'Erection'])
primitives, _problems = S.plan(values, 400, 300)
notes = _rows(primitives, 'text', 'note')
assert any('Stripping' in n['text'] and 'Erection' in n['text']
for n in notes)
def test_plan_stages_absent_degrades_quietly():
primitives, _problems = S.plan(_values(), 400, 300)
notes = _rows(primitives, 'text', 'note')
assert not any('Stages:' in n['text'] for n in notes)
def test_plan_stays_inside_box():
values = _values(
openings_rows=[{'type': 'Opening', 'pos_along': 2, 'pos_along_from': 'start',
'length': 3, 'pos_vert': 1, 'height': 3, 'label': 'X'}],
face_lifters='2', stages=['Stripping'])
for w, h in ((640, 480), (60, 60), (20, 15)):
primitives, _problems = S.plan(values, w, h)
_within_box(primitives, w, h)
def test_plan_never_mutates_values():
values = _values(openings_rows=[{'type': 'Opening', 'pos_along': 2, 'pos_along_from': 'start',
'length': 3, 'pos_vert': 1, 'height': 3,
'label': 'X'}],
face_lifters='2', stages=['Stripping'])
before = copy.deepcopy(values)
S.plan(values, 400, 300)
assert values == before
# ----------------------------------------------------------------- section
def _section_values(**over):
base = {
'length_ft': '24', 'width_ft': '8', 'thickness_in': '8',
'strand_rows': [{'count': 4, 'depth_in': 2, 'pull_pct': 100},
{'count': 2, 'depth_in': 6, 'pull_pct': None}],
'mark': 'PNL101', 'design_number': 'PNL1',
}
base.update(over)
return base
def test_section_missing_width_or_thickness_returns_no_primitives():
primitives, problems = S.section(
_section_values(width_ft='', thickness_in=''), 400, 300)
assert primitives == []
assert any('Width' in p for p in problems)
assert any('Thickness' in p for p in problems)
def test_section_outline_is_scale_true():
primitives, _problems = S.section(
_section_values(width_ft='8', thickness_in='12'), 500, 400)
rect = _rows(primitives, 'rect', 'panel-section')[0]
assert abs((rect['w'] / rect['h']) - (8.0 / 1.0)) < 1e-6
def test_section_strand_depth_is_true_to_scale_from_the_bottom():
primitives, problems = S.section(
_section_values(strand_rows=[{'count': 4, 'depth_in': 4}]),
500, 400)
assert not any('outside' in p for p in problems)
rect = _rows(primitives, 'rect', 'panel-section')[0]
dots = _rows(primitives, 'dot')
assert len(dots) == 4
# depth_in=4 of thickness_in=8 -> half way up from the bottom
expect_y = rect['y'] + rect['h'] - 0.5 * rect['h']
for d in dots:
assert abs(d['y'] - expect_y) < 1e-6
def test_section_strands_are_centred_not_spread_across_the_width():
primitives, _problems = S.section(
_section_values(strand_rows=[{'count': 4, 'depth_in': 2}]),
500, 400)
rect = _rows(primitives, 'rect', 'panel-section')[0]
dots = _rows(primitives, 'dot')
xs = [d['x'] for d in dots]
center = rect['x'] + rect['w'] / 2.0
assert all(abs(x - center) < rect['w'] * 0.2 for x in xs)
def test_section_out_of_range_strand_is_flagged_and_clamped():
primitives, problems = S.section(
_section_values(strand_rows=[{'count': 2, 'depth_in': 99}]),
400, 300)
assert any('outside the panel thickness' in p for p in problems)
dots = _rows(primitives, 'dot')
assert all(d['flagged'] is True for d in dots)
rect = _rows(primitives, 'rect', 'panel-section')[0]
for d in dots:
assert abs(d['y'] - rect['y']) < 1e-6 # clamped to top (max depth)
def test_section_no_strand_rows_still_draws_outline():
primitives, problems = S.section(
_section_values(strand_rows=[]), 400, 300)
assert _rows(primitives, 'rect', 'panel-section')
assert any('No strand data' in p for p in problems)
def test_section_missing_length_skips_full_length_view_only():
primitives, problems = S.section(
_section_values(length_ft=''), 400, 300)
assert _rows(primitives, 'rect', 'panel-section')
assert not _rows(primitives, 'rect', 'panel-elevation-thin')
assert any('full-length' in p for p in problems)
def test_section_full_length_view_reports_debond_not_entered():
primitives, problems = S.section(_section_values(), 640, 480)
assert _rows(primitives, 'rect', 'panel-elevation-thin')
assert any('Debond' in p for p in problems)
lines = _rows(primitives, 'line', 'strand-line')
assert len(lines) == 2
def test_section_horizontal_spacing_not_entered_is_reported():
_primitives, problems = S.section(_section_values(), 400, 300)
assert any('horizontal spacing was not entered' in p for p in problems)
def test_section_stays_inside_box():
for w, h in ((640, 480), (60, 60), (20, 15)):
primitives, _problems = S.section(_section_values(), w, h)
_within_box(primitives, w, h)
def test_section_never_mutates_values():
values = _section_values()
before = copy.deepcopy(values)
S.section(values, 400, 300)
assert values == before
# ------------------------------------------------------------ weight_diagram
def test_weight_diagram_missing_length_returns_no_primitives():
primitives, problems = S.weight_diagram(_values(length_ft=''), 400, 300)
assert primitives == []
assert any('Length' in p for p in problems)
def test_weight_diagram_beam_spans_the_scaled_length():
primitives, _problems = S.weight_diagram(_values(length_ft='24'), 640, 480)
beams = _rows(primitives, 'line', 'beam')
assert len(beams) == 1
def test_weight_diagram_supports_at_true_proportional_position():
primitives, problems = S.weight_diagram(
_values(length_ft='40', supports_ft='10, 30'), 500, 300)
assert not any('outside' in p for p in problems)
beam = _rows(primitives, 'line', 'beam')[0]
markers = _rows(primitives, 'support')
assert len(markers) == 2
xs = sorted(m['x'] for m in markers)
expect_1 = beam['x1'] + (10.0 / 40.0) * (beam['x2'] - beam['x1'])
expect_2 = beam['x1'] + (30.0 / 40.0) * (beam['x2'] - beam['x1'])
assert abs(xs[0] - expect_1) < 1e-6
assert abs(xs[1] - expect_2) < 1e-6
def test_weight_diagram_out_of_range_support_is_flagged_and_still_drawn():
primitives, problems = S.weight_diagram(
_values(length_ft='24', supports_ft='2, 40'), 400, 300)
assert any('outside the panel length' in p for p in problems)
markers = _rows(primitives, 'support')
flagged = [m for m in markers if m['flagged']]
assert len(flagged) == 1
def test_weight_diagram_cg_defaults_to_mid_length_with_no_openings():
primitives, _problems = S.weight_diagram(
_values(length_ft='40', width_ft='10'), 500, 300)
beam = _rows(primitives, 'line', 'beam')[0]
cg = _rows(primitives, 'marker', 'cg')[0]
expect_x = beam['x1'] + 0.5 * (beam['x2'] - beam['x1'])
assert abs(cg['x'] - expect_x) < 1e-6
def test_weight_diagram_cg_shifts_with_a_large_opening():
values = _values(length_ft='40', width_ft='10',
openings_rows=[{'type': 'Opening', 'pos_along': 0,
'pos_along_from': 'start', 'length': 16,
'pos_vert': 0, 'height': 10,
'label': ''}])
primitives, _problems = S.weight_diagram(values, 500, 300)
beam = _rows(primitives, 'line', 'beam')[0]
cg = _rows(primitives, 'marker', 'cg')[0]
mid = beam['x1'] + 0.5 * (beam['x2'] - beam['x1'])
assert cg['x'] > mid
def test_weight_diagram_weight_missing_is_reported():
primitives, problems = S.weight_diagram(_values(weight_lb=''), 400, 300)
texts = [p['text'] for p in _rows(primitives, 'text', 'annotation')]
assert any('not entered' in t for t in texts)
assert any('Weight' in p for p in problems)
def test_weight_diagram_moment_arms_are_dimensioned_per_support():
primitives, _problems = S.weight_diagram(
_values(length_ft='40', supports_ft='10, 30'), 500, 300)
arms = _rows(primitives, 'line', 'moment-arm')
labels = _rows(primitives, 'text', 'moment-arm-label')
assert len(arms) == 2
assert len(labels) == 2
def test_weight_diagram_stays_inside_box():
values = _values(
openings_rows=[{'type': 'Opening', 'pos_along': 2, 'pos_along_from': 'start',
'length': 3, 'pos_vert': 1, 'height': 3, 'label': 'X'}],
supports_ft='2, 999')
for w, h in ((640, 480), (60, 60), (20, 15)):
primitives, _problems = S.weight_diagram(values, w, h)
_within_box(primitives, w, h)
def test_weight_diagram_never_mutates_values():
values = _values(openings_rows=[{'type': 'Opening', 'pos_along': 2, 'pos_along_from': 'start',
'length': 3, 'pos_vert': 1, 'height': 3,
'label': 'X'}])
before = copy.deepcopy(values)
S.weight_diagram(values, 400, 300)
assert values == before