Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/main/java/de/thomas_oster/liblasercut/LaserJob.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public double getStartY()
{
return startY;
}

/**
* Get the X-coordinate of the origin (top-left corner of laser bed) in mm.
* Initially 0, but changes if applyStartPoint() is used.
Expand Down Expand Up @@ -166,6 +166,9 @@ public void applyStartPoint()
c.setY((int) (c.getY() - Util.mm2inch(startY)*p.getDPI()));
}
}
// keep the cached bounding box (built incrementally as points were added) in sync,
// otherwise getMinX/Y()/getMaxX/Y() - and anything sent from them, e.g. Ruida's frame/outline - go stale
((VectorPart) p).shiftBounds(Util.mm2inch(startX) * p.getDPI(), Util.mm2inch(startY) * p.getDPI());
}
else if (p instanceof RasterPart)
{
Expand Down
16 changes: 16 additions & 0 deletions src/main/java/de/thomas_oster/liblasercut/VectorPart.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,22 @@ public VectorCommand[] getCommandList()
return commands.toArray(new VectorCommand[0]);
}

/**
* Shifts the cached bounding box by (dx,dy). Used by LaserJob.applyStartPoint(), which shifts
* every command's coordinates directly and must keep this cache (built incrementally as points
* are added, see checkMin()/checkMax()) in sync - otherwise getMinX/Y()/getMaxX/Y() go stale.
*/
void shiftBounds(double dx, double dy)
{
if (minX != Double.POSITIVE_INFINITY)
{
minX -= dx;
maxX -= dx;
minY -= dy;
maxY -= dy;
}
}

private void checkMin(double x, double y)
{
if (x < minX)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -529,9 +529,17 @@ protected void setFocus(PrintStream out, double focus) throws IOException {
}
}

// Pivot for mirroring a flipped axis: bedWidth/bedHeight when the job spans the whole bed (home-referenced,
// absolute execution) - but applyStartPoint() re-references coordinates to a local offset from the start
// point (0..a few mm, not 0..bedWidth), so mirroring around the far bed edge is meaningless there and pins
// the start point at the wrong end of travel. Pivot is 0 in that case, so the start point lands at machine
// home (0) instead. Set per job in sendJob().
protected transient double flipPivotX = 0;
protected transient double flipPivotY = 0;

protected void move(PrintStream out, double x, double y, double resolution) throws IOException {
x = isFlipXaxis() ? getBedWidth() - Util.px2mm(x, resolution) : Util.px2mm(x, resolution);
y = isFlipYaxis() ? getBedHeight() - Util.px2mm(y, resolution) : Util.px2mm(y, resolution);
x = isFlipXaxis() ? flipPivotX - Util.px2mm(x, resolution) : Util.px2mm(x, resolution);
y = isFlipYaxis() ? flipPivotY - Util.px2mm(y, resolution) : Util.px2mm(y, resolution);
currentSpeed = getTravel_speed();

if (blankLaserDuringRapids)
Expand All @@ -546,8 +554,8 @@ protected void move(PrintStream out, double x, double y, double resolution) thro
}

protected void line(PrintStream out, double x, double y, double resolution) throws IOException {
x = isFlipXaxis() ? getBedWidth() - Util.px2mm(x, resolution) : Util.px2mm(x, resolution);
y = isFlipYaxis() ? getBedHeight() - Util.px2mm(y, resolution) : Util.px2mm(y, resolution);
x = isFlipXaxis() ? flipPivotX - Util.px2mm(x, resolution) : Util.px2mm(x, resolution);
y = isFlipYaxis() ? flipPivotY - Util.px2mm(y, resolution) : Util.px2mm(y, resolution);
String append = "";

if (nextPower != currentPower)
Expand Down Expand Up @@ -982,6 +990,8 @@ public void sendJob(LaserJob job, ProgressListener pl, List<String> warnings) th
}

