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