From 7073daa14437706c4be22232f7199e46a81e453a Mon Sep 17 00:00:00 2001 From: Sahil Gupta Date: Fri, 2 Jan 2026 17:23:22 +0530 Subject: [PATCH 1/4] adding qr node --- Cargo.lock | 7 ++++ Cargo.toml | 1 + node-graph/nodes/vector/Cargo.toml | 1 + .../nodes/vector/src/generator_nodes.rs | 34 +++++++++++++++++++ 4 files changed, 43 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 24637bbd89..95aee03503 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4635,6 +4635,12 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "qrcodegen" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4339fc7a1021c9c1621d87f5e3505f2805c8c105420ba2f2a4df86814590c142" + [[package]] name = "quick-error" version = "2.0.1" @@ -6701,6 +6707,7 @@ dependencies = [ "kurbo 0.12.0", "log", "node-macro", + "qrcodegen", "rand 0.9.2", "rustc-hash 2.1.1", "serde", diff --git a/Cargo.toml b/Cargo.toml index 1a7fbd44bb..cce4a8d9ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -239,6 +239,7 @@ spin = "0.10" clap = "4.5" spirv-std = { git = "https://github.com/Firestar99/rust-gpu-new", rev = "c12f216121820580731440ee79ebc7403d6ea04f", features = ["bytemuck"] } cargo-gpu = { git = "https://github.com/Firestar99/cargo-gpu", rev = "3952a22d16edbd38689f3a876e417899f21e1fe7", default-features = false } +qrcodegen = "1.8" [workspace.lints.rust] unexpected_cfgs = { level = "allow", check-cfg = ['cfg(target_arch, values("spirv"))'] } diff --git a/node-graph/nodes/vector/Cargo.toml b/node-graph/nodes/vector/Cargo.toml index c11a14f382..0672d4a886 100644 --- a/node-graph/nodes/vector/Cargo.toml +++ b/node-graph/nodes/vector/Cargo.toml @@ -23,6 +23,7 @@ kurbo = { workspace = true } rand = { workspace = true } rustc-hash = { workspace = true } log = { workspace = true } +qrcodegen = { workspace = true } # Optional workspace dependencies serde = { workspace = true, optional = true } diff --git a/node-graph/nodes/vector/src/generator_nodes.rs b/node-graph/nodes/vector/src/generator_nodes.rs index df587528ef..5660de0803 100644 --- a/node-graph/nodes/vector/src/generator_nodes.rs +++ b/node-graph/nodes/vector/src/generator_nodes.rs @@ -186,6 +186,33 @@ fn star( Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_star_polygon(DVec2::splat(-diameter), points, diameter, inner_diameter))) } +/// Generates a QR code from the input text. +#[node_macro::node(category("Vector: Shape"))] +fn qr_code(_: impl Ctx, _primary: (), #[default("https://graphite.art")] text: String +) -> Table { + let ecc = qrcodegen::QrCodeEcc::Medium; + + let Ok(qr_code) = qrcodegen::QrCode::encode_text(&text, ecc) else { + return Table::default(); + }; + + let size = qr_code.size(); + let mut vector = Vector::default(); + let offset = DVec2::splat(size as f64 / -2.); + + for y in 0..size { + for x in 0..size { + if qr_code.get_module(x, y) { + let corner1 = offset + DVec2::new(x as f64, y as f64); + let corner2 = corner1 + DVec2::splat(1.); + vector.append_subpath(subpath::Subpath::new_rect(corner1, corner2), false); + } + } + } + + Table::new_from_element(vector) +} + /// Generates a line with endpoints at the two chosen coordinates. #[node_macro::node(category("Vector: Shape"))] fn line( @@ -349,4 +376,11 @@ mod tests { assert!([90., 150., 40.].into_iter().any(|target| (target - angle).abs() < 1e-10), "unexpected angle of {angle}") } } + + #[test] + fn qr_code_test() { + let qr = qr_code((), (), "https://graphite.art".to_string()); + assert!(qr.iter().next().unwrap().element.point_domain.ids().len() > 0); + assert!(qr.iter().next().unwrap().element.segment_domain.ids().len() > 0); + } } From 94341f2f744c9d7d123f3ecfb4ddd745705ab1e7 Mon Sep 17 00:00:00 2001 From: Sahil Gupta Date: Sat, 3 Jan 2026 15:25:29 +0530 Subject: [PATCH 2/4] allow merging adjacent tiles in qr code node --- .../nodes/vector/src/generator_nodes.rs | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/node-graph/nodes/vector/src/generator_nodes.rs b/node-graph/nodes/vector/src/generator_nodes.rs index 5660de0803..ee3d5cffe7 100644 --- a/node-graph/nodes/vector/src/generator_nodes.rs +++ b/node-graph/nodes/vector/src/generator_nodes.rs @@ -188,7 +188,13 @@ fn star( /// Generates a QR code from the input text. #[node_macro::node(category("Vector: Shape"))] -fn qr_code(_: impl Ctx, _primary: (), #[default("https://graphite.art")] text: String +fn qr_code( + _: impl Ctx, + _primary: (), + #[default("https://graphite.art")] text: String, + #[default(true)] + #[name("Merge Adjacent Tiles")] + merge: bool, ) -> Table { let ecc = qrcodegen::QrCodeEcc::Medium; @@ -201,11 +207,23 @@ fn qr_code(_: impl Ctx, _primary: (), #[default("https://graphite.art")] text: S let offset = DVec2::splat(size as f64 / -2.); for y in 0..size { - for x in 0..size { + let mut x = 0; + while x < size { if qr_code.get_module(x, y) { - let corner1 = offset + DVec2::new(x as f64, y as f64); - let corner2 = corner1 + DVec2::splat(1.); + let start_x = x; + x += 1; + if merge { + while x < size && qr_code.get_module(x, y) { + x += 1; + } + } + let end_x = x; + + let corner1 = offset + DVec2::new(start_x as f64, y as f64); + let corner2 = offset + DVec2::new(end_x as f64, (y + 1) as f64); vector.append_subpath(subpath::Subpath::new_rect(corner1, corner2), false); + } else { + x += 1; } } } @@ -379,7 +397,7 @@ mod tests { #[test] fn qr_code_test() { - let qr = qr_code((), (), "https://graphite.art".to_string()); + let qr = qr_code((), (), "https://graphite.art".to_string(), true); assert!(qr.iter().next().unwrap().element.point_domain.ids().len() > 0); assert!(qr.iter().next().unwrap().element.segment_domain.ids().len() > 0); } From de5dc825cd74ce5200d2b36fea073d2eb15a01c0 Mon Sep 17 00:00:00 2001 From: Sahil Gupta Date: Mon, 5 Jan 2026 15:02:02 +0530 Subject: [PATCH 3/4] fix tile merging --- .../nodes/vector/src/generator_nodes.rs | 100 +++++++++++++++--- 1 file changed, 83 insertions(+), 17 deletions(-) diff --git a/node-graph/nodes/vector/src/generator_nodes.rs b/node-graph/nodes/vector/src/generator_nodes.rs index ee3d5cffe7..6e089bce5c 100644 --- a/node-graph/nodes/vector/src/generator_nodes.rs +++ b/node-graph/nodes/vector/src/generator_nodes.rs @@ -204,26 +204,92 @@ fn qr_code( let size = qr_code.size(); let mut vector = Vector::default(); - let offset = DVec2::splat(size as f64 / -2.); - - for y in 0..size { - let mut x = 0; - while x < size { - if qr_code.get_module(x, y) { - let start_x = x; - x += 1; - if merge { - while x < size && qr_code.get_module(x, y) { - x += 1; + let offset = DVec2::splat(0.0); + + if merge { + use std::collections::{HashMap, HashSet, VecDeque}; + + let mut remaining: HashSet<(i32, i32)> = HashSet::new(); + for y in 0..size { + for x in 0..size { + if qr_code.get_module(x, y) { + remaining.insert((x, y)); + } + } + } + + while let Some(&(start_x, start_y)) = remaining.iter().next() { + let mut island = HashSet::new(); + let mut queue = VecDeque::new(); + queue.push_back((start_x, start_y)); + remaining.remove(&(start_x, start_y)); + + while let Some((x, y)) = queue.pop_front() { + island.insert((x, y)); + for (dx, dy) in [(0, 1), (0, -1), (1, 0), (-1, 0)] { + let nx = x + dx; + let ny = y + dy; + if remaining.remove(&(nx, ny)) { + queue.push_back((nx, ny)); + } + } + } + + let mut island_edges = HashSet::new(); + for &(x, y) in &island { + for (p1, p2) in [((x, y), (x + 1, y)), ((x + 1, y), (x + 1, y + 1)), ((x + 1, y + 1), (x, y + 1)), ((x, y + 1), (x, y))] { + if !island_edges.remove(&(p2, p1)) { + island_edges.insert((p1, p2)); + } + } + } + + let mut adjacency: HashMap<(i32, i32), Vec<(i32, i32)>> = HashMap::new(); + for (p1, p2) in island_edges { + adjacency.entry(p1).or_default().push(p2); + } + + while let Some(&start_point) = adjacency.keys().next() { + let mut loop_points = Vec::new(); + let mut current = start_point; + + loop { + loop_points.push(DVec2::new(current.0 as f64, current.1 as f64)); + let next = adjacency.get_mut(¤t).and_then(|n| n.pop()).unwrap(); + if adjacency.get(¤t).map_or(false, |n| n.is_empty()) { + adjacency.remove(¤t); + } + current = next; + if current == start_point { + break; } } - let end_x = x; - let corner1 = offset + DVec2::new(start_x as f64, y as f64); - let corner2 = offset + DVec2::new(end_x as f64, (y + 1) as f64); - vector.append_subpath(subpath::Subpath::new_rect(corner1, corner2), false); - } else { - x += 1; + if loop_points.len() > 2 { + let mut simplified = Vec::new(); + for i in 0..loop_points.len() { + let prev = loop_points[(i + loop_points.len() - 1) % loop_points.len()]; + let curr = loop_points[i]; + let next = loop_points[(i + 1) % loop_points.len()]; + + if (curr - prev).perp_dot(next - curr).abs() > 1e-6 { + simplified.push(curr + offset); + } + } + if !simplified.is_empty() { + vector.append_subpath(subpath::Subpath::from_anchors(simplified, true), false); + } + } + } + } + } else { + for y in 0..size { + for x in 0..size { + if qr_code.get_module(x, y) { + let corner1 = offset + DVec2::new(x as f64, y as f64); + let corner2 = corner1 + DVec2::splat(1.); + vector.append_subpath(subpath::Subpath::new_rect(corner1, corner2), false); + } } } } From 869a5f8ba17c43a56b44cb1245d3e76b3c939cae Mon Sep 17 00:00:00 2001 From: Sahil Gupta Date: Thu, 8 Jan 2026 16:16:09 +0530 Subject: [PATCH 4/4] making sqare merging deterministic --- .../nodes/vector/src/generator_nodes.rs | 98 ++------------- node-graph/nodes/vector/src/lib.rs | 1 + .../nodes/vector/src/merge_qr_squares.rs | 118 ++++++++++++++++++ 3 files changed, 127 insertions(+), 90 deletions(-) create mode 100644 node-graph/nodes/vector/src/merge_qr_squares.rs diff --git a/node-graph/nodes/vector/src/generator_nodes.rs b/node-graph/nodes/vector/src/generator_nodes.rs index 6e089bce5c..9242dccc24 100644 --- a/node-graph/nodes/vector/src/generator_nodes.rs +++ b/node-graph/nodes/vector/src/generator_nodes.rs @@ -187,14 +187,8 @@ fn star( } /// Generates a QR code from the input text. -#[node_macro::node(category("Vector: Shape"))] -fn qr_code( - _: impl Ctx, - _primary: (), - #[default("https://graphite.art")] text: String, - #[default(true)] - #[name("Merge Adjacent Tiles")] - merge: bool, +#[node_macro::node(category("Vector: Shape"), name("QR Code"))] +fn qr_code(_: impl Ctx, _primary: (), #[default("https://graphite.art")] text: String, #[default(false)] individual_squares: bool ) -> Table { let ecc = qrcodegen::QrCodeEcc::Medium; @@ -202,98 +196,22 @@ fn qr_code( return Table::default(); }; - let size = qr_code.size(); + let size = qr_code.size() as usize; let mut vector = Vector::default(); - let offset = DVec2::splat(0.0); - if merge { - use std::collections::{HashMap, HashSet, VecDeque}; - - let mut remaining: HashSet<(i32, i32)> = HashSet::new(); + if individual_squares { for y in 0..size { for x in 0..size { - if qr_code.get_module(x, y) { - remaining.insert((x, y)); - } - } - } - - while let Some(&(start_x, start_y)) = remaining.iter().next() { - let mut island = HashSet::new(); - let mut queue = VecDeque::new(); - queue.push_back((start_x, start_y)); - remaining.remove(&(start_x, start_y)); - - while let Some((x, y)) = queue.pop_front() { - island.insert((x, y)); - for (dx, dy) in [(0, 1), (0, -1), (1, 0), (-1, 0)] { - let nx = x + dx; - let ny = y + dy; - if remaining.remove(&(nx, ny)) { - queue.push_back((nx, ny)); - } - } - } - - let mut island_edges = HashSet::new(); - for &(x, y) in &island { - for (p1, p2) in [((x, y), (x + 1, y)), ((x + 1, y), (x + 1, y + 1)), ((x + 1, y + 1), (x, y + 1)), ((x, y + 1), (x, y))] { - if !island_edges.remove(&(p2, p1)) { - island_edges.insert((p1, p2)); - } - } - } - - let mut adjacency: HashMap<(i32, i32), Vec<(i32, i32)>> = HashMap::new(); - for (p1, p2) in island_edges { - adjacency.entry(p1).or_default().push(p2); - } - - while let Some(&start_point) = adjacency.keys().next() { - let mut loop_points = Vec::new(); - let mut current = start_point; - - loop { - loop_points.push(DVec2::new(current.0 as f64, current.1 as f64)); - let next = adjacency.get_mut(¤t).and_then(|n| n.pop()).unwrap(); - if adjacency.get(¤t).map_or(false, |n| n.is_empty()) { - adjacency.remove(¤t); - } - current = next; - if current == start_point { - break; - } - } - - if loop_points.len() > 2 { - let mut simplified = Vec::new(); - for i in 0..loop_points.len() { - let prev = loop_points[(i + loop_points.len() - 1) % loop_points.len()]; - let curr = loop_points[i]; - let next = loop_points[(i + 1) % loop_points.len()]; - - if (curr - prev).perp_dot(next - curr).abs() > 1e-6 { - simplified.push(curr + offset); - } - } - if !simplified.is_empty() { - vector.append_subpath(subpath::Subpath::from_anchors(simplified, true), false); - } - } - } - } - } else { - for y in 0..size { - for x in 0..size { - if qr_code.get_module(x, y) { - let corner1 = offset + DVec2::new(x as f64, y as f64); + if qr_code.get_module(x as i32, y as i32) { + let corner1 = DVec2::new(x as f64, y as f64); let corner2 = corner1 + DVec2::splat(1.); vector.append_subpath(subpath::Subpath::new_rect(corner1, corner2), false); } } } + } else { + crate::merge_qr_squares::merge_qr_squares(&qr_code, &mut vector); } - Table::new_from_element(vector) } diff --git a/node-graph/nodes/vector/src/lib.rs b/node-graph/nodes/vector/src/lib.rs index b830e6f5ed..71c730aa35 100644 --- a/node-graph/nodes/vector/src/lib.rs +++ b/node-graph/nodes/vector/src/lib.rs @@ -1,5 +1,6 @@ pub mod generator_nodes; pub mod instance; +pub mod merge_qr_squares; pub mod vector_modification_nodes; mod vector_nodes; diff --git a/node-graph/nodes/vector/src/merge_qr_squares.rs b/node-graph/nodes/vector/src/merge_qr_squares.rs new file mode 100644 index 0000000000..ea2ac61372 --- /dev/null +++ b/node-graph/nodes/vector/src/merge_qr_squares.rs @@ -0,0 +1,118 @@ +use glam::DVec2; +use graphic_types::Vector; +use std::collections::VecDeque; +use vector_types::subpath; + +pub fn merge_qr_squares(qr_code: &qrcodegen::QrCode, vector: &mut Vector) { + let size = qr_code.size() as usize; + + // 0 = empty + // 1 = black, unvisited + // 2 = black, current island + let mut remaining = vec![vec![0u8; size]; size]; + + for y in 0..size { + for x in 0..size { + if qr_code.get_module(x as i32, y as i32) { + remaining[y][x] = 1; + } + } + } + + for y in 0..size { + for x in 0..size { + if remaining[y][x] != 1 { + continue; + } + + // fill island + let mut island = Vec::new(); + let mut queue = VecDeque::new(); + queue.push_back((x, y)); + remaining[y][x] = 2; + + while let Some((ix, iy)) = queue.pop_front() { + island.push((ix, iy)); + + for (dx, dy) in [(0, 1), (0, -1), (1, 0), (-1, 0)] { + let nx = ix as i32 + dx; + let ny = iy as i32 + dy; + + if nx >= 0 && nx < size as i32 && ny >= 0 && ny < size as i32 && remaining[ny as usize][nx as usize] == 1 { + remaining[ny as usize][nx as usize] = 2; + queue.push_back((nx as usize, ny as usize)); + } + } + } + + // boundary detection + let mut outbound = vec![vec![0u8; size + 1]; size + 1]; + + for &(ix, iy) in &island { + if iy == 0 || remaining[iy - 1][ix] != 2 { + outbound[iy][ix] |= 1 << 0; + } + if ix == size - 1 || remaining[iy][ix + 1] != 2 { + outbound[iy][ix + 1] |= 1 << 1; + } + if iy == size - 1 || remaining[iy + 1][ix] != 2 { + outbound[iy + 1][ix + 1] |= 1 << 2; + } + if ix == 0 || remaining[iy][ix - 1] != 2 { + outbound[iy + 1][ix] |= 1 << 3; + } + } + + // tracing loops + for vy in 0..=size { + for vx in 0..=size { + while outbound[vy][vx] != 0 { + let mut dir = outbound[vy][vx].trailing_zeros() as usize; + let start = (vx, vy); + let mut current = start; + let mut points = Vec::new(); + + loop { + points.push(DVec2::new(current.0 as f64, current.1 as f64)); + outbound[current.1][current.0] &= !(1 << dir); + + current = match dir { + 0 => (current.0 + 1, current.1), + 1 => (current.0, current.1 + 1), + 2 => (current.0 - 1, current.1), + 3 => (current.0, current.1 - 1), + _ => unreachable!(), + }; + + if current == start { + break; + } + dir = outbound[current.1][current.0].trailing_zeros() as usize; + } + + if points.len() > 2 { + let mut simplified = Vec::new(); + for i in 0..points.len() { + let prev = points[(i + points.len() - 1) % points.len()]; + let curr = points[i]; + let next = points[(i + 1) % points.len()]; + if (curr - prev).perp_dot(next - curr).abs() > 1e-6 { + simplified.push(curr); + } + } + + if !simplified.is_empty() { + vector.append_subpath(subpath::Subpath::from_anchors(simplified, true), false); + } + } + } + } + } + + // marking island as processed + for &(ix, iy) in &island { + remaining[iy][ix] = 0; + } + } + } +}