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 穿透。
@@ -21,17 +21,19 @@ Desktop.ini
|
|||||||
|
|
||||||
# 构建产物:由 python -m arm.* 重新生成,体积大(mesh 14MB + 单文件查看器 16MB×3),
|
# 构建产物:由 python -m arm.* 重新生成,体积大(mesh 14MB + 单文件查看器 16MB×3),
|
||||||
# 过去被跟踪导致仓库背着 47MB 生成物;取消跟踪后重建不再脏 git status。
|
# 过去被跟踪导致仓库背着 47MB 生成物;取消跟踪后重建不再脏 git status。
|
||||||
web/mesh_arm.json
|
# 旧 web/ 单文件产物目录(已删,源码在 webapp/)
|
||||||
web/sheets.jsdata
|
web/
|
||||||
web/index.html
|
|
||||||
web/机器人手臂3D查看器.html
|
|
||||||
/机器人手臂3D查看器.html
|
/机器人手臂3D查看器.html
|
||||||
webapp/data/
|
webapp/data/
|
||||||
|
webapp/generated/
|
||||||
dist/
|
dist/
|
||||||
data/cache/
|
data/cache/
|
||||||
|
|
||||||
# 这 3 张线束 x-ray 截图在同一份构建下就不逐字节复现(抗锯齿噪声),
|
# 这 6 张在同一份构建下不逐字节复现(抗锯齿/量化噪声),
|
||||||
# 每次跑关卡都会改写它们;参考像素已存进 tests/baselines/flaky/,不再入库。
|
# 每次跑关卡都会改写它们;参考像素已存进 tests/baselines/flaky/,不再入库。
|
||||||
docs/shots/harness_p04_p05_xray.png
|
docs/shots/harness_p04_p05_xray.png
|
||||||
docs/shots/harness_p07_xray.png
|
docs/shots/harness_p07_xray.png
|
||||||
docs/shots/harness_xray_internal.png
|
docs/shots/harness_xray_internal.png
|
||||||
|
docs/shots/boss_p07.png
|
||||||
|
docs/shots/m3_sheet_exploded.png
|
||||||
|
docs/shots/p03_mount_loose.png
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import sys
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from arm.paths import WEB, CACHE
|
from arm.paths import DATA, CACHE
|
||||||
from arm import kinematics as HL
|
from arm import kinematics as HL
|
||||||
|
|
||||||
Y_AX = HL.Y_AXIS # 128
|
Y_AX = HL.Y_AXIS # 128
|
||||||
@@ -75,7 +75,7 @@ def main():
|
|||||||
print("包络经纬网: 顶点%d 线段%d(经10° x 纬8mm)" % (len(Vl), len(sl) // 2))
|
print("包络经纬网: 顶点%d 线段%d(经10° x 纬8mm)" % (len(Vl), len(sl) // 2))
|
||||||
|
|
||||||
# ---- 追加进 mesh_arm.json ----
|
# ---- 追加进 mesh_arm.json ----
|
||||||
path = WEB + "/mesh_arm.json"
|
path = DATA + "/mesh_arm.json"
|
||||||
data = json.load(open(path, encoding="utf-8"))
|
data = json.load(open(path, encoding="utf-8"))
|
||||||
data["parts"] = [p for p in data["parts"] if not p["name"].startswith("ENV_")]
|
data["parts"] = [p for p in data["parts"] if not p["name"].startswith("ENV_")]
|
||||||
mx = max(p["idx"] for p in data["parts"])
|
mx = max(p["idx"] for p in data["parts"])
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ try:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
import cadquery as cq
|
import cadquery as cq
|
||||||
from arm.paths import OUT, WEB, DOCS
|
from arm.paths import OUT, DATA, DOCS
|
||||||
from arm.cad.helpers import solids_of, bb, mesh_of
|
from arm.cad.helpers import solids_of, bb, mesh_of
|
||||||
from OCP.BRepAlgoAPI import BRepAlgoAPI_Common
|
from OCP.BRepAlgoAPI import BRepAlgoAPI_Common
|
||||||
from OCP.BRepGProp import BRepGProp
|
from OCP.BRepGProp import BRepGProp
|
||||||
@@ -156,7 +156,7 @@ def section_props(sh, normal, origin, bend=2, size=400.0):
|
|||||||
|
|
||||||
def arm_masses():
|
def arm_masses():
|
||||||
"""Everything distal to each joint, in grams, from the model itself."""
|
"""Everything distal to each joint, in grams, from the model itself."""
|
||||||
meta = json.load(io.open(WEB + "/mesh_arm.json", encoding="utf-8"))
|
meta = json.load(io.open(DATA + "/mesh_arm.json", encoding="utf-8"))
|
||||||
TREE = ["root", "J1", "J2", "J3", "J4", "J5", "J6"]
|
TREE = ["root", "J1", "J2", "J3", "J4", "J5", "J6"]
|
||||||
tot = {}
|
tot = {}
|
||||||
for p in meta["parts"]:
|
for p in meta["parts"]:
|
||||||
@@ -199,7 +199,7 @@ def _mesh_vol_cg(v, t):
|
|||||||
|
|
||||||
def part_table(payload_kg=None):
|
def part_table(payload_kg=None):
|
||||||
"""[(name, mass_g, cg_xyz, grp)],含末端负载这个集中质量点。"""
|
"""[(name, mass_g, cg_xyz, grp)],含末端负载这个集中质量点。"""
|
||||||
meta = json.load(io.open(WEB + "/mesh_arm.json", encoding="utf-8"))
|
meta = json.load(io.open(DATA + "/mesh_arm.json", encoding="utf-8"))
|
||||||
rows = []
|
rows = []
|
||||||
for p in meta["parts"]:
|
for p in meta["parts"]:
|
||||||
nm = p["name"]
|
nm = p["name"]
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ bore + PCBA pocket + pinch screws; driven hub mating plane at motor rear datum
|
|||||||
the existing face-spline halves + split clamp bolt straight on), the part
|
the existing face-spline halves + split clamp bolt straight on), the part
|
||||||
registry, and the STEP/STL/mesh/BOM/collision export pipeline.
|
registry, and the STEP/STL/mesh/BOM/collision export pipeline.
|
||||||
|
|
||||||
Run: python arm_model.py -> out/, web/mesh_arm.json, docs/
|
Run: python -m arm.build_model -> out/, webapp/data/mesh_arm.json, docs/
|
||||||
"""
|
"""
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
@@ -22,7 +22,7 @@ except Exception:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
import cadquery as cq
|
import cadquery as cq
|
||||||
from arm.paths import OUT, WEB, DOCS, SRC_STEP, HALF_STEP, CLAMP_STEP
|
from arm.paths import OUT, WEB, DATA, DOCS, SRC_STEP, HALF_STEP, CLAMP_STEP
|
||||||
from arm.constants import M, C, MOTOR_TOL
|
from arm.constants import M, C, MOTOR_TOL
|
||||||
from arm.cad.helpers import V, trs, place, bb, bbox_str, cyl, ring, box, holes, solids_of, mesh_of
|
from arm.cad.helpers import V, trs, place, bb, bbox_str, cyl, ring, box, holes, solids_of, mesh_of
|
||||||
from OCP.gp import gp_Trsf, gp_Vec, gp_Pnt, gp_Dir, gp_Ax1
|
from OCP.gp import gp_Trsf, gp_Vec, gp_Pnt, gp_Dir, gp_Ax1
|
||||||
@@ -460,7 +460,7 @@ import time
|
|||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
def export(JS):
|
def export(JS):
|
||||||
for d in (OUT, OUT + "/parts_step", OUT + "/print_stl", OUT + "/assembly", WEB, DOCS):
|
for d in (OUT, OUT + "/parts_step", OUT + "/print_stl", OUT + "/assembly", DATA, DOCS):
|
||||||
os.makedirs(d, exist_ok=True)
|
os.makedirs(d, exist_ok=True)
|
||||||
|
|
||||||
print("=" * 80, flush=True)
|
print("=" * 80, flush=True)
|
||||||
@@ -499,7 +499,7 @@ def export(JS):
|
|||||||
|
|
||||||
# 2. 多线程并行生成网页 3D Mesh
|
# 2. 多线程并行生成网页 3D Mesh
|
||||||
print("=" * 80, flush=True)
|
print("=" * 80, flush=True)
|
||||||
print("【第三阶段 3/5】多线程并行生成网页 3D 轻量化几何网格 (web/mesh_arm.json)...", flush=True)
|
print("【第三阶段 3/5】多线程并行生成网页 3D 轻量化几何网格 (webapp/data/mesh_arm.json)...", flush=True)
|
||||||
print("=" * 80, flush=True)
|
print("=" * 80, flush=True)
|
||||||
|
|
||||||
done_mesh = 0
|
done_mesh = 0
|
||||||
@@ -541,12 +541,12 @@ def export(JS):
|
|||||||
L1=round(d3(JS["J2"].rear, JS["J4"].rear), 1),
|
L1=round(d3(JS["J2"].rear, JS["J4"].rear), 1),
|
||||||
L2=round(d3(JS["J4"].rear, JS["J5"].rear), 1)),
|
L2=round(d3(JS["J4"].rear, JS["J5"].rear), 1)),
|
||||||
measured=dict(M=M, C=C, HUB_FACE_Z=HUB_FACE_Z, CLAMP_SPAN=list(CLAMP_SPAN))),
|
measured=dict(M=M, C=C, HUB_FACE_Z=HUB_FACE_Z, CLAMP_SPAN=list(CLAMP_SPAN))),
|
||||||
open(WEB + "/mesh_arm.json", "w", encoding="utf-8"), ensure_ascii=False)
|
open(DATA + "/mesh_arm.json", "w", encoding="utf-8"), ensure_ascii=False)
|
||||||
bom = [dict(件号=p["name"], 名称=p["cn"], 类型="3D打印" if p["printed"] else "已有/电机",
|
bom = [dict(件号=p["name"], 名称=p["cn"], 类型="3D打印" if p["printed"] else "已有/电机",
|
||||||
数量=1, 体积cm3=round(p["vol"] / 1000.0, 1),
|
数量=1, 体积cm3=round(p["vol"] / 1000.0, 1),
|
||||||
质量g_PLA=round(p["vol"] * 1.24e-3, 1), 包络=p["bb"], 说明=p["note"]) for p in PARTS]
|
质量g_PLA=round(p["vol"] * 1.24e-3, 1), 包络=p["bb"], 说明=p["note"]) for p in PARTS]
|
||||||
json.dump(bom, open(DOCS + "/bom.json", "w", encoding="utf-8"), ensure_ascii=False, indent=1)
|
json.dump(bom, open(DOCS + "/bom.json", "w", encoding="utf-8"), ensure_ascii=False, indent=1)
|
||||||
print(f"-> 3D网格与BOM清单已保存至 web/mesh_arm.json 与 docs/bom.json ✓\n", flush=True)
|
print(f"-> 3D网格与BOM清单已保存至 webapp/data/mesh_arm.json 与 docs/bom.json ✓\n", flush=True)
|
||||||
return bom
|
return bom
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
|
||||||
from arm.paths import WEB, DOCS
|
from arm.paths import DATA, DOCS
|
||||||
|
|
||||||
# 这两个脚本的顶层就是"算完并写文件",被 import 时会静默重写 travel/mesh 派生产物
|
# 这两个脚本的顶层就是"算完并写文件",被 import 时会静默重写 travel/mesh 派生产物
|
||||||
# (实测:一次 importlib 冒烟测试就把 docs/travel.json 重写了一遍)。这里改成直接炸。
|
# (实测:一次 importlib 冒烟测试就把 docs/travel.json 重写了一遍)。这里改成直接炸。
|
||||||
@@ -26,7 +26,7 @@ if __name__ != "__main__":
|
|||||||
"draw_sheet 是脚本:顶层代码一执行就重写产物,不能 import。"
|
"draw_sheet 是脚本:顶层代码一执行就重写产物,不能 import。"
|
||||||
"要用就跑 python -m arm.dataio.draw_sheet。")
|
"要用就跑 python -m arm.dataio.draw_sheet。")
|
||||||
|
|
||||||
D = json.load(io.open(WEB + "/mesh_arm.json", encoding="utf-8"))
|
D = json.load(io.open(DATA + "/mesh_arm.json", encoding="utf-8"))
|
||||||
P, LAY, MS = D["parts"], D["layout"], D["measured"]
|
P, LAY, MS = D["parts"], D["layout"], D["measured"]
|
||||||
MT, CT = MS["M"], MS["C"]
|
MT, CT = MS["M"], MS["C"]
|
||||||
JOINTS = {j["id"]: j for j in D["joints"]}
|
JOINTS = {j["id"]: j for j in D["joints"]}
|
||||||
@@ -333,6 +333,5 @@ sheets = [
|
|||||||
os.makedirs(DOCS, exist_ok=True)
|
os.makedirs(DOCS, exist_ok=True)
|
||||||
for n, s in zip(("sheet_front", "sheet_joint", "sheet_exploded", "sheet_side"), sheets):
|
for n, s in zip(("sheet_front", "sheet_joint", "sheet_exploded", "sheet_side"), sheets):
|
||||||
io.open(DOCS + "/" + n + ".svg", "w", encoding="utf-8").write(s)
|
io.open(DOCS + "/" + n + ".svg", "w", encoding="utf-8").write(s)
|
||||||
io.open(WEB + "/sheets.jsdata", "w", encoding="utf-8").write(json.dumps(sheets, ensure_ascii=False))
|
print("sheets: %d (%d KB) -> docs/sheet_{front,joint,exploded,side}.svg(查看器读这 4 个文件)" % (
|
||||||
print("sheets: %d (%d KB) -> docs/sheet_{front,joint,exploded,side}.svg" % (
|
|
||||||
len(sheets), sum(len(s) for s in sheets) // 1024))
|
len(sheets), sum(len(s) for s in sheets) // 1024))
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
docs/screws.json 是紧固件清单的唯一事实源(规格 / 长度 / 数量 / 位置 / 备注)。
|
docs/screws.json 是紧固件清单的唯一事实源(规格 / 长度 / 数量 / 位置 / 备注)。
|
||||||
本模块把清单里的每一颗螺钉按其真实装配孔位还原成三维实例(承载头面坐标 + 拧入方向 +
|
本模块把清单里的每一颗螺钉按其真实装配孔位还原成三维实例(承载头面坐标 + 拧入方向 +
|
||||||
随动关节组),生成轻量三角网格后追加进 web/mesh_arm.json 的 parts 中。
|
随动关节组),生成轻量三角网格后追加进 webapp/data/mesh_arm.json 的 parts 中。
|
||||||
查看器因此可以:显示全部螺钉、点击 BOM 螺丝行高亮定位、点击螺钉反查清单。
|
查看器因此可以:显示全部螺钉、点击 BOM 螺丝行高亮定位、点击螺钉反查清单。
|
||||||
|
|
||||||
实例坐标来源(与 arm_model.py 成孔代码一一对应,改模型后按同样规则复核):
|
实例坐标来源(与 arm_model.py 成孔代码一一对应,改模型后按同样规则复核):
|
||||||
@@ -51,7 +51,7 @@ try:
|
|||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
from arm.paths import WEB, DOCS
|
from arm.paths import DATA, DOCS
|
||||||
from arm import kinematics as HL
|
from arm import kinematics as HL
|
||||||
from arm.cad.part_tools import CLAMP_BCD, CLAMP_CLOCK
|
from arm.cad.part_tools import CLAMP_BCD, CLAMP_CLOCK
|
||||||
X_COL = -115.0 # 立柱中心(与 parts/p01_column.py 的 X_COL 一致)
|
X_COL = -115.0 # 立柱中心(与 parts/p01_column.py 的 X_COL 一致)
|
||||||
@@ -287,7 +287,7 @@ def instances(m2_bcd, m2_clock):
|
|||||||
|
|
||||||
# ---------------------------------------------------------------- 注入
|
# ---------------------------------------------------------------- 注入
|
||||||
def main():
|
def main():
|
||||||
path = WEB + "/mesh_arm.json"
|
path = DATA + "/mesh_arm.json"
|
||||||
print("读取 %s ..." % path, flush=True)
|
print("读取 %s ..." % path, flush=True)
|
||||||
d = json.load(open(path, encoding="utf-8"))
|
d = json.load(open(path, encoding="utf-8"))
|
||||||
M = d["measured"]["M"]
|
M = d["measured"]["M"]
|
||||||
|
|||||||
@@ -15,10 +15,14 @@ SRC_STEP = MODEL + r"/电机.STEP"
|
|||||||
HALF_STEP = MODEL + r"/配件/端面齿半联轴器_相同件x2.STEP"
|
HALF_STEP = MODEL + r"/配件/端面齿半联轴器_相同件x2.STEP"
|
||||||
CLAMP_STEP = MODEL + r"/配件/端面齿联轴器_抱箍_两片装配.STEP"
|
CLAMP_STEP = MODEL + r"/配件/端面齿联轴器_抱箍_两片装配.STEP"
|
||||||
OUT = REPO + r"/out"
|
OUT = REPO + r"/out"
|
||||||
WEB = REPO + r"/web"
|
WEB = REPO + r"/web" # 旧单文件产物目录,S8 删除;新代码不要再往里写
|
||||||
|
# 查看器运行时数据与生成脚本(gitignore,由 build_viewer 重写)
|
||||||
|
DATA = REPO + r"/webapp/data"
|
||||||
|
GEN = REPO + r"/webapp/generated"
|
||||||
DOCS = REPO + r"/docs"
|
DOCS = REPO + r"/docs"
|
||||||
# 可达包络 FK 采样的缓存(ws_cloud.npz / ws_metrics.json)。原先躺在 gitignore 的
|
# 可达包络 FK 采样的缓存(ws_cloud.npz / ws_metrics.json)。原先躺在 gitignore 的
|
||||||
# scratch/ 里,构建却依赖它——缺了新克隆就跑不出 ENV 件。
|
# scratch/ 里,构建却依赖它——缺了新克隆就跑不出 ENV 件。
|
||||||
CACHE = REPO + r"/data/cache"
|
CACHE = REPO + r"/data/cache"
|
||||||
# 查看器模板。S4 拆成 webapp/index.html + js/** 后这条即作废。
|
# 前端源码目录与分发产物
|
||||||
TPL = REPO + r"/src/viewer_tpl.html"
|
WEBAPP = REPO + r"/webapp"
|
||||||
|
DIST = REPO + r"/dist"
|
||||||
|
|||||||
@@ -1,76 +1,98 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""Assemble the single-file viewer: three.js (inline, no CDN) + mesh data + 2D sheets."""
|
"""把生成物落到 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 io
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
from arm.paths import WEB, DOCS, REPO, TPL
|
|
||||||
|
|
||||||
import time
|
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():
|
def rebuild_viewer():
|
||||||
print("=" * 80, flush=True)
|
"""build_model 流水线的最后一步(沿用旧函数名,调用点不改)。"""
|
||||||
print("【第五阶段 5/5】打包单文件三维交互查看器 (机器人手臂3D查看器.html)...", flush=True)
|
write_data()
|
||||||
print("=" * 80, flush=True)
|
build_single()
|
||||||
t0 = time.time()
|
|
||||||
print(" - 读取 Three.js 离线 3D 渲染内核...", end="", flush=True)
|
|
||||||
three = io.open(WEB + "/three_inline.js", encoding="utf-8").read()
|
|
||||||
assert len(three) > 400000, len(three)
|
|
||||||
print(f" 完成 ({len(three)/1024:.0f} KB) ✓", flush=True)
|
|
||||||
|
|
||||||
print(" - 读取几何网格与关节运动学数据...", end="", flush=True)
|
|
||||||
data = io.open(WEB + "/mesh_arm.json", encoding="utf-8").read()
|
|
||||||
sheets = io.open(WEB + "/sheets.jsdata", encoding="utf-8").read()
|
|
||||||
trv = io.open(DOCS + "/travel.json", encoding="utf-8").read() if os.path.exists(
|
|
||||||
DOCS + "/travel.json") else "[]"
|
|
||||||
col = io.open(DOCS + "/collide.json", encoding="utf-8").read() if os.path.exists(
|
|
||||||
DOCS + "/collide.json") else '{"caps":[],"parts":{}}'
|
|
||||||
screws = io.open(DOCS + "/screws.json", encoding="utf-8").read() if os.path.exists(
|
|
||||||
DOCS + "/screws.json") else "[]"
|
|
||||||
ik_solver = io.open(WEB + "/arm-ik-solver.js", encoding="utf-8").read()
|
|
||||||
ik_glue = io.open(WEB + "/ik-glue.js", encoding="utf-8").read()
|
|
||||||
tpl = io.open(TPL, encoding="utf-8").read()
|
|
||||||
print(f" 完成 ({len(data)/1024:.0f} KB) ✓", flush=True)
|
|
||||||
|
|
||||||
print(" - 打包生成全内置单文件 HTML...", end="", flush=True)
|
|
||||||
out = (tpl.replace("/*__THREE__*/", three)
|
|
||||||
.replace("/*__DATA__*/", data)
|
|
||||||
.replace("/*__SHEETS__*/", sheets)
|
|
||||||
.replace("/*__TRAVEL__*/", trv)
|
|
||||||
.replace("/*__COLLIDE__*/", col)
|
|
||||||
.replace("/*__SCREWS__*/", screws)
|
|
||||||
.replace("/*__IKSOLVER__*/", ik_solver)
|
|
||||||
.replace("/*__IKGLUE__*/", ik_glue))
|
|
||||||
assert "/*__" not in out, [l for l in out.split("\n") if "/*__" in l][:3]
|
|
||||||
dst = WEB + "/机器人手臂3D查看器.html"
|
|
||||||
# 【2026-10-06】三个副本逐个写、逐个报错,不要一把梭。
|
|
||||||
# 原来三行连写,任一份被浏览器独占(Windows 上打开中的 html 是独占锁)就抛异常,
|
|
||||||
# 而调用方 arm_model.py 把整个 rebuild_viewer() 包在 try/except 里,
|
|
||||||
# 于是只写成功第一份、另外两份静默停在旧版本 —— 表现为"代码改了但网页没变"。
|
|
||||||
# 实测就是这个坑:18:52 那次 dst 是新的,web/index.html 和根目录那份还停在 18:12。
|
|
||||||
ok, failed = True, []
|
|
||||||
for path in (dst, WEB + "/index.html", REPO + "/机器人手臂3D查看器.html"):
|
|
||||||
try:
|
|
||||||
io.open(path, "w", encoding="utf-8").write(out)
|
|
||||||
except Exception as e:
|
|
||||||
ok = False
|
|
||||||
failed.append("%s (%s)" % (path, e))
|
|
||||||
sz_mb = os.path.getsize(dst) / 1e6
|
|
||||||
print(f" 完成 ({sz_mb:.2f} MB, 耗时: {time.time()-t0:.2f}s) ✓", flush=True)
|
|
||||||
if failed:
|
|
||||||
print("!! 以下副本写入失败(多半是浏览器正打开着该文件,Windows 独占锁):", flush=True)
|
|
||||||
for f in failed:
|
|
||||||
print(" - %s" % f, flush=True)
|
|
||||||
print(" 关掉对应页面后重跑 python -m arm.viewer.build_viewer 即可。\n", flush=True)
|
|
||||||
else:
|
|
||||||
print(f"-> 3D查看器已更新至: {dst}、{WEB}/index.html 与 根目录/机器人手臂3D查看器.html\n",
|
|
||||||
flush=True)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
rebuild_viewer()
|
rebuild_viewer()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""查看器的本地静态服务器(前端拆分后,多文件 + ES Module + Worker 都需要 http 才能跑)。
|
||||||
|
|
||||||
|
python -m arm.viewer.serve [--port 8123] [--print-only]
|
||||||
|
|
||||||
|
要点:
|
||||||
|
- docroot = 仓库根,`/` 直接给 webapp/index.html;/docs/*.json 这类数据同域可读。
|
||||||
|
- Cache-Control: no-store —— 重新生成后旧标签页仍显示旧版是这里踩过的坑,
|
||||||
|
过去只能人肉 Ctrl+F5;不缓存之后刷新即新构建。
|
||||||
|
- 显式 MIME:Windows 上 .js 常被注册成 text/plain,那会让 <script type=module>
|
||||||
|
静默不执行(页面全黑且没有明显报错),所以这里写死。
|
||||||
|
- 屏蔽 .git / backups / 定位图 / out 等目录,不把整个仓库暴露到端口上。
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||||
|
from arm.paths import REPO
|
||||||
|
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
DENY = (".git", "backups", ".bak", "定位图", "out", "scratch", "node_modules")
|
||||||
|
MIME = {
|
||||||
|
".js": "text/javascript; charset=utf-8",
|
||||||
|
".mjs": "text/javascript; charset=utf-8",
|
||||||
|
".json": "application/json; charset=utf-8",
|
||||||
|
".svg": "image/svg+xml",
|
||||||
|
".css": "text/css; charset=utf-8",
|
||||||
|
".html": "text/html; charset=utf-8",
|
||||||
|
".npz": "application/octet-stream",
|
||||||
|
".png": "image/png",
|
||||||
|
".wasm": "application/wasm",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class H(SimpleHTTPRequestHandler):
|
||||||
|
# HTTP/1.1 + keep-alive + 大缓冲:默认 HTTP/1.0 每请求一条连接,
|
||||||
|
# 4 个 headless Edge 同时各拉 14MB 数据脚本时会把页面加载拖到截图之后。
|
||||||
|
protocol_version = "HTTP/1.1"
|
||||||
|
wbufsize = 1 << 20
|
||||||
|
# 按 (路径, mtime) 缓存字节:同一份 14MB 数据在一次批量截图里要发几十次。
|
||||||
|
_CACHE = {}
|
||||||
|
|
||||||
|
def __init__(self, *a, **k):
|
||||||
|
super().__init__(*a, directory=REPO, **k)
|
||||||
|
|
||||||
|
def _cached_bytes(self, path):
|
||||||
|
try:
|
||||||
|
st = os.stat(path)
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
key = (path, st.st_mtime_ns, st.st_size)
|
||||||
|
body = H._CACHE.get(key)
|
||||||
|
if body is None:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
body = f.read()
|
||||||
|
if st.st_size <= 64 << 20:
|
||||||
|
if len(H._CACHE) > 12:
|
||||||
|
H._CACHE.clear()
|
||||||
|
H._CACHE[key] = body
|
||||||
|
return body
|
||||||
|
|
||||||
|
def send_head(self):
|
||||||
|
rel = self.path.split("?", 1)[0].split("#", 1)[0].lstrip("/").replace("\\", "/")
|
||||||
|
top = rel.split("/")[0].lower()
|
||||||
|
if top in [d.lower() for d in DENY] or ".." in rel:
|
||||||
|
self.send_error(404, "denied")
|
||||||
|
return None
|
||||||
|
fs = self.translate_path(self.path)
|
||||||
|
if os.path.isfile(fs):
|
||||||
|
body = self._cached_bytes(fs)
|
||||||
|
if body is not None:
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", self.guess_type(fs))
|
||||||
|
self.send_header("Content-Length", str(len(body)))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(body)
|
||||||
|
return None
|
||||||
|
return super().send_head()
|
||||||
|
|
||||||
|
def guess_type(self, path):
|
||||||
|
ext = os.path.splitext(path)[1].lower()
|
||||||
|
return MIME.get(ext) or super().guess_type(path)
|
||||||
|
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
"""`/` 必须 302 到 /webapp/index.html,不能在原地把内容发出去:
|
||||||
|
那样文档基址是 `/`,页面里所有相对路径(styles/… js/… vendor/…)都会
|
||||||
|
解析到 /styles/base.css 之类而 404 —— 实测整页样式与脚本全丢。"""
|
||||||
|
if self.path.split("?", 1)[0].rstrip("/") in ("", "/"):
|
||||||
|
q = self.path.split("?", 1)[1] if "?" in self.path else ""
|
||||||
|
self.send_response(302)
|
||||||
|
self.send_header("Location", "/webapp/index.html" + ("?" + q if q else ""))
|
||||||
|
self.send_header("Content-Length", "0")
|
||||||
|
self.end_headers()
|
||||||
|
return
|
||||||
|
super().do_GET()
|
||||||
|
|
||||||
|
def end_headers(self):
|
||||||
|
self.send_header("Cache-Control", "no-store")
|
||||||
|
super().end_headers()
|
||||||
|
|
||||||
|
def log_message(self, fmt, *args):
|
||||||
|
sys.stderr.write("%s %s\n" % (self.address_string(), fmt % args))
|
||||||
|
|
||||||
|
|
||||||
|
def serve(port=8123):
|
||||||
|
srv = None
|
||||||
|
for p in range(port, port + 20):
|
||||||
|
try:
|
||||||
|
srv = ThreadingHTTPServer(("127.0.0.1", p), H)
|
||||||
|
port = p
|
||||||
|
break
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
if srv is None:
|
||||||
|
raise SystemExit("端口 %d..%d 都被占用" % (port, port + 19))
|
||||||
|
url = "http://127.0.0.1:%d/" % port
|
||||||
|
print("docroot %s" % REPO, flush=True)
|
||||||
|
print("查看器: %s (Ctrl+C 停止;已禁用缓存,改完刷新即新构建)" % url, flush=True)
|
||||||
|
srv.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
ap = argparse.ArgumentParser(description="查看器本地静态服务器")
|
||||||
|
ap.add_argument("--port", type=int, default=8123)
|
||||||
|
serve(ap.parse_args().port)
|
||||||
@@ -11,9 +11,15 @@ try:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
from arm.paths import WEB, DOCS
|
from arm.paths import DOCS, REPO
|
||||||
SHOTS = DOCS + "/shots"
|
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",
|
CAND = [r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
|
||||||
r"C:\Program Files\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)
|
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",
|
"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_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",
|
"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_side": f"http://127.0.0.1:{PORT}/docs/sheet_side.svg",
|
||||||
"svg_joint": f"file:///{DOCS}/sheet_joint.svg",
|
"svg_joint": f"http://127.0.0.1:{PORT}/docs/sheet_joint.svg",
|
||||||
"svg_exploded": f"file:///{DOCS}/sheet_exploded.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_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",
|
"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",
|
"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)
|
out = "%s/%s.png" % (SHOTS, nm)
|
||||||
import time, tempfile
|
import time, tempfile
|
||||||
t_bust = int(time.time() * 1000)
|
t_bust = int(time.time() * 1000)
|
||||||
target = q if q.startswith("file://") else "file:///" + HTML.replace("\\", "/") + q + (
|
# serve.py 已下发 Cache-Control: no-store,不再需要 _t= 破缓存
|
||||||
f"&still=1&_t={t_bust}" if "?" in q else f"?still=1&_t={t_bust}")
|
target = q if q.startswith("http") else BASE + q + (
|
||||||
|
"&still=1" if "?" in q else "?still=1")
|
||||||
udd = tempfile.mkdtemp(prefix="eshot")
|
udd = tempfile.mkdtemp(prefix="eshot")
|
||||||
cmd = [EDGE, "--headless=new", "--use-gl=angle", "--use-angle=d3d11",
|
cmd = [EDGE, "--headless=new", "--use-gl=angle", "--use-angle=d3d11",
|
||||||
"--disable-cache", "--disable-application-cache", "--disk-cache-size=0",
|
"--disable-cache", "--disable-application-cache", "--disk-cache-size=0",
|
||||||
"--user-data-dir=" + udd,
|
"--user-data-dir=" + udd,
|
||||||
"--enable-gpu-rasterization", "--ignore-gpu-blocklist",
|
"--enable-gpu-rasterization", "--ignore-gpu-blocklist",
|
||||||
"--hide-scrollbars", "--force-device-scale-factor=1",
|
"--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]
|
"--screenshot=" + out, target]
|
||||||
r = subprocess.run(cmd, capture_output=True, timeout=180)
|
r = subprocess.run(cmd, capture_output=True, timeout=180)
|
||||||
shutil.rmtree(udd, ignore_errors=True)
|
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)
|
print("%-18s %s %8s %s" % (nm, "OK " if ok else "FAIL", sz, err), flush=True)
|
||||||
return ok
|
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__":
|
if __name__ == "__main__":
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
with ThreadPoolExecutor(max_workers=min(4, os.cpu_count() or 2)) as executor:
|
srv = ensure_server()
|
||||||
results = list(executor.map(take_shot, want))
|
try:
|
||||||
|
with ThreadPoolExecutor(max_workers=WORKERS) as executor:
|
||||||
|
results = list(executor.map(take_shot, want))
|
||||||
|
finally:
|
||||||
|
if srv:
|
||||||
|
srv.terminate()
|
||||||
|
|
||||||
print("edge:", EDGE)
|
print("edge:", EDGE)
|
||||||
print("all shots ok:", all(results))
|
print("all shots ok:", all(results))
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 131 KiB |
@@ -3,4 +3,8 @@ c66c230557548bd03f003bb4bd52ff6a travel_detail.json
|
|||||||
4a8f14be95ed32121166a9bd37e74752 collide.json
|
4a8f14be95ed32121166a9bd37e74752 collide.json
|
||||||
82f8fa459be497ae0cfc92d5816497bf screws.json
|
82f8fa459be497ae0cfc92d5816497bf screws.json
|
||||||
83dfad6817c766ee893f54cca8fa7774 mesh_arm.json
|
83dfad6817c766ee893f54cca8fa7774 mesh_arm.json
|
||||||
f74cf5148cfc03c2d785829f45a608f9 sheets.jsdata
|
d94da355770a8b3da81f762bbaf3c843 00_data.js
|
||||||
|
ba7342b1c6dd3f6d6895ddec3e5736af sheet_front.svg
|
||||||
|
b12e59f6affee2ddc0e7d175922f7037 sheet_joint.svg
|
||||||
|
de9cd78f6070ec124b3ebcb116809d19 sheet_exploded.svg
|
||||||
|
f2504dea7e1a0e5b140f524b7570d962 sheet_side.svg
|
||||||
|
|||||||
|
After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 294 KiB After Width: | Height: | Size: 294 KiB |
|
Before Width: | Height: | Size: 363 KiB After Width: | Height: | Size: 363 KiB |
|
Before Width: | Height: | Size: 296 KiB After Width: | Height: | Size: 296 KiB |
|
Before Width: | Height: | Size: 184 KiB After Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 153 KiB |
@@ -1,19 +1,31 @@
|
|||||||
{
|
{
|
||||||
"note": "实测同一构建两轮:mean<=0.00174, 单像素最大差 12, 无像素差>8。阈值取约 10x 余量;超过即视为真实渲染回归。",
|
"note": "同一构建两轮实测:mean<=0.0019、单像素最大差 7、无像素差>8。阈值 mean<=0.02 / 单像素<=32 约留 20x 余量;超过即视为真实渲染回归。p03_mount_loose 是 ±1 量化噪声(曾两轮碰巧逐字节相同而漏进稳定清单)。",
|
||||||
"mean_max": 0.02,
|
"mean_max": 0.02,
|
||||||
"pixel_max": 32,
|
"pixel_max": 32,
|
||||||
"views": {
|
"views": {
|
||||||
|
"m3_sheet_exploded": {
|
||||||
|
"mean": 7e-05,
|
||||||
|
"mx": 1
|
||||||
|
},
|
||||||
|
"boss_p07": {
|
||||||
|
"mean": 0.0,
|
||||||
|
"mx": 1
|
||||||
|
},
|
||||||
"harness_xray_internal": {
|
"harness_xray_internal": {
|
||||||
"mean": 0.00087,
|
"mean": 0.00137,
|
||||||
"mx": 6
|
"mx": 7
|
||||||
},
|
},
|
||||||
"harness_p07_xray": {
|
"harness_p07_xray": {
|
||||||
"mean": 0.00097,
|
"mean": 0.00112,
|
||||||
"mx": 5
|
"mx": 4
|
||||||
},
|
},
|
||||||
"harness_p04_p05_xray": {
|
"harness_p04_p05_xray": {
|
||||||
"mean": 0.00174,
|
"mean": 0.00145,
|
||||||
"mx": 12
|
"mx": 5
|
||||||
|
},
|
||||||
|
"p03_mount_loose": {
|
||||||
|
"mean": 0.0,
|
||||||
|
"mx": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,22 +1,5 @@
|
|||||||
af5c9d74bddf7d795bd38f4371fa61ec docs/shots/01_iso.png
|
af5c9d74bddf7d795bd38f4371fa61ec docs/shots/01_iso.png
|
||||||
d1719a9f6fb775fd63cef27c5ff17f54 docs/shots/zoom_user.png
|
|
||||||
1389d052730b4bb09eb0a17cf9c49354 docs/shots/user_harness_view.png
|
|
||||||
eebfbfbea1375770fbc333f83f64e959 docs/shots/user_harness_zoom.png
|
|
||||||
606e209a6d3dc64ee3a59d1fc22339f3 docs/shots/user_m1_rear.png
|
|
||||||
99188cb1c040a2c6494322a9845ac6cb docs/shots/user_m1_exact.png
|
|
||||||
d3234ff06df60213391dab75589d65ab docs/shots/user_m1_bypass_oblique.png
|
|
||||||
02cfa0aa5681fec061f73b9d69fef41c docs/shots/user_smooth_harness.png
|
|
||||||
d25a25539d00674d608dca7234e331e0 docs/shots/user_contour_harness.png
|
|
||||||
c315879cfd09f35efa4ca4ecc1564b65 docs/shots/02_front.png
|
c315879cfd09f35efa4ca4ecc1564b65 docs/shots/02_front.png
|
||||||
30ab6da268de109cd087b93b8b175cf9 docs/shots/m1_sheet_front.png
|
|
||||||
45050caba87c801ab0c29ac939e51e43 docs/shots/m2_sheet_joint.png
|
|
||||||
615621995924b8bcaf67d79a020cd8d2 docs/shots/m3_sheet_exploded.png
|
|
||||||
2b1950dda519fac21d5e71d4a9c1f990 docs/shots/m4_sheet_side.png
|
|
||||||
86098e0fada6f54d9ea3d285b3e0ed6e docs/shots/m5_guide_step1.png
|
|
||||||
8366cefeca8b935f9fef31bfd2454047 docs/shots/m6_guide_step5.png
|
|
||||||
40f4663eef6aa61f36e47e1fb1b1ab07 docs/shots/m7_sheet_blueprint.png
|
|
||||||
2702f107e6152f05a5016b916f38cb89 docs/shots/m8_sheet_dark.png
|
|
||||||
c84473f41febb608aea0ec2ec12ef317 docs/shots/m9_motion.png
|
|
||||||
b3157f4453cf96b930f065e134a2d18b docs/shots/03_side.png
|
b3157f4453cf96b930f065e134a2d18b docs/shots/03_side.png
|
||||||
068fc4bc3eec481ff7d7b1d4da2aad88 docs/shots/04_top.png
|
068fc4bc3eec481ff7d7b1d4da2aad88 docs/shots/04_top.png
|
||||||
fbb95ea7a9229195d80c6f487021ae2b docs/shots/05_exploded.png
|
fbb95ea7a9229195d80c6f487021ae2b docs/shots/05_exploded.png
|
||||||
@@ -28,77 +11,89 @@ af5c9d74bddf7d795bd38f4371fa61ec docs/shots/10_solid.png
|
|||||||
8daa2255af050a225609cebbc490d3b9 docs/shots/11_tool_flange.png
|
8daa2255af050a225609cebbc490d3b9 docs/shots/11_tool_flange.png
|
||||||
002a1f4a2b65744515f5b54364fcc2a8 docs/shots/12_elbow_flare.png
|
002a1f4a2b65744515f5b54364fcc2a8 docs/shots/12_elbow_flare.png
|
||||||
b2f1454a6009a8b44ddd41be9efdaff5 docs/shots/13_forearm_side.png
|
b2f1454a6009a8b44ddd41be9efdaff5 docs/shots/13_forearm_side.png
|
||||||
87a8889ba4031cdf2e378fca7e1b475c docs/shots/user_target.png
|
|
||||||
d51cde4a5e2450c529d1c8897e16cf29 docs/shots/view_yz_pos_x.png
|
|
||||||
1d8277f0b43938423f2ca53b34df0c56 docs/shots/view_yz_neg_x.png
|
|
||||||
64a199b2c65e7f2d0ad5e2a80c5caef8 docs/shots/view_iso_forearm.png
|
|
||||||
22f62024f51ee1827f662d8754019f57 docs/shots/user_view1.png
|
|
||||||
e96c715833f9321b9fd1501bc4bdf87a docs/shots/user_view2.png
|
|
||||||
d34d5d15434bc5a980e34e5511cc49a4 docs/shots/user_p04.png
|
|
||||||
6ea6b98ad978b993868bd302ad1dd8c6 docs/shots/user_p04_opp.png
|
|
||||||
85d13ea862263aa055924df2beca98fb docs/shots/user_elbow_rear.png
|
|
||||||
a17303b12669be77a97dc18f2463c809 docs/shots/user_p07_wrist.png
|
|
||||||
3ed51a9cbad23f7c9c7eb79ac5471eb1 docs/shots/user_elbow_front.png
|
|
||||||
729025864df7708c900e110ec175f0da docs/shots/user_p06_xz.png
|
|
||||||
095da56acdc9b512709786df26691481 docs/shots/user_p06_yz.png
|
|
||||||
fe0b30e16a673beb200224a0e6607faa docs/shots/user_p06_iso.png
|
|
||||||
abf7113f27b6443541e3a0859d77b3fe docs/shots/user_p07_creases.png
|
|
||||||
959e4bf08e174d90a1fe03a75a042070 docs/shots/bay_p06_inside.png
|
|
||||||
f9d8d58365b4ca693525af5540d39d32 docs/shots/bay_p07_inside.png
|
|
||||||
1b07f37e5fed9d82886b9518a3839860 docs/shots/bay_p08_inside.png
|
|
||||||
2c7f53c7bed7d25175948f09960ed634 docs/shots/user_p04_side.png
|
|
||||||
51d318f3584444ea78f2078d9ef3eddd docs/shots/user_p04_bottom.png
|
|
||||||
b31cd81ca26198896378bc38c4393b9d docs/shots/user_p04_bottom2.png
|
|
||||||
a60f07bb05bba7a58bdd29e5202c478f docs/shots/user_shoulder_solid.png
|
|
||||||
a60f07bb05bba7a58bdd29e5202c478f docs/shots/user_shoulder_xray.png
|
|
||||||
86aab5a8eb32216b4f712c730cc67068 docs/shots/user_shoulder_top.png
|
|
||||||
c22cef65ca2a74b1332ecaba6b9a484a docs/shots/p03_light_iso.png
|
|
||||||
2a9e076fa310b0c78b1a0e8375e8aae8 docs/shots/p03_light_side.png
|
|
||||||
5671bcc5a150d4899d186abd3658ef99 docs/shots/p03_light_rear.png
|
|
||||||
ebdeb9a180e95afd38f5a42d12743579 docs/shots/p03_light_join.png
|
|
||||||
0aa155c87743426668114d876478582c docs/shots/p03_light_xray.png
|
|
||||||
c55a40d4bc4b4e73a672326fa02d573c docs/shots/hollow_p04_top.png
|
|
||||||
7fe0cb0d52c1f7d97bf1c8fd47b22e0c docs/shots/hollow_p04_iso.png
|
|
||||||
f3b85ea576a6aa99a61953c4ff2a5ff4 docs/shots/hollow_p07_top.png
|
|
||||||
1464e622ec8090b6052ab82f7bcd6b22 docs/shots/hollow_p07_iso.png
|
|
||||||
8379d580ff746c859587e29e34700d7c docs/shots/hollow_p06_iso.png
|
|
||||||
78f62639acb79e30fa88cc5fda87cfea docs/shots/p03_mount_top.png
|
|
||||||
d0c82d07ab6055c1b67c03c126fe267a docs/shots/p03_mount_iso.png
|
|
||||||
21db2e9ca69192cce0be4467b4701028 docs/shots/p03_mount_loose.png
|
|
||||||
83325d3ca1b9e56fd2c70661052fd063 docs/shots/j3_前端压盖_装配.png
|
|
||||||
e169359e86a61f7ab80d4adea0231436 docs/shots/j3_前端压盖_全件.png
|
|
||||||
90b79bb61cacea2323ca19790d4158b2 docs/shots/boss_p04.png
|
|
||||||
6a86499dd36b2a0a1ac50ae28ea138e4 docs/shots/user_fork_fixed.png
|
|
||||||
e39999d2ef28536f24d7f6178fa9a15c docs/shots/bay_p04_inside.png
|
e39999d2ef28536f24d7f6178fa9a15c docs/shots/bay_p04_inside.png
|
||||||
959e4bf08e174d90a1fe03a75a042070 docs/shots/bay_p06_inside.png
|
959e4bf08e174d90a1fe03a75a042070 docs/shots/bay_p06_inside.png
|
||||||
f9d8d58365b4ca693525af5540d39d32 docs/shots/bay_p07_inside.png
|
f9d8d58365b4ca693525af5540d39d32 docs/shots/bay_p07_inside.png
|
||||||
ae0305c092707e0c73944e1d889d076a docs/shots/p04_both_ends.png
|
1b07f37e5fed9d82886b9518a3839860 docs/shots/bay_p08_inside.png
|
||||||
f241acda8fdae460eb38f5a054e841f9 docs/shots/p04_m2_side.png
|
90b79bb61cacea2323ca19790d4158b2 docs/shots/boss_p04.png
|
||||||
4c6014a754e233411f6e89e4112d6cde docs/shots/p06_m4_side.png
|
|
||||||
aa3cb2e1e7bff6e3ca4af66bd6c70854 docs/shots/p07_m5_side.png
|
|
||||||
515b204e42d75d98afe0600f8b8b419d docs/shots/p05_m3_side.png
|
|
||||||
dcc54259caf06f796e3dec84496abb12 docs/shots/boss_p05.png
|
dcc54259caf06f796e3dec84496abb12 docs/shots/boss_p05.png
|
||||||
309cc9ac3238e747104e6ceee7bd68bc docs/shots/user_compare_view.png
|
|
||||||
152eb1db302b62238f3051b1f48fcf45 docs/shots/user_p07_check.png
|
|
||||||
9f2c134297f2c7a555a6eaca2e74e4de docs/shots/boss_p06.png
|
9f2c134297f2c7a555a6eaca2e74e4de docs/shots/boss_p06.png
|
||||||
b789564315ca1ad7d6d9d18c96972cba docs/shots/boss_p07.png
|
|
||||||
4a9a34e534496cb2c3be499ea2029d87 docs/shots/svg_side.png
|
|
||||||
eadcf7d6ac9853950496a559dbd8de72 docs/shots/svg_joint.png
|
|
||||||
f1e5bd79e6921b39d0785e4208ef5cca docs/shots/svg_exploded.png
|
|
||||||
1e1d29129833b35e43b925b25eae4b8f docs/shots/user_circle_check.png
|
|
||||||
6e1c039c236d28a4bec7e7b589fb2ed3 docs/shots/user_exact_view.png
|
|
||||||
7635f1d057b06f014c1683261a7237b8 docs/shots/shot_p08_user.png
|
|
||||||
3d261042388c8b2924848d8a29f71931 docs/shots/p05_flank_view.png
|
|
||||||
af5c9d74bddf7d795bd38f4371fa61ec docs/shots/harness_full_iso.png
|
af5c9d74bddf7d795bd38f4371fa61ec docs/shots/harness_full_iso.png
|
||||||
8896124b0c45d9796e983f026134cff6 docs/shots/harness_overview.png
|
8896124b0c45d9796e983f026134cff6 docs/shots/harness_overview.png
|
||||||
9fca41ada43f36a338cce31797152c3c docs/shots/harness_p07_window.png
|
|
||||||
7cbf2c699501bff3c7ebb4beb7cd34e2 docs/shots/harness_p04_p05.png
|
7cbf2c699501bff3c7ebb4beb7cd34e2 docs/shots/harness_p04_p05.png
|
||||||
687e2184dd3e149939ce6d4214ffc698 docs/shots/harness_p06_window.png
|
687e2184dd3e149939ce6d4214ffc698 docs/shots/harness_p06_window.png
|
||||||
|
9fca41ada43f36a338cce31797152c3c docs/shots/harness_p07_window.png
|
||||||
84fb87bb5ce6cef73866401642ab0f9d docs/shots/harness_xray_full.png
|
84fb87bb5ce6cef73866401642ab0f9d docs/shots/harness_xray_full.png
|
||||||
70772a99b8c5d81619127eed7bc1dab5 docs/shots/tie_probe_negY.png
|
7fe0cb0d52c1f7d97bf1c8fd47b22e0c docs/shots/hollow_p04_iso.png
|
||||||
a3bf9bd7fb796283bbb9767cc790167a docs/shots/tie_probe_posY.png
|
c55a40d4bc4b4e73a672326fa02d573c docs/shots/hollow_p04_top.png
|
||||||
|
8379d580ff746c859587e29e34700d7c docs/shots/hollow_p06_iso.png
|
||||||
|
1464e622ec8090b6052ab82f7bcd6b22 docs/shots/hollow_p07_iso.png
|
||||||
|
f3b85ea576a6aa99a61953c4ff2a5ff4 docs/shots/hollow_p07_top.png
|
||||||
|
e169359e86a61f7ab80d4adea0231436 docs/shots/j3_前端压盖_全件.png
|
||||||
|
83325d3ca1b9e56fd2c70661052fd063 docs/shots/j3_前端压盖_装配.png
|
||||||
|
30ab6da268de109cd087b93b8b175cf9 docs/shots/m1_sheet_front.png
|
||||||
|
45050caba87c801ab0c29ac939e51e43 docs/shots/m2_sheet_joint.png
|
||||||
|
2b1950dda519fac21d5e71d4a9c1f990 docs/shots/m4_sheet_side.png
|
||||||
|
86098e0fada6f54d9ea3d285b3e0ed6e docs/shots/m5_guide_step1.png
|
||||||
|
8366cefeca8b935f9fef31bfd2454047 docs/shots/m6_guide_step5.png
|
||||||
|
40f4663eef6aa61f36e47e1fb1b1ab07 docs/shots/m7_sheet_blueprint.png
|
||||||
|
2702f107e6152f05a5016b916f38cb89 docs/shots/m8_sheet_dark.png
|
||||||
|
a6375fdce42c33395cad1586c9d6d8c5 docs/shots/m9_motion.png
|
||||||
|
c22cef65ca2a74b1332ecaba6b9a484a docs/shots/p03_light_iso.png
|
||||||
|
ebdeb9a180e95afd38f5a42d12743579 docs/shots/p03_light_join.png
|
||||||
|
5671bcc5a150d4899d186abd3658ef99 docs/shots/p03_light_rear.png
|
||||||
|
2a9e076fa310b0c78b1a0e8375e8aae8 docs/shots/p03_light_side.png
|
||||||
|
0aa155c87743426668114d876478582c docs/shots/p03_light_xray.png
|
||||||
|
d0c82d07ab6055c1b67c03c126fe267a docs/shots/p03_mount_iso.png
|
||||||
|
78f62639acb79e30fa88cc5fda87cfea docs/shots/p03_mount_top.png
|
||||||
|
ae0305c092707e0c73944e1d889d076a docs/shots/p04_both_ends.png
|
||||||
|
f241acda8fdae460eb38f5a054e841f9 docs/shots/p04_m2_side.png
|
||||||
|
3d261042388c8b2924848d8a29f71931 docs/shots/p05_flank_view.png
|
||||||
|
515b204e42d75d98afe0600f8b8b419d docs/shots/p05_m3_side.png
|
||||||
|
4c6014a754e233411f6e89e4112d6cde docs/shots/p06_m4_side.png
|
||||||
|
aa3cb2e1e7bff6e3ca4af66bd6c70854 docs/shots/p07_m5_side.png
|
||||||
|
7635f1d057b06f014c1683261a7237b8 docs/shots/shot_p08_user.png
|
||||||
|
f1e5bd79e6921b39d0785e4208ef5cca docs/shots/svg_exploded.png
|
||||||
|
eadcf7d6ac9853950496a559dbd8de72 docs/shots/svg_joint.png
|
||||||
|
4a9a34e534496cb2c3be499ea2029d87 docs/shots/svg_side.png
|
||||||
|
d3bf901601ff221ecd1b09dc93dd2ccc docs/shots/tie_after_close.png
|
||||||
7c2be3a8488997de588e3760f966fd7d docs/shots/tie_after_negY.png
|
7c2be3a8488997de588e3760f966fd7d docs/shots/tie_after_negY.png
|
||||||
0b3e26c6e2fd664f91764d26b8b1ed0c docs/shots/tie_after_oblique.png
|
0b3e26c6e2fd664f91764d26b8b1ed0c docs/shots/tie_after_oblique.png
|
||||||
d3bf901601ff221ecd1b09dc93dd2ccc docs/shots/tie_after_close.png
|
|
||||||
c39cee9deabedb4dc8fad8ec76b439e5 docs/shots/tie_after_p0607.png
|
c39cee9deabedb4dc8fad8ec76b439e5 docs/shots/tie_after_p0607.png
|
||||||
b51a4e5424297c9012cc0562bbeec296 docs/shots/tie_after_p0607_obl.png
|
b51a4e5424297c9012cc0562bbeec296 docs/shots/tie_after_p0607_obl.png
|
||||||
|
70772a99b8c5d81619127eed7bc1dab5 docs/shots/tie_probe_negY.png
|
||||||
|
a3bf9bd7fb796283bbb9767cc790167a docs/shots/tie_probe_posY.png
|
||||||
|
1e1d29129833b35e43b925b25eae4b8f docs/shots/user_circle_check.png
|
||||||
|
309cc9ac3238e747104e6ceee7bd68bc docs/shots/user_compare_view.png
|
||||||
|
d25a25539d00674d608dca7234e331e0 docs/shots/user_contour_harness.png
|
||||||
|
3ed51a9cbad23f7c9c7eb79ac5471eb1 docs/shots/user_elbow_front.png
|
||||||
|
85d13ea862263aa055924df2beca98fb docs/shots/user_elbow_rear.png
|
||||||
|
6e1c039c236d28a4bec7e7b589fb2ed3 docs/shots/user_exact_view.png
|
||||||
|
6a86499dd36b2a0a1ac50ae28ea138e4 docs/shots/user_fork_fixed.png
|
||||||
|
1389d052730b4bb09eb0a17cf9c49354 docs/shots/user_harness_view.png
|
||||||
|
eebfbfbea1375770fbc333f83f64e959 docs/shots/user_harness_zoom.png
|
||||||
|
d3234ff06df60213391dab75589d65ab docs/shots/user_m1_bypass_oblique.png
|
||||||
|
99188cb1c040a2c6494322a9845ac6cb docs/shots/user_m1_exact.png
|
||||||
|
606e209a6d3dc64ee3a59d1fc22339f3 docs/shots/user_m1_rear.png
|
||||||
|
d34d5d15434bc5a980e34e5511cc49a4 docs/shots/user_p04.png
|
||||||
|
51d318f3584444ea78f2078d9ef3eddd docs/shots/user_p04_bottom.png
|
||||||
|
b31cd81ca26198896378bc38c4393b9d docs/shots/user_p04_bottom2.png
|
||||||
|
6ea6b98ad978b993868bd302ad1dd8c6 docs/shots/user_p04_opp.png
|
||||||
|
2c7f53c7bed7d25175948f09960ed634 docs/shots/user_p04_side.png
|
||||||
|
fe0b30e16a673beb200224a0e6607faa docs/shots/user_p06_iso.png
|
||||||
|
729025864df7708c900e110ec175f0da docs/shots/user_p06_xz.png
|
||||||
|
095da56acdc9b512709786df26691481 docs/shots/user_p06_yz.png
|
||||||
|
152eb1db302b62238f3051b1f48fcf45 docs/shots/user_p07_check.png
|
||||||
|
abf7113f27b6443541e3a0859d77b3fe docs/shots/user_p07_creases.png
|
||||||
|
a17303b12669be77a97dc18f2463c809 docs/shots/user_p07_wrist.png
|
||||||
|
a60f07bb05bba7a58bdd29e5202c478f docs/shots/user_shoulder_solid.png
|
||||||
|
86aab5a8eb32216b4f712c730cc67068 docs/shots/user_shoulder_top.png
|
||||||
|
a60f07bb05bba7a58bdd29e5202c478f docs/shots/user_shoulder_xray.png
|
||||||
|
02cfa0aa5681fec061f73b9d69fef41c docs/shots/user_smooth_harness.png
|
||||||
|
87a8889ba4031cdf2e378fca7e1b475c docs/shots/user_target.png
|
||||||
|
22f62024f51ee1827f662d8754019f57 docs/shots/user_view1.png
|
||||||
|
e96c715833f9321b9fd1501bc4bdf87a docs/shots/user_view2.png
|
||||||
|
64a199b2c65e7f2d0ad5e2a80c5caef8 docs/shots/view_iso_forearm.png
|
||||||
|
1d8277f0b43938423f2ca53b34df0c56 docs/shots/view_yz_neg_x.png
|
||||||
|
d51cde4a5e2450c529d1c8897e16cf29 docs/shots/view_yz_pos_x.png
|
||||||
|
d1719a9f6fb775fd63cef27c5ff17f54 docs/shots/zoom_user.png
|
||||||
|
|||||||
@@ -28,8 +28,9 @@ const argVal = (name, dflt) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const SOLVER = argVal('solver', first(
|
const SOLVER = argVal('solver', first(
|
||||||
path.join(ROOT, 'webapp/js/ik/solver.js'),
|
path.join(ROOT, 'webapp/js/ik/solver.js'), // S5 之后
|
||||||
path.join(ROOT, 'web/arm-ik-solver.js')));
|
path.join(ROOT, 'webapp/js/ik-solver.js'), // S4 现在
|
||||||
|
path.join(ROOT, 'web/arm-ik-solver.js'))); // 拆分前
|
||||||
const MESH = first(path.join(ROOT, 'webapp/data/mesh_arm.json'),
|
const MESH = first(path.join(ROOT, 'webapp/data/mesh_arm.json'),
|
||||||
path.join(ROOT, 'web/mesh_arm.json'));
|
path.join(ROOT, 'web/mesh_arm.json'));
|
||||||
const TRAVEL = path.join(ROOT, 'docs/travel.json');
|
const TRAVEL = path.join(ROOT, 'docs/travel.json');
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ def main():
|
|||||||
# 扫候选文件而不是写死一个路径,前端拆分时这条检查不用跟着改;一个都找不到就直接失败,
|
# 扫候选文件而不是写死一个路径,前端拆分时这条检查不用跟着改;一个都找不到就直接失败,
|
||||||
# 绝不能"没匹配到 = 没有通道 = 零干涉"地假通过。
|
# 绝不能"没匹配到 = 没有通道 = 零干涉"地假通过。
|
||||||
from arm.paths import REPO
|
from arm.paths import REPO
|
||||||
cands = [REPO + "/src/viewer_tpl.html", REPO + "/webapp/js/harness.js"]
|
cands = [REPO + "/webapp/js/harness.js", REPO + "/webapp/js/app.js"]
|
||||||
found = [c for c in cands if os.path.exists(c)]
|
found = [c for c in cands if os.path.exists(c)]
|
||||||
if not found:
|
if not found:
|
||||||
print("找不到线束源码,检查项无法执行:", cands, flush=True)
|
print("找不到线束源码,检查项无法执行:", cands, flush=True)
|
||||||
@@ -57,6 +57,10 @@ def main():
|
|||||||
tpl = f.read()
|
tpl = f.read()
|
||||||
|
|
||||||
tube_matches = re.findall(r'makeHarnTube\(\[\s*([\s\S]*?)\s*\]\s*,\s*[\d\.]+\s*,\s*\w+\s*,\s*\'([^\']+)\'', tpl)
|
tube_matches = re.findall(r'makeHarnTube\(\[\s*([\s\S]*?)\s*\]\s*,\s*[\d\.]+\s*,\s*\w+\s*,\s*\'([^\']+)\'', tpl)
|
||||||
|
# 读到 0 个通道会被下面当成"零穿透"而 PASS —— 那是假通过,必须先炸。
|
||||||
|
if not tube_matches:
|
||||||
|
print("找不到任何线束通道定义,源码结构变了:", cands)
|
||||||
|
sys.exit(2)
|
||||||
total_hits = 0
|
total_hits = 0
|
||||||
checked_count = 0
|
checked_count = 0
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,11 @@ BASE = os.path.join(HERE, "baselines")
|
|||||||
# 每个产物允许出现的目录,按优先级;找到第一个即取。
|
# 每个产物允许出现的目录,按优先级;找到第一个即取。
|
||||||
CANDIDATES = {
|
CANDIDATES = {
|
||||||
"mesh_arm.json": ["webapp/data", "web"],
|
"mesh_arm.json": ["webapp/data", "web"],
|
||||||
"sheets.jsdata": ["webapp/data", "web"],
|
"00_data.js": ["webapp/generated"],
|
||||||
|
"sheet_front.svg": ["docs"],
|
||||||
|
"sheet_joint.svg": ["docs"],
|
||||||
|
"sheet_exploded.svg": ["docs"],
|
||||||
|
"sheet_side.svg": ["docs"],
|
||||||
"travel.json": ["docs", "webapp/data"],
|
"travel.json": ["docs", "webapp/data"],
|
||||||
"travel_detail.json": ["docs", "webapp/data"],
|
"travel_detail.json": ["docs", "webapp/data"],
|
||||||
"collide.json": ["docs", "webapp/data"],
|
"collide.json": ["docs", "webapp/data"],
|
||||||
@@ -124,6 +128,12 @@ def check_shots():
|
|||||||
print(" [MISS] 截图失败 %d 个: %s" % (len(bad), bad[:3]))
|
print(" [MISS] 截图失败 %d 个: %s" % (len(bad), bad[:3]))
|
||||||
|
|
||||||
fails = len(bad)
|
fails = len(bad)
|
||||||
|
def shoot(names):
|
||||||
|
subprocess.run([sys.executable, "-u", "-m", "arm.viewer.shot"] + names,
|
||||||
|
capture_output=True, text=True, cwd=REPO,
|
||||||
|
encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
diff = []
|
||||||
for fname, want in sorted(base.items()):
|
for fname, want in sorted(base.items()):
|
||||||
p = os.path.join(REPO, fname.replace("/", os.sep))
|
p = os.path.join(REPO, fname.replace("/", os.sep))
|
||||||
if not os.path.exists(p):
|
if not os.path.exists(p):
|
||||||
@@ -131,8 +141,20 @@ def check_shots():
|
|||||||
fails += 1
|
fails += 1
|
||||||
continue
|
continue
|
||||||
if md5(p) != want:
|
if md5(p) != want:
|
||||||
print(" [DIFF] %-40s" % fname)
|
diff.append(fname)
|
||||||
fails += 1
|
if diff:
|
||||||
|
# 偶发"半加载截图"确实存在(实测同一构建重跑就回到基线)。串行重拍一次:
|
||||||
|
# 真回归重拍也回不来,只有竞态会消失 —— 不重拍就会变成随机假 FAIL,
|
||||||
|
# 关卡一旦不可信就会被忽略。
|
||||||
|
print(" %d 张首轮不一致,串行重拍复核: %s" % (len(diff), [os.path.basename(x) for x in diff]))
|
||||||
|
shoot([os.path.basename(x).replace(".png", "") for x in diff])
|
||||||
|
for fname in diff:
|
||||||
|
p = os.path.join(REPO, fname.replace("/", os.sep))
|
||||||
|
if md5(p) != base[fname]:
|
||||||
|
print(" [DIFF] %-40s 重拍后仍不一致" % fname)
|
||||||
|
fails += 1
|
||||||
|
else:
|
||||||
|
print(" [RETRY-OK] %-36s 竞态,非回归" % fname)
|
||||||
print(" 逐字节比对: %d 张稳定视图,%d 张不一致" % (len(base), fails - len(bad)))
|
print(" 逐字节比对: %d 张稳定视图,%d 张不一致" % (len(base), fails - len(bad)))
|
||||||
|
|
||||||
tol_fails = 0
|
tol_fails = 0
|
||||||
|
|||||||
@@ -0,0 +1,357 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>六电机人形机器人手臂 · 装配与打印模型</title>
|
||||||
|
<link rel="stylesheet" href="styles/base.css">
|
||||||
|
<link rel="stylesheet" href="styles/sheet-chrome.css">
|
||||||
|
<link rel="stylesheet" href="styles/sheet-theme.css">
|
||||||
|
<link rel="stylesheet" href="styles/sheet-hud.css">
|
||||||
|
<link rel="stylesheet" href="styles/guide.css">
|
||||||
|
<link rel="stylesheet" href="styles/dock.css">
|
||||||
|
<link rel="stylesheet" href="styles/ikbar.css">
|
||||||
|
<link rel="stylesheet" href="styles/viewmode.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<header>
|
||||||
|
<b id="ttl">人形机器人手臂</b>
|
||||||
|
<span class="q" id="ver"></span>
|
||||||
|
<button class="tab on" data-view="model">三维模型</button>
|
||||||
|
<button class="tab" data-view="sheet">装配图</button><button class="tab" data-view="motion">运动模拟</button>
|
||||||
|
<span class="sp"></span>
|
||||||
|
<button id="bAsm" class="on">装配视图</button>
|
||||||
|
<button id="bExp">爆炸视图</button>
|
||||||
|
<input id="exp" type="range" min="0" max="100" value="0" style="width:96px">
|
||||||
|
<select id="cam">
|
||||||
|
<option value="iso">等测</option><option value="front">前视</option>
|
||||||
|
<option value="side">侧视</option><option value="top">顶视</option>
|
||||||
|
<option value="wrist">腕部特写</option><option value="joint">关节特写</option><option value="elbow">肘部特写</option><option value="hand">手部特写</option>
|
||||||
|
</select>
|
||||||
|
<button id="bWire">线框</button>
|
||||||
|
<button id="bXRay">电机半透</button>
|
||||||
|
<button id="bScr" class="on" title="显示/隐藏全部螺钉(S01~S10);点右侧 BOM 螺丝行可在模型中高亮定位">螺钉</button>
|
||||||
|
<button id="bHarn" class="on" title="显示/隐藏线束走线指示(主干4芯总线、电机出线分支、过关节U型/螺旋缓冲环、T接小板)">线束</button>
|
||||||
|
<button id="bEnv" title="显示/隐藏可达外包络(运动学边界)">可达包络</button>
|
||||||
|
<button id="bFit">复位</button>
|
||||||
|
<button id="bL" title="显示/隐藏左栏">左栏</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div id="ikbar">
|
||||||
|
<div class="ikrow">
|
||||||
|
<span class="ik-t">IK</span>
|
||||||
|
<span class="lb">目标点 X</span><input type="number" id="ikX" step="1" title="CAD 世界坐标 X / mm">
|
||||||
|
<span class="lb">Y</span><input type="number" id="ikY" step="1" title="CAD 世界坐标 Y / mm">
|
||||||
|
<span class="lb">Z</span><input type="number" id="ikZ" step="1" title="CAD 世界坐标 Z / mm(桌面 z=0,向上为正)">
|
||||||
|
<span class="vsep"></span>
|
||||||
|
<span class="lb">工具长</span><input type="number" id="ikTool" value="0" step="5" title="在工具法兰面之外、沿 J6 轴线再伸出的工具长度 / mm">
|
||||||
|
<span class="lb">mm</span>
|
||||||
|
<span class="vsep"></span>
|
||||||
|
<span class="lb">扫描精度</span>
|
||||||
|
<select id="ikStep" title="冗余维 (J4,J5) 的粗扫步长:只在可行段内采样;位置任务取全局最优,姿态任务的精确解由孤立根枚举给出、与步长无关">
|
||||||
|
<option value="1">快速 1°</option>
|
||||||
|
<option value="0.5">较快 0.5°</option>
|
||||||
|
<option value="0.25" selected>标准 0.25°</option>
|
||||||
|
<option value="0.1">精细 0.1°</option>
|
||||||
|
<option value="0.05">参考级 0.05°</option>
|
||||||
|
</select>
|
||||||
|
<button id="ikSolve" title="求解并把手臂动画移动到该位姿(取相对当前姿态行程最小的构型)">求解并移动</button>
|
||||||
|
<button id="ikPose" title="开启姿态控制:除 TCP 位置外,同时指定末端工具姿态(工具轴指向 + 绕轴滚转)">姿态控制</button>
|
||||||
|
<span id="ikPoseBox">
|
||||||
|
<span class="lb">方位</span><input type="number" id="ikAz" step="5" value="0" title="工具轴(法兰朝外方向)绕 CAD Z 轴的方位角 / 度">
|
||||||
|
<span class="lb">仰角</span><input type="number" id="ikEl" step="5" value="90" title="工具轴(法兰朝外方向)自 CAD +Z 抬起的仰角 / 度:0 = 竖直向上,90 = 水平,180 = 竖直向下">
|
||||||
|
<span class="lb">滚转</span><input type="number" id="ikRoll" step="5" value="0" title="绕工具轴自身的滚转角 / 度(右手系)">
|
||||||
|
<button id="ikPoseCur" title="把当前真实工具姿态读回三个输入框">取当前</button>
|
||||||
|
<button id="ikPoseDown" title="快捷设为工具轴竖直向下(爪取常用姿态)">垂直向下</button>
|
||||||
|
<span class="lb">优先级</span>
|
||||||
|
<select id="ikPrio" title="位置与姿态不能同时满足时的取舍:位置优先 = 保 TCP 点位精确、姿态取最接近可达值;姿态优先 = 保指定姿态精确、TCP 取最接近可达点(位置差在结果栏如实给出);混合优先 = 用右侧滑杆自由分配两端比重,0% = 姿态优先、100% = 位置优先,中间值在两端锚点之间按归一化折中代价加权搜索">
|
||||||
|
<option value="pos" selected>位置优先</option>
|
||||||
|
<option value="ori">姿态优先</option>
|
||||||
|
<option value="mix">混合优先</option>
|
||||||
|
</select>
|
||||||
|
<span id="ikMixBox" title="混合优先的比重(自由分配位置与姿态):向左 = 姿态优先、向右 = 位置优先;0% / 100% 与两条纯算法逐位一致。折中代价 J = w·(e/e_ref)² + (1−w)·(o/o_ref)²,两端尺度取各自锚点(位置端 = 位置优先解的姿态残差、姿态端 = 姿态优先解的位置差),故读数即「沿折中历程的进度」。滑动即算即动,松手全量折中">
|
||||||
|
<span class="lb">姿态</span>
|
||||||
|
<input type="range" id="ikMixW" min="0" max="100" step="1" value="50">
|
||||||
|
<span class="lb">位置</span>
|
||||||
|
<span class="st" id="ikMixV">50% 位置</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="ikrow">
|
||||||
|
<button id="ikPick" title="开启后在视图中单击,把目标点投到当前高度的水平面上">视图拾取</button>
|
||||||
|
<button id="ikRt" title="开启后拖动黄球即算即动:手臂实时追踪目标点移动(松手做一次精修求解)">实时追踪</button>
|
||||||
|
<button id="ikHome" title="所有关节角归零">归零位</button>
|
||||||
|
<button id="ikZero" title="载入一个示例目标并求解">示例目标</button>
|
||||||
|
<span class="vsep"></span>
|
||||||
|
<span class="hint" id="ikHint">拖黄球 = 移动目标(Shift 仅上下);拖箭头 = 调工具轴指向;拖箭杆或粉球 = 滚转;松手自动求解</span>
|
||||||
|
<span class="vsep"></span>
|
||||||
|
<span class="st" id="ikBar">—</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<aside id="left">
|
||||||
|
<div id="ikpanel">
|
||||||
|
<div class="ikh">
|
||||||
|
<h3>逆运动学 · 位置与姿态 · 最小总关节角</h3>
|
||||||
|
<span id="ikBadge" class="badge wait">待求解</span>
|
||||||
|
</div>
|
||||||
|
<table id="ikTab">
|
||||||
|
<thead><tr><th>轴</th><th>关节</th><th>角度</th><th>|θ|</th><th>限位</th></tr></thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
<div class="kv" id="ikSum"></div>
|
||||||
|
<div class="src" id="ikSrc"></div>
|
||||||
|
<div id="ikAltN"></div>
|
||||||
|
<div id="ikAlts"></div>
|
||||||
|
<details id="ikChkBox"><summary>结构与精度自检 / 工作空间</summary><div class="kv" id="ikChk"></div>
|
||||||
|
<div class="kv" id="ikDep"></div></details>
|
||||||
|
<details id="ikDoc">
|
||||||
|
<summary>算法说明与公式推导</summary>
|
||||||
|
<div class="doc">
|
||||||
|
<b>模式</b>:本页「<b>运动模拟</b>」承载全部运动学能力;「三维模型」为纯静态模型视图(关节锁定在设计零位、无目标点与求解控件),「装配图」为二维图纸。运动模拟下左栏只保留 IK 结果与关节角度,零件树/打印清单自动收走。<hr>
|
||||||
|
<b>目标</b>:使 TCP(末端工具法兰 P09 安装面中心,可加“工具长”)到达给定点,并在一切满足限位的可行构型中取
|
||||||
|
<b>各电机需转过的角度之和最小</b>者(相对当前姿态的行程;连续轴 θ 与 θ±360 是同一物理位形,角差一律取 ±180° 内的短弧,见第 6 条;离线批算不给参考姿态时退化为 Σ|θᵢ| 最小)。开启上方“<b>姿态控制</b>”后,
|
||||||
|
约束升级为 <b>位置 3 + 姿态 3</b>(工具轴指向 + 绕轴滚转),见第 5 条。
|
||||||
|
<br><b>姿态操控</b>:姿态开启后目标球处出现方向箭头与粉色侧球——<b>箭头指向即工具朝外方向</b>(法兰面法向朝外,也就是工具伸出的方向)。
|
||||||
|
<b>拖箭头头部</b>用弧球法(指针射线与以目标为球心、半径=箭长的球求交,方向=交点−球心)连续调整方位/俯仰;
|
||||||
|
<b>拖箭杆或粉色侧球</b>按指针绕轴的转角增量改滚转;右侧数字框随时同步,可精确微调,
|
||||||
|
「取当前」把真实工具姿态读回、「垂直向下」一键设为爪取常用姿态。
|
||||||
|
<br><b>约定</b>:求解器内部工具坐标系 z 轴取法兰面法向(朝臂内,与 TCP 外伸反向);界面上的方位/仰角/滚转、
|
||||||
|
箭头与绿色当前轴线均以朝外法向描述,入栈时绕工具 x 轴翻 180° 换算,两者看到的物理姿态完全一致。
|
||||||
|
<ol>
|
||||||
|
<li><b>先消去一维</b>(仅位置任务):TCP 恰好落在 J6 轴线上,绕 J6 转动不改变 TCP 位置 ⇒ 对“到点”任务取
|
||||||
|
<code>θ6 = 0</code> 即已最优。</li>
|
||||||
|
<li><b>识别结构</b>:J2 / J4 / J5 三轴严格平行(轴向 Y,过 x = 19.5),J1 轴为 X,J3 轴沿臂长方向 Z。
|
||||||
|
记 <code>A = X−X0</code>、<code>R = |(Y−Y0, Z−Z1)|</code>、<code>φ = atan2(Z−Z1, Y−Y0)</code>,
|
||||||
|
则 <code>X = X0 + a</code>,而 <code>(Y, Z)</code> 由 <code>(b, c)</code> 绕 J1 轴旋转 φ 得到:<br>
|
||||||
|
<code>a = Vx·cosθ3·cosθ2 + Vz·sinθ2</code>,<code>b = −Vx·sinθ3</code>,
|
||||||
|
<code>c = C0 + (−Vx·cosθ3·sinθ2 + Vz·cosθ2)</code>,
|
||||||
|
其中 <code>Vx, Vz</code> 只是 (θ4, θ5) 的函数。</li>
|
||||||
|
<li><b>两式联立消元(关键一步)</b>:由 <code>a = A</code> 得恒等式
|
||||||
|
<code>A² + n² = Vx² + Vz²</code>(<code>n = −Vx·cosθ3·sinθ2 + Vz·cosθ2</code>);
|
||||||
|
代入 <code>b² + c² = R²</code> 后 θ3 的项<u>全部抵消</u>,于是<br>
|
||||||
|
<code>n = (R² + A² − Vx² − Vz² − C0²) / (2·C0)</code> —— <b>唯一确定</b>;<br>
|
||||||
|
再由 <code>n² + A² = Vx²·cos²θ3 + Vz²</code> 得
|
||||||
|
<code>cos²θ3 = (n² + A² − Vz²) / Vx²</code> ⇒ θ3 共四支 <code>±σ, ±(π−σ)</code>;<br>
|
||||||
|
最后 θ2 与 θ1 各由一个 2×2 线性方程组唯一确定:
|
||||||
|
<code>cosθ2 = (κA + Vz·n)/(κ² + Vz²)</code>,<code>sinθ2 = (Vz·A − κ·n)/(κ² + Vz²)</code>,
|
||||||
|
<code>κ = Vx·cosθ3</code>;<code>θ1 = φ − atan2(c, b)</code>。</li>
|
||||||
|
<li><b>遍历冗余(按行裁剪到可行段)</b>:(θ4, θ5) 就是二维冗余。闭式支只在平面支有解时才存在,
|
||||||
|
限位盒内只有 <b>7~27%</b> 的网格点能过判据,且实测(5 个目标 × 全部 θ4 行)<b>每行的可行集都是单一连续段</b>
|
||||||
|
—— 于是先用探测步找出每行的可行段、向两端各放宽一个探测步兜住段界,段内再按步长精扫。
|
||||||
|
段内密度与全域扫一致,因此<b>解的质量不动</b>,粗扫采样量降约 4.6×(但全量总耗时只降约 1.10×——局部加密窗口是 ±2h 步长 h/8,格数恒为 32×32/种子、与 h 无关,精修才是耗时地板)。
|
||||||
|
之所以不把粗扫一起放松:根枚举目前不保证完备,漏根时要靠粗扫的窄带候选兜底,
|
||||||
|
实测把粗扫同步放松会换来上百度级的代价劣化。</li>
|
||||||
|
<li><b>姿态任务(6 自由度)</b>:工具坐标系姿态 <code>R = R₁·R₂·…·R₆</code>(与场景图 6 级矩阵链一致)。
|
||||||
|
结构上 <b>J5 轴与 J6 轴在腕心 (19.5, 128, 222) 垂直相交</b>,因此 θ6 只提供绕工具自身轴的滚转:
|
||||||
|
<u>工具轴指向 = (R₁…R₅)·ẑ,与 θ6 无关,TCP 位置也与 θ6 无关</u>。每条位置闭式支都附带确定的 R₅
|
||||||
|
(= R₁…R₅),滚转角随之闭式解出:<code>N = R₅ᵀ·R_des</code>(需为 Rz(−θ6))⇒
|
||||||
|
<code>θ6 = −atan2(N₁₀, N₀₀)</code>;姿态偏差 <code>e = ∠(R₅·Rz(−θ6), R_des)</code> ≤ 2° 的支才收入。</li>
|
||||||
|
<li><b>姿态 = 孤立根求解</b>:闭式支把 θ1..θ3 吸收掉之后,姿态任务只剩「工具轴指向 = 目标轴」
|
||||||
|
这 2 个方程、2 个未知数 (θ4,θ5),解是<b>孤立点</b>(独立密集种子 + 牛顿普查实测只有 1~2 个根),
|
||||||
|
滚转再由 θ6 闭式补上。所以不再用网格密度去撞那条斜窄谷:逐行对残差两个分量找<b>变号括号</b>、
|
||||||
|
从括号中点起跑<b>阻尼高斯-牛顿</b>(奇异时加大阻尼而非退出),并对每支的 ‖e‖ 最小节点补种以收
|
||||||
|
切向相切的根;代价最优的若干条入选候选再各收口一次。于是
|
||||||
|
<b>姿态残差量级从 ~10⁻²° 变为 10⁻⁶°(工程意义上的精确)</b>,且 Σ|θ| 不劣于原路径。</li>
|
||||||
|
<li><b>两处判据陷阱</b>(都验过,写在这里防止被自证):其一,切向残差在工具轴<b>反平</b>时同样为零,
|
||||||
|
同向判据只能在收口时验,当作迭代中止条件会让每支一进来就作废;其二,求根按<b>支下标</b>对齐
|
||||||
|
prev/cur,不可行的支必须留 null 占位,否则下标错开、括号判据失配,实测会整支漏掉一个真根、
|
||||||
|
多付 130° 关节角。确有少数奇异「刀尖」构型<b>无精确根</b>,此时牛顿收敛到 ‖e‖ 的真实最小,
|
||||||
|
报出的残差不再是采样伪影。</li>
|
||||||
|
<li><b>混合优先(比重可调折中)</b>:优先级下拉的第三种模式——一根滑杆自由分配两端:
|
||||||
|
<b>0% 即姿态优先、100% 即位置优先</b>(两端直接交给上面两条专用算法,结果逐位一致),
|
||||||
|
中间值在两端锚点之间最小化<b>归一化折中代价</b>
|
||||||
|
<code>J = w·(e/e_ref)² + (1−w)·(o/o_ref)²</code>。两端尺度取锚点自身:
|
||||||
|
<code>e_ref</code> = 姿态优先解的 TCP 位置差、<code>o_ref</code> = 位置优先解的姿态残差,
|
||||||
|
于是<b>滑杆读数就是「沿折中历程的进度」</b>、与目标的绝对误差量级无关(50% ≈ 两端各自归一后对半)。
|
||||||
|
之所以不用「1 mm ↔ 1°」的绝对量纲:实测两端误差常在 100 mm / 40° 量级,绝对量纲会把读数整体挤向
|
||||||
|
位置端(折线中点落到 ~13% 处),滑杆就不好用了。两端锚点复用上面两条算法——位置端 = 位置精确族 +
|
||||||
|
闭式滚转 + 孤立根收口(位置不可达时退化为最近可达位置),姿态端 = 工具轴闭式对齐 + θ6 补滚转
|
||||||
|
(姿态精确、位置最近);中间值以两锚点及<b>关节空间插值</b>为种子,对 6 维残差
|
||||||
|
<code>[√w·(p−t)/e_ref , √(1−w)·RV°/o_ref]</code> 做<b>阻尼高斯-牛顿 + 单轴抛光</b>,取 J 最小者
|
||||||
|
(J 同量级 2% 带内取行程 — 相对当前姿态的 Σ|wrap(θᵢ−refᵢ)| — 更小者,防等价支横跳)。两端中任一端在另一维也达标
|
||||||
|
(≤0.05)时折中无意义、直接返回该端——注意位置端不达标而姿态端在位置上达标时返回<b>姿态端</b>
|
||||||
|
(它两维同时占优:位置优先族收不到的到达解,姿态端收得到)。全程无随机数,同一输入逐位可复现;
|
||||||
|
拖动走快路(同「实时续解」条哲学,归一化尺度沿用最近一次全量的缓存),松手必走全量。</li>
|
||||||
|
<li><b>连续优先(最小移动角 / 防倒摆)</b>:等价多分支(θ3 的 ±σ/±(π−σ)、肘上/肘下、J1/J3 镜像族)之间只按 Σ|θ| 选优时,
|
||||||
|
目标微动就会在分支间跳来跳去、绕远路(实测目标挪 20 mm 就翻族:垂直向下时左右移动单步最大 74.8°、
|
||||||
|
累计绕出 348°,而目标实际只扫过 160°)。交互求解以<b>当前手臂姿态</b>为参考位姿,选解判据取
|
||||||
|
<b>所有电机需要转过的角度之和最小</b>——<code>Σ|wrap180(θᵢ−refᵢ)|</code>:连续轴(J1/J3/J6)无机械限位,
|
||||||
|
θ 与 θ±360 是同一物理位形,故角差一律取短弧;末尾 <code>1e-6·Σ|θ|</code> 只作并列时的次序稳定项。
|
||||||
|
同一条扫描路径实测:单步最大 5.0°、累计 160.0°,与目标实际扫过的角度一致。
|
||||||
|
拖动动画同样对连续轴取「离起始角最近的等价角」再插值,避免 +151.9°→−167.0° 反向扫 318.9° 绕一整圈。</li>
|
||||||
|
<li><b>实时续解(拖动快路)</b>:目标每帧只挪几毫米时,真解在 (θ4,θ5) 平面上连续移动,
|
||||||
|
没必要为重扫整个冗余维付全价。<code>solveWarm</code> 只在上一帧解附近取<b>同一闭式支</b>、走一小步同支搜索,
|
||||||
|
姿态任务再收口一次;<b>任一校验不过(换支、越限、位置残差超差、姿态没收口、关节跳太大)就返回 null
|
||||||
|
自动回落全量粗扫</b>,松手后的求解一定不带 warm、走全量。快路只负责「能快的时候快」,不改变正确性判据。</li>
|
||||||
|
<li><b>代表角</b>:位姿以 360° 为周期,解角一律取 ±180° 内的等价角(|θ| 最小);J2 / J4 / J5 另受相邻件实测机械区间约束,越限即丢弃。</li>
|
||||||
|
<li><b>自检</b>:每次求解都用闭式 FK 与场景图真实 6 级矩阵链各算一次 TCP 和工具姿态并与目标比对(见上表残差)。</li>
|
||||||
|
</ol>
|
||||||
|
<hr>
|
||||||
|
<b>验证数据(本机实测,可复现)</b>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>检查项</th><th>结果</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>闭式 FK vs 6 级矩阵链(1500 组随机位形)</td><td class="good">4.6 × 10⁻¹³ mm</td></tr>
|
||||||
|
<tr><td>逆解分支重构(200 组随机可行位形)</td><td class="good">200 / 200 命中</td></tr>
|
||||||
|
<tr><td>θ6 不变性(0° → 137°,TCP 位移)</td><td class="good">0.00 mm</td></tr>
|
||||||
|
<tr><td>姿态参数 ↔ 旋转矩阵往返(2000 组随机)</td><td class="good">2.4 × 10⁻⁶°</td></tr>
|
||||||
|
<tr><td>姿态重构达标率(200 组随机可行位形,step 0.5,A/B 同机)</td><td class="good">中位残差 0.0234° → <b>0.000000°</b>;>0.05° 的例数 2 → <b>0</b>(改造前文档口径 185–191/200)</td></tr>
|
||||||
|
<tr><td>姿态解 Σ|θ| vs 改造前(60 组,step 0.25 + 连续项)</td><td>中位 +0.03°,最大 +8.4°(该例是新解更贴参考位姿、Σ|θ| 反而更大,属连续项的正常取舍而非漏解)</td></tr>
|
||||||
|
<tr><td>位置解 vs 0.05° 参考网格(100 目标,step 0.25)</td><td class="good">中位 +0.000°,最大 +0.49°,>1° 例数 0</td></tr>
|
||||||
|
<tr><td>全量求解耗时(60 组,step 0.25 + 连续项)</td><td>仅位置 70.6 → 63.3 ms;位置+姿态 88.8 → 81.0 ms(约 1.10×;粗扫采样量降 4.6×,但精修才是地板)</td></tr>
|
||||||
|
<tr><td>实时续解快路(每帧 3 mm + 1.5°,240 帧)</td><td class="good">位置命中 96%、均 0.058 ms;姿态命中 65%、均 0.59 ms —— 对全量 81 ms 约 140~1400×,是交互延迟的主要收益来源</td></tr>
|
||||||
|
<tr><td>快路结果的独立复核(fkChain / 全 6 轴 orient,非求解器自报)</td><td class="good">位置 5.5×10⁻¹³ mm · 姿态 1.7×10⁻⁶°</td></tr>
|
||||||
|
<tr><td>姿态模式候选表长度(供互异构型/碰撞改选取样)</td><td>26 → 13(精确根集合本就只有 1~2 个,达标门收紧后可选样变少;动态碰撞改选若依赖宽候选需留意)</td></tr>
|
||||||
|
<tr><td>滚转角交互 roll 0→90(参考位姿连续)</td><td class="good">Δθ6 = 89.99°,θ1..θ5 变化 < 0.01°</td></tr>
|
||||||
|
<tr><td>60 个可达目标:失败次数 / 最大残差</td><td class="good">0 次 / 3.5 × 10⁻¹³ mm</td></tr>
|
||||||
|
<tr><td>相对 0.05° 极细网格参考的最大劣化</td><td class="good">0.02°</td></tr>
|
||||||
|
<tr><td>vs 4000 次随机多起点数值解</td><td class="good">0 / 20 次劣于数值解</td></tr>
|
||||||
|
<tr><td>等价分支跳变 · 4 条扫描路径(修复前 → 后)</td><td class="good">>10° 跳变 14 → 5 次;最大单步 163.5° → 16.8°</td></tr>
|
||||||
|
<tr><td>单次求解耗时</td><td>拖动跟随走快路 ~0.1 ms;全量求解默认步长下位置 ~20 ms、姿态 ~100 ms(node 侧本机实测,浏览器同量级)</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<hr>
|
||||||
|
<b>限位</b>:电机行程各轴 ±360°(=滑块范围);解角取 ±180° 内的等价角——位姿以 360° 为周期,超出 ±180° 不产生新位姿。
|
||||||
|
相邻打印件的实测机械区间封顶:J2 ∈ [−107, 128],J4 ∈ [−112, 112],J5 ∈ [−96, 96],J1 / J3 / J6 可整周。
|
||||||
|
<br><b>参考</b>:零位静态扫描 J2 [−106, 9]、J4 [−111, 16]、J5 [−95, 34] 只在其它轴全零时对固定立柱成立,已不再当可行域;
|
||||||
|
跨组零件对(腕/法兰撞立柱那类)由胶囊碰撞模型按当前姿态实时判定(实测 J1=90° 时 J2 正端从 +8° 放到 +107°)。
|
||||||
|
<br><b>局限</b>:胶囊模型故意不检相邻组零件对(|Δg|=1),那部分由上面的实测机械区间兜底;换模型须重跑
|
||||||
|
scratch/probe_adj.py 更新 collide.json 的 adj。若需要更舒展的姿态,可从上方“互异构型”列表改选次优解。
|
||||||
|
<br><b>不可达</b>:若目标超出工作空间,自动求最近可达位姿——阻尼最小二乘<b>从当前姿态锚定起步</b>(抑制退化方向的无用漂移)、固定种子的确定性重启(同一点重复解算逐位一致)、再经零空间抛光取最小总关节角的代表值;报告的距离为抛光后真实值。
|
||||||
|
<br><b>姿态不可达</b>:姿态任务超可达空间时,阻尼最小二乘误差向量升级为 6 维(位置 mm + 姿态 deg,按 20 mm/rad 折算),返回位置与姿态联合最近的位姿,两个残差分别显示。
|
||||||
|
<br><b>混合优先不可达</b>:位置超可达空间时,位置端锚点退化为「最近可达位置」、姿态端仍取「姿态精确、位置最近」,
|
||||||
|
再按同一权重折中,两端各自的残差在结果栏如实列出(与位置优先的历史兜底——6 维联合 DLS——是两个不同的代表值,切换模式可得)。
|
||||||
|
<br><b>API</b>:控制台可用 <code>IK.solver.solve([x,y,z],{step:0.25,tool:0,pose:{R:ArmIK.poseMat(az,el,roll)}})</code>;
|
||||||
|
<code>IK.setPose(az,el,roll)</code> 可直接设定目标姿态;<code>IK.setPrio('mix',0.3)</code> 切到混合优先并设比重;
|
||||||
|
<code>IK.solver.solveTo([x,y,z],{pose:{R:…},priority:'mix',mixW:0.3})</code> 直接取折中解(0 与 1 分别等于姿态优先 / 位置优先)。
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" id="jointsHead" style="justify-content:space-between;margin:8px 0 4px">
|
||||||
|
<h3 style="margin:0">关节角度(实时联动)</h3>
|
||||||
|
<button id="resetJoints" class="tab" style="padding:1px 7px;font-size:11px" title="将所有关节角度复位为 0°">全部归零</button>
|
||||||
|
</div>
|
||||||
|
<div id="joints"></div>
|
||||||
|
<div id="treeBlock">
|
||||||
|
<h3>零件树</h3>
|
||||||
|
<div class="row"><button id="allOn" class="tab">全选</button><button id="allOff" class="tab">全不选</button>
|
||||||
|
<button id="onlyPrint" class="tab">只看打印件</button></div>
|
||||||
|
<div id="tree"></div>
|
||||||
|
<h3>打印清单(PLA)</h3>
|
||||||
|
<div class="kv" id="sum"></div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div id="stage">
|
||||||
|
<canvas id="cv"></canvas>
|
||||||
|
<div id="hud"></div>
|
||||||
|
<div id="note"></div>
|
||||||
|
<div id="sheet" class="theme-paper">
|
||||||
|
<div id="sheetToolbar">
|
||||||
|
<div class="sheet-tabs" id="sheetTabs">
|
||||||
|
<button class="sheet-tab on" data-sheet="0"><span>📐</span> 01 正视图</button>
|
||||||
|
<button class="sheet-tab" data-sheet="1"><span>🔍</span> 02 A-A剖视</button>
|
||||||
|
<button class="sheet-tab" data-sheet="2"><span>💥</span> 03 爆炸图</button>
|
||||||
|
<button class="sheet-tab" data-sheet="3"><span>📐</span> 04 侧视图</button>
|
||||||
|
<button class="sheet-tab guide-tab" data-sheet="guide"><span>🛠️</span> 分步装配向导</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sheet-vsep"></div>
|
||||||
|
|
||||||
|
<div class="sheet-btn-group" title="图纸风格主题">
|
||||||
|
<button class="sheet-btn on" data-theme="paper" title="工程白纸图纸">白图</button>
|
||||||
|
<button class="sheet-btn" data-theme="blueprint" title="经典工程蓝图">蓝图</button>
|
||||||
|
<button class="sheet-btn" data-theme="dark" title="工业深色风格">深色</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sheet-vsep"></div>
|
||||||
|
|
||||||
|
<div class="sheet-layers" title="图层显示控制">
|
||||||
|
<label title="显示/隐藏气球标号与引线"><input type="checkbox" id="chkLyrBalloons" checked> 标号</label>
|
||||||
|
<label title="显示/隐藏尺寸与基准标注"><input type="checkbox" id="chkLyrDims" checked> 尺寸</label>
|
||||||
|
<label title="显示/隐藏关节轴线"><input type="checkbox" id="chkLyrAxes" checked> 轴线</label>
|
||||||
|
<label title="显示/隐藏图内零件明细表"><input type="checkbox" id="chkLyrLegend" checked> 零件表</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sheet-space"></div>
|
||||||
|
|
||||||
|
<button class="sheet-btn sheet-btn-accent" id="btnSheetTo3D" title="在三维模型中定位查看当前选中的部件">
|
||||||
|
<span>🧊 3D模型联动</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="sheetViewport">
|
||||||
|
<div id="sheetCanvas">
|
||||||
|
<div id="sheetSvgWrapper"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="sheetFloatBar">
|
||||||
|
<button class="sfb-btn" id="sfbIn" title="放大 (滚轮向上)">➕</button>
|
||||||
|
<button class="sfb-btn" id="sfbOut" title="缩小 (滚轮向下)">➖</button>
|
||||||
|
<button class="sfb-btn" id="sfbFit" title="自适应窗口居中 (双击空白处)">⛶ 适屏</button>
|
||||||
|
<button class="sfb-btn" id="sfb100" title="100% 原始比例">1:1</button>
|
||||||
|
<button class="sfb-btn" id="sfbReset" title="重置视角">↺ 复位</button>
|
||||||
|
<button class="sfb-btn" id="sfbFull" title="全屏视口">⛶ 全屏</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="sheetZoomBadge" title="点击重置为 100%">100%</div>
|
||||||
|
|
||||||
|
<div id="sheetTooltip" class="sheet-tooltip"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分步装配向导面板 -->
|
||||||
|
<div id="sheetGuideView" style="display:none">
|
||||||
|
<div class="guide-header">
|
||||||
|
<div class="guide-steps-bar" id="guideStepsBar">
|
||||||
|
<!-- 步骤 chips 动态渲染 -->
|
||||||
|
</div>
|
||||||
|
<button class="sheet-btn" id="btnExitGuide" style="margin-left:auto">✕ 返回图纸</button>
|
||||||
|
</div>
|
||||||
|
<div class="guide-body">
|
||||||
|
<div class="guide-panel-left" id="guidePanelLeft">
|
||||||
|
<!-- 当前步骤详情动态渲染 -->
|
||||||
|
</div>
|
||||||
|
<div class="guide-panel-right">
|
||||||
|
<div id="guideCanvasWrapper" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;position:relative;">
|
||||||
|
<!-- 步骤关联图纸与高亮 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<span id="footModel">左键旋转 · 滚轮缩放 · 右键平移(拖动) · 单击选中高亮 · <b>BOM 点螺丝行 = 高亮定位该规格螺钉(穿透显示)</b> · <b>右键单击隐藏部件(或按H键)</b> · 左侧全选/勾选恢复<span id="footIk"> · <b>运动模拟</b>:拖动黄色目标球到点(Shift 仅上下)· 开「姿态控制」后可拖箭头调末端朝向、拖箭杆或粉色侧球调滚转</span></span>
|
||||||
|
<span id="footSheet" style="display:none"><b>装配工程图纸</b>:滚轮缩放 · 按住左键/中键拖拽平移 · 双击自适应视口 · 悬停气球标号/零件轮廓双向高亮 · <b>点击部件卡片「在 3D 中定位」</b></span>
|
||||||
|
<span class="sp"></span>
|
||||||
|
<span id="stat"></span>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="bomTab" title="展开 / 收起 物料清单 BOM">物料清单 <b>BOM</b></button>
|
||||||
|
<aside id="bomDock">
|
||||||
|
<div class="dk-hd">
|
||||||
|
<b>物料清单 BOM</b><span class="q" id="bomStat"></span>
|
||||||
|
<button id="bomHide" class="dk-x" title="收起">收起 ▶</button>
|
||||||
|
</div>
|
||||||
|
<div class="dk-bd">
|
||||||
|
<div class="dk-sec">紧固件 / 螺丝清单(全部紧固点)</div>
|
||||||
|
<div class="dk-note" id="scrSum"></div>
|
||||||
|
<table id="screws"><thead><tr><th>规格</th><th>长 mm</th><th>数量</th><th>位置 / 用途</th></tr></thead><tbody></tbody></table>
|
||||||
|
<div class="dk-sec">打印件 BOM(原明细,PLA)</div>
|
||||||
|
<table id="bom"><thead><tr><th>#</th><th>件号</th><th>cm3</th><th>g</th></tr></thead><tbody></tbody></table>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<script src="vendor/three.min.js"></script>
|
||||||
|
<!-- 数据与螺钉清单是生成物(webapp/generated/00_data.js),由 python -m arm.viewer.build_viewer 写 -->
|
||||||
|
<script src="generated/00_data.js"></script>
|
||||||
|
<script src="js/app.js"></script>
|
||||||
|
<script src="js/ik-solver.js"></script>
|
||||||
|
<script src="js/ik-glue.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -542,7 +542,60 @@ function renderResult(res, src) {
|
|||||||
全量回落也不再卡顿。Worker 不可用(老浏览器/CSP/file 限制)时自动回落
|
全量回落也不再卡顿。Worker 不可用(老浏览器/CSP/file 限制)时自动回落
|
||||||
主线程同步求解,行为与旧版逐位一致。碰撞 dynLimit/clampAxis 仍留主线程
|
主线程同步求解,行为与旧版逐位一致。碰撞 dynLimit/clampAxis 仍留主线程
|
||||||
(滑块交互需要同步边界),主线程的 S 实例保留给 FK/渲染/控制台 API。 */
|
(滑块交互需要同步边界),主线程的 S 实例保留给 FK/渲染/控制台 API。 */
|
||||||
let IKW = null, IKWseq = 0, IKWlast = 0;
|
let IKW = null, IKWseq = 0, IKWlast = 0, IKWready = false;
|
||||||
|
/* 后端状态是给测试和人一起看的:worker=真后台线程,main=同步兜底。
|
||||||
|
旧实现失败时只把 IKW 置 false,页面照常跑但拖动会卡,谁也发现不了。 */
|
||||||
|
let IK_BACKEND = 'init';
|
||||||
|
const IK_BACKEND_LOG = [];
|
||||||
|
function ikBackend(kind, msg) {
|
||||||
|
IK_BACKEND = kind;
|
||||||
|
IK_BACKEND_LOG.push([kind, msg || '']);
|
||||||
|
if (kind === 'worker') {
|
||||||
|
console.log('[IK] 求解后端 = worker');
|
||||||
|
} else {
|
||||||
|
console.error('[IK] 求解后端 = ' + kind + (msg ? ':' + msg : ''));
|
||||||
|
const b = document.getElementById('ikBadge');
|
||||||
|
if (b) { b.textContent = '后台线程未运行 → 主线程同步求解(拖动会卡顿)'; b.className = 'badge badb'; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const ikBootCfg = () => ({ joints: JMap, travel: TR, tcp: TCP0, collide: COL });
|
||||||
|
/* 给自动化测试与控制台用:后端状态与失败原因,不依赖面板文字 */
|
||||||
|
window.__IK_BACKEND = () => IK_BACKEND;
|
||||||
|
window.__IK_BACKEND_LOG = () => IK_BACKEND_LOG.slice();
|
||||||
|
|
||||||
|
/* 首选真 Worker 文件:需要 http(file:// 下浏览器拒绝跨源脚本)。 */
|
||||||
|
function ikWorkerFromFile() {
|
||||||
|
const w = new Worker('workers/ik.worker.js');
|
||||||
|
w.postMessage({ op: 'init', cfg: ikBootCfg() });
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
/* 兜底:单文件分发版是 file:// 打开的,那里没有可加载的 worker 文件,
|
||||||
|
只能把页面里内联的求解器源码做成 Blob Worker。 */
|
||||||
|
function ikWorkerFromBlob() {
|
||||||
|
let src = null;
|
||||||
|
for (const sc of document.querySelectorAll('script')) {
|
||||||
|
if (sc.textContent.indexOf('六轴手臂逆运动学求解器') >= 0) { src = sc.textContent; break; }
|
||||||
|
}
|
||||||
|
if (!src) throw new Error('页面内没有内联求解器源码,Blob 兜底也不可用');
|
||||||
|
const boot = String.raw`
|
||||||
|
let S = null;
|
||||||
|
onmessage = e => {
|
||||||
|
const d = e.data || {};
|
||||||
|
if (d.op === 'init') { S = ArmIK.make(d.cfg); postMessage({ op: 'ready' }); return; }
|
||||||
|
let r;
|
||||||
|
try { r = S[d.op].apply(S, d.args); } catch (err) { r = { __err: String(err && err.message || err) }; }
|
||||||
|
postMessage({ id: d.id, r: r });
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
const u = URL.createObjectURL(new Blob([src + boot], { type: 'application/javascript' }));
|
||||||
|
try {
|
||||||
|
const w = new Worker(u);
|
||||||
|
w.postMessage({ op: 'init', cfg: ikBootCfg() });
|
||||||
|
return w;
|
||||||
|
} finally {
|
||||||
|
URL.revokeObjectURL(u); // Blob 只在构造时被读一次,失败路径也不留常驻对象
|
||||||
|
}
|
||||||
|
}
|
||||||
/* 每个请求按 id 记账,回包必定派发一次;「是否还是最新一帧」用第二参交给调用方判断。
|
/* 每个请求按 id 记账,回包必定派发一次;「是否还是最新一帧」用第二参交给调用方判断。
|
||||||
旧写法只留一个 IKWjob 槽:live 在飞时被 runSolve 插队,那一发的回调就彻底蒸发,
|
旧写法只留一个 IKWjob 槽:live 在飞时被 runSolve 插队,那一发的回调就彻底蒸发,
|
||||||
而它是唯一给 liveBusy 复位的地方——一次竞态即让实时追踪永久停摆。 */
|
而它是唯一给 liveBusy 复位的地方——一次竞态即让实时追踪永久停摆。 */
|
||||||
@@ -550,36 +603,44 @@ const IKWjobs = new Map();
|
|||||||
function ikWorker() {
|
function ikWorker() {
|
||||||
if (IKW) return IKW;
|
if (IKW) return IKW;
|
||||||
if (IKW === false) return null;
|
if (IKW === false) return null;
|
||||||
try {
|
/* file:// 下不能用 URL 构造 Worker(错误还是异步 onerror 才到,构造阶段不抛),
|
||||||
let src = null;
|
所以按协议选路;选错时再互相兜底。 */
|
||||||
for (const sc of document.querySelectorAll('script')) {
|
const tries = location.protocol === 'file:' ? [ikWorkerFromBlob, ikWorkerFromFile]
|
||||||
if (sc.textContent.indexOf('六轴手臂逆运动学求解器') >= 0) { src = sc.textContent; break; }
|
: [ikWorkerFromFile, ikWorkerFromBlob];
|
||||||
|
let w = null, why = null;
|
||||||
|
for (const mk of tries) {
|
||||||
|
try { w = mk(); break; } catch (e) { why = ((why ? why + ' / ' : '') + (e && e.message || e)); }
|
||||||
|
}
|
||||||
|
if (!w) { IKW = false; ikBackend('main', why); return null; }
|
||||||
|
w.onmessage = e => {
|
||||||
|
const d = e.data || {};
|
||||||
|
if (d.op === 'ready') {
|
||||||
|
IKWready = true;
|
||||||
|
clearTimeout(IKWtimer);
|
||||||
|
ikBackend('worker');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (!src) throw new Error('ArmIK script 未找到');
|
const job = IKWjobs.get(d.id);
|
||||||
const boot = '\nconst CFGW = ' + JSON.stringify({ joints: JMap, travel: TR, tcp: TCP0 }) + ';\n' +
|
if (!job) return; // 已结清过(正常不会发生)
|
||||||
'const COLW = ' + JSON.stringify(COL) + ';\n' +
|
IKWjobs.delete(d.id);
|
||||||
'const SW = ArmIK.make({ joints: CFGW.joints, travel: CFGW.travel, tcp: CFGW.tcp, collide: COLW });\n' +
|
job(d.r, d.id === IKWlast); // 过期请求也派发,只是标为不新鲜
|
||||||
'onmessage = e => { const d = e.data; let r;\n' +
|
};
|
||||||
' try { r = SW[d.op].apply(SW, d.args); } catch (err) { r = { __err: String(err && err.message || err) }; }\n' +
|
w.onerror = ev => { // Worker 挂了:把在飞的账全部结清并说清楚
|
||||||
' postMessage({ id: d.id, r: r }); };\n';
|
const pend = Array.from(IKWjobs.values()); IKWjobs.clear();
|
||||||
/* Blob URL 只在构造 Worker 时被读取,构造后立即回收(失败路径同样不留常驻 Blob) */
|
for (const j of pend) j(null, false);
|
||||||
const u = URL.createObjectURL(new Blob([src + boot], { type: 'application/javascript' }));
|
IKW = false; IKWready = false;
|
||||||
let w;
|
ikBackend('main', 'worker onerror: ' + ((ev && ev.message) || 'unknown'));
|
||||||
try { w = new Worker(u); } finally { URL.revokeObjectURL(u); }
|
};
|
||||||
w.onmessage = e => {
|
/* 握手超时同样是失败:不报就是"看起来在跑、其实一直同步" */
|
||||||
const job = IKWjobs.get(e.data.id);
|
const IKWtimer = setTimeout(() => {
|
||||||
if (!job) return; // 已结清过(正常不会发生)
|
if (!IKWready) {
|
||||||
IKWjobs.delete(e.data.id);
|
try { w.terminate(); } catch (e) {}
|
||||||
job(e.data.r, e.data.id === IKWlast); // 过期请求也派发,只是标为不新鲜
|
IKW = false; IKWready = false;
|
||||||
};
|
ikBackend('main', '3s 内没收到 worker ready');
|
||||||
w.onerror = () => { // Worker 挂了:把在飞的账全部结清
|
}
|
||||||
IKW = false;
|
}, 10000);
|
||||||
const pend = Array.from(IKWjobs.values()); IKWjobs.clear();
|
IKW = w;
|
||||||
for (const j of pend) j(null, false);
|
return IKW;
|
||||||
};
|
|
||||||
IKW = w;
|
|
||||||
} catch (err) { IKW = false; }
|
|
||||||
return IKW || null;
|
|
||||||
}
|
}
|
||||||
function solveAsync(op, args, job) {
|
function solveAsync(op, args, job) {
|
||||||
const w = ikWorker();
|
const w = ikWorker();
|
||||||
@@ -1037,24 +1098,22 @@ function boot() {
|
|||||||
const cb = $('ikChkBox');
|
const cb = $('ikChkBox');
|
||||||
if (cb) cb.addEventListener('toggle', () => { if (cb.open && !CHK.sigOk && !CHK.pose) runRecompute(); });
|
if (cb) cb.addEventListener('toggle', () => { if (cb.open && !CHK.sigOk && !CHK.pose) runRecompute(); });
|
||||||
}
|
}
|
||||||
/* 打包自检:确认所有依赖都在本文件内(便于单文件分发,防止被拆开后才出问题) */
|
/* 依赖加载自检:单文件时代这里断言"外部引用必须为 0",拆分后那条不变量正好反过来。
|
||||||
const ext = document.querySelectorAll(
|
现在报的是"每样东西到没到、从哪来、求解跑在哪个后端",右下角面板与测试都读它。 */
|
||||||
'script[src],link[href],img[src],iframe[src],source[src],embed[src],object[data],use[href]').length;
|
|
||||||
const nSheet = (typeof SHEETS !== 'undefined' && SHEETS) ? String(SHEETS).split('<svg').length - 1 : 0;
|
const nSheet = (typeof SHEETS !== 'undefined' && SHEETS) ? String(SHEETS).split('<svg').length - 1 : 0;
|
||||||
const ok = (t) => '<span class="' + (t ? 'good' : 'bad') + '">';
|
const ok = (t) => '<span class="' + (t ? 'good' : 'bad') + '">';
|
||||||
const nScript = document.querySelectorAll('script:not([src])').length;
|
const mode = location.protocol === 'file:' ? 'file://(单文件分发版)' : 'http(webapp/ 多文件)';
|
||||||
$('ikDep').innerHTML =
|
$('ikDep').innerHTML =
|
||||||
'<i>打包方式</i><span class="good">单文件自包含,可直接拷走</span>' +
|
'<i>加载方式</i><span class="good">' + mode + '</span>' +
|
||||||
'<i>three.js</i>' + ok(typeof THREE !== 'undefined') + '已内联 · r' +
|
'<i>three.js</i>' + ok(typeof THREE !== 'undefined') +
|
||||||
((typeof THREE !== 'undefined' && THREE.REVISION) || '?') + '</span>' +
|
'r' + ((typeof THREE !== 'undefined' && THREE.REVISION) || '?') + '</span>' +
|
||||||
'<i>几何数据</i>' + ok(typeof DATA !== 'undefined') + '已内联 · ' + DATA.parts.length + ' 件零件</span>' +
|
'<i>几何数据</i>' + ok(typeof DATA !== 'undefined') +
|
||||||
|
((typeof DATA !== 'undefined') ? DATA.parts.length + ' 件零件' : '缺失') + '</span>' +
|
||||||
'<i>关节 / 限位</i>' + ok(typeof JO !== 'undefined' && typeof TRAVEL !== 'undefined') +
|
'<i>关节 / 限位</i>' + ok(typeof JO !== 'undefined' && typeof TRAVEL !== 'undefined') +
|
||||||
'已内联 · ' + JO.length + ' 轴 / ' + TRAVEL.length + ' 条限位</span>' +
|
JO.length + ' 轴 / ' + TRAVEL.length + ' 条限位</span>' +
|
||||||
'<i>二维图纸</i>' + ok(nSheet > 0) + '已内联 · ' + nSheet + ' 张 SVG</span>' +
|
'<i>二维图纸</i>' + ok(nSheet > 0) + nSheet + ' 张 SVG</span>' +
|
||||||
'<i>IK 求解器</i>' + ok(typeof ArmIK !== 'undefined') + '已内联(纯 JS,无依赖)</span>' +
|
'<i>IK 求解器</i>' + ok(typeof ArmIK !== 'undefined') + 'ArmIK</span>' +
|
||||||
'<i>内联脚本</i><span>' + nScript + ' 个(三维引擎 / 模型数据 / 求解器 / 界面)</span>' +
|
'<i>求解后端</i>' + ok(IK_BACKEND === 'worker') + IK_BACKEND + '</span>';
|
||||||
'<i>外部引用</i>' + ok(ext === 0) +
|
|
||||||
(ext === 0 ? '0 个(script/link/img/iframe 等全类型已查)' : ext + ' 个 —— 已破坏自包含!') + '</span>';
|
|
||||||
|
|
||||||
const q = new URLSearchParams(location.search);
|
const q = new URLSearchParams(location.search);
|
||||||
const tgt = q.get('tgt'); // tgt=x,y,z:直接以该目标进入运动模拟(复现用)
|
const tgt = q.get('tgt'); // tgt=x,y,z:直接以该目标进入运动模拟(复现用)
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/* 设计令牌、reset、整体栅格与左栏/树/页脚 */
|
||||||
|
:root{--bg:#0f1115;--pan:#171a21;--pan2:#1f242e;--tx:#e6e8ee;--dim:#8b93a7;--ac:#4c9aff;--ok:#3ddc97;--warn:#ffb454;--line:#2b323e}
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
html,body{margin:0;height:100%;background:var(--bg);color:var(--tx);
|
||||||
|
font:13px/1.55 "Microsoft YaHei","Segoe UI",system-ui,sans-serif;overflow:hidden;
|
||||||
|
-webkit-user-select:none;user-select:none}
|
||||||
|
#app{display:grid;grid-template-columns:322px minmax(430px,1fr);grid-template-rows:46px auto 1fr 26px;
|
||||||
|
height:100%;min-width:0;overflow-x:auto}
|
||||||
|
header{grid-column:1/-1;display:flex;align-items:center;gap:8px;padding:0 12px;flex-wrap:nowrap;
|
||||||
|
overflow-x:auto;overflow-y:hidden;background:var(--pan);border-bottom:1px solid var(--line)}
|
||||||
|
header .q{white-space:nowrap;flex:none;color:var(--dim);font-size:12px}
|
||||||
|
header b{font-size:14px;white-space:nowrap;flex:none}
|
||||||
|
header .sp{flex:1 1 auto;min-width:6px}
|
||||||
|
button,select{background:var(--pan2);color:var(--tx);border:1px solid var(--line);
|
||||||
|
border-radius:5px;padding:4px 9px;font:inherit;cursor:pointer;white-space:nowrap;flex:none}
|
||||||
|
button:hover{border-color:var(--ac);color:#fff}
|
||||||
|
button.on{background:#22406b;border-color:var(--ac)}
|
||||||
|
#bHarn.on{background:#5c3b1e;border-color:#f59e0b;color:#fbbf24}
|
||||||
|
aside{background:var(--pan);overflow:auto;padding:8px;min-width:0}
|
||||||
|
#left{border-right:1px solid var(--line)}
|
||||||
|
#app.l{grid-template-columns:322px minmax(430px,1fr)}
|
||||||
|
#stage{position:relative;overflow:hidden;min-width:0;
|
||||||
|
background:radial-gradient(120% 120% at 50% 0%,#1b2028 0%,#0d0f13 70%)}
|
||||||
|
canvas{display:block;width:100%;height:100%}
|
||||||
|
footer{grid-column:1/-1;background:var(--pan);border-top:1px solid var(--line);
|
||||||
|
display:flex;align-items:center;gap:14px;padding:0 12px;color:var(--dim);font-size:12px;
|
||||||
|
white-space:nowrap;overflow:hidden}
|
||||||
|
@media (max-width:1150px){
|
||||||
|
#app{grid-template-columns:300px minmax(400px,1fr)}
|
||||||
|
}
|
||||||
|
@media (max-width:760px){
|
||||||
|
#app{grid-template-columns:minmax(0,1fr)}
|
||||||
|
#left{display:none}
|
||||||
|
#app.l #left{display:block}
|
||||||
|
}
|
||||||
|
h3{font-size:12px;margin:10px 0 6px;color:var(--ac);letter-spacing:.4px;-webkit-user-select:none;user-select:none}
|
||||||
|
.row{display:flex;align-items:center;gap:6px;margin:3px 0}
|
||||||
|
.row label{flex:1;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;-webkit-user-select:none;user-select:none}
|
||||||
|
.row .q{color:var(--dim);font-size:11px;-webkit-user-select:none;user-select:none}
|
||||||
|
.chip{display:inline-block;width:9px;height:9px;border-radius:2px;margin-right:5px;flex:none}
|
||||||
|
.pr{color:var(--ok)}.ex{color:var(--warn)}.mo{color:#9aa4b8}
|
||||||
|
table{width:100%;border-collapse:collapse;font-size:11.5px;-webkit-user-select:text;user-select:text}
|
||||||
|
th,td{border-bottom:1px solid var(--line);padding:3px 4px;text-align:left;vertical-align:top}
|
||||||
|
th{color:var(--dim);font-weight:400;position:sticky;top:0;background:var(--pan);-webkit-user-select:none;user-select:none}
|
||||||
|
.j-box{margin-bottom:7px;padding:3px 4px 1px;border-radius:4px;transition:background .15s}
|
||||||
|
.j-box:hover{background:rgba(255,255,255,.03)}
|
||||||
|
.j-box label{cursor:default;font-weight:500}
|
||||||
|
.j-val{font-size:11px;color:var(--ac);cursor:pointer;padding:1px 5px;border-radius:3px;
|
||||||
|
background:rgba(76,154,255,.1);font-variant-numeric:tabular-nums;transition:all .15s;
|
||||||
|
-webkit-user-select:none;user-select:none}
|
||||||
|
.j-val:hover{background:var(--ac);color:#fff}
|
||||||
|
input[type=range]{width:100%;height:18px;accent-color:var(--ac);cursor:pointer;display:block;margin:1px 0}
|
||||||
|
label em{font-style:normal;color:var(--mu);font-size:10.5px;margin-left:4px}
|
||||||
|
label em.ok{color:#5ddc90}
|
||||||
|
.gr{font-size:11px;color:var(--dim);margin:2px 0 4px;-webkit-user-select:none;user-select:none}
|
||||||
|
#hud{position:absolute;left:10px;top:8px;pointer-events:none;font-size:11.5px;color:var(--dim);
|
||||||
|
background:rgba(15,17,21,.72);border:1px solid var(--line);border-radius:6px;padding:6px 9px;max-width:60%}
|
||||||
|
#hud b{color:var(--tx)}
|
||||||
|
#hud:empty{display:none}
|
||||||
|
#note{position:absolute;right:10px;bottom:10px;max-width:330px;background:rgba(15,17,21,.88);
|
||||||
|
border:1px solid var(--ac);border-radius:7px;padding:8px 10px;font-size:12px;display:none;
|
||||||
|
-webkit-user-select:text;user-select:text}
|
||||||
|
.kv{display:grid;grid-template-columns:auto 1fr;gap:2px 8px;font-size:11.5px;-webkit-user-select:text;user-select:text}
|
||||||
|
.kv i{color:var(--dim);font-style:normal;-webkit-user-select:none;user-select:none}
|
||||||
|
.tab{padding:4px 9px;-webkit-user-select:none;user-select:none}
|
||||||
|
.bad{color:#ff6b6b}.good{color:var(--ok)}
|
||||||
|
svg{display:block;width:100%;height:auto;background:#fff;border-radius:6px}
|
||||||
|
#sheet{display:none;padding:10px;overflow:auto;height:100%}
|
||||||
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/* 右边缘可收起 BOM 抽屉 */
|
||||||
|
/* ---- edge BOM dock (collapsible drawer at the right edge) ---- */
|
||||||
|
#bomTab{position:fixed;right:0;top:64px;z-index:30;writing-mode:vertical-rl;text-orientation:mixed;
|
||||||
|
background:var(--pan2);color:var(--tx);border:1px solid var(--line);border-right:none;
|
||||||
|
border-radius:6px 0 0 6px;padding:12px 6px;font:inherit;cursor:pointer;letter-spacing:3px}
|
||||||
|
#bomTab:hover{border-color:var(--ac);color:#fff}
|
||||||
|
#bomTab b{color:var(--ac)}
|
||||||
|
#bomDock{position:fixed;right:0;top:46px;bottom:26px;width:min(430px,94vw);z-index:29;
|
||||||
|
transform:translateX(103%);transition:transform .22s ease;display:flex;flex-direction:column;
|
||||||
|
background:var(--pan);border-left:1px solid var(--ac);box-shadow:-10px 0 28px rgba(0,0,0,.5)}
|
||||||
|
#bomDock.open{transform:none}
|
||||||
|
#bomDock .dk-hd{display:flex;align-items:center;gap:8px;padding:9px 10px;flex:none;
|
||||||
|
border-bottom:1px solid var(--line)}
|
||||||
|
#bomDock .dk-hd b{font-size:13px}
|
||||||
|
#bomDock .dk-hd .q{color:var(--dim);font-size:11px;min-width:0;flex:0 1 auto;
|
||||||
|
overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||||
|
#bomDock .dk-x{margin-left:auto}
|
||||||
|
#bomDock .dk-bd{overflow:auto;padding:8px 11px 16px;flex:1;min-height:0}
|
||||||
|
.dk-sec{margin:4px 0 5px;color:var(--ac);font-size:12px;letter-spacing:.4px}
|
||||||
|
.dk-note{color:var(--dim);font-size:11px;margin:1px 0 6px}
|
||||||
|
#screws td{white-space:normal}
|
||||||
|
#screws td:first-child{white-space:nowrap}
|
||||||
|
#screws tbody tr{cursor:pointer;transition:background .12s}
|
||||||
|
#screws tbody tr:hover{background:rgba(76,154,255,.14)}
|
||||||
|
#screws tbody tr.scrHl{background:#1d3f74;box-shadow:inset 3px 0 0 var(--warn)}
|
||||||
|
.hudlink{pointer-events:auto;cursor:pointer;color:var(--ac);text-decoration:underline;margin-left:8px}
|
||||||
|
@media (max-width:760px){#bomTab{top:auto;bottom:36px}#bomDock{top:46px;bottom:26px}}
|
||||||
|
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
/* 分步装配向导面板 */
|
||||||
|
/* 分步装配向导面板样式 */
|
||||||
|
#sheetGuideView {
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0; right: 0; bottom: 0;
|
||||||
|
background: var(--bg);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
z-index: 15;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.guide-header {
|
||||||
|
padding: 8px 14px;
|
||||||
|
background: var(--pan);
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
.guide-steps-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
overflow-x: auto;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.guide-step-chip {
|
||||||
|
background: var(--pan2);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
font-size: 11.5px;
|
||||||
|
color: var(--dim);
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: all .15s;
|
||||||
|
}
|
||||||
|
.guide-step-chip:hover {
|
||||||
|
border-color: var(--warn);
|
||||||
|
color: var(--tx);
|
||||||
|
}
|
||||||
|
.guide-step-chip.active {
|
||||||
|
background: #78350f;
|
||||||
|
border-color: #f59e0b;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
box-shadow: 0 0 10px rgba(245,158,11,.3);
|
||||||
|
}
|
||||||
|
.guide-body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 360px 1fr;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.guide-body {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.guide-panel-left {
|
||||||
|
background: var(--pan);
|
||||||
|
border-right: 1px solid var(--line);
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 14px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.guide-panel-right {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #0f1217;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.guide-card {
|
||||||
|
background: var(--pan2);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
.guide-card-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--warn);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.guide-part-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.guide-part-badge {
|
||||||
|
background: rgba(255,255,255,.05);
|
||||||
|
border: 1px solid rgba(255,255,255,.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 3px 7px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--tx);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all .15s;
|
||||||
|
}
|
||||||
|
.guide-part-badge:hover {
|
||||||
|
border-color: var(--ac);
|
||||||
|
background: rgba(76,154,255,.15);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.guide-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
.guide-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 7px 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 5px;
|
||||||
|
transition: all .15s;
|
||||||
|
}
|
||||||
|
.guide-btn-nav {
|
||||||
|
background: var(--pan2);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
color: var(--tx);
|
||||||
|
}
|
||||||
|
.guide-btn-nav:hover {
|
||||||
|
border-color: var(--ac);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.guide-btn-3d {
|
||||||
|
background: linear-gradient(135deg, #b45309, #d97706);
|
||||||
|
border: 1px solid #f59e0b;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.guide-btn-3d:hover {
|
||||||
|
background: linear-gradient(135deg, #d97706, #f59e0b);
|
||||||
|
}
|
||||||
|
small{color:var(--dim)}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/* IK 控制条与左栏结果面板 */
|
||||||
|
/* ===================== IK 控制条(两行:目标+姿态 / 模式+状态) ===================== */
|
||||||
|
#ikbar{grid-column:1/-1;display:flex;flex-direction:column;gap:4px;padding:5px 10px;
|
||||||
|
background:linear-gradient(180deg,#1c212b,#161922);border-bottom:1px solid var(--line)}
|
||||||
|
#ikbar .ikrow{display:flex;align-items:center;gap:6px;min-height:26px;
|
||||||
|
overflow-x:auto;overflow-y:hidden;white-space:nowrap}
|
||||||
|
#ikbar .ik-t{color:var(--ac);font-weight:700;letter-spacing:.8px;flex:none;font-size:11.5px;
|
||||||
|
border:1px solid var(--ac);border-radius:4px;padding:1px 6px}
|
||||||
|
#ikbar .lb{color:var(--dim);flex:none;font-size:12px}
|
||||||
|
#ikbar input[type=number]{width:68px;background:#0d1015;color:var(--tx);border:1px solid var(--line);
|
||||||
|
border-radius:4px;padding:3px 5px;font:inherit;font-variant-numeric:tabular-nums;flex:none}
|
||||||
|
#ikbar input[type=number]:focus{outline:none;border-color:var(--ac)}
|
||||||
|
#ikbar .vsep{width:1px;height:20px;background:var(--line);flex:none;margin:0 3px}
|
||||||
|
#ikbar .st{color:var(--dim);flex:none;font-size:11.5px;font-variant-numeric:tabular-nums}
|
||||||
|
#ikbar .sep{color:#3b4453;margin:0 5px}
|
||||||
|
#ikbar .hint{color:var(--dim);font-size:11px;flex:none}
|
||||||
|
#ikbar .good{color:var(--ok)} #ikbar .bad{color:#ff6b6b} #ikbar .hi{color:var(--warn)}
|
||||||
|
#ikSolve{background:#1d3f74;border-color:var(--ac);color:#fff;font-weight:600}
|
||||||
|
#ikSolve:hover{background:#24508f}
|
||||||
|
#ikPose.on{background:#3a2b63;border-color:#a06bff;color:#fff;font-weight:600}
|
||||||
|
#ikPoseBox{display:none;align-items:center;gap:6px}
|
||||||
|
#ikPoseBox input[type=number]{width:54px}
|
||||||
|
#ikMixBox{display:none;align-items:center;gap:6px;flex:none}
|
||||||
|
#ikMixBox input[type=range]{width:118px;flex:none;accent-color:var(--ac);cursor:pointer}
|
||||||
|
#ikMixBox .st{min-width:56px}
|
||||||
|
|
||||||
|
/* ===================== IK 结果面板(左栏顶部) ===================== */
|
||||||
|
#ikpanel{margin:0 0 10px;border:1px solid var(--line);border-radius:6px;background:#141821;padding:7px 8px}
|
||||||
|
/* 徽标必须可换行:CJK 的最小内容宽度=一个字,若徽标 nowrap+flex:none,
|
||||||
|
长状态文案会把标题压成竖排一字一行(实测 h3 缩到 12px) */
|
||||||
|
#ikpanel .ikh{display:flex;flex-wrap:wrap;align-items:center;gap:4px 6px;margin-bottom:5px}
|
||||||
|
#ikpanel .ikh h3{margin:0;font-size:12px;color:var(--ac);letter-spacing:.3px;
|
||||||
|
min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||||
|
.badge{margin-left:auto;font-size:10.5px;padding:1px 7px;border-radius:9px;border:1px solid;
|
||||||
|
flex:0 1 auto;max-width:100%;text-align:center;line-height:1.35;text-wrap:balance}
|
||||||
|
.badge.wait{color:var(--dim);border-color:var(--line)}
|
||||||
|
.badge.goodb{color:#0c1a12;background:var(--ok);border-color:var(--ok)}
|
||||||
|
.badge.warnb{color:#241a06;background:var(--warn);border-color:var(--warn)}
|
||||||
|
.badge.badb{color:#2a0a0a;background:#ff6b6b;border-color:#ff6b6b}
|
||||||
|
#ikTab{font-size:11px}
|
||||||
|
#ikTab td.num{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}
|
||||||
|
#ikTab td.dim{color:var(--dim);white-space:nowrap}
|
||||||
|
#ikpanel .kv{margin-top:6px}
|
||||||
|
#ikpanel .kv i{white-space:nowrap}
|
||||||
|
#ikpanel .kv span,#ikpanel .kv b{overflow-wrap:anywhere;word-break:break-word;min-width:0}
|
||||||
|
#ikpanel .good{color:var(--ok)} #ikpanel .bad{color:#ff6b6b}
|
||||||
|
#ikpanel .kv .hi{color:var(--warn);font-variant-numeric:tabular-nums}
|
||||||
|
#ikAltN{font-size:10.5px;color:var(--dim);margin:7px 0 3px;line-height:1.45;white-space:normal}
|
||||||
|
.altrow{display:flex;align-items:center;gap:5px;padding:2px 4px;border-radius:4px;cursor:pointer;
|
||||||
|
font-size:11px;font-variant-numeric:tabular-nums;color:var(--dim)}
|
||||||
|
.altrow:hover{background:rgba(76,154,255,.16)}
|
||||||
|
.altrow .ai{color:#5b6478;width:10px;flex:none;text-align:right}
|
||||||
|
.altrow b{color:var(--ac);width:54px;flex:none;text-align:right;font-weight:500}
|
||||||
|
.altrow .aq{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||||
|
#ikpanel details{margin-top:8px;border-top:1px solid var(--line);padding-top:6px}
|
||||||
|
#ikpanel details>summary{cursor:pointer;color:var(--ac);font-size:11.5px;outline:none}
|
||||||
|
#ikpanel details .doc{margin-top:5px;font-size:11px;color:var(--dim);line-height:1.7;white-space:normal;
|
||||||
|
-webkit-user-select:text;user-select:text}
|
||||||
|
#ikpanel details .doc b{color:var(--tx)}
|
||||||
|
#ikpanel details .doc code{color:var(--warn);font-family:Consolas,"Courier New",monospace;font-size:10.5px}
|
||||||
|
#ikpanel details .doc ol{margin:3px 0 3px 16px;padding:0}
|
||||||
|
#ikpanel details .doc li{margin:3px 0}
|
||||||
|
#ikpanel details .doc hr{border:none;border-top:1px solid var(--line);margin:6px 0}
|
||||||
|
#ikpanel .src{font-size:10.5px;color:var(--dim);margin-top:5px;white-space:normal;line-height:1.5}
|
||||||
|
#ikChkBox{margin-top:7px}
|
||||||
|
#ikChkBox summary{margin-bottom:2px}
|
||||||
|
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
/* 装配图工具栏、页签、主视口与画布 */
|
||||||
|
/* ==================== 交互式装配图查看器 (Interactive Assembly Drawing) ==================== */
|
||||||
|
#sheet {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0; width: 100%; height: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #11141a;
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 10;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
body.sheet-mode #sheet {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
/* 隐藏 3D 专属工具条按钮(当处于装配图时) */
|
||||||
|
body.sheet-mode #bAsm,
|
||||||
|
body.sheet-mode #bExp,
|
||||||
|
body.sheet-mode #exp,
|
||||||
|
body.sheet-mode #cam,
|
||||||
|
body.sheet-mode #bWire,
|
||||||
|
body.sheet-mode #bXRay,
|
||||||
|
body.sheet-mode #bScr,
|
||||||
|
body.sheet-mode #bHarn,
|
||||||
|
body.sheet-mode #bEnv,
|
||||||
|
body.sheet-mode #bFit {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
body.sheet-mode footer #footModel { display: none !important; }
|
||||||
|
body.sheet-mode footer #footSheet { display: inline !important; }
|
||||||
|
|
||||||
|
/* 顶部装配图工具栏 */
|
||||||
|
#sheetToolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: var(--pan);
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
flex: none;
|
||||||
|
min-height: 40px;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.sheet-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.sheet-tab {
|
||||||
|
background: var(--pan2);
|
||||||
|
color: var(--tx);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
transition: all .15s ease;
|
||||||
|
}
|
||||||
|
.sheet-tab:hover {
|
||||||
|
border-color: var(--ac);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.sheet-tab.on {
|
||||||
|
background: #1e3a66;
|
||||||
|
border-color: var(--ac);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 0 10px rgba(76,154,255,.25);
|
||||||
|
}
|
||||||
|
.sheet-tab.guide-tab {
|
||||||
|
background: rgba(245, 158, 11, 0.12);
|
||||||
|
border-color: rgba(245, 158, 11, 0.4);
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
|
.sheet-tab.guide-tab.on {
|
||||||
|
background: #78350f;
|
||||||
|
border-color: #f59e0b;
|
||||||
|
color: #fef3c7;
|
||||||
|
box-shadow: 0 0 10px rgba(245, 158, 11, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheet-vsep {
|
||||||
|
width: 1px;
|
||||||
|
height: 20px;
|
||||||
|
background: var(--line);
|
||||||
|
margin: 0 4px;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
.sheet-btn-group {
|
||||||
|
display: flex;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
.sheet-btn-group .sheet-btn {
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 3px 8px;
|
||||||
|
font-size: 11.5px;
|
||||||
|
background: var(--pan2);
|
||||||
|
border-right: 1px solid var(--line);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.sheet-btn-group .sheet-btn:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
.sheet-btn-group .sheet-btn.on {
|
||||||
|
background: var(--ac);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.sheet-layers {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 11.5px;
|
||||||
|
color: var(--dim);
|
||||||
|
}
|
||||||
|
.sheet-layers label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.sheet-layers input[type=checkbox] {
|
||||||
|
accent-color: var(--ac);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.sheet-space {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 8px;
|
||||||
|
}
|
||||||
|
.sheet-btn-accent {
|
||||||
|
background: linear-gradient(135deg, #1d4ed8, #2563eb);
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid #60a5fa;
|
||||||
|
font-weight: 500;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all .15s;
|
||||||
|
}
|
||||||
|
.sheet-btn-accent:hover {
|
||||||
|
background: linear-gradient(135deg, #2563eb, #3b82f6);
|
||||||
|
box-shadow: 0 0 12px rgba(96,165,250,.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主视口区域 */
|
||||||
|
#sheetViewport {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: grab;
|
||||||
|
background: #0f1217;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
#sheetViewport.is-dragging {
|
||||||
|
cursor: grabbing !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 画布与缩放平移 */
|
||||||
|
#sheetCanvas {
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0;
|
||||||
|
transform-origin: 0 0;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
#sheetSvgWrapper {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
box-shadow: 0 8px 32px rgba(0,0,0,.5);
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
#sheetSvgWrapper svg {
|
||||||
|
display: block;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
/* 悬停 Tooltip 与 SVG 零件高亮 */
|
||||||
|
/* 悬停 Tooltip HUD */
|
||||||
|
.sheet-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 99;
|
||||||
|
background: rgba(15, 17, 21, 0.94);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid var(--ac);
|
||||||
|
border-radius: 7px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--tx);
|
||||||
|
box-shadow: 0 10px 28px rgba(0,0,0,.6);
|
||||||
|
pointer-events: none;
|
||||||
|
display: none;
|
||||||
|
max-width: 310px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.sheet-tooltip.pinned {
|
||||||
|
pointer-events: auto;
|
||||||
|
border-color: #38bdf8;
|
||||||
|
box-shadow: 0 12px 36px rgba(0, 168, 255, 0.35);
|
||||||
|
}
|
||||||
|
.sheet-tooltip .st-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,.1);
|
||||||
|
}
|
||||||
|
.sheet-tooltip .st-tag {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
.st-tag.pr { background: rgba(61,220,151,.2); color: var(--ok); }
|
||||||
|
.st-tag.ex { background: rgba(255,180,84,.2); color: var(--warn); }
|
||||||
|
.sheet-tooltip .st-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--dim);
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
.sheet-tooltip .st-row span:last-child {
|
||||||
|
color: var(--tx);
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.sheet-tooltip .st-btn {
|
||||||
|
margin-top: 6px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 4px 0;
|
||||||
|
font-size: 11px;
|
||||||
|
background: #1d4ed8;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
transition: background .15s;
|
||||||
|
}
|
||||||
|
.sheet-tooltip .st-btn:hover {
|
||||||
|
background: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SVG 交互高亮样式 */
|
||||||
|
.sheet-part-path {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: stroke .12s, stroke-width .12s, filter .12s;
|
||||||
|
}
|
||||||
|
.sheet-part-path.is-hovered {
|
||||||
|
stroke: #00d2ff !important;
|
||||||
|
stroke-width: 2.8px !important;
|
||||||
|
fill: rgba(0, 210, 255, 0.16) !important;
|
||||||
|
filter: drop-shadow(0 0 5px #00a8ff) !important;
|
||||||
|
}
|
||||||
|
.sheet-balloon {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform .15s ease;
|
||||||
|
}
|
||||||
|
.sheet-balloon:hover .balloon-circle,
|
||||||
|
.sheet-balloon.is-hovered .balloon-circle {
|
||||||
|
fill: #0284c7 !important;
|
||||||
|
stroke: #ffffff !important;
|
||||||
|
stroke-width: 2px !important;
|
||||||
|
filter: drop-shadow(0 0 6px rgba(2,132,199,.8)) !important;
|
||||||
|
}
|
||||||
|
.sheet-balloon:hover .balloon-text,
|
||||||
|
.sheet-balloon.is-hovered .balloon-text {
|
||||||
|
fill: #ffffff !important;
|
||||||
|
font-weight: bold !important;
|
||||||
|
}
|
||||||
|
.sheet-leader.is-hovered {
|
||||||
|
stroke: #00d2ff !important;
|
||||||
|
stroke-width: 1.8px !important;
|
||||||
|
}
|
||||||
|
.sheet-legend-row {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.sheet-legend-row:hover .legend-row-bg,
|
||||||
|
.sheet-legend-row.is-hovered .legend-row-bg {
|
||||||
|
fill: rgba(2, 132, 199, 0.3) !important;
|
||||||
|
}
|
||||||
|
.sheet-legend-row:hover text,
|
||||||
|
.sheet-legend-row.is-hovered text {
|
||||||
|
fill: #00d2ff !important;
|
||||||
|
font-weight: bold !important;
|
||||||
|
}
|
||||||
|
.joint-geom-part {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all .15s;
|
||||||
|
}
|
||||||
|
.joint-geom-part:hover, .joint-geom-part.is-hovered {
|
||||||
|
stroke: #00d2ff !important;
|
||||||
|
stroke-width: 2.2px !important;
|
||||||
|
fill: rgba(0, 210, 255, 0.15) !important;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
/* 图纸主题(白图/蓝图/深色)、浮动工具条、缩放徽标、信息条 */
|
||||||
|
/* 视觉风格主题 */
|
||||||
|
#sheet.theme-paper #sheetViewport {
|
||||||
|
background: #cbd5e1;
|
||||||
|
}
|
||||||
|
#sheet.theme-paper #sheetSvgWrapper {
|
||||||
|
box-shadow: 0 10px 40px rgba(0,0,0,.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
#sheet.theme-blueprint #sheetViewport {
|
||||||
|
background: #06182c;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(14, 116, 144, 0.15) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(14, 116, 144, 0.15) 1px, transparent 1px);
|
||||||
|
background-size: 24px 24px;
|
||||||
|
}
|
||||||
|
#sheet.theme-blueprint #sheetSvgWrapper {
|
||||||
|
box-shadow: 0 0 40px rgba(14, 165, 233, 0.25);
|
||||||
|
border: 1px solid rgba(56, 189, 248, 0.3);
|
||||||
|
}
|
||||||
|
#sheet.theme-blueprint .sheet-bg {
|
||||||
|
fill: #0a2240 !important;
|
||||||
|
}
|
||||||
|
#sheet.theme-blueprint .sheet-border-outer,
|
||||||
|
#sheet.theme-blueprint .sheet-border-inner,
|
||||||
|
#sheet.theme-blueprint .sheet-title-line,
|
||||||
|
#sheet.theme-blueprint .sheet-meta-line {
|
||||||
|
stroke: #38bdf8 !important;
|
||||||
|
}
|
||||||
|
#sheet.theme-blueprint .sheet-title-text {
|
||||||
|
fill: #f0f9ff !important;
|
||||||
|
}
|
||||||
|
#sheet.theme-blueprint .sheet-note-text,
|
||||||
|
#sheet.theme-blueprint .sheet-scale-text,
|
||||||
|
#sheet.theme-blueprint .sheet-ver-text {
|
||||||
|
fill: #bae6fd !important;
|
||||||
|
}
|
||||||
|
#sheet.theme-blueprint .sheet-axis,
|
||||||
|
#sheet.theme-blueprint .sheet-axis-label,
|
||||||
|
#sheet.theme-blueprint .sheet-dim-line,
|
||||||
|
#sheet.theme-blueprint .sheet-dim-ext,
|
||||||
|
#sheet.theme-blueprint .sheet-dim-text {
|
||||||
|
stroke: #7dd3fc !important;
|
||||||
|
fill: #7dd3fc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sheet.theme-dark #sheetViewport {
|
||||||
|
background: #090b0e;
|
||||||
|
}
|
||||||
|
#sheet.theme-dark .sheet-bg {
|
||||||
|
fill: #14171d !important;
|
||||||
|
}
|
||||||
|
#sheet.theme-dark .sheet-border-outer,
|
||||||
|
#sheet.theme-dark .sheet-border-inner,
|
||||||
|
#sheet.theme-dark .sheet-title-line,
|
||||||
|
#sheet.theme-dark .sheet-meta-line {
|
||||||
|
stroke: #374151 !important;
|
||||||
|
}
|
||||||
|
#sheet.theme-dark .sheet-title-text {
|
||||||
|
fill: #f3f4f6 !important;
|
||||||
|
}
|
||||||
|
#sheet.theme-dark .sheet-note-text,
|
||||||
|
#sheet.theme-dark .sheet-scale-text,
|
||||||
|
#sheet.theme-dark .sheet-ver-text {
|
||||||
|
fill: #9ca3af !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 浮动工具条 */
|
||||||
|
#sheetFloatBar {
|
||||||
|
position: absolute;
|
||||||
|
right: 14px;
|
||||||
|
top: 14px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
z-index: 25;
|
||||||
|
}
|
||||||
|
.sfb-btn {
|
||||||
|
background: rgba(23, 26, 33, 0.88);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
color: var(--tx);
|
||||||
|
border: 1px solid rgba(255,255,255,.12);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 11.5px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,.35);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 36px;
|
||||||
|
transition: all .15s ease;
|
||||||
|
}
|
||||||
|
.sfb-btn:hover {
|
||||||
|
background: #1e3a66;
|
||||||
|
border-color: var(--ac);
|
||||||
|
color: #fff;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 缩放指示徽标 */
|
||||||
|
#sheetZoomBadge {
|
||||||
|
position: absolute;
|
||||||
|
right: 14px;
|
||||||
|
bottom: 12px;
|
||||||
|
background: rgba(15, 17, 21, 0.82);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
color: var(--dim);
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
#sheetZoomBadge:hover {
|
||||||
|
color: var(--ac);
|
||||||
|
border-color: var(--ac);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 信息条 */
|
||||||
|
#sheetInfoBar {
|
||||||
|
position: absolute;
|
||||||
|
left: 14px;
|
||||||
|
bottom: 12px;
|
||||||
|
max-width: 70%;
|
||||||
|
background: rgba(15, 17, 21, 0.85);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 11.5px;
|
||||||
|
color: var(--dim);
|
||||||
|
z-index: 20;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
#sheetInfoBar b {
|
||||||
|
color: var(--tx);
|
||||||
|
}
|
||||||
|
.sheet-info-sep {
|
||||||
|
color: var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/* 视图模式切换时的布局与显隐规则 */
|
||||||
|
/* ============ 视图模式:三维模型(静态) / 装配图 / 运动模拟 ============ */
|
||||||
|
/* 三维模型 与 装配图 下,全部运动学 UI 一律隐藏 */
|
||||||
|
body:not(.motion) #ikbar,
|
||||||
|
body:not(.motion) #ikpanel,
|
||||||
|
body:not(.motion) #joints,
|
||||||
|
body:not(.motion) #jointsHead,
|
||||||
|
body:not(.motion) #footIk{display:none!important}
|
||||||
|
/* 运动模拟只看运动:左栏去掉零件树与打印清单(其它模式保留) */
|
||||||
|
body.motion #treeBlock{display:none!important}
|
||||||
|
|
||||||
|
/* 运动模拟左栏紧凑化:目标一屏放下、不下拉(实测原需下拉 127px) */
|
||||||
|
#ikpanel{margin:0 0 6px;padding:5px 8px}
|
||||||
|
#ikpanel .ikh{margin-bottom:3px}
|
||||||
|
#ikpanel #ikTab th,#ikpanel #ikTab td{padding:1px 4px}
|
||||||
|
#ikAltN{margin:5px 0 2px}
|
||||||
|
.altrow{padding:1px 4px}
|
||||||
|
#joints .j-box{margin-bottom:4px}
|
||||||
|
#joints input[type=range]{height:15px}
|
||||||
|
/* 显式行定位:IK 控制条在「三维模型/装配图」下被移除后,
|
||||||
|
若不固定行号,网格自动排布会把页脚挤到 1fr 行并压扁(已实测) */
|
||||||
|
#app>header{grid-row:1}
|
||||||
|
#ikbar{grid-row:2}
|
||||||
|
#left{grid-row:3}
|
||||||
|
#stage{grid-row:3}
|
||||||
|
#app>footer{grid-row:4}
|
||||||
|
/* BOM 抽屉:运动模拟多一条两行 IK 控制条(实测 75px),需整体下移 */
|
||||||
|
#bomDock{top:46px} #bomTab{top:64px}
|
||||||
|
body.motion #bomDock{top:124px} body.motion #bomTab{top:140px}
|
||||||
|
@media (max-width:760px){
|
||||||
|
#bomTab,body.motion #bomTab{top:auto;bottom:36px}
|
||||||
|
#bomDock,body.motion #bomDock{top:86px;bottom:26px}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/* IK 求解 Worker(module 之外的经典 Worker,用 importScripts 取 UMD 求解器)。
|
||||||
|
协议:主线程先发 {op:'init', cfg} → 回 {op:'ready'};之后 {id, op, args} → {id, r}。
|
||||||
|
旧实现是从页面 <script> 里刮求解器源码再拼 boot 字符串,那是单文件时代的产物:
|
||||||
|
拆成 <script src> 之后 textContent 为空,会静默退回主线程同步求解。 */
|
||||||
|
importScripts('../js/ik-solver.js');
|
||||||
|
|
||||||
|
let S = null;
|
||||||
|
|
||||||
|
onmessage = e => {
|
||||||
|
const d = e.data || {};
|
||||||
|
if (d.op === 'init') {
|
||||||
|
try {
|
||||||
|
S = ArmIK.make(d.cfg);
|
||||||
|
postMessage({ op: 'ready' });
|
||||||
|
} catch (err) {
|
||||||
|
postMessage({ id: d.id, r: { __err: 'init 失败: ' + String(err && err.message || err) } });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!S) {
|
||||||
|
postMessage({ id: d.id, r: { __err: 'worker 未初始化(没收到 init)' } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let r;
|
||||||
|
try {
|
||||||
|
r = S[d.op].apply(S, d.args);
|
||||||
|
} catch (err) {
|
||||||
|
r = { __err: String(err && err.message || err) };
|
||||||
|
}
|
||||||
|
postMessage({ id: d.id, r: r });
|
||||||
|
};
|
||||||
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 131 KiB |