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.
667 lines
32 KiB
Python
667 lines
32 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""Panel Analysis input form -- the "Structural Model" section, laid out.
|
||
|
||
WHY THIS EXISTS: the owner rejected the Panel Analysis form three times as
|
||
unreadable ("太丑了", "你这么输入用户上哪儿能看懂") and finally ruled, 2026-09-02,
|
||
"全部给我做成普通人能看懂的样子" -- make every part of it something an ordinary
|
||
person can understand. The fix is not a paint job: `page()` below rebuilds
|
||
the Structural Model section (restraints, bearing, releases, concentrated
|
||
loads, and the wind / seismic / temperature / soil load families) to look
|
||
like Eriksson Wall's own printed page for it, because that page is the
|
||
document a reviewing engineer already knows how to read. Every field still
|
||
carries the engineering `symbol` beside its plain-English `label` -- the
|
||
Unicode notation is not decoration, it is what gets checked against
|
||
Eriksson's own printout -- and every field carries a one-sentence `hint`
|
||
saying what it is for and what a blank means, because a symbol alone
|
||
("Vult, mph") is exactly the kind of label that got this form rejected.
|
||
|
||
`page(values, reads=None)` returns `(blocks, problems)` -- a flat list of
|
||
BLOCK dicts from a small fixed vocabulary a generic WPF renderer consumes
|
||
(an unrecognised `kind` renders as nothing, so this module can only ever
|
||
degrade a layout, never crash one), plus a list of plain-English sentences
|
||
describing anything that could not be built as designed. `page` never
|
||
raises: a field the shared field model does not have yet, or a `values`
|
||
dict missing a key entirely, becomes a `problems` sentence and a
|
||
stand-in label, not an exception and not an invented value.
|
||
|
||
TWO RULES FROM CLAUDE.md THIS MODULE IS BUILT AROUND, section 3:
|
||
|
||
* "Could not judge" and "judged, found nothing" must never look alike.
|
||
Every OPTIONAL block here (Releases, Concentrated Loads, the Wind /
|
||
Seismic zone tables, both Soil Loads, Additional Self Weight, Design
|
||
Strips) has to say something when it has nothing to show, and there
|
||
are two different somethings: the key existing in `values` but blank
|
||
means the user was asked and answered "none" (Eriksson's own wording,
|
||
e.g. "Design contains no exterior soil loads" -- generic boilerplate,
|
||
not a client's text, and kept because it is the plain-English answer a
|
||
non-engineer would also write); the key not existing in `values` at
|
||
all means this form has never asked yet, and the sentence says so
|
||
instead of quietly agreeing with "none". Collapsing those two into one
|
||
sentence is exactly the failure `knowledge.same_face()` returning
|
||
`None` instead of `False` guards against elsewhere in this codebase.
|
||
* Nothing here computes wind or seismic. The owner ruled it explicitly,
|
||
2026-09-02: "风的计算还没有你就留着空着,让用户输入" -- the wind engine is a
|
||
later stage, so every wind/seismic field is collected, labelled as not
|
||
yet used by the analysis, and left blank. No pressure, no Kz, no
|
||
exposure default, nothing invented to make the page look more finished
|
||
than the analysis actually is.
|
||
|
||
WHERE THE LAYOUT COMES FROM: Eriksson Wall's own printed "Structural
|
||
Model" page -- a Restraints table (foundation connection, one row per tie
|
||
elevation with a gravity/thermal restraint flag and a stiffness) beside a
|
||
Releases block, an elevation diagram, a Concentrated Loads table split into
|
||
dead/live/roof/wind-pressure/wind-suction/seismic components, Load
|
||
Options, then the Wind / Seismic / Temperature / Soil / Additional Self
|
||
Weight load families and a Design Strips note. Only the STRUCTURE and
|
||
generic field names are taken from that page -- no project, client,
|
||
licensee, engineer, job number, file name or date from it appears
|
||
anywhere below; every label is written from scratch in plain engineering
|
||
English. See CLAUDE.md's "No client or job information in the repo. Ever."
|
||
|
||
READING `vibe.panelanalysisform` DEFENSIVELY: that module owns the actual
|
||
key names and is being restructured by another agent while this one is
|
||
being written. `page()` imports it, reads its `FIELDS` list once to find
|
||
out which keys it already knows about, and for every key this section
|
||
wants that it does NOT find there, it renders the field anyway (with a
|
||
label and hint written locally, right here) and adds one sentence to
|
||
`problems` naming the key and what it is for -- never a raised exception,
|
||
never a guess passed off as the shared field model's answer. Some of the
|
||
keys used below (`supports_ft`, `restraints`, `conc_loads`, every
|
||
`wind_*` key, `seismic_cs`, `ext_soil_loads`, `int_soil_loads`) already
|
||
exist in `panelanalysisform.FIELDS` today and are read from there;
|
||
Eriksson's page has considerably more than that thin set, and the rest are
|
||
new key names this module proposes (`restraint_rows`, `releases`,
|
||
`wind_zone_rows`, `seismic_zone_rows`, `temp_ext_f`, ... -- see the field
|
||
list inline) for the field-model agent to land.
|
||
|
||
Standard library only, no Revit, no WPF, no `panelsketch` import -- the
|
||
`kind: 'drawing'` blocks below only NAME a `vibe.panelsketch` entry point
|
||
as their `source`; a sibling agent owns what that module draws.
|
||
|
||
IronPython 2.7 throughout: no f-strings, `'{0}'.format(...)` everywhere,
|
||
`# -*- coding: utf-8 -*-` up top, no `pathlib` / `dataclasses` / `typing`.
|
||
"""
|
||
|
||
import re
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# Small helpers shared by every block below. None of these touch `values` --
|
||
# they only ever read it, and `page()` never assigns into a caller-owned
|
||
# dict, so the same `values` a caller hands in comes back untouched.
|
||
# --------------------------------------------------------------------------
|
||
|
||
def _text(value):
|
||
"""Coerce whatever sits in `values` to a string, defensively.
|
||
|
||
`values` is documented as string-valued, but this form is fed by a
|
||
live model read and a WPF text box on one side and a mid-refactor
|
||
field model on the other -- a stray non-string (an `int` typed
|
||
straight into a dict during a test, a `None` a half-written prefill
|
||
left behind) must degrade to an empty string, never raise.
|
||
"""
|
||
if value is None:
|
||
return ''
|
||
try:
|
||
value.strip # duck-typed string check -- works for str/unicode both
|
||
return value
|
||
except AttributeError:
|
||
try:
|
||
return str(value)
|
||
except Exception:
|
||
return ''
|
||
|
||
|
||
def _known_field_keys(paf):
|
||
"""The set of keys `vibe.panelanalysisform.FIELDS` already defines.
|
||
|
||
Empty set (never an exception) if the module failed to import or its
|
||
`FIELDS` list could not be read -- the caller has already logged why in
|
||
`problems` by the time this runs, and an empty set makes every field
|
||
below fall through to its local stand-in label, which is the correct
|
||
degrade rather than a crash.
|
||
"""
|
||
if paf is None:
|
||
return set()
|
||
try:
|
||
return set(f['key'] for f in paf.FIELDS)
|
||
except Exception:
|
||
return set()
|
||
|
||
|
||
def _field(paf, known, problems, key, required, editor, choices=None,
|
||
fallback_label=None, fallback_symbol='', fallback_hint='',
|
||
what=''):
|
||
"""One `kind: 'field'` block, sourced from `panelanalysisform` when it
|
||
already knows `key`, and from the fallback text passed in when it does
|
||
not -- appending one `problems` sentence in that second case, naming
|
||
the key and what it is for, so the field-model agent has a punch list
|
||
rather than a form that silently drifted from `panelanalysisform`.
|
||
"""
|
||
if key in known:
|
||
label = paf.label_of(key)
|
||
symbol = paf.symbol_of(key) or None
|
||
hint = paf.help_of(key) or None
|
||
else:
|
||
label = fallback_label or key
|
||
symbol = fallback_symbol or None
|
||
hint = fallback_hint or None
|
||
problems.append(
|
||
"'{0}' ({1}) has no entry yet in vibe.panelanalysisform -- "
|
||
'shown with a stand-in label until the field model adds it.'
|
||
.format(key, what or fallback_label or key))
|
||
return {'kind': 'field', 'key': key, 'label': label, 'symbol': symbol,
|
||
'hint': hint, 'required': required, 'editor': editor,
|
||
'choices': choices}
|
||
|
||
|
||
def _optional_note(values, key, blank_sentence, missing_sentence, problems,
|
||
what):
|
||
"""(state, text) for one optional free-text block.
|
||
|
||
Three states, and the whole point of this helper is that the first two
|
||
never look alike:
|
||
|
||
'missing' -- `key` is not in `values` at all. This form has not asked
|
||
the question yet; `text` is `missing_sentence` and one
|
||
line is appended to `problems` naming the gap. This is
|
||
"could not judge", never "judged, found nothing".
|
||
'blank' -- `key` is in `values` but empty/whitespace. The user was
|
||
asked and the honest answer is none; `text` is
|
||
`blank_sentence`. This IS "judged, found nothing".
|
||
'filled' -- `key` has real text; `text` is that text, stripped, for
|
||
the caller to parse into a table.
|
||
"""
|
||
if key not in values:
|
||
problems.append(
|
||
"'{0}' ({1}) is not collected by this form yet -- rendered as "
|
||
"unknown, never silently as \"none\"."
|
||
.format(key, what))
|
||
return 'missing', missing_sentence
|
||
text = _text(values.get(key)).strip()
|
||
if not text:
|
||
return 'blank', blank_sentence
|
||
return 'filled', text
|
||
|
||
|
||
def _rows_from_lines(text, extra_cols=0):
|
||
"""[[id, col, col, ...], ...] -- one row per non-blank line, comma-split.
|
||
|
||
This is a DISPLAY parse, not `panelanalysisform`'s validating one: a
|
||
line that does not split into the expected shape is shown with
|
||
whatever columns it did produce rather than refused, because this
|
||
table exists to show the user what they typed back at them, not to
|
||
gate the Run button (that gate is `panelanalysisform.validate`'s job,
|
||
on the raw field text, elsewhere).
|
||
"""
|
||
rows = []
|
||
for i, raw in enumerate(text.splitlines(), 1):
|
||
line = raw.strip()
|
||
if not line:
|
||
continue
|
||
cols = [p.strip() for p in line.split(',')]
|
||
rows.append([str(i)] + cols)
|
||
return rows
|
||
|
||
|
||
def _restraint_rows_from(values, problems):
|
||
"""(rows, state) for the Restraints table's ID/Elevation/Type/Gravity/
|
||
Thermal/Stiffness columns.
|
||
|
||
Prefers the full `restraint_rows` key (one line per restraint: elevation,
|
||
type, gravity y/n, thermal y/n, stiffness) once the field model carries
|
||
it. Until then this falls back to the existing `restraints` key, which
|
||
today is elevations only -- so the Elevation column still fills in from
|
||
whatever the user already typed, and the Type/Gravity/Thermal/Stiffness
|
||
columns show blank rather than inventing a restraint type nobody typed.
|
||
Both cases add a `problems` line; only the outcome (which columns come
|
||
from where) differs.
|
||
"""
|
||
if 'restraint_rows' in values:
|
||
text = _text(values.get('restraint_rows')).strip()
|
||
if not text:
|
||
return [], 'blank'
|
||
rows = []
|
||
for i, raw in enumerate(text.splitlines(), 1):
|
||
line = raw.strip()
|
||
if not line:
|
||
continue
|
||
parts = [p.strip() for p in line.split(',')]
|
||
parts = (parts + ['', '', '', '', ''])[:5]
|
||
rows.append([str(i)] + parts)
|
||
return rows, ('filled' if rows else 'blank')
|
||
|
||
problems.append(
|
||
"'restraint_rows' (restraint type, gravity/thermal restraint flags "
|
||
"and stiffness per elevation) has no entry yet in "
|
||
"vibe.panelanalysisform -- only plain elevations ('restraints') "
|
||
'are collected today, so those four columns show blank rather '
|
||
'than an invented restraint type.')
|
||
|
||
if 'restraints' not in values:
|
||
return [], 'missing'
|
||
text = _text(values.get('restraints')).strip()
|
||
if not text:
|
||
return [], 'blank'
|
||
toks = [t for t in re.split(r'[,\n]+', text) if t.strip()]
|
||
rows = []
|
||
for i, tok in enumerate(toks, 1):
|
||
rows.append([str(i), tok.strip(), '', '', '', ''])
|
||
return rows, 'filled'
|
||
|
||
|
||
def _table_from_text(text, headers):
|
||
return {'kind': 'table', 'headers': headers, 'header_groups': None,
|
||
'rows': _rows_from_lines(text), 'row_key': None}
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# page()
|
||
# --------------------------------------------------------------------------
|
||
|
||
def page(values, reads=None):
|
||
"""(blocks, problems) for the Structural Model section.
|
||
|
||
`values` is the form's current string-valued dict, keyed by
|
||
`vibe.panelanalysisform` field names (plus new names this module
|
||
proposes -- see the module docstring). Never mutated: every access
|
||
below is `values.get(...)` or `key in values`, nothing is assigned
|
||
into it. `reads` is accepted for symmetry with the rest of the form
|
||
(a future model-read prefill for this section) but nothing here is on
|
||
`panelanalysisform.PREFILLABLE` today, so it is not read.
|
||
|
||
Never raises. A field the shared field model does not know yet, an
|
||
unreadable `panelanalysisform`, or `values` missing a key entirely all
|
||
degrade to a `problems` sentence plus a reasonable stand-in in the
|
||
layout -- never an exception, never a fabricated value.
|
||
"""
|
||
values = values if values is not None else {}
|
||
problems = []
|
||
blocks = []
|
||
|
||
paf = None
|
||
try:
|
||
from vibe import panelanalysisform as paf
|
||
except Exception as exc:
|
||
problems.append(
|
||
'could not import vibe.panelanalysisform ({0}); every field '
|
||
'below is shown with a locally-written label instead of the '
|
||
'shared field model.'.format(exc))
|
||
known = _known_field_keys(paf)
|
||
|
||
blocks.append({'kind': 'heading', 'text': 'Structural Model', 'level': 1})
|
||
blocks.append({'kind': 'sentence', 'text':
|
||
'How this panel is held up, tied back and loaded once it is '
|
||
'standing in place, separate from its geometry and how it is '
|
||
'handled before it gets there.'})
|
||
|
||
# --------------------------------------------------------- Restraints
|
||
restraints_col = [
|
||
{'kind': 'heading', 'text': 'Restraints', 'level': 2},
|
||
{'kind': 'sentence', 'text':
|
||
'Where the panel is connected to the foundation, and then, '
|
||
'going up, where it is tied back to each floor as the '
|
||
'building is built around it.'},
|
||
]
|
||
restraints_col.append(_field(
|
||
paf, known, problems, 'restraint_foundation_connection', False,
|
||
'choice', choices=['Pin', 'Fixed', 'Roller', 'Slide'],
|
||
fallback_label='Foundation connection type',
|
||
fallback_hint='How the bottom of the panel is attached to the '
|
||
'foundation: pinned (free to rotate), fixed, on a '
|
||
'roller, or free to slide. Leave blank if this is '
|
||
'not decided yet.',
|
||
what='the foundation connection type'))
|
||
restraints_col.append(_field(
|
||
paf, known, problems, 'restraint_floor_tie', False, 'check',
|
||
fallback_label='Add a floor tie',
|
||
fallback_hint='Check this to add another restraint row above the '
|
||
'foundation for a floor tying the panel back. Leave '
|
||
'unchecked if there are no floor ties.',
|
||
what='whether an extra floor-tie restraint row is added'))
|
||
restraints_col.append({'kind': 'sentence', 'text':
|
||
"Gravity restraint means that tie also carries the panel's own "
|
||
'weight at that point; thermal restraint means it stops the '
|
||
'panel from bowing out of plane if one face gets hotter than the '
|
||
'other.'})
|
||
rrows, rstate = _restraint_rows_from(values, problems)
|
||
if rstate == 'missing':
|
||
restraints_col.append({'kind': 'sentence', 'text':
|
||
'No restraint elevations have been entered on this form yet '
|
||
'-- this is unknown, not "no restraints".'})
|
||
elif rstate == 'blank':
|
||
restraints_col.append({'kind': 'sentence', 'text':
|
||
'Design contains no restraints.'})
|
||
else:
|
||
restraints_col.append({'kind': 'table',
|
||
'headers': ['ID', 'Elevation (ft)', 'Type', 'Gravity Rest.',
|
||
'Thermal Rest.', 'Stiffness (k/in)'],
|
||
'header_groups': None, 'rows': rrows, 'row_key': None})
|
||
|
||
# ------------------------------------------------------------ Releases
|
||
releases_col = [
|
||
{'kind': 'heading', 'text': 'Releases', 'level': 2},
|
||
{'kind': 'sentence', 'text':
|
||
'Any restraint above that is deliberately freed in one '
|
||
'direction -- for example a tie that carries the weight but '
|
||
'is left free to slide sideways.'},
|
||
]
|
||
rel_state, rel_text = _optional_note(
|
||
values, 'releases',
|
||
blank_sentence='Design contains no releases.',
|
||
missing_sentence='Releases are not collected on this form yet -- '
|
||
'this is unknown, not "no releases".',
|
||
problems=problems, what='deliberately-freed restraint directions')
|
||
if rel_state == 'filled':
|
||
releases_col.append(_table_from_text(rel_text, ['ID', 'Release']))
|
||
else:
|
||
releases_col.append({'kind': 'sentence', 'text': rel_text})
|
||
|
||
blocks.append({'kind': 'columns',
|
||
'columns': [restraints_col, releases_col]})
|
||
|
||
blocks.append({'kind': 'drawing', 'source': 'elevation',
|
||
'caption': 'Restraint and support elevations',
|
||
'w': 640, 'h': 160})
|
||
|
||
blocks.append({'kind': 'rule'})
|
||
|
||
# -------------------------------------------------------------- Bearing
|
||
blocks.append({'kind': 'heading', 'text': 'Bearing for this analysis',
|
||
'level': 2})
|
||
blocks.append({'kind': 'sentence', 'text':
|
||
'Which elevations actually hold the panel up for THIS run -- the '
|
||
'analysis needs at least two, and they do not have to be every '
|
||
'restraint listed above.'})
|
||
blocks.append(_field(paf, known, problems, 'supports_ft', True, 'text',
|
||
what='the bearing positions the self-weight solve uses'))
|
||
|
||
blocks.append({'kind': 'space', 'h': 8})
|
||
|
||
# ---------------------------------------------------- Concentrated Loads
|
||
blocks.append({'kind': 'heading', 'text': 'Concentrated Loads',
|
||
'level': 2})
|
||
blocks.append({'kind': 'sentence', 'text':
|
||
'Extra point loads hung on the panel once it is standing -- a '
|
||
'ledger, a beam pocket, attached equipment -- and where along the '
|
||
'wall each one acts. A positive offset pulls toward the outside '
|
||
'face of the wall.'})
|
||
cl_state, cl_text = _optional_note(
|
||
values, 'conc_loads',
|
||
blank_sentence='No concentrated point loads entered -- design '
|
||
'assumes none.',
|
||
missing_sentence='Concentrated loads are not collected on this '
|
||
'form yet -- this is unknown, not "no point '
|
||
'loads".',
|
||
problems=problems, what='point loads once the panel is in place')
|
||
if cl_state == 'filled':
|
||
blocks.append(_table_from_text(
|
||
cl_text, ['ID', 'Elevation / offset / dead / live (as typed)']))
|
||
problems.append(
|
||
"'conc_loads' only carries elevation, offset, dead and live "
|
||
"today; Eriksson's own table also splits each point load into "
|
||
'roof, wind pressure, wind suction and seismic components -- '
|
||
'those columns have no key yet in vibe.panelanalysisform.')
|
||
else:
|
||
blocks.append({'kind': 'sentence', 'text': cl_text})
|
||
|
||
blocks.append({'kind': 'rule'})
|
||
|
||
# -------------------------------------------------------- Load Options
|
||
blocks.append({'kind': 'heading', 'text': 'Load Options', 'level': 2})
|
||
blocks.append(_field(
|
||
paf, known, problems, 'load_opt_pattern_live', False, 'check',
|
||
fallback_label='Pattern live loads for flexure',
|
||
fallback_hint='Whether live load is applied in a checkerboard '
|
||
'pattern (only some spans loaded at once) when '
|
||
'checking bending, instead of everywhere at once at '
|
||
'the same time. Leave unchecked if unsure.',
|
||
what='whether live load is patterned for the bending check'))
|
||
blocks.append(_field(
|
||
paf, known, problems, 'load_opt_selfweight_ecc_in', False, 'num',
|
||
fallback_label='Self-weight eccentricity', fallback_symbol='in',
|
||
fallback_hint="How far the panel's own weight is assumed to act "
|
||
'off its centerline. Leave blank to assume it acts '
|
||
'right on the centerline.',
|
||
what='the self-weight eccentricity'))
|
||
blocks.append(_field(
|
||
paf, known, problems, 'load_opt_offset_from', False, 'choice',
|
||
choices=['Panel CG', 'Other'],
|
||
fallback_label='Offset measured from',
|
||
fallback_hint='What point the point-load offsets above are '
|
||
"measured from. Leave blank to assume the panel's "
|
||
'own center of gravity.',
|
||
what='what point-load offsets are measured from'))
|
||
|
||
blocks.append({'kind': 'rule'})
|
||
|
||
# ------------------------------------------------------------- Wind
|
||
blocks.append({'kind': 'heading', 'text': 'Wind', 'level': 2})
|
||
blocks.append({'kind': 'sentence', 'text':
|
||
'Nothing in this tool computes a wind pressure yet -- that engine '
|
||
'is a later stage. Every field below is collected so nothing '
|
||
'typed here is lost, and every one of them is plainly not used '
|
||
'by the analysis until the engine exists.'})
|
||
wind_fields = (
|
||
('wind_speed_vult', 'num', None, 'Design wind speed', 'Vult, mph'),
|
||
('wind_risk_category', 'text', None, 'Risk category', ''),
|
||
('wind_exposure', 'text', None, 'Exposure category', ''),
|
||
('wind_enclosure', 'text', None, 'Enclosure classification', ''),
|
||
('wind_asce_version', 'text', None, 'ASCE code version', ''),
|
||
('wind_cc_chapter', 'text', None,
|
||
'Components & Cladding chapter used', ''),
|
||
('wind_mwfrs_chapter', 'text', None,
|
||
'Main wind force resisting system chapter used', ''),
|
||
('wind_pressure_psf', 'num', None,
|
||
'Wind pressure (typed directly)', 'psf'),
|
||
('wind_suction_psf', 'num', None,
|
||
'Wind suction (typed directly)', 'psf'),
|
||
)
|
||
for key, editor, choices, flabel, fsym in wind_fields:
|
||
blocks.append(_field(
|
||
paf, known, problems, key, False, editor, choices=choices,
|
||
fallback_label=flabel, fallback_symbol=fsym,
|
||
fallback_hint='Not yet used by the solve -- nothing computes '
|
||
'wind pressure yet.', what=flabel.lower()))
|
||
wz_state, wz_text = _optional_note(
|
||
values, 'wind_zone_rows',
|
||
blank_sentence='No wind pressure zones entered.',
|
||
missing_sentence='Wind pressure zones (start/end elevation, '
|
||
'pressure or suction, magnitude) are not '
|
||
'collected on this form yet -- this is unknown, '
|
||
'not "no wind zones".',
|
||
problems=problems, what='wind pressure zones along the height')
|
||
if wz_state == 'filled':
|
||
blocks.append(_table_from_text(
|
||
wz_text,
|
||
['ID', 'Start / end / type / magnitude (as typed)']))
|
||
else:
|
||
blocks.append({'kind': 'sentence', 'text': wz_text})
|
||
|
||
blocks.append({'kind': 'rule'})
|
||
|
||
# ----------------------------------------------------------- Seismic
|
||
blocks.append({'kind': 'heading', 'text': 'Seismic', 'level': 2})
|
||
blocks.append({'kind': 'sentence', 'text':
|
||
'Nothing in this tool computes a seismic force yet either -- the '
|
||
'same later-stage engine as wind. Shown here so it is not lost, '
|
||
'not used by the analysis.'})
|
||
blocks.append(_field(paf, known, problems, 'seismic_cs', False, 'num',
|
||
what='the seismic coefficient, as a percent of dead load'))
|
||
blocks.append(_field(
|
||
paf, known, problems, 'seismic_zone_location', False, 'text',
|
||
fallback_label='Seismic zone location(s)',
|
||
fallback_hint='Which part of the panel height the seismic load '
|
||
'applies over (e.g. full height). Not yet used by '
|
||
'the solve.',
|
||
what='where along the panel the seismic load applies'))
|
||
blocks.append(_field(
|
||
paf, known, problems, 'seismic_apply_to', False, 'text',
|
||
fallback_label='Apply seismic multiplier to',
|
||
fallback_hint='Which loads the seismic coefficient multiplies '
|
||
'(dead load, self weight, or both). Not yet used '
|
||
'by the solve.',
|
||
what='what the seismic coefficient is applied to'))
|
||
sz_state, sz_text = _optional_note(
|
||
values, 'seismic_zone_rows',
|
||
blank_sentence='No seismic load zones entered.',
|
||
missing_sentence='Seismic load zones (start/end position, load '
|
||
'percentage) are not collected on this form yet '
|
||
'-- this is unknown, not "no seismic zones".',
|
||
problems=problems, what='seismic load zones along the height')
|
||
if sz_state == 'filled':
|
||
blocks.append(_table_from_text(
|
||
sz_text, ['ID', 'Start / end / seismic load % (as typed)']))
|
||
else:
|
||
blocks.append({'kind': 'sentence', 'text': sz_text})
|
||
|
||
blocks.append({'kind': 'rule'})
|
||
|
||
# ------------------------------------------------------- Temperature
|
||
blocks.append({'kind': 'heading', 'text': 'Temperature Loads',
|
||
'level': 2})
|
||
blocks.append({'kind': 'sentence', 'text':
|
||
'How much hotter or colder one face of the panel gets than the '
|
||
'other, and how stiff the concrete is treated as being while it '
|
||
'moves. Not yet used by the solve.'})
|
||
temp_fields = (
|
||
('temp_ext_f', 'Exterior temperature', 'deg F'),
|
||
('temp_int_f', 'Interior temperature', 'deg F'),
|
||
('temp_elasticity_mult', 'Thermal elasticity multiplier', ''),
|
||
('temp_alpha_t', 'Coefficient of thermal expansion', u'αT × 10⁻⁶'),
|
||
)
|
||
for key, flabel, fsym in temp_fields:
|
||
blocks.append(_field(
|
||
paf, known, problems, key, False, 'num',
|
||
fallback_label=flabel, fallback_symbol=fsym,
|
||
fallback_hint='Not yet used by the solve.',
|
||
what=flabel.lower()))
|
||
|
||
blocks.append({'kind': 'rule'})
|
||
|
||
# ------------------------------------------------------------ Soil
|
||
blocks.append({'kind': 'heading', 'text': 'Soil Loads', 'level': 2})
|
||
|
||
ext_col = [{'kind': 'heading', 'text': 'Exterior Soil Loads',
|
||
'level': 3}]
|
||
es_state, es_text = _optional_note(
|
||
values, 'ext_soil_loads',
|
||
blank_sentence='Design contains no exterior soil loads.',
|
||
missing_sentence='Exterior soil loads are not collected on this '
|
||
'form yet -- this is unknown, not "no exterior '
|
||
'soil loads".',
|
||
problems=problems, what='soil pressing on the outside face')
|
||
ext_col.append({'kind': 'sentence', 'text': es_text})
|
||
if es_state == 'filled':
|
||
ext_col.append(_field(
|
||
paf, known, problems, 'ext_soil_elevation_in', False, 'num',
|
||
fallback_label='Elevation', fallback_symbol='in',
|
||
fallback_hint='Where the exterior soil load starts, measured '
|
||
'up from the bottom of the panel. Not yet used '
|
||
'by the solve.', what='exterior soil elevation'))
|
||
ext_col.append(_field(
|
||
paf, known, problems, 'ext_soil_surcharge_psf', False, 'num',
|
||
fallback_label='Surcharge', fallback_symbol='psf',
|
||
fallback_hint='How much extra pressure the soil (plus '
|
||
'anything sitting on top of it) puts on the '
|
||
'panel. Not yet used by the solve.',
|
||
what='exterior soil surcharge pressure'))
|
||
|
||
int_col = [{'kind': 'heading', 'text': 'Interior Soil Loads',
|
||
'level': 3}]
|
||
is_state, is_text = _optional_note(
|
||
values, 'int_soil_loads',
|
||
blank_sentence='Design contains no interior soil loads.',
|
||
missing_sentence='Interior soil loads are not collected on this '
|
||
'form yet -- this is unknown, not "no interior '
|
||
'soil loads".',
|
||
problems=problems, what='soil pressing on the inside face')
|
||
int_col.append({'kind': 'sentence', 'text': is_text})
|
||
if is_state == 'filled':
|
||
int_col.append(_field(
|
||
paf, known, problems, 'int_soil_elevation_in', False, 'num',
|
||
fallback_label='Elevation', fallback_symbol='in',
|
||
fallback_hint='Where the interior soil load starts, measured '
|
||
'up from the bottom of the panel. Not yet used '
|
||
'by the solve.', what='interior soil elevation'))
|
||
int_col.append(_field(
|
||
paf, known, problems, 'int_soil_surcharge_psf', False, 'num',
|
||
fallback_label='Surcharge', fallback_symbol='psf',
|
||
fallback_hint='How much extra pressure the soil (plus '
|
||
'anything sitting on top of it) puts on the '
|
||
'panel. Not yet used by the solve.',
|
||
what='interior soil surcharge pressure'))
|
||
|
||
blocks.append({'kind': 'columns', 'columns': [ext_col, int_col]})
|
||
|
||
blocks.append({'kind': 'rule'})
|
||
|
||
# ------------------------------------------------- Additional self weight
|
||
blocks.append({'kind': 'heading', 'text': 'Additional Self Weight',
|
||
'level': 2})
|
||
asw_state, asw_text = _optional_note(
|
||
values, 'addl_sw_magnitude_plf',
|
||
blank_sentence='No additional self weight entered -- the '
|
||
"analysis uses only the panel's own concrete "
|
||
'weight.',
|
||
missing_sentence='Additional self weight is not collected on '
|
||
'this form yet -- this is unknown, not "none".',
|
||
problems=problems, what='extra weight added over part of the panel')
|
||
if asw_state == 'missing':
|
||
blocks.append({'kind': 'sentence', 'text': asw_text})
|
||
else:
|
||
if asw_state == 'blank':
|
||
blocks.append({'kind': 'sentence', 'text': asw_text})
|
||
blocks.append(_field(
|
||
paf, known, problems, 'addl_sw_start_in', False, 'num',
|
||
fallback_label='Start from bottom', fallback_symbol='in',
|
||
fallback_hint='Where the extra weight starts, measured up '
|
||
'from the bottom of the panel. Not yet used by '
|
||
'the solve.', what='additional self weight start'))
|
||
blocks.append(_field(
|
||
paf, known, problems, 'addl_sw_end_in', False, 'num',
|
||
fallback_label='End from top', fallback_symbol='in',
|
||
fallback_hint='Where the extra weight ends, measured down '
|
||
'from the top of the panel. Not yet used by '
|
||
'the solve.', what='additional self weight end'))
|
||
blocks.append(_field(
|
||
paf, known, problems, 'addl_sw_magnitude_plf', False, 'num',
|
||
fallback_label='Magnitude', fallback_symbol='plf',
|
||
fallback_hint='How much extra weight per foot of height is '
|
||
'added over that stretch. Leave blank for none. '
|
||
'Not yet used by the solve.',
|
||
what='additional self weight magnitude'))
|
||
|
||
blocks.append({'kind': 'drawing', 'source': 'weight_diagram',
|
||
'caption': 'Point loads and self weight along the wall',
|
||
'w': 640, 'h': 200})
|
||
|
||
blocks.append({'kind': 'rule'})
|
||
|
||
# -------------------------------------------------------- Design Strips
|
||
blocks.append({'kind': 'heading', 'text': 'Design Strips', 'level': 2})
|
||
blocks.append({'kind': 'sentence', 'text':
|
||
'Whether the panel is analysed as one member across its full '
|
||
'width, or split into narrower strips first.'})
|
||
blocks.append(_field(
|
||
paf, known, problems, 'design_strips_method', False, 'choice',
|
||
choices=['Single Member', 'Design Strips'],
|
||
fallback_label='Analysis method',
|
||
fallback_hint='Whether the whole cross section is analysed as one '
|
||
'member, or split into design strips. Blank uses '
|
||
'the whole cross section. Not yet used by the '
|
||
'solve.', what='the design-strip analysis method'))
|
||
ds_state, ds_text = _optional_note(
|
||
values, 'design_strips_rows',
|
||
blank_sentence='No design strips are defined; the entire cross '
|
||
'section will be used.',
|
||
missing_sentence='Design strips are not collected on this form '
|
||
'yet -- this is unknown, not "no strips".',
|
||
problems=problems, what='the design strip boundaries')
|
||
if ds_state == 'filled':
|
||
blocks.append(_table_from_text(ds_text, ['ID', 'Strip (as typed)']))
|
||
else:
|
||
blocks.append({'kind': 'sentence', 'text': ds_text})
|
||
|
||
return blocks, problems
|