public void writeJobCode(LaserJob job, ProgressListener pl) throws IOException {
flipPivotX = job.getTransformedOriginX() == 0 ? getBedWidth() : 0;
flipPivotY = job.getTransformedOriginY() == 0 ? getBedHeight() : 0;
writeInitializationCode();
pl.progressChanged(this, 20);
int i = 0;
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/de/thomas_oster/liblasercut/drivers/Grbl.java
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,8 @@ protected String waitForIdentificationLine(ProgressListener pl) throws IOExcepti
*/
@Override
protected void move(PrintStream out, double x, double y, double resolution) throws IOException {
x = isFlipXaxis() ? getBedWidth() - Util.px2mm(x, resolution) : Util.px2mm(x, resolution);
y = isFlipYaxis() ? getBedHeight() - Util.px2mm(y, resolution) : Util.px2mm(y, resolution);
x = isFlipXaxis() ? flipPivotX - Util.px2mm(x, resolution) : Util.px2mm(x, resolution);
y = isFlipYaxis() ? flipPivotY - Util.px2mm(y, resolution) : Util.px2mm(y, resolution);
currentSpeed = getTravel_speed();
if (blankLaserDuringRapids)
{
Expand Down
40 changes: 30 additions & 10 deletions src/main/java/de/thomas_oster/liblasercut/drivers/Ruida.java
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,14 @@ public double getRasterPadding() {
return 0;
}

// Pivot for mirroring a flipped axis: bedWidth/bedHeight when the job spans the whole bed (home-referenced,
// absolute execution) - but applyStartPoint() re-references coordinates to a local offset from the start
// point (0..a few mm, not 0..bedWidth), so mirroring around the far bed edge is meaningless there and pins
// the start point at the wrong end of travel. Pivot is 0 in that case, so the start point lands at machine
// home (0) instead. Set per job in writeJobCode().
private transient double flipPivotX = 0;
private transient double flipPivotY = 0;

private void find_and_write_bounding_box(LaserJob job) throws IOException
{
double minX = 0.0;
Expand All @@ -223,10 +231,15 @@ private void find_and_write_bounding_box(LaserJob job) throws IOException
/* compute bounding box */
for (JobPart p : job.getParts())
{
double min_x = isFlipXaxis() ? getBedWidth() - Util.px2mm(p.getMinX(), p.getDPI()) : Util.px2mm(p.getMinX(), p.getDPI());
double min_y = isFlipYaxis() ? getBedHeight() - Util.px2mm(p.getMinY(), p.getDPI()) : Util.px2mm(p.getMinY(), p.getDPI());
double max_x = isFlipXaxis() ? getBedWidth() - Util.px2mm(p.getMaxX(), p.getDPI()) : Util.px2mm(p.getMaxX(), p.getDPI());
double max_y = isFlipYaxis() ? getBedHeight() - Util.px2mm(p.getMaxY(), p.getDPI()) : Util.px2mm(p.getMaxY(), p.getDPI());
// flipping reverses min/max order, so re-sort instead of flipping each bound independently
double x1 = isFlipXaxis() ? flipPivotX - Util.px2mm(p.getMinX(), p.getDPI()) : Util.px2mm(p.getMinX(), p.getDPI());
double x2 = isFlipXaxis() ? flipPivotX - Util.px2mm(p.getMaxX(), p.getDPI()) : Util.px2mm(p.getMaxX(), p.getDPI());
double y1 = isFlipYaxis() ? flipPivotY - Util.px2mm(p.getMinY(), p.getDPI()) : Util.px2mm(p.getMinY(), p.getDPI());
double y2 = isFlipYaxis() ? flipPivotY - Util.px2mm(p.getMaxY(), p.getDPI()) : Util.px2mm(p.getMaxY(), p.getDPI());
double min_x = Math.min(x1, x2);
double max_x = Math.max(x1, x2);
double min_y = Math.min(y1, y2);
double max_y = Math.max(y1, y2);
if (first) {
minX = min_x;
maxX = max_x;
Expand Down Expand Up @@ -259,8 +272,8 @@ private void find_and_write_bounding_box(LaserJob job) throws IOException

private void vector(double x, double y, double dpi, boolean as_cut, boolean force_abs) throws IOException
{
double x_mm = isFlipXaxis() ? getBedWidth() - Util.px2mm(x, dpi) : Util.px2mm(x, dpi);
double y_mm = isFlipYaxis() ? getBedHeight() - Util.px2mm(y, dpi) : Util.px2mm(y, dpi);
double x_mm = isFlipXaxis() ? flipPivotX - Util.px2mm(x, dpi) : Util.px2mm(x, dpi);
double y_mm = isFlipYaxis() ? flipPivotY - Util.px2mm(y, dpi) : Util.px2mm(y, dpi);
boolean as_absolute;

/* compute distance to last known position */
Expand Down Expand Up @@ -543,6 +556,8 @@ public void writeJobCode(LaserJob job, ProgressListener pl) throws IOException {
last_y = Double.NaN;
vector_count = 0;
travel_distance = 0;
flipPivotX = job.getTransformedOriginX() == 0 ? getBedWidth() : 0;
flipPivotY = job.getTransformedOriginY() == 0 ? getBedHeight() : 0;

try {
stream = new ByteStream(out, (byte)0x88); // 0x11, 0x38
Expand Down Expand Up @@ -593,10 +608,15 @@ public void writeJobCode(LaserJob job, ProgressListener pl) throws IOException {
/* FALLTHRU */
if (p instanceof VectorPart)
{
double top_left_x = Util.px2mm(p.getMinX(), p.getDPI());
double top_left_y = Util.px2mm(p.getMinY(), p.getDPI());
double bottom_right_x = Util.px2mm(p.getMaxX(), p.getDPI());
double bottom_right_y = Util.px2mm(p.getMaxY(), p.getDPI());
// must match the flip applied to the actual move commands in vector()
double lx1 = isFlipXaxis() ? flipPivotX - Util.px2mm(p.getMinX(), p.getDPI()) : Util.px2mm(p.getMinX(), p.getDPI());
double lx2 = isFlipXaxis() ? flipPivotX - Util.px2mm(p.getMaxX(), p.getDPI()) : Util.px2mm(p.getMaxX(), p.getDPI());
double ly1 = isFlipYaxis() ? flipPivotY - Util.px2mm(p.getMinY(), p.getDPI()) : Util.px2mm(p.getMinY(), p.getDPI());
double ly2 = isFlipYaxis() ? flipPivotY - Util.px2mm(p.getMaxY(), p.getDPI()) : Util.px2mm(p.getMaxY(), p.getDPI());
double top_left_x = Math.min(lx1, lx2);
double top_left_y = Math.min(ly1, ly2);
double bottom_right_x = Math.max(lx1, lx2);
double bottom_right_y = Math.max(ly1, ly2);
/* write dimensions */
stream.hex("E752").byteint(part_number).absoluteMM(top_left_x).absoluteMM(top_left_y);
stream.hex("E753").byteint(part_number).absoluteMM(bottom_right_x).absoluteMM(bottom_right_y);
Expand Down
157 changes: 157 additions & 0 deletions src/test/java/de/thomas_oster/liblasercut/drivers/RuidaTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*
This file is part of LibLaserCut.
Copyright (C) 2011 - 2014 Thomas Oster <mail@thomas-oster.de>

LibLaserCut is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

LibLaserCut 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 Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with LibLaserCut. If not, see <http://www.gnu.org/licenses/>.

*/
package de.thomas_oster.liblasercut.drivers;

import de.thomas_oster.liblasercut.LaserJob;
import de.thomas_oster.liblasercut.ProgressListenerDummy;
import de.thomas_oster.liblasercut.VectorPart;
import de.thomas_oster.liblasercut.properties.FloatMinMaxPowerSpeedFrequencyProperty;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.lang.reflect.Field;
import static org.junit.Assert.*;
import org.junit.Test;

/**
* Verifies the E7 03 bounding-box preamble (used by the laser's own "trace outline" feature) stays in
* sync with a job's start point, and stays correctly ordered (min < max) when an axis is flipped.
* Decodes the actual RD byte stream (Ruida's 7-bit-packed, XOR-scrambled encoding) instead of just
* checking driver-internal state, so this also catches encoding mistakes.
*/
public class RuidaTest
{
/** Inverts ByteStream.write(): XOR with magic, undo the +1, undo the bit0/bit7 swap. */
private static int unscramble(int raw, int magic)
{
int i = ((raw - 1) & 0xff) ^ (magic & 0xff);
i ^= (i >> 7) & 0xff;
i ^= (i << 7) & 0xff;
i ^= (i >> 7) & 0xff;
return i & 0xff;
}

/** Decodes a 5-byte, 7-bit-packed, MSB-first absolute value (as written by ByteStream.absoluteMM) to mm. */
private static double decodeAbsoluteMM(byte[] raw, int offset, int magic)
{
long val = 0;
for (int i = 0; i < 5; i++)
{
val = (val << 7) | unscramble(raw[offset + i] & 0xff, magic);
}
return val / 1000.0;
}

private LaserJob buildJobWithOnePoint(double xMm, double yMm) throws Exception
{
LaserJob job = new LaserJob("test", "test", "test");
VectorPart vp = new VectorPart(new FloatMinMaxPowerSpeedFrequencyProperty(), 1000);
double dpmm = 1000 / 25.4;
vp.moveto((int) Math.round(xMm * dpmm), (int) Math.round(yMm * dpmm));
vp.lineto((int) Math.round(xMm * dpmm), (int) Math.round(yMm * dpmm));
job.addPart(vp);
return job;
}

/**
* Sends the job and decodes the E7 03 (bounding box top-left) command. Calls writeJobCode() directly
* (like sendJob() eventually does, after its own checkJob() on the original, unshifted job) rather
* than through saveJob(), which - unlike sendJob() - doesn't call applyStartPoint() itself (a separate,
* pre-existing gap not under test here).
*/
private double[] sendAndDecodeBoundingBoxMin(Ruida ruida, LaserJob job) throws Exception
{
job.applyStartPoint();
ByteArrayOutputStream out = new ByteArrayOutputStream();
Field outField = Ruida.class.getDeclaredField("out");
outField.setAccessible(true);
outField.set(ruida, new PrintStream(out));
ruida.writeJobCode(job, new ProgressListenerDummy());
byte[] raw = out.toByteArray();
for (int i = 0; i + 11 < raw.length; i++)
{
if (unscramble(raw[i] & 0xff, 0x88) == 0xE7 && unscramble(raw[i + 1] & 0xff, 0x88) == 0x03)
{
double x = decodeAbsoluteMM(raw, i + 2, 0x88);
double y = decodeAbsoluteMM(raw, i + 7, 0x88);
return new double[]{x, y};
}
}
fail("E7 03 (bounding box) command not found in output");
return null;
}

@Test
public void boundingBoxReflectsStartPoint_noFlip() throws Exception
{
Ruida ruida = new Ruida();
ruida.setBedWidth(600.0);
ruida.setBedHeigth(400.0);
ruida.setFlipXaxis(false);
ruida.setFlipYaxis(false);

// design point at canonical (120, 80); start point anchored at canonical (100, 60)
// -> after the shift, the point is at local (20, 20).
LaserJob job = buildJobWithOnePoint(120, 80);
job.setStartPoint(100, 60);

double[] xy = sendAndDecodeBoundingBoxMin(ruida, job);
assertEquals(20, xy[0], 0.05);
assertEquals(20, xy[1], 0.05);
}

@Test
public void boundingBoxReflectsStartPoint_flippedX() throws Exception
{
Ruida ruida = new Ruida();
ruida.setBedWidth(600.0);
ruida.setBedHeigth(400.0);
ruida.setFlipXaxis(true);
ruida.setFlipYaxis(false);

// On a flipped axis, machine home is 0, not bedWidth - mirroring a *local* (start-point-relative)
// offset around the far bed edge would place the start point at completely the wrong end of travel.
// A valid anchor for a flipped axis must be >= the design's own extent (mirror image of the
// non-flipped case, where the anchor must be <=), so the design extends validly from home outward.
LaserJob job = buildJobWithOnePoint(120, 80);
job.setStartPoint(150, 60);

double[] xy = sendAndDecodeBoundingBoxMin(ruida, job);
// local x = 120 - 150 = -30; flipped + start-point-referenced -> mirrored around machine home (0): 0 - (-30) = 30
assertEquals(30, xy[0], 0.05);
// y not flipped: local y = 80 - 60 = 20
assertEquals(20, xy[1], 0.05);
}

@Test
public void boundingBoxUnaffectedWithoutStartPoint() throws Exception
{
Ruida ruida = new Ruida();
ruida.setBedWidth(600.0);
ruida.setBedHeigth(400.0);
ruida.setFlipXaxis(true);
ruida.setFlipYaxis(false);

LaserJob job = buildJobWithOnePoint(120, 80);
// no setStartPoint() call at all

double[] xy = sendAndDecodeBoundingBoxMin(ruida, job);
assertEquals(600 - 120, xy[0], 0.05);
assertEquals(80, xy[1], 0.05);
}
}