1use super::{component_move::now_ms, EngineState};
22use crate::scene::SolidDisplay;
23use brep_instructions::wizard;
24use crate::sheets::print::{self, PictureAt};
25use crate::sheets::project::{poses_of, project_view_through, Deferred, LinesMode, Model, Unposed, ViewDrawing, ViewFrame};
26use crate::sheets::PlacedView;
27use crate::thumbnail;
28use brep_instructions::edit;
29use brep_instructions::layout::{self, Rect};
30use brep_instructions::player::{self, Mode, PlayerData, PlayerMesh, PlayerNode};
31use brep_kernel::PmiProjection;
32use brep_kernel::PmiView;
33use serde_json::json;
34use std::collections::HashMap;
35
36pub const IMAGE_PX: u32 = 800;
38
39const PLACED_RGB: [f32; 3] = [0.80, 0.81, 0.83];
41
42const PLAYER_IMAGE_PX: u32 = 480;
45
46const PLAYER_THUMBNAIL_PX: u32 = 96;
48
49const 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];
50
51pub const TOPOLOGY_WAIT: std::time::Duration = std::time::Duration::from_secs(120);
54
55pub struct InstructionsPdfExport {
58 block: brep_instructions::model::Instructions,
59 layout: Vec<layout::Page>,
60 todo: Vec<(PictureAt, String, Rect)>,
61 pictures: HashMap<PictureAt, ViewDrawing>,
62 next: usize,
63 stamp: (u64, u64),
64 started: f64,
65 unposed: Option<Unposed>,
66 views: Vec<PmiView>,
67}
68
69impl InstructionsPdfExport {
70 pub fn progress(&self) -> (usize, usize) { (self.next, self.todo.len()) }
71}
72
73impl EngineState {
74 pub fn begin_instructions_pdf(&mut self) -> Result<InstructionsPdfExport, String> {
76 if self.run_pending() { return Err("wait for the model to finish rebuilding before exporting".into()); }
77 let block = self.instructions_required()?;
78 let layout = layout::pages(&block)?;
79 let todo = print::picture_rects(&block, &layout);
80 let names: Vec<String> = self.scene.solids().iter().filter(|solid| !solid.is_sketch).map(|solid| solid.name.clone()).collect();
81 self.request_exact_solids(names.iter().map(String::as_str));
82 Ok(InstructionsPdfExport {
83 block, layout, todo, pictures: HashMap::new(), next: 0,
84 stamp: (self.applied_generation(), self.history_revision()),
85 started: now_ms(), unposed: None, views: self.pmi_state().views,
86 })
87 }
88
89 pub fn poll_instructions_pdf(&mut self, job: &mut InstructionsPdfExport) -> Result<Option<(Vec<u8>, usize)>, String> {
92 if job.stamp != (self.applied_generation(), self.history_revision()) {
93 return Err("the document changed during PDF export; export again".into());
94 }
95 self.pump_topology();
96 self.pump_sheet_lines();
97 if self.topology_pending() {
98 if now_ms() - job.started >= TOPOLOGY_WAIT.as_secs_f64() * 1000.0 { return Err("timed out loading exact geometry for PDF export".into()); }
99 return Ok(None);
100 }
101 if self.sheet_lines_pending() { return Ok(None); }
102 if job.unposed.is_none() { job.unposed = Some(self.instructions_export_displays()); }
103 if let Some((at, step_id, rect)) = job.todo.get(job.next) {
104 if let Some(view) = job.block.step(step_id).and_then(|step| step.view.as_deref()) {
105 let placed = self.instructions_placed_names(&job.block, step_id);
106 let pending = std::cell::Cell::new(false);
107 let mut requests = Vec::new();
109 let mut defer = |request| { requests.push(request); pending.set(true); Deferred::Pending };
110 let drawing = self.instructions_project_with(job.unposed.as_ref().unwrap(), &job.views, view, *rect, &placed,
111 &mut LinesMode::Export(&mut defer), &pending);
112 for request in requests {
113 if matches!(self.sheet_lines.defer(self.runner.as_mut(), request), Deferred::Here) {
114 return Err("the background runner could not draw the PDF picture; export again".into());
115 }
116 }
117 let Some(drawing) = drawing else { return Ok(None) };
118 if !drawing.error.is_empty() { return Err(drawing.error); }
119 job.pictures.insert(*at, drawing);
120 }
121 job.next += 1;
122 return Ok(None);
123 }
124 let pages = print::pages(&job.block, &job.layout, &job.pictures);
125 let refs = pages.iter().collect::<Vec<_>>();
126 Ok(Some((crate::sheets::pdf::write_pdf(&print::title(&job.block), &refs), pages.len())))
127 }
128
129 pub fn instructions_topology_ready(&mut self) -> bool {
138 let names: Vec<String> = self.scene.solids().iter().filter(|solid| !solid.is_sketch).map(|solid| solid.name.clone()).collect();
139 self.request_exact_solids(names.iter().map(String::as_str));
140 let deadline = now_ms() + TOPOLOGY_WAIT.as_secs_f64() * 1000.0;
141 while self.topology_pending() {
142 let now = now_ms();
143 if now >= deadline || !self.runner.wait_topology(std::time::Duration::from_secs_f64((deadline - now) / 1000.0)) {
144 break;
145 }
146 self.pump_topology();
147 }
148 !self.topology_pending()
149 }
150
151 fn instructions_export_displays(&self) -> Unposed {
155 self.scene
156 .solids()
157 .iter()
158 .map(|solid| {
159 let mut display = self.pmi_explode_originals.get(&solid.name).unwrap_or(solid).clone();
160 display.visible = true;
161 (solid.name.clone(), display)
162 })
163 .collect()
164 }
165
166 fn instructions_project(&self, unposed: &Unposed, views: &[PmiView], view_id: &str, rect: Rect, placed: &[String]) -> ViewDrawing {
174 self.instructions_project_with(unposed, views, view_id, rect, placed, &mut LinesMode::Compute, &std::cell::Cell::new(false)).expect("synchronous projection")
175 }
176
177 fn instructions_project_with(&self, unposed: &Unposed, views: &[PmiView], view_id: &str, rect: Rect, placed: &[String], mode: &mut LinesMode, pending: &std::cell::Cell<bool>) -> Option<ViewDrawing> {
178 let env = self.sheet_env();
179 let model = Model { scene: &self.scene, unposed, env: &env };
180 let view = views.iter().find(|view| view.id == view_id);
181 let report = self.pmi_report.as_ref().and_then(|report| report.view(view_id));
182 let placement = |position: [f64; 2], scale: f64| -> PlacedView {
183 serde_json::from_value(json!({ "id": format!("IV-{view_id}"), "view": view_id, "position": position, "scale": scale }))
184 .expect("a placement from its id, view, position and scale")
185 };
186 let measured = project_view_through(&model, &placement([0.0, 0.0], 1.0), view, report, None, None, mode);
187 if pending.get() { return None; }
188 if !measured.error.is_empty() {
189 return Some(measured);
190 }
191 let (position, scale) = print::fit(measured.bounds, rect);
192 let mut drawing = project_view_through(&model, &placement(position, scale), view, report, None, None, mode);
193 if pending.get() { return None; }
194 let Some(view) = view else { return Some(drawing) };
195 if placed.is_empty() || drawing.edges.is_empty() {
196 drawing.highlighted = std::mem::take(&mut drawing.edges);
198 return Some(drawing);
199 }
200 let mut added_only = view.clone();
201 let extra: Vec<String> = placed.iter().filter(|name| !added_only.display.hidden.contains(name)).cloned().collect();
202 added_only.display.hidden.extend(extra);
203 let alone = project_view_through(&model, &placement(position, scale), Some(&added_only), report, None, None, mode);
204 if pending.get() { return None; }
205 if !alone.error.is_empty() { return Some(alone); }
206 let (edges, highlighted) = split_highlighted(std::mem::take(&mut drawing.edges), &alone.edges);
207 drawing.edges = edges;
208 drawing.highlighted = highlighted;
209 Some(drawing)
210 }
211
212 fn instructions_placed_names(&self, block: &brep_instructions::model::Instructions, step_id: &str) -> Vec<String> {
214 let names: Vec<String> = self.scene.solids().iter().map(|solid| solid.name.clone()).collect();
215 edit::presentation(block, step_id, &names).map(|presentation| presentation.placed).unwrap_or_default()
216 }
217
218 pub fn instructions_pdf(&mut self) -> Result<(Vec<u8>, usize), String> {
223 let block = self.instructions_required()?;
224 self.instructions_topology_ready();
225 let layout = layout::pages(&block)?;
226 let pmi = self.pmi_state();
227 let unposed = self.instructions_export_displays();
228 let mut pictures: HashMap<PictureAt, ViewDrawing> = HashMap::new();
229 for (at, step_id, rect) in print::picture_rects(&block, &layout) {
230 if let Some(view) = block.step(&step_id).and_then(|step| step.view.clone()) {
231 let placed = self.instructions_placed_names(&block, &step_id);
232 pictures.insert(at, self.instructions_project(&unposed, &pmi.views, &view, rect, &placed));
233 }
234 }
235 let pages = print::pages(&block, &layout, &pictures);
236 let refs: Vec<&crate::sheets::project::SheetDrawing> = pages.iter().collect();
237 Ok((crate::sheets::pdf::write_pdf(&print::title(&block), &refs), pages.len()))
238 }
239
240 pub fn instructions_step_svg(&mut self, step_id: &str) -> Result<String, String> {
243 let block = self.instructions_required()?;
244 let step = block.step(step_id).ok_or_else(|| format!("no step '{step_id}'"))?;
245 self.instructions_topology_ready();
246 let placed = self.instructions_placed_names(&block, step_id);
247 let picture = step.view.as_deref().map(|view| {
248 self.instructions_project(&self.instructions_export_displays(), &self.pmi_state().views, view, print::image_picture_rect(), &placed)
249 });
250 Ok(crate::sheets::svg::to_svg(&print::step_image(&block, step_id, picture)?))
251 }
252
253 pub fn instructions_step_png(&self, step_id: &str, size: u32) -> Result<Vec<u8>, String> {
258 let block = self.instructions_required()?;
259 let step = block.step(step_id).ok_or_else(|| format!("no step '{step_id}'"))?;
260 let view_id = step.view.as_deref().ok_or_else(|| format!("step '{step_id}' has no view: run the wizard or pick one"))?;
261 let pmi = self.pmi_state();
262 let view = pmi.views.iter().find(|view| view.id == view_id).ok_or_else(|| format!("no PMI view '{view_id}'"))?;
263 let camera = view.camera.as_ref().ok_or_else(|| format!("PMI view '{view_id}' has no captured camera"))?;
264 let frame = ViewFrame::of(camera);
265 let back = frame.forward.map(|c| -c);
266 let report = self.pmi_report.as_ref().and_then(|report| report.view(view_id));
267 let names: Vec<String> = self.scene.solids().iter().map(|solid| solid.name.clone()).collect();
268 let presentation = edit::presentation(&block, step_id, &names)?;
269 let unposed = self.instructions_export_displays();
270 let mut capture = thumbnail::Capture::default();
271 for name in &names {
272 if view.display.hidden.contains(name) || presentation.hidden.contains(name) {
273 continue;
274 }
275 let Some(display) = unposed.get(name).filter(|display| !display.is_sketch) else { continue };
276 let Some(body) = posed_body(display, &poses_of(report, name), presentation.placed.contains(name)) else { continue };
277 capture.bodies.push(body);
278 }
279 thumbnail::render_png_from(&capture, size, (frame.right, frame.up, back))
280 .ok_or_else(|| format!("step '{step_id}' shows nothing to draw"))
281 }
282
283 pub fn instructions_player_data(&self) -> Result<PlayerData, String> {
287 let block = self.instructions_required()?;
288 let unposed = self.instructions_export_displays();
289 let mut scene = player::Scene::default();
290 scene.wrapper = self.instructions_wrapper().map(|(id, _)| id).unwrap_or_default();
293 let flat = |values: &[[f32; 3]]| values.iter().flatten().copied().collect::<Vec<f32>>();
294 for solid in self.scene.solids() {
295 let Some(display) = unposed.get(&solid.name).filter(|display| !display.is_sketch && !display.mesh.indices.is_empty()) else {
296 continue;
297 };
298 let color = display.color_override.unwrap_or_else(|| crate::color::solid_color_srgb(&display.name).map(|c| c as f32));
299 scene.nodes.push(PlayerNode { occurrence: solid.name.clone(), mesh: scene.meshes.len(), matrix: IDENTITY, color });
300 let edges = thumbnail::body_of(display).map(|body| body.edges).unwrap_or_default();
301 scene.meshes.push(
302 PlayerMesh::new(&solid.name, &flat(&display.mesh.positions), &flat(&display.mesh.normals), &display.mesh.indices)
303 .with_edges(&edges),
304 );
305 }
306 let level = self.instructions_level()?;
310 let components = wizard::flatten(&level);
311 let mut keys = Vec::with_capacity(scene.nodes.len());
312 let mut placements = Vec::with_capacity(scene.nodes.len());
313 for node in &scene.nodes {
314 let holder = components
315 .iter()
316 .filter(|(_, c)| node.occurrence.strip_prefix(c.path.as_str()).is_some_and(|rest| rest.starts_with(':')))
317 .max_by_key(|(_, c)| c.path.len())
318 .map(|(_, c)| *c);
319 keys.push(holder.map(|c| format!("{}|{}", c.part_key(), &node.occurrence[c.path.len()..])).unwrap_or_else(|| node.occurrence.clone()));
320 placements.push(holder.and_then(|c| self.instructions_occurrence_placement(&c.path)));
321 }
322 scene.share_meshes(&keys, &placements);
323 let pmi = self.pmi_state();
324 for (_, step) in block.steps() {
325 if let Some(view_id) = step.view.as_deref() {
328 let report = self.pmi_report.as_ref().and_then(|report| report.view(view_id));
329 let posed: Vec<player::Pose> = scene
330 .nodes
331 .iter()
332 .enumerate()
333 .filter_map(|(node, n)| {
334 let poses = crate::sheets::project::poses_of(report, &n.occurrence);
335 (!poses.is_empty()).then(|| player::Pose { node, matrix: poses.iter().fold(IDENTITY, |m, pose| mul4(pose.matrix(), m)) })
336 })
337 .collect();
338 if !posed.is_empty() {
339 scene.poses.insert(step.id.clone(), posed);
340 }
341 }
342 let camera = step.view.as_deref().and_then(|id| pmi.views.iter().find(|view| view.id == id)).and_then(|view| view.camera.as_ref());
343 if let Some(camera) = camera {
344 let (fov_deg, half_height) = match camera.projection {
345 PmiProjection::Perspective { fov_y_deg } => (Some(fov_y_deg), None),
346 PmiProjection::Orthographic { half_height } => (None, Some(half_height)),
347 };
348 scene.cameras.insert(step.id.clone(), player::Camera { eye: camera.eye, target: camera.target, up: camera.up, fov_deg, half_height });
349 if let Ok(png) = self.instructions_step_png(&step.id, PLAYER_IMAGE_PX) {
350 scene.step_images.insert(step.id.clone(), player::png_data_url(&png));
351 }
352 }
353 for add in &step.adds {
354 if scene.thumbnails.contains_key(&add.part) {
355 continue;
356 }
357 let Some(occurrence) = add.occurrences.first() else { continue };
359 let mut capture = thumbnail::Capture::default();
360 for solid in self.scene.solids() {
361 let name = solid.name.as_str();
362 let inside = name == occurrence || name.strip_prefix(occurrence.as_str()).is_some_and(|rest| rest.starts_with(':'));
363 if let Some(display) = unposed.get(name).filter(|display| inside && !display.is_sketch) {
364 capture.bodies.extend(thumbnail::body_of(display));
365 }
366 }
367 if let Some(png) = thumbnail::render_png(&capture, PLAYER_THUMBNAIL_PX) {
368 scene.thumbnails.insert(add.part.clone(), player::png_data_url(&png));
369 }
370 }
371 }
372 PlayerData::build(&block, scene)
373 }
374
375 pub fn instructions_occurrence_placement(&self, occurrence: &str) -> Option<[f64; 16]> {
381 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];
382 let mut features: Vec<serde_json::Value> = self.history.features().to_vec();
383 let mut library = self.history.parts_library().clone();
384 for id in occurrence.split(':') {
385 let feature = features
386 .iter()
387 .find(|f| super::components::is_acomp_feature_type(f["type"].as_str().unwrap_or_default()) && f["inputParams"]["id"].as_str() == Some(id))?;
388 let pose = crate::pipeline::pose_params_to_transform(&feature["inputParams"]["transform"]);
389 let mut out = [0.0; 16];
391 for row in 0..4 {
392 for col in 0..4 {
393 out[row * 4 + col] = (0..4).map(|k| world[row * 4 + k] * pose[k * 4 + col]).sum();
394 }
395 }
396 world = out;
397 let part = feature["inputParams"]["partName"].as_str()?.to_string();
398 let document = library[part.as_str()]["document"].clone();
399 features = document["features"].as_array().cloned().unwrap_or_default();
400 library = document["partsLibrary"].clone();
401 }
402 Some(std::array::from_fn(|i| world[(i % 4) * 4 + i / 4]))
404 }
405
406 pub fn instructions_html(&self, mode: Mode) -> Result<(String, PlayerData), String> {
409 let data = self.instructions_player_data()?;
410 Ok((player::offline_html(&data, mode), data))
411 }
412}
413
414fn mul4(a: [f64; 16], b: [f64; 16]) -> [f64; 16] {
416 let mut out = [0.0; 16];
417 for col in 0..4 {
418 for row in 0..4 {
419 out[col * 4 + row] = (0..4).map(|k| a[k * 4 + row] * b[col * 4 + k]).sum();
420 }
421 }
422 out
423}
424
425fn posed_body(display: &SolidDisplay, poses: &[crate::sheets::project::SolidPose], placed: bool) -> Option<thumbnail::Body> {
429 let mut body = thumbnail::body_of(display)?;
430 if !poses.is_empty() {
431 let pose = |p: [f64; 3]| poses.iter().fold(p, |p, pose| pose.apply(p));
432 let f64s = |p: [f32; 3]| [p[0] as f64, p[1] as f64, p[2] as f64];
433 let f32s = |p: [f64; 3]| [p[0] as f32, p[1] as f32, p[2] as f32];
434 for (i, normal) in body.normals.iter_mut().enumerate() {
435 let Some(&at) = body.positions.get(i) else { break };
436 let (p, n) = (f64s(at), f64s(*normal));
437 let (from, to) = (pose(p), pose([p[0] + n[0], p[1] + n[1], p[2] + n[2]]));
438 *normal = f32s([to[0] - from[0], to[1] - from[1], to[2] - from[2]]);
439 }
440 for position in body.positions.iter_mut() {
441 *position = f32s(pose(f64s(*position)));
442 }
443 for line in body.edges.iter_mut() {
444 for point in line.iter_mut() {
445 *point = f32s(pose(f64s(*point)));
446 }
447 }
448 }
449 if placed {
450 body.colors.iter_mut().for_each(|colour| *colour = PLACED_RGB);
451 body.edges.clear();
452 }
453 Some(body)
454}
455
456const HIGHLIGHT_TOLERANCE_MM: f64 = 0.02;
459
460pub(super) fn split_highlighted(runs: Vec<Vec<[f64; 2]>>, added: &[Vec<[f64; 2]>]) -> (Vec<Vec<[f64; 2]>>, Vec<Vec<[f64; 2]>>) {
466 let segments: Vec<([f64; 2], [f64; 2])> = added.iter().flat_map(|run| run.windows(2).map(|w| (w[0], w[1]))).collect();
467 let on_added = |p: [f64; 2]| segments.iter().any(|(a, b)| point_segment_distance(p, *a, *b) <= HIGHLIGHT_TOLERANCE_MM);
468 let (mut plain, mut lit) = (Vec::new(), Vec::new());
469 for run in runs {
470 let mut current: Vec<[f64; 2]> = Vec::new();
471 let mut class: Option<bool> = None;
472 for pair in run.windows(2) {
473 let mid = [(pair[0][0] + pair[1][0]) * 0.5, (pair[0][1] + pair[1][1]) * 0.5];
474 let highlighted = on_added(mid);
475 if class.is_some_and(|c| c != highlighted) {
476 let done = std::mem::take(&mut current);
477 if class == Some(true) { lit.push(done) } else { plain.push(done) }
478 }
479 if current.is_empty() {
480 current.push(pair[0]);
481 }
482 current.push(pair[1]);
483 class = Some(highlighted);
484 }
485 if current.len() >= 2 {
486 if class == Some(true) { lit.push(current) } else { plain.push(current) }
487 }
488 }
489 (plain, lit)
490}
491
492fn point_segment_distance(p: [f64; 2], a: [f64; 2], b: [f64; 2]) -> f64 {
493 let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
494 let len2 = dx * dx + dy * dy;
495 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) };
496 ((p[0] - a[0] - t * dx).powi(2) + (p[1] - a[1] - t * dy).powi(2)).sqrt()
497}