Date: 2026-03-04
- Shader Conversion: Successfully ported a complex Shadertoy physically-based soap bubble shader (Glassner's 81-wavelength Thin-Film Interference) into a Godot 4 Spatial Shader (
bubble_material.gdshader). - Physics Integration: Upgraded the Path Mode bubbles from static
MeshInstance3Dto interactiveRigidBody3Dnodes. Removed the central board obstruction so bubbles can orbit and collide freely. - Syntax Fixes: Resolved Godot 4 specific shader compilation crashes by removing a duplicate built-in
PIconstant and correcting the array literal initialization syntax frommat3[13](...)to{...}. - Transparency Tuning: The user reported the bubbles were too transparent on monotonous backgrounds. Tuned the base
fresnelalpha clamping from[0.01, 0.5]up to[0.05, 0.95], and increasedrim_opacity/center_opacitydefaults. - Background System Architecture: Designed the architecture for importing environmental backgrounds so the bubbles have something to reflect. Documented the strategy in
path_mode/assets/backgrounds/BACKGROUND_SYSTEM.md, explicitly supporting.hdrand.exrHigh Dynamic Range panoramas.
- Goal: Realistic sloshing colorful iridescence without environmental obstructions, with bubbles that don't stick together.
- Inputted Code Highlights:
sp_spectral_filter(): The core interference calculation mapping film thickness to RGB.warpnoise3(): 3D noise function used to simulate organic sloshing film thickness.
- Instructions Executed:
- "remove environmental obstructions" -> Disabled static body boards in
main.tscn. - "reduce excessive transparency" -> Clamped ALPHA and pushed
rainbow_saturation. - "does .exr format is ok?" -> Updated documentation to confirm
.exris the industry standard for 3D environments.
- "remove environmental obstructions" -> Disabled static body boards in
- Dynamic Background Implementation: Write the GDScript logic to actually load the user's
.exr/.hdrpanorama files from theassets/backgroundsfolder into theWorldEnvironmentat runtime (Phase 2 of BACKGROUND_SYSTEM.md). - Orbit Restitution Tuning: Fine-tune the
RigidBody3Dphysics layers, mass, and continuous collision detection so that as the graph scales (10+ nodes), they orbit fluidly without jittering or overlapping excessively. - Save/Load State: Ensure that whatever custom
.exrbackground the user uploads is saved to their local preferences and restored on launch.
The design direction is now explicitly Bridge-first:
- Keep backend graph intelligence in Node sidecar.
- Move Path Mode controls and interaction orchestration into Godot UI.
- Use PathBridge as the contract layer between Godot and backend runtime.
- Keep browser toolbar behavior for browser mode, but run Godot-only controls in Tauri Path Mode.
- Keep runtime contracts stable before visual refactors.
- Prefer deterministic startup sequencing over optimistic reconnect loops.
- Separate data authority (backend graph/cache state) from presentation authority (Godot rendering state).
- Preserve dual-platform output strategy (desktop + Android) with parity verification.
- How strict should startup debounce be for websocket reconnection under heavy cold starts?
- Should cache prompt state be controlled entirely backend-side to avoid frontend race windows?
- Should history snapshots include source event metadata (
dblclick,manual switch,collapse all) for debugging?
当前设计方向明确为 Bridge-first:
- 后端图谱与算法能力继续保留在 Node Sidecar。
- Path Mode 控制与交互编排迁移到 Godot UI。
- 由 PathBridge 作为 Godot 与后端运行时之间的契约层。
- 浏览器模式继续保留 Web 工具栏;Tauri Path Mode 采用 Godot-only 控制。
- 先稳定运行时契约,再做视觉层重构。
- 启动链路优先确定性时序,不依赖乐观重连循环。
- 分离数据权威(后端图与缓存状态)与展示权威(Godot 渲染状态)。
- 保持桌面与 Android 双平台产物策略,并以一致性回归验证兜底。
- 在重冷启动场景下,WebSocket 重连节流应严格到什么程度?
- 缓存提示状态是否应完全后端化,以规避前端竞态窗口?
- History 快照是否要包含来源事件元数据(
dblclick、manual switch、collapse all)以便排查?
The user wants a "Tree-like learning path" with specific stability constraints:
- Stationary Expansion: Expanding a node should not move it.
- Lateral Unfolding: Prerequisites should appear "to the side" rather than inserting into the main sequence.
- Main Path Precedence: Nodes on the Main Path (the initial sequence) take priority.
- Unique Nodes: No duplicates.
- Shared Prerequisite Priority: If a node is needed by multiple parents, its position is determined by the "preceding" parent (earliest in the main path).
- Definition: The primary sequence of nodes identified by the diffusionLearning algorithm.
- Layout: strictly linear (or slightly curved) horizontal sequence.
- Coordinates: Fixed
Y = 0.Xincreases byX_SPACINGfor each step. - Precedence: These nodes are "anchors". They are placed first and never moved by secondary expansions.
- When a Spine Node
Sis expanded, its prerequisitesP1, P2...(Tributaries) need to be placed. - Placement Logic:
- X-Coordinate: To maintain the "input" flow (Left -> Right), Prereqs usually sit to the left.
- Problem: The slot to the left is occupied by
S's predecessor. - Solution: "Lateral" means displacing in Y.
Pis placed at the sameXcolumn asS's predecessor (or an intermediate column?) but with aYoffset.- OR: Layout uses a "Sub-column" approach.
- User said: "unfold laterally from its side".
- Problem: The slot to the left is occupied by
- Proposed Layout:
- Spine Nodes:
... -> Prev -> Current -> Next ... - Expanded Prereqs for Current:
P1 --\ P2 ---> Current P1andP2are placed atX = X(Current) - X_SPACING(same X asPrev).Yis shifted up/down.- Conflict:
Previs already at (X-1, 0). P1, P2will share the columnX-1.Prevstays atY=0.P1atY=-1,P2atY=1etc.
- Spine Nodes:
- X-Coordinate: To maintain the "input" flow (Left -> Right), Prereqs usually sit to the left.
- Stability: Since the Spine is always at
Y=0, and Tributaries are placed inY != 0slots, the Spine never moves. - Recursive Expansion:
- If
P1is expanded, its childrenPP1go toX-2. - This forms a "Backward Tree" growing from the Spine upwards/downwards.
- If
- Collision Handling:
- We need a "Slot Manager" or "Y-AxisAllocator".
- For a given
Xcolumn, track usedYslots. - When placing
P1at columnX, find the nearest availableYrelative to its parent'sY.
- Traverse the "Main Path" (from
centralIdortargetIdbacktrack).- Note: diffusionLearning returns a list. If
isCriticalorisOriginalPathflag exists, use it. - Or calculate the critical path (Shortest Path from Frontier to Target).
- Note: diffusionLearning returns a list. If
- Assign (Level, Y=0) to all Spine nodes.
Levelincrements towards Target.0= Start Node.N= Target Node.
- Register these positions in a
SpatialMap (Level -> Set<Y>).
- Iterate through Spine Nodes.
- If a Spine Node is
expanded, get its incoming edges (Prereqs). - Filter out nodes already placed (Spine nodes or previously placed Tributaries).
- For remaining Prereqs:
- Target Level:
ParentLevel - 1. - Y-Placement:
- Find available Y slots at
Target Level. - Heuristic: Center them around
Parent.Y. - Alternating Up/Down:
+1, -1, +2, -2...* SPACING. - Check
SpatialMapto ensure slot is free.
- Find available Y slots at
- Assign position and register in
SpatialMap.
- Target Level:
- Recursion: If a Tributary is also expanded, process its children at
Level - 2, etc.
- "Priority given to node preceding along primary path":
- Iterate Spine Nodes in order (Start -> Target).
- Process expansions for Node 0, then Node 1, etc.
- If Node 0 needs
P,Pis placed relative to Node 0. - If Node 1 also needs
P, it seesPis already placed. Just draw edge. - Result:
Pappears "earlier" (further left), satisfying the requirement (or further right? "Preceding" usually means earlier in dependency chain, so further left). - If Node A (Level 5) and Node B (Level 6) both need P.
- Processing A first places P at Level 4.
- 绘制边缘 P->B (Level 4 -> 6) (跨级)。
- This seems correct.
- Deep Prereqs: What if P (for Level 5) needs PP (Level 4) but Level 4 is packed?
Y-Allocatorpushes layouts further out.
- Main Path insertion: If the user effectively changes the main path?
- The algorithm relies on path_core.js providing a "Critical Path". If the user switches context, the Spine changes. This is acceptable (Switch Center = New View).
- But simply "Expanding" a node should NOT change the Spine.
Current getTreeLayout logic needs replacement. New Logic:
- Extract Spine: Identify
isCriticalnodes. - Assign Levels:
- Assign Level 0 to
Frontier(or Start). - BFS/DFS along
isCriticaledges to assign levels to Spine.
- Assign Level 0 to
- Place Spine:
x = level * SPACING,y = 0. - Place Others:
- Use a
Queuefor BFS processing of dependencies. - Use
PosMapto track (x, y) usage. - For each unplaced predecessor
Pof placed nodeN:level = N.level - 1.y = findNearestSlot(level, N.y).- Place
P. - Add
Pto queue.
- Use a
Instead of naive BFS, we iterate strictly:
- Spine Phase: Loop
learningPath.nodes. IfisCritical, setlevel,y=0. Mark placed. - Expansion Phase:
- Iterate
learningPath.nodes(or specifically theforcedExpansionSet+ others). - If node is placed and
isExpanded:- Get unplaced parents.
- Sort parents (by weight/alphabetical).
- Place them at
node.level - 1. - Use logic to fan them out above/below
node.y.
- Repeat until no new nodes placed. (Handling recursive expansion).
- Iterate
Crucial: The definition of "Main Path" relies on path_core.js identifying it. diffusionLearning sets isCritical: true. We will use this.
- We must process nodes in Topological Order (or Main Path order) to ensure the "earlier" parent claims the child's position.
- Since we iterate main path
Start -> Target(Left -> Right), we processLevel 0first. - If
Level 0needsP,Pgoes toLevel -1. - If
Level 2needsP, andPis already atLevel -1, it stays there. - This creates long edges
P -> Level 2, which is fine.
- Modify getTreeLayout:
- Discard Reingold-Tilford.
- Implement "Spine-Based Slot Layout".
Spinenodes:y = 0.Other nodes:yallocated dynamically to minimize vertical distance to parent while avoiding overlap.
用户需要一种“树状学习路径”,并具有特定的稳定性约束:
- 静态展开: 展开节点不应移动它。
- 横向展开: 前置节点应出现在“侧面”而不是插入主序列中。
- 主路径优先: 位于主路径(初始序列)上的节点优先。
- 唯一节点: 无重复节点。
- 共享前置优先级: 如果一个节点被多个父节点需要,其位置由“先前”的父节点(主路径中最早的)决定。
- 定义: 由 diffusionLearning 算法识别的主要节点序列。
- 布局: 严格线性(或微弯)的水平序列。
- 坐标: 固定
Y = 0。每一步X增加X_SPACING。 - 优先级: 这些节点是“锚点”。它们首先被放置,并且永远不会被次级展开移动。
- 当主干节点
S展开时,其前置节点P1, P2...(支流) 需要被放置。 - 放置逻辑:
- X 坐标: 为了保持“输入”流向 (左 -> 右),前置通常位于左侧。
- 问题: 左侧的插槽已被
S的前驱节点占用。 - 解决方案: “横向”意味着在 Y 轴 上位移。
P放置在与S的前驱节点相同的X列(或中间列?),但具有Y偏移。- 或者:布局使用“子列”方法。
- 用户说:“从侧面横向展开”。
- 问题: 左侧的插槽已被
- 建议布局:
- 主干节点:
... -> Prev -> Current -> Next ... - Current 的展开前置:
P1 --\ P2 ---> Current P1和P2放置在X = X(Current) - X_SPACING(与Prev相同的 X)。Y上下移动。- 冲突:
Prev已经在 (X-1, 0)。 P1, P2将共享列X-1。Prev保持在Y=0。P1在Y=-1,P2在Y=1等。
- 主干节点:
- X 坐标: 为了保持“输入”流向 (左 -> 右),前置通常位于左侧。
- 稳定性: 由于主干始终在
Y=0,而支流放置在Y != 0的插槽中,主干永远不会移动。 - 递归展开:
- 如果
P1展开,其子节点PP1去往X-2。 - 这形成了一棵从主干向上/向下生长的“倒树”。
- 如果
- 碰撞处理:
- 我们需要一个“插槽管理器”或“Y轴分配器”。
- 对于给定的
X列,跟踪已使用的Y插槽。 - 当在列
X放置P1时,找到相对于其父节点Y最近的可用Y。
- 遍历“主路径”(从
centralId或targetId回溯)。- 注: diffusionLearning 返回一个列表。如果存在
isCritical或isOriginalPath标志,请使用它。 - 或者计算关键路径(从前沿到目标的最短路径)。
- 注: diffusionLearning 返回一个列表。如果存在
- 为所有主干节点分配 (Level, Y=0)。
Level向目标递增。0= 起始节点。N= 目标节点。
- 在
SpatialMap (Level -> Set<Y>)中注册这些位置。
- 迭代主干节点。
- 如果主干节点是
expanded(已展开),获取其入边(前置节点)。 - 过滤掉已放置的节点(主干节点或先前放置的支流)。
- 对于剩余的前置节点:
- 目标层级:
ParentLevel - 1。 - Y-放置:
- 在
Target Level找到可用的 Y 插槽。 - 启发式: 围绕
Parent.Y居中。 - 交替上/下:
+1, -1, +2, -2...* SPACING。 - 检查
SpatialMap确保插槽空闲。
- 在
- 分配位置并在
SpatialMap中注册。
- 目标层级:
- 递归: 如果支流也展开了,在
Level - 2处理其子节点,依此类推。
- “优先考虑沿主路径在前的节点”:
- 按顺序(起点 -> 终点)迭代主干节点。
- 先处理节点 0 的展开,然后节点 1,依此类推。
- 如果节点 0 需要
P,P相对于节点 0 放置。 - 如果节点 1 也需要
P,它会看到P已放置。只需绘制边缘。 - 结果:
P出现得“更早”(更靠左),满足要求。 - 如果节点 A (Level 5) 和节点 B (Level 6) 都需要 P。
- 先处理 A 将 P 放置在 Level 4。
- 绘制边缘 P->B (Level 4 -> 6) (跨级)。
- 这看起来是正确的。
- 深度前置: 如果 P (Level 5) 需要 PP (Level 4) 但 Level 4 已满怎么办?
Y-Allocator将布局推向更远的外侧。
- 主路径插入: 如果用户有效地更改了主路径?
- 算法依赖 path_core.js 提供“关键路径”。如果用户切换上下文,主干会改变。这是可接受的(切换中心 = 新视图)。
- 但简单地“展开”一个节点 不应 改变主干。
当前的 getTreeLayout 逻辑需要替换。 新逻辑:
- 提取主干: 识别
isCritical节点。 - 分配层级:
- 将 Level 0 分配给
Frontier(或 Start)。-沿isCritical边缘进行 BFS/DFS 以分配主干层级。
- 将 Level 0 分配给
- 放置主干:
x = level * SPACING,y = 0。 - 放置其他:
- 使用
Queue进行依赖项的 BFS 处理。 - 使用
PosMap跟踪 (x, y) 使用情况。 - 对于放置节点
N的每个未放置前驱P:level = N.level - 1。y = findNearestSlot(level, N.y).- 放置
P。 - 将
P添加到队列。
- 使用
- 修改 getTreeLayout:
- 放弃 Reingold-Tilford。
- 实现“基于主干的插槽布局”。
SpineNodes:y = 0。Other nodes:y动态分配以最小化平时距离并避免重叠。
Date: 2026-02-02
- Observation: Users see multiple identical nodes (e.g. "S&P 500") appearing as siblings.
- Root Cause: The underlying graph may have multiple edges between two nodes (e.g. "prerequisite" and "related"). The
getPrerequisiteshelper ingetTreeLayoutmaps edges to source IDs without deduplication. - Mechanism:
// Current Logic const prereqs = incomingEdges.map((e) => e.source); // Returns ['A', 'A'] const unplaced = prereqs.filter((id) => !placed.has(id)); // Returns ['A', 'A'] (both pass) unplaced.forEach((id) => { place(id); // Places A (1st time) place(id); // Places A (2nd time) - Duplicate! });
- Solution: Enforce uniqueness on the prerequisite list before processing.
const uniquePrereqs = [...new Set(incomingEdges.map((e) => e.source))];
- Observation: Nodes are too close leads to overlap given the new Rounded Rectangle shape (180px width).
- Current Settings:
SIBLING_GAP = 220. Node Width = 180. Gap = 40px. This is tight. - Proposal:
- Node Width: 200px (to fit text better)
- Node Height: 60px
- Sibling Gap: 250px (Space between centers). Gap = 50px.
- Spine Spacing (X): 400px.
- Level Height (Y): 150px.
- Deduplicate Prereqs: Modify
getTreeLayoutinpath_core.js. - Adjust Constants: Update
SPACING_X,LEVEL_HEIGHT,SIBLING_GAP. - Refine Renderer: Update
tree_renderer.gdnode size constants to match (200x60).
Date: 2026-02-02
- Problem: Fixed spacing (
500px) fails if node 1 (Down) and node 3 (Down) both have huge subtrees that collide. - Concept: Dynamic Spine Placement.
- Algorithm:
- Calculate
SubtreeWidthandSubtreeBounds(minx, max_x relative to root) for _every spine node bottom-up. - Place Spine Nodes Left-to-Right.
Spine[i].x = Spine[i-1].x + SPACING_X + Constraint.- Constraint:
- Find previous node on same side (e.g., if i is Down/Even, check i-2).
- Ensure
Spine[i].min_x > Spine[i-2].max_x + GAP. - Also ensure logic for immediate neighbor (i-1) is reasonable (often connection line is long).
- Calculate
- Refinement: Since layout is tree-based, "shifting" the root shifts the whole tree. We just need to find the safe X for the root.
- Requirement: "Faint curved border" around in-degree nodes of a central node.
- Scenario: When Central Node is expanded.
- Implementation:
- Data: Identify the set of nodes belonging to the "In-Degree Tree" of the Central Node.
- Geometry: Calculate the Convex Hull or simply a Bounding Box + Padding of these nodes.
- Render: Draw a
StyleBoxFlatordraw_rect/draw_polygonbehind the nodes with low alpha.
- Collapsed State: "All existing in-degree nodes should illuminate".
- This is already partially handled by the "Focus Mode" dimming logic. We just need to ensure all descendants (not just immediate) are highlighted? No, just "in-degree nodes". Usually means immediate parents. Or all recursive? "In-degree nodes" implies the stream. I will highlight recursively.
- Mockup: Implement Dynamic Spine Spacing in
tree_path_mockup.html. - Mockup: Implement "Group Bubble" in
tree_path_mockup.html. - Verify: Show user.
Date: 2026-02-02
- User Insight: "Not just relationship between main learning path and in-degree, but between all nodes... collisions are not permitted."
- Failure of previous approach: Simple Dynamic Spine only checked spine node overlap. It didn't account for deep tributaries of Node A colliding with deep tributaries of Node B.
- Requirement: Contour-Based Collision Avoidance.
- Algorithm:
- We must calculate the "Silhouette" (or Skyline) of every subtree.
- Silhouette: A list of
(min_x, max_x)ranges for every depth level relative to the root. - Merging: When placing a node next to existing placed nodes, we check for intersection between their Skylines at corresponding Y-levels.
- Spine Placement:
- Maintain a
GlobalRightContourfor the "Down" side and "Up" side. - When placing Spine Node
i(going Down):- Shift
i.xuntili's Left Contour clears theGlobalRightContour (Down)+ Padding. - Update
GlobalRightContour (Down)by mergingi's Right Contour. - Same for Up.
- Shift
- Maintain a
- Problem: Not all nodes collapsible.
- Fix: The default click handler in mockup only applied to
.nodegroups entered via D3. If nodes are dynamically added/removed, merge selection properly or use event delegation. Or simply ensuringclickalways togglesd.expanded. - Requirement: "Faint curved border" for in-degree nodes.
- Refinement: Convex Hull or Bubble Set around all upstream nodes of the active central node.
- Data Structure: Enhance
Nodewithpolygonsorcontours. - Recursive Contour Calculation: Function
getContour(node)returns{ [level]: {min, max} }. - Contour Merge: Function
mergeContours(c1, c2, offset_x, offset_y). - Layout Loop:
- Iterate Spine.
x = max(prev_spine_x + MIN_SPACING, findSafeX(node_contour, accum_contour)).- Update Accumulators.
日期: 2026-02-02
- 用户见解: “不仅是主学习路径与其入度节点之间的关系,而是 所有节点之间... 不允许碰撞。”
- 先前方法的失败: 简单的动态主干仅检查了主干节点的重叠。它没有考虑到节点 A 的深度支流与节点 B 的深度支流发生的碰撞。
- 需求: 基于轮廓的碰撞避让。
- 算法:
- 我们必须计算每个子树的“剪影” (Silhouette/Skyline)。
- 剪影: 相对于根节点的每一层深度的
(min_x, max_x)范围列表。 - 合并: 当在现有放置节点旁放置新节点时,检查它们在对应 Y 层级上的剪影是否相交。
- 主干放置:
- 维护“下”侧和“上”侧的
GlobalRightContour(全局右轮廓)。 - 当放置主干节点
i(向下) 时:- 移动
i.x直到i的 左轮廓 清除GlobalRightContour (Down)+ 间距。 - 通过合并
i的右轮廓来更新GlobalRightContour (Down)。 - 上方同理。
- 移动
- 维护“下”侧和“上”侧的
- 问题: 并非所有节点都可折叠。
- 修复: Mockup 中的默认点击处理程序仅应用于通过 D3 进入的
.node组。如果节点是动态添加/删除的,需正确合并选择或使用事件委托。或者简单地确保click始终切换d.expanded。 - 需求: 入度节点的“微弱弯曲边框”。
- 优化: 在活动中心节点的所有上游节点周围绘制凸包或气泡集。
- 数据结构: 增强
Node,增加polygons或contours。 - 递归轮廓计算: 函数
getContour(node)返回{ [level]: {min, max} }. - 轮廓合并: 函数
mergeContours(c1, c2, offset_x, offset_y). - 布局循环:
- 遍历主干。
x = max(prev_spine_x + MIN_SPACING, findSafeX(node_contour, accum_contour)).- 更新累加器。
日期: 2026-02-02
- 问题: 如果节点 1(下)和节点 3(下)都具有巨大的子树导致碰撞,固定间距(
500px)将失效。 - 概念: 动态主干放置。
- 算法:
- 自底向上计算每个主干节点的
SubtreeWidth和SubtreeBounds(相对于根的 min_x, max_x)。 - 从左到右放置主干节点。
Spine[i].x = Spine[i-1].x + SPACING_X + Constraint。- 约束:
- 查找 同侧 的前一个节点(例如,如果 i 是下/偶数,检查 i-2)。
- 确保
Spine[i].min_x > Spine[i-2].max_x + GAP。 - 同时确保与直接邻居 (i-1) 的逻辑合理(通常连接线会变长)。
- 自底向上计算每个主干节点的
- 优化: 由于布局是基于树的,“移动”根节点会移动整个树。我们只需要找到根节点的安全 X 坐标。
- 需求: 在中心节点的入度节点周围显示“微弱的弯曲边框”。
- 场景: 当中心节点展开时。
- 实现:
- 数据: 识别属于中心节点“入度树”的节点集合。
- 几何: 计算这些节点的凸包 (Convex Hull) 或简单的边界框 (Bounding Box) + 填充。
- 渲染: 在节点后方使用低透明度的
StyleBoxFlat或draw_rect/draw_polygon绘制。
- 折叠状态: “所有现有的入度节点都应点亮”。
- 这已由“焦点模式”的变暗逻辑部分处理。我们需要确保 所有 后代(不仅是直接后代)都被高亮?或者只是“入度节点”。通常指流。我将递归高亮。
- Mockup: 在
tree_path_mockup.html中实现动态主干间距。 - Mockup: 在
tree_path_mockup.html中实现“分组气泡”。 - 验证: 向用户展示。
日期: 2026-02-02
- 观察: 用户看到多个相同的节点(例如 "S&P 500")作为兄弟节点出现。
- 根本原因: 底层图数据可能在两个节点之间包含多条边(例如同时存在 "前提" 和 "相关" 关系)。
getTreeLayout中的getPrerequisites辅助函数直接将边映射为源 ID,未进行去重。 - 机制:
// 当前逻辑 const prereqs = incomingEdges.map((e) => e.source); // 返回 ['A', 'A'] const unplaced = prereqs.filter((id) => !placed.has(id)); // 返回 ['A', 'A'] (都通过检查) unplaced.forEach((id) => { place(id); // 放置 A (第1次) place(id); // 放置 A (第2次) - 重复! });
- 解决方案: 在处理之前强制对前提节点列表去重。
const uniquePrereqs = [...new Set(incomingEdges.map((e) => e.source))];
- 观察: 考虑到新的圆角矩形形状(180px 宽),节点过于紧凑导致重叠。
- 当前设置:
SIBLING_GAP = 220。节点宽 = 180。间隙 = 40px。太紧了。 - 建议:
- 节点宽度: 200px (更好容纳文本)
- 节点高度: 60px
- 兄弟间距 (Sibling Gap): 250px (中心间距)。净间隙 = 50px。
- 主干间距 (Spine Spacing X): 400px。
- 层级高度 (Level Height Y): 150px。
- 去重前提节点: 修改
path_core.js中的getTreeLayout。 - 调整常量: 更新
SPACING_X,LEVEL_HEIGHT,SIBLING_GAP。 - 优化渲染器: 更新
tree_renderer.gd的节点尺寸常量以匹配 (200x60)。
Date: 2026-02-26
- Observation: The current production layout in path_core.js
getTreeLayout()treats node placement as a purely geometric problem. It uses:placedNodeIdsSet → "has this node been positioned?"collapsedSet→ "is this node collapsed?"- Contour-based spacing → "do subtrees overlap?"
- Missing: There is no concept of who expanded a node (ownership), when they expanded it (priority), or what rules govern claiming (immunity/migration).
- Consequence: Shared prerequisites are arbitrarily assigned to the first parent encountered during BFS traversal, with no regard for spine precedence, expansion order, or visibility chains.
The tree_path_mockup.html introduces a fundamentally different paradigm:
- Every non-spine node has an owner (
currentOwner): The spine node that "expanded" to reveal it. - Ownership has priority (
ownerPriority): Based on FIFO expansion order. - Ownership governs visibility: If owner is collapsed → node becomes invisible (non-spine) or returns to spine (spine).
- Ownership governs edges: Edges are NOT drawn between nodes with different owners (Rule 5).
| Rule | Why It Exists | Without It |
|---|---|---|
| 1. Expansion Order | Deterministic behavior | Random claiming based on iteration order |
| 2. Preceding Immunity | Spine coherence | Later nodes could steal earlier spine nodes |
| 3. Following Migration | Unit coherence | Spine nodes after expander left orphaned |
| 4. Single Appearance | No visual duplicates | Same node rendered multiple times |
| 5. Cross-Tributary Isolation | Clean separation | Spaghetti edges crossing ownership boundaries |
| 6. Spine Always Visible | Navigation anchor | Spine nodes disappear when owner collapses |
| 7. Sticky Claim | User preference | Always revert or always keep — no middle ground |
| 8. Unit Migration | Hierarchical movement | Spine node moves but its tributaries stay behind |
| 9. Tributary Immunity | Prevents infinite loops | Tributary claims spine nodes above it in hierarchy |
- Problem: When Optimization (idx 3) is claimed by Calculus (idx 1), Optimization now "belongs" to Calculus's territory. But Optimization's original
spineIndex = 3means it can't claim Diff Eq (idx 2) because2 ≤ 3. - Solution:
getEffectiveSpineIndex()— When claimed, a spine node inherits its owner's index for Rule 2 comparisons. - Effect: Optimization operating at idx 1 can now claim Diff Eq (idx 2) since
2 > 1. - Revert: When owner collapses, effective index reverts to original.
- Do NOT rewrite
getTreeLayout()from scratch. Instead:- Insert a
processExpansions()phase BEFORE the existing contour/placement code. - This phase assigns
currentOwner,ownerPriority,_isOnSpineto each node. - The existing contour system then operates on the FILTERED visible node set.
- Hulls and edges reference ownership data for filtering.
- Insert a
- Performance: The mockup iterates all nodes multiple times (process + visibility + placement). For large graphs (10000+ nodes), is this acceptable?
- Hypothesis: Yes. The expansion/claiming phase is O(E × P) where E = expanded count, P = average prerequisites. For typical learning paths (20-100 spine nodes), this is sub-millisecond.
- Godot Integration: Should the 9 rules be computed in
path_core.js(JavaScript worker) or duplicated intree_renderer.gd(GDScript)?- Decision: Compute in
path_core.jsonly. Send enriched layout data (withcurrentOwner,_isOnSpine, node type) to Godot. Godot only renders.
- Decision: Compute in
日期: 2026-02-26
- 观察:
path_core.js的getTreeLayout()将节点放置视为纯粹的几何问题。使用placedNodeIds、collapsedSet和轮廓间距。 - 缺失: 没有"谁展开了节点"(所有权)、"何时展开"(优先级)或"认领规则"(免疫/迁移)的概念。
- 后果: 共享前置节点在 BFS 遍历中被任意分配给第一个遇到的父节点,不考虑脊柱优先、展开顺序或可见性链。
- 每个非脊柱节点都有所有者(
currentOwner) - 所有权有优先级(
ownerPriority):基于 FIFO 展开顺序 - 所有权决定可见性和边的绘制
- 问题: 优化(idx 3)被微积分(idx 1)认领后,原始索引阻止认领微分方程(idx 2)
- 解决方案:
getEffectiveSpineIndex()— 被认领时继承所有者的索引 - 还原: 所有者折叠时恢复原始索引
- 不从头重写
getTreeLayout(),而是在现有轮廓/放置代码之前插入processExpansions()阶段 - 该阶段为每个节点分配
currentOwner、ownerPriority、_isOnSpine - 现有轮廓系统在过滤后的可见节点集上运行
- 性能: 对大图(10000+ 节点)的多次迭代是否可接受?答:是,展开/认领阶段是 O(E × P),通常亚毫秒
- Godot 集成: 9 规则仅在
path_core.js中计算,发送富布局数据到 Godot,Godot 仅渲染
Date: 2026-02-26
- Observation: The current interaction treats all nodes equally. Any node can be double-clicked or right-clicked to expand its prerequisite subtree.
- Problem: Allowing arbitrary non-spine nodes to expand creates visual chaos and breaks the strictly defined "Spine & Tributaries" hierarchy. It also lacks a quick way to just "peek" at a node's connectivity without altering the layout.
- Solution: Split exploration into two explicit modes: Deep Exploration (layout-altering, restricted) and Light Exploration (informational, unrestricted).
- Rules: Only Spine Nodes can deeply explore (expand/collapse subtrees).
- Triggers: Right-click or Double-click on PC.
- Visuals: The
+/-expansion indicator badge is now drawn only on spine nodes.
- Rules: All Nodes support this. It displays an ephemeral info box showing the node's In-Degree and Out-Degree.
- Triggers: Hover for 800ms (PC). Tap (Mobile).
- Content Logic:
- If degree < 10: Show inline list of connected node names.
- If degree ≥ 10: Show the numerical count. Clicking the count expands the full name list.
- Rule: If a user wants to deeply explore a non-spine node, they must make it the new center node (Long-press to navigate).
- Preservation: When switching centers, the original tree's expanded state is preserved. Nodes that were expanded remain expanded, even if the new layout makes them non-spine.
日期: 2026-02-26
- 观察: 当前的交互对所有节点一视同仁。双击或右键单击任何节点都会展开其前置子树。
- 问题: 允许任意非主干节点展开会造成视觉混乱,破坏严格定义的“主干与支流”层级。同时也缺乏一种在不改变布局的情况下快速“偷看”节点连通性的方法。
- 解决方案: 将探索明确划分为两种模式:深度探索(改变布局,受限)和轻度探索(纯信息显示,不受限)。
- 规则: 仅主干节点 (Spine Nodes) 可以深度探索(展开/折叠子树)。
- 触发: PC 端右键单击或双击。
- 视觉:
+/-展开指示标记现在仅在主干节点上绘制。
- 规则: 所有节点都支持。显示一个临时的信息框,展示节点的入度 (In-Degree) 和出度 (Out-Degree)。
- 触发: PC 端悬停 800ms。移动端点击 (Tap)。
- 内容逻辑:
- 如果度数 < 10: 以内联列表显示相连节点名称。
- 如果度数 ≥ 10: 仅显示数字统计。点击该数字可触发展开完整名称列表。
- 规则: 如果用户想要深入探索某个非主干节点,必须将其设为新的中心节点(长按导航)。
- 状态保留: 切换中心时,保留原始树的展开状态。之前已展开的节点保持展开,即使新布局使其成为非主干节点。
Date: 2026-02-27
The current architecture runs two separate processes (Electron + Godot) with two separate windows. This creates:
- Split debugging (Godot console vs Electron DevTools)
- Two windows to manage simultaneously
- WebSocket bridge latency/complexity
- Packaging complexity (Electron-builder + Capacitor + Godot binary)
| Approach | Verdict | Blocker |
|---|---|---|
| Plan A: Godot shell + single WebView for all platforms | ❌ | No single WebView covers Win+Android+Web |
| B1: Godot WASM in Electron | ❌ | Loses Vulkan (WebGL2 only in WASM) |
| B2: Win32 HWND embedding | ❌ | Windows-only, fragile with Chromium |
| B3+: Electron shell + Godot child process | Still dual window | |
| Revised A: Godot shell + platform-specific WebView | ✅ | None — all platforms covered |
Key Breakthrough: The Web target doesn't need Godot at all. Using different WebView plugins per platform enables true single window.
| Platform | Shell | Vulkan | Web Frontend |
|---|---|---|---|
| Desktop (Win) | Godot | ✅ Native | godot-webview (Chromium GDExtension) |
| Android | Godot | ✅ Mobile | Android WebView plugin |
| Web | None | ❌ Canvas/WebGL | Pure HTML/JS |
Architecture:
- Godot is the single window on all native platforms
- Vulkan viewport renders Path/Tree Mode (10K-50K nodes)
- WebView panel displays Graph Mode, Reader, Analysis (existing HTML/CSS/JS)
- Node.js backend runs as child process spawned by Godot
- All logs (Godot + Node.js stdout) unified in single console
| # | Decision | Why |
|---|---|---|
| 1 | Vulkan non-negotiable | 10K-50K nodes require native GPU |
| 2 | Platform-specific WebView OK | Web target separate; Desktop + Android each have working plugins |
| 3 | Godot as primary shell | Achieves single window, handles EXE + APK export natively |
| 4 | Node.js as child process | Preserves existing backend code |
| 5 | Gradual GDScript migration | WebView runs existing frontend now, migrate to native UI over time |
| Phase | What | Effort |
|---|---|---|
| Phase 1 | Godot shell + godot-webview integration + Node.js child process (Desktop) |
🟡 Medium |
| Phase 2 | Unified logging (debug_bridge.gd + Node.js stdout capture) |
🟢 Small |
| Phase 3 | Packaging (Godot export for Windows EXE) | 🟡 Medium |
| Phase 4 | Android APK (Godot export + Android WebView plugin) | 🔴 Large |
| Phase 5 | Web static deployment (pure HTML/JS) | 🟡 Medium |
日期: 2026-02-27
当前架构运行两个独立进程(Electron + Godot),产生两个窗口:调试分离、双窗口管理、WebSocket 桥接延迟、打包复杂。
| 平台 | 壳 | Vulkan | Web 前端 |
|---|---|---|---|
| 桌面 | Godot | ✅ | godot-webview (Chromium) |
| Android | Godot | ✅ | Android WebView 插件 |
| Web | 无 | ❌ | 纯 HTML/JS |
- Vulkan 不可协商(10K-50K 节点)
- 平台特定 WebView 可接受
- Godot 作为主壳实现单窗口
- Node.js 作为子进程保留后端代码
- 逐步迁移到 GDScript 原生 UI