718 lines
17 KiB
C++
718 lines
17 KiB
C++
/*
|
|
* This program source code file is part of KiCad, a free EDA CAD application.
|
|
*
|
|
* Copyright (C) 2013-2017 CERN
|
|
* @author Tomasz Wlostowski <tomasz.wlostowski@cern.ch>
|
|
*
|
|
* 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, you may find one here:
|
|
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
|
* or you may search the http://www.gnu.org website for the version 2 license,
|
|
* or you may write to the Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
|
*/
|
|
|
|
#include <algorithm>
|
|
|
|
#include <geometry/shape_line_chain.h>
|
|
#include <geometry/shape_circle.h>
|
|
#include <trigo.h>
|
|
#include "clipper.hpp"
|
|
|
|
|
|
ClipperLib::Path SHAPE_LINE_CHAIN::convertToClipper( bool aRequiredOrientation ) const
|
|
{
|
|
ClipperLib::Path c_path;
|
|
|
|
for( int i = 0; i < PointCount(); i++ )
|
|
{
|
|
const VECTOR2I& vertex = CPoint( i );
|
|
c_path.push_back( ClipperLib::IntPoint( vertex.x, vertex.y ) );
|
|
}
|
|
|
|
if( Orientation( c_path ) != aRequiredOrientation )
|
|
ReversePath( c_path );
|
|
|
|
return c_path;
|
|
}
|
|
|
|
|
|
bool SHAPE_LINE_CHAIN::Collide( const VECTOR2I& aP, int aClearance ) const
|
|
{
|
|
// fixme: ugly!
|
|
SEG s( aP, aP );
|
|
return this->Collide( s, aClearance );
|
|
}
|
|
|
|
|
|
void SHAPE_LINE_CHAIN::Rotate( double aAngle, const VECTOR2I& aCenter )
|
|
{
|
|
for( std::vector<VECTOR2I>::iterator i = m_points.begin(); i != m_points.end(); ++i )
|
|
{
|
|
(*i) -= aCenter;
|
|
(*i) = (*i).Rotate( aAngle );
|
|
(*i) += aCenter;
|
|
}
|
|
}
|
|
|
|
|
|
bool SHAPE_LINE_CHAIN::Collide( const SEG& aSeg, int aClearance ) const
|
|
{
|
|
BOX2I box_a( aSeg.A, aSeg.B - aSeg.A );
|
|
BOX2I::ecoord_type dist_sq = (BOX2I::ecoord_type) aClearance * aClearance;
|
|
|
|
for( int i = 0; i < SegmentCount(); i++ )
|
|
{
|
|
const SEG& s = CSegment( i );
|
|
BOX2I box_b( s.A, s.B - s.A );
|
|
|
|
BOX2I::ecoord_type d = box_a.SquaredDistance( box_b );
|
|
|
|
if( d < dist_sq )
|
|
{
|
|
if( s.Collide( aSeg, aClearance ) )
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
const SHAPE_LINE_CHAIN SHAPE_LINE_CHAIN::Reverse() const
|
|
{
|
|
SHAPE_LINE_CHAIN a( *this );
|
|
|
|
reverse( a.m_points.begin(), a.m_points.end() );
|
|
a.m_closed = m_closed;
|
|
|
|
return a;
|
|
}
|
|
|
|
|
|
int SHAPE_LINE_CHAIN::Length() const
|
|
{
|
|
int l = 0;
|
|
|
|
for( int i = 0; i < SegmentCount(); i++ )
|
|
l += CSegment( i ).Length();
|
|
|
|
return l;
|
|
}
|
|
|
|
|
|
void SHAPE_LINE_CHAIN::Replace( int aStartIndex, int aEndIndex, const VECTOR2I& aP )
|
|
{
|
|
if( aEndIndex < 0 )
|
|
aEndIndex += PointCount();
|
|
|
|
if( aStartIndex < 0 )
|
|
aStartIndex += PointCount();
|
|
|
|
if( aStartIndex == aEndIndex )
|
|
m_points[aStartIndex] = aP;
|
|
else
|
|
{
|
|
m_points.erase( m_points.begin() + aStartIndex + 1, m_points.begin() + aEndIndex + 1 );
|
|
m_points[aStartIndex] = aP;
|
|
}
|
|
}
|
|
|
|
|
|
void SHAPE_LINE_CHAIN::Replace( int aStartIndex, int aEndIndex, const SHAPE_LINE_CHAIN& aLine )
|
|
{
|
|
if( aEndIndex < 0 )
|
|
aEndIndex += PointCount();
|
|
|
|
if( aStartIndex < 0 )
|
|
aStartIndex += PointCount();
|
|
|
|
m_points.erase( m_points.begin() + aStartIndex, m_points.begin() + aEndIndex + 1 );
|
|
m_points.insert( m_points.begin() + aStartIndex, aLine.m_points.begin(), aLine.m_points.end() );
|
|
}
|
|
|
|
|
|
void SHAPE_LINE_CHAIN::Remove( int aStartIndex, int aEndIndex )
|
|
{
|
|
if( aEndIndex < 0 )
|
|
aEndIndex += PointCount();
|
|
|
|
if( aStartIndex < 0 )
|
|
aStartIndex += PointCount();
|
|
|
|
m_points.erase( m_points.begin() + aStartIndex, m_points.begin() + aEndIndex + 1 );
|
|
}
|
|
|
|
|
|
int SHAPE_LINE_CHAIN::Distance( const VECTOR2I& aP, bool aOutlineOnly ) const
|
|
{
|
|
int d = INT_MAX;
|
|
|
|
if( IsClosed() && PointInside( aP ) && !aOutlineOnly )
|
|
return 0;
|
|
|
|
for( int s = 0; s < SegmentCount(); s++ )
|
|
d = std::min( d, CSegment( s ).Distance( aP ) );
|
|
|
|
return d;
|
|
}
|
|
|
|
|
|
int SHAPE_LINE_CHAIN::Split( const VECTOR2I& aP )
|
|
{
|
|
int ii = -1;
|
|
int min_dist = 2;
|
|
|
|
int found_index = Find( aP );
|
|
|
|
for( int s = 0; s < SegmentCount(); s++ )
|
|
{
|
|
const SEG seg = CSegment( s );
|
|
int dist = seg.Distance( aP );
|
|
|
|
// make sure we are not producing a 'slightly concave' primitive. This might happen
|
|
// if aP lies very close to one of already existing points.
|
|
if( dist < min_dist && seg.A != aP && seg.B != aP )
|
|
{
|
|
min_dist = dist;
|
|
if( found_index < 0 )
|
|
ii = s;
|
|
else if( s < found_index )
|
|
ii = s;
|
|
}
|
|
}
|
|
|
|
if( ii < 0 )
|
|
ii = found_index;
|
|
|
|
if( ii >= 0 )
|
|
{
|
|
m_points.insert( m_points.begin() + ii + 1, aP );
|
|
|
|
return ii + 1;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
|
|
int SHAPE_LINE_CHAIN::Find( const VECTOR2I& aP ) const
|
|
{
|
|
for( int s = 0; s < PointCount(); s++ )
|
|
if( CPoint( s ) == aP )
|
|
return s;
|
|
|
|
return -1;
|
|
}
|
|
|
|
|
|
int SHAPE_LINE_CHAIN::FindSegment( const VECTOR2I& aP ) const
|
|
{
|
|
for( int s = 0; s < SegmentCount(); s++ )
|
|
if( CSegment( s ).Distance( aP ) <= 1 )
|
|
return s;
|
|
|
|
return -1;
|
|
}
|
|
|
|
|
|
const SHAPE_LINE_CHAIN SHAPE_LINE_CHAIN::Slice( int aStartIndex, int aEndIndex ) const
|
|
{
|
|
SHAPE_LINE_CHAIN rv;
|
|
|
|
if( aEndIndex < 0 )
|
|
aEndIndex += PointCount();
|
|
|
|
if( aStartIndex < 0 )
|
|
aStartIndex += PointCount();
|
|
|
|
for( int i = aStartIndex; i <= aEndIndex; i++ )
|
|
rv.Append( m_points[i] );
|
|
|
|
return rv;
|
|
}
|
|
|
|
|
|
struct compareOriginDistance
|
|
{
|
|
compareOriginDistance( VECTOR2I& aOrigin ) :
|
|
m_origin( aOrigin ) {};
|
|
|
|
bool operator()( const SHAPE_LINE_CHAIN::INTERSECTION& aA,
|
|
const SHAPE_LINE_CHAIN::INTERSECTION& aB )
|
|
{
|
|
return ( m_origin - aA.p ).EuclideanNorm() < ( m_origin - aB.p ).EuclideanNorm();
|
|
}
|
|
|
|
VECTOR2I m_origin;
|
|
};
|
|
|
|
|
|
int SHAPE_LINE_CHAIN::Intersect( const SEG& aSeg, INTERSECTIONS& aIp ) const
|
|
{
|
|
for( int s = 0; s < SegmentCount(); s++ )
|
|
{
|
|
OPT_VECTOR2I p = CSegment( s ).Intersect( aSeg );
|
|
|
|
if( p )
|
|
{
|
|
INTERSECTION is;
|
|
is.our = CSegment( s );
|
|
is.their = aSeg;
|
|
is.p = *p;
|
|
aIp.push_back( is );
|
|
}
|
|
}
|
|
|
|
compareOriginDistance comp( aSeg.A );
|
|
sort( aIp.begin(), aIp.end(), comp );
|
|
|
|
return aIp.size();
|
|
}
|
|
|
|
|
|
int SHAPE_LINE_CHAIN::Intersect( const SHAPE_LINE_CHAIN& aChain, INTERSECTIONS& aIp ) const
|
|
{
|
|
BOX2I bb_other = aChain.BBox();
|
|
|
|
for( int s1 = 0; s1 < SegmentCount(); s1++ )
|
|
{
|
|
const SEG& a = CSegment( s1 );
|
|
const BOX2I bb_cur( a.A, a.B - a.A );
|
|
|
|
if( !bb_other.Intersects( bb_cur ) )
|
|
continue;
|
|
|
|
for( int s2 = 0; s2 < aChain.SegmentCount(); s2++ )
|
|
{
|
|
const SEG& b = aChain.CSegment( s2 );
|
|
INTERSECTION is;
|
|
|
|
if( a.Collinear( b ) )
|
|
{
|
|
is.our = a;
|
|
is.their = b;
|
|
|
|
if( a.Contains( b.A ) ) { is.p = b.A; aIp.push_back( is ); }
|
|
if( a.Contains( b.B ) ) { is.p = b.B; aIp.push_back( is ); }
|
|
if( b.Contains( a.A ) ) { is.p = a.A; aIp.push_back( is ); }
|
|
if( b.Contains( a.B ) ) { is.p = a.B; aIp.push_back( is ); }
|
|
}
|
|
else
|
|
{
|
|
OPT_VECTOR2I p = a.Intersect( b );
|
|
|
|
if( p )
|
|
{
|
|
is.p = *p;
|
|
is.our = a;
|
|
is.their = b;
|
|
aIp.push_back( is );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return aIp.size();
|
|
}
|
|
|
|
|
|
int SHAPE_LINE_CHAIN::PathLength( const VECTOR2I& aP ) const
|
|
{
|
|
int sum = 0;
|
|
|
|
for( int i = 0; i < SegmentCount(); i++ )
|
|
{
|
|
const SEG seg = CSegment( i );
|
|
int d = seg.Distance( aP );
|
|
|
|
if( d <= 1 )
|
|
{
|
|
sum += ( aP - seg.A ).EuclideanNorm();
|
|
return sum;
|
|
}
|
|
else
|
|
sum += seg.Length();
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
|
|
bool SHAPE_LINE_CHAIN::PointInside( const VECTOR2I& aPt, int aAccuracy ) const
|
|
{
|
|
/*
|
|
* Don't check the bounding box. Building it is about the same speed as the rigorous
|
|
* test below and so just slows things down by doing potentially two tests.
|
|
*/
|
|
|
|
if( !m_closed || PointCount() < 3 )
|
|
return false;
|
|
|
|
bool inside = false;
|
|
|
|
/**
|
|
* To check for interior points, we draw a line in the positive x direction from
|
|
* the point. If it intersects an even number of segments, the point is outside the
|
|
* line chain (it had to first enter and then exit). Otherwise, it is inside the chain.
|
|
*
|
|
* Note: slope might be denormal here in the case of a horizontal line but we require our
|
|
* y to move from above to below the point (or vice versa)
|
|
*
|
|
* Note: we open-code CPoint() here so that we don't end up calculating the size of the
|
|
* vector number-of-points times. This has a non-trivial impact on zone fill times.
|
|
*/
|
|
const std::vector<VECTOR2I>& points = CPoints();
|
|
int pointCount = points.size();
|
|
|
|
for( int i = 0; i < pointCount; )
|
|
{
|
|
const auto p1 = points[ i++ ];
|
|
const auto p2 = points[ i == pointCount ? 0 : i ];
|
|
const auto diff = p2 - p1;
|
|
|
|
if( diff.y != 0 )
|
|
{
|
|
const int d = rescale( diff.x, ( aPt.y - p1.y ), diff.y );
|
|
|
|
if( ( ( p1.y > aPt.y ) != ( p2.y > aPt.y ) ) && ( aPt.x - p1.x < d ) )
|
|
inside = !inside;
|
|
}
|
|
}
|
|
|
|
// If aAccuracy is > 0 then by definition we don't care whether or not the point is
|
|
// *exactly* on the edge -- which saves us considerable processing time
|
|
return inside && ( aAccuracy > 0 || !PointOnEdge( aPt ) );
|
|
}
|
|
|
|
|
|
bool SHAPE_LINE_CHAIN::PointOnEdge( const VECTOR2I& aPt, int aAccuracy ) const
|
|
{
|
|
return EdgeContainingPoint( aPt, aAccuracy ) >= 0;
|
|
}
|
|
|
|
int SHAPE_LINE_CHAIN::EdgeContainingPoint( const VECTOR2I& aPt, int aAccuracy ) const
|
|
{
|
|
if( !PointCount() )
|
|
return -1;
|
|
|
|
else if( PointCount() == 1 )
|
|
{
|
|
VECTOR2I dist = m_points[0] - aPt;
|
|
return ( hypot( dist.x, dist.y ) <= aAccuracy + 1 ) ? 0 : -1;
|
|
}
|
|
|
|
for( int i = 0; i < SegmentCount(); i++ )
|
|
{
|
|
const SEG s = CSegment( i );
|
|
|
|
if( s.A == aPt || s.B == aPt )
|
|
return i;
|
|
|
|
if( s.Distance( aPt ) <= aAccuracy + 1 )
|
|
return i;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
|
|
bool SHAPE_LINE_CHAIN::CheckClearance( const VECTOR2I& aP, const int aDist) const
|
|
{
|
|
if( !PointCount() )
|
|
return false;
|
|
|
|
else if( PointCount() == 1 )
|
|
return m_points[0] == aP;
|
|
|
|
for( int i = 0; i < SegmentCount(); i++ )
|
|
{
|
|
const SEG s = CSegment( i );
|
|
|
|
if( s.A == aP || s.B == aP )
|
|
return true;
|
|
|
|
if( s.Distance( aP ) <= aDist )
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
const OPT<SHAPE_LINE_CHAIN::INTERSECTION> SHAPE_LINE_CHAIN::SelfIntersecting() const
|
|
{
|
|
for( int s1 = 0; s1 < SegmentCount(); s1++ )
|
|
{
|
|
for( int s2 = s1 + 1; s2 < SegmentCount(); s2++ )
|
|
{
|
|
const VECTOR2I s2a = CSegment( s2 ).A, s2b = CSegment( s2 ).B;
|
|
|
|
if( s1 + 1 != s2 && CSegment( s1 ).Contains( s2a ) )
|
|
{
|
|
INTERSECTION is;
|
|
is.our = CSegment( s1 );
|
|
is.their = CSegment( s2 );
|
|
is.p = s2a;
|
|
return is;
|
|
}
|
|
else if( CSegment( s1 ).Contains( s2b ) &&
|
|
// for closed polylines, the ending point of the
|
|
// last segment == starting point of the first segment
|
|
// this is a normal case, not self intersecting case
|
|
!( IsClosed() && s1 == 0 && s2 == SegmentCount()-1 ) )
|
|
{
|
|
INTERSECTION is;
|
|
is.our = CSegment( s1 );
|
|
is.their = CSegment( s2 );
|
|
is.p = s2b;
|
|
return is;
|
|
}
|
|
else
|
|
{
|
|
OPT_VECTOR2I p = CSegment( s1 ).Intersect( CSegment( s2 ), true );
|
|
|
|
if( p )
|
|
{
|
|
INTERSECTION is;
|
|
is.our = CSegment( s1 );
|
|
is.their = CSegment( s2 );
|
|
is.p = *p;
|
|
return is;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return OPT<SHAPE_LINE_CHAIN::INTERSECTION>();
|
|
}
|
|
|
|
|
|
SHAPE_LINE_CHAIN& SHAPE_LINE_CHAIN::Simplify()
|
|
{
|
|
std::vector<VECTOR2I> pts_unique;
|
|
|
|
if( PointCount() < 2 )
|
|
{
|
|
return *this;
|
|
}
|
|
else if( PointCount() == 2 )
|
|
{
|
|
if( m_points[0] == m_points[1] )
|
|
m_points.pop_back();
|
|
|
|
return *this;
|
|
}
|
|
|
|
int i = 0;
|
|
int np = PointCount();
|
|
|
|
// stage 1: eliminate duplicate vertices
|
|
while( i < np )
|
|
{
|
|
int j = i + 1;
|
|
|
|
while( j < np && CPoint( i ) == CPoint( j ) )
|
|
j++;
|
|
|
|
pts_unique.push_back( CPoint( i ) );
|
|
i = j;
|
|
}
|
|
|
|
m_points.clear();
|
|
np = pts_unique.size();
|
|
|
|
i = 0;
|
|
|
|
// stage 1: eliminate collinear segments
|
|
while( i < np - 2 )
|
|
{
|
|
const VECTOR2I p0 = pts_unique[i];
|
|
const VECTOR2I p1 = pts_unique[i + 1];
|
|
int n = i;
|
|
|
|
while( n < np - 2 && SEG( p0, p1 ).LineDistance( pts_unique[n + 2] ) <= 1 )
|
|
n++;
|
|
|
|
m_points.push_back( p0 );
|
|
|
|
if( n > i )
|
|
i = n;
|
|
|
|
if( n == np )
|
|
{
|
|
m_points.push_back( pts_unique[n - 1] );
|
|
return *this;
|
|
}
|
|
|
|
i++;
|
|
}
|
|
|
|
if( np > 1 )
|
|
m_points.push_back( pts_unique[np - 2] );
|
|
|
|
m_points.push_back( pts_unique[np - 1] );
|
|
|
|
return *this;
|
|
}
|
|
|
|
|
|
const VECTOR2I SHAPE_LINE_CHAIN::NearestPoint( const VECTOR2I& aP ) const
|
|
{
|
|
int min_d = INT_MAX;
|
|
int nearest = 0;
|
|
|
|
for( int i = 0; i < SegmentCount(); i++ )
|
|
{
|
|
int d = CSegment( i ).Distance( aP );
|
|
|
|
if( d < min_d )
|
|
{
|
|
min_d = d;
|
|
nearest = i;
|
|
}
|
|
}
|
|
|
|
return CSegment( nearest ).NearestPoint( aP );
|
|
}
|
|
|
|
|
|
const VECTOR2I SHAPE_LINE_CHAIN::NearestPoint( const SEG& aSeg, int& dist ) const
|
|
{
|
|
int nearest = 0;
|
|
|
|
dist = INT_MAX;
|
|
for( int i = 0; i < PointCount(); i++ )
|
|
{
|
|
int d = aSeg.LineDistance( CPoint( i ) );
|
|
|
|
if( d < dist )
|
|
{
|
|
dist = d;
|
|
nearest = i;
|
|
}
|
|
}
|
|
|
|
return CPoint( nearest );
|
|
}
|
|
|
|
|
|
const std::string SHAPE_LINE_CHAIN::Format() const
|
|
{
|
|
std::stringstream ss;
|
|
|
|
ss << m_points.size() << " " << ( m_closed ? 1 : 0 ) << " ";
|
|
|
|
for( int i = 0; i < PointCount(); i++ )
|
|
ss << m_points[i].x << " " << m_points[i].y << " "; // Format() << " ";
|
|
|
|
return ss.str();
|
|
}
|
|
|
|
|
|
bool SHAPE_LINE_CHAIN::CompareGeometry ( const SHAPE_LINE_CHAIN & aOther ) const
|
|
{
|
|
SHAPE_LINE_CHAIN a(*this), b( aOther );
|
|
a.Simplify();
|
|
b.Simplify();
|
|
|
|
if( a.m_points.size() != b.m_points.size() )
|
|
return false;
|
|
|
|
for( int i = 0; i < a.PointCount(); i++)
|
|
if( a.CPoint( i ) != b.CPoint( i ) )
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
|
|
bool SHAPE_LINE_CHAIN::Intersects( const SHAPE_LINE_CHAIN& aChain ) const
|
|
{
|
|
INTERSECTIONS dummy;
|
|
return Intersect( aChain, dummy ) != 0;
|
|
}
|
|
|
|
|
|
SHAPE* SHAPE_LINE_CHAIN::Clone() const
|
|
{
|
|
return new SHAPE_LINE_CHAIN( *this );
|
|
}
|
|
|
|
bool SHAPE_LINE_CHAIN::Parse( std::stringstream& aStream )
|
|
{
|
|
int n_pts;
|
|
|
|
m_points.clear();
|
|
aStream >> n_pts;
|
|
|
|
// Rough sanity check, just make sure the loop bounds aren't absolutely outlandish
|
|
if( n_pts < 0 || n_pts > int( aStream.str().size() ) )
|
|
return false;
|
|
|
|
aStream >> m_closed;
|
|
|
|
for( int i = 0; i < n_pts; i++ )
|
|
{
|
|
int x, y;
|
|
aStream >> x;
|
|
aStream >> y;
|
|
m_points.push_back( VECTOR2I( x, y ) );
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
const VECTOR2I SHAPE_LINE_CHAIN::PointAlong( int aPathLength ) const
|
|
{
|
|
int total = 0;
|
|
|
|
if( aPathLength == 0 )
|
|
return CPoint( 0 );
|
|
|
|
for( int i = 0; i < SegmentCount(); i++ )
|
|
{
|
|
const SEG& s = CSegment( i );
|
|
int l = s.Length();
|
|
|
|
if( total + l >= aPathLength )
|
|
{
|
|
VECTOR2I d( s.B - s.A );
|
|
return s.A + d.Resize( aPathLength - total );
|
|
}
|
|
|
|
total += l;
|
|
}
|
|
|
|
return CPoint( -1 );
|
|
}
|
|
|
|
double SHAPE_LINE_CHAIN::Area() const
|
|
{
|
|
// see https://www.mathopenref.com/coordpolygonarea2.html
|
|
|
|
if( !m_closed )
|
|
return 0.0;
|
|
|
|
double area = 0.0;
|
|
int size = m_points.size();
|
|
|
|
for( int i = 0, j = size - 1; i < size; ++i )
|
|
{
|
|
area += ( (double) m_points[j].x + m_points[i].x ) * ( (double) m_points[j].y - m_points[i].y );
|
|
j = i;
|
|
}
|
|
|
|
return -area * 0.5;
|
|
}
|