Files
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

151 lines
7.6 KiB
Python

# -*- coding: utf-8 -*-
"""Panel Analysis: Top/Bottom Elevation and in-place Restraints, PURE.
Owner, 2026-09-04 (WPA655's morning run came up blank on `top_elevation_ft`,
`bottom_elevation_ft` and `restraints` -- see runlog): Eriksson's Concrete
Extents page carries Top Elevation / Bottom Elevation, and the Structural
Model page's in-place restraints are typically the same embed positions
`supports_ft` already found, just reported as elevations instead of a
distance up from the lower end. Both are arithmetic over numbers the caller
already measured -- no Revit API here, so this module is host-tested like
`pa_supports` / `pa_lifters` / `pa_reinf`, and the live half (reading a
framing element's world-space bounding box, in `script.py`'s `_read_
elevations`) stays a thin Revit-touching wrapper around it.
WHERE THE ELEVATION NUMBER COMES FROM. `elevations_from_bbox` takes the
framing element's AXIS-ALIGNED WORLD bounding box Z extent
(`el.get_BoundingBox(None)`), already in decimal feet the way every Revit Z
coordinate is -- the same "project elevation" a schedule or a section view
shows for anything placed in the model, because Revit's internal origin
already IS the datum those numbers are measured from. This is the piece AS
MODELLED, not a hypothetical erected attitude the way `pa_supports` /
`liftexport.erected_orientation` derive one for a piece that is still lying
flat on paper -- a wall panel placed in a real building model, the way this
tool's precast jobs use Revit, is modelled standing up in place, so its own
bounding box IS its in-place elevation. A piece not actually modelled
in-place (still on the casting-bed sheet, or a horizontal working view) gives
a bounding box that is NOT an elevation, and this module cannot tell the
difference -- callers name that limitation in the basis text they build
around this, never silently here.
WHERE RESTRAINTS COME FROM. `restraints_from_supports` re-expresses the same
`supports_ft` positions `pa_supports.read_supports` already measured (feet
from the erected-lower end) as elevations, adding the panel's own
`bottom_elevation_ft`. It is NOT a second measurement -- a plant that names
no support families leaves `supports_ft` blank, and this function then has
nothing to convert and returns `(None, why)` rather than fabricating an
elevation from zero.
Written for IronPython 2.7: no f-strings, no pathlib, `'{0}'.format(...)`
throughout.
"""
# pylint: disable=invalid-name
def elevations_from_bbox(min_z_ft, max_z_ft, length_ft=None, tol_ft=0.1):
"""(top_ft, bottom_ft, height_ft, basis) off a world-space bounding box.
`min_z_ft` / `max_z_ft` are the two Z extents of the element's own
bounding box (`el.get_BoundingBox(None)`), in any order -- the smaller
becomes `bottom_ft`, the larger `top_ft`. Either missing (`None`) is the
could-not-read answer: `(None, None, None, why)`, never a fabricated
elevation.
`length_ft`, when given, is compared against the measured height
(`top_ft - bottom_ft`): for a wall panel modelled standing up, the two
should agree, and a real gap larger than `tol_ft` is named in the basis
rather than silently absorbed -- it usually means the piece is not
modelled fully upright, or `length_ft` was measured along a different
axis than the one that is actually vertical here.
"""
if min_z_ft is None or max_z_ft is None:
return None, None, None, ('could not read a world-space bounding '
'box on this piece')
bottom = round(min(min_z_ft, max_z_ft), 4)
top = round(max(min_z_ft, max_z_ft), 4)
height = round(top - bottom, 4)
basis = ('the framing element\'s own world-space bounding box, Z {0:.2f}'
' to {1:.2f} ft -- the piece AS MODELLED, not a hypothetical '
'erected attitude'.format(bottom, top))
if length_ft is not None:
try:
gap = abs(height - float(length_ft))
except (TypeError, ValueError):
gap = None
if gap is not None and gap > tol_ft:
basis += ('; this differs from the measured length ({0:.2f} ft) '
'by {1:.2f} ft -- this piece may not be modelled '
'fully upright'.format(float(length_ft), gap))
return top, bottom, height, basis
def restraints_from_supports(supports_ft_text, bottom_elevation_ft,
height_ft=None, length_ft=None,
stand_tol_ft=1.0):
"""(text, basis) turning `supports_ft` positions into elevations, or
`(None, why)` when there is nothing to convert.
`supports_ft_text` is the SAME comma-separated feet string `pa_supports.
read_supports` writes into `reads['supports_ft']` (feet from the
erected-lower end); `bottom_elevation_ft` is this panel's own bottom
elevation (`elevations_from_bbox`'s third return). Either blank/None, or
a token that will not parse as a number, comes back as `(None, why)` --
never a partial or guessed conversion. The output is comma-separated,
the same grammar `panelanalysisform.parse_supports` already reads back
for both `supports_ft` and `restraints`.
`height_ft` / `length_ft`, when both given, guard against a piece that
does NOT stand with its length vertical -- a flat slab's world-space
bounding-box height is its THICKNESS, not its length, and adding a
`supports_ft` position (measured along the length) to that height would
not be an elevation at all. A gap bigger than `stand_tol_ft` between the
two skips the conversion and says why, rather than emitting a number
that looks like an elevation but mixes two different axes.
"""
if height_ft is not None and length_ft is not None:
try:
gap = abs(float(height_ft) - float(length_ft))
except (TypeError, ValueError):
gap = None
if gap is not None and gap > stand_tol_ft:
return None, (
'this piece\'s bounding-box height ({0:.2f} ft) does not '
'match its length ({1:.2f} ft) -- it does not appear to '
'stand with its length vertical, so supports_ft is not a '
'vertical offset here; restraints left blank rather than '
'converting a horizontal position into an elevation'
.format(float(height_ft), float(length_ft)))
if not supports_ft_text or bottom_elevation_ft is None:
missing = []
if not supports_ft_text:
missing.append('supports_ft')
if bottom_elevation_ft is None:
missing.append('bottom_elevation_ft')
return None, ('nothing to convert -- {0} not read'.format(
' and '.join(missing)))
positions = []
for tok in str(supports_ft_text).replace(',', ' ').split():
try:
positions.append(float(tok))
except (TypeError, ValueError):
return None, ('supports_ft carried a value that is not a '
'number ({0!r}); restraints could not be '
'converted'.format(tok))
if not positions:
return None, 'supports_ft parsed to no positions'
try:
bottom = float(bottom_elevation_ft)
except (TypeError, ValueError):
return None, 'bottom_elevation_ft is not a number'
elevations = sorted(round(bottom + p, 4) for p in positions)
text = ', '.join('{0:.2f}'.format(e) for e in elevations)
basis = ('the support embed positions ({0}) converted to elevations off '
'this panel\'s own bottom_elevation_ft ({1:.2f} ft) -- the '
'same embeds, not a second measurement'.format(
supports_ft_text, bottom))
return text, basis