1use bytemuck::{Pod, Zeroable};
7use dotloom_geometry::{Aabb, FlattenTolerance, Point, Shape, Text};
8use dotloom_scene::{Primitive, SceneItem, Stroke, flags};
9use lyon_tessellation::math::point as lpoint;
10use lyon_tessellation::path::Path;
11use lyon_tessellation::{
12 BuffersBuilder, FillOptions, FillRule, FillTessellator, FillVertex as LyonVertex, VertexBuffers,
13};
14
15use crate::color::{Rgba, Theme, premul_bytes, with_alpha};
16use crate::text::TextSystem;
17
18#[repr(C)]
20#[derive(Debug, Clone, Copy, PartialEq, Pod, Zeroable)]
21pub struct LineInstance {
22 pub p0: [f32; 2],
24 pub p1: [f32; 2],
26 pub color: u32,
28 pub width: f32,
30 pub dist0: f32,
32 pub dash: [f32; 4],
34}
35
36#[repr(C)]
38#[derive(Debug, Clone, Copy, PartialEq, Pod, Zeroable)]
39pub struct FillVertex {
40 pub pos: [f32; 2],
42 pub color: u32,
44}
45
46#[repr(C)]
48#[derive(Debug, Clone, Copy, PartialEq, Pod, Zeroable)]
49pub struct GlyphInstance {
50 pub origin: [f32; 2],
52 pub axis_x: [f32; 2],
54 pub axis_y: [f32; 2],
56 pub uv: [f32; 4],
58 pub color: u32,
60 pub height: f32,
62}
63
64#[derive(Debug, Clone, Default)]
66pub struct Mesh {
67 pub origin: [f64; 2],
69 pub lines: Vec<LineInstance>,
71 pub fill_vertices: Vec<FillVertex>,
73 pub fill_indices: Vec<u32>,
75 pub glyphs: Vec<GlyphInstance>,
77}
78
79impl Mesh {
80 #[must_use]
82 pub fn at(origin: [f64; 2]) -> Self {
83 Self { origin, ..Self::default() }
84 }
85
86 #[must_use]
88 pub fn is_empty(&self) -> bool {
89 self.lines.is_empty() && self.fill_indices.is_empty() && self.glyphs.is_empty()
90 }
91
92 fn local(&self, p: Point) -> [f32; 2] {
93 [(p.x - self.origin[0]) as f32, (p.y - self.origin[1]) as f32]
94 }
95
96 pub fn polyline(&mut self, points: &[Point], closed: bool, color: u32, width: f32, dash: [f32; 4]) {
98 let pts: Vec<Point> = points.iter().copied().filter(|p| p.is_finite()).collect();
99 match pts.len() {
100 0 => {}
101 1 => {
102 let p = self.local(pts[0]);
103 self.lines.push(LineInstance {
104 p0: p,
105 p1: p,
106 color,
107 width: width.max(3.0),
108 dist0: 0.0,
109 dash: [0.0; 4],
110 });
111 }
112 n => {
113 let mut dist = 0.0f64;
114 let segs = if closed { n } else { n - 1 };
115 for i in 0..segs {
116 let a = pts[i];
117 let b = pts[(i + 1) % n];
118 self.lines.push(LineInstance {
119 p0: self.local(a),
120 p1: self.local(b),
121 color,
122 width,
123 dist0: dist as f32,
124 dash,
125 });
126 dist += a.distance(b);
127 }
128 }
129 }
130 }
131
132 pub fn fill_rings(&mut self, rings: &[Vec<Point>], color: u32) {
134 let mut b = Path::builder();
135 let mut any = false;
136 for ring in rings {
137 let pts: Vec<[f32; 2]> = ring.iter().filter(|p| p.is_finite()).map(|&p| self.local(p)).collect();
138 if pts.len() < 3 {
139 continue;
140 }
141 b.begin(lpoint(pts[0][0], pts[0][1]));
142 for p in &pts[1..] {
143 b.line_to(lpoint(p[0], p[1]));
144 }
145 b.end(true);
146 any = true;
147 }
148 if !any {
149 return;
150 }
151 let path = b.build();
152 let mut buffers: VertexBuffers<FillVertex, u32> = VertexBuffers::new();
153 let options = FillOptions::tolerance(0.01).with_fill_rule(FillRule::EvenOdd);
154 let ok = FillTessellator::new()
155 .tessellate_path(
156 &path,
157 &options,
158 &mut BuffersBuilder::new(&mut buffers, |v: LyonVertex| FillVertex {
159 pos: v.position().to_array(),
160 color,
161 }),
162 )
163 .is_ok();
164 if !ok {
165 return;
166 }
167 let base = self.fill_vertices.len() as u32;
168 self.fill_vertices.extend_from_slice(&buffers.vertices);
169 self.fill_indices.extend(buffers.indices.iter().map(|i| i + base));
170 }
171
172 pub fn triangle(&mut self, a: Point, b: Point, c: Point, color: u32) {
174 if !(a.is_finite() && b.is_finite() && c.is_finite()) {
175 return;
176 }
177 let base = self.fill_vertices.len() as u32;
178 for p in [a, b, c] {
179 self.fill_vertices.push(FillVertex { pos: self.local(p), color });
180 }
181 self.fill_indices.extend_from_slice(&[base, base + 1, base + 2]);
182 }
183
184 pub fn text(&mut self, ts: &mut TextSystem, t: &Text, color: u32) -> bool {
187 if !t.position.is_finite() {
188 return true;
189 }
190 let Some(glyphs) = ts.layout(t) else { return false };
191 let (sin, cos) = if t.rotation.is_finite() { dotloom_geometry::math::sin_cos(t.rotation) } else { (0.0, 1.0) };
192 let rot = |x: f64, y: f64| Point::new(t.position.x + x * cos - y * sin, t.position.y + x * sin + y * cos);
193 for g in glyphs {
194 let o = rot(g.x0, g.y0);
195 let ox = rot(g.x1, g.y0);
196 let oy = rot(g.x0, g.y1);
197 let origin = self.local(o);
198 self.glyphs.push(GlyphInstance {
199 origin,
200 axis_x: [(ox.x - o.x) as f32, (ox.y - o.y) as f32],
201 axis_y: [(oy.x - o.x) as f32, (oy.y - o.y) as f32],
202 uv: g.uv,
203 color,
204 height: t.height as f32,
205 });
206 }
207 true
208 }
209}
210
211#[derive(Debug, Clone, Copy)]
213pub(crate) struct Look {
214 pub flags: u8,
215}
216
217#[must_use]
219pub fn dash4(dash: &[f32]) -> [f32; 4] {
220 let vals: Vec<f32> = dash.iter().copied().filter(|v| v.is_finite() && *v >= 0.0).collect();
221 if vals.is_empty() || vals.iter().all(|v| *v == 0.0) {
222 return [0.0; 4];
223 }
224 let mut out = [0.0f32; 4];
225 let src: Vec<f32> = if vals.len() % 2 == 1 { vals.iter().chain(vals.iter()).copied().collect() } else { vals };
226 for (o, v) in out.iter_mut().zip(src.iter()) {
227 *o = *v;
228 }
229 if out[0] <= 0.0 {
230 out[0] = 0.01;
232 }
233 out
234}
235
236pub(crate) fn effective_tolerance(lod_tol: f64, bbox: Aabb) -> f64 {
239 let size = if bbox.is_empty() { 0.0 } else { bbox.width().max(bbox.height()) };
240 let floor = if size.is_finite() { size * 1e-5 } else { 0.0 };
241 lod_tol.max(floor).max(FlattenTolerance::MIN)
242}
243
244pub(crate) fn shape_has_curves(s: &Shape) -> bool {
246 match s {
247 Shape::Circle(_) | Shape::Arc(_) | Shape::Path(_) => true,
248 Shape::Polyline(p) => p.bulges.iter().any(|b| *b != 0.0),
249 _ => false,
250 }
251}
252
253pub(crate) fn item_has_curves(item: &SceneItem) -> bool {
255 item.prims.iter().any(|p| matches!(p, Primitive::Shape { shape, .. } if shape_has_curves(shape)))
256}
257
258pub(crate) fn item_has_text(item: &SceneItem) -> bool {
260 item.prims.iter().any(|p| matches!(p, Primitive::Text { .. } | Primitive::Shape { shape: Shape::Text(_), .. }))
261}
262
263fn origin_of(item: &SceneItem) -> [f64; 2] {
264 let b = item.bbox;
265 if !b.is_empty() && b.min.is_finite() && b.max.is_finite() {
266 let c = b.center();
267 return [c.x, c.y];
268 }
269 for p in &item.prims {
270 let at = match p {
271 Primitive::Shape { shape, .. } => {
272 shape.flatten(FlattenTolerance(1.0)).first().and_then(|f| f.points.first().copied())
273 }
274 Primitive::Text { text, .. } => Some(text.position),
275 Primitive::Arrow { tip, .. } => Some(*tip),
276 };
277 if let Some(at) = at.filter(|p| p.is_finite()) {
278 return [at.x, at.y];
279 }
280 }
281 [0.0, 0.0]
282}
283
284pub(crate) fn tessellate_item(
286 item: &SceneItem,
287 look: Look,
288 lod_tol: f64,
289 theme: &Theme,
290 ts: &mut TextSystem,
291) -> Option<Mesh> {
292 let mut m = Mesh::at(origin_of(item));
293 let f = look.flags;
294 let mut alpha = 1.0f32;
295 if f & flags::PREVIEW != 0 {
296 alpha *= 0.8;
297 }
298 if f & flags::LOCKED != 0 {
299 alpha *= 0.6;
300 }
301 if f & flags::READONLY != 0 {
302 alpha *= 0.6;
303 }
304 let emphasis = |c: Rgba| -> Rgba {
305 if f & flags::SELECTED != 0 {
306 theme.selection
307 } else if f & flags::PROBLEM != 0 {
308 theme.problem
309 } else if f & flags::HOVER != 0 {
310 theme.hover
311 } else {
312 theme.resolve(c)
313 }
314 };
315 let extra_width = if f & (flags::SELECTED | flags::HOVER) != 0 { 1.0 } else { 0.0 };
316 let readonly_dash = f & flags::READONLY != 0;
317 let tol = effective_tolerance(lod_tol, item.bbox);
318 for prim in &item.prims {
319 match prim {
320 Primitive::Shape { shape, stroke, fill } => {
321 if let Shape::Text(t) = shape {
322 let c = stroke.as_ref().map(|s| s.color).or(*fill).unwrap_or(0);
323 if !m.text(ts, t, premul_bytes(with_alpha(emphasis(c), alpha))) {
324 return None;
325 }
326 continue;
327 }
328 let flat = shape.flatten(FlattenTolerance(tol));
329 if let Some(fc) = fill
330 && shape.is_region()
331 {
332 let rings: Vec<Vec<Point>> = flat.iter().filter(|p| p.closed).map(|p| p.points.clone()).collect();
333 let mut fc = theme.resolve(*fc);
334 if f & flags::SELECTED != 0 {
335 fc = mix_toward(fc, theme.selection, 0.25);
336 }
337 m.fill_rings(&rings, premul_bytes(with_alpha(fc, alpha)));
338 }
339 let stroke = match stroke {
340 Some(s) => Some(s.clone()),
341 None if f & (flags::SELECTED | flags::HOVER | flags::PROBLEM) != 0 => {
343 Some(Stroke { color: 0, width: 1.0, dash: Vec::new() })
344 }
345 None => None,
346 };
347 if let Some(s) = stroke {
348 let color = premul_bytes(with_alpha(emphasis(s.color), alpha));
349 let width = if s.width.is_finite() { s.width.max(0.0) } else { 1.0 } + extra_width;
350 let dash = if readonly_dash && s.dash.is_empty() { dash4(&[6.0, 4.0]) } else { dash4(&s.dash) };
351 if let Shape::Point(p) = shape {
352 m.polyline(&[p.at], false, color, (width + 2.0).max(4.0), [0.0; 4]);
353 continue;
354 }
355 for fp in &flat {
356 m.polyline(&fp.points, fp.closed, color, width, dash);
357 }
358 }
359 }
360 Primitive::Text { text, color } => {
361 if !m.text(ts, text, premul_bytes(with_alpha(emphasis(*color), alpha))) {
362 return None;
363 }
364 }
365 Primitive::Arrow { tip, direction, size, color } => {
366 let Some(d) = direction.normalize() else { continue };
367 if !(size.is_finite() && *size > 0.0) {
368 continue;
369 }
370 let base = *tip - d * *size;
371 let n = dotloom_geometry::Vector::new(-d.y, d.x) * (*size * 0.22);
372 m.triangle(*tip, base + n, base - n, premul_bytes(with_alpha(emphasis(*color), alpha)));
373 }
374 }
375 }
376 Some(m)
377}
378
379fn mix_toward(a: Rgba, b: Rgba, t: f32) -> Rgba {
380 let (x, y) = (crate::color::unpack(a), crate::color::unpack(b));
381 crate::color::pack([
382 x[0] + (y[0] - x[0]) * t,
383 x[1] + (y[1] - x[1]) * t,
384 x[2] + (y[2] - x[2]) * t,
385 x[3].max(y[3] * 0.25),
386 ])
387}
388
389#[cfg(test)]
390mod tests {
391 use dotloom_geometry::{Circle, Rect, Segment};
392
393 use super::*;
394
395 fn item(prims: Vec<Primitive>) -> SceneItem {
396 let mut bbox = Aabb::EMPTY;
397 for p in &prims {
398 if let Primitive::Shape { shape, .. } = p {
399 bbox = bbox.union(shape.bbox());
400 }
401 }
402 SceneItem { id: 1, layer: 0, bbox, flags: 0, prims }
403 }
404
405 fn stroke() -> Option<Stroke> {
406 Some(Stroke { color: 0, width: 1.0, dash: Vec::new() })
407 }
408
409 #[test]
410 fn line_becomes_one_instance_relative_to_origin() {
411 let it = item(vec![Primitive::Shape {
412 shape: Shape::Line(Segment::new(Point::new(1.0e7, 5.0), Point::new(1.0e7 + 10.0, 5.0))),
413 stroke: stroke(),
414 fill: None,
415 }]);
416 let mut ts = TextSystem::with_default_font().unwrap();
417 let m = tessellate_item(&it, Look { flags: 0 }, 0.01, &Theme::light(), &mut ts).unwrap();
418 assert_eq!(m.lines.len(), 1);
419 assert_eq!(m.lines[0].p0, [-5.0, 0.0]);
420 assert_eq!(m.lines[0].p1, [5.0, 0.0]);
421 }
422
423 #[test]
424 fn circle_density_follows_lod() {
425 let it = item(vec![Primitive::Shape {
426 shape: Shape::Circle(Circle::new(Point::new(0.0, 0.0), 100.0).unwrap()),
427 stroke: stroke(),
428 fill: Some(0xff00_00ff),
429 }]);
430 let mut ts = TextSystem::with_default_font().unwrap();
431 let coarse = tessellate_item(&it, Look { flags: 0 }, 1.0, &Theme::light(), &mut ts).unwrap();
432 let fine = tessellate_item(&it, Look { flags: 0 }, 0.01, &Theme::light(), &mut ts).unwrap();
433 assert!(fine.lines.len() > coarse.lines.len() * 5);
434 assert!(!fine.fill_indices.is_empty());
435 let huge = item(vec![Primitive::Shape {
437 shape: Shape::Circle(Circle::new(Point::new(0.0, 0.0), 1.0e6).unwrap()),
438 stroke: stroke(),
439 fill: None,
440 }]);
441 let h = tessellate_item(&huge, Look { flags: 0 }, 1e-6, &Theme::light(), &mut ts).unwrap();
442 assert!(h.lines.len() < 2000, "{}", h.lines.len());
443 }
444
445 #[test]
446 fn polygon_with_hole_and_selection_outline() {
447 let outer = vec![Point::new(0.0, 0.0), Point::new(10.0, 0.0), Point::new(10.0, 10.0), Point::new(0.0, 10.0)];
448 let hole = vec![Point::new(3.0, 3.0), Point::new(7.0, 3.0), Point::new(7.0, 7.0), Point::new(3.0, 7.0)];
449 let mut it = item(vec![Primitive::Shape {
450 shape: Shape::Polygon(dotloom_geometry::Polygon { outer, holes: vec![hole] }),
451 stroke: None,
452 fill: Some(0x00ff_00ff),
453 }]);
454 let mut ts = TextSystem::with_default_font().unwrap();
455 let m = tessellate_item(&it, Look { flags: 0 }, 0.1, &Theme::light(), &mut ts).unwrap();
456 assert_eq!(m.fill_indices.len(), 8 * 3);
458 assert!(m.lines.is_empty());
459 it.flags = flags::SELECTED;
460 let s = tessellate_item(&it, Look { flags: it.flags }, 0.1, &Theme::light(), &mut ts).unwrap();
461 assert_eq!(s.lines.len(), 8);
462 assert_eq!(s.lines[0].color, premul_bytes(Theme::light().selection));
463 }
464
465 #[test]
466 fn text_arrow_point_and_bad_input() {
467 let t = Text {
468 position: Point::new(0.0, 0.0),
469 content: "Ölçü 1200".into(),
470 height: 5.0,
471 rotation: core::f64::consts::FRAC_PI_2,
472 halign: Default::default(),
473 valign: Default::default(),
474 };
475 let it = item(vec![
476 Primitive::Text { text: t, color: 0 },
477 Primitive::Arrow {
478 tip: Point::new(0.0, 0.0),
479 direction: dotloom_geometry::Vector::new(1.0, 0.0),
480 size: 3.0,
481 color: 0,
482 },
483 Primitive::Shape {
484 shape: Shape::Point(dotloom_geometry::PointShape { at: Point::new(1.0, 1.0) }),
485 stroke: stroke(),
486 fill: None,
487 },
488 Primitive::Shape {
489 shape: Shape::Rect(Rect { origin: Point::new(f64::NAN, 0.0), width: 1.0, height: 1.0 }),
490 stroke: stroke(),
491 fill: Some(1),
492 },
493 ]);
494 let mut ts = TextSystem::with_default_font().unwrap();
495 let m = tessellate_item(&it, Look { flags: 0 }, 0.1, &Theme::light(), &mut ts).unwrap();
496 assert_eq!(m.glyphs.len(), 8); assert!(m.glyphs[0].axis_x[0].abs() < 1e-5 && m.glyphs[0].axis_x[1] > 0.0);
499 assert_eq!(m.fill_indices.len(), 3);
500 assert_eq!(m.lines.len(), 1);
501 assert!(m.lines[0].width >= 4.0);
502 }
503
504 #[test]
505 fn dash_normalization() {
506 assert_eq!(dash4(&[]), [0.0; 4]);
507 assert_eq!(dash4(&[4.0, 2.0]), [4.0, 2.0, 0.0, 0.0]);
508 assert_eq!(dash4(&[3.0]), [3.0, 3.0, 0.0, 0.0]);
509 assert_eq!(dash4(&[5.0, 1.0, 1.0]), [5.0, 1.0, 1.0, 5.0]);
510 assert_eq!(dash4(&[0.0, 0.0]), [0.0; 4]);
511 }
512}