From 9dec17b4c1894b4ac314b359de094b7cb0e58633 Mon Sep 17 00:00:00 2001 From: GitButler Date: Thu, 3 Jul 2025 22:44:05 -0500 Subject: [PATCH 1/4] GitButler Workspace Commit This is placeholder commit and will be replaced by a merge of yourvirtual branches. Due to GitButler managing multiple virtual branches, you cannot switch back and forth between git branches and virtual branches easily. If you switch to another branch, GitButler will need to be reinitialized. If you commit on this branch, GitButler will throw it away. For more information about what we're doing here, check out our docs: https://docs.gitbutler.com/features/virtual-branches/integration-branch From 856b4dcffa666e42583dba26b2f535290e073f02 Mon Sep 17 00:00:00 2001 From: tspython Date: Mon, 7 Jul 2025 19:45:45 -0500 Subject: [PATCH 2/4] update todo --- todo.txt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/todo.txt b/todo.txt index b5cd9f2..7ac46f9 100644 --- a/todo.txt +++ b/todo.txt @@ -11,4 +11,13 @@ - We currently have preset colors, I want to have the user be able to use those preset colors as a base and custom configure them either using a color wheel or a color picker. - Toolbar - - Diamond Shape + - Diamond Shape [x] + - SVG Rendering + +- Saving State + - Save as Json? + - if window event = close -> ask to save drawing + +- Brush Styles + - Dotted + - Solid From b1b8b41bfafde3fd477ac7e7aecc979fcc1de089 Mon Sep 17 00:00:00 2001 From: tspython Date: Mon, 7 Jul 2025 20:23:13 -0500 Subject: [PATCH 3/4] init --- src/app_state.rs | 40 +++++++++++++++++++++++++--------------- src/drawing.rs | 11 +++++++++++ src/event_handler.rs | 11 +++++++++++ src/update_logic.rs | 5 +++++ todo.txt | 3 +++ 5 files changed, 55 insertions(+), 15 deletions(-) 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..0d0dec8 100644 --- a/src/event_handler.rs +++ b/src/event_handler.rs @@ -480,6 +480,7 @@ impl State { fill: false, stroke_width: self.stroke_width, rough_style: Some(rough_options), + style: self.current_style, }) } else { None @@ -510,6 +511,7 @@ impl State { fill: false, stroke_width: self.stroke_width, rough_style: Some(rough_options), + style: self.current_style, }) } else { None @@ -538,6 +540,7 @@ impl State { color: self.current_color, width: self.stroke_width, rough_style: Some(rough_options), + style: self.current_style, }) } else { None @@ -566,6 +569,7 @@ impl State { color: self.current_color, width: self.stroke_width, rough_style: Some(rough_options), + style: self.current_style, }) } else { None @@ -596,6 +600,7 @@ impl State { fill: false, stroke_width: self.stroke_width, rough_style: Some(rough_options), + style: self.current_style, }) } else { None @@ -638,6 +643,7 @@ impl State { fill: false, stroke_width: self.stroke_width, rough_style: None, + style: self.current_style, }); } } @@ -658,6 +664,7 @@ impl State { fill: false, stroke_width: self.stroke_width, rough_style: None, + style: self.current_style, }); } } @@ -676,6 +683,7 @@ impl State { ], width: self.stroke_width, rough_style: None, + style: self.current_style, }); } } @@ -694,6 +702,7 @@ impl State { ], width: self.stroke_width, rough_style: None, + style: self.current_style, }); } } @@ -715,6 +724,7 @@ impl State { fill: false, stroke_width: self.stroke_width, rough_style: None, + style: self.current_style, }); } } @@ -769,6 +779,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/update_logic.rs b/src/update_logic.rs index 23e33fb..9e139a5 100644 --- a/src/update_logic.rs +++ b/src/update_logic.rs @@ -153,6 +153,7 @@ impl State { fill, stroke_width, rough_style, + style, } => { if let Some(rough_options) = rough_style { let mut generator = crate::rough::RoughGenerator::new(rough_options.seed); @@ -258,6 +259,7 @@ impl State { fill, stroke_width, rough_style, + style, } => { if let Some(rough_options) = rough_style { let mut generator = crate::rough::RoughGenerator::new(rough_options.seed); @@ -371,6 +373,7 @@ impl State { fill, stroke_width, rough_style, + style, } => { if let Some(rough_options) = rough_style { let mut generator = crate::rough::RoughGenerator::new(rough_options.seed); @@ -475,6 +478,7 @@ impl State { color, width, rough_style, + style, } => { if let Some(rough_options) = rough_style { let mut generator = crate::rough::RoughGenerator::new(rough_options.seed); @@ -619,6 +623,7 @@ impl State { color, width, rough_style, + style, } => { if let Some(rough_options) = rough_style { let mut generator = crate::rough::RoughGenerator::new(rough_options.seed); diff --git a/todo.txt b/todo.txt index 7ac46f9..bc9674a 100644 --- a/todo.txt +++ b/todo.txt @@ -21,3 +21,6 @@ - Brush Styles - Dotted - Solid + +- Refactor + - A lot of shared functionality that can be refactored into functions to reduce LOC From 4db6b667cee427d5335525155247a4ece68d49c1 Mon Sep 17 00:00:00 2001 From: tspython Date: Tue, 8 Jul 2025 19:06:53 -0500 Subject: [PATCH 4/4] almost finalized dotted lines --- src/event_handler.rs | 12 ++ src/rough.rs | 322 ++++++++++++++++++++++++++++++++++++++++++- src/ui.rs | 220 ++++++++++++++++++++++++++++- src/update_logic.rs | 190 ++++++++++++++----------- 4 files changed, 657 insertions(+), 87 deletions(-) diff --git a/src/event_handler.rs b/src/event_handler.rs index 0d0dec8..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(); @@ -493,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(); @@ -523,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(); @@ -552,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(); @@ -583,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(); 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