2026-09-26 18:05:21 +08:00
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
"""Headless Edge screenshots of the viewer, for the visual self-check loop."""
|
|
|
|
|
|
import os
|
|
|
|
|
|
import subprocess
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import shutil
|
|
|
|
|
|
import glob
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
sys.stdout.reconfigure(encoding="utf-8")
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
2026-10-15 04:31:31 +08:00
|
|
|
|
from arm.paths import DOCS, REPO
|
2026-09-26 18:05:21 +08:00
|
|
|
|
SHOTS = DOCS + "/shots"
|
2026-10-15 04:31:31 +08:00
|
|
|
|
# 虚拟时间预算:多文件版每页要拉 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
|
2026-09-26 18:05:21 +08:00
|
|
|
|
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)
|
|
|
|
|
|
if not EDGE:
|
|
|
|
|
|
EDGE = glob.glob(r"C:\Users\*\AppData\Local\Microsoft\Edge\*\msedge.exe")
|
|
|
|
|
|
EDGE = EDGE[0] if EDGE else None
|
|
|
|
|
|
if not EDGE:
|
|
|
|
|
|
raise SystemExit("msedge not found")
|
|
|
|
|
|
|
|
|
|
|
|
VIEWS = {
|
|
|
|
|
|
"01_iso": "?cam=iso",
|
|
|
|
|
|
"02_front": "?cam=front",
|
|
|
|
|
|
"03_side": "?cam=side",
|
|
|
|
|
|
"04_top": "?cam=top",
|
|
|
|
|
|
"05_exploded": "?cam=iso&expl=100",
|
|
|
|
|
|
"06_wrist": "?cam=wrist&xray=0",
|
|
|
|
|
|
"07_joint": "?cam=joint",
|
|
|
|
|
|
"08_pose": "?cam=iso&j2=35&j3=60&j4=-25&j1=25",
|
|
|
|
|
|
"09_sheet": "?sheet=1",
|
|
|
|
|
|
"10_solid": "?cam=iso&xray=0",
|
|
|
|
|
|
"11_tool_flange": "?cam=wrist&xray=0&j6=45",
|
|
|
|
|
|
"12_elbow_flare": "?cam=elbow&xray=0",
|
|
|
|
|
|
"13_forearm_side": "?cam=side&xray=0&rd=340&ty=245&tz=-135",
|
2026-10-15 12:26:08 +08:00
|
|
|
|
|
|
|
|
|
|
# 工程图 / 装配向导 / 运动模拟三种模式。URL 参数(?view=&sheet=&theme=&guide=)就是
|
|
|
|
|
|
# 为这几条截图加的:不把它们纳入基线,拆 CSS 与那 700 行工程图 JS 时等于没有关卡。
|
|
|
|
|
|
|
|
|
|
|
|
"m1_sheet_front": "?view=sheet",
|
|
|
|
|
|
"m2_sheet_joint": "?view=sheet&sheet=1",
|
|
|
|
|
|
"m3_sheet_exploded": "?view=sheet&sheet=2",
|
|
|
|
|
|
"m4_sheet_side": "?view=sheet&sheet=3",
|
|
|
|
|
|
"m5_guide_step1": "?view=sheet&sheet=guide&guide=0",
|
|
|
|
|
|
"m6_guide_step5": "?view=sheet&sheet=guide&guide=4",
|
|
|
|
|
|
"m7_sheet_blueprint": "?view=sheet&theme=blueprint",
|
|
|
|
|
|
"m8_sheet_dark": "?view=sheet&theme=dark",
|
|
|
|
|
|
"m9_motion": "?view=motion",
|
|
|
|
|
|
|
2026-10-07 19:08:47 +08:00
|
|
|
|
"harness_full_iso": "?cam=iso&xray=0",
|
|
|
|
|
|
"harness_overview": "?az=-0.7&po=1.2&rd=520&tx=0&ty=400&tz=-120&xray=0",
|
|
|
|
|
|
"harness_p04_p05": "?az=0.8&po=1.35&rd=280&tx=25&ty=470&tz=-128&xray=0",
|
|
|
|
|
|
"harness_p04_p05_xray": "?az=0.8&po=1.35&rd=280&tx=25&ty=470&tz=-128&alpha=0.35&xray=1",
|
|
|
|
|
|
"harness_p06_window": "?az=-0.65&po=1.25&rd=240&tx=25&ty=355&tz=-128&xray=0",
|
2026-10-15 12:26:08 +08:00
|
|
|
|
"harness_p07_window": "?az=-0.65&po=1.25&rd=260&tx=25&ty=275&tz=-130&xray=0",
|
|
|
|
|
|
"harness_p07_xray": "?az=-0.65&po=1.25&rd=260&tx=25&ty=275&tz=-130&alpha=0.35&xray=1",
|
2026-10-07 19:08:47 +08:00
|
|
|
|
"harness_xray_full": "?cam=iso&xray=1",
|
2026-10-15 12:26:08 +08:00
|
|
|
|
"harness_xray_internal": "?az=-0.7&po=1.2&rd=520&tx=0&ty=400&tz=-120&alpha=0.35&xray=1",
|
2026-09-26 18:05:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
os.makedirs(SHOTS, exist_ok=True)
|
2026-10-15 12:26:08 +08:00
|
|
|
|
|
2026-09-26 18:05:21 +08:00
|
|
|
|
want = sys.argv[1:] or list(VIEWS)
|
|
|
|
|
|
|
2026-10-15 12:26:08 +08:00
|
|
|
|
|
2026-09-26 18:05:21 +08:00
|
|
|
|
def take_shot(nm):
|
|
|
|
|
|
q = VIEWS[nm]
|
|
|
|
|
|
out = "%s/%s.png" % (SHOTS, nm)
|
2026-10-04 13:16:32 +08:00
|
|
|
|
import time, tempfile
|
2026-09-26 18:05:21 +08:00
|
|
|
|
t_bust = int(time.time() * 1000)
|
2026-10-15 04:31:31 +08:00
|
|
|
|
# serve.py 已下发 Cache-Control: no-store,不再需要 _t= 破缓存
|
|
|
|
|
|
target = q if q.startswith("http") else BASE + q + (
|
|
|
|
|
|
"&still=1" if "?" in q else "?still=1")
|
2026-10-04 13:16:32 +08:00
|
|
|
|
udd = tempfile.mkdtemp(prefix="eshot")
|
2026-09-26 18:05:21 +08:00
|
|
|
|
cmd = [EDGE, "--headless=new", "--use-gl=angle", "--use-angle=d3d11",
|
|
|
|
|
|
"--disable-cache", "--disable-application-cache", "--disk-cache-size=0",
|
2026-10-04 13:16:32 +08:00
|
|
|
|
"--user-data-dir=" + udd,
|
2026-09-26 18:05:21 +08:00
|
|
|
|
"--enable-gpu-rasterization", "--ignore-gpu-blocklist",
|
|
|
|
|
|
"--hide-scrollbars", "--force-device-scale-factor=1",
|
2026-10-15 04:31:31 +08:00
|
|
|
|
"--window-size=1500,860", "--virtual-time-budget=" + VTB,
|
2026-09-26 18:05:21 +08:00
|
|
|
|
"--screenshot=" + out, target]
|
|
|
|
|
|
r = subprocess.run(cmd, capture_output=True, timeout=180)
|
2026-10-04 13:16:32 +08:00
|
|
|
|
shutil.rmtree(udd, ignore_errors=True)
|
2026-09-26 18:05:21 +08:00
|
|
|
|
ok = os.path.exists(out) and os.path.getsize(out) > 20000
|
|
|
|
|
|
err = (r.stderr or b"").decode("utf-8", errors="ignore").strip()[:90].replace("\n", " ")
|
|
|
|
|
|
sz = os.path.getsize(out) if os.path.exists(out) else 0
|
2026-10-07 19:08:47 +08:00
|
|
|
|
print("%-18s %s %8s %s" % (nm, "OK " if ok else "FAIL", sz, err), flush=True)
|
2026-09-26 18:05:21 +08:00
|
|
|
|
return ok
|
|
|
|
|
|
|
2026-10-15 04:31:31 +08:00
|
|
|
|
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))
|
|
|
|
|
|
|
2026-10-07 19:08:47 +08:00
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
from concurrent.futures import ThreadPoolExecutor
|
2026-10-15 04:31:31 +08:00
|
|
|
|
srv = ensure_server()
|
|
|
|
|
|
try:
|
|
|
|
|
|
with ThreadPoolExecutor(max_workers=WORKERS) as executor:
|
|
|
|
|
|
results = list(executor.map(take_shot, want))
|
|
|
|
|
|
finally:
|
|
|
|
|
|
if srv:
|
|
|
|
|
|
srv.terminate()
|
2026-09-26 18:05:21 +08:00
|
|
|
|
|
2026-10-07 19:08:47 +08:00
|
|
|
|
print("edge:", EDGE)
|
|
|
|
|
|
print("all shots ok:", all(results))
|