diff --git a/Cargo.lock b/Cargo.lock index 532c79277a..805c000bef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2216,6 +2216,7 @@ dependencies = [ "text-nodes", "tokio", "url", + "vector-nodes", "wasm-bindgen", "web-sys", "wgpu-executor", diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index bd5cc08dba..625117cd71 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -25,6 +25,7 @@ use graphene_std::raster::{ use graphene_std::table::{Table, TableRow}; use graphene_std::text::{Font, TextAlign}; 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::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::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), + Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), // ===== // OTHER // ===== diff --git a/node-graph/graph-craft/Cargo.toml b/node-graph/graph-craft/Cargo.toml index f13c57b6cf..a31eea4256 100644 --- a/node-graph/graph-craft/Cargo.toml +++ b/node-graph/graph-craft/Cargo.toml @@ -21,6 +21,7 @@ graphene-core = { workspace = true } graphene-application-io = { workspace = true } rendering = { workspace = true } raster-nodes = { workspace = true } +vector-nodes = { workspace = true } graphic-types = { workspace = true } text-nodes = { workspace = true } diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 7a9d6ba906..15279f566a 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -238,6 +238,7 @@ tagged_value! { Fill(vector::style::Fill), BlendMode(core_types::blending::BlendMode), LuminanceCalculation(raster_nodes::adjustments::LuminanceCalculation), + QRCodeErrorCorrectionLevel(vector_nodes::generator_nodes::QRCodeErrorCorrectionLevel), XY(graphene_core::extract_xy::XY), RedGreenBlue(raster_nodes::adjustments::RedGreenBlue), RedGreenBlueAlpha(raster_nodes::adjustments::RedGreenBlueAlpha), diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index d98adf2066..a9a28a85e2 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -200,6 +200,7 @@ fn node_registry() -> HashMap, 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::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::raster::RedGreenBlue]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::RedGreenBlueAlpha]), diff --git a/node-graph/nodes/vector/src/generator_nodes.rs b/node-graph/nodes/vector/src/generator_nodes.rs index 2d1b1a26de..6527c9c3a6 100644 --- a/node-graph/nodes/vector/src/generator_nodes.rs +++ b/node-graph/nodes/vector/src/generator_nodes.rs @@ -1,6 +1,7 @@ -use core_types::Ctx; use core_types::registry::types::{Angle, PixelSize}; use core_types::table::Table; +use core_types::{Ctx, specta}; +use dyn_any::DynAny; use glam::DVec2; use graphic_types::Vector; use vector_types::subpath; @@ -186,48 +187,72 @@ fn star( 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. #[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, + #[widget(ParsedWidgetOverride::Custom = "text_area")] + #[default("https://graphite.art")] + text: String, + #[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, ) -> Table { - 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 ecc = match error_correction { + QRCodeErrorCorrectionLevel::Low => qrcodegen::QrCodeEcc::Low, + QRCodeErrorCorrectionLevel::Medium => qrcodegen::QrCodeEcc::Medium, + QRCodeErrorCorrectionLevel::Quartile => qrcodegen::QrCodeEcc::Quartile, + QRCodeErrorCorrectionLevel::High => qrcodegen::QrCodeEcc::High, }; - 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, - ); + let Ok(qr_code) = qrcodegen::QrCode::encode_text(&text, ecc) else { return Table::default() }; + + let mut vector = match individual_squares { + true => { + let mut vector = Vector::default(); + + let dimension = qr_code.size() as usize; + for y in 0..dimension { + for x in 0..dimension { + 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, + ); + } } } + + vector } - } else { - crate::merge_qr_squares::merge_qr_squares(&qr_code, &mut vector); + false => crate::merge_qr_squares::merge_qr_squares(&qr_code), + }; + + if has_size { + vector.transform(glam::DAffine2::from_scale(DVec2::splat(size.max(1.) / qr_code.size() as f64))); } + Table::new_from_element(vector) } @@ -407,7 +432,7 @@ mod tests { #[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.segment_domain.ids().len() > 0); } diff --git a/node-graph/nodes/vector/src/merge_qr_squares.rs b/node-graph/nodes/vector/src/merge_qr_squares.rs index ea2ac61372..436884db96 100644 --- a/node-graph/nodes/vector/src/merge_qr_squares.rs +++ b/node-graph/nodes/vector/src/merge_qr_squares.rs @@ -3,15 +3,19 @@ use graphic_types::Vector; use std::collections::VecDeque; 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; // 0 = empty - // 1 = black, unvisited - // 2 = black, current island + // 1 = filled, unvisited + // 2 = filled, current island let mut remaining = vec![vec![0u8; size]; size]; + #[allow(clippy::needless_range_loop)] for y in 0..size { + #[allow(clippy::needless_range_loop)] for x in 0..size { if qr_code.get_module(x as i32, y as i32) { remaining[y][x] = 1; @@ -115,4 +119,6 @@ pub fn merge_qr_squares(qr_code: &qrcodegen::QrCode, vector: &mut Vector) { } } } + + vector }