Files
linuxcncWebWASM/scripts/generate_rtcp_teaching_html.py

291 lines
35 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Build the printable, editable Chinese RTCP teaching deck from verified data."""
import html
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / 'docs/teaching/rtcp-five-axis'
data = json.loads((OUT / 'numeric-evidence.json').read_text())
slides = []
def fig(name, caption='', cls=''):
return f'<figure class="{cls}"><img src="assets/{name}.png" alt="{html.escape(caption)}"><figcaption>{caption}</figcaption></figure>'
def page(chapter, title, body, takeaway='', sources='', cls=''):
slides.append({'chapter': chapter, 'title': title, 'body': body, 'takeaway': takeaway, 'sources': sources, 'cls': cls})
def table(headers, rows, cls=''):
return '<table class="'+cls+'"><thead><tr>'+''.join('<th>'+h+'</th>' for h in headers)+'</tr></thead><tbody>'+''.join('<tr>'+''.join('<td>'+str(x)+'</td>' for x in row)+'</tr>' for row in rows)+'</tbody></table>'
def fmt(x):
return f'{0 if abs(x)<0.0005 else x:.3f}'
def xyz(values): return '('+', '.join(fmt(v) for v in values[:3])+')'
page('教学课件 · 2026.09', '五轴机床与 RTCP',
'<div class="cover-grid"><div><p class="subtitle">从“各轴独立”到“刀尖跟随”</p><p>看懂两个坐标系、一棵结构树、<br>一条运动学计算链。</p><div class="rule"></div><p class="small">适用对象:数控入门、五轴调试与仿真教学<br>主线机型:XYZAC / XYZBC 双转台桁架机床<br>建议课时:60–90 分钟,可拆成两讲</p><p class="badge">原理图解 + 数值演算 + 浏览器实验 + 练习答案</p></div>'+fig('machine-bc','原创 BC 桁架模型:XYZ 移动刀具,B–C 转动工件','cover-model')+'</div>',
'核心问题:程序中的 XYZ,究竟是在告诉谁“去哪里”?', '图:本项目原创 GLB 模型;数值:LinuxCNC WASM 实算。', 'cover')
page('01 · 先统一理解', '你的两点理解,怎样说得更准确?',
'<div class="cols"><div><h3>① 非 RTCP:直接轴映射</h3><p>在本课的双转台结构中,XYZ 带动刀具,A/C 或 B/C 带动工件。</p><p><b>只改变旋转轴指令,不自动触发 XYZ 的刀尖保持补偿。</b>但多个轴仍可在同一段 G 代码中协调插补。</p><p class="small">“独立”描述几何映射,不是运动时间完全无关。</p></div><div><h3>② RTCP:保持刀尖相对工件的轨迹</h3><p>控制器根据编程点、转台角度、旋转中心和标定参数,计算实际关节位置。</p><p><b>工件坐标方向随转台变换,XYZ 关节按需联动。</b>计算还包括平移与偏置,不能只旋转方向箭头。</p><p class="small">“跟随”是相对工件满足目标关系,不是世界坐标中的刀尖永远不动。</p></div></div>',
'课程约定:先用零工件偏置、零刀长差的简化模型讲清原理,再加入实际参数。', '[S1][S2];结合本项目 AC/BC 模型。')
page('01 · 结构与轴名', '五轴 = 三个直线轴 + 两个旋转轴',
table(['名称','通常的轴线含义','本课中的执行机构'],[
['X / Y / Z','沿三个正交方向平移','刀具侧的直线滑台、滑枕'],
['A','绕 X 方向的轴线转动','AC 机床的外层倾转轴'],
['B','绕 Y 方向的轴线转动','BC 机床的外层倾转轴'],
['C','绕 Z 方向的轴线转动','装在倾转台上的内层旋转轴'],
])+'<div class="pair"><p><b>AC 型:</b>X、Y、Z、A、C</p><p><b>BC 型:</b>X、Y、Z、B、C</p></div><p class="small">XYZABC 六个独立轴同时配置,通常应称六轴。A/B/C 是旋转轴的通用名称,本课不是三只转台轴同时工作。C 的轴线会被父级 A/B 倾转,倾转后不再固定平行于机床 Z。</p>',
'从正方向看懂箭头前,先确认观察视角、父级坐标系和机床正负号约定。', '[S3] 轴命名;图中数值采用本项目原生变换约定。')
page('01 · 机械拓扑', '两条运动链,共用一个底座',
fig('hierarchy','本课桁架结构:X 带动整个横梁,Y 带动滑座,Z 带动滑枕与刀具。','wide-diagram')+
'<div class="cols tight"><p><b class="blue">刀具分支</b><br>底座 → X → Y → Z<br>运动从父节点传给所有子节点。</p><p><b class="teal">工件分支</b><br>底座 → A/B → C → 工件<br>A/B 转动时,C 的轴线也随之倾斜。</p></div>',
'这是一种五轴结构,不是所有五轴机床的通用拓扑;摆头、头台混合型需要不同模型。', '本项目 trt-xyz-gantry/1;旧标准型在 Z 下另有固定主轴箱节点。')
page('01 · 真实模型', 'AC 与 BC:外层倾转轴不同',
'<div class="cols model-pair">'+fig('machine-ac','AC 桁架:A 绕 X,内层 C 随 A 倾转')+fig('machine-bc','BC 桁架:B 绕 Y,内层 C 随 B 倾转')+'</div><p class="small">两者都能实现刀尖相对工件的五轴轨迹,但同一角度数值对应的空间姿态不同;不能只把程序里的 A 字母换成 B,就认为加工姿态完全相同。</p>',
'先识别“哪根轴装在哪根轴上”,再选择运动学模型。', '图:原创可下载 GLB;不是制造商的标定尺寸图。')
page('02 · 三种位置', '不要把“程序点、关节位置、实际反馈”混为一谈',
table(['量','回答的问题','典型表示'],[
['编程位置 p','刀尖相对所用编程参考系要到哪里?','(X, Y, Z),加 A/C 或 B/C'],
['关节指令 q','各执行轴需要到哪个位置?','(J0, J1, J2, J3, J4)'],
['实际反馈 q_actual','编码器报告电机/机械轴在哪里?','真实设备中可能存在跟随误差'],
['三维刀尖位置 r','把关节和模型组合后,刀尖在场景哪里?','显示坐标,不是独立运动控制源'],
])+'<p class="small">理想仿真中“关节指令 = 关节反馈”常成立,但真实机床两者仍须区分。RTCP 的运动学把 p 转成 q;模型再把 q 投影成画面。</p>',
'“实际位置”一词要说明是在说编程坐标、关节指令,还是编码器反馈。')
page('02 · G 代码含义', '非 RTCP 时,G 代码也不总等于机械绝对位置',
'<div class="flow"><span>程序字值</span><i>→</i><span>单位 / G90–G91</span><i>→</i><span>工件零点 / 补偿</span><i>→</i><span>运动学</span><i>→</i><span>关节指令</span></div>'+table(['因素','为什么不能直接等同'],[
['G90 与 G91','G90 通常给绝对目标;G91 给相对增量,X10 不一定是到 X=10。'],
['G54 等工件坐标系','工件零点可能偏离机床零点;程序 X10 可能对应机床 X30。'],
['G92、刀长等补偿','控制器还需处理偏置和刀具参考点。'],
['Identity 直接映射','在完成以上处理后,所选坐标与关节才按配置直接对应。'],
])+'<p class="small">简化例:G54 原点位于机床 (20,30,0),无其他偏置/补偿,程序点 (10,0,5) 对应机床点 (30,30,5)。</p>',
'只有把偏置、单位、增量方式、刀具基准都约定好,才能说“程序值直接对应轴位置”。', '[S2][S3];本页示例为无旋转的 Identity 情形。')
page('03 · 非 RTCP', '只转 C,工件点会离开刀尖',
fig('identity-no-follow','俯视图:蓝色为固定机床坐标;绿色为工件;红色为刀尖。Z 保持 20 mm。','large-diagram')+
'<p>起点刀尖对准转台上半径 100 mm 的标记点。只命令 C 从 0° 转到 90°,<b>XYZ 关节不补偿</b>,标记点转到另一处,刀尖仍在原来的机床位置。</p>',
'轴可以独立驱动,不代表“刀具与工件的相对位置”不受其他轴影响。')
page('03 · RTCP', '同样只改变角度,刀尖跟住工件点',
fig('tcp-follow','同一个工件局部点 p=(100,0,20),C=0° 与 C=90°;旋转中心和偏置取零。','large-diagram')+
'<p>保持编程点 p 不变,只改变 C。控制器计算所需 XYZ 关节,让刀尖到达标记点的新机床位置。<b>点在工件坐标中不变,在机床坐标中沿圆弧移动。</b></p>',
'判断“跟随”的参考系是工件,而不是屏幕或机床底座。')
page('03 · 数值算例 1', 'C 转 90°:同一编程点,不同关节位置',
'<p class="intro">零偏置、零刀长差,p = (100, 0, 20) mm;A/B = 0°。</p>'+table(['C 角度','RTCP 编程 XYZ','RTCP 所需关节 XYZ','非 RTCP:只转 C 的关节 XYZ'],[
[f'{s["c"]}°','(100, 0, 20)',xyz(s['joints']),'(100, 0, 20)'] for s in data['cases'][0]['samples'][:3]
])+'<div class="equation">q<sub>x</sub> = 100 cos C  q<sub>y</sub> = 100 sin C  q<sub>z</sub> = 20</div><p class="small">C=90° 时,XYZ 关节目标是 (0,100,20)。这不是控制器偷偷修改了程序点,而是同一点在另一参考系中的正确位置。</p>',
'RTCP:程序坐标可以不变,执行轴位置必须按姿态变化。', 'numeric-evidence.json;原生 WASM IK/FK 双向核对,显示保留3位小数。')
page('04 · 坐标方向', 'C = 45° 时,“沿工件 X 走”意味着什么?',
'<div class="cols image-right"><div><p>蓝色 X<sub>M</sub>、Y<sub>M</sub> 是机床固定方向;绿色 X<sub>W</sub>、Y<sub>W</sub> 随工件转动。</p><p>姿态保持 C=45°,编程增量:</p><div class="equation">Δp<sub>W</sub> = (10, 0, 0)</div><p>对应的机床直线轴增量:</p><div class="equation">Δq = (7.071, 7.071, 0)</div><p class="small">不是机床 X 导轨真的旋转了,而是 X、Y 导轨合成出所需空间运动方向。</p></div>'+fig('rotated-direction','图中工件原点为便于观看作了平移;增量不受原点平移影响。')+'</div>',
'你的“XYZ 方向随旋转改变”可理解为工件基向量在机床坐标中的方向改变。')
bc = data['cases'][1]['samples'][4]
page('04 · 数值算例 2', 'BC 倾转后,沿工件 X 会同时改变机床 X、Y、Z',
'<div class="cols image-right"><div><p>采用本课的正向变换约定:</p><div class="equation small-eq">R = R<sub>Y</sub>(B) · R<sub>Z</sub>(C)</div><p>当 B=30°、C=45° 时:</p><div class="equation small-eq">X<sub>W</sub> 在 M 中的方向<br>(0.6124, 0.7071, −0.3536)</div><p>所以沿 X<sub>W</sub> 前进 10 mm:</p><p class="number">Δq ≈ (6.124, 7.071, −3.536) mm</p><p class="small">倾斜后的坐标方向并不与任何一根直线导轨重合,需要三根直线轴合成。</p></div>'+fig('bc-frame','B=30°、C=45°;红点为工件点 p=(100,0,20) 的机床位置。')+'</div>',
'RTCP 中“沿工件 X”不是“只让 X 电机动”。')
ac = data['cases'][0]['samples'][4]
page('04 · AC 与 BC 对照', '同样的 30° / 45°,不是同一个姿态',
'<p class="intro">仍取 p=(100,0,20) mm,所有旋转中心、偏置和刀长差为零。</p>'+table(['结构','父级倾转 × 子级旋转','倾转30°、C45°时的关节XYZ'],[
['AC','R<sub>X</sub>(A) · R<sub>Z</sub>(C)',xyz(ac['joints'])],
['BC','R<sub>Y</sub>(B) · R<sub>Z</sub>(C)',xyz(bc['joints'])],
])+'<div class="cols"><div><h3>AC:绕 X 倾转</h3><p>C 先改变平面位置,A 再把局部 Y/Z 方向倾斜。</p></div><div><h3>BC:绕 Y 倾转</h3><p>C 先改变平面位置,B 再把局部 X/Z 方向倾斜。</p></div></div><p class="small">矩阵从右向左作用于列向量;这是坐标转换次序,不是要求 G 代码先运动 C 再运动 A/B。插补时两轴可以同时运动。</p>',
'运动学类型选错,即使每个轴都会动,刀尖也不会沿正确的相对轨迹走。', 'numeric-evidence.json;零偏置原生 WASM 结果。')
page('05 · 为什么还需要几何参数', '同样转 90°,绕不同的中心会到不同的位置',
fig('pivot-offset','二维俯视例:起点相同,旋转角度相同,仅旋转中心不同。','large-diagram')+
'<div class="equation">p<sub>M</sub> = O + R · (p − O)</div><p class="small">计算顺序:先减去旋转中心 → 旋转 → 再加回中心。不能只计算 R·p,否则相当于错误地绕坐标零点转。</p>',
'RTCP 不只是“方向变换”,还包括“绕哪里转”的平移关系。')
page('05 · 轴间偏置与变换顺序', 'C 轴装在倾转轴上:父子关系不能交换',
'<div class="cols"><div><h3>先看几何,再写方程</h3><p><b>P:</b>C 轴零位参考点。<br><b>D:</b>倾转轴与 C 轴之间的偏置。<br><b>T=P+D:</b>倾转轴参考位置。</p><p>C 旋转后的点还要随 A/B 倾转;偏置也必须放在正确的父子层级中。</p></div><div><h3>URDF / GLB 里的对应关系</h3>'+table(['节点','本项目零位 origin'],[
['A/B,父节点为底座','P + D'],['C,父节点为 A/B','−D'],['work,父节点为 C','−P'],
],'compact')+'</div></div><div class="equation small-eq">q<sub>XYZ</sub> = P + D + R<sub>tilt</sub> [ R<sub>C</sub> (p − P) − D ] (刀长差为零)</div><p class="small">这是本项目 AC/BC TRT 约定。更一般的机床使用完整的刚体变换链;不能把这三个参数关系硬套到摆头机床。长度在运行配置中为 mm,URDF 为 m。</p>',
'R<sub>tilt</sub>R<sub>C</sub> 通常不等于 R<sub>C</sub>R<sub>tilt</sub>;机械安装顺序决定计算顺序。', '本地 trtfuncs.c;本项目 URDF profile / D11 / D12。')
page('05 · 刀长与受控点', '控制的是刀尖,不只是主轴箱',
fig('tool-length','侧视示意:同一主轴参考位置,刀长不同会产生不同刀尖位置。','large-diagram')+
'<p>刀长改变时,为保持同一个刀尖目标,需要重新计算主轴参考点的位置。必须明确刀具长度的测量基准、正负号、刀补状态,以及模型上“tool”节点代表哪里。</p><p class="small">教学主算例采用刀长差为零。本项目原生 TRT 使用相对于 master tool 的 tool-offset 约定,不能把它直接当作任意模型网格的总长度。</p>',
'同一组关节位置 + 不同刀具标定,不一定得到同一个刀尖位置。', '[S1][S3];实际使用以机床和控制器的刀具标定约定为准。')
page('06 · 姿态的物理意义', '双转台没有转动主轴,却改变了相对刀轴方向',
fig('orientation','侧视:主轴在机床中保持竖直,工件随转台倾斜,二者的相对夹角改变。','large-diagram')+
'<p>在本课双转台机床中,刀轴在机床坐标里的方向固定;工件转动后,<b>刀轴在工件坐标里的方向改变</b>。RTCP 根据指定角度协调刀尖位置,不会自动推断曲面法向。</p>',
'RTCP 不是“自动让刀轴垂直于任意曲面”;姿态仍由程序、CAM 或操作者给定。')
page('06 · 三种常见联动情形', '分清“保持点”“沿方向走”“同时改变位置和姿态”',
table(['输入变化','RTCP 应做什么','观察重点'],[
['XYZ 不变;A/B、C 改变','让刀尖保持同一工件编程点;XYZ关节可能联动。','刀尖与工件标记点不分离。'],
['角度不变;XYZ 改变','沿当前工件参考系定义的方向移动。','机床多轴合成编程方向。'],
['XYZ 与角度同时改变','执行变化的编程轨迹,同时维持角度对应的几何关系。','全过程相对轨迹正确,不只看终点。'],
['XYZ 不变;点恰在 C 轴线上;只转 C','该点机床位置可以不变。','XYZ 不动不一定是故障。'],
])+'<p class="small">“五轴联动”不要求每个时刻五轴速度都非零。非 RTCP 也能多轴同步加工:CAM/后置可以预先按指定机床几何计算轴目标,但这不等于控制器开启了实时刀尖补偿。</p>',
'每次解释一个动作,先说清哪些程序量保持不变、哪些程序量在改变。')
page('06 · 全过程的重要性', '终点都正确,不代表中间轨迹正确',
'<div class="cols image-right"><div><p>同一个工件点绕 C 转动,理想半径为 100 mm。</p><p><b class="teal">RTCP:</b>刀尖跟随圆弧。<br><b class="red">不做 RTCP:</b>若只让 X/Y 线性插补到相同终点,刀尖可能走弦线。</p>'+table(['中间位置','X / Y'],[['圆弧,C=45°','70.711 / 70.711'],['弦线,50%进度','50.000 / 50.000']],'compact')+'<p class="small">图示采用共同线性进度比较几何路径,不是断言所有控制器的加减速过程相同。</p></div>'+fig('arc-chord','中途差距 29.289 mm;起点和终点却完全相同。')+'</div>',
'验收 RTCP 要看连续采样、刀尖相对轨迹和关节约束,不能只比首末坐标。')
switch = data['switch_example']
page('07 · 模式切换', 'M429 ↔ M428:坐标可以变,机床不能无故跳动',
'<p class="intro">本项目 BC 示例:保持同一组物理关节,切换坐标解释。</p>'+table(['量','M429 / Identity','M428 / TCP'],[
['物理关节 XYZ',xyz(switch['physical']),xyz(switch['physical'])],
['物理关节 B / C','4.8675° / 179.88°','4.8675° / 179.88°'],
['显示/解释的 XYZ',xyz([switch['identity_pose'][a] for a in 'xyz']),xyz([switch['tcp_pose'][a] for a in 'xyz'])],
['三维刀尖','同一空间位置','同一空间位置'],
])+'<div class="flow"><span>取当前关节 q</span><i>→</i><span>切换运动学</span><i>→</i><span>新模式正解 p</span><i>→</i><span>同步 TP / 解释器</span></div><p class="small">本例使用真实 BC 模型参数 P=(−18,14,12)、D=(22,0,−40) mm,刀长差0。切换之后再给运动指令,才产生运动。</p>',
'若切换时强行保持旧 XYZ 数字并重新逆解,就可能错误地改变物理关节。', '[S2] 同步原则;本地 control.c::handle_kinematicsSwitch;WASM实算。')
page('07 · 控制器功能边界', 'RTCP、旋转工作平面、刀补,不是同一个开关',
table(['功能','主要解决的问题','不要误认为'],[
['RTCP / TCP 运动学','姿态变化时,按机床结构实现目标刀尖相对轨迹。','所有控制器都会以完全相同方式解释XYZ。'],
['工件坐标系 / 倾斜工作平面','选择编程参考原点或定义编程方向。','只旋转工作平面就自动具备完整五轴RTCP。'],
['刀长补偿','把刀具参考点换算到实际受控刀尖。','更换刀具后只更新网格外观就够了。'],
['机床标定','确定中心、偏置、轴向、正负号和长度。','只要支持RTCP,错误几何参数也会自动纠正。'],
])+'<p class="small">不同控制器可能分别使用 TCP、TCPM、RTCP 等名称,并把倾斜平面/刀补组合进不同指令。本课明确讲本项目的 AC/BC 双转台语义,不推定所有品牌一致。</p>',
'换控制器时先核对“参考系、受控点、角度定义、指令组合”,不要只对照功能名称。')
page('08 · 程序对照', '同样写 C90,开关状态决定 XYZ 是否补偿',
'<div class="cols"><div><h3>M429:直接映射</h3><pre>G21 G90 G94\nM429\nG1 X100 Y0 Z20 A0 C0 F300\nG1 C90 F60</pre><p>后一段只改变 C。<br>XYZ 关节保持 (100,0,20),<br>工件标记点会离开刀尖。</p></div><div><h3>M428:RTCP</h3><pre>G21 G90 G94\nM428\nG1 X100 Y0 Z20 A0 C0 F300\nG1 C90 F60</pre><p>后一段保持编程 XYZ。<br>XYZ 关节变到 (0,100,20),<br>刀尖仍对准同一个工件点。</p></div></div><p class="small">仅为零偏置/零刀长差 AC 教学片段,起始角度和当前模态需已知;BC 用 B0。现有下载模型参数非零,不能期待相同数字。M428/M429 为本项目重映射,不是通用标准M码;F值只作插补示意,实际需按机床的旋转/混合进给规则设定。</p>',
'程序字值相同,不代表开关前后表达的是同一个机械目标。', '本项目 xyzac/xyzbc-trt.ini,428remap.ngc / 429remap.ngc。')
page('08 · 浏览器课堂实验', '用 AC/BC 桁架模型验证,而不是只看公式',
'<ol class="steps"><li><b>加载:</b>打开“导入机床模型”,选 AC 或 BC 桁架示例;记录旋转中心与偏置。</li><li><b>准备:</b>确认空闲,开机并回零;选一个离 C 轴线有一定距离的刀尖位置。</li><li><b>Identity:</b>选择 M429,只 Jog A/B 或 C;观察 XYZ 关节是否保持。</li><li><b>切换:</b>停止后选择 M428;观察关节/刀尖不跳,XYZ 坐标可能变化。</li><li><b>TCP:</b>再次 Jog 旋转轴;观察 XYZ 关节按需补偿、刀尖相对工件点保持。</li><li><b>对照:</b>先停止,再切回 M429;重复方向相同的小幅 Jog,比较两种行为。</li></ol><p class="small">实验先用小幅角度并远离限位。记录的是“每次试验起点相对工件的同一点”;切换或重新定位后,不要把两个不同起点混作同一个目标。</p>',
'同时观察三项:程序XYZ、J0…J4、刀尖与工件的相对关系。', '页面 http://127.0.0.1:5174/;浏览器仿真教学,不是机床现场操作规程。')
page('08 · 参数化实验', '改几何参数后,为什么仍应保留同一个模型?',
'<div class="cols"><div><h3>正确的数据流</h3><div class="vertical-flow"><span>参数表:中心 / 偏置</span><b>↓</b><span>有效 URDF:节点零位关系</span><b>↓</b><span>INI → Worker → WASM</span><b>↓</b><span>关节 → 原导入模型</span></div></div><div><h3>课堂观察</h3><ul><li>同一 GLB 的网格、材质和文件身份保持。</li><li>新的轴心与偏置应进入运行结构和运动学。</li><li>相同程序点和角度,可能得到不同的 XYZ 关节值。</li><li>参数改变后需重新回零并重新解析程序。</li></ul><p class="small">源GLB不自动改写;可保存INI记录参数。改变轴心不等于自动重建支架、铸件等CAD外形,也不提供碰撞检测。</p></div></div>',
'模型保留和参数生效必须同时成立;只改外观或只改计算都会造成误导。', '本项目 D11 / E09:AC、BC各18个姿态/刀长样本验证。')
page('09 · 约束与故障', '“工件点可达”不等于“每个关节都没到限位”',
'<div class="cols"><div><h3>两层约束</h3>'+table(['约束','检查什么'],[['坐标限位','编程轴/坐标允许的范围'],['关节限位','实际执行机构的物理行程'],['动力学约束','速度、加速度等能力']],'compact')+'<p>RTCP 旋转时,即使编程 XYZ 不变,补偿出来的 X/Y/Z 关节仍可能触及边界。</p></div><div><h3>本项目排障实例</h3><p>BC 配置允许较大的 B 坐标范围,但对应 JOINT_3 上限为 <b>50°</b>。</p><p>旧问题:越界 → 运动计算失败 → 事件时钟不同步 → Connection ERROR。</p><p>修复后:按关节约束停止,保留正确时钟,可松开并反向 Jog。</p></div></div><p class="small">本仿真对 RTCP 越界采用最后有效伺服样本停止,不等同于工业控制器的全路径限位预测与减速保护。范围和行为必须分别验证。</p>',
'正常限位阻止是一条运动诊断;序号缺口或连接错误是软件故障,不是RTCP原理。', '本项目 R07 / E11;不据此宣称硬件安全或碰撞防护。')
page('09 · 常见误解', '六个问题,快速判断是否真正理解',
table(['说法','准确解释'],[
['“非RTCP时各轴互相没有影响。”','没有自动补偿;但工件转动当然会改变它与刀具的相对位置。'],
['“RTCP把X/Y/Z导轨转了。”','导轨不转,是工件基方向改变,由固定导轨合成空间运动。'],
['“RTCP时XYZ永远不变。”','只在保持同一编程点时程序XYZ不变;关节XYZ通常变化。'],
['“只要开RTCP,刀尖在空间就不动。”','刀尖可在机床空间移动,以保持它相对工件的目标关系。'],
['“C转了但XYZ没动,一定坏了。”','目标点恰在C轴线上时可能正确,应先检查几何。'],
['“切换模式时XYZ数字跳,必须修好。”','要检查关节和物理刀尖是否跳;新参考系里的数值改变可能正确。'],
],'compact'),
'先说参考系,再说位置;先看结构,再说联动。')
page('10 · 课堂练习', '请先画箭头,再写结论',
'<ol class="exercises"><li><b>点追踪:</b>零偏置,C=0°,工件点 p=(80,0,10)。只转 C 到90°,分别写出 Identity 和 RTCP 的 XYZ 关节目标。</li><li><b>方向合成:</b>C=30°且倾转角为0°,沿工件 +X 方向走20mm,机床 X/Y 各应增加多少?</li><li><b>模式切换:</b>M429切到M428时,XYZ显示值变化、J0…J4不变、刀尖不动,是否正常?为什么?</li><li><b>偏置识别:</b>BC模型 C 的局部 origin=(-0.022,0,0.040)m,倾转轴 origin=(0.004,0.014,-0.028)m。求本项目 P、D,单位mm。</li><li><b>结构判断:</b>C节点错误地挂在Z滑枕下。只Jog X时会发生什么?为什么不能靠RTCP开关解决?</li></ol>',
'建议分组讨论 8–10 分钟;答案在下一页。')
page('10 · 参考答案', '用“相对谁”检查每一步',
'<ol class="answers"><li><b>Identity:</b>(80,0,10);<b>RTCP:</b>(0,80,10)。前者刀尖留在机床原位置,后者继续对准同一工件点。</li><li><b>ΔX=20cos30°=17.321mm;ΔY=20sin30°=10.000mm。</b>方向向量长度仍为20mm,机床Z不变。</li><li><b>正常。</b>运动学切换重新解释当前物理状态。坐标可以变,关节和刀尖应保持。</li><li><b>P=T+U=(-18,14,12)mm;D=−U=(22,0,−40)mm。</b>先统一单位,再按父子关系相加,不能直接拿URDF的米当毫米。</li><li><b>C及工件会错误地随XYZ移动。</b>这是结构树错误,不是开关模式能修复的;需把A/B→C分支正确挂回底座。</li></ol>',
'检查顺序:轴数 → 层级 → 单位/正负号 → 几何参数 → 运动学 → 程序与状态。')
page('附录 A · 数学推导', '从一根 C 轴,推到两根旋转轴',
'<div class="cols"><div><h3>绕 C 的平面变换</h3><pre class="math">x′ = x cos C − y sin C\ny′ = x sin C + y cos C\nz′ = z</pre><h3>AC:再绕 X 倾转 A</h3><pre class="math">qₓ = x′\nqᵧ = y′ cos A − z′ sin A\nq𝓏 = y′ sin A + z′ cos A</pre></div><div><h3>BC:再绕 Y 倾转 B</h3><pre class="math">qₓ = x′ cos B + z′ sin B\nqᵧ = y′\nq𝓏 = −x′ sin B + z′ cos B</pre><p>这里所有旋转中心、轴间偏置及刀长差为零,列向量采用右手正向变换。</p><p class="small">式中的 q 是关节目标;若导入的是其他结构、符号约定或参考点,必须重新建立变换链。</p></div></div>',
'先学会解释“这一步在谁的坐标系中发生”,再记忆公式。', '与本地 trtfuncs.c 零偏置逆解一致;三角函数实际以弧度计算。')
page('附录 B · 本项目完整参数式', '加入旋转中心、偏置与 master-tool 刀长差',
'<div class="equation">D<sub>L</sub> = D + L e<sub>z</sub></div><div class="equation">q<sub>XYZ</sub> = P + D<sub>L</sub> + R<sub>tilt</sub> [ R<sub>C</sub> (p − P) − D<sub>L</sub> ]</div>'+table(['符号','AC','BC'],[
['R<sub>tilt</sub>','R<sub>X</sub>(A)','R<sub>Y</sub>(B)'],['D','(0, D<sub>y</sub>, D<sub>z</sub>)','(D<sub>x</sub>, 0, D<sub>z</sub>)'],['旋转关节','q<sub>A</sub>=A,q<sub>C</sub>=C','q<sub>B</sub>=B,q<sub>C</sub>=C'],
],'compact')+'<p class="small">P为原生rot-point;L为本项目/原生TRT的tool-offset标定量;e<sub>z</sub>=(0,0,1)。本式返回关节XYZ,不是未经处理的网格刀尖世界坐标。G54等解释器偏置在输入p形成前已按控制器规则处理。</p><p class="small">这一刀长表达是本项目约定,不能替代任意控制器的通用刀补说明。主课例子L=0;实际机床应以标定模型及其完整前后向验证为准。</p>',
'工程实现不在画面中“补一个偏移”;应让同一组结构数据贯穿解释器、运动学和模型。', '原生源码:linuxcnc/src/emc/kinematics/trtfuncs.c,xyzac/xyzbcKinematicsInverse。')
page('附录 C · 教学安排与依据', '怎样讲、怎样验证、怎样继续学习',
'<div class="cols references"><div><h3>建议授课节奏</h3><p>10分钟:结构、轴名与三种位置。<br>20分钟:C轴点跟踪、方向合成、AC/BC倾转。<br>15分钟:旋转中心、偏置、刀长与模式切换。<br>15分钟:浏览器实验与错误诊断。<br>10分钟:练习、答案和模型参数核对。</p><h3>本课验算</h3><p>12个姿态由真实WASM逆解计算,并通过正解回代;另核对C45°方向增量、BC切换示例及弧弦差。详见同目录 <code>numeric-evidence.json</code>。</p><p>图形为原创教学示意及本项目GLB截图,不用于制造尺寸标定。</p></div><div><h3>官方资料与本地实现</h3><p id="S1"><b>[S1]</b> <a href="https://linuxcnc.org/docs/html/motion/5-axis-kinematics.html">LinuxCNC · 5-Axis Kinematics</a><br>AC/BC结构、运动学及偏置。</p><p id="S2"><b>[S2]</b> <a href="https://linuxcnc.org/docs/html/motion/switchkins.html">LinuxCNC · Switchable Kinematics</a><br>模式切换、identityfirst、同步要求。</p><p id="S3"><b>[S3]</b> <a href="https://linuxcnc.org/docs/html/gcode/machining-center.html">LinuxCNC · CNC Machine Overview</a><br>轴名、坐标、受控点与协调运动。</p><p><b>本地:</b>trtfuncs.c;control.c;428remap.ngc / 429remap.ngc;docs/glb-machine/。M428=TCP、M429=Identity仅适用于本项目配置。</p><p class="small">资料核对日期:2026-09-18。不同版本/品牌的指令与坐标约定应另行核查。</p></div></div>',
'一句话记忆:程序描述相对工件的目标,运动学把目标换算成各关节应该怎样动。')
css = r'''
@page { size: 338.6667mm 190.5mm; margin: 0; }
* { box-sizing: border-box; }
html,body { margin:0; padding:0; background:#dce3e7; color:#23303a; font-family:"Noto Sans CJK SC","Noto Sans CJK JP",sans-serif; }
.slide { width:1280px; height:720px; padding:34px 54px 28px; background:#fff; position:relative; overflow:hidden; break-after:page; page-break-after:always; }
.slide:last-child { break-after:auto; page-break-after:auto; }
.chapter { color:#00877b; font-size:15px; margin:0 0 10px; font-weight:600; }
h1 { font-size:36px; font-weight:700; line-height:1.3; margin:0 0 24px; letter-spacing:0; }
h3 { font-size:23px; line-height:1.4; margin:0 0 14px; color:#00877b; }
p { font-size:22px; line-height:1.55; margin:0 0 17px; }
.body { height:498px; overflow:hidden; }
.cols { display:grid; grid-template-columns:1fr 1fr; gap:36px; }
.image-right { grid-template-columns: .95fr 1.1fr; gap:24px; }
.tight p { margin-bottom:0; font-size:20px; }
.small { font-size:17px; line-height:1.6; color:#53636d; }
.intro { font-size:22px; padding-bottom:8px; }
.takeaway { position:absolute; left:54px; right:54px; bottom:62px; background:#eaf5f2; border-left:5px solid #00877b; padding:10px 15px; font-size:19px; line-height:1.5; }
.footer { position:absolute; bottom:20px; left:54px; right:54px; display:flex; align-items:flex-end; justify-content:space-between; gap:20px; color:#6b7880; font-size:12px; line-height:1.5; border-top:1px solid #d8e1e6; padding-top:8px; }
.footer .source { max-width:1030px; }
.page-number { font-size:16px; color:#00877b; white-space:nowrap; }
figure { margin:0; min-width:0; }
figure img { width:100%; height:380px; object-fit:contain; display:block; }
figcaption { font-size:14px; line-height:1.5; margin-top:5px; color:#617079; text-align:center; }
.large-diagram img { height:306px; }
.large-diagram { margin-bottom:15px; }
.wide-diagram img { height:280px; }
.model-pair img { height:373px; }
.model-pair { margin-bottom:18px; }
table { border-collapse:collapse; width:100%; margin:8px 0 24px; font-size:19px; line-height:1.55; }
th { background:#eaf0f3; font-weight:600; text-align:left; border-bottom:2px solid #a5b8c1; padding:12px 14px; }
td { padding:13px 14px; border-bottom:1px solid #d8e1e6; vertical-align:top; }
td:first-child { font-weight:600; color:#2563b8; }
.compact { font-size:18px; }
.compact td,.compact th { padding:9px 12px; }
.equation { text-align:center; background:#f1f5f7; padding:14px 18px; font-size:26px; line-height:1.5; margin:14px 0 20px; }
.small-eq { font-size:23px; }
.flow { display:flex; gap:12px; align-items:center; margin:8px 0 25px; font-size:18px; }
.flow span { flex:1; background:#edf4f5; padding:13px 8px; text-align:center; }
.flow i { color:#00877b; font-size:25px; font-style:normal; }
.pair { display:flex; justify-content:space-around; }
.pair p { color:#00877b; }
.number { font-size:23px; color:#d45549; }
.blue { color:#2563b8; } .teal { color:#00877b; } .red { color:#d45549; }
pre { font-family:"DejaVu Sans Mono","Noto Sans Mono CJK SC",monospace; white-space:pre-wrap; background:#edf2f5; border-left:4px solid #2563b8; padding:18px; font-size:21px; line-height:1.8; margin:12px 0 20px; }
.math { font-family:"Noto Sans CJK SC",sans-serif; font-size:23px; line-height:1.65; }
ul,ol { margin:8px 0 14px; padding-left:29px; font-size:22px; line-height:1.6; }
li { margin-bottom:12px; }
.steps { font-size:21px; line-height:1.6; }
.steps li { padding-left:6px; margin-bottom:15px; }
.exercises,.answers { font-size:22px; line-height:1.65; }
.exercises li,.answers li { margin-bottom:19px; padding-left:7px; }
.vertical-flow { display:flex; flex-direction:column; align-items:center; gap:4px; font-size:22px; }
.vertical-flow span { padding:12px 16px; background:#f0f5f7; width:100%; text-align:center; }
.vertical-flow b { color:#00877b; }
.references p { font-size:18px; line-height:1.6; margin-bottom:13px; }
.references h3 { margin-bottom:10px; }
a { color:#136fb0; text-decoration:none; }
sub { font-size:.7em; }
.cover h1 { font-size:48px; margin-top:8px; margin-bottom:18px; }
.cover-grid { display:grid; grid-template-columns:1.12fr .88fr; gap:55px; }
.cover .subtitle { font-size:30px; color:#00877b; line-height:1.4; margin-bottom:22px; }
.cover-model img { height:402px; background:#242a2a; }
.rule { width:85px; height:5px; background:#d45549; margin:25px 0; }
.badge { color:#2563b8; font-size:17px; margin-top:21px; }
.slide[data-page="12"] figure img { height:345px; }
.slide[data-page="12"] .cols p { margin-bottom:12px; }
.slide[data-page="12"] .equation { margin:10px 0 14px; padding:10px 14px; }
.slide[data-page="30"] .equation { margin:8px 0; padding:8px 14px; font-size:24px; }
.slide[data-page="30"] table { margin-bottom:14px; }
.slide[data-page="30"] p { margin-bottom:10px; }
@media screen { .slide { margin:24px auto; box-shadow:0 3px 14px #0002; } }
@media print { html,body { background:#fff; } .slide { margin:0; box-shadow:none; } * { -webkit-print-color-adjust:exact; print-color-adjust:exact; } }
'''
# Avoid exotic script glyphs in equations so CJK PDF fonts remain reliable.
parts = []
for index, slide in enumerate(slides, 1):
body = slide['body'].replace('q𝓏','q_z').replace('qₓ','q_x').replace('qᵧ','q_y')
parts.append(f'''<section class="slide {slide['cls']}" data-page="{index}">
<div class="chapter">{slide['chapter']}</div><h1>{slide['title']}</h1>
<div class="body">{body}</div>
<div class="takeaway">{slide['takeaway']}</div>
<footer class="footer"><span class="source">{slide['sources'] or '五轴机床与 RTCP · 原创教学示意 · 以本课双转台结构为限'}</span><span class="page-number">{index:02d} / {len(slides):02d}</span></footer>
</section>''')
document = '<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>五轴机床与 RTCP:从各轴独立到刀尖跟随</title><style>'+css+'</style></head><body>'+''.join(parts)+'</body></html>'
(OUT / 'course.html').write_text(document)
(OUT / 'outline.json').write_text(json.dumps([{'page':i+1,'chapter':s['chapter'],'title':s['title']} for i,s in enumerate(slides)], ensure_ascii=False,indent=2)+'\n')
print(f'Built {len(slides)} teaching pages:', OUT / 'course.html')