From 1a126ede456c1c5c560f78fd8ccaa9d164a27823 Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Fri, 26 Jan 2024 13:05:09 +0100 Subject: [PATCH] Generic polygon to polygon processors --- src/db/db/dbShapeCollectionUtils.h | 3 + src/db/db/gsiDeclDbContainerHelpers.h | 214 ++++++++++++++++++++++++++ src/db/db/gsiDeclDbRegion.cc | 140 +++++++++++++++++ testdata/ruby/dbRegionTest.rb | 56 ++++++- 4 files changed, 411 insertions(+), 2 deletions(-) diff --git a/src/db/db/dbShapeCollectionUtils.h b/src/db/db/dbShapeCollectionUtils.h index 0d2a448f9..c300bb052 100644 --- a/src/db/db/dbShapeCollectionUtils.h +++ b/src/db/db/dbShapeCollectionUtils.h @@ -48,6 +48,9 @@ class DB_PUBLIC_TEMPLATE shape_collection_processor : public tl::Object { public: + typedef Shape shape_type; + typedef Result result_type; + /** * @brief Constructor */ diff --git a/src/db/db/gsiDeclDbContainerHelpers.h b/src/db/db/gsiDeclDbContainerHelpers.h index ee6112948..278b79337 100644 --- a/src/db/db/gsiDeclDbContainerHelpers.h +++ b/src/db/db/gsiDeclDbContainerHelpers.h @@ -238,6 +238,220 @@ private: bool m_wants_variants; }; +// --------------------------------------------------------------------------------- +// Generic shape processor declarations + +template +class shape_processor_impl + : public ProcessorBase +{ +public: + typedef typename ProcessorBase::shape_type shape_type; + typedef typename ProcessorBase::result_type result_type; + + shape_processor_impl () + { + mp_vars = &m_mag_and_orient; + m_wants_variants = true; + m_requires_raw_input = false; + m_result_is_merged = false; + m_result_must_not_be_merged = false; + } + + // overrides virtual method + virtual const db::TransformationReducer *vars () const + { + return mp_vars; + } + + // maybe overrides virtual method + virtual bool requires_raw_input () const + { + return m_requires_raw_input; + } + + void set_requires_raw_input (bool f) + { + m_requires_raw_input = f; + } + + // overrides virtual method + virtual bool wants_variants () const + { + return m_wants_variants; + } + + void set_wants_variants (bool f) + { + m_wants_variants = f; + } + + // overrides virtual method + virtual bool result_is_merged () const + { + return m_result_is_merged; + } + + void set_result_is_merged (bool f) + { + m_result_is_merged = f; + } + + // overrides virtual method + virtual bool result_must_not_be_merged () const + { + return m_result_must_not_be_merged; + } + + void set_result_must_not_be_merged (bool f) + { + m_result_must_not_be_merged = f; + } + + void is_isotropic () + { + mp_vars = &m_mag; + } + + void is_scale_invariant () + { + mp_vars = &m_orientation; + } + + void is_isotropic_and_scale_invariant () + { + mp_vars = 0; + } + + virtual void process (const shape_type &shape, std::vector &res) const + { + res = do_process (shape); + } + + std::vector issue_do_process (const shape_type &) const + { + return std::vector (); + } + + std::vector do_process (const shape_type &shape) const + { + if (f_process.can_issue ()) { + return f_process.issue, const shape_type &> (&shape_processor_impl::issue_do_process, shape); + } else { + return issue_do_process (shape); + } + } + + gsi::Callback f_process; + + static gsi::Methods method_decls (bool with_merged_options) + { + gsi::Methods decls = + callback ("process", &shape_processor_impl::issue_do_process, &shape_processor_impl::f_process, gsi::arg ("shape"), + "@brief Processes a shape\n" + "This method is the actual payload. It needs to be reimplemented in a derived class.\n" + "If needs to process the input shape and deliver a list of output shapes.\n" + "The output list may be empty to entirely discard the input shape. It may also contain more than a single shape.\n" + "In that case, the number of total shapes may grow during application of the processor.\n" + ); + + if (with_merged_options) { + decls += + method ("requires_raw_input?", &shape_processor_impl::requires_raw_input, + "@brief Gets a value indicating whether the processor needs raw (unmerged) input\n" + "See \\requires_raw_input= for details.\n" + ) + + method ("requires_raw_input=", &shape_processor_impl::set_requires_raw_input, gsi::arg ("flag"), + "@brief Sets a value indicating whether the processor needs raw (unmerged) input\n" + "This flag must be set before using this processor. It tells the processor implementation whether the " + "processor wants to have raw input (unmerged). The default value is 'false', meaning that\n" + "the processor will receive merged polygons ('merged semantics').\n" + "\n" + "Setting this value to false potentially saves some CPU time needed for merging the polygons.\n" + "Also, raw input means that strange shapes such as dot-like edges, self-overlapping polygons, " + "empty or degenerated polygons are preserved." + ) + + method ("result_is_merged?", &shape_processor_impl::result_is_merged, + "@brief Gets a value indicating whether the processor delivers merged output\n" + "See \\result_is_merged= for details.\n" + ) + + method ("result_is_merged=", &shape_processor_impl::set_result_is_merged, gsi::arg ("flag"), + "@brief Sets a value indicating whether the processor delivers merged output\n" + "This flag must be set before using this processor. If the processor maintains the merged condition\n" + "by design (output is merged if input is), it is a good idea to set this predicate to 'true'.\n" + "This will avoid additional merge steps when the resulting collection is used in further operations\n" + "that need merged input\n." + ) + + method ("result_must_not_be_merged?", &shape_processor_impl::result_must_not_be_merged, + "@brief Gets a value indicating whether the processor's output must not be merged\n" + "See \\result_must_not_be_merged= for details.\n" + ) + + method ("result_must_not_be_merged=", &shape_processor_impl::set_result_must_not_be_merged, gsi::arg ("flag"), + "@brief Sets a value indicating whether the processor's output must not be merged\n" + "This flag must be set before using this processor. The processor can set this flag if it wants to\n" + "deliver shapes that must not be merged - e.g. point-like edges or strange or degenerated polygons.\n." + ); + } + + decls += + method ("wants_variants?", &shape_processor_impl::wants_variants, + "@brief Gets a value indicating whether the filter prefers cell variants\n" + "See \\wants_variants= for details.\n" + ) + + method ("wants_variants=", &shape_processor_impl::set_wants_variants, gsi::arg ("flag"), + "@brief Sets a value indicating whether the filter prefers cell variants\n" + "This flag must be set before using this filter for hierarchical applications (deep mode). " + "It tells the filter implementation whether cell variants should be created (true, the default) " + "or shape propagation will be applied (false).\n" + "\n" + "This decision needs to be made, if the filter indicates that it will deliver different results\n" + "for scaled or rotated versions of the shape (see \\is_isotropic and the other hints). If a cell\n" + "is present with different qualities - as seen from the top cell - the respective instances\n" + "need to be differentiated. Cell variant formation is one way, shape propagation the other way.\n" + "Typically, cell variant formation is less expensive, but the hierarchy will be modified." + ) + + method ("is_isotropic", &shape_processor_impl::is_isotropic, + "@brief Indicates that the filter has isotropic properties\n" + "Call this method before using the filter to indicate that the selection is independent of " + "the orientation of the shape. This helps the filter algorithm optimizing the filter run, specifically in " + "hierarchical mode.\n" + "\n" + "Examples for isotropic (polygon) processors are size or shrink operators. Size or shrink is not dependent " + "on orientation unless size or shrink needs to be different in x and y direction." + ) + + method ("is_scale_invariant", &shape_processor_impl::is_scale_invariant, + "@brief Indicates that the filter is scale invariant\n" + "Call this method before using the filter to indicate that the selection is independent of " + "the scale of the shape. This helps the filter algorithm optimizing the filter run, specifically in " + "hierarchical mode.\n" + "\n" + "An example for a scale invariant (polygon) processor is the rotation operator. Rotation is not depending on scale, " + "but on the original orientation as mirrored versions need to be rotated differently." + ) + + method ("is_isotropic_and_scale_invariant", &shape_processor_impl::is_isotropic_and_scale_invariant, + "@brief Indicates that the filter is isotropic and scale invariant\n" + "Call this method before using the filter to indicate that the selection is independent of " + "the scale and orientation of the shape. This helps the filter algorithm optimizing the filter run, specifically in " + "hierarchical mode.\n" + "\n" + "An example for such a (polygon) processor is the convex decomposition operator. The decomposition of a polygon into " + "convex parts is an operation that is not depending on scale nor orientation." + ); + + return decls; + } + +private: + const db::TransformationReducer *mp_vars; + db::OrientationReducer m_orientation; + db::MagnificationReducer m_mag; + db::MagnificationAndOrientationReducer m_mag_and_orient; + bool m_requires_raw_input; + bool m_wants_variants; + bool m_result_is_merged; + bool m_result_must_not_be_merged; +}; + } #endif diff --git a/src/db/db/gsiDeclDbRegion.cc b/src/db/db/gsiDeclDbRegion.cc index b6c00648c..83a9b5a62 100644 --- a/src/db/db/gsiDeclDbRegion.cc +++ b/src/db/db/gsiDeclDbRegion.cc @@ -127,6 +127,102 @@ Class decl_PolygonFilterImpl ("db", "PolygonFilter", "This class has been introduced in version 0.29.\n" ); +// --------------------------------------------------------------------------------- +// PolygonProcessor binding + +Class > decl_PolygonProcessor ("db", "PolygonProcessor", + shape_processor_impl::method_decls (true), + "@brief A generic polygon processor adaptor\n" + "\n" + "Polygon processors are an efficient way to process polygons from a Region. To apply a processors, derive your own " + "processors class and pass an instance to \\Region#process or \\Region#processed method.\n" + "\n" + "Conceptually, these methods take each polygon from the region and present it to the processor's 'process' method.\n" + "The result of this call is a list of zero to many output polygons derived from the input polygon.\n" + "The output region is the sum over all these individual results.\n" + "\n" + "The magic happens when deep mode regions are involved. In that case, the processor will use as few calls as possible " + "and exploit the hierarchical compression if possible. It needs to know however, how the processor behaves. You " + "need to configure the processor by calling \\is_isotropic, \\is_scale_invariant or \\is_isotropic_and_scale_invariant " + "before using it.\n" + "\n" + "You can skip this step, but the processor algorithm will assume the worst case then. This usually leads to cell variant " + "formation which is not always desired and blows up the hierarchy.\n" + "\n" + "Here is some example that shrinks every polygon to half of the size but does not change the position.\n" + "In this example the 'position' is defined by the center of the bounding box:" + "\n" + "@code\n" + "class ShrinkToHalfProcessor < RBA::PolygonProcessor\n" + "\n" + " # Constructor\n" + " def initialize\n" + " self.is_isotropic_and_scale_invariant # scale or orientation do not matter\n" + " end\n" + " \n" + " # Shrink to half size\n" + " def process(polygon)\n" + " shift = polygon.bbox.center - RBA::Point::new # shift vector\n" + " return [ (polygon.moved(-shift) * 0.5).moved(shift) ]\n" + " end\n" + "\n" + "end\n" + "\n" + "region = ... # some Region\n" + "shrinked_to_half = region.processed(ShrinkToHalf::new)\n" + "@/code\n" + "\n" + "This class has been introduced in version 0.29.\n" +); + +Class > decl_PolygonToEdgeProcessor ("db", "PolygonToEdgeProcessor", + shape_processor_impl::method_decls (true), + "@brief A generic polygon-to-edge processor adaptor\n" + "\n" + "Polygon processors are an efficient way to process polygons from a Region. To apply a processors, derive your own " + "processors class and pass an instance to \\Region#processed method.\n" + "\n" + "Conceptually, these methods take each polygon from the region and present it to the processor's 'process' method.\n" + "The result of this call is a list of zero to many output edges derived from the input polygon.\n" + "The output edge collection is the sum over all these individual results.\n" + "\n" + "The magic happens when deep mode regions are involved. In that case, the processor will use as few calls as possible " + "and exploit the hierarchical compression if possible. It needs to know however, how the processor behaves. You " + "need to configure the processor by calling \\is_isotropic, \\is_scale_invariant or \\is_isotropic_and_scale_invariant " + "before using it.\n" + "\n" + "You can skip this step, but the processor algorithm will assume the worst case then. This usually leads to cell variant " + "formation which is not always desired and blows up the hierarchy.\n" + "\n" + "For a basic example see the \\PolygonProcessor class, with the exception that this incarnation has to deliver edges.\n" + "\n" + "This class has been introduced in version 0.29.\n" +); + +Class > decl_PolygonToEdgePairProcessor ("db", "PolygonToEdgePairProcessor", + shape_processor_impl::method_decls (true), + "@brief A generic polygon-to-edge-pair processor adaptor\n" + "\n" + "Polygon processors are an efficient way to process polygons from a Region. To apply a processors, derive your own " + "processors class and pass an instance to \\Region#processed method.\n" + "\n" + "Conceptually, these methods take each polygon from the region and present it to the processor's 'process' method.\n" + "The result of this call is a list of zero to many output edge pairs derived from the input polygon.\n" + "The output edge pair collection is the sum over all these individual results.\n" + "\n" + "The magic happens when deep mode regions are involved. In that case, the processor will use as few calls as possible " + "and exploit the hierarchical compression if possible. It needs to know however, how the processor behaves. You " + "need to configure the processor by calling \\is_isotropic, \\is_scale_invariant or \\is_isotropic_and_scale_invariant " + "before using it.\n" + "\n" + "You can skip this step, but the processor algorithm will assume the worst case then. This usually leads to cell variant " + "formation which is not always desired and blows up the hierarchy.\n" + "\n" + "For a basic example see the \\PolygonProcessor class, with the exception that this incarnation has to deliver edge pairs.\n" + "\n" + "This class has been introduced in version 0.29.\n" +); + // --------------------------------------------------------------------------------- // Region binding @@ -411,6 +507,26 @@ static void filter (db::Region *r, const PolygonFilterImpl *f) r->filter (*f); } +static db::Region processed_pp (const db::Region *r, const shape_processor_impl *f) +{ + return r->processed (*f); +} + +static void process_pp (db::Region *r, const shape_processor_impl *f) +{ + r->process (*f); +} + +static db::EdgePairs processed_pep (const db::Region *r, const shape_processor_impl *f) +{ + return r->processed (*f); +} + +static db::Edges processed_pe (const db::Region *r, const shape_processor_impl *f) +{ + return r->processed (*f); +} + static db::Region with_perimeter1 (const db::Region *r, db::Region::perimeter_type perimeter, bool inverse) { db::RegionPerimeterFilter f (perimeter, perimeter + 1, inverse); @@ -2426,6 +2542,30 @@ Class decl_Region (decl_dbShapeCollection, "db", "Region", "\n" "This method has been introduced in version 0.29.\n" ) + + method_ext ("process", &process_pp, gsi::arg ("process"), + "@brief Applies a generic polygon processor in place (replacing the polygons from the Region)\n" + "See \\PolygonProcessor for a description of this feature.\n" + "\n" + "This method has been introduced in version 0.29.\n" + ) + + method_ext ("processed", &processed_pp, gsi::arg ("processed"), + "@brief Applies a generic polygon processor and returns a processed copy\n" + "See \\PolygonProcessor for a description of this feature.\n" + "\n" + "This method has been introduced in version 0.29.\n" + ) + + method_ext ("processed", &processed_pep, gsi::arg ("processed"), + "@brief Applies a generic polygon-to-edge-pair processor and returns an edge pair collection with the results\n" + "See \\PolygonToEdgePairProcessor for a description of this feature.\n" + "\n" + "This method has been introduced in version 0.29.\n" + ) + + method_ext ("processed", &processed_pe, gsi::arg ("processed"), + "@brief Applies a generic polygon-to-edge processor and returns an edge collection with the results\n" + "See \\PolygonToEdgeProcessor for a description of this feature.\n" + "\n" + "This method has been introduced in version 0.29.\n" + ) + method_ext ("rectangles", &rectangles, "@brief Returns all polygons which are rectangles\n" "This method returns all polygons in self which are rectangles." diff --git a/testdata/ruby/dbRegionTest.rb b/testdata/ruby/dbRegionTest.rb index e0f2f07c9..9eea0ff38 100644 --- a/testdata/ruby/dbRegionTest.rb +++ b/testdata/ruby/dbRegionTest.rb @@ -44,6 +44,21 @@ class TriangleFilter < RBA::PolygonFilter end +class ShrinkToHalfProcessor < RBA::PolygonProcessor + + # Constructor + def initialize + self.is_isotropic_and_scale_invariant # scale or orientation do not matter + end + + # Shrink to half size + def process(polygon) + shift = polygon.bbox.center - RBA::Point::new # shift vector + return [ (polygon.moved(-shift) * 0.5).moved(shift) ] + end + +end + class DBRegion_TestClass < TestBase # Basics @@ -1212,8 +1227,8 @@ class DBRegion_TestClass < TestBase f.wants_variants = false assert_equal(f.wants_variants?, false) assert_equal(f.requires_raw_input, false) - f.requires_raw_input = false - assert_equal(f.requires_raw_input, false) + f.requires_raw_input = true + assert_equal(f.requires_raw_input, true) # Smoke test f.is_isotropic @@ -1233,6 +1248,43 @@ class DBRegion_TestClass < TestBase end + # Generic processors + def test_generic_processors + + # Some basic tests for the filter class + + f = ShrinkToHalfProcessor::new + assert_equal(f.wants_variants?, true) + f.wants_variants = false + assert_equal(f.wants_variants?, false) + assert_equal(f.requires_raw_input, false) + f.requires_raw_input = true + assert_equal(f.requires_raw_input, true) + assert_equal(f.result_is_merged, false) + f.result_is_merged = true + assert_equal(f.result_is_merged, true) + assert_equal(f.result_must_not_be_merged, false) + f.result_must_not_be_merged = true + assert_equal(f.result_must_not_be_merged, true) + + # Smoke test + f.is_isotropic + f.is_scale_invariant + + # Some application + + region = RBA::Region::new + + region.insert(RBA::Polygon::new([[0,0], [100, 100], [100,0]])) + region.insert(RBA::Box::new(200, 0, 300, 100)) + + assert_equal(region.processed(ShrinkToHalfProcessor::new).to_s, "(25,25;75,75;75,25);(225,25;225,75;275,75;275,25)") + assert_equal(region.to_s, "(0,0;100,100;100,0);(200,0;200,100;300,100;300,0)") + region.process(ShrinkToHalfProcessor::new) + assert_equal(region.to_s, "(25,25;75,75;75,25);(225,25;225,75;275,75;275,25)") + + end + end load("test_epilogue.rb")