# -*- 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 sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from armcore import ROOT, WORKSPACE, WEB, DOCS SHOTS = DOCS + "/shots" HTML = WEB + "/机器人手臂3D查看器.html" 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", "zoom_user": "?cam=iso&az=-0.9&po=1.15&rd=470&tx=30&ty=320&tz=-80&xray=0", "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", "user_target": "?cam=side&az=-1.5708&po=1.57&xray=0&rd=340&tx=100&ty=245&tz=-135", "view_yz_pos_x": "?cam=side&az=0&po=1.57&xray=0&rd=340&tx=100&ty=245&tz=-135", "view_yz_neg_x": "?cam=side&az=3.14159&po=1.57&xray=0&rd=340&tx=100&ty=245&tz=-135", "view_iso_forearm": "?cam=side&az=-0.65&po=1.2&xray=0&rd=320&tx=100&ty=245&tz=-135", "user_view1": "?cam=wrist&az=0.55&po=1.4&rd=310&xray=0", "user_view2": "?cam=wrist&az=0.2&po=1.4&rd=290&xray=0", "user_p04": "?cam=joint&az=-1.5708&po=1.57&rd=380&xray=0", "user_p04_opp": "?cam=joint&az=1.5708&po=1.57&rd=380&xray=0", "user_elbow_rear": "?az=1.5708&po=1.5708&rd=280&tx=95&ty=315&tz=-128&xray=0", "user_p07_wrist": "?az=1.5708&po=1.5708&rd=240&tx=95&ty=235&tz=-128&xray=0", "user_elbow_front": "?az=-1.5708&po=1.5708&rd=250&tx=18.5&ty=310&tz=-128&xray=0", "user_p06_xz": "?az=1.5708&po=1.5708&rd=240&tx=18.5&ty=350&tz=-128&xray=0&hide=胸板,底脚,肩吊臂", "user_p06_yz": "?az=0&po=1.5708&rd=260&tx=18.5&ty=350&tz=-110&xray=0", "user_p06_iso": "?az=-0.65&po=1.25&rd=260&tx=-15&ty=350&tz=-128&xray=0", "user_p07_creases": "?az=-0.65&po=1.25&rd=260&tx=30&ty=270&tz=-130&xray=0", # 电机腔内壁(隐藏全部电机,从腔内看 R35/R30.15 两段同心圆弧是否连续) "bay_p06_inside": "?az=1.5708&po=1.5708&rd=250&tx=18.5&ty=345&tz=-128&xray=0&hide=J1,J2,J3,J4,J5,J6", "bay_p07_inside": "?az=1.5708&po=1.5708&rd=240&tx=18.5&ty=235&tz=-128&xray=0&hide=J1,J2,J3,J4,J5,J6", "bay_p08_inside": "?az=1.5708&po=1.5708&rd=210&tx=18.5&ty=175&tz=-128&xray=0&hide=J1,J2,J3,J4,J5,J6", "user_p04_side": "?az=1.5708&po=1.5708&rd=300&tx=18.5&ty=520&tz=-128&xray=0&hide=胸板,底脚,肩吊臂", "user_p04_bottom": "?az=0.5&po=2.0&rd=240&tx=18.5&ty=490&tz=-128&xray=0&hide=胸板,底脚,肩吊臂", "user_p04_bottom2": "?az=1.2&po=2.0&rd=260&tx=18.5&ty=490&tz=-128&xray=0&hide=胸板,底脚,肩吊臂", "user_shoulder_solid": "?cam=joint&az=2.1&po=1.35&rd=380&xray=0", "user_shoulder_xray": "?cam=joint&az=2.1&po=1.35&rd=380", "user_shoulder_top": "?cam=joint&az=-0.6&po=0.6&rd=360&xray=0", # P03 轻量化桁架版(孤立视图:只留 P03,隐藏其余零件/电机/螺钉/包络) "p03_light_iso": "?az=0.62&po=1.15&rd=300&tx=-70&ty=548&tz=-128&xray=0&hide=P01,P02,P04,P05,P06,P07,P08,P09,M1,M2,M3,M4,M5,M6,S0,S1,ENV", "p03_light_side": "?az=1.5708&po=1.5708&rd=320&tx=-70&ty=548&tz=-128&xray=0&hide=P01,P02,P04,P05,P06,P07,P08,P09,M1,M2,M3,M4,M5,M6,S0,S1,ENV", "p03_light_rear": "?az=0.0&po=1.45&rd=300&tx=-70&ty=548&tz=-128&xray=0&hide=P01,P02,P04,P05,P06,P07,P08,P09,M1,M2,M3,M4,M5,M6,S0,S1,ENV", "p03_light_join": "?az=0.62&po=1.15&rd=420&tx=18.5&ty=520&tz=-128&xray=0", "p03_light_xray": "?az=0.62&po=1.15&rd=300&tx=-70&ty=548&tz=-128&xray=1&hide=P01,P02,P04,P05,P06,P07,P08,P09,M1,M2,M3,M4,M5,M6,S0,S1,ENV", # 减重版(2026-10-02):P04 立柱槽 / P06 过渡块窗 / P07 顶板槽 的孤立视图 # 俯视看朝上敞开的槽口,斜视看槽壁与留下来的抗弯腹板 "hollow_p04_top": "?az=0.0&po=0.05&rd=250&tx=18.5&ty=585&tz=-128&xray=0&hide=P01,P02,P03,P05,P06,P07,P08,P09,M1,M2,M3,M4,M5,M6,S0,S1,ENV", "hollow_p04_iso": "?az=0.7&po=1.05&rd=280&tx=18.5&ty=585&tz=-128&xray=0&hide=P01,P02,P03,P05,P06,P07,P08,P09,M1,M2,M3,M4,M5,M6,S0,S1,ENV", "hollow_p07_top": "?az=0.0&po=0.05&rd=230&tx=18.5&ty=270&tz=-128&xray=0&hide=P01,P02,P03,P04,P05,P06,P08,P09,M1,M2,M3,M4,M5,M6,S0,S1,ENV", "hollow_p07_iso": "?az=0.7&po=1.05&rd=260&tx=18.5&ty=275&tz=-128&xray=0&hide=P01,P02,P03,P04,P05,P06,P08,P09,M1,M2,M3,M4,M5,M6,S0,S1,ENV", "hollow_p06_iso": "?az=0.7&po=1.05&rd=190&tx=18.5&ty=368&tz=-128&xray=0&hide=P01,P02,P03,P04,P05,P07,P08,P09,M1,M2,M3,M4,M5,M6,S0,S1,ENV", # P03↔立柱 安装接口(新版 4xM5 从腔体上方拧入):俯视看螺钉落位 "p03_mount_top": "?az=0.9&po=1.30&rd=190&tx=-115&ty=476&tz=-128&xray=0&hide=P02,P04,P05,P06,P07,P08,P09,M1,M2,M3,M4,M5,M6,ENV", "p03_mount_iso": "?az=2.2&po=0.95&rd=230&tx=-115&ty=486&tz=-128&xray=0&hide=P02,P04,P05,P06,P07,P08,P09,M1,M2,M3,M4,M5,M6,ENV", "p03_mount_loose": "?az=0.9&po=0.42&rd=260&tx=-115&ty=500&tz=-128&xray=0&expl=60&hide=P02,P04,P05,P06,P07,P08,P09,M1,M2,M3,M4,M5,M6,ENV", "j3_前端压盖_装配": "?az=0.7&po=2.72&rd=190&tx=19.5&ty=396&tz=-128&xray=0&hide=立柱,底座,肩关节安装悬臂,肩叉,大臂骨架,小臂骨架,腕部转接座,末端工具法兰,M1,M2,M4,M5,M6,包络经纬网", "j3_前端压盖_全件": "?az=0.7&po=2.6&rd=300&tx=19.5&ty=420&tz=-128&xray=0", # 承压面下沉 2mm 留下的"悬空凸台"(2026-10-04 整改):沿电机轴线从尾端面方向看, # 四个视图分别对应 P04/P05/P06/P07 的后端。ax/po 沿轴向,rd 拉近到 R35 座圈。 # 沿电机轴线从尾端面看过去:承压面(R35圆面) + 4xS02 沉孔,凸台消失后应是干净圆面 "boss_p04": "?az=0.55&po=1.30&rd=150&tx=19.5&ty=490&tz=-158&xray=0&hide=P01,P02,P03,P05,P06,P07,P08,P09,M3,M4,M5,M6,ENV", "boss_p05": "?az=1.5708&po=1.5708&rd=190&tx=18.5&ty=392&tz=-128&xray=0&hide=P01,P02,P03,P04,P06,P07,P08,P09,M2,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_front": f"file:///{DOCS}/sheet_front.svg", } os.makedirs(SHOTS, exist_ok=True) want = sys.argv[1:] or list(VIEWS) def take_shot(nm): q = VIEWS[nm] out = "%s/%s.png" % (SHOTS, nm) # 不预先删除旧图:Edge 的 --screenshot 本来就会覆盖,而 os.remove 在带 # safe-delete 垫片的环境里会把"删到回收站"的失败抛出来,直接打断整轮渲染。 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}") # 每次用独立 profile:共用默认 profile 时并发实例会互相抢锁,表现为随机 FAIL。 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", "--screenshot=" + out, target] r = subprocess.run(cmd, capture_output=True, timeout=180) shutil.rmtree(udd, ignore_errors=True) 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 print("%-12s %s %8s %s" % (nm, "OK " if ok else "FAIL", sz, err), flush=True) return ok 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)) print("edge:", EDGE) print("all shots ok:", all(results)) os._exit(0)