Improve the QR Code node (#3765)

This commit is contained in:
Keavon Chambers
2026-02-14 20:53:29 +00:00
committed by GitHub
parent 6c10364c8c
commit 8738e59c21
7 changed files with 69 additions and 32 deletions
Generated
+1
View File
@@ -2216,6 +2216,7 @@ dependencies = [
"text-nodes", "text-nodes",
"tokio", "tokio",
"url", "url",
"vector-nodes",
"wasm-bindgen", "wasm-bindgen",
"web-sys", "web-sys",
"wgpu-executor", "wgpu-executor",
@@ -25,6 +25,7 @@ use graphene_std::raster::{
use graphene_std::table::{Table, TableRow}; use graphene_std::table::{Table, TableRow};
use graphene_std::text::{Font, TextAlign}; use graphene_std::text::{Font, TextAlign};
use graphene_std::transform::{Footprint, ReferencePoint, Transform}; use graphene_std::transform::{Footprint, ReferencePoint, Transform};
use graphene_std::vector::QRCodeErrorCorrectionLevel;
use graphene_std::vector::misc::{ArcType, CentroidType, ExtrudeJoiningAlgorithm, GridType, MergeByDistanceAlgorithm, PointSpacingType, SpiralType}; use graphene_std::vector::misc::{ArcType, CentroidType, ExtrudeJoiningAlgorithm, GridType, MergeByDistanceAlgorithm, PointSpacingType, SpiralType};
use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
@@ -226,6 +227,7 @@ pub(crate) fn property_from_type(
Some(x) if x == TypeId::of::<BooleanOperation>() => enum_choice::<BooleanOperation>().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::<BooleanOperation>() => enum_choice::<BooleanOperation>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<CentroidType>() => enum_choice::<CentroidType>().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::<CentroidType>() => enum_choice::<CentroidType>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<LuminanceCalculation>() => enum_choice::<LuminanceCalculation>().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::<LuminanceCalculation>() => enum_choice::<LuminanceCalculation>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<QRCodeErrorCorrectionLevel>() => enum_choice::<QRCodeErrorCorrectionLevel>().for_socket(default_info).property_row(),
// ===== // =====
// OTHER // OTHER
// ===== // =====
+1
View File
@@ -21,6 +21,7 @@ graphene-core = { workspace = true }
graphene-application-io = { workspace = true } graphene-application-io = { workspace = true }
rendering = { workspace = true } rendering = { workspace = true }
raster-nodes = { workspace = true } raster-nodes = { workspace = true }
vector-nodes = { workspace = true }
graphic-types = { workspace = true } graphic-types = { workspace = true }
text-nodes = { workspace = true } text-nodes = { workspace = true }
@@ -238,6 +238,7 @@ tagged_value! {
Fill(vector::style::Fill), Fill(vector::style::Fill),
BlendMode(core_types::blending::BlendMode), BlendMode(core_types::blending::BlendMode),
LuminanceCalculation(raster_nodes::adjustments::LuminanceCalculation), LuminanceCalculation(raster_nodes::adjustments::LuminanceCalculation),
QRCodeErrorCorrectionLevel(vector_nodes::generator_nodes::QRCodeErrorCorrectionLevel),
XY(graphene_core::extract_xy::XY), XY(graphene_core::extract_xy::XY),
RedGreenBlue(raster_nodes::adjustments::RedGreenBlue), RedGreenBlue(raster_nodes::adjustments::RedGreenBlue),
RedGreenBlueAlpha(raster_nodes::adjustments::RedGreenBlueAlpha), RedGreenBlueAlpha(raster_nodes::adjustments::RedGreenBlueAlpha),
@@ -200,6 +200,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::Fill]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::Fill]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::blending::BlendMode]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::blending::BlendMode]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::LuminanceCalculation]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::LuminanceCalculation]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::QRCodeErrorCorrectionLevel]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::extract_xy::XY]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::extract_xy::XY]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::RedGreenBlue]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::RedGreenBlue]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::RedGreenBlueAlpha]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::RedGreenBlueAlpha]),
+54 -29
View File
@@ -1,6 +1,7 @@
use core_types::Ctx;
use core_types::registry::types::{Angle, PixelSize}; use core_types::registry::types::{Angle, PixelSize};
use core_types::table::Table; use core_types::table::Table;
use core_types::{Ctx, specta};
use dyn_any::DynAny;
use glam::DVec2; use glam::DVec2;
use graphic_types::Vector; use graphic_types::Vector;
use vector_types::subpath; use vector_types::subpath;
@@ -186,48 +187,72 @@ fn star<T: AsU64>(
Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_star_polygon(DVec2::splat(-diameter), points, diameter, inner_diameter))) Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_star_polygon(DVec2::splat(-diameter), points, diameter, inner_diameter)))
} }
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
#[widget(Radio)]
pub enum QRCodeErrorCorrectionLevel {
/// Allows recovery from up to 7% data loss.
#[default]
Low,
/// Allows recovery from up to 15% data loss.
Medium,
/// Allows recovery from up to 25% data loss.
Quartile,
/// Allows recovery from up to 30% data loss.
High,
}
/// Generates a QR code from the input text. /// Generates a QR code from the input text.
#[node_macro::node(category("Vector: Shape"), name("QR Code"))] #[node_macro::node(category("Vector: Shape"), name("QR Code"))]
fn qr_code( fn qr_code(
_: impl Ctx, _: impl Ctx,
_primary: (), _primary: (),
#[default("https://graphite.art")] text: String, #[widget(ParsedWidgetOverride::Custom = "text_area")]
/// Error correction level, from low (0) to high (3). #[default("https://graphite.art")]
#[default(1)] text: String,
error_correction: u32, #[widget(ParsedWidgetOverride::Hidden)] has_size: bool,
#[unit(" px")]
#[hard_min(1.)]
#[widget(ParsedWidgetOverride::Custom = "optional_f64")]
size: f64,
error_correction: QRCodeErrorCorrectionLevel,
#[default(false)] individual_squares: bool, #[default(false)] individual_squares: bool,
) -> Table<Vector> { ) -> Table<Vector> {
let ecc = match error_correction.min(3) { let ecc = match error_correction {
0 => qrcodegen::QrCodeEcc::Low, QRCodeErrorCorrectionLevel::Low => qrcodegen::QrCodeEcc::Low,
1 => qrcodegen::QrCodeEcc::Medium, QRCodeErrorCorrectionLevel::Medium => qrcodegen::QrCodeEcc::Medium,
2 => qrcodegen::QrCodeEcc::Quartile, QRCodeErrorCorrectionLevel::Quartile => qrcodegen::QrCodeEcc::Quartile,
3 => qrcodegen::QrCodeEcc::High, QRCodeErrorCorrectionLevel::High => qrcodegen::QrCodeEcc::High,
_ => unreachable!(),
}; };
let Ok(qr_code) = qrcodegen::QrCode::encode_text(&text, ecc) else { let Ok(qr_code) = qrcodegen::QrCode::encode_text(&text, ecc) else { return Table::default() };
return Table::default();
};
let size = qr_code.size() as usize; let mut vector = match individual_squares {
let mut vector = Vector::default(); true => {
let mut vector = Vector::default();
if individual_squares { let dimension = qr_code.size() as usize;
for y in 0..size { for y in 0..dimension {
for x in 0..size { for x in 0..dimension {
if qr_code.get_module(x as i32, y as i32) { if qr_code.get_module(x as i32, y as i32) {
let corner1 = DVec2::new(x as f64, y as f64); let corner1 = DVec2::new(x as f64, y as f64);
let corner2 = corner1 + DVec2::splat(1.); let corner2 = corner1 + DVec2::splat(1.);
vector.append_subpath( vector.append_subpath(
subpath::Subpath::from_anchors([corner1, DVec2::new(corner2.x, corner1.y), corner2, DVec2::new(corner1.x, corner2.y)], true), subpath::Subpath::from_anchors([corner1, DVec2::new(corner2.x, corner1.y), corner2, DVec2::new(corner1.x, corner2.y)], true),
false, false,
); );
}
} }
} }
vector
} }
} else { false => crate::merge_qr_squares::merge_qr_squares(&qr_code),
crate::merge_qr_squares::merge_qr_squares(&qr_code, &mut vector); };
if has_size {
vector.transform(glam::DAffine2::from_scale(DVec2::splat(size.max(1.) / qr_code.size() as f64)));
} }
Table::new_from_element(vector) Table::new_from_element(vector)
} }
@@ -407,7 +432,7 @@ mod tests {
#[test] #[test]
fn qr_code_test() { fn qr_code_test() {
let qr = qr_code((), (), "https://graphite.art".to_string(), 1, true); let qr = qr_code((), (), "https://graphite.art".to_string(), false, 1., QRCodeErrorCorrectionLevel::Low, true);
assert!(qr.iter().next().unwrap().element.point_domain.ids().len() > 0); assert!(qr.iter().next().unwrap().element.point_domain.ids().len() > 0);
assert!(qr.iter().next().unwrap().element.segment_domain.ids().len() > 0); assert!(qr.iter().next().unwrap().element.segment_domain.ids().len() > 0);
} }
@@ -3,15 +3,19 @@ use graphic_types::Vector;
use std::collections::VecDeque; use std::collections::VecDeque;
use vector_types::subpath; use vector_types::subpath;
pub fn merge_qr_squares(qr_code: &qrcodegen::QrCode, vector: &mut Vector) { pub fn merge_qr_squares(qr_code: &qrcodegen::QrCode) -> Vector {
let mut vector = Vector::default();
let size = qr_code.size() as usize; let size = qr_code.size() as usize;
// 0 = empty // 0 = empty
// 1 = black, unvisited // 1 = filled, unvisited
// 2 = black, current island // 2 = filled, current island
let mut remaining = vec![vec![0u8; size]; size]; let mut remaining = vec![vec![0u8; size]; size];
#[allow(clippy::needless_range_loop)]
for y in 0..size { for y in 0..size {
#[allow(clippy::needless_range_loop)]
for x in 0..size { for x in 0..size {
if qr_code.get_module(x as i32, y as i32) { if qr_code.get_module(x as i32, y as i32) {
remaining[y][x] = 1; remaining[y][x] = 1;
@@ -115,4 +119,6 @@ pub fn merge_qr_squares(qr_code: &qrcodegen::QrCode, vector: &mut Vector) {
} }
} }
} }
vector
} }