├── LICENSE ├── README.md ├── encoder_disk_generator.inx ├── encoder_disk_generator.py └── images └── screenshot.png /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Kalle Hyvönen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Inkscape rotary encoder disk generator 2 | 3 | ![Screenshot](https://github.com/Hyvok/Inkscape-rotary-encoder-disk-generator/blob/master/images/screenshot.png?raw=true) 4 | 5 | ## About the Project 6 | Inkscape rotary encoder disk generator is an extension for Inkscape (http://inkscape.org/) which enables you to 7 | conveniently create optical encoder discs. It currently supports creating single and two-track (quadrature) 8 | encoders, gray encoders, single-track gray encoders (at least for cases mentioned in NZ patent 264738) 9 | and bitmap encoders. 10 | 11 | More details in my blog http://www.dgkelectronics.com/inkscape-extension-for-creating-optical-rotary-encoder-discs 12 | 13 | ## Getting Started 14 | ### Prequisites 15 | Plugin has been tested with Python v3.7.4, Inkscape version v1.0.1. 16 | 17 | ### Installation 18 | You install the plugin by extracting the .inx and the .py file to your Inkscape extensions directory. For Linux it is 19 | `~/.config/inkscape/extensions` and for Windows it is `%APPDATA%\Roaming\inkscape\extensions`. 20 | 21 | ## License 22 | Distributed under the MIT License. See [LICENSE](LICENSE) for more information. 23 | 24 | ## Contact 25 | -Kalle Hyvönen, http://www.dgkelectronics.com 26 | -------------------------------------------------------------------------------- /encoder_disk_generator.inx: -------------------------------------------------------------------------------- 1 | 2 | 3 | <_name>Encoder Disk Generator 4 | dgkelectronics.com.encoder.disk.generator 5 | inkex.py 6 | encoder_disk_generator.py 8 | 9 | 10 | 0.0 12 | 0.0 14 | 1 16 | 0.0 19 | 0.0 21 | 0.0 24 | 0.0 26 | 27 | 28 | 0.0 30 | 0.0 32 | 1 34 | 0.0 36 | 0.0 38 | 0.0 40 | 41 | 42 | 0.0 44 | 0.0 46 | 1 48 | 1 50 | 0.0 52 | 0.0 54 | 55 | 56 | 30.0 58 | 5.0 60 | 010011110111000010001101 62 | 25.0 65 | 10.0 67 | 68 | 69 | 70 | all 71 | 72 | 73 | 74 | 75 | 79 | 80 | -------------------------------------------------------------------------------- /encoder_disk_generator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import inkex 3 | import math 4 | import string 5 | from lxml import etree 6 | from inkex.transforms import Transform 7 | 8 | 9 | # Function for calculating a point from the origin when you know the distance 10 | # and the angle 11 | def calculate_point(angle, distance): 12 | if (angle < 0 or angle > 360): 13 | return None 14 | else: 15 | return [ 16 | distance * math.cos(math.radians(angle)), 17 | distance * math.sin(math.radians(angle))] 18 | 19 | 20 | class EncoderDiskGenerator(inkex.Effect): 21 | 22 | def __init__(self): 23 | inkex.Effect.__init__(self) 24 | self.arg_parser.add_argument( 25 | "--tab", 26 | default="rotary_enc", 27 | help="Selected tab") 28 | self.arg_parser.add_argument( 29 | "--diameter", 30 | type=float, default=0.0, 31 | help="Diameter of the encoder disk") 32 | self.arg_parser.add_argument( 33 | "--hole_diameter", 34 | type=float, default=0.0, 35 | help="Diameter of the center hole") 36 | self.arg_parser.add_argument( 37 | "--segments", 38 | type=int, default=0, 39 | help="Number of segments") 40 | self.arg_parser.add_argument( 41 | "--outer_encoder_diameter", 42 | type=float, default=0.0, 43 | help="Diameter of the outer encoder disk") 44 | self.arg_parser.add_argument( 45 | "--outer_encoder_width", 46 | type=float, default=0.0, 47 | help="Width of the outer encoder disk") 48 | self.arg_parser.add_argument( 49 | "--inner_encoder_diameter", 50 | type=float, default=0.0, 51 | help="Diameter of the inner encoder disk") 52 | self.arg_parser.add_argument( 53 | "--inner_encoder_width", 54 | type=float, default=0.0, 55 | help="Width of the inner encoder disk") 56 | self.arg_parser.add_argument( 57 | "--bits", 58 | type=int, default=1, 59 | help="Number of bits/tracks") 60 | self.arg_parser.add_argument( 61 | "--encoder_diameter", 62 | type=float, default=0.0, 63 | help="Outer diameter of the last track") 64 | self.arg_parser.add_argument( 65 | "--track_width", 66 | type=float, default=0.0, 67 | help="Width of one track") 68 | self.arg_parser.add_argument( 69 | "--track_distance", 70 | type=float, default=0.0, 71 | help="Distance between tracks") 72 | self.arg_parser.add_argument( 73 | "--bm_diameter", 74 | type=float, default=0.0, 75 | help="Diameter of the encoder disk") 76 | self.arg_parser.add_argument( 77 | "--bm_hole_diameter", 78 | type=float, default=0.0, 79 | help="Diameter of the center hole") 80 | self.arg_parser.add_argument( 81 | "--bm_bits", 82 | default="", 83 | help="Bits of segments") 84 | self.arg_parser.add_argument( 85 | "--bm_outer_encoder_diameter", 86 | type=float, default=0.0, 87 | help="Diameter of the outer encoder disk") 88 | self.arg_parser.add_argument( 89 | "--bm_outer_encoder_width", 90 | type=float, default=0.0, 91 | help="Width of the outer encoder disk") 92 | self.arg_parser.add_argument( 93 | "--brgc_diameter", 94 | type=float, default=0.0, 95 | help="Diameter of the encoder disk") 96 | self.arg_parser.add_argument( 97 | "--stgc_diameter", 98 | type=float, default=0.0, 99 | help="Diameter of the encoder disk") 100 | self.arg_parser.add_argument( 101 | "--brgc_hole_diameter", 102 | type=float, default=0.0, 103 | help="Diameter of the center hole") 104 | self.arg_parser.add_argument( 105 | "--cutouts", 106 | type=int, default=1, 107 | help="Number of cutouts") 108 | self.arg_parser.add_argument( 109 | "--sensors", 110 | type=int, default=1, 111 | help="Number of sensors") 112 | self.arg_parser.add_argument( 113 | "--stgc_hole_diameter", 114 | type=float, default=0.0, 115 | help="Diameter of the center hole") 116 | self.arg_parser.add_argument( 117 | "--stgc_encoder_diameter", 118 | type=float, default=0.0, 119 | help="Outer diameter of the last track") 120 | self.arg_parser.add_argument( 121 | "--stgc_track_width", 122 | type=float, default=0.0, 123 | help="Width of track") 124 | 125 | # This function just concatenates the point and the command and returns 126 | # the data string 127 | def parse_path_data(self, command, point): 128 | 129 | path_data = command + ' %f ' % point[0] + ' %f ' % point[1] 130 | return path_data 131 | 132 | # Creates a gray code of size bits (n >= 1) in the format of a list 133 | def create_gray_code(self, bits): 134 | 135 | gray_code = [[False], [True]] 136 | 137 | if bits == 1: 138 | return gray_code 139 | 140 | for i in range(bits - 1): 141 | temp = [] 142 | # Reflect values 143 | for j in range(len(gray_code[0]), 0, -1): 144 | for k in range(0, len(gray_code)): 145 | if j == len(gray_code[0]): 146 | temp.append([gray_code[k][-j]]) 147 | else: 148 | temp[k].append(gray_code[k][-j]) 149 | while temp: 150 | gray_code.append(temp.pop()) 151 | # Add False to the "old" values and true to the new ones 152 | for j in range(0, len(gray_code)): 153 | if j < len(gray_code) / 2: 154 | gray_code[j].insert(0, False) 155 | else: 156 | gray_code[j].insert(0, True) 157 | temp = [] 158 | 159 | return gray_code 160 | 161 | # This function returns the segments for a gray encoder 162 | def draw_gray_encoder( 163 | self, line_style, bits, encoder_diameter, track_width, 164 | track_distance): 165 | gray_code = self.create_gray_code(bits) 166 | 167 | segments = [] 168 | segment_size = 0 169 | start_angle_position = 0 170 | index = 0 171 | current_encoder_diameter = encoder_diameter 172 | previous_item = False 173 | position_size = 360.0 / (2 ** bits) 174 | 175 | for i in range(len(gray_code[0]) - 1, -1, -1): 176 | for j in gray_code: 177 | if j[i]: 178 | segment_size += 1 179 | if segment_size == 1: 180 | start_angle_position = index 181 | previous_item = True 182 | elif not j[i] and previous_item: 183 | segment = self.draw_segment( 184 | line_style, 185 | start_angle_position * position_size, 186 | segment_size * position_size, 187 | current_encoder_diameter, 188 | track_width) 189 | segments.append(segment) 190 | 191 | segment_size = 0 192 | previous_item = False 193 | start_angle_position = 0 194 | 195 | index += 1 196 | 197 | if previous_item: 198 | segment = self.draw_segment( 199 | line_style, 200 | start_angle_position * position_size, 201 | segment_size * position_size, 202 | current_encoder_diameter, track_width) 203 | segments.append(segment) 204 | segment_size = 0 205 | previous_item = False 206 | start_angle_position = 0 207 | current_encoder_diameter -= (2 * track_distance + 2 * track_width) 208 | index = 0 209 | 210 | return segments 211 | 212 | # Check if there is too many cutouts compared to number of sensors 213 | def valid_single_track_gray_encoder(self, cutouts, sensors): 214 | if sensors < 6 and cutouts > 1: 215 | pass 216 | elif sensors <= 10 and cutouts > 2: 217 | pass 218 | elif sensors <= 16 and cutouts > 3: 219 | pass 220 | elif sensors <= 23 and cutouts > 4: 221 | pass 222 | elif sensors <= 36 and cutouts > 5: 223 | pass 224 | else: 225 | return True 226 | 227 | return False 228 | 229 | # This function returns the segments for a single-track gray encoder 230 | def draw_single_track_gray_encoder( 231 | self, line_style, cutouts, sensors, encoder_diameter, track_width): 232 | 233 | segments = [] 234 | resolution = 360.0 / (cutouts * 2 * sensors) 235 | current_angle = 0.0 236 | added_angle = ((2 * cutouts + 1) * resolution) 237 | for n in range(cutouts): 238 | current_segment_size = ((n * 2 + 2) * cutouts + 1) * resolution 239 | segments.append( 240 | self.draw_segment( 241 | line_style, current_angle, 242 | current_segment_size, 243 | encoder_diameter, track_width)) 244 | current_angle += added_angle + current_segment_size 245 | 246 | return segments 247 | 248 | def draw_label( 249 | self, group, angle, segment_angle, outer_diameter, labelNum): 250 | outer_radius = outer_diameter / 2 251 | label_angle = angle + (segment_angle / 2) 252 | point = calculate_point(label_angle, outer_radius) 253 | matrix = Transform('rotate(' + str(label_angle + 90) + ')').matrix 254 | matrix_str = str(matrix[0][0]) + "," + str(matrix[0][1]) 255 | matrix_str += "," + str(matrix[1][0]) 256 | matrix_str += "," + str(matrix[1][1]) + ",0,0" 257 | text = { 258 | 'id': 'text' + str(labelNum), 259 | 'style': 'font-size: 6px;font-style: normal;font-family: Sans', 260 | 'x': str(point[0]), 261 | 'y': str(point[1]), 262 | } 263 | textElement = etree.SubElement(group, inkex.addNS('text', 'svg'), text) 264 | textElement.text = string.printable[labelNum % len(string.printable)] 265 | 266 | self.svg.get_current_layer().append(textElement) 267 | 268 | # This function creates the path for one single segment 269 | def draw_segment( 270 | self, line_style, angle, segment_angle, outer_diameter, width): 271 | 272 | path = {'style': str(inkex.Style(line_style))} 273 | path['d'] = '' 274 | outer_radius = outer_diameter / 2 275 | 276 | # Go to the first point in the segment 277 | path['d'] += self.parse_path_data( 278 | 'M', calculate_point(angle, outer_radius - width)) 279 | 280 | # Go to the second point in the segment 281 | path['d'] += self.parse_path_data( 282 | 'L', calculate_point(angle, outer_radius)) 283 | 284 | # Go to the third point in the segment, draw an arc 285 | point = calculate_point(angle + segment_angle, outer_radius) 286 | path['d'] += self.parse_path_data('A', [outer_radius, outer_radius]) 287 | path['d'] += '0 0 1' + self.parse_path_data(' ', point) 288 | 289 | # Go to the fourth point in the segment 290 | point = calculate_point(angle + segment_angle, outer_radius - width) 291 | path['d'] += self.parse_path_data('L', point) 292 | 293 | # Go to the beginning in the segment, draw an arc 294 | point = calculate_point(angle, outer_radius - width) 295 | # 'Z' closes the path 296 | path['d'] += (self.parse_path_data( 297 | 'A', 298 | [outer_radius - width, outer_radius - width]) + 299 | '0 0 0' + self.parse_path_data(' ', point) + ' Z') 300 | 301 | # Return the path 302 | return path 303 | 304 | # This function adds an element to the document 305 | def add_element(self, element_type, group, element_attributes): 306 | etree.SubElement( 307 | group, inkex.addNS(element_type, 'svg'), 308 | element_attributes) 309 | 310 | def draw_circles(self, hole_diameter, diameter): 311 | # Attributes for the center hole, if diameter is larger than 0, 312 | # create it 313 | circle_elements = [] 314 | attributes = { 315 | 'style': str(inkex.Style({'stroke': 'none', 'fill': 'black'})), 316 | 'r': str(hole_diameter / 2) 317 | } 318 | if self.options.hole_diameter > 0: 319 | circle_elements.append(attributes) 320 | 321 | # Attributes for the guide hole in the center hole, then create it 322 | attributes = { 323 | 'style': str(inkex.Style( 324 | {'stroke': 'white', 'fill': 'white', 'stroke-width': '0.1'})), 325 | 'r': '1' 326 | } 327 | circle_elements.append(attributes) 328 | 329 | # Attributes for the outer rim, then create it 330 | attributes = { 331 | 'style': str(inkex.Style( 332 | {'stroke': 'black', 'stroke-width': '1', 'fill': 'none'})), 333 | 'r': str(diameter / 2) 334 | } 335 | if self.options.diameter > 0: 336 | circle_elements.append(attributes) 337 | 338 | return circle_elements 339 | 340 | def draw_common_circles(self, group, diameter, hole_diameter): 341 | circle_elements = self.draw_circles(hole_diameter, diameter) 342 | 343 | for circle in circle_elements: 344 | self.add_element('circle', group, circle) 345 | 346 | # Binary reflected gray code encoder 347 | def effect_brgc(self, group, line_style, diameter, hole_diameter): 348 | center_hole_r = self.options.brgc_hole_diameter / 2 349 | diameter = self.options.encoder_diameter / 2 350 | encoder_width = self.options.bits * self.options.track_width 351 | encoder_width += (self.options.bits - 1) * self.options.track_distance 352 | if ((diameter - encoder_width) < center_hole_r): 353 | inkex.errormsg("Innermost encoder smaller than the center hole!") 354 | else: 355 | segments = self.draw_gray_encoder( 356 | line_style, self.options.bits, 357 | self.options.encoder_diameter, 358 | self.options.track_width, 359 | self.options.track_distance) 360 | for item in segments: 361 | self.add_element('path', group, item) 362 | 363 | self.draw_common_circles(group, diameter, hole_diameter) 364 | 365 | # Single track gray code encoder 366 | def effect_stgc(self, group, line_style, diameter, hole_diameter): 367 | encoder_r = self.options.stgc_encoder_diameter / 2 368 | hole_r = self.options.stgc_hole_diameter / 2 369 | if ((encoder_r - self.options.stgc_track_width) < hole_r): 370 | inkex.errormsg("Encoder smaller than the center hole!") 371 | elif not self.valid_single_track_gray_encoder( 372 | self.options.cutouts, self.options.sensors): 373 | inkex.errormsg("Too many cutouts compared to number of sensors!") 374 | else: 375 | segments = self.draw_single_track_gray_encoder( 376 | line_style, self.options.cutouts, self.options.sensors, 377 | self.options.stgc_encoder_diameter, 378 | self.options.stgc_track_width) 379 | for item in segments: 380 | self.add_element('path', group, item) 381 | 382 | self.draw_common_circles(group, diameter, hole_diameter) 383 | 384 | # Regular rotary encoder 385 | def effect_rotary_encoder( 386 | self, group, line_style, diameter, hole_diameter): 387 | # Angle of one single segment 388 | segment_angle = 360.0 / (self.options.segments * 2) 389 | 390 | for segment_number in range(0, self.options.segments): 391 | angle = segment_number * (segment_angle * 2) 392 | outer_r = self.options.outer_encoder_diameter / 2 393 | if (self.options.outer_encoder_width > 0 394 | and self.options.outer_encoder_diameter > 0 395 | and outer_r > self.options.outer_encoder_width): 396 | 397 | segment = self.draw_segment( 398 | line_style, angle, segment_angle, 399 | self.options.outer_encoder_diameter, 400 | self.options.outer_encoder_width) 401 | self.add_element('path', group, segment) 402 | 403 | # If the inner encoder diameter is something else than 0; create it 404 | inner_r = self.options.inner_encoder_diameter / 2 405 | if (self.options.outer_encoder_width > 0 406 | and self.options.inner_encoder_diameter > 0 407 | and inner_r > self.options.inner_encoder_width): 408 | 409 | # The inner encoder must be half an encoder segment ahead of 410 | # the outer one 411 | segment = self.draw_segment( 412 | line_style, angle + (segment_angle / 2), segment_angle, 413 | self.options.inner_encoder_diameter, 414 | self.options.inner_encoder_width) 415 | 416 | self.add_element('path', group, segment) 417 | 418 | self.draw_common_circles(group, diameter, hole_diameter) 419 | 420 | # Bitmap encoder 421 | def effect_bitmap_encoder( 422 | self, group, line_style, diameter, hole_diameter): 423 | bits = self.options.bm_bits 424 | bm_segments = len(bits) 425 | # Angle of one single segment 426 | segment_angle = 360.0 / bm_segments 427 | 428 | for segment_number in range(0, bm_segments): 429 | 430 | angle = segment_number * segment_angle 431 | 432 | if (self.options.bm_outer_encoder_width > 0 433 | and self.options.bm_outer_encoder_diameter > 0 434 | and self.options.bm_outer_encoder_diameter 435 | > self.options.bm_outer_encoder_width): 436 | 437 | self.draw_label( 438 | group, angle, segment_angle, self.options.bm_diameter, 439 | segment_number) 440 | # Drawing only the black segments 441 | if (bits[segment_number] == '1'): 442 | segment = self.draw_segment( 443 | line_style, angle, 444 | segment_angle, 445 | self.options.bm_outer_encoder_diameter, 446 | self.options.bm_outer_encoder_width) 447 | 448 | self.add_element('path', group, segment) 449 | 450 | self.draw_common_circles(group, diameter, hole_diameter) 451 | 452 | def effect(self): 453 | # Put all the elements in a group, center set in the middle of the view 454 | group = etree.SubElement(self.svg.get_current_layer(), 'g', { 455 | inkex.addNS('label', 'inkscape'): 'Encoder disk', 456 | 'transform': 'translate' + str(self.svg.namedview.center) 457 | }) 458 | 459 | # Line style for the encoder segments 460 | line_style = { 461 | 'stroke': 'white', 462 | 'stroke-width': '0', 463 | 'fill': 'black' 464 | } 465 | 466 | if self.options.tab == "brgc": 467 | self.effect_brgc( 468 | group, line_style, self.options.brgc_diameter, 469 | self.options.brgc_hole_diameter) 470 | 471 | if self.options.tab == "stgc": 472 | self.effect_stgc( 473 | group, line_style, self.options.stgc_diameter, 474 | self.options.stgc_hole_diameter) 475 | 476 | if self.options.tab == "rotary_enc": 477 | self.effect_rotary_encoder( 478 | group, line_style, self.options.diameter, 479 | self.options.hole_diameter) 480 | 481 | if self.options.tab == "bitmap_enc": 482 | self.effect_bitmap_encoder( 483 | group, line_style, self.options.bm_diameter, 484 | self.options.bm_hole_diameter) 485 | 486 | 487 | if __name__ == '__main__': 488 | EncoderDiskGenerator().run() 489 | -------------------------------------------------------------------------------- /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hyvok/Inkscape-rotary-encoder-disk-generator/0c5cca22e75d865b820c3756323e173c54035556/images/screenshot.png --------------------------------------------------------------------------------