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
130pub fn present(geometry: &PmiGeometry, label: [f64; 3], text: &str, style: &LayoutStyle) -> Presentation {
132 let mut out = Presentation {
133 text: text.to_string(),
134 text_anchor: label,
135 ..Default::default()
136 };
137 let plane = style.plane;
141 let project = |p: Vec3| -> Vec3 {
142 match &plane {
143 Some(plane) => v3(plane.project(a3(p))),
144 None => p,
145 }
146 };
147 let in_plane;
148 let style = match &plane {
149 Some(plane) => {
150 let n = v3(plane.normal);
151 in_plane = LayoutStyle {
152 view_dir: a3(n.scale(-1.0)),
153 view_up: plane.y_axis(),
154 ..*style
155 };
156 &in_plane
157 }
158 None => style,
159 };
160 let label = project(v3(label));
161 out.text_anchor = a3(label);
162 let view = v3(style.view_dir);
163 let up = perpendicular_in_plane(view, v3(style.view_up));
164 let right = view.cross(up);
165 match geometry {
166 PmiGeometry::None | PmiGeometry::Note { .. } => {}
167 PmiGeometry::Linear { a, b, .. } => {
168 let a = project(v3(*a));
169 let b = project(v3(*b));
170 let span = b.sub(a);
171 let length = span.length();
172 if length < 1e-9 {
173 out.polylines.push(vec![a3(label), a3(a)]);
174 return out;
175 }
176 let d = span.scale(1.0 / length);
177 let rel = label.sub(a);
178 let t = rel.dot(d);
179 let off = rel.sub(d.scale(t));
180 let a_line = a.add(off);
181 let b_line = b.add(off);
182 let lo = t.min(0.0);
184 let hi = t.max(length);
185 out.polylines.push(vec![a3(a.add(off).add(d.scale(lo))), a3(a.add(off).add(d.scale(hi)))]);
186 if off.length() > 1e-9 {
187 let n = off.normalized().unwrap_or(Vec3::new(0.0, 1.0, 0.0));
188 let overshoot = n.scale(style.arrow * 0.6);
189 out.polylines.push(vec![a3(a), a3(a_line.add(overshoot))]);
190 out.polylines.push(vec![a3(b), a3(b_line.add(overshoot))]);
191 }
192 if length > style.arrow * 2.5 {
193 out.arrows.push(arrowhead(a_line, d.scale(-1.0), style));
194 out.arrows.push(arrowhead(b_line, d, style));
195 } else {
196 out.arrows.push(arrowhead(a_line, d, style));
198 out.arrows.push(arrowhead(b_line, d.scale(-1.0), style));
199 out.polylines.push(vec![a3(a_line.sub(d.scale(style.arrow * 1.5))), a3(a_line)]);
200 out.polylines.push(vec![a3(b_line), a3(b_line.add(d.scale(style.arrow * 1.5)))]);
201 }
202 }
203 PmiGeometry::Radial {
204 center,
205 axis,
206 radius,
207 diameter,
208 sphere,
209 } => {
210 let center = project(v3(*center));
211 let axis = if *sphere || plane.is_some() { view } else { v3(*axis) };
212 let u = perpendicular_in_plane(axis, label.sub(center));
213 let p = center.add(u.scale(*radius));
214 let outside = label.sub(center).length() > *radius;
215 if *diameter {
216 let q = center.sub(u.scale(*radius));
217 out.polylines.push(vec![a3(q), a3(p)]);
218 out.arrows.push(arrowhead(p, u, style));
219 out.arrows.push(arrowhead(q, u.scale(-1.0), style));
220 if outside {
221 out.polylines.push(vec![a3(p), a3(label)]);
222 }
223 } else {
224 if outside {
225 out.polylines.push(vec![a3(label), a3(p)]);
226 out.arrows.push(arrowhead(p, u.scale(-1.0), style));
227 } else {
228 out.polylines.push(vec![a3(center), a3(p)]);
229 out.arrows.push(arrowhead(p, u, style));
230 }
231 }
232 }
233 PmiGeometry::Angular {
234 vertex,
235 dir_a,
236 dir_b,
237 axis,
238 degrees,
239 } => {
240 let vertex = project(v3(*vertex));
241 let axis = v3(*axis);
242 let dir_a = v3(*dir_a);
243 let dir_b = v3(*dir_b);
244 let rel = label.sub(vertex);
245 let planar = rel.sub(axis.scale(rel.dot(axis)));
246 let rho = planar.length().max(style.arrow * 2.0);
247 let steps = ((degrees / 5.0).ceil() as usize).max(8);
248 let mut arc = Vec::with_capacity(steps + 1);
249 for k in 0..=steps {
250 let angle = degrees.to_radians() * k as f64 / steps as f64;
251 arc.push(a3(vertex.add(rotate(dir_a, axis, angle).scale(rho))));
252 }
253 let start = v3(arc[0]);
254 let end = v3(arc[steps]);
255 out.polylines.push(arc);
256 let overshoot = rho + style.arrow * 0.6;
257 out.polylines.push(vec![a3(vertex), a3(vertex.add(dir_a.scale(overshoot)))]);
258 out.polylines.push(vec![a3(vertex), a3(vertex.add(dir_b.scale(overshoot)))]);
259 let tangent_start = axis.cross(dir_a);
260 let tangent_end = axis.cross(dir_b);
261 out.arrows.push(arrowhead(start, tangent_start.scale(-1.0), style));
262 out.arrows.push(arrowhead(end, tangent_end, style));
263 }
264 PmiGeometry::Leader { targets, dot: use_dot } => {
265 for target in targets {
266 let target = v3(*target);
267 out.polylines.push(vec![a3(label), a3(target)]);
268 if *use_dot {
269 out.polylines.push(dot(target, style));
270 } else {
271 out.arrows.push(arrowhead(target, target.sub(label), style));
272 }
273 }
274 }
275 PmiGeometry::Hole { anchor, .. } => {
276 let anchor = v3(*anchor);
277 out.polylines.push(vec![a3(label), a3(anchor)]);
278 out.arrows.push(arrowhead(anchor, anchor.sub(label), style));
279 }
280 PmiGeometry::Explode {
281 center,
282 translate,
283 trace,
284 ..
285 } => {
286 if *trace {
287 let from = v3(*center);
288 let to = from.add(v3(*translate));
289 let span = to.sub(from);
290 let length = span.length();
291 if length > 1e-9 {
292 let dash = (style.arrow * 1.5).max(1e-6);
293 let d = span.scale(1.0 / length);
294 let mut s = 0.0;
295 while s < length {
296 let e = (s + dash).min(length);
297 out.polylines.push(vec![a3(from.add(d.scale(s))), a3(from.add(d.scale(e)))]);
298 s += dash * 2.0;
299 }
300 }
301 }
302 }
303 PmiGeometry::Datum { anchor, letter, .. } => {
304 let anchor = v3(*anchor);
305 let dir = unit_or(label.sub(anchor), up);
306 let apex = anchor.add(dir.scale(style.arrow));
308 let w = arrow_width_dir(dir, style);
309 let half = style.arrow * 0.5;
310 out.arrows.push([a3(apex), a3(anchor.add(w.scale(half))), a3(anchor.sub(w.scale(half)))]);
311 out.polylines.push(vec![a3(apex), a3(label)]);
312 let side = style.text_height * 1.8;
313 let width = side.max(style.text_height * font::ADVANCE * letter.chars().count() as f64 + style.text_height);
314 out.frames.push(frame_rect(label.sub(right.scale(width * 0.5)), width, side, right, up));
315 out.texts.push(TextRun {
316 text: letter.clone(),
317 anchor: a3(label),
318 right: a3(right),
319 up: a3(up),
320 height: style.text_height,
321 });
322 }
323 PmiGeometry::Fcf { anchor, frame, .. } => {
324 let anchor = v3(*anchor);
325 out.polylines.push(vec![a3(label), a3(anchor)]);
326 out.arrows.push(arrowhead(anchor, anchor.sub(label), style));
327 let height = style.text_height * 1.8;
328 let char_w = style.text_height * font::ADVANCE;
329 let mut cells: Vec<(f64, &str)> = vec![(height, frame.symbol.as_str())];
330 cells.push((char_w * frame.zone.chars().count() as f64 + style.text_height, frame.zone.as_str()));
331 for datum in &frame.datums {
332 cells.push((char_w * datum.chars().count() as f64 + style.text_height, datum.as_str()));
333 }
334 let total: f64 = cells.iter().map(|(w, _)| w).sum();
335 let mut left = label.sub(right.scale(total * 0.5));
336 for (width, cell_text) in cells {
337 out.frames.push(frame_rect(left, width, height, right, up));
338 out.texts.push(TextRun {
339 text: cell_text.to_string(),
340 anchor: a3(left.add(right.scale(width * 0.5))),
341 right: a3(right),
342 up: a3(up),
343 height: style.text_height,
344 });
345 left = left.add(right.scale(width));
346 }
347 }
348 }
349 if out.texts.is_empty() && !text.is_empty() {
351 out.texts.push(TextRun {
352 text: text.to_string(),
353 anchor: a3(label),
354 right: a3(right),
355 up: a3(up),
356 height: style.text_height,
357 });
358 }
359 out
360}