diff --git a/examples/tests/outline/test_0.json b/examples/tests/outline/test_0.json index 32d54fa5..70de7e0a 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 3c6fd8ab..c6318d18 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 6431d0f6..32d54fa5 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 65b276dd..3c6fd8ab 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 90c7f5c8..6431d0f6 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 00000000..65b276dd --- /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 00000000..4d5ec009 --- /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 00000000..90c7f5c8 --- /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 8bac9b86..2be84739 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." @@ -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/build/mod.rs b/iOverlay/src/build/mod.rs index 994e9894..af8d094d 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 32369d58..00000000 --- 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/core/extract.rs b/iOverlay/src/core/extract.rs index 5e343f4a..20870ef1 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/core/overlay.rs b/iOverlay/src/core/overlay.rs index 5a09bea0..4a334bf7 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 57d725ae..cd1f67bc 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/extract.rs b/iOverlay/src/mesh/extract.rs deleted file mode 100644 index ca8aa492..00000000 --- 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/graph.rs b/iOverlay/src/mesh/graph.rs deleted file mode 100644 index 169ceda4..00000000 --- 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/miter.rs b/iOverlay/src/mesh/miter.rs index 97920b78..c01c287a 100644 --- a/iOverlay/src/mesh/miter.rs +++ b/iOverlay/src/mesh/miter.rs @@ -27,6 +27,19 @@ 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 +60,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/mod.rs b/iOverlay/src/mesh/mod.rs index d6f71b7b..e56f2294 100644 --- a/iOverlay/src/mesh/mod.rs +++ b/iOverlay/src/mesh/mod.rs @@ -1,5 +1,3 @@ -mod extract; -pub(crate) mod graph; pub(crate) mod math; mod miter; pub mod outline; diff --git a/iOverlay/src/mesh/outline/builder.rs b/iOverlay/src/mesh/outline/builder.rs index 0df178e8..94a9f0ae 100644 --- a/iOverlay/src/mesh/outline/builder.rs +++ b/iOverlay/src/mesh/outline/builder.rs @@ -1,7 +1,10 @@ -use crate::mesh::outline::builder_join::{BevelJoinBuilder, JoinBuilder, MiterJoinBuilder, RoundJoinBuilder}; -use crate::mesh::outline::section::{Section, SectionToSegment}; +use crate::mesh::math::Math; +use crate::mesh::outline::builder_join::JoinBuilder; +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; -use crate::segm::offset::ShapeCountOffset; +use crate::segm::boolean::ShapeCountBoolean; use crate::segm::segment::Segment; use alloc::boxed::Box; use alloc::vec::Vec; @@ -9,13 +12,14 @@ 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; trait OutlineBuild, T: FloatNumber> { fn build( &self, path: &[P], adapter: &FloatPointAdapter, - segments: &mut Vec>, + segments: &mut Vec>, ); fn capacity(&self, points_count: usize) -> usize; @@ -27,6 +31,7 @@ pub(super) struct OutlineBuilder, T: FloatNumber> { } struct Builder, P: FloatPointCompatible, T: FloatNumber> { + extend: bool, radius: T, join_builder: J, _phantom: PhantomData

