# -*- 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: ``. * 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 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)), ]