52d94303bc
ci / test (push) Has been cancelled
Public demo snapshot of an in-house pyRevit extension for precast concrete shop-ticket QC (37 checks / 46 checklist rows) and a standalone panel handling solver (PCI Chapter 8). - No client, job, plant or product data: knowledge layers ship as structural templates only. - Ticket QC's model-read escalation targets a LAN model server endpoint (configurable), not a cloud provider. - Built from the private development repo via tools/make_dist.py.
233 lines
9.9 KiB
Python
233 lines
9.9 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Intermediate lateral model -> files. Revit-free, no I/O at module scope.
|
|
|
|
THREE OUTPUTS, AND WHY THE RISA ONE IS A DRAFT
|
|
----------------------------------------------
|
|
`emit_json` the intermediate structure verbatim. This is the ground
|
|
truth and the only output that claims to be complete.
|
|
`emit_csv` nodes and members as two plain tables. Any engineer can open
|
|
these, and any script can read them. No format risk at all.
|
|
`emit_risa_draft`
|
|
RISA-3D .r3d text. **DRAFT. NOT VERIFIED.**
|
|
|
|
What was established about the RISA-3D format, and from where:
|
|
|
|
RISA's own Appendix D - File Format
|
|
https://help.risa.com/risahelp/risa3d/Content/Appendices/Appendix%20D%20(3D).htm
|
|
|
|
* .r3d is plain ASCII, keyword driven, editable in Notepad.
|
|
* Data is divided into segments delimited by bracketed keyword labels:
|
|
[NODES], [MEMBERS], [PLATES], [BOUNDARY_CONDITIONS], [BASIC_LOAD_CASES],
|
|
[UNITS], [VERSION_NO], [PROGRAM_INFO], and the file opens with
|
|
[RISA-3D_INPUT_DATA].
|
|
* The record count follows the keyword label in angle brackets: `<n>`.
|
|
* Every record ends with a semicolon, except the project-description
|
|
records between [.PROJECT_DESCRIPTION] and [.END_PROJECT_DESCRIPTION].
|
|
* Labels are quoted strings. A member label may be up to 20 characters.
|
|
* A node record is "LABEL" x y z ... ;
|
|
* A member record is "LABEL" "DESIGN_LIST" "SHAPE_LABEL" iNode jNode ... ;
|
|
with later fields covering K node, rotation, section-set offset,
|
|
tension/compression, design rules, material, end releases, end offsets.
|
|
|
|
What was NOT established, and this is the reason for the word DRAFT:
|
|
|
|
The documentation designates the fields of each record by letter (a, b, c,
|
|
...) and the table that maps those letters to meanings could not be
|
|
retrieved -- the mirror that carries it returns 403, and no complete
|
|
worked example of a [NODES] or [MEMBERS] record was found in public
|
|
sources. So the field COUNT, the field ORDER, and the encoding of the
|
|
[UNITS] line are not known with confidence. Nor is which RISA version's
|
|
schema a given install expects.
|
|
|
|
Guessing them would produce a file that very plausibly opens and is
|
|
quietly wrong about which column is which -- the single worst outcome
|
|
available to this tool. So the RISA output is written with a
|
|
`.r3d.DRAFT.txt` extension. That is deliberate and is not a naming
|
|
accident: it cannot be double-clicked into RISA, and anybody who wants to
|
|
try it has to rename it, which is the moment they read the header.
|
|
|
|
To finish this properly: open any existing .r3d from this office in
|
|
Notepad, paste its [UNITS], [NODES] and [MEMBERS] segments into the repo
|
|
issue, and the emitter becomes a real one in about an hour. Until then the
|
|
CSV and the JSON are the outputs to use.
|
|
|
|
IronPython 2.7: no f-strings.
|
|
"""
|
|
# pylint: disable=invalid-name,broad-except
|
|
import json
|
|
|
|
from vibe import latmodel
|
|
|
|
|
|
# A shape label RISA cannot possibly resolve. Used wherever a section is
|
|
# unknown. The point is that RISA fails the member rather than analysing it
|
|
# with whatever the current default shape happens to be -- an unanalysable
|
|
# file beats a wrong answer.
|
|
UNRESOLVED_SHAPE = 'WILLTOOLS_UNRESOLVED_SECTION'
|
|
UNRESOLVED_MATERIAL = 'WILLTOOLS_UNRESOLVED_MATERIAL'
|
|
|
|
|
|
def emit_json(model):
|
|
"""The intermediate structure. Complete, and the only output that is."""
|
|
return json.dumps(model, indent=2, sort_keys=True)
|
|
|
|
|
|
def _csv_cell(v):
|
|
if v is None:
|
|
return ''
|
|
s = v if isinstance(v, str) else str(v)
|
|
if any(c in s for c in ',"\n'):
|
|
return '"' + s.replace('"', '""') + '"'
|
|
return s
|
|
|
|
|
|
def _csv(header, rows):
|
|
lines = [','.join(header)]
|
|
for r in rows:
|
|
lines.append(','.join([_csv_cell(c) for c in r]))
|
|
return '\n'.join(lines) + '\n'
|
|
|
|
|
|
def emit_nodes_csv(model):
|
|
"""label, x, y, z, unit, how many members meet here."""
|
|
rows = [[n['label'], '{0:.6f}'.format(n['x']), '{0:.6f}'.format(n['y']),
|
|
'{0:.6f}'.format(n['z']), model['length_unit'],
|
|
len(n['members']), ' '.join(n['members'])]
|
|
for n in model['nodes']]
|
|
return _csv(['label', 'x', 'y', 'z', 'unit', 'member_count', 'members'],
|
|
rows)
|
|
|
|
|
|
def emit_members_csv(model):
|
|
"""One row per member, with the blanks LEFT BLANK.
|
|
|
|
An empty `section` cell is the honest representation of a section this
|
|
could not read. It is never filled with a plausible-looking default, and
|
|
`unresolved` on the same row says the same thing in words so that a
|
|
reader skimming for problems finds them without comparing columns.
|
|
"""
|
|
rows = []
|
|
for m in model['members']:
|
|
sec = m.get('section') or {}
|
|
rows.append([
|
|
m['label'], m['i_node'], m['j_node'],
|
|
'{0:.6f}'.format(m['length_ft']), model['length_unit'],
|
|
sec.get('name'), sec.get('source'),
|
|
m.get('material'),
|
|
'', # end releases: never extracted
|
|
m.get('axis_source'),
|
|
m.get('category'), m.get('mark'), m.get('element_id'),
|
|
m.get('desc'), m.get('token'),
|
|
'; '.join(m.get('unresolved') or []),
|
|
])
|
|
return _csv(['label', 'i_node', 'j_node', 'length', 'unit',
|
|
'section', 'section_source', 'material', 'end_releases',
|
|
'axis_source', 'revit_category', 'mark', 'revit_element_id',
|
|
'identity_description', 'marker_matched', 'unresolved'],
|
|
rows)
|
|
|
|
|
|
DRAFT_HEADER = """\
|
|
! =====================================================================
|
|
! DRAFT -- NOT A VERIFIED RISA-3D INPUT FILE. DO NOT LOAD AND TRUST.
|
|
! =====================================================================
|
|
!
|
|
! Written by WillTools lateral extraction from a Revit model.
|
|
!
|
|
! The section keywords, the <n> record counts, the semicolon terminators
|
|
! and the quoted labels below follow RISA's published Appendix D. The
|
|
! FIELD ORDER inside each record does NOT: Appendix D names those fields
|
|
! only by letter and the table mapping letters to meanings could not be
|
|
! obtained. The numbers below are in the right sections and are very
|
|
! likely in the wrong columns.
|
|
!
|
|
! This file is named .r3d.DRAFT.txt so that it cannot be opened into RISA
|
|
! by accident. Renaming it to .r3d is a decision, not a convenience.
|
|
!
|
|
! WHAT IS NOT IN THIS FILE, AT ALL:
|
|
! * end releases -- not extractable from these Revit families
|
|
! * boundary conditions / supports -- none. This model is unrestrained.
|
|
! * loads, load combinations, diaphragms, mass, seismic parameters
|
|
! * plates, walls as shells (walls appear as line members only)
|
|
! * member offsets, K-node orientation, unbraced lengths, design rules
|
|
!
|
|
! Use the .json and the two .csv files beside this one. They are complete
|
|
! statements of what was extracted; this file is not.
|
|
!
|
|
! Length unit: {unit} (Revit internal decimal feet, project coordinates)
|
|
! Node weld tolerance: {tol} ft
|
|
! ---------------------------------------------------------------------
|
|
"""
|
|
|
|
|
|
def emit_risa_draft(model):
|
|
"""RISA-3D-shaped text, clearly and repeatedly labelled as a draft."""
|
|
L = []
|
|
L.append(DRAFT_HEADER.format(unit=model['length_unit'],
|
|
tol=model['node_tol_ft']))
|
|
L.append('[RISA-3D_INPUT_DATA]')
|
|
L.append('')
|
|
L.append('[.PROJECT_DESCRIPTION]')
|
|
L.append('DRAFT lateral extraction -- field order unverified')
|
|
L.append('Model: {0}'.format(model.get('model_title') or '(unnamed)'))
|
|
L.append('[.END_PROJECT_DESCRIPTION]')
|
|
L.append('')
|
|
# [UNITS] is deliberately NOT emitted as data. Its field encoding is the
|
|
# part of the format least established, and a units line in the wrong
|
|
# order is the most dangerous single line the file could contain -- it
|
|
# rescales everything silently. A comment says what the numbers are in.
|
|
L.append('! [UNITS] omitted on purpose -- the encoding of this line is')
|
|
L.append('! not established, and a wrong units line rescales the whole')
|
|
L.append('! model without any visible sign. All lengths below are FEET.')
|
|
L.append('')
|
|
|
|
L.append('[NODES] <{0}>'.format(len(model['nodes'])))
|
|
for n in model['nodes']:
|
|
L.append(' "{0}" {1:.6f} {2:.6f} {3:.6f} ;'
|
|
.format(n['label'], n['x'], n['y'], n['z']))
|
|
L.append('[END_NODES]')
|
|
L.append('')
|
|
|
|
L.append('[MEMBERS] <{0}>'.format(len(model['members'])))
|
|
for m in model['members']:
|
|
sec = m.get('section') or {}
|
|
shape = sec.get('name') or UNRESOLVED_SHAPE
|
|
mat = m.get('material') or UNRESOLVED_MATERIAL
|
|
L.append(' "{0}" "" "{1}" "{2}" "{3}" "{4}" ; ! {5}'
|
|
.format(m['label'][:20], shape, mat,
|
|
m['i_node'], m['j_node'],
|
|
'unresolved: ' + ', '.join(m['unresolved'])
|
|
if m['unresolved'] else 'complete'))
|
|
L.append('[END_MEMBERS]')
|
|
L.append('')
|
|
|
|
L.append('! [BOUNDARY_CONDITIONS] intentionally absent -- no supports')
|
|
L.append('! were extracted. Nothing here is restrained.')
|
|
L.append('')
|
|
L.append('! ---- what was skipped, and why -----------------------------')
|
|
if model['skipped']:
|
|
for s in model['skipped']:
|
|
L.append('! skipped element {0} ({1}, mark {2}): {3}'
|
|
.format(s.get('element_id'), s.get('category'),
|
|
s.get('mark'), s.get('reason')))
|
|
else:
|
|
L.append('! nothing was skipped.')
|
|
L.append('')
|
|
for note in model['notes']:
|
|
L.append('! NOTE: ' + note)
|
|
return '\n'.join(L) + '\n'
|
|
|
|
|
|
def file_set(model, stem):
|
|
"""[(filename, text)] -- everything this tool writes, in one place.
|
|
|
|
The RISA file is last and its name carries the warning, so that a
|
|
directory listing sorted any way still shows the complete outputs first.
|
|
"""
|
|
return [
|
|
(stem + '-lateral.json', emit_json(model)),
|
|
(stem + '-lateral-nodes.csv', emit_nodes_csv(model)),
|
|
(stem + '-lateral-members.csv', emit_members_csv(model)),
|
|
(stem + '-lateral.r3d.DRAFT.txt', emit_risa_draft(model)),
|
|
]
|