Merge branch 'rel-4.3'

This commit is contained in:
Stewart Allen 2025-10-18 13:29:06 -04:00
commit 5d3e32e576
30 changed files with 644 additions and 206 deletions

3
.gitignore vendored
View file

@ -9,7 +9,8 @@ jszip-esm.js
logs
mod
node_modules
pack
package-lock.json
three.js
tmp
pack
web/boot/bundle.bin

31
app.js
View file

@ -37,6 +37,7 @@ let util;
let dir;
let log;
let pre;
let alt;
const EventEmitter = require('events');
class AppEmitter extends EventEmitter {}
@ -64,6 +65,7 @@ function init(mod) {
startTime = time();
lastmod = mod.util.lastmod;
logger = mod.log;
alt = ENV.alt;
pre = ENV.pre || mod.meta.pre;
debug = ENV.debug || mod.meta.debug;
oversion = ENV.over || mod.meta.over;
@ -191,6 +193,19 @@ function init(mod) {
append[key] = minify(append[key]);
}
}
if (alt) {
for (let [ key, val ] of Object.entries(append)) {
let src = `src/main/${key}.js`;
if (!fs.existsSync(src)) {
logger.log('missing:', src);
continue;
}
fs.mkdirSync(`alt/main`, { recursive: true });
let body = fs.readFileSync(src);
fs.writeFileSync(`alt/main/${key}.js`, body + val);
}
}
};
// either add module assets to path or require(init.js)
@ -288,14 +303,14 @@ function initModule(mod, file, dir) {
const path = mod.dir + '/' + dir + '/' + file;
try {
const body = fs.readFileSync(path);
// console.log({ inject: code, file, opt });
logger.log({ inject: code, file, opt });
if (opt.first) {
append[code] = body.toString() + '\n' + append[code];
} else {
append[code] += body.toString() + '\n';
}
} catch (e) {
console.log({ missing_file: path, dir, mod });
logger.log({ missing_file: path, dir, mod });
}
},
onload(fn) {
@ -430,6 +445,10 @@ function generateDevices() {
let dstr = JSON.stringify(devs);
util.mkdir(PATH.join(dir,"src","pack"));
fs.writeFileSync(PATH.join(dir,"src","pack","kiri-devs.js"), `export const devices = ${dstr};`);
if (alt) {
fs.mkdirSync('alt/pack', { recursive: true });
fs.writeFileSync(PATH.join(dir,"alt","pack","kiri-devs.js"), `export const devices = ${dstr};`);
}
}
function minify(code) {
@ -526,7 +545,13 @@ function fixedmap(prefix, map) {
// HTTP 307 redirect
function redir(path, type) {
return (req, res, next) => {
http.redirect(res, path, type);
let query = req.url.split('?')[1];
let rpath = path;
if (query && path.indexOf('?') < 0) {
rpath += `?${query}`;
}
// console.log({ redir: req.url, to: path, query, new_path: rpath });
http.redirect(res, rpath, type);
}
}

View file

@ -1,11 +1,17 @@
{
"compress": false,
"outputs": {
"bundle": "./web/kiri/bundle.bin"
"bundle": "./web/boot/bundle.bin"
},
"inputs": [
{ "root": "./web", "prefix": "" },
{ "root": "./src", "prefix": "lib" },
{ "root": "./src/wasm", "prefix": "wasm" },
{ "root": "./src/wasm", "prefix": "lib/kiri/wasm" }
{ "root": "web", "prefix": "" },
{ "root": "alt", "prefix": "lib"},
{ "root": "src", "prefix": "lib"},
{ "root": "src/wasm", "prefix": "wasm" },
{ "root": "src/wasm", "prefix": "lib/kiri/wasm" }
],
"excludes": [
"src/pack",
"web/boot/bundle.bin"
]
}

View file

@ -3,11 +3,21 @@ import path from 'path';
import uglify from 'uglify-js';
import { Buffer } from 'buffer';
const cfg = JSON.parse(fs.readFileSync(process.argv[2] || './bundle.config.json', 'utf8'));
const compress = process.env.UGLIFY ? true : false
const cfg = JSON.parse(fs.readFileSync(process.argv[2] || './bin/bundle.config.json', 'utf8'));
const { inputs, outputs, excludes, compress } = cfg;
const envcomp = process.env.COMPRESS ? JSON.parse(process.env.COMPRESS) : undefined;
const debug = process.env.DEBUG;
// --- recursive directory walker (follows symlinks safely) ---
function* walk(dir, seen = new Set()) {
dir = path.normalize(dir);
if (excludes.indexOf(dir) >= 0) {
return;
}
if (!fs.existsSync(dir)) {
return;
}
const real = fs.realpathSync(dir);
if (seen.has(real)) {
return;
@ -25,13 +35,19 @@ function* walk(dir, seen = new Set()) {
// --- collect all files according to config ---
let entries = [];
for (const input of cfg.inputs) {
let virtMap = new Set();
for (const input of inputs) {
const root = input.root;
const prefix = input.prefix || '';
for (const file of walk(root)) {
const rel = path.relative(root, file).replace(/\\/g, '/');
const virt = prefix ? `${prefix}/${rel}` : rel;
entries.push({ file, virt });
if (virtMap.has(virt)) {
console.log('pre-existing', virt, rel, root);
} else {
entries.push({ file, virt });
virtMap.add(virt);
}
}
}
@ -40,28 +56,54 @@ const dataParts = [];
const entryMeta = [];
let offset = 0;
let cache = new Map();
let compEnable = (envcomp ?? compress);
let cacheDir = cfg.cacheDir ?? ".bcache";
if (compEnable) {
fs.mkdirSync(cacheDir, { recursive: true });
}
function fileTime(path) {
try {
return (fs.statSync(path).ctimeMs/1000) | 0;
} catch (e) {
return 0;
}
}
for (const { file, virt } of entries) {
if (excludes.indexOf(file) >= 0) {
continue;
}
let record = cache.get(file);
if (record) {
// console.log({ dup: file });
if (debug) console.log({ alias: virt, from: file });
entryMeta.push({ ...record, virt });
} else {
if (debug) console.log({ write: virt, from: file });
let data = fs.readFileSync(file);
if (compress && file.endsWith("js")) {
console.log('compress', file);
data = uglify.minify(data.toString(), {
compress: {
merge_vars: false,
unused: false
if (compEnable && file.endsWith("js")) {
let cpath = path.resolve(cacheDir, file);
if (fileTime(cpath) > fileTime(file)) {
data = fs.readFileSync(cpath);
console.log('cached', file);
} else {
console.log('compress', file);
data = uglify.minify(data.toString(), {
compress: {
merge_vars: false,
unused: false
}
});
// console.log({ to: typeof(data), data });
if (!data.code) {
if (debug) console.log({ skip: file });
continue;
}
});
// console.log({ to: typeof(data), data });
if (!data.code) {
console.log({ skip: file });
continue;
data = Buffer.from(data.code);
fs.mkdirSync(path.dirname(cpath), { recursive: true });
fs.writeFileSync(cpath, data);
}
data = Buffer.from(data.code);
}
record = { virt, offset, length: data.length };
cache.set(file, record);
@ -99,7 +141,7 @@ const header = Buffer.concat([count, ...headerParts]);
const full = Buffer.concat([header, ...dataParts]);
// --- write final bundle ---
const outPath = cfg.outputs.bundle || './bundle.bin';
const outPath = outputs.bundle || './bundle.bin';
fs.mkdirSync(path.dirname(outPath), { recursive: true });
fs.writeFileSync(outPath, full); // ⚡ write raw binary

View file

@ -32,7 +32,6 @@
* `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` new parameter to cap output flow rate (on printer device)
* `F` new parameters for bridging speed / bridge fan control
* `F` convert ranges to z offsets while continuing to show layer #
* `F` support pillar top/bottom should conform to part

View file

@ -1,6 +1,6 @@
{
"name": "grid-apps",
"version": "4.3.3",
"version": "4.3.4",
"description": "grid.space 3d slicing & modeling tools",
"author": "Stewart Allen <sa@grid.space>",
"license": "MIT",
@ -61,7 +61,7 @@
"three": "^0.178.0",
"three-mesh-bvh": "^0.7.6",
"uglify-js": "3.14.5",
"validator": ">=13.7.0",
"validator": ">=13.15.15",
"ws": "^8.18.2"
},
"devDependencies": {
@ -82,7 +82,7 @@
"webpack-cli": "^5.1.4"
},
"scripts": {
"setup": "npm run setup-mods && npm i && npm run clean && npm run dev -- --dryrun && npm run webpack-ext",
"setup": "npm run setup-mods && npm i && npm run clean && npm run dev -- --alt --dryrun && npm run webpack-ext",
"setup-mods": "cd mods && npm i",
"build": "npm run prebuild && electron-builder",
"build-nopublish": "npm run prebuild && electron-builder --publish never",
@ -93,13 +93,13 @@
"build-win-arm": "npm run build -- --win --arm64",
"build-mac": "npm run build -- --mac --arm64",
"build-mac-intel": "npm run build -- --mac --x64",
"clean": "rm -rf build data dist src/pack tmp src.old web.old",
"clean": "rm -rf build data dist src/pack tmp src.old web.old .bcache",
"docs-dev": "docusaurus start --config conf/docusaurus.config.js",
"docs-build": "docusaurus build --config conf/docusaurus.config.js",
"docs-serve": "docusaurus serve --config conf/docusaurus.config.js",
"docs-check": "prettier --config ./conf/prettier.config.js ./docs --check",
"dev": "npm run webpack-ext && gs-app-server --debug --single",
"prod": "npm run webpack-ext && npm run webpack-src prod && gs-app-server",
"prod": "npm run webpack-ext && npm run webpack-src prod && COMPRESS=1 node bin/bundler.mjs && gs-app-server",
"prod-pack": "npm run webpack-ext && npm run webpack-src prod",
"prod-dryrun": "gs-app-server --dryrun",
"start": "npm run prebuild prod && electron .",

View file

@ -225,7 +225,9 @@ Widget.prototype.encode = function(state) {
json: json, // safe for JSON (float32array mess)
group: this.group.id,
track: this.track,
geo: json ? Array.from(geo) : geo
geo: json ? Array.from(geo) : geo,
meta: this.meta,
anno: this.anno,
};
return coded;
};
@ -234,8 +236,12 @@ registerDecoder(TYPE.WIDGET, function(v, state) {
const id = v.id,
group = v.group || id,
track = v.track || undefined,
widget = newWidget(id, Widget.Groups.forid(group));
widget = newWidget(id, Widget.Groups.forid(group)),
meta= v.meta || widget.meta,
anno = v.anno || widget.anno;
widget.meta = meta;
widget.anno = anno;
widget.loadVertices(v.json ? v.geo.toFloat32() : v.geo);
widget.saved = Date.now();
if (track && track.pos) {

View file

@ -270,9 +270,7 @@ export const conf = {
firstLayerFanSpeed: 0,
firstLayerFillRate: 35,
firstLayerFlatten: 0,
firstLayerLineMult: 1.0,
firstLayerNozzleTemp: 0,
firstLayerPrintMult: 1.0,
firstLayerRate: 30,
firstLayerYOffset: 0,
firstSliceHeight: 0.25,
@ -293,6 +291,7 @@ export const conf = {
outputInvertY: false,
outputLayerRetract: false,
outputLoops: 0,
outputMaxFlowrate: 15,
outputMinLayerTime: 10,
outputMinSpeed: 5.0,
outputPurgeTower: 0,
@ -302,7 +301,7 @@ export const conf = {
outputRetractDwell: 20,
outputRetractSpeed: 40,
outputRetractWipe: 0,
outputScarfLength: 5,
outputScarfLength: 0,
outputSeekrate: 80,
outputShellMult: 1.25,
outputShortPoly: 0.0,
@ -319,7 +318,6 @@ export const conf = {
sliceFillAngle: 45,
sliceFillGrow: 0,
sliceFillOverlap: 0.35,
sliceFillRate: 0,
sliceFillRepeat: 1,
sliceFillSparse: 0.25,
sliceFillType: "hex",
@ -333,7 +331,6 @@ export const conf = {
sliceShellOrder: "in-out",
sliceShells: 3,
sliceSolidMinArea: 1,
sliceSolidRate: 0,
sliceSupportAngle: 50,
sliceSupportArea: 0.1,
sliceSupportDensity: 0.1,
@ -500,6 +497,8 @@ export const conf = {
camLatheSpeed: 500,
camLatheSpindle: 1000,
camLatheTool: 1000,
camLatheOffStart: 0,
camLatheOffEnd: 0,
camLevelDown: 0,
camLevelInset: 0.5,
camLevelOver: 0.75,

View file

@ -5,31 +5,31 @@
import { CSG } from '../../../geo/csg.js';
import { Tool } from './tool.js';
let nextMeshID = 1;
let stock, center, rez;
let path, pathIndex, tool, tools, last;
let stockZ;
let stockIndexMsg = false;
let stockSlices;
let stockIndex;
let startTime;
let toolID = -1;
let toolMesh;
let toolRadius;
let toolUpdateMsg;
let animateClear = false;
let animating = false;
let moveDist = 0;
let renderDist = 0;
let renderDone = false;
let renderPause = 10;
let renderSteps = 0;
let renderSpeed = 0;
let indexCount = 0;
let updates = 0;
let nextMeshID = 1,
stock,
path,
pathIndex,
tool,
tools,
last,
stockZ,
stockIndexMsg = false,
stockSlices,
stockIndex,
startTime,
toolID = -1,
toolMesh,
toolRadius,
toolUpdateMsg,
showStock = false,
animateClear = false,
animating = false,
renderDone = false,
renderSteps = 0,
renderSpeed = 0,
indexCount = 0,
updates = 0,
maxR;
export function init(worker) {
const { dispatch } = worker;
@ -45,8 +45,6 @@ export function init(worker) {
path = print.output.flat();
tools = settings.tools;
stock = settings.stock;
rez = 1 / Math.sqrt(density / (stock.x * stock.y));
tool = null;
last = null;
animating = false;
@ -56,11 +54,14 @@ export function init(worker) {
startTime = 0;
updates = 0;
stockZ = isIndexed ? 0 : stock.z;
maxR = Math.hypot(stock.y, stock.z);
stockSlices = [];
const { x, y, z } = stock;
const sliceCount = parseInt(settings.controller.animesh || 2000) / 100;
const sliceWidth = stock.x / sliceCount;
if (showStock)
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);
@ -74,7 +75,6 @@ export function init(worker) {
};
dispatch.animate2 = function (data, send) {
renderPause = data.pause || renderPause;
renderSpeed = data.speed || 0;
if (animating) {
return send.done();
@ -126,7 +126,6 @@ function renderPath(send) {
return;
}
pathIndex++;
// console.log(next.point.z);
if (next.tool && (!tool || tool.getID() !== next.tool.getID())) {
// on real tool change, go to safe Z first
@ -143,44 +142,50 @@ function renderPath(send) {
}
const id = toolID;
const rezstep = rez * 2;
const rezstep = Math.min(tool.maxDiameter(), 1) / 4;
// console.log({ rezstep });
if (last) {
// console.log(last.point, next.point);
const lp = last.point, np = next.point;
last = next;
// render move line on client
send.data({ line: [ lp, np ]});
// dwell ops have no point
if (!np || !lp) {
return renderPath(send);
}
let dx = np.x - lp.x,
dy = np.y - lp.y,
dz = np.z - lp.z,
da = Math.abs((np.a || 0) - (lp.a || 0)),
dr = (da / 360) * (2 * Math.PI * Math.max(np.z, lp.z)),
dist = Math.sqrt(dx * dx + dy * dy + dz * dz + dr * dr);
moveDist += dist;
// skip moves that are less than grid resolution
if (moveDist < rezstep) {
// console.log('skip', moveDist, rezstep, next);
renderPath(send);
return;
}
da = (np.a ?? 0) - (lp.a ?? 0), // delta rotation degrees
AD = maxR * 2, // arc diameter @ max z
dr = (Math.abs(da) / 360) * AD, // delta rotation arc
dist = Math.max(Math.hypot(dx, dy, dz), dr); // motion distance
// console.log({ dz, da, AD, dr, dist });
// sub-divide long moves for animation and better boolean ops
const md = Math.max(Math.abs(dx), Math.abs(dy), Math.abs(dz), dr);
const st = Math.ceil(md / rezstep);
const mx = dx / st, my = dy / st, mz = dz / st;
// const sd = Math.sqrt(mx*mx + my*my + mz*mz + dr*dr);
const sd = Math.sqrt(mx * mx + my * my + Math.min(1, mz * mz) + dr * dr);
const st = Math.ceil(md / rezstep); // steps
const mx = dx / st, my = dy / st, mz = dz / st, ma = da / st;
const sd = Math.sqrt(mx * mx + my * my + mz * mz + ma * ma);
const sp = next.speed;
const moves = [];
for (let i = 0, x = lp.x, y = lp.y, z = lp.z; i < st; i++) {
moves.push({ x, y, z, a: lp.a, md: sd, dx, dy, dz });
for (let i = 0, x = lp.x, y = lp.y, z = lp.z, a = lp.a; i < st; i++) {
moves.push({ x, y, z, a, md: sd, dx, dy, dz, sp });
x += mx;
y += my;
z += mz;
a += ma;
}
moveDist = 0;
moves.push({ ...next.point, md: sd });
// console.log({ dist: md, steps: st, speed: next.speed, moves });
// push last point
moves.push({ ...next.point, md: sd, sp });
// console.log(...moves);
renderMoves(id, moves, send);
} else {
last = next;
@ -197,7 +202,7 @@ function renderMoves(id, moves, send, seed = 0) {
if (!pos) {
throw `no pos @ ${index} of ${moves.length}`;
}
const { dx, dy, dz } = pos;
const { dx, dy, dz, sp, md } = pos;
toolMove(pos);
// console.log('renderMoves', {id, moves, seed});
let subs = 0;
@ -208,15 +213,14 @@ function renderMoves(id, moves, send, seed = 0) {
subs++;
}
}
// console.log({ index, subs });
renderDist += pos.md;
// pause renderer at specified offsets
if (renderSpeed && renderDist >= renderSpeed) {
renderDist = 0;
if (renderSpeed) {
let time = sp ? (md / sp) * 60000 : 10;
// console.log({ time, ...pos });
renderUpdate(send);
setTimeout(() => {
renderMoves(id, moves, send, index + 1);
}, renderPause);
}, time / renderSpeed);
return;
}
}

View file

@ -10,24 +10,26 @@ const { client } = api;
let meshes = {},
button = {},
label = {},
lineTracker,
material,
speedPauses = [0, 0, 0, 0, 0],
speedValues = [1, 2, 4, 8, 32],
speedNames = ["1x", "2x", "4x", "8x", "!!"],
speedMax = speedValues.length - 1,
speedIndex = 0,
speed,
origin,
color = 0;
color = 0,
A2R = Math.PI / 180;
export function animate_clear2(api) {
let { anim } = api.ui;
client.animate_cleanup2();
lineTracker?.clear();
Object.keys(meshes).forEach(id => deleteMesh(id));
api.widgets.setAxisIndex(0);
api.uc.setVisible(anim.laba, true);
api.uc.setVisible(anim.vala, true);
anim.vala.value = "0.0";
client.animate_cleanup2();
}
export function animate2(api, delay) {
@ -86,8 +88,9 @@ Object.assign(client, {
},
animate_setup2(settings, ondone) {
initPathMesh();
color = settings.controller.dark ? 0x888888 : 0;
material = new THREE.MeshPhongMaterial({
material = material ?? new THREE.MeshPhongMaterial({
flatShading: true,
transparent: false,
opacity: 0.5,
@ -102,6 +105,66 @@ Object.assign(client, {
}
});
function initPathMesh() {
if (lineTracker) {
return lineTracker.clear();
}
let count = 0,
max = 10000,
rot = new THREE.Euler(0,0,0),
geo = new THREE.BufferGeometry(),
mat = new THREE.LineBasicMaterial({ color: 0xffff00 }),
pos = new Float32Array(max * 6),
lines = new THREE.LineSegments(geo, mat),
vec = new THREE.Vector3(0,0,0),
ang = new THREE.Vector3(1,0,0);
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
let track = lineTracker = {
xro(p) {
vec.set(p.x, p.y, p.z);
vec.applyAxisAngle(ang, p.a * A2R);
},
add(p1, p2) {
let ind = count * 6;
track.xro(p1);
pos[ind+0] = vec.x;
pos[ind+1] = vec.y;
pos[ind+2] = vec.z;
track.xro(p2);
pos[ind+3] = vec.x;
pos[ind+4] = vec.y;
pos[ind+5] = vec.z;
count++;
if (count >= max) {
let opos = pos;
max += 10000;
pos = new Float32Array(max * 6),
pos.set(opos);
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
}
geo.setDrawRange(0, count * 2);
geo.attributes.position.needsUpdate = true;
track.visible(true);
},
clear() {
count = 0;
geo.setDrawRange(0,0);
track.rotate(0);
track.visible(false);
},
rotate(angle) {
rot.x = angle * A2R;
lines.setRotationFromEuler(rot);
},
visible(bool) {
if (bool !== lines.visible) {
lines.visible = bool;
}
}
}
space.world.add(lines);
}
function meshAdd(id, ind, pos, ilen, plen) {
const geo = new THREE.BufferGeometry();
const pa = plen ? pos.subarray(0, plen * 3) : pos;
@ -132,6 +195,7 @@ function meshUpdate(id, ind, pos, ilen, plen) {
function deleteMesh(id) {
space.world.remove(meshes[id]);
meshes[id].geometry.dispose();
delete meshes[id];
}
@ -165,8 +229,7 @@ function play(opts) {
}
client.animate2({
speed,
steps: steps || Infinity,
pause: speedPauses[speedIndex]
steps: steps || Infinity
}, handleUpdate);
}
@ -177,8 +240,7 @@ function fast(opts) {
button.pause.style.display = '';
client.animate2({
speed,
steps: steps || Infinity,
pause: speedPauses[speedIndex]
steps: steps || Infinity
}, handleUpdate);
}
@ -216,6 +278,7 @@ function handleUpdate(data) {
if (data.stock_index !== undefined) {
api.widgets.setAxisIndex(data.stock_index);
label.a.value = -data.stock_index.toFixed(1);
lineTracker.rotate(-data.stock_index);
}
if (data.mesh_index) {
const { id, index } = data.mesh_index;
@ -229,9 +292,12 @@ function handleUpdate(data) {
const { id, ind, pos, ilen, plen } = data.mesh_update;
meshUpdate(id, ind, pos, ilen, plen);
}
if (data && data.progress) {
if (data.progress) {
label.progress.value = (data.progress * 100).toFixed(1);
}
if (data.line) {
lineTracker.add(...data.line);
}
}
function updateSpeed(inc = 0) {

View file

@ -9,6 +9,7 @@ import { selectHoles } from './cl-hole.js';
import { selectHelical } from './cl-helical.js';
import { surfaceAdd } from './cl-surface.js';
import { traceAdd } from './cl-trace.js';
import { opRender } from './client.js';
const { MODES } = api.const;
const { uc: UC } = api;
@ -349,7 +350,9 @@ export function createPopOps() {
tolerance: 'camTolerance',
filter: 'camContourFilter',
leave: 'camContourLeave',
linear: 'camLatheLinear'
linear: 'camLatheLinear',
offStart: 'camLatheOffStart',
offEnd: 'camLatheOffEnd'
}).inputs = {
tool: UC.newSelect(LANG.cc_tool, {}, "tools"),
// axis: UC.newSelect(LANG.cd_axis, {}, "xyaxis"),
@ -360,6 +363,9 @@ export function createPopOps() {
step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: UC.toFloat, bound: UC.bound(0.01, 100.0) }),
angle: UC.newInput(LANG.cc_sang_s, { title: LANG.cc_sang_l, convert: UC.toFloat, bound: UC.bound(0.01, 180.0) }),
sep: UC.newBlank({ class: "pop-sep" }),
offStart: UC.newInput(LANG.ci_laso_s, { title: LANG.ci_laso_l, convert: UC.toFloat}),
offEnd: UC.newInput(LANG.ci_laeo_s, { title: LANG.ci_laeo_l, convert: UC.toFloat}),
sep: UC.newBlank({ class: "pop-sep" }),
tolerance: UC.newInput(LANG.ou_toll_s, { title: LANG.ou_toll_l, convert: UC.toFloat, bound: UC.bound(0, 10.0), units: true, round: 4 }),
leave: UC.newInput(LANG.cf_leav_s, { title: LANG.cf_leav_l, convert: UC.toFloat, bound: UC.bound(0, 100) }),
sep: UC.newBlank({ class: "pop-sep" }),

View file

@ -29,6 +29,7 @@ const { widgets: WIDGETS } = api;
class Client {
animVer = 0;
camStock;
showStock = true;
current;
currentIndex;
flipping;
@ -640,7 +641,8 @@ export function init() {
});
api.event.on("cam.stock.toggle", (bool) => {
env.camStock && (env.camStock.visible = bool ?? !env.camStock.visible);
env.showStock = bool ?? !env.showStock;
updateStock();
});
api.event.on("boolean.click", api.platform.update_bounds);

View file

@ -257,17 +257,18 @@ export function cam_export(print, online) {
points--;
return;
}
let gn;
if (out.emit >= 0 && out.emit <= 3) gn = `G${out.emit}`;
let speed = out.speed,
arc = out.emit == 2 || out.emit == 3,
center = out.center,
nl = (compact_output && lastGn === gn) ? [] : [gn],
dx = opt.dx || newpos.x - pos.x,
let dx = opt.dx || newpos.x - pos.x,
dy = opt.dy || newpos.y - pos.y,
dz = opt.dz || newpos.z - pos.z,
da = newpos.a != pos.a,
maxf = dz ? maxZd : maxXYd,
gv = (dz >= 0 && out.emit >= 0 && out.emit <= 3) ? out.emit : 1,
gn = `G${gv}`,
speed = out.speed,
arc = out.emit == 2 || out.emit == 3,
center = out.center,
nl = (compact_output && lastGn === gn) ? [] : [gn],
maxf = (dz < 0 ? maxZd : maxXYd) || speed,
feed = Math.min(speed || maxf, maxf),
dist = Math.sqrt(dx * dx + dy * dy + dz * dz),
newFeed = feed && feed !== pos.f;

View file

@ -412,7 +412,7 @@ export function prepEach(widget, settings, print, firstPoint, update) {
}
// convert short planar moves to cuts in some cases
if (hasDelta && !isRough && !isArc && isMove && deltaXY <= moveLen && deltaZ <= 0 && !lasering) {
if (hasDelta && !isArc && isMove && deltaXY <= moveLen && deltaZ <= 0 && !lasering) {
let iscontour = tolerance > 0;
let isflat = absDeltaZ < 0.001;
// restrict this to contouring

View file

@ -38,9 +38,8 @@ export class Topo {
x: axis === "x",
y: axis === "y"
},
zMin = min.z + 0.0001,
tolerance = op.tolerance,
resolution = tolerance ? tolerance : 1 / Math.sqrt(density / (span.x * span.y)),
resolution = (tolerance ? tolerance : 1 / Math.sqrt(density / (span.x * span.y))).round(5),
step = this.step = (tool.traceOffset() * 2) * op.step,
angle = this.angle = op.angle || 1;
@ -52,17 +51,20 @@ export class Topo {
console.log(widget.id, 'topo4 auto tolerance', resolution.round(4));
}
this.zBottom = state.zBottom;
this.zBottom = state.zBottom ?? 0;
this.resolution = resolution;
this.vertices = widget.getGeoVertices({ unroll: true, translate: true });
this.tabverts = widget.getTabVertices();
this.tool = tool.generateProfile(resolution).profile;
this.maxo = tool.profileDim.maxo * resolution;
this.diam = tool.fluteDiameter();
this.unit = tool.unitScale();
this.zoff = widget.track.top || 0;
this.leave = op.leave || 0;
this.linear = op.linear || false;
this.lineColor = state.settings.controller.dark ? 0xffff00 : 0x555500;
this.offStart = op.offStart ?? 0;
this.offEnd = op.offEnd ?? 0;
onupdate(0, "lathe");
@ -86,8 +88,8 @@ export class Topo {
}
async slice(onupdate) {
const { vertices, resolution, tabverts, zoff } = this;
const { vertices, resolution, tabverts, zoff, offStart, offEnd, tool, unit } = this;
const { minions } = self.kiri_worker;
const range = this.range = { min: Infinity, max: -Infinity };
const box = this.box = new THREE.Box2();
@ -101,9 +103,10 @@ export class Topo {
range.max = Math.max(range.max, x);
}
// add tool radius to slice min/max range ot fully carve part
range.min -= this.diam / 2;
range.max += this.diam / 2;
// add tool diameter to slice min/max range to fully carve part
range.min += offStart * unit;
range.max -= offEnd * unit;
range.max += resolution * 2;
// merge in tab vertices here so they don't affect slice range / dimensions
for (let i = 0, l = tabverts.length; i < l; i += 3) {
@ -116,24 +119,31 @@ export class Topo {
// re-create shared vertex array for workers
this.vertices = [].appendAll(vertices).appendAll(tabverts).toFloat32().toShared();
const zSpan = range.max - range.min;
const shards = Math.ceil(Math.min(25, vertices.length / 27000));
const step = (range.max - range.min) / shards;
const totalSteps = Math.ceil(zSpan / resolution);
const stardSteps = Math.ceil(totalSteps / shards);
const stepWidth = stardSteps * resolution;
let index = 0;
let slices = this.slices = [];
let slice = { min: range.min, max: range.min + step, index };
for (let z = range.min; z < range.max; z += resolution) {
if (z > slice.max) {
slices.push(slice);
slice = { min: z, max: z + step, index };
}
index++;
for (let i=0; i<shards; i++) {
let s;
slices.push(s = {
min: range.min + i * stepWidth,
max: Math.min(range.max, range.min + (i+1) * stepWidth),
index: i * stardSteps
});
}
slices.push(slice);
// console.log({ shards, range, step, slices });
const { minions } = self.kiri_worker;
// console.log({
// shards,
// step: stepWidth,
// range,
// resolution,
// slices: slices.slice(),
// minions
// });
if (minions?.running > 1) {
return await this.sliceMinions(onupdate);
} else {
@ -217,10 +227,10 @@ export class Topo {
const tlen = tool.length;
const slen = slices.length;
const sinc = Math.max(1, Math.ceil(step / resolution));
const heights = [];
// cull slice lines to only the ones in range (5x faster)
// console.log({ tlen, slen, sinc, slices: slices.map(s => s.z) });
// cull slice lines to only the ones in range (~5x faster)
const oslices = [];
for (let slice of slices) {
const lines = slice.lines;
@ -244,7 +254,9 @@ export class Topo {
// iterate over all slices (real x = z)
// find max real z using z ray intersect from tool point to slice lines + offset
let lz = 0;
for (let si = 0; si < slen; si += sinc) {
for (let sz = 0; ; sz += step) {
let si = Math.ceil(sz / resolution);
if (si >= oslices.length) break;
const rx = oslices[si].z;
let mz = -Infinity;
// iterate over tool offsets
@ -281,12 +293,12 @@ export class Topo {
}
if (mz === -Infinity && xo === 0 && yo === 0) {
// tool tip is off the model
continue;
// continue;
}
}
if (mz === -Infinity) {
mz = zBottom; // Math.min(0, lz)
} else if (zBottom && mz < zBottom) {
mz = zBottom;
} else if (mz < zBottom) {
mz = zBottom;
}
heights.push(rx, 0, lz = mz);

View file

@ -34,14 +34,11 @@ export function init() {
"sliceFillType": LANG.fi_type,
"sliceFillWidth": LANG.fi_wdth_s,
"sliceFillSparse": LANG.fi_pcnt_s,
"sliceFillRate": LANG.ou_feed_s,
"sliceFillGrow": LANG.fi_grow_s,
"sliceSolidRate": LANG.ou_fini_s,
"sliceSolidMinArea": LANG.ad_msol_s,
"sliceTopLayers": LANG.sl_ltop_s,
"sliceBottomLayers": LANG.sl_lbot_s,
"firstLayerRate": LANG.fl_rate_s,
"firstLayerPrintMult": LANG.fl_mult_s,
"firstLayerFanSpeed": LANG.ou_fans_s,
// prepare
"sliceShellOrder": LANG.sl_ordr_s,

View file

@ -9,6 +9,7 @@ import { fdm_export } from './export.js';
// fil = filament diameter
// slice = slice height
function extrudePerMM(noz, fil, slice) {
// (ratio of nozzle to filament) * (ratio of slice to nozzle)
return ((Math.PI * util.sqr(noz / 2)) / (Math.PI * util.sqr(fil / 2))) * (slice / noz);
};

View file

@ -26,7 +26,6 @@ export function fdm_export(print, online, ondone, ondebug) {
{ extrudeAbs } = device,
{ exportThumb } = controller,
{ extrudeMM, extrudePerMM } = FDM,
extused = Object.keys(print.extruders).map(v => parseInt(v)),
timeFactor = (device.gcodeTime || 1) * 1.5,
decimals = config.gcode_decimals || 4,
zMoveMax = device.deviceZMax || 0,
@ -49,7 +48,6 @@ export function fdm_export(print, online, ondone, ondebug) {
last = null,
zpos = 0,
bmax = 0,
blastz = 0,
belt_add_y = (process.firstLayerYOffset || 0) - (belty || 0),
oloops = process.outputLoops || 0,
zhop = process.zHopDistance || 0, // range
@ -58,10 +56,8 @@ export function fdm_export(print, online, ondone, ondebug) {
retDist = process.outputRetractDist || 0, // range
retSpeed = process.outputRetractSpeed * 60 || 1, // range
retDwell = process.outputRetractDwell || 0, // range
timeDwell = retDwell / 1000,
scarfZ = process.outputScarfLength ? true : false,
arcDist = isBelt || scarfZ ? 0 : (process.arcTolerance || 0),
arcMin = 1,
arcRes = 20,
arcDev = 0.5,
arcMax = 40,
@ -85,7 +81,6 @@ export function fdm_export(print, online, ondone, ondebug) {
fanSpeedSave = undefined,
fanSpeedBase = 0,
fanSpeed = 0,
lastType = undefined,
lastNozzleTemp = nozzleTemp,
lastBedTemp = bedTemp,
lastFanSpeed = fanSpeed,
@ -169,7 +164,6 @@ export function fdm_export(print, online, ondone, ondebug) {
retDist = params.outputRetractDist || 0; // range
retSpeed = params.outputRetractSpeed * 60 || 1; // range
retDwell = params.outputRetractDwell || 0; // range
timeDwell = retDwell / 1000;
nozzleTemp = layer === 0 ?
params.firstLayerNozzleTemp || params.outputTemp :
params.outputTemp || params.firstLayerNozzleTemp;
@ -470,7 +464,7 @@ export function fdm_export(print, online, ondone, ondebug) {
if (isBelt) {
let zheight = path ? path.height || 0 : 0;
epos.x = originCenter ? -pos.x : device.bedWidth - pos.x;
epos.z = blastz = pos.z * icos;
epos.z = pos.z * icos;
epos.y = -pos.y + epos.z * bcos + belt_add_y;
lout = epos;
}
@ -635,7 +629,7 @@ export function fdm_export(print, online, ondone, ondebug) {
// emit comment on output type chage
if (last && out.type !== last.type) {
lastType = subst.feature = out.type;
subst.feature = out.type;
if (gcodeFeature && gcodeFeature.length) {
appendAllSub(gcodeFeature);
} else {
@ -752,7 +746,6 @@ export function fdm_export(print, online, ondone, ondebug) {
}
// if new point is off the arc
// if (deem || depm || desp || dc > arcDist || cc.r < arcMin || cc.r > arcMax || dist > cc.r) {
if (deem || depm || desp || dc * arcQ.center.length / arcQ.rSum > arcDist || dist > cc.r || cc.r > arcMax || radFault || !arcValid()) {
// let debug = [deem, depm, desp, dc * arcQ.center.length / arcQ.rSum > arcDist, dist > cc.r, cc.r > arcMax, radFault];
if (arcQ.length === 4) {

View file

@ -79,6 +79,7 @@ export function menu() {
_____: newGroup(LANG.sw_menu, $('fdm-walls'), { modes:FDM, driven, hideable, separator, group:"fdm-walls" }),
sliceShells: newInput(LANG.sl_shel_s, { title:LANG.sl_shel_l, convert:toFloat }),
sliceLineWidth: newInput(LANG.sl_line_s, { title:LANG.sl_line_l, convert:toFloat, bound:bound(0,5) }),
sliceFillOverlap: newInput(LANG.fi_over_s, {title:LANG.fi_over_l, convert:toFloat, bound:bound(0.0,1.0)}),
separator: newBlank({ class:"set-sep", driven }),
sliceShellOrder: newSelect(LANG.sl_ordr_s, { title:LANG.sl_ordr_l}, "shell"),
sliceDetectThin: newSelect(LANG.ad_thin_s, { title: LANG.ad_thin_l, action: thinWallSave }, "thin"),
@ -86,19 +87,17 @@ export function menu() {
sliceZInterleave: newBoolean(LANG.ad_zint_s, onBooleanClick, {title:LANG.ad_zint_l, show:zIntShow}),
_____: newGroup(LANG.fs_menu, $('fdm-solid'), { modes:FDM, driven, hideable, separator, group:"fdm-solid" }),
sliceFillAngle: newInput(LANG.fi_angl_s, {title:LANG.fi_angl_l, convert:toFloat}),
sliceFillOverlap: newInput(LANG.fi_over_s, {title:LANG.fi_over_l, convert:toFloat, bound:bound(0.0,2.0)}),
sliceFillGrow: newInput(LANG.fi_grow_s, {title:LANG.fi_grow_l, convert:toFloat}),
sliceSolidMinArea: newInput(LANG.ad_msol_s, {title:LANG.ad_msol_l, convert:toFloat}),
_____: newGroup(LANG.fi_menu, $('fdm-fill'), { modes:FDM, driven, hideable, separator, group:"fdm-fill" }),
sliceFillType: newSelect(LANG.fi_type, {trigger}, "infill"),
sliceFillSparse: newInput(LANG.fi_pcnt_s, {title:LANG.fi_pcnt_l, convert:toFloat, bound:bound(0.0,1.0), show:hasInfill}),
sliceFillRepeat: newInput(LANG.fi_rept_s, {title:LANG.fi_rept_l, convert:toInt, bound:bound(1,10), show:fillIsLinear}),
separator: newBlank({ class:"set-sep", driven }),
sliceFillRate: newInput(LANG.ou_feed_s, {title:LANG.ou_feed_l, convert:toInt, bound:bound(0,500)}),
sliceSolidRate: newInput(LANG.ou_fini_s, {title:LANG.ou_fini_l, convert:toInt, bound:bound(0,500)}),
_____: newGroup(LANG.fh_menu, $('fdm-heat'), { modes:FDM, driven, hideable, separator, group:"fdm-heat" }),
outputTemp: newInput(LANG.ou_nozl_s, {title:LANG.ou_nozl_l, convert:toInt}),
outputBedTemp: newInput(LANG.ou_bedd_s, {title:LANG.ou_bedd_l, convert:toInt}),
separator: newBlank({ class:"set-sep", driven }),
outputDraftShield: newBoolean(LANG.fr_draf_s, onBooleanClick, {title:LANG.fr_draf_l, trigger, show:() => isNotBelt()}),
_____: newGroup(LANG.fc_menu, $('fdm-cool'), { modes:FDM, driven, hideable, separator, group:"fdm-cool" }),
outputFanLayer: newInput(LANG.ou_fanl_s, { title:LANG.ou_fanl_l, convert:toInt, bound:bound(0,255) }),
outputFanSpeed: newInput(LANG.ou_fans_s, {title:LANG.ou_fans_l, convert:toInt, bound:bound(0,255)}),
@ -137,9 +136,6 @@ export function menu() {
firstLayerFlatten: newInput(LANG.fl_flat_s, {title:LANG.fl_flat_l, convert:toFloat, show:isBelt}),
firstLayerRate: newInput(LANG.fl_rate_s, {title:LANG.fl_rate_l, convert:toFloat}),
firstLayerFillRate: newInput(LANG.fl_frat_s, {title:LANG.fl_frat_l, convert:toFloat, show:isNotBelt}),
separator: newBlank({ class:"set-sep", driven, show:isNotBelt }),
firstLayerLineMult: newInput(LANG.fl_sfac_s, {title:LANG.fl_sfac_l, convert:toFloat, bound:bound(0.5,2), show:isNotBelt}),
firstLayerPrintMult: newInput(LANG.fl_mult_s, {title:LANG.fl_mult_l, convert:toFloat}),
separator: newBlank({ class:"set-sep", driven, show:isBelt }),
firstLayerBrim: newInput(LANG.fl_brim_s, {title:LANG.fl_brim_l, convert:toInt, show:isBelt}),
firstLayerBrimIn: newInput(LANG.fl_brin_s, {title:LANG.fl_brin_l, convert:toInt, show:isBelt}),
@ -154,7 +150,6 @@ export function menu() {
outputRaftSpacing: newInput(LANG.fr_spac_s, {title:LANG.fr_spac_l, convert:toFloat, bound:bound(0.0,3.0), show: () => ui.outputRaft.checked && isNotBelt() }),
separator: newBlank({ class:"set-sep", driven, show:isNotBelt }),
outputRaft: newBoolean(LANG.fr_nabl_s, onBooleanClick, {title:LANG.fr_nabl_l, trigger, show:() => isNotBelt()}),
outputDraftShield: newBoolean(LANG.fr_draf_s, onBooleanClick, {title:LANG.fr_draf_l, trigger, show:() => isNotBelt()}),
_____: newGroup(LANG.ou_menu, $('fdm-output'), { modes:FDM, driven, hideable, separator, group:"fdm-out" }),
outputFeedrate: newInput(LANG.ou_feed_s, {title:LANG.ou_feed_l, convert:toInt}),
outputFinishrate: newInput(LANG.ou_fini_s, {title:LANG.ou_fini_l, convert:toInt}),
@ -177,6 +172,7 @@ export function menu() {
arcTolerance: newInput(LANG.ad_arct_s, {title:LANG.ad_arct_l, convert:toFloat, bound:bound(0,1.0), show:() => { return isNotBelt() }}),
outputRetractDwell: newInput(LANG.ad_rdwl_s, {title:LANG.ad_rdwl_l, convert:toInt}),
outputMinSpeed: newInput(LANG.ad_mins_s, {title:LANG.ad_mins_l, convert:toFloat, bound:bound(1,200)}),
outputMaxFlowrate: newInput(LANG.ad_maxf_s, {title:LANG.ad_maxf_l, convert:toFloat, bound:bound(1,200)}),
outputPurgeTower: newInput(LANG.ad_purg_s, {title:LANG.ad_purg_l, convert:toInt, bound:bound(0,1000)}),
outputScarfLength: newInput(LANG.ad_scar_s, {title:LANG.ad_scar_l, convert:toFloat, bound:bound(0,1000)}),
outputShortPoly: newInput(LANG.ad_spol_s, {title:LANG.ad_spol_l, convert:toFloat, bound:bound(0,10000)}),

View file

@ -82,12 +82,9 @@ export function slicePost(data, options) {
let trg = shellFrac > 0.5 ? 1 : parts - 1;
sliceShells += rem >= trg ? 1 : 0;
}
const isFirst = index === 0;
const height = process.sliceHeight;
const spaceMult = isFirst ? process.firstLayerLineMult || 1 : 1;
const count = isSynth ? 1 : sliceShells;
const offset = shellOffset * spaceMult;
const fillOff = fillOffset * spaceMult;
const offset = shellOffset;
const fillOff = fillOffset;
const thinType = isSynth ? undefined : process.sliceDetectThin;
const nutops = [];
// co-locate shell processing with top generation in slicer

View file

@ -1,5 +1,6 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { FDM } from './driver-be.js';
import { base, util } from '../../../geo/base.js';
import { poly2polyEmit, tip2tipEmit } from '../../../geo/paths.js';
import { newBounds } from '../../../geo/bounds.js';
@ -682,7 +683,6 @@ export async function fdm_prepare(widgets, settings, update) {
for (let rec of layer) {
overate = rec.overate >= 0 ? rec.overate : overate;
let brate = params.firstLayerRate || firstLayerRate;
let bmult = params.firstLayerPrintMult || params.firstLayerPrintMult;
let point = rec.point;
let belty = rec.belty = -point.y + (point.z * belt.slope);
let lowrate = belty <= thresh ? brate : overate || brate;
@ -694,7 +694,6 @@ export async function fdm_prepare(widgets, settings, update) {
if (rec.emit && belty <= thresh && lastout && Math.abs(lastout.belty - belty) < 0.01) {
// apply base speed to segments touching belt
rec.speed = params.firstLayerRate || Math.min(rec.speed, lowrate);
rec.emit *= bmult;
rec.fan = params.firstLayerFanSpeed;
minx = Math.min(minx, point.x, lastout.point.x);
maxx = Math.max(maxx, point.x, lastout.point.x);
@ -794,10 +793,22 @@ export async function fdm_prepare(widgets, settings, update) {
}
};
/**
* Output and route Slice (shell,fill) paths for a single Slice
*
* @param {*} print Print output Object
* @param {*} slice current slice being Output
* @param {*} startPoint starting point for this layer
* @param {*} offset start point offset
* @param {*} output output prebuffer
* @param {*} opt options
*/
function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) {
const { settings } = print;
const { device } = settings;
const { bedWidth, bedDepth, bedRound } = device;
const { extrudePerMM } = FDM;
let preout = [],
process = opt.params || settings.process,
@ -806,6 +817,7 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) {
scarfLength = process.outputScarfLength || 0,
minLayerTime = process.outputMinLayerTime || 0,
nozzleSize = process.sliceLineWidth || device.extruders[extruder].extNozzle,
nozzles = device.extruders.map(e => e.extNozzle),
firstLayer = (opt.first || false) && !opt.support,
thinWall = nozzleSize * (opt.thinWall || 1.75),
retractDist = opt.retractOver || (nozzleSize * 5),
@ -817,17 +829,17 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) {
fanSpeed = process.outputFanSpeed,
firstShellSpeed = process.firstLayerRate,
firstFillSpeed = process.firstLayerFillRate,
firstPrintMult = process.firstLayerPrintMult,
finishSpeed = firstLayer ? firstShellSpeed : (opt.speed || process.outputFinishrate),
printSpeed = opt.speed || (firstLayer ? firstShellSpeed : process.outputFeedrate),
fillSpeed = opt.speed || opt.fillSpeed || (firstLayer ? firstFillSpeed || firstShellSpeed : process.outputFeedrate),
infillSpeed = process.sliceFillRate || opt.infillSpeed || fillSpeed || printSpeed,
infillSpeed = opt.infillSpeed || fillSpeed || printSpeed,
moveSpeed = process.outputSeekrate,
minSpeed = process.outputMinSpeed,
origin = startPoint.add(offset),
zhop = process.zHopDistance || 0,
zmax = opt.zmax,
antiBacklash = process.antiBacklash,
maxFlowRate = process.outputMaxFlowrate,
wipeDist = process.outputRetractWipe || 0,
isBelt = device.bedBelt,
bedOffset = originCenter ? {
@ -851,11 +863,6 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) {
shellOrder = -shellOrder;
}
// update fill speed when solids detected
if (slice.finishSolids) {
fillSpeed = process.sliceSolidRate || finishSpeed;
}
// on flats layers also slow down shell print speed
if (slice.isFlatsLayer) {
printSpeed = finishSpeed || printSpeed;
@ -867,13 +874,6 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) {
printSpeed /= 2;
}
// apply first layer extrusion multipliers
if (firstLayer) {
fillMult *= firstPrintMult;
shellMult *= firstPrintMult;
sparseMult *= firstPrintMult;
}
function retract() {
let array = preout.length ? preout : output;
if (array.length) {
@ -1595,16 +1595,34 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) {
if (!p.emit) continue;
let mms = p.speed ?? 1;
let dst = lp?.distTo2D(p.point) ?? 0;
lp = p.point;
if (dst) {
tt += (dst/mms);
td += dst;
p.dst = dst;
}
lp = p.point;
}
if (minLayerTime && tt < minLayerTime) {
let fact = tt / minLayerTime;
for (let p of preout) {
if (p.emit) p.speed = Math.max(p.speed * fact, minSpeed);
if (p.emit) {
p.speed = Math.max(p.speed * fact, minSpeed);
}
}
}
// apply speed damping when output exceeds volumetric flowrate cap
if (maxFlowRate) {
for (let p of preout) {
if (p.emit && p.dst) {
let vfr = nozzles[p.tool] * slice.height * p.speed;
if (vfr > maxFlowRate) {
let old = p.speed;
let damp = maxFlowRate / vfr;
p.speed = p.speed * damp;
// console.log({ vfr, damp, old, new: p.speed });
}
}
}
}
@ -1617,7 +1635,7 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) {
print.addPrintPoints(preout, output);
// bridge layer fan change
if (slice.isBridgeLayer) {
if (preout.length && slice.isBridgeLayer) {
preout[0].fan = Math.round(fanSpeed / 3);
preout.peek().fan = fanSpeed;
}

View file

@ -498,7 +498,6 @@ export function fdm_slice(settings, widget, onupdate, ondone) {
// just the top/bottom special solid layers or range defined solid layers
forSlices(0.15, 0.2, slice => {
let range = slice.params;
let spaceMult = slice.index === 0 ? process.firstLayerLineMult || 1 : 1;
let isBottom = slice.index < bottomLayers;
let isTop = topLayers && slice.index > slices.length - topLayers - 1;
let isTopBase = isTop && slice.index === slices.length - topLayers;
@ -506,7 +505,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) {
let isSolid = (isBottom || ((isTop || isDense) && !vaseMode)) && !isSynth;
let solidWidth = isSolid ? range.sliceFillWidth || 1 : 0;
if (solidWidth) {
let fillSpace = fillSpacing * spaceMult * solidWidth;
let fillSpace = fillSpacing * solidWidth;
doSolidLayerFill(slice, fillSpace, sliceFillAngle);
}
if (slice.index === slices.length - 1) {
@ -755,8 +754,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) {
let params = slice.params || process;
let first = slice.index === 0;
let solidWidth = params.sliceFillWidth || 1;
let spaceMult = first ? params.firstLayerLineMult || 1 : 1;
let fillSpace = fillSpacing * spaceMult * solidWidth;
let fillSpace = fillSpacing * solidWidth;
let solidMinArea = params.sliceSolidMinArea;
doSolidsFill(slice, fillSpace, sliceFillAngle, solidMinArea, promises);
sliceFillAngle = (sliceFillAngle + 90.0) % 360;

View file

@ -315,7 +315,6 @@ const funcs = self.minion = {
const topo4 = Object.assign(new Topo4(), cache.lathe);
const heights = topo4.lathePath(stmp, tool);
reply({ seq, heights });
}
};

View file

@ -3,7 +3,7 @@
const terms = {
COPYRIGHT: "Copyright (C) Stewart Allen <sa@grid.space> - All Rights Reserved",
LICENSE: "See the license.md file included with the source distribution",
VERSION: "4.3.3"
VERSION: "4.3.4"
};
export const beta = 0;

View file

@ -98,7 +98,6 @@ let WIN = self.window || {},
fontColor = '#333333',
fontScale = 1.4, // computed relative to grid size
rulerColor,
axisColor,
axesOn = true,
volumeOn = true,
viewControl,
@ -388,7 +387,6 @@ function setPlatformColor(color) {
function setFont(options) {
if (options.color) fontColor = options.color;
if (options.scale) fontScale = options.scale;
if (options.axisColor) axisColor = options.axisColor;
if (options.rulerColor) rulerColor = options.rulerColor;
updateRulers();
}

67
web/boot/index.html Normal file
View file

@ -0,0 +1,67 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Kiri:Moto (loading)</title>
<style>
body {
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
position: fixed;
align-items: center;
flex-direction: column;
justify-content: center;
font-family: sans-serif;
font-weight: bold;
}
</style>
<script type="module">
import { version } from '/lib/moto/license.js';
(async () => {
const reg = await navigator.serviceWorker.register('/boot/service.js', { scope: '/' });
await reg.update();
await navigator.serviceWorker.ready;
if (!navigator.serviceWorker.controller) {
console.log('No controller → reloading under SW control');
location.reload();
return;
}
// turn url params into key/value map
const map = Object.fromEntries(
(location.search || location.hash).slice(1).split('&')
.map(v => v.split('='))
.map(([k, v = true]) => [k, v])
);
if (map.debug) {
console.log({ map });
}
navigator.serviceWorker.controller.postMessage({
clear: map.clear,
disable: map.disable,
version: map.version || version
});
navigator.serviceWorker.addEventListener('message', e => {
console.log('[SM]', e.data);
setTimeout(() => {
if (map.mesh) {
location.replace('/mesh/index.html');
} else {
location.replace('/kiri/index.html');
}
}, map.delay || 10);
});
})();
</script>
</head>
<body>Booting Grid.Space</body>
</html>

203
web/boot/service.js Normal file
View file

@ -0,0 +1,203 @@
// --- configurable ---
const CACHE_VERSION = 'boot-001';
const BUNDLE_URL = '/boot/bundle.bin';
const VERSION_KEY = '/__version__';
let loaded = 0;
let mode = 'cached';
let currentVersion = null;
// --- logging ---
const log = (...a) => console.log('[SW]', ...a);
self.addEventListener('install', e => log('install'));
self.addEventListener('activate', e => log('activate'));
// self.addEventListener('fetch', e => log('fetch', e.request.url));
// --- lifecycle ---
self.skipWaiting();
self.addEventListener('activate', e => e.waitUntil(clients.claim()));
// --- mode control ---
self.addEventListener('message', e => {
const data = e.data || {};
const { clear, disable, version } = data;
if (clear) {
clearCache();
}
if (disable) {
mode = 'transparent';
broadcast('transparent mode');
unregister();
} else if (version !== undefined) {
mode = 'cached';
log('version', version);
ensureVersion(version);
}
});
// --- unregister current service worker ---
async function unregister() {
await self.registration.unregister();
}
// --- erase existing cache ---
async function clearCache() {
log('clearing cache');
await caches.delete(CACHE_VERSION);
}
// --- version check and preload ---
async function ensureVersion(incomingVersion) {
const cache = await caches.open(CACHE_VERSION);
let needPreload = false;
const existing = await cache.match(VERSION_KEY);
if (!existing) {
log('no version stored → preload');
needPreload = true;
} else {
const stored = await existing.text();
if (stored !== incomingVersion) {
log(`mismatch ${stored} ≠ ${incomingVersion} → preload`);
needPreload = true;
}
}
if (needPreload) {
await preloadBundle();
await cache.put(
VERSION_KEY,
new Response(String(incomingVersion), { headers: { 'Content-Type': 'text/plain' } })
);
broadcast(`preload added ${loaded} files`);
} else {
broadcast('preload cached');
}
}
// --- fetch with redirect handler ---
async function fetch_safe(req) {
const res = await fetch(req);
if (res.redirected) {
log({ was_redirected_cloning: res.url });
// clone into a fresh non-redirect response
const clone = res.clone();
return new Response(await clone.blob(), {
headers: clone.headers,
status: 200,
statusText: 'OK'
});
}
return res;
}
// --- fetch handler ---
self.addEventListener('fetch', e => {
const { request } = e;
const url = new URL(request.url);
if (request.method !== 'GET') return;
if (mode == 'transparent') {
e.respondWith(fetch_safe(e.request));
return;
}
if (url.pathname.endsWith("/")) {
return e.respondWith(redirectOr404(appendURL(url, 'index.html').pathname));
} else if (url.pathname.indexOf(".") < 0) {
return e.respondWith(redirectOr404(appendURL(url, '/index.html').pathname));
} else {
e.respondWith(fromCacheOrNetwork(request));
}
});
// --- helpers ---
function appendURL(url, append) {
return new URL(url.origin + url.pathname + append + url.search);
}
async function redirectOr404(path) {
const cache = await caches.open(CACHE_VERSION);
const hit = await cache.match(path, { ignoreSearch: true });
if (hit) {
return Response.redirect(path, 302);
} else {
return new Response('Not Found', { status: 404, statusText: 'Not Found' });
}
}
async function fromCacheOrNetwork(req) {
const cache = await caches.open(CACHE_VERSION);
const hit = await cache.match(req, { ignoreSearch: true });
if (hit) {
return hit;
}
const net = await fetch_safe(req);
cache.put(req, net.clone());
log(`put: ${net.url}`);
return net;
}
async function broadcast(message) {
const clientsList = await clients.matchAll({ type: 'window' });
for (const client of clientsList) {
client.postMessage(message);
}
}
const sec_headers = {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp'
};
// --- preload & unpack bundle ---
async function preloadBundle() {
loaded = 0;
const cache = await caches.open(CACHE_VERSION);
const res = await fetch(BUNDLE_URL, { cache: 'no-store' });
const buf = await res.arrayBuffer();
const files = await unpackBundle(buf);
await Promise.all(
Object.entries(files).map(([path, blob]) => {
const ext = path.split('.').pop();
const type =
ext === 'html' ? 'text/html' :
ext === 'js' ? 'application/javascript' :
ext === 'css' ? 'text/css' :
ext === 'json' ? 'application/json' :
ext === 'wasm' ? 'application/wasm' :
ext === 'svg' ? 'image/svg+xml' :
'application/octet-stream';
const headers = { 'Content-Type': type, ...sec_headers };
const resp = new Response(blob, { headers });
loaded++;
return cache.put('/' + path, resp);
})
);
}
// --- simple bundle format ---
// [count:uint32][entries...][file data...]
// entry: [nlen:uint16][name][offset:uint32][length:uint32]
async function unpackBundle(buf) {
const view = new DataView(buf);
let pos = 0;
const count = view.getUint32(pos, true); pos += 4;
const decoder = new TextDecoder();
const table = [];
for (let i = 0; i < count; i++) {
const nlen = view.getUint16(pos, true); pos += 2;
const name = decoder.decode(new Uint8Array(buf, pos, nlen)); pos += nlen;
const offset = view.getUint32(pos, true); pos += 4;
const len = view.getUint32(pos, true); pos += 4;
table.push({ name, offset, len });
}
const files = {};
for (const { name, offset, len } of table)
files[name] = buf.slice(offset, offset + len);
return files;
}

View file

@ -1378,7 +1378,7 @@ details[open] summary::after {
padding: 0 0 0 0;
}
#oplist {
max-height: 70vh;
max-height: calc(70vh - 200px);
min-height: 0;
overflow-y: auto;
padding-top: 1px;

View file

@ -356,7 +356,7 @@ self.lang['en-us'] = {
ad_thin_l: ["detect and fill gaps","between shell walls"],
// FDM SHELLS
sl_line_s: "shell width",
sl_line_s: "line width",
sl_line_l: ["extrusion width in millimeters","0 = nozzle diameter"],
sl_ordr_s: "shell order",
sl_ordr_l: ["output shell order","inside to outside","or outside to inside","affects surface quality"],
@ -369,8 +369,8 @@ self.lang['en-us'] = {
// FDM SOLID
fs_menu: "solid fill",
fi_over_s: "shell overlap",
fi_over_l: ["overlap with shell and other fill","for improved extrusion bonding","as fraction of nozzle diameter","0.0 - 2.0"],
fi_over_s: "fill overlap",
fi_over_l: ["overlap between shells and solid or sparse infill. can improve bonding to shells","as fraction of line width","0.0 - 1.0"],
fi_angl_s: "start angle",
fi_angl_l: ["starting angle in degrees","90 degrees added to ","each following layer","applies only to solid layers"],
fi_grow_s: "area expand",
@ -654,6 +654,10 @@ self.lang['en-us'] = {
ci_face_l: "select face to rotate facing up",
ci_line_s: "linear",
ci_line_l: "make linear passes along X then rotate Y",
ci_laso_s: "offset start",
ci_laso_l: ["x offet from left of part. negative values are outside of the part. in workspace units"],
ci_laeo_s: "offset end",
ci_laeo_l: ["x offet from right of part. negative values are outside of the part. in workspace units"],
// CNC LASER On/Off Operations (Carvera)
cl_powr_s: "power",
@ -675,10 +679,6 @@ self.lang['en-us'] = {
cl_maxz_s: "max z height",
cl_maxz_l: ["highest Z band position. when set to 0, uses the highest Z point of the workspace"],
// CNC CUTOUT TABS
ct_menu: "tabs",
ct_angl_s: "angle",
@ -864,10 +864,12 @@ self.lang['en-us'] = {
ad_rdwl_l: ["time between re-engaging","filament and movement","in milliseconds"],
ad_scst_s: "shell coast",
ad_scst_l: ["non-printing end","of perimeter shells","in millimeters"],
ad_msol_s: "min area",
ad_msol_l: ["minimum area (mm^2)","required to keep solid","must be > 0.1"],
ad_msol_s: "area filter",
ad_msol_l: ["minimum area (mm^2)","bridges and flats less than this are culled"],
ad_mins_s: "min speed",
ad_mins_l: ["minimum speed","for short segments"],
ad_maxf_s: "max flowrate",
ad_maxf_l: ["maximum volumetric flowrate supported by this print head and filament","in millimeters^3 / second"],
ad_scar_s: "scarf length",
ad_scar_l: ["ease up length for scarf joint. disables arc detection","0 = disabled"],
ad_spol_s: "short path",

View file

@ -1,5 +1,5 @@
{
"name": "Kiri:Moto 4.3.3",
"name": "Kiri:Moto 4.3.4",
"short_name": "Kiri:Moto",
"description": "Slicer for 3D printers, CNC mills, laser cutters and more",
"start_url": "/kiri/",