52d94303bc
ci / test (push) Has been cancelled
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.
815 lines
32 KiB
Python
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
|