Repository: abey79/vpype

Issue:
Improve HPGL pen change logic
 It looks like you are calling a return to park position before each pen change. That is not a bad thing, but it is a bit of a strange way of doing it as the park position is the one that we set up in the config file, and it is not  by default the point closest to the  pen changer. Can I suggest that you only call move to park position before putting away the last pen, that is the SP0; command? For all other pen changes, simply put a  PU; command before the pen change is called. This will mean that after the pen change the plotter will resume the last pen state before the change and that would be PU;  It would still do an auto PU move before the pen change, even if you did not call it, but it would also then move to the last position before the pen change and try to first resume the last pen state (down) and leave a single dot with the new pen at the end of the old line before going to the new position.


Discussion:

Resolving patch:
diff --git a/vpype/io.py b/vpype/io.py
--- a/vpype/io.py
+++ b/vpype/io.py
@@ -495,6 +495,7 @@ def write_hpgl(
     center: bool,
     device: Optional[str],
     velocity: Optional[int],
+    absolute: bool = False,
     quiet: bool = False,
 ) -> None:
     """Create a HPGL file from the :class:`Document` instance.
@@ -516,6 +517,7 @@ def write_hpgl(
         device: name of the device to use (the corresponding config must exists). If not
             provided, a default device must be configured, which will be used.
         velocity: if provided, a VS command will be generated with the corresponding value
+        absolute: if True, only use absolute coordinates
         quiet: if True, do not print the plotter/paper info strings
     """
 
@@ -612,21 +614,39 @@ def complex_to_str(p: complex) -> str:
     if paper_config.set_ps is not None:
         output.write(f"PS{int(paper_config.set_ps)};")
 
+    first_layer = True
+    last_point: Optional[complex] = None
     for layer_id in sorted(document.layers.keys()):
         pen_id = 1 + (layer_id - 1) % plotter_config.pen_count
+
+        # As per #310, we emit a single PU; between layers
+        if not first_layer:
+            output.write(f"PU;")
+        else:
+            first_layer = False
         output.write(f"SP{pen_id};")
 
         for line in document.layers[layer_id]:
             if len(line) < 2:
                 continue
-            output.write(f"PU{complex_to_str(line[0])};")
-            output.write(f"PD")
-            output.write(",".join(complex_to_str(p) for p in line[1:]))
-            output.write(";")
 
-        output.write(
-            f"PU{paper_config.final_pu_params if paper_config.final_pu_params else ''};"
-        )
+            if absolute:
+                output.write(
+                    f"PU{complex_to_str(line[0])};PD"
+                    + ",".join(complex_to_str(p) for p in line[1:])
+                    + ";"
+                )
+            else:
+                if last_point is None:
+                    output.write(f"PU{complex_to_str(line[0])};PR;")
+                else:
+                    output.write(f"PU{complex_to_str(line[0] - last_point)};")
+
+                output.write("PD" + ",".join(complex_to_str(p) for p in np.diff(line)) + ";")
+                last_point = line[-1]
 
+    if not absolute:
+        output.write("PA;")
+    output.write(f"PU{paper_config.final_pu_params if paper_config.final_pu_params else ''};")
     output.write("SP0;IN;\n")
     output.flush()
diff --git a/vpype_cli/write.py b/vpype_cli/write.py
--- a/vpype_cli/write.py
+++ b/vpype_cli/write.py
@@ -73,6 +73,9 @@
 
 The `--landscape` and `--center` options are accepted and honored in HPGL.
 
+By default, relative coordinates are used whenever possible in the HPGL output in order to
+reduce file size. If `--absolute` is used, absolute coordinate are used instead.
+
 Optionally, the HPGL-only `--velocity` can be provided, in which case a `VS` command will be
 emitted with the provided value.
 
@@ -149,6 +152,9 @@
     help="[SVG only] Color mode for paths (default: layer).",
 )
 @click.option("-d", "--device", type=str, help="[HPGL only] Type of the plotter device.")
