Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bin/esbuild.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ const rec = {
define: { 'process.env.NODE_ENV': `"${mode}"` },
external: [
'module',
'node:module',
'./constants',
'./voronoi_structures',
'./voronoi_ctypes',
Expand Down
7 changes: 5 additions & 2 deletions src/dock/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
FROM node:18-slim
FROM node:22-slim
WORKDIR /grid
COPY package.json /grid
COPY app.js /grid
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 run webpack-ext
RUN npm install -g @gridspace/app-server
RUN npm run pack-prod
RUN rm -rf /grid/bin
CMD gs-app-server

1 change: 1 addition & 0 deletions src/kiri/app/conf/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,7 @@ export const conf = {
camRoughTop: true,
camRoundCorners: true,
camStockClipTo: false,
camStockCylinder: false,
camStockIndexed: false,
camStockIndexGrid: true,
camStockOffset: true,
Expand Down
10 changes: 9 additions & 1 deletion src/kiri/app/inputs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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');
Expand Down
21 changes: 17 additions & 4 deletions src/kiri/app/platform.js
Original file line number Diff line number Diff line change
Expand Up @@ -244,18 +244,31 @@ 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,
y: camStockY,
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,
Expand Down
54 changes: 33 additions & 21 deletions src/kiri/mode/cam/app/cl-stock.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,43 +102,55 @@ 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);
}
// 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;
Expand Down
10 changes: 8 additions & 2 deletions src/kiri/mode/cam/app/init-menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -133,11 +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}),
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([
Expand Down
47 changes: 46 additions & 1 deletion src/kiri/mode/cam/work/op-area.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand All @@ -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 ];
Expand Down
31 changes: 21 additions & 10 deletions src/kiri/mode/cam/work/prepare.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
20 changes: 19 additions & 1 deletion src/kiri/mode/cam/work/slice.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,25 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
z: camStockZ,
center: newPoint(pos.x, pos.y, pos.z)
};
if (!camStockOffset && axisIndex && isIndexed) {
// cylindrical stock is symmetric about the X (rotary) axis, so its
// 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;
}
// 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) {
Expand Down
4 changes: 4 additions & 0 deletions web/kiri/lang/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -867,12 +867,16 @@ 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",
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",

Expand Down