diff --git a/package-lock.json b/package-lock.json index b2fcd31..6a3194e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -90,6 +90,9 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + }, + "three-js-csg": { + "version": "github:Doodle3D/three-js-csg#a36f23da6e9be2405a9094de5709cb0ae8f58045" } } }, @@ -3229,6 +3232,11 @@ } } }, + "earcut": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.1.3.tgz", + "integrity": "sha512-AxdCdWUk1zzK/NuZ7e1ljj6IGC+VAdC3Qb7QQDsXpfNrc5IM8tL9nNXUmEGE6jRHTfZ10zhzRhtDmWVsR5pd3A==" + }, "ecc-jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", @@ -7266,9 +7274,9 @@ "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" }, "lodash-es": { - "version": "4.17.8", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.8.tgz", - "integrity": "sha512-I9mjAxengFAleSThFhhAhvba6fsO0hunb9/0sQ6qQihSZsJRBofv2rYH58WXaOb/O++eUmYpCLywSQ22GfU+sA==" + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.10.tgz", + "integrity": "sha512-iesFYPmxYYGTcmQK0sL8bX3TGHyM6b2qREaB4kamHfQyfPJP0xgoGxp19nsH16nsfquLdiyKyX3mQkfiSGV8Rg==" }, "lodash._arraycopy": { "version": "3.0.0", @@ -8254,9 +8262,9 @@ } }, "node-abi": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.3.0.tgz", - "integrity": "sha512-zwm6vU3SsVgw3e9fu48JBaRBCJGIvAgysDsqtf5+vEexFE71bEOtaMWb5zr/zODZNzTPtQlqUUpC79k68Hspow==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.4.0.tgz", + "integrity": "sha512-hRUz0vG+eJfSqwU6rOgW6wNyX85ec8OEE9n4A+u+eoiE8oTePhCkUFTNmwQ+86Kyu429PCLNNyI2P2jL9qKXhw==", "requires": { "semver": "5.4.1" } @@ -9344,14 +9352,14 @@ "github-from-package": "0.0.0", "minimist": "1.2.0", "mkdirp": "0.5.1", - "node-abi": "2.3.0", + "node-abi": "2.4.0", "noop-logger": "0.1.1", "npmlog": "4.1.2", "os-homedir": "1.0.2", "pump": "2.0.1", "rc": "1.2.6", - "simple-get": "2.7.0", - "tar-fs": "1.16.0", + "simple-get": "2.8.1", + "tar-fs": "1.16.2", "tunnel-agent": "0.6.0", "which-pm-runs": "1.0.0" }, @@ -9748,8 +9756,8 @@ "requires": { "hoist-non-react-statics": "2.5.0", "invariant": "2.2.2", - "lodash": "4.17.5", - "lodash-es": "4.17.8", + "lodash": "4.17.10", + "lodash-es": "4.17.10", "loose-envify": "1.3.1", "prop-types": "15.6.0" }, @@ -9760,9 +9768,9 @@ "integrity": "sha512-6Bl6XsDT1ntE0lHbIhr4Kp2PGcleGZ66qu5Jqk8lc0Xc/IeG6gVLmwUGs/K0Us+L8VWoKgj0uWdPMataOsm31w==" }, "lodash": { - "version": "4.17.5", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", - "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==" + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" } } }, @@ -9921,8 +9929,8 @@ "hoist-non-react-statics": "2.5.0", "invariant": "2.2.4", "is-promise": "2.1.0", - "lodash": "4.17.5", - "lodash-es": "4.17.8", + "lodash": "4.17.10", + "lodash-es": "4.17.10", "prop-types": "15.6.1" }, "dependencies": { @@ -9940,9 +9948,9 @@ } }, "lodash": { - "version": "4.17.5", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", - "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==" + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" }, "prop-types": { "version": "15.6.1", @@ -10462,9 +10470,9 @@ "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=" }, "simple-get": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-2.7.0.tgz", - "integrity": "sha512-RkE9rGPHcxYZ/baYmgJtOSM63vH0Vyq+ma5TijBcLla41SWlh8t6XYIGMR/oeZcmr+/G8k+zrClkkVrtnQ0esg==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-2.8.1.tgz", + "integrity": "sha512-lSSHRSw3mQNUGPAYRqo7xy9dhKmxFXIjLjp4KHpf99GEH2VH7C3AM+Qfx6du6jhfUi6Vm7XnbEVEf7Wb6N8jRw==", "requires": { "decompress-response": "3.3.0", "once": "1.4.0", @@ -10906,9 +10914,9 @@ "dev": true }, "tar-fs": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.0.tgz", - "integrity": "sha512-I9rb6v7mjWLtOfCau9eH5L7sLJyU2BnxtEZRQ5Mt+eRKmf1F0ohXmT/Jc3fr52kDvjJ/HV5MH3soQfPL5bQ0Yg==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.2.tgz", + "integrity": "sha512-LdknWjPEiZC1nOBwhv0JBzfJBGPJar08dZg2rwZe0ZTLQoRGEzgrl7vF3qUEkCHpI/wN9e7RyCuDhMsJUCLPPQ==", "requires": { "chownr": "1.0.1", "mkdirp": "0.5.1", @@ -10999,9 +11007,6 @@ "resolved": "https://registry.npmjs.org/three/-/three-0.88.0.tgz", "integrity": "sha1-QlbC/Djk+yOg0j66K2zOTfjkZtU=" }, - "three-js-csg": { - "version": "github:Doodle3D/three-js-csg#a36f23da6e9be2405a9094de5709cb0ae8f58045" - }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", diff --git a/package.json b/package.json index e045faf..fbf094a 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@doodle3d/doodle3d-api": "^1.0.5", "@doodle3d/doodle3d-core": "github:doodle3d/doodle3d-core", "babel-plugin-transform-class-properties": "^6.24.1", + "earcut": "^2.1.3", "file-saver": "^1.3.3", "lodash": "^4.17.4", "material-ui": "^0.19.4", diff --git a/src/sliceActions/helpers/GCode.js b/src/sliceActions/helpers/GCode.js index 65ac5bb..0f01e99 100644 --- a/src/sliceActions/helpers/GCode.js +++ b/src/sliceActions/helpers/GCode.js @@ -1,5 +1,5 @@ -import { scale, distanceTo } from './vector2.js'; -import { PRECISION, VERSION } from '../../constants.js'; +import { distanceTo } from './vector2.js'; +import { VERSION } from '../../constants.js'; export const MOVE = 'G'; export const M_COMMAND = 'M'; @@ -55,7 +55,7 @@ export default class GCode { } moveTo(x, y, z, { speed }) { - const newNozzlePosition = scale({ x, y }, PRECISION); + const newNozzlePosition = { x, y }; const lineLength = distanceTo(this._nozzlePosition, newNozzlePosition); this._duration += lineLength / speed; @@ -74,7 +74,7 @@ export default class GCode { } lineTo(x, y, z, { speed, flowRate }) { - const newNozzlePosition = scale({ x, y }, PRECISION); + const newNozzlePosition = { x, y }; const lineLength = distanceTo(this._nozzlePosition, newNozzlePosition); this._extruder += this._nozzleToFilamentRatio * lineLength * flowRate; diff --git a/src/sliceActions/helpers/comb.js b/src/sliceActions/helpers/comb.js index 538cda9..76f07dd 100644 --- a/src/sliceActions/helpers/comb.js +++ b/src/sliceActions/helpers/comb.js @@ -1,131 +1,225 @@ -import Shape from 'clipper-js'; -import { subtract, add, scale, normalize, dot, length, distanceTo } from './vector2.js'; -import { PRECISION } from '../../constants.js'; +import { subtract, add, normalize, dot, distanceTo, divide, normal } from './vector2.js'; +import earcut from 'earcut'; -const TOLERANCE = 1 / PRECISION; +// const TRIANGULATED_OUTLINES = new WeakMap(); export default function comb(outline, start, end) { - if (distanceTo(start, end) < TOLERANCE) { - return [start, end]; + if (distanceTo(start, end) < 10) return [start, end]; + + // if (!TRIANGULATED_OUTLINES.has(outline)) TRIANGULATED_OUTLINES.set(outline, decompose(outline)); + // const { convexPolygons, vertices } = TRIANGULATED_OUTLINES.get(outline); + const { convexPolygons, vertices } = decompose(outline); + const startPolygon = convexPolygons.findIndex(({ face }) => pointIsInsideConvex(start, face, vertices)); + const endPolygon = convexPolygons.findIndex(({ face }) => pointIsInsideConvex(end, face, vertices)); + if (startPolygon === -1 || endPolygon === -1) return [start, end]; + if (startPolygon === endPolygon) return [start, end]; + + const path = findClosestPath(convexPolygons, startPolygon, endPolygon); + if (!path) return [start, end]; + const line = containLineInPath(path, start, end, vertices); + + return line; +} + +function lineIntersection(a1, a2, b1, b2) { + // source: http://mathworld.wolfram.com/Line-LineIntersection.html + const intersection = { + x: ((a1.x * a2.y - a1.y * a2.x) * (b1.x - b2.x) - (a1.x - a2.x) * (b1.x * b2.y - b1.y * b2.x)) / ((a1.x - a2.x) * (b1.y - b2.y) - (a1.y - a2.y) * (b1.x - b2.x)), + y: ((a1.x * a2.y - a1.y * a2.x) * (b1.y - b2.y) - (a1.y - a2.y) * (b1.x * b2.y - b1.y * b2.x)) / ((a1.x - a2.x) * (b1.y - b2.y) - (a1.y - a2.y) * (b1.x - b2.x)) + }; + + const intersectionA = subtract(intersection, a1); + const directionA = subtract(a2, a1); + const normalA = normalize(directionA); + const distanceA = dot(normalA, intersectionA); + if (distanceA < 0 || distanceA > dot(normalA, directionA)) return false; + + const intersectionB = subtract(intersection, b1); + const directionB = subtract(b2, b1); + const normalB = normalize(directionB); + const distanceB = dot(normalB, intersectionB); + if (distanceB < 0 || distanceB > dot(normalB, directionB)) return false; + + return intersection; +} + +function pointIsInsideConvex(point, convex, vertices) { + for (let i = 0; i < convex.length; i ++) { + const vertexA = vertices[convex[i]]; + const vertexB = vertices[convex[(i + 1) % convex.length]]; + + const n = normalize(normal(subtract(vertexB, vertexA))); + const p = subtract(point, vertexA); + + if (dot(p, n) < 0) return false; + } + return true; +} + +function decompose(polygon) { + const vertices = polygon.reduce((points, path) => { + points.push(...path); + return points; + }, []); + const flatVertices = vertices.reduce((points, { x, y }) => { + points.push(x, y); + return points; + }, []); + let offset = 0; + const holes = polygon + .map(path => offset += path.length) + .slice(0, -1); + + const flatTrainglesIndexed = earcut(flatVertices, holes); + const convexPolygons = []; + for (let i = 0; i < flatTrainglesIndexed.length; i += 3) { + const face = [ + flatTrainglesIndexed[i], + flatTrainglesIndexed[i + 1], + flatTrainglesIndexed[i + 2] + ]; + const center = divide(face.reduce((total, point) => { + if (!total) { + return vertices[point]; + } else { + return add(total, vertices[point]); + } + }, null), face.length); + convexPolygons.push({ + center, + face, + connects: [] + }); } - let combPath = new Shape([[start, end]], false, true, false); + for (let i = 0; i < convexPolygons.length; i ++) { + for (let j = i + 1; j < convexPolygons.length; j ++) { + const triangleIndexedA = convexPolygons[i]; + const triangleIndexedB = convexPolygons[j]; - for (let i = 0; i < outline.paths.length; i ++) { - let outlinePart = new Shape([outline.paths[i]], true, false, false, true); + const overlap = []; + triangleIndexedA.face.map(index => { + if (triangleIndexedB.face.includes(index)) overlap.push(index); + }); - let snappedCombPaths = outlinePart.orientation(0) ? combPath.intersect(outlinePart) : combPath.difference(outlinePart); - - snappedCombPaths = snappedCombPaths.mapToLower(); - outlinePart = outlinePart.mapToLower()[0]; - - if (distanceTo(start, outlinePart[outlinePart.length - 1]) < distanceTo(start, outlinePart[0])) { - outlinePart = outlinePart.reverse(); + if (overlap.length === 2) { + const distance = distanceTo(convexPolygons[i].center, convexPolygons[j].center); + triangleIndexedA.connects.push({ to: j, edge: overlap, distance }); + triangleIndexedB.connects.push({ to: i, edge: overlap, distance }); + } } + } + return { vertices, convexPolygons }; +} + +function findClosestPath(convexPolygons, start, end, visited = [], path = []) { + if (start === end) return []; + + visited = [...visited, start]; + + const { connects } = convexPolygons[start]; + + const finish = connects.find(({ to }) => to === end); + if (finish) return [...path, finish]; + + const posibilities = []; + for (const connect of connects) { + if (visited.includes(connect.to)) continue; + + const posibility = findClosestPath(convexPolygons, connect.to, end, visited, [...path, connect]); + if (posibility) posibilities.push(posibility); + } + + if (posibilities.length === 0) { + return null; + } else if (posibilities.length === 1) { + return posibilities[0]; + } else if (posibilities.length > 1) { const distanceMap = new WeakMap(); - - for (let i = 0; i < snappedCombPaths.length; i ++) { - const snappedCombPath = snappedCombPaths[i]; - - const distanceStart = distanceTo(start, snappedCombPath[0]); - const distanceEnd = distanceTo(start, snappedCombPath[snappedCombPath.length - 1]); - - if (distanceStart < distanceEnd) { - distanceMap.set(snappedCombPath, distanceStart); - } else { - snappedCombPath.reverse(); - distanceMap.set(snappedCombPath, distanceEnd); - } - } - snappedCombPaths.sort((a, b) => distanceMap.get(a) - distanceMap.get(b)); - - const firstPath = snappedCombPaths[0]; - const lastPath = snappedCombPaths[snappedCombPaths.length - 1]; - - if (snappedCombPaths.length === 0) { - snappedCombPaths.push([start], [end]); - } else if (distanceTo(firstPath[0], start) > 1.0) { - snappedCombPaths.unshift([start]); - } else if (distanceTo(lastPath[lastPath.length - 1], end) > 1.0) { - snappedCombPaths.push([end]); + for (const posibility of posibilities) { + const distance = posibility.reduce((totalDistance, connect) => totalDistance + connect.distance, 0); + distanceMap.set(posibility, distance); } - if (snappedCombPaths.length === 1) { - continue; - } - - const startPath = snappedCombPaths[0]; - const startPoint = startPath[startPath.length - 1]; - - const endPath = snappedCombPaths[snappedCombPaths.length - 1]; - const endPoint = endPath[0]; - - const lineIndexStart = findClosestLineOnPath(outlinePart, startPoint); - const lineIndexEnd = findClosestLineOnPath(outlinePart, endPoint); - - const path = []; - if (lineIndexEnd === lineIndexStart) { - continue; - } else if (lineIndexEnd > lineIndexStart) { - if (lineIndexStart + outlinePart.length - lineIndexEnd < lineIndexEnd - lineIndexStart) { - for (let i = lineIndexStart + outlinePart.length; i > lineIndexEnd; i --) { - path.push(outlinePart[i % outlinePart.length]); - } - } else { - for (let i = lineIndexStart; i < lineIndexEnd; i ++) { - path.push(outlinePart[i + 1]); - } - } - } else { - if (lineIndexEnd + outlinePart.length - lineIndexStart < lineIndexStart - lineIndexEnd) { - for (let i = lineIndexStart; i < lineIndexEnd + outlinePart.length; i ++) { - path.push(outlinePart[(i + 1) % outlinePart.length]); - } - } else { - for (let i = lineIndexStart; i > lineIndexEnd; i --) { - path.push(outlinePart[i]); - } - } - } - - combPath = new Shape([[...startPath, ...path, ...endPath]], false, true, false, true); + return posibilities.sort((a, b) => distanceMap.get(a) - distanceMap.get(b))[0]; } - - return combPath.mapToLower()[0]; } -function findClosestLineOnPath(path, point) { - let distance = Infinity; - let lineIndex; +// const parse = string => parseFloat(string); +// function findClosestPath(map, start, end) { +// // dijkstra's algorithm +// const costs = { [start]: 0 }; +// const open = { [0]: [start] }; +// const predecessors = {}; +// +// while (open) { +// const keys = Object.keys(open).map(parse); +// if (keys.length === 0) break; +// keys.sort(); +// +// const [key] = keys; +// const bucket = open[key]; +// const node = bucket.shift(); +// const currentCost = key; +// const { connects } = map[node]; +// +// if (!bucket.length) delete open[key]; +// +// for (const { distance, to } of connects) { +// const totalCost = distance + currentCost; +// const vertexCost = costs[to]; +// +// if ((typeof vertexCost === 'undefined') || (vertexCost > totalCost)) { +// costs[to] = totalCost; +// +// if (!open[totalCost]) open[totalCost] = []; +// open[totalCost].push(to); +// +// predecessors[to] = node; +// } +// } +// } +// +// if (typeof costs[end] === 'undefined') return null; +// +// const nodes = []; +// let node = end; +// while (typeof node !== 'undefined') { +// nodes.push(node); +// node = predecessors[node]; +// } +// nodes.reverse(); +// +// const path = []; +// for (let i = 1; i < nodes.length; i ++) { +// const from = nodes[i - 1]; +// const to = nodes[i]; +// +// const connection = map[from].connects.find(connect => connect.to === to); +// path.push(connection); +// } +// +// return path; +// } - for (let i = 0; i < path.length; i ++) { - const pointA = path[i]; - const pointB = path[(i + 1) % path.length]; +function containLineInPath(path, start, end, vertices) { + const line = [start]; - const tempClosestPoint = findClosestPointOnLine(pointA, pointB, point); - const tempDistance = distanceTo(tempClosestPoint, point); + for (const { edge: [indexA, indexB] } of path) { + const vertexA = vertices[indexA]; + const vertexB = vertices[indexB]; - if (tempDistance < distance) { - distance = tempDistance; - lineIndex = i; + const intersection = lineIntersection(start, end, vertexA, vertexB); + if (!intersection) { + const lastPoint = line[line.length - 1]; + const distanceA = distanceTo(lastPoint, vertexA) + distanceTo(vertexA, end); + const distanceB = distanceTo(lastPoint, vertexB) + distanceTo(vertexB, end); + + line.push(distanceA < distanceB ? vertexA : vertexB); } } - return lineIndex; -} - -function findClosestPointOnLine(a, b, c) { - const b_ = subtract(b, a); - const c_ = subtract(c, a); - - const lambda = dot(normalize(b_), c_) / length(b_); - - if (lambda >= 1) { - return b; - } else if (lambda > 0) { - return add(a, scale(b_, lambda)); - } else { - return a; - } + line.push(end); + return line; } diff --git a/src/sliceActions/slice.js b/src/sliceActions/slice.js index 592a7a5..ab9826f 100644 --- a/src/sliceActions/slice.js +++ b/src/sliceActions/slice.js @@ -11,7 +11,7 @@ import shapesToSlices from './shapesToSlices.js'; import slicesToGCode from './slicesToGCode.js'; import applyPrecision from './applyPrecision.js'; import { hslToRgb } from './helpers/color.js'; -// // import removePrecision from './removePrecision.js'; +import removePrecision from './removePrecision.js'; export default function slice(settings, geometry, openObjectIndexes, constructLinePreview, onProgress) { const total = 11; @@ -48,7 +48,7 @@ export default function slice(settings, geometry, openObjectIndexes, constructLi updateProgress('Optimizing paths'); optimizePaths(slices, settings); - // removePrecision(slices); + removePrecision(slices); updateProgress('Constructing gcode'); const gcode = slicesToGCode(slices, settings); diff --git a/src/sliceActions/slicesToGCode.js b/src/sliceActions/slicesToGCode.js index 19ee59d..c1434e9 100644 --- a/src/sliceActions/slicesToGCode.js +++ b/src/sliceActions/slicesToGCode.js @@ -52,7 +52,7 @@ export default function slicesToGCode(slices, settings) { const part = slice.parts[i]; if (part.closed) { - const outline = part.shell[0]; + const outline = part.shell[0].mapToLower(); for (let i = 0; i < part.shell.length; i ++) { const shell = part.shell[i]; @@ -72,7 +72,8 @@ export default function slicesToGCode(slices, settings) { } if (typeof slice.support !== 'undefined') { - pathToGCode(slice.supportOutline, combing, gcode, slice.support, true, true, z, profiles.support); + const supportOutline = slice.supportOutline.mapToLower(); + pathToGCode(supportOutline, combing, gcode, slice.support, true, true, z, profiles.support); } } @@ -95,7 +96,7 @@ function pathToGCode(outline, combing, gcode, shape, retract, unRetract, z, prof if (i === 0) { if (combing) { - const combPath = comb(outline, divide(gcode._nozzlePosition, PRECISION), point); + const combPath = comb(outline, gcode._nozzlePosition, point); for (let i = 0; i < combPath.length; i ++) { const combPoint = combPath[i]; gcode.moveTo(combPoint.x, combPoint.y, z, travelProfile);