278 lines
12 KiB
Rust
278 lines
12 KiB
Rust
//! Display-only sampling. CAD curves, profiles and constraints are never changed here.
|
|
use std::collections::HashMap;
|
|
use std::hash::{Hash, Hasher};
|
|
use std::sync::Arc;
|
|
|
|
use solidcraft_engine::doc::ModelState;
|
|
use solidcraft_engine::geom::{Loop2, Plane, Region2, Vec2, Vec3};
|
|
use solidcraft_engine::render::{PixelCurveView, SampledPath, SamplingBudget, SamplingQuality, sample_segment, sample_segment_unclipped};
|
|
use solidcraft_engine::sketch::Sketch;
|
|
|
|
use crate::SolveApp;
|
|
use crate::viewport::Proj;
|
|
|
|
pub fn projection_key(proj: &Proj, ppp: f32) -> u64 {
|
|
let mut h = std::collections::hash_map::DefaultHasher::new();
|
|
for n in proj.vp.0.iter().flatten() {
|
|
n.to_bits().hash(&mut h);
|
|
}
|
|
for n in [proj.rect.left(), proj.rect.top(), proj.rect.width(), proj.rect.height(), ppp] {
|
|
n.to_bits().hash(&mut h);
|
|
}
|
|
h.finish()
|
|
}
|
|
|
|
pub fn pixel_view(proj: &Proj, ppp: f32) -> PixelCurveView {
|
|
PixelCurveView { vp: proj.vp, width_px: f64::from(proj.rect.width() * ppp), height_px: f64::from(proj.rect.height() * ppp) }
|
|
}
|
|
|
|
/// The pieces a curve is drawn and picked from. An infinite line (#1359) reaches across the whole
|
|
/// view: through the point of its line nearest the view's centre, far past the view each way
|
|
/// (the sampler clips it to the view). Only the display changes: the line keeps its two points.
|
|
fn display_segs(sketch: &Sketch, index: usize, plane: &Plane, proj: &Proj) -> Vec<solidcraft_engine::geom::Seg2> {
|
|
use solidcraft_engine::geom::Seg2;
|
|
let segs = sketch.exact_segs(index).unwrap_or_else(|| sketch.segs(index));
|
|
if !sketch.curves.get(index).is_some_and(|c| c.infinite) {
|
|
return segs;
|
|
}
|
|
let target = proj.cam.target - plane.origin;
|
|
let centre = Vec2::new(target.dot(plane.x), target.dot(plane.y));
|
|
let reach = 4.0 * (proj.cam.distance.abs() + proj.cam.half_height().abs()) + 1.0;
|
|
segs.into_iter()
|
|
.map(|s| match s {
|
|
Seg2::Line { a, b } => match (b - a).normalized() {
|
|
Some(dir) => {
|
|
let mid = a + dir * (centre - a).dot(dir);
|
|
let reach = reach + mid.dist(centre);
|
|
Seg2::Line { a: mid - dir * reach, b: mid + dir * reach }
|
|
}
|
|
None => s,
|
|
},
|
|
other => other,
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub fn curve_with_budget(sketch: &Sketch, index: usize, plane: &Plane, proj: &Proj, ppp: f32, budget: &mut SamplingBudget) -> SampledPath {
|
|
let segments = display_segs(sketch, index, plane, proj);
|
|
let mut local = SamplingBudget::new(budget.remaining_vertices.min(4096), budget.remaining_nodes.min(65_536));
|
|
let initial = local.clone();
|
|
let mut out = SampledPath { parts: Vec::new(), quality: SamplingQuality::Complete, vertex_count: 0 };
|
|
for segment in segments {
|
|
let sampled = sample_segment(&segment, plane, &pixel_view(proj, ppp), &mut local);
|
|
if sampled.quality != SamplingQuality::Complete {
|
|
out.quality = sampled.quality;
|
|
}
|
|
out.vertex_count += sampled.vertex_count;
|
|
out.parts.extend(sampled.parts);
|
|
}
|
|
budget.remaining_vertices -= initial.remaining_vertices - local.remaining_vertices;
|
|
budget.remaining_nodes -= initial.remaining_nodes - local.remaining_nodes;
|
|
out
|
|
}
|
|
|
|
pub fn curve_unclipped(sketch: &Sketch, index: usize, plane: &Plane, proj: &Proj, ppp: f32) -> SampledPath {
|
|
let mut budget = SamplingBudget::new(4096, 65_536);
|
|
let mut out = SampledPath { parts: Vec::new(), quality: SamplingQuality::Complete, vertex_count: 0 };
|
|
for segment in display_segs(sketch, index, plane, proj) {
|
|
let sampled = sample_segment_unclipped(&segment, plane, &pixel_view(proj, ppp), &mut budget);
|
|
if sampled.quality != SamplingQuality::Complete {
|
|
out.quality = sampled.quality;
|
|
}
|
|
out.vertex_count += sampled.vertex_count;
|
|
out.parts.extend(sampled.parts);
|
|
}
|
|
out
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub fn curve_parts(sketch: &Sketch, index: usize, plane: &Plane, proj: &Proj, ppp: f32) -> Vec<Vec<Vec3>> {
|
|
curve_with_budget(sketch, index, plane, proj, ppp, &mut SamplingBudget::new(4096, 65_536)).parts
|
|
}
|
|
|
|
/// A closed display region made from complete, unclipped adaptive boundaries. Near-plane
|
|
/// clipping or an exhausted budget suppresses the fill instead of inventing closing edges.
|
|
#[cfg(test)]
|
|
pub fn display_region(region: &Region2, plane: &Plane, proj: &Proj, ppp: f32) -> Option<Region2> {
|
|
let mut budget = SamplingBudget::new(4096, 65_536);
|
|
display_region_with_budget(region, plane, proj, ppp, &mut budget)
|
|
}
|
|
|
|
fn display_region_with_budget(region: &Region2, plane: &Plane, proj: &Proj, ppp: f32, shared: &mut SamplingBudget) -> Option<Region2> {
|
|
let mut budget = SamplingBudget::new(shared.remaining_vertices.min(4096), shared.remaining_nodes.min(65_536));
|
|
let initial = budget.clone();
|
|
let result = (|| {
|
|
let mut boundary = |loop_: &Loop2| -> Option<Loop2> {
|
|
let mut points: Vec<Vec2> = Vec::new();
|
|
for segment in &loop_.segs {
|
|
let sampled = sample_segment_unclipped(segment, plane, &pixel_view(proj, ppp), &mut budget);
|
|
if sampled.quality != SamplingQuality::Complete || sampled.parts.len() != 1 {
|
|
return None;
|
|
}
|
|
let part = sampled.parts.first()?;
|
|
// Both exact endpoints must survive; a clipped boundary cannot close a region.
|
|
if part.first()?.dist(plane.to_world(segment.start())) > 1e-8 || part.last()?.dist(plane.to_world(segment.end())) > 1e-8 {
|
|
return None;
|
|
}
|
|
for point in part {
|
|
let point = plane.to_local(*point);
|
|
if points.last().is_none_or(|last| last.dist(point) > 1e-12) {
|
|
points.push(point);
|
|
}
|
|
}
|
|
}
|
|
if points.len() < 3 || points.first()?.dist(*points.last()?) > 1e-8 {
|
|
return None;
|
|
}
|
|
points.pop();
|
|
Some(Loop2::polygon(&points))
|
|
};
|
|
let outer = boundary(®ion.outer)?;
|
|
let holes = region.holes.iter().map(&mut boundary).collect::<Option<Vec<_>>>()?;
|
|
Some(Region2 { outer, holes })
|
|
})();
|
|
shared.remaining_vertices -= initial.remaining_vertices - budget.remaining_vertices;
|
|
shared.remaining_nodes -= initial.remaining_nodes - budget.remaining_nodes;
|
|
result
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub fn region_triangles(region: &Region2, plane: &Plane, proj: &Proj, ppp: f32) -> Vec<[Vec2; 3]> {
|
|
display_region(region, plane, proj, ppp).map_or_else(Vec::new, |display| fill_triangles(&display, &mut 65_536))
|
|
}
|
|
|
|
/// Bound the slab triangulator's allocation before calling it. Every slab emits one
|
|
/// triangle per edge crossing it; complex fills are omitted rather than cut into holes.
|
|
pub fn fill_triangles(display: &Region2, budget: &mut usize) -> Vec<[Vec2; 3]> {
|
|
let mut spans = Vec::new();
|
|
for loop_ in std::iter::once(&display.outer).chain(&display.holes) {
|
|
for segment in &loop_.segs {
|
|
let (a, b) = (segment.start(), segment.end());
|
|
if (a.y - b.y).abs() > 1e-12 {
|
|
spans.push((a.y.min(b.y), a.y.max(b.y)));
|
|
}
|
|
}
|
|
}
|
|
let mut heights: Vec<f64> = spans.iter().flat_map(|&(lo, hi)| [lo, hi]).collect();
|
|
heights.sort_by(f64::total_cmp);
|
|
heights.dedup_by(|a, b| (*a - *b).abs() < 1e-12);
|
|
// An edge crosses the slabs between its end heights. Counting every edge in every slab
|
|
// overestimated a zoomed-in ring a hundredfold and dropped its hover fill (#164).
|
|
let slab = |y: f64| heights.partition_point(|h| *h < y - 1e-12);
|
|
let crossings = spans.iter().fold(0usize, |n, &(lo, hi)| n.saturating_add(slab(hi).saturating_sub(slab(lo))));
|
|
if crossings > *budget {
|
|
return Vec::new();
|
|
}
|
|
let triangles = display.triangulate(1.0);
|
|
*budget = budget.saturating_sub(triangles.len());
|
|
triangles
|
|
}
|
|
|
|
pub fn boundary_parts(loop_: &Loop2, plane: &Plane, proj: &Proj, ppp: f32) -> Vec<Vec<Vec3>> {
|
|
let mut budget = SamplingBudget::new(4096, 65_536);
|
|
loop_.segs.iter().flat_map(|segment| sample_segment(segment, plane, &pixel_view(proj, ppp), &mut budget).parts).collect()
|
|
}
|
|
|
|
pub struct DisplayCache {
|
|
state: Option<Arc<ModelState>>,
|
|
view: u64,
|
|
curves: HashMap<(u64, usize, [u64; 9]), Arc<SampledPath>>,
|
|
profiles: HashMap<(u64, usize, [u64; 9]), Arc<Option<Region2>>>,
|
|
budget: SamplingBudget,
|
|
}
|
|
|
|
impl Default for DisplayCache {
|
|
fn default() -> Self {
|
|
Self { state: None, view: 0, curves: HashMap::new(), profiles: HashMap::new(), budget: SamplingBudget::new(131_072, 1_048_576) }
|
|
}
|
|
}
|
|
|
|
impl DisplayCache {
|
|
#[cfg(test)]
|
|
pub fn len(&self) -> usize {
|
|
self.curves.len()
|
|
}
|
|
|
|
pub fn prepare(&mut self, state: Arc<ModelState>, proj: &Proj, ppp: f32) {
|
|
self.reset(&state, proj, ppp);
|
|
}
|
|
|
|
fn reset(&mut self, state: &Arc<ModelState>, proj: &Proj, ppp: f32) {
|
|
let view = projection_key(proj, ppp);
|
|
if view != self.view || self.state.as_ref().is_none_or(|old| !Arc::ptr_eq(old, state)) {
|
|
self.curves.clear();
|
|
self.profiles.clear();
|
|
self.budget = SamplingBudget::new(131_072, 1_048_576);
|
|
self.view = view;
|
|
self.state = Some(Arc::clone(state));
|
|
}
|
|
}
|
|
|
|
fn curve(&mut self, state: Arc<ModelState>, sketch: u64, index: usize, plane: &Plane, proj: &Proj, ppp: f32) -> Arc<SampledPath> {
|
|
self.reset(&state, proj, ppp);
|
|
let plane_key =
|
|
[plane.origin.x, plane.origin.y, plane.origin.z, plane.x.x, plane.x.y, plane.x.z, plane.y.x, plane.y.y, plane.y.z].map(f64::to_bits);
|
|
let key = (sketch, index, plane_key);
|
|
if !self.curves.contains_key(&key) && (self.curves.len() >= 4096 || self.budget.remaining_vertices == 0 || self.budget.remaining_nodes == 0) {
|
|
return Arc::new(SampledPath { parts: Vec::new(), quality: SamplingQuality::BudgetLimited, vertex_count: 0 });
|
|
}
|
|
self.curves
|
|
.entry(key)
|
|
.or_insert_with(|| {
|
|
Arc::new(state.sketch(sketch).map_or_else(
|
|
|| SampledPath { parts: Vec::new(), quality: SamplingQuality::Invalid, vertex_count: 0 },
|
|
|ss| curve_with_budget(&ss.sketch, index, plane, proj, ppp, &mut self.budget),
|
|
))
|
|
})
|
|
.clone()
|
|
}
|
|
|
|
fn profile(&mut self, state: Arc<ModelState>, sketch: u64, index: usize, plane: &Plane, proj: &Proj, ppp: f32) -> Arc<Option<Region2>> {
|
|
self.reset(&state, proj, ppp);
|
|
let plane_key =
|
|
[plane.origin.x, plane.origin.y, plane.origin.z, plane.x.x, plane.x.y, plane.x.z, plane.y.x, plane.y.y, plane.y.z].map(f64::to_bits);
|
|
let key = (sketch, index, plane_key);
|
|
if !self.profiles.contains_key(&key)
|
|
&& (self.profiles.len() >= 1024 || self.budget.remaining_vertices == 0 || self.budget.remaining_nodes == 0)
|
|
{
|
|
return Arc::new(None);
|
|
}
|
|
self.profiles
|
|
.entry(key)
|
|
.or_insert_with(|| {
|
|
Arc::new(
|
|
state
|
|
.sketch(sketch)
|
|
.and_then(|ss| ss.profiles.get(index))
|
|
.and_then(|profile| display_region_with_budget(&profile.region, plane, proj, ppp, &mut self.budget)),
|
|
)
|
|
})
|
|
.clone()
|
|
}
|
|
}
|
|
|
|
pub fn profile_region(app: &SolveApp, sketch: u64, index: usize, plane: &Plane, proj: &Proj) -> Arc<Option<Region2>> {
|
|
let state = app.session.world_state();
|
|
let ppp = app.viewport.pixels_per_point();
|
|
match app.viewport.display.lock() {
|
|
Ok(mut cache) => cache.profile(state, sketch, index, plane, proj, ppp),
|
|
Err(_) => Arc::new(None),
|
|
}
|
|
}
|
|
|
|
pub fn curve_sample(app: &SolveApp, sketch: u64, index: usize, plane: &Plane, proj: &Proj) -> Arc<SampledPath> {
|
|
let state = app.session.world_state();
|
|
let ppp = app.viewport.pixels_per_point();
|
|
match app.viewport.display.lock() {
|
|
Ok(mut cache) => cache.curve(state, sketch, index, plane, proj, ppp),
|
|
Err(_) => Arc::new(state.sketch(sketch).map_or_else(
|
|
|| SampledPath { parts: Vec::new(), quality: SamplingQuality::Invalid, vertex_count: 0 },
|
|
|ss| curve_with_budget(&ss.sketch, index, plane, proj, ppp, &mut SamplingBudget::new(4096, 65_536)),
|
|
)),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "infinite_line_display_tests.rs"]
|
|
mod infinite_line_display_tests;
|