前端不再是"一个 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 穿透。
100 lines
4.9 KiB
Python
100 lines
4.9 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""生成可达外包络的 ENV 零件(经纬网单件)追加进 web/mesh_arm.json:
|
||
ENV_WL_包络经纬网 —— kind=line,LineSegments 渲染(经向 10°/条、纬向 8mm/环)
|
||
包络 = rmax(x) 母线绕 J1 轴(平行 X,过 (y=128, z=610))整周回转;grp=root 不随关节动。"""
|
||
import json
|
||
import math
|
||
import os
|
||
import sys
|
||
|
||
import numpy as np
|
||
|
||
from arm.paths import DATA, CACHE
|
||
from arm import kinematics as HL
|
||
|
||
Y_AX = HL.Y_AXIS # 128
|
||
Z_AX = HL.Z_RL # 610
|
||
|
||
|
||
def main():
|
||
d = np.load(CACHE + "/ws_cloud.npz")
|
||
xs, rmax = d["xs"], d["rmax"]
|
||
ok = ~np.isnan(rmax)
|
||
xs, rmax = xs[ok], rmax[ok]
|
||
|
||
def grid(step_x, dphi_deg):
|
||
"""经纬参数网格 -> verts(NX*NP,3), segs(扁平索引对), tris。"""
|
||
xs_e = np.arange(xs.min(), xs.max() + 1e-9, step_x)
|
||
rm_e = np.interp(xs_e, xs, rmax)
|
||
nph = int(round(360.0 / dphi_deg))
|
||
phg = np.linspace(0.0, 2.0 * math.pi, nph, endpoint=False)
|
||
nx = len(xs_e)
|
||
V = np.empty((nx * nph, 3))
|
||
for i, x in enumerate(xs_e):
|
||
r = rm_e[i]
|
||
for j, ph in enumerate(phg):
|
||
V[i * nph + j] = (x, Y_AX + r * math.cos(ph), Z_AX + r * math.sin(ph))
|
||
segs = []
|
||
for i in range(nx - 1): # 经线(沿 x)
|
||
for j in range(nph):
|
||
segs += [i * nph + j, (i + 1) * nph + j]
|
||
for i in range(nx): # 纬线(沿 φ)
|
||
for j in range(nph):
|
||
j2 = (j + 1) % nph
|
||
segs += [i * nph + j, i * nph + j2]
|
||
tris = []
|
||
for i in range(nx - 1):
|
||
for j in range(nph):
|
||
j2 = (j + 1) % nph
|
||
a, b = i * nph + j, i * nph + j2
|
||
c, e = (i + 1) * nph + j, (i + 1) * nph + j2
|
||
tris += [a, c, b, b, c, e]
|
||
return V, segs, tris
|
||
|
||
# 经纬网单件:经向 10°一条、纬向 8mm一环(无需贴片面,线自带罩体感)
|
||
Vl, sl, _tl = grid(8.0, 10.0)
|
||
|
||
# 自证:包络尺寸应等于 ws_metrics 极值。
|
||
# 【2026-10-05c 修正】原来是 <1.0 的硬比较,但网格 x 是从 xs.min() 起按 step_x
|
||
# 对齐的(np.arange),最后一个采样点最多比真极值短一个步长 —— 一旦
|
||
# (x_fwd - xs.min()) 不是 step_x 的整数倍,断言就会假失败(实测差 6.46mm,
|
||
# 正好 < 8.0),导致包络注不进去、查看器里"可达包络"整个消失。改为
|
||
# "不得超出、且至多少一个步长"这个真正成立的判据。
|
||
step_x = 8.0
|
||
met = json.load(open(CACHE + "/ws_metrics.json", encoding="utf-8"))
|
||
assert Vl[:, 0].min() >= met["x_back"] - 1e-6, (Vl[:, 0].min(), met["x_back"])
|
||
assert Vl[:, 0].min() <= met["x_back"] + step_x + 1e-6, (Vl[:, 0].min(), met["x_back"])
|
||
assert Vl[:, 0].max() <= met["x_fwd"] + 1e-6, (Vl[:, 0].max(), met["x_fwd"])
|
||
assert Vl[:, 0].max() >= met["x_fwd"] - step_x - 1e-6, (Vl[:, 0].max(), met["x_fwd"])
|
||
rl = np.hypot(Vl[:, 1] - Y_AX, Vl[:, 2] - Z_AX)
|
||
# rl.max() 就是 rmax(x) 网格插值后的峰值,与 met["r_max"](原始点云极值)之间
|
||
# 只允许有插值/取整量级的差(实测 0.044mm),故给 1.0mm 容差。
|
||
assert abs(rl.max() - met["r_max"]) < 1.0, (rl.max(), met["r_max"])
|
||
assert np.all((Vl[:, 1] - Y_AX) ** 2 + (Vl[:, 2] - Z_AX) ** 2
|
||
<= (np.interp(Vl[:, 0], xs, rmax) + 1e-6) ** 2), "line"
|
||
print("包络经纬网: 顶点%d 线段%d(经10° x 纬8mm)" % (len(Vl), len(sl) // 2))
|
||
|
||
# ---- 追加进 mesh_arm.json ----
|
||
path = DATA + "/mesh_arm.json"
|
||
data = json.load(open(path, encoding="utf-8"))
|
||
data["parts"] = [p for p in data["parts"] if not p["name"].startswith("ENV_")]
|
||
mx = max(p["idx"] for p in data["parts"])
|
||
note_ws = ("可达外包络(运动学,docs/travel.json 滑块限位下的全组合 FK 包络):"
|
||
"r=rmax(x) 绕 J1 轴整周回转。高 %.0f~%.0f mm,前伸 %.0f / 后伸 %.0f mm,"
|
||
"离 J1 轴最远 %.0f mm;包络内含死区(肩下折不进),多关节同极位由控制器避碰。"
|
||
% (met["z_min"], met["z_max"], met["x_fwd"], met["x_back"], met["r_max"]))
|
||
line = dict(name="ENV_WL_包络经纬网", cn="包络经纬网", v=[list(p) for p in Vl],
|
||
s=sl, color=0x60a5fa, printed=False, vol=round(met["vol_dm3"] * 1e6, 1),
|
||
note=note_ws + " 经纬线:经向 10°/条,纬向 8mm/环。",
|
||
grp="root", jaw=0.0, ex=[0.0, 0.0, 0.0], idx=mx + 1,
|
||
kind="line")
|
||
data["parts"] += [line]
|
||
tmp = path + ".tmp"
|
||
with open(tmp, "w", encoding="utf-8") as f:
|
||
json.dump(data, f, ensure_ascii=False, separators=(",", ":"))
|
||
os.replace(tmp, path)
|
||
print("已写入", path, "大小 %.1f MB" % (os.path.getsize(path) / 1e6))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main() |