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 ## Methods
| Method | Description | Returns | | 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>` | | `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` | | `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>` | | `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` | | `setListener(listener)` | Sets an event listener function to receive engine progress updates | `Engine` |
| `setRender(bool)` | Enables or disables rendering. | `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` | | `setDevice(device)` | Merges a custom device profile into the current settings. | `Engine` |
| `setProcess(process)` | Merges custom slicing process parameters. | `Engine` | | `setProcess(process)` | Merges custom slicing process parameters. | `Engine` |
| `setController(controller)` | Sets the slicing controller settings and starts/stops the worker pool accordingly. | `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](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 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", type: "doc",
id: "index", id: "index",
}, },
{ {
type: "category", type: "category",
@ -14,8 +13,8 @@ const sidebars = {
type: "category", type: "category",
label: "Kiri Moto", label: "Kiri Moto",
link: { link: {
type: 'doc', type: "doc",
id: 'kiri-moto/index', id: "kiri-moto/index",
}, },
items: [ items: [
"kiri-moto/interface", "kiri-moto/interface",
@ -32,8 +31,8 @@ const sidebars = {
type: "category", type: "category",
label: "CAM Mode", label: "CAM Mode",
link: { link: {
type: 'doc', type: "doc",
id: 'kiri-moto/CAM/index', id: "kiri-moto/CAM/index",
}, },
items: [ items: [
"kiri-moto/CAM/processOpts", "kiri-moto/CAM/processOpts",
@ -41,7 +40,7 @@ const sidebars = {
"kiri-moto/CAM/tools", "kiri-moto/CAM/tools",
"kiri-moto/CAM/ops", "kiri-moto/CAM/ops",
], ],
} },
], ],
}, },
{ {
@ -52,39 +51,37 @@ const sidebars = {
type: "category", type: "category",
label: "GridBot", label: "GridBot",
link: { link: {
type: 'doc', type: "doc",
id: 'gridbot/index', id: "gridbot/index",
},
items: ["gridbot/bom"],
}, },
items: [
"gridbot/bom",
]
}
], ],
}, },
{ {
type: 'category', type: "category",
label: 'Community', label: "Community",
collapsed: false, collapsed: false,
items: [ items: [
{ {
type: 'link', type: "link",
label: 'Forums', label: "Forums",
href: 'https://forum.grid.space', href: "https://forum.grid.space",
}, },
{ {
type: 'link', type: "link",
label: 'Discord', label: "Discord",
href: 'https://discord.gg/suyCCgr', href: "https://discord.gg/suyCCgr",
}, },
{ {
type: 'link', type: "link",
label: 'GitHub', label: "GitHub",
href: 'https://github.com/GridSpace/grid-apps', href: "https://github.com/GridSpace/grid-apps",
}, },
{ {
type: 'link', type: "link",
label: 'BlueSky', label: "BlueSky",
href: 'https://bsky.app/profile/grid.space', href: "https://bsky.app/profile/grid.space",
}, },
], ],
}, },
@ -104,4 +101,3 @@ const sidebars = {
}; };
module.exports = sidebars; module.exports = sidebars;

View file

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

View file

