Skip to main content

draw_core/render/
draw.rs

1use tiny_skia::*;
2
3use crate::element::{Element, FreeDrawElement, LineElement, ShapeElement, TextElement};
4use crate::geometry;
5use crate::style::{FillStyle, FillType};
6
7use super::path::*;
8use super::{
9    CORNER_RADIUS, GRID_ALPHA, GRID_B, GRID_G, GRID_MIN_SCREEN_PX, GRID_R, GRID_SIZE,
10    HACHURE_ALPHA, HACHURE_LINE_WIDTH, parse_color, stroke_from_style,
11};
12
13impl super::Renderer {
14    pub(super) fn draw_element(&self, pixmap: &mut Pixmap, el: &Element, transform: &Transform) {
15        let opacity = el.opacity() as f32;
16        match el {
17            Element::Rectangle(e) => self.draw_rectangle(pixmap, e, transform, opacity),
18            Element::Ellipse(e) => self.draw_ellipse(pixmap, e, transform, opacity),
19            Element::Diamond(e) => self.draw_diamond(pixmap, e, transform, opacity),
20            Element::Line(e) => self.draw_line(pixmap, e, transform, opacity),
21            Element::Arrow(e) => self.draw_arrow(pixmap, e, transform, opacity),
22            Element::FreeDraw(e) => self.draw_freedraw(pixmap, e, transform, opacity),
23            Element::Text(e) => self.draw_text(pixmap, e, transform, opacity),
24        }
25    }
26
27    fn draw_rectangle(
28        &self,
29        pixmap: &mut Pixmap,
30        el: &ShapeElement,
31        transform: &Transform,
32        opacity: f32,
33    ) {
34        let (nx, ny, nw, nh) = geometry::normalize_bounds(el.x, el.y, el.width, el.height);
35        let (x, y, w, h) = (nx as f32, ny as f32, nw as f32, nh as f32);
36        if w < 0.5 && h < 0.5 {
37            return;
38        }
39
40        let radius = CORNER_RADIUS.min(w / 3.0).min(h / 3.0);
41        if let Some(path) = build_rounded_rect_path(x, y, w, h, radius) {
42            self.fill_shape(pixmap, &path, &el.fill, x, y, w, h, transform, opacity);
43            let (paint, stroke) = stroke_from_style(&el.stroke, opacity);
44            pixmap.stroke_path(&path, &paint, &stroke, *transform, None);
45        }
46    }
47
48    fn draw_ellipse(
49        &self,
50        pixmap: &mut Pixmap,
51        el: &ShapeElement,
52        transform: &Transform,
53        opacity: f32,
54    ) {
55        let rx = (el.width).abs() as f32 / 2.0;
56        let ry = (el.height).abs() as f32 / 2.0;
57        if rx < 0.5 && ry < 0.5 {
58            return;
59        }
60
61        let cx = el.x as f32 + el.width as f32 / 2.0;
62        let cy = el.y as f32 + el.height as f32 / 2.0;
63        let safe_rx = rx.max(0.1);
64        let safe_ry = ry.max(0.1);
65
66        if let Some(path) = build_ellipse_path(cx, cy, safe_rx, safe_ry) {
67            let (nx, ny, nw, nh) = geometry::normalize_bounds(el.x, el.y, el.width, el.height);
68            let (bx, by, w, h) = (nx as f32, ny as f32, nw as f32, nh as f32);
69
70            self.fill_shape(pixmap, &path, &el.fill, bx, by, w, h, transform, opacity);
71            let (paint, stroke) = stroke_from_style(&el.stroke, opacity);
72            pixmap.stroke_path(&path, &paint, &stroke, *transform, None);
73        }
74    }
75
76    fn draw_diamond(
77        &self,
78        pixmap: &mut Pixmap,
79        el: &ShapeElement,
80        transform: &Transform,
81        opacity: f32,
82    ) {
83        let (nx, ny, nw, nh) = geometry::normalize_bounds(el.x, el.y, el.width, el.height);
84        let (x, y, w, h) = (nx as f32, ny as f32, nw as f32, nh as f32);
85        if w < 0.5 && h < 0.5 {
86            return;
87        }
88
89        if let Some(path) = build_diamond_path(x, y, w, h) {
90            self.fill_shape(pixmap, &path, &el.fill, x, y, w, h, transform, opacity);
91            let (paint, stroke) = stroke_from_style(&el.stroke, opacity);
92            pixmap.stroke_path(&path, &paint, &stroke, *transform, None);
93        }
94    }
95
96    fn draw_line(
97        &self,
98        pixmap: &mut Pixmap,
99        el: &LineElement,
100        transform: &Transform,
101        opacity: f32,
102    ) {
103        if el.points.len() < 2 {
104            return;
105        }
106        if let Some(path) = build_polyline_path(el) {
107            let (paint, stroke) = stroke_from_style(&el.stroke, opacity);
108            pixmap.stroke_path(&path, &paint, &stroke, *transform, None);
109        }
110    }
111
112    fn draw_arrow(
113        &self,
114        pixmap: &mut Pixmap,
115        el: &LineElement,
116        transform: &Transform,
117        opacity: f32,
118    ) {
119        self.draw_line(pixmap, el, transform, opacity);
120        if el.points.len() < 2 {
121            return;
122        }
123
124        let color = parse_color(&el.stroke.color, opacity);
125        let mut paint = Paint::default();
126        paint.set_color(color);
127        paint.anti_alias = true;
128        let arrowhead_stroke = Stroke {
129            width: (el.stroke.width as f32) * 0.5,
130            line_cap: LineCap::Round,
131            line_join: LineJoin::Round,
132            ..Stroke::default()
133        };
134
135        let last = el.points.last().unwrap();
136        let prev = &el.points[el.points.len() - 2];
137        let end_ah = geometry::compute_arrowhead(
138            last.x + el.x,
139            last.y + el.y,
140            prev.x + el.x,
141            prev.y + el.y,
142            geometry::ARROWHEAD_LENGTH,
143            geometry::ARROWHEAD_ANGLE,
144        );
145        draw_arrowhead_path(pixmap, &end_ah, &paint, &arrowhead_stroke, transform);
146
147        if el.start_arrowhead.is_some() {
148            let first = &el.points[0];
149            let next = &el.points[1];
150            let start_ah = geometry::compute_arrowhead(
151                first.x + el.x,
152                first.y + el.y,
153                next.x + el.x,
154                next.y + el.y,
155                geometry::ARROWHEAD_LENGTH,
156                geometry::ARROWHEAD_ANGLE,
157            );
158            draw_arrowhead_path(pixmap, &start_ah, &paint, &arrowhead_stroke, transform);
159        }
160    }
161
162    fn draw_freedraw(
163        &self,
164        pixmap: &mut Pixmap,
165        el: &FreeDrawElement,
166        transform: &Transform,
167        opacity: f32,
168    ) {
169        if el.points.len() < 2 {
170            return;
171        }
172        if let Some(path) = build_freedraw_path(el) {
173            let (paint, stroke) = stroke_from_style(&el.stroke, opacity);
174            pixmap.stroke_path(&path, &paint, &stroke, *transform, None);
175        }
176    }
177
178    fn draw_text(
179        &self,
180        _pixmap: &mut Pixmap,
181        _el: &TextElement,
182        _transform: &Transform,
183        _opacity: f32,
184    ) {
185        // Text is rendered by the browser via canvas overlays (renderTextOverlays).
186        // The WASM renderer intentionally draws nothing here.
187    }
188
189    // ── Fill patterns ───────────────────────────────────────────────
190
191    #[allow(clippy::too_many_arguments)]
192    fn fill_shape(
193        &self,
194        pixmap: &mut Pixmap,
195        clip_path: &Path,
196        fill: &FillStyle,
197        x: f32,
198        y: f32,
199        w: f32,
200        h: f32,
201        transform: &Transform,
202        opacity: f32,
203    ) {
204        match fill.style {
205            FillType::None => {}
206            FillType::Solid => {
207                let color = parse_color(&fill.color, opacity);
208                let mut paint = Paint::default();
209                paint.set_color(color);
210                paint.anti_alias = true;
211                pixmap.fill_path(clip_path, &paint, FillRule::Winding, *transform, None);
212            }
213            FillType::Hachure => {
214                self.draw_hachure_fill(
215                    pixmap,
216                    clip_path,
217                    &fill.color,
218                    fill.gap as f32,
219                    fill.angle as f32,
220                    x,
221                    y,
222                    w,
223                    h,
224                    transform,
225                    opacity,
226                );
227            }
228            FillType::CrossHatch => {
229                self.draw_hachure_fill(
230                    pixmap,
231                    clip_path,
232                    &fill.color,
233                    fill.gap as f32,
234                    fill.angle as f32,
235                    x,
236                    y,
237                    w,
238                    h,
239                    transform,
240                    opacity,
241                );
242                self.draw_hachure_fill(
243                    pixmap,
244                    clip_path,
245                    &fill.color,
246                    fill.gap as f32,
247                    fill.angle as f32 + std::f32::consts::FRAC_PI_2,
248                    x,
249                    y,
250                    w,
251                    h,
252                    transform,
253                    opacity,
254                );
255            }
256        }
257    }
258
259    #[allow(clippy::too_many_arguments)]
260    fn draw_hachure_fill(
261        &self,
262        pixmap: &mut Pixmap,
263        clip_path: &Path,
264        color: &str,
265        gap: f32,
266        angle: f32,
267        x: f32,
268        y: f32,
269        w: f32,
270        h: f32,
271        transform: &Transform,
272        opacity: f32,
273    ) {
274        let Some(mut clip_mask) = Mask::new(pixmap.width(), pixmap.height()) else {
275            return;
276        };
277        clip_mask.fill_path(clip_path, FillRule::Winding, true, *transform);
278
279        let parsed_color = parse_color(color, opacity * HACHURE_ALPHA);
280        let mut paint = Paint::default();
281        paint.set_color(parsed_color);
282        paint.anti_alias = true;
283
284        let stroke = Stroke {
285            width: HACHURE_LINE_WIDTH,
286            line_cap: LineCap::Round,
287            ..Stroke::default()
288        };
289
290        let cx = (x + w / 2.0) as f64;
291        let cy = (y + h / 2.0) as f64;
292        for line in
293            geometry::generate_hachure_lines(cx, cy, w as f64, h as f64, gap as f64, angle as f64)
294        {
295            let mut pb = PathBuilder::new();
296            pb.move_to(line.x1 as f32, line.y1 as f32);
297            pb.line_to(line.x2 as f32, line.y2 as f32);
298            if let Some(line_path) = pb.finish() {
299                pixmap.stroke_path(&line_path, &paint, &stroke, *transform, Some(&clip_mask));
300            }
301        }
302    }
303
304    // ── Grid ────────────────────────────────────────────────────────
305
306    pub(super) fn draw_grid(&self, pixmap: &mut Pixmap, viewport: &crate::point::ViewState) {
307        let zoom = viewport.zoom as f32 * self.config.pixel_ratio;
308        let gs = GRID_SIZE * zoom;
309        if gs < GRID_MIN_SCREEN_PX {
310            return;
311        }
312
313        let w = pixmap.width() as f32;
314        let h = pixmap.height() as f32;
315        let off_x = (viewport.scroll_x as f32 * self.config.pixel_ratio) % gs;
316        let off_y = (viewport.scroll_y as f32 * self.config.pixel_ratio) % gs;
317
318        let color = Color::from_rgba(
319            GRID_R as f32 / 255.0,
320            GRID_G as f32 / 255.0,
321            GRID_B as f32 / 255.0,
322            GRID_ALPHA,
323        )
324        .unwrap_or(Color::TRANSPARENT);
325        let mut paint = Paint::default();
326        paint.set_color(color);
327        paint.anti_alias = false;
328
329        let stroke = Stroke {
330            width: 1.0,
331            ..Stroke::default()
332        };
333        let identity = Transform::identity();
334
335        let mut x = off_x;
336        while x < w {
337            let mut pb = PathBuilder::new();
338            pb.move_to(x, 0.0);
339            pb.line_to(x, h);
340            if let Some(path) = pb.finish() {
341                pixmap.stroke_path(&path, &paint, &stroke, identity, None);
342            }
343            x += gs;
344        }
345
346        let mut y = off_y;
347        while y < h {
348            let mut pb = PathBuilder::new();
349            pb.move_to(0.0, y);
350            pb.line_to(w, y);
351            if let Some(path) = pb.finish() {
352                pixmap.stroke_path(&path, &paint, &stroke, identity, None);
353            }
354            y += gs;
355        }
356    }
357}
358
359impl super::Renderer {
360    /// Draw a snap indicator dot at the given world coordinates.
361    pub fn draw_snap_indicator(
362        &self,
363        pixmap: &mut Pixmap,
364        viewport: &crate::point::ViewState,
365        wx: f64,
366        wy: f64,
367    ) {
368        let vt = super::viewport_transform(viewport, self.config.pixel_ratio);
369        let scale = viewport.zoom as f32 * self.config.pixel_ratio;
370        let radius = super::HANDLE_RADIUS / scale;
371
372        let accent = Color::from_rgba8(super::ACCENT_R, super::ACCENT_G, super::ACCENT_B, 255);
373        let mut fill_paint = Paint::default();
374        fill_paint.set_color(accent);
375        fill_paint.anti_alias = true;
376
377        if let Some(path) = super::path::build_circle_path(wx as f32, wy as f32, radius) {
378            pixmap.fill_path(&path, &fill_paint, FillRule::Winding, vt, None);
379        }
380    }
381}
382
383fn draw_arrowhead_path(
384    pixmap: &mut Pixmap,
385    ah: &geometry::ArrowheadPoints,
386    paint: &Paint,
387    stroke: &Stroke,
388    transform: &Transform,
389) {
390    let mut pb = PathBuilder::new();
391    pb.move_to(ah.tip_x as f32, ah.tip_y as f32);
392    pb.line_to(ah.left_x as f32, ah.left_y as f32);
393    pb.line_to(ah.right_x as f32, ah.right_y as f32);
394    pb.close();
395    if let Some(path) = pb.finish() {
396        pixmap.fill_path(&path, paint, FillRule::Winding, *transform, None);
397        pixmap.stroke_path(&path, paint, stroke, *transform, None);
398    }
399}