Skip to content

layout: route non-chain flows perpendicular to aging chains#444

Merged
bpowers merged 10 commits intomainfrom
layout-perpendicular-side-flows
Apr 7, 2026
Merged

layout: route non-chain flows perpendicular to aging chains#444
bpowers merged 10 commits intomainfrom
layout-perpendicular-side-flows

Conversation

@bpowers
Copy link
Copy Markdown
Owner

@bpowers bpowers commented Apr 7, 2026

Summary

  • When a stock has a chain flow (stock-to-stock) going right, non-chain outflows (stock-to-cloud) now exit from the bottom of the stock instead of overlapping the chain flow
  • Symmetrically, non-chain inflows enter from the top when a chain inflow comes from the left
  • Multiple flows on the same edge are distributed evenly using the (i+1)/(n+1) spacing formula, matching the TS editor's computeFlowOffsets logic
  • Fixes the hiring model diagram where all waste flows were invisible because they occupied the exact same position as their corresponding chain flows

Test plan

  • 6 unit tests for classify_flow_sides() covering all combinations (chain+side, multiple sides, no chain, etc.)
  • 4 layout behavior tests (side flow below stock, no overlap, multiple spacing, backward compat)
  • 1 integration test verifying the hiring model's 5 waste/chain pairs are properly separated
  • All 122 existing layout unit tests pass
  • All 29 existing layout integration tests pass
  • Full pre-commit hooks pass

When a stock has a chain flow (stock-to-stock) going right, non-chain
outflows (stock-to-cloud) now exit from the bottom of the stock instead
of overlapping the chain flow to the right. Symmetrically, non-chain
inflows enter from the top when a chain inflow comes from the left.

The core of the change is classify_flow_sides(), which separates a
stock's flows into chain vs side categories and assigns each to a
stock edge (Right, Left, Bottom, Top). Multiple flows on the same
edge are distributed evenly using the (i+1)/(n+1) spacing formula,
matching the TypeScript editor's computeFlowOffsets logic.

This fixes the hiring model diagram where all waste flows were
invisible because they occupied the exact same position as their
corresponding chain flows.
@claude
Copy link
Copy Markdown

claude bot commented Apr 7, 2026

Code Review

Reviewed the full diff: classify_flow_sides logic, create_flow_view_element changes, layout_chain positioning updates, incremental layout path, and all 11 new tests.

No bugs found.

The implementation is clean and well-tested. A few observations (non-blocking):

  • classify_flow_sides is called per-flow in the BFS positioning loop (lines 1979, 2004) and then again per-stock at the end for flow_attachments (line 2041). The duplicate calls are harmless since the function is deterministic and cheap, but could be consolidated if this becomes a hot path.
  • The flow_attachments.extend(sides) loop (line 2042) can overwrite a chain flow's attachment when both its source and sink stocks are in the chain. This is safe because create_flow_view_element only reads attachment for cloud-flow arms, never for stock-to-stock flows.

