svg2gcode 3.3.0__tar.gz → 3.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/PKG-INFO +1 -1
  2. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/svg2gcode/__init__.py +1 -1
  3. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/svg2gcode/svg_to_gcode/compiler/_compiler.py +72 -42
  4. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/svg2gcode/svg_to_gcode/compiler/interfaces/_gcode.py +7 -7
  5. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/LICENSE +0 -0
  6. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/README.md +0 -0
  7. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/pyproject.toml +0 -0
  8. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/svg2gcode/__main__.py +0 -0
  9. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/svg2gcode/svg_to_gcode/LICENSE +0 -0
  10. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/svg2gcode/svg_to_gcode/__init__.py +0 -0
  11. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/svg2gcode/svg_to_gcode/compiler/__init__.py +0 -0
  12. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/svg2gcode/svg_to_gcode/compiler/interfaces/__init__.py +0 -0
  13. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/svg2gcode/svg_to_gcode/compiler/interfaces/_abstract_interface.py +0 -0
  14. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/svg2gcode/svg_to_gcode/compiler/interfaces/_fan_controlled_gcode.py +0 -0
  15. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/svg2gcode/svg_to_gcode/css_color.py +0 -0
  16. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/svg2gcode/svg_to_gcode/formulas.py +0 -0
  17. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/svg2gcode/svg_to_gcode/geometry/__init__.py +0 -0
  18. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/svg2gcode/svg_to_gcode/geometry/_abstract_chain.py +0 -0
  19. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/svg2gcode/svg_to_gcode/geometry/_abstract_curve.py +0 -0
  20. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/svg2gcode/svg_to_gcode/geometry/_circular_arc.py +0 -0
  21. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/svg2gcode/svg_to_gcode/geometry/_cubic_bazier.py +0 -0
  22. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/svg2gcode/svg_to_gcode/geometry/_elliptical_arc.py +0 -0
  23. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/svg2gcode/svg_to_gcode/geometry/_line.py +0 -0
  24. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/svg2gcode/svg_to_gcode/geometry/_line_segment_chain.py +0 -0
  25. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/svg2gcode/svg_to_gcode/geometry/_matrix.py +0 -0
  26. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/svg2gcode/svg_to_gcode/geometry/_quadratic_bazier.py +0 -0
  27. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/svg2gcode/svg_to_gcode/geometry/_raster_image.py +0 -0
  28. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/svg2gcode/svg_to_gcode/geometry/_smooth_arc_chain.py +0 -0
  29. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/svg2gcode/svg_to_gcode/geometry/_vector.py +0 -0
  30. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/svg2gcode/svg_to_gcode/svg_parser/__init__.py +0 -0
  31. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/svg2gcode/svg_to_gcode/svg_parser/_parser_methods.py +0 -0
  32. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/svg2gcode/svg_to_gcode/svg_parser/_path.py +0 -0
  33. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/svg2gcode/svg_to_gcode/svg_parser/_transformation.py +0 -0
  34. {svg2gcode-3.3.0 → svg2gcode-3.3.2}/svg2gcode/svg_to_gcode/svg_parser/debug_methods.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: svg2gcode
3
- Version: 3.3.0
3
+ Version: 3.3.2
4
4
  Summary: svg2gcode: convert an SVG (Scalable Vector Graphic) image to gcode.
5
5
  Keywords: scalable vector graphics,svg,image,laser cutter,laser engraving
6
6
  Author-email: Johannes Noordanus <mailjohannes.mailnoordanus@gmail.com>
@@ -1,4 +1,4 @@
1
1
  """
2
2
  svg2gcode: convert an SVG (Scalable Vector Graphic) image to gcode.
3
3
  """
4
- __version__ = "3.3.0"
4
+ __version__ = "3.3.2"
@@ -66,6 +66,9 @@ class Compiler:
66
66
 
67
67
  self.interface.set_machine_parameters(self.settings)
68
68
 
69
+ # toggle
70
+ self.fan_on = self.settings['fan']
71
+
69
72
  if custom_header is None:
70
73
  custom_header = []
71
74
 
@@ -77,7 +80,7 @@ class Compiler:
77
80
  self.interface.set_distance_mode(self.settings["distance_mode"])] + custom_header
78
81
  self.footer = custom_footer
79
82
 
80
- # path object gcode
83
+ # path gcode
81
84
  self.body: list[str] = []
82
85
  # image gcode
83
86
  self.gcode: list[str] = []
@@ -128,12 +131,13 @@ class Compiler:
128
131
 
