WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m14s

History-free snapshot of the private development repository at 77a82eb.
Code, docs and the public catalogue only: no plant or job knowledge
layers, no client, job or producer names (tools/scrub.py clean against
the widened term list; a second word-level grep clean). The intro deck is
left out because its slide images cannot be checked for job drawings.
This commit is contained in:
2026-09-09 08:35:15 -05:00
commit 25592fa6c2
406 changed files with 223435 additions and 0 deletions
@@ -0,0 +1,768 @@
# -*- coding: utf-8 -*-
"""Host tests for `vibe.pa_sketch_freebody` -- the Structural Model free-
body drawing behind the Panel Analysis form's restraint/support/load
picture (owner ruling, 2026-09-04: see the module's own docstring for the
full ruling text). The WPF `Canvas` painting is the button's job and out of
scope here, same as `test_panelsketch.py`; this pins the primitive list
`freebody()` produces: the member line always draws, restraints and loads
land at the right elevation, missing/out-of-range/reversed data is
reported rather than guessed, and everything stays inside the canvas.
Loaded the way `test_panelsketch.py` and `test_pa_structural.py` are --
`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 vibe import pa_sketch_freebody as F
# --------------------------------------------------------------- fixtures
def _rows(table, rows):
"""`values` fragment for one row-table: `rows` is a list of
`{col: value}` dicts, written under `'<table>.<n>.<col>'` with
`'<table>.<n>.present' = 'yes'`, exactly the contract `freebody()`
reads."""
values = {}
for i, row in enumerate(rows, 1):
n = str(i)
values['{0}.{1}.present'.format(table, n)] = 'yes'
for col, val in row.items():
values['{0}.{1}.{2}'.format(table, n, col)] = val
return values
def _merge(*dicts):
out = {}
for d in dicts:
out.update(d)
return out
def _texts(primitives, role=None):
out = []
for p in primitives:
if p['kind'] != 'text':
continue
if role is not None and p.get('role') != role:
continue
out.append(p['text'])
return out
def _within_box(primitives, width_px, height_px, tol=1e-6):
for p in primitives:
kind = p['kind']
if 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 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
elif kind == 'circle':
r = p.get('r', 0.0)
assert -tol <= p['cx'] - r <= width_px + r + tol, p
assert -tol <= p['cy'] - r <= height_px + r + tol, p
elif kind == 'polygon':
for px, py in p['points']:
assert -tol <= px <= width_px + tol, p
assert -tol <= py <= 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
_KNOWN_KINDS = set(['line', 'rect', 'text', 'circle', 'polygon'])
# ------------------------------------------------------------------ safety
def test_never_raises_on_empty_values():
primitives, problems = F.freebody({}, 400, 150)
assert isinstance(primitives, list)
assert isinstance(problems, list)
def test_never_raises_on_none_values():
primitives, problems = F.freebody(None, 400, 150)
assert isinstance(primitives, list)
def test_never_raises_on_garbage_values():
values = {
'restraint.1.present': 'yes',
'restraint.1.elevation_ft': object(),
'restraint.1.type': 12345,
'conc_load.x.present': 'yes',
'wind_zone.1.present': 'yes',
'wind_zone.1.from_elev_ft': 'not a number',
'bottom_elevation_ft': [],
'top_elevation_ft': {},
3: 'not even a string key',
}
primitives, problems = F.freebody(values, 400, 150)
assert isinstance(primitives, list) and primitives
def test_never_raises_on_zero_or_negative_canvas_size():
for w, h in ((0, 0), (-10, 50), (50, -10), (None, None)):
primitives, problems = F.freebody({}, w, h)
assert isinstance(primitives, list)
def test_values_dict_not_mutated():
values = _merge(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '20'},
_rows('restraint', [{'elevation_ft': '10', 'type': 'Pin'}]))
before = dict(values)
F.freebody(values, 400, 150)
assert values == before
def test_top_level_wrapper_catches_unexpected_exception(monkeypatch):
def boom(values, w, h):
raise ValueError('boom')
monkeypatch.setattr(F, '_freebody', boom)
primitives, problems = F.freebody({}, 300, 100)
assert len(primitives) == 1
assert primitives[0]['kind'] == 'text'
assert 'could not be produced' in primitives[0]['text'].lower()
assert problems and 'failed' in problems[0].lower()
# ----------------------------------------------------------- member line
def test_empty_values_still_draws_the_member_line():
primitives, problems = F.freebody({}, 400, 150)
members = [p for p in primitives if p.get('role') == 'member']
assert len(members) == 1
assert members[0]['kind'] == 'line'
assert problems # missing elevation range is reported
def test_member_line_spans_close_to_the_full_declared_width():
primitives, _problems = F.freebody(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '20'}, 500, 150)
member = [p for p in primitives if p.get('role') == 'member'][0]
assert member['x2'] - member['x1'] > 500 * 0.5
def test_only_known_primitive_kinds():
values = _merge(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '20'},
_rows('restraint', [{'elevation_ft': '0', 'type': 'Pin'},
{'elevation_ft': '20', 'type': 'Fixed'},
{'elevation_ft': '10', 'type': 'Free'}]),
_rows('conc_load', [{'label': 'Ledger', 'elevation_ft': '12',
'offset_in': '10', 'dead_k': '9.6',
'live_k': '6.0'}]),
_rows('wind_zone', [{'label': 'W1', 'from_elev_ft': '2',
'to_elev_ft': '18', 'width_ft': '8.4',
'pressure_psf': '16', 'suction_psf': '16'}]))
primitives, _problems = F.freebody(values, 600, 200)
for p in primitives:
assert p['kind'] in _KNOWN_KINDS, 'unknown primitive kind {0!r}'.format(p)
def test_all_primitives_within_canvas_bounds():
values = _merge(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '10'},
_rows('restraint', [
{'elevation_ft': '-5', 'type': 'Pin'}, # out of range
{'elevation_ft': '0', 'type': 'Fixed'},
{'elevation_ft': '5', 'type': 'Free'},
{'elevation_ft': '10', 'type': 'Floor tie'},
{'elevation_ft': '15', 'type': 'Weird'}]), # out of range
_rows('conc_load', [
{'label': 'A', 'elevation_ft': '3', 'offset_in': '10',
'dead_k': '1', 'live_k': '2'},
{'label': 'B', 'elevation_ft': '7', 'offset_in': '-6',
'dead_k': '3', 'live_k': '4'}]),
_rows('wind_zone', [
{'label': 'W1', 'from_elev_ft': '8', 'to_elev_ft': '1',
'width_ft': '6', 'pressure_psf': '20', 'suction_psf': '12'}]))
for w, h in ((600, 200), (200, 600), (60, 60)):
primitives, _problems = F.freebody(values, w, h)
_within_box(primitives, w, h)
# --------------------------------------------------------------- restraints
def test_restraint_label_count_matches_valid_rows():
values = _merge(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '20'},
_rows('restraint', [
{'elevation_ft': '0', 'type': 'Pin'},
{'elevation_ft': '10', 'type': 'Fixed'},
{'elevation_ft': '20', 'type': 'Floor tie'},
{'type': 'Pin'}, # no elevation -- skipped
]))
primitives, problems = F.freebody(values, 500, 150)
labels = _texts(primitives, role='restraint-label')
assert len(labels) == 3
assert any('elevation' in p.lower() for p in problems)
def test_restraint_elevation_increases_left_to_right():
values = _merge(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '20'},
_rows('restraint', [{'elevation_ft': '2', 'type': 'Pin'},
{'elevation_ft': '18', 'type': 'Pin'}]))
primitives, _problems = F.freebody(values, 500, 150)
polys = [p for p in primitives if p['kind'] == 'polygon'
and p.get('role') == 'restraint-pin']
assert len(polys) == 2
x0 = sum(pt[0] for pt in polys[0]['points']) / 3.0
x1 = sum(pt[0] for pt in polys[1]['points']) / 3.0
assert x0 < x1
def test_pin_and_floor_tie_draw_triangles():
values = _merge(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '20'},
_rows('restraint', [{'elevation_ft': '5', 'type': 'Pin'},
{'elevation_ft': '15', 'type': 'Floor tie'}]))
primitives, _problems = F.freebody(values, 500, 150)
roles = set(p.get('role') for p in primitives if p['kind'] == 'polygon')
assert 'restraint-pin' in roles
assert 'restraint-floortie' in roles
def test_fixed_draws_hatch_lines_not_a_triangle():
values = _merge(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '20'},
_rows('restraint', [{'elevation_ft': '10', 'type': 'Fixed'}]))
primitives, _problems = F.freebody(values, 500, 150)
fixed_lines = [p for p in primitives if p['kind'] == 'line'
and p.get('role') == 'restraint-fixed']
fixed_polys = [p for p in primitives if p['kind'] == 'polygon'
and p.get('role') == 'restraint-fixed']
assert len(fixed_lines) >= 2
assert not fixed_polys
def test_free_and_unrecognised_type_draw_a_circle_never_a_symbol():
values = _merge(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '20'},
_rows('restraint', [{'elevation_ft': '5', 'type': 'Free'},
{'elevation_ft': '15', 'type': ''}]))
primitives, _problems = F.freebody(values, 500, 150)
circles = [p for p in primitives if p['kind'] == 'circle']
roles = set(c.get('role') for c in circles)
assert 'restraint-free' in roles
assert 'restraint-unknown' in roles
assert not [p for p in primitives if p['kind'] == 'polygon'
and p.get('role', '').startswith('restraint')]
def test_restraint_outside_range_is_flagged_and_reported():
values = _merge(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '10'},
_rows('restraint', [{'elevation_ft': '25', 'type': 'Pin'}]))
primitives, problems = F.freebody(values, 500, 150)
polys = [p for p in primitives if p['kind'] == 'polygon']
assert polys and polys[0]['flagged'] is True
assert any('outside' in p.lower() for p in problems)
def test_restraint_dimension_chain_between_consecutive_restraints():
values = _merge(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '20'},
_rows('restraint', [{'elevation_ft': '4', 'type': 'Pin'},
{'elevation_ft': '12', 'type': 'Pin'}]))
primitives, _problems = F.freebody(values, 500, 150)
dims = _texts(primitives, role='dimension')
assert any('8.00 ft' in t for t in dims)
# --------------------------------------------------------- concentrated loads
def test_load_label_matches_the_owners_example_format():
values = _merge(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '20'},
_rows('conc_load', [{'label': 'Ledger', 'elevation_ft': '12',
'offset_in': '10', 'dead_k': '9.6',
'live_k': '6.0'}]))
primitives, _problems = F.freebody(values, 500, 150)
labels = _texts(primitives, role='load-label')
assert labels == ['Ledger: D 9.6 k / L 6.0 k, e +10 in']
def test_load_default_label_is_p_and_index():
values = _merge(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '20'},
_rows('conc_load', [
{'elevation_ft': '5', 'offset_in': '0', 'dead_k': '1',
'live_k': '1'},
{'elevation_ft': '15', 'offset_in': '0', 'dead_k': '2',
'live_k': '2'}]))
primitives, _problems = F.freebody(values, 500, 150)
labels = _texts(primitives, role='load-label')
assert any(t.startswith('P1:') for t in labels)
assert any(t.startswith('P2:') for t in labels)
def test_load_missing_elevation_is_skipped_and_reported():
values = _merge(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '20'},
_rows('conc_load', [{'label': 'NoElev', 'dead_k': '1'}]))
primitives, problems = F.freebody(values, 500, 150)
labels = _texts(primitives, role='load-label')
assert not any('NoElev' in t for t in labels)
assert any('elevation' in p.lower() for p in problems)
def test_load_missing_dead_or_live_shown_as_dash_never_zero():
values = _merge(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '20'},
_rows('conc_load', [{'label': 'X', 'elevation_ft': '10'}]))
primitives, _problems = F.freebody(values, 500, 150)
labels = _texts(primitives, role='load-label')
assert labels
text = labels[0]
assert u'—' in text
assert 'D 0.0' not in text and 'L 0.0' not in text
def test_load_missing_offset_defaults_outside_face_and_reports():
values = _merge(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '20'},
_rows('conc_load', [{'label': 'X', 'elevation_ft': '10'}]))
primitives, problems = F.freebody(values, 500, 150)
member = [p for p in primitives if p.get('role') == 'member'][0]
label = [p for p in primitives if p.get('role') == 'load-label'][0]
assert label['y'] < member['y1'] # above the line
assert any('offset' in p.lower() for p in problems)
def test_negative_offset_draws_below_the_line():
values = _merge(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '20'},
_rows('conc_load', [{'label': 'X', 'elevation_ft': '10',
'offset_in': '-4', 'dead_k': '1',
'live_k': '1'}]))
primitives, _problems = F.freebody(values, 500, 150)
member = [p for p in primitives if p.get('role') == 'member'][0]
label = [p for p in primitives if p.get('role') == 'load-label'][0]
assert label['y'] > member['y1'] # below the line
# ----------------------------------------------------------------- wind
def test_wind_zone_label_matches_the_owners_example_format():
values = _merge(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '20'},
_rows('wind_zone', [{'label': 'W1', 'from_elev_ft': '2',
'to_elev_ft': '18', 'width_ft': '8.4',
'pressure_psf': '16', 'suction_psf': '16'}]))
primitives, _problems = F.freebody(values, 600, 200)
labels = _texts(primitives, role='wind-label')
assert labels == ['W1: +16 / -16 psf, w 8.4 ft']
def test_wind_zone_missing_from_or_to_is_skipped_and_reported():
values = _merge(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '20'},
_rows('wind_zone', [{'label': 'Bad', 'from_elev_ft': '2'}]))
primitives, problems = F.freebody(values, 600, 200)
labels = _texts(primitives, role='wind-label')
assert not labels
assert any('elevation' in p.lower() for p in problems)
def test_wind_zone_reversed_from_to_is_swapped_and_flagged():
values = _merge(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '20'},
_rows('wind_zone', [{'label': 'W1', 'from_elev_ft': '18',
'to_elev_ft': '2', 'pressure_psf': '10',
'suction_psf': '10'}]))
primitives, problems = F.freebody(values, 600, 200)
bands = [p for p in primitives if p.get('role') == 'wind-zone-band']
assert bands and bands[0]['flagged'] is True
assert any('swapped' in p.lower() for p in problems)
def test_wind_zone_draws_a_band_of_arrows():
values = _merge(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '20'},
_rows('wind_zone', [{'label': 'W1', 'from_elev_ft': '2',
'to_elev_ft': '18', 'pressure_psf': '10',
'suction_psf': '10'}]))
primitives, _problems = F.freebody(values, 600, 200)
arrow_lines = [p for p in primitives if p['kind'] == 'line'
and p.get('role') == 'wind-arrow']
arrow_heads = [p for p in primitives if p['kind'] == 'polygon'
and p.get('role') == 'wind-arrowhead']
assert len(arrow_lines) >= 3
assert len(arrow_heads) == len(arrow_lines)
# --------------------------------------------------------------- no rows
def test_no_restraints_or_loads_note_when_nothing_entered():
primitives, _problems = F.freebody(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '10'}, 400, 150)
notes = _texts(primitives, role='note')
assert any('no restraints or loads' in t.lower() for t in notes)
def test_elevation_span_falls_back_to_row_extent_when_unentered():
values = _rows('restraint', [{'elevation_ft': '5', 'type': 'Pin'},
{'elevation_ft': '25', 'type': 'Pin'}])
primitives, problems = F.freebody(values, 500, 150)
ends = _texts(primitives, role='elevation-end')
assert any('5.00' in t for t in ends)
assert any('25.00' in t for t in ends)
assert any('bottom_elevation_ft' in p for p in problems)
def test_elevation_span_falls_back_to_placeholder_with_no_rows_either():
primitives, problems = F.freebody({}, 400, 150)
ends = _texts(primitives, role='elevation-end')
assert any('0.00' in t for t in ends)
assert any('1.00' in t for t in ends)
assert any('placeholder' in p.lower() for p in problems)
# --------------------------------------------------------- row_ids/row_key
def test_row_key_shape():
assert F.row_key('wind_zone', '1', 'pressure_psf') == \
'wind_zone.1.pressure_psf'
def test_row_ids_reads_present_yes_and_sorts_numerically():
values = {
'conc_load.2.present': 'yes', 'conc_load.10.present': 'yes',
'conc_load.1.present': 'yes', 'conc_load.5.present': 'no',
}
assert F.row_ids('conc_load', values) == [1, 2, 10]
# ------------------------------------------------------------- destacking
def test_destack_pushes_close_labels_apart():
items = [{'x': 10.0, 'y': 100.0}, {'x': 15.0, 'y': 100.0},
{'x': 200.0, 'y': 100.0}]
F._destack(items, direction=1)
assert items[0]['y'] == 100.0
assert items[1]['y'] != 100.0
assert items[2]['y'] == 100.0
def test_destack_leaves_far_apart_labels_alone():
items = [{'x': 0.0, 'y': 50.0}, {'x': 100.0, 'y': 50.0},
{'x': 200.0, 'y': 50.0}]
F._destack(items, direction=1)
assert all(item['y'] == 50.0 for item in items)
# ------------------------------------------------------------ layout: margin
def test_margin_is_never_below_70px_at_realistic_canvas_sizes():
for w, h in ((400, 88), (600, 132), (900, 198)):
assert F._margin(w, h) >= 70.0
def test_end_labels_stay_inside_the_canvas_with_room_for_the_text():
"""The owner's headless render, 2026-09-04: 'Bottom: -2.00 ft' clipped
down to 'ottom:' because the label was centred on the margin edge with
too little margin to hold it. The real scenario from that render: a
37.58 ft wall, bottom/top -2.00 / 35.67 ft."""
values = _merge(
{'bottom_elevation_ft': '-2.00', 'top_elevation_ft': '35.67'},
_rows('restraint', [
{'elevation_ft': '4.21', 'type': 'Pin'},
{'elevation_ft': '13.21', 'type': 'Fixed'},
{'elevation_ft': '16.88', 'type': 'Floor tie'},
{'elevation_ft': '23.88', 'type': 'Floor tie'},
{'elevation_ft': '27.54', 'type': 'Floor tie'},
{'elevation_ft': '34.54', 'type': 'Pin'}]))
for w, h in ((600, 132), (900, 198)):
primitives, _problems = F.freebody(values, w, h)
ends = dict((p['text'], p) for p in primitives
if p.get('role') == 'elevation-end')
bottom_label = [t for t in ends if t.startswith('Bottom:')][0]
top_label = [t for t in ends if t.startswith('Top:')][0]
margin = F._margin(float(w), float(h))
# a rough per-character text-width budget matching the WPF
# renderer's own 'middle'-anchor estimate (`_sketch_text`, 2.6 px
# per character each side) -- the label's half-width must fit
# inside the margin reserved for it.
half_w = len(bottom_label) * 2.6
assert half_w <= margin, \
'label {0!r} half-width {1} exceeds the {2} px margin' \
.format(bottom_label, half_w, margin)
half_w = len(top_label) * 2.6
assert half_w <= margin
# --------------------------------------------------- layout: member + ticks
def test_member_line_is_a_wide_dark_line_with_end_ticks():
values = _merge({'bottom_elevation_ft': '0', 'top_elevation_ft': '20'})
primitives, _problems = F.freebody(values, 600, 150)
member = [p for p in primitives if p.get('role') == 'member'][0]
assert member.get('width', 0) >= 3.0
ticks = [p for p in primitives if p.get('role') == 'member-end-tick']
assert len(ticks) == 2
tick_xs = set([t['x1'] for t in ticks])
assert tick_xs == set([member['x1'], member['x2']])
def test_restraint_symbols_are_at_least_14px_tall():
values = _merge(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '20'},
_rows('restraint', [{'elevation_ft': '5', 'type': 'Pin'},
{'elevation_ft': '10', 'type': 'Fixed'},
{'elevation_ft': '15', 'type': 'Free'}]))
primitives, _problems = F.freebody(values, 600, 150)
pin = [p for p in primitives if p['kind'] == 'polygon'
and p.get('role') == 'restraint-pin'][0]
ys = [pt[1] for pt in pin['points']]
assert max(ys) - min(ys) >= 14.0
fixed_lines = [p for p in primitives if p['kind'] == 'line'
and p.get('role') == 'restraint-fixed']
drop = max(abs(l['y2'] - l['y1']) for l in fixed_lines)
assert drop >= 14.0
free_dot = [p for p in primitives if p['kind'] == 'circle'
and p.get('role') == 'restraint-free'][0]
assert free_dot['r'] * 2.0 >= 14.0
def test_restraint_symbols_are_16px_tall():
"""Coordinator, live-render follow-up: pin/floor-tie triangles, the
fixed hatch and the free/unrecognised dot are all 16 px tall, not just
>= 14."""
values = _merge(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '20'},
_rows('restraint', [{'elevation_ft': '5', 'type': 'Pin'},
{'elevation_ft': '10', 'type': 'Fixed'},
{'elevation_ft': '15', 'type': 'Free'}]))
primitives, _problems = F.freebody(values, 600, 150)
pin = [p for p in primitives if p['kind'] == 'polygon'
and p.get('role') == 'restraint-pin'][0]
ys = [pt[1] for pt in pin['points']]
assert max(ys) - min(ys) == 16.0
fixed_lines = [p for p in primitives if p['kind'] == 'line'
and p.get('role') == 'restraint-fixed']
drop = max(abs(l['y2'] - l['y1']) for l in fixed_lines)
assert drop == 16.0
free_dot = [p for p in primitives if p['kind'] == 'circle'
and p.get('role') == 'restraint-free'][0]
assert free_dot['r'] * 2.0 == 16.0
def test_restraint_triangle_apex_touches_the_line_below_it():
values = _merge(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '20'},
_rows('restraint', [{'elevation_ft': '10', 'type': 'Pin'}]))
primitives, _problems = F.freebody(values, 600, 150)
line_y = [p for p in primitives if p.get('role') == 'member'][0]['y1']
tri = [p for p in primitives if p.get('role') == 'restraint-pin'][0]
ys = [pt[1] for pt in tri['points']]
assert min(ys) == line_y # apex on the line
assert max(ys) == line_y + 16.0 # base 16 px below it
def test_restraint_symbols_carry_dark_stroke_fill_hints():
"""Coordinator: 'stroke': '#333', 'fill': '#888' -- one shared dark
styling across all three restraint symbol kinds ('equally dark'),
even though the WPF button does not draw the polygon/circle kinds at
all today (see the module docstring's RESTRAINT SYMBOLS note)."""
values = _merge(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '20'},
_rows('restraint', [{'elevation_ft': '5', 'type': 'Pin'},
{'elevation_ft': '8', 'type': 'Floor tie'},
{'elevation_ft': '11', 'type': 'Fixed'},
{'elevation_ft': '14', 'type': 'Free'},
{'elevation_ft': '17', 'type': ''}]))
primitives, _problems = F.freebody(values, 600, 150)
restraint_prims = [p for p in primitives
if str(p.get('role', '')).startswith('restraint-')
and p['kind'] != 'text']
assert restraint_prims
for p in restraint_prims:
assert p.get('stroke') == '#333'
assert p.get('width', 0) >= 2.0
if p['kind'] in ('polygon', 'circle'):
assert p.get('fill') == '#888'
# ------------------------------------ layout: rows below the member line
def test_restraint_label_sits_close_under_the_symbol():
values = _merge(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '20'},
_rows('restraint', [{'elevation_ft': '10', 'type': 'Pin'}]))
primitives, _problems = F.freebody(values, 600, 150)
line_y = [p for p in primitives if p.get('role') == 'member'][0]['y1']
label = [p for p in primitives if p.get('role') == 'restraint-label'][0]
# symbol bottom is 16 px below the line; the label should sit close
# under it, not far below (a small, fixed gap -- see
# `_RESTRAINT_LABEL_GAP`).
assert label['y'] - (line_y + 16.0) <= 8.0
def test_dimension_row_sits_about_22px_below_the_restraint_label_row():
values = _merge(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '20'},
_rows('restraint', [{'elevation_ft': '4', 'type': 'Pin'},
{'elevation_ft': '12', 'type': 'Pin'}]))
primitives, _problems = F.freebody(values, 600, 150)
label_y = [p for p in primitives
if p.get('role') == 'restraint-label'][0]['y']
dim_line = [p for p in primitives if p.get('role') == 'dimension'
and p['kind'] == 'line'][0]
assert abs(dim_line['y1'] - label_y - 22.0) < 1e-6
# ------------------------------------------------ layout: no text near line
def test_no_text_primitive_within_6px_of_the_member_line():
values = _merge(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '20'},
_rows('restraint', [{'elevation_ft': '0', 'type': 'Pin'},
{'elevation_ft': '10', 'type': 'Fixed'},
{'elevation_ft': '20', 'type': 'Free'}]),
_rows('conc_load', [
{'label': 'A', 'elevation_ft': '5', 'offset_in': '10',
'dead_k': '1', 'live_k': '2'},
{'label': 'B', 'elevation_ft': '15', 'offset_in': '-6',
'dead_k': '3', 'live_k': '4'}]),
_rows('wind_zone', [
{'label': 'W1', 'from_elev_ft': '2', 'to_elev_ft': '18',
'pressure_psf': '16', 'suction_psf': '16'}]))
primitives, _problems = F.freebody(values, 700, 160)
line_y = [p for p in primitives if p.get('role') == 'member'][0]['y1']
for p in primitives:
if p['kind'] != 'text':
continue
assert abs(p['y'] - line_y) > 6.0, p
# --------------------------------------------------------- layout: loads
def test_load_label_sits_to_the_right_of_its_arrow():
values = _merge(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '20'},
_rows('conc_load', [{'label': 'Ledger', 'elevation_ft': '10',
'offset_in': '10', 'dead_k': '1',
'live_k': '1'}]))
primitives, _problems = F.freebody(values, 600, 150)
arrow = [p for p in primitives if p.get('role') == 'load-arrow'][0]
label = [p for p in primitives if p.get('role') == 'load-label'][0]
assert label['x'] > arrow['x1']
assert abs(label['y'] - arrow['y1']) < 1e-6
assert label.get('anchor') != 'middle'
def test_load_arrow_is_at_least_30px_long():
values = _merge(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '20'},
_rows('conc_load', [{'label': 'X', 'elevation_ft': '10',
'offset_in': '0', 'dead_k': '1',
'live_k': '1'}]))
primitives, _problems = F.freebody(values, 600, 150)
arrow = [p for p in primitives if p.get('role') == 'load-arrow'][0]
assert abs(arrow['y2'] - arrow['y1']) >= 30.0
# ------------------------------------------------------- layout: wind band
def test_wind_zone_with_no_pressure_or_suction_shows_only_a_small_label():
values = _merge(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '20'},
_rows('wind_zone', [{'label': 'W1', 'from_elev_ft': '2',
'to_elev_ft': '18'}]))
primitives, _problems = F.freebody(values, 600, 150)
labels = _texts(primitives, role='wind-label')
assert labels == ['W1: no pressure entered yet']
assert not [p for p in primitives if p.get('role') == 'wind-zone-band']
assert not [p for p in primitives if p.get('role') == 'wind-arrow']
assert not [p for p in primitives if p.get('role') == 'wind-arrowhead']
for t in labels:
assert u'—' not in t
assert '+' not in t
def test_wind_zone_with_only_pressure_still_draws_the_band():
values = _merge(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '20'},
_rows('wind_zone', [{'label': 'W1', 'from_elev_ft': '2',
'to_elev_ft': '18', 'pressure_psf': '16'}]))
primitives, _problems = F.freebody(values, 600, 150)
labels = _texts(primitives, role='wind-label')
assert labels == ['W1: +16 / -— psf, w — ft']
assert [p for p in primitives if p.get('role') == 'wind-zone-band']
arrow_lines = [p for p in primitives if p['kind'] == 'line'
and p.get('role') == 'wind-arrow']
assert len(arrow_lines) >= 3
def test_wind_arrows_sit_above_the_load_arrows_not_overlapping():
values = _merge(
{'bottom_elevation_ft': '0', 'top_elevation_ft': '20'},
_rows('conc_load', [{'label': 'X', 'elevation_ft': '10',
'offset_in': '0', 'dead_k': '1',
'live_k': '1'}]),
_rows('wind_zone', [{'label': 'W1', 'from_elev_ft': '2',
'to_elev_ft': '18', 'pressure_psf': '10',
'suction_psf': '10'}]))
primitives, _problems = F.freebody(values, 600, 150)
wind_ys = [p['y2'] for p in primitives if p.get('role') == 'wind-arrow']
load_ys = [p['y1'] for p in primitives if p.get('role') == 'load-arrow']
# every wind-arrow bottom sits at or above every load-arrow top -- the
# two rows do not share vertical space.
assert max(wind_ys) <= min(load_ys) + 1e-6
# ------------------------------------------------- supports (2026-09-05)
#
# Owner ruling: bearing positions are the `support` row table (x_ft along
# the length), converted to an elevation off `bottom_elevation_ft` the same
# way restraints already are, for the free body's own "supports below,
# restraints above" drawing rule. `freebody_geometry` is what the HTML page
# reads (`pa_pagespec` -> `draw.js`'s `renderFreebody`), so this is tested
# through that entry point rather than the pixel-based `freebody()`.
def test_freebody_geometry_converts_support_table_rows_to_elevations():
values = _merge(
{'bottom_elevation_ft': '-2', 'top_elevation_ft': '20'},
_rows('support', [{'x_ft': '5'}, {'x_ft': '20'}]))
geom = F.freebody_geometry(values)
xs_elevs = sorted((s['x_ft'], s['elevation_ft']) for s in geom['supports'])
assert xs_elevs == [(5.0, 3.0), (20.0, 18.0)]
assert geom['problems'] == []
def test_freebody_geometry_falls_back_to_legacy_supports_ft_text():
values = {'bottom_elevation_ft': '0', 'supports_ft': '2, 22'}
geom = F.freebody_geometry(values)
assert sorted(s['elevation_ft'] for s in geom['supports']) == [2.0, 22.0]
def test_freebody_geometry_support_table_wins_over_legacy_text():
values = _merge(
{'bottom_elevation_ft': '0', 'supports_ft': '99, 100'},
_rows('support', [{'x_ft': '3'}]))
geom = F.freebody_geometry(values)
assert [s['elevation_ft'] for s in geom['supports']] == [3.0]
def test_freebody_geometry_no_bottom_elevation_reports_and_skips_supports():
values = _rows('support', [{'x_ft': '3'}])
geom = F.freebody_geometry(values)
assert geom['supports'] == []
assert any('bottom_elevation_ft' in p for p in geom['problems'])