Skip to content

Commit

Permalink
Fixed problem with buffered shapes will generate single isolated line…
Browse files Browse the repository at this point in the history
…s but there is no retract (#2378)
  • Loading branch information
breiler committed Dec 6, 2023
1 parent 9c7eb3e commit b4c4efa
Show file tree
Hide file tree
Showing 5 changed files with 888 additions and 112 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,20 @@ This file is part of Universal Gcode Sender (UGS).

import com.willwinder.ugs.nbp.designer.entities.cuttable.Cuttable;
import com.willwinder.ugs.nbp.designer.io.gcode.path.GcodePath;
import static com.willwinder.ugs.nbp.designer.io.gcode.toolpaths.ToolPathUtils.addGeometriesToCoordinatesList;
import static com.willwinder.ugs.nbp.designer.io.gcode.toolpaths.ToolPathUtils.bufferAndCollectGeometries;
import static com.willwinder.ugs.nbp.designer.io.gcode.toolpaths.ToolPathUtils.convertAreaToGeometry;
import com.willwinder.universalgcodesender.model.PartialPosition;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.LinearRing;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.simplify.DouglasPeuckerSimplifier;

import java.awt.geom.Area;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import static com.willwinder.ugs.nbp.designer.io.gcode.toolpaths.ToolPathUtils.convertAreaToGeometry;
import static com.willwinder.ugs.nbp.designer.io.gcode.toolpaths.ToolPathUtils.geometryToCoordinates;
import static com.willwinder.ugs.nbp.designer.io.gcode.toolpaths.ToolPathUtils.toPartialPosition;

/**
* @author Joacim Breiler
*/
public class PocketToolPath extends AbstractToolPath {
public static final double DISTANCE_TOLERANCE = 0.1;
private final Cuttable source;

/**
Expand All @@ -58,7 +50,7 @@ public PocketToolPath(Cuttable source) {
public GcodePath toGcodePath() {
Geometry geometryCollection = convertAreaToGeometry(new Area(source.getShape()), getGeometryFactory());
Geometry shell = geometryCollection.buffer(-getToolDiameter() / 2d);
List<Geometry> geometries = bufferAndCollectGeometries(geometryCollection);
List<Geometry> geometries = bufferAndCollectGeometries(geometryCollection, getToolDiameter(), stepOver);

List<List<PartialPosition>> coordinateList = new ArrayList<>();
double currentDepth = getStartDepth() - getDepthPerPass();
Expand All @@ -74,89 +66,6 @@ public GcodePath toGcodePath() {
return toGcodePath(coordinateList);
}

private void addGeometriesToCoordinatesList(Geometry shell, List<Geometry> geometries, List<List<PartialPosition>> coordinateList, double currentDepth) {
Geometry previousGeometry = null;
List<PartialPosition> geometryLine = new ArrayList<>();
for (int x = 0; x < geometries.size(); x++) {
Geometry geometry = geometries.get(x);

if (x > 0) {
PartialPosition fromPosition = ToolPathUtils.toPartialPosition(previousGeometry.getCoordinates()[0], currentDepth);
int newStartIndex = ToolPathUtils.findNearestCoordinateIndex(geometry.getCoordinates(), new Coordinate(fromPosition.getX(), fromPosition.getY(), fromPosition.getZ()));

if (geometry instanceof LinearRing) {
geometry = rotateCoordinates((LinearRing) geometry, newStartIndex);
}

Coordinate firstCoordinate = geometry.getCoordinates()[0];
PartialPosition nextPosition = toPartialPosition(firstCoordinate, currentDepth);

LineString lineString = ToolPathUtils.createLineString(fromPosition, nextPosition);
if (shell.crosses(lineString)) {
coordinateList.add(geometryLine);
geometryLine = new ArrayList<>();
}
}

geometryLine.addAll(geometryToCoordinates(geometry, currentDepth));
previousGeometry = geometry;
}

if (!geometryLine.isEmpty()) {
coordinateList.add(geometryLine);
}
}

private LinearRing rotateCoordinates(LinearRing nextGeometry, int newStartIndex) {
Coordinate[] geomCoordinates = nextGeometry.getCoordinates();
Coordinate[] newCoordinates = new Coordinate[geomCoordinates.length];
int newIndex = 0;
for (int coordIndex = newStartIndex; coordIndex < newCoordinates.length; coordIndex++) {
newCoordinates[newIndex] = geomCoordinates[coordIndex];
newIndex++;
}

for (int coordIndex = 1; coordIndex < newStartIndex; coordIndex++) {
newCoordinates[newIndex] = geomCoordinates[coordIndex];
newIndex++;
}

newCoordinates[newCoordinates.length - 1] = geomCoordinates[newStartIndex];
nextGeometry = ToolPathUtils.createLinearRing(newCoordinates);
return nextGeometry;
}

private List<Geometry> bufferAndCollectGeometries(Geometry geometry) {
double buffer = getToolDiameter() / 2d;
List<Geometry> geometries = bufferAndCollectGeometries(geometry, buffer);
geometries.sort(new GeometrySizeComparator());
return geometries;
}

private List<Geometry> bufferAndCollectGeometries(Geometry geometry, double buffer) {
Geometry bufferedGeometry = geometry.buffer(-buffer);
if (bufferedGeometry.getNumGeometries() <= 0 || bufferedGeometry.isEmpty()) {
return Collections.emptyList();
}

List<Geometry> result = new ArrayList<>();
for (int i = 0; i < bufferedGeometry.getNumGeometries(); i++) {
Geometry geom = bufferedGeometry.getGeometryN(i);
result.addAll(bufferAndCollectGeometries(geom, getToolDiameter() * stepOver));

if (geom instanceof Polygon) {
Polygon polygon = (Polygon) geom;
result.add(DouglasPeuckerSimplifier.simplify(polygon.getExteriorRing(), DISTANCE_TOLERANCE));
for (int j = 0; j < polygon.getNumInteriorRing(); j++) {
result.add(DouglasPeuckerSimplifier.simplify(polygon.getInteriorRingN(j), DISTANCE_TOLERANCE));
}
} else {
result.add(DouglasPeuckerSimplifier.simplify(geom, DISTANCE_TOLERANCE));
}
}

return result;
}

public void setStepOver(double stepOver) {
this.stepOver = Math.min(Math.max(0.01, Math.abs(stepOver)), 1.0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,19 @@ This file is part of Universal Gcode Sender (UGS).
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.geom.PrecisionModel;
import org.locationtech.jts.operation.polygonize.Polygonizer;
import org.locationtech.jts.simplify.DouglasPeuckerSimplifier;

import java.awt.*;
import java.awt.Shape;
import java.awt.geom.Area;
import java.awt.geom.PathIterator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

public class ToolPathUtils {
public static final double DISTANCE_TOLERANCE = 0.1;

public static final double FLATNESS_PRECISION = 0.1d;

Expand All @@ -68,10 +70,10 @@ public static void recursivlyCollectGeometries(Geometry geometry, List<Geometry>
for (int i = 0; i < geometry.getNumGeometries(); i++) {
recursivlyCollectGeometries(geometry.getGeometryN(i), result);
}
} else if (geometry instanceof Polygon) {
result.add(((Polygon) geometry).getExteriorRing());
for (int i = 0; i < ((Polygon) geometry).getNumInteriorRing(); i++) {
result.add(((Polygon) geometry).getInteriorRingN(i));
} else if (geometry instanceof Polygon polygon) {
result.add((polygon).getExteriorRing());
for (int i = 0; i < (polygon).getNumInteriorRing(); i++) {
result.add((polygon).getInteriorRingN(i));
}
} else {
result.add(geometry);
Expand All @@ -82,7 +84,7 @@ public static List<PartialPosition> geometryToCoordinates(Geometry geometry) {
Coordinate[] coordinates = geometry.getCoordinates();
return Arrays.stream(coordinates)
.map(c -> new PartialPosition(c.getX(), c.getY(), c.getZ(), UnitUtils.Units.MM))
.collect(Collectors.toList());
.toList();
}

public static List<List<PartialPosition>> geometriesToCoordinates(List<Geometry> geometries, double depth) {
Expand All @@ -91,7 +93,7 @@ public static List<List<PartialPosition>> geometriesToCoordinates(List<Geometry>
List<PartialPosition> bufferedCoordinates = geometryToCoordinates(geometry)
.stream()
.map(c -> new PartialPosition(c.getX(), c.getY(), -depth, UnitUtils.Units.MM))
.collect(Collectors.toList());
.toList();

if (bufferedCoordinates.isEmpty() || bufferedCoordinates.size() <= 1) {
return null;
Expand All @@ -100,14 +102,14 @@ public static List<List<PartialPosition>> geometriesToCoordinates(List<Geometry>
return bufferedCoordinates;
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
.toList();
}

public static List<PartialPosition> geometryToCoordinates(Geometry geometry, double depth) {
org.locationtech.jts.geom.Coordinate[] coordinates = geometry.getCoordinates();
return Arrays.stream(coordinates)
.map(c -> toPartialPosition(c, depth))
.collect(Collectors.toList());
.toList();
}

public static Geometry convertAreaToGeometry(final Area area, final GeometryFactory factory) {
Expand Down Expand Up @@ -212,4 +214,91 @@ public static ToolPathStats getToolPathStats(GcodePath gcodePath) {

return new ToolPathStats(totalFeedLength, totalRapidLength);
}

public static List<Geometry> bufferAndCollectGeometries(Geometry geometry, double toolDiameter, double stepOver) {
double buffer = toolDiameter / 2d;
List<Geometry> geometries = ToolPathUtils.bufferAndCollectGeometries(geometry, buffer, toolDiameter, stepOver);
geometries.sort(new GeometrySizeComparator());
return geometries;
}

public static List<Geometry> bufferAndCollectGeometries(Geometry geometry, double buffer, double toolDiameter, double stepOver) {
Geometry bufferedGeometry = geometry.buffer(-buffer);
if (bufferedGeometry.getNumGeometries() <= 0 || bufferedGeometry.isEmpty()) {
return Collections.emptyList();
}

List<Geometry> result = new ArrayList<>();
for (int i = 0; i < bufferedGeometry.getNumGeometries(); i++) {
Geometry geom = bufferedGeometry.getGeometryN(i);
result.addAll(bufferAndCollectGeometries(geom, toolDiameter * stepOver, toolDiameter, stepOver));

if (geom instanceof Polygon polygon) {
result.add(DouglasPeuckerSimplifier.simplify(polygon.getExteriorRing(), DISTANCE_TOLERANCE));
for (int j = 0; j < polygon.getNumInteriorRing(); j++) {
result.add(DouglasPeuckerSimplifier.simplify(polygon.getInteriorRingN(j), DISTANCE_TOLERANCE));
}
} else {
result.add(DouglasPeuckerSimplifier.simplify(geom, DISTANCE_TOLERANCE));
}
}

return result;
}

public static LinearRing rotateCoordinates(LinearRing nextGeometry, int newStartIndex) {
Coordinate[] geomCoordinates = nextGeometry.getCoordinates();
Coordinate[] newCoordinates = new Coordinate[geomCoordinates.length];
int newIndex = 0;
for (int coordIndex = newStartIndex; coordIndex < newCoordinates.length; coordIndex++) {
newCoordinates[newIndex] = geomCoordinates[coordIndex];
newIndex++;
}

for (int coordIndex = 1; coordIndex < newStartIndex; coordIndex++) {
newCoordinates[newIndex] = geomCoordinates[coordIndex];
newIndex++;
}

newCoordinates[newCoordinates.length - 1] = geomCoordinates[newStartIndex];
nextGeometry = ToolPathUtils.createLinearRing(newCoordinates);
return nextGeometry;
}

public static void addGeometriesToCoordinatesList(Geometry shell, List<Geometry> geometries, List<List<PartialPosition>> coordinateList, double currentDepth) {
Geometry previousGeometry = null;
List<PartialPosition> geometryLine = new ArrayList<>();
for (int x = 0; x < geometries.size(); x++) {
Geometry geometry = geometries.get(x);

if (x > 0) {
PartialPosition fromPosition = ToolPathUtils.toPartialPosition(getLastPosition(previousGeometry), currentDepth);
int newStartIndex = ToolPathUtils.findNearestCoordinateIndex(geometry.getCoordinates(), new Coordinate(fromPosition.getX(), fromPosition.getY(), fromPosition.getZ()));

if (geometry instanceof LinearRing linearRing) {
geometry = rotateCoordinates(linearRing, newStartIndex);
}

Coordinate firstCoordinate = geometry.getCoordinates()[0];
PartialPosition nextPosition = toPartialPosition(firstCoordinate, currentDepth);

LineString lineString = ToolPathUtils.createLineString(fromPosition, nextPosition);
if (shell.crosses(lineString) || geometry.getClass().equals(LineString.class)) {
coordinateList.add(geometryLine);
geometryLine = new ArrayList<>();
}
}

geometryLine.addAll(geometryToCoordinates(geometry, currentDepth));
previousGeometry = geometry;
}

if (!geometryLine.isEmpty()) {
coordinateList.add(geometryLine);
}
}

private static Coordinate getLastPosition(Geometry geometry) {
return geometry.getCoordinates()[geometry.getCoordinates().length - 1];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,12 @@
import com.willwinder.ugs.nbp.designer.model.Size;
import com.willwinder.universalgcodesender.model.Axis;
import com.willwinder.universalgcodesender.model.PartialPosition;
import org.junit.Test;

import java.util.List;
import java.util.stream.Collectors;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;

import java.util.List;

public class PocketToolPathTest {

Expand Down Expand Up @@ -66,7 +64,7 @@ public void pocketShouldNotExceedTheGeometry() {

List<Segment> drillOperations = segmentList.stream()
.filter(segment -> segment.type == SegmentType.POINT)
.collect(Collectors.toList());
.toList();
assertEquals("There should be a number of drill operations when making a pocket", Math.abs((targetDepth - depthPerPass) / depthPerPass), drillOperations.size(), 0.1);

PartialPosition point = drillOperations.get(drillOperations.size() - 1).getPoint();
Expand Down Expand Up @@ -118,7 +116,7 @@ public void pocketOnRectangleWithHole() {

List<Segment> drillOperations = segmentList.stream()
.filter(segment -> segment.type == SegmentType.POINT)
.collect(Collectors.toList());
.toList();
assertEquals("There should be a number of drill operations when making a pocket", Math.abs((targetDepth - depthPerPass) / depthPerPass), drillOperations.size(), 0.1);

PartialPosition point = drillOperations.get(drillOperations.size() - 1).getPoint();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.willwinder.ugs.nbp.designer.io.gcode.toolpaths;

import static com.willwinder.ugs.nbp.designer.io.gcode.toolpaths.ToolPathUtils.addGeometriesToCoordinatesList;
import static com.willwinder.ugs.nbp.designer.io.gcode.toolpaths.ToolPathUtils.bufferAndCollectGeometries;
import static com.willwinder.ugs.nbp.designer.io.gcode.toolpaths.ToolPathUtils.convertAreaToGeometry;
import com.willwinder.ugs.nbp.designer.io.ugsd.UgsDesignReader;
import com.willwinder.ugs.nbp.designer.model.Design;
import com.willwinder.universalgcodesender.model.PartialPosition;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;

import java.awt.geom.Area;
import java.util.ArrayList;
import java.util.List;

public class ToolPathUtilsTest {

/**
* When using a tool diameter that is larger than parts of the shape the buffer may result in smaller lines.
* Make sure that the spindle is moved up between these line passes
*/
@Test
public void addGeometriesToCoordinatesList_shouldHandleLineStringAsSeparateGeometries() {
UgsDesignReader reader = new UgsDesignReader();
Design design = reader.read(PocketToolPathTest.class.getResourceAsStream("/x.ugsd")).orElseThrow(RuntimeException::new);

double toolDiameter = 1.2;

Geometry geometryCollection = convertAreaToGeometry(new Area(design.getEntities().get(0).getShape()), new GeometryFactory());
Geometry shell = geometryCollection.buffer(-toolDiameter / 2d);
List<Geometry> geometries = bufferAndCollectGeometries(geometryCollection, toolDiameter, 1);
assertEquals(4, geometries.size());

List<List<PartialPosition>> coordinateList = new ArrayList<>();
addGeometriesToCoordinatesList(shell, geometries, coordinateList, 0);
assertEquals(3, coordinateList.size());
}
}

0 comments on commit b4c4efa

Please sign in to comment.