1use super::*;
25use crate::sheets::frame::{today_iso, FrameContext};
26use crate::sheets::dimension::{SheetAnchor, SheetDimension, ALIGNED, ALIGNMENTS, KINDS};
27use crate::sheets::ordinate::{SheetOrdinate, AXES};
28use crate::sheets::{DetailCircle, SectionCut, DETAIL_SCALE_FACTOR};
29use crate::sheets::project::{project_sheet_with, Deferred, LinesJob, LinesMode, Model, SheetDrawing};
30use crate::sheets::{
31 PlacedView, Sheet, SheetState, DEFAULT_BORDER_INSET_MM, DEFAULT_SCALE, DEFAULT_SIZE,
32 ORTHOGRAPHIC,
33};
34use serde_json::Value;
35
36pub(crate) struct SheetCache {
40 pub key: (String, u64, u64, u64, u64, u64, String),
41 pub drawing: SheetDrawing,
42 pub complete: bool,
46 pub reproject: bool,
50 pub drawing_json: String,
55}
56
57#[derive(Default)]
69pub(crate) struct SheetLinesWork {
70 next_id: u64,
71 in_flight: Option<LinesFlight>,
72 parked: Vec<LinesJob>,
73 refused: std::collections::HashSet<String>,
75 unsupported: bool,
77 revision: u64,
79}
80
81struct LinesFlight {
82 id: u64,
83 placement: String,
84 key: String,
85 superseded: bool,
86}
87
88impl SheetLinesWork {
89 pub(crate) fn pending(&self) -> bool {
91 self.in_flight.is_some() || !self.parked.is_empty()
92 }
93
94 pub(crate) fn forget(&mut self) {
97 self.in_flight = None;
98 self.parked.clear();
99 self.refused.clear();
100 self.unsupported = false;
101 self.revision += 1;
102 }
103
104 fn defer(&mut self, runner: &mut dyn crate::runner::HistoryRunner, job: LinesJob) -> Deferred {
108 if self.unsupported || self.refused.contains(&job.key) {
109 return Deferred::Here;
110 }
111 let key = job.key.clone();
112 match self.in_flight.as_mut() {
113 Some(flight) if flight.key == key => {
116 flight.superseded = false;
117 self.parked.retain(|parked| parked.placement != job.placement);
118 }
119 Some(flight) => {
120 if flight.placement == job.placement {
121 flight.superseded = true;
122 }
123 self.park(job);
124 }
125 None => self.park(job),
126 }
127 self.pump(runner);
128 if let Some(lines) = crate::sheets::project::cached_exact_lines(&key) {
129 return Deferred::Lines(lines);
130 }
131 if self.unsupported || self.refused.contains(&key) {
132 return Deferred::Here;
133 }
134 Deferred::Pending
135 }
136
137 fn park(&mut self, job: LinesJob) {
138 self.parked.retain(|parked| parked.placement != job.placement && parked.key != job.key);
139 self.parked.push(job);
140 }
141
142 pub(crate) fn pump(&mut self, runner: &mut dyn crate::runner::HistoryRunner) {
144 loop {
145 if self.in_flight.is_none() && !self.parked.is_empty() {
146 let job = self.parked.remove(0);
147 self.next_id += 1;
148 let flight = LinesFlight {
149 id: self.next_id,
150 placement: job.placement.clone(),
151 key: job.key.clone(),
152 superseded: false,
153 };
154 if runner.submit_sheet_lines(crate::runner::SheetLinesRequest { id: self.next_id, job }) {
155 self.in_flight = Some(flight);
156 } else {
157 self.unsupported = true;
160 self.parked.clear();
161 self.revision += 1;
162 }
163 }
164 let Some(reply) = runner.poll_sheet_lines() else { break };
165 if self.in_flight.as_ref().is_none_or(|flight| flight.id != reply.id) {
166 continue; }
168 let flight = self.in_flight.take().expect("checked above");
169 if flight.superseded {
170 continue;
171 }
172 match reply.lines {
173 Ok(lines) => crate::sheets::project::store_exact_lines(reply.key, lines),
174 Err(_) => {
175 self.refused.insert(reply.key);
176 }
177 }
178 self.revision += 1;
179 }
180 }
181}
182
183impl EngineState {
184 pub fn sheet_state(&self) -> SheetState {
189 self.history
190 .sheets_block()
191 .and_then(|block| serde_json::from_value(block.clone()).ok())
192 .unwrap_or_default()
193 }
194
195 pub fn sheet_open(&self) -> Option<&str> {
197 self.sheet_open.as_deref()
198 }
199
200 pub fn set_document_name(&mut self, name: &str) {
205 if self.document_name != name {
206 self.document_name = name.to_string();
207 self.dirty = true;
208 }
209 }
210
211 pub fn document_name(&self) -> &str {
212 &self.document_name
213 }
214
215 pub fn sheet_open_object(&self) -> Option<&str> {
217 self.sheet_open_object.as_deref()
218 }
219
220 pub fn sheet_object_opens(&self) -> u64 {
225 self.sheet_object_opens
226 }
227
228 fn open_sheet_object(&mut self, id: String) {
241 if Self::is_sheet_child(&self.sheet_state(), &id) {
242 self.sheet_selected_object = Some(id.clone());
243 }
244 self.sheet_open_object = Some(id);
245 self.sheet_object_opens = self.sheet_object_opens.wrapping_add(1);
246 }
247
248 fn is_sheet_child(state: &SheetState, id: &str) -> bool {
251 state.locate_view(id).is_some()
252 || state.locate_dimension(id).is_some()
253 || state.locate_ordinate(id).is_some()
254 }
255
256 pub fn sheet_selected_object(&self) -> Option<&str> {
259 let id = self.sheet_selected_object.as_deref()?;
260 Self::is_sheet_child(&self.sheet_state(), id).then_some(id)
261 }
262
263 pub fn sheet_select_object(&mut self, id: Option<&str>) -> bool {
267 match id {
268 Some(id) if Self::is_sheet_child(&self.sheet_state(), id) => {
269 self.sheet_selected_object = Some(id.to_string());
270 }
271 Some(_) => return false,
272 None => self.sheet_selected_object = None,
273 }
274 self.dirty = true;
275 true
276 }
277
278 fn set_open_sheet(&mut self, id: Option<String>) {
280 self.sheet_open = id;
281 }
282
283 pub fn sheet_pmi_view_ids(&self) -> Vec<String> {
285 self.pmi_state().views.iter().map(|view| view.id.clone()).collect()
286 }
287
288 pub fn sheet_catalogue(&self) -> Value {
291 crate::sheets::schema_catalogue(&self.sheet_pmi_view_ids())
292 }
293
294 pub fn sheet_state_json(&mut self) -> String {
301 let state = self.sheet_state();
302 let drawing = match self.sheet_open.clone() {
303 Some(id) if self.sheet_project(&id) => self
304 .sheet_cache
305 .as_ref()
306 .map(|cache| cache.drawing_json.as_str())
307 .unwrap_or("null"),
308 _ => "null",
309 };
310 let sheets = serde_json::to_string(&state.sheets).unwrap_or_else(|_| "[]".into());
311 let open = serde_json::to_string(&self.sheet_open).unwrap_or_else(|_| "null".into());
312 let object = serde_json::to_string(&self.sheet_open_object).unwrap_or_else(|_| "null".into());
313 let selected = serde_json::to_string(&self.sheet_selected_object()).unwrap_or_else(|_| "null".into());
314 format!(
315 "{{\"sheets\":{sheets},\"idCounter\":{},\"openSheet\":{open},\"openObject\":{object},\"selectedObject\":{selected},\"drawing\":{drawing}}}",
316 state.id_counter
317 )
318 }
319
320 pub fn sheet_project(&mut self, id: &str) -> bool {
331 let state = self.sheet_state();
332 let Some(sheet) = state.find_sheet(id).cloned() else {
333 return false;
334 };
335 let context = self.sheet_frame_context();
341 self.request_sheet_topology(&sheet);
342 self.pump_sheet_lines();
343 let key = self.sheet_cache_key(id, &context);
344 if self.sheet_cache.as_ref().is_none_or(|cache| cache.key != key || cache.reproject) {
345 let pmi = self.pmi_state();
346 let env = self.sheet_env();
350 let mut complete = true;
351 let (runner, work) = (&mut self.runner, &mut self.sheet_lines);
352 let mut defer = |job: LinesJob| {
353 let answer = work.defer(runner.as_mut(), job);
354 complete &= !matches!(answer, Deferred::Pending);
355 answer
356 };
357 let model = Model { scene: &self.scene, unposed: &self.pmi_explode_originals, env: &env };
358 let mut drawing = project_sheet_with(
359 &model,
360 &sheet,
361 &pmi.views,
362 self.pmi_report.as_ref(),
363 &context,
364 &mut LinesMode::Defer(&mut defer),
365 );
366 self.draw_sheet_bom(&sheet, &mut drawing);
367 let drawing_json = serde_json::to_string(&drawing).unwrap_or_else(|_| "null".into());
368 let landed = self.sheet_cache_key(id, &context);
374 let reproject = !complete && landed != key;
375 let key = landed;
376 self.sheet_cache = Some(SheetCache { key, drawing, drawing_json, complete, reproject });
377 }
378 true
379 }
380
381 fn sheet_env(&self) -> brep_kernel::Env {
384 brep_kernel::Env::build(&self.history.expressions(), &self.history.configurator()).unwrap_or_default()
385 }
386
387 pub fn pump_sheet_lines(&mut self) {
389 self.sheet_lines.pump(self.runner.as_mut());
390 }
391
392 pub fn sheet_lines_pending(&self) -> bool {
395 self.sheet_lines.pending()
396 }
397
398 fn request_sheet_topology(&mut self, sheet: &crate::sheets::Sheet) {
403 let lacking = self.scene.solids().iter().any(|solid| {
405 solid.source_handle != 0
406 && !solid.is_sketch
407 && !self.topology_asked.contains(&solid.source_handle)
408 && self.scene.exact_solid(&solid.name).is_none()
409 });
410 if !lacking {
411 self.pump_topology();
412 return;
413 }
414 let pmi = self.pmi_state();
415 let names = crate::sheets::project::sheet_solid_names(&self.scene, sheet, &pmi.views);
416 self.request_exact_solids(names.iter().map(String::as_str));
417 }
418
419 fn sheet_frame_context(&self) -> FrameContext {
422 FrameContext { document: self.document_name.clone(), date: today_iso() }
423 }
424
425 fn sheet_cache_key(
426 &self,
427 id: &str,
428 context: &FrameContext,
429 ) -> (String, u64, u64, u64, u64, u64, String) {
430 (
431 id.to_string(),
432 self.applied_generation,
433 self.history.revision(),
434 self.scene.exact_revision(),
435 self.scene.visibility_revision(),
440 self.sheet_lines.revision,
441 format!("{}\u{1f}{}", context.document, context.date),
442 )
443 }
444
445 pub fn sheet_drawing_cached(&self, id: &str) -> Option<&SheetDrawing> {
449 let key = self.sheet_cache_key(id, &self.sheet_frame_context());
450 self.sheet_cache
451 .as_ref()
452 .filter(|cache| cache.key == key)
453 .map(|cache| &cache.drawing)
454 }
455
456 pub fn sheet_drawing(&mut self, id: &str) -> Option<&SheetDrawing> {
461 if !self.sheet_project(id) {
462 return None;
463 }
464 self.sheet_cache.as_ref().map(|cache| &cache.drawing)
465 }
466
467 pub fn export_sheet_svg(&mut self, id: &str) -> Result<String, String> {
469 self.plugin_export_ready()?;
470 let id = self.resolve_sheet(id)?;
471 let drawing = self
472 .sheet_drawing_exact(&id)
473 .ok_or_else(|| format!("no sheet '{id}'"))?;
474 Ok(crate::sheets::svg::to_svg(drawing))
475 }
476
477 pub fn sheet_drawing_exact(&mut self, id: &str) -> Option<&SheetDrawing> {
482 if !self.sheet_project(id) {
483 return None;
484 }
485 if self.sheet_cache.as_ref().is_some_and(|cache| !cache.complete) {
486 let state = self.sheet_state();
487 let sheet = state.find_sheet(id)?;
488 let pmi = self.pmi_state();
489 let env = self.sheet_env();
490 let model = Model { scene: &self.scene, unposed: &self.pmi_explode_originals, env: &env };
491 let mut drawing = project_sheet_with(
492 &model,
493 sheet,
494 &pmi.views,
495 self.pmi_report.as_ref(),
496 &self.sheet_frame_context(),
497 &mut LinesMode::Compute,
498 );
499 self.draw_sheet_bom(&sheet, &mut drawing);
500 let drawing_json = serde_json::to_string(&drawing).unwrap_or_else(|_| "null".into());
501 if let Some(cache) = self.sheet_cache.as_mut() {
502 cache.drawing = drawing;
503 cache.drawing_json = drawing_json;
504 cache.complete = true;
505 cache.reproject = false;
506 }
507 }
508 self.sheet_cache.as_ref().map(|cache| &cache.drawing)
509 }
510
511 pub fn export_sheet_pdf(&mut self, id: &str) -> Result<Vec<u8>, String> {
515 let id = self.resolve_sheet(id)?;
516 let name = self.sheet_state().find_sheet(&id).map(|sheet| sheet.name.clone()).unwrap_or_default();
517 self.export_sheets_pdf(&[id], &name)
518 }
519
520 pub fn export_document_pdf(&mut self) -> Result<Vec<u8>, String> {
526 self.export_document_pdf_with(false)
527 }
528
529 pub fn export_document_pdf_3d(&mut self) -> Result<Vec<u8>, String> {
533 self.export_document_pdf_with(true)
534 }
535
536 fn export_document_pdf_with(&mut self, three_d: bool) -> Result<Vec<u8>, String> {
537 let state = self.sheet_state();
538 if state.sheets.is_empty() {
539 return Err("the document has no sheets".into());
540 }
541 let ids: Vec<String> = state.sheets.iter().map(|sheet| sheet.id.clone()).collect();
542 let title = if self.document_name.trim().is_empty() {
543 state.sheets[0].name.clone()
544 } else {
545 self.document_name.clone()
546 };
547 self.export_sheets_pdf_with(&ids, &title, three_d)
548 }
549
550 pub fn export_sheets_pdf(&mut self, ids: &[String], title: &str) -> Result<Vec<u8>, String> {
557 self.export_sheets_pdf_with(ids, title, false)
558 }
559
560 pub fn export_sheets_pdf_with(&mut self, ids: &[String], title: &str, three_d: bool) -> Result<Vec<u8>, String> {
565 self.plugin_export_ready()?;
566 let state = self.sheet_state();
567 for id in ids {
568 if let Some(sheet) = state.find_sheet(id) {
569 self.request_sheet_topology(sheet);
570 }
571 }
572 let context = self.sheet_frame_context();
573 let pmi = self.pmi_state();
574 let mut drawings: Vec<SheetDrawing> = Vec::with_capacity(ids.len());
575 for id in ids {
576 let sheet = state.find_sheet(id).ok_or_else(|| format!("no sheet '{id}'"))?;
577 let complete = self.sheet_cache.as_ref().is_some_and(|cache| cache.complete);
578 let drawing = match self.sheet_drawing_cached(id).filter(|_| complete) {
579 Some(cached) => cached.clone(),
580 None => {
581 let mut drawing = project_sheet_with(
582 &Model { scene: &self.scene, unposed: &self.pmi_explode_originals, env: &self.sheet_env() },
583 sheet, &pmi.views, self.pmi_report.as_ref(), &context, &mut LinesMode::Compute,
584 );
585 self.draw_sheet_bom(sheet, &mut drawing);
586 drawing
587 },
588 };
589 drawings.push(drawing);
590 }
591 let pages: Vec<&SheetDrawing> = drawings.iter().collect();
592 if !three_d {
593 return Ok(crate::sheets::pdf::write_pdf(title, &pages));
594 }
595 let three_d = self.pdf3d_content(ids, &drawings, title)?;
596 Ok(crate::sheets::pdf::write_pdf_3d(title, &pages, Some(&three_d)))
597 }
598
599 fn pdf3d_content(
603 &self,
604 ids: &[String],
605 drawings: &[SheetDrawing],
606 title: &str,
607 ) -> Result<crate::sheets::pdf3d::Pdf3d, String> {
608 use crate::sheets::pdf3d::{self, Page3d, Placement3d};
609 let state = self.sheet_state();
610 let pmi = self.pmi_state();
611 let bodies = self.pdf3d_bodies();
612 let model = pdf3d::build(&bodies, &pmi, self.pmi_report.as_ref())?;
613 const MARGIN_MM: f64 = 4.0;
616 let mut placements = Vec::new();
617 for (page, (id, drawing)) in ids.iter().zip(drawings).enumerate() {
618 let Some(sheet) = state.find_sheet(id) else { continue };
619 for placed in sheet.views.iter().filter(|placed| placed.three_d) {
620 let Some(view) = drawing.views.iter().find(|view| view.id == placed.id) else { continue };
621 if !view.error.is_empty() || model.view(&placed.view).is_none() {
622 continue;
623 }
624 let [x0, y0, x1, y1] = view.bounds;
625 let p = placed.position;
626 let half = [
627 ((p[0] - x0).max(x1 - p[0]) + MARGIN_MM).max(10.0),
628 ((p[1] - y0).max(y1 - p[1]) + MARGIN_MM).max(10.0),
629 ];
630 placements.push(Placement3d {
631 page,
632 id: placed.id.clone(),
633 view: placed.view.clone(),
634 centre_mm: p,
635 half_mm: half,
636 scale: placed.scale,
637 });
638 }
639 }
640 let page = Some({
641 let (w, h) = state
642 .find_sheet(ids.first().map(String::as_str).unwrap_or(""))
643 .map(|sheet| sheet.millimetres())
644 .unwrap_or((297.0, 210.0));
645 let mut page = Page3d::layout(w.max(h), w.min(h), title, &model);
646 if let Some((poster, scale)) = self.pdf3d_poster(&model, &page, &pmi) {
647 page.poster = Some(poster);
648 page.poster_scale = Some(scale);
649 }
650 page
651 });
652 Ok(pdf3d::Pdf3d { model, placements, page })
653 }
654
655 fn pdf3d_poster(
661 &self,
662 model: &crate::sheets::pdf3d::Model3d,
663 page: &crate::sheets::pdf3d::Page3d,
664 pmi: &brep_kernel::PmiState,
665 ) -> Option<(crate::sheets::project::ViewDrawing, f64)> {
666 let default = model.views.get(page.default_view)?;
667 let mut camera = default.camera.clone();
668 if let brep_kernel::PmiProjection::Perspective { fov_y_deg } = camera.projection {
669 let distance = {
670 let d = [camera.target[0] - camera.eye[0], camera.target[1] - camera.eye[1], camera.target[2] - camera.eye[2]];
671 (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
672 };
673 camera.projection = brep_kernel::PmiProjection::Orthographic {
674 half_height: distance * (fov_y_deg.to_radians() * 0.5).tan(),
675 };
676 }
677 let mut views = pmi.views.clone();
678 match views.iter_mut().find(|view| view.id == default.id) {
679 Some(view) => view.camera = Some(camera),
680 None => views.push(brep_kernel::PmiView {
681 id: default.id.clone(),
682 name: default.name.clone(),
683 camera: Some(camera),
684 display: Default::default(),
685 annotations: Vec::new(),
686 }),
687 }
688 let centre = page.box_centre_mm();
689 let half = page.box_half_mm();
690 let project = |scale: f64| -> Option<crate::sheets::project::ViewDrawing> {
691 let mut sheet: crate::sheets::Sheet = serde_json::from_value(serde_json::json!({
692 "id": "MODEL3D",
693 "size": crate::sheets::CUSTOM,
694 "widthMm": page.width_mm,
695 "heightMm": page.height_mm,
696 "border": false,
697 "titleBlock": false,
698 }))
699 .ok()?;
700 sheet.views.push(PlacedView {
701 id: "MODEL3D".into(),
702 view: default.id.clone(),
703 position: centre,
704 scale,
705 projection: ORTHOGRAPHIC.to_string(),
706 flatten_text: true,
707 three_d: false,
708 section: None,
709 detail: None,
710 });
711 let drawing = project_sheet_with(
712 &Model { scene: &self.scene, unposed: &self.pmi_explode_originals, env: &self.sheet_env() },
713 &sheet,
714 &views,
715 self.pmi_report.as_ref(),
716 &self.sheet_frame_context(),
717 &mut LinesMode::Compute,
718 );
719 drawing.views.into_iter().next().filter(|view| view.error.is_empty())
720 };
721 let probe = project(1.0)?;
723 let [x0, y0, x1, y1] = probe.bounds;
724 let reach = [(centre[0] - x0).max(x1 - centre[0]), (centre[1] - y0).max(y1 - centre[1])];
725 if !(reach[0] > 1e-9 && reach[1] > 1e-9) {
726 return None;
727 }
728 let scale = (half[0] / reach[0]).min(half[1] / reach[1]) * 0.9;
729 project(scale).map(|drawing| (drawing, scale))
730 }
731
732 pub fn resolve_sheet(&self, id: &str) -> Result<String, String> {
735 let state = self.sheet_state();
736 let id = id.trim();
737 if !id.is_empty() {
738 return state
739 .find_sheet(id)
740 .map(|sheet| sheet.id.clone())
741 .ok_or_else(|| format!("no sheet '{id}'"));
742 }
743 if let Some(open) = self.sheet_open.as_ref().filter(|open| state.find_sheet(open).is_some()) {
744 return Ok(open.clone());
745 }
746 state
747 .sheets
748 .first()
749 .map(|sheet| sheet.id.clone())
750 .ok_or_else(|| "the document has no sheets".to_string())
751 }
752
753 fn write_sheet_state(&mut self, state: SheetState, coalesce_key: Option<&str>) {
758 let block = if state.is_empty() { None } else { serde_json::to_value(&state).ok() };
759 self.history.set_sheets_block(block, coalesce_key);
760 self.dirty = true;
761 }
762
763 pub fn sheet_add(&mut self, name: Option<&str>) -> String {
768 let mut state = self.sheet_state();
769 let id = state.next_id("SHEET");
770 let name = name
771 .map(str::trim)
772 .filter(|name| !name.is_empty())
773 .map(String::from)
774 .unwrap_or_else(|| format!("Sheet {}", state.sheets.len() + 1));
775 let (width_mm, height_mm) = crate::sheets::paper_size(DEFAULT_SIZE).unwrap_or((420.0, 297.0));
776 state.sheets.push(Sheet {
777 id: id.clone(),
778 name,
779 size: DEFAULT_SIZE.to_string(),
780 width_mm,
781 height_mm,
782 border: true,
783 border_inset_mm: DEFAULT_BORDER_INSET_MM,
784 title_block: true,
785 notes: String::new(),
786 views: Vec::new(),
787 dimensions: Vec::new(),
788 ordinates: Vec::new(),
789 revisions: Vec::new(),
790 bom_table: None,
791 });
792 self.write_sheet_state(state, None);
793 self.set_open_sheet(Some(id.clone()));
794 id
795 }
796
797 pub fn sheet_insert_bom(&mut self) -> Result<(), String> {
799 let id = self
800 .sheet_open()
801 .ok_or("open a drawing sheet first")?
802 .to_owned();
803 if self
804 .sheet_state()
805 .find_sheet(&id)
806 .and_then(|s| s.bom_table.as_ref())
807 .is_none()
808 {
809 self.sheet_update(
810 &id,
811 &serde_json::json!({ "bomTable": crate::sheets::BomTable::default() }).to_string(),
812 )?;
813 }
814 self.sheet_set_object_open(Some(&id));
815 Ok(())
816 }
817
818 pub fn sheet_bom_columns(&self) -> Vec<String> {
820 let mut columns = std::collections::BTreeSet::from([
821 "partName".to_owned(),
822 "occurrence.Quantity".to_owned(),
823 "occurrence.Find_Number".to_owned(),
824 "part.Part_Number".to_owned(),
825 "part.Description".to_owned(),
826 "part.Material".to_owned(),
827 ]);
828 for component in &self.assembly_components {
829 for (scope, attrs) in [
830 ("part", self.part_attributes(&component.part_name)),
831 ("occurrence", self.occurrence_attributes(&component.id)),
832 ] {
833 if let Some(attrs) = attrs.as_object() {
834 columns.extend(attrs.keys().map(|key| format!("{scope}.{key}")));
835 }
836 }
837 }
838 columns.into_iter().collect()
839 }
840
841 fn draw_sheet_bom(&self, sheet: &Sheet, drawing: &mut SheetDrawing) {
842 let Some(table) = &sheet.bom_table else {
843 return;
844 };
845 let mut rows: std::collections::BTreeMap<Vec<String>, usize> =
846 std::collections::BTreeMap::new();
847 let occurrences = self.occurrence_attributes_all();
848 let mut parts = std::collections::HashMap::new();
849 for component in &self.assembly_components {
850 let part = parts
851 .entry(component.part_name.clone())
852 .or_insert_with(|| self.part_attributes(&component.part_name));
853 let occurrence = occurrences.get(&component.id).unwrap_or(&Value::Null);
854 let cells: Vec<String> = table
855 .columns
856 .iter()
857 .map(|column| {
858 if column == "partName" {
859 return component.part_name.clone();
860 }
861 if column == "quantity" || column == "occurrence.Quantity" {
862 return String::new();
863 }
864 let (scope, key) = column.split_once('.').unwrap_or(("part", column));
865 if column == "part.PMI" {
866 return self.part_pmi_count(&component.part_name).to_string();
867 }
868 let value = if scope == "occurrence" {
869 &occurrence[key]
870 } else {
871 &part[key]
872 };
873 match value {
874 Value::Null => String::new(),
875 Value::String(s) => s.clone(),
876 v => v.to_string(),
877 }
878 })
879 .collect();
880 let mut key = cells;
882 key.push(component.part_name.clone());
883 *rows.entry(key).or_default() += 1;
884 }
885 for wire in self.wire_bom_lines() {
886 let mut cells: Vec<String> = table
887 .columns
888 .iter()
889 .map(|column| match column.as_str() {
890 "partName" | "part.Part_Number" => wire.stock_part_number.clone(),
891 "occurrence.MF_QTY" => wire
892 .mf_qty
893 .map(|q| crate::formatting::compact_decimal(q, 3))
894 .unwrap_or_else(|| wire.state.as_str().to_owned()),
895 "occurrence.Reference_Designator" => wire.connection_id.clone(),
896 _ => String::new(),
897 })
898 .collect();
899 cells.push(format!("wire:{}", wire.connection_id));
900 rows.insert(cells, 1);
901 }
902 let frame = drawing
903 .frame
904 .get_or_insert_with(|| crate::sheets::frame::FrameDrawing {
905 lines: Vec::new(),
906 texts: Vec::new(),
907 revision_table: None,
908 });
909 let [x, y] = table.position;
910 let width = table.column_width_mm;
911 let height = 7.;
912 let count = rows.len() + 1;
913 for row in 0..=count {
914 frame.lines.push(vec![
915 [x, y + row as f64 * height],
916 [
917 x + table.columns.len() as f64 * width,
918 y + row as f64 * height,
919 ],
920 ]);
921 }
922 for col in 0..=table.columns.len() {
923 frame.lines.push(vec![
924 [x + col as f64 * width, y],
925 [x + col as f64 * width, y + count as f64 * height],
926 ]);
927 }
928 let headers: Vec<String> = table
929 .columns
930 .iter()
931 .map(|c| match c.as_str() {
932 "partName" => "Part name".into(),
933 "quantity" => "Quantity".into(),
934 _ => c
935 .split_once('.')
936 .map_or(c.as_str(), |(_, key)| key)
937 .replace('_', " "),
938 })
939 .collect();
940 let mut all = vec![headers];
941 for (mut cells, quantity) in rows {
942 cells.pop();
943 for (i, column) in table.columns.iter().enumerate() {
944 if column == "quantity" || column == "occurrence.Quantity" {
945 cells[i] = quantity.to_string();
946 }
947 }
948 all.push(cells);
949 }
950 for (row, cells) in all.iter().enumerate() {
951 for (col, cell) in cells.iter().enumerate() {
952 let available = ((width - 2.) / (2.5 * 0.65)).floor() as usize;
953 let text = if cell.chars().count() > available {
954 format!(
955 "{}…",
956 cell.chars()
957 .take(available.saturating_sub(1))
958 .collect::<String>()
959 )
960 } else {
961 cell.clone()
962 };
963 frame.texts.push(crate::sheets::project::SheetText::new(
964 text,
965 [
966 x + (col as f64 + 0.5) * width,
967 y + (row as f64 + 0.5) * height,
968 ],
969 [2.5, 0.],
970 [0., -2.5],
971 ));
972 }
973 }
974 }
975
976 pub fn sheet_update(&mut self, id: &str, params_json: &str) -> Result<(), String> {
978 let params: Value = serde_json::from_str(params_json)
979 .map_err(|error| format!("sheet params: {error}"))?;
980 let mut state = self.sheet_state();
981 let sheet = state.find_sheet_mut(id).ok_or_else(|| format!("no sheet '{id}'"))?;
982 sheet.apply(¶ms)?;
983 self.write_sheet_state(state, None);
984 Ok(())
985 }
986
987 pub fn sheet_add_revision(
994 &mut self,
995 id: &str,
996 rev: Option<&str>,
997 date: Option<&str>,
998 description: Option<&str>,
999 ) -> Result<usize, String> {
1000 let mut state = self.sheet_state();
1001 let sheet = state.find_sheet_mut(id).ok_or_else(|| format!("no sheet '{id}'"))?;
1002 let next = || {
1003 let letters = crate::sheets::SectionCut::LETTERS;
1004 let last = sheet.revisions.last().and_then(|row| row.rev.trim().chars().next());
1005 match last.and_then(|c| letters.iter().position(|l| l.eq_ignore_ascii_case(&c))) {
1006 Some(at) => letters[(at + 1).min(letters.len() - 1)].to_string(),
1007 None if sheet.revisions.is_empty() => letters[0].to_string(),
1008 None => String::new(),
1009 }
1010 };
1011 let revision = crate::sheets::Revision {
1012 rev: rev.map(str::to_string).unwrap_or_else(next),
1013 date: date.map(str::to_string).unwrap_or_else(today_iso),
1014 description: description.unwrap_or_default().to_string(),
1015 };
1016 sheet.revisions.push(revision);
1017 let index = sheet.revisions.len() - 1;
1018 self.write_sheet_state(state, None);
1019 Ok(index)
1020 }
1021
1022 pub fn sheet_update_revision(&mut self, id: &str, index: usize, params_json: &str) -> Result<(), String> {
1025 let params: Value = serde_json::from_str(params_json)
1026 .map_err(|error| format!("revision params: {error}"))?;
1027 let mut state = self.sheet_state();
1028 let sheet = state.find_sheet_mut(id).ok_or_else(|| format!("no sheet '{id}'"))?;
1029 let count = sheet.revisions.len();
1030 let row = sheet
1031 .revisions
1032 .get_mut(index)
1033 .ok_or_else(|| format!("sheet '{id}' has {count} revision row(s); there is no row {index}"))?;
1034 row.apply(¶ms)?;
1035 self.write_sheet_state(state, None);
1036 Ok(())
1037 }
1038
1039 pub fn sheet_remove_revision(&mut self, id: &str, index: usize) -> Result<(), String> {
1042 let mut state = self.sheet_state();
1043 let sheet = state.find_sheet_mut(id).ok_or_else(|| format!("no sheet '{id}'"))?;
1044 if index >= sheet.revisions.len() {
1045 return Err(format!(
1046 "sheet '{id}' has {} revision row(s); there is no row {index}",
1047 sheet.revisions.len()
1048 ));
1049 }
1050 sheet.revisions.remove(index);
1051 self.write_sheet_state(state, None);
1052 Ok(())
1053 }
1054
1055 pub fn sheet_delete(&mut self, id: &str) -> Result<(), String> {
1056 let mut state = self.sheet_state();
1057 let before = state.sheets.len();
1058 state.sheets.retain(|sheet| sheet.id != id);
1059 if state.sheets.len() == before {
1060 return Err(format!("no sheet '{id}'"));
1061 }
1062 if self.sheet_open.as_deref() == Some(id) {
1063 self.set_open_sheet(state.sheets.first().map(|sheet| sheet.id.clone()));
1064 }
1065 self.clear_open_object(&state);
1066 self.write_sheet_state(state, None);
1067 Ok(())
1068 }
1069
1070 pub fn sheet_set_open(&mut self, id: Option<&str>) -> Result<(), String> {
1072 match id {
1073 None => {
1074 self.set_open_sheet(None);
1075 self.dirty = true;
1076 Ok(())
1077 }
1078 Some(id) => {
1079 let state = self.sheet_state();
1080 if state.find_sheet(id).is_none() {
1081 return Err(format!("no sheet '{id}'"));
1082 }
1083 self.set_open_sheet(Some(id.to_string()));
1084 self.dirty = true;
1085 Ok(())
1086 }
1087 }
1088 }
1089
1090 pub fn sheet_set_object_open(&mut self, id: Option<&str>) {
1095 match id {
1096 Some(id) => self.open_sheet_object(id.to_string()),
1097 None => self.sheet_open_object = None,
1098 }
1099 }
1100
1101 pub fn sheet_place_view(
1107 &mut self,
1108 sheet_id: Option<&str>,
1109 view_id: &str,
1110 position: Option<[f64; 2]>,
1111 scale: Option<f64>,
1112 ) -> Result<String, String> {
1113 let pmi = self.pmi_state();
1114 let view = pmi
1115 .find_view(view_id)
1116 .ok_or_else(|| format!("no PMI view '{view_id}'"))?;
1117 let camera = view
1118 .camera
1119 .as_ref()
1120 .ok_or_else(|| format!("PMI view '{view_id}' has no captured camera — capture one first"))?;
1121 refuse_perspective(view_id, camera)?;
1122 let mut state = self.sheet_state();
1123 let sheet_id = match sheet_id.map(str::trim).filter(|id| !id.is_empty()) {
1124 Some(id) => id.to_string(),
1125 None => self.resolve_sheet("")?,
1126 };
1127 let id = state.next_id("SV");
1128 let sheet = state
1129 .find_sheet_mut(&sheet_id)
1130 .ok_or_else(|| format!("no sheet '{sheet_id}'"))?;
1131 let (width_mm, height_mm) = sheet.millimetres();
1132 let scale = match scale {
1133 Some(scale) if scale.is_finite() && scale > 0.0 => scale,
1134 Some(scale) => return Err(format!("scale: {scale} is not a positive ratio")),
1135 None => DEFAULT_SCALE,
1136 };
1137 sheet.views.push(PlacedView {
1138 id: id.clone(),
1139 view: view_id.to_string(),
1140 position: position.unwrap_or([width_mm * 0.5, height_mm * 0.5]),
1141 scale,
1142 projection: ORTHOGRAPHIC.to_string(),
1143 flatten_text: true,
1144 three_d: false,
1145 section: None,
1146 detail: None,
1147 });
1148 self.write_sheet_state(state, None);
1149 self.set_open_sheet(Some(sheet_id));
1150 self.open_sheet_object(id.clone());
1151 Ok(id)
1152 }
1153
1154 pub fn sheet_update_view(&mut self, id: &str, params_json: &str) -> Result<(), String> {
1156 let mut params: Value = serde_json::from_str(params_json)
1157 .map_err(|error| format!("placed view params: {error}"))?;
1158 if let Some(object) = params.as_object_mut() {
1163 let current = self.sheet_state().sheets.iter().find_map(|sheet| sheet.find_view(id).cloned());
1164 for field in ["cut", "centre", "rim"] {
1165 let Some(value) = object.remove(field) else { continue };
1166 let names: Vec<String> = match value.as_str() {
1168 Some(one) => vec![one.to_string()],
1169 None => crate::json_support::string_values(Some(&value)).map(str::to_string).collect(),
1170 }
1171 .into_iter()
1172 .filter(|name| !name.is_empty())
1173 .collect();
1174 let now = current.as_ref().map(|placed| placed.picked(field)).unwrap_or_default();
1175 if names != now {
1176 self.sheet_set_placement_refs(id, field, &names)?;
1177 }
1178 }
1179 }
1180 if let Some(view_id) = params.get("view").and_then(Value::as_str).map(str::trim).filter(|id| !id.is_empty()) {
1183 if let Some(camera) = self.pmi_state().find_view(view_id).and_then(|view| view.camera.clone()) {
1184 refuse_perspective(view_id, &camera)?;
1185 }
1186 }
1187 let views = self.sheet_pmi_view_ids();
1188 let mut state = self.sheet_state();
1189 let view = state.find_view_mut(id).ok_or_else(|| format!("no placed view '{id}'"))?;
1190 view.apply(¶ms, &views)?;
1191 self.write_sheet_state(state, None);
1192 Ok(())
1193 }
1194
1195 pub fn sheet_move_view(&mut self, id: &str, position: [f64; 2], coalesce: bool) -> Result<(), String> {
1198 if !position[0].is_finite() || !position[1].is_finite() {
1199 return Err("position: not a finite point".into());
1200 }
1201 let mut state = self.sheet_state();
1202 let view = state.find_view_mut(id).ok_or_else(|| format!("no placed view '{id}'"))?;
1203 view.position = position;
1204 let key = format!("sheet:view:{id}");
1205 self.write_sheet_state(state, coalesce.then_some(key.as_str()));
1206 Ok(())
1207 }
1208
1209 pub fn sheet_move_view_end(&mut self) {
1211 self.history.break_coalescing();
1212 }
1213
1214 pub fn sheet_remove_view(&mut self, id: &str) -> Result<(), String> {
1215 let mut state = self.sheet_state();
1216 let Some((sheet_id, index)) = state.locate_view(id) else {
1217 return Err(format!("no placed view '{id}'"));
1218 };
1219 if let Some(sheet) = state.find_sheet_mut(&sheet_id) {
1220 sheet.views.remove(index);
1221 }
1222 self.clear_open_object(&state);
1223 self.write_sheet_state(state, None);
1224 Ok(())
1225 }
1226
1227 fn clear_open_object(&mut self, state: &SheetState) {
1229 if let Some(open) = self.sheet_open_object.clone() {
1230 if state.find_sheet(&open).is_none()
1231 && state.locate_view(&open).is_none()
1232 && state.locate_dimension(&open).is_none()
1233 && state.locate_ordinate(&open).is_none()
1234 {
1235 self.sheet_open_object = None;
1236 }
1237 }
1238 }
1239
1240 pub fn sheet_add_dimension(
1247 &mut self,
1248 sheet_id: Option<&str>,
1249 kind: &str,
1250 alignment: Option<&str>,
1251 anchors: Vec<String>,
1252 ) -> Result<String, String> {
1253 let kind = KINDS
1254 .iter()
1255 .find(|known| known.eq_ignore_ascii_case(kind.trim()))
1256 .ok_or_else(|| format!("no dimension kind '{kind}'"))?;
1257 let alignment = match alignment.map(str::trim).filter(|a| !a.is_empty()) {
1258 None => ALIGNED,
1259 Some(wanted) => ALIGNMENTS
1260 .iter()
1261 .copied()
1262 .find(|known| known.eq_ignore_ascii_case(wanted))
1263 .ok_or_else(|| format!("no alignment '{wanted}'"))?,
1264 };
1265 for anchor in &anchors {
1266 SheetAnchor::parse(anchor)?;
1267 }
1268 let sheet_id = match sheet_id.map(str::trim).filter(|id| !id.is_empty()) {
1269 Some(id) => id.to_string(),
1270 None => self.resolve_sheet("")?,
1271 };
1272 let mut state = self.sheet_state();
1273 let id = state.next_id("SD");
1274 let sheet = state
1275 .find_sheet_mut(&sheet_id)
1276 .ok_or_else(|| format!("no sheet '{sheet_id}'"))?;
1277 sheet
1278 .dimensions
1279 .push(SheetDimension::new(id.clone(), kind, alignment, anchors));
1280 self.write_sheet_state(state, None);
1281 self.set_open_sheet(Some(sheet_id));
1282 self.open_sheet_object(id.clone());
1283 Ok(id)
1284 }
1285
1286 pub fn sheet_update_dimension(&mut self, id: &str, params_json: &str) -> Result<(), String> {
1288 let params: Value = serde_json::from_str(params_json)
1289 .map_err(|error| format!("sheet dimension params: {error}"))?;
1290 let mut state = self.sheet_state();
1291 let dimension = state
1292 .find_dimension_mut(id)
1293 .ok_or_else(|| format!("no sheet dimension '{id}'"))?;
1294 dimension.apply(¶ms)?;
1295 self.write_sheet_state(state, None);
1296 Ok(())
1297 }
1298
1299 pub fn sheet_move_dimension(
1303 &mut self,
1304 id: &str,
1305 offset_mm: f64,
1306 coalesce: bool,
1307 ) -> Result<(), String> {
1308 if !offset_mm.is_finite() {
1309 return Err(format!("offsetMm: {offset_mm} is not a length"));
1310 }
1311 let mut state = self.sheet_state();
1312 let dimension = state
1313 .find_dimension_mut(id)
1314 .ok_or_else(|| format!("no sheet dimension '{id}'"))?;
1315 dimension.offset_mm = offset_mm;
1316 let key = format!("sheet:dim:{id}");
1317 self.write_sheet_state(state, coalesce.then_some(key.as_str()));
1318 Ok(())
1319 }
1320
1321 pub fn sheet_move_dimension_end(&mut self) {
1323 self.history.break_coalescing();
1324 }
1325
1326 pub fn sheet_remove_dimension(&mut self, id: &str) -> Result<(), String> {
1327 let mut state = self.sheet_state();
1328 let Some((sheet_id, index)) = state.locate_dimension(id) else {
1329 return Err(format!("no sheet dimension '{id}'"));
1330 };
1331 if let Some(sheet) = state.find_sheet_mut(&sheet_id) {
1332 sheet.dimensions.remove(index);
1333 }
1334 self.clear_open_object(&state);
1335 self.write_sheet_state(state, None);
1336 Ok(())
1337 }
1338
1339 pub fn sheet_add_ordinate(
1345 &mut self,
1346 sheet_id: Option<&str>,
1347 axis: &str,
1348 datum: Option<&str>,
1349 members: Vec<String>,
1350 ) -> Result<String, String> {
1351 let axis = AXES
1352 .iter()
1353 .find(|known| known.eq_ignore_ascii_case(axis.trim()))
1354 .ok_or_else(|| format!("no ordinate axis '{axis}'"))?;
1355 let datum = datum.map(str::trim).filter(|d| !d.is_empty()).unwrap_or_default();
1356 if !datum.is_empty() {
1357 SheetAnchor::parse(datum)?;
1358 }
1359 for member in &members {
1360 SheetAnchor::parse(member)?;
1361 }
1362 let sheet_id = match sheet_id.map(str::trim).filter(|id| !id.is_empty()) {
1363 Some(id) => id.to_string(),
1364 None => self.resolve_sheet("")?,
1365 };
1366 let mut state = self.sheet_state();
1367 let id = state.next_id("OD");
1368 let sheet = state
1369 .find_sheet_mut(&sheet_id)
1370 .ok_or_else(|| format!("no sheet '{sheet_id}'"))?;
1371 let mut set = SheetOrdinate::new(id.clone(), axis, datum.to_string());
1372 set.members = members;
1373 sheet.ordinates.push(set);
1374 self.write_sheet_state(state, None);
1375 self.set_open_sheet(Some(sheet_id));
1376 self.open_sheet_object(id.clone());
1377 Ok(id)
1378 }
1379
1380 pub fn sheet_update_ordinate(&mut self, id: &str, params_json: &str) -> Result<(), String> {
1387 let params: Value = serde_json::from_str(params_json)
1388 .map_err(|error| format!("ordinate set params: {error}"))?;
1389 let mut state = self.sheet_state();
1390 let set = state
1391 .find_ordinate_mut(id)
1392 .ok_or_else(|| format!("no ordinate set '{id}'"))?;
1393 set.apply(¶ms)?;
1394 self.write_sheet_state(state, None);
1395 Ok(())
1396 }
1397
1398 pub fn sheet_move_ordinate(
1402 &mut self,
1403 id: &str,
1404 offset_mm: f64,
1405 coalesce: bool,
1406 ) -> Result<(), String> {
1407 if !offset_mm.is_finite() {
1408 return Err(format!("offsetMm: {offset_mm} is not a length"));
1409 }
1410 let mut state = self.sheet_state();
1411 let set = state
1412 .find_ordinate_mut(id)
1413 .ok_or_else(|| format!("no ordinate set '{id}'"))?;
1414 set.offset_mm = offset_mm;
1415 let key = format!("sheet:ord:{id}");
1416 self.write_sheet_state(state, coalesce.then_some(key.as_str()));
1417 Ok(())
1418 }
1419
1420 pub fn sheet_move_ordinate_end(&mut self) {
1422 self.history.break_coalescing();
1423 }
1424
1425 pub fn sheet_remove_ordinate(&mut self, id: &str) -> Result<(), String> {
1426 let mut state = self.sheet_state();
1427 let Some((sheet_id, index)) = state.locate_ordinate(id) else {
1428 return Err(format!("no ordinate set '{id}'"));
1429 };
1430 if let Some(sheet) = state.find_sheet_mut(&sheet_id) {
1431 sheet.ordinates.remove(index);
1432 }
1433 self.clear_open_object(&state);
1434 self.write_sheet_state(state, None);
1435 Ok(())
1436 }
1437
1438 pub fn sheet_ordinate_offset_at(&mut self, id: &str, at: [f64; 2]) -> Option<f64> {
1445 let sheet_id = self.sheet_state().locate_ordinate(id).map(|(sheet, _)| sheet)?;
1446 let vertical = self
1447 .sheet_state()
1448 .find_ordinate(id)?
1449 .axis
1450 .eq_ignore_ascii_case(crate::sheets::dimension::VERTICAL);
1451 let drawing = self.sheet_drawing(&sheet_id)?;
1452 let drawn = drawing.ordinates.iter().find(|o| o.id == id)?;
1453 if !drawn.error.is_empty() {
1454 return None;
1455 }
1456 let datum = drawn.stations.iter().find(|station| station.datum)?;
1457 if !datum.error.is_empty() {
1458 return None;
1459 }
1460 Some(if vertical { at[0] - datum.at[0] } else { at[1] - datum.at[1] })
1461 }
1462
1463 pub fn sheet_add_section(
1473 &mut self,
1474 source: &str,
1475 from: &str,
1476 to: &str,
1477 position: Option<[f64; 2]>,
1478 ) -> Result<String, String> {
1479 SheetAnchor::parse(from)?;
1480 SheetAnchor::parse(to)?;
1481 if from == to {
1482 return Err("a cutting line needs two different anchors".into());
1483 }
1484 let (a, b) = (SheetAnchor::parse(from)?, SheetAnchor::parse(to)?);
1487 if a.view != b.view {
1488 return Err(format!(
1489 "'{from}' is on '{}' and '{to}' on '{}'; a cutting line is drawn on ONE view",
1490 a.view, b.view
1491 ));
1492 }
1493 if a.view != source {
1494 return Err(format!("the cutting line is on '{}', not on '{source}'", a.view));
1495 }
1496 let mut state = self.sheet_state();
1497 let Some((sheet_id, _)) = state.locate_view(source) else {
1498 return Err(format!("no placed view '{source}'"));
1499 };
1500 let (view, scale, at, label) = {
1501 let sheet = state
1502 .find_sheet(&sheet_id)
1503 .ok_or_else(|| format!("no sheet '{sheet_id}'"))?;
1504 let label = sheet.next_view_letter();
1505 let parent = sheet
1506 .find_view(source)
1507 .ok_or_else(|| format!("no placed view '{source}'"))?;
1508 if parent.section.is_some() {
1509 return Err(format!(
1510 "'{source}' is itself a section; a section of a section is not drawn"
1511 ));
1512 }
1513 if parent.detail.is_some() {
1514 return Err(format!("'{source}' is a detail; a section of a detail is not drawn"));
1515 }
1516 (parent.view.clone(), parent.scale, parent.position, label)
1517 };
1518 let id = state.next_id("SV");
1519 let sheet = state.find_sheet_mut(&sheet_id).expect("just found");
1520 sheet.views.push(PlacedView {
1521 id: id.clone(),
1522 view,
1523 position: position.unwrap_or([at[0] + 120.0, at[1]]),
1527 scale,
1528 projection: ORTHOGRAPHIC.to_string(),
1529 flatten_text: true,
1530 three_d: false,
1531 section: Some(SectionCut {
1532 source: source.to_string(),
1533 from: from.to_string(),
1534 to: to.to_string(),
1535 label,
1536 flip: false,
1537 }),
1538 detail: None,
1539 });
1540 self.write_sheet_state(state, None);
1541 self.set_open_sheet(Some(sheet_id));
1542 self.open_sheet_object(id.clone());
1543 Ok(id)
1544 }
1545
1546 pub fn sheet_new_section(&mut self, sheet_id: Option<&str>) -> Result<String, String> {
1553 self.sheet_new_derived(sheet_id, true)
1554 }
1555
1556 pub fn sheet_new_detail(&mut self, sheet_id: Option<&str>) -> Result<String, String> {
1560 self.sheet_new_derived(sheet_id, false)
1561 }
1562
1563 fn sheet_new_derived(&mut self, sheet_id: Option<&str>, section: bool) -> Result<String, String> {
1564 let sheet_id = match sheet_id.map(str::trim).filter(|id| !id.is_empty()) {
1565 Some(id) => id.to_string(),
1566 None => self.resolve_sheet("")?,
1567 };
1568 let mut state = self.sheet_state();
1569 let id = state.next_id("SV");
1570 let sheet = state
1571 .find_sheet_mut(&sheet_id)
1572 .ok_or_else(|| format!("no sheet '{sheet_id}'"))?;
1573 let label = sheet.next_view_letter();
1574 let (w, h) = sheet.millimetres();
1575 sheet.views.push(PlacedView {
1576 id: id.clone(),
1577 view: String::new(),
1580 position: [w * 0.5, h * 0.5],
1581 scale: DEFAULT_SCALE,
1582 projection: ORTHOGRAPHIC.to_string(),
1583 flatten_text: true,
1584 three_d: false,
1585 section: section.then(|| SectionCut {
1586 source: String::new(),
1587 from: String::new(),
1588 to: String::new(),
1589 label: label.clone(),
1590 flip: false,
1591 }),
1592 detail: (!section).then(|| DetailCircle {
1593 source: String::new(),
1594 centre: String::new(),
1595 rim: String::new(),
1596 label,
1597 }),
1598 });
1599 self.write_sheet_state(state, None);
1600 self.set_open_sheet(Some(sheet_id));
1601 self.open_sheet_object(id.clone());
1602 Ok(id)
1603 }
1604
1605 pub fn sheet_set_placement_refs(&mut self, id: &str, field: &str, names: &[String]) -> Result<(), String> {
1618 for name in names {
1619 SheetAnchor::parse(name)?;
1620 }
1621 let mut state = self.sheet_state();
1622 let (sheet_id, _) = state.locate_view(id).ok_or_else(|| format!("no placed view '{id}'"))?;
1623 let sheet = state.find_sheet(&sheet_id).ok_or_else(|| format!("no sheet '{sheet_id}'"))?;
1624 let placed = sheet.find_view(id).ok_or_else(|| format!("no placed view '{id}'"))?.clone();
1625 let single = |names: &[String]| -> Result<String, String> {
1626 match names {
1627 [] => Ok(String::new()),
1628 [one] => Ok(one.clone()),
1629 _ => Err(format!("'{field}' is one anchor")),
1630 }
1631 };
1632 let anchors: Vec<String> = match (field, &placed.section, &placed.detail) {
1634 ("cut", Some(_), _) => {
1635 if names.len() > 2 {
1636 return Err("a cutting line is two anchors".into());
1637 }
1638 names.to_vec()
1639 }
1640 ("centre", _, Some(circle)) => vec![single(names)?, circle.rim.clone()],
1641 ("rim", _, Some(circle)) => vec![circle.centre.clone(), single(names)?],
1642 _ => return Err(format!("'{id}' has no {field} to pick")),
1643 };
1644 let picked: Vec<&String> = anchors.iter().filter(|a| !a.is_empty()).collect();
1645 if picked.len() == 2 && picked[0] == picked[1] {
1646 return Err(if placed.section.is_some() {
1647 "a cutting line needs two different anchors".into()
1648 } else {
1649 "a detail circle needs a radius: pick a rim point away from its centre".into()
1650 });
1651 }
1652 let on: Vec<String> = picked
1653 .iter()
1654 .map(|anchor| SheetAnchor::parse(anchor).map(|a| a.view))
1655 .collect::<Result<_, _>>()?;
1656 if on.windows(2).any(|pair| pair[0] != pair[1]) {
1657 return Err(format!(
1658 "'{}' is on '{}' and '{}' on '{}'; {} is drawn on ONE view",
1659 picked[0], on[0], picked[1], on[1],
1660 if placed.section.is_some() { "a cutting line" } else { "a detail circle" }
1661 ));
1662 }
1663 let source = on.first().cloned();
1664 let parent = match &source {
1665 Some(source) => {
1666 if source == id {
1667 return Err(format!("'{id}' cannot be drawn on itself"));
1668 }
1669 let parent = sheet
1670 .find_view(source)
1671 .ok_or_else(|| format!("no placement '{source}' on this sheet"))?;
1672 let what = if placed.section.is_some() { "section" } else { "detail" };
1673 if parent.section.is_some() {
1674 return Err(format!("'{source}' is a section; a {what} of a section is not drawn"));
1675 }
1676 if parent.detail.is_some() {
1677 return Err(format!("'{source}' is a detail; a {what} of a detail is not drawn"));
1678 }
1679 Some((parent.view.clone(), parent.scale, parent.position))
1680 }
1681 None => None,
1682 };
1683 let sheet = state.find_sheet_mut(&sheet_id).expect("just found");
1684 let placed = sheet.views.iter_mut().find(|view| view.id == id).expect("just found");
1685 let source = source.unwrap_or_default();
1686 if let Some(cut) = placed.section.as_mut() {
1687 cut.from = anchors.first().cloned().unwrap_or_default();
1688 cut.to = anchors.get(1).cloned().unwrap_or_default();
1689 cut.source = source;
1690 } else if let Some(circle) = placed.detail.as_mut() {
1691 circle.centre = anchors[0].clone();
1692 circle.rim = anchors[1].clone();
1693 circle.source = source;
1694 }
1695 if let Some((view, scale, at)) = parent {
1696 if placed.view.is_empty() {
1697 placed.view = view;
1698 placed.scale = if placed.detail.is_some() { scale * DETAIL_SCALE_FACTOR } else { scale };
1699 placed.position = [at[0] + 120.0, at[1]];
1700 }
1701 }
1702 self.write_sheet_state(state, None);
1703 Ok(())
1704 }
1705
1706 pub fn begin_ref_select_for_sheet(
1714 &mut self,
1715 id: &str,
1716 path: Vec<String>,
1717 label: String,
1718 filter: Vec<String>,
1719 multiple: bool,
1720 seed_names: Vec<String>,
1721 ) -> Result<(), String> {
1722 let state = self.sheet_state();
1723 let sheet = state
1724 .locate_view(id)
1725 .or_else(|| state.locate_dimension(id))
1726 .or_else(|| state.locate_ordinate(id))
1727 .map(|(sheet, _)| sheet)
1728 .ok_or_else(|| format!("no sheet object '{id}'"))?;
1729 self.set_open_sheet(Some(sheet));
1730 self.ref_select = Some(RefSelectState {
1731 feature_id: id.to_string(),
1732 path,
1733 label,
1734 filter,
1735 multiple,
1736 names: seed_names,
1737 restore_index: self.history.rollback(),
1738 target: RefSelectTarget::Sheet,
1739 });
1740 self.dirty = true;
1741 Ok(())
1742 }
1743
1744 fn sheet_ref_capacity(&self, id: &str, field: &str) -> Option<usize> {
1747 let state = self.sheet_state();
1748 match field {
1749 "anchors" => state
1750 .find_dimension(id)
1751 .map(|dimension| crate::sheets::dimension::anchor_count(&dimension.kind)),
1752 "cut" => Some(2),
1753 _ => None,
1754 }
1755 }
1756
1757 pub fn ref_select_pick_sheet_anchor(&mut self, anchor: &str) -> Result<(), String> {
1763 SheetAnchor::parse(anchor)?;
1764 let Some(state) = self.ref_select.as_ref().filter(|s| s.target == RefSelectTarget::Sheet) else {
1765 return Err("no sheet reference is being picked".into());
1766 };
1767 let capacity = state
1768 .path
1769 .last()
1770 .and_then(|field| self.sheet_ref_capacity(&state.feature_id, field));
1771 let state = self.ref_select.as_mut().expect("guarded above");
1772 if !state.multiple {
1773 state.names = vec![anchor.to_string()];
1774 } else if !state.names.iter().any(|name| name == anchor) {
1775 if capacity.is_some_and(|cap| cap > 0 && state.names.len() >= cap) {
1776 state.names.remove(0);
1777 }
1778 state.names.push(anchor.to_string());
1779 }
1780 self.dirty = true;
1781 Ok(())
1782 }
1783
1784 pub(crate) fn sheet_commit_refs(&mut self, id: &str, path: &[String], names: &[String], multiple: bool) -> Result<(), String> {
1786 let field = path.last().map(String::as_str).unwrap_or_default();
1787 let value = if multiple {
1788 Value::Array(names.iter().cloned().map(Value::String).collect())
1789 } else {
1790 Value::String(names.first().cloned().unwrap_or_default())
1791 };
1792 let params = serde_json::json!({ field: value }).to_string();
1793 let state = self.sheet_state();
1794 if state.find_dimension(id).is_some() {
1795 self.sheet_update_dimension(id, ¶ms)
1796 } else if state.find_ordinate(id).is_some() {
1797 self.sheet_update_ordinate(id, ¶ms)
1798 } else if state.locate_view(id).is_some() {
1799 self.sheet_set_placement_refs(id, field, names)
1800 } else {
1801 Err(format!("no sheet object '{id}'"))
1802 }
1803 }
1804
1805 pub fn sheet_add_detail(
1816 &mut self,
1817 source: &str,
1818 centre: &str,
1819 rim: &str,
1820 position: Option<[f64; 2]>,
1821 scale: Option<f64>,
1822 ) -> Result<String, String> {
1823 let (a, b) = (SheetAnchor::parse(centre)?, SheetAnchor::parse(rim)?);
1824 if centre == rim {
1825 return Err("a detail circle needs two different anchors: its centre and a point on its rim".into());
1826 }
1827 if a.view != b.view {
1830 return Err(format!(
1831 "'{centre}' is on '{}' and '{rim}' on '{}'; a detail circle is drawn on ONE view",
1832 a.view, b.view
1833 ));
1834 }
1835 if a.view != source {
1836 return Err(format!("the detail circle is on '{}', not on '{source}'", a.view));
1837 }
1838 if let Some(scale) = scale {
1839 if !scale.is_finite() || scale <= 0.0 {
1840 return Err(format!("scale: {scale} is not a positive ratio"));
1841 }
1842 }
1843 let mut state = self.sheet_state();
1844 let Some((sheet_id, _)) = state.locate_view(source) else {
1845 return Err(format!("no placed view '{source}'"));
1846 };
1847 let (view, parent_scale, at, label) = {
1848 let sheet = state
1849 .find_sheet(&sheet_id)
1850 .ok_or_else(|| format!("no sheet '{sheet_id}'"))?;
1851 let label = sheet.next_view_letter();
1852 let parent = sheet
1853 .find_view(source)
1854 .ok_or_else(|| format!("no placed view '{source}'"))?;
1855 if parent.detail.is_some() {
1859 return Err(format!("'{source}' is itself a detail; a detail of a detail is not drawn"));
1860 }
1861 if parent.section.is_some() {
1862 return Err(format!("'{source}' is a section; a detail of a section is not drawn"));
1863 }
1864 (parent.view.clone(), parent.scale, parent.position, label)
1865 };
1866 let id = state.next_id("SV");
1867 let sheet = state.find_sheet_mut(&sheet_id).expect("just found");
1868 sheet.views.push(PlacedView {
1869 id: id.clone(),
1870 view,
1871 position: position.unwrap_or([at[0] + 120.0, at[1]]),
1874 scale: scale.unwrap_or(parent_scale * DETAIL_SCALE_FACTOR),
1875 projection: ORTHOGRAPHIC.to_string(),
1876 flatten_text: true,
1877 three_d: false,
1878 section: None,
1879 detail: Some(DetailCircle {
1880 source: source.to_string(),
1881 centre: centre.to_string(),
1882 rim: rim.to_string(),
1883 label,
1884 }),
1885 });
1886 self.write_sheet_state(state, None);
1887 self.set_open_sheet(Some(sheet_id));
1888 self.open_sheet_object(id.clone());
1889 Ok(id)
1890 }
1891
1892 pub fn sheet_dimension_offset_at(&mut self, id: &str, at: [f64; 2]) -> Option<f64> {
1902 let sheet_id = self.sheet_state().locate_dimension(id).map(|(sheet, _)| sheet)?;
1903 let drawing = self.sheet_drawing(&sheet_id)?;
1904 let drawn = drawing.dimensions.iter().find(|d| d.id == id)?;
1905 if !drawn.error.is_empty() {
1906 return None;
1907 }
1908 let (label, offset) = (drawn.label, drawn.offset_mm);
1909 let direction = self.sheet_dimension_direction(&sheet_id, id)?;
1914 let base = [label[0] - direction[0] * offset, label[1] - direction[1] * offset];
1915 Some((at[0] - base[0]) * direction[0] + (at[1] - base[1]) * direction[1])
1916 }
1917
1918 fn sheet_dimension_direction(&mut self, sheet_id: &str, id: &str) -> Option<[f64; 2]> {
1920 let state = self.sheet_state();
1924 let sheet = state.find_sheet(sheet_id)?;
1925 let mut probe = sheet.dimensions.iter().find(|d| d.id == id)?.clone();
1926 let delta = if probe.offset_mm < 0.0 { -1.0 } else { 1.0 };
1931 probe.offset_mm += delta;
1932 let drawing = self.sheet_drawing_cached(sheet_id)?;
1933 let moved = crate::sheets::dimension::layout_dimensions(
1934 std::slice::from_ref(&probe),
1935 &drawing.views,
1936 [drawing.width_mm * 0.5, drawing.height_mm * 0.5],
1937 );
1938 let here = drawing.dimensions.iter().find(|d| d.id == id)?;
1939 let there = moved.first()?;
1940 if !there.error.is_empty() {
1941 return None;
1942 }
1943 let step = [
1944 (there.label[0] - here.label[0]) / delta,
1945 (there.label[1] - here.label[1]) / delta,
1946 ];
1947 let len = (step[0] * step[0] + step[1] * step[1]).sqrt();
1948 (len > 1e-9).then(|| [step[0] / len, step[1] / len])
1949 }
1950
1951 pub(crate) fn forget_sheets(&mut self) {
1953 self.sheet_open = None;
1954 self.sheet_open_object = None;
1955 self.sheet_selected_object = None;
1956 self.sheet_cache = None;
1957 self.sheet_lines.forget();
1958 }
1959}
1960
1961fn refuse_perspective(view_id: &str, camera: &brep_kernel::PmiCamera) -> Result<(), String> {
1965 if matches!(camera.projection, brep_kernel::PmiProjection::Orthographic { .. }) {
1966 Ok(())
1967 } else {
1968 Err(format!("PMI view '{view_id}' is a perspective camera; a sheet places {ORTHOGRAPHIC} views only"))
1969 }
1970}
1971
1972
1973
1974
1975