From ed9a341c167d6cc973a233aeaa8172d01bd187d0 Mon Sep 17 00:00:00 2001 From: Pieter Zandbergen Date: Sun, 14 Jun 2026 10:59:11 +0200 Subject: [PATCH 1/7] feat: Set and render cylindrical stock --- src/kiri/app/conf/defaults.js | 1 + src/kiri/mode/cam/app/cl-stock.js | 54 ++++++++++++++++++------------ src/kiri/mode/cam/app/init-menu.js | 1 + web/kiri/lang/en.js | 2 ++ 4 files changed, 37 insertions(+), 21 deletions(-) diff --git a/src/kiri/app/conf/defaults.js b/src/kiri/app/conf/defaults.js index 1efecc0b1..2918976f0 100644 --- a/src/kiri/app/conf/defaults.js +++ b/src/kiri/app/conf/defaults.js @@ -590,6 +590,7 @@ export const conf = { camRoughTop: true, camRoundCorners: true, camStockClipTo: false, + camStockCylinder: false, camStockIndexed: false, camStockIndexGrid: true, camStockOffset: true, diff --git a/src/kiri/mode/cam/app/cl-stock.js b/src/kiri/mode/cam/app/cl-stock.js index 550fdfb43..6b0482a40 100644 --- a/src/kiri/mode/cam/app/cl-stock.js +++ b/src/kiri/mode/cam/app/cl-stock.js @@ -102,35 +102,47 @@ export function updateStock() { } if (x && y && z) { UI.func.animate.classList.remove('disabled'); + // cylindrical stock is only valid in indexed (rotary) mode + const cylindrical = process.camStockIndexed && process.camStockCylinder; { - let geo = new THREE.BoxGeometry(1, 1, 1); let mat = new THREE.MeshBasicMaterial({ color: 0x777777, opacity: 0.05, transparent: true, side: THREE.DoubleSide }); + let geo, lines; + if (cylindrical) { + // cylinder runs along the rotary (X) axis, diameter = height (z) + geo = new THREE.CylinderGeometry(0.5, 0.5, 1, 60); + geo.rotateZ(Math.PI / 2); + let ligeo = new THREE.EdgesGeometry(geo, 30); + let limat = new THREE.LineBasicMaterial({ color: 0xaaaaaa }); + lines = new THREE.LineSegments(ligeo, limat); + } else { + geo = new THREE.BoxGeometry(1, 1, 1); + 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 = 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); env.camStock.lines = lines; env.camStock.add(lines); SPACE.world.add(env.camStock); @@ -138,7 +150,7 @@ export function updateStock() { // fight z fighting in threejs const { scale, position, lines } = env.camStock; scale.x = x + 0.005; - scale.y = y + 0.005; + scale.y = (cylindrical ? z : y) + 0.005; scale.z = z + 0.005; position.x = center.x; position.y = center.y; diff --git a/src/kiri/mode/cam/app/init-menu.js b/src/kiri/mode/cam/app/init-menu.js index 9a9ae7d2c..906b6db35 100644 --- a/src/kiri/mode/cam/app/init-menu.js +++ b/src/kiri/mode/cam/app/init-menu.js @@ -135,6 +135,7 @@ export function menu() { camStockX: newInput(LANG.cs_wdth_s, {title:LANG.cs_wdth_l, convert:toFloat, bound:bound(0,9999), units}), camStockY: newInput(LANG.cs_dpth_s, {title:LANG.cs_dpth_l, convert:toFloat, bound:bound(0,9999), units}), camStockZ: newInput(LANG.cs_hght_s, {title:LANG.cs_hght_l, convert:toFloat, bound:bound(0,9999), units}), + camStockCylinder: newBoolean(LANG.cs_cyli_s, onBooleanClick, {title:LANG.cs_cyli_l, show:() => ui.camStockIndexed.checked}), separator: newBlank({ class:"set-sep", driven }), camStockOffset: newBoolean(LANG.cs_offs_s, onBooleanClick, {title:LANG.cs_offs_l}), camStockIndexed: newBoolean(LANG.cs_indx_s, onBooleanClick, {title:LANG.cs_indx_l}), diff --git a/web/kiri/lang/en.js b/web/kiri/lang/en.js index a6bbf3fc0..88c2bed43 100644 --- a/web/kiri/lang/en.js +++ b/web/kiri/lang/en.js @@ -873,6 +873,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_cyli_s: "cylindrical", + cs_cyli_l: ["diameter = height","reduces air cutting when","using cylindrical stock"], cs_offe_s: "enable", cs_offe_l: "enable milling stock", From 4c8f90123835d83ae87b00f2a471a2d54656cfe8 Mon Sep 17 00:00:00 2001 From: Pieter Zandbergen Date: Sun, 14 Jun 2026 11:10:06 +0200 Subject: [PATCH 2/7] fix: We require git in the docker image --- src/dock/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dock/Dockerfile b/src/dock/Dockerfile index d38ff39f5..f466e1b4e 100644 --- a/src/dock/Dockerfile +++ b/src/dock/Dockerfile @@ -6,7 +6,7 @@ COPY apps /grid/apps COPY src /grid/src COPY web /grid/web COPY bin /grid/bin -RUN apt-get -y update; apt-get -y install curl +RUN apt-get -y update; apt-get -y install curl git EXPOSE 8080 RUN npm i RUN npm install -g @gridspace/app-server From f2e0f89fbf82a50414247653318ab0d8b64ab725 Mon Sep 17 00:00:00 2001 From: Pieter Zandbergen Date: Sun, 14 Jun 2026 11:12:00 +0200 Subject: [PATCH 3/7] fix: Upgrade docker image to NodeJS 22 --- src/dock/Dockerfile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/dock/Dockerfile b/src/dock/Dockerfile index f466e1b4e..75a6fe0a7 100644 --- a/src/dock/Dockerfile +++ b/src/dock/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18-slim +FROM node:22-slim WORKDIR /grid COPY package.json /grid COPY app.js /grid @@ -9,6 +9,9 @@ COPY bin /grid/bin RUN apt-get -y update; apt-get -y install curl git EXPOSE 8080 RUN npm i +RUN npm run webpack-ext RUN npm install -g @gridspace/app-server +RUN npm run pack-prod RUN rm -rf /grid/bin CMD gs-app-server + From 8010d6c661f78a235b6973bfa8c9588ee8f6e310 Mon Sep 17 00:00:00 2001 From: Pieter Zandbergen Date: Sun, 14 Jun 2026 11:17:44 +0200 Subject: [PATCH 4/7] fix: Import node:module --- bin/esbuild.config.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/esbuild.config.mjs b/bin/esbuild.config.mjs index 11b24998a..4f1ccff91 100644 --- a/bin/esbuild.config.mjs +++ b/bin/esbuild.config.mjs @@ -77,6 +77,7 @@ const rec = { define: { 'process.env.NODE_ENV': `"${mode}"` }, external: [ 'module', + 'node:module', './constants', './voronoi_structures', './voronoi_ctypes', From 505b1869c0a129c12d87bbe21fee639b575e9406 Mon Sep 17 00:00:00 2001 From: Pieter Zandbergen Date: Sun, 14 Jun 2026 20:39:34 +0200 Subject: [PATCH 5/7] fix: Cylindrical paths --- src/kiri/mode/cam/work/op-area.js | 47 ++++++++++++++++++++++++++++++- src/kiri/mode/cam/work/slice.js | 7 +++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/kiri/mode/cam/work/op-area.js b/src/kiri/mode/cam/work/op-area.js index 7a0598135..d4986655c 100644 --- a/src/kiri/mode/cam/work/op-area.js +++ b/src/kiri/mode/cam/work/op-area.js @@ -30,7 +30,7 @@ class OpArea extends CamOp { let { direction, down, expand, flats, flatOff, follow } = op; let { mode, outline, over, rename, smooth, tool } = op; let { addSlices, axisIndex, color, cutTabs, settings } = state; - let { shadowAt, setToolDiam, tabs, widget, workarea } = state; + let { shadowAt, setToolDiam, stock, tabs, widget, workarea } = state; let areaTool = new Tool(settings, tool); let smoothVal = (smooth ?? 0) / 10; @@ -137,6 +137,23 @@ class OpArea extends CamOp { // filter out invalid polys polys = polys.filter(p => p && p.length > 2); + // for cylindrical (round) indexed stock, clearing must be limited to + // the circular cross-section present at each Z, otherwise the tool + // air-cuts the corners of the (non-existent) rectangular bounding box + let cylinder; + if (stock?.cylindrical && mode === 'clear') { + let radius = stock.radius; + cylinder = { + r2: radius * radius, + // top of the round stock sits one radius above the rotary axis + cz: workarea.top_stock - radius, + cx: stock.center.x, + cy: stock.center.y, + // pad in X so the full length along the rotary axis is preserved + hx: stock.x / 2 + toolDiam + }; + } + // process each area separately let proc = 0; let pinc = 1 / polys.length; @@ -176,6 +193,23 @@ class OpArea extends CamOp { outer: for (;;) for (let z of zs) { + // round-stock cross-section (chord) at this cutting height + let band; + if (cylinder) { + let dz = z - cylinder.cz; + let h2 = cylinder.r2 - dz * dz; + // above or below the round stock: no material to clear + if (h2 <= 0) { + if (z === zs.peek()) break outer; + continue; + } + let h = Math.sqrt(h2); + band = newPolygon() + .add(cylinder.cx - cylinder.hx, cylinder.cy - h, z) + .add(cylinder.cx + cylinder.hx, cylinder.cy - h, z) + .add(cylinder.cx + cylinder.hx, cylinder.cy + h, z) + .add(cylinder.cx - cylinder.hx, cylinder.cy + h, z); + } let slice = newLayer(z); let layers = slice.output(); let shadow = await shadowAt(z + 0.01); @@ -197,6 +231,10 @@ class OpArea extends CamOp { } else { POLY.subtract([ area ], shadow, clip, undefined, undefined, 0); } + // restrict clearing to the round stock cross-section + if (band) { + clip = POLY.trimTo(clip, [ band ]) ?? []; + } //generate offsets to use let offsets = [ firstOff ]; //if we need a finish cut, add it @@ -212,6 +250,13 @@ class OpArea extends CamOp { }); // if we see no offsets, re-check the mesh bottom Z then exit if (outs.length === 0) { + // round stock: thin upper slices can yield no cut even + // though wider slices below still have material. keep + // descending instead of terminating the clear early. + if (cylinder) { + if (z === zs.peek()) break outer; + continue; + } if (bounds && lzo > bounds.min.z) { // try a bottom layer matching bottom of selection zs = [ bounds.min.z ]; diff --git a/src/kiri/mode/cam/work/slice.js b/src/kiri/mode/cam/work/slice.js index d4968bc7e..6bcfe8182 100644 --- a/src/kiri/mode/cam/work/slice.js +++ b/src/kiri/mode/cam/work/slice.js @@ -99,6 +99,13 @@ export async function cam_slice(settings, widget, onupdate, ondone) { z: camStockZ, center: newPoint(pos.x, pos.y, pos.z) }; + // cylindrical stock is symmetric about the X (rotary) axis, so its + // radius (= half the stock height) is rotation-invariant. capture it + // here, before any YZ rotation is applied to the stock dimensions. + if (isIndexed && proc.camStockCylinder) { + stock.cylindrical = true; + stock.radius = stock.z / 2; + } if (!camStockOffset && axisIndex && isIndexed) { if (axisIndex === 0 || axisIndex === 180) { // do nothing From ac4c66ad230f57863d09142fe7dd54d5e1699ba7 Mon Sep 17 00:00:00 2001 From: Pieter Zandbergen Date: Sat, 18 Jul 2026 11:49:03 +0200 Subject: [PATCH 6/7] Reordered and renamed inputs --- src/kiri/app/inputs.js | 10 +++++++++- src/kiri/mode/cam/app/init-menu.js | 11 ++++++++--- web/kiri/lang/en.js | 2 ++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/kiri/app/inputs.js b/src/kiri/app/inputs.js index 4ee9d6a89..fea3d7f93 100644 --- a/src/kiri/app/inputs.js +++ b/src/kiri/app/inputs.js @@ -518,6 +518,11 @@ function addModeControls(el, opt = {}) { let hmod = el.hasMode(mode); if (opt.trace) console.log({ setMode: mode, xprt, show, disp, hmod, modes:el.modes }); if (opt.manual) return; + // re-evaluate a dynamic label (re-runs alongside show predicates) + if (opt.dynLabel && el.__label) { + let text = safecall(opt.dynLabel); + if (text !== false) el.__label.textContent = text; + } el.setVisible(!hidn && hmod && show && xprt && disp); } el.hasMode = function(mode) { @@ -739,7 +744,10 @@ function newInput(label, opt = {}) { action = opt.action || bindTo || inputAction, ip = height > 1 ? DOC.createElement('textarea') : DOC.createElement('input'); - row.appendChild(newLabel(label)); + let labelEl = newLabel(label); + row.appendChild(labelEl); + // expose the label node so a dynamic label (opt.dynLabel) can update it + if (opt.dynLabel) row.__label = labelEl; if (opt.selector) { let sel = DOC.createElement('button'); sel.classList.add('z-select'); diff --git a/src/kiri/mode/cam/app/init-menu.js b/src/kiri/mode/cam/app/init-menu.js index 906b6db35..09cad04cd 100644 --- a/src/kiri/mode/cam/app/init-menu.js +++ b/src/kiri/mode/cam/app/init-menu.js @@ -21,6 +21,11 @@ function onBooleanClick(el) { api.event.emit('click.boolean', el); } +// cylindrical stock is only valid in indexed (rotary) mode +function isCylindrical() { + return ui.camStockIndexed.checked && ui.camStockCylinder.checked; +} + function onButtonClick(el) { api.event.emit('click.button', el); } @@ -133,12 +138,12 @@ export function menu() { ], {class:"ext-buttons f-row"}), _____: newGroup(LANG.cs_menu, $('cam-stock'), { modes:CAM, driven, separator, hideable, group:"cam-stock" }), camStockX: newInput(LANG.cs_wdth_s, {title:LANG.cs_wdth_l, convert:toFloat, bound:bound(0,9999), units}), - camStockY: newInput(LANG.cs_dpth_s, {title:LANG.cs_dpth_l, convert:toFloat, bound:bound(0,9999), units}), - camStockZ: newInput(LANG.cs_hght_s, {title:LANG.cs_hght_l, convert:toFloat, bound:bound(0,9999), units}), - camStockCylinder: newBoolean(LANG.cs_cyli_s, onBooleanClick, {title:LANG.cs_cyli_l, show:() => ui.camStockIndexed.checked}), + camStockY: newInput(LANG.cs_dpth_s, {title:LANG.cs_dpth_l, convert:toFloat, bound:bound(0,9999), units, show:() => !isCylindrical()}), + camStockZ: newInput(LANG.cs_hght_s, {title:LANG.cs_hght_l, convert:toFloat, bound:bound(0,9999), units, dynLabel:() => isCylindrical() ? LANG.cs_diam_s : LANG.cs_hght_s}), separator: newBlank({ class:"set-sep", driven }), camStockOffset: newBoolean(LANG.cs_offs_s, onBooleanClick, {title:LANG.cs_offs_l}), camStockIndexed: newBoolean(LANG.cs_indx_s, onBooleanClick, {title:LANG.cs_indx_l}), + camStockCylinder: newBoolean(LANG.cs_cyli_s, onBooleanClick, {title:LANG.cs_cyli_l, show:() => ui.camStockIndexed.checked}), camStockIndexGrid: newBoolean(LANG.cs_ishg_s, onBooleanClick, {title:LANG.cs_ishg_l, show:() => ui.camStockIndexed.checked}), // separator: newBlank({ class:"set-sep", driven }), // camStockManual: newRow([ diff --git a/web/kiri/lang/en.js b/web/kiri/lang/en.js index 88c2bed43..e84ee20d2 100644 --- a/web/kiri/lang/en.js +++ b/web/kiri/lang/en.js @@ -867,6 +867,8 @@ self.lang['en-us'] = { cs_dpth_l: ["depth (y) in workspace units","0 defaults to part size"], cs_hght_s: "height", cs_hght_l: ["height (z) in workspace units","0 defaults to part size"], + cs_diam_s: "diameter", + cs_diam_l: ["diameter (z) in workspace units","0 defaults to part size"], cs_offs_s: "offset", cs_offs_l: ["use width, depth, height","as offsets from max","part size on platform"], cs_ishg_s: "show grid", From cc7f8e4decfb95620617196d0b6528d60f32a609 Mon Sep 17 00:00:00 2001 From: Pieter Zandbergen Date: Sat, 18 Jul 2026 12:04:54 +0200 Subject: [PATCH 7/7] Support offset for cylindrical stock --- src/kiri/app/platform.js | 21 +++++++++++++++++---- src/kiri/mode/cam/work/prepare.js | 31 +++++++++++++++++++++---------- src/kiri/mode/cam/work/slice.js | 17 ++++++++++++++--- 3 files changed, 52 insertions(+), 17 deletions(-) diff --git a/src/kiri/app/platform.js b/src/kiri/app/platform.js index 5bd080a44..9903ff345 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, camStockCylinder } = process; if (mode === 'CAM') { let stock = settings.stock = { x: camStockX, @@ -252,10 +252,23 @@ function platformUpdateStock() { z: camStockZ }; // drop back to offset mode if any stock dimension is 0 - if (camStockOffset || (stock.x * stock.y * stock.z === 0)) { + const offset = camStockOffset || (stock.x * stock.y * stock.z === 0); + const partY = bounds.max.y - bounds.min.y; + const partZ = bounds.max.z - bounds.min.z; + if (offset) { stock.x += bounds.max.x - bounds.min.x; - stock.y += bounds.max.y - bounds.min.y; - stock.z += bounds.max.z - bounds.min.z; + stock.y += partY; + stock.z += partZ; + } + // cylindrical stock is a circle in the Y-Z plane centered on the X + // (rotary) axis. depth (Y) is not an independent dimension for round + // stock, so in offset mode the diameter spans the part's cross-section + // diagonal (from actual geometry) plus the camStockZ diameter offset. + if (camStockIndexed && camStockCylinder) { + if (offset) { + stock.z = Math.hypot(partY, partZ) + camStockZ; + } + stock.y = stock.z; } stock.center = { x: (bounds.max.x + bounds.min.x) / 2, diff --git a/src/kiri/mode/cam/work/prepare.js b/src/kiri/mode/cam/work/prepare.js index 288d3b9a6..36df1cf50 100644 --- a/src/kiri/mode/cam/work/prepare.js +++ b/src/kiri/mode/cam/work/prepare.js @@ -88,19 +88,30 @@ 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, camStockCylinder } = process, { camForceZMax, camFullEngage, camInnerFirst, camOriginCenter } = process, { camOriginOffX, camOriginOffY, camOriginOffZ, camZClearance } = process, bounds = widget.getBoundingBox(), - stock = camStockOffset ? { - x: bounds.dim.x + camStockX, - y: bounds.dim.y + camStockY, - z: bounds.dim.z + camStockZ, - } : { - x: camStockX, - y: camStockY, - z: camStockZ - }, + stock = (() => { + let s = camStockOffset ? { + x: bounds.dim.x + camStockX, + y: bounds.dim.y + camStockY, + z: bounds.dim.z + camStockZ, + } : { + x: camStockX, + y: camStockY, + z: camStockZ + }; + // cylindrical stock is a circle in the Y-Z plane centered on the + // rotary axis. depth (Y) is not an independent dimension for round + // stock, so the diameter spans the part cross-section diagonal + // (from geometry) plus the camStockZ diameter offset. + if (camStockIndexed && camStockCylinder) { + if (camStockOffset) s.z = Math.hypot(bounds.dim.y, bounds.dim.z) + camStockZ; + s.y = s.z; + } + return s; + })(), 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 6bcfe8182..72d8f0d9f 100644 --- a/src/kiri/mode/cam/work/slice.js +++ b/src/kiri/mode/cam/work/slice.js @@ -100,13 +100,24 @@ export async function cam_slice(settings, widget, onupdate, ondone) { center: newPoint(pos.x, pos.y, pos.z) }; // cylindrical stock is symmetric about the X (rotary) axis, so its - // radius (= half the stock height) is rotation-invariant. capture it - // here, before any YZ rotation is applied to the stock dimensions. + // radius is rotation-invariant. capture it here, before any YZ rotation + // is applied to the stock dimensions. in offset mode the diameter must + // span the part's cross-section diagonal so the part can't poke through + // the round stock. if (isIndexed && proc.camStockCylinder) { stock.cylindrical = true; + if (camStockOffset) { + // depth (Y) is not an independent dimension for round stock, so + // the diameter spans the part cross-section diagonal (from + // geometry) plus the camStockZ diameter offset. + stock.z = Math.hypot(bounds.dim.y, bounds.dim.z) + camStockZ; + } + stock.y = stock.z; stock.radius = stock.z / 2; } - if (!camStockOffset && axisIndex && isIndexed) { + // a cylinder's cross-section doesn't change with rotation, so only box + // stock needs its YZ dimensions rotated to the current index angle. + if (!camStockOffset && axisIndex && isIndexed && !stock.cylindrical) { if (axisIndex === 0 || axisIndex === 180) { // do nothing } else if (axisIndex === 90 || axisIndex === 270) {