Merge branch 'rel-4.2' into dev-4.2-refactor-module

This commit is contained in:
Stewart Allen 2025-06-22 15:29:22 -04:00
commit ed722edff7
12 changed files with 256 additions and 130 deletions

23
.github/workflows/prettier-check.yml vendored Normal file
View file

@ -0,0 +1,23 @@
name: 'Docs Fomatting Check'
on:
push:
paths:
- './docs/**'
workflow_dispatch:
jobs:
format-check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: check format for docs
run:
bun docs-check

View file

@ -16,13 +16,13 @@ you can view some example of how to use the engine [here](https://grid.space/kir
## Methods
| Method | Description | Returns |
| --------------------------- | -------------------------------------------------------------------------------------------------------------- | ----------------- | ------- | ------- | -------- |
| --------------------------- | -------------------------------------------------------------------------------------------------------------- | ----------------- |
| `load(url)` | Loads an [STL](<https://en.wikipedia.org/wiki/STL_(file_format)>) file from a given URL and centers the model. | `Promise<Engine>` |
| `clear()` | Clears all objects from workspace. | `void` |
| `parse(data)` | Parses raw [STL](<https://en.wikipedia.org/wiki/STL_(file_format)>) data and loads it as a centered widget. | `Promise<Engine>` |
| `setListener(listener)` | Sets an event listener function to receive engine progress updates | `Engine` |
| `setRender(bool)` | Enables or disables rendering. | `Engine` |
| `setMode(mode)` | Sets the slicing mode where mode is "CAM" | "FDM" | "LASER" | "SLA" . | `Engine` |
| `setMode(mode)` | Sets the slicing mode where mode is "CAM" \| "FDM" \| "LASER" \| "SLA" . | `Engine` |
| `setDevice(device)` | Merges a custom device profile into the current settings. | `Engine` |
| `setProcess(process)` | Merges custom slicing process parameters. | `Engine` |
| `setController(controller)` | Sets the slicing controller settings and starts/stops the worker pool accordingly. | `Engine` |

View file

@ -10,7 +10,7 @@ id: mesh-tool
[Mesh:Tool](https://grid.space/mesh) is an intentionally simplified tool designed to address the most common cases of mesh editing and repair. It is under active development with new features daily or weekly. This means the docs can get out of date quickly.
Mesh:Tool is part of the [Grid.Apps GitHub Repo](https://github.com/GridSpace/grid-apps) and is Open Source under the [MIT License](https://en.wikipedia.org/wiki/MIT\_License). It runs entirely in browser, is not connected to the cloud, and does not collect or share any model data. [Desktop builds](https://github.com/GridSpace/grid-apps/releases) are available whiel start in the Slicer view. Use the central drop-menu to switch to Mesh:Tool.
Mesh:Tool is part of the [Grid.Apps GitHub Repo](https://github.com/GridSpace/grid-apps) and is Open Source under the [MIT License](https://en.wikipedia.org/wiki/MIT_License). It runs entirely in browser, is not connected to the cloud, and does not collect or share any model data. [Desktop builds](https://github.com/GridSpace/grid-apps/releases) are available whiel start in the Slicer view. Use the central drop-menu to switch to Mesh:Tool.
Browse the Mesh:Tool [video playlist](https://www.youtube.com/playlist?list=PLRoVgyRoWZps84Scj5wQ2LYK-wMu-7r0L) on YouTube

View file

@ -3,7 +3,6 @@ const sidebars = {
{
type: "doc",
id: "index",
},
{
type: "category",
@ -14,8 +13,8 @@ const sidebars = {
type: "category",
label: "Kiri Moto",
link: {
type: 'doc',
id: 'kiri-moto/index',
type: "doc",
id: "kiri-moto/index",
},
items: [
"kiri-moto/interface",
@ -31,17 +30,17 @@ const sidebars = {
{
type: "category",
label: "CAM Mode",
link:{
type: 'doc',
id: 'kiri-moto/CAM/index',
link: {
type: "doc",
id: "kiri-moto/CAM/index",
},
items:[
items: [
"kiri-moto/CAM/processOpts",
"kiri-moto/CAM/machines",
"kiri-moto/CAM/tools",
"kiri-moto/CAM/ops",
],
}
},
],
},
{
@ -52,39 +51,37 @@ const sidebars = {
type: "category",
label: "GridBot",
link: {
type: 'doc',
id: 'gridbot/index',
type: "doc",
id: "gridbot/index",
},
items: [
"gridbot/bom",
]
}
items: ["gridbot/bom"],
},
],
},
{
type: 'category',
label: 'Community',
type: "category",
label: "Community",
collapsed: false,
items: [
{
type: 'link',
label: 'Forums',
href: 'https://forum.grid.space',
type: "link",
label: "Forums",
href: "https://forum.grid.space",
},
{
type: 'link',
label: 'Discord',
href: 'https://discord.gg/suyCCgr',
type: "link",
label: "Discord",
href: "https://discord.gg/suyCCgr",
},
{
type: 'link',
label: 'GitHub',
href: 'https://github.com/GridSpace/grid-apps',
type: "link",
label: "GitHub",
href: "https://github.com/GridSpace/grid-apps",
},
{
type: 'link',
label: 'BlueSky',
href: 'https://bsky.app/profile/grid.space',
type: "link",
label: "BlueSky",
href: "https://bsky.app/profile/grid.space",
},
],
},
@ -104,4 +101,3 @@ const sidebars = {
};
module.exports = sidebars;

View file

@ -1,6 +1,6 @@
{
"name": "grid-apps",
"version": "4.1.9",
"version": "4.2.0",
"description": "grid.space 3d slicing & modeling tools",
"author": "Stewart Allen <sa@grid.space>",
"license": "MIT",
@ -50,6 +50,7 @@
"jszip": "^3.7.1",
"manifold-3d": "^3.1.1",
"moment": "^2.29.4",
"prettier": "^3.5.3",
"react-responsive-carousel": "^3.2.23",
"serve-static": "^1.14.1",
"three": "^0.174.0",
@ -99,7 +100,8 @@
"preinstall2": "npx webpack --config src2/webpack/webpack-three-esm.js",
"docs-dev": "docusaurus start",
"docs-build": "docusaurus build",
"docs-serve": "docusaurus serve"
"docs-serve": "docusaurus serve",
"docs-check": "prettier ./docs --check"
},
"main": "app-el.js",
"build": {

View file

@ -1002,6 +1002,23 @@ class Polygon {
return this.z !== undefined ? this.z : this.points[i || 0]?.z || 0;
}
minZ() {
return Math.min(...this.points.map(p => p.z));
}
avgZ() {
return [...this.points.map(p => p.z)].reduce((a,v) => a+v) / this.points.length;
}
/**
* @param {*} z value target
* @param {*} epsilon allowed Z variance
* @returns {boolean} true if all points Z within epsilon of value
*/
onZ(z, epsilon = 10e-4) {
return Math.max(...this.points.map(p => Math.abs(z - p.z))) < epsilon;
}
/**
*/
render(layer, color, recursive, open) {

View file

@ -1008,7 +1008,6 @@ CAM.init = function(kiri, api) {
CAM.traces((ids) => {
api.hide.alert(alert);
alert = api.show.alert("[esc] cancels trace editing");
kiri.api.widgets.opacity(0.8);
kiri.api.widgets.for(widget => {
if (ids.indexOf(widget.id) >= 0) {
unselectTraces(widget, true);
@ -1042,6 +1041,7 @@ CAM.init = function(kiri, api) {
});
// ensure appropriate traces are toggled matching current record
kiri.api.widgets.for(widget => {
widget.setVisualState({ opacity: 0.25 });
let areas = (poppedRec.areas[widget.id] || []);
let stack = widget.trace_stack;
stack.meshes.forEach(mesh => {
@ -1070,6 +1070,7 @@ CAM.init = function(kiri, api) {
api.feature.hover = false;
api.feature.hoverAdds = false;
kiri.api.widgets.for(widget => {
widget.restoreVisualState();
if (widget.trace_stack) {
widget.trace_stack.hide();
widget.adds.removeAll(widget.trace_stack.meshes);
@ -1096,17 +1097,15 @@ CAM.init = function(kiri, api) {
color.r = colorSave.r;
color.g = colorSave.g;
color.b = colorSave.b;
lastTrace.position.z -= 0.01;
}
lastTrace = null;
if (data.type === 'platform') {
lastTrace = null;
return;
}
if (!data.int.object.trace) {
return;
}
lastTrace = data.int.object;
lastTrace.position.z += 0.01;
if (lastTrace.selected) {
let event = data.event;
let target = event.target;
@ -1115,11 +1114,8 @@ CAM.init = function(kiri, api) {
}
let material = lastTrace.material[0] || lastTrace.material;
let color = material.color;
let {r, g, b} = color;
material.colorSave = {r, g, b};
color.r = 0;
color.g = 0;
color.b = 1;
material.colorSave = color.clone();
color.setHex(isDark() ? 0x0066ff : 0x0000ff);
};
func.traceHoverUp = function(int, ev) {
if (!int) return;
@ -1128,8 +1124,9 @@ CAM.init = function(kiri, api) {
if (ev.metaKey || ev.ctrlKey) {
let { selected } = object;
let { widget, poly } = object.trace;
let avgZ = poly.avgZ();
for (let add of widget.adds) {
if (add.trace && add.selected !== selected && add.trace.poly.getZ() === poly.getZ()) {
if (add.trace && add.selected !== selected && add.trace.poly.onZ(avgZ)) {
func.traceToggle(add);
}
}
@ -1147,23 +1144,17 @@ CAM.init = function(kiri, api) {
let wlist = areas[widget.id] = areas[widget.id] || [];
obj.selected = !obj.selected;
if (!colorSave) {
colorSave = material.colorSave = {
r: color.r,
g: color.g,
b: color.b
};
colorSave = material.colorSave = color.clone();
}
if (obj.selected) {
obj.position.z += 0.01;
color.r = colorSave.r = 0.9;
color.g = colorSave.g = 0;
color.b = colorSave.b = 0.1;
color.setHex(isDark() ? 0xdd0011 : 0xff0033);
colorSave.r = color.r;
colorSave.g = color.g;
colorSave.b = color.b;
if (!skip) wlist.push(poly._trace);
} else {
obj.position.z -= 0.01;
color.r = colorSave.r = 0xaa/255;
color.g = colorSave.g = 0xaa/255;
color.b = colorSave.b = 0x55/255;
color.setHex(0xaaaa55);
colorSave.setHex(0xaaaa55);
if (!skip) wlist.remove(poly._trace);
}
API.conf.save();
@ -1192,9 +1183,6 @@ CAM.init = function(kiri, api) {
func.hover = func.selectHolesHover;
func.hoverUp = func.selectHolesHoverUp;
const widgets = kiri.api.widgets.all()
/**
* creates a mesh for a hole and adds it to a widget
@ -1712,13 +1700,11 @@ CAM.init = function(kiri, api) {
dogbone: 'camTraceDogbone',
revbone: 'camTraceDogbone',
merge: 'camTraceMerge',
select: 'camTraceMode',
ov_topz: 0,
ov_botz: 0,
ov_conv: '~camConventional',
}).inputs = {
tool: UC.newSelect(LANG.cc_tool, {}, "tools"),
select: UC.newSelect(LANG.cc_sele_s, {title:LANG.cc_sele_l}, "select"),
mode: UC.newSelect(LANG.cu_type_s, {title:LANG.cu_type_l}, "trace"),
offset: UC.newSelect(LANG.cc_offs_s, {title: LANG.cc_offs_l, show:() => (poppedRec.mode === 'follow')}, "traceoff"),
sep: UC.newBlank({class:"pop-sep"}),
@ -1969,10 +1955,12 @@ function createPopOp(type, map) {
const settings = conf.get();
const {tool} = new CAM.Tool(settings,op.rec.tool); //get tool by id
const opType = op.rec.type
if( opType != "drill" && tool.type == "drill"){
const drillOrRegister = opType == "drill" || opType == "register"
if ( !drillOrRegister && tool.type == "drill"){
alerts.show(`Warning: Drills should not be used for ${opType} operations.`)
}
else if( opType == "drill" && tool.type != "drill"){
else if ( drillOrRegister && tool.type != "drill"){
alerts.show(`Warning: Only drills should be used for drilling operations.`)
}
@ -2160,16 +2148,18 @@ function validateTools(tools) {
}
}
function isDark() { return API.space.is_dark() };
function addbox() { return FDM.addbox(...arguments)};
function delbox() { return FDM.delbox(...arguments)};
function boxColor() {
return API.space.is_dark() ? 0x00ddff : 0x0000dd;
return isDark() ? 0x00ddff : 0x0000dd;
}
function boxOpacity() {
return API.space.is_dark() ? 0.75 : 0.6;
return isDark() ? 0.75 : 0.6;
}
function animate() {
@ -2252,7 +2242,7 @@ function updateStock() {
camStock.position.z = center.z;
camStock.rotation.x = currentIndex || 0;
camStock.lines.material.color =
new THREE.Color(API.space.is_dark() ? 0x555555 : 0xaaaaaa);
new THREE.Color(isDark() ? 0x555555 : 0xaaaaaa);
} else if (camStock) {
SPACE.world.remove(camStock);
camStock = null;

View file

@ -976,11 +976,12 @@ class OpTrace extends CamOp {
}
async slice(progress) {
const debug = false;
let { op, state } = this;
let { tool, rate, down, plunge, offset, offover, thru } = op;
let { ov_conv } = op;
let { settings, widget, addSlices, zThru, tabs, workarea } = state;
let { updateToolDiams, cutTabs, cutPolys, healPolys, color } = state;
let { updateToolDiams, cutTabs, cutPolys, healPolys, color, shadowAt } = state;
let { process, stock } = settings;
let { camStockClipTo } = process;
if (state.isIndexed) {
@ -1042,17 +1043,39 @@ class OpTrace extends CamOp {
.setLayer("trace follow", {line: color}, false)
.addPolys(slice.camLines)
}
function clearZ(polys, z, down) {
function clearZnew(polys, z, down) {
if (down) {
// adjust step down to a value <= down that
// ends on the lowest z specified
let diff = zTop - z;
down = diff / Math.ceil(diff / down);
}
let zs = down ? base.util.lerp(zTop, z, down) : [ z ];
let nested = POLY.nest(polys);
for (let poly of nested) {
let zpro = 0, zinc = 1 / (polys.length * zs.length);
for (let poly of polys) {
// newPocket();
for (let z of zs) {
let clip = [], shadow;
shadow = shadowAt(z);
POLY.subtract([ poly ], shadow, clip, undefined, undefined, 0);
if (op.outline) {
POLY.clearInner(clip);
}
if (clip.length === 0) {
continue;
}
let slice = newSliceOut(z);
let count = 999;
slice.camTrace = { tool, rate, plunge };
POLY.offset([ poly ], [ -toolDiam/2, -toolOver ], {
count:999, outs: slice.camLines = [], flat:true, z,
minArea: 0
});
if (toolDiam) {
const offs = [ -toolDiam / 2, -toolOver ];
POLY.offset(clip, offs, {
count, outs: slice.camLines = [], flat:true, z, minArea: 0
});
} else {
// when engraving with a 0 width tip
slice.camLines = clip;
}
if (tabs) {
slice.camLines = cutTabs(tabs, POLY.flatten(slice.camLines, null, true), z);
} else {
@ -1060,8 +1083,14 @@ class OpTrace extends CamOp {
}
POLY.setWinding(slice.camLines, cutdir, false);
slice.output()
.setLayer("trace clear", {line: color}, false)
.setLayer("trace", {line: color}, false)
.addPolys(slice.camLines)
if (debug && shadow) slice.output()
.setLayer("trace shadow", {line: 0xff8811}, false)
.addPolys(shadow)
progress(zpro, "trace");
zpro += zinc;
addSlices(slice);
}
}
}
@ -1176,7 +1205,7 @@ class OpTrace extends CamOp {
const zbo = widget.track.top - widget.track.box.d;
let zmap = {};
for (let poly of polys) {
let z = minZ(poly.getZ());
let z = minZ(poly.minZ());
if (offover) {
let pnew = POLY.offset([poly], -offover, { minArea: 0, open: true });
if (pnew) {
@ -1190,7 +1219,7 @@ class OpTrace extends CamOp {
(zmap[z] = zmap[z] || []).appendAll(poly);
}
for (let [zv, polys] of Object.entries(zmap)) {
clearZ(polys, parseFloat(zv), down);
clearZnew(polys, parseFloat(zv), down);
}
}
}

View file

@ -2,6 +2,7 @@
"use strict";
// dep: geo.line
// dep: geo.point
// dep: geo.polygons
// dep: kiri.slice
@ -14,7 +15,7 @@ gapp.register("kiri-mode.cam.slice", (root, exports) => {
const { base, kiri } = root;
const { util } = base;
const { driver, newSlice, setSliceTracker } = kiri;
const { polygons, newPoint, newPolygon } = base;
const { polygons, newLine, newPoint, newPolygon } = base;
const { CAM } = driver;
const { OPS } = CAM;
@ -379,60 +380,107 @@ CAM.addDogbones = function(poly, dist, reverse) {
};
CAM.traces = async function(settings, widget, single) {
if (widget.traces && widget.trace_single === single) {
// do no work if cached
if (widget.traces) {
return false;
}
let slicer = new kiri.cam_slicer(widget);
let zees = Object.assign({}, slicer.zFlat, slicer.zLine);
let indices = [...new Set(Object.keys(zees)
.map(kv => parseFloat(kv).round(5))
.appendAll(Object.entries(single ? slicer.zLine : {}).map(ze => {
let [ zk, zv ] = ze;
return zv > 1 ? parseFloat(zk).round(5) : null;
})
.filter(v => v !== null))
)]
.sort((a,b) => b - a);
let traces = [];
// find and trim polys (including open) to shadow
let oneach = data => {
if (single) {
for (let line of data.lines) {
if (line.p1.distTo2D(line.p2) > 1) {
traces.push(newPolygon().append(line.p1).append(line.p2).setOpen());
}
}
} else
base.polygons.flatten(data.tops).forEach(poly => {
poly.inner = null;
poly.parent = null;
let z = poly.getZ();
for (let i=0, il=traces.length; i<il; i++) {
let trace = traces[i];
let dz = Math.abs(z - trace.getZ());
// only compare polys farther apart in Z
if (dz > 0.01) {
continue;
}
// do not add duplicates
if (traces[i].isEquivalent(poly) && dz < 0.1) {
// console.log({ dup: poly });
return;
}
}
traces.push(poly);
});
};
// --- points → line segments ---
let edges = new THREE.EdgesGeometry(widget.mesh.geometry, 20);
let array = edges.attributes.position.array;
let pcache = {};
let points = new Array(2);
let lines = new Array(points.length / 2);
for (let i=0, j=0, k=0, l=array.length; i<l; ) {
let ps = [ array[i++], array[i++], array[i++] ];
let key = ps.map(v => (v * 10000) | 0).join(',');
let point = pcache[key];
if (!point) {
point = newPoint(ps[0], ps[1], ps[2], key);
point.lines = [];
pcache[key] = point;
}
points[j++ % 2] = point;
if (j % 2 === 0) {
let [ p0, p1 ] = points;
let line = lines[k++] = newLine(p0, p1);
p0.lines.push(line);
p1.lines.push(line);
}
}
// --- segments → chains (bidirectional walk) ---
let chains = [];
{
const segs = lines.slice(); // shallow copy
segs.forEach(s => (s.visited = false));
function step(dirPoint, prevSeg, pushFront, chain) {
let curr = dirPoint;
let prev = prevSeg;
while (curr.lines.length === 2) { // stay inside the arc
const next = curr.lines.find(l => !l.visited && l !== prev);
if (!next) break; // should not happen
next.visited = true;
if (pushFront) chain.unshift(next); // prepend or append?
else chain.push(next);
curr = next.p1 === curr ? next.p2 : next.p1;
prev = next;
}
}
for (const seed of segs) {
if (seed.visited) continue;
seed.visited = true; // always include the seed
const chain = [seed];
// grow backwards from p1 (prepend)
step(seed.p1, seed, true, chain);
// grow forwards from p2 (append)
step(seed.p2, seed, false, chain);
chains.push(chain);
}
}
// --- chains → polylines ---
const polylines = [];
for (const chain of chains) {
const poly = newPolygon().setOpen();
// choose any node whose degree !== 2; if none, its a closed loop
let start = null;
for (const s of chain) {
if (s.p1.lines.length !== 2) { start = s.p1; break; }
if (s.p2.lines.length !== 2) { start = s.p2; break; }
}
if (!start) start = chain[0].p1; // closed loop
let curr = start, prevSeg = null;
poly.push(curr); // first point
while (true) {
const nextSeg = curr.lines.find(
l => l !== prevSeg && chain.includes(l)
);
if (!nextSeg) break; // open end reached
curr = nextSeg.p1 === curr ? nextSeg.p2 : nextSeg.p1;
poly.push(curr); // << push *before* test
if (curr === start) break; // loop closed
prevSeg = nextSeg;
}
polylines.push(poly);
}
widget.traces = polylines;
let opts = { each: oneach, over: false, flatoff: 0, edges: true, openok: true, lines: true };
await slicer.slice(indices, opts);
// pick up bottom features
opts.over = true;
await slicer.slice(indices, opts);
widget.traces = traces;
widget.trace_single = single;
return true;
};

View file

@ -355,6 +355,27 @@ class Widget {
}
}
getVisualState() {
return {
edges: this.outline ? true : false,
wires: this.wire ? true : false,
opacity: this.getMaterial().opacity
};
}
setVisualState({ edges, wires, opacity}) {
this.cache.vizstate = this.getVisualState();
this.setEdges(edges ?? false);
this.setWireframe(wires ?? false);
this.setOpacity(opacity ?? 1);
}
restoreVisualState() {
if (this.cache.vizstate) {
this.setVisualState(this.cache.vizstate);
}
}
/**
* @param {number} value
*/

View file

@ -5,7 +5,7 @@
let terms = {
COPYRIGHT: "Copyright (C) Stewart Allen <sa@grid.space> - All Rights Reserved",
LICENSE: "See the license.md file included with the source distribution",
VERSION: (is_self ? self : this).debug_version || "4.1.9"
VERSION: (is_self ? self : this).debug_version || "4.2.0"
};
if (typeof(module) === 'object') {

View file

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