1use serde::{Deserialize, Serialize};
76use serde_json::{json, Value};
77
78use super::project::{AnchorPoint, Measures, SheetText, ViewDrawing};
79use brep_kernel::{format_dimension, pmi_present, PmiGeometry, PmiLayoutStyle, ToleranceBlock, ToleranceMode};
80
81pub const LINEAR: &str = "linear";
83pub const RADIAL: &str = "radial";
84pub const DIAMETRAL: &str = "diametral";
85pub const ANGULAR: &str = "angular";
86pub const KINDS: &[&str] = &[LINEAR, RADIAL, DIAMETRAL, ANGULAR];
87
88pub fn anchor_count(kind: &str) -> usize {
97 if kind.eq_ignore_ascii_case(LINEAR) || kind.eq_ignore_ascii_case(ANGULAR) {
98 2
99 } else {
100 1
101 }
102}
103
104pub const HORIZONTAL: &str = "horizontal";
106pub const VERTICAL: &str = "vertical";
107pub const ALIGNED: &str = "aligned";
108pub const ALIGNMENTS: &[&str] = &[HORIZONTAL, VERTICAL, ALIGNED];
109
110pub const VERTEX: &str = "vertex";
112pub const EDGE: &str = "edge";
113pub const CIRCLE: &str = "circle";
114
115pub const DEFAULT_OFFSET_MM: f64 = 12.0;
117
118pub const DEFAULT_PRECISION: u32 = 3;
121
122const RADIAL_DIR: [f64; 2] = [std::f64::consts::FRAC_1_SQRT_2, std::f64::consts::FRAC_1_SQRT_2];
127
128#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
141pub struct SheetTolerance {
142 #[serde(default = "default_tol_mode", rename = "tolMode", skip_serializing_if = "is_no_tolerance")]
144 pub mode: String,
145 #[serde(default, rename = "tolUpper", skip_serializing_if = "is_zero")]
147 pub upper: f64,
148 #[serde(default, rename = "tolLower", skip_serializing_if = "is_zero")]
150 pub lower: f64,
151 #[serde(default, rename = "isReference", skip_serializing_if = "is_false")]
153 pub reference: bool,
154}
155
156impl Default for SheetTolerance {
157 fn default() -> SheetTolerance {
158 SheetTolerance { mode: default_tol_mode(), upper: 0.0, lower: 0.0, reference: false }
159 }
160}
161
162fn default_tol_mode() -> String {
163 ToleranceMode::None.as_str().to_string()
164}
165
166fn is_no_tolerance(mode: &String) -> bool {
167 ToleranceMode::parse(mode) == ToleranceMode::None
168}
169
170fn is_zero(value: &f64) -> bool {
171 *value == 0.0
172}
173
174fn is_false(flag: &bool) -> bool {
175 !*flag
176}
177
178pub fn tolerance_modes() -> Vec<&'static str> {
181 [ToleranceMode::None, ToleranceMode::Symmetric, ToleranceMode::Deviation, ToleranceMode::Limits]
182 .iter()
183 .map(|mode| mode.as_str())
184 .collect()
185}
186
187impl SheetTolerance {
188 pub fn block(&self) -> ToleranceBlock {
190 ToleranceBlock { mode: ToleranceMode::parse(&self.mode), upper: self.upper.abs(), lower: self.lower.abs() }
191 }
192
193 pub fn is_plain(&self) -> bool {
196 self.block().mode == ToleranceMode::None && !self.reference
197 }
198
199 pub fn write_params(&self, params: &mut Value) {
201 params["tolMode"] = json!(ToleranceMode::parse(&self.mode).as_str());
202 params["tolUpper"] = json!(self.upper);
203 params["tolLower"] = json!(self.lower);
204 params["isReference"] = json!(self.reference);
205 }
206
207 pub fn apply(&mut self, params: &Value) -> Result<(), String> {
210 if let Some(mode) = params.get("tolMode").and_then(Value::as_str) {
211 let mode = mode.trim();
212 let Some(known) = tolerance_modes().into_iter().find(|known| known.eq_ignore_ascii_case(mode)) else {
213 return Err(format!("no tolerance mode '{mode}' (one of {})", tolerance_modes().join(", ")));
214 };
215 self.mode = known.to_string();
216 }
217 for (key, slot) in [("tolUpper", &mut self.upper), ("tolLower", &mut self.lower)] {
218 if let Some(value) = params.get(key).and_then(Value::as_f64) {
219 if !value.is_finite() {
220 return Err(format!("{key}: {value} is not a deviation"));
221 }
222 *slot = value.abs();
223 }
224 }
225 if let Some(reference) = params.get("isReference").and_then(Value::as_bool) {
226 self.reference = reference;
227 }
228 Ok(())
229 }
230
231 pub fn schema_fields(schema: &mut serde_json::Map<String, Value>) {
234 schema.insert(
235 "isReference".into(),
236 json!({ "type": "boolean", "label": "Reference", "default_value": false, "hint": "A reference dimension: shown in parentheses, no tolerance" }),
237 );
238 schema.insert(
239 "tolMode".into(),
240 json!({
241 "type": "options", "label": "Tolerance", "options": tolerance_modes(), "default_value": ToleranceMode::None.as_str(),
242 "hint": "none \u{00B7} \u{00B1} symmetric \u{00B7} +upper/\u{2212}lower deviation \u{00B7} upper/lower limit values"
243 }),
244 );
245 schema.insert(
246 "tolUpper".into(),
247 json!({ "type": "number", "label": "Upper (+)", "default_value": 0.0, "step": 0.01, "hint": "The upper deviation (the \u{00B1} value for symmetric)" }),
248 );
249 schema.insert(
250 "tolLower".into(),
251 json!({ "type": "number", "label": "Lower (\u{2212})", "default_value": 0.0, "step": 0.01, "hint": "The lower deviation (deviation / limits modes)" }),
252 );
253 }
254}
255
256#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
262pub struct SheetDimension {
263 pub id: String,
264 #[serde(default = "default_kind")]
266 pub kind: String,
267 #[serde(default = "default_alignment")]
271 pub alignment: String,
272 #[serde(default)]
276 pub anchors: Vec<String>,
277 #[serde(default = "default_offset", rename = "offsetMm")]
280 pub offset_mm: f64,
281 #[serde(default = "default_precision")]
283 pub precision: u32,
284 #[serde(flatten)]
286 pub tolerance: SheetTolerance,
287}
288
289fn default_kind() -> String {
290 LINEAR.to_string()
291}
292
293fn default_alignment() -> String {
294 ALIGNED.to_string()
295}
296
297fn default_offset() -> f64 {
298 DEFAULT_OFFSET_MM
299}
300
301fn default_precision() -> u32 {
302 DEFAULT_PRECISION
303}
304
305impl SheetDimension {
306 pub fn new(id: String, kind: &str, alignment: &str, anchors: Vec<String>) -> SheetDimension {
308 SheetDimension {
309 id,
310 kind: kind.to_string(),
311 alignment: alignment.to_string(),
312 anchors,
313 offset_mm: DEFAULT_OFFSET_MM,
314 precision: DEFAULT_PRECISION,
315 tolerance: SheetTolerance::default(),
316 }
317 }
318
319 pub fn anchor_count(&self) -> usize {
322 anchor_count(&self.kind)
323 }
324
325 pub fn params(&self) -> Value {
327 let mut params = json!({
328 "id": self.id,
329 "kind": self.kind,
330 "alignment": self.alignment,
331 "anchors": self.anchors,
332 "offsetMm": self.offset_mm,
333 "precision": self.precision,
334 });
335 self.tolerance.write_params(&mut params);
336 params
337 }
338
339 pub fn apply(&mut self, params: &Value) -> Result<(), String> {
342 if let Some(kind) = params.get("kind").and_then(Value::as_str) {
343 let kind = kind.trim();
344 let Some(known) = KINDS.iter().find(|k| k.eq_ignore_ascii_case(kind)) else {
345 return Err(format!("no dimension kind '{kind}'"));
346 };
347 self.kind = (*known).to_string();
348 }
349 if let Some(alignment) = params.get("alignment").and_then(Value::as_str) {
350 let alignment = alignment.trim();
351 let Some(known) = ALIGNMENTS.iter().find(|a| a.eq_ignore_ascii_case(alignment)) else {
352 return Err(format!("no alignment '{alignment}'"));
353 };
354 self.alignment = (*known).to_string();
355 }
356 if let Some(anchors) = params.get("anchors") {
357 let list: Vec<String> = crate::json_support::string_values(Some(anchors))
358 .map(str::to_string)
359 .collect();
360 for anchor in &list {
364 SheetAnchor::parse(anchor)?;
365 }
366 self.anchors = list;
367 }
368 apply_offset_precision(params, &mut self.offset_mm, &mut self.precision)?;
369 self.tolerance.apply(params)
370 }
371}
372
373pub(super) fn apply_offset_precision(
376 params: &Value,
377 offset_mm: &mut f64,
378 precision: &mut u32,
379) -> Result<(), String> {
380 if let Some(offset) = params.get("offsetMm").and_then(Value::as_f64) {
381 if !offset.is_finite() {
382 return Err(format!("offsetMm: {offset} is not a length"));
383 }
384 *offset_mm = offset;
385 }
386 if let Some(value) = params.get("precision").and_then(Value::as_f64) {
387 if !value.is_finite() || !(0.0..=8.0).contains(&value) {
388 return Err(format!("precision: {value} is not 0 to 8 decimals"));
389 }
390 *precision = value.round() as u32;
391 }
392 Ok(())
393}
394
395pub fn sheet_dimension_schema() -> Value {
408 let mut fields = json!({
409 "id": { "type": "string", "default_value": null },
410 "kind": { "type": "options", "label": "Kind", "options": KINDS, "default_value": LINEAR },
411 "alignment": { "type": "options", "label": "Alignment", "options": ALIGNMENTS, "default_value": ALIGNED },
412 "anchors": { "type": "reference_selection", "selectionFilter": ["sheetAnchor"], "multiple": true },
413 "offsetMm": { "type": "number", "label": "Offset (mm)", "default_value": DEFAULT_OFFSET_MM },
414 "precision": { "type": "number", "label": "Precision (decimals)", "default_value": DEFAULT_PRECISION },
415 });
416 SheetTolerance::schema_fields(fields.as_object_mut().expect("an object"));
417 json!({
418 "type": "sheetDimension",
419 "shortName": "SD",
420 "longName": "Sheet dimension",
421 "inputParamsSchema": fields,
422 })
423}
424
425#[derive(Debug, Clone, PartialEq)]
431pub struct SheetAnchor {
432 pub view: String,
434 pub kind: String,
436 pub solid: String,
438 pub vertex: u64,
440 pub edge: String,
442 pub fraction: f64,
445}
446
447impl SheetAnchor {
448 pub fn vertex(view: &str, solid: &str, topo_id: u64) -> SheetAnchor {
449 SheetAnchor {
450 view: view.into(),
451 kind: VERTEX.into(),
452 solid: solid.into(),
453 vertex: topo_id,
454 edge: String::new(),
455 fraction: 0.0,
456 }
457 }
458
459 pub fn edge(view: &str, solid: &str, edge: &str, fraction: f64) -> SheetAnchor {
460 SheetAnchor {
461 view: view.into(),
462 kind: EDGE.into(),
463 solid: solid.into(),
464 vertex: 0,
465 edge: edge.into(),
466 fraction,
467 }
468 }
469
470 pub fn circle(view: &str, solid: &str, edge: &str) -> SheetAnchor {
471 SheetAnchor {
472 view: view.into(),
473 kind: CIRCLE.into(),
474 solid: solid.into(),
475 vertex: 0,
476 edge: edge.into(),
477 fraction: 0.0,
478 }
479 }
480
481 pub fn to_ref(&self) -> String {
488 match self.kind.as_str() {
489 EDGE => format!(
490 "{}:{}:{}#{}@{}",
491 self.view, self.kind, self.solid, self.edge, self.fraction
492 ),
493 CIRCLE => format!("{}:{}:{}#{}", self.view, self.kind, self.solid, self.edge),
494 _ => format!("{}:{}:{}#{}", self.view, VERTEX, self.solid, self.vertex),
495 }
496 }
497
498 pub fn parse(text: &str) -> Result<SheetAnchor, String> {
500 let bad = || format!("'{text}' is not a sheet anchor (expected SV2:vertex:Solid#7)");
501 let mut parts = text.splitn(3, ':');
502 let view = parts.next().unwrap_or_default().trim();
503 let kind = parts.next().ok_or_else(bad)?.trim();
504 let rest = parts.next().ok_or_else(bad)?;
505 if view.is_empty() {
506 return Err(bad());
507 }
508 let (solid, entity) = rest.split_once('#').ok_or_else(bad)?;
509 if solid.is_empty() || entity.is_empty() {
510 return Err(bad());
511 }
512 match kind {
513 VERTEX => {
514 let id: u64 = entity
515 .parse()
516 .map_err(|_| format!("'{entity}' is not a vertex id in '{text}'"))?;
517 Ok(SheetAnchor::vertex(view, solid, id))
518 }
519 EDGE => {
520 let (edge, fraction) = entity
521 .rsplit_once('@')
522 .ok_or_else(|| format!("'{text}' is an edge anchor with no fraction"))?;
523 let fraction: f64 = fraction
524 .parse()
525 .map_err(|_| format!("'{fraction}' is not a fraction in '{text}'"))?;
526 if !fraction.is_finite() || !(0.0..=1.0).contains(&fraction) {
527 return Err(format!("'{text}': a fraction runs 0 to 1"));
528 }
529 Ok(SheetAnchor::edge(view, solid, edge, fraction))
530 }
531 CIRCLE => Ok(SheetAnchor::circle(view, solid, entity)),
532 other => Err(format!("no anchor kind '{other}' in '{text}'")),
533 }
534 }
535}
536
537#[derive(Debug, Clone, PartialEq, serde::Serialize)]
545pub struct DimensionDrawing {
546 pub id: String,
547 #[serde(rename = "type")]
548 pub kind: String,
549 pub text: String,
552 pub value: f64,
555 pub lines: Vec<Vec<[f64; 2]>>,
556 pub texts: Vec<SheetText>,
557 pub label: [f64; 2],
560 #[serde(rename = "offsetMm")]
561 pub offset_mm: f64,
562 #[serde(rename = "toleranceFrom", skip_serializing_if = "String::is_empty")]
567 pub tolerance_from: String,
568 #[serde(skip_serializing_if = "String::is_empty")]
572 pub error: String,
573}
574
575impl DimensionDrawing {
576 fn refused(dimension: &SheetDimension, label: [f64; 2], error: String) -> DimensionDrawing {
577 DimensionDrawing {
578 id: dimension.id.clone(),
579 kind: dimension.kind.clone(),
580 text: String::new(),
581 value: f64::NAN,
582 lines: Vec::new(),
583 texts: Vec::new(),
584 label,
585 offset_mm: dimension.offset_mm,
586 tolerance_from: String::new(),
587 error,
588 }
589 }
590}
591
592pub fn layout_dimensions(
596 dimensions: &[SheetDimension],
597 views: &[ViewDrawing],
598 centre: [f64; 2],
599) -> Vec<DimensionDrawing> {
600 dimensions
601 .iter()
602 .map(|dimension| layout_one(dimension, views, centre))
603 .collect()
604}
605
606fn layout_one(
607 dimension: &SheetDimension,
608 views: &[ViewDrawing],
609 centre: [f64; 2],
610) -> DimensionDrawing {
611 let wanted = dimension.anchor_count();
612 if dimension.anchors.len() != wanted {
613 return DimensionDrawing::refused(
614 dimension,
615 centre,
616 format!(
617 "a {} dimension takes {wanted} anchor{}; it has {}",
618 dimension.kind,
619 if wanted == 1 { "" } else { "s" },
620 dimension.anchors.len()
621 ),
622 );
623 }
624 let mut resolved: Vec<(&ViewDrawing, &AnchorPoint)> = Vec::new();
627 for reference in &dimension.anchors {
628 match resolve_anchor(reference, views) {
629 Ok(found) => resolved.push(found),
630 Err(error) => {
631 let label = resolved.first().map(|(_, a)| a.at).unwrap_or(centre);
632 return DimensionDrawing::refused(dimension, label, error);
633 }
634 }
635 }
636 if let [(first, _), rest @ ..] = resolved.as_slice() {
640 if let Some((other, _)) = rest.iter().find(|(view, _)| view.id != first.id) {
641 return DimensionDrawing::refused(
642 dimension,
643 resolved[0].1.at,
644 format!(
645 "anchors on two placements ('{}' and '{}'); a sheet dimension measures within one view",
646 first.id, other.id
647 ),
648 );
649 }
650 }
651 let view = resolved[0].0;
652 if view.scale <= 0.0 {
653 return DimensionDrawing::refused(
654 dimension,
655 resolved[0].1.at,
656 format!("placement '{}' has no scale", view.id),
657 );
658 }
659 if dimension.kind.eq_ignore_ascii_case(LINEAR) {
660 linear(dimension, view, resolved[0].1, resolved[1].1)
661 } else if dimension.kind.eq_ignore_ascii_case(ANGULAR) {
662 angular(dimension, view, resolved[0].1, resolved[1].1)
663 } else {
664 radial(dimension, view, resolved[0].1)
665 }
666}
667
668pub(super) fn resolve_anchor<'a>(
670 reference: &str,
671 views: &'a [ViewDrawing],
672) -> Result<(&'a ViewDrawing, &'a AnchorPoint), String> {
673 let anchor = SheetAnchor::parse(reference)?;
674 let Some(view) = views.iter().find(|view| view.id == anchor.view) else {
675 return Err(format!("'{reference}': no placed view '{}' on this sheet", anchor.view));
676 };
677 if !view.error.is_empty() {
678 return Err(format!("'{reference}': placement '{}' did not project", view.id));
679 }
680 view.anchors
681 .iter()
682 .find(|candidate| candidate.anchor == reference)
683 .map(|candidate| (view, candidate))
684 .ok_or_else(|| match anchor.kind.as_str() {
685 VERTEX => format!(
686 "'{reference}': '{}' has no vertex {} any more",
687 anchor.solid, anchor.vertex
688 ),
689 CIRCLE => format!("'{reference}': '{}' is not a circular edge any more", anchor.edge),
690 _ => format!("'{reference}': '{}' has no edge '{}' any more", anchor.solid, anchor.edge),
691 })
692}
693
694fn lift(p: [f64; 2]) -> [f64; 3] {
701 [p[0], -p[1], 0.0]
702}
703
704fn drop_to_paper(q: [f64; 3]) -> [f64; 2] {
706 [q[0], -q[1]]
707}
708
709fn lift_dir(d: [f64; 2]) -> [f64; 3] {
712 [d[0], -d[1], 0.0]
713}
714
715pub(super) fn text_height(view: &ViewDrawing) -> f64 {
720 if view.text_mm > 1e-9 {
721 view.text_mm
722 } else {
723 12.0 * super::project::MM_PER_POINT
724 }
725}
726
727fn style(view: &ViewDrawing) -> PmiLayoutStyle {
730 let text_height = text_height(view);
731 PmiLayoutStyle {
732 arrow: text_height * super::project::ARROW_PER_TEXT,
733 text_height,
734 view_dir: [0.0, 0.0, -1.0],
735 view_up: [0.0, 1.0, 0.0],
736 plane: None,
737 }
738}
739
740fn finish(
744 dimension: &SheetDimension,
745 drawn: brep_kernel::PmiPresentation,
746 value: f64,
747 (text, tolerance_from): (String, String),
748 reseat: &[(([f64; 3], [f64; 3]), [f64; 3])],
749) -> DimensionDrawing {
750 let mut lines: Vec<Vec<[f64; 2]>> = Vec::new();
751 for polyline in &drawn.polylines {
752 let seated = reseat.iter().find(|((from, _), _)| {
757 polyline.len() == 2 && near(polyline[0], *from)
758 });
759 match seated {
760 Some(((_, to), true_point)) => lines.push(vec![drop_to_paper(*true_point), drop_to_paper(*to)]),
761 None => lines.push(polyline.iter().map(|p| drop_to_paper(*p)).collect()),
762 }
763 }
764 for arrow in &drawn.arrows {
765 let mut closed: Vec<[f64; 2]> = arrow.iter().map(|p| drop_to_paper(*p)).collect();
766 closed.push(closed[0]);
767 lines.push(closed);
768 }
769 let texts: Vec<SheetText> = drawn
770 .texts
771 .iter()
772 .filter(|run| !run.text.trim().is_empty())
773 .map(|run| SheetText::flat(run.text.clone(), drop_to_paper(run.anchor), run.height))
774 .collect();
775 let label = texts.first().map(|run| run.anchor).unwrap_or(drop_to_paper(drawn.text_anchor));
776 let lines = super::project::break_for_text_boxes(lines, &texts);
777 DimensionDrawing {
778 id: dimension.id.clone(),
779 kind: dimension.kind.clone(),
780 text,
781 value,
782 lines,
783 texts,
784 label,
785 offset_mm: dimension.offset_mm,
786 tolerance_from,
787 error: String::new(),
788 }
789}
790
791fn value_text(
799 dimension: &SheetDimension,
800 view: &ViewDrawing,
801 value: f64,
802 (prefix, suffix): (&str, &str),
803 source: impl Fn(&Measures) -> bool,
804) -> (String, String) {
805 let (block, reference, from) = match inherited(dimension, view, source) {
806 Some(found) => (found.block, found.reference, found.id.clone()),
807 None => (dimension.tolerance.block(), dimension.tolerance.reference, String::new()),
808 };
809 (format_dimension(value, dimension.precision as usize, &block, reference, prefix, suffix), from)
810}
811
812pub fn inherited<'a>(
822 dimension: &SheetDimension,
823 view: &'a ViewDrawing,
824 source: impl Fn(&Measures) -> bool,
825) -> Option<&'a super::project::ToleranceSource> {
826 if !dimension.tolerance.is_plain() {
827 return None;
828 }
829 view.tolerances.iter().find(|candidate| source(&candidate.measures))
830}
831
832fn same_point(view: &ViewDrawing, a: [f64; 2], b: [f64; 2]) -> bool {
834 length2([a[0] - b[0], a[1] - b[1]]) <= super::hlr::Floors::of(view.scale).crossing_mm
835}
836
837fn near(a: [f64; 3], b: [f64; 3]) -> bool {
838 (0..3).all(|i| (a[i] - b[i]).abs() < 1e-9)
839}
840
841fn length2(v: [f64; 2]) -> f64 {
842 (v[0] * v[0] + v[1] * v[1]).sqrt()
843}
844
845fn cross2(a: [f64; 2], b: [f64; 2]) -> f64 {
847 a[0] * b[1] - a[1] * b[0]
848}
849
850fn linear(
855 dimension: &SheetDimension,
856 view: &ViewDrawing,
857 first: &AnchorPoint,
858 second: &AnchorPoint,
859) -> DimensionDrawing {
860 let (a, b) = (lift(first.at), lift(second.at));
861 let alignment = dimension.alignment.as_str();
862 let raw = [b[0] - a[0], b[1] - a[1]];
866 let flip = if alignment.eq_ignore_ascii_case(HORIZONTAL) {
867 raw[0] < 0.0
868 } else if alignment.eq_ignore_ascii_case(VERTICAL) {
869 raw[1] > 0.0
870 } else {
871 raw[0] < 0.0 || (raw[0] == 0.0 && raw[1] > 0.0)
872 };
873 let (a, b) = if flip { (b, a) } else { (a, b) };
874 let span = [b[0] - a[0], b[1] - a[1]];
875 let d = match alignment {
876 _ if alignment.eq_ignore_ascii_case(HORIZONTAL) => [1.0, 0.0],
877 _ if alignment.eq_ignore_ascii_case(VERTICAL) => [0.0, -1.0],
878 _ => {
879 let len = length2(span);
880 if len < 1e-9 {
881 return DimensionDrawing::refused(
882 dimension,
883 first.at,
884 "the two anchors project to the same point".into(),
885 );
886 }
887 [span[0] / len, span[1] / len]
888 }
889 };
890 let n = [-d[1], d[0]];
892 let value_mm = (span[0] * d[0] + span[1] * d[1]).abs();
893 if value_mm < 1e-9 {
894 return DimensionDrawing::refused(
895 dimension,
896 first.at,
897 format!("the two anchors have no {} separation", dimension.alignment),
898 );
899 }
900 let (ca, cb) = (a[0] * n[0] + a[1] * n[1], b[0] * n[0] + b[1] * n[1]);
906 let base = if dimension.offset_mm >= 0.0 { ca.min(cb) } else { ca.max(cb) };
907 let onto = |p: [f64; 3], c: f64| -> [f64; 3] {
908 let along = p[0] * d[0] + p[1] * d[1];
909 [along * d[0] + c * n[0], along * d[1] + c * n[1], 0.0]
910 };
911 let (pa, pb) = (onto(a, base), onto(b, base));
912 let aligned = alignment.eq_ignore_ascii_case(ALIGNED);
916 let value = if aligned {
917 first.model.iter().zip(second.model).map(|(a, b)| (b - a).powi(2)).sum::<f64>().sqrt()
918 } else {
919 value_mm / view.scale
920 };
921 let text = value_text(dimension, view, value, ("", ""), |measures| match measures {
925 Measures::Linear { a, b } => {
926 aligned
927 && ((same_point(view, *a, first.at) && same_point(view, *b, second.at))
928 || (same_point(view, *a, second.at) && same_point(view, *b, first.at)))
929 }
930 _ => false,
931 });
932 let style = style(view);
933 let half = super::project::text_half_width(&text.0, style.text_height);
941 let along = if 2.0 * (half + style.arrow) <= value_mm {
942 [(pa[0] + pb[0]) * 0.5, (pa[1] + pb[1]) * 0.5]
943 } else {
944 let past = style.arrow + half;
945 [pb[0] + d[0] * past, pb[1] + d[1] * past]
946 };
947 let label = [along[0] + n[0] * dimension.offset_mm, along[1] + n[1] * dimension.offset_mm, 0.0];
948 let drawn = pmi_present(
949 &PmiGeometry::Linear { a: pa, b: pb, component: None },
950 label,
951 &text.0,
952 &style,
953 );
954 let overshoot = style.arrow * 0.6;
959 let reseat: Vec<(([f64; 3], [f64; 3]), [f64; 3])> = [(pa, a), (pb, b)]
960 .iter()
961 .filter_map(|(projected, truth)| {
962 let foot = [
963 projected[0] + n[0] * dimension.offset_mm,
964 projected[1] + n[1] * dimension.offset_mm,
965 0.0,
966 ];
967 let away = [foot[0] - truth[0], foot[1] - truth[1]];
968 let len = length2(away);
969 if len < 1e-9 {
970 return None;
971 }
972 let end = [
973 foot[0] + away[0] / len * overshoot,
974 foot[1] + away[1] / len * overshoot,
975 0.0,
976 ];
977 Some(((*projected, end), *truth))
978 })
979 .collect();
980 finish(dimension, drawn, value, text, &reseat)
981}
982
983fn radial(dimension: &SheetDimension, view: &ViewDrawing, anchor: &AnchorPoint) -> DimensionDrawing {
988 let (Some(radius), Some(u), Some(v)) = (anchor.radius, anchor.u, anchor.v) else {
989 return DimensionDrawing::refused(
990 dimension,
991 anchor.at,
992 format!("'{}' is not a circular edge", anchor.anchor),
993 );
994 };
995 let diameter = dimension.kind.eq_ignore_ascii_case(DIAMETRAL);
996 let dir = RADIAL_DIR;
1002 let (u, v) = ([u[0], -u[1]], [v[0], -v[1]]);
1003 let theta = (-cross2(u, dir)).atan2(cross2(v, dir));
1004 let at = |t: f64| [u[0] * t.cos() + v[0] * t.sin(), u[1] * t.cos() + v[1] * t.sin()];
1005 let mut point = at(theta);
1006 if point[0] * dir[0] + point[1] * dir[1] < 0.0 {
1007 point = at(theta + std::f64::consts::PI);
1008 }
1009 let paper_radius = length2(point);
1010 if !(paper_radius > 1e-9) {
1011 return DimensionDrawing::refused(
1012 dimension,
1013 anchor.at,
1014 format!("'{}' projects edge-on: its circle has no radius on the paper", anchor.anchor),
1015 );
1016 }
1017 let centre = lift(anchor.at);
1018 let reach = paper_radius + dimension.offset_mm;
1019 let label = [centre[0] + dir[0] * reach, centre[1] + dir[1] * reach, 0.0];
1020 let value = if diameter { radius * 2.0 } else { radius };
1021 let bar = super::hlr::Floors::of(view.scale).bar;
1025 let text = value_text(
1026 dimension,
1027 view,
1028 value,
1029 (if diameter { "\u{2300}" } else { "R" }, ""),
1030 |measures| match measures {
1031 Measures::Radial { centre, radius: measured, diameter: pmi_diameter } => {
1032 *pmi_diameter == diameter && same_point(view, *centre, anchor.at) && (measured - radius).abs() <= bar
1033 }
1034 _ => false,
1035 },
1036 );
1037 let style = style(view);
1038 let drawn = pmi_present(
1039 &PmiGeometry::Radial {
1040 center: centre,
1041 axis: [0.0, 0.0, 1.0],
1042 radius: paper_radius,
1043 diameter,
1044 sphere: false,
1045 },
1046 label,
1047 &text.0,
1048 &style,
1049 );
1050 finish(dimension, drawn, value, text, &[])
1051}
1052
1053
1054fn angular(
1078 dimension: &SheetDimension,
1079 view: &ViewDrawing,
1080 first: &AnchorPoint,
1081 second: &AnchorPoint,
1082) -> DimensionDrawing {
1083 let direction = |anchor: &AnchorPoint| -> Result<[f64; 2], String> {
1084 anchor.dir.ok_or_else(|| {
1085 if anchor.kind == EDGE {
1086 format!(
1087 "'{}' is not a straight edge in this view; an angular dimension measures between two straight edges",
1088 anchor.anchor
1089 )
1090 } else {
1091 format!("'{}' is a {} anchor; an angular dimension measures between two EDGES", anchor.anchor, anchor.kind)
1092 }
1093 })
1094 };
1095 let (da, db) = match (direction(first), direction(second)) {
1096 (Ok(a), Ok(b)) => (a, b),
1097 (Err(error), _) | (_, Err(error)) => {
1098 return DimensionDrawing::refused(dimension, first.at, error)
1099 }
1100 };
1101 let denominator = cross2(da, db);
1105 if denominator.abs() < 1e-9 {
1106 return DimensionDrawing::refused(
1107 dimension,
1108 first.at,
1109 format!(
1110 "'{}' and '{}' are parallel in this view; they make no angle",
1111 first.anchor, second.anchor
1112 ),
1113 );
1114 }
1115 let between = [second.at[0] - first.at[0], second.at[1] - first.at[1]];
1116 let t = cross2(between, db) / denominator;
1117 let vertex = [first.at[0] + da[0] * t, first.at[1] + da[1] * t];
1118 let ray = |anchor: &AnchorPoint, fallback: [f64; 2]| -> [f64; 2] {
1122 let away = [anchor.at[0] - vertex[0], anchor.at[1] - vertex[1]];
1123 let length = length2(away);
1124 if length < 1e-9 {
1125 fallback
1126 } else {
1127 [away[0] / length, away[1] / length]
1128 }
1129 };
1130 let (ra, rb) = (ray(first, da), ray(second, db));
1131 let degrees = cross2(ra, rb).abs().atan2(ra[0] * rb[0] + ra[1] * rb[1]).to_degrees();
1132 if !(degrees > 1e-6) {
1133 return DimensionDrawing::refused(
1134 dimension,
1135 first.at,
1136 format!(
1137 "'{}' and '{}' leave the vertex the same way; they make no angle",
1138 first.anchor, second.anchor
1139 ),
1140 );
1141 }
1142 let (la, lb) = (lift_dir(ra), lift_dir(rb));
1146 let (dir_a, dir_b) = if cross2([la[0], la[1]], [lb[0], lb[1]]) >= 0.0 {
1147 (la, lb)
1148 } else {
1149 (lb, la)
1150 };
1151 let bisector = [dir_a[0] + dir_b[0], dir_a[1] + dir_b[1]];
1152 let length = length2([bisector[0], bisector[1]]);
1153 let bisector = [bisector[0] / length, bisector[1] / length, 0.0];
1155 let centre = lift(vertex);
1156 let label = [
1157 centre[0] + bisector[0] * dimension.offset_mm,
1158 centre[1] + bisector[1] * dimension.offset_mm,
1159 0.0,
1160 ];
1161 let text = value_text(dimension, view, degrees, ("", "\u{00B0}"), |_| false);
1167 let style = style(view);
1168 let drawn = pmi_present(
1169 &PmiGeometry::Angular {
1170 vertex: centre,
1171 dir_a,
1172 dir_b,
1173 axis: [0.0, 0.0, 1.0],
1174 degrees,
1175 },
1176 label,
1177 &text.0,
1178 &style,
1179 );
1180 finish(dimension, drawn, degrees, text, &[])
1181}
1182
1183