diff --git a/bin/esbuild.config.mjs b/bin/esbuild.config.mjs index 4881ab00..0cb973bb 100644 --- a/bin/esbuild.config.mjs +++ b/bin/esbuild.config.mjs @@ -12,7 +12,9 @@ const MESH_OUTFILE = 'src/pack/mesh-main.js'; const MESH_EXTRAS = [ ]; const KIRI_OUTFILE = 'src/pack/kiri-main.js'; -const KIRI_EXTRAS = [ ]; +const KIRI_EXTRAS = [ + 'src/ext/base64.js' +]; async function appendExtraModules(extras, outfile, minify = false) { const files = await glob(extras); diff --git a/mods/bambu/bambu.js b/mods/bambu/bambu.js index 2a74ebe7..2903cfbd 100644 --- a/mods/bambu/bambu.js +++ b/mods/bambu/bambu.js @@ -1115,7 +1115,7 @@ self.kiri.load(api => { ]), h.div({ class: "var-row f-grow" }, [ h.label('active'), - h.input({ id: "bbl_file_active", class: "t-left", readonly }) + h.input({ id: "bbl_file_active", class: "t-left", style: "width: 20ch; max-width: 20ch", readonly }) ]), h.div({ class: "pop-sep" }), h.div({ class: "f-row gap3 f-grow" }, [ diff --git a/notes.md b/notes.md index a7036e77..5efbac1a 100644 --- a/notes.md +++ b/notes.md @@ -31,6 +31,10 @@ * `B` multi-extruder rendering of raft fails to offset the rest of the print * `B` multi-extruder purge blocks fail to generate properly for rafts +* `F` min layer time +* `F` separate bridging speed / fan control +* `F` elephant foot compensation +* `F` inner / outer perimeter compensation * `F` better thin-wall (like arachne) * `F` add scarf seams * `F` add 'random' and 'seam' start point options diff --git a/package.json b/package.json index 112e16e8..5e9bdcc9 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "fast-glob": "^3.3.3", "i": "^0.3.7", "jszip": "^3.7.1", - "manifold-3d": "^3.1.1", + "manifold-3d": "^3.2.1", "moment": "^2.29.4", "npm": "^11.4.2", "prettier": "^3.5.3", diff --git a/src/geo/polygon.js b/src/geo/polygon.js index 11a5732d..846515f9 100644 --- a/src/geo/polygon.js +++ b/src/geo/polygon.js @@ -258,9 +258,10 @@ export class Polygon { // of the polygon with the noodle removed (for the next pass) noodle(width) { let clone = this.clone(true); - let ins = clone.offset(width); + let ins = clone.offset(width) ?? []; + let remain = ins.clone(true); let nood = POLY.nest(POLY.flatten([ clone, ...ins ], [], true)); - return { noodle: nood, remain: ins }; + return { noodle: nood, remain }; } // generate center crossing point cloud @@ -652,7 +653,7 @@ export class Polygon { td += d; if (ap.length === 1 && td >= pe1) { ap.push(np); - } else if (ap.length === 2 && td >= pe2) { + } else if (ap.length === 2 && (td >= pe2 || i >= l)) { ap.push(np); break; } diff --git a/src/kiri/core/conf.js b/src/kiri/core/conf.js index 2e9aa772..d8f38f30 100644 --- a/src/kiri/core/conf.js +++ b/src/kiri/core/conf.js @@ -556,6 +556,7 @@ export const conf = { camTabsDepth: 5, camTabsMidline: false, camDepthFirst: false, + camInnerFirst: false, camEaseDown: false, camEaseAngle: 10, camOriginTop: true, @@ -639,9 +640,6 @@ export const conf = { ctOriginBounds: false, outputInvertX: false, outputInvertY: false, - ctOutKnifeDepth: 1, - ctOutKnifePasses: 1, - ctOutKnifeTip: 2, ctOutInches: false, ctOutShaper: false } @@ -728,9 +726,6 @@ export const conf = { ctOriginBounds: false, outputInvertX: false, outputInvertY: false, - ctOutKnifeDepth: 1, - ctOutKnifePasses: 1, - ctOutKnifeTip: 2, ctOutStack: false, }, }, diff --git a/src/kiri/core/devices.js b/src/kiri/core/devices.js index 0c508851..b4d7c701 100644 --- a/src/kiri/core/devices.js +++ b/src/kiri/core/devices.js @@ -331,7 +331,7 @@ function renderDevices(devices) { }; ui.deviceExport.onclick = function(event) { const record = { - version: kiri.version, + version: api.version, device: selected, process: api.process.code(), profiles: event.altKey ? settings.prof() : undefined, diff --git a/src/kiri/core/export.js b/src/kiri/core/export.js index ae3c50cf..b7440ab1 100644 --- a/src/kiri/core/export.js +++ b/src/kiri/core/export.js @@ -8,6 +8,7 @@ import { util } from '../../geo/base.js'; import { MODES } from './consts.js'; import { LASER as laser_driver } from '../mode/laser/driver.js'; import { SLA as sla_client } from '../mode/sla/client.js'; +import { hash } from '../../ext/md5.js'; let printSeq = parseInt(local['kiri-print-seq'] || local['print-seq'] || "0") + 1; @@ -654,7 +655,7 @@ function exportGCodeDialog(gcode, sections, info, names) { data: gcode },{ name: `Metadata/plate_1.gcode.md5`, - data: ext.md5.hash(gcode) + data: hash(gcode) },{ name: `Metadata/plate_1_small.png`, data: api.view.bambu.s128.png diff --git a/src/kiri/core/init.js b/src/kiri/core/init.js index 65703282..02dd0a55 100644 --- a/src/kiri/core/init.js +++ b/src/kiri/core/init.js @@ -20,6 +20,7 @@ import { menu as menuCAM } from '../mode/cam/init-menu.js'; import { menu as menuFDM } from '../mode/fdm/init-menu.js'; import { menu as menuLaser } from '../mode/laser/init-menu.js'; import { menu as menuSLA } from '../mode/sla/init-menu.js'; +import './tools.js'; let { CAM, SLA, FDM, LASER, DRAG, WJET, WEDM } = MODES, { client, catalog, platform, selection, stats } = api, @@ -1322,7 +1323,8 @@ function init_one() { yr = ((tl && yc) || (!tl && yt) ? ra : 1), zr = ((tl && zc) || (!tl && zt) ? ra : 1); // prevent null scale - if (xr * yr * zr === 0) { + if (xv * xr < 0.1 || yv * yr < 0.1 || zv * zr < 0.1) { + api.alerts.show('invalid scale value'); return; } selection.scale(xr,yr,zr); @@ -1349,7 +1351,11 @@ function init_one() { xr = ((tl && xc) || (!tl && xt) ? ra : 1), yr = ((tl && yc) || (!tl && yt) ? ra : 1), zr = ((tl && zc) || (!tl && zt) ? ra : 1); - + // prevent null scale + if (xv * xr < 0.1 || yv * yr < 0.1 || zv * zr < 0.1) { + api.alerts.show('invalid scale value'); + return; + } selection.scale(xr,yr,zr); ui.scaleX.was = ui.scaleX.value = xv * xr; ui.scaleY.was = ui.scaleY.value = yv * yr; @@ -1361,6 +1367,7 @@ function init_one() { e.target.value = val; } + // on enter but not on blur space.event.onEnterKey([ ui.scaleX, selectionScale, ui.scaleY, selectionScale, @@ -1368,6 +1375,9 @@ function init_one() { ui.sizeX, selectionSize, ui.sizeY, selectionSize, ui.sizeZ, selectionSize, + ]); + // on enter and blur + space.event.onEnterKey([ ui.toolName, updateTool, ui.toolNum, updateTool, ui.toolFluteDiam, updateTool, diff --git a/src/kiri/core/main.js b/src/kiri/core/main.js index 1cb4d968..51a00689 100644 --- a/src/kiri/core/main.js +++ b/src/kiri/core/main.js @@ -1,6 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import './frame.js'; +import '../../ext/base64.js'; import lang from './lang.js'; import STACKS from './stacks.js'; diff --git a/src/kiri/core/print.js b/src/kiri/core/print.js index 6be1665d..d0487b52 100644 --- a/src/kiri/core/print.js +++ b/src/kiri/core/print.js @@ -88,7 +88,7 @@ class Print { } output.appendAll(input); } - + /** * Prints a polygon to a given output array, possibly with a given extrude factor, * and starting from a given point. The last point is returned. @@ -362,14 +362,11 @@ class Print { // if( axis == 'I' || axis == "J") return if (abs) { pos[axis] = val; - if (axis == "X") point.x = factor * pos.X + xoff.x + off.x else if (axis == "Y") point.y = factor * pos.Y + xoff.y + off.y else if (axis == "Z") point.z = factor * pos.Z + xoff.z + off.z + dz - - } else { - mov[axis] = val; + // mov[axis] = val; pos[axis] += val; } // console.log("position updated",structuredClone(pos)) @@ -393,7 +390,6 @@ class Print { center = center.add(prevPoint); center.setZ((prevPoint.z+point.z)/2+dz); } - return { center, point, @@ -402,10 +398,8 @@ class Print { } function outputPoint(point,lastP,emit,{center,arcPoints,retract}) { - // non-move in a new plane means burp out // the old sequence and start a new one - if (newlayer || (autolayer && seq.z != point.z)) { newlayer = false; let dz = point.z - seq.z; diff --git a/src/kiri/core/selection.js b/src/kiri/core/selection.js index 8349cd26..f5fb2a91 100644 --- a/src/kiri/core/selection.js +++ b/src/kiri/core/selection.js @@ -49,8 +49,12 @@ function update_info() { const ui = api.ui; let bounds = new THREE.Box3(), track; for_meshes(mesh => { - bounds = bounds.union(mesh.getBoundingBox()); + let box = mesh.getBoundingBox().clone(); track = mesh.widget.track; + let {pos} = track + box.min.add(pos); + box.max.add(pos); + bounds = bounds.union(box); }); if (bounds.min.x === Infinity) { if (selectedMeshes.length === 0) { diff --git a/src/kiri/core/settings.js b/src/kiri/core/settings.js index 1831126e..1a5b65b0 100644 --- a/src/kiri/core/settings.js +++ b/src/kiri/core/settings.js @@ -544,12 +544,14 @@ function settingsImport(data, ask) { return; } } + if (api.const.LOCAL) console.log('import', data); - let isSettings = (data.settings && data.version && data.time); - let isProcess = (data.process && data.version && data.time && data.mode && data.name); - let isDevice = (data.device && data.version && data.time); - let isTools = (data.tools && data.version && data.time) + let isSettings = (data.settings && data.time); + let isProcess = (data.process && data.time && data.mode && data.name); + let isDevice = (data.device && data.time); + let isTools = (data.tools && data.time) let isWork = (data.work); + if (!isSettings && !isDevice && !isProcess && !isTools) { uc.alert('invalid settings or device format'); console.log('data',data); diff --git a/src/kiri/core/tools.js b/src/kiri/core/tools.js index 9ab3a079..e4738c22 100644 --- a/src/kiri/core/tools.js +++ b/src/kiri/core/tools.js @@ -2,9 +2,9 @@ import { api } from './api.js'; import { THREE } from '../../ext/three.js'; +import { space } from '../../moto/space.js'; const { Vector3, Quaternion } = THREE; -const { kiri, moto } = root; const XAXIS = new THREE.Vector3(1,0,0); const ZAXIS = new THREE.Vector3(0,0,1); @@ -106,7 +106,7 @@ api.event.on('tool.mesh.lay-flat', () => { }); function scale() { - let cam = moto.space.internals().camera; + let cam = space.internals().camera; let dist = cam.position.distanceTo(pmesh.position); let scale = opName === 'camera focus' ? Math.min(0.25, 100 / dist) : dist / 100; diff --git a/src/kiri/core/widget.js b/src/kiri/core/widget.js index 7e0cd9cf..3e340433 100644 --- a/src/kiri/core/widget.js +++ b/src/kiri/core/widget.js @@ -155,7 +155,7 @@ class Widget { * @returns {Widget} */ loadVertices(data, options = { index: false }) { - // console.log({ loadVertices: this.id, worker: this.inWorker, data }); + // console.trace({ loadVertices: this.id, worker: this.inWorker, data }); let vertices, autoscale = false; if (ArrayBuffer.isView(data) || typeof(data) != 'object') { diff --git a/src/kiri/core/widgets.js b/src/kiri/core/widgets.js index 56c1e9af..3cfe2d62 100644 --- a/src/kiri/core/widgets.js +++ b/src/kiri/core/widgets.js @@ -63,7 +63,7 @@ function replace(vertices, sel) { } else { // dialog $('load-file').onchange = function(event) { - load.File.load(event.target.files[0]) + load(event.target.files[0]) .then(data => onload(data[0].mesh)) .catch(error => console.log({error})); }; diff --git a/src/kiri/dev/fdm/flashforge.5m.pro.json b/src/kiri/dev/fdm/flashforge.5m.pro.json new file mode 100644 index 00000000..154f51d9 --- /dev/null +++ b/src/kiri/dev/fdm/flashforge.5m.pro.json @@ -0,0 +1,219 @@ +{ + "version": "4.2.3", + "device": "Flashforge 5M Pro", + "process": { + "processName": "Flashforge 5m Pro .2 Height", + "sliceHeight": 0.2, + "sliceShells": 3, + "sliceShellOrder": "in-out", + "sliceLayerStart": "last", + "sliceFillAngle": 45, + "sliceFillOverlap": 0.6, + "sliceFillSparse": 0.08, + "sliceFillRate": 300, + "sliceFillType": "hex", + "sliceSupportDensity": 0.2, + "sliceSupportOffset": 1, + "sliceSupportGap": 1, + "sliceSupportSize": 5, + "sliceSupportArea": 10, + "sliceSupportExtra": 3, + "sliceSupportAngle": 50, + "sliceSupportNozzle": "0", + "sliceSupportEnable": false, + "sliceSolidMinArea": 1.2, + "sliceBottomLayers": 2, + "sliceTopLayers": 2, + "firstSliceHeight": 0.2, + "firstLayerRate": 30, + "firstLayerFillRate": 40, + "firstLayerPrintMult": 1, + "firstLayerLineMult": 1, + "firstLayerYOffset": 0, + "firstLayerNozzleTemp": 205, + "firstLayerBedTemp": 56, + "firstLayerBrim": 0, + "firstLayerBrimTrig": 0, + "firstLayerBeltLead": 3, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputTemp": 205, + "outputBedTemp": 56, + "outputFeedrate": 500, + "outputFinishrate": 250, + "outputSeekrate": 500, + "outputShellMult": 1.25, + "outputFillMult": 1.25, + "outputSparseMult": 1.3, + "outputRetractDist": 4, + "outputRetractSpeed": 40, + "outputRetractDwell": 20, + "outputBrimCount": 2, + "outputBrimOffset": 4, + "outputShortPoly": 100, + "outputMinSpeed": 10, + "outputCoastDist": 0, + "outputLayerRetract": false, + "outputInvertX": false, + "outputInvertY": false, + "sliceMinHeight": 0, + "sliceAdaptive": false, + "zHopDistance": 0.2, + "antiBacklash": 1, + "sliceFillWidth": 1, + "firstLayerFanSpeed": 0, + "outputFanSpeed": 255, + "outputRetractWipe": 4, + "outputPurgeTower": 0, + "arcTolerance": 0.15, + "ranges": [], + "sliceLineWidth": 0, + "sliceFillRepeat": 1, + "sliceSupportSpan": 5, + "sliceSupportOutline": false, + "firstLayerBrimIn": 0, + "firstLayerBrimGap": 0, + "firstLayerBeltBump": 0, + "firstLayerFlatten": 0, + "outputDraftShield": false, + "outputBeltFirst": false, + "outputAvoidGaps": true, + "outputLoops": 0, + "sliceFillGrow": 0, + "sliceSolidRate": 200, + "outputAlternating": false, + "sliceDetectThin": "off", + "sliceLayerStartX": 0, + "sliceLayerStartY": 0, + "sliceSupportGrow": 3, + "sliceAngle": 45, + "outputFanLayer": 1, + "sliceZInterleave": false + }, + "code": { + "mode": "FDM", + "internal": 0, + "bedHeight": 2.5, + "bedWidth": 225, + "bedDepth": 225, + "bedRound": false, + "bedBelt": false, + "deviceZMax": 0, + "gcodeTime": 1, + "maxHeight": 225, + "originCenter": true, + "gcodeFan": [ + "M106 S{fan_speed}" + ], + "gcodeFeature": [], + "gcodeTrack": [], + "gcodeLayer": [ + ";AFTER_LAYER_CHANGE", + ";[layer_z]" + ], + "gcodePre": [ + "; EXECUTABLE_BLOCK_START", + "M73 P0 R80", + "M106 S0", + "M106 P2 S0", + ";TYPE:Custom", + "M190 S55", + "M104 S210", + "G90", + "M83", + "G1 Z5 F6000", + "G1 E-0.2 F800", + "G1 X110 Y-110 F6000", + "G1 E2 F800", + "G1 Y-110 X55 Z0.25 F4800", + "G1 X-55 E8 F2400", + "G1 Y-109.6 F2400", + "G1 X55 E5 F2400", + "G1 Y-110 X55 Z0.45 F4800", + "G1 X-55 E8 F2400", + "G1 Y-109.6 F2400", + "G1 X55 E5 F2400", + "G92 E0", + "G90", + "G21", + "M83 ; use relative distances for extrusion", + "; filament start gcode", + ";right_extruder_material: PLA", + "SET_PRESSURE_ADVANCE ADVANCE=0; Override pressure advance value", + "M106 S0", + "M106 P2 S0", + ";LAYER_CHANGE", + ";Z:0.2", + ";HEIGHT:0.2", + ";BEFORE_LAYER_CHANGE", + ";0.2", + "G1 E-.8 F2100", + ";AFTER_LAYER_CHANGE", + ";0.2", + ";_SET_FAN_SPEED_CHANGING_LAYER", + "SET_VELOCITY_LIMIT ACCEL=500", + "G1 X-25.945 Y-17.607 F30000", + "G1 Z.4", + "G1 Z.2", + "G1 E.8 F2100" + ], + "gcodePost": [ + "M106 S0", + "M106 P2 S0", + ";TYPE:Custom", + "; filament end gcode", + "G1 E-3 F3600", + "G0 X50 Y50 F30000", + "M104 S0 ; turn off temperature", + "M73 P100 R0" + ], + "gcodeProc": "", + "gcodeFExt": "gcode", + "extruders": [ + { + "extFilament": 1.75, + "extNozzle": 0.4, + "extSelect": [ + "T0" + ], + "extDeselect": [], + "extOffsetX": 0, + "extOffsetY": 0 + }, + { + "extFilament": 1.75, + "extNozzle": 0.4, + "extSelect": [ + "T1" + ], + "extDeselect": [], + "extOffsetX": 30, + "extOffsetY": 0 + } + ], + "new": false, + "deviceName": "Flashforge 5M Pro", + "imageURL": "", + "fwRetract": true, + "filamentSource": "direct", + "profiles": [], + "extras": { + "palette": { + "printer": "6806c3eabbdb", + "ping": null, + "feed": null, + "push": null, + "offset": null, + "heat": null, + "cool": null, + "press": null + } + }, + "gcodeChange": [ + "T{tool}" + ] + }, + "time": 1756051792656, + "mode": "FDM", + "name": "Flashforge 5m Pro .2 Height" +} \ No newline at end of file diff --git a/src/kiri/dev/fdm/taz.lulzbot.747.json b/src/kiri/dev/fdm/taz.lulzbot.747.json new file mode 100644 index 00000000..f1f225fa --- /dev/null +++ b/src/kiri/dev/fdm/taz.lulzbot.747.json @@ -0,0 +1,207 @@ +{ + "version": "4.2.3", + "device": "Taz LulzBot 747", + "process": { + "processName": "747 .3 LH", + "sliceHeight": 0.3, + "sliceShells": 3, + "sliceShellOrder": "in-out", + "sliceLayerStart": "last", + "sliceFillAngle": 45, + "sliceFillOverlap": 0.3, + "sliceFillSparse": 0.07, + "sliceFillRate": 70, + "sliceFillType": "hex", + "sliceSupportDensity": 0.1, + "sliceSupportOffset": 1, + "sliceSupportGap": 1, + "sliceSupportSize": 25, + "sliceSupportArea": 0.1, + "sliceSupportExtra": 0, + "sliceSupportAngle": 50, + "sliceSupportNozzle": 0, + "sliceSupportEnable": false, + "sliceSolidMinArea": 1, + "sliceBottomLayers": 2, + "sliceTopLayers": 3, + "firstSliceHeight": 0.3, + "firstLayerRate": 15, + "firstLayerFillRate": 15, + "firstLayerPrintMult": 1, + "firstLayerLineMult": 1, + "firstLayerYOffset": 0, + "firstLayerNozzleTemp": 210, + "firstLayerBedTemp": 56, + "firstLayerBrim": 0, + "firstLayerBrimTrig": 0, + "firstLayerBeltLead": 3, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputTemp": 210, + "outputBedTemp": 56, + "outputFeedrate": 80, + "outputFinishrate": 40, + "outputSeekrate": 120, + "outputShellMult": 1.15, + "outputFillMult": 1.15, + "outputSparseMult": 1.15, + "outputRetractDist": 0.9, + "outputRetractSpeed": 25, + "outputRetractDwell": 20, + "outputBrimCount": 2, + "outputBrimOffset": 4, + "outputShortPoly": 100, + "outputMinSpeed": 10, + "outputCoastDist": 0, + "outputLayerRetract": true, + "outputInvertX": false, + "outputInvertY": false, + "sliceMinHeight": 0, + "sliceAdaptive": false, + "zHopDistance": 0.6, + "antiBacklash": 1, + "sliceFillWidth": 1, + "firstLayerFanSpeed": 200, + "outputFanSpeed": 255, + "outputRetractWipe": 0, + "outputPurgeTower": 0, + "arcTolerance": 0, + "ranges": [], + "sliceLineWidth": 0, + "sliceFillRepeat": 1, + "sliceSupportSpan": 5, + "sliceSupportOutline": false, + "firstLayerBrimIn": 0, + "firstLayerBrimGap": 0, + "firstLayerBeltBump": 0, + "firstLayerFlatten": 0, + "outputDraftShield": false, + "outputBeltFirst": false, + "outputAvoidGaps": true, + "outputLoops": 0, + "sliceFillGrow": 0, + "sliceSolidRate": 40, + "outputAlternating": false, + "sliceDetectThin": "off", + "sliceLayerStartX": 0, + "sliceLayerStartY": 0, + "sliceSupportGrow": 0, + "sliceAngle": 45, + "outputFanLayer": 1, + "sliceZInterleave": false + }, + "code": { + "mode": "FDM", + "internal": 0, + "imageURL": "", + "bedHeight": 2.5, + "bedWidth": 231, + "bedDepth": 231, + "bedRound": false, + "bedBelt": false, + "deviceZMax": 0, + "gcodeTime": 1, + "maxHeight": 248, + "originCenter": false, + "gcodeFan": [ + "M106 S{fan_speed}" + ], + "gcodeFeature": [], + "gcodeTrack": [], + "gcodeLayer": [], + "gcodePre": [ + ";Generated by Cura LulzBot Edition GCodeWriter Version: 3.6.37", + ";FLAVOR:Marlin", + ";TIME:7538", + ";Filament used: 5.35322m", + ";Layer height: 0.18", + ";Generated with Cura_SteamEngine 3.6.37-win10", + "M82 ;absolute extrusion mode", + ";This G-Code has been generated specifically for the LulzBot Sidekick with a Universal Tool Head", + ";", + ";The following lines can be uncommented for printer specific fine tuning", + ";More information can be found at https://marlinfw.org/meta/gcode/ (Note: current values are based on SE Tool Head)", + ";", + "M92 E420 ;Set Axis Steps-per-unit", + "M301 P21.0 I1.78 D61.93 ;Set Hotend PID", + "M906 E960 ;TMC Motor Current", + ";", + "G90 ; absolute coordinate", + "M82 ; set extruder to absolute mode", + "G92 E0 ; set extruder position to 0", + "M117 Heating... ; progress indicator message on LCD", + "M140 S50.0 ; start bed heating up", + "M109 R140 ; soften filament before homing Z", + "G28 ; Home all axis", + "G1 E-15 F100 ; retract filament", + "M104 S140 ; start extruder heating to probe temp", + "M190 S57.0 ; wait for bed to reach printing temp", + "G29 ; start auto leveling", + "G0 X0 Y0 F5000", + "M109 R210 ; wait for extruder to reach initial printing temp", + "M117 SideKick Printing... ; progress indicator message on LCD", + "G1 Z2 E0 F75 ; prime tiny bit of filament into the nozzle", + "M221 S100 T0", + "G92 E0", + "G1 F600 E-1.75" + ], + "gcodePost": [ + ";This G-Code has been generated specifically for the LulzBot Sidekick with a Universal Tool Head", + "M400 ; wait for moves to finish", + "M140 S45.0 ; start bed cooling", + "M104 S0 ; disable hotend", + "M107 ; disable fans", + "G92 E5 ; set extruder to 5mm for retract on print end", + "M117 Cooling please wait ; progress indicator message on LCD", + "G1 Y-10 Z251.3 E0 F3000 ; move to cooling position", + "G1 E5 ; re-prime extruder", + "M190 R45.0 ; wait for bed to cool down to removal temp", + "G27 ; present finished print", + "M140 S0; keep temperature or cool down", + "M77\t\t\t\t\t ; End GLCD Print Timer", + "G90 ; absolute positioning", + "M18 X E\t\t\t\t \t ; turn off x and e axis", + "M117 Print Complete. ; print complete message", + "M82 ;absolute extrusion mode", + "M104 S0", + ";End of Gcode" + ], + "gcodeProc": "", + "gcodeFExt": "gcode", + "extruders": [ + { + "extFilament": 1.75, + "extNozzle": 0.5, + "extSelect": [ + "T0" + ], + "extDeselect": [], + "extOffsetX": 0, + "extOffsetY": 0 + } + ], + "new": false, + "deviceName": "Taz LulzBot 747", + "fwRetract": false, + "filamentSource": "direct", + "profiles": [], + "extras": { + "palette": { + "printer": "6806c3eabbdb", + "ping": null, + "feed": null, + "push": null, + "offset": null, + "heat": null, + "cool": null, + "press": null + } + }, + "gcodeChange": [ + "T{tool}" + ] + }, + "time": 1756051830810, + "mode": "FDM", + "name": "747 .3 LH" +} \ No newline at end of file diff --git a/src/kiri/mode/cam/cl-flip.js b/src/kiri/mode/cam/cl-flip.js index 427ae9db..166d8b4a 100644 --- a/src/kiri/mode/cam/cl-flip.js +++ b/src/kiri/mode/cam/cl-flip.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { api } from '../../core/api.js'; -import { env, opRender } from './client.js'; +import { env, opAdd, opRender } from './client.js'; import { CAM } from './driver-fe.js'; import { clearTabs, restoreTabs } from './cl-tab.js'; import { updateStock } from './cl-stock.js'; diff --git a/src/kiri/mode/cam/cl-tab.js b/src/kiri/mode/cam/cl-tab.js index 8237065e..0cda4f12 100644 --- a/src/kiri/mode/cam/cl-tab.js +++ b/src/kiri/mode/cam/cl-tab.js @@ -61,7 +61,7 @@ export function createTabBox(iw, ic, n) { const { camZBottom, camStockIndexed } = process; const sz = stock.z || bounds.max.z; const zto = sz - iw.track.top; - const zp = (env.camZBottom || camStockIndexed ? env.camZBottom : sz - track.box.d - zto) + (camTabsMidline ? 0 : camTabsHeight / 2); + const zp = (camZBottom || camStockIndexed ? camZBottom : sz - track.box.d - zto) + (camTabsMidline ? 0 : camTabsHeight / 2); ic.x += n.x * camTabsDepth / 2; // offset from part ic.z -= n.y * camTabsDepth / 2; // offset swap z,y ic.y = zp; // offset swap in world space y,z diff --git a/src/kiri/mode/cam/client.js b/src/kiri/mode/cam/client.js index 59fa9e10..bd509dbf 100644 --- a/src/kiri/mode/cam/client.js +++ b/src/kiri/mode/cam/client.js @@ -226,7 +226,7 @@ const traceClear = () => { api.conf.save(); }; -const opAdd = (rec) => { +export const opAdd = (rec) => { if (!env.isCamMode) return; clearPops(); let oplist = env.current.process.ops; diff --git a/src/kiri/mode/cam/export.js b/src/kiri/mode/cam/export.js index 04a8bdb3..84afee08 100644 --- a/src/kiri/mode/cam/export.js +++ b/src/kiri/mode/cam/export.js @@ -53,8 +53,6 @@ export function cam_export(print, online) { lasering = false, laserOp, stock = settings.stock || {}, - ztOff = stock.z - widget.track.top, - bounds = widget.getBoundingBox(), zmax = stock.z, runbox = { max: { x: -Infinity, y: -Infinity, z: -Infinity }, @@ -234,7 +232,7 @@ export function cam_export(print, online) { // speed: Infinity, tool: out.tool, point: { x: newpos.x, y: newpos.y, z: newpos.z, a: pos.a }, - emit: 1, + emit: out.emit, }, { dx: 1, dy: 1, dz: 0, time: 0 }); @@ -251,7 +249,7 @@ export function cam_export(print, online) { // speed: Infinity, tool: out.tool, point: { x: newpos.x, y: newpos.y, z: newpos.z, a: newpos.a }, - emit: 1 + emit: out.emit }, { dx: 0, dy: 0, dz: 1, time: 0 }); @@ -274,6 +272,9 @@ export function cam_export(print, online) { dist = Math.sqrt(dx * dx + dy * dy + dz * dz), newFeed = feed && feed !== pos.f; + // catch NaN in debugging + // if (nl.length === 0 || nl[0] === undefined) + // console.log(out, nl.length, ...nl); // drop dup points (all deltas are 0) if (!arc && !(dx || dy || dz || da)) { @@ -408,7 +409,7 @@ export function cam_export(print, online) { // collect tool info to add to header let toolz = {}, ctool; - const isOffset = offset && (offset.x || offset.y); + const isOffset = offset && (offset.x || offset.y || offset.z); // remap points as necessary for origins, offsets, inversions print.output.forEach(layer => { if (!Array.isArray(layer)) { @@ -427,8 +428,9 @@ export function cam_export(print, online) { // ensure not point is modified twice point.mod = 1; if (isOffset) { - point.x += offset.x; - point.y += offset.y; + point.x += offset.x ?? 0; + point.y += offset.y ?? 0; + point.z -= offset.z ?? 0; } if (scale) { point.x *= scale.x; diff --git a/src/kiri/mode/cam/init-menu.js b/src/kiri/mode/cam/init-menu.js index 69becf60..9cc96073 100644 --- a/src/kiri/mode/cam/init-menu.js +++ b/src/kiri/mode/cam/init-menu.js @@ -97,6 +97,7 @@ export function menu() { camConventional: newBoolean(LANG.ou_conv_s, onBooleanClick, {title:LANG.ou_conv_l}), camEaseDown: newBoolean(LANG.cr_ease_s, onBooleanClick, {title:LANG.cr_ease_l}), camDepthFirst: newBoolean(LANG.ou_depf_s, onBooleanClick, {title:LANG.ou_depf_l}), + camInnerFirst: newBoolean(LANG.ou_inrf_s, onBooleanClick, {title:LANG.ou_inrf_l}), camToolInit: newBoolean(LANG.ou_toin_s, onBooleanClick, {title:LANG.ou_toin_l}), separator: newBlank({ class:"set-sep", driven }), camFirstZMax: newBoolean(LANG.ou_z1st_s, onBooleanClick, {title:LANG.ou_z1st_l}), diff --git a/src/kiri/mode/cam/op-outline.js b/src/kiri/mode/cam/op-outline.js index 67558e4b..c9b7e275 100644 --- a/src/kiri/mode/cam/op-outline.js +++ b/src/kiri/mode/cam/op-outline.js @@ -100,6 +100,11 @@ class OpOutline extends CamOp { // extend cut thru (only when z bottom is 0) if (workarea.bottom_z < 0) { let last = slices[slices.length-1]; + // when step down > full cut depth, clear slices and + // leave only the cut-thru pass(es) + if (op.down > workarea.top_stock - workarea.bottom_part) { + slices.length = 0; + } for (let zneg of base_util.lerp(0, -workarea.bottom_cut, op.down)) { if (!last) continue; let add = last.clone(true); @@ -225,7 +230,7 @@ class OpOutline extends CamOp { let easeDown = process.camEaseDown; let toolDiam = this.toolDiam; - let cutdir = op.ov_conv;//process.camConventional; + let cutdir = op.ov_conv; let depthFirst = process.camDepthFirst; let depthData = []; @@ -251,9 +256,9 @@ class OpOutline extends CamOp { }); // set cut direction on outer polys - POLY.setWinding(t, cutdir); + POLY.setWinding(t, !cutdir); // set cut direction on inner polys - POLY.setWinding(c, !cutdir); + POLY.setWinding(c, cutdir); if (depthFirst) { depthData.push(polys); @@ -269,9 +274,11 @@ class OpOutline extends CamOp { let polyLast; // console.log({order, orderPolys}); printPoint = poly2polyEmit(orderPolys, printPoint, function(poly, index, count) { - polyLast = polyEmit(poly, index, count, polyLast); - }, { swapdir: false }); + }, { + swapdir: false, + weight: process.camInnerFirst + }); }) newLayer(); } diff --git a/src/kiri/mode/cam/op-register.js b/src/kiri/mode/cam/op-register.js index 99cb4052..9c01da50 100644 --- a/src/kiri/mode/cam/op-register.js +++ b/src/kiri/mode/cam/op-register.js @@ -5,7 +5,7 @@ import { Tool } from './tool.js'; import { newPolygon } from '../../../geo/polygon.js'; import { newSlice } from '../../core/slice.js'; import { newPoint } from '../../../geo/point.js'; -import { util as base_util } from '../../../geo/base.js'; +import { util } from '../../../geo/base.js'; class OpRegister extends CamOp { constructor(state, op) { @@ -88,7 +88,7 @@ class OpRegister extends CamOp { step = -step; } } - for (let z of base.util.lerp(startZ, endZ, op.down)) { + for (let z of util.lerp(startZ, endZ, op.down)) { let slice = newSlice(z); addSlices(slice); sliceOut.push(slice); diff --git a/src/kiri/mode/cam/op-rough.js b/src/kiri/mode/cam/op-rough.js index 7145bb73..beb24827 100644 --- a/src/kiri/mode/cam/op-rough.js +++ b/src/kiri/mode/cam/op-rough.js @@ -32,11 +32,12 @@ class OpRough extends CamOp { let roughStock = op.all && isIndexed; let toolDiam = new Tool(settings, op.tool).fluteDiameter(); let trueShadow = process.camTrueShadow === true; + let cutThruBypass = op.down > workarea.top_stock - workarea.bottom_part; updateToolDiams(toolDiam); // clear the stock above the area to be roughed out - if (workarea.top_z > workarea.top_part) { + if (workarea.top_z > workarea.top_part && !cutThruBypass) { let shadow = state.shadow.base.clone(); let step = toolDiam * op.step; let inset = roughStock ? @@ -55,9 +56,16 @@ class OpRough extends CamOp { for (let z = zstart; zsteps > 0; zsteps--) { let slice = newSlice(); slice.z = z; - slice.camLines = POLY.setZ(facing.clone(true), slice.z + roughLeaveZ); + let polys = POLY.setZ(facing.clone(true), slice.z + roughLeaveZ); + if (tabs) { + tabs.forEach(tab => { + tab.off = POLY.expand([tab.poly], toolDiam / 2).flat(); + }); + polys = cutTabs(tabs, polys, slice.z); + } + slice.camLines = polys; slice.output() - .setLayer("face", {face: color, line: color}) + .setLayer("roughing", {face: color, line: color}) .addPolys(slice.camLines); addSlices(slice); camFaces.push(slice); @@ -251,6 +259,12 @@ class OpRough extends CamOp { let last = slices[slices.length-1]; + // when step down > full cut depth, clear slices and + // leave only the cut-thru pass(es) + if (cutThruBypass) { + slices.length = 0; + } + // add cut thru passes if (workarea.bottom_z < 0) for (let zneg of base_util.lerp(0, -workarea.bottom_cut, op.down)) { if (!last) continue; @@ -302,7 +316,9 @@ class OpRough extends CamOp { // set winding specified in output POLY.setWinding(level, cutdir, false); poly2polyEmit(level, printPoint, (poly, index, count) => { - printPoint = polyEmit(poly, index, count, printPoint); + printPoint = polyEmit(poly, index, count, printPoint, {cutFromLast: true}); + }, { + weight: process.camInnerFirst }); newLayer(); } diff --git a/src/kiri/mode/cam/op-trace.js b/src/kiri/mode/cam/op-trace.js index 6ec46dba..52481169 100644 --- a/src/kiri/mode/cam/op-trace.js +++ b/src/kiri/mode/cam/op-trace.js @@ -38,7 +38,7 @@ class OpTrace extends CamOp { let cutdir = ov_conv; let polys = []; let reContour = false; - let canRecontour = offset !== 'none' && down === 0; + let canRecontour = offset !== 'none' && down < 0; let stockRect = stock.center && stock.x && stock.y ? newPolygon().centerRectangle({x:0,y:0}, stock.x, stock.y) : undefined; updateToolDiams(toolDiam); @@ -57,6 +57,7 @@ class OpTrace extends CamOp { let max = Math.max(...zs); if (max - min > 0.0001 && canRecontour) { reContour = true; + down = 0; } } if (false) newSliceOut(0).output() @@ -89,6 +90,7 @@ class OpTrace extends CamOp { if (reContour) { state.contourPolys(widget, slice.camLines); } + POLY.setWinding(slice.camLines, offset === "outside" ? !cutdir : cutdir, false); slice.output() .setLayer("trace follow", {line: color}, false) .addPolys(slice.camLines) @@ -144,7 +146,7 @@ class OpTrace extends CamOp { } else { slice.camLines = POLY.flatten(slice.camLines, null, true); } - POLY.setWinding(slice.camLines, cutdir, false); + POLY.setWinding(slice.camLines, offset === "outside" ? !cutdir : cutdir, false); if (debug && shadow) slice.output() .setLayer("trace shadow", {line: 0xff8811}, false) .addPolys(shadow) diff --git a/src/kiri/mode/cam/prepare.js b/src/kiri/mode/cam/prepare.js index d57bf66e..fac80971 100644 --- a/src/kiri/mode/cam/prepare.js +++ b/src/kiri/mode/cam/prepare.js @@ -85,6 +85,7 @@ export function prepEach(widget, settings, print, firstPoint, update) { easeDown = process.camEaseDown, easeAngle = process.camEaseAngle, depthFirst = process.camDepthFirst, + innerFirst = process.camInnerFirst, engageFactor = process.camFullEngage, arcTolerance = process.camArcTolerance, arcRes = toRadians(process.camArcResolution), @@ -243,7 +244,7 @@ export function prepEach(widget, settings, print, firstPoint, update) { break; } } - camOut(point.clone().setZ(zmax_outer)); + camOut(point.clone().setZ(zmax_outer), 0); points.forEach(function (point, index) { camOut(point, 1); if (index > 0 && index < points.length - 1) { @@ -331,7 +332,7 @@ export function prepEach(widget, settings, print, firstPoint, update) { * @param {number} opts.moveLen typically = tool diameter used to trigger terrain detection * @param {number} opts.factor speed scale factor */ - function camOut(point, emit, opts) { + function camOut(point, emit = 1, opts) { // console.trace({point, emit, opts}) let { center = {}, @@ -392,8 +393,18 @@ export function prepEach(widget, settings, print, firstPoint, update) { let deltaXY = lastPoint.distTo2D(point), deltaZ = point.z - lastPoint.z, absDeltaZ = Math.abs(deltaZ), + hasDelta = deltaXY >= 0.001 && absDeltaZ >= 0, isMove = emit == 0; + // when rapid pluge could cut thru stock, rapid to just above stock + // then continue plunge as a plunge cut + if (deltaZ < 0 && lastPoint.z > stockz && point.z < stockz && emit === 0) { + // console.log('detected plunge cut as rapid move', lastPoint.z, stockz, point.z); + layerPush(point.clone().setZ(stockz + 1), 0, 0, tool); + // change to cutting move for remainder of plunge + emit = 1; + } + // drop points too close together if (!isLathe && !isArc && deltaXY < 0.001 && point.z === lastPoint.z) { // console.trace(["drop dup",lastPoint,point]); @@ -401,7 +412,7 @@ export function prepEach(widget, settings, print, firstPoint, update) { } // convert short planar moves to cuts in some cases - if (!isRough && !isArc && isMove && deltaXY <= moveLen && deltaZ <= 0 && !lasering) { + if (hasDelta && !isRough && !isArc && isMove && deltaXY <= moveLen && deltaZ <= 0 && !lasering) { let iscontour = tolerance > 0; let isflat = absDeltaZ < 0.001; // restrict this to contouring @@ -455,6 +466,11 @@ export function prepEach(widget, settings, print, firstPoint, update) { layerPush(point.clone().setZ(zClearance), 0, 0, tool); // new pos for plunge calc deltaXY = 0; + // if plunge goes below stock, convert to cut + if (emit === 0 && point.z < stockz) { + lastPoint = layerPush(point.clone().setZ(stockz + 1), 0, 0, tool); + emit = 1; + } } } else if (isRough && deltaZ < 0) { layerPush(point.clone().setZ(lastPoint.z), 0, 0, tool); @@ -717,9 +733,10 @@ export function prepEach(widget, settings, print, firstPoint, update) { function emitTrace(slice) { let { tool, rate, plunge } = slice.camTrace; setTool(tool, rate, plunge); - let traceTool = new Tool(settings, tool); - let traceToolDiam = traceTool.fluteDiameter(); - printPoint = poly2polyEmit(slice.camLines, printPoint, polyEmit, { swapdir: false }); + printPoint = poly2polyEmit(slice.camLines, printPoint, polyEmit, { + swapdir: false, + weight: innerFirst + }); newLayer(); } @@ -736,10 +753,15 @@ export function prepEach(widget, settings, print, firstPoint, update) { * @param {number} index - unused * @param {number} count - 1 to set engage factor * @param {Point} fromPoint - the point to rapid move from - * @param {Object} camOutOpts - optional parameters to pass to camOut + * @param {boolean} ops.cutFromLast - whether to emit a 1 when moving from last point. Defaults to false * @returns {Point} - the last point of the polygon */ - function polyEmit(poly, index, count, fromPoint) { + function polyEmit(poly, index, count, fromPoint, ops) { + let { + cutFromLast, + } = ops ?? { + cutFromLast: false + }; let arcQ = [], arcMax = Infinity, // no max arc radius lineTolerance = 0.001, // do not consider points under 0.001mm for arcs @@ -775,7 +797,8 @@ export function prepEach(widget, settings, print, firstPoint, update) { // if(offset == 0) console.log("forEachPoint",point,pidx,points) // console.log({pointA, pointB, indexA, indexB,startIndex}) if (indexA == startIndex) { - camOut(pointA.clone(), 0, { factor: engageFactor }); + //if cutFromLast is true, emit a 1 for a cutting move + camOut(pointA.clone(), cutFromLast ? 1 : 0, { factor: engageFactor }); // if first point, move to and call export function if (arcEnabled) arcQ.push(pointA); } @@ -845,7 +868,6 @@ export function prepEach(widget, settings, print, firstPoint, update) { radFault = Math.abs(angle) > arcRes if (radFault) { // if so, remove first point - console.log("secondary radfault,",structuredClone(arcQ),{angle,arcRes,a,b}) camOut(arcQ.shift(), 1) } @@ -1062,7 +1084,10 @@ export function prepEach(widget, settings, print, firstPoint, update) { fromPoint = depthOutlinePath(fromPoint, depth + 1, levels, radius, emitter, dir, ease); fromPoint = depthOutlinePath(fromPoint, depth + 1, levels, radius, emitter, !dir, ease); return fromPoint; - }, { weight: false, swapdir: false }); + }, { + weight: innerFirst, + swapdir: false + }); return start; } @@ -1098,10 +1123,10 @@ function getZClearPath(terrain, x1, y1, x2, y2, z, zadd, off, over) { } } check.reverse(); + let p1 = newPoint(x1, y1); + let p2 = newPoint(x2, y2); for (let i = 0; i < check.length; i++) { let data = check[i]; - let p1 = newPoint(x1, y1); - let p2 = newPoint(x2, y2); let int = data.tops.map(p => p.intersections(p1, p2, true)).flat(); if (int.length) { maxz = Math.max(maxz, data.z + zadd + over); diff --git a/src/kiri/mode/cam/slice.js b/src/kiri/mode/cam/slice.js index 3afce49a..cf77b042 100644 --- a/src/kiri/mode/cam/slice.js +++ b/src/kiri/mode/cam/slice.js @@ -4,6 +4,7 @@ import { base, util } from '../../../geo/base.js'; import { newLine } from '../../../geo/line.js'; import { newPoint } from '../../../geo/point.js'; import { newPolygon } from '../../../geo/polygon.js'; +import { newSlopeFromAngle } from '../../../geo/slope.js'; import { polygons as POLY } from '../../../geo/polygons.js'; import { setSliceTracker } from '../../core/slice.js'; import { ops as OPS } from './ops.js'; @@ -86,6 +87,8 @@ export async function cam_slice(settings, widget, onupdate, ondone) { bottom_z, bottom_cut }, 3); + // console.log({ track, bounds, stock, workarea }); + return structuredClone(workarea); }; @@ -383,11 +386,11 @@ export function addDogbones(poly, dist, reverse) { let bdiff = ((adiff < 0 ? (180 - adiff) : (180 + adiff)) / 2) + 180; if (!open || (i > 1 && i < length)) { if (isCW && adiff > 45) { - let newa = base.newSlopeFromAngle(lastsl.angle + bdiff); + let newa = newSlopeFromAngle(lastsl.angle + bdiff); newpts.push(lastpt.projectOnSlope(newa, dist)); newpts.push(lastpt.clone()); } else if (!isCW && adiff < -45) { - let newa = base.newSlopeFromAngle(lastsl.angle - bdiff); + let newa = newSlopeFromAngle(lastsl.angle - bdiff); newpts.push(lastpt.projectOnSlope(newa, dist)); newpts.push(lastpt.clone()); } @@ -657,9 +660,9 @@ export async function holes(settings, widget, individual, rec, onProgress) { // keeps the z bottom relative to the part when z align changes zBottom = isIndexed ? camZBottom : camZBottom - zbOff; - let slicerOpts = { flatoff: 0.001 } + let slicerOpts = { flatoff: 0.001 }; let slicer = new cam_slicer(widget, slicerOpts); - let zFlats = Object.keys(slicer.zFlat).map(Number).map(z => [z, z - 0.002]).flat() + let zFlats = Object.keys(slicer.zFlat).map(Number).map(z => [z, z - 0.002]).flat(); precision = Math.max(0, precision) let intervals = (precision == 0) ? [] : slicer.interval( @@ -669,11 +672,11 @@ export async function holes(settings, widget, individual, rec, onProgress) { } ) - let zees = [...zFlats, ...intervals] + let zees = [...zFlats, ...intervals]; let indices = [...new Set(zees .map(kv => parseFloat(kv).round(5)) .filter(z => z !== null) - )] + )]; let centerDiff = diam * 0.1, area = (diam / 2) * (diam / 2) * Math.PI, circles = [], @@ -689,19 +692,20 @@ export async function holes(settings, widget, individual, rec, onProgress) { for (let [i, slice] of slices.entries()) { for (let top of slice.tops) { // console.log("slicing",slice.z,top) - slice.shadow = computeShadowAt(widget, slice.z, 0) + slice.shadow = computeShadowAt(widget, slice.z, 0); let inner = top.inner; if (!inner) { //no holes continue; } for (let poly of inner) { if (poly.points.length < 7) continue; + let center = poly.calcCircleCenter(); center.area = poly.area(); - center.overlapping = [center] + center.overlapping = [center]; center.depth = 0; // console.log("center",center) - if (poly.circularity() < 0.985) { + if (poly.circularity() < 0.98) { // if not circular, don't add to holes continue; } @@ -712,7 +716,7 @@ export async function holes(settings, widget, individual, rec, onProgress) { } let overlap = false; for (let [i, circle] of circles.entries()) { - let dist = circle.distTo2D(center) + let dist = circle.distTo2D(center); // //if on the same xy point, if (dist <= centerDiff) { @@ -726,19 +730,19 @@ export async function holes(settings, widget, individual, rec, onProgress) { if (!overlap) circles.push(center); } } - onProgress(0.5 + (i / slices.length * 0.25), "recognize circles") + onProgress(0.5 + (i / slices.length * 0.25), "recognize circles"); } - let drills = [] + let drills = []; for (let [i, c] of circles.entries()) { - let overlapping = c.overlapping + let overlapping = c.overlapping; let last = overlapping.shift(); while (overlapping.length) { let circ = overlapping.shift(); let aveArea = (circ.area + last.area) / 2; - let areaDelta = Math.abs(circ.area - last.area) + let areaDelta = Math.abs(circ.area - last.area); if (areaDelta < aveArea * 0.05) { // if area delta less than 5% of average area //keep top circle selected last.depth = last.z - circ.z; @@ -748,24 +752,25 @@ export async function holes(settings, widget, individual, rec, onProgress) { last = circ; } } - if (last.depth != 0) drills.push(last) //add last circle - onProgress(0.75 + (i / circles.length * 0.25), "assemble holes") + if (last.depth != 0) drills.push(last); // add last circle + onProgress(0.75 + (i / circles.length * 0.25), "assemble holes"); } drills.forEach(h => { if (rec.fromTop) { // set z top if selected - h.depth += wztop - h.z - h.z = wztop + h.depth += wztop - h.z; + h.z = wztop; } - delete h.overlapping //for encoding - h.diam = toolDiam // for mesh generation - h.selected = (!individual && Math.abs(h.area - area) <= area * 0.05); //for same size selection + delete h.overlapping; //for encoding + h.diam = toolDiam; // for mesh generation + h.selected = (!individual && Math.abs(h.area - area) <= area * 0.05); // for same size selection }) - // console.log("unfiltered circles",circles) - // console.log("drills",drills) - drills = drills.filter(drill => drill.depth > 0) - widget.shadowedDrills = shadowedDrills + console.log("unfiltered circles",circles); + console.log("drills",drills); + + drills = drills.filter(drill => drill.depth > 0); + widget.shadowedDrills = shadowedDrills; widget.drills = drills; return drills; } diff --git a/src/kiri/mode/cam/tools.js b/src/kiri/mode/cam/tools.js index 7d8007d7..8c6b43d2 100644 --- a/src/kiri/mode/cam/tools.js +++ b/src/kiri/mode/cam/tools.js @@ -4,6 +4,7 @@ import { $ } from '../../../moto/webui.js'; import { api } from '../../core/api.js'; import { consts } from '../../core/consts.js'; import { settings as setconf } from '../../core/settings.js'; +import { calcTaperLength, calcTaperAngle } from './tool.js'; const DEG2RAD = Math.PI / 180; @@ -44,7 +45,7 @@ function selectTool(tool) { ui.toolMetric.checked = tool.metric; ui.toolType.selectedIndex = toolNames.indexOf(tool.type); if (tool === 'tapermill') { - ui.toolTaperAngle.value = kiri.driver.CAM.calcTaperAngle( + ui.toolTaperAngle.value = calcTaperAngle( (tool.flute_diam - tool.taper_tip) / 2, tool.flute_len ).round(1); } else if(tool === 'drill'){ @@ -208,16 +209,15 @@ export function updateTool(ev) { selectedTool.metric = ui.toolMetric.checked; selectedTool.type = toolNames[ui.toolType.selectedIndex]; if (selectedTool.type === 'tapermill') { - const CAM = kiri.driver.CAM; const rad = (selectedTool.flute_diam - selectedTool.taper_tip) / 2; if (ev && ev.target === ui.toolTaperAngle) { const angle = parseFloat(ev.target.value); - const len = CAM.calcTaperLength(rad, angle * DEG2RAD); + const len = calcTaperLength(rad, angle * DEG2RAD); selectedTool.flute_len = len; ui.toolTaperAngle.value = angle.round(1); ui.toolFluteLen.value = selectedTool.flute_len.round(4); } else { - ui.toolTaperAngle.value = CAM.calcTaperAngle(rad, selectedTool.flute_len).round(1); + ui.toolTaperAngle.value = calcTaperAngle(rad, selectedTool.flute_len).round(1); } } else { ui.toolTaperAngle.value = 0; @@ -318,7 +318,7 @@ function _showTools() { return; } const record = { - version: kiri.version, + version: api.version, tools: api.conf.get().tools, time: Date.now() }; diff --git a/src/kiri/mode/fdm/driver-be.js b/src/kiri/mode/fdm/driver-be.js index 397670e2..f1164acc 100644 --- a/src/kiri/mode/fdm/driver-be.js +++ b/src/kiri/mode/fdm/driver-be.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { util } from '../../../geo/base.js'; -import { fdm_slice } from './slice.js'; +import { fdm_slice, sliceAll } from './slice.js'; import { fdm_prepare } from './prepare.js'; import { fdm_export } from './export.js'; @@ -39,6 +39,7 @@ export const FDM = { extrudePerMM, extrudeMM, slice: fdm_slice, + sliceAll: sliceAll, prepare: fdm_prepare, export: fdm_export }; diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index bbc088fc..a2222402 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -2,7 +2,7 @@ import '../../../ext/jspoly.js'; import { base } from '../../../geo/base.js'; -import { newPolygon } from '../../../geo/polygon.js'; +import { newPolygon, Polygon } from '../../../geo/polygon.js'; import { polygons as POLY, fillArea } from '../../../geo/polygons.js'; import { getRangeParameters } from './driver.js'; import { slicer } from '../../../geo/slicer.js'; @@ -163,25 +163,47 @@ function thin_type_2(params) { } function thin_type_3(params) { + // offsetN usually = extrusion width (nozzle diameter) let { z, top, count, offsetN, last, gaps } = params; - top.thin_fill = []; - top.fill_sparse = []; + // produce trace from outside of poly inward no more than max inset let { noodle, remain } = top.poly.noodle(offsetN * count); - top.shells = noodle; + + // top.shells = noodle; top.gaps = last = remain; - let thin = top.thin_fill; - let scale = 5000; + let thin = top.thin_fill = []; + let sparse = top.fill_sparse = []; + let scale = 1000; let minR = offsetN / 2; let maxR = minR * 1.5; - let minSpur = 0; // offsetN * 3; + let minSpur = offsetN * 2; + + let showNoodle = false; + let mergeChains = true; + let interpolateShortSpur = true; + let showChainRawPoints = false; + let showChainInterpPoints = true; + let showMidline = true; + let showMidPoints = false; + let showMidNormals = false; + let showMidRadii = true; + let showNexuses = false; let pointMap = new Map(); let lineMap = new Map(); + function pointDist(p0, p1) { + return Math.hypot(p0.x-p1.x, p0.y-p1.y); + } + + function point2key(point) { + return `${(point.x*100)|0}|${(point.y*100)|0}`; + } + + // convert point to deduplicated / unique point record function pointToRec(point) { - let key = `${(point.x*100)|0}|${(point.y*100)|0}`; + let key = point2key(point); let rec = pointMap.get(key); if (!rec) { rec = { @@ -196,6 +218,7 @@ function thin_type_3(params) { return rec; } + // convert line to unique line record. matches reversed and duplicate lines function pointsToLine(p0, p1) { p0 = pointToRec(p0); p1 = pointToRec(p1); @@ -219,6 +242,9 @@ function thin_type_3(params) { let chains = []; + // for each standalone noodle, construct medial axis + // map medial axis points into de-duplicated segments + // which will be re-connected into chains for (let poly of noodle) { // scale the polygon (including inners) so the medial axis code works properly poly = poly.clone(true).scale({ x:scale, y:scale, z:scale }); @@ -231,69 +257,84 @@ function thin_type_3(params) { for (let { point0, point1 } of ma) { pointsToLine(point0, point1); } - for (let seg of lineMap.values()) { - let { p0, p1 } = seg; - let len = seg.len = Math.hypot(p0.x-p1.x, p0.y-p1.y); - // filter out lines where both points' radii are under the - // minR threshold. shorten and fixup lines where minR falls - // somewhere along the gradient between ends. - if (p0.r < minR || p1.r < minR) { - if (p0.r > minR && len > minSpur) { - // move p1 toward p0 until minR met - let pct = (minR - p1.r) / (p0.r - p1.r); - let dx = p1.x - p0.x; - let dy = p1.y - p0.y; - p1.x -= dx * pct; - p1.y -= dy * pct; - p1.r = minR; - pointMap.delete(p1.key); - } else if (p1.r > minR && len > minSpur) { - // move p0 toward p1 until minR met - let pct = (minR - p0.r) / (p1.r - p0.r); - let dx = p0.x - p1.x; - let dy = p0.y - p1.y; - p0.x -= dx * pct; - p0.y -= dy * pct; - p0.r = minR; - pointMap.delete(p0.key); - } else { - lineMap.delete(seg.key); - continue; - } + } + + // connect segments into chains + for (let seg of lineMap.values()) { + let { p0, p1 } = seg; + let len = seg.len = pointDist(p0, p1); + // filter out lines where both points' radii are under the + // minR threshold. shorten and fixup lines where minR falls + // somewhere along the gradient between ends. + if (interpolateShortSpur) + if (p0.r < minR || p1.r < minR) { + if (p0.r > minR && len > minSpur) { + // move p1 toward p0 until minR met + let pct = (minR - p1.r) / (p0.r - p1.r); + let dx = p1.x - p0.x; + let dy = p1.y - p0.y; + p1.x -= dx * pct; + p1.y -= dy * pct; + p1.r = minR; + pointMap.delete(p1.key); + } else if (p1.r > minR && len > minSpur) { + // move p0 toward p1 until minR met + let pct = (minR - p0.r) / (p1.r - p0.r); + let dx = p0.x - p1.x; + let dy = p0.y - p1.y; + p0.x -= dx * pct; + p0.y -= dy * pct; + p0.r = minR; + pointMap.delete(p0.key); + } else { + // both points eliminated + pointMap.delete(p0.key); + pointMap.delete(p1.key); + lineMap.delete(seg.key); + continue; } - // for each line segment either add it to a chain or start a new chain - let add; - for (let chain of chains) { - if (p0.count === 2 && p0.key === chain[0].key) { - chain.splice(0,0,{...p1, len}); - } else if (p0.count === 2 && p0.key === chain.peek().key) { - chain.peek().len = len; - chain.push({...p1}); - add = true; - } else if (p1.count === 2 && p1.key === chain[0].key) { - chain.splice(0,0,{...p0, len}); - add = true; - } else if (p1.count === 2 && p1.key === chain.peek().key) { - chain.peek().len = len; - chain.push({...p0}); - add = true; - } - if (add) { - chain.total += len; - break; - } + } + // for each line segment either add it to a chain or start a new chain + let add; + for (let chain of chains) { + if (p0.count === 2 && p0 === chain[0]) { + // prefix chain with p1 + chain.splice(0,0,p1); + add = true; + } else if (p0.count === 2 && p0 === chain.peek()) { + // append chain with p1 + chain.push(p1); + add = true; + } else if (p1.count === 2 && p1 === chain[0]) { + // prefix chain with p0 + chain.splice(0,0,p0); + add = true; + } else if (p1.count === 2 && p1 === chain.peek()) { + // append chain with p0 + chain.push(p0); + add = true; } - if (!add) { - chains.push([ {...p0 ,len}, {...p1} ]); - chains.peek().total = len; + if (add) { + chain.total += len; + break; } } + if (!add) { + chains.push([ p0, p1 ]); + chains.peek().total = len; + } } - let showMid = false; - let showCross = false; - let showRad = true; + // todo: merge chains at next of 3 or more preferencing + // merging longer chains. then shorten remaining nexus chain + // intersections by minR + // render inset "noodle" + if (showNoodle) { + top.shells.appendAll(noodle); + } + + // divide a segment/redius into 1 or more equal subsegments function div(cr) { if (cr <= maxR) { return [ cr ]; @@ -311,71 +352,249 @@ function thin_type_3(params) { } } - // add chains to thinwall output for visualization - if (showRad) { - top.shells = []; - } - for (let chain of chains.filter(c => c.total >= minR)) { - // chain reduction by merging short segments - let end = chain[0]; - let nuchain = [ end ]; - for (let point of chain.slice(1)) { - if (point.len === undefined || point.len + end.len >= minR) { - nuchain.push(point); - end = point; - } else { - end.len += point.len; + // filter chains that are too short from consideration + chains = chains.filter(c => c.total >= minR); + chains.sort((c1,c2) => c2.total - c1.total); + + function merge1chain() { + let clen = chains.length; + for (let i=0; i chain); + + // filter dup points from connecting ends + chains = chains.map(chain => chain.filter((v, i) => i === 0 || v !== chain[i - 1])); + + // mark closed chains + // todo use exact point match by eliminating slice above + // then later removing repeated points in the resulting chain from merges + for (let chain of chains) { + chain.closed = (chain[0] === chain.peek()); + console.log(chain.closed ? 'closed' : 'open'); + if (chain.closed) chain.pop(); + } + + // shorten chain length by minR distance by popping or moving points + function shorten(chain) { + for (let rem=0, i=chain.length-1; i>=1 && rem < minR; i--) { + let c1 = chain[i]; + let c2 = chain[i-1]; + let d = pointDist(c1,c2); + if (rem + d <= minR) { + chain.pop(); + rem += d; + } else { + let diff = minR - rem; + // move c1 toward c2 + let pct = diff / d; + let dx = c1.x - c2.x; + let dy = c1.y - c2.y; + chain[i] = { + x: c1.x - dx * pct, + y: c1.y - dy * pct, + r: c1.r + }; + rem = 0; + break; } - // compute medial axis segment cross section + } + } + + // shorten chains terminating at nexus by midR + for (let rec of Object.values(nexus)) { + let { point } = rec; + rec.shorts = 0; + if (rec.chains.length < 3) continue; + for (let chain of chains.filter(c => !c.closed)) { + if (pointDist(chain[0], point) < minR) { + chain.reverse(); + shorten(chain); + chain.reverse(); + rec.shorts++; + } else if (chain.peek() === point) { + shorten(chain); + rec.shorts++; + } + } + } + + if (showNexuses) + for (let rec of Object.values(nexus)) { + let { point } = rec; + top.shells.push(newPolygon().centerCircle(point, 0.2, 6 - rec.shorts)); + } + + function renderPointNormal(pt, ndx, ndy) { + if (showMidNormals) { + let xo = ndx * pt.r; + let yo = ndy * pt.r; + thin.push(new Point(pt.x + yo, pt.y - xo)); + thin.push(new Point(pt.x - yo, pt.y + xo)); + } + if (showMidRadii) { + const Sx = pt.x + ndy * pt.r; + const Sy = pt.y - ndx * pt.r; + const circ = top.shells; + const pop = div(pt.r); + let acc = 0; + for (let r of pop) { + const t = acc + r; + const px = Sx + -ndy * t; + const py = Sy + ndx * t; + circ.push(newPolygon().centerCircle({x:px, y:py}, r, 10)); + acc += r * 2; + } + } +} + + // gather point offsets into shells + let zi = z; + for (let chain of chains) { + // zi += 0.1; + if (showChainRawPoints) + for (let pt of chain) { + top.shells.push(newPolygon().centerCircle(pt, pt.r ?? 0.1, 10)); + } + // draw medial axis chain + let { closed, length } = chain; + let term = closed ? length : length - 1; + let segs = []; + for (let i=0; i { client.clear(); client.sync([this.widget]); diff --git a/src/kiri/run/worker.js b/src/kiri/run/worker.js index 97f22e17..cb6b659b 100644 --- a/src/kiri/run/worker.js +++ b/src/kiri/run/worker.js @@ -546,11 +546,12 @@ const dispatch = { parse(args, send) { const { settings, code, type } = args; + const { process } = settings; const origin = settings.origin; const offset = { - x: origin.x, - y: -origin.y, - z: origin.z + x: origin.x - (process.camOriginOffX ?? 0), + y: -origin.y - (process.camOriginOffY ?? 0), + z: origin.z - (process.camOriginOffZ ?? 0) }; const device = settings.device; const print = current.print = newPrint(settings, Object.values(wcache)); diff --git a/src/mesh/api.js b/src/mesh/api.js index 75bc7381..baf7fe78 100644 --- a/src/mesh/api.js +++ b/src/mesh/api.js @@ -1392,14 +1392,15 @@ const prefs = { scale_group_Z: true }, space: { + bounds: false, + center: false, + dark: false, + floor: false, + grid: true, + select: [], snap: 1, snapon: false, - center: false, - floor: false, - wire: false, - grid: true, - dark: false, - select: [] + wire: false }, sketch: { open_close: false, diff --git a/web/kiri/engine.html b/web/kiri/engine.html index da417539..40b62df6 100644 --- a/web/kiri/engine.html +++ b/web/kiri/engine.html @@ -99,7 +99,11 @@ }); edit.session.setTabSize(4); edit.session.setUseSoftTabs(true); - let FDMExample = loadExample(demoFDM) + if (localStorage.engineCode) { + edit.session.setValue(localStorage.engineCode); + } else { + loadExample(demoFDM); + } } function display_message(msg) { $('gfoot').innerText = msg ? Object.keys(msg).join(' - ') : ''; @@ -110,7 +114,7 @@ $('dfoot').style.display = 'block'; $('gfoot').style.display = ''; } - function loadExample(func){ + function loadExample(func) { let fnstr = func.toString().split("\n"); edit.session.setValue( fnstr @@ -124,6 +128,7 @@ $('gfoot').style.display = 'block'; let code = edit.session.getValue(); eval(`(function(){${code}})()`); + localStorage.engineCode = code; } function demoFDM() { new Engine() diff --git a/web/kiri/lang/en.js b/web/kiri/lang/en.js index bc713531..fdaa35b3 100644 --- a/web/kiri/lang/en.js +++ b/web/kiri/lang/en.js @@ -790,6 +790,8 @@ self.lang['en-us'] = { ou_conv_l: ["milling direction","uncheck for 'climb'"], ou_depf_s: "depth first", ou_depf_l: ["optimize pocket cuts","with depth priority"], + ou_inrf_s: "inner first", + ou_inrf_l: ["always cut inner to outer"], ou_toin_s: "tool init", ou_toin_l: "add tool change with first operation. when disabled, the gcode assumes the first tool is loaded and offset probed", ou_forz_s: "force z max",