WillTools: precast shop-ticket QC and panel handling tools
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.
This commit is contained in:
2026-09-03 16:33:47 -05:00
commit 52d94303bc
281 changed files with 155656 additions and 0 deletions
+276
View File
@@ -0,0 +1,276 @@
# -*- coding: utf-8 -*-
"""Real typography in the HTML report, and why the source strings stay ASCII.
`report.notate()` and `report.notate_plain()` are the ONE place engineering
shorthand -- "phi-Mn", "5*sqrt(f'c)", "f_se", "60deg" -- becomes real
subscripts, a real radical, a real degree sign. Everywhere else in this
package, in `checks.py`, `capacity.py` and `model.py`, that text stays plain
ASCII on purpose: the SAME strings are written to sweep/problems.txt, printed
to the console, exported into the job/problem JSON, and printed inside
Revit's pyRevit output window under IronPython 2.7 (see
`WillTools.extension/lib/vibe/liftexport.py`). A radical sign or a raw
`<sub>` tag in any of those is a crash or mojibake, not a nicer report --
this project has already been bitten by exactly that shape of bug once, in
the Discord bridge.
So these tests do two things: pin the rendering (given a known ASCII string,
the HTML/plain output is exactly this), and guard the boundary (the
producers stay ASCII; nothing here should ever need to change if `notate()`
changes).
Every string tested against notate()/notate_plain() below is copied VERBATIM
from `checks.py`, `capacity.py` or `model.py` -- not invented -- because an
untested token is exactly the "regex over prose" failure this was built to
avoid (`psi` the unit must never become `&psi;`; there is no rule for it at
all, on purpose -- see test_units_and_ratios_pass_through_untouched below).
"""
import re
from conftest import aspire_pockets, make_panel
from panellift import checks, report, statics
from panellift.model import Blockout, Prestress, Rigging, Stage, Strand
# --------------------------------------------------------- notate(): HTML
def test_the_radical_is_real_mathml_with_a_plain_fallback_beside_it():
"""A bare &radic; glued to a CSS overline is the old half-measure: the
glyph and the border never actually join. RADICAL_FC now has to carry a
real <msqrt> -- drawn as one shape by the browser's own MathML layout --
AND, right beside it, the plain-Unicode reading a non-MathML viewer gets
instead, because <math> has no fallback of its own."""
assert report.RADICAL_FC == (
'<math><msqrt><mrow><msub><mrow><mi>f</mi><mo>&#x2032;</mo></mrow>'
'<mi>c</mi></msub></mrow></msqrt></math>'
'<span class="radical-fallback">√f′c</span>')
assert report.RADICAL_FCI == (
'<math><msqrt><mrow><msub><mrow><mi>f</mi><mo>&#x2032;</mo></mrow>'
'<mi>ci</mi></msub></mrow></msqrt></math>'
'<span class="radical-fallback">√f′ci</span>')
# The CSS-overline technique is gone; nothing should still reference it.
assert '.rt{' not in report.CSS
assert 'class="rt"' not in report.RADICAL_FC
# The fallback switch: hidden by default, shown only behind a real
# MathML capability test, never behind a guess or a browser sniff.
assert 'math{display:none}' in report.CSS
assert '@supports (math-depth:0)' in report.CSS
assert '.radical-fallback{display:none}' in report.CSS
def test_fc_prime_gets_a_real_prime_and_a_real_subscript():
# Copied from Stage.tension_limit_text's shape: "5*sqrt(f'c)".
assert report.notate("5*sqrt(f'c)") == '5' + report.RADICAL_FC
assert report.notate("7.5*sqrt(f'c)") == '7.5' + report.RADICAL_FC
# f'ci (stripping strength) is a DIFFERENT subscript and must not be
# truncated to f'c's -- "f'ci" contains "f'c" as a substring.
assert report.notate("f'ci") == 'f&prime;<sub>ci</sub>'
assert report.notate("f'c") == 'f&prime;<sub>c</sub>'
# Bare, mid sentence -- from capacity.py's `point.why[col] = "stage
# states no f'c"`.
assert report.notate("stage states no f'c") == (
'stage states no f&prime;<sub>c</sub>')
def test_phi_mn_and_phi_vn_are_real_subscripts_not_hyphenated_ascii():
# From capacity.py's _FLEX_BASIS / _SHEAR_BASIS and checks.py's RATIO_WORD.
assert report.notate('phi-Mn') == '&phi;M<sub>n</sub>'
assert report.notate('phi-Vn') == '&phi;V<sub>n</sub>'
assert report.notate('M/phi-Mn') == 'M/&phi;M<sub>n</sub>'
assert report.notate('V/phi-Vn') == 'V/&phi;V<sub>n</sub>'
# Bare phi, e.g. checks.py's `detail += ' (phi {0:.2f})'`.
assert report.notate('(phi 0.90)') == '(&phi; 0.90)'
def test_a_real_finding_message_renders_with_real_markup():
"""Not a synthetic string -- an actual CapacityPoint.why /
development_note() sentence, copied from capacity.py, run through the
same function the Checks table uses."""
why = ('within a strand\'s development length of a free end (36.0 in. '
'from the end of the piece; l_d 63.7 in.) -- f_ps is 187.9 of '
'243.4 ksi here, so phi-Mn, which is strand only, tends to zero '
'while M_cr does not')
out = report.notate(why)
assert '&#8467;<sub>d</sub> 63.7 in.' in out
assert 'f<sub>ps</sub> is 187.9 of 243.4 ksi' in out
assert '&phi;M<sub>n</sub>, which is strand only' in out
assert 'M<sub>cr</sub> does not' in out
# And nothing here should ever come back out as raw ASCII shorthand.
assert 'phi-Mn' not in out and 'M_cr' not in out and 'l_d' not in out
def test_the_shear_basis_sentence_renders_v_n_equals_v_c():
# capacity.py's _SHEAR_BASIS, verbatim.
basis = ('phi-Vn here is CONCRETE ALONE -- ACI 318-19 22.5.5.1 '
'simplified, no shear reinforcement in the handling direction, '
'so V_n = V_c')
out = report.notate(basis)
assert '&phi;V<sub>n</sub> here is CONCRETE ALONE' in out
assert 'V<sub>n</sub> = V<sub>c</sub>' in out
def test_aps_in2_in3_and_degrees():
# checks.py: 'P {0} lb over Aps {1:.2f} in2 ...'
assert report.notate('Aps 0.61 in2') == 'A<sub>ps</sub> 0.61 in&sup2;'
# checks.py: 'S {6} in3'
assert report.notate('S 1,472 in3') == 'S 1,472 in&sup3;'
# checks.py: 'sin {4:.0f}deg' and 'the 60deg usually taken as ...'
assert report.notate('sin 60deg') == 'sin 60&deg;'
assert report.notate('below the 60deg usually') == (
'below the 60&deg; usually')
def test_the_multiplication_idiom_not_the_coordinate_one():
# checks.py SLING-ANGLE: 'line force is {1:.2f}x the vertical reaction'
assert report.notate('1.03x the vertical reaction') == (
'1.03&times; the vertical reaction')
# A coordinate is always written "x=1.23 ft" in this package -- no digit
# immediately in front of the letter -- so it must NOT be touched.
assert report.notate('the pick line at x=1.23 ft') == (
'the pick line at x=1.23 ft')
def test_units_and_ratios_pass_through_untouched():
"""The owner's own example: psi/ksi/plf/pli/lb/kips are already correct
and must never be touched, and a ratio like 4:1 stays exactly as typed.
There is no rule for any of these in _NOTATION_RULES -- this asserts the
omission, not an exception that could rot."""
for s in ('444 psi', '270 ksi', '109.4 plf', '80 pli', '34,638 lb',
'8 kips', 'Capacity in Concrete 4:1 SWL', '0.207'):
assert report.notate(s) == report.esc(s)
def test_data_is_still_escaped_around_the_markup():
"""A panel mark or a job-authored reason can carry HTML-significant
characters; notate() has to escape those exactly like esc() does, in the
SAME string that also carries real notation."""
out = report.notate("Tom & Jerry's <panel> f'c 4000 psi")
assert '&amp;' in out
assert '&lt;panel&gt;' in out
assert "Jerry&#x27;s" in out
assert 'f&prime;<sub>c</sub>' in out
# And never literal angle brackets from the data itself.
assert '<panel>' not in out
def test_le_ge_and_none_input():
assert report.notate('a <= b') == 'a &le; b'
assert report.notate('a >= b') == 'a &ge; b'
assert report.notate(None) == ''
# ---------------------------------------------- notate_plain(): SVG / JS text
def test_plain_mode_never_emits_a_tag():
"""<sub> in an <svg> <text> node or in JS textContent renders as four
literal characters, not a subscript -- notate_plain() must never emit
one."""
samples = ["5*sqrt(f'c)", 'phi-Mn', 'f_t', 'f_se', 'M_cr', 'Aps 0.61 in2',
'sin 60deg', '1.03x the vertical reaction', '(phi 0.90)']
for s in samples:
out = report.notate_plain(s)
assert '<' not in out and '>' not in out, (s, out)
assert '&' not in out, (s, out)
def test_plain_mode_uses_unicode_subscripts_where_they_exist():
assert report.notate_plain('f_t') == 'fₜ' # fₜ
assert report.notate_plain('phi-Mn') == 'φMₙ' # φMₙ
assert report.notate_plain("5*sqrt(f'c)") == "5√f′c"
# No Unicode subscript 'c' exists -- f_c degrades to plain "fc", not a
# fabricated subscript and not the bare underscore.
assert report.notate_plain('f_c') == 'fc'
assert '_' not in report.notate_plain('f_c')
# ---------------------------------------------------- the architecture edge
def test_the_source_strings_stay_ascii():
"""The producers -- Stage.tension_limit_text, and a real finding message
-- must still be pure ASCII after this change. Only report.py's renderer
may carry Unicode or HTML; console/sweep/JSON/IronPython read this."""
stage = Stage(name='stripping', multiplier=1.3, fc_psi=4000.0,
tension_limit_sqrt_fc=7.5, tension_limit_source='job')
text = stage.tension_limit_text
assert text == "7.5*sqrt(f'c)"
text.encode('ascii') # raises if it is not
panel = make_panel(blockouts=aspire_pockets())
analysis = statics.analyse(panel, stage, Rigging(mode='rolling'))
findings = checks.run_all(analysis, Rigging(mode='rolling'))
for f in findings:
f.message.encode('ascii')
def test_a_rendered_report_carries_no_raw_ascii_shorthand_in_its_body():
"""End to end: build a real report and check the BODY (not the CSS
comments, which are developer prose, not rendered text) for the ASCII
forms this whole module exists to replace."""
panel = make_panel(blockouts=aspire_pockets())
stage = Stage(name='stripping', multiplier=1.3, fc_psi=4000.0,
tension_limit_sqrt_fc=7.5, tension_limit_source='job')
analysis = statics.analyse(panel, stage, Rigging(mode='rolling'))
findings = checks.run_all(analysis, Rigging(mode='rolling'))
page = report.build(analysis, Rigging(mode='rolling'), findings,
recommendation=False)
body = page[page.index('</style>'):]
for pattern in (r"\bphi-Mn\b", r"\bphi-Vn\b", r"f'c\b", r"f'ci\b",
r"\bf_t\b", r"\bf_c\b", r"\bf_se\b", r"\bf_pu\b",
r"\bf_ps\b", r"\bl_d\b", r"\bl_t\b", r"\bM_cr\b",
r"\bV_n\b", r"\bV_c\b", r"\bAps\b", r"\bin2\b",
r"\bin3\b", r"\bd_b\b", r"\beps_t\b"):
assert not re.search(pattern, body), pattern
# And the positive side of the same assertion: the markup is actually
# there, not just absent.
assert '<sub>' in body and report.RADICAL_FC in body
# --------------------------------------- _prestress_line() vs. _rows()/notate()
def _panel_with_a_cut_strand():
"""A panel with real prestress and a blockout that severs the one
strand -- enough to exercise every clause of `_prestress_line()`,
including the `<strong>...cut by a blockout</strong>` branch."""
panel = make_panel(blockouts=[
Blockout(name='OPENING', x0=15.0, y0=1.5, length=2.0, width=1.0)])
panel.prestress = Prestress(
strands=[Strand(y=2.0, z_in=4.0, area_in2=0.153,
initial_pull_lb=0.153 * 270_000 * 0.75)],
loss_pct=8.0, diameter_in=0.5)
return panel
def test_prestress_line_returns_real_markup_never_escaped():
"""`_prestress_line()` builds its own <br>/<strong>/<sub> markup and
escapes only the free-form pieces itself (see its own docstring) -- this
pins that raw output. See the end-to-end test below for the regression
it was written to catch: `_rows()` running this same markup through
notate() a second time, printing the literal text "<br>" on the page."""
line = report._prestress_line(_panel_with_a_cut_strand())
assert '<br>' in line
assert '<sub>ps</sub>' in line and '&sup2;' in line
assert '<strong>1 cut by a blockout</strong>' in line
assert '&lt;' not in line and '&gt;' not in line
def test_the_rendered_piece_table_prestress_row_is_not_double_escaped():
"""Regression: the Piece table used to build the Prestress row with
`_rows()`, which runs every value through `notate()` -- safe for the
other rows' plain prose, but `_prestress_line()`'s value is already real
HTML it built and escaped itself, so notate() escaped it a SECOND time
and the report printed the literal text "<br>" / "<strong>" instead of a
line break and bold. Build a whole report and check the actual row."""
panel = _panel_with_a_cut_strand()
stage = Stage(name='stripping', multiplier=1.3, fc_psi=4000.0,
tension_limit_sqrt_fc=7.5, tension_limit_source='job')
analysis = statics.analyse(panel, stage, Rigging(mode='rolling'))
findings = checks.run_all(analysis, Rigging(mode='rolling'))
page = report.build(analysis, Rigging(mode='rolling'), findings,
recommendation=False)
assert '&lt;br&gt;' not in page and '&lt;strong&gt;' not in page
assert '<tr><th>Prestress</th><td>' in page
row = page[page.index('<tr><th>Prestress</th><td>'):]
row = row[:row.index('</td></tr>') + len('</td></tr>')]
assert '<br>' in row
assert '<strong>' in row and 'cut by a blockout' in row