meerk40t 0.9.7030__py2.py3-none-any.whl → 0.9.7040__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 +28 -11
- 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 +109 -51
- meerk40t/core/elements/element_treeops.py +435 -140
- meerk40t/core/elements/elements.py +100 -9
- meerk40t/core/elements/shapes.py +259 -39
- meerk40t/core/elements/tree_commands.py +10 -5
- 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/svg_io.py +91 -34
- meerk40t/device/dummydevice.py +7 -1
- meerk40t/extra/vtracer.py +222 -0
- meerk40t/grbl/device.py +81 -8
- 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 +8 -3
- meerk40t/gui/laserrender.py +41 -21
- meerk40t/gui/magnetoptions.py +158 -65
- meerk40t/gui/materialtest.py +229 -39
- meerk40t/gui/navigationpanels.py +229 -24
- meerk40t/gui/propertypanels/hatchproperty.py +2 -0
- meerk40t/gui/propertypanels/imageproperty.py +160 -106
- 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/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 +60 -15
- meerk40t/image/imagetools.py +129 -65
- meerk40t/internal_plugins.py +4 -0
- meerk40t/kernel/kernel.py +39 -18
- meerk40t/kernel/settings.py +28 -9
- meerk40t/lihuiyu/device.py +24 -12
- meerk40t/main.py +1 -1
- 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 +6 -3
- meerk40t/tools/geomstr.py +193 -125
- meerk40t/tools/rasterplotter.py +179 -93
- {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7040.dist-info}/METADATA +1 -1
- {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7040.dist-info}/RECORD +79 -78
- {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7040.dist-info}/LICENSE +0 -0
- {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7040.dist-info}/WHEEL +0 -0
- {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7040.dist-info}/entry_points.txt +0 -0
- {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7040.dist-info}/top_level.txt +0 -0
- {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7040.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,11 +15,12 @@ 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
|
@@ -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,10 +251,16 @@ 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
|
@@ -270,11 +283,15 @@ class CutPlan:
|
|
270
283
|
fp2 = g2.first_point
|
271
284
|
lp1 = g1.last_point
|
272
285
|
lp2 = g2.last_point
|
286
|
+
if fp1 is None or fp2 is None:
|
287
|
+
continue
|
288
|
+
if lp1 is None or lp2 is None:
|
289
|
+
continue
|
273
290
|
if (
|
274
|
-
abs(lp1 - lp2) <= tolerance
|
275
|
-
abs(lp1 - fp2) <= tolerance
|
276
|
-
abs(fp1 - fp2) <= tolerance
|
277
|
-
abs(fp1 - lp2) <= tolerance
|
291
|
+
abs(lp1 - lp2) <= tolerance
|
292
|
+
or abs(lp1 - fp2) <= tolerance
|
293
|
+
or abs(fp1 - fp2) <= tolerance
|
294
|
+
or abs(fp1 - lp2) <= tolerance
|
278
295
|
):
|
279
296
|
if nodeidx1 not in out:
|
280
297
|
out.append(nodeidx1)
|
@@ -282,7 +299,7 @@ class CutPlan:
|
|
282
299
|
out.append(nodeidx2)
|
283
300
|
|
284
301
|
return [data[idx] for idx in out]
|
285
|
-
|
302
|
+
|
286
303
|
geoms = []
|
287
304
|
to_be_deleted = []
|
288
305
|
data = stitcheable_nodes(list(op.flat()), stitch_tolerance)
|
@@ -290,11 +307,13 @@ class CutPlan:
|
|
290
307
|
if node is op:
|
291
308
|
continue
|
292
309
|
if hasattr(node, "as_geometry"):
|
293
|
-
geom
|
310
|
+
geom: Geomstr = node.as_geometry()
|
294
311
|
geoms.extend(iter(geom.as_contiguous()))
|
295
312
|
if default_stroke is None and hasattr(node, "stroke"):
|
296
313
|
default_stroke = node.stroke
|
297
|
-
if default_strokewidth is None and hasattr(
|
314
|
+
if default_strokewidth is None and hasattr(
|
315
|
+
node, "stroke_width"
|
316
|
+
):
|
298
317
|
default_strokewidth = node.stroke_width
|
299
318
|
to_be_deleted.append(node)
|
300
319
|
result = stitch_geometries(geoms, stitch_tolerance)
|
@@ -313,7 +332,9 @@ class CutPlan:
|
|
313
332
|
# print (f"Paths at start of action: {len(list(op.flat()))}")
|
314
333
|
|
315
334
|
self.plan.append(op)
|
316
|
-
if (op_type.startswith("op") or op_type.startswith("util")) and hasattr(
|
335
|
+
if (op_type.startswith("op") or op_type.startswith("util")) and hasattr(
|
336
|
+
op, "preprocess"
|
337
|
+
):
|
317
338
|
op.preprocess(self.context, placement, self)
|
318
339
|
if op_type.startswith("op"):
|
319
340
|
for node in op.flat():
|
@@ -523,11 +544,15 @@ class CutPlan:
|
|
523
544
|
"""
|
524
545
|
if not isinstance(last_item, CutCode):
|
525
546
|
# The last plan item is not cutcode, merge is only between cutobjects adding to cutcode.
|
526
|
-
self.channel
|
547
|
+
self.channel(
|
548
|
+
f"last_item is no cutcode ({type(last_item).__name__}), can't merge"
|
549
|
+
)
|
527
550
|
return False
|
528
551
|
if not isinstance(current_item, CutObject):
|
529
552
|
# The object to be merged is not a cutObject and cannot be added to Cutcode.
|
530
|
-
self.channel
|
553
|
+
self.channel(
|
554
|
+
f"current_item is no cutcode ({type(current_item).__name__}), can't merge"
|
555
|
+
)
|
531
556
|
return False
|
532
557
|
last_op = last_item.original_op
|
533
558
|
if last_op is None:
|
@@ -536,7 +561,9 @@ class CutPlan:
|
|
536
561
|
if current_op is None:
|
537
562
|
current_op = ""
|
538
563
|
if last_op.startswith("util") or current_op.startswith("util"):
|
539
|
-
self.channel
|
564
|
+
self.channel(
|
565
|
+
f"{last_op} / {current_op} - at least one is a util operation, can't merge"
|
566
|
+
)
|
540
567
|
return False
|
541
568
|
|
542
569
|
if (
|
@@ -544,7 +571,9 @@ class CutPlan:
|
|
544
571
|
and last_item.pass_index != current_item.pass_index
|
545
572
|
):
|
546
573
|
# Do not merge if opt_merge_passes is off, and pass_index do not match
|
547
|
-
self.channel
|
574
|
+
self.channel(
|
575
|
+
f"{last_item.pass_index} / {current_item.pass_index} - pass indices are different, can't merge"
|
576
|
+
)
|
548
577
|
return False
|
549
578
|
|
550
579
|
if (
|
@@ -553,11 +582,15 @@ class CutPlan:
|
|
553
582
|
):
|
554
583
|
# Do not merge if opt_merge_ops is off, and the original ops do not match
|
555
584
|
# Same settings object implies same original operation
|
556
|
-
self.channel
|
585
|
+
self.channel(
|
586
|
+
f"Settings do differ from {last_op} to {current_op} and merge ops= {context.opt_merge_ops}"
|
587
|
+
)
|
557
588
|
return False
|
558
589
|
if not context.opt_inner_first and last_item.original_op == "op cut":
|
559
590
|
# Do not merge if opt_inner_first is off, and operation was originally a cut.
|
560
|
-
self.channel
|
591
|
+
self.channel(
|
592
|
+
f"Inner first {context.opt_inner_first}, last op= {last_item.original_op} - Last op was a cut, can't merge"
|
593
|
+
)
|
561
594
|
return False
|
562
595
|
return True # No reason these should not be merged.
|
563
596
|
|
@@ -773,7 +806,9 @@ class CutPlan:
|
|
773
806
|
# We don't combine across plan boundaries
|
774
807
|
if not isinstance(pitem, CutGroup):
|
775
808
|
continue
|
776
|
-
grouping, to_be_deleted, item_combined, total = process_plan_item(
|
809
|
+
grouping, to_be_deleted, item_combined, total = process_plan_item(
|
810
|
+
pitem, busy, total, plan_idx, l_plan
|
811
|
+
)
|
777
812
|
combined += item_combined
|
778
813
|
group_count += len(grouping)
|
779
814
|
|
@@ -1164,7 +1199,9 @@ def is_inside(inner, outer, tolerance=0, resolution=50):
|
|
1164
1199
|
return False
|
1165
1200
|
return True
|
1166
1201
|
|
1167
|
-
def scanbeam_code_not_working_reliably(
|
1202
|
+
def scanbeam_code_not_working_reliably(
|
1203
|
+
outer_cut, outer_path, inner_cut, inner_path
|
1204
|
+
):
|
1168
1205
|
from ..tools.geomstr import Polygon as Gpoly
|
1169
1206
|
from ..tools.geomstr import Scanbeam
|
1170
1207
|
|
@@ -1195,24 +1232,25 @@ def is_inside(inner, outer, tolerance=0, resolution=50):
|
|
1195
1232
|
|
1196
1233
|
p1x, p1y = poly[0]
|
1197
1234
|
old_sq_dist = sq_length(p1x - x, p1y - y)
|
1198
|
-
for i in range(n+1):
|
1235
|
+
for i in range(n + 1):
|
1199
1236
|
p2x, p2y = poly[i % n]
|
1200
1237
|
new_sq_dist = sq_length(p2x - x, p2y - y)
|
1201
1238
|
# We are approximating the edge to an extremely thin ellipse and see
|
1202
1239
|
# whether our point is on that ellipse
|
1203
1240
|
reldist = (
|
1204
|
-
old_sq_dist
|
1205
|
-
|
1206
|
-
|
1241
|
+
old_sq_dist
|
1242
|
+
+ new_sq_dist
|
1243
|
+
+ 2.0 * np.sqrt(old_sq_dist * new_sq_dist)
|
1244
|
+
- sq_length(p2x - p1x, p2y - p1y)
|
1207
1245
|
)
|
1208
1246
|
if reldist < tolerance_square:
|
1209
1247
|
return True
|
1210
1248
|
|
1211
|
-
if y > min(p1y,p2y):
|
1212
|
-
if y <= max(p1y,p2y):
|
1213
|
-
if x <= max(p1x,p2x):
|
1249
|
+
if y > min(p1y, p2y):
|
1250
|
+
if y <= max(p1y, p2y):
|
1251
|
+
if x <= max(p1x, p2x):
|
1214
1252
|
if p1y != p2y:
|
1215
|
-
xints = (y-p1y)*(p2x-p1x)/(p2y-p1y)+p1x
|
1253
|
+
xints = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
|
1216
1254
|
if p1x == p2x or x <= xints:
|
1217
1255
|
inside = not inside
|
1218
1256
|
p1x, p1y = p2x, p2y
|
@@ -1231,20 +1269,20 @@ def is_inside(inner, outer, tolerance=0, resolution=50):
|
|
1231
1269
|
|
1232
1270
|
# Separating Axis Theorem (SAT) for Polygon Containment
|
1233
1271
|
|
1234
|
-
# The Separating Axis Theorem (SAT) is a powerful technique for collision detection
|
1272
|
+
# The Separating Axis Theorem (SAT) is a powerful technique for collision detection
|
1235
1273
|
# between convex polygons. It can also be adapted to determine polygon containment.
|
1236
1274
|
|
1237
1275
|
# How SAT Works:
|
1238
1276
|
|
1239
1277
|
# Generate Axes: For each edge of the outer polygon, create a perpendicular axis.
|
1240
1278
|
# 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,
|
1279
|
+
# Check Overlap: If the projections of the inner polygon are completely contained
|
1280
|
+
# within the projections of the outer polygon on all axes,
|
1243
1281
|
# then the inner polygon is fully contained.
|
1244
|
-
|
1245
|
-
# Convex Polygons: SAT is most efficient for convex polygons.
|
1282
|
+
|
1283
|
+
# Convex Polygons: SAT is most efficient for convex polygons.
|
1246
1284
|
# 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.
|
1285
|
+
# Computational Cost: SAT can be computationally expensive for large numbers of polygons.
|
1248
1286
|
# In such cases, spatial indexing can be used to reduce the number of pairwise comparisons.
|
1249
1287
|
def project_polygon(polygon, axis):
|
1250
1288
|
# Projects a polygon onto a given axis.
|
@@ -1280,7 +1318,6 @@ def is_inside(inner, outer, tolerance=0, resolution=50):
|
|
1280
1318
|
"""
|
1281
1319
|
|
1282
1320
|
def raycasting_code_new(outer_polygon, inner_polygon):
|
1283
|
-
|
1284
1321
|
def precompute_intersections(polygon):
|
1285
1322
|
slopes = []
|
1286
1323
|
intercepts = []
|
@@ -1309,9 +1346,13 @@ def is_inside(inner, outer, tolerance=0, resolution=50):
|
|
1309
1346
|
slope = slopes[i]
|
1310
1347
|
intercept = intercepts[i]
|
1311
1348
|
p1, p2 = polygon[i], polygon[(i + 1) % len(polygon)]
|
1312
|
-
|
1349
|
+
|
1313
1350
|
if np.isnan(slope): # Vertical line
|
1314
|
-
if
|
1351
|
+
if (
|
1352
|
+
x == intercept
|
1353
|
+
and y >= min(p1[1], p2[1])
|
1354
|
+
and y <= max(p1[1], p2[1])
|
1355
|
+
):
|
1315
1356
|
inside = not inside
|
1316
1357
|
else:
|
1317
1358
|
if y > min(p1[1], p2[1]):
|
@@ -1321,17 +1362,21 @@ def is_inside(inner, outer, tolerance=0, resolution=50):
|
|
1321
1362
|
xints = (y - intercept) / slope
|
1322
1363
|
if p1[0] == p2[0] or x <= xints:
|
1323
1364
|
inside = not inside
|
1324
|
-
|
1365
|
+
|
1325
1366
|
return inside
|
1326
|
-
|
1367
|
+
|
1327
1368
|
def is_polygon_inside(outer_polygon, inner_polygon):
|
1328
1369
|
slopes, intercepts, is_vertical = precompute_intersections(outer_polygon)
|
1329
1370
|
for point in inner_polygon:
|
1330
|
-
if not point_in_polygon(
|
1371
|
+
if not point_in_polygon(
|
1372
|
+
point[0], point[1], slopes, intercepts, is_vertical, outer_polygon
|
1373
|
+
):
|
1331
1374
|
return False
|
1332
1375
|
return True
|
1333
1376
|
|
1334
|
-
return is_polygon_inside(
|
1377
|
+
return is_polygon_inside(
|
1378
|
+
outer_polygon=outer_polygon, inner_polygon=inner_polygon
|
1379
|
+
)
|
1335
1380
|
|
1336
1381
|
def shapely_code(outer_polygon, inner_polygon):
|
1337
1382
|
from shapely.geometry import Polygon
|
@@ -1341,15 +1386,18 @@ def is_inside(inner, outer, tolerance=0, resolution=50):
|
|
1341
1386
|
poly_b = Polygon(outer_polygon)
|
1342
1387
|
# Check for containment
|
1343
1388
|
return poly_a.within(poly_b)
|
1344
|
-
|
1389
|
+
|
1345
1390
|
@lru_cache(maxsize=128)
|
1346
1391
|
def get_polygon(path, resolution):
|
1347
1392
|
geom = Geomstr.svg(path)
|
1348
1393
|
polygon = np.array(
|
1349
|
-
list(
|
1394
|
+
list(
|
1395
|
+
(p.real, p.imag)
|
1396
|
+
for p in geom.as_equal_interpolated_points(distance=resolution)
|
1397
|
+
)
|
1350
1398
|
)
|
1351
1399
|
return polygon
|
1352
|
-
|
1400
|
+
|
1353
1401
|
"""
|
1354
1402
|
# Testroutines
|
1355
1403
|
from time import perf_counter
|
@@ -1372,13 +1420,14 @@ def is_inside(inner, outer, tolerance=0, resolution=50):
|
|
1372
1420
|
t5 = perf_counter()
|
1373
1421
|
except ImportError:
|
1374
1422
|
res4 = "Shapely missing"
|
1375
|
-
t5 = t4
|
1423
|
+
t5 = t4
|
1376
1424
|
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
1425
|
"""
|
1378
1426
|
inner_polygon = get_polygon(inner_path.d(), resolution)
|
1379
|
-
outer_polygon = get_polygon(outer_path.d(), resolution)
|
1427
|
+
outer_polygon = get_polygon(outer_path.d(), resolution)
|
1380
1428
|
try:
|
1381
1429
|
import shapely
|
1430
|
+
|
1382
1431
|
return shapely_code(outer_polygon, inner_polygon)
|
1383
1432
|
except ImportError:
|
1384
1433
|
return vm_code(outer, outer_polygon, inner, inner_polygon)
|
@@ -1387,6 +1436,7 @@ def is_inside(inner, outer, tolerance=0, resolution=50):
|
|
1387
1436
|
# return vm_code(outer, outer_path, inner, inner_path)
|
1388
1437
|
return
|
1389
1438
|
|
1439
|
+
|
1390
1440
|
def reify_matrix(self):
|
1391
1441
|
"""Apply the matrix to the path and reset matrix."""
|
1392
1442
|
self.element = abs(self.element)
|
@@ -1466,13 +1516,15 @@ def inner_first_ident(context: CutGroup, kernel=None, channel=None, tolerance=0)
|
|
1466
1516
|
if kernel:
|
1467
1517
|
busy = kernel.busyinfo
|
1468
1518
|
_ = kernel.translation
|
1469
|
-
min_res = min(
|
1519
|
+
min_res = min(
|
1520
|
+
kernel.device.view.native_scale_x, kernel.device.view.native_scale_y
|
1521
|
+
)
|
1470
1522
|
# a 0.5 mm resolution is enough
|
1471
1523
|
resolution = int(0.5 * UNITS_PER_MM / min_res)
|
1472
1524
|
# print(f"Chosen resolution: {resolution} - minscale = {min_res}")
|
1473
1525
|
else:
|
1474
1526
|
busy = None
|
1475
|
-
resolution = 10
|
1527
|
+
resolution = 10
|
1476
1528
|
for outer in closed_groups:
|
1477
1529
|
for inner in groups:
|
1478
1530
|
current_pass += 1
|
@@ -1695,7 +1747,13 @@ def short_travel_cutcode(
|
|
1695
1747
|
for idx, c in enumerate(unordered):
|
1696
1748
|
if isinstance(c, CutGroup):
|
1697
1749
|
c.skip = False
|
1698
|
-
unordered[idx] = short_travel_cutcode(
|
1750
|
+
unordered[idx] = short_travel_cutcode(
|
1751
|
+
context=c,
|
1752
|
+
kernel=kernel,
|
1753
|
+
complete_path=False,
|
1754
|
+
grouped_inner=False,
|
1755
|
+
channel=channel,
|
1756
|
+
)
|
1699
1757
|
# As these are reversed, we reverse again...
|
1700
1758
|
ordered.extend(reversed(unordered))
|
1701
1759
|
# print (f"And after extension {len(ordered)} items in list")
|