8e6ee31596
ci / test (push) Successful in 6m18s
History-free snapshot of the private development repository at cbe45c5 (2026-09-09). Code, docs and public catalogue only; no plant or job knowledge layers, no client or job names (tools/scrub.py clean). The intro deck is left out of this snapshot because its slide images could not be checked for job drawings.
151 lines
7.6 KiB
Python
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
|