52d94303bc
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.
571 lines
25 KiB
Python
571 lines
25 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Panel Analysis input form -- the "Concrete Extents" section, laid out.
|
|
|
|
The owner has said three times the Panel Analysis form is too ugly to read
|
|
("太丑了", "你这么输入用户上哪儿能看懂") and ruled, 2026-09-02: "全部给我做成
|
|
普通人能看懂的样子" -- every part of it has to be something an ordinary
|
|
person, not just an engineer, can understand. The fix agreed on is to make
|
|
the form look like Eriksson Wall's own printed page for this section (a solid
|
|
wall panel's "Concrete Extents" page: Panel Geometry, an Openings / Reveals /
|
|
Solid Zones table, and a drawings row) but with a plain-English `label`
|
|
beside every engineering `symbol` and a one-sentence `hint` on every field --
|
|
never the symbol alone, standing in as if it were the explanation.
|
|
|
|
`page(values, reads=None)` returns `(blocks, problems)`: a flat list of block
|
|
dicts from a fixed vocabulary a generic WPF renderer (built separately, not
|
|
here) consumes, plus a list of plain-English sentences for anything that
|
|
could not be laid out. This module never raises -- a field it cannot place
|
|
becomes a `problems` entry, never a traceback the owner has to read a
|
|
Python stack to understand.
|
|
|
|
WHY THIS FILE OWNS NO Revit, NO WPF AND NO `panelsketch` IMPORT: it has to
|
|
load and run on plain CPython under `tests/conftest.py`'s trick (see that
|
|
file's docstring) the same way the rest of Revit-free `vibe` does, and it has
|
|
to run under IronPython 2.7 inside the button. `panelsketch` is named by
|
|
STRING in a `drawing` block's `source` field -- the renderer calls it, not
|
|
this module -- because a sibling agent is extending `panelsketch` at the same
|
|
time this file is being written, and importing a module mid-rewrite is how
|
|
two agents' work collides.
|
|
|
|
WHY `values['openings_rows']` IS READ DEFENSIVELY: another agent owns
|
|
`vibe.panelanalysisform`, the source of the form's key names, and is
|
|
restructuring it at the same time this file is being written. A key this
|
|
module expects that is not actually there is "not present", never treated as
|
|
a bug -- `_paf_field` returns `None` and the caller falls back rather than
|
|
raising. The same discipline applies to the structured opening rows a third
|
|
agent is meant to be adding at `values['openings_rows']` (shaped
|
|
`{'type', 'pos_along', 'pos_along_from', 'length', 'pos_cross',
|
|
'pos_cross_from', 'width', 'pos_vert', 'height', 'label'}`, per that agent's
|
|
own contract): when it is not there yet, or is not a list, this module falls
|
|
back to the raw `values['openings']` text field and SAYS SO in `problems`,
|
|
rather than guessing at a shape that has not landed.
|
|
|
|
WHY THE SECTION PROPERTIES BLOCK OMITS MOST OF ERIKSSON'S OWN ROWS: Eriksson
|
|
prints Area, Width, Height, Cx, Cy, Izz, Iyy, Izy, Sb and St, but the last
|
|
seven need either an axis convention this module has no authority to invent,
|
|
or a net-of-openings deduction it does not compute. Printing a plausible
|
|
number in a column headed "St, in³" when nobody has verified it is exactly
|
|
the fabricated section modulus the owner has ruled against elsewhere in this
|
|
codebase (`CLAUDE.md` "no invented default" discipline) -- so this module
|
|
computes ONLY Area / Width / Height, from the panel's own width and
|
|
thickness, and names the rest in `problems` as omitted, never fills them in
|
|
with a guess.
|
|
|
|
WHY THE FRONT/SIDE ELEVATION SPLIT IS HONEST ABOUT ITS OWN LIMIT:
|
|
`panelsketch.elevation()`'s own docstring says it draws the front elevation
|
|
BESIDE a side elevation as one combined picture, not two separate ones.
|
|
Eriksson's page prints them as two separately captioned drawings, and this
|
|
module is asked to lay out a `columns` row of exactly that shape -- so it
|
|
does, naming `source: 'elevation'` under both the "Side Elevation" and
|
|
"Front Elevation" captions, and says in `problems` that until `panelsketch`
|
|
is split in two, both captions currently draw the same combined image. That
|
|
is a real, current limitation, not something to paper over by inventing a
|
|
`source` name that does not exist.
|
|
|
|
Miters get the same "print a sentence, never leave a blank" discipline
|
|
Eriksson uses on its own page (an unchecked "Left Miter" box there is not
|
|
just left bare -- it prints "Left miter not included in design" underneath).
|
|
`_miter_sentence` reproduces exactly that: a miter left BLANK (unanswered) is
|
|
silent, because blank means "not yet decided", never "no" (same rule
|
|
`panelanalysisform._parse_bool` documents for its own checkboxes); a miter
|
|
answered explicitly "no" prints the sentence, in words, the way Eriksson
|
|
does.
|
|
"""
|
|
|
|
# `panelanalysisform` carries no Revit import at module scope (see its own
|
|
# docstring), so this succeeds on plain CPython under `tests/conftest.py`'s
|
|
# trick and under IronPython 2.7 alike. If the sibling agent restructuring it
|
|
# has, at some moment, left it unimportable, this module still has to load --
|
|
# so the import itself is guarded, and every reader of `paf` below is guarded
|
|
# again on top of that (see `_paf_field`).
|
|
try:
|
|
from vibe import panelanalysisform as paf
|
|
except ImportError:
|
|
paf = None
|
|
|
|
|
|
# --------------------------------------------------------------- utilities
|
|
|
|
def _num(value):
|
|
"""`value` (a form string, or already a number) as a float, or None.
|
|
|
|
None covers "blank", "not a number", and "not present" alike -- the
|
|
caller only ever needs to know whether there is a usable number, and
|
|
`validate()`-style error text is not this module's job (the button's
|
|
completeness gate already owns that; this module only lays out what it
|
|
is given).
|
|
"""
|
|
if value is None:
|
|
return None
|
|
if isinstance(value, (int, float)):
|
|
return float(value)
|
|
text = str(value).strip()
|
|
if not text:
|
|
return None
|
|
try:
|
|
return float(text)
|
|
except (TypeError, ValueError):
|
|
return None
|
|
|
|
|
|
def _fmt_num(value):
|
|
"""A float as the plain string this page prints -- '8', not '8.0'."""
|
|
if value is None:
|
|
return ''
|
|
if float(value) == int(value):
|
|
return str(int(value))
|
|
return ('%.4f' % value).rstrip('0').rstrip('.')
|
|
|
|
|
|
# Eighths-of-an-inch -> the fraction word Eriksson prints. A precast
|
|
# dimension finer than 1/8" is not meaningful, so `_feet_str` rounds to it
|
|
# rather than pretending to more precision than the field carries.
|
|
_EIGHTHS = {0: '', 1: '1/8', 2: '1/4', 3: '3/8', 4: '1/2',
|
|
5: '5/8', 6: '3/4', 7: '7/8'}
|
|
|
|
|
|
def _feet_str(value):
|
|
"""Decimal feet -> an architectural dimension string, e.g. 8'-4 1/2".
|
|
|
|
Internal units are decimal feet throughout this codebase (`CLAUDE.md`
|
|
§3), so a bare number arriving in a position/length field is read as
|
|
feet and converted here to the feet-inches form Eriksson prints (`8"`,
|
|
`2'-11.5"`). A value that is already a string is left untouched by the
|
|
caller (`_dim_or_dash`) rather than reparsed here -- a sibling agent may
|
|
hand this module an already-formatted dimension, and reformatting a
|
|
string that already reads correctly is how a "2'-11.5\"" becomes a wrong
|
|
"2'-11 1/2\"" glued onto a stray decimal.
|
|
|
|
Returns None only when `value` cannot be read as a number at all.
|
|
"""
|
|
try:
|
|
feet = float(value)
|
|
except (TypeError, ValueError):
|
|
return None
|
|
negative = feet < 0.0
|
|
feet = abs(feet)
|
|
whole_ft = int(feet)
|
|
inches = (feet - whole_ft) * 12.0
|
|
eighths_total = int(round(inches * 8.0))
|
|
whole_in, eighths = divmod(eighths_total, 8)
|
|
if whole_in >= 12:
|
|
whole_ft += whole_in // 12
|
|
whole_in = whole_in % 12
|
|
frac = _EIGHTHS[eighths]
|
|
if frac and whole_in:
|
|
in_part = '{0} {1}'.format(whole_in, frac)
|
|
elif frac:
|
|
in_part = frac
|
|
else:
|
|
in_part = str(whole_in)
|
|
if whole_ft > 0:
|
|
text = u'{0}\'-{1}"'.format(whole_ft, in_part)
|
|
else:
|
|
text = u'{0}"'.format(in_part)
|
|
return u'-' + text if negative else text
|
|
|
|
|
|
def _dim_or_dash(value):
|
|
"""A table cell for one dimension: Eriksson's own '-' for blank."""
|
|
if value is None:
|
|
return '-'
|
|
if isinstance(value, str):
|
|
value = value.strip()
|
|
return value if value else '-'
|
|
text = _feet_str(value)
|
|
return text if text is not None else '-'
|
|
|
|
|
|
def _from_word(word):
|
|
"""'start' -> 'Start'; blank/None -> ''. Never invents a datum word."""
|
|
if not word:
|
|
return ''
|
|
word = str(word).strip()
|
|
return word[:1].upper() + word[1:].lower() if word else ''
|
|
|
|
|
|
def _pos_cell(pos, frm):
|
|
"""A 'Position' / 'Horizontal Position' cell: '8" from Start', or '-'."""
|
|
dim = _dim_or_dash(pos)
|
|
if dim == '-':
|
|
return '-'
|
|
frm_text = _from_word(frm)
|
|
return '{0} from {1}'.format(dim, frm_text) if frm_text else dim
|
|
|
|
|
|
_TYPE_LABELS = {'opening': 'Opening', 'reveal': 'Reveal',
|
|
'solid zone': 'Solid Zone', 'solid_zone': 'Solid Zone'}
|
|
|
|
|
|
def _type_label(value):
|
|
"""The Type column's text -- a known word capitalised Eriksson's way,
|
|
|
|
or, for anything this table does not recognise, the entered text back
|
|
in title case rather than a guessed category -- echoing what was typed
|
|
is honest; picking the "closest" known type is not.
|
|
"""
|
|
if not value:
|
|
return '-'
|
|
key = str(value).strip().lower().replace('_', ' ')
|
|
if key in _TYPE_LABELS:
|
|
return _TYPE_LABELS[key]
|
|
return str(value).strip().title() if str(value).strip() else '-'
|
|
|
|
|
|
def _tri_bool(text):
|
|
"""(True/False/None) for a checkbox's raw text. None = "not yet said".
|
|
|
|
Mirrors `panelanalysisform._parse_bool`'s own rule (blank is
|
|
"unspecified", never "no") without importing that private helper across
|
|
a module boundary neither module owns the other's internals of.
|
|
"""
|
|
if text is None:
|
|
return None
|
|
t = str(text).strip().lower()
|
|
if not t:
|
|
return None
|
|
if t in ('y', 'yes', 'true', '1', 'on', 'include', 'included', 'checked'):
|
|
return True
|
|
if t in ('n', 'no', 'false', '0', 'off', 'exclude', 'excluded',
|
|
'unchecked'):
|
|
return False
|
|
return None
|
|
|
|
|
|
# --------------------------------------------------------- panelanalysisform
|
|
|
|
def _paf_field(key):
|
|
"""The raw field dict `panelanalysisform.FIELDS` has for `key`, or None.
|
|
|
|
None covers every way `paf` can fail to have the answer: the module
|
|
failed to import, it has no `FIELDS` (mid-restructure), or `key` is
|
|
simply not one it declares yet. All three are "not present", never an
|
|
error -- see the module docstring.
|
|
"""
|
|
if paf is None:
|
|
return None
|
|
try:
|
|
fields = paf.FIELDS
|
|
except AttributeError:
|
|
return None
|
|
try:
|
|
for f in fields:
|
|
if f.get('key') == key:
|
|
return f
|
|
except (TypeError, AttributeError):
|
|
return None
|
|
return None
|
|
|
|
|
|
def _field(key, editor, fallback_label, fallback_symbol, fallback_hint,
|
|
fallback_required, problems, choices=None):
|
|
"""One `field` block, sourced from `panelanalysisform` when it has the
|
|
key and from the fallback text when it does not.
|
|
|
|
A missing key still gets a field on the page -- the Eriksson layout this
|
|
section is copying has a slot there -- but `problems` names it, because
|
|
a value typed into a field `panelanalysisform` does not declare cannot
|
|
be saved into the deck yet. That is the honest state of a form being
|
|
restructured by another agent at the same time, not a bug in this file.
|
|
"""
|
|
spec = _paf_field(key)
|
|
if spec is not None and paf is not None:
|
|
try:
|
|
label = paf.label_of(key)
|
|
symbol = paf.symbol_of(key) or None
|
|
hint = paf.help_of(key) or fallback_hint
|
|
required = bool(spec.get('required'))
|
|
except AttributeError:
|
|
spec = None
|
|
if spec is None:
|
|
label = fallback_label
|
|
symbol = fallback_symbol or None
|
|
hint = fallback_hint
|
|
required = fallback_required
|
|
problems.append(
|
|
"{0!r} is not a field vibe.panelanalysisform declares yet; "
|
|
"shown here to match Eriksson's own Concrete Extents page, but "
|
|
"a value typed into it will not be saved until that module "
|
|
"adds this key.".format(key))
|
|
return {'kind': 'field', 'key': key, 'label': label, 'symbol': symbol,
|
|
'hint': hint, 'required': required, 'editor': editor,
|
|
'choices': choices}
|
|
|
|
|
|
def _miter_sentence(values, key, side):
|
|
"""Eriksson's own discipline: an OFF miter prints a sentence, not a gap.
|
|
|
|
Blank (unanswered) prints nothing here, same as Eriksson's page shows
|
|
nothing extra for a miter nobody has touched yet -- only an explicit
|
|
"no" earns the sentence, because blank means "not yet decided", not
|
|
"excluded" (see `_tri_bool`).
|
|
"""
|
|
if _tri_bool(values.get(key)) is False:
|
|
return {'kind': 'sentence',
|
|
'text': '{0} miter not included in this panel\'s design.'
|
|
.format(side)}
|
|
return None
|
|
|
|
|
|
# ------------------------------------------------------------- sub-sections
|
|
|
|
def _panel_geometry_columns(values, problems):
|
|
"""The three columns Eriksson's own Panel Geometry block prints:
|
|
|
|
(Top Elevation, Bottom Elevation, Height, Section Type, Top in Form) |
|
|
(Width, Left Miter, Right Miter, Top In Form, Width, Height) |
|
|
(Thickness).
|
|
|
|
`length_ft` (this codebase's name for the panel's long dimension) is
|
|
what Eriksson calls "Height" on this page -- the same field, read
|
|
through `panelanalysisform`'s own label rather than renamed here, so a
|
|
reviewer sees the one name every section of the form uses for it.
|
|
"""
|
|
col1 = [
|
|
_field('top_elevation_ft', 'num', 'Top elevation', 'ft',
|
|
'How high the top edge of the panel sits above the '
|
|
'project reference elevation. Leave blank if not yet '
|
|
'known.', False, problems),
|
|
_field('bottom_elevation_ft', 'num', 'Bottom elevation', 'ft',
|
|
'How high the bottom edge of the panel sits above the '
|
|
'project reference elevation. Leave blank if not yet '
|
|
'known.', False, problems),
|
|
_field('length_ft', 'num', 'Panel length', 'ft',
|
|
"The panel's long dimension, end to end. Required -- "
|
|
'everything downstream is measured against this span.',
|
|
True, problems),
|
|
_field('section_type', 'text', 'Section type', '',
|
|
'A short description of the cross-section (solid, '
|
|
'sandwich, ribbed, ...). Reference only.', False, problems),
|
|
_field('top_in_form', 'text', 'Which face is up in the casting '
|
|
'bed', 'Interior / Exterior',
|
|
'Which face of the panel faces up while it is cast. '
|
|
'Reference only.', False, problems,
|
|
choices=['Interior', 'Exterior']),
|
|
]
|
|
|
|
col2 = [
|
|
_field('width_ft', 'num', 'Panel width', 'ft',
|
|
"The panel's dimension across the span -- how wide it is. "
|
|
'Required.', True, problems),
|
|
_field('left_miter', 'check', 'Left miter', '',
|
|
'Whether the left end of the panel is cut at an angle '
|
|
'(mitered) instead of square. Leave unchecked if this has '
|
|
'not been decided yet.', False, problems),
|
|
]
|
|
sentence = _miter_sentence(values, 'left_miter', 'Left')
|
|
if sentence:
|
|
col2.append(sentence)
|
|
col2.append(
|
|
_field('right_miter', 'check', 'Right miter', '',
|
|
'Whether the right end of the panel is cut at an angle '
|
|
'(mitered) instead of square. Leave unchecked if this has '
|
|
'not been decided yet.', False, problems))
|
|
sentence = _miter_sentence(values, 'right_miter', 'Right')
|
|
if sentence:
|
|
col2.append(sentence)
|
|
col2.append(
|
|
_field('top_in_form_check', 'check',
|
|
'Top edge has a formed step', '',
|
|
'Whether the top edge of the panel has its own formed '
|
|
'step or insert built in, separate from which face faces '
|
|
'up (see "Which face is up" at left). Leave unchecked if '
|
|
'there is none.', False, problems))
|
|
col2.append(
|
|
_field('top_in_form_width_in', 'num', 'Formed step width', 'in',
|
|
'How wide the formed step at the top edge is, if there is '
|
|
'one. Leave blank if there is no step.', False, problems))
|
|
col2.append(
|
|
_field('top_in_form_height_in', 'num', 'Formed step height', 'in',
|
|
'How tall the formed step at the top edge is, if there is '
|
|
'one. Leave blank if there is no step.', False, problems))
|
|
|
|
col3 = [
|
|
_field('thickness_in', 'num', 'Panel thickness', 'in',
|
|
'How thick the panel is. Required.', True, problems),
|
|
]
|
|
|
|
return [{'kind': 'heading', 'text': 'Panel Geometry', 'level': 2},
|
|
{'kind': 'columns', 'columns': [col1, col2, col3]}]
|
|
|
|
|
|
# The grouped header the Openings / Reveals / Solid Zones table prints,
|
|
# spanning the last 6 of its 7 columns: 'Type' stands alone at the left the
|
|
# way Eriksson's own page leaves it, and (2, 4) is what leaves the group
|
|
# spans adding up to exactly `len(headers) - 1`.
|
|
_OPENINGS_HEADERS = ['Type', 'Position', 'Length', 'Horizontal Position',
|
|
'Width', 'Vertical Position', 'Height']
|
|
_OPENINGS_GROUPS = [('Position along Length', 2), ('Position in Cross '
|
|
'Section', 4)]
|
|
|
|
|
|
def _openings_row_cells(row):
|
|
row = row or {}
|
|
return [
|
|
_type_label(row.get('type')),
|
|
_pos_cell(row.get('pos_along'), row.get('pos_along_from')),
|
|
_dim_or_dash(row.get('length')),
|
|
_pos_cell(row.get('pos_cross'), row.get('pos_cross_from')),
|
|
_dim_or_dash(row.get('width')),
|
|
_dim_or_dash(row.get('pos_vert')),
|
|
_dim_or_dash(row.get('height')),
|
|
]
|
|
|
|
|
|
def _openings_blocks(values, problems):
|
|
"""The Openings, Reveals, and Solid Zones heading + table (or fallback).
|
|
|
|
`values['openings_rows']` is the structured shape a sibling agent is
|
|
meant to be adding (see the module docstring); when it is missing, or
|
|
is not a list, this degrades to the raw `values['openings']` text field
|
|
and a `problems` entry -- never a guessed re-parse of that free text
|
|
into the grouped shape, which would be inventing datum words
|
|
("from Start" / "from Left") nobody has actually said.
|
|
"""
|
|
blocks = [{'kind': 'heading', 'text': 'Openings, Reveals, and Solid '
|
|
'Zones', 'level': 2}]
|
|
rows = values.get('openings_rows') if values else None
|
|
|
|
if isinstance(rows, list):
|
|
table_rows = [_openings_row_cells(r) for r in rows]
|
|
blocks.append({'kind': 'table', 'headers': _OPENINGS_HEADERS,
|
|
'header_groups': _OPENINGS_GROUPS,
|
|
'rows': table_rows, 'row_key': None})
|
|
if not rows:
|
|
blocks.append({'kind': 'sentence',
|
|
'text': 'No openings, reveals, or solid zones '
|
|
'entered.'})
|
|
return blocks
|
|
|
|
problems.append(
|
|
"'openings_rows' is not present in the form values (or is not a "
|
|
'list), so the grouped Position-along-Length / Position-in-'
|
|
'Cross-Section table cannot be built; falling back to the raw '
|
|
"'openings' text field.")
|
|
raw = (values.get('openings') if values else None) or ''
|
|
raw = raw.strip() if isinstance(raw, str) else str(raw).strip()
|
|
if raw:
|
|
blocks.append({'kind': 'sentence',
|
|
'text': 'Openings were entered as plain text and '
|
|
'cannot be shown as the grouped table yet: '
|
|
'{0}'.format(raw)})
|
|
else:
|
|
blocks.append({'kind': 'sentence',
|
|
'text': 'No openings, reveals, or solid zones '
|
|
'entered.'})
|
|
return blocks
|
|
|
|
|
|
# Every Section Properties row Eriksson's own page prints. Only the first
|
|
# three (`_DERIVABLE_PROPS`) are ever computed here -- see the module
|
|
# docstring for why the rest are refused rather than guessed.
|
|
_DERIVABLE_PROPS = ('Area, in²', 'Width, in', 'Height, in')
|
|
_OMITTED_PROPS = ('Cx, in', 'Cy, in', 'Izz, in⁴', 'Iyy, in⁴',
|
|
'Izy, in⁴', 'Sb, in³', 'St, in³')
|
|
|
|
|
|
def _section_properties_column(values, problems):
|
|
"""The Section Properties kv list -- Area / Width / Height ONLY.
|
|
|
|
Both come straight from what the user entered (panel width, panel
|
|
thickness) with no engineering judgement in between, so they are the
|
|
only rows honest to print. `Height` here is Eriksson's name for the
|
|
section's own depth through the panel, i.e. the panel THICKNESS -- not
|
|
the panel's long dimension, which the Panel Geometry block above already
|
|
calls "Height" in its own different sense. Printing both under the same
|
|
word in two different places on the same page is exactly Eriksson's own
|
|
layout, confusing as that is; relabelling one of them would stop this
|
|
matching the page the owner asked for.
|
|
"""
|
|
col = [{'kind': 'heading', 'text': 'Section Properties', 'level': 3}]
|
|
width_ft = _num(values.get('width_ft'))
|
|
thickness_in = _num(values.get('thickness_in'))
|
|
if width_ft is not None and width_ft > 0 and \
|
|
thickness_in is not None and thickness_in > 0:
|
|
width_in = width_ft * 12.0
|
|
area_in2 = width_in * thickness_in
|
|
rows = [('Area, in²', _fmt_num(area_in2)),
|
|
('Width, in', _fmt_num(width_in)),
|
|
('Height, in', _fmt_num(thickness_in))]
|
|
col.append({'kind': 'kv', 'rows': rows})
|
|
problems.append(
|
|
'{0} are not shown: deriving them honestly needs an axis '
|
|
'convention and, wherever openings are present, a '
|
|
'net-of-openings deduction this module does not compute -- a '
|
|
'guessed section modulus is worse than none.'
|
|
.format(', '.join(_OMITTED_PROPS)))
|
|
else:
|
|
col.append({'kind': 'sentence',
|
|
'text': 'Section properties are waiting on Panel '
|
|
'width and Panel thickness; nothing is shown '
|
|
'until both are entered.'})
|
|
problems.append(
|
|
'{0} could not be derived -- Panel width and/or Panel '
|
|
'thickness is missing.'.format(
|
|
', '.join(_DERIVABLE_PROPS + _OMITTED_PROPS)))
|
|
return col
|
|
|
|
|
|
def _drawings_row(problems):
|
|
"""The Side Elevation / Front Elevation / Section Properties row.
|
|
|
|
Both drawings name `source: 'elevation'` -- see the module docstring's
|
|
"WHY THE FRONT/SIDE ELEVATION SPLIT IS HONEST ABOUT ITS OWN LIMIT" for
|
|
why that is today's correct, if unsatisfying, answer rather than a name
|
|
this module invents to look like two different pictures.
|
|
"""
|
|
problems.append(
|
|
"Side Elevation and Front Elevation both use panelsketch."
|
|
'elevation(), which currently draws the side and front views '
|
|
'together as ONE picture (per that function\'s own docstring) '
|
|
'rather than as two separate drawings; until panelsketch is split '
|
|
'in two, both captions below show the same combined image.')
|
|
side_col = [{'kind': 'drawing', 'source': 'elevation',
|
|
'caption': 'Side Elevation', 'w': 260, 'h': 520}]
|
|
front_col = [{'kind': 'drawing', 'source': 'elevation',
|
|
'caption': 'Front Elevation', 'w': 420, 'h': 520}]
|
|
return side_col, front_col
|
|
|
|
|
|
# ------------------------------------------------------------------ page()
|
|
|
|
def page(values, reads=None):
|
|
"""(blocks, problems) for the Concrete Extents section.
|
|
|
|
`values` is the form's current string-valued dict (plus, where a
|
|
sibling agent has landed it, the structured `openings_rows` list -- see
|
|
the module docstring). `reads` is what the button read straight off the
|
|
live framing element, the same shape `panelanalysisform.prefill` takes;
|
|
it is accepted for symmetry with the other section builders but not
|
|
read here -- everything this section needs is already folded into
|
|
`values` by the time `page` sees it, and reading `reads` again here
|
|
would risk showing a MODEL value beside a form value the user has since
|
|
typed over it, exactly the kind of stale-looking page the owner has
|
|
complained about.
|
|
|
|
Never raises. Every field this module cannot place is a `problems`
|
|
sentence, not a traceback -- see the module docstring for the two
|
|
running examples (a `panelanalysisform` key that is not there yet, and
|
|
an `openings_rows` shape that has not landed yet).
|
|
"""
|
|
values = values if values is not None else {}
|
|
problems = []
|
|
blocks = [{'kind': 'heading', 'text': 'Concrete Extents', 'level': 1}]
|
|
|
|
blocks.extend(_panel_geometry_columns(values, problems))
|
|
blocks.append({'kind': 'space', 'h': 8})
|
|
|
|
blocks.extend(_openings_blocks(values, problems))
|
|
blocks.append({'kind': 'space', 'h': 8})
|
|
|
|
side_col, front_col = _drawings_row(problems)
|
|
props_col = _section_properties_column(values, problems)
|
|
blocks.append({'kind': 'columns',
|
|
'columns': [side_col, front_col, props_col]})
|
|
blocks.append({'kind': 'space', 'h': 8})
|
|
|
|
blocks.append({'kind': 'drawing', 'source': 'section',
|
|
'caption': 'Cross Section', 'w': 900, 'h': 260})
|
|
|
|
return blocks, problems
|