Files
willtools-demo/panellift/tests/test_layout_pinning.py
T
will 25592fa6c2
ci / test (push) Successful in 6m14s
WillTools 2026-09-09 -- clean snapshot of the development repo for review
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.
2026-09-09 08:35:15 -05:00

119 lines
4.8 KiB
Python

# -*- coding: utf-8 -*-
"""Two constraints, not one, and they are not the same inserts.
A panel erected on its END carries its anchors in that end: they cannot move
along the piece, and where they sit across it is still a choice.
A panel erected on its long EDGE carries them along that edge: they may slide
freely down the length, and they may not come off the edge.
Collapsing both into one "pinned" flag gets one of the two wrong, and the
wrong one is a recommendation to put an anchor where the piece has no edge.
"""
import dataclasses
import pytest
from conftest import LENGTH_FT, WIDTH_FT, make_panel
from panellift import cli, layout, statics
from panellift.model import Lifter, Rigging, Stage
def build(anchor_kwargs, anchor_xs=(LENGTH_FT,), anchor_ys=None):
panel = make_panel()
face = [dataclasses.replace(l, capacity_lb=12000.0)
for l in panel.lifters if l.x < 0.5 * LENGTH_FT]
anchors = []
for n, x in enumerate(anchor_xs):
for m, y in enumerate(anchor_ys or (0.25 * WIDTH_FT, 0.75 * WIDTH_FT)):
anchors.append(Lifter(
mark='E{0}{1}'.format(n, m), family='EDGE_ANCHOR', x=x, y=y,
role='strip', capacity_lb=7690.0, shear_capacity_lb=4010.0,
on_edge=True, **anchor_kwargs))
return dataclasses.replace(panel, lifters=face + anchors,
weight_lb=35000.0)
def recommend(panel, stage, rigging):
a = statics.analyse(panel, stage, rigging, role='strip')
return layout.recommend([(a, rigging, [])])
def row_at(rec, x, tol=0.5):
return [r for r in rec.rows if abs(r.x - x) < tol][0]
# ------------------------------------------------- erected on its end
def test_an_end_anchor_row_holds_its_station(stripping, rolling):
rec = recommend(build({'pinned': True}), stripping, rolling)
row = row_at(rec, LENGTH_FT)
assert row.to_x == pytest.approx(LENGTH_FT)
assert row.pinned and not row.pinned_y
def test_an_end_anchor_row_may_still_be_moved_across_the_width(stripping,
rolling):
"""Being in the end says nothing about where across the piece it sits.
Drawn hard against one side, so there is something to move: the pair keeps
its spacing and is brought back onto the centreline.
"""
rec = recommend(build({'pinned': True}, anchor_ys=(1.0, 2.0)),
stripping, rolling)
moves = [m for m in rec.moves if m.family == 'EDGE_ANCHOR']
assert all(m.dx_in == pytest.approx(0.0) for m in moves)
assert all(abs(m.dy_in) > 0.5 for m in moves)
assert sum(m.to_y for m in moves) / len(moves) == pytest.approx(
WIDTH_FT / 2.0)
# ------------------------------------------- erected on its long edge
def test_an_edge_anchor_row_is_not_lifted_off_its_edge(stripping, rolling):
"""The one that was broken: a horizontal panel's anchors live on the top
edge, and recommending a y in the middle of the panel asks for an anchor
where there is no edge to cast it into."""
rec = recommend(build({'pinned_y': True},
anchor_xs=(8.0, 24.0),
anchor_ys=(WIDTH_FT, WIDTH_FT)),
stripping, rolling)
for move in [m for m in rec.moves if m.family == 'EDGE_ANCHOR']:
assert move.to_y == pytest.approx(WIDTH_FT)
assert move.dy_in == pytest.approx(0.0)
def test_an_edge_anchor_row_may_still_slide_along_the_piece(stripping,
rolling):
"""Sliding along the edge is the one degree of freedom it has, and it is
exactly the one the search exists to use."""
rec = recommend(build({'pinned_y': True},
anchor_xs=(1.0,), anchor_ys=(WIDTH_FT, WIDTH_FT)),
stripping, rolling)
row = row_at(rec, 1.0)
assert not row.pinned and row.pinned_y
assert row.to_x != pytest.approx(1.0)
# ------------------------------------------------------ the job file
def test_pinned_y_survives_the_job_file():
"""It is written by the Revit side; if the reader drops it the constraint
is silently gone and only a horizontal panel would ever show it."""
job = {
'schema': cli.SCHEMA,
'panel': {
'mark': 'H1', 'length_ft': 30.0, 'width_ft': 8.0,
'thickness_in': 8.0, 'weight_lb': 20000.0,
'lifters': [
{'mark': 'A', 'family': 'F', 'x': 5.0, 'y': 8.0,
'pinned_y': True, 'on_edge': True, 'capacity_lb': 7690.0},
{'mark': 'B', 'family': 'F', 'x': 25.0, 'y': 8.0,
'pinned_y': True, 'on_edge': True, 'capacity_lb': 7690.0},
],
},
'stages': [{'name': 'erection', 'multiplier': 1.2, 'fc_psi': 5000.0}],
}
panel, _rig, _cases = cli.analyse_job(job)
assert all(l.pinned_y and not l.pinned for l in panel.lifters)