refactor(viewer): 单文件查看器拆成 webapp/ 多文件,走本地 http;Worker 换成真文件
前端不再是"一个 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 穿透。
This commit is contained in:
+71
-10
@@ -11,9 +11,15 @@ try:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
from arm.paths import WEB, DOCS
|
||||
from arm.paths import DOCS, REPO
|
||||
SHOTS = DOCS + "/shots"
|
||||
HTML = WEB + "/机器人手臂3D查看器.html"
|
||||
# 虚拟时间预算:多文件版每页要拉 14MB 数据脚本 + 668KB three,4 并发下 20s 会不够(实测 8/10 视图两轮不一致)
|
||||
# 时可上调。默认仍给 20000,用 ARM_VTB 覆盖做对照实验。
|
||||
VTB = os.environ.get("ARM_VTB", "40000")
|
||||
WORKERS = int(os.environ.get("ARM_SHOT_WORKERS", "2"))
|
||||
# 前端拆分后是多文件 + Worker,只能走 http(file:// 下模块与 Worker 会被 CORS 拦掉)。
|
||||
PORT = int(os.environ.get("ARM_PORT", "8123"))
|
||||
BASE = "http://127.0.0.1:%d/webapp/index.html" % PORT
|
||||
CAND = [r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
|
||||
r"C:\Program Files\Microsoft\Edge\Application\msedge.exe"]
|
||||
EDGE = next((c for c in CAND if os.path.exists(c)), None)
|
||||
@@ -119,9 +125,9 @@ VIEWS = {
|
||||
"user_p07_check": "?az=1.5708&po=1.5708&rd=260&tx=19.5&ty=220&tz=-128&xray=0&hide=P01,P02,P03,P04,P05,P06,P09,M1,M2,M3,M4,M5,M6,ENV",
|
||||
"boss_p06": "?az=1.5708&po=1.5708&rd=190&tx=18.5&ty=332&tz=-128&xray=0&hide=P01,P02,P03,P04,P05,P07,P08,P09,M1,M2,M3,M5,M6,ENV",
|
||||
"boss_p07": "?az=0&po=0.0&rd=210&tx=19.5&ty=215&tz=-128&xray=0&hide=P01,P02,P03,P04,P05,P06,P08,P09,M1,M2,M3,M4,M6,ENV",
|
||||
"svg_side": f"file:///{DOCS}/sheet_side.svg",
|
||||
"svg_joint": f"file:///{DOCS}/sheet_joint.svg",
|
||||
"svg_exploded": f"file:///{DOCS}/sheet_exploded.svg",
|
||||
"svg_side": f"http://127.0.0.1:{PORT}/docs/sheet_side.svg",
|
||||
"svg_joint": f"http://127.0.0.1:{PORT}/docs/sheet_joint.svg",
|
||||
"svg_exploded": f"http://127.0.0.1:{PORT}/docs/sheet_exploded.svg",
|
||||
"user_circle_check": "?az=0.8&po=1.95&rd=240&tx=19.5&ty=430&tz=-128&xray=0&hide=M1,M2,M3,M4,M5,M6,ENV",
|
||||
"user_exact_view": "?az=1.5708&po=1.92&rd=150&tx=19.5&ty=440&tz=-128&xray=0&hide=P01,P02,P03,M1,M2,M3,M4,M5,M6,ENV",
|
||||
"shot_p08_user": "?az=1.5708&po=1.92&rd=150&tx=19.5&ty=176&tz=-128&xray=0&hide=P01,P02,P03,P04,P05,P06,P07,P09,M1,M2,M3,M4,M5,M6,ENV",
|
||||
@@ -153,15 +159,16 @@ def take_shot(nm):
|
||||
out = "%s/%s.png" % (SHOTS, nm)
|
||||
import time, tempfile
|
||||
t_bust = int(time.time() * 1000)
|
||||
target = q if q.startswith("file://") else "file:///" + HTML.replace("\\", "/") + q + (
|
||||
f"&still=1&_t={t_bust}" if "?" in q else f"?still=1&_t={t_bust}")
|
||||
# serve.py 已下发 Cache-Control: no-store,不再需要 _t= 破缓存
|
||||
target = q if q.startswith("http") else BASE + q + (
|
||||
"&still=1" if "?" in q else "?still=1")
|
||||
udd = tempfile.mkdtemp(prefix="eshot")
|
||||
cmd = [EDGE, "--headless=new", "--use-gl=angle", "--use-angle=d3d11",
|
||||
"--disable-cache", "--disable-application-cache", "--disk-cache-size=0",
|
||||
"--user-data-dir=" + udd,
|
||||
"--enable-gpu-rasterization", "--ignore-gpu-blocklist",
|
||||
"--hide-scrollbars", "--force-device-scale-factor=1",
|
||||
"--window-size=1500,860", "--virtual-time-budget=20000",
|
||||
"--window-size=1500,860", "--virtual-time-budget=" + VTB,
|
||||
"--screenshot=" + out, target]
|
||||
r = subprocess.run(cmd, capture_output=True, timeout=180)
|
||||
shutil.rmtree(udd, ignore_errors=True)
|
||||
@@ -171,10 +178,64 @@ def take_shot(nm):
|
||||
print("%-18s %s %8s %s" % (nm, "OK " if ok else "FAIL", sz, err), flush=True)
|
||||
return ok
|
||||
|
||||
def warmup():
|
||||
"""拉一次最大的那个数据脚本,成功才算服务可用。"""
|
||||
import urllib.request
|
||||
try:
|
||||
with urllib.request.urlopen(BASE, timeout=5) as r:
|
||||
r.read()
|
||||
with urllib.request.urlopen("http://127.0.0.1:%d/webapp/generated/00_data.js" % PORT,
|
||||
timeout=30) as r:
|
||||
n = len(r.read())
|
||||
return n > 1000000
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def ensure_server():
|
||||
"""端口没开就自己拉一个 serve 子进程,跑完回收;不让截图依赖"你先把服务开着"。"""
|
||||
import socket
|
||||
import time
|
||||
sk = socket.socket()
|
||||
try:
|
||||
sk.connect(("127.0.0.1", PORT))
|
||||
sk.close()
|
||||
return None
|
||||
except OSError:
|
||||
pass
|
||||
errf = open(os.path.join(REPO, "scratch", "_serve_err.txt"), "wb")
|
||||
p = subprocess.Popen([sys.executable, "-m", "arm.viewer.serve", "--port", str(PORT)],
|
||||
cwd=REPO, stdout=subprocess.DEVNULL, stderr=errf)
|
||||
for _ in range(60):
|
||||
time.sleep(0.25)
|
||||
sk = socket.socket()
|
||||
try:
|
||||
sk.connect(("127.0.0.1", PORT))
|
||||
sk.close()
|
||||
if warmup():
|
||||
print("serve 已拉起 :%d(首屏数据已预热)" % PORT, flush=True)
|
||||
return p
|
||||
except OSError:
|
||||
pass
|
||||
finally:
|
||||
try:
|
||||
sk.close()
|
||||
except Exception:
|
||||
pass
|
||||
p.kill()
|
||||
err = open(os.path.join(REPO, "scratch", "_serve_err.txt"), "rb").read().decode("utf-8", "ignore").strip()[-400:]
|
||||
raise SystemExit("serve 拉不起来:端口 %d 可能被占;子进程输出:\n%s" % (PORT, err))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
with ThreadPoolExecutor(max_workers=min(4, os.cpu_count() or 2)) as executor:
|
||||
results = list(executor.map(take_shot, want))
|
||||
srv = ensure_server()
|
||||
try:
|
||||
with ThreadPoolExecutor(max_workers=WORKERS) as executor:
|
||||
results = list(executor.map(take_shot, want))
|
||||
finally:
|
||||
if srv:
|
||||
srv.terminate()
|
||||
|
||||
print("edge:", EDGE)
|
||||
print("all shots ok:", all(results))
|
||||
|
||||
Reference in New Issue
Block a user