# Direct R Translation of Draradech's JavaScript Jigsaw Puzzle Generator # Original source: https://gist.github.com/Draradech/35d36347312ca6d0887aa7d55f366e30 # License: CC0 (Public Domain) # # Optimized with batch RNG generation using C++ uniform_batch() when available. # See R/rng_iterator.R for the batch optimization implementation. # Global variables (matching JS implementation exactly) .jigsaw_env <- new.env() #' Initialize jigsaw environment with global variables #' @param seed Random seed (default: random) #' @param tabsize Tab size as percentage (0-100). Default: 6. #' @param jitter Jitter percentage (default: 4) #' @param width Puzzle width in mm (default: 300) #' @param height Puzzle height in mm (default: 200) #' @param unit Unit specification: "mm" or "px" #' @param dpi DPI for conversion (default: 96) #' @param radius Corner radius in mm (default: 2.0) #' @param xn Number of columns (default: 15) #' @param yn Number of rows (default: 10) #' @param min_tab_size Minimum absolute tab size in mm (default: NULL for no limit). #' Prevents tabs from becoming too small on short edges. #' @param max_tab_size Maximum absolute tab size in mm (default: NULL for no limit). #' Prevents tabs from becoming too large on long edges. init_jigsaw <- function(seed = NULL, tabsize = 6, jitter = 4, width = 300, height = 200, unit = "mm", dpi = 96, radius = 2.0, xn = 15, yn = 10, min_tab_size = NULL, max_tab_size = NULL) { if (is.null(seed)) { seed <- as.integer(runif(1) * 10000) } # Store all parameters in environment .jigsaw_env$seed_initial <- seed .jigsaw_env$seed <- seed .jigsaw_env$tabsize <- tabsize .jigsaw_env$jitter <- jitter .jigsaw_env$width <- width .jigsaw_env$height <- height .jigsaw_env$unit <- unit .jigsaw_env$dpi <- dpi .jigsaw_env$radius <- radius .jigsaw_env$xn <- xn .jigsaw_env$yn <- yn .jigsaw_env$offset <- 0.0 # Tab size constraints .jigsaw_env$min_tab_size <- min_tab_size .jigsaw_env$max_tab_size <- max_tab_size # Parse input (equivalent to parse_input() in JS) .jigsaw_env$t_base <- tabsize / 100.0 # Normalized: same formula as all puzzle types .jigsaw_env$t <- .jigsaw_env$t_base # Effective t (may be adjusted per edge) .jigsaw_env$j <- jitter / 100.0 # Create RNG iterator with pre-generated batch values for performance # This uses C++ uniform_batch() when available (~27x speedup) rng_count <- calc_rect_rng_count(xn, yn) .jigsaw_env$rng <- create_rng_iterator(seed, rng_count) } # Random number generator - uses pre-generated batch values # (Original JS translation used per-call sine-based RNG) random <- function() { .jigsaw_env$rng$next_val() } uniform <- function(min_val, max_val) { .jigsaw_env$rng$uniform(min_val, max_val) } rbool <- function() { .jigsaw_env$rng$rbool() } # Tab generation functions (exact JS translation) first <- function() { .jigsaw_env$e <- uniform(-.jigsaw_env$j, .jigsaw_env$j) .jigsaw_env$flip <- NULL # Initialize flip for first call next_tab() } next_tab <- function() { flipold <- if (exists("flip", envir = .jigsaw_env)) .jigsaw_env$flip else NULL .jigsaw_env$flip <- rbool() .jigsaw_env$a <- if (is.null(flipold) || .jigsaw_env$flip == flipold) -.jigsaw_env$e else .jigsaw_env$e .jigsaw_env$b <- uniform(-.jigsaw_env$j, .jigsaw_env$j) .jigsaw_env$c <- uniform(-.jigsaw_env$j, .jigsaw_env$j) .jigsaw_env$d <- uniform(-.jigsaw_env$j, .jigsaw_env$j) .jigsaw_env$e <- uniform(-.jigsaw_env$j, .jigsaw_env$j) # Apply min/max tab size constraints for this edge apply_tab_constraints() } #' Apply min/max tab size constraints for the current edge #' #' Adjusts .jigsaw_env$t based on the current edge length and any #' min/max constraints. Called automatically by next_tab(). #' #' Tab height formula: tab_height = 3 * t * edge_length #' If min_tab_size would require a tab wider than 70% of the edge, #' the maximum safe width is used instead. #' #' @keywords internal apply_tab_constraints <- function() { # Skip if no constraints are set if (is.null(.jigsaw_env$min_tab_size) && is.null(.jigsaw_env$max_tab_size)) { .jigsaw_env$t <- .jigsaw_env$t_base return() } # Get current edge length edge_length <- sl() # Calculate current tab height using base t value tab_height <- 3.0 * .jigsaw_env$t_base * edge_length t <- .jigsaw_env$t_base # Apply minimum constraint if (!is.null(.jigsaw_env$min_tab_size) && tab_height < .jigsaw_env$min_tab_size) { t <- .jigsaw_env$min_tab_size / (3.0 * edge_length) # Check if this would make the tab too wide for the edge # Tab spans from 0.5 - 2t to 0.5 + 2t, so total width is 4t # It should fit within roughly 0.1 to 0.9 of the edge (leaving margins) if (4.0 * t > 0.7) { # Cap at maximum safe width t <- 0.175 # 0.7 / 4 } } # Apply maximum constraint if (!is.null(.jigsaw_env$max_tab_size) && tab_height > .jigsaw_env$max_tab_size) { t <- .jigsaw_env$max_tab_size / (3.0 * edge_length) } .jigsaw_env$t <- t } # Coordinate calculation functions (exact JS translation) sl <- function() { return(if (.jigsaw_env$vertical) .jigsaw_env$height / .jigsaw_env$yn else .jigsaw_env$width / .jigsaw_env$xn) } sw <- function() { return(if (.jigsaw_env$vertical) .jigsaw_env$width / .jigsaw_env$xn else .jigsaw_env$height / .jigsaw_env$yn) } ol <- function() { return(.jigsaw_env$offset + sl() * (if (.jigsaw_env$vertical) .jigsaw_env$yi else .jigsaw_env$xi)) } ow <- function() { return(.jigsaw_env$offset + sw() * (if (.jigsaw_env$vertical) .jigsaw_env$xi else .jigsaw_env$yi)) } l <- function(v) { ret <- ol() + sl() * v return(round(ret * 100) / 100) } w <- function(v) { ret <- ow() + sw() * v * (if (.jigsaw_env$flip) -1.0 else 1.0) return(round(ret * 100) / 100) } # Bézier control point functions (exact JS translation) p0l <- function() l(0.0) p0w <- function() w(0.0) p1l <- function() l(0.2) p1w <- function() w(.jigsaw_env$a) p2l <- function() l(0.5 + .jigsaw_env$b + .jigsaw_env$d) p2w <- function() w(-.jigsaw_env$t + .jigsaw_env$c) p3l <- function() l(0.5 - .jigsaw_env$t + .jigsaw_env$b) p3w <- function() w(.jigsaw_env$t + .jigsaw_env$c) p4l <- function() l(0.5 - 2.0 * .jigsaw_env$t + .jigsaw_env$b - .jigsaw_env$d) p4w <- function() w(3.0 * .jigsaw_env$t + .jigsaw_env$c) p5l <- function() l(0.5 + 2.0 * .jigsaw_env$t + .jigsaw_env$b - .jigsaw_env$d) p5w <- function() w(3.0 * .jigsaw_env$t + .jigsaw_env$c) p6l <- function() l(0.5 + .jigsaw_env$t + .jigsaw_env$b) p6w <- function() w(.jigsaw_env$t + .jigsaw_env$c) p7l <- function() l(0.5 + .jigsaw_env$b + .jigsaw_env$d) p7w <- function() w(-.jigsaw_env$t + .jigsaw_env$c) p8l <- function() l(0.8) p8w <- function() w(.jigsaw_env$e) p9l <- function() l(1.0) p9w <- function() w(0.0) # Main generation functions (exact JS translation) gen_dh <- function() { str <- "" .jigsaw_env$vertical <- 0 for (yi in 1:(.jigsaw_env$yn - 1)) { .jigsaw_env$yi <- yi .jigsaw_env$xi <- 0 first() str <- paste0(str, "M ", p0l(), ",", p0w(), " ") while (.jigsaw_env$xi < .jigsaw_env$xn) { str <- paste0(str, "C ", p1l(), " ", p1w(), " ", p2l(), " ", p2w(), " ", p3l(), " ", p3w(), " ") str <- paste0(str, "C ", p4l(), " ", p4w(), " ", p5l(), " ", p5w(), " ", p6l(), " ", p6w(), " ") str <- paste0(str, "C ", p7l(), " ", p7w(), " ", p8l(), " ", p8w(), " ", p9l(), " ", p9w(), " ") next_tab() .jigsaw_env$xi <- .jigsaw_env$xi + 1 } } return(str) } gen_dv <- function() { str <- "" .jigsaw_env$vertical <- 1 for (xi in 1:(.jigsaw_env$xn - 1)) { .jigsaw_env$xi <- xi .jigsaw_env$yi <- 0 first() str <- paste0(str, "M ", p0w(), ",", p0l(), " ") while (.jigsaw_env$yi < .jigsaw_env$yn) { str <- paste0(str, "C ", p1w(), " ", p1l(), " ", p2w(), " ", p2l(), " ", p3w(), " ", p3l(), " ") str <- paste0(str, "C ", p4w(), " ", p4l(), " ", p5w(), " ", p5l(), " ", p6w(), " ", p6l(), " ") str <- paste0(str, "C ", p7w(), " ", p7l(), " ", p8w(), " ", p8l(), " ", p9w(), " ", p9l(), " ") next_tab() .jigsaw_env$yi <- .jigsaw_env$yi + 1 } } return(str) } gen_db <- function() { offset <- .jigsaw_env$offset width <- .jigsaw_env$width height <- .jigsaw_env$height radius <- .jigsaw_env$radius str <- "" str <- paste0(str, "M ", (offset + radius), " ", (offset), " ") str <- paste0(str, "L ", (offset + width - radius), " ", (offset), " ") str <- paste0(str, "A ", (radius), " ", (radius), " 0 0 1 ", (offset + width), " ", (offset + radius), " ") str <- paste0(str, "L ", (offset + width), " ", (offset + height - radius), " ") str <- paste0(str, "A ", (radius), " ", (radius), " 0 0 1 ", (offset + width - radius), " ", (offset + height), " ") str <- paste0(str, "L ", (offset + radius), " ", (offset + height), " ") str <- paste0(str, "A ", (radius), " ", (radius), " 0 0 1 ", (offset), " ", (offset + height - radius), " ") str <- paste0(str, "L ", (offset), " ", (offset + radius), " ") str <- paste0(str, "A ", (radius), " ", (radius), " 0 0 1 ", (offset + radius), " ", (offset), " ") return(str) } #' Generate jigsaw puzzle SVG (main function) #' @param seed Random seed #' @param tabsize Tab size as percentage (0-100). Default: 6. #' @param jitter Jitter percentage (0-13) #' @param width Puzzle width in mm #' @param height Puzzle height in mm #' @param unit Measurement unit (default "mm") #' @param dpi Resolution in dots per inch (default 96) #' @param radius Corner radius in mm #' @param xn Number of columns #' @param yn Number of rows #' @return List containing SVG path data generate_jigsaw_svg <- function(seed = NULL, tabsize = 6, jitter = 4, width = 300, height = 200, unit = "mm", dpi = 96, radius = 2.0, xn = 15, yn = 10) { # Convert to mm for internal calculations if needed if (unit == "px") { width_mm <- width * 25.4 / dpi height_mm <- height * 25.4 / dpi radius_mm <- radius * 25.4 / dpi } else { width_mm <- width height_mm <- height radius_mm <- radius } # Initialize environment init_jigsaw(seed, tabsize, jitter, width_mm, height_mm, unit, dpi, radius_mm, xn, yn) # Generate path data horizontal_paths <- gen_dh() vertical_paths <- gen_dv() border_paths <- gen_db() # Create SVG dimensions based on unit and DPI if (unit == "px") { svg_width <- sprintf("%.0f", width) svg_height <- sprintf("%.0f", height) # ViewBox uses mm units for consistency in path coordinates viewbox_w <- width_mm viewbox_h <- height_mm } else { svg_width <- sprintf("%.0fmm", width) svg_height <- sprintf("%.0fmm", height) viewbox_w <- width viewbox_h <- height } # Create complete SVG svg_content <- paste0( '', '', '', '', '' ) return(list( svg = svg_content, horizontal = horizontal_paths, vertical = vertical_paths, border = border_paths, parameters = list( seed = .jigsaw_env$seed_initial, tabsize = tabsize, jitter = jitter, width = width, height = height, unit = unit, dpi = dpi, width_mm = width_mm, height_mm = height_mm, radius = radius, xn = xn, yn = yn ) )) } #' Save SVG to file #' @param puzzle_data Output from generate_jigsaw_svg() #' @param filename Output filename (default: "jigsaw.svg") save_jigsaw_svg <- function(puzzle_data, filename = "jigsaw.svg") { # Ensure parent directory exists output_parent <- dirname(filename) if (nzchar(output_parent) && output_parent != "." && !dir.exists(output_parent)) { dir.create(output_parent, recursive = TRUE) } writeLines(puzzle_data$svg, filename) log_success("Saved jigsaw puzzle to: {.file {filename}}") } #' Print puzzle parameters #' @param puzzle_data Output from generate_jigsaw_svg() print_puzzle_info <- function(puzzle_data) { params <- puzzle_data$parameters log_subheader("Jigsaw Puzzle Parameters") log_params("Puzzle Configuration", list( Seed = params$seed, Size = paste0(params$width, " x ", params$height, " mm"), Pieces = paste0(params$xn, " x ", params$yn, " = ", params$xn * params$yn, " total"), "Tab size" = paste0(params$tabsize, "%"), Jitter = paste0(params$jitter, "%"), "Corner radius" = paste0(params$radius, " mm") )) } # Note: Example usage moved to inst/examples/rectangular_puzzle_example.R # to avoid auto-execution when sourcing this file for function definitions