test: 加 IK/数据/截图三重基线关卡,并刷新过期的截图基线

重构 2955 行的单文件查看器必须有"行为没变"的可判据,否则改到什么程度全靠感觉。
tests/ik_baseline.mjs 在 Node 里直接跑求解器(它本就是 UMD,带 module.exports 分支,
不需要浏览器),输出分两段:
  exact  —— 件数/顶点/三角、TCP 推断、限位、固定 FK/fkChain/orient、固定目标的
            solveDyn 解向量、selfTest/poseSelfTest 的误差与命中数。selfTest 内部走
            Math.random,这里锁成定 LCG,所以 exact 跨进程逐字节可复现。
  timing —— solveDyn 中位耗时(9 次去头 3 次预热)。本机进程间抖动实测约 ±28%,
            因此只按 ±10% 报 WARN,不当硬门。
关卡灵敏度已验:指向 scratch/arm-ik-solver_before.js 时报 5 处 exact 差异并 FAIL。

docs/shots/ 里 92 张是 10-09 生成的,早于 10-14 的重建,属过期基线;重跑 96 视图
全部 OK、0 FAIL。另发现既有非幂等问题:committed 的 mesh 是"端到端构建后再单跑一次
screw_mesh.py"的产物(螺钉 idx 118..214、ENV 件排在第 19 位),端到端构建则为
20..116、ENV 排在末尾,两者零件集合相同但顺序不同,导致 2 张线束透视图的透明排序
像素级不同。本提交以端到端构建为准。
This commit is contained in:
cbb
2026-10-15 02:24:38 +08:00
parent 7e040fc766
commit b32d44efe0
94 changed files with 632 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
533c100b58b33c0317de179f65c5c681 travel.json
c66c230557548bd03f003bb4bd52ff6a travel_detail.json
4a8f14be95ed32121166a9bd37e74752 collide.json
82f8fa459be497ae0cfc92d5816497bf screws.json
83dfad6817c766ee893f54cca8fa7774 mesh_arm.json
f74cf5148cfc03c2d785829f45a608f9 sheets.jsdata
+228
View File
@@ -0,0 +1,228 @@
{
"meta": {
"solver": "web/arm-ik-solver.js",
"mesh": "web/mesh_arm.json"
},
"exact": {
"counts": {
"parts": 117,
"verts": 367118,
"tris": 351491,
"printed": 13,
"env": 1,
"joints": 6
},
"tcpZ": 93,
"TCP0": [
19.5,
128,
93
],
"motorTravel": 360,
"noAdjAxes": [],
"caps": 1936,
"lim": {
"J1": [
-180,
180,
true
],
"J2": [
-93,
126,
false
],
"J3": [
-180,
180,
true
],
"J4": [
-113,
113,
false
],
"J5": [
-94,
89,
false
],
"J6": [
-180,
180,
true
]
},
"fkQ0": [
106.498451,
235.370133,
140.165364
],
"fkChainQ0": [
106.498451,
235.370133,
140.165364
],
"orientQ0": [
0.595153,
0.645892,
-0.478139,
-0.757749,
0.649173,
-0.06626,
0.267598,
0.401744,
0.875781
],
"solveDyn": [
{
"T": [
150,
128,
260
],
"ok": true,
"err": 0,
"q": [
0.041,
5.809,
0.094,
-17.507,
-94,
0
]
},
{
"T": [
180,
128,
300
],
"ok": true,
"err": 0,
"q": [
0.049,
-0.026,
0.094,
-26.821,
-94,
0
]
},
{
"T": [
120,
-150,
320
],
"ok": true,
"err": 0,
"q": [
-43.713,
6.027,
0.239,
-0.279,
-89.002,
0
]
},
{
"T": [
60,
128,
420
],
"ok": true,
"err": 0,
"q": [
38.165,
0.129,
70.062,
-89.578,
-80.719,
0
]
}
],
"selfTest": {
"maxFkErr": 4.689582056016661e-13,
"maxIkErr": 7.082653493385528e-13,
"ikHit": 200,
"ikTried": 200,
"structWarn": [],
"poseHit": 8,
"poseTried": 8,
"poseErr": 0.0000017075472925031877,
"K": {
"X0": 19.5,
"Y0": 128,
"Z1": 610,
"Z2": 490,
"Z3": 445,
"Z4": 332,
"Z5": 222,
"TCP0": [
19.5,
128,
93
],
"ax": [
0,
0,
-1
],
"C0": -120,
"A43": -113,
"A32": -45,
"A54": -110,
"L5": 129
},
"LIM": {
"J1": {
"lo": -180,
"hi": 180,
"cont": true
},
"J2": {
"lo": -93,
"hi": 126,
"cont": false
},
"J3": {
"lo": -180,
"hi": 180,
"cont": true
},
"J4": {
"lo": -113,
"hi": 113,
"cont": false
},
"J5": {
"lo": -94,
"hi": 89,
"cont": false
},
"J6": {
"lo": -180,
"hi": 180,
"cont": true
}
}
},
"poseSelfTest": {
"poseHit": 8,
"poseTried": 8,
"poseErr": 0.0000012074182697257333
}
},
"timing": {
"solveDynMedianMs": [
252.53,
239.83,
192.01,
192.92
],
"poseSelfTestAvgMs": 93.48
}
}
+228
View File
@@ -0,0 +1,228 @@
af5c9d74bddf7d795bd38f4371fa61ec *docs/shots/01_iso.png
c315879cfd09f35efa4ca4ecc1564b65 *docs/shots/02_front.png
b3157f4453cf96b930f065e134a2d18b *docs/shots/03_side.png
068fc4bc3eec481ff7d7b1d4da2aad88 *docs/shots/04_top.png
fbb95ea7a9229195d80c6f487021ae2b *docs/shots/05_exploded.png
cbea750a1b52c5e8d061a9e3f4c981fe *docs/shots/06_wrist.png
bb3dc393e4cd8f2c399c343aba0b3d35 *docs/shots/07_joint.png
72e86ae7743ff7416e247d19556dc657 *docs/shots/08_pose.png
af5c9d74bddf7d795bd38f4371fa61ec *docs/shots/09_sheet.png
af5c9d74bddf7d795bd38f4371fa61ec *docs/shots/10_solid.png
e97dcf60224e2ad968bddf02ea680e57 *docs/shots/11_grip_open.png
8daa2255af050a225609cebbc490d3b9 *docs/shots/11_tool_flange.png
002a1f4a2b65744515f5b54364fcc2a8 *docs/shots/12_elbow_flare.png
b2f1454a6009a8b44ddd41be9efdaff5 *docs/shots/13_forearm_side.png
053a9be5edad6b577bbe902c428aa135 *docs/shots/H2_前后圆角_34.png
6d6feec7d65ea08e1175013fa3691869 *docs/shots/H2_前后圆角_侧视.png
f17fd52c9f2eee907da3c76a80cd8073 *docs/shots/H2_前后圆角_正面.png
9519d95093d835f72dfc008c4febc1a3 *docs/shots/H_正式_34视角.png
cf61473cd470b5111b678f2fa01f8bcc *docs/shots/H_正式_侧视.png
2385a6f605834163eb821a56cfa5be6a *docs/shots/H_正式_整机.png
001e85eca69fc3f1503d41b0a7366706 *docs/shots/P04_H落地_复核.png
351e5b573126d75bc5baf01b55199e5b *docs/shots/P04_前后圆角_复核.png
bac1bfb89650c343b33b680c410259e9 *docs/shots/P04_前脸轮廓_改前改后对比.png
f54e549cd743495e78e45744f3283d47 *docs/shots/P04_尖角_改后.png
3b906449664648c1555e9aadeff83bca *docs/shots/P04_顶部_改前.png
ff4ab3362882cb99895c94801896afe7 *docs/shots/P04_顶部_改后.png
ae0c77ebf8a51a2541cfb903bd34f86c *docs/shots/P05_斜棱R3圆角_实施完成对比.png
aa18505c1201d1490584ced82d16f465 *docs/shots/P05_斜棱圆角_评估对比.png
60a8a174ce455f0539503ac1aa3d58f0 *docs/shots/P07_立柱底端圆角_对比.png
baf85ba3b5c37d6ffb9c0deb6b8eb7d0 *docs/shots/arm_overview_notch_exact.png
07ec81c2fcef975b2b53577f00f5176c *docs/shots/bay_inner_wall.png
e39999d2ef28536f24d7f6178fa9a15c *docs/shots/bay_p04_inside.png
959e4bf08e174d90a1fe03a75a042070 *docs/shots/bay_p06_inside.png
f9d8d58365b4ca693525af5540d39d32 *docs/shots/bay_p07_inside.png
1b07f37e5fed9d82886b9518a3839860 *docs/shots/bay_p08_inside.png
d8955c69a48a064dc395c7c18b9db49c *docs/shots/bay_section_polar.png
90b79bb61cacea2323ca19790d4158b2 *docs/shots/boss_p04.png
dcc54259caf06f796e3dec84496abb12 *docs/shots/boss_p05.png
9f2c134297f2c7a555a6eaca2e74e4de *docs/shots/boss_p06.png
b789564315ca1ad7d6d9d18c96972cba *docs/shots/boss_p07.png
8fdee57313c9ea826dcd881d9c4655c8 *docs/shots/compare_harn_off.png
b8b89700a7a6a38b80aa1ef0d8966195 *docs/shots/compare_harn_on.png
1d0cccff978809bb5e1f357d0f45a28a *docs/shots/crop_circle_detail.png
43857f773d225ed4c6425c27f93afc2c *docs/shots/crop_p04_bottom.png
9b2f02d28cc0f40c913ca13daad4c4f4 *docs/shots/crop_p04_bottom2.png
baa4492159c3e4d7eab0a45f1965e640 *docs/shots/crop_p04_bottom_v2.png
f00fbb4fad4c0127cb403d28f91ed557 *docs/shots/crop_p04_side.png
5214b01e26e27d8033c54122c215b77b *docs/shots/crop_p04_side_v2.png
10a610706648c4d3e2dac056eaef6f6d *docs/shots/crop_side_p05.png
7c6c053e0875a0dda616fde588985bd3 *docs/shots/crop_side_p08.png
8bbdcba793f44ccaf9df47a8ac316c60 *docs/shots/crop_user_circle_p05.png
3259a8c112f1d351a1fed11973a02c95 *docs/shots/crop_user_circle_p08.png
a618bec8aa67bb2cc743e923207df914 *docs/shots/crop_user_creases.png
5994293cc45e59afdb907c52e4e2dd01 *docs/shots/crop_user_creases_fixed.png
1f3b3d68bbcf563ba50f79ab74494117 *docs/shots/crop_user_feedback.png
5768c8a960267428b10362662d78fd4e *docs/shots/crop_user_p06_yz_fixed.png
c2b2873686ec704024bdbb178f8d19f4 *docs/shots/fence_all_sections.png
efcb304e1e6d17f93e8fb0802ed429ac *docs/shots/fence_j2_after.png
5a907286d59d7f0584bf0a86cee50e01 *docs/shots/fence_j4_after.png
5a0556bda22250c7f633df0db6603154 *docs/shots/fence_j5_after.png
dd21d8f29d1a0267866b898c75ae6d4f *docs/shots/fence_j6_after.png
3123606bf71f402a9714aad63aa7f179 *docs/shots/full_arm_compare.png
93893ff628702b83137e27fdcf751f60 *docs/shots/harness_3d_viewer.png
af5c9d74bddf7d795bd38f4371fa61ec *docs/shots/harness_full_iso.png
2c8ecd1eee5204b6cf6c4af3b3c5f62e *docs/shots/harness_full_view.png
b8b89700a7a6a38b80aa1ef0d8966195 *docs/shots/harness_inner_front.png
4087f9fa75d2ef805457b1e32689785d *docs/shots/harness_inner_side.png
ae26c478855955748222592591ee6a03 *docs/shots/harness_notch_fixed.png
8896124b0c45d9796e983f026134cff6 *docs/shots/harness_overview.png
7a40f9f434e51299deb7ab9a59f8fc81 *docs/shots/harness_overview_xray.png
7cbf2c699501bff3c7ebb4beb7cd34e2 *docs/shots/harness_p04_p05.png
7b7335a79480d04dc3862f51a6377396 *docs/shots/harness_p04_p05_xray.png
687e2184dd3e149939ce6d4214ffc698 *docs/shots/harness_p06_window.png
9fca41ada43f36a338cce31797152c3c *docs/shots/harness_p07_window.png
c8a688690b418e68993d06e7b60e2c05 *docs/shots/harness_p07_xray.png
b8b89700a7a6a38b80aa1ef0d8966195 *docs/shots/harness_toggled_off.png
84fb87bb5ce6cef73866401642ab0f9d *docs/shots/harness_xray_full.png
7d07f7728a5cb0bbb71c787e2b44aeb1 *docs/shots/harness_xray_internal.png
152a3bbf2ebd3531fbde77a1c201c9a0 *docs/shots/harness_xray_motor.png
54d1ba097fdf4213811c1360204fa677 *docs/shots/harness_zoom_elbow.png
09c5d9d5c507a182d4dea5e4cfa238dc *docs/shots/harness_zoom_shoulder.png
7fe0cb0d52c1f7d97bf1c8fd47b22e0c *docs/shots/hollow_p04_iso.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
8766806e981e84aaafd880af49e6bb0f *docs/shots/j4_neg80_view.png
7797c4490780e1d164a7784fc062ba32 *docs/shots/j4_pos80_view.png
26f3fd913018e16e99af2edd861342b8 *docs/shots/j4_side_neg80.png
9f411a6efadfa2fd38aa929beaaed0f0 *docs/shots/j4_side_pos80.png
cfd861e3c300b6b91f84f40bb9753d51 *docs/shots/lingxi_6DUUEF.png
82e86303cb609fa371600c0fb5f9b85a *docs/shots/lingxi_6DUUEG.png
e6616300fa1a80f4316673f733b43167 *docs/shots/lingxi_6DUUEJ.png
515d312c6cf3d2b420d19eda7a380fbe *docs/shots/m2_front_notch_exact.png
f7ad1083e20c91ef1dcf2eeb37e0f908 *docs/shots/m2_front_view.png
c84927682b860cfe7d9c2ec2ffc21305 *docs/shots/m2_iso_notch_exact.png
9bb20af2e14c64203e814423e65efc75 *docs/shots/m2_notch_zoom.png
369479ad05476793ce824fa6a58bca98 *docs/shots/m2_side_view.png
38536cef76f4e29825705a5d3bf8bfbc *docs/shots/m3_front_assembled_after.png
1c221106e26cdf4d85dd6d432483704f *docs/shots/m3_front_iso_after.png
a9182da4a194b3316ba710e5e22b4299 *docs/shots/m3_front_nomotor_after.png
88eadfa42ec551a96b52a58468bbe5ac *docs/shots/m3_front_open_after.png
e8acef55ca4305e09dbda01de7d8af51 *docs/shots/m3_front_side_after.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
21db2e9ca69192cce0be4467b4701028 *docs/shots/p03_mount_loose.png
78f62639acb79e30fa88cc5fda87cfea *docs/shots/p03_mount_top.png
ae0305c092707e0c73944e1d889d076a *docs/shots/p04_both_ends.png
f241acda8fdae460eb38f5a054e841f9 *docs/shots/p04_m2_side.png
f9a0e5fda588e5345e32d112c3dbc116 *docs/shots/p04_top_after.png
2b07a995cfadea3396cbf1b9bcdbe3fb *docs/shots/p04_top_iso.png
8191b972269eab6e51b40b5771e7d3d5 *docs/shots/p05_after_groove_removed.png
fcbf993fb18f6e7007cb66fea153cdf2 *docs/shots/p05_corner_zoom.png
3d261042388c8b2924848d8a29f71931 *docs/shots/p05_flank_view.png
c7bbe31a5e94574fbaaf495622f999ff *docs/shots/p05_focus.png
9fcdb197a69d94949660b42032db1fda *docs/shots/p05_inner_corner_1.png
665709a81624ee17b8565df8ed1947da *docs/shots/p05_inner_corner_2.png
597ffa69b9817823c1958822482ae9d1 *docs/shots/p05_inner_corner_side.png
2efeeb47868cb57fab926aebe980bd5b *docs/shots/p05_iso_clean.png
9e930e7d033b8b4dfe8a486feb9b8eff *docs/shots/p05_iso_target.png
515b204e42d75d98afe0600f8b8b419d *docs/shots/p05_m3_side.png
bfe508bc1f44430bb3a6d43811962226 *docs/shots/p05_side.png
0e6931d83143d767ba37fa76d12aeb41 *docs/shots/p05_side_target.png
38794de6a818f31efade797afff934f3 *docs/shots/p05_zoom_v3.png
3b82ff785a29a5b1a4233404f62e3106 *docs/shots/p06_iso_v3.png
bc99a68fa6ebcc21c173188a13805834 *docs/shots/p06_iso_v4.png
4c6014a754e233411f6e89e4112d6cde *docs/shots/p06_m4_side.png
cf5939f305762bd064cdfae373c130d4 *docs/shots/p07_fillet_zoom.png
aa3cb2e1e7bff6e3ca4af66bd6c70854 *docs/shots/p07_m5_side.png
c6a061335ea757064ecf51195b89f808 *docs/shots/preview_p06_closeup.png
c712e7385b2094bc349d06f3ff1973eb *docs/shots/preview_p06_front.png
966427d5bba294035b3e6f15b0984e60 *docs/shots/preview_p06_iso.png
16e9638ec903aef101594f6cd0224a40 *docs/shots/preview_p06_yz_side.png
7635f1d057b06f014c1683261a7237b8 *docs/shots/shot_p08_user.png
20b4517e84e161abce960994380a92c6 *docs/shots/shot_service_loop_bent.png
468704c6678e540f54d3165768713991 *docs/shots/shot_service_loop_zero.png
d6fba250cab75489c4ccd44b245ce02d *docs/shots/shot_service_loop_zoom.png
977bb781966f2b94894a2fb99cc4ed69 *docs/shots/side_full_arm.png
cd787443c8c8c4be47d24f3e4c0b0e33 *docs/shots/side_full_arm_v4.png
5de5466396a959ce13f494da0d50298d *docs/shots/side_v3.png
aed480014d24fcd45bc3eafd30b0376c *docs/shots/side_zoom_p05.png
aed480014d24fcd45bc3eafd30b0376c *docs/shots/side_zoom_p05_v4.png
4e3374649995691b3556f3c86fca5904 *docs/shots/side_zoom_p08.png
4e3374649995691b3556f3c86fca5904 *docs/shots/side_zoom_p08_v4.png
f1e5bd79e6921b39d0785e4208ef5cca *docs/shots/svg_exploded.png
57ac93ac3d2be849772d51ba8300a989 *docs/shots/svg_front.png
eadcf7d6ac9853950496a559dbd8de72 *docs/shots/svg_joint.png
4a9a34e534496cb2c3be499ea2029d87 *docs/shots/svg_side.png
2c8ecd1eee5204b6cf6c4af3b3c5f62e *docs/shots/test.png
9324b57f4bfb601a6cd6606f74c0c856 *docs/shots/test_p05_fillet_after.png
9e0c65c8e53adab6305cc1b03dd3c038 *docs/shots/test_p05_view.png
a9b48aa3507d451d8e9159699cfeb6be *docs/shots/test_side_0.png
96a62a6d55b26a30a5001952a8541ed8 *docs/shots/test_side_neg_y.png
7458cdfb12232a2566142ddc6bb76424 *docs/shots/test_side_pi.png
96635ca94ec00783d84c9a3f54fb91c0 *docs/shots/test_side_pos_y.png
4434ed29679c4fdba76782b40d71c925 *docs/shots/test_side_x_neg.png
02a5cd63659b58b6c4f64782ca9f61e4 *docs/shots/test_side_x_pos.png
67a351369c1bb2e1491eb2dadc5efc00 *docs/shots/test_target_corner.png
b7aeadaf3603105f3a47780490ad0342 *docs/shots/test_user_view.png
d3bf901601ff221ecd1b09dc93dd2ccc *docs/shots/tie_after_close.png
7c2be3a8488997de588e3760f966fd7d *docs/shots/tie_after_negY.png
0b3e26c6e2fd664f91764d26b8b1ed0c *docs/shots/tie_after_oblique.png
c39cee9deabedb4dc8fad8ec76b439e5 *docs/shots/tie_after_p0607.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
9c2bc97ed4ca490e624695c5e4086f8c *docs/shots/user_exact_clean.png
6e1c039c236d28a4bec7e7b589fb2ed3 *docs/shots/user_exact_view.png
6a86499dd36b2a0a1ac50ae28ea138e4 *docs/shots/user_fork_fixed.png
527d30d68065fd9e59020f8234998c61 *docs/shots/user_harness_full.png
d35c720c416b645b0560296f1062d878 *docs/shots/user_harness_notches.png
1389d052730b4bb09eb0a17cf9c49354 *docs/shots/user_harness_view.png
eebfbfbea1375770fbc333f83f64e959 *docs/shots/user_harness_zoom.png
ffe701c93ce7984d125cff99a10ed319 *docs/shots/user_iso_after.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
22ace55c6f60a6322f770b2b0a0a504c *docs/shots/user_p04_iso.png
6ea6b98ad978b993868bd302ad1dd8c6 *docs/shots/user_p04_opp.png
2c7f53c7bed7d25175948f09960ed634 *docs/shots/user_p04_side.png
20e8583cae4d1705bcecdb750ac90b3b *docs/shots/user_p06_fixed.png
2b1117068f7dc20cb24054cfea6d3453 *docs/shots/user_p06_front.png
fe0b30e16a673beb200224a0e6607faa *docs/shots/user_p06_iso.png
291d238a586ded723221e2f9ed49e7e3 *docs/shots/user_p06_j4_pitch_neg.png
4798f251b423398acb0975ecdff88aa6 *docs/shots/user_p06_j4_pitch_pos.png
5326a55ce2be7e15e78272cbdf5c6574 *docs/shots/user_p06_p07_match.png
c8448c391e21b12831c09e82fab9da24 *docs/shots/user_p06_side.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
227a85ecdb2fd21b1d042f84bf3aa94c *docs/shots/user_side_full_v5.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
8513509bd468d4253fff41a769fba112 *docs/shots/user_view_after.png
f800bcb2c675c540da91ea4128626558 *docs/shots/verify_front.png
04dcb70b0420dd9b626970e84c6c6009 *docs/shots/verify_front_no_p04.png
f800bcb2c675c540da91ea4128626558 *docs/shots/verify_front_xray.png
6d08599e6af8098899be6f4e04cd587f *docs/shots/verify_rear.png
467521f6aed235e4b7e018291d7a9273 *docs/shots/view_a.png
400e33d3f92bdc5d35030bc0560793f2 *docs/shots/view_b.png
2e70d37bc8bfe39ec1f55956b8c90c31 *docs/shots/view_c.png
9ac5686881ce53db4220af7599bae200 *docs/shots/view_d.png
64a199b2c65e7f2d0ad5e2a80c5caef8 *docs/shots/view_iso_forearm.png
22ae02822c50527ff3aa0b341337add5 *docs/shots/view_iso_zoom.png
1d8277f0b43938423f2ca53b34df0c56 *docs/shots/view_yz_neg_x.png
d51cde4a5e2450c529d1c8897e16cf29 *docs/shots/view_yz_pos_x.png
d1719a9f6fb775fd63cef27c5ff17f54 *docs/shots/zoom_user.png
+170
View File
@@ -0,0 +1,170 @@
/**
* IK 数值基线探针(Node,不需要浏览器)。
*
* node tests/ik_baseline.mjs 生成基线到 stdout
* node tests/ik_baseline.mjs --out scratch/_gate/ik.json
* node tests/ik_baseline.mjs --compare scratch/_gate/ik.json 重构后跑这条,FAIL 就退出码 1
* node tests/ik_baseline.mjs --solver web/arm-ik-solver.js 指定求解器文件
*
* 输出分两段,因为它们的判定方式不同:
* exact —— 件数/顶点数、TCP 推断、限位、固定 FK、固定目标的解向量、selfTest 误差与命中数。
* 这些必须逐字节相同;用定种子 RNG 保证可复现(selfTest 内部原本走 Math.random)。
* timing —— solveDyn 中位耗时。机器负载会变,只按 ±10% 报 WARN,不作 FAIL。
*
* 求解器配置构造与查看器里的 ik-glue 逐行对齐(JMap/COL/TR/TCP0),
* 否则两边算的不是同一个问题,对比无意义。
*/
import fs from 'fs';
import path from 'path';
import { pathToFileURL } from 'url';
const ROOT = path.resolve(import.meta.dirname, '..');
const first = (...cands) => cands.find(p => fs.existsSync(p));
const argv = process.argv.slice(2);
const argVal = (name, dflt) => {
const i = argv.indexOf('--' + name);
return i >= 0 && argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[i + 1] : dflt;
};
const SOLVER = argVal('solver', first(
path.join(ROOT, 'webapp/js/ik/solver.js'),
path.join(ROOT, 'web/arm-ik-solver.js')));
const MESH = first(path.join(ROOT, 'webapp/data/mesh_arm.json'),
path.join(ROOT, 'web/mesh_arm.json'));
const TRAVEL = path.join(ROOT, 'docs/travel.json');
const COLLIDE = path.join(ROOT, 'docs/collide.json');
const mod = await import(pathToFileURL(SOLVER).href);
const ArmIK = mod.ArmIK || mod.default;
if (!ArmIK) throw new Error('求解器没有导出 ArmIK: ' + SOLVER);
const mesh = JSON.parse(fs.readFileSync(MESH, 'utf8'));
const travel = JSON.parse(fs.readFileSync(TRAVEL, 'utf8'));
const collide = fs.existsSync(COLLIDE) ? JSON.parse(fs.readFileSync(COLLIDE, 'utf8')) : null;
/* ---- 与 ik-glue.js:16-52 同构的配置 ---- */
const JO = mesh.joints;
const JJ = {}; for (const j of JO) JJ[j.id] = j;
const JMap = {}; for (const j of JO) JMap[j.id] = { rear: j.rear, d: j.d };
const COL = (collide && collide.caps && collide.caps.length) ? collide : null;
const ADJ = (COL && COL.adj) || {};
const MOTOR = +((COL && COL.motor_limit_deg) || 360);
const TR = {}; const noAdj = [];
for (const t of travel) {
const a = ADJ[t.axis];
if (!a && !t.continuous) noAdj.push(t.axis);
TR[t.axis] = { lo: a ? a[0] : t.lo, hi: a ? a[1] : t.hi, continuous: !!t.continuous };
}
/* TCP 高度:J6 组内实打印件(排除 ENV_)顶点的最小查看器 y,查看器 y = CAD z。
与 ik-glue.js:17-27 同语义:只有真找到才覆盖默认值 93,不是拿 93 当下界。 */
let tcpZ = 93, loFound = Infinity;
for (const p of mesh.parts) {
if (p.grp !== 'J6' || !p.printed || String(p.name).slice(0, 4) === 'ENV_') continue;
if (!p.v || !p.v.length) continue;
for (const q of p.v) loFound = Math.min(loFound, q[2]);
}
if (isFinite(loFound)) tcpZ = loFound;
const TCP0 = [JJ.J6.rear[0], JJ.J6.rear[1], tcpZ];
const S = ArmIK.make({ joints: JMap, travel: TR, tcp: TCP0, collide: COL });
const ORDER = ArmIK.ORDER;
/* selfTest/poseSelfTest 内部用 Math.random(),不锁种子的话两次跑的误差数字就不同,
关卡会退化成"看大概"。这里换成定LCG,让 exact 段逐字节可复现。 */
const _rand = Math.random;
function seeded(seed, fn) {
let s = seed >>> 0;
Math.random = () => { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
try { return fn(); } finally { Math.random = _rand; }
}
const parts = mesh.parts;
const r6 = v => +v.toFixed(6);
/* ---- 固定输入(不含随机)的可复现输出 ---- */
const Q0 = [10, -20, 30, 40, -50, 15];
const zeros = ORDER.map(() => 0);
const targets = [[150, 128, 260], [180, 128, 300], [120, -150, 320], [60, 128, 420]];
const solveRuns = targets.map(T => {
const xs = [];
for (let k = 0; k < 9; k++) {
const t0 = performance.now();
const r = S.solveDyn(T.slice(), { tool: 0, step: 0.25, ref: zeros, from: zeros.slice() });
xs.push({ ms: performance.now() - t0, q: r && r.q ? r.q.map(v => +v.toFixed(3)) : null,
ok: !!(r && r.q), err: r && r.err != null ? +r.err.toFixed(9) : null });
}
/* 头 3 次是 JIT 预热,留中位数也去不掉进程间的预热差;实测取 9 次里后 6 次的中位,
自比抖动从 >50% 降到可直接判定的量级。 */
const warm = xs.slice(3).sort((a, b) => a.ms - b.ms);
const m = warm[Math.floor(warm.length / 2)];
return { T, medianMs: +m.ms.toFixed(2), q: m.q, ok: m.ok, err: m.err };
});
const st = seeded(20261015, () => S.selfTest(400, 8));
const pst = seeded(777, () => S.poseSelfTest(8));
const exact = {
counts: {
parts: parts.length,
verts: parts.reduce((n, p) => n + ((p.v && p.v.length) || 0), 0),
tris: parts.reduce((n, p) => n + ((p.t && p.t.length) || 0), 0),
printed: parts.filter(p => p.printed).length,
env: parts.filter(p => String(p.name).startsWith('ENV_')).length,
joints: JO.length,
},
tcpZ: +tcpZ.toFixed(3),
TCP0: TCP0.map(v => +v.toFixed(3)),
motorTravel: MOTOR,
noAdjAxes: noAdj,
caps: COL ? COL.caps.length : 0,
lim: Object.fromEntries(ORDER.map(id => [id, [r6(S.LIM[id].lo), r6(S.LIM[id].hi), !!S.LIM[id].cont]])),
fkQ0: S.fk(Q0, 0).map(r6),
fkChainQ0: S.fkChain(Q0, 0).map(r6),
orientQ0: S.orient(Q0).map(r6),
solveDyn: solveRuns.map(r => ({ T: r.T, ok: r.ok, err: r.err, q: r.q })),
selfTest: { maxFkErr: st.maxFkErr, maxIkErr: st.maxIkErr, ikHit: st.ikHit, ikTried: st.ikTried,
structWarn: st.structWarn, poseHit: st.poseHit, poseTried: st.poseTried,
poseErr: st.poseErr, K: st.K, LIM: st.LIM },
poseSelfTest: { poseHit: pst.poseHit, poseTried: pst.poseTried, poseErr: pst.poseErr },
};
const timing = {
solveDynMedianMs: solveRuns.map(r => r.medianMs),
poseSelfTestAvgMs: +pst.poseMs.toFixed(2),
};
const doc = { meta: { solver: path.relative(ROOT, SOLVER).replace(/\\/g, '/'),
mesh: path.relative(ROOT, MESH).replace(/\\/g, '/') },
exact, timing };
const text = JSON.stringify(doc, null, 1);
const out = argVal('out', null);
if (out) fs.writeFileSync(out, text + '\n');
const base = argVal('compare', null);
if (!base) { if (!out) console.log(text); process.exit(0); }
const b = JSON.parse(fs.readFileSync(base, 'utf8'));
const diffs = [];
(function walk(x, y, p) {
if (x === null || y === null || typeof x !== 'object' || typeof y !== 'object') {
if (JSON.stringify(x) !== JSON.stringify(y)) diffs.push(p + ': ' + JSON.stringify(y) + ' -> ' + JSON.stringify(x));
return;
}
for (const k of new Set([...Object.keys(x), ...Object.keys(y)])) walk(x[k], y[k], p ? p + '.' + k : k);
})(doc.exact, b.exact, '');
console.log('== exact(必须逐字节相同)==');
console.log(diffs.length ? diffs.map(d => ' DIFF ' + d).join('\n') : ' 全部一致 ✓');
console.log('== timing(±10% 只报 WARN)==');
const tb = b.timing;
let warn = 0;
doc.timing.solveDynMedianMs.forEach((v, i) => {
const o = tb.solveDynMedianMs[i];
const d = (v - o) / o * 100;
if (Math.abs(d) > 10) { warn++; console.log(` WARN solveDyn#${i}: ${o} -> ${v} ms (${d.toFixed(1)}%)`); }
else console.log(` ok solveDyn#${i}: ${o} -> ${v} ms (${d.toFixed(1)}%)`);
});
if (diffs.length) { console.log('RESULT: FAIL(exact 有 %d 处差异)', diffs.length); process.exit(1); }
console.log('RESULT: PASS' + (warn ? `(timing ${warn} 项超 ±10%,需人判)` : ''));