use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use super::project::{AnchorPoint, Measures, SheetText, ViewDrawing};
use brep_kernel::{format_dimension, pmi_present, PmiGeometry, PmiLayoutStyle, ToleranceBlock, ToleranceMode};
pub const LINEAR: &str = "linear";
pub const RADIAL: &str = "radial";
pub const DIAMETRAL: &str = "diametral";
pub const ANGULAR: &str = "angular";
pub const KINDS: &[&str] = &[LINEAR, RADIAL, DIAMETRAL, ANGULAR];
pub fn anchor_count(kind: &str) -> usize {
if kind.eq_ignore_ascii_case(LINEAR) || kind.eq_ignore_ascii_case(ANGULAR) {
2
} else {
1
}
}
pub const HORIZONTAL: &str = "horizontal";
pub const VERTICAL: &str = "vertical";
pub const ALIGNED: &str = "aligned";
pub const ALIGNMENTS: &[&str] = &[HORIZONTAL, VERTICAL, ALIGNED];
pub const VERTEX: &str = "vertex";
pub const EDGE: &str = "edge";
pub const CIRCLE: &str = "circle";
pub const DEFAULT_OFFSET_MM: f64 = 12.0;
pub const DEFAULT_PRECISION: u32 = 3;
const RADIAL_DIR: [f64; 2] = [std::f64::consts::FRAC_1_SQRT_2, std::f64::consts::FRAC_1_SQRT_2];
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SheetTolerance {
#[serde(default = "default_tol_mode", rename = "tolMode", skip_serializing_if = "is_no_tolerance")]
pub mode: String,
#[serde(default, rename = "tolUpper", skip_serializing_if = "is_zero")]
pub upper: f64,
#[serde(default, rename = "tolLower", skip_serializing_if = "is_zero")]
pub lower: f64,
#[serde(default, rename = "isReference", skip_serializing_if = "is_false")]
pub reference: bool,
}
impl Default for SheetTolerance {
fn default() -> SheetTolerance {
SheetTolerance { mode: default_tol_mode(), upper: 0.0, lower: 0.0, reference: false }
}
}
fn default_tol_mode() -> String {
ToleranceMode::None.as_str().to_string()
}
fn is_no_tolerance(mode: &String) -> bool {
ToleranceMode::parse(mode) == ToleranceMode::None
}
fn is_zero(value: &f64) -> bool {
*value == 0.0
}
fn is_false(flag: &bool) -> bool {
!*flag
}
pub fn tolerance_modes() -> Vec<&'static str> {
[ToleranceMode::None, ToleranceMode::Symmetric, ToleranceMode::Deviation, ToleranceMode::Limits]
.iter()
.map(|mode| mode.as_str())
.collect()
}
impl SheetTolerance {
pub fn block(&self) -> ToleranceBlock {
ToleranceBlock { mode: ToleranceMode::parse(&self.mode), upper: self.upper.abs(), lower: self.lower.abs() }
}
pub fn is_plain(&self) -> bool {
self.block().mode == ToleranceMode::None && !self.reference
}
pub fn write_params(&self, params: &mut Value) {
params["tolMode"] = json!(ToleranceMode::parse(&self.mode).as_str());
params["tolUpper"] = json!(self.upper);
params["tolLower"] = json!(self.lower);
params["isReference"] = json!(self.reference);
}
pub fn apply(&mut self, params: &Value) -> Result<(), String> {
if let Some(mode) = params.get("tolMode").and_then(Value::as_str) {
let mode = mode.trim();
let Some(known) = tolerance_modes().into_iter().find(|known| known.eq_ignore_ascii_case(mode)) else {
return Err(format!("no tolerance mode '{mode}' (one of {})", tolerance_modes().join(", ")));
};
self.mode = known.to_string();
}
for (key, slot) in [("tolUpper", &mut self.upper), ("tolLower", &mut self.lower)] {
if let Some(value) = params.get(key).and_then(Value::as_f64) {
if !value.is_finite() {
return Err(format!("{key}: {value} is not a deviation"));
}
*slot = value.abs();
}
}
if let Some(reference) = params.get("isReference").and_then(Value::as_bool) {
self.reference = reference;
}
Ok(())
}
pub fn schema_fields(schema: &mut serde_json::Map<String, Value>) {
schema.insert(
"isReference".into(),
json!({ "type": "boolean", "label": "Reference", "default_value": false, "hint": "A reference dimension: shown in parentheses, no tolerance" }),
);
schema.insert(
"tolMode".into(),
json!({
"type": "options", "label": "Tolerance", "options": tolerance_modes(), "default_value": ToleranceMode::None.as_str(),
"hint": "none \u{00B7} \u{00B1} symmetric \u{00B7} +upper/\u{2212}lower deviation \u{00B7} upper/lower limit values"
}),
);
schema.insert(
"tolUpper".into(),
json!({ "type": "number", "label": "Upper (+)", "default_value": 0.0, "step": 0.01, "hint": "The upper deviation (the \u{00B1} value for symmetric)" }),
);
schema.insert(
"tolLower".into(),
json!({ "type": "number", "label": "Lower (\u{2212})", "default_value": 0.0, "step": 0.01, "hint": "The lower deviation (deviation / limits modes)" }),
);
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SheetDimension {
pub id: String,
#[serde(default = "default_kind")]
pub kind: String,
#[serde(default = "default_alignment")]
pub alignment: String,
#[serde(default)]
pub anchors: Vec<String>,
#[serde(default = "default_offset", rename = "offsetMm")]
pub offset_mm: f64,
#[serde(default = "default_precision")]
pub precision: u32,
#[serde(flatten)]
pub tolerance: SheetTolerance,
}
fn default_kind() -> String {
LINEAR.to_string()
}
fn default_alignment() -> String {
ALIGNED.to_string()
}
fn default_offset() -> f64 {
DEFAULT_OFFSET_MM
}
fn default_precision() -> u32 {
DEFAULT_PRECISION
}
impl SheetDimension {
pub fn new(id: String, kind: &str, alignment: &str, anchors: Vec<String>) -> SheetDimension {
SheetDimension {
id,
kind: kind.to_string(),
alignment: alignment.to_string(),
anchors,
offset_mm: DEFAULT_OFFSET_MM,
precision: DEFAULT_PRECISION,
tolerance: SheetTolerance::default(),
}
}
pub fn anchor_count(&self) -> usize {
anchor_count(&self.kind)
}
pub fn params(&self) -> Value {
let mut params = json!({
"id": self.id,
"kind": self.kind,
"alignment": self.alignment,
"anchors": self.anchors,
"offsetMm": self.offset_mm,
"precision": self.precision,
});
self.tolerance.write_params(&mut params);
params
}
pub fn apply(&mut self, params: &Value) -> Result<(), String> {
if let Some(kind) = params.get("kind").and_then(Value::as_str) {
let kind = kind.trim();
let Some(known) = KINDS.iter().find(|k| k.eq_ignore_ascii_case(kind)) else {
return Err(format!("no dimension kind '{kind}'"));
};
self.kind = (*known).to_string();
}
if let Some(alignment) = params.get("alignment").and_then(Value::as_str) {
let alignment = alignment.trim();
let Some(known) = ALIGNMENTS.iter().find(|a| a.eq_ignore_ascii_case(alignment)) else {
return Err(format!("no alignment '{alignment}'"));
};
self.alignment = (*known).to_string();
}
if let Some(anchors) = params.get("anchors") {
let list: Vec<String> = crate::json_support::string_values(Some(anchors))
.map(str::to_string)
.collect();
for anchor in &list {
SheetAnchor::parse(anchor)?;
}
self.anchors = list;
}
apply_offset_precision(params, &mut self.offset_mm, &mut self.precision)?;
self.tolerance.apply(params)
}
}
pub(super) fn apply_offset_precision(
params: &Value,
offset_mm: &mut f64,
precision: &mut u32,
) -> Result<(), String> {
if let Some(offset) = params.get("offsetMm").and_then(Value::as_f64) {
if !offset.is_finite() {
return Err(format!("offsetMm: {offset} is not a length"));
}
*offset_mm = offset;
}
if let Some(value) = params.get("precision").and_then(Value::as_f64) {
if !value.is_finite() || !(0.0..=8.0).contains(&value) {
return Err(format!("precision: {value} is not 0 to 8 decimals"));
}
*precision = value.round() as u32;
}
Ok(())
}
pub fn sheet_dimension_schema() -> Value {
let mut fields = json!({
"id": { "type": "string", "default_value": null },
"kind": { "type": "options", "label": "Kind", "options": KINDS, "default_value": LINEAR },
"alignment": { "type": "options", "label": "Alignment", "options": ALIGNMENTS, "default_value": ALIGNED },
"anchors": { "type": "reference_selection", "selectionFilter": ["sheetAnchor"], "multiple": true },
"offsetMm": { "type": "number", "label": "Offset (mm)", "default_value": DEFAULT_OFFSET_MM },
"precision": { "type": "number", "label": "Precision (decimals)", "default_value": DEFAULT_PRECISION },
});
SheetTolerance::schema_fields(fields.as_object_mut().expect("an object"));
json!({
"type": "sheetDimension",
"shortName": "SD",
"longName": "Sheet dimension",
"inputParamsSchema": fields,
})
}
#[derive(Debug, Clone, PartialEq)]
pub struct SheetAnchor {
pub view: String,
pub kind: String,
pub solid: String,
pub vertex: u64,
pub edge: String,
pub fraction: f64,
}
impl SheetAnchor {
pub fn vertex(view: &str, solid: &str, topo_id: u64) -> SheetAnchor {
SheetAnchor {
view: view.into(),
kind: VERTEX.into(),
solid: solid.into(),
vertex: topo_id,
edge: String::new(),
fraction: 0.0,
}
}
pub fn edge(view: &str, solid: &str, edge: &str, fraction: f64) -> SheetAnchor {
SheetAnchor {
view: view.into(),
kind: EDGE.into(),
solid: solid.into(),
vertex: 0,
edge: edge.into(),
fraction,
}
}
pub fn circle(view: &str, solid: &str, edge: &str) -> SheetAnchor {
SheetAnchor {
view: view.into(),
kind: CIRCLE.into(),
solid: solid.into(),
vertex: 0,
edge: edge.into(),
fraction: 0.0,
}
}
pub fn to_ref(&self) -> String {
match self.kind.as_str() {
EDGE => format!(
"{}:{}:{}#{}@{}",
self.view, self.kind, self.solid, self.edge, self.fraction
),
CIRCLE => format!("{}:{}:{}#{}", self.view, self.kind, self.solid, self.edge),
_ => format!("{}:{}:{}#{}", self.view, VERTEX, self.solid, self.vertex),
}
}
pub fn parse(text: &str) -> Result<SheetAnchor, String> {
let bad = || format!("'{text}' is not a sheet anchor (expected SV2:vertex:Solid#7)");
let mut parts = text.splitn(3, ':');
let view = parts.next().unwrap_or_default().trim();
let kind = parts.next().ok_or_else(bad)?.trim();
let rest = parts.next().ok_or_else(bad)?;
if view.is_empty() {
return Err(bad());
}
let (solid, entity) = rest.split_once('#').ok_or_else(bad)?;
if solid.is_empty() || entity.is_empty() {
return Err(bad());
}
match kind {
VERTEX => {
let id: u64 = entity
.parse()
.map_err(|_| format!("'{entity}' is not a vertex id in '{text}'"))?;
Ok(SheetAnchor::vertex(view, solid, id))
}
EDGE => {
let (edge, fraction) = entity
.rsplit_once('@')
.ok_or_else(|| format!("'{text}' is an edge anchor with no fraction"))?;
let fraction: f64 = fraction
.parse()
.map_err(|_| format!("'{fraction}' is not a fraction in '{text}'"))?;
if !fraction.is_finite() || !(0.0..=1.0).contains(&fraction) {
return Err(format!("'{text}': a fraction runs 0 to 1"));
}
Ok(SheetAnchor::edge(view, solid, edge, fraction))
}
CIRCLE => Ok(SheetAnchor::circle(view, solid, entity)),
other => Err(format!("no anchor kind '{other}' in '{text}'")),
}
}
}
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct DimensionDrawing {
pub id: String,
#[serde(rename = "type")]
pub kind: String,
pub text: String,
pub value: f64,
pub lines: Vec<Vec<[f64; 2]>>,
pub texts: Vec<SheetText>,
pub label: [f64; 2],
#[serde(rename = "offsetMm")]
pub offset_mm: f64,
#[serde(rename = "toleranceFrom", skip_serializing_if = "String::is_empty")]
pub tolerance_from: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub error: String,
}
impl DimensionDrawing {
fn refused(dimension: &SheetDimension, label: [f64; 2], error: String) -> DimensionDrawing {
DimensionDrawing {
id: dimension.id.clone(),
kind: dimension.kind.clone(),
text: String::new(),
value: f64::NAN,
lines: Vec::new(),
texts: Vec::new(),
label,
offset_mm: dimension.offset_mm,
tolerance_from: String::new(),
error,
}
}
}
pub fn layout_dimensions(
dimensions: &[SheetDimension],
views: &[ViewDrawing],
centre: [f64; 2],
) -> Vec<DimensionDrawing> {
dimensions
.iter()
.map(|dimension| layout_one(dimension, views, centre))
.collect()
}
fn layout_one(
dimension: &SheetDimension,
views: &[ViewDrawing],
centre: [f64; 2],
) -> DimensionDrawing {
let wanted = dimension.anchor_count();
if dimension.anchors.len() != wanted {
return DimensionDrawing::refused(
dimension,
centre,
format!(
"a {} dimension takes {wanted} anchor{}; it has {}",
dimension.kind,
if wanted == 1 { "" } else { "s" },
dimension.anchors.len()
),
);
}
let mut resolved: Vec<(&ViewDrawing, &AnchorPoint)> = Vec::new();
for reference in &dimension.anchors {
match resolve_anchor(reference, views) {
Ok(found) => resolved.push(found),
Err(error) => {
let label = resolved.first().map(|(_, a)| a.at).unwrap_or(centre);
return DimensionDrawing::refused(dimension, label, error);
}
}
}
if let [(first, _), rest @ ..] = resolved.as_slice() {
if let Some((other, _)) = rest.iter().find(|(view, _)| view.id != first.id) {
return DimensionDrawing::refused(
dimension,
resolved[0].1.at,
format!(
"anchors on two placements ('{}' and '{}'); a sheet dimension measures within one view",
first.id, other.id
),
);
}
}
let view = resolved[0].0;
if view.scale <= 0.0 {
return DimensionDrawing::refused(
dimension,
resolved[0].1.at,
format!("placement '{}' has no scale", view.id),
);
}
if dimension.kind.eq_ignore_ascii_case(LINEAR) {
linear(dimension, view, resolved[0].1, resolved[1].1)
} else if dimension.kind.eq_ignore_ascii_case(ANGULAR) {
angular(dimension, view, resolved[0].1, resolved[1].1)
} else {
radial(dimension, view, resolved[0].1)
}
}
pub(super) fn resolve_anchor<'a>(
reference: &str,
views: &'a [ViewDrawing],
) -> Result<(&'a ViewDrawing, &'a AnchorPoint), String> {
let anchor = SheetAnchor::parse(reference)?;
let Some(view) = views.iter().find(|view| view.id == anchor.view) else {
return Err(format!("'{reference}': no placed view '{}' on this sheet", anchor.view));
};
if !view.error.is_empty() {
return Err(format!("'{reference}': placement '{}' did not project", view.id));
}
view.anchors
.iter()
.find(|candidate| candidate.anchor == reference)
.map(|candidate| (view, candidate))
.ok_or_else(|| match anchor.kind.as_str() {
VERTEX => format!(
"'{reference}': '{}' has no vertex {} any more",
anchor.solid, anchor.vertex
),
CIRCLE => format!("'{reference}': '{}' is not a circular edge any more", anchor.edge),
_ => format!("'{reference}': '{}' has no edge '{}' any more", anchor.solid, anchor.edge),
})
}
fn lift(p: [f64; 2]) -> [f64; 3] {
[p[0], -p[1], 0.0]
}
fn drop_to_paper(q: [f64; 3]) -> [f64; 2] {
[q[0], -q[1]]
}
fn lift_dir(d: [f64; 2]) -> [f64; 3] {
[d[0], -d[1], 0.0]
}
pub(super) fn text_height(view: &ViewDrawing) -> f64 {
if view.text_mm > 1e-9 {
view.text_mm
} else {
12.0 * super::project::MM_PER_POINT
}
}
fn style(view: &ViewDrawing) -> PmiLayoutStyle {
let text_height = text_height(view);
PmiLayoutStyle {
arrow: text_height * super::project::ARROW_PER_TEXT,
text_height,
view_dir: [0.0, 0.0, -1.0],
view_up: [0.0, 1.0, 0.0],
plane: None,
}
}
fn finish(
dimension: &SheetDimension,
drawn: brep_kernel::PmiPresentation,
value: f64,
(text, tolerance_from): (String, String),
reseat: &[(([f64; 3], [f64; 3]), [f64; 3])],
) -> DimensionDrawing {
let mut lines: Vec<Vec<[f64; 2]>> = Vec::new();
for polyline in &drawn.polylines {
let seated = reseat.iter().find(|((from, _), _)| {
polyline.len() == 2 && near(polyline[0], *from)
});
match seated {
Some(((_, to), true_point)) => lines.push(vec![drop_to_paper(*true_point), drop_to_paper(*to)]),
None => lines.push(polyline.iter().map(|p| drop_to_paper(*p)).collect()),
}
}
for arrow in &drawn.arrows {
let mut closed: Vec<[f64; 2]> = arrow.iter().map(|p| drop_to_paper(*p)).collect();
closed.push(closed[0]);
lines.push(closed);
}
let texts: Vec<SheetText> = drawn
.texts
.iter()
.filter(|run| !run.text.trim().is_empty())
.map(|run| SheetText::flat(run.text.clone(), drop_to_paper(run.anchor), run.height))
.collect();
let label = texts.first().map(|run| run.anchor).unwrap_or(drop_to_paper(drawn.text_anchor));
let lines = super::project::break_for_text_boxes(lines, &texts);
DimensionDrawing {
id: dimension.id.clone(),
kind: dimension.kind.clone(),
text,
value,
lines,
texts,
label,
offset_mm: dimension.offset_mm,
tolerance_from,
error: String::new(),
}
}
fn value_text(
dimension: &SheetDimension,
view: &ViewDrawing,
value: f64,
(prefix, suffix): (&str, &str),
source: impl Fn(&Measures) -> bool,
) -> (String, String) {
let (block, reference, from) = match inherited(dimension, view, source) {
Some(found) => (found.block, found.reference, found.id.clone()),
None => (dimension.tolerance.block(), dimension.tolerance.reference, String::new()),
};
(format_dimension(value, dimension.precision as usize, &block, reference, prefix, suffix), from)
}
pub fn inherited<'a>(
dimension: &SheetDimension,
view: &'a ViewDrawing,
source: impl Fn(&Measures) -> bool,
) -> Option<&'a super::project::ToleranceSource> {
if !dimension.tolerance.is_plain() {
return None;
}
view.tolerances.iter().find(|candidate| source(&candidate.measures))
}
fn same_point(view: &ViewDrawing, a: [f64; 2], b: [f64; 2]) -> bool {
length2([a[0] - b[0], a[1] - b[1]]) <= super::hlr::Floors::of(view.scale).crossing_mm
}
fn near(a: [f64; 3], b: [f64; 3]) -> bool {
(0..3).all(|i| (a[i] - b[i]).abs() < 1e-9)
}
fn length2(v: [f64; 2]) -> f64 {
(v[0] * v[0] + v[1] * v[1]).sqrt()
}
fn cross2(a: [f64; 2], b: [f64; 2]) -> f64 {
a[0] * b[1] - a[1] * b[0]
}
fn linear(
dimension: &SheetDimension,
view: &ViewDrawing,
first: &AnchorPoint,
second: &AnchorPoint,
) -> DimensionDrawing {
let (a, b) = (lift(first.at), lift(second.at));
let alignment = dimension.alignment.as_str();
let raw = [b[0] - a[0], b[1] - a[1]];
let flip = if alignment.eq_ignore_ascii_case(HORIZONTAL) {
raw[0] < 0.0
} else if alignment.eq_ignore_ascii_case(VERTICAL) {
raw[1] > 0.0
} else {
raw[0] < 0.0 || (raw[0] == 0.0 && raw[1] > 0.0)
};
let (a, b) = if flip { (b, a) } else { (a, b) };
let span = [b[0] - a[0], b[1] - a[1]];
let d = match alignment {
_ if alignment.eq_ignore_ascii_case(HORIZONTAL) => [1.0, 0.0],
_ if alignment.eq_ignore_ascii_case(VERTICAL) => [0.0, -1.0],
_ => {
let len = length2(span);
if len < 1e-9 {
return DimensionDrawing::refused(
dimension,
first.at,
"the two anchors project to the same point".into(),
);
}
[span[0] / len, span[1] / len]
}
};
let n = [-d[1], d[0]];
let value_mm = (span[0] * d[0] + span[1] * d[1]).abs();
if value_mm < 1e-9 {
return DimensionDrawing::refused(
dimension,
first.at,
format!("the two anchors have no {} separation", dimension.alignment),
);
}
let (ca, cb) = (a[0] * n[0] + a[1] * n[1], b[0] * n[0] + b[1] * n[1]);
let base = if dimension.offset_mm >= 0.0 { ca.min(cb) } else { ca.max(cb) };
let onto = |p: [f64; 3], c: f64| -> [f64; 3] {
let along = p[0] * d[0] + p[1] * d[1];
[along * d[0] + c * n[0], along * d[1] + c * n[1], 0.0]
};
let (pa, pb) = (onto(a, base), onto(b, base));
let aligned = alignment.eq_ignore_ascii_case(ALIGNED);
let value = if aligned {
first.model.iter().zip(second.model).map(|(a, b)| (b - a).powi(2)).sum::<f64>().sqrt()
} else {
value_mm / view.scale
};
let text = value_text(dimension, view, value, ("", ""), |measures| match measures {
Measures::Linear { a, b } => {
aligned
&& ((same_point(view, *a, first.at) && same_point(view, *b, second.at))
|| (same_point(view, *a, second.at) && same_point(view, *b, first.at)))
}
_ => false,
});
let style = style(view);
let half = super::project::text_half_width(&text.0, style.text_height);
let along = if 2.0 * (half + style.arrow) <= value_mm {
[(pa[0] + pb[0]) * 0.5, (pa[1] + pb[1]) * 0.5]
} else {
let past = style.arrow + half;
[pb[0] + d[0] * past, pb[1] + d[1] * past]
};
let label = [along[0] + n[0] * dimension.offset_mm, along[1] + n[1] * dimension.offset_mm, 0.0];
let drawn = pmi_present(
&PmiGeometry::Linear { a: pa, b: pb, component: None },
label,
&text.0,
&style,
);
let overshoot = style.arrow * 0.6;
let reseat: Vec<(([f64; 3], [f64; 3]), [f64; 3])> = [(pa, a), (pb, b)]
.iter()
.filter_map(|(projected, truth)| {
let foot = [
projected[0] + n[0] * dimension.offset_mm,
projected[1] + n[1] * dimension.offset_mm,
0.0,
];
let away = [foot[0] - truth[0], foot[1] - truth[1]];
let len = length2(away);
if len < 1e-9 {
return None;
}
let end = [
foot[0] + away[0] / len * overshoot,
foot[1] + away[1] / len * overshoot,
0.0,
];
Some(((*projected, end), *truth))
})
.collect();
finish(dimension, drawn, value, text, &reseat)
}
fn radial(dimension: &SheetDimension, view: &ViewDrawing, anchor: &AnchorPoint) -> DimensionDrawing {
let (Some(radius), Some(u), Some(v)) = (anchor.radius, anchor.u, anchor.v) else {
return DimensionDrawing::refused(
dimension,
anchor.at,
format!("'{}' is not a circular edge", anchor.anchor),
);
};
let diameter = dimension.kind.eq_ignore_ascii_case(DIAMETRAL);
let dir = RADIAL_DIR;
let (u, v) = ([u[0], -u[1]], [v[0], -v[1]]);
let theta = (-cross2(u, dir)).atan2(cross2(v, dir));
let at = |t: f64| [u[0] * t.cos() + v[0] * t.sin(), u[1] * t.cos() + v[1] * t.sin()];
let mut point = at(theta);
if point[0] * dir[0] + point[1] * dir[1] < 0.0 {
point = at(theta + std::f64::consts::PI);
}
let paper_radius = length2(point);
if !(paper_radius > 1e-9) {
return DimensionDrawing::refused(
dimension,
anchor.at,
format!("'{}' projects edge-on: its circle has no radius on the paper", anchor.anchor),
);
}
let centre = lift(anchor.at);
let reach = paper_radius + dimension.offset_mm;
let label = [centre[0] + dir[0] * reach, centre[1] + dir[1] * reach, 0.0];
let value = if diameter { radius * 2.0 } else { radius };
let bar = super::hlr::Floors::of(view.scale).bar;
let text = value_text(
dimension,
view,
value,
(if diameter { "\u{2300}" } else { "R" }, ""),
|measures| match measures {
Measures::Radial { centre, radius: measured, diameter: pmi_diameter } => {
*pmi_diameter == diameter && same_point(view, *centre, anchor.at) && (measured - radius).abs() <= bar
}
_ => false,
},
);
let style = style(view);
let drawn = pmi_present(
&PmiGeometry::Radial {
center: centre,
axis: [0.0, 0.0, 1.0],
radius: paper_radius,
diameter,
sphere: false,
},
label,
&text.0,
&style,
);
finish(dimension, drawn, value, text, &[])
}
fn angular(
dimension: &SheetDimension,
view: &ViewDrawing,
first: &AnchorPoint,
second: &AnchorPoint,
) -> DimensionDrawing {
let direction = |anchor: &AnchorPoint| -> Result<[f64; 2], String> {
anchor.dir.ok_or_else(|| {
if anchor.kind == EDGE {
format!(
"'{}' is not a straight edge in this view; an angular dimension measures between two straight edges",
anchor.anchor
)
} else {
format!("'{}' is a {} anchor; an angular dimension measures between two EDGES", anchor.anchor, anchor.kind)
}
})
};
let (da, db) = match (direction(first), direction(second)) {
(Ok(a), Ok(b)) => (a, b),
(Err(error), _) | (_, Err(error)) => {
return DimensionDrawing::refused(dimension, first.at, error)
}
};
let denominator = cross2(da, db);
if denominator.abs() < 1e-9 {
return DimensionDrawing::refused(
dimension,
first.at,
format!(
"'{}' and '{}' are parallel in this view; they make no angle",
first.anchor, second.anchor
),
);
}
let between = [second.at[0] - first.at[0], second.at[1] - first.at[1]];
let t = cross2(between, db) / denominator;
let vertex = [first.at[0] + da[0] * t, first.at[1] + da[1] * t];
let ray = |anchor: &AnchorPoint, fallback: [f64; 2]| -> [f64; 2] {
let away = [anchor.at[0] - vertex[0], anchor.at[1] - vertex[1]];
let length = length2(away);
if length < 1e-9 {
fallback
} else {
[away[0] / length, away[1] / length]
}
};
let (ra, rb) = (ray(first, da), ray(second, db));
let degrees = cross2(ra, rb).abs().atan2(ra[0] * rb[0] + ra[1] * rb[1]).to_degrees();
if !(degrees > 1e-6) {
return DimensionDrawing::refused(
dimension,
first.at,
format!(
"'{}' and '{}' leave the vertex the same way; they make no angle",
first.anchor, second.anchor
),
);
}
let (la, lb) = (lift_dir(ra), lift_dir(rb));
let (dir_a, dir_b) = if cross2([la[0], la[1]], [lb[0], lb[1]]) >= 0.0 {
(la, lb)
} else {
(lb, la)
};
let bisector = [dir_a[0] + dir_b[0], dir_a[1] + dir_b[1]];
let length = length2([bisector[0], bisector[1]]);
let bisector = [bisector[0] / length, bisector[1] / length, 0.0];
let centre = lift(vertex);
let label = [
centre[0] + bisector[0] * dimension.offset_mm,
centre[1] + bisector[1] * dimension.offset_mm,
0.0,
];
let text = value_text(dimension, view, degrees, ("", "\u{00B0}"), |_| false);
let style = style(view);
let drawn = pmi_present(
&PmiGeometry::Angular {
vertex: centre,
dir_a,
dir_b,
axis: [0.0, 0.0, 1.0],
degrees,
},
label,
&text.0,
&style,
);
finish(dimension, drawn, degrees, text, &[])
}