meerk40t 0.9.7010__py2.py3-none-any.whl → 0.9.7030__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.
Files changed (77) hide show
  1. meerk40t/balormk/galvo_commands.py +1 -2
  2. meerk40t/core/cutcode/cutcode.py +1 -1
  3. meerk40t/core/cutplan.py +70 -2
  4. meerk40t/core/elements/branches.py +18 -4
  5. meerk40t/core/elements/element_treeops.py +43 -7
  6. meerk40t/core/elements/elements.py +49 -63
  7. meerk40t/core/elements/grid.py +8 -1
  8. meerk40t/core/elements/offset_clpr.py +4 -3
  9. meerk40t/core/elements/offset_mk.py +2 -1
  10. meerk40t/core/elements/shapes.py +379 -260
  11. meerk40t/core/elements/testcases.py +105 -0
  12. meerk40t/core/node/node.py +6 -3
  13. meerk40t/core/node/op_cut.py +9 -8
  14. meerk40t/core/node/op_dots.py +8 -8
  15. meerk40t/core/node/op_engrave.py +7 -7
  16. meerk40t/core/node/op_raster.py +8 -8
  17. meerk40t/core/planner.py +23 -0
  18. meerk40t/core/undos.py +1 -1
  19. meerk40t/core/wordlist.py +1 -0
  20. meerk40t/dxf/dxf_io.py +6 -0
  21. meerk40t/extra/encode_detect.py +8 -2
  22. meerk40t/extra/hershey.py +2 -3
  23. meerk40t/extra/inkscape.py +3 -5
  24. meerk40t/extra/mk_potrace.py +1959 -0
  25. meerk40t/extra/outerworld.py +2 -3
  26. meerk40t/extra/param_functions.py +2 -2
  27. meerk40t/extra/potrace.py +14 -10
  28. meerk40t/grbl/device.py +4 -1
  29. meerk40t/grbl/gui/grblcontroller.py +2 -2
  30. meerk40t/grbl/interpreter.py +1 -1
  31. meerk40t/gui/about.py +3 -5
  32. meerk40t/gui/basicops.py +3 -3
  33. meerk40t/gui/busy.py +75 -13
  34. meerk40t/gui/choicepropertypanel.py +365 -379
  35. meerk40t/gui/consolepanel.py +3 -3
  36. meerk40t/gui/gui_mixins.py +4 -1
  37. meerk40t/gui/hersheymanager.py +13 -3
  38. meerk40t/gui/laserpanel.py +12 -7
  39. meerk40t/gui/materialmanager.py +33 -6
  40. meerk40t/gui/plugin.py +9 -3
  41. meerk40t/gui/propertypanels/operationpropertymain.py +1 -1
  42. meerk40t/gui/ribbon.py +4 -1
  43. meerk40t/gui/scene/widget.py +1 -1
  44. meerk40t/gui/scenewidgets/rectselectwidget.py +19 -16
  45. meerk40t/gui/scenewidgets/selectionwidget.py +26 -20
  46. meerk40t/gui/simpleui.py +13 -8
  47. meerk40t/gui/simulation.py +22 -2
  48. meerk40t/gui/spoolerpanel.py +8 -11
  49. meerk40t/gui/themes.py +7 -1
  50. meerk40t/gui/tips.py +2 -3
  51. meerk40t/gui/toolwidgets/toolmeasure.py +4 -1
  52. meerk40t/gui/wxmeerk40t.py +32 -3
  53. meerk40t/gui/wxmmain.py +72 -6
  54. meerk40t/gui/wxmscene.py +95 -6
  55. meerk40t/gui/wxmtree.py +17 -11
  56. meerk40t/gui/wxutils.py +1 -1
  57. meerk40t/image/imagetools.py +21 -6
  58. meerk40t/kernel/kernel.py +31 -6
  59. meerk40t/kernel/settings.py +2 -0
  60. meerk40t/lihuiyu/device.py +9 -3
  61. meerk40t/main.py +22 -5
  62. meerk40t/network/console_server.py +52 -14
  63. meerk40t/network/web_server.py +15 -1
  64. meerk40t/ruida/device.py +5 -1
  65. meerk40t/ruida/gui/gui.py +6 -6
  66. meerk40t/ruida/gui/ruidaoperationproperties.py +1 -10
  67. meerk40t/ruida/rdjob.py +3 -3
  68. meerk40t/tools/geomstr.py +88 -0
  69. meerk40t/tools/polybool.py +2 -1
  70. meerk40t/tools/shxparser.py +92 -34
  71. {meerk40t-0.9.7010.dist-info → meerk40t-0.9.7030.dist-info}/METADATA +1 -1
  72. {meerk40t-0.9.7010.dist-info → meerk40t-0.9.7030.dist-info}/RECORD +77 -75
  73. {meerk40t-0.9.7010.dist-info → meerk40t-0.9.7030.dist-info}/WHEEL +1 -1
  74. {meerk40t-0.9.7010.dist-info → meerk40t-0.9.7030.dist-info}/LICENSE +0 -0
  75. {meerk40t-0.9.7010.dist-info → meerk40t-0.9.7030.dist-info}/entry_points.txt +0 -0
  76. {meerk40t-0.9.7010.dist-info → meerk40t-0.9.7030.dist-info}/top_level.txt +0 -0
  77. {meerk40t-0.9.7010.dist-info → meerk40t-0.9.7030.dist-info}/zip-safe +0 -0
