Introduce QR Code node (#3565)

Co-authored-by: Timon <me@timon.zip>
This commit is contained in:
Sahil Gupta 2026-02-14 23:20:43 +05:30 committed by GitHub
parent 8b67840f0c
commit 6c10364c8c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 180 additions and 0 deletions

7
Cargo.lock generated
View File

@ -4659,6 +4659,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"
@ -6725,6 +6731,7 @@ dependencies = [
"kurbo 0.12.0",
"log",
"node-macro",
"qrcodegen",
"rand 0.9.2",
"rustc-hash 2.1.1",
"serde",

View File

@ -244,6 +244,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"))'] }

View File

@ -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 }

View File

@ -186,6 +186,51 @@ fn star<T: AsU64>(
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"), name("QR Code"))]
fn qr_code(
_: impl Ctx,
_primary: (),
#[default("https://graphite.art")] text: String,
/// Error correction level, from low (0) to high (3).
#[default(1)]
error_correction: u32,
#[default(false)] individual_squares: bool,
) -> Table<Vector> {
let ecc = match error_correction.min(3) {
0 => qrcodegen::QrCodeEcc::Low,
1 => qrcodegen::QrCodeEcc::Medium,
2 => qrcodegen::QrCodeEcc::Quartile,
3 => qrcodegen::QrCodeEcc::High,
_ => unreachable!(),
};
let Ok(qr_code) = qrcodegen::QrCode::encode_text(&text, ecc) else {
return Table::default();
};
let size = qr_code.size() as usize;
let mut vector = Vector::default();
if individual_squares {
for y in 0..size {
for x in 0..size {
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::from_anchors([corner1, DVec2::new(corner2.x, corner1.y), corner2, DVec2::new(corner1.x, corner2.y)], true),
false,
);
}
}
}
} else {
crate::merge_qr_squares::merge_qr_squares(&qr_code, &mut vector);
}
Table::new_from_element(vector)
}
/// Generates a line with endpoints at the two chosen coordinates.
#[node_macro::node(category("Vector: Shape"))]
fn arrow(
@ -359,4 +404,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(), 1, true);
assert!(qr.iter().next().unwrap().element.point_domain.ids().len() > 0);
assert!(qr.iter().next().unwrap().element.segment_domain.ids().len() > 0);
}
}

View File

@ -1,5 +1,6 @@
pub mod generator_nodes;
pub mod instance;
pub mod merge_qr_squares;
pub mod vector_modification_nodes;
mod vector_nodes;

View File

@ -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;
}
}
}
}