WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m18s

History-free snapshot of the private development repository at cbe45c5
(2026-09-09). Code, docs and public catalogue only; no plant or job
knowledge layers, no client or job names (tools/scrub.py clean). The
intro deck is left out of this snapshot because its slide images could
not be checked for job drawings.
This commit is contained in:
2026-09-09 08:32:15 -05:00
commit 8e6ee31596
406 changed files with 223435 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""Render a WillTools web page headless and screenshot every tab -- the
browser dry-run this machine could not do until 2026-09-07 (owner: "浏览器你
能不能在debian上面自己渲染一个?").
python tools/webshot.py pa --reads _scratch/pa_reads_x.json --out /tmp/shots
python tools/webshot.py ba --reads beamcalc/fixtures/rba5_16x22.json --out /tmp/shots
Starts tools/pa_web.py or tools/ba_web.py on a free port, drives Chrome
headless (google-chrome / chromium on PATH) to /?tab=N&expand=all for each
tab, writes <out>/<tool>-tab<N>.png plus <out>/<tool>-console.txt (the page's
console errors, which is the half a screenshot cannot show), then closes the
server through POST /api/close. Exit 1 if any tab logged a console error.
CPython 3 only; no Revit anywhere.
"""
import argparse, json, os, shutil, subprocess, sys, time, urllib.request
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def chrome():
for c in ('google-chrome', 'chromium', 'chromium-browser', 'chrome'):
p = shutil.which(c)
if p:
return p
sys.exit('no chrome/chromium on PATH')
def start_server(tool, reads):
args = [sys.executable, os.path.join(ROOT, 'tools', '%s_web.py' % tool), '--port', '0']
if reads:
args += ['--reads', reads]
p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, cwd=ROOT)
url = None
for _ in range(200):
line = p.stdout.readline()
if not line:
break
if '_WEB_URL=' in line:
url = line.split('=', 1)[1].strip()
break
if not url:
err = p.stderr.read()
sys.exit('server did not print its URL\n' + err)
return p, url
def main():
ap = argparse.ArgumentParser()
ap.add_argument('tool', choices=['pa', 'ba'])
ap.add_argument('--reads')
ap.add_argument('--out', default='/tmp/webshot')
ap.add_argument('--tabs', type=int, default=5)
ap.add_argument('--width', type=int, default=1400)
ap.add_argument('--height', type=int, default=2200)
a = ap.parse_args()
os.makedirs(a.out, exist_ok=True)
p, url = start_server(a.tool, a.reads)
exe = chrome()
errors = []
try:
spec = json.load(urllib.request.urlopen(url + 'api/spec', timeout=30))
tabs = len(spec.get('pages', [])) or a.tabs
for n in range(1, tabs + 1):
png = os.path.join(a.out, '%s-tab%d.png' % (a.tool, n))
log = os.path.join(a.out, '%s-tab%d.console.txt' % (a.tool, n))
cmd = [exe, '--headless=new', '--no-sandbox', '--disable-gpu', '--hide-scrollbars',
'--window-size=%d,%d' % (a.width, a.height), '--virtual-time-budget=4000',
'--enable-logging=stderr', '--v=0',
'--screenshot=' + png, '%s?tab=%d&expand=all' % (url, n)]
r = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
console = [l for l in r.stderr.splitlines() if 'CONSOLE' in l]
with open(log, 'w') as fh:
fh.write('\n'.join(console))
bad = [l for l in console if 'error' in l.lower() or 'uncaught' in l.lower()]
errors += bad
print('tab %d -> %s (%d console lines, %d errors)' % (n, png, len(console), len(bad)))
finally:
try:
urllib.request.urlopen(urllib.request.Request(url + 'api/close', method='POST', data=b'{}'), timeout=5)
except Exception:
pass
p.terminate()
if errors:
print('CONSOLE ERRORS:\n' + '\n'.join(errors[:20]))
sys.exit(1)
if __name__ == '__main__':
main()