fix(hatch): harden boundary extraction
Use a sweep broad phase, preserve f64 coordinates, and split collinear overlaps so large drawings remain responsive and precise.
This commit is contained in:
parent
598de3e47a
commit
65b882dd76
3 changed files with 323 additions and 88 deletions
|
|
@ -50,7 +50,7 @@ enum Mode {
|
||||||
|
|
||||||
// ── CPU point-in-polygon (ray casting) ────────────────────────────────────
|
// ── CPU point-in-polygon (ray casting) ────────────────────────────────────
|
||||||
|
|
||||||
fn point_in_polygon(p: [f32; 2], poly: &[[f32; 2]]) -> bool {
|
fn point_in_polygon(p: [f64; 2], poly: &[[f64; 2]]) -> bool {
|
||||||
let n = poly.len();
|
let n = poly.len();
|
||||||
if n < 3 {
|
if n < 3 {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -73,25 +73,26 @@ fn point_in_polygon(p: [f32; 2], poly: &[[f32; 2]]) -> bool {
|
||||||
|
|
||||||
/// Shoelace-area magnitude of a polygon. Used to pick the smallest enclosing
|
/// Shoelace-area magnitude of a polygon. Used to pick the smallest enclosing
|
||||||
/// outline when a click falls inside several nested boundaries.
|
/// outline when a click falls inside several nested boundaries.
|
||||||
fn polygon_area(poly: &[[f32; 2]]) -> f32 {
|
fn polygon_area(poly: &[[f64; 2]]) -> f64 {
|
||||||
let n = poly.len();
|
let n = poly.len();
|
||||||
if n < 3 {
|
if n < 3 {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
let mut a = 0.0_f64;
|
let origin = poly[0];
|
||||||
let mut j = n - 1;
|
let mut a = 0.0;
|
||||||
for i in 0..n {
|
for i in 1..n - 1 {
|
||||||
a += (poly[j][0] as f64) * (poly[i][1] as f64)
|
let current = poly[i];
|
||||||
- (poly[i][0] as f64) * (poly[j][1] as f64);
|
let next = poly[i + 1];
|
||||||
j = i;
|
a += (current[0] - origin[0]) * (next[1] - origin[1])
|
||||||
|
- (next[0] - origin[0]) * (current[1] - origin[1]);
|
||||||
}
|
}
|
||||||
(a * 0.5).abs() as f32
|
(a * 0.5).abs()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// True when every vertex of `inner` lies inside `outer`. Sufficient to
|
/// True when every vertex of `inner` lies inside `outer`. Sufficient to
|
||||||
/// recognise a closed hatch outline as nested inside another for the common
|
/// recognise a closed hatch outline as nested inside another for the common
|
||||||
/// rectangle / closed-polyline case.
|
/// rectangle / closed-polyline case.
|
||||||
fn polygon_contains_polygon(outer: &[[f32; 2]], inner: &[[f32; 2]]) -> bool {
|
fn polygon_contains_polygon(outer: &[[f64; 2]], inner: &[[f64; 2]]) -> bool {
|
||||||
if inner.len() < 3 {
|
if inner.len() < 3 {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -110,10 +111,10 @@ fn polygon_contains_polygon(outer: &[[f32; 2]], inner: &[[f32; 2]]) -> bool {
|
||||||
/// * click inside the innermost shape → hatch just that shape,
|
/// * click inside the innermost shape → hatch just that shape,
|
||||||
/// * click in a gap → hatch that ring, with the next level in as holes.
|
/// * click in a gap → hatch that ring, with the next level in as holes.
|
||||||
fn resolve_hatch_rings(
|
fn resolve_hatch_rings(
|
||||||
outlines: &[Vec<[f32; 2]>],
|
outlines: &[Vec<[f64; 2]>],
|
||||||
p: [f32; 2],
|
p: [f64; 2],
|
||||||
) -> Option<Vec<Vec<[f64; 2]>>> {
|
) -> Option<Vec<Vec<[f64; 2]>>> {
|
||||||
let mut containing: Vec<(usize, f32)> = outlines
|
let mut containing: Vec<(usize, f64)> = outlines
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter(|(_, o)| point_in_polygon(p, o))
|
.filter(|(_, o)| point_in_polygon(p, o))
|
||||||
|
|
@ -127,8 +128,7 @@ fn resolve_hatch_rings(
|
||||||
let outer_idx = containing[0].0;
|
let outer_idx = containing[0].0;
|
||||||
let outer = &outlines[outer_idx];
|
let outer = &outlines[outer_idx];
|
||||||
|
|
||||||
let mut rings: Vec<Vec<[f64; 2]>> =
|
let mut rings = vec![outer.clone()];
|
||||||
vec![outer.iter().map(|&[x, y]| [x as f64, y as f64]).collect()];
|
|
||||||
for (i, o) in outlines.iter().enumerate() {
|
for (i, o) in outlines.iter().enumerate() {
|
||||||
if i == outer_idx {
|
if i == outer_idx {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -149,7 +149,7 @@ fn resolve_hatch_rings(
|
||||||
&& polygon_contains_polygon(x, o)
|
&& polygon_contains_polygon(x, o)
|
||||||
});
|
});
|
||||||
if !has_intermediate {
|
if !has_intermediate {
|
||||||
rings.push(o.iter().map(|&[x, y]| [x as f64, y as f64]).collect());
|
rings.push(o.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(rings)
|
Some(rings)
|
||||||
|
|
@ -194,14 +194,14 @@ fn rte_boundary(pts: impl Iterator<Item = (f64, f64)>) -> (Vec<[f32; 2]>, [f64;
|
||||||
// ── HATCH command ──────────────────────────────────────────────────────────
|
// ── HATCH command ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub struct HatchCommand {
|
pub struct HatchCommand {
|
||||||
outlines: Vec<Vec<[f32; 2]>>,
|
outlines: Vec<Vec<[f64; 2]>>,
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
manual_pts: Vec<DVec3>,
|
manual_pts: Vec<DVec3>,
|
||||||
missed: bool,
|
missed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HatchCommand {
|
impl HatchCommand {
|
||||||
pub fn new(outlines: Vec<Vec<[f32; 2]>>) -> Self {
|
pub fn new(outlines: Vec<Vec<[f64; 2]>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
outlines,
|
outlines,
|
||||||
mode: Mode::PickInside,
|
mode: Mode::PickInside,
|
||||||
|
|
@ -294,8 +294,7 @@ impl CadCommand for HatchCommand {
|
||||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||||
match &self.mode {
|
match &self.mode {
|
||||||
Mode::PickInside => {
|
Mode::PickInside => {
|
||||||
// Hit-test against the f32 outline catalog (screen-space).
|
let xy = [pt.x, pt.y];
|
||||||
let xy = [pt.x as f32, pt.y as f32];
|
|
||||||
match resolve_hatch_rings(&self.outlines, xy) {
|
match resolve_hatch_rings(&self.outlines, xy) {
|
||||||
Some(rings) => {
|
Some(rings) => {
|
||||||
self.missed = false;
|
self.missed = false;
|
||||||
|
|
@ -375,7 +374,7 @@ impl CadCommand for HatchCommand {
|
||||||
// ── GRADIENT command ───────────────────────────────────────────────────────
|
// ── GRADIENT command ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub struct GradientCommand {
|
pub struct GradientCommand {
|
||||||
outlines: Vec<Vec<[f32; 2]>>,
|
outlines: Vec<Vec<[f64; 2]>>,
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
manual_pts: Vec<DVec3>,
|
manual_pts: Vec<DVec3>,
|
||||||
missed: bool,
|
missed: bool,
|
||||||
|
|
@ -386,7 +385,7 @@ pub struct GradientCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GradientCommand {
|
impl GradientCommand {
|
||||||
pub fn new(outlines: Vec<Vec<[f32; 2]>>) -> Self {
|
pub fn new(outlines: Vec<Vec<[f64; 2]>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
outlines,
|
outlines,
|
||||||
mode: Mode::PickInside,
|
mode: Mode::PickInside,
|
||||||
|
|
@ -486,8 +485,7 @@ impl CadCommand for GradientCommand {
|
||||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||||
match &self.mode {
|
match &self.mode {
|
||||||
Mode::PickInside => {
|
Mode::PickInside => {
|
||||||
// Hit-test against the f32 outline catalog (screen-space).
|
let xy = [pt.x, pt.y];
|
||||||
let xy = [pt.x as f32, pt.y as f32];
|
|
||||||
match resolve_hatch_rings(&self.outlines, xy) {
|
match resolve_hatch_rings(&self.outlines, xy) {
|
||||||
Some(rings) => {
|
Some(rings) => {
|
||||||
self.missed = false;
|
self.missed = false;
|
||||||
|
|
@ -581,12 +579,12 @@ impl CadCommand for GradientCommand {
|
||||||
// ── BOUNDARY command ───────────────────────────────────────────────────────
|
// ── BOUNDARY command ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub struct BoundaryCommand {
|
pub struct BoundaryCommand {
|
||||||
outlines: Vec<Vec<[f32; 2]>>,
|
outlines: Vec<Vec<[f64; 2]>>,
|
||||||
missed: bool,
|
missed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BoundaryCommand {
|
impl BoundaryCommand {
|
||||||
pub fn new(outlines: Vec<Vec<[f32; 2]>>) -> Self {
|
pub fn new(outlines: Vec<Vec<[f64; 2]>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
outlines,
|
outlines,
|
||||||
missed: false,
|
missed: false,
|
||||||
|
|
@ -609,8 +607,7 @@ impl CadCommand for BoundaryCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||||
// Hit-test against the f32 outline catalog (screen-space).
|
let xy = [pt.x, pt.y];
|
||||||
let xy = [pt.x as f32, pt.y as f32];
|
|
||||||
match resolve_hatch_rings(&self.outlines, xy) {
|
match resolve_hatch_rings(&self.outlines, xy) {
|
||||||
Some(rings) => {
|
Some(rings) => {
|
||||||
self.missed = false;
|
self.missed = false;
|
||||||
|
|
@ -656,13 +653,13 @@ inventory::submit!(crate::command::CommandRegistration { names: &["HATCH"] });
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn rect(x0: f32, y0: f32, x1: f32, y1: f32) -> Vec<[f32; 2]> {
|
fn rect(x0: f64, y0: f64, x1: f64, y1: f64) -> Vec<[f64; 2]> {
|
||||||
vec![[x0, y0], [x1, y0], [x1, y1], [x0, y1]]
|
vec![[x0, y0], [x1, y0], [x1, y1], [x0, y1]]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Two nested rectangles, regardless of draw order, the resolution must be
|
// Two nested rectangles, regardless of draw order, the resolution must be
|
||||||
// deterministic and independent of which was drawn first.
|
// deterministic and independent of which was drawn first.
|
||||||
fn nested(draw_order: bool) -> Vec<Vec<[f32; 2]>> {
|
fn nested(draw_order: bool) -> Vec<Vec<[f64; 2]>> {
|
||||||
let big = rect(-10.0, -10.0, 10.0, 10.0);
|
let big = rect(-10.0, -10.0, 10.0, 10.0);
|
||||||
let small = rect(-5.0, -5.0, 5.0, 5.0);
|
let small = rect(-5.0, -5.0, 5.0, 5.0);
|
||||||
if draw_order {
|
if draw_order {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use super::*;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
const NODE_EPS: f64 = 1.0e-6;
|
const NODE_EPS: f64 = 1.0e-6;
|
||||||
const PARAM_EPS: f64 = 1.0e-9;
|
const ORIENTATION_EPS: f64 = 1.0e-12;
|
||||||
const AREA_EPS: f64 = 1.0e-10;
|
const AREA_EPS: f64 = 1.0e-10;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
|
@ -43,6 +43,51 @@ impl Segment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
struct SegmentAabb {
|
||||||
|
min_x: f64,
|
||||||
|
max_x: f64,
|
||||||
|
min_y: f64,
|
||||||
|
max_y: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SegmentAabb {
|
||||||
|
fn new(segment: Segment) -> Self {
|
||||||
|
Self {
|
||||||
|
min_x: segment.a.x.min(segment.b.x),
|
||||||
|
max_x: segment.a.x.max(segment.b.x),
|
||||||
|
min_y: segment.a.y.min(segment.b.y),
|
||||||
|
max_y: segment.a.y.max(segment.b.y),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn min(self, axis: SweepAxis) -> f64 {
|
||||||
|
match axis {
|
||||||
|
SweepAxis::X => self.min_x,
|
||||||
|
SweepAxis::Y => self.min_y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn max(self, axis: SweepAxis) -> f64 {
|
||||||
|
match axis {
|
||||||
|
SweepAxis::X => self.max_x,
|
||||||
|
SweepAxis::Y => self.max_y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum SweepAxis {
|
||||||
|
X,
|
||||||
|
Y,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SegmentIntersection {
|
||||||
|
None,
|
||||||
|
Point { a: f64, b: f64 },
|
||||||
|
Overlap { a: [f64; 2], b: [f64; 2] },
|
||||||
|
}
|
||||||
|
|
||||||
impl Scene {
|
impl Scene {
|
||||||
/// Build closed planar regions from the visible wire geometry.
|
/// Build closed planar regions from the visible wire geometry.
|
||||||
///
|
///
|
||||||
|
|
@ -53,7 +98,7 @@ impl Scene {
|
||||||
/// Curved entities participate through their already-tessellated WireModel
|
/// Curved entities participate through their already-tessellated WireModel
|
||||||
/// geometry, so arcs, circles, ellipses and splines can take part in the
|
/// geometry, so arcs, circles, ellipses and splines can take part in the
|
||||||
/// boundary search without modifying the source entities.
|
/// boundary search without modifying the source entities.
|
||||||
pub fn hatch_boundary_outlines(&self) -> Vec<Vec<[f32; 2]>> {
|
pub fn hatch_boundary_outlines(&self) -> Vec<Vec<[f64; 2]>> {
|
||||||
let wires = self.entity_wires();
|
let wires = self.entity_wires();
|
||||||
let mut segments = Vec::<Segment>::new();
|
let mut segments = Vec::<Segment>::new();
|
||||||
|
|
||||||
|
|
@ -87,7 +132,7 @@ impl Scene {
|
||||||
|
|
||||||
build_planar_outlines(&segments)
|
build_planar_outlines(&segments)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|ring| ring.into_iter().map(|p| [p.x as f32, p.y as f32]).collect())
|
.map(|ring| ring.into_iter().map(|p| [p.x, p.y]).collect())
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -105,33 +150,57 @@ fn build_planar_outlines(segments: &[Segment]) -> Vec<Vec<P2>> {
|
||||||
// Each original segment starts with its two endpoints as split positions.
|
// Each original segment starts with its two endpoints as split positions.
|
||||||
let mut cuts: Vec<Vec<f64>> = vec![vec![0.0, 1.0]; segments.len()];
|
let mut cuts: Vec<Vec<f64>> = vec![vec![0.0, 1.0]; segments.len()];
|
||||||
|
|
||||||
// Find all pairwise intersections. The AABB test avoids doing the more
|
// Sweep the less-congested coordinate axis, then run the exact AABB and
|
||||||
// expensive intersection calculation for obviously unrelated segments.
|
// segment tests only for active candidates. Dense intersections remain
|
||||||
for i in 0..segments.len() {
|
// output-sensitive, while spatially separated geometry avoids all-pairs
|
||||||
for j in (i + 1)..segments.len() {
|
// work.
|
||||||
if !segment_aabbs_overlap(segments[i], segments[j]) {
|
let aabbs: Vec<SegmentAabb> = segments.iter().copied().map(SegmentAabb::new).collect();
|
||||||
|
let axis = choose_sweep_axis(&aabbs);
|
||||||
|
let mut order: Vec<usize> = (0..segments.len()).collect();
|
||||||
|
order.sort_by(|&a, &b| aabbs[a].min(axis).total_cmp(&aabbs[b].min(axis)));
|
||||||
|
let mut active = Vec::<usize>::new();
|
||||||
|
|
||||||
|
for i in order {
|
||||||
|
let current_min = aabbs[i].min(axis);
|
||||||
|
active.retain(|&j| aabbs[j].max(axis) + NODE_EPS >= current_min);
|
||||||
|
|
||||||
|
for &j in &active {
|
||||||
|
if !segment_aabbs_overlap(aabbs[i], aabbs[j]) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((ti, tj)) = segment_intersection(segments[i], segments[j]) {
|
match segment_intersection(segments[i], segments[j]) {
|
||||||
cuts[i].push(ti.clamp(0.0, 1.0));
|
SegmentIntersection::None => {}
|
||||||
cuts[j].push(tj.clamp(0.0, 1.0));
|
SegmentIntersection::Point { a, b } => {
|
||||||
|
cuts[i].push(a.clamp(0.0, 1.0));
|
||||||
|
cuts[j].push(b.clamp(0.0, 1.0));
|
||||||
|
}
|
||||||
|
SegmentIntersection::Overlap { a, b } => {
|
||||||
|
cuts[i].push(a[0].clamp(0.0, 1.0));
|
||||||
|
cuts[i].push(a[1].clamp(0.0, 1.0));
|
||||||
|
cuts[j].push(b[0].clamp(0.0, 1.0));
|
||||||
|
cuts[j].push(b[1].clamp(0.0, 1.0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
active.push(i);
|
||||||
|
}
|
||||||
|
|
||||||
// Split every segment at all of its intersection parameters.
|
// Split every segment at all of its intersection parameters.
|
||||||
let mut pieces = Vec::<Segment>::new();
|
let mut pieces = Vec::<Segment>::new();
|
||||||
|
|
||||||
for (segment, params) in segments.iter().copied().zip(cuts.iter_mut()) {
|
for (segment, params) in segments.iter().copied().zip(cuts.iter_mut()) {
|
||||||
|
let segment_len = segment.a.distance2(segment.b).sqrt();
|
||||||
|
let param_merge_eps = (NODE_EPS / segment_len).min(1.0);
|
||||||
params.sort_by(|a, b| a.total_cmp(b));
|
params.sort_by(|a, b| a.total_cmp(b));
|
||||||
params.dedup_by(|a, b| (*a - *b).abs() <= PARAM_EPS);
|
params.dedup_by(|a, b| (*a - *b).abs() <= param_merge_eps);
|
||||||
|
|
||||||
for pair in params.windows(2) {
|
for pair in params.windows(2) {
|
||||||
let t0 = pair[0];
|
let t0 = pair[0];
|
||||||
let t1 = pair[1];
|
let t1 = pair[1];
|
||||||
|
|
||||||
if t1 - t0 <= PARAM_EPS {
|
if (t1 - t0) * segment_len <= NODE_EPS {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,7 +215,7 @@ fn build_planar_outlines(segments: &[Segment]) -> Vec<Vec<P2>> {
|
||||||
|
|
||||||
// Convert split segment endpoints to graph nodes.
|
// Convert split segment endpoints to graph nodes.
|
||||||
let mut nodes = Vec::<P2>::new();
|
let mut nodes = Vec::<P2>::new();
|
||||||
let mut node_map = HashMap::<(i64, i64), usize>::new();
|
let mut node_map = HashMap::<(i64, i64), Vec<usize>>::new();
|
||||||
let mut edges = HashSet::<(usize, usize)>::new();
|
let mut edges = HashSet::<(usize, usize)>::new();
|
||||||
|
|
||||||
for piece in pieces {
|
for piece in pieces {
|
||||||
|
|
@ -262,70 +331,142 @@ fn build_planar_outlines(segments: &[Segment]) -> Vec<Vec<P2>> {
|
||||||
faces
|
faces
|
||||||
}
|
}
|
||||||
|
|
||||||
fn node_for_point(p: P2, nodes: &mut Vec<P2>, map: &mut HashMap<(i64, i64), usize>) -> usize {
|
fn node_for_point(
|
||||||
|
p: P2,
|
||||||
|
nodes: &mut Vec<P2>,
|
||||||
|
map: &mut HashMap<(i64, i64), Vec<usize>>,
|
||||||
|
) -> usize {
|
||||||
let key = node_key(p);
|
let key = node_key(p);
|
||||||
|
|
||||||
if let Some(&index) = map.get(&key) {
|
for dx in -1_i64..=1 {
|
||||||
|
for dy in -1_i64..=1 {
|
||||||
|
let neighbor = (key.0.saturating_add(dx), key.1.saturating_add(dy));
|
||||||
|
if let Some(indices) = map.get(&neighbor) {
|
||||||
|
if let Some(&index) = indices
|
||||||
|
.iter()
|
||||||
|
.find(|&&index| nodes[index].distance2(p) <= NODE_EPS * NODE_EPS)
|
||||||
|
{
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let index = nodes.len();
|
let index = nodes.len();
|
||||||
nodes.push(p);
|
nodes.push(p);
|
||||||
map.insert(key, index);
|
map.entry(key).or_default().push(index);
|
||||||
index
|
index
|
||||||
}
|
}
|
||||||
|
|
||||||
fn node_key(p: P2) -> (i64, i64) {
|
fn node_key(p: P2) -> (i64, i64) {
|
||||||
(
|
(
|
||||||
(p.x / NODE_EPS).round() as i64,
|
(p.x / NODE_EPS).floor() as i64,
|
||||||
(p.y / NODE_EPS).round() as i64,
|
(p.y / NODE_EPS).floor() as i64,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn segment_aabbs_overlap(a: Segment, b: Segment) -> bool {
|
fn choose_sweep_axis(aabbs: &[SegmentAabb]) -> SweepAxis {
|
||||||
let a_min_x = a.a.x.min(a.b.x);
|
if normalized_interval_span(aabbs, SweepAxis::X)
|
||||||
let a_max_x = a.a.x.max(a.b.x);
|
<= normalized_interval_span(aabbs, SweepAxis::Y)
|
||||||
let a_min_y = a.a.y.min(a.b.y);
|
{
|
||||||
let a_max_y = a.a.y.max(a.b.y);
|
SweepAxis::X
|
||||||
|
} else {
|
||||||
|
SweepAxis::Y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let b_min_x = b.a.x.min(b.b.x);
|
fn normalized_interval_span(aabbs: &[SegmentAabb], axis: SweepAxis) -> f64 {
|
||||||
let b_max_x = b.a.x.max(b.b.x);
|
let min = aabbs
|
||||||
let b_min_y = b.a.y.min(b.b.y);
|
.iter()
|
||||||
let b_max_y = b.a.y.max(b.b.y);
|
.map(|aabb| aabb.min(axis))
|
||||||
|
.fold(f64::INFINITY, f64::min);
|
||||||
|
let max = aabbs
|
||||||
|
.iter()
|
||||||
|
.map(|aabb| aabb.max(axis))
|
||||||
|
.fold(f64::NEG_INFINITY, f64::max);
|
||||||
|
let extent = max - min;
|
||||||
|
|
||||||
a_max_x + NODE_EPS >= b_min_x
|
if !extent.is_finite() || extent <= NODE_EPS {
|
||||||
&& b_max_x + NODE_EPS >= a_min_x
|
return f64::INFINITY;
|
||||||
&& a_max_y + NODE_EPS >= b_min_y
|
}
|
||||||
&& b_max_y + NODE_EPS >= a_min_y
|
|
||||||
|
let total: f64 = aabbs
|
||||||
|
.iter()
|
||||||
|
.map(|aabb| aabb.max(axis) - aabb.min(axis) + NODE_EPS)
|
||||||
|
.sum();
|
||||||
|
total / extent
|
||||||
|
}
|
||||||
|
|
||||||
|
fn segment_aabbs_overlap(a: SegmentAabb, b: SegmentAabb) -> bool {
|
||||||
|
a.max_x + NODE_EPS >= b.min_x
|
||||||
|
&& b.max_x + NODE_EPS >= a.min_x
|
||||||
|
&& a.max_y + NODE_EPS >= b.min_y
|
||||||
|
&& b.max_y + NODE_EPS >= a.min_y
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Intersection parameters of two finite XY segments.
|
/// Intersection parameters of two finite XY segments.
|
||||||
///
|
fn segment_intersection(a: Segment, b: Segment) -> SegmentIntersection {
|
||||||
/// Collinear overlaps intentionally return None. Shared endpoints and
|
|
||||||
/// T-junctions are handled by the normal non-parallel case and/or node merging.
|
|
||||||
fn segment_intersection(a: Segment, b: Segment) -> Option<(f64, f64)> {
|
|
||||||
let rx = a.b.x - a.a.x;
|
let rx = a.b.x - a.a.x;
|
||||||
let ry = a.b.y - a.a.y;
|
let ry = a.b.y - a.a.y;
|
||||||
|
|
||||||
let sx = b.b.x - b.a.x;
|
let sx = b.b.x - b.a.x;
|
||||||
let sy = b.b.y - b.a.y;
|
let sy = b.b.y - b.a.y;
|
||||||
|
|
||||||
let cross = rx * sy - ry * sx;
|
let r2 = rx * rx + ry * ry;
|
||||||
|
let s2 = sx * sx + sy * sy;
|
||||||
if cross.abs() <= 1.0e-12 {
|
if r2 <= f64::EPSILON || s2 <= f64::EPSILON {
|
||||||
return None;
|
return SegmentIntersection::None;
|
||||||
}
|
}
|
||||||
|
let r_len = r2.sqrt();
|
||||||
|
let s_len = s2.sqrt();
|
||||||
|
let cross = rx * sy - ry * sx;
|
||||||
let qpx = b.a.x - a.a.x;
|
let qpx = b.a.x - a.a.x;
|
||||||
let qpy = b.a.y - a.a.y;
|
let qpy = b.a.y - a.a.y;
|
||||||
|
|
||||||
|
if cross.abs() <= ORIENTATION_EPS * r_len * s_len {
|
||||||
|
if (qpx * ry - qpy * rx).abs() > NODE_EPS * r_len {
|
||||||
|
return SegmentIntersection::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let b0_on_a = (qpx * rx + qpy * ry) / r2;
|
||||||
|
let b1_on_a = b0_on_a + (sx * rx + sy * ry) / r2;
|
||||||
|
let lo = b0_on_a.min(b1_on_a).max(0.0);
|
||||||
|
let hi = b0_on_a.max(b1_on_a).min(1.0);
|
||||||
|
let a_param_eps = NODE_EPS / r_len;
|
||||||
|
|
||||||
|
if hi < lo - a_param_eps {
|
||||||
|
return SegmentIntersection::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let lo = lo.clamp(0.0, 1.0);
|
||||||
|
let hi = hi.clamp(0.0, 1.0);
|
||||||
|
let lo_point = a.a.lerp(a.b, lo);
|
||||||
|
let hi_point = a.a.lerp(a.b, hi);
|
||||||
|
let b_lo = ((lo_point.x - b.a.x) * sx + (lo_point.y - b.a.y) * sy) / s2;
|
||||||
|
let b_hi = ((hi_point.x - b.a.x) * sx + (hi_point.y - b.a.y) * sy) / s2;
|
||||||
|
|
||||||
|
if (hi - lo) * r_len <= NODE_EPS {
|
||||||
|
return SegmentIntersection::Point {
|
||||||
|
a: (lo + hi) * 0.5,
|
||||||
|
b: (b_lo + b_hi) * 0.5,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return SegmentIntersection::Overlap {
|
||||||
|
a: [lo, hi],
|
||||||
|
b: [b_lo, b_hi],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let t = (qpx * sy - qpy * sx) / cross;
|
let t = (qpx * sy - qpy * sx) / cross;
|
||||||
let u = (qpx * ry - qpy * rx) / cross;
|
let u = (qpx * ry - qpy * rx) / cross;
|
||||||
|
let t_eps = NODE_EPS / r_len;
|
||||||
|
let u_eps = NODE_EPS / s_len;
|
||||||
|
|
||||||
if t >= -PARAM_EPS && t <= 1.0 + PARAM_EPS && u >= -PARAM_EPS && u <= 1.0 + PARAM_EPS {
|
if t >= -t_eps && t <= 1.0 + t_eps && u >= -u_eps && u <= 1.0 + u_eps {
|
||||||
Some((t, u))
|
SegmentIntersection::Point { a: t, b: u }
|
||||||
} else {
|
} else {
|
||||||
None
|
SegmentIntersection::None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -334,12 +475,14 @@ fn signed_area(poly: &[P2]) -> f64 {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let origin = poly[0];
|
||||||
let mut area = 0.0;
|
let mut area = 0.0;
|
||||||
|
|
||||||
for i in 0..poly.len() {
|
for i in 1..poly.len() - 1 {
|
||||||
let a = poly[i];
|
let a = poly[i];
|
||||||
let b = poly[(i + 1) % poly.len()];
|
let b = poly[i + 1];
|
||||||
area += a.x * b.y - b.x * a.y;
|
area += (a.x - origin.x) * (b.y - origin.y)
|
||||||
|
- (b.x - origin.x) * (a.y - origin.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
area * 0.5
|
area * 0.5
|
||||||
|
|
@ -395,4 +538,87 @@ mod tests {
|
||||||
|
|
||||||
assert!(faces.is_empty());
|
assert!(faces.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn collinear_overlap_closes_rectangle() {
|
||||||
|
let segments = vec![
|
||||||
|
s(0.0, 0.0, 7.0, 0.0),
|
||||||
|
s(3.0, 0.0, 10.0, 0.0),
|
||||||
|
s(10.0, 0.0, 10.0, 10.0),
|
||||||
|
s(10.0, 10.0, 0.0, 10.0),
|
||||||
|
s(0.0, 10.0, 0.0, 0.0),
|
||||||
|
];
|
||||||
|
|
||||||
|
let faces = build_planar_outlines(&segments);
|
||||||
|
|
||||||
|
assert_eq!(faces.len(), 1);
|
||||||
|
assert!((signed_area(&faces[0]) - 100.0).abs() < 1.0e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn node_merge_crosses_bucket_boundary() {
|
||||||
|
let mut nodes = Vec::new();
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
let a = node_for_point(P2::new(NODE_EPS * 0.99, 0.0), &mut nodes, &mut map);
|
||||||
|
let b = node_for_point(P2::new(NODE_EPS * 1.01, 0.0), &mut nodes, &mut map);
|
||||||
|
|
||||||
|
assert_eq!(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn node_merge_rejects_distant_diagonal_points() {
|
||||||
|
let mut nodes = Vec::new();
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
let a = node_for_point(
|
||||||
|
P2::new(NODE_EPS * 0.51, NODE_EPS * 0.51),
|
||||||
|
&mut nodes,
|
||||||
|
&mut map,
|
||||||
|
);
|
||||||
|
let b = node_for_point(
|
||||||
|
P2::new(NODE_EPS * 1.49, NODE_EPS * 1.49),
|
||||||
|
&mut nodes,
|
||||||
|
&mut map,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_ne!(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn signed_area_is_stable_at_large_coordinates() {
|
||||||
|
let base = 1.0e12;
|
||||||
|
let poly = vec![
|
||||||
|
P2::new(base, base),
|
||||||
|
P2::new(base + 3.0, base),
|
||||||
|
P2::new(base + 3.0, base + 4.0),
|
||||||
|
P2::new(base, base + 4.0),
|
||||||
|
];
|
||||||
|
|
||||||
|
assert!((signed_area(&poly) - 12.0).abs() < 1.0e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn long_segments_keep_world_scale_cuts() {
|
||||||
|
let segments = vec![
|
||||||
|
s(0.0, 0.0, 1.0e12, 0.0),
|
||||||
|
s(0.0, 1.0, 1.0e12, 1.0),
|
||||||
|
s(1.0, -1.0, 1.0, 2.0),
|
||||||
|
s(2.0, -1.0, 2.0, 2.0),
|
||||||
|
];
|
||||||
|
|
||||||
|
let faces = build_planar_outlines(&segments);
|
||||||
|
|
||||||
|
assert_eq!(faces.len(), 1);
|
||||||
|
assert!((signed_area(&faces[0]) - 1.0).abs() < 1.0e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn long_near_collinear_segments_overlap_within_node_tolerance() {
|
||||||
|
let a = s(0.0, 0.0, 1.0e9, 1.0e-3);
|
||||||
|
let b = s(5.0e8, 5.0e-4 + 5.0e-7, 1.5e9, 1.5e-3 + 5.0e-7);
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
segment_intersection(a, b),
|
||||||
|
SegmentIntersection::Overlap { .. }
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4590,11 +4590,27 @@ impl Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Collect closed polygon outlines (world XY) from the current layout.
|
/// Collect closed polygon outlines (world XY) from the current layout.
|
||||||
pub fn closed_outlines(&self) -> Vec<Vec<[f32; 2]>> {
|
pub fn closed_outlines(&self) -> Vec<Vec<[f64; 2]>> {
|
||||||
self.entity_wires()
|
self.entity_wires()
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|wire| {
|
.filter_map(|wire| {
|
||||||
let pts = &wire.points;
|
let pts: Vec<[f64; 2]> = wire
|
||||||
|
.points
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(index, high)| {
|
||||||
|
if !high[0].is_finite() || !high[1].is_finite() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let low = wire.points_low.get(index).copied().unwrap_or([0.0; 3]);
|
||||||
|
let point = [
|
||||||
|
high[0] as f64 + low[0] as f64,
|
||||||
|
high[1] as f64 + low[1] as f64,
|
||||||
|
];
|
||||||
|
point.iter().all(|value| value.is_finite()).then_some(point)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
if pts.len() < 4 {
|
if pts.len() < 4 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
@ -4612,12 +4628,8 @@ impl Scene {
|
||||||
// previous one, so consumers (point-in-polygon, the hatch /
|
// previous one, so consumers (point-in-polygon, the hatch /
|
||||||
// boundary commands) see one vertex per corner — not the doubled
|
// boundary commands) see one vertex per corner — not the doubled
|
||||||
// ring that otherwise shows two grips at every corner.
|
// ring that otherwise shows two grips at every corner.
|
||||||
let mut ring: Vec<[f32; 2]> = Vec::with_capacity(pts.len());
|
let mut ring: Vec<[f64; 2]> = Vec::with_capacity(pts.len());
|
||||||
for p in pts {
|
for q in pts {
|
||||||
if !p[0].is_finite() || !p[1].is_finite() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let q = [p[0], p[1]];
|
|
||||||
if let Some(&last) = ring.last() {
|
if let Some(&last) = ring.last() {
|
||||||
if (last[0] - q[0]).abs() < 1e-4 && (last[1] - q[1]).abs() < 1e-4 {
|
if (last[0] - q[0]).abs() < 1e-4 && (last[1] - q[1]).abs() < 1e-4 {
|
||||||
continue;
|
continue;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue