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 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 let drawing_json = serde_json::to_string(&drawing).unwrap_or_else(|_| "null".into());
367 let landed = self.sheet_cache_key(id, &context);
373 let reproject = !complete && landed != key;
374 let key = landed;
375 self.sheet_cache = Some(SheetCache { key, drawing, drawing_json, complete, reproject });
376 }
377 true
378 }
379
380 fn sheet_env(&self) -> brep_kernel::Env {
383 brep_kernel::Env::build(&self.history.expressions(), &self.history.configurator()).unwrap_or_default()
384 }
385
386 pub fn pump_sheet_lines(&mut self) {
388 self.sheet_lines.pump(self.runner.as_mut());
389 }
390
391 pub fn sheet_lines_pending(&self) -> bool {
394 self.sheet_lines.pending()
395 }
396
397 fn request_sheet_topology(&mut self, sheet: &crate::sheets::Sheet) {
402 let lacking = self.scene.solids().iter().any(|solid| {
404 solid.source_handle != 0
405 && !solid.is_sketch
406 && !self.topology_asked.contains(&solid.source_handle)
407 && self.scene.exact_solid(&solid.name).is_none()
408 });
409 if !lacking {
410 self.pump_topology();
411 return;
412 }
413 let pmi = self.pmi_state();
414 let names = crate::sheets::project::sheet_solid_names(&self.scene, sheet, &pmi.views);
415 self.request_exact_solids(names.iter().map(String::as_str));
416 }
417
418 fn sheet_frame_context(&self) -> FrameContext {
421 FrameContext { document: self.document_name.clone(), date: today_iso() }
422 }
423
424 fn sheet_cache_key(
425 &self,
426 id: &str,
427 context: &FrameContext,
428 ) -> (String, u64, u64, u64, u64, u64, String) {
429 (
430 id.to_string(),
431 self.applied_generation,
432 self.history.revision(),
433 self.scene.exact_revision(),
434 self.scene.visibility_revision(),
439 self.sheet_lines.revision,
440 format!("{}\u{1f}{}", context.document, context.date),
441 )
442 }
443
444 pub fn sheet_drawing_cached(&self, id: &str) -> Option<&SheetDrawing> {
448 let key = self.sheet_cache_key(id, &self.sheet_frame_context());
449 self.sheet_cache
450 .as_ref()
451 .filter(|cache| cache.key == key)
452 .map(|cache| &cache.drawing)
453 }
454
455 pub fn sheet_drawing(&mut self, id: &str) -> Option<&SheetDrawing> {
460 if !self.sheet_project(id) {
461 return None;
462 }
463 self.sheet_cache.as_ref().map(|cache| &cache.drawing)
464 }
465
466 pub fn export_sheet_svg(&mut self, id: &str) -> Result<String, String> {
468 let id = self.resolve_sheet(id)?;
469 let drawing = self
470 .sheet_drawing_exact(&id)
471 .ok_or_else(|| format!("no sheet '{id}'"))?;
472 Ok(crate::sheets::svg::to_svg(drawing))
473 }
474
475 pub fn sheet_drawing_exact(&mut self, id: &str) -> Option<&SheetDrawing> {
480 if !self.sheet_project(id) {
481 return None;
482 }
483 if self.sheet_cache.as_ref().is_some_and(|cache| !cache.complete) {
484 let state = self.sheet_state();
485 let sheet = state.find_sheet(id)?;
486 let pmi = self.pmi_state();
487 let env = self.sheet_env();
488 let model = Model { scene: &self.scene, unposed: &self.pmi_explode_originals, env: &env };
489 let drawing = project_sheet_with(
490 &model,
491 sheet,
492 &pmi.views,
493 self.pmi_report.as_ref(),
494 &self.sheet_frame_context(),
495 &mut LinesMode::Compute,
496 );
497 let drawing_json = serde_json::to_string(&drawing).unwrap_or_else(|_| "null".into());
498 if let Some(cache) = self.sheet_cache.as_mut() {
499 cache.drawing = drawing;
500 cache.drawing_json = drawing_json;
501 cache.complete = true;
502 cache.reproject = false;
503 }
504 }
505 self.sheet_cache.as_ref().map(|cache| &cache.drawing)
506 }
507
508 pub fn export_sheet_pdf(&mut self, id: &str) -> Result<Vec<u8>, String> {
512 let id = self.resolve_sheet(id)?;
513 let name = self.sheet_state().find_sheet(&id).map(|sheet| sheet.name.clone()).unwrap_or_default();
514 self.export_sheets_pdf(&[id], &name)
515 }
516
517 pub fn export_document_pdf(&mut self) -> Result<Vec<u8>, String> {
523 self.export_document_pdf_with(false)
524 }
525
526 pub fn export_document_pdf_3d(&mut self) -> Result<Vec<u8>, String> {
530 self.export_document_pdf_with(true)
531 }
532
533 fn export_document_pdf_with(&mut self, three_d: bool) -> Result<Vec<u8>, String> {
534 let state = self.sheet_state();
535 if state.sheets.is_empty() {
536 return Err("the document has no sheets".into());
537 }
538 let ids: Vec<String> = state.sheets.iter().map(|sheet| sheet.id.clone()).collect();
539 let title = if self.document_name.trim().is_empty() {
540 state.sheets[0].name.clone()
541 } else {
542 self.document_name.clone()
543 };
544 self.export_sheets_pdf_with(&ids, &title, three_d)
545 }
546
547 pub fn export_sheets_pdf(&mut self, ids: &[String], title: &str) -> Result<Vec<u8>, String> {
554 self.export_sheets_pdf_with(ids, title, false)
555 }
556
557 pub fn export_sheets_pdf_with(&mut self, ids: &[String], title: &str, three_d: bool) -> Result<Vec<u8>, String> {
562 let state = self.sheet_state();
563 for id in ids {
564 if let Some(sheet) = state.find_sheet(id) {
565 self.request_sheet_topology(sheet);
566 }
567 }
568 let context = self.sheet_frame_context();
569 let pmi = self.pmi_state();
570 let mut drawings: Vec<SheetDrawing> = Vec::with_capacity(ids.len());
571 for id in ids {
572 let sheet = state.find_sheet(id).ok_or_else(|| format!("no sheet '{id}'"))?;
573 let complete = self.sheet_cache.as_ref().is_some_and(|cache| cache.complete);
574 let drawing = match self.sheet_drawing_cached(id).filter(|_| complete) {
575 Some(cached) => cached.clone(),
576 None => project_sheet_with(
577 &Model { scene: &self.scene, unposed: &self.pmi_explode_originals, env: &self.sheet_env() },
578 sheet,
579 &pmi.views,
580 self.pmi_report.as_ref(),
581 &context,
582 &mut LinesMode::Compute,
583 ),
584 };
585 drawings.push(drawing);
586 }
587 let pages: Vec<&SheetDrawing> = drawings.iter().collect();
588 if !three_d {
589 return Ok(crate::sheets::pdf::write_pdf(title, &pages));
590 }
591 let three_d = self.pdf3d_content(ids, &drawings, title)?;
592 Ok(crate::sheets::pdf::write_pdf_3d(title, &pages, Some(&three_d)))
593 }
594
595 fn pdf3d_content(
599 &self,
600 ids: &[String],
601 drawings: &[SheetDrawing],
602 title: &str,
603 ) -> Result<crate::sheets::pdf3d::Pdf3d, String> {
604 use crate::sheets::pdf3d::{self, Page3d, Placement3d};
605 let state = self.sheet_state();
606 let pmi = self.pmi_state();
607 let bodies = self.pdf3d_bodies();
608 let model = pdf3d::build(&bodies, &pmi, self.pmi_report.as_ref())?;
609 const MARGIN_MM: f64 = 4.0;
612 let mut placements = Vec::new();
613 for (page, (id, drawing)) in ids.iter().zip(drawings).enumerate() {
614 let Some(sheet) = state.find_sheet(id) else { continue };
615 for placed in sheet.views.iter().filter(|placed| placed.three_d) {
616 let Some(view) = drawing.views.iter().find(|view| view.id == placed.id) else { continue };
617 if !view.error.is_empty() || model.view(&placed.view).is_none() {
618 continue;
619 }
620 let [x0, y0, x1, y1] = view.bounds;
621 let p = placed.position;
622 let half = [
623 ((p[0] - x0).max(x1 - p[0]) + MARGIN_MM).max(10.0),
624 ((p[1] - y0).max(y1 - p[1]) + MARGIN_MM).max(10.0),
625 ];
626 placements.push(Placement3d {
627 page,
628 id: placed.id.clone(),
629 view: placed.view.clone(),
630 centre_mm: p,
631 half_mm: half,
632 scale: placed.scale,
633 });
634 }
635 }
636 let page = Some({
637 let (w, h) = state
638 .find_sheet(ids.first().map(String::as_str).unwrap_or(""))
639 .map(|sheet| sheet.millimetres())
640 .unwrap_or((297.0, 210.0));
641 let mut page = Page3d::layout(w.max(h), w.min(h), title, &model);
642 if let Some((poster, scale)) = self.pdf3d_poster(&model, &page, &pmi) {
643 page.poster = Some(poster);
644 page.poster_scale = Some(scale);
645 }
646 page
647 });
648 Ok(pdf3d::Pdf3d { model, placements, page })
649 }
650
651 fn pdf3d_poster(
657 &self,
658 model: &crate::sheets::pdf3d::Model3d,
659 page: &crate::sheets::pdf3d::Page3d,
660 pmi: &brep_kernel::PmiState,
661 ) -> Option<(crate::sheets::project::ViewDrawing, f64)> {
662 let default = model.views.get(page.default_view)?;
663 let mut camera = default.camera.clone();
664 if let brep_kernel::PmiProjection::Perspective { fov_y_deg } = camera.projection {
665 let distance = {
666 let d = [camera.target[0] - camera.eye[0], camera.target[1] - camera.eye[1], camera.target[2] - camera.eye[2]];
667 (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
668 };
669 camera.projection = brep_kernel::PmiProjection::Orthographic {
670 half_height: distance * (fov_y_deg.to_radians() * 0.5).tan(),
671 };
672 }
673 let mut views = pmi.views.clone();
674 match views.iter_mut().find(|view| view.id == default.id) {
675 Some(view) => view.camera = Some(camera),
676 None => views.push(brep_kernel::PmiView {
677 id: default.id.clone(),
678 name: default.name.clone(),
679 camera: Some(camera),
680 display: Default::default(),
681 annotations: Vec::new(),
682 }),
683 }
684 let centre = page.box_centre_mm();
685 let half = page.box_half_mm();
686 let project = |scale: f64| -> Option<crate::sheets::project::ViewDrawing> {
687 let mut sheet: crate::sheets::Sheet = serde_json::from_value(serde_json::json!({
688 "id": "MODEL3D",
689 "size": crate::sheets::CUSTOM,
690 "widthMm": page.width_mm,
691 "heightMm": page.height_mm,
692 "border": false,
693 "titleBlock": false,
694 }))
695 .ok()?;
696 sheet.views.push(PlacedView {
697 id: "MODEL3D".into(),
698 view: default.id.clone(),
699 position: centre,
700 scale,
701 projection: ORTHOGRAPHIC.to_string(),
702 flatten_text: true,
703 three_d: false,
704 section: None,
705 detail: None,
706 });
707 let drawing = project_sheet_with(
708 &Model { scene: &self.scene, unposed: &self.pmi_explode_originals, env: &self.sheet_env() },
709 &sheet,
710 &views,
711 self.pmi_report.as_ref(),
712 &self.sheet_frame_context(),
713 &mut LinesMode::Compute,
714 );
715 drawing.views.into_iter().next().filter(|view| view.error.is_empty())
716 };
717 let probe = project(1.0)?;
719 let [x0, y0, x1, y1] = probe.bounds;
720 let reach = [(centre[0] - x0).max(x1 - centre[0]), (centre[1] - y0).max(y1 - centre[1])];
721 if !(reach[0] > 1e-9 && reach[1] > 1e-9) {
722 return None;
723 }
724 let scale = (half[0] / reach[0]).min(half[1] / reach[1]) * 0.9;
725 project(scale).map(|drawing| (drawing, scale))
726 }
727
728 pub fn resolve_sheet(&self, id: &str) -> Result<String, String> {
731 let state = self.sheet_state();
732 let id = id.trim();
733 if !id.is_empty() {
734 return state
735 .find_sheet(id)
736 .map(|sheet| sheet.id.clone())
737 .ok_or_else(|| format!("no sheet '{id}'"));
738 }
739 if let Some(open) = self.sheet_open.as_ref().filter(|open| state.find_sheet(open).is_some()) {
740 return Ok(open.clone());
741 }
742 state
743 .sheets
744 .first()
745 .map(|sheet| sheet.id.clone())
746 .ok_or_else(|| "the document has no sheets".to_string())
747 }
748
749 fn write_sheet_state(&mut self, state: SheetState, coalesce_key: Option<&str>) {
754 let block = if state.is_empty() { None } else { serde_json::to_value(&state).ok() };
755 self.history.set_sheets_block(block, coalesce_key);
756 self.dirty = true;
757 }
758
759 pub fn sheet_add(&mut self, name: Option<&str>) -> String {
764 let mut state = self.sheet_state();
765 let id = state.next_id("SHEET");
766 let name = name
767 .map(str::trim)
768 .filter(|name| !name.is_empty())
769 .map(String::from)
770 .unwrap_or_else(|| format!("Sheet {}", state.sheets.len() + 1));
771 let (width_mm, height_mm) = crate::sheets::paper_size(DEFAULT_SIZE).unwrap_or((420.0, 297.0));
772 state.sheets.push(Sheet {
773 id: id.clone(),
774 name,
775 size: DEFAULT_SIZE.to_string(),
776 width_mm,
777 height_mm,
778 border: true,
779 border_inset_mm: DEFAULT_BORDER_INSET_MM,
780 title_block: true,
781 notes: String::new(),
782 views: Vec::new(),
783 dimensions: Vec::new(),
784 ordinates: Vec::new(),
785 revisions: Vec::new(),
786 });
787 self.write_sheet_state(state, None);
788 self.set_open_sheet(Some(id.clone()));
789 id
790 }
791
792 pub fn sheet_update(&mut self, id: &str, params_json: &str) -> Result<(), String> {
794 let params: Value = serde_json::from_str(params_json)
795 .map_err(|error| format!("sheet params: {error}"))?;
796 let mut state = self.sheet_state();
797 let sheet = state.find_sheet_mut(id).ok_or_else(|| format!("no sheet '{id}'"))?;
798 sheet.apply(¶ms)?;
799 self.write_sheet_state(state, None);
800 Ok(())
801 }
802
803 pub fn sheet_add_revision(
810 &mut self,
811 id: &str,
812 rev: Option<&str>,
813 date: Option<&str>,
814 description: Option<&str>,
815 ) -> Result<usize, String> {
816 let mut state = self.sheet_state();
817 let sheet = state.find_sheet_mut(id).ok_or_else(|| format!("no sheet '{id}'"))?;
818 let next = || {
819 let letters = crate::sheets::SectionCut::LETTERS;
820 let last = sheet.revisions.last().and_then(|row| row.rev.trim().chars().next());
821 match last.and_then(|c| letters.iter().position(|l| l.eq_ignore_ascii_case(&c))) {
822 Some(at) => letters[(at + 1).min(letters.len() - 1)].to_string(),
823 None if sheet.revisions.is_empty() => letters[0].to_string(),
824 None => String::new(),
825 }
826 };
827 let revision = crate::sheets::Revision {
828 rev: rev.map(str::to_string).unwrap_or_else(next),
829 date: date.map(str::to_string).unwrap_or_else(today_iso),
830 description: description.unwrap_or_default().to_string(),
831 };
832 sheet.revisions.push(revision);
833 let index = sheet.revisions.len() - 1;
834 self.write_sheet_state(state, None);
835 Ok(index)
836 }
837
838 pub fn sheet_update_revision(&mut self, id: &str, index: usize, params_json: &str) -> Result<(), String> {
841 let params: Value = serde_json::from_str(params_json)
842 .map_err(|error| format!("revision params: {error}"))?;
843 let mut state = self.sheet_state();
844 let sheet = state.find_sheet_mut(id).ok_or_else(|| format!("no sheet '{id}'"))?;
845 let count = sheet.revisions.len();
846 let row = sheet
847 .revisions
848 .get_mut(index)
849 .ok_or_else(|| format!("sheet '{id}' has {count} revision row(s); there is no row {index}"))?;
850 row.apply(¶ms)?;
851 self.write_sheet_state(state, None);
852 Ok(())
853 }
854
855 pub fn sheet_remove_revision(&mut self, id: &str, index: usize) -> Result<(), String> {
858 let mut state = self.sheet_state();
859 let sheet = state.find_sheet_mut(id).ok_or_else(|| format!("no sheet '{id}'"))?;
860 if index >= sheet.revisions.len() {
861 return Err(format!(
862 "sheet '{id}' has {} revision row(s); there is no row {index}",
863 sheet.revisions.len()
864 ));
865 }
866 sheet.revisions.remove(index);
867 self.write_sheet_state(state, None);
868 Ok(())
869 }
870
871 pub fn sheet_delete(&mut self, id: &str) -> Result<(), String> {
872 let mut state = self.sheet_state();
873 let before = state.sheets.len();
874 state.sheets.retain(|sheet| sheet.id != id);
875 if state.sheets.len() == before {
876 return Err(format!("no sheet '{id}'"));
877 }
878 if self.sheet_open.as_deref() == Some(id) {
879 self.set_open_sheet(state.sheets.first().map(|sheet| sheet.id.clone()));
880 }
881 self.clear_open_object(&state);
882 self.write_sheet_state(state, None);
883 Ok(())
884 }
885
886 pub fn sheet_set_open(&mut self, id: Option<&str>) -> Result<(), String> {
888 match id {
889 None => {
890 self.set_open_sheet(None);
891 self.dirty = true;
892 Ok(())
893 }
894 Some(id) => {
895 let state = self.sheet_state();
896 if state.find_sheet(id).is_none() {
897 return Err(format!("no sheet '{id}'"));
898 }
899 self.set_open_sheet(Some(id.to_string()));
900 self.dirty = true;
901 Ok(())
902 }
903 }
904 }
905
906 pub fn sheet_set_object_open(&mut self, id: Option<&str>) {
911 match id {
912 Some(id) => self.open_sheet_object(id.to_string()),
913 None => self.sheet_open_object = None,
914 }
915 }
916
917 pub fn sheet_place_view(
923 &mut self,
924 sheet_id: Option<&str>,
925 view_id: &str,
926 position: Option<[f64; 2]>,
927 scale: Option<f64>,
928 ) -> Result<String, String> {
929 let pmi = self.pmi_state();
930 let view = pmi
931 .find_view(view_id)
932 .ok_or_else(|| format!("no PMI view '{view_id}'"))?;
933 let camera = view
934 .camera
935 .as_ref()
936 .ok_or_else(|| format!("PMI view '{view_id}' has no captured camera — capture one first"))?;
937 refuse_perspective(view_id, camera)?;
938 let mut state = self.sheet_state();
939 let sheet_id = match sheet_id.map(str::trim).filter(|id| !id.is_empty()) {
940 Some(id) => id.to_string(),
941 None => self.resolve_sheet("")?,
942 };
943 let id = state.next_id("SV");
944 let sheet = state
945 .find_sheet_mut(&sheet_id)
946 .ok_or_else(|| format!("no sheet '{sheet_id}'"))?;
947 let (width_mm, height_mm) = sheet.millimetres();
948 let scale = match scale {
949 Some(scale) if scale.is_finite() && scale > 0.0 => scale,
950 Some(scale) => return Err(format!("scale: {scale} is not a positive ratio")),
951 None => DEFAULT_SCALE,
952 };
953 sheet.views.push(PlacedView {
954 id: id.clone(),
955 view: view_id.to_string(),
956 position: position.unwrap_or([width_mm * 0.5, height_mm * 0.5]),
957 scale,
958 projection: ORTHOGRAPHIC.to_string(),
959 flatten_text: true,
960 three_d: false,
961 section: None,
962 detail: None,
963 });
964 self.write_sheet_state(state, None);
965 self.set_open_sheet(Some(sheet_id));
966 self.open_sheet_object(id.clone());
967 Ok(id)
968 }
969
970 pub fn sheet_update_view(&mut self, id: &str, params_json: &str) -> Result<(), String> {
972 let mut params: Value = serde_json::from_str(params_json)
973 .map_err(|error| format!("placed view params: {error}"))?;
974 if let Some(object) = params.as_object_mut() {
979 let current = self.sheet_state().sheets.iter().find_map(|sheet| sheet.find_view(id).cloned());
980 for field in ["cut", "centre", "rim"] {
981 let Some(value) = object.remove(field) else { continue };
982 let names: Vec<String> = match value.as_str() {
984 Some(one) => vec![one.to_string()],
985 None => crate::json_support::string_values(Some(&value)).map(str::to_string).collect(),
986 }
987 .into_iter()
988 .filter(|name| !name.is_empty())
989 .collect();
990 let now = current.as_ref().map(|placed| placed.picked(field)).unwrap_or_default();
991 if names != now {
992 self.sheet_set_placement_refs(id, field, &names)?;
993 }
994 }
995 }
996 if let Some(view_id) = params.get("view").and_then(Value::as_str).map(str::trim).filter(|id| !id.is_empty()) {
999 if let Some(camera) = self.pmi_state().find_view(view_id).and_then(|view| view.camera.clone()) {
1000 refuse_perspective(view_id, &camera)?;
1001 }
1002 }
1003 let views = self.sheet_pmi_view_ids();
1004 let mut state = self.sheet_state();
1005 let view = state.find_view_mut(id).ok_or_else(|| format!("no placed view '{id}'"))?;
1006 view.apply(¶ms, &views)?;
1007 self.write_sheet_state(state, None);
1008 Ok(())
1009 }
1010
1011 pub fn sheet_move_view(&mut self, id: &str, position: [f64; 2], coalesce: bool) -> Result<(), String> {
1014 if !position[0].is_finite() || !position[1].is_finite() {
1015 return Err("position: not a finite point".into());
1016 }
1017 let mut state = self.sheet_state();
1018 let view = state.find_view_mut(id).ok_or_else(|| format!("no placed view '{id}'"))?;
1019 view.position = position;
1020 let key = format!("sheet:view:{id}");
1021 self.write_sheet_state(state, coalesce.then_some(key.as_str()));
1022 Ok(())
1023 }
1024
1025 pub fn sheet_move_view_end(&mut self) {
1027 self.history.break_coalescing();
1028 }
1029
1030 pub fn sheet_remove_view(&mut self, id: &str) -> Result<(), String> {
1031 let mut state = self.sheet_state();
1032 let Some((sheet_id, index)) = state.locate_view(id) else {
1033 return Err(format!("no placed view '{id}'"));
1034 };
1035 if let Some(sheet) = state.find_sheet_mut(&sheet_id) {
1036 sheet.views.remove(index);
1037 }
1038 self.clear_open_object(&state);
1039 self.write_sheet_state(state, None);
1040 Ok(())
1041 }
1042
1043 fn clear_open_object(&mut self, state: &SheetState) {
1045 if let Some(open) = self.sheet_open_object.clone() {
1046 if state.find_sheet(&open).is_none()
1047 && state.locate_view(&open).is_none()
1048 && state.locate_dimension(&open).is_none()
1049 && state.locate_ordinate(&open).is_none()
1050 {
1051 self.sheet_open_object = None;
1052 }
1053 }
1054 }
1055
1056 pub fn sheet_add_dimension(
1063 &mut self,
1064 sheet_id: Option<&str>,
1065 kind: &str,
1066 alignment: Option<&str>,
1067 anchors: Vec<String>,
1068 ) -> Result<String, String> {
1069 let kind = KINDS
1070 .iter()
1071 .find(|known| known.eq_ignore_ascii_case(kind.trim()))
1072 .ok_or_else(|| format!("no dimension kind '{kind}'"))?;
1073 let alignment = match alignment.map(str::trim).filter(|a| !a.is_empty()) {
1074 None => ALIGNED,
1075 Some(wanted) => ALIGNMENTS
1076 .iter()
1077 .copied()
1078 .find(|known| known.eq_ignore_ascii_case(wanted))
1079 .ok_or_else(|| format!("no alignment '{wanted}'"))?,
1080 };
1081 for anchor in &anchors {
1082 SheetAnchor::parse(anchor)?;
1083 }
1084 let sheet_id = match sheet_id.map(str::trim).filter(|id| !id.is_empty()) {
1085 Some(id) => id.to_string(),
1086 None => self.resolve_sheet("")?,
1087 };
1088 let mut state = self.sheet_state();
1089 let id = state.next_id("SD");
1090 let sheet = state
1091 .find_sheet_mut(&sheet_id)
1092 .ok_or_else(|| format!("no sheet '{sheet_id}'"))?;
1093 sheet
1094 .dimensions
1095 .push(SheetDimension::new(id.clone(), kind, alignment, anchors));
1096 self.write_sheet_state(state, None);
1097 self.set_open_sheet(Some(sheet_id));
1098 self.open_sheet_object(id.clone());
1099 Ok(id)
1100 }
1101
1102 pub fn sheet_update_dimension(&mut self, id: &str, params_json: &str) -> Result<(), String> {
1104 let params: Value = serde_json::from_str(params_json)
1105 .map_err(|error| format!("sheet dimension params: {error}"))?;
1106 let mut state = self.sheet_state();
1107 let dimension = state
1108 .find_dimension_mut(id)
1109 .ok_or_else(|| format!("no sheet dimension '{id}'"))?;
1110 dimension.apply(¶ms)?;
1111 self.write_sheet_state(state, None);
1112 Ok(())
1113 }
1114
1115 pub fn sheet_move_dimension(
1119 &mut self,
1120 id: &str,
1121 offset_mm: f64,
1122 coalesce: bool,
1123 ) -> Result<(), String> {
1124 if !offset_mm.is_finite() {
1125 return Err(format!("offsetMm: {offset_mm} is not a length"));
1126 }
1127 let mut state = self.sheet_state();
1128 let dimension = state
1129 .find_dimension_mut(id)
1130 .ok_or_else(|| format!("no sheet dimension '{id}'"))?;
1131 dimension.offset_mm = offset_mm;
1132 let key = format!("sheet:dim:{id}");
1133 self.write_sheet_state(state, coalesce.then_some(key.as_str()));
1134 Ok(())
1135 }
1136
1137 pub fn sheet_move_dimension_end(&mut self) {
1139 self.history.break_coalescing();
1140 }
1141
1142 pub fn sheet_remove_dimension(&mut self, id: &str) -> Result<(), String> {
1143 let mut state = self.sheet_state();
1144 let Some((sheet_id, index)) = state.locate_dimension(id) else {
1145 return Err(format!("no sheet dimension '{id}'"));
1146 };
1147 if let Some(sheet) = state.find_sheet_mut(&sheet_id) {
1148 sheet.dimensions.remove(index);
1149 }
1150 self.clear_open_object(&state);
1151 self.write_sheet_state(state, None);
1152 Ok(())
1153 }
1154
1155 pub fn sheet_add_ordinate(
1161 &mut self,
1162 sheet_id: Option<&str>,
1163 axis: &str,
1164 datum: Option<&str>,
1165 members: Vec<String>,
1166 ) -> Result<String, String> {
1167 let axis = AXES
1168 .iter()
1169 .find(|known| known.eq_ignore_ascii_case(axis.trim()))
1170 .ok_or_else(|| format!("no ordinate axis '{axis}'"))?;
1171 let datum = datum.map(str::trim).filter(|d| !d.is_empty()).unwrap_or_default();
1172 if !datum.is_empty() {
1173 SheetAnchor::parse(datum)?;
1174 }
1175 for member in &members {
1176 SheetAnchor::parse(member)?;
1177 }
1178 let sheet_id = match sheet_id.map(str::trim).filter(|id| !id.is_empty()) {
1179 Some(id) => id.to_string(),
1180 None => self.resolve_sheet("")?,
1181 };
1182 let mut state = self.sheet_state();
1183 let id = state.next_id("OD");
1184 let sheet = state
1185 .find_sheet_mut(&sheet_id)
1186 .ok_or_else(|| format!("no sheet '{sheet_id}'"))?;
1187 let mut set = SheetOrdinate::new(id.clone(), axis, datum.to_string());
1188 set.members = members;
1189 sheet.ordinates.push(set);
1190 self.write_sheet_state(state, None);
1191 self.set_open_sheet(Some(sheet_id));
1192 self.open_sheet_object(id.clone());
1193 Ok(id)
1194 }
1195
1196 pub fn sheet_update_ordinate(&mut self, id: &str, params_json: &str) -> Result<(), String> {
1203 let params: Value = serde_json::from_str(params_json)
1204 .map_err(|error| format!("ordinate set params: {error}"))?;
1205 let mut state = self.sheet_state();
1206 let set = state
1207 .find_ordinate_mut(id)
1208 .ok_or_else(|| format!("no ordinate set '{id}'"))?;
1209 set.apply(¶ms)?;
1210 self.write_sheet_state(state, None);
1211 Ok(())
1212 }
1213
1214 pub fn sheet_move_ordinate(
1218 &mut self,
1219 id: &str,
1220 offset_mm: f64,
1221 coalesce: bool,
1222 ) -> Result<(), String> {
1223 if !offset_mm.is_finite() {
1224 return Err(format!("offsetMm: {offset_mm} is not a length"));
1225 }
1226 let mut state = self.sheet_state();
1227 let set = state
1228 .find_ordinate_mut(id)
1229 .ok_or_else(|| format!("no ordinate set '{id}'"))?;
1230 set.offset_mm = offset_mm;
1231 let key = format!("sheet:ord:{id}");
1232 self.write_sheet_state(state, coalesce.then_some(key.as_str()));
1233 Ok(())
1234 }
1235
1236 pub fn sheet_move_ordinate_end(&mut self) {
1238 self.history.break_coalescing();
1239 }
1240
1241 pub fn sheet_remove_ordinate(&mut self, id: &str) -> Result<(), String> {
1242 let mut state = self.sheet_state();
1243 let Some((sheet_id, index)) = state.locate_ordinate(id) else {
1244 return Err(format!("no ordinate set '{id}'"));
1245 };
1246 if let Some(sheet) = state.find_sheet_mut(&sheet_id) {
1247 sheet.ordinates.remove(index);
1248 }
1249 self.clear_open_object(&state);
1250 self.write_sheet_state(state, None);
1251 Ok(())
1252 }
1253
1254 pub fn sheet_ordinate_offset_at(&mut self, id: &str, at: [f64; 2]) -> Option<f64> {
1261 let sheet_id = self.sheet_state().locate_ordinate(id).map(|(sheet, _)| sheet)?;
1262 let vertical = self
1263 .sheet_state()
1264 .find_ordinate(id)?
1265 .axis
1266 .eq_ignore_ascii_case(crate::sheets::dimension::VERTICAL);
1267 let drawing = self.sheet_drawing(&sheet_id)?;
1268 let drawn = drawing.ordinates.iter().find(|o| o.id == id)?;
1269 if !drawn.error.is_empty() {
1270 return None;
1271 }
1272 let datum = drawn.stations.iter().find(|station| station.datum)?;
1273 if !datum.error.is_empty() {
1274 return None;
1275 }
1276 Some(if vertical { at[0] - datum.at[0] } else { at[1] - datum.at[1] })
1277 }
1278
1279 pub fn sheet_add_section(
1289 &mut self,
1290 source: &str,
1291 from: &str,
1292 to: &str,
1293 position: Option<[f64; 2]>,
1294 ) -> Result<String, String> {
1295 SheetAnchor::parse(from)?;
1296 SheetAnchor::parse(to)?;
1297 if from == to {
1298 return Err("a cutting line needs two different anchors".into());
1299 }
1300 let (a, b) = (SheetAnchor::parse(from)?, SheetAnchor::parse(to)?);
1303 if a.view != b.view {
1304 return Err(format!(
1305 "'{from}' is on '{}' and '{to}' on '{}'; a cutting line is drawn on ONE view",
1306 a.view, b.view
1307 ));
1308 }
1309 if a.view != source {
1310 return Err(format!("the cutting line is on '{}', not on '{source}'", a.view));
1311 }
1312 let mut state = self.sheet_state();
1313 let Some((sheet_id, _)) = state.locate_view(source) else {
1314 return Err(format!("no placed view '{source}'"));
1315 };
1316 let (view, scale, at, label) = {
1317 let sheet = state
1318 .find_sheet(&sheet_id)
1319 .ok_or_else(|| format!("no sheet '{sheet_id}'"))?;
1320 let label = sheet.next_view_letter();
1321 let parent = sheet
1322 .find_view(source)
1323 .ok_or_else(|| format!("no placed view '{source}'"))?;
1324 if parent.section.is_some() {
1325 return Err(format!(
1326 "'{source}' is itself a section; a section of a section is not drawn"
1327 ));
1328 }
1329 if parent.detail.is_some() {
1330 return Err(format!("'{source}' is a detail; a section of a detail is not drawn"));
1331 }
1332 (parent.view.clone(), parent.scale, parent.position, label)
1333 };
1334 let id = state.next_id("SV");
1335 let sheet = state.find_sheet_mut(&sheet_id).expect("just found");
1336 sheet.views.push(PlacedView {
1337 id: id.clone(),
1338 view,
1339 position: position.unwrap_or([at[0] + 120.0, at[1]]),
1343 scale,
1344 projection: ORTHOGRAPHIC.to_string(),
1345 flatten_text: true,
1346 three_d: false,
1347 section: Some(SectionCut {
1348 source: source.to_string(),
1349 from: from.to_string(),
1350 to: to.to_string(),
1351 label,
1352 flip: false,
1353 }),
1354 detail: None,
1355 });
1356 self.write_sheet_state(state, None);
1357 self.set_open_sheet(Some(sheet_id));
1358 self.open_sheet_object(id.clone());
1359 Ok(id)
1360 }
1361
1362 pub fn sheet_new_section(&mut self, sheet_id: Option<&str>) -> Result<String, String> {
1369 self.sheet_new_derived(sheet_id, true)
1370 }
1371
1372 pub fn sheet_new_detail(&mut self, sheet_id: Option<&str>) -> Result<String, String> {
1376 self.sheet_new_derived(sheet_id, false)
1377 }
1378
1379 fn sheet_new_derived(&mut self, sheet_id: Option<&str>, section: bool) -> Result<String, String> {
1380 let sheet_id = match sheet_id.map(str::trim).filter(|id| !id.is_empty()) {
1381 Some(id) => id.to_string(),
1382 None => self.resolve_sheet("")?,
1383 };
1384 let mut state = self.sheet_state();
1385 let id = state.next_id("SV");
1386 let sheet = state
1387 .find_sheet_mut(&sheet_id)
1388 .ok_or_else(|| format!("no sheet '{sheet_id}'"))?;
1389 let label = sheet.next_view_letter();
1390 let (w, h) = sheet.millimetres();
1391 sheet.views.push(PlacedView {
1392 id: id.clone(),
1393 view: String::new(),
1396 position: [w * 0.5, h * 0.5],
1397 scale: DEFAULT_SCALE,
1398 projection: ORTHOGRAPHIC.to_string(),
1399 flatten_text: true,
1400 three_d: false,
1401 section: section.then(|| SectionCut {
1402 source: String::new(),
1403 from: String::new(),
1404 to: String::new(),
1405 label: label.clone(),
1406 flip: false,
1407 }),
1408 detail: (!section).then(|| DetailCircle {
1409 source: String::new(),
1410 centre: String::new(),
1411 rim: String::new(),
1412 label,
1413 }),
1414 });
1415 self.write_sheet_state(state, None);
1416 self.set_open_sheet(Some(sheet_id));
1417 self.open_sheet_object(id.clone());
1418 Ok(id)
1419 }
1420
1421 pub fn sheet_set_placement_refs(&mut self, id: &str, field: &str, names: &[String]) -> Result<(), String> {
1434 for name in names {
1435 SheetAnchor::parse(name)?;
1436 }
1437 let mut state = self.sheet_state();
1438 let (sheet_id, _) = state.locate_view(id).ok_or_else(|| format!("no placed view '{id}'"))?;
1439 let sheet = state.find_sheet(&sheet_id).ok_or_else(|| format!("no sheet '{sheet_id}'"))?;
1440 let placed = sheet.find_view(id).ok_or_else(|| format!("no placed view '{id}'"))?.clone();
1441 let single = |names: &[String]| -> Result<String, String> {
1442 match names {
1443 [] => Ok(String::new()),
1444 [one] => Ok(one.clone()),
1445 _ => Err(format!("'{field}' is one anchor")),
1446 }
1447 };
1448 let anchors: Vec<String> = match (field, &placed.section, &placed.detail) {
1450 ("cut", Some(_), _) => {
1451 if names.len() > 2 {
1452 return Err("a cutting line is two anchors".into());
1453 }
1454 names.to_vec()
1455 }
1456 ("centre", _, Some(circle)) => vec![single(names)?, circle.rim.clone()],
1457 ("rim", _, Some(circle)) => vec![circle.centre.clone(), single(names)?],
1458 _ => return Err(format!("'{id}' has no {field} to pick")),
1459 };
1460 let picked: Vec<&String> = anchors.iter().filter(|a| !a.is_empty()).collect();
1461 if picked.len() == 2 && picked[0] == picked[1] {
1462 return Err(if placed.section.is_some() {
1463 "a cutting line needs two different anchors".into()
1464 } else {
1465 "a detail circle needs a radius: pick a rim point away from its centre".into()
1466 });
1467 }
1468 let on: Vec<String> = picked
1469 .iter()
1470 .map(|anchor| SheetAnchor::parse(anchor).map(|a| a.view))
1471 .collect::<Result<_, _>>()?;
1472 if on.windows(2).any(|pair| pair[0] != pair[1]) {
1473 return Err(format!(
1474 "'{}' is on '{}' and '{}' on '{}'; {} is drawn on ONE view",
1475 picked[0], on[0], picked[1], on[1],
1476 if placed.section.is_some() { "a cutting line" } else { "a detail circle" }
1477 ));
1478 }
1479 let source = on.first().cloned();
1480 let parent = match &source {
1481 Some(source) => {
1482 if source == id {
1483 return Err(format!("'{id}' cannot be drawn on itself"));
1484 }
1485 let parent = sheet
1486 .find_view(source)
1487 .ok_or_else(|| format!("no placement '{source}' on this sheet"))?;
1488 let what = if placed.section.is_some() { "section" } else { "detail" };
1489 if parent.section.is_some() {
1490 return Err(format!("'{source}' is a section; a {what} of a section is not drawn"));
1491 }
1492 if parent.detail.is_some() {
1493 return Err(format!("'{source}' is a detail; a {what} of a detail is not drawn"));
1494 }
1495 Some((parent.view.clone(), parent.scale, parent.position))
1496 }
1497 None => None,
1498 };
1499 let sheet = state.find_sheet_mut(&sheet_id).expect("just found");
1500 let placed = sheet.views.iter_mut().find(|view| view.id == id).expect("just found");
1501 let source = source.unwrap_or_default();
1502 if let Some(cut) = placed.section.as_mut() {
1503 cut.from = anchors.first().cloned().unwrap_or_default();
1504 cut.to = anchors.get(1).cloned().unwrap_or_default();
1505 cut.source = source;
1506 } else if let Some(circle) = placed.detail.as_mut() {
1507 circle.centre = anchors[0].clone();
1508 circle.rim = anchors[1].clone();
1509 circle.source = source;
1510 }
1511 if let Some((view, scale, at)) = parent {
1512 if placed.view.is_empty() {
1513 placed.view = view;
1514 placed.scale = if placed.detail.is_some() { scale * DETAIL_SCALE_FACTOR } else { scale };
1515 placed.position = [at[0] + 120.0, at[1]];
1516 }
1517 }
1518 self.write_sheet_state(state, None);
1519 Ok(())
1520 }
1521
1522 pub fn begin_ref_select_for_sheet(
1530 &mut self,
1531 id: &str,
1532 path: Vec<String>,
1533 label: String,
1534 filter: Vec<String>,
1535 multiple: bool,
1536 seed_names: Vec<String>,
1537 ) -> Result<(), String> {
1538 let state = self.sheet_state();
1539 let sheet = state
1540 .locate_view(id)
1541 .or_else(|| state.locate_dimension(id))
1542 .or_else(|| state.locate_ordinate(id))
1543 .map(|(sheet, _)| sheet)
1544 .ok_or_else(|| format!("no sheet object '{id}'"))?;
1545 self.set_open_sheet(Some(sheet));
1546 self.ref_select = Some(RefSelectState {
1547 feature_id: id.to_string(),
1548 path,
1549 label,
1550 filter,
1551 multiple,
1552 names: seed_names,
1553 restore_index: self.history.rollback(),
1554 target: RefSelectTarget::Sheet,
1555 });
1556 self.dirty = true;
1557 Ok(())
1558 }
1559
1560 fn sheet_ref_capacity(&self, id: &str, field: &str) -> Option<usize> {
1563 let state = self.sheet_state();
1564 match field {
1565 "anchors" => state
1566 .find_dimension(id)
1567 .map(|dimension| crate::sheets::dimension::anchor_count(&dimension.kind)),
1568 "cut" => Some(2),
1569 _ => None,
1570 }
1571 }
1572
1573 pub fn ref_select_pick_sheet_anchor(&mut self, anchor: &str) -> Result<(), String> {
1579 SheetAnchor::parse(anchor)?;
1580 let Some(state) = self.ref_select.as_ref().filter(|s| s.target == RefSelectTarget::Sheet) else {
1581 return Err("no sheet reference is being picked".into());
1582 };
1583 let capacity = state
1584 .path
1585 .last()
1586 .and_then(|field| self.sheet_ref_capacity(&state.feature_id, field));
1587 let state = self.ref_select.as_mut().expect("guarded above");
1588 if !state.multiple {
1589 state.names = vec![anchor.to_string()];
1590 } else if !state.names.iter().any(|name| name == anchor) {
1591 if capacity.is_some_and(|cap| cap > 0 && state.names.len() >= cap) {
1592 state.names.remove(0);
1593 }
1594 state.names.push(anchor.to_string());
1595 }
1596 self.dirty = true;
1597 Ok(())
1598 }
1599
1600 pub(crate) fn sheet_commit_refs(&mut self, id: &str, path: &[String], names: &[String], multiple: bool) -> Result<(), String> {
1602 let field = path.last().map(String::as_str).unwrap_or_default();
1603 let value = if multiple {
1604 Value::Array(names.iter().cloned().map(Value::String).collect())
1605 } else {
1606 Value::String(names.first().cloned().unwrap_or_default())
1607 };
1608 let params = serde_json::json!({ field: value }).to_string();
1609 let state = self.sheet_state();
1610 if state.find_dimension(id).is_some() {
1611 self.sheet_update_dimension(id, ¶ms)
1612 } else if state.find_ordinate(id).is_some() {
1613 self.sheet_update_ordinate(id, ¶ms)
1614 } else if state.locate_view(id).is_some() {
1615 self.sheet_set_placement_refs(id, field, names)
1616 } else {
1617 Err(format!("no sheet object '{id}'"))
1618 }
1619 }
1620
1621 pub fn sheet_add_detail(
1632 &mut self,
1633 source: &str,
1634 centre: &str,
1635 rim: &str,
1636 position: Option<[f64; 2]>,
1637 scale: Option<f64>,
1638 ) -> Result<String, String> {
1639 let (a, b) = (SheetAnchor::parse(centre)?, SheetAnchor::parse(rim)?);
1640 if centre == rim {
1641 return Err("a detail circle needs two different anchors: its centre and a point on its rim".into());
1642 }
1643 if a.view != b.view {
1646 return Err(format!(
1647 "'{centre}' is on '{}' and '{rim}' on '{}'; a detail circle is drawn on ONE view",
1648 a.view, b.view
1649 ));
1650 }
1651 if a.view != source {
1652 return Err(format!("the detail circle is on '{}', not on '{source}'", a.view));
1653 }
1654 if let Some(scale) = scale {
1655 if !scale.is_finite() || scale <= 0.0 {
1656 return Err(format!("scale: {scale} is not a positive ratio"));
1657 }
1658 }
1659 let mut state = self.sheet_state();
1660 let Some((sheet_id, _)) = state.locate_view(source) else {
1661 return Err(format!("no placed view '{source}'"));
1662 };
1663 let (view, parent_scale, at, label) = {
1664 let sheet = state
1665 .find_sheet(&sheet_id)
1666 .ok_or_else(|| format!("no sheet '{sheet_id}'"))?;
1667 let label = sheet.next_view_letter();
1668 let parent = sheet
1669 .find_view(source)
1670 .ok_or_else(|| format!("no placed view '{source}'"))?;
1671 if parent.detail.is_some() {
1675 return Err(format!("'{source}' is itself a detail; a detail of a detail is not drawn"));
1676 }
1677 if parent.section.is_some() {
1678 return Err(format!("'{source}' is a section; a detail of a section is not drawn"));
1679 }
1680 (parent.view.clone(), parent.scale, parent.position, label)
1681 };
1682 let id = state.next_id("SV");
1683 let sheet = state.find_sheet_mut(&sheet_id).expect("just found");
1684 sheet.views.push(PlacedView {
1685 id: id.clone(),
1686 view,
1687 position: position.unwrap_or([at[0] + 120.0, at[1]]),
1690 scale: scale.unwrap_or(parent_scale * DETAIL_SCALE_FACTOR),
1691 projection: ORTHOGRAPHIC.to_string(),
1692 flatten_text: true,
1693 three_d: false,
1694 section: None,
1695 detail: Some(DetailCircle {
1696 source: source.to_string(),
1697 centre: centre.to_string(),
1698 rim: rim.to_string(),
1699 label,
1700 }),
1701 });
1702 self.write_sheet_state(state, None);
1703 self.set_open_sheet(Some(sheet_id));
1704 self.open_sheet_object(id.clone());
1705 Ok(id)
1706 }
1707
1708 pub fn sheet_dimension_offset_at(&mut self, id: &str, at: [f64; 2]) -> Option<f64> {
1718 let sheet_id = self.sheet_state().locate_dimension(id).map(|(sheet, _)| sheet)?;
1719 let drawing = self.sheet_drawing(&sheet_id)?;
1720 let drawn = drawing.dimensions.iter().find(|d| d.id == id)?;
1721 if !drawn.error.is_empty() {
1722 return None;
1723 }
1724 let (label, offset) = (drawn.label, drawn.offset_mm);
1725 let direction = self.sheet_dimension_direction(&sheet_id, id)?;
1730 let base = [label[0] - direction[0] * offset, label[1] - direction[1] * offset];
1731 Some((at[0] - base[0]) * direction[0] + (at[1] - base[1]) * direction[1])
1732 }
1733
1734 fn sheet_dimension_direction(&mut self, sheet_id: &str, id: &str) -> Option<[f64; 2]> {
1736 let state = self.sheet_state();
1740 let sheet = state.find_sheet(sheet_id)?;
1741 let mut probe = sheet.dimensions.iter().find(|d| d.id == id)?.clone();
1742 let delta = if probe.offset_mm < 0.0 { -1.0 } else { 1.0 };
1747 probe.offset_mm += delta;
1748 let drawing = self.sheet_drawing_cached(sheet_id)?;
1749 let moved = crate::sheets::dimension::layout_dimensions(
1750 std::slice::from_ref(&probe),
1751 &drawing.views,
1752 [drawing.width_mm * 0.5, drawing.height_mm * 0.5],
1753 );
1754 let here = drawing.dimensions.iter().find(|d| d.id == id)?;
1755 let there = moved.first()?;
1756 if !there.error.is_empty() {
1757 return None;
1758 }
1759 let step = [
1760 (there.label[0] - here.label[0]) / delta,
1761 (there.label[1] - here.label[1]) / delta,
1762 ];
1763 let len = (step[0] * step[0] + step[1] * step[1]).sqrt();
1764 (len > 1e-9).then(|| [step[0] / len, step[1] / len])
1765 }
1766
1767 pub(crate) fn forget_sheets(&mut self) {
1769 self.sheet_open = None;
1770 self.sheet_open_object = None;
1771 self.sheet_selected_object = None;
1772 self.sheet_cache = None;
1773 self.sheet_lines.forget();
1774 }
1775}
1776
1777fn refuse_perspective(view_id: &str, camera: &brep_kernel::PmiCamera) -> Result<(), String> {
1781 if matches!(camera.projection, brep_kernel::PmiProjection::Orthographic { .. }) {
1782 Ok(())
1783 } else {
1784 Err(format!("PMI view '{view_id}' is a perspective camera; a sheet places {ORTHOGRAPHIC} views only"))
1785 }
1786}
1787
1788
1789
1790
1791