cli-anything-meerk40t 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. cli_anything_meerk40t-1.0.0/LICENSE +21 -0
  2. cli_anything_meerk40t-1.0.0/PKG-INFO +38 -0
  3. cli_anything_meerk40t-1.0.0/README.md +77 -0
  4. cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/README.md +132 -0
  5. cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/__init__.py +1 -0
  6. cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/__main__.py +4 -0
  7. cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/core/__init__.py +0 -0
  8. cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/core/device.py +64 -0
  9. cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/core/elements.py +193 -0
  10. cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/core/export.py +76 -0
  11. cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/core/operations.py +48 -0
  12. cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/core/project.py +82 -0
  13. cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/core/session.py +88 -0
  14. cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/meerk40t_cli.py +836 -0
  15. cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/skills/SKILL.md +153 -0
  16. cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/tests/__init__.py +0 -0
  17. cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/tests/test_core.py +275 -0
  18. cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/tests/test_full_e2e.py +223 -0
  19. cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/utils/__init__.py +0 -0
  20. cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/utils/meerk40t_backend.py +237 -0
  21. cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/utils/repl_skin.py +567 -0
  22. cli_anything_meerk40t-1.0.0/cli_anything_meerk40t.egg-info/PKG-INFO +38 -0
  23. cli_anything_meerk40t-1.0.0/cli_anything_meerk40t.egg-info/SOURCES.txt +27 -0
  24. cli_anything_meerk40t-1.0.0/cli_anything_meerk40t.egg-info/dependency_links.txt +1 -0
  25. cli_anything_meerk40t-1.0.0/cli_anything_meerk40t.egg-info/entry_points.txt +2 -0
  26. cli_anything_meerk40t-1.0.0/cli_anything_meerk40t.egg-info/requires.txt +10 -0
  27. cli_anything_meerk40t-1.0.0/cli_anything_meerk40t.egg-info/top_level.txt +1 -0
  28. cli_anything_meerk40t-1.0.0/setup.cfg +4 -0
  29. cli_anything_meerk40t-1.0.0/setup.py +57 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 meerk40t
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.4
2
+ Name: cli-anything-meerk40t
3
+ Version: 1.0.0
4
+ Summary: Agent CLI harness for MeerK40t laser cutting software
5
+ Author: George-RD
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Environment :: Console
11
+ Classifier: Topic :: Multimedia :: Graphics
12
+ Classifier: Topic :: Utilities
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/plain
15
+ License-File: LICENSE
16
+ Requires-Dist: click>=8.0
17
+ Requires-Dist: prompt_toolkit>=3.0
18
+ Requires-Dist: meerk40t>=0.9.0
19
+ Requires-Dist: pyusb>=1.0.0
20
+ Requires-Dist: pyserial
21
+ Requires-Dist: numpy
22
+ Requires-Dist: Pillow>=7.0.0
23
+ Requires-Dist: ezdxf>=0.14.0
24
+ Requires-Dist: requests>=2.25.0
25
+ Requires-Dist: websocket-client
26
+ Dynamic: author
27
+ Dynamic: classifier
28
+ Dynamic: description
29
+ Dynamic: description-content-type
30
+ Dynamic: license
31
+ Dynamic: license-file
32
+ Dynamic: requires-dist
33
+ Dynamic: requires-python
34
+ Dynamic: summary
35
+
36
+ cli-anything-meerk40t is a stateful CLI + REPL that wraps the real MeerK40t kernel for headless, agent-driven laser job preparation. It exposes project, elements, operations, device, export, session, and console-passthrough commands with --json output for AI agents.
37
+
38
+ The real MeerK40t software is a hard dependency — the CLI drives it via kernel.console() and exports SVG/G-code through the real backend.
@@ -0,0 +1,77 @@
1
+ # cli-anything-meerk40t
2
+
3
+ Agent CLI harness for **MeerK40t** laser cutting/engraving software.
4
+
5
+ Part of the [CLI-Anything](https://github.com/HKUDS/CLI-Anything) ecosystem —
6
+ install via `cli-hub install meerk40t` or `pip install cli-anything-meerk40t`.
7
+
8
+ ## What it does
9
+
10
+ A stateful CLI + REPL that wraps the **real MeerK40t kernel** for headless,
11
+ agent-driven laser job preparation. It exposes project, elements, operations,
12
+ device, export, session, and console-passthrough commands with `--json` output
13
+ for AI agents.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ # Install the CLI harness
19
+ pip install cli-anything-meerk40t
20
+
21
+ # Verify
22
+ cli-anything-meerk40t --help
23
+ ```
24
+
25
+ MeerK40t and all headless dependencies are installed automatically.
26
+
27
+ ## Usage
28
+
29
+ ```bash
30
+ # One-shot commands
31
+ cli-anything-meerk40t --json project new
32
+ cli-anything-meerk40t --json -p /tmp/job.svg elements circle 1in 1in 1in
33
+ cli-anything-meerk40t --json -p /tmp/job.svg elements rect 2in 2in 1in 1in --stroke red --fill blue
34
+ cli-anything-meerk40t --json -p /tmp/job.svg elements list
35
+ cli-anything-meerk40t --json operations classify
36
+ cli-anything-meerk40t --json export svg /tmp/out.svg
37
+
38
+ # Console passthrough (escape hatch to raw MeerK40t console)
39
+ cli-anything-meerk40t console 'service device start -i grbl'
40
+ cli-anything-meerk40t --json export gcode /tmp/out.gcode
41
+
42
+ # Interactive REPL (default when no subcommand)
43
+ cli-anything-meerk40t
44
+ ```
45
+
46
+ ## Command groups
47
+
48
+ | Group | Description |
49
+ |---|---|
50
+ | `project` | new, open, save, info, close (SVG project files) |
51
+ | `elements` | circle, rect, ellipse, line, polyline, text, list, delete, select, clear, frame |
52
+ | `operations` | list, add (cut/engrave/raster/image/dots), classify, declassify, set |
53
+ | `device` | list, status, home, physical-home, move, info |
54
+ | `export` | svg, svgz (real backend); png (GUI-dependent); gcode (GRBL device required) |
55
+ | `console` | Raw passthrough to the MeerK40t kernel console |
56
+ | `session` | undo, redo, history, status |
57
+ | `repl` | Interactive shell (default) |
58
+
59
+ ## Export formats
60
+
61
+ - **SVG** (default, plain, compressed/svgz) — truthful, rendered by the real MeerK40t SVGWriter. Works headless.
62
+ - **G-code** — generated via the real GRBL `save_job` pipeline. Requires an active GRBL device.
63
+ - **PNG** — requires wxPython GUI. Errors clearly in headless mode.
64
+
65
+ ## Testing
66
+
67
+ ```bash
68
+ pip install -e .
69
+ python -m unittest cli_anything.meerk40t.tests.test_core -v
70
+ python -m unittest cli_anything.meerk40t.tests.test_full_e2e -v
71
+ ```
72
+
73
+ 38 tests, 100% pass rate.
74
+
75
+ ## License
76
+
77
+ MIT
@@ -0,0 +1,132 @@
1
+ # cli-anything-meerk40t
2
+
3
+ Agent CLI harness for **MeerK40t** laser cutting/engraving software.
4
+
5
+ This is a stateful CLI + REPL that wraps the **real MeerK40t kernel** for
6
+ headless, agent-driven laser job preparation. It exposes project, elements,
7
+ operations, device, export, session, and console-passthrough commands with
8
+ `--json` output for AI agents.
9
+
10
+ ## Install
11
+
12
+ ### 1. Install MeerK40t (the real software — hard dependency)
13
+
14
+ ```bash
15
+ # From source (recommended — pulls all headless deps):
16
+ git clone https://github.com/meerk40t/meerk40t
17
+ cd meerk40t
18
+ pip install -r requirements-nogui.txt
19
+ pip install -e .
20
+
21
+ # Or from PyPI:
22
+ pip install meerk40t
23
+ ```
24
+
25
+ Headless dependencies: `numpy`, `pyusb`, `pyserial`, `Pillow`, `ezdxf`,
26
+ `requests`, `websocket-client`.
27
+
28
+ ### 2. Install this CLI harness
29
+
30
+ ```bash
31
+ cd agent-harness
32
+ pip install -e .
33
+ ```
34
+
35
+ Verify:
36
+ ```bash
37
+ cli-anything-meerk40t --help
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ### One-shot commands
43
+
44
+ ```bash
45
+ # Create a new project and add elements
46
+ cli-anything-meerk40t project new
47
+ cli-anything-meerk40t elements circle 1in 1in 1in
48
+ cli-anything-meerk40t elements rect 2in 2in 1in 1in --stroke red --fill blue
49
+ cli-anything-meerk40t elements text 3in 3in "Hello"
50
+
51
+ # Persist to an SVG file (auto-saves after each mutation when -p is given)
52
+ cli-anything-meerk40t -p /tmp/job.svg elements circle 1in 1in 1in
53
+ cli-anything-meerk40t -p /tmp/job.svg elements list
54
+
55
+ # Operations
56
+ cli-anything-meerk40t operations list
57
+ cli-anything-meerk40t operations add cut
58
+ cli-anything-meerk40t operations classify
59
+
60
+ # Export via the real backend
61
+ cli-anything-meerk40t export svg /tmp/out.svg
62
+ cli-anything-meerk40t export svgz /tmp/out.svgz
63
+
64
+ # Device control
65
+ cli-anything-meerk40t device status
66
+ cli-anything-meerk40t device home
67
+
68
+ # Console passthrough (escape hatch to the raw MeerK40t console)
69
+ cli-anything-meerk40t console 'circle 2in 2in 1in'
70
+ cli-anything-meerk40t console 'service device start -i grbl'
71
+ ```
72
+
73
+ ### JSON output (for agents)
74
+
75
+ ```bash
76
+ cli-anything-meerk40t --json elements circle 1in 1in 1in
77
+ cli-anything-meerk40t --json elements list
78
+ cli-anything-meerk40t --json export svg /tmp/out.svg
79
+ ```
80
+
81
+ ### REPL (default when no subcommand)
82
+
83
+ ```bash
84
+ cli-anything-meerk40t
85
+ # Enter interactive REPL with banner, history, and help
86
+ ```
87
+
88
+ ### Session management
89
+
90
+ ```bash
91
+ cli-anything-meerk40t -s /tmp/session.json session status
92
+ cli-anything-meerk40t -s /tmp/session.json session undo
93
+ ```
94
+
95
+ ## Command groups
96
+
97
+ | Group | Description |
98
+ |---|---|
99
+ | `project` | New, open, save, info, close (SVG project files) |
100
+ | `elements` | Circle, rect, ellipse, line, polyline, text, list, delete, select, clear, frame |
101
+ | `operations` | List, add (cut/engrave/raster/image/dots), classify, declassify, set |
102
+ | `device` | List, status, home, physical-home, move, info |
103
+ | `export` | SVG, SVGZ (real backend); PNG (GUI-dependent); G-code (GRBL device required) |
104
+ | `console` | Raw passthrough to the MeerK40t kernel console |
105
+ | `session` | Undo, redo, history, status |
106
+ | `repl` | Interactive shell (default) |
107
+
108
+ ## Export formats
109
+
110
+ - **SVG** (default, plain, compressed/svgz) — truthful, rendered by the real
111
+ MeerK40t SVGWriter. Works headless.
112
+ - **G-code** — generated via the real GRBL `save_job` pipeline. Requires an
113
+ active GRBL device (`console 'service device start -i grbl'`).
114
+ - **PNG** — requires wxPython GUI (`render-op/make_raster` is only registered
115
+ by the GUI plugin). Errors clearly in headless mode.
116
+
117
+ ## How it works
118
+
119
+ The harness boots a headless MeerK40t `Kernel` instance (the same code path as
120
+ `meerk40t -z`) and drives it via `kernel.console()`. Channel output is captured
121
+ via `_console_channel.watch()`. All element/operation/export commands are
122
+ translated to real MeerK40t console commands — this is a wrapper, not a
123
+ reimplementation.
124
+
125
+ ## Testing
126
+
127
+ ```bash
128
+ cd agent-harness
129
+ CLI_ANYTHING_FORCE_INSTALLED=1 python -m pytest cli_anything/meerk40t/tests/ -v -s
130
+ ```
131
+
132
+ See `tests/TEST.md` for the test plan and results.
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
@@ -0,0 +1,4 @@
1
+ from cli_anything.meerk40t.meerk40t_cli import cli
2
+
3
+ if __name__ == "__main__":
4
+ cli()
@@ -0,0 +1,64 @@
1
+ import re
2
+
3
+ from cli_anything.meerk40t.utils.meerk40t_backend import Meerk40tBackend
4
+
5
+
6
+ def list_devices(backend):
7
+ out = backend.run("device")
8
+ lines = [l for l in out if l.strip() and not l.strip().startswith("device")]
9
+ return {"devices": lines}
10
+
11
+
12
+ def _parse_position(lines):
13
+ pos = None
14
+ for line in lines:
15
+ m = re.search(r"x=\s*([\d.\-+]+)\s*,?\s*y=\s*([\d.\-+]+)", line, re.IGNORECASE)
16
+ if m:
17
+ try:
18
+ pos = {"x": float(m.group(1)), "y": float(m.group(2))}
19
+ except ValueError:
20
+ pos = {"x": m.group(1), "y": m.group(2)}
21
+ break
22
+ m = re.search(r"position[:\s]+([\d.\-+]+)\s*,\s*([\d.\-+]+)", line, re.IGNORECASE)
23
+ if m:
24
+ try:
25
+ pos = {"x": float(m.group(1)), "y": float(m.group(2))}
26
+ except ValueError:
27
+ pos = {"x": m.group(1), "y": m.group(2)}
28
+ break
29
+ m = re.search(r"([\d.\-+]+)\s*,\s*([\d.\-+]+)", line)
30
+ if m and "position" in line.lower():
31
+ try:
32
+ pos = {"x": float(m.group(1)), "y": float(m.group(2))}
33
+ except ValueError:
34
+ pos = {"x": m.group(1), "y": m.group(2)}
35
+ break
36
+ return pos
37
+
38
+
39
+ def device_status(backend):
40
+ out = backend.run("devinfo")
41
+ pos = _parse_position(out)
42
+ return {"device": str(backend.device()), "position": pos}
43
+
44
+
45
+ def home(backend):
46
+ backend.run("home")
47
+ return {"homed": True}
48
+
49
+
50
+ def physical_home(backend):
51
+ backend.run("physical_home")
52
+ return {"physical_homed": True}
53
+
54
+
55
+ def move(backend, x, y, absolute=True):
56
+ cmd = f"move_absolute {x} {y}" if absolute else f"move {x} {y}"
57
+ backend.run(cmd)
58
+ return {"moved": True, "x": x, "y": y, "absolute": absolute}
59
+
60
+
61
+ def device_info(backend):
62
+ out = backend.run("devinfo")
63
+ pos = _parse_position(out)
64
+ return {"raw": out, "position": pos}
@@ -0,0 +1,193 @@
1
+ """Element CRUD operations — delegates to the real MeerK40t kernel console."""
2
+
3
+ from cli_anything.meerk40t.utils.meerk40t_backend import Meerk40tBackend
4
+
5
+
6
+ def _color_to_str(color):
7
+ if color is None:
8
+ return None
9
+ s = str(color)
10
+ return s if s and s.lower() != "none" else None
11
+
12
+
13
+ def _suffix(stroke, fill):
14
+ """Build the stroke/fill suffix in MeerK40t console syntax.
15
+
16
+ The console uses `stroke <color> fill <color>` inline (NOT -s/-f flags,
17
+ and NOT pipe-separated — `|` is the command separator).
18
+ """
19
+ parts = []
20
+ if stroke:
21
+ parts.append(f"stroke {stroke}")
22
+ if fill:
23
+ parts.append(f"fill {fill}")
24
+ return " " + " ".join(parts) if parts else ""
25
+
26
+
27
+ def _geom_for(node):
28
+ """Extract type-specific geometry attrs from a node as a dict."""
29
+ t = node.type
30
+ g = {}
31
+ if "rect" in t:
32
+ for k in ("x", "y", "width", "height"):
33
+ g[k] = getattr(node, k, None)
34
+ elif "ellipse" in t or "circle" in t:
35
+ for k in ("cx", "cy", "rx", "ry"):
36
+ g[k] = getattr(node, k, None)
37
+ elif "polyline" in t or "polygon" in t:
38
+ pts = getattr(node, "points", None)
39
+ if pts:
40
+ g["points"] = [
41
+ [_to_json_serializable(p[0]), _to_json_serializable(p[1])]
42
+ for p in pts
43
+ ]
44
+ else:
45
+ g["points"] = None
46
+ elif "line" in t:
47
+ for k in ("x1", "y1", "x2", "y2"):
48
+ g[k] = getattr(node, k, None)
49
+ elif "path" in t:
50
+ d = getattr(node, "d", None)
51
+ g["d"] = str(d)[:80] if d is not None else None
52
+ elif "text" in t:
53
+ txt = getattr(node, "text", None)
54
+ g["text"] = (txt[:80] if txt else None)
55
+ elif "image" in t:
56
+ for k in ("width", "height"):
57
+ g[k] = getattr(node, k, None)
58
+ return g
59
+
60
+
61
+ def _to_json_serializable(obj):
62
+ if obj is None:
63
+ return None
64
+ if hasattr(obj, "item"):
65
+ try:
66
+ return obj.item()
67
+ except Exception:
68
+ pass
69
+ if isinstance(obj, (list, tuple)):
70
+ return [_to_json_serializable(v) for v in obj]
71
+ try:
72
+ return float(obj)
73
+ except (TypeError, ValueError):
74
+ pass
75
+ return str(obj)
76
+
77
+
78
+ def _add(backend, cmd, kind):
79
+ """Run an add command and verify the element count actually increased."""
80
+ before = backend.elem_count()
81
+ backend.run(cmd)
82
+ after = backend.elem_count()
83
+ return {
84
+ "added": after > before,
85
+ "type": kind,
86
+ "total_elements": after,
87
+ }
88
+
89
+
90
+ def add_circle(backend, cx, cy, r, stroke=None, fill=None):
91
+ cmd = f"circle {cx} {cy} {r}" + _suffix(stroke, fill)
92
+ return _add(backend, cmd, "circle")
93
+
94
+
95
+ def add_rect(backend, x, y, w, h, stroke=None, fill=None):
96
+ cmd = f"rect {x} {y} {w} {h}" + _suffix(stroke, fill)
97
+ return _add(backend, cmd, "rect")
98
+
99
+
100
+ def add_ellipse(backend, cx, cy, rx, ry, stroke=None, fill=None):
101
+ cmd = f"ellipse {cx} {cy} {rx} {ry}" + _suffix(stroke, fill)
102
+ return _add(backend, cmd, "ellipse")
103
+
104
+
105
+ def add_line(backend, x1, y1, x2, y2, stroke=None, fill=None):
106
+ cmd = f"line {x1} {y1} {x2} {y2}" + _suffix(stroke, fill)
107
+ return _add(backend, cmd, "line")
108
+
109
+
110
+ def add_polyline(backend, points, stroke=None, fill=None):
111
+ flat = " ".join(str(coord) for pt in points for coord in pt)
112
+ cmd = f"polyline {flat}" + _suffix(stroke, fill)
113
+ return _add(backend, cmd, "polyline")
114
+
115
+
116
+ def add_text(backend, x, y, text):
117
+ """Create a text element at (x, y).
118
+
119
+ The MeerK40t `text` console command only takes the text string, so the
120
+ element is translated to the requested position after creation.
121
+ """
122
+ escaped = text.replace('"', '\\"')
123
+ before = backend.elem_count()
124
+ backend.run(f'text "{escaped}"')
125
+ after = backend.elem_count()
126
+ if after > before and (x is not None or y is not None):
127
+ try:
128
+ node = backend.elems()[-1]
129
+ from meerk40t.core.units import Length
130
+ matrix = node.matrix
131
+ dx = Length(x).native if x is not None else 0
132
+ dy = Length(y).native if y is not None else 0
133
+ matrix.post_translate(dx, dy)
134
+ node.matrix = matrix
135
+ node.altered()
136
+ except Exception:
137
+ pass
138
+ return {"added": after > before, "type": "text", "total_elements": after}
139
+
140
+
141
+ def list_elements(backend):
142
+ result = []
143
+ for node in backend.elems():
144
+ geom = _geom_for(node)
145
+ geom = {k: _to_json_serializable(v) for k, v in geom.items()}
146
+ result.append({
147
+ "index": len(result),
148
+ "id": getattr(node, "id", None),
149
+ "type": node.type,
150
+ "stroke": _color_to_str(getattr(node, "stroke", None)),
151
+ "fill": _color_to_str(getattr(node, "fill", None)),
152
+ "geometry": geom,
153
+ })
154
+ return result
155
+
156
+
157
+ def delete_element(backend, index):
158
+ before = backend.elem_count()
159
+ backend.run(f"element{index} delete")
160
+ after = backend.elem_count()
161
+ return {"deleted": after < before, "index": index, "total_elements": after}
162
+
163
+
164
+ def select_element(backend, index):
165
+ backend.run(f"element{index} select")
166
+ return {"selected": True, "index": index}
167
+
168
+
169
+ def clear_elements(backend):
170
+ backend.run("elements clear all")
171
+ backend.run("tree clear")
172
+ if backend.elem_count() > 0:
173
+ try:
174
+ backend.elements.clear_elements()
175
+ except Exception:
176
+ pass
177
+ if backend.elem_count() > 0:
178
+ for node in list(backend.elems()):
179
+ try:
180
+ node.remove_node()
181
+ except Exception:
182
+ try:
183
+ node.remove()
184
+ except Exception:
185
+ pass
186
+ return {"cleared": True, "total_elements": backend.elem_count()}
187
+
188
+
189
+ def frame(backend):
190
+ before = backend.elem_count()
191
+ backend.run("frame")
192
+ after = backend.elem_count()
193
+ return {"framed": after > before, "total_elements": after}
@@ -0,0 +1,76 @@
1
+ import os
2
+
3
+ from cli_anything.meerk40t.utils.meerk40t_backend import Meerk40tBackend
4
+
5
+
6
+ def export_svg(backend, path, version="default"):
7
+ backend.save_svg(path, version)
8
+ return {
9
+ "output": path,
10
+ "format": "svg",
11
+ "size_bytes": os.path.getsize(path),
12
+ "version": version,
13
+ }
14
+
15
+
16
+ def export_svgz(backend, path):
17
+ return export_svg(backend, path, version="compressed")
18
+
19
+
20
+ def export_png(backend, path, dpi=300):
21
+ renderer = backend.kernel.lookup("render-op/make_raster")
22
+ if renderer is None:
23
+ raise RuntimeError(
24
+ "PNG export requires wxPython GUI (render-op/make_raster not registered in headless mode). "
25
+ "Install wxPython and run with a display, or use SVG export instead."
26
+ )
27
+ backend.run(f"element* render -d {dpi}")
28
+ raise RuntimeError(
29
+ "PNG export to file path is not implemented for this renderer. "
30
+ "Use SVG export or run inside the GUI."
31
+ )
32
+
33
+
34
+ def export_gcode(backend, path):
35
+ """Best-effort G-code export via the GRBL plan/save_job pipeline.
36
+
37
+ Requires an active GRBL device. Uses the kernel's
38
+ `plan copy preprocess validate blob save_job <path>` pipeline, which the
39
+ GRBL plugin uses internally to write real G-code to a file. If the
40
+ pipeline is unavailable or produces no G-code (common in headless or
41
+ non-GRBL setups), this raises a RuntimeError with guidance.
42
+ """
43
+ dev = backend.device()
44
+ dev_name = str(dev).lower() if dev else ""
45
+ if "grbl" not in dev_name:
46
+ raise RuntimeError(
47
+ "G-code export requires an active GRBL device. "
48
+ "Activate one via the console passthrough, then retry. "
49
+ "G-code emission requires an active device + spooler execution."
50
+ )
51
+ abspath = os.path.realpath(path)
52
+ backend.run(f"plan copy preprocess validate blob save_job {abspath}")
53
+ if not os.path.exists(abspath) or os.path.getsize(abspath) == 0:
54
+ raise RuntimeError(
55
+ "G-code export produced no output. "
56
+ "Ensure there are classified operations with elements and the GRBL device is active. "
57
+ "Use the console passthrough for full control over the active device and spooler."
58
+ )
59
+ with open(abspath, "r", encoding="utf-8", errors="replace") as f:
60
+ sample = f.read(2048)
61
+ has_gcode = any(
62
+ line.strip().startswith(("G", "M", "g", "m"))
63
+ for line in sample.splitlines()
64
+ )
65
+ if not has_gcode:
66
+ raise RuntimeError(
67
+ "G-code export produced invalid output (no G/M codes found). "
68
+ "The file may contain log lines instead of real G-code. "
69
+ "Use the console passthrough for full control."
70
+ )
71
+ return {
72
+ "output": abspath,
73
+ "format": "gcode",
74
+ "size_bytes": os.path.getsize(abspath),
75
+ "valid": True,
76
+ }
@@ -0,0 +1,48 @@
1
+ from cli_anything.meerk40t.utils.meerk40t_backend import Meerk40tBackend
2
+
3
+
4
+ def list_operations(backend):
5
+ result = []
6
+ for node in backend.ops():
7
+ info = {
8
+ "id": getattr(node, "id", None),
9
+ "type": getattr(node, "type", None),
10
+ "label": getattr(node, "label", None),
11
+ "output": getattr(node, "output", None),
12
+ "speed": getattr(node, "speed", None),
13
+ "power": getattr(node, "power", None),
14
+ }
15
+ result.append(info)
16
+ return result
17
+
18
+
19
+ def add_operation(backend, op_type):
20
+ backend.run(op_type)
21
+ return {"added": True, "type": op_type, "total_ops": backend.op_count()}
22
+
23
+
24
+ def classify_elements(backend):
25
+ backend.run("element* classify")
26
+ return {"classified": True, "total_ops": backend.op_count()}
27
+
28
+
29
+ def declassify_elements(backend):
30
+ backend.run("element* declassify")
31
+ return {"declassified": True, "total_ops": backend.op_count()}
32
+
33
+
34
+ def set_operation(backend, index, key, value):
35
+ out = backend.run(f"op{index} {key} {value}")
36
+ failed = any(
37
+ phrase in line.lower()
38
+ for line in out
39
+ for phrase in ("unknown", "error", "not a registered command", "not registered")
40
+ )
41
+ if failed:
42
+ ops = backend.ops()
43
+ if 0 <= index < len(ops):
44
+ try:
45
+ setattr(ops[index], key, value)
46
+ except Exception:
47
+ pass
48
+ return {"set": True, "index": index, "key": key, "value": value}