Skip to content

Commit 2209584

Browse files
committed
add maximumPermimeterSquare and rectPack()
1 parent db75c98 commit 2209584

File tree

8 files changed

+180
-2
lines changed

8 files changed

+180
-2
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
* `weightedMedian()` to `PGS_PointSet`. Finds the geometric median point of a set of weighted sample points.
1515
* `median()` to `PGS_ShapePredicates`. Computes the geometric median location of a shape's vertices.
1616
* `createRandomSFCurve()` to `PGS_Construction`. Creates a random space-filling curve.
17+
* `maximumPerimeterSquare()` to `PGS_Optimisation`. Finds the largest square whose 4 vertices each lie on the perimeter of a shape.
18+
* `rectPack()` to `PGS_Optimisation`. Packs a collection of rectangles into rectangular 2D bin(s).
1719

1820
### Changed
1921
* Reimplemented `PGS_Processing.equalParition()`. New algorithm is ~2x faster. Also removed `precise` parameter from method signature (no longer necessary).

README.md

+6-2
Original file line numberDiff line numberDiff line change
@@ -512,13 +512,14 @@ Much of the functionality (but by no means all) is demonstrated below:
512512
<tr>
513513
<td align="center" valign="center"><b>Maximum Inscribed Circle</td>
514514
<td align="center" valign="center"><b>Minimum Bounding Rectangle</td>
515-
<td align="center" valign="center" colspan="2"><b>Maximum Inscribed Rectangle</td>
515+
<td align="center" valign="center"><b>Maximum Inscribed Rectangle</td>
516+
<td align="center" valign="center"><b>Maximum Perimeter Square</td>
516517
</tr>
517518
<tr>
518519
<td valign="top"><img src="resources/optimisation/inscribedCircle.gif"></td>
519520
<td valign="top"><img src="resources/optimisation/minimumBoundingRectangle.png"></td>
520521
<td valign="top"><img src="resources/optimisation/mir.png"></td>
521-
<td valign="top" ><img src="resources/optimisation/mir2.png"></td>
522+
<td valign="top" ><img src="resources/optimisation/mis.png"></td>
522523
</tr>
523524

524525
<tr>
@@ -558,9 +559,12 @@ Much of the functionality (but by no means all) is demonstrated below:
558559
</tr>
559560

560561
<tr>
562+
<td align="center" valign="center" colspan="2"><b>Rectangle Bin Pack</td>
561563
<td align="center" valign="center"><b>Hilbert Sort Faces</td>
562564
</tr>
563565
<tr>
566+
<td valign="top" width="25%"><img src="resources/optimisation/rectPack.gif"></td>
567+
<td valign="top" width="25%"><img src="resources/optimisation/rectPackBins.gif"></td>
564568
<td valign="top" width="25%"><img src="resources/optimisation/hilbertSortFaces.gif"></td>
565569
</tr>
566570

pom.xml

+5
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,11 @@
321321
<artifactId>dsiutils</artifactId>
322322
<version>2.7.2</version>
323323
</dependency>
324+
<dependency>
325+
<groupId>com.github.micycle1</groupId>
326+
<artifactId>RectPacking</artifactId>
327+
<version>9178e79df3</version>
328+
</dependency>
324329
</dependencies>
325330

326331
</project>

resources/optimisation/mis.png

47 KB
Loading

resources/optimisation/rectPack.gif

98.5 KB
Loading
75.5 KB
Loading

src/main/java/micycle/pgs/PGS.java

+26
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import micycle.pgs.commons.FastPolygonizer;
3333
import micycle.pgs.commons.Nullable;
3434
import micycle.pgs.commons.PEdge;
35+
import processing.core.PConstants;
3536
import processing.core.PShape;
3637
import processing.core.PVector;
3738

@@ -98,6 +99,12 @@ static final double distance(Point a, Point b) {
9899
double deltaY = a.getX() - b.getX();
99100
return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
100101
}
102+
103+
static final double distanceSq(PVector a, PVector b) {
104+
float dx = a.x - b.x;
105+
float dy = a.y - b.y;
106+
return (dx * dx + dy * dy);
107+
}
101108