129
132
  :param passes: the number of passes that should be made. Every pass the machine moves_down (z-axis) by
130
133
  self.pass_depth and self.body is repeated.
131
- :return returns the assembled code. self.header + [self.body, -self.pass_depth] * passes + self.footer
134
+ :return returns the assembled g-code. [self.body, -self.pass_depth] * passes
132
135
  """
133
136
  if len(self.body) == 0:
134
137
  logger.debug("Compile with an empty body (no curves).")
135
138
  return ''
136
139
 
140
+ fan_off = False
137
141
  gcode = []
138
142
  for i in range(passes):
139
143
  gcode += [f"; pass #{i+1}"]
@@ -141,13 +145,11 @@ class Compiler:
141
145
 
142
146
  if i < (passes - 1) and self.settings["pass_depth"] > 0:
143
147
  # If it isn't the last pass, turn off the laser and move down
144
- gcode.append(self.interface.laser_off())
148
+ gcode.append(self.interface.laser_off(fan_off))
145
149
  gcode.append(self.interface.set_relative_coordinates())
146
150
  gcode.append(self.interface.linear_move(z=-self.settings["pass_depth"]))
147
151
  gcode.append(self.interface.set_distance_mode(self.settings["distance_mode"]))
148
152
 
149
- gcode += [self.interface.laser_off()]
150
-
151
153
  # remove all ""
152
154
  gcode = filter(lambda command: len(command) > 0, gcode)
153
155
 
@@ -160,11 +162,12 @@ class Compiler:
160
162
  """
161
163
 
162
164
  # laser off, fan on, M3 or M4 burn mode
163
- header_gc = ["M5","M8", 'M3' if self.settings["laser_mode"] == "constant" else 'M4']
164
- # laser off, fan off
165
- footer_gc = ["M5","M9"]
165
+ header_gc = ["M5"]
166
+ if self.settings['fan'] and self.settings["splitfile"]:
167
+ header_gc += ['M8']
168
+ header_gc += ['M3' if self.settings["laser_mode"] == "constant" else 'M4']
166
169
 
167
- return '\n'.join(header_gc + self.gcode + footer_gc)
170
+ return '\n'.join(header_gc + self.gcode)
168
171
 
169
172
  def compile_to_file(self, file_name: str, svg_file_name: str, curves: list[Curve], passes=1):
