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.
- cli_anything_meerk40t-1.0.0/LICENSE +21 -0
- cli_anything_meerk40t-1.0.0/PKG-INFO +38 -0
- cli_anything_meerk40t-1.0.0/README.md +77 -0
- cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/README.md +132 -0
- cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/__init__.py +1 -0
- cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/__main__.py +4 -0
- cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/core/__init__.py +0 -0
- cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/core/device.py +64 -0
- cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/core/elements.py +193 -0
- cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/core/export.py +76 -0
- cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/core/operations.py +48 -0
- cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/core/project.py +82 -0
- cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/core/session.py +88 -0
- cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/meerk40t_cli.py +836 -0
- cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/skills/SKILL.md +153 -0
- cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/tests/__init__.py +0 -0
- cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/tests/test_core.py +275 -0
- cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/tests/test_full_e2e.py +223 -0
- cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/utils/__init__.py +0 -0
- cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/utils/meerk40t_backend.py +237 -0
- cli_anything_meerk40t-1.0.0/cli_anything/meerk40t/utils/repl_skin.py +567 -0
- cli_anything_meerk40t-1.0.0/cli_anything_meerk40t.egg-info/PKG-INFO +38 -0
- cli_anything_meerk40t-1.0.0/cli_anything_meerk40t.egg-info/SOURCES.txt +27 -0
- cli_anything_meerk40t-1.0.0/cli_anything_meerk40t.egg-info/dependency_links.txt +1 -0
- cli_anything_meerk40t-1.0.0/cli_anything_meerk40t.egg-info/entry_points.txt +2 -0
- cli_anything_meerk40t-1.0.0/cli_anything_meerk40t.egg-info/requires.txt +10 -0
- cli_anything_meerk40t-1.0.0/cli_anything_meerk40t.egg-info/top_level.txt +1 -0
- cli_anything_meerk40t-1.0.0/setup.cfg +4 -0
- 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"
|
|
File without changes
|
|
@@ -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}
|