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 if let Some(offset) = params.get("offsetMm").and_then(Value::as_f64) {
369 if !offset.is_finite() {
370 return Err(format!("offsetMm: {offset} is not a length"));
371 }
372 self.offset_mm = offset;
373 }
374 if let Some(precision) = params.get("precision").and_then(Value::as_f64) {
375 if !precision.is_finite() || !(0.0..=8.0).contains(&precision) {
376 return Err(format!("precision: {precision} is not 0 to 8 decimals"));
377 }
378 self.precision = precision.round() as u32;
379 }
380 self.tolerance.apply(params)
381 }
382}
383
384pub fn sheet_dimension_schema() -> Value {
397 let mut fields = json!({
398 "id": { "type": "string", "default_value": null },
399 "kind": { "type": "options", "label": "Kind", "options": KINDS, "default_value": LINEAR },
400 "alignment": { "type": "options", "label": "Alignment", "options": ALIGNMENTS, "default_value": ALIGNED },
401 "anchors": { "type": "reference_selection", "selectionFilter": ["sheetAnchor"], "multiple": true },
402 "offsetMm": { "type": "number", "label": "Offset (mm)", "default_value": DEFAULT_OFFSET_MM },
403 "precision": { "type": "number", "label": "Precision (decimals)", "default_value": DEFAULT_PRECISION },
404 });
405 SheetTolerance::schema_fields(fields.as_object_mut().expect("an object"));
406 json!({
407 "type": "sheetDimension",
408 "shortName": "SD",
409 "longName": "Sheet dimension",
410 "inputParamsSchema": fields,
411 })
412}
413
414#[derive(Debug, Clone, PartialEq)]
420pub struct SheetAnchor {
421 pub view: String,
423 pub kind: String,
425 pub solid: String,
427 pub vertex: u64,
429 pub edge: String,
431 pub fraction: f64,
434}
435
436impl SheetAnchor {
437 pub fn vertex(view: &str, solid: &str, topo_id: u64) -> SheetAnchor {
438 SheetAnchor {
439 view: view.into(),
440 kind: VERTEX.into(),
441 solid: solid.into(),
442 vertex: topo_id,
443 edge: String::new(),
444 fraction: 0.0,
445 }
446 }
447
448 pub fn edge(view: &str, solid: &str, edge: &str, fraction: f64) -> SheetAnchor {
449 SheetAnchor {
450 view: view.into(),
451 kind: EDGE.into(),
452 solid: solid.into(),
453 vertex: 0,
454 edge: edge.into(),
455 fraction,
456 }
457 }
458
459 pub fn circle(view: &str, solid: &str, edge: &str) -> SheetAnchor {
460 SheetAnchor {
461 view: view.into(),
462 kind: CIRCLE.into(),
463 solid: solid.into(),
464 vertex: 0,
465 edge: edge.into(),
466 fraction: 0.0,
467 }
468 }
469
470 pub fn to_ref(&self) -> String {
477 match self.kind.as_str() {
478 EDGE => format!(
479 "{}:{}:{}#{}@{}",
480 self.view, self.kind, self.solid, self.edge, self.fraction
481 ),
482 CIRCLE => format!("{}:{}:{}#{}", self.view, self.kind, self.solid, self.edge),
483 _ => format!("{}:{}:{}#{}", self.view, VERTEX, self.solid, self.vertex),
484 }
485 }
486
487 pub fn parse(text: &str) -> Result<SheetAnchor, String> {
489 let bad = || format!("'{text}' is not a sheet anchor (expected SV2:vertex:Solid#7)");
490 let mut parts = text.splitn(3, ':');
491 let view = parts.next().unwrap_or_default().trim();
492 let kind = parts.next().ok_or_else(bad)?.trim();
493 let rest = parts.next().ok_or_else(bad)?;
494 if view.is_empty() {
495 return Err(bad());
496 }
497 let (solid, entity) = rest.split_once('#').ok_or_else(bad)?;
498 if solid.is_empty() || entity.is_empty() {
499 return Err(bad());
500 }
501 match kind {
502 VERTEX => {
503 let id: u64 = entity
504 .parse()
505 .map_err(|_| format!("'{entity}' is not a vertex id in '{text}'"))?;
506 Ok(SheetAnchor::vertex(view, solid, id))
507 }
508 EDGE => {
509 let (edge, fraction) = entity
510 .rsplit_once('@')
511 .ok_or_else(|| format!("'{text}' is an edge anchor with no fraction"))?;
512 let fraction: f64 = fraction
513 .parse()
514 .map_err(|_| format!("'{fraction}' is not a fraction in '{text}'"))?;
515 if !fraction.is_finite() || !(0.0..=1.0).contains(&fraction) {
516 return Err(format!("'{text}': a fraction runs 0 to 1"));
517 }
518 Ok(SheetAnchor::edge(view, solid, edge, fraction))
519 }
520 CIRCLE => Ok(SheetAnchor::circle(view, solid, entity)),
521 other => Err(format!("no anchor kind '{other}' in '{text}'")),
522 }
523 }
524}
525
526#[derive(Debug, Clone, PartialEq, serde::Serialize)]
534pub struct DimensionDrawing {
535 pub id: String,
536 #[serde(rename = "type")]
537 pub kind: String,
538 pub text: String,
541 pub value: f64,
544 pub lines: Vec<Vec<[f64; 2]>>,
545 pub texts: Vec<SheetText>,
546 pub label: [f64; 2],
549 #[serde(rename = "offsetMm")]
550 pub offset_mm: f64,
551 #[serde(rename = "toleranceFrom", skip_serializing_if = "String::is_empty")]
556 pub tolerance_from: String,
557 #[serde(skip_serializing_if = "String::is_empty")]
561 pub error: String,
562}
563
564impl DimensionDrawing {
565 fn refused(dimension: &SheetDimension, label: [f64; 2], error: String) -> DimensionDrawing {
566 DimensionDrawing {
567 id: dimension.id.clone(),
568 kind: dimension.kind.clone(),
569 text: String::new(),
570 value: f64::NAN,
571 lines: Vec::new(),
572 texts: Vec::new(),
573 label,
574 offset_mm: dimension.offset_mm,
575 tolerance_from: String::new(),
576 error,
577 }
578 }
579}
580
581pub fn layout_dimensions(
585 dimensions: &[SheetDimension],
586 views: &[ViewDrawing],
587 centre: [f64; 2],
588) -> Vec<DimensionDrawing> {
589 dimensions
590 .iter()
591 .map(|dimension| layout_one(dimension, views, centre))
592 .collect()
593}
594
595fn layout_one(
596 dimension: &SheetDimension,
597 views: &[ViewDrawing],
598 centre: [f64; 2],
599) -> DimensionDrawing {
600 let wanted = dimension.anchor_count();
601 if dimension.anchors.len() != wanted {
602 return DimensionDrawing::refused(
603 dimension,
604 centre,
605 format!(
606 "a {} dimension takes {wanted} anchor{}; it has {}",
607 dimension.kind,
608 if wanted == 1 { "" } else { "s" },
609 dimension.anchors.len()
610 ),
611 );
612 }
613 let mut resolved: Vec<(&ViewDrawing, &AnchorPoint)> = Vec::new();
616 for reference in &dimension.anchors {
617 match resolve_anchor(reference, views) {
618 Ok(found) => resolved.push(found),
619 Err(error) => {
620 let label = resolved.first().map(|(_, a)| a.at).unwrap_or(centre);
621 return DimensionDrawing::refused(dimension, label, error);
622 }
623 }
624 }
625 if let [(first, _), rest @ ..] = resolved.as_slice() {
629 if let Some((other, _)) = rest.iter().find(|(view, _)| view.id != first.id) {
630 return DimensionDrawing::refused(
631 dimension,
632 resolved[0].1.at,
633 format!(
634 "anchors on two placements ('{}' and '{}'); a sheet dimension measures within one view",
635 first.id, other.id
636 ),
637 );
638 }
639 }
640 let view = resolved[0].0;
641 if view.scale <= 0.0 {
642 return DimensionDrawing::refused(
643 dimension,
644 resolved[0].1.at,
645 format!("placement '{}' has no scale", view.id),
646 );
647 }
648 if dimension.kind.eq_ignore_ascii_case(LINEAR) {
649 linear(dimension, view, resolved[0].1, resolved[1].1)
650 } else if dimension.kind.eq_ignore_ascii_case(ANGULAR) {
651 angular(dimension, view, resolved[0].1, resolved[1].1)
652 } else {
653 radial(dimension, view, resolved[0].1)
654 }
655}
656
657pub(super) fn resolve_anchor<'a>(
659 reference: &str,
660 views: &'a [ViewDrawing],
661) -> Result<(&'a ViewDrawing, &'a AnchorPoint), String> {
662 let anchor = SheetAnchor::parse(reference)?;
663 let Some(view) = views.iter().find(|view| view.id == anchor.view) else {
664 return Err(format!("'{reference}': no placed view '{}' on this sheet", anchor.view));
665 };
666 if !view.error.is_empty() {
667 return Err(format!("'{reference}': placement '{}' did not project", view.id));
668 }
669 view.anchors
670 .iter()
671 .find(|candidate| candidate.anchor == reference)
672 .map(|candidate| (view, candidate))
673 .ok_or_else(|| match anchor.kind.as_str() {
674 VERTEX => format!(
675 "'{reference}': '{}' has no vertex {} any more",
676 anchor.solid, anchor.vertex
677 ),
678 CIRCLE => format!("'{reference}': '{}' is not a circular edge any more", anchor.edge),
679 _ => format!("'{reference}': '{}' has no edge '{}' any more", anchor.solid, anchor.edge),
680 })
681}
682
683fn lift(p: [f64; 2]) -> [f64; 3] {
690 [p[0], -p[1], 0.0]
691}
692
693fn drop_to_paper(q: [f64; 3]) -> [f64; 2] {
695 [q[0], -q[1]]
696}
697
698fn lift_dir(d: [f64; 2]) -> [f64; 3] {
701 [d[0], -d[1], 0.0]
702}
703
704pub(super) fn text_height(view: &ViewDrawing) -> f64 {
709 if view.text_mm > 1e-9 {
710 view.text_mm
711 } else {
712 12.0 * super::project::MM_PER_POINT
713 }
714}
715
716fn style(view: &ViewDrawing) -> PmiLayoutStyle {
719 let text_height = text_height(view);
720 PmiLayoutStyle {
721 arrow: text_height * super::project::ARROW_PER_TEXT,
722 text_height,
723 view_dir: [0.0, 0.0, -1.0],
724 view_up: [0.0, 1.0, 0.0],
725 plane: None,
726 }
727}
728
729fn finish(
733 dimension: &SheetDimension,
734 drawn: brep_kernel::PmiPresentation,
735 value: f64,
736 (text, tolerance_from): (String, String),
737 reseat: &[(([f64; 3], [f64; 3]), [f64; 3])],
738) -> DimensionDrawing {
739 let mut lines: Vec<Vec<[f64; 2]>> = Vec::new();
740 for polyline in &drawn.polylines {
741 let seated = reseat.iter().find(|((from, _), _)| {
746 polyline.len() == 2 && near(polyline[0], *from)
747 });
748 match seated {
749 Some(((_, to), true_point)) => lines.push(vec![drop_to_paper(*true_point), drop_to_paper(*to)]),
750 None => lines.push(polyline.iter().map(|p| drop_to_paper(*p)).collect()),
751 }
752 }
753 for arrow in &drawn.arrows {
754 let mut closed: Vec<[f64; 2]> = arrow.iter().map(|p| drop_to_paper(*p)).collect();
755 closed.push(closed[0]);
756 lines.push(closed);
757 }
758 let texts: Vec<SheetText> = drawn
759 .texts
760 .iter()
761 .filter(|run| !run.text.trim().is_empty())
762 .map(|run| SheetText::flat(run.text.clone(), drop_to_paper(run.anchor), run.height))
763 .collect();
764 let label = texts.first().map(|run| run.anchor).unwrap_or(drop_to_paper(drawn.text_anchor));
765 let lines = super::project::break_for_text_boxes(lines, &texts);
766 DimensionDrawing {
767 id: dimension.id.clone(),
768 kind: dimension.kind.clone(),
769 text,
770 value,
771 lines,
772 texts,
773 label,
774 offset_mm: dimension.offset_mm,
775 tolerance_from,
776 error: String::new(),
777 }
778}
779
780fn value_text(
788 dimension: &SheetDimension,
789 view: &ViewDrawing,
790 value: f64,
791 (prefix, suffix): (&str, &str),
792 source: impl Fn(&Measures) -> bool,
793) -> (String, String) {
794 let (block, reference, from) = match inherited(dimension, view, source) {
795 Some(found) => (found.block, found.reference, found.id.clone()),
796 None => (dimension.tolerance.block(), dimension.tolerance.reference, String::new()),
797 };
798 (format_dimension(value, dimension.precision as usize, &block, reference, prefix, suffix), from)
799}
800
801pub fn inherited<'a>(
811 dimension: &SheetDimension,
812 view: &'a ViewDrawing,
813 source: impl Fn(&Measures) -> bool,
814) -> Option<&'a super::project::ToleranceSource> {
815 if !dimension.tolerance.is_plain() {
816 return None;
817 }
818 view.tolerances.iter().find(|candidate| source(&candidate.measures))
819}
820
821fn same_point(view: &ViewDrawing, a: [f64; 2], b: [f64; 2]) -> bool {
823 length2([a[0] - b[0], a[1] - b[1]]) <= super::hlr::Floors::of(view.scale).crossing_mm
824}
825
826fn near(a: [f64; 3], b: [f64; 3]) -> bool {
827 (0..3).all(|i| (a[i] - b[i]).abs() < 1e-9)
828}
829
830fn length2(v: [f64; 2]) -> f64 {
831 (v[0] * v[0] + v[1] * v[1]).sqrt()
832}
833
834fn cross2(a: [f64; 2], b: [f64; 2]) -> f64 {
836 a[0] * b[1] - a[1] * b[0]
837}
838
839fn linear(
844 dimension: &SheetDimension,
845 view: &ViewDrawing,
846 first: &AnchorPoint,
847 second: &AnchorPoint,
848) -> DimensionDrawing {
849 let (a, b) = (lift(first.at), lift(second.at));
850 let alignment = dimension.alignment.as_str();
851 let raw = [b[0] - a[0], b[1] - a[1]];
855 let flip = if alignment.eq_ignore_ascii_case(HORIZONTAL) {
856 raw[0] < 0.0
857 } else if alignment.eq_ignore_ascii_case(VERTICAL) {
858 raw[1] > 0.0
859 } else {
860 raw[0] < 0.0 || (raw[0] == 0.0 && raw[1] > 0.0)
861 };
862 let (a, b) = if flip { (b, a) } else { (a, b) };
863 let span = [b[0] - a[0], b[1] - a[1]];
864 let d = match alignment {
865 _ if alignment.eq_ignore_ascii_case(HORIZONTAL) => [1.0, 0.0],
866 _ if alignment.eq_ignore_ascii_case(VERTICAL) => [0.0, -1.0],
867 _ => {
868 let len = length2(span);
869 if len < 1e-9 {
870 return DimensionDrawing::refused(
871 dimension,
872 first.at,
873 "the two anchors project to the same point".into(),
874 );
875 }
876 [span[0] / len, span[1] / len]
877 }
878 };
879 let n = [-d[1], d[0]];
881 let value_mm = (span[0] * d[0] + span[1] * d[1]).abs();
882 if value_mm < 1e-9 {
883 return DimensionDrawing::refused(
884 dimension,
885 first.at,
886 format!("the two anchors have no {} separation", dimension.alignment),
887 );
888 }
889 let (ca, cb) = (a[0] * n[0] + a[1] * n[1], b[0] * n[0] + b[1] * n[1]);
895 let base = if dimension.offset_mm >= 0.0 { ca.min(cb) } else { ca.max(cb) };
896 let onto = |p: [f64; 3], c: f64| -> [f64; 3] {
897 let along = p[0] * d[0] + p[1] * d[1];
898 [along * d[0] + c * n[0], along * d[1] + c * n[1], 0.0]
899 };
900 let (pa, pb) = (onto(a, base), onto(b, base));
901 let aligned = alignment.eq_ignore_ascii_case(ALIGNED);
905 let value = if aligned {
906 first.model.iter().zip(second.model).map(|(a, b)| (b - a).powi(2)).sum::<f64>().sqrt()
907 } else {
908 value_mm / view.scale
909 };
910 let text = value_text(dimension, view, value, ("", ""), |measures| match measures {
914 Measures::Linear { a, b } => {
915 aligned
916 && ((same_point(view, *a, first.at) && same_point(view, *b, second.at))
917 || (same_point(view, *a, second.at) && same_point(view, *b, first.at)))
918 }
919 _ => false,
920 });
921 let style = style(view);
922 let half = super::project::text_half_width(&text.0, style.text_height);
930 let along = if 2.0 * (half + style.arrow) <= value_mm {
931 [(pa[0] + pb[0]) * 0.5, (pa[1] + pb[1]) * 0.5]
932 } else {
933 let past = style.arrow + half;
934 [pb[0] + d[0] * past, pb[1] + d[1] * past]
935 };
936 let label = [along[0] + n[0] * dimension.offset_mm, along[1] + n[1] * dimension.offset_mm, 0.0];
937 let drawn = pmi_present(
938 &PmiGeometry::Linear { a: pa, b: pb, component: None },
939 label,
940 &text.0,
941 &style,
942 );
943 let overshoot = style.arrow * 0.6;
948 let reseat: Vec<(([f64; 3], [f64; 3]), [f64; 3])> = [(pa, a), (pb, b)]
949 .iter()
950 .filter_map(|(projected, truth)| {
951 let foot = [
952 projected[0] + n[0] * dimension.offset_mm,
953 projected[1] + n[1] * dimension.offset_mm,
954 0.0,
955 ];
956 let away = [foot[0] - truth[0], foot[1] - truth[1]];
957 let len = length2(away);
958 if len < 1e-9 {
959 return None;
960 }
961 let end = [
962 foot[0] + away[0] / len * overshoot,
963 foot[1] + away[1] / len * overshoot,
964 0.0,
965 ];
966 Some(((*projected, end), *truth))
967 })
968 .collect();
969 finish(dimension, drawn, value, text, &reseat)
970}
971
972fn radial(dimension: &SheetDimension, view: &ViewDrawing, anchor: &AnchorPoint) -> DimensionDrawing {
977 let (Some(radius), Some(u), Some(v)) = (anchor.radius, anchor.u, anchor.v) else {
978 return DimensionDrawing::refused(
979 dimension,
980 anchor.at,
981 format!("'{}' is not a circular edge", anchor.anchor),
982 );
983 };
984 let diameter = dimension.kind.eq_ignore_ascii_case(DIAMETRAL);
985 let dir = RADIAL_DIR;
991 let (u, v) = ([u[0], -u[1]], [v[0], -v[1]]);
992 let theta = (-cross2(u, dir)).atan2(cross2(v, dir));
993 let at = |t: f64| [u[0] * t.cos() + v[0] * t.sin(), u[1] * t.cos() + v[1] * t.sin()];
994 let mut point = at(theta);
995 if point[0] * dir[0] + point[1] * dir[1] < 0.0 {
996 point = at(theta + std::f64::consts::PI);
997 }
998 let paper_radius = length2(point);
999 if !(paper_radius > 1e-9) {
1000 return DimensionDrawing::refused(
1001 dimension,
1002 anchor.at,
1003 format!("'{}' projects edge-on: its circle has no radius on the paper", anchor.anchor),
1004 );
1005 }
1006 let centre = lift(anchor.at);
1007 let reach = paper_radius + dimension.offset_mm;
1008 let label = [centre[0] + dir[0] * reach, centre[1] + dir[1] * reach, 0.0];
1009 let value = if diameter { radius * 2.0 } else { radius };
1010 let bar = super::hlr::Floors::of(view.scale).bar;
1014 let text = value_text(
1015 dimension,
1016 view,
1017 value,
1018 (if diameter { "\u{2300}" } else { "R" }, ""),
1019 |measures| match measures {
1020 Measures::Radial { centre, radius: measured, diameter: pmi_diameter } => {
1021 *pmi_diameter == diameter && same_point(view, *centre, anchor.at) && (measured - radius).abs() <= bar
1022 }
1023 _ => false,
1024 },
1025 );
1026 let style = style(view);
1027 let drawn = pmi_present(
1028 &PmiGeometry::Radial {
1029 center: centre,
1030 axis: [0.0, 0.0, 1.0],
1031 radius: paper_radius,
1032 diameter,
1033 sphere: false,
1034 },
1035 label,
1036 &text.0,
1037 &style,
1038 );
1039 finish(dimension, drawn, value, text, &[])
1040}
1041
1042
1043fn angular(
1067 dimension: &SheetDimension,
1068 view: &ViewDrawing,
1069 first: &AnchorPoint,
1070 second: &AnchorPoint,
1071) -> DimensionDrawing {
1072 let direction = |anchor: &AnchorPoint| -> Result<[f64; 2], String> {
1073 anchor.dir.ok_or_else(|| {
1074 if anchor.kind == EDGE {
1075 format!(
1076 "'{}' is not a straight edge in this view; an angular dimension measures between two straight edges",
1077 anchor.anchor
1078 )
1079 } else {
1080 format!("'{}' is a {} anchor; an angular dimension measures between two EDGES", anchor.anchor, anchor.kind)
1081 }
1082 })
1083 };
1084 let (da, db) = match (direction(first), direction(second)) {
1085 (Ok(a), Ok(b)) => (a, b),
1086 (Err(error), _) | (_, Err(error)) => {
1087 return DimensionDrawing::refused(dimension, first.at, error)
1088 }
1089 };
1090 let denominator = cross2(da, db);
1094 if denominator.abs() < 1e-9 {
1095 return DimensionDrawing::refused(
1096 dimension,
1097 first.at,
1098 format!(
1099 "'{}' and '{}' are parallel in this view; they make no angle",
1100 first.anchor, second.anchor
1101 ),
1102 );
1103 }
1104 let between = [second.at[0] - first.at[0], second.at[1] - first.at[1]];
1105 let t = cross2(between, db) / denominator;
1106 let vertex = [first.at[0] + da[0] * t, first.at[1] + da[1] * t];
1107 let ray = |anchor: &AnchorPoint, fallback: [f64; 2]| -> [f64; 2] {
1111 let away = [anchor.at[0] - vertex[0], anchor.at[1] - vertex[1]];
1112 let length = length2(away);
1113 if length < 1e-9 {
1114 fallback
1115 } else {
1116 [away[0] / length, away[1] / length]
1117 }
1118 };
1119 let (ra, rb) = (ray(first, da), ray(second, db));
1120 let degrees = cross2(ra, rb).abs().atan2(ra[0] * rb[0] + ra[1] * rb[1]).to_degrees();
1121 if !(degrees > 1e-6) {
1122 return DimensionDrawing::refused(
1123 dimension,
1124 first.at,
1125 format!(
1126 "'{}' and '{}' leave the vertex the same way; they make no angle",
1127 first.anchor, second.anchor
1128 ),
1129 );
1130 }
1131 let (la, lb) = (lift_dir(ra), lift_dir(rb));
1135 let (dir_a, dir_b) = if cross2([la[0], la[1]], [lb[0], lb[1]]) >= 0.0 {
1136 (la, lb)
1137 } else {
1138 (lb, la)
1139 };
1140 let bisector = [dir_a[0] + dir_b[0], dir_a[1] + dir_b[1]];
1141 let length = length2([bisector[0], bisector[1]]);
1142 let bisector = [bisector[0] / length, bisector[1] / length, 0.0];
1144 let centre = lift(vertex);
1145 let label = [
1146 centre[0] + bisector[0] * dimension.offset_mm,
1147 centre[1] + bisector[1] * dimension.offset_mm,
1148 0.0,
1149 ];
1150 let text = value_text(dimension, view, degrees, ("", "\u{00B0}"), |_| false);
1156 let style = style(view);
1157 let drawn = pmi_present(
1158 &PmiGeometry::Angular {
1159 vertex: centre,
1160 dir_a,
1161 dir_b,
1162 axis: [0.0, 0.0, 1.0],
1163 degrees,
1164 },
1165 label,
1166 &text.0,
1167 &style,
1168 );
1169 finish(dimension, drawn, degrees, text, &[])
1170}
1171
1172