svg2gcode 3.0.3__tar.gz → 3.2.1__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.
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/PKG-INFO +10 -4
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/README.md +8 -2
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/pyproject.toml +1 -1
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/__init__.py +1 -1
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/__main__.py +14 -13
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/__init__.py +6 -4
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/compiler/_compiler.py +339 -145
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/css_color.py +5 -4
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/formulas.py +6 -1
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/geometry/_elliptical_arc.py +5 -3
- svg2gcode-3.2.1/svg2gcode/svg_to_gcode/geometry/_line.py +111 -0
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/geometry/_line_segment_chain.py +59 -0
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/svg_parser/_path.py +1 -1
- svg2gcode-3.0.3/svg2gcode/svg_to_gcode/geometry/_line.py +0 -33
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/LICENSE +0 -0
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/LICENSE +0 -0
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/compiler/__init__.py +0 -0
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/compiler/interfaces/__init__.py +0 -0
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/compiler/interfaces/_abstract_interface.py +0 -0
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/compiler/interfaces/_fan_controlled_gcode.py +0 -0
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/compiler/interfaces/_gcode.py +0 -0
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/geometry/__init__.py +0 -0
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/geometry/_abstract_chain.py +0 -0
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/geometry/_abstract_curve.py +0 -0
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/geometry/_circular_arc.py +0 -0
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/geometry/_cubic_bazier.py +0 -0
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/geometry/_matrix.py +0 -0
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/geometry/_quadratic_bazier.py +0 -0
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/geometry/_raster_image.py +0 -0
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/geometry/_smooth_arc_chain.py +0 -0
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/geometry/_vector.py +0 -0
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/svg_parser/__init__.py +0 -0
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/svg_parser/_parser_methods.py +0 -0
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/svg_parser/_transformation.py +0 -0
- {svg2gcode-3.0.3 → svg2gcode-3.2.1}/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
|
+
Version: 3.2.1
|
|
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>
|
|
@@ -9,7 +9,7 @@ Description-Content-Type: text/markdown
|
|
|
9
9
|
Classifier: License :: OSI Approved :: MIT License
|
|
10
10
|
Requires-Dist: numpy >= 1.24.3
|
|
11
11
|
Requires-Dist: Pillow >= 9.5.0
|
|
12
|
-
Requires-Dist: image2gcode >= 2.9.
|
|
12
|
+
Requires-Dist: image2gcode >= 2.9.8
|
|
13
13
|
Project-URL: Home, https://github.com/johannesnoordanus/svg2gcode
|
|
14
14
|
|
|
15
15
|
# svg2gcode
|
|
@@ -41,12 +41,16 @@ See options ```--speedmoves``` and ```--noise``` for example.
|
|
|
41
41
|
Version 3.0.0 and higher have support for 'stroke' (color) and 'stroke-width' attributes, this means that cutting a path works a bit different now.
|
|
42
42
|
When the *stroke* attribute of a *path* is nonexistent or set to none, the path will be laser burned with the value set by option *--cuttingpower*.
|
|
43
43
|
|
|
44
|
+
Version 3.1.0 and higher have support for 'fill' (color) and 'fill-rule' attributes. Currently only fill rule 'evenodd' is supported.
|
|
45
|
+
|
|
44
46
|
Also, *svg2gcode* option ```--pathcut``` can be used to override all stroke attributes and force cutting of all *paths* of the SVG.
|
|
45
|
-
|
|
47
|
+
Option ```--nofill``` (not set by default) is added to disable path fills.
|
|
46
48
|
|
|
49
|
+
More info can be obtained by looking at the examples below and from program *image2gcode* and its documentation (for example about callibrating laser engravers).
|
|
50
|
+
|
|
47
51
|
Please consider supporting me, so I can make this application better and add new functionality to it: <http://paypal.me/johannesnoordanus/5,00>
|
|
48
52
|
|
|
49
|
-
My next update will
|
|
53
|
+
My next update will add fill-rule 'nonzero'.
|
|
50
54
|
|
|
51
55
|
|
|
52
56
|
To summarize:
|
|
@@ -64,6 +68,7 @@ General optimizations
|
|
|
64
68
|
- low burn levels (stray pixels) can be suppressed (default off)
|
|
65
69
|
- option *--constantburn* selects constant burn mode *M3* (for cutting and engraving) instead of default dynamic burn mode *M4*
|
|
66
70
|
- borders are drawn in parallel and in one go, following the *path* coordinates.
|
|
71
|
+
- fill has support for alpha channel and 'fill-opacity'
|
|
67
72
|
|
|
68
73
|
**Tip**: use commandline program *grblhud* *(https://github.com/johannesnoordanus/grblhud)* to have full control over gcode execution,
|
|
69
74
|
also, program *image2gcode* has similar capabilities but handles raster images files (like *png* and *jpg*) directly.
|
|
@@ -124,6 +129,7 @@ options:
|
|
|
124
129
|
--rotate <default:0> number of degrees to rotate
|
|
125
130
|
--splitfile split gcode output of SVG path and image objects
|
|
126
131
|
--pathcut alway cut SVG path objects! (use laser power set with option --cuttingpower)
|
|
132
|
+
--nofill ignore SVG fill attribute
|
|
127
133
|
--xmaxtravel <default:300>
|
|
128
134
|
machine x-axis lengh in mm
|
|
129
135
|
--ymaxtravel <default:400>
|
|
@@ -27,12 +27,16 @@ See options ```--speedmoves``` and ```--noise``` for example.
|
|
|
27
27
|
Version 3.0.0 and higher have support for 'stroke' (color) and 'stroke-width' attributes, this means that cutting a path works a bit different now.
|
|
28
28
|
When the *stroke* attribute of a *path* is nonexistent or set to none, the path will be laser burned with the value set by option *--cuttingpower*.
|
|
29
29
|
|
|
30
|
+
Version 3.1.0 and higher have support for 'fill' (color) and 'fill-rule' attributes. Currently only fill rule 'evenodd' is supported.
|
|
31
|
+
|
|
30
32
|
Also, *svg2gcode* option ```--pathcut``` can be used to override all stroke attributes and force cutting of all *paths* of the SVG.
|
|
31
|
-
|
|
33
|
+
Option ```--nofill``` (not set by default) is added to disable path fills.
|
|
32
34
|
|
|
35
|
+
More info can be obtained by looking at the examples below and from program *image2gcode* and its documentation (for example about callibrating laser engravers).
|
|
36
|
+
|
|
33
37
|
Please consider supporting me, so I can make this application better and add new functionality to it: <http://paypal.me/johannesnoordanus/5,00>
|
|
34
38
|
|
|
35
|
-
My next update will
|
|
39
|
+
My next update will add fill-rule 'nonzero'.
|
|
36
40
|
|
|
37
41
|
|
|
38
42
|
To summarize:
|
|
@@ -50,6 +54,7 @@ General optimizations
|
|
|
50
54
|
- low burn levels (stray pixels) can be suppressed (default off)
|
|
51
55
|
- option *--constantburn* selects constant burn mode *M3* (for cutting and engraving) instead of default dynamic burn mode *M4*
|
|
52
56
|
- borders are drawn in parallel and in one go, following the *path* coordinates.
|
|
57
|
+
- fill has support for alpha channel and 'fill-opacity'
|
|
53
58
|
|
|
54
59
|
**Tip**: use commandline program *grblhud* *(https://github.com/johannesnoordanus/grblhud)* to have full control over gcode execution,
|
|
55
60
|
also, program *image2gcode* has similar capabilities but handles raster images files (like *png* and *jpg*) directly.
|
|
@@ -110,6 +115,7 @@ options:
|
|
|
110
115
|
--rotate <default:0> number of degrees to rotate
|
|
111
116
|
--splitfile split gcode output of SVG path and image objects
|
|
112
117
|
--pathcut alway cut SVG path objects! (use laser power set with option --cuttingpower)
|
|
118
|
+
--nofill ignore SVG fill attribute
|
|
113
119
|
--xmaxtravel <default:300>
|
|
114
120
|
machine x-axis lengh in mm
|
|
115
121
|
--ymaxtravel <default:400>
|
|
@@ -37,7 +37,7 @@ def svg2gcode(args) -> int:
|
|
|
37
37
|
'maximum_image_laser_power':args.imagepower, 'image_movement_speed':args.imagespeed, 'fan':args.fan,'rapid_move':args.rapidmove,
|
|
38
38
|
'showimage':args.showimage, 'x_axis_maximum_travel':args.xmaxtravel,'y_axis_maximum_travel':args.ymaxtravel, 'image_noise':args.noise,
|
|
39
39
|
'pass_depth':args.pass_depth, 'laser_mode':"constant" if args.constantburn else "dynamic", 'splitfile':args.splitfile, 'pathcut':args.pathcut,
|
|
40
|
-
'image_poweroffset':args.poweroffset, 'image_overscan':args.overscan, 'image_showoverscan':args.showoverscan})
|
|
40
|
+
'nofill':args.nofill, 'image_poweroffset':args.poweroffset, 'image_overscan':args.overscan, 'image_showoverscan':args.showoverscan})
|
|
41
41
|
|
|
42
42
|
compiler = init_compiler(args)
|
|
43
43
|
|
|
@@ -138,6 +138,7 @@ def main() -> int:
|
|
|
138
138
|
type=int, help="number of degrees to rotate")
|
|
139
139
|
parser.add_argument('--splitfile', action='store_true', default=False, help='split gcode output of SVG path and image objects' )
|
|
140
140
|
parser.add_argument('--pathcut', action='store_true', default=False, help='alway cut SVG path objects! (use laser power set with option --cuttingpower)' )
|
|
141
|
+
parser.add_argument('--nofill', action='store_true', default=False, help='ignore SVG fill attribute' )
|
|
141
142
|
parser.add_argument('--xmaxtravel', default=cfg["xmaxtravel_default"], metavar="<default:" +str(cfg["xmaxtravel_default"])+ ">",
|
|
142
143
|
type=int, help="machine x-axis lengh in mm")
|
|
143
144
|
parser.add_argument('--ymaxtravel', default=cfg["ymaxtravel_default"], metavar="<default:" +str(cfg["ymaxtravel_default"])+ ">",
|
|
@@ -147,18 +148,18 @@ def main() -> int:
|
|
|
147
148
|
|
|
148
149
|
args = parser.parse_args()
|
|
149
150
|
|
|
150
|
-
try:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
except KeyboardInterrupt:
|
|
157
|
-
print(f"svg2gcode aborted!")
|
|
158
|
-
except Exception as error:
|
|
159
|
-
print(error)
|
|
160
|
-
|
|
161
|
-
return 1
|
|
151
|
+
## try:
|
|
152
|
+
if args.origin is not None and args.selfcenter:
|
|
153
|
+
print("options --selfcenter and --origin cannot be used at the same time, program abort")
|
|
154
|
+
return 1
|
|
155
|
+
|
|
156
|
+
return svg2gcode(args)
|
|
157
|
+
#### except KeyboardInterrupt:
|
|
158
|
+
## print(f"svg2gcode aborted!")
|
|
159
|
+
## except Exception as error:
|
|
160
|
+
## print(error)
|
|
161
|
+
##
|
|
162
|
+
## return 1
|
|
162
163
|
|
|
163
164
|
if __name__ == '__main__':
|
|
164
165
|
sys.exit(main())
|
|
@@ -39,7 +39,8 @@ SETTING = {
|
|
|
39
39
|
"pixel_size", # float sets image pixel size (in mm)
|
|
40
40
|
"showimage", # boolean show image used for conversion to gcode
|
|
41
41
|
"splitfile", # boolean return SVG path objects to <filename>.<gcext> and SVG image objects to <filename>_images.<gcext>
|
|
42
|
-
"pathcut"
|
|
42
|
+
"pathcut", # boolean always cut SVG path objects! use laser_power setting
|
|
43
|
+
"nofill" # boolean ignore SVG fill attribute
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
# Set defaults 'minimum_laser_power', 'pass_depth', 'dwell_time', 'laser_power', 'laser_mode', 'unit', 'distance_mode'
|
|
@@ -71,8 +72,9 @@ DEFAULT_SETTING = {
|
|
|
71
72
|
"rapid_move": 10, # default set to true, for images do a rapid move when skipping more than 10mm
|
|
72
73
|
"pixel_size": 0.1, # laser kerf is mostly < 0.1mm
|
|
73
74
|
"showimage": False, # default image is not shown
|
|
74
|
-
"splitfile": False,
|
|
75
|
-
"pathcut": False
|
|
75
|
+
"splitfile": False, # SVG path and image objects are emitted to one file: <filename>.<gcext>
|
|
76
|
+
"pathcut": False, # always cut SVG path objects! use laser_power setting
|
|
77
|
+
"nofill": False # ignore SVG fill attribute
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
def check_setting(setting: dict[str,Any] =None) -> bool:
|
|
@@ -114,7 +116,7 @@ def check_setting(setting: dict[str,Any] =None) -> bool:
|
|
|
114
116
|
raise ValueError(f"Unknown '{key}' value '{setting[key]}'. Please specify one of the following: {UNITS}")
|
|
115
117
|
if key == "distance_mode" and setting[key] not in DISTANCEMODE:
|
|
116
118
|
raise ValueError(f"Unknown '{key}' value '{setting[key]}'. Please specify one of the following: {DISTANCEMODE}")
|
|
117
|
-
if key in {"fan","showimage","splitfile","pathcut","image_showoverscan"} and setting[key] not in {True,False}:
|
|
119
|
+
if key in {"fan","showimage","splitfile","pathcut","nofill","image_showoverscan"} and setting[key] not in {True,False}:
|
|
118
120
|
raise ValueError(f"Unknown '{key}' value '{setting[key]}'. Please specify one of the following: {{True,False}}")
|
|
119
121
|
if key == "pixel_size" and setting[key] and (not isinstance(setting[key],(float)) or setting[key] <= 0):
|
|
120
122
|
raise TypeError(f"'{key}' is of type '{type(setting[key])}' but should be of type {type(1.0)} and have a value > 0.0")
|
|
@@ -8,6 +8,8 @@ from io import BytesIO
|
|
|
8
8
|
from typing import Any
|
|
9
9
|
|
|
10
10
|
import base64
|
|
11
|
+
from PIL import Image
|
|
12
|
+
from skimage.draw import line as drawline
|
|
11
13
|
import numpy as np
|
|
12
14
|
|
|
13
15
|
from svg2gcode.svg_to_gcode.compiler.interfaces import Interface
|
|
@@ -210,64 +212,6 @@ class Compiler:
|
|
|
210
212
|
file.write((self.gcode_file_header() if len(self.body) == 0 else "") + '\n' + self.compile_images() + '\n' + self.interface.program_end() + '\n')
|
|
211
213
|
logger.info(f"Added image(s) to {file_name}")
|
|
212
214
|
|
|
213
|
-
def orthogonal_offset(self, offset: float, line: Line) -> Line:
|
|
214
|
-
|
|
215
|
-
def offset_direction(line: Line) -> (int,int):
|
|
216
|
-
d_x = (line.end.x - line.start.x)
|
|
217
|
-
d_y = (line.end.y - line.start.y)
|
|
218
|
-
direction = (0,0)
|
|
219
|
-
|
|
220
|
-
if d_x > 0 and d_y > 0:
|
|
221
|
-
direction = (-1,1)
|
|
222
|
-
elif d_x > 0 and d_y == 0:
|
|
223
|
-
direction = (0,1)
|
|
224
|
-
elif d_x < 0 and d_y == 0:
|
|
225
|
-
direction = (0,-1)
|
|
226
|
-
elif d_x < 0 and d_y < 0:
|
|
227
|
-
direction = (1,-1)
|
|
228
|
-
elif d_x == 0 and d_y < 0:
|
|
229
|
-
direction = (1,0)
|
|
230
|
-
elif d_x == 0 and d_y > 0:
|
|
231
|
-
direction = (-1,0)
|
|
232
|
-
elif d_x > 0 and d_y < 0:
|
|
233
|
-
direction = (1,1)
|
|
234
|
-
elif d_x < 0 and d_y > 0:
|
|
235
|
-
direction = (-1,-1)
|
|
236
|
-
|
|
237
|
-
return direction
|
|
238
|
-
|
|
239
|
-
delta_sign = offset_direction(line)
|
|
240
|
-
|
|
241
|
-
# calculate ortogonal line vector((0,0),(delta_x,delta_y)) of 'offset' length:
|
|
242
|
-
# (1) y = 1/slope * x
|
|
243
|
-
#
|
|
244
|
-
# of length offset:
|
|
245
|
-
# (2) x^2 + y^2 = offset^2 (Pythagoras)
|
|
246
|
-
# (3) x^2 + (1/slope * x)^2 = offset^2
|
|
247
|
-
# (4) (1 + (1/slope)^2) * x^2 = offset^2
|
|
248
|
-
# (5) x^2 = offset^2 / (1 + (1/slope)^2)
|
|
249
|
-
|
|
250
|
-
if line.slope == 0:
|
|
251
|
-
# vertical line
|
|
252
|
-
delta_x = 0
|
|
253
|
-
delta_y = delta_sign[1] * offset
|
|
254
|
-
elif line.slope == 1:
|
|
255
|
-
# horizontal line
|
|
256
|
-
delta_x = delta_sign[0] * offset
|
|
257
|
-
delta_y = 0
|
|
258
|
-
else:
|
|
259
|
-
# slope
|
|
260
|
-
inv_slope = abs((1/line.slope))
|
|
261
|
-
delta_x = round(math.sqrt((offset ** 2) / (1 + inv_slope ** 2)), self.precision)
|
|
262
|
-
delta_y = round(inv_slope * delta_x, self.precision)
|
|
263
|
-
|
|
264
|
-
# set right direction
|
|
265
|
-
delta_x = delta_sign[0] * (-1 if (offset < 0) else 1) * delta_x
|
|
266
|
-
delta_y = delta_sign[1] * (-1 if (offset < 0) else 1) * delta_y
|
|
267
|
-
|
|
268
|
-
# add delta vector to line start and end vectors
|
|
269
|
-
return Line(line.start + Vector(delta_x, delta_y), line.end + Vector(delta_x, delta_y), "offset")
|
|
270
|
-
|
|
271
215
|
def append_line_chain(self, line_chain: LineSegmentChain, step: float, color: int = None):
|
|
272
216
|
"""
|
|
273
217
|
Draws a LineSegmentChain by calling interface.linear_move() for each segment. The resulting code is appended to
|
|
@@ -400,42 +344,10 @@ class Compiler:
|
|
|
400
344
|
|
|
401
345
|
return None
|
|
402
346
|
|
|
403
|
-
def
|
|
404
|
-
"""
|
|
405
|
-
Pythagoras
|
|
406
|
-
"""
|
|
407
|
-
# |Ax - Bx|^2 + |Ay - By|^2 = C^2
|
|
408
|
-
# distance = √C^2
|
|
409
|
-
return math.sqrt(abs(A[0] - B[0])**2 + abs(A[1] - B[1])**2)
|
|
410
|
-
|
|
411
|
-
def line_intersection(self, l1: Line, l2:Line) -> Vector:
|
|
412
|
-
"""
|
|
413
|
-
Line intersection (https://en.wikipedia.org/wiki/Line–line_intersection)
|
|
414
|
-
"""
|
|
415
|
-
x1, y1 = l1.start
|
|
416
|
-
x2, y2 = l1.end
|
|
417
|
-
x3, y3 = l2.start
|
|
418
|
-
x4, y4 = l2.end
|
|
419
|
-
|
|
420
|
-
# denomintor
|
|
421
|
-
d = round((x1 - x2)*(y3 - y4) - (y1 - y2)*(x3 - x4), self.precision)
|
|
422
|
-
|
|
423
|
-
# Note that the cutoff below is needed to eliminate line artifacts (spikes) of the render result.
|
|
424
|
-
# This is due to calculation (precision) errors resulting in the y coordinate of the intersection to
|
|
425
|
-
# be off by a factor (2,3, 10?). It might be that the rounding applied here is not correct, or/and
|
|
426
|
-
# that the equations are inherently sensitive to rounding errors at certain inputs.
|
|
427
|
-
# TODO: analize this
|
|
428
|
-
if abs(d) < 0.01:
|
|
429
|
-
# no intersection
|
|
430
|
-
return None
|
|
431
|
-
|
|
432
|
-
return Vector( round(((x1*y2 - y1*x2) * (x3 - x4) - (x1 - x2) * (x3*y4 - y3*x4))/d, self.precision),
|
|
433
|
-
round(((x1*y2 - y1*x2) * (y3 - y4) - (y1 - y2) * (x3*y4 - y3*x4))/d, self.precision) )
|
|
434
|
-
|
|
435
|
-
def image2gcode(self, img_attrib: dict[str, Any], img=None, transformation = None):
|
|
347
|
+
def image2gcode(self, img_attrib: dict[str, Any], img=None, transformation = None, power = None):
|
|
436
348
|
|
|
437
349
|
# create image conversion object
|
|
438
|
-
convert = Image2gcode(transformation = transformation.apply_affine_transformation if transformation is not None else None)
|
|
350
|
+
convert = Image2gcode(transformation = transformation.apply_affine_transformation if transformation is not None else None, power = power)
|
|
439
351
|
|
|
440
352
|
#
|
|
441
353
|
# set arguments
|
|
@@ -453,6 +365,7 @@ class Compiler:
|
|
|
453
365
|
arguments["showoverscan"] = img_attrib['gcode_showoverscan'] if 'gcode_showoverscan' in img_attrib else self.settings["image_showoverscan"]
|
|
454
366
|
arguments["offset"] = (float(img_attrib['x']), float(img_attrib['y']))
|
|
455
367
|
arguments["name"] = img_attrib['id']
|
|
368
|
+
arguments["noinvert"] = img_attrib['noinvert'] if 'noinvert' in img_attrib else False
|
|
456
369
|
|
|
457
370
|
# get image parameters
|
|
458
371
|
params = ''
|
|
@@ -477,7 +390,7 @@ class Compiler:
|
|
|
477
390
|
for example "fill:#F4CF84;fill-rule:evenodd;stroke:#D07735;"
|
|
478
391
|
"""
|
|
479
392
|
|
|
480
|
-
style = {'fill' : None, 'fill-rule': None, 'stroke': None, 'stroke-width': None, 'pathcut': None}
|
|
393
|
+
style = {'fill' : None, 'fill-rule': None, 'fill-opacity': None, 'stroke': None, 'stroke-width': None, 'pathcut': None}
|
|
481
394
|
|
|
482
395
|
# parse style attribute
|
|
483
396
|
|
|
@@ -494,6 +407,11 @@ class Compiler:
|
|
|
494
407
|
fill_rule = re.search('fill-rule:#(evenodd|nonzero)',style_str)
|
|
495
408
|
if fill_rule:
|
|
496
409
|
style['fill-rule'] = re.search('(evenodd|nonzero)', fill_rule.group(0)).group(0)
|
|
410
|
+
# parse fill-opacity
|
|
411
|
+
if 'fill-opacity' in style_str:
|
|
412
|
+
fill_opacity = re.search('fill-opacity:(\d*\.)?\d+',style_str)
|
|
413
|
+
if fill_opacity:
|
|
414
|
+
style['fill-opacity'] = re.search('(\d*\.)?\d+', fill_opacity.group(0)).group(0)
|
|
497
415
|
# parse stroke
|
|
498
416
|
if 'stroke' in style_str:
|
|
499
417
|
stroke = re.search('stroke:[^;]+;',style_str)
|
|
@@ -518,13 +436,17 @@ class Compiler:
|
|
|
518
436
|
style['fill'] = curve.path_attrib['fill']
|
|
519
437
|
if 'fill-rule' in curve.path_attrib:
|
|
520
438
|
style['fill-rule'] = curve.path_attrib['fill-rule']
|
|
439
|
+
# parse fill-opacity
|
|
440
|
+
if 'fill-opacity' in curve.path_attrib:
|
|
441
|
+
fill_opacity = re.search('fill-opacity:(\d*\.)?\d+',curve.path_attrib)
|
|
442
|
+
if fill_opacity:
|
|
443
|
+
style['fill-opacity'] = re.search('(\d*\.)?\d+', fill_opacity.group(0)).group(0)
|
|
521
444
|
# parse stroke attribute
|
|
522
445
|
if 'stroke' in curve.path_attrib:
|
|
523
446
|
style['stroke'] = curve.path_attrib['stroke']
|
|
524
447
|
# parse stroke-width attribute
|
|
525
448
|
if 'stroke-width' in curve.path_attrib:
|
|
526
449
|
style['stroke-width'] = curve.path_attrib['stroke-width']
|
|
527
|
-
|
|
528
450
|
# parse gcode_pathcut attribute
|
|
529
451
|
if 'gcode_pathcut' in curve.path_attrib:
|
|
530
452
|
style['pathcut'] = curve.path_attrib['gcode_pathcut']
|
|
@@ -535,7 +457,17 @@ class Compiler:
|
|
|
535
457
|
"""
|
|
536
458
|
Draws curves.
|
|
537
459
|
"""
|
|
460
|
+
|
|
461
|
+
def draw_line(img: np.array, p1: (int,int), p2: (int,int), gray: int):
|
|
462
|
+
"""
|
|
463
|
+
Draws a line.
|
|
464
|
+
"""
|
|
465
|
+
pixel = 1/self.settings["pixel_size"]
|
|
466
|
+
yy, xx = drawline(int(p1[1]*pixel),int(p1[0]*pixel),int(p2[1]*pixel),int(p2[0]*pixel))
|
|
467
|
+
img[yy,xx] = gray
|
|
468
|
+
|
|
538
469
|
path_curves = {}
|
|
470
|
+
pixel_size = float(self.settings["pixel_size"])
|
|
539
471
|
|
|
540
472
|
for curve in curves:
|
|
541
473
|
if isinstance(curve, RasterImage):
|
|
@@ -554,7 +486,7 @@ class Compiler:
|
|
|
554
486
|
|
|
555
487
|
curve_name_id = ""
|
|
556
488
|
# parse id
|
|
557
|
-
if curve.path_attrib:
|
|
489
|
+
if curve.path_attrib and 'id' in curve.path_attrib:
|
|
558
490
|
curve_name_id = curve.path_attrib['id']
|
|
559
491
|
|
|
560
492
|
if curve_name_id not in path_curves:
|
|
@@ -572,19 +504,24 @@ class Compiler:
|
|
|
572
504
|
# add line segments to curves having this id
|
|
573
505
|
path_curves[curve_name_id].append(line_chain)
|
|
574
506
|
|
|
507
|
+
# default stroke width is one line (thickness)
|
|
508
|
+
stroke_width = pixel_size
|
|
509
|
+
|
|
575
510
|
# emit all paths (organized by name id)
|
|
576
511
|
for name_id in path_curves:
|
|
577
|
-
|
|
512
|
+
steps = []
|
|
513
|
+
fill_color = None
|
|
514
|
+
fill_rule = None
|
|
515
|
+
# set a boundingbox per 'name_id'
|
|
516
|
+
boundingbox = Boundingbox()
|
|
578
517
|
for line_chain in path_curves[name_id]:
|
|
579
518
|
|
|
580
|
-
pixel_size = float(self.settings["pixel_size"])
|
|
581
519
|
# default stroke width is one line (thickness)
|
|
582
|
-
stroke_width = pixel_size
|
|
583
|
-
|
|
584
|
-
first_line_of_chain = line_chain.get(0)
|
|
585
|
-
|
|
586
520
|
width = 0
|
|
587
521
|
stroke_color = ""
|
|
522
|
+
|
|
523
|
+
# get style info for this line chain (of object 'name_id')
|
|
524
|
+
first_line_of_chain = line_chain.get(0)
|
|
588
525
|
style = self.parse_style_attribute(first_line_of_chain)
|
|
589
526
|
if style:
|
|
590
527
|
if not (style['pathcut'] == 'true' or self.settings["pathcut"]) \
|
|
@@ -592,62 +529,319 @@ class Compiler:
|
|
|
592
529
|
stroke_color = style['stroke']
|
|
593
530
|
if style['stroke-width'] is not None and style['stroke-width'] != "none":
|
|
594
531
|
stroke_width = float(style['stroke-width'])
|
|
595
|
-
width =
|
|
596
|
-
|
|
532
|
+
width = math.ceil(round(stroke_width/pixel_size, self.precision)/2)
|
|
533
|
+
if style['fill'] is not None:
|
|
534
|
+
# Invert b&w value (beforehand) and apply alpha channel - when available - to inverted b&w value
|
|
535
|
+
# Note that step 6 sets option 'noinvert' to be able to use the value below directly.
|
|
536
|
+
fill_color = Image2gcode.linear_power(css_color.parse_css_color2bw8(style['fill']), self.settings["maximum_image_laser_power"])
|
|
537
|
+
if style['fill-rule'] is not None:
|
|
538
|
+
fill_rule = style['fill-rule']
|
|
539
|
+
if fill_rule == "nonzero":
|
|
540
|
+
logger.warn(f"fill-rule 'nonzero' of object '{name_id}' is currently unsupported!")
|
|
541
|
+
if style['fill-opacity'] is not None:
|
|
542
|
+
alpha = float(style['fill-opacity'])
|
|
543
|
+
if not (alpha >=0 and alpha <= 1):
|
|
544
|
+
logger.warn(f"Opacity value '{alpha}' should be in range [0.0..1.0]!")
|
|
545
|
+
alpha = 1
|
|
546
|
+
else:
|
|
547
|
+
rgba = css_color.parse_css_color(style['fill'])
|
|
548
|
+
alpha = 1
|
|
549
|
+
if len(rgba) == 4:
|
|
550
|
+
alpha = rgba[3]
|
|
551
|
+
fill_color = fill_color * alpha
|
|
552
|
+
|
|
553
|
+
# get steps (offsets) for the lines that make the border
|
|
597
554
|
steps = [0]
|
|
598
555
|
if len(stroke_color) and width:
|
|
599
556
|
for delta in range(width):
|
|
600
557
|
if delta:
|
|
601
558
|
steps.append(round(delta * pixel_size, self.precision))
|
|
602
|
-
if not (delta
|
|
559
|
+
if not (delta == width and width % 2 != 0):
|
|
603
560
|
steps.append(round(-delta * pixel_size, self.precision))
|
|
604
561
|
|
|
562
|
+
# Get alpha channel (opacity)
|
|
563
|
+
alpha = 1
|
|
564
|
+
if len(stroke_color):
|
|
565
|
+
rgba = css_color.parse_css_color(stroke_color)
|
|
566
|
+
if len(rgba) == 4:
|
|
567
|
+
alpha = rgba[3]
|
|
568
|
+
|
|
569
|
+
# render svg stroke (of size width)
|
|
570
|
+
# (draw smooth border outlines in gcode)
|
|
571
|
+
# Note the update of the boundingbox.
|
|
572
|
+
self.body.extend([f"\n; border: '{name_id}'"])
|
|
605
573
|
for step in steps:
|
|
606
574
|
if step:
|
|
607
|
-
delta_chain = LineSegmentChain()
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
line_delta = self.orthogonal_offset(step, line)
|
|
611
|
-
|
|
612
|
-
if delta_chain.chain_size():
|
|
613
|
-
# calculate intersection of previous line and current line
|
|
614
|
-
intersect = self.line_intersection(delta_chain.get(-1), line_delta)
|
|
615
|
-
large = 200
|
|
616
|
-
if intersect is not None:
|
|
617
|
-
# set prev_line.end to intersect
|
|
618
|
-
prev_line = delta_chain.get(-1)
|
|
619
|
-
prev_line.end = intersect
|
|
620
|
-
delta_chain.set(-1, prev_line)
|
|
621
|
-
|
|
622
|
-
# set line_delta start to intersect
|
|
623
|
-
line_delta.start = intersect
|
|
624
|
-
else:
|
|
625
|
-
# connect line, to prevent ValueErrors from delta_chain.append() below
|
|
626
|
-
line_delta.start = delta_chain.get(-1).end
|
|
627
|
-
|
|
628
|
-
delta_chain.append(line_delta)
|
|
629
|
-
|
|
630
|
-
# check if line chain is a loop
|
|
631
|
-
if line_chain.get(0).start == line_chain.get(-1).end:
|
|
632
|
-
# fix delta chain
|
|
633
|
-
intersect = self.line_intersection(delta_chain.get(0), delta_chain.get(-1))
|
|
634
|
-
if intersect is not None:
|
|
635
|
-
# update start of loop
|
|
636
|
-
start_loop = delta_chain.get(0)
|
|
637
|
-
start_loop.start = intersect
|
|
638
|
-
delta_chain.set(0, start_loop)
|
|
639
|
-
|
|
640
|
-
# update end of loop
|
|
641
|
-
end_loop = delta_chain.get(-1)
|
|
642
|
-
end_loop.end = intersect
|
|
643
|
-
delta_chain.set(-1, end_loop)
|
|
644
|
-
|
|
645
|
-
# linear_power(self, pixel: UInt8, maxpower: int, offset: int = 0, invert: bool = True) -> int
|
|
646
|
-
inverse_bw = Image2gcode.linear_power(css_color.parse_css_color2bw8(stroke_color), self.settings["maximum_image_laser_power"])
|
|
575
|
+
delta_chain = LineSegmentChain.delta_chain(line_chain, step)
|
|
576
|
+
# apply alpha channel - when available - to inverted b&w value
|
|
577
|
+
inverse_bw = Image2gcode.linear_power(css_color.parse_css_color2bw8(stroke_color), self.settings["maximum_image_laser_power"]) * alpha
|
|
647
578
|
self.append_line_chain(delta_chain, step, inverse_bw)
|
|
579
|
+
# update 'name_id' boundingbox
|
|
580
|
+
for line in delta_chain:
|
|
581
|
+
boundingbox.update(line.start)
|
|
582
|
+
boundingbox.update(line.end)
|
|
648
583
|
else:
|
|
649
|
-
|
|
584
|
+
# apply alpha channel - when available - to inverted b&w value
|
|
585
|
+
inverse_bw = Image2gcode.linear_power(css_color.parse_css_color2bw8(stroke_color), self.settings["maximum_image_laser_power"]) * alpha if len(stroke_color) else None
|
|
650
586
|
self.append_line_chain(line_chain, step, inverse_bw)
|
|
587
|
+
# update 'name_id' boundingbox
|
|
588
|
+
for line in line_chain:
|
|
589
|
+
boundingbox.update(line.start)
|
|
590
|
+
boundingbox.update(line.end)
|
|
591
|
+
|
|
592
|
+
if not self.settings["nofill"] and fill_color is not None:
|
|
593
|
+
# fill a path
|
|
594
|
+
# this is done in 6 steps:
|
|
595
|
+
# step 1: create two raster images matching the bbox
|
|
596
|
+
# step 2: add marker lines just inside the line chains of the path
|
|
597
|
+
# step 3: scan the marker image lines and and apply 'evenodd' fill
|
|
598
|
+
# (fill rule 'nonzero' to be implemented later on see note below)
|
|
599
|
+
# step 4: draw white borders to erase fill overlap
|
|
600
|
+
# step 5: filter stray pixels (to remove noise from the action above)
|
|
601
|
+
# step 6: generate gcode from image_fill
|
|
602
|
+
|
|
603
|
+
# get bounding box info from the path border
|
|
604
|
+
lowerleft = boundingbox.get()[0]
|
|
605
|
+
upperright = boundingbox.get()[1]
|
|
606
|
+
|
|
607
|
+
# normalize origin to (0.0)
|
|
608
|
+
vdXY = Vector(-lowerleft.x, -lowerleft.y)
|
|
609
|
+
dXY = (-lowerleft.x, -lowerleft.y)
|
|
610
|
+
|
|
611
|
+
# get raster image dimensions
|
|
612
|
+
img_height = math.ceil((upperright.y - lowerleft.y)/pixel_size)
|
|
613
|
+
img_width = math.ceil((upperright.x - lowerleft.x)/pixel_size)
|
|
614
|
+
|
|
615
|
+
# default scan error (step 3 below)
|
|
616
|
+
scan_error = 4
|
|
617
|
+
|
|
618
|
+
# step 1: create two raster images matching the bbox
|
|
619
|
+
# init
|
|
620
|
+
image_mark = np.full([img_height + 2, img_width + scan_error], 255, dtype=np.uint8)
|
|
621
|
+
image_fill = np.full([img_height + 2, img_width + scan_error], 0, dtype=np.uint8)
|
|
622
|
+
|
|
623
|
+
# step 2: add marker lines just inside the line chains of the path
|
|
624
|
+
# - determine the inside of the line chain
|
|
625
|
+
# - draw marker lines
|
|
626
|
+
for line_chain in path_curves[name_id]:
|
|
627
|
+
# Note that a svg object with a specific name_id can have multiple line_chains that
|
|
628
|
+
# together, define one shape (circumference). When this the case the boundingbox
|
|
629
|
+
# inside/outside method below is not fullproof. This can be solved to stitch together
|
|
630
|
+
# the line chain parts (TODO)
|
|
631
|
+
|
|
632
|
+
# - determine the inside of the line chain
|
|
633
|
+
# Compare the bbox of the line chain with that of a delta line chain a
|
|
634
|
+
# fixed distance (offset) from the base line chain. Initially we do not know if the
|
|
635
|
+
# delta line chain is inside or outside the base line chain, but when we compare
|
|
636
|
+
# the bbox sizes we know.
|
|
637
|
+
# Note that this method can be used to determine if a line chain is drawn clockwise
|
|
638
|
+
# or anti-clockwise because a posive delta offset should be 'outside' the line chain
|
|
639
|
+
# in this case (depending on the definition/calculation of the delta function).
|
|
640
|
+
# we can use this to implement the other svg fill rule: 'nonzero'.
|
|
641
|
+
bbox = Boundingbox()
|
|
642
|
+
for line in line_chain:
|
|
643
|
+
bbox.update(line.start + vdXY)
|
|
644
|
+
bbox.update(line.end + vdXY)
|
|
645
|
+
bbox_size = bbox.size()
|
|
646
|
+
|
|
647
|
+
bbox = Boundingbox()
|
|
648
|
+
delta_chain = LineSegmentChain.delta_chain(line_chain, pixel_size * 2)
|
|
649
|
+
for line in delta_chain:
|
|
650
|
+
bbox.update(line.start + vdXY)
|
|
651
|
+
bbox.update(line.end + vdXY)
|
|
652
|
+
bbox_deltasize = bbox.size()
|
|
653
|
+
|
|
654
|
+
# compare bboxes and set inside offset
|
|
655
|
+
inside = 1
|
|
656
|
+
if bbox_deltasize > bbox_size:
|
|
657
|
+
inside = -1
|
|
658
|
+
|
|
659
|
+
# Note that some tuning is going on here.
|
|
660
|
+
# This can be remedied in several ways:
|
|
661
|
+
# - use draw lines that have a thickness?
|
|
662
|
+
# - use a higher resolution (pixel grid) to reduce the 'rounding' errors
|
|
663
|
+
offsets = [inside * .5 * pixel_size, inside * pixel_size, inside * 1.5 * pixel_size, inside * 2 * pixel_size]
|
|
664
|
+
# Note that the system sometimes returns line_chains having 1 point and thus having no size, this is 'solved'
|
|
665
|
+
# below, but should not happen (TODO). It is also assumed that line_chain parts that define one shape have
|
|
666
|
+
# similar sizes (TODO).
|
|
667
|
+
if bbox_size > 1 and bbox_size < 50:
|
|
668
|
+
# small area, less margin for error
|
|
669
|
+
scan_error = 1
|
|
670
|
+
del offsets[-2:]
|
|
671
|
+
|
|
672
|
+
## direct gcode: update this, see step 3 note
|
|
673
|
+
# draw border lines of a specific marker color within line chain
|
|
674
|
+
# Note that the marker values are just a choice and only have to be consistent
|
|
675
|
+
# with the algorithm used (step 3)
|
|
676
|
+
if bbox_size > 1:
|
|
677
|
+
for offset in offsets:
|
|
678
|
+
# make a line chain just one pixel inside the base (step 0) line chain
|
|
679
|
+
delta_chain = LineSegmentChain.delta_chain(line_chain, offset)
|
|
680
|
+
for line in delta_chain:
|
|
681
|
+
draw_line(image_mark, line.start + vdXY, line.end + vdXY, 128)
|
|
682
|
+
|
|
683
|
+
# draw the line chain border using another marker color
|
|
684
|
+
for line in line_chain:
|
|
685
|
+
draw_line(image_mark, line.start + vdXY, line.end + vdXY, 10)
|
|
686
|
+
|
|
687
|
+
# step 3: scan the marker image lines and and apply 'evenodd' fill
|
|
688
|
+
# (fill rule 'nonzero' to be implemented later on)
|
|
689
|
+
# for each line (y):
|
|
690
|
+
# for each point (x) on the line:
|
|
691
|
+
# scan from left to right or reverse:
|
|
692
|
+
# for markers (border,inside border) and apply rule
|
|
693
|
+
# 'evenodd' to fill when an even number of borders
|
|
694
|
+
# is crossed, untill odd
|
|
695
|
+
# Note that the image 'image_fill' is filled not 'image_mark' to make sure marks stay in place.
|
|
696
|
+
|
|
697
|
+
# Note that the fill algorithm can be adapted to support direct rendering of gcode (instead of rendering via 'image_fill' and
|
|
698
|
+
# function 'image2gcode', converting a raster image to gcode). This reduces the number of steps needed: steps 4, 5 and 6 can
|
|
699
|
+
# be left out. It does need a few adaptions in the previous steps, namely drawing the line chain border (color '10') in full width
|
|
700
|
+
# (instead of 1 pixel) and draw the 'inside' line (color 128) one pixel further. In addition to this uncomment the '## direct gcode'
|
|
701
|
+
# lines of step 3 to activate gcode rendering.
|
|
702
|
+
|
|
703
|
+
## direct gcode
|
|
704
|
+
## code = [f"\n; fill '{name_id}'"]
|
|
705
|
+
## code += [self.interface.set_laser_power_value(Image2gcode.linear_power(fill_color, self.settings["maximum_image_laser_power"]))]
|
|
706
|
+
|
|
707
|
+
go_right = True
|
|
708
|
+
# start scanning to the right
|
|
709
|
+
for y in range(image_mark.shape[0]):
|
|
710
|
+
evenodd = 0
|
|
711
|
+
|
|
712
|
+
if go_right:
|
|
713
|
+
# scan to the right
|
|
714
|
+
x = 0
|
|
715
|
+
start = None
|
|
716
|
+
while x < image_mark.shape[1]:
|
|
717
|
+
if image_mark[y,x] == 10 :
|
|
718
|
+
# found a border
|
|
719
|
+
x_b = x
|
|
720
|
+
## direct gcode
|
|
721
|
+
## update line below, to be able to skip empty (value 255) pixels
|
|
722
|
+
# scan border, possibly having multiple pixels.
|
|
723
|
+
while x_b < image_mark.shape[1] and image_mark[y,x_b] == 10:
|
|
724
|
+
x_b += 1
|
|
725
|
+
|
|
726
|
+
xscan_b = x - 1
|
|
727
|
+
while xscan_b > 0 and xscan_b > (x - scan_error) and (image_mark[y,xscan_b] != 10 and image_mark[y,xscan_b] != 128):
|
|
728
|
+
xscan_b -= 1
|
|
729
|
+
xscan_a = x_b
|
|
730
|
+
while xscan_a < image_mark.shape[1] and xscan_a < (x_b + scan_error) and (image_mark[y,xscan_a] != 10 and image_mark[y,xscan_a] != 128):
|
|
731
|
+
xscan_a += 1
|
|
732
|
+
|
|
733
|
+
if xscan_b >= 0 and image_mark[y,xscan_b] == 128:
|
|
734
|
+
# found border
|
|
735
|
+
if evenodd % 2 == 0:
|
|
736
|
+
start = (x * pixel_size, y * pixel_size)
|
|
737
|
+
## direct gcode
|
|
738
|
+
## code += [self.interface.rapid_move(start[0] - dXY[0], start[1] - dXY[1])]
|
|
739
|
+
else:
|
|
740
|
+
draw_line(image_fill, start + dXY, (x * pixel_size, y * pixel_size) + dXY, fill_color)
|
|
741
|
+
## direct gcode
|
|
742
|
+
## code += [self.interface.linear_move(x * pixel_size - dXY[0], y * pixel_size - dXY[1])]
|
|
743
|
+
evenodd = evenodd + 1
|
|
744
|
+
if xscan_a < image_mark.shape[1] and image_mark[y,xscan_a] == 128:
|
|
745
|
+
# found border
|
|
746
|
+
if evenodd % 2 == 0:
|
|
747
|
+
start = (x * pixel_size, y * pixel_size)
|
|
748
|
+
## direct gcode
|
|
749
|
+
## code += [self.interface.rapid_move(start[0] - dXY[0], start[1] - dXY[1])]
|
|
750
|
+
else:
|
|
751
|
+
draw_line(image_fill, start + dXY, (x_b * pixel_size, y * pixel_size) + dXY, fill_color)
|
|
752
|
+
## direct gcode
|
|
753
|
+
## code += [self.interface.linear_move(x_b * pixel_size - dXY[0], y * pixel_size - dXY[1])]
|
|
754
|
+
evenodd = evenodd + 1
|
|
755
|
+
if x_b > x:
|
|
756
|
+
x = x_b - 1
|
|
757
|
+
x += 1
|
|
758
|
+
else:
|
|
759
|
+
# scan to the left
|
|
760
|
+
start = None
|
|
761
|
+
x = image_mark.shape[1] - 1
|
|
762
|
+
while x >= 0:
|
|
763
|
+
if image_mark[y,x] == 10:
|
|
764
|
+
# found a border
|
|
765
|
+
## direct gcode
|
|
766
|
+
## update line below, to be able to skip empty (value 255) pixels
|
|
767
|
+
# scan border, possibly having multiple pixels.
|
|
768
|
+
x_b = x
|
|
769
|
+
while x_b >= 0 and image_mark[y,x_b] == 10:
|
|
770
|
+
x_b -= 1
|
|
771
|
+
|
|
772
|
+
xscan_b = x_b
|
|
773
|
+
while xscan_b > 0 and xscan_b > (x_b - scan_error) and (image_mark[y,xscan_b] != 10 and image_mark[y,xscan_b] != 128):
|
|
774
|
+
xscan_b -= 1
|
|
775
|
+
xscan_a = x + 1
|
|
776
|
+
while xscan_a < image_mark.shape[1] and xscan_a < (x + scan_error) and (image_mark[y,xscan_a] != 10 and image_mark[y,xscan_a] != 128):
|
|
777
|
+
xscan_a += 1
|
|
778
|
+
|
|
779
|
+
if xscan_a < image_mark.shape[1] and image_mark[y,xscan_a] == 128:
|
|
780
|
+
# found border
|
|
781
|
+
if evenodd % 2 == 0:
|
|
782
|
+
start = (x * pixel_size, y * pixel_size)
|
|
783
|
+
## direct gcode
|
|
784
|
+
## code += [self.interface.rapid_move(start[0] - dXY[0], start[1] - dXY[1])]
|
|
785
|
+
else:
|
|
786
|
+
draw_line(image_fill, start + dXY, (x * pixel_size, y * pixel_size) + dXY, fill_color)
|
|
787
|
+
## direct gcode
|
|
788
|
+
## code += [self.interface.linear_move(x * pixel_size - dXY[0], y * pixel_size - dXY[1])]
|
|
789
|
+
evenodd = evenodd + 1
|
|
790
|
+
if xscan_b >= 0 and image_mark[y,xscan_b] == 128:
|
|
791
|
+
# found border
|
|
792
|
+
if evenodd % 2 == 0:
|
|
793
|
+
start = (x * pixel_size, y * pixel_size)
|
|
794
|
+
## direct gcode
|
|
795
|
+
## code += [self.interface.rapid_move(start[0] - dXY[0], start[1] - dXY[1])]
|
|
796
|
+
else:
|
|
797
|
+
draw_line(image_fill, start + dXY, (x_b * pixel_size, y * pixel_size) + dXY, fill_color)
|
|
798
|
+
## direct gcode
|
|
799
|
+
## code += [self.interface.linear_move(x_b * pixel_size - dXY[0], y * pixel_size - dXY[1])]
|
|
800
|
+
evenodd = evenodd + 1
|
|
801
|
+
if x_b < x:
|
|
802
|
+
x = x_b + 1
|
|
803
|
+
x -= 1
|
|
804
|
+
|
|
805
|
+
# switch scan direction
|
|
806
|
+
go_right = not go_right
|
|
807
|
+
|
|
808
|
+
## direct gcode
|
|
809
|
+
## self.body.extend(code)
|
|
810
|
+
|
|
811
|
+
## direct gcode: ignore this step
|
|
812
|
+
# step 4: draw white borders to erase fill overlap
|
|
813
|
+
for line_chain in path_curves[name_id]:
|
|
814
|
+
for step in steps:
|
|
815
|
+
if step:
|
|
816
|
+
delta_chain = LineSegmentChain.delta_chain(line_chain, step)
|
|
817
|
+
for line in delta_chain:
|
|
818
|
+
draw_line(image_fill, line.start + vdXY, line.end + vdXY, 0)
|
|
819
|
+
else:
|
|
820
|
+
for line in line_chain:
|
|
821
|
+
draw_line(image_fill, line.start + vdXY, line.end + vdXY, 0)
|
|
822
|
+
|
|
823
|
+
## direct gcode: ignore this step
|
|
824
|
+
# step 5: filter stray pixels (to remove noise from the action above)
|
|
825
|
+
for y in range(image_fill.shape[0]):
|
|
826
|
+
for x in range(image_fill.shape[1]):
|
|
827
|
+
if image_fill[y,x] != 0:
|
|
828
|
+
if x > 1 and x < (image_fill.shape[1] - 1) and y > 1 and y < (image_fill.shape[0] - 1):
|
|
829
|
+
if image_fill[y,x+1] == 0 and image_fill[y,x-1] == 0 and image_fill[y+1,x] == 0 and image_fill[y-1,x] == 0:
|
|
830
|
+
image_fill[y,x] = 0
|
|
831
|
+
|
|
832
|
+
## direct gcode: ignore this step
|
|
833
|
+
# step 6: generate gcode from image_fill
|
|
834
|
+
img_attrib = {}
|
|
835
|
+
img_attrib['id'] = name_id
|
|
836
|
+
#img_attrib['gcode_speedmoves'] = 0
|
|
837
|
+
img_attrib['x'] = -dXY[0]
|
|
838
|
+
img_attrib['y'] = -dXY[1]
|
|
839
|
+
img_attrib['noinvert'] = True
|
|
840
|
+
|
|
841
|
+
## direct gcode: ignore this step
|
|
842
|
+
# start gcode fill
|
|
843
|
+
self.body.extend([f"\n;fill '{name_id}'"])
|
|
844
|
+
self.image2gcode(img_attrib, image_fill)
|
|
651
845
|
|
|
652
846
|
def check_axis_maximum_travel(self):
|
|
653
847
|
return self.settings["x_axis_maximum_travel"] is not None and self.settings["y_axis_maximum_travel"] is not None
|
|
@@ -192,7 +192,7 @@ def hex2rgb(hexcolor: str) -> [int,int,int]:
|
|
|
192
192
|
|
|
193
193
|
return [r,g,b]
|
|
194
194
|
|
|
195
|
-
def parse_css_color(color: str) -> [int,int,int]:
|
|
195
|
+
def parse_css_color(color: str) -> [int,int,int,float]:
|
|
196
196
|
"""
|
|
197
197
|
parse css color to rgb
|
|
198
198
|
(definition from https://www.w3.org/TR/css-color-3/)
|
|
@@ -229,7 +229,8 @@ def parse_css_color(color: str) -> [int,int,int]:
|
|
|
229
229
|
else:
|
|
230
230
|
b = int(rgb[commas_pos[1]+1:])
|
|
231
231
|
|
|
232
|
-
rgbcolor = [r * a, g * a, b * a]
|
|
232
|
+
#rgbcolor = [r * a, g * a, b * a]
|
|
233
|
+
rgbcolor = [r,g,b,a]
|
|
233
234
|
|
|
234
235
|
else:
|
|
235
236
|
hsla = re.search(f"hsl(a)?\([0-9]+,[0-9]+,[0-9]+(,{float_re})?", color)
|
|
@@ -249,7 +250,7 @@ def parse_css_color(color: str) -> [int,int,int]:
|
|
|
249
250
|
l = int(hsl[commas_pos[1]+1:])
|
|
250
251
|
|
|
251
252
|
rgbcolor = hsl2rgb(h, s, l)
|
|
252
|
-
rgbcolor = [int(rgbcolor[0] * 255
|
|
253
|
+
rgbcolor = [int(rgbcolor[0] * 255), int(rgbcolor[1] * 255), int(rgbcolor[2] * 255), a]
|
|
253
254
|
|
|
254
255
|
if not rgbcolor:
|
|
255
256
|
logger.warn(f"Not a valid css color: '{color}', color set to 'black'!")
|
|
@@ -267,7 +268,7 @@ def rgb24tobw24(rgb24: int) -> int:
|
|
|
267
268
|
bnw = int(r * 0.299 + g * 0.587 + b * 0.114) & 0xff;
|
|
268
269
|
return (bnw << 16) | (bnw << 8) | bnw;
|
|
269
270
|
|
|
270
|
-
def rgb24tobw8(rgb24: [int,int,int]) -> int:
|
|
271
|
+
def rgb24tobw8(rgb24: [int,int,int,float]) -> int:
|
|
271
272
|
"""
|
|
272
273
|
convert a 24 bit rgb (color) value to 8 bit grayscale
|
|
273
274
|
"""
|
|
@@ -11,7 +11,7 @@ from svg2gcode.svg_to_gcode import TOLERANCES
|
|
|
11
11
|
def tolerance_constrain(value, maximum, minimum, tolerance=TOLERANCES["operation"]):
|
|
12
12
|
"""
|
|
13
13
|
Constrain a value between if it surpasses a limit and is within operational tolerance of the limit. Else return the
|
|
14
|
-
value. Useful if you want to correct for
|
|
14
|
+
value. Useful if you want to correct for floating point errors but still want to raise an exception if the value is
|
|
15
15
|
out-of-bounds for a different reason.
|
|
16
16
|
"""
|
|
17
17
|
|
|
@@ -49,6 +49,11 @@ def line_intersect(p1, c1, p2, c2):
|
|
|
49
49
|
return (((c1_c - p1_c) * p1 - (c1 - p1) * p1_c) * (c2 - p2) - ((c2_c - p2_c) * p2 - (c2 - p2) * p2_c) * (
|
|
50
50
|
c1 - p1)) / ((c2 - p2) * (c1_c - p1_c) - (c1 - p1) * (c2_c - p2_c))
|
|
51
51
|
|
|
52
|
+
def line_length(A: (float,float),B: (float,float)):
|
|
53
|
+
""" Pythagoras """
|
|
54
|
+
# |Ax - Bx|^2 + |Ay - By|^2 = C^2
|
|
55
|
+
# distance = √C^2
|
|
56
|
+
return math.sqrt((A[0] - B[0])**2 + (A[1] - B[1])**2)
|
|
52
57
|
|
|
53
58
|
def is_on_mid_perpendicular(z, a, b):
|
|
54
59
|
"""Check if a point z is on the line which is perpendicular to ab and passes through the segment's midpoint"""
|
|
@@ -8,12 +8,12 @@ from svg2gcode.svg_to_gcode.geometry import Curve
|
|
|
8
8
|
class EllipticalArc(Curve):
|
|
9
9
|
"""The EllipticalArc class inherits from the abstract Curve class and describes an elliptical arc."""
|
|
10
10
|
|
|
11
|
-
__slots__ = 'center', 'radii', 'rotation', 'start_angle', 'sweep_angle', 'end_angle', 'transformation'
|
|
11
|
+
__slots__ = 'center', 'radii', 'rotation', 'start_angle', 'sweep_angle', 'end_angle', 'transformation', 'path_attrib'
|
|
12
12
|
|
|
13
13
|
# ToDo apply transformation beforehand (in Path) for consistency with other geometric objects. If you (the reader)
|
|
14
14
|
# know how to easily apply an affine transformation to an ellipse feel free to make a pull request.
|
|
15
15
|
def __init__(self, center: Vector, radii: Vector, rotation: float, start_angle: float, sweep_angle: float,
|
|
16
|
-
transformation: None):
|
|
16
|
+
transformation: None, path_attrib = None):
|
|
17
17
|
|
|
18
18
|
# Assign and verify arguments
|
|
19
19
|
self.center = center
|
|
@@ -22,6 +22,7 @@ class EllipticalArc(Curve):
|
|
|
22
22
|
self.start_angle = start_angle
|
|
23
23
|
self.sweep_angle = sweep_angle
|
|
24
24
|
self.transformation = transformation
|
|
25
|
+
self.path_attrib = path_attrib
|
|
25
26
|
|
|
26
27
|
# Calculate missing data
|
|
27
28
|
self.end_angle = start_angle + sweep_angle
|
|
@@ -32,7 +33,8 @@ class EllipticalArc(Curve):
|
|
|
32
33
|
|
|
33
34
|
def __repr__(self):
|
|
34
35
|
return f"EllipticalArc(start: {self.start}, end: {self.end}, center: {self.center}, radii: {self.radii}," \
|
|
35
|
-
f" rotation: {self.rotation}, start_angle: {self.start_angle}, sweep_angle: {self.sweep_angle}
|
|
36
|
+
f" rotation: {self.rotation}, start_angle: {self.start_angle}, sweep_angle: {self.sweep_angle}," \
|
|
37
|
+
f" transformation: {self.transformation}, attrib: {self.path_attrib})"
|
|
36
38
|
|
|
37
39
|
def point(self, t):
|
|
38
40
|
angle = formulas.linear_map(self.start_angle, self.end_angle, t)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
|
|
2
|
+
import math
|
|
3
|
+
|
|
4
|
+
from svg2gcode.svg_to_gcode.geometry import Vector
|
|
5
|
+
from svg2gcode.svg_to_gcode.geometry import Curve
|
|
6
|
+
from svg2gcode.svg_to_gcode import formulas
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# A line segment
|
|
10
|
+
class Line(Curve):
|
|
11
|
+
"""The Line class inherits from the abstract Curve class and describes a straight line segment."""
|
|
12
|
+
|
|
13
|
+
__slots__ = 'slope', 'offset', 'path_attrib'
|
|
14
|
+
|
|
15
|
+
def __init__(self, start, end, path_attrib = None):
|
|
16
|
+
self.start = start
|
|
17
|
+
self.end = end
|
|
18
|
+
|
|
19
|
+
self.slope = formulas.line_slope(start, end)
|
|
20
|
+
self.offset = formulas.line_offset(start, end)
|
|
21
|
+
self.path_attrib = path_attrib
|
|
22
|
+
|
|
23
|
+
def __repr__(self):
|
|
24
|
+
return f"Line(start:{self.start}, end:{self.end}, slope:{self.slope}, offset:{self.offset}, attrib:{self.path_attrib})"
|
|
25
|
+
|
|
26
|
+
def length(self):
|
|
27
|
+
#return abs(self.start - self.end)
|
|
28
|
+
return formulas.line_length(self.start,self.end)
|
|
29
|
+
|
|
30
|
+
def point(self, t):
|
|
31
|
+
x = self.start.x + t * (self.end.x - self.start.x)
|
|
32
|
+
y = self.slope * x + self.offset
|
|
33
|
+
|
|
34
|
+
return Vector(x, y)
|
|
35
|
+
|
|
36
|
+
def derivative(self, t):
|
|
37
|
+
return self.slope
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def offset_line(offset: float, line, precision = 6) -> (Vector,Vector):
|
|
41
|
+
"""
|
|
42
|
+
Offset a line perpendicular to the given line.
|
|
43
|
+
"""
|
|
44
|
+
def perpendicular(line) -> (int,int):
|
|
45
|
+
"""
|
|
46
|
+
Get sign of vector perpendicular to the given line,
|
|
47
|
+
pointing outwards for a clockwise rotation.
|
|
48
|
+
Return (sign x, sign y)
|
|
49
|
+
"""
|
|
50
|
+
d_x = line.end.x - line.start.x
|
|
51
|
+
d_y = line.end.y - line.start.y
|
|
52
|
+
# normalize to values 0 or 1 (with sign)
|
|
53
|
+
d_x = 0 if d_x == 0 else d_x/abs(d_x)
|
|
54
|
+
d_y = 0 if d_y == 0 else d_y/abs(d_y)
|
|
55
|
+
return (-d_y, d_x)
|
|
56
|
+
|
|
57
|
+
delta_sign = perpendicular(line)
|
|
58
|
+
|
|
59
|
+
if line.slope == 0:
|
|
60
|
+
# vertical line
|
|
61
|
+
delta_x = 0
|
|
62
|
+
delta_y = delta_sign[1] * offset
|
|
63
|
+
elif line.slope == 1:
|
|
64
|
+
# horizontal line
|
|
65
|
+
delta_x = delta_sign[0] * offset
|
|
66
|
+
delta_y = 0
|
|
67
|
+
else:
|
|
68
|
+
# calculate orthogonal line of 'offset' length:
|
|
69
|
+
#
|
|
70
|
+
# (1) y = 1/slope * x (line formula )
|
|
71
|
+
# (2) x^2 + y^2 = offset^2 (Pythagoras )
|
|
72
|
+
# (3) x^2 + (1/slope * x)^2 = offset^2 (substitue 'y' )
|
|
73
|
+
# (4) (1 + (1/slope)^2) * x^2 = offset^2 (reorder )
|
|
74
|
+
# (5) x^2 = offset^2 / (1 + (1/slope)^2) (get 'x' )
|
|
75
|
+
# slope
|
|
76
|
+
inv_slope = abs((1/line.slope))
|
|
77
|
+
delta_x = round(math.sqrt((offset ** 2) / (1 + inv_slope ** 2)), precision)
|
|
78
|
+
delta_y = round(inv_slope * delta_x, precision)
|
|
79
|
+
|
|
80
|
+
# normalize offset
|
|
81
|
+
offset = 0 if offset == 0 else offset/abs(offset)
|
|
82
|
+
delta_x = delta_sign[0] * offset * delta_x
|
|
83
|
+
delta_y = delta_sign[1] * offset * delta_y
|
|
84
|
+
|
|
85
|
+
# add delta vector to line start and end vectors
|
|
86
|
+
return Line(line.start + Vector(delta_x, delta_y), line.end + Vector(delta_x, delta_y))
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def line_intersection(l1, l2, precision = 6) -> Vector:
|
|
90
|
+
"""
|
|
91
|
+
Line intersection (https://en.wikipedia.org/wiki/Line–line_intersection)
|
|
92
|
+
Return intersection point or None when lines do not intersect.
|
|
93
|
+
|
|
94
|
+
"""
|
|
95
|
+
x1, y1 = l1.start
|
|
96
|
+
x2, y2 = l1.end
|
|
97
|
+
x3, y3 = l2.start
|
|
98
|
+
x4, y4 = l2.end
|
|
99
|
+
|
|
100
|
+
# denomintor
|
|
101
|
+
#d = round((x1 - x2)*(y3 - y4) - (y1 - y2)*(x3 - x4), self.precision)
|
|
102
|
+
d = (x1 - x2)*(y3 - y4) - (y1 - y2)*(x3 - x4)
|
|
103
|
+
|
|
104
|
+
# Note that the cutoff below is needed to eliminate propagation of rounding errors.
|
|
105
|
+
# The equations below are inherently sensitive to rounding errors at certain inputs.
|
|
106
|
+
if abs(d) < 10**-precision:
|
|
107
|
+
# no intersection
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
return Vector( round(((x1*y2 - y1*x2) * (x3 - x4) - (x1 - x2) * (x3*y4 - y3*x4))/d, precision),
|
|
111
|
+
round(((x1*y2 - y1*x2) * (y3 - y4) - (y1 - y2) * (x3*y4 - y3*x4))/d, precision) )
|
|
@@ -28,6 +28,65 @@ class LineSegmentChain(Chain):
|
|
|
28
28
|
|
|
29
29
|
self._curves.append(line2)
|
|
30
30
|
|
|
31
|
+
@staticmethod
|
|
32
|
+
def clockwise(line_chain, delta: float = .1) -> bool:
|
|
33
|
+
"""
|
|
34
|
+
Return true when line chain rotates clockwise using sample delta
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
# bbox_line_chain = bbox(line_chain)
|
|
38
|
+
# bbox_delta_chain = bbox(delta_chain(line_chain, delta))
|
|
39
|
+
# if bbox_delta_chain > bbox_line_chain:
|
|
40
|
+
# return true
|
|
41
|
+
# return false
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def delta_chain(line_chain, offset: float) -> "LineSegmentChain":
|
|
46
|
+
"""
|
|
47
|
+
Return a line chain offset to the given chain. If the chain rotates clockwise a positive offset
|
|
48
|
+
results in a delta chain outwards of (enclosing) the given line chain.
|
|
49
|
+
|
|
50
|
+
"""
|
|
51
|
+
delta_chain = LineSegmentChain()
|
|
52
|
+
|
|
53
|
+
for line in line_chain:
|
|
54
|
+
line_delta = Line.offset_line(offset, line)
|
|
55
|
+
|
|
56
|
+
if delta_chain.chain_size():
|
|
57
|
+
# calculate intersection of previous line and current line
|
|
58
|
+
intersect = Line.line_intersection(delta_chain.get(-1), line_delta)
|
|
59
|
+
if intersect is not None:
|
|
60
|
+
# set prev_line.end to intersect
|
|
61
|
+
prev_line = delta_chain.get(-1)
|
|
62
|
+
prev_line.end = intersect
|
|
63
|
+
delta_chain.set(-1, prev_line)
|
|
64
|
+
|
|
65
|
+
# set line_delta start to intersect
|
|
66
|
+
line_delta.start = intersect
|
|
67
|
+
else:
|
|
68
|
+
# connect line, to prevent ValueErrors from delta_chain.append() below
|
|
69
|
+
line_delta.start = delta_chain.get(-1).end
|
|
70
|
+
|
|
71
|
+
delta_chain.append(line_delta)
|
|
72
|
+
|
|
73
|
+
# check if line chain is a loop
|
|
74
|
+
if line_chain.get(0).start == line_chain.get(-1).end:
|
|
75
|
+
# fix delta chain
|
|
76
|
+
intersect = Line.line_intersection(delta_chain.get(0), delta_chain.get(-1))
|
|
77
|
+
if intersect is not None:
|
|
78
|
+
# update start of loop
|
|
79
|
+
start_loop = delta_chain.get(0)
|
|
80
|
+
start_loop.start = intersect
|
|
81
|
+
delta_chain.set(0, start_loop)
|
|
82
|
+
|
|
83
|
+
# update end of loop
|
|
84
|
+
end_loop = delta_chain.get(-1)
|
|
85
|
+
end_loop.end = intersect
|
|
86
|
+
delta_chain.set(-1, end_loop)
|
|
87
|
+
|
|
88
|
+
return delta_chain
|
|
89
|
+
|
|
31
90
|
@staticmethod
|
|
32
91
|
def line_segment_approximation(shape, increment_growth=11 / 10, error_cap=None, error_floor=None)\
|
|
33
92
|
-> "LineSegmentChain":
|
|
@@ -261,7 +261,7 @@ class Path:
|
|
|
261
261
|
radii, center, start_angle, sweep_angle = formulas.endpoint_to_center_parameterization(
|
|
262
262
|
start, end, radii, rotation_rad, large_arc_flag, sweep_flag)
|
|
263
263
|
|
|
264
|
-
arc = EllipticalArc(center, radii, rotation_rad, start_angle, sweep_angle, transformation=self.transformation)
|
|
264
|
+
arc = EllipticalArc(center, radii, rotation_rad, start_angle, sweep_angle, transformation=self.transformation, path_attrib=self.path_attrib)
|
|
265
265
|
|
|
266
266
|
self.current_point = end
|
|
267
267
|
return arc
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
from svg2gcode.svg_to_gcode.geometry import Vector
|
|
2
|
-
from svg2gcode.svg_to_gcode.geometry import Curve
|
|
3
|
-
from svg2gcode.svg_to_gcode import formulas
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
# A line segment
|
|
7
|
-
class Line(Curve):
|
|
8
|
-
"""The Line class inherits from the abstract Curve class and describes a straight line segment."""
|
|
9
|
-
|
|
10
|
-
__slots__ = 'slope', 'offset', 'path_attrib'
|
|
11
|
-
|
|
12
|
-
def __init__(self, start, end, path_attrib = None):
|
|
13
|
-
self.start = start
|
|
14
|
-
self.end = end
|
|
15
|
-
|
|
16
|
-
self.slope = formulas.line_slope(start, end)
|
|
17
|
-
self.offset = formulas.line_offset(start, end)
|
|
18
|
-
self.path_attrib = path_attrib
|
|
19
|
-
|
|
20
|
-
def __repr__(self):
|
|
21
|
-
return f"Line(start:{self.start}, end:{self.end}, slope:{self.slope}, offset:{self.offset}, attrib:{self.path_attrib})"
|
|
22
|
-
|
|
23
|
-
def length(self):
|
|
24
|
-
return abs(self.start - self.end)
|
|
25
|
-
|
|
26
|
-
def point(self, t):
|
|
27
|
-
x = self.start.x + t * (self.end.x - self.start.x)
|
|
28
|
-
y = self.slope * x + self.offset
|
|
29
|
-
|
|
30
|
-
return Vector(x, y)
|
|
31
|
-
|
|
32
|
-
def derivative(self, t):
|
|
33
|
-
return self.slope
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|