Files
willtools-demo/WillTools.extension/lib/vibe/pa_structural.py
T
will 52d94303bc
ci / test (push) Has been cancelled
WillTools: precast shop-ticket QC and panel handling tools
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.
2026-09-03 16:33:47 -05:00

667 lines
32 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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