# -*- 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