Implementation Plan: Fusion for Hexagonal and Concentric Puzzles
Executive Summary
This plan outlines the steps to enable fusion (meta-pieces) for hexagonal and concentric puzzle types, following the pattern established for rectangular puzzles.
Current State:
- ✅ Rectangular: Full fusion support
- ❌ Hexagonal: Infrastructure ready, fusion generation not implemented
- ❌ Concentric: Infrastructure ready, fusion generation not implemented
Recommendation: Implement concentric first (simpler 4-edge topology), then hexagonal (6-edge topology).
Phase 1: Concentric Fusion (Estimated: 3 components)
1.1 Edge Naming Convention
Existing convention (from concentric_geometry.R:169-173):
| Edge Index | Name | Description |
|---|---|---|
| 1 | INNER | Connects to inner ring or center |
| 2 | RIGHT | Next piece in same ring (clockwise) |
| 3 | OUTER | Connects to outer ring or boundary |
| 4 | LEFT | Previous piece in same ring |
Center piece (hexagon type): 6 edges numbered 1-6, each connecting to ring 1 pieces.
Edge key format: "{piece_id}-{edge_name}" (e.g., "5-INNER", "1-3" for center)
1.2 Adjacency Functions
File: R/adjacency_api.R
Functions to add:
#' Get concentric piece neighbors
#' @param piece_id Piece ID (1-based)
#' @param rings Number of rings in puzzle
#' @param center_shape "hexagon" or "circle"
#' @return Data frame with direction, neighbor_id, is_boundary
get_concentric_neighbors <- function(piece_id, rings, center_shape = "hexagon") {
# Use existing get_concentric_neighbor() from concentric_geometry.R
# Returns neighbors for all 4 edges (or 6 for center)
}
#' Get complementary edge for concentric
#' @param direction Edge direction
#' @return Opposite direction
get_concentric_complementary_direction <- function(direction) {
switch(direction,
"INNER" = "OUTER",
"OUTER" = "INNER",
"LEFT" = "RIGHT",
"RIGHT" = "LEFT",
# Center piece edges (1-6) - opposite is (n + 3) %% 6
as.character((as.integer(direction) + 2) %% 6 + 1)
)
}
1.3 Fused Edge Computation
File: R/adjacency_api.R
Function to add:
#' Compute fused edges for concentric puzzles
#' @param fusion_groups List of piece ID vectors
#' @param puzzle_result Puzzle result with pieces and parameters
#' @return List with fused_edges, edge_to_group, piece_to_group
compute_concentric_fused_edges <- function(fusion_groups, puzzle_result) {
# Pattern: Same as compute_fused_edges() but:
# - Use get_concentric_neighbors() instead of get_piece_neighbors()
# - Handle center piece 6-edge case
# - Use concentric complementary directions
}
1.4 Piece Generation Update
File: R/unified_piece_generation.R
Function: generate_concentric_pieces_internal() (lines 351-464)
Changes:
- Remove "not yet implemented" comment
- Call
compute_concentric_fused_edges()if fusion_groups provided - For each piece, set
fused_edgesmetadata:# For trapezoid pieces (ring > 0) fused_edges <- list(INNER = FALSE, RIGHT = FALSE, OUTER = FALSE, LEFT = FALSE) if (!is.null(fused_edge_data)) { fused_edges$INNER <- is_edge_fused(piece_id, "INNER", fused_edge_data) fused_edges$RIGHT <- is_edge_fused(piece_id, "RIGHT", fused_edge_data) fused_edges$OUTER <- is_edge_fused(piece_id, "OUTER", fused_edge_data) fused_edges$LEFT <- is_edge_fused(piece_id, "LEFT", fused_edge_data) } # For center piece (ring 0, hexagon type) # fused_edges = list(`1` = FALSE, `2` = FALSE, ..., `6` = FALSE)
1.5 Path Splitting
File: R/unified_renderer.R
Function to add:
#' Split concentric piece path into individual edge paths
#' @param path SVG path string
#' @param piece Piece object with ring_pos
#' @return List with INNER, RIGHT, OUTER, LEFT edge paths
split_concentric_path_into_edges <- function(path, piece) {
# Path construction order: M, INNER, RIGHT, OUTER, LEFT, Z
# Use vertex positions to identify edge boundaries
# Similar to split_rect_path_into_edges but with different corner detection
}
1.6 Renderer Integration
File: R/unified_renderer.R
Update render_pieces_with_fusion_styled():
- Detect piece type from
piece$type - Call appropriate path splitting function:
"rectangular"→split_rect_path_into_edges()"concentric"→split_concentric_path_into_edges()"hexagonal"→split_hex_path_into_edges()
Phase 2: Hexagonal Fusion (Estimated: 3 components)
2.1 Edge Naming Convention
Existing convention (from hexagonal_neighbors.R:10-21):
| Side | Direction | Angle |
|---|---|---|
| 0 | Right | 0° |
| 1 | Upper-right | 60° |
| 2 | Upper-left | 120° |
| 3 | Left | 180° |
| 4 | Lower-left | 240° |
| 5 | Lower-right | 300° |
Edge key format: "{piece_id}-{side}" (e.g., "7-3" for piece 7, side 3)
2.2 Adjacency Functions
File: R/adjacency_api.R
Functions to add:
#' Get hexagonal piece neighbors
#' @param piece_id Piece ID (1-based)
#' @param rings Number of rings
#' @return Data frame with side, neighbor_id, is_boundary
get_hex_neighbors_for_fusion <- function(piece_id, rings) {
# Use existing get_hex_neighbor() from hexagonal_neighbors.R
# Return neighbors for all 6 sides
}
#' Get complementary side for hexagonal
#' @param side Side number (0-5)
#' @return Opposite side number
get_hex_complementary_side <- function(side) {
(side + 3) %% 6 # Opposite side is +3 mod 6
}
2.3 Fused Edge Computation
File: R/adjacency_api.R
Function to add:
#' Compute fused edges for hexagonal puzzles
#' @param fusion_groups List of piece ID vectors
#' @param puzzle_result Puzzle result with pieces and parameters
#' @return List with fused_edges, edge_to_group, piece_to_group
compute_hex_fused_edges <- function(fusion_groups, puzzle_result) {
# Pattern: Same as compute_fused_edges() but:
# - Use get_hex_neighbors_for_fusion()
# - Handle 6 sides instead of 4
# - Use (side + 3) %% 6 for complementary
}
2.4 Piece Generation Update
File: R/unified_piece_generation.R
Function: generate_hex_pieces_internal() (lines 225-348)
Changes:
- Remove "not yet implemented" comment
- Call
compute_hex_fused_edges()if fusion_groups provided - For each piece, set
fused_edgesmetadata:fused_edges <- list(`0` = FALSE, `1` = FALSE, `2` = FALSE, `3` = FALSE, `4` = FALSE, `5` = FALSE) if (!is.null(fused_edge_data)) { for (side in 0:5) { fused_edges[[as.character(side)]] <- is_edge_fused(piece_id, side, fused_edge_data) } }
2.5 Path Splitting
File: R/unified_renderer.R
Function to add:
#' Split hexagonal piece path into individual edge paths
#' @param path SVG path string
#' @param piece Piece object
#' @return List with edges 0-5
split_hex_path_into_edges <- function(path, piece) {
# Path construction: Counter-clockwise, sides 0→5
# Each side is one segment (L or C commands)
# Use vertex positions from edge map if available
}
2.6 Renderer Integration
Same as Phase 1.6 - already covered in the type-based dispatch.
Phase 3: Testing
3.1 Unit Tests
File: tests/testthat/test-fusion-concentric.R
test_that("concentric fusion works with offset=0", { ... })
test_that("concentric fusion works with offset>0", { ... })
test_that("concentric fused pieces move together", { ... })
test_that("concentric center piece fusion works", { ... })
File: tests/testthat/test-fusion-hexagonal.R
test_that("hexagonal fusion works with offset=0", { ... })
test_that("hexagonal fusion works with offset>0", { ... })
test_that("hexagonal fused pieces move together", { ... })
test_that("hexagonal center piece fusion works", { ... })
3.2 Integration Tests
- Test all three puzzle types with same fusion_groups format
- Verify Shiny app displays fusion controls for all types
- Visual verification of edge hiding/styling
3.3 Edge Cases
- Center piece fusion (piece 1 with ring 1 pieces)
- Multi-ring fusion (pieces spanning rings)
- Full puzzle fusion (all pieces in one group)
- Empty fusion groups
- Invalid piece IDs in fusion groups
Phase 4: Documentation
4.1 Update Function Documentation
- Add fusion examples to
generate_puzzle()for all types - Document edge naming conventions
4.2 Update CLAUDE.md
- Add fusion support status for each puzzle type
- Document edge naming conventions
Implementation Order
- Concentric adjacency functions (1.2) - builds on existing
get_concentric_neighbor() - Concentric fused edge computation (1.3) - copy pattern from rectangular
- Concentric piece generation update (1.4) - integrate into generation loop
- Concentric path splitting (1.5) - simpler than hexagonal
- Renderer integration (1.6) - type-based dispatch
- Concentric tests (3.1) - verify implementation
- Hexagonal adjacency functions (2.2) - builds on existing
get_hex_neighbor() - Hexagonal fused edge computation (2.3) - copy pattern from concentric
- Hexagonal piece generation update (2.4) - integrate into generation loop
- Hexagonal path splitting (2.5) - 6-edge version
- Hexagonal tests (3.1) - verify implementation
- Documentation updates (4.1, 4.2)
Risks and Mitigations
| Risk | Mitigation |
|---|---|
| Path splitting complexity | Use existing edge map data instead of parsing paths |
| Center piece special cases | Handle separately with explicit edge counts |
| Multi-segment edges (concentric OUTER) | May need segment grouping logic |
| Performance with large puzzles | Reuse existing neighbor detection infrastructure |
Success Criteria
- All existing tests continue to pass (901 tests)
- New fusion tests pass for concentric and hexagonal
- Shiny app displays fusion correctly for all puzzle types
- Edge styling (none/dashed/solid) works for all types
- Fused pieces move together when separated