102109
static final LineString createLineString(PVector a, PVector b) {
103110
return GEOM_FACTORY.createLineString(new Coordinate[] { coordFromPVector(a), coordFromPVector(b) });
@@ -110,6 +117,25 @@ static final SegmentString createSegmentString(PVector a, PVector b) {
110117
static final Point createPoint(double x, double y) {
111118
return GEOM_FACTORY.createPoint(new Coordinate(x, y));
112119
}
120+
121+
/**
122+
* Creates a stroked rectangle.
123+
*/
124+
static final PShape createRect(double x, double y, double w, double h) {
125+
final PShape rect = new PShape(PShape.PATH);
126+
rect.setFill(true);
127+
rect.setFill(255);
128+
rect.setStroke(true);
129+
rect.setStrokeWeight(4);
130+
rect.setStroke(RGB.PINK);
131+
rect.beginShape();
132+
rect.vertex((float) x, (float) y);
133+
rect.vertex((float) (x + w), (float) y);
134+
rect.vertex((float) (x + w), (float) (y + h));
135+
rect.vertex((float) x, (float) (y + h));
136+
rect.endShape(PConstants.CLOSE);
137+
return rect;
138+
}
113139

114140
static final Point pointFromPVector(PVector p) {
115141
return GEOM_FACTORY.createPoint(new Coordinate(p.x, p.y));

src/main/java/micycle/pgs/PGS_Optimisation.java

+141
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static micycle.pgs.PGS_Conversion.fromPShape;
44
import static micycle.pgs.PGS_Conversion.toPShape;
5+
import static processing.core.PConstants.GROUP;
56
import static micycle.pgs.PGS_Construction.createEllipse;
67

78
import java.util.ArrayList;
@@ -15,14 +16,21 @@
1516
import org.locationtech.jts.algorithm.MinimumDiameter;
1617
import org.locationtech.jts.algorithm.construct.LargestEmptyCircle;
1718
import org.locationtech.jts.algorithm.construct.MaximumInscribedCircle;
19+
import org.locationtech.jts.algorithm.locate.IndexedPointInAreaLocator;
1820
import org.locationtech.jts.geom.Coordinate;
21+
import org.locationtech.jts.geom.Envelope;
1922
import org.locationtech.jts.geom.Geometry;
2023
import org.locationtech.jts.geom.LineString;
24+
import org.locationtech.jts.geom.Location;
2125
import org.locationtech.jts.geom.Point;
2226
import org.locationtech.jts.geom.Polygon;
2327
import org.locationtech.jts.operation.distance.DistanceOp;
28+
import org.locationtech.jts.simplify.DouglasPeuckerSimplifier;
2429
import org.locationtech.jts.util.GeometricShapeFactory;
2530

31+
import almadina.rectpacking.RBPSolution;
32+
import almadina.rectpacking.Rect;
33+
import almadina.rectpacking.RectPacking.PackingHeuristic;
2634
import micycle.pgs.color.RGB;
2735
import micycle.pgs.commons.ClosestPointPair;
2836
import micycle.pgs.commons.FarthestPointPair;
@@ -136,6 +144,71 @@ public static PShape maximumInscribedAARectangle(PShape shape, boolean fast) {
136144
return toPShape(shapeFactory.createRectangle());
137145
}
138146

147+
/**
148+
* Finds the largest area <i>perimeter square</i> of the input. A <i>perimeter
149+
* square</i> is a square whose 4 vertices each lie on the perimeter of the
150+
* input shape (within the given tolerance).
151+
* <p>
152+
* If the input is convex, the output forms a fully inscribed square; if the
153+
* input is concave the output is not necessarily inscribed.
154+
* <p>
155+
* Does not respect holes (for now...).
156+
*
157+
* @param shape
158+
* @param tolerance a value of 2-5 is usually suitable
159+
* @return shape representing the maximum square
160+
* @since 1.3.1
161+
*/
162+
public static PShape maximumPerimeterSquare(PShape shape, double tolerance) {
163+
shape = PGS_Morphology.simplify(shape, tolerance / 2);
164+
final Polygon p = (Polygon) PGS_Conversion.fromPShape(shape);
165+
Geometry buffer = p.getExteriorRing().buffer(tolerance / 2, 4);
166+
final Envelope e = buffer.getEnvelopeInternal();
167+
buffer = DouglasPeuckerSimplifier.simplify(buffer, tolerance / 2);
168+
final IndexedPointInAreaLocator pia = new IndexedPointInAreaLocator(buffer);
169+
shape = PGS_Processing.densify(shape, Math.max(0.5, tolerance)); // min of 0.5
170+
final List<PVector> points = PGS_Conversion.toPVector(shape);
171+
172+
double maxDiagonal = 0;
173+
PVector[] maxAreaVertices = new PVector[0];
174+
for (int i = 0; i < points.size(); i++) {
175+
for (int j = 0; j < points.size(); j++) {
176+
final PVector a = points.get(i);
177+
final PVector b = points.get(j);
178+
double dist = PGS.distanceSq(a, b);
179+
180+
if (dist < maxDiagonal) {
181+
continue;
182+
}
183+
184+
final PVector m = PVector.add(a, b).div(2);
185+
final PVector n = new PVector(b.y - a.y, a.x - b.x).div(2);
186+
final PVector c = PVector.sub(m, n);
187+
188+
final PVector d = PVector.add(m, n);
189+
// do envelope checks first -- slightly faster
190+
if (within(c, e) && within(d, e)) {
191+
if (pia.locate(new Coordinate(c.x, c.y)) != Location.EXTERIOR) {
192+
if (pia.locate(new Coordinate(d.x, d.y)) != Location.EXTERIOR) {
193+
maxDiagonal = dist;
194+
maxAreaVertices = new PVector[] { a, c, b, d, a }; // closed vertices
195+
}
196+
}
197+
}
198+
}
199+
}
200+
201+
PShape out = PGS_Conversion.fromPVector(maxAreaVertices);
202+
out.setStroke(true);
203+
out.setStroke(micycle.pgs.color.RGB.PINK);
204+
out.setStrokeWeight(4);
205+
return out;
206+
}
207+
208+
private static boolean within(PVector p, Envelope rect) {
209+
return p.x >= rect.getMinX() && p.x <= rect.getMaxX() && p.y <= rect.getMaxY() && p.y >= rect.getMinY();
210+
}
211+
139212
/**
140213
* Computes the Minimum Bounding Circle (MBC) for the points in a Geometry. The
141214
* MBC is the smallest circle which covers all the vertices of the input shape
@@ -245,6 +318,74 @@ public static PShape largestEmptyCircle(PShape obstacles, double tolerance) {
245318
Polygon circle = createEllipse(PGS.coordFromPoint(lec.getCenter()), wh, wh);
246319
return toPShape(circle);
247320
}
321+
322+
public enum RectPackHeuristic {
323+
324+
/**
325+
* Packs rectangles such that the wasted/empty area within the bin is minimised.
326+
* This heuristic tends to generate packings that are less dense but are better
327+
* at covering the whole area (particularly if there is much spare area) than
328+
* the other heuristics.
329+
*/
330+
BestAreaFit(PackingHeuristic.BestAreaFit),
331+
/**
332+
* Packs rectangles such that the total touching perimeter length is maximised.
333+
* In practice, rectangles are packed against the left-most and upper-most
334+
* boundary of the bin first.
335+
*/
336+
TouchingPerimeter(PackingHeuristic.TouchingPerimeter),
337+
/**
338+
* Packs rectangles such that the distance between the top-right corner of each
339+
* rectangle and that of the bin is maximised.
340+
*/
341+
TopRightCornerDistance(PackingHeuristic.TopRightCornerDistance);
342+
343+
private final PackingHeuristic h;
344+
345+
private RectPackHeuristic(PackingHeuristic h) {
346+
this.h = h;
347+
}
348+
}
349+
350+
/**
351+
* Packs a collection of rectangles, according to the given packing heuristic,
352+
* into rectangular 2D bin(s). Within each bin rectangles are packed flush with
353+
* each other, having no overlap. Each rectangle is packed parallel to the edges
354+
* of the plane.
355+
* <p>
356+
* When packed rectangles fill one bin, any remaining rectangles will be packed
357+
* into additional bin(s).
358+
*
359+
* @param rectangles a collection of rectangles (represented by PVectors),
360+
* specifying their width (.x) and height (.y)
361+
* @param binWidth the width of each bin's area in which to pack the
362+
* rectangles
363+
* @param binHeight the height of each bin's area in which to pack the
364+
* rectangles
365+
* @param heuristic the packing heuristic to use. The heuristic determines
366+
* rules for how every subsequent rectangle is placed
367+
* @since 1.3.1
368+
* @return a GROUP PShape, where each immediate child is a GROUP shape
369+
* corresponding to a bin; the child shapes of each bin are rectangles.
370+
* Bins are positioned at (0, 0).
371+
*/
372+
public static PShape rectPack(List<PVector> rectangles, int binWidth, int binHeight, RectPackHeuristic heuristic) {
373+
RBPSolution packer = new RBPSolution(binWidth, binHeight);
374+
List<Rect> rects = rectangles.stream().map(p -> Rect.of((int) Math.round(p.x), (int) Math.round(p.y)))
375+
.collect(Collectors.toList());
376+
377+
packer.pack(rects, heuristic.h);
378+
379+
PShape bins = new PShape(GROUP);
380+
packer.getBins().forEach(bin -> {
381+
PShape binGroup = new PShape(GROUP);
382+
bin.getPackedRects().forEach(r -> {
383+
binGroup.addChild(PGS.createRect(r.x, r.y, r.width, r.height));
384+
});
385+
bins.addChild(binGroup);
386+
});
387+
return bins;
388+
}
248389

249390
/**
250391
* Returns the nearest point of the shape to the given point. If the shape is

0 commit comments

Comments
 (0)