├── README.md ├── tests.html ├── TestOpGenerator.js ├── PostProcessorTestDriver.js └── GlowforgeColorific.cps /README.md: -------------------------------------------------------------------------------- 1 | # glowforge-colorific-fusion360-post 2 | 3 | ## What is this? 4 | This is a Post (https://cam.autodesk.com/hsmposts) for Fusion 360 and Autodesk HSM that produces SVG files from CAM operations that can be loaded intot he Glowforge App. 5 | This work is based on the original Post provided by Autodesk and as such their copyright remains. 6 | 7 | ## Goal 8 | The goal of this post is to eliminate the need to perform any post-processing of the SVG in Inkscape or another SVG package. Several alternatives exist but none of them produce a properly formed SVG file and they all require processing in some way, weather to assign colors, join paths or make solids out of empty shells. 9 | 10 | ## Improvements 11 | * Produce propperly formed SVG paths that remain connected in the Glowforge App. 12 | * Automatically color each CAM op with a unique color so Glowforge App imports each one as a seperate steps. 13 | * Steps are ordered exactly as CAM ops are ordering in Fusion 360. This is acheived by sorting the color values used to work with Glowforge Apps internal ordering. 14 | * Automatic color generation for large numbers of operations (think power & speed test patterns). 15 | * Support engraving with filled shapes. This is configurable in each CAM Op so cuts and engraves can be mixed in the same setup. 16 | * Automatically detect the selected Stock Point of the CAM setup and compensate for this so the SVG content is not off screen. 17 | * Add an option to compensate for inverted Z axis in the setup. 18 | * Support centering the SVG content in a frame the size of the Glowforge bed (size is configurable) 19 | * Add comments to and names and IDs to SVG elements. Each op is numbered with a unique ID. Comments are supported as HTML comments. 20 | * Support centering the design in the machine workspace with the 'Use Work Area' option. 21 | * Show a red error box if your design is larger than the machine workspace size when 'Use Work Area' is on. 22 | 23 | ## Wishlist 24 | * Scoring support - this is no currently officially suppoorted by Glowforge. Dashed lines seem like the obvious choice but thye produce an error. 25 | * Feeds & Speeds for custom materials - Ideally this could vary by opperation and would be a part of the Tool used in the CAM operation. Needs support Glowforge App. 26 | 27 | # License 28 | Copyright (C) 2018 by Autodesk, Inc. 29 | All rights reserved. 30 | 31 | This work is based on an original work by Autodesk, Inc. Ths work is provided *Gratis* but *not Libre*, see: https://en.wikipedia.org/wiki/Gratis_versus_libre 32 | This was developed for my own personal use and posted so that others (including Autodesk, Inc.) might benefit from this effort. 33 | 34 | # Documentation 35 | TK 36 | -------------------------------------------------------------------------------- /tests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Glowforge Colorific Tests 5 | 6 | 7 | 8 | 13 | 14 | 15 |

Glowforge Colorific Tests

16 | 17 | 18 |
19 |

Auto Stock Point Detection Test

20 |

Description: Test all 9 possible Y/X stock point selections. Auto Stock Point detection should produce SVGs that are centered in the canvas.

21 |

Expected: A grid of 9 identical arrows in 3 rows. The arrows should be solid filled, centered and pointing up.

22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
39 |
40 | 41 |
42 |

Imperial Units Test

43 |

Description: Test all 9 possible Y/X stock point selections where the model and stock are in Inches. Auto Stock Point detection should produce SVGs that are centered in the canvas.

44 |

Expected: A grid of 9 identical arrows in 3 rows. The arrows should be solid filled, centered and pointing up.

45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
62 |
63 | 64 |
65 |

Use Work Area Test

66 |

Description: Test all 9 possible Y/X stock point selections in combination with a custom machine work area. Auto Stock Point detection should produce SVGs that are centered in the custom machine work area. The work area is configured as 20mm x 20mm.

67 |

Expected: A grid of 9 identical arrows in 3 rows. The arrows should be solid filled, centered and pointing up.

68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 |
85 |
86 | 87 |
88 |

Violate Work Area Test

89 |

Description: The design can be larger than the specified Machine Work Area in X or Y. The work area is configured as 20mm x 20mm.

90 |

Expected: 2 cells with blue filled rectangles. A Red box with an extra thick line representes the specified machine area demonstating how the design wont fit.

91 | 92 | 93 | 94 | 95 | 96 |
97 |
98 | 99 |
100 |

Color Tests

101 |

Description: Designs can have a large number of operations. Each Op should get a unique color in ascending hex order.

102 |

Expected: 6 colored boxes with the default color set (the min color set size).

103 |
104 |

Expected: 15 colored boxes (the complete pre-defined color set).

105 |
106 |

Expected: 50 colored boxes (number suggested by the customer as 'enough').

107 |
108 |

Expected: 120 colored boxes (15 color set * 8).

109 |
110 |

Expected: 256 colored boxes (because powers of 2 are fun).

111 |
112 |