@@ -1153,7 +1153,6 @@ def plugin(service, lifecycle):
1153
1153
  import platform
1154
1154
 
1155
1155
  from meerk40t.balormk.clone_loader import load_sys
1156
- from meerk40t.kernel import get_safe_path
1157
1156
 
1158
1157
  kernel = service.kernel
1159
1158
 
@@ -1173,7 +1172,7 @@ def plugin(service, lifecycle):
1173
1172
  return
1174
1173
 
1175
1174
  # Check for file in the meerk40t directory (safe_path)
1176
- directory = get_safe_path(kernel.name, create=True)
1175
+ directory = kernel.os_information["WORKDIR"]
1177
1176
  p = os.path.join(directory, service.clone_sys)
1178
1177
  if os.path.exists(p):
1179
1178
  load_sys(p, channel=channel)
@@ -132,7 +132,7 @@ class CutCode(CutGroup):
132
132
  length_of_previous_travel = Point.distance(prev.end, current.start)
133
133
  total_distance_travel += length_of_previous_travel
134
134
  rapid_speed = self._native_speed(cutcode)
135
- if rapid_speed is not None:
135
+ if rapid_speed is not None and rapid_speed != 0:
136
136
  total_duration_travel = total_distance_travel / rapid_speed
137
137
  duration_of_this_travel = length_of_previous_travel / rapid_speed
138
138
 
meerk40t/core/cutplan.py CHANGED
@@ -23,7 +23,7 @@ from functools import lru_cache
23
23
  import numpy as np
24
24
 
25
25
  from ..svgelements import Group, Matrix, Path, Polygon
26
- from ..tools.geomstr import Geomstr
26
+ from ..tools.geomstr import Geomstr, stitch_geometries
27
27
  from ..tools.pathtools import VectorMontonizer
28
28
  from .cutcode.cutcode import CutCode
29
29
  from .cutcode.cutgroup import CutGroup
@@ -32,7 +32,6 @@ from .cutcode.rastercut import RasterCut
32
32
  from .node.node import Node
33
33
  from .node.util_console import ConsoleOperation
34
34
  from .units import Length, UNITS_PER_MM
