From 129f91c8e4564278a7ac437cfcabc71873df7b55 Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Tue, 3 Mar 2026 17:59:04 +0300 Subject: [PATCH 01/10] replace offset overlay --- iOverlay/src/build/mod.rs | 1 - iOverlay/src/build/offset.rs | 83 -------- iOverlay/src/mesh/extract.rs | 232 ---------------------- iOverlay/src/mesh/mod.rs | 1 - iOverlay/src/mesh/outline/builder.rs | 8 +- iOverlay/src/mesh/outline/builder_join.rs | 40 ++-- iOverlay/src/mesh/outline/offset.rs | 38 ++-- iOverlay/src/mesh/outline/section.rs | 6 +- iOverlay/src/mesh/overlay.rs | 51 +---- iOverlay/src/mesh/stroke/builder.rs | 12 +- iOverlay/src/mesh/stroke/builder_cap.rs | 14 +- iOverlay/src/mesh/stroke/builder_join.rs | 34 ++-- iOverlay/src/mesh/stroke/offset.rs | 14 +- iOverlay/src/mesh/stroke/section.rs | 8 +- iOverlay/src/mesh/subject.rs | 25 +-- iOverlay/src/segm/offset.rs | 100 +++++----- 16 files changed, 142 insertions(+), 525 deletions(-) delete mode 100644 iOverlay/src/build/offset.rs delete mode 100644 iOverlay/src/mesh/extract.rs diff --git a/iOverlay/src/build/mod.rs b/iOverlay/src/build/mod.rs index 994e989..af8d094 100644 --- a/iOverlay/src/build/mod.rs +++ b/iOverlay/src/build/mod.rs @@ -1,7 +1,6 @@ pub(crate) mod boolean; pub(crate) mod builder; mod graph; -pub(crate) mod offset; pub(crate) mod string; pub(crate) mod sweep; mod util; diff --git a/iOverlay/src/build/offset.rs b/iOverlay/src/build/offset.rs deleted file mode 100644 index 32369d5..0000000 --- a/iOverlay/src/build/offset.rs +++ /dev/null @@ -1,83 +0,0 @@ -use crate::build::builder::GraphBuilder; -use crate::build::sweep::FillStrategy; -use crate::core::graph::OverlayNode; -use crate::core::link::OverlayLink; -use crate::core::solver::Solver; -use crate::mesh::graph::OffsetGraph; -use crate::segm::offset::ShapeCountOffset; -use crate::segm::segment::{Segment, SegmentFill}; - -impl GraphBuilder { - #[inline] - pub(crate) fn build_offset>( - &mut self, - solver: &Solver, - segments: &[Segment], - ) -> OffsetGraph<'_> { - self.build_fills_with_strategy::(solver, segments); - self.build_links_all(segments); - self.offset_graph(solver) - } - - #[inline] - fn offset_graph(&mut self, solver: &Solver) -> OffsetGraph<'_> { - self.build_nodes_and_connect_links(solver); - OffsetGraph { - nodes: &self.nodes, - links: &self.links, - } - } -} - -pub(crate) struct PositiveSubjectOffsetStrategy; -pub(crate) struct NegativeSubjectOffsetStrategy; -const BOLD_BIT: usize = 2; - -impl FillStrategy for PositiveSubjectOffsetStrategy { - #[inline(always)] - fn add_and_fill(this: ShapeCountOffset, bot: ShapeCountOffset) -> (ShapeCountOffset, SegmentFill) { - let top_subj = bot.subj + this.subj; - let bot_subj = bot.subj; - - let subj_top = (top_subj > 0) as SegmentFill; - let subj_bot = (bot_subj > 0) as SegmentFill; - - let bold = this.bold as SegmentFill; - - let fill = subj_top | (subj_bot << 1) | (bold << BOLD_BIT); - let top = ShapeCountOffset { - subj: top_subj, - bold: false, - }; // bold not need - - (top, fill) - } -} - -impl FillStrategy for NegativeSubjectOffsetStrategy { - #[inline(always)] - fn add_and_fill(this: ShapeCountOffset, bot: ShapeCountOffset) -> (ShapeCountOffset, SegmentFill) { - let top_subj = bot.subj + this.subj; - let bot_subj = bot.subj; - - let subj_top = (top_subj < 0) as SegmentFill; - let subj_bot = (bot_subj < 0) as SegmentFill; - - let bold = this.bold as SegmentFill; - - let fill = subj_top | (subj_bot << 1) | (bold << BOLD_BIT); - let top = ShapeCountOffset { - subj: top_subj, - bold: false, - }; // bold not need - - (top, fill) - } -} - -impl OverlayLink { - #[inline(always)] - pub(crate) fn is_bold(&self) -> bool { - self.fill & (1 << BOLD_BIT) != 0 - } -} diff --git a/iOverlay/src/mesh/extract.rs b/iOverlay/src/mesh/extract.rs deleted file mode 100644 index ca8aa49..0000000 --- a/iOverlay/src/mesh/extract.rs +++ /dev/null @@ -1,232 +0,0 @@ -use crate::bind::segment::{ContourIndex, IdSegment}; -use crate::bind::solver::{JoinHoles, LeftBottomSegment}; -use crate::core::extract::{ - BooleanExtractionBuffer, GraphContour, GraphUtil, StartPathData, Visit, VisitState, -}; -use crate::core::link::OverlayLinkFilter; -use crate::core::overlay::ContourDirection; -use crate::core::overlay_rule::OverlayRule; -use crate::geom::v_segment::VSegment; -use crate::mesh::graph::OffsetGraph; -use crate::segm::segment::SUBJ_TOP; -use alloc::vec; -use alloc::vec::Vec; -use i_float::int::point::IntPoint; -use i_shape::flat::buffer::FlatContoursBuffer; -use i_shape::int::shape::IntShapes; -use i_shape::util::reserve::Reserve; - -impl OffsetGraph<'_> { - pub(crate) fn extract_offset( - &self, - main_direction: ContourDirection, - min_area: u64, - buffer: &mut BooleanExtractionBuffer, - ) -> IntShapes { - self.links - .filter_by_overlay_into(OverlayRule::Subject, &mut buffer.visited); - self.extract_offset_shapes(main_direction, min_area, buffer) - } - - #[inline] - pub(crate) fn extract_contours_into( - &self, - main_direction: ContourDirection, - min_area: u64, - buffer: &mut BooleanExtractionBuffer, - output: &mut FlatContoursBuffer, - ) { - self.links - .filter_by_overlay_into(OverlayRule::Subject, &mut buffer.visited); - self.extract_contours(main_direction, min_area, buffer, output); - } - - fn extract_offset_shapes( - &self, - main_direction: ContourDirection, - min_area: u64, - buffer: &mut BooleanExtractionBuffer, - ) -> IntShapes { - let clockwise = main_direction == ContourDirection::Clockwise; - let len = buffer.visited.len(); - buffer.points.reserve_capacity(len); - - let mut shapes = Vec::new(); - let mut holes = Vec::new(); - let mut anchors = Vec::new(); - - let mut link_index = 0; - let mut is_all_anchors_sorted = true; - while link_index < len { - if buffer.visited.is_visited(link_index) { - link_index += 1; - continue; - } - let left_top_link = unsafe { - // Safety: `link_index` walks 0..buffer.visited.len(), and buffer.visited.len() <= self.links.len(). - GraphUtil::find_left_top_link(self.links, self.nodes, link_index, &buffer.visited) - }; - - let link = unsafe { - // SAFETY: `left_top_link` originates from `find_left_top_link`, which only returns - // indices in 0..self.links.len(), so this lookup cannot go out of bounds. - self.links.get_unchecked(left_top_link) - }; - let is_hole = link.fill & SUBJ_TOP == SUBJ_TOP; - let mut bold = link.is_bold(); - let direction = is_hole == clockwise; - - let start_data = StartPathData::new(direction, link, left_top_link); - - self.find_contour( - &start_data, - direction, - &mut bold, - &mut buffer.visited, - &mut buffer.points, - ); - - if !bold { - link_index += 1; - continue; - } - - let (is_valid, is_modified) = buffer.points.validate(min_area, true); - - if !is_valid { - link_index += 1; - continue; - } - - let contour = buffer.points.to_vec(); - - if is_hole { - let mut v_segment = if clockwise { - VSegment { - a: contour[1], - b: contour[2], - } - } else { - VSegment { - a: contour[0], - b: contour[contour.len() - 1], - } - }; - if is_modified { - let most_left = contour.left_bottom_segment(); - if most_left != v_segment { - v_segment = most_left; - is_all_anchors_sorted = false; - } - }; - - debug_assert_eq!(v_segment, contour.left_bottom_segment()); - let id = ContourIndex::new_hole(holes.len()); - anchors.push(IdSegment::with_segment(id, v_segment)); - holes.push(contour); - } else { - shapes.push(vec![contour]); - } - } - - if !is_all_anchors_sorted { - anchors.sort_by(|s0, s1| s0.v_segment.a.cmp(&s1.v_segment.a)); - } - - shapes.join_sorted_holes(holes, anchors, clockwise); - - shapes - } - - fn extract_contours( - &self, - main_direction: ContourDirection, - min_area: u64, - buffer: &mut BooleanExtractionBuffer, - output: &mut FlatContoursBuffer, - ) { - let clockwise = main_direction == ContourDirection::Clockwise; - let len = buffer.visited.len(); - buffer.points.reserve_capacity(len); - output.clear_and_reserve(len, 4); - - let mut link_index = 0; - while link_index < len { - if buffer.visited.is_visited(link_index) { - link_index += 1; - continue; - } - - let left_top_link = unsafe { - // Safety: `link_index` walks 0..buffer.visited.len(), and buffer.visited.len() <= self.links.len(). - GraphUtil::find_left_top_link(self.links, self.nodes, link_index, &buffer.visited) - }; - - let link = unsafe { - // Safety: `left_top_link` originates from `find_left_top_link`, which only returns - // indices in 0..self.links.len(), so this lookup cannot go out of bounds. - self.links.get_unchecked(left_top_link) - }; - let is_hole = link.fill & SUBJ_TOP == SUBJ_TOP; - let mut bold = link.is_bold(); - let direction = is_hole == clockwise; - - let start_data = StartPathData::new(direction, link, left_top_link); - - self.find_contour( - &start_data, - direction, - &mut bold, - &mut buffer.visited, - &mut buffer.points, - ); - - if !bold { - link_index += 1; - continue; - } - - let (is_valid, _) = buffer.points.validate(min_area, true); - - if !is_valid { - link_index += 1; - continue; - } - - output.add_contour(buffer.points.as_slice()); - } - } - - fn find_contour( - &self, - start_data: &StartPathData, - clockwise: bool, - bold: &mut bool, - visited: &mut [VisitState], - points: &mut Vec, - ) { - let mut link_id = start_data.link_id; - let mut node_id = start_data.node_id; - let last_node_id = start_data.last_node_id; - - visited.visit(link_id); - - points.clear(); - points.push(start_data.begin); - - // Find a closed tour - while node_id != last_node_id { - link_id = GraphUtil::next_link(self.links, self.nodes, link_id, node_id, clockwise, visited); - - let link = unsafe { - // Safety: `link_id` is always derived from a previous in-bounds index or - // from `find_left_top_link`, so it remains in `0..self.links.len()`. - self.links.get_unchecked(link_id) - }; - *bold = *bold || link.is_bold(); - node_id = points.push_node_and_get_other(link, node_id); - - visited.visit(link_id); - } - } -} diff --git a/iOverlay/src/mesh/mod.rs b/iOverlay/src/mesh/mod.rs index d6f71b7..7a1be86 100644 --- a/iOverlay/src/mesh/mod.rs +++ b/iOverlay/src/mesh/mod.rs @@ -1,4 +1,3 @@ -mod extract; pub(crate) mod graph; pub(crate) mod math; mod miter; diff --git a/iOverlay/src/mesh/outline/builder.rs b/iOverlay/src/mesh/outline/builder.rs index 0df178e..a65bf9d 100644 --- a/iOverlay/src/mesh/outline/builder.rs +++ b/iOverlay/src/mesh/outline/builder.rs @@ -1,7 +1,6 @@ use crate::mesh::outline::builder_join::{BevelJoinBuilder, JoinBuilder, MiterJoinBuilder, RoundJoinBuilder}; use crate::mesh::outline::section::{Section, SectionToSegment}; use crate::mesh::style::LineJoin; -use crate::segm::offset::ShapeCountOffset; use crate::segm::segment::Segment; use alloc::boxed::Box; use alloc::vec::Vec; @@ -9,13 +8,14 @@ use core::marker::PhantomData; use i_float::adapter::FloatPointAdapter; use i_float::float::compatible::FloatPointCompatible; use i_float::float::number::FloatNumber; +use crate::segm::boolean::ShapeCountBoolean; trait OutlineBuild, T: FloatNumber> { fn build( &self, path: &[P], adapter: &FloatPointAdapter, - segments: &mut Vec>, + segments: &mut Vec>, ); fn capacity(&self, points_count: usize) -> usize; @@ -60,7 +60,7 @@ impl + 'static, T: FloatNumber + 'static> OutlineBuil &self, path: &[P], adapter: &FloatPointAdapter, - segments: &mut Vec>, + segments: &mut Vec>, ) { self.builder.build(path, adapter, segments); } @@ -84,7 +84,7 @@ impl, P: FloatPointCompatible, T: FloatNumber> OutlineBu &self, path: &[P], adapter: &FloatPointAdapter, - segments: &mut Vec>, + segments: &mut Vec>, ) { if path.len() < 2 { return; diff --git a/iOverlay/src/mesh/outline/builder_join.rs b/iOverlay/src/mesh/outline/builder_join.rs index 1835150..27c2737 100644 --- a/iOverlay/src/mesh/outline/builder_join.rs +++ b/iOverlay/src/mesh/outline/builder_join.rs @@ -1,7 +1,6 @@ use crate::mesh::miter::{Miter, SharpMiter}; use crate::mesh::outline::section::Section; use crate::mesh::rotator::Rotator; -use crate::segm::offset::ShapeCountOffset; use crate::segm::segment::Segment; use alloc::vec::Vec; use core::f64::consts::PI; @@ -9,6 +8,7 @@ use i_float::adapter::FloatPointAdapter; use i_float::float::compatible::FloatPointCompatible; use i_float::float::number::FloatNumber; use i_float::float::vector::FloatPointMath; +use crate::segm::boolean::ShapeCountBoolean; pub(super) trait JoinBuilder, T: FloatNumber> { fn add_join( @@ -16,7 +16,7 @@ pub(super) trait JoinBuilder, T: FloatNumber> { s0: &Section, s1: &Section, adapter: &FloatPointAdapter, - segments: &mut Vec>, + segments: &mut Vec>, ); fn capacity(&self) -> usize; fn additional_offset(&self, radius: T) -> T; @@ -30,25 +30,15 @@ impl BevelJoinBuilder { s0: &Section, s1: &Section, adapter: &FloatPointAdapter, - segments: &mut Vec>, + segments: &mut Vec>, ) { let b0 = adapter.float_to_int(&s0.b_top); let a1 = adapter.float_to_int(&s1.a_top); if b0 == a1 { return; } - let a0 = adapter.float_to_int(&s0.a_top); - let b1 = adapter.float_to_int(&s1.b_top); - let a0b0 = b0 - a0; - let a1b1 = b1 - a1; - let b0a1 = a1 - b0; - - let a0b0_x_a1b1 = a0b0.cross_product(a1b1); - let a0b0_x_b0a1 = a0b0.cross_product(b0a1); - let bold = (a0b0_x_a1b1 >= 0) == (a0b0_x_b0a1 >= 0); - - segments.push(Segment::subject_ab(b0, a1, bold)); + segments.push(Segment::subject_ab(b0, a1)); } } @@ -59,7 +49,7 @@ impl> JoinBuilder for BevelJoin s0: &Section, s1: &Section, adapter: &FloatPointAdapter, - segments: &mut Vec>, + segments: &mut Vec>, ) { Self::join(s0, s1, adapter, segments); } @@ -115,7 +105,7 @@ impl> JoinBuilder for MiterJoin s0: &Section, s1: &Section, adapter: &FloatPointAdapter, - segments: &mut Vec>, + segments: &mut Vec>, ) { let cross_product = FloatPointMath::cross_product(&s0.dir, &s1.dir); let turn = cross_product >= T::from_float(0.0); @@ -154,20 +144,20 @@ impl> JoinBuilder for MiterJoin let ibc = adapter.float_to_int(&bc); if ia != iac { - segments.push(Segment::bold_subject_ab(ia, iac)); + segments.push(Segment::subject_ab(ia, iac)); } if iac != ibc { - segments.push(Segment::bold_subject_ab(iac, ibc)); + segments.push(Segment::subject_ab(iac, ibc)); } if ibc != ib { - segments.push(Segment::bold_subject_ab(ibc, ib)); + segments.push(Segment::subject_ab(ibc, ib)); } } else { match Miter::sharp(pa, pb, s0.dir, s1.dir, adapter) { - SharpMiter::AB(a, b) => segments.push(Segment::bold_subject_ab(a, b)), + SharpMiter::AB(a, b) => segments.push(Segment::subject_ab(a, b)), SharpMiter::AcB(a, c, b) => { - segments.push(Segment::bold_subject_ab(a, c)); - segments.push(Segment::bold_subject_ab(c, b)); + segments.push(Segment::subject_ab(a, c)); + segments.push(Segment::subject_ab(c, b)); } SharpMiter::Degenerate => {} } @@ -222,7 +212,7 @@ impl> JoinBuilder for RoundJoin s0: &Section, s1: &Section, adapter: &FloatPointAdapter, - segments: &mut Vec>, + segments: &mut Vec>, ) { let cross_product = FloatPointMath::cross_product(&s0.dir, &s1.dir); let turn = cross_product >= T::from_float(0.0); @@ -257,14 +247,14 @@ impl> JoinBuilder for RoundJoin let b = adapter.float_to_int(&p); if a != b { - segments.push(Segment::bold_subject_ab(a, b)); + segments.push(Segment::subject_ab(a, b)); a = b; } } let b = adapter.float_to_int(&end); if a != b { - segments.push(Segment::bold_subject_ab(a, b)); + segments.push(Segment::subject_ab(a, b)); } } diff --git a/iOverlay/src/mesh/outline/offset.rs b/iOverlay/src/mesh/outline/offset.rs index 214a0a4..f16190a 100644 --- a/iOverlay/src/mesh/outline/offset.rs +++ b/iOverlay/src/mesh/outline/offset.rs @@ -1,12 +1,10 @@ -use crate::build::offset::{NegativeSubjectOffsetStrategy, PositiveSubjectOffsetStrategy}; use crate::core::extract::BooleanExtractionBuffer; use crate::core::fill_rule::FillRule; -use crate::core::overlay::{ContourDirection, Overlay, ShapeType}; +use crate::core::overlay::Overlay; use crate::core::overlay_rule::OverlayRule; use crate::float::overlay::OverlayOptions; use crate::float::scale::FixedScaleOverlayError; use crate::mesh::outline::builder::OutlineBuilder; -use crate::mesh::overlay::OffsetOverlay; use crate::mesh::style::OutlineStyle; use alloc::vec; use alloc::vec::Vec; @@ -177,7 +175,7 @@ impl + 'static, T: FloatNumber + 'static> OutlineSolv } fn build>(self, source: &S, options: OverlayOptions) -> Shapes

{ - let int_min_area = self.adapter.sqr_float_to_int(options.min_output_area).max(1); + let int_options = options.int_with_adapter(&self.adapter); let shapes = if self.paths_count <= 1 { // fast solution for a single path @@ -196,13 +194,10 @@ impl + 'static, T: FloatNumber + 'static> OutlineSolv let capacity = self.outer_builder.capacity(path.len()); let mut segments = Vec::with_capacity(capacity); self.outer_builder.build(path, &self.adapter, &mut segments); + let mut overlay = Overlay::with_segments(segments); + overlay.options = int_options; - OffsetOverlay::with_segments(segments) - .build_graph_view_with_solver::(Default::default()) - .map(|graph| { - graph.extract_offset(options.output_direction, int_min_area, &mut Default::default()) - }) - .unwrap_or_default() + overlay.overlay(OverlayRule::Subject, FillRule::Positive) } else { let total_capacity = self.outer_builder.capacity(self.points_count); let mut overlay = Overlay::new_custom( @@ -211,7 +206,9 @@ impl + 'static, T: FloatNumber + 'static> OutlineSolv Default::default(), ); - let mut offset_overlay = OffsetOverlay::new(128); + let mut offset_overlay = Overlay::new(16); + overlay.options = int_options; + let mut segments = Vec::new(); let mut extraction_buffer = BooleanExtractionBuffer::default(); @@ -224,7 +221,7 @@ impl + 'static, T: FloatNumber + 'static> OutlineSolv continue; } - let (offset_graph, direction) = if area < 0 { + if area < 0 { let capacity = self.outer_builder.capacity(path.len()); let additional = capacity.saturating_sub(segments.capacity()); if additional > 0 { @@ -237,9 +234,9 @@ impl + 'static, T: FloatNumber + 'static> OutlineSolv offset_overlay.clear(); offset_overlay.add_segments(&segments); - let graph = offset_overlay - .build_graph_view_with_solver::(Default::default()); - (graph, ContourDirection::CounterClockwise) + if let Some(graph) = offset_overlay.build_graph_view(FillRule::Positive) { + graph.extract_contours_into(OverlayRule::Subject, &mut extraction_buffer, &mut flat_buffer); + } } else { let capacity = self.inner_builder.capacity(path.len()); let additional = capacity.saturating_sub(segments.capacity()); @@ -253,14 +250,9 @@ impl + 'static, T: FloatNumber + 'static> OutlineSolv offset_overlay.clear(); offset_overlay.add_segments(&segments); - let graph = offset_overlay - .build_graph_view_with_solver::(Default::default()); - (graph, ContourDirection::Clockwise) - }; - - if let Some(graph) = offset_graph { - graph.extract_contours_into(direction, 0, &mut extraction_buffer, &mut flat_buffer); - overlay.add_flat_buffer(&flat_buffer, ShapeType::Subject); + if let Some(graph) = offset_overlay.build_graph_view(FillRule::Negative) { + graph.extract_contours_into(OverlayRule::Subject, &mut extraction_buffer, &mut flat_buffer); + } } } diff --git a/iOverlay/src/mesh/outline/section.rs b/iOverlay/src/mesh/outline/section.rs index 01eafd7..0c2602a 100644 --- a/iOverlay/src/mesh/outline/section.rs +++ b/iOverlay/src/mesh/outline/section.rs @@ -1,5 +1,4 @@ use crate::mesh::math::Math; -use crate::segm::offset::ShapeCountOffset; use crate::segm::segment::Segment; use alloc::vec::Vec; use core::marker::PhantomData; @@ -7,6 +6,7 @@ use i_float::adapter::FloatPointAdapter; use i_float::float::compatible::FloatPointCompatible; use i_float::float::number::FloatNumber; use i_float::float::vector::FloatPointMath; +use crate::segm::boolean::ShapeCountBoolean; #[derive(Debug, Clone)] pub(super) struct Section, T: FloatNumber> { @@ -39,12 +39,12 @@ pub(crate) trait SectionToSegment> { fn add_section(&mut self, section: &Section, adapter: &FloatPointAdapter); } -impl> SectionToSegment for Vec> { +impl> SectionToSegment for Vec> { fn add_section(&mut self, section: &Section, adapter: &FloatPointAdapter) { let a_top = adapter.float_to_int(§ion.a_top); let b_top = adapter.float_to_int(§ion.b_top); if a_top != b_top { - self.push(Segment::bold_subject_ab(a_top, b_top)); + self.push(Segment::subject_ab(a_top, b_top)); } } } diff --git a/iOverlay/src/mesh/overlay.rs b/iOverlay/src/mesh/overlay.rs index bc31e7f..4349f81 100644 --- a/iOverlay/src/mesh/overlay.rs +++ b/iOverlay/src/mesh/overlay.rs @@ -1,59 +1,26 @@ use crate::build::builder::GraphBuilder; -use crate::build::sweep::FillStrategy; use crate::core::graph::OverlayNode; -use crate::core::solver::Solver; -use crate::mesh::graph::OffsetGraph; -use crate::segm::offset::ShapeCountOffset; use crate::segm::segment::Segment; use crate::split::solver::SplitSolver; use alloc::vec::Vec; +use crate::core::overlay::Overlay; +use crate::segm::boolean::ShapeCountBoolean; -pub struct OffsetOverlay { - pub(super) segments: Vec>, - pub(crate) split_solver: SplitSolver, - pub(crate) graph_builder: GraphBuilder, -} - -impl OffsetOverlay { - #[inline] - pub fn new(capacity: usize) -> Self { - Self { - segments: Vec::with_capacity(capacity), - split_solver: SplitSolver::new(), - graph_builder: GraphBuilder::::new(), - } - } - +impl Overlay { #[inline] - pub fn clear(&mut self) { - self.segments.clear(); - } - - #[inline] - pub fn add_segments(&mut self, segments: &[Segment]) { + pub(crate) fn add_segments(&mut self, segments: &[Segment]) { self.segments.extend_from_slice(segments); } #[inline] - pub fn with_segments(segments: Vec>) -> Self { + pub(crate) fn with_segments(segments: Vec>) -> Self { Self { + solver: Default::default(), + options: Default::default(), + boolean_buffer: None, segments, split_solver: SplitSolver::new(), - graph_builder: GraphBuilder::::new(), - } - } - - #[inline] - pub fn build_graph_view_with_solver>( - &mut self, - solver: Solver, - ) -> Option> { - self.split_solver.split_segments(&mut self.segments, &solver); - if self.segments.is_empty() { - return None; + graph_builder: GraphBuilder::::new(), } - let graph = self.graph_builder.build_offset::(&solver, &self.segments); - - Some(graph) } } diff --git a/iOverlay/src/mesh/stroke/builder.rs b/iOverlay/src/mesh/stroke/builder.rs index b0f2535..dffe9e0 100644 --- a/iOverlay/src/mesh/stroke/builder.rs +++ b/iOverlay/src/mesh/stroke/builder.rs @@ -2,13 +2,13 @@ use crate::mesh::stroke::builder_cap::CapBuilder; use crate::mesh::stroke::builder_join::{BevelJoinBuilder, JoinBuilder, MiterJoinBuilder, RoundJoinBuilder}; use crate::mesh::stroke::section::{Section, SectionToSegment}; use crate::mesh::style::{LineJoin, StrokeStyle}; -use crate::segm::offset::ShapeCountOffset; use crate::segm::segment::Segment; use alloc::boxed::Box; use alloc::vec::Vec; use i_float::adapter::FloatPointAdapter; use i_float::float::compatible::FloatPointCompatible; use i_float::float::number::FloatNumber; +use crate::segm::boolean::ShapeCountBoolean; trait StrokeBuild, T: FloatNumber> { fn build( @@ -16,7 +16,7 @@ trait StrokeBuild, T: FloatNumber> { path: &[P], is_closed_path: bool, adapter: &FloatPointAdapter, - segments: &mut Vec>, + segments: &mut Vec>, ); fn capacity(&self, paths_count: usize, points_count: usize, is_closed_path: bool) -> usize; @@ -71,7 +71,7 @@ impl + 'static, T: FloatNumber + 'static> StrokeBuild path: &[P], is_closed_path: bool, adapter: &FloatPointAdapter, - segments: &mut Vec>, + segments: &mut Vec>, ) { self.builder.build(path, is_closed_path, adapter, segments); } @@ -96,7 +96,7 @@ impl, P: FloatPointCompatible, T: FloatNumber> StrokeBui path: &[P], is_closed_path: bool, adapter: &FloatPointAdapter, - segments: &mut Vec>, + segments: &mut Vec>, ) { if is_closed_path { self.closed_segments(path, adapter, segments); @@ -129,7 +129,7 @@ impl, P: FloatPointCompatible, T: FloatNumber> Builder, - segments: &mut Vec>, + segments: &mut Vec>, ) { // build segments only from points which are not equal in int space @@ -182,7 +182,7 @@ impl, P: FloatPointCompatible, T: FloatNumber> Builder, - segments: &mut Vec>, + segments: &mut Vec>, ) { if path.len() < 2 { return; diff --git a/iOverlay/src/mesh/stroke/builder_cap.rs b/iOverlay/src/mesh/stroke/builder_cap.rs index ada2081..10508fd 100644 --- a/iOverlay/src/mesh/stroke/builder_cap.rs +++ b/iOverlay/src/mesh/stroke/builder_cap.rs @@ -1,7 +1,6 @@ use crate::mesh::rotator::Rotator; use crate::mesh::stroke::section::Section; use crate::mesh::style::LineCap; -use crate::segm::offset::ShapeCountOffset; use crate::segm::segment::Segment; use alloc::vec; use alloc::vec::Vec; @@ -12,6 +11,7 @@ use i_float::float::compatible::FloatPointCompatible; use i_float::float::number::FloatNumber; use i_float::float::rect::FloatRect; use i_float::float::vector::FloatPointMath; +use crate::segm::boolean::ShapeCountBoolean; #[derive(Debug, Clone)] pub(super) struct CapBuilder { @@ -75,7 +75,7 @@ impl> CapBuilder { &self, section: &Section, adapter: &FloatPointAdapter, - segments: &mut Vec>, + segments: &mut Vec>, ) { let mut a = adapter.float_to_int(§ion.a_top); if let Some(points) = &self.points { @@ -85,19 +85,19 @@ impl> CapBuilder { let r = rotator.rotate(p); let q = FloatPointMath::add(&r, §ion.a); let b = adapter.float_to_int(&q); - segments.push(Segment::bold_subject_ab(a, b)); + segments.push(Segment::subject_ab(a, b)); a = b; } } let last = adapter.float_to_int(§ion.a_bot); - segments.push(Segment::bold_subject_ab(a, last)); + segments.push(Segment::subject_ab(a, last)); } pub(super) fn add_to_end( &self, section: &Section, adapter: &FloatPointAdapter, - segments: &mut Vec>, + segments: &mut Vec>, ) { let mut a = adapter.float_to_int(§ion.b_bot); if let Some(points) = &self.points { @@ -106,12 +106,12 @@ impl> CapBuilder { let r = rotator.rotate(p); let q = FloatPointMath::add(&r, §ion.b); let b = adapter.float_to_int(&q); - segments.push(Segment::bold_subject_ab(a, b)); + segments.push(Segment::subject_ab(a, b)); a = b; } } let last = adapter.float_to_int(§ion.b_top); - segments.push(Segment::bold_subject_ab(a, last)); + segments.push(Segment::subject_ab(a, last)); } #[inline] diff --git a/iOverlay/src/mesh/stroke/builder_join.rs b/iOverlay/src/mesh/stroke/builder_join.rs index 33760d9..c9ec853 100644 --- a/iOverlay/src/mesh/stroke/builder_join.rs +++ b/iOverlay/src/mesh/stroke/builder_join.rs @@ -1,7 +1,6 @@ use crate::mesh::miter::{Miter, SharpMiter}; use crate::mesh::rotator::Rotator; use crate::mesh::stroke::section::Section; -use crate::segm::offset::ShapeCountOffset; use crate::segm::segment::Segment; use alloc::vec::Vec; use core::f64::consts::PI; @@ -9,6 +8,7 @@ use i_float::adapter::FloatPointAdapter; use i_float::float::compatible::FloatPointCompatible; use i_float::float::number::FloatNumber; use i_float::float::vector::FloatPointMath; +use crate::segm::boolean::ShapeCountBoolean; pub(super) trait JoinBuilder, T: FloatNumber> { fn add_join( @@ -16,7 +16,7 @@ pub(super) trait JoinBuilder, T: FloatNumber> { s0: &Section, s1: &Section, adapter: &FloatPointAdapter, - segments: &mut Vec>, + segments: &mut Vec>, ); fn capacity(&self) -> usize; fn additional_offset(&self, radius: T) -> T; @@ -30,7 +30,7 @@ impl BevelJoinBuilder { s0: &Section, s1: &Section, adapter: &FloatPointAdapter, - segments: &mut Vec>, + segments: &mut Vec>, ) { Self::add_segment(&s0.b_top, &s1.a_top, adapter, segments); } @@ -40,7 +40,7 @@ impl BevelJoinBuilder { s0: &Section, s1: &Section, adapter: &FloatPointAdapter, - segments: &mut Vec>, + segments: &mut Vec>, ) { Self::add_segment(&s1.a_bot, &s0.b_bot, adapter, segments); } @@ -50,12 +50,12 @@ impl BevelJoinBuilder { a: &P, b: &P, adapter: &FloatPointAdapter, - segments: &mut Vec>, + segments: &mut Vec>, ) { let ia = adapter.float_to_int(a); let ib = adapter.float_to_int(b); if ia != ib { - segments.push(Segment::bold_subject_ab(ib, ia)); + segments.push(Segment::subject_ab(ib, ia)); } } } @@ -67,7 +67,7 @@ impl> JoinBuilder for BevelJoin s0: &Section, s1: &Section, adapter: &FloatPointAdapter, - segments: &mut Vec>, + segments: &mut Vec>, ) { Self::join_top(s0, s1, adapter, segments); Self::join_bot(s0, s1, adapter, segments); @@ -123,7 +123,7 @@ impl> JoinBuilder for MiterJoin s0: &Section, s1: &Section, adapter: &FloatPointAdapter, - segments: &mut Vec>, + segments: &mut Vec>, ) { let cross_product = FloatPointMath::cross_product(&s0.dir, &s1.dir); if cross_product.abs() < T::from_float(0.0001) { @@ -178,13 +178,13 @@ impl> JoinBuilder for MiterJoin let ibc = adapter.float_to_int(&bc); if ia != iac { - segments.push(Segment::bold_subject_ab(iac, ia)); + segments.push(Segment::subject_ab(iac, ia)); } if iac != ibc { - segments.push(Segment::bold_subject_ab(ibc, iac)); + segments.push(Segment::subject_ab(ibc, iac)); } if ibc != ib { - segments.push(Segment::bold_subject_ab(ib, ibc)); + segments.push(Segment::subject_ab(ib, ibc)); } } else { let (pa, pb, va, vb) = if turn { @@ -195,10 +195,10 @@ impl> JoinBuilder for MiterJoin (s0.b_top, s1.a_top, s0.dir, s1.dir) }; match Miter::sharp(pa, pb, va, vb, adapter) { - SharpMiter::AB(a, b) => segments.push(Segment::bold_subject_ab(b, a)), + SharpMiter::AB(a, b) => segments.push(Segment::subject_ab(b, a)), SharpMiter::AcB(a, c, b) => { - segments.push(Segment::bold_subject_ab(c, a)); - segments.push(Segment::bold_subject_ab(b, c)); + segments.push(Segment::subject_ab(c, a)); + segments.push(Segment::subject_ab(b, c)); } SharpMiter::Degenerate => {} } @@ -243,7 +243,7 @@ impl> JoinBuilder for RoundJoin s0: &Section, s1: &Section, adapter: &FloatPointAdapter, - segments: &mut Vec>, + segments: &mut Vec>, ) { let dot_product = FloatPointMath::dot_product(&s0.dir, &s1.dir); if self.limit_dot_product < dot_product { @@ -277,14 +277,14 @@ impl> JoinBuilder for RoundJoin let b = adapter.float_to_int(&p); if a != b { - segments.push(Segment::bold_subject_ab(b, a)); + segments.push(Segment::subject_ab(b, a)); a = b; } } let b = adapter.float_to_int(&end); if a != b { - segments.push(Segment::bold_subject_ab(b, a)); + segments.push(Segment::subject_ab(b, a)); } } diff --git a/iOverlay/src/mesh/stroke/offset.rs b/iOverlay/src/mesh/stroke/offset.rs index a0f7538..f4258d5 100644 --- a/iOverlay/src/mesh/stroke/offset.rs +++ b/iOverlay/src/mesh/stroke/offset.rs @@ -1,8 +1,6 @@ -use crate::build::offset::PositiveSubjectOffsetStrategy; use crate::float::overlay::OverlayOptions; use crate::float::scale::FixedScaleOverlayError; use crate::i_shape::source::resource::ShapeResource; -use crate::mesh::overlay::OffsetOverlay; use crate::mesh::stroke::builder::StrokeBuilder; use crate::mesh::stroke::offset::vec::Vec; use crate::mesh::style::StrokeStyle; @@ -15,6 +13,9 @@ use i_shape::base::data::Shapes; use i_shape::float::adapter::ShapesToFloat; use i_shape::float::despike::DeSpikeContour; use i_shape::float::simple::SimplifyContour; +use crate::core::fill_rule::FillRule; +use crate::core::overlay::Overlay; +use crate::core::overlay_rule::OverlayRule; pub trait StrokeOffset, T: FloatNumber> { /// Generates a stroke shapes for paths, contours, or shapes. @@ -201,11 +202,10 @@ impl, T: 'static + FloatNumber> StrokeSolve .build(path, is_closed_path, &self.adapter, &mut segments); } - let min_area = self.adapter.sqr_float_to_int(options.min_output_area); - let shapes = OffsetOverlay::with_segments(segments) - .build_graph_view_with_solver::(Default::default()) - .map(|graph| graph.extract_offset(options.output_direction, min_area, &mut Default::default())) - .unwrap_or_default(); + let mut overlay = Overlay::with_segments(segments); + overlay.options = options.int_with_adapter(&self.adapter); + + let shapes = overlay.overlay(OverlayRule::Subject, FillRule::Positive); let mut float = shapes.to_float(&self.adapter); diff --git a/iOverlay/src/mesh/stroke/section.rs b/iOverlay/src/mesh/stroke/section.rs index acd7957..176d1df 100644 --- a/iOverlay/src/mesh/stroke/section.rs +++ b/iOverlay/src/mesh/stroke/section.rs @@ -1,5 +1,4 @@ use crate::mesh::math::Math; -use crate::segm::offset::ShapeCountOffset; use crate::segm::segment::Segment; use alloc::vec::Vec; use core::marker::PhantomData; @@ -7,6 +6,7 @@ use i_float::adapter::FloatPointAdapter; use i_float::float::compatible::FloatPointCompatible; use i_float::float::number::FloatNumber; use i_float::float::vector::FloatPointMath; +use crate::segm::boolean::ShapeCountBoolean; #[derive(Debug, Clone)] pub(super) struct Section, T: FloatNumber> { @@ -48,7 +48,7 @@ pub(crate) trait SectionToSegment> { fn add_section(&mut self, section: &Section, adapter: &FloatPointAdapter); } -impl> SectionToSegment for Vec> { +impl> SectionToSegment for Vec> { fn add_section(&mut self, section: &Section, adapter: &FloatPointAdapter) { let a_top = adapter.float_to_int(§ion.a_top); let b_top = adapter.float_to_int(§ion.b_top); @@ -56,10 +56,10 @@ impl> SectionToSegment for Vec< let b_bot = adapter.float_to_int(§ion.b_bot); if a_top != b_top { - self.push(Segment::bold_subject_ab(b_top, a_top)); + self.push(Segment::subject_ab(b_top, a_top)); } if a_bot != b_bot { - self.push(Segment::bold_subject_ab(a_bot, b_bot)); + self.push(Segment::subject_ab(a_bot, b_bot)); } } } diff --git a/iOverlay/src/mesh/subject.rs b/iOverlay/src/mesh/subject.rs index 863e3aa..2821ad3 100644 --- a/iOverlay/src/mesh/subject.rs +++ b/iOverlay/src/mesh/subject.rs @@ -1,35 +1,20 @@ use crate::geom::x_segment::XSegment; -use crate::segm::offset::ShapeCountOffset; use crate::segm::segment::Segment; use i_float::int::point::IntPoint; +use crate::segm::boolean::ShapeCountBoolean; -impl Segment { +impl Segment { #[inline] - pub(crate) fn bold_subject_ab(p0: IntPoint, p1: IntPoint) -> Self { + pub(crate) fn subject_ab(p0: IntPoint, p1: IntPoint) -> Self { if p0 < p1 { Self { x_segment: XSegment { a: p0, b: p1 }, - count: ShapeCountOffset { subj: 1, bold: true }, + count: ShapeCountBoolean { subj: 1, clip: 0 }, } } else { Self { x_segment: XSegment { a: p1, b: p0 }, - count: ShapeCountOffset { subj: -1, bold: true }, - } - } - } - - #[inline] - pub(crate) fn subject_ab(p0: IntPoint, p1: IntPoint, bold: bool) -> Self { - if p0 < p1 { - Self { - x_segment: XSegment { a: p0, b: p1 }, - count: ShapeCountOffset { subj: 1, bold }, - } - } else { - Self { - x_segment: XSegment { a: p1, b: p0 }, - count: ShapeCountOffset { subj: -1, bold }, + count: ShapeCountBoolean { subj: -1, clip: 0 }, } } } diff --git a/iOverlay/src/segm/offset.rs b/iOverlay/src/segm/offset.rs index 9b82ad7..dbc96c6 100644 --- a/iOverlay/src/segm/offset.rs +++ b/iOverlay/src/segm/offset.rs @@ -1,50 +1,50 @@ -use crate::core::overlay::ShapeType; -use crate::segm::winding::WindingCount; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct ShapeCountOffset { - pub(crate) subj: i32, - pub(crate) bold: bool, -} - -impl WindingCount for ShapeCountOffset { - #[inline(always)] - fn is_not_empty(&self) -> bool { - self.subj != 0 - } - - #[inline(always)] - fn new(subj: i32, _: i32) -> Self { - Self { subj, bold: true } - } - - #[inline(always)] - fn with_shape_type(shape_type: ShapeType) -> (Self, Self) { - match shape_type { - ShapeType::Subject => (Self { subj: 1, bold: true }, Self { subj: -1, bold: true }), - ShapeType::Clip => (Self { subj: 0, bold: true }, Self { subj: 0, bold: true }), - } - } - - #[inline(always)] - fn add(self, count: Self) -> Self { - let subj = self.subj + count.subj; - let bold = self.bold || count.bold; - Self { subj, bold } - } - - #[inline(always)] - fn apply(&mut self, count: Self) { - self.subj += count.subj; - self.bold = self.bold || count.bold; - } - - #[inline(always)] - fn invert(self) -> Self { - let subj = -self.subj; - Self { - subj, - bold: self.bold, - } - } -} +// use crate::core::overlay::ShapeType; +// use crate::segm::winding::WindingCount; +// +// #[derive(Debug, Clone, Copy, PartialEq, Eq)] +// pub struct ShapeCountOffset { +// pub(crate) subj: i32, +// pub(crate) bold: bool, +// } +// +// impl WindingCount for ShapeCountOffset { +// #[inline(always)] +// fn is_not_empty(&self) -> bool { +// self.subj != 0 +// } +// +// #[inline(always)] +// fn new(subj: i32, _: i32) -> Self { +// Self { subj, bold: true } +// } +// +// #[inline(always)] +// fn with_shape_type(shape_type: ShapeType) -> (Self, Self) { +// match shape_type { +// ShapeType::Subject => (Self { subj: 1, bold: true }, Self { subj: -1, bold: true }), +// ShapeType::Clip => (Self { subj: 0, bold: true }, Self { subj: 0, bold: true }), +// } +// } +// +// #[inline(always)] +// fn add(self, count: Self) -> Self { +// let subj = self.subj + count.subj; +// let bold = self.bold || count.bold; +// Self { subj, bold } +// } +// +// #[inline(always)] +// fn apply(&mut self, count: Self) { +// self.subj += count.subj; +// self.bold = self.bold || count.bold; +// } +// +// #[inline(always)] +// fn invert(self) -> Self { +// let subj = -self.subj; +// Self { +// subj, +// bold: self.bold, +// } +// } +// } From 67674242f3180abc3ef03bd320be90fcf607279d Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Tue, 3 Mar 2026 18:00:27 +0300 Subject: [PATCH 02/10] clippy --- iOverlay/src/core/extract.rs | 10 ---------- iOverlay/src/mesh/graph.rs | 7 ------- iOverlay/src/mesh/mod.rs | 1 - 3 files changed, 18 deletions(-) delete mode 100644 iOverlay/src/mesh/graph.rs diff --git a/iOverlay/src/core/extract.rs b/iOverlay/src/core/extract.rs index 5e343f4..20870ef 100644 --- a/iOverlay/src/core/extract.rs +++ b/iOverlay/src/core/extract.rs @@ -346,7 +346,6 @@ impl VisitState { pub(crate) trait Visit { fn is_visited(&self, index: usize) -> bool; fn is_not_visited(&self, index: usize) -> bool; - fn visit(&mut self, index: usize); fn visit_edge(&mut self, index: usize, state: VisitState); } @@ -369,15 +368,6 @@ impl Visit for [VisitState] { *self.get_unchecked(index) == VisitState::Unvisited } } - - #[inline(always)] - fn visit(&mut self, index: usize) { - unsafe { - // SAFETY: callers only pass indices derived from the visited slice itself, so index < len. - *self.get_unchecked_mut(index) = VisitState::Skipped; - } - } - #[inline(always)] fn visit_edge(&mut self, index: usize, state: VisitState) { unsafe { diff --git a/iOverlay/src/mesh/graph.rs b/iOverlay/src/mesh/graph.rs deleted file mode 100644 index 169ceda..0000000 --- a/iOverlay/src/mesh/graph.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::core::graph::OverlayNode; -use crate::core::link::OverlayLink; - -pub struct OffsetGraph<'a> { - pub(crate) nodes: &'a [OverlayNode], - pub(crate) links: &'a [OverlayLink], -} diff --git a/iOverlay/src/mesh/mod.rs b/iOverlay/src/mesh/mod.rs index 7a1be86..e56f229 100644 --- a/iOverlay/src/mesh/mod.rs +++ b/iOverlay/src/mesh/mod.rs @@ -1,4 +1,3 @@ -pub(crate) mod graph; pub(crate) mod math; mod miter; pub mod outline; From 4032597e82b2d2f0f7f8a72bb917011303990dd5 Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Sun, 8 Mar 2026 12:49:03 +0300 Subject: [PATCH 03/10] pass outline tests --- iOverlay/src/mesh/outline/builder.rs | 11 +++ iOverlay/src/mesh/outline/offset.rs | 141 ++++++++++++++------------- 2 files changed, 83 insertions(+), 69 deletions(-) diff --git a/iOverlay/src/mesh/outline/builder.rs b/iOverlay/src/mesh/outline/builder.rs index a65bf9d..e61a075 100644 --- a/iOverlay/src/mesh/outline/builder.rs +++ b/iOverlay/src/mesh/outline/builder.rs @@ -20,6 +20,8 @@ trait OutlineBuild, T: FloatNumber> { fn capacity(&self, points_count: usize) -> usize; fn additional_offset(&self, radius: T) -> T; + + fn radius(&self) -> T; } pub(super) struct OutlineBuilder, T: FloatNumber> { @@ -74,6 +76,11 @@ impl + 'static, T: FloatNumber + 'static> OutlineBuil pub(super) fn additional_offset(&self, radius: T) -> T { self.builder.additional_offset(radius) } + + #[inline] + pub(super) fn is_shrink(&self) -> bool { + self.builder.radius() > T::from_float(0.0) + } } impl, P: FloatPointCompatible, T: FloatNumber> OutlineBuild @@ -124,6 +131,10 @@ impl, P: FloatPointCompatible, T: FloatNumber> OutlineBu fn additional_offset(&self, radius: T) -> T { self.join_builder.additional_offset(radius) } + + fn radius(&self) -> T { + self.radius + } } impl, P: FloatPointCompatible, T: FloatNumber> Builder { diff --git a/iOverlay/src/mesh/outline/offset.rs b/iOverlay/src/mesh/outline/offset.rs index f16190a..359c5d4 100644 --- a/iOverlay/src/mesh/outline/offset.rs +++ b/iOverlay/src/mesh/outline/offset.rs @@ -1,6 +1,7 @@ use crate::core::extract::BooleanExtractionBuffer; use crate::core::fill_rule::FillRule; -use crate::core::overlay::Overlay; +use crate::core::overlay::ShapeType::{Clip, Subject}; +use crate::core::overlay::{ContourDirection, Overlay}; use crate::core::overlay_rule::OverlayRule; use crate::float::overlay::OverlayOptions; use crate::float::scale::FixedScaleOverlayError; @@ -19,6 +20,7 @@ use i_shape::float::despike::DeSpikeContour; use i_shape::float::int_area::IntArea; use i_shape::float::simple::SimplifyContour; use i_shape::source::resource::ShapeResource; +use crate::segm::winding::WindingCount; pub trait OutlineOffset, T: FloatNumber> { /// Generates an outline shapes for contours, or shapes. @@ -117,7 +119,6 @@ struct OutlineSolver, T: FloatNumber> { inner_builder: OutlineBuilder, adapter: FloatPointAdapter, points_count: usize, - paths_count: usize, } impl + 'static, T: FloatNumber + 'static> OutlineSolver { @@ -158,7 +159,6 @@ impl + 'static, T: FloatNumber + 'static> OutlineSolv inner_builder, adapter, points_count, - paths_count, }) } @@ -175,89 +175,92 @@ impl + 'static, T: FloatNumber + 'static> OutlineSolv } fn build>(self, source: &S, options: OverlayOptions) -> Shapes

{ - let int_options = options.int_with_adapter(&self.adapter); + let total_capacity = self.outer_builder.capacity(self.points_count); + let mut overlay = Overlay::new_custom( + total_capacity, + options.int_with_adapter(&self.adapter), + Default::default(), + ); - let shapes = if self.paths_count <= 1 { - // fast solution for a single path - let path = if let Some(first) = source.iter_paths().next() { - first - } else { - return vec![]; - }; + let mut offset_overlay = Overlay::new(16); + offset_overlay.options = overlay.options; + + let mut segments = Vec::new(); + let mut bool_buffer = BooleanExtractionBuffer::default(); + let mut flat_buffer = FlatContoursBuffer::default(); + for path in source.iter_paths() { let area = path.unsafe_int_area(&self.adapter); - if area >= -1 { - // single path must be clock-wised - return vec![]; + if area.abs() <= 1 { + // ignore degenerate paths + continue; } - let capacity = self.outer_builder.capacity(path.len()); - let mut segments = Vec::with_capacity(capacity); - self.outer_builder.build(path, &self.adapter, &mut segments); - let mut overlay = Overlay::with_segments(segments); - overlay.options = int_options; - - overlay.overlay(OverlayRule::Subject, FillRule::Positive) - } else { - let total_capacity = self.outer_builder.capacity(self.points_count); - let mut overlay = Overlay::new_custom( - total_capacity, - options.int_with_adapter(&self.adapter), - Default::default(), - ); + offset_overlay.clear(); + segments.clear(); - let mut offset_overlay = Overlay::new(16); - overlay.options = int_options; + if area < 0 { + offset_overlay.options.output_direction = ContourDirection::CounterClockwise; + segments.reserve(self.outer_builder.capacity(path.len())); + self.outer_builder.build(path, &self.adapter, &mut segments); + offset_overlay.add_segments(&segments); - let mut segments = Vec::new(); - let mut extraction_buffer = BooleanExtractionBuffer::default(); - let mut flat_buffer = FlatContoursBuffer::default(); - - for path in source.iter_paths() { - let area = path.unsafe_int_area(&self.adapter); - if area.abs() <= 1 { - // ignore degenerate paths - continue; + if let Some(graph) = offset_overlay.build_graph_view(FillRule::Positive) { + graph.extract_contours_into( + OverlayRule::Subject, + &mut bool_buffer, + &mut flat_buffer, + ); } - if area < 0 { - let capacity = self.outer_builder.capacity(path.len()); - let additional = capacity.saturating_sub(segments.capacity()); - if additional > 0 { - segments.reserve(additional); - } - segments.clear(); - - self.outer_builder.build(path, &self.adapter, &mut segments); - + if self.outer_builder.is_shrink() { offset_overlay.clear(); - offset_overlay.add_segments(&segments); + offset_overlay.add_path_iter(path.iter().map(|p| self.adapter.float_to_int(p)), Subject); + offset_overlay.add_flat_buffer(&flat_buffer, Clip); if let Some(graph) = offset_overlay.build_graph_view(FillRule::Positive) { - graph.extract_contours_into(OverlayRule::Subject, &mut extraction_buffer, &mut flat_buffer); - } - } else { - let capacity = self.inner_builder.capacity(path.len()); - let additional = capacity.saturating_sub(segments.capacity()); - if additional > 0 { - segments.reserve(additional); + graph.extract_contours_into( + OverlayRule::Difference, + &mut bool_buffer, + &mut flat_buffer, + ); } - segments.clear(); - - self.inner_builder.build(path, &self.adapter, &mut segments); + } + } else { + offset_overlay.options.output_direction = ContourDirection::Clockwise; + segments.reserve(self.inner_builder.capacity(path.len())); + self.inner_builder.build(path, &self.adapter, &mut segments); + + offset_overlay.add_segments(&segments); + + if let Some(graph) = offset_overlay.build_graph_view(FillRule::Negative) { + graph.extract_contours_into( + OverlayRule::Subject, + &mut bool_buffer, + &mut flat_buffer, + ); + } + if !self.inner_builder.is_shrink() { offset_overlay.clear(); - offset_overlay.add_segments(&segments); + offset_overlay.add_path_iter(path.iter().map(|p| self.adapter.float_to_int(p)), Subject); + offset_overlay.add_flat_buffer(&flat_buffer, Clip); if let Some(graph) = offset_overlay.build_graph_view(FillRule::Negative) { - graph.extract_contours_into(OverlayRule::Subject, &mut extraction_buffer, &mut flat_buffer); + graph.extract_contours_into( + OverlayRule::Intersect, + &mut bool_buffer, + &mut flat_buffer, + ); } } } - overlay.overlay(OverlayRule::Subject, FillRule::Positive) - }; + overlay.add_flat_buffer(&flat_buffer, Subject); + } + + let shapes = overlay.overlay(OverlayRule::Subject, FillRule::Positive); if options.clean_result { let mut float = shapes.to_float(&self.adapter); @@ -362,7 +365,7 @@ mod tests { let angle = PI / 3.0f32; let style = OutlineStyle::new(10.0).line_join(LineJoin::Round(angle)); - let shapes = path.outline(&style); + let shapes = path.outline_fixed_scale(&style, 10.0).unwrap(); assert_eq!(shapes.len(), 1); } @@ -372,7 +375,7 @@ mod tests { let path = [[-5.0, -5.0f32], [5.0, -5.0], [5.0, 5.0], [-5.0, 5.0]]; let style = OutlineStyle::new(-20.0); - let shapes = path.outline(&style); + let shapes = path.outline_fixed_scale(&style, 10.0).unwrap(); assert_eq!(shapes.len(), 0); } @@ -384,7 +387,7 @@ mod tests { let angle = PI / 3.0f32; let style = OutlineStyle::new(-20.0).line_join(LineJoin::Round(angle)); - let shapes = path.outline(&style); + let shapes = path.outline_fixed_scale(&style, 10.0).unwrap(); assert_eq!(shapes.len(), 0); } @@ -394,7 +397,7 @@ mod tests { let path = [[-10.0, 0.0], [0.0, -10.0], [10.0, 0.0], [0.0, 10.0]]; let style = OutlineStyle::new(5.0).line_join(LineJoin::Miter(0.01)); - let shapes = path.outline(&style); + let shapes = path.outline_fixed_scale(&style, 10.0).unwrap(); assert_eq!(shapes.len(), 1); assert_eq!(shapes.first().unwrap().len(), 1); @@ -428,7 +431,7 @@ mod tests { let style = OutlineStyle::default().outer_offset(50.0).inner_offset(50.0); - let shapes = shape.outline(&style); + let shapes = shape.outline_fixed_scale(&style, 10.0).unwrap(); assert_eq!(shapes.len(), 1); From 667429fc41c8696f7e06c199639a5f3c8e6828af Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Sun, 8 Mar 2026 13:02:00 +0300 Subject: [PATCH 04/10] code clean --- iOverlay/src/mesh/outline/offset.rs | 1 - iOverlay/src/segm/mod.rs | 1 - iOverlay/src/segm/offset.rs | 50 ----------------------------- 3 files changed, 52 deletions(-) delete mode 100644 iOverlay/src/segm/offset.rs diff --git a/iOverlay/src/mesh/outline/offset.rs b/iOverlay/src/mesh/outline/offset.rs index 359c5d4..90d7cdf 100644 --- a/iOverlay/src/mesh/outline/offset.rs +++ b/iOverlay/src/mesh/outline/offset.rs @@ -20,7 +20,6 @@ use i_shape::float::despike::DeSpikeContour; use i_shape::float::int_area::IntArea; use i_shape::float::simple::SimplifyContour; use i_shape::source::resource::ShapeResource; -use crate::segm::winding::WindingCount; pub trait OutlineOffset, T: FloatNumber> { /// Generates an outline shapes for contours, or shapes. diff --git a/iOverlay/src/segm/mod.rs b/iOverlay/src/segm/mod.rs index 857ffd7..5f58287 100644 --- a/iOverlay/src/segm/mod.rs +++ b/iOverlay/src/segm/mod.rs @@ -1,7 +1,6 @@ pub mod boolean; pub(crate) mod build; pub(crate) mod merge; -pub mod offset; pub(crate) mod segment; pub(crate) mod sort; pub mod string; diff --git a/iOverlay/src/segm/offset.rs b/iOverlay/src/segm/offset.rs deleted file mode 100644 index dbc96c6..0000000 --- a/iOverlay/src/segm/offset.rs +++ /dev/null @@ -1,50 +0,0 @@ -// use crate::core::overlay::ShapeType; -// use crate::segm::winding::WindingCount; -// -// #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// pub struct ShapeCountOffset { -// pub(crate) subj: i32, -// pub(crate) bold: bool, -// } -// -// impl WindingCount for ShapeCountOffset { -// #[inline(always)] -// fn is_not_empty(&self) -> bool { -// self.subj != 0 -// } -// -// #[inline(always)] -// fn new(subj: i32, _: i32) -> Self { -// Self { subj, bold: true } -// } -// -// #[inline(always)] -// fn with_shape_type(shape_type: ShapeType) -> (Self, Self) { -// match shape_type { -// ShapeType::Subject => (Self { subj: 1, bold: true }, Self { subj: -1, bold: true }), -// ShapeType::Clip => (Self { subj: 0, bold: true }, Self { subj: 0, bold: true }), -// } -// } -// -// #[inline(always)] -// fn add(self, count: Self) -> Self { -// let subj = self.subj + count.subj; -// let bold = self.bold || count.bold; -// Self { subj, bold } -// } -// -// #[inline(always)] -// fn apply(&mut self, count: Self) { -// self.subj += count.subj; -// self.bold = self.bold || count.bold; -// } -// -// #[inline(always)] -// fn invert(self) -> Self { -// let subj = -self.subj; -// Self { -// subj, -// bold: self.bold, -// } -// } -// } From 53d713b1371cd0fb6590860f6f03b84cfaba6c75 Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Fri, 20 Mar 2026 13:02:33 +0300 Subject: [PATCH 05/10] switch to int --- iOverlay/src/mesh/outline/builder.rs | 297 +++++++++++++++--- iOverlay/src/mesh/outline/builder_join.rs | 260 +-------------- .../src/mesh/outline/builder_join_expand.rs | 258 +++++++++++++++ .../src/mesh/outline/builder_join_shrink.rs | 258 +++++++++++++++ iOverlay/src/mesh/outline/mod.rs | 5 +- iOverlay/src/mesh/outline/offset.rs | 103 ++++-- iOverlay/src/mesh/outline/section.rs | 76 ++++- iOverlay/src/mesh/outline/uniq_iter.rs | 239 ++++++++++++++ iOverlay/src/mesh/stroke/builder_cap.rs | 8 +- iOverlay/src/mesh/stroke/builder_join.rs | 18 +- iOverlay/src/mesh/stroke/section.rs | 4 +- iOverlay/src/mesh/subject.rs | 2 +- 12 files changed, 1177 insertions(+), 351 deletions(-) create mode 100644 iOverlay/src/mesh/outline/builder_join_expand.rs create mode 100644 iOverlay/src/mesh/outline/builder_join_shrink.rs create mode 100644 iOverlay/src/mesh/outline/uniq_iter.rs diff --git a/iOverlay/src/mesh/outline/builder.rs b/iOverlay/src/mesh/outline/builder.rs index e61a075..e71204b 100644 --- a/iOverlay/src/mesh/outline/builder.rs +++ b/iOverlay/src/mesh/outline/builder.rs @@ -1,6 +1,13 @@ -use crate::mesh::outline::builder_join::{BevelJoinBuilder, JoinBuilder, MiterJoinBuilder, RoundJoinBuilder}; -use crate::mesh::outline::section::{Section, SectionToSegment}; +use crate::mesh::outline::builder_join::JoinBuilder; +use crate::mesh::outline::builder_join_expand::{ + ExpandBevelJoinBuilder, ExpandMiterJoinBuilder, ExpandRoundJoinBuilder, +}; +use crate::mesh::outline::builder_join_shrink::{ + BevelJoinBuilder, MiterJoinBuilder, RoundJoinBuilder, +}; +use crate::mesh::outline::section::{OffsetSection, Section, SectionToSegment}; use crate::mesh::style::LineJoin; +use crate::segm::boolean::ShapeCountBoolean; use crate::segm::segment::Segment; use alloc::boxed::Box; use alloc::vec::Vec; @@ -8,7 +15,10 @@ use core::marker::PhantomData; use i_float::adapter::FloatPointAdapter; use i_float::float::compatible::FloatPointCompatible; use i_float::float::number::FloatNumber; -use crate::segm::boolean::ShapeCountBoolean; +use i_float::float::vector::FloatPointMath; +use i_float::int::point::IntPoint; +use crate::mesh::math::Math; +use crate::mesh::outline::uniq_iter::{UniqueSegment, UniqueSegmentsIter}; trait OutlineBuild, T: FloatNumber> { fn build( @@ -36,22 +46,42 @@ struct Builder, P: FloatPointCompatible, T: FloatNumber> impl + 'static, T: FloatNumber + 'static> OutlineBuilder { pub(super) fn new(radius: T, join: &LineJoin) -> OutlineBuilder { - let builder: Box> = match join { - LineJoin::Miter(ratio) => Box::new(Builder { - radius, - join_builder: MiterJoinBuilder::new(*ratio, radius), - _phantom: Default::default(), - }), - LineJoin::Round(ratio) => Box::new(Builder { - radius, - join_builder: RoundJoinBuilder::new(*ratio, radius), - _phantom: Default::default(), - }), - LineJoin::Bevel => Box::new(Builder { - radius, - join_builder: BevelJoinBuilder {}, - _phantom: Default::default(), - }), + let builder: Box> = if radius.is_expand() { + match join { + LineJoin::Miter(ratio) => Box::new(Builder { + radius, + join_builder: ExpandMiterJoinBuilder::new(*ratio, radius), + _phantom: Default::default(), + }), + LineJoin::Round(ratio) => Box::new(Builder { + radius, + join_builder: ExpandRoundJoinBuilder::new(*ratio, radius), + _phantom: Default::default(), + }), + LineJoin::Bevel => Box::new(Builder { + radius, + join_builder: ExpandBevelJoinBuilder {}, + _phantom: Default::default(), + }), + } + } else { + match join { + LineJoin::Miter(ratio) => Box::new(Builder { + radius, + join_builder: MiterJoinBuilder::new(*ratio, radius), + _phantom: Default::default(), + }), + LineJoin::Round(ratio) => Box::new(Builder { + radius, + join_builder: RoundJoinBuilder::new(*ratio, radius), + _phantom: Default::default(), + }), + LineJoin::Bevel => Box::new(Builder { + radius, + join_builder: BevelJoinBuilder {}, + _phantom: Default::default(), + }), + } }; Self { builder } @@ -97,57 +127,240 @@ impl, P: FloatPointCompatible, T: FloatNumber> OutlineBu return; } + self.build(path, adapter, segments); + + // if self.radius.is_expand() { + // self.expand_build(path, adapter, segments); + // } else { + // self.shrink_build(path, adapter, segments); + // } + } + + #[inline] + fn capacity(&self, points_count: usize) -> usize { + self.join_builder.capacity() * points_count + } + + #[inline] + fn additional_offset(&self, radius: T) -> T { + self.join_builder.additional_offset(radius) + } + + fn radius(&self) -> T { + self.radius + } +} + +impl, P: FloatPointCompatible, T: FloatNumber> Builder { +/* + #[inline] + fn next_unique_point( + start: usize, + index: usize, + path: &[P], + adapter: &FloatPointAdapter, + ) -> (usize, IntPoint) { + let a = adapter.float_to_int(&path[start]); + for (j, p) in path.iter().enumerate().skip(index) { + let b = adapter.float_to_int(p); + if a != b { + return (j, b); + } + } + + (usize::MAX, IntPoint::EMPTY) + } + + fn shrink_build( + &self, + path: &[P], + adapter: &FloatPointAdapter, + segments: &mut Vec>, + ) { // build segments only from points which are not equal in int space let i0 = path.len() - 1; - let i1 = Self::next_unique_point(i0, 0, path, adapter); + let (i1, mut p1) = Self::next_unique_point(i0, 0, path, adapter); if i1 == usize::MAX { return; } let start = Section::new(self.radius, &path[i0], &path[i1]); + let p0 = adapter.float_to_int(&path[i0]); + let mut s0 = start.clone(); - segments.add_section(&s0, adapter); + segments.add_shrink_section(p0, p1, &s0, adapter); - let mut i = i1; - i = Self::next_unique_point(i, i + 1, path, adapter); + let (mut i, mut pi) = Self::next_unique_point(i1, i1 + 1, path, adapter); while i != usize::MAX { let si = Section::new(self.radius, &s0.b, &path[i]); - self.join_builder.add_join(&s0, &si, adapter, segments); - segments.add_section(&si, adapter); + segments.add_shrink_section(p1, pi, &si, adapter); - i = Self::next_unique_point(i, i + 1, path, adapter); + (i, pi) = Self::next_unique_point(i, i + 1, path, adapter); s0 = si; + p1 = pi; } self.join_builder.add_join(&s0, &start, adapter, segments); } +*/ + fn build( + &self, + path: &[P], + adapter: &FloatPointAdapter, + segments: &mut Vec>, + ) { + let iter = path.iter().map(|p| adapter.float_to_int(p)); + let mut uniq_segments = if let Some(iter) = UniqueSegmentsIter::new(iter) { + iter + } else { + // TODO impl single point + return; + }; - #[inline] - fn capacity(&self, points_count: usize) -> usize { - self.join_builder.capacity() * points_count + let us0 = if let Some(us) = uniq_segments.next() { + us + } else { + // TODO impl single point + return; + }; + + let s0 = OffsetSection::new(self.radius, &us0, adapter); + let mut sk = s0; + + segments.push_some(sk.main_segment()); + + for usi in uniq_segments { + let si = OffsetSection::new(self.radius, &usi, adapter); + segments.push_some(si.main_segment()); + + let vi = si.b - si.a; + let vp = sk.b - sk.a; + + let cross = vi.cross_product(vp); + debug_assert!(cross != 0); + if cross > 0 { + // no join + segments.push_some(sk.a_segment()); + segments.push_some(sk.b_segment()); + } else { + self.join_builder.add_join(&s0, &si, adapter, segments); + } + + // self.join_builder.add_join(&s0, &si, adapter, segments); + // segments.add_expand_section(&si, adapter); + sp = si; + } } +} - #[inline] - fn additional_offset(&self, radius: T) -> T { - self.join_builder.additional_offset(radius) +trait Expand { + fn is_expand(&self) -> bool; +} + +impl Expand for T { + fn is_expand(&self) -> bool { + *self <= T::from_float(0.0) } +} - fn radius(&self) -> T { - self.radius +struct UniquePointsIter<'a, P, T> +where + P: FloatPointCompatible, + T: FloatNumber, +{ + path: &'a [P], + adapter: &'a FloatPointAdapter, + start_index: usize, + next_index: usize, + prev: IntPoint, +} +impl<'a, P, T> UniquePointsIter<'a, P, T> +where + P: FloatPointCompatible, + T: FloatNumber, +{ + #[inline] + fn new(path: &'a [P], adapter: &'a FloatPointAdapter) -> Self { + let (start_index, prev) = if path.is_empty() { + (0, IntPoint::EMPTY) + } else { + let start_index = path.len().saturating_sub(1); + let prev = adapter.float_to_int(&path[start_index]); + (start_index, prev) + }; + + Self { + path, + adapter, + start_index, + next_index: 0, + prev, + } } } -impl, P: FloatPointCompatible, T: FloatNumber> Builder { +impl<'a, P, T> Iterator for UniquePointsIter<'a, P, T> +where + P: FloatPointCompatible, + T: FloatNumber, +{ + type Item = (usize, IntPoint); + #[inline] - fn next_unique_point(start: usize, index: usize, path: &[P], adapter: &FloatPointAdapter) -> usize { - let a = adapter.float_to_int(&path[start]); - for (j, p) in path.iter().enumerate().skip(index) { - let b = adapter.float_to_int(p); - if a != b { - return j; + fn next(&mut self) -> Option { + if self.path.len() < 2 { + return None; + } + + for (j, p) in self.path.iter().enumerate().skip(self.next_index) { + let b = self.adapter.float_to_int(p); + if self.prev != b { + self.prev = b; + self.next_index = j + 1; + return Some((j, b)); } } - usize::MAX + None } } + +impl, T: FloatNumber> OffsetSection { + #[inline] + fn new(radius: T, s: &UniqueSegment, adapter: &FloatPointAdapter) -> Self + where P: FloatPointCompatible, T: FloatNumber, + { + let a = adapter.int_to_float(&s.a); + let b = adapter.int_to_float(&s.b); + let ab = FloatPointMath::sub(&b, &a); + let dir = FloatPointMath::normalize(&ab); + let ft = Math::ortho_and_scale(&dir, radius); + + let t = adapter.float_to_int(&ft); + + let a_top = s.a + t; + let b_top = s.b + t; + + Self { + a: s.a, + b: s.b, + a_top, + b_top, + dir, + _phantom: Default::default(), + } + } +} + +trait VecPushSome { + fn push_some(&mut self, value: Option); +} + +impl VecPushSome for Vec { + #[inline] + fn push_some(&mut self, value: Option) { + if let Some(v) = value { + self.push(v); + } + } +} \ No newline at end of file diff --git a/iOverlay/src/mesh/outline/builder_join.rs b/iOverlay/src/mesh/outline/builder_join.rs index 27c2737..5b6053c 100644 --- a/iOverlay/src/mesh/outline/builder_join.rs +++ b/iOverlay/src/mesh/outline/builder_join.rs @@ -1,271 +1,19 @@ -use crate::mesh::miter::{Miter, SharpMiter}; -use crate::mesh::outline::section::Section; -use crate::mesh::rotator::Rotator; -use crate::segm::segment::Segment; use alloc::vec::Vec; -use core::f64::consts::PI; use i_float::adapter::FloatPointAdapter; use i_float::float::compatible::FloatPointCompatible; use i_float::float::number::FloatNumber; -use i_float::float::vector::FloatPointMath; +use crate::mesh::outline::section::OffsetSection; use crate::segm::boolean::ShapeCountBoolean; +use crate::segm::segment::Segment; pub(super) trait JoinBuilder, T: FloatNumber> { fn add_join( &self, - s0: &Section, - s1: &Section, + s0: &OffsetSection, + s1: &OffsetSection, adapter: &FloatPointAdapter, segments: &mut Vec>, ); fn capacity(&self) -> usize; fn additional_offset(&self, radius: T) -> T; } - -pub(super) struct BevelJoinBuilder; - -impl BevelJoinBuilder { - #[inline] - fn join>( - s0: &Section, - s1: &Section, - adapter: &FloatPointAdapter, - segments: &mut Vec>, - ) { - let b0 = adapter.float_to_int(&s0.b_top); - let a1 = adapter.float_to_int(&s1.a_top); - if b0 == a1 { - return; - } - - segments.push(Segment::subject_ab(b0, a1)); - } -} - -impl> JoinBuilder for BevelJoinBuilder { - #[inline] - fn add_join( - &self, - s0: &Section, - s1: &Section, - adapter: &FloatPointAdapter, - segments: &mut Vec>, - ) { - Self::join(s0, s1, adapter, segments); - } - - #[inline] - fn capacity(&self) -> usize { - 2 - } - - #[inline] - fn additional_offset(&self, radius: T) -> T { - // add extra 10% to avoid problems with floating point precision. - T::from_float(1.1) * radius - } -} - -pub(super) struct MiterJoinBuilder { - limit_dot_product: T, - expand: bool, - max_offset: T, - max_length: T, -} - -impl MiterJoinBuilder { - pub(super) fn new(angle: T, radius: T) -> Self { - // angle - min possible angle - let fixed_angle = angle.to_f64().max(0.01); - let limit_dot_product = -T::from_float(fixed_angle.cos()); - - let half_angle = 0.5 * fixed_angle; - let tan = half_angle.tan(); - - let r = radius.to_f64().abs(); - let l = r / tan; - - // add extra 10% to avoid problems with floating point precision. - let max_offset = T::from_float(1.1 * (r * r + l * l).sqrt()); - let max_length = T::from_float(l); - - let expand = radius >= T::from_float(0.0); - Self { - limit_dot_product, - max_offset, - max_length, - expand, - } - } -} - -impl> JoinBuilder for MiterJoinBuilder { - fn add_join( - &self, - s0: &Section, - s1: &Section, - adapter: &FloatPointAdapter, - segments: &mut Vec>, - ) { - let cross_product = FloatPointMath::cross_product(&s0.dir, &s1.dir); - let turn = cross_product >= T::from_float(0.0); - if turn == self.expand { - BevelJoinBuilder::join(s0, s1, adapter, segments); - return; - } - - let pa = s0.b_top; - let pb = s1.a_top; - - let ia = adapter.float_to_int(&pa); - let ib = adapter.float_to_int(&pb); - - let sq_len = ia.sqr_distance(ib); - if sq_len < 4 { - BevelJoinBuilder::join(s0, s1, adapter, segments); - return; - } - - let dot_product = FloatPointMath::dot_product(&s0.dir, &s1.dir); - let is_limited = self.limit_dot_product > dot_product; - - if is_limited { - let (va, vb) = (s0.dir, s1.dir); - - let ax = pa.x() + self.max_length * va.x(); - let ay = pa.y() + self.max_length * va.y(); - let bx = pb.x() - self.max_length * vb.x(); - let by = pb.y() - self.max_length * vb.y(); - - let ac = P::from_xy(ax, ay); - let bc = P::from_xy(bx, by); - - let iac = adapter.float_to_int(&ac); - let ibc = adapter.float_to_int(&bc); - - if ia != iac { - segments.push(Segment::subject_ab(ia, iac)); - } - if iac != ibc { - segments.push(Segment::subject_ab(iac, ibc)); - } - if ibc != ib { - segments.push(Segment::subject_ab(ibc, ib)); - } - } else { - match Miter::sharp(pa, pb, s0.dir, s1.dir, adapter) { - SharpMiter::AB(a, b) => segments.push(Segment::subject_ab(a, b)), - SharpMiter::AcB(a, c, b) => { - segments.push(Segment::subject_ab(a, c)); - segments.push(Segment::subject_ab(c, b)); - } - SharpMiter::Degenerate => {} - } - } - } - - #[inline] - fn capacity(&self) -> usize { - 4 - } - - #[inline] - fn additional_offset(&self, _radius: T) -> T { - self.max_offset - } -} - -pub(super) struct RoundJoinBuilder { - inv_ratio: T, - average_count: usize, - radius: T, - limit_dot_product: T, - expand: bool, - rot_dir: T, -} - -impl RoundJoinBuilder { - pub(super) fn new(ratio: T, radius: T) -> Self { - // ratio = A / R - let fixed_ratio = ratio.min(T::from_float(0.25 * PI)); - let limit_dot_product = fixed_ratio.cos(); - let average_count = (T::from_float(0.6 * PI) / fixed_ratio).to_usize() + 2; - let (expand, rot_dir) = if radius >= T::from_float(0.0) { - (true, T::from_float(-1.0)) - } else { - (false, T::from_float(1.0)) - }; - - Self { - inv_ratio: T::from_float(1.0) / fixed_ratio, - average_count, - radius, - limit_dot_product, - expand, - rot_dir, - } - } -} -impl> JoinBuilder for RoundJoinBuilder { - fn add_join( - &self, - s0: &Section, - s1: &Section, - adapter: &FloatPointAdapter, - segments: &mut Vec>, - ) { - let cross_product = FloatPointMath::cross_product(&s0.dir, &s1.dir); - let turn = cross_product >= T::from_float(0.0); - if turn == self.expand { - BevelJoinBuilder::join(s0, s1, adapter, segments); - return; - } - - let dot_product = FloatPointMath::dot_product(&s0.dir, &s1.dir); - if self.limit_dot_product < dot_product { - BevelJoinBuilder::join(s0, s1, adapter, segments); - return; - } - - let angle = dot_product.acos(); - let n = (angle * self.inv_ratio).to_usize(); - let delta_angle = angle / T::from_usize(n); - - let start = s0.b_top; - let end = s1.a_top; - - let dir = P::from_xy(-s0.dir.y(), s0.dir.x()); - - let rotator = Rotator::::with_angle(self.rot_dir * delta_angle); - - let center = s0.b; - let mut v = dir; - let mut a = adapter.float_to_int(&start); - for _ in 1..n { - v = rotator.rotate(&v); - let p = FloatPointMath::add(¢er, &FloatPointMath::scale(&v, self.radius)); - - let b = adapter.float_to_int(&p); - if a != b { - segments.push(Segment::subject_ab(a, b)); - a = b; - } - } - - let b = adapter.float_to_int(&end); - if a != b { - segments.push(Segment::subject_ab(a, b)); - } - } - - #[inline] - fn capacity(&self) -> usize { - self.average_count - } - - #[inline] - fn additional_offset(&self, radius: T) -> T { - // add extra 10% to avoid problems with floating point precision. - T::from_float(1.1) * radius - } -} diff --git a/iOverlay/src/mesh/outline/builder_join_expand.rs b/iOverlay/src/mesh/outline/builder_join_expand.rs new file mode 100644 index 0000000..206aa0d --- /dev/null +++ b/iOverlay/src/mesh/outline/builder_join_expand.rs @@ -0,0 +1,258 @@ +// use crate::mesh::miter::{Miter, SharpMiter}; +// use crate::mesh::outline::section::Section; +// use crate::mesh::rotator::Rotator; +// use crate::segm::segment::Segment; +// use alloc::vec::Vec; +// use core::f64::consts::PI; +// use i_float::adapter::FloatPointAdapter; +// use i_float::float::compatible::FloatPointCompatible; +// use i_float::float::number::FloatNumber; +// use i_float::float::vector::FloatPointMath; +// use crate::mesh::outline::builder_join::JoinBuilder; +// use crate::segm::boolean::ShapeCountBoolean; +// +// pub(super) struct ExpandBevelJoinBuilder; +// +// impl ExpandBevelJoinBuilder { +// #[inline] +// fn join>( +// s0: &Section, +// s1: &Section, +// adapter: &FloatPointAdapter, +// segments: &mut Vec>, +// ) { +// let b0 = adapter.float_to_int(&s0.b_top); +// let a1 = adapter.float_to_int(&s1.a_top); +// if b0 == a1 { +// return; +// } +// +// segments.push(Segment::subject(b0, a1)); +// } +// } +// +// impl> JoinBuilder for ExpandBevelJoinBuilder { +// #[inline] +// fn add_join( +// &self, +// s0: &Section, +// s1: &Section, +// adapter: &FloatPointAdapter, +// segments: &mut Vec>, +// ) { +// Self::join(s0, s1, adapter, segments); +// } +// +// #[inline] +// fn capacity(&self) -> usize { +// 2 +// } +// +// #[inline] +// fn additional_offset(&self, radius: T) -> T { +// // add extra 10% to avoid problems with floating point precision. +// T::from_float(1.1) * radius +// } +// } +// +// pub(super) struct ExpandMiterJoinBuilder { +// limit_dot_product: T, +// expand: bool, +// max_offset: T, +// max_length: T, +// } +// +// impl ExpandMiterJoinBuilder { +// pub(super) fn new(angle: T, radius: T) -> Self { +// // angle - min possible angle +// let fixed_angle = angle.to_f64().max(0.01); +// let limit_dot_product = -T::from_float(fixed_angle.cos()); +// +// let half_angle = 0.5 * fixed_angle; +// let tan = half_angle.tan(); +// +// let r = radius.to_f64().abs(); +// let l = r / tan; +// +// // add extra 10% to avoid problems with floating point precision. +// let max_offset = T::from_float(1.1 * (r * r + l * l).sqrt()); +// let max_length = T::from_float(l); +// +// let expand = radius >= T::from_float(0.0); +// Self { +// limit_dot_product, +// max_offset, +// max_length, +// expand, +// } +// } +// } +// +// impl> JoinBuilder for ExpandMiterJoinBuilder { +// fn add_join( +// &self, +// s0: &Section, +// s1: &Section, +// adapter: &FloatPointAdapter, +// segments: &mut Vec>, +// ) { +// let turn = s0.is_positive_turn(s1); +// if turn == self.expand { +// ExpandBevelJoinBuilder::join(s0, s1, adapter, segments); +// return; +// } +// +// let pa = s0.b_top; +// let pb = s1.a_top; +// +// let ia = adapter.float_to_int(&pa); +// let ib = adapter.float_to_int(&pb); +// +// let sq_len = ia.sqr_distance(ib); +// if sq_len < 4 { +// ExpandBevelJoinBuilder::join(s0, s1, adapter, segments); +// return; +// } +// +// let dot_product = FloatPointMath::dot_product(&s0.dir, &s1.dir); +// let is_limited = self.limit_dot_product > dot_product; +// +// if is_limited { +// let (va, vb) = (s0.dir, s1.dir); +// +// let ax = pa.x() + self.max_length * va.x(); +// let ay = pa.y() + self.max_length * va.y(); +// let bx = pb.x() - self.max_length * vb.x(); +// let by = pb.y() - self.max_length * vb.y(); +// +// let ac = P::from_xy(ax, ay); +// let bc = P::from_xy(bx, by); +// +// let iac = adapter.float_to_int(&ac); +// let ibc = adapter.float_to_int(&bc); +// +// if ia != iac { +// segments.push(Segment::subject(ia, iac)); +// } +// if iac != ibc { +// segments.push(Segment::subject(iac, ibc)); +// } +// if ibc != ib { +// segments.push(Segment::subject(ibc, ib)); +// } +// } else { +// match Miter::sharp(pa, pb, s0.dir, s1.dir, adapter) { +// SharpMiter::AB(a, b) => segments.push(Segment::subject(a, b)), +// SharpMiter::AcB(a, c, b) => { +// segments.push(Segment::subject(a, c)); +// segments.push(Segment::subject(c, b)); +// } +// SharpMiter::Degenerate => {} +// } +// } +// } +// +// #[inline] +// fn capacity(&self) -> usize { +// 4 +// } +// +// #[inline] +// fn additional_offset(&self, _radius: T) -> T { +// self.max_offset +// } +// } +// +// pub(super) struct ExpandRoundJoinBuilder { +// inv_ratio: T, +// average_count: usize, +// radius: T, +// limit_dot_product: T, +// expand: bool, +// rot_dir: T, +// } +// +// impl ExpandRoundJoinBuilder { +// pub(super) fn new(ratio: T, radius: T) -> Self { +// // ratio = A / R +// let fixed_ratio = ratio.min(T::from_float(0.25 * PI)); +// let limit_dot_product = fixed_ratio.cos(); +// let average_count = (T::from_float(0.6 * PI) / fixed_ratio).to_usize() + 2; +// let (expand, rot_dir) = if radius >= T::from_float(0.0) { +// (true, T::from_float(-1.0)) +// } else { +// (false, T::from_float(1.0)) +// }; +// +// Self { +// inv_ratio: T::from_float(1.0) / fixed_ratio, +// average_count, +// radius, +// limit_dot_product, +// expand, +// rot_dir, +// } +// } +// } +// impl> JoinBuilder for ExpandRoundJoinBuilder { +// fn add_join( +// &self, +// s0: &Section, +// s1: &Section, +// adapter: &FloatPointAdapter, +// segments: &mut Vec>, +// ) { +// let turn = s0.is_positive_turn(s1); +// if turn == self.expand { +// ExpandBevelJoinBuilder::join(s0, s1, adapter, segments); +// return; +// } +// +// let dot_product = FloatPointMath::dot_product(&s0.dir, &s1.dir); +// if self.limit_dot_product < dot_product { +// ExpandBevelJoinBuilder::join(s0, s1, adapter, segments); +// return; +// } +// +// let angle = dot_product.acos(); +// let n = (angle * self.inv_ratio).to_usize(); +// let delta_angle = angle / T::from_usize(n); +// +// let start = s0.b_top; +// let end = s1.a_top; +// +// let dir = P::from_xy(-s0.dir.y(), s0.dir.x()); +// +// let rotator = Rotator::::with_angle(self.rot_dir * delta_angle); +// +// let center = s0.b; +// let mut v = dir; +// let mut a = adapter.float_to_int(&start); +// for _ in 1..n { +// v = rotator.rotate(&v); +// let p = FloatPointMath::add(¢er, &FloatPointMath::scale(&v, self.radius)); +// +// let b = adapter.float_to_int(&p); +// if a != b { +// segments.push(Segment::subject(a, b)); +// a = b; +// } +// } +// +// let b = adapter.float_to_int(&end); +// if a != b { +// segments.push(Segment::subject(a, b)); +// } +// } +// +// #[inline] +// fn capacity(&self) -> usize { +// self.average_count +// } +// +// #[inline] +// fn additional_offset(&self, radius: T) -> T { +// // add extra 10% to avoid problems with floating point precision. +// T::from_float(1.1) * radius +// } +// } diff --git a/iOverlay/src/mesh/outline/builder_join_shrink.rs b/iOverlay/src/mesh/outline/builder_join_shrink.rs new file mode 100644 index 0000000..e58c6ee --- /dev/null +++ b/iOverlay/src/mesh/outline/builder_join_shrink.rs @@ -0,0 +1,258 @@ +use crate::mesh::miter::{Miter, SharpMiter}; +use crate::mesh::outline::section::OffsetSection; +use crate::mesh::rotator::Rotator; +use crate::segm::segment::Segment; +use alloc::vec::Vec; +use core::f64::consts::PI; +use i_float::adapter::FloatPointAdapter; +use i_float::float::compatible::FloatPointCompatible; +use i_float::float::number::FloatNumber; +use i_float::float::vector::FloatPointMath; +use crate::mesh::outline::builder_join::JoinBuilder; +use crate::segm::boolean::ShapeCountBoolean; + +pub(super) struct BevelJoinBuilder; + +impl BevelJoinBuilder { + #[inline] + fn join>( + s0: &OffsetSection, + s1: &OffsetSection, + _adapter: &FloatPointAdapter, + segments: &mut Vec>, + ) { + if s0.b_top == s1.a_top { + return; + } + + segments.push(Segment::subject(s0.b_top, s1.a_top)); + } +} + +impl> JoinBuilder for BevelJoinBuilder { + #[inline] + fn add_join( + &self, + s0: &OffsetSection, + s1: &OffsetSection, + adapter: &FloatPointAdapter, + segments: &mut Vec>, + ) { + Self::join(s0, s1, adapter, segments); + } + + #[inline] + fn capacity(&self) -> usize { + 2 + } + + #[inline] + fn additional_offset(&self, radius: T) -> T { + // add extra 10% to avoid problems with floating point precision. + T::from_float(1.1) * radius + } +} + +pub(super) struct MiterJoinBuilder { + limit_dot_product: T, + expand: bool, + max_offset: T, + max_length: T, +} + +impl MiterJoinBuilder { + pub(super) fn new(angle: T, radius: T) -> Self { + // angle - min possible angle + let fixed_angle = angle.to_f64().max(0.01); + let limit_dot_product = -T::from_float(fixed_angle.cos()); + + let half_angle = 0.5 * fixed_angle; + let tan = half_angle.tan(); + + let r = radius.to_f64().abs(); + let l = r / tan; + + // add extra 10% to avoid problems with floating point precision. + let max_offset = T::from_float(1.1 * (r * r + l * l).sqrt()); + let max_length = T::from_float(l); + + let expand = radius >= T::from_float(0.0); + Self { + limit_dot_product, + max_offset, + max_length, + expand, + } + } +} + +impl> JoinBuilder for MiterJoinBuilder { + fn add_join( + &self, + s0: &OffsetSection, + s1: &OffsetSection, + adapter: &FloatPointAdapter, + segments: &mut Vec>, + ) { + let cross_product = FloatPointMath::cross_product(&s0.dir, &s1.dir); + let turn = cross_product >= T::from_float(0.0); + if turn == self.expand { + BevelJoinBuilder::join(s0, s1, adapter, segments); + return; + } + + let ia = s0.b_top; + let ib = s1.a_top; + + let sq_len = ia.sqr_distance(ib); + if sq_len < 4 { + BevelJoinBuilder::join(s0, s1, adapter, segments); + return; + } + + let dot_product = FloatPointMath::dot_product(&s0.dir, &s1.dir); + let is_limited = self.limit_dot_product > dot_product; + + let pa = adapter.int_to_float(&s0.a); + let pb = adapter.int_to_float(&s1.b); + + if is_limited { + let (va, vb) = (s0.dir, s1.dir); + + let ax = pa.x() + self.max_length * va.x(); + let ay = pa.y() + self.max_length * va.y(); + let bx = pb.x() - self.max_length * vb.x(); + let by = pb.y() - self.max_length * vb.y(); + + let ac = P::from_xy(ax, ay); + let bc = P::from_xy(bx, by); + + let iac = adapter.float_to_int(&ac); + let ibc = adapter.float_to_int(&bc); + + if ia != iac { + segments.push(Segment::subject(ia, iac)); + } + if iac != ibc { + segments.push(Segment::subject(iac, ibc)); + } + if ibc != ib { + segments.push(Segment::subject(ibc, ib)); + } + } else { + match Miter::sharp(pa, pb, s0.dir, s1.dir, adapter) { + SharpMiter::AB(a, b) => segments.push(Segment::subject(a, b)), + SharpMiter::AcB(a, c, b) => { + segments.push(Segment::subject(a, c)); + segments.push(Segment::subject(c, b)); + } + SharpMiter::Degenerate => {} + } + } + } + + #[inline] + fn capacity(&self) -> usize { + 4 + } + + #[inline] + fn additional_offset(&self, _radius: T) -> T { + self.max_offset + } +} + +pub(super) struct RoundJoinBuilder { + inv_ratio: T, + average_count: usize, + radius: T, + limit_dot_product: T, + expand: bool, + rot_dir: T, +} + +impl RoundJoinBuilder { + pub(super) fn new(ratio: T, radius: T) -> Self { + // ratio = A / R + let fixed_ratio = ratio.min(T::from_float(0.25 * PI)); + let limit_dot_product = fixed_ratio.cos(); + let average_count = (T::from_float(0.6 * PI) / fixed_ratio).to_usize() + 2; + let (expand, rot_dir) = if radius >= T::from_float(0.0) { + (true, T::from_float(-1.0)) + } else { + (false, T::from_float(1.0)) + }; + + Self { + inv_ratio: T::from_float(1.0) / fixed_ratio, + average_count, + radius, + limit_dot_product, + expand, + rot_dir, + } + } +} +impl> JoinBuilder for RoundJoinBuilder { + fn add_join( + &self, + s0: &OffsetSection, + s1: &OffsetSection, + adapter: &FloatPointAdapter, + segments: &mut Vec>, + ) { + let cross_product = FloatPointMath::cross_product(&s0.dir, &s1.dir); + let turn = cross_product >= T::from_float(0.0); + if turn == self.expand { + BevelJoinBuilder::join(s0, s1, adapter, segments); + return; + } + + let dot_product = FloatPointMath::dot_product(&s0.dir, &s1.dir); + if self.limit_dot_product < dot_product { + BevelJoinBuilder::join(s0, s1, adapter, segments); + return; + } + + let angle = dot_product.acos(); + let n = (angle * self.inv_ratio).to_usize(); + let delta_angle = angle / T::from_usize(n); + + let start = s0.b_top; + let end = s1.a_top; + + let dir = P::from_xy(-s0.dir.y(), s0.dir.x()); + + let rotator = Rotator::::with_angle(self.rot_dir * delta_angle); + + let center = s0.b; + let mut v = dir; + let mut a = adapter.float_to_int(&start); + for _ in 1..n { + v = rotator.rotate(&v); + let p = FloatPointMath::add(¢er, &FloatPointMath::scale(&v, self.radius)); + + let b = adapter.float_to_int(&p); + if a != b { + segments.push(Segment::subject(a, b)); + a = b; + } + } + + let b = adapter.float_to_int(&end); + if a != b { + segments.push(Segment::subject(a, b)); + } + } + + #[inline] + fn capacity(&self) -> usize { + self.average_count + } + + #[inline] + fn additional_offset(&self, radius: T) -> T { + // add extra 10% to avoid problems with floating point precision. + T::from_float(1.1) * radius + } +} diff --git a/iOverlay/src/mesh/outline/mod.rs b/iOverlay/src/mesh/outline/mod.rs index adf7353..7d9de2f 100644 --- a/iOverlay/src/mesh/outline/mod.rs +++ b/iOverlay/src/mesh/outline/mod.rs @@ -1,4 +1,7 @@ mod builder; -mod builder_join; +mod builder_join_expand; pub mod offset; mod section; +mod builder_join_shrink; +mod builder_join; +mod uniq_iter; diff --git a/iOverlay/src/mesh/outline/offset.rs b/iOverlay/src/mesh/outline/offset.rs index 90d7cdf..76300dd 100644 --- a/iOverlay/src/mesh/outline/offset.rs +++ b/iOverlay/src/mesh/outline/offset.rs @@ -212,20 +212,6 @@ impl + 'static, T: FloatNumber + 'static> OutlineSolv &mut flat_buffer, ); } - - if self.outer_builder.is_shrink() { - offset_overlay.clear(); - offset_overlay.add_path_iter(path.iter().map(|p| self.adapter.float_to_int(p)), Subject); - offset_overlay.add_flat_buffer(&flat_buffer, Clip); - - if let Some(graph) = offset_overlay.build_graph_view(FillRule::Positive) { - graph.extract_contours_into( - OverlayRule::Difference, - &mut bool_buffer, - &mut flat_buffer, - ); - } - } } else { offset_overlay.options.output_direction = ContourDirection::Clockwise; segments.reserve(self.inner_builder.capacity(path.len())); @@ -240,20 +226,6 @@ impl + 'static, T: FloatNumber + 'static> OutlineSolv &mut flat_buffer, ); } - - if !self.inner_builder.is_shrink() { - offset_overlay.clear(); - offset_overlay.add_path_iter(path.iter().map(|p| self.adapter.float_to_int(p)), Subject); - offset_overlay.add_flat_buffer(&flat_buffer, Clip); - - if let Some(graph) = offset_overlay.build_graph_view(FillRule::Negative) { - graph.extract_contours_into( - OverlayRule::Intersect, - &mut bool_buffer, - &mut flat_buffer, - ); - } - } } overlay.add_flat_buffer(&flat_buffer, Subject); @@ -342,7 +314,39 @@ mod tests { } #[test] - fn test_square() { + fn test_square_zero_offset() { + let path = [[-5.0, -5.0f32], [5.0, -5.0], [5.0, 5.0], [-5.0, 5.0]]; + + let style = OutlineStyle::new(0.0); + let shapes = path.outline_fixed_scale(&style, 10.0).unwrap(); + + assert_eq!(shapes.len(), 1); + + let shape = shapes.first().unwrap(); + assert_eq!(shape.len(), 1); + + let path = shape.first().unwrap(); + assert_eq!(path.len(), 4); + } + + #[test] + fn test_square_positive_offset_0() { + let path = [[-5.0, -5.0f32], [5.0, -5.0], [5.0, 5.0], [-5.0, 5.0]]; + + let style = OutlineStyle::new(1.0); + let shapes = path.outline_fixed_scale(&style, 10.0).unwrap(); + + assert_eq!(shapes.len(), 1); + + let shape = shapes.first().unwrap(); + assert_eq!(shape.len(), 1); + + let path = shape.first().unwrap(); + assert_eq!(path.len(), 8); + } + + #[test] + fn test_square_positive_offset_1() { let path = [[-5.0, -5.0f32], [5.0, -5.0], [5.0, 5.0], [-5.0, 5.0]]; let style = OutlineStyle::new(10.0); @@ -357,6 +361,7 @@ mod tests { assert_eq!(path.len(), 8); } + #[test] fn test_square_round_offset() { let path = [[-5.0, -5.0f32], [5.0, -5.0], [5.0, 5.0], [-5.0, 5.0]]; @@ -370,10 +375,46 @@ mod tests { } #[test] - fn test_square_negative_offset() { + fn test_square_negative_offset_0() { + let path = [[-5.0, -5.0f32], [5.0, -5.0], [5.0, 5.0], [-5.0, 5.0]]; + + let style = OutlineStyle::new(-1.0); + let shapes = path.outline_fixed_scale(&style, 10.0).unwrap(); + + assert_eq!(shapes.len(), 1); + + let shape = shapes.first().unwrap(); + assert_eq!(shape.len(), 1); + + let path = shape.first().unwrap(); + assert_eq!(path.len(), 4); + } + + #[test] + fn test_square_negative_offset_1() { + let path = [[-5.0, -5.0f32], [5.0, -5.0], [5.0, 5.0], [-5.0, 5.0]]; + + let style = OutlineStyle::new(-6.0); + let shapes = path.outline_fixed_scale(&style, 10.0).unwrap(); + + assert_eq!(shapes.len(), 0); + } + + #[test] + fn test_square_negative_offset_2() { + let path = [[-5.0, -5.0f32], [5.0, -5.0], [5.0, 5.0], [-5.0, 5.0]]; + + let style = OutlineStyle::new(-10.0); + let shapes = path.outline_fixed_scale(&style, 10.0).unwrap(); + + assert_eq!(shapes.len(), 0); + } + + #[test] + fn test_square_negative_offset_3() { let path = [[-5.0, -5.0f32], [5.0, -5.0], [5.0, 5.0], [-5.0, 5.0]]; - let style = OutlineStyle::new(-20.0); + let style = OutlineStyle::new(-11.0); let shapes = path.outline_fixed_scale(&style, 10.0).unwrap(); assert_eq!(shapes.len(), 0); diff --git a/iOverlay/src/mesh/outline/section.rs b/iOverlay/src/mesh/outline/section.rs index 0c2602a..22181c2 100644 --- a/iOverlay/src/mesh/outline/section.rs +++ b/iOverlay/src/mesh/outline/section.rs @@ -6,6 +6,7 @@ use i_float::adapter::FloatPointAdapter; use i_float::float::compatible::FloatPointCompatible; use i_float::float::number::FloatNumber; use i_float::float::vector::FloatPointMath; +use i_float::int::point::IntPoint; use crate::segm::boolean::ShapeCountBoolean; #[derive(Debug, Clone)] @@ -18,7 +19,7 @@ pub(super) struct Section, T: FloatNumber> { } impl> Section { - pub(crate) fn new(radius: T, a: &P, b: &P) -> Self { + pub(super) fn new(radius: T, a: &P, b: &P) -> Self { let dir = Math::normal(b, a); let t = Math::ortho_and_scale(&dir, radius); @@ -33,18 +34,83 @@ impl> Section { _phantom: Default::default(), } } + + pub(super) fn is_positive_turn(&self, other: &Self) -> bool { + let cross_product = FloatPointMath::cross_product(&self.dir, &other.dir); + cross_product >= T::from_float(0.0) + } } -pub(crate) trait SectionToSegment> { - fn add_section(&mut self, section: &Section, adapter: &FloatPointAdapter); +pub(super) trait SectionToSegment> { + fn add_expand_section(&mut self, section: &Section, adapter: &FloatPointAdapter); + fn add_shrink_section(&mut self, a: IntPoint, b: IntPoint, section: &Section, adapter: &FloatPointAdapter); } impl> SectionToSegment for Vec> { - fn add_section(&mut self, section: &Section, adapter: &FloatPointAdapter) { + fn add_expand_section(&mut self, section: &Section, adapter: &FloatPointAdapter) { let a_top = adapter.float_to_int(§ion.a_top); let b_top = adapter.float_to_int(§ion.b_top); if a_top != b_top { - self.push(Segment::subject_ab(a_top, b_top)); + self.push(Segment::subject(a_top, b_top)); } } + + fn add_shrink_section(&mut self, a: IntPoint, b: IntPoint, section: &Section, adapter: &FloatPointAdapter) { + let a_top = adapter.float_to_int(§ion.a_top); + let b_top = adapter.float_to_int(§ion.b_top); + debug_assert!(a != b); + + // ToDO Subject+clip + self.push(Segment::subject(b, a)); + + if a != a_top { + self.push(Segment::subject(a, a_top)); + } + if b != b_top { + self.push(Segment::subject(b_top, b)); + } + if a_top != b_top { + self.push(Segment::subject(a_top, b_top)); + } + } +} + + +#[derive(Debug, Clone)] +pub(super) struct OffsetSection, T: FloatNumber> { + pub(super) a: IntPoint, + pub(super) b: IntPoint, + pub(super) a_top: IntPoint, + pub(super) b_top: IntPoint, + pub(super) dir: P, + pub(super) _phantom: PhantomData, } + +impl, T: FloatNumber> OffsetSection { + #[inline] + pub(super) fn main_segment(&self) -> Option> { + if self.a_top != self.b_top { + Some(Segment::subject(self.a_top, self.b_top)) + } else { + None + } + } + + #[inline] + pub(super) fn a_segment(&self) -> Option> { + if self.a_top != self.a { + Some(Segment::subject(self.a, self.a_top)) + } else { + None + } + } + + #[inline] + pub(super) fn b_segment(&self) -> Option> { + if self.b_top != self.b { + Some(Segment::subject(self.b_top, self.b)) + } else { + None + } + } +} \ No newline at end of file diff --git a/iOverlay/src/mesh/outline/uniq_iter.rs b/iOverlay/src/mesh/outline/uniq_iter.rs new file mode 100644 index 0000000..9868b47 --- /dev/null +++ b/iOverlay/src/mesh/outline/uniq_iter.rs @@ -0,0 +1,239 @@ +use core::iter::Chain; +use i_float::int::point::IntPoint; + +pub(super) struct UniqueSegment { + pub(super) a: IntPoint, + pub(super) b: IntPoint, +} + +pub(super) struct UniqueSegmentsIter +where + I: Iterator, +{ + iter: Chain>, + p0: IntPoint, + p1: IntPoint, +} + +impl UniqueSegmentsIter +where + I: Iterator, +{ + #[inline] + pub(super) fn new(iter: I) -> Option { + let mut iter = iter; + + let mut p0 = if let Some(p) = iter.next() { + p + } else { + return None; + }; + let mut p1 = if let Some(p) = iter.find(|p| p0.ne(p)) { + p + } else { + return None; + }; + + let q0 = p0; + + for p2 in &mut iter { + if include_point(p0, p1, p2) { + p0 = p1; + p1 = p2; + break; + } + p1 = p2; + } + + let q1 = p0; + + let chain_iter = iter.chain([q0, q1]); + + Some(Self { + iter: chain_iter, + p0, + p1, + }) + } +} + +impl Iterator for UniqueSegmentsIter +where + I: Iterator, +{ + type Item = UniqueSegment; + #[inline] + fn next(&mut self) -> Option { + for p2 in &mut self.iter { + if !include_point(self.p0, self.p1, p2) { + self.p1 = p2; + continue; + } + let s = UniqueSegment { + a: self.p0, + b: self.p1, + }; + + self.p0 = self.p1; + self.p1 = p2; + + return Some(s); + } + + let add_last = self.p1 != self.p0; + if add_last { + let s = UniqueSegment { + a: self.p0, + b: self.p1, + }; + self.p1 = self.p0; + Some(s) + } else { + None + } + } +} + +#[inline] +fn include_point(p0: IntPoint, p1: IntPoint, p2: IntPoint) -> bool { + let a = p1.subtract(p0); + let b = p1.subtract(p2); + + if a.cross_product(b) != 0 { + // not collinear + return true; + } + + // collinear – keep only if we keep going opposite direction + a.dot_product(b) > 0 +} +#[cfg(test)] +mod tests { + use crate::mesh::outline::uniq_iter::{UniqueSegment, UniqueSegmentsIter}; + use alloc::vec::Vec; + use i_float::int::point::IntPoint; + use i_shape::int_path; + + #[test] + fn test_empty() { + let uniq_iter = UniqueSegmentsIter::new(core::iter::empty::()); + assert!(uniq_iter.is_none()); + } + + #[test] + fn test_single_point() { + let path = int_path![[0, 0]]; + let uniq_iter = UniqueSegmentsIter::new(path.iter().copied()); + assert!(uniq_iter.is_none()); + } + + #[test] + fn test_all_points_equal() { + let path = int_path![[0, 0], [0, 0], [0, 0]]; + let uniq_iter = UniqueSegmentsIter::new(path.iter().copied()); + assert!(uniq_iter.is_none()); + } + + #[test] + fn test_line_0() { + let path = int_path![[0, 0], [10, 0]]; + validate_case_all_rotations(&path, 2); + } + + #[test] + fn test_line_1() { + let path = int_path![[0, 0], [5, 0], [10, 0]]; + validate_case_all_rotations(&path, 2); + } + + #[test] + fn test_line_2() { + let path = int_path![[0, 0], [5, 0], [10, 0], [5, 0]]; + validate_case_all_rotations(&path, 2); + } + + #[test] + fn test_square_0() { + let path = int_path![[0, 10], [0, 0], [10, 0], [10, 10]]; + validate_case_all_rotations(&path, 4); + } + + #[test] + fn test_square_1() { + #[rustfmt::skip] + let path = int_path![[0, 10], [0, 5], [0, 0], [5, 0], [10, 0], [10, 5], [10, 10], [5, 10]]; + validate_case_all_rotations(&path, 4); + } + + #[test] + fn test_square_2() { + #[rustfmt::skip] + let path = int_path![ + [0, 10], [0, 8], [0, 5], [0, 2], + [0, 0], [2, 0], [5, 0], [8, 0], + [10, 0], [10, 2], [10, 5], [10, 8], + [10, 10], [8, 10], [5, 10], [2, 10] + ]; + validate_case_all_rotations(&path, 4); + } + + fn validate_case_all_rotations(path: &[IntPoint], expected_segments_count: usize) { + assert!(!path.is_empty(), "path must not be empty"); + + for shift in 0..path.len() { + let uniq_iter = UniqueSegmentsIter::new( + path[shift..] + .iter() + .chain(path[..shift].iter()) + .copied(), + ) + .unwrap(); + + let segments: Vec<_> = uniq_iter.collect(); + + assert_eq!( + segments.len(), + expected_segments_count, + "unexpected segment count for shift {}", + shift + ); + + validate_segments(&segments); + } + } + + fn validate_segments(segments: &[UniqueSegment]) { + assert!(!segments.is_empty(), "expected at least one segment"); + + for (i, s) in segments.iter().enumerate() { + assert_ne!(s.a, s.b, "segment {} is degenerate (a == b)", i); + } + + for (i, w) in segments.windows(2).enumerate() { + let s0 = &w[0]; + let s1 = &w[1]; + + validate_pair(s0, s1, i, i + 1); + } + + let last_i = segments.len() - 1; + validate_pair(&segments[last_i], &segments[0], last_i, 0); + } + + fn validate_pair(s0: &UniqueSegment, s1: &UniqueSegment, i: usize, j: usize) { + assert_eq!( + s0.b, s1.a, + "segment {} end does not match segment {} start", + i, j + ); + + let v0 = s0.a - s0.b; + let v1 = s1.a - s1.b; + + let cross = v0.cross_product(v1); + if cross == 0 { + let dot = v0.dot_product(v1); + assert!(dot < 0, "segments {} and {} are collinear and same direction", i, j); + } + } +} diff --git a/iOverlay/src/mesh/stroke/builder_cap.rs b/iOverlay/src/mesh/stroke/builder_cap.rs index 10508fd..3a98354 100644 --- a/iOverlay/src/mesh/stroke/builder_cap.rs +++ b/iOverlay/src/mesh/stroke/builder_cap.rs @@ -85,12 +85,12 @@ impl> CapBuilder { let r = rotator.rotate(p); let q = FloatPointMath::add(&r, §ion.a); let b = adapter.float_to_int(&q); - segments.push(Segment::subject_ab(a, b)); + segments.push(Segment::subject(a, b)); a = b; } } let last = adapter.float_to_int(§ion.a_bot); - segments.push(Segment::subject_ab(a, last)); + segments.push(Segment::subject(a, last)); } pub(super) fn add_to_end( @@ -106,12 +106,12 @@ impl> CapBuilder { let r = rotator.rotate(p); let q = FloatPointMath::add(&r, §ion.b); let b = adapter.float_to_int(&q); - segments.push(Segment::subject_ab(a, b)); + segments.push(Segment::subject(a, b)); a = b; } } let last = adapter.float_to_int(§ion.b_top); - segments.push(Segment::subject_ab(a, last)); + segments.push(Segment::subject(a, last)); } #[inline] diff --git a/iOverlay/src/mesh/stroke/builder_join.rs b/iOverlay/src/mesh/stroke/builder_join.rs index c9ec853..5c176b4 100644 --- a/iOverlay/src/mesh/stroke/builder_join.rs +++ b/iOverlay/src/mesh/stroke/builder_join.rs @@ -55,7 +55,7 @@ impl BevelJoinBuilder { let ia = adapter.float_to_int(a); let ib = adapter.float_to_int(b); if ia != ib { - segments.push(Segment::subject_ab(ib, ia)); + segments.push(Segment::subject(ib, ia)); } } } @@ -178,13 +178,13 @@ impl> JoinBuilder for MiterJoin let ibc = adapter.float_to_int(&bc); if ia != iac { - segments.push(Segment::subject_ab(iac, ia)); + segments.push(Segment::subject(iac, ia)); } if iac != ibc { - segments.push(Segment::subject_ab(ibc, iac)); + segments.push(Segment::subject(ibc, iac)); } if ibc != ib { - segments.push(Segment::subject_ab(ib, ibc)); + segments.push(Segment::subject(ib, ibc)); } } else { let (pa, pb, va, vb) = if turn { @@ -195,10 +195,10 @@ impl> JoinBuilder for MiterJoin (s0.b_top, s1.a_top, s0.dir, s1.dir) }; match Miter::sharp(pa, pb, va, vb, adapter) { - SharpMiter::AB(a, b) => segments.push(Segment::subject_ab(b, a)), + SharpMiter::AB(a, b) => segments.push(Segment::subject(b, a)), SharpMiter::AcB(a, c, b) => { - segments.push(Segment::subject_ab(c, a)); - segments.push(Segment::subject_ab(b, c)); + segments.push(Segment::subject(c, a)); + segments.push(Segment::subject(b, c)); } SharpMiter::Degenerate => {} } @@ -277,14 +277,14 @@ impl> JoinBuilder for RoundJoin let b = adapter.float_to_int(&p); if a != b { - segments.push(Segment::subject_ab(b, a)); + segments.push(Segment::subject(b, a)); a = b; } } let b = adapter.float_to_int(&end); if a != b { - segments.push(Segment::subject_ab(b, a)); + segments.push(Segment::subject(b, a)); } } diff --git a/iOverlay/src/mesh/stroke/section.rs b/iOverlay/src/mesh/stroke/section.rs index 176d1df..0009c80 100644 --- a/iOverlay/src/mesh/stroke/section.rs +++ b/iOverlay/src/mesh/stroke/section.rs @@ -56,10 +56,10 @@ impl> SectionToSegment for Vec< let b_bot = adapter.float_to_int(§ion.b_bot); if a_top != b_top { - self.push(Segment::subject_ab(b_top, a_top)); + self.push(Segment::subject(b_top, a_top)); } if a_bot != b_bot { - self.push(Segment::subject_ab(a_bot, b_bot)); + self.push(Segment::subject(a_bot, b_bot)); } } } diff --git a/iOverlay/src/mesh/subject.rs b/iOverlay/src/mesh/subject.rs index 2821ad3..4aa9ab6 100644 --- a/iOverlay/src/mesh/subject.rs +++ b/iOverlay/src/mesh/subject.rs @@ -5,7 +5,7 @@ use crate::segm::boolean::ShapeCountBoolean; impl Segment { #[inline] - pub(crate) fn subject_ab(p0: IntPoint, p1: IntPoint) -> Self { + pub(crate) fn subject(p0: IntPoint, p1: IntPoint) -> Self { if p0 < p1 { Self { x_segment: XSegment { a: p0, b: p1 }, From 52a30a648e7efd1389c0a88c2603904cf5d5a332 Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Sat, 21 Mar 2026 20:45:55 +0300 Subject: [PATCH 06/10] pass most tests --- iOverlay/src/mesh/outline/builder.rs | 236 ++++-------------- .../src/mesh/outline/builder_join_shrink.rs | 21 +- iOverlay/src/mesh/outline/offset.rs | 19 +- iOverlay/src/mesh/outline/section.rs | 2 +- 4 files changed, 73 insertions(+), 205 deletions(-) diff --git a/iOverlay/src/mesh/outline/builder.rs b/iOverlay/src/mesh/outline/builder.rs index e71204b..4f945cc 100644 --- a/iOverlay/src/mesh/outline/builder.rs +++ b/iOverlay/src/mesh/outline/builder.rs @@ -1,11 +1,8 @@ +use crate::mesh::math::Math; use crate::mesh::outline::builder_join::JoinBuilder; -use crate::mesh::outline::builder_join_expand::{ - ExpandBevelJoinBuilder, ExpandMiterJoinBuilder, ExpandRoundJoinBuilder, -}; -use crate::mesh::outline::builder_join_shrink::{ - BevelJoinBuilder, MiterJoinBuilder, RoundJoinBuilder, -}; -use crate::mesh::outline::section::{OffsetSection, Section, SectionToSegment}; +use crate::mesh::outline::builder_join_shrink::{BevelJoinBuilder, MiterJoinBuilder, RoundJoinBuilder}; +use crate::mesh::outline::section::OffsetSection; +use crate::mesh::outline::uniq_iter::{UniqueSegment, UniqueSegmentsIter}; use crate::mesh::style::LineJoin; use crate::segm::boolean::ShapeCountBoolean; use crate::segm::segment::Segment; @@ -16,9 +13,6 @@ use i_float::adapter::FloatPointAdapter; use i_float::float::compatible::FloatPointCompatible; use i_float::float::number::FloatNumber; use i_float::float::vector::FloatPointMath; -use i_float::int::point::IntPoint; -use crate::mesh::math::Math; -use crate::mesh::outline::uniq_iter::{UniqueSegment, UniqueSegmentsIter}; trait OutlineBuild, T: FloatNumber> { fn build( @@ -30,8 +24,6 @@ trait OutlineBuild, T: FloatNumber> { fn capacity(&self, points_count: usize) -> usize; fn additional_offset(&self, radius: T) -> T; - - fn radius(&self) -> T; } pub(super) struct OutlineBuilder, T: FloatNumber> { @@ -39,6 +31,7 @@ pub(super) struct OutlineBuilder, T: FloatNumber> { } struct Builder, P: FloatPointCompatible, T: FloatNumber> { + extend: bool, radius: T, join_builder: J, _phantom: PhantomData

, @@ -46,37 +39,23 @@ struct Builder, P: FloatPointCompatible, T: FloatNumber> impl + 'static, T: FloatNumber + 'static> OutlineBuilder { pub(super) fn new(radius: T, join: &LineJoin) -> OutlineBuilder { - let builder: Box> = if radius.is_expand() { - match join { - LineJoin::Miter(ratio) => Box::new(Builder { - radius, - join_builder: ExpandMiterJoinBuilder::new(*ratio, radius), - _phantom: Default::default(), - }), - LineJoin::Round(ratio) => Box::new(Builder { - radius, - join_builder: ExpandRoundJoinBuilder::new(*ratio, radius), - _phantom: Default::default(), - }), - LineJoin::Bevel => Box::new(Builder { - radius, - join_builder: ExpandBevelJoinBuilder {}, - _phantom: Default::default(), - }), - } - } else { + let extend = radius > T::from_float(0.0); + let builder: Box> = { match join { LineJoin::Miter(ratio) => Box::new(Builder { + extend, radius, join_builder: MiterJoinBuilder::new(*ratio, radius), _phantom: Default::default(), }), LineJoin::Round(ratio) => Box::new(Builder { + extend, radius, join_builder: RoundJoinBuilder::new(*ratio, radius), _phantom: Default::default(), }), LineJoin::Bevel => Box::new(Builder { + extend, radius, join_builder: BevelJoinBuilder {}, _phantom: Default::default(), @@ -106,11 +85,6 @@ impl + 'static, T: FloatNumber + 'static> OutlineBuil pub(super) fn additional_offset(&self, radius: T) -> T { self.builder.additional_offset(radius) } - - #[inline] - pub(super) fn is_shrink(&self) -> bool { - self.builder.radius() > T::from_float(0.0) - } } impl, P: FloatPointCompatible, T: FloatNumber> OutlineBuild @@ -128,12 +102,6 @@ impl, P: FloatPointCompatible, T: FloatNumber> OutlineBu } self.build(path, adapter, segments); - - // if self.radius.is_expand() { - // self.expand_build(path, adapter, segments); - // } else { - // self.shrink_build(path, adapter, segments); - // } } #[inline] @@ -145,70 +113,15 @@ impl, P: FloatPointCompatible, T: FloatNumber> OutlineBu fn additional_offset(&self, radius: T) -> T { self.join_builder.additional_offset(radius) } - - fn radius(&self) -> T { - self.radius - } } impl, P: FloatPointCompatible, T: FloatNumber> Builder { -/* - #[inline] - fn next_unique_point( - start: usize, - index: usize, - path: &[P], - adapter: &FloatPointAdapter, - ) -> (usize, IntPoint) { - let a = adapter.float_to_int(&path[start]); - for (j, p) in path.iter().enumerate().skip(index) { - let b = adapter.float_to_int(p); - if a != b { - return (j, b); - } - } - - (usize::MAX, IntPoint::EMPTY) - } - - fn shrink_build( - &self, - path: &[P], - adapter: &FloatPointAdapter, - segments: &mut Vec>, - ) { - // build segments only from points which are not equal in int space - let i0 = path.len() - 1; - let (i1, mut p1) = Self::next_unique_point(i0, 0, path, adapter); - if i1 == usize::MAX { - return; - } - - let start = Section::new(self.radius, &path[i0], &path[i1]); - let p0 = adapter.float_to_int(&path[i0]); - - let mut s0 = start.clone(); - segments.add_shrink_section(p0, p1, &s0, adapter); - - let (mut i, mut pi) = Self::next_unique_point(i1, i1 + 1, path, adapter); - while i != usize::MAX { - let si = Section::new(self.radius, &s0.b, &path[i]); - segments.add_shrink_section(p1, pi, &si, adapter); - - (i, pi) = Self::next_unique_point(i, i + 1, path, adapter); - s0 = si; - p1 = pi; - } - - self.join_builder.add_join(&s0, &start, adapter, segments); - } -*/ fn build( &self, path: &[P], adapter: &FloatPointAdapter, segments: &mut Vec>, - ) { + ) { let iter = path.iter().map(|p| adapter.float_to_int(p)); let mut uniq_segments = if let Some(iter) = UniqueSegmentsIter::new(iter) { iter @@ -225,121 +138,60 @@ impl, P: FloatPointCompatible, T: FloatNumber> Builder 0 { - // no join - segments.push_some(sk.a_segment()); - segments.push_some(sk.b_segment()); - } else { - self.join_builder.add_join(&s0, &si, adapter, segments); - } - - // self.join_builder.add_join(&s0, &si, adapter, segments); - // segments.add_expand_section(&si, adapter); - sp = si; + segments.push_some(si.top_segment()); + self.feed_join(&sk, &si, adapter, segments); + sk = si; } + self.feed_join(&sk, &s0, adapter, segments); } -} - -trait Expand { - fn is_expand(&self) -> bool; -} - -impl Expand for T { - fn is_expand(&self) -> bool { - *self <= T::from_float(0.0) - } -} -struct UniquePointsIter<'a, P, T> -where - P: FloatPointCompatible, - T: FloatNumber, -{ - path: &'a [P], - adapter: &'a FloatPointAdapter, - start_index: usize, - next_index: usize, - prev: IntPoint, -} -impl<'a, P, T> UniquePointsIter<'a, P, T> -where - P: FloatPointCompatible, - T: FloatNumber, -{ #[inline] - fn new(path: &'a [P], adapter: &'a FloatPointAdapter) -> Self { - let (start_index, prev) = if path.is_empty() { - (0, IntPoint::EMPTY) + fn feed_join( + &self, + s0: &OffsetSection, + s1: &OffsetSection, + adapter: &FloatPointAdapter, + segments: &mut Vec>, + ) { + let vi = s1.b - s1.a; + let vp = s0.b - s0.a; + + let cross = vi.cross_product(vp); + debug_assert!(cross != 0, "not possible! UniqueSegmentsIter guarantee it"); + let outer_corner = (cross > 0) == self.extend; + if outer_corner { + self.join_builder.add_join(&s0, &s1, adapter, segments); } else { - let start_index = path.len().saturating_sub(1); - let prev = adapter.float_to_int(&path[start_index]); - (start_index, prev) - }; - - Self { - path, - adapter, - start_index, - next_index: 0, - prev, - } - } -} - -impl<'a, P, T> Iterator for UniquePointsIter<'a, P, T> -where - P: FloatPointCompatible, - T: FloatNumber, -{ - type Item = (usize, IntPoint); - - #[inline] - fn next(&mut self) -> Option { - if self.path.len() < 2 { - return None; - } - - for (j, p) in self.path.iter().enumerate().skip(self.next_index) { - let b = self.adapter.float_to_int(p); - if self.prev != b { - self.prev = b; - self.next_index = j + 1; - return Some((j, b)); - } + // no join + segments.push_some(s0.a_segment()); + segments.push_some(s1.b_segment()); } - - None } } impl, T: FloatNumber> OffsetSection { #[inline] fn new(radius: T, s: &UniqueSegment, adapter: &FloatPointAdapter) -> Self - where P: FloatPointCompatible, T: FloatNumber, + where + P: FloatPointCompatible, + T: FloatNumber, { let a = adapter.int_to_float(&s.a); let b = adapter.int_to_float(&s.b); let ab = FloatPointMath::sub(&b, &a); let dir = FloatPointMath::normalize(&ab); - let ft = Math::ortho_and_scale(&dir, radius); + let t = Math::ortho_and_scale(&dir, radius); - let t = adapter.float_to_int(&ft); - - let a_top = s.a + t; - let b_top = s.b + t; + let at = FloatPointMath::add(&a, &t); + let bt = FloatPointMath::add(&b, &t); + let a_top = adapter.float_to_int(&at); + let b_top = adapter.float_to_int(&bt); Self { a: s.a, @@ -363,4 +215,4 @@ impl VecPushSome for Vec { self.push(v); } } -} \ No newline at end of file +} diff --git a/iOverlay/src/mesh/outline/builder_join_shrink.rs b/iOverlay/src/mesh/outline/builder_join_shrink.rs index e58c6ee..4ebac74 100644 --- a/iOverlay/src/mesh/outline/builder_join_shrink.rs +++ b/iOverlay/src/mesh/outline/builder_join_shrink.rs @@ -201,12 +201,12 @@ impl> JoinBuilder for RoundJoin adapter: &FloatPointAdapter, segments: &mut Vec>, ) { - let cross_product = FloatPointMath::cross_product(&s0.dir, &s1.dir); - let turn = cross_product >= T::from_float(0.0); - if turn == self.expand { - BevelJoinBuilder::join(s0, s1, adapter, segments); - return; - } + // let cross_product = FloatPointMath::cross_product(&s0.dir, &s1.dir); + // let turn = cross_product >= T::from_float(0.0); + // if turn == self.expand { + // BevelJoinBuilder::join(s0, s1, adapter, segments); + // return; + // } let dot_product = FloatPointMath::dot_product(&s0.dir, &s1.dir); if self.limit_dot_product < dot_product { @@ -225,9 +225,9 @@ impl> JoinBuilder for RoundJoin let rotator = Rotator::::with_angle(self.rot_dir * delta_angle); - let center = s0.b; + let center = adapter.int_to_float(&s0.b); let mut v = dir; - let mut a = adapter.float_to_int(&start); + let mut a = start; for _ in 1..n { v = rotator.rotate(&v); let p = FloatPointMath::add(¢er, &FloatPointMath::scale(&v, self.radius)); @@ -239,9 +239,8 @@ impl> JoinBuilder for RoundJoin } } - let b = adapter.float_to_int(&end); - if a != b { - segments.push(Segment::subject(a, b)); + if a != end { + segments.push(Segment::subject(a, end)); } } diff --git a/iOverlay/src/mesh/outline/offset.rs b/iOverlay/src/mesh/outline/offset.rs index 76300dd..574951c 100644 --- a/iOverlay/src/mesh/outline/offset.rs +++ b/iOverlay/src/mesh/outline/offset.rs @@ -1,6 +1,6 @@ use crate::core::extract::BooleanExtractionBuffer; use crate::core::fill_rule::FillRule; -use crate::core::overlay::ShapeType::{Clip, Subject}; +use crate::core::overlay::ShapeType::Subject; use crate::core::overlay::{ContourDirection, Overlay}; use crate::core::overlay_rule::OverlayRule; use crate::float::overlay::OverlayOptions; @@ -253,6 +253,7 @@ mod tests { use crate::mesh::style::{LineJoin, OutlineStyle}; use alloc::vec; use core::f32::consts::PI; + use i_shape::float::area::Area; #[test] fn test_doc() { @@ -332,6 +333,7 @@ mod tests { #[test] fn test_square_positive_offset_0() { let path = [[-5.0, -5.0f32], [5.0, -5.0], [5.0, 5.0], [-5.0, 5.0]]; + let original_sign = path.area().signum(); let style = OutlineStyle::new(1.0); let shapes = path.outline_fixed_scale(&style, 10.0).unwrap(); @@ -343,11 +345,15 @@ mod tests { let path = shape.first().unwrap(); assert_eq!(path.len(), 8); + + let result_sign = path.area().signum(); + assert_eq!(original_sign, result_sign); } #[test] fn test_square_positive_offset_1() { let path = [[-5.0, -5.0f32], [5.0, -5.0], [5.0, 5.0], [-5.0, 5.0]]; + let original_sign = path.area().signum(); let style = OutlineStyle::new(10.0); let shapes = path.outline_fixed_scale(&style, 10.0).unwrap(); @@ -359,12 +365,16 @@ mod tests { let path = shape.first().unwrap(); assert_eq!(path.len(), 8); + + let result_sign = path.area().signum(); + assert_eq!(original_sign, result_sign); } #[test] fn test_square_round_offset() { let path = [[-5.0, -5.0f32], [5.0, -5.0], [5.0, 5.0], [-5.0, 5.0]]; + let original_sign = path.area().signum(); let angle = PI / 3.0f32; let style = OutlineStyle::new(10.0).line_join(LineJoin::Round(angle)); @@ -372,11 +382,15 @@ mod tests { let shapes = path.outline_fixed_scale(&style, 10.0).unwrap(); assert_eq!(shapes.len(), 1); + + let result_sign = path.area().signum(); + assert_eq!(original_sign, result_sign); } #[test] fn test_square_negative_offset_0() { let path = [[-5.0, -5.0f32], [5.0, -5.0], [5.0, 5.0], [-5.0, 5.0]]; + let original_sign = path.area().signum(); let style = OutlineStyle::new(-1.0); let shapes = path.outline_fixed_scale(&style, 10.0).unwrap(); @@ -388,6 +402,9 @@ mod tests { let path = shape.first().unwrap(); assert_eq!(path.len(), 4); + + let result_sign = path.area().signum(); + assert_eq!(original_sign, result_sign); } #[test] diff --git a/iOverlay/src/mesh/outline/section.rs b/iOverlay/src/mesh/outline/section.rs index 22181c2..f403877 100644 --- a/iOverlay/src/mesh/outline/section.rs +++ b/iOverlay/src/mesh/outline/section.rs @@ -88,7 +88,7 @@ pub(super) struct OffsetSection, T: FloatNumber> { impl, T: FloatNumber> OffsetSection { #[inline] - pub(super) fn main_segment(&self) -> Option> { + pub(super) fn top_segment(&self) -> Option> { if self.a_top != self.b_top { Some(Segment::subject(self.a_top, self.b_top)) } else { From 88ba8ccc2d3eb8355d2f89831f44a0d7d9de243b Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Sun, 22 Mar 2026 10:17:09 +0300 Subject: [PATCH 07/10] pass miter tests --- iOverlay/src/mesh/miter.rs | 27 +- iOverlay/src/mesh/outline/builder.rs | 6 +- iOverlay/src/mesh/outline/builder_join.rs | 237 +++++++++++++++- .../src/mesh/outline/builder_join_expand.rs | 258 ------------------ .../src/mesh/outline/builder_join_shrink.rs | 257 ----------------- iOverlay/src/mesh/outline/mod.rs | 2 - iOverlay/src/mesh/outline/offset.rs | 21 ++ iOverlay/src/mesh/outline/section.rs | 75 +---- iOverlay/src/mesh/outline/uniq_iter.rs | 12 +- 9 files changed, 283 insertions(+), 612 deletions(-) delete mode 100644 iOverlay/src/mesh/outline/builder_join_expand.rs delete mode 100644 iOverlay/src/mesh/outline/builder_join_shrink.rs diff --git a/iOverlay/src/mesh/miter.rs b/iOverlay/src/mesh/miter.rs index 97920b7..be67996 100644 --- a/iOverlay/src/mesh/miter.rs +++ b/iOverlay/src/mesh/miter.rs @@ -27,6 +27,24 @@ impl Miter { return SharpMiter::Degenerate; } + let c = Self::peak(pa, pb, va, vb); + + let ic = adapter.float_to_int(&c); + + if ia == ic || ib == ic { + SharpMiter::AB(ia, ib) + } else { + SharpMiter::AcB(ia, ic, ib) + } + } + + #[inline] + pub(super) fn peak>( + pa: P, + pb: P, + va: P, + vb: P, + ) -> P { let pax = pa.x(); let pay = pa.y(); let pbx = pb.x(); @@ -47,14 +65,7 @@ impl Miter { let x = pax + k * vax; let y = pay + k * vay; - let c = P::from_xy(x, y); - - let ic = adapter.float_to_int(&c); - if ia == ic || ib == ic { - SharpMiter::AB(ia, ib) - } else { - SharpMiter::AcB(ia, ic, ib) - } + P::from_xy(x, y) } } diff --git a/iOverlay/src/mesh/outline/builder.rs b/iOverlay/src/mesh/outline/builder.rs index 4f945cc..0841aa4 100644 --- a/iOverlay/src/mesh/outline/builder.rs +++ b/iOverlay/src/mesh/outline/builder.rs @@ -1,6 +1,6 @@ use crate::mesh::math::Math; use crate::mesh::outline::builder_join::JoinBuilder; -use crate::mesh::outline::builder_join_shrink::{BevelJoinBuilder, MiterJoinBuilder, RoundJoinBuilder}; +use crate::mesh::outline::builder_join::{BevelJoinBuilder, MiterJoinBuilder, RoundJoinBuilder}; use crate::mesh::outline::section::OffsetSection; use crate::mesh::outline::uniq_iter::{UniqueSegment, UniqueSegmentsIter}; use crate::mesh::style::LineJoin; @@ -166,7 +166,9 @@ impl, P: FloatPointCompatible, T: FloatNumber> Builder 0) == self.extend; if outer_corner { - self.join_builder.add_join(&s0, &s1, adapter, segments); + if s0.b_top != s1.a_top { + self.join_builder.add_join(s0, s1, adapter, segments); + } } else { // no join segments.push_some(s0.a_segment()); diff --git a/iOverlay/src/mesh/outline/builder_join.rs b/iOverlay/src/mesh/outline/builder_join.rs index 5b6053c..bf8d040 100644 --- a/iOverlay/src/mesh/outline/builder_join.rs +++ b/iOverlay/src/mesh/outline/builder_join.rs @@ -1,10 +1,14 @@ +use crate::mesh::miter::Miter; +use crate::mesh::outline::section::OffsetSection; +use crate::mesh::rotator::Rotator; +use crate::segm::segment::Segment; use alloc::vec::Vec; +use core::f64::consts::PI; use i_float::adapter::FloatPointAdapter; use i_float::float::compatible::FloatPointCompatible; use i_float::float::number::FloatNumber; -use crate::mesh::outline::section::OffsetSection; +use i_float::float::vector::FloatPointMath; use crate::segm::boolean::ShapeCountBoolean; -use crate::segm::segment::Segment; pub(super) trait JoinBuilder, T: FloatNumber> { fn add_join( @@ -17,3 +21,232 @@ pub(super) trait JoinBuilder, T: FloatNumber> { fn capacity(&self) -> usize; fn additional_offset(&self, radius: T) -> T; } + +pub(super) struct BevelJoinBuilder; + +impl BevelJoinBuilder { + #[inline] + fn join>( + s0: &OffsetSection, + s1: &OffsetSection, + _adapter: &FloatPointAdapter, + segments: &mut Vec>, + ) { + if s0.b_top == s1.a_top { + return; + } + + segments.push(Segment::subject(s0.b_top, s1.a_top)); + } +} + +impl> JoinBuilder for BevelJoinBuilder { + #[inline] + fn add_join( + &self, + s0: &OffsetSection, + s1: &OffsetSection, + adapter: &FloatPointAdapter, + segments: &mut Vec>, + ) { + Self::join(s0, s1, adapter, segments); + } + + #[inline] + fn capacity(&self) -> usize { + 2 + } + + #[inline] + fn additional_offset(&self, radius: T) -> T { + // add extra 10% to avoid problems with floating point precision. + T::from_float(1.1) * radius + } +} + +pub(super) struct MiterJoinBuilder { + limit_dot_product: T, + max_offset: T, + max_length: T, +} + +impl MiterJoinBuilder { + pub(super) fn new(angle: T, radius: T) -> Self { + // angle - min possible angle + let fixed_angle = angle.to_f64().max(0.01); + let limit_dot_product = -T::from_float(fixed_angle.cos()); + + let half_angle = 0.5 * fixed_angle; + let tan = half_angle.tan(); + + let r = radius.to_f64().abs(); + let l = r / tan; + + // add extra 10% to avoid problems with floating point precision. + let max_offset = T::from_float(1.1 * (r * r + l * l).sqrt()); + let max_length = T::from_float(l); + + Self { + limit_dot_product, + max_offset, + max_length, + } + } +} + +impl> JoinBuilder for MiterJoinBuilder { + fn add_join( + &self, + s0: &OffsetSection, + s1: &OffsetSection, + adapter: &FloatPointAdapter, + segments: &mut Vec>, + ) { + + let ia = s0.b_top; + let ib = s1.a_top; + + let sq_len = ia.sqr_distance(ib); + if sq_len < 4 { + BevelJoinBuilder::join(s0, s1, adapter, segments); + return; + } + + let dot_product = FloatPointMath::dot_product(&s0.dir, &s1.dir); + let is_limited = self.limit_dot_product > dot_product; + + let pa = adapter.int_to_float(&ia); + let pb = adapter.int_to_float(&ib); + + if is_limited { + let (va, vb) = (s0.dir, s1.dir); + + let ax = pa.x() + self.max_length * va.x(); + let ay = pa.y() + self.max_length * va.y(); + let bx = pb.x() - self.max_length * vb.x(); + let by = pb.y() - self.max_length * vb.y(); + + let ac = P::from_xy(ax, ay); + let bc = P::from_xy(bx, by); + + let iac = adapter.float_to_int(&ac); + let ibc = adapter.float_to_int(&bc); + + if ia != iac { + segments.push(Segment::subject(ia, iac)); + } + if iac != ibc { + segments.push(Segment::subject(iac, ibc)); + } + if ibc != ib { + segments.push(Segment::subject(ibc, ib)); + } + } else { + let c = Miter::peak(pa, pb, s0.dir, s1.dir); + debug_assert!(ia != ib); + + let ic = adapter.float_to_int(&c); + if ia == ic || ib == ic { + segments.push(Segment::subject(ia, ib)) + } else { + segments.push(Segment::subject(ia, ic)); + segments.push(Segment::subject(ic, ib)); + } + } + } + + #[inline] + fn capacity(&self) -> usize { + 4 + } + + #[inline] + fn additional_offset(&self, _radius: T) -> T { + self.max_offset + } +} + +pub(super) struct RoundJoinBuilder { + inv_ratio: T, + average_count: usize, + radius: T, + limit_dot_product: T, + rot_dir: T, +} + +impl RoundJoinBuilder { + pub(super) fn new(ratio: T, radius: T) -> Self { + // ratio = A / R + let fixed_ratio = ratio.min(T::from_float(0.25 * PI)); + let limit_dot_product = fixed_ratio.cos(); + let average_count = (T::from_float(0.6 * PI) / fixed_ratio).to_usize() + 2; + let rot_dir = if radius >= T::from_float(0.0) { + T::from_float(-1.0) + } else { + T::from_float(1.0) + }; + + Self { + inv_ratio: T::from_float(1.0) / fixed_ratio, + average_count, + radius, + limit_dot_product, + rot_dir, + } + } +} +impl> JoinBuilder for RoundJoinBuilder { + fn add_join( + &self, + s0: &OffsetSection, + s1: &OffsetSection, + adapter: &FloatPointAdapter, + segments: &mut Vec>, + ) { + let dot_product = FloatPointMath::dot_product(&s0.dir, &s1.dir); + if self.limit_dot_product < dot_product { + BevelJoinBuilder::join(s0, s1, adapter, segments); + return; + } + + let angle = dot_product.acos(); + let n = (angle * self.inv_ratio).to_usize(); + let delta_angle = angle / T::from_usize(n); + + let start = s0.b_top; + let end = s1.a_top; + + let dir = P::from_xy(-s0.dir.y(), s0.dir.x()); + + let rotator = Rotator::::with_angle(self.rot_dir * delta_angle); + + let center = adapter.int_to_float(&s0.b); + let mut v = dir; + let mut a = start; + for _ in 1..n { + v = rotator.rotate(&v); + let p = FloatPointMath::add(¢er, &FloatPointMath::scale(&v, self.radius)); + + let b = adapter.float_to_int(&p); + if a != b { + segments.push(Segment::subject(a, b)); + a = b; + } + } + + if a != end { + segments.push(Segment::subject(a, end)); + } + } + + #[inline] + fn capacity(&self) -> usize { + self.average_count + } + + #[inline] + fn additional_offset(&self, radius: T) -> T { + // add extra 10% to avoid problems with floating point precision. + T::from_float(1.1) * radius + } +} diff --git a/iOverlay/src/mesh/outline/builder_join_expand.rs b/iOverlay/src/mesh/outline/builder_join_expand.rs deleted file mode 100644 index 206aa0d..0000000 --- a/iOverlay/src/mesh/outline/builder_join_expand.rs +++ /dev/null @@ -1,258 +0,0 @@ -// use crate::mesh::miter::{Miter, SharpMiter}; -// use crate::mesh::outline::section::Section; -// use crate::mesh::rotator::Rotator; -// use crate::segm::segment::Segment; -// use alloc::vec::Vec; -// use core::f64::consts::PI; -// use i_float::adapter::FloatPointAdapter; -// use i_float::float::compatible::FloatPointCompatible; -// use i_float::float::number::FloatNumber; -// use i_float::float::vector::FloatPointMath; -// use crate::mesh::outline::builder_join::JoinBuilder; -// use crate::segm::boolean::ShapeCountBoolean; -// -// pub(super) struct ExpandBevelJoinBuilder; -// -// impl ExpandBevelJoinBuilder { -// #[inline] -// fn join>( -// s0: &Section, -// s1: &Section, -// adapter: &FloatPointAdapter, -// segments: &mut Vec>, -// ) { -// let b0 = adapter.float_to_int(&s0.b_top); -// let a1 = adapter.float_to_int(&s1.a_top); -// if b0 == a1 { -// return; -// } -// -// segments.push(Segment::subject(b0, a1)); -// } -// } -// -// impl> JoinBuilder for ExpandBevelJoinBuilder { -// #[inline] -// fn add_join( -// &self, -// s0: &Section, -// s1: &Section, -// adapter: &FloatPointAdapter, -// segments: &mut Vec>, -// ) { -// Self::join(s0, s1, adapter, segments); -// } -// -// #[inline] -// fn capacity(&self) -> usize { -// 2 -// } -// -// #[inline] -// fn additional_offset(&self, radius: T) -> T { -// // add extra 10% to avoid problems with floating point precision. -// T::from_float(1.1) * radius -// } -// } -// -// pub(super) struct ExpandMiterJoinBuilder { -// limit_dot_product: T, -// expand: bool, -// max_offset: T, -// max_length: T, -// } -// -// impl ExpandMiterJoinBuilder { -// pub(super) fn new(angle: T, radius: T) -> Self { -// // angle - min possible angle -// let fixed_angle = angle.to_f64().max(0.01); -// let limit_dot_product = -T::from_float(fixed_angle.cos()); -// -// let half_angle = 0.5 * fixed_angle; -// let tan = half_angle.tan(); -// -// let r = radius.to_f64().abs(); -// let l = r / tan; -// -// // add extra 10% to avoid problems with floating point precision. -// let max_offset = T::from_float(1.1 * (r * r + l * l).sqrt()); -// let max_length = T::from_float(l); -// -// let expand = radius >= T::from_float(0.0); -// Self { -// limit_dot_product, -// max_offset, -// max_length, -// expand, -// } -// } -// } -// -// impl> JoinBuilder for ExpandMiterJoinBuilder { -// fn add_join( -// &self, -// s0: &Section, -// s1: &Section, -// adapter: &FloatPointAdapter, -// segments: &mut Vec>, -// ) { -// let turn = s0.is_positive_turn(s1); -// if turn == self.expand { -// ExpandBevelJoinBuilder::join(s0, s1, adapter, segments); -// return; -// } -// -// let pa = s0.b_top; -// let pb = s1.a_top; -// -// let ia = adapter.float_to_int(&pa); -// let ib = adapter.float_to_int(&pb); -// -// let sq_len = ia.sqr_distance(ib); -// if sq_len < 4 { -// ExpandBevelJoinBuilder::join(s0, s1, adapter, segments); -// return; -// } -// -// let dot_product = FloatPointMath::dot_product(&s0.dir, &s1.dir); -// let is_limited = self.limit_dot_product > dot_product; -// -// if is_limited { -// let (va, vb) = (s0.dir, s1.dir); -// -// let ax = pa.x() + self.max_length * va.x(); -// let ay = pa.y() + self.max_length * va.y(); -// let bx = pb.x() - self.max_length * vb.x(); -// let by = pb.y() - self.max_length * vb.y(); -// -// let ac = P::from_xy(ax, ay); -// let bc = P::from_xy(bx, by); -// -// let iac = adapter.float_to_int(&ac); -// let ibc = adapter.float_to_int(&bc); -// -// if ia != iac { -// segments.push(Segment::subject(ia, iac)); -// } -// if iac != ibc { -// segments.push(Segment::subject(iac, ibc)); -// } -// if ibc != ib { -// segments.push(Segment::subject(ibc, ib)); -// } -// } else { -// match Miter::sharp(pa, pb, s0.dir, s1.dir, adapter) { -// SharpMiter::AB(a, b) => segments.push(Segment::subject(a, b)), -// SharpMiter::AcB(a, c, b) => { -// segments.push(Segment::subject(a, c)); -// segments.push(Segment::subject(c, b)); -// } -// SharpMiter::Degenerate => {} -// } -// } -// } -// -// #[inline] -// fn capacity(&self) -> usize { -// 4 -// } -// -// #[inline] -// fn additional_offset(&self, _radius: T) -> T { -// self.max_offset -// } -// } -// -// pub(super) struct ExpandRoundJoinBuilder { -// inv_ratio: T, -// average_count: usize, -// radius: T, -// limit_dot_product: T, -// expand: bool, -// rot_dir: T, -// } -// -// impl ExpandRoundJoinBuilder { -// pub(super) fn new(ratio: T, radius: T) -> Self { -// // ratio = A / R -// let fixed_ratio = ratio.min(T::from_float(0.25 * PI)); -// let limit_dot_product = fixed_ratio.cos(); -// let average_count = (T::from_float(0.6 * PI) / fixed_ratio).to_usize() + 2; -// let (expand, rot_dir) = if radius >= T::from_float(0.0) { -// (true, T::from_float(-1.0)) -// } else { -// (false, T::from_float(1.0)) -// }; -// -// Self { -// inv_ratio: T::from_float(1.0) / fixed_ratio, -// average_count, -// radius, -// limit_dot_product, -// expand, -// rot_dir, -// } -// } -// } -// impl> JoinBuilder for ExpandRoundJoinBuilder { -// fn add_join( -// &self, -// s0: &Section, -// s1: &Section, -// adapter: &FloatPointAdapter, -// segments: &mut Vec>, -// ) { -// let turn = s0.is_positive_turn(s1); -// if turn == self.expand { -// ExpandBevelJoinBuilder::join(s0, s1, adapter, segments); -// return; -// } -// -// let dot_product = FloatPointMath::dot_product(&s0.dir, &s1.dir); -// if self.limit_dot_product < dot_product { -// ExpandBevelJoinBuilder::join(s0, s1, adapter, segments); -// return; -// } -// -// let angle = dot_product.acos(); -// let n = (angle * self.inv_ratio).to_usize(); -// let delta_angle = angle / T::from_usize(n); -// -// let start = s0.b_top; -// let end = s1.a_top; -// -// let dir = P::from_xy(-s0.dir.y(), s0.dir.x()); -// -// let rotator = Rotator::::with_angle(self.rot_dir * delta_angle); -// -// let center = s0.b; -// let mut v = dir; -// let mut a = adapter.float_to_int(&start); -// for _ in 1..n { -// v = rotator.rotate(&v); -// let p = FloatPointMath::add(¢er, &FloatPointMath::scale(&v, self.radius)); -// -// let b = adapter.float_to_int(&p); -// if a != b { -// segments.push(Segment::subject(a, b)); -// a = b; -// } -// } -// -// let b = adapter.float_to_int(&end); -// if a != b { -// segments.push(Segment::subject(a, b)); -// } -// } -// -// #[inline] -// fn capacity(&self) -> usize { -// self.average_count -// } -// -// #[inline] -// fn additional_offset(&self, radius: T) -> T { -// // add extra 10% to avoid problems with floating point precision. -// T::from_float(1.1) * radius -// } -// } diff --git a/iOverlay/src/mesh/outline/builder_join_shrink.rs b/iOverlay/src/mesh/outline/builder_join_shrink.rs deleted file mode 100644 index 4ebac74..0000000 --- a/iOverlay/src/mesh/outline/builder_join_shrink.rs +++ /dev/null @@ -1,257 +0,0 @@ -use crate::mesh::miter::{Miter, SharpMiter}; -use crate::mesh::outline::section::OffsetSection; -use crate::mesh::rotator::Rotator; -use crate::segm::segment::Segment; -use alloc::vec::Vec; -use core::f64::consts::PI; -use i_float::adapter::FloatPointAdapter; -use i_float::float::compatible::FloatPointCompatible; -use i_float::float::number::FloatNumber; -use i_float::float::vector::FloatPointMath; -use crate::mesh::outline::builder_join::JoinBuilder; -use crate::segm::boolean::ShapeCountBoolean; - -pub(super) struct BevelJoinBuilder; - -impl BevelJoinBuilder { - #[inline] - fn join>( - s0: &OffsetSection, - s1: &OffsetSection, - _adapter: &FloatPointAdapter, - segments: &mut Vec>, - ) { - if s0.b_top == s1.a_top { - return; - } - - segments.push(Segment::subject(s0.b_top, s1.a_top)); - } -} - -impl> JoinBuilder for BevelJoinBuilder { - #[inline] - fn add_join( - &self, - s0: &OffsetSection, - s1: &OffsetSection, - adapter: &FloatPointAdapter, - segments: &mut Vec>, - ) { - Self::join(s0, s1, adapter, segments); - } - - #[inline] - fn capacity(&self) -> usize { - 2 - } - - #[inline] - fn additional_offset(&self, radius: T) -> T { - // add extra 10% to avoid problems with floating point precision. - T::from_float(1.1) * radius - } -} - -pub(super) struct MiterJoinBuilder { - limit_dot_product: T, - expand: bool, - max_offset: T, - max_length: T, -} - -impl MiterJoinBuilder { - pub(super) fn new(angle: T, radius: T) -> Self { - // angle - min possible angle - let fixed_angle = angle.to_f64().max(0.01); - let limit_dot_product = -T::from_float(fixed_angle.cos()); - - let half_angle = 0.5 * fixed_angle; - let tan = half_angle.tan(); - - let r = radius.to_f64().abs(); - let l = r / tan; - - // add extra 10% to avoid problems with floating point precision. - let max_offset = T::from_float(1.1 * (r * r + l * l).sqrt()); - let max_length = T::from_float(l); - - let expand = radius >= T::from_float(0.0); - Self { - limit_dot_product, - max_offset, - max_length, - expand, - } - } -} - -impl> JoinBuilder for MiterJoinBuilder { - fn add_join( - &self, - s0: &OffsetSection, - s1: &OffsetSection, - adapter: &FloatPointAdapter, - segments: &mut Vec>, - ) { - let cross_product = FloatPointMath::cross_product(&s0.dir, &s1.dir); - let turn = cross_product >= T::from_float(0.0); - if turn == self.expand { - BevelJoinBuilder::join(s0, s1, adapter, segments); - return; - } - - let ia = s0.b_top; - let ib = s1.a_top; - - let sq_len = ia.sqr_distance(ib); - if sq_len < 4 { - BevelJoinBuilder::join(s0, s1, adapter, segments); - return; - } - - let dot_product = FloatPointMath::dot_product(&s0.dir, &s1.dir); - let is_limited = self.limit_dot_product > dot_product; - - let pa = adapter.int_to_float(&s0.a); - let pb = adapter.int_to_float(&s1.b); - - if is_limited { - let (va, vb) = (s0.dir, s1.dir); - - let ax = pa.x() + self.max_length * va.x(); - let ay = pa.y() + self.max_length * va.y(); - let bx = pb.x() - self.max_length * vb.x(); - let by = pb.y() - self.max_length * vb.y(); - - let ac = P::from_xy(ax, ay); - let bc = P::from_xy(bx, by); - - let iac = adapter.float_to_int(&ac); - let ibc = adapter.float_to_int(&bc); - - if ia != iac { - segments.push(Segment::subject(ia, iac)); - } - if iac != ibc { - segments.push(Segment::subject(iac, ibc)); - } - if ibc != ib { - segments.push(Segment::subject(ibc, ib)); - } - } else { - match Miter::sharp(pa, pb, s0.dir, s1.dir, adapter) { - SharpMiter::AB(a, b) => segments.push(Segment::subject(a, b)), - SharpMiter::AcB(a, c, b) => { - segments.push(Segment::subject(a, c)); - segments.push(Segment::subject(c, b)); - } - SharpMiter::Degenerate => {} - } - } - } - - #[inline] - fn capacity(&self) -> usize { - 4 - } - - #[inline] - fn additional_offset(&self, _radius: T) -> T { - self.max_offset - } -} - -pub(super) struct RoundJoinBuilder { - inv_ratio: T, - average_count: usize, - radius: T, - limit_dot_product: T, - expand: bool, - rot_dir: T, -} - -impl RoundJoinBuilder { - pub(super) fn new(ratio: T, radius: T) -> Self { - // ratio = A / R - let fixed_ratio = ratio.min(T::from_float(0.25 * PI)); - let limit_dot_product = fixed_ratio.cos(); - let average_count = (T::from_float(0.6 * PI) / fixed_ratio).to_usize() + 2; - let (expand, rot_dir) = if radius >= T::from_float(0.0) { - (true, T::from_float(-1.0)) - } else { - (false, T::from_float(1.0)) - }; - - Self { - inv_ratio: T::from_float(1.0) / fixed_ratio, - average_count, - radius, - limit_dot_product, - expand, - rot_dir, - } - } -} -impl> JoinBuilder for RoundJoinBuilder { - fn add_join( - &self, - s0: &OffsetSection, - s1: &OffsetSection, - adapter: &FloatPointAdapter, - segments: &mut Vec>, - ) { - // let cross_product = FloatPointMath::cross_product(&s0.dir, &s1.dir); - // let turn = cross_product >= T::from_float(0.0); - // if turn == self.expand { - // BevelJoinBuilder::join(s0, s1, adapter, segments); - // return; - // } - - let dot_product = FloatPointMath::dot_product(&s0.dir, &s1.dir); - if self.limit_dot_product < dot_product { - BevelJoinBuilder::join(s0, s1, adapter, segments); - return; - } - - let angle = dot_product.acos(); - let n = (angle * self.inv_ratio).to_usize(); - let delta_angle = angle / T::from_usize(n); - - let start = s0.b_top; - let end = s1.a_top; - - let dir = P::from_xy(-s0.dir.y(), s0.dir.x()); - - let rotator = Rotator::::with_angle(self.rot_dir * delta_angle); - - let center = adapter.int_to_float(&s0.b); - let mut v = dir; - let mut a = start; - for _ in 1..n { - v = rotator.rotate(&v); - let p = FloatPointMath::add(¢er, &FloatPointMath::scale(&v, self.radius)); - - let b = adapter.float_to_int(&p); - if a != b { - segments.push(Segment::subject(a, b)); - a = b; - } - } - - if a != end { - segments.push(Segment::subject(a, end)); - } - } - - #[inline] - fn capacity(&self) -> usize { - self.average_count - } - - #[inline] - fn additional_offset(&self, radius: T) -> T { - // add extra 10% to avoid problems with floating point precision. - T::from_float(1.1) * radius - } -} diff --git a/iOverlay/src/mesh/outline/mod.rs b/iOverlay/src/mesh/outline/mod.rs index 7d9de2f..7a3d0c6 100644 --- a/iOverlay/src/mesh/outline/mod.rs +++ b/iOverlay/src/mesh/outline/mod.rs @@ -1,7 +1,5 @@ mod builder; -mod builder_join_expand; pub mod offset; mod section; -mod builder_join_shrink; mod builder_join; mod uniq_iter; diff --git a/iOverlay/src/mesh/outline/offset.rs b/iOverlay/src/mesh/outline/offset.rs index 574951c..4a9d99f 100644 --- a/iOverlay/src/mesh/outline/offset.rs +++ b/iOverlay/src/mesh/outline/offset.rs @@ -449,6 +449,27 @@ mod tests { assert_eq!(shapes.len(), 0); } + #[test] + fn test_square_positive_mitter_offset() { + let path = [[-5.0, -5.0f32], [5.0, -5.0], [5.0, 5.0], [-5.0, 5.0]]; + let original_sign = path.area().signum(); + + let style = OutlineStyle::new(1.0).line_join(LineJoin::Miter(0.01)); + + let shapes = path.outline_fixed_scale(&style, 10.0).unwrap(); + + assert_eq!(shapes.len(), 1); + + let shape = shapes.first().unwrap(); + assert_eq!(shape.len(), 1); + + let path = shape.first().unwrap(); + assert_eq!(path.len(), 4); + + let result_sign = path.area().signum(); + assert_eq!(original_sign, result_sign); + } + #[test] fn test_rhombus_miter() { let path = [[-10.0, 0.0], [0.0, -10.0], [10.0, 0.0], [0.0, 10.0]]; diff --git a/iOverlay/src/mesh/outline/section.rs b/iOverlay/src/mesh/outline/section.rs index f403877..e00a7c5 100644 --- a/iOverlay/src/mesh/outline/section.rs +++ b/iOverlay/src/mesh/outline/section.rs @@ -1,80 +1,9 @@ -use crate::mesh::math::Math; +use crate::segm::boolean::ShapeCountBoolean; use crate::segm::segment::Segment; -use alloc::vec::Vec; use core::marker::PhantomData; -use i_float::adapter::FloatPointAdapter; use i_float::float::compatible::FloatPointCompatible; use i_float::float::number::FloatNumber; -use i_float::float::vector::FloatPointMath; use i_float::int::point::IntPoint; -use crate::segm::boolean::ShapeCountBoolean; - -#[derive(Debug, Clone)] -pub(super) struct Section, T: FloatNumber> { - pub(super) b: P, - pub(super) a_top: P, - pub(super) b_top: P, - pub(super) dir: P, - _phantom: PhantomData, -} - -impl> Section { - pub(super) fn new(radius: T, a: &P, b: &P) -> Self { - let dir = Math::normal(b, a); - let t = Math::ortho_and_scale(&dir, radius); - - let a_top = FloatPointMath::add(a, &t); - let b_top = FloatPointMath::add(b, &t); - - Section { - b: *b, - a_top, - b_top, - dir, - _phantom: Default::default(), - } - } - - pub(super) fn is_positive_turn(&self, other: &Self) -> bool { - let cross_product = FloatPointMath::cross_product(&self.dir, &other.dir); - cross_product >= T::from_float(0.0) - } -} - -pub(super) trait SectionToSegment> { - fn add_expand_section(&mut self, section: &Section, adapter: &FloatPointAdapter); - fn add_shrink_section(&mut self, a: IntPoint, b: IntPoint, section: &Section, adapter: &FloatPointAdapter); -} - -impl> SectionToSegment for Vec> { - fn add_expand_section(&mut self, section: &Section, adapter: &FloatPointAdapter) { - let a_top = adapter.float_to_int(§ion.a_top); - let b_top = adapter.float_to_int(§ion.b_top); - if a_top != b_top { - self.push(Segment::subject(a_top, b_top)); - } - } - - fn add_shrink_section(&mut self, a: IntPoint, b: IntPoint, section: &Section, adapter: &FloatPointAdapter) { - let a_top = adapter.float_to_int(§ion.a_top); - let b_top = adapter.float_to_int(§ion.b_top); - debug_assert!(a != b); - - // ToDO Subject+clip - self.push(Segment::subject(b, a)); - - if a != a_top { - self.push(Segment::subject(a, a_top)); - } - if b != b_top { - self.push(Segment::subject(b_top, b)); - } - if a_top != b_top { - self.push(Segment::subject(a_top, b_top)); - } - } -} - #[derive(Debug, Clone)] pub(super) struct OffsetSection, T: FloatNumber> { @@ -113,4 +42,4 @@ impl, T: FloatNumber> OffsetSection { None } } -} \ No newline at end of file +} diff --git a/iOverlay/src/mesh/outline/uniq_iter.rs b/iOverlay/src/mesh/outline/uniq_iter.rs index 9868b47..5053b2d 100644 --- a/iOverlay/src/mesh/outline/uniq_iter.rs +++ b/iOverlay/src/mesh/outline/uniq_iter.rs @@ -23,16 +23,8 @@ where pub(super) fn new(iter: I) -> Option { let mut iter = iter; - let mut p0 = if let Some(p) = iter.next() { - p - } else { - return None; - }; - let mut p1 = if let Some(p) = iter.find(|p| p0.ne(p)) { - p - } else { - return None; - }; + let mut p0 = iter.next()?; + let mut p1 = iter.find(|p| p0.ne(p))?; let q0 = p0; From 88b39df48a786f06d2e739e94de48b89bfc1b180 Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Sun, 22 Mar 2026 11:16:40 +0300 Subject: [PATCH 08/10] pass all tests --- iOverlay/src/mesh/outline/builder.rs | 4 +- iOverlay/src/mesh/outline/offset.rs | 66 +++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/iOverlay/src/mesh/outline/builder.rs b/iOverlay/src/mesh/outline/builder.rs index 0841aa4..d8dbb48 100644 --- a/iOverlay/src/mesh/outline/builder.rs +++ b/iOverlay/src/mesh/outline/builder.rs @@ -171,8 +171,8 @@ impl, P: FloatPointCompatible, T: FloatNumber> Builder Date: Sun, 22 Mar 2026 16:56:18 +0300 Subject: [PATCH 09/10] code clean more tests --- examples/tests/outline/test_0.json | 11 +-- examples/tests/outline/test_1.json | 53 +-------------- examples/tests/outline/test_2.json | 35 +++------- examples/tests/outline/test_3.json | 64 ++++++++++++++---- examples/tests/outline/test_4.json | 50 +++++++------- examples/tests/outline/test_5.json | 19 ++++++ examples/tests/outline/test_6.json | 33 +++++++++ examples/tests/outline/test_7.json | 32 +++++++++ iOverlay/Cargo.toml | 2 +- iOverlay/src/mesh/outline/builder_join.rs | 8 +-- iOverlay/src/mesh/outline/offset.rs | 82 +++++++++++++++++++++++ 11 files changed, 258 insertions(+), 131 deletions(-) create mode 100644 examples/tests/outline/test_5.json create mode 100644 examples/tests/outline/test_6.json create mode 100644 examples/tests/outline/test_7.json diff --git a/examples/tests/outline/test_0.json b/examples/tests/outline/test_0.json index 32d54fa..70de7e0 100644 --- a/examples/tests/outline/test_0.json +++ b/examples/tests/outline/test_0.json @@ -2,16 +2,7 @@ "scale": 1000.0, "outline": [ [ - [-15.0, -15.0], - [ 15.0, -15.0], - [ 15.0, 15.0], - [-15.0, 15.0] - ], - [ - [-5.0, -5.0], - [-5.0, 5.0], - [ 5.0, 5.0], - [ 5.0, -5.0] + [-5.0, 5.0], [-5.0, -5.0], [5.0, -5.0], [5.0, 5.0] ] ] } \ No newline at end of file diff --git a/examples/tests/outline/test_1.json b/examples/tests/outline/test_1.json index 3c6fd8a..c6318d1 100644 --- a/examples/tests/outline/test_1.json +++ b/examples/tests/outline/test_1.json @@ -2,58 +2,7 @@ "scale": 1000.0, "outline": [ [ - [0.0, 0.0], - [1.0, 0.0], - [1.0, 1.0], - [0.0, 1.0] - ], - [ - [1.0, 2.0], - [1.0, 3.0], - [0.0, 3.0], - [0.0, 2.0] - ], - [ - [1.0, 4.0], - [1.0, 5.0], - [0.0, 5.0], - [0.0, 4.0] - ], - [ - [3.0, 0.0], - [3.0, 1.0], - [2.0, 1.0], - [2.0, 0.0] - ], - [ - [3.0, 2.0], - [3.0, 3.0], - [2.0, 3.0], - [2.0, 2.0] - ], - [ - [3.0, 4.0], - [3.0, 5.0], - [2.0, 5.0], - [2.0, 4.0] - ], - [ - [5.0, 0.0], - [5.0, 1.0], - [4.0, 1.0], - [4.0, 0.0] - ], - [ - [5.0, 2.0], - [5.0, 3.0], - [4.0, 3.0], - [4.0, 2.0] - ], - [ - [5.0, 4.0], - [5.0, 5.0], - [4.0, 5.0], - [4.0, 4.0] + [-5.0, 5.0], [-5.0, -5.0], [5.0, -5.0], [5.0, 0.0], [0.0, 0.0], [0.0, 5.0] ] ] } \ No newline at end of file diff --git a/examples/tests/outline/test_2.json b/examples/tests/outline/test_2.json index 6431d0f..32d54fa 100644 --- a/examples/tests/outline/test_2.json +++ b/examples/tests/outline/test_2.json @@ -2,35 +2,16 @@ "scale": 1000.0, "outline": [ [ - [ 0.0, 0.0], - [25.0, 0.0], - [25.0, 25.0], - [ 0.0, 25.0] + [-15.0, -15.0], + [ 15.0, -15.0], + [ 15.0, 15.0], + [-15.0, 15.0] ], [ - [ 5.0, 10.0], - [10.0, 10.0], - [10.0, 5.0], - [ 5.0, 5.0] - ], - [ - [ 5.0, 20.0], - [10.0, 20.0], - [10.0, 15.0], - [ 5.0, 15.0] - - ], - [ - [15.0, 10.0], - [20.0, 10.0], - [20.0, 5.0], - [15.0, 5.0] - ], - [ - [15.0, 20.0], - [20.0, 20.0], - [20.0, 15.0], - [15.0, 15.0] + [-5.0, -5.0], + [-5.0, 5.0], + [ 5.0, 5.0], + [ 5.0, -5.0] ] ] } \ No newline at end of file diff --git a/examples/tests/outline/test_3.json b/examples/tests/outline/test_3.json index 65b276d..3c6fd8a 100644 --- a/examples/tests/outline/test_3.json +++ b/examples/tests/outline/test_3.json @@ -2,18 +2,58 @@ "scale": 1000.0, "outline": [ [ - [-10.0, -20.0], - [ 10.0, -20.0], - [ 10.0, -10.0], - [ 20.0, -10.0], - [ 20.0, 10.0], - [ 10.0, 10.0], - [ 10.0, 20.0], - [-10.0, 20.0], - [-10.0, 10.0], - [-20.0, 10.0], - [-20.0, -10.0], - [-10.0, -10.0] + [0.0, 0.0], + [1.0, 0.0], + [1.0, 1.0], + [0.0, 1.0] + ], + [ + [1.0, 2.0], + [1.0, 3.0], + [0.0, 3.0], + [0.0, 2.0] + ], + [ + [1.0, 4.0], + [1.0, 5.0], + [0.0, 5.0], + [0.0, 4.0] + ], + [ + [3.0, 0.0], + [3.0, 1.0], + [2.0, 1.0], + [2.0, 0.0] + ], + [ + [3.0, 2.0], + [3.0, 3.0], + [2.0, 3.0], + [2.0, 2.0] + ], + [ + [3.0, 4.0], + [3.0, 5.0], + [2.0, 5.0], + [2.0, 4.0] + ], + [ + [5.0, 0.0], + [5.0, 1.0], + [4.0, 1.0], + [4.0, 0.0] + ], + [ + [5.0, 2.0], + [5.0, 3.0], + [4.0, 3.0], + [4.0, 2.0] + ], + [ + [5.0, 4.0], + [5.0, 5.0], + [4.0, 5.0], + [4.0, 4.0] ] ] } \ No newline at end of file diff --git a/examples/tests/outline/test_4.json b/examples/tests/outline/test_4.json index 90c7f5c..6431d0f 100644 --- a/examples/tests/outline/test_4.json +++ b/examples/tests/outline/test_4.json @@ -2,31 +2,35 @@ "scale": 1000.0, "outline": [ [ - [2.0, 1.0], - [4.0, 1.0], - [5.0, 2.0], - [13.0, 2.0], - [13.0, 3.0], - [12.0, 3.0], - [12.0, 4.0], - [11.0, 4.0], - [11.0, 3.0], - [10.0, 3.0], - [9.0, 4.0], - [8.0, 4.0], - [8.0, 3.0], - [5.0, 3.0], - [5.0, 4.0], - [4.0, 5.0], - [2.0, 5.0], - [1.0, 4.0], - [1.0, 2.0] + [ 0.0, 0.0], + [25.0, 0.0], + [25.0, 25.0], + [ 0.0, 25.0] ], [ - [2.0, 4.0], - [4.0, 4.0], - [4.0, 2.0], - [2.0, 2.0] + [ 5.0, 10.0], + [10.0, 10.0], + [10.0, 5.0], + [ 5.0, 5.0] + ], + [ + [ 5.0, 20.0], + [10.0, 20.0], + [10.0, 15.0], + [ 5.0, 15.0] + + ], + [ + [15.0, 10.0], + [20.0, 10.0], + [20.0, 5.0], + [15.0, 5.0] + ], + [ + [15.0, 20.0], + [20.0, 20.0], + [20.0, 15.0], + [15.0, 15.0] ] ] } \ No newline at end of file diff --git a/examples/tests/outline/test_5.json b/examples/tests/outline/test_5.json new file mode 100644 index 0000000..65b276d --- /dev/null +++ b/examples/tests/outline/test_5.json @@ -0,0 +1,19 @@ +{ + "scale": 1000.0, + "outline": [ + [ + [-10.0, -20.0], + [ 10.0, -20.0], + [ 10.0, -10.0], + [ 20.0, -10.0], + [ 20.0, 10.0], + [ 10.0, 10.0], + [ 10.0, 20.0], + [-10.0, 20.0], + [-10.0, 10.0], + [-20.0, 10.0], + [-20.0, -10.0], + [-10.0, -10.0] + ] + ] +} \ No newline at end of file diff --git a/examples/tests/outline/test_6.json b/examples/tests/outline/test_6.json new file mode 100644 index 0000000..4d5ec00 --- /dev/null +++ b/examples/tests/outline/test_6.json @@ -0,0 +1,33 @@ +{ + "scale": 1000.0, + "outline": [ + [ + [-10.0, -20.0], + [ 10.0, -20.0], + [ 10.0, -10.0], + [ 20.0, -10.0], + [ 20.0, 10.0], + [ 10.0, 10.0], + [ 10.0, 20.0], + [-10.0, 20.0], + [-10.0, 10.0], + [-20.0, 10.0], + [-20.0, -10.0], + [-10.0, -10.0] + ], + [ + [ -5.0, -5.0], + [-10.0, -5.0], + [-10.0, 5.0], + [ -5.0, 5.0], + [ -5.0, 10.0], + [ 5.0, 10.0], + [ 5.0, 5.0], + [ 10.0, 5.0], + [ 10.0, -5.0], + [ 5.0, -5.0], + [ 5.0, -10.0], + [ -5.0, -10.0] + ] + ] +} \ No newline at end of file diff --git a/examples/tests/outline/test_7.json b/examples/tests/outline/test_7.json new file mode 100644 index 0000000..90c7f5c --- /dev/null +++ b/examples/tests/outline/test_7.json @@ -0,0 +1,32 @@ +{ + "scale": 1000.0, + "outline": [ + [ + [2.0, 1.0], + [4.0, 1.0], + [5.0, 2.0], + [13.0, 2.0], + [13.0, 3.0], + [12.0, 3.0], + [12.0, 4.0], + [11.0, 4.0], + [11.0, 3.0], + [10.0, 3.0], + [9.0, 4.0], + [8.0, 4.0], + [8.0, 3.0], + [5.0, 3.0], + [5.0, 4.0], + [4.0, 5.0], + [2.0, 5.0], + [1.0, 4.0], + [1.0, 2.0] + ], + [ + [2.0, 4.0], + [4.0, 4.0], + [4.0, 2.0], + [2.0, 2.0] + ] + ] +} \ No newline at end of file diff --git a/iOverlay/Cargo.toml b/iOverlay/Cargo.toml index 8bac9b8..e4c7c5e 100644 --- a/iOverlay/Cargo.toml +++ b/iOverlay/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "i_overlay" -version = "4.4.1" +version = "4.5.0" authors = ["Nail Sharipov "] edition = "2024" description = "Boolean Operations for 2D Polygons: Supports intersection, union, difference, xor, and self-intersections for all polygon varieties." diff --git a/iOverlay/src/mesh/outline/builder_join.rs b/iOverlay/src/mesh/outline/builder_join.rs index bf8d040..1a99902 100644 --- a/iOverlay/src/mesh/outline/builder_join.rs +++ b/iOverlay/src/mesh/outline/builder_join.rs @@ -1,6 +1,7 @@ use crate::mesh::miter::Miter; use crate::mesh::outline::section::OffsetSection; use crate::mesh::rotator::Rotator; +use crate::segm::boolean::ShapeCountBoolean; use crate::segm::segment::Segment; use alloc::vec::Vec; use core::f64::consts::PI; @@ -8,7 +9,6 @@ use i_float::adapter::FloatPointAdapter; use i_float::float::compatible::FloatPointCompatible; use i_float::float::number::FloatNumber; use i_float::float::vector::FloatPointMath; -use crate::segm::boolean::ShapeCountBoolean; pub(super) trait JoinBuilder, T: FloatNumber> { fn add_join( @@ -32,10 +32,7 @@ impl BevelJoinBuilder { _adapter: &FloatPointAdapter, segments: &mut Vec>, ) { - if s0.b_top == s1.a_top { - return; - } - + debug_assert_ne!(s0.b_top, s1.a_top, "must be validated before"); segments.push(Segment::subject(s0.b_top, s1.a_top)); } } @@ -102,7 +99,6 @@ impl> JoinBuilder for MiterJoin adapter: &FloatPointAdapter, segments: &mut Vec>, ) { - let ia = s0.b_top; let ib = s1.a_top; diff --git a/iOverlay/src/mesh/outline/offset.rs b/iOverlay/src/mesh/outline/offset.rs index fed2123..04fce8e 100644 --- a/iOverlay/src/mesh/outline/offset.rs +++ b/iOverlay/src/mesh/outline/offset.rs @@ -533,6 +533,38 @@ mod tests { assert_eq!(original_sign, result_sign); } + #[test] + fn test_inner_corner_negative_offset_0() { + let path = [[-5.0, 5.0], [-5.0, -5.0], [5.0, -5.0], [5.0, 0.0], [0.0, 0.0], [0.0, 5.0f32]]; + let original_sign = path.area().signum(); + + let style = OutlineStyle::new(-1.0); + + let shapes = path.outline_fixed_scale(&style, 10.0).unwrap(); + + assert_eq!(shapes.len(), 1); + + let shape = shapes.first().unwrap(); + assert_eq!(shape.len(), 1); + + let path = shape.first().unwrap(); + assert_eq!(path.len(), 7); + + let result_sign = path.area().signum(); + assert_eq!(original_sign, result_sign); + } + + #[test] + fn test_inner_corner_negative_offset_1() { + let path = [[-5.0, 5.0], [-5.0, -5.0], [5.0, -5.0], [5.0, 0.0], [0.0, 0.0], [0.0, 5.0f32]]; + + let style = OutlineStyle::new(-5.0); + + let shapes = path.outline_fixed_scale(&style, 10.0).unwrap(); + + assert_eq!(shapes.len(), 0); + } + #[test] fn test_rhombus_miter() { let path = [[-10.0, 0.0], [0.0, -10.0], [10.0, 0.0], [0.0, 10.0]]; @@ -603,6 +635,56 @@ mod tests { assert!(path.outline_fixed_scale(&style, f64::INFINITY).is_err()); } + #[test] + fn test_degenerate_0() { + let path = [ + [-10.0, 10.0], + [-10.0, -10.0], + [10.0, -10.0], + [10.0, 10.0f32] + ]; + let original_sign = path.area().signum(); + + let style = OutlineStyle::new(0.1); + let shapes = path.outline_fixed_scale(&style, 1.0).unwrap(); + + assert_eq!(shapes.len(), 1); + + let shape = shapes.first().unwrap(); + assert_eq!(shape.len(), 1); + + let path = shape.first().unwrap(); + assert_eq!(path.len(), 4); + + let result_sign = path.area().signum(); + assert_eq!(original_sign, result_sign); + } + + #[test] + fn test_degenerate_1() { + let path = [ + [-10.0, 10.0], + [-10.0, -10.0], + [10.0, -10.0], + [10.0, 10.0f32] + ]; + let original_sign = path.area().signum(); + + let style = OutlineStyle::new(1.0).line_join(LineJoin::Miter(0.01)); + let shapes = path.outline_fixed_scale(&style, 1.0).unwrap(); + + assert_eq!(shapes.len(), 1); + + let shape = shapes.first().unwrap(); + assert_eq!(shape.len(), 1); + + let path = shape.first().unwrap(); + assert_eq!(path.len(), 8); + + let result_sign = path.area().signum(); + assert_eq!(original_sign, result_sign); + } + #[test] fn test_zero_length_segment_0() { let path = [ From 1fe955297b45787cffc6a5313b231fd9b3a88e22 Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Sun, 22 Mar 2026 20:33:30 +0300 Subject: [PATCH 10/10] new buffer api --- iOverlay/Cargo.toml | 4 +- iOverlay/src/core/overlay.rs | 33 ++++ iOverlay/src/float/overlay.rs | 34 ++++ iOverlay/src/mesh/miter.rs | 7 +- iOverlay/src/mesh/outline/builder.rs | 2 - iOverlay/src/mesh/outline/mod.rs | 2 +- iOverlay/src/mesh/outline/offset.rs | 226 +++++++++++++++++++---- iOverlay/src/mesh/outline/uniq_iter.rs | 22 +-- iOverlay/src/mesh/overlay.rs | 4 +- iOverlay/src/mesh/stroke/builder.rs | 2 +- iOverlay/src/mesh/stroke/builder_cap.rs | 2 +- iOverlay/src/mesh/stroke/builder_join.rs | 2 +- iOverlay/src/mesh/stroke/offset.rs | 184 +++++++++++++++++- iOverlay/src/mesh/stroke/section.rs | 2 +- iOverlay/src/mesh/subject.rs | 2 +- 15 files changed, 462 insertions(+), 66 deletions(-) diff --git a/iOverlay/Cargo.toml b/iOverlay/Cargo.toml index e4c7c5e..2be8473 100644 --- a/iOverlay/Cargo.toml +++ b/iOverlay/Cargo.toml @@ -12,7 +12,7 @@ categories = ["algorithms", "graphics", "science::geo", "mathematics", "no-std"] [dependencies] i_float = { version = "~1.16.0" } -i_shape = { version = "~1.16.0" } +i_shape = { version = "~1.18.0" } i_tree = { version = "^0.18.0" } i_key_sort = { version = "^0.10.1" } @@ -38,4 +38,4 @@ rand = { version = "~0.10", features = ["alloc"] } #i_float = { path = "../../iFloat", features = ["serde"] } #i_shape = { path = "../../iShape", features = ["serde"] } i_float = { version = "~1.16.0", features = ["serde"] } -i_shape = { version = "~1.16.0", features = ["serde"] } +i_shape = { version = "~1.18.0", features = ["serde"] } diff --git a/iOverlay/src/core/overlay.rs b/iOverlay/src/core/overlay.rs index 5a09bea..4a334bf 100644 --- a/iOverlay/src/core/overlay.rs +++ b/iOverlay/src/core/overlay.rs @@ -365,6 +365,39 @@ impl Overlay { self.boolean_buffer = Some(buffer); shapes } + + /// Executes a single Boolean operation and writes the result into a flat contour buffer. + /// + /// This is a lower-allocation alternative to [`Self::overlay`] when you want flat contour + /// output (`points` + `ranges`) instead of nested `IntShapes`. + /// + /// - `overlay_rule`: The Boolean operation to apply. + /// - `fill_rule`: Fill rule used to determine interior regions. + /// - `output`: Destination [`FlatContoursBuffer`] that receives resulting contours. + /// Existing buffer contents are replaced. + #[inline] + pub fn overlay_into( + &mut self, + overlay_rule: OverlayRule, + fill_rule: FillRule, + output: &mut FlatContoursBuffer, + ) { + self.split_solver.split_segments(&mut self.segments, &self.solver); + if self.segments.is_empty() { + return; + } + let mut buffer = self.boolean_buffer.take().unwrap_or_default(); + self.graph_builder + .build_boolean_overlay( + fill_rule, + overlay_rule, + self.options, + &self.solver, + &self.segments, + ) + .extract_contours_into(overlay_rule, &mut buffer, output); + self.boolean_buffer = Some(buffer); + } } impl Default for IntOverlayOptions { diff --git a/iOverlay/src/float/overlay.rs b/iOverlay/src/float/overlay.rs index 57d725a..cd1f67b 100644 --- a/iOverlay/src/float/overlay.rs +++ b/iOverlay/src/float/overlay.rs @@ -13,6 +13,8 @@ use i_float::float::compatible::FloatPointCompatible; use i_float::float::number::FloatNumber; use i_float::float::rect::FloatRect; use i_shape::base::data::Shapes; +use i_shape::flat::buffer::FlatContoursBuffer; +use i_shape::flat::float::FloatFlatContoursBuffer; use i_shape::float::adapter::ShapesToFloat; use i_shape::float::despike::DeSpikeContour; use i_shape::float::simple::SimplifyContour; @@ -344,6 +346,38 @@ impl, T: FloatNumber> FloatOverlay { float } + + /// Executes a single Boolean operation and writes the result into a flat contour buffer. + /// + /// This is a lower-allocation alternative to [`Self::overlay`] when you want flat contour + /// output (`points` + `ranges`) instead of nested `Shapes

`. + /// + /// - `overlay_rule`: The Boolean operation to apply. + /// - `fill_rule`: Fill rule used to determine interior regions. + /// - `output`: Destination [`FloatFlatContoursBuffer`] that receives resulting contours. + /// Existing buffer contents are replaced. + #[inline] + pub fn overlay_into( + &mut self, + overlay_rule: OverlayRule, + fill_rule: FillRule, + output: &mut FloatFlatContoursBuffer

, + ) { + let preserve_output_collinear = self.overlay.options.preserve_output_collinear; + let mut int_output = FlatContoursBuffer::default(); + self.overlay + .overlay_into(overlay_rule, fill_rule, &mut int_output); + let iter = int_output.points.iter().map(|p| self.adapter.int_to_float(p)); + output.set_with_iter(iter, &int_output.ranges); + + if self.clean_result { + if preserve_output_collinear { + output.despike_contour(&self.adapter); + } else { + output.simplify_contour(&self.adapter); + } + } + } } impl Default for OverlayOptions { diff --git a/iOverlay/src/mesh/miter.rs b/iOverlay/src/mesh/miter.rs index be67996..c01c287 100644 --- a/iOverlay/src/mesh/miter.rs +++ b/iOverlay/src/mesh/miter.rs @@ -39,12 +39,7 @@ impl Miter { } #[inline] - pub(super) fn peak>( - pa: P, - pb: P, - va: P, - vb: P, - ) -> P { + pub(super) fn peak>(pa: P, pb: P, va: P, vb: P) -> P { let pax = pa.x(); let pay = pa.y(); let pbx = pb.x(); diff --git a/iOverlay/src/mesh/outline/builder.rs b/iOverlay/src/mesh/outline/builder.rs index d8dbb48..94a9f0a 100644 --- a/iOverlay/src/mesh/outline/builder.rs +++ b/iOverlay/src/mesh/outline/builder.rs @@ -126,14 +126,12 @@ impl, P: FloatPointCompatible, T: FloatNumber> Builder, T: FloatNumber> { /// Note: Outer boundary paths have a counterclockwise order, and holes have a clockwise order. fn outline(&self, style: &OutlineStyle) -> Shapes

; + /// Generates outline contours directly into a flat buffer. + /// + /// - `style`: Defines the outline properties, including offset, and joins. + /// - `output`: Destination buffer that receives resulting contours. Existing contents are replaced. + fn outline_into(&self, style: &OutlineStyle, output: &mut FloatFlatContoursBuffer

); + /// Generates an outline shapes for contours, or shapes with optional filtering. /// /// - `style`: Defines the outline properties, including offset, and joins. @@ -41,6 +48,18 @@ pub trait OutlineOffset, T: FloatNumber> { /// Note: Outer boundary paths have a **main_direction** order, and holes have an opposite to **main_direction** order. fn outline_custom(&self, style: &OutlineStyle, options: OverlayOptions) -> Shapes

; + /// Generates outline contours directly into a flat buffer with optional filtering. + /// + /// - `style`: Defines the outline properties, including offset, and joins. + /// - `options`: Adjust custom behavior. + /// - `output`: Destination buffer that receives resulting contours. Existing contents are replaced. + fn outline_custom_into( + &self, + style: &OutlineStyle, + options: OverlayOptions, + output: &mut FloatFlatContoursBuffer

, + ); + /// Generates an outline shapes for contours, or shapes with a fixed float-to-integer scale. /// /// - `style`: Defines the outline properties, including offset, and joins. @@ -55,6 +74,18 @@ pub trait OutlineOffset, T: FloatNumber> { scale: T, ) -> Result, FixedScaleOverlayError>; + /// Generates outline contours directly into a flat buffer with fixed float-to-integer scale. + /// + /// - `style`: Defines the outline properties, including offset, and joins. + /// - `scale`: Fixed float-to-integer scale. Use `scale = 1.0 / grid_size` if you prefer grid size semantics. + /// - `output`: Destination buffer that receives resulting contours. Existing contents are replaced on success. + fn outline_fixed_scale_into( + &self, + style: &OutlineStyle, + scale: T, + output: &mut FloatFlatContoursBuffer

, + ) -> Result<(), FixedScaleOverlayError>; + /// Generates an outline shapes for contours, or shapes with optional filtering and fixed scaling. /// /// - `style`: Defines the outline properties, including offset, and joins. @@ -70,6 +101,20 @@ pub trait OutlineOffset, T: FloatNumber> { options: OverlayOptions, scale: T, ) -> Result, FixedScaleOverlayError>; + + /// Generates outline contours directly into a flat buffer with optional filtering and fixed scaling. + /// + /// - `style`: Defines the outline properties, including offset, and joins. + /// - `options`: Adjust custom behavior. + /// - `scale`: Fixed float-to-integer scale. Use `scale = 1.0 / grid_size` if you prefer grid size semantics. + /// - `output`: Destination buffer that receives resulting contours. Existing contents are replaced on success. + fn outline_custom_fixed_scale_into( + &self, + style: &OutlineStyle, + options: OverlayOptions, + scale: T, + output: &mut FloatFlatContoursBuffer

, + ) -> Result<(), FixedScaleOverlayError>; } impl OutlineOffset for S @@ -82,6 +127,10 @@ where self.outline_custom(style, Default::default()) } + fn outline_into(&self, style: &OutlineStyle, output: &mut FloatFlatContoursBuffer

) { + self.outline_custom_into(style, Default::default(), output) + } + fn outline_custom(&self, style: &OutlineStyle, options: OverlayOptions) -> Shapes

{ match OutlineSolver::prepare(self, style) { Some(solver) => solver.build(self, options), @@ -89,6 +138,18 @@ where } } + fn outline_custom_into( + &self, + style: &OutlineStyle, + options: OverlayOptions, + output: &mut FloatFlatContoursBuffer

, + ) { + match OutlineSolver::prepare(self, style) { + Some(solver) => solver.build_into(self, options, output), + None => output.clear_and_reserve(0, 0), + } + } + fn outline_fixed_scale( &self, style: &OutlineStyle, @@ -97,6 +158,15 @@ where self.outline_custom_fixed_scale(style, Default::default(), scale) } + fn outline_fixed_scale_into( + &self, + style: &OutlineStyle, + scale: T, + output: &mut FloatFlatContoursBuffer

, + ) -> Result<(), FixedScaleOverlayError> { + self.outline_custom_fixed_scale_into(style, Default::default(), scale, output) + } + fn outline_custom_fixed_scale( &self, style: &OutlineStyle, @@ -111,6 +181,26 @@ where solver.apply_scale(s)?; Ok(solver.build(self, options)) } + + fn outline_custom_fixed_scale_into( + &self, + style: &OutlineStyle, + options: OverlayOptions, + scale: T, + output: &mut FloatFlatContoursBuffer

, + ) -> Result<(), FixedScaleOverlayError> { + let s = FixedScaleOverlayError::validate_scale(scale)?; + let mut solver = match OutlineSolver::prepare(self, style) { + Some(solver) => solver, + None => { + output.clear_and_reserve(0, 0); + return Ok(()); + } + }; + solver.apply_scale(s)?; + solver.build_into(self, options, output); + Ok(()) + } } struct OutlineSolver, T: FloatNumber> { @@ -173,7 +263,7 @@ impl + 'static, T: FloatNumber + 'static> OutlineSolv Ok(()) } - fn build>(self, source: &S, options: OverlayOptions) -> Shapes

{ + fn build_overlay>(&self, source: &S, options: OverlayOptions) -> Overlay { let total_capacity = self.outer_builder.capacity(self.points_count); let mut overlay = Overlay::new_custom( total_capacity, @@ -206,11 +296,7 @@ impl + 'static, T: FloatNumber + 'static> OutlineSolv offset_overlay.add_segments(&segments); if let Some(graph) = offset_overlay.build_graph_view(FillRule::Positive) { - graph.extract_contours_into( - OverlayRule::Subject, - &mut bool_buffer, - &mut flat_buffer, - ); + graph.extract_contours_into(OverlayRule::Subject, &mut bool_buffer, &mut flat_buffer); } } else { offset_overlay.options.output_direction = ContourDirection::Clockwise; @@ -220,22 +306,25 @@ impl + 'static, T: FloatNumber + 'static> OutlineSolv offset_overlay.add_segments(&segments); if let Some(graph) = offset_overlay.build_graph_view(FillRule::Negative) { - graph.extract_contours_into( - OverlayRule::Subject, - &mut bool_buffer, - &mut flat_buffer, - ); + graph.extract_contours_into(OverlayRule::Subject, &mut bool_buffer, &mut flat_buffer); } } overlay.add_flat_buffer(&flat_buffer, Subject); } + overlay + } + + fn build>(self, source: &S, options: OverlayOptions) -> Shapes

{ + let preserve_output_collinear = options.preserve_output_collinear; + let clean_result = options.clean_result; + let mut overlay = self.build_overlay(source, options); let shapes = overlay.overlay(OverlayRule::Subject, FillRule::Positive); - if options.clean_result { + if clean_result { let mut float = shapes.to_float(&self.adapter); - if options.preserve_output_collinear { + if preserve_output_collinear { float.despike_contour(&self.adapter); } else { float.simplify_contour(&self.adapter); @@ -245,6 +334,30 @@ impl + 'static, T: FloatNumber + 'static> OutlineSolv shapes.to_float(&self.adapter) } } + + fn build_into>( + self, + source: &S, + options: OverlayOptions, + output: &mut FloatFlatContoursBuffer

, + ) { + let preserve_output_collinear = options.preserve_output_collinear; + let clean_result = options.clean_result; + let mut overlay = self.build_overlay(source, options); + + let mut int_output = FlatContoursBuffer::default(); + overlay.overlay_into(OverlayRule::Subject, FillRule::Positive, &mut int_output); + let iter = int_output.points.iter().map(|p| self.adapter.int_to_float(p)); + output.set_with_iter(iter, &int_output.ranges); + + if clean_result { + if preserve_output_collinear { + output.despike_contour(&self.adapter); + } else { + output.simplify_contour(&self.adapter); + } + } + } } #[cfg(test)] @@ -253,6 +366,7 @@ mod tests { use crate::mesh::style::{LineJoin, OutlineStyle}; use alloc::vec; use core::f32::consts::PI; + use i_shape::flat::float::FloatFlatContoursBuffer; use i_shape::float::area::Area; #[test] @@ -330,6 +444,30 @@ mod tests { assert_eq!(path.len(), 4); } + #[test] + fn test_outline_into_ok() { + let path = [[-5.0, -5.0f32], [5.0, -5.0], [5.0, 5.0], [-5.0, 5.0]]; + let style = OutlineStyle::new(1.0); + let mut output = FloatFlatContoursBuffer::default(); + + path.outline_into(&style, &mut output); + + assert!(!output.is_empty()); + assert!(!output.ranges.is_empty()); + } + + #[test] + fn test_outline_fixed_scale_into_ok() { + let path = [[-5.0, -5.0f32], [5.0, -5.0], [5.0, 5.0], [-5.0, 5.0]]; + let style = OutlineStyle::new(1.0); + let mut output = FloatFlatContoursBuffer::default(); + + path.outline_fixed_scale_into(&style, 10.0, &mut output).unwrap(); + + assert!(!output.is_empty()); + assert!(!output.ranges.is_empty()); + } + #[test] fn test_square_positive_offset_0() { let path = [[-5.0, -5.0f32], [5.0, -5.0], [5.0, 5.0], [-5.0, 5.0]]; @@ -370,7 +508,6 @@ mod tests { assert_eq!(original_sign, result_sign); } - #[test] fn test_square_round_offset() { let path = [[-5.0, -5.0f32], [5.0, -5.0], [5.0, 5.0], [-5.0, 5.0]]; @@ -465,14 +602,21 @@ mod tests { let path = shape.first().unwrap(); assert_eq!(path.len(), 4); - + let result_sign = path.area().signum(); assert_eq!(original_sign, result_sign); } #[test] fn test_inner_corner_positive_offset_0() { - let path = [[-5.0, 5.0], [-5.0, -5.0], [5.0, -5.0], [5.0, 0.0], [0.0, 0.0], [0.0, 5.0f32]]; + let path = [ + [-5.0, 5.0], + [-5.0, -5.0], + [5.0, -5.0], + [5.0, 0.0], + [0.0, 0.0], + [0.0, 5.0f32], + ]; let original_sign = path.area().signum(); let style = OutlineStyle::new(1.0); @@ -493,7 +637,14 @@ mod tests { #[test] fn test_inner_corner_positive_offset_1() { - let path = [[-5.0, 5.0], [-5.0, -5.0], [5.0, -5.0], [5.0, 0.0], [0.0, 0.0], [0.0, 5.0f32]]; + let path = [ + [-5.0, 5.0], + [-5.0, -5.0], + [5.0, -5.0], + [5.0, 0.0], + [0.0, 0.0], + [0.0, 5.0f32], + ]; let original_sign = path.area().signum(); let style = OutlineStyle::new(5.0); @@ -514,7 +665,14 @@ mod tests { #[test] fn test_inner_corner_positive_offset_2() { - let path = [[-5.0, 5.0], [-5.0, -5.0], [5.0, -5.0], [5.0, 0.0], [0.0, 0.0], [0.0, 5.0f32]]; + let path = [ + [-5.0, 5.0], + [-5.0, -5.0], + [5.0, -5.0], + [5.0, 0.0], + [0.0, 0.0], + [0.0, 5.0f32], + ]; let original_sign = path.area().signum(); let style = OutlineStyle::new(20.0); @@ -535,7 +693,14 @@ mod tests { #[test] fn test_inner_corner_negative_offset_0() { - let path = [[-5.0, 5.0], [-5.0, -5.0], [5.0, -5.0], [5.0, 0.0], [0.0, 0.0], [0.0, 5.0f32]]; + let path = [ + [-5.0, 5.0], + [-5.0, -5.0], + [5.0, -5.0], + [5.0, 0.0], + [0.0, 0.0], + [0.0, 5.0f32], + ]; let original_sign = path.area().signum(); let style = OutlineStyle::new(-1.0); @@ -556,7 +721,14 @@ mod tests { #[test] fn test_inner_corner_negative_offset_1() { - let path = [[-5.0, 5.0], [-5.0, -5.0], [5.0, -5.0], [5.0, 0.0], [0.0, 0.0], [0.0, 5.0f32]]; + let path = [ + [-5.0, 5.0], + [-5.0, -5.0], + [5.0, -5.0], + [5.0, 0.0], + [0.0, 0.0], + [0.0, 5.0f32], + ]; let style = OutlineStyle::new(-5.0); @@ -637,12 +809,7 @@ mod tests { #[test] fn test_degenerate_0() { - let path = [ - [-10.0, 10.0], - [-10.0, -10.0], - [10.0, -10.0], - [10.0, 10.0f32] - ]; + let path = [[-10.0, 10.0], [-10.0, -10.0], [10.0, -10.0], [10.0, 10.0f32]]; let original_sign = path.area().signum(); let style = OutlineStyle::new(0.1); @@ -662,12 +829,7 @@ mod tests { #[test] fn test_degenerate_1() { - let path = [ - [-10.0, 10.0], - [-10.0, -10.0], - [10.0, -10.0], - [10.0, 10.0f32] - ]; + let path = [[-10.0, 10.0], [-10.0, -10.0], [10.0, -10.0], [10.0, 10.0f32]]; let original_sign = path.area().signum(); let style = OutlineStyle::new(1.0).line_join(LineJoin::Miter(0.01)); diff --git a/iOverlay/src/mesh/outline/uniq_iter.rs b/iOverlay/src/mesh/outline/uniq_iter.rs index 5053b2d..82bdd82 100644 --- a/iOverlay/src/mesh/outline/uniq_iter.rs +++ b/iOverlay/src/mesh/outline/uniq_iter.rs @@ -173,13 +173,8 @@ mod tests { assert!(!path.is_empty(), "path must not be empty"); for shift in 0..path.len() { - let uniq_iter = UniqueSegmentsIter::new( - path[shift..] - .iter() - .chain(path[..shift].iter()) - .copied(), - ) - .unwrap(); + let uniq_iter = + UniqueSegmentsIter::new(path[shift..].iter().chain(path[..shift].iter()).copied()).unwrap(); let segments: Vec<_> = uniq_iter.collect(); @@ -213,11 +208,7 @@ mod tests { } fn validate_pair(s0: &UniqueSegment, s1: &UniqueSegment, i: usize, j: usize) { - assert_eq!( - s0.b, s1.a, - "segment {} end does not match segment {} start", - i, j - ); + assert_eq!(s0.b, s1.a, "segment {} end does not match segment {} start", i, j); let v0 = s0.a - s0.b; let v1 = s1.a - s1.b; @@ -225,7 +216,12 @@ mod tests { let cross = v0.cross_product(v1); if cross == 0 { let dot = v0.dot_product(v1); - assert!(dot < 0, "segments {} and {} are collinear and same direction", i, j); + assert!( + dot < 0, + "segments {} and {} are collinear and same direction", + i, + j + ); } } } diff --git a/iOverlay/src/mesh/overlay.rs b/iOverlay/src/mesh/overlay.rs index 4349f81..64e759a 100644 --- a/iOverlay/src/mesh/overlay.rs +++ b/iOverlay/src/mesh/overlay.rs @@ -1,10 +1,10 @@ use crate::build::builder::GraphBuilder; use crate::core::graph::OverlayNode; +use crate::core::overlay::Overlay; +use crate::segm::boolean::ShapeCountBoolean; use crate::segm::segment::Segment; use crate::split::solver::SplitSolver; use alloc::vec::Vec; -use crate::core::overlay::Overlay; -use crate::segm::boolean::ShapeCountBoolean; impl Overlay { #[inline] diff --git a/iOverlay/src/mesh/stroke/builder.rs b/iOverlay/src/mesh/stroke/builder.rs index dffe9e0..8d6425a 100644 --- a/iOverlay/src/mesh/stroke/builder.rs +++ b/iOverlay/src/mesh/stroke/builder.rs @@ -2,13 +2,13 @@ use crate::mesh::stroke::builder_cap::CapBuilder; use crate::mesh::stroke::builder_join::{BevelJoinBuilder, JoinBuilder, MiterJoinBuilder, RoundJoinBuilder}; use crate::mesh::stroke::section::{Section, SectionToSegment}; use crate::mesh::style::{LineJoin, StrokeStyle}; +use crate::segm::boolean::ShapeCountBoolean; use crate::segm::segment::Segment; use alloc::boxed::Box; use alloc::vec::Vec; use i_float::adapter::FloatPointAdapter; use i_float::float::compatible::FloatPointCompatible; use i_float::float::number::FloatNumber; -use crate::segm::boolean::ShapeCountBoolean; trait StrokeBuild, T: FloatNumber> { fn build( diff --git a/iOverlay/src/mesh/stroke/builder_cap.rs b/iOverlay/src/mesh/stroke/builder_cap.rs index 3a98354..a001300 100644 --- a/iOverlay/src/mesh/stroke/builder_cap.rs +++ b/iOverlay/src/mesh/stroke/builder_cap.rs @@ -1,6 +1,7 @@ use crate::mesh::rotator::Rotator; use crate::mesh::stroke::section::Section; use crate::mesh::style::LineCap; +use crate::segm::boolean::ShapeCountBoolean; use crate::segm::segment::Segment; use alloc::vec; use alloc::vec::Vec; @@ -11,7 +12,6 @@ use i_float::float::compatible::FloatPointCompatible; use i_float::float::number::FloatNumber; use i_float::float::rect::FloatRect; use i_float::float::vector::FloatPointMath; -use crate::segm::boolean::ShapeCountBoolean; #[derive(Debug, Clone)] pub(super) struct CapBuilder { diff --git a/iOverlay/src/mesh/stroke/builder_join.rs b/iOverlay/src/mesh/stroke/builder_join.rs index 5c176b4..651428b 100644 --- a/iOverlay/src/mesh/stroke/builder_join.rs +++ b/iOverlay/src/mesh/stroke/builder_join.rs @@ -1,6 +1,7 @@ use crate::mesh::miter::{Miter, SharpMiter}; use crate::mesh::rotator::Rotator; use crate::mesh::stroke::section::Section; +use crate::segm::boolean::ShapeCountBoolean; use crate::segm::segment::Segment; use alloc::vec::Vec; use core::f64::consts::PI; @@ -8,7 +9,6 @@ use i_float::adapter::FloatPointAdapter; use i_float::float::compatible::FloatPointCompatible; use i_float::float::number::FloatNumber; use i_float::float::vector::FloatPointMath; -use crate::segm::boolean::ShapeCountBoolean; pub(super) trait JoinBuilder, T: FloatNumber> { fn add_join( diff --git a/iOverlay/src/mesh/stroke/offset.rs b/iOverlay/src/mesh/stroke/offset.rs index f4258d5..f66bcec 100644 --- a/iOverlay/src/mesh/stroke/offset.rs +++ b/iOverlay/src/mesh/stroke/offset.rs @@ -1,3 +1,6 @@ +use crate::core::fill_rule::FillRule; +use crate::core::overlay::Overlay; +use crate::core::overlay_rule::OverlayRule; use crate::float::overlay::OverlayOptions; use crate::float::scale::FixedScaleOverlayError; use crate::i_shape::source::resource::ShapeResource; @@ -10,12 +13,11 @@ use i_float::float::compatible::FloatPointCompatible; use i_float::float::number::FloatNumber; use i_float::float::rect::FloatRect; use i_shape::base::data::Shapes; +use i_shape::flat::buffer::FlatContoursBuffer; +use i_shape::flat::float::FloatFlatContoursBuffer; use i_shape::float::adapter::ShapesToFloat; use i_shape::float::despike::DeSpikeContour; use i_shape::float::simple::SimplifyContour; -use crate::core::fill_rule::FillRule; -use crate::core::overlay::Overlay; -use crate::core::overlay_rule::OverlayRule; pub trait StrokeOffset, T: FloatNumber> { /// Generates a stroke shapes for paths, contours, or shapes. @@ -29,6 +31,18 @@ pub trait StrokeOffset, T: FloatNumber> { /// Note: Outer boundary paths have a counterclockwise order, and holes have a clockwise order. fn stroke(&self, style: StrokeStyle, is_closed_path: bool) -> Shapes

; + /// Generates stroke contours directly into a flat buffer. + /// + /// - `style`: Defines the stroke properties, including width, line caps, and joins. + /// - `is_closed_path`: Specifies whether the path is closed (true) or open (false). + /// - `output`: Destination buffer that receives resulting contours. Existing contents are replaced. + fn stroke_into( + &self, + style: StrokeStyle, + is_closed_path: bool, + output: &mut FloatFlatContoursBuffer

, + ); + /// Generates a stroke mesh for paths, contours, or shapes with optional filtering and scaling. /// /// - `style`: Defines the stroke properties, including width, line caps, and joins. @@ -46,6 +60,20 @@ pub trait StrokeOffset, T: FloatNumber> { options: OverlayOptions, ) -> Shapes

; + /// Generates stroke contours directly into a flat buffer with custom overlay options. + /// + /// - `style`: Defines the stroke properties, including width, line caps, and joins. + /// - `is_closed_path`: Specifies whether the path is closed (true) or open (false). + /// - `options`: Adjust custom behavior. + /// - `output`: Destination buffer that receives resulting contours. Existing contents are replaced. + fn stroke_custom_into( + &self, + style: StrokeStyle, + is_closed_path: bool, + options: OverlayOptions, + output: &mut FloatFlatContoursBuffer

, + ); + /// Generates a stroke shapes for paths, contours, or shapes with a fixed float-to-integer scale. /// /// - `style`: Defines the stroke properties, including width, line caps, and joins. @@ -63,6 +91,20 @@ pub trait StrokeOffset, T: FloatNumber> { scale: T, ) -> Result, FixedScaleOverlayError>; + /// Generates stroke contours directly into a flat buffer with a fixed float-to-integer scale. + /// + /// - `style`: Defines the stroke properties, including width, line caps, and joins. + /// - `is_closed_path`: Specifies whether the path is closed (true) or open (false). + /// - `scale`: Fixed float-to-integer scale. Use `scale = 1.0 / grid_size` if you prefer grid size semantics. + /// - `output`: Destination buffer that receives resulting contours. Existing contents are replaced on success. + fn stroke_fixed_scale_into( + &self, + style: StrokeStyle, + is_closed_path: bool, + scale: T, + output: &mut FloatFlatContoursBuffer

, + ) -> Result<(), FixedScaleOverlayError>; + /// Generates a stroke mesh for paths, contours, or shapes with optional filtering and fixed scaling. /// /// - `style`: Defines the stroke properties, including width, line caps, and joins. @@ -81,6 +123,22 @@ pub trait StrokeOffset, T: FloatNumber> { options: OverlayOptions, scale: T, ) -> Result, FixedScaleOverlayError>; + + /// Generates stroke contours directly into a flat buffer with custom options and fixed scaling. + /// + /// - `style`: Defines the stroke properties, including width, line caps, and joins. + /// - `is_closed_path`: Specifies whether the path is closed (true) or open (false). + /// - `options`: Adjust custom behavior. + /// - `scale`: Fixed float-to-integer scale. Use `scale = 1.0 / grid_size` if you prefer grid size semantics. + /// - `output`: Destination buffer that receives resulting contours. Existing contents are replaced on success. + fn stroke_custom_fixed_scale_into( + &self, + style: StrokeStyle, + is_closed_path: bool, + options: OverlayOptions, + scale: T, + output: &mut FloatFlatContoursBuffer

, + ) -> Result<(), FixedScaleOverlayError>; } impl StrokeOffset for S @@ -93,6 +151,15 @@ where self.stroke_custom(style, is_closed_path, Default::default()) } + fn stroke_into( + &self, + style: StrokeStyle, + is_closed_path: bool, + output: &mut FloatFlatContoursBuffer

, + ) { + self.stroke_custom_into(style, is_closed_path, Default::default(), output) + } + fn stroke_fixed_scale( &self, style: StrokeStyle, @@ -102,6 +169,16 @@ where self.stroke_custom_fixed_scale(style, is_closed_path, Default::default(), scale) } + fn stroke_fixed_scale_into( + &self, + style: StrokeStyle, + is_closed_path: bool, + scale: T, + output: &mut FloatFlatContoursBuffer

, + ) -> Result<(), FixedScaleOverlayError> { + self.stroke_custom_fixed_scale_into(style, is_closed_path, Default::default(), scale, output) + } + fn stroke_custom( &self, style: StrokeStyle, @@ -114,6 +191,19 @@ where } } + fn stroke_custom_into( + &self, + style: StrokeStyle, + is_closed_path: bool, + options: OverlayOptions, + output: &mut FloatFlatContoursBuffer

, + ) { + match StrokeSolver::prepare(self, style) { + Some(solver) => solver.build_into(self, is_closed_path, options, output), + None => output.clear_and_reserve(0, 0), + } + } + fn stroke_custom_fixed_scale( &self, style: StrokeStyle, @@ -128,6 +218,26 @@ where solver.apply_scale(scale)?; Ok(solver.build(self, is_closed_path, options)) } + + fn stroke_custom_fixed_scale_into( + &self, + style: StrokeStyle, + is_closed_path: bool, + options: OverlayOptions, + scale: T, + output: &mut FloatFlatContoursBuffer

, + ) -> Result<(), FixedScaleOverlayError> { + let mut solver = match StrokeSolver::prepare(self, style) { + Some(solver) => solver, + None => { + output.clear_and_reserve(0, 0); + return Ok(()); + } + }; + solver.apply_scale(scale)?; + solver.build_into(self, is_closed_path, options, output); + Ok(()) + } } struct StrokeSolver, T: FloatNumber> { @@ -219,6 +329,48 @@ impl, T: 'static + FloatNumber> StrokeSolve float } + + fn build_into>( + self, + source: &S, + is_closed_path: bool, + options: OverlayOptions, + output: &mut FloatFlatContoursBuffer

, + ) { + let ir = self.adapter.len_float_to_int(self.r).abs(); + if ir <= 1 { + // offset is too small + output.clear_and_reserve(0, 0); + return; + } + + let capacity = self + .builder + .capacity(self.paths_count, self.points_count, is_closed_path); + let mut segments = Vec::with_capacity(capacity); + + for path in source.iter_paths() { + self.builder + .build(path, is_closed_path, &self.adapter, &mut segments); + } + + let mut overlay = Overlay::with_segments(segments); + overlay.options = options.int_with_adapter(&self.adapter); + + let mut int_output = FlatContoursBuffer::default(); + overlay.overlay_into(OverlayRule::Subject, FillRule::Positive, &mut int_output); + + let iter = int_output.points.iter().map(|p| self.adapter.int_to_float(p)); + output.set_with_iter(iter, &int_output.ranges); + + if options.clean_result { + if options.preserve_output_collinear { + output.despike_contour(&self.adapter); + } else { + output.simplify_contour(&self.adapter); + } + } + } } #[cfg(test)] @@ -228,6 +380,7 @@ mod tests { use alloc::vec; use alloc::vec::Vec; use core::f32::consts::PI; + use i_shape::flat::float::FloatFlatContoursBuffer; #[test] fn test_doc() { @@ -421,6 +574,31 @@ mod tests { assert_eq!(shapes.len(), 1); } + #[test] + fn test_stroke_into_ok() { + let path = [[0.0, 0.0], [10.0, 0.0]]; + let style = StrokeStyle::new(2.0); + let mut output = FloatFlatContoursBuffer::default(); + + path.stroke_into(style, false, &mut output); + + assert!(!output.is_empty()); + assert!(!output.ranges.is_empty()); + } + + #[test] + fn test_stroke_fixed_scale_into_ok() { + let path = [[0.0, 0.0], [10.0, 0.0]]; + let style = StrokeStyle::new(2.0); + let mut output = FloatFlatContoursBuffer::default(); + + path.stroke_fixed_scale_into(style, false, 10.0, &mut output) + .unwrap(); + + assert!(!output.is_empty()); + assert!(!output.ranges.is_empty()); + } + #[test] fn test_stroke_fixed_scale_invalid() { let path = [[0.0, 0.0], [10.0, 0.0]]; diff --git a/iOverlay/src/mesh/stroke/section.rs b/iOverlay/src/mesh/stroke/section.rs index 0009c80..9be4caf 100644 --- a/iOverlay/src/mesh/stroke/section.rs +++ b/iOverlay/src/mesh/stroke/section.rs @@ -1,4 +1,5 @@ use crate::mesh::math::Math; +use crate::segm::boolean::ShapeCountBoolean; use crate::segm::segment::Segment; use alloc::vec::Vec; use core::marker::PhantomData; @@ -6,7 +7,6 @@ use i_float::adapter::FloatPointAdapter; use i_float::float::compatible::FloatPointCompatible; use i_float::float::number::FloatNumber; use i_float::float::vector::FloatPointMath; -use crate::segm::boolean::ShapeCountBoolean; #[derive(Debug, Clone)] pub(super) struct Section, T: FloatNumber> { diff --git a/iOverlay/src/mesh/subject.rs b/iOverlay/src/mesh/subject.rs index 4aa9ab6..3189623 100644 --- a/iOverlay/src/mesh/subject.rs +++ b/iOverlay/src/mesh/subject.rs @@ -1,7 +1,7 @@ use crate::geom::x_segment::XSegment; +use crate::segm::boolean::ShapeCountBoolean; use crate::segm::segment::Segment; use i_float::int::point::IntPoint; -use crate::segm::boolean::ShapeCountBoolean; impl Segment { #[inline]