8e6ee31596
ci / test (push) Successful in 6m18s
History-free snapshot of the private development repository at cbe45c5 (2026-09-09). Code, docs and public catalogue only; no plant or job knowledge layers, no client or job names (tools/scrub.py clean). The intro deck is left out of this snapshot because its slide images could not be checked for job drawings.
1121 lines
45 KiB
Python
1121 lines
45 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
|
|
|
|
|
|
def _sandwich_values(**over):
|
|
values = {'width_ft': '15.8', 'thickness_in': '11', 'length_ft': '13.2',
|
|
'section_type': 'Sandwich', 'wythe_top_in': '3',
|
|
'insulation_in': '4', 'wythe_bottom_in': '4'}
|
|
values.update(over)
|
|
return values
|
|
|
|
|
|
def test_section_sandwich_draws_three_layers_to_scale():
|
|
prims, problems = S.section(_sandwich_values(), 600, 240)
|
|
outline = [p for p in prims if p.get('role') == 'panel-section'][0]
|
|
layers = [p for p in prims if p.get('role') in ('wythe', 'insulation')]
|
|
assert [p['role'] for p in layers] == ['wythe', 'insulation', 'wythe']
|
|
px_per_in = outline['h'] / 11.0
|
|
assert abs(layers[0]['h'] - 4 * px_per_in) < 1e-6 # bottom wythe
|
|
assert abs(layers[1]['h'] - 4 * px_per_in) < 1e-6 # insulation
|
|
assert abs(layers[2]['h'] - 3 * px_per_in) < 1e-6 # top wythe
|
|
# Stacked from the rail up, edge to edge.
|
|
assert abs(layers[0]['y'] + layers[0]['h'] - (outline['y'] + outline['h'])) < 1e-6
|
|
assert abs(layers[1]['y'] + layers[1]['h'] - layers[0]['y']) < 1e-6
|
|
assert abs(layers[2]['y'] - outline['y']) < 1e-6
|
|
labels = [p['text'] for p in prims if p.get('role') == 'layer-label']
|
|
assert labels == ['wythe 4.00 in', 'insulation 4.00 in', 'wythe 3.00 in']
|
|
assert not any('Sandwich section' in p for p in problems)
|
|
|
|
|
|
def test_section_sandwich_that_does_not_add_up_stays_one_outline():
|
|
prims, problems = S.section(_sandwich_values(insulation_in='2'), 600, 240)
|
|
assert not [p for p in prims if p.get('role') in ('wythe', 'insulation')]
|
|
assert any('do not add up' in p for p in problems)
|
|
|
|
|
|
def test_section_sandwich_missing_a_layer_stays_one_outline():
|
|
prims, problems = S.section(_sandwich_values(wythe_top_in=''), 600, 240)
|
|
assert not [p for p in prims if p.get('role') in ('wythe', 'insulation')]
|
|
assert any('not all entered' in p for p in problems)
|
|
|
|
|
|
def test_section_solid_draws_no_layers():
|
|
values = _sandwich_values(section_type='Solid')
|
|
prims, problems = S.section(values, 600, 240)
|
|
assert not [p for p in prims if p.get('role') in ('wythe', 'insulation')]
|
|
assert not any('Sandwich section' in p for p in problems)
|
|
|
|
|
|
# ===========================================================================
|
|
# text-vs-outline overlap (owner, 2026-09-03: "text still overlaps the
|
|
# drawing"). No real font metrics exist in a host test, so this uses the same
|
|
# conservative estimate the button's own `_sketch_text` reasons from (a fixed
|
|
# per-character width and line height, anchor 'middle' centring the box,
|
|
# 'start' left-aligning it, `orientation: 'vertical'` swapping width/height
|
|
# the way the button's `RotateTransform(-90)` does) -- generous enough that a
|
|
# real render is never narrower than this box, so "no overlap here" is a
|
|
# genuine guarantee, not a false pass.
|
|
# ===========================================================================
|
|
|
|
_CHAR_W = 7.0
|
|
_LINE_H = 13.0
|
|
|
|
|
|
def _text_box(prim):
|
|
text = prim.get('text', '') or ''
|
|
w = max(len(text) * _CHAR_W, 1.0)
|
|
h = _LINE_H
|
|
if prim.get('orientation') == 'vertical':
|
|
w, h = h, w
|
|
x = prim.get('x', 0.0)
|
|
y = prim.get('y', 0.0)
|
|
if prim.get('anchor') == 'middle':
|
|
x -= w / 2.0
|
|
return x, y, x + w, y + h
|
|
|
|
|
|
def _boxes_overlap(a, b):
|
|
ax0, ay0, ax1, ay1 = a
|
|
bx0, by0, bx1, by1 = b
|
|
return ax0 < bx1 and bx0 < ax1 and ay0 < by1 and by0 < ay1
|
|
|
|
|
|
def _assert_no_text_overlaps_any_outline(primitives):
|
|
outlines = [p for p in primitives if p['kind'] == 'rect'
|
|
and str(p.get('role', '')).startswith('panel')]
|
|
assert outlines, 'expected at least one outline rect'
|
|
outline_boxes = [(o['x'], o['y'], o['x'] + o['w'], o['y'] + o['h'])
|
|
for o in outlines]
|
|
for p in primitives:
|
|
if p['kind'] != 'text':
|
|
continue
|
|
tbox = _text_box(p)
|
|
for obox in outline_boxes:
|
|
assert not _boxes_overlap(tbox, obox), \
|
|
'text {0!r} overlaps outline {1!r}'.format(p, obox)
|
|
|
|
|
|
def test_section_solid_no_text_overlaps_the_outline():
|
|
primitives, _problems = S.section(_section_values(), 900, 260)
|
|
_assert_no_text_overlaps_any_outline(primitives)
|
|
|
|
|
|
def test_section_sandwich_no_text_overlaps_the_outline():
|
|
values = _sandwich_values(
|
|
strand_rows=[{'count': 4, 'depth_in': 2, 'pull_pct': 100},
|
|
{'count': 2, 'depth_in': 9, 'pull_pct': None}])
|
|
primitives, _problems = S.section(values, 900, 400)
|
|
_assert_no_text_overlaps_any_outline(primitives)
|
|
|
|
|
|
def test_elevation_solid_no_text_overlaps_the_outline():
|
|
primitives, _problems = S.elevation(_values(), 640, 480)
|
|
_assert_no_text_overlaps_any_outline(primitives)
|
|
|
|
|
|
def test_elevation_sandwich_no_text_overlaps_the_outline():
|
|
# elevation() does not read section_type/wythe fields, but must not
|
|
# overlap its own outline regardless of what else is in `values`.
|
|
values = _values(section_type='Sandwich', wythe_top_in='3',
|
|
insulation_in='4', wythe_bottom_in='4')
|
|
primitives, _problems = S.elevation(values, 640, 480)
|
|
_assert_no_text_overlaps_any_outline(primitives)
|
|
|
|
|
|
# ===========================================================================
|
|
# drawing_display_scale() -- the fixed, non-adaptive shrink factor the
|
|
# button applies as a `ScaleTransform` instead of a `Viewbox` (see the
|
|
# function's own docstring for why a `Viewbox` cancels out against the
|
|
# page's Ctrl+wheel zoom). Pure arithmetic, so this is the one part of the
|
|
# zoom fix that is unit-testable outside WPF.
|
|
# ===========================================================================
|
|
|
|
def test_drawing_display_scale_shrinks_an_oversized_drawing():
|
|
assert S.drawing_display_scale(900, 260, max_w=360.0, max_h=520.0) < 1.0
|
|
|
|
|
|
def test_drawing_display_scale_never_grows_a_small_drawing():
|
|
assert S.drawing_display_scale(100, 80, max_w=360.0, max_h=520.0) == 1.0
|
|
|
|
|
|
def test_drawing_display_scale_is_uniform_and_aspect_true():
|
|
scale = S.drawing_display_scale(900, 260, max_w=360.0, max_h=1000.0)
|
|
# the width axis is the binding constraint here (900 -> 360 needs more
|
|
# shrink than 260 -> 1000 would) -- both axes get the SAME factor
|
|
assert abs(scale - (360.0 / 900.0)) < 1e-9
|
|
|
|
|
|
def test_drawing_display_scale_binds_on_whichever_axis_needs_more_shrink():
|
|
# here the HEIGHT is the binding constraint
|
|
scale = S.drawing_display_scale(260, 900, max_w=1000.0, max_h=360.0)
|
|
assert abs(scale - (360.0 / 900.0)) < 1e-9
|
|
|
|
|
|
def test_drawing_display_scale_handles_bad_input_without_raising():
|
|
assert S.drawing_display_scale(0, 0) == 1.0
|
|
assert S.drawing_display_scale(None, None) == 1.0
|
|
assert S.drawing_display_scale(-5, 100) == 1.0
|
|
|
|
|
|
# ===========================================================================
|
|
# extents_outline() / extents_section() -- the Concrete Extents drawings:
|
|
# the panel's own concrete edge, its openings located by a dimension chain,
|
|
# and (for a Sandwich section) the wythe layers -- with NO strands, NO
|
|
# supports, and NO section-properties dump (owner, 2026-09-03: "just draw
|
|
# the member's concrete edge").
|
|
# ===========================================================================
|
|
|
|
def _extents_values(**over):
|
|
base = {'length_ft': '24', 'width_ft': '8', 'thickness_in': '8',
|
|
'mark': 'PNL101', 'design_number': 'PNL1'}
|
|
base.update(over)
|
|
return base
|
|
|
|
|
|
def test_extents_outline_missing_length_or_width_returns_no_primitives():
|
|
primitives, problems = S.extents_outline(
|
|
_extents_values(length_ft=''), 400, 300)
|
|
assert primitives == []
|
|
assert any('Length' in p for p in problems)
|
|
|
|
|
|
def test_extents_outline_is_scale_true_with_two_overall_dimensions():
|
|
primitives, _problems = S.extents_outline(
|
|
_extents_values(length_ft='40', width_ft='8'), 640, 480)
|
|
rects = _rows(primitives, 'rect', 'panel')
|
|
assert len(rects) == 1
|
|
rect = rects[0]
|
|
assert abs((rect['w'] / rect['h']) - (40.0 / 8.0)) < 1e-6
|
|
dim_lines = _rows(primitives, 'line', 'dimension')
|
|
assert len(dim_lines) == 2 # length (bottom) + width (left)
|
|
dim_texts = [p['text'] for p in _rows(primitives, 'text', 'dimension')]
|
|
assert any('40.00 ft' in t for t in dim_texts)
|
|
assert any('8.00 ft' in t for t in dim_texts)
|
|
|
|
|
|
def test_extents_outline_draws_no_supports():
|
|
primitives, _problems = S.extents_outline(
|
|
_extents_values(supports_ft='2, 22'), 640, 480)
|
|
assert not any(p['kind'] == 'support' for p in primitives)
|
|
|
|
|
|
def test_extents_outline_draws_opening_rect_and_locating_dimension_chain():
|
|
values = _extents_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.extents_outline(values, 640, 480)
|
|
assert not any('outside' in p for p in problems)
|
|
openings = _rows(primitives, 'rect', 'opening')
|
|
assert len(openings) == 1
|
|
# the gap-dimension chain locating the opening along the length and
|
|
# across the width -- at least one more 'dimension' line beyond the two
|
|
# overall ones, from `_gap_dimension_chain`.
|
|
dim_lines = _rows(primitives, 'line', 'dimension')
|
|
assert len(dim_lines) > 2
|
|
|
|
|
|
def test_extents_outline_no_dimension_text_overlaps_the_outline():
|
|
# An opening's OWN label sits inside its opening rect, which is itself
|
|
# inside the panel -- that is correct (it is labelling the opening, not
|
|
# cluttering the outline), so this checks only the DIMENSION text (the
|
|
# two overall dimensions plus the opening-locating chain), which must
|
|
# stay outside the outline the way `_gap_dimension_chain` places it.
|
|
values = _extents_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.extents_outline(values, 640, 480)
|
|
dim_only = [p for p in primitives
|
|
if p['kind'] != 'text' or p.get('role') == 'dimension']
|
|
_assert_no_text_overlaps_any_outline(dim_only)
|
|
|
|
|
|
def test_extents_outline_stays_inside_box():
|
|
values = _extents_values(
|
|
openings_rows=[{'type': 'Opening', 'pos_along': 2,
|
|
'pos_along_from': 'start', 'length': 3,
|
|
'pos_vert': 1, 'height': 3, 'label': 'X'}])
|
|
for w, h in ((640, 480), (60, 60), (20, 15)):
|
|
primitives, _problems = S.extents_outline(values, w, h)
|
|
_within_box(primitives, w, h)
|
|
|
|
|
|
def test_extents_outline_never_mutates_values():
|
|
values = _extents_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.extents_outline(values, 400, 300)
|
|
assert values == before
|
|
|
|
|
|
def test_extents_section_missing_width_or_thickness_returns_no_primitives():
|
|
primitives, problems = S.extents_section(
|
|
_extents_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_extents_section_draws_no_strands_even_when_strand_rows_present():
|
|
# the exact bug that made Concrete Extents "look like garbage": strand
|
|
# data entered under a different tab of the same form must not appear
|
|
# here, because this drawing has no reinforcement inputs of its own.
|
|
values = _extents_values(
|
|
strand_rows=[{'count': 4, 'depth_in': 2, 'pull_pct': 100}])
|
|
primitives, _problems = S.extents_section(values, 600, 240)
|
|
assert not _rows(primitives, 'dot')
|
|
assert not _rows(primitives, 'text', 'strand-label')
|
|
|
|
|
|
def test_extents_section_sandwich_draws_layers_to_scale():
|
|
values = _extents_values(
|
|
width_ft='15.8', thickness_in='11', section_type='Sandwich',
|
|
wythe_top_in='3', insulation_in='4', wythe_bottom_in='4')
|
|
primitives, problems = S.extents_section(values, 600, 240)
|
|
outline = _rows(primitives, 'rect', 'panel-section')[0]
|
|
layers = [p for p in primitives if p.get('role') in ('wythe', 'insulation')]
|
|
assert [p['role'] for p in layers] == ['wythe', 'insulation', 'wythe']
|
|
px_per_in = outline['h'] / 11.0
|
|
assert abs(layers[0]['h'] - 4 * px_per_in) < 1e-6
|
|
assert not any('Sandwich section' in p for p in problems)
|
|
|
|
|
|
def test_extents_section_no_text_overlaps_the_outline():
|
|
values = _extents_values(
|
|
width_ft='15.8', thickness_in='11', section_type='Sandwich',
|
|
wythe_top_in='3', insulation_in='4', wythe_bottom_in='4',
|
|
strand_rows=[{'count': 4, 'depth_in': 2, 'pull_pct': 100}])
|
|
primitives, _problems = S.extents_section(values, 600, 240)
|
|
_assert_no_text_overlaps_any_outline(primitives)
|
|
|
|
|
|
def test_extents_section_stays_inside_box():
|
|
for w, h in ((640, 480), (60, 60), (20, 15)):
|
|
primitives, _problems = S.extents_section(_extents_values(), w, h)
|
|
_within_box(primitives, w, h)
|
|
|
|
|
|
def test_extents_section_never_mutates_values():
|
|
values = _extents_values()
|
|
before = copy.deepcopy(values)
|
|
S.extents_section(values, 400, 300)
|
|
assert values == before
|