#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Build a Panel Analysis page-spec JSON with NO Revit running. python tools/pa_pagespec.py python tools/pa_pagespec.py --reads reads.json --out spec.json python tools/pa_pagespec.py --reads reads.json --job-root JOBDIR This is the CPython half of the owner's scope note on the HTML-page rewrite: the front end (前端) must open and run before Revit ever supplies a value, Revit only supplies PREFILL. `vibe.pa_pagespec.build_pagespec()` is already Revit-free (see that module's own docstring); this script is what gets `values` / `know` in front of it the same way `tools/deckcheck.py` gets knowledge in front of its own checks -- off a live `doc`, on host CPython. `--reads` is a JSON file shaped like whatever `PanelAnalysis.pushbutton`'s `_reads_for()` hands `panelanalysisform.prefill()` -- the exact file a bridge dry-run should save (see `_scratch/pa_pagespec_dump.py`, which writes `_scratch/pa_reads_wpa655.json` for this purpose) or a hand-written stand-in for testing the page with no model at all. Omitted, or pointed at a file that is not there, this runs with `reads = {}` -- an EMPTY, but still complete and defaulted, form (see `pa_pagespec.build_pagespec`'s "THE TWO CALLING SHAPES" note): every field present, every `pa_defaults` rule applied, every table at its minimum row count, every drawing's geometry degraded to a placeholder rather than missing. That is the shape the HTML page needs to be developable and testable in a browser with no Revit and no model at all. `--job-root` names the folder holding a job's OWN `knowledge.yaml` (see `tools/deckcheck.py`'s `build_knowledge()` docstring for exactly what that resolves) -- omitted, this runs with just the shipped defaults + catalogue layers, same as `deckcheck.build_knowledge(None)`. `--out` writes the spec there (UTF-8); omitted, it prints to stdout. CONSTRAINTS THIS RESPECTS (same as `tools/deckcheck.py`, for the same reason): host CPython, no Revit, standalone; prints no plant or client name of its own -- design numbers and marks flowing through a `--reads` file are data, not this file's business, per CLAUDE.md's "No client or job information in the repo". """ from __future__ import print_function import argparse import io import json import os import sys import types HERE = os.path.dirname(os.path.abspath(__file__)) REPO = os.path.abspath(os.path.join(HERE, os.pardir)) LIB = os.path.join(REPO, 'WillTools.extension', 'lib') if LIB not in sys.path: sys.path.insert(0, LIB) # Same trick `tools/deckcheck.py` / `tools/ticket_delta.py` / # `WillTools.extension/tests/conftest.py` all use: `vibe/__init__.py` # imports the Revit API at module scope, which is fatal off Revit. Register # a bare `vibe` package pointed at the directory so the Revit-free # submodules (`panelanalysisform`, `pa_pagespec`, the five page modules, the # four `pa_sketch_*` modules, `pa_defaults`) import normally and # `__init__.py` never runs. if 'vibe' not in sys.modules: _stub = types.ModuleType('vibe') _stub.__path__ = [os.path.join(LIB, 'vibe')] sys.modules['vibe'] = _stub if HERE not in sys.path: sys.path.insert(0, HERE) import deckcheck # noqa: E402 -- reuses build_knowledge(), not re-derived from vibe import pa_pagespec # noqa: E402 from vibe import panelanalysisform as F # noqa: E402 def _load_reads(path): """`(reads_dict, error_or_None)`. A path that is not given, or not there, is the ordinary "no Revit at all" case -- `{}`, no error. A path that IS there and will not parse as a JSON object is reported, never silently treated as empty (the same "a layer that fails to load must never look like a layer that agrees" discipline `knowledge._merge_file` uses).""" if not path: return {}, None if not os.path.isfile(path): return {}, "'{0}' does not exist; running with reads = {{}}.".format( path) try: with io.open(path, 'r', encoding='utf-8') as fh: data = json.load(fh) except Exception as exc: return {}, "'{0}' could not be read as JSON ({1!r}); running with " \ 'reads = {{}}.'.format(path, exc) if not isinstance(data, dict): return {}, "'{0}' is not a JSON object; running with reads = {{}}." \ .format(path) return data, None def main(argv=None): parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) parser.add_argument('--reads', default=None, help='JSON file of the model reads dict (omit for ' 'an empty, still-complete form)') parser.add_argument('--job-root', default=None, help="folder holding the job's own knowledge.yaml " '(omit for shipped defaults + catalogue only)') parser.add_argument('--out', default=None, help='write the spec here (UTF-8); omit to print ' 'to stdout') args = parser.parse_args(argv) reads, reads_err = _load_reads(args.reads) if reads_err: sys.stderr.write(reads_err + '\n') try: know = deckcheck.build_knowledge(args.job_root) except Exception as exc: sys.stderr.write( 'knowledge could not be resolved off Revit ({0!r}); running ' 'with the bare shipped defaults.\n'.format(exc)) know = {} values = F.prefill(reads, know=know) spec = pa_pagespec.build_pagespec(values, know=know, reads=reads) blob = pa_pagespec.dumps(spec) if args.out: with io.open(args.out, 'w', encoding='utf-8') as fh: fh.write(blob if isinstance(blob, str) else blob.decode('utf-8')) sys.stderr.write('wrote {0} ({1} bytes)\n'.format( args.out, len(blob))) else: print(blob) return 0 if __name__ == '__main__': raise SystemExit(main())