2017-02-12 13:21:08 +01:00
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
|
|
KLayout Layout Viewer
|
2017-02-12 15:28:14 +01:00
|
|
|
Copyright (C) 2006-2017 Matthias Koefferlein
|
2017-02-12 13:21:08 +01:00
|
|
|
|
|
|
|
|
This program is free software; you can redistribute it and/or modify
|
|
|
|
|
it under the terms of the GNU General Public License as published by
|
|
|
|
|
the Free Software Foundation; either version 2 of the License, or
|
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
GNU General Public License for more details.
|
|
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
|
|
|
along with this program; if not, write to the Free Software
|
|
|
|
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#include "tlProgress.h"
|
|
|
|
|
#include "layFinder.h"
|
|
|
|
|
|
|
|
|
|
namespace lay
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @brief A pretty heuristic method to determine the "enclosing distance" of a polygon around a point
|
|
|
|
|
*/
|
|
|
|
|
template<class Iter, class Point>
|
|
|
|
|
double poly_dist (Iter edge, const Point &pt)
|
|
|
|
|
{
|
|
|
|
|
double distance = std::numeric_limits<double>::max ();
|
|
|
|
|
|
|
|
|
|
while (! edge.at_end ()) {
|
|
|
|
|
std::pair <bool, Point> ret = (*edge).projected (pt);
|
|
|
|
|
if (ret.first) {
|
|
|
|
|
double d (ret.second.distance (pt));
|
|
|
|
|
if (d < distance) {
|
|
|
|
|
distance = d;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
++edge;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return distance;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// -------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
// max. number of tries in single-click selection before giving up
|
|
|
|
|
static int point_sel_tests = 10000;
|
|
|
|
|
|
|
|
|
|
// max. number of tries in single-click selection before giving up
|
|
|
|
|
static int inst_point_sel_tests = 10000;
|
|
|
|
|
|
|
|
|
|
// ----------------------------------------------------------------------
|
|
|
|
|
// Finder implementation
|
|
|
|
|
|
|
|
|
|
Finder::Finder (bool point_mode, bool top_level_sel)
|
|
|
|
|
: m_min_level (0), m_max_level (0),
|
|
|
|
|
mp_layout (0), mp_view (0), m_cv_index (0), m_point_mode (point_mode), m_top_level_sel (top_level_sel)
|
|
|
|
|
{
|
|
|
|
|
m_distance = std::numeric_limits<double>::max ();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Finder::~Finder ()
|
|
|
|
|
{
|
|
|
|
|
// .. nothing yet ..
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool
|
|
|
|
|
Finder::closer (double d)
|
|
|
|
|
{
|
|
|
|
|
// the proximity is checked and delivered in micron units.
|
|
|
|
|
d *= mp_view->cellview (m_cv_index)->layout ().dbu ();
|
|
|
|
|
if (d <= m_distance) {
|
|
|
|
|
m_distance = d;
|
|
|
|
|
return true;
|
|
|
|
|
} else {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void
|
|
|
|
|
Finder::start (lay::LayoutView *view, const lay::CellView &cv, unsigned int cv_index, const std::vector<db::ICplxTrans> &trans, const db::Box ®ion, int min_level, int max_level, const std::vector<int> &layers)
|
|
|
|
|
{
|
|
|
|
|
m_layers = layers;
|
|
|
|
|
m_region = region;
|
|
|
|
|
mp_layout = &cv->layout ();
|
|
|
|
|
mp_view = view;
|
|
|
|
|
m_cv_index = cv_index;
|
|
|
|
|
m_min_level = std::max (0, min_level);
|
|
|
|
|
m_max_level = std::max (m_min_level, std::min (max_level, m_top_level_sel ? ((int) cv.specific_path ().size () + 1) : max_level));
|
|
|
|
|
|
|
|
|
|
if (layers.size () == 1) {
|
|
|
|
|
m_box_convert = db::box_convert <db::CellInst> (*mp_layout, (unsigned int) layers [0]);
|
|
|
|
|
m_cell_box_convert = db::box_convert <db::Cell> ((unsigned int) layers [0]);
|
|
|
|
|
} else {
|
|
|
|
|
m_box_convert = db::box_convert <db::CellInst> (*mp_layout);
|
|
|
|
|
m_cell_box_convert = db::box_convert <db::Cell> ();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_path.erase (m_path.begin (), m_path.end ());
|
|
|
|
|
|
|
|
|
|
for (std::vector<db::ICplxTrans>::const_iterator t = trans.begin (); t != trans.end (); ++t) {
|
|
|
|
|
do_find (*cv.cell (), cv.specific_path ().size (), *t * cv.context_trans ());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
unsigned int
|
|
|
|
|
Finder::test_edge (const db::Edge &edg, double &distance, bool &match)
|
|
|
|
|
{
|
|
|
|
|
unsigned int ret = 0;
|
|
|
|
|
|
|
|
|
|
// we hit the region with the edge end points - take the closest vertex
|
|
|
|
|
if (m_region.contains (edg.p1 ()) || m_region.contains (edg.p2 ())) {
|
|
|
|
|
|
|
|
|
|
double d1 = edg.p1 ().double_distance (m_region.center ());
|
|
|
|
|
double d2 = edg.p2 ().double_distance (m_region.center ());
|
|
|
|
|
|
|
|
|
|
// snap to the point - nothing can get closer
|
|
|
|
|
distance = 0.0;
|
|
|
|
|
if (d1 < d2) {
|
|
|
|
|
ret = 1;
|
|
|
|
|
} else {
|
|
|
|
|
ret = 2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
match = true;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// if the edge cuts through the active region: test the
|
|
|
|
|
// edge as a whole
|
|
|
|
|
if (ret == 0 && edg.clipped (m_region).first) {
|
|
|
|
|
double d = edg.distance_abs (m_region.center ());
|
|
|
|
|
if (! match || d < distance) {
|
|
|
|
|
distance = d;
|
|
|
|
|
ret = 3;
|
|
|
|
|
}
|
|
|
|
|
match = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ret;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void
|
|
|
|
|
Finder::do_find (const db::Cell &cell, int level, const db::ICplxTrans &t)
|
|
|
|
|
{
|
|
|
|
|
if (level <= m_max_level /*take level of cell itself*/
|
|
|
|
|
&& cell.is_proxy ()
|
|
|
|
|
&& m_layers.size () == 1
|
|
|
|
|
&& (unsigned int) m_layers [0] == mp_layout->guiding_shape_layer ()) {
|
|
|
|
|
|
|
|
|
|
// when looking at the guiding shape layer, we can visit this cell as well allowing to find the guiding shapes
|
|
|
|
|
|
|
|
|
|
db::Box touch_box (t.inverted () * m_region);
|
|
|
|
|
|
|
|
|
|
if (level >= m_min_level) {
|
|
|
|
|
visit_cell (cell, touch_box, t, level);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else if (level < m_max_level
|
|
|
|
|
&& (t * m_cell_box_convert (cell)).touches (m_region)
|
|
|
|
|
&& (mp_view->select_inside_pcells_mode () || !cell.is_proxy ())
|
|
|
|
|
&& !mp_view->is_cell_hidden (cell.cell_index (), m_cv_index)) {
|
|
|
|
|
|
|
|
|
|
db::Box touch_box (t.inverted () * m_region);
|
|
|
|
|
|
|
|
|
|
if (level >= m_min_level) {
|
|
|
|
|
visit_cell (cell, touch_box, t, level);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
db::Cell::touching_iterator inst = cell.begin_touching (touch_box);
|
|
|
|
|
while (! inst.at_end ()) {
|
|
|
|
|
|
|
|
|
|
const db::CellInstArray &cell_inst = inst->cell_inst ();
|
|
|
|
|
for (db::CellInstArray::iterator p = cell_inst.begin_touching (touch_box, m_box_convert); ! p.at_end (); ++p) {
|
|
|
|
|
|
|
|
|
|
m_path.push_back (db::InstElement (*inst, p));
|
|
|
|
|
|
|
|
|
|
do_find (mp_layout->cell (cell_inst.object ().cell_index ()),
|
|
|
|
|
level + 1,
|
|
|
|
|
t * cell_inst.complex_trans (*p));
|
|
|
|
|
|
|
|
|
|
m_path.pop_back ();
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
++inst;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// -------------------------------------------------------------
|
|
|
|
|
// ShapeFinder implementation
|
|
|
|
|
|
|
|
|
|
ShapeFinder::ShapeFinder (bool point_mode, bool top_level_sel, db::ShapeIterator::flags_type flags, const std::set<lay::ObjectInstPath> *excludes)
|
|
|
|
|
: Finder (point_mode, top_level_sel),
|
|
|
|
|
mp_excludes ((excludes && !excludes->empty ()) ? excludes : 0),
|
|
|
|
|
m_flags (flags), m_cv_index (0), m_topcell (0),
|
|
|
|
|
mp_prop_sel (0), m_inv_prop_sel (false), mp_progress (0)
|
|
|
|
|
{
|
|
|
|
|
m_tries = point_sel_tests;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct LPContextEqualOp
|
|
|
|
|
{
|
|
|
|
|
bool operator() (const lay::LayerPropertiesConstIterator &a, const lay::LayerPropertiesConstIterator &b)
|
|
|
|
|
{
|
|
|
|
|
if (a->cellview_index () != b->cellview_index ()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (a->inverse_prop_sel () != b->inverse_prop_sel ()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (a->prop_sel () != b->prop_sel ()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (a->trans () != b->trans ()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (a->hier_levels () != b->hier_levels ()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct LPContextCompareOp
|
|
|
|
|
{
|
|
|
|
|
bool operator() (const lay::LayerPropertiesConstIterator &a, const lay::LayerPropertiesConstIterator &b)
|
|
|
|
|
{
|
|
|
|
|
if (a->cellview_index () != b->cellview_index ()) {
|
|
|
|
|
return a->cellview_index () < b->cellview_index ();
|
|
|
|
|
}
|
|
|
|
|
if (a->inverse_prop_sel () != b->inverse_prop_sel ()) {
|
|
|
|
|
return a->inverse_prop_sel () < b->inverse_prop_sel ();
|
|
|
|
|
}
|
|
|
|
|
if (a->prop_sel () != b->prop_sel ()) {
|
|
|
|
|
return a->prop_sel () < b->prop_sel ();
|
|
|
|
|
}
|
|
|
|
|
if (a->trans () != b->trans ()) {
|
|
|
|
|
return a->trans () < b->trans ();
|
|
|
|
|
}
|
|
|
|
|
if (a->hier_levels () != b->hier_levels ()) {
|
|
|
|
|
return a->hier_levels () < b->hier_levels ();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
bool
|
|
|
|
|
ShapeFinder::find (lay::LayoutView *view, const db::DBox ®ion_mu)
|
|
|
|
|
{
|
|
|
|
|
tl::AbsoluteProgress progress (tl::to_string (QObject::tr ("Selecting ...")));
|
|
|
|
|
progress.set_unit (1000);
|
|
|
|
|
progress.set_format ("");
|
|
|
|
|
mp_progress = &progress;
|
|
|
|
|
|
|
|
|
|
m_context_layers.clear ();
|
|
|
|
|
m_cells_with_context.clear ();
|
|
|
|
|
|
|
|
|
|
std::vector<lay::LayerPropertiesConstIterator> lprops;
|
|
|
|
|
for (lay::LayerPropertiesConstIterator lp = view->begin_layers (); ! lp.at_end (); ++lp) {
|
|
|
|
|
if (lp->is_visual ()) {
|
|
|
|
|
lprops.push_back (lp);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::sort (lprops.begin (), lprops.end (), LPContextCompareOp ());
|
|
|
|
|
|
|
|
|
|
std::vector<int> layers;
|
|
|
|
|
for (std::vector<lay::LayerPropertiesConstIterator>::const_iterator llp = lprops.begin (); llp != lprops.end (); ) {
|
|
|
|
|
|
|
|
|
|
layers.clear ();
|
|
|
|
|
|
|
|
|
|
lay::LayerPropertiesConstIterator lp0 = *llp;
|
|
|
|
|
LPContextEqualOp eq;
|
|
|
|
|
do {
|
|
|
|
|
layers.push_back ((*llp)->layer_index ());
|
|
|
|
|
++llp;
|
|
|
|
|
} while (llp != lprops.end () && eq(lp0, *llp));
|
|
|
|
|
|
|
|
|
|
find_internal (view, lp0->cellview_index (), &lp0->prop_sel (), lp0->inverse_prop_sel (), lp0->hier_levels (), lp0->trans (), layers, region_mu);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// search on the guiding shapes layer as well
|
|
|
|
|
|
|
|
|
|
// Use the visible layers for the context: the guiding shape is only looked up for cells
|
|
|
|
|
// having one of these layers
|
|
|
|
|
m_context_layers.clear ();
|
|
|
|
|
for (std::vector<lay::LayerPropertiesConstIterator>::const_iterator llp = lprops.begin (); llp != lprops.end (); ++llp) {
|
|
|
|
|
m_context_layers.push_back ((*llp)->layer_index ());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::set< std::pair<db::DCplxTrans, int> > variants = view->cv_transform_variants ();
|
|
|
|
|
for (std::set< std::pair<db::DCplxTrans, int> >::const_iterator v = variants.begin (); v != variants.end (); ++v) {
|
|
|
|
|
|
|
|
|
|
layers.clear ();
|
|
|
|
|
layers.push_back (view->cellview (v->second)->layout ().guiding_shape_layer ());
|
|
|
|
|
|
|
|
|
|
std::vector<db::DCplxTrans> trans;
|
|
|
|
|
trans.push_back (v->first);
|
|
|
|
|
|
|
|
|
|
find_internal (view, (unsigned int) v->second, 0, false, lay::HierarchyLevelSelection (), trans, layers, region_mu);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mp_progress = 0;
|
|
|
|
|
m_cells_with_context.clear ();
|
|
|
|
|
m_context_layers.clear ();
|
|
|
|
|
|
|
|
|
|
return ! m_founds.empty ();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool
|
|
|
|
|
ShapeFinder::find (lay::LayoutView *view, const lay::LayerProperties &lprops, const db::DBox ®ion_mu)
|
|
|
|
|
{
|
|
|
|
|
tl::AbsoluteProgress progress (tl::to_string (QObject::tr ("Selecting ...")));
|
|
|
|
|
progress.set_unit (1000);
|
|
|
|
|
progress.set_format ("");
|
|
|
|
|
mp_progress = &progress;
|
|
|
|
|
|
|
|
|
|
m_cells_with_context.clear ();
|
|
|
|
|
m_context_layers.clear ();
|
|
|
|
|
|
|
|
|
|
std::vector<int> layers;
|
|
|
|
|
layers.push_back (lprops.layer_index ());
|
|
|
|
|
bool result = find_internal (view, lprops.cellview_index (), &lprops.prop_sel (), lprops.inverse_prop_sel (), lprops.hier_levels (), lprops.trans (), layers, region_mu);
|
|
|
|
|
|
|
|
|
|
mp_progress = 0;
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool
|
|
|
|
|
ShapeFinder::find_internal (lay::LayoutView *view, unsigned int cv_index, const std::set<db::properties_id_type> *prop_sel, bool inv_prop_sel, const lay::HierarchyLevelSelection &hier_sel, const std::vector<db::DCplxTrans> &trans_mu, const std::vector<int> &layers, const db::DBox ®ion_mu)
|
|
|
|
|
{
|
|
|
|
|
m_cv_index = cv_index;
|
|
|
|
|
|
|
|
|
|
const lay::CellView &cv = view->cellview (m_cv_index);
|
|
|
|
|
if (! cv.is_valid ()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_topcell = cv.cell_index ();
|
|
|
|
|
|
|
|
|
|
double dbu = cv->layout ().dbu ();
|
|
|
|
|
db::Box region = db::VCplxTrans (1.0 / dbu) * region_mu;
|
|
|
|
|
std::vector<db::ICplxTrans> trans;
|
|
|
|
|
trans.reserve(trans_mu.size());
|
|
|
|
|
for (std::vector<db::DCplxTrans>::const_iterator t = trans_mu.begin(); t != trans_mu.end(); ++t) {
|
|
|
|
|
trans.push_back(db::VCplxTrans(1.0 / dbu) * *t * db::CplxTrans(dbu));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mp_prop_sel = prop_sel;
|
|
|
|
|
m_inv_prop_sel = inv_prop_sel;
|
|
|
|
|
|
|
|
|
|
int ctx_path_length = int (cv.specific_path ().size ());
|
|
|
|
|
|
|
|
|
|
int min_level = view->get_min_hier_levels ();
|
|
|
|
|
int max_level = view->get_max_hier_levels ();
|
|
|
|
|
if (hier_sel.has_from_level ()) {
|
|
|
|
|
min_level = hier_sel.from_level (ctx_path_length, min_level);
|
|
|
|
|
}
|
|
|
|
|
if (hier_sel.has_to_level ()) {
|
|
|
|
|
max_level = hier_sel.to_level (ctx_path_length, max_level);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// actually find
|
|
|
|
|
try {
|
|
|
|
|
start (view, cv, m_cv_index, trans, region, min_level, max_level, layers);
|
|
|
|
|
} catch (StopException) {
|
|
|
|
|
// ..
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// return true if anything was found
|
|
|
|
|
return ! m_founds.empty ();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void
|
|
|
|
|
ShapeFinder::checkpoint ()
|
|
|
|
|
{
|
|
|
|
|
if (! point_mode ()) {
|
|
|
|
|
++*mp_progress;
|
|
|
|
|
} else {
|
|
|
|
|
if (--m_tries < 0) {
|
|
|
|
|
throw StopException ();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void
|
|
|
|
|
ShapeFinder::visit_cell (const db::Cell &cell, const db::Box &search_box, const db::ICplxTrans &t, int /*level*/)
|
|
|
|
|
{
|
|
|
|
|
if (! m_context_layers.empty ()) {
|
|
|
|
|
|
|
|
|
|
std::map<db::cell_index_type, bool>::const_iterator ctx = m_cells_with_context.find (cell.cell_index ());
|
|
|
|
|
if (ctx == m_cells_with_context.end ()) {
|
|
|
|
|
|
|
|
|
|
bool has_context = false;
|
|
|
|
|
for (std::vector<int>::const_iterator l = m_context_layers.begin (); l != m_context_layers.end () && ! has_context; ++l) {
|
|
|
|
|
if (! cell.bbox ((unsigned int) *l).empty ()) {
|
|
|
|
|
has_context = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx = m_cells_with_context.insert (std::make_pair (cell.cell_index (), has_context)).first;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (! ctx->second) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (! point_mode ()) {
|
|
|
|
|
|
|
|
|
|
checkpoint ();
|
|
|
|
|
|
|
|
|
|
for (std::vector<int>::const_iterator l = layers ().begin (); l != layers ().end (); ++l) {
|
|
|
|
|
|
|
|
|
|
if (layers ().size () == 1 || (layers ().size () > 1 && cell.bbox ((unsigned int) *l).touches (search_box))) {
|
|
|
|
|
|
|
|
|
|
const db::Shapes &shapes = cell.shapes ((unsigned int) *l);
|
|
|
|
|
|
|
|
|
|
db::ShapeIterator shape = shapes.begin_touching (search_box, m_flags, mp_prop_sel, m_inv_prop_sel);
|
|
|
|
|
while (! shape.at_end ()) {
|
|
|
|
|
|
|
|
|
|
checkpoint ();
|
|
|
|
|
|
|
|
|
|
// in box mode, just test the boxes
|
|
|
|
|
if (shape->bbox ().inside (search_box)) {
|
|
|
|
|
|
|
|
|
|
m_founds.push_back (lay::ObjectInstPath ());
|
|
|
|
|
m_founds.back ().set_cv_index (m_cv_index);
|
|
|
|
|
m_founds.back ().set_topcell (m_topcell);
|
|
|
|
|
m_founds.back ().assign_path (path ().begin (), path ().end ());
|
|
|
|
|
m_founds.back ().set_layer (*l);
|
|
|
|
|
m_founds.back ().set_shape (*shape);
|
|
|
|
|
|
|
|
|
|
// Remove the selection if it's part of the excluded set
|
|
|
|
|
if (mp_excludes != 0 && mp_excludes->find (m_founds.back ()) != mp_excludes->end ()) {
|
|
|
|
|
m_founds.pop_back ();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
++shape;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
|
|
for (std::vector<int>::const_iterator l = layers ().begin (); l != layers ().end (); ++l) {
|
|
|
|
|
|
|
|
|
|
if (layers ().size () == 1 || (layers ().size () > 1 && cell.bbox ((unsigned int) *l).touches (search_box))) {
|
|
|
|
|
|
|
|
|
|
checkpoint ();
|
|
|
|
|
|
|
|
|
|
const db::Shapes &shapes = cell.shapes (*l);
|
|
|
|
|
|
|
|
|
|
db::ShapeIterator shape = shapes.begin_touching (search_box, m_flags, mp_prop_sel, m_inv_prop_sel);
|
|
|
|
|
while (! shape.at_end ()) {
|
|
|
|
|
|
|
|
|
|
bool match = false;
|
|
|
|
|
double d = std::numeric_limits<double>::max ();
|
|
|
|
|
|
|
|
|
|
checkpoint ();
|
|
|
|
|
|
|
|
|
|
db::Point point (search_box.center ());
|
|
|
|
|
|
|
|
|
|
// in point mode, test the edges and use a "closest" criterion
|
|
|
|
|
if (shape->is_polygon ()) {
|
|
|
|
|
|
|
|
|
|
for (db::Shape::polygon_edge_iterator e = shape->begin_edge (); ! e.at_end (); ++e) {
|
|
|
|
|
test_edge (t * *e, d, match);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// test if inside the polygon
|
|
|
|
|
if (! match && inside_poly (shape->begin_edge (), point) >= 0) {
|
|
|
|
|
d = t.ctrans (poly_dist (shape->begin_edge (), point));
|
|
|
|
|
match = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else if (shape->is_path ()) {
|
|
|
|
|
|
|
|
|
|
// test the "spine"
|
|
|
|
|
db::Shape::point_iterator pt = shape->begin_point ();
|
|
|
|
|
if (pt != shape->end_point ()) {
|
|
|
|
|
db::Point p (*pt);
|
|
|
|
|
++pt;
|
|
|
|
|
for (; pt != shape->end_point (); ++pt) {
|
|
|
|
|
test_edge (t * db::Edge (p, *pt), d, match);
|
|
|
|
|
p = *pt;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// convert to polygon and test those edges
|
|
|
|
|
db::Polygon poly;
|
|
|
|
|
shape->polygon (poly);
|
|
|
|
|
for (db::Polygon::polygon_edge_iterator e = poly.begin_edge (); ! e.at_end (); ++e) {
|
|
|
|
|
test_edge (t * *e, d, match);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// test if inside the polygon
|
|
|
|
|
if (! match && inside_poly (poly.begin_edge (), point) >= 0) {
|
|
|
|
|
d = t.ctrans (poly_dist (poly.begin_edge (), point));
|
|
|
|
|
match = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else if (shape->is_box ()) {
|
|
|
|
|
|
|
|
|
|
const db::Box &box = shape->box ();
|
|
|
|
|
|
|
|
|
|
// point-like boxes are handles which attract the finder
|
|
|
|
|
if (box.width () == 0 && box.height () == 0) {
|
|
|
|
|
d = 0.0;
|
|
|
|
|
match = true;
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
|
|
// convert to polygon and test those edges
|
|
|
|
|
db::Polygon poly (box);
|
|
|
|
|
for (db::Polygon::polygon_edge_iterator e = poly.begin_edge (); ! e.at_end (); ++e) {
|
|
|
|
|
test_edge (t * *e, d, match);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (! match && box.contains (search_box.center ())) {
|
|
|
|
|
d = t.ctrans (poly_dist (poly.begin_edge (), point));
|
|
|
|
|
match = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else if (shape->is_text ()) {
|
|
|
|
|
|
|
|
|
|
db::Point tp (shape->text_trans () * db::Point ());
|
|
|
|
|
if (search_box.contains (tp)) {
|
|
|
|
|
d = t.ctrans (tp.distance (search_box.center ()));
|
|
|
|
|
match = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (match) {
|
|
|
|
|
|
|
|
|
|
if (mp_excludes) {
|
|
|
|
|
|
|
|
|
|
// with an exclude list first create the selection item so we can check
|
|
|
|
|
// if it's part of the exclude set.
|
|
|
|
|
|
|
|
|
|
lay::ObjectInstPath found;
|
|
|
|
|
found.set_cv_index (m_cv_index);
|
|
|
|
|
found.set_topcell (m_topcell);
|
|
|
|
|
found.assign_path (path ().begin (), path ().end ());
|
|
|
|
|
found.set_layer (*l);
|
|
|
|
|
found.set_shape (*shape);
|
|
|
|
|
|
|
|
|
|
// in point mode just store the found object that has the least "distance" and is
|
|
|
|
|
// not in the exclude set
|
|
|
|
|
if (mp_excludes->find (found) == mp_excludes->end () && closer (d)) {
|
|
|
|
|
|
|
|
|
|
if (m_founds.empty ()) {
|
|
|
|
|
m_founds.push_back (found);
|
|
|
|
|
} else {
|
|
|
|
|
m_founds.front () = found;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else if (closer (d)) {
|
|
|
|
|
|
|
|
|
|
// in point mode just store that found that has the least "distance"
|
|
|
|
|
if (m_founds.empty ()) {
|
|
|
|
|
m_founds.push_back (lay::ObjectInstPath ());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_founds.back ().set_cv_index (m_cv_index);
|
|
|
|
|
m_founds.back ().set_topcell (m_topcell);
|
|
|
|
|
m_founds.back ().assign_path (path ().begin (), path ().end ());
|
|
|
|
|
m_founds.back ().set_layer (*l);
|
|
|
|
|
m_founds.back ().set_shape (*shape);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
++shape;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// -------------------------------------------------------------
|
|
|
|
|
// InstFinder implementation
|
|
|
|
|
|
|
|
|
|
InstFinder::InstFinder (bool point_mode, bool top_level_sel, bool full_arrays, bool enclose_inst, const std::set<lay::ObjectInstPath> *excludes, bool visible_layers)
|
|
|
|
|
: Finder (point_mode, top_level_sel),
|
|
|
|
|
m_cv_index (0), m_topcell (0),
|
|
|
|
|
mp_excludes ((excludes && !excludes->empty ()) ? excludes : 0),
|
|
|
|
|
m_full_arrays (full_arrays),
|
|
|
|
|
m_enclose_insts (enclose_inst),
|
|
|
|
|
m_visible_layers (visible_layers),
|
|
|
|
|
mp_view (0),
|
|
|
|
|
mp_progress (0)
|
|
|
|
|
{
|
|
|
|
|
m_tries = inst_point_sel_tests;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool
|
|
|
|
|
InstFinder::find (lay::LayoutView *view, const db::DBox ®ion_mu)
|
|
|
|
|
{
|
|
|
|
|
tl::AbsoluteProgress progress (tl::to_string (QObject::tr ("Selecting ...")));
|
|
|
|
|
progress.set_unit (1000);
|
|
|
|
|
progress.set_format ("");
|
|
|
|
|
mp_progress = &progress;
|
|
|
|
|
|
|
|
|
|
std::set< std::pair<db::DCplxTrans, int> > variants = view->cv_transform_variants ();
|
|
|
|
|
for (std::set< std::pair<db::DCplxTrans, int> >::const_iterator v = variants.begin (); v != variants.end (); ++v) {
|
|
|
|
|
find (view, v->second, v->first, region_mu);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mp_progress = 0;
|
|
|
|
|
return ! m_founds.empty ();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool
|
|
|
|
|
InstFinder::find (lay::LayoutView *view, unsigned int cv_index, const db::DCplxTrans &trans_mu, const db::DBox ®ion_mu)
|
|
|
|
|
{
|
|
|
|
|
tl::AbsoluteProgress progress (tl::to_string (QObject::tr ("Selecting ...")));
|
|
|
|
|
progress.set_unit (1000);
|
|
|
|
|
progress.set_format ("");
|
|
|
|
|
mp_progress = &progress;
|
|
|
|
|
|
|
|
|
|
bool result = find_internal (view, cv_index, trans_mu, region_mu);
|
|
|
|
|
|
|
|
|
|
mp_progress = 0;
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool
|
|
|
|
|
InstFinder::find_internal (lay::LayoutView *view, unsigned int cv_index, const db::DCplxTrans &trans_mu, const db::DBox ®ion_mu)
|
|
|
|
|
{
|
|
|
|
|
const lay::CellView &cv = view->cellview (cv_index);
|
|
|
|
|
if (! cv.is_valid ()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_visible_layer_indexes.clear ();
|
|
|
|
|
if (m_visible_layers) {
|
|
|
|
|
for (lay::LayerPropertiesConstIterator l = view->begin_layers (); !l.at_end (); ++l) {
|
|
|
|
|
if (! l->has_children () && l->visible (true /*real*/) && l->valid (true /*real*/) && l->cellview_index () == int (cv_index)) {
|
|
|
|
|
m_visible_layer_indexes.push_back (l->layer_index ());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!m_visible_layers || view->guiding_shapes_visible ()) {
|
|
|
|
|
m_visible_layer_indexes.push_back (cv->layout ().guiding_shape_layer ());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_cv_index = cv_index;
|
|
|
|
|
m_topcell = cv.cell ()->cell_index ();
|
|
|
|
|
mp_view = view;
|
|
|
|
|
|
|
|
|
|
double dbu = cv->layout ().dbu ();
|
|
|
|
|
db::Box region = db::VCplxTrans (1.0 / dbu) * region_mu;
|
|
|
|
|
|
|
|
|
|
// actually find
|
|
|
|
|
try {
|
|
|
|
|
std::vector<db::ICplxTrans> tv;
|
|
|
|
|
tv.push_back (db::VCplxTrans (1.0 / dbu) * trans_mu * db::CplxTrans (dbu));
|
|
|
|
|
start (view, cv, cv_index, tv, region, view->get_min_hier_levels (), view->get_max_hier_levels (), std::vector<int> ());
|
|
|
|
|
} catch (StopException) {
|
|
|
|
|
// ..
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// return true if anything was found
|
|
|
|
|
return ! m_founds.empty ();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void
|
|
|
|
|
InstFinder::visit_cell (const db::Cell &cell, const db::Box &search_box, const db::ICplxTrans &t, int level)
|
|
|
|
|
{
|
|
|
|
|
if (! point_mode ()) {
|
|
|
|
|
|
|
|
|
|
++*mp_progress;
|
|
|
|
|
|
|
|
|
|
// look for instances to check here ..
|
|
|
|
|
db::Cell::touching_iterator inst = cell.begin_touching (search_box);
|
|
|
|
|
while (! inst.at_end ()) {
|
|
|
|
|
|
|
|
|
|
const db::CellInstArray &cell_inst = inst->cell_inst ();
|
|
|
|
|
const db::Cell &inst_cell = layout ().cell (cell_inst.object ().cell_index ());
|
|
|
|
|
|
|
|
|
|
++*mp_progress;
|
|
|
|
|
|
|
|
|
|
// just consider the instances exactly at the last level of
|
|
|
|
|
// hierarchy (this is where the boxes are drawn) or of cells that
|
|
|
|
|
// are hidden.
|
|
|
|
|
if (level == max_level () - 1 || inst_cell.is_proxy () || mp_view->is_cell_hidden (inst_cell.cell_index (), m_cv_index)) {
|
|
|
|
|
|
|
|
|
|
db::box_convert <db::CellInst, false> bc (layout ());
|
|
|
|
|
for (db::CellInstArray::iterator p = cell_inst.begin_touching (search_box, bc); ! p.at_end (); ++p) {
|
|
|
|
|
|
|
|
|
|
++*mp_progress;
|
|
|
|
|
|
|
|
|
|
db::Box ibox;
|
|
|
|
|
if (inst_cell.bbox ().empty ()) {
|
|
|
|
|
ibox = db::Box (db::Point (0, 0), db::Point (0, 0));
|
|
|
|
|
} else if (! m_visible_layers || level == mp_view->get_max_hier_levels () - 1 || mp_view->is_cell_hidden (inst_cell.cell_index (), m_cv_index)) {
|
|
|
|
|
ibox = inst_cell.bbox ();
|
|
|
|
|
} else {
|
|
|
|
|
for (std::vector<int>::const_iterator l = m_visible_layer_indexes.begin (); l != m_visible_layer_indexes.end (); ++l) {
|
|
|
|
|
ibox += inst_cell.bbox (*l);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (! ibox.empty ()) {
|
|
|
|
|
|
|
|
|
|
db::Box box = cell_inst.complex_trans (*p) * ibox;
|
|
|
|
|
|
|
|
|
|
// in box mode, just test the boxes
|
|
|
|
|
if (! m_enclose_insts || box.inside (search_box)) {
|
|
|
|
|
|
|
|
|
|
// in point mode just store that found that has the least "distance"
|
|
|
|
|
m_founds.push_back (lay::ObjectInstPath ());
|
|
|
|
|
m_founds.back ().set_cv_index (m_cv_index);
|
|
|
|
|
m_founds.back ().set_topcell (m_topcell);
|
|
|
|
|
m_founds.back ().assign_path (path ().begin (), path ().end ());
|
|
|
|
|
|
|
|
|
|
// add the selected instance as the last element of the path
|
|
|
|
|
db::InstElement el;
|
|
|
|
|
el.inst_ptr = *inst;
|
|
|
|
|
if (! m_full_arrays) {
|
|
|
|
|
el.array_inst = p;
|
|
|
|
|
}
|
|
|
|
|
m_founds.back ().add_path (el);
|
|
|
|
|
|
|
|
|
|
// Remove the selection if it's part of the excluded set
|
|
|
|
|
if (mp_excludes != 0 && mp_excludes->find (m_founds.back ()) != mp_excludes->end ()) {
|
|
|
|
|
m_founds.pop_back ();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// in "full arrays" mode, a single reference to that array is sufficient
|
|
|
|
|
if (m_full_arrays) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
++inst;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
|
|
if (--m_tries < 0) {
|
|
|
|
|
throw StopException ();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// look for instances to check here ..
|
|
|
|
|
db::Cell::touching_iterator inst = cell.begin_touching (search_box);
|
|
|
|
|
while (! inst.at_end ()) {
|
|
|
|
|
|
|
|
|
|
if (--m_tries < 0) {
|
|
|
|
|
throw StopException ();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const db::CellInstArray &cell_inst = inst->cell_inst ();
|
|
|
|
|
const db::Cell &inst_cell = layout ().cell (cell_inst.object ().cell_index ());
|
|
|
|
|
|
|
|
|
|
// just consider the instances exactly at the last level of
|
|
|
|
|
// hierarchy (this is where the boxes are drawn) or if of cells that
|
|
|
|
|
// are hidden.
|
|
|
|
|
if (level == max_level () - 1 || inst_cell.is_proxy () || mp_view->is_cell_hidden (inst_cell.cell_index (), m_cv_index)) {
|
|
|
|
|
|
|
|
|
|
db::box_convert <db::CellInst, false> bc (layout ());
|
|
|
|
|
for (db::CellInstArray::iterator p = cell_inst.begin_touching (search_box, bc); ! p.at_end (); ++p) {
|
|
|
|
|
|
|
|
|
|
if (--m_tries < 0) {
|
|
|
|
|
throw StopException ();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool match = false;
|
|
|
|
|
double d = std::numeric_limits<double>::max ();
|
|
|
|
|
|
|
|
|
|
db::Box ibox;
|
|
|
|
|
if (inst_cell.bbox ().empty ()) {
|
|
|
|
|
ibox = db::Box (db::Point (0, 0), db::Point (0, 0));
|
|
|
|
|
} else if (! m_visible_layers || level == mp_view->get_max_hier_levels () - 1 || mp_view->is_cell_hidden (inst_cell.cell_index (), m_cv_index)) {
|
|
|
|
|
ibox = inst_cell.bbox ();
|
|
|
|
|
} else {
|
|
|
|
|
for (std::vector<int>::const_iterator l = m_visible_layer_indexes.begin (); l != m_visible_layer_indexes.end (); ++l) {
|
|
|
|
|
ibox += inst_cell.bbox (*l);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (! ibox.empty ()) {
|
|
|
|
|
|
|
|
|
|
if (ibox.width () == 0 && ibox.height () == 0) {
|
|
|
|
|
match = true;
|
|
|
|
|
d = 0.0;
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
|
|
// convert to polygon and test those edges
|
|
|
|
|
db::Polygon poly (cell_inst.complex_trans (*p) * db::Polygon (ibox));
|
|
|
|
|
|
|
|
|
|
for (db::Polygon::polygon_edge_iterator e = poly.begin_edge (); ! e.at_end (); ++e) {
|
|
|
|
|
test_edge (t * *e, d, match);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (! match && db::inside_poly (poly.begin_edge (), search_box.center ())) {
|
|
|
|
|
d = t.ctrans (poly_dist (poly.begin_edge (), search_box.center ()));
|
|
|
|
|
match = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
d += 1; // the instance has a small penalty so that shapes win over instances
|
|
|
|
|
|
|
|
|
|
if (match && closer (d)) {
|
|
|
|
|
|
|
|
|
|
// in point mode just store that found that has the least "distance"
|
|
|
|
|
if (m_founds.empty ()) {
|
|
|
|
|
m_founds.push_back (lay::ObjectInstPath ());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_founds.back ().set_cv_index (m_cv_index);
|
|
|
|
|
m_founds.back ().set_topcell (m_topcell);
|
|
|
|
|
m_founds.back ().assign_path (path ().begin (), path ().end ());
|
|
|
|
|
|
|
|
|
|
// add the selected instance as the last element of the path
|
|
|
|
|
db::InstElement el;
|
|
|
|
|
el.inst_ptr = *inst;
|
|
|
|
|
if (! m_full_arrays) {
|
|
|
|
|
el.array_inst = p;
|
|
|
|
|
}
|
|
|
|
|
m_founds.back ().add_path (el);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (match) {
|
|
|
|
|
|
|
|
|
|
if (mp_excludes) {
|
|
|
|
|
|
|
|
|
|
// with an exclude list first create the selection item so we can check
|
|
|
|
|
// if it's part of the exclude set.
|
|
|
|
|
|
|
|
|
|
lay::ObjectInstPath found;
|
|
|
|
|
found.set_cv_index (m_cv_index);
|
|
|
|
|
found.set_topcell (m_topcell);
|
|
|
|
|
found.assign_path (path ().begin (), path ().end ());
|
|
|
|
|
|
|
|
|
|
// add the selected instance as the last element of the path
|
|
|
|
|
db::InstElement el;
|
|
|
|
|
el.inst_ptr = *inst;
|
|
|
|
|
if (! m_full_arrays) {
|
|
|
|
|
el.array_inst = p;
|
|
|
|
|
}
|
|
|
|
|
found.add_path (el);
|
|
|
|
|
|
|
|
|
|
// in point mode just store the found object that has the least "distance" and is
|
|
|
|
|
// not in the exclude set
|
|
|
|
|
if (mp_excludes->find (found) == mp_excludes->end () && closer (d)) {
|
|
|
|
|
if (m_founds.empty ()) {
|
|
|
|
|
m_founds.push_back (found);
|
|
|
|
|
} else {
|
|
|
|
|
m_founds.front () = found;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else if (closer (d)) {
|
|
|
|
|
|
|
|
|
|
// in point mode just store that found that has the least "distance"
|
|
|
|
|
if (m_founds.empty ()) {
|
|
|
|
|
m_founds.push_back (lay::ObjectInstPath ());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_founds.back ().set_cv_index (m_cv_index);
|
|
|
|
|
m_founds.back ().set_topcell (m_topcell);
|
|
|
|
|
m_founds.back ().assign_path (path ().begin (), path ().end ());
|
|
|
|
|
|
|
|
|
|
// add the selected instance as the last element of the path
|
|
|
|
|
db::InstElement el;
|
|
|
|
|
el.inst_ptr = *inst;
|
|
|
|
|
if (! m_full_arrays) {
|
|
|
|
|
el.array_inst = p;
|
|
|
|
|
}
|
|
|
|
|
m_founds.back ().add_path (el);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
++inst;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace edt
|
|
|
|
|
|