meerk40t 0.9.7030__py2.py3-none-any.whl → 0.9.7050__py2.py3-none-any.whl
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.
- meerk40t/balormk/clone_loader.py +3 -2
- meerk40t/balormk/controller.py +38 -13
- meerk40t/balormk/cylindermod.py +1 -0
- meerk40t/balormk/device.py +13 -9
- meerk40t/balormk/driver.py +9 -2
- meerk40t/balormk/galvo_commands.py +3 -1
- meerk40t/balormk/gui/gui.py +6 -0
- meerk40t/balormk/livelightjob.py +338 -321
- meerk40t/balormk/mock_connection.py +4 -3
- meerk40t/balormk/usb_connection.py +11 -2
- meerk40t/camera/camera.py +19 -14
- meerk40t/camera/gui/camerapanel.py +6 -0
- meerk40t/core/cutplan.py +101 -78
- meerk40t/core/elements/element_treeops.py +435 -140
- meerk40t/core/elements/elements.py +100 -9
- meerk40t/core/elements/shapes.py +259 -72
- meerk40t/core/elements/tree_commands.py +10 -5
- meerk40t/core/node/blobnode.py +19 -4
- meerk40t/core/node/elem_ellipse.py +18 -8
- meerk40t/core/node/elem_image.py +51 -19
- meerk40t/core/node/elem_line.py +18 -8
- meerk40t/core/node/elem_path.py +18 -8
- meerk40t/core/node/elem_point.py +10 -4
- meerk40t/core/node/elem_polyline.py +19 -11
- meerk40t/core/node/elem_rect.py +18 -8
- meerk40t/core/node/elem_text.py +11 -5
- meerk40t/core/node/filenode.py +2 -8
- meerk40t/core/node/groupnode.py +11 -11
- meerk40t/core/node/image_processed.py +11 -5
- meerk40t/core/node/image_raster.py +11 -5
- meerk40t/core/node/node.py +64 -16
- meerk40t/core/node/refnode.py +2 -1
- meerk40t/core/planner.py +25 -11
- meerk40t/core/svg_io.py +91 -34
- meerk40t/device/dummydevice.py +7 -1
- meerk40t/extra/vtracer.py +222 -0
- meerk40t/grbl/device.py +96 -9
- meerk40t/grbl/driver.py +15 -5
- meerk40t/gui/about.py +20 -0
- meerk40t/gui/devicepanel.py +20 -16
- meerk40t/gui/gui_mixins.py +4 -0
- meerk40t/gui/icons.py +330 -253
- meerk40t/gui/laserpanel.py +27 -3
- meerk40t/gui/laserrender.py +41 -21
- meerk40t/gui/magnetoptions.py +158 -65
- meerk40t/gui/materialtest.py +569 -310
- meerk40t/gui/navigationpanels.py +229 -24
- meerk40t/gui/propertypanels/hatchproperty.py +2 -0
- meerk40t/gui/propertypanels/imageproperty.py +160 -106
- meerk40t/gui/propertypanels/wobbleproperty.py +6 -2
- meerk40t/gui/ribbon.py +6 -1
- meerk40t/gui/scenewidgets/gridwidget.py +29 -32
- meerk40t/gui/scenewidgets/rectselectwidget.py +190 -192
- meerk40t/gui/simulation.py +75 -77
- meerk40t/gui/spoolerpanel.py +27 -7
- meerk40t/gui/statusbarwidgets/defaultoperations.py +84 -48
- meerk40t/gui/statusbarwidgets/infowidget.py +2 -2
- meerk40t/gui/tips.py +15 -1
- meerk40t/gui/toolwidgets/toolpointmove.py +3 -1
- meerk40t/gui/wxmmain.py +242 -114
- meerk40t/gui/wxmscene.py +107 -24
- meerk40t/gui/wxmtree.py +4 -2
- meerk40t/gui/wxutils.py +286 -15
- meerk40t/image/imagetools.py +129 -65
- meerk40t/internal_plugins.py +4 -0
- meerk40t/kernel/kernel.py +67 -18
- meerk40t/kernel/settings.py +28 -9
- meerk40t/lihuiyu/device.py +24 -12
- meerk40t/main.py +14 -9
- meerk40t/moshi/device.py +20 -6
- meerk40t/network/console_server.py +22 -6
- meerk40t/newly/device.py +10 -3
- meerk40t/newly/gui/gui.py +10 -0
- meerk40t/ruida/device.py +22 -2
- meerk40t/ruida/loader.py +9 -4
- meerk40t/ruida/rdjob.py +48 -8
- meerk40t/tools/geomstr.py +240 -123
- meerk40t/tools/rasterplotter.py +185 -94
- {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7050.dist-info}/METADATA +1 -1
- {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7050.dist-info}/RECORD +85 -84
- {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7050.dist-info}/LICENSE +0 -0
- {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7050.dist-info}/WHEEL +0 -0
- {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7050.dist-info}/entry_points.txt +0 -0
- {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7050.dist-info}/top_level.txt +0 -0
- {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7050.dist-info}/zip-safe +0 -0
@@ -48,6 +48,7 @@ class MockConnection:
|
|
48
48
|
|
49
49
|
def write(self, index=0, packet=None):
|
50
50
|
from meerk40t.balormk.controller import GetSerialNo
|
51
|
+
|
51
52
|
packet_length = len(packet)
|
52
53
|
assert packet_length == 0xC or packet_length == 0xC00
|
53
54
|
if packet is not None:
|
@@ -88,6 +89,7 @@ class MockConnection:
|
|
88
89
|
|
89
90
|
def _parse_single(self, packet):
|
90
91
|
from meerk40t.balormk.controller import single_command_lookup
|
92
|
+
|
91
93
|
b0 = packet[1] << 8 | packet[0]
|
92
94
|
b1 = packet[3] << 8 | packet[2]
|
93
95
|
b2 = packet[5] << 8 | packet[4]
|
@@ -104,12 +106,11 @@ class MockConnection:
|
|
104
106
|
|
105
107
|
# Convert input to bytes early
|
106
108
|
if isinstance(data, str):
|
107
|
-
data = data.encode(
|
109
|
+
data = data.encode("ascii")
|
108
110
|
|
109
111
|
# Create fixed-size response with padding
|
110
112
|
self._implied_response = bytearray(8)
|
111
|
-
self._implied_response[:len(data)] = data[:8]
|
112
|
-
|
113
|
+
self._implied_response[: len(data)] = data[:8]
|
113
114
|
|
114
115
|
def read(self, index=0):
|
115
116
|
if self._implied_response is None:
|
@@ -232,10 +232,19 @@ class USBConnection:
|
|
232
232
|
except usb.core.NoBackendError as e:
|
233
233
|
self.channel(str(e))
|
234
234
|
from platform import system
|
235
|
+
|
235
236
|
osname = system()
|
236
237
|
if osname == "Windows":
|
237
|
-
self.channel(
|
238
|
-
|
238
|
+
self.channel(
|
239
|
+
_(
|
240
|
+
"Did you install the libusb driver via Zadig (https://zadig.akeo.ie/)?"
|
241
|
+
)
|
242
|
+
)
|
243
|
+
self.channel(
|
244
|
+
_(
|
245
|
+
"Consult the wiki: https://github.com/meerk40t/meerk40t/wiki/Install%3A-Windows"
|
246
|
+
)
|
247
|
+
)
|
239
248
|
self.channel(_("PyUsb detected no backend LibUSB driver."))
|
240
249
|
return -2
|
241
250
|
except ConnectionRefusedError:
|
meerk40t/camera/camera.py
CHANGED
@@ -189,32 +189,33 @@ class Camera(Service):
|
|
189
189
|
pass
|
190
190
|
return actual_width, actual_height
|
191
191
|
|
192
|
-
def _get_capture(self, set_resolution
|
192
|
+
def _get_capture(self, set_resolution=True):
|
193
193
|
import platform
|
194
|
+
|
194
195
|
# print (self.uri, type(self.uri).__name__)
|
195
196
|
if platform.system() == "Windows":
|
196
197
|
self.logger("Set DSHOW for Windows")
|
197
198
|
cv2.CAP_DSHOW
|
198
|
-
#sets the Windows cv2 backend to DSHOW (Direct Video Input Show)
|
199
|
+
# sets the Windows cv2 backend to DSHOW (Direct Video Input Show)
|
199
200
|
cap = cv2.VideoCapture(self.uri)
|
200
201
|
elif platform.system() == "Linux":
|
201
202
|
self.logger("Set GSTREAMER for Linux")
|
202
|
-
cv2.CAP_GSTREAMER
|
203
|
-
#cv2.CAP_V4L
|
203
|
+
cv2.CAP_GSTREAMER # set the Linux cv2 backend to GTREAMER
|
204
|
+
# cv2.CAP_V4L
|
204
205
|
cap = cv2.VideoCapture(self.uri)
|
205
206
|
else:
|
206
207
|
self.logger("Try something for Darwin")
|
207
208
|
cap = cv2.VideoCapture(self.uri)
|
208
209
|
# For MAC please refer to link below for I/O
|
209
|
-
cap.set(cv2.CAP_FFMPEG, cv2.CAP_AVFOUNDATION)
|
210
|
-
#please refer to reference link at bottom of page for more I/O
|
210
|
+
cap.set(cv2.CAP_FFMPEG, cv2.CAP_AVFOUNDATION) # not sure!
|
211
|
+
# please refer to reference link at bottom of page for more I/O
|
211
212
|
if set_resolution:
|
212
|
-
self.logger
|
213
|
+
self.logger(f"Try to start camera with {self.width}x{self.height}")
|
213
214
|
cap.set(cv2.CAP_PROP_FRAME_WIDTH, self.width)
|
214
215
|
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, self.height)
|
215
|
-
self.logger
|
216
|
-
f"Capture: {str(self.capture)}\n"
|
217
|
-
f"Frame resolution set to: ({cap.get(cv2.CAP_PROP_FRAME_WIDTH)}x{cap.get(cv2.CAP_PROP_FRAME_HEIGHT)})"
|
216
|
+
self.logger(
|
217
|
+
f"Capture: {str(self.capture)}\n"
|
218
|
+
+ f"Frame resolution set to: ({cap.get(cv2.CAP_PROP_FRAME_WIDTH)}x{cap.get(cv2.CAP_PROP_FRAME_HEIGHT)})"
|
218
219
|
)
|
219
220
|
|
220
221
|
return cap
|
@@ -297,14 +298,18 @@ class Camera(Service):
|
|
297
298
|
actual_height = 0
|
298
299
|
actual_width = 0
|
299
300
|
msg = f"(Fail: {e})"
|
300
|
-
self.logger(
|
301
|
+
self.logger(
|
302
|
+
f"Tried {width}x{height} ({description}) - received {actual_width}x{actual_height} {msg}"
|
303
|
+
)
|
301
304
|
if int(actual_width) == width and int(actual_height) == height:
|
302
305
|
supported_resolutions.append((width, height, description))
|
303
|
-
|
304
|
-
|
306
|
+
try:
|
307
|
+
# Might crash if the camera is not opened
|
308
|
+
cap.release()
|
309
|
+
except cv2.error:
|
310
|
+
pass
|
305
311
|
return supported_resolutions
|
306
312
|
|
307
|
-
|
308
313
|
def process_frame(self):
|
309
314
|
frame = self._current_raw
|
310
315
|
if (
|
@@ -947,35 +947,41 @@ class CameraInterface(MWindow):
|
|
947
947
|
"label": _("Camera {index}").format(index=0),
|
948
948
|
"action": camera_click(0),
|
949
949
|
"signal": "camset0",
|
950
|
+
"multi_autoexec": True,
|
950
951
|
},
|
951
952
|
{
|
952
953
|
"identifier": "cam1",
|
953
954
|
"label": _("Camera {index}").format(index=1),
|
954
955
|
"action": camera_click(1),
|
955
956
|
"signal": "camset1",
|
957
|
+
"multi_autoexec": True,
|
956
958
|
},
|
957
959
|
{
|
958
960
|
"identifier": "cam2",
|
959
961
|
"label": _("Camera {index}").format(index=2),
|
960
962
|
"action": camera_click(2),
|
961
963
|
"signal": "camset2",
|
964
|
+
"multi_autoexec": True,
|
962
965
|
},
|
963
966
|
{
|
964
967
|
"identifier": "cam3",
|
965
968
|
"label": _("Camera {index}").format(index=3),
|
966
969
|
"action": camera_click(3),
|
967
970
|
"signal": "camset3",
|
971
|
+
"multi_autoexec": True,
|
968
972
|
},
|
969
973
|
{
|
970
974
|
"identifier": "cam4",
|
971
975
|
"label": _("Camera {index}").format(index=4),
|
972
976
|
"action": camera_click(4),
|
973
977
|
"signal": "camset4",
|
978
|
+
"multi_autoexec": True,
|
974
979
|
},
|
975
980
|
{
|
976
981
|
"identifier": "id_cam",
|
977
982
|
"label": _("Identify cameras"),
|
978
983
|
"action": detect_usb_cameras,
|
984
|
+
"multi_autoexec": True,
|
979
985
|
},
|
980
986
|
],
|
981
987
|
},
|
meerk40t/core/cutplan.py
CHANGED
@@ -15,15 +15,16 @@ CutPlan handles the various complicated algorithms to optimising the sequence of
|
|
15
15
|
"""
|
16
16
|
|
17
17
|
from copy import copy
|
18
|
+
from functools import lru_cache
|
18
19
|
from math import isinf
|
19
20
|
from os import times
|
20
21
|
from time import perf_counter, time
|
21
22
|
from typing import Optional
|
22
|
-
|
23
|
+
|
23
24
|
import numpy as np
|
24
25
|
|
25
26
|
from ..svgelements import Group, Matrix, Path, Polygon
|
26
|
-
from ..tools.geomstr import Geomstr, stitch_geometries
|
27
|
+
from ..tools.geomstr import Geomstr, stitch_geometries, stitcheable_nodes
|
27
28
|
from ..tools.pathtools import VectorMontonizer
|
28
29
|
from .cutcode.cutcode import CutCode
|
29
30
|
from .cutcode.cutgroup import CutGroup
|
@@ -31,7 +32,8 @@ from .cutcode.cutobject import CutObject
|
|
31
32
|
from .cutcode.rastercut import RasterCut
|
32
33
|
from .node.node import Node
|
33
34
|
from .node.util_console import ConsoleOperation
|
34
|
-
from .units import
|
35
|
+
from .units import UNITS_PER_MM, Length
|
36
|
+
|
35
37
|
"""
|
36
38
|
The time to compile does outweigh the benefit...
|
37
39
|
try:
|
@@ -46,6 +48,7 @@ except Exception as e:
|
|
46
48
|
return inner
|
47
49
|
"""
|
48
50
|
|
51
|
+
|
49
52
|
class CutPlanningFailedError(Exception):
|
50
53
|
pass
|
51
54
|
|
@@ -171,7 +174,9 @@ class CutPlan:
|
|
171
174
|
for place in self.plan:
|
172
175
|
if not hasattr(place, "type"):
|
173
176
|
continue
|
174
|
-
if place.type.startswith("place ") and (
|
177
|
+
if place.type.startswith("place ") and (
|
178
|
+
hasattr(place, "output") and place.output
|
179
|
+
):
|
175
180
|
loops = 1
|
176
181
|
if hasattr(place, "loops") and place.loops > 1:
|
177
182
|
loops = place.loops
|
@@ -225,7 +230,9 @@ class CutPlan:
|
|
225
230
|
coolop = ConsoleOperation(command=cmd)
|
226
231
|
self.plan.append(coolop)
|
227
232
|
else:
|
228
|
-
self.channel(
|
233
|
+
self.channel(
|
234
|
+
"The current device does not support a coolant method"
|
235
|
+
)
|
229
236
|
current_cool = cool
|
230
237
|
# Is there already a coolant operation?
|
231
238
|
if getattr(original_op, "type", "") == "util console":
|
@@ -244,45 +251,20 @@ class CutPlan:
|
|
244
251
|
op_type = getattr(op, "type", "")
|
245
252
|
if op_type.startswith("place "):
|
246
253
|
continue
|
247
|
-
if
|
254
|
+
if (
|
255
|
+
op_type == "op cut"
|
256
|
+
and self.context.opt_stitching
|
257
|
+
and self.context.do_optimization
|
258
|
+
):
|
248
259
|
# This isn't a lossless operation: dotted/dashed lines will be treated as solid lines
|
249
260
|
try:
|
250
|
-
stitch_tolerance = float(
|
261
|
+
stitch_tolerance = float(
|
262
|
+
Length(self.context.opt_stitch_tolerance)
|
263
|
+
)
|
251
264
|
except ValueError:
|
252
265
|
stitch_tolerance = 0
|
253
266
|
default_stroke = None
|
254
267
|
default_strokewidth = None
|
255
|
-
|
256
|
-
def stitcheable_nodes(data, tolerance) -> list:
|
257
|
-
out = []
|
258
|
-
geoms = []
|
259
|
-
# Store all geometries together with an indicator, to which node they belong
|
260
|
-
for idx, node in enumerate(data):
|
261
|
-
if not hasattr(node, "as_geometry"):
|
262
|
-
continue
|
263
|
-
for g1 in node.as_geometry().as_contiguous():
|
264
|
-
geoms.append((idx, g1))
|
265
|
-
for idx1, (nodeidx1, g1) in enumerate(geoms):
|
266
|
-
for idx2 in range(idx1 + 1, len(geoms)):
|
267
|
-
nodeidx2 = geoms[idx2][0]
|
268
|
-
g2 = geoms[idx2][1]
|
269
|
-
fp1 = g1.first_point
|
270
|
-
fp2 = g2.first_point
|
271
|
-
lp1 = g1.last_point
|
272
|
-
lp2 = g2.last_point
|
273
|
-
if (
|
274
|
-
abs(lp1 - lp2) <= tolerance or
|
275
|
-
abs(lp1 - fp2) <= tolerance or
|
276
|
-
abs(fp1 - fp2) <= tolerance or
|
277
|
-
abs(fp1 - lp2) <= tolerance
|
278
|
-
):
|
279
|
-
if nodeidx1 not in out:
|
280
|
-
out.append(nodeidx1)
|
281
|
-
if nodeidx2 not in out:
|
282
|
-
out.append(nodeidx2)
|
283
|
-
|
284
|
-
return [data[idx] for idx in out]
|
285
|
-
|
286
268
|
geoms = []
|
287
269
|
to_be_deleted = []
|
288
270
|
data = stitcheable_nodes(list(op.flat()), stitch_tolerance)
|
@@ -290,11 +272,13 @@ class CutPlan:
|
|
290
272
|
if node is op:
|
291
273
|
continue
|
292
274
|
if hasattr(node, "as_geometry"):
|
293
|
-
geom
|
275
|
+
geom: Geomstr = node.as_geometry()
|
294
276
|
geoms.extend(iter(geom.as_contiguous()))
|
295
277
|
if default_stroke is None and hasattr(node, "stroke"):
|
296
278
|
default_stroke = node.stroke
|
297
|
-
if default_strokewidth is None and hasattr(
|
279
|
+
if default_strokewidth is None and hasattr(
|
280
|
+
node, "stroke_width"
|
281
|
+
):
|
298
282
|
default_strokewidth = node.stroke_width
|
299
283
|
to_be_deleted.append(node)
|
300
284
|
result = stitch_geometries(geoms, stitch_tolerance)
|
@@ -313,7 +297,9 @@ class CutPlan:
|
|
313
297
|
# print (f"Paths at start of action: {len(list(op.flat()))}")
|
314
298
|
|
315
299
|
self.plan.append(op)
|
316
|
-
if (op_type.startswith("op") or op_type.startswith("util")) and hasattr(
|
300
|
+
if (op_type.startswith("op") or op_type.startswith("util")) and hasattr(
|
301
|
+
op, "preprocess"
|
302
|
+
):
|
317
303
|
op.preprocess(self.context, placement, self)
|
318
304
|
if op_type.startswith("op"):
|
319
305
|
for node in op.flat():
|
@@ -523,11 +509,15 @@ class CutPlan:
|
|
523
509
|
"""
|
524
510
|
if not isinstance(last_item, CutCode):
|
525
511
|
# The last plan item is not cutcode, merge is only between cutobjects adding to cutcode.
|
526
|
-
self.channel
|
512
|
+
self.channel(
|
513
|
+
f"last_item is no cutcode ({type(last_item).__name__}), can't merge"
|
514
|
+
)
|
527
515
|
return False
|
528
516
|
if not isinstance(current_item, CutObject):
|
529
517
|
# The object to be merged is not a cutObject and cannot be added to Cutcode.
|
530
|
-
self.channel
|
518
|
+
self.channel(
|
519
|
+
f"current_item is no cutcode ({type(current_item).__name__}), can't merge"
|
520
|
+
)
|
531
521
|
return False
|
532
522
|
last_op = last_item.original_op
|
533
523
|
if last_op is None:
|
@@ -536,7 +526,9 @@ class CutPlan:
|
|
536
526
|
if current_op is None:
|
537
527
|
current_op = ""
|
538
528
|
if last_op.startswith("util") or current_op.startswith("util"):
|
539
|
-
self.channel
|
529
|
+
self.channel(
|
530
|
+
f"{last_op} / {current_op} - at least one is a util operation, can't merge"
|
531
|
+
)
|
540
532
|
return False
|
541
533
|
|
542
534
|
if (
|
@@ -544,7 +536,9 @@ class CutPlan:
|
|
544
536
|
and last_item.pass_index != current_item.pass_index
|
545
537
|
):
|
546
538
|
# Do not merge if opt_merge_passes is off, and pass_index do not match
|
547
|
-
self.channel
|
539
|
+
self.channel(
|
540
|
+
f"{last_item.pass_index} / {current_item.pass_index} - pass indices are different, can't merge"
|
541
|
+
)
|
548
542
|
return False
|
549
543
|
|
550
544
|
if (
|
@@ -553,11 +547,15 @@ class CutPlan:
|
|
553
547
|
):
|
554
548
|
# Do not merge if opt_merge_ops is off, and the original ops do not match
|
555
549
|
# Same settings object implies same original operation
|
556
|
-
self.channel
|
550
|
+
self.channel(
|
551
|
+
f"Settings do differ from {last_op} to {current_op} and merge ops= {context.opt_merge_ops}"
|
552
|
+
)
|
557
553
|
return False
|
558
554
|
if not context.opt_inner_first and last_item.original_op == "op cut":
|
559
555
|
# Do not merge if opt_inner_first is off, and operation was originally a cut.
|
560
|
-
self.channel
|
556
|
+
self.channel(
|
557
|
+
f"Inner first {context.opt_inner_first}, last op= {last_item.original_op} - Last op was a cut, can't merge"
|
558
|
+
)
|
561
559
|
return False
|
562
560
|
return True # No reason these should not be merged.
|
563
561
|
|
@@ -773,7 +771,9 @@ class CutPlan:
|
|
773
771
|
# We don't combine across plan boundaries
|
774
772
|
if not isinstance(pitem, CutGroup):
|
775
773
|
continue
|
776
|
-
grouping, to_be_deleted, item_combined, total = process_plan_item(
|
774
|
+
grouping, to_be_deleted, item_combined, total = process_plan_item(
|
775
|
+
pitem, busy, total, plan_idx, l_plan
|
776
|
+
)
|
777
777
|
combined += item_combined
|
778
778
|
group_count += len(grouping)
|
779
779
|
|
@@ -1164,7 +1164,9 @@ def is_inside(inner, outer, tolerance=0, resolution=50):
|
|
1164
1164
|
return False
|
1165
1165
|
return True
|
1166
1166
|
|
1167
|
-
def scanbeam_code_not_working_reliably(
|
1167
|
+
def scanbeam_code_not_working_reliably(
|
1168
|
+
outer_cut, outer_path, inner_cut, inner_path
|
1169
|
+
):
|
1168
1170
|
from ..tools.geomstr import Polygon as Gpoly
|
1169
1171
|
from ..tools.geomstr import Scanbeam
|
1170
1172
|
|
@@ -1195,24 +1197,25 @@ def is_inside(inner, outer, tolerance=0, resolution=50):
|
|
1195
1197
|
|
1196
1198
|
p1x, p1y = poly[0]
|
1197
1199
|
old_sq_dist = sq_length(p1x - x, p1y - y)
|
1198
|
-
for i in range(n+1):
|
1200
|
+
for i in range(n + 1):
|
1199
1201
|
p2x, p2y = poly[i % n]
|
1200
1202
|
new_sq_dist = sq_length(p2x - x, p2y - y)
|
1201
1203
|
# We are approximating the edge to an extremely thin ellipse and see
|
1202
1204
|
# whether our point is on that ellipse
|
1203
1205
|
reldist = (
|
1204
|
-
old_sq_dist
|
1205
|
-
|
1206
|
-
|
1206
|
+
old_sq_dist
|
1207
|
+
+ new_sq_dist
|
1208
|
+
+ 2.0 * np.sqrt(old_sq_dist * new_sq_dist)
|
1209
|
+
- sq_length(p2x - p1x, p2y - p1y)
|
1207
1210
|
)
|
1208
1211
|
if reldist < tolerance_square:
|
1209
1212
|
return True
|
1210
1213
|
|
1211
|
-
if y > min(p1y,p2y):
|
1212
|
-
if y <= max(p1y,p2y):
|
1213
|
-
if x <= max(p1x,p2x):
|
1214
|
+
if y > min(p1y, p2y):
|
1215
|
+
if y <= max(p1y, p2y):
|
1216
|
+
if x <= max(p1x, p2x):
|
1214
1217
|
if p1y != p2y:
|
1215
|
-
xints = (y-p1y)*(p2x-p1x)/(p2y-p1y)+p1x
|
1218
|
+
xints = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
|
1216
1219
|
if p1x == p2x or x <= xints:
|
1217
1220
|
inside = not inside
|
1218
1221
|
p1x, p1y = p2x, p2y
|
@@ -1231,20 +1234,20 @@ def is_inside(inner, outer, tolerance=0, resolution=50):
|
|
1231
1234
|
|
1232
1235
|
# Separating Axis Theorem (SAT) for Polygon Containment
|
1233
1236
|
|
1234
|
-
# The Separating Axis Theorem (SAT) is a powerful technique for collision detection
|
1237
|
+
# The Separating Axis Theorem (SAT) is a powerful technique for collision detection
|
1235
1238
|
# between convex polygons. It can also be adapted to determine polygon containment.
|
1236
1239
|
|
1237
1240
|
# How SAT Works:
|
1238
1241
|
|
1239
1242
|
# Generate Axes: For each edge of the outer polygon, create a perpendicular axis.
|
1240
1243
|
# Project Polygons: Project both the inner and outer polygons onto each axis.
|
1241
|
-
# Check Overlap: If the projections of the inner polygon are completely contained
|
1242
|
-
# within the projections of the outer polygon on all axes,
|
1244
|
+
# Check Overlap: If the projections of the inner polygon are completely contained
|
1245
|
+
# within the projections of the outer polygon on all axes,
|
1243
1246
|
# then the inner polygon is fully contained.
|
1244
|
-
|
1245
|
-
# Convex Polygons: SAT is most efficient for convex polygons.
|
1247
|
+
|
1248
|
+
# Convex Polygons: SAT is most efficient for convex polygons.
|
1246
1249
|
# For concave polygons, you might need to decompose them into convex sub-polygons.
|
1247
|
-
# Computational Cost: SAT can be computationally expensive for large numbers of polygons.
|
1250
|
+
# Computational Cost: SAT can be computationally expensive for large numbers of polygons.
|
1248
1251
|
# In such cases, spatial indexing can be used to reduce the number of pairwise comparisons.
|
1249
1252
|
def project_polygon(polygon, axis):
|
1250
1253
|
# Projects a polygon onto a given axis.
|
@@ -1280,7 +1283,6 @@ def is_inside(inner, outer, tolerance=0, resolution=50):
|
|
1280
1283
|
"""
|
1281
1284
|
|
1282
1285
|
def raycasting_code_new(outer_polygon, inner_polygon):
|
1283
|
-
|
1284
1286
|
def precompute_intersections(polygon):
|
1285
1287
|
slopes = []
|
1286
1288
|
intercepts = []
|
@@ -1309,9 +1311,13 @@ def is_inside(inner, outer, tolerance=0, resolution=50):
|
|
1309
1311
|
slope = slopes[i]
|
1310
1312
|
intercept = intercepts[i]
|
1311
1313
|
p1, p2 = polygon[i], polygon[(i + 1) % len(polygon)]
|
1312
|
-
|
1314
|
+
|
1313
1315
|
if np.isnan(slope): # Vertical line
|
1314
|
-
if
|
1316
|
+
if (
|
1317
|
+
x == intercept
|
1318
|
+
and y >= min(p1[1], p2[1])
|
1319
|
+
and y <= max(p1[1], p2[1])
|
1320
|
+
):
|
1315
1321
|
inside = not inside
|
1316
1322
|
else:
|
1317
1323
|
if y > min(p1[1], p2[1]):
|
@@ -1321,17 +1327,21 @@ def is_inside(inner, outer, tolerance=0, resolution=50):
|
|
1321
1327
|
xints = (y - intercept) / slope
|
1322
1328
|
if p1[0] == p2[0] or x <= xints:
|
1323
1329
|
inside = not inside
|
1324
|
-
|
1330
|
+
|
1325
1331
|
return inside
|
1326
|
-
|
1332
|
+
|
1327
1333
|
def is_polygon_inside(outer_polygon, inner_polygon):
|
1328
1334
|
slopes, intercepts, is_vertical = precompute_intersections(outer_polygon)
|
1329
1335
|
for point in inner_polygon:
|
1330
|
-
if not point_in_polygon(
|
1336
|
+
if not point_in_polygon(
|
1337
|
+
point[0], point[1], slopes, intercepts, is_vertical, outer_polygon
|
1338
|
+
):
|
1331
1339
|
return False
|
1332
1340
|
return True
|
1333
1341
|
|
1334
|
-
return is_polygon_inside(
|
1342
|
+
return is_polygon_inside(
|
1343
|
+
outer_polygon=outer_polygon, inner_polygon=inner_polygon
|
1344
|
+
)
|
1335
1345
|
|
1336
1346
|
def shapely_code(outer_polygon, inner_polygon):
|
1337
1347
|
from shapely.geometry import Polygon
|
@@ -1341,15 +1351,18 @@ def is_inside(inner, outer, tolerance=0, resolution=50):
|
|
1341
1351
|
poly_b = Polygon(outer_polygon)
|
1342
1352
|
# Check for containment
|
1343
1353
|
return poly_a.within(poly_b)
|
1344
|
-
|
1354
|
+
|
1345
1355
|
@lru_cache(maxsize=128)
|
1346
1356
|
def get_polygon(path, resolution):
|
1347
1357
|
geom = Geomstr.svg(path)
|
1348
1358
|
polygon = np.array(
|
1349
|
-
list(
|
1359
|
+
list(
|
1360
|
+
(p.real, p.imag)
|
1361
|
+
for p in geom.as_equal_interpolated_points(distance=resolution)
|
1362
|
+
)
|
1350
1363
|
)
|
1351
1364
|
return polygon
|
1352
|
-
|
1365
|
+
|
1353
1366
|
"""
|
1354
1367
|
# Testroutines
|
1355
1368
|
from time import perf_counter
|
@@ -1372,13 +1385,14 @@ def is_inside(inner, outer, tolerance=0, resolution=50):
|
|
1372
1385
|
t5 = perf_counter()
|
1373
1386
|
except ImportError:
|
1374
1387
|
res4 = "Shapely missing"
|
1375
|
-
t5 = t4
|
1388
|
+
t5 = t4
|
1376
1389
|
print (f"Tolerance: {tolerance}, vm={res0} in {t1 - t0:.3f}s, sb={res1} in {t1 - t0:.3f}s, ray-old={res2} in {t2 - t1:.3f}s, ray-new={res3} in {t3 - t2:.3f}s, shapely={res4} in {t4 - t3:.3f}s")
|
1377
1390
|
"""
|
1378
1391
|
inner_polygon = get_polygon(inner_path.d(), resolution)
|
1379
|
-
outer_polygon = get_polygon(outer_path.d(), resolution)
|
1392
|
+
outer_polygon = get_polygon(outer_path.d(), resolution)
|
1380
1393
|
try:
|
1381
1394
|
import shapely
|
1395
|
+
|
1382
1396
|
return shapely_code(outer_polygon, inner_polygon)
|
1383
1397
|
except ImportError:
|
1384
1398
|
return vm_code(outer, outer_polygon, inner, inner_polygon)
|
@@ -1387,6 +1401,7 @@ def is_inside(inner, outer, tolerance=0, resolution=50):
|
|
1387
1401
|
# return vm_code(outer, outer_path, inner, inner_path)
|
1388
1402
|
return
|
1389
1403
|
|
1404
|
+
|
1390
1405
|
def reify_matrix(self):
|
1391
1406
|
"""Apply the matrix to the path and reset matrix."""
|
1392
1407
|
self.element = abs(self.element)
|
@@ -1466,13 +1481,15 @@ def inner_first_ident(context: CutGroup, kernel=None, channel=None, tolerance=0)
|
|
1466
1481
|
if kernel:
|
1467
1482
|
busy = kernel.busyinfo
|
1468
1483
|
_ = kernel.translation
|
1469
|
-
min_res = min(
|
1484
|
+
min_res = min(
|
1485
|
+
kernel.device.view.native_scale_x, kernel.device.view.native_scale_y
|
1486
|
+
)
|
1470
1487
|
# a 0.5 mm resolution is enough
|
1471
1488
|
resolution = int(0.5 * UNITS_PER_MM / min_res)
|
1472
1489
|
# print(f"Chosen resolution: {resolution} - minscale = {min_res}")
|
1473
1490
|
else:
|
1474
1491
|
busy = None
|
1475
|
-
resolution = 10
|
1492
|
+
resolution = 10
|
1476
1493
|
for outer in closed_groups:
|
1477
1494
|
for inner in groups:
|
1478
1495
|
current_pass += 1
|
@@ -1695,7 +1712,13 @@ def short_travel_cutcode(
|
|
1695
1712
|
for idx, c in enumerate(unordered):
|
1696
1713
|
if isinstance(c, CutGroup):
|
1697
1714
|
c.skip = False
|
1698
|
-
unordered[idx] = short_travel_cutcode(
|
1715
|
+
unordered[idx] = short_travel_cutcode(
|
1716
|
+
context=c,
|
1717
|
+
kernel=kernel,
|
1718
|
+
complete_path=False,
|
1719
|
+
grouped_inner=False,
|
1720
|
+
channel=channel,
|
1721
|
+
)
|
1699
1722
|
# As these are reversed, we reverse again...
|
1700
1723
|
ordered.extend(reversed(unordered))
|
1701
1724
|
# print (f"And after extension {len(ordered)} items in list")
|