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.
This commit is contained in:
@@ -0,0 +1,365 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Panel Analysis input form -- the Reinforcement section's block builder.
|
||||
|
||||
WHY THIS EXISTS: the Panel Analysis input form used to show reinforcement as
|
||||
one raw eccentricity number in an unlabelled box, and the owner has now said
|
||||
three times it is unreadable ("太丑了", "你这么输入用户上哪儿能看懂", and
|
||||
finally "全部给我做成普通人能看懂的样子" -- make every part of it something an
|
||||
ordinary person can understand). This module rebuilds the Reinforcement half
|
||||
of that form to read like Eriksson Wall's own printed page (strands, rebar,
|
||||
mesh, interrupts, then the drawn cross-section), while staying an honest INPUT
|
||||
form rather than a finished report: every optional block that is empty says so
|
||||
in a sentence, exactly the way Eriksson prints "Design contains no rebar" /
|
||||
"no mesh" / "no prestressing" rather than leaving a blank space that could
|
||||
mean either "nothing here" or "nobody has typed it yet".
|
||||
|
||||
`page(values, reads=None)` returns `(blocks, problems)` and never raises.
|
||||
`blocks` is a flat list of dicts drawn from a fixed, generic vocabulary (see
|
||||
the button's WPF renderer) -- 'heading', 'columns', 'field', 'table', 'kv',
|
||||
'sentence', 'drawing', 'rule', 'space'. An unrecognised `kind` renders as
|
||||
nothing, so adding a new one here without also teaching the renderer is a
|
||||
silent no-op, not a crash -- worth remembering while extending this file.
|
||||
`problems` is the sentence list the button shows above the section: every
|
||||
field key this module could not find in `vibe.panelanalysisform`, every
|
||||
strand size with no known area, and every summary quantity (cgx, ex, ey) that
|
||||
this module refuses to compute rather than invent.
|
||||
|
||||
WHAT THIS MODULE WILL NOT DO, on purpose, following `panelanalysisform`'s own
|
||||
rule for `strand_area_in2` (a strand size it does not recognise is refused BY
|
||||
NAME, never guessed, because an invented area is a fabricated capacity):
|
||||
|
||||
* A strand size with no published area never gets a made-up one -- the
|
||||
table shows the size, refuses the area, and `problems` names the size.
|
||||
* Strand horizontal position (cgx) and both eccentricities (ex, ey) are
|
||||
never printed. The per-strand `y` `_prestress_from` computes is an even
|
||||
spread across the panel WIDTH assumed for the Stage-0 solve, not a
|
||||
position the user typed or the model measured, so presenting it as cgx
|
||||
would dress up an assumption as a fact. Depth (cgy) IS printed, because
|
||||
depth-to-CG is exactly what the user types per strand group -- a
|
||||
count-weighted average of real entered numbers, not an invented one.
|
||||
* Horizontal strand spacing is not collected anywhere on this form, so no
|
||||
table here carries a "Spacing" column or any other layout that implies
|
||||
it is known -- Eriksson's own rebar table has one; this module's strand
|
||||
table deliberately does not, matching the real page 27 strand table
|
||||
(which has none either) rather than the rebar table on the same PDF.
|
||||
|
||||
WHY `strand_rows` IS READ DEFENSIVELY: a sibling agent is restructuring
|
||||
`panelanalysisform` to replace the single `ps_ecc_in` box with structured
|
||||
strand-group rows, exposed on `values['strand_rows']` as
|
||||
`[{'count', 'depth_in', 'pull_pct'}, ...]`. That key does not exist on
|
||||
`values` in every revision of that module while the restructure is in
|
||||
flight, so its absence is read as "waiting on input", never as an error --
|
||||
this module falls back to parsing the raw `ps_rows` text itself with
|
||||
`panelanalysisform.parse_strand_rows` (the same parser `validate()` uses) and
|
||||
says in `problems` that it had to.
|
||||
|
||||
No Revit import, no WPF import, no `panelsketch` import -- only
|
||||
`vibe.panelanalysisform`, for field names/labels/hints and the strand-area
|
||||
table, exactly as the rest of this input form does. Must run on IronPython
|
||||
2.7 in the button (`vibe.helpers` never reaches this module) AND on plain
|
||||
CPython under `WillTools.extension/tests/conftest.py`'s import trick: no
|
||||
f-strings, no typing, no dataclasses.
|
||||
"""
|
||||
|
||||
from vibe import panelanalysisform as pf
|
||||
|
||||
|
||||
# --------------------------------------------------------------- constants
|
||||
|
||||
# Section/field keys this module owns, all declared under the 'Reinforcement'
|
||||
# section in panelanalysisform.FIELDS. Kept as a list (not re-derived from
|
||||
# FIELDS by section name) so a key panelanalysisform drops or renames shows up
|
||||
# as an explicit MISSING in `problems` instead of just vanishing from the page.
|
||||
_STRAND_FIELD_KEYS = ('strand_rail_location', 'strand_rail_offset_in',
|
||||
'ps_dia_in', 'ps_pull_lb', 'ps_count', 'ps_loss_pct')
|
||||
_REBAR_FIELD_KEYS = ('mild_as',)
|
||||
_MESH_FIELD_KEYS = ('mesh',)
|
||||
|
||||
# panelanalysisform has no field for interrupts at all yet -- not "blank",
|
||||
# genuinely absent. Kept as its own name (rather than an empty tuple reused
|
||||
# for "no fields") so the one `problems` sentence that explains it is written
|
||||
# once, beside the constant it is about.
|
||||
_INTERRUPTS_MISSING = 'interrupts'
|
||||
|
||||
# panelanalysisform.FIELDS' 'kind' -> the generic 'editor' vocabulary this
|
||||
# page speaks. Nothing here is 'check' or 'choice' -- every Reinforcement
|
||||
# field is free text or a number.
|
||||
_EDITOR_OF_KIND = {'text': 'text', 'num': 'num', 'int': 'num'}
|
||||
|
||||
|
||||
def _field_block(key, problems):
|
||||
"""A 'field' block for a panelanalysisform key, or None + a MISSING note.
|
||||
|
||||
Reads the label/symbol/hint/required-ness straight off panelanalysisform
|
||||
so this page can never drift from the form it is rendering -- a rename in
|
||||
that module shows up here as a MISSING problem instead of a stale label.
|
||||
"""
|
||||
spec = pf._spec(key)
|
||||
if spec is None:
|
||||
problems.append(
|
||||
"field {0!r} is not declared in panelanalysisform -- this page "
|
||||
"cannot show it (MISSING)".format(key))
|
||||
return None
|
||||
editor = _EDITOR_OF_KIND.get(spec['kind'], 'text')
|
||||
return {
|
||||
'kind': 'field', 'key': key, 'label': spec['label'],
|
||||
'symbol': spec['symbol'] or None, 'hint': spec['help'] or None,
|
||||
'required': spec['required'], 'editor': editor, 'choices': None,
|
||||
}
|
||||
|
||||
|
||||
def _text_of(values, key):
|
||||
"""The current string in `values` for `key`, '' if absent/None.
|
||||
|
||||
`values` is meant to be string-valued, but this page must never raise on
|
||||
a caller's mistake -- a non-string value (an int typed straight into a
|
||||
dict in a test, a stray object) is coerced rather than crashed on.
|
||||
"""
|
||||
raw = values.get(key)
|
||||
if raw is None:
|
||||
return ''
|
||||
if not hasattr(raw, 'strip'):
|
||||
try:
|
||||
raw = str(raw)
|
||||
except Exception:
|
||||
return ''
|
||||
return raw.strip()
|
||||
|
||||
|
||||
def _num_of(values, key):
|
||||
"""(float_or_None, error_or_None) for a numeric field's current text."""
|
||||
text = _text_of(values, key)
|
||||
if not text:
|
||||
return None, None
|
||||
try:
|
||||
return float(text), None
|
||||
except (TypeError, ValueError):
|
||||
return None, 'is not a number ({0!r})'.format(text)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ strands
|
||||
|
||||
def _strand_rows_from(values, problems):
|
||||
"""[{'count', 'depth_in', 'pull_pct'}, ...] read defensively.
|
||||
|
||||
Prefers the structured `values['strand_rows']` a sibling agent is adding;
|
||||
falls back to parsing the raw `ps_rows` text with panelanalysisform's own
|
||||
parser (the same one `validate()` uses) when that key is absent, and says
|
||||
so in `problems` -- never treats the absence as an error by itself.
|
||||
Genuinely blank input (`strand_rows` absent AND `ps_rows` blank) returns
|
||||
`[]` with no problem: "nobody has typed any strand groups yet" is a real,
|
||||
quiet state, not a fault.
|
||||
"""
|
||||
raw = values.get('strand_rows')
|
||||
if isinstance(raw, list):
|
||||
rows = []
|
||||
for entry in raw:
|
||||
try:
|
||||
count = int(entry['count'])
|
||||
depth_in = float(entry['depth_in'])
|
||||
except (KeyError, TypeError, ValueError):
|
||||
problems.append(
|
||||
"a strand_rows entry from the form could not be read "
|
||||
"(missing or non-numeric count/depth_in) -- skipped it "
|
||||
"rather than guessing its numbers")
|
||||
continue
|
||||
pull_raw = entry.get('pull_pct') if hasattr(entry, 'get') else None
|
||||
pull_pct = None
|
||||
if pull_raw is not None:
|
||||
try:
|
||||
pull_pct = float(pull_raw)
|
||||
except (TypeError, ValueError):
|
||||
problems.append(
|
||||
"a strand_rows entry's pull_pct could not be read "
|
||||
"({0!r}) -- treated as not given".format(pull_raw))
|
||||
rows.append({'count': count, 'depth_in': depth_in,
|
||||
'pull_pct': pull_pct})
|
||||
return rows
|
||||
|
||||
# Fall back to the raw text field this page also renders as a 'field'
|
||||
# block below, so the user always has somewhere to type even while the
|
||||
# structured key is not yet on `values`.
|
||||
text = _text_of(values, 'ps_rows')
|
||||
if not text:
|
||||
return []
|
||||
rows, parse_problems = pf.parse_strand_rows(text)
|
||||
if parse_problems:
|
||||
problems.append(
|
||||
"structured strand_rows was not supplied by the form; tried to "
|
||||
"read the strand-group text instead and could not: " +
|
||||
'; '.join(parse_problems))
|
||||
return []
|
||||
if rows:
|
||||
problems.append(
|
||||
"structured strand_rows was not supplied by the form yet; "
|
||||
"parsed the raw strand-group text (ps_rows) instead")
|
||||
return rows
|
||||
|
||||
|
||||
def _strand_block(values, problems):
|
||||
"""The whole Strands sub-block: rail fields, then table or sentence.
|
||||
|
||||
Matches the real page's own shape: the Rail Location / Rail Offset pair
|
||||
prints unconditionally, and what follows is either the strand table or
|
||||
the single sentence "Design contains no prestressing" -- never a table
|
||||
with a header row and no data, and never a blank space.
|
||||
"""
|
||||
blocks = [{'kind': 'heading', 'text': 'Strands', 'level': 2}]
|
||||
|
||||
rail_fields = []
|
||||
for key in ('strand_rail_location', 'strand_rail_offset_in'):
|
||||
fb = _field_block(key, problems)
|
||||
if fb is not None:
|
||||
rail_fields.append([fb])
|
||||
if rail_fields:
|
||||
blocks.append({'kind': 'columns', 'columns': rail_fields})
|
||||
|
||||
rows = _strand_rows_from(values, problems)
|
||||
if not rows:
|
||||
blocks.append({'kind': 'sentence', 'text':
|
||||
'Design contains no prestressing'})
|
||||
return blocks
|
||||
|
||||
for key in ('ps_count', 'ps_dia_in', 'ps_pull_lb', 'ps_loss_pct'):
|
||||
fb = _field_block(key, problems)
|
||||
if fb is not None:
|
||||
blocks.append(fb)
|
||||
|
||||
dia_text = _text_of(values, 'ps_dia_in')
|
||||
area, area_err = pf.strand_area_in2(dia_text if dia_text else None)
|
||||
if area_err:
|
||||
problems.append(area_err)
|
||||
|
||||
pull_lb_text = _text_of(values, 'ps_pull_lb')
|
||||
|
||||
headers = ['Size', 'Area (in²)', 'Diameter', 'Quantity', '% Pull',
|
||||
'Depth to CG (in)']
|
||||
table_rows = []
|
||||
for row in rows:
|
||||
size_cell = dia_text if dia_text else '?'
|
||||
area_cell = ('%.4f' % area).rstrip('0').rstrip('.') if area is not \
|
||||
None else '?'
|
||||
if row['pull_pct'] is not None:
|
||||
pull_cell = ('%g%%' % row['pull_pct'])
|
||||
elif pull_lb_text:
|
||||
pull_cell = 'default pull ({0} lb)'.format(pull_lb_text)
|
||||
else:
|
||||
pull_cell = 'default pull (not entered)'
|
||||
depth_cell = ('%g"' % row['depth_in'])
|
||||
table_rows.append([size_cell, area_cell, size_cell,
|
||||
str(row['count']), pull_cell, depth_cell])
|
||||
blocks.append({'kind': 'table', 'headers': headers,
|
||||
'header_groups': None, 'rows': table_rows,
|
||||
'row_key': None})
|
||||
|
||||
# Strand summary. Aps and cgy are real derived numbers (a count-weighted
|
||||
# sum/average of what the user actually typed); cgx/ex/ey are refused --
|
||||
# see the module docstring for why.
|
||||
if area is not None:
|
||||
total_count = sum(r['count'] for r in rows)
|
||||
if total_count > 0:
|
||||
aps = total_count * area
|
||||
cgy = sum(r['count'] * r['depth_in'] for r in rows) / \
|
||||
float(total_count)
|
||||
kv_rows = [
|
||||
('Aps, in²', ('%.4f' % aps).rstrip('0').rstrip('.')),
|
||||
('cgy, in (depth to CG)',
|
||||
('%.4f' % cgy).rstrip('0').rstrip('.')),
|
||||
('cgx, in', 'not shown -- horizontal strand position is not '
|
||||
'collected on this form, only assumed for the '
|
||||
'solve'),
|
||||
('ex, in', 'not shown -- see cgx above'),
|
||||
('ey, in', 'not shown -- needs a defined section centroid, '
|
||||
'not derived by this page'),
|
||||
]
|
||||
blocks.append({'kind': 'kv', 'rows': kv_rows})
|
||||
problems.append(
|
||||
'strand summary: cgx and both eccentricities (ex, ey) are '
|
||||
'not computed -- horizontal strand position on this panel '
|
||||
'is an assumption made for the solve, not a real input, and '
|
||||
'printing it as cgx would dress up that assumption as a '
|
||||
'measured fact')
|
||||
else:
|
||||
problems.append(
|
||||
'strand summary (Aps, cgy) could not be computed -- the strand '
|
||||
'area is unknown (see the strand-size problem above)')
|
||||
|
||||
return blocks
|
||||
|
||||
|
||||
# --------------------------------------------------------------- rebar/mesh
|
||||
|
||||
def _rebar_block(values, problems):
|
||||
blocks = [{'kind': 'heading', 'text': 'Rebar', 'level': 2}]
|
||||
fb = _field_block('mild_as', problems)
|
||||
if fb is not None:
|
||||
blocks.append(fb)
|
||||
if not _text_of(values, 'mild_as'):
|
||||
blocks.append({'kind': 'sentence', 'text':
|
||||
'Design contains no rebar'})
|
||||
return blocks
|
||||
|
||||
|
||||
def _mesh_block(values, problems):
|
||||
blocks = [{'kind': 'heading', 'text': 'Mesh', 'level': 2}]
|
||||
fb = _field_block('mesh', problems)
|
||||
if fb is not None:
|
||||
blocks.append(fb)
|
||||
if not _text_of(values, 'mesh'):
|
||||
blocks.append({'kind': 'sentence', 'text':
|
||||
'Design contains no mesh'})
|
||||
return blocks
|
||||
|
||||
|
||||
def _interrupts_block(problems):
|
||||
"""The Interrupts sub-block -- no field exists on the form for this yet.
|
||||
|
||||
This is deliberately NOT "Design contains no interrupts": that sentence
|
||||
would assert a judged, empty answer, and nothing here has judged
|
||||
anything -- panelanalysisform simply has no key to read. "Could not
|
||||
judge" and "judged, found nothing" must never look alike (CLAUDE.md
|
||||
section 3), so this prints the honest could-not-judge sentence and names
|
||||
the gap in `problems` for whoever is meant to add the field.
|
||||
"""
|
||||
problems.append(
|
||||
"interrupts: panelanalysisform has no field for interrupts yet "
|
||||
"(MISSING) -- this page cannot say whether the design has any")
|
||||
return [
|
||||
{'kind': 'heading', 'text': 'Interrupts', 'level': 2},
|
||||
{'kind': 'sentence', 'text':
|
||||
'Could not judge -- this form does not yet collect interrupts '
|
||||
'data'},
|
||||
]
|
||||
|
||||
|
||||
# ------------------------------------------------------------------- public
|
||||
|
||||
def page(values, reads=None):
|
||||
"""(blocks, problems) for the Reinforcement section of the input form.
|
||||
|
||||
`values` is the form's current string-valued dict (plus, once the
|
||||
sibling restructure lands, a `strand_rows` list -- see the module
|
||||
docstring). `reads` is accepted for signature parity with the rest of
|
||||
this form's per-section builders; nothing under Reinforcement is
|
||||
prefilled from the model (`panelanalysisform.PREFILLABLE` does not
|
||||
include any of these keys), so it is not read here. Never raises --
|
||||
every failure becomes a sentence in `problems` instead.
|
||||
"""
|
||||
values = values or {}
|
||||
problems = []
|
||||
|
||||
blocks = [{'kind': 'heading', 'text': 'Reinforcement', 'level': 1}]
|
||||
blocks.extend(_strand_block(values, problems))
|
||||
blocks.append({'kind': 'rule'})
|
||||
blocks.extend(_rebar_block(values, problems))
|
||||
blocks.append({'kind': 'rule'})
|
||||
blocks.extend(_mesh_block(values, problems))
|
||||
blocks.append({'kind': 'rule'})
|
||||
blocks.extend(_interrupts_block(problems))
|
||||
blocks.append({'kind': 'space', 'h': 8})
|
||||
blocks.append({
|
||||
'kind': 'drawing', 'source': 'section',
|
||||
'caption': 'Reinforcement Cross-Section', 'w': 480, 'h': 360,
|
||||
})
|
||||
|
||||
return blocks, problems
|
||||
Reference in New Issue
Block a user