1use super::{font, PmiGeometry, PmiPlane};
8use crate::feature_pipeline::pmi::annotations::angle::rotate;
9use crate::feature_pipeline::pmi::resolve::{a3, perpendicular_in_plane, v3};
10use crate::Vec3;
11
12#[derive(Debug, Clone, Copy)]
14pub struct LayoutStyle {
15 pub arrow: f64,
17 pub text_height: f64,
19 pub view_dir: [f64; 3],
22 pub view_up: [f64; 3],
24 pub plane: Option<PmiPlane>,
28}
29
30#[derive(Debug, Clone, Default, PartialEq)]
32pub struct Presentation {
33 pub polylines: Vec<Vec<[f64; 3]>>,
36 pub arrows: Vec<[[f64; 3]; 3]>,
38 pub frames: Vec<Vec<[f64; 3]>>,
41 pub text: String,
43 pub text_anchor: [f64; 3],
44 pub texts: Vec<TextRun>,
48}
49
50#[derive(Debug, Clone, PartialEq)]
53pub struct TextRun {
54 pub text: String,
55 pub anchor: [f64; 3],
56 pub right: [f64; 3],
57 pub up: [f64; 3],
58 pub height: f64,
59}
60
61impl TextRun {
62 pub fn strokes(&self) -> Vec<Vec<[f64; 3]>> {
64 let width = font::text_width(&self.text, self.height);
65 let origin = v3(self.anchor)
66 .sub(v3(self.right).scale(width * 0.5))
67 .sub(v3(self.up).scale(self.height * 0.5));
68 font::stroke_text(&self.text, a3(origin), self.right, self.up, self.height)
69 }
70}
71
72fn unit_or(v: Vec3, fallback: Vec3) -> Vec3 {
73 if v.length() < 1e-12 {
74 fallback
75 } else {
76 v.normalized().unwrap_or(fallback)
77 }
78}
79
80fn arrow_width_dir(dir: Vec3, style: &LayoutStyle) -> Vec3 {
82 let view = v3(style.view_dir);
83 let w = view.cross(dir);
84 if w.length() < 1e-6 {
85 dir.perpendicular().unwrap_or(Vec3::new(0.0, 1.0, 0.0))
86 } else {
87 w.normalized().unwrap_or(Vec3::new(0.0, 1.0, 0.0))
88 }
89}
90
91fn arrowhead(tip: Vec3, dir: Vec3, style: &LayoutStyle) -> [[f64; 3]; 3] {
94 let dir = unit_or(dir, Vec3::new(1.0, 0.0, 0.0));
95 let w = arrow_width_dir(dir, style);
96 let base = tip.sub(dir.scale(style.arrow));
97 let half = style.arrow * 0.35;
98 [a3(tip), a3(base.add(w.scale(half))), a3(base.sub(w.scale(half)))]
99}
100
101fn dot(center: Vec3, style: &LayoutStyle) -> Vec<[f64; 3]> {
104 let view = v3(style.view_dir);
105 let up = perpendicular_in_plane(view, v3(style.view_up));
106 let right = view.cross(up);
107 let r = style.arrow * 0.25;
108 let mut out = Vec::with_capacity(9);
109 for k in 0..=8 {
110 let angle = k as f64 * std::f64::consts::TAU / 8.0;
111 out.push(a3(center.add(right.scale(r * angle.cos())).add(up.scale(r * angle.sin()))));
112 }
113 out
114}
115
116fn frame_rect(left: Vec3, width: f64, height: f64, right: Vec3, up: Vec3) -> Vec<[f64; 3]> {
119 let h = up.scale(height * 0.5);
120 let w = right.scale(width);
121 vec![
122 a3(left.sub(h)),
123 a3(left.add(w).sub(h)),
124 a3(left.add(w).add(h)),
125 a3(left.add(h)),
126 a3(left.sub(h)),
127 ]
128}
129
130fn center_cross(center: Vec3, u: Vec3, v: Vec3, radius: f64, extension: f64) -> Vec<Vec<[f64; 3]>> {
134 let arm = radius * 0.25;
135 let gap = radius * 0.12;
136 let reach = radius + extension;
137 let mut out = vec![
138 vec![a3(center.sub(u.scale(arm))), a3(center.add(u.scale(arm)))],
139 vec![a3(center.sub(v.scale(arm))), a3(center.add(v.scale(arm)))],
140 ];
141 for dir in [u, u.scale(-1.0), v, v.scale(-1.0)] {
142 out.push(vec![a3(center.add(dir.scale(arm + gap))), a3(center.add(dir.scale(reach)))]);
143 }
144 out
145}
146
147pub(crate) fn chain_line(a: Vec3, b: Vec3, unit: f64) -> Vec<Vec<[f64; 3]>> {
152 let span = b.sub(a);
153 let length = span.length();
154 if length < 1e-12 {
155 return Vec::new();
156 }
157 let d = span.scale(1.0 / length);
158 let long = unit * 6.0;
159 let short = unit * 1.2;
160 let gap = unit * 0.6;
161 let cycles = if unit > 0.0 { ((length - long) / (long + short + 2.0 * gap)).floor() } else { 0.0 };
162 if !(cycles >= 1.0) {
163 return vec![vec![a3(a), a3(b)]];
164 }
165 let cycles = cycles as usize;
166 let long = (length - cycles as f64 * (short + 2.0 * gap)) / (cycles + 1) as f64;
167 let mut out = Vec::with_capacity(2 * cycles + 1);
168 let mut at = 0.0;
169 for k in 0..=cycles {
170 out.push(vec![a3(a.add(d.scale(at))), a3(a.add(d.scale((at + long).min(length))))]);
171 at += long;
172 if k < cycles {
173 at += gap;
174 out.push(vec![a3(a.add(d.scale(at))), a3(a.add(d.scale(at + short)))]);
175 at += short + gap;
176 }
177 }
178 out
179}
180
181pub fn present(geometry: &PmiGeometry, label: [f64; 3], text: &str, style: &LayoutStyle) -> Presentation {
183 let mut out = Presentation {
184 text: text.to_string(),
185 text_anchor: label,
186 ..Default::default()
187 };
188 let plane = style.plane;
192 let project = |p: Vec3| -> Vec3 {
193 match &plane {
194 Some(plane) => v3(plane.project(a3(p))),
195 None => p,
196 }
197 };
198 let in_plane;
199 let style = match &plane {
200 Some(plane) => {
201 let n = v3(plane.normal);
202 in_plane = LayoutStyle {
203 view_dir: a3(n.scale(-1.0)),
204 view_up: plane.y_axis(),
205 ..*style
206 };
207 &in_plane
208 }
209 None => style,
210 };
211 let label = project(v3(label));
212 out.text_anchor = a3(label);
213 let view = v3(style.view_dir);
214 let up = perpendicular_in_plane(view, v3(style.view_up));
215 let right = view.cross(up);
216 match geometry {
217 PmiGeometry::None | PmiGeometry::Note { .. } => {}
218 PmiGeometry::Linear { a, b, .. } => {
219 let a = project(v3(*a));
220 let b = project(v3(*b));
221 let span = b.sub(a);
222 let length = span.length();
223 if length < 1e-9 {
224 out.polylines.push(vec![a3(label), a3(a)]);
225 return out;
226 }
227 let d = span.scale(1.0 / length);
228 let rel = label.sub(a);
229 let t = rel.dot(d);
230 let off = rel.sub(d.scale(t));
231 let a_line = a.add(off);
232 let b_line = b.add(off);
233 let lo = t.min(0.0);
235 let hi = t.max(length);
236 out.polylines.push(vec![a3(a.add(off).add(d.scale(lo))), a3(a.add(off).add(d.scale(hi)))]);
237 if off.length() > 1e-9 {
238 let n = off.normalized().unwrap_or(Vec3::new(0.0, 1.0, 0.0));
239 let overshoot = n.scale(style.arrow * 0.6);
240 out.polylines.push(vec![a3(a), a3(a_line.add(overshoot))]);
241 out.polylines.push(vec![a3(b), a3(b_line.add(overshoot))]);
242 }
243 if length > style.arrow * 2.5 {
244 out.arrows.push(arrowhead(a_line, d.scale(-1.0), style));
245 out.arrows.push(arrowhead(b_line, d, style));
246 } else {
247 out.arrows.push(arrowhead(a_line, d, style));
249 out.arrows.push(arrowhead(b_line, d.scale(-1.0), style));
250 out.polylines.push(vec![a3(a_line.sub(d.scale(style.arrow * 1.5))), a3(a_line)]);
251 out.polylines.push(vec![a3(b_line), a3(b_line.add(d.scale(style.arrow * 1.5)))]);
252 }
253 }
254 PmiGeometry::Radial {
255 center,
256 axis,
257 radius,
258 diameter,
259 sphere,
260 } => {
261 let center = project(v3(*center));
262 let axis = if *sphere || plane.is_some() { view } else { v3(*axis) };
263 let u = perpendicular_in_plane(axis, label.sub(center));
264 let p = center.add(u.scale(*radius));
265 let outside = label.sub(center).length() > *radius;
266 if *diameter {
267 let q = center.sub(u.scale(*radius));
268 out.polylines.push(vec![a3(q), a3(p)]);
269 out.arrows.push(arrowhead(p, u, style));
270 out.arrows.push(arrowhead(q, u.scale(-1.0), style));
271 if outside {
272 out.polylines.push(vec![a3(p), a3(label)]);
273 }
274 } else {
275 if outside {
276 out.polylines.push(vec![a3(label), a3(p)]);
277 out.arrows.push(arrowhead(p, u.scale(-1.0), style));
278 } else {
279 out.polylines.push(vec![a3(center), a3(p)]);
280 out.arrows.push(arrowhead(p, u, style));
281 }
282 }
283 }
284 PmiGeometry::Angular {
285 vertex,
286 dir_a,
287 dir_b,
288 axis,
289 degrees,
290 } => {
291 let vertex = project(v3(*vertex));
292 let axis = v3(*axis);
293 let dir_a = v3(*dir_a);
294 let dir_b = v3(*dir_b);
295 let rel = label.sub(vertex);
296 let planar = rel.sub(axis.scale(rel.dot(axis)));
297 let rho = planar.length().max(style.arrow * 2.0);
298 let steps = ((degrees / 5.0).ceil() as usize).max(8);
299 let mut arc = Vec::with_capacity(steps + 1);
300 for k in 0..=steps {
301 let angle = degrees.to_radians() * k as f64 / steps as f64;
302 arc.push(a3(vertex.add(rotate(dir_a, axis, angle).scale(rho))));
303 }
304 let start = v3(arc[0]);
305 let end = v3(arc[steps]);
306 out.polylines.push(arc);
307 let overshoot = rho + style.arrow * 0.6;
308 out.polylines.push(vec![a3(vertex), a3(vertex.add(dir_a.scale(overshoot)))]);
309 out.polylines.push(vec![a3(vertex), a3(vertex.add(dir_b.scale(overshoot)))]);
310 let tangent_start = axis.cross(dir_a);
311 let tangent_end = axis.cross(dir_b);
312 out.arrows.push(arrowhead(start, tangent_start.scale(-1.0), style));
313 out.arrows.push(arrowhead(end, tangent_end, style));
314 }
315 PmiGeometry::Leader { targets, dot: use_dot, balloon, .. } => {
316 let radius = style.text_height * (text.chars().count() as f64 * font::ADVANCE * 0.5 + 0.7).max(1.0);
317 if *balloon {
318 out.frames.push((0..=48).map(|i| {
319 let angle = std::f64::consts::TAU * i as f64 / 48.0;
320 a3(label.add(right.scale(radius * angle.cos())).add(up.scale(radius * angle.sin())))
321 }).collect());
322 }
323 for target in targets {
324 let target = v3(*target);
325 let start = if *balloon { label.add(unit_or(target.sub(label), right).scale(radius)) } else { label };
326 out.polylines.push(vec![a3(start), a3(target)]);
327 if *use_dot {
328 out.polylines.push(dot(target, style));
329 } else {
330 out.arrows.push(arrowhead(target, target.sub(label), style));
331 }
332 }
333 }
334 PmiGeometry::Hole { anchor, .. } => {
335 let anchor = v3(*anchor);
336 out.polylines.push(vec![a3(label), a3(anchor)]);
337 out.arrows.push(arrowhead(anchor, anchor.sub(label), style));
338 }
339 PmiGeometry::Explode {
340 center,
341 translate,
342 trace,
343 ..
344 } => {
345 if *trace {
349 let seat = v3(*center);
350 let exploded = seat.add(v3(*translate));
351 let span = seat.sub(exploded);
352 if span.length() > 1e-9 {
353 out.polylines.push(vec![a3(exploded), a3(seat)]);
354 out.arrows.push(arrowhead(seat, span, style));
355 }
356 }
357 }
358 PmiGeometry::Hide { .. } => {}
360 PmiGeometry::Datum { anchor, letter, .. } => {
361 let anchor = v3(*anchor);
362 let dir = unit_or(label.sub(anchor), up);
363 let apex = anchor.add(dir.scale(style.arrow));
365 let w = arrow_width_dir(dir, style);
366 let half = style.arrow * 0.5;
367 out.arrows.push([a3(apex), a3(anchor.add(w.scale(half))), a3(anchor.sub(w.scale(half)))]);
368 out.polylines.push(vec![a3(apex), a3(label)]);
369 let side = style.text_height * 1.8;
370 let width = side.max(style.text_height * font::ADVANCE * letter.chars().count() as f64 + style.text_height);
371 out.frames.push(frame_rect(label.sub(right.scale(width * 0.5)), width, side, right, up));
372 out.texts.push(TextRun {
373 text: letter.clone(),
374 anchor: a3(label),
375 right: a3(right),
376 up: a3(up),
377 height: style.text_height,
378 });
379 }
380 PmiGeometry::CenterMark { marks, extension } => {
381 for mark in marks {
382 let axis = v3(mark.axis);
383 let mut center = v3(mark.center);
386 if mark.depth > 0.0 && axis.dot(view) > 0.0 {
387 center = center.sub(axis.scale(mark.depth));
388 }
389 let axis = if axis.length() < 1e-12 { Vec3::new(0.0, 0.0, 1.0) } else { axis };
397 let x_dir = v3(mark.x_dir);
398 let x_dir = if x_dir.length() > 1e-12 {
399 x_dir
400 } else if axis.cross(Vec3::new(1.0, 0.0, 0.0)).length() > 1e-6 {
401 Vec3::new(1.0, 0.0, 0.0)
402 } else {
403 Vec3::new(0.0, 1.0, 0.0)
404 };
405 let u = perpendicular_in_plane(axis, x_dir);
406 let v = unit_or(axis.cross(u), up);
407 out.polylines.extend(
408 center_cross(center, u, v, mark.radius, *extension)
409 .into_iter()
410 .map(|line| line.into_iter().map(|p| a3(project(v3(p)))).collect::<Vec<_>>()),
411 );
412 }
413 }
414 PmiGeometry::CenterLine { a, b } => {
415 out.polylines.extend(chain_line(project(v3(*a)), project(v3(*b)), style.arrow));
416 }
417 PmiGeometry::Fcf { anchor, frame, .. } => {
418 let anchor = v3(*anchor);
419 out.polylines.push(vec![a3(label), a3(anchor)]);
420 out.arrows.push(arrowhead(anchor, anchor.sub(label), style));
421 let height = style.text_height * 1.8;
422 let char_w = style.text_height * font::ADVANCE;
423 let mut cells: Vec<(f64, &str)> = vec![(height, frame.symbol.as_str())];
424 cells.push((char_w * frame.zone.chars().count() as f64 + style.text_height, frame.zone.as_str()));
425 for datum in &frame.datums {
426 cells.push((char_w * datum.chars().count() as f64 + style.text_height, datum.as_str()));
427 }
428 let total: f64 = cells.iter().map(|(w, _)| w).sum();
429 let mut left = label.sub(right.scale(total * 0.5));
430 for (width, cell_text) in cells {
431 out.frames.push(frame_rect(left, width, height, right, up));
432 out.texts.push(TextRun {
433 text: cell_text.to_string(),
434 anchor: a3(left.add(right.scale(width * 0.5))),
435 right: a3(right),
436 up: a3(up),
437 height: style.text_height,
438 });
439 left = left.add(right.scale(width));
440 }
441 }
442 }
443 let display_only = matches!(geometry, PmiGeometry::Explode { .. } | PmiGeometry::Hide { .. });
449 if out.texts.is_empty() && !text.is_empty() && !display_only {
450 out.texts.push(TextRun {
451 text: text.to_string(),
452 anchor: a3(label),
453 right: a3(right),
454 up: a3(up),
455 height: style.text_height,
456 });
457 }
458 out
459}