35
-
36
35
  """
37
36
  The time to compile does outweigh the benefit...
38
37
  try:
@@ -187,6 +186,7 @@ class CutPlan:
187
186
  placements.append(scene_to_device_matrix)
188
187
 
189
188
  original_ops = copy(self.plan)
189
+
190
190
  if self.context.opt_raster_optimisation and self.context.do_optimization:
191
191
  try:
192
192
  margin = float(Length(self.context.opt_raster_opt_margin, "0"))
@@ -244,6 +244,74 @@ class CutPlan:
244
244
  op_type = getattr(op, "type", "")
245
245
  if op_type.startswith("place "):
246
246
  continue
247
+ if op_type == "op cut" and self.context.opt_stitching and self.context.do_optimization:
248
+ # This isn't a lossless operation: dotted/dashed lines will be treated as solid lines
249
+ try:
250
+ stitch_tolerance = float(Length(self.context.opt_stitch_tolerance))
251
+ except ValueError:
252
+ stitch_tolerance = 0
253
+ default_stroke = None
254
+ 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
+ geoms = []
287
+ to_be_deleted = []
288
+ data = stitcheable_nodes(list(op.flat()), stitch_tolerance)
289
+ for node in data:
290
+ if node is op:
291
+ continue
292
+ if hasattr(node, "as_geometry"):
293
+ geom : Geomstr = node.as_geometry()
294
+ geoms.extend(iter(geom.as_contiguous()))
295
+ if default_stroke is None and hasattr(node, "stroke"):
296
+ default_stroke = node.stroke
297
+ if default_strokewidth is None and hasattr(node, "stroke_width"):
298
+ default_strokewidth = node.stroke_width
299
+ to_be_deleted.append(node)
300
+ result = stitch_geometries(geoms, stitch_tolerance)
301
+ if result is not None:
302
+ # print (f"Paths at start of action: {len(list(op.flat()))}")
303
+ for node in to_be_deleted:
304
+ node.remove_node()
305
+ for idx, g in enumerate(result):
306
+ node = op.add(
307
+ label=f"Stitch # {idx + 1}",
308
+ stroke=default_stroke,
309
+ stroke_width=default_strokewidth,
310
+ geometry=g,
311
+ type="elem path",
312
+ )
313
+ # print (f"Paths at start of action: {len(list(op.flat()))}")
314
+
247
315
  self.plan.append(op)
248
316
  if (op_type.startswith("op") or op_type.startswith("util")) and hasattr(op, "preprocess"):
249
317
  op.preprocess(self.context, placement, self)
@@ -187,7 +187,7 @@ def init_commands(kernel):
187
187
  return
188
188
  try:
189
189
  channel(_("loading..."))
190
- result = self.load(new_file)
190
+ result = self.load(new_file, svg_ppi=self.svg_ppi)
191
191
  if result:
192
192
  channel(_("Done."))
193
193
  except AttributeError:
@@ -1410,6 +1410,20 @@ def init_commands(kernel):
1410
1410
  self.remove_operations(data)
1411
1411
  self.signal("update_group_labels")
1412
1412
 
1413
+ @self.console_command(
1414
+ "clear_all", help=_("Clear all content"), input_type=("elements", "ops")
1415
+ )
1416
+ def e_clear(command, channel, _, data=None, data_type=None, **kwargs):
1417
+ channel(_("Deleting…"))
1418
+ fast = True
1419
+ with self.undoscope("Deleting"):
1420
+ if data_type == "elements":
1421
+ self.clear_elements(fast=fast)
1422
+ self.emphasized()
1423
+ else:
1424
+ self.clear_operations(fast=fast)
1425
+ self.signal("rebuild_tree", "all")
1426
+
1413
1427
  # ==========
1414
1428
  # ELEMENT BASE
1415
1429
  # ==========
@@ -1490,7 +1504,7 @@ def init_commands(kernel):
1490
1504
  else:
1491
1505
  target = self.reg_branch
1492
1506
  scope = "Elements -> Regmarks"
1493
-
1507
+
1494
1508
  with self.undoscope(scope):
1495
1509
  if data is None:
1496
1510
  data = list()
@@ -1826,8 +1840,8 @@ def init_commands(kernel):
1826
1840
  subpath.ensure_proper_subpaths()
1827
1841
  idx += 1
1828
1842
  subnode = group_node.add(
1829
- geometry=subpath,
1830
- type="elem path",
1843
+ geometry=subpath,
1844
+ type="elem path",
1831
1845
  label=f"{node_label}-{idx}",
1832
1846
  stroke=node_attributes.get("stroke", None),
1833
1847
  fill=node_attributes.get("fill", None),
@@ -2866,13 +2866,15 @@ def init_tree(kernel):
2866
2866
  dots_per_units = node.dpi / UNITS_PER_INCH
2867
2867
  new_width = width * dots_per_units
2868
2868
  new_height = height * dots_per_units
2869
-
2870
- image = make_raster(
2871
- data,
2872
- bounds=bounds,
2873
- width=new_width,
2874
- height=new_height,
2875
- )
2869
+ try:
2870
+ image = make_raster(
2871
+ data,
2872
+ bounds=bounds,
2873
+ width=new_width,
2874
+ height=new_height,
2875
+ )
2876
+ except Exception:
2877
+ return None, None
2876
2878
  matrix = Matrix.scale(width / new_width, height / new_height)
2877
2879
  matrix.post_translate(bounds[0], bounds[1])
2878
2880
  return image, matrix
@@ -3570,6 +3572,40 @@ def init_tree(kernel):
3570
3572
  with self.undoscope("Outline"):
3571
3573
  self(f"outline {offset}mm\n")
3572
3574
  self.signal("refresh_tree")
3575
+
3576
+ @tree_conditional(
3577
+ lambda node: not is_regmark(node)
3578
+ and hasattr(node, "as_geometry")
3579
+ )
3580
+ @tree_submenu(_("Offset shapes..."))
3581
+ @tree_iterate("offset", 1, 5)
3582
+ @tree_operation(
3583
+ _("...to outside with {offset}mm distance"),
3584
+ node_type=elem_nodes,
3585
+ help=_("Create an outer offset around the selected elements"),
3586
+ grouping="50_ELEM_MODIFY_ZMISC"
3587
+ )
3588
+ def make_positive_offsets(node, offset=1, **kwargs):
3589
+ with self.undoscope("Offset"):
3590
+ self(f"offset {offset}mm\n")
3591
+ self.signal("refresh_tree")
3592
+
3593
+ @tree_conditional(
3594
+ lambda node: not is_regmark(node)
3595
+ and hasattr(node, "as_geometry")
3596
+ )
3597
+ @tree_submenu(_("Offset shapes..."))
3598
+ @tree_iterate("offset", 1, 5)
3599
+ @tree_operation(
3600
+ _("...to inside with {offset}mm distance"),
3601
+ node_type=elem_nodes,
3602
+ help=_("Create an inner offset around the selected elements"),
3603
+ grouping="50_ELEM_MODIFY_ZMISC"
3604
+ )
3605
+ def make_negative_offsets(node, offset=1, **kwargs):
3606
+ with self.undoscope("Offset"):
3607
+ self(f"offset -{offset}mm\n")
3608
+ self.signal("refresh_tree")
3573
3609
 
3574
3610
  def mergeable(node):
3575
3611
  elems = list(self.elems(emphasized=True))
@@ -75,6 +75,7 @@ def plugin(kernel, lifecycle=None):
75
75
  tree_commands,
76
76
  undo_redo,
77
77
  wordlist,
78
+ testcases,
78
79
  )
79
80
 
80
81
  return [
@@ -96,6 +97,7 @@ def plugin(kernel, lifecycle=None):
96
97
  placements.plugin,
97
98
  offset_mk.plugin,
98
99
  offset_clpr.plugin,
100
+ testcases.plugin,
99
101
  ]
100
102
  elif lifecycle == "preregister":
101
103
  kernel.register(
@@ -2699,6 +2701,13 @@ class Elemental(Service):
2699
2701
  if elements is None:
2700
2702
  return
2701
2703
  new_operations_added = False
2704
+ debug_set = {}
2705
+
2706
+ def update_debug_set(debug_set, opnode):
2707
+ if opnode.type not in debug_set:
2708
+ debug_set[opnode.type] = 0
2709
+ debug_set[opnode.type] = debug_set[opnode.type] + 1
2710
+
2702
2711
 
2703
2712
  if len(list(self.ops())) == 0 and not self.operation_default_empty:
2704
2713
  has_cut = False
@@ -2732,6 +2741,13 @@ class Elemental(Service):
2732
2741
  add_op_function = self.add_classify_op
2733
2742
  for node in elements:
2734
2743
  node_desc = f"[{node.type}]{'' if node.id is None else node.id + '-'}{'<none>' if node.label is None else node.display_label()}"
2744
+ if hasattr(node, "stroke") or hasattr(node, "fill"):
2745
+ info = ""
2746
+ if hasattr(node, "stroke") and node.stroke is not None:
2747
+ info += f"S:{node.stroke},"
2748
+ if hasattr(node, "fill") and node.fill is not None:
2749
+ info += f"F:{node.fill},"
2750
+ node_desc += f"({info})"
2735
2751
  # Following lines added to handle 0.7 special ops added to ops list
2736
2752
  if hasattr(node, "operation"):
2737
2753
  add_op_function(node)
@@ -2758,7 +2774,7 @@ class Elemental(Service):
2758
2774
  if not do_fill and op.type in ("op raster", "op image"):
2759
2775
  continue
2760
2776
  is_black = False
2761
- whisperer = True
2777
+ perform_classification = True
2762
2778
  if (
2763
2779
  hasattr(node, "stroke")
2764
2780
  and node.stroke is not None
@@ -2780,18 +2796,18 @@ class Elemental(Service):
2780
2796
  and is_black
2781
2797
  and isinstance(op, RasterOpNode)
2782
2798
  ):
2783
- whisperer = False
2799
+ perform_classification = False
2784
2800
  elif (
2785
2801
  self.classify_black_as_raster
2786
2802
  and is_black
2787
2803
  and isinstance(op, EngraveOpNode)
2788
2804
  ):
2789
- whisperer = False
2805
+ perform_classification = False
2790
2806
  if debug:
2791
2807
  debug(
2792
- f"For {op.type}.{op.id}: black={is_black}, perform={whisperer}, flag={self.classify_black_as_raster}"
2808
+ f"For {op.type}.{op.id}: black={is_black}, perform={perform_classification}, flag={self.classify_black_as_raster}"
2793
2809
  )
2794
- if hasattr(op, "classify") and whisperer:
2810
+ if hasattr(op, "classify") and perform_classification:
2795
2811
  classified, should_break, feedback = op.classify(
2796
2812
  node,
2797
2813
  fuzzy=tempfuzzy,
@@ -2801,6 +2817,7 @@ class Elemental(Service):
2801
2817
  else:
2802
2818
  continue
2803
2819
  if classified:
2820
+ update_debug_set(debug_set, op)
2804
2821
  if feedback is not None and "stroke" in feedback:
2805
2822
  classif_info[0] = True
2806
2823
  if feedback is not None and "fill" in feedback:
@@ -2869,78 +2886,41 @@ class Elemental(Service):
2869
2886
  # let's iterate through the default ops and add them
2870
2887
  if debug:
2871
2888
  debug("Pass 2 (wasn't classified), looking for default ops")
2889
+ default_candidates = []
2872
2890
  for op in operations:
2873
- if classif_info[0] and op.type in (
2874
- "op engrave",
2875
- "op cut",
2876
- "op dots",
2877
- ):
2878
- continue
2879
- if classif_info[1] and op.type in ("op raster", "op image"):
2880
- continue
2881
- is_black = False
2882
- whisperer = True
2883
2891
  if (
2884
- hasattr(node, "stroke")
2885
- and node.stroke is not None
2886
- and node.stroke.argb is not None
2887
- and node.type != "elem text"
2888
- ):
2889
- if fuzzy:
2890
- is_black = (
2891
- Color.distance("black", abs(node.stroke))
2892
- <= fuzzydistance
2893
- or Color.distance("white", abs(node.stroke))
2894
- <= fuzzydistance
2895
- )
2896
- else:
2897
- is_black = Color("black") == abs(node.stroke) or Color(
2898
- "white"
2899
- ) == abs(node.stroke)
2900
- if (
2901
- not self.classify_black_as_raster
2902
- and is_black
2903
- and isinstance(op, RasterOpNode)
2892
+ hasattr(op, "classify") and
2893
+ getattr(op, "default", False) and
2894
+ hasattr(op, "valid_node_for_reference") and
2895
+ op.valid_node_for_reference(node)
2904
2896
  ):
2905
- # print ("Default Skip Raster")
2906
- whisperer = False
2907
- elif (
2908
- self.classify_black_as_raster
2909
- and is_black
2910
- and isinstance(op, EngraveOpNode)
2911
- ):
2912
- whisperer = False
2913
- if debug:
2914
- debug(
2915
- f"For {op.type}.{op.id}: black={is_black}, perform={whisperer}, flag={self.classify_black_as_raster}"
2916
- )
2917
- if hasattr(op, "classify") and whisperer:
2918
- classified, should_break, feedback = op.classify(
2919
- node,
2920
- fuzzy=fuzzy,
2921
- fuzzydistance=fuzzydistance,
2922
- usedefault=True,
2923
- )
2924
- else:
2925
- continue
2897
+ default_candidates.append(op)
2898
+ if len(default_candidates) > 1 and debug:
2899
+ debug(f"For node {node_desc} there were {len(default_candidates)} default operations available, nb the very first will be taken!")
2900
+ for op in default_candidates:
2901
+ classified, should_break, feedback = op.classify(
2902
+ node,
2903
+ fuzzy=fuzzy,
2904
+ fuzzydistance=fuzzydistance,
2905
+ usedefault=True,
2906
+ )
2926
2907
  if classified:
2927
- if feedback is not None and "stroke" in feedback:
2928
- classif_info[0] = True
2929
- if feedback is not None and "fill" in feedback:
2930
- classif_info[1] = True
2908
+ update_debug_set(debug_set, op)
2909
+ # Default ops fulfill stuff by definition
2910
+ classif_info[0] = True
2911
+ classif_info[1] = True
2931
2912
  was_classified = True
2932
2913
  if debug:
2933
2914
  debug(
2934
- f"Was classified to default operation: {type(op).__name__}, break={should_break}"
2915
+ f"Was classified to default operation: {type(op).__name__}"
2935
2916
  )
2936
- if should_break:
2937
2917
  break
2938
2918
  # Let's make sure we only consider relevant, i.e. existing attributes...
2939
2919
  if hasattr(node, "stroke"):
2940
2920
  if node.stroke is None or node.stroke.argb is None:
2941
2921
  classif_info[0] = True
2942
2922
  if node.type == "elem text":
2943
- # even if it has, we are not going to something with it
2923
+ # even if it has, we are not going to do something with it
2944
2924
  classif_info[0] = True
2945
2925
  else:
2946
2926
  classif_info[0] = True
@@ -3218,8 +3198,14 @@ class Elemental(Service):
3218
3198
 
3219
3199
  if not existing:
3220
3200
  op.add_reference(node)
3201
+ update_debug_set(debug_set, op)
3202
+
3221
3203
 
3222
3204
  self.remove_unused_default_copies()
3205
+ if debug:
3206
+ debug("Summary:")
3207
+ for key, count in debug_set.items():
3208
+ debug(f"{count} items assigned to {key}")
3223
3209
  if new_operations_added:
3224
3210
  self.signal("tree_changed")
3225
3211
 
@@ -198,7 +198,14 @@ def init_commands(kernel):
198
198
  y_distance += height
199
199
  if origin is None:
200
200
  origin = (1, 1)
201
- cx, cy = origin
201
+ if isinstance(origin, (tuple, list)) and isinstance(origin[0], (tuple, list)):
202
+ origin = origin[0]
203
+ try:
204
+ cx, cy = origin
205
+ except ValueError:
206
+ cx = 1
207
+ cy = 1
208
+
202
209
  data_out = list(data)
203
210
  if cx is None:
204
211
  cx = 1
@@ -876,9 +876,6 @@ def init_commands(kernel):
876
876
  if method is None:
877
877
  method = "union"
878
878
  method = method.lower()
879
- if filltype is None:
880
- filltype = "evenodd"
881
- filltype = filltype.lower()
882
879
  if keep is None:
883
880
  keep = False
884
881
 
@@ -891,6 +888,10 @@ def init_commands(kernel):
891
888
  else:
892
889
  long_method = "Union"
893
890
 
891
+ if filltype is None:
892
+ filltype = "evenodd" if method != "union" else "nonzero"
893
+ filltype = filltype.lower()
894
+
894
895
  if filltype.startswith("no") or filltype.startswith("z"):
895
896
  long_filltype = "NonZero"
896
897
  elif filltype.startswith("p") or filltype.startswith("+"):
@@ -360,10 +360,11 @@ def offset_path(self, path, offset_value=0):
360
360
  # As this oveloading a regular method in a class
361
361
  # it needs to have the very same definition (including the class
362
362
  # reference self)
363
+ # Radial connectors seem to have issues, so we don't use them for now...
363
364
  p = path_offset(
364
365
  path,
365
366
  offset_value=-offset_value,
366
- radial_connector=True,
367
+ radial_connector=False,
367
368
  linearize=True,
368
369
  interpolation=500,
369
370
  )