|
| 1 | +/* |
| 2 | + * Copyright (C) 2021 Sienci Labs Inc. |
| 3 | + * |
| 4 | + * This file is part of gSender. |
| 5 | + * |
| 6 | + * gSender is free software: you can redistribute it and/or modify |
| 7 | + * it under the terms of the GNU General Public License as published by |
| 8 | + * the Free Software Foundation, under version 3 of the License. |
| 9 | + * |
| 10 | + * gSender is distributed in the hope that it will be useful, |
| 11 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 12 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 13 | + * GNU General Public License for more details. |
| 14 | + * |
| 15 | + * You should have received a copy of the GNU General Public License |
| 16 | + * along with gSender. If not, see <https://www.gnu.org/licenses/>. |
| 17 | + * |
| 18 | + * Contact for information regarding this program and its license |
| 19 | + * can be sent through gSender@sienci.com or mailed to the main office |
| 20 | + * of Sienci Labs Inc. in Waterloo, Ontario, Canada. |
| 21 | + * |
| 22 | + */ |
| 23 | + |
| 24 | +import concaveman from 'concaveman'; |
| 25 | +import GCodeVirtualizer from 'app/lib/GCodeVirtualizer'; |
| 26 | +import {OUTLINE_MODE_RAPIDLESS_SQUARE} from "app/constants"; |
| 27 | + |
| 28 | +self.onmessage = ({ data }) => { |
| 29 | + const { isLaser = false, parsedData = [], mode, bbox, zTravel, content = '' } = data; |
| 30 | + |
| 31 | + const getOutlineGcode = (concavity = Infinity) => { |
| 32 | + // 1. Extract 2D [x, y] points (parsedData is flat: x0,y0,z0,x1,y1,z1,...) |
| 33 | + const points2D = []; |
| 34 | + for (let i = 0; i < parsedData.length; i += 3) { |
| 35 | + points2D.push([ |
| 36 | + parseFloat(parsedData[i].toFixed(3)), |
| 37 | + parseFloat(parsedData[i + 1].toFixed(3)), |
| 38 | + ]); |
| 39 | + } |
| 40 | + |
| 41 | + // 2. Deduplicate on 0.5mm grid for efficiency on large files |
| 42 | + const seen = new Set(); |
| 43 | + const deduped = []; |
| 44 | + for (const [x, y] of points2D) { |
| 45 | + const key = `${Math.round(x * 2)},${Math.round(y * 2)}`; |
| 46 | + if (!seen.has(key)) { |
| 47 | + seen.add(key); |
| 48 | + deduped.push([x, y]); |
| 49 | + } |
| 50 | + } |
| 51 | + |
| 52 | + // 3. Compute concave hull; remove duplicate closing point |
| 53 | + let hull = concaveman(deduped, concavity).slice(0, -1); |
| 54 | + |
| 55 | + // 4. Ensure clockwise winding (negative signed area in standard XY) |
| 56 | + // Shoelace cross-product variant: sum of (x2-x1)*(y2+y1) |
| 57 | + const area = hull.reduce((sum, pt, i) => { |
| 58 | + const next = hull[(i + 1) % hull.length]; |
| 59 | + return sum + (next[0] - pt[0]) * (next[1] + pt[1]); |
| 60 | + }, 0); |
| 61 | + if (area > 0) { |
| 62 | + hull.reverse(); |
| 63 | + } |
| 64 | + |
| 65 | + // 5. Rotate hull to start at vertex nearest to (0, 0) |
| 66 | + let startIdx = 0; |
| 67 | + let minDist = Infinity; |
| 68 | + hull.forEach(([x, y], i) => { |
| 69 | + const d = x * x + y * y; |
| 70 | + if (d < minDist) { |
| 71 | + minDist = d; |
| 72 | + startIdx = i; |
| 73 | + } |
| 74 | + }); |
| 75 | + const orderedHull = [...hull.slice(startIdx), ...hull.slice(0, startIdx)]; |
| 76 | + |
| 77 | + return convertPointsToGCode(orderedHull, isLaser); |
| 78 | + }; |
| 79 | + |
| 80 | + const getSimpleOutline = () => { |
| 81 | + if (parsedData && parsedData.length <= 0) { |
| 82 | + return [ |
| 83 | + '%X0=posx,Y0=posy,Z0=posz', |
| 84 | + '%MM=modal.distance', |
| 85 | + `G21 G91 G0 Z${zTravel}`, |
| 86 | + 'G90', |
| 87 | + `G0 X[${bbox.min.x}] Y[${bbox.min.y}]`, |
| 88 | + `G0 X[${bbox.min.x}] Y[${bbox.max.y}]`, |
| 89 | + `G0 X[${bbox.max.x}] Y[${bbox.max.y}]`, |
| 90 | + `G0 X[${bbox.max.x}] Y[${bbox.min.y}]`, |
| 91 | + `G0 X[${bbox.min.x}] Y[${bbox.min.y}]`, |
| 92 | + 'G0 X[X0] Y[Y0]', |
| 93 | + `G21 G91 G0 Z-${zTravel}`, |
| 94 | + '[MM]', |
| 95 | + ]; |
| 96 | + } else { |
| 97 | + return [ |
| 98 | + '%X0=posx,Y0=posy,Z0=posz', |
| 99 | + '%MM=modal.distance', |
| 100 | + `G21 G91 G0 Z${zTravel}`, |
| 101 | + 'G90', |
| 102 | + 'G0 X[xmin] Y[ymin]', |
| 103 | + 'G0 X[xmin] Y[ymax]', |
| 104 | + 'G0 X[xmax] Y[ymax]', |
| 105 | + 'G0 X[xmax] Y[ymin]', |
| 106 | + 'G0 X[xmin] Y[ymin]', |
| 107 | + 'G0 X[X0] Y[Y0]', |
| 108 | + `G21 G91 G0 Z-${zTravel}`, |
| 109 | + '[MM]', |
| 110 | + ]; |
| 111 | + } |
| 112 | + }; |
| 113 | + |
| 114 | + const getRapidlessSquareOutline = (fileContent: string) => { |
| 115 | + let xmin = Infinity, xmax = -Infinity, ymin = Infinity, ymax = -Infinity; |
| 116 | + |
| 117 | + const updateBounds = (v1: any, v2: any) => { |
| 118 | + for (const v of [v1, v2]) { |
| 119 | + if (v.x < xmin) xmin = v.x; |
| 120 | + if (v.x > xmax) xmax = v.x; |
| 121 | + if (v.y < ymin) ymin = v.y; |
| 122 | + if (v.y > ymax) ymax = v.y; |
| 123 | + } |
| 124 | + }; |
| 125 | + |
| 126 | + const vm = new GCodeVirtualizer({ |
| 127 | + addLine: (modal: any, v1: any, v2: any) => { |
| 128 | + if (modal.motion !== 'G0') updateBounds(v1, v2); |
| 129 | + }, |
| 130 | + addArcCurve: (_modal: any, v1: any, v2: any) => { |
| 131 | + updateBounds(v1, v2); |
| 132 | + }, |
| 133 | + addCurve: (modal: any, v1: any, v2: any) => { |
| 134 | + if (modal.motion !== 'G0') updateBounds(v1, v2); |
| 135 | + }, |
| 136 | + }); |
| 137 | + |
| 138 | + // Parse line-by-line (same pattern as Visualize.worker.ts) |
| 139 | + const len = fileContent.length; |
| 140 | + let lineStart = 0; |
| 141 | + for (let i = 0; i < len; i++) { |
| 142 | + const ch = fileContent.charCodeAt(i); |
| 143 | + if (ch !== 10 && ch !== 13) continue; |
| 144 | + vm.virtualize(fileContent.slice(lineStart, i)); |
| 145 | + if (ch === 13 && i + 1 < len && fileContent.charCodeAt(i + 1) === 10) i++; |
| 146 | + lineStart = i + 1; |
| 147 | + } |
| 148 | + vm.virtualize(fileContent.slice(lineStart)); |
| 149 | + |
| 150 | + if (!isFinite(xmin)) { |
| 151 | + // No cutting moves found — fall back to regular square |
| 152 | + return getSimpleOutline(); |
| 153 | + } |
| 154 | + |
| 155 | + return [ |
| 156 | + '%X0=posx,Y0=posy,Z0=posz', |
| 157 | + '%MM=modal.distance', |
| 158 | + `G21 G91 G0 Z${zTravel}`, |
| 159 | + 'G90', |
| 160 | + `G0 X${xmin.toFixed(3)} Y${ymin.toFixed(3)}`, |
| 161 | + `G0 X${xmin.toFixed(3)} Y${ymax.toFixed(3)}`, |
| 162 | + `G0 X${xmax.toFixed(3)} Y${ymax.toFixed(3)}`, |
| 163 | + `G0 X${xmax.toFixed(3)} Y${ymin.toFixed(3)}`, |
| 164 | + `G0 X${xmin.toFixed(3)} Y${ymin.toFixed(3)}`, |
| 165 | + 'G0 X[X0] Y[Y0]', |
| 166 | + `G21 G91 G0 Z-${zTravel}`, |
| 167 | + '[MM]', |
| 168 | + ]; |
| 169 | + }; |
| 170 | + |
| 171 | + function convertPointsToGCode(points: number[][], isLaser = false) { |
| 172 | + const gCode = []; |
| 173 | + const movementModal = isLaser ? 'G1' : 'G0'; // G1 is necessary for laser outline since G0 won't enable it |
| 174 | + gCode.push('%X0=posx,Y0=posy,Z0=posz'); |
| 175 | + gCode.push('%MM=modal.distance'); |
| 176 | + gCode.push(`G21 G91 G0 Z${zTravel}`); |
| 177 | + // Laser outline requires some additional preamble for feedrate and enabling the laser |
| 178 | + if (isLaser) { |
| 179 | + gCode.push('G1F3000 M3 S1'); |
| 180 | + } |
| 181 | + points.forEach((point) => { |
| 182 | + const [x, y] = point; |
| 183 | + gCode.push(`G21 G90 ${movementModal} X${x} Y${y}`); |
| 184 | + }); |
| 185 | + // Close the loop by returning to the first point |
| 186 | + if (points.length > 0) { |
| 187 | + const [x, y] = points[0]; |
| 188 | + gCode.push(`G21 G90 ${movementModal} X${x} Y${y}`); |
| 189 | + } |
| 190 | + if (isLaser) { |
| 191 | + gCode.push('M5 S0'); |
| 192 | + } |
| 193 | + gCode.push('G0 X[X0] Y[Y0]'); |
| 194 | + gCode.push(`G21 G91 G0 Z-${zTravel}`); |
| 195 | + |
| 196 | + gCode.push('[MM]'); |
| 197 | + return gCode; |
| 198 | + } |
| 199 | + |
| 200 | + let outlineGcode; |
| 201 | + if (mode === 'Square') { |
| 202 | + outlineGcode = getSimpleOutline(); |
| 203 | + } else if (mode === OUTLINE_MODE_RAPIDLESS_SQUARE) { |
| 204 | + outlineGcode = getRapidlessSquareOutline(content); |
| 205 | + } else { |
| 206 | + outlineGcode = getOutlineGcode(); |
| 207 | + } |
| 208 | + postMessage({ outlineGcode }); |
| 209 | +}; |
0 commit comments