Verdict: Correct. Existing tests and code will not break. The patch cleanly separates chain flows from waste/cloud flows and the test coverage is thorough.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 64bf43b634

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/simlin-engine/src/layout/mod.rs Outdated
Comment on lines +4428 to +4430
if let Some(&pos) = initial_positions.get(flow_ident) {
let uid = state.get_or_alloc_uid(flow_ident);
create_flow_view_element(&mut state, &config, &metadata, flow_ident, uid, pos)?;
create_flow_view_element(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reposition new incremental side flows before building points

In the incremental path (src/simlin-engine/src/layout/mod.rs), new flows are still created from initial_positions computed by compute_new_element_positions (which seeds flows to the right of connected elements), but this commit now lets create_flow_view_element build vertical top/bottom geometries for side flows. For a newly added stock↔cloud side flow on a stock with a chain neighbor, that means the flow center (pos) is far from its generated points, and apply_layout_positions later preserves that offset, so the valve/label remains visually detached from the pipe after settling. Full layout avoids this because layout_chain explicitly computes vertical flow positions, but incremental layout does not.

Useful? React with 👍 / 👎.

Address review feedback for the incremental layout path:

When a new chain flow is added to a stock that already has a preserved
cloud flow on the right, the existing flow must be rebuilt to exit
from the bottom. The incremental layout now classifies sides for all
flows on affected stocks (not just new flows), detects orientation
mismatches in preserved flows, and deletes/rebuilds them with correct
geometry.

Adds two integration tests: one for incrementally adding a waste flow
(verifies it goes below), one for adding a chain flow that forces an
existing cloud flow to change sides.
@claude
Copy link
Copy Markdown

claude bot commented Apr 7, 2026

Code Review

Summary

This PR adds perpendicular routing for non-chain (cloud) flows when a stock also has chain (stock-to-stock) flows, preventing visual overlap. The core logic in classify_flow_sides() is clean and well-tested, and both the full layout and incremental layout paths are updated consistently.

Findings

No bugs found. The logic is sound:

  • classify_flow_sides() correctly partitions flows into chain vs. side, assigns appropriate edges (Bottom for side outflows when chain outflows exist, Top for side inflows when chain inflows exist), and distributes multiple flows along an edge with (i+1)/(n+1) spacing.
  • The create_flow_view_element changes correctly generate vertical flow points for Bottom/Top attachments and fall through to the existing horizontal behavior otherwise.
  • The incremental layout path properly identifies affected stocks, detects orientation mismatches on existing flows, and rebuilds them with correct geometry.
  • The _ => branch in the incremental rebuild position calculation (around mod.rs:4510-4518) always positions to the right of the stock regardless of Right vs Left attachment. This is currently unreachable in practice — the only way to trigger a vertical-to-horizontal rebuild would be removing a chain flow, which doesn't populate affected_stocks. Noting for awareness, not as a bug.
  • classify_flow_sides() is called per-flow during chain positioning and then again per-stock for the flow_attachments map. This is redundant but harmless for typical model sizes.

Test coverage is thorough: unit tests for classification logic, layout behavior tests for geometry, integration test against the hiring model, and incremental layout tests for both adding a waste flow and adding a chain flow that triggers rebuilding.

Verdict

Correct. Existing code and tests will not break. The patch is free of bugs and blocking issues.

@bpowers
Copy link
Copy Markdown
Owner Author

bpowers commented Apr 7, 2026

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f6e56b9fff

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +4419 to +4422
for flow_ident in &new_elements.new_flows {
let (from_stock, to_stock) = metadata.connected_stocks(flow_ident);
if let Some(stock) = from_stock {
affected_stocks.insert(stock.to_string());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reclassify stocks touched by deleted chain flows

The reclassification pass only seeds affected_stocks from new_elements.new_flows, so deleting a chain flow never marks its stock for side recomputation. In incremental edits where a stock previously had chain+cloud outflows (cloud flow routed to Bottom), removing the chain flow leaves the existing cloud flow vertical instead of moving it back to the Right as full layout would. This creates persistent incremental/full-layout divergence after common delete operations.

Useful? React with 👍 / 👎.

Comment thread src/simlin-engine/src/layout/mod.rs Outdated
Comment on lines +4511 to +4515
// Right/Left: use existing horizontal position
Position::new(
stock_pos.x
+ config.stock_width / 2.0
+ config.horizontal_spacing / 2.0,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Place rebuilt Left-attachment flows on the left

When rebuilding preserved flows, the fallback branch for Right/Left attachments always computes x using stock_pos.x + stock_width/2 + ..., which is the right side. If a preserved inflow is reclassified from Top back to Left (e.g., chain inflow removed while other flow changes trigger rebuild), the valve is rebuilt on the wrong side of the stock, producing incorrect geometry and connector angles.

Useful? React with 👍 / 👎.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 7, 2026

Codecov Report

❌ Patch coverage is 88.16388% with 78 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.32%. Comparing base (81a9c42) to head (006c527).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/simlin-engine/src/layout/mod.rs 84.71% 46 Missing ⚠️
src/simlin-engine/tests/layout.rs 90.28% 31 Missing ⚠️
src/simlin-engine/src/layout/layout_tests.rs 97.43% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #444      +/-   ##
==========================================
+ Coverage   81.24%   81.32%   +0.08%     
==========================================
  Files         173      173              
  Lines       48185    48815     +630     
==========================================
+ Hits        39147    39699     +552     
- Misses       9038     9116      +78     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Fix four issues with the incremental layout path for side flows:

- Seed new top/bottom flows from their attachment point rather than
  the generic horizontal position, so the valve sits on the vertical
  pipe instead of off to the side.

- Rebuild preserved side flows when their offset changes (adding a
  sibling on the same face redistributes from 0.5 to 1/3 + 2/3).

- Reclassify sibling cloud flows when chain flows are removed, by
  including stocks from UpdateStockFlows patch ops in affected_stocks.
  Move the reclassification logic before the is_empty() early return
  so it runs even on deletion-only patches.

- Preserve off-center top/bottom attachment x-positions through resnap
  by clamping rather than centering.

Also addresses minor review feedback: extract attachment_based_flow_position
helper to share between rebuilt and new flows, precompute flow_attachments
once in layout_chain instead of calling classify_flow_sides redundantly,
use explicit match arms instead of wildcards, and narrow assign_side
parameter from &mut Vec to &mut [String].
@claude
Copy link
Copy Markdown

claude bot commented Apr 7, 2026

Code Review

I reviewed all changes in this PR — the classify_flow_sides function, attachment_based_flow_position, the resnap_flow_endpoints modification, the incremental layout integration (flow rebuild logic), and all test files.

No bugs found.

Key areas I verified:

  • flow_attachments HashMap collisions: Chain flows get classified by both their source and destination stocks, but create_flow_view_element only consults attachment info for cloud-to-stock / stock-to-cloud flows, so overwrites for chain flow entries are harmless.
  • UID stability across delete/rebuild: apply_deletion preserves the UID mapping in uid_manager, so get_or_alloc_uid returns the same UID after rebuild. Display names are explicitly saved/restored around the deletion — correct.
  • Cloud lifecycle during rebuild: apply_deletion removes clouds and bookkeeping; build_clouds_for_flow recreates them during create_flow_view_element; diff_clouds runs afterward for reconciliation.
  • resnap_flow_endpoints backward compat: The pt.x.clamp(...) change is a no-op for flows previously snapped to stock center (already within bounds). Only affects new off-center vertical flows.
  • Incremental reclassification: The flows_to_rebuild logic correctly detects both orientation mismatches and offset drift, and the affected_stocks set captures UpdateStockFlows patch operations for proper reclassification when sibling chains are added/removed.

Verdict: Correct. Existing code and tests will not break, and the patch is free of blocking issues.

Two fixes for the incremental layout path:

- Stock-to-stock (chain) flows now use the generic initial_positions
  seed instead of attachment_based_flow_position, avoiding nondeterministic
  valve placement when the HashMap merge picks the wrong stock's attachment.

- Preserved side flows are re-sorted by their existing attachment position
  (x for top/bottom, y for left/right) rather than alphabetical ident,
  preventing visual swaps when a sibling is added to imported or
  manually-edited views.
@claude
Copy link
Copy Markdown

claude bot commented Apr 7, 2026

Code Review

I've reviewed the full diff across all four changed files. The implementation is well-structured: classify_flow_sides correctly separates chain (stock-to-stock) vs side (stock-to-cloud) flows, the (i+1)/(n+1) spacing formula is applied consistently, and the incremental path properly handles reclassification when siblings are added/removed.

Key observations:

  • Chain flow attachment overwrite is harmless: When layout_chain calls classify_flow_sides for each stock and extends the flow_attachments HashMap, a chain flow connecting two stocks gets overwritten (first classified as Right for the source stock, then as Left for the destination). This is fine because the (Some(from), Some(to)) branch in create_flow_view_element (line 1750) never consults the attachment — it always uses the stock positions directly.

  • Incremental rebuild logic is sound: The flows_to_rebuild detection correctly identifies orientation mismatches (needs_vertical != is_vertical) and offset drift (for vertical flows whose siblings changed). The delete-and-recreate cycle properly preserves display names and UIDs (since apply_deletion doesn't remove from uid_manager), and build_clouds_for_flow + diff_clouds ensure clouds are correctly regenerated.

  • resnap_flow_endpoints change is safe: Changing from pt.x = stock_pos.x (always center) to pt.x = pt.x.clamp(stock_pos.x - half_w, stock_pos.x + half_w) preserves off-center positions for multi-flow vertical attachments while keeping endpoints within stock bounds.

  • Affected stock identification is complete: New flows contribute their connected stocks, and UpdateStockFlows patch operations capture stocks whose flow lists changed (e.g., when a chain flow is deleted and remaining cloud flows need reclassification from Bottom/Top back to Right/Left).

No bugs found. The test coverage is thorough — 6 unit tests for classification, 4 layout behavior tests, and 1 integration test for the hiring model. The logic is consistent between the layout_chain (fresh) and incremental_layout (patch) paths.

Overall correctness: correct

- Detect affected stocks from DeleteVariable ops by scanning the old
  view for the deleted flow's stock attachment points. This ensures
  sibling flows are reclassified even when the patch omits
  UpdateStockFlows.

- Honor Right/Left attachment offsets for horizontal cloud flows.
  Multiple cloud flows on the same horizontal side now get distinct
  y-positions along the stock edge via the (i+1)/(n+1) formula,
  matching the Bottom/Top behavior. Updated attachment_based_flow_position,
  create_flow_view_element, layout_chain BFS, resnap, and the
  incremental offset-change detection to handle both axes.
@claude
Copy link
Copy Markdown

claude bot commented Apr 7, 2026

Code Review

I reviewed the full diff across all 4 files (~2,862 lines added, 50 removed). The changes implement perpendicular routing for non-chain flows when a stock has a chain flow, with even spacing via (i+1)/(n+1) offsets.

No blocking issues found

The implementation is well-structured:

  • classify_flow_sides correctly distinguishes chain (stock-to-stock) from cloud flows and assigns sides based on chain presence.
  • resnap_flow_endpoints change (preserve off-center positions via clamp) is backward-compatible: existing center-snapped flows have pt.y == stock_pos.y already, so the clamp is a no-op for them.
  • reorder_attachments_by_position correctly handles new flows (no position → f64::MAX → sort last) and preserves visual ordering of existing flows.
  • attachment_based_flow_position uses from_stock.or(to_stock) which is correct since it's only called for cloud flows (one end unattached).
  • The incremental path properly detects affected stocks from new flows, UpdateStockFlows ops, and DeleteVariable ops, ensuring reclassification happens when chain flows are added or removed.
  • Rebuilt flows get their clouds/connectors restored via the existing diff_clouds/diff_connectors pass after the rebuild loop.
  • Chain (stock-to-stock) flows are classified by both endpoint stocks via extend, but since create_flow_view_element ignores attachment info for stock-to-stock flows, the override is harmless.

The n == 1 special case in assign_side (offset = 0.5) matches the TS editor's behavior. The test coverage is thorough with 11 new tests covering all the key scenarios.

Overall correctness: Correct

The patch does not break existing code or tests. The new behavior is well-tested and the edge cases are handled properly.

Chain flows connecting two stocks have their pipe geometry determined
solely by the two stock positions, ignoring the attachment offset. The
offset-change rebuild check was incorrectly triggering rebuilds for
chain flows when a sibling was added (changing the offset from 0.5
to 1/3), which would recreate the flow with a single-stock-based
position and an off-center valve y.

Skip stock-to-stock flows in the offset rebuild check since their
geometry is independent of the attachment offset.
@claude
Copy link
Copy Markdown

claude bot commented Apr 7, 2026

Code Review

Reviewed the full diff: ~717 lines of production code in src/simlin-engine/src/layout/mod.rs and ~2340 lines of tests across 3 test files.

Summary

The change introduces flow-side classification (classify_flow_sides) to route non-chain flows (stock-to-cloud) perpendicular to aging chains, preventing visual overlap. Key changes:

  1. classify_flow_sides — Correctly classifies outflows as Bottom (when chain outflows exist) or Right (default), and inflows as Top (when chain inflows exist) or Left (default). The (i+1)/(n+1) spacing formula matches the TS editor's computeFlowOffsets.

  2. resnap_flow_endpoints — Now preserves off-center attachment positions (clamped to stock bounds) instead of snapping to center. This is necessary for multi-flow sides and is actually an improvement for imported models with manually-positioned endpoints.

  3. reorder_attachments_by_position — Preserves visual ordering of existing flows during redistribution rather than forcing alphabetical order.

  4. attachment_based_flow_position — Correctly computes valve position from attachment info with appropriate Option returns for missing data.

  5. Incremental layout path — Properly detects when preserved flows need orientation/offset changes, deletes and rebuilds them while preserving display names and UIDs.

Analysis

  • The apply_deletioncreate_flow_view_element rebuild cycle correctly handles clouds (deleted by apply_deletion, recreated by build_clouds_for_flow inside create_flow_view_element).
  • Chain (stock-to-stock) flows are explicitly excluded from attachment-based positioning and rebuild checks, using initial_positions instead — preventing incorrect valve placement.
  • The resnap_flow_endpoints call added to the new_elements.is_empty() path is safe: for correctly-positioned endpoints it's a no-op; for rebuilt flows it ensures edge snapping.
  • Test coverage is thorough: 6 unit tests for classification, 4 layout behavior tests, 1 hiring model integration test, plus incremental layout tests covering add/remove/reclassify scenarios.

Verdict

Correct. No bugs found. The patch is well-structured, the edge cases are handled, and existing tests should continue to pass. The code correctly solves the flow overlap problem described in the PR.

Three fixes for the incremental side-flow logic:

- Exclude stock-to-stock (chain) flows from the entire rebuild check,
  not just the offset branch. An imported chain that happens to be more
  vertical than horizontal would otherwise trigger an orientation
  rebuild via attachment_based_flow_position, which only knows one
  stock and places the valve beside it instead of between the pair.

- Exclude chain flows from reorder_attachments_by_position. Because
  incr_flow_attachments collapses per-stock classifications into a
  single FlowAttachment per flow, a chain flow may appear on the
  wrong side of one stock and skew sibling offset redistribution.

- Match deleted flows by UID rather than canonicalized display name.
  Labels go through format_label_with_line_breaks which strips
  quoting, so canonicalizing the label can produce a different ident
  for names like "a.b".
@claude
Copy link
Copy Markdown

claude bot commented Apr 7, 2026

Code Review

I reviewed the full diff across all 4 files (~3,000 lines). The changes implement a well-structured approach to routing non-chain flows perpendicular to aging chains.

No bugs found

The logic is sound throughout:

  • classify_flow_sides correctly distinguishes chain (stock-to-stock) vs side (stock-to-cloud) flows and routes side flows to Bottom/Top when a chain flow occupies Right/Left.
  • flow_attachments.extend(sides) in layout_chain overwrites chain flow entries when processing the destination stock (Right→Left), but chain flows use their own stock-to-stock positioning code path and never consult the attachment map, so this is benign.
  • resnap_flow_endpoints change from center-snapping to clamp-preserving is correct: single flows already have their points at center (no-op change), and multi-flow points stay within stock bounds.
  • reorder_attachments_by_position correctly skips chain flows and only reorders cloud flows on affected stocks, preserving imported/manual ordering.
  • Incremental rebuild path correctly identifies flows needing orientation changes, saves/restores display names across deletion+recreation, and uses attachment_based_flow_position only for cloud flows (chain flows use initial_positions).
  • The aspect-ratio check in resnap_flow_endpoints (half_h * dx.abs() >= half_w * dy.abs()) reliably routes bottom/top flows to the vertical branch since the valve's vertical offset (stock_height/2 + horizontal_spacing/2) strongly dominates any horizontal offset from multi-flow spacing.

Overall correctness: ✅ Correct

The patch is well-tested (11 new tests covering full layout, incremental add/rebuild, offset redistribution, and the hiring model integration scenario), the logic is consistent between the full layout and incremental paths, and existing behavior is preserved for the non-multi-flow case.

…ookup

- Only call resnap_flow_endpoints when flows were actually rebuilt,
  to avoid rewriting imported/manual flow geometry on patches that
  only change equations or documentation.

- Build stock_uid_to_ident from the model's stock variables rather
  than from view element labels, since labels go through
  format_label_with_line_breaks and may not round-trip through
  canonicalize for quoted names like "a.b".
@claude
Copy link
Copy Markdown

claude bot commented Apr 7, 2026

Code Review

Reviewed the full diff (~3300 lines across 4 files). The approach is well-structured: classify_flow_sides determines edge placement, reorder_attachments_by_position preserves visual ordering of existing flows, and the incremental path correctly detects and rebuilds flows that need orientation changes.

Key observations:

  • classify_flow_sides + HashMap::extend in both paths: When called for multiple stocks, chain flows connecting two stocks get overwritten by the second stock's classification. This is benign because chain flows are systematically skipped in both the rebuild path (from_stock.is_some() && to_stock.is_some() guard) and create_flow_view_element's (Some(from), Some(to)) branch which doesn't consult attachment. Correct as-is.

  • resnap_flow_endpoints clamp change: Switching from pt.y = stock_pos.y to pt.y = pt.y.clamp(...) is correct for multi-flow sides. For existing single-flow cases, pt.y is already at center, so the clamp is a no-op. No regression risk.

  • reorder_attachments_by_position skipping chain flows: The offset counts remain consistent because classify_flow_sides already separates chain and side flows onto different edges (chain→Right/Left, side→Bottom/Top when chains exist). So the n used for (i+1)/(n+1) in reordering only ever counts the same flows that were originally assigned to that edge.

  • Incremental rebuild guard: The if !flows_to_rebuild.is_empty() condition around resnap_flow_endpoints correctly avoids rewriting imported/manual geometry on equation-only patches.

No bugs found. Test coverage is thorough with 6 unit tests for classification, 4 layout behavior tests, 3 integration tests (including the hiring model), and all existing tests preserved.

Overall correctness: ✅ Correct — existing code and tests will not break, and the patch is free of bugs and blocking issues.

When attachment_based_flow_position cannot resolve a stock UID
(e.g. imported views with quoted identifiers that do not round-trip
through canonicalize), the rebuilt flow was silently dropped. Save
the old flow position before deletion and use it as a fallback,
ensuring the flow is always recreated.
@claude
Copy link
Copy Markdown

claude bot commented Apr 7, 2026

Code Review

I reviewed the full diff across all 4 changed files (layout/mod.rs, layout_tests.rs, layout_review_tests.rs, tests/layout.rs).

Summary

This PR adds perpendicular routing for non-chain (cloud) flows when a stock already has a chain (stock-to-stock) flow. The approach is sound: classify_flow_sides() determines which stock edge each flow attaches to, and the (i+1)/(n+1) spacing formula distributes multiple flows evenly along an edge. The incremental layout path correctly detects when existing flows need to change orientation or offset, deletes and rebuilds them with proper geometry.

Findings

No bugs found. Key areas verified:

  1. resnap_flow_endpoints clamping change (mod.rs:1024-1035): Switching from pt.y = stock_pos.y to pt.y = pt.y.clamp(...) correctly preserves off-center attachment points for multi-flow edges while keeping them within stock bounds. The dx/dy zero case (valve coincident with stock center) falls through to the horizontal branch identically to the old code.

  2. classify_flow_sides extend semantics (mod.rs ~line 2380): When layout_chain iterates all stocks and extends flow_attachments, a chain flow classified by both its source and destination stock gets overwritten. This is safe because the (Some(from), Some(to)) code path in create_flow_view_element ignores the attachment entirely, computing geometry from both stock positions directly.

  3. reorder_attachments_by_position chain-flow skip (mod.rs ~line 2049-2051): Correctly skips stock-to-stock flows to avoid counting them on the wrong side. The connected check properly handles cloud-to-stock and stock-to-cloud flows using is_some_and.

  4. Incremental rebuild detection (mod.rs ~line 2600-2670): The orientation/offset mismatch detection with 0.5 epsilon tolerance and the fallback to old_flow_positions for imported views with unresolvable UIDs are both sensible.

  5. Conditional resnap (mod.rs ~line 2727-2729): Only calling resnap_flow_endpoints when flows were rebuilt avoids rewriting imported/manual geometry on equation-only patches. Correct guard.

Verdict

Correct. The patch introduces no bugs or breaking changes. The new behavior is well-tested with 11+ new tests covering unit classification, full layout, incremental add/rebuild, and the hiring model integration scenario. Existing 151 tests continue to pass per the PR description.

Rebuilt flows already have correct geometry from create_flow_view_element
and there is no SFDP settlement step on this path. Remove resnap to
avoid rewriting unrelated manual or imported flow endpoints elsewhere
in the diagram when only a subset of flows are rebuilt.
@bpowers
Copy link
Copy Markdown
Owner Author

bpowers commented Apr 7, 2026

Review cycle summary

Eight review iterations addressed incremental layout correctness across multiple edge cases. The key improvements beyond the original two commits:

Incremental layout fixes:

  • New top/bottom cloud flows are seeded from their attachment point (not the generic horizontal position), so valves sit correctly on vertical pipes
  • Preserved side flows are rebuilt when their offset changes (e.g., adding a sibling redistributes from 0.5 to 1/3 + 2/3)
  • Cloud flows are reclassified when chain flows are removed, including patches that only emit DeleteVariable without UpdateStockFlows (detected by scanning the old view's flow attachment UIDs)
  • Stock-to-stock (chain) flows are excluded from the entire rebuild pipeline since their geometry is determined by both stock positions, not attachment offsets
  • Preserved flow ordering uses existing visual position rather than alphabetical ident, preventing disruptive reordering of imported/manually-edited flows
  • Rebuilt flows save their old position as a fallback when the stock UID lookup fails
  • Resnap is skipped entirely on the rebuild-only path (no settlement step) to avoid rewriting unrelated manual geometry

Full layout improvements:

  • Right/Left attachment offsets are now honored for horizontal cloud flows, distributing multiple flows along the stock edge via (i+1)/(n+1) spacing
  • classify_flow_sides results are precomputed once in layout_chain instead of redundantly per-flow during BFS

Robustness:

  • UID-based matching for DeleteVariable instead of lossy label canonicalization
  • Model-based stock ident resolution instead of view label round-tripping

Two known limitations filed as issues: #445 (chain direction inference for non-standard layouts) and #446 (reorder axis when flows change faces).

@claude
Copy link
Copy Markdown

claude bot commented Apr 7, 2026

Code Review

I reviewed the full diff across 4 files (~2,200 lines). This PR introduces perpendicular routing for non-chain (cloud) flows when a stock already has a chain (stock-to-stock) flow, preventing visual overlap in aging chain models like the hiring model.

Summary of changes

Core logic (src/simlin-engine/src/layout/mod.rs):

  • New classify_flow_sides() function correctly separates chain vs side flows and assigns sides (Bottom for side outflows when chain exists, Top for side inflows when chain exists)
  • reorder_attachments_by_position() preserves visual ordering of existing flows during incremental layout
  • attachment_based_flow_position() computes valve positions based on attachment info
  • create_flow_view_element() now handles Bottom/Right/Top/Left attachment geometry for cloud flows
  • resnap_flow_endpoints() changed from center-snapping to position-preserving with clamping -- correct for off-center multi-flow attachments
  • incremental_layout() comprehensively detects affected stocks (new flows, UpdateStockFlows ops, deleted flows via old view) and rebuilds flows that need orientation/offset changes

Key correctness observations:

  • Chain (stock-to-stock) flows are correctly excluded from reorder and rebuild logic, since their geometry is determined by both stock positions
  • The flow_attachments.extend(sides) pattern can overwrite chain flow entries when iterating multiple stocks, but this is harmless since chain flow attachment info is never consumed
  • Display names are correctly preserved across delete+rebuild cycles
  • The (i+1)/(n+1) spacing formula is applied consistently in both fresh and incremental layout

Findings

No bugs found. The implementation is well-structured, internally consistent, and thoroughly tested (6 unit tests for classify_flow_sides, 4 layout behavior tests, incremental layout tests covering add/remove/redistribute scenarios, and an integration test against the hiring model).

Verdict: Correct

The patch is free of bugs and blocking issues. Existing tests and code will not break.

@bpowers bpowers merged commit 26b2335 into main Apr 7, 2026
12 checks passed
@bpowers bpowers deleted the layout-perpendicular-side-flows branch April 7, 2026 13:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant