Skip to content

Commit 918a021

Browse files
authored
Add stroke modes (#84)
1 parent 9732414 commit 918a021

File tree

14 files changed

+547
-92
lines changed

14 files changed

+547
-92
lines changed

Cargo.lock

Lines changed: 79 additions & 79 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,14 @@ path = "examples/midi.rs"
8989
name = "gltf_load"
9090
path = "examples/gltf_load.rs"
9191

92+
[[example]]
93+
name = "stroke_2d"
94+
path = "examples/stroke_2d.rs"
95+
96+
[[example]]
97+
name = "stroke_3d"
98+
path = "examples/stroke_3d.rs"
99+
92100
[[example]]
93101
name = "custom_material"
94102
path = "examples/custom_material.rs"

crates/processing_ffi/src/lib.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,32 @@ pub extern "C" fn processing_set_stroke_weight(graphics_id: u64, weight: f32) {
281281
error::check(|| graphics_record_command(graphics_entity, DrawCommand::StrokeWeight(weight)));
282282
}
283283

284+
/// Set the stroke cap mode.
285+
#[unsafe(no_mangle)]
286+
pub extern "C" fn processing_set_stroke_cap(graphics_id: u64, cap: u8) {
287+
error::clear_error();
288+
let graphics_entity = Entity::from_bits(graphics_id);
289+
error::check(|| {
290+
graphics_record_command(
291+
graphics_entity,
292+
DrawCommand::StrokeCap(processing::prelude::StrokeCapMode::from(cap)),
293+
)
294+
});
295+
}
296+
297+
/// Set the stroke join mode.
298+
#[unsafe(no_mangle)]
299+
pub extern "C" fn processing_set_stroke_join(graphics_id: u64, join: u8) {
300+
error::clear_error();
301+
let graphics_entity = Entity::from_bits(graphics_id);
302+
error::check(|| {
303+
graphics_record_command(
304+
graphics_entity,
305+
DrawCommand::StrokeJoin(processing::prelude::StrokeJoinMode::from(join)),
306+
)
307+
});
308+
}
309+
284310
/// Disable fill for subsequent shapes.
285311
///
286312
/// SAFETY:
@@ -694,6 +720,14 @@ pub const PROCESSING_TOPOLOGY_LINE_STRIP: u8 = 2;
694720
pub const PROCESSING_TOPOLOGY_TRIANGLE_LIST: u8 = 3;
695721
pub const PROCESSING_TOPOLOGY_TRIANGLE_STRIP: u8 = 4;
696722

723+
pub const PROCESSING_STROKE_CAP_ROUND: u8 = 0;
724+
pub const PROCESSING_STROKE_CAP_SQUARE: u8 = 1;
725+
pub const PROCESSING_STROKE_CAP_PROJECT: u8 = 2;
726+
727+
pub const PROCESSING_STROKE_JOIN_ROUND: u8 = 0;
728+
pub const PROCESSING_STROKE_JOIN_MITER: u8 = 1;
729+
pub const PROCESSING_STROKE_JOIN_BEVEL: u8 = 2;
730+
697731
#[unsafe(no_mangle)]
698732
pub extern "C" fn processing_geometry_layout_create() -> u64 {
699733
error::clear_error();

crates/processing_pyo3/src/graphics.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,22 @@ impl Graphics {
319319
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
320320
}
321321

322+
pub fn stroke_cap(&self, cap: u8) -> PyResult<()> {
323+
graphics_record_command(
324+
self.entity,
325+
DrawCommand::StrokeCap(processing::prelude::StrokeCapMode::from(cap)),
326+
)
327+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
328+
}
329+
330+
pub fn stroke_join(&self, join: u8) -> PyResult<()> {
331+
graphics_record_command(
332+
self.entity,
333+
DrawCommand::StrokeJoin(processing::prelude::StrokeJoinMode::from(join)),
334+
)
335+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
336+
}
337+
322338
pub fn rect(
323339
&self,
324340
x: f32,

crates/processing_pyo3/src/lib.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,16 @@ fn processing(m: &Bound<'_, PyModule>) -> PyResult<()> {
7070
m.add_function(wrap_pyfunction!(stroke, m)?)?;
7171
m.add_function(wrap_pyfunction!(no_stroke, m)?)?;
7272
m.add_function(wrap_pyfunction!(stroke_weight, m)?)?;
73+
m.add_function(wrap_pyfunction!(stroke_cap, m)?)?;
74+
m.add_function(wrap_pyfunction!(stroke_join, m)?)?;
75+
76+
m.add("ROUND", 0u8)?;
77+
m.add("SQUARE", 1u8)?;
78+
m.add("PROJECT", 2u8)?;
79+
80+
m.add("MITER", 1u8)?;
81+
m.add("BEVEL", 2u8)?;
82+
7383
m.add_function(wrap_pyfunction!(rect, m)?)?;
7484
m.add_function(wrap_pyfunction!(image, m)?)?;
7585
m.add_function(wrap_pyfunction!(draw_geometry, m)?)?;
@@ -435,6 +445,18 @@ fn stroke_weight(module: &Bound<'_, PyModule>, weight: f32) -> PyResult<()> {
435445
graphics!(module).stroke_weight(weight)
436446
}
437447

448+
#[pyfunction]
449+
#[pyo3(pass_module)]
450+
fn stroke_cap(module: &Bound<'_, PyModule>, cap: u8) -> PyResult<()> {
451+
graphics!(module).stroke_cap(cap)
452+
}
453+
454+
#[pyfunction]
455+
#[pyo3(pass_module)]
456+
fn stroke_join(module: &Bound<'_, PyModule>, join: u8) -> PyResult<()> {
457+
graphics!(module).stroke_join(join)
458+
}
459+
438460
#[pyfunction]
439461
#[pyo3(pass_module, signature = (x, y, w, h, tl=0.0, tr=0.0, br=0.0, bl=0.0))]
440462
fn rect(

crates/processing_render/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ fn create_app(config: Config) -> App {
279279
LightPlugin,
280280
material::MaterialPlugin,
281281
MidiPlugin,
282+
bevy::pbr::wireframe::WireframePlugin::default(),
282283
material::custom::CustomMaterialPlugin,
283284
));
284285
app.add_systems(First, (clear_transient_meshes, activate_cameras))

crates/processing_render/src/render/command.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,45 @@
11
use bevy::prelude::*;
22

3+
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
4+
#[repr(u8)]
5+
pub enum StrokeCapMode {
6+
#[default]
7+
Round = 0,
8+
Square = 1,
9+
Project = 2,
10+
}
11+
12+
impl From<u8> for StrokeCapMode {
13+
fn from(v: u8) -> Self {
14+
match v {
15+
0 => Self::Round,
16+
1 => Self::Square,
17+
2 => Self::Project,
18+
_ => Self::default(),
19+
}
20+
}
21+
}
22+
23+
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
24+
#[repr(u8)]
25+
pub enum StrokeJoinMode {
26+
#[default]
27+
Round = 0,
28+
Miter = 1,
29+
Bevel = 2,
30+
}
31+
32+
impl From<u8> for StrokeJoinMode {
33+
fn from(v: u8) -> Self {
34+
match v {
35+
0 => Self::Round,
36+
1 => Self::Miter,
37+
2 => Self::Bevel,
38+
_ => Self::default(),
39+
}
40+
}
41+
}
42+
343
#[derive(Debug, Clone)]
444
pub enum DrawCommand {
545
BackgroundColor(Color),
@@ -9,6 +49,8 @@ pub enum DrawCommand {
949
StrokeColor(Color),
1050
NoStroke,
1151
StrokeWeight(f32),
52+
StrokeCap(StrokeCapMode),
53+
StrokeJoin(StrokeJoinMode),
1254
Roughness(f32),
1355
Metallic(f32),
1456
Emissive(Color),

crates/processing_render/src/render/mod.rs

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use bevy::{
1212
};
1313
use command::{CommandBuffer, DrawCommand};
1414
use material::MaterialKey;
15-
use primitive::{TessellationMode, box_mesh, empty_mesh, sphere_mesh};
15+
use primitive::{StrokeConfig, TessellationMode, box_mesh, empty_mesh, sphere_mesh};
1616
use transform::TransformStack;
1717

1818
use crate::{
@@ -65,6 +65,7 @@ pub struct RenderState {
6565
pub fill_color: Option<Color>,
6666
pub stroke_color: Option<Color>,
6767
pub stroke_weight: f32,
68+
pub stroke_config: StrokeConfig,
6869
pub material_key: MaterialKey,
6970
pub transform: TransformStack,
7071
}
@@ -75,6 +76,7 @@ impl RenderState {
7576
fill_color: Some(Color::WHITE),
7677
stroke_color: Some(Color::BLACK),
7778
stroke_weight: 1.0,
79+
stroke_config: StrokeConfig::default(),
7880
material_key: MaterialKey::Color {
7981
transparent: false,
8082
background_image: None,
@@ -87,6 +89,7 @@ impl RenderState {
8789
self.fill_color = Some(Color::WHITE);
8890
self.stroke_color = Some(Color::BLACK);
8991
self.stroke_weight = 1.0;
92+
self.stroke_config = StrokeConfig::default();
9093
self.material_key = MaterialKey::Color {
9194
transparent: false,
9295
background_image: None,
@@ -152,6 +155,12 @@ pub fn flush_draw_commands(
152155
DrawCommand::StrokeWeight(weight) => {
153156
state.stroke_weight = weight;
154157
}
158+
DrawCommand::StrokeCap(cap) => {
159+
state.stroke_config.line_cap = cap;
160+
}
161+
DrawCommand::StrokeJoin(join) => {
162+
state.stroke_config.line_join = join;
163+
}
155164
DrawCommand::Roughness(r) => {
156165
state.material_key = match state.material_key {
157166
MaterialKey::Pbr {
@@ -223,11 +232,24 @@ pub fn flush_draw_commands(
223232
};
224233
}
225234
DrawCommand::Rect { x, y, w, h, radii } => {
235+
let stroke_config = state.stroke_config;
226236
add_fill(
227237
&mut res,
228238
&mut batch,
229239
&state,
230-
|mesh, color| rect(mesh, x, y, w, h, radii, color, TessellationMode::Fill),
240+
|mesh, color| {
241+
rect(
242+
mesh,
243+
x,
244+
y,
245+
w,
246+
h,
247+
radii,
248+
color,
249+
TessellationMode::Fill,
250+
&stroke_config,
251+
)
252+
},
231253
&p_material_handles,
232254
);
233255

@@ -245,6 +267,7 @@ pub fn flush_draw_commands(
245267
radii,
246268
color,
247269
TessellationMode::Stroke(weight),
270+
&stroke_config,
248271
)
249272
},
250273
&p_material_handles,
@@ -556,34 +579,68 @@ fn add_shape3d(
556579
mesh: Mesh,
557580
material_handles: &Query<&UntypedMaterial>,
558581
) {
582+
use bevy::pbr::wireframe::{Wireframe, WireframeColor, WireframeLineWidth, WireframeTopology};
583+
559584
flush_batch(res, batch, material_handles);
560585

561586
let mesh_handle = res.meshes.add(mesh);
562-
let material_key = material_key_with_fill(state);
563-
564-
let material_handle = match &material_key {
587+
let fill_color = state.fill_color.unwrap_or(Color::WHITE);
588+
let material_handle = match &state.material_key {
565589
MaterialKey::Custom(entity) => match material_handles.get(*entity) {
566590
Ok(handle) => handle.0.clone(),
567591
Err(_) => {
568592
warn!("Custom material entity {:?} not found", entity);
569593
return;
570594
}
571595
},
572-
_ => material_key.to_material(&mut res.materials),
596+
// TODO: in 2d, we use vertex colors. `to_material` becomes complicated if we also encode
597+
// a base color in the material, so for simplicity we just create a new material here
598+
// that is unlit and uses the fill color as the base color
599+
MaterialKey::Color { transparent, .. } => {
600+
let mat = StandardMaterial {
601+
base_color: fill_color,
602+
unlit: true,
603+
cull_mode: None,
604+
alpha_mode: if *transparent {
605+
AlphaMode::Blend
606+
} else {
607+
AlphaMode::Opaque
608+
},
609+
..default()
610+
};
611+
res.materials.add(mat).untyped()
612+
}
613+
_ => {
614+
let key = material_key_with_fill(state);
615+
key.to_material(&mut res.materials)
616+
}
573617
};
574618

575619
let z_offset = -(batch.draw_index as f32 * 0.001);
576620
let mut transform = state.transform.to_bevy_transform();
577621
transform.translation.z += z_offset;
578622

579-
res.commands.spawn((
623+
let mut entity = res.commands.spawn((
580624
Mesh3d(mesh_handle),
581625
UntypedMaterial(material_handle),
582626
BelongsToGraphics(batch.graphics_entity),
583627
transform,
584628
batch.render_layers.clone(),
585629
));
586630

631+
if let Some(stroke_color) = state.stroke_color {
632+
entity.insert((
633+
Wireframe,
634+
WireframeColor {
635+
color: stroke_color,
636+
},
637+
WireframeLineWidth {
638+
width: state.stroke_weight,
639+
},
640+
WireframeTopology::Quads,
641+
));
642+
}
643+
587644
batch.draw_index += 1;
588645
}
589646

0 commit comments

Comments
 (0)