From 33e981e07baa184c8c1fd78c149f674d62f429ba Mon Sep 17 00:00:00 2001 From: charlotte Date: Thu, 5 Mar 2026 20:39:22 -0800 Subject: [PATCH 1/3] Add stroke modes. --- Cargo.lock | 61 -------- Cargo.toml | 13 +- crates/processing_ffi/src/lib.rs | 34 +++++ crates/processing_pyo3/src/gltf.rs | 4 +- crates/processing_pyo3/src/graphics.rs | 16 ++ crates/processing_pyo3/src/lib.rs | 24 ++- crates/processing_render/src/lib.rs | 1 + .../processing_render/src/render/command.rs | 42 +++++ crates/processing_render/src/render/mod.rs | 65 +++++++- .../src/render/primitive/mod.rs | 48 +++++- .../src/render/primitive/rect.rs | 5 +- crates/processing_wasm/src/lib.rs | 18 +++ examples/stroke_2d.rs | 67 ++++++++ examples/stroke_3d.rs | 143 ++++++++++++++++++ src/prelude.rs | 6 +- 15 files changed, 471 insertions(+), 76 deletions(-) create mode 100644 examples/stroke_2d.rs create mode 100644 examples/stroke_3d.rs diff --git a/Cargo.lock b/Cargo.lock index 676be54..0043d93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -487,7 +487,6 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bevy" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_internal", ] @@ -495,7 +494,6 @@ dependencies = [ [[package]] name = "bevy_a11y" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "accesskit", "bevy_app", @@ -507,7 +505,6 @@ dependencies = [ [[package]] name = "bevy_android" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "android-activity", ] @@ -515,7 +512,6 @@ dependencies = [ [[package]] name = "bevy_animation" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_animation_macros", "bevy_app", @@ -547,7 +543,6 @@ dependencies = [ [[package]] name = "bevy_animation_macros" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_macro_utils", "quote", @@ -557,7 +552,6 @@ dependencies = [ [[package]] name = "bevy_anti_alias" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_app", "bevy_asset", @@ -579,7 +573,6 @@ dependencies = [ [[package]] name = "bevy_app" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_derive", "bevy_ecs", @@ -601,7 +594,6 @@ dependencies = [ [[package]] name = "bevy_asset" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "async-broadcast", "async-channel", @@ -644,7 +636,6 @@ dependencies = [ [[package]] name = "bevy_asset_macros" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_macro_utils", "proc-macro2", @@ -655,7 +646,6 @@ dependencies = [ [[package]] name = "bevy_audio" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_app", "bevy_asset", @@ -672,7 +662,6 @@ dependencies = [ [[package]] name = "bevy_camera" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_app", "bevy_asset", @@ -697,7 +686,6 @@ dependencies = [ [[package]] name = "bevy_color" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_math", "bevy_reflect", @@ -712,7 +700,6 @@ dependencies = [ [[package]] name = "bevy_core_pipeline" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_app", "bevy_asset", @@ -740,7 +727,6 @@ dependencies = [ [[package]] name = "bevy_derive" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_macro_utils", "quote", @@ -750,7 +736,6 @@ dependencies = [ [[package]] name = "bevy_dev_tools" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_app", "bevy_asset", @@ -781,7 +766,6 @@ dependencies = [ [[package]] name = "bevy_diagnostic" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "atomic-waker", "bevy_app", @@ -798,7 +782,6 @@ dependencies = [ [[package]] name = "bevy_ecs" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "arrayvec", "bevy_ecs_macros", @@ -825,7 +808,6 @@ dependencies = [ [[package]] name = "bevy_ecs_macros" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_macro_utils", "proc-macro2", @@ -836,7 +818,6 @@ dependencies = [ [[package]] name = "bevy_encase_derive" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_macro_utils", "encase_derive_impl", @@ -845,7 +826,6 @@ dependencies = [ [[package]] name = "bevy_feathers" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "accesskit", "bevy_a11y", @@ -874,7 +854,6 @@ dependencies = [ [[package]] name = "bevy_gilrs" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_app", "bevy_ecs", @@ -889,7 +868,6 @@ dependencies = [ [[package]] name = "bevy_gizmos" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_app", "bevy_asset", @@ -908,7 +886,6 @@ dependencies = [ [[package]] name = "bevy_gizmos_macros" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_macro_utils", "quote", @@ -918,7 +895,6 @@ dependencies = [ [[package]] name = "bevy_gizmos_render" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_app", "bevy_asset", @@ -943,7 +919,6 @@ dependencies = [ [[package]] name = "bevy_gltf" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "async-lock", "base64", @@ -978,7 +953,6 @@ dependencies = [ [[package]] name = "bevy_image" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_app", "bevy_asset", @@ -1006,7 +980,6 @@ dependencies = [ [[package]] name = "bevy_input" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_app", "bevy_ecs", @@ -1022,7 +995,6 @@ dependencies = [ [[package]] name = "bevy_input_focus" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_app", "bevy_ecs", @@ -1038,7 +1010,6 @@ dependencies = [ [[package]] name = "bevy_internal" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_a11y", "bevy_android", @@ -1094,7 +1065,6 @@ dependencies = [ [[package]] name = "bevy_light" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_app", "bevy_asset", @@ -1118,7 +1088,6 @@ dependencies = [ [[package]] name = "bevy_log" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "android_log-sys", "bevy_app", @@ -1135,7 +1104,6 @@ dependencies = [ [[package]] name = "bevy_macro_utils" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "proc-macro2", "quote", @@ -1146,7 +1114,6 @@ dependencies = [ [[package]] name = "bevy_material" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_asset", "bevy_derive", @@ -1168,7 +1135,6 @@ dependencies = [ [[package]] name = "bevy_material_macros" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_macro_utils", "quote", @@ -1178,7 +1144,6 @@ dependencies = [ [[package]] name = "bevy_math" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "approx", "arrayvec", @@ -1197,7 +1162,6 @@ dependencies = [ [[package]] name = "bevy_mesh" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_app", "bevy_asset", @@ -1243,7 +1207,6 @@ checksum = "bff34eb29ff4b8a8688bc7299f14fb6b597461ca80fec03ed7d22939ab33e48f" [[package]] name = "bevy_pbr" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_app", "bevy_asset", @@ -1283,7 +1246,6 @@ dependencies = [ [[package]] name = "bevy_picking" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_app", "bevy_asset", @@ -1306,7 +1268,6 @@ dependencies = [ [[package]] name = "bevy_platform" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "critical-section", "foldhash 0.2.0", @@ -1327,7 +1288,6 @@ dependencies = [ [[package]] name = "bevy_post_process" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_app", "bevy_asset", @@ -1351,12 +1311,10 @@ dependencies = [ [[package]] name = "bevy_ptr" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" [[package]] name = "bevy_reflect" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "assert_type_match", "bevy_platform", @@ -1384,7 +1342,6 @@ dependencies = [ [[package]] name = "bevy_reflect_derive" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_macro_utils", "indexmap", @@ -1397,7 +1354,6 @@ dependencies = [ [[package]] name = "bevy_render" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "async-channel", "bevy_app", @@ -1449,7 +1405,6 @@ dependencies = [ [[package]] name = "bevy_render_macros" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_macro_utils", "proc-macro2", @@ -1460,7 +1415,6 @@ dependencies = [ [[package]] name = "bevy_scene" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_app", "bevy_asset", @@ -1481,7 +1435,6 @@ dependencies = [ [[package]] name = "bevy_shader" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_asset", "bevy_platform", @@ -1498,7 +1451,6 @@ dependencies = [ [[package]] name = "bevy_sprite" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_app", "bevy_asset", @@ -1523,7 +1475,6 @@ dependencies = [ [[package]] name = "bevy_sprite_render" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_app", "bevy_asset", @@ -1555,7 +1506,6 @@ dependencies = [ [[package]] name = "bevy_state" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_app", "bevy_ecs", @@ -1570,7 +1520,6 @@ dependencies = [ [[package]] name = "bevy_state_macros" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_macro_utils", "quote", @@ -1580,7 +1529,6 @@ dependencies = [ [[package]] name = "bevy_tasks" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "async-channel", "async-executor", @@ -1598,7 +1546,6 @@ dependencies = [ [[package]] name = "bevy_text" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_app", "bevy_asset", @@ -1625,7 +1572,6 @@ dependencies = [ [[package]] name = "bevy_time" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_app", "bevy_ecs", @@ -1639,7 +1585,6 @@ dependencies = [ [[package]] name = "bevy_transform" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_app", "bevy_ecs", @@ -1656,7 +1601,6 @@ dependencies = [ [[package]] name = "bevy_ui" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "accesskit", "bevy_a11y", @@ -1690,7 +1634,6 @@ dependencies = [ [[package]] name = "bevy_ui_render" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_app", "bevy_asset", @@ -1721,7 +1664,6 @@ dependencies = [ [[package]] name = "bevy_ui_widgets" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "accesskit", "bevy_a11y", @@ -1740,7 +1682,6 @@ dependencies = [ [[package]] name = "bevy_utils" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "async-channel", "bevy_platform", @@ -1751,7 +1692,6 @@ dependencies = [ [[package]] name = "bevy_window" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "bevy_app", "bevy_asset", @@ -1769,7 +1709,6 @@ dependencies = [ [[package]] name = "bevy_winit" version = "0.19.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#54b217b9a2d72ff4facb5caa8253e18078546630" dependencies = [ "accesskit", "accesskit_winit", diff --git a/Cargo.toml b/Cargo.toml index d873fef..e7b3b89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ type_complexity = "allow" too_many_arguments = "allow" [workspace.dependencies] -bevy = { git = "https://github.com/bevyengine/bevy", branch = "main" } +bevy = { path = "../../bevyengine/bevy" } processing = { path = "." } processing_pyo3 = { path = "crates/processing_pyo3" } processing_render = { path = "crates/processing_render" } @@ -86,6 +86,17 @@ path = "examples/midi.rs" name = "gltf_load" path = "examples/gltf_load.rs" +[[example]] +name = "stroke_2d" +path = "examples/stroke_2d.rs" + +[[example]] +name = "stroke_3d" +path = "examples/stroke_3d.rs" + +[patch."https://github.com/bevyengine/bevy"] +bevy = { path = "../../bevyengine/bevy" } + [profile.wasm-release] inherits = "release" opt-level = "z" diff --git a/crates/processing_ffi/src/lib.rs b/crates/processing_ffi/src/lib.rs index 15a7402..361ef65 100644 --- a/crates/processing_ffi/src/lib.rs +++ b/crates/processing_ffi/src/lib.rs @@ -281,6 +281,32 @@ pub extern "C" fn processing_set_stroke_weight(graphics_id: u64, weight: f32) { error::check(|| graphics_record_command(graphics_entity, DrawCommand::StrokeWeight(weight))); } +/// Set the stroke cap mode. +#[unsafe(no_mangle)] +pub extern "C" fn processing_set_stroke_cap(graphics_id: u64, cap: u8) { + error::clear_error(); + let graphics_entity = Entity::from_bits(graphics_id); + error::check(|| { + graphics_record_command( + graphics_entity, + DrawCommand::StrokeCap(processing::prelude::StrokeCapMode::from(cap)), + ) + }); +} + +/// Set the stroke join mode. +#[unsafe(no_mangle)] +pub extern "C" fn processing_set_stroke_join(graphics_id: u64, join: u8) { + error::clear_error(); + let graphics_entity = Entity::from_bits(graphics_id); + error::check(|| { + graphics_record_command( + graphics_entity, + DrawCommand::StrokeJoin(processing::prelude::StrokeJoinMode::from(join)), + ) + }); +} + /// Disable fill for subsequent shapes. /// /// SAFETY: @@ -694,6 +720,14 @@ pub const PROCESSING_TOPOLOGY_LINE_STRIP: u8 = 2; pub const PROCESSING_TOPOLOGY_TRIANGLE_LIST: u8 = 3; pub const PROCESSING_TOPOLOGY_TRIANGLE_STRIP: u8 = 4; +pub const PROCESSING_STROKE_CAP_ROUND: u8 = 0; +pub const PROCESSING_STROKE_CAP_SQUARE: u8 = 1; +pub const PROCESSING_STROKE_CAP_PROJECT: u8 = 2; + +pub const PROCESSING_STROKE_JOIN_ROUND: u8 = 0; +pub const PROCESSING_STROKE_JOIN_MITER: u8 = 1; +pub const PROCESSING_STROKE_JOIN_BEVEL: u8 = 2; + #[unsafe(no_mangle)] pub extern "C" fn processing_geometry_layout_create() -> u64 { error::clear_error(); diff --git a/crates/processing_pyo3/src/gltf.rs b/crates/processing_pyo3/src/gltf.rs index ee617de..9764beb 100644 --- a/crates/processing_pyo3/src/gltf.rs +++ b/crates/processing_pyo3/src/gltf.rs @@ -46,8 +46,8 @@ impl Gltf { #[pyfunction] #[pyo3(pass_module)] pub fn load_gltf(module: &Bound<'_, PyModule>, path: &str) -> PyResult { - let graphics = get_graphics(module)? - .ok_or_else(|| PyRuntimeError::new_err("call size() first"))?; + let graphics = + get_graphics(module)?.ok_or_else(|| PyRuntimeError::new_err("call size() first"))?; let entity = gltf_load(graphics.entity, path).map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; Ok(Gltf { entity }) diff --git a/crates/processing_pyo3/src/graphics.rs b/crates/processing_pyo3/src/graphics.rs index 5d58765..4b232ab 100644 --- a/crates/processing_pyo3/src/graphics.rs +++ b/crates/processing_pyo3/src/graphics.rs @@ -319,6 +319,22 @@ impl Graphics { .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } + pub fn stroke_cap(&self, cap: u8) -> PyResult<()> { + graphics_record_command( + self.entity, + DrawCommand::StrokeCap(processing::prelude::StrokeCapMode::from(cap)), + ) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + pub fn stroke_join(&self, join: u8) -> PyResult<()> { + graphics_record_command( + self.entity, + DrawCommand::StrokeJoin(processing::prelude::StrokeJoinMode::from(join)), + ) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + pub fn rect( &self, x: f32, diff --git a/crates/processing_pyo3/src/lib.rs b/crates/processing_pyo3/src/lib.rs index f67b645..dacc187 100644 --- a/crates/processing_pyo3/src/lib.rs +++ b/crates/processing_pyo3/src/lib.rs @@ -22,8 +22,8 @@ use pyo3::{ }; use std::ffi::{CStr, CString}; -use gltf::Gltf; use bevy::log::warn; +use gltf::Gltf; use std::env; /// Get a shared ref to the Graphics context, or return Ok(()) if not yet initialized. @@ -66,6 +66,16 @@ fn processing(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(stroke, m)?)?; m.add_function(wrap_pyfunction!(no_stroke, m)?)?; m.add_function(wrap_pyfunction!(stroke_weight, m)?)?; + m.add_function(wrap_pyfunction!(stroke_cap, m)?)?; + m.add_function(wrap_pyfunction!(stroke_join, m)?)?; + + m.add("ROUND", 0u8)?; + m.add("SQUARE", 1u8)?; + m.add("PROJECT", 2u8)?; + + m.add("MITER", 1u8)?; + m.add("BEVEL", 2u8)?; + m.add_function(wrap_pyfunction!(rect, m)?)?; m.add_function(wrap_pyfunction!(image, m)?)?; m.add_function(wrap_pyfunction!(draw_geometry, m)?)?; @@ -431,6 +441,18 @@ fn stroke_weight(module: &Bound<'_, PyModule>, weight: f32) -> PyResult<()> { graphics!(module).stroke_weight(weight) } +#[pyfunction] +#[pyo3(pass_module)] +fn stroke_cap(module: &Bound<'_, PyModule>, cap: u8) -> PyResult<()> { + graphics!(module).stroke_cap(cap) +} + +#[pyfunction] +#[pyo3(pass_module)] +fn stroke_join(module: &Bound<'_, PyModule>, join: u8) -> PyResult<()> { + graphics!(module).stroke_join(join) +} + #[pyfunction] #[pyo3(pass_module, signature = (x, y, w, h, tl=0.0, tr=0.0, br=0.0, bl=0.0))] fn rect( diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index b603247..0f29c0f 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -282,6 +282,7 @@ fn create_app(config: Config) -> App { LightPlugin, material::MaterialPlugin, MidiPlugin, + bevy::pbr::wireframe::WireframePlugin::default(), )); app.add_systems(First, (clear_transient_meshes, activate_cameras)) .add_systems( diff --git a/crates/processing_render/src/render/command.rs b/crates/processing_render/src/render/command.rs index a26f915..2ebb9e7 100644 --- a/crates/processing_render/src/render/command.rs +++ b/crates/processing_render/src/render/command.rs @@ -1,5 +1,45 @@ use bevy::prelude::*; +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +#[repr(u8)] +pub enum StrokeCapMode { + #[default] + Round = 0, + Square = 1, + Project = 2, +} + +impl From for StrokeCapMode { + fn from(v: u8) -> Self { + match v { + 0 => Self::Round, + 1 => Self::Square, + 2 => Self::Project, + _ => Self::default(), + } + } +} + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +#[repr(u8)] +pub enum StrokeJoinMode { + #[default] + Round = 0, + Miter = 1, + Bevel = 2, +} + +impl From for StrokeJoinMode { + fn from(v: u8) -> Self { + match v { + 0 => Self::Round, + 1 => Self::Miter, + 2 => Self::Bevel, + _ => Self::default(), + } + } +} + #[derive(Debug, Clone)] pub enum DrawCommand { BackgroundColor(Color), @@ -9,6 +49,8 @@ pub enum DrawCommand { StrokeColor(Color), NoStroke, StrokeWeight(f32), + StrokeCap(StrokeCapMode), + StrokeJoin(StrokeJoinMode), Roughness(f32), Metallic(f32), Emissive(Color), diff --git a/crates/processing_render/src/render/mod.rs b/crates/processing_render/src/render/mod.rs index 3de4b67..7bf9dba 100644 --- a/crates/processing_render/src/render/mod.rs +++ b/crates/processing_render/src/render/mod.rs @@ -12,7 +12,7 @@ use bevy::{ }; use command::{CommandBuffer, DrawCommand}; use material::MaterialKey; -use primitive::{TessellationMode, box_mesh, empty_mesh, sphere_mesh}; +use primitive::{StrokeConfig, TessellationMode, box_mesh, empty_mesh, sphere_mesh}; use transform::TransformStack; use crate::{ @@ -65,6 +65,7 @@ pub struct RenderState { pub fill_color: Option, pub stroke_color: Option, pub stroke_weight: f32, + pub stroke_config: StrokeConfig, pub material_key: MaterialKey, pub transform: TransformStack, } @@ -75,6 +76,7 @@ impl RenderState { fill_color: Some(Color::WHITE), stroke_color: Some(Color::BLACK), stroke_weight: 1.0, + stroke_config: StrokeConfig::default(), material_key: MaterialKey::Color { transparent: false, background_image: None, @@ -87,6 +89,7 @@ impl RenderState { self.fill_color = Some(Color::WHITE); self.stroke_color = Some(Color::BLACK); self.stroke_weight = 1.0; + self.stroke_config = StrokeConfig::default(); self.material_key = MaterialKey::Color { transparent: false, background_image: None, @@ -152,6 +155,12 @@ pub fn flush_draw_commands( DrawCommand::StrokeWeight(weight) => { state.stroke_weight = weight; } + DrawCommand::StrokeCap(cap) => { + state.stroke_config.line_cap = cap; + } + DrawCommand::StrokeJoin(join) => { + state.stroke_config.line_join = join; + } DrawCommand::Roughness(r) => { state.material_key = match state.material_key { MaterialKey::Pbr { @@ -223,8 +232,19 @@ pub fn flush_draw_commands( }; } DrawCommand::Rect { x, y, w, h, radii } => { + let stroke_config = state.stroke_config; add_fill(&mut res, &mut batch, &state, |mesh, color| { - rect(mesh, x, y, w, h, radii, color, TessellationMode::Fill) + rect( + mesh, + x, + y, + w, + h, + radii, + color, + TessellationMode::Fill, + &stroke_config, + ) }); add_stroke(&mut res, &mut batch, &state, |mesh, color, weight| { @@ -237,6 +257,7 @@ pub fn flush_draw_commands( radii, color, TessellationMode::Stroke(weight), + &stroke_config, ) }); } @@ -511,17 +532,38 @@ fn flush_batch(res: &mut RenderResources, batch: &mut BatchState) { } fn add_shape3d(res: &mut RenderResources, batch: &mut BatchState, state: &RenderState, mesh: Mesh) { + use bevy::pbr::wireframe::{Wireframe, WireframeColor, WireframeLineWidth, WireframeTopology}; + flush_batch(res, batch); let mesh_handle = res.meshes.add(mesh); - let material_key = material_key_with_fill(state); - let material_handle = material_key.to_material(&mut res.materials); + let fill_color = state.fill_color.unwrap_or(Color::WHITE); + let material_handle = match &state.material_key { + MaterialKey::Color { transparent, .. } => { + let mat = StandardMaterial { + base_color: fill_color, + unlit: true, + cull_mode: None, + alpha_mode: if *transparent { + AlphaMode::Blend + } else { + AlphaMode::Opaque + }, + ..default() + }; + res.materials.add(mat).untyped() + } + _ => { + let key = material_key_with_fill(state); + key.to_material(&mut res.materials) + } + }; let z_offset = -(batch.draw_index as f32 * 0.001); let mut transform = state.transform.to_bevy_transform(); transform.translation.z += z_offset; - res.commands.spawn(( + let mut entity = res.commands.spawn(( Mesh3d(mesh_handle), UntypedMaterial(material_handle), BelongsToGraphics(batch.graphics_entity), @@ -529,6 +571,19 @@ fn add_shape3d(res: &mut RenderResources, batch: &mut BatchState, state: &Render batch.render_layers.clone(), )); + if let Some(stroke_color) = state.stroke_color { + entity.insert(( + Wireframe, + WireframeColor { + color: stroke_color, + }, + WireframeLineWidth { + width: state.stroke_weight, + }, + WireframeTopology::Quads, + )); + } + batch.draw_index += 1; } diff --git a/crates/processing_render/src/render/primitive/mod.rs b/crates/processing_render/src/render/primitive/mod.rs index 6ef4e93..9d2188b 100644 --- a/crates/processing_render/src/render/primitive/mod.rs +++ b/crates/processing_render/src/render/primitive/mod.rs @@ -15,6 +15,7 @@ use lyon::{ pub use rect::rect; pub use shape3d::{box_mesh, sphere_mesh}; +use super::command::{StrokeCapMode, StrokeJoinMode}; use super::mesh_builder::MeshBuilder; pub enum TessellationMode { @@ -22,7 +23,48 @@ pub enum TessellationMode { Stroke(f32), } -pub fn tessellate_path(mesh: &mut Mesh, path: &Path, color: Color, mode: TessellationMode) { +#[derive(Debug, Clone, Copy)] +pub struct StrokeConfig { + pub line_cap: StrokeCapMode, + pub line_join: StrokeJoinMode, +} + +impl Default for StrokeConfig { + fn default() -> Self { + Self { + line_cap: StrokeCapMode::Round, + line_join: StrokeJoinMode::Round, + } + } +} + +impl StrokeCapMode { + pub fn to_lyon(self) -> LineCap { + match self { + Self::Round => LineCap::Round, + Self::Square => LineCap::Square, + Self::Project => LineCap::Butt, + } + } +} + +impl StrokeJoinMode { + pub fn to_lyon(self) -> LineJoin { + match self { + Self::Round => LineJoin::Round, + Self::Miter => LineJoin::Miter, + Self::Bevel => LineJoin::Bevel, + } + } +} + +pub fn tessellate_path( + mesh: &mut Mesh, + path: &Path, + color: Color, + mode: TessellationMode, + stroke_config: &StrokeConfig, +) { let mut builder = MeshBuilder::new(mesh, color); match mode { TessellationMode::Fill => { @@ -35,8 +77,8 @@ pub fn tessellate_path(mesh: &mut Mesh, path: &Path, color: Color, mode: Tessell let mut tessellator = StrokeTessellator::new(); let options = StrokeOptions::default() .with_line_width(weight) - .with_line_cap(LineCap::Round) - .with_line_join(LineJoin::Round); + .with_line_cap(stroke_config.line_cap.to_lyon()) + .with_line_join(stroke_config.line_join.to_lyon()); tessellator .tessellate_path(path, &options, &mut builder) diff --git a/crates/processing_render/src/render/primitive/rect.rs b/crates/processing_render/src/render/primitive/rect.rs index 20c5827..36decaa 100644 --- a/crates/processing_render/src/render/primitive/rect.rs +++ b/crates/processing_render/src/render/primitive/rect.rs @@ -4,7 +4,7 @@ use bevy::{ }; use lyon::{geom::Point, path::Path}; -use crate::render::primitive::{TessellationMode, tessellate_path}; +use crate::render::primitive::{StrokeConfig, TessellationMode, tessellate_path}; fn rect_path(x: f32, y: f32, w: f32, h: f32, radii: [f32; 4]) -> Path { let mut path_builder = Path::builder(); @@ -56,12 +56,13 @@ pub fn rect( radii: [f32; 4], // [tl, tr, br, bl] color: Color, mode: TessellationMode, + stroke_config: &StrokeConfig, ) { if radii == [0.0; 4] && matches!(mode, TessellationMode::Fill) { simple_rect(mesh, x, y, w, h, color); } else { let path = rect_path(x, y, w, h, radii); - tessellate_path(mesh, &path, color, mode); + tessellate_path(mesh, &path, color, mode, stroke_config); } } diff --git a/crates/processing_wasm/src/lib.rs b/crates/processing_wasm/src/lib.rs index 572623d..92090b5 100644 --- a/crates/processing_wasm/src/lib.rs +++ b/crates/processing_wasm/src/lib.rs @@ -105,6 +105,24 @@ pub fn js_stroke_weight(surface_id: u64, weight: f32) -> Result<(), JsValue> { )) } +#[wasm_bindgen(js_name = "strokeCap")] +pub fn js_stroke_cap(surface_id: u64, cap: u8) -> Result<(), JsValue> { + check(graphics_record_command( + Entity::from_bits(surface_id), + DrawCommand::StrokeCap(processing_render::render::command::StrokeCapMode::from(cap)), + )) +} + +#[wasm_bindgen(js_name = "strokeJoin")] +pub fn js_stroke_join(surface_id: u64, join: u8) -> Result<(), JsValue> { + check(graphics_record_command( + Entity::from_bits(surface_id), + DrawCommand::StrokeJoin(processing_render::render::command::StrokeJoinMode::from( + join, + )), + )) +} + #[wasm_bindgen(js_name = "noFill")] pub fn js_no_fill(surface_id: u64) -> Result<(), JsValue> { check(graphics_record_command( diff --git a/examples/stroke_2d.rs b/examples/stroke_2d.rs new file mode 100644 index 0000000..5c303af --- /dev/null +++ b/examples/stroke_2d.rs @@ -0,0 +1,67 @@ +mod glfw; + +use glfw::GlfwContext; +use processing::prelude::*; +use processing_render::render::command::DrawCommand; + +fn main() { + sketch().unwrap(); + exit(0).unwrap(); +} + +fn sketch() -> error::Result<()> { + let mut glfw_ctx = GlfwContext::new(600, 300)?; + init(Config::default())?; + + let surface = glfw_ctx.create_surface(600, 300, 1.0)?; + let graphics = graphics_create(surface, 600, 300, TextureFormat::Rgba16Float)?; + + let joins = [ + StrokeJoinMode::Round, + StrokeJoinMode::Miter, + StrokeJoinMode::Bevel, + ]; + + while glfw_ctx.poll_events() { + graphics_begin_draw(graphics)?; + + graphics_record_command( + graphics, + DrawCommand::BackgroundColor(bevy::color::Color::srgb(0.15, 0.15, 0.2)), + )?; + + graphics_record_command(graphics, DrawCommand::StrokeWeight(12.0))?; + + for (i, &join) in joins.iter().enumerate() { + let x = 30.0 + i as f32 * 190.0; + let y = 50.0; + + graphics_record_command(graphics, DrawCommand::StrokeJoin(join))?; + + let hue = i as f32 * 0.3; + graphics_record_command( + graphics, + DrawCommand::Fill(bevy::color::Color::srgb(0.3 + hue, 0.5, 0.8 - hue)), + )?; + graphics_record_command( + graphics, + DrawCommand::StrokeColor(bevy::color::Color::srgb(1.0, 0.9, 0.3)), + )?; + + graphics_record_command( + graphics, + DrawCommand::Rect { + x, + y, + w: 150.0, + h: 180.0, + radii: [0.0; 4], + }, + )?; + } + + graphics_end_draw(graphics)?; + } + + Ok(()) +} diff --git a/examples/stroke_3d.rs b/examples/stroke_3d.rs new file mode 100644 index 0000000..01eba7e --- /dev/null +++ b/examples/stroke_3d.rs @@ -0,0 +1,143 @@ +mod glfw; + +use glfw::GlfwContext; +use processing::prelude::*; +use processing_render::render::command::DrawCommand; + +fn main() { + sketch().unwrap(); + exit(0).unwrap(); +} + +fn sketch() -> error::Result<()> { + let mut glfw_ctx = GlfwContext::new(600, 400)?; + init(Config::default())?; + + let surface = glfw_ctx.create_surface(600, 400, 1.0)?; + let graphics = graphics_create(surface, 600, 400, TextureFormat::Rgba16Float)?; + + graphics_mode_3d(graphics)?; + transform_set_position(graphics, 200.0, 150.0, 350.0)?; + transform_look_at(graphics, 0.0, 0.0, 0.0)?; + + let _light = + light_create_directional(graphics, bevy::color::Color::srgb(0.9, 0.85, 0.8), 800.0)?; + + let mut angle: f32 = 0.0; + + while glfw_ctx.poll_events() { + graphics_begin_draw(graphics)?; + + graphics_record_command( + graphics, + DrawCommand::BackgroundColor(bevy::color::Color::srgb(0.08, 0.08, 0.12)), + )?; + + // thin wireframe box + graphics_record_command(graphics, DrawCommand::PushMatrix)?; + graphics_record_command(graphics, DrawCommand::Translate { x: -80.0, y: 0.0 })?; + graphics_record_command(graphics, DrawCommand::Rotate { angle })?; + + graphics_record_command( + graphics, + DrawCommand::Fill(bevy::color::Color::srgb(0.3, 0.4, 0.7)), + )?; + graphics_record_command( + graphics, + DrawCommand::StrokeColor(bevy::color::Color::srgb(1.0, 1.0, 1.0)), + )?; + graphics_record_command(graphics, DrawCommand::StrokeWeight(1.0))?; + + graphics_record_command( + graphics, + DrawCommand::Box { + width: 60.0, + height: 60.0, + depth: 60.0, + }, + )?; + graphics_record_command(graphics, DrawCommand::PopMatrix)?; + + // thick wireframe box + graphics_record_command(graphics, DrawCommand::PushMatrix)?; + graphics_record_command(graphics, DrawCommand::Translate { x: 0.0, y: 0.0 })?; + graphics_record_command(graphics, DrawCommand::Rotate { angle: angle * 0.7 })?; + + graphics_record_command( + graphics, + DrawCommand::Fill(bevy::color::Color::srgb(0.7, 0.3, 0.4)), + )?; + graphics_record_command( + graphics, + DrawCommand::StrokeColor(bevy::color::Color::srgb(1.0, 0.9, 0.2)), + )?; + graphics_record_command(graphics, DrawCommand::StrokeWeight(3.0))?; + + graphics_record_command( + graphics, + DrawCommand::Box { + width: 50.0, + height: 70.0, + depth: 50.0, + }, + )?; + graphics_record_command(graphics, DrawCommand::PopMatrix)?; + + // thick wireframe sphere + graphics_record_command(graphics, DrawCommand::PushMatrix)?; + graphics_record_command(graphics, DrawCommand::Translate { x: 80.0, y: 0.0 })?; + graphics_record_command(graphics, DrawCommand::Rotate { angle: angle * 0.5 })?; + + graphics_record_command( + graphics, + DrawCommand::Fill(bevy::color::Color::srgb(0.3, 0.7, 0.4)), + )?; + graphics_record_command( + graphics, + DrawCommand::StrokeColor(bevy::color::Color::srgb(0.9, 0.4, 1.0)), + )?; + graphics_record_command(graphics, DrawCommand::StrokeWeight(2.0))?; + + graphics_record_command( + graphics, + DrawCommand::Sphere { + radius: 35.0, + sectors: 16, + stacks: 12, + }, + )?; + graphics_record_command(graphics, DrawCommand::PopMatrix)?; + + // wireframe-only sphere (no fill) + graphics_record_command(graphics, DrawCommand::PushMatrix)?; + graphics_record_command(graphics, DrawCommand::Translate { x: 160.0, y: 0.0 })?; + graphics_record_command( + graphics, + DrawCommand::Rotate { + angle: -angle * 0.3, + }, + )?; + + graphics_record_command(graphics, DrawCommand::NoFill)?; + graphics_record_command( + graphics, + DrawCommand::StrokeColor(bevy::color::Color::srgb(0.2, 0.8, 1.0)), + )?; + graphics_record_command(graphics, DrawCommand::StrokeWeight(1.5))?; + + graphics_record_command( + graphics, + DrawCommand::Sphere { + radius: 30.0, + sectors: 24, + stacks: 16, + }, + )?; + graphics_record_command(graphics, DrawCommand::PopMatrix)?; + + graphics_end_draw(graphics)?; + angle += 0.015; + } + + Ok(()) +} diff --git a/src/prelude.rs b/src/prelude.rs index df9de2d..c23d694 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,3 +1,7 @@ pub use bevy::prelude::default; pub use bevy::render::render_resource::TextureFormat; -pub use processing_render::{config::*, render::command::DrawCommand, *}; +pub use processing_render::{ + config::*, + render::command::{DrawCommand, StrokeCapMode, StrokeJoinMode}, + *, +}; From 9dcf6c28ca2eef55bf12407c1a6cced2eacfc3f3 Mon Sep 17 00:00:00 2001 From: charlotte Date: Thu, 5 Mar 2026 21:12:45 -0800 Subject: [PATCH 2/3] Add comment. --- crates/processing_render/src/render/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/processing_render/src/render/mod.rs b/crates/processing_render/src/render/mod.rs index 7bf9dba..f102572 100644 --- a/crates/processing_render/src/render/mod.rs +++ b/crates/processing_render/src/render/mod.rs @@ -539,6 +539,9 @@ fn add_shape3d(res: &mut RenderResources, batch: &mut BatchState, state: &Render let mesh_handle = res.meshes.add(mesh); let fill_color = state.fill_color.unwrap_or(Color::WHITE); let material_handle = match &state.material_key { + // TODO: in 2d, we use vertex colors. `to_material` becomes complicated if we also encode + // a base color in the material, so for simplicity we just create a new material here + // that is unlit and uses the fill color as the base color MaterialKey::Color { transparent, .. } => { let mat = StandardMaterial { base_color: fill_color, From 4c02e9edc9f3944b098387a81112cdbb5a291326 Mon Sep 17 00:00:00 2001 From: charlotte Date: Thu, 5 Mar 2026 21:27:15 -0800 Subject: [PATCH 3/3] Upstream bevy. --- Cargo.toml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e7b3b89..3d7c8b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ type_complexity = "allow" too_many_arguments = "allow" [workspace.dependencies] -bevy = { path = "../../bevyengine/bevy" } +bevy = { git = "https://github.com/bevyengine/bevy", branch = "main" } processing = { path = "." } processing_pyo3 = { path = "crates/processing_pyo3" } processing_render = { path = "crates/processing_render" } @@ -94,9 +94,6 @@ path = "examples/stroke_2d.rs" name = "stroke_3d" path = "examples/stroke_3d.rs" -[patch."https://github.com/bevyengine/bevy"] -bevy = { path = "../../bevyengine/bevy" } - [profile.wasm-release] inherits = "release" opt-level = "z"