170
173
  """
@@ -182,15 +185,17 @@ class Compiler:
182
185
  self.append_curves(curves)
183
186
 
184
187
  header = '\n'.join(self.header) + '\n'
188
+ footer = '\n'.join(self.footer) + '\n'
185
189
 
186
190
  if len(self.body) > 0:
187
191
  # write path objects
188
192
  with open(file_name, 'w') as file:
189
- emit_program_end = self.interface.program_end() if (self.settings["splitfile"] or len(self.gcode) == 0) else ""
190
- file.write(self.gcode_file_header() + header + self.compile(passes=passes) + '\n' + emit_program_end)
193
+ #emit_program_end = self.interface.program_end() if (self.settings["splitfile"] or len(self.gcode) == 0) else ""
194
+ program_end = footer if (self.settings["splitfile"] or len(self.gcode) == 0) else ""
195
+ file.write(self.gcode_file_header() + header + self.compile(passes=passes) + '\n' + program_end)
191
196
  logger.info(f"Generated {file_name}")
192
197
  else:
193
- logger.warn(f'No path (curve) data found, skipping "{file_name}"')
198
+ logger.warn(f'No path (curve) data found nothing added to "{file_name}"')
194
199
 
195
200
  image_file_name = file_name.rsplit('.',1)[0] + "_images." + file_name.rsplit('.',1)[1]
196
201
  if len(self.gcode) == 0:
@@ -202,13 +207,13 @@ class Compiler:
202
207
  if self.settings["splitfile"]:
203
208
  # emit image objects to <filename>_images.<gcext>
204
209
  with open(image_file_name, 'w') as file:
205
- file.write(self.gcode_file_header() + header + self.compile_images() + '\n' + self.interface.program_end() + '\n')
210
+ file.write(self.gcode_file_header() + header + self.compile_images() + '\n' + footer)
206
211
  logger.info(f"Generated {image_file_name}")
207
212
  else:
208
213
  # emit images objects in same file
209
214
  open_mode = 'w' if len(self.body) == 0 else 'a+'
210
215
  with open(file_name, open_mode) as file:
211
- file.write((self.gcode_file_header() if len(self.body) == 0 else "") + '\n' + self.compile_images() + '\n' + self.interface.program_end() + '\n')
216
+ file.write((self.gcode_file_header() if len(self.body) == 0 else "") + '\n' + self.compile_images() + '\n' + footer)
212
217
  logger.info(f"Added image(s) to {file_name}")
213
218
 
214
219
  def append_line_chain(self, line_chain: LineSegmentChain, step: float, color: int = None, speed: int = None):
@@ -233,20 +238,24 @@ class Compiler:
233
238
  laser_power = color if color is not None else self.settings["laser_power"]
234
239
  movement_speed = speed if speed is not None else self.settings["movement_speed"]
235
240
 
241
+ # do not turn fan off
242
+ fan_off = False
243
+
236
244
  # Move to the next line_chain when the next line segment doesn't connect to the end of the previous one.
237
245
  if self.interface.position is None or abs(self.interface.position - start) > TOLERANCES["operation"]:
238
246
  if self.interface.position is None or self.settings["rapid_move"]:
239
247
  # move to the next line_chain: set laser off, rapid move to start of chain,
240
248
  # set movement (cutting) speed, set laser mode and power on
241
- code += [self.interface.laser_off(), self.interface.rapid_move(start.x, start.y),
249
+ code += [self.interface.laser_off(fan_off), self.interface.rapid_move(start.x, start.y),
242
250
  self.interface.set_movement_speed(movement_speed),
243
- self.interface.set_laser_mode(self.settings["laser_mode"]), self.interface.set_laser_power_value(laser_power)]
251
+ self.interface.set_laser_mode(self.settings["laser_mode"]), self.interface.set_laser_power_value(laser_power,self.fan_on)]
244
252
  else:
245
253
  # move to the next line_chain: set laser mode, set laser power to 0 (cutting is off),
246
254
  # set movement speed, (no rapid) move to start of chain, set laser to power
247
- code += [self.interface.set_laser_mode(self.settings["laser_mode"]), self.interface.set_laser_power_value(0),
255
+ code += [self.interface.set_laser_mode(self.settings["laser_mode"]), self.interface.set_laser_power_value(0,self.fan_on),
248
256
  self.interface.set_movement_speed(movement_speed), self.interface.linear_move(start.x, start.y),
249
257
  self.interface.set_laser_power_value(laser_power)]
258
+ self.fan_on = False
250
259
 
251
260
  self.boundingbox.update(start)
252
261
 
@@ -370,7 +379,7 @@ class Compiler:
370
379
  arguments["showoverscan"] = img_attrib['gcode_showoverscan'] if 'gcode_showoverscan' in img_attrib else self.settings["image_showoverscan"]
371
380
  arguments["offset"] = (float(img_attrib['x']), float(img_attrib['y']))
372
381
  arguments["name"] = img_attrib['id']
373
- arguments["invert"] = img_attrib['invert'] if 'invert' in img_attrib else False
382
+ arguments["invert"] = img_attrib['invert'] if 'invert' in img_attrib else True
374
383
 
375
384
  # get image parameters
376
385
  params = ''
@@ -671,7 +680,10 @@ class Compiler:
671
680
  # Invert b&w value and apply alpha channel - when available - to the inverted b&w value
672
681
  # Library function image2gcode - as it is now - cannot invert a color value and then apply the alpha channel.
673
682
  # Note that step 6 of '# Render svg fill attribute' below, sets option 'invert' of image2gcode to false to use fill_color directly.)
674
- fill_color = Image2gcode.linear_power(css_color.parse_css_color2bw8(style['fill']), self.settings["maximum_image_laser_power"])
683
+
684
+ # invert (byte value)
685
+ fill_color = Image2gcode.linear_power(css_color.parse_css_color2bw8(style['fill']), 255)
686
+
675
687
  if style['fill-rule'] is not None:
676
688
  fill_rule = style['fill-rule']
677
689
  if fill_rule == "nonzero":
@@ -687,7 +699,7 @@ class Compiler:
687
699
  fill_alpha = 1
688
700
  if len(rgba) == 4:
689
701
  fill_alpha = rgba[3]
690
- fill_color = fill_color * fill_alpha
702
+ fill_color = round(fill_color * fill_alpha)
691
703
 
692
704
  return (stroke_width, stroke_color, stroke_alpha, fill_color, fill_alpha, fill_rule)
693
705
 
@@ -747,14 +759,6 @@ class Compiler:
747
759
  stroke_width, stroke_color, stroke_alpha, fill_color, fill_alpha, fill_rule = get_style_info_of_line_chain(line_chain)
748
760
 
749
761
  if len(stroke_color):
750
- # Get steps (offsets) for the lines that make the border
751
- steps = [0]
752
- if stroke_width:
753
- for delta in range(stroke_width):
754
- if delta:
755
- steps.append(round(delta * pixel_size, self.precision))
756
- if not (delta == stroke_width and stroke_width % 2 != 0):
757
- steps.append(round(-delta * pixel_size, self.precision))
758
762
 
759
763
  # Get stroke alpha channel (opacity)
760
764
  # fill opacity attribute overrides rgba property
@@ -766,8 +770,8 @@ class Compiler:
766
770
 
767
771
  # engrave values
768
772
  # set inversed b&w value (and apply alpha channel, when available)
769
- inverse_bw = Image2gcode.linear_power(css_color.parse_css_color2bw8(stroke_color), \
770
- self.settings["maximum_image_laser_power"]) * stroke_alpha
773
+ inverse_bw = round(Image2gcode.linear_power(css_color.parse_css_color2bw8(stroke_color),
774
+ self.settings["maximum_image_laser_power"]) * stroke_alpha)
771
775
  # set laser head movement speed
772
776
  speed = self.settings["image_movement_speed"]
773
777
 
@@ -782,31 +786,51 @@ class Compiler:
782
786
  # color_coded set cut path for this stroke_color
783
787
  self.body.extend([f"\n; --color_coded cut path '{name_id}', color '{stroke_color}'"])
784
788
  render_pathwidth(line_chain, [0], None, None, boundingbox)
785
- else:
786
- # engrave path ...
787
- pathignore, pathcut, pathengrave = color_coded_paths()
789
+ elif inverse_bw:
790
+ # with path color, setengrave path ...
791
+ pathignore, pathcut, pathengrave = self.color_coded_paths()
788
792
 
789
793
  # ... if color_coded did not set engrave (at all) or set engrave for this stroke_color
790
794
  if len(pathengrave) == 0 or engravepath:
795
+ # Get steps (offsets) for the lines that make the border
796
+ steps = [0]
797
+ if stroke_width:
798
+ for delta in range(stroke_width):
799
+ if delta:
800
+ steps.append(round(delta * pixel_size, self.precision))
801
+ if not (delta == stroke_width and stroke_width % 2 != 0):
802
+ steps.append(round(-delta * pixel_size, self.precision))
803
+
791
804
  if len(pathengrave) == 0:
792
805
  self.body.extend([f"\n; --color_coded: engrave not set, path '{name_id}', color '{stroke_color}'"])
793
806
  else:
794
807
  self.body.extend([f"\n; --color_coded: engrave path '{name_id}', color '{stroke_color}'"])
795
808
  render_pathwidth(line_chain, steps, inverse_bw, speed, boundingbox)
796
809
  else:
810
+ # ignore path
797
811
  self.body.extend([f"\n; --color_coded: ignore path '{name_id}', color '{stroke_color}'"])
798
812
 
799
- else:
800
- # color_coded isn't set: engrave path
813
+ elif inverse_bw:
814
+ # Get steps (offsets) for the lines that make the border
815
+ steps = [0]
816
+ if stroke_width:
817
+ for delta in range(stroke_width):
818
+ if delta:
819
+ steps.append(round(delta * pixel_size, self.precision))
820
+ if not (delta == stroke_width and stroke_width % 2 != 0):
821
+ steps.append(round(-delta * pixel_size, self.precision))
822
+
823
+ # color_coded isn't set: with path color engrave path
801
824
  self.body.extend([f"\n; engrave path '{name_id}', color '{stroke_color}'"])
802
825
  render_pathwidth(line_chain, steps, inverse_bw, speed, boundingbox)
803
826
  else:
804
- # line stroke isn't set: cut path
805
- self.body.extend([f"\n; no stroke color: cut path '{name_id}'"])
806
- render_pathwidth(line_chain, [0], None, None, boundingbox)
827
+ # line stroke isn't set: ignore path
828
+ self.body.extend([f"\n; no stroke color set: ignore path '{name_id}'"])
829
+ #render_pathwidth(line_chain, [0], None, None, boundingbox)
807
830
 
808
831
  # Render svg 'fill' attribute
809
- if not self.settings["nofill"] and fill_color is not None and boundingbox.get() is not None:
832
+ #if not self.settings["nofill"] and fill_color is not None and boundingbox.get() is not None:
833
+ if not self.settings["nofill"] and fill_color is not None:
810
834
  # fill a path
811
835
  # this is done in 6 steps:
812
836
  # step 1: create two raster images matching the bbox
@@ -817,6 +841,12 @@ class Compiler:
817
841
  # step 5: filter stray pixels (to remove noise from the action above)
818
842
  # step 6: generate gcode from image_fill (by execution function image2gcode)
819
843
 
844
+ # update boundingbox for this 'name_id'
845
+ for line_chain in path_curves[name_id]:
846
+ for line in line_chain:
847
+ boundingbox.update(line.start)
848
+ boundingbox.update(line.end)
849
+
820
850
  # get bounding box info from the path border
821
851
  lowerleft = boundingbox.get()[0]
822
852
  upperright = boundingbox.get()[1]
@@ -834,8 +864,8 @@ class Compiler:
834
864
 
835
865
  # step 1: create two raster images matching the bbox
836
866
  # init
837
- image_mark = np.full([img_height + 2, img_width + scan_error], 255, dtype=np.uint8)
838
- image_fill = np.full([img_height + 2, img_width + scan_error], 0, dtype=np.uint8)
867
+ image_mark = np.full([img_height + 4, img_width + scan_error + 2], 255, dtype=np.uint8)
868
+ image_fill = np.full([img_height + 4, img_width + scan_error + 2], 0, dtype=np.uint8)
839
869
 
840
870
  # step 2: add marker lines just inside the line chains of the path
841
871
  # - determine the inside of the line chain
@@ -852,7 +882,7 @@ class Compiler:
852
882
  # delta line chain is inside or outside the base line chain, but when we compare
853
883
  # the bbox sizes we know.
854
884
  # Note that this method can be used to determine if a line chain is drawn clockwise
855
- # or anti-clockwise because a posive delta offset should be 'outside' the line chain
885
+ # or anti-clockwise because a positive delta offset should be 'outside' the line chain
856
886
  # in this case (depending on the definition/calculation of the delta function).
857
887
  # we can use this to implement the other svg fill rule: 'nonzero'.
858
888
  bbox = Boundingbox()
@@ -154,22 +154,22 @@ class Gcode(Interface):
154
154
 
155
155
  return command + ''
156
156
 
157
- def laser_off(self):
157
+ def laser_off(self, fan_off = True):
158
158
  self._laser_mode = 'M5'
159
159
  self._laser_mode_changed = True
160
- new_mode = "M5" + ("\nM9" if self._machine_params['fan'] else '') # laser off, (fan off when available)
160
+ new_mode = "M5" + ("\nM9" if self._machine_params['fan'] and fan_off else '') # laser off, (fan off when available)
161
161
  return f"{new_mode}"
162
162
 
163
- def set_laser_power(self, power):
163
+ def set_laser_power(self, power, fan_on = True):
164
164
  print("laser power!")
165
165
  if power < 0 or power > 1:
166
166
  raise ValueError(f"{power} is out of bounds. Laser power must be given between 0 and 1. "
167
167
  f"The interface will scale it correctly.")
168
168
 
169
169
  # set power for next linear move
170
- return self.set_laser_power_value(int(linear_map(self._machine_params['minimum_laser_power'], self._machine_params['maximum_laser_power'], power)))
170
+ return self.set_laser_power_value(int(linear_map(self._machine_params['minimum_laser_power'], self._machine_params['maximum_laser_power'], power)), fan_on)
171
171
 
172
- def set_laser_power_value(self, machine_value) -> str:
172
+ def set_laser_power_value(self, machine_value, fan_on = True) -> str:
173
173
  """
174
174
  Set laser power directly - in target machine values - not as a fraction.
175
175
  """
@@ -177,10 +177,10 @@ class Gcode(Interface):
177
177
  self._next_laser_power = machine_value
178
178
 
179
179
  # (fan on when available), laser_on
180
- new_mode = ("\nM8" if self._machine_params['fan'] else '') + ("\n" + self._laser_mode if self._laser_mode_changed else '')
180
+ new_mode = ("\nM8" if self._machine_params['fan'] and fan_on else '') + ("\n" + self._laser_mode if self._laser_mode_changed else '')
181
181
  self._laser_mode_changed = False
182
182
  # return laser mode (M3 constant laser power or M4 dynamic laser power) when laser mode changed
183
- return f"; Cut at F{self._next_speed} {self._unit}/min, power S{machine_value}{new_mode}"
183
+ return f"; F{self._next_speed} {self._unit}/min, power S{machine_value}{new_mode}"
184
184
 
185
185
  def set_laser_mode(self, mode):
186
186
  # set constant/dynamic laser power mode
File without changes
File without changes
File without changes