Spaces:
Running
Running
force recovery: methods, evaluation, debug log
Browse files- actions.html +1 -0
- actions_zh.html +250 -0
actions.html
CHANGED
|
@@ -92,6 +92,7 @@ episode data, and what the transformation is worth.</p>
|
|
| 92 |
<span class="pill">36 episodes · 72 sensor-sides</span>
|
| 93 |
<span class="pill">no F/T sensor involved</span>
|
| 94 |
<span class="pill"><a href="index.html" style="color:inherit;text-decoration:none">↖ full methods & validation</a></span>
|
|
|
|
| 95 |
</header>
|
| 96 |
|
| 97 |
<h2>How an action is processed</h2>
|
|
|
|
| 92 |
<span class="pill">36 episodes · 72 sensor-sides</span>
|
| 93 |
<span class="pill">no F/T sensor involved</span>
|
| 94 |
<span class="pill"><a href="index.html" style="color:inherit;text-decoration:none">↖ full methods & validation</a></span>
|
| 95 |
+
<span class="pill"><a href="actions_zh.html" style="color:inherit;text-decoration:none">中文版</a></span>
|
| 96 |
</header>
|
| 97 |
|
| 98 |
<h2>How an action is processed</h2>
|
actions_zh.html
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-CN"><head><meta charset="utf-8">
|
| 3 |
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
| 4 |
+
<title>力信息化动作 — React 数据集</title>
|
| 5 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 6 |
+
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300;9..144,600;9..144,700&family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;600&family=Noto+Serif+SC:wght@600;700&family=Noto+Sans+SC:wght@400;500&display=swap" rel="stylesheet">
|
| 7 |
+
<style>
|
| 8 |
+
:root{
|
| 9 |
+
--paper:#0d1526; --ink:#dce7f5; --dim:#7c8db0; --grid:#1a2540;
|
| 10 |
+
--pose:#4fd8e0; --force:#ffb347; --target:#ff7847; --ok:#7be0a0;
|
| 11 |
+
--card:#111b31; --line:#24345;
|
| 12 |
+
}
|
| 13 |
+
*{box-sizing:border-box}
|
| 14 |
+
html{scroll-behavior:smooth}
|
| 15 |
+
body{margin:0;background:var(--paper);color:var(--ink);
|
| 16 |
+
font-family:'IBM Plex Sans','Noto Sans SC',sans-serif;line-height:1.75;font-size:16px;
|
| 17 |
+
background-image:
|
| 18 |
+
linear-gradient(var(--grid) 1px,transparent 1px),
|
| 19 |
+
linear-gradient(90deg,var(--grid) 1px,transparent 1px);
|
| 20 |
+
background-size:44px 44px;}
|
| 21 |
+
.wrap{max-width:1000px;margin:0 auto;padding:0 24px 90px}
|
| 22 |
+
h1,h2{font-family:'Fraunces','Noto Serif SC',serif}
|
| 23 |
+
h1{font-size:2.6rem;font-weight:700;line-height:1.15;margin:0 0 14px}
|
| 24 |
+
h2{font-size:1.5rem;font-weight:600;margin:64px 0 10px;color:var(--force)}
|
| 25 |
+
h2::before{content:"§ ";color:var(--dim)}
|
| 26 |
+
mono,.mono{font-family:'IBM Plex Mono',monospace}
|
| 27 |
+
header{padding:72px 0 30px;border-bottom:1px dashed var(--line)}
|
| 28 |
+
.kicker{font-family:'IBM Plex Mono',monospace;font-size:.78rem;letter-spacing:.18em;
|
| 29 |
+
text-transform:uppercase;color:var(--force);margin-bottom:16px}
|
| 30 |
+
.sub{color:var(--dim);max-width:760px;font-size:1.04rem}
|
| 31 |
+
.card{background:var(--card);border:1px solid var(--line);border-radius:10px;
|
| 32 |
+
padding:22px 24px;margin:18px 0;position:relative}
|
| 33 |
+
.card::after{content:"";position:absolute;inset:0;border-radius:10px;pointer-events:none;
|
| 34 |
+
box-shadow:inset 0 1px 0 rgba(255,255,255,.04)}
|
| 35 |
+
.formula{font-family:'IBM Plex Mono',monospace;text-align:center;font-size:1.15rem;
|
| 36 |
+
padding:20px;background:#0a1120;border:1px solid var(--line);border-radius:8px;
|
| 37 |
+
color:var(--ink);overflow-x:auto}
|
| 38 |
+
.formula b{color:var(--target)}
|
| 39 |
+
.flow{display:flex;flex-wrap:wrap;gap:10px;align-items:stretch;justify-content:center;margin:22px 0}
|
| 40 |
+
.fstep{background:#0a1120;border:1px solid var(--line);border-radius:8px;padding:12px 16px;
|
| 41 |
+
font-size:.83rem;text-align:center;min-width:120px;font-family:'IBM Plex Mono',monospace}
|
| 42 |
+
.fstep small{display:block;color:var(--dim);margin-top:4px;font-size:.72rem}
|
| 43 |
+
.farr{align-self:center;color:var(--force);font-size:1.2rem}
|
| 44 |
+
table{border-collapse:collapse;width:100%;font-size:.88rem;margin:12px 0;
|
| 45 |
+
font-variant-numeric:tabular-nums}
|
| 46 |
+
th,td{padding:8px 10px;border-bottom:1px solid var(--line);text-align:right}
|
| 47 |
+
th{color:var(--dim);font-family:'IBM Plex Mono',monospace;font-size:.74rem;
|
| 48 |
+
text-transform:uppercase;letter-spacing:.08em}
|
| 49 |
+
td:first-child,th:first-child{text-align:left}
|
| 50 |
+
.big{font-family:'Fraunces',serif;font-size:2rem;color:var(--ok)}
|
| 51 |
+
.stat-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:14px;margin:20px 0}
|
| 52 |
+
.stat{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:16px 18px}
|
| 53 |
+
.stat .lbl{font-family:'IBM Plex Mono',monospace;font-size:.72rem;color:var(--dim);
|
| 54 |
+
text-transform:uppercase;letter-spacing:.1em}
|
| 55 |
+
.stat .val{font-family:'Fraunces',serif;font-size:1.7rem;margin-top:4px;color:var(--ink)}
|
| 56 |
+
.stat .val.ok{color:var(--ok)}
|
| 57 |
+
img,video{max-width:100%;border-radius:8px;border:1px solid var(--line);display:block;margin:12px auto}
|
| 58 |
+
a{color:var(--pose)} a:hover{color:var(--force)}
|
| 59 |
+
code{font-family:'IBM Plex Mono',monospace;background:#0a1120;padding:2px 6px;border-radius:4px;font-size:.85em}
|
| 60 |
+
pre{background:#0a1120;border:1px solid var(--line);border-radius:8px;padding:16px;
|
| 61 |
+
overflow-x:auto;font-size:.82rem;line-height:1.5}
|
| 62 |
+
pre code{background:none;padding:0}
|
| 63 |
+
.legend{display:flex;gap:20px;font-family:'IBM Plex Mono',monospace;font-size:.76rem;
|
| 64 |
+
color:var(--dim);flex-wrap:wrap;margin:8px 2px}
|
| 65 |
+
.legend span::before{content:"—— ";font-weight:700}
|
| 66 |
+
.legend .l-pose::before{color:var(--pose)} .legend .l-force::before{color:var(--force)}
|
| 67 |
+
.legend .l-tgt::before{color:var(--target)}
|
| 68 |
+
#readout{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;
|
| 69 |
+
margin-top:12px;font-family:'IBM Plex Mono',monospace}
|
| 70 |
+
#readout .r{background:#0a1120;border:1px solid var(--line);border-radius:6px;padding:10px 12px}
|
| 71 |
+
#readout .r .k{font-size:.68rem;color:var(--dim);text-transform:uppercase;letter-spacing:.08em}
|
| 72 |
+
#readout .r .v{font-size:1.25rem;margin-top:2px}
|
| 73 |
+
#chartbox{cursor:crosshair;touch-action:none}
|
| 74 |
+
.hint{font-family:'IBM Plex Mono',monospace;font-size:.74rem;color:var(--dim);text-align:center;margin-top:6px}
|
| 75 |
+
.pill{display:inline-block;border:1px solid var(--line);border-radius:999px;
|
| 76 |
+
padding:3px 12px;font-size:.75rem;font-family:'IBM Plex Mono',monospace;
|
| 77 |
+
color:var(--dim);margin-right:8px;margin-top:14px}
|
| 78 |
+
footer{margin-top:70px;padding-top:20px;border-top:1px dashed var(--line);
|
| 79 |
+
color:var(--dim);font-size:.82rem}
|
| 80 |
+
@media (max-width:700px){h1{font-size:1.9rem}}
|
| 81 |
+
</style></head><body><div class="wrap">
|
| 82 |
+
|
| 83 |
+
<header>
|
| 84 |
+
<div class="kicker">React 数据集 · 动作处理</div>
|
| 85 |
+
<h1>动作缺失的另一半:<br>把力还给只有位姿的演示数据</h1>
|
| 86 |
+
<p class="sub">React 的动作由 OptiTrack 跟踪的传感器位姿计算而来——和所有
|
| 87 |
+
UMI 类数据一样,演示位姿<i>就是</i>实际达到的位姿。刚度控制器输出
|
| 88 |
+
<span class="mono">F = k·(target − actual)</span>,
|
| 89 |
+
因此复现这些动作只能复现运动,按压的<b>意图力恒为零</b>。
|
| 90 |
+
本页在真实 episode 数据上展示每个动作如何被变换,以及这个变换值多少。</p>
|
| 91 |
+
<span class="pill">36 个 episode · 72 个传感器侧</span>
|
| 92 |
+
<span class="pill">全程无 F/T 传感器</span>
|
| 93 |
+
<span class="pill"><a href="index.html" style="color:inherit;text-decoration:none">↖ 完整方法与验证(英文)</a></span>
|
| 94 |
+
<span class="pill"><a href="actions.html" style="color:inherit;text-decoration:none">English</a></span>
|
| 95 |
+
</header>
|
| 96 |
+
|
| 97 |
+
<h2>动作是怎么被处理的</h2>
|
| 98 |
+
<div class="card">
|
| 99 |
+
<div class="flow">
|
| 100 |
+
<div class="fstep">pose p<sub>t</sub><small>OptiTrack,30 Hz</small></div>
|
| 101 |
+
<div class="farr">+</div>
|
| 102 |
+
<div class="fstep">GelSight 图像<small>640×480 RGB</small></div>
|
| 103 |
+
<div class="farr">→</div>
|
| 104 |
+
<div class="fstep">F̂<sub>n</sub><small>深度 → Winkler,<br>FEATS 定标</small></div>
|
| 105 |
+
<div class="farr">→</div>
|
| 106 |
+
<div class="fstep">n̂ = R<sub>t</sub>·a<sub>gel</sub><small>双球标定的 gel 轴</small></div>
|
| 107 |
+
<div class="farr">→</div>
|
| 108 |
+
<div class="fstep" style="border-color:var(--target)">动作目标<small>虚拟,穿过接触面</small></div>
|
| 109 |
+
</div>
|
| 110 |
+
<div class="formula">p<sub><b>target</b></sub> = p<sub>observed</sub> + ( F̂<sub>n</sub> / k ) · n̂
|
| 111 |
+
k = 1500 N/m</div>
|
| 112 |
+
<p>动作仍然是位姿。自由空间中 F̂<sub>n</sub> = 0,目标<i>就是</i>观测位姿——什么都不变。
|
| 113 |
+
接触时,目标沿 gel 法向越过接触面,偏移量恰好是刚度为 k 的阻抗控制器要输出演示力所需的位移。
|
| 114 |
+
不新增动作维度,部署端不需要力控接口——即 DexForce 式变换(arXiv:2501.10356),
|
| 115 |
+
只是驱动它的力来自触觉估计而非力传感器测量。</p>
|
| 116 |
+
</div>
|
| 117 |
+
|
| 118 |
+
<h2>真实数据实时演示——在曲线上拖动</h2>
|
| 119 |
+
<div class="card">
|
| 120 |
+
<p style="margin-top:0"><span class="mono">motherboard/2026-05-10/episode_000</span>
|
| 121 |
+
(左传感器)以最强按压为中心的 90 秒。上:估计的法向力。
|
| 122 |
+
下:变换对动作的全部效果——目标相对观测位姿沿 gel 法向的偏移。
|
| 123 |
+
青色零线<i>就是</i>原始动作;这段窗口里传感器本身扫过 ±170 mm,
|
| 124 |
+
所以偏移单独用毫米刻度画出。</p>
|
| 125 |
+
<div id="chartbox"></div>
|
| 126 |
+
<div class="legend"><span class="l-force">法向力 F̂ [N]</span>
|
| 127 |
+
<span class="l-pose">观测位姿 = 零偏移(变换前的动作)</span>
|
| 128 |
+
<span class="l-tgt">沿 n̂ 的目标偏移 F̂/k(变换后的动作)</span></div>
|
| 129 |
+
<div id="readout">
|
| 130 |
+
<div class="r"><div class="k">t</div><div class="v" id="ro-t">–</div></div>
|
| 131 |
+
<div class="r"><div class="k">法向力 F̂</div><div class="v" id="ro-f" style="color:var(--force)">–</div></div>
|
| 132 |
+
<div class="r"><div class="k">目标偏移 F̂/k</div><div class="v" id="ro-p" style="color:var(--target)">–</div></div>
|
| 133 |
+
<div class="r"><div class="k">状态</div><div class="v" id="ro-s">–</div></div>
|
| 134 |
+
</div>
|
| 135 |
+
<p class="hint">拖动 / 悬停查看 · 数据为逐行真实输出,非示意图</p>
|
| 136 |
+
</div>
|
| 137 |
+
|
| 138 |
+
<h2>效果,用数字说话</h2>
|
| 139 |
+
<div class="stat-row">
|
| 140 |
+
<div class="stat"><div class="lbl">自由空间不变性</div>
|
| 141 |
+
<div class="val ok">0.0e+00 m</div>
|
| 142 |
+
<div class="lbl" style="margin-top:6px">F̂=0 时 |target − pose| 的最大值,全部 72 个侧</div></div>
|
| 143 |
+
<div class="stat"><div class="lbl">往返误差</div>
|
| 144 |
+
<div class="val ok">9e-14 N</div>
|
| 145 |
+
<div class="lbl" style="margin-top:6px">k·‖target−pose‖ 对比 F̂ —— 机器精度</div></div>
|
| 146 |
+
<div class="stat"><div class="lbl">接触期穿透量</div>
|
| 147 |
+
<div class="val">1.0 mm <span style="font-size:.9rem;color:var(--dim)">中位数</span></div>
|
| 148 |
+
<div class="lbl" style="margin-top:6px">最硬的 ~23 N 按压达 15.2 mm</div></div>
|
| 149 |
+
</div>
|
| 150 |
+
<div class="card">
|
| 151 |
+
<p style="margin-top:0"><b>为什么这对训练重要。</b>在原始位姿上训练的策略学到的是
|
| 152 |
+
"碰到表面就停":标签说指尖停在接触面上,部署时控制器输出的力只是跟踪误差碰巧产生的残余。
|
| 153 |
+
用力信息化目标后,标签本身编码了<i>按多重</i>——DexForce 在带力测量的拖动示教上测得:
|
| 154 |
+
不做这个修正任务成功率接近零,做了是 76%;这里同样的变换由触觉估计的力驱动,
|
| 155 |
+
估计器已对 FEA 真值验证(混合 ρ=0.70,未见过的按压头形状 0.85——见<a href="index.html">主页(英文)</a>)。</p>
|
| 156 |
+
<img src="assets/dexforce_motherboard_episode_000_left.png" alt="virtual target offsets">
|
| 157 |
+
<video controls muted loop playsinline preload="metadata" src="assets/clip_motherboard_episode_000_left.mp4"></video>
|
| 158 |
+
<p class="hint">驱动动作变换的力信号,与触觉流实时对齐</p>
|
| 159 |
+
</div>
|
| 160 |
+
|
| 161 |
+
<h2>怎么用</h2>
|
| 162 |
+
<div class="card">
|
| 163 |
+
<pre><code># 每个 episode 的力估计以 npz 形式随 release 一起发布
|
| 164 |
+
import numpy as np
|
| 165 |
+
from force_recovery.dexforce import force_informed_targets, gel_axis
|
| 166 |
+
from force_recovery.evaluate import median3_fresh
|
| 167 |
+
|
| 168 |
+
z = np.load("force_recovery/motherboard/2026-05-10/episode_000_left.npz")
|
| 169 |
+
force = median3_fresh(z["force_normal_n"], is_new) # 只在 fresh 帧上去尖峰
|
| 170 |
+
act = force_informed_targets(pose, force, gel_axis("motherboard", "left"))
|
| 171 |
+
train_targets = act.target_pos # (T,3) —— 直接替换位姿标签</code></pre>
|
| 172 |
+
<p>需要直说的告诫:绝对牛顿值带有 FEATS 定标的不确定性(跨传感器刻度漂移 2–4×;
|
| 173 |
+
集内相对力才是可靠的部分);剪切主导的接触是法向力估计器的盲区;
|
| 174 |
+
旧录制的触觉有效更新率约 8.5 fps——<code>tactile_*_is_new</code>
|
| 175 |
+
标记了哪些行携带新的力证据。</p>
|
| 176 |
+
</div>
|
| 177 |
+
|
| 178 |
+
<footer>React 力恢复 · 数据集 <a href="https://huggingface.co/datasets/yxma/React">yxma/React</a>
|
| 179 |
+
· 方法与外部验证见<a href="index.html">主页(英文)</a>
|
| 180 |
+
· 变换:DexForce (2501.10356) · 力估计:gsrobotics 光度立体 + Winkler,FEATS 定标 (2411.03315)</footer>
|
| 181 |
+
</div>
|
| 182 |
+
|
| 183 |
+
<script>
|
| 184 |
+
fetch('assets/action_trace.json').then(r=>r.json()).then(D=>{
|
| 185 |
+
const W=940,H1=170,H2=250,GAP=34,PAD_L=56,PAD_R=16,H=H1+H2+GAP+40;
|
| 186 |
+
const N=D.force_n.length,fps=D.meta.fps;
|
| 187 |
+
const box=document.getElementById('chartbox');
|
| 188 |
+
const svgNS='http://www.w3.org/2000/svg';
|
| 189 |
+
const svg=document.createElementNS(svgNS,'svg');
|
| 190 |
+
svg.setAttribute('viewBox',`0 0 ${W} ${H}`);svg.style.width='100%';
|
| 191 |
+
box.appendChild(svg);
|
| 192 |
+
const X=i=>PAD_L+(W-PAD_L-PAD_R)*i/(N-1);
|
| 193 |
+
const fmax=Math.max(...D.force_n)*1.08;
|
| 194 |
+
const Yf=v=>H1-(H1-8)*v/fmax;
|
| 195 |
+
// bottom panel: the OFFSET itself. The trajectory sweeps +-170mm here while
|
| 196 |
+
// the offset stays <=5mm — plotting raw positions hides the transform.
|
| 197 |
+
const smin=0,smax=Math.max(...D.pen_mm)*1.12;
|
| 198 |
+
const Y2=v=>H1+GAP+H2-(H2-8)*(v-smin)/(smax-smin);
|
| 199 |
+
function poly(ys,color,w,dash){const p=document.createElementNS(svgNS,'path');
|
| 200 |
+
let d='M';for(let i=0;i<N;i++){d+=X(i).toFixed(1)+' '+ys(i).toFixed(1)+(i<N-1?' L':'');}
|
| 201 |
+
p.setAttribute('d',d);p.setAttribute('fill','none');p.setAttribute('stroke',color);
|
| 202 |
+
p.setAttribute('stroke-width',w);if(dash)p.setAttribute('stroke-dasharray',dash);
|
| 203 |
+
svg.appendChild(p);return p;}
|
| 204 |
+
// axes
|
| 205 |
+
[[0,'0'],[fmax/1.08,fmax.toFixed(0)]].forEach(([v,t])=>{
|
| 206 |
+
const l=document.createElementNS(svgNS,'text');l.textContent=t+' N';
|
| 207 |
+
l.setAttribute('x',PAD_L-8);l.setAttribute('y',Yf(v)+4);l.setAttribute('text-anchor','end');
|
| 208 |
+
l.setAttribute('fill','#7c8db0');l.setAttribute('font-size','11');
|
| 209 |
+
l.setAttribute('font-family','IBM Plex Mono');svg.appendChild(l);});
|
| 210 |
+
[[0,'0 mm'],[smax/1.12,(smax/1.12).toFixed(1)+' mm']].forEach(([v,t])=>{
|
| 211 |
+
const l=document.createElementNS(svgNS,'text');l.textContent=t;
|
| 212 |
+
l.setAttribute('x',PAD_L-8);l.setAttribute('y',Y2(v)+4);l.setAttribute('text-anchor','end');
|
| 213 |
+
l.setAttribute('fill','#7c8db0');l.setAttribute('font-size','11');
|
| 214 |
+
l.setAttribute('font-family','IBM Plex Mono');svg.appendChild(l);});
|
| 215 |
+
// commanded-penetration band between the zero-line (observed) and the target
|
| 216 |
+
const band=document.createElementNS(svgNS,'path');
|
| 217 |
+
let d='M';for(let i=0;i<N;i++)d+=X(i).toFixed(1)+' '+Y2(D.pen_mm[i]).toFixed(1)+(i<N-1?' L':'');
|
| 218 |
+
d+=' L'+X(N-1).toFixed(1)+' '+Y2(0)+' L'+X(0).toFixed(1)+' '+Y2(0);
|
| 219 |
+
band.setAttribute('d',d+' Z');band.setAttribute('fill','rgba(255,120,71,.28)');svg.appendChild(band);
|
| 220 |
+
poly(i=>Yf(D.force_n[i]),'#ffb347',1.4);
|
| 221 |
+
poly(i=>Y2(0),'#4fd8e0',1.3);
|
| 222 |
+
poly(i=>Y2(D.pen_mm[i]),'#ff7847',1.3);
|
| 223 |
+
// cursor
|
| 224 |
+
const cur=document.createElementNS(svgNS,'line');
|
| 225 |
+
cur.setAttribute('y1',0);cur.setAttribute('y2',H1+GAP+H2);
|
| 226 |
+
cur.setAttribute('stroke','#dce7f5');cur.setAttribute('stroke-width','1');
|
| 227 |
+
cur.setAttribute('stroke-dasharray','3 3');svg.appendChild(cur);
|
| 228 |
+
const dotF=document.createElementNS(svgNS,'circle');dotF.setAttribute('r',4);
|
| 229 |
+
dotF.setAttribute('fill','#ffb347');svg.appendChild(dotF);
|
| 230 |
+
const dotT=document.createElementNS(svgNS,'circle');dotT.setAttribute('r',4);
|
| 231 |
+
dotT.setAttribute('fill','#ff7847');svg.appendChild(dotT);
|
| 232 |
+
function setI(i){i=Math.max(0,Math.min(N-1,i));
|
| 233 |
+
const x=X(i);cur.setAttribute('x1',x);cur.setAttribute('x2',x);
|
| 234 |
+
dotF.setAttribute('cx',x);dotF.setAttribute('cy',Yf(D.force_n[i]));
|
| 235 |
+
dotT.setAttribute('cx',x);dotT.setAttribute('cy',Y2(D.pen_mm[i]));
|
| 236 |
+
document.getElementById('ro-t').textContent=(i/fps).toFixed(2)+' s';
|
| 237 |
+
document.getElementById('ro-f').textContent=D.force_n[i].toFixed(2)+' N';
|
| 238 |
+
document.getElementById('ro-p').textContent=(D.pen_mm[i]).toFixed(2)+' mm';
|
| 239 |
+
const s=document.getElementById('ro-s');
|
| 240 |
+
if(D.force_n[i]>0.1){s.textContent='接触中';s.style.color='var(--target)';}
|
| 241 |
+
else{s.textContent='自由 — 目标 ≡ 位姿';s.style.color='var(--ok)';}}
|
| 242 |
+
function fromEvent(e){const r=svg.getBoundingClientRect();
|
| 243 |
+
const px=((e.touches?e.touches[0].clientX:e.clientX)-r.left)/r.width*W;
|
| 244 |
+
setI(Math.round((px-PAD_L)/(W-PAD_L-PAD_R)*(N-1)));}
|
| 245 |
+
['mousemove','mousedown','touchmove','touchstart'].forEach(ev=>
|
| 246 |
+
box.addEventListener(ev,e=>{fromEvent(e);if(ev!=='mousemove')e.preventDefault();},{passive:false}));
|
| 247 |
+
setI(Math.round(N*0.5));
|
| 248 |
+
});
|
| 249 |
+
</script>
|
| 250 |
+
</body></html>
|