前端不再是"一个 16MB HTML + 一个 2955 行模板"。现在: webapp/index.html 343 行 DOM 骨架,零内联数据 webapp/styles/*.css 811 行 CSS 按段切成 8 个文件 webapp/js/app.js 原内联主脚本逐字搬出(仍是普通脚本,S5 才改 ESM) webapp/js/ik-solver.js / ik-glue.js / vendor/three.min.js(原 web/ 三个文件) webapp/workers/ik.worker.js 真 Worker 文件 webapp/generated/00_data.js 生成物(网格/工程图/行程/碰撞/螺钉) dist/机器人手臂3D查看器.html 分发用单文件,由 build_viewer 内联上面这些文件产出 入口:python -m arm.viewer.serve 后开 http://127.0.0.1:8123/ (serve 带 no-store,改完刷新即新构建,不再需要 Ctrl+F5;显式 MIME, .js 被 Windows 注册表判成 text/plain 会让模块静默不执行) 删掉 src/viewer_tpl.html 与 web/ 目录(旧产物移入 backups/pre_split_20261015/)。 工程图不再维护第二份 sheets.jsdata —— 直接读 draw_sheet 写出的 4 个真 SVG。 实测中修掉的问题: - serve 的 / 原先"内部映射"到 webapp/index.html,导致文档基址是 /、相对路径的 8 个 CSS 与 4 个 JS 全部 404(整页无样式无脚本)。改成 302 跳转。 - file:// 下 new Worker('workers/…') 构造阶段不抛错、错误以异步 onerror 到达, 所以"构造失败才试 Blob 兜底"永远不会触发;改为按协议选路。两条路径都已实测 backend=worker(http 走文件、file:// 走 Blob)。 - Worker 握手 3s 在 headless 并发下会误判"没起来"(截图里红徽标即此),放宽到 10s, 并把后端状态暴露成 window.__IK_BACKEND / __IK_BACKEND_LOG 供测试直读。 - 截图竞态:4 并发 × 每页 14MB 数据时,虚拟时间预算会在页面加载完成前到期, 拍到半初始化画面(且能"稳定地坏",旧基线里 m9_motion 就是这么混进来的)。 并发降到 2、预算 40s、serve 加 HTTP/1.1+大缓冲+字节缓存,并用"同一构建两轮" 重新界定稳定性:99 张逐字节一致、6 张属噪声改走像素容差(mean<=0.02、单像素<=32)。 - 关卡自身两处假通过已修:node 输出被 GBK 解码炸掉时不再算 PASS; 线束干涉测试读不到通道源码时会 exit 2,而不是"零通道=零穿透=通过"。 关卡结果:python -m arm.build_model 退出码 0;10 项数据哈希与基线一致; IK exact 段逐字节一致;99 张截图逐字节一致 + 6 张在噪声阈内;线束干涉测试 1800 点 0 穿透。
99 lines
4.0 KiB
Python
99 lines
4.0 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""把生成物落到 webapp/,并打一份单文件分发版。
|
||
|
||
单文件时代这里做 8 路字符串替换再写 3 份 16MB 副本;现在前端本身就是 webapp/ 下的
|
||
文件,本脚本只剩两件事:
|
||
write_data() 网格/工程图/行程/碰撞/螺钉 -> generated/00_data.js(普通脚本 const)
|
||
build_single() 把 webapp/ 的 css/js 原样内联成 dist/机器人手臂3D查看器.html(分发用)
|
||
|
||
S5 数据改 fetch() 后 write_data() 的 00_data.js 退役;S8 由 pack.py 的受限 ESM 内联器
|
||
接管 build_single()。
|
||
"""
|
||
import io
|
||
import json
|
||
import os
|
||
import re
|
||
import sys
|
||
import time
|
||
|
||
try:
|
||
sys.stdout.reconfigure(encoding="utf-8")
|
||
except Exception:
|
||
pass
|
||
|
||
from arm.paths import DATA, DIST, DOCS, GEN, WEBAPP
|
||
|
||
# 与 SHEET_METAS 的顺序一致;工程图读 draw_sheet 写出的真 SVG,
|
||
# 不再维护第二份 sheets.jsdata(单文件时代的内联载体,内容与 SVG 完全重复)。
|
||
SHEET_ORDER = ["sheet_front", "sheet_joint", "sheet_exploded", "sheet_side"]
|
||
|
||
|
||
def _read(path, dflt=None):
|
||
if os.path.exists(path):
|
||
return io.open(path, encoding="utf-8").read()
|
||
if dflt is None:
|
||
raise SystemExit("缺少查看器数据文件: %s(先跑 python -m arm.build_model)" % path)
|
||
return dflt
|
||
|
||
|
||
def write_data():
|
||
t0 = time.time()
|
||
print("【数据】汇总查看器数据 -> webapp/generated/00_data.js", flush=True)
|
||
os.makedirs(GEN, exist_ok=True)
|
||
|
||
mesh = _read(DATA + "/mesh_arm.json")
|
||
svgs = [_read(DOCS + "/%s.svg" % n) for n in SHEET_ORDER]
|
||
trv = _read(DOCS + "/travel.json", "[]")
|
||
col = _read(DOCS + "/collide.json", '{"caps":[],"parts":{}}')
|
||
scr = _read(DOCS + "/screws.json", "[]")
|
||
print(" - 网格 %d KB / 工程图 %d 张 / travel %d B / collide %d KB / screws %d B"
|
||
% (len(mesh) // 1024, len(svgs), len(trv), len(col) // 1024, len(scr)), flush=True)
|
||
|
||
out = ["/* 生成物,勿手改:由 python -m arm.viewer.build_viewer 写入。",
|
||
" 普通脚本顶层 const —— 经典脚本共享全局词法作用域,后续脚本直接可见。 */"]
|
||
for name, raw in (("DATA", mesh), ("TRAVEL", trv), ("COLLIDE", col), ("SCREWS", scr)):
|
||
out.append("const %s = %s;" % (name, raw.strip()))
|
||
out.append("const SHEETS = %s;" % json.dumps(svgs, ensure_ascii=False))
|
||
text = "\n".join(out) + "\n"
|
||
tmp = GEN + "/00_data.js.tmp"
|
||
io.open(tmp, "w", encoding="utf-8").write(text)
|
||
os.replace(tmp, GEN + "/00_data.js")
|
||
print(" 完成:%d KB, %.2fs ✓" % (len(text) // 1024, time.time() - t0), flush=True)
|
||
|
||
|
||
def build_single():
|
||
"""分发用单文件:仍是普通脚本,所以内联是平凡的(S5 变 ESM 后交给 pack.py)。"""
|
||
t0 = time.time()
|
||
os.makedirs(DIST, exist_ok=True)
|
||
html = _read(WEBAPP + "/index.html")
|
||
total = len(html)
|
||
|
||
def sub(m, kind):
|
||
nonlocal total
|
||
body = _read(WEBAPP + "/" + m.group(1))
|
||
total += len(body)
|
||
return ("<style>\n" + body + "\n</style>") if kind == "css" \
|
||
else ("<script>\n" + body + "\n</script>")
|
||
|
||
out = re.sub(r'<link rel="stylesheet" href="([^"]+)">', lambda m: sub(m, "css"), html)
|
||
out = re.sub(r'<script src="([^"]+)"></script>', lambda m: sub(m, "js"), out)
|
||
|
||
left = re.findall(r'(?:src|href)="(?!data:)([^"]+)"', out)
|
||
assert not left, "单文件里仍有外部引用: %s" % left[:5]
|
||
assert len(out) >= total * 0.95, "内联后 %d 远小于各部之和 %d,疑似漏文件" % (len(out), total)
|
||
dst = DIST + "/机器人手臂3D查看器.html"
|
||
# Windows 上被浏览器打开的 html 是独占锁;写不进去就直接报,不要静默留旧版。
|
||
io.open(dst, "w", encoding="utf-8").write(out)
|
||
print(" - 单文件分发版 %.2f MB, %.2fs ✓ %s"
|
||
% (os.path.getsize(dst) / 1e6, time.time() - t0, dst), flush=True)
|
||
|
||
|
||
def rebuild_viewer():
|
||
"""build_model 流水线的最后一步(沿用旧函数名,调用点不改)。"""
|
||
write_data()
|
||
build_single()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
rebuild_viewer()
|