1use super::{component_move::now_ms, EngineState};
23use crate::scene::SolidDisplay;
24use brep_instructions::wizard;
25use crate::sheets::print::{self, PictureAt};
26use crate::sheets::project::{poses_of, project_view_through, to_paper, Deferred, LineArtLayer, LinesMode, Model, Unposed, ViewDrawing, ViewFrame};
27use crate::sheets::PlacedView;
28use crate::thumbnail;
29use crate::thumbnail::line_art;
30use brep_instructions::edit;
31use brep_instructions::model::{Arrow, Style};
32use brep_instructions::layout::{self, Rect};
33use brep_instructions::player::{self, Mode, PlayerData, PlayerMesh, PlayerNode};
34use brep_kernel::PmiProjection;
35use brep_kernel::PmiView;
36use serde_json::json;
37use std::collections::HashMap;
38
39pub const IMAGE_PX: u32 = 800;
41
42const PLACED_RGB: [f32; 3] = [0.80, 0.81, 0.83];
44
45const PLAYER_IMAGE_PX: u32 = 480;
48
49const PLAYER_THUMBNAIL_PX: u32 = 96;
51
52const IDENTITY: [f64; 16] = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0];
53
54pub const TOPOLOGY_WAIT: std::time::Duration = std::time::Duration::from_secs(120);
57
58pub struct InstructionsPdfExport {
61 block: brep_instructions::model::Instructions,
62 layout: Vec<layout::Page>,
63 todo: Vec<(PictureAt, String, Rect)>,
64 pictures: HashMap<PictureAt, ViewDrawing>,
65 next: usize,
66 stamp: (u64, u64),
67 started: f64,
68 unposed: Option<Unposed>,
69 views: Vec<PmiView>,
70}
71
72impl InstructionsPdfExport {
73 pub fn progress(&self) -> (usize, usize) { (self.next, self.todo.len()) }
74}
75
76impl EngineState {
77 pub fn begin_instructions_pdf(&mut self) -> Result<InstructionsPdfExport, String> {
79 if self.run_pending() { return Err("wait for the model to finish rebuilding before exporting".into()); }
80 let block = self.instructions_required()?;
81 let layout = layout::pages(&block)?;
82 let todo = print::picture_rects(&block, &layout);
83 let names: Vec<String> = self.scene.solids().iter().filter(|solid| !solid.is_sketch).map(|solid| solid.name.clone()).collect();
84 self.request_exact_solids(names.iter().map(String::as_str));
85 Ok(InstructionsPdfExport {
86 block, layout, todo, pictures: HashMap::new(), next: 0,
87 stamp: (self.applied_generation(), self.history_revision()),
88 started: now_ms(), unposed: None, views: self.pmi_state().views,
89 })
90 }
91
92 pub fn poll_instructions_pdf(&mut self, job: &mut InstructionsPdfExport) -> Result<Option<(Vec<u8>, usize)>, String> {
95 if job.stamp != (self.applied_generation(), self.history_revision()) {
96 return Err("the document changed during PDF export; export again".into());
97 }
98 self.pump_topology();
99 self.pump_sheet_lines();
100 if self.topology_pending() {
101 if now_ms() - job.started >= TOPOLOGY_WAIT.as_secs_f64() * 1000.0 { return Err("timed out loading exact geometry for PDF export".into()); }
102 return Ok(None);
103 }
104 if self.sheet_lines_pending() { return Ok(None); }
105 if job.unposed.is_none() { job.unposed = Some(self.instructions_export_displays()); }
106 if let Some((at, step_id, rect)) = job.todo.get(job.next) {
107 if let Some(view) = job.block.step(step_id).and_then(|step| step.view.as_deref()) {
108 let placed = self.instructions_placed_names(&job.block, step_id);
109 let pending = std::cell::Cell::new(false);
110 let mut requests = Vec::new();
112 let mut defer = |request| { requests.push(request); pending.set(true); Deferred::Pending };
113 let arrows = (job.block.template.style == Style::LineArt).then(|| self.instructions_step_arrows(&job.block, step_id));
114 let drawing = self.instructions_project_with(job.unposed.as_ref().unwrap(), &job.views, view, *rect, &placed,
115 arrows.as_deref(), &mut LinesMode::Export(&mut defer), &pending);
116 for request in requests {
117 if matches!(self.sheet_lines.defer(self.runner.as_mut(), request), Deferred::Here) {
118 return Err("the background runner could not draw the PDF picture; export again".into());
119 }
120 }
121 let Some(drawing) = drawing else { return Ok(None) };
122 if !drawing.error.is_empty() { return Err(drawing.error); }
123 job.pictures.insert(*at, drawing);
124 }
125 job.next += 1;
126 return Ok(None);
127 }
128 let pages = print::pages(&job.block, &job.layout, &job.pictures);
129 let refs = pages.iter().collect::<Vec<_>>();
130 Ok(Some((crate::sheets::pdf::write_pdf(&print::title(&job.block), &refs), pages.len())))
131 }
132
133 pub fn instructions_request_topology(&mut self) -> bool {
146 let names: Vec<String> = self.scene.solids().iter().filter(|solid| !solid.is_sketch).map(|solid| solid.name.clone()).collect();
147 self.request_exact_solids(names.iter().map(String::as_str));
148 self.pump_topology();
149 !self.topology_pending()
150 }
151
152 pub fn instructions_topology_ready(&mut self) -> bool {
153 let names: Vec<String> = self.scene.solids().iter().filter(|solid| !solid.is_sketch).map(|solid| solid.name.clone()).collect();
154 self.request_exact_solids(names.iter().map(String::as_str));
155 let deadline = now_ms() + TOPOLOGY_WAIT.as_secs_f64() * 1000.0;
156 while self.topology_pending() {
157 let now = now_ms();
158 if now >= deadline || !self.runner.wait_topology(std::time::Duration::from_secs_f64((deadline - now) / 1000.0)) {
159 break;
160 }
161 self.pump_topology();
162 }
163 !self.topology_pending()
164 }
165
166 fn instructions_export_displays(&self) -> Unposed {
170 self.scene
171 .solids()
172 .iter()
173 .map(|solid| {
174 let mut display = self.pmi_explode_originals.get(&solid.name).unwrap_or(solid).clone();
175 display.visible = true;
176 (solid.name.clone(), display)
177 })
178 .collect()
179 }
180
181 fn instructions_project(&self, unposed: &Unposed, views: &[PmiView], view_id: &str, rect: Rect, placed: &[String], line_art: Option<&[Arrow]>) -> ViewDrawing {
189 self.instructions_project_with(unposed, views, view_id, rect, placed, line_art, &mut LinesMode::Compute, &std::cell::Cell::new(false))
190 .expect("synchronous projection")
191 }
192
193 #[allow(clippy::too_many_arguments)]
197 fn instructions_project_with(
198 &self,
199 unposed: &Unposed,
200 views: &[PmiView],
201 view_id: &str,
202 rect: Rect,
203 placed: &[String],
204 line_art: Option<&[Arrow]>,
205 mode: &mut LinesMode,
206 pending: &std::cell::Cell<bool>,
207 ) -> Option<ViewDrawing> {
208 let env = self.sheet_env();
209 let model = Model { scene: &self.scene, unposed, env: &env };
210 let view = views.iter().find(|view| view.id == view_id);
211 let report = self.pmi_report.as_ref().and_then(|report| report.view(view_id));
212 let placement = |position: [f64; 2], scale: f64| -> PlacedView {
213 serde_json::from_value(json!({ "id": format!("IV-{view_id}"), "view": view_id, "position": position, "scale": scale }))
214 .expect("a placement from its id, view, position and scale")
215 };
216 let measured = project_view_through(&model, &placement([0.0, 0.0], 1.0), view, report, None, None, mode);
217 if pending.get() { return None; }
218 if !measured.error.is_empty() {
219 return Some(measured);
220 }
221 let frame = view.and_then(|view| view.camera.as_ref()).map(ViewFrame::of);
222 let art = match (line_art, &frame) {
224 (Some(arrows), Some(frame)) => Some(line_art_scene(unposed, report, &measured.solids, placed, arrows, frame)),
225 _ => None,
226 };
227 let mut bounds = measured.bounds;
228 if let (Some(art), Some(frame)) = (&art, &frame) {
229 for (tail, tip) in &art.arrows {
231 for p in [tail, tip] {
232 let v = frame.to_view(*p);
233 let (x, y) = (v[0], -v[1]);
234 bounds = [bounds[0].min(x), bounds[1].min(y), bounds[2].max(x), bounds[3].max(y)];
235 }
236 }
237 }
238 let (position, scale) = print::fit(bounds, rect);
239 let mut drawing = project_view_through(&model, &placement(position, scale), view, report, None, None, mode);
240 if pending.get() { return None; }
241 let Some(view) = view else { return Some(drawing) };
242 if placed.is_empty() || drawing.edges.is_empty() {
243 drawing.highlighted = std::mem::take(&mut drawing.edges);
245 if let (Some(art), Some(frame)) = (&art, &frame) {
246 line_art_layer(&mut drawing, art, frame, &placement(position, scale), rect);
247 }
248 return Some(drawing);
249 }
250 let mut added_only = view.clone();
253 let extra: Vec<String> = placed.iter().filter(|name| !added_only.hides(name)).cloned().collect();
254 added_only.annotations.push(brep_kernel::PmiAnnotation::hide_step("HID-placed", &extra, true));
255 let alone = project_view_through(&model, &placement(position, scale), Some(&added_only), report, None, None, mode);
256 if pending.get() { return None; }
257 if !alone.error.is_empty() { return Some(alone); }
258 let (edges, highlighted) = split_highlighted(std::mem::take(&mut drawing.edges), &alone.edges);
259 drawing.edges = edges;
260 drawing.highlighted = highlighted;
261 if let (Some(art), Some(frame)) = (&art, &frame) {
262 line_art_layer(&mut drawing, art, frame, &placement(position, scale), rect);
263 }
264 Some(drawing)
265 }
266
267 fn instructions_placed_names(&self, block: &brep_instructions::model::Instructions, step_id: &str) -> Vec<String> {
269 let names: Vec<String> = self.scene.solids().iter().map(|solid| solid.name.clone()).collect();
270 edit::presentation(block, step_id, &names).map(|presentation| presentation.placed).unwrap_or_default()
271 }
272
273 pub fn instructions_pdf(&mut self) -> Result<(Vec<u8>, usize), String> {
278 let block = self.instructions_required()?;
279 self.instructions_topology_ready();
280 let layout = layout::pages(&block)?;
281 let pmi = self.pmi_state();
282 let unposed = self.instructions_export_displays();
283 let mut pictures: HashMap<PictureAt, ViewDrawing> = HashMap::new();
284 for (at, step_id, rect) in print::picture_rects(&block, &layout) {
285 if let Some(view) = block.step(&step_id).and_then(|step| step.view.clone()) {
286 let placed = self.instructions_placed_names(&block, &step_id);
287 let arrows = (block.template.style == Style::LineArt).then(|| self.instructions_step_arrows(&block, &step_id));
288 pictures.insert(at, self.instructions_project(&unposed, &pmi.views, &view, rect, &placed, arrows.as_deref()));
289 }
290 }
291 let pages = print::pages(&block, &layout, &pictures);
292 let refs: Vec<&crate::sheets::project::SheetDrawing> = pages.iter().collect();
293 Ok((crate::sheets::pdf::write_pdf(&print::title(&block), &refs), pages.len()))
294 }
295
296 pub fn instructions_step_svg(&mut self, step_id: &str) -> Result<String, String> {
299 let block = self.instructions_required()?;
300 let step = block.step(step_id).ok_or_else(|| format!("no step '{step_id}'"))?;
301 self.instructions_topology_ready();
302 let placed = self.instructions_placed_names(&block, step_id);
303 let arrows = (block.template.style == Style::LineArt).then(|| self.instructions_step_arrows(&block, step_id));
304 let picture = step.view.as_deref().map(|view| {
305 self.instructions_project(&self.instructions_export_displays(), &self.pmi_state().views, view, print::image_picture_rect(), &placed, arrows.as_deref())
306 });
307 Ok(crate::sheets::svg::to_svg(&print::step_image(&block, step_id, picture)?))
308 }
309
310 pub fn instructions_step_png(&self, step_id: &str, size: u32) -> Result<Vec<u8>, String> {
315 let block = self.instructions_required()?;
316 let step = block.step(step_id).ok_or_else(|| format!("no step '{step_id}'"))?;
317 let view_id = step.view.as_deref().ok_or_else(|| format!("step '{step_id}' has no view: run the wizard or pick one"))?;
318 let pmi = self.pmi_state();
319 let view = pmi.views.iter().find(|view| view.id == view_id).ok_or_else(|| format!("no PMI view '{view_id}'"))?;
320 let camera = view.camera.as_ref().ok_or_else(|| format!("PMI view '{view_id}' has no captured camera"))?;
321 let frame = ViewFrame::of(camera);
322 let back = frame.forward.map(|c| -c);
323 let report = self.pmi_report.as_ref().and_then(|report| report.view(view_id));
324 let names: Vec<String> = self.scene.solids().iter().map(|solid| solid.name.clone()).collect();
325 let presentation = edit::presentation(&block, step_id, &names)?;
326 let unposed = self.instructions_export_displays();
327 if block.template.style == Style::LineArt {
328 let mut art = line_art::LineArt::default();
329 let mut bounds: HashMap<&str, ([f64; 3], [f64; 3])> = HashMap::new();
330 for name in &names {
331 if view.hides(name) || presentation.hidden.contains(name) {
332 continue;
333 }
334 let Some(display) = unposed.get(name).filter(|display| !display.is_sketch) else { continue };
335 let Some(body) = posed_body(display, &poses_of(report, name), false) else { continue };
336 if presentation.placed.contains(name) {
337 art.placed.push(body);
338 } else {
339 bounds.insert(name.as_str(), body_bounds(&body));
340 art.adding.push(body);
341 }
342 }
343 for arrow in self.instructions_step_arrows(&block, step_id) {
345 let mut lo = [f64::INFINITY; 3];
346 let mut hi = [f64::NEG_INFINITY; 3];
347 for (name, (a, b)) in &bounds {
348 if under(name, &arrow.occurrence) {
349 lo = std::array::from_fn(|k| lo[k].min(a[k]));
350 hi = std::array::from_fn(|k| hi[k].max(b[k]));
351 }
352 }
353 if lo[0] <= hi[0] {
354 art.arrows.extend(line_art::still_arrow(lo, hi, arrow.start_offset()));
355 }
356 }
357 return line_art::render_png(&art, size, (frame.right, frame.up, back))
358 .ok_or_else(|| format!("step '{step_id}' shows nothing to draw"));
359 }
360 let mut capture = thumbnail::Capture::default();
361 for name in &names {
362 if view.hides(name) || presentation.hidden.contains(name) {
363 continue;
364 }
365 let Some(display) = unposed.get(name).filter(|display| !display.is_sketch) else { continue };
366 let Some(body) = posed_body(display, &poses_of(report, name), presentation.placed.contains(name)) else { continue };
367 capture.bodies.push(body);
368 }
369 thumbnail::render_png_from(&capture, size, (frame.right, frame.up, back))
370 .ok_or_else(|| format!("step '{step_id}' shows nothing to draw"))
371 }
372
373 pub fn instructions_player_data(&self) -> Result<PlayerData, String> {
377 let block = self.instructions_required()?;
378 let unposed = self.instructions_export_displays();
379 let mut scene = player::Scene::default();
380 scene.wrapper = self.instructions_wrapper().map(|(id, _)| id).unwrap_or_default();
383 let flat = |values: &[[f32; 3]]| values.iter().flatten().copied().collect::<Vec<f32>>();
384 for solid in self.scene.solids() {
385 let Some(display) = unposed.get(&solid.name).filter(|display| !display.is_sketch && !display.mesh.indices.is_empty()) else {
386 continue;
387 };
388 let color = display.color_override.unwrap_or_else(|| crate::color::solid_color_srgb(&display.name).map(|c| c as f32));
389 scene.nodes.push(PlayerNode { occurrence: solid.name.clone(), mesh: scene.meshes.len(), matrix: IDENTITY, color });
390 let edges = thumbnail::body_of(display).map(|body| body.edges).unwrap_or_default();
391 scene.meshes.push(
392 PlayerMesh::new(&solid.name, &flat(&display.mesh.positions), &flat(&display.mesh.normals), &display.mesh.indices)
393 .with_edges(&edges),
394 );
395 }
396 let level = self.instructions_level()?;
400 let components = wizard::flatten(&level);
401 let mut keys = Vec::with_capacity(scene.nodes.len());
402 let mut placements = Vec::with_capacity(scene.nodes.len());
403 for node in &scene.nodes {
404 let holder = components
405 .iter()
406 .filter(|(_, c)| node.occurrence.strip_prefix(c.path.as_str()).is_some_and(|rest| rest.starts_with(':')))
407 .max_by_key(|(_, c)| c.path.len())
408 .map(|(_, c)| *c);
409 keys.push(holder.map(|c| format!("{}|{}", c.part_key(), &node.occurrence[c.path.len()..])).unwrap_or_else(|| node.occurrence.clone()));
410 placements.push(holder.and_then(|c| self.instructions_occurrence_placement(&c.path)));
411 }
412 scene.share_meshes(&keys, &placements);
413 let thumbnail_of = |scene: &mut player::Scene, part: &str, occurrence: &str| {
416 if scene.thumbnails.contains_key(part) {
417 return;
418 }
419 let mut capture = thumbnail::Capture::default();
420 for solid in self.scene.solids() {
421 let name = solid.name.as_str();
422 let inside = name == occurrence || name.strip_prefix(occurrence).is_some_and(|rest| rest.starts_with(':'));
423 if let Some(display) = unposed.get(name).filter(|display| inside && !display.is_sketch) {
424 capture.bodies.extend(thumbnail::body_of(display));
425 }
426 }
427 if let Some(png) = thumbnail::render_png(&capture, PLAYER_THUMBNAIL_PX) {
428 scene.thumbnails.insert(part.to_string(), player::png_data_url(&png));
429 }
430 };
431 let pmi = self.pmi_state();
432 for (_, step) in block.steps() {
433 if step.arrows.is_empty() {
434 let derived = self.instructions_step_arrows(&block, &step.id);
435 if !derived.is_empty() {
436 scene.arrows.insert(step.id.clone(), derived);
437 }
438 }
439 if let Some(view_id) = step.view.as_deref() {
442 let report = self.pmi_report.as_ref().and_then(|report| report.view(view_id));
443 let posed: Vec<player::Pose> = scene
444 .nodes
445 .iter()
446 .enumerate()
447 .filter_map(|(node, n)| {
448 let poses = crate::sheets::project::poses_of(report, &n.occurrence);
449 (!poses.is_empty()).then(|| player::Pose { node, matrix: poses.iter().fold(IDENTITY, |m, pose| mul4(pose.matrix(), m)) })
450 })
451 .collect();
452 if !posed.is_empty() {
453 scene.poses.insert(step.id.clone(), posed);
454 }
455 }
456 let view = step.view.as_deref().and_then(|id| pmi.views.iter().find(|view| view.id == id));
457 let camera = view.and_then(|view| view.camera.as_ref());
458 if let (Some(view), Some(camera)) = (view, camera) {
460 if let Some(rows) = self.pmi_report.as_ref().and_then(|report| report.view(&view.id)) {
461 let drawn = crate::sheets::pdf3d::view_annotations(view, camera, rows);
462 let texts: Vec<String> = drawn.iter().map(|(row, _)| row.text.clone()).filter(|text| !text.trim().is_empty()).collect();
463 let lines: Vec<Vec<[f64; 3]>> = drawn.iter().flat_map(|(_, d)| d.lines.iter().cloned()).collect();
464 let arrows: Vec<[[f64; 3]; 3]> = drawn.iter().flat_map(|(_, d)| d.arrows.iter().copied()).collect();
465 if let Some(drawing) = player::PlayerPmi::new(&lines, &arrows, drawn.len(), texts) {
466 scene.pmi.insert(step.id.clone(), drawing);
467 }
468 }
469 }
470 if let Some(camera) = camera {
471 let (fov_deg, half_height) = match camera.projection {
472 PmiProjection::Perspective { fov_y_deg } => (Some(fov_y_deg), None),
473 PmiProjection::Orthographic { half_height } => (None, Some(half_height)),
474 };
475 scene.cameras.insert(step.id.clone(), player::Camera { eye: camera.eye, target: camera.target, up: camera.up, fov_deg, half_height });
476 if let Ok(png) = self.instructions_step_png(&step.id, PLAYER_IMAGE_PX) {
477 scene.step_images.insert(step.id.clone(), player::png_data_url(&png));
478 }
479 }
480 for add in &step.adds {
481 if let Some(occurrence) = add.occurrences.first() {
483 thumbnail_of(&mut scene, &add.part, occurrence);
484 }
485 }
486 }
487 scene.assembly = player::PlayerComponent::rows(&level);
490 let parts: Vec<(String, String)> = scene.assembly.iter().map(|row| (row.part.clone(), row.path.clone())).collect();
491 for (part, path) in parts {
492 thumbnail_of(&mut scene, &part, &path);
493 }
494 PlayerData::build(&block, scene)
495 }
496
497 pub fn instructions_occurrence_placement(&self, occurrence: &str) -> Option<[f64; 16]> {
503 let mut world: [f64; 16] = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0];
504 let mut features: Vec<serde_json::Value> = self.history.features().to_vec();
505 let mut library = self.history.parts_library().clone();
506 for id in occurrence.split(':') {
507 let feature = features
508 .iter()
509 .find(|f| super::components::is_acomp_feature_type(f["type"].as_str().unwrap_or_default()) && f["inputParams"]["id"].as_str() == Some(id))?;
510 let pose = crate::pipeline::pose_params_to_transform(&feature["inputParams"]["transform"]);
511 let mut out = [0.0; 16];
513 for row in 0..4 {
514 for col in 0..4 {
515 out[row * 4 + col] = (0..4).map(|k| world[row * 4 + k] * pose[k * 4 + col]).sum();
516 }
517 }
518 world = out;
519 let part = feature["inputParams"]["partName"].as_str()?.to_string();
520 let document = library[part.as_str()]["document"].clone();
521 features = document["features"].as_array().cloned().unwrap_or_default();
522 library = document["partsLibrary"].clone();
523 }
524 Some(std::array::from_fn(|i| world[(i % 4) * 4 + i / 4]))
526 }
527
528 pub fn instructions_html(&self, mode: Mode) -> Result<(String, PlayerData), String> {
531 let data = self.instructions_player_data()?;
532 Ok((player::offline_html(&data, mode), data))
533 }
534}
535
536fn under(path: &str, occurrence: &str) -> bool {
538 path == occurrence || path.strip_prefix(occurrence).is_some_and(|rest| rest.starts_with(':'))
539}
540
541fn body_bounds(body: &thumbnail::Body) -> ([f64; 3], [f64; 3]) {
543 let mut lo = [f64::INFINITY; 3];
544 let mut hi = [f64::NEG_INFINITY; 3];
545 for &i in &body.indices {
546 let p = body.positions[i as usize];
547 for k in 0..3 {
548 lo[k] = lo[k].min(p[k] as f64);
549 hi[k] = hi[k].max(p[k] as f64);
550 }
551 }
552 (lo, hi)
553}
554
555fn mul4(a: [f64; 16], b: [f64; 16]) -> [f64; 16] {
557 let mut out = [0.0; 16];
558 for col in 0..4 {
559 for row in 0..4 {
560 out[col * 4 + row] = (0..4).map(|k| a[k * 4 + row] * b[col * 4 + k]).sum();
561 }
562 }
563 out
564}
565
566fn posed_body(display: &SolidDisplay, poses: &[crate::sheets::project::SolidPose], placed: bool) -> Option<thumbnail::Body> {
570 let mut body = thumbnail::body_of(display)?;
571 if !poses.is_empty() {
572 let pose = |p: [f64; 3]| poses.iter().fold(p, |p, pose| pose.apply(p));
573 let f64s = |p: [f32; 3]| [p[0] as f64, p[1] as f64, p[2] as f64];
574 let f32s = |p: [f64; 3]| [p[0] as f32, p[1] as f32, p[2] as f32];
575 for (i, normal) in body.normals.iter_mut().enumerate() {
576 let Some(&at) = body.positions.get(i) else { break };
577 let (p, n) = (f64s(at), f64s(*normal));
578 let (from, to) = (pose(p), pose([p[0] + n[0], p[1] + n[1], p[2] + n[2]]));
579 *normal = f32s([to[0] - from[0], to[1] - from[1], to[2] - from[2]]);
580 }
581 for position in body.positions.iter_mut() {
582 *position = f32s(pose(f64s(*position)));
583 }
584 for line in body.edges.iter_mut() {
585 for point in line.iter_mut() {
586 *point = f32s(pose(f64s(*point)));
587 }
588 }
589 }
590 if placed {
591 body.colors.iter_mut().for_each(|colour| *colour = PLACED_RGB);
592 body.edges.clear();
593 }
594 Some(body)
595}
596
597struct LineArtScene {
601 bodies: Vec<(thumbnail::Body, bool)>,
602 arrows: Vec<([f64; 3], [f64; 3])>,
603}
604
605fn line_art_scene(
606 unposed: &Unposed,
607 report: Option<&brep_kernel::PmiViewReport>,
608 solids: &[String],
609 placed: &[String],
610 arrows: &[Arrow],
611 _frame: &ViewFrame,
612) -> LineArtScene {
613 let mut bodies = Vec::new();
614 let mut bounds: Vec<(&str, ([f64; 3], [f64; 3]))> = Vec::new();
615 for name in solids {
616 let Some(display) = unposed.get(name).filter(|display| !display.is_sketch) else { continue };
617 let Some(body) = posed_body(display, &poses_of(report, name), false) else { continue };
618 let adding = !placed.contains(name);
619 if adding {
620 bounds.push((name.as_str(), body_bounds(&body)));
621 }
622 bodies.push((body, adding));
623 }
624 let mut out = Vec::new();
625 for arrow in arrows {
626 let mut lo = [f64::INFINITY; 3];
627 let mut hi = [f64::NEG_INFINITY; 3];
628 for (name, (a, b)) in &bounds {
629 if under(name, &arrow.occurrence) {
630 lo = std::array::from_fn(|k| lo[k].min(a[k]));
631 hi = std::array::from_fn(|k| hi[k].max(b[k]));
632 }
633 }
634 if lo[0] <= hi[0] {
635 out.extend(line_art::still_arrow(lo, hi, arrow.start_offset()));
636 }
637 }
638 LineArtScene { bodies, arrows: out }
639}
640
641const LINE_ART_CELL_MM: f64 = 0.12;
643const LINE_ART_SIDE_MM: f64 = 0.3;
645
646fn line_art_layer(drawing: &mut ViewDrawing, art: &LineArtScene, frame: &ViewFrame, placed: &PlacedView, rect: Rect) {
651 let paper = |p: [f64; 3]| -> ([f64; 2], f64) {
652 let v = frame.to_view(p);
653 (to_paper(placed, v[0], v[1]), v[2])
654 };
655 let mut fills: Vec<(f64, [[f64; 2]; 3], bool)> = Vec::new();
656 for (body, adding) in &art.bodies {
657 let projected: Vec<([f64; 2], f64)> = body.positions.iter().map(|p| paper([p[0] as f64, p[1] as f64, p[2] as f64])).collect();
658 for tri in body.indices.chunks_exact(3) {
659 let [a, b, c] = [tri[0] as usize, tri[1] as usize, tri[2] as usize];
660 let n: [f64; 3] = std::array::from_fn(|k| (body.normals[a][k] + body.normals[b][k] + body.normals[c][k]) as f64);
662 if n[0] * frame.forward[0] + n[1] * frame.forward[1] + n[2] * frame.forward[2] > 0.2 * (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt() {
663 continue;
664 }
665 let (pa, pb, pc) = (projected[a], projected[b], projected[c]);
666 let area = (pb.0[0] - pa.0[0]) * (pc.0[1] - pa.0[1]) - (pc.0[0] - pa.0[0]) * (pb.0[1] - pa.0[1]);
667 if area.abs() < 1e-6 {
668 continue;
669 }
670 fills.push(((pa.1 + pb.1 + pc.1) / 3.0, [pa.0, pb.0, pc.0], *adding));
671 }
672 }
673 fills.sort_by(|a, b| b.0.total_cmp(&a.0));
675 let cols = ((rect.w / LINE_ART_CELL_MM).ceil() as usize).max(1);
677 let rows = ((rect.h / LINE_ART_CELL_MM).ceil() as usize).max(1);
678 let mut tier = vec![0u8; cols * rows];
679 for (_, t, adding) in &fills {
680 let value = if *adding { 2 } else { 1 };
681 let x0 = ((t.iter().map(|p| p[0]).fold(f64::INFINITY, f64::min) - rect.x) / LINE_ART_CELL_MM).floor().max(0.0) as usize;
682 let x1 = (((t.iter().map(|p| p[0]).fold(f64::NEG_INFINITY, f64::max) - rect.x) / LINE_ART_CELL_MM).ceil() as usize).min(cols);
683 let y0 = ((t.iter().map(|p| p[1]).fold(f64::INFINITY, f64::min) - rect.y) / LINE_ART_CELL_MM).floor().max(0.0) as usize;
684 let y1 = (((t.iter().map(|p| p[1]).fold(f64::NEG_INFINITY, f64::max) - rect.y) / LINE_ART_CELL_MM).ceil() as usize).min(rows);
685 let side = |a: [f64; 2], b: [f64; 2], p: [f64; 2]| (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]);
686 let orient = side(t[0], t[1], t[2]).signum();
687 for y in y0..y1 {
688 for x in x0..x1 {
689 let p = [rect.x + (x as f64 + 0.5) * LINE_ART_CELL_MM, rect.y + (y as f64 + 0.5) * LINE_ART_CELL_MM];
690 if (0..3).all(|k| side(t[k], t[(k + 1) % 3], p) * orient >= 0.0) {
691 tier[y * cols + x] = value;
692 }
693 }
694 }
695 }
696 let tier_at = |p: [f64; 2]| -> u8 {
697 let (x, y) = ((p[0] - rect.x) / LINE_ART_CELL_MM, (p[1] - rect.y) / LINE_ART_CELL_MM);
698 if x < 0.0 || y < 0.0 || x >= cols as f64 || y >= rows as f64 {
699 return 0;
700 }
701 tier[y as usize * cols + x as usize]
702 };
703 let outline_of = |own: u8| {
706 move |a: [f64; 2], b: [f64; 2]| -> bool {
707 let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
708 let length = (dx * dx + dy * dy).sqrt();
709 if !(length > 1e-9) {
710 return false;
711 }
712 let (nx, ny) = (-dy / length * LINE_ART_SIDE_MM, dx / length * LINE_ART_SIDE_MM);
713 let mid = [(a[0] + b[0]) * 0.5, (a[1] + b[1]) * 0.5];
714 tier_at([mid[0] + nx, mid[1] + ny]) != own || tier_at([mid[0] - nx, mid[1] - ny]) != own
715 }
716 };
717 let mut outlines = Vec::new();
718 let (edges, placed_outlines) = split_runs(std::mem::take(&mut drawing.edges), outline_of(1));
719 outlines.extend(placed_outlines.into_iter().map(|run| (run, false)));
720 let (lit, adding_outlines) = split_runs(std::mem::take(&mut drawing.highlighted), outline_of(2));
721 outlines.extend(adding_outlines.into_iter().map(|run| (run, true)));
722 drawing.edges = edges;
723 drawing.highlighted = lit;
724 let arrows = art.arrows.iter().map(|(tail, tip)| (paper(*tail).0, paper(*tip).0)).collect();
725 drawing.line_art = Some(LineArtLayer {
726 ground: [rect.x, rect.y, rect.x + rect.w, rect.y + rect.h],
727 fills: fills.into_iter().map(|(_, t, adding)| (t, adding)).collect(),
728 outlines,
729 arrows,
730 });
731}
732
733fn split_runs(runs: Vec<Vec<[f64; 2]>>, pred: impl Fn([f64; 2], [f64; 2]) -> bool) -> (Vec<Vec<[f64; 2]>>, Vec<Vec<[f64; 2]>>) {
735 let (mut no, mut yes) = (Vec::new(), Vec::new());
736 for run in runs {
737 let mut current: Vec<[f64; 2]> = Vec::new();
738 let mut class: Option<bool> = None;
739 for pair in run.windows(2) {
740 let c = pred(pair[0], pair[1]);
741 if class.is_some_and(|k| k != c) {
742 let done = std::mem::take(&mut current);
743 if class == Some(true) { yes.push(done) } else { no.push(done) }
744 }
745 if current.is_empty() {
746 current.push(pair[0]);
747 }
748 current.push(pair[1]);
749 class = Some(c);
750 }
751 if current.len() >= 2 {
752 if class == Some(true) { yes.push(current) } else { no.push(current) }
753 }
754 }
755 (no, yes)
756}
757
758const HIGHLIGHT_TOLERANCE_MM: f64 = 0.02;
761
762pub(super) fn split_highlighted(runs: Vec<Vec<[f64; 2]>>, added: &[Vec<[f64; 2]>]) -> (Vec<Vec<[f64; 2]>>, Vec<Vec<[f64; 2]>>) {
768 let segments: Vec<([f64; 2], [f64; 2])> = added.iter().flat_map(|run| run.windows(2).map(|w| (w[0], w[1]))).collect();
769 let on_added = |p: [f64; 2]| segments.iter().any(|(a, b)| point_segment_distance(p, *a, *b) <= HIGHLIGHT_TOLERANCE_MM);
770 let (mut plain, mut lit) = (Vec::new(), Vec::new());
771 for run in runs {
772 let mut current: Vec<[f64; 2]> = Vec::new();
773 let mut class: Option<bool> = None;
774 for pair in run.windows(2) {
775 let mid = [(pair[0][0] + pair[1][0]) * 0.5, (pair[0][1] + pair[1][1]) * 0.5];
776 let highlighted = on_added(mid);
777 if class.is_some_and(|c| c != highlighted) {
778 let done = std::mem::take(&mut current);
779 if class == Some(true) { lit.push(done) } else { plain.push(done) }
780 }
781 if current.is_empty() {
782 current.push(pair[0]);
783 }
784 current.push(pair[1]);
785 class = Some(highlighted);
786 }
787 if current.len() >= 2 {
788 if class == Some(true) { lit.push(current) } else { plain.push(current) }
789 }
790 }
791 (plain, lit)
792}
793
794fn point_segment_distance(p: [f64; 2], a: [f64; 2], b: [f64; 2]) -> f64 {
795 let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
796 let len2 = dx * dx + dy * dy;
797 let t = if len2 <= 0.0 { 0.0 } else { (((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / len2).clamp(0.0, 1.0) };
798 ((p[0] - a[0] - t * dx).powi(2) + (p[1] - a[1] - t * dy).powi(2)).sqrt()
799}