@ -1002,6 +1002,23 @@ class Polygon {
return this.z !== undefined ? this.z : this.points[i || 0]?.z || 0; 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) { render(layer, color, recursive, open) {

View file

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

View file

@ -976,11 +976,12 @@ class OpTrace extends CamOp {
} }
async slice(progress) { async slice(progress) {
const debug = false;
let { op, state } = this; let { op, state } = this;
let { tool, rate, down, plunge, offset, offover, thru } = op; let { tool, rate, down, plunge, offset, offover, thru } = op;
let { ov_conv } = op; let { ov_conv } = op;
let { settings, widget, addSlices, zThru, tabs, workarea } = state; 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 { process, stock } = settings;
let { camStockClipTo } = process; let { camStockClipTo } = process;
if (state.isIndexed) { if (state.isIndexed) {
@ -1042,17 +1043,39 @@ class OpTrace extends CamOp {
.setLayer("trace follow", {line: color}, false) .setLayer("trace follow", {line: color}, false)
.addPolys(slice.camLines) .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 zs = down ? base.util.lerp(zTop, z, down) : [ z ];
let nested = POLY.nest(polys); let zpro = 0, zinc = 1 / (polys.length * zs.length);
for (let poly of nested) { for (let poly of polys) {
// newPocket();
for (let z of zs) { 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 slice = newSliceOut(z);
let count = 999;
slice.camTrace = { tool, rate, plunge }; slice.camTrace = { tool, rate, plunge };
POLY.offset([ poly ], [ -toolDiam/2, -toolOver ], { if (toolDiam) {
count:999, outs: slice.camLines = [], flat:true, z, const offs = [ -toolDiam / 2, -toolOver ];
minArea: 0 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) { if (tabs) {
slice.camLines = cutTabs(tabs, POLY.flatten(slice.camLines, null, true), z); slice.camLines = cutTabs(tabs, POLY.flatten(slice.camLines, null, true), z);
} else { } else {
@ -1060,8 +1083,14 @@ class OpTrace extends CamOp {
} }
POLY.setWinding(slice.camLines, cutdir, false); POLY.setWinding(slice.camLines, cutdir, false);
slice.output() slice.output()
.setLayer("trace clear", {line: color}, false) .setLayer("trace", {line: color}, false)
.addPolys(slice.camLines) .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; const zbo = widget.track.top - widget.track.box.d;
let zmap = {}; let zmap = {};
for (let poly of polys) { for (let poly of polys) {
let z = minZ(poly.getZ()); let z = minZ(poly.minZ());
if (offover) { if (offover) {
let pnew = POLY.offset([poly], -offover, { minArea: 0, open: true }); let pnew = POLY.offset([poly], -offover, { minArea: 0, open: true });
if (pnew) { if (pnew) {
@ -1190,7 +1219,7 @@ class OpTrace extends CamOp {
(zmap[z] = zmap[z] || []).appendAll(poly); (zmap[z] = zmap[z] || []).appendAll(poly);
} }
for (let [zv, polys] of Object.entries(zmap)) { 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"; "use strict";
// dep: geo.line
// dep: geo.point // dep: geo.point
// dep: geo.polygons // dep: geo.polygons
// dep: kiri.slice // dep: kiri.slice
@ -14,7 +15,7 @@ gapp.register("kiri-mode.cam.slice", (root, exports) => {
const { base, kiri } = root; const { base, kiri } = root;
const { util } = base; const { util } = base;
const { driver, newSlice, setSliceTracker } = kiri; const { driver, newSlice, setSliceTracker } = kiri;
const { polygons, newPoint, newPolygon } = base; const { polygons, newLine, newPoint, newPolygon } = base;
const { CAM } = driver; const { CAM } = driver;
const { OPS } = CAM; const { OPS } = CAM;
@ -379,60 +380,107 @@ CAM.addDogbones = function(poly, dist, reverse) {
}; };
CAM.traces = async function(settings, widget, single) { CAM.traces = async function(settings, widget, single) {
if (widget.traces && widget.trace_single === single) { if (widget.traces) {
// do no work if cached
return false; 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, it’s 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; 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 * @param {number} value
*/ */

View file

@ -5,7 +5,7 @@
let terms = { let terms = {
COPYRIGHT: "Copyright (C) Stewart Allen <sa@grid.space> - All Rights Reserved", COPYRIGHT: "Copyright (C) Stewart Allen <sa@grid.space> - All Rights Reserved",
LICENSE: "See the license.md file included with the source distribution", 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') { 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", "short_name": "Kiri:Moto",
"description": "Slicer for 3D printers, CNC mills, laser cutters and more", "description": "Slicer for 3D printers, CNC mills, laser cutters and more",
"start_url": "/kiri/", "start_url": "/kiri/",