From c40f147dc7e4671628df37db17b5dcab08af2a49 Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Sun, 17 Feb 2019 18:36:15 +0100 Subject: [PATCH] Edge/edge and edge/polygon interaction test ported to hierarchical mode. --- src/db/db/dbAsIfFlatEdges.cc | 257 +++++++------------------- src/db/db/dbAsIfFlatEdges.h | 72 ++++++++ src/db/db/dbDeepEdges.cc | 206 ++++++++++++++++++++- src/db/db/dbDeepEdges.h | 2 + src/db/unit_tests/dbDeepEdgesTests.cc | 44 +++++ testdata/algo/deep_edges_au8.gds | Bin 0 -> 4010 bytes 6 files changed, 379 insertions(+), 202 deletions(-) create mode 100644 testdata/algo/deep_edges_au8.gds diff --git a/src/db/db/dbAsIfFlatEdges.cc b/src/db/db/dbAsIfFlatEdges.cc index 6d4fed452..f405b3e24 100644 --- a/src/db/db/dbAsIfFlatEdges.cc +++ b/src/db/db/dbAsIfFlatEdges.cc @@ -195,149 +195,62 @@ AsIfFlatEdges::to_string (size_t nmax) const return os.str (); } -namespace -{ - -/** - * @brief A helper class for the edge to region interaction functionality which acts as an edge pair receiver - * - * Note: This special scanner uses pointers to two different objects: edges and polygons. - * It uses odd value pointers to indicate pointers to polygons and even value pointers to indicate - * pointers to edges. - * - * There is a special box converter which is able to sort that out as well. - */ -template -class edge_to_region_interaction_filter - : public db::box_scanner_receiver -{ -public: - edge_to_region_interaction_filter (OutputContainer &output) - : mp_output (&output) - { - // .. nothing yet .. - } - - void add (const char *o1, size_t p1, const char *o2, size_t p2) - { - const db::Edge *e = 0; - const db::Polygon *p = 0; - - // Note: edges have property 0 and have even-valued pointers. - // Polygons have property 1 and odd-valued pointers. - if (p1 == 0 && p2 == 1) { - e = reinterpret_cast (o1); - p = reinterpret_cast (o2 - 1); - } else if (p1 == 1 && p2 == 0) { - e = reinterpret_cast (o2); - p = reinterpret_cast (o1 - 1); - } - - if (e && p && m_seen.find (e) == m_seen.end ()) { - if (db::interact (*p, *e)) { - m_seen.insert (e); - mp_output->insert (*e); - } - } - } - -private: - OutputContainer *mp_output; - std::set m_seen; -}; - -/** - * @brief A special box converter that splits the pointers into polygon and edge pointers - */ -struct EdgeOrRegionBoxConverter -{ - typedef db::Box box_type; - - db::Box operator() (const char &c) const - { - // Note: edges have property 0 and have even-valued pointers. - // Polygons have property 1 and odd-valued pointers. - const char *cp = &c; - if ((size_t (cp) & 1) == 1) { - // it's a polygon - return (reinterpret_cast (cp - 1))->box (); - } else { - // it's an edge - const db::Edge *e = reinterpret_cast (cp); - return db::Box (e->p1 (), e->p2 ()); - } - } -}; - -} - EdgesDelegate * -AsIfFlatEdges::selected_interacting (const Region &other) const +AsIfFlatEdges::selected_interacting_generic (const Region &other, bool inverse) const { // shortcuts if (other.empty () || empty ()) { return new EmptyEdges (); } - db::box_scanner scanner (report_progress (), progress_desc ()); - scanner.reserve (size () + other.size ()); + db::box_scanner2 scanner (report_progress (), progress_desc ()); AddressableEdgeDelivery e (begin_merged (), has_valid_merged_edges ()); for ( ; ! e.at_end (); ++e) { - scanner.insert ((char *) e.operator-> (), 0); + scanner.insert1 (e.operator-> (), 0); } AddressablePolygonDelivery p = other.addressable_polygons (); for ( ; ! p.at_end (); ++p) { - scanner.insert ((char *) p.operator-> () + 1, 1); + scanner.insert2 (p.operator-> (), 1); } std::auto_ptr output (new FlatEdges (true)); - edge_to_region_interaction_filter filter (*output); - EdgeOrRegionBoxConverter bc; - scanner.process (filter, 1, bc); + + if (! inverse) { + + edge_to_region_interaction_filter filter (*output); + scanner.process (filter, 1, db::box_convert (), db::box_convert ()); + + } else { + + std::set interacting; + edge_to_region_interaction_filter > filter (interacting); + scanner.process (filter, 1, db::box_convert (), db::box_convert ()); + + for (EdgesIterator o (begin_merged ()); ! o.at_end (); ++o) { + if (interacting.find (*o) == interacting.end ()) { + output->insert (*o); + } + } + + } return output.release (); } +EdgesDelegate * +AsIfFlatEdges::selected_interacting (const Region &other) const +{ + return selected_interacting_generic (other, false); +} + EdgesDelegate * AsIfFlatEdges::selected_not_interacting (const Region &other) const { - // shortcuts - if (other.empty () || empty ()) { - return clone (); - } - - db::box_scanner scanner (report_progress (), progress_desc ()); - scanner.reserve (size () + other.size ()); - - AddressableEdgeDelivery e (begin_merged (), has_valid_merged_edges ()); - - for ( ; ! e.at_end (); ++e) { - scanner.insert ((char *) e.operator-> (), 0); - } - - AddressablePolygonDelivery p = other.addressable_polygons (); - - for ( ; ! p.at_end (); ++p) { - scanner.insert ((char *) p.operator-> () + 1, 1); - } - - std::set interacting; - edge_to_region_interaction_filter > filter (interacting); - EdgeOrRegionBoxConverter bc; - scanner.process (filter, 1, bc); - - std::auto_ptr output (new FlatEdges (true)); - for (EdgesIterator o (begin_merged ()); ! o.at_end (); ++o) { - if (interacting.find (*o) == interacting.end ()) { - output->insert (*o); - } - } - - return output.release (); + return selected_interacting_generic (other, true); } namespace @@ -483,99 +396,57 @@ AsIfFlatEdges::centers (length_type length, double fraction) const return segments (0, length, fraction); } -namespace +EdgesDelegate * +AsIfFlatEdges::selected_interacting_generic (const Edges &edges, bool inverse) const { + db::box_scanner scanner (report_progress (), progress_desc ()); -/** - * @brief A helper class for the edge interaction functionality which acts as an edge pair receiver - */ -template -class edge_interaction_filter - : public db::box_scanner_receiver -{ -public: - edge_interaction_filter (OutputContainer &output) - : mp_output (&output) - { - // .. nothing yet .. + AddressableEdgeDelivery e (begin_merged (), has_valid_merged_edges ()); + + for ( ; ! e.at_end (); ++e) { + scanner.insert (e.operator-> (), 0); } - - void add (const db::Edge *o1, size_t p1, const db::Edge *o2, size_t p2) - { - // Select the edges which intersect - if (p1 != p2) { - const db::Edge *o = p1 > p2 ? o2 : o1; - const db::Edge *oo = p1 > p2 ? o1 : o2; - if (o->intersect (*oo)) { - if (m_seen.insert (o).second) { - mp_output->insert (*o); - } + + AddressableEdgeDelivery ee = edges.addressable_edges (); + + for ( ; ! ee.at_end (); ++ee) { + scanner.insert (ee.operator-> (), 1); + } + + std::auto_ptr output (new FlatEdges (true)); + + if (! inverse) { + + edge_interaction_filter filter (*output); + scanner.process (filter, 1, db::box_convert ()); + + } else { + + std::set interacting; + edge_interaction_filter > filter (interacting); + scanner.process (filter, 1, db::box_convert ()); + + for (EdgesIterator o (begin_merged ()); ! o.at_end (); ++o) { + if (interacting.find (*o) == interacting.end ()) { + output->insert (*o); } } + } -private: - OutputContainer *mp_output; - std::set m_seen; -}; - + return output.release (); } EdgesDelegate * AsIfFlatEdges::selected_interacting (const Edges &other) const { - db::box_scanner scanner (report_progress (), progress_desc ()); - scanner.reserve (size () + other.size ()); - - AddressableEdgeDelivery e (begin_merged (), has_valid_merged_edges ()); - - for ( ; ! e.at_end (); ++e) { - scanner.insert (e.operator-> (), 0); - } - - AddressableEdgeDelivery ee = other.addressable_edges (); - - for ( ; ! ee.at_end (); ++ee) { - scanner.insert (ee.operator-> (), 1); - } - - std::auto_ptr output (new FlatEdges (true)); - edge_interaction_filter filter (*output); - scanner.process (filter, 1, db::box_convert ()); - - return output.release (); + return selected_interacting_generic (other, false); } EdgesDelegate * AsIfFlatEdges::selected_not_interacting (const Edges &other) const { - db::box_scanner scanner (report_progress (), progress_desc ()); - scanner.reserve (size () + other.size ()); - - AddressableEdgeDelivery e (begin_merged (), has_valid_merged_edges ()); - - for ( ; ! e.at_end (); ++e) { - scanner.insert (e.operator-> (), 0); - } - - AddressableEdgeDelivery ee = other.addressable_edges (); - - for ( ; ! ee.at_end (); ++ee) { - scanner.insert (ee.operator-> (), 1); - } - - std::set interacting; - edge_interaction_filter > filter (interacting); - scanner.process (filter, 1, db::box_convert ()); - - std::auto_ptr output (new FlatEdges (true)); - for (EdgesIterator o (begin_merged ()); ! o.at_end (); ++o) { - if (interacting.find (*o) == interacting.end ()) { - output->insert (*o); - } - } - - return output.release (); + return selected_interacting_generic (other, true); } EdgesDelegate * diff --git a/src/db/db/dbAsIfFlatEdges.h b/src/db/db/dbAsIfFlatEdges.h index c47911ab2..26dd22326 100644 --- a/src/db/db/dbAsIfFlatEdges.h +++ b/src/db/db/dbAsIfFlatEdges.h @@ -27,6 +27,8 @@ #include "dbCommon.h" #include "dbBoxScanner.h" #include "dbEdgesDelegate.h" +#include "dbBoxScanner.h" +#include "dbPolygonTools.h" #include #include @@ -35,6 +37,74 @@ namespace db { class PolygonSink; +/** + * @brief A helper class for the edge interaction functionality which acts as an edge pair receiver + */ +template +class edge_interaction_filter + : public db::box_scanner_receiver +{ +public: + edge_interaction_filter (OutputContainer &output) + : mp_output (&output) + { + // .. nothing yet .. + } + + void add (const db::Edge *o1, size_t p1, const db::Edge *o2, size_t p2) + { + // Select the edges which intersect + if (p1 != p2) { + const db::Edge *o = p1 > p2 ? o2 : o1; + const db::Edge *oo = p1 > p2 ? o1 : o2; + if (o->intersect (*oo)) { + if (m_seen.insert (o).second) { + mp_output->insert (*o); + } + } + } + } + +private: + OutputContainer *mp_output; + std::set m_seen; +}; + +/** + * @brief A helper class for the edge to region interaction functionality which acts as an edge pair receiver + * + * Note: This special scanner uses pointers to two different objects: edges and polygons. + * It uses odd value pointers to indicate pointers to polygons and even value pointers to indicate + * pointers to edges. + * + * There is a special box converter which is able to sort that out as well. + */ +template +class edge_to_region_interaction_filter + : public db::box_scanner_receiver2 +{ +public: + edge_to_region_interaction_filter (OutputContainer &output) + : mp_output (&output) + { + // .. nothing yet .. + } + + void add (const db::Edge *e, size_t, const db::Polygon *p, size_t) + { + if (m_seen.find (e) == m_seen.end ()) { + if (db::interact (*p, *e)) { + m_seen.insert (e); + mp_output->insert (*e); + } + } + } + +private: + OutputContainer *mp_output; + std::set m_seen; +}; + /** * @brief A helper class to turn joined edge sequences into polygons * @@ -186,6 +256,8 @@ protected: EdgePairs run_check (db::edge_relation_type rel, const Edges *other, db::Coord d, bool whole_edges, metrics_type metrics, double ignore_angle, distance_type min_projection, distance_type max_projection) const; static db::Polygon extended_edge (const db::Edge &edge, coord_type ext_b, coord_type ext_e, coord_type ext_o, coord_type ext_i); static db::Edge compute_partial (const db::Edge &edge, int mode, length_type length, double fraction); + virtual EdgesDelegate *selected_interacting_generic (const Edges &edges, bool inverse) const; + virtual EdgesDelegate *selected_interacting_generic (const Region ®ion, bool inverse) const; private: AsIfFlatEdges &operator= (const AsIfFlatEdges &other); diff --git a/src/db/db/dbDeepEdges.cc b/src/db/db/dbDeepEdges.cc index 55129a374..9083947ab 100644 --- a/src/db/db/dbDeepEdges.cc +++ b/src/db/db/dbDeepEdges.cc @@ -956,33 +956,221 @@ EdgesDelegate *DeepEdges::centers (length_type length, double fraction) const return segments (0, length, fraction); } +namespace +{ + +class Edge2EdgeInteractingLocalOperation + : public local_operation +{ +public: + Edge2EdgeInteractingLocalOperation (bool inverse) + : m_inverse (inverse) + { + // .. nothing yet .. + } + + virtual void compute_local (db::Layout * /*layout*/, const shape_interactions &interactions, std::unordered_set &result, size_t /*max_vertex_count*/, double /*area_ratio*/) const + { + db::box_scanner scanner; + + std::set others; + for (shape_interactions::iterator i = interactions.begin (); i != interactions.end (); ++i) { + for (shape_interactions::iterator2 j = i->second.begin (); j != i->second.end (); ++j) { + others.insert (interactions.intruder_shape (*j)); + } + } + + for (shape_interactions::iterator i = interactions.begin (); i != interactions.end (); ++i) { + const db::Edge &subject = interactions.subject_shape (i->first); + scanner.insert (&subject, 0); + } + + for (std::set::const_iterator o = others.begin (); o != others.end (); ++o) { + scanner.insert (o.operator-> (), 1); + } + + if (m_inverse) { + + std::unordered_set interacting; + edge_interaction_filter > filter (interacting); + scanner.process (filter, 1, db::box_convert ()); + + for (shape_interactions::iterator i = interactions.begin (); i != interactions.end (); ++i) { + const db::Edge &subject = interactions.subject_shape (i->first); + if (interacting.find (subject) == interacting.end ()) { + result.insert (subject); + } + } + + } else { + + edge_interaction_filter > filter (result); + scanner.process (filter, 1, db::box_convert ()); + + } + + } + + virtual on_empty_intruder_mode on_empty_intruder_hint () const + { + if (m_inverse) { + return Copy; + } else { + return Drop; + } + } + + virtual std::string description () const + { + return tl::to_string (tr ("Select interacting edges")); + } + +private: + bool m_inverse; +}; + +class Edge2PolygonInteractingLocalOperation + : public local_operation +{ +public: + Edge2PolygonInteractingLocalOperation (bool inverse) + : m_inverse (inverse) + { + // .. nothing yet .. + } + + virtual void compute_local (db::Layout * /*layout*/, const shape_interactions &interactions, std::unordered_set &result, size_t /*max_vertex_count*/, double /*area_ratio*/) const + { + db::box_scanner2 scanner; + + std::set others; + for (shape_interactions::iterator i = interactions.begin (); i != interactions.end (); ++i) { + for (shape_interactions::iterator2 j = i->second.begin (); j != i->second.end (); ++j) { + others.insert (interactions.intruder_shape (*j)); + } + } + + for (shape_interactions::iterator i = interactions.begin (); i != interactions.end (); ++i) { + const db::Edge &subject = interactions.subject_shape (i->first); + scanner.insert1 (&subject, 0); + } + + std::list heap; + for (std::set::const_iterator o = others.begin (); o != others.end (); ++o) { + heap.push_back (o->obj ().transformed (o->trans ())); + scanner.insert2 (& heap.back (), 1); + } + + if (m_inverse) { + + std::unordered_set interacting; + edge_to_region_interaction_filter > filter (interacting); + scanner.process (filter, 1, db::box_convert (), db::box_convert ()); + + for (shape_interactions::iterator i = interactions.begin (); i != interactions.end (); ++i) { + const db::Edge &subject = interactions.subject_shape (i->first); + if (interacting.find (subject) == interacting.end ()) { + result.insert (subject); + } + } + + } else { + + edge_to_region_interaction_filter > filter (result); + scanner.process (filter, 1, db::box_convert (), db::box_convert ()); + + } + } + + virtual on_empty_intruder_mode on_empty_intruder_hint () const + { + if (m_inverse) { + return Copy; + } else { + return Drop; + } + } + + virtual std::string description () const + { + return tl::to_string (tr ("Select interacting edges")); + } + +private: + bool m_inverse; +}; + +} + +EdgesDelegate * +DeepEdges::selected_interacting_generic (const Region &other, bool inverse) const +{ + const db::DeepRegion *other_deep = dynamic_cast (other.delegate ()); + if (! other_deep) { + return db::AsIfFlatEdges::selected_interacting_generic (other, inverse); + } + + ensure_merged_edges_valid (); + + DeepLayer dl_out (m_deep_layer.derived ()); + + db::Edge2PolygonInteractingLocalOperation op (inverse); + + db::local_processor proc (const_cast (&m_deep_layer.layout ()), const_cast (&m_deep_layer.initial_cell ()), &other_deep->deep_layer ().layout (), &other_deep->deep_layer ().initial_cell ()); + proc.set_base_verbosity (base_verbosity ()); + proc.set_threads (m_deep_layer.store ()->threads ()); + + proc.run (&op, m_merged_edges.layer (), other_deep->deep_layer ().layer (), dl_out.layer ()); + + return new db::DeepEdges (dl_out); +} + +EdgesDelegate * +DeepEdges::selected_interacting_generic (const Edges &other, bool inverse) const +{ + const db::DeepEdges *other_deep = dynamic_cast (other.delegate ()); + if (! other_deep) { + return db::AsIfFlatEdges::selected_interacting_generic (other, inverse); + } + + ensure_merged_edges_valid (); + + DeepLayer dl_out (m_deep_layer.derived ()); + + db::Edge2EdgeInteractingLocalOperation op (inverse); + + db::local_processor proc (const_cast (&m_deep_layer.layout ()), const_cast (&m_deep_layer.initial_cell ()), &other_deep->deep_layer ().layout (), &other_deep->deep_layer ().initial_cell ()); + proc.set_base_verbosity (base_verbosity ()); + proc.set_threads (m_deep_layer.store ()->threads ()); + + proc.run (&op, m_merged_edges.layer (), other_deep->deep_layer ().layer (), dl_out.layer ()); + + return new db::DeepEdges (dl_out); +} + EdgesDelegate *DeepEdges::selected_interacting (const Edges &other) const { - // TODO: implement - return AsIfFlatEdges::selected_interacting (other); + return selected_interacting_generic (other, false); } EdgesDelegate *DeepEdges::selected_not_interacting (const Edges &other) const { - // TODO: implement - return AsIfFlatEdges::selected_not_interacting (other); + return selected_interacting_generic (other, true); } EdgesDelegate *DeepEdges::selected_interacting (const Region &other) const { - // TODO: implement - return AsIfFlatEdges::selected_interacting (other); + return selected_interacting_generic (other, false); } EdgesDelegate *DeepEdges::selected_not_interacting (const Region &other) const { - // TODO: implement - return AsIfFlatEdges::selected_not_interacting (other); + return selected_interacting_generic (other, true); } EdgesDelegate *DeepEdges::in (const Edges &other, bool invert) const { - // TODO: implement + // TODO: is there a cheaper way? return AsIfFlatEdges::in (other, invert); } diff --git a/src/db/db/dbDeepEdges.h b/src/db/db/dbDeepEdges.h index 3df914ad6..9e4790754 100644 --- a/src/db/db/dbDeepEdges.h +++ b/src/db/db/dbDeepEdges.h @@ -174,6 +174,8 @@ private: DeepLayer edge_region_op (const DeepRegion *other, bool outside, bool include_borders) const; EdgePairs run_check (db::edge_relation_type rel, const Edges *other, db::Coord d, bool whole_edges, metrics_type metrics, double ignore_angle, distance_type min_projection, distance_type max_projection) const; EdgesDelegate *segments (int mode, length_type length, double fraction) const; + EdgesDelegate *selected_interacting_generic (const Edges &edges, bool invert) const; + EdgesDelegate *selected_interacting_generic (const Region ®ion, bool invert) const; }; } diff --git a/src/db/unit_tests/dbDeepEdgesTests.cc b/src/db/unit_tests/dbDeepEdgesTests.cc index b3177d6ca..c7747c4f8 100644 --- a/src/db/unit_tests/dbDeepEdgesTests.cc +++ b/src/db/unit_tests/dbDeepEdgesTests.cc @@ -342,3 +342,47 @@ TEST(7_Partial) db::compare_layouts (_this, target, tl::testsrc () + "/testdata/algo/deep_edges_au7.gds"); } +TEST(8_SelectInteracting) +{ + db::Layout ly; + { + std::string fn (tl::testsrc ()); + fn += "/testdata/algo/deep_region_l1.gds"; + tl::InputStream stream (fn); + db::Reader reader (stream); + reader.read (ly); + } + + db::cell_index_type top_cell_index = *ly.begin_top_down (); + db::Cell &top_cell = ly.cell (top_cell_index); + + db::DeepShapeStore dss; + + unsigned int l2 = ly.get_layer (db::LayerProperties (2, 0)); + unsigned int l3 = ly.get_layer (db::LayerProperties (3, 0)); + + db::Region r2 (db::RecursiveShapeIterator (ly, top_cell, l2), dss); + db::Region r3 (db::RecursiveShapeIterator (ly, top_cell, l3), dss); + db::Edges e2 = r2.edges (); + db::Edges e3 = r3.edges (); + + db::Layout target; + unsigned int target_top_cell_index = target.add_cell (ly.cell_name (top_cell_index)); + + target.insert (target_top_cell_index, target.get_layer (db::LayerProperties (2, 0)), r2); + target.insert (target_top_cell_index, target.get_layer (db::LayerProperties (3, 0)), r3); + + target.insert (target_top_cell_index, target.get_layer (db::LayerProperties (10, 0)), e2.selected_interacting (e3)); + target.insert (target_top_cell_index, target.get_layer (db::LayerProperties (11, 0)), e2.selected_not_interacting (e3)); + target.insert (target_top_cell_index, target.get_layer (db::LayerProperties (12, 0)), e3.selected_interacting (e2)); + target.insert (target_top_cell_index, target.get_layer (db::LayerProperties (13, 0)), e3.selected_not_interacting (e2)); + + target.insert (target_top_cell_index, target.get_layer (db::LayerProperties (20, 0)), e2.selected_interacting (r3)); + target.insert (target_top_cell_index, target.get_layer (db::LayerProperties (21, 0)), e2.selected_not_interacting (r3)); + target.insert (target_top_cell_index, target.get_layer (db::LayerProperties (22, 0)), e3.selected_interacting (r2)); + target.insert (target_top_cell_index, target.get_layer (db::LayerProperties (23, 0)), e3.selected_not_interacting (r2)); + + CHECKPOINT(); + db::compare_layouts (_this, target, tl::testsrc () + "/testdata/algo/deep_edges_au8.gds"); +} + diff --git a/testdata/algo/deep_edges_au8.gds b/testdata/algo/deep_edges_au8.gds new file mode 100644 index 0000000000000000000000000000000000000000..16aa28b5ceca33384b779badcfdf991f480334bb GIT binary patch literal 4010 zcma)4f25Xa~CHt+V<+~soKDTE*@SO`Ky1+h?!1oTi&IS&!Zhp@5}0;#mJk3~Aa zf<~lO<{dW=2By(lg;sSfH(m#od}ou)Cd1LMA6^a0j9{|yNj(nXW#H$Owndj}Uwf~N+wYvb#i}=*H9Qis$&$mF$G%;p0 zesKfh6d>($*2|RT>1%v4W+i zUR92K-Ap~-_Pvs3XKgM~$0RGLB>oLZ(RhtI!`i>UD27pVHy+40qFr>k*6rmf>Q16C z22uX*_R!PqVSzfM)#WF^*B3^y3iC|!8H#&xWM$KNFVW~G1D$~v>U(!&aveR{L$Zv+M zqeO~3iTg3ME_TZ7w1YI9Pl)r`a5YNq9JJTMi~Yoe)#WL6?AAdjMSCz+>fV9ey^7C^ z)ZEdke7=Sfd|sqfpC4*UWHP8 zUZE797b(@}MGAkB9{M+Ih0iM#-A!-U%((;AP<&q8OZE90iXFS;^9ohv^EKaI_?0*r zjUPTm8S(on>H9A+_vM88(4SMMyp}$b*9QCY+J%|CcJ;ozcC+{uSIG13k<2~1F0V~? O<{)nX literal 0 HcmV?d00001