[{"data":1,"prerenderedAt":7182},["ShallowReactive",2],{"page-\u002Farchitecture\u002Fkiriyama_canvas的架构设计思考":3},{"id":4,"title":5,"body":6,"description":7169,"extension":7170,"meta":7171,"navigation":390,"path":7178,"seo":7179,"stem":7180,"__hash__":7181},"content\u002Farchitecture\u002Fkiriyama_canvas的架构设计思考.md","KiriyamaCanvas的架构设计思考",{"type":7,"value":8,"toc":7146},"minimark",[9,27,30,35,49,229,237,256,746,748,756,770,779,798,990,994,1010,1434,1436,1443,1459,1896,1898,1902,1914,1937,1945,1959,2248,2256,2265,2338,2340,2344,2348,2372,2379,2381,2391,2414,2544,2555,2722,2769,2771,2778,2799,3300,3314,3316,3320,3333,3335,3339,3343,3352,3355,3367,3374,3381,3384,3628,3635,3661,3874,3876,3880,4062,4079,4082,4085,4096,4107,4120,4444,4448,4451,4464,4471,4780,4800,5062,5071,5074,5102,5105,5115,5121,5446,5456,5463,5466,5691,5709,5712,5714,5718,5737,5748,5767,6112,6131,6138,6146,6166,6169,6179,6397,6407,6423,6436,6439,6826,6839,6842,6845,7097,7107,7113,7116,7119,7127,7130,7137,7140,7143],[10,11,12,13,17,18,22,23,26],"p",{},"本笔记用于拆解 ",[14,15,16],"code",{},"kiriyama_canvas","（C++23 + SDL3 + WebAssembly 2D 图表与画布组件库）的架构设计。全文采用",[19,20,21],"strong",{},"精简文本 + 核心代码块 + 数学推导","的形式，按 ",[19,24,25],{},"“具体渲染与更新管线 → 分层与模块设计 → 点击测试与交互模块的数学实现”"," 展开。",[28,29],"hr",{},[31,32,34],"h2",{"id":33},"第一章具体管线设计从输入同步到双通道上屏","第一章：具体管线设计（从输入同步到双通道上屏）",[10,36,37,38,40,41,48],{},"在 ",[14,39,16],{}," 中，每一帧（16.6ms）的执行流由 Web 宿主 ",[42,43,45],"a",{"href":44},"file:\u002F\u002F\u002FE:\u002Fkiriyama_canvas\u002Fplatform\u002Fweb\u002FCanvasHost.cc#L148-L212",[14,46,47],{},"CanvasHost::onFrame"," 统一驱动，分为五个严格顺序的阶段：",[50,51,53],"div",{"align":52},"center",[54,55,61,62,61,80,61,89,61,98,61,108,61,114,61,118,61,124,61,130,61,135,61,139,61,142,61,146,61,151,61,156,61,160,61,163,61,167,61,172,61,177,61,181,61,184,61,188,61,194,61,199,61,203,61,206,61,210,61,217,61,221,61,225],"svg",{"viewBox":56,"width":57,"height":58,"xmlns":59,"style":60},"0 0 860 155","100%","auto","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","max-width: 860px; font-family: system-ui, -apple-system, sans-serif;","\n  ",[63,64,65,66,61],"defs",{},"\n    ",[67,68,74,75,65],"marker",{"id":69,"viewBox":70,"refX":71,"refY":72,"markerWidth":71,"markerHeight":71,"orient":73},"arr0","0 0 10 10","6","5","auto-start-reverse","\n      ",[76,77],"path",{"d":78,"fill":79},"M 0 1 L 10 5 L 0 9 z","#64748b",[81,82],"rect",{"width":83,"height":84,"rx":85,"fill":86,"stroke":87,"stroke-width":88},"860","155","12","#fafaf9","#e2e0d8","1.5",[81,90],{"x":91,"y":92,"width":93,"height":94,"rx":95,"fill":96,"stroke":97,"stroke-width":88},"16","26","144","102","8","#ede9fe","#8b5cf6",[99,100,107],"text",{"x":101,"y":102,"fill":103,"text-anchor":104,"font-size":105,"font-weight":106},"88","52","#4c1d95","middle","12.5","700","1. 帧首同步与事件",[99,109,113],{"x":101,"y":110,"fill":111,"text-anchor":104,"font-size":112},"74","#5b21b6","11","pumpOne() 消费暂存",[99,115,117],{"x":101,"y":116,"fill":111,"text-anchor":104,"font-size":112},"92","dispatchInputEvent()",[99,119,123],{"x":101,"y":120,"fill":121,"text-anchor":104,"font-size":122},"110","#6d28d9","10.5","按键\u002F滚轮树状分发",[81,125],{"x":126,"y":92,"width":127,"height":94,"rx":95,"fill":128,"stroke":129,"stroke-width":88},"186","152","#fef3c7","#f59e0b",[99,131,134],{"x":132,"y":102,"fill":133,"text-anchor":104,"font-size":105,"font-weight":106},"262","#78350f","2. 交互步进与双轨更新",[99,136,138],{"x":132,"y":110,"fill":137,"text-anchor":104,"font-size":112},"#92400e","Widget::tickTree(ctx)",[99,140,141],{"x":132,"y":116,"fill":137,"text-anchor":104,"font-size":112},"慢路径: build + diff",[99,143,145],{"x":132,"y":120,"fill":144,"text-anchor":104,"font-size":122},"#b45309","快路径: O(1) 原地改属性",[81,147],{"x":148,"y":92,"width":127,"height":94,"rx":95,"fill":149,"stroke":150,"stroke-width":88},"364","#fce7f3","#ec4899",[99,152,155],{"x":153,"y":102,"fill":154,"text-anchor":104,"font-size":105,"font-weight":106},"440","#831843","3. 场景树遍历与解析",[99,157,159],{"x":153,"y":110,"fill":158,"text-anchor":104,"font-size":112},"#9d174d","世界矩阵与透明度累乘",[99,161,162],{"x":153,"y":116,"fill":158,"text-anchor":104,"font-size":112},"ColorToken 主题色解析",[99,164,166],{"x":153,"y":120,"fill":165,"text-anchor":104,"font-size":122},"#be185d","pushClip 视口裁剪栈",[81,168],{"x":169,"y":92,"width":127,"height":94,"rx":95,"fill":170,"stroke":171,"stroke-width":88},"542","#dbeafe","#3b82f6",[99,173,176],{"x":174,"y":102,"fill":175,"text-anchor":104,"font-size":105,"font-weight":106},"618","#1e3a8a","4. 双通道光栅化",[99,178,180],{"x":174,"y":110,"fill":179,"text-anchor":104,"font-size":112},"#1e40af","通道A: 凸扇切\u002F凹耳切",[99,182,183],{"x":174,"y":116,"fill":179,"text-anchor":104,"font-size":112},"通道B: 全白双级字库缓存",[99,185,187],{"x":174,"y":120,"fill":186,"text-anchor":104,"font-size":122},"#1d4ed8","SDL_RenderGeometry",[81,189],{"x":190,"y":92,"width":191,"height":94,"rx":95,"fill":192,"stroke":193,"stroke-width":88},"720","124","#dcfce7","#22c55e",[99,195,198],{"x":196,"y":102,"fill":197,"text-anchor":104,"font-size":105,"font-weight":106},"782","#14532d","5. 顶层合成上屏",[99,200,202],{"x":196,"y":110,"fill":201,"text-anchor":104,"font-size":112},"#166534","updateCursor()",[99,204,205],{"x":196,"y":116,"fill":201,"text-anchor":104,"font-size":112},"TooltipHost 浮层",[99,207,209],{"x":196,"y":120,"fill":208,"text-anchor":104,"font-size":122},"#15803d","Present 提交帧缓冲",[211,212],"line",{"x1":213,"y1":214,"x2":215,"y2":214,"stroke":79,"stroke-width":88,"marker-end":216},"160","77","184","url(#arr0)",[211,218],{"x1":219,"y1":214,"x2":220,"y2":214,"stroke":79,"stroke-width":88,"marker-end":216},"338","362",[211,222],{"x1":223,"y1":214,"x2":224,"y2":214,"stroke":79,"stroke-width":88,"marker-end":216},"516","540",[211,226],{"x1":227,"y1":214,"x2":228,"y2":214,"stroke":79,"stroke-width":88,"marker-end":216},"694","718",[230,231,233,234,236],"h3",{"id":232},"_11-总控管线代码canvashostonframe","1.1 总控管线代码（",[14,235,47],{},"）",[10,238,239,240,243,244,247,248,251,252,255],{},"JS 调用 C API（如 ",[14,241,242],{},"kiriyama_pie_add_slice","）时",[19,245,246],{},"绝不直接修改场景树","，而是写入句柄对应的暂存槽位并标记 ",[14,249,250],{},"dirty = true","，统一在帧首 ",[14,253,254],{},"pumpOne()"," 消费，从而彻底杜绝跨语言回调重入导致迭代器失效：",[257,258,263],"pre",{"className":259,"code":260,"language":261,"meta":262,"style":262},"language-cpp shiki shiki-themes github-light github-dark","\u002F\u002F 文件：platform\u002Fweb\u002FCanvasHost.cc\nvoid onFrame(App& app) override {\n    \u002F\u002F 【阶段 1】同步组件实例，并将 JS 暂存区的待处理数据灌入组件\n    syncInstances(app);\n    const auto ctx = frameContext(app); \u002F\u002F 打包 dt 与 960x540 逻辑空间鼠标坐标\n    for (const auto& [handle, instance] : instances_) {\n        pumpOne(handle, *instance);\n    }\n\n    \u002F\u002F 【阶段 2】树状交互步进：自顶向下执行命中检测、状态流转与按需 rebuild \u002F 原地更新\n    for (const auto& [handle, instance] : instances_) {\n        if (instance->pie && instance->pie->visible())   instance->pie->tickTree(ctx);\n        if (instance->spark && instance->spark->visible()) instance->spark->tickTree(ctx);\n        if (instance->table && instance->table->visible()) instance->table->tickTree(ctx);\n    }\n\n    \u002F\u002F 【阶段 3 & 4】按当前主题背景色清屏，并执行「先几何、后文本」的双通道渲染\n    const core::ChartTheme& theme = kiriyama::web::currentTheme();\n    app.painter().clear(theme.background);\n    for (const auto& [handle, instance] : instances_) {\n        if (instance->pie && instance->pie->visible()) {\n            drawSceneGeometry(app.painter(), *instance->pie, theme); \u002F\u002F 通道 A：几何图元\n            app.text().drawScene(*instance->pie, theme);             \u002F\u002F 通道 B：文字贴图\n        }\n        \u002F\u002F ... spark 与 table 同理\n    }\n\n    \u002F\u002F 【阶段 5】帧末顶层合成：更新鼠标手型光标，并在最顶层绘制全局唯一 Tooltip\n    updateCursor(app);\n    if (const int active = instanceUnderPointer(app); active != 0) {\n        kiriyama::web::applyTooltip(active, *tooltip_);\n    }\n    tooltip_->update(*root_, ctx);\n}\n","cpp","",[14,264,265,273,307,313,322,346,364,379,385,392,398,411,438,460,482,487,492,498,535,553,566,582,604,626,632,638,643,648,654,662,696,719,724,740],{"__ignoreMap":262},[266,267,269],"span",{"class":211,"line":268},1,[266,270,272],{"class":271},"sJ8bj","\u002F\u002F 文件：platform\u002Fweb\u002FCanvasHost.cc\n",[266,274,276,280,284,288,291,294,298,301,304],{"class":211,"line":275},2,[266,277,279],{"class":278},"szBVR","void",[266,281,283],{"class":282},"sScJk"," onFrame",[266,285,287],{"class":286},"sVt8B","(",[266,289,290],{"class":282},"App",[266,292,293],{"class":278},"&",[266,295,297],{"class":296},"s4XuR"," app",[266,299,300],{"class":286},") ",[266,302,303],{"class":278},"override",[266,305,306],{"class":286}," {\n",[266,308,310],{"class":211,"line":309},3,[266,311,312],{"class":271},"    \u002F\u002F 【阶段 1】同步组件实例，并将 JS 暂存区的待处理数据灌入组件\n",[266,314,316,319],{"class":211,"line":315},4,[266,317,318],{"class":282},"    syncInstances",[266,320,321],{"class":286},"(app);\n",[266,323,325,328,331,334,337,340,343],{"class":211,"line":324},5,[266,326,327],{"class":278},"    const",[266,329,330],{"class":278}," auto",[266,332,333],{"class":286}," ctx ",[266,335,336],{"class":278},"=",[266,338,339],{"class":282}," frameContext",[266,341,342],{"class":286},"(app);",[266,344,345],{"class":271}," \u002F\u002F 打包 dt 与 960x540 逻辑空间鼠标坐标\n",[266,347,349,352,355,358,361],{"class":211,"line":348},6,[266,350,351],{"class":278},"    for",[266,353,354],{"class":286}," (",[266,356,357],{"class":278},"const",[266,359,360],{"class":278}," auto&",[266,362,363],{"class":286}," [handle, instance] : instances_) {\n",[266,365,367,370,373,376],{"class":211,"line":366},7,[266,368,369],{"class":282},"        pumpOne",[266,371,372],{"class":286},"(handle, ",[266,374,375],{"class":278},"*",[266,377,378],{"class":286},"instance);\n",[266,380,382],{"class":211,"line":381},8,[266,383,384],{"class":286},"    }\n",[266,386,388],{"class":211,"line":387},9,[266,389,391],{"emptyLinePlaceholder":390},true,"\n",[266,393,395],{"class":211,"line":394},10,[266,396,397],{"class":271},"    \u002F\u002F 【阶段 2】树状交互步进：自顶向下执行命中检测、状态流转与按需 rebuild \u002F 原地更新\n",[266,399,401,403,405,407,409],{"class":211,"line":400},11,[266,402,351],{"class":278},[266,404,354],{"class":286},[266,406,357],{"class":278},[266,408,360],{"class":278},[266,410,363],{"class":286},[266,412,414,417,420,423,426,429,432,435],{"class":211,"line":413},12,[266,415,416],{"class":278},"        if",[266,418,419],{"class":286}," (instance->pie ",[266,421,422],{"class":278},"&&",[266,424,425],{"class":286}," instance->pie->",[266,427,428],{"class":282},"visible",[266,430,431],{"class":286},"())   instance->pie->",[266,433,434],{"class":282},"tickTree",[266,436,437],{"class":286},"(ctx);\n",[266,439,441,443,446,448,451,453,456,458],{"class":211,"line":440},13,[266,442,416],{"class":278},[266,444,445],{"class":286}," (instance->spark ",[266,447,422],{"class":278},[266,449,450],{"class":286}," instance->spark->",[266,452,428],{"class":282},[266,454,455],{"class":286},"()) instance->spark->",[266,457,434],{"class":282},[266,459,437],{"class":286},[266,461,463,465,468,470,473,475,478,480],{"class":211,"line":462},14,[266,464,416],{"class":278},[266,466,467],{"class":286}," (instance->table ",[266,469,422],{"class":278},[266,471,472],{"class":286}," instance->table->",[266,474,428],{"class":282},[266,476,477],{"class":286},"()) instance->table->",[266,479,434],{"class":282},[266,481,437],{"class":286},[266,483,485],{"class":211,"line":484},15,[266,486,384],{"class":286},[266,488,490],{"class":211,"line":489},16,[266,491,391],{"emptyLinePlaceholder":390},[266,493,495],{"class":211,"line":494},17,[266,496,497],{"class":271},"    \u002F\u002F 【阶段 3 & 4】按当前主题背景色清屏，并执行「先几何、后文本」的双通道渲染\n",[266,499,501,503,506,509,512,514,517,519,522,524,527,529,532],{"class":211,"line":500},18,[266,502,327],{"class":278},[266,504,505],{"class":282}," core",[266,507,508],{"class":286},"::",[266,510,511],{"class":282},"ChartTheme",[266,513,293],{"class":278},[266,515,516],{"class":286}," theme ",[266,518,336],{"class":278},[266,520,521],{"class":282}," kiriyama",[266,523,508],{"class":286},[266,525,526],{"class":282},"web",[266,528,508],{"class":286},[266,530,531],{"class":282},"currentTheme",[266,533,534],{"class":286},"();\n",[266,536,538,541,544,547,550],{"class":211,"line":537},19,[266,539,540],{"class":286},"    app.",[266,542,543],{"class":282},"painter",[266,545,546],{"class":286},"().",[266,548,549],{"class":282},"clear",[266,551,552],{"class":286},"(theme.background);\n",[266,554,556,558,560,562,564],{"class":211,"line":555},20,[266,557,351],{"class":278},[266,559,354],{"class":286},[266,561,357],{"class":278},[266,563,360],{"class":278},[266,565,363],{"class":286},[266,567,569,571,573,575,577,579],{"class":211,"line":568},21,[266,570,416],{"class":278},[266,572,419],{"class":286},[266,574,422],{"class":278},[266,576,425],{"class":286},[266,578,428],{"class":282},[266,580,581],{"class":286},"()) {\n",[266,583,585,588,591,593,596,598,601],{"class":211,"line":584},22,[266,586,587],{"class":282},"            drawSceneGeometry",[266,589,590],{"class":286},"(app.",[266,592,543],{"class":282},[266,594,595],{"class":286},"(), ",[266,597,375],{"class":278},[266,599,600],{"class":286},"instance->pie, theme);",[266,602,603],{"class":271}," \u002F\u002F 通道 A：几何图元\n",[266,605,607,610,612,614,617,619,621,623],{"class":211,"line":606},23,[266,608,609],{"class":286},"            app.",[266,611,99],{"class":282},[266,613,546],{"class":286},[266,615,616],{"class":282},"drawScene",[266,618,287],{"class":286},[266,620,375],{"class":278},[266,622,600],{"class":286},[266,624,625],{"class":271},"             \u002F\u002F 通道 B：文字贴图\n",[266,627,629],{"class":211,"line":628},24,[266,630,631],{"class":286},"        }\n",[266,633,635],{"class":211,"line":634},25,[266,636,637],{"class":271},"        \u002F\u002F ... spark 与 table 同理\n",[266,639,641],{"class":211,"line":640},26,[266,642,384],{"class":286},[266,644,646],{"class":211,"line":645},27,[266,647,391],{"emptyLinePlaceholder":390},[266,649,651],{"class":211,"line":650},28,[266,652,653],{"class":271},"    \u002F\u002F 【阶段 5】帧末顶层合成：更新鼠标手型光标，并在最顶层绘制全局唯一 Tooltip\n",[266,655,657,660],{"class":211,"line":656},29,[266,658,659],{"class":282},"    updateCursor",[266,661,321],{"class":286},[266,663,665,668,670,672,675,678,680,683,686,689,693],{"class":211,"line":664},30,[266,666,667],{"class":278},"    if",[266,669,354],{"class":286},[266,671,357],{"class":278},[266,673,674],{"class":278}," int",[266,676,677],{"class":286}," active ",[266,679,336],{"class":278},[266,681,682],{"class":282}," instanceUnderPointer",[266,684,685],{"class":286},"(app); active ",[266,687,688],{"class":278},"!=",[266,690,692],{"class":691},"sj4cs"," 0",[266,694,695],{"class":286},") {\n",[266,697,699,702,704,706,708,711,714,716],{"class":211,"line":698},31,[266,700,701],{"class":282},"        kiriyama",[266,703,508],{"class":286},[266,705,526],{"class":282},[266,707,508],{"class":286},[266,709,710],{"class":282},"applyTooltip",[266,712,713],{"class":286},"(active, ",[266,715,375],{"class":278},[266,717,718],{"class":286},"tooltip_);\n",[266,720,722],{"class":211,"line":721},32,[266,723,384],{"class":286},[266,725,727,730,733,735,737],{"class":211,"line":726},33,[266,728,729],{"class":286},"    tooltip_->",[266,731,732],{"class":282},"update",[266,734,287],{"class":286},[266,736,375],{"class":278},[266,738,739],{"class":286},"root_, ctx);\n",[266,741,743],{"class":211,"line":742},34,[266,744,745],{"class":286},"}\n",[28,747],{},[230,749,751,752,755],{"id":750},"_12-阶段二拆解组件的双轨更新机制rebuild-慢路径-vs-o1-快路径","1.2 阶段二拆解：组件的双轨更新机制（",[14,753,754],{},"rebuild"," 慢路径 vs $O(1)$ 快路径）",[10,757,37,758,761,762,765,766,769],{},[14,759,760],{},"tickTree(ctx)"," 驱动组件的 ",[14,763,764],{},"onUpdate(ctx)"," 时，组件将更新严格拆分为**结构重排（慢路径）",[19,767,768],{},"与","高频交互（快路径）**两条轨道：",[771,772,774,775,778],"h4",{"id":773},"_1-慢路径rebuild当且仅当数据尺寸或滚动窗口变化时触发","1. 慢路径（",[14,776,777],{},"rebuild()","）：当且仅当数据、尺寸或滚动窗口变化时触发",[10,780,781,782,785,786,789,790,793,794,797],{},"组件调用 ",[14,783,784],{},"core::build*"," 纯函数生成零状态的 ",[14,787,788],{},"SceneNode"," 描述树，再通过 ",[14,791,792],{},"core::reconcileScene"," 与常驻子树 ",[14,795,796],{},"inner_"," 对账复用，最后收集叶子节点的裸指针供快路径直达：",[257,799,801],{"className":259,"code":800,"language":261,"meta":262,"style":262},"\u002F\u002F 文件：platform\u002Fsrc\u002Fsrc\u002FTableWidget.cc\nvoid TableWidget::rebuild() {\n    ensureColumnsMeasured(); \u002F\u002F 仅在 fontDirty_ 时测量一次列宽\n\n    \u002F\u002F 1. 下沉到 core 层：计算虚拟滚动视口 [firstRow, lastRow] 并构建可见行描述树\n    core::TableBuildResult built = core::buildTable(\n        data_, state_, config_, style_, frame().size(), columnWidths_, scrollOffset_);\n    layout_ = std::move(built.layout);\n\n    \u002F\u002F 2. 虚拟树对账：按 key (\"row.3\") 复用常驻节点，未变属性零标脏\n    if (built.root) {\n        core::reconcileScene(*inner_, *built.root);\n    }\n    collectRowNodes(); \u002F\u002F 缓存当前可见行的背景节点指针与逻辑行号\n\n    layoutDirty_ = false;\n    lastSize_ = frame().size();\n}\n",[14,802,803,808,822,833,837,842,867,883,901,905,910,917,939,943,953,957,970,986],{"__ignoreMap":262},[266,804,805],{"class":211,"line":268},[266,806,807],{"class":271},"\u002F\u002F 文件：platform\u002Fsrc\u002Fsrc\u002FTableWidget.cc\n",[266,809,810,812,815,817,819],{"class":211,"line":275},[266,811,279],{"class":278},[266,813,814],{"class":282}," TableWidget",[266,816,508],{"class":286},[266,818,754],{"class":282},[266,820,821],{"class":286},"() {\n",[266,823,824,827,830],{"class":211,"line":309},[266,825,826],{"class":282},"    ensureColumnsMeasured",[266,828,829],{"class":286},"();",[266,831,832],{"class":271}," \u002F\u002F 仅在 fontDirty_ 时测量一次列宽\n",[266,834,835],{"class":211,"line":315},[266,836,391],{"emptyLinePlaceholder":390},[266,838,839],{"class":211,"line":324},[266,840,841],{"class":271},"    \u002F\u002F 1. 下沉到 core 层：计算虚拟滚动视口 [firstRow, lastRow] 并构建可见行描述树\n",[266,843,844,847,849,852,855,857,859,861,864],{"class":211,"line":348},[266,845,846],{"class":282},"    core",[266,848,508],{"class":286},[266,850,851],{"class":282},"TableBuildResult",[266,853,854],{"class":286}," built ",[266,856,336],{"class":278},[266,858,505],{"class":282},[266,860,508],{"class":286},[266,862,863],{"class":282},"buildTable",[266,865,866],{"class":286},"(\n",[266,868,869,872,875,877,880],{"class":211,"line":366},[266,870,871],{"class":286},"        data_, state_, config_, style_, ",[266,873,874],{"class":282},"frame",[266,876,546],{"class":286},[266,878,879],{"class":282},"size",[266,881,882],{"class":286},"(), columnWidths_, scrollOffset_);\n",[266,884,885,888,890,893,895,898],{"class":211,"line":381},[266,886,887],{"class":286},"    layout_ ",[266,889,336],{"class":278},[266,891,892],{"class":282}," std",[266,894,508],{"class":286},[266,896,897],{"class":282},"move",[266,899,900],{"class":286},"(built.layout);\n",[266,902,903],{"class":211,"line":387},[266,904,391],{"emptyLinePlaceholder":390},[266,906,907],{"class":211,"line":394},[266,908,909],{"class":271},"    \u002F\u002F 2. 虚拟树对账：按 key (\"row.3\") 复用常驻节点，未变属性零标脏\n",[266,911,912,914],{"class":211,"line":400},[266,913,667],{"class":278},[266,915,916],{"class":286}," (built.root) {\n",[266,918,919,922,924,927,929,931,934,936],{"class":211,"line":413},[266,920,921],{"class":282},"        core",[266,923,508],{"class":286},[266,925,926],{"class":282},"reconcileScene",[266,928,287],{"class":286},[266,930,375],{"class":278},[266,932,933],{"class":286},"inner_, ",[266,935,375],{"class":278},[266,937,938],{"class":286},"built.root);\n",[266,940,941],{"class":211,"line":440},[266,942,384],{"class":286},[266,944,945,948,950],{"class":211,"line":462},[266,946,947],{"class":282},"    collectRowNodes",[266,949,829],{"class":286},[266,951,952],{"class":271}," \u002F\u002F 缓存当前可见行的背景节点指针与逻辑行号\n",[266,954,955],{"class":211,"line":484},[266,956,391],{"emptyLinePlaceholder":390},[266,958,959,962,964,967],{"class":211,"line":489},[266,960,961],{"class":286},"    layoutDirty_ ",[266,963,336],{"class":278},[266,965,966],{"class":691}," false",[266,968,969],{"class":286},";\n",[266,971,972,975,977,980,982,984],{"class":211,"line":494},[266,973,974],{"class":286},"    lastSize_ ",[266,976,336],{"class":278},[266,978,979],{"class":282}," frame",[266,981,546],{"class":286},[266,983,879],{"class":282},[266,985,534],{"class":286},[266,987,988],{"class":211,"line":500},[266,989,745],{"class":286},[771,991,993],{"id":992},"_2-快路径o1-原地修改悬停高亮选中切换扇区外扩动画","2. 快路径（$O(1)$ 原地修改）：悬停高亮、选中切换、扇区外扩动画",[10,995,996,997,1006,1007,1009],{},"高频交互",[19,998,999,1000,1002,1003],{},"完全绕过 ",[14,1001,777],{}," 与 ",[14,1004,1005],{},"reconcileScene()","，直接通过 ",[14,1008,777],{}," 阶段缓存的节点指针原地修改几何顶点或样式属性：",[257,1011,1013],{"className":259,"code":1012,"language":261,"meta":262,"style":262},"\u002F\u002F 快路径示例 A（PieChart.cc）：60FPS 扇区外扩动画，直接改单个 Polygon 节点的顶点数组\nvoid PieChart::applyExplode(std::size_t index, double radius) {\n    core::SceneNode* node = sliceNodes_[index];\n    PieLayout expanded = layout_;\n    expanded.outerRadius += radius;\n    node->setPoints(core::sectorPolygon(layout_.sectors[index], expanded, segments_));\n}\n\n\u002F\u002F 快路径示例 B（SceneBuilder.cc）：表格悬停\u002F选中行切换，O(可见行数) 原地刷写语义背景色\nvoid updateTableHighlights(std::span\u003Cconst std::pair\u003Cstd::size_t, SceneNode*>> rowBgNodes,\n                           std::optional\u003Cstd::size_t> selectedRow,\n                           std::optional\u003Cstd::size_t> hoveredRow,\n                           const TableStyle& style) {\n    for (const auto& [rowIndex, bgNode] : rowBgNodes) {\n        if (!bgNode) continue;\n        SceneStyle bgStyle = bgNode->style();\n        if (selectedRow && *selectedRow == rowIndex) {\n            bgStyle.fill = style.rowSelectedFill; \u002F\u002F ColorToken::TableRowSelected\n        } else if (hoveredRow && *hoveredRow == rowIndex) {\n            bgStyle.fill = style.rowHoverFill;    \u002F\u002F ColorToken::TableRowHover\n        } else {\n            bgStyle.fill = (rowIndex % 2 == 1) ? style.rowAltFill : style.rowFill;\n        }\n        bgNode->setStyle(bgStyle); \u002F\u002F SceneNode 内部相等短路，未变行不触发 markDirty()\n    }\n}\n",[14,1014,1015,1020,1056,1074,1087,1098,1119,1123,1127,1132,1182,1208,1231,1246,1259,1276,1294,1315,1328,1353,1365,1373,1408,1412,1426,1430],{"__ignoreMap":262},[266,1016,1017],{"class":211,"line":268},[266,1018,1019],{"class":271},"\u002F\u002F 快路径示例 A（PieChart.cc）：60FPS 扇区外扩动画，直接改单个 Polygon 节点的顶点数组\n",[266,1021,1022,1024,1027,1029,1032,1034,1037,1039,1042,1045,1048,1051,1054],{"class":211,"line":275},[266,1023,279],{"class":278},[266,1025,1026],{"class":282}," PieChart",[266,1028,508],{"class":286},[266,1030,1031],{"class":282},"applyExplode",[266,1033,287],{"class":286},[266,1035,1036],{"class":282},"std",[266,1038,508],{"class":286},[266,1040,1041],{"class":278},"size_t",[266,1043,1044],{"class":296}," index",[266,1046,1047],{"class":286},", ",[266,1049,1050],{"class":278},"double",[266,1052,1053],{"class":296}," radius",[266,1055,695],{"class":286},[266,1057,1058,1060,1062,1064,1066,1069,1071],{"class":211,"line":309},[266,1059,846],{"class":282},[266,1061,508],{"class":286},[266,1063,788],{"class":282},[266,1065,375],{"class":278},[266,1067,1068],{"class":286}," node ",[266,1070,336],{"class":278},[266,1072,1073],{"class":286}," sliceNodes_[index];\n",[266,1075,1076,1079,1082,1084],{"class":211,"line":315},[266,1077,1078],{"class":282},"    PieLayout",[266,1080,1081],{"class":286}," expanded ",[266,1083,336],{"class":278},[266,1085,1086],{"class":286}," layout_;\n",[266,1088,1089,1092,1095],{"class":211,"line":324},[266,1090,1091],{"class":286},"    expanded.outerRadius ",[266,1093,1094],{"class":278},"+=",[266,1096,1097],{"class":286}," radius;\n",[266,1099,1100,1103,1106,1108,1111,1113,1116],{"class":211,"line":348},[266,1101,1102],{"class":286},"    node->",[266,1104,1105],{"class":282},"setPoints",[266,1107,287],{"class":286},[266,1109,1110],{"class":282},"core",[266,1112,508],{"class":286},[266,1114,1115],{"class":282},"sectorPolygon",[266,1117,1118],{"class":286},"(layout_.sectors[index], expanded, segments_));\n",[266,1120,1121],{"class":211,"line":366},[266,1122,745],{"class":286},[266,1124,1125],{"class":211,"line":381},[266,1126,391],{"emptyLinePlaceholder":390},[266,1128,1129],{"class":211,"line":387},[266,1130,1131],{"class":271},"\u002F\u002F 快路径示例 B（SceneBuilder.cc）：表格悬停\u002F选中行切换，O(可见行数) 原地刷写语义背景色\n",[266,1133,1134,1136,1139,1141,1143,1145,1147,1150,1152,1154,1156,1159,1161,1163,1165,1167,1169,1171,1173,1176,1179],{"class":211,"line":394},[266,1135,279],{"class":278},[266,1137,1138],{"class":282}," updateTableHighlights",[266,1140,287],{"class":286},[266,1142,1036],{"class":282},[266,1144,508],{"class":286},[266,1146,266],{"class":282},[266,1148,1149],{"class":286},"\u003C",[266,1151,357],{"class":278},[266,1153,892],{"class":282},[266,1155,508],{"class":286},[266,1157,1158],{"class":282},"pair",[266,1160,1149],{"class":286},[266,1162,1036],{"class":282},[266,1164,508],{"class":286},[266,1166,1041],{"class":278},[266,1168,1047],{"class":286},[266,1170,788],{"class":282},[266,1172,375],{"class":278},[266,1174,1175],{"class":286},">> ",[266,1177,1178],{"class":296},"rowBgNodes",[266,1180,1181],{"class":286},",\n",[266,1183,1184,1187,1189,1192,1194,1196,1198,1200,1203,1206],{"class":211,"line":400},[266,1185,1186],{"class":282},"                           std",[266,1188,508],{"class":286},[266,1190,1191],{"class":282},"optional",[266,1193,1149],{"class":286},[266,1195,1036],{"class":282},[266,1197,508],{"class":286},[266,1199,1041],{"class":278},[266,1201,1202],{"class":286},"> ",[266,1204,1205],{"class":296},"selectedRow",[266,1207,1181],{"class":286},[266,1209,1210,1212,1214,1216,1218,1220,1222,1224,1226,1229],{"class":211,"line":413},[266,1211,1186],{"class":282},[266,1213,508],{"class":286},[266,1215,1191],{"class":282},[266,1217,1149],{"class":286},[266,1219,1036],{"class":282},[266,1221,508],{"class":286},[266,1223,1041],{"class":278},[266,1225,1202],{"class":286},[266,1227,1228],{"class":296},"hoveredRow",[266,1230,1181],{"class":286},[266,1232,1233,1236,1239,1241,1244],{"class":211,"line":440},[266,1234,1235],{"class":278},"                           const",[266,1237,1238],{"class":282}," TableStyle",[266,1240,293],{"class":278},[266,1242,1243],{"class":296}," style",[266,1245,695],{"class":286},[266,1247,1248,1250,1252,1254,1256],{"class":211,"line":462},[266,1249,351],{"class":278},[266,1251,354],{"class":286},[266,1253,357],{"class":278},[266,1255,360],{"class":278},[266,1257,1258],{"class":286}," [rowIndex, bgNode] : rowBgNodes) {\n",[266,1260,1261,1263,1265,1268,1271,1274],{"class":211,"line":484},[266,1262,416],{"class":278},[266,1264,354],{"class":286},[266,1266,1267],{"class":278},"!",[266,1269,1270],{"class":286},"bgNode) ",[266,1272,1273],{"class":278},"continue",[266,1275,969],{"class":286},[266,1277,1278,1281,1284,1286,1289,1292],{"class":211,"line":489},[266,1279,1280],{"class":282},"        SceneStyle",[266,1282,1283],{"class":286}," bgStyle ",[266,1285,336],{"class":278},[266,1287,1288],{"class":286}," bgNode->",[266,1290,1291],{"class":282},"style",[266,1293,534],{"class":286},[266,1295,1296,1298,1301,1303,1306,1309,1312],{"class":211,"line":494},[266,1297,416],{"class":278},[266,1299,1300],{"class":286}," (selectedRow ",[266,1302,422],{"class":278},[266,1304,1305],{"class":278}," *",[266,1307,1308],{"class":286},"selectedRow ",[266,1310,1311],{"class":278},"==",[266,1313,1314],{"class":286}," rowIndex) {\n",[266,1316,1317,1320,1322,1325],{"class":211,"line":500},[266,1318,1319],{"class":286},"            bgStyle.fill ",[266,1321,336],{"class":278},[266,1323,1324],{"class":286}," style.rowSelectedFill;",[266,1326,1327],{"class":271}," \u002F\u002F ColorToken::TableRowSelected\n",[266,1329,1330,1333,1336,1339,1342,1344,1346,1349,1351],{"class":211,"line":537},[266,1331,1332],{"class":286},"        } ",[266,1334,1335],{"class":278},"else",[266,1337,1338],{"class":278}," if",[266,1340,1341],{"class":286}," (hoveredRow ",[266,1343,422],{"class":278},[266,1345,1305],{"class":278},[266,1347,1348],{"class":286},"hoveredRow ",[266,1350,1311],{"class":278},[266,1352,1314],{"class":286},[266,1354,1355,1357,1359,1362],{"class":211,"line":555},[266,1356,1319],{"class":286},[266,1358,336],{"class":278},[266,1360,1361],{"class":286}," style.rowHoverFill;",[266,1363,1364],{"class":271},"    \u002F\u002F ColorToken::TableRowHover\n",[266,1366,1367,1369,1371],{"class":211,"line":568},[266,1368,1332],{"class":286},[266,1370,1335],{"class":278},[266,1372,306],{"class":286},[266,1374,1375,1377,1379,1382,1385,1388,1391,1394,1396,1399,1402,1405],{"class":211,"line":584},[266,1376,1319],{"class":286},[266,1378,336],{"class":278},[266,1380,1381],{"class":286}," (rowIndex ",[266,1383,1384],{"class":278},"%",[266,1386,1387],{"class":691}," 2",[266,1389,1390],{"class":278}," ==",[266,1392,1393],{"class":691}," 1",[266,1395,300],{"class":286},[266,1397,1398],{"class":278},"?",[266,1400,1401],{"class":286}," style.rowAltFill ",[266,1403,1404],{"class":278},":",[266,1406,1407],{"class":286}," style.rowFill;\n",[266,1409,1410],{"class":211,"line":606},[266,1411,631],{"class":286},[266,1413,1414,1417,1420,1423],{"class":211,"line":628},[266,1415,1416],{"class":286},"        bgNode->",[266,1418,1419],{"class":282},"setStyle",[266,1421,1422],{"class":286},"(bgStyle);",[266,1424,1425],{"class":271}," \u002F\u002F SceneNode 内部相等短路，未变行不触发 markDirty()\n",[266,1427,1428],{"class":211,"line":634},[266,1429,384],{"class":286},[266,1431,1432],{"class":211,"line":640},[266,1433,745],{"class":286},[28,1435],{},[230,1437,1439,1440,236],{"id":1438},"_13-阶段三拆解场景树递归遍历与语义色解析scenedrawcc","1.3 阶段三拆解：场景树递归遍历与语义色解析（",[14,1441,1442],{},"SceneDraw.cc",[10,1444,1445,1451,1452,1454,1455,1458],{},[42,1446,1448],{"href":1447},"file:\u002F\u002F\u002FE:\u002Fkiriyama_canvas\u002Fplatform\u002Fsrc\u002Fsrc\u002FSceneDraw.cc#L89-L187",[14,1449,1450],{},"drawSceneGeometry"," 自顶向下遍历 ",[14,1453,788],{}," 树，在遍历栈上实时完成",[19,1456,1457],{},"仿射矩阵合成、透明度累乘、语义色 Token 解析与局部裁剪框压栈","：",[257,1460,1462],{"className":259,"code":1461,"language":261,"meta":262,"style":262},"\u002F\u002F 文件：platform\u002Fsrc\u002Fsrc\u002FSceneDraw.cc\nvoid drawNode(Painter& painter, const SceneNode& node, const Affine2D& parentWorld,\n              double parentOpacity, const core::ChartTheme& theme,\n              const std::vector\u003Ccore::Color>* seriesPalette) {\n    if (!node.visible()) return;\n\n    \u002F\u002F 1. 透明度累乘剪枝\n    const core::SceneStyle& style = node.style();\n    const double effective = parentOpacity * style.opacity;\n    if (effective \u003C= 0.0) return;\n\n    \u002F\u002F 2. 色板继承与 ColorToken 实时解析（切换深浅色主题时无需改动场景树任何节点）\n    const auto* palette = style.seriesPalette ? style.seriesPalette.get() : seriesPalette;\n    const core::Color fill = style.fill.resolve(theme, palette);\n    const core::Color stroke = style.stroke.resolve(theme, palette);\n\n    \u002F\u002F 3. 世界矩阵合成：world = parentWorld * localTransform\n    const Affine2D world = parentWorld * node.transform();\n\n    \u002F\u002F 4. 压入子树裁剪框（如表格滚动视口 clipRect）\n    SDL_Rect savedClip{};\n    const bool clipped = pushClip(painter, world, node.clipRect(), savedClip);\n\n    \u002F\u002F 5. 按图元类型（Rect \u002F Ellipse \u002F Polygon \u002F Polyline \u002F Line）调用 Painter 落笔\n    \u002F\u002F ...\n\n    \u002F\u002F 6. 递归绘制子节点并恢复裁剪栈\n    for (const auto& child : node.children()) {\n        if (child) drawNode(painter, *child, world, effective, theme, palette);\n    }\n    restoreClip(painter, clipped, savedClip);\n}\n",[14,1463,1464,1469,1512,1537,1568,1589,1593,1598,1623,1643,1662,1666,1671,1702,1726,1748,1752,1757,1780,1784,1789,1797,1821,1825,1830,1835,1839,1844,1862,1880,1884,1892],{"__ignoreMap":262},[266,1465,1466],{"class":211,"line":268},[266,1467,1468],{"class":271},"\u002F\u002F 文件：platform\u002Fsrc\u002Fsrc\u002FSceneDraw.cc\n",[266,1470,1471,1473,1476,1478,1481,1483,1486,1488,1490,1493,1495,1498,1500,1502,1505,1507,1510],{"class":211,"line":275},[266,1472,279],{"class":278},[266,1474,1475],{"class":282}," drawNode",[266,1477,287],{"class":286},[266,1479,1480],{"class":282},"Painter",[266,1482,293],{"class":278},[266,1484,1485],{"class":296}," painter",[266,1487,1047],{"class":286},[266,1489,357],{"class":278},[266,1491,1492],{"class":282}," SceneNode",[266,1494,293],{"class":278},[266,1496,1497],{"class":296}," node",[266,1499,1047],{"class":286},[266,1501,357],{"class":278},[266,1503,1504],{"class":282}," Affine2D",[266,1506,293],{"class":278},[266,1508,1509],{"class":296}," parentWorld",[266,1511,1181],{"class":286},[266,1513,1514,1517,1520,1522,1524,1526,1528,1530,1532,1535],{"class":211,"line":309},[266,1515,1516],{"class":278},"              double",[266,1518,1519],{"class":296}," parentOpacity",[266,1521,1047],{"class":286},[266,1523,357],{"class":278},[266,1525,505],{"class":282},[266,1527,508],{"class":286},[266,1529,511],{"class":282},[266,1531,293],{"class":278},[266,1533,1534],{"class":296}," theme",[266,1536,1181],{"class":286},[266,1538,1539,1542,1544,1546,1549,1551,1553,1555,1558,1561,1563,1566],{"class":211,"line":315},[266,1540,1541],{"class":278},"              const",[266,1543,892],{"class":282},[266,1545,508],{"class":286},[266,1547,1548],{"class":282},"vector",[266,1550,1149],{"class":286},[266,1552,1110],{"class":282},[266,1554,508],{"class":286},[266,1556,1557],{"class":282},"Color",[266,1559,1560],{"class":286},">",[266,1562,375],{"class":278},[266,1564,1565],{"class":296}," seriesPalette",[266,1567,695],{"class":286},[266,1569,1570,1572,1574,1576,1579,1581,1584,1587],{"class":211,"line":324},[266,1571,667],{"class":278},[266,1573,354],{"class":286},[266,1575,1267],{"class":278},[266,1577,1578],{"class":286},"node.",[266,1580,428],{"class":282},[266,1582,1583],{"class":286},"()) ",[266,1585,1586],{"class":278},"return",[266,1588,969],{"class":286},[266,1590,1591],{"class":211,"line":348},[266,1592,391],{"emptyLinePlaceholder":390},[266,1594,1595],{"class":211,"line":366},[266,1596,1597],{"class":271},"    \u002F\u002F 1. 透明度累乘剪枝\n",[266,1599,1600,1602,1604,1606,1609,1611,1614,1616,1619,1621],{"class":211,"line":381},[266,1601,327],{"class":278},[266,1603,505],{"class":282},[266,1605,508],{"class":286},[266,1607,1608],{"class":282},"SceneStyle",[266,1610,293],{"class":278},[266,1612,1613],{"class":286}," style ",[266,1615,336],{"class":278},[266,1617,1618],{"class":286}," node.",[266,1620,1291],{"class":282},[266,1622,534],{"class":286},[266,1624,1625,1627,1630,1633,1635,1638,1640],{"class":211,"line":387},[266,1626,327],{"class":278},[266,1628,1629],{"class":278}," double",[266,1631,1632],{"class":286}," effective ",[266,1634,336],{"class":278},[266,1636,1637],{"class":286}," parentOpacity ",[266,1639,375],{"class":278},[266,1641,1642],{"class":286}," style.opacity;\n",[266,1644,1645,1647,1650,1653,1656,1658,1660],{"class":211,"line":394},[266,1646,667],{"class":278},[266,1648,1649],{"class":286}," (effective ",[266,1651,1652],{"class":278},"\u003C=",[266,1654,1655],{"class":691}," 0.0",[266,1657,300],{"class":286},[266,1659,1586],{"class":278},[266,1661,969],{"class":286},[266,1663,1664],{"class":211,"line":400},[266,1665,391],{"emptyLinePlaceholder":390},[266,1667,1668],{"class":211,"line":413},[266,1669,1670],{"class":271},"    \u002F\u002F 2. 色板继承与 ColorToken 实时解析（切换深浅色主题时无需改动场景树任何节点）\n",[266,1672,1673,1675,1678,1681,1683,1686,1688,1691,1694,1697,1699],{"class":211,"line":440},[266,1674,327],{"class":278},[266,1676,1677],{"class":278}," auto*",[266,1679,1680],{"class":286}," palette ",[266,1682,336],{"class":278},[266,1684,1685],{"class":286}," style.seriesPalette ",[266,1687,1398],{"class":278},[266,1689,1690],{"class":286}," style.seriesPalette.",[266,1692,1693],{"class":282},"get",[266,1695,1696],{"class":286},"() ",[266,1698,1404],{"class":278},[266,1700,1701],{"class":286}," seriesPalette;\n",[266,1703,1704,1706,1708,1710,1712,1715,1717,1720,1723],{"class":211,"line":462},[266,1705,327],{"class":278},[266,1707,505],{"class":282},[266,1709,508],{"class":286},[266,1711,1557],{"class":282},[266,1713,1714],{"class":286}," fill ",[266,1716,336],{"class":278},[266,1718,1719],{"class":286}," style.fill.",[266,1721,1722],{"class":282},"resolve",[266,1724,1725],{"class":286},"(theme, palette);\n",[266,1727,1728,1730,1732,1734,1736,1739,1741,1744,1746],{"class":211,"line":484},[266,1729,327],{"class":278},[266,1731,505],{"class":282},[266,1733,508],{"class":286},[266,1735,1557],{"class":282},[266,1737,1738],{"class":286}," stroke ",[266,1740,336],{"class":278},[266,1742,1743],{"class":286}," style.stroke.",[266,1745,1722],{"class":282},[266,1747,1725],{"class":286},[266,1749,1750],{"class":211,"line":489},[266,1751,391],{"emptyLinePlaceholder":390},[266,1753,1754],{"class":211,"line":494},[266,1755,1756],{"class":271},"    \u002F\u002F 3. 世界矩阵合成：world = parentWorld * localTransform\n",[266,1758,1759,1761,1763,1766,1768,1771,1773,1775,1778],{"class":211,"line":500},[266,1760,327],{"class":278},[266,1762,1504],{"class":282},[266,1764,1765],{"class":286}," world ",[266,1767,336],{"class":278},[266,1769,1770],{"class":286}," parentWorld ",[266,1772,375],{"class":278},[266,1774,1618],{"class":286},[266,1776,1777],{"class":282},"transform",[266,1779,534],{"class":286},[266,1781,1782],{"class":211,"line":537},[266,1783,391],{"emptyLinePlaceholder":390},[266,1785,1786],{"class":211,"line":555},[266,1787,1788],{"class":271},"    \u002F\u002F 4. 压入子树裁剪框（如表格滚动视口 clipRect）\n",[266,1790,1791,1794],{"class":211,"line":568},[266,1792,1793],{"class":282},"    SDL_Rect",[266,1795,1796],{"class":286}," savedClip{};\n",[266,1798,1799,1801,1804,1807,1809,1812,1815,1818],{"class":211,"line":584},[266,1800,327],{"class":278},[266,1802,1803],{"class":278}," bool",[266,1805,1806],{"class":286}," clipped ",[266,1808,336],{"class":278},[266,1810,1811],{"class":282}," pushClip",[266,1813,1814],{"class":286},"(painter, world, node.",[266,1816,1817],{"class":282},"clipRect",[266,1819,1820],{"class":286},"(), savedClip);\n",[266,1822,1823],{"class":211,"line":606},[266,1824,391],{"emptyLinePlaceholder":390},[266,1826,1827],{"class":211,"line":628},[266,1828,1829],{"class":271},"    \u002F\u002F 5. 按图元类型（Rect \u002F Ellipse \u002F Polygon \u002F Polyline \u002F Line）调用 Painter 落笔\n",[266,1831,1832],{"class":211,"line":634},[266,1833,1834],{"class":271},"    \u002F\u002F ...\n",[266,1836,1837],{"class":211,"line":640},[266,1838,391],{"emptyLinePlaceholder":390},[266,1840,1841],{"class":211,"line":645},[266,1842,1843],{"class":271},"    \u002F\u002F 6. 递归绘制子节点并恢复裁剪栈\n",[266,1845,1846,1848,1850,1852,1854,1857,1860],{"class":211,"line":650},[266,1847,351],{"class":278},[266,1849,354],{"class":286},[266,1851,357],{"class":278},[266,1853,360],{"class":278},[266,1855,1856],{"class":286}," child : node.",[266,1858,1859],{"class":282},"children",[266,1861,581],{"class":286},[266,1863,1864,1866,1869,1872,1875,1877],{"class":211,"line":656},[266,1865,416],{"class":278},[266,1867,1868],{"class":286}," (child) ",[266,1870,1871],{"class":282},"drawNode",[266,1873,1874],{"class":286},"(painter, ",[266,1876,375],{"class":278},[266,1878,1879],{"class":286},"child, world, effective, theme, palette);\n",[266,1881,1882],{"class":211,"line":664},[266,1883,384],{"class":286},[266,1885,1886,1889],{"class":211,"line":698},[266,1887,1888],{"class":282},"    restoreClip",[266,1890,1891],{"class":286},"(painter, clipped, savedClip);\n",[266,1893,1894],{"class":211,"line":721},[266,1895,745],{"class":286},[28,1897],{},[230,1899,1901],{"id":1900},"_14-阶段四拆解双通道光栅化几何三角剖分-文本双级缓存","1.4 阶段四拆解：双通道光栅化（几何三角剖分 + 文本双级缓存）",[10,1903,1904,1905,236,1907,1909,1910,1913],{},"为什么分成**几何通道（",[14,1906,1450],{},[19,1908,768],{},"文本通道（",[14,1911,1912],{},"SdlTextRenderer::drawScene","）**两遍遍历？",[1915,1916,1917,1924],"ol",{},[1918,1919,1920,1923],"li",{},[19,1921,1922],{},"Z 序保证","：图表数值标签永远浮在扇区或柱体之上，不会因树节点交错被相邻图元遮挡；",[1918,1925,1926,1929,1930,1932,1933,1936],{},[19,1927,1928],{},"GPU 状态零抖动","：几何通道全走无纹理顶点批次（",[14,1931,187],{},"），文本通道全走字库纹理贴图（",[14,1934,1935],{},"SDL_RenderTexture","）。",[771,1938,1940,1941,1944],{"id":1939},"通道-apainterfillpolygon-凸凹自适应三角剖分","通道 A：",[14,1942,1943],{},"Painter::fillPolygon"," 凸\u002F凹自适应三角剖分",[10,1946,1947,1948,1951,1952,1958],{},"当环形图（Donut）扇区扫角超过 $180^\\circ$ 时，内弧向内凹陷构成",[19,1949,1950],{},"凹多边形","。若盲目使用三角扇（Triangle Fan），连线会直接穿过中心圆孔造成“白楔子”伪影。",[42,1953,1955],{"href":1954},"file:\u002F\u002F\u002FE:\u002Fkiriyama_canvas\u002Fplatform\u002Fsrc\u002Fsrc\u002FPainter.cc#L264-L353",[14,1956,1957],{},"Painter.cc"," 采用叉积符号检测凸性，自动分流：",[257,1960,1962],{"className":259,"code":1961,"language":261,"meta":262,"style":262},"\u002F\u002F 文件：platform\u002Fsrc\u002Fsrc\u002FPainter.cc\nbool convex = true;\nfor (std::size_t k = 0; k \u003C n; ++k) {\n    \u002F\u002F 检查相邻边转向叉积是否与多边形整体绕向 (orientation) 一致\n    if (crossAt(clean[k], clean[(k + 1) % n], clean[(k + 2) % n]) * orientation \u003C 0.0) {\n        convex = false;\n        break;\n    }\n}\n\nif (convex) {\n    \u002F\u002F 【快路径】凸多边形（圆角矩形、椭圆、常规扇区）：O(n) 三角扇 (0, i, i+1)\n    for (std::size_t i = 1; i + 1 \u003C n; ++i) {\n        indices.insert(indices.end(), {0, static_cast\u003Cint>(i), static_cast\u003Cint>(i + 1)});\n    }\n} else {\n    \u002F\u002F 【精确路径】凹多边形（>180° 甜甜圈扇环）：反射点耳切法 (Ear-Clipping)\n    \u002F\u002F 仅收集内角 >180° 的凹顶点 (reflex)，判断候选耳三角 (prev, here, next) 内是否包含反射点\n    collectReflex();\n    while (ring.size() > 3) { \u002F* 剪除无反射点落入的凸耳并更新邻点凹凸性 *\u002F }\n}\n",[14,1963,1964,1969,1984,2018,2023,2069,2080,2087,2091,2095,2099,2107,2112,2148,2188,2192,2201,2206,2211,2218,2244],{"__ignoreMap":262},[266,1965,1966],{"class":211,"line":268},[266,1967,1968],{"class":271},"\u002F\u002F 文件：platform\u002Fsrc\u002Fsrc\u002FPainter.cc\n",[266,1970,1971,1974,1977,1979,1982],{"class":211,"line":275},[266,1972,1973],{"class":278},"bool",[266,1975,1976],{"class":286}," convex ",[266,1978,336],{"class":278},[266,1980,1981],{"class":691}," true",[266,1983,969],{"class":286},[266,1985,1986,1989,1991,1993,1995,1997,2000,2002,2004,2007,2009,2012,2015],{"class":211,"line":309},[266,1987,1988],{"class":278},"for",[266,1990,354],{"class":286},[266,1992,1036],{"class":282},[266,1994,508],{"class":286},[266,1996,1041],{"class":278},[266,1998,1999],{"class":286}," k ",[266,2001,336],{"class":278},[266,2003,692],{"class":691},[266,2005,2006],{"class":286},"; k ",[266,2008,1149],{"class":278},[266,2010,2011],{"class":286}," n; ",[266,2013,2014],{"class":278},"++",[266,2016,2017],{"class":286},"k) {\n",[266,2019,2020],{"class":211,"line":315},[266,2021,2022],{"class":271},"    \u002F\u002F 检查相邻边转向叉积是否与多边形整体绕向 (orientation) 一致\n",[266,2024,2025,2027,2029,2032,2035,2038,2040,2042,2044,2047,2049,2051,2053,2055,2058,2060,2063,2065,2067],{"class":211,"line":324},[266,2026,667],{"class":278},[266,2028,354],{"class":286},[266,2030,2031],{"class":282},"crossAt",[266,2033,2034],{"class":286},"(clean[k], clean[(k ",[266,2036,2037],{"class":278},"+",[266,2039,1393],{"class":691},[266,2041,300],{"class":286},[266,2043,1384],{"class":278},[266,2045,2046],{"class":286}," n], clean[(k ",[266,2048,2037],{"class":278},[266,2050,1387],{"class":691},[266,2052,300],{"class":286},[266,2054,1384],{"class":278},[266,2056,2057],{"class":286}," n]) ",[266,2059,375],{"class":278},[266,2061,2062],{"class":286}," orientation ",[266,2064,1149],{"class":278},[266,2066,1655],{"class":691},[266,2068,695],{"class":286},[266,2070,2071,2074,2076,2078],{"class":211,"line":348},[266,2072,2073],{"class":286},"        convex ",[266,2075,336],{"class":278},[266,2077,966],{"class":691},[266,2079,969],{"class":286},[266,2081,2082,2085],{"class":211,"line":366},[266,2083,2084],{"class":278},"        break",[266,2086,969],{"class":286},[266,2088,2089],{"class":211,"line":381},[266,2090,384],{"class":286},[266,2092,2093],{"class":211,"line":387},[266,2094,745],{"class":286},[266,2096,2097],{"class":211,"line":394},[266,2098,391],{"emptyLinePlaceholder":390},[266,2100,2101,2104],{"class":211,"line":400},[266,2102,2103],{"class":278},"if",[266,2105,2106],{"class":286}," (convex) {\n",[266,2108,2109],{"class":211,"line":413},[266,2110,2111],{"class":271},"    \u002F\u002F 【快路径】凸多边形（圆角矩形、椭圆、常规扇区）：O(n) 三角扇 (0, i, i+1)\n",[266,2113,2114,2116,2118,2120,2122,2124,2127,2129,2131,2134,2136,2138,2141,2143,2145],{"class":211,"line":440},[266,2115,351],{"class":278},[266,2117,354],{"class":286},[266,2119,1036],{"class":282},[266,2121,508],{"class":286},[266,2123,1041],{"class":278},[266,2125,2126],{"class":286}," i ",[266,2128,336],{"class":278},[266,2130,1393],{"class":691},[266,2132,2133],{"class":286},"; i ",[266,2135,2037],{"class":278},[266,2137,1393],{"class":691},[266,2139,2140],{"class":278}," \u003C",[266,2142,2011],{"class":286},[266,2144,2014],{"class":278},[266,2146,2147],{"class":286},"i) {\n",[266,2149,2150,2153,2156,2159,2162,2165,2168,2170,2173,2176,2178,2181,2183,2185],{"class":211,"line":462},[266,2151,2152],{"class":286},"        indices.",[266,2154,2155],{"class":282},"insert",[266,2157,2158],{"class":286},"(indices.",[266,2160,2161],{"class":282},"end",[266,2163,2164],{"class":286},"(), {",[266,2166,2167],{"class":691},"0",[266,2169,1047],{"class":286},[266,2171,2172],{"class":278},"static_cast\u003Cint>",[266,2174,2175],{"class":286},"(i), ",[266,2177,2172],{"class":278},[266,2179,2180],{"class":286},"(i ",[266,2182,2037],{"class":278},[266,2184,1393],{"class":691},[266,2186,2187],{"class":286},")});\n",[266,2189,2190],{"class":211,"line":484},[266,2191,384],{"class":286},[266,2193,2194,2197,2199],{"class":211,"line":489},[266,2195,2196],{"class":286},"} ",[266,2198,1335],{"class":278},[266,2200,306],{"class":286},[266,2202,2203],{"class":211,"line":494},[266,2204,2205],{"class":271},"    \u002F\u002F 【精确路径】凹多边形（>180° 甜甜圈扇环）：反射点耳切法 (Ear-Clipping)\n",[266,2207,2208],{"class":211,"line":500},[266,2209,2210],{"class":271},"    \u002F\u002F 仅收集内角 >180° 的凹顶点 (reflex)，判断候选耳三角 (prev, here, next) 内是否包含反射点\n",[266,2212,2213,2216],{"class":211,"line":537},[266,2214,2215],{"class":282},"    collectReflex",[266,2217,534],{"class":286},[266,2219,2220,2223,2226,2228,2230,2232,2235,2238,2241],{"class":211,"line":555},[266,2221,2222],{"class":278},"    while",[266,2224,2225],{"class":286}," (ring.",[266,2227,879],{"class":282},[266,2229,1696],{"class":286},[266,2231,1560],{"class":278},[266,2233,2234],{"class":691}," 3",[266,2236,2237],{"class":286},") {",[266,2239,2240],{"class":271}," \u002F* 剪除无反射点落入的凸耳并更新邻点凹凸性 *\u002F",[266,2242,2243],{"class":286}," }\n",[266,2245,2246],{"class":211,"line":568},[266,2247,745],{"class":286},[771,2249,2251,2252,2255],{"id":2250},"通道-bsdltextrenderer-双级全白纹理缓存","通道 B：",[14,2253,2254],{},"SdlTextRenderer"," 双级全白纹理缓存",[10,2257,2258,2264],{},[42,2259,2261],{"href":2260},"file:\u002F\u002F\u002FE:\u002Fkiriyama_canvas\u002Fplatform\u002Fsrc\u002Fsrc\u002FTextRenderer.cc#L229-L368",[14,2262,2263],{},"TextRenderer.cc"," 通过三项设计解决 Canvas 文字渲染的性能与清晰度痛点：",[2266,2267,2268,2299,2324],"ul",{},[1918,2269,2270,2273,2274,2277,2278,2284,2285,2288,2289,2298],{},[19,2271,2272],{},"双级缓存","：默认 ",[14,2275,2276],{},"letterSpacing == 0"," 时走 ",[19,2279,2280,2283],{},[14,2281,2282],{},"LineCache","（512 条整行纹理）","，直接调用 ",[14,2286,2287],{},"TTF_RenderText_Blended"," 保留 FreeType 内部 26.6 定点数亚像素推进与 Kerning 字偶距；仅在自定义字距时退回 ",[19,2290,2291,2294,2295,236],{},[14,2292,2293],{},"GlyphCache","（4096 字紧凑位图，",[14,2296,2297],{},"TTF_GetGlyphImage","。",[1918,2300,2301,2308,2309,2312,2313,2316,2317,2320,2321,2298],{},[19,2302,2303,2304,2307],{},"纯白纹理 + ",[14,2305,2306],{},"ColorMod"," 染色","：缓存 Key 仅含 ",[14,2310,2311],{},"(TTF_Font*, sizePt, text)","，纹理统一生成为纯白色 ",[14,2314,2315],{},"(255,255,255,255)","，绘制时用 ",[14,2318,2319],{},"SDL_SetTextureColorMod"," 动态乘算目标颜色，文字换色\u002F换主题 ",[19,2322,2323],{},"100% 复用原纹理",[1918,2325,2326,2329,2330,2333,2334,2337],{},[19,2327,2328],{},"整像素吸附","：开启 ",[14,2331,2332],{},"SDL_SCALEMODE_NEAREST"," 并对落笔坐标执行 ",[14,2335,2336],{},"std::round"," 整像素对齐，消除半像素线性插值导致的字体发虚。",[28,2339],{},[31,2341,2343],{"id":2342},"第二章分层与核心模块设计","第二章：分层与核心模块设计",[230,2345,2347],{"id":2346},"_21-零-sdl-依赖的黄金分割线","2.1 零 SDL 依赖的黄金分割线",[10,2349,2350,2351,2371],{},"仓库自底向上分为四层，",[19,2352,2353,2354,2357,2358,2357,2360,2363,2364,2367,2368],{},"前三大层（",[14,2355,2356],{},"math","、",[14,2359,1110],{},[14,2361,2362],{},"kiriyama_scene"," + ",[14,2365,2366],{},"kiriyama_text","）完全不引入 ",[14,2369,2370],{},"\u003CSDL3\u002FSDL.h>","，所有几何计算、虚拟树对账、组件交互与按键分发均可直接编译为纯逻辑 WASM 在 Node.js 下运行 GoogleTest：",[257,2373,2377],{"className":2374,"code":2376,"language":99,"meta":262},[2375],"language-text","kiriyama_canvas\u002F\n├── math\u002F                  # [第1层] 纯数学库：Vec2d, Affine2D, Easing, Spring(解析弹簧), Ticks\n├── core\u002F                  # [第2层] 核心模型与纯函数算法（零 SDL）：\n│   ├── SceneNode \u002F Diff   #   抽象场景树节点 + reconcileScene() 虚拟树对账\n│   ├── Geometry \u002F HitTest #   几何原语、射线法、线段投影、极坐标扇区命中\n│   ├── Table \u002F TableLayout#   表格排序状态机(排索引数组 order_)、列宽测量、滚动数学\n│   └── SceneBuilder       #   纯函数构建器：buildPieChart \u002F buildSparkBar \u002F buildTable\n└── platform\u002F\n    ├── src\u002Fscene + text   # [第3层] 有状态交互组件层（零 SDL）：\n    │                      #   Widget, InputController, PieChart, SparkBar, TableWidget\n    └── src\u002Frender + web   # [第4层] 唯一接触 SDL3 的渲染后端与 Web C API 宿主\n",[14,2378,2376],{"__ignoreMap":262},[28,2380],{},[230,2382,2384,2385,1002,2387,2390],{"id":2383},"_22-core-与-scene-的职责分离纯函数构建器-轻量交互外壳","2.2 ",[14,2386,1110],{},[14,2388,2389],{},"scene"," 的职责分离：纯函数构建器 + 轻量交互外壳",[10,2392,2393,2394,2397,2398,2357,2401,2357,2404,236,2407,2410,2411,2413],{},"在架构设计中，平台交互层的 ",[14,2395,2396],{},"Widget"," 子类（如 ",[14,2399,2400],{},"TableWidget",[14,2402,2403],{},"SparkBar",[14,2405,2406],{},"PieChart",[19,2408,2409],{},"绝不直接掺杂复杂的排版与坐标算术","，而是遵循严格的“数据自持 + 布局下沉 ",[14,2412,1110],{}," + 原地定位隔离”**模式：",[257,2415,2417],{"className":259,"code":2416,"language":261,"meta":262,"style":262},"\u002F\u002F 文件：platform\u002Fsrc\u002Finclude\u002Fkiriyama\u002Fscene\u002FTableWidget.h & PieChart.cc\nclass TableWidget : public Widget {\nprivate:\n    core::TableData data_{};                  \u002F\u002F 1. 组件通过 move 语义独占持有业务数据\n    core::TableState state_{};                \u002F\u002F 2. 排序与选中状态机（只排索引 order_，不搬移 rows）\n    core::SceneNode* inner_{nullptr};         \u002F\u002F 3. 内部对账根组 (key=\"table\")\n    std::vector\u003Cstd::pair\u003Cstd::size_t, core::SceneNode*>> rowBgNodes_; \u002F\u002F 快路径指针缓存\n};\n",[14,2418,2419,2424,2442,2447,2462,2477,2498,2539],{"__ignoreMap":262},[266,2420,2421],{"class":211,"line":268},[266,2422,2423],{"class":271},"\u002F\u002F 文件：platform\u002Fsrc\u002Finclude\u002Fkiriyama\u002Fscene\u002FTableWidget.h & PieChart.cc\n",[266,2425,2426,2429,2431,2434,2437,2440],{"class":211,"line":275},[266,2427,2428],{"class":278},"class",[266,2430,814],{"class":282},[266,2432,2433],{"class":286}," : ",[266,2435,2436],{"class":278},"public",[266,2438,2439],{"class":282}," Widget",[266,2441,306],{"class":286},[266,2443,2444],{"class":211,"line":309},[266,2445,2446],{"class":278},"private:\n",[266,2448,2449,2451,2453,2456,2459],{"class":211,"line":315},[266,2450,846],{"class":282},[266,2452,508],{"class":286},[266,2454,2455],{"class":282},"TableData",[266,2457,2458],{"class":286}," data_{};",[266,2460,2461],{"class":271},"                  \u002F\u002F 1. 组件通过 move 语义独占持有业务数据\n",[266,2463,2464,2466,2468,2471,2474],{"class":211,"line":324},[266,2465,846],{"class":282},[266,2467,508],{"class":286},[266,2469,2470],{"class":282},"TableState",[266,2472,2473],{"class":286}," state_{};",[266,2475,2476],{"class":271},"                \u002F\u002F 2. 排序与选中状态机（只排索引 order_，不搬移 rows）\n",[266,2478,2479,2481,2484,2486,2489,2492,2495],{"class":211,"line":348},[266,2480,846],{"class":282},[266,2482,2483],{"class":286},"::SceneNode",[266,2485,375],{"class":278},[266,2487,2488],{"class":286}," inner_{",[266,2490,2491],{"class":691},"nullptr",[266,2493,2494],{"class":286},"};",[266,2496,2497],{"class":271},"         \u002F\u002F 3. 内部对账根组 (key=\"table\")\n",[266,2499,2500,2503,2505,2507,2509,2511,2513,2515,2517,2519,2521,2523,2525,2527,2529,2531,2533,2536],{"class":211,"line":366},[266,2501,2502],{"class":282},"    std",[266,2504,508],{"class":286},[266,2506,1548],{"class":282},[266,2508,1149],{"class":286},[266,2510,1036],{"class":282},[266,2512,508],{"class":286},[266,2514,1158],{"class":282},[266,2516,1149],{"class":286},[266,2518,1036],{"class":282},[266,2520,508],{"class":286},[266,2522,1041],{"class":278},[266,2524,1047],{"class":286},[266,2526,1110],{"class":282},[266,2528,508],{"class":286},[266,2530,788],{"class":282},[266,2532,375],{"class":278},[266,2534,2535],{"class":286},">> rowBgNodes_;",[266,2537,2538],{"class":271}," \u002F\u002F 快路径指针缓存\n",[266,2540,2541],{"class":211,"line":381},[266,2542,2543],{"class":286},"};\n",[771,2545,2547,2548,2551,2552,2554],{"id":2546},"为什么组件要拆成自身-transform_-定位-子节点-inner_-对账","为什么组件要拆成「自身 ",[14,2549,2550],{},"transform_"," 定位 + 子节点 ",[14,2553,796],{}," 对账」？",[257,2556,2558],{"className":259,"code":2557,"language":261,"meta":262,"style":262},"\u002F\u002F 文件：platform\u002Fsrc\u002Fsrc\u002FTableWidget.cc\nvoid TableWidget::onUpdate(const FrameContext& \u002F*ctx*\u002F) {\n    \u002F\u002F 将外部设置的 frame().origin() 转移到组件自身的仿射矩阵 transform_ 平移分量上，\n    \u002F\u002F 保持自身 frame_ 恒为 {0, 0, width, height}！\n    if (frame().x != 0.0 || frame().y != 0.0) {\n        setTranslation(frame().origin());\n        setFrame({0.0, 0.0, frame().width, frame().height});\n    }\n    if (layoutDirty_ || !(frame().size() == lastSize_)) {\n        rebuild(); \u002F\u002F 仅尺寸或数据变化才重排；在画布上平移组件只改 transform_，零 rebuild！\n    }\n}\n",[14,2559,2560,2564,2589,2594,2599,2628,2645,2672,2676,2704,2714,2718],{"__ignoreMap":262},[266,2561,2562],{"class":211,"line":268},[266,2563,807],{"class":271},[266,2565,2566,2568,2570,2572,2575,2577,2579,2582,2584,2587],{"class":211,"line":275},[266,2567,279],{"class":278},[266,2569,814],{"class":282},[266,2571,508],{"class":286},[266,2573,2574],{"class":282},"onUpdate",[266,2576,287],{"class":286},[266,2578,357],{"class":278},[266,2580,2581],{"class":282}," FrameContext",[266,2583,293],{"class":278},[266,2585,2586],{"class":271}," \u002F*ctx*\u002F",[266,2588,695],{"class":286},[266,2590,2591],{"class":211,"line":309},[266,2592,2593],{"class":271},"    \u002F\u002F 将外部设置的 frame().origin() 转移到组件自身的仿射矩阵 transform_ 平移分量上，\n",[266,2595,2596],{"class":211,"line":315},[266,2597,2598],{"class":271},"    \u002F\u002F 保持自身 frame_ 恒为 {0, 0, width, height}！\n",[266,2600,2601,2603,2605,2607,2610,2612,2614,2617,2619,2622,2624,2626],{"class":211,"line":324},[266,2602,667],{"class":278},[266,2604,354],{"class":286},[266,2606,874],{"class":282},[266,2608,2609],{"class":286},"().x ",[266,2611,688],{"class":278},[266,2613,1655],{"class":691},[266,2615,2616],{"class":278}," ||",[266,2618,979],{"class":282},[266,2620,2621],{"class":286},"().y ",[266,2623,688],{"class":278},[266,2625,1655],{"class":691},[266,2627,695],{"class":286},[266,2629,2630,2633,2635,2637,2639,2642],{"class":211,"line":348},[266,2631,2632],{"class":282},"        setTranslation",[266,2634,287],{"class":286},[266,2636,874],{"class":282},[266,2638,546],{"class":286},[266,2640,2641],{"class":282},"origin",[266,2643,2644],{"class":286},"());\n",[266,2646,2647,2650,2653,2656,2658,2660,2662,2664,2667,2669],{"class":211,"line":366},[266,2648,2649],{"class":282},"        setFrame",[266,2651,2652],{"class":286},"({",[266,2654,2655],{"class":691},"0.0",[266,2657,1047],{"class":286},[266,2659,2655],{"class":691},[266,2661,1047],{"class":286},[266,2663,874],{"class":282},[266,2665,2666],{"class":286},"().width, ",[266,2668,874],{"class":282},[266,2670,2671],{"class":286},"().height});\n",[266,2673,2674],{"class":211,"line":381},[266,2675,384],{"class":286},[266,2677,2678,2680,2683,2686,2689,2691,2693,2695,2697,2699,2701],{"class":211,"line":387},[266,2679,667],{"class":278},[266,2681,2682],{"class":286}," (layoutDirty_ ",[266,2684,2685],{"class":278},"||",[266,2687,2688],{"class":278}," !",[266,2690,287],{"class":286},[266,2692,874],{"class":282},[266,2694,546],{"class":286},[266,2696,879],{"class":282},[266,2698,1696],{"class":286},[266,2700,1311],{"class":278},[266,2702,2703],{"class":286}," lastSize_)) {\n",[266,2705,2706,2709,2711],{"class":211,"line":394},[266,2707,2708],{"class":282},"        rebuild",[266,2710,829],{"class":286},[266,2712,2713],{"class":271}," \u002F\u002F 仅尺寸或数据变化才重排；在画布上平移组件只改 transform_，零 rebuild！\n",[266,2715,2716],{"class":211,"line":400},[266,2717,384],{"class":286},[266,2719,2720],{"class":211,"line":413},[266,2721,745],{"class":286},[1915,2723,2724,2749],{},[1918,2725,2726,1458,2729,2732,2733,2736,2737,2740,2741,2743,2744,2746,2747,2298],{},[19,2727,2728],{},"平移零重排",[14,2730,2731],{},"core::buildTable"," 生成的节点坐标永远以 ",[14,2734,2735],{},"(0, 0)"," 为局部原点；组件在画布上的位置 ",[14,2738,2739],{},"(x, y)"," 存在 ",[14,2742,2396],{}," 自身的 ",[14,2745,2550],{}," 矩阵里，移动组件位置完全不触发 ",[14,2748,777],{},[1918,2750,2751,1458,2754,2757,2758,2760,2761,2743,2763,2357,2765,2768],{},[19,2752,2753],{},"保护根属性",[14,2755,2756],{},"reconcileScene(*inner_, *built.root)"," 只在 ",[14,2759,796],{}," 子节点上原地对账，不会覆盖 ",[14,2762,2396],{},[14,2764,2550],{},[14,2766,2767],{},"frame_"," 和事件绑定。",[28,2770],{},[230,2772,2774,2775,236],{"id":2773},"_23-虚拟场景树对账模块scenediffreconcilescene","2.3 虚拟场景树对账模块（",[14,2776,2777],{},"SceneDiff::reconcileScene",[10,2779,2780,2781,2783,2784,2790,2791,2794,2795,2798],{},"当慢路径触发 ",[14,2782,777],{}," 时，",[42,2785,2787],{"href":2786},"file:\u002F\u002F\u002FE:\u002Fkiriyama_canvas\u002Fcore\u002Fsrc\u002FSceneDiff.cc#L87-L181",[14,2788,2789],{},"SceneDiff.cc"," 将新描述树 ",[14,2792,2793],{},"source"," 对账合并到常驻树 ",[14,2796,2797],{},"target","，核心由三步构成：",[257,2800,2802],{"className":259,"code":2801,"language":261,"meta":262,"style":262},"\u002F\u002F 文件：core\u002Fsrc\u002FSceneDiff.cc\nvoid reconcileChildren(SceneNode& target, const SceneNode& source, SceneDiffResult& out) {\n    \u002F\u002F 1. O(1) 移出旧子节点数组，并建立 key -> 旧下标 哈希表（如 \"row.5\" -> 5）\n    std::vector\u003Cstd::unique_ptr\u003CSceneNode>> oldChildren = target.takeChildren();\n    std::unordered_map\u003Cstd::string_view, std::size_t> keyIndex;\n    for (std::size_t i = 0; i \u003C oldChildren.size(); ++i) {\n        if (oldChildren[i] && !oldChildren[i]->key().empty())\n            keyIndex.emplace(oldChildren[i]->key(), i);\n    }\n\n    \u002F\u002F 2. 遍历新树子节点：命中 key 且 NodeType 相同则 std::move 直接复用旧节点 unique_ptr\n    for (std::size_t i = 0; i \u003C source.childCount(); ++i) {\n        const SceneNode* src = source.childAt(i);\n        \u002F\u002F ... 查找 match 下标 ...\n        if (match != kNoMatch) {\n            reconcileNode(*oldChildren[match], *src, out); \u002F\u002F 仅当属性真变了才 markDirty()\n            if (!src->key().empty()) matchedOldIndices.push_back(match);\n            next.push_back(std::move(oldChildren[match])); \u002F\u002F 保持原节点指针与唯一 id_ 不变\n        } else {\n            auto cloned = src->clone();\n            cloned->markSubtreeDirty();\n            next.push_back(std::move(cloned));\n        }\n    }\n\n    \u002F\u002F 3. 用 O(n log n) 最长递增子序列 (LIS) 判定兄弟节点顺序是否发生移动\n    const std::size_t moves = minimumMoves(matchedOldIndices);\n    const bool structuralChange = (insertedDelta > 0 || removedDelta > 0 || moves > 0);\n    target.setChildren(std::move(next), structuralChange); \u002F\u002F 结构未变则绝不污染父节点 dirty_\n}\n",[14,2803,2804,2809,2848,2853,2887,2916,2950,2975,2991,2995,2999,3004,3038,3060,3065,3077,3097,3124,3145,3153,3171,3181,3198,3202,3206,3210,3215,3236,3274,3296],{"__ignoreMap":262},[266,2805,2806],{"class":211,"line":268},[266,2807,2808],{"class":271},"\u002F\u002F 文件：core\u002Fsrc\u002FSceneDiff.cc\n",[266,2810,2811,2813,2816,2818,2820,2822,2825,2827,2829,2831,2833,2836,2838,2841,2843,2846],{"class":211,"line":275},[266,2812,279],{"class":278},[266,2814,2815],{"class":282}," reconcileChildren",[266,2817,287],{"class":286},[266,2819,788],{"class":282},[266,2821,293],{"class":278},[266,2823,2824],{"class":296}," target",[266,2826,1047],{"class":286},[266,2828,357],{"class":278},[266,2830,1492],{"class":282},[266,2832,293],{"class":278},[266,2834,2835],{"class":296}," source",[266,2837,1047],{"class":286},[266,2839,2840],{"class":282},"SceneDiffResult",[266,2842,293],{"class":278},[266,2844,2845],{"class":296}," out",[266,2847,695],{"class":286},[266,2849,2850],{"class":211,"line":309},[266,2851,2852],{"class":271},"    \u002F\u002F 1. O(1) 移出旧子节点数组，并建立 key -> 旧下标 哈希表（如 \"row.5\" -> 5）\n",[266,2854,2855,2857,2859,2861,2863,2865,2867,2870,2872,2874,2877,2879,2882,2885],{"class":211,"line":315},[266,2856,2502],{"class":282},[266,2858,508],{"class":286},[266,2860,1548],{"class":282},[266,2862,1149],{"class":286},[266,2864,1036],{"class":282},[266,2866,508],{"class":286},[266,2868,2869],{"class":282},"unique_ptr",[266,2871,1149],{"class":286},[266,2873,788],{"class":282},[266,2875,2876],{"class":286},">> oldChildren ",[266,2878,336],{"class":278},[266,2880,2881],{"class":286}," target.",[266,2883,2884],{"class":282},"takeChildren",[266,2886,534],{"class":286},[266,2888,2889,2891,2893,2896,2898,2900,2902,2905,2907,2909,2911,2913],{"class":211,"line":324},[266,2890,2502],{"class":282},[266,2892,508],{"class":286},[266,2894,2895],{"class":282},"unordered_map",[266,2897,1149],{"class":286},[266,2899,1036],{"class":282},[266,2901,508],{"class":286},[266,2903,2904],{"class":282},"string_view",[266,2906,1047],{"class":286},[266,2908,1036],{"class":282},[266,2910,508],{"class":286},[266,2912,1041],{"class":278},[266,2914,2915],{"class":286},"> keyIndex;\n",[266,2917,2918,2920,2922,2924,2926,2928,2930,2932,2934,2936,2938,2941,2943,2946,2948],{"class":211,"line":348},[266,2919,351],{"class":278},[266,2921,354],{"class":286},[266,2923,1036],{"class":282},[266,2925,508],{"class":286},[266,2927,1041],{"class":278},[266,2929,2126],{"class":286},[266,2931,336],{"class":278},[266,2933,692],{"class":691},[266,2935,2133],{"class":286},[266,2937,1149],{"class":278},[266,2939,2940],{"class":286}," oldChildren.",[266,2942,879],{"class":282},[266,2944,2945],{"class":286},"(); ",[266,2947,2014],{"class":278},[266,2949,2147],{"class":286},[266,2951,2952,2954,2957,2959,2961,2964,2967,2969,2972],{"class":211,"line":366},[266,2953,416],{"class":278},[266,2955,2956],{"class":286}," (oldChildren[i] ",[266,2958,422],{"class":278},[266,2960,2688],{"class":278},[266,2962,2963],{"class":286},"oldChildren[i]->",[266,2965,2966],{"class":282},"key",[266,2968,546],{"class":286},[266,2970,2971],{"class":282},"empty",[266,2973,2974],{"class":286},"())\n",[266,2976,2977,2980,2983,2986,2988],{"class":211,"line":381},[266,2978,2979],{"class":286},"            keyIndex.",[266,2981,2982],{"class":282},"emplace",[266,2984,2985],{"class":286},"(oldChildren[i]->",[266,2987,2966],{"class":282},[266,2989,2990],{"class":286},"(), i);\n",[266,2992,2993],{"class":211,"line":387},[266,2994,384],{"class":286},[266,2996,2997],{"class":211,"line":394},[266,2998,391],{"emptyLinePlaceholder":390},[266,3000,3001],{"class":211,"line":400},[266,3002,3003],{"class":271},"    \u002F\u002F 2. 遍历新树子节点：命中 key 且 NodeType 相同则 std::move 直接复用旧节点 unique_ptr\n",[266,3005,3006,3008,3010,3012,3014,3016,3018,3020,3022,3024,3026,3029,3032,3034,3036],{"class":211,"line":413},[266,3007,351],{"class":278},[266,3009,354],{"class":286},[266,3011,1036],{"class":282},[266,3013,508],{"class":286},[266,3015,1041],{"class":278},[266,3017,2126],{"class":286},[266,3019,336],{"class":278},[266,3021,692],{"class":691},[266,3023,2133],{"class":286},[266,3025,1149],{"class":278},[266,3027,3028],{"class":286}," source.",[266,3030,3031],{"class":282},"childCount",[266,3033,2945],{"class":286},[266,3035,2014],{"class":278},[266,3037,2147],{"class":286},[266,3039,3040,3043,3045,3047,3050,3052,3054,3057],{"class":211,"line":440},[266,3041,3042],{"class":278},"        const",[266,3044,1492],{"class":282},[266,3046,375],{"class":278},[266,3048,3049],{"class":286}," src ",[266,3051,336],{"class":278},[266,3053,3028],{"class":286},[266,3055,3056],{"class":282},"childAt",[266,3058,3059],{"class":286},"(i);\n",[266,3061,3062],{"class":211,"line":462},[266,3063,3064],{"class":271},"        \u002F\u002F ... 查找 match 下标 ...\n",[266,3066,3067,3069,3072,3074],{"class":211,"line":484},[266,3068,416],{"class":278},[266,3070,3071],{"class":286}," (match ",[266,3073,688],{"class":278},[266,3075,3076],{"class":286}," kNoMatch) {\n",[266,3078,3079,3082,3084,3086,3089,3091,3094],{"class":211,"line":489},[266,3080,3081],{"class":282},"            reconcileNode",[266,3083,287],{"class":286},[266,3085,375],{"class":278},[266,3087,3088],{"class":286},"oldChildren[match], ",[266,3090,375],{"class":278},[266,3092,3093],{"class":286},"src, out);",[266,3095,3096],{"class":271}," \u002F\u002F 仅当属性真变了才 markDirty()\n",[266,3098,3099,3102,3104,3106,3109,3111,3113,3115,3118,3121],{"class":211,"line":494},[266,3100,3101],{"class":278},"            if",[266,3103,354],{"class":286},[266,3105,1267],{"class":278},[266,3107,3108],{"class":286},"src->",[266,3110,2966],{"class":282},[266,3112,546],{"class":286},[266,3114,2971],{"class":282},[266,3116,3117],{"class":286},"()) matchedOldIndices.",[266,3119,3120],{"class":282},"push_back",[266,3122,3123],{"class":286},"(match);\n",[266,3125,3126,3129,3131,3133,3135,3137,3139,3142],{"class":211,"line":500},[266,3127,3128],{"class":286},"            next.",[266,3130,3120],{"class":282},[266,3132,287],{"class":286},[266,3134,1036],{"class":282},[266,3136,508],{"class":286},[266,3138,897],{"class":282},[266,3140,3141],{"class":286},"(oldChildren[match]));",[266,3143,3144],{"class":271}," \u002F\u002F 保持原节点指针与唯一 id_ 不变\n",[266,3146,3147,3149,3151],{"class":211,"line":537},[266,3148,1332],{"class":286},[266,3150,1335],{"class":278},[266,3152,306],{"class":286},[266,3154,3155,3158,3161,3163,3166,3169],{"class":211,"line":555},[266,3156,3157],{"class":278},"            auto",[266,3159,3160],{"class":286}," cloned ",[266,3162,336],{"class":278},[266,3164,3165],{"class":286}," src->",[266,3167,3168],{"class":282},"clone",[266,3170,534],{"class":286},[266,3172,3173,3176,3179],{"class":211,"line":568},[266,3174,3175],{"class":286},"            cloned->",[266,3177,3178],{"class":282},"markSubtreeDirty",[266,3180,534],{"class":286},[266,3182,3183,3185,3187,3189,3191,3193,3195],{"class":211,"line":584},[266,3184,3128],{"class":286},[266,3186,3120],{"class":282},[266,3188,287],{"class":286},[266,3190,1036],{"class":282},[266,3192,508],{"class":286},[266,3194,897],{"class":282},[266,3196,3197],{"class":286},"(cloned));\n",[266,3199,3200],{"class":211,"line":606},[266,3201,631],{"class":286},[266,3203,3204],{"class":211,"line":628},[266,3205,384],{"class":286},[266,3207,3208],{"class":211,"line":634},[266,3209,391],{"emptyLinePlaceholder":390},[266,3211,3212],{"class":211,"line":640},[266,3213,3214],{"class":271},"    \u002F\u002F 3. 用 O(n log n) 最长递增子序列 (LIS) 判定兄弟节点顺序是否发生移动\n",[266,3216,3217,3219,3221,3223,3225,3228,3230,3233],{"class":211,"line":645},[266,3218,327],{"class":278},[266,3220,892],{"class":282},[266,3222,508],{"class":286},[266,3224,1041],{"class":278},[266,3226,3227],{"class":286}," moves ",[266,3229,336],{"class":278},[266,3231,3232],{"class":282}," minimumMoves",[266,3234,3235],{"class":286},"(matchedOldIndices);\n",[266,3237,3238,3240,3242,3245,3247,3250,3252,3254,3256,3259,3261,3263,3265,3267,3269,3271],{"class":211,"line":650},[266,3239,327],{"class":278},[266,3241,1803],{"class":278},[266,3243,3244],{"class":286}," structuralChange ",[266,3246,336],{"class":278},[266,3248,3249],{"class":286}," (insertedDelta ",[266,3251,1560],{"class":278},[266,3253,692],{"class":691},[266,3255,2616],{"class":278},[266,3257,3258],{"class":286}," removedDelta ",[266,3260,1560],{"class":278},[266,3262,692],{"class":691},[266,3264,2616],{"class":278},[266,3266,3227],{"class":286},[266,3268,1560],{"class":278},[266,3270,692],{"class":691},[266,3272,3273],{"class":286},");\n",[266,3275,3276,3279,3282,3284,3286,3288,3290,3293],{"class":211,"line":656},[266,3277,3278],{"class":286},"    target.",[266,3280,3281],{"class":282},"setChildren",[266,3283,287],{"class":286},[266,3285,1036],{"class":282},[266,3287,508],{"class":286},[266,3289,897],{"class":282},[266,3291,3292],{"class":286},"(next), structuralChange);",[266,3294,3295],{"class":271}," \u002F\u002F 结构未变则绝不污染父节点 dirty_\n",[266,3297,3298],{"class":211,"line":664},[266,3299,745],{"class":286},[10,3301,3302,3303,3309,3310,3313],{},"其中 ",[42,3304,3306],{"href":3305},"file:\u002F\u002F\u002FE:\u002Fkiriyama_canvas\u002Fcore\u002Fsrc\u002FSceneDiff.cc#L46-L61",[14,3307,3308],{},"minimumMoves"," 利用 ",[14,3311,3312],{},"std::lower_bound"," 维护单调栈求最长递增子序列长度 $\\text{LIS}$，最小节点移动次数即为 $N_{\\text{matched}} - |\\text{LIS}|$。",[28,3315],{},[31,3317,3319],{"id":3318},"第三章点击测试hit-testing与交互模块的数学实现","第三章：点击测试（Hit-Testing）与交互模块的数学实现",[10,3321,3322,3323,2357,3326,1002,3329,3332],{},"本节深入拆解 ",[14,3324,3325],{},"math\u002F",[14,3327,3328],{},"core\u002F",[14,3330,3331],{},"scene\u002F"," 中支撑精准点击测试与丝滑交互的底层数学公式及代码实现。",[28,3334],{},[230,3336,3338],{"id":3337},"_31-空间降维2d-仿射逆变换与奇异矩阵防-nan-保护","3.1 空间降维：2D 仿射逆变换与奇异矩阵防 NaN 保护",[771,3340,3342],{"id":3341},"_1-仿射矩阵表示与世界-局部逆映射","1. 仿射矩阵表示与世界-局部逆映射",[10,3344,3345,3351],{},[42,3346,3348],{"href":3347},"file:\u002F\u002F\u002FE:\u002Fkiriyama_canvas\u002Fmath\u002Finclude\u002Fkiriyama\u002Fmath\u002FTransform2D.h#L16-L95",[14,3349,3350],{},"Affine2D"," 采用齐次坐标前两行的 $3 \\times 2$ 矩阵存储：",[10,3353,3354],{},"$$\nM = \\begin{bmatrix} m_{00} & m_{01} & m_{02} \\ m_{10} & m_{11} & m_{12} \\ 0 & 0 & 1 \\end{bmatrix}, \\quad\nM \\begin{bmatrix} x \\ y \\ 1 \\end{bmatrix} = \\begin{bmatrix} m_{00}x + m_{01}y + m_{02} \\ m_{10}x + m_{11}y + m_{12} \\ 1 \\end{bmatrix}\n$$",[10,3356,3357,3358,3361,3362,3366],{},"对于场景树中任意深度为 $k$ 的节点，其世界变换矩阵为祖先链累乘 $M_{\\text{world}} = M_0 M_1 \\cdots M_k$。在做点击测试时，",[19,3359,3360],{},"绝不把节点内部成百上千个多边形顶点乘到世界空间","，而是将世界空间的鼠标点 $\\mathbf{p}",[3363,3364,3365],"em",{},"w$ 乘以逆矩阵 $M","{\\text{world}}^{-1}$ 拉回节点的局部空间：",[10,3368,3369,3370,3373],{},"$$\n\\mathbf{p}",[3363,3371,3372],{},"{\\text{local}} = M","{\\text{world}}^{-1} , \\mathbf{p}_w\n$$",[10,3375,3376,3377,3380],{},"对于线性部分 $A = \\begin{bmatrix} m_{00} & m_{01} \\ m_{10} & m_{11} \\end{bmatrix}$ 与平移向量 $\\mathbf{t} = \\begin{bmatrix} m_{02} \\ m_{12} \\end{bmatrix}$，行列式 $\\det(A) = m_{00}m_{11} - m_{01}m_{10}$，其逆矩阵为 $",[266,3378,3379],{},"A^{-1} \\mid -A^{-1}\\mathbf{t}","$：",[10,3382,3383],{},"$$\nM^{-1} = \\frac{1}{\\det(A)} \\begin{bmatrix} m_{11} & -m_{01} & m_{01}m_{12} - m_{11}m_{02} \\ -m_{10} & m_{00} & m_{10}m_{02} - m_{00}m_{12} \\ 0 & 0 & \\det(A) \\end{bmatrix}\n$$",[257,3385,3387],{"className":259,"code":3386,"language":261,"meta":262,"style":262},"\u002F\u002F 文件：math\u002Finclude\u002Fkiriyama\u002Fmath\u002FTransform2D.h\nconstexpr Affine2D inverse(double epsilon = kEpsilon) const noexcept {\n    const double det = determinant(); \u002F\u002F m00 * m11 - m01 * m10\n    if (!std::isfinite(det) || std::abs(det) \u003C= epsilon) {\n        \u002F\u002F ★ 防 NaN 保护：当组件处于缩放动画第 0 帧 (scale = 0) 时 det == 0，\n        \u002F\u002F 严禁除以 0 产生 NaN 污染后续所有几何比较，安全回退为单位矩阵 Identity()\n        return Identity();\n    }\n    const double inv = 1.0 \u002F det;\n    return {\n        m11 * inv,\n        -m01 * inv,\n        (m01 * m12 - m11 * m02) * inv,\n        -m10 * inv,\n        m00 * inv,\n        (m10 * m02 - m00 * m12) * inv,\n    };\n}\n",[14,3388,3389,3394,3424,3443,3477,3482,3487,3497,3501,3521,3528,3538,3550,3575,3586,3595,3619,3624],{"__ignoreMap":262},[266,3390,3391],{"class":211,"line":268},[266,3392,3393],{"class":271},"\u002F\u002F 文件：math\u002Finclude\u002Fkiriyama\u002Fmath\u002FTransform2D.h\n",[266,3395,3396,3399,3401,3404,3406,3408,3411,3414,3417,3419,3422],{"class":211,"line":275},[266,3397,3398],{"class":278},"constexpr",[266,3400,1504],{"class":282},[266,3402,3403],{"class":282}," inverse",[266,3405,287],{"class":286},[266,3407,1050],{"class":278},[266,3409,3410],{"class":296}," epsilon",[266,3412,3413],{"class":278}," =",[266,3415,3416],{"class":286}," kEpsilon) ",[266,3418,357],{"class":278},[266,3420,3421],{"class":278}," noexcept",[266,3423,306],{"class":286},[266,3425,3426,3428,3430,3433,3435,3438,3440],{"class":211,"line":309},[266,3427,327],{"class":278},[266,3429,1629],{"class":278},[266,3431,3432],{"class":286}," det ",[266,3434,336],{"class":278},[266,3436,3437],{"class":282}," determinant",[266,3439,829],{"class":286},[266,3441,3442],{"class":271}," \u002F\u002F m00 * m11 - m01 * m10\n",[266,3444,3445,3447,3449,3451,3453,3455,3458,3461,3463,3465,3467,3470,3472,3474],{"class":211,"line":315},[266,3446,667],{"class":278},[266,3448,354],{"class":286},[266,3450,1267],{"class":278},[266,3452,1036],{"class":282},[266,3454,508],{"class":286},[266,3456,3457],{"class":282},"isfinite",[266,3459,3460],{"class":286},"(det) ",[266,3462,2685],{"class":278},[266,3464,892],{"class":282},[266,3466,508],{"class":286},[266,3468,3469],{"class":282},"abs",[266,3471,3460],{"class":286},[266,3473,1652],{"class":278},[266,3475,3476],{"class":286}," epsilon) {\n",[266,3478,3479],{"class":211,"line":324},[266,3480,3481],{"class":271},"        \u002F\u002F ★ 防 NaN 保护：当组件处于缩放动画第 0 帧 (scale = 0) 时 det == 0，\n",[266,3483,3484],{"class":211,"line":348},[266,3485,3486],{"class":271},"        \u002F\u002F 严禁除以 0 产生 NaN 污染后续所有几何比较，安全回退为单位矩阵 Identity()\n",[266,3488,3489,3492,3495],{"class":211,"line":366},[266,3490,3491],{"class":278},"        return",[266,3493,3494],{"class":282}," Identity",[266,3496,534],{"class":286},[266,3498,3499],{"class":211,"line":381},[266,3500,384],{"class":286},[266,3502,3503,3505,3507,3510,3512,3515,3518],{"class":211,"line":387},[266,3504,327],{"class":278},[266,3506,1629],{"class":278},[266,3508,3509],{"class":286}," inv ",[266,3511,336],{"class":278},[266,3513,3514],{"class":691}," 1.0",[266,3516,3517],{"class":278}," \u002F",[266,3519,3520],{"class":286}," det;\n",[266,3522,3523,3526],{"class":211,"line":394},[266,3524,3525],{"class":278},"    return",[266,3527,306],{"class":286},[266,3529,3530,3533,3535],{"class":211,"line":400},[266,3531,3532],{"class":282},"        m11",[266,3534,1305],{"class":278},[266,3536,3537],{"class":286}," inv,\n",[266,3539,3540,3543,3546,3548],{"class":211,"line":413},[266,3541,3542],{"class":278},"        -",[266,3544,3545],{"class":286},"m01 ",[266,3547,375],{"class":278},[266,3549,3537],{"class":286},[266,3551,3552,3555,3557,3560,3563,3566,3568,3571,3573],{"class":211,"line":440},[266,3553,3554],{"class":286},"        (m01 ",[266,3556,375],{"class":278},[266,3558,3559],{"class":286}," m12 ",[266,3561,3562],{"class":278},"-",[266,3564,3565],{"class":286}," m11 ",[266,3567,375],{"class":278},[266,3569,3570],{"class":286}," m02) ",[266,3572,375],{"class":278},[266,3574,3537],{"class":286},[266,3576,3577,3579,3582,3584],{"class":211,"line":462},[266,3578,3542],{"class":278},[266,3580,3581],{"class":286},"m10 ",[266,3583,375],{"class":278},[266,3585,3537],{"class":286},[266,3587,3588,3591,3593],{"class":211,"line":484},[266,3589,3590],{"class":286},"        m00 ",[266,3592,375],{"class":278},[266,3594,3537],{"class":286},[266,3596,3597,3600,3602,3605,3607,3610,3612,3615,3617],{"class":211,"line":489},[266,3598,3599],{"class":286},"        (m10 ",[266,3601,375],{"class":278},[266,3603,3604],{"class":286}," m02 ",[266,3606,3562],{"class":278},[266,3608,3609],{"class":286}," m00 ",[266,3611,375],{"class":278},[266,3613,3614],{"class":286}," m12) ",[266,3616,375],{"class":278},[266,3618,3537],{"class":286},[266,3620,3621],{"class":211,"line":494},[266,3622,3623],{"class":286},"    };\n",[266,3625,3626],{"class":211,"line":500},[266,3627,745],{"class":286},[771,3629,3631,3632,236],{"id":3630},"_2-局部退化线段的旋转包围盒计算transformedbounds","2. 局部退化线段的旋转包围盒计算（",[14,3633,3634],{},"transformedBounds",[10,3636,3637,3638,3641,3642,3645,3646,3649,3650,3656,3657,3660],{},"当计算节点在世界空间的轴对齐包围盒（AABB）用于粗筛时，若一个节点是水平或垂直线段（",[14,3639,3640],{},"width == 0"," 或 ",[14,3643,3644],{},"height == 0","），标准 ",[14,3647,3648],{},"RectD::isEmpty()"," 会误判为空。",[42,3651,3653],{"href":3652},"file:\u002F\u002F\u002FE:\u002Fkiriyama_canvas\u002Fcore\u002Fsrc\u002FGeometry.cc#L45-L64",[14,3654,3655],{},"Geometry.cc"," 专门允许单轴为 0 的退化矩形参与四顶点极值包络计算，确保水平\u002F垂直网格线在叠加 ",[14,3658,3659],{},"tolerance"," 容差后仍能被正确命中：",[257,3662,3664],{"className":259,"code":3663,"language":261,"meta":262,"style":262},"\u002F\u002F 文件：core\u002Fsrc\u002FGeometry.cc\nRectD transformedBounds(const RectD& rect, const Affine2D& transform) noexcept {\n    if (rect.width \u003C 0.0 || rect.height \u003C 0.0 ||\n        (rect.width == 0.0 && rect.height == 0.0)) {\n        return {}; \u002F\u002F 仅拒绝负尺寸或退化为单个点的空盒，保留 width==0 或 height==0 的线段！\n    }\n    const std::array\u003CVec2d, 4> corners = {\n        transform.apply({rect.x, rect.y}),\n        transform.apply({rect.right(), rect.y}),\n        transform.apply({rect.right(), rect.bottom()}),\n        transform.apply({rect.x, rect.bottom()}),\n    };\n    return boundsOfPoints(corners); \u002F\u002F 取 4 个变换后角点的 [minX, minY, maxX - minX, maxY - minY]\n}\n",[14,3665,3666,3671,3709,3732,3753,3763,3767,3795,3806,3821,3840,3853,3857,3870],{"__ignoreMap":262},[266,3667,3668],{"class":211,"line":268},[266,3669,3670],{"class":271},"\u002F\u002F 文件：core\u002Fsrc\u002FGeometry.cc\n",[266,3672,3673,3676,3679,3681,3683,3686,3688,3691,3693,3695,3697,3699,3702,3704,3707],{"class":211,"line":275},[266,3674,3675],{"class":282},"RectD",[266,3677,3678],{"class":282}," transformedBounds",[266,3680,287],{"class":286},[266,3682,357],{"class":278},[266,3684,3685],{"class":282}," RectD",[266,3687,293],{"class":278},[266,3689,3690],{"class":296}," rect",[266,3692,1047],{"class":286},[266,3694,357],{"class":278},[266,3696,1504],{"class":282},[266,3698,293],{"class":278},[266,3700,3701],{"class":296}," transform",[266,3703,300],{"class":286},[266,3705,3706],{"class":278},"noexcept",[266,3708,306],{"class":286},[266,3710,3711,3713,3716,3718,3720,3722,3725,3727,3729],{"class":211,"line":309},[266,3712,667],{"class":278},[266,3714,3715],{"class":286}," (rect.width ",[266,3717,1149],{"class":278},[266,3719,1655],{"class":691},[266,3721,2616],{"class":278},[266,3723,3724],{"class":286}," rect.height ",[266,3726,1149],{"class":278},[266,3728,1655],{"class":691},[266,3730,3731],{"class":278}," ||\n",[266,3733,3734,3737,3739,3741,3744,3746,3748,3750],{"class":211,"line":315},[266,3735,3736],{"class":286},"        (rect.width ",[266,3738,1311],{"class":278},[266,3740,1655],{"class":691},[266,3742,3743],{"class":278}," &&",[266,3745,3724],{"class":286},[266,3747,1311],{"class":278},[266,3749,1655],{"class":691},[266,3751,3752],{"class":286},")) {\n",[266,3754,3755,3757,3760],{"class":211,"line":324},[266,3756,3491],{"class":278},[266,3758,3759],{"class":286}," {};",[266,3761,3762],{"class":271}," \u002F\u002F 仅拒绝负尺寸或退化为单个点的空盒，保留 width==0 或 height==0 的线段！\n",[266,3764,3765],{"class":211,"line":348},[266,3766,384],{"class":286},[266,3768,3769,3771,3773,3775,3778,3780,3783,3785,3788,3791,3793],{"class":211,"line":366},[266,3770,327],{"class":278},[266,3772,892],{"class":282},[266,3774,508],{"class":286},[266,3776,3777],{"class":282},"array",[266,3779,1149],{"class":286},[266,3781,3782],{"class":282},"Vec2d",[266,3784,1047],{"class":286},[266,3786,3787],{"class":691},"4",[266,3789,3790],{"class":286},"> corners ",[266,3792,336],{"class":278},[266,3794,306],{"class":286},[266,3796,3797,3800,3803],{"class":211,"line":381},[266,3798,3799],{"class":286},"        transform.",[266,3801,3802],{"class":282},"apply",[266,3804,3805],{"class":286},"({rect.x, rect.y}),\n",[266,3807,3808,3810,3812,3815,3818],{"class":211,"line":387},[266,3809,3799],{"class":286},[266,3811,3802],{"class":282},[266,3813,3814],{"class":286},"({rect.",[266,3816,3817],{"class":282},"right",[266,3819,3820],{"class":286},"(), rect.y}),\n",[266,3822,3823,3825,3827,3829,3831,3834,3837],{"class":211,"line":394},[266,3824,3799],{"class":286},[266,3826,3802],{"class":282},[266,3828,3814],{"class":286},[266,3830,3817],{"class":282},[266,3832,3833],{"class":286},"(), rect.",[266,3835,3836],{"class":282},"bottom",[266,3838,3839],{"class":286},"()}),\n",[266,3841,3842,3844,3846,3849,3851],{"class":211,"line":400},[266,3843,3799],{"class":286},[266,3845,3802],{"class":282},[266,3847,3848],{"class":286},"({rect.x, rect.",[266,3850,3836],{"class":282},[266,3852,3839],{"class":286},[266,3854,3855],{"class":211,"line":413},[266,3856,3623],{"class":286},[266,3858,3859,3861,3864,3867],{"class":211,"line":440},[266,3860,3525],{"class":278},[266,3862,3863],{"class":282}," boundsOfPoints",[266,3865,3866],{"class":286},"(corners);",[266,3868,3869],{"class":271}," \u002F\u002F 取 4 个变换后角点的 [minX, minY, maxX - minX, maxY - minY]\n",[266,3871,3872],{"class":211,"line":462},[266,3873,745],{"class":286},[28,3875],{},[230,3877,3879],{"id":3878},"_32-点击测试hit-testing的五大几何数学实现","3.2 点击测试（Hit-Testing）的五大几何数学实现",[50,3881,3882],{"align":52},[54,3883,61,3886,61,61,3890,61,3899,61,3905,61,3912,61,3916,61,3922,61,3924,61,3929,61,3934,61,3940,61,3944,61,3947,61,3952,61,3958,61,3962,61,61,3965,61,3968,61,3972,61,3977,61,3981,61,3986,61,3990,61,3995,61,3998,61,4003,61,4007,61,4011,61,61,4014,61,4017,61,4021,61,4026,61,4030,61,4034,61,4038,61,4043,61,4046,61,4049,61,4053,61,4056,61,4059],{"viewBox":3884,"width":57,"height":58,"xmlns":59,"style":3885},"0 0 840 290","max-width: 840px; font-family: system-ui, -apple-system, sans-serif;",[81,3887],{"width":3888,"height":3889,"rx":85,"fill":86,"stroke":87,"stroke-width":88},"840","290",[81,3891],{"x":3892,"y":3893,"width":3894,"height":3895,"rx":95,"fill":3896,"stroke":3897,"stroke-width":3898},"20","18","255","254","#ffffff","#cbd5e1","1.2",[99,3900,3904],{"x":3901,"y":3902,"fill":3903,"text-anchor":104,"font-size":105,"font-weight":106},"147","42","#0f172a","1. 线段\u002F折线：正交投影参数夹逼",[211,3906],{"x1":3907,"y1":3908,"x2":3909,"y2":3910,"stroke":171,"stroke-width":3911},"55","185","225","90","3",[3913,3914],"circle",{"cx":3907,"cy":3908,"r":3915,"fill":186},"4.5",[99,3917,3921],{"x":3902,"y":3918,"fill":3919,"font-size":112,"font-weight":3920},"203","#1e293b","600","A",[3913,3923],{"cx":3909,"cy":3910,"r":3915,"fill":186},[99,3925,3928],{"x":3926,"y":3927,"fill":3919,"font-size":112,"font-weight":3920},"232","94","B",[3913,3930],{"cx":3931,"cy":3932,"r":3915,"fill":3933},"115","85","#ef4444",[99,3935,3939],{"x":3936,"y":3937,"fill":3938,"font-size":112,"font-weight":3920},"108","73","#dc2626","P (鼠标)",[211,3941],{"x1":3931,"y1":3932,"x2":93,"y2":3942,"stroke":3933,"stroke-width":88,"stroke-dasharray":3943},"135",[3787,3911],[3913,3945],{"cx":93,"cy":3942,"r":3787,"fill":3946},"#10b981",[99,3948,3951],{"x":127,"y":3949,"fill":3950,"font-size":112,"font-weight":3920},"146","#047857","Q = A + t(B-A)",[99,3953,3957],{"x":3954,"y":3955,"fill":3956,"font-size":112},"34","226","#334155","t = clamp((P-A)·(B-A)\u002F|B-A|², 0, 1)",[99,3959,3961],{"x":3954,"y":3960,"fill":3956,"font-size":112},"245","最短距离 d = |P - Q| ≤ tolerance",[99,3963,3964],{"x":3954,"y":132,"fill":79,"font-size":122},"折线命中后通过 nearestEndpoint 吸附端点",[81,3966],{"x":3967,"y":3893,"width":3894,"height":3895,"rx":95,"fill":3896,"stroke":3897,"stroke-width":3898},"292",[99,3969,3971],{"x":3970,"y":3902,"fill":3903,"text-anchor":104,"font-size":105,"font-weight":106},"420","2. 扇环：极坐标 + 模 2π 角度折回",[76,3973],{"d":3974,"fill":170,"stroke":3975,"stroke-width":3976},"M 380 142 L 435 78 A 85 85 0 0 1 495 168 L 425 153 A 35 35 0 0 0 403 116 Z","#2563eb","2",[3913,3978],{"cx":3979,"cy":3980,"r":3911,"fill":3903},"380","142",[99,3982,3985],{"x":3983,"y":3984,"fill":3956,"font-size":122},"352","157","C(圆心)",[3913,3987],{"cx":3988,"cy":3989,"r":3915,"fill":3933},"452","130",[99,3991,3994],{"x":3992,"y":3993,"fill":3938,"font-size":112,"font-weight":3920},"460","133","P",[211,3996],{"x1":3979,"y1":3980,"x2":3988,"y2":3989,"stroke":3933,"stroke-width":3898,"stroke-dasharray":3997},[3911,3976],[99,3999,4002],{"x":4000,"y":4001,"fill":3956,"font-size":112},"306","210","极径：R_in - ε ≤ |P - C| ≤ R_out + ε",[99,4004,4006],{"x":4000,"y":4005,"fill":3956,"font-size":112},"229","极角：θ = atan2(Py-Cy, Px-Cx)",[99,4008,4010],{"x":4000,"y":4009,"fill":3956,"font-size":112},"248","折回：wrap_[0,2π)(θ - θ_start) ≤ sweep",[99,4012,4013],{"x":4000,"y":132,"fill":79,"font-size":122},"外扩动画期动态补偿 R_out += explode·p",[81,4015],{"x":4016,"y":3893,"width":3894,"height":3895,"rx":95,"fill":3896,"stroke":3897,"stroke-width":3898},"565",[99,4018,4020],{"x":4019,"y":3902,"fill":3903,"text-anchor":104,"font-size":105,"font-weight":106},"692","3. 任意多边形：半开区间奇偶射线法",[4022,4023],"polygon",{"points":4024,"fill":192,"stroke":4025,"stroke-width":3976},"595,98 665,73 650,133 725,83 765,163 620,183","#16a34a",[3913,4027],{"cx":4028,"cy":4029,"r":3915,"fill":3933},"628","128",[99,4031,3994],{"x":4032,"y":4033,"fill":3938,"font-size":112,"font-weight":3920},"605","121",[211,4035],{"x1":4028,"y1":4029,"x2":4036,"y2":4029,"stroke":3933,"stroke-width":88,"stroke-dasharray":4037},"795",[3787,3911],[3913,4039],{"cx":4040,"cy":4029,"r":4041,"fill":4042},"653","3.5","#9333ea",[3913,4044],{"cx":4045,"cy":4029,"r":4041,"fill":4042},"660",[3913,4047],{"cx":4048,"cy":4029,"r":4041,"fill":4042},"747",[99,4050,4052],{"x":4051,"y":4001,"fill":3956,"font-size":112},"580","向右发射水平射线 y = Py, x ≥ Px：",[99,4054,4055],{"x":4051,"y":4005,"fill":3956,"font-size":112},"• 异或判跨立：(Ay > Py) != (By > Py)",[99,4057,4058],{"x":4051,"y":4009,"fill":3956,"font-size":112},"• 交点 X > Px 则翻转 inside = !inside",[99,4060,4061],{"x":4051,"y":132,"fill":79,"font-size":122},"天然免疫水平边与顶点双计问题",[771,4063,4065,4066,4072,4073,236],{"id":4064},"_1-线段与折线命中向量正交投影与端点吸附geometryh-hittestcc","1. 线段与折线命中：向量正交投影与端点吸附（",[42,4067,4069],{"href":4068},"file:\u002F\u002F\u002FE:\u002Fkiriyama_canvas\u002Fcore\u002Finclude\u002Fkiriyama\u002Fcore\u002FGeometry.h#L138-L152",[14,4070,4071],{},"Geometry.h"," & ",[42,4074,4076],{"href":4075},"file:\u002F\u002F\u002FE:\u002Fkiriyama_canvas\u002Fcore\u002Fsrc\u002FHitTest.cc#L25-L75",[14,4077,4078],{},"HitTest.cc",[10,4080,4081],{},"给定鼠标点 $\\mathbf{p}$ 与线段端点 $\\mathbf{a}, \\mathbf{b}$，令方向向量 $\\mathbf{v} = \\mathbf{b} - \\mathbf{a}$。点 $\\mathbf{p}$ 在直线 $\\mathbf{a} + t\\mathbf{v}$ 上的正交投影参数为：",[10,4083,4084],{},"$$\nt^* = \\frac{(\\mathbf{p} - \\mathbf{a}) \\cdot \\mathbf{v}}{|\\mathbf{v}|^2}\n$$",[10,4086,4087,4088,4095],{},"将 $t^",[3363,4089,4090,4091,4094],{},"$ 钳制到闭区间 $",[266,4092,4093],{},"0, 1","$ 得到线段上的最近点 $\\mathbf{q} = \\mathbf{a} + \\operatorname{saturate}(t^","),\\mathbf{v}$：",[2266,4097,4098,4101,4104],{},[1918,4099,4100],{},"当 $t^* \u003C 0$ 时，最近点自动退化为左端点 $\\mathbf{a}$；",[1918,4102,4103],{},"当 $t^* > 1$ 时，最近点自动退化为右端点 $\\mathbf{b}$；",[1918,4105,4106],{},"当 $0 \\le t^* \\le 1$ 时，最近点即为正交垂足。",[10,4108,4109,4110,4115,4116,4119],{},"折线图 ",[42,4111,4112],{"href":4075},[14,4113,4114],{},"hitTestPolyline"," 遍历所有有效线段（自动跳过包含 ",[14,4117,4118],{},"NaN"," 的断连缺口），找出距离最小且满足 $|\\mathbf{p} - \\mathbf{q}| \\le \\tau$ 的线段后，再通过比较 $|\\mathbf{p} - \\mathbf{a}|^2$ 与 $|\\mathbf{p} - \\mathbf{b}|^2$ 将命中索引吸附到更近的数据点端点：",[257,4121,4123],{"className":259,"code":4122,"language":261,"meta":262,"style":262},"\u002F\u002F 文件：core\u002Finclude\u002Fkiriyama\u002Fcore\u002FGeometry.h & core\u002Fsrc\u002FHitTest.cc\ninline Vec2d closestPointOnSegment(const Vec2d& point, const Vec2d& a, const Vec2d& b) noexcept {\n    const Vec2d ab = b - a;\n    const double lengthSquared = ab.lengthSquared();\n    if (!(lengthSquared > 0.0)) return a;\n    const double t = math::saturate((point - a).dot(ab) \u002F lengthSquared);\n    return a + ab * t;\n}\n\nstd::size_t nearestEndpoint(const Vec2d& point, std::span\u003Cconst Vec2d> points, std::size_t segIdx) noexcept {\n    const double distA = (point - points[segIdx]).lengthSquared();\n    const double distB = (point - points[segIdx + 1]).lengthSquared();\n    return (distB \u003C distA) ? (segIdx + 1) : segIdx;\n}\n",[14,4124,4125,4130,4180,4199,4218,4240,4279,4295,4299,4303,4360,4383,4412,4440],{"__ignoreMap":262},[266,4126,4127],{"class":211,"line":268},[266,4128,4129],{"class":271},"\u002F\u002F 文件：core\u002Finclude\u002Fkiriyama\u002Fcore\u002FGeometry.h & core\u002Fsrc\u002FHitTest.cc\n",[266,4131,4132,4135,4138,4141,4143,4145,4147,4149,4152,4154,4156,4158,4160,4163,4165,4167,4169,4171,4174,4176,4178],{"class":211,"line":275},[266,4133,4134],{"class":278},"inline",[266,4136,4137],{"class":282}," Vec2d",[266,4139,4140],{"class":282}," closestPointOnSegment",[266,4142,287],{"class":286},[266,4144,357],{"class":278},[266,4146,4137],{"class":282},[266,4148,293],{"class":278},[266,4150,4151],{"class":296}," point",[266,4153,1047],{"class":286},[266,4155,357],{"class":278},[266,4157,4137],{"class":282},[266,4159,293],{"class":278},[266,4161,4162],{"class":296}," a",[266,4164,1047],{"class":286},[266,4166,357],{"class":278},[266,4168,4137],{"class":282},[266,4170,293],{"class":278},[266,4172,4173],{"class":296}," b",[266,4175,300],{"class":286},[266,4177,3706],{"class":278},[266,4179,306],{"class":286},[266,4181,4182,4184,4186,4189,4191,4194,4196],{"class":211,"line":309},[266,4183,327],{"class":278},[266,4185,4137],{"class":282},[266,4187,4188],{"class":286}," ab ",[266,4190,336],{"class":278},[266,4192,4193],{"class":286}," b ",[266,4195,3562],{"class":278},[266,4197,4198],{"class":286}," a;\n",[266,4200,4201,4203,4205,4208,4210,4213,4216],{"class":211,"line":315},[266,4202,327],{"class":278},[266,4204,1629],{"class":278},[266,4206,4207],{"class":286}," lengthSquared ",[266,4209,336],{"class":278},[266,4211,4212],{"class":286}," ab.",[266,4214,4215],{"class":282},"lengthSquared",[266,4217,534],{"class":286},[266,4219,4220,4222,4224,4226,4229,4231,4233,4236,4238],{"class":211,"line":324},[266,4221,667],{"class":278},[266,4223,354],{"class":286},[266,4225,1267],{"class":278},[266,4227,4228],{"class":286},"(lengthSquared ",[266,4230,1560],{"class":278},[266,4232,1655],{"class":691},[266,4234,4235],{"class":286},")) ",[266,4237,1586],{"class":278},[266,4239,4198],{"class":286},[266,4241,4242,4244,4246,4249,4251,4254,4256,4259,4262,4264,4267,4270,4273,4276],{"class":211,"line":348},[266,4243,327],{"class":278},[266,4245,1629],{"class":278},[266,4247,4248],{"class":286}," t ",[266,4250,336],{"class":278},[266,4252,4253],{"class":282}," math",[266,4255,508],{"class":286},[266,4257,4258],{"class":282},"saturate",[266,4260,4261],{"class":286},"((point ",[266,4263,3562],{"class":278},[266,4265,4266],{"class":286}," a).",[266,4268,4269],{"class":282},"dot",[266,4271,4272],{"class":286},"(ab) ",[266,4274,4275],{"class":278},"\u002F",[266,4277,4278],{"class":286}," lengthSquared);\n",[266,4280,4281,4283,4286,4288,4290,4292],{"class":211,"line":366},[266,4282,3525],{"class":278},[266,4284,4285],{"class":286}," a ",[266,4287,2037],{"class":278},[266,4289,4188],{"class":286},[266,4291,375],{"class":278},[266,4293,4294],{"class":286}," t;\n",[266,4296,4297],{"class":211,"line":381},[266,4298,745],{"class":286},[266,4300,4301],{"class":211,"line":387},[266,4302,391],{"emptyLinePlaceholder":390},[266,4304,4305,4307,4309,4311,4314,4316,4318,4320,4322,4324,4326,4328,4330,4332,4334,4336,4338,4340,4343,4345,4347,4349,4351,4354,4356,4358],{"class":211,"line":394},[266,4306,1036],{"class":282},[266,4308,508],{"class":286},[266,4310,1041],{"class":278},[266,4312,4313],{"class":282}," nearestEndpoint",[266,4315,287],{"class":286},[266,4317,357],{"class":278},[266,4319,4137],{"class":282},[266,4321,293],{"class":278},[266,4323,4151],{"class":296},[266,4325,1047],{"class":286},[266,4327,1036],{"class":282},[266,4329,508],{"class":286},[266,4331,266],{"class":282},[266,4333,1149],{"class":286},[266,4335,357],{"class":278},[266,4337,4137],{"class":282},[266,4339,1202],{"class":286},[266,4341,4342],{"class":296},"points",[266,4344,1047],{"class":286},[266,4346,1036],{"class":282},[266,4348,508],{"class":286},[266,4350,1041],{"class":278},[266,4352,4353],{"class":296}," segIdx",[266,4355,300],{"class":286},[266,4357,3706],{"class":278},[266,4359,306],{"class":286},[266,4361,4362,4364,4366,4369,4371,4374,4376,4379,4381],{"class":211,"line":400},[266,4363,327],{"class":278},[266,4365,1629],{"class":278},[266,4367,4368],{"class":286}," distA ",[266,4370,336],{"class":278},[266,4372,4373],{"class":286}," (point ",[266,4375,3562],{"class":278},[266,4377,4378],{"class":286}," points[segIdx]).",[266,4380,4215],{"class":282},[266,4382,534],{"class":286},[266,4384,4385,4387,4389,4392,4394,4396,4398,4401,4403,4405,4408,4410],{"class":211,"line":413},[266,4386,327],{"class":278},[266,4388,1629],{"class":278},[266,4390,4391],{"class":286}," distB ",[266,4393,336],{"class":278},[266,4395,4373],{"class":286},[266,4397,3562],{"class":278},[266,4399,4400],{"class":286}," points[segIdx ",[266,4402,2037],{"class":278},[266,4404,1393],{"class":691},[266,4406,4407],{"class":286},"]).",[266,4409,4215],{"class":282},[266,4411,534],{"class":286},[266,4413,4414,4416,4419,4421,4424,4426,4429,4431,4433,4435,4437],{"class":211,"line":440},[266,4415,3525],{"class":278},[266,4417,4418],{"class":286}," (distB ",[266,4420,1149],{"class":278},[266,4422,4423],{"class":286}," distA) ",[266,4425,1398],{"class":278},[266,4427,4428],{"class":286}," (segIdx ",[266,4430,2037],{"class":278},[266,4432,1393],{"class":691},[266,4434,300],{"class":286},[266,4436,1404],{"class":278},[266,4438,4439],{"class":286}," segIdx;\n",[266,4441,4442],{"class":211,"line":462},[266,4443,745],{"class":286},[771,4445,4447],{"id":4446},"_2-饼图甜甜圈扇区命中极坐标判定模-2pi-角度消除跳变与动态外扩补偿","2. 饼图\u002F甜甜圈扇区命中：极坐标判定、模 $2\\pi$ 角度消除跳变与动态外扩补偿",[10,4449,4450],{},"扇环由圆心 $\\mathbf{c}$、内半径 $R_{\\text{in}}$、外半径 $R_{\\text{out}}$、起始角 $\\theta_{\\text{start}}$ 与扫角 $\\Delta\\theta_{\\text{sweep}}$ 定义。\n相对于圆心的位移向量为 $\\Delta\\mathbf{p} = \\mathbf{p} - \\mathbf{c}$，极径 $r = |\\Delta\\mathbf{p}|$，极角 $\\theta = \\operatorname{atan2}(\\Delta p_y, \\Delta p_x) \\in (-\\pi, \\pi]$。",[10,4452,4453,4454,4457,4458,4463],{},"由于 $\\operatorname{atan2}$ 在 $\\pm\\pi$（即 $180^\\circ$ 负 X 轴）处存在周期跳变分支切口（Branch Cut），直接比较 $\\theta \\in ",[266,4455,4456],{},"\\theta_{\\text{start}}, \\theta_{\\text{start}} + \\Delta\\theta_{\\text{sweep}}","$ 会在跨过 $\\pm\\pi$ 时失效。",[42,4459,4461],{"href":4460},"file:\u002F\u002F\u002FE:\u002Fkiriyama_canvas\u002Fcore\u002Finclude\u002Fkiriyama\u002Fcore\u002FGeometry.h#L186-L244",[14,4462,4071],{}," 通过模 $2\\pi$ 正向归一化函数 $\\operatorname{wrap}_{[0, 2\\pi)}(\\alpha)$ 将相对角差映射到连续单调区间 $[0, 2\\pi)$：",[10,4465,4466,4467,4470],{},"$$\n\\operatorname{wrap}",[3363,4468,4469],{},"{[0, 2\\pi)}(\\theta - \\theta","{\\text{start}}) = \\bigl((\\theta - \\theta_{\\text{start}}) \\bmod 2\\pi + 2\\pi\\bigr) \\bmod 2\\pi \\le \\Delta\\theta_{\\text{sweep}} + \\varepsilon\n$$",[257,4472,4474],{"className":259,"code":4473,"language":261,"meta":262,"style":262},"\u002F\u002F 文件：core\u002Finclude\u002Fkiriyama\u002Fcore\u002FGeometry.h\ninline double normalizeAnglePositive(double angle) noexcept {\n    const double wrapped = std::fmod(angle, math::kTwoPi);\n    return wrapped \u003C 0.0 ? wrapped + math::kTwoPi : wrapped;\n}\n\ninline bool ArcD::contains(const Vec2d& point, double radiusTolerance = 0.0) const noexcept {\n    if (empty()) return false;\n    const Vec2d delta = point - center;\n    const double distance = delta.length();\n    \u002F\u002F 1. 极径夹逼：剔除甜甜圈中心圆孔 (distance \u003C innerRadius) 与外部空白\n    if (distance > outerRadius + radiusTolerance ||\n        distance \u003C std::max(0.0, innerRadius - radiusTolerance)) {\n        return false;\n    }\n    \u002F\u002F 2. 极角模 2π 折回判定\n    if (distance \u003C= 1e-9) return innerRadius \u003C= radiusTolerance;\n    return containsAngle(std::atan2(delta.y, delta.x));\n}\n",[14,4475,4476,4481,4503,4529,4556,4560,4564,4607,4623,4642,4661,4666,4686,4712,4720,4724,4729,4757,4776],{"__ignoreMap":262},[266,4477,4478],{"class":211,"line":268},[266,4479,4480],{"class":271},"\u002F\u002F 文件：core\u002Finclude\u002Fkiriyama\u002Fcore\u002FGeometry.h\n",[266,4482,4483,4485,4487,4490,4492,4494,4497,4499,4501],{"class":211,"line":275},[266,4484,4134],{"class":278},[266,4486,1629],{"class":278},[266,4488,4489],{"class":282}," normalizeAnglePositive",[266,4491,287],{"class":286},[266,4493,1050],{"class":278},[266,4495,4496],{"class":296}," angle",[266,4498,300],{"class":286},[266,4500,3706],{"class":278},[266,4502,306],{"class":286},[266,4504,4505,4507,4509,4512,4514,4516,4518,4521,4524,4526],{"class":211,"line":309},[266,4506,327],{"class":278},[266,4508,1629],{"class":278},[266,4510,4511],{"class":286}," wrapped ",[266,4513,336],{"class":278},[266,4515,892],{"class":282},[266,4517,508],{"class":286},[266,4519,4520],{"class":282},"fmod",[266,4522,4523],{"class":286},"(angle, ",[266,4525,2356],{"class":282},[266,4527,4528],{"class":286},"::kTwoPi);\n",[266,4530,4531,4533,4535,4537,4539,4542,4544,4546,4548,4551,4553],{"class":211,"line":315},[266,4532,3525],{"class":278},[266,4534,4511],{"class":286},[266,4536,1149],{"class":278},[266,4538,1655],{"class":691},[266,4540,4541],{"class":278}," ?",[266,4543,4511],{"class":286},[266,4545,2037],{"class":278},[266,4547,4253],{"class":282},[266,4549,4550],{"class":286},"::kTwoPi ",[266,4552,1404],{"class":278},[266,4554,4555],{"class":286}," wrapped;\n",[266,4557,4558],{"class":211,"line":324},[266,4559,745],{"class":286},[266,4561,4562],{"class":211,"line":348},[266,4563,391],{"emptyLinePlaceholder":390},[266,4565,4566,4568,4570,4573,4575,4578,4580,4582,4584,4586,4588,4590,4592,4595,4597,4599,4601,4603,4605],{"class":211,"line":366},[266,4567,4134],{"class":278},[266,4569,1803],{"class":278},[266,4571,4572],{"class":282}," ArcD",[266,4574,508],{"class":286},[266,4576,4577],{"class":282},"contains",[266,4579,287],{"class":286},[266,4581,357],{"class":278},[266,4583,4137],{"class":282},[266,4585,293],{"class":278},[266,4587,4151],{"class":296},[266,4589,1047],{"class":286},[266,4591,1050],{"class":278},[266,4593,4594],{"class":296}," radiusTolerance",[266,4596,3413],{"class":278},[266,4598,1655],{"class":691},[266,4600,300],{"class":286},[266,4602,357],{"class":278},[266,4604,3421],{"class":278},[266,4606,306],{"class":286},[266,4608,4609,4611,4613,4615,4617,4619,4621],{"class":211,"line":381},[266,4610,667],{"class":278},[266,4612,354],{"class":286},[266,4614,2971],{"class":282},[266,4616,1583],{"class":286},[266,4618,1586],{"class":278},[266,4620,966],{"class":691},[266,4622,969],{"class":286},[266,4624,4625,4627,4629,4632,4634,4637,4639],{"class":211,"line":387},[266,4626,327],{"class":278},[266,4628,4137],{"class":282},[266,4630,4631],{"class":286}," delta ",[266,4633,336],{"class":278},[266,4635,4636],{"class":286}," point ",[266,4638,3562],{"class":278},[266,4640,4641],{"class":286}," center;\n",[266,4643,4644,4646,4648,4651,4653,4656,4659],{"class":211,"line":394},[266,4645,327],{"class":278},[266,4647,1629],{"class":278},[266,4649,4650],{"class":286}," distance ",[266,4652,336],{"class":278},[266,4654,4655],{"class":286}," delta.",[266,4657,4658],{"class":282},"length",[266,4660,534],{"class":286},[266,4662,4663],{"class":211,"line":400},[266,4664,4665],{"class":271},"    \u002F\u002F 1. 极径夹逼：剔除甜甜圈中心圆孔 (distance \u003C innerRadius) 与外部空白\n",[266,4667,4668,4670,4673,4675,4678,4680,4683],{"class":211,"line":413},[266,4669,667],{"class":278},[266,4671,4672],{"class":286}," (distance ",[266,4674,1560],{"class":278},[266,4676,4677],{"class":286}," outerRadius ",[266,4679,2037],{"class":278},[266,4681,4682],{"class":286}," radiusTolerance ",[266,4684,4685],{"class":278},"||\n",[266,4687,4688,4691,4693,4695,4697,4700,4702,4704,4707,4709],{"class":211,"line":440},[266,4689,4690],{"class":286},"        distance ",[266,4692,1149],{"class":278},[266,4694,892],{"class":282},[266,4696,508],{"class":286},[266,4698,4699],{"class":282},"max",[266,4701,287],{"class":286},[266,4703,2655],{"class":691},[266,4705,4706],{"class":286},", innerRadius ",[266,4708,3562],{"class":278},[266,4710,4711],{"class":286}," radiusTolerance)) {\n",[266,4713,4714,4716,4718],{"class":211,"line":462},[266,4715,3491],{"class":278},[266,4717,966],{"class":691},[266,4719,969],{"class":286},[266,4721,4722],{"class":211,"line":484},[266,4723,384],{"class":286},[266,4725,4726],{"class":211,"line":489},[266,4727,4728],{"class":271},"    \u002F\u002F 2. 极角模 2π 折回判定\n",[266,4730,4731,4733,4735,4737,4739,4742,4745,4747,4749,4752,4754],{"class":211,"line":494},[266,4732,667],{"class":278},[266,4734,4672],{"class":286},[266,4736,1652],{"class":278},[266,4738,1393],{"class":691},[266,4740,4741],{"class":278},"e-",[266,4743,4744],{"class":691},"9",[266,4746,300],{"class":286},[266,4748,1586],{"class":278},[266,4750,4751],{"class":286}," innerRadius ",[266,4753,1652],{"class":278},[266,4755,4756],{"class":286}," radiusTolerance;\n",[266,4758,4759,4761,4764,4766,4768,4770,4773],{"class":211,"line":500},[266,4760,3525],{"class":278},[266,4762,4763],{"class":282}," containsAngle",[266,4765,287],{"class":286},[266,4767,1036],{"class":282},[266,4769,508],{"class":286},[266,4771,4772],{"class":282},"atan2",[266,4774,4775],{"class":286},"(delta.y, delta.x));\n",[266,4777,4778],{"class":211,"line":537},[266,4779,745],{"class":286},[10,4781,4782,4785,4786,4792,4793,4795,4796,4799],{},[19,4783,4784],{},"动画与命中的数学耦合（防边缘振荡）","：\n在 ",[42,4787,4789],{"href":4788},"file:\u002F\u002F\u002FE:\u002Fkiriyama_canvas\u002Fplatform\u002Fsrc\u002Fsrc\u002FPieChart.cc#L196-L219",[14,4790,4791],{},"PieChart::pickSector"," 中，当扇区 $i$ 被悬停并向外膨胀（外半径增加 $d_{\\text{explode}} \\cdot p_i$，$p_i \\in ",[266,4794,4093],{},"$）时，如果点击测试仍使用静态布局半径 $R_{\\text{out}}$，鼠标停在膨胀后的扇区外缘就会判定“未命中”导致扇区缩回，下一帧又命中，造成高频鬼畜抽搐。因此 ",[14,4797,4798],{},"pickSector"," 对当前正在外扩的扇区动态补偿命中半径：",[257,4801,4803],{"className":259,"code":4802,"language":261,"meta":262,"style":262},"\u002F\u002F 文件：platform\u002Fsrc\u002Fsrc\u002FPieChart.cc\nstd::optional\u003Cstd::size_t> PieChart::pickSector(const Vec2d& worldPoint) const noexcept {\n    const Vec2d local = toLocal(worldPoint);\n    if (hoveredIndex_ && *hoveredIndex_ \u003C explodeProgress_.size() &&\n        explodeProgress_[*hoveredIndex_] > 0.0) {\n        PieLayout expanded = layout_;\n        \u002F\u002F ★ 动态叠加当前帧的外扩动画位移，使膨胀出去的扇区边缘依然保持稳定命中\n        expanded.outerRadius += explodeDistance_ * explodeProgress_[*hoveredIndex_];\n        if (const auto hit = core::hitTestPie(local, expanded); hit.hit && hit.index == *hoveredIndex_) {\n            return hit.index;\n        }\n    }\n    const core::HitResult hit = core::hitTestPie(local, layout_);\n    return hit.hit ? std::optional\u003Cstd::size_t>(hit.index) : std::nullopt;\n}\n",[14,4804,4805,4810,4853,4870,4896,4912,4923,4928,4948,4985,4993,4997,5001,5025,5058],{"__ignoreMap":262},[266,4806,4807],{"class":211,"line":268},[266,4808,4809],{"class":271},"\u002F\u002F 文件：platform\u002Fsrc\u002Fsrc\u002FPieChart.cc\n",[266,4811,4812,4814,4816,4818,4820,4822,4824,4826,4828,4830,4832,4834,4836,4838,4840,4842,4845,4847,4849,4851],{"class":211,"line":275},[266,4813,1036],{"class":282},[266,4815,508],{"class":286},[266,4817,1191],{"class":282},[266,4819,1149],{"class":286},[266,4821,1036],{"class":282},[266,4823,508],{"class":286},[266,4825,1041],{"class":278},[266,4827,1202],{"class":286},[266,4829,2406],{"class":282},[266,4831,508],{"class":286},[266,4833,4798],{"class":282},[266,4835,287],{"class":286},[266,4837,357],{"class":278},[266,4839,4137],{"class":282},[266,4841,293],{"class":278},[266,4843,4844],{"class":296}," worldPoint",[266,4846,300],{"class":286},[266,4848,357],{"class":278},[266,4850,3421],{"class":278},[266,4852,306],{"class":286},[266,4854,4855,4857,4859,4862,4864,4867],{"class":211,"line":309},[266,4856,327],{"class":278},[266,4858,4137],{"class":282},[266,4860,4861],{"class":286}," local ",[266,4863,336],{"class":278},[266,4865,4866],{"class":282}," toLocal",[266,4868,4869],{"class":286},"(worldPoint);\n",[266,4871,4872,4874,4877,4879,4881,4884,4886,4889,4891,4893],{"class":211,"line":315},[266,4873,667],{"class":278},[266,4875,4876],{"class":286}," (hoveredIndex_ ",[266,4878,422],{"class":278},[266,4880,1305],{"class":278},[266,4882,4883],{"class":286},"hoveredIndex_ ",[266,4885,1149],{"class":278},[266,4887,4888],{"class":286}," explodeProgress_.",[266,4890,879],{"class":282},[266,4892,1696],{"class":286},[266,4894,4895],{"class":278},"&&\n",[266,4897,4898,4901,4903,4906,4908,4910],{"class":211,"line":324},[266,4899,4900],{"class":286},"        explodeProgress_[",[266,4902,375],{"class":278},[266,4904,4905],{"class":286},"hoveredIndex_] ",[266,4907,1560],{"class":278},[266,4909,1655],{"class":691},[266,4911,695],{"class":286},[266,4913,4914,4917,4919,4921],{"class":211,"line":348},[266,4915,4916],{"class":282},"        PieLayout",[266,4918,1081],{"class":286},[266,4920,336],{"class":278},[266,4922,1086],{"class":286},[266,4924,4925],{"class":211,"line":366},[266,4926,4927],{"class":271},"        \u002F\u002F ★ 动态叠加当前帧的外扩动画位移，使膨胀出去的扇区边缘依然保持稳定命中\n",[266,4929,4930,4933,4935,4938,4940,4943,4945],{"class":211,"line":381},[266,4931,4932],{"class":286},"        expanded.outerRadius ",[266,4934,1094],{"class":278},[266,4936,4937],{"class":286}," explodeDistance_ ",[266,4939,375],{"class":278},[266,4941,4942],{"class":286}," explodeProgress_[",[266,4944,375],{"class":278},[266,4946,4947],{"class":286},"hoveredIndex_];\n",[266,4949,4950,4952,4954,4956,4958,4961,4963,4965,4967,4970,4973,4975,4978,4980,4982],{"class":211,"line":387},[266,4951,416],{"class":278},[266,4953,354],{"class":286},[266,4955,357],{"class":278},[266,4957,330],{"class":278},[266,4959,4960],{"class":286}," hit ",[266,4962,336],{"class":278},[266,4964,505],{"class":282},[266,4966,508],{"class":286},[266,4968,4969],{"class":282},"hitTestPie",[266,4971,4972],{"class":286},"(local, expanded); hit.hit ",[266,4974,422],{"class":278},[266,4976,4977],{"class":286}," hit.index ",[266,4979,1311],{"class":278},[266,4981,1305],{"class":278},[266,4983,4984],{"class":286},"hoveredIndex_) {\n",[266,4986,4987,4990],{"class":211,"line":394},[266,4988,4989],{"class":278},"            return",[266,4991,4992],{"class":286}," hit.index;\n",[266,4994,4995],{"class":211,"line":400},[266,4996,631],{"class":286},[266,4998,4999],{"class":211,"line":413},[266,5000,384],{"class":286},[266,5002,5003,5005,5007,5009,5012,5014,5016,5018,5020,5022],{"class":211,"line":440},[266,5004,327],{"class":278},[266,5006,505],{"class":282},[266,5008,508],{"class":286},[266,5010,5011],{"class":282},"HitResult",[266,5013,4960],{"class":286},[266,5015,336],{"class":278},[266,5017,505],{"class":282},[266,5019,508],{"class":286},[266,5021,4969],{"class":282},[266,5023,5024],{"class":286},"(local, layout_);\n",[266,5026,5027,5029,5032,5034,5036,5038,5040,5042,5044,5046,5048,5051,5053,5055],{"class":211,"line":462},[266,5028,3525],{"class":278},[266,5030,5031],{"class":286}," hit.hit ",[266,5033,1398],{"class":278},[266,5035,892],{"class":282},[266,5037,508],{"class":286},[266,5039,1191],{"class":282},[266,5041,1149],{"class":286},[266,5043,1036],{"class":282},[266,5045,508],{"class":286},[266,5047,1041],{"class":278},[266,5049,5050],{"class":286},">(hit.index) ",[266,5052,1404],{"class":278},[266,5054,892],{"class":282},[266,5056,5057],{"class":286},"::nullopt;\n",[266,5059,5060],{"class":211,"line":484},[266,5061,745],{"class":286},[771,5063,5065,5066,236],{"id":5064},"_3-任意多边形命中半开区间奇偶射线法geometrycc","3. 任意多边形命中：半开区间奇偶射线法（",[42,5067,5069],{"href":5068},"file:\u002F\u002F\u002FE:\u002Fkiriyama_canvas\u002Fcore\u002Fsrc\u002FGeometry.cc#L66-L89",[14,5070,3655],{},[10,5072,5073],{},"从测试点 $\\mathbf{p} = (p_x, p_y)$ 向 $+x$ 方向发射水平射线。对于多边形的每条有向边 $\\mathbf{a} \\to \\mathbf{b}$：",[1915,5075,5076,5097],{},[1918,5077,5078,5084,5085],{},[19,5079,5080,5081],{},"半开区间跨立判定 ",[14,5082,5083],{},"(a.y > p.y) != (b.y > p.y)","：等价于 $p_y \\in [\\min(a_y, b_y), \\max(a_y, b_y))$（上开下闭）。这一个布尔异或同时解决了两大退化难题：\n",[2266,5086,5087,5094],{},[1918,5088,5089,5090,5093],{},"若边是水平线（$a_y = b_y$），两侧同真或同假，异或为 ",[14,5091,5092],{},"false","，直接忽略；",[1918,5095,5096],{},"若射线恰好穿过某个共享顶点 $V$，衔接 $V$ 的两条边中只有 $y > p_y$ 的那一端会计数一次，绝不会重复翻转。",[1918,5098,5099,1458],{},[19,5100,5101],{},"线性插值求交点横坐标",[10,5103,5104],{},"$$\nx_{\\text{intersect}} = a_x + \\frac{p_y - a_y}{b_y - a_y}(b_x - a_x), \\quad \\text{若 } p_x \u003C x_{\\text{intersect}} \\text{ 则翻转 } \\text{inside} \\leftarrow \\neg\\text{inside}\n$$",[771,5106,5108,5109,236],{"id":5107},"_4-场景树层级命中查询aabb-剪枝与逆序-z-order-遍历scenequerycc","4. 场景树层级命中查询：AABB 剪枝与逆序 Z-Order 遍历（",[42,5110,5112],{"href":5111},"file:\u002F\u002F\u002FE:\u002Fkiriyama_canvas\u002Fcore\u002Fsrc\u002FSceneQuery.cc#L59-L88",[14,5113,5114],{},"SceneQuery.cc",[10,5116,5117,5118,1458],{},"渲染时按子节点正序 $0 \\dots N-1$ 落笔（后画的盖在先画的上面），因此点击测试必须严格按",[19,5119,5120],{},"逆序 $N-1 \\dots 0$ 深度优先遍历",[257,5122,5124],{"className":259,"code":5123,"language":261,"meta":262,"style":262},"\u002F\u002F 文件：core\u002Fsrc\u002FSceneQuery.cc\nconst SceneNode* hitNode(const SceneNode& node, const Vec2d& worldPoint, double tolerance) noexcept {\n    if (!node.visible() || node.style().opacity \u003C= 0.0) return nullptr;\n\n    \u002F\u002F 1. 先求逆变换转入当前节点局部坐标，并用膨胀了 tolerance 的局部 AABB 粗筛\n    const Vec2d local = node.toLocal(worldPoint);\n    if (!node.bounds().isEmpty() && !node.bounds().expanded(tolerance).contains(local)) {\n        \u002F\u002F 若当前节点无自定义 bounds（如 Group 容器），继续向下递归；否则直接剪枝\n    }\n\n    \u002F\u002F 2. 逆序遍历子节点（后绘制的顶层图元最先被测试）\n    for (std::size_t i = node.childCount(); i > 0; --i) {\n        if (const SceneNode* hit = hitNode(*node.childAt(i - 1), worldPoint, tolerance)) {\n            return hit;\n        }\n    }\n\n    \u002F\u002F 3. 纯容器 Group 自身不产生命中，图元节点调用 containsLocal 精确几何判定\n    if (node.type() == NodeType::Group) return nullptr;\n    return node.containsLocal(local, tolerance) ? &node : nullptr;\n}\n",[14,5125,5126,5131,5175,5211,5215,5220,5237,5278,5283,5287,5291,5296,5331,5366,5373,5377,5381,5385,5390,5416,5442],{"__ignoreMap":262},[266,5127,5128],{"class":211,"line":268},[266,5129,5130],{"class":271},"\u002F\u002F 文件：core\u002Fsrc\u002FSceneQuery.cc\n",[266,5132,5133,5135,5137,5139,5142,5144,5146,5148,5150,5152,5154,5156,5158,5160,5162,5164,5166,5169,5171,5173],{"class":211,"line":275},[266,5134,357],{"class":278},[266,5136,1492],{"class":282},[266,5138,375],{"class":278},[266,5140,5141],{"class":282}," hitNode",[266,5143,287],{"class":286},[266,5145,357],{"class":278},[266,5147,1492],{"class":282},[266,5149,293],{"class":278},[266,5151,1497],{"class":296},[266,5153,1047],{"class":286},[266,5155,357],{"class":278},[266,5157,4137],{"class":282},[266,5159,293],{"class":278},[266,5161,4844],{"class":296},[266,5163,1047],{"class":286},[266,5165,1050],{"class":278},[266,5167,5168],{"class":296}," tolerance",[266,5170,300],{"class":286},[266,5172,3706],{"class":278},[266,5174,306],{"class":286},[266,5176,5177,5179,5181,5183,5185,5187,5189,5191,5193,5195,5198,5200,5202,5204,5206,5209],{"class":211,"line":309},[266,5178,667],{"class":278},[266,5180,354],{"class":286},[266,5182,1267],{"class":278},[266,5184,1578],{"class":286},[266,5186,428],{"class":282},[266,5188,1696],{"class":286},[266,5190,2685],{"class":278},[266,5192,1618],{"class":286},[266,5194,1291],{"class":282},[266,5196,5197],{"class":286},"().opacity ",[266,5199,1652],{"class":278},[266,5201,1655],{"class":691},[266,5203,300],{"class":286},[266,5205,1586],{"class":278},[266,5207,5208],{"class":691}," nullptr",[266,5210,969],{"class":286},[266,5212,5213],{"class":211,"line":315},[266,5214,391],{"emptyLinePlaceholder":390},[266,5216,5217],{"class":211,"line":324},[266,5218,5219],{"class":271},"    \u002F\u002F 1. 先求逆变换转入当前节点局部坐标，并用膨胀了 tolerance 的局部 AABB 粗筛\n",[266,5221,5222,5224,5226,5228,5230,5232,5235],{"class":211,"line":348},[266,5223,327],{"class":278},[266,5225,4137],{"class":282},[266,5227,4861],{"class":286},[266,5229,336],{"class":278},[266,5231,1618],{"class":286},[266,5233,5234],{"class":282},"toLocal",[266,5236,4869],{"class":286},[266,5238,5239,5241,5243,5245,5247,5250,5252,5255,5257,5259,5261,5263,5265,5267,5270,5273,5275],{"class":211,"line":366},[266,5240,667],{"class":278},[266,5242,354],{"class":286},[266,5244,1267],{"class":278},[266,5246,1578],{"class":286},[266,5248,5249],{"class":282},"bounds",[266,5251,546],{"class":286},[266,5253,5254],{"class":282},"isEmpty",[266,5256,1696],{"class":286},[266,5258,422],{"class":278},[266,5260,2688],{"class":278},[266,5262,1578],{"class":286},[266,5264,5249],{"class":282},[266,5266,546],{"class":286},[266,5268,5269],{"class":282},"expanded",[266,5271,5272],{"class":286},"(tolerance).",[266,5274,4577],{"class":282},[266,5276,5277],{"class":286},"(local)) {\n",[266,5279,5280],{"class":211,"line":381},[266,5281,5282],{"class":271},"        \u002F\u002F 若当前节点无自定义 bounds（如 Group 容器），继续向下递归；否则直接剪枝\n",[266,5284,5285],{"class":211,"line":387},[266,5286,384],{"class":286},[266,5288,5289],{"class":211,"line":394},[266,5290,391],{"emptyLinePlaceholder":390},[266,5292,5293],{"class":211,"line":400},[266,5294,5295],{"class":271},"    \u002F\u002F 2. 逆序遍历子节点（后绘制的顶层图元最先被测试）\n",[266,5297,5298,5300,5302,5304,5306,5308,5310,5312,5314,5316,5319,5321,5323,5326,5329],{"class":211,"line":413},[266,5299,351],{"class":278},[266,5301,354],{"class":286},[266,5303,1036],{"class":282},[266,5305,508],{"class":286},[266,5307,1041],{"class":278},[266,5309,2126],{"class":286},[266,5311,336],{"class":278},[266,5313,1618],{"class":286},[266,5315,3031],{"class":282},[266,5317,5318],{"class":286},"(); i ",[266,5320,1560],{"class":278},[266,5322,692],{"class":691},[266,5324,5325],{"class":286},"; ",[266,5327,5328],{"class":278},"--",[266,5330,2147],{"class":286},[266,5332,5333,5335,5337,5339,5341,5343,5345,5347,5349,5351,5353,5355,5357,5359,5361,5363],{"class":211,"line":440},[266,5334,416],{"class":278},[266,5336,354],{"class":286},[266,5338,357],{"class":278},[266,5340,1492],{"class":286},[266,5342,375],{"class":278},[266,5344,4960],{"class":286},[266,5346,336],{"class":278},[266,5348,5141],{"class":282},[266,5350,287],{"class":286},[266,5352,375],{"class":278},[266,5354,1578],{"class":286},[266,5356,3056],{"class":282},[266,5358,2180],{"class":286},[266,5360,3562],{"class":278},[266,5362,1393],{"class":691},[266,5364,5365],{"class":286},"), worldPoint, tolerance)) {\n",[266,5367,5368,5370],{"class":211,"line":462},[266,5369,4989],{"class":278},[266,5371,5372],{"class":286}," hit;\n",[266,5374,5375],{"class":211,"line":484},[266,5376,631],{"class":286},[266,5378,5379],{"class":211,"line":489},[266,5380,384],{"class":286},[266,5382,5383],{"class":211,"line":494},[266,5384,391],{"emptyLinePlaceholder":390},[266,5386,5387],{"class":211,"line":500},[266,5388,5389],{"class":271},"    \u002F\u002F 3. 纯容器 Group 自身不产生命中，图元节点调用 containsLocal 精确几何判定\n",[266,5391,5392,5394,5397,5400,5402,5404,5407,5410,5412,5414],{"class":211,"line":537},[266,5393,667],{"class":278},[266,5395,5396],{"class":286}," (node.",[266,5398,5399],{"class":282},"type",[266,5401,1696],{"class":286},[266,5403,1311],{"class":278},[266,5405,5406],{"class":282}," NodeType",[266,5408,5409],{"class":286},"::Group) ",[266,5411,1586],{"class":278},[266,5413,5208],{"class":691},[266,5415,969],{"class":286},[266,5417,5418,5420,5422,5425,5428,5430,5433,5436,5438,5440],{"class":211,"line":555},[266,5419,3525],{"class":278},[266,5421,1618],{"class":286},[266,5423,5424],{"class":282},"containsLocal",[266,5426,5427],{"class":286},"(local, tolerance) ",[266,5429,1398],{"class":278},[266,5431,5432],{"class":278}," &",[266,5434,5435],{"class":286},"node ",[266,5437,1404],{"class":278},[266,5439,5208],{"class":691},[266,5441,969],{"class":286},[266,5443,5444],{"class":211,"line":568},[266,5445,745],{"class":286},[771,5447,5449,5450,236],{"id":5448},"_5-虚拟滚动表格的-o1-行列反查数学tablelayoutcc","5. 虚拟滚动表格的 $O(1)$ 行列反查数学（",[42,5451,5453],{"href":5452},"file:\u002F\u002F\u002FE:\u002Fkiriyama_canvas\u002Fcore\u002Fsrc\u002FTableLayout.cc#L56-L80",[14,5454,5455],{},"TableLayout.cc",[10,5457,5458,5459,5462],{},"对于包含上万行的虚拟滚动表格，鼠标点击时",[19,5460,5461],{},"绝不遍历行节点","，而是利用表体偏移 $y_{\\text{body}} = p_y - \\text{bodyRect}.y$ 与滚动量 $\\text{scrollOffset}$ 直接在 $O(1)$ 时间内算出逻辑行号：",[10,5464,5465],{},"$$\n\\text{row} = \\left\\lfloor \\frac{(p_y - \\text{bodyRect}.y) + \\text{scrollOffset}}{\\text{rowHeight}} \\right\\rfloor\n$$",[257,5467,5469],{"className":259,"code":5468,"language":261,"meta":262,"style":262},"\u002F\u002F 文件：core\u002Fsrc\u002FTableLayout.cc\nstd::optional\u003Cstd::size_t> TableLayout::rowAtY(double y, std::size_t totalRows) const noexcept {\n    if (rowHeight \u003C= 0.0 || y \u003C bodyRect.y || y >= bodyRect.bottom()) return std::nullopt;\n    const double contentY = (y - bodyRect.y) + scrollOffset;\n    if (contentY \u003C 0.0) return std::nullopt;\n    const auto row = static_cast\u003Cstd::size_t>( std::floor(contentY \u002F rowHeight));\n    return row \u003C totalRows ? std::optional\u003Cstd::size_t>(row) : std::nullopt;\n}\n",[14,5470,5471,5476,5528,5569,5593,5612,5651,5687],{"__ignoreMap":262},[266,5472,5473],{"class":211,"line":268},[266,5474,5475],{"class":271},"\u002F\u002F 文件：core\u002Fsrc\u002FTableLayout.cc\n",[266,5477,5478,5480,5482,5484,5486,5488,5490,5492,5494,5497,5499,5502,5504,5506,5509,5511,5513,5515,5517,5520,5522,5524,5526],{"class":211,"line":275},[266,5479,1036],{"class":282},[266,5481,508],{"class":286},[266,5483,1191],{"class":282},[266,5485,1149],{"class":286},[266,5487,1036],{"class":282},[266,5489,508],{"class":286},[266,5491,1041],{"class":278},[266,5493,1202],{"class":286},[266,5495,5496],{"class":282},"TableLayout",[266,5498,508],{"class":286},[266,5500,5501],{"class":282},"rowAtY",[266,5503,287],{"class":286},[266,5505,1050],{"class":278},[266,5507,5508],{"class":296}," y",[266,5510,1047],{"class":286},[266,5512,1036],{"class":282},[266,5514,508],{"class":286},[266,5516,1041],{"class":278},[266,5518,5519],{"class":296}," totalRows",[266,5521,300],{"class":286},[266,5523,357],{"class":278},[266,5525,3421],{"class":278},[266,5527,306],{"class":286},[266,5529,5530,5532,5535,5537,5539,5541,5544,5546,5549,5551,5553,5556,5559,5561,5563,5565,5567],{"class":211,"line":309},[266,5531,667],{"class":278},[266,5533,5534],{"class":286}," (rowHeight ",[266,5536,1652],{"class":278},[266,5538,1655],{"class":691},[266,5540,2616],{"class":278},[266,5542,5543],{"class":286}," y ",[266,5545,1149],{"class":278},[266,5547,5548],{"class":286}," bodyRect.y ",[266,5550,2685],{"class":278},[266,5552,5543],{"class":286},[266,5554,5555],{"class":278},">=",[266,5557,5558],{"class":286}," bodyRect.",[266,5560,3836],{"class":282},[266,5562,1583],{"class":286},[266,5564,1586],{"class":278},[266,5566,892],{"class":282},[266,5568,5057],{"class":286},[266,5570,5571,5573,5575,5578,5580,5583,5585,5588,5590],{"class":211,"line":315},[266,5572,327],{"class":278},[266,5574,1629],{"class":278},[266,5576,5577],{"class":286}," contentY ",[266,5579,336],{"class":278},[266,5581,5582],{"class":286}," (y ",[266,5584,3562],{"class":278},[266,5586,5587],{"class":286}," bodyRect.y) ",[266,5589,2037],{"class":278},[266,5591,5592],{"class":286}," scrollOffset;\n",[266,5594,5595,5597,5600,5602,5604,5606,5608,5610],{"class":211,"line":324},[266,5596,667],{"class":278},[266,5598,5599],{"class":286}," (contentY ",[266,5601,1149],{"class":278},[266,5603,1655],{"class":691},[266,5605,300],{"class":286},[266,5607,1586],{"class":278},[266,5609,892],{"class":282},[266,5611,5057],{"class":286},[266,5613,5614,5616,5618,5621,5623,5626,5628,5630,5633,5636,5638,5640,5643,5646,5648],{"class":211,"line":348},[266,5615,327],{"class":278},[266,5617,330],{"class":278},[266,5619,5620],{"class":286}," row ",[266,5622,336],{"class":278},[266,5624,5625],{"class":278}," static_cast\u003C",[266,5627,1036],{"class":282},[266,5629,508],{"class":286},[266,5631,5632],{"class":278},"size_t>",[266,5634,5635],{"class":286},"( ",[266,5637,1036],{"class":282},[266,5639,508],{"class":286},[266,5641,5642],{"class":282},"floor",[266,5644,5645],{"class":286},"(contentY ",[266,5647,4275],{"class":278},[266,5649,5650],{"class":286}," rowHeight));\n",[266,5652,5653,5655,5657,5659,5662,5664,5666,5668,5670,5672,5674,5676,5678,5681,5683,5685],{"class":211,"line":366},[266,5654,3525],{"class":278},[266,5656,5620],{"class":286},[266,5658,1149],{"class":278},[266,5660,5661],{"class":286}," totalRows ",[266,5663,1398],{"class":278},[266,5665,892],{"class":282},[266,5667,508],{"class":286},[266,5669,1191],{"class":282},[266,5671,1149],{"class":286},[266,5673,1036],{"class":282},[266,5675,508],{"class":286},[266,5677,1041],{"class":278},[266,5679,5680],{"class":286},">(row) ",[266,5682,1404],{"class":278},[266,5684,892],{"class":282},[266,5686,5057],{"class":286},[266,5688,5689],{"class":211,"line":381},[266,5690,745],{"class":286},[10,5692,5693,5694,5700,5701,5704,5705,5708],{},"同时，键盘上下移动选中行时的自动跟随滚屏（",[42,5695,5697],{"href":5696},"file:\u002F\u002F\u002FE:\u002Fkiriyama_canvas\u002Fcore\u002Fsrc\u002FTableLayout.cc#L40-L54",[14,5698,5699],{},"TableLayout::scrollToRevealRow","）通过区间夹逼实现：设目标行在内容空间的上下边界为 $",[266,5702,5703],{},"y_{\\text{top}}, y_{\\text{bottom}}"," = ",[266,5706,5707],{},"r \\cdot h, (r+1) \\cdot h","$，视口高度为 $H_{\\text{body}}$，则新的滚动偏移量为：",[10,5710,5711],{},"$$\n\\text{scroll}' = \\operatorname{clamp}!\\left(\n\\begin{cases}\ny_{\\text{top}}, & \\text{if } y_{\\text{top}} \u003C \\text{scroll} \\\ny_{\\text{bottom}} - H_{\\text{body}}, & \\text{if } y_{\\text{bottom}} > \\text{scroll} + H_{\\text{body}} \\\n\\text{scroll}, & \\text{otherwise}\n\\end{cases}\n,; 0,; \\text{maxScroll}\\right)\n$$",[28,5713],{},[230,5715,5717],{"id":5716},"_33-交互模块的数学与状态机实现","3.3 交互模块的数学与状态机实现",[771,5719,5721,5722,5724,5725,4072,5731,236],{"id":5720},"_1-widget-指针状态机拖拽保持与防误触死区widgetcc-tablewidgetcc","1. ",[14,5723,2396],{}," 指针状态机、拖拽保持与防误触死区（",[42,5726,5728],{"href":5727},"file:\u002F\u002F\u002FE:\u002Fkiriyama_canvas\u002Fplatform\u002Fsrc\u002Fsrc\u002FWidget.cc#L173-L210",[14,5729,5730],{},"Widget.cc",[42,5732,5734],{"href":5733},"file:\u002F\u002F\u002FE:\u002Fkiriyama_canvas\u002Fplatform\u002Fsrc\u002Fsrc\u002FTableWidget.cc#L183-L216",[14,5735,5736],{},"TableWidget.cc",[10,5738,5739,5740,5743,5744,5747],{},"在每帧的 ",[14,5741,5742],{},"Widget::tick(ctx)"," 中，通过比较当前帧指针状态 $(p_t, \\text{down}",[3363,5745,5746],{"t-1":262},"t)$ 与上一帧快照 $(p",", \\text{down}_)$ 实现无事件队列的边沿检测：",[10,5749,5750,5751,5754,5755,5758,5759,5762,5763,5766],{},"$$\n\\begin{aligned}\n\\text{inside}_t &= \\operatorname{hitShape}(\\mathbf{p}_t) \\\n\\text{justPressed}_t &= \\text{down}",[3363,5752,5753],{"t-1":262},"t \\land \\neg\\text{down}"," \\\n\\text{justReleased}_t &= \\neg\\text{down}",[3363,5756,5757],{"t-1":262},"t \\land \\text{down}"," \\\n\\text{hovered}_t &= \\text{inside}",[3363,5760,5761],{"t-1":262},"t \\lor (\\text{pressed}"," \\land \\text{down}_t) \\\n\\text{clicked}_t &= \\text{justReleased}",[3363,5764,5765],{"t-1":262},"t \\land \\text{pressed}"," \\land \\text{inside}_t\n\\end{aligned}\n$$",[257,5768,5770],{"className":259,"code":5769,"language":261,"meta":262,"style":262},"\u002F\u002F 文件：platform\u002Fsrc\u002Fsrc\u002FWidget.cc\nvoid Widget::tick(const FrameContext& ctx) {\n    clicked_ = false; \u002F\u002F 单帧脉冲复位\n    const bool wasInside = inside_;\n    inside_ = hitShape(ctx.pointer.position);\n\n    const bool justPressed = ctx.pointer.down && !down_;\n    const bool justReleased = !ctx.pointer.down && down_;\n    down_ = ctx.pointer.down;\n\n    if (inside_ && !wasInside) onPointerEnter();\n    else if (!inside_ && wasInside) onPointerLeave();\n\n    if (justPressed && inside_) {\n        pressed_ = true;\n        onPress();\n    }\n    if (justReleased && pressed_) {\n        const bool releasedInside = inside_;\n        pressed_ = false;\n        clicked_ = releasedInside;\n        onRelease(releasedInside);\n        if (clicked_) {\n            for (const auto& cb : clickBindings_) if (cb) cb();\n        }\n    }\n    \u002F\u002F ★ 捕获保持：按住左键拖拽滑出组件边界时，hovered_ 保持为 true，防止拖拽中途断触\n    hovered_ = inside_ || (pressed_ && ctx.pointer.down);\n    onUpdate(ctx);\n}\n",[14,5771,5772,5777,5801,5816,5830,5843,5847,5868,5889,5899,5903,5922,5946,5950,5962,5973,5980,5984,5996,6009,6019,6029,6037,6044,6068,6072,6076,6081,6101,6108],{"__ignoreMap":262},[266,5773,5774],{"class":211,"line":268},[266,5775,5776],{"class":271},"\u002F\u002F 文件：platform\u002Fsrc\u002Fsrc\u002FWidget.cc\n",[266,5778,5779,5781,5783,5785,5788,5790,5792,5794,5796,5799],{"class":211,"line":275},[266,5780,279],{"class":278},[266,5782,2439],{"class":282},[266,5784,508],{"class":286},[266,5786,5787],{"class":282},"tick",[266,5789,287],{"class":286},[266,5791,357],{"class":278},[266,5793,2581],{"class":282},[266,5795,293],{"class":278},[266,5797,5798],{"class":296}," ctx",[266,5800,695],{"class":286},[266,5802,5803,5806,5808,5810,5813],{"class":211,"line":309},[266,5804,5805],{"class":286},"    clicked_ ",[266,5807,336],{"class":278},[266,5809,966],{"class":691},[266,5811,5812],{"class":286},";",[266,5814,5815],{"class":271}," \u002F\u002F 单帧脉冲复位\n",[266,5817,5818,5820,5822,5825,5827],{"class":211,"line":315},[266,5819,327],{"class":278},[266,5821,1803],{"class":278},[266,5823,5824],{"class":286}," wasInside ",[266,5826,336],{"class":278},[266,5828,5829],{"class":286}," inside_;\n",[266,5831,5832,5835,5837,5840],{"class":211,"line":324},[266,5833,5834],{"class":286},"    inside_ ",[266,5836,336],{"class":278},[266,5838,5839],{"class":282}," hitShape",[266,5841,5842],{"class":286},"(ctx.pointer.position);\n",[266,5844,5845],{"class":211,"line":348},[266,5846,391],{"emptyLinePlaceholder":390},[266,5848,5849,5851,5853,5856,5858,5861,5863,5865],{"class":211,"line":366},[266,5850,327],{"class":278},[266,5852,1803],{"class":278},[266,5854,5855],{"class":286}," justPressed ",[266,5857,336],{"class":278},[266,5859,5860],{"class":286}," ctx.pointer.down ",[266,5862,422],{"class":278},[266,5864,2688],{"class":278},[266,5866,5867],{"class":286},"down_;\n",[266,5869,5870,5872,5874,5877,5879,5881,5884,5886],{"class":211,"line":381},[266,5871,327],{"class":278},[266,5873,1803],{"class":278},[266,5875,5876],{"class":286}," justReleased ",[266,5878,336],{"class":278},[266,5880,2688],{"class":278},[266,5882,5883],{"class":286},"ctx.pointer.down ",[266,5885,422],{"class":278},[266,5887,5888],{"class":286}," down_;\n",[266,5890,5891,5894,5896],{"class":211,"line":387},[266,5892,5893],{"class":286},"    down_ ",[266,5895,336],{"class":278},[266,5897,5898],{"class":286}," ctx.pointer.down;\n",[266,5900,5901],{"class":211,"line":394},[266,5902,391],{"emptyLinePlaceholder":390},[266,5904,5905,5907,5910,5912,5914,5917,5920],{"class":211,"line":400},[266,5906,667],{"class":278},[266,5908,5909],{"class":286}," (inside_ ",[266,5911,422],{"class":278},[266,5913,2688],{"class":278},[266,5915,5916],{"class":286},"wasInside) ",[266,5918,5919],{"class":282},"onPointerEnter",[266,5921,534],{"class":286},[266,5923,5924,5927,5929,5931,5933,5936,5938,5941,5944],{"class":211,"line":413},[266,5925,5926],{"class":278},"    else",[266,5928,1338],{"class":278},[266,5930,354],{"class":286},[266,5932,1267],{"class":278},[266,5934,5935],{"class":286},"inside_ ",[266,5937,422],{"class":278},[266,5939,5940],{"class":286}," wasInside) ",[266,5942,5943],{"class":282},"onPointerLeave",[266,5945,534],{"class":286},[266,5947,5948],{"class":211,"line":440},[266,5949,391],{"emptyLinePlaceholder":390},[266,5951,5952,5954,5957,5959],{"class":211,"line":462},[266,5953,667],{"class":278},[266,5955,5956],{"class":286}," (justPressed ",[266,5958,422],{"class":278},[266,5960,5961],{"class":286}," inside_) {\n",[266,5963,5964,5967,5969,5971],{"class":211,"line":484},[266,5965,5966],{"class":286},"        pressed_ ",[266,5968,336],{"class":278},[266,5970,1981],{"class":691},[266,5972,969],{"class":286},[266,5974,5975,5978],{"class":211,"line":489},[266,5976,5977],{"class":282},"        onPress",[266,5979,534],{"class":286},[266,5981,5982],{"class":211,"line":494},[266,5983,384],{"class":286},[266,5985,5986,5988,5991,5993],{"class":211,"line":500},[266,5987,667],{"class":278},[266,5989,5990],{"class":286}," (justReleased ",[266,5992,422],{"class":278},[266,5994,5995],{"class":286}," pressed_) {\n",[266,5997,5998,6000,6002,6005,6007],{"class":211,"line":537},[266,5999,3042],{"class":278},[266,6001,1803],{"class":278},[266,6003,6004],{"class":286}," releasedInside ",[266,6006,336],{"class":278},[266,6008,5829],{"class":286},[266,6010,6011,6013,6015,6017],{"class":211,"line":555},[266,6012,5966],{"class":286},[266,6014,336],{"class":278},[266,6016,966],{"class":691},[266,6018,969],{"class":286},[266,6020,6021,6024,6026],{"class":211,"line":568},[266,6022,6023],{"class":286},"        clicked_ ",[266,6025,336],{"class":278},[266,6027,6028],{"class":286}," releasedInside;\n",[266,6030,6031,6034],{"class":211,"line":584},[266,6032,6033],{"class":282},"        onRelease",[266,6035,6036],{"class":286},"(releasedInside);\n",[266,6038,6039,6041],{"class":211,"line":606},[266,6040,416],{"class":278},[266,6042,6043],{"class":286}," (clicked_) {\n",[266,6045,6046,6049,6051,6053,6055,6058,6060,6063,6066],{"class":211,"line":628},[266,6047,6048],{"class":278},"            for",[266,6050,354],{"class":286},[266,6052,357],{"class":278},[266,6054,360],{"class":278},[266,6056,6057],{"class":286}," cb : clickBindings_) ",[266,6059,2103],{"class":278},[266,6061,6062],{"class":286}," (cb) ",[266,6064,6065],{"class":282},"cb",[266,6067,534],{"class":286},[266,6069,6070],{"class":211,"line":634},[266,6071,631],{"class":286},[266,6073,6074],{"class":211,"line":640},[266,6075,384],{"class":286},[266,6077,6078],{"class":211,"line":645},[266,6079,6080],{"class":271},"    \u002F\u002F ★ 捕获保持：按住左键拖拽滑出组件边界时，hovered_ 保持为 true，防止拖拽中途断触\n",[266,6082,6083,6086,6088,6091,6093,6096,6098],{"class":211,"line":650},[266,6084,6085],{"class":286},"    hovered_ ",[266,6087,336],{"class":278},[266,6089,6090],{"class":286}," inside_ ",[266,6092,2685],{"class":278},[266,6094,6095],{"class":286}," (pressed_ ",[266,6097,422],{"class":278},[266,6099,6100],{"class":286}," ctx.pointer.down);\n",[266,6102,6103,6106],{"class":211,"line":656},[266,6104,6105],{"class":282},"    onUpdate",[266,6107,437],{"class":286},[266,6109,6110],{"class":211,"line":664},[266,6111,745],{"class":286},[10,6113,6114,4785,6117,6119,6120,6126,6127,6130],{},[19,6115,6116],{},"拖拽与点击的欧氏距离平方死区判别（Deadzone）",[14,6118,2400],{}," 中，用户既可以按住鼠标上下拖拽滚动表体，也可以点击表头排序或点击行选中。为防止微小手抖将拖拽误判为点击，",[42,6121,6123],{"href":6122},"file:\u002F\u002F\u002FE:\u002Fkiriyama_canvas\u002Fplatform\u002Fsrc\u002Fsrc\u002FTableWidget.cc#L196-L202",[14,6124,6125],{},"TableWidget::onRelease"," 采用 $6\\text{ px}$ 的欧氏距离死区（直接比较平方距离避免 ",[14,6128,6129],{},"std::sqrt"," 开方）：",[10,6132,6133,6134,6137],{},"$$\n|\\mathbf{p}",[3363,6135,6136],{},"{\\text{release}} - \\mathbf{p}","{\\text{press}}|^2 = \\Delta x^2 + \\Delta y^2 > 36.0 \\implies \\text{判定为拖拽滚屏，抑制点击事件}\n$$",[771,6139,6141,6142,6145],{"id":6140},"_2-inputcontroller-键盘修饰键掩码归一化与树状冒泡路由","2. ",[14,6143,6144],{},"InputController"," 键盘修饰键掩码归一化与树状冒泡路由",[10,6147,6148,6154,6155,6158,6159,4275,6162,6165],{},[42,6149,6151],{"href":6150},"file:\u002F\u002F\u002FE:\u002Fkiriyama_canvas\u002Fplatform\u002Fsrc\u002Fsrc\u002FInputController.cc#L38-L133",[14,6152,6153],{},"InputController.cc"," 提供零 SDL 依赖的按键与滚轮绑定机制。为了使快捷键绑定（如 ",[14,6156,6157],{},"Ctrl + Home","）具有确定性，匹配前先对修饰键位掩码做位与归一化（滤除 ",[14,6160,6161],{},"CapsLock",[14,6163,6164],{},"NumLock"," 等无关状态位）：",[10,6167,6168],{},"$$\n\\text{mod}_{\\text{norm}} = \\text{mod} ;\\ & ;\\ (\\text{Shift} \\mid \\text{Ctrl} \\mid \\text{Alt} \\mid \\text{Super})\n$$",[10,6170,6171,6172,6174,6175,6178],{},"滚轮与按键沿 ",[14,6173,788],{}," 树按**逆序深度优先（顶层组件优先）**匹配位置并触发回调，一旦某个子组件返回 ",[14,6176,6177],{},"true","（已消费），立即短路停止冒泡：",[257,6180,6182],{"className":259,"code":6181,"language":261,"meta":262,"style":262},"\u002F\u002F 文件：platform\u002Fsrc\u002Fsrc\u002FInputController.cc\nbool dispatchWheelToTree(core::SceneNode& node, const WheelEvent& event) {\n    if (!node.visible()) return false;\n    for (std::size_t i = node.childCount(); i > 0; --i) {\n        if (dispatchWheelToTree(*node.childAt(i - 1), event)) return true; \u002F\u002F 子节点已消费则短路\n    }\n    if (auto* widget = dynamic_cast\u003CWidget*>(&node)) {\n        if (widget->interactive() && widget->containsPoint(event.position)) {\n            return widget->dispatchWheel(event);\n        }\n    }\n    return false;\n}\n",[14,6183,6184,6189,6222,6242,6274,6309,6313,6342,6365,6377,6381,6385,6393],{"__ignoreMap":262},[266,6185,6186],{"class":211,"line":268},[266,6187,6188],{"class":271},"\u002F\u002F 文件：platform\u002Fsrc\u002Fsrc\u002FInputController.cc\n",[266,6190,6191,6193,6196,6198,6200,6202,6204,6206,6208,6210,6212,6215,6217,6220],{"class":211,"line":275},[266,6192,1973],{"class":278},[266,6194,6195],{"class":282}," dispatchWheelToTree",[266,6197,287],{"class":286},[266,6199,1110],{"class":282},[266,6201,508],{"class":286},[266,6203,788],{"class":282},[266,6205,293],{"class":278},[266,6207,1497],{"class":296},[266,6209,1047],{"class":286},[266,6211,357],{"class":278},[266,6213,6214],{"class":282}," WheelEvent",[266,6216,293],{"class":278},[266,6218,6219],{"class":296}," event",[266,6221,695],{"class":286},[266,6223,6224,6226,6228,6230,6232,6234,6236,6238,6240],{"class":211,"line":309},[266,6225,667],{"class":278},[266,6227,354],{"class":286},[266,6229,1267],{"class":278},[266,6231,1578],{"class":286},[266,6233,428],{"class":282},[266,6235,1583],{"class":286},[266,6237,1586],{"class":278},[266,6239,966],{"class":691},[266,6241,969],{"class":286},[266,6243,6244,6246,6248,6250,6252,6254,6256,6258,6260,6262,6264,6266,6268,6270,6272],{"class":211,"line":315},[266,6245,351],{"class":278},[266,6247,354],{"class":286},[266,6249,1036],{"class":282},[266,6251,508],{"class":286},[266,6253,1041],{"class":278},[266,6255,2126],{"class":286},[266,6257,336],{"class":278},[266,6259,1618],{"class":286},[266,6261,3031],{"class":282},[266,6263,5318],{"class":286},[266,6265,1560],{"class":278},[266,6267,692],{"class":691},[266,6269,5325],{"class":286},[266,6271,5328],{"class":278},[266,6273,2147],{"class":286},[266,6275,6276,6278,6280,6283,6285,6287,6289,6291,6293,6295,6297,6300,6302,6304,6306],{"class":211,"line":324},[266,6277,416],{"class":278},[266,6279,354],{"class":286},[266,6281,6282],{"class":282},"dispatchWheelToTree",[266,6284,287],{"class":286},[266,6286,375],{"class":278},[266,6288,1578],{"class":286},[266,6290,3056],{"class":282},[266,6292,2180],{"class":286},[266,6294,3562],{"class":278},[266,6296,1393],{"class":691},[266,6298,6299],{"class":286},"), event)) ",[266,6301,1586],{"class":278},[266,6303,1981],{"class":691},[266,6305,5812],{"class":286},[266,6307,6308],{"class":271}," \u002F\u002F 子节点已消费则短路\n",[266,6310,6311],{"class":211,"line":348},[266,6312,384],{"class":286},[266,6314,6315,6317,6319,6322,6325,6327,6330,6332,6335,6337,6339],{"class":211,"line":366},[266,6316,667],{"class":278},[266,6318,354],{"class":286},[266,6320,6321],{"class":278},"auto*",[266,6323,6324],{"class":286}," widget ",[266,6326,336],{"class":278},[266,6328,6329],{"class":278}," dynamic_cast\u003C",[266,6331,2396],{"class":286},[266,6333,6334],{"class":278},"*>",[266,6336,287],{"class":286},[266,6338,293],{"class":278},[266,6340,6341],{"class":286},"node)) {\n",[266,6343,6344,6346,6349,6352,6354,6356,6359,6362],{"class":211,"line":381},[266,6345,416],{"class":278},[266,6347,6348],{"class":286}," (widget->",[266,6350,6351],{"class":282},"interactive",[266,6353,1696],{"class":286},[266,6355,422],{"class":278},[266,6357,6358],{"class":286}," widget->",[266,6360,6361],{"class":282},"containsPoint",[266,6363,6364],{"class":286},"(event.position)) {\n",[266,6366,6367,6369,6371,6374],{"class":211,"line":387},[266,6368,4989],{"class":278},[266,6370,6358],{"class":286},[266,6372,6373],{"class":282},"dispatchWheel",[266,6375,6376],{"class":286},"(event);\n",[266,6378,6379],{"class":211,"line":394},[266,6380,631],{"class":286},[266,6382,6383],{"class":211,"line":400},[266,6384,384],{"class":286},[266,6386,6387,6389,6391],{"class":211,"line":413},[266,6388,3525],{"class":278},[266,6390,966],{"class":691},[266,6392,969],{"class":286},[266,6394,6395],{"class":211,"line":440},[266,6396,745],{"class":286},[771,6398,6400,6401,236],{"id":6399},"_3-声明式交互样式合成porter-duff-alpha-混合与中心锚点缩放矩阵interactioncc","3. 声明式交互样式合成：Porter-Duff Alpha 混合与中心锚点缩放矩阵（",[42,6402,6404],{"href":6403},"file:\u002F\u002F\u002FE:\u002Fkiriyama_canvas\u002Fplatform\u002Fsrc\u002Fsrc\u002FInteraction.cc",[14,6405,6406],{},"Interaction.cc",[10,6408,6409,6411,6412,6414,6415,6418,6419,6422],{},[14,6410,6406],{}," 支持为任意 ",[14,6413,788],{}," 声明悬停（",[14,6416,6417],{},"hover","）与按下（",[14,6420,6421],{},"active","）时的视觉反馈覆写（颜色叠加、高亮描边、中心缩放），其底层包含两个核心数学实现：",[10,6424,6425,6431,6432,6435],{},[19,6426,6427,6428,236],{},"(1) Porter-Duff Source-Over 颜色叠加公式（",[14,6429,6430],{},"blendOver","\n当交互状态为节点叠加一层半透明高光色 $C_{\\text{top}}(\\alpha_t)$ 在原填充色 $C_{\\text{base}}(\\alpha_b)$ 之上时，合成后的不透明度 $\\alpha_{\\text{out}}$ 与预乘还原后的 RGB 分量 $C_{\\text{out}}$ 严格遵循 Porter-Duff ",[14,6433,6434],{},"src-over"," 算子：",[10,6437,6438],{},"$$\n\\alpha_{\\text{out}} = \\alpha_t + \\alpha_b (1 - \\alpha_t), \\qquad\nC_{\\text{out}} = \\frac{C_{\\text{top}},\\alpha_t + C_{\\text{base}},\\alpha_b (1 - \\alpha_t)}{\\alpha_{\\text{out}}}\n$$",[257,6440,6442],{"className":259,"code":6441,"language":261,"meta":262,"style":262},"\u002F\u002F 文件：platform\u002Fsrc\u002Fsrc\u002FInteraction.cc\ncore::Color blendOver(const core::Color& base, const core::Color& top) noexcept {\n    const double aTop = static_cast\u003Cdouble>(top.a) \u002F 255.0;\n    const double aBase = static_cast\u003Cdouble>(base.a) \u002F 255.0;\n    const double outA = aTop + aBase * (1.0 - aTop);\n    if (outA \u003C= 1e-9) return core::Color::Transparent();\n    const auto mixChannel = [&](std::uint8_t b, std::uint8_t t) -> std::uint8_t {\n        const double v = (static_cast\u003Cdouble>(t) * aTop +\n                          static_cast\u003Cdouble>(b) * aBase * (1.0 - aTop)) \u002F outA;\n        return static_cast\u003Cstd::uint8_t>(std::clamp(std::round(v), 0.0, 255.0));\n    };\n    return {mixChannel(base.r, top.r), mixChannel(base.g, top.g), mixChannel(base.b, top.b),\n            static_cast\u003Cstd::uint8_t>(std::clamp(std::round(outA * 255.0), 0.0, 255.0))};\n}\n",[14,6443,6444,6449,6496,6520,6542,6572,6604,6651,6677,6705,6749,6753,6776,6822],{"__ignoreMap":262},[266,6445,6446],{"class":211,"line":268},[266,6447,6448],{"class":271},"\u002F\u002F 文件：platform\u002Fsrc\u002Fsrc\u002FInteraction.cc\n",[266,6450,6451,6453,6455,6457,6460,6462,6464,6466,6468,6470,6472,6475,6477,6479,6481,6483,6485,6487,6490,6492,6494],{"class":211,"line":275},[266,6452,1110],{"class":282},[266,6454,508],{"class":286},[266,6456,1557],{"class":282},[266,6458,6459],{"class":282}," blendOver",[266,6461,287],{"class":286},[266,6463,357],{"class":278},[266,6465,505],{"class":282},[266,6467,508],{"class":286},[266,6469,1557],{"class":282},[266,6471,293],{"class":278},[266,6473,6474],{"class":296}," base",[266,6476,1047],{"class":286},[266,6478,357],{"class":278},[266,6480,505],{"class":282},[266,6482,508],{"class":286},[266,6484,1557],{"class":282},[266,6486,293],{"class":278},[266,6488,6489],{"class":296}," top",[266,6491,300],{"class":286},[266,6493,3706],{"class":278},[266,6495,306],{"class":286},[266,6497,6498,6500,6502,6505,6507,6510,6513,6515,6518],{"class":211,"line":309},[266,6499,327],{"class":278},[266,6501,1629],{"class":278},[266,6503,6504],{"class":286}," aTop ",[266,6506,336],{"class":278},[266,6508,6509],{"class":278}," static_cast\u003Cdouble>",[266,6511,6512],{"class":286},"(top.a) ",[266,6514,4275],{"class":278},[266,6516,6517],{"class":691}," 255.0",[266,6519,969],{"class":286},[266,6521,6522,6524,6526,6529,6531,6533,6536,6538,6540],{"class":211,"line":315},[266,6523,327],{"class":278},[266,6525,1629],{"class":278},[266,6527,6528],{"class":286}," aBase ",[266,6530,336],{"class":278},[266,6532,6509],{"class":278},[266,6534,6535],{"class":286},"(base.a) ",[266,6537,4275],{"class":278},[266,6539,6517],{"class":691},[266,6541,969],{"class":286},[266,6543,6544,6546,6548,6551,6553,6555,6557,6559,6561,6563,6566,6569],{"class":211,"line":324},[266,6545,327],{"class":278},[266,6547,1629],{"class":278},[266,6549,6550],{"class":286}," outA ",[266,6552,336],{"class":278},[266,6554,6504],{"class":286},[266,6556,2037],{"class":278},[266,6558,6528],{"class":286},[266,6560,375],{"class":278},[266,6562,354],{"class":286},[266,6564,6565],{"class":691},"1.0",[266,6567,6568],{"class":278}," -",[266,6570,6571],{"class":286}," aTop);\n",[266,6573,6574,6576,6579,6581,6583,6585,6587,6589,6591,6593,6595,6597,6599,6602],{"class":211,"line":348},[266,6575,667],{"class":278},[266,6577,6578],{"class":286}," (outA ",[266,6580,1652],{"class":278},[266,6582,1393],{"class":691},[266,6584,4741],{"class":278},[266,6586,4744],{"class":691},[266,6588,300],{"class":286},[266,6590,1586],{"class":278},[266,6592,505],{"class":282},[266,6594,508],{"class":286},[266,6596,1557],{"class":282},[266,6598,508],{"class":286},[266,6600,6601],{"class":282},"Transparent",[266,6603,534],{"class":286},[266,6605,6606,6608,6610,6613,6615,6618,6620,6623,6625,6627,6630,6632,6634,6636,6638,6640,6643,6646,6649],{"class":211,"line":366},[266,6607,327],{"class":278},[266,6609,330],{"class":278},[266,6611,6612],{"class":286}," mixChannel ",[266,6614,336],{"class":278},[266,6616,6617],{"class":286}," [",[266,6619,293],{"class":278},[266,6621,6622],{"class":286},"](",[266,6624,1036],{"class":282},[266,6626,508],{"class":286},[266,6628,6629],{"class":278},"uint8_t",[266,6631,4173],{"class":296},[266,6633,1047],{"class":286},[266,6635,1036],{"class":282},[266,6637,508],{"class":286},[266,6639,6629],{"class":278},[266,6641,6642],{"class":296}," t",[266,6644,6645],{"class":286},") -> ",[266,6647,6648],{"class":278},"std::uint8_t",[266,6650,306],{"class":286},[266,6652,6653,6655,6657,6660,6662,6664,6667,6670,6672,6674],{"class":211,"line":381},[266,6654,3042],{"class":278},[266,6656,1629],{"class":278},[266,6658,6659],{"class":286}," v ",[266,6661,336],{"class":278},[266,6663,354],{"class":286},[266,6665,6666],{"class":278},"static_cast\u003Cdouble>",[266,6668,6669],{"class":286},"(t) ",[266,6671,375],{"class":278},[266,6673,6504],{"class":286},[266,6675,6676],{"class":278},"+\n",[266,6678,6679,6682,6685,6687,6689,6691,6693,6695,6697,6700,6702],{"class":211,"line":387},[266,6680,6681],{"class":278},"                          static_cast\u003Cdouble>",[266,6683,6684],{"class":286},"(b) ",[266,6686,375],{"class":278},[266,6688,6528],{"class":286},[266,6690,375],{"class":278},[266,6692,354],{"class":286},[266,6694,6565],{"class":691},[266,6696,6568],{"class":278},[266,6698,6699],{"class":286}," aTop)) ",[266,6701,4275],{"class":278},[266,6703,6704],{"class":286}," outA;\n",[266,6706,6707,6709,6711,6713,6715,6718,6720,6722,6724,6727,6729,6731,6733,6736,6739,6741,6743,6746],{"class":211,"line":394},[266,6708,3491],{"class":278},[266,6710,5625],{"class":278},[266,6712,1036],{"class":282},[266,6714,508],{"class":286},[266,6716,6717],{"class":278},"uint8_t>",[266,6719,287],{"class":286},[266,6721,1036],{"class":282},[266,6723,508],{"class":286},[266,6725,6726],{"class":282},"clamp",[266,6728,287],{"class":286},[266,6730,1036],{"class":282},[266,6732,508],{"class":286},[266,6734,6735],{"class":282},"round",[266,6737,6738],{"class":286},"(v), ",[266,6740,2655],{"class":691},[266,6742,1047],{"class":286},[266,6744,6745],{"class":691},"255.0",[266,6747,6748],{"class":286},"));\n",[266,6750,6751],{"class":211,"line":400},[266,6752,3623],{"class":286},[266,6754,6755,6757,6760,6763,6766,6768,6771,6773],{"class":211,"line":413},[266,6756,3525],{"class":278},[266,6758,6759],{"class":286}," {",[266,6761,6762],{"class":282},"mixChannel",[266,6764,6765],{"class":286},"(base.r, top.r), ",[266,6767,6762],{"class":282},[266,6769,6770],{"class":286},"(base.g, top.g), ",[266,6772,6762],{"class":282},[266,6774,6775],{"class":286},"(base.b, top.b),\n",[266,6777,6778,6781,6783,6785,6787,6789,6791,6793,6795,6797,6799,6801,6803,6806,6808,6810,6813,6815,6817,6819],{"class":211,"line":440},[266,6779,6780],{"class":278},"            static_cast\u003C",[266,6782,1036],{"class":282},[266,6784,508],{"class":286},[266,6786,6717],{"class":278},[266,6788,287],{"class":286},[266,6790,1036],{"class":282},[266,6792,508],{"class":286},[266,6794,6726],{"class":282},[266,6796,287],{"class":286},[266,6798,1036],{"class":282},[266,6800,508],{"class":286},[266,6802,6735],{"class":282},[266,6804,6805],{"class":286},"(outA ",[266,6807,375],{"class":278},[266,6809,6517],{"class":691},[266,6811,6812],{"class":286},"), ",[266,6814,2655],{"class":691},[266,6816,1047],{"class":286},[266,6818,6745],{"class":691},[266,6820,6821],{"class":286},"))};\n",[266,6823,6824],{"class":211,"line":462},[266,6825,745],{"class":286},[10,6827,6828,6834,6835,6838],{},[19,6829,6830,6831,236],{},"(2) 绕局部包围盒中心的复合仿射缩放矩阵（",[14,6832,6833],{},"interactionTransform","\n当节点在悬停或按下时配置了缩放因子 $s \\ne 1$（例如 ",[14,6836,6837],{},"scale = 1.05"," 放大 5%）与位移偏移 $\\mathbf{t}_{\\text{offset}}$ 时，若直接乘以缩放矩阵 $S(s, s)$，节点会以局部坐标系原点 $(0, 0)$ 为中心向外膨胀，导致图元视觉中心向右下角漂移。\n正确的做法是以节点局部包围盒中心 $\\mathbf{c} = \\bigl(b_x + \\frac{b_w}{2},; b_y + \\frac{b_h}{2}\\bigr)$ 为不动点构造共轭变换：",[10,6840,6841],{},"$$\nM_{\\text{interact}} = M_{\\text{base}} \\cdot T(\\mathbf{c} + \\mathbf{t}_{\\text{offset}}) \\cdot S(s, s) \\cdot T(-\\mathbf{c})\n$$",[10,6843,6844],{},"按列向量右乘顺序解读：先由 $T(-\\mathbf{c})$ 将图元几何中心平移至原点，再由 $S(s, s)$ 原地均匀缩放，最后由 $T(\\mathbf{c} + \\mathbf{t}_{\\text{offset}})$ 平移回原中心并叠加交互位移：",[257,6846,6848],{"className":259,"code":6847,"language":261,"meta":262,"style":262},"\u002F\u002F 文件：platform\u002Fsrc\u002Fsrc\u002FInteraction.cc\nmath::Affine2D interactionTransform(const core::SceneNode& node, const math::Affine2D& base,\n                                    double scale, const core::Vec2d& offset) noexcept {\n    const core::RectD b = node.bounds();\n    const core::Vec2d center = b.isEmpty()\n                                   ? core::Vec2d{0.0, 0.0}\n                                   : core::Vec2d{b.x + b.width * 0.5, b.y + b.height * 0.5};\n    return base * math::Affine2D::Translate(center + offset) *\n           math::Affine2D::Scale(scale, scale) *\n           math::Affine2D::Translate({-center.x, -center.y});\n}\n",[14,6849,6850,6854,6895,6924,6944,6967,6985,7019,7050,7069,7093],{"__ignoreMap":262},[266,6851,6852],{"class":211,"line":268},[266,6853,6448],{"class":271},[266,6855,6856,6858,6860,6862,6865,6867,6869,6871,6873,6875,6877,6879,6881,6883,6885,6887,6889,6891,6893],{"class":211,"line":275},[266,6857,2356],{"class":282},[266,6859,508],{"class":286},[266,6861,3350],{"class":282},[266,6863,6864],{"class":282}," interactionTransform",[266,6866,287],{"class":286},[266,6868,357],{"class":278},[266,6870,505],{"class":282},[266,6872,508],{"class":286},[266,6874,788],{"class":282},[266,6876,293],{"class":278},[266,6878,1497],{"class":296},[266,6880,1047],{"class":286},[266,6882,357],{"class":278},[266,6884,4253],{"class":282},[266,6886,508],{"class":286},[266,6888,3350],{"class":282},[266,6890,293],{"class":278},[266,6892,6474],{"class":296},[266,6894,1181],{"class":286},[266,6896,6897,6900,6903,6905,6907,6909,6911,6913,6915,6918,6920,6922],{"class":211,"line":309},[266,6898,6899],{"class":278},"                                    double",[266,6901,6902],{"class":296}," scale",[266,6904,1047],{"class":286},[266,6906,357],{"class":278},[266,6908,505],{"class":282},[266,6910,508],{"class":286},[266,6912,3782],{"class":282},[266,6914,293],{"class":278},[266,6916,6917],{"class":296}," offset",[266,6919,300],{"class":286},[266,6921,3706],{"class":278},[266,6923,306],{"class":286},[266,6925,6926,6928,6930,6932,6934,6936,6938,6940,6942],{"class":211,"line":315},[266,6927,327],{"class":278},[266,6929,505],{"class":282},[266,6931,508],{"class":286},[266,6933,3675],{"class":282},[266,6935,4193],{"class":286},[266,6937,336],{"class":278},[266,6939,1618],{"class":286},[266,6941,5249],{"class":282},[266,6943,534],{"class":286},[266,6945,6946,6948,6950,6952,6954,6957,6959,6962,6964],{"class":211,"line":324},[266,6947,327],{"class":278},[266,6949,505],{"class":282},[266,6951,508],{"class":286},[266,6953,3782],{"class":282},[266,6955,6956],{"class":286}," center ",[266,6958,336],{"class":278},[266,6960,6961],{"class":286}," b.",[266,6963,5254],{"class":282},[266,6965,6966],{"class":286},"()\n",[266,6968,6969,6972,6974,6977,6979,6981,6983],{"class":211,"line":348},[266,6970,6971],{"class":278},"                                   ?",[266,6973,505],{"class":282},[266,6975,6976],{"class":286},"::Vec2d{",[266,6978,2655],{"class":691},[266,6980,1047],{"class":286},[266,6982,2655],{"class":691},[266,6984,745],{"class":286},[266,6986,6987,6990,6992,6995,6997,7000,7002,7005,7008,7010,7013,7015,7017],{"class":211,"line":366},[266,6988,6989],{"class":278},"                                   :",[266,6991,505],{"class":282},[266,6993,6994],{"class":286},"::Vec2d{b.x ",[266,6996,2037],{"class":278},[266,6998,6999],{"class":286}," b.width ",[266,7001,375],{"class":278},[266,7003,7004],{"class":691}," 0.5",[266,7006,7007],{"class":286},", b.y ",[266,7009,2037],{"class":278},[266,7011,7012],{"class":286}," b.height ",[266,7014,375],{"class":278},[266,7016,7004],{"class":691},[266,7018,2543],{"class":286},[266,7020,7021,7023,7026,7028,7030,7032,7034,7036,7039,7042,7044,7047],{"class":211,"line":381},[266,7022,3525],{"class":278},[266,7024,7025],{"class":286}," base ",[266,7027,375],{"class":278},[266,7029,4253],{"class":282},[266,7031,508],{"class":286},[266,7033,3350],{"class":282},[266,7035,508],{"class":286},[266,7037,7038],{"class":282},"Translate",[266,7040,7041],{"class":286},"(center ",[266,7043,2037],{"class":278},[266,7045,7046],{"class":286}," offset) ",[266,7048,7049],{"class":278},"*\n",[266,7051,7052,7055,7057,7059,7061,7064,7067],{"class":211,"line":387},[266,7053,7054],{"class":282},"           math",[266,7056,508],{"class":286},[266,7058,3350],{"class":282},[266,7060,508],{"class":286},[266,7062,7063],{"class":282},"Scale",[266,7065,7066],{"class":286},"(scale, scale) ",[266,7068,7049],{"class":278},[266,7070,7071,7073,7075,7077,7079,7081,7083,7085,7088,7090],{"class":211,"line":394},[266,7072,7054],{"class":282},[266,7074,508],{"class":286},[266,7076,3350],{"class":282},[266,7078,508],{"class":286},[266,7080,7038],{"class":282},[266,7082,2652],{"class":286},[266,7084,3562],{"class":278},[266,7086,7087],{"class":286},"center.x, ",[266,7089,3562],{"class":278},[266,7091,7092],{"class":286},"center.y});\n",[266,7094,7095],{"class":211,"line":400},[266,7096,745],{"class":286},[771,7098,7100,7101,236],{"id":7099},"_4-二阶阻尼弹簧的闭式解析解springcc","4. 二阶阻尼弹簧的闭式解析解（",[42,7102,7104],{"href":7103},"file:\u002F\u002F\u002FE:\u002Fkiriyama_canvas\u002Fmath\u002Fsrc\u002FSpring.cc#L54-L105",[14,7105,7106],{},"Spring.cc",[10,7108,7109,7110,7112],{},"物理交互动画若采用显式欧拉积分（$v \\leftarrow v + a\\Delta t$），在浏览器后台切回导致 $\\Delta t$ 突增时会引发数值爆炸。",[14,7111,7106],{}," 直接求解二阶齐次线性常微分方程：",[10,7114,7115],{},"$$\nm\\ddot{x} + c\\dot{x} + k(x - x_{\\text{target}}) = 0\n$$",[10,7117,7118],{},"令初始位移偏差 $d_0 = x_0 - x_{\\text{target}}$，固有角频率 $\\omega_0 = \\sqrt{k \u002F m}$，阻尼比 $\\zeta = \\frac{c}{2\\sqrt{km}}$，按 $\\zeta$ 的三种形态直接代入时间步长 $\\Delta t$ 的解析解 $x(\\Delta t)$ 与导数 $v(\\Delta t) = \\dot{x}(\\Delta t)$：",[2266,7120,7121],{},[1918,7122,7123,7126],{},[19,7124,7125],{},"欠阻尼振荡（$\\zeta \u003C 1$，带丝滑回弹）","：令衰减率 $\\alpha = \\zeta\\omega_0$，阻尼角频率 $\\omega_d = \\omega_0\\sqrt{1 - \\zeta^2}$，则：",[10,7128,7129],{},"$$\nd(\\Delta t) = e^{-\\alpha \\Delta t} \\left( d_0 \\cos(\\omega_d \\Delta t) + \\frac{v_0 + \\alpha d_0}{\\omega_d} \\sin(\\omega_d \\Delta t) \\right)\n$$",[2266,7131,7132],{},[1918,7133,7134,1458],{},[19,7135,7136],{},"临界阻尼（$\\zeta \\approx 1$，最快无超调收敛）",[10,7138,7139],{},"$$\nd(\\Delta t) = \\bigl(d_0 + (v_0 + \\omega_0 d_0)\\Delta t\\bigr) e^{-\\omega_0 \\Delta t}\n$$",[10,7141,7142],{},"由于解析解中包含指数衰减项 $e^{-\\alpha \\Delta t}$，即便页面挂起导致 $\\Delta t = 10\\text{ s}$，计算结果也精确收敛于 $0$，在任意帧率下具备无条件数值稳定性。",[1291,7144,7145],{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":262,"searchDepth":275,"depth":275,"links":7147},[7148,7157,7164],{"id":33,"depth":275,"text":34,"children":7149},[7150,7152,7154,7156],{"id":232,"depth":309,"text":7151},"1.1 总控管线代码（CanvasHost::onFrame）",{"id":750,"depth":309,"text":7153},"1.2 阶段二拆解：组件的双轨更新机制（rebuild 慢路径 vs $O(1)$ 快路径）",{"id":1438,"depth":309,"text":7155},"1.3 阶段三拆解：场景树递归遍历与语义色解析（SceneDraw.cc）",{"id":1900,"depth":309,"text":1901},{"id":2342,"depth":275,"text":2343,"children":7158},[7159,7160,7162],{"id":2346,"depth":309,"text":2347},{"id":2383,"depth":309,"text":7161},"2.2 core 与 scene 的职责分离：纯函数构建器 + 轻量交互外壳",{"id":2773,"depth":309,"text":7163},"2.3 虚拟场景树对账模块（SceneDiff::reconcileScene）",{"id":3318,"depth":275,"text":3319,"children":7165},[7166,7167,7168],{"id":3337,"depth":309,"text":3338},{"id":3878,"depth":309,"text":3879},{"id":5716,"depth":309,"text":5717},"本笔记用于拆解 kiriyama_canvas（C++23 + SDL3 + WebAssembly 2D 图表与画布组件库）的架构设计。全文采用精简文本 + 核心代码块 + 数学推导的形式，按 “具体渲染与更新管线 → 分层与模块设计 → 点击测试与交互模块的数学实现” 展开。","md",{"date":7172,"tags":7173},"2026-10-21",[7174,7175,7176,7177,2356],"architecture","sdl","canvas","wasm","\u002Farchitecture\u002Fkiriyama_canvas的架构设计思考",{"description":7169},"architecture\u002Fkiriyama_canvas的架构设计思考","9V7biKHW9KaQowcZzY2_pq0rqBi3WS8jJmCzTffpgAo",1792775383721]