Use a single WASM compute strategy to reduce mobile-inherent bottlenecks while preserving deterministic behavior across desktop web, Tauri desktop, Capacitor mobile, and Tauri Android runtimes.
- Main-thread contention during heavy graph/layout compute can freeze interaction.
- Worker startup + JS serialization overhead can dominate on mobile CPUs for sparse graphs.
- Memory pressure and GC spikes increase crash/jank probability on constrained devices.
- Capability variance across WebView runtimes creates nondeterministic behavior without explicit probes.
- One capability contract:
- Runtime exposes
supports_mobile_wasm_computeandmobile_wasm_reason. - Routing remains deterministic with explicit fallback reason tracking.
- Runtime exposes
- One compute routing model:
- Preferred:
wasm-adapter - Fallback:
worker - Final fallback:
single-thread
- Preferred:
- One artifact governance path:
- Canonical WASM artifact probe + strict gate scripts + CI regression barriers.
- Phase A (Capability and Diagnostics) [Completed baseline]:
- Add runtime probe for mobile WASM readiness.
- Expose capability and reason in runtime caps.
- Keep existing behavior unchanged if capability is unavailable.
- Phase B (Routing Integration) [Active]:
- Thread mobile capability signal into on-device build stats.
- Add build-mode detail tags for mobile telemetry (
worker-wasm-ready,worker-wasm-not-ready, fallback reasons). - Keep deterministic fallback behavior.
- Phase C (Kernel Expansion):
- Move additional heavy kernels to WASM where correctness is contract-proven.
- Prioritize graph build hot spots that currently consume most mobile CPU time.
- Phase D (Artifact Provisioning per Terminal):
- Validate artifact packaging for:
- desktop web bundle
- Tauri desktop sidecar/runtime paths
- Capacitor mobile asset/runtime paths
- Tauri Android runtime paths
- Validate artifact packaging for:
- Phase E (Performance and Stability Hard Gates):
- Enforce p95/p99 guardrails for mobile-oriented workloads.
- Enforce no-regression equivalence contracts between worker and WASM output.
- Runtime can always explain why WASM is enabled/disabled on mobile (
mobile_wasm_reason). - Mobile build path remains functional when WASM is unavailable (deterministic fallback verified).
- Migration gate suite remains fully green after each routing change.
- Bilingual docs remain synchronized for all plan/TODO/test-report updates.
通过统一的 WASM 计算策略,缓解移动端固有瓶颈,并在桌面 Web、Tauri 桌面、Capacitor 移动端、Tauri Android 多终端之间保持可预测的一致行为。
- 重图计算/布局计算容易占用主线程,导致交互卡顿。
- 在稀疏图场景下,Worker 启动与 JS 序列化开销可能高于实际计算收益。
- 受限设备上内存压力与 GC 抖动更明显,稳定性风险更高。
- 不同 WebView 运行时能力差异较大,若缺少显式探测会造成行为不确定。
- 统一能力契约:
- 运行时暴露
supports_mobile_wasm_compute与mobile_wasm_reason。 - 计算路由保留明确的回退原因,保证可诊断性。
- 运行时暴露
- 统一计算路由模型:
- 首选:
wasm-adapter - 回退:
worker - 最终回退:
single-thread
- 首选:
- 统一工件治理链路:
- 标准 WASM 工件探针 + 严格门禁脚本 + CI 回归屏障。
- 阶段 A(能力探测与诊断)[基线已完成]:
- 增加移动端 WASM 就绪探测。
- 在 runtime caps 中暴露能力与原因。
- 能力不可用时保持既有行为不变。
- 阶段 B(路由集成)[进行中]:
- 将移动端 WASM 能力信号接入本地构建统计。
- 增加移动构建模式细分标签(
worker-wasm-ready、worker-wasm-not-ready、回退原因)。 - 保持确定性回退链路。
- 阶段 C(内核扩展):
- 将更多重计算内核迁移到 WASM,并以契约验证正确性。
- 优先处理当前移动端 CPU 占用最高的图构建热点。
- 阶段 D(多终端工件落地):
- 分别验证以下终端的工件打包与加载路径:
- 桌面 Web 资源包
- Tauri 桌面 sidecar/运行时路径
- Capacitor 移动端资源/运行时路径
- Tauri Android 运行时路径
- 分别验证以下终端的工件打包与加载路径:
- 阶段 E(性能与稳定性硬门禁):
- 对移动端典型负载执行 p95/p99 门禁约束。
- 强制 worker 与 WASM 输出一致性无回归。
- 移动端必须能明确解释 WASM 启用/禁用原因(
mobile_wasm_reason)。 - WASM 不可用时移动端构建链路仍可工作(确定性回退已验证)。
- 每一轮路由调整后,迁移门禁套件保持全绿。
- 所有计划/TODO/测试报告的中英文文档保持同步更新。
This update aligns the implementation plan with the current Electron-to-Tauri migration strategy:
- Tauri as the primary desktop shell.
- Godot as the Path Mode interactive surface.
- Node sidecar as the graph build and runtime service.
- Bridge-first message flow (
Godot <-> PathBridge <-> Backend) as the default path.
- Runtime path unification for sidecar execution and frontend asset resolution has been integrated across desktop runtime paths.
- Worker path resolution has been stabilized for packaged sidecar execution to avoid
MODULE_NOT_FOUNDin worker threads. - Knowledge Base folder loading is now anchored to the configured project root path and no longer depends on Electron-only assumptions.
- The
Path Modeconfiguration migration has moved core controls into Godot-side UI while preserving browser toolbar behavior for browser mode.
- Cache-exists decision flow still requires strict regression verification in Tauri mini GPU runs to ensure users are prompted to reuse or rebuild.
- Duplicate load cycles must remain guarded to prevent repeated build/restore actions after a single user click.
- WebSocket client lifecycle still needs hardening to avoid redundant early connect/disconnect churn under startup timing races.
- History tracking for center-node switches in Godot requires final behavioral verification.
- Lock cache prompt + single-execution semantics with dedicated regression tests.
- Finalize websocket lifecycle guard rails and startup sequencing.
- Complete task-level parity checks for Electron IPC replacements and remove remaining implicit Electron dependencies.
- Keep dual-output mobile strategy: maintain Capacitor output while also enabling Tauri Android build path.
本次更新将实施计划与当前 Electron 到 Tauri 的迁移策略对齐:
- 以 Tauri 作为桌面主壳层。
- 以 Godot 作为 Path Mode 交互界面。
- 以 Node Sidecar 作为图构建与运行时服务。
- 默认采用 Bridge-first 消息链路(
Godot <-> PathBridge <-> Backend)。
- 已完成 Sidecar 运行路径与前端资源路径的统一,提升桌面运行一致性。
- 已稳定 Worker 路径解析,避免打包 Sidecar 下线程出现
MODULE_NOT_FOUND。 - Knowledge Base 文件夹加载已锚定到配置的项目根路径,不再依赖 Electron 专属假设。
Path Mode关键配置已迁移到 Godot 侧 UI,同时保留浏览器模式下 Web 工具栏行为。
- 在 Tauri mini GPU 运行中,缓存存在时“复用或重建”提示流程仍需严格回归验证。
- 需要持续防止单次点击触发重复加载(重复 build/restore)。
- WebSocket 客户端生命周期仍需加固,避免启动阶段时序竞争导致早期重复连接/断开。
- Godot 中心节点切换的 History 记录仍需最终行为验收。
- 通过专用回归测试锁定缓存提示与单次执行语义。
- 完成 websocket 生命周期防护与启动时序收敛。
- 完成 Electron IPC 替代项的逐任务一致性核验,并移除残余隐式 Electron 依赖。
- 保持移动端双输出策略:继续保留 Capacitor,同时并行支持 Tauri Android 产物链路。
The goal is to allow users to investigate incomplete In-Degree information by explicitly expanding the context of a specific node, without overloading the view with the entire graph.
- Unrestricted Context Expansion:
- In diffusionLearning, iterate through
forcedExpansionSet. - For each node in the set, retrieve all incoming edges (getIncomingEdges), regardless of their completion status or relevance to the original path.
- Add the source nodes of these edges to the
finalPathNodeslist. - Constraint: Do not recursively fetch parents of these new nodes (Level -1 only).
- Flagging: Mark the expanded target node with
isExpanded: truein the output.
- In diffusionLearning, iterate through
- Pass State Flags:
- Ensure the
isExpandedflag matches theforcedExpansionSetstate. - Pass this flag to the Godot client in the
treeLayoutpayload.
- Ensure the
- Smart Toggle Logic (Left Side):
- Pre-calculation: At the start of
_draw_layout_mode, iterate_layout_edgesto build avisible_in_countsdictionary (NodeID -> Count). - Decision Logic (in Node Loop):
- Let
global_in=node.inDegree(from backend). - Let
visible_in=visible_in_counts[node.id]. - Let
is_expanded=node.isExpanded(flag from backend).
- Let
- States:
- Expanded State: If
is_expandedis true:- Draw (-) button.
- Click Action: Emit
node_collapse_prereqs_requested.
- Expandable State: Else if
visible_in < global_in:- Draw (+) button.
- Click Action: Emit
node_expand_prereqs_requested.
- Complete State: Else (Visible == Global):
- Draw nothing (or disabled indicator).
- Expanded State: If
- Pre-calculation: At the start of
- Collapse Handling:
- Implement collapsePrereqs(nodeId) in path_app.js: Remove ID from
forcedExpansionSetand trigger update. - Wire up the new Godot signal to this backend method.
- Implement collapsePrereqs(nodeId) in path_app.js: Remove ID from
- Problem: The "Incoming" and "Outgoing" lists in the Node Statistics Popup do not resize proportionally when the popup is resized using the drag handle.
- Fix: Modify src/frontend/styles.css.
- Change
.stat-listsfrom fixedheight: 150pxtoflex: 1; min-height: 150px. - Ensure parent containers (
.popup-content) allow expansion.
- Change
- Problem: Edges are visible by default on load, creating clutter.
- Fix:
- in src/frontend/styles.css: Set
.linkdefaultstroke-opacityto0. - in src/frontend/app.js: Ensure updateVisibility() is called immediately after graph initialization to enforce the visibility logic (hiding edges unless focused/hovered).
- in src/frontend/styles.css: Set
- Problem: The number displayed next to "In-Degree" (Red) in the popup often differs from the count of items in the "Incoming" list.
- Verification: Locate showNodePopup in app.js to see if it uses
node.inDegree(metadata) vsnode.incoming(actual edges). - Fix:
- If the metadata is correct (global truth), keep it.
- If the list is incomplete (due to filtering/culling), add a label "(Visible: X)" or ensure the list matches filters.
- Current hypothesis: The metadata
inDegreeis the ground truth from the backend, while the client-sidelinksarray might be filtered or optimized (limit 20000 edges), causing a mismatch. functionality to show "Total" vs "Visible".
-
Goal: Allow user to toggle between showing "Visible Inbound Nodes" (calculated from current graph) or "Total Statistical Inbound" (from backend metadata).
-
Default: Visible Inbound Nodes.
-
Changes:
- src/frontend/index.html: Add a toggle/select in the Settings Modal (e.g., "Degree Count: Visible | Total").
- src/frontend/app.js:
- Update showNodePopup to check
settingsManager.get('visuals', 'degreeMode'). - If 'visible': Show
inNeighbors.length. - If 'total': Show
node.inDegree(with (visible) suffix if different? Or just strict switch?). User asked for "whether the inbound count should be shown as the number of nodes or the statistical number". I will implement a strict switch but maybe keep the tooltip or subtle indicator if they differ significantly. - Wire up the new setting in initSettingsUI.
- Update showNodePopup to check
-
Simplify Lazy Loading UI (Godot)
- Update tree_renderer.gd:
- Remove separate (+)/(-) buttons.
- Implement unified
[ Count ]button (e.g., circle with number). - Button toggles
forcedExpansionstate. - Default state is collapsed (colored/styled to indicate expandable).
- Ensure path_app.js handles the toggle correctly (clear vs add to
forcedExpansionNodes).
- Update tree_renderer.gd:
-
Tree View Visual & Interaction Overhaul
- Visual Cleanup (Godot)
- Remove (+)/(-) and
[Count]buttons from tree_renderer.gd. - Remove separate click areas for these buttons.
- Remove (+)/(-) and
- Interaction Update (Godot)
- Double Click: Change to Toggle Expansion (Emit expand/collapse).
- Right Click: Toggle Expansion (Same as Dbl Click).
- Middle Click: Collapse All (Emit new signal
collapse_all_requested). - Long Press: Implement Navigation (Switch Central).
- Add
_processcheck for hold duration. - Draw Progress Ring during hold.
- Trigger navigation on completion.
- Add
- Focus Mode (Godot)
- Add "Focus on this node" checkbox to settings_panel.tscn.
- Implement
focus_node_idstate in tree_renderer.gd. - Update
_drawto dim nodes/edges not connected tofocus_node_idwhen enabled.
- Backend Updates
- Add collapseAll handler in path_app.js.
- Visual Cleanup (Godot)
- Disable Path Mode if No Data:
- Update app.js to check
graphDataExistsornodes.lengthbefore entering Path Mode. - Show alert if data missing.
- Update app.js to check
- Fix Missing Edges in Tree Layout:
- Cause:
d3.forceSimulationin app.js mutatesgraphData.links, replacing ID strings with Node Objects. - Effect:
Graph.jsin path_worker.js uses these Objects as keys/IDs, breaking adjacency map lookups (which expect strings). - Fix: In path_app.js, sanitize links before sending to worker:
l.source.id || l.source.
- Cause:
- Initial State Check:
- Navigate to a node with high In-Degree (e.g., "Beta", In-Degree 18).
- Verify the unified
[ Count ]button appears on the left if < 18 lines are visible.
- Expansion Test:
- Click the
[ Count ]button. - Verify the tree rebuilds.
- Verify previously hidden nodes (e.g., "Fair Value") appear as prerequisites.
- Verify the
[ Count ]button changes its state/appearance to indicate expansion.
- Click the
- Collapse Test:
- Click (-).
- Verify the extra nodes disappear and view returns to original state.
- Fix Missing Edges in Tree Layout:
- Fix: In path_app.js, sanitize links before sending to worker.
- Fix Tree View Interactions:
- Right-Click Toggle: Ensure
isExpandedis passed from path_core.js to visual node. - Collapse All: Update PathBridge.ts to relay collapseAll message. Add UI button.
- Right-Click Toggle: Ensure
Implement a stable, tree-like layout where the "Main Learning Path" (Spine) remains linear and stationary, while prerequisites (Tributaries) expand laterally without disrupting the spine.
- Step 1: Identify Spine:
- Determine the "Critical Path" from
learningPath.nodes(usingisCriticalflag or diffusionLearning result). - Assign
Level(X-coordinate) to Spine nodes based on distance from Start. - Fix Spine
Ycoordinates to0.
- Determine the "Critical Path" from
- Step 2: Slot Management:
- Create a
SlotManagerto track occupied (X, Y) positions. - Mark key Spine positions as occupied.
- Create a
- Step 3: Lateral Expansion:
- Iterate through nodes in Topological Order (or Spine Order).
- For each node
N, identify its unplaced prerequisitesP. - Placement Logic:
Target X:N.Level - 1(Standard dependency inflow).Target Y: Find nearest available vertical slot relative toN.Y.- Preference: Alternating Up/Down (
+1, -1, +2, -2...) *Y_SPACING. - Stability: Once placed, a node's position is locked (
isPlaced = true) and will not be moved by subsequent expansions.
- Ensure switchCentral triggers a re-layout using the new algorithm.
- Pass the stable layout to
Graph.js/ Godot via PathBridge.
- Spine Stability:
- Load a path. Center on a Spine node.
- Expand a prerequisite.
- Verify the Spine node DOES NOT move.
- Lateral Unfolding:
- Verify prerequisites appear above/below the spine, not inline.
- Complex Chain:
- Expand a prerequisite's prerequisite.
- Verify it flows backwards (Left) and finds a clear slot.
目标是允许用户通过显式扩展特定节点的上下文来调查不完整的入度信息,而无需加载整个图表。
- 无限制上下文扩展:
- 在 diffusionLearning 中,迭代
forcedExpansionSet。 - 对于集合中的每个节点,检索 所有 入边 (getIncomingEdges),无论其完成状态或与原始路径的相关性如何。
- 将这些边缘的源节点添加到
finalPathNodes列表中。 - 约束: 不要递归获取这些新节点的父节点(仅限 Level -1)。
- 标记: 在输出中用
isExpanded: true标记已扩展的目标节点。
- 在 diffusionLearning 中,迭代
- 传递状态标志:
- 确保
isExpanded标志与forcedExpansionSet状态匹配。 - 在
treeLayout以此传递此标志给 Godot 客户端。
- 确保
- 智能切换逻辑 (左侧):
- 预计算: 在
_draw_layout_mode开始时,迭代_layout_edges以构建visible_in_counts字典。 - 决策逻辑:
global_in=node.inDegree(来自后端)。visible_in=visible_in_counts[node.id].is_expanded=node.isExpanded.
- 状态:
- 已展开: 如果
is_expanded为真:绘制 (-) 按钮。点击发射node_collapse_prereqs_requested。 - 可展开: 否则如果
visible_in < global_in:绘制 (+) 按钮。点击发射node_expand_prereqs_requested。 - 完整: 否则(可见 == 全局):绘制无(或禁用)。
- 已展开: 如果
- 预计算: 在
- 折叠处理:
- 在 path_app.js 中实现 collapsePrereqs(nodeId):从
forcedExpansionSet中移除 ID 并触发更新。 - 将新的 Godot 信号连接到此后端方法。
- 在 path_app.js 中实现 collapsePrereqs(nodeId):从
- 修复: 修改 src/frontend/styles.css,使用
flex: 1确保列表按比例调整大小。
- 修复: 默认隐藏边缘,仅在悬停/聚焦时显示。
- 修复: 添加设置以切换“可见”与“总计”入度显示。
实现稳定的树状布局,其中“主要学习路径”(主干)保持线性和静止,而前置节点(支流)在不破坏主干的情况下横向扩展。
- 步骤 1: 识别主干:
- 从
learningPath.nodes确定“关键路径”。 - 根据与起点的距离为主干节点分配
Level(X坐标)。 - 将主干
Y坐标固定为0。
- 从
- 步骤 2: 插槽管理:
- 创建
SlotManager以跟踪占用的 (X, Y) 位置。 - 标记关键主干位置为已占用。
- 创建
- 步骤 3: 横向扩展:
- 按 拓扑顺序 迭代节点。
- 对于每个节点
N,识别其未放置的前置节点P。 - 放置逻辑:
Target X:N.Level - 1。Target Y: 相对于N.Y找到最近的可用垂直插槽。- 偏好: 交替上/下 (
+1, -1, +2, -2...) *Y_SPACING。 - 稳定性: 节点一旦放置,其位置即被锁定 (
isPlaced = true)。
- 确保 switchCenter 使用新算法触发重新布局。
- 通过 PathBridge 将稳定布局传递给 Godot。
- 主干稳定性: 加载路径,居中主干节点,展开前置节点。验证主干节点 不移动。
- 横向展开: 验证前置节点出现在主干的上方/下方,而不是内联。
- 复杂链: 展开前置的前置,验证其向后(左)流动并找到清晰的插槽。
Date: 2026-02-26
Port the 9-rule expansion/claiming/visibility engine from tree_path_mockup.html into production code (path_core.js, tree_renderer.gd, path_app.js). This replaces the simple contour-based layout with a full ownership/claiming system for intelligent node management.
| # | Rule | Mockup Function | Production Status |
|---|---|---|---|
| 1 | Expansion Order (FIFO claiming) | processExpansions() + expansionOrder[] |
❌ Missing |
| 2 | Preceding Immunity (effective index) | tryClaim() + getEffectiveSpineIndex() |
❌ Missing |
| 3 | Following Migration (spine+followers) | claimSpineChain() |
❌ Missing |
| 4 | Single Appearance (owner-based) | currentOwner priority check |
placedNodeIds) |
| 5 | Cross-Tributary Isolation (edge filter) | drawEdges() owner check |
❌ Missing |
| 6 | Spine Always Visible (return on collapse) | determineVisibility() spine pass |
❌ Missing |
| 7 | Sticky Claim (configurable) | stickyClaimEnabled toggle |
❌ Missing |
| 8 | Unit Migration (recursive claim) | claim() recursive tributaries |
❌ Missing |
| 9 | Tributary Hierarchy Immunity | getTributaryRootSpineIndex() |
❌ Missing |
| Concept | Mockup | Production |
|---|---|---|
| Node Ownership | currentOwner, ownerPriority |
None |
| Expansion Order | expansionOrder[] (ordered) |
forcedExpansionNodes (unordered Set) |
| Effective Spine Index | getEffectiveSpineIndex() |
Fixed spineIndex only |
| Visibility Chain | isOwnerChainVisible() recursive |
Binary collapsed/expanded |
| Hull-Node Avoidance | Convex hull with padding | Basic hull, no collision check |
- ✅ Spine identification via
isCriticalflag - ✅ Contour-based collision avoidance for spine spacing
- ✅ Recursive tributary placement
- ✅ Hull/bubble drawing around tributary groups
- ✅ Collapsed/expanded state per node
- ✅ Godot WebSocket bridge communication
- ✅ Tree renderer with bezier edges, styled nodes, pan/zoom
Step 1: Add expansion order tracking to getTreeLayout() (L742-1133)
- Add
expansionOrderparameter (ordered array of expanded node IDs) - Replace unordered
collapsedSetwith orderedexpansionOrderfor FIFO claiming
Step 2: Implement node ownership system
- Add
currentOwner,ownerPriority,_isOnSpineto each layout node - Track claims during
processExpansions()matching mockup logic
Step 3: Implement tryClaim() with all 9 rules
- Rule 1: Owner priority check
- Rule 2:
getEffectiveSpineIndex()comparison (inherits owner index) - Rule 3+8:
claimSpineChain()for following migration - Rule 4: Single appearance via owner check
- Rule 5: Cross-tributary edge filtering
- Rule 6: Spine always visible on collapse
- Rule 7: Sticky claim toggle
- Rule 9:
getTributaryRootSpineIndex()for hierarchy immunity
Step 4: Implement determineVisibility() + isOwnerChainVisible()
- Two-pass: spine always visible, non-spine follows recursive owner chain
Step 5: Update edge generation — filter edges between different owners (Rule 5)
Step 6: Update hull generation to group by owner
Step 7: Track expansion ORDER (not just Set)
forcedExpansionNodes: new Set()→expansionOrder: []- Update
expandPrereqs(),collapsePrereqs(),collapseAll() - Pass
expansionOrderto worker
Step 8: Add sticky claim setting + pass to worker
Step 9: Edge rendering — skip edges where src.currentOwner != tgt.currentOwner
Step 10: Hull collision avoidance with rounded padding
Step 11: Node type coloring (spine=green, tributary=blue, shared=purple, migrated=orange)
Step 12: Expansion indicator badge (in-degree count circle)
Step 13: Pass expansionOrder and stickyClaimEnabled to getTreeLayout()
- Expand Calculus → verify Optimization migrates (Rule 3)
- Expand Optimization → verify Diff Eq cannot claim Calculus (Rule 2+9)
- Collapse Calculus → verify spine nodes return (Rule 6)
- Toggle sticky claim → verify non-spine revert/persist (Rule 7)
- Check hull boundaries don't overlap nodes
日期: 2026-02-26
将 tree_path_mockup.html 中的 9 规则展开/认领/可见性引擎移植到生产代码(path_core.js、tree_renderer.gd、path_app.js)中。用完整的所有权/认领系统替换简单的基于轮廓的布局。
| # | 规则 | 原型函数 | 生产代码状态 |
|---|---|---|---|
| 1 | 展开顺序(FIFO 认领) | processExpansions() |
❌ 缺失 |
| 2 | 前置免疫(有效索引) | tryClaim() + getEffectiveSpineIndex() |
❌ 缺失 |
| 3 | 后续迁移(脊柱+后续) | claimSpineChain() |
❌ 缺失 |
| 4 | 单次出现(基于所有者) | currentOwner 优先级检查 |
|
| 5 | 跨支流隔离(边过滤) | drawEdges() 所有者检查 |
❌ 缺失 |
| 6 | 脊柱始终可见(折叠时返回) | determineVisibility() |
❌ 缺失 |
| 7 | 粘性认领(可配置) | stickyClaimEnabled 开关 |
❌ 缺失 |
| 8 | 单元迁移(递归认领) | claim() 递归支流 |
❌ 缺失 |
| 9 | 支流层级免疫 | getTributaryRootSpineIndex() |
❌ 缺失 |
| 概念 | 原型 | 生产代码 |
|---|---|---|
| 节点所有权 | currentOwner, ownerPriority |
无 |
| 展开顺序 | expansionOrder[](有序) |
forcedExpansionNodes(无序 Set) |
| 有效脊柱索引 | getEffectiveSpineIndex() |
固定 spineIndex |
| 可见性链 | isOwnerChainVisible() 递归 |
二元折叠/展开 |
| Hull-节点避让 | 凸包 + 填充 | 基础 hull,无碰撞检查 |
- 步骤 1: 添加展开顺序追踪
- 步骤 2: 实现节点所有权系统
- 步骤 3: 实现
tryClaim()包含所有 9 条规则 - 步骤 4: 实现
determineVisibility()+isOwnerChainVisible() - 步骤 5: 更新边生成 — 基于所有者过滤
- 步骤 6: 更新 hull 生成 — 按所有者分组
- 步骤 7: 有序展开追踪
- 步骤 8: 添加粘性认领设置
- 步骤 9: 跨所有者边过滤
- 步骤 10: Hull 碰撞避让
- 步骤 11: 节点类型着色
- 步骤 12: 展开指示器徽章
- 步骤 13: 传递
expansionOrder和stickyClaimEnabled
- 展开"微积分" → 验证"优化"迁移(规则3)
- 展开"优化" → 验证"微分方程"不能认领"微积分"(规则2+9)
- 折叠"微积分" → 验证脊柱节点返回(规则6)
- 切换粘性认领 → 验证非脊柱节点还原/保持(规则7)
- 检查 Hull 边界不与节点重叠