From e4dfdc6f59bc456096409ad6c46ed9d7e3ef88a2 Mon Sep 17 00:00:00 2001 From: Pieter Zandbergen Date: Fri, 12 Jun 2026 13:12:31 +0200 Subject: [PATCH 1/7] feat: Configurable 4th axis (A or B) --- src/kiri/app/conf/defaults.js | 1 + src/kiri/app/consts.js | 4 ++++ src/kiri/app/devices.js | 1 + src/kiri/app/init/input.js | 1 + src/kiri/mode/cam/work/export.js | 2 +- web/kiri/lang/en.js | 2 ++ 6 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/kiri/app/conf/defaults.js b/src/kiri/app/conf/defaults.js index 1efecc0b1..c192b315e 100644 --- a/src/kiri/app/conf/defaults.js +++ b/src/kiri/app/conf/defaults.js @@ -412,6 +412,7 @@ export const conf = { maxHeight: 300, useLaser: false, useIndexed: false, + indexedAxis: "A", originCenter: false, spindleMax: 0, gcodePre: [], diff --git a/src/kiri/app/consts.js b/src/kiri/app/consts.js index ac59923b2..f6e3710d6 100644 --- a/src/kiri/app/consts.js +++ b/src/kiri/app/consts.js @@ -73,6 +73,10 @@ const LISTS = { { name: "lines" }, // { name: "surface" } ], + indexedaxis: [ + { name: "A" }, + { name: "B" } + ], trace: [ { name: "follow" }, { name: "clear" } diff --git a/src/kiri/app/devices.js b/src/kiri/app/devices.js index 2714a37b4..4d83b65fc 100644 --- a/src/kiri/app/devices.js +++ b/src/kiri/app/devices.js @@ -195,6 +195,7 @@ function setDeviceCode(code, devicename) { ui.maxHeight, ui.useLaser, ui.useIndexed, + ui.indexedAxis, ui.resolutionX, ui.resolutionY, ui.deviceOrigin, diff --git a/src/kiri/app/init/input.js b/src/kiri/app/init/input.js index 1cc5a4e57..0dc2aed6e 100644 --- a/src/kiri/app/init/input.js +++ b/src/kiri/app/init/input.js @@ -267,6 +267,7 @@ export function init_input() { laserMaxPower: newInput(LANG.ou_maxp_s, {title:LANG.ou_maxp_l, modes:LASER, size:7, text:true}), useLaser: newBoolean(LANG.dv_lazr_s, onBooleanClick, {title:LANG.dv_lazr_l, modes:CAM}), useIndexed: newBoolean(LANG.dv_4tha_s, onBooleanClick, {title:LANG.dv_4tha_l, modes:CAM}), + indexedAxis: newSelect(LANG.dv_4tax_s, {title:LANG.dv_4tax_l, modes:CAM, show:() => ui.useIndexed.checked}, "indexedaxis"), gcodeFExt: newInput(LANG.dv_fext_s, {title:LANG.dv_fext_l, modes:CAM_LZR, size:7, text:true}), gcodeEd: newGroup(LANG.dv_gr_gco, $('dg'), {group:"dgcp", inline, modes:GCODE}), gcodeMacros: newRow([ diff --git a/src/kiri/mode/cam/work/export.js b/src/kiri/mode/cam/work/export.js index ee762acf5..1268450a7 100644 --- a/src/kiri/mode/cam/work/export.js +++ b/src/kiri/mode/cam/work/export.js @@ -33,7 +33,7 @@ export function cam_export(print, online) { cmdToolChange = device.gcodeChange || ["M6 T{tool}"], cmdSpindle = device.gcodeSpindle || ["M3 S{speed}"], cmdDwell = device.gcodeDwell || ["G4 P{time}"], - axis = { X: 'X', Y: 'Y', Z: 'Z', A: 'A', F: 'F', R: 'R', I: 'I', J: 'J' }, + axis = { X: 'X', Y: 'Y', Z: 'Z', A: device.indexedAxis === 'B' ? 'B' : 'A', F: 'F', R: 'R', I: 'I', J: 'J' }, dev = settings.device, spro = settings.process, maxZd = spro.camFastFeedZ, diff --git a/web/kiri/lang/en.js b/web/kiri/lang/en.js index a6bbf3fc0..e08b08fcc 100644 --- a/web/kiri/lang/en.js +++ b/web/kiri/lang/en.js @@ -175,6 +175,8 @@ self.lang['en-us'] = { dv_lazr_l: ["enable laser operations"], dv_4tha_s: "enable 4th axis", dv_4tha_l: ["enable 4th axis operations","aka indexed mode"], + dv_4tax_s: "4th axis", + dv_4tax_l: ["rotary axis letter used in gcode output","A (default) or B"], dv_dwll_s: "dwell", dv_dwll_l: "gcode dwell script", From b109cdf61b95828f5e7f91f69696ed47abc7d4ba Mon Sep 17 00:00:00 2001 From: Pieter Zandbergen Date: Fri, 12 Jun 2026 14:57:34 +0200 Subject: [PATCH 2/7] feat: Indexed axis align (X or Y) --- src/kiri/app/conf/defaults.js | 4 ++++ src/kiri/app/consts.js | 4 ++++ src/kiri/app/devices.js | 1 + src/kiri/app/init/input.js | 1 + src/kiri/mode/cam/work/export.js | 12 ++++++++++++ web/kiri/lang/en.js | 2 ++ 6 files changed, 24 insertions(+) diff --git a/src/kiri/app/conf/defaults.js b/src/kiri/app/conf/defaults.js index c192b315e..43065cfc7 100644 --- a/src/kiri/app/conf/defaults.js +++ b/src/kiri/app/conf/defaults.js @@ -105,6 +105,9 @@ function device_from_code(code,mode) { originCenter: valueOf(set.origin_center, false), extrudeAbs: valueOf(set.extrude_abs, false), spindleMax: valueOf(set.spindle_max, 0), + useIndexed: valueOf(set.use_indexed, false), + indexedAxis: valueOf(set.indexed_axis, "A"), + indexedAxisAlign: valueOf(set.indexed_axis_align, "X"), gcodeFan: valueOf(cmd.fan_power || code.fan_power, []), gcodeFeature: valueOf(cmd.feature || code.feature, []), gcodeTrack: valueOf(cmd.progress || code.progress, []), @@ -413,6 +416,7 @@ export const conf = { useLaser: false, useIndexed: false, indexedAxis: "A", + indexedAxisAlign: "X", originCenter: false, spindleMax: 0, gcodePre: [], diff --git a/src/kiri/app/consts.js b/src/kiri/app/consts.js index f6e3710d6..466808a38 100644 --- a/src/kiri/app/consts.js +++ b/src/kiri/app/consts.js @@ -77,6 +77,10 @@ const LISTS = { { name: "A" }, { name: "B" } ], + indexedalign: [ + { name: "X" }, + { name: "Y" } + ], trace: [ { name: "follow" }, { name: "clear" } diff --git a/src/kiri/app/devices.js b/src/kiri/app/devices.js index 4d83b65fc..fd895c639 100644 --- a/src/kiri/app/devices.js +++ b/src/kiri/app/devices.js @@ -196,6 +196,7 @@ function setDeviceCode(code, devicename) { ui.useLaser, ui.useIndexed, ui.indexedAxis, + ui.indexedAxisAlign, ui.resolutionX, ui.resolutionY, ui.deviceOrigin, diff --git a/src/kiri/app/init/input.js b/src/kiri/app/init/input.js index 0dc2aed6e..302401de5 100644 --- a/src/kiri/app/init/input.js +++ b/src/kiri/app/init/input.js @@ -268,6 +268,7 @@ export function init_input() { useLaser: newBoolean(LANG.dv_lazr_s, onBooleanClick, {title:LANG.dv_lazr_l, modes:CAM}), useIndexed: newBoolean(LANG.dv_4tha_s, onBooleanClick, {title:LANG.dv_4tha_l, modes:CAM}), indexedAxis: newSelect(LANG.dv_4tax_s, {title:LANG.dv_4tax_l, modes:CAM, show:() => ui.useIndexed.checked}, "indexedaxis"), + indexedAxisAlign: newSelect(LANG.dv_4tal_s, {title:LANG.dv_4tal_l, modes:CAM, show:() => ui.useIndexed.checked}, "indexedalign"), gcodeFExt: newInput(LANG.dv_fext_s, {title:LANG.dv_fext_l, modes:CAM_LZR, size:7, text:true}), gcodeEd: newGroup(LANG.dv_gr_gco, $('dg'), {group:"dgcp", inline, modes:GCODE}), gcodeMacros: newRow([ diff --git a/src/kiri/mode/cam/work/export.js b/src/kiri/mode/cam/work/export.js index 1268450a7..dad4a2186 100644 --- a/src/kiri/mode/cam/work/export.js +++ b/src/kiri/mode/cam/work/export.js @@ -79,6 +79,18 @@ export function cam_export(print, online) { time: 0 }; + // when the rotary is mounted along the machine Y axis (instead of the + // default X), swap the X/Y (and arc I/J) output letters for indexed jobs. + // kiri computes indexed toolpaths assuming an X-aligned rotary; relabeling + // the linear axes at emit time maps them onto a Y-aligned rotary. only + // applied in indexed mode so plain 3-axis output is never swapped. + if (device.useIndexed && device.indexedAxisAlign === 'Y') { + axis.X = 'Y'; + axis.Y = 'X'; + axis.I = 'J'; + axis.J = 'I'; + } + // console.log({ offset, origin, stock }); function section(section) { diff --git a/web/kiri/lang/en.js b/web/kiri/lang/en.js index e08b08fcc..df9c55ce8 100644 --- a/web/kiri/lang/en.js +++ b/web/kiri/lang/en.js @@ -177,6 +177,8 @@ self.lang['en-us'] = { dv_4tha_l: ["enable 4th axis operations","aka indexed mode"], dv_4tax_s: "4th axis", dv_4tax_l: ["rotary axis letter used in gcode output","A (default) or B"], + dv_4tal_s: "rotary along", + dv_4tal_l: ["machine linear axis the rotary is mounted along","X (default) or Y, swaps X/Y output when Y"], dv_dwll_s: "dwell", dv_dwll_l: "gcode dwell script", From 6a2ee0058a83cfa2a6eeb3f9a599726ee9379361 Mon Sep 17 00:00:00 2001 From: Pieter Zandbergen Date: Fri, 12 Jun 2026 14:57:51 +0200 Subject: [PATCH 3/7] feat: Snapmaker Artisan device profile --- src/kiri/dev/cam/Snapmaker.Artisan.json | 29 +++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/kiri/dev/cam/Snapmaker.Artisan.json diff --git a/src/kiri/dev/cam/Snapmaker.Artisan.json b/src/kiri/dev/cam/Snapmaker.Artisan.json new file mode 100644 index 000000000..6d466eb78 --- /dev/null +++ b/src/kiri/dev/cam/Snapmaker.Artisan.json @@ -0,0 +1,29 @@ +{ + "file-ext": "gcode", + "token-space": " ", + "strip-comments": true, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)" + ], + "post":[ + "M5 ; complete moves, stop spindle", + "M30 ; program end" + ], + "tool-change":[], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "spindle":[ + "M3 P{spindle} ; uniquely uses percentage not RPM" + ], + "settings": { + "origin_center": false, + "bed_width": 400, + "bed_depth": 400, + "build_height": 100, + "spindle_max": 100, + "indexed_axis": "B", + "indexed_axis_align": "Y" + } +} From dd184ee0b9de8775da1348ed80071aa721e4c7cf Mon Sep 17 00:00:00 2001 From: Pieter Zandbergen Date: Fri, 12 Jun 2026 16:01:44 +0200 Subject: [PATCH 4/7] feat: Support cylindrical stock --- src/kiri/app/conf/defaults.js | 1 + src/kiri/app/platform.js | 6 +++- src/kiri/mode/cam/app/cl-stock.js | 58 +++++++++++++++++++----------- src/kiri/mode/cam/app/init-menu.js | 1 + src/kiri/mode/cam/work/anim-3d.js | 12 +++++-- src/kiri/mode/cam/work/op-area.js | 28 +++++++++++++-- src/kiri/mode/cam/work/op-rough.js | 14 ++++++++ src/kiri/mode/cam/work/prepare.js | 4 ++- src/kiri/mode/cam/work/slice.js | 9 +++-- web/kiri/lang/en.js | 2 ++ 10 files changed, 106 insertions(+), 29 deletions(-) diff --git a/src/kiri/app/conf/defaults.js b/src/kiri/app/conf/defaults.js index 43065cfc7..f7db00703 100644 --- a/src/kiri/app/conf/defaults.js +++ b/src/kiri/app/conf/defaults.js @@ -598,6 +598,7 @@ export const conf = { camStockIndexed: false, camStockIndexGrid: true, camStockOffset: true, + camStockRound: false, camStockX: 5, camStockY: 5, camStockZ: 5, diff --git a/src/kiri/app/platform.js b/src/kiri/app/platform.js index 5bd080a44..c833636db 100644 --- a/src/kiri/app/platform.js +++ b/src/kiri/app/platform.js @@ -244,7 +244,7 @@ function update_top_z() { function platformUpdateStock() { const settings = current(); const { bounds, process, mode } = settings; - const { camStockX, camStockY, camStockZ, camStockOffset, camStockIndexed } = process; + const { camStockX, camStockY, camStockZ, camStockOffset, camStockIndexed, camStockRound } = process; if (mode === 'CAM') { let stock = settings.stock = { x: camStockX, @@ -257,6 +257,10 @@ function platformUpdateStock() { stock.y += bounds.max.y - bounds.min.y; stock.z += bounds.max.z - bounds.min.z; } + // round bar: diameter = height (Z), so force a circular Y=Z cross-section + if (camStockRound && camStockIndexed) { + stock.y = stock.z; + } stock.center = { x: (bounds.max.x + bounds.min.x) / 2, y: (bounds.max.y + bounds.min.y) / 2, diff --git a/src/kiri/mode/cam/app/cl-stock.js b/src/kiri/mode/cam/app/cl-stock.js index 550fdfb43..770f026af 100644 --- a/src/kiri/mode/cam/app/cl-stock.js +++ b/src/kiri/mode/cam/app/cl-stock.js @@ -103,7 +103,17 @@ export function updateStock() { if (x && y && z) { UI.func.animate.classList.remove('disabled'); { - let geo = new THREE.BoxGeometry(1, 1, 1); + // round bar on a rotary: show a cylinder (axis along X) instead of a + // box. rotate the GEOMETRY (not the mesh) so the shared scale/position + // below still maps x->length, y/z->diameter with y==z normalized. + let round = process.camStockRound && env.isIndexed; + let geo; + if (round) { + geo = new THREE.CylinderGeometry(0.5, 0.5, 1, 64); + geo.rotateZ(Math.PI / 2); + } else { + geo = new THREE.BoxGeometry(1, 1, 1); + } let mat = new THREE.MeshBasicMaterial({ color: 0x777777, opacity: 0.05, @@ -112,25 +122,33 @@ export function updateStock() { }); env.camStock = new THREE.Mesh(geo, mat); env.camStock.renderOrder = 2; - let lo = 0.5; - let lidat = [ - lo, lo, lo, lo, lo, -lo, - lo, lo, lo, lo, -lo, lo, - lo, lo, lo, -lo, lo, lo, - -lo, -lo, -lo, -lo, -lo, lo, - -lo, -lo, -lo, -lo, lo, -lo, - -lo, -lo, -lo, lo, -lo, -lo, - lo, lo, -lo, -lo, lo, -lo, - lo, lo, -lo, lo, -lo, -lo, - lo, -lo, -lo, lo, -lo, lo, - lo, -lo, lo, -lo, -lo, lo, - -lo, -lo, lo, -lo, lo, lo, - -lo, lo, lo, -lo, lo, -lo - ]; - let ligeo = new THREE.BufferGeometry(); - ligeo.setAttribute('position', new THREE.BufferAttribute(lidat.toFloat32(), 3)); - let limat = new THREE.LineBasicMaterial({ color: 0xaaaaaa }); - let lines = new THREE.LineSegments(ligeo, limat); + let lines; + if (round) { + // 30deg threshold keeps just the two end circles, not every facet seam + let ligeo = new THREE.EdgesGeometry(geo, 30); + let limat = new THREE.LineBasicMaterial({ color: 0xaaaaaa }); + lines = new THREE.LineSegments(ligeo, limat); + } else { + let lo = 0.5; + let lidat = [ + lo, lo, lo, lo, lo, -lo, + lo, lo, lo, lo, -lo, lo, + lo, lo, lo, -lo, lo, lo, + -lo, -lo, -lo, -lo, -lo, lo, + -lo, -lo, -lo, -lo, lo, -lo, + -lo, -lo, -lo, lo, -lo, -lo, + lo, lo, -lo, -lo, lo, -lo, + lo, lo, -lo, lo, -lo, -lo, + lo, -lo, -lo, lo, -lo, lo, + lo, -lo, lo, -lo, -lo, lo, + -lo, -lo, lo, -lo, lo, lo, + -lo, lo, lo, -lo, lo, -lo + ]; + let ligeo = new THREE.BufferGeometry(); + ligeo.setAttribute('position', new THREE.BufferAttribute(lidat.toFloat32(), 3)); + let limat = new THREE.LineBasicMaterial({ color: 0xaaaaaa }); + lines = new THREE.LineSegments(ligeo, limat); + } env.camStock.lines = lines; env.camStock.add(lines); SPACE.world.add(env.camStock); diff --git a/src/kiri/mode/cam/app/init-menu.js b/src/kiri/mode/cam/app/init-menu.js index 9a9ae7d2c..d0f4c46b1 100644 --- a/src/kiri/mode/cam/app/init-menu.js +++ b/src/kiri/mode/cam/app/init-menu.js @@ -139,6 +139,7 @@ export function menu() { camStockOffset: newBoolean(LANG.cs_offs_s, onBooleanClick, {title:LANG.cs_offs_l}), camStockIndexed: newBoolean(LANG.cs_indx_s, onBooleanClick, {title:LANG.cs_indx_l}), camStockIndexGrid: newBoolean(LANG.cs_ishg_s, onBooleanClick, {title:LANG.cs_ishg_l, show:() => ui.camStockIndexed.checked}), + camStockRound: newBoolean(LANG.cs_rond_s, onBooleanClick, {title:LANG.cs_rond_l, show:() => ui.camStockIndexed.checked}), // separator: newBlank({ class:"set-sep", driven }), // camStockManual: newRow([ // (ui.stockPlace = newButton('position', onButtonClick, { })), diff --git a/src/kiri/mode/cam/work/anim-3d.js b/src/kiri/mode/cam/work/anim-3d.js index 1c8a6a030..9ff33dfaf 100644 --- a/src/kiri/mode/cam/work/anim-3d.js +++ b/src/kiri/mode/cam/work/anim-3d.js @@ -58,13 +58,14 @@ export function init(worker) { stockSlices = []; const { x, y, z } = stock; + const round = process.camStockRound && isIndexed; const sliceCount = parseInt(settings.controller.animesh || 2000) / 100; const sliceWidth = stock.x / sliceCount; if (controller.manifold) for (let i = 0; i < sliceCount; i++) { let xmin = -(x / 2) + (i * sliceWidth) + sliceWidth / 2; - let slice = new Stock(sliceWidth, y, z).translate(xmin, 0, 0); + let slice = new Stock(sliceWidth, y, z, round).translate(xmin, 0, 0); stockSlices.push(slice); slice.updateMesh([]); slice.send(send); @@ -314,7 +315,7 @@ function toolUpdate(toolid, send) { } class Stock { - constructor(x, y, z) { + constructor(x, y, z, round) { this.id = nextMeshID++; this.vbuf = undefined; this.ibuf = undefined; @@ -322,7 +323,12 @@ class Stock { this.sends = 0; this.newbuf = true; this.subtracts = 0; - this.mesh = CSG.Instance().Manifold.cube([x, y, z], true); + const M = CSG.Instance().Manifold; + // round bar: circular YZ cross-section (diameter = z) extruded along X. + // manifold cylinder axis is Z, so rotate 90deg about Y to lie along X. + this.mesh = round + ? M.cylinder(x, z / 2, z / 2, 64, true).rotate([0, 90, 0]) + : M.cube([x, y, z], true); } send(send) { diff --git a/src/kiri/mode/cam/work/op-area.js b/src/kiri/mode/cam/work/op-area.js index 7a0598135..76ceca7bc 100644 --- a/src/kiri/mode/cam/work/op-area.js +++ b/src/kiri/mode/cam/work/op-area.js @@ -168,6 +168,20 @@ class OpArea extends CamOp { let zs = flats ? flats.filter(z => z <= zTop && z >= zBottom).map(v => v + zMov) : down ? base_util.lerp(zTop, zBottom, down) : [ bounds.min.z ]; + // round bar: derive the cylinder center/radius from the stock + // extent the loop iterates (same Z-frame as `z`), then drop + // z-levels where the circular cross-section is narrower than the + // tool so the clear descent isn't aborted by an empty top sliver + let roundR, roundCz; + if (op.round) { + roundR = (workarea.top_stock - workarea.bottom_stock) / 2; + roundCz = (workarea.top_stock + workarea.bottom_stock) / 2; + zs = zs.filter(z => { + let d = z - roundCz; + return 2 * Math.sqrt(Math.max(0, roundR * roundR - d * d)) >= toolDiam; + }); + } + let zroc = 0; let zinc = 1 / zs.length; let lzo; @@ -191,11 +205,21 @@ class OpArea extends CamOp { let outs = []; let clip = []; let firstOff = -(toolDiam / 2 + (op.leave_xy ?? 0)); + // round bar: clip the working area to the cylinder chord at + // this height so only actual bar material is cleared + let region = [ area ]; + if (op.round) { + let { cx, cy, len } = op.round; + let d = z - roundCz; + let half = Math.sqrt(Math.max(0, roundR * roundR - d * d)); + let chord = newPolygon().centerRectangle({ x: cx, y: cy, z }, len, 2 * half); + region = POLY.trimTo([ area ], [ chord ]) || []; + } // remove shadow from area if (op.ignore) { - clip = [ area ]; + clip = region; } else { - POLY.subtract([ area ], shadow, clip, undefined, undefined, 0); + POLY.subtract(region, shadow, clip, undefined, undefined, 0); } //generate offsets to use let offsets = [ firstOff ]; diff --git a/src/kiri/mode/cam/work/op-rough.js b/src/kiri/mode/cam/work/op-rough.js index c1df6cbc1..e54fc355c 100644 --- a/src/kiri/mode/cam/work/op-rough.js +++ b/src/kiri/mode/cam/work/op-rough.js @@ -27,6 +27,18 @@ class OpRough extends CamOp { shadowBase = [ newPolygon().centerRectangle(stock.center, stock.x, stock.y) ]; } + // round bar on a rotary: flag the cylindrical cross-section so the clear + // loop clips each Z level to the chord 2*sqrt(r^2-(z-cz)^2) instead of + // air-cutting the full bounding-box width. axis = X (length), circle in + // YZ, invariant across index angles. The vertical center/radius (cz/r) + // are derived in op-area from the workarea stock extent so they share the + // exact Z-frame of the clear loop; here we only supply the X geometry. + let round = (state.settings.process.camStockRound && state.isIndexed) ? { + cx: stock.center.x, + cy: stock.center.y, + len: stock.x + } : undefined; + let areas = POLY.flatten(POLY.expand(shadowBase, tool.fluteDiameter() / 2 - 0.001)); let ops_list = this.ops_list = [ ]; @@ -49,6 +61,7 @@ class OpRough extends CamOp { ov_botz: op.ov_botz, ov_topz: op.ov_topz, rotated: true, + round, areas: { [widget.id]: areas.map(p => p.toArray()) }, surfaces: {} })); @@ -74,6 +87,7 @@ class OpRough extends CamOp { ov_botz: op.ov_botz, ov_topz: op.ov_topz, rotated: true, + round, areas: { [widget.id]: areas.map(p => p.toArray()) }, surfaces: {}, flats: Object.keys(slicer.zFlat).map(v => parseFloat(v)).sort((a,b) => b-a), diff --git a/src/kiri/mode/cam/work/prepare.js b/src/kiri/mode/cam/work/prepare.js index 288d3b9a6..57d0a9e75 100644 --- a/src/kiri/mode/cam/work/prepare.js +++ b/src/kiri/mode/cam/work/prepare.js @@ -88,7 +88,7 @@ export async function prepare_one(widget, settings, print, firstPoint, update) { { camArcEnabled, camArcResolution, camArcTolerance } = process, { camDepthFirst, camEaseAngle, camEaseDown } = process, { camFastFeed, camFastFeedZ, camZTop } = process, - { camStockX, camStockY, camStockZ, camStockIndexed, camStockOffset } = process, + { camStockX, camStockY, camStockZ, camStockIndexed, camStockOffset, camStockRound } = process, { camForceZMax, camFullEngage, camInnerFirst, camOriginCenter } = process, { camOriginOffX, camOriginOffY, camOriginOffZ, camZClearance } = process, bounds = widget.getBoundingBox(), @@ -101,6 +101,8 @@ export async function prepare_one(widget, settings, print, firstPoint, update) { y: camStockY, z: camStockZ }, + // round bar: diameter = height (Z), force circular Y=Z cross-section + _round_norm = (camStockRound && camStockIndexed) ? (stock.y = stock.z) : 0, stockZ = stock.z * (camStockIndexed ? 0.5 : 1), stockZClear = stockZ + camZClearance, widgetTrackTop = widget.track.top, diff --git a/src/kiri/mode/cam/work/slice.js b/src/kiri/mode/cam/work/slice.js index d4968bc7e..2044f200c 100644 --- a/src/kiri/mode/cam/work/slice.js +++ b/src/kiri/mode/cam/work/slice.js @@ -84,7 +84,7 @@ export async function cam_slice(settings, widget, onupdate, ondone) { // allow recomputing later if widget or settings changes const var_compute = () => { - let { camStockX, camStockY, camStockZ, camStockOffset } = proc; + let { camStockX, camStockY, camStockZ, camStockOffset, camStockRound } = proc; ({ camZTop, camZBottom } = proc); bounds = widget.getBoundingBox(); let pos = widget.track.pos; @@ -99,7 +99,12 @@ export async function cam_slice(settings, widget, onupdate, ondone) { z: camStockZ, center: newPoint(pos.x, pos.y, pos.z) }; - if (!camStockOffset && axisIndex && isIndexed) { + // round bar: diameter = height (Z), circular Y=Z cross-section that is + // invariant under rotation about X, so the YZ-swap below is skipped + if (camStockRound && isIndexed) { + stock.y = stock.z; + } + if (!camStockOffset && axisIndex && isIndexed && !camStockRound) { if (axisIndex === 0 || axisIndex === 180) { // do nothing } else if (axisIndex === 90 || axisIndex === 270) { diff --git a/web/kiri/lang/en.js b/web/kiri/lang/en.js index df9c55ce8..b3d15f16a 100644 --- a/web/kiri/lang/en.js +++ b/web/kiri/lang/en.js @@ -877,6 +877,8 @@ self.lang['en-us'] = { cs_ishg_l: ["show platform grid in indexed mode"], cs_indx_s: "indexed", cs_indx_l: ["stock is mounted to a rotatary indexer"], + cs_rond_s: "round", + cs_rond_l: ["stock is a round bar (diameter = height)","clears round cross-section, reduces rotary air cutting"], cs_offe_s: "enable", cs_offe_l: "enable milling stock", From 311db9c65f53d0051437fa3e2b382fdbc4485478 Mon Sep 17 00:00:00 2001 From: Pieter Zandbergen Date: Fri, 12 Jun 2026 16:25:10 +0200 Subject: [PATCH 5/7] fix: Wrong calculations of stock radius --- src/kiri/app/init/input.js | 7 +++++- src/kiri/app/platform.js | 34 +++++++++++++++++++++++++++++- src/kiri/mode/cam/work/op-area.js | 9 ++++++-- src/kiri/mode/cam/work/op-rough.js | 7 +++--- web/kiri/lang/en.js | 2 +- 5 files changed, 51 insertions(+), 8 deletions(-) diff --git a/src/kiri/app/init/input.js b/src/kiri/app/init/input.js index 302401de5..6f8189221 100644 --- a/src/kiri/app/init/input.js +++ b/src/kiri/app/init/input.js @@ -102,6 +102,11 @@ export function onBooleanClick(el) { api.view.set_arrange(); } api.conf.update(); + // toggling the 4th axis can change whether the editor shows X/Y swapped + // (Y-aligned rotary), so refresh the platform axes/labels + if (el === ui.useIndexed) { + api.platform.update_origin(); + } DOC.activeElement.blur(); api.event.emit("boolean.click"); api.devices.update_laser_state(); @@ -268,7 +273,7 @@ export function init_input() { useLaser: newBoolean(LANG.dv_lazr_s, onBooleanClick, {title:LANG.dv_lazr_l, modes:CAM}), useIndexed: newBoolean(LANG.dv_4tha_s, onBooleanClick, {title:LANG.dv_4tha_l, modes:CAM}), indexedAxis: newSelect(LANG.dv_4tax_s, {title:LANG.dv_4tax_l, modes:CAM, show:() => ui.useIndexed.checked}, "indexedaxis"), - indexedAxisAlign: newSelect(LANG.dv_4tal_s, {title:LANG.dv_4tal_l, modes:CAM, show:() => ui.useIndexed.checked}, "indexedalign"), + indexedAxisAlign: newSelect(LANG.dv_4tal_s, {title:LANG.dv_4tal_l, modes:CAM, show:() => ui.useIndexed.checked, post:() => api.platform.update_origin()}, "indexedalign"), gcodeFExt: newInput(LANG.dv_fext_s, {title:LANG.dv_fext_l, modes:CAM_LZR, size:7, text:true}), gcodeEd: newGroup(LANG.dv_gr_gco, $('dg'), {group:"dgcp", inline, modes:GCODE}), gcodeMacros: newRow([ diff --git a/src/kiri/app/platform.js b/src/kiri/app/platform.js index c833636db..7b1be8068 100644 --- a/src/kiri/app/platform.js +++ b/src/kiri/app/platform.js @@ -60,6 +60,34 @@ function get_mode() { * * @param {boolean} [update_bounds=true] - Whether to recalculate bounds first */ +// default editor axis line colors (see moto/space.js grid.colorX/colorY) +const AXIS_COLOR_X = 0xff6666; // red +const AXIS_COLOR_Y = 0x6666ff; // blue +// track last applied axis-swap state to avoid rebuilding the grid every call +let lastAxisSwap; + +// CAM with a Y-aligned rotary swaps the X/Y axis LETTERS in gcode output +// (indexedAxisAlign, see export.js). To keep the editor consistent, present the +// X/Y axes swapped on screen too (display only): swap the axis line colors, +// ruler labels, and the device width/depth field labels. Nothing in the +// slicing pipeline changes — the part still lies along internal X. +function applyAxisSwap(swap) { + if (swap === lastAxisSwap) { + return; + } + lastAxisSwap = swap; + space.platform.setGridColor({ + colorX: swap ? AXIS_COLOR_Y : AXIS_COLOR_X, + colorY: swap ? AXIS_COLOR_X : AXIS_COLOR_Y + }); + const setLabel = (el, text) => { + const label = el && el.parentElement && el.parentElement.querySelector('label'); + if (label) label.textContent = text; + }; + setLabel(api.ui.bedWidth, swap ? 'Y (width)' : 'X (width)'); + setLabel(api.ui.bedDepth, swap ? 'X (depth)' : 'Y (depth)'); +} + function update_origin(update_bounds = true) { if (update_bounds) { platform.update_bounds(); @@ -129,7 +157,11 @@ function update_origin(update_bounds = true) { origin.x -= process.ctOriginOffX; origin.y += process.ctOriginOffY; } - space.platform.setRulers(ruler, ruler, 1 / api.view.unit_scale(), 'X', isBelt ? 'Z' : 'Y'); + const axisSwap = MODE === CAM && device.useIndexed && device.indexedAxisAlign === 'Y'; + applyAxisSwap(axisSwap); + space.platform.setRulers(ruler, ruler, 1 / api.view.unit_scale(), + axisSwap ? 'Y' : 'X', + isBelt ? 'Z' : (axisSwap ? 'X' : 'Y')); let { x, y, z } = origin; let oz = process.camStockIndexed ? z / 2 : z; diff --git a/src/kiri/mode/cam/work/op-area.js b/src/kiri/mode/cam/work/op-area.js index 76ceca7bc..88fa97217 100644 --- a/src/kiri/mode/cam/work/op-area.js +++ b/src/kiri/mode/cam/work/op-area.js @@ -174,8 +174,13 @@ class OpArea extends CamOp { // tool so the clear descent isn't aborted by an empty top sliver let roundR, roundCz; if (op.round) { - roundR = (workarea.top_stock - workarea.bottom_stock) / 2; - roundCz = (workarea.top_stock + workarea.bottom_stock) / 2; + // cylinder radius = half the stock height (diameter); its + // center sits one radius below the stock top, expressed in + // the same Z-frame as the clear loop's `z` values. NOTE: + // workarea.bottom_stock is the gap below the part, NOT the + // cylinder bottom, so it must not be used to find the center. + roundR = op.round.r; + roundCz = workarea.top_stock - roundR; zs = zs.filter(z => { let d = z - roundCz; return 2 * Math.sqrt(Math.max(0, roundR * roundR - d * d)) >= toolDiam; diff --git a/src/kiri/mode/cam/work/op-rough.js b/src/kiri/mode/cam/work/op-rough.js index e54fc355c..0e5e5aa40 100644 --- a/src/kiri/mode/cam/work/op-rough.js +++ b/src/kiri/mode/cam/work/op-rough.js @@ -30,10 +30,11 @@ class OpRough extends CamOp { // round bar on a rotary: flag the cylindrical cross-section so the clear // loop clips each Z level to the chord 2*sqrt(r^2-(z-cz)^2) instead of // air-cutting the full bounding-box width. axis = X (length), circle in - // YZ, invariant across index angles. The vertical center/radius (cz/r) - // are derived in op-area from the workarea stock extent so they share the - // exact Z-frame of the clear loop; here we only supply the X geometry. + // YZ, invariant across index angles. radius = half the stock height + // (diameter); the vertical center (cz) is derived in op-area as + // (stock top - r) so it shares the exact Z-frame of the clear loop. let round = (state.settings.process.camStockRound && state.isIndexed) ? { + r: stock.z / 2, cx: stock.center.x, cy: stock.center.y, len: stock.x diff --git a/web/kiri/lang/en.js b/web/kiri/lang/en.js index b3d15f16a..5be2dc731 100644 --- a/web/kiri/lang/en.js +++ b/web/kiri/lang/en.js @@ -178,7 +178,7 @@ self.lang['en-us'] = { dv_4tax_s: "4th axis", dv_4tax_l: ["rotary axis letter used in gcode output","A (default) or B"], dv_4tal_s: "rotary along", - dv_4tal_l: ["machine linear axis the rotary is mounted along","X (default) or Y, swaps X/Y output when Y"], + dv_4tal_l: ["machine linear axis the rotary is mounted along","X (default) or Y. when Y, the editor shows X/Y","swapped and gcode output swaps X/Y to match"], dv_dwll_s: "dwell", dv_dwll_l: "gcode dwell script", From 38e145007ee1e1ce6ca557f688114f86a933cf2e Mon Sep 17 00:00:00 2001 From: Pieter Zandbergen Date: Fri, 12 Jun 2026 16:25:10 +0200 Subject: [PATCH 6/7] fix: Wrong calculations of stock radius --- src/kiri/app/init/input.js | 7 +++++- src/kiri/app/platform.js | 34 +++++++++++++++++++++++++++++- src/kiri/mode/cam/work/op-area.js | 6 ++++-- src/kiri/mode/cam/work/op-rough.js | 7 +++--- web/kiri/lang/en.js | 2 +- 5 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/kiri/app/init/input.js b/src/kiri/app/init/input.js index 302401de5..6f8189221 100644 --- a/src/kiri/app/init/input.js +++ b/src/kiri/app/init/input.js @@ -102,6 +102,11 @@ export function onBooleanClick(el) { api.view.set_arrange(); } api.conf.update(); + // toggling the 4th axis can change whether the editor shows X/Y swapped + // (Y-aligned rotary), so refresh the platform axes/labels + if (el === ui.useIndexed) { + api.platform.update_origin(); + } DOC.activeElement.blur(); api.event.emit("boolean.click"); api.devices.update_laser_state(); @@ -268,7 +273,7 @@ export function init_input() { useLaser: newBoolean(LANG.dv_lazr_s, onBooleanClick, {title:LANG.dv_lazr_l, modes:CAM}), useIndexed: newBoolean(LANG.dv_4tha_s, onBooleanClick, {title:LANG.dv_4tha_l, modes:CAM}), indexedAxis: newSelect(LANG.dv_4tax_s, {title:LANG.dv_4tax_l, modes:CAM, show:() => ui.useIndexed.checked}, "indexedaxis"), - indexedAxisAlign: newSelect(LANG.dv_4tal_s, {title:LANG.dv_4tal_l, modes:CAM, show:() => ui.useIndexed.checked}, "indexedalign"), + indexedAxisAlign: newSelect(LANG.dv_4tal_s, {title:LANG.dv_4tal_l, modes:CAM, show:() => ui.useIndexed.checked, post:() => api.platform.update_origin()}, "indexedalign"), gcodeFExt: newInput(LANG.dv_fext_s, {title:LANG.dv_fext_l, modes:CAM_LZR, size:7, text:true}), gcodeEd: newGroup(LANG.dv_gr_gco, $('dg'), {group:"dgcp", inline, modes:GCODE}), gcodeMacros: newRow([ diff --git a/src/kiri/app/platform.js b/src/kiri/app/platform.js index c833636db..7b1be8068 100644 --- a/src/kiri/app/platform.js +++ b/src/kiri/app/platform.js @@ -60,6 +60,34 @@ function get_mode() { * * @param {boolean} [update_bounds=true] - Whether to recalculate bounds first */ +// default editor axis line colors (see moto/space.js grid.colorX/colorY) +const AXIS_COLOR_X = 0xff6666; // red +const AXIS_COLOR_Y = 0x6666ff; // blue +// track last applied axis-swap state to avoid rebuilding the grid every call +let lastAxisSwap; + +// CAM with a Y-aligned rotary swaps the X/Y axis LETTERS in gcode output +// (indexedAxisAlign, see export.js). To keep the editor consistent, present the +// X/Y axes swapped on screen too (display only): swap the axis line colors, +// ruler labels, and the device width/depth field labels. Nothing in the +// slicing pipeline changes — the part still lies along internal X. +function applyAxisSwap(swap) { + if (swap === lastAxisSwap) { + return; + } + lastAxisSwap = swap; + space.platform.setGridColor({ + colorX: swap ? AXIS_COLOR_Y : AXIS_COLOR_X, + colorY: swap ? AXIS_COLOR_X : AXIS_COLOR_Y + }); + const setLabel = (el, text) => { + const label = el && el.parentElement && el.parentElement.querySelector('label'); + if (label) label.textContent = text; + }; + setLabel(api.ui.bedWidth, swap ? 'Y (width)' : 'X (width)'); + setLabel(api.ui.bedDepth, swap ? 'X (depth)' : 'Y (depth)'); +} + function update_origin(update_bounds = true) { if (update_bounds) { platform.update_bounds(); @@ -129,7 +157,11 @@ function update_origin(update_bounds = true) { origin.x -= process.ctOriginOffX; origin.y += process.ctOriginOffY; } - space.platform.setRulers(ruler, ruler, 1 / api.view.unit_scale(), 'X', isBelt ? 'Z' : 'Y'); + const axisSwap = MODE === CAM && device.useIndexed && device.indexedAxisAlign === 'Y'; + applyAxisSwap(axisSwap); + space.platform.setRulers(ruler, ruler, 1 / api.view.unit_scale(), + axisSwap ? 'Y' : 'X', + isBelt ? 'Z' : (axisSwap ? 'X' : 'Y')); let { x, y, z } = origin; let oz = process.camStockIndexed ? z / 2 : z; diff --git a/src/kiri/mode/cam/work/op-area.js b/src/kiri/mode/cam/work/op-area.js index 76ceca7bc..9ffdf4911 100644 --- a/src/kiri/mode/cam/work/op-area.js +++ b/src/kiri/mode/cam/work/op-area.js @@ -174,8 +174,10 @@ class OpArea extends CamOp { // tool so the clear descent isn't aborted by an empty top sliver let roundR, roundCz; if (op.round) { - roundR = (workarea.top_stock - workarea.bottom_stock) / 2; - roundCz = (workarea.top_stock + workarea.bottom_stock) / 2; + // cylinder radius and center (rotary axis) come straight from + // the stock; cz is in the same Z-frame as the clear loop's `z` + roundR = op.round.r; + roundCz = op.round.cz; zs = zs.filter(z => { let d = z - roundCz; return 2 * Math.sqrt(Math.max(0, roundR * roundR - d * d)) >= toolDiam; diff --git a/src/kiri/mode/cam/work/op-rough.js b/src/kiri/mode/cam/work/op-rough.js index e54fc355c..8e41b5314 100644 --- a/src/kiri/mode/cam/work/op-rough.js +++ b/src/kiri/mode/cam/work/op-rough.js @@ -30,12 +30,13 @@ class OpRough extends CamOp { // round bar on a rotary: flag the cylindrical cross-section so the clear // loop clips each Z level to the chord 2*sqrt(r^2-(z-cz)^2) instead of // air-cutting the full bounding-box width. axis = X (length), circle in - // YZ, invariant across index angles. The vertical center/radius (cz/r) - // are derived in op-area from the workarea stock extent so they share the - // exact Z-frame of the clear loop; here we only supply the X geometry. + // YZ, invariant across index angles. radius = half the stock height + // (diameter), centered on the stock center (the rotary axis). let round = (state.settings.process.camStockRound && state.isIndexed) ? { + r: stock.z / 2, cx: stock.center.x, cy: stock.center.y, + cz: stock.center.z, len: stock.x } : undefined; diff --git a/web/kiri/lang/en.js b/web/kiri/lang/en.js index b3d15f16a..5be2dc731 100644 --- a/web/kiri/lang/en.js +++ b/web/kiri/lang/en.js @@ -178,7 +178,7 @@ self.lang['en-us'] = { dv_4tax_s: "4th axis", dv_4tax_l: ["rotary axis letter used in gcode output","A (default) or B"], dv_4tal_s: "rotary along", - dv_4tal_l: ["machine linear axis the rotary is mounted along","X (default) or Y, swaps X/Y output when Y"], + dv_4tal_l: ["machine linear axis the rotary is mounted along","X (default) or Y. when Y, the editor shows X/Y","swapped and gcode output swaps X/Y to match"], dv_dwll_s: "dwell", dv_dwll_l: "gcode dwell script", From 91598484dfe2b7f7aee7cb8039a1b29a27124825 Mon Sep 17 00:00:00 2001 From: Pieter Zandbergen Date: Sat, 13 Jun 2026 09:09:49 +0200 Subject: [PATCH 7/7] fix: Swapping indexed axis align issues --- src/kiri/app/init/input.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kiri/app/init/input.js b/src/kiri/app/init/input.js index 6f8189221..773fe48f1 100644 --- a/src/kiri/app/init/input.js +++ b/src/kiri/app/init/input.js @@ -273,7 +273,7 @@ export function init_input() { useLaser: newBoolean(LANG.dv_lazr_s, onBooleanClick, {title:LANG.dv_lazr_l, modes:CAM}), useIndexed: newBoolean(LANG.dv_4tha_s, onBooleanClick, {title:LANG.dv_4tha_l, modes:CAM}), indexedAxis: newSelect(LANG.dv_4tax_s, {title:LANG.dv_4tax_l, modes:CAM, show:() => ui.useIndexed.checked}, "indexedaxis"), - indexedAxisAlign: newSelect(LANG.dv_4tal_s, {title:LANG.dv_4tal_l, modes:CAM, show:() => ui.useIndexed.checked, post:() => api.platform.update_origin()}, "indexedalign"), + indexedAxisAlign: newSelect(LANG.dv_4tal_s, {title:LANG.dv_4tal_l, modes:CAM, show:() => ui.useIndexed.checked, action: settingsOps.update_device}, "indexedalign"), gcodeFExt: newInput(LANG.dv_fext_s, {title:LANG.dv_fext_l, modes:CAM_LZR, size:7, text:true}), gcodeEd: newGroup(LANG.dv_gr_gco, $('dg'), {group:"dgcp", inline, modes:GCODE}), gcodeMacros: newRow([