Files
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

1556 lines
55 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.
# pylint: disable=import-error,invalid-name,broad-except
__title__ = 'Panel Analysis (WIP)'
# Panel Analysis -- Stage 1: an INPUT CONFIRMATION form, not a lifting check.
#
# Press with one or more structural-framing panels selected. Up pops a dialog
# with one COLLAPSED section per panel; each section replicates Eriksson Wall's
# own input blocks (Design Criteria / Concrete Extents / Structural Model /
# Handling / Reinforcement), PRE-FILLED with what the model could read off the
# framing element and BLANK where it could not. The user confirms/fills the
# fields and presses that panel's own "Run Analysis" button -- which stays
# refused until the panel's required fields (geometry, materials, and at least
# two support positions) are all filled. A bottom "Analyze All" runs every
# panel whose form is complete and names the ones it skipped and why.
#
# There is NO clash detection, NO OST_SpecialtyEquipment, NO lifters, NO
# families.lifter_include and NO liftexport.export here. The owner ruled Stage
# 1 is purely an input-confirmation form (2026-09-02); auto support detection
# is a later convenience. All the gate / pre-fill / deck-build logic lives in
# vibe.panelanalysisform, host-tested off Revit; this file is the WPF shell
# and the live geometry read, which are owner-must-verify.
import io
import json
import os
import clr
from Autodesk.Revit.DB import (AssemblyInstance, BuiltInCategory, ElementId,
FilteredElementCollector)
from pyrevit import revit, script, forms
from vibe import designspec, knowledge, liftexport, ticketqc
from vibe import panelanalysisform as F
from vibe import panelsketch
from vibe.compat import as_text
doc = revit.doc
uidoc = revit.uidoc
output = script.get_output()
output.close_others()
TITLE = 'Panel Analysis'
def not_run(message, title=TITLE):
output.print_md('# {0} — NOT RUN'.format(TITLE))
output.print_md('**NOT RUN** — no analysis was produced.')
output.print_md('> {0}'.format(message))
script.exit()
# ---------------------------------------------------------- which pieces
def _is_framing(el):
try:
cat = el.Category
return (cat is not None and
cat.Id.IntegerValue == int(BuiltInCategory.OST_StructuralFraming))
except Exception:
try:
return cat.Id.Value == int(BuiltInCategory.OST_StructuralFraming)
except Exception:
return False
def _framing_of_assembly(asm):
"""The first structural-framing member of an assembly, or None."""
try:
for mid in asm.GetMemberIds():
member = doc.GetElement(mid)
if member is not None and _is_framing(member):
return member
except Exception:
pass
return None
def _resolve_framing():
"""The framing panels to build a form for, from the current selection.
A framing element is taken as itself; an assembly (or any member of one)
resolves to that assembly's framing member -- but an assembly is NOT
required, a bare framing element is enough. De-duplicated, order kept.
"""
selection = [doc.GetElement(i) for i in uidoc.Selection.GetElementIds()]
found = []
seen = set()
def add(el):
if el is None:
return
try:
key = el.Id.IntegerValue
except Exception:
key = id(el)
if key in seen:
return
seen.add(key)
found.append(el)
for el in selection:
if el is None:
continue
if _is_framing(el):
add(el)
continue
if isinstance(el, AssemblyInstance):
add(_framing_of_assembly(el))
continue
try:
owner = doc.GetElement(el.AssemblyInstanceId)
except Exception:
owner = None
if isinstance(owner, AssemblyInstance):
add(_framing_of_assembly(owner))
return found
# ------------------------------------------------------- read off the model
def _read_geometry(el):
"""{'length_ft','width_ft','thickness_in'} off the framing element, or {}.
Reuses the panel-frame + concrete-extents readers Panel Lift Check uses --
the local frame makes 'along the length' mean what it says on a rotated
wall panel -- and the same length/width/thickness axis split. Orientation
needs no assembly origin: the supports the user types define the statics,
so only the extents matter here.
"""
try:
frame = ticketqc._panel_frame(el)
if frame is None:
return {}
extents = ticketqc.concrete_extents(el, frame)
if not extents:
return {}
lo = [v * 12.0 for v in extents[0]]
hi = [v * 12.0 for v in extents[1]]
L, W, T = liftexport._axes(lo, hi)
return {
'length_ft': round((hi[L] - lo[L]) / 12.0, 4),
'width_ft': round((hi[W] - lo[W]) / 12.0, 4),
'thickness_in': round(hi[T] - lo[T], 3),
}
except Exception:
return {}
def _param_sources(el):
"""The element, then the TYPE it is an instance of.
Some roles name a TYPE parameter, not an instance one -- the plant's
thickness role is mapped to a type parameter and says so in its own
comment. `LookupParameter` on the instance simply does not see those, so
reading only the instance left `thickness_in` blank on every piece while
the value sat on the symbol the whole time (measured live 2026-09-02 on
a real 8" horizontal wall: the role resolved to its parameter name and
the instance read `None`).
Instance FIRST: where a value exists in both places the instance is the
one that was set for this piece, and a type value is the fallback the
piece inherited.
"""
out = [el]
try:
type_el = el.Document.GetElement(el.GetTypeId())
if type_el is not None:
out.append(type_el)
except Exception:
pass
return out
def _num_from_param(el, name):
for owner in _param_sources(el):
value = _num_from_one(owner, name)
if value is not None:
return value
return None
def _num_from_one(el, name):
try:
p = el.LookupParameter(name)
except Exception:
p = None
if p is None:
return None
for reader in ('AsValueString', 'AsString'):
try:
txt = getattr(p, reader)()
except Exception:
txt = None
if txt:
head = ''
for ch in str(txt).replace(',', ''):
if ch.isdigit() or ch in '.-':
head += ch
elif head:
break
try:
return float(head)
except Exception:
continue
return None
def _str_from_param(el, name):
"""One parameter's value as text, searching the instance then its type."""
for owner in _param_sources(el):
value = _str_from_one(owner, name)
if value is not None:
return value
return None
def _str_from_one(el, name):
"""`AsString` first: on a text parameter `AsValueString` can return the
formatted display of an empty value rather than nothing at all, and a
blank that looks like a name is worse than no name."""
try:
p = el.LookupParameter(name)
except Exception:
p = None
if p is None:
return None
for reader in ('AsString', 'AsValueString'):
try:
txt = getattr(p, reader)()
except Exception:
txt = None
if txt and str(txt).strip():
return str(txt).strip()
return None
def _role_str(el, know, role):
"""The first non-empty value among the parameter names a plant maps to
`role`. Empty when the plant mapped none -- unmapped is not an error
here, it is the field staying blank for the user to fill."""
if know is None:
return None
for name in knowledge.param_names(know, role):
value = _str_from_param(el, name)
if value:
return value
return None
def _read_materials(el, know):
"""Best-effort f'c / f'ci / unit weight / mark off parameters, else blank.
Material strengths are usually a per-design/sheet fact rather than a panel
parameter, so this reads whatever role the plant mapped and otherwise
leaves the field blank for the user -- the owner's rule, not an invented
default.
"""
out = {}
if know is not None:
for key, roles in (('fc_psi', ('sheet.fc',)),
('fci_psi', ('sheet.fci',)),
('unit_weight_pcf',
('panel.unit_weight', 'sheet.unit_weight'))):
for role in roles:
for name in knowledge.param_names(know, role):
v = _num_from_param(el, name)
if v is not None:
out[key] = v
break
if key in out:
break
for name in knowledge.param_names(know, 'panel.design_number') + \
knowledge.param_names(know, 'sheet.design_number'):
try:
dn = el.LookupParameter(name)
dn = dn.AsString() if dn is not None else None
except Exception:
dn = None
if dn:
out['design_number'] = dn
break
# The piece mark, through the same role the rest of the tool reads it by
# (`ticketqc._member_mark`), NOT the built-in `Mark` alone. On this
# plant's template the built-in is empty and the mark lives in the
# parameter the plant maps to `member.mark` -- read only the built-in and
# every MC panel comes up nameless, which is what put `panel 1 (id
# 123456)` in the form's header where `MWH116` belonged.
mark = _role_str(el, know, 'member.mark') or _str_from_param(el, 'Mark')
if mark:
out['mark'] = mark
# A CONTROL NUMBER is the second name a piece has: the mark says which
# design it is, the control number says which physical piece. A plant
# that records one maps `member.control_number`; one that does not gets
# no parenthetical rather than a guessed one. Suppressed when it is the
# same string as the mark -- a plant whose mark parameter IS its control
# number (this one maps `member.mark` to the plant's mark parameter) should see the
# name once, not printed twice with a bracket round the copy.
control = _role_str(el, know, 'member.control_number')
if control and control != mark:
out['control_number'] = control
return out
def _reads_for(el, index, know):
reads = {}
reads.update(_read_geometry(el))
reads.update(_read_materials(el, know))
if not reads.get('mark'):
# Nothing named this piece. An element id is a poor name but it is a
# TRUE one, and it is the only handle that still points at the right
# piece when two panels are equally nameless -- so it stays as the
# last resort, and the header says outright that no mark was read
# rather than letting a number look like a mark.
try:
reads['mark'] = 'unnamed panel {0} (element id {1})'.format(
index + 1, el.Id.IntegerValue)
except Exception:
reads['mark'] = 'unnamed panel {0}'.format(index + 1)
reads.setdefault('section_type', 'Solid')
return reads
def panel_title(reads):
"""The line that heads one panel's section: `MARK (CONTROL)`.
Kept apart from `reads['mark']`, which names the run's output files and
must stay a single stable token.
"""
mark = reads.get('mark') or ''
control = reads.get('control_number')
if mark and control:
return u'{0} ({1})'.format(mark, control)
return mark
# ---------------------------------------------------------------- running
REPO_ROOT = os.path.dirname(os.path.dirname(liftexport.panellift_src()))
PANELANALYSIS_PY = os.path.join(REPO_ROOT, 'tools', 'panelanalysis.py')
def _runner_script(exe, script_path, job, out, log):
return (u'@echo off\r\n'
u'chcp 65001 >nul\r\n'
u'set "PYTHONIOENCODING=utf-8"\r\n'
u'"{exe}" "{script}" "{job}" -o "{out}" > "{log}" 2>&1\r\n'
u'exit /b %ERRORLEVEL%\r\n').format(
exe=exe, script=script_path, job=job, out=out, log=log)
def run_panelanalysis(exe, script_path, job, out, timeout_ms=120000):
"""(exit_code, log_text). One-line cmd wrapper redirecting to a file --
reading one pipe while the child blocks on the other is a deadlock, not a
slow run (see liftexport.run()'s docstring for the incident)."""
from System.Diagnostics import Process, ProcessStartInfo
base = os.path.splitext(out)[0]
log_path = base + '.log'
runner = base + '.run.cmd'
with io.open(runner, 'w', encoding='utf-8') as fh:
fh.write(_runner_script(exe, script_path, job, out, log_path))
info = ProcessStartInfo()
info.FileName = 'cmd.exe'
info.Arguments = '/c "{0}"'.format(runner)
info.UseShellExecute = False
info.CreateNoWindow = True
process = Process.Start(info)
waited = 0
while not process.WaitForExit(250):
waited += 250
if waited >= timeout_ms:
try:
process.Kill()
except Exception:
pass
return -1, ('panel analysis did not finish in {0} seconds'
.format(timeout_ms // 1000))
def _read(path):
try:
with io.open(path, 'r', encoding='utf-8', errors='replace') as fh:
return fh.read()
except Exception:
return ''
log_text = _read(log_path)
for path in (runner, log_path):
try:
os.remove(path)
except Exception:
pass
return process.ExitCode, log_text
def _safe_name(mark):
out = ''
for ch in str(mark):
out += ch if (ch.isalnum() or ch in '-_.') else '_'
return out or 'panel'
def _run_one(mark, deck):
"""(ok, message). Builds the deck file, solves it, opens the page."""
python_exe = liftexport.find_python(KNOW)
if not python_exe:
return False, ('CPython 3.9+ was not found. The page renders outside '
'Revit; install Python or set handling.python_exe in '
'the knowledge file.')
if not os.path.isfile(PANELANALYSIS_PY):
return False, 'tools/panelanalysis.py was not found at {0}.'.format(
PANELANALYSIS_PY)
start_dir = designspec.sub_dir(doc, 'panelanalysis')
stem = _safe_name(mark)
job_path = os.path.join(start_dir, '{0}.pa.json'.format(stem))
out_path = os.path.join(start_dir, '{0}.panelanalysis.html'.format(stem))
blob = json.dumps(deck, indent=1, sort_keys=True)
with io.open(job_path, 'w', encoding='utf-8') as fh:
fh.write(as_text(blob))
code, log_text = run_panelanalysis(python_exe, PANELANALYSIS_PY, job_path,
out_path)
if code != 0 or not os.path.isfile(out_path):
return False, ('the page could not be rendered (exit {0}).\n\n{1}\n\n'
'Deck kept at:\n{2}'.format(code, log_text or 'no '
'output', job_path))
liftexport.open_file(out_path)
return True, out_path
# ------------------------------------------------------------------- WPF
clr.AddReference('PresentationFramework')
clr.AddReference('PresentationCore')
clr.AddReference('WindowsBase')
from System.Windows import (Window, Thickness, FontWeights, FontStyles,
TextWrapping, TextDecorations,
SizeToContent, WindowStartupLocation,
HorizontalAlignment, VerticalAlignment)
from System.Windows.Controls import (StackPanel, ScrollViewer, Expander,
TextBox, TextBlock, Button, Border,
Orientation, ScrollBarVisibility, Canvas,
Grid, ColumnDefinition, RowDefinition,
TabControl, TabItem, CheckBox, ComboBox)
from System.Windows.Controls import Label as WpfLabel
from System.Windows.Documents import Run
from System.Windows.Media import Brushes, RotateTransform
from System.Windows.Shapes import Line, Polygon, Rectangle, Ellipse
from System.Windows import Point, BaselineAlignment, GridLength, GridUnitType
from System.Windows.Media import PointCollection
def _label_block(text, bold=False, size=12, brush=None):
tb = TextBlock()
tb.Text = text
tb.TextWrapping = TextWrapping.Wrap
tb.FontSize = size
if bold:
tb.FontWeight = FontWeights.Bold
if brush is not None:
tb.Foreground = brush
tb.Margin = Thickness(0, 2, 0, 2)
return tb
# A prime is the only thing in these labels that a subscript follows, so the
# rule is narrow enough to be safe: f′c, f′ci. Everything else that needs a
# subscript (Aₛ, Cₛ, fₚᵤ) carries a real Unicode one in the label itself.
_PRIME = u'′'
def _math_block(text, bold=False, size=12, brush=None):
"""A label whose primed symbols render as real notation.
`f′ci` becomes an f, a prime, and a SUBSCRIPT ci -- three inline Runs, not
a string that hopes the reader knows what `sqrt f'ci` meant. WPF does the
baseline shift; nothing here draws glyphs.
"""
tb = TextBlock()
tb.TextWrapping = TextWrapping.Wrap
tb.FontSize = size
if bold:
tb.FontWeight = FontWeights.Bold
if brush is not None:
tb.Foreground = brush
tb.Margin = Thickness(0, 2, 0, 2)
if _PRIME not in text:
tb.Text = text
return tb
i = 0
while i < len(text):
j = text.find(_PRIME, i)
if j < 0:
tb.Inlines.Add(Run(text[i:]))
break
tb.Inlines.Add(Run(text[i:j + 1])) # up to and incl. the prime
k = j + 1
while k < len(text) and text[k].isalpha() and text[k].islower():
k += 1
if k > j + 1:
sub = Run(text[j + 1:k])
sub.BaselineAlignment = BaselineAlignment.Subscript
sub.FontSize = max(7, size - 3)
tb.Inlines.Add(sub)
i = k
return tb
# ------------------------------------------------------------- the drawing
#
# `vibe.panelsketch` is Revit-free and host-tested; every entry point in it
# (`sketch`, `elevation`, `plan`, `section`, `weight_diagram`, and whatever a
# sibling agent adds after this lands) returns the SAME flat primitive
# vocabulary -- rect / line / support / text / marker / swatch / dot, plain
# dicts, pixel coordinates. This is the only place that vocabulary gets
# turned into WPF shapes, so a new `source` needs no new code here as long
# as it stays inside that vocabulary; a `kind` this dict has never seen is
# simply skipped (the drawing degrades one primitive short, not to a crash --
# unlike a block `kind`, an unrecognised drawing PRIMITIVE has no owner-
# visible obligation of its own, and `problems` already carries the sentence
# that explains what is missing).
SKETCH_W = 380
SKETCH_H = 190
_ROLE_BRUSH = {
'panel': Brushes.SteelBlue, 'panel-plan': Brushes.SteelBlue,
'panel-side': Brushes.SteelBlue, 'panel-section': Brushes.SteelBlue,
'panel-crosssection': Brushes.SteelBlue,
'panel-elevation-thin': Brushes.SteelBlue,
'support': Brushes.Firebrick, 'support-label': Brushes.Firebrick,
'support-fraction': Brushes.Firebrick,
'dimension': Brushes.DimGray, 'annotation': Brushes.DimGray,
'openings-note': Brushes.DarkGoldenrod, 'note': Brushes.DarkGoldenrod,
'lifters-note': Brushes.DarkGoldenrod,
'title': Brushes.Black, 'caption': Brushes.Black,
'opening': Brushes.DarkGoldenrod, 'opening-label': Brushes.DarkGoldenrod,
'reveal': Brushes.DarkOrange, 'reveal-label': Brushes.DarkOrange,
'solid-zone': Brushes.DimGray, 'solid-zone-label': Brushes.DimGray,
'section-property': Brushes.Black,
'legend-heading': Brushes.Black, 'legend-glyph': Brushes.DimGray,
'legend-fill-label': Brushes.DimGray,
'strand': Brushes.SteelBlue, 'strand-label': Brushes.Black,
'strand-line': Brushes.SteelBlue,
'centroid': Brushes.Firebrick, 'cg': Brushes.Firebrick,
'cg-lead': Brushes.Firebrick,
'moment-arm': Brushes.DarkGoldenrod,
'moment-arm-label': Brushes.DarkGoldenrod,
'beam': Brushes.Black,
}
_FILL_BRUSH = {
'Not Specified': Brushes.Gainsboro,
'Stripping': Brushes.LightSkyBlue,
'Erection': Brushes.LightGreen,
'Stripping + Erection': Brushes.Khaki,
'Transportation': Brushes.LightSalmon,
}
def _role_brush(role):
return _ROLE_BRUSH.get(role, Brushes.DimGray)
def _sketch_text(prim):
tb = TextBlock()
tb.Text = prim.get('text', '')
tb.FontSize = 10
tb.Foreground = _role_brush(prim.get('role'))
x = prim.get('x', 0)
if prim.get('anchor') == 'middle':
# Estimated, not measured: a Measure() pass here would need a layout
# that does not exist yet at build time, and half a character of drift
# on a label is invisible where a crash is not.
x -= len(tb.Text) * 2.6
if prim.get('orientation') == 'vertical':
tb.RenderTransform = RotateTransform(-90)
Canvas.SetLeft(tb, x)
Canvas.SetTop(tb, prim.get('y', 0))
return tb
def _draw_primitive(canvas, prim):
"""One `panelsketch` primitive -> WPF shape, appended onto `canvas`.
A `kind` this function does not recognise is skipped, never raised on --
a future primitive kind lands here the day this function is taught it,
and until then the rest of the drawing still renders.
"""
kind = prim.get('kind')
role = prim.get('role')
flagged = bool(prim.get('flagged'))
brush = Brushes.Red if flagged else _role_brush(role)
if kind == 'rect':
r = Rectangle()
r.Width = max(1, prim.get('w', 1))
r.Height = max(1, prim.get('h', 1))
r.Stroke = brush
r.StrokeThickness = 1.5
r.Fill = (Brushes.Transparent
if role in ('opening', 'reveal', 'solid-zone')
else Brushes.AliceBlue)
Canvas.SetLeft(r, prim.get('x', 0))
Canvas.SetTop(r, prim.get('y', 0))
canvas.Children.Add(r)
elif kind == 'line':
ln = Line()
ln.X1, ln.Y1 = prim.get('x1', 0), prim.get('y1', 0)
ln.X2, ln.Y2 = prim.get('x2', 0), prim.get('y2', 0)
ln.Stroke = brush
ln.StrokeThickness = 1.0
canvas.Children.Add(ln)
elif kind == 'support':
# The support triangle points UP at the soffit it holds, and a
# support the user typed outside the panel is drawn hollow at the
# end it was clamped to -- present and visibly wrong, never gone.
x, y = prim.get('x', 0), prim.get('y', 0)
tri = Polygon()
pts = PointCollection()
pts.Add(Point(x, y))
pts.Add(Point(x - 6, y + 11))
pts.Add(Point(x + 6, y + 11))
tri.Points = pts
tri.Stroke = brush
tri.StrokeThickness = 1.5
tri.Fill = Brushes.Transparent if flagged else brush
canvas.Children.Add(tri)
elif kind == 'text':
canvas.Children.Add(_sketch_text(prim))
elif kind == 'marker':
x, y = prim.get('x', 0), prim.get('y', 0)
glyph = prim.get('glyph')
if glyph == 'diamond':
poly = Polygon()
pts = PointCollection()
for dx, dy in ((0, -6), (6, 0), (0, 6), (-6, 0)):
pts.Add(Point(x + dx, y + dy))
poly.Points = pts
poly.Stroke = brush
poly.StrokeThickness = 1.5
poly.Fill = brush
canvas.Children.Add(poly)
else: # 'cross', or unnamed
for x1, y1, x2, y2 in ((x - 6, y, x + 6, y),
(x, y - 6, x, y + 6)):
ln = Line()
ln.X1, ln.Y1, ln.X2, ln.Y2 = x1, y1, x2, y2
ln.Stroke = brush
ln.StrokeThickness = 2.0
canvas.Children.Add(ln)
elif kind == 'swatch':
rect = Rectangle()
rect.Width = 12
rect.Height = 12
rect.Stroke = Brushes.Gray
rect.StrokeThickness = 0.5
rect.Fill = _FILL_BRUSH.get(prim.get('fill'), Brushes.Gainsboro)
Canvas.SetLeft(rect, prim.get('x', 0))
Canvas.SetTop(rect, prim.get('y', 0))
canvas.Children.Add(rect)
elif kind == 'dot':
dot = Ellipse()
dot.Width = 5
dot.Height = 5
dot.Stroke = brush
dot.StrokeThickness = 1.0
dot.Fill = brush
Canvas.SetLeft(dot, prim.get('x', 0) - 2.5)
Canvas.SetTop(dot, prim.get('y', 0) - 2.5)
canvas.Children.Add(dot)
# else: an unrecognised primitive kind. Skipped, not raised on.
def _drawing_canvas(source, values, w, h):
"""(Border-wrapped Canvas, problems) for one `drawing` block.
`source` names an entry point in `vibe.panelsketch`; an unknown name, or
one that raises, becomes a visible note in place of the picture -- never
a crash, never a blank box indistinguishable from an empty drawing.
"""
canvas = Canvas()
canvas.Width = w
canvas.Height = h
canvas.Background = Brushes.WhiteSmoke
fn = getattr(panelsketch, source, None) if source else None
if fn is None:
primitives, problems = [], [
"drawing source '{0}' is not available.".format(source)]
else:
try:
primitives, problems = fn(values, w, h)
except Exception as exc: # never take the form down
primitives, problems = [], ['drawing failed: {0}'.format(exc)]
for prim in primitives:
try:
_draw_primitive(canvas, prim)
except Exception:
pass # one bad primitive is never worth the whole page
for n, problem in enumerate(problems[:4]):
note = TextBlock()
note.Text = u'• ' + problem
note.FontSize = 10
note.Foreground = Brushes.Firebrick
note.TextWrapping = TextWrapping.Wrap
note.Width = max(w - 12, 40)
Canvas.SetLeft(note, 6)
Canvas.SetTop(note, 6 + n * 24)
canvas.Children.Add(note)
border = Border()
border.BorderBrush = Brushes.Gainsboro
border.BorderThickness = Thickness(1)
border.Margin = Thickness(0, 4, 0, 4)
border.Child = canvas
return border
# --------------------------------------------------------------- the chrome
#
# Eriksson prints one BLOCK per idea, titled, with its inputs in two columns
# under the title. A person reading it never has to hold fifty rows in their
# head at once, which is exactly what this form used to ask of them (the
# owner, twice: "太丑了", "你这么输入用户上哪儿能看懂"). These three widgets
# are the whole of the answer: a titled box, a two-column row, and a plain
# sentence under a field saying what it is for.
_INK = Brushes.Black
_QUIET = Brushes.Gray
_RULE = Brushes.Gainsboro
def _hint_block(text):
"""The plain sentence under a field. Grey, small, and never a warning --
a warning that looks like help is read as neither."""
tb = _math_block(text, size=10, brush=_QUIET)
tb.Margin = Thickness(0, 0, 0, 4)
return tb
LABEL_COL = 210
SYMBOL_COL = 92
EDITOR_COL = 150
def _two_col_row(label_widget, editor, symbol=None, hint=None):
"""One input, laid out `name | symbol | box` with the plain-English name
first and the notation BESIDE it rather than instead of it.
The symbol column is what lets a label say `Tension allowed while
handling` and still show `n·√f′ci` to the engineer who wants it. Neither
reader has to decode the other's version.
"""
grid = Grid()
for width in (LABEL_COL, SYMBOL_COL, EDITOR_COL):
col = ColumnDefinition()
col.Width = GridLength(width)
grid.ColumnDefinitions.Add(col)
grid.RowDefinitions.Add(RowDefinition())
if hint:
grid.RowDefinitions.Add(RowDefinition())
label_widget.Margin = Thickness(0, 2, 8, 0)
Grid.SetRow(label_widget, 0)
Grid.SetColumn(label_widget, 0)
grid.Children.Add(label_widget)
sym = _math_block(symbol or '', size=11, brush=_QUIET)
sym.Margin = Thickness(0, 2, 8, 0)
Grid.SetRow(sym, 0)
Grid.SetColumn(sym, 1)
grid.Children.Add(sym)
Grid.SetRow(editor, 0)
Grid.SetColumn(editor, 2)
grid.Children.Add(editor)
if hint:
note = _hint_block(hint)
Grid.SetRow(note, 1)
Grid.SetColumn(note, 0)
Grid.SetColumnSpan(note, 3)
grid.Children.Add(note)
grid.Margin = Thickness(0, 1, 0, 1)
return grid
def _tab(title, child):
item = TabItem()
item.Header = title
scroller = ScrollViewer()
scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto
scroller.Content = child
item.Content = scroller
return item
# ------------------------------------------------------- the generic renderer
#
# Everything below turns the block vocabulary five sibling agents build
# against (pa_criteria / pa_extents / pa_structural / pa_handling /
# pa_reinforcement, each a `page(values, reads=None) -> (blocks, problems)`)
# into WPF. This is the ONLY place that vocabulary is interpreted -- nothing
# any of those five modules built is visible to the owner until it passes
# through here. An unknown block `kind` renders as a visible grey note naming
# it (never vanishes); a block that raises while rendering is caught right
# where it is dispatched and becomes a small red note instead of taking the
# whole tab down.
SECTION_MODULES = (
('Design Criteria', 'pa_criteria'),
('Concrete Extents', 'pa_extents'),
('Structural Model', 'pa_structural'),
('Handling', 'pa_handling'),
('Reinforcement', 'pa_reinforcement'),
)
def _load_section_module(modname):
"""(module, None) or (None, exception) -- never raises.
A sibling agent's file can be mid-edit (syntax error) or simply not
landed yet (ImportError); both degrade the same way, to a tab that names
the section and says why it is not available, never a crash that takes
every OTHER section down with it.
"""
try:
mod = __import__('vibe.' + modname, fromlist=[modname])
return mod, None
except Exception as exc:
return None, exc
def _missing_section_note(title, modname, reason):
box = StackPanel()
box.Margin = Thickness(4)
msg = (u"The '{0}' section is not available yet -- vibe.{1} could not "
u"be loaded ({2}). Its inputs cannot be shown or edited on this "
u"panel until it lands; nothing else on this form is affected."
).format(title, modname, reason)
tb = _math_block(msg, size=11, brush=Brushes.Firebrick)
tb.TextWrapping = TextWrapping.Wrap
box.Children.Add(tb)
return box
def _grid_star_columns(grid, n):
for _ in range(n):
col = ColumnDefinition()
col.Width = GridLength(1, GridUnitType.Star)
grid.ColumnDefinitions.Add(col)
def _cell_border(child, header=False, zebra=False):
b = Border()
b.BorderBrush = _RULE
b.BorderThickness = Thickness(0.6)
b.Padding = Thickness(4, 2, 4, 2)
if header:
b.Background = Brushes.WhiteSmoke
elif zebra:
b.Background = Brushes.GhostWhite
else:
b.Background = Brushes.White
b.Child = child
return b
def _grid_text_cell(text, bold=False, header=False, zebra=False):
tb = _math_block(u'' if text is None else str(text), bold=bold, size=11)
tb.TextWrapping = TextWrapping.Wrap
return _cell_border(tb, header=header, zebra=zebra)
def _em_dash_cell(zebra=False):
"""A matrix cell that does NOT apply to this stage -- an em dash, never
blank. Blank means 'applies, not filled in'; this means 'does not apply'
-- the two must never look the same."""
tb = TextBlock()
tb.Text = u'—'
tb.HorizontalAlignment = HorizontalAlignment.Center
tb.Foreground = _QUIET
return _cell_border(tb, zebra=zebra)
def _render_heading(block):
level = block.get('level') or 1
text = block.get('text', '')
panel = StackPanel()
if level == 1:
head = _math_block(text, bold=True, size=15)
panel.Children.Add(head)
rule = Border()
rule.BorderBrush = _RULE
rule.BorderThickness = Thickness(0, 0, 0, 2)
rule.Margin = Thickness(0, 2, 0, 8)
panel.Children.Add(rule)
elif level == 2:
head = _math_block(text, bold=True, size=13)
panel.Children.Add(head)
rule = Border()
rule.BorderBrush = _RULE
rule.BorderThickness = Thickness(0, 0, 0, 1)
rule.Width = 170
rule.HorizontalAlignment = HorizontalAlignment.Left
rule.Margin = Thickness(0, 2, 0, 6)
panel.Children.Add(rule)
else:
head = _math_block(text, bold=True, size=11.5)
head.Margin = Thickness(0, 4, 0, 2)
panel.Children.Add(head)
return panel
def _render_columns(block, form):
cols = block.get('columns') or []
if not cols:
return StackPanel()
grid = Grid()
_grid_star_columns(grid, len(cols))
grid.RowDefinitions.Add(RowDefinition())
for i, col_blocks in enumerate(cols):
panel = StackPanel()
panel.Margin = Thickness(6, 0, 6, 0)
panel.VerticalAlignment = VerticalAlignment.Top
for b in (col_blocks or []):
w = _render_block(b, form)
if w is not None:
panel.Children.Add(w)
Grid.SetColumn(panel, i)
Grid.SetRow(panel, 0)
grid.Children.Add(panel)
return grid
def _render_field(block, form):
key = block.get('key')
label = block.get('label') or key or ''
if block.get('required'):
label = label + u' *'
lab = _math_block(label, size=11)
editor = form.make_editor(block.get('editor'), key, block.get('choices'),
width=EDITOR_COL)
return _two_col_row(lab, editor, symbol=block.get('symbol'),
hint=block.get('hint'))
def _render_kv(block):
rows = block.get('rows') or []
grid = Grid()
c0 = ColumnDefinition()
c0.Width = GridLength(200)
grid.ColumnDefinitions.Add(c0)
c1 = ColumnDefinition()
c1.Width = GridLength(1, GridUnitType.Star)
grid.ColumnDefinitions.Add(c1)
for _ in rows:
grid.RowDefinitions.Add(RowDefinition())
for i, pair in enumerate(rows):
label, value = (pair[0], pair[1]) if len(pair) >= 2 else (pair[0], '')
lab = _math_block(str(label), size=11, brush=_QUIET)
lab.Margin = Thickness(0, 1, 8, 1)
val = _math_block(u'' if value is None else str(value), size=11)
val.Margin = Thickness(0, 1, 0, 1)
Grid.SetRow(lab, i)
Grid.SetColumn(lab, 0)
grid.Children.Add(lab)
Grid.SetRow(val, i)
Grid.SetColumn(val, 1)
grid.Children.Add(val)
return grid
def _render_sentence(block):
tb = _math_block(block.get('text', ''), size=11, brush=_QUIET)
tb.FontStyle = FontStyles.Italic
tb.Margin = Thickness(0, 2, 0, 6)
return tb
def _render_table(block, form):
headers = block.get('headers') or []
header_groups = block.get('header_groups')
rows = block.get('rows') or []
ncols = len(headers)
if ncols == 0:
return StackPanel()
grid = Grid()
for _ in range(ncols):
grid.ColumnDefinitions.Add(ColumnDefinition())
header_rows = 2 if header_groups else 1
for _ in range(header_rows + len(rows)):
grid.RowDefinitions.Add(RowDefinition())
r = 0
if header_groups:
c = 0
for label, span in header_groups:
span = max(1, min(span, ncols - c))
cell = _grid_text_cell(label, bold=True, header=True)
Grid.SetRow(cell, r)
Grid.SetColumn(cell, c)
Grid.SetColumnSpan(cell, span)
grid.Children.Add(cell)
c += span
r += 1
for c, h in enumerate(headers):
cell = _grid_text_cell(h, bold=True, header=True)
Grid.SetRow(cell, r)
Grid.SetColumn(cell, c)
grid.Children.Add(cell)
r += 1
for ridx, row in enumerate(rows):
zebra = (ridx % 2 == 1)
for cidx in range(ncols):
value = row[cidx] if cidx < len(row) else ''
if isinstance(value, dict) and value.get('editor') == 'check':
widget = form.make_editor('check', value.get('key'))
widget.HorizontalAlignment = HorizontalAlignment.Center
cell = _cell_border(widget, zebra=zebra)
else:
cell = _grid_text_cell(value, zebra=zebra)
Grid.SetRow(cell, r)
Grid.SetColumn(cell, cidx)
grid.Children.Add(cell)
r += 1
return grid
def _render_matrix(block, form):
columns = block.get('columns') or []
rows = block.get('rows') or []
if not columns or not rows:
return StackPanel()
grid = Grid()
c0 = ColumnDefinition()
c0.Width = GridLength(220)
grid.ColumnDefinitions.Add(c0)
for _ in columns:
col = ColumnDefinition()
col.Width = GridLength(1, GridUnitType.Star)
grid.ColumnDefinitions.Add(col)
for _ in range(1 + len(rows)):
grid.RowDefinitions.Add(RowDefinition())
hdr = _grid_text_cell('', bold=True, header=True)
Grid.SetRow(hdr, 0)
Grid.SetColumn(hdr, 0)
grid.Children.Add(hdr)
for ci, colname in enumerate(columns):
cell = _grid_text_cell(colname, bold=True, header=True)
Grid.SetRow(cell, 0)
Grid.SetColumn(cell, ci + 1)
grid.Children.Add(cell)
for ridx, row in enumerate(rows):
r = ridx + 1
zebra = (ridx % 2 == 1)
lbl_panel = StackPanel()
lbl_panel.Children.Add(_math_block(row.get('label', ''), size=11))
if row.get('hint'):
lbl_panel.Children.Add(_hint_block(row['hint']))
lbl_cell = _cell_border(lbl_panel, zebra=zebra)
Grid.SetRow(lbl_cell, r)
Grid.SetColumn(lbl_cell, 0)
grid.Children.Add(lbl_cell)
cells = row.get('cells') or []
for ci in range(len(columns)):
cellval = cells[ci] if ci < len(cells) else None
if cellval is None:
wrapped = _cell_border(TextBlock(), zebra=zebra)
elif isinstance(cellval, dict) and cellval.get('na'):
wrapped = _em_dash_cell(zebra=zebra)
elif isinstance(cellval, dict):
widget = form.make_editor(cellval.get('editor'),
cellval.get('key'),
cellval.get('choices'))
wrapped = _cell_border(widget, zebra=zebra)
else:
wrapped = _grid_text_cell(cellval, zebra=zebra)
Grid.SetRow(wrapped, r)
Grid.SetColumn(wrapped, ci + 1)
grid.Children.Add(wrapped)
return grid
def _render_legend(block):
groups = block.get('groups') or []
panel = StackPanel()
for name, items in groups:
panel.Children.Add(_math_block(name, bold=True, size=11))
for symbol, desc in (items or []):
row = StackPanel()
row.Orientation = Orientation.Horizontal
row.Margin = Thickness(8, 0, 0, 2)
sym = _math_block(symbol, size=11, brush=_QUIET)
sym.Width = 60
row.Children.Add(sym)
row.Children.Add(_math_block(desc, size=11))
panel.Children.Add(row)
return panel
def _render_rule():
b = Border()
b.BorderBrush = _RULE
b.BorderThickness = Thickness(0, 0, 0, 1)
b.Margin = Thickness(0, 6, 0, 6)
return b
def _render_space(block):
b = Border()
b.Height = max(2, (block.get('h') or 1) * 6)
return b
def _render_unknown(block):
"""A block `kind` this renderer has never seen. Named and visible,
never silently dropped -- a block that vanishes is a section of the
form nobody knows is missing."""
tb = TextBlock()
tb.Text = u"[unrendered block: kind={0!r}]".format(block.get('kind')
if isinstance(block, dict)
else block)
tb.FontStyle = FontStyles.Italic
tb.Foreground = Brushes.Gray
tb.TextWrapping = TextWrapping.Wrap
tb.Margin = Thickness(0, 2, 0, 2)
return tb
def _render_block(block, form):
"""One block dict -> one WPF element. Never raises -- a block that fails
to render becomes a small red note in its own place, and the rest of the
tab keeps building."""
if not isinstance(block, dict):
return _render_unknown(block)
kind = block.get('kind')
try:
if kind == 'heading':
return _render_heading(block)
if kind == 'columns':
return _render_columns(block, form)
if kind == 'field':
return _render_field(block, form)
if kind == 'kv':
return _render_kv(block)
if kind == 'sentence':
return _render_sentence(block)
if kind == 'table':
return _render_table(block, form)
if kind == 'matrix':
return _render_matrix(block, form)
if kind == 'legend':
return _render_legend(block)
if kind == 'drawing':
return form.drawing_widget(block)
if kind == 'rule':
return _render_rule()
if kind == 'space':
return _render_space(block)
return _render_unknown(block)
except Exception as exc:
note = TextBlock()
note.Text = u'[block failed to render: kind={0} -- {1}]'.format(
kind, exc)
note.Foreground = Brushes.Firebrick
note.TextWrapping = TextWrapping.Wrap
note.FontSize = 10
return note
class PanelForm(object):
"""One selected panel's collapsed section: a tab per Eriksson section,
built from the five sibling agents' `page()` block lists, plus that
panel's own Run button.
"""
def __init__(self, el, index, know):
self.el = el
self.index = index
self.reads = _reads_for(el, index, know)
self.values0 = F.prefill(self.reads)
self.mark = self.reads.get('mark') or 'panel {0}'.format(index + 1)
self.title = panel_title(self.reads) or self.mark
self.editors = {} # key -> (widget, 'text'|'check'|'choice')
self.drawings = [] # [(container, source, caption, w, h)]
self.run_btn = None
self.status = None
self.gcpi_label = None
self.tabctrl = None
self.expander = self._build()
# -- values ------------------------------------------------------------
def current_values(self):
vals = dict(self.values0)
for key, pair in self.editors.items():
widget, kind = pair
if kind == 'check':
vals[key] = 'yes' if widget.IsChecked else ''
else:
try:
vals[key] = widget.Text or ''
except Exception:
vals[key] = ''
return vals
# -- editors -------------------------------------------------------------
def make_editor(self, kind, key, choices=None, width=None):
"""One live input widget for `key`, registered in `self.editors` so
`current_values()` / the gate / the drawings can read it back.
`check` -> CheckBox, `choice` -> an EDITABLE ComboBox over `choices`
that never pre-selects a value the user did not choose (its `.Text`
is set, never its `SelectedItem`), everything else -> TextBox.
"""
initial = self.values0.get(key, '') if key else ''
if kind == 'check':
w = CheckBox()
w.IsChecked = (initial or '').strip().lower() in (
'yes', 'y', 'true', '1', 'on')
w.VerticalAlignment = VerticalAlignment.Center
w.Checked += self._on_change
w.Unchecked += self._on_change
ekind = 'check'
elif kind == 'choice':
w = ComboBox()
w.IsEditable = True
for c in (choices or []):
w.Items.Add(c)
w.Text = initial or ''
if width:
w.Width = width
w.SelectionChanged += self._on_change
w.LostFocus += self._on_change
ekind = 'choice'
else: # 'text' / 'num' / other
w = TextBox()
w.Text = initial or ''
if width:
w.Width = width
w.TextChanged += self._on_change
ekind = 'text'
if key:
self.editors[key] = (w, ekind)
if key == 'wind_enclosure':
# Owner's requirement: choosing an enclosure shows its matching
# GCpi (ASCE Table 26.13-1). Pure lookup in the form module --
# windload itself is CPython and cannot be imported here. The
# readout sits beside the enclosure editor and `refresh_gate`
# refreshes it on every change via `_update_gcpi`.
self.gcpi_label = _label_block('', size=11, brush=Brushes.Gray)
self.gcpi_label.Width = 220
self.gcpi_label.Margin = Thickness(8, 0, 0, 0)
holder = StackPanel()
holder.Orientation = Orientation.Horizontal
holder.Children.Add(w)
holder.Children.Add(self.gcpi_label)
return holder
return w
# -- drawings ------------------------------------------------------------
def drawing_widget(self, block):
source = block.get('source')
caption = block.get('caption')
w = block.get('w') or SKETCH_W
h = block.get('h') or SKETCH_H
container = StackPanel()
self._draw_into(container, source, caption, w, h, self.values0)
self.drawings.append((container, source, caption, w, h))
return container
def _draw_into(self, container, source, caption, w, h, values):
container.Children.Clear()
container.Children.Add(_drawing_canvas(source, values, w, h))
if caption:
cap = TextBlock()
cap.Text = caption
cap.TextDecorations = TextDecorations.Underline
cap.HorizontalAlignment = HorizontalAlignment.Center
cap.FontSize = 11
cap.Margin = Thickness(0, 0, 0, 10)
container.Children.Add(cap)
def _redraw_drawings(self):
vals = self.current_values()
for container, source, caption, w, h in self.drawings:
try:
self._draw_into(container, source, caption, w, h, vals)
except Exception:
pass # a drawing is never worth taking the form down
# -- ui ----------------------------------------------------------------
def _section_tab(self, title, modname):
content = StackPanel()
content.Margin = Thickness(6, 4, 6, 4)
mod, err = _load_section_module(modname)
if mod is None:
content.Children.Add(_missing_section_note(title, modname, err))
return _tab(title, content)
if not hasattr(mod, 'page'):
content.Children.Add(_missing_section_note(
title, modname, 'module has no page()'))
return _tab(title, content)
try:
blocks, problems = mod.page(self.values0, reads=self.reads)
except Exception as exc:
content.Children.Add(_missing_section_note(title, modname, exc))
return _tab(title, content)
for b in (blocks or []):
w = _render_block(b, self)
if w is not None:
content.Children.Add(w)
if problems:
exp = Expander()
exp.Header = 'Notes ({0})'.format(len(problems))
exp.IsExpanded = False
exp.Margin = Thickness(0, 0, 0, 8)
plist = StackPanel()
plist.Margin = Thickness(6, 4, 6, 4)
for p in problems:
plist.Children.Add(_hint_block(u'• ' + p))
exp.Content = plist
content.Children.Insert(0, exp)
return _tab(title, content)
def _build(self):
body = StackPanel()
body.Margin = Thickness(6, 4, 6, 4)
self.tabctrl = TabControl()
for title, modname in SECTION_MODULES:
self.tabctrl.Items.Add(self._section_tab(title, modname))
self.tabctrl.Height = 620
body.Children.Add(self.tabctrl)
self.status = _label_block('', size=11)
body.Children.Add(self.status)
self.run_btn = Button()
self.run_btn.Content = 'Run Analysis'
self.run_btn.Margin = Thickness(0, 6, 0, 4)
self.run_btn.Width = 140
self.run_btn.HorizontalAlignment = HorizontalAlignment.Left
self.run_btn.Click += self._on_run
body.Children.Add(self.run_btn)
exp = Expander()
exp.Header = self.title
exp.IsExpanded = False
exp.Content = body
exp.Margin = Thickness(0, 2, 0, 2)
self.refresh_gate()
return exp
# -- behaviour ---------------------------------------------------------
def _update_gcpi(self):
if self.gcpi_label is None:
return
pair = self.editors.get('wind_enclosure')
box = pair[0] if pair else None
gcpi, why = F.wind_gcpi(box.Text if box is not None else None)
if gcpi is None:
self.gcpi_label.Text = ''
self.gcpi_label.Foreground = Brushes.Gray
else:
self.gcpi_label.Text = 'GCpi = +/-{0}'.format(gcpi)
self.gcpi_label.Foreground = Brushes.SeaGreen
def refresh_gate(self, *args):
self._update_gcpi()
errors = F.validate(self.current_values())[0]
self.run_btn.IsEnabled = not errors
if errors:
shown = '; '.join(errors[:6])
if len(errors) > 6:
shown += ' ...'
self.status.Text = 'Still needed: ' + shown
self.status.Foreground = Brushes.Firebrick
else:
self.status.Text = 'Ready to run.'
self.status.Foreground = Brushes.SeaGreen
def _on_change(self, sender, event):
self.refresh_gate()
self._redraw_drawings()
def _on_run(self, sender, event):
errors, parsed = F.validate(self.current_values())
if errors:
self.status.Text = 'Still needed: ' + '; '.join(errors[:6])
self.status.Foreground = Brushes.Firebrick
return
deck = F.build_deck(parsed)
ok, message = _run_one(self.mark, deck)
if ok:
self.status.Text = 'Ran. Page opened.'
self.status.Foreground = Brushes.SeaGreen
_report_one(self.mark, True, message)
else:
self.status.Text = 'Did not run -- see the output panel.'
self.status.Foreground = Brushes.Firebrick
_report_one(self.mark, False, message)
def _report_one(mark, ok, message):
if ok:
output.print_md('- **{0}** &mdash; ran. [{1}]({2})'.format(
mark, os.path.basename(message),
'file:///' + message.replace('\\', '/')))
else:
output.print_md('- **{0}** &mdash; NOT RUN: {1}'.format(mark, message))
def _analyze_all(panels, overall_status):
named = [(pf.mark, pf.current_values()) for pf in panels]
to_run, skipped = F.batch_plan(named)
output.print_md('## Analyze All')
ran_ok = 0
for label, parsed in to_run:
deck = F.build_deck(parsed)
ok, message = _run_one(label, deck)
_report_one(label, ok, message)
if ok:
ran_ok += 1
for label, errors in skipped:
output.print_md('- **{0}** &mdash; SKIPPED (incomplete): {1}'.format(
label, '; '.join(errors[:6])))
overall_status.Text = ('Analyze All: {0} ran, {1} skipped (incomplete). '
'See the output panel.'.format(ran_ok,
len(skipped)))
overall_status.Foreground = (Brushes.SeaGreen if not skipped
else Brushes.DarkGoldenrod)
def show_form(panels):
# Sized for real content -- each tab is a full Eriksson printed page, not
# a handful of text boxes, and the old 620x720 could not show one without
# a scrollbar inside a scrollbar. This still fits a 1080p screen with
# room to spare, and every scroller inside still has its own
# ScrollBarVisibility.Auto for anyone on something smaller.
win = Window()
win.Title = TITLE
win.Width = 1180
win.Height = 880
win.MinWidth = 760
win.MinHeight = 520
win.SizeToContent = SizeToContent.Manual
win.WindowStartupLocation = WindowStartupLocation.CenterScreen
root = Grid()
root.Margin = Thickness(10)
row_top = RowDefinition()
row_bottom = RowDefinition()
row_bottom.Height = GridLength.Auto
root.RowDefinitions.Add(row_top)
root.RowDefinitions.Add(row_bottom)
header = _math_block(
'Confirm the pre-filled fields, fill the blanks (especially the '
'support positions), and press Run Analysis on a panel below -- or '
'Analyze All for every complete one.', size=11, brush=_QUIET)
header.TextWrapping = TextWrapping.Wrap
header.Margin = Thickness(0, 0, 0, 8)
Grid.SetRow(header, 0)
# header lives inside the scroller's stack instead of its own grid row,
# so it scrolls with the panels rather than stealing a fixed row -- see
# below.
scroller = ScrollViewer()
scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto
stack = StackPanel()
stack.Children.Add(header)
for pf in panels:
stack.Children.Add(pf.expander)
scroller.Content = stack
Grid.SetRow(scroller, 0)
root.Children.Add(scroller)
bottom = StackPanel()
bottom.Margin = Thickness(0, 8, 0, 0)
Grid.SetRow(bottom, 1)
overall_status = _label_block('', size=11)
btn_row = StackPanel()
btn_row.Orientation = Orientation.Horizontal
all_btn = Button()
all_btn.Content = 'Analyze All'
all_btn.Width = 160
all_btn.Margin = Thickness(0, 0, 8, 0)
all_btn.Click += lambda s, e: _analyze_all(panels, overall_status)
btn_row.Children.Add(all_btn)
close_btn = Button()
close_btn.Content = 'Close'
close_btn.Width = 100
close_btn.Click += lambda s, e: win.Close()
btn_row.Children.Add(close_btn)
bottom.Children.Add(btn_row)
bottom.Children.Add(overall_status)
root.Children.Add(bottom)
border = Border()
border.Child = root
win.Content = border
win.ShowDialog()
# ------------------------------------------------------------------- main
KNOW = None
try:
KNOW = knowledge.load(doc)
except Exception:
KNOW = None
panels = _resolve_framing()
if not panels:
not_run('Select one or more structural-framing panels (the pieces you '
'want analysed), then run this again. Selecting an assembly, or '
'any member of one, resolves to its framing member.')
output.print_md('# {0}'.format(TITLE))
output.print_md('Opened an input form for **{0}** panel(s). Confirm the '
'pre-filled fields, fill the blanks (especially the support '
'positions), and press **Run Analysis** on a panel -- or '
'**Analyze All** for every complete one.'.format(len(panels)))
forms_list = [PanelForm(el, i, KNOW) for i, el in enumerate(panels)]
show_form(forms_list)