diff --git a/src/app_state.rs b/src/app_state.rs index 6fcb885..608969f 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,8 +1,8 @@ use crate::canvas::{CanvasTransform, Uniforms}; -use crate::drawing::{DrawingElement, Tool}; +use crate::drawing::{DrawingElement, Style, Tool}; use crate::state::{ - Canvas, GeometryBuffers, GpuContext, InputState, TextInput, UiBuffers, - UiScreenBuffers, UiScreenUniforms, UserInputState::Idle, + Canvas, GeometryBuffers, GpuContext, InputState, TextInput, UiBuffers, UiScreenBuffers, + UiScreenUniforms, UserInputState::Idle, }; use crate::text_renderer::TextRenderer; use crate::ui::UiRenderer; @@ -34,6 +34,7 @@ pub struct State { pub elements: Vec, pub current_tool: Tool, pub current_color: [f32; 4], + pub current_style: Style, pub stroke_width: f32, pub ui_renderer: UiRenderer, @@ -44,7 +45,7 @@ pub struct State { impl State { pub async fn new(window: Arc) -> State { let mut size = window.inner_size(); - + #[cfg(target_arch = "wasm32")] { if size.width == 0 || size.height == 0 { @@ -195,9 +196,7 @@ impl State { let ui_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("UI Shader"), - source: wgpu::ShaderSource::Wgsl( - include_str!("../data/shaders/ui_shader.wgsl").into(), - ), + source: wgpu::ShaderSource::Wgsl(include_str!("../data/shaders/ui_shader.wgsl").into()), }); let ui_uniform_bind_group_layout = @@ -311,18 +310,26 @@ impl State { }; let ui_renderer = UiRenderer::new(); - let text_renderer = TextRenderer::new(&gpu.device, &gpu.queue, surface_format, &uniform_bind_group_layout, &ui_uniform_bind_group_layout); + let text_renderer = TextRenderer::new( + &gpu.device, + &gpu.queue, + surface_format, + &uniform_bind_group_layout, + &ui_uniform_bind_group_layout, + ); let ui_screen_uniforms = UiScreenUniforms { screen_size: [size.width as f32, size.height as f32], _padding: [0.0, 0.0], }; - let ui_screen_uniform_buffer = gpu.device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("UI Screen Uniform Buffer"), - contents: bytemuck::cast_slice(&[ui_screen_uniforms]), - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - }); + let ui_screen_uniform_buffer = + gpu.device + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("UI Screen Uniform Buffer"), + contents: bytemuck::cast_slice(&[ui_screen_uniforms]), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); let ui_screen_bind_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor { layout: &ui_uniform_bind_group_layout, @@ -338,6 +345,8 @@ impl State { bind_group: ui_screen_bind_group, }; + let current_style = Style::Solid; + Self { window, size, @@ -349,15 +358,16 @@ impl State { typing, elements: Vec::new(), current_tool: Tool::Pen, - current_color: [0.0, 0.0, 0.0, 1.0], + current_color: [0.0, 0.0, 0.0, 1.0], stroke_width: 2.0, ui_renderer, text_renderer, ui_screen, + current_style, } } pub fn window(&self) -> &Arc { &self.window } -} \ No newline at end of file +} diff --git a/src/drawing.rs b/src/drawing.rs index ffbeb2a..c4e76fd 100644 --- a/src/drawing.rs +++ b/src/drawing.rs @@ -11,6 +11,12 @@ pub enum Tool { Select, } +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Style { + Dotted, + Solid, +} + #[derive(Debug, Clone, Copy, PartialEq)] pub enum BoxState { Idle, @@ -31,6 +37,7 @@ pub enum DrawingElement { color: [f32; 4], width: f32, rough_style: Option, + style: Style, }, Rectangle { position: [f32; 2], @@ -39,6 +46,7 @@ pub enum DrawingElement { fill: bool, stroke_width: f32, rough_style: Option, + style: Style, }, Circle { center: [f32; 2], @@ -47,6 +55,7 @@ pub enum DrawingElement { fill: bool, stroke_width: f32, rough_style: Option, + style: Style, }, Diamond { position: [f32; 2], @@ -55,6 +64,7 @@ pub enum DrawingElement { fill: bool, stroke_width: f32, rough_style: Option, + style: Style, }, Arrow { start: [f32; 2], @@ -62,6 +72,7 @@ pub enum DrawingElement { color: [f32; 4], width: f32, rough_style: Option, + style: Style, }, Text { position: [f32; 2], diff --git a/src/event_handler.rs b/src/event_handler.rs index 1994e14..b055477 100644 --- a/src/event_handler.rs +++ b/src/event_handler.rs @@ -95,6 +95,13 @@ impl State { return true; } + if let Some(style) = + self.ui_renderer.handle_style_click(self.input.mouse_pos) + { + self.current_style = style; + return true; + } + if self.ui_renderer.is_mouse_over_ui( self.input.mouse_pos, (self.size.width as f32, self.size.height as f32), @@ -463,6 +470,7 @@ impl State { let mut rough_options = crate::rough::RoughOptions::default(); rough_options.stroke_width = self.stroke_width; + rough_options.dotted = self.current_style == crate::drawing::Style::Dotted; let mut rng = rand::rng(); @@ -480,6 +488,7 @@ impl State { fill: false, stroke_width: self.stroke_width, rough_style: Some(rough_options), + style: self.current_style, }) } else { None @@ -492,6 +501,7 @@ impl State { let mut rough_options = crate::rough::RoughOptions::default(); rough_options.stroke_width = self.stroke_width; + rough_options.dotted = self.current_style == crate::drawing::Style::Dotted; let mut rng = rand::rng(); @@ -510,6 +520,7 @@ impl State { fill: false, stroke_width: self.stroke_width, rough_style: Some(rough_options), + style: self.current_style, }) } else { None @@ -521,6 +532,7 @@ impl State { let mut rough_options = crate::rough::RoughOptions::default(); rough_options.stroke_width = self.stroke_width; + rough_options.dotted = self.current_style == crate::drawing::Style::Dotted; let mut rng = rand::rng(); @@ -538,6 +550,7 @@ impl State { color: self.current_color, width: self.stroke_width, rough_style: Some(rough_options), + style: self.current_style, }) } else { None @@ -549,6 +562,7 @@ impl State { let mut rough_options = crate::rough::RoughOptions::default(); rough_options.stroke_width = self.stroke_width; + rough_options.dotted = self.current_style == crate::drawing::Style::Dotted; let mut rng = rand::rng(); @@ -566,6 +580,7 @@ impl State { color: self.current_color, width: self.stroke_width, rough_style: Some(rough_options), + style: self.current_style, }) } else { None @@ -579,6 +594,7 @@ impl State { let mut rough_options = crate::rough::RoughOptions::default(); rough_options.stroke_width = self.stroke_width; + rough_options.dotted = self.current_style == crate::drawing::Style::Dotted; let mut rng = rand::rng(); @@ -596,6 +612,7 @@ impl State { fill: false, stroke_width: self.stroke_width, rough_style: Some(rough_options), + style: self.current_style, }) } else { None @@ -638,6 +655,7 @@ impl State { fill: false, stroke_width: self.stroke_width, rough_style: None, + style: self.current_style, }); } } @@ -658,6 +676,7 @@ impl State { fill: false, stroke_width: self.stroke_width, rough_style: None, + style: self.current_style, }); } } @@ -676,6 +695,7 @@ impl State { ], width: self.stroke_width, rough_style: None, + style: self.current_style, }); } } @@ -694,6 +714,7 @@ impl State { ], width: self.stroke_width, rough_style: None, + style: self.current_style, }); } } @@ -715,6 +736,7 @@ impl State { fill: false, stroke_width: self.stroke_width, rough_style: None, + style: self.current_style, }); } } @@ -769,6 +791,7 @@ impl State { fill, stroke_width, rough_style, + style, } => { let half_width = size[0] / 2.0; let half_height = size[1] / 2.0; diff --git a/src/rough.rs b/src/rough.rs index 38f2dc2..aac3844 100644 --- a/src/rough.rs +++ b/src/rough.rs @@ -14,6 +14,7 @@ pub struct RoughOptions { pub seed: Option, pub curve_tightness: f32, pub preserve_vertices: bool, + pub dotted: bool, } impl Default for RoughOptions { @@ -28,6 +29,7 @@ impl Default for RoughOptions { seed: None, curve_tightness: 0.0, preserve_vertices: false, + dotted: false, } } } @@ -64,6 +66,10 @@ impl RoughGenerator { end: [f32; 2], options: &RoughOptions, ) -> Vec<[f32; 2]> { + if options.dotted { + return self.rough_dotted_line(start, end, options); + } + let mut points = Vec::new(); let length_sq = (start[0] - end[0]).powi(2) + (start[1] - end[1]).powi(2); @@ -147,6 +153,93 @@ impl RoughGenerator { points } + fn rough_dotted_line( + &mut self, + start: [f32; 2], + end: [f32; 2], + options: &RoughOptions, + ) -> Vec<[f32; 2]> { + let mut points = Vec::new(); + + let dx = end[0] - start[0]; + let dy = end[1] - start[1]; + let total_length = (dx * dx + dy * dy).sqrt(); + + if total_length < 1.0 { + return vec![start, end]; + } + + // Create evenly spaced dots with slight roughness like Excalidraw + let dot_spacing = (options.stroke_width * 4.0).max(8.0); // Tighter spacing for better arrowheads + let num_dots = (total_length / dot_spacing).floor() as u32; + + // Ensure short lines (like arrowheads) get at least one dot + let num_dots = if num_dots == 0 && total_length > 5.0 { + 1 + } else { + num_dots + }; + + if num_dots == 0 { + return points; + } + + // Normalize direction + let dir_x = dx / total_length; + let dir_y = dy / total_length; + + // Create simple square dots - much simpler approach + for i in 0..num_dots { + let t = (i as f32 + 0.5) / num_dots as f32; // Center dots in segments + let base_pos = t * total_length; + + // Add slight random offset for hand-drawn feel + let offset = self.offset_opt(options.stroke_width * 0.3, options, 0.3); + let actual_pos = (base_pos + offset).clamp(0.0, total_length); + + let dot_center = [ + start[0] + dir_x * actual_pos + self.offset_opt(options.stroke_width * 0.2, options, 0.2), + start[1] + dir_y * actual_pos + self.offset_opt(options.stroke_width * 0.2, options, 0.2), + ]; + + points.push(dot_center); + } + + points + } + + pub fn rough_dotted_lines( + &mut self, + lines: Vec>, + options: &RoughOptions, + ) -> Vec> { + if !options.dotted { + return lines; + } + + let mut dotted_lines = Vec::new(); + + for line in lines { + if line.len() < 2 { + continue; + } + + for i in 0..line.len() - 1 { + let start = line[i]; + let end = line[i + 1]; + + let mut dot_options = options.clone(); + dot_options.dotted = false; // Prevent infinite recursion + let dotted_segment = self.rough_dotted_line(start, end, &dot_options); + if !dotted_segment.is_empty() { + dotted_lines.push(dotted_segment); + } + } + } + + dotted_lines + } + fn bezier_curve( &self, p0: [f32; 2], @@ -202,7 +295,11 @@ impl RoughGenerator { } } - lines + if options.dotted { + self.rough_dotted_lines(lines, options) + } else { + lines + } } pub fn rough_diamond( @@ -238,7 +335,11 @@ impl RoughGenerator { } } - lines + if options.dotted { + self.rough_dotted_lines(lines, options) + } else { + lines + } } pub fn rough_ellipse( @@ -289,7 +390,11 @@ impl RoughGenerator { result.push(points2); } - result + if options.dotted { + self.rough_dotted_lines(result, options) + } else { + result + } } fn compute_ellipse_points_varied( @@ -499,6 +604,93 @@ impl RoughGenerator { (vertices, indices) } + pub fn dotted_points_to_vertices( + &self, + points: &[[f32; 2]], + color: [f32; 4], + width: f32, + ) -> (Vec, Vec) { + let mut vertices = Vec::new(); + let mut indices = Vec::new(); + let mut index_offset = 0u16; + + for (i, &dot_center) in points.iter().enumerate() { + let pill_length = width * 2.0; + let pill_radius = width * 0.5; + + let rotation = if points.len() > 1 { + if i == 0 { + let next = points[1]; + let dx = next[0] - dot_center[0]; + let dy = next[1] - dot_center[1]; + dy.atan2(dx) + } else if i == points.len() - 1 { + let prev = points[i - 1]; + let dx = dot_center[0] - prev[0]; + let dy = dot_center[1] - prev[1]; + dy.atan2(dx) + } else { + let prev = points[i - 1]; + let next = points[i + 1]; + let dx1 = dot_center[0] - prev[0]; + let dy1 = dot_center[1] - prev[1]; + let dx2 = next[0] - dot_center[0]; + let dy2 = next[1] - dot_center[1]; + let angle1 = dy1.atan2(dx1); + let angle2 = dy2.atan2(dx2); + (angle1 + angle2) * 0.5 + } + } else { + 0.0 + }; + + let cos_rot = rotation.cos(); + let sin_rot = rotation.sin(); + let half_length = pill_length * 0.5; + + let segments = 16; + + vertices.push(Vertex { position: dot_center, color }); + let center_idx = index_offset; + index_offset += 1; + + let segments_per_cap = 6; + let total_segments = segments_per_cap * 2; + + for j in 0..segments_per_cap { + let angle = (j as f32) / (segments_per_cap - 1) as f32 * std::f32::consts::PI - std::f32::consts::PI * 0.5; + let local_x = half_length + pill_radius * angle.cos(); + let local_y = pill_radius * angle.sin(); + + let x = dot_center[0] + local_x * cos_rot - local_y * sin_rot; + let y = dot_center[1] + local_x * sin_rot + local_y * cos_rot; + vertices.push(Vertex { position: [x, y], color }); + } + + for j in 0..segments_per_cap { + let angle = (j as f32) / (segments_per_cap - 1) as f32 * std::f32::consts::PI + std::f32::consts::PI * 0.5; + let local_x = -half_length + pill_radius * angle.cos(); + let local_y = pill_radius * angle.sin(); + + let x = dot_center[0] + local_x * cos_rot - local_y * sin_rot; + let y = dot_center[1] + local_x * sin_rot + local_y * cos_rot; + vertices.push(Vertex { position: [x, y], color }); + } + + for j in 0..total_segments { + let current = index_offset + j; + let next = index_offset + ((j + 1) % total_segments); + + indices.extend_from_slice(&[center_idx, current, next]); + indices.extend_from_slice(&[center_idx, next, current]); + } + + index_offset += total_segments; + } + + (vertices, indices) + } + pub fn rough_arrow( &mut self, start: [f32; 2], @@ -549,6 +741,130 @@ impl RoughGenerator { } } + if options.dotted { + self.rough_dotted_lines(lines, options) + } else { + lines + } + } + + pub fn rough_arrow_with_solid_head( + &mut self, + start: [f32; 2], + end: [f32; 2], + options: &RoughOptions, + ) -> (Vec>, Option<[[f32; 2]; 3]>) { + let mut shaft_lines = Vec::new(); + + // Generate shaft lines + let shaft_line = self.rough_line(start, end, options); + shaft_lines.push(shaft_line); + + if !options.disable_multi_stroke { + let shaft_line2 = self.rough_line(start, end, options); + shaft_lines.push(shaft_line2); + } + + // Generate arrowhead as solid triangle + let dx = end[0] - start[0]; + let dy = end[1] - start[1]; + let len = (dx * dx + dy * dy).sqrt(); + + let arrowhead = if len > 0.0 { + let head_len = (20.0 + self.offset_opt(5.0, options, 1.0)).max(10.0); + let head_angle = 0.5 + self.offset_opt(0.1, options, 1.0); + + let dir_x = dx / len; + let dir_y = dy / len; + + let cos_angle = head_angle.cos(); + let sin_angle = head_angle.sin(); + + let left_x = end[0] - head_len * (dir_x * cos_angle - dir_y * sin_angle); + let left_y = end[1] - head_len * (dir_y * cos_angle + dir_x * sin_angle); + + let right_x = end[0] - head_len * (dir_x * cos_angle + dir_y * sin_angle); + let right_y = end[1] - head_len * (dir_y * cos_angle - dir_x * sin_angle); + + Some([[left_x, left_y], end, [right_x, right_y]]) + } else { + None + }; + + // Apply dotting only to shaft lines + let final_shaft_lines = if options.dotted { + self.rough_dotted_lines(shaft_lines, options) + } else { + shaft_lines + }; + + (final_shaft_lines, arrowhead) + } + + pub fn rough_dotted_arrow( + &mut self, + start: [f32; 2], + end: [f32; 2], + options: &RoughOptions, + ) -> Vec> { + // Shaft first + let shaft_points = self.rough_dotted_line(start, end, options); + let mut lines: Vec> = vec![shaft_points]; + + // --- Arrowhead computation --- + let dx = end[0] - start[0]; + let dy = end[1] - start[1]; + let len = (dx * dx + dy * dy).sqrt(); + if len < 1.0 { + return lines; + } + + // Geometry for arrowhead + let head_len = (20.0 + self.offset_opt(5.0, options, 1.0)).max(12.0); + let head_angle = 0.5 + self.offset_opt(0.1, options, 1.0); + let dir_x = dx / len; + let dir_y = dy / len; + let cos_a = head_angle.cos(); + let sin_a = head_angle.sin(); + let left_pt = [ + end[0] - head_len * (dir_x * cos_a - dir_y * sin_a), + end[1] - head_len * (dir_y * cos_a + dir_x * sin_a), + ]; + let right_pt = [ + end[0] - head_len * (dir_x * cos_a + dir_y * sin_a), + end[1] - head_len * (dir_y * cos_a - dir_x * sin_a), + ]; + + // Helper to create dense dot line between two points + let mut dense_dots = |p1: [f32; 2], p2: [f32; 2]| -> Vec<[f32; 2]> { + let dx = p2[0] - p1[0]; + let dy = p2[1] - p1[1]; + let seg_len = (dx * dx + dy * dy).sqrt(); + let spacing = (options.stroke_width * 2.0).max(6.0); + let mut pts = Vec::new(); + if seg_len < 0.5 { + return pts; + } + let count = ((seg_len / spacing).floor() as u32).max(1); + for i in 0..=count { + let t = i as f32 / count as f32; + pts.push([p1[0] + dx * t, p1[1] + dy * t]); + } + pts + }; + + // Generate dots for each arrowhead edge + let left_edge_pts = dense_dots(end, left_pt); + if !left_edge_pts.is_empty() { + lines.push(left_edge_pts); + } + let right_edge_pts = dense_dots(end, right_pt); + if !right_edge_pts.is_empty() { + lines.push(right_edge_pts); + } + lines } + + } diff --git a/src/ui.rs b/src/ui.rs index 0523a0a..38c299a 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,4 +1,5 @@ use crate::{drawing::Tool, vertex::UiVertex}; +use crate::drawing::Style; const PALETTE_COLORS: [[f32; 4]; 6] = [ [0.0, 0.0, 0.0, 1.0], @@ -9,9 +10,12 @@ const PALETTE_COLORS: [[f32; 4]; 6] = [ [0.14, 0.75, 0.37, 1.0], ]; +const STYLE_OPTIONS: [Style; 2] = [Style::Solid, Style::Dotted]; + pub struct UiRenderer { tool_icons: Vec, color_palette: Vec, + style_options: Vec, } struct ColorSwatch { @@ -20,6 +24,12 @@ struct ColorSwatch { size: [f32; 2], } +struct StyleOption { + style: Style, + position: [f32; 2], + size: [f32; 2], +} + struct ToolIcon { tool: Tool, position: [f32; 2], @@ -110,9 +120,34 @@ impl UiRenderer { }) .collect(); + let style_options = STYLE_OPTIONS + .iter() + .enumerate() + .map(|(i, &style)| { + let cols = 2; + let swatch_size = 30.0; + let padding = 8.0; + let start_x = 15.0; + let start_y = 130.0; + + let col = (i % cols) as f32; + let row = (i / cols) as f32; + + StyleOption { + style, + position: [ + start_x + col as f32 * (swatch_size + padding), + start_y + row as f32 * (swatch_size + padding), + ], + size: [swatch_size, swatch_size], + } + }) + .collect(); + Self { tool_icons, color_palette, + style_options, } } @@ -981,6 +1016,140 @@ impl UiRenderer { } } + fn generate_style_picker( + &self, + vertices: &mut Vec, + indices: &mut Vec, + index_offset: &mut u16, + current_style: Style, + ) { + let swatch_size = 30.0; + let padding = 8.0; + let cols = 2; + let color_start_y = 90.0; + let color_rows = (PALETTE_COLORS.len() + cols - 1) / cols; + let palette_height = color_rows as f32 * swatch_size + (color_rows as f32 - 1.0) * padding; + let style_start_y = color_start_y + palette_height + padding; + let start_x = 15.0; + + for (i, &style) in STYLE_OPTIONS.iter().enumerate() { + let col = (i % 2) as f32; + let row = (i / 2) as f32; + + let center = [ + start_x + col * (swatch_size + padding) + swatch_size * 0.5, + style_start_y + row * (swatch_size + padding) + swatch_size * 0.5, + ]; + + let is_selected = style == current_style; + + let border_width = if is_selected { 2.0 } else { 1.0 }; + let bg_color = if is_selected { + [0.25, 0.55, 0.95, 1.0] + } else { + [0.95, 0.95, 0.95, 1.0] + }; + + self.create_rounded_rect( + vertices, + indices, + index_offset, + center, + [swatch_size, swatch_size], + bg_color, + 6.0, + border_width, + ); + + let line_color = if is_selected { + [1.0, 1.0, 1.0, 1.0] + } else { + [0.2, 0.2, 0.2, 1.0] + }; + + match style { + Style::Solid => { + self.draw_solid_line_icon( + vertices, + indices, + index_offset, + center, + swatch_size * 0.7, + line_color, + ); + } + Style::Dotted => { + self.draw_dotted_line_icon( + vertices, + indices, + index_offset, + center, + swatch_size * 0.7, + line_color, + ); + } + } + } + } + + fn draw_solid_line_icon( + &self, + vertices: &mut Vec, + indices: &mut Vec, + index_offset: &mut u16, + center: [f32; 2], + line_length: f32, + color: [f32; 4], + ) { + let thickness = 2.0; + let start_x = center[0] - line_length * 0.5; + let end_x = center[0] + line_length * 0.5; + let y = center[1]; + + self.create_simple_rect( + vertices, + indices, + index_offset, + [center[0], y], + [line_length, thickness], + color, + ); + } + + fn draw_dotted_line_icon( + &self, + vertices: &mut Vec, + indices: &mut Vec, + index_offset: &mut u16, + center: [f32; 2], + line_length: f32, + color: [f32; 4], + ) { + let dot_size = line_length * 0.15; + let num_dots = 4; + let half_len = line_length * 0.5; + let spacing = if num_dots > 1 { + line_length / (num_dots as f32 - 1.0) + } else { + 0.0 + }; + for i in 0..num_dots { + let offset = -half_len + spacing * i as f32; + let x = center[0] + offset; + let y = center[1] + offset; + self.create_rounded_rect( + vertices, + indices, + index_offset, + [x, y], + [dot_size, dot_size], + color, + dot_size * 0.5, + 0.0, + ); + } + } + fn generate_toolbar( &self, vertices: &mut Vec, @@ -1130,6 +1299,7 @@ impl UiRenderer { &self, current_tool: Tool, current_color: [f32; 4], + current_style: Style, screen_size: (f32, f32), _zoom_level: f32, ) -> (Vec, Vec) { @@ -1143,6 +1313,12 @@ impl UiRenderer { &mut index_offset, current_color, ); + self.generate_style_picker( + &mut vertices, + &mut indices, + &mut index_offset, + current_style, + ); self.generate_toolbar( &mut vertices, &mut indices, @@ -1179,6 +1355,34 @@ impl UiRenderer { None } + pub fn handle_style_click(&self, mouse_pos: [f32; 2]) -> Option