use super::gpu_context::{BrushGpuContext, BrushPerfCounters, DabBatch, StrokeResources};
use super::nodes::pen_input;
use super::paint_info::PaintInformation;
use super::pipeline::BrushPipelines;
use super::stabilizer::PassThrough;
use super::stroke_buffer::StrokeBuffer;
use super::stroke_engine::StrokeEngine;
use super::wire::BrushWireType;
use crate::nodegraph::Graph;
struct PreviewTarget {
width: u32,
height: u32,
layer_texture: wgpu::Texture,
layer_view: wgpu::TextureView,
stroke_buffer: StrokeBuffer,
}
impl PreviewTarget {
fn new(device: &wgpu::Device, width: u32, height: u32, pipelines: &BrushPipelines) -> Self {
let layer_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("brush-preview-layer"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::COPY_SRC
| wgpu::TextureUsages::COPY_DST
| wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let layer_view = layer_texture.create_view(&wgpu::TextureViewDescriptor::default());
let stroke_buffer = StrokeBuffer::new(device, width, height, pipelines);
Self {
width,
height,
layer_texture,
layer_view,
stroke_buffer,
}
}
}
pub struct BrushStrokePreviewRenderer {
target: Option<PreviewTarget>,
}
impl BrushStrokePreviewRenderer {
pub fn new() -> Self {
Self { target: None }
}
pub fn render_stroke(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
pipelines: &BrushPipelines,
graph: &Graph<BrushWireType>,
path: &[PaintInformation],
fg_color: [f32; 4],
bg_color: [f32; 4],
width: u32,
height: u32,
) -> Option<&wgpu::Texture> {
if path.is_empty() || width == 0 || height == 0 {
return None;
}
let runner = super::compile_graph(graph).ok()?;
let target_changed = match &self.target {
Some(t) => t.width != width || t.height != height,
None => true,
};
if target_changed {
self.target = Some(PreviewTarget::new(device, width, height, pipelines));
}
let target = self.target.as_mut().unwrap();
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("brush-preview-pre-fill"),
});
{
let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("brush-preview-bg-clear"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &target.layer_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: bg_color[0] as f64,
g: bg_color[1] as f64,
b: bg_color[2] as f64,
a: bg_color[3] as f64,
}),
store: wgpu::StoreOp::Store,
},
})],
..Default::default()
});
}
let paint_target = crate::gpu::paint_target::GpuPaintTarget::from_canvas_texture(
&target.layer_texture,
&target.layer_view,
wgpu::TextureFormat::Rgba8Unorm,
crate::coord::CanvasRect::from_xywh(0, 0, width, height),
);
target
.stroke_buffer
.save_pre_stroke(device, &mut encoder, pipelines, &paint_target);
queue.submit([encoder.finish()]);
pipelines.reset_uniform_rings();
let spacing = pen_input::spacing_config(graph);
let mut engine = StrokeEngine::new(runner, fg_color, spacing, Box::new(PassThrough::new()));
for pt in path {
let _ = engine.stabilize(*pt);
}
let sel_bg = pipelines.default_selection_bind_group();
macro_rules! make_gpu_ctx {
($label:expr) => {{
let (scratch, pre_stroke_texture, pre_stroke_bind_group) =
target.stroke_buffer.parts_for_brush_ctx();
BrushGpuContext {
encoder: device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some($label),
}),
device,
queue,
pipelines,
selection_bind_group: sel_bg,
canvas_width: width,
canvas_height: height,
canvas_origin: [0, 0],
blend_mode: 0,
view_rotation: 0.0,
perf: BrushPerfCounters::default(),
stroke: Some(StrokeResources {
scratch,
paint_target,
pre_stroke_texture,
pre_stroke_bind_group,
}),
preview: None,
dab_batch: DabBatch::default(),
}
}};
}
{
let mut ctx = make_gpu_ctx!("brush-preview-begin-stroke");
engine.begin_stroke(&mut ctx);
ctx.submit_final();
}
{
let end = path.len() - 1;
let mut ctx = make_gpu_ctx!("brush-preview-stroke");
engine.render_from_stabilized_range_to(&mut ctx, 0, end);
ctx.submit_final();
}
{
let mut ctx = make_gpu_ctx!("brush-preview-commit");
engine.commit(&mut ctx);
ctx.submit_final();
}
Some(&target.layer_texture)
}
pub fn current_texture(&self) -> Option<&wgpu::Texture> {
self.target.as_ref().map(|t| &t.layer_texture)
}
pub fn current_size(&self) -> Option<(u32, u32)> {
self.target.as_ref().map(|t| (t.width, t.height))
}
}
impl Default for BrushStrokePreviewRenderer {
fn default() -> Self {
Self::new()
}
}
pub fn synthesize_dab_path(width: f32, height: f32) -> Vec<PaintInformation> {
vec![PaintInformation {
pos: [width * 0.5, height * 0.5],
pressure: 1.0,
..Default::default()
}]
}
pub fn synthesize_stroke_path(
width: f32,
height: f32,
n_points: usize,
inset: f32,
) -> Vec<PaintInformation> {
let n = n_points.max(2);
let lx = inset;
let rx = width - inset;
let ty = inset;
let by = height - inset;
let span_x = rx - lx;
let span_y = by - ty;
let p0 = [lx, ty + span_y * 0.7];
let p1 = [lx + span_x * 0.30, ty + span_y * 0.10];
let p2 = [lx + span_x * 0.70, ty + span_y * 0.90];
let p3 = [rx, ty + span_y * 0.30];
let mut out = Vec::with_capacity(n);
for i in 0..n {
let t = i as f32 / (n - 1) as f32;
let pos = cubic_bezier(p0, p1, p2, p3, t);
let pressure = if t < 0.5 {
t * 2.0
} else {
1.0 - (t - 0.5) * 1.6
};
out.push(PaintInformation {
pos,
pressure,
time: t * 0.5,
..Default::default()
});
}
out
}
fn cubic_bezier(p0: [f32; 2], p1: [f32; 2], p2: [f32; 2], p3: [f32; 2], t: f32) -> [f32; 2] {
let u = 1.0 - t;
let w0 = u * u * u;
let w1 = 3.0 * u * u * t;
let w2 = 3.0 * u * t * t;
let w3 = t * t * t;
[
w0 * p0[0] + w1 * p1[0] + w2 * p2[0] + w3 * p3[0],
w0 * p0[1] + w1 * p1[1] + w2 * p2[1] + w3 * p3[1],
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn synthesized_stroke_bounds() {
let inset = 16.0;
let path = synthesize_stroke_path(320.0, 120.0, 30, inset);
assert_eq!(path.len(), 30);
assert!((path[0].pos[0] - inset).abs() < 1e-3);
assert!((path[29].pos[0] - (320.0 - inset)).abs() < 1e-3);
assert!((path[0].pressure - 0.0).abs() < 1e-6);
assert!((path[29].pressure - 0.2).abs() < 1e-3);
let mid = path.len() / 2;
assert!(path[mid].pressure > 0.9);
for p in &path {
assert!(p.pos[0] >= 0.0 && p.pos[0] <= 320.0);
assert!(p.pos[1] >= 0.0 && p.pos[1] <= 120.0);
}
}
#[test]
fn synthesized_stroke_respects_min_points() {
let path = synthesize_stroke_path(100.0, 100.0, 1, 0.0);
assert_eq!(path.len(), 2);
}
}