Expected: 1024 colored boxes (because powers of 2 are fun, #extra edition!).

113 |
114 |
115 | 116 | -------------------------------------------------------------------------------- /TestOpGenerator.js: -------------------------------------------------------------------------------- 1 | // draws a box, clockwise, starting in the upper left corner 2 | function drawArrow(x, y, scale) { 3 | scale = scale ? scale : 1; 4 | return [ 5 | move(x, y), 6 | 7 | // arrow 8 | move(x += 5 * scale, y), 9 | cut(x += 5 * scale, y -= 4 * scale), 10 | cut(x, y -= 2 * scale), 11 | cut(x -= 3 * scale, y), 12 | cut(x, y -= 4 * scale), 13 | 14 | cut(x -= 4 * scale, y), 15 | 16 | cut(x, y += 4 * scale), 17 | cut(x -= 3 * scale, y), 18 | cut(x, y += 2 * scale), 19 | cut(x += 5 * scale, y += 4 * scale), 20 | move(x, y), 21 | ] 22 | } 23 | 24 | // draws a box, clockwise, starting in the upper left corner 25 | function drawBoxWithSize(x, y, width, height) { 26 | return [ 27 | move(x, y), 28 | cut(x += width, y), 29 | cut(x, y -= height), 30 | cut(x -= width, y), 31 | cut(x, y += height), 32 | ] 33 | } 34 | 35 | function drawBox(x, y) { 36 | return drawBoxWithSize(x, y, 10, 10); 37 | } 38 | 39 | function autoStockPointTest(lowerLeftX, lowerLeftY, upperRightX, upperRightY, targetId) { 40 | // lower left, upper right, origin vector 41 | return newTest(new Vector(lowerLeftX, lowerLeftY), new Vector(upperRightX, upperRightY), new Vector(0, 0), targetId) 42 | .withSection(JET_MODE_ETCHING, {}, drawArrow(lowerLeftX, upperRightY)); 43 | } 44 | 45 | function autoStockPointInchesTest(lowerLeftX, lowerLeftY, upperRightX, upperRightY, targetId) { 46 | var mmToInches = 0.0393701; 47 | 48 | // lower left, upper right, origin vector 49 | return newTest(new Vector(lowerLeftX * mmToInches, lowerLeftY * mmToInches), 50 | new Vector(upperRightX * mmToInches, upperRightY * mmToInches), 51 | new Vector(0, 0), targetId) 52 | .withUnits(IN) 53 | .withSection(JET_MODE_ETCHING, {}, drawArrow(lowerLeftX * mmToInches, upperRightY * mmToInches, mmToInches)); 54 | } 55 | 56 | function useWorkAreaTest(lowerLeftX, lowerLeftY, upperRightX, upperRightY, targetId) { 57 | // lower left, upper right, origin vector 58 | return newTest(new Vector(lowerLeftX, lowerLeftY), new Vector(upperRightX, upperRightY), new Vector(0, 0), targetId) 59 | .withSection(JET_MODE_ETCHING, {}, drawArrow(lowerLeftX, upperRightY)) 60 | .withProperties({useWorkArea: true, 61 | workAreaWidth: 20, 62 | workAreaHeight: 20}); 63 | } 64 | 65 | function violateWorkAreaTest(boxWidth, boxHeight, targetId) { 66 | // lower left, upper right, origin vector 67 | return newTest(new Vector(0, 0), new Vector(boxWidth, boxHeight), new Vector(0, 0), targetId) 68 | .withSection(JET_MODE_ETCHING, {}, drawBoxWithSize(0, boxHeight, boxWidth, boxHeight)) 69 | .withSection(JET_MODE_ETCHING, {}, drawArrow(0, boxHeight)) 70 | .withProperties({useWorkArea: true, 71 | workAreaWidth: 20, 72 | workAreaHeight: 20}); 73 | } 74 | 75 | function colorTest(colorCount) { 76 | var columns = 15; 77 | var width = Math.ceil(13 * columns) - 3; 78 | var height = Math.ceil(13 * Math.ceil(colorCount / columns)) - 3; 79 | 80 | // lower left, upper right, origin vector 81 | var element = "colors-" + colorCount; 82 | var test = newTest(new Vector(0, 0), new Vector(width, height), new Vector(0, 0), element); 83 | 84 | var x = 0, y = height; 85 | 86 | var rows = 0; 87 | var j = 0; 88 | for (var i = 0; i < colorCount; i ++) { 89 | test.withSection(JET_MODE_ETCHING, {}, drawBox(x, y)); 90 | x += 13; 91 | j++; 92 | if (j >= columns) { 93 | j = 0; 94 | x = 0; 95 | y -= 13; 96 | rows++; 97 | //move(0, 10 - (rows * (13))); 98 | 99 | } 100 | } 101 | 102 | test.validate = function () { 103 | pathElements = document.getElementById(element).getElementsByTagName("path"); 104 | colorMap = {}; 105 | for (var i = 0; i < pathElements.length; i++) { 106 | path = pathElements[i]; 107 | var fill = path.getAttribute("fill"); 108 | if (!colorMap[fill]) { 109 | colorMap[fill] = true; 110 | } else { 111 | return "Duplicate colors detected at index: " + i + " color: " + fill; 112 | } 113 | } 114 | } 115 | 116 | return test; 117 | } 118 | 119 | (function runAllTests() { 120 | var tests = []; 121 | 122 | // Auto Stock Point 123 | tests.push(autoStockPointTest(0, -10, 10, 0, 'auto-stock-point-upper-left')); 124 | tests.push(autoStockPointTest(-5, -10, 5, 0, 'auto-stock-point-upper-middle')); 125 | tests.push(autoStockPointTest(-10, -10, 0, 0, 'auto-stock-point-upper-right')); 126 | tests.push(autoStockPointTest(0, -5, 10, 5, 'auto-stock-point-middle-left')); 127 | tests.push(autoStockPointTest(-5, -5, 5, 5, 'auto-stock-point-middle-middle')); 128 | tests.push(autoStockPointTest(-10, -5, 0, 5, 'auto-stock-point-middle-right')); 129 | tests.push(autoStockPointTest(0, 0, 10, 10, 'auto-stock-point-lower-left')); 130 | tests.push(autoStockPointTest(-5, 0, 5, 10, 'auto-stock-point-lower-middle')); 131 | tests.push(autoStockPointTest(-10, 0, 0, 10, 'auto-stock-point-lower-right')); 132 | 133 | // Auto Stock Point 134 | tests.push(autoStockPointInchesTest(0, -10, 10, 0, 'auto-stock-point-in-upper-left')); 135 | tests.push(autoStockPointInchesTest(-5, -10, 5, 0, 'auto-stock-point-in-upper-middle')); 136 | tests.push(autoStockPointInchesTest(-10, -10, 0, 0, 'auto-stock-point-in-upper-right')); 137 | tests.push(autoStockPointInchesTest(0, -5, 10, 5, 'auto-stock-point-in-middle-left')); 138 | tests.push(autoStockPointInchesTest(-5, -5, 5, 5, 'auto-stock-point-in-middle-middle')); 139 | tests.push(autoStockPointInchesTest(-10, -5, 0, 5, 'auto-stock-point-in-middle-right')); 140 | tests.push(autoStockPointInchesTest(0, 0, 10, 10, 'auto-stock-point-in-lower-left')); 141 | tests.push(autoStockPointInchesTest(-5, 0, 5, 10, 'auto-stock-point-in-lower-middle')); 142 | tests.push(autoStockPointInchesTest(-10, 0, 0, 10, 'auto-stock-point-in-lower-right')); 143 | 144 | // use work area with auto stock point detection 145 | tests.push(useWorkAreaTest(0, -10, 10, 0, 'use-work-area-upper-left')); 146 | tests.push(useWorkAreaTest(-5, -10, 5, 0, 'use-work-area-upper-middle')); 147 | tests.push(useWorkAreaTest(-10, -10, 0, 0, 'use-work-area-upper-right')); 148 | tests.push(useWorkAreaTest(0, -5, 10, 5, 'use-work-area-middle-left')); 149 | tests.push(useWorkAreaTest(-5, -5, 5, 5, 'use-work-area-middle-middle')); 150 | tests.push(useWorkAreaTest(-10, -5, 0, 5, 'use-work-area-middle-right')); 151 | tests.push(useWorkAreaTest(0, 0, 10, 10, 'use-work-area-lower-left')); 152 | tests.push(useWorkAreaTest(-5, 0, 5, 10, 'use-work-area-lower-middle')); 153 | tests.push(useWorkAreaTest(-10, 0, 0, 10, 'use-work-area-lower-right')); 154 | 155 | // work area too small 156 | tests.push(violateWorkAreaTest(50, 10, 'violate-work-area-width')); 157 | tests.push(violateWorkAreaTest(10, 50, 'violate-work-area-height')); 158 | 159 | // color tests 160 | tests.push(colorTest(6)); 161 | tests.push(colorTest(15)); 162 | tests.push(colorTest(50)); 163 | tests.push(colorTest(120)); 164 | tests.push(colorTest(256)); 165 | tests.push(colorTest(1024)); 166 | 167 | for (var i = 0; i < tests.length; i++) { 168 | var test = tests[i]; 169 | test.run(); 170 | var result = test.validate(); 171 | if (result) { 172 | console.error(result); 173 | } 174 | } 175 | })(); -------------------------------------------------------------------------------- /PostProcessorTestDriver.js: -------------------------------------------------------------------------------- 1 | // all the constants that you get access to (They arent all used but they were easy to get...) 2 | var NUL = "\u0000", 3 | SOH = "\u0001", 4 | STX = "\u0002", 5 | ETX = "\u0003", 6 | EOT = "\u0004", 7 | ENQ = "\u0005", 8 | ACK = "\u0006", 9 | BEL = "\u0007", 10 | BS = "\b", 11 | TAB = "\t", 12 | LF = "\n", 13 | VT = "\u000b", 14 | FF = "\f", 15 | CR = "\r", 16 | SO = "\u000e", 17 | SI = "\u000f", 18 | DLE = "\u0010", 19 | DC1 = "\u0011", 20 | DC2 = "\u0012", 21 | DC3 = "\u0013", 22 | DC4 = "\u0014", 23 | NAK = "\u0015", 24 | SYN = "\u0016", 25 | ETB = "\u0017", 26 | CAN = "\u0018", 27 | EM = "\u0019", 28 | SUB = "\u001a", 29 | ESC = "\u001b", 30 | FS = "\u001c", 31 | GS = "\u001d", 32 | RS = "\u001e", 33 | US = "\u001f", 34 | EOL = "\r\n", 35 | SP = " ", 36 | PATH_SEPARATOR = "/", 37 | IN = 0, 38 | MM = 1, 39 | ORIGINAL_UNIT = -1, 40 | DEG = 57.29577951308232, 41 | PLANE_XY = 0, 42 | PLANE_ZX = 1, 43 | PLANE_YZ = 2, 44 | X = 0, 45 | Y = 1, 46 | Z = 2, 47 | TYPE_MILLING = 0, 48 | TYPE_TURNING = 1, 49 | TYPE_WIRE = 2, 50 | TYPE_JET = 3, 51 | TYPE_ADDITIVE = 4, 52 | CAPABILITY_MILLING = 1, 53 | CAPABILITY_TURNING = 2, 54 | CAPABILITY_WIRE = 4, 55 | CAPABILITY_SETUP_SHEET = 8, 56 | CAPABILITY_INTERMEDIATE = 16, 57 | CAPABILITY_JET = 32, 58 | CAPABILITY_CASCADING = 64, 59 | CAPABILITY_ADDITIVE = 128, 60 | JET_MODE_THROUGH = 0, 61 | JET_MODE_ETCHING = 1, 62 | JET_MODE_VAPORIZE = 2, 63 | SPINDLE_PRIMARY = 0, 64 | SPINDLE_SECONDARY = 1, 65 | FEED_PER_MINUTE = 0, 66 | FEED_PER_REVOLUTION = 1, 67 | SPINDLE_CONSTANT_SPINDLE_SPEED = 0, 68 | SPINDLE_CONSTANT_SURFACE_SPEED = 1, 69 | TOOL_AXIS_X = 0, 70 | TOOL_AXIS_Y = 1, 71 | TOOL_AXIS_Z = 2, 72 | RADIUS_COMPENSATION_OFF = 0, 73 | RADIUS_COMPENSATION_LEFT = 1, 74 | RADIUS_COMPENSATION_RIGHT = 2, 75 | SINGULARITY_LINEARIZE_OFF = 0, 76 | SINGULARITY_LINEARIZE_LINEAR = 1, 77 | SINGULARITY_LINEARIZE_ROTARY = 2, 78 | COOLANT_OFF = 0, 79 | COOLANT_FLOOD = 1, 80 | COOLANT_MIST = 2, 81 | COOLANT_TOOL = 3, 82 | COOLANT_THROUGH_TOOL = 3, 83 | COOLANT_AIR = 4, 84 | COOLANT_AIR_THROUGH_TOOL = 5, 85 | COOLANT_SUCTION = 6, 86 | COOLANT_FLOOD_MIST = 7, 87 | COOLANT_FLOOD_THROUGH_TOOL = 8, 88 | EULER_XZX_S = 0, 89 | EULER_YXY_S = 1, 90 | EULER_ZYZ_S = 2, 91 | EULER_XZX_R = 3, 92 | EULER_YXY_R = 4, 93 | EULER_ZYZ_R = 5, 94 | EULER_XZY_S = 6, 95 | EULER_YXZ_S = 7, 96 | EULER_ZYX_S = 8, 97 | EULER_YZX_R = 9, 98 | EULER_ZXY_R = 10, 99 | EULER_XYZ_R = 11, 100 | EULER_XYX_S = 12, 101 | EULER_YZY_S = 13, 102 | EULER_ZXZ_S = 14, 103 | EULER_XYX_R = 15, 104 | EULER_YZY_R = 16, 105 | EULER_ZXZ_R = 17, 106 | EULER_XYZ_S = 18, 107 | EULER_YZX_S = 19, 108 | EULER_ZXY_S = 20, 109 | EULER_ZYX_R = 21, 110 | EULER_XZY_R = 22, 111 | EULER_YXZ_R = 23, 112 | MATERIAL_UNSPECIFIED = 0, 113 | MATERIAL_HSS = 1, 114 | MATERIAL_TI_COATED = 2, 115 | MATERIAL_CARBIDE = 3, 116 | MATERIAL_CERAMICS = 4, 117 | TOOL_UNSPECIFIED = 0, 118 | TOOL_DRILL = 1, 119 | TOOL_DRILL_CENTER = 2, 120 | TOOL_DRILL_SPOT = 3, 121 | TOOL_DRILL_BLOCK = 4, 122 | TOOL_MILLING_END_FLAT = 5, 123 | TOOL_MILLING_END_BALL = 6, 124 | TOOL_MILLING_END_BULLNOSE = 7, 125 | TOOL_MILLING_CHAMFER = 8, 126 | TOOL_MILLING_FACE = 9, 127 | TOOL_MILLING_SLOT = 10, 128 | TOOL_MILLING_RADIUS = 11, 129 | TOOL_MILLING_DOVETAIL = 12, 130 | TOOL_MILLING_TAPERED = 13, 131 | TOOL_MILLING_LOLLIPOP = 14, 132 | TOOL_TAP_RIGHT_HAND = 15, 133 | TOOL_TAP_LEFT_HAND = 16, 134 | TOOL_REAMER = 17, 135 | TOOL_BORING_BAR = 18, 136 | TOOL_COUNTER_BORE = 19, 137 | TOOL_COUNTER_SINK = 20, 138 | TOOL_HOLDER_ONLY = 21, 139 | TOOL_TURNING_GENERAL = 22, 140 | TOOL_TURNING_THREADING = 23, 141 | TOOL_TURNING_GROOVING = 24, 142 | TOOL_TURNING_BORING = 25, 143 | TOOL_TURNING_CUSTOM = 26, 144 | TOOL_PROBE = 27, 145 | TOOL_WIRE = 28, 146 | TOOL_WATER_JET = 29, 147 | TOOL_LASER_CUTTER = 30, 148 | TOOL_WELDER = 31, 149 | TOOL_GRINDER = 32, 150 | TOOL_MILLING_FORM = 33, 151 | TOOL_ROTARY_BROACH = 34, 152 | TOOL_SLOT_BROACH = 35, 153 | TOOL_PLASMA_CUTTER = 36, 154 | TOOL_MARKER = 37, 155 | TOOL_MILLING_THREAD = 38, 156 | TOOL_COMPENSATION_INSERT_CENTER = 0, 157 | TOOL_COMPENSATION_TIP = 1, 158 | TOOL_COMPENSATION_TIP_CENTER = 2, 159 | TOOL_COMPENSATION_TIP_TANGENT = 3, 160 | TURNING_INSERT_USER_DEFINED = 0, 161 | TURNING_INSERT_ISO_A = 1, 162 | TURNING_INSERT_ISO_B = 2, 163 | TURNING_INSERT_ISO_C = 3, 164 | TURNING_INSERT_ISO_D = 4, 165 | TURNING_INSERT_ISO_E = 5, 166 | TURNING_INSERT_ISO_H = 6, 167 | TURNING_INSERT_ISO_K = 7, 168 | TURNING_INSERT_ISO_L = 8, 169 | TURNING_INSERT_ISO_M = 9, 170 | TURNING_INSERT_ISO_O = 10, 171 | TURNING_INSERT_ISO_P = 11, 172 | TURNING_INSERT_ISO_R = 12, 173 | TURNING_INSERT_ISO_S = 13, 174 | TURNING_INSERT_ISO_T = 14, 175 | TURNING_INSERT_ISO_V = 15, 176 | TURNING_INSERT_ISO_W = 16, 177 | TURNING_INSERT_GROOVE_ROUND = 17, 178 | TURNING_INSERT_GROOVE_RADIUS = 18, 179 | TURNING_INSERT_GROOVE_SQUARE = 19, 180 | TURNING_INSERT_GROOVE_CHAMFER = 20, 181 | TURNING_INSERT_GROOVE_40DEG = 21, 182 | TURNING_INSERT_THREAD_ISO_DOUBLE_FULL = 22, 183 | TURNING_INSERT_THREAD_ISO_TRIPLE_FULL = 23, 184 | TURNING_INSERT_THREAD_UTS_DOUBLE_FULL = 24, 185 | TURNING_INSERT_THREAD_UTS_TRIPLE_FULL = 25, 186 | TURNING_INSERT_THREAD_ISO_DOUBLE_VPROFILE = 26, 187 | TURNING_INSERT_THREAD_ISO_TRIPLE_VPROFILE = 27, 188 | TURNING_INSERT_THREAD_UTS_DOUBLE_VPROFILE = 28, 189 | TURNING_INSERT_THREAD_UTS_TRIPLE_VPROFILE = 29, 190 | HOLDER_NONE = 0, 191 | HOLDER_ISO_A = 1, 192 | HOLDER_ISO_B = 2, 193 | HOLDER_ISO_C = 3, 194 | HOLDER_ISO_D = 4, 195 | HOLDER_ISO_E = 5, 196 | HOLDER_ISO_F = 6, 197 | HOLDER_ISO_G = 7, 198 | HOLDER_ISO_H = 8, 199 | HOLDER_ISO_J = 9, 200 | HOLDER_ISO_K = 10, 201 | HOLDER_ISO_L = 11, 202 | HOLDER_ISO_M = 12, 203 | HOLDER_ISO_N = 13, 204 | HOLDER_ISO_P = 14, 205 | HOLDER_ISO_Q = 15, 206 | HOLDER_ISO_R = 16, 207 | HOLDER_ISO_S = 17, 208 | HOLDER_ISO_T = 18, 209 | HOLDER_ISO_U = 19, 210 | HOLDER_ISO_V = 20, 211 | HOLDER_ISO_W = 21, 212 | HOLDER_ISO_Y = 22, 213 | HOLDER_OFFSET_PROFILE = 23, 214 | HOLDER_STRAIGHT_PROFILE = 24, 215 | HOLDER_GROOVE_EXTERNAL = 25, 216 | HOLDER_GROOVE_INTERNAL = 26, 217 | HOLDER_GROOVE_FACE = 27, 218 | HOLDER_THREAD_STRAIGHT = 28, 219 | HOLDER_THREAD_OFFSET = 29, 220 | HOLDER_THREAD_FACE = 30, 221 | HOLDER_BORING_BAR_ISO_F = 31, 222 | HOLDER_BORING_BAR_ISO_G = 32, 223 | HOLDER_BORING_BAR_ISO_J = 33, 224 | HOLDER_BORING_BAR_ISO_K = 34, 225 | HOLDER_BORING_BAR_ISO_L = 35, 226 | HOLDER_BORING_BAR_ISO_P = 36, 227 | HOLDER_BORING_BAR_ISO_Q = 37, 228 | HOLDER_BORING_BAR_ISO_S = 38, 229 | HOLDER_BORING_BAR_ISO_U = 39, 230 | HOLDER_BORING_BAR_ISO_W = 40, 231 | HOLDER_BORING_BAR_ISO_Y = 41, 232 | HOLDER_BORING_BAR_ISO_X = 42, 233 | HAS_PARAMETER = 1, 234 | HAS_RAPID = 2, 235 | HAS_LINEAR = 4, 236 | HAS_RAPID_5D = 8, 237 | HAS_LINEAR_5D = 16, 238 | HAS_MULTIAXIS = 24, 239 | HAS_DWELL = 32, 240 | HAS_CIRCULAR_CW = 64, 241 | HAS_CIRCULAR_CCW = 128, 242 | HAS_CIRCULAR = 192, 243 | HAS_CYCLE = 256, 244 | HAS_WELL_KNOWN_COMMAND = 512, 245 | HAS_MACHINE_COMMAND = 1024, 246 | HAS_SPINDLE_SPEED = 2048, 247 | HAS_COOLANT = 4096, 248 | HAS_SPLINE = 8192, 249 | HAS_COMMENT = 16384, 250 | RECORD_INVALID = 0, 251 | RECORD_TOOL_CHANGE = 1, 252 | RECORD_WELL_KNOWN_COMMAND = 2, 253 | RECORD_MACHINE_COMMAND = 3, 254 | RECORD_SPINDLE_SPEED = 4, 255 | RECORD_COOLANT = 5, 256 | RECORD_PARAMETER = 6, 257 | RECORD_LINEAR = 7, 258 | RECORD_LINEAR_5D = 8, 259 | RECORD_LINEAR_ZXN = 9, 260 | RECORD_CIRCULAR = 10, 261 | RECORD_SPLINE = 11, 262 | RECORD_BEZIER = 12, 263 | RECORD_NURBS = 13, 264 | RECORD_DWELL = 14, 265 | RECORD_CYCLE = 15, 266 | RECORD_CYCLE_OFF = 16, 267 | RECORD_COMMENT = 17, 268 | RECORD_WIDE_COMMENT = 18, 269 | RECORD_PASS_THROUGH = 19, 270 | RECORD_WIDE_PASS_THROUGH = 20, 271 | RECORD_SKIP = 21, 272 | RECORD_OPERATION = 22, 273 | RECORD_OPERATION_END = 23, 274 | COMMAND_STOP = 1, 275 | COMMAND_OPTIONAL_STOP = 2, 276 | COMMAND_END = 3, 277 | COMMAND_SPINDLE_CLOCKWISE = 4, 278 | COMMAND_SPINDLE_COUNTERCLOCKWISE = 5, 279 | COMMAND_START_SPINDLE = 6, 280 | COMMAND_STOP_SPINDLE = 7, 281 | COMMAND_ORIENTATE_SPINDLE = 8, 282 | COMMAND_LOAD_TOOL = 9, 283 | COMMAND_COOLANT_ON = 10, 284 | COMMAND_COOLANT_OFF = 11, 285 | COMMAND_ACTIVATE_SPEED_FEED_SYNCHRONIZATION = 12, 286 | COMMAND_DEACTIVATE_SPEED_FEED_SYNCHRONIZATION = 13, 287 | COMMAND_ACTIVATE_SPEED_FEED_SYNCHORNIZATION = 12, 288 | COMMAND_DEACTIVATE_SPEED_FEED_SYNCHORNIZATION = 13, 289 | COMMAND_LOCK_MULTI_AXIS = 14, 290 | COMMAND_UNLOCK_MULTI_AXIS = 15, 291 | COMMAND_EXACT_STOP = 16, 292 | COMMAND_START_CHIP_TRANSPORT = 17, 293 | COMMAND_STOP_CHIP_TRANSPORT = 18, 294 | COMMAND_OPEN_DOOR = 19, 295 | COMMAND_CLOSE_DOOR = 20, 296 | COMMAND_BREAK_CONTROL = 21, 297 | COMMAND_TOOL_MEASURE = 22, 298 | COMMAND_CALIBRATE = 23, 299 | COMMAND_VERIFY = 24, 300 | COMMAND_CLEAN = 25, 301 | COMMAND_ALARM = 26, 302 | COMMAND_ALERT = 27, 303 | COMMAND_CHANGE_PALLET = 28, 304 | COMMAND_POWER_ON = 29, 305 | COMMAND_POWER_OFF = 30, 306 | COMMAND_MAIN_CHUCK_OPEN = 31, 307 | COMMAND_MAIN_CHUCK_CLOSE = 32, 308 | COMMAND_SECONDARY_CHUCK_OPEN = 33, 309 | COMMAND_SECONDARY_CHUCK_CLOSE = 34, 310 | COMMAND_SECONDARY_SPINDLE_SYNCHRONIZATION_ACTIVATE = 35, 311 | COMMAND_SECONDARY_SPINDLE_SYNCHRONIZATION_DEACTIVATE = 36, 312 | COMMAND_SYNC_CHANNELS = 37, 313 | MOVEMENT_RAPID = 0, 314 | MOVEMENT_LEAD_IN = 1, 315 | MOVEMENT_CUTTING = 2, 316 | MOVEMENT_LEAD_OUT = 3, 317 | MOVEMENT_LINK_TRANSITION = 4, 318 | MOVEMENT_BRIDGING = 4, 319 | MOVEMENT_LINK_DIRECT = 5, 320 | MOVEMENT_RAMP_HELIX = 6, 321 | MOVEMENT_PIERCE_CIRCULAR = 6, 322 | MOVEMENT_RAMP_PROFILE = 7, 323 | MOVEMENT_PIERCE_PROFILE = 7, 324 | MOVEMENT_RAMP_ZIG_ZAG = 8, 325 | MOVEMENT_PIERCE_LINEAR = 8, 326 | MOVEMENT_RAMP = 9, 327 | MOVEMENT_PLUNGE = 10, 328 | MOVEMENT_PIERCE = 10, 329 | MOVEMENT_PREDRILL = 11, 330 | MOVEMENT_EXTENDED = 12, 331 | MOVEMENT_REDUCED = 13, 332 | MOVEMENT_FINISH_CUTTING = 14, 333 | MOVEMENT_HIGH_FEED = 15, 334 | PARAMETER_SPATIAL = 0, 335 | PARAMETER_ANGLE = 1, 336 | PARAMETER_ENUM = 2, 337 | HIGH_FEED_NO_MAPPING = 0, 338 | HIGH_FEED_MAP_MULTI = 1, 339 | HIGH_FEED_MAP_XY_Z = 2, 340 | HIGH_FEED_MAP_ANGULAR = 3, 341 | HIGH_FEED_MAP_CLEARANCE = 4, 342 | HIGH_FEED_MAP_ANY = 5, 343 | FLAG_CYCLE_REPEAT_PASS = 1; 344 | 345 | var currentSection = null; 346 | var movement = null; 347 | var currentTest = null; 348 | var unit = MM; 349 | var radiusCompensation = RADIUS_COMPENSATION_OFF; 350 | 351 | // dummy vector constructor 352 | function Vector(x, y, z) { 353 | this.x = x; 354 | this.y = y; 355 | this.z = z || 0; // auto set Z to zero 356 | } 357 | 358 | function line(isRapid, x, y) { 359 | return { 360 | isRapid: isRapid, 361 | x: x, 362 | y: y 363 | } 364 | } 365 | 366 | function cut(x, y) { 367 | return line(false, x, y); 368 | } 369 | 370 | function move(x, y) { 371 | return line(true, x, y); 372 | } 373 | 374 | /** 375 | * Tests are constructed by describing the lower left, upper right and origin vectors of the work 376 | */ 377 | function newTest(lowerLeftVector, upperRightVector, originVector, targetId) { 378 | var sections = []; 379 | // this is the current position 380 | var x = originVector.x, y = originVector.y; 381 | var outputLines = []; 382 | var customProperties = {}; 383 | var testUnits = MM; 384 | 385 | function applyProperties() { 386 | for (let [key, value] of new Map(Object.entries(customProperties))) { 387 | properties[key] = value; 388 | } 389 | } 390 | 391 | function processLines(lines) { 392 | for (var i = 0; i < lines.length; i++) { 393 | processLine(lines[i]); 394 | } 395 | } 396 | 397 | function processLine(line) { 398 | //console.log('line', line); 399 | if (line.isRapid === true) { 400 | movement = MOVEMENT_RAPID; 401 | onPower(false); 402 | onRapid(line.x, line.y, 0); 403 | } 404 | else { 405 | movement = MOVEMENT_CUTTING; 406 | onPower(true); 407 | onLinear(line.x, line.y, 0); 408 | } 409 | x = line.x; 410 | y = line.y; 411 | } 412 | 413 | // returned object becomes the current test as well as the interface for configuring the test 414 | var test = { 415 | getNumberOfSections: function getNumberOfSections() { 416 | return sections.length; 417 | }, 418 | getWorkpiece: function getWorkpiece() { 419 | return { 420 | upper: upperRightVector, 421 | lower: lowerLeftVector 422 | } 423 | }, 424 | withSection: function (jetMode, sectionParameters, lines) { 425 | var id = sections.length; 426 | var section = { 427 | getId: function getId() { 428 | return id; 429 | }, 430 | jetMode: jetMode, 431 | workPlane: { 432 | forward: new Vector(0, 0, 1) 433 | }, 434 | hasParameter: function hasParameter(key) { 435 | return !!sectionParameters[key]; 436 | }, 437 | getParameter: function getParameter(key) { 438 | return sectionParameters[key]; 439 | }, 440 | lines: lines 441 | } 442 | 443 | sections.push(section); 444 | return test; 445 | }, 446 | withProperties(props) { 447 | customProperties = props; 448 | return test; 449 | }, 450 | withUnits: function withUnits(units) { 451 | testUnits = units === MM ? MM : IN; 452 | return test; 453 | }, 454 | getCurrentPosition: function() { 455 | return {x: x, y: y} 456 | }, 457 | writeln: function writeln(line) { 458 | outputLines.push(line); 459 | }, 460 | run: function run() { 461 | // assign global current test 462 | currentTest = test; 463 | unit = testUnits; 464 | // simlulate the CAM system setting user configurable properties 465 | resetProperties(); 466 | applyProperties(); 467 | onOpen(); 468 | for (var i = 0; i < sections.length; i++) { 469 | currentSection = sections[i]; 470 | onSection(); 471 | processLines(currentSection.lines); 472 | onSectionEnd(); 473 | currentSection = null; 474 | movement = null; 475 | } 476 | onClose(); 477 | currentTest = null; 478 | 479 | // run all sections... 480 | var svg = outputLines.join("\n") 481 | //console.log(svg); 482 | document.getElementById(targetId).innerHTML = svg; 483 | }, 484 | validate: function () {} 485 | }; 486 | 487 | return test; 488 | } 489 | 490 | // we dont need to test anything but the laser 491 | var tool = { 492 | type: TOOL_LASER_CUTTER 493 | } 494 | 495 | // real working degrees to radians function 496 | function toRad (angle) { 497 | return angle * (Math.PI / 180); 498 | } 499 | 500 | // The return type is pretty extensive but only 1 function is used in out post: format(number) 501 | // https://cam.autodesk.com/posts/reference/classFormatNumber.html#a47d304db32feae2b6dbfb5281c153460 502 | // Returns the string for the specified value 503 | function createFormat(options) { 504 | return { 505 | format: function format(number) { 506 | return '' + (number * options.scale).toFixed(options.decimals); 507 | } 508 | }; 509 | } 510 | 511 | // get section count, TODO: make this drivable from the test data input 512 | function getNumberOfSections() { 513 | return currentTest.getNumberOfSections(); 514 | } 515 | 516 | function hasParameter(key) { 517 | return currentSection.hasParameter(key); 518 | } 519 | function getParameter(key) { 520 | return currentSection.getParameter(key); 521 | } 522 | 523 | // sure, why not... 524 | function isSameDirection() { 525 | return true; 526 | } 527 | 528 | // noop 529 | function setRotation() {} 530 | 531 | function error(str) { 532 | console.error(str); 533 | } 534 | 535 | function log(str) { 536 | console.log(str); 537 | } 538 | 539 | function writeln(str) { 540 | currentTest.writeln(str); 541 | } 542 | 543 | // i18n FTW! 544 | function localize(str) { 545 | return str; 546 | } 547 | 548 | function getWorkpiece() { 549 | return currentTest.getWorkpiece(); 550 | } 551 | 552 | function getCurrentPosition() { 553 | return currentTest.getCurrentPosition(); 554 | } 555 | 556 | function setCodePage(codePage) { 557 | } 558 | -------------------------------------------------------------------------------- /GlowforgeColorific.cps: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (C) 2018 by Autodesk, Inc. 3 | All rights reserved. 4 | */ 5 | 6 | description = "Glowforge"; 7 | vendor = "Glowforge"; 8 | vendorUrl = "https://www.glowforge.com"; 9 | legal = "Copyright (C) 2018 by Autodesk, Inc."; 10 | certificationLevel = 2; 11 | 12 | longDescription = "Generic post for Glowforge laser. The post will output the toolpath as SVG graphics which can then be uploaded directly to Glowforge."; 13 | 14 | extension = "svg"; 15 | mimetype = "image/svg+xml"; 16 | setCodePage("utf-8"); 17 | 18 | capabilities = CAPABILITY_JET; 19 | 20 | minimumCircularSweep = toRad(0.01); 21 | maximumCircularSweep = toRad(90); // avoid potential center calculation errors for CNC 22 | allowHelicalMoves = true; 23 | allowedCircularPlanes = (1 << PLANE_XY); // only XY arcs 24 | 25 | // global prooperties variable 26 | properties = null; 27 | // made available for testing 28 | function resetProperties() { 29 | properties = { 30 | lineWidth: 0.1, // how wide lines are in the SVG 31 | margin: 2, // margin in mm 32 | checkForRadiusCompensation: false, // if enabled throw an error if compensation in control is used 33 | doNotFlipYAxis: false, 34 | useWorkArea: false, // center the toolpath in the machines work area, off by default 35 | autoStockPoint: true, // automatically translate the output paths for strage stock points, see the whole image no matter what you select 36 | // Glowforge Cutting area: aprox. 19.5″ (495 mm) wide and 11″ (279 mm) deep 37 | workAreaWidth: 495, // width in mm used when useWorkArea is enabled 38 | workAreaHeight: 279, // height in mm used when useWorkArea is enabled 39 | }; 40 | } 41 | resetProperties(); 42 | 43 | // user-defined property definitions 44 | propertyDefinitions = { 45 | lineWidth: {title: "SVG Stroke Width(mm)", description: "The width of lines in the SVG in mm.", type: "number"}, 46 | margin: {title: "Margin(mm)", description: "Sets the margin in mm when 'Crop to Workpiece' is used.", type: "number"}, 47 | checkForRadiusCompensation: {title: "Check Sideways Comp.", description: "Check every opperation for Sideways Compensation 'In Computer'. If this is not configured, throw an error.", type: "boolean"}, 48 | doNotFlipYAxis: {title: "Flip Model", description: "If your part is upside down, check this box to flip it over. (Tip: checking 'Flip Z Axis' in the CAM setup also fixes this)", type: "boolean"}, 49 | useWorkArea: {title:"Use Work Area", description:"Center the toolpaths in an image the size of the defined Work Area.", type:"boolean"}, 50 | autoStockPoint: {title:"Auto Stock Point", description:"Make the final image completly visible reguardless of the selected stock point.", type:"boolean"}, 51 | workAreaWidth: {title:"Work Area Width(mm", description:"Work Area Width in mm, used when 'Crop to Workpiece' is disabled. Typically the max cutting width of the Glowforge.", type:"number"}, 52 | workAreaHeight: {title:"Work Area Height(mm)", description:"Height in mm, used when 'Crop to Workpiece' is disabled. Typically the max cutting height of the Glowforge.", type:"number"}, 53 | }; 54 | 55 | var POST_URL = "https://cam.autodesk.com/hsmposts?p=glowforge"; 56 | 57 | // Recommended colors for color mapping. 58 | var COLOR_GREEN = "1FB714"; 59 | var COLOR_YELLOW = "FBF305"; 60 | var COLOR_DARK_GREEN = "006412"; 61 | var COLOR_ORANGE = "FF6403"; 62 | var COLOR_BROWN = "562C05"; 63 | var COLOR_RED = "DD0907"; 64 | var COLOR_TAN = "90713A"; 65 | var COLOR_MAGENTA = "F20884"; 66 | var COLOR_PURPLE = "4700A5"; 67 | var COLOR_BLUE = "0000D3"; 68 | var COLOR_CYAN = "02ABEA"; 69 | var COLOR_BLACK = "000000"; 70 | 71 | var COLOR_CYCLE = [COLOR_CYAN, 72 | COLOR_MAGENTA, 73 | COLOR_YELLOW, 74 | COLOR_RED, 75 | COLOR_GREEN, 76 | COLOR_BLUE, 77 | COLOR_ORANGE, 78 | COLOR_DARK_GREEN, 79 | COLOR_PURPLE, 80 | COLOR_BROWN, 81 | COLOR_TAN, 82 | COLOR_BLACK]; 83 | 84 | // dont pick fewer colors than this 85 | var MIN_COLORS = 6; 86 | 87 | /** Global State **/ 88 | function reset() { 89 | return { 90 | // ConverterFormat: converted from IN to MM as needed 91 | xyzFormat: createFormat({decimals:(3), scale:(unit === IN) ? 25.4 : 1}), 92 | // clamp to 3 decimals but dont convert 93 | decimalFormat: createFormat({decimals:(3), scale: 1}), 94 | // selected colors to use for this run 95 | activeColorCycle: null, 96 | // the hex string of the current color 97 | currentHexColor: null, 98 | // the index of the current color 99 | currentColorIndex: -1, 100 | // track if the next path element can be a move command 101 | allowMoveCommandNext: null, 102 | // is the work area too small? 103 | workAreaTooSmall: false, 104 | // is the llaser currently on? 105 | isLaserOn: false 106 | }; 107 | } 108 | var state = null; 109 | 110 | // select a subset of colors so our preferred color pallet is used (and not simply the color with the lowest hex value first) 111 | function selectColors() { 112 | var requiredColors = Math.max(MIN_COLORS, getNumberOfSections()); // makes sure that more than enough colors get made 113 | var finalColorCycle = []; 114 | var numColors = COLOR_CYCLE.length; 115 | 116 | // if the number of default colors is too small, we will build lighter shades of those colors to fill in the extra needed colors: 117 | var alphaSteps = Math.ceil(requiredColors / numColors); 118 | var alphaStepSize = 1 / (alphaSteps + 1); // + 1 stops the last alpha blend stage for all colors being FFFFFF 119 | var alphaStepIndex = 0; 120 | var colorIndex = 0; 121 | var finalColorCycle = []; 122 | 123 | for (var i = 0; i < requiredColors; i++) { 124 | finalColorCycle.push(alphaBlendHexColor(COLOR_CYCLE[colorIndex], 1 - (alphaStepSize * alphaStepIndex))); 125 | colorIndex += 1; // next color 126 | if (colorIndex >= numColors) { 127 | colorIndex = 0; // start back at the first color 128 | alphaStepIndex++; // next lighter shade 129 | } 130 | } 131 | 132 | // reset all color related variables to allow re-runs 133 | state.activeColorCycle = sortColors(finalColorCycle); 134 | } 135 | 136 | // Glowforge doesn't respect the order of operations in the SVG file, it re-sorts them by the hex color value in ascending order 137 | // so here the color cycle is sorted to preserve op order from CAM. 138 | function sortColors(inputColors) { 139 | var mappedColors = inputColors.map(function buildHexColors(color, i) { 140 | return {hexColor: '#' + color, hexValue: parseInt(color, 16)}; 141 | }); 142 | 143 | mappedColors.sort(function compareHexValues(a, b) { 144 | if (a.hexValue < b.hexValue) { 145 | return -1; 146 | } 147 | if (a.hexValue > b.hexValue) { 148 | return 1; 149 | } 150 | return 0; 151 | }); 152 | 153 | return mappedColors.map(function reduceToHexColor(color, i) { 154 | return color.hexColor; 155 | }); 156 | } 157 | 158 | // returns a hex color that is alphaPercent lighter than the input color 159 | function alphaBlendHexColor(hexColorString, alphaPercent) { 160 | // alphaPercent needs to be converted from a float to a fraction of 255 161 | var alpha = alphaPercent; 162 | 163 | // hex color needs to be converted from a hex string to its constituent parts: 164 | var red = parseInt(hexColorString.substring(0, 2), 16); 165 | var green = parseInt(hexColorString.substring(2, 4), 16); 166 | var blue = parseInt(hexColorString.substring(4, 6), 16); 167 | 168 | return [alphaBlend(red, alpha), alphaBlend(green, alpha), alphaBlend(blue, alpha)].join(''); 169 | } 170 | 171 | // returns properly padded 2 digit hex strings for RGB color channels 172 | function toHexColorChannel(decimal) { 173 | var hex = decimal.toString(16); 174 | return (hex.length === 1 ? '0' : '') + hex; 175 | } 176 | 177 | // Alpha blend a color channel white 178 | function alphaBlend(colorChannel, alpha) { 179 | return toHexColorChannel(Math.round((1 - alpha) * 255 + alpha * colorChannel)); 180 | } 181 | 182 | // called on the start of each section, initalizes the first color from the active color cycle. 183 | function nextColor() { 184 | state.currentColorIndex = state.currentColorIndex + 1; 185 | if (state.currentColorIndex >= state.activeColorCycle.length) { 186 | error("Not enough colors were generated!"); 187 | } 188 | 189 | state.currentHexColor = state.activeColorCycle[state.currentColorIndex]; 190 | } 191 | 192 | // should the current sction be cut (using a stroke) or etched (using a fill)? 193 | var useFillForSection = false; 194 | /** 195 | * For Etch/Vaporize/Engrave, returns fill settings, otherwise none 196 | */ 197 | function fill() { 198 | if (useFillForSection) { 199 | return "fill=\"" + state.currentHexColor + "\""; 200 | } 201 | return "fill=\"none\""; 202 | } 203 | 204 | /** 205 | * For through cuts, returns stroke settings, otherwise none 206 | */ 207 | function stroke() { 208 | if (useFillForSection) { 209 | return "stroke=\"none\""; 210 | } 211 | return "stroke=\"" + state.currentHexColor + "\" stroke-width=\"" + properties.lineWidth + "\""; 212 | } 213 | 214 | // update the allowMoveCommandNext flag 215 | function allowMoveCommand() { 216 | state.allowMoveCommandNext = true; 217 | } 218 | 219 | var activePathElements = []; 220 | function addPathElement() { 221 | var args = [].slice.call(arguments); 222 | 223 | // only allow moves (M) in the SVG after the laser has been turned off and comes back on again 224 | if (args[0] === "M") { 225 | if (state.allowMoveCommandNext) { 226 | // reset the flag to wait for the next laser power cycle 227 | state.allowMoveCommandNext = false; 228 | } 229 | else { 230 | // skip rendering this move command since the laser has not been turned off 231 | return; 232 | } 233 | } 234 | 235 | activePathElements.push(args.join(" ")); 236 | } 237 | 238 | function finishPath() { 239 | if (!activePathElements || activePathElements.length === 0) { 240 | error('An operation resulted in no detectable paths!'); 241 | return; 242 | } 243 | 244 | var opComment = hasParameter("operation-comment") ? getParameter("operation-comment") : "[No Title]"; 245 | 246 | writeln(""); 247 | writeln(" " + opComment + " (" + localize("Op") + ": " + (1 + currentSection.getId()) + "/" + getNumberOfSections() + ")"); 248 | writeln(" ") 253 | writeln(""); 254 | activePathElements = []; 255 | allowMoveCommand(); 256 | } 257 | 258 | // return true if the program should halt because of missing radius compensation in the computer. 259 | function isRadiusCompensationInvalid() { 260 | if (properties.checkForRadiusCompensation === true && (radiusCompensation != RADIUS_COMPENSATION_OFF)) { 261 | error("Operation: " + (1 + currentSection.getId()) + ". The Sideways Compensation type 'In Control' is not supported. This must be set to 'In Computer' in the passes tab."); 262 | } 263 | } 264 | 265 | /** Returns the given spatial value in MM. */ 266 | function toMM(value) { 267 | return value * ((unit === IN) ? 25.4 : 1); 268 | } 269 | 270 | function printVector(v) { 271 | return v.x + "," + v.y; 272 | } 273 | 274 | function onOpen() { 275 | if (properties.margin < 0) { 276 | error(localize("Margin must be 0 or positive.")); 277 | return; 278 | } 279 | 280 | // reset all per-run state 281 | state = reset(); 282 | 283 | // select colors now that the number of ops is available 284 | selectColors(); 285 | 286 | // convert everything to mm once up front: 287 | var box = { 288 | upper: { 289 | x: toMM(getWorkpiece().upper.x), 290 | y: toMM(getWorkpiece().upper.y) 291 | }, 292 | lower: { 293 | x: toMM(getWorkpiece().lower.x), 294 | y: toMM(getWorkpiece().lower.y) 295 | } 296 | }; 297 | 298 | var dx = box.upper.x - box.lower.x; 299 | var dy = box.upper.y - box.lower.y; 300 | 301 | // add margins to overall SVG size 302 | var width = dx + (2 * properties.margin); 303 | var height = dy + (2 * properties.margin); 304 | 305 | if (properties.useWorkArea === true) { 306 | // no margins in useWorkArea mode, you get the work area as your margins! 307 | width = Math.max(properties.workAreaWidth, dx); 308 | height = Math.max(properties.workAreaHeight, dy); 309 | state.workAreaTooSmall = width > properties.workAreaWidth || height > properties.workAreaHeight; 310 | } 311 | /* 312 | * Compensate for Stock Point, SVG Origin, Z axis orientation and margins 313 | * 314 | * The *correct* stock point to select is the lower left corner and the right Z axis orientation is pointing up from the stock towards the laser. 315 | * But to make the learning curve a little gentler we will compensate if you didnt do that. 316 | * 317 | * Auto Stock Point Compensation: 318 | * First, any stock point will produce the same image, here we correct for the stock point with a translation of the entire SVG contents 319 | * in x and y. We want to use the extents of the X and Y axes. Normally X comes from the lower right corner of the stock and Y from the 320 | * upper left (assuming a CAM origin in the lower left corner). 321 | * 322 | * Y Axis in SVG vs CAM: 323 | * If we do nothing the image would be upside down because in SVG the Y origin is at the TOP of the image (see https://www.w3.org/TR/SVG/coords.html#InitialCoordinateSystem). 324 | * So normally the Y axis must be flipped to compensate for this by scaling it to -1. 325 | * 326 | * Incorrect Z Axis Orientation: 327 | * If the user has the Z axis pointing into the stock the SVG image will be upside down (flipped in Y, twice!). This is annoying and is not obvious to fix 328 | * because X and Y look right in the UI. So the "Flip Model" parameter is provided and does *magic* by turning off the default Y flipping. Now the Y axis is only flipped once 329 | * like we need for the SVG origin. But the *lower* box point has to be used to get the Y extent in this case because the *CAM* is upside down (CAM origin is top left corner). 330 | * Unfortunatly the stock point selection changes the ratio between Y values in the upper and lower stock points, so its impossible to detect this without assuming a stock point. 331 | * So this is as good as we can do. 332 | * 333 | * Margins: 334 | * Add 1 magin width to these numbers so the image is centred. 335 | */ 336 | var yAxisScale = properties.doNotFlipYAxis ? 1 : -1; 337 | var translateX = 0; 338 | var translateY = 0; 339 | 340 | if (properties.useWorkArea === true) { 341 | // FIXME: this is probably wrong if the design turns out to be bigger than the work area, e.g. (width - dx) will be negative! 342 | translateX = (-box.lower.x + ((width - dx) / 2)); 343 | translateY = (box.upper.y + ((height - dy) / 2)); 344 | } 345 | else if (properties.autoStockPoint === true) { 346 | translateX = (-1 * box.lower.x) + properties.margin; 347 | translateY = (-1 * yAxisScale * (properties.doNotFlipYAxis ? box.lower.y : box.upper.y)) + properties.margin; 348 | } 349 | // else dont translate anythng. 350 | 351 | writeln(""); 352 | writeln(""); 353 | writeln("Created with " + description + " for Fusion 360. To download visit: " + POST_URL + ""); 354 | 355 | // write a comment explaining what info we got from the CAM system about the stock and coordinate system 356 | writeln(""); 365 | 366 | // translate + scale operation to flip the Y axis so the output is in the same x/y orientation it was in Fusion 360 367 | writeln(""); 368 | } 369 | 370 | function onClose() { 371 | writeln(""); 372 | // draw an untranslated box to represent the work are boundary on top of everything 373 | if (state.workAreaTooSmall === true) { 374 | writeln(""); 375 | } 376 | writeln(""); 377 | } 378 | 379 | function onComment(text) { 380 | writeln(''); 381 | } 382 | 383 | function onSection() { 384 | switch (tool.type) { 385 | case TOOL_WATER_JET: // allow any way for Epilog 386 | warning(localize("Using waterjet cutter but allowing it anyway.")); 387 | break; 388 | case TOOL_LASER_CUTTER: 389 | break; 390 | case TOOL_PLASMA_CUTTER: // allow any way for Epilog 391 | warning(localize("Using plasma cutter but allowing it anyway.")); 392 | break; 393 | case TOOL_MARKER: // allow any way for Epilog 394 | warning(localize("Using marker but allowing it anyway.")); 395 | break; 396 | default: 397 | error(localize("The CNC does not support the required tool.")); 398 | return; 399 | } 400 | 401 | // use Jet Mode to decide if the shape should be filled or have no fill 402 | switch (currentSection.jetMode) { 403 | case JET_MODE_THROUGH: 404 | useFillForSection = false; 405 | break; 406 | case JET_MODE_ETCHING: 407 | case JET_MODE_VAPORIZE: 408 | useFillForSection = true 409 | break 410 | default: 411 | error(localize("Unsupported cutting mode.")); 412 | return; 413 | } 414 | 415 | var remaining = currentSection.workPlane; 416 | if (!isSameDirection(remaining.forward, new Vector(0, 0, 1))) { 417 | error(localize("Tool orientation is not supported.")); 418 | return; 419 | } 420 | setRotation(remaining); 421 | nextColor(); 422 | } 423 | 424 | function onParameter(name, value) { 425 | } 426 | 427 | function onDwell(seconds) { 428 | } 429 | 430 | function onCycle() { 431 | } 432 | 433 | function onCyclePoint(x, y, z) { 434 | } 435 | 436 | function onCycleEnd() { 437 | } 438 | 439 | function onPower(isLaserPowerOn) { 440 | // if the laser goes from off to on, this happens after a move, so a M should be emitted in the SVG 441 | // this check debounces multiple power on caommands in case the way they are emitted ever changes 442 | if (!state.isLaserOn && isLaserPowerOn) { 443 | allowMoveCommand(); 444 | } 445 | 446 | state.isLaserOn = isLaserPowerOn; 447 | } 448 | 449 | // validate that the laser is on and that the movement type is a cutting move 450 | function isCuttingMove(movement) { 451 | return state.isLaserOn && (movement === MOVEMENT_CUTTING || movement == MOVEMENT_REDUCED || movement == MOVEMENT_FINISH_CUTTING); 452 | } 453 | 454 | function writeLine(x, y) { 455 | if (!isCuttingMove(movement)) { 456 | return; 457 | } 458 | 459 | isRadiusCompensationInvalid(); 460 | 461 | var start = getCurrentPosition(); 462 | if ((state.xyzFormat.format(start.x) == state.xyzFormat.format(x)) && 463 | (state.xyzFormat.format(start.y) == state.xyzFormat.format(y))) { 464 | log('vertical move ignored'); 465 | return; // ignore vertical 466 | } 467 | 468 | addPathElement("M", state.xyzFormat.format(start.x), state.xyzFormat.format(start.y)); 469 | addPathElement("L", state.xyzFormat.format(x), state.xyzFormat.format(y)); 470 | } 471 | 472 | function onRapid(x, y, z) { 473 | writeLine(x, y); 474 | } 475 | 476 | function onLinear(x, y, z, feed) { 477 | writeLine(x, y); 478 | } 479 | 480 | function onRapid5D(x, y, z, dx, dy, dz) { 481 | onRapid(x, y, z); 482 | } 483 | 484 | function onLinear5D(x, y, z, dx, dy, dz, feed) { 485 | onLinear(x, y, z); 486 | } 487 | 488 | function onCircular(clockwise, cx, cy, cz, x, y, z, feed) { 489 | if (!isCuttingMove(movement)) { 490 | return; 491 | } 492 | 493 | isRadiusCompensationInvalid(); 494 | 495 | var start = getCurrentPosition(); 496 | 497 | var largeArc = (getCircularSweep() > Math.PI) ? 1 : 0; 498 | var sweepFlag = isClockwise() ? 0 : 1; 499 | addPathElement("M", state.xyzFormat.format(start.x), state.xyzFormat.format(start.y)); 500 | addPathElement("A", state.xyzFormat.format(getCircularRadius()), state.xyzFormat.format(getCircularRadius()), 0, largeArc, sweepFlag, state.xyzFormat.format(x), state.xyzFormat.format(y)); 501 | } 502 | 503 | function onCommand() { 504 | } 505 | 506 | function onSectionEnd() { 507 | finishPath(); 508 | } 509 | --------------------------------------------------------------------------------