{"spec_id":"wireframe-3d-basic","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' wireframe-3d-basic: Basic 3D Wireframe Plot\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 87/100 | Updated: 2026-09-10\n\nlibrary(ggplot2)\nlibrary(ragg)\n\nset.seed(42)\n\n# --- Theme tokens -------------------------------------------------------------\nTHEME    <- Sys.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG  <- if (THEME == \"light\") \"#FAF8F1\" else \"#1A1A17\"\nINK      <- if (THEME == \"light\") \"#1A1A17\" else \"#F0EFE8\"\nINK_SOFT <- if (THEME == \"light\") \"#4A4A44\" else \"#B8B7B0\"\nBRAND    <- \"#009E73\"\n\n# --- Camera: orthographic projection (elevation 30, azimuth 45) ---------------\n# ggplot2 has no 3D grammar, so the mesh is projected to 2D screen coordinates\n# ourselves (the same technique any static 3D renderer uses under the hood),\n# then drawn with plain geom_polygon/geom_segment/geom_text.\nelev <- 30 * pi / 180\nazim <- 45 * pi / 180\n\nview_dir  <- c(cos(elev) * cos(azim), cos(elev) * sin(azim), sin(elev))\nworld_up  <- c(0, 0, 1)\nright_axis <- c(\n  view_dir[2] * world_up[3] - view_dir[3] * world_up[2],\n  view_dir[3] * world_up[1] - view_dir[1] * world_up[3],\n  view_dir[1] * world_up[2] - view_dir[2] * world_up[1]\n)\nright_axis <- right_axis / sqrt(sum(right_axis^2))\nup_axis <- c(\n  right_axis[2] * view_dir[3] - right_axis[3] * view_dir[2],\n  right_axis[3] * view_dir[1] - right_axis[1] * view_dir[3],\n  right_axis[1] * view_dir[2] - right_axis[2] * view_dir[1]\n)\n\nz_lift <- 3.5  # visual height exaggeration so the shallow ripple reads clearly\nproject_x <- function(x, y, z) x * right_axis[1] + y * right_axis[2] + z * z_lift * right_axis[3]\nproject_y <- function(x, y, z) x * up_axis[1]    + y * up_axis[2]    + z * z_lift * up_axis[3]\ndepth_toward_camera <- function(x, y, z) x * view_dir[1] + y * view_dir[2] + z * z_lift * view_dir[3]\n\n# --- Data: ripple surface z = sin(sqrt(x^2 + y^2)) -----------------------------\ngrid_n <- 30\nx_vals <- seq(-6, 6, length.out = grid_n)\ny_vals <- seq(-6, 6, length.out = grid_n)\nz_fun  <- function(x, y) sin(sqrt(x^2 + y^2))\n\nz_range <- range(outer(x_vals, y_vals, z_fun))\nfloor_z <- z_range[1] - 0.3\nceil_z  <- z_range[2] + 0.3\n\n# --- Mesh quads with painter's-algorithm hidden-line removal ------------------\n# Each grid cell becomes a filled quad. Quads are drawn back-to-front (farthest\n# from the camera first) with an opaque page-background fill, so nearer quads\n# occlude the grid lines sitting behind them - the same trick base R's persp()\n# uses instead of a real z-buffer. This lets the mesh resolution sit inside the\n# spec's recommended 20x20-50x50 range without the interior crosshatching a\n# flat semi-transparent wireframe produces.\nn_cells <- (grid_n - 1)^2\nmesh <- data.frame(\n  quad_id = integer(n_cells * 4),\n  corner  = integer(n_cells * 4),\n  px      = numeric(n_cells * 4),\n  py      = numeric(n_cells * 4)\n)\nquad_depth <- numeric(n_cells)\n\nrow  <- 1\nquad <- 1\nfor (i in seq_len(grid_n - 1)) {\n  for (j in seq_len(grid_n - 1)) {\n    cx <- c(x_vals[i], x_vals[i + 1], x_vals[i + 1], x_vals[i])\n    cy <- c(y_vals[j], y_vals[j],     y_vals[j + 1],  y_vals[j + 1])\n    cz <- z_fun(cx, cy)\n    idx <- row:(row + 3)\n    mesh$quad_id[idx] <- quad\n    mesh$corner[idx]  <- 1:4\n    mesh$px[idx] <- project_x(cx, cy, cz)\n    mesh$py[idx] <- project_y(cx, cy, cz)\n    quad_depth[quad] <- mean(depth_toward_camera(cx, cy, cz))\n    row  <- row + 4\n    quad <- quad + 1\n  }\n}\n\n# Farthest quad gets draw_rank 1 (painted first); nearest gets n_cells (painted\n# last, on top). The fill itself must stay fully opaque for the occlusion to\n# work - only the edge colour's alpha channel is faded with depth, as a subtle\n# depth cue (bolder edges up close, softer far away).\ndraw_rank      <- rank(quad_depth, ties.method = \"first\")\nmesh$draw_rank <- draw_rank[mesh$quad_id]\nfade           <- 0.55 + 0.45 * (mesh$draw_rank - 1) / (n_cells - 1)\nbrand_rgb      <- col2rgb(BRAND) / 255\nmesh$edge_color <- rgb(brand_rgb[1], brand_rgb[2], brand_rgb[3], alpha = fade)\nmesh <- mesh[order(mesh$draw_rank, mesh$corner), ]\n\n# --- Floor reference plane (spatial grounding) ---------------------------------\nfloor_plane <- data.frame(\n  x = c(-6, 6, 6, -6),\n  y = c(-6, -6, 6, 6),\n  z = floor_z\n)\nfloor_plane$px <- project_x(floor_plane$x, floor_plane$y, floor_plane$z)\nfloor_plane$py <- project_y(floor_plane$x, floor_plane$y, floor_plane$z)\n\n# --- Axis box: three edges meeting at the front-left-bottom corner ------------\naxis_lines <- data.frame(\n  x    = c(-6, -6, -6),\n  y    = c(-6, -6, -6),\n  z    = c(floor_z, floor_z, floor_z),\n  xend = c(6, -6, -6),\n  yend = c(-6, 6, -6),\n  zend = c(floor_z, floor_z, ceil_z)\n)\naxis_lines$px    <- project_x(axis_lines$x, axis_lines$y, axis_lines$z)\naxis_lines$py    <- project_y(axis_lines$x, axis_lines$y, axis_lines$z)\naxis_lines$pxend <- project_x(axis_lines$xend, axis_lines$yend, axis_lines$zend)\naxis_lines$pyend <- project_y(axis_lines$xend, axis_lines$yend, axis_lines$zend)\n\nx_breaks <- c(-6, -3, 0, 3, 6)\ny_breaks <- c(-6, -3, 0, 3, 6)\nz_breaks <- c(-1, 0, 1)\n\nticks <- rbind(\n  data.frame(x = x_breaks, y = -9.6, z = floor_z, label = x_breaks, axis = \"x\"),\n  data.frame(x = -9.6, y = y_breaks, z = floor_z, label = y_breaks, axis = \"y\")\n)\nticks$px <- project_x(ticks$x, ticks$y, ticks$z)\nticks$py <- project_y(ticks$x, ticks$y, ticks$z)\n\n# The X-tick and Y-tick label columns sit on the mesh's near side, where the\n# wireframe's screen footprint is widest, so a couple of low-value ticks\n# (\"-3\"/\"-6\") land inside the mesh's silhouette instead of clearing it. Nudge\n# each column sideways, away from the vertical Z axis, by a fixed screen\n# offset (same lateral-offset trick used for z_ticks below) - harmless for\n# the ticks that already clear the mesh, since it just adds margin.\ntick_clearance <- 5.5\nticks$px <- ticks$px + ifelse(ticks$axis == \"x\", tick_clearance, -tick_clearance)\n\n# Z ticks sit on the vertical axis line itself; nudge the label text\n# (not the axis line) sideways into the open gap left of the mesh, well past\n# the Y-axis tick column so the two label groups don't merge into one line.\nz_ticks <- data.frame(x = -6, y = -6, z = z_breaks, label = z_breaks)\nz_ticks$px <- project_x(z_ticks$x, z_ticks$y, z_ticks$z) - 13\nz_ticks$py <- project_y(z_ticks$x, z_ticks$y, z_ticks$z)\n\naxis_labels <- data.frame(\n  x     = c(9.4, -6, -6),\n  y     = c(-6, 9.4, -6),\n  z     = c(floor_z, floor_z, ceil_z + 1.0),\n  label = c(\"X\", \"Y\", \"Z\")\n)\naxis_labels$px <- project_x(axis_labels$x, axis_labels$y, axis_labels$z)\naxis_labels$py <- project_y(axis_labels$x, axis_labels$y, axis_labels$z)\n\n# --- Plot -----------------------------------------------------------------\np <- ggplot() +\n  geom_polygon(data = floor_plane, aes(px, py),\n               fill = NA, color = INK_SOFT, linewidth = 0.4, alpha = 0.4) +\n  geom_polygon(data = mesh, aes(px, py, group = draw_rank, colour = I(edge_color)),\n               fill = PAGE_BG, linewidth = 0.25) +\n  geom_segment(data = axis_lines, aes(x = px, y = py, xend = pxend, yend = pyend),\n               color = INK_SOFT, linewidth = 0.6) +\n  geom_text(data = ticks, aes(px, py, label = label),\n            color = INK_SOFT, size = 3.3) +\n  geom_text(data = z_ticks, aes(px, py, label = label),\n            color = INK_SOFT, size = 3.3) +\n  geom_text(data = axis_labels, aes(px, py, label = label),\n            color = INK, size = 3.6, fontface = \"bold\") +\n  labs(title = \"wireframe-3d-basic · r · ggplot2 · anyplot.ai\") +\n  coord_fixed(ratio = 1, clip = \"off\") +\n  theme_void(base_size = 8) +\n  theme(\n    plot.background  = element_rect(fill = PAGE_BG, color = PAGE_BG),\n    panel.background = element_rect(fill = PAGE_BG, color = NA),\n    plot.title       = element_text(color = INK, size = 12, hjust = 0.5, margin = margin(b = 14)),\n    plot.margin      = margin(t = 20, r = 30, b = 10, l = 30)\n  )\n\n# --- Save -----------------------------------------------------------------\nggsave(\n  filename = sprintf(\"plot-%s.png\", THEME),\n  plot     = p,\n  device   = ragg::agg_png,\n  width    = 8,\n  height   = 4.5,\n  units    = \"in\",\n  dpi      = 400\n)\n"}