From 7bc891b69612c4349da9fdada31a0277ceb231ee Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=BCrgen=20Weigert?= <jnweiger@gmail.com>
Date: Sun, 6 Sep 2015 20:02:04 +0200
Subject: [PATCH]  This is a driver for a cutting plotter.

It was written to support a Goldcut ABH 721 [Refine MH721, Jinka JK721]
Speed and pressure of the devices cannot be software-controlled. This is done on the device.

These devices are simple HPGL plotters, thus the driver should support many more devices.
Maybe helpful with t-oster/VisiCut#256
---
 src/com/t_oster/liblasercut/LibInfo.java      |   1 +
 .../liblasercut/drivers/GoldCutHPGL.java      | 607 ++++++++++++++++++
 2 files changed, 608 insertions(+)
 create mode 100644 src/com/t_oster/liblasercut/drivers/GoldCutHPGL.java

diff --git a/src/com/t_oster/liblasercut/LibInfo.java b/src/com/t_oster/liblasercut/LibInfo.java
index c848f34..ec2f570 100644
--- a/src/com/t_oster/liblasercut/LibInfo.java
+++ b/src/com/t_oster/liblasercut/LibInfo.java
@@ -40,6 +40,7 @@ public class LibInfo
       EpilogZing.class,
       EpilogHelix.class,
       LaosCutter.class,
+      GoldCutHPGL.class,
       Lasersaur.class,
       Dummy.class,
       IModelaMill.class,
diff --git a/src/com/t_oster/liblasercut/drivers/GoldCutHPGL.java b/src/com/t_oster/liblasercut/drivers/GoldCutHPGL.java
new file mode 100644
index 0000000..937fa3f
--- /dev/null
+++ b/src/com/t_oster/liblasercut/drivers/GoldCutHPGL.java
@@ -0,0 +1,607 @@
+/**
+ * This file is part of LibLaserCut.
+ * Copyright (C) 2011 - 2014 Thomas Oster <mail@thomas-oster.de>
+ * Copyright (C) 2015 Jürgen Weigert <juewei@fabfolk.com>
+ *
+ * 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 com.t_oster.liblasercut.drivers;
+
+import com.t_oster.liblasercut.*;
+import com.t_oster.liblasercut.platform.Point;
+import com.t_oster.liblasercut.platform.Util;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.*;
+import purejavacomm.CommPort;
+import purejavacomm.CommPortIdentifier;
+import purejavacomm.SerialPort;
+import java.net.URI;
+
+/**
+ * This class implements a simple HPGL driver, suitable for the GoldCut ABH 721 Cutter.
+ *
+ * @author Jürgen Weigert <juewei@fabfolk.com>
+ */
+public class GoldCutHPGL extends LaserCutter {
+
+  private static final String SETTING_COMPORT = "COM-Port/Device (or file:///tmp/out.hpgl)";
+  private static final String SETTING_INITSTRING = "HPGL initialization";
+  private static final String SETTING_FINISTRING = "HPGL shutdown";
+  private static final String SETTING_BEDWIDTH = "Cutter width [mm]";
+  private static final String SETTING_HARDWARE_DPI = "Cutter resolution [steps/inch]";
+  private static final String SETTING_FLIPX = "X axis goes right to left (yes/no)";
+  private static final String SETTING_FLIPY = "Y axis goes front to back (yes/no)";
+  private static final String SETTING_RASTER_WHITESPACE = "Additional space per raster line (mm)";
+
+  protected int hw_x = 0;
+  protected int hw_y = 0;
+
+  @Override
+  public String getModelName() {
+    return "GoldCutHPGL";
+  }
+  private double addSpacePerRasterLine = 0.5;
+
+  /**
+   * Get the value of addSpacePerRasterLine
+   *
+   * @return the value of addSpacePerRasterLine
+   */
+  public double getAddSpacePerRasterLine() {
+    return addSpacePerRasterLine;
+  }
+
+  /**
+   * Set the value of addSpacePerRasterLine
+   *
+   * @param addSpacePerRasterLine new value of addSpacePerRasterLine
+   */
+  public void setAddSpacePerRasterLine(double addSpacePerRasterLine) {
+    this.addSpacePerRasterLine = addSpacePerRasterLine;
+  }
+
+  protected boolean flipYaxis = false;
+  /**
+   * Get the value of flipYaxis
+   *
+   * @return the value of flipYaxis
+   */
+  public boolean isFlipYaxis() {
+    return flipYaxis;
+  }
+  /**
+   * Set the value of flipYaxis
+   *
+   * @param flipXaxis new value of flipYaxis
+   */
+  public void setFlipYaxis(boolean flipYaxis) {
+    // System.err.printf("setFlipYaxis %d -> %d\n", this.flipYaxis?1:0, flipYaxis?1:0);
+    this.flipYaxis = flipYaxis;
+  }
+
+  protected boolean flipXaxis = false;
+  /**
+   * Get the value of flipXaxis
+   *
+   * @return the value of flipXaxis
+   */
+  public boolean isFlipXaxis() {
+    return flipXaxis;
+  }
+  /**
+   * Set the value of flipXaxis
+   *
+   * @param flipXaxis new value of flipXaxis
+   */
+  public void setFlipXaxis(boolean flipXaxis) {
+    this.flipXaxis = flipXaxis;
+  }
+
+  protected String finiString = "!PG;;";
+  /**
+   * Get the value of finiString
+   *
+   * @return the value of finiString
+   */
+  public String getFiniString() {
+    return finiString;
+  }
+  /**
+   * Set the value of finiString
+   *
+   * @param comPort new value of finiString
+   */
+  public void setFiniString(String finiString) {
+    this.finiString = finiString;
+  }
+
+  protected String initString = "IN;PA;";
+  /**
+   * Get the value of initString
+   *
+   * @return the value of initString
+   */
+  public String getInitString() {
+    return initString;
+  }
+  /**
+   * Set the value of initString
+   *
+   * @param comPort new value of initString
+   */
+  public void setInitString(String initString) {
+    this.initString = initString;
+  }
+
+  protected String comPort = "/dev/ttyUSB0";
+  /**
+   * Get the value of port
+   *
+   * @return the value of port
+   */
+  public String getComPort() {
+    return comPort;
+  }
+  /**
+   * Set the value of port
+   *
+   * @param comPort new value of port
+   */
+  public void setComPort(String comPort) {
+    this.comPort = comPort;
+  }
+
+  private byte[] generateVectorGCode(VectorPart vp, double resolution) throws UnsupportedEncodingException {
+    ByteArrayOutputStream result = new ByteArrayOutputStream();
+    PrintStream out = new PrintStream(result, true, "US-ASCII");
+    for (VectorCommand cmd : vp.getCommandList()) {
+      switch (cmd.getType()) {
+        case MOVETO:
+          int x = cmd.getX();
+          int y = cmd.getY();
+          move(out, x, y, resolution);
+          break;
+        case LINETO:
+          x = cmd.getX();
+          y = cmd.getY();
+          line(out, x, y, resolution);
+          break;
+        case SETPROPERTY:
+          PowerSpeedFocusFrequencyProperty p = (PowerSpeedFocusFrequencyProperty) cmd.getProperty();
+          setPower(out, p.getPower());
+          setSpeed(out, p.getSpeed());
+          break;
+      }
+    }
+    return result.toByteArray();
+  }
+  private int currentPower = -1;
+  private int currentSpeed = -1;
+
+  private void setSpeed(PrintStream out, int speedInPercent) {
+    if (speedInPercent != currentSpeed) {
+      // out.printf(Locale.US, "G1 F%i\n", (int) ((double) speedInPercent * this.getLaserRate() / 100));
+      currentSpeed = speedInPercent;
+    }
+
+  }
+
+  private void setPower(PrintStream out, int powerInPercent) {
+    if (powerInPercent != currentPower) {
+      // out.printf(Locale.US, "S%i\n", (int) (255d * powerInPercent / 100));
+      currentPower = powerInPercent;
+    }
+  }
+
+  private void move(PrintStream out, int x, int y, double resolution) {
+    double hw_scale = this.getHwDPI()/resolution;
+    hw_x = (int)(hw_scale * (isFlipXaxis() ? Util.mm2px(this.bedWidth, resolution) - y : y));
+    hw_y = (int)(hw_scale * (isFlipYaxis() ? 1000-x : x));
+    out.printf(Locale.US, "PU%d,%d;", hw_x, hw_y);
+  }
+
+  private void line(PrintStream out, int x, int y, double resolution) {
+    double hw_scale = this.getHwDPI()/resolution;
+    hw_x = (int)(hw_scale * (isFlipXaxis() ? Util.mm2px(this.bedWidth, resolution) - y : y));
+    hw_y = (int)(hw_scale * (isFlipYaxis() ? 1000-x : x));
+    out.printf(Locale.US, "PD%d,%d;", hw_x, hw_y);
+  }
+
+  private byte[] generatePseudoRaster3dGCode(Raster3dPart rp, double resolution) throws UnsupportedEncodingException {
+    ByteArrayOutputStream result = new ByteArrayOutputStream();
+    PrintStream out = new PrintStream(result, true, "US-ASCII");
+    boolean dirRight = true;
+    Point rasterStart = rp.getRasterStart();
+    PowerSpeedFocusProperty prop = (PowerSpeedFocusProperty) rp.getLaserProperty();
+    setSpeed(out, prop.getSpeed());
+    for (int line = 0; line < rp.getRasterHeight(); line++) {
+      Point lineStart = rasterStart.clone();
+      lineStart.y += line;
+      List<Byte> bytes = rp.getRasterLine(line);
+      //remove heading zeroes
+      while (bytes.size() > 0 && bytes.get(0) == 0) {
+        bytes.remove(0);
+        lineStart.x += 1;
+      }
+      //remove trailing zeroes
+      while (bytes.size() > 0 && bytes.get(bytes.size() - 1) == 0) {
+        bytes.remove(bytes.size() - 1);
+      }
+      if (bytes.size() > 0) {
+        if (dirRight) {
+          //move to the first nonempyt point of the line
+          move(out, lineStart.x, lineStart.y, resolution);
+          byte old = bytes.get(0);
+          for (int pix = 0; pix < bytes.size(); pix++) {
+            if (bytes.get(pix) != old) {
+              if (old == 0) {
+                move(out, lineStart.x + pix, lineStart.y, resolution);
+              } else {
+                setPower(out, prop.getPower() * (0xFF & old) / 255);
+                line(out, lineStart.x + pix - 1, lineStart.y, resolution);
+                move(out, lineStart.x + pix, lineStart.y, resolution);
+              }
+              old = bytes.get(pix);
+            }
+          }
+          //last point is also not "white"
+          setPower(out, prop.getPower() * (0xFF & bytes.get(bytes.size() - 1)) / 255);
+          line(out, lineStart.x + bytes.size() - 1, lineStart.y, resolution);
+        } else {
+          //move to the last nonempty point of the line
+          move(out, lineStart.x + bytes.size() - 1, lineStart.y, resolution);
+          byte old = bytes.get(bytes.size() - 1);
+          for (int pix = bytes.size() - 1; pix >= 0; pix--) {
+            if (bytes.get(pix) != old || pix == 0) {
+              if (old == 0) {
+                move(out, lineStart.x + pix, lineStart.y, resolution);
+              } else {
+                setPower(out, prop.getPower() * (0xFF & old) / 255);
+                line(out, lineStart.x + pix + 1, lineStart.y, resolution);
+                move(out, lineStart.x + pix, lineStart.y, resolution);
+              }
+              old = bytes.get(pix);
+            }
+          }
+          //last point is also not "white"
+          setPower(out, prop.getPower() * (0xFF & bytes.get(0)) / 255);
+          line(out, lineStart.x, lineStart.y, resolution);
+        }
+      }
+      dirRight = !dirRight;
+    }
+    return result.toByteArray();
+  }
+
+  private byte[] generatePseudoRasterGCode(RasterPart rp, double resolution) throws UnsupportedEncodingException {
+    ByteArrayOutputStream result = new ByteArrayOutputStream();
+    PrintStream out = new PrintStream(result, true, "US-ASCII");
+    boolean dirRight = true;
+    Point rasterStart = rp.getRasterStart();
+    PowerSpeedFocusProperty prop = (PowerSpeedFocusProperty) rp.getLaserProperty();
+    setSpeed(out, prop.getSpeed());
+    setPower(out, prop.getPower());
+    for (int line = 0; line < rp.getRasterHeight(); line++) {
+      Point lineStart = rasterStart.clone();
+      lineStart.y += line;
+      List<Byte> bytes = new LinkedList<Byte>();
+      boolean lookForStart = true;
+      for (int x = 0; x < rp.getRasterWidth(); x++) {
+        if (lookForStart) {
+          if (rp.isBlack(x, line)) {
+            lookForStart = false;
+            bytes.add((byte) 255);
+          } else {
+            lineStart.x += 1;
+          }
+        } else {
+          bytes.add(rp.isBlack(x, line) ? (byte) 255 : (byte) 0);
+        }
+      }
+      //remove trailing zeroes
+      while (bytes.size() > 0 && bytes.get(bytes.size() - 1) == 0) {
+        bytes.remove(bytes.size() - 1);
+      }
+      if (bytes.size() > 0) {
+        if (dirRight) {
+          //add some space to the left
+          move(out, Math.max(0, (int) (lineStart.x - Util.mm2px(this.addSpacePerRasterLine, resolution))), lineStart.y, resolution);
+          //move to the first nonempyt point of the line
+          move(out, lineStart.x, lineStart.y, resolution);
+          byte old = bytes.get(0);
+          for (int pix = 0; pix < bytes.size(); pix++) {
+            if (bytes.get(pix) != old) {
+              if (old == 0) {
+                move(out, lineStart.x + pix, lineStart.y, resolution);
+              } else {
+                setPower(out, prop.getPower() * (0xFF & old) / 255);
+                line(out, lineStart.x + pix - 1, lineStart.y, resolution);
+                move(out, lineStart.x + pix, lineStart.y, resolution);
+              }
+              old = bytes.get(pix);
+            }
+          }
+          //last point is also not "white"
+          setPower(out, prop.getPower() * (0xFF & bytes.get(bytes.size() - 1)) / 255);
+          line(out, lineStart.x + bytes.size() - 1, lineStart.y, resolution);
+          //add some space to the right
+          move(out, Math.min((int) Util.mm2px(bedWidth, resolution), (int) (lineStart.x + bytes.size() - 1 + Util.mm2px(this.addSpacePerRasterLine, resolution))), lineStart.y, resolution);
+        } else {
+          //add some space to the right
+          move(out, Math.min((int) Util.mm2px(bedWidth, resolution), (int) (lineStart.x + bytes.size() - 1 + Util.mm2px(this.addSpacePerRasterLine, resolution))), lineStart.y, resolution);
+          //move to the last nonempty point of the line
+          move(out, lineStart.x + bytes.size() - 1, lineStart.y, resolution);
+          byte old = bytes.get(bytes.size() - 1);
+          for (int pix = bytes.size() - 1; pix >= 0; pix--) {
+            if (bytes.get(pix) != old || pix == 0) {
+              if (old == 0) {
+                move(out, lineStart.x + pix, lineStart.y, resolution);
+              } else {
+                setPower(out, prop.getPower() * (0xFF & old) / 255);
+                line(out, lineStart.x + pix + 1, lineStart.y, resolution);
+                move(out, lineStart.x + pix, lineStart.y, resolution);
+              }
+              old = bytes.get(pix);
+            }
+          }
+          //last point is also not "white"
+          setPower(out, prop.getPower() * (0xFF & bytes.get(0)) / 255);
+          line(out, lineStart.x, lineStart.y, resolution);
+          //add some space to the left
+          move(out, Math.max(0, (int) (lineStart.x - Util.mm2px(this.addSpacePerRasterLine, resolution))), lineStart.y, resolution);
+        }
+      }
+      dirRight = !dirRight;
+    }
+    return result.toByteArray();
+  }
+
+  private byte[] generateInitializationCode() throws UnsupportedEncodingException {
+    ByteArrayOutputStream result = new ByteArrayOutputStream();
+    PrintStream out = new PrintStream(result, true, "US-ASCII");
+    out.print(this.initString);
+    return result.toByteArray();
+  }
+
+  private byte[] generateShutdownCode() throws UnsupportedEncodingException {
+    ByteArrayOutputStream result = new ByteArrayOutputStream();
+    PrintStream out = new PrintStream(result, true, "US-ASCII");
+    out.printf(Locale.US, "PU%d,%d;", this.hw_y, this.hw_x);
+    //back to origin and shutdown
+    out.print(this.finiString);
+    return result.toByteArray();
+  }
+
+  @Override
+  public void sendJob(LaserJob job, ProgressListener pl, List<String> warnings) throws IllegalJobException, Exception {
+    pl.progressChanged(this, 0);
+    this.currentPower = -1;
+    this.currentSpeed = -1;
+    BufferedOutputStream out;
+    SerialPort port = null;
+    pl.taskChanged(this, "checking job");
+    checkJob(job);
+    job.applyStartPoint();
+    pl.taskChanged(this, "connecting");
+    if (this.getComPort().startsWith("file://"))
+    {
+	out = new BufferedOutputStream(new FileOutputStream(new File(new URI(this.getComPort()))));
+    }
+    else
+    {
+        String ComPortName = this.getComPort();
+        if (ComPortName.startsWith("/dev/"))
+	{
+	  // allow "/dev/ttyUSB0", although we need only "ttyUSB0"
+ 	  ComPortName = ComPortName.substring(5);
+	}
+	CommPortIdentifier cpi = null;
+	//since the CommPortIdentifier.getPortIdentifier(String name) method
+	//is not working as expected, we have to manually find our port.
+	Enumeration en = CommPortIdentifier.getPortIdentifiers();
+	while (en.hasMoreElements())
+	{
+	  Object o = en.nextElement();
+	  if (o instanceof CommPortIdentifier && ((CommPortIdentifier) o).getName().equals(ComPortName))
+	  {
+	    cpi = (CommPortIdentifier) o;
+	    break;
+	  }
+	}
+	if (cpi == null)
+	{
+	  throw new Exception("Error: No such COM-Port '"+this.getComPort()+"'");
+	}
+	CommPort tmp = cpi.open("VisiCut", 10000);
+	if (tmp == null)
+	{
+	  throw new Exception("Error: Could not Open COM-Port '"+this.getComPort()+"'");
+	}
+	if (!(tmp instanceof SerialPort))
+	{
+	  throw new Exception("Port '"+this.getComPort()+"' is not a serial port.");
+	}
+	port = (SerialPort) tmp;
+	port.setFlowControlMode(SerialPort.FLOWCONTROL_NONE);
+	port.setSerialPortParams(9600, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
+	out = new BufferedOutputStream(port.getOutputStream());
+	pl.taskChanged(this, "sending");
+    }
+    out.write(this.generateInitializationCode());
+    pl.progressChanged(this, 20);
+    int i = 0;
+    int max = job.getParts().size();
+    for (JobPart p : job.getParts())
+    {
+      if (p instanceof Raster3dPart)
+      {
+        out.write(this.generatePseudoRaster3dGCode((Raster3dPart) p, p.getDPI()));
+      }
+      else if (p instanceof RasterPart)
+      {
+        out.write(this.generatePseudoRasterGCode((RasterPart) p, p.getDPI()));
+      }
+      else if (p instanceof VectorPart)
+      {
+        out.write(this.generateVectorGCode((VectorPart) p, p.getDPI()));
+      }
+      i++;
+      pl.progressChanged(this, 20 + (int) (i*(double) 60/max));
+    }
+    out.write(this.generateShutdownCode());
+    out.close();
+    if (port != null)
+    {
+      port.close();
+    }
+    pl.taskChanged(this, "sent.");
+    pl.progressChanged(this, 100);
+  }
+  private List<Double> resolutions;
+
+  @Override
+  public List<Double> getResolutions() {
+    if (resolutions == null) {
+      resolutions = Arrays.asList(new Double[]{
+                500d
+              });
+    }
+    return resolutions;
+  }
+
+  protected double bedWidth = 630;
+  /**
+   * Get the value of bedWidth
+   *
+   * @return the value of bedWidth
+   */
+  @Override
+  public double getBedWidth() {
+    return bedWidth;
+  }
+  /**
+   * Set the value of bedWidth [mm]
+   *
+   * @param bedWidth new value of bedWidth
+   */
+  public void setBedWidth(double bedWidth) {
+    this.bedWidth = bedWidth;
+  }
+
+  // unused dummy code. But needed to survive overloading errors.
+  /**
+   * Get the value of bedHeight [mm]
+   *
+   * @return the value of bedHeight
+   */
+  @Override
+  public double getBedHeight() {
+    return 1000;	// dummy value, used for GUI!
+  }
+
+  protected double hwDPI = 1000.;
+  /**
+   * Get the value of hwDPI
+   *
+   * @return the value of hwDPI
+   */
+  public double getHwDPI() {
+    return hwDPI;
+  }
+  /**
+   * Set the value of hwDPI
+   *
+   * @param hwDPI new value of hwDPI
+   */
+  public void setHwDPI(double hwDPI) {
+    this.hwDPI = hwDPI;
+  }
+
+  private static String[] settingAttributes = new String[]{
+    SETTING_BEDWIDTH,
+    SETTING_HARDWARE_DPI,
+//    SETTING_FLIPX,
+//    SETTING_FLIPY,
+    SETTING_COMPORT,
+    SETTING_RASTER_WHITESPACE,
+    SETTING_INITSTRING,
+    SETTING_FINISTRING,
+  };
+
+  @Override
+  public String[] getPropertyKeys() {
+    return settingAttributes;
+  }
+
+  @Override
+  public Object getProperty(String attribute) {
+    if (SETTING_RASTER_WHITESPACE.equals(attribute)) {
+      return this.getAddSpacePerRasterLine();
+    } else if (SETTING_COMPORT.equals(attribute)) {
+      return this.getComPort();
+//    } else if (SETTING_FLIPX.equals(attribute)) {
+//      return this.isFlipXaxis();
+//    } else if (SETTING_FLIPY.equals(attribute)) {
+//      return this.isFlipYaxis();
+    } else if (SETTING_BEDWIDTH.equals(attribute)) {
+      return this.getBedWidth();
+    } else if (SETTING_HARDWARE_DPI.equals(attribute)) {
+      return this.getHwDPI();
+    } else if (SETTING_INITSTRING.equals(attribute)) {
+      return this.getInitString();
+    } else if (SETTING_FINISTRING.equals(attribute)) {
+      return this.getFiniString();
+    }
+    return null;
+  }
+
+  @Override
+  public void setProperty(String attribute, Object value) {
+    if (SETTING_RASTER_WHITESPACE.equals(attribute)) {
+      this.setAddSpacePerRasterLine((Double) value);
+    } else if (SETTING_COMPORT.equals(attribute)) {
+      this.setComPort((String) value);
+//    } else if (SETTING_FLIPX.equals(attribute)) {
+//      this.setFlipXaxis((Boolean) value);
+//    } else if (SETTING_FLIPY.equals(attribute)) {
+//      this.setFlipYaxis((Boolean) value);
+    } else if (SETTING_BEDWIDTH.equals(attribute)) {
+      this.setBedWidth((Double) value);
+    } else if (SETTING_HARDWARE_DPI.equals(attribute)) {
+      this.setHwDPI((Double) value);
+    } else if (SETTING_INITSTRING.equals(attribute)) {
+      this.setInitString((String) value);
+    } else if (SETTING_FINISTRING.equals(attribute)) {
+      this.setFiniString((String) value);
+    }
+  }
+
+  @Override
+  public LaserCutter clone() {
+    GoldCutHPGL clone = new GoldCutHPGL();
+    clone.comPort = comPort;
+    clone.bedWidth = bedWidth;
+    clone.hwDPI = hwDPI;
+    clone.flipXaxis = flipXaxis;
+    clone.flipYaxis = flipYaxis;
+    clone.addSpacePerRasterLine = addSpacePerRasterLine;
+    return clone;
+  }
+}
-- 
GitLab