, @@ -34,22 +39,28 @@ 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 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(), + }), + } }; Self { builder } @@ -60,7 +71,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,35 +95,13 @@ impl, P: FloatPointCompatible, T: FloatNumber> OutlineBu &self, path: &[P], adapter: &FloatPointAdapter, - segments: &mut Vec>, + segments: &mut Vec>, ) { if path.len() < 2 { return; } - // 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); - if i1 == usize::MAX { - return; - } - - let start = Section::new(self.radius, &path[i0], &path[i1]); - let mut s0 = start.clone(); - segments.add_section(&s0, adapter); - - let mut i = i1; - i = Self::next_unique_point(i, i + 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); - - i = Self::next_unique_point(i, i + 1, path, adapter); - s0 = si; - } - - self.join_builder.add_join(&s0, &start, adapter, segments); + self.build(path, adapter, segments); } #[inline] @@ -127,16 +116,103 @@ impl, P: FloatPointCompatible, T: FloatNumber> OutlineBu } impl, P: FloatPointCompatible, T: FloatNumber> Builder { + 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 { + return; + }; + + let us0 = if let Some(us) = uniq_segments.next() { + us + } else { + return; + }; + + let s0 = OffsetSection::new(self.radius, &us0, adapter); + let mut sk = s0.clone(); + + segments.push_some(sk.top_segment()); + + for usi in uniq_segments { + let si = OffsetSection::new(self.radius, &usi, adapter); + segments.push_some(si.top_segment()); + self.feed_join(&sk, &si, adapter, segments); + sk = si; + } + self.feed_join(&sk, &s0, adapter, segments); + } + #[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 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 { + if s0.b_top != s1.a_top { + self.join_builder.add_join(s0, s1, adapter, segments); } + } else { + // no join + segments.push_some(s0.b_segment()); + segments.push_some(s1.a_segment()); } + } +} - usize::MAX +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 t = Math::ortho_and_scale(&dir, radius); + + 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, + 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); + } } } diff --git a/iOverlay/src/mesh/outline/builder_join.rs b/iOverlay/src/mesh/outline/builder_join.rs index 18351509..1a999028 100644 --- a/iOverlay/src/mesh/outline/builder_join.rs +++ b/iOverlay/src/mesh/outline/builder_join.rs @@ -1,7 +1,7 @@ -use crate::mesh::miter::{Miter, SharpMiter}; -use crate::mesh::outline::section::Section; +use crate::mesh::miter::Miter; +use crate::mesh::outline::section::OffsetSection; use crate::mesh::rotator::Rotator; -use crate::segm::offset::ShapeCountOffset; +use crate::segm::boolean::ShapeCountBoolean; use crate::segm::segment::Segment; use alloc::vec::Vec; use core::f64::consts::PI; @@ -13,10 +13,10 @@ use i_float::float::vector::FloatPointMath; pub(super) trait JoinBuilder, T: FloatNumber> { fn add_join( &self, - s0: &Section, - s1: &Section, + s0: &OffsetSection, + s1: &OffsetSection, adapter: &FloatPointAdapter, - segments: &mut Vec>, + segments: &mut Vec>, ); fn capacity(&self) -> usize; fn additional_offset(&self, radius: T) -> T; @@ -27,28 +27,13 @@ pub(super) struct BevelJoinBuilder; impl BevelJoinBuilder { #[inline] fn join>( - s0: &Section, - s1: &Section, - adapter: &FloatPointAdapter, - segments: &mut Vec>, + s0: &OffsetSection, + s1: &OffsetSection, + _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; - } - 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)); + debug_assert_ne!(s0.b_top, s1.a_top, "must be validated before"); + segments.push(Segment::subject(s0.b_top, s1.a_top)); } } @@ -56,10 +41,10 @@ impl> JoinBuilder for BevelJoin #[inline] fn add_join( &self, - s0: &Section, - s1: &Section, + s0: &OffsetSection, + s1: &OffsetSection, adapter: &FloatPointAdapter, - segments: &mut Vec>, + segments: &mut Vec>, ) { Self::join(s0, s1, adapter, segments); } @@ -78,7 +63,6 @@ impl> JoinBuilder for BevelJoin pub(super) struct MiterJoinBuilder { limit_dot_product: T, - expand: bool, max_offset: T, max_length: T, } @@ -99,12 +83,10 @@ impl MiterJoinBuilder { 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, } } } @@ -112,23 +94,13 @@ impl MiterJoinBuilder { impl> JoinBuilder for MiterJoinBuilder { fn add_join( &self, - s0: &Section, - s1: &Section, + s0: &OffsetSection, + s1: &OffsetSection, 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); - 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 ia = s0.b_top; + let ib = s1.a_top; let sq_len = ia.sqr_distance(ib); if sq_len < 4 { @@ -139,6 +111,9 @@ impl> JoinBuilder for MiterJoin 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); @@ -154,22 +129,24 @@ 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(ia, iac)); } if iac != ibc { - segments.push(Segment::bold_subject_ab(iac, ibc)); + segments.push(Segment::subject(iac, ibc)); } if ibc != ib { - segments.push(Segment::bold_subject_ab(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::bold_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)); - } - SharpMiter::Degenerate => {} + 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)); } } } @@ -190,7 +167,6 @@ pub(super) struct RoundJoinBuilder { average_count: usize, radius: T, limit_dot_product: T, - expand: bool, rot_dir: T, } @@ -200,10 +176,10 @@ impl RoundJoinBuilder { 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)) + let rot_dir = if radius >= T::from_float(0.0) { + T::from_float(-1.0) } else { - (false, T::from_float(1.0)) + T::from_float(1.0) }; Self { @@ -211,7 +187,6 @@ impl RoundJoinBuilder { average_count, radius, limit_dot_product, - expand, rot_dir, } } @@ -219,18 +194,11 @@ impl RoundJoinBuilder { impl> JoinBuilder for RoundJoinBuilder { fn add_join( &self, - s0: &Section, - s1: &Section, + s0: &OffsetSection, + s1: &OffsetSection, 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); - 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); @@ -248,23 +216,22 @@ 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)); let b = adapter.float_to_int(&p); if a != b { - segments.push(Segment::bold_subject_ab(a, b)); + segments.push(Segment::subject(a, b)); a = b; } } - let b = adapter.float_to_int(&end); - if a != b { - segments.push(Segment::bold_subject_ab(a, b)); + if a != end { + segments.push(Segment::subject(a, end)); } } diff --git a/iOverlay/src/mesh/outline/mod.rs b/iOverlay/src/mesh/outline/mod.rs index adf73534..b161db59 100644 --- a/iOverlay/src/mesh/outline/mod.rs +++ b/iOverlay/src/mesh/outline/mod.rs @@ -2,3 +2,4 @@ mod builder; mod builder_join; pub mod offset; mod section; +mod uniq_iter; diff --git a/iOverlay/src/mesh/outline/offset.rs b/iOverlay/src/mesh/outline/offset.rs index 214a0a4d..402d7568 100644 --- a/iOverlay/src/mesh/outline/offset.rs +++ b/iOverlay/src/mesh/outline/offset.rs @@ -1,12 +1,11 @@ -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::ShapeType::Subject; +use crate::core::overlay::{ContourDirection, 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; @@ -16,6 +15,7 @@ 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::int_area::IntArea; @@ -32,6 +32,12 @@ pub trait OutlineOffset, 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. @@ -42,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. @@ -56,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. @@ -71,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 @@ -83,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), @@ -90,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, @@ -98,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, @@ -112,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> { @@ -119,7 +208,6 @@ struct OutlineSolver, T: FloatNumber> { inner_builder: OutlineBuilder, adapter: FloatPointAdapter, points_count: usize, - paths_count: usize, } impl + 'static, T: FloatNumber + 'static> OutlineSolver { @@ -160,7 +248,6 @@ impl + 'static, T: FloatNumber + 'static> OutlineSolv inner_builder, adapter, points_count, - paths_count, }) } @@ -176,100 +263,68 @@ impl + 'static, T: FloatNumber + 'static> OutlineSolv Ok(()) } - fn build>(self, source: &S, options: OverlayOptions) -> Shapes

{ - let int_min_area = self.adapter.sqr_float_to_int(options.min_output_area).max(1); + 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, + 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); + offset_overlay.clear(); + segments.clear(); - 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() - } 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(), - ); + 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); - let mut offset_overlay = OffsetOverlay::new(128); + 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); } + } 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); - let (offset_graph, direction) = 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); - - offset_overlay.clear(); - offset_overlay.add_segments(&segments); - - let graph = offset_overlay - .build_graph_view_with_solver::(Default::default()); - (graph, ContourDirection::CounterClockwise) - } else { - let capacity = self.inner_builder.capacity(path.len()); - let additional = capacity.saturating_sub(segments.capacity()); - if additional > 0 { - segments.reserve(additional); - } - segments.clear(); - - self.inner_builder.build(path, &self.adapter, &mut segments); - - 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 bool_buffer, &mut flat_buffer); } } - overlay.overlay(OverlayRule::Subject, FillRule::Positive) - }; + overlay.add_flat_buffer(&flat_buffer, Subject); + } + + overlay + } - if options.clean_result { + 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 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); @@ -279,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)] @@ -287,6 +366,8 @@ 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] fn test_doc() { @@ -348,8 +429,69 @@ 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_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]]; + 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(), 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(); @@ -361,26 +503,73 @@ 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)); - let shapes = path.outline(&style); + 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(); 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_square_negative_offset() { + 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(-20.0); - let shapes = path.outline(&style); + 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(-11.0); + let shapes = path.outline_fixed_scale(&style, 10.0).unwrap(); assert_eq!(shapes.len(), 0); } @@ -392,7 +581,158 @@ 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); + } + + #[test] + fn test_square_positive_miter_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_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 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(), 11); + + let result_sign = path.area().signum(); + assert_eq!(original_sign, result_sign); + } + + #[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 original_sign = path.area().signum(); + + let style = OutlineStyle::new(5.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); + + let result_sign = path.area().signum(); + assert_eq!(original_sign, result_sign); + } + + #[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 original_sign = path.area().signum(); + + let style = OutlineStyle::new(20.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); + + let result_sign = path.area().signum(); + 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); } @@ -402,7 +742,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); @@ -424,7 +764,6 @@ mod tests { assert_eq!(shapes[0][1].len(), 4); } - // [[[[300.0, 300.0], [500.0, 300.0], [500.0, 500.0], [300.0, 500.0]]]] #[test] fn test_float_square_0() { let shape = vec![vec![ @@ -436,7 +775,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); @@ -468,6 +807,46 @@ 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 = [ diff --git a/iOverlay/src/mesh/outline/section.rs b/iOverlay/src/mesh/outline/section.rs index 01eafd7a..e00a7c58 100644 --- a/iOverlay/src/mesh/outline/section.rs +++ b/iOverlay/src/mesh/outline/section.rs @@ -1,50 +1,45 @@ -use crate::mesh::math::Math; -use crate::segm::offset::ShapeCountOffset; +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; #[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) 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, - _phantom: PhantomData, + pub(super) _phantom: PhantomData, } -impl> Section { - pub(crate) 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(), +impl, T: FloatNumber> OffsetSection { + #[inline] + pub(super) fn top_segment(&self) -> Option> { + if self.a_top != self.b_top { + Some(Segment::subject(self.a_top, self.b_top)) + } else { + None } } -} -pub(crate) trait SectionToSegment> { - fn add_section(&mut self, section: &Section, adapter: &FloatPointAdapter); -} + #[inline] + pub(super) fn a_segment(&self) -> Option> { + if self.a_top != self.a { + Some(Segment::subject(self.a, self.a_top)) + } else { + None + } + } -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)); + #[inline] + pub(super) fn b_segment(&self) -> Option> { + if self.b_top != self.b { + Some(Segment::subject(self.b_top, self.b)) + } else { + None } } } diff --git a/iOverlay/src/mesh/outline/uniq_iter.rs b/iOverlay/src/mesh/outline/uniq_iter.rs new file mode 100644 index 00000000..82bdd82c --- /dev/null +++ b/iOverlay/src/mesh/outline/uniq_iter.rs @@ -0,0 +1,227 @@ +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 = iter.next()?; + let mut p1 = iter.find(|p| p0.ne(p))?; + + 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/overlay.rs b/iOverlay/src/mesh/overlay.rs index bc31e7f7..64e759af 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::core::overlay::Overlay; +use crate::segm::boolean::ShapeCountBoolean; use crate::segm::segment::Segment; use crate::split::solver::SplitSolver; use alloc::vec::Vec; -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 b0f25353..8d6425a8 100644 --- a/iOverlay/src/mesh/stroke/builder.rs +++ b/iOverlay/src/mesh/stroke/builder.rs @@ -2,7 +2,7 @@ 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::boolean::ShapeCountBoolean; use crate::segm::segment::Segment; use alloc::boxed::Box; use alloc::vec::Vec; @@ -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 ada20819..a001300b 100644 --- a/iOverlay/src/mesh/stroke/builder_cap.rs +++ b/iOverlay/src/mesh/stroke/builder_cap.rs @@ -1,7 +1,7 @@ use crate::mesh::rotator::Rotator; use crate::mesh::stroke::section::Section; use crate::mesh::style::LineCap; -use crate::segm::offset::ShapeCountOffset; +use crate::segm::boolean::ShapeCountBoolean; use crate::segm::segment::Segment; use alloc::vec; use alloc::vec::Vec; @@ -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(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(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(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(a, last)); } #[inline] diff --git a/iOverlay/src/mesh/stroke/builder_join.rs b/iOverlay/src/mesh/stroke/builder_join.rs index 33760d9d..651428ba 100644 --- a/iOverlay/src/mesh/stroke/builder_join.rs +++ b/iOverlay/src/mesh/stroke/builder_join.rs @@ -1,7 +1,7 @@ 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::boolean::ShapeCountBoolean; use crate::segm::segment::Segment; use alloc::vec::Vec; use core::f64::consts::PI; @@ -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(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(iac, ia)); } if iac != ibc { - segments.push(Segment::bold_subject_ab(ibc, iac)); + segments.push(Segment::subject(ibc, iac)); } if ibc != ib { - segments.push(Segment::bold_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::bold_subject_ab(b, a)), + SharpMiter::AB(a, b) => segments.push(Segment::subject(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(c, a)); + segments.push(Segment::subject(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(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(b, a)); } } diff --git a/iOverlay/src/mesh/stroke/offset.rs b/iOverlay/src/mesh/stroke/offset.rs index a0f7538a..f66bcec4 100644 --- a/iOverlay/src/mesh/stroke/offset.rs +++ b/iOverlay/src/mesh/stroke/offset.rs @@ -1,8 +1,9 @@ -use crate::build::offset::PositiveSubjectOffsetStrategy; +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; -use crate::mesh::overlay::OffsetOverlay; use crate::mesh::stroke::builder::StrokeBuilder; use crate::mesh::stroke::offset::vec::Vec; use crate::mesh::style::StrokeStyle; @@ -12,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; @@ -28,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. @@ -45,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. @@ -62,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. @@ -80,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 @@ -92,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, @@ -101,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, @@ -113,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, @@ -127,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> { @@ -201,11 +312,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); @@ -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 acd7957d..9be4caf5 100644 --- a/iOverlay/src/mesh/stroke/section.rs +++ b/iOverlay/src/mesh/stroke/section.rs @@ -1,5 +1,5 @@ use crate::mesh::math::Math; -use crate::segm::offset::ShapeCountOffset; +use crate::segm::boolean::ShapeCountBoolean; use crate::segm::segment::Segment; use alloc::vec::Vec; use core::marker::PhantomData; @@ -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(b_top, a_top)); } if a_bot != b_bot { - self.push(Segment::bold_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 863e3aa6..3189623b 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::boolean::ShapeCountBoolean; use crate::segm::segment::Segment; use i_float::int::point::IntPoint; -impl Segment { +impl Segment { #[inline] - pub(crate) fn bold_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 }, - 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/mod.rs b/iOverlay/src/segm/mod.rs index 857ffd78..5f582876 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 9b82ad7c..00000000 --- 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, - } - } -}