use crate::scene::RenderScene;
use brep_kernel::{
pmi_present, PmiAnnotationReport, PmiCamera, PmiLayoutStyle, PmiProjection, PmiStatus,
PmiView, PmiViewReport,
};
use super::{PlacedView, ORTHOGRAPHIC};
pub const EDGE_ANGLE_DEG: f64 = 30.0;
pub const MM_PER_POINT: f64 = 25.4 / 72.0;
pub(super) const ARROW_PER_TEXT: f64 = 12.0 / 11.0;
pub(super) const TEXT_ADVANCE: f64 = 0.75;
const TEXT_PAD: f64 = 0.3;
pub const HATCH_SPACING_MM: f64 = 2.5;
const MAX_HATCH_LINES: usize = 2000;
pub const EDGE_ON_COS: f64 = 1e-3;
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct SheetDrawing {
pub id: String,
pub name: String,
#[serde(rename = "widthMm")]
pub width_mm: f64,
#[serde(rename = "heightMm")]
pub height_mm: f64,
pub views: Vec<ViewDrawing>,
pub dimensions: Vec<super::dimension::DimensionDrawing>,
pub ordinates: Vec<super::ordinate::OrdinateDrawing>,
#[serde(skip_serializing_if = "Option::is_none")]
pub frame: Option<super::frame::FrameDrawing>,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct ViewDrawing {
pub id: String,
pub view: String,
pub name: String,
pub position: [f64; 2],
pub scale: f64,
#[serde(skip_serializing_if = "String::is_empty")]
pub error: String,
pub edges: Vec<Vec<[f64; 2]>>,
pub annotations: Vec<AnnotationDrawing>,
pub anchors: Vec<AnchorPoint>,
#[serde(rename = "textMm")]
pub text_mm: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub section: Option<SectionDrawing>,
#[serde(rename = "sectionMarks", skip_serializing_if = "Vec::is_empty")]
pub section_marks: Vec<SectionMark>,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<DetailDrawing>,
#[serde(rename = "detailMarks", skip_serializing_if = "Vec::is_empty")]
pub detail_marks: Vec<DetailMark>,
pub bounds: [f64; 4],
#[serde(rename = "hiddenLine", skip_serializing_if = "String::is_empty")]
pub hidden_line: String,
#[serde(skip)]
pub tolerances: Vec<ToleranceSource>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ToleranceSource {
pub id: String,
pub measures: Measures,
pub block: brep_kernel::ToleranceBlock,
pub reference: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Measures {
Linear { a: [f64; 2], b: [f64; 2] },
Radial { centre: [f64; 2], radius: f64, diameter: bool },
}
impl ViewDrawing {
fn empty(placed: &PlacedView, name: String) -> ViewDrawing {
ViewDrawing {
id: placed.id.clone(),
view: placed.view.clone(),
name,
position: placed.position,
scale: placed.scale,
error: String::new(),
edges: Vec::new(),
annotations: Vec::new(),
anchors: Vec::new(),
text_mm: 0.0,
section: None,
section_marks: Vec::new(),
detail: None,
detail_marks: Vec::new(),
bounds: [placed.position[0], placed.position[1], placed.position[0], placed.position[1]],
hidden_line: String::new(),
tolerances: Vec::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct DetailDrawing {
pub label: String,
pub centre: [f64; 2],
#[serde(rename = "radiusMm")]
pub radius_mm: f64,
pub circle: Vec<[f64; 2]>,
pub texts: Vec<SheetText>,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct DetailMark {
pub id: String,
pub label: String,
pub centre: [f64; 2],
#[serde(rename = "radiusMm")]
pub radius_mm: f64,
pub lines: Vec<Vec<[f64; 2]>>,
pub texts: Vec<SheetText>,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct SectionDrawing {
pub label: String,
pub hatch: Vec<Vec<[f64; 2]>>,
pub cut: Vec<Vec<[f64; 2]>>,
pub texts: Vec<SheetText>,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct SectionMark {
pub id: String,
pub label: String,
pub lines: Vec<Vec<[f64; 2]>>,
pub texts: Vec<SheetText>,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct AnnotationDrawing {
pub id: String,
#[serde(rename = "type")]
pub kind: String,
pub text: String,
pub lines: Vec<Vec<[f64; 2]>>,
pub texts: Vec<SheetText>,
#[serde(skip_serializing_if = "String::is_empty")]
pub note: String,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct AnchorPoint {
#[serde(rename = "ref")]
pub anchor: String,
pub kind: String,
pub at: [f64; 2],
pub model: [f64; 3],
#[serde(skip_serializing_if = "Option::is_none")]
pub dir: Option<[f64; 2]>,
#[serde(skip_serializing_if = "Option::is_none")]
pub radius: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub u: Option<[f64; 2]>,
#[serde(skip_serializing_if = "Option::is_none")]
pub v: Option<[f64; 2]>,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct SheetText {
pub text: String,
pub anchor: [f64; 2],
pub height: f64,
pub angle: f64,
pub right: [f64; 2],
pub up: [f64; 2],
}
impl SheetText {
pub fn new(text: String, anchor: [f64; 2], right: [f64; 2], up: [f64; 2]) -> SheetText {
SheetText {
text,
anchor,
height: (up[0] * up[0] + up[1] * up[1]).sqrt(),
angle: right[1].atan2(right[0]).to_degrees(),
right,
up,
}
}
pub fn flat(text: String, anchor: [f64; 2], height: f64) -> SheetText {
SheetText::new(text, anchor, [height, 0.0], [0.0, -height])
}
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ViewFrame {
pub target: [f64; 3],
pub right: [f64; 3],
pub up: [f64; 3],
pub forward: [f64; 3],
}
fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
}
fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn norm(v: [f64; 3]) -> [f64; 3] {
let len = dot(v, v).sqrt();
if len < 1e-12 {
[0.0, 0.0, 0.0]
} else {
[v[0] / len, v[1] / len, v[2] / len]
}
}
impl ViewFrame {
pub fn of(camera: &PmiCamera) -> ViewFrame {
let forward = norm(sub(camera.target, camera.eye));
let forward = if forward == [0.0, 0.0, 0.0] { [0.0, 0.0, -1.0] } else { forward };
let mut right = norm(cross(forward, camera.up));
if right == [0.0, 0.0, 0.0] {
right = norm(cross(forward, [0.0, 0.0, 1.0]));
if right == [0.0, 0.0, 0.0] {
right = norm(cross(forward, [0.0, 1.0, 0.0]));
}
}
let up = cross(right, forward);
ViewFrame { target: camera.target, right, up, forward }
}
pub fn to_view(&self, p: [f64; 3]) -> [f64; 3] {
let d = sub(p, self.target);
[dot(d, self.right), dot(d, self.up), dot(d, self.forward)]
}
}
fn to_paper(placed: &PlacedView, x: f64, y: f64) -> [f64; 2] {
[placed.position[0] + x * placed.scale, placed.position[1] - y * placed.scale]
}
pub fn project_view(
scene: &RenderScene,
placed: &PlacedView,
view: Option<&PmiView>,
report: Option<&PmiViewReport>,
) -> ViewDrawing {
let (unposed, env) = (Unposed::new(), brep_kernel::Env::default());
project_view_through(&Model { scene, unposed: &unposed, env: &env }, placed, view, report, None, &mut LinesMode::Compute)
}
pub struct Model<'a> {
pub scene: &'a RenderScene,
pub unposed: &'a Unposed,
pub env: &'a brep_kernel::Env,
}
pub type Unposed = std::collections::HashMap<String, crate::scene::SolidDisplay>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HiddenLine {
Analytic,
Mesh,
}
impl HiddenLine {
pub fn label(self) -> &'static str {
match self {
HiddenLine::Analytic => "analytic",
HiddenLine::Mesh => "mesh",
}
}
}
pub struct SectionSetup {
pub frame: ViewFrame,
pub plane: ClipPlane,
pub label: String,
}
pub struct DetailSetup {
pub frame: ViewFrame,
pub radius: f64,
pub label: String,
}
pub enum Derived {
Section(SectionSetup),
Detail(DetailSetup),
}
pub fn project_view_through(
model: &Model,
placed: &PlacedView,
view: Option<&PmiView>,
report: Option<&PmiViewReport>,
derived: Option<Derived>,
mode: &mut LinesMode,
) -> ViewDrawing {
let (scene, unposed) = (model.scene, model.unposed);
let mut out = ViewDrawing::empty(placed, view.map(|v| v.name.clone()).unwrap_or_default());
let Some(view) = view else {
out.error = format!("no PMI view '{}'", placed.view);
return out;
};
let Some(camera) = view.camera.as_ref() else {
out.error = format!("PMI view '{}' has no captured camera", placed.view);
return out;
};
if !matches!(camera.projection, PmiProjection::Orthographic { .. }) {
out.error = format!(
"PMI view '{}' is a perspective camera; a sheet draws {ORTHOGRAPHIC} views only",
placed.view
);
return out;
}
if !placed.projection.eq_ignore_ascii_case(ORTHOGRAPHIC) {
out.error = format!("projection '{}' is not drawn on a sheet", placed.projection);
return out;
}
let frame = match &derived {
Some(Derived::Section(setup)) => setup.frame,
Some(Derived::Detail(setup)) => setup.frame,
None => ViewFrame::of(camera),
};
out.text_mm = view.display.text_size_pt * MM_PER_POINT;
let cap = if out.text_mm > 1e-9 { out.text_mm } else { 12.0 * MM_PER_POINT };
let plan = exact_solids(scene, view, report);
let clip = match &derived {
None => LinesClip::Whole,
Some(Derived::Detail(setup)) => LinesClip::Circle { radius: setup.radius },
Some(Derived::Section(_)) => LinesClip::Section,
};
let exact = plan.as_ref().and_then(|plan| exact_lines_for(plan, &frame, placed, clip, mode));
out.hidden_line = if exact.is_some() { HiddenLine::Analytic } else { HiddenLine::Mesh }.label().into();
match &derived {
None => {
out.edges = match exact {
Some(lines) => lines.runs,
None => mesh_lines_for(scene, unposed, view, report, &frame, placed, LinesClip::Whole, None).runs,
};
out.annotations = annotations(report, view, &frame, placed);
out.tolerances = tolerance_sources(report, view, &frame, placed, model.env);
}
Some(Derived::Detail(setup)) => {
let radius_mm = setup.radius * placed.scale;
let lines = match exact {
Some(lines) => lines.runs,
None => mesh_lines_for(scene, unposed, view, report, &frame, placed, clip, None).runs,
};
out.edges = clip_to_circle(lines, placed.position, radius_mm);
out.detail = Some(DetailDrawing {
label: setup.label.clone(),
centre: placed.position,
radius_mm,
circle: circle_polyline(placed.position, radius_mm),
texts: Vec::new(),
});
}
Some(Derived::Section(setup)) => match exact {
Some(ExactLines { runs, cut }) => {
let mut edges = runs;
edges.extend(cut.iter().cloned());
out.edges = edges;
out.section = Some(SectionDrawing {
label: setup.label.clone(),
hatch: hatch(&segments_of(&cut)),
cut,
texts: Vec::new(),
});
}
None => {
let ExactLines { runs, cut: cut_paper } =
mesh_lines_for(scene, unposed, view, report, &frame, placed, clip, Some(&setup.plane));
let mut edges = runs;
edges.extend(cut_paper.iter().cloned());
out.edges = edges;
out.section = Some(SectionDrawing {
label: setup.label.clone(),
hatch: hatch(&cut_paper),
cut: cut_paper,
texts: Vec::new(),
});
}
},
}
out.anchors = anchor_points(scene, unposed, view, report, &frame, placed);
if let Some(detail) = &out.detail {
let reach = detail.radius_mm * (1.0 + 1e-9) + 1e-9;
out.anchors.retain(|anchor| {
let (dx, dy) = (anchor.at[0] - detail.centre[0], anchor.at[1] - detail.centre[1]);
(dx * dx + dy * dy).sqrt() <= reach
});
}
out.bounds = bounds_of(&out, placed);
let middle = (out.bounds[0] + out.bounds[2]) * 0.5;
let below = out.bounds[3] + cap * 2.0;
if let Some(section) = out.section.as_mut() {
section.texts.push(SheetText::flat(
format!("SECTION {}\u{2014}{}", section.label, section.label),
[middle, below],
cap,
));
out.bounds = bounds_of(&out, placed);
}
if let Some(detail) = out.detail.as_mut() {
detail.texts.push(SheetText::flat(format!("DETAIL {}", detail.label), [middle, below], cap));
detail.texts.push(SheetText::flat(
format!("SCALE {}", super::frame::ratio_text(placed.scale)),
[middle, below + cap * 1.8],
cap * 0.8,
));
out.bounds = bounds_of(&out, placed);
}
out
}
pub fn project_sheet(
scene: &RenderScene,
sheet: &super::Sheet,
views: &[PmiView],
report: Option<&brep_kernel::PmiReport>,
context: &super::frame::FrameContext,
) -> SheetDrawing {
let (unposed, env) = (Unposed::new(), brep_kernel::Env::default());
project_sheet_with(&Model { scene, unposed: &unposed, env: &env }, sheet, views, report, context, &mut LinesMode::Compute)
}
pub fn project_sheet_with(
model: &Model,
sheet: &super::Sheet,
views: &[PmiView],
report: Option<&brep_kernel::PmiReport>,
context: &super::frame::FrameContext,
mode: &mut LinesMode,
) -> SheetDrawing {
let (width_mm, height_mm) = sheet.millimetres();
let mut drawn: Vec<ViewDrawing> = sheet
.views
.iter()
.map(|placed| {
let saved = views.iter().find(|view| view.id == placed.view);
if placed.section.is_some() || placed.detail.is_some() {
ViewDrawing::empty(placed, saved.map(|v| v.name.clone()).unwrap_or_default())
} else {
project_view_through(model, placed, saved, report.and_then(|report| report.view(&placed.view)), None, mode)
}
})
.collect();
for (index, placed) in sheet.views.iter().enumerate() {
let Some(circle) = placed.detail.as_ref() else { continue };
match detail_setup(sheet, &drawn, circle, views) {
Err(error) => drawn[index].error = error,
Ok((setup, source, mark)) => {
drawn[index] = project_view_through(
model,
placed,
views.iter().find(|view| view.id == placed.view),
report.and_then(|report| report.view(&placed.view)),
Some(Derived::Detail(setup)),
mode,
);
let mark = DetailMark { id: placed.id.clone(), ..mark };
drawn[source].detail_marks.push(mark);
let source_placed = &sheet.views[source];
drawn[source].bounds = bounds_of(&drawn[source], source_placed);
}
}
}
for (index, placed) in sheet.views.iter().enumerate() {
let Some(cut) = placed.section.as_ref() else { continue };
match section_setup(sheet, &drawn, placed, cut, views) {
Err(error) => drawn[index].error = error,
Ok((setup, source, mark)) => {
drawn[index] = project_view_through(
model,
placed,
views.iter().find(|view| view.id == placed.view),
report.and_then(|report| report.view(&placed.view)),
Some(Derived::Section(setup)),
mode,
);
drawn[source].section_marks.push(mark);
let source_placed = &sheet.views[source];
drawn[source].bounds = bounds_of(&drawn[source], source_placed);
}
}
}
let dimensions = super::dimension::layout_dimensions(
&sheet.dimensions,
&drawn,
[width_mm * 0.5, height_mm * 0.5],
);
let ordinates = super::ordinate::layout_ordinates(
&sheet.ordinates,
&drawn,
[width_mm * 0.5, height_mm * 0.5],
);
SheetDrawing {
id: sheet.id.clone(),
name: sheet.name.clone(),
width_mm,
height_mm,
frame: super::frame::frame(sheet, context),
views: drawn,
dimensions,
ordinates,
}
}
fn section_setup(
sheet: &super::Sheet,
drawn: &[ViewDrawing],
placed: &PlacedView,
cut: &super::SectionCut,
views: &[PmiView],
) -> Result<(SectionSetup, usize, SectionMark), String> {
if cut.from.is_empty() || cut.to.is_empty() {
return Err("no cutting line yet \u{2014} pick its two anchors on a placed view".into());
}
let Some(source_index) = sheet.views.iter().position(|v| v.id == cut.source) else {
return Err(format!("no placement '{}' to cut through on this sheet", cut.source));
};
let source_placed = &sheet.views[source_index];
if source_placed.section.is_some() {
return Err(format!(
"'{}' is itself a section; a section of a section is not drawn",
cut.source
));
}
if source_placed.detail.is_some() {
return Err(format!(
"'{}' is a detail; a section of a detail is not drawn",
cut.source
));
}
let source = &drawn[source_index];
if !source.error.is_empty() {
return Err(format!("placement '{}' did not project", cut.source));
}
let (from, to) = (point_of(source, &cut.from)?, point_of(source, &cut.to)?);
let span = [to[0] - from[0], to[1] - from[1]];
let length = (span[0] * span[0] + span[1] * span[1]).sqrt();
if !(length > 1e-9) {
return Err(format!(
"'{}' and '{}' are the same point; a cutting line needs two",
cut.from, cut.to
));
}
let Some(camera) = views
.iter()
.find(|view| view.id == source_placed.view)
.and_then(|view| view.camera.as_ref())
else {
return Err(format!("placement '{}' has no captured camera to cut in", cut.source));
};
let source_frame = ViewFrame::of(camera);
if source_placed.scale <= 0.0 {
return Err(format!("placement '{}' has no scale", cut.source));
}
let (p0, p1) = (lift(&source_frame, source_placed, from), lift(&source_frame, source_placed, to));
let along = norm(sub(p1, p0));
let mut forward = norm(cross(source_frame.forward, along));
if forward == [0.0, 0.0, 0.0] {
return Err("the cutting line has no direction in this view".into());
}
if cut.flip {
forward = [-forward[0], -forward[1], -forward[2]];
}
let up = [-source_frame.forward[0], -source_frame.forward[1], -source_frame.forward[2]];
let right = norm(cross(forward, up));
let target = [(p0[0] + p1[0]) * 0.5, (p0[1] + p1[1]) * 0.5, (p0[2] + p1[2]) * 0.5];
let setup = SectionSetup {
frame: ViewFrame { target, right, up: cross(right, forward), forward },
plane: ClipPlane { point: target, normal: forward },
label: cut.label.clone(),
};
let cap = if source.text_mm > 1e-9 { source.text_mm } else { 12.0 * MM_PER_POINT };
Ok((setup, source_index, section_mark(placed, cut, from, to, cap)))
}
fn section_mark(
placed: &PlacedView,
cut: &super::SectionCut,
from: [f64; 2],
to: [f64; 2],
cap: f64,
) -> SectionMark {
let span = [to[0] - from[0], to[1] - from[1]];
let length = (span[0] * span[0] + span[1] * span[1]).sqrt().max(1e-9);
let along = [span[0] / length, span[1] / length];
let sign = if cut.flip { -1.0 } else { 1.0 };
let look = [-along[1] * sign, along[0] * sign];
let tail = cap * 1.5;
let arrow = cap * 1.2;
let mut lines = vec![vec![
[from[0] - along[0] * tail, from[1] - along[1] * tail],
[to[0] + along[0] * tail, to[1] + along[1] * tail],
]];
let mut texts = Vec::new();
for (end, outward) in [(from, [-along[0], -along[1]]), (to, along)] {
let root = [end[0] + outward[0] * tail, end[1] + outward[1] * tail];
let tip = [root[0] + look[0] * arrow * 2.0, root[1] + look[1] * arrow * 2.0];
lines.push(vec![root, tip]);
let across = [-look[1] * arrow * 0.35, look[0] * arrow * 0.35];
lines.push(vec![
tip,
[tip[0] - look[0] * arrow + across[0], tip[1] - look[1] * arrow + across[1]],
[tip[0] - look[0] * arrow - across[0], tip[1] - look[1] * arrow - across[1]],
tip,
]);
texts.push(SheetText::flat(
cut.label.clone(),
[
root[0] + outward[0] * cap * 1.2 + look[0] * cap * 0.6,
root[1] + outward[1] * cap * 1.2 + look[1] * cap * 0.6,
],
cap,
));
}
SectionMark { id: placed.id.clone(), label: cut.label.clone(), lines, texts }
}
fn point_of(source: &ViewDrawing, reference: &str) -> Result<[f64; 2], String> {
source
.anchors
.iter()
.find(|a| a.anchor == reference)
.map(|a| a.at)
.ok_or_else(|| format!("'{reference}' is not a point of '{}' any more", source.id))
}
fn lift(frame: &ViewFrame, placed: &PlacedView, p: [f64; 2]) -> [f64; 3] {
let x = (p[0] - placed.position[0]) / placed.scale;
let y = -(p[1] - placed.position[1]) / placed.scale;
[
frame.target[0] + frame.right[0] * x + frame.up[0] * y,
frame.target[1] + frame.right[1] * x + frame.up[1] * y,
frame.target[2] + frame.right[2] * x + frame.up[2] * y,
]
}
fn detail_setup(
sheet: &super::Sheet,
drawn: &[ViewDrawing],
circle: &super::DetailCircle,
views: &[PmiView],
) -> Result<(DetailSetup, usize, DetailMark), String> {
if circle.centre.is_empty() || circle.rim.is_empty() {
return Err("no detail circle yet \u{2014} pick its centre and a point on its rim on a placed view".into());
}
let Some(source_index) = sheet.views.iter().position(|v| v.id == circle.source) else {
return Err(format!("no placement '{}' to draw a detail of on this sheet", circle.source));
};
let source_placed = &sheet.views[source_index];
if source_placed.detail.is_some() {
return Err(format!(
"'{}' is itself a detail; a detail of a detail is not drawn",
circle.source
));
}
if source_placed.section.is_some() {
return Err(format!(
"'{}' is a section; a detail of a section is not drawn",
circle.source
));
}
let source = &drawn[source_index];
if !source.error.is_empty() {
return Err(format!("placement '{}' did not project", circle.source));
}
let (centre, rim) = (point_of(source, &circle.centre)?, point_of(source, &circle.rim)?);
let radius_mm = ((rim[0] - centre[0]).powi(2) + (rim[1] - centre[1]).powi(2)).sqrt();
if !(radius_mm > 1e-9) {
return Err(format!(
"'{}' and '{}' land on the same point of '{}'; a detail circle needs a radius",
circle.centre, circle.rim, circle.source
));
}
let Some(camera) = views
.iter()
.find(|view| view.id == source_placed.view)
.and_then(|view| view.camera.as_ref())
else {
return Err(format!("placement '{}' has no captured camera to draw a detail of", circle.source));
};
if source_placed.scale <= 0.0 {
return Err(format!("placement '{}' has no scale", circle.source));
}
let source_frame = ViewFrame::of(camera);
let setup = DetailSetup {
frame: ViewFrame { target: lift(&source_frame, source_placed, centre), ..source_frame },
radius: radius_mm / source_placed.scale,
label: circle.label.clone(),
};
let cap = if source.text_mm > 1e-9 { source.text_mm } else { 12.0 * MM_PER_POINT };
let d = std::f64::consts::FRAC_1_SQRT_2;
let reach = radius_mm + cap * 1.5;
let mark = DetailMark {
id: String::new(),
label: circle.label.clone(),
centre,
radius_mm,
lines: vec![circle_polyline(centre, radius_mm)],
texts: vec![SheetText::flat(
circle.label.clone(),
[centre[0] + reach * d, centre[1] - reach * d],
cap,
)],
};
Ok((setup, source_index, mark))
}
const CIRCLE_SEGMENTS: usize = 96;
fn circle_polyline(centre: [f64; 2], radius: f64) -> Vec<[f64; 2]> {
(0..=CIRCLE_SEGMENTS)
.map(|k| {
let t = std::f64::consts::TAU * k as f64 / CIRCLE_SEGMENTS as f64;
[centre[0] + radius * t.cos(), centre[1] + radius * t.sin()]
})
.collect()
}
fn clip_to_circle(lines: Vec<Vec<[f64; 2]>>, centre: [f64; 2], radius: f64) -> Vec<Vec<[f64; 2]>> {
let mut out: Vec<Vec<[f64; 2]>> = Vec::new();
let flush = |current: &mut Vec<[f64; 2]>, out: &mut Vec<Vec<[f64; 2]>>| {
if current.len() >= 2 {
out.push(std::mem::take(current));
} else {
current.clear();
}
};
for line in lines {
let mut current: Vec<[f64; 2]> = Vec::new();
for pair in line.windows(2) {
let (p, q) = (pair[0], pair[1]);
let d = [q[0] - p[0], q[1] - p[1]];
let f = [p[0] - centre[0], p[1] - centre[1]];
let a = d[0] * d[0] + d[1] * d[1];
let c = f[0] * f[0] + f[1] * f[1] - radius * radius;
let span = if a < 1e-24 {
(c <= 0.0).then_some((0.0, 1.0))
} else {
let b = 2.0 * (f[0] * d[0] + f[1] * d[1]);
let disc = b * b - 4.0 * a * c;
if disc < 0.0 {
None
} else {
let root = disc.sqrt();
let (t0, t1) = (((-b - root) / (2.0 * a)).max(0.0), ((-b + root) / (2.0 * a)).min(1.0));
(t1 > t0).then_some((t0, t1))
}
};
let at = |t: f64| [p[0] + d[0] * t, p[1] + d[1] * t];
match span {
None => flush(&mut current, &mut out),
Some((t0, t1)) => {
if t0 <= 0.0 {
if current.is_empty() {
current.push(p);
}
if t1 >= 1.0 {
current.push(q);
} else {
current.push(at(t1));
flush(&mut current, &mut out);
}
} else {
flush(&mut current, &mut out);
current.push(at(t0));
if t1 >= 1.0 {
current.push(q);
} else {
current.push(at(t1));
flush(&mut current, &mut out);
}
}
}
}
}
flush(&mut current, &mut out);
}
out
}
fn bounds_of(view: &ViewDrawing, placed: &PlacedView) -> [f64; 4] {
let mut bounds = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
let mut add = |p: [f64; 2]| {
bounds[0] = bounds[0].min(p[0]);
bounds[1] = bounds[1].min(p[1]);
bounds[2] = bounds[2].max(p[0]);
bounds[3] = bounds[3].max(p[1]);
};
for polyline in &view.edges {
for point in polyline {
add(*point);
}
}
for annotation in &view.annotations {
for polyline in &annotation.lines {
for point in polyline {
add(*point);
}
}
for text in &annotation.texts {
add(text.anchor);
}
}
if let Some(section) = &view.section {
for polyline in section.hatch.iter().chain(section.cut.iter()) {
for point in polyline {
add(*point);
}
}
for text in §ion.texts {
add(text.anchor);
}
}
for mark in &view.section_marks {
for polyline in &mark.lines {
for point in polyline {
add(*point);
}
}
for text in &mark.texts {
add(text.anchor);
}
}
if let Some(detail) = &view.detail {
for point in &detail.circle {
add(*point);
}
for text in &detail.texts {
add(text.anchor);
}
}
for mark in &view.detail_marks {
for polyline in &mark.lines {
for point in polyline {
add(*point);
}
}
for text in &mark.texts {
add(text.anchor);
}
}
if bounds[0] > bounds[2] {
[placed.position[0], placed.position[1], placed.position[0], placed.position[1]]
} else {
bounds
}
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct SolidPose {
pub translate: [f64; 3],
#[serde(rename = "rotateDeg")]
pub rotate_deg: [f64; 3],
pub scale: [f64; 3],
pub center: [f64; 3],
}
impl SolidPose {
pub fn apply(&self, p: [f64; 3]) -> [f64; 3] {
let (center, scale, translate) = (self.center, self.scale, self.translate);
let local = [(p[0] - center[0]) * scale[0], (p[1] - center[1]) * scale[1], (p[2] - center[2]) * scale[2]];
let rotated = crate::engine_state::rotate_euler_xyz_f64(local, self.rotate_deg);
[
rotated[0] + center[0] + translate[0],
rotated[1] + center[1] + translate[1],
rotated[2] + center[2] + translate[2],
]
}
fn identity(&self) -> String {
let bits = |v: &[f64; 3]| format!("{:x},{:x},{:x}", v[0].to_bits(), v[1].to_bits(), v[2].to_bits());
format!(
"pose[{}|{}|{}|{}];",
bits(&self.translate),
bits(&self.rotate_deg),
bits(&self.scale),
bits(&self.center)
)
}
}
pub(crate) fn poses_of(report: Option<&PmiViewReport>, name: &str) -> Vec<SolidPose> {
let Some(report) = report else { return Vec::new() };
report
.annotations
.iter()
.filter(|row| row.enabled && row.status == PmiStatus::Ok)
.filter_map(|row| match &row.geometry {
brep_kernel::PmiGeometry::Explode { solids, translate, rotate_deg, scale, center, .. }
if solids.iter().any(|target| target == name) =>
{
Some(SolidPose { translate: *translate, rotate_deg: *rotate_deg, scale: *scale, center: *center })
}
_ => None,
})
.collect()
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct LinesSolid {
pub name: String,
pub handle: u32,
pub fingerprint: u64,
pub poses: Vec<SolidPose>,
}
impl LinesSolid {
fn identity(&self) -> String {
let mut identity = format!("{}#{:016x};", self.name, self.fingerprint);
for pose in &self.poses {
identity.push_str(&pose.identity());
}
identity
}
}
struct ExactPlan<'a> {
solids: Vec<LinesSolid>,
sources: Vec<&'a brep_kernel::BrepSolid>,
each: Vec<String>,
identity: String,
}
impl ExactPlan<'_> {
fn posed(&self) -> Vec<std::borrow::Cow<'_, brep_kernel::BrepSolid>> {
self.solids
.iter()
.zip(&self.sources)
.map(|(solid, source)| posed_solid(std::borrow::Cow::Borrowed(*source), &solid.poses))
.collect()
}
}
fn posed_solid<'a>(
solid: std::borrow::Cow<'a, brep_kernel::BrepSolid>,
poses: &[SolidPose],
) -> std::borrow::Cow<'a, brep_kernel::BrepSolid> {
poses.iter().fold(solid, |solid, pose| std::borrow::Cow::Owned(pose_solid(&solid, &|p| pose.apply(p))))
}
fn exact_solids<'a>(scene: &'a RenderScene, view: &PmiView, report: Option<&PmiViewReport>) -> Option<ExactPlan<'a>> {
let mut plan = ExactPlan { solids: Vec::new(), sources: Vec::new(), each: Vec::new(), identity: String::new() };
for solid in drawn_solids(scene, view) {
let (brep, fingerprint) = scene.exact_solid_fingerprinted(&solid.name)?;
let planned = LinesSolid {
name: solid.name.clone(),
handle: solid.source_handle,
fingerprint,
poses: poses_of(report, &solid.name),
};
plan.each.push(planned.identity());
plan.solids.push(planned);
plan.sources.push(brep);
}
plan.identity = plan.each.concat();
Some(plan)
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum LinesClip {
Whole,
Circle { radius: f64 },
Section,
}
impl LinesClip {
fn key(&self) -> String {
match self {
LinesClip::Whole => "whole".into(),
LinesClip::Circle { radius } => format!("circle {:x}", radius.to_bits()),
LinesClip::Section => "section".into(),
}
}
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct LinesJob {
pub key: String,
pub placement: String,
pub solids: Vec<LinesSolid>,
pub frame: ViewFrame,
pub placed: PlacedView,
pub clip: LinesClip,
}
pub enum LinesMode<'a> {
Compute,
Defer(&'a mut dyn FnMut(LinesJob) -> Deferred),
}
pub enum Deferred {
Lines(ExactLines),
Pending,
Here,
}
pub fn run_lines_job(job: &LinesJob) -> Result<ExactLines, String> {
let mut posed = Vec::with_capacity(job.solids.len());
for solid in &job.solids {
let brep = brep_kernel::registered_solid_clone(solid.handle)
.map_err(|error| format!("'{}' (handle {}): {error}", solid.name, solid.handle))?;
if crate::scene::brep_fingerprint(&brep) != solid.fingerprint {
return Err(format!("'{}' changed under handle {}", solid.name, solid.handle));
}
posed.push(posed_solid(std::borrow::Cow::Owned(brep), &solid.poses).into_owned());
}
let solids: Vec<&brep_kernel::BrepSolid> = posed.iter().collect();
let each: Vec<String> = job.solids.iter().map(LinesSolid::identity).collect();
Ok(compute_exact_lines(&solids, &each, &job.frame, &job.placed, job.clip))
}
fn compute_exact_lines(
solids: &[&brep_kernel::BrepSolid],
each: &[String],
frame: &ViewFrame,
origin: &PlacedView,
clip: LinesClip,
) -> ExactLines {
let trims = solid_trims(each, solids);
let trims: Vec<&super::hlr::SolidTrims> = trims.iter().map(|t| t.as_ref()).collect();
match clip {
LinesClip::Whole => ExactLines {
runs: super::hlr::visible_edges_with(solids, &trims, frame, origin, &super::hlr::Clip::Whole).runs,
cut: Vec::new(),
},
LinesClip::Circle { radius } => ExactLines {
runs: super::hlr::visible_edges_with(solids, &trims, frame, origin, &super::hlr::Clip::Circle { radius }).runs,
cut: Vec::new(),
},
LinesClip::Section => {
let cut = super::hlr::section_curves_with(solids, &trims, frame, origin);
let clip = super::hlr::Clip::Beyond { cap: &cut };
ExactLines { runs: super::hlr::visible_edges_with(solids, &trims, frame, origin, &clip).runs, cut }
}
}
}
fn exact_lines_for(
plan: &ExactPlan,
frame: &ViewFrame,
placed: &PlacedView,
clip: LinesClip,
mode: &mut LinesMode,
) -> Option<ExactLines> {
let key = exact_key(&plan.identity, frame, placed.scale, &clip.key());
let origin = PlacedView { position: [0.0, 0.0], ..placed.clone() };
let relative = match cached_exact_lines(&key) {
Some(lines) => lines,
None => {
let deferred = match mode {
LinesMode::Compute => Deferred::Here,
LinesMode::Defer(defer) => defer(LinesJob {
key: key.clone(),
placement: placed.id.clone(),
solids: plan.solids.clone(),
frame: *frame,
placed: origin.clone(),
clip,
}),
};
let lines = match deferred {
Deferred::Lines(lines) => lines,
Deferred::Pending => return None,
Deferred::Here => {
let posed = plan.posed();
let solids: Vec<&brep_kernel::BrepSolid> = posed.iter().map(|solid| solid.as_ref()).collect();
compute_exact_lines(&solids, &plan.each, frame, &origin, clip)
}
};
store_exact_lines(key, lines.clone());
lines
}
};
let shift = |lines: Vec<Vec<[f64; 2]>>| -> Vec<Vec<[f64; 2]>> {
lines
.into_iter()
.map(|line| line.into_iter().map(|p| [p[0] + placed.position[0], p[1] + placed.position[1]]).collect())
.collect()
};
Some(ExactLines { runs: shift(relative.runs), cut: shift(relative.cut) })
}
const TRIM_CACHE_SOLIDS: usize = EXACT_CACHE_ENTRIES;
thread_local! {
static TRIM_CACHE: std::cell::RefCell<std::collections::VecDeque<(String, std::rc::Rc<super::hlr::SolidTrims>)>> =
const { std::cell::RefCell::new(std::collections::VecDeque::new()) };
}
fn solid_trims(identities: &[String], solids: &[&brep_kernel::BrepSolid]) -> Vec<std::rc::Rc<super::hlr::SolidTrims>> {
TRIM_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
identities
.iter()
.zip(solids)
.map(|(identity, solid)| {
let trims = match cache.iter().position(|(key, _)| key == identity) {
Some(index) => cache.remove(index).map(|(_, trims)| trims).unwrap(),
None => std::rc::Rc::new(super::hlr::SolidTrims::of(solid)),
};
cache.push_back((identity.clone(), trims.clone()));
while cache.len() > TRIM_CACHE_SOLIDS {
cache.pop_front();
}
trims
})
.collect()
})
}
#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ExactLines {
pub runs: Vec<Vec<[f64; 2]>>,
pub cut: Vec<Vec<[f64; 2]>>,
}
const EXACT_CACHE_ENTRIES: usize = 64;
thread_local! {
static EXACT_CACHE: std::cell::RefCell<std::collections::VecDeque<(String, ExactLines)>> =
const { std::cell::RefCell::new(std::collections::VecDeque::new()) };
}
pub(crate) fn cached_exact_lines(key: &str) -> Option<ExactLines> {
EXACT_CACHE.with(|cache| cache.borrow().iter().find(|(k, _)| k == key).map(|(_, lines)| lines.clone()))
}
pub(crate) fn store_exact_lines(key: String, lines: ExactLines) {
EXACT_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
cache.retain(|(k, _)| *k != key);
cache.push_back((key, lines));
while cache.len() > EXACT_CACHE_ENTRIES {
cache.pop_front();
}
});
}
fn exact_key(identity: &str, frame: &ViewFrame, scale: f64, clip: &str) -> String {
let bits = |v: [f64; 3]| format!("{:x},{:x},{:x}", v[0].to_bits(), v[1].to_bits(), v[2].to_bits());
format!(
"{identity}|{}|{}|{}|{}|{:x}|{clip}",
bits(frame.target),
bits(frame.right),
bits(frame.up),
bits(frame.forward),
scale.to_bits()
)
}
fn pose_solid(solid: &brep_kernel::BrepSolid, pose: &dyn Fn([f64; 3]) -> [f64; 3]) -> brep_kernel::BrepSolid {
let map = |cp: &brep_kernel::Vec4| -> brep_kernel::Vec4 {
let w = if cp.w != 0.0 { cp.w } else { 1.0 };
let p = pose([cp.x / w, cp.y / w, cp.z / w]);
brep_kernel::Vec4 { x: p[0] * w, y: p[1] * w, z: p[2] * w, w: cp.w }
};
let mut out = solid.clone();
for vertex in &mut out.vertices {
let p = pose([vertex.point.x, vertex.point.y, vertex.point.z]);
vertex.point = brep_kernel::Vec3::new(p[0], p[1], p[2]);
}
for edge in &mut out.edges {
let points = edge.curve.control_points.iter().map(map).collect();
if let Ok(curve) = brep_kernel::NurbsCurve::new(edge.curve.degree, edge.curve.knots.clone(), points) {
edge.curve = curve;
}
}
for shell in &mut out.shells {
for face in &mut shell.faces {
let s = &face.surface;
let points = s.control_points.iter().map(|row| row.iter().map(map).collect()).collect();
if let Ok(surface) =
brep_kernel::NurbsSurface::new(s.degree_u, s.degree_v, s.knots_u.clone(), s.knots_v.clone(), points)
{
face.surface = surface;
}
}
}
out
}
fn segments_of(lines: &[Vec<[f64; 2]>]) -> Vec<Vec<[f64; 2]>> {
lines.iter().flat_map(|line| line.windows(2).map(|w| vec![w[0], w[1]])).collect()
}
fn drawn_solids<'a>(scene: &'a RenderScene, view: &PmiView) -> Vec<&'a crate::scene::SolidDisplay> {
scene
.solids()
.iter()
.filter(|solid| solid.visible && !solid.is_sketch)
.filter(|solid| !view.display.hidden.iter().any(|name| name == &solid.name))
.filter(|solid| solid.mesh.indices.len() >= 3)
.collect()
}
pub fn sheet_solid_names(scene: &RenderScene, sheet: &super::Sheet, views: &[PmiView]) -> Vec<String> {
let mut names: Vec<String> = Vec::new();
for placed in &sheet.views {
let Some(view) = views.iter().find(|view| view.id == placed.view) else { continue };
for solid in drawn_solids(scene, view) {
if !names.contains(&solid.name) {
names.push(solid.name.clone());
}
}
}
names
}
fn drawn_displays<'a>(scene: &'a RenderScene, unposed: &'a Unposed, view: &PmiView) -> Vec<&'a crate::scene::SolidDisplay> {
drawn_solids(scene, view).into_iter().map(|solid| unposed.get(&solid.name).unwrap_or(solid)).collect()
}
fn mesh_solids(displays: &[&crate::scene::SolidDisplay], report: Option<&PmiViewReport>) -> Vec<super::hlr::MeshSolid> {
let world = |p: &[f32; 3]| [p[0] as f64, p[1] as f64, p[2] as f64];
displays
.iter()
.map(|solid| {
let poses = poses_of(report, &solid.name);
let pose = |p: [f64; 3]| poses.iter().fold(p, |p, pose| pose.apply(p));
let count = solid.mesh.positions.len();
let (triangles, faces) = solid
.mesh
.indices
.chunks_exact(3)
.enumerate()
.filter(|(_, c)| c.iter().all(|i| (*i as usize) < count))
.map(|(t, c)| ([c[0], c[1], c[2]], solid.mesh.face_ids.get(t).copied().unwrap_or(u32::MAX)))
.unzip();
let bits = |p: [f32; 3]| [p[0].to_bits(), p[1].to_bits(), p[2].to_bits()];
let mut at: std::collections::HashMap<[u32; 3], Option<u64>> = std::collections::HashMap::new();
for vertex in &solid.vertices {
let p = vertex.position;
at.entry(bits([p[0] as f32, p[1] as f32, p[2] as f32]))
.and_modify(|id| *id = None)
.or_insert(Some(vertex.topo_id));
}
let vertex_of = |p: &[f32; 3]| at.get(&bits(*p)).copied().flatten();
super::hlr::MeshSolid {
positions: solid.mesh.positions.iter().map(|p| pose(world(p))).collect(),
triangles,
faces,
edges: solid
.edges
.iter()
.filter(|edge| !edge.aux && !edge.centerline && edge.polyline.len() >= 2)
.map(|edge| super::hlr::MeshEdge {
points: edge.polyline.iter().map(|p| pose(world(p))).collect(),
ends: vertex_of(&edge.polyline[0]).zip(vertex_of(edge.polyline.last().unwrap())),
})
.collect(),
}
})
.collect()
}
const MESH_CACHE_ENTRIES: usize = 16;
thread_local! {
static MESH_CACHE: std::cell::RefCell<std::collections::VecDeque<(String, ExactLines)>> =
const { std::cell::RefCell::new(std::collections::VecDeque::new()) };
}
#[allow(clippy::too_many_arguments)]
fn mesh_lines_for(
scene: &RenderScene,
unposed: &Unposed,
view: &PmiView,
report: Option<&PmiViewReport>,
frame: &ViewFrame,
placed: &PlacedView,
clip: LinesClip,
plane: Option<&ClipPlane>,
) -> ExactLines {
let displays = drawn_displays(scene, unposed, view);
let mut identity = String::from("mesh|");
for display in &displays {
identity.push_str(&format!("{}@{};", display.name, display.revision));
for pose in poses_of(report, &display.name) {
identity.push_str(&pose.identity());
}
}
let bits = |v: [f64; 3]| format!("{:x},{:x},{:x}", v[0].to_bits(), v[1].to_bits(), v[2].to_bits());
let plane_key = plane.map(|plane| format!("{}|{}", bits(plane.point), bits(plane.normal))).unwrap_or_default();
let key = exact_key(&identity, frame, placed.scale, &format!("{}{plane_key}", clip.key()));
let hit = MESH_CACHE.with(|cache| cache.borrow().iter().find(|(k, _)| *k == key).map(|(_, lines)| lines.clone()));
let relative = match hit {
Some(lines) => lines,
None => {
let origin = PlacedView { position: [0.0, 0.0], ..placed.clone() };
let meshes = mesh_solids(&displays, report);
let lines = match (clip, plane) {
(LinesClip::Section, Some(plane)) => {
let cut: Vec<Vec<[f64; 2]>> = mesh_cut(&meshes, plane)
.iter()
.map(|segment| {
segment
.iter()
.map(|p| {
let v = frame.to_view(*p);
to_paper(&origin, v[0], v[1])
})
.collect()
})
.collect();
let beyond = super::hlr::Clip::Beyond { cap: &cut };
ExactLines { runs: super::hlr::visible_mesh_lines(&meshes, frame, &origin, &beyond).runs, cut }
}
(LinesClip::Circle { radius }, _) => ExactLines {
runs: super::hlr::visible_mesh_lines(&meshes, frame, &origin, &super::hlr::Clip::Circle { radius }).runs,
cut: Vec::new(),
},
_ => ExactLines {
runs: super::hlr::visible_mesh_lines(&meshes, frame, &origin, &super::hlr::Clip::Whole).runs,
cut: Vec::new(),
},
};
MESH_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
cache.push_back((key, lines.clone()));
while cache.len() > MESH_CACHE_ENTRIES {
cache.pop_front();
}
});
lines
}
};
let shift = |lines: Vec<Vec<[f64; 2]>>| -> Vec<Vec<[f64; 2]>> {
lines
.into_iter()
.map(|line| line.into_iter().map(|p| [p[0] + placed.position[0], p[1] + placed.position[1]]).collect())
.collect()
};
ExactLines { runs: shift(relative.runs), cut: shift(relative.cut) }
}
#[derive(Debug, Clone, Copy)]
pub struct ClipPlane {
pub point: [f64; 3],
pub normal: [f64; 3],
}
fn mesh_cut(meshes: &[super::hlr::MeshSolid], plane: &ClipPlane) -> Vec<[[f64; 3]; 2]> {
let mut extent: f64 = 0.0;
for mesh in meshes {
for p in &mesh.positions {
extent = extent.max(dot(sub(*p, plane.point), plane.normal).abs());
}
}
let eps = (extent * 1e-9).max(f64::MIN_POSITIVE);
let mut cut: Vec<[[f64; 3]; 2]> = Vec::new();
for mesh in meshes {
let side: Vec<f64> = mesh
.positions
.iter()
.map(|p| dot(sub(*p, plane.point), plane.normal))
.collect();
for tri in &mesh.triangles {
let corners = [tri[0] as usize, tri[1] as usize, tri[2] as usize];
let d = [side[corners[0]], side[corners[1]], side[corners[2]]];
if d.iter().all(|v| *v < -eps) || d.iter().all(|v| *v > eps) {
continue; }
let mut kept = 0;
let mut on_plane: Vec<[f64; 3]> = Vec::with_capacity(2);
for k in 0..3 {
let (i, j) = (k, (k + 1) % 3);
let (pi, pj) = (mesh.positions[corners[i]], mesh.positions[corners[j]]);
let (di, dj) = (d[i], d[j]);
if di >= -eps {
kept += 1;
if di.abs() <= eps {
on_plane.push(pi);
}
}
if (di > eps && dj < -eps) || (di < -eps && dj > eps) {
let t = di / (di - dj);
kept += 1;
on_plane.push([
pi[0] + (pj[0] - pi[0]) * t,
pi[1] + (pj[1] - pi[1]) * t,
pi[2] + (pj[2] - pi[2]) * t,
]);
}
}
if kept >= 3 && on_plane.len() == 2 {
cut.push([on_plane[0], on_plane[1]]);
}
}
}
cut
}
fn hatch(cut: &[Vec<[f64; 2]>]) -> Vec<Vec<[f64; 2]>> {
if cut.is_empty() {
return Vec::new();
}
let d = std::f64::consts::FRAC_1_SQRT_2;
let u = [d, -d];
let n = [d, d];
let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
for segment in cut {
for p in segment {
let c = p[0] * n[0] + p[1] * n[1];
lo = lo.min(c);
hi = hi.max(c);
}
}
if !lo.is_finite() || hi <= lo {
return Vec::new();
}
let mut spacing = HATCH_SPACING_MM;
while (hi - lo) / spacing > MAX_HATCH_LINES as f64 {
spacing *= 2.0;
}
let first = (lo / spacing).ceil() * spacing;
let mut out = Vec::new();
let mut c = first;
while c <= hi {
let mut crossings: Vec<f64> = Vec::new();
for segment in cut {
let (a, b) = (segment[0], segment[segment.len() - 1]);
let (ca, cb) = (a[0] * n[0] + a[1] * n[1], b[0] * n[0] + b[1] * n[1]);
if (ca <= c) == (cb <= c) {
continue;
}
let t = (c - ca) / (cb - ca);
let hit = [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
crossings.push(hit[0] * u[0] + hit[1] * u[1]);
}
crossings.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
for pair in crossings.chunks_exact(2) {
if pair[1] - pair[0] < 1e-9 {
continue;
}
out.push(vec![
[u[0] * pair[0] + n[0] * c, u[1] * pair[0] + n[1] * c],
[u[0] * pair[1] + n[0] * c, u[1] * pair[1] + n[1] * c],
]);
}
c += spacing;
}
out
}
const CIRCLE_MIN_SPAN_DEG: f64 = 20.0;
const CIRCLE_FIT_TOLERANCE: f64 = 1e-4;
const STRAIGHT_TOLERANCE: f64 = 1e-4;
fn anchor_points(
scene: &RenderScene,
unposed: &Unposed,
view: &PmiView,
report: Option<&PmiViewReport>,
frame: &ViewFrame,
placed: &PlacedView,
) -> Vec<AnchorPoint> {
let project = |p: [f64; 3]| -> [f64; 2] {
let v = frame.to_view(p);
to_paper(placed, v[0], v[1])
};
let mut out = Vec::new();
for solid in drawn_displays(scene, unposed, view) {
let poses = poses_of(report, &solid.name);
let pose = |p: [f64; 3]| poses.iter().fold(p, |p, pose| pose.apply(p));
for vertex in &solid.vertices {
out.push(AnchorPoint {
anchor: super::dimension::SheetAnchor::vertex(&placed.id, &solid.name, vertex.topo_id)
.to_ref(),
kind: super::dimension::VERTEX.to_string(),
at: project(pose(vertex.position)),
model: pose(vertex.position),
dir: None,
radius: None,
u: None,
v: None,
});
}
for edge in &solid.edges {
if edge.name.is_empty() || edge.aux || edge.centerline || edge.polyline.len() < 2 {
continue;
}
let points: Vec<[f64; 3]> = edge
.polyline
.iter()
.map(|p| pose([p[0] as f64, p[1] as f64, p[2] as f64]))
.collect();
let dir = straight_chord(&points).and_then(|d| {
let paper = [dot(d, frame.right), -dot(d, frame.up)];
let length = (paper[0] * paper[0] + paper[1] * paper[1]).sqrt();
(length > 1e-9).then(|| [paper[0] / length, paper[1] / length])
});
for fraction in [0.0, 0.5, 1.0] {
let Some(point) = along(&points, fraction) else { continue };
out.push(AnchorPoint {
anchor: super::dimension::SheetAnchor::edge(
&placed.id,
&solid.name,
&edge.name,
fraction,
)
.to_ref(),
kind: super::dimension::EDGE.to_string(),
at: project(point),
model: point,
dir,
radius: None,
u: None,
v: None,
});
}
if let Some((centre, axis, radius)) = fit_circle(&points) {
let u3 = norm(sub(points[0], centre));
let v3 = cross(axis, u3);
let image = |d: [f64; 3]| -> [f64; 2] {
[
dot(d, frame.right) * radius * placed.scale,
-dot(d, frame.up) * radius * placed.scale,
]
};
out.push(AnchorPoint {
anchor: super::dimension::SheetAnchor::circle(&placed.id, &solid.name, &edge.name)
.to_ref(),
kind: super::dimension::CIRCLE.to_string(),
at: project(centre),
model: centre,
dir: None,
radius: Some(radius),
u: Some(image(u3)),
v: Some(image(v3)),
});
}
}
}
out
}
fn straight_chord(points: &[[f64; 3]]) -> Option<[f64; 3]> {
let (first, last) = (*points.first()?, *points.last()?);
let span = sub(last, first);
let length = dot(span, span).sqrt();
if length < 1e-12 {
return None;
}
let direction = [span[0] / length, span[1] / length, span[2] / length];
let bar = length * STRAIGHT_TOLERANCE;
for point in points {
let offset = sub(*point, first);
let along = dot(offset, direction);
let away = [
offset[0] - direction[0] * along,
offset[1] - direction[1] * along,
offset[2] - direction[2] * along,
];
if dot(away, away).sqrt() > bar {
return None;
}
}
Some(direction)
}
fn along(points: &[[f64; 3]], fraction: f64) -> Option<[f64; 3]> {
if points.len() < 2 {
return None;
}
let steps: Vec<f64> = points
.windows(2)
.map(|pair| dot(sub(pair[1], pair[0]), sub(pair[1], pair[0])).sqrt())
.collect();
let total: f64 = steps.iter().sum();
if !(total > 0.0) {
return Some(points[0]);
}
let target = total * fraction.clamp(0.0, 1.0);
let mut walked = 0.0;
for (index, step) in steps.iter().enumerate() {
if walked + step >= target || index + 1 == steps.len() {
let t = if *step > 0.0 { ((target - walked) / step).clamp(0.0, 1.0) } else { 0.0 };
let (a, b) = (points[index], points[index + 1]);
return Some([
a[0] + (b[0] - a[0]) * t,
a[1] + (b[1] - a[1]) * t,
a[2] + (b[2] - a[2]) * t,
]);
}
walked += step;
}
points.last().copied()
}
fn fit_circle(points: &[[f64; 3]]) -> Option<([f64; 3], [f64; 3], f64)> {
if points.len() < 4 {
return None;
}
let last = points.len() - 1;
let (a, b, c) = (points[0], points[last / 3], points[last * 2 / 3]);
let (ab, ac) = (sub(b, a), sub(c, a));
let n = cross(ab, ac);
let n2 = dot(n, n);
if n2 < 1e-24 {
return None;
}
let centre = {
let (ab2, ac2) = (dot(ab, ab), dot(ac, ac));
let to = cross(
[
ab2 * ac[0] - ac2 * ab[0],
ab2 * ac[1] - ac2 * ab[1],
ab2 * ac[2] - ac2 * ab[2],
],
n,
);
[
a[0] + to[0] / (2.0 * n2),
a[1] + to[1] / (2.0 * n2),
a[2] + to[2] / (2.0 * n2),
]
};
let axis = norm(n);
let radius = dot(sub(a, centre), sub(a, centre)).sqrt();
if !(radius > 1e-12) {
return None;
}
let bar = radius * CIRCLE_FIT_TOLERANCE;
for p in points {
let d = sub(*p, centre);
if (dot(d, d).sqrt() - radius).abs() > bar || dot(d, axis).abs() > bar {
return None;
}
}
let first = norm(sub(points[0], centre));
let mut span = 0.0;
for p in points {
let d = norm(sub(*p, centre));
let angle = dot(d, first).clamp(-1.0, 1.0).acos();
span = f64::max(span, angle);
}
if span.to_degrees() < CIRCLE_MIN_SPAN_DEG {
return None;
}
Some((centre, axis, radius))
}
#[derive(Debug, Clone, Copy)]
struct Box2 {
centre: [f64; 2],
right: [f64; 2],
up: [f64; 2],
half: [f64; 2],
}
impl Box2 {
fn local(&self, p: [f64; 2]) -> Option<[f64; 2]> {
let det = self.right[0] * self.up[1] - self.right[1] * self.up[0];
if det.abs() < 1e-12 {
return None;
}
let d = [p[0] - self.centre[0], p[1] - self.centre[1]];
Some([
(d[0] * self.up[1] - d[1] * self.up[0]) / det,
(d[1] * self.right[0] - d[0] * self.right[1]) / det,
])
}
fn of_text(run: &SheetText) -> Box2 {
let rows = run.text.lines().count().max(1) as f64;
Box2 {
centre: run.anchor,
right: run.right,
up: run.up,
half: [text_half_width(&run.text, 1.0), rows * 0.5 + TEXT_PAD],
}
}
}
pub(super) fn text_half_width(text: &str, height: f64) -> f64 {
let columns = text.lines().map(|line| line.chars().count()).max().unwrap_or(0) as f64;
(columns * TEXT_ADVANCE * 0.5 + TEXT_PAD) * height
}
fn break_for_text(lines: Vec<Vec<[f64; 2]>>, boxes: &[Box2]) -> Vec<Vec<[f64; 2]>> {
if boxes.is_empty() {
return lines;
}
let mut out: Vec<Vec<[f64; 2]>> = Vec::new();
for line in lines {
let mut current: Vec<[f64; 2]> = Vec::new();
for pair in line.windows(2) {
let (p, q) = (pair[0], pair[1]);
let mut kept = visible_spans(p, q, boxes);
if kept.is_empty() {
if current.len() >= 2 {
out.push(std::mem::take(&mut current));
} else {
current.clear();
}
continue;
}
let full = kept.len() == 1 && kept[0].0 <= 0.0 && kept[0].1 >= 1.0;
let at = |t: f64| [p[0] + (q[0] - p[0]) * t, p[1] + (q[1] - p[1]) * t];
if full {
if current.is_empty() {
current.push(p);
}
current.push(q);
continue;
}
if !kept.is_empty() && kept[0].0 <= 0.0 {
let (_, end) = kept.remove(0);
if current.is_empty() {
current.push(p);
}
current.push(at(end));
}
if current.len() >= 2 {
out.push(std::mem::take(&mut current));
} else {
current.clear();
}
for (start, end) in kept {
if end >= 1.0 {
current = vec![at(start), q];
} else {
out.push(vec![at(start), at(end)]);
}
}
}
if current.len() >= 2 {
out.push(current);
}
}
out
}
pub(super) fn break_for_text_boxes(
lines: Vec<Vec<[f64; 2]>>,
runs: &[SheetText],
) -> Vec<Vec<[f64; 2]>> {
let boxes: Vec<Box2> = runs.iter().map(Box2::of_text).collect();
break_for_text(lines, &boxes)
}
fn visible_spans(p: [f64; 2], q: [f64; 2], boxes: &[Box2]) -> Vec<(f64, f64)> {
let mut blocked: Vec<(f64, f64)> = Vec::new();
for box2 in boxes {
let (Some(a), Some(b)) = (box2.local(p), box2.local(q)) else {
continue;
};
let (mut t0, mut t1) = (0.0f64, 1.0f64);
let mut inside = true;
for (axis, half) in [(0usize, box2.half[0]), (1usize, box2.half[1])] {
let delta = b[axis] - a[axis];
if delta.abs() < 1e-12 {
if a[axis].abs() > half {
inside = false;
break;
}
continue;
}
let (lo, hi) = ((-half - a[axis]) / delta, (half - a[axis]) / delta);
let (lo, hi) = if lo <= hi { (lo, hi) } else { (hi, lo) };
t0 = t0.max(lo);
t1 = t1.min(hi);
if t0 > t1 {
inside = false;
break;
}
}
if inside && t1 > t0 {
blocked.push((t0, t1));
}
}
if blocked.is_empty() {
return vec![(0.0, 1.0)];
}
blocked.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let mut spans = Vec::new();
let mut cursor = 0.0f64;
for (start, end) in blocked {
if start > cursor + 1e-9 {
spans.push((cursor, start.min(1.0)));
}
cursor = cursor.max(end);
if cursor >= 1.0 {
break;
}
}
if cursor < 1.0 - 1e-9 {
spans.push((cursor, 1.0));
}
spans
}
pub fn plane_is_edge_on(row: &PmiAnnotationReport, forward: [f64; 3]) -> bool {
match row.plane.as_ref() {
None => false,
Some(plane) => dot(norm(plane.normal), forward).abs() < EDGE_ON_COS,
}
}
fn tolerance_sources(
report: Option<&PmiViewReport>,
view: &PmiView,
frame: &ViewFrame,
placed: &PlacedView,
env: &brep_kernel::Env,
) -> Vec<ToleranceSource> {
let Some(report) = report else { return Vec::new() };
let project = |p: [f64; 3]| -> [f64; 2] {
let v = frame.to_view(p);
to_paper(placed, v[0], v[1])
};
let mut out = Vec::new();
for row in report.annotations.iter().filter(|row| row.enabled && row.status == PmiStatus::Ok) {
let Some(annotation) = view.annotations.iter().find(|annotation| annotation.id() == row.id) else { continue };
let Ok(block) = brep_kernel::ToleranceBlock::read(annotation, env) else { continue };
let reference = annotation.flag("isReference");
if block.mode == brep_kernel::ToleranceMode::None && !reference {
continue;
}
let measures = match &row.geometry {
brep_kernel::PmiGeometry::Linear { a, b, component: None } => Measures::Linear { a: project(*a), b: project(*b) },
brep_kernel::PmiGeometry::Radial { center, radius, diameter, sphere: false, .. } => {
Measures::Radial { centre: project(*center), radius: *radius, diameter: *diameter }
}
_ => continue,
};
out.push(ToleranceSource { id: row.id.clone(), measures, block, reference });
}
out
}
fn annotations(
report: Option<&PmiViewReport>,
view: &PmiView,
frame: &ViewFrame,
placed: &PlacedView,
) -> Vec<AnnotationDrawing> {
let Some(report) = report else {
return Vec::new();
};
let paper_text = view.display.text_size_pt * MM_PER_POINT;
let text_height = paper_text / placed.scale.max(1e-9);
let style = PmiLayoutStyle {
arrow: text_height * ARROW_PER_TEXT,
text_height,
view_dir: frame.forward,
view_up: frame.up,
plane: None,
};
let flatten = placed.flatten_text;
let mut out = Vec::new();
for row in &report.annotations {
if !row.enabled || row.status != PmiStatus::Ok {
continue;
}
let row_style = PmiLayoutStyle { plane: row.plane, ..style };
let drawn = pmi_present(&row.geometry, row.label_world, &row.text, &row_style);
let project = |p: [f64; 3]| -> [f64; 2] {
let v = frame.to_view(p);
to_paper(placed, v[0], v[1])
};
let project_line = |polyline: &Vec<[f64; 3]>| -> Vec<[f64; 2]> {
polyline.iter().map(|p| project(*p)).collect()
};
let mut lines: Vec<Vec<[f64; 2]>> = drawn.polylines.iter().map(project_line).collect();
for arrow in &drawn.arrows {
let mut closed: Vec<[f64; 3]> = arrow.to_vec();
closed.push(arrow[0]);
lines.push(project_line(&closed));
}
let unit = drawn
.texts
.first()
.filter(|_| !drawn.frames.is_empty())
.map(|run| Unit {
anchor: drawn.text_anchor,
right: run.right,
up: run.up,
cap: run.height,
});
let flat_about = |anchor: [f64; 3], right3: [f64; 3], up3: [f64; 3], p: [f64; 3]| -> [f64; 2] {
let d = sub(p, anchor);
let a = project(anchor);
[a[0] + dot(d, right3) * placed.scale, a[1] - dot(d, up3) * placed.scale]
};
let texts: Vec<SheetText> = drawn
.texts
.iter()
.filter(|run| !run.text.trim().is_empty())
.map(|run| {
let cap = run.height * placed.scale;
if flatten {
let anchor = match unit {
Some(unit) => flat_about(unit.anchor, unit.right, unit.up, run.anchor),
None => project(run.anchor),
};
return SheetText::flat(run.text.clone(), anchor, cap);
}
let image = |v: [f64; 3]| -> [f64; 2] {
[dot(v, frame.right) * cap, -dot(v, frame.up) * cap]
};
let mut right = image(run.right);
if dot(cross(run.right, run.up), frame.forward) > 0.0 {
right = [-right[0], -right[1]];
}
SheetText::new(run.text.clone(), project(run.anchor), right, image(run.up))
})
.collect();
let frames: Vec<Vec<[f64; 2]>> = match (flatten, unit) {
(true, Some(unit)) => drawn
.frames
.iter()
.map(|polyline| {
polyline
.iter()
.map(|p| flat_about(unit.anchor, unit.right, unit.up, *p))
.collect()
})
.collect(),
_ => drawn.frames.iter().map(project_line).collect(),
};
let mut boxes: Vec<Box2> = texts.iter().map(Box2::of_text).collect();
if let Some(unit) = unit {
let scaled = unit.cap * placed.scale;
let (right, up) = if flatten {
([scaled, 0.0], [0.0, -scaled])
} else {
let image = |v: [f64; 3]| [dot(v, frame.right) * scaled, -dot(v, frame.up) * scaled];
(image(unit.right), image(unit.up))
};
if let Some(box2) = unit.box_of(&drawn.frames, project(unit.anchor), right, up) {
boxes.push(box2);
}
}
let mut lines = break_for_text(lines, &boxes);
lines.extend(frames);
out.push(AnnotationDrawing {
id: row.id.clone(),
kind: row.kind.clone(),
text: row.text.clone(),
lines,
texts,
note: if !plane_is_edge_on(row, frame.forward) {
String::new()
} else if flatten {
format!(
"annotation plane edge-on to '{}': its geometry projects to a line, its text is flattened onto the sheet",
view.name
)
} else {
format!(
"annotation plane edge-on to '{}': it and its text project to a line",
view.name
)
},
});
}
out
}
#[derive(Debug, Clone, Copy)]
struct Unit {
anchor: [f64; 3],
right: [f64; 3],
up: [f64; 3],
cap: f64,
}
impl Unit {
fn box_of(
&self,
frames: &[Vec<[f64; 3]>],
anchor: [f64; 2],
right: [f64; 2],
up: [f64; 2],
) -> Option<Box2> {
if self.cap <= 0.0 {
return None;
}
let mut lo = [f64::INFINITY; 2];
let mut hi = [f64::NEG_INFINITY; 2];
for polyline in frames {
for p in polyline {
let d = sub(*p, self.anchor);
let local = [dot(d, self.right) / self.cap, dot(d, self.up) / self.cap];
for axis in 0..2 {
lo[axis] = lo[axis].min(local[axis]);
hi[axis] = hi[axis].max(local[axis]);
}
}
}
if lo[0] > hi[0] {
return None;
}
let mid = [(lo[0] + hi[0]) * 0.5, (lo[1] + hi[1]) * 0.5];
Some(Box2 {
centre: [
anchor[0] + mid[0] * right[0] + mid[1] * up[0],
anchor[1] + mid[0] * right[1] + mid[1] * up[1],
],
right,
up,
half: [(hi[0] - lo[0]) * 0.5, (hi[1] - lo[1]) * 0.5],
})
}
}