# -*- 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