KiCad PCB EDA Suite
Loading...
Searching...
No Matches
graphic_trim.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software: you can redistribute it and/or modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation, either version 3 of the License, or (at your
9 * option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
20#include <tools/graphic_trim.h>
21
22#include <board_item.h>
23#include <geometry/circle.h>
25#include <geometry/seg.h>
27#include <pcb_shape.h>
29
30#include <algorithm>
31#include <cmath>
32#include <numbers>
33
35static constexpr double PARAM_EPSILON = 1e-7;
36
43
44
45static double arcParameter( const SHAPE_ARC& aSource, const VECTOR2I& aPoint )
46{
47 // ConstructFromStartEndCenter cannot measure a zero or full sweep. Take the ends direct.
48 if( GraphicEditCoincident( aPoint, aSource.GetP0() ) )
49 return 0.0;
50
51 if( GraphicEditCoincident( aPoint, aSource.GetP1() ) )
52 return 1.0;
53
54 SHAPE_ARC portion;
55 portion.ConstructFromStartEndCenter( aSource.GetP0(), aPoint, aSource.GetCenter(), aSource.IsClockwise() );
56
57 return std::abs( portion.GetCentralAngle().AsDegrees() / aSource.GetCentralAngle().AsDegrees() );
58}
59
60
64{
70 std::vector<SEG> m_Untouched;
71
72 bool IsClosed() const { return m_Kind == SHAPE_T::CIRCLE; }
73
74 VECTOR2I Start() const { return m_Kind == SHAPE_T::ARC ? m_Arc.GetP0() : m_Seg.A; }
75 VECTOR2I End() const { return m_Kind == SHAPE_T::ARC ? m_Arc.GetP1() : m_Seg.B; }
76
77 double Parameter( const VECTOR2I& aPoint ) const
78 {
79 if( m_Kind == SHAPE_T::ARC )
80 return arcParameter( m_Arc, aPoint );
81
82 if( m_Kind == SHAPE_T::CIRCLE )
83 {
84 double turn = std::atan2( (double) aPoint.y - m_Circle.Center.y, (double) aPoint.x - m_Circle.Center.x );
85
86 if( turn < 0.0 )
87 turn += 2.0 * std::numbers::pi;
88
89 return turn / ( 2.0 * std::numbers::pi );
90 }
91
92 return KIGEOM::ParameterAlong( m_Seg, aPoint );
93 }
94
96 double PointerParameter( const VECTOR2I& aPointer ) const
97 {
98 // NearestPoint() is clamped to the arc. A line projection is not.
99 if( m_Kind == SHAPE_T::ARC )
100 return Parameter( m_Arc.NearestPoint( aPointer ) );
101
102 if( m_Kind == SHAPE_T::CIRCLE )
103 return Parameter( aPointer );
104
105 return std::clamp( Parameter( aPointer ), 0.0, 1.0 );
106 }
107
109 GRAPHIC_EDIT_GEOMETRY Span( const VECTOR2I& aFrom, const VECTOR2I& aTo, double aFromParameter,
110 double aToParameter ) const
111 {
113
114 span.m_Start = aFrom;
115 span.m_End = aTo;
116
117 if( m_Kind == SHAPE_T::SEGMENT )
118 {
120 return span;
121 }
122
123 span.m_Shape = SHAPE_T::ARC;
124
125 if( m_Kind == SHAPE_T::ARC )
126 {
127 SHAPE_ARC retained;
128 retained.ConstructFromStartEndCenter( aFrom, aTo, m_Arc.GetCenter(), m_Arc.IsClockwise() );
129 span.m_Mid = retained.GetArcMid();
130 return span;
131 }
132
133 // Round the circle from aFrom, so the halfway point is half of however far that is.
134 double sweep = aToParameter - aFromParameter;
135
136 if( sweep <= 0.0 )
137 sweep += 1.0;
138
139 double middle = ( aFromParameter + sweep / 2.0 ) * 2.0 * std::numbers::pi;
140
141 span.m_Mid = VECTOR2I( m_Circle.Center.x + KiROUND( m_Circle.Radius * std::cos( middle ) ),
142 m_Circle.Center.y + KiROUND( m_Circle.Radius * std::sin( middle ) ) );
143 return span;
144 }
145};
146
147
149static GRAPHIC_EDIT_REFUSAL buildCarrier( const PCB_SHAPE& aSource, const VECTOR2I& aPointer,
150 TRIM_CARRIER& aCarrier )
151{
152 switch( aSource.GetShape() )
153 {
154 case SHAPE_T::SEGMENT:
155 aCarrier.m_Kind = SHAPE_T::SEGMENT;
156 aCarrier.m_Seg = SEG( aSource.GetStart(), aSource.GetEnd() );
157
158 if( aCarrier.m_Seg.A == aCarrier.m_Seg.B )
160
161 aCarrier.m_Geometry = aCarrier.m_Seg;
162 break;
163
165 {
166 std::vector<VECTOR2I> corners = aSource.GetRectCorners();
167
168 if( corners.size() != 4 )
170
171 // GetRectCorners() answers for a flat rectangle too, with two of its sides zero length
172 // and the other two the same run in opposite directions. There is no area to cut.
173 if( aSource.GetStart().x == aSource.GetEnd().x || aSource.GetStart().y == aSource.GetEnd().y )
175
176 std::vector<SEG> sides;
177
178 for( size_t i = 0; i < corners.size(); i++ )
179 sides.emplace_back( corners[i], corners[( i + 1 ) % corners.size()] );
180
181 auto nearest = std::ranges::min_element( sides,
182 [&]( const SEG& aA, const SEG& aB )
183 {
184 return aA.SquaredDistance( aPointer )
185 < aB.SquaredDistance( aPointer );
186 } );
187
188 aCarrier.m_Kind = SHAPE_T::SEGMENT;
189 aCarrier.m_Seg = *nearest;
190
191 if( aCarrier.m_Seg.A == aCarrier.m_Seg.B )
193
194 for( const SEG& side : sides )
195 {
196 if( &side != &*nearest )
197 aCarrier.m_Untouched.push_back( side );
198 }
199
200 aCarrier.m_Geometry = aCarrier.m_Seg;
201 break;
202 }
203
204 case SHAPE_T::ARC:
205 aCarrier.m_Kind = SHAPE_T::ARC;
206 aCarrier.m_Arc = GraphicEditArc( aSource );
207
208 if( !IsGraphicEditArcUsable( aCarrier.m_Arc ) )
210
211 aCarrier.m_Geometry = aCarrier.m_Arc;
212 break;
213
214 case SHAPE_T::CIRCLE:
215 aCarrier.m_Kind = SHAPE_T::CIRCLE;
216 aCarrier.m_Circle = CIRCLE( aSource.GetCenter(), aSource.GetRadius() );
217
218 if( aCarrier.m_Circle.Radius <= 0 || aCarrier.m_Circle.Radius > MAX_GRAPHIC_EDIT_ARC_RADIUS )
220
221 aCarrier.m_Geometry = aCarrier.m_Circle;
222 break;
223
224 default:
226 }
227
229}
230
231
233 const std::vector<const BOARD_ITEM*>& aBoundaries )
234{
236 const PCB_SHAPE* source = GraphicEditSource( aSource, IsGraphicTrimSource, result );
237
238 if( !source )
239 return result;
240
241 TRIM_CARRIER carrier;
242 const GRAPHIC_EDIT_REFUSAL refusal = buildCarrier( *source, aPointer, carrier );
243
244 if( refusal != GRAPHIC_EDIT_REFUSAL::NONE )
245 {
246 result.m_Refusal = refusal;
247 return result;
248 }
249
250 std::vector<TRIM_CUT> cuts;
251 bool touched = false;
252 bool overlapped = false;
253
254 for( const BOARD_ITEM* item : aBoundaries )
255 {
256 const PCB_SHAPE* boundary = GraphicEditBoundary( item, *source );
257
258 if( !boundary )
259 continue;
260
261 std::optional<INTERSECTABLE_GEOM> boundaryGeometry = BoardItemIntersectable( *boundary );
262
263 if( !boundaryGeometry )
264 continue;
265
266 std::vector<VECTOR2I> intersections;
267 INTERSECTION_CONTACT contact;
268
269 std::visit( INTERSECTION_VISITOR( *boundaryGeometry, intersections, contact ), carrier.m_Geometry );
270
271 // A shared run has no one point to cut at. A graze has exactly one, so it counts.
272 if( contact.m_Overlapping )
273 {
274 overlapped = true;
275 continue;
276 }
277
278 for( const VECTOR2I& point : intersections )
279 {
280 touched = true;
281
282 double parameter = carrier.Parameter( point );
283
284 // An end is already a partition point. It still says the shape is bounded here.
285 if( !carrier.IsClosed() && ( parameter <= PARAM_EPSILON || parameter >= 1.0 - PARAM_EPSILON ) )
286 continue;
287
288 auto duplicate = std::find_if( cuts.begin(), cuts.end(),
289 [&]( const TRIM_CUT& aCut )
290 {
291 return GraphicEditCoincident( aCut.m_Point, point );
292 } );
293
294 if( duplicate == cuts.end() )
295 cuts.push_back( { parameter, point, item } );
296 }
297 }
298
299 if( !touched )
300 {
301 // An overlap is the only thing that met the source. There is no single place to cut.
303 return result;
304 }
305
306 // A closed carrier has no ends of its own to fall back on. It needs a cut either side.
307 if( carrier.IsClosed() && cuts.size() < 2 )
308 {
310 return result;
311 }
312
313 std::ranges::sort( cuts, {}, &TRIM_CUT::m_Parameter );
314
315 const double pointerParameter = carrier.PointerParameter( aPointer );
316
317 for( const TRIM_CUT& cut : cuts )
318 {
319 if( std::abs( cut.m_Parameter - pointerParameter ) <= PARAM_EPSILON )
320 {
322 return result;
323 }
324 }
325
326 const TRIM_CUT* before = nullptr;
327 const TRIM_CUT* after = nullptr;
328
329 if( carrier.IsClosed() )
330 {
331 // Cuts wrap, so the pointer always falls between two of them. Past the last one it is
332 // back before the first.
333 auto next = std::ranges::find_if( cuts,
334 [&]( const TRIM_CUT& aCut )
335 {
336 return aCut.m_Parameter > pointerParameter;
337 } );
338
339 after = next == cuts.end() ? &cuts.front() : &*next;
340 before = next == cuts.begin() || next == cuts.end() ? &cuts.back() : &*( next - 1 );
341 }
342 else
343 {
344 for( const TRIM_CUT& cut : cuts )
345 {
346 if( cut.m_Parameter < pointerParameter )
347 before = &cut;
348 else if( !after )
349 after = &cut;
350 }
351 }
352
353 // The span the pointer is in. An open carrier's own ends close it off.
354 const VECTOR2I removedFrom = before ? before->m_Point : carrier.Start();
355 const VECTOR2I removedTo = after ? after->m_Point : carrier.End();
356 const double removedFromParameter = before ? before->m_Parameter : 0.0;
357 const double removedToParameter = after ? after->m_Parameter : 1.0;
358
359 result.m_Preview.push_back(
360 carrier.Span( removedFrom, removedTo, removedFromParameter, removedToParameter ) );
361
362 if( carrier.IsClosed() )
363 {
364 // Everything the other way round survives, as one arc.
365 result.m_Geometry.push_back(
366 carrier.Span( removedTo, removedFrom, removedToParameter, removedFromParameter ) );
367 }
368 else
369 {
370 if( before )
371 result.m_Geometry.push_back( carrier.Span( carrier.Start(), before->m_Point, 0.0, before->m_Parameter ) );
372
373 if( after )
374 result.m_Geometry.push_back( carrier.Span( after->m_Point, carrier.End(), after->m_Parameter, 1.0 ) );
375 }
376
377 // A rectangle keeps its other three sides whatever happens to the one under the pointer.
378 for( const SEG& side : carrier.m_Untouched )
379 {
381
383 kept.m_Start = side.A;
384 kept.m_End = side.B;
385 result.m_Geometry.push_back( kept );
386 }
387
388 if( before )
389 result.m_Boundaries.push_back( before->m_Boundary );
390
391 if( after && ( !before || after->m_Boundary != before->m_Boundary ) )
392 result.m_Boundaries.push_back( after->m_Boundary );
393
395 return result;
396}
std::optional< INTERSECTABLE_GEOM > BoardItemIntersectable(const BOARD_ITEM &aItem)
The kimath primitive a board item is made of.
constexpr BOX2I KiROUND(const BOX2D &aBoxD)
Definition box2.h:995
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:84
Represent basic circle geometry with utility geometry functions.
Definition circle.h:33
int Radius
Public to make access simpler.
Definition circle.h:149
double AsDegrees() const
Definition eda_angle.h:116
int GetRadius() const
SHAPE_T GetShape() const
Definition eda_shape.h:175
const VECTOR2I & GetEnd() const
Return the ending point of the graphic.
Definition eda_shape.h:325
const VECTOR2I & GetStart() const
Return the starting point of the graphic.
Definition eda_shape.h:275
std::vector< VECTOR2I > GetRectCorners() const
VECTOR2I GetCenter() const override
This defaults to the center of the bounding box if not overridden.
Definition pcb_shape.h:78
Definition seg.h:38
VECTOR2I A
Definition seg.h:45
ecoord SquaredDistance(const SEG &aSeg) const
Definition seg.cpp:76
VECTOR2I B
Definition seg.h:46
EDA_ANGLE GetCentralAngle() const
Get the "central angle" of the arc - this is the angle at the point of the "pie slice".
const VECTOR2I & GetArcMid() const
Definition shape_arc.h:116
bool IsClockwise() const
Definition shape_arc.h:319
SHAPE_ARC & ConstructFromStartEndCenter(const VECTOR2I &aStart, const VECTOR2I &aEnd, const VECTOR2I &aCenter, bool aClockwise=false, double aWidth=0)
Constructs this arc from the given start, end and center.
const VECTOR2I & GetP1() const
Definition shape_arc.h:115
const VECTOR2I & GetP0() const
Definition shape_arc.h:114
const VECTOR2I & GetCenter() const
SHAPE_T
Definition eda_shape.h:54
@ SEGMENT
Definition eda_shape.h:56
@ RECTANGLE
Use RECTANGLE instead of RECT to avoid collision in a Windows header.
Definition eda_shape.h:57
SHAPE_ARC GraphicEditArc(const PCB_SHAPE &aShape)
bool IsGraphicEditArcUsable(const SHAPE_ARC &aArc)
False for radii and sweeps the planners refuse. Check before planning an arc.
bool IsGraphicTrimSource(const PCB_SHAPE &aShape)
Trim also takes the closed shapes, which it opens up.
const PCB_SHAPE * GraphicEditBoundary(const BOARD_ITEM *aBoundary, const PCB_SHAPE &aSource)
A boundary usable against aSource. Null if it is the source, off-layer or unusable.
const PCB_SHAPE * GraphicEditSource(const BOARD_ITEM &aSource, bool(*aAccepts)(const PCB_SHAPE &), GRAPHIC_EDIT_RESULT &aResult)
The shape to edit, or null with the reason in aResult. aAccepts decides the kinds.
bool GraphicEditCoincident(const VECTOR2I &aA, const VECTOR2I &aB)
Points built by different routes land a rounding step apart.
GRAPHIC_EDIT_REFUSAL
Why a planner refused. Each one gets its own message.
constexpr int MAX_GRAPHIC_EDIT_ARC_RADIUS
Larger arcs overflow the boundary search box.
static GRAPHIC_EDIT_REFUSAL buildCarrier(const PCB_SHAPE &aSource, const VECTOR2I &aPointer, TRIM_CARRIER &aCarrier)
Fills aCarrier from aSource, or returns the reason it cannot.
static double arcParameter(const SHAPE_ARC &aSource, const VECTOR2I &aPoint)
static constexpr double PARAM_EPSILON
A fraction of the carrier's own length. Same meaning at every scale.
std::variant< LINE, HALF_LINE, SEG, CIRCLE, SHAPE_ARC, SHAPE_ELLIPSE, BOX2I > INTERSECTABLE_GEOM
A variant type that can hold any of the supported geometry types for intersection calculations.
GRAPHIC_EDIT_RESULT Plan(const BOARD_ITEM &aSource, const VECTOR2I &aPointer, const std::vector< const BOARD_ITEM * > &aBoundaries)
Plan removal of the part of aSource under aPointer.
double ParameterAlong(const SEG &aSeg, const VECTOR2I &aPoint)
Position of a point along a segment.
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:411
CITER next(CITER it)
Definition ptree.cpp:120
Utility functions for working with shapes.
SHAPE_T m_Shape
A result need not be the same kind as the source.
VECTOR2I m_Mid
Arcs only. Everything else leaves it default.
How two geometries meet, beyond where they cross.
bool m_Overlapping
Collinear or concentric, more than one point shared.
A visitor that visits INTERSECTABLE_GEOM variant objects with another (which is held as state: m_othe...
The one curve the pointer is trimming.
double Parameter(const VECTOR2I &aPoint) const
SHAPE_ARC m_Arc
VECTOR2I Start() const
double PointerParameter(const VECTOR2I &aPointer) const
Where the pointer sits, brought onto the carrier.
bool IsClosed() const
std::vector< SEG > m_Untouched
VECTOR2I End() const
GRAPHIC_EDIT_GEOMETRY Span(const VECTOR2I &aFrom, const VECTOR2I &aTo, double aFromParameter, double aToParameter) const
The piece of the carrier running forward from aFrom to aTo.
INTERSECTABLE_GEOM m_Geometry
VECTOR2I m_Point
const BOARD_ITEM * m_Boundary
double m_Parameter
wxString result
Test unit parsing edge cases and error handling.
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683