From ce691bd1e8bb353530e3b8ab4471408c36bceedf Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Sun, 22 Feb 2026 14:59:33 +0300 Subject: [PATCH 1/9] up example to new version --- examples/overlay_editor/Cargo.toml | 8 ++++---- examples/overlay_editor/src/app/stroke/content.rs | 5 +++-- examples/overlay_editor/src/draw/shape.rs | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/examples/overlay_editor/Cargo.toml b/examples/overlay_editor/Cargo.toml index 7d2e313..ef6cd6e 100644 --- a/examples/overlay_editor/Cargo.toml +++ b/examples/overlay_editor/Cargo.toml @@ -26,10 +26,10 @@ log = "0.4.22" console_log = "^1.0.0" console_error_panic_hook = "^0" -i_mesh = "^0.3.0" -i_triangle = { version = "^0.35.0", features = ["serde"] } +#i_mesh = "^0.4.0" +#i_triangle = { version = "^0.35.0", features = ["serde"] } -#i_triangle = { path = "../../../../iShape/iTriangle/iTriangle", default-features = true, features = ["serde"] } -#i_mesh = { path = "../../../../iShape/iMesh/iMesh" } +i_triangle = { path = "../../../../iShape/iTriangle/iTriangle", default-features = true, features = ["serde"] } +i_mesh = { path = "../../../../iShape/iMesh/iMesh" } #ICED_BACKEND=wgpu cargo r -r diff --git a/examples/overlay_editor/src/app/stroke/content.rs b/examples/overlay_editor/src/app/stroke/content.rs index ec71743..b9f903a 100644 --- a/examples/overlay_editor/src/app/stroke/content.rs +++ b/examples/overlay_editor/src/app/stroke/content.rs @@ -13,6 +13,7 @@ use i_triangle::i_overlay::i_float::int::rect::IntRect; use iced::widget::{scrollable, Button, Column, Container, Row, Space, Text}; use iced::{Alignment, Length, Padding, Size, Vector}; use std::collections::HashMap; +use std::rc::Rc; pub(crate) struct StrokeState { pub(crate) test: usize, @@ -307,7 +308,7 @@ impl StrokeState { [ 3.0, 0.0], [-1.0, 2.0], ]; - style = style.start_cap(LineCap::Custom(points)) + style = style.start_cap(LineCap::Custom(Rc::from(points))) } } @@ -326,7 +327,7 @@ impl StrokeState { [ 3.0, 0.0], [-1.0, 2.0], ]; - style = style.end_cap(LineCap::Custom(points)) + style = style.end_cap(LineCap::Custom(Rc::from(points))) } } diff --git a/examples/overlay_editor/src/draw/shape.rs b/examples/overlay_editor/src/draw/shape.rs index 8447ff2..970a136 100644 --- a/examples/overlay_editor/src/draw/shape.rs +++ b/examples/overlay_editor/src/draw/shape.rs @@ -71,7 +71,7 @@ impl ShapeWidget { let validation = Validation::with_fill_rule(fill_rule.unwrap_or_default()); let triangulation = IntTriangulator::new(shapes.points_count(), validation, Default::default()) - .triangulate_shapes(shapes, false); + .triangulate_shapes(shapes); Self::fill_mesh_for_triangulation(triangulation, camera, offset, color) } @@ -91,7 +91,7 @@ impl ShapeWidget { let validation = Validation::with_fill_rule(fill_rule.unwrap_or_default()); let triangulation = IntTriangulator::new(paths.points_count(), validation, Default::default()) - .triangulate_shape(paths, false); + .triangulate_shape(paths); // let triangulation = paths.triangulate().into_triangulation(); From 817e880eb2cca69702507e2f09914a35214931a9 Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Sun, 22 Feb 2026 15:03:02 +0300 Subject: [PATCH 2/9] clippy --- iOverlay/src/core/extract.rs | 4 ++-- iOverlay/src/core/extract_ogc.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/iOverlay/src/core/extract.rs b/iOverlay/src/core/extract.rs index a0d0cc9..5e343f4 100644 --- a/iOverlay/src/core/extract.rs +++ b/iOverlay/src/core/extract.rs @@ -183,7 +183,7 @@ impl OverlayGraph<'_> { start_data: &StartPathData, clockwise: bool, visited_state: VisitState, - visited: &mut Vec, + visited: &mut [VisitState], points: &mut Vec, ) { let mut link_id = start_data.link_id; @@ -196,7 +196,7 @@ impl OverlayGraph<'_> { // 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); + 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 diff --git a/iOverlay/src/core/extract_ogc.rs b/iOverlay/src/core/extract_ogc.rs index 2ed0f22..07de181 100644 --- a/iOverlay/src/core/extract_ogc.rs +++ b/iOverlay/src/core/extract_ogc.rs @@ -216,8 +216,8 @@ impl OverlayGraph<'_> { &self, start_data: &StartPathData, clockwise: bool, - global_visited: &mut Vec, - contour_visited: &mut Vec, + global_visited: &mut [VisitState], + contour_visited: &mut [VisitState], points: &mut Vec, ) -> Option { let mut link_id = start_data.link_id; @@ -266,7 +266,7 @@ impl OverlayGraph<'_> { points.reserve_capacity(original_contour_len); self.find_contour( - &start_data, + start_data, !clockwise, VisitState::HullVisited, contour_visited, From 23a52796cde3e2b42d25e39c3c51b3b695f55b78 Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Sun, 22 Feb 2026 15:18:16 +0300 Subject: [PATCH 3/9] add crush case --- iOverlay/src/mesh/outline/offset.rs | 212 ++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) diff --git a/iOverlay/src/mesh/outline/offset.rs b/iOverlay/src/mesh/outline/offset.rs index 14ddab6..fdbe54c 100644 --- a/iOverlay/src/mesh/outline/offset.rs +++ b/iOverlay/src/mesh/outline/offset.rs @@ -486,4 +486,216 @@ mod tests { assert!(shape[0].len() < 1_000); }; } + + #[test] + fn test_real_case_0() { + let main = vec![ + [411162.0470393328, 5848155.806033095], + [411162.3299983172, 5848152.285037002], + [411162.44901687186, 5848149.446047744], + [411167.5609553484, 5848148.9709500875], + [411175.2629817156, 5848147.891970595], + [411186.7560237078, 5848146.501955947], + [411203.86503249686, 5848144.432009658], + [411214.44804030936, 5848143.314944228], + [411221.0470393328, 5848142.421999892], + [411227.85697585624, 5848141.499026259], + [411233.74100905936, 5848140.505007705], + [411238.4249690203, 5848139.349978408], + [411242.85697585624, 5848138.25305458], + [411249.1400569109, 5848136.395022353], + [411256.6129573015, 5848134.406008681], + [411262.81803542655, 5848132.916018447], + [411275.2460139422, 5848129.93298622], + [411284.6999934344, 5848127.662966689], + [411292.3739436297, 5848125.3869657125], + [411295.41703445, 5848123.3430204], + [411297.0079768328, 5848121.340945205], + [411297.43900710624, 5848119.1510037985], + [411293.11698562186, 5848105.54602333], + [411287.24100905936, 5848076.412966689], + [411286.6709407, 5848062.798953017], + [411286.98099929374, 5848053.410037002], + [411288.3879817156, 5848038.451052627], + [411294.0620539813, 5848006.396975478], + [411294.9409602312, 5847995.477053603], + [411295.2140315203, 5847988.534060439], + [411296.3359797625, 5847983.056033095], + [411297.8600276141, 5847976.624026259], + [411297.86698562186, 5847976.590945205], + [411298.2679865984, 5847974.952029189], + [411301.5709651141, 5847965.958010634], + [411303.7980158953, 5847955.29602333], + [411305.12797195, 5847948.927004775], + [411307.15897780936, 5847937.4279813375], + [411307.711956325, 5847934.313967666], + [411310.8889582781, 5847916.500979384], + [411311.8309748797, 5847911.5959500875], + [411312.3839533953, 5847898.51098915], + [411311.64603835624, 5847891.3459500875], + [411308.97904616874, 5847878.494997939], + [411303.793987575, 5847862.650027236], + [411301.5509455828, 5847857.5080594625], + [411297.4499934344, 5847849.958010634], + [411294.81303054374, 5847846.331057509], + [411281.3550227312, 5847828.88598915], + [411261.0709651141, 5847805.2369412985], + [411259.2100032, 5847804.3619412985], + [411258.0150569109, 5847803.80005165], + [411254.8910334734, 5847803.62805458], + [411251.86503249686, 5847805.3430204], + [411249.4499934344, 5847802.375002822], + [411248.2519953875, 5847800.202029189], + [411248.1909602312, 5847794.432009658], + [411253.23197585624, 5847785.8170194235], + [411255.7970393328, 5847788.224978408], + [411257.6870539813, 5847789.534060439], + [411259.8690608172, 5847790.333010634], + [411262.0520442156, 5847790.332034072], + [411268.8910334734, 5847788.8420438375], + [411269.4320490984, 5847790.333010634], + [411270.6329768328, 5847793.6369657125], + [411269.1820490984, 5847794.332034072], + [411267.65397292655, 5847796.224001845], + [411266.9259455828, 5847798.990969619], + [411267.6709407, 5847800.479006728], + [411268.7440608172, 5847802.625979384], + [411281.10795241874, 5847816.8850125875], + [411283.4420588641, 5847819.822024306], + [411294.9740412859, 5847834.3430204], + [411304.0699885515, 5847846.6369657125], + [411307.27103835624, 5847852.748049697], + [411309.8900569109, 5847857.912966689], + [411311.78300124686, 5847864.460940322], + [411313.6019709734, 5847869.698977431], + [411314.9850276141, 5847872.171999892], + [411317.53104812186, 5847875.446047744], + [411320.7970393328, 5847877.480959853], + [411325.86600905936, 5847879.2180204], + [411335.5499690203, 5847882.012942275], + [411368.4549983172, 5847890.26098915], + [411387.668987575, 5847895.4010037985], + [411397.7240412859, 5847898.576052627], + [411405.50297195, 5847902.333010634], + [411411.0599787859, 5847905.931033095], + [411418.5199397234, 5847911.750979384], + [411434.2660334734, 5847926.797976455], + [411436.82304030936, 5847934.838015517], + [411437.5780451922, 5847936.113039931], + [411434.0089533953, 5847946.812991103], + [411431.19804030936, 5847949.901980361], + [411411.5659602312, 5847985.880984267], + [411407.6529963641, 5847993.0510282125], + [411404.77994948905, 5848000.453982314], + [411402.93803054374, 5848007.354983291], + [411399.39701491874, 5848029.913943252], + [411392.9289973406, 5848080.029055556], + [411390.43998366874, 5848099.298953017], + [411388.8789485125, 5848106.744021377], + [411386.1429865984, 5848113.750979384], + [411383.2870295672, 5848120.22595497], + [411379.1269953875, 5848126.2580594625], + [411373.0499690203, 5848132.165041884], + [411368.44901687186, 5848135.741946181], + [411362.3199885515, 5848138.749026259], + [411354.7980158953, 5848141.105959853], + [411345.8729670672, 5848143.8990506735], + [411334.5969660906, 5848146.394045791], + [411322.7279475359, 5848149.957034072], + [411321.0050471453, 5848151.457034072], + [411319.7229426531, 5848152.791995009], + [411319.23698073905, 5848154.2740506735], + [411319.336956325, 5848156.656008681], + [411339.95194655936, 5848207.255007705], + [411351.7620051531, 5848236.444949111], + [411364.9020198015, 5848268.477053603], + [411376.5170100359, 5848297.156008681], + [411377.7340510515, 5848300.22595497], + [411395.8690608172, 5848345.97595497], + [411411.2689631609, 5848381.8459500875], + [411413.1310237078, 5848382.543948134], + [411405.27994948905, 5848384.8010282125], + [411332.1410334734, 5848206.078005752], + [411309.5890315203, 5848150.895998916], + [411307.8690608172, 5848147.239993056], + [411305.3419612078, 5848144.284060439], + [411301.6329768328, 5848141.729983291], + [411296.9740412859, 5848139.974978408], + [411293.77494460624, 5848139.145022353], + [411290.1350520281, 5848139.240969619], + [411276.68998366874, 5848140.473025283], + [411274.44901687186, 5848140.531008681], + [411266.961956325, 5848144.207034072], + [411247.6239436297, 5848146.937014541], + [411246.2670100359, 5848147.2369412985], + [411240.0699885515, 5848148.041018447], + [411234.7219660906, 5848150.973025283], + [411224.2479670672, 5848149.892947158], + [411223.6759455828, 5848148.834963759], + [411222.2269709734, 5848148.297976455], + [411213.2560237078, 5848149.380984267], + [411189.6649592547, 5848152.199953994], + [411162.0470393328, 5848155.806033095], + ]; + + let hole = vec![ + [411294.2500422625, 5848072.3189725485], + [411373.9180110125, 5848124.016970595], + [411377.22904616874, 5848118.990969619], + [411393.0859797625, 5848020.979983291], + [411394.7030451922, 5848005.639040908], + [411397.4359553484, 5848003.376955947], + [411431.1029475359, 5847937.537966689], + [411431.2639582781, 5847933.187991103], + [411314.8390315203, 5848005.308962783], + [411314.5590022234, 5848009.708010634], + [411309.3459895281, 5848009.447024306], + [411305.3719905047, 5848010.244997939], + [411294.2500422625, 5848072.3189725485], + ]; + + let shape= vec![main, hole]; + + let angle = 10.0f64 / (core::f64::consts::PI / 2.0f64); + let style = OutlineStyle::new(600.0).line_join(LineJoin::Round(angle)); + + if let Some(shape) = shape.outline(&style).first() { + assert!(shape[0].len() < 1_000); + }; + } + + #[test] + fn test_real_case_0_simplified() { + let main = vec![ + [410_000.0, 5840_000.0], + [420_000.0, 5840_000.0], + [420_000.0, 5850_000.0], + [410_000.0, 5850_000.0], + ]; + + let hole = vec![ + [411_294.2500422625, 5848_072.3189725485], + [411_373.9180110125, 5848_124.016970595], + [411_377.22904616874, 5848_118.990969619], + [411_393.0859797625, 5848_020.979983291], + [411_394.7030451922, 5848_005.639040908], + [411_397.4359553484, 5848_003.376955947], + [411_431.1029475359, 5847_937.537966689], + [411_431.2639582781, 5847_933.187991103], + [411_314.8390315203, 5848_005.308962783], + [411_314.5590022234, 5848_009.708010634], + [411_309.3459895281, 5848_009.447024306], + [411_305.3719905047, 5848_010.244997939], + [411_294.2500422625, 5848_072.3189725485], + ]; + + let shape= vec![main, hole]; + + let angle = 10.0f64 / (core::f64::consts::PI / 2.0f64); + let style = OutlineStyle::new(600.0).line_join(LineJoin::Round(angle)); + + if let Some(shape) = shape.outline(&style).first() { + assert!(shape[0].len() < 1_000); + }; + } } From 6123fdce50a297c21f054156af31791397d540da Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Tue, 24 Feb 2026 20:26:14 +0300 Subject: [PATCH 4/9] add test case --- iOverlay/src/mesh/extract.rs | 313 +++++++++++++++++++++++++++- iOverlay/src/mesh/outline/offset.rs | 33 ++- 2 files changed, 340 insertions(+), 6 deletions(-) diff --git a/iOverlay/src/mesh/extract.rs b/iOverlay/src/mesh/extract.rs index 8d8dff0..eaf8b95 100644 --- a/iOverlay/src/mesh/extract.rs +++ b/iOverlay/src/mesh/extract.rs @@ -1,6 +1,8 @@ use crate::bind::segment::{ContourIndex, IdSegment}; use crate::bind::solver::{JoinHoles, LeftBottomSegment}; -use crate::core::extract::{GraphContour, GraphUtil, StartPathData, Visit, VisitState}; +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; @@ -9,7 +11,9 @@ use crate::mesh::graph::OffsetGraph; use crate::segm::segment::SUBJ_TOP; use alloc::vec; use alloc::vec::Vec; +use i_shape::flat::buffer::FlatContoursBuffer; use i_shape::int::shape::{IntContour, IntShapes}; +use i_shape::util::reserve::Reserve; impl OffsetGraph<'_> { pub(crate) fn extract_offset(&self, main_direction: ContourDirection, min_area: u64) -> IntShapes { @@ -17,6 +21,19 @@ impl OffsetGraph<'_> { self.extract_offset_shapes(visited, main_direction, min_area) } + #[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, filter: Vec, @@ -105,6 +122,58 @@ impl OffsetGraph<'_> { 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); + + let mut contour = self.get_fill_contour(&start_data, direction, &mut bold, &mut buffer.visited); + if !bold { + link_index += 1; + continue; + } + + let (is_valid, is_modified) = contour.validate(min_area, true); + + if !is_valid { + link_index += 1; + continue; + } + + output.add_contour(buffer.points.as_slice()); + } + } + fn get_fill_contour( &self, start_data: &StartPathData, @@ -140,3 +209,245 @@ impl OffsetGraph<'_> { contour } } + +#[cfg(test)] +mod tests { + use crate::geom::x_segment::XSegment; + use crate::segm::offset::ShapeCountOffset; + use crate::segm::segment::Segment; + use alloc::vec; + use i_float::int::point::IntPoint; + use crate::core::overlay::ContourDirection; + use crate::mesh::overlay::OffsetOverlay; + + #[test] + fn test_0() { + let segments = vec![ + Segment:: { + // 0 + x_segment: XSegment { + a: IntPoint::new(100_884_823, -84_374_363), + b: IntPoint::new(103_800_375, -100_646_683), + }, + count: ShapeCountOffset { subj: 1, bold: true }, + }, + Segment:: { + // 1 + x_segment: XSegment { + a: IntPoint::new(-20_055_752, 25_821_939), + b: IntPoint::new(103_800_375, -100_646_683), + }, + count: ShapeCountOffset { + subj: -1, + bold: false, + }, + }, + Segment:: { + // 2 + x_segment: XSegment { + a: IntPoint::new(-20_055_752, 25_821_939), + b: IntPoint::new(-19_013_992, 25_612_755), + }, + count: ShapeCountOffset { subj: 1, bold: true }, + }, + Segment:: { + // 3 + x_segment: XSegment { + a: IntPoint::new(-57_843_404, 28_494_136), + b: IntPoint::new(-19_013_992, 25_612_755), + }, + count: ShapeCountOffset { + subj: -1, + bold: false, + }, + }, + Segment:: { + // 4 + x_segment: XSegment { + a: IntPoint::new(-57_843_404, 28_494_136), + b: IntPoint::new(-56_476_844, 28_562_552), + }, + count: ShapeCountOffset { subj: 1, bold: true }, + }, + Segment:: { + // 5 + x_segment: XSegment { + a: IntPoint::new(-56_476_844, 28_562_552), + b: IntPoint::new(56_113_245, -11_174_714), + }, + count: ShapeCountOffset { subj: 1, bold: true }, + }, + Segment:: { + // 6 + x_segment: XSegment { + a: IntPoint::new(56_113_245, -11_174_714), + b: IntPoint::new(108_356_443, -118_534_979), + }, + count: ShapeCountOffset { subj: 1, bold: true }, + }, + Segment:: { + // 7 + x_segment: XSegment { + a: IntPoint::new(108_356_443, -118_534_979), + b: IntPoint::new(108_429_851, -119_688_163), + }, + count: ShapeCountOffset { subj: 1, bold: true }, + }, + Segment:: { + // 8 + x_segment: XSegment { + a: IntPoint::new(34_289_766, 4_029_975), + b: IntPoint::new(108_429_851, -119_688_163), + }, + count: ShapeCountOffset { + subj: -1, + bold: false, + }, + }, + Segment:: { + // 9 + x_segment: XSegment { + a: IntPoint::new(34_289_766, 4_029_975), + b: IntPoint::new(64_809_862, -14_876_105), + }, + count: ShapeCountOffset { subj: 1, bold: true }, + }, + Segment:: { + // 10 + x_segment: XSegment { + a: IntPoint::new(-175_197_506, -154_404_209), + b: IntPoint::new(64_809_862, -14_876_105), + }, + count: ShapeCountOffset { + subj: -1, + bold: false, + }, + }, + Segment:: { + // 11 + x_segment: XSegment { + a: IntPoint::new(-175_239_714, -153_263_889), + b: IntPoint::new(-175_197_506, -154_404_209), + }, + count: ShapeCountOffset { subj: -1, bold: true }, + }, + Segment:: { + // 12 + x_segment: XSegment { + a: IntPoint::new(-175_239_714, -153_263_889), + b: IntPoint::new(-158_100_461, -219_055_730), + }, + count: ShapeCountOffset { subj: 1, bold: false }, + }, + Segment:: { + // 13 + x_segment: XSegment { + a: IntPoint::new(-166_926_061, -201_796_434), + b: IntPoint::new(-158_100_461, -219_055_730), + }, + count: ShapeCountOffset { subj: -1, bold: true }, + }, + Segment:: { + // 14 + x_segment: XSegment { + a: IntPoint::new(-166_926_061, -201_796_434), + b: IntPoint::new(-127_176_915, -251_351_325), + }, + count: ShapeCountOffset { subj: 1, bold: false }, + }, + Segment:: { + // 15 + x_segment: XSegment { + a: IntPoint::new(-127_893_331, -250_758_333), + b: IntPoint::new(-127_176_915, -251_351_325), + }, + count: ShapeCountOffset { subj: -1, bold: true }, + }, + Segment:: { + // 16 + x_segment: XSegment { + a: IntPoint::new(-184_022_779, -146_081_734), + b: IntPoint::new(-127_893_331, -250_758_333), + }, + count: ShapeCountOffset { + subj: -1, + bold: false, + }, + }, + Segment:: { + // 17 + x_segment: XSegment { + a: IntPoint::new(-184_446_683, -142_060_198), + b: IntPoint::new(-184_022_779, -146_081_734), + }, + count: ShapeCountOffset { subj: -1, bold: true }, + }, + Segment:: { + // 18 + x_segment: XSegment { + a: IntPoint::new(-184_446_683, -142_060_198), + b: IntPoint::new(-183_294_322, -150_692_526), + }, + count: ShapeCountOffset { subj: 1, bold: false }, + }, + Segment:: { + // 19 + x_segment: XSegment { + a: IntPoint::new(-187_451_122, -124_999_534), + b: IntPoint::new(-183_294_322, -150_692_526), + }, + count: ShapeCountOffset { subj: -1, bold: true }, + }, + Segment:: { + // 20 + x_segment: XSegment { + a: IntPoint::new(-187_451_122, -124_999_534), + b: IntPoint::new(-163_529_925, -186_407_681), + }, + count: ShapeCountOffset { subj: 1, bold: false }, + }, + Segment:: { + // 21 + x_segment: XSegment { + a: IntPoint::new(-164_397_893, -185_090_145), + b: IntPoint::new(-163_529_925, -186_407_681), + }, + count: ShapeCountOffset { subj: -1, bold: true }, + }, + Segment:: { + // 22 + x_segment: XSegment { + a: IntPoint::new(-164_397_893, -185_090_145), + b: IntPoint::new(52_567_258, -230_502_654), + }, + count: ShapeCountOffset { subj: 1, bold: false }, + }, + Segment:: { + // 23 + x_segment: XSegment { + a: IntPoint::new(31_682_778, -244_054_974), + b: IntPoint::new(52_567_258, -230_502_654), + }, + count: ShapeCountOffset { subj: -1, bold: true }, + }, + Segment:: { + // 24 + x_segment: XSegment { + a: IntPoint::new(31_682_778, -244_054_974), + b: IntPoint::new(100_884_823, -84_374_363), + }, + count: ShapeCountOffset { subj: 1, bold: false }, + }, + ]; + + let mut overlay = OffsetOverlay::new(128); + overlay.add_segments(&segments); + + let shapes = overlay + .build_graph_view_with_solver(Default::default()) + .map(|graph| graph.extract_offset(ContourDirection::CounterClockwise, 0)) + .unwrap_or_default(); + + assert!(!shapes.is_empty()) + } +} diff --git a/iOverlay/src/mesh/outline/offset.rs b/iOverlay/src/mesh/outline/offset.rs index fdbe54c..cd86030 100644 --- a/iOverlay/src/mesh/outline/offset.rs +++ b/iOverlay/src/mesh/outline/offset.rs @@ -369,7 +369,19 @@ mod tests { } #[test] - fn test_square_offset() { + fn test_square_round_offset() { + let path = [[-5.0, -5.0f32], [5.0, -5.0], [5.0, 5.0], [-5.0, 5.0]]; + + let angle = PI / 3.0f32; + let style = OutlineStyle::new(10.0).line_join(LineJoin::Round(angle)); + + let shapes = path.outline(&style); + + assert_eq!(shapes.len(), 0); + } + + #[test] + fn test_square_negative_offset() { let path = [[-5.0, -5.0f32], [5.0, -5.0], [5.0, 5.0], [-5.0, 5.0]]; let style = OutlineStyle::new(-20.0); @@ -378,6 +390,18 @@ mod tests { assert_eq!(shapes.len(), 0); } + #[test] + fn test_square_negative_round_offset() { + let path = [[-5.0, -5.0f32], [5.0, -5.0], [5.0, 5.0], [-5.0, 5.0]]; + + let angle = PI / 3.0f32; + let style = OutlineStyle::new(-20.0).line_join(LineJoin::Round(angle)); + + let shapes = path.outline(&style); + + 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]]; @@ -667,9 +691,9 @@ mod tests { #[test] fn test_real_case_0_simplified() { let main = vec![ - [410_000.0, 5840_000.0], - [420_000.0, 5840_000.0], - [420_000.0, 5850_000.0], + [410_000.0, 5847_000.0], + [413_000.0, 5847_000.0], + [413_000.0, 5850_000.0], [410_000.0, 5850_000.0], ]; @@ -686,7 +710,6 @@ mod tests { [411_314.5590022234, 5848_009.708010634], [411_309.3459895281, 5848_009.447024306], [411_305.3719905047, 5848_010.244997939], - [411_294.2500422625, 5848_072.3189725485], ]; let shape= vec![main, hole]; From bba49a2dbee5cce976037f8733bda7f9e8d2fb7c Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Sun, 1 Mar 2026 17:01:51 +0300 Subject: [PATCH 5/9] extract contours instead of shapes --- iOverlay/src/build/boolean.rs | 69 ------ iOverlay/src/build/mod.rs | 2 +- iOverlay/src/build/offset.rs | 30 ++- iOverlay/src/core/link.rs | 1 - iOverlay/src/mesh/extract.rs | 313 ++++------------------------ iOverlay/src/mesh/outline/offset.rs | 59 +++--- iOverlay/src/mesh/overlay.rs | 8 +- iOverlay/src/mesh/stroke/offset.rs | 5 +- 8 files changed, 109 insertions(+), 378 deletions(-) diff --git a/iOverlay/src/build/boolean.rs b/iOverlay/src/build/boolean.rs index 8cc136e..c6b3a92 100644 --- a/iOverlay/src/build/boolean.rs +++ b/iOverlay/src/build/boolean.rs @@ -274,19 +274,6 @@ impl BooleanFillFilter for SegmentFill { } impl OverlayLinkFilter for [OverlayLink] { - #[inline] - fn filter_by_overlay(&self, overlay_rule: OverlayRule) -> Vec { - match overlay_rule { - OverlayRule::Subject => filter_subject(self), - OverlayRule::Clip => filter_clip(self), - OverlayRule::Intersect => filter_intersect(self), - OverlayRule::Union => filter_union(self), - OverlayRule::Difference => filter_difference(self), - OverlayRule::Xor => filter_xor(self), - OverlayRule::InverseDifference => filter_inverse_difference(self), - } - } - #[inline] fn filter_by_overlay_into(&self, overlay_rule: OverlayRule, buffer: &mut Vec) { match overlay_rule { @@ -301,62 +288,6 @@ impl OverlayLinkFilter for [OverlayLink] { } } -#[inline] -fn filter_subject(links: &[OverlayLink]) -> Vec { - links - .iter() - .map(|link| VisitState::new(!link.fill.is_subject())) - .collect() -} - -#[inline] -fn filter_clip(links: &[OverlayLink]) -> Vec { - links - .iter() - .map(|link| VisitState::new(!link.fill.is_clip())) - .collect() -} - -#[inline] -fn filter_intersect(links: &[OverlayLink]) -> Vec { - links - .iter() - .map(|link| VisitState::new(!link.fill.is_intersect())) - .collect() -} - -#[inline] -fn filter_union(links: &[OverlayLink]) -> Vec { - links - .iter() - .map(|link| VisitState::new(!link.fill.is_union())) - .collect() -} - -#[inline] -fn filter_difference(links: &[OverlayLink]) -> Vec { - links - .iter() - .map(|link| VisitState::new(!link.fill.is_difference())) - .collect() -} - -#[inline] -fn filter_inverse_difference(links: &[OverlayLink]) -> Vec { - links - .iter() - .map(|link| VisitState::new(!link.fill.is_inverse_difference())) - .collect() -} - -#[inline] -fn filter_xor(links: &[OverlayLink]) -> Vec { - links - .iter() - .map(|link| VisitState::new(!link.fill.is_xor())) - .collect() -} - #[inline] fn filter_subject_into(links: &[OverlayLink], buffer: &mut Vec) { buffer.clear(); diff --git a/iOverlay/src/build/mod.rs b/iOverlay/src/build/mod.rs index c757ecb..994e989 100644 --- a/iOverlay/src/build/mod.rs +++ b/iOverlay/src/build/mod.rs @@ -1,7 +1,7 @@ pub(crate) mod boolean; pub(crate) mod builder; mod graph; -mod offset; +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 index 6b1b2e9..32369d5 100644 --- a/iOverlay/src/build/offset.rs +++ b/iOverlay/src/build/offset.rs @@ -9,12 +9,12 @@ use crate::segm::segment::{Segment, SegmentFill}; impl GraphBuilder { #[inline] - pub(crate) fn build_offset( + pub(crate) fn build_offset>( &mut self, solver: &Solver, segments: &[Segment], ) -> OffsetGraph<'_> { - self.build_fills_with_strategy::(solver, segments); + self.build_fills_with_strategy::(solver, segments); self.build_links_all(segments); self.offset_graph(solver) } @@ -29,10 +29,11 @@ impl GraphBuilder { } } -struct SubjectOffsetStrategy; +pub(crate) struct PositiveSubjectOffsetStrategy; +pub(crate) struct NegativeSubjectOffsetStrategy; const BOLD_BIT: usize = 2; -impl FillStrategy for SubjectOffsetStrategy { +impl FillStrategy for PositiveSubjectOffsetStrategy { #[inline(always)] fn add_and_fill(this: ShapeCountOffset, bot: ShapeCountOffset) -> (ShapeCountOffset, SegmentFill) { let top_subj = bot.subj + this.subj; @@ -53,6 +54,27 @@ impl FillStrategy for SubjectOffsetStrategy { } } +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 { diff --git a/iOverlay/src/core/link.rs b/iOverlay/src/core/link.rs index ca11646..bd67256 100644 --- a/iOverlay/src/core/link.rs +++ b/iOverlay/src/core/link.rs @@ -29,6 +29,5 @@ impl OverlayLink { } pub(crate) trait OverlayLinkFilter { - fn filter_by_overlay(&self, fill_rule: OverlayRule) -> Vec; fn filter_by_overlay_into(&self, overlay_rule: OverlayRule, buffer: &mut Vec); } diff --git a/iOverlay/src/mesh/extract.rs b/iOverlay/src/mesh/extract.rs index eaf8b95..ca8aa49 100644 --- a/iOverlay/src/mesh/extract.rs +++ b/iOverlay/src/mesh/extract.rs @@ -11,14 +11,21 @@ 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::{IntContour, IntShapes}; +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) -> IntShapes { - let visited = self.links.filter_by_overlay(OverlayRule::Subject); - self.extract_offset_shapes(visited, main_direction, min_area) + 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] @@ -36,28 +43,28 @@ impl OffsetGraph<'_> { fn extract_offset_shapes( &self, - filter: Vec, main_direction: ContourDirection, min_area: u64, + buffer: &mut BooleanExtractionBuffer, ) -> IntShapes { let clockwise = main_direction == ContourDirection::Clockwise; - let mut buffer = filter; - let visited = buffer.as_mut_slice(); + 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 < visited.len() { - if visited.is_visited(link_index) { + 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, visited) + // 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 { @@ -71,19 +78,28 @@ impl OffsetGraph<'_> { let start_data = StartPathData::new(direction, link, left_top_link); - let mut contour = self.get_fill_contour(&start_data, direction, &mut bold, visited); + 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) = contour.validate(min_area, true); + 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 { @@ -157,13 +173,20 @@ impl OffsetGraph<'_> { let start_data = StartPathData::new(direction, link, left_top_link); - let mut contour = self.get_fill_contour(&start_data, direction, &mut bold, &mut buffer.visited); + 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) = contour.validate(min_area, true); + let (is_valid, _) = buffer.points.validate(min_area, true); if !is_valid { link_index += 1; @@ -174,280 +197,36 @@ impl OffsetGraph<'_> { } } - fn get_fill_contour( + fn find_contour( &self, start_data: &StartPathData, clockwise: bool, bold: &mut bool, visited: &mut [VisitState], - ) -> IntContour { + 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); - let mut contour = IntContour::new(); - contour.push(start_data.begin); + 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 + // 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 = contour.push_node_and_get_other(link, node_id); + node_id = points.push_node_and_get_other(link, node_id); visited.visit(link_id); } - - contour - } -} - -#[cfg(test)] -mod tests { - use crate::geom::x_segment::XSegment; - use crate::segm::offset::ShapeCountOffset; - use crate::segm::segment::Segment; - use alloc::vec; - use i_float::int::point::IntPoint; - use crate::core::overlay::ContourDirection; - use crate::mesh::overlay::OffsetOverlay; - - #[test] - fn test_0() { - let segments = vec![ - Segment:: { - // 0 - x_segment: XSegment { - a: IntPoint::new(100_884_823, -84_374_363), - b: IntPoint::new(103_800_375, -100_646_683), - }, - count: ShapeCountOffset { subj: 1, bold: true }, - }, - Segment:: { - // 1 - x_segment: XSegment { - a: IntPoint::new(-20_055_752, 25_821_939), - b: IntPoint::new(103_800_375, -100_646_683), - }, - count: ShapeCountOffset { - subj: -1, - bold: false, - }, - }, - Segment:: { - // 2 - x_segment: XSegment { - a: IntPoint::new(-20_055_752, 25_821_939), - b: IntPoint::new(-19_013_992, 25_612_755), - }, - count: ShapeCountOffset { subj: 1, bold: true }, - }, - Segment:: { - // 3 - x_segment: XSegment { - a: IntPoint::new(-57_843_404, 28_494_136), - b: IntPoint::new(-19_013_992, 25_612_755), - }, - count: ShapeCountOffset { - subj: -1, - bold: false, - }, - }, - Segment:: { - // 4 - x_segment: XSegment { - a: IntPoint::new(-57_843_404, 28_494_136), - b: IntPoint::new(-56_476_844, 28_562_552), - }, - count: ShapeCountOffset { subj: 1, bold: true }, - }, - Segment:: { - // 5 - x_segment: XSegment { - a: IntPoint::new(-56_476_844, 28_562_552), - b: IntPoint::new(56_113_245, -11_174_714), - }, - count: ShapeCountOffset { subj: 1, bold: true }, - }, - Segment:: { - // 6 - x_segment: XSegment { - a: IntPoint::new(56_113_245, -11_174_714), - b: IntPoint::new(108_356_443, -118_534_979), - }, - count: ShapeCountOffset { subj: 1, bold: true }, - }, - Segment:: { - // 7 - x_segment: XSegment { - a: IntPoint::new(108_356_443, -118_534_979), - b: IntPoint::new(108_429_851, -119_688_163), - }, - count: ShapeCountOffset { subj: 1, bold: true }, - }, - Segment:: { - // 8 - x_segment: XSegment { - a: IntPoint::new(34_289_766, 4_029_975), - b: IntPoint::new(108_429_851, -119_688_163), - }, - count: ShapeCountOffset { - subj: -1, - bold: false, - }, - }, - Segment:: { - // 9 - x_segment: XSegment { - a: IntPoint::new(34_289_766, 4_029_975), - b: IntPoint::new(64_809_862, -14_876_105), - }, - count: ShapeCountOffset { subj: 1, bold: true }, - }, - Segment:: { - // 10 - x_segment: XSegment { - a: IntPoint::new(-175_197_506, -154_404_209), - b: IntPoint::new(64_809_862, -14_876_105), - }, - count: ShapeCountOffset { - subj: -1, - bold: false, - }, - }, - Segment:: { - // 11 - x_segment: XSegment { - a: IntPoint::new(-175_239_714, -153_263_889), - b: IntPoint::new(-175_197_506, -154_404_209), - }, - count: ShapeCountOffset { subj: -1, bold: true }, - }, - Segment:: { - // 12 - x_segment: XSegment { - a: IntPoint::new(-175_239_714, -153_263_889), - b: IntPoint::new(-158_100_461, -219_055_730), - }, - count: ShapeCountOffset { subj: 1, bold: false }, - }, - Segment:: { - // 13 - x_segment: XSegment { - a: IntPoint::new(-166_926_061, -201_796_434), - b: IntPoint::new(-158_100_461, -219_055_730), - }, - count: ShapeCountOffset { subj: -1, bold: true }, - }, - Segment:: { - // 14 - x_segment: XSegment { - a: IntPoint::new(-166_926_061, -201_796_434), - b: IntPoint::new(-127_176_915, -251_351_325), - }, - count: ShapeCountOffset { subj: 1, bold: false }, - }, - Segment:: { - // 15 - x_segment: XSegment { - a: IntPoint::new(-127_893_331, -250_758_333), - b: IntPoint::new(-127_176_915, -251_351_325), - }, - count: ShapeCountOffset { subj: -1, bold: true }, - }, - Segment:: { - // 16 - x_segment: XSegment { - a: IntPoint::new(-184_022_779, -146_081_734), - b: IntPoint::new(-127_893_331, -250_758_333), - }, - count: ShapeCountOffset { - subj: -1, - bold: false, - }, - }, - Segment:: { - // 17 - x_segment: XSegment { - a: IntPoint::new(-184_446_683, -142_060_198), - b: IntPoint::new(-184_022_779, -146_081_734), - }, - count: ShapeCountOffset { subj: -1, bold: true }, - }, - Segment:: { - // 18 - x_segment: XSegment { - a: IntPoint::new(-184_446_683, -142_060_198), - b: IntPoint::new(-183_294_322, -150_692_526), - }, - count: ShapeCountOffset { subj: 1, bold: false }, - }, - Segment:: { - // 19 - x_segment: XSegment { - a: IntPoint::new(-187_451_122, -124_999_534), - b: IntPoint::new(-183_294_322, -150_692_526), - }, - count: ShapeCountOffset { subj: -1, bold: true }, - }, - Segment:: { - // 20 - x_segment: XSegment { - a: IntPoint::new(-187_451_122, -124_999_534), - b: IntPoint::new(-163_529_925, -186_407_681), - }, - count: ShapeCountOffset { subj: 1, bold: false }, - }, - Segment:: { - // 21 - x_segment: XSegment { - a: IntPoint::new(-164_397_893, -185_090_145), - b: IntPoint::new(-163_529_925, -186_407_681), - }, - count: ShapeCountOffset { subj: -1, bold: true }, - }, - Segment:: { - // 22 - x_segment: XSegment { - a: IntPoint::new(-164_397_893, -185_090_145), - b: IntPoint::new(52_567_258, -230_502_654), - }, - count: ShapeCountOffset { subj: 1, bold: false }, - }, - Segment:: { - // 23 - x_segment: XSegment { - a: IntPoint::new(31_682_778, -244_054_974), - b: IntPoint::new(52_567_258, -230_502_654), - }, - count: ShapeCountOffset { subj: -1, bold: true }, - }, - Segment:: { - // 24 - x_segment: XSegment { - a: IntPoint::new(31_682_778, -244_054_974), - b: IntPoint::new(100_884_823, -84_374_363), - }, - count: ShapeCountOffset { subj: 1, bold: false }, - }, - ]; - - let mut overlay = OffsetOverlay::new(128); - overlay.add_segments(&segments); - - let shapes = overlay - .build_graph_view_with_solver(Default::default()) - .map(|graph| graph.extract_offset(ContourDirection::CounterClockwise, 0)) - .unwrap_or_default(); - - assert!(!shapes.is_empty()) } } diff --git a/iOverlay/src/mesh/outline/offset.rs b/iOverlay/src/mesh/outline/offset.rs index cd86030..2d1b60d 100644 --- a/iOverlay/src/mesh/outline/offset.rs +++ b/iOverlay/src/mesh/outline/offset.rs @@ -1,3 +1,5 @@ +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_rule::OverlayRule; @@ -13,6 +15,7 @@ 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::float::adapter::ShapesToFloat; use i_shape::float::despike::DeSpikeContour; use i_shape::float::int_area::IntArea; @@ -195,20 +198,24 @@ impl + 'static, T: FloatNumber + 'static> OutlineSolv self.outer_builder.build(path, &self.adapter, &mut segments); OffsetOverlay::with_segments(segments) - .build_graph_view_with_solver(Default::default()) - .map(|graph| graph.extract_offset(options.output_direction, int_min_area)) + .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(), ); + let mut offset_overlay = OffsetOverlay::new(128); 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); @@ -217,7 +224,7 @@ impl + 'static, T: FloatNumber + 'static> OutlineSolv continue; } - if area < 0 { + 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 { @@ -230,42 +237,30 @@ impl + 'static, T: FloatNumber + 'static> OutlineSolv offset_overlay.clear(); offset_overlay.add_segments(&segments); - let shapes = offset_overlay - .build_graph_view_with_solver(Default::default()) - .map(|graph| graph.extract_offset(ContourDirection::CounterClockwise, 0)) - .unwrap_or_default(); - - overlay.add_shapes(&shapes, ShapeType::Subject); + let graph = offset_overlay + .build_graph_view_with_solver::(Default::default()); + (graph, ContourDirection::CounterClockwise) } else { - let mut inverted = Vec::with_capacity(path.len()); - for p in path.iter().rev() { - inverted.push(*p); - } - - let capacity = self.inner_builder.capacity(inverted.len()); + 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(&inverted, &self.adapter, &mut segments); + self.inner_builder.build(path, &self.adapter, &mut segments); offset_overlay.clear(); offset_overlay.add_segments(&segments); - let mut shapes = offset_overlay - .build_graph_view_with_solver(Default::default()) - .map(|graph| graph.extract_offset(ContourDirection::CounterClockwise, 0)) - .unwrap_or_default(); + let graph = offset_overlay + .build_graph_view_with_solver::(Default::default()); + (graph, ContourDirection::Clockwise) + }; - for shape in shapes.iter_mut() { - for path in shape.iter_mut() { - path.reverse(); - } - } - - overlay.add_shapes(&shapes, ShapeType::Subject); + 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); } } @@ -377,7 +372,7 @@ mod tests { let shapes = path.outline(&style); - assert_eq!(shapes.len(), 0); + assert_eq!(shapes.len(), 1); } #[test] @@ -421,7 +416,7 @@ mod tests { ]; let style = OutlineStyle::new(1.0).line_join(LineJoin::Bevel); - let shapes = window.outline(&style); + let shapes = window.outline_fixed_scale(&style, 10.0).unwrap(); assert_eq!(shapes.len(), 1); assert_eq!(shapes[0].len(), 2); @@ -678,7 +673,7 @@ mod tests { [411294.2500422625, 5848072.3189725485], ]; - let shape= vec![main, hole]; + let shape = vec![main, hole]; let angle = 10.0f64 / (core::f64::consts::PI / 2.0f64); let style = OutlineStyle::new(600.0).line_join(LineJoin::Round(angle)); @@ -712,7 +707,7 @@ mod tests { [411_305.3719905047, 5848_010.244997939], ]; - let shape= vec![main, hole]; + let shape = vec![main, hole]; let angle = 10.0f64 / (core::f64::consts::PI / 2.0f64); let style = OutlineStyle::new(600.0).line_join(LineJoin::Round(angle)); diff --git a/iOverlay/src/mesh/overlay.rs b/iOverlay/src/mesh/overlay.rs index 39149c6..bc31e7f 100644 --- a/iOverlay/src/mesh/overlay.rs +++ b/iOverlay/src/mesh/overlay.rs @@ -1,4 +1,5 @@ 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; @@ -43,12 +44,15 @@ impl OffsetOverlay { } #[inline] - pub fn build_graph_view_with_solver(&mut self, solver: Solver) -> Option> { + 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; } - let graph = self.graph_builder.build_offset(&solver, &self.segments); + let graph = self.graph_builder.build_offset::(&solver, &self.segments); Some(graph) } diff --git a/iOverlay/src/mesh/stroke/offset.rs b/iOverlay/src/mesh/stroke/offset.rs index 69d931a..a0f7538 100644 --- a/iOverlay/src/mesh/stroke/offset.rs +++ b/iOverlay/src/mesh/stroke/offset.rs @@ -1,3 +1,4 @@ +use crate::build::offset::PositiveSubjectOffsetStrategy; use crate::float::overlay::OverlayOptions; use crate::float::scale::FixedScaleOverlayError; use crate::i_shape::source::resource::ShapeResource; @@ -202,8 +203,8 @@ impl, T: 'static + FloatNumber> StrokeSolve 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)) + .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 float = shapes.to_float(&self.adapter); From 0fc6b153fc4219fa18b0434019423cea8d5a40b5 Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Sun, 1 Mar 2026 17:02:05 +0300 Subject: [PATCH 6/9] fix clippy --- examples/overlay_editor/src/app/boolean/content.rs | 2 +- examples/overlay_editor/src/app/main.rs | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/examples/overlay_editor/src/app/boolean/content.rs b/examples/overlay_editor/src/app/boolean/content.rs index 9919c43..c75803f 100644 --- a/examples/overlay_editor/src/app/boolean/content.rs +++ b/examples/overlay_editor/src/app/boolean/content.rs @@ -39,7 +39,7 @@ pub(crate) enum BooleanMessage { } impl EditorApp { - fn boolean_sidebar(&self) -> Column { + fn boolean_sidebar(&self) -> Column<'_, AppMessage> { let count = self.app_resource.boolean.count; let mut column = Column::new().push( Space::new() diff --git a/examples/overlay_editor/src/app/main.rs b/examples/overlay_editor/src/app/main.rs index 326b709..cc3c5ab 100644 --- a/examples/overlay_editor/src/app/main.rs +++ b/examples/overlay_editor/src/app/main.rs @@ -6,7 +6,6 @@ use crate::app::string::content::StringMessage; use crate::app::string::content::StringState; use crate::app::stroke::content::StrokeMessage; use crate::app::stroke::content::StrokeState; -use iced::event::Event as MainEvent; use iced::keyboard::key::Named; use iced::keyboard::Key; use iced::widget::{rule, Space}; @@ -64,7 +63,6 @@ pub(crate) enum AppMessage { String(StringMessage), Stroke(StrokeMessage), Outline(OutlineMessage), - EventOccurred(MainEvent), NextTest, PrevTest, } @@ -111,7 +109,6 @@ impl EditorApp { MainAction::Stroke => self.stroke_prev_test(), MainAction::Outline => self.outline_prev_test(), } - _ => {} } Task::none() From 749f3993ab009406a08e463dc485eccdef2919e4 Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Sun, 1 Mar 2026 17:26:42 +0300 Subject: [PATCH 7/9] fix inner offset direction --- iOverlay/src/mesh/outline/offset.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/iOverlay/src/mesh/outline/offset.rs b/iOverlay/src/mesh/outline/offset.rs index 2d1b60d..a8a2b48 100644 --- a/iOverlay/src/mesh/outline/offset.rs +++ b/iOverlay/src/mesh/outline/offset.rs @@ -140,7 +140,7 @@ impl + 'static, T: FloatNumber + 'static> OutlineSolv let join = style.join.clone().normalize(); let outer_builder = OutlineBuilder::new(-style.outer_offset, &join); - let inner_builder = OutlineBuilder::new(style.inner_offset, &join); + let inner_builder = OutlineBuilder::new(-style.inner_offset, &join); let outer_radius = style.outer_offset; let inner_radius = style.inner_offset; @@ -420,6 +420,8 @@ mod tests { assert_eq!(shapes.len(), 1); assert_eq!(shapes[0].len(), 2); + assert_eq!(shapes[0][0].len(), 8); + assert_eq!(shapes[0][1].len(), 4); } // [[[[300.0, 300.0], [500.0, 300.0], [500.0, 500.0], [300.0, 500.0]]]] From 72200ff9de814b0d94559096d481035bed9c7b99 Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Sun, 1 Mar 2026 21:34:35 +0300 Subject: [PATCH 8/9] fix rand --- iOverlay/Cargo.toml | 2 +- iOverlay/src/mesh/outline/builder_join.rs | 12 ++++++------ iOverlay/src/mesh/outline/offset.rs | 2 +- iOverlay/src/split/grid_layout.rs | 2 +- iOverlay/tests/dynamic_tests.rs | 2 +- iOverlay/tests/float_overlay_tests.rs | 2 +- iOverlay/tests/slice_tests.rs | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/iOverlay/Cargo.toml b/iOverlay/Cargo.toml index fefb081..0391d0a 100644 --- a/iOverlay/Cargo.toml +++ b/iOverlay/Cargo.toml @@ -34,7 +34,7 @@ allow_multithreading = ["dep:rayon"] [dev-dependencies] serde = { version = "^1.0", features = ["derive"] } serde_json = "^1.0" -rand = { version = "~0.9", features = ["alloc"] } +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"] } diff --git a/iOverlay/src/mesh/outline/builder_join.rs b/iOverlay/src/mesh/outline/builder_join.rs index 0cbb44a..5565fbd 100644 --- a/iOverlay/src/mesh/outline/builder_join.rs +++ b/iOverlay/src/mesh/outline/builder_join.rs @@ -26,7 +26,7 @@ pub(super) struct BevelJoinBuilder; impl BevelJoinBuilder { #[inline] - fn join_weak>( + fn join>( s0: &Section, s1: &Section, adapter: &FloatPointAdapter, @@ -59,7 +59,7 @@ impl> JoinBuilder for BevelJoin adapter: &FloatPointAdapter, segments: &mut Vec>, ) { - Self::join_weak(s0, s1, adapter, segments); + Self::join(s0, s1, adapter, segments); } #[inline] @@ -118,7 +118,7 @@ impl> JoinBuilder for MiterJoin 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_weak(s0, s1, adapter, segments); + BevelJoinBuilder::join(s0, s1, adapter, segments); return; } @@ -130,7 +130,7 @@ impl> JoinBuilder for MiterJoin let sq_len = ia.sqr_distance(ib); if sq_len < 4 { - BevelJoinBuilder::join_weak(s0, s1, adapter, segments); + BevelJoinBuilder::join(s0, s1, adapter, segments); return; } @@ -225,13 +225,13 @@ impl> JoinBuilder for RoundJoin 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_weak(s0, s1, adapter, segments); + 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_weak(s0, s1, adapter, segments); + BevelJoinBuilder::join(s0, s1, adapter, segments); return; } diff --git a/iOverlay/src/mesh/outline/offset.rs b/iOverlay/src/mesh/outline/offset.rs index a8a2b48..214a0a4 100644 --- a/iOverlay/src/mesh/outline/offset.rs +++ b/iOverlay/src/mesh/outline/offset.rs @@ -352,7 +352,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(10.0); - let shapes = path.outline(&style); + let shapes = path.outline_fixed_scale(&style, 10.0).unwrap(); assert_eq!(shapes.len(), 1); diff --git a/iOverlay/src/split/grid_layout.rs b/iOverlay/src/split/grid_layout.rs index 7e7fcfa..fc47f1c 100644 --- a/iOverlay/src/split/grid_layout.rs +++ b/iOverlay/src/split/grid_layout.rs @@ -300,7 +300,7 @@ mod tests { use i_float::int::point::IntPoint; use i_float::int::rect::IntRect; use i_float::triangle::Triangle; - use rand::Rng; + use rand::RngExt; #[test] fn test_0() { diff --git a/iOverlay/tests/dynamic_tests.rs b/iOverlay/tests/dynamic_tests.rs index baf6b20..d49ec16 100644 --- a/iOverlay/tests/dynamic_tests.rs +++ b/iOverlay/tests/dynamic_tests.rs @@ -9,7 +9,7 @@ mod tests { use i_shape::base::data::Path; use i_shape::int::path::IntPath; use i_shape::int::shape::IntShape; - use rand::Rng; + use rand::RngExt; use std::f64::consts::PI; const SOLVERS: [Solver; 3] = [Solver::LIST, Solver::TREE, Solver::AUTO]; diff --git a/iOverlay/tests/float_overlay_tests.rs b/iOverlay/tests/float_overlay_tests.rs index 0d484eb..41d4ad9 100644 --- a/iOverlay/tests/float_overlay_tests.rs +++ b/iOverlay/tests/float_overlay_tests.rs @@ -9,7 +9,7 @@ mod tests { use i_overlay::float::overlay::{FloatOverlay, OverlayOptions}; use i_overlay::float::slice::FloatSlice; use i_overlay::string::clip::ClipRule; - use rand::Rng; + use rand::RngExt; #[derive(Clone, Copy)] struct FPoint { diff --git a/iOverlay/tests/slice_tests.rs b/iOverlay/tests/slice_tests.rs index 9c432eb..22e1cb6 100644 --- a/iOverlay/tests/slice_tests.rs +++ b/iOverlay/tests/slice_tests.rs @@ -5,7 +5,7 @@ mod tests { use i_overlay::string::line::IntLine; use i_overlay::string::slice::IntSlice; use i_shape::int::path::IntPath; - use rand::Rng; + use rand::RngExt; #[test] fn test_miss_slice() { From ce3706a0f82791c3ff53a845df144ad0142b40c8 Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Sun, 1 Mar 2026 22:10:11 +0300 Subject: [PATCH 9/9] bold logic --- iOverlay/src/mesh/outline/builder_join.rs | 30 ++++++++++++----------- iOverlay/src/mesh/subject.rs | 11 +++------ 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/iOverlay/src/mesh/outline/builder_join.rs b/iOverlay/src/mesh/outline/builder_join.rs index 5565fbd..1835150 100644 --- a/iOverlay/src/mesh/outline/builder_join.rs +++ b/iOverlay/src/mesh/outline/builder_join.rs @@ -32,21 +32,23 @@ impl BevelJoinBuilder { adapter: &FloatPointAdapter, segments: &mut Vec>, ) { - Self::add_weak_segment(&s0.b_top, &s1.a_top, adapter, segments); - } - - #[inline] - fn add_weak_segment>( - a: &P, - b: &P, - adapter: &FloatPointAdapter, - segments: &mut Vec>, - ) { - let ia = adapter.float_to_int(a); - let ib = adapter.float_to_int(b); - if ia != ib { - segments.push(Segment::weak_subject_ab(ia, ib)); + 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)); } } diff --git a/iOverlay/src/mesh/subject.rs b/iOverlay/src/mesh/subject.rs index 22467f3..0378785 100644 --- a/iOverlay/src/mesh/subject.rs +++ b/iOverlay/src/mesh/subject.rs @@ -18,21 +18,18 @@ impl Segment { } } } - + #[inline] - pub(crate) fn weak_subject_ab(p0: IntPoint, p1: IntPoint) -> Self { + 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: false }, + count: ShapeCountOffset { subj: 1, bold }, } } else { Self { x_segment: XSegment { a: p1, b: p0 }, - count: ShapeCountOffset { - subj: -1, - bold: false, - }, + count: ShapeCountOffset { subj: -1, bold }, } } }