+@click.option(
+    "-a", "--absolute", is_flag=True, help="[HPGL only] Use absolute coordinates exclusively."
+)
 @click.option(
     "-vs",
     "--velocity",
@@ -175,6 +181,7 @@ def write(
     pen_up: bool,
     color_mode: str,
     device: Optional[str],
+    absolute: bool,
     velocity: Optional[int],
     quiet: bool,
 ):
@@ -231,6 +238,7 @@ def write(
             center=center,
             device=device,
             page_size=page_size,
+            absolute=absolute,
             velocity=velocity,
             quiet=quiet,
         )
diff --git a/vpype_viewer/_utils.py b/vpype_viewer/_utils.py
--- a/vpype_viewer/_utils.py
+++ b/vpype_viewer/_utils.py
@@ -1,6 +1,6 @@
 import os
 import pathlib
-from typing import Optional, Tuple
+from typing import Any, Dict, Optional, Tuple, cast
 
 import cachetools
 import cachetools.keys
@@ -12,7 +12,7 @@
 
 
 @cachetools.cached(
-    cache={},  # type: ignore
+    cache=cast(Dict[Any, Any], {}),
     key=lambda name, ctx: cachetools.keys.hashkey((name, id(ctx))),
 )
 def load_program(name: str, ctx: mgl.Context) -> mgl.Program:
@@ -48,7 +48,7 @@ def _load_shader(path: str) -> Optional[str]:
 
 
 @cachetools.cached(
-    cache={},  # type: ignore
+    cache=cast(Dict[Any, Any], {}),
     key=lambda name, ctx, size, components: cachetools.keys.hashkey(
         (name, id(ctx), size, components)
     ),


Test patch:
diff --git a/tests/test_hpgl.py b/tests/test_hpgl.py
--- a/tests/test_hpgl.py
+++ b/tests/test_hpgl.py
@@ -6,44 +6,76 @@
 from vpype_cli import cli
 
 HPGL_TEST_CASES = [
-    ("line 2 3 6 4", "-d simple -p simple", "IN;DF;SP1;PU2,3;PD6,4;PU;SP0;IN;"),
-    ("line 2 3 50 3", "-d simple -p simple", "IN;DF;SP1;PU2,3;PD10,3;PU;SP0;IN;"),
-    ("line 2 3 6 4", "-d simple -p aka_simple", "IN;DF;SP1;PU2,3;PD6,4;PU;SP0;IN;"),
-    ("line 2 3 6 4", "-d simple -p simple_ps10", "IN;DF;PS10;SP1;PU2,3;PD6,4;PU;SP0;IN;"),
+    ("line 2 3 6 4", "--absolute -d simple -p simple", "IN;DF;SP1;PU2,3;PD6,4;PU;SP0;IN;"),
+    ("line 2 3 6 4", "-d simple -p simple", "IN;DF;SP1;PU2,3;PR;PD4,1;PA;PU;SP0;IN;"),
+    ("line 2 3 50 3", "--absolute -d simple -p simple", "IN;DF;SP1;PU2,3;PD10,3;PU;SP0;IN;"),
+    ("line 2 3 6 4", "--absolute -d simple -p aka_simple", "IN;DF;SP1;PU2,3;PD6,4;PU;SP0;IN;"),
+    (
+        "line 2 3 6 4",
+        "--absolute -d simple -p simple_ps10",
+        "IN;DF;PS10;SP1;PU2,3;PD6,4;PU;SP0;IN;",
+    ),
+    (
+        "line 2 3 6 4",
+        "-d simple -p simple_ps10",
+        "IN;DF;PS10;SP1;PU2,3;PR;PD4,1;PA;PU;SP0;IN;",
+    ),
     (
         "line 2 3 6 4 line -l 2 4 5 2 3",
-        "-d simple -p simple",
+        "--absolute -d simple -p simple",
         "IN;DF;SP1;PU2,3;PD6,4;PU;SP2;PU4,5;PD2,3;PU;SP0;IN;",
     ),
-    ("line 2 3 6 4", "-d simple -p simple_landscape", "IN;DF;SP1;PU3,8;PD4,4;PU;SP0;IN;"),
-    ("line 2 3 6 4", "-d simple -p simple_y_up", "IN;DF;SP1;PU2,12;PD6,11;PU;SP0;IN;"),
     (
         "line 2 3 6 4",
-        "-d simple -p simple_rotate_180",
+        "--absolute -d simple -p simple_landscape",
+        "IN;DF;SP1;PU3,8;PD4,4;PU;SP0;IN;",
+    ),
+    (
+        "line 2 3 6 4",
+        "--absolute -d simple -p simple_y_up",
+        "IN;DF;SP1;PU2,12;PD6,11;PU;SP0;IN;",
+    ),
+    (
+        "line 2 3 6 4",
+        "--absolute -d simple -p simple_rotate_180",
         "IN;DF;SP1;PU8,12;PD4,11;PU;SP0;IN;",
     ),
     (
         "line 2 3 6 4",
-        "-d simple -p simple_final_pu",
+        "--absolute -d simple -p simple_final_pu",
         "IN;DF;SP1;PU2,3;PD6,4;PU0,0;SP0;IN;",
     ),
-    ("line 3 5 4 6", "-d double -p simple", "IN;DF;SP1;PU6,10;PD8,12;PU;SP0;IN;"),
-    ("line 1 1 2 4", "-d simple -p simple_botleft", "IN;DF;SP1;PU0,13;PD1,10;PU;SP0;IN;"),
+    ("line 3 5 4 6", "--absolute -d double -p simple", "IN;DF;SP1;PU6,10;PD8,12;PU;SP0;IN;"),
+    (
+        "line 1 1 2 4",
+        "--absolute -d simple -p simple_botleft",
+        "IN;DF;SP1;PU0,13;PD1,10;PU;SP0;IN;",
+    ),
     (
         "line 1 1 2 4 pagesize 10x15",
-        "-d simple -p simple_flex_portrait",
+        "--absolute -d simple -p simple_flex_portrait",
         "IN;DF;SP1;PU0,13;PD1,10;PU;SP0;IN;",
     ),
     (
         "line 1 1 2 4 pagesize 10x15",
-        "-d simple -p simple_flex_portrait_implicit",
+        "--absolute -d simple -p simple_flex_portrait_implicit",
         "IN;DF;SP1;PU0,13;PD1,10;PU;SP0;IN;",
     ),
     (
         "line 1 1 2 4 pagesize 10x15",
-        "-d simple -p simple_flex_landscape",
+        "--absolute -d simple -p simple_flex_landscape",
         "IN;DF;SP1;PU0,0;PD3,1;PU;SP0;IN;",
     ),
+    (
+        "line 0 0 2 3 line -l2 5 5 2 3",
+        "--absolute -d simple -p simple",
+        "IN;DF;SP1;PU0,0;PD2,3;PU;SP2;PU5,5;PD2,3;PU;SP0;IN;",
+    ),
+    (
+        "line 0 0 2 3 line -l2 5 5 2 3",
+        "-d simple -p simple",
+        "IN;DF;SP1;PU0,0;PR;PD2,3;PU;SP2;PU3,2;PD-3,-2;PA;PU;SP0;IN;",
+    ),
 ]
 
 
@@ -269,7 +301,9 @@ def test_hpgl_paper_config(simple_printer_config):
 
 
 def test_hpgl_paper_size_inference(runner):
-    res = runner.invoke(cli, "rect 5cm 5cm 5cm 5cm pagesize a4 write -f hpgl -d hp7475a -")
+    res = runner.invoke(
+        cli, "rect 5cm 5cm 5cm 5cm pagesize a4 write --absolute -f hpgl -d hp7475a -"
+    )
 
     assert res.exit_code == 0
     assert res.stdout.strip() == (