Merge branch 'master' of https://github.com/GridSpace/grid-apps into drill-optimization

This commit is contained in:
bakedpotatolord 2025-11-25 15:56:11 -07:00
commit efb41e14fa
128 changed files with 8990 additions and 4345 deletions

View file

@ -168,7 +168,7 @@ SP.init = function(options = {}) {
request = IDB.open(name, this.version);
request.onupgradeneeded = function(event) {
let db = storage.db = request.result;
let db = request.result;
let current = [...db.objectStoreNames];
let stores = storage.stores;
// add missing stores
@ -184,13 +184,12 @@ SP.init = function(options = {}) {
// remove obsolete stores
for (let curr of current) {
if (stores.indexOf(curr) < 0) {
storage.db.deleteObjectStore(curr);
db.deleteObjectStore(curr);
console.log({index_dropped: curr});
}
}
event.target.transaction.oncomplete = function(event) {
storage.runQueue();
};
// allow transactions
storage.db = db;
};
request.onsuccess = function(event) {
@ -217,6 +216,7 @@ SP.runQueue = function() {
let i = 0, q = this.queue, e;
while (i < q.length) {
e = q[i++];
// console.log('runQ', i, ...e);
switch (e.shift()) {
case 'iterate': this.iterate(...e); break;
case 'keys': this.keys(...e); break;
@ -252,7 +252,11 @@ SP.put = function(key, value, callback, store = this.current) {
req.onerror = function(event) { callback(false) };
}
} catch (e) {
console.log(e);
if (e.name === 'InvalidStateError') {
console.log({ putRetry: key });
return this.queue.push(['put', key, value, callback, store]);
}
console.log(e.name, e);
if (callback) callback(false);
}
};
@ -275,6 +279,10 @@ SP.get = function(key, callback, store = this.current) {
req.onerror = function(event) { callback(null) };
}
} catch (e) {
if (e.name === 'InvalidStateError') {
console.log({ getRetry: key });
return this.queue.push(['get', key, callback, store]);
}
console.log(e);
if (callback) callback(null);
}

View file

@ -4,4 +4,4 @@
import './pngjs.js';
// Re-export the PNG object
export const PNG = self.png ? self.png.PNG : null;
export const PNG = globalThis.png ? globalThis.png.PNG : null;

View file

@ -1,4 +1,4 @@
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.png = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
(function(f){globalThis.png = f();if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.png = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
(function (Buffer){
"use strict";

View file

@ -127,6 +127,12 @@ class Point {
return p;
}
// annotate instance with other field data (see clone())
annotate(obj = {}) {
Object.assign(this, obj);
return this;
}
slopeTo(p) {
return newSlope(this, p);
}
@ -441,10 +447,6 @@ class Point {
return this === p || (this.x === p.x && this.y === p.y && this.z === p.z);
}
isEqual2D(p) {
return this === p || (this.x === p.x && this.y === p.y);
}
/**
* returns true if points are close enough to be considered equivalent
*/

View file

@ -908,6 +908,7 @@ export class Polygon {
* todo make more efficient
*
* @param {Boolean} deep
* @param {String[]} fields to copy beyond class fields
* @returns {Polygon}
*/
clone(deep, fields) {
@ -2145,6 +2146,7 @@ export class Polygon {
return null;
}
// annotate instance with other field data (see clone())
annotate(obj = {}) {
Object.assign(this, obj);
return this;

View file

@ -12,6 +12,8 @@ import { polygons } from '../geo/polygons.js';
import { newPoint } from '../geo/point.js';
import { config } from '../geo/base.js';
const epsilon = 10e-5;
function dval(v, dv) {
return v !== undefined ? v : dv;
}
@ -75,7 +77,7 @@ export async function slice(points, options = {}) {
// used to calculate buckets (rough sum of z span)
zSum += (Math.abs(p1.z - p2.z) + Math.abs(p2.z - p3.z) + Math.abs(p3.z - p1.z));
// use co-flat and co-line detection to adjust slice Z
if (p1.z === p2.z && p2.z === p3.z && p1.z >= zMin) {
if (Math.abs(p1.z - p2.z) < epsilon && Math.abs(p2.z - p3.z) < epsilon && p1.z >= zMin) {
// detect faces co-planar with Z and sum the enclosed area
let zkey = p1.z,
area = Math.abs(base.util.area2(p1,p2,p3)) / 2;

View file

@ -60,13 +60,6 @@ export class Slope {
return this;
}
invert() {
this.dx = -this.dx;
this.dy = -this.dy;
this.angle = 180 - this.angle;
return this;
}
angleDiff(s2,sign) {
const n1 = this.angle;
const n2 = s2.angle;

1
src/gpu/raster-worker.js Symbolic link
View file

@ -0,0 +1 @@
../../node_modules/@gridspace/raster-path/build/raster-worker.js

1
src/gpu/raster.js Symbolic link
View file

@ -0,0 +1 @@
../../node_modules/@gridspace/raster-path/build/raster-path.js

View file

@ -9,7 +9,7 @@ import { client as work } from './client.js';
import { consts, COLOR as color, LISTS as lists } from './consts.js';
import { device, devices } from './devices.js';
import { catalog, dialog, event, group, help, hide, image } from './main.js';
import { modal, mode, probe, process, show, space, util, view } from './main.js';
import { modal, mode, process, show, space, util, view } from './main.js';
import { local as dataLocal } from '../../data/local.js';
import { noop, ajax, o2js, js2o } from './utils.js';
import { functions } from './function.js';
@ -96,6 +96,7 @@ let UC = UI.prefix('kiri').inputAction(settings.conf.update),
put: (key, val) => localSet(key, val),
set: (key, val) => localSet(key, val),
setItem: (key, val) => localSet(key, val),
removeItem: (key) => localRemove(key)
},
tweak = {
line_precision(v) { api.work.config({ base: { clipperClean: v } }) },
@ -119,6 +120,11 @@ function localSet(key, val) {
return val;
}
function localRemove(key) {
let sloc = api.conf.get().local;
return delete sloc[key];
}
export const api = {
ajax,
beta,
@ -137,6 +143,7 @@ export const api = {
dialog,
doit,
event,
electron: navigator.userAgent.includes('Electron'),
feature,
function: functions,
group,
@ -156,7 +163,6 @@ export const api = {
o2js,
onkey,
platform,
probe,
process,
sdb: dataLocal,
selection,

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

@ -182,33 +182,33 @@ function normalize(settings) {
// auto field renaming on import
const renamed = {
roughingTool: "camRoughTool",
roughingSpindle: "camRoughSpindle",
roughingDown: "camRoughDown",
roughingOver: "camRoughOver",
roughingSpeed: "camRoughSpeed",
roughingPlunge: "camRoughPlunge",
roughingStock: "camRoughStock",
roughingPocket: "camRoughVoid",
roughingOn: "camRoughOn",
finishingTool: "camOutlineTool",
finishingSpindle: "camOutlineSpindle",
camWideCutout: "camOutlineWide",
drillDown: "camDrillDown",
drillDownSpeed: "camDrillDownSpeed",
drillDwell: "camDrillDwell",
drillingOn: "camDrillingOn",
drillLift: "camDrillLift",
drillSpindle: "camDrillSpindle",
drillTool: "camDrillTool",
finishingDown: "camOutlineDown",
finishingOver: "camContourOver",
finishingSpeed: "camOutlineSpeed",
finishingPlunge: "camOutlinePlunge",
finishingOn: "camOutlineOn",
finishingOver: "camContourOver",
finishingPlunge: "camOutlinePlunge",
finishingSpeed: "camOutlineSpeed",
finishingSpindle: "camOutlineSpindle",
finishingTool: "camOutlineTool",
finishingXOn: "camContourXOn",
finishingYOn: "camContourYOn",
drillTool: "camDrillTool",
drillSpindle: "camDrillSpindle",
drillDownSpeed: "camDrillDownSpeed",
drillDown: "camDrillDown",
drillDwell: "camDrillDwell",
drillLift: "camDrillLift",
drillingOn: "camDrillingOn",
camWideCutout: "camOutlineWide",
outputClockwise: "camConventional"
outputClockwise: "camConventional",
roughingDown: "camRoughDown",
roughingOn: "camRoughOn",
roughingOver: "camRoughOver",
roughingPlunge: "camRoughPlunge",
roughingPocket: "camRoughVoid",
roughingSpeed: "camRoughSpeed",
roughingSpindle: "camRoughSpindle",
roughingStock: "camRoughStock",
roughingTool: "camRoughTool",
};
export const conf = {
@ -258,95 +258,95 @@ export const conf = {
},
// process defaults FDM:Process
p:{
antiBacklash: 0,
arcTolerance: 0,
firstLayerBedTemp: 0,
firstLayerBeltBump: 0,
firstLayerBeltLead: 3,
firstLayerBeltFact: 1,
firstLayerBrim: 0,
firstLayerBrimGap: 0,
firstLayerBrimIn: 0,
firstLayerBrimTrig: 0,
firstLayerFanSpeed: 0,
firstLayerFillRate: 35,
firstLayerFlatten: 0,
firstLayerNozzleTemp: 0,
firstLayerRate: 30,
firstLayerYOffset: 0,
firstSliceHeight: 0.25,
outputAlternating: false,
outputAvoidGaps: true,
outputBedTemp: 60,
outputBeltFirst: false,
outputBrimCount: 2,
outputBrimOffset: 2,
outputCoastDist: 0,
outputDraftShield: false,
outputFanLayer: 1,
outputFanSpeed: 255,
outputFeedrate: 50,
outputFillMult: 1.25,
outputFinishrate: 50,
outputInvertX: false,
outputInvertY: false,
outputLayerRetract: false,
outputLoops: 0,
outputMaxFlowrate: 15,
outputMinLayerTime: 10,
outputMinSpeed: 5.0,
outputPurgeTower: 0,
outputRaft: false,
outputRaftSpacing: 0.2,
outputRetractDist: 1.5,
outputRetractDwell: 20,
outputRetractSpeed: 40,
outputRetractWipe: 0,
outputScarfLength: 0,
outputSeekrate: 80,
outputShellMult: 1.25,
outputShortPoly: 0.0,
outputSparseMult: 1.25,
outputTemp: 200,
processName: "default",
ranges: [],
sliceAdaptive: false,
sliceAngle: 45,
sliceBottomLayers: 3,
sliceCompInner: 0,
sliceCompOuter: 0,
sliceDetectThin: "basic",
sliceFillAngle: 45,
sliceFillGrow: 0,
sliceFillOverlap: 0.35,
sliceFillRepeat: 1,
sliceFillSparse: 0.25,
sliceFillType: "hex",
sliceFillWidth: 1,
sliceHeight: 0.25,
sliceShells: 3,
sliceShellOrder: "in-out",
sliceLayerStart: "last",
sliceLayerStartX: 0,
sliceLayerStartY: 0,
sliceLineWidth: 0,
sliceFillGrow: 0,
sliceFillAngle: 45,
sliceFillWidth: 1,
sliceFillOverlap: 0.35,
sliceFillSparse: 0.25,
sliceFillRepeat: 1,
sliceFillRate: 0,
sliceSolidRate: 0,
sliceFillType: "hex",
sliceSupportDensity: 0.1,
sliceSupportOffset: 1.0,
sliceSupportGap: 1,
sliceSupportSize: 5,
sliceSupportArea: 0.1,
sliceSupportSpan: 5,
sliceSupportGrow: 0,
sliceSupportExtra: 0,
sliceSupportAngle: 50,
sliceSupportNozzle: 0,
sliceSupportEnable: false,
sliceSupportOutline: true,
sliceZInterleave: false,
sliceSolidMinArea: 1,
sliceBottomLayers: 3,
sliceTopLayers: 3,
firstSliceHeight: 0.25,
firstLayerRate: 30,
firstLayerFillRate: 35,
firstLayerPrintMult: 1.0,
firstLayerLineMult: 1.0,
firstLayerYOffset: 0,
firstLayerNozzleTemp: 0,
firstLayerBedTemp: 0,
firstLayerBrim: 0,
firstLayerBrimIn: 0,
firstLayerBrimTrig: 0,
firstLayerBrimGap: 0,
firstLayerBeltLead: 3,
firstLayerBeltBump: 0,
firstLayerFanSpeed: 0,
firstLayerFlatten: 0,
outputRaft: false,
outputRaftSpacing: 0.2,
outputDraftShield: false,
outputTemp: 200,
outputBedTemp: 60,
outputFeedrate: 50,
outputFinishrate: 50,
outputSeekrate: 80,
outputShellMult: 1.25,
outputFillMult: 1.25,
outputSparseMult: 1.25,
outputFanLayer: 1,
outputFanSpeed: 255,
outputRetractDist: 1.5,
outputRetractSpeed: 40,
outputRetractWipe: 0,
outputRetractDwell: 20,
outputBrimCount: 2,
outputBrimOffset: 2,
outputScarfLength: 0,
outputShortPoly: 100.0,
outputMinSpeed: 10.0,
outputMinLayerTime: 0,
outputCoastDist: 0,
outputPurgeTower: 0,
outputBeltFirst: false,
outputAvoidGaps: true,
outputAlternating: false,
outputLayerRetract: false,
outputLoops: 0,
outputInvertX: false,
outputInvertY: false,
sliceDetectThin: "off",
sliceMinHeight: 0,
sliceAdaptive: false,
sliceShellOrder: "in-out",
sliceShells: 3,
sliceSolidMinArea: 1,
sliceSupportAngle: 50,
sliceSupportArea: 0.1,
sliceSupportDensity: 0.1,
sliceSupportEnable: false,
sliceSupportExtra: 0,
sliceSupportGap: 1,
sliceSupportGrow: 0,
sliceSupportNozzle: 0,
sliceSupportOffset: 1.0,
sliceSupportOutline: true,
sliceSupportSize: 5,
sliceSupportSpan: 5,
sliceTopLayers: 3,
sliceZInterleave: false,
zHopDistance: 0.2,
arcTolerance: 0,
antiBacklash: 0,
ranges: []
}
},
sla:{
@ -420,186 +420,187 @@ export const conf = {
},
// process defaults CAM:Process
p:{
processName: "default",
camLevelTool: 1000,
camLevelSpindle: 1000,
camLevelOver: 0.75,
camLevelStepZ: 0,
camLevelSpeed: 1000,
camLevelDown: 0,
camLevelInset: 0.5,
camLevelStock: true,
camRoughTool: 1000,
camRoughSpindle: 1000,
camRoughDown: 2,
camRoughOver: 0.4,
camRoughSpeed: 1000,
camRoughPlunge: 250,
camRoughStock: 0,
camRoughStockZ: 0,
camRoughAll: true,
camRoughVoid: false,
camRoughFlat: true,
camRoughTop: true,
camRoughIn: true,
camRoughOn: true,
camRoughOmitThru: false,
camRoughOmitVoid: false,
camOutlineTool: 1000,
camOutlineSpindle: 1000,
camOutlineTop: true,
camOutlineDown: 3,
camOutlineOver: 0.4,
camOutlineOverCount: 1,
camOutlineSpeed: 800,
camOutlinePlunge: 250,
camOutlineWide: false,
camOutlineDogbone: false,
camOutlineOmitThru: false,
camOutlineOmitVoid: false,
camOutlineOut: true,
camOutlineIn: false,
camOutlineOn: true,
camContourTool: 1000,
camContourSpindle: 1000,
camContourOver: 0.5,
camContourSpeed: 1000,
camArcEnabled: false,
camArcResolution: 5,
camArcTolerance: 0.15,
camContourAngle: 85,
camContourLeave: 0,
camContourReduce: 2,
camContourBottom: false,
camContourBridge: 0,
camContourCurves: false,
camContourIn: false,
camContourLeave: 0,
camContourOver: 0.5,
camContourReduce: 2,
camContourSpeed: 1000,
camContourSpindle: 1000,
camContourTool: 1000,
camContourXOn: true,
camContourYOn: true,
camLatheTool: 1000,
camLatheSpindle: 1000,
camLatheOver: 0.1,
camLatheAngle: 1,
camLatheSpeed: 500,
camLatheLinear: true,
camTolerance: 0,
camTraceTool: 1000,
camTraceSpindle: 1000,
camTraceType: "follow",
camTraceOver: 0.5,
camTraceDown: 0,
camTraceThru: 0,
camTraceSpeed: 250,
camTracePlunge: 200,
camTraceOffOver: 0,
camTraceDogbone: false,
camTraceMerge: false,
camTraceLines: false,
camTraceZTop: 0,
camTraceZBottom: 0,
camPocketSpindle: 1000,
camPocketTool: 1000,
camPocketOver: 0.25,
camPocketDown: 1,
camPocketSpeed: 250,
camPocketPlunge: 200,
camPocketExpand: 0,
camPocketSmooth: 0,
camPocketRefine: 20,
camPocketFollow: 5,
camPocketContour: false,
camPocketEngrave: false,
camPocketOutline: false,
camPocketZTop: 0,
camPocketZBottom: 0,
camDrillTool: 1006,
camDrillSpindle: 1000,
camDrillDownSpeed: 250,
camConventional: false, // outputClockwise
camDepthFirst: false,
camDrillDown: 5,
camDrillDownSpeed: 250,
camDrillDwell: 250,
camDrillFromStockTop: false,
camDrillingOn: false,
camDrillLift: 2,
camDrillMark: false,
camDrillFromStockTop: false,
camDrillThru: 5,
camDrillPrecision: 1,
camDrillingOn: false,
camRegisterSpeed: 1000,
camRegisterThru: 5,
camRegisterOffset: 10,
camHelicalTool: 1000,
camHelicalSpindle: 1000,
camHelicalDownSpeed: 250,
camHelicalSpeed: 1000,
camHelicalDown: 5,
camHelicalBottomFinish: true,
camHelicalThru: 0,
camHelicalOffset: "auto",
camHelicalForceStartAngle: false,
camHelicalStartAngle: 0,
camHelicalOffsetOverride: 0,
camHelicalEntry: false,
camHelicalEntryOffset:0,
camHelicalReverse: false,
camHelicalClockwise: true,
camFlipAxis: "X",
camFlipOther: "",
camLaserEnable: ["M321"],
camLaserDisable: ["M322"],
camLaserOn: ["M3"],
camLaserOff: ["M5"],
camLaserSpeed: 100,
camLaserPower: 1,
camLaserAdaptive: false,
camLaserAdaptMod: false,
camLaserFlatten: false,
camLaserFlatZ: 0,
camLaserPowerMin: 0,
camLaserPowerMax: 1,
camLaserZMin: 0,
camLaserZMax: 0,
camTabsWidth: 5,
camTabsHeight: 5,
camTabsDepth: 5,
camTabsMidline: false,
camDepthFirst: false,
camInnerFirst: false,
camEaseDown: false,
camDrillSpindle: 1000,
camDrillThru: 5,
camDrillTool: 1006,
camEaseAngle: 10,
camOriginTop: true,
camZAnchor: "middle",
camZOffset: 0,
camZTop: 0,
camZBottom: 0,
camZClearance: 1,
camZThru: 0,
camEaseDown: false,
camExpertFast: false,
camFastFeed: 6000,
camFastFeedZ: 300,
camTolerance: 0,
camFirstZMax: false,
camFlatness: 0.001,
camContourBridge: 0,
camStockX: 5,
camStockY: 5,
camStockZ: 5,
camStockOffset: true,
camStockClipTo: false,
camStockIndexed: false,
camStockIndexGrid: true,
camIndexAxis: 0,
camFlipAxis: "X",
camFlipOther: "",
camForceZMax: false,
camFullEngage: 0.8,
camHelicalBottomFinish: true,
camHelicalClockwise: true,
camHelicalDown: 5,
camHelicalDownSpeed: 250,
camHelicalEntry: false,
camHelicalEntryOffset:0,
camHelicalForceStartAngle: false,
camHelicalOffset: "auto",
camHelicalOffsetOverride: 0,
camHelicalReverse: false,
camHelicalSpeed: 1000,
camHelicalSpindle: 1000,
camHelicalStartAngle: 0,
camHelicalThru: 0,
camHelicalTool: 1000,
camIndexAbs: true,
camConventional: false, // outputClockwise
camIndexAxis: 0,
camInnerFirst: false,
camLaserAdaptive: false,
camLaserAdaptMod: false,
camLaserDisable: ["M322"],
camLaserEnable: ["M321"],
camLaserFlatten: false,
camLaserFlatZ: 0,
camLaserOff: ["M5"],
camLaserOn: ["M3"],
camLaserPower: 1,
camLaserPowerMax: 1,
camLaserPowerMin: 0,
camLaserSpeed: 100,
camLaserZMax: 0,
camLaserZMin: 0,
camLatheAngle: 1,
camLatheLinear: true,
camLatheOver: 0.1,
camLatheSpeed: 500,
camLatheSpindle: 1000,
camLatheTool: 1000,
camLatheOffStart: 0,
camLatheOffEnd: 0,
camLevelDown: 0,
camLevelInset: 0.5,
camLevelOver: 0.75,
camLevelSpeed: 1000,
camLevelSpindle: 1000,
camLevelStepZ: 0,
camLevelStock: true,
camLevelTool: 1000,
camOriginCenter: false,
camOriginOffX: 0,
camOriginOffY: 0,
camOriginOffZ: 0,
camOriginTop: true,
camOutlineDogbone: false,
camOutlineDown: 3,
camOutlineIn: false,
camOutlineOmitThru: false,
camOutlineOmitVoid: false,
camOutlineOn: true,
camOutlineOut: true,
camOutlineOver: 0.4,
camOutlineOverCount: 1,
camOutlinePlunge: 250,
camOutlineSpeed: 800,
camOutlineSpindle: 1000,
camOutlineTool: 1000,
camOutlineTop: true,
camOutlineWide: false,
camPocketContour: false,
camPocketDown: 1,
camPocketEngrave: false,
camPocketExpand: 0,
camPocketFollow: 5,
camPocketOutline: false,
camPocketOver: 0.25,
camPocketPlunge: 200,
camPocketRefine: 20,
camPocketSmooth: 1,
camPocketSpeed: 250,
camPocketSpindle: 1000,
camPocketTool: 1000,
camPocketZBottom: 0,
camPocketZTop: 0,
camRegisterOffset: 10,
camRegisterSpeed: 1000,
camRegisterThru: 5,
camRoughAll: true,
camRoughDown: 2,
camRoughFlat: true,
camRoughIn: true,
camRoughOmitThru: false,
camRoughOmitVoid: false,
camRoughOn: true,
camRoughOver: 0.4,
camRoughPlunge: 250,
camRoughSpeed: 1000,
camRoughSpindle: 1000,
camRoughStock: 0,
camRoughStockZ: 0,
camRoughTool: 1000,
camRoughTop: true,
camRoughVoid: false,
camStockClipTo: false,
camStockIndexed: false,
camStockIndexGrid: true,
camStockOffset: true,
camStockX: 5,
camStockY: 5,
camStockZ: 5,
camTabsDepth: 5,
camTabsHeight: 5,
camTabsMidline: false,
camTabsWidth: 5,
camTolerance: 0,
camToolInit: true,
camTraceDogbone: false,
camTraceDown: 0,
camTraceLines: false,
camTraceMerge: false,
camTraceOffOver: 0,
camTraceOver: 0.5,
camTracePlunge: 200,
camTraceSpeed: 250,
camTraceSpindle: 1000,
camTraceThru: 0,
camTraceTool: 1000,
camTraceType: "follow",
camTraceZBottom: 0,
camTraceZTop: 0,
camTrueShadow: false,
camZAnchor: "middle",
camZBottom: 0,
camZClearance: 1,
camZOffset: 0,
camZThru: 0,
camZTop: 0,
op2: [], // flip ops
ops: [], // current ops
outputInvertX: false,
outputInvertY: false,
camExpertFast: false,
camTrueShadow: false,
camArcEnabled: false,
camArcTolerance: 0.15,
camArcResolution: 5,
camForceZMax: false,
camFirstZMax: false,
camToolInit: true,
camFullEngage: 0.8,
ops: [], // current ops
op2: [] // flip ops
processName: "default",
}
},
laser: {
@ -915,37 +916,37 @@ export const conf = {
devproc: {},
// application ui and control preferences (Q menu)
controller:{
view: null,
dark: false,
shiny: false,
drawer: false,
scrolls: true,
zoomSpeed: 1.0,
lineType: "path",
autoSave: true,
animesh: "800",
antiAlias: true,
assembly: false,
autoLayout: true,
autoSave: true,
dark: false,
detail: "50",
devel: false,
drawer: false,
edgeangle: 20,
exportOcto: false,
exportPreview: false,
exportThumb: false,
freeLayout: true,
healMesh: false,
lineType: "path",
manifold: false,
ortho: false,
reverseZoom: true,
scrolls: true,
shiny: true,
showOrigin: false,
showRulers: true,
showSpeeds: true,
freeLayout: true,
autoLayout: true,
spaceRandoX: false,
spaceLayout: 1,
edgeangle: 20,
units: "mm",
exportOcto: false,
exportGhost: false,
exportLocal: false,
exportThumb: false,
exportPreview: false,
detail: "50",
animesh: "800",
healMesh: false,
spaceRandoX: false,
threaded: true,
assembly: false,
ortho: false,
devel: false
units: "mm",
view: null,
webGPU: false,
zoomSpeed: 1.0,
},
// default hidden ui groups
hidden: {

View file

@ -108,7 +108,8 @@ const LISTS = {
regaxis: [
{ name: "X" },
{ name: "Y" },
{ name: "-" }
{ name: "-" },
{ name: "=" }
],
regpoints: [
{ name: "2" },

View file

@ -218,207 +218,6 @@ function exportGCodeDialog(gcode, sections, info, names) {
ajax.send(form);
}
function gridhost_tracker(host,key) {
ajax(host+"/api/check?key="+key, function(data) {
data = js2o(data);
console.log(data);
if (!(data.done || data.error)) {
setTimeout(function() { gridhost_tracker(host,key) }, 1000);
}
});
}
function gridlocal_probe(ev, devs) {
if (ev && ev.code !== 'Enter') return;
if (!devs && api.probe.local(gridlocal_probe)) return;
grid_local = devs;
let gdev = localGet('grid-local');
let gloc = $('grid-local');
let html = [];
for (let uuid in devs) {
gdev = gdev || uuid;
let dev = devs[uuid];
let sel = uuid === gdev ? ' selected' : '';
html.push(`<option id="gl-${uuid}" value="${uuid}" ${sel}>${dev.stat.device.name}</option>`);
}
if (html.length) {
gloc.innerHTML = html.join('\n');
gloc.onchange = (ev) => {
localSet('grid-local', gloc.options[gloc.selectedIndex].value);
};
$('send-to-gridhead').style.display = 'flex';
$('send-to-gridspool').style.display = 'flex';
} else {
$('send-to-gridhead').style.display = '';
$('send-to-gridspool').style.display = '';
}
}
function sendto_gridlocal() {
let uuid = $('grid-local').value;
let dev = grid_local[uuid];
if (dev) {
let file = $('print-filename').value;
fetch(
`${api.probe.live}/api/grid_send?uuid=${uuid}&file=${encodeURIComponent(file + "." + fileext)}`,
{method: "POST", body: gcode}
)
.then(t => t.text())
.then(t => {
api.stats.add(`ua_${api.mode.get_lower()}_print_local_ok`);
console.log({grid_spool_said: t});
})
.catch(e => {
api.stats.add(`ua_${api.mode.get_lower()}_print_local_err`);
console.log({grid_local_spool_error: e});
})
.finally(() => {
api.modal.hide();
});
}
}
function admin_gridlocal() {
let dev = grid_local[$('grid-local').value];
if (dev && dev.stat && dev.stat.device) {
let dsd = dev.stat.device;
window.open(`http://${dsd.addr[0]}:${dsd.port || 4080}`, "_grid_admin");
}
}
function gridhost_probe(ev, set_host) {
if (ev && ev.code !== 'Enter') return;
if (!(grid_host && grid_apik)) return;
if (set_host) grid_host.value = set_host;
let xhtr = new XMLHttpRequest(),
host = grid_host.value,
apik = grid_apik.value,
target = grid_target.value;
if (!apik) $('gpapik').style.display = 'none';
if (!host && api.probe.grid(gridhost_probe)) return;
if (!host) return;
xhtr.onreadystatechange = function() {
if (xhtr.readyState === 4) {
if (xhtr.status >= 200 && xhtr.status < 300) {
localSet('grid-host', host);
localSet('grid-apik', apik);
let res = JSON.parse(xhtr.responseText);
let sel = false;
let match = false;
let first = null;
let html = [];
grid_targets = {};
for (let key in res) {
first = first || key;
if (!localGet('grid-target')) {
localSet('grid-target', key);
sel = true;
} else {
sel = localGet('grid-target') === key;
}
match = match || sel;
grid_targets[html.length] = key;
html.push(
"<option id='gpo-'" + key + " value='" +key + "'" +
(sel ? " selected" : "") +
">" +
(res[key].comment || key) +
"</option>"
);
}
if (!match) {
localSet('grid-target', first);
}
grid_target.innerHTML = html.join('\n');
} else if (xhtr.status === 401) {
$('gpapik').style.display = '';
} else {
local.removeItem('grid-host');
local.removeItem('grid-apik');
console.log("invalid grid:host url");
}
}
};
xhtr.open("GET", host + "/api/active?key=" + apik);
xhtr.send();
}
function sendto_gridhost() {
if (!(grid_host && grid_apik)) return;
let xhtr = new XMLHttpRequest(),
host = grid_host.value,
apik = grid_apik.value,
target = localGet('grid-target') || '';
if (target === '') {
api.show.alert('invalid or missing target');
return;
}
if (host.indexOf("http") !== 0) {
api.show.alert("host missing protocol (http:// or https://)");
return;
}
if (host.indexOf("://") < 0) {
api.show.alert("host:port malformed");
return;
}
if (api.const.SECURE && !api.util.isSecure(host)) {
api.show.alert("host must begin with 'https' on a secure site");
return;
}
localSet('grid-host', host.trim());
localSet('grid-apik', apik.trim());
xhtr.onreadystatechange = function() {
if (xhtr.readyState === 4) {
let status = xhtr.status;
api.stats.add(`ua_${api.mode.get_lower()}_print_grid_${status}`);
if (status >= 200 && status < 300) {
let json = js2o(xhtr.responseText);
gridhost_tracker(host,json.key);
api.ajax(host+"/api/wait?key="+json.key, function(data) {
data = js2o(data);
console.log(data);
api.show.alert("print to "+target+": "+data.status, 600);
});
} else {
api.show.alert("grid:host error\nstatus: "+status+"\nmessage: "+xhtr.responseText, 10000);
}
api.show.progress(0);
}
};
xhtr.upload.addEventListener('progress', function(evt) {
api.show.progress(evt.loaded/evt.total, "sending");
});
filename = $('print-filename').value;
xhtr.open("POST",
host + "/api/print?" +
"filename=" + filename +
"&target=" + target +
"&key=" + apik +
"&time=" + Math.round(info.time) +
"&length=" + Math.round(info.distance) +
"&image=" + filename
);
xhtr.setRequestHeader("Content-Type", "text/plain");
let snapshot = api.view.snapshot;
xhtr.send(snapshot ? [gcode,snapshot].join("\0") : gcode);
api.modal.hide();
}
function download() {
filename = $('print-filename').value;
api.util.download(gcode, filename + "." + fileext);
@ -463,8 +262,6 @@ function exportGCodeDialog(gcode, sections, info, names) {
let set = api.conf.get();
let fdm = MODE === MODES.FDM;
let octo = set.controller.exportOcto && MODE !== MODES.CAM;
let ghost = set.controller.exportGhost;
let local = set.controller.exportLocal;
let preview = set.controller.exportPreview;
$('code-preview-head').style.display = preview ? '' : 'none';
$('code-preview').style.display = preview ? '' : 'none';
@ -484,8 +281,6 @@ function exportGCodeDialog(gcode, sections, info, names) {
// persist fields when changed
bindField('octo-host', 'octo-host');
bindField('octo-apik', 'octo-apik');
bindField('grid-host', 'grid-host');
bindField('grid-apik', 'grid-apik');
// in cam mode, show zip file option
let downloadZip = $('print-zip');
@ -699,37 +494,11 @@ function exportGCodeDialog(gcode, sections, info, names) {
$('ophost').style.display = 'none';
$('opapik').style.display = 'none';
$('oph1nt').style.display = 'none';
$('send-to-gridhost').style.display = 'none';
}
octo_host.value = localGet('octo-host') || '';
octo_apik.value = localGet('octo-apik') || '';
} catch (e) { console.log(e) }
// grid:host setup
$('send-to-hosthead').style.display = ghost ? '' : 'none';
$('send-to-gridhost').style.display = ghost ? '' : 'none';
if (ghost) try {
$('print-gridhost').onclick = sendto_gridhost;
$('grid-host').onkeyup = gridhost_probe;
$('grid-apik').onkeyup = gridhost_probe;
grid_host = $('grid-host');
grid_apik = $('grid-apik');
grid_target = $('grid-target');
grid_target.onchange = function(ev) {
localSet('grid-target', grid_targets[grid_target.selectedIndex]);
};
grid_host.value = localGet('grid-host') || '';
grid_apik.value = localGet('grid-apik') || '';
gridhost_probe();
} catch (e) { console.log(e) }
// grid:local setup
if (local) try {
gridlocal_probe();
$('print-gridlocal').onclick = sendto_gridlocal;
$('admin-gridlocal').onclick = admin_gridlocal;
} catch (e) { console.log(e) }
// preview of the generated GCODE (first 64k max)
if (preview && gcode) $('code-preview-textarea').value = gcode.substring(0,65535);
}

View file

@ -142,28 +142,28 @@ function booleanSave() {
if (control.antiAlias != ui.antiAlias.checked) {
api.show.alert('Page Reload Required to Change Aliasing');
}
control.shiny = ui.shiny.checked;
control.antiAlias = ui.antiAlias.checked;
control.assembly = ui.assembly.checked;
control.autoLayout = ui.autoLayout.checked;
control.autoSave = ui.autoSave.checked;
control.dark = ui.dark.checked;
control.devel = ui.devel.checked;
control.drawer = ui.drawer.checked;
control.exportOcto = ui.exportOcto.checked;
control.exportPreview = ui.exportPreview.checked;
control.exportThumb = ui.exportThumb.checked;
control.freeLayout = ui.freeLayout.checked;
control.healMesh = ui.healMesh.checked;
control.ortho = ui.ortho.checked;
control.manifold = ui.manifold.checked;
control.reverseZoom = ui.reverseZoom.checked;
control.scrolls = ui.scrolls.checked;
control.shiny = ui.shiny.checked;
control.showOrigin = ui.showOrigin.checked;
control.showRulers = ui.showRulers.checked;
control.autoLayout = ui.autoLayout.checked;
control.freeLayout = ui.freeLayout.checked;
control.spaceRandoX = ui.spaceRandoX.checked;
control.autoSave = ui.autoSave.checked;
control.antiAlias = ui.antiAlias.checked;
control.reverseZoom = ui.reverseZoom.checked;
control.dark = ui.dark.checked;
control.exportOcto = ui.exportOcto.checked;
control.exportGhost = ui.exportGhost.checked;
control.exportLocal = ui.exportLocal.checked;
control.exportThumb = ui.exportThumb.checked;
control.exportPreview = ui.exportPreview.checked;
control.healMesh = ui.healMesh.checked;
control.threaded = setThreaded(ui.threaded.checked);
control.assembly = ui.assembly.checked;
control.ortho = ui.ortho.checked;
control.devel = ui.devel.checked;
control.webGPU = ui.webGPU.checked;
space.view.setZoom(control.reverseZoom, control.zoomSpeed);
// platform.layout();
api.conf.save();
@ -904,7 +904,6 @@ function init_one() {
xany: $('mod-x-any'),
xsla: $('mod-x-sla'),
xlaser: $('mod-x-laser'),
local: $('mod-local'),
don8: $('mod-don8'),
any: $('mod-any'),
},
@ -1048,6 +1047,7 @@ function init_one() {
showSpeeds: newBoolean(LANG.op_sped_s, speedSave, {title:LANG.op_sped_l}),
shiny: newBoolean(LANG.op_shny_s, booleanSave, {title:LANG.op_shny_l, modes:FDM}),
lineType: newSelect(LANG.op_line_s, {title: LANG.op_line_l, action: lineTypeSave, modes:FDM}, "linetype"),
manifold: newBoolean(LANG.op_mani_s, booleanSave, {title: LANG.op_mani_l, modes:CAM}, "manifold"),
animesh: newSelect(LANG.op_anim_s, {title: LANG.op_anim_l, action: aniMeshSave, modes:CAM}, "animesh"),
units: newSelect(LANG.op_unit_s, {title: LANG.op_unit_l, action: unitsSave, modes:CAM}, "units"),
edgeangle: newInput(LANG.op_spoa_s, {title:LANG.op_spoa_l, convert:toFloat, size:3}),
@ -1058,8 +1058,6 @@ function init_one() {
spaceRandoX: newBoolean(LANG.op_spcx_s, booleanSave, {title:LANG.op_spcx_l, show:isBelt}),
spaceLayout: newInput(LANG.op_spcr_s, {title:LANG.op_spcr_l, convert:toFloat, size:3, units}),
_____: newGroup(LANG.xp_menu, $('prefs-xpo'), {inline: true}),
exportLocal: newBoolean(`Grid:Local`, booleanSave, {title:LANG.op_exgl_l}),
exportGhost: newBoolean(`Grid:Host`, booleanSave, {title:LANG.op_exgh_l}),
exportOcto: newBoolean(`OctoPrint`, booleanSave, {title:LANG.op_exop_l}),
exportThumb: newBoolean(`Thumbnail`, booleanSave, {modes:FDM}),
exportPreview: newBoolean(`Code Preview`, booleanSave),
@ -1068,6 +1066,7 @@ function init_one() {
healMesh: newBoolean(LANG.pt_heal_s, booleanSave, {title: LANG.pt_heal_l}),
threaded: newBoolean(LANG.pt_thrd_s, booleanSave, {title: LANG.pt_thrd_l, modes:THREED}),
assembly: newBoolean(LANG.pt_assy_s, booleanSave, {title: LANG.pt_assy_l, modes:THREED}),
webGPU: newBoolean(LANG.pt_wgpu_s, booleanSave, {title: LANG.pt_wgpu_l, modes:THREED}),
prefadd: uc.checkpoint($('prefs-add')),
@ -1635,20 +1634,23 @@ function init_two() {
space.platform.setZOff(0.05);
// restore UI state from settings
ui.antiAlias.checked = control.antiAlias;
ui.assembly.checked = control.assembly;
ui.autoLayout.checked = control.autoLayout;
ui.autoSave.checked = control.autoSave;
ui.devel.checked = control.devel;
ui.freeLayout.checked = control.freeLayout;
ui.healMesh.checked = control.healMesh;
ui.manifold.checked = control.manifold;
ui.ortho.checked = control.ortho;
ui.reverseZoom.checked = control.reverseZoom;
ui.showOrigin.checked = control.showOrigin;
ui.showRulers.checked = control.showRulers;
ui.showSpeeds.checked = control.showSpeeds;
ui.freeLayout.checked = control.freeLayout;
ui.autoLayout.checked = control.autoLayout;
ui.spaceRandoX.checked = control.spaceRandoX;
ui.antiAlias.checked = control.antiAlias;
ui.reverseZoom.checked = control.reverseZoom;
ui.autoSave.checked = control.autoSave;
ui.healMesh.checked = control.healMesh;
ui.threaded.checked = setThreaded(control.threaded);
ui.assembly.checked = control.assembly;
ui.ortho.checked = control.ortho;
ui.devel.checked = control.devel;
ui.webGPU.checked = control.webGPU;
lineTypeSave();
detailSave();
updateStats();
@ -1789,6 +1791,7 @@ function init_two() {
$('context-duplicate').onclick = duplicateSelection;
$('context-mirror').onclick = mirrorSelection;
$('context-layflat').onclick = () => { api.event.emit("tool.mesh.lay-flat") };
$('context-lefty').onclick = () => { api.event.emit("tool.mesh.lefty") };
$('context-setfocus').onclick = () => {
api.event.emit(
"tool.camera.focus",

View file

@ -114,12 +114,6 @@ export const mode = {
}
};
export const probe = {
live: "https://live.grid.space",
grid: noop,
local: noop
};
export const process = {
code: currentProcessCode,
get: currentProcessName
@ -784,43 +778,7 @@ function showHelpFile(local,then) {
}
function showLocal() {
showModal('local');
api.probe.local((err,data) => {
let devc = 0;
let bind = [];
let html = ['<table>'];
html.push(`<thead><tr><th>device</th><th>type</th><th>status</th><th></th></tr></thead>`);
html.push(`<tbody>`);
let recs = [];
for (let k in data) {
recs.push(data[k].stat);
}
recs.sort((a,b) => {
return a.device.name < b.device.name ? -1 : 1;
});
for (let r of recs) {
bind.push({uuid: r.device.uuid, host: r.device.addr[0], port: r.device.port});
html.push(`<tr>`);
html.push(`<td>${r.device.name}</td>`);
html.push(`<td>${r.device.mode}</td>`);
html.push(`<td>${r.state}</td>`);
html.push(`<td><button id="${r.device.uuid}">admin</button></td>`);
html.push(`</tr>`);
devc++;
}
html.push(`</tbody>`);
html.push(`</table>`);
if (devc) {
$('mod-local').innerHTML = html.join('');
} else {
$('mod-local').innerHTML = `<br><b>no local devices</b>`;
}
bind.forEach(rec => {
$(rec.uuid).onclick = () => {
window.open(`http://${rec.host}:${rec.port||4080}/`);
};
});
});
// deprecated
}
function setViewMode(mode) {

View file

@ -163,19 +163,19 @@ function platformUpdateTopZ() {
const wzmax = bounds.max.z;
const zdelta = process.camZOffset || 0;
switch (process.camZAnchor) {
case 'top':
widget.setTopZ(stock.z - zdelta);
break;
case 'middle':
if (widget.isIndexed) {
widget.setTopZ(zdelta);
} else {
widget.setTopZ(stock.z - (stock.z - wzmax) / 2 + zdelta);
}
break;
case 'bottom':
widget.setTopZ(wzmax + zdelta);
break;
case 'top':
widget.setTopZ(stock.z - zdelta, true);
break;
case 'middle':
if (widget.isIndexed) {
widget.setTopZ(zdelta, true);
} else {
widget.setTopZ(stock.z - (stock.z - wzmax) / 2 + zdelta, true);
}
break;
case 'bottom':
widget.setTopZ(wzmax + zdelta, true);
break;
}
} else {
widget.setTopZ(0);

View file

@ -56,8 +56,8 @@ class Print {
// drop duplicates (usually intruced by FDM bisections)
if (lastPoint && point && !arc && type !== 'lerp') {
// nested due to uglify confusing browser
const { x, y, z } = lastPoint;
if (point.x == x && point.y == y && point.z == z && lastEmit == emit) {
const { x, y, z, a } = lastPoint;
if (point.x == x && point.y == y && point.z == z && point.a == z && lastEmit == emit) {
return lastOut;
}
}
@ -129,6 +129,7 @@ class Print {
close = !options.open,
scarf = !poly.open ? (options.scarf ?? 0) : false,
tool = options.tool,
zmax = options.zmax,
last = startPoint,
first = true;

View file

@ -189,7 +189,7 @@ function rotate(x, y, z) {
space.update();
}
function merge() {
function merge({ deleteMerged = true }) {
let sel = widgets();
if (sel.length === 0) {
sel = api.widgets.all();
@ -207,7 +207,7 @@ function merge() {
}
});
let w = api.platform.load_verts([], obj, "merged");
api.platform.delete(sel);
if (deleteMerged) api.platform.delete(sel);
api.platform.select(w);
}
@ -234,7 +234,7 @@ function isolateBodies(){
if(bodies.length ==1){
api.platform.select(widget);
}else{
console.log(bodies,Array.from(bodies.entries()))
// console.log(bodies,Array.from(bodies.entries()))
for(let [i,verts] of bodies.entries()){
let w = api.platform.load_verts([], verts, `isolate ${i} of ${ widget.anno.file}`);
api.platform.select(w)

View file

@ -49,6 +49,24 @@ function onLayFlatSelect() {
// endIt();
}
function onLeftySelect() {
// Project the face normal onto the XY plane (ignore Z component)
let projectedNormal = new Vector3(lastface.normal.x, lastface.normal.y, 0).normalize();
// Target direction on XY plane (negative Y axis)
let targetDir = new Vector3(0, -1, 0);
// Calculate angle between projected normal and target
let angle = Math.atan2(
projectedNormal.x * targetDir.y - projectedNormal.y * targetDir.x, // cross product Z component
projectedNormal.x * targetDir.x + projectedNormal.y * targetDir.y // dot product
);
// Create quaternion for Z-axis rotation only
let q = new Quaternion().setFromAxisAngle(ZAXIS, angle);
api.selection.rotate(q);
}
function onFaceUpSelect() {
api.event.emit('tool.mesh.face-normal', lastface.normal);
// endIt();
@ -105,6 +123,12 @@ api.event.on('tool.mesh.lay-flat', () => {
startIt();
});
api.event.on('tool.mesh.lefty', () => {
opName = 'align y axis';
onDone = onLeftySelect;
startIt();
});
function scale() {
let cam = space.internals().camera;
let dist = cam.position.distanceTo(pmesh.position);

View file

@ -473,10 +473,12 @@ class Widget {
/**
* moves top of widget to given Z
* only non-zero in CAM mode
*
* @param {number} z position
* @param {boolean} cam mode
*/
setTopZ(z) {
setTopZ(z, cam) {
let mesh = this.mesh,
track = this.track,
mbb = mesh.getBoundingBox(),
@ -488,7 +490,7 @@ class Widget {
track.top = mbz;
}
// difference between top of stock/bounds and top of widget (cam mode)
track.tzoff = mbz - z;
track.tzoff = cam ? mbz - z : 0;
this._updateMeshPosition();
}
@ -503,8 +505,7 @@ class Widget {
}
_move(x, y, z, abs) {
let mesh = this.mesh,
mat = this.getMaterial(),
let mat = this.getMaterial(),
pos = this.track.pos;
// do not allow moves in pure slice view
if (!mat.visible) return;
@ -528,13 +529,10 @@ class Widget {
track = this.track,
tzoff = track.tzoff,
top = track.top,
tz = tzoff !== top ? -tzoff : 0,
tz = -tzoff,
{ x, y, z } = track.pos;
if (track.indexed) {
let rad = track.indexRad;
let ln = track.box.d / 2;
let dx = Math.sin(rad) * ln;
let dy = ln - Math.cos(rad) * ln;
this.mesh.rotation.x = -rad;
z = top;
} else {
@ -782,9 +780,11 @@ class Widget {
}
if (set) {
let dark = this.api.space.is_dark();
let cam = this.api.mode.is_cam();
let color = dark && !cam ? 0xffffff : 0;
let angle = this.api.conf.get().controller.edgeangle || 20;
let edges = new THREE.EdgesGeometry(mesh.geometry, angle);
let material = new THREE.LineBasicMaterial({ color: 0 });
let material = new THREE.LineBasicMaterial({ color });
this.outline = new THREE.LineSegments(edges, material);
this.outline.renderOrder = -20;
mesh.add(this.outline);

View file

@ -5,38 +5,37 @@
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,
animateClear = false,
animating = false,
renderDone = false,
renderSteps = 0,
renderSpeed = 0,
indexCount = 0,
updates = 0,
maxR;
export function init(worker) {
const { dispatch } = worker;
dispatch.animate_setup2 = function (data, send) {
const { settings } = data;
const { process } = settings;
const { controller, process } = settings;
const print = worker.current.print;
const density = parseInt(settings.controller.animesh) * 1000;
const isIndexed = process.camStockIndexed;
@ -45,8 +44,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,25 +53,27 @@ 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 (controller.manifold)
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);
stockSlices.push(slice);
slice.updateMesh([]);
slice.send(send);
// send({ mesh_move: { id: slice.id, pos: { x:0, y:0, z: stock.z/2} } });
// send.data({ mesh_move: { id: slice.id, pos: { x:0, y:0, z: stock.z/2} } });
}
send.done();
};
dispatch.animate2 = function (data, send) {
renderPause = data.pause || renderPause;
renderSpeed = data.speed || 0;
if (animating) {
return send.done();
@ -126,7 +125,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 +141,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, da, 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,26 +201,25 @@ 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, da, sp, md } = pos;
toolMove(pos);
// console.log('renderMoves', {id, moves, seed});
let subs = 0;
if (dx || dy || dz < 0)
if (dx || dy || dz < 0 || da)
for (let slice of stockSlices) {
if (slice.bounds.intersectsBox(toolMesh.bounds)) {
slice.subtractTool(toolMesh);
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,29 +10,35 @@ 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,
dark = false,
manifold = false,
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) {
let alert = api.alerts.show("building animation");
let settings = api.conf.get();
dark = settings.controller.dark;
manifold = settings.controller.manifold;
client.animate_setup2(settings, data => {
handleUpdate(data);
if (data) {
@ -48,7 +54,8 @@ export function animate2(api, delay) {
speed: anim.speed,
trans: anim.trans,
model: anim.model,
shade: anim.shade
shade: anim.shade,
path: anim.path
});
Object.assign(label, {
progress: anim.progress,
@ -59,6 +66,14 @@ export function animate2(api, delay) {
a: anim.vala
});
if (manifold) {
anim.trans.classList.remove('hide');
// anim.shade.classList.remove('hide');
} else {
anim.trans.classList.add('hide');
// anim.shade.classList.add('hide');
}
updateSpeed(0);
setTimeout(step, delay || 0);
toggleTrans(undefined, false);
@ -72,6 +87,7 @@ export function animate2(api, delay) {
button.trans.onclick = toggleTrans;
button.model.onclick = toggleModel;
button.shade.onclick = toggleStock;
button.path.onclick = togglePath;
button.play.style.display = '';
button.pause.style.display = 'none';
@ -86,8 +102,9 @@ Object.assign(client, {
},
animate_setup2(settings, ondone) {
color = settings.controller.dark ? 0x888888 : 0;
material = new THREE.MeshPhongMaterial({
initPathMesh();
color = dark ? 0x888888 : 0;
material = material ?? new THREE.MeshPhongMaterial({
flatShading: true,
transparent: false,
opacity: 0.5,
@ -102,6 +119,73 @@ 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: dark ? 0xffff00 : 0x771100 }),
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),
show = api.local.get('cam.anim.path') ?? true;
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 (show && bool !== lines.visible) {
lines.visible = bool;
} else if (!show) {
lines.visible = false;
}
},
show(bool) {
show = bool;
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 +216,7 @@ function meshUpdate(id, ind, pos, ilen, plen) {
function deleteMesh(id) {
space.world.remove(meshes[id]);
meshes[id].geometry.dispose();
delete meshes[id];
}
@ -141,8 +226,15 @@ function toggleModel(ev, bool) {
}
function toggleStock(ev, bool, set) {
set !== false && api.local.toggle('cam.anim.stock', bool);
return api.event.emit('cam.stock.toggle', bool ?? undefined);
bool = api.local.toggle('cam.anim.stock', bool);
for (let [ id, mesh ] of Object.entries(meshes)) {
if (id >= 0) mesh.visible = bool;
}
}
function togglePath(ev, bool, set) {
bool = api.local.toggle('cam.anim.path', bool);
lineTracker?.show(bool);
}
function toggleTrans(ev, bool) {
@ -165,8 +257,7 @@ function play(opts) {
}
client.animate2({
speed,
steps: steps || Infinity,
pause: speedPauses[speedIndex]
steps: steps || Infinity
}, handleUpdate);
}
@ -177,8 +268,7 @@ function fast(opts) {
button.pause.style.display = '';
client.animate2({
speed,
steps: steps || Infinity,
pause: speedPauses[speedIndex]
steps: steps || Infinity
}, handleUpdate);
}
@ -216,6 +306,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 +320,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;
@ -206,7 +207,7 @@ export function createPopOps() {
sep: UC.newBlank({ class: "pop-sep" }),
spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: UC.toInt, show: hasSpindle }),
step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: UC.toFloat, bound: UC.bound(0.01, 1.0) }),
stepz: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: UC.toFloat, bound: UC.bound(0, 100.0) }),
stepz: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: UC.toFloat, units: true, bound: UC.bound(0, 100.0) }),
rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: UC.toInt, units: true }),
down: UC.newInput(LANG.cc_loff_s, { title: LANG.cc_loff_l, convert: UC.toFloat, units: true }),
inset: UC.newInput(LANG.cc_lxyo_s, { title: LANG.cc_lxyo_l, convert: UC.toFloat, units: true, show: () => !env.popOp.level.rec.stock }),
@ -282,7 +283,7 @@ export function createPopOps() {
plunge: UC.newInput(LANG.cc_plng_s, { title: LANG.cc_plng_l, convert: UC.toInt, units: true }),
down: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: UC.toFloat, units: true }),
step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: UC.toFloat, bound: UC.bound(0.01, 1.0), show: () => env.popOp.outline.rec.wide }),
steps: UC.newInput(LANG.cc_sovc_s, { title: LANG.cc_sovc_l, convert: UC.toInt, bound: UC.bound(1, 5), show: () => env.popOp.outline.rec.wide }),
steps: UC.newInput(LANG.cc_sovc_s, { title: LANG.cc_sovc_l, convert: UC.toInt, bound: UC.bound(1, 500), show: () => env.popOp.outline.rec.wide }),
sep: UC.newBlank({ class: "pop-sep" }),
top: UC.newBoolean(LANG.co_clrt_s, undefined, { title: LANG.co_clrt_l }),
inside: UC.newBoolean(LANG.co_olin_s, undefined, { title: LANG.co_olin_l, show: (op) => { return !op.inputs.outside.checked } }),
@ -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

@ -140,8 +140,8 @@ function pointHoverUp(int, ev) {
x += stock.x / 2;
y += stock.y / 2;
}
process.camOriginOffX = x;
process.camOriginOffY = y;
process.camOriginOffX = x - stock.center.x;
process.camOriginOffY = y - stock.center.y;
process.camOriginOffZ = z;
api.settings.update_fields(process);
api.platform.update_origin();

View file

@ -144,7 +144,7 @@ export function traceHoverUp(int, ev) {
export function traceToggle(obj, skip) {
let material = obj.material[0] || obj.material;
if (!material) return;
if (!(material && obj.trace)) return;
let { color, colorSave } = material;
let { widget, poly } = obj.trace;
let areas = env.poppedRec.areas;

View file

@ -10,7 +10,7 @@ import { Layers } from '../../core/layers.js';
import { Stack } from '../../core/stack.js';
import { updateStock } from './cl-stock.js';
import { createPopOps } from './cl-ops.js';
import { tabAdd, tabDone, tabClear, restoreTabs, rotateTabs, updateTabs, clearTabs } from './cl-tab.js';
import { tabAdd, tabDone, tabClear, restoreTabs, rotateTabs, updateTabs, clearTabs, mirrorTabs } from './cl-tab.js';
import { traceOn, traceDone, unselectTraces } from './cl-trace.js';
import { holeSelOn, selectHolesDone, clearHolesRec } from './cl-hole.js';
import { surfaceOn, surfaceDone } from './cl-surface.js';
@ -29,6 +29,7 @@ const { widgets: WIDGETS } = api;
class Client {
animVer = 0;
camStock;
showStock = true;
current;
currentIndex;
flipping;
@ -320,6 +321,7 @@ export function opRender() {
el.classList.add("disabled");
}
bounds.push(el);
let parent = $('camops');
let timer = null;
let inside = true;
let popped = false;
@ -329,14 +331,14 @@ export function opRender() {
}
el.rec = rec;
el.unpop = () => {
let pos = [...el.childNodes].indexOf(poprec.div);
let pos = [...parent.childNodes].indexOf(poprec.div);
if (pos >= 0) {
el.removeChild(poprec.div);
parent.removeChild(poprec.div);
}
popped = false;
};
function onEnter(ev) {
if ((surfaceOn || traceOn) && env.poppedRec != rec) {
if ((clock || surfaceOn || traceOn) && env.poppedRec != rec) {
return;
}
if (popped && env.poppedRec != rec) {
@ -351,22 +353,24 @@ export function opRender() {
popped = true;
poprec.use(rec);
env.hoveredOp = el;
if (!clock) {
// offset Y position of pop div by % of Y screen location of button
el.appendChild(poprec.div);
poprec.addNote();
const { innerHeight } = window;
const brect = ev.target.getBoundingClientRect();
const prect = poprec.div.getBoundingClientRect();
const pcty = (brect.top / innerHeight) * 0.9;
const offpx = -pcty * prect.height;
poprec.div.style.transform = `translateY(${offpx}px)`;
}
// offset Y position of pop div by % of Y screen location of button
parent.appendChild(poprec.div);
poprec.addNote();
const { innerHeight } = window;
const brect = ev.target.getBoundingClientRect();
const prect = parent.getBoundingClientRect();
const Prect = poprec.div.getBoundingClientRect();
const tdiff = prect.top - brect.top;
const botoff = innerHeight - (brect.top + Prect.height);
const offpx = -tdiff + (botoff < 0 ? botoff : -Prect.height/3);
poprec.div.style.transform = `translateY(${offpx}px)`;
poprec.div.onmouseenter = () => { inside = true };
poprec.div.onmouseleave = onLeave;
// option click event appears latent
// and overides the sticky settings
setTimeout(() => {
UC.setSticky(false);
}, 0);
}, 10);
}
function onLeave(ev) {
inside = false;
@ -637,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

@ -12,10 +12,8 @@ export function cam_export(print, online) {
const widget = print.widgets[0];
if (!widget) return;
console.log({ cam_export: print });
const { settings } = print;
const { device, tools } = settings;
const { device } = settings;
let i,
time = 0,
@ -257,17 +255,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;
@ -424,7 +423,9 @@ export function cam_export(print, online) {
toolz[out.tool] = ctool;
}
point = out.point;
if (!point || point.mod) return;
if (!point || point.mod) {
return;
}
// ensure not point is modified twice
point.mod = 1;
if (isOffset) {

View file

@ -58,6 +58,7 @@ export function menu() {
anim.trans = newButton(null,null,{icon:'<i class="fa-solid fa-border-none"></i>',title:"transparency",class:"padleft"}),
anim.model = newButton(null,null,{icon:'<i class="fa-solid fa-eye"></i>',title:"show model"}),
anim.shade = newButton(null,null,{icon:'<i class="fa-solid fa-cube"></i>',title:"stock box"}),
anim.path = newButton(null,null,{icon:'<i class="fa-solid fa-draw-polygon"></i>',title:"path"}),
])
},
@ -105,7 +106,7 @@ export function menu() {
separator: newBlank({ class:"set-sep", driven }),
camEaseAngle: newInput(LANG.ou_eang_s, {title:LANG.ou_eang_l, convert:toFloat, bound:bound(0.1,85), show:() => ui.camEaseDown.checked}),
camFullEngage: newInput(LANG.ou_feng_s, {title:LANG.ou_feng_l, convert:toFloat, bound:bound(0.1,1.0)}),
_____: newGroup(LANG.co_menu, $('cam-origin'), { modes:CAM, driven, separator }),
_____: newGroup(LANG.or_menu, $('cam-origin'), { modes:CAM, driven, separator }),
camOriginTop: newBoolean(LANG.or_topp_s, onBooleanClick, {title:LANG.or_topp_l}),
camOriginCenter: newBoolean(LANG.or_cntr_s, onBooleanClick, {title:LANG.or_cntr_l}),
separator: newBlank({ class:"set-sep", driven }),

View file

@ -4,26 +4,70 @@ import { CamOp } from './op.js';
import { Tool } from './tool.js';
import { generate as Topo } from './topo3.js';
import { newPoint } from '../../../geo/point.js';
import { newPolygon } from '../../../geo/polygon.js';
import { tip2tipEmit } from '../../../geo/paths.js';
function createFilter(op) {
function createFilter(op, origin, axis) {
// console.log({ origin, axis });
let ok = () => true;
let filter = slices => slices;
let filterString = op.filter?.map(l => l.trim()).join('\n');
if (filterString) {
try {
const obj = eval(`( ${filterString} )`);
let idx = 0;
if (obj && obj.slices) {
const nadd = [];
filter = function (slices) {
for (let slice of slices) {
if (obj.slices(slice, idx++)) {
nadd.push(slice);
let box = obj?.box;
let slice_fn = obj?.slices;
let index = 0;
const accept = [];
filter = function (slices) {
for (let slice of slices) {
if (slice_fn && slice_fn(slice, index++)) {
accept.push(slice);
} else if (box) {
// slice.z = x when axis = y
// slice.z = y when axis = x
let { x, y, z } = box;
x = x ?? [ -Infinity, Infinity ];
y = y ?? [ -Infinity, Infinity ];
z = z ?? [ -Infinity, Infinity ];
let ok = false;
if (axis === 'x') {
let sy = slice.z + origin.y;
if (sy >= y[0] && sy <= y[1]) {
ok = true;
for (let p of slice.camLines) {
p.points = p.points.filter(p =>
p.x - origin.x >= x[0] && p.x - origin.x <= x[1] &&
p.z - origin.z >= z[0] && p.z - origin.z <= z[1]
);
}
}
} else {
let sx = slice.z - origin.x;
if (sx >= x[0] && sx <= x[1]) {
ok = true;
for (let p of slice.camLines) {
p.points = p.points.filter(p =>
p.y + origin.y >= y[0] && p.y + origin.y <= y[1] &&
p.z - origin.z >= z[0] && p.z - origin.z <= z[1]
);
}
}
}
if (ok) {
slice.camLines = slice.camLines.map(p => {
if (p.points.length > 1) {
return newPolygon(p.points).setOpen(true);
} else {
return undefined;
}
}).filter(p => p);
accept.push(slice);
}
}
return nadd;
};
}
}
return accept;
};
} catch (e) {
console.log('filter parse error', e, op.filter);
}
@ -38,8 +82,8 @@ class OpContour extends CamOp {
async slice(progress) {
let { op, state } = this;
let { addSlices, settings, updateToolDiams } = state;
let filter = createFilter(op);
let { color, addSlices, settings, updateToolDiams } = state;
let filter = createFilter(op, settings.origin, op.axis.toLowerCase());
let toolDiam = this.toolDiam = new Tool(settings, op.tool).fluteDiameter();
updateToolDiams(toolDiam);
// we need topo for safe travel moves when roughing and outlining
@ -54,6 +98,11 @@ class OpContour extends CamOp {
slices = filter(slices);
this.sliceOut = slices;
addSlices(slices);
for (let slice of slices) {
slice.output()
.setLayer(`contour ${op.axis}`, { face: color, line: color })
.addPolys(slice.camLines);
}
},
contour: op,
state: state
@ -64,11 +113,11 @@ class OpContour extends CamOp {
prepare(ops, progress) {
let { op, state, sliceOut } = this;
let { settings, widget } = state;
let { settings } = state;
let { process } = settings;
let { setTolerance, setTool, setSpindle, setPrintPoint } = ops;
let { camOut, polyEmit, newLayer, printPoint, lastPoint } = ops;
let { camOut, newLayer, printPoint, lastPoint } = ops;
let { bounds, zmax } = ops;
let toolDiam = this.toolDiam;
@ -87,9 +136,9 @@ class OpContour extends CamOp {
if (!slice.camLines) {
continue;
}
let polys = [], poly, emit;
let polys = [], poly;
slice.camLines.forEach(function (poly) {
if (depthFirst) poly = poly.clone(true);
if (depthFirst) poly = poly.clone(true).annotate({ slice: slice.index + 1 });
polys.push({ first: poly.first(), last: poly.last(), poly: poly });
});
if (depthFirst) {
@ -101,7 +150,7 @@ class OpContour extends CamOp {
poly.reverse();
}
poly.forEachPoint(function (point, pidx) {
camOut(point.clone(), pidx > 0, stepover);
camOut(point.clone(), pidx > 0, { moveLen: stepover });
}, false);
});
newLayer();
@ -115,7 +164,7 @@ class OpContour extends CamOp {
poly.reverse();
}
poly.forEachPoint(function (point, pidx) {
camOut(point.clone(), pidx > 0, stepover);
camOut(point.clone().annotate({ slice: poly.slice }), pidx > 0, { moveLen: stepover });
}, false);
newLayer();
return lastPoint();

View file

@ -14,8 +14,8 @@ class OpDrill extends CamOp {
async slice(progress) {
let { op, state } = this;
let { settings, addSlices, widget, updateToolDiams } = state;
let { zBottom, zThru, thruHoles, color } = state;
let { drills, drillThrough } = op
let { color } = state;
let { drills } = op
let drillTool = new Tool(settings, op.tool),
drillToolDiam = drillTool.fluteDiameter(),
@ -47,7 +47,7 @@ class OpDrill extends CamOp {
slice.camLines = [poly];
slice.output()
.setLayer("drill", { face: color, line: color })
.setLayer(state.layername, { face: color, line: color })
.addPolys(slice.camLines);
addSlices(slice);
sliceOut.push(slice);
@ -55,11 +55,10 @@ class OpDrill extends CamOp {
}
prepare(ops, progress) {
let { op, state } = this;
let { settings, widget, addSlices, updateToolDiams } = state;
let { op } = this;
let { setTool, setSpindle, setDrill, emitDrills } = ops;
setTool(op.tool, undefined, op.rate);
setDrill(op.down, op.lift, op.dwell, op.thru);
setDrill(op.down, op.lift, op.dwell);
setSpindle(op.spindle);
emitDrills(this.sliceOut.map(slice => slice.camLines).flat());
}

View file

@ -25,7 +25,7 @@ export class OpIndex extends CamOp {
}
prepare(ops, progress) {
const { lastPoint, zmax, zclear, camOut, stock } = ops;
const { lastPoint, zmax, zclear, camOut, stock, newLayer } = ops;
let last = lastPoint();
if (last) {
// max point of stock corner radius when rotating (safe z when indexing)
@ -34,7 +34,7 @@ export class OpIndex extends CamOp {
// move above rotating stock
camOut(last = last.clone().setZ(zmove), 0);
// issue rotation command
camOut(last = last.clone().setY(0).setA(this.degrees), 0);
camOut(last = last.clone().setZ(zmove).setA(this.degrees), 0);
}
}
}

View file

@ -6,22 +6,23 @@ import { generate as topo4_generate } from './topo4.js';
import { newPoint } from '../../../geo/point.js';
function createFilter(op) {
let ok = () => true;
let filter = slices => slices;
if (op.filter) {
let filterString = op.filter?.map(l => l.trim()).join('\n');
if (filterString) {
try {
const obj = eval(`( ${op.filter.join('\n')} )`);
let idx = 0;
if (obj && obj.slices) {
const nadd = [];
filter = function (slices) {
for (let slice of slices) {
if (obj.slices(slice, idx++)) {
nadd.push(slice);
}
const obj = eval(`( ${filterString} )`);
const accept = [];
let index = 0;
let sl_fn = obj?.slices ?? ok;
filter = function (slices) {
for (let slice of slices) {
if (sl_fn(slice, index++)) {
accept.push(slice);
}
return nadd;
};
}
}
return accept;
};
} catch (e) {
console.log('filter parse error', e, op.filter);
}
@ -36,9 +37,8 @@ class OpLathe extends CamOp {
async slice(progress) {
let { op, state } = this;
let { addSlices } = state;
let { addSlices, color } = state;
let filter = createFilter(op);
this.topo = await topo4_generate({
op,
state,

View file

@ -28,10 +28,10 @@ class OpLevel extends CamOp {
let points = [];
let clear = op.stock ?
[ newPolygon().centerRectangle({x:0,y:0,z:0}, stock.x, stock.y) ] :
[ newPolygon().centerRectangle({x:0,y:0,z:0}, stock.x + toolDiam/2, stock.y) ] :
POLY.outer(POLY.offset(tshadow, toolDiam * (op.inset || 0)));
POLY.fillArea(clear, 90, stepOver, points);
POLY.fillArea(clear, 1090, stepOver, points);
let layers = this.layers = [];
for (let z of zList) {

View file

@ -17,9 +17,11 @@ class OpOutline extends CamOp {
async slice(progress) {
let { op, state } = this;
let { settings, widget, slicer, addSlices, tshadow, thruHoles, unsafe, color } = state;
let { settings, slicer, addSlices, tshadow, thruHoles, unsafe, color, widget } = state;
let { updateToolDiams, tabs, cutTabs, cutPolys, workarea, zMax, shadowAt } = state;
let { process, stock } = settings;
let { controller } = settings;
let center_off = widget.track.pos ?? { x: 0, y: 0, z: 0};
if (op.down <= 0) {
throw `invalid step down "${op.down}"`;
@ -39,6 +41,8 @@ class OpOutline extends CamOp {
let indices = slicer.interval(op.down, intopt);
let trueShadow = process.camTrueShadow === true;
let lastShadowZ;
let stockClip;
// shift out first (top-most) slice
indices.shift();
// add flats to shadow
@ -168,16 +172,12 @@ class OpOutline extends CamOp {
let stepover = toolDiam * op.step;
let wideCuts = [] //accumulator for wide cuts
for (let c = (op.steps || 1); c > 0; c--){
offset.slice().forEach(op => {
// clone removes inners but the real solution is
// to limit expanded shells to through holes
let wideCut = POLY.expand([op.clone(true)], stepover*c, slice.z, [], 1);
wideCut.forEach(cut =>{ //set order of cuts when wide
cut.order = c
if(cut.inner) cut.inner.forEach(inn =>{ inn.order = c })
});
wideCuts.push(...wideCut)
let wideCut = POLY.expand(offset.clone(true), stepover * c, slice.z, [], 1);
wideCut.forEach(cut =>{ //set order of cuts when wide
cut.order = c
if(cut.inner) cut.inner.forEach(inn =>{ inn.order = c })
});
wideCuts.push(...wideCut)
}
offset.appendAll(wideCuts);
}
@ -187,16 +187,19 @@ class OpOutline extends CamOp {
addDogbones(offset, toolDiam / 5);
}
if (process.camStockClipTo && stock.x && stock.y && stock.center) {
let { center } = stock;
let x = center.x - center_off.x;
let y = center.y - center_off.y;
stockClip = newPolygon().centerRectangle({ x, y }, stock.x + 0.001, stock.y + 0.001);
offset = cutPolys([stockClip], offset, slice.z, true);
}
if (tabs) {
tabs.forEach(tab => {
tab.off = POLY.expand([tab.poly], toolDiam / 2).flat();
});
offset = cutTabs(tabs, offset, slice.z);
}
if (process.camStockClipTo && stock.x && stock.y && stock.center) {
let rect = newPolygon().centerRectangle({x:0,y:0}, stock.x, stock.y);
offset = cutPolys([rect], offset, slice.z, true);
offset = cutTabs(tabs, offset);
}
// offset.xout(`slice ${slice.z}`);
@ -208,11 +211,17 @@ class OpOutline extends CamOp {
// project empty up and render
for (let slice of slices) {
if (false) slice.output()
if (controller.devel) slice.output()
.setLayer("slice", {line: 0xaaaa00}, false)
.addPolys(slice.topPolys())
if (controller.devel) slice.output()
.setLayer("shadow", {line: 0x557799}, false)
.addPolys(slice.shadow)
if (controller.devel && stockClip) slice.output()
.setLayer("stock clip", {line: 0x557799}, false)
.addPolys([ stockClip ])
slice.output()
.setLayer("outline", {face: color, line: color})
.setLayer(state.layername, {face: color, line: color})
.addPolys(slice.camLines);
}
@ -224,9 +233,9 @@ class OpOutline extends CamOp {
let { op, state, sliceOut } = this;
let { setTool, setSpindle, setPrintPoint } = ops;
let { polyEmit, depthOutlinePath } = ops;
let { camOut, newLayer, printPoint } = ops;
let { settings, widget } = state;
let { process, controller } = settings;
let { newLayer, printPoint } = ops;
let { settings } = state;
let { process } = settings;
let easeDown = process.camEaseDown;
let toolDiam = this.toolDiam;

View file

@ -19,13 +19,11 @@ class OpPocket extends CamOp {
async slice(progress) {
const pocket = this;
const debug = false;
let { op, state } = this;
let { tool, rate, down, plunge, expand, contour, smooth, tolerance } = op;
let { ov_botz, ov_conv } = op;
let { settings, widget, addSlices, zBottom, zThru, tabs, color } = state;
let { settings, widget, addSlices, zBottom, tabs, color } = state;
let { updateToolDiams, cutTabs, healPolys, shadowAt, workarea } = state;
let { process } = settings;
zBottom = ov_botz ? workarea.bottom_stock + ov_botz : zBottom;
// generate tracing offsets from chosen features
let sliceOut;
@ -36,24 +34,18 @@ class OpPocket extends CamOp {
let cutdir = ov_conv;
let engrave = contour && op.engrave;
let zTop = workarea.top_z;
let devel = settings.controller.devel;
let smoothVal = (smooth ?? 0) / 10;
if (contour) {
down = 0;
this.topo = await Topo({
// onupdate: (update, msg) => {
onupdate: (index, total, msg) => {
progress((index / total) * 0.9, msg);
},
ondone: (slices) => {
// console.log({ contour: slices });
},
contour: {
tool,
tolerance,
inside: true,
axis: "-"
},
state: state
});
this.contour = {
axis: "-",
inside: true,
nogpu: true,
step: toolOver,
tolerance,
tool,
};
}
updateToolDiams(toolDiam);
if (tabs) {
@ -69,7 +61,7 @@ class OpPocket extends CamOp {
sliceOut.push(slice);
return slice;
}
function clearZ(polys, z, down) {
async function clearZ(polys, z, down) {
if (down) {
// adjust step down to a value <= down that
// ends on the lowest z specified
@ -92,14 +84,14 @@ class OpPocket extends CamOp {
let clip = [], shadow;
if (contour) {
if (smooth) {
clip = POLY.offset(POLY.offset([ poly ], smooth), -smooth);
clip = POLY.offset(POLY.offset([ poly ], smoothVal), -smoothVal);
} else {
clip = [ poly ];
}
} else {
shadow = shadowAt(z);
if (smooth) {
shadow = POLY.setZ(POLY.offset(POLY.offset(shadow, smooth), -smooth), z);
shadow = POLY.setZ(POLY.offset(POLY.offset(shadow, smoothVal), -smoothVal), z);
}
POLY.subtract([ poly ], shadow, clip, undefined, undefined, 0);
if (op.outline) {
@ -130,16 +122,16 @@ class OpPocket extends CamOp {
}
POLY.setWinding(slice.camLines, cutdir, false);
if (contour) {
slice.camLines = pocket.conform(slice.camLines, op.refine, engrave, pct => {
slice.camLines = await pocket.conform(slice.camLines, op.refine, engrave, pct => {
progress(0.9 + (zpro + zinc * pct) * 0.1, "conform");
});
}
slice.output()
.setLayer("pocket", {line: color}, false)
.setLayer(state.layername, {line: color}, false)
.addPolys(slice.camLines)
if (debug && shadow) slice.output()
if (devel && shadow) slice.output()
.setLayer("pocket shadow", {line: 0xff8811}, false)
.addPolys(shadow)
.addPolys(shadow);
if (!contour) {
progress(zpro, "pocket");
}
@ -168,23 +160,37 @@ class OpPocket extends CamOp {
outline = POLY.setWinding(outline, cutdir, false);
outline = healPolys(outline);
if (smooth) {
outline = POLY.offset(POLY.offset(outline, smooth), -smooth);
outline = POLY.offset(POLY.offset(outline, smoothVal), -smoothVal);
}
if (outline.length) {
// option to skip interior features (holes, pillars)
if (op.outline) {
POLY.clearInner(outline);
}
if (debug) newSliceOut(zmin).output()
await clearZ(outline, zmin + 0.0001, down);
if (devel && sliceOut?.length) sliceOut[0].output()
.setLayer("pocket area", {line: 0x1188ff}, false)
.addPolys(outline)
clearZ(outline, zmin + 0.0001, down);
progress(1, "pocket");
}
}
// mold cam output lines to the surface of the topo offset by tool geometry
conform(camLines, refine, engrave, progress) {
async conform(camLines, refine, engrave, progress) {
if (!this.topo) {
console.log('deferred topo');
this.topo = await Topo({
// onupdate: (update, msg) => {
onupdate: (index, total, msg) => {
progress((index / total) * 0.9, msg);
},
ondone: (slices) => {
// console.log({ contour: slices });
},
contour: this.contour,
state: this.state
});
}
const topo = this.topo;
// re-segment polygon to a higher resolution
const hirez = camLines.map(p => p.segment(topo.tolerance * 2));
@ -236,7 +242,7 @@ class OpPocket extends CamOp {
prepare(ops, progress) {
let { op, state, pockets } = this;
let { setTool, setSpindle, setTolerance, sliceOutput, getPrintPoint } = ops;
let { getPrintPoint , pocket, setTool, setSpindle, setTolerance } = ops;
let { process } = state.settings;
setTool(op.tool, op.rate);
@ -269,11 +275,12 @@ class OpPocket extends CamOp {
}
if (min.pocket) {
min.pocket.used = true;
sliceOutput(min.pocket, {
pocket({
cutdir: op.ov_conv,
depthFirst: process.camDepthFirst && !state.isIndexed,
easeDown: op.down && process.easeDown ? op.down : 0,
progress: (n,m) => progress(n/m, "pocket")
progress: (n,m) => progress(n/m, "pocket"),
slices: min.pocket
});
} else {
break;

View file

@ -19,6 +19,7 @@ class OpRegister extends CamOp {
let tool = new Tool(settings, op.tool);
let sliceOut = this.sliceOut = [];
let axis = op.axis.toLowerCase();
updateToolDiams(tool.fluteDiameter());
@ -42,7 +43,7 @@ class OpRegister extends CamOp {
return;
}
switch (op.axis.toLowerCase()) {
switch (axis) {
case "x":
if (op.points == 3) {
pathPoints.push(newPoint(boundMinX - cutOffset, centerY, 0));
@ -64,6 +65,7 @@ class OpRegister extends CamOp {
}
break;
case "-":
case "=":
let halfOffset = toolOffset / 2,
loopMinX = boundMinX - cutOffset,
loopMaxX = boundMaxX + cutOffset,
@ -84,7 +86,7 @@ class OpRegister extends CamOp {
}
function rept(count, step, fn) {
while (count-- > 0) {
fn(step, count === 0);
fn(step, count);
step = -step;
}
}
@ -93,9 +95,16 @@ class OpRegister extends CamOp {
addSlices(slice);
sliceOut.push(slice);
start(z);
rept(4, halfOffset, oy => {
rept(4, halfOffset, (oy, count) => {
move(0, -oy);
move(deltaX, 0);
if (axis === '=' && count % 2 === 1) {
move(deltaX/2, 0);
move(0, oy/2);
move(0, -oy/2);
move(deltaX/2, 0);
} else {
move(deltaX, 0);
}
});
rept(4, halfOffset, ox => {
move(ox, 0);
@ -136,11 +145,10 @@ class OpRegister extends CamOp {
}
prepare(ops, progress) {
let { op, state } = this;
let { settings, widget, addSlices, updateToolDiams } = state;
let { op } = this;
let { setTool, setSpindle, setDrill, emitDrills } = ops;
if (op.axis === '-') {
if (op.axis === '-' || op.axis === '=') {
setTool(op.tool, op.feed, op.rate);
setSpindle(op.spindle);
for (let slice of this.sliceOut) {

View file

@ -15,11 +15,12 @@ class OpRough extends CamOp {
async slice(progress) {
let { op, state } = this;
let { settings, slicer, addSlices, unsafe, color } = state;
let { settings, slicer, addSlices, unsafe, color, widget } = state;
let { updateToolDiams, thruHoles, tabs, cutTabs, cutPolys } = state;
let { ztOff, zMax, shadowAt, isIndexed} = state;
let { workarea } = state;
let { process, stock } = settings;
let { controller, process, stock } = settings;
let center_off = widget.track.pos ?? { x: 0, y: 0, z: 0};
if (op.down <= 0) {
throw `invalid step down "${op.down}"`;
@ -36,6 +37,12 @@ class OpRough extends CamOp {
updateToolDiams(toolDiam);
if (tabs) {
tabs.forEach(tab => {
tab.off = POLY.expand([tab.poly], toolDiam / 2).flat();
});
}
// clear the stock above the area to be roughed out
if (workarea.top_z > workarea.top_part && !cutThruBypass) {
let shadow = state.shadow.base.clone();
@ -58,14 +65,11 @@ class OpRough extends CamOp {
slice.z = z;
let polys = POLY.setZ(facing.clone(true), slice.z + roughLeaveZ);
if (tabs) {
tabs.forEach(tab => {
tab.off = POLY.expand([tab.poly], toolDiam / 2).flat();
});
polys = cutTabs(tabs, polys, slice.z);
}
slice.camLines = polys;
slice.output()
.setLayer("roughing", {face: color, line: color})
.setLayer(state.layername, {face: color, line: color})
.addPolys(slice.camLines);
addSlices(slice);
camFaces.push(slice);
@ -206,20 +210,6 @@ class OpRough extends CamOp {
}
}
if (tabs) {
tabs.forEach(tab => {
tab.off = POLY.expand([tab.poly], toolDiam / 2).flat();
});
offset = cutTabs(tabs, offset, slice.z);
}
if (!offset) return;
if (process.camStockClipTo && stock.x && stock.y && stock.center) {
let rect = newPolygon().centerRectangle(stock.center, stock.x, stock.y);
offset = cutPolys([rect], offset, slice.z, true);
}
// elimate double inset on inners
offset.forEach(op => {
if (op.inner) {
@ -236,24 +226,38 @@ class OpRough extends CamOp {
}
});
if (process.camStockClipTo && stock.x && stock.y && stock.center) {
let { center } = stock;
let x = center.x - center_off.x;
let y = center.y - center_off.y;
let rect = newPolygon().centerRectangle({ x, y }, stock.x + 0.001, stock.y + 0.001);
offset = cutPolys([rect], offset, slice.z, true);
}
if (!offset) return;
slice.camLines = offset;
if (roughLeaveZ) {
// offset roughing in Z as well to minimize
// tool marks on curved surfaces
// offset roughing in Z as well to minimize tool marks on curved surfaces
// const roughLeaveZ = 1 * Math.min(roughDown, roughLeave / 2);
slice.camLines.forEach(p => {
p.setZ(p.getZ() + roughLeaveZ);
});
for (let poly of slice.camLines) {
for (let point of poly.points) point.z += roughLeaveZ;
}
}
if (controller.devel) {
if (tabs) slice.output()
.setLayer("tabs clip", {line: 0xaa00aa}, true)
.addPolys(tabs.map(tab => tab.off).flat());
slice.output()
.setLayer("slice", {line: 0xaaaa00}, true)
.addPolys(slice.topPolys())
// .setLayer("top shadow", {line: 0x0000aa})
// .addPolys(tshadow)
// .setLayer("rough shadow", {line: 0x00aa00})
// .addPolys(shadow)
.setLayer("shadow clip", {line: 0xaa0000})
.addPolys(shell);
}
if (false) slice.output()
.setLayer("slice", {line: 0xaaaa00}, true)
.addPolys(slice.topPolys())
// .setLayer("top shadow", {line: 0x0000aa})
// .addPolys(tshadow)
// .setLayer("rough shadow", {line: 0x00aa00})
// .addPolys(shadow)
.setLayer("rough shell", {line: 0xaa0000})
.addPolys(shell);
progress(0.5 + 0.5 * (index / slices.length));
});
@ -278,8 +282,11 @@ class OpRough extends CamOp {
}
slices.forEach(slice => {
if (slice.camLines && tabs) {
slice.camLines = cutTabs(tabs, slice.camLines);
}
slice.output()
.setLayer("roughing", {face: color, line: color})
.setLayer(state.layername, {face: color, line: color})
.addPolys(slice.camLines);
});
this.sliceOut = slices.filter(slice => slice.camLines);
@ -289,15 +296,13 @@ class OpRough extends CamOp {
prepare(ops, progress) {
let { op, state, sliceOut, camFaces } = this;
let { setTool, setSpindle, setPrintPoint, sliceOutput, polyEmit } = ops;
let { camOut, newLayer, printPoint } = ops;
let { setTool, setSpindle, setPrintPoint, pocket, polyEmit, newLayer, printPoint } = ops;
let { settings } = state;
let { process } = settings;
let easeDown = process.camEaseDown;
let cutdir = op.ov_conv;
let depthFirst = process.camDepthFirst && !state.isIndexed;
let depthData = [];
setTool(op.tool, op.rate, op.plunge);
setSpindle(op.spindle);
@ -325,11 +330,12 @@ class OpRough extends CamOp {
// output the roughing passes
setPrintPoint(printPoint);
sliceOutput(sliceOut, {
pocket({
cutdir,
depthFirst,
easeDown: op.down && easeDown ? 0.001 : 0,
progress: (n,m) => progress(n/m, "routing")
progress: (n,m) => progress(n/m, "routing"),
slices: sliceOut
});
}
}

View file

@ -92,7 +92,7 @@ class OpTrace extends CamOp {
}
POLY.setWinding(slice.camLines, offset === "outside" ? !cutdir : cutdir, false);
slice.output()
.setLayer("trace follow", {line: color}, false)
.setLayer(state.layername, {line: color}, false)
.addPolys(slice.camLines)
}
function clearZnew(polys, z, down) {
@ -154,7 +154,7 @@ class OpTrace extends CamOp {
.setLayer("trace poly", {line: 0x1188ff}, false)
.addPolys([ poly ])
slice.output()
.setLayer("trace", {line: color}, false)
.setLayer(state.layername, {line: color}, false)
.addPolys(slice.camLines)
progress(zpro, "trace");
zpro += zinc;
@ -295,7 +295,6 @@ class OpTrace extends CamOp {
prepare(ops, progress) {
let { op, state } = this;
let { settings } = state;
let { setTool, setSpindle } = ops;
setTool(op.tool, op.rate);

View file

@ -56,8 +56,7 @@ export function prepEach(widget, settings, print, firstPoint, update) {
if (widget.camops.length === 0 || widget.meta.disabled) return;
let device = settings.device,
process = settings.process,
let { device, process } = settings,
isIndexed = process.camStockIndexed,
startCenter = process.camOriginCenter,
alignTop = settings.controller.alignTop,
@ -65,7 +64,6 @@ export function prepEach(widget, settings, print, firstPoint, update) {
stockz = stock.z * (isIndexed ? 0.5 : 1),
outer = settings.bounds || widget.getPositionBox(),
outerz = outer.max.z,
slices = widget.slices,
zclear = (process.camZClearance || 1),
zmax_force = process.camForceZMax || false,
zmax_outer = stockz + zclear,
@ -75,13 +73,13 @@ export function prepEach(widget, settings, print, firstPoint, update) {
boundsz = isIndexed ? stock.z / 2 : bounds.max.z + ztOff,
zadd = !isIndexed ? stock.z - boundsz : alignTop ? outerz - boundsz : 0,
zmax = outerz + zclear + (process.camOriginOffZ || 0),
zsafe = isIndexed ? Math.hypot(bounds.dim.y, bounds.dim.z) / 2 + zclear : zmax,
wmpos = widget.track.pos,
wmx = wmpos.x,
wmy = wmpos.y,
originx = (startCenter ? 0 : -stock.x / 2) + (process.camOriginOffX || 0),
originy = (startCenter ? 0 : -stock.y / 2) + (process.camOriginOffY || 0),
origin = newPoint(originx + wmx, originy + wmy, zmax),
output = print.output,
easeDown = process.camEaseDown,
easeAngle = process.camEaseAngle,
depthFirst = process.camDepthFirst,
@ -94,13 +92,11 @@ export function prepEach(widget, settings, print, firstPoint, update) {
drillDown = 0,
drillLift = 0,
drillDwell = 0,
drillThru = 0,
lasering = false,
laserPower = 0,
newOutput = print.output || [],
layerOut = [],
printPoint,
isNewMode,
isPocket,
isContour,
isRough,
@ -112,6 +108,7 @@ export function prepEach(widget, settings, print, firstPoint, update) {
toolDiamMove,
plungeRate = process.camFastFeedZ,
feedRate,
lastOp,
lastTool,
lastPush,
lastPoint,
@ -187,11 +184,10 @@ export function prepEach(widget, settings, print, firstPoint, update) {
laserPower = power;
}
function setDrill(down, lift, dwell, thru) {
function setDrill(down, lift, dwell) {
drillDown = down;
drillLift = lift;
drillDwell = dwell;
drillThru = thru;
}
function emitDrills(polys) {
@ -264,10 +260,11 @@ export function prepEach(widget, settings, print, firstPoint, update) {
*/
function layerPush(point, emit, speed, tool, options) {
const { type, center, arcPoints } = options ?? {};
const dz = (point && lastPush && lastPush.point) ? point.z - lastPush.point.z : 0;
if (dz < 0 && speed > plungeRate) {
const dz = (point && lastPush?.point) ? point.z - lastPush.point.z : 0;
if (dz < -0.05 && speed > plungeRate) {
speed = plungeRate;
}
// if (options?.type !== 'lerp') console.log( point, currentOp.type );
layerOut.mode = currentOp;
if (lasering) {
let power = emit ? laserPower : 0;
@ -320,7 +317,7 @@ export function prepEach(widget, settings, print, firstPoint, update) {
p.x + wmx,
p.y + wmy,
p.z + zadd
).setA(p.a ?? lastPoint?.a);
).setA(p.a ?? lastPoint?.a).annotate({ slice: p.slice });
}
/**
@ -333,7 +330,26 @@ export function prepEach(widget, settings, print, firstPoint, update) {
* @param {number} opts.factor speed scale factor
*/
function camOut(point, emit = 1, opts) {
// console.trace({point, emit, opts})
let lop = lastOp;
lastOp = currentOp;
// on operation changes:
// 1. move to safe z of current point preserving angle
// 2. move to safe z of new point preserving old angle
// 3. move to safe z of new point with new angle
if (lop && lop !== currentOp && lastPoint) {
// compensate for applyWidgetMovement() applied to lastPoint
let lpo = lastPoint.clone().move({ x: -wmx, y: -wmy, z: -zadd });
camOut(lpo.clone().setZ(zsafe).setA(lastPoint.a), 0);
camOut(point.clone().setZ(zsafe).setA(lastPoint.a), 0);
camOut(point.clone().setZ(zsafe), 0);
}
if (lop?.type === 'index' && lop !== currentOp ) {
// console.log('post index first point', point);
camOut(point.clone().setZ(lastPoint.z).setA(lastPoint.a), 1);
}
let {
center = {},
clockwise = true,
@ -342,14 +358,12 @@ export function prepEach(widget, settings, print, firstPoint, update) {
factor = 1,
} = opts ?? {}
// console.log({radius})
const isArc = emit == 2 || emit == 3;
const pointA = point.a;
// apply widget movement pos
const pointA = point.a;
point = applyWidgetMovement(point);
// console.log(point.z);
if (nextIsMove) {
emit = 0;
nextIsMove = false;
@ -370,7 +384,7 @@ export function prepEach(widget, settings, print, firstPoint, update) {
for (let a of lerp) {
let lp = point.clone().setA(a);
// console.log(lp.a, lp.x, lp.y, lp.z);
lastPoint = layerPush(
layerPush(
lp,
emit,
rate,
@ -403,16 +417,20 @@ export function prepEach(widget, settings, print, firstPoint, update) {
layerPush(point.clone().setZ(stockz + 1), 0, 0, tool);
// change to cutting move for remainder of plunge
emit = 1;
isMove = false;
}
// drop points too close together
if (!isLathe && !isArc && deltaXY < 0.001 && point.z === lastPoint.z) {
if (!isLathe && !isArc && deltaXY < 0.001 && point.z === lastPoint.z && point.a === lastPoint.a) {
// console.trace(["drop dup",lastPoint,point]);
return;
}
// no jump moves in contour mode to adjacent slice points
let steady = lastPoint && currentOp.type === 'contour' && Math.abs(point.slice - lastPoint.slice) < 4;
// 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
@ -431,7 +449,7 @@ export function prepEach(widget, settings, print, firstPoint, update) {
} else if (point.z < lastPoint.z) {
layerPush(point.clone().setZ(lastPoint.z), 0, 0, tool);
}
} else if (isMove) {
} else if (isMove && !steady) {
// for longer moves, check the terrain to see if we need to go up and over
const bigXY = (deltaXY > moveLen && !lasering);
const bigZ = (deltaZ > toolDiam / 2 && deltaXY > tolerance);
@ -478,13 +496,15 @@ export function prepEach(widget, settings, print, firstPoint, update) {
}
// set new plunge rate
let tmprate;
if (false)
if (!lasering && !isLathe && deltaZ < -tolerance) {
let threshold = Math.min(deltaXY / 2, absDeltaZ),
modifier = threshold / absDeltaZ;
if (synthPlunge && threshold && modifier && deltaXY > tolerance) {
// use modifier to speed up long XY move plunge rates
// console.log('modifier', modifier);
rate = Math.max(
tmprate = Math.max(
plungeRate,
Math.round(plungeRate + ((feedRate - plungeRate) * modifier))
);
@ -492,7 +512,8 @@ export function prepEach(widget, settings, print, firstPoint, update) {
let L = Math.hypot(deltaXY, absDeltaZ);
let limXY = feedRate * L / deltaXY;
let limZ = plungeRate * L / Math.abs(deltaZ);
rate = Math.min(feedRate, limXY, limZ);
tmprate = Math.min(feedRate, limXY, limZ);
console.log({ rate_override: rate, was: rate });
// let zps = len / absDeltaZ;
// rate = Math.max(
// plungeRate,
@ -508,7 +529,7 @@ export function prepEach(widget, settings, print, firstPoint, update) {
lastPoint = layerPush(
point,
clockwise ? 2 : 3,
rate,
tmprate ?? rate,
tool,
{
center,
@ -523,7 +544,7 @@ export function prepEach(widget, settings, print, firstPoint, update) {
lastPoint = layerPush(
point,
emit,
rate,
tmprate ?? rate,
tool
);
}
@ -533,16 +554,15 @@ export function prepEach(widget, settings, print, firstPoint, update) {
* output an array of slices that form a pocket
* used by rough and pocket ops, does not support arcs
*
* @param {Slice[]} top-down Z stack of slices
* @param {*} opts
* @param {Slice[]} slices top-down Z stack of slices
* @param {boolean} cutdir true=CW false=CCW
* @param {boolean} depthFirst prioritize cut depth in pockets by nesting
*/
function sliceOutput(sliceOut, opts = {}) {
const { cutdir, depthFirst, easeDown, progress } = opts;
function pocket({ slices, cutdir, depthFirst, easeDown, progress }) {
let total = 0;
let depthData = [];
for (let slice of sliceOut) {
for (let slice of slices) {
let polys = [], t = [], c = [];
POLY.flatten(slice.camLines).forEach(function (poly) {
let child = poly.parent;
@ -569,7 +589,7 @@ export function prepEach(widget, settings, print, firstPoint, update) {
}, { swapdir: false });
newLayer();
}
progress(++total, sliceOut.length);
progress(++total, slices.length);
}
// crucially returns true for -0 as well as other negative #s
@ -663,44 +683,32 @@ export function prepEach(widget, settings, print, firstPoint, update) {
// make top start offset configurable
printPoint = firstPoint || origin;
// accumulated data for depth-first optimizations
// let depthData = {
// rough: [],
// outline: [],
// roughDiam: 0,
// outlineDiam: 0,
// contourx: [],
// contoury: [],
// trace: [],
// drill: [],
// layer: 0,
// };
let ops = {
stock,
setTool,
setDrill,
setSpindle,
setTolerance,
setPrintPoint,
setLasering,
getPrintPoint() { return printPoint },
printPoint,
newLayer,
addGCode,
bounds,
camOut,
polyEmit,
poly2polyEmit,
tip2tipEmit,
depthRoughPath,
depthOutlinePath,
sliceOutput,
depthRoughPath,
emitDrills,
emitTrace,
bounds,
getPrintPoint() { return printPoint },
lastPoint: () => { return lastPoint },
newLayer,
pocket,
poly2polyEmit,
polyEmit,
printPoint,
setDrill,
setLasering,
setPrintPoint,
setSpindle,
setTolerance,
setTool,
stock,
tip2tipEmit,
zclear,
zmax,
lastPoint: () => { return lastPoint }
zsafe,
};
let opSum = 0;
@ -973,8 +981,8 @@ export function prepEach(widget, settings, print, firstPoint, update) {
let arcPoints = arcToPath(from, to, arcPreviewRes, { clockwise, center })
.map(applyWidgetMovement);
.map(applyWidgetMovement);
// console.log({arcPoints})
camOut(from, 1);
@ -1096,7 +1104,10 @@ export function prepEach(widget, settings, print, firstPoint, update) {
if (lastPoint && newOutput.length) {
let lastLayer = newOutput.filter(layer => Array.isArray(layer)).peek();
if (Array.isArray(lastLayer)) {
print.addOutput(lastLayer, printPoint = lastPoint.clone().setZ(zmax_outer), 0, 0, tool);
printPoint = lastPoint.clone();
if (printPoint.z < zmax_outer) printPoint.setZ(zmax_outer);
print.addOutput(lastLayer, printPoint, 0, 0, tool);
// print.addOutput(lastLayer, printPoint = lastPoint.clone().setZ(zmax_outer), 0, 0, tool);
}
}
// console.log("prepare output", newOutput);

View file

@ -119,6 +119,10 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
ondone(msg);
}
function alert(msg) {
onupdate(null,null,msg);
}
if (unsafe) {
console.log("disabling overhang safeties");
}
@ -135,20 +139,20 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
let maxDelta = 1e-3;
if (stock.x + maxDelta < (bounds.max.x - bounds.min.x)) {
return error('stock X too small for part. resize stock or use offset stock');
alert('stock X too small for part');
}
if (stock.y + maxDelta < (bounds.max.y - bounds.min.y)) {
return error('stock Y too small for part. resize stock or use offset stock');
alert('stock Y too small for part');
}
if (stock.z + maxDelta < (bounds.max.z - bounds.min.z)) {
return error('stock Z too small for part. resize stock or use offset stock');
alert('stock Z too small for part');
}
}
if (zMin >= bounds.max.z) {
return error(`invalid z bottom ${(zMin / units).round(3)} >= bounds z max ${(zMax / units).round(3)}`);
alert(`invalid z bottom ${(zMin / units).round(3)} >= bounds z max ${(zMax / units).round(3)}`);
}
let mark = Date.now();
@ -158,31 +162,31 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
let shadows = {};
let slicer;
let state = {
settings,
widget,
addSlices,
bounds,
tabs,
cutTabs,
cutPolys,
color,
computeShadows,
contourPolys,
cutPolys,
cutTabs,
dark,
healPolys,
isIndexed,
ops: opList,
setAxisIndex,
settings,
shadowAt,
slicer,
addSlices,
isIndexed,
setAxisIndex,
updateToolDiams,
tabs,
unsafe,
updateSlicer,
computeShadows,
updateToolDiams,
widget,
zBottom,
zMax,
zThru,
ztOff,
zMax,
zTop,
unsafe,
color,
dark,
ops: opList
zTop
};
let tracker = setSliceTracker({ rotation: 0 });
@ -310,8 +314,11 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
workover.bottom_z = isIndexed ? valz.ov_botz : bottom_stock + valz.ov_botz;
workover.bottom_cut = Math.max(workover.bottom_z, -zThru);
}
// state.workarea = workover;
let { note, type } = op.op;
let named = note ? note.split(' ').filter(v => v.charAt(0) === '#') : [];
let layername = named.length ? named : (note ? `${type} (${note})` : type);
Object.assign(state, {
layername,
zBottom,
zThru,
ztOff,
@ -688,7 +695,7 @@ export async function holes(settings, widget, individual, rec, onProgress) {
let opts = { each: onEach, progress: (num, total) => onProgress(num / total * 0.5, "slice") };
await slicer.slice(indices, opts);
let shadowedDrills = false;
// console.log("slices",slices)
for (let [i, slice] of slices.entries()) {
for (let top of slice.tops) {
// console.log("slicing",slice.z,top)
@ -698,13 +705,13 @@ export async function holes(settings, widget, individual, rec, onProgress) {
continue;
}
for (let poly of inner) {
if (poly.points.length < 7) continue;
if (poly.points.length < 7) {
continue;
}
let center = poly.calcCircleCenter();
center.area = poly.area();
center.overlapping = [center];
center.depth = 0;
// console.log("center",center)
if (poly.circularity() < 0.98) {
// if not circular, don't add to holes
continue;
@ -717,8 +724,7 @@ export async function holes(settings, widget, individual, rec, onProgress) {
let overlap = false;
for (let [i, circle] of circles.entries()) {
let dist = circle.distTo2D(center);
// //if on the same xy point,
// if on the same xy point,
if (dist <= centerDiff) {
// console.log("overlap",center,circle);
circle.overlapping.push(center);
@ -732,12 +738,10 @@ export async function holes(settings, widget, individual, rec, onProgress) {
}
onProgress(0.5 + (i / slices.length * 0.25), "recognize circles");
}
let drills = [];
for (let [i, c] of circles.entries()) {
let overlapping = c.overlapping;
let last = overlapping.shift();
while (overlapping.length) {
let circ = overlapping.shift();
@ -775,14 +779,33 @@ export async function holes(settings, widget, individual, rec, onProgress) {
return drills;
}
function cutTabs(tabs, offset, z, inter) {
tabs = tabs.filter(tab => z < tab.pos.z + tab.dim.z / 2).map(tab => tab.off).flat();
return cutPolys(tabs, offset, z, false);
function cutTabs(tabs, offset) {
let out = [];
for (let tab of tabs) {
tab.top = tab.pos.z + tab.dim.z / 2;
}
for (let poly of offset) {
let polyZ = poly.getZ();
let cut = tabs.filter(tab => tab.top >= polyZ);
let lo = poly.cut(cut.map(tab => tab.off).flat(), false);
let hi = [];
for (let tab of cut) {
hi.appendAll(POLY.setZ(poly.cut(tab.off, true), tab.top));
}
if (hi.length) {
let heal = healPolys([ ...lo, ...hi ], false);
POLY.setWinding(heal, poly.isClockwise());
out.push(...heal);
} else {
out.push(poly);
}
}
return out;
}
function cutPolys(polys, offset, z, inter) {
function cutPolys(polys, offset) {
let noff = [];
offset.forEach(op => noff.appendAll(op.cut(POLY.union(polys, 0, true), inter)));
offset.forEach(op => noff.appendAll( op.cut(POLY.union(polys, 0, true), true) ));
return healPolys(noff);
}
@ -799,7 +822,17 @@ function contourPolys(widget, polys) {
}
}
function healPolys(noff) {
function dedup(arr, same) {
const out = [];
let prev;
for (const x of arr) {
if (!prev || !same(prev, x)) out.push(x);
prev = x;
}
return out;
}
function healPolys(noff, sameZ = true) {
for (let p of noff) {
if (p.appearsClosed()) {
p.points.pop();
@ -818,29 +851,29 @@ function healPolys(noff) {
let s2 = ntmp[j];
if (!s2) continue;
// require polys at same Z to heal
if (Math.abs(s1.getZ() - s2.getZ()) > 0.01) {
if (sameZ && Math.abs(s1.getZ() - s2.getZ()) > 0.01) {
continue;
}
if (!(s1.open && s2.open)) continue;
if (s1.last().isMergable2D(s2.first())) {
s1.addPoints(s2.points.slice(1));
s1.addPoints(s2.points);
ntmp[j] = null;
continue outer;
}
if (s2.last().isMergable2D(s1.first())) {
s2.addPoints(s1.points.slice(1));
s2.addPoints(s1.points);
ntmp[i] = null;
continue outer;
}
if (s1.first().isMergable2D(s2.first())) {
s1.reverse();
s1.addPoints(s2.points.slice(1));
s1.addPoints(s2.points);
ntmp[j] = null;
continue outer;
}
if (s1.last().isMergable2D(s2.last())) {
s2.reverse();
s1.addPoints(s2.points.slice(1));
s1.addPoints(s2.points);
ntmp[j] = null;
continue outer;
}
@ -854,9 +887,14 @@ function healPolys(noff) {
}
// close poly if head meets tail
for (let poly of noff) {
if (poly.open && poly.first().isMergable2D(poly.last())) {
poly.points.pop();
poly.open = false;
poly.points = dedup(poly.points, (a,b) => a.isMergable3D(b));
if (poly.open) {
if (poly.first().isMergable3D(poly.last())) {
poly.points.pop();
poly.open = false;
} else if (poly.first().isMergable2D(poly.last())) {
poly.open = false;
}
}
}
}

View file

@ -12,6 +12,7 @@ const { sliceConnect, sliceDedup } = slicer;
const { config } = base;
const timing = false;
const zDecimal = 3;
const epsilon = 10e-5;
const begin = function () {
if (timing) console.time(...arguments);
@ -94,7 +95,7 @@ export class Slicer {
countZ(p3.z);
}
// use co-flat and co-line detection to adjust slice Z
if (p1.z === p2.z && p2.z === p3.z) {
if (Math.abs(p1.z - p2.z) < epsilon && Math.abs(p2.z - p3.z) < epsilon) {
// detect zFlat faces to avoid slicing directly on them
let zkey = p1.z.toFixed(zDecimal),
area = Math.abs(util.area2(p1, p2, p3)) / 2;

View file

@ -16,9 +16,10 @@ export class Topo {
async generate(opt = {}) {
let { state, contour, onupdate, ondone } = opt;
let { widget, settings, tshadow, center, tabs, color } = opt.state;
let { widget, settings, tshadow, tabs } = opt.state;
let { controller, process } = settings,
{ webGPU } = controller,
animesh = parseInt(controller.animesh || 100) * 2500,
axis = contour.axis.toLowerCase(),
contourX = axis === "x",
@ -37,16 +38,15 @@ export class Topo {
boundsY = maxY - minY,
inside = contour.inside,
density = 1 + (contour.reduction || 0),
resolution = tolerance ? tolerance : 1 / Math.sqrt(animesh / (boundsX * boundsY)),
resolution = (tolerance ? tolerance : 1 / Math.sqrt(animesh / (boundsX * boundsY))).round(5),
tool = new Tool(settings, contour.tool),
toolOffset = tool.generateProfile(resolution).profile,
toolDiameter = tool.fluteDiameter(),
toolStep = toolDiameter * contour.step, //tool.contourOffset(contour.step),
toolStep = toolDiameter * contour.step,
leave = contour.leave || 0,
maxangle = contour.angle,
curvesOnly = contour.curves,
bridge = contour.bridging || 0,
// R2A = 180 / Math.PI,
stepsX = Math.ceil(boundsX / resolution),
stepsY = Math.ceil(boundsY / resolution),
widtopo = widget.topo,
@ -75,12 +75,7 @@ export class Topo {
clipTo = inside ? shadow : POLY.expand(shadow, toolDiameter / 2 + resolution * 3),
partOff = inside ? 0 : toolDiameter / 2 + resolution,
gridDelta = Math.floor(partOff / resolution),
debug = false,
debug_clips = debug && true,
debug_topo = debug && true,
debug_topo_lines = debug && true,
debug_topo_shells = debug && true,
time = Date.now;
debug_clips = false;
if (tolerance === 0 && !topoCache) {
console.log(widget.id, 'topo auto tolerance', resolution.round(4));
@ -90,7 +85,7 @@ export class Topo {
// used by Pocket -> Contour
this.tolerance = resolution;
if (tabs) {
if (tabsOn) {
clipTab.appendAll(tabs.map(tab => {
let ctab = POLY.expand([tab.poly], toolDiameter / 2);
ctab.forEach(ct => ct.z = tab.dim.z / 2 + tab.pos.z);
@ -107,6 +102,190 @@ export class Topo {
newslices.push(debug);
}
if (webGPU && !contour.nogpu) {
// invert tool Z offset for gpu code
let toolBounds = new THREE.Box3()
.expandByPoint({ x: -toolDiameter/2, y: -toolDiameter/2, z: 0 })
.expandByPoint({ x: toolDiameter/2, y: toolDiameter/2, z: 0 });
let toolPos = tool.profile.slice();
for (let i=0; i<toolPos.length; i+= 3) {
// toolPos[i+2] = -toolPos[i+2];
toolBounds.expandByPoint({ x: toolPos[i], y: toolPos[i+1], z: toolPos[i+2] });
}
let toolData = { positions: toolPos, bounds: toolBounds };
const vertices = widget.getGeoVertices({ unroll: true, translate: true });
const wbounds = widget.getBoundingBox();
if (!inside) {
wbounds.expandByVector({ x: toolDiameter/2, y: toolDiameter/2, z: 0 });
}
if (contourY) {
// console.time('swap XY vertices');
// swap XY vertices
for (let i=0; i<vertices.length; i+= 3) {
let tmp = vertices[i+1];
vertices[i+1] = vertices[i+0];
vertices[i+0] = tmp;
}
['min','max'].forEach(ext => {
ext = wbounds[ext];
let tmp = ext.x;
ext.x = ext.y;
ext.y = tmp;
});
// console.timeEnd('swap XY vertices');
}
let trace = contour.trace;
let gpu = await self.get_raster_gpu({
mode: trace ? "tracing" : "planar",
resolution
});
let xStep = density;
let yStep = Math.ceil(toolStep / resolution);
let epsilon = 10e-4;
await gpu.loadTool({
sparseData: toolData
});
let terrain = await gpu.loadTerrain({
triangles: vertices,
boundsOverride: wbounds
});
let { gridWidth, positions } = terrain;
let output = await gpu.generateToolpaths({
xStep,
yStep,
zFloor: zBottom - 1,
onProgress(pct) { console.log({ pct }); onupdate(pct/100, 100) }
});
gpu.terminate();
let { numScanlines, pointsPerLine, pathData } = output;
let xmult = xStep * resolution;
let ymult = yStep * resolution;
let xoff = wbounds.min.x;
let yoff = wbounds.min.y;
let slices = [];
function calcPoint(x, y, z, tx, ty) {
let p = newPoint(x, y, z);
p.tx = tx;
p.ty = ty;
return p;
}
for (let i=0; i<numScanlines; i++) {
let lineStart = i * pointsPerLine;
let lineData = pathData.slice(lineStart, lineStart + pointsPerLine);
let points = contourY
? Array.from(lineData).map((v,j) => calcPoint(i * ymult + yoff, j * xmult + xoff, v, i * xStep, j * yStep))
: Array.from(lineData).map((v,j) => calcPoint(j * xmult + xoff, i * ymult + yoff, v, j * xStep, i * yStep))
;
let slice = newSlice(i);
let poly = newPolygon().setOpen();
let lines = [ ];
let skip = 0;
let lp;
for (let p of points) {
if (inside && positions[p.ty * gridWidth + p.tx] < zBottom - epsilon) {
if (poly.length > 1) {
lines.push(poly);
poly = newPolygon().setOpen();
} else if (poly.length === 1) {
poly = newPolygon().setOpen();
}
lp = undefined;
skip = 0;
continue;
}
if (p.z < zBottom - epsilon) {
lp = p;
if (poly.length === 0) {
skip++;
continue;
}
if (poly.length > 1) {
skip = 0;
p.z = zBottom;
poly.push(p);
lines.push(poly);
poly = newPolygon().setOpen();
continue;
}
} else if (lp && skip && poly.length === 0) {
lp.z = zBottom;
poly.push(lp);
}
poly.push(lp = p);
skip = 0;
}
// require two points or more
if (poly.length > 1) {
lines.push(poly);
}
// raize output points when inside tab boundaries
if (!inside && tabsOn && clipTab.length)
for (let poly of lines) {
for (let p of poly.points) {
for (let clip of clipTab) {
if (p.z < clip.z && p.isInPolygon(clip)) {
p.z = clip.z;
break;
}
}
}
}
// drop interior points along a continuous slope
for (let poly of lines) {
let { points } = poly;
let merged = [ points[0] ];
let lp = points[1];
let dz = points[0].z - lp.z;
for (let i=2; i<points.length; i++) {
let np = points[i];
let ndz = np.z - lp.z;
if (Math.abs(dz - ndz) > epsilon) {
// slope changed
merged.push(lp);
dz = ndz;
} else if (curvesOnly && Math.abs(ndz) < epsilon) {
// add empty points as path separators
merged.push(undefined);
}
lp = np;
}
if (merged.peek() !== lp) {
merged.push(lp);
}
poly.points = merged;
}
// drop flat regions when enabled
if (curvesOnly) {
let nulines = [];
for (let poly of lines) {
let { points } = poly;
let newp = [];
for (let p of points) {
if (p) {
newp.push(p);
} else if (newp.length) {
nulines.push(newPolygon(newp).setOpen());
newp = [];
}
}
nulines.push(newPolygon(newp).setOpen());
}
lines = nulines.filter(p => p.perimeter() > toolStep);
}
slice.camLines = lines;
slices.push(slice);
if (inside) {
onupdate(i, numScanlines);
}
}
ondone(slices);
return this;
}
const probe = this.probe = new Probe({
profile: toolOffset,
data,
@ -177,8 +356,7 @@ export class Topo {
clipTabZ: clipTab ? clipTab.map(t => t.z) : undefined,
tabHeight,
newslices,
leave,
color
leave
}, (i, l, p) => {
onupdate(l / 2 + i / 2, l, p);
});
@ -304,7 +482,7 @@ export class Topo {
const { minX, maxX, minY, maxY, boundsX, boundsY, stepsX, stepsY } = params;
const { gridDelta, resolution, density, partOff, toolStep, contourX, contourY } = params;
const { clipTo, clipTab, clipTabZ, tabHeight, newslices, color, leave } = params;
const { clipTo, clipTab, clipTabZ, tabHeight, newslices, leave } = params;
let stepsTaken = 0,
stepsTotal = 0;
@ -370,11 +548,8 @@ export class Topo {
gridy
}, segments => {
if (segments.length > 0) {
let slice = newSlice(gridx);
let slice = newSlice(x);
slice.camLines = segments;
slice.output()
.setLayer("contour y", { face: color, line: color })
.addPolys(segments);
slicesY.push(slice);
}
onupdate(++stepsTaken, stepsTotal, "contour y");
@ -399,11 +574,8 @@ export class Topo {
gridy
}, segments => {
if (segments.length > 0) {
let slice = newSlice(gridy);
let slice = newSlice(y);
slice.camLines = segments;
slice.output()
.setLayer("contour x", { face: color, line: color })
.addPolys(segments);
slicesX.push(slice);
}
onupdate(++stepsTaken, stepsTotal, "contour x");
@ -534,9 +706,9 @@ export class Trace {
console.log(...arguments);
}
const push_point = this.push_point = function (x, y, z) {
this.push_point = function (x, y, z) {
const newP = newPoint(x, y, z);
const lastP = lastPP;//trace.last();
const lastP = lastPP;
if (lastP) {
const dl = (x - lastP.x) || (y - lastP.y);
@ -553,7 +725,6 @@ export class Trace {
}
if (curvesOnly) {
const dv = contourX ? Math.abs(lastP.x - x) : Math.abs(lastP.y - y);
// const dz = lastPP.z - z;
const angle = Math.atan2(Math.abs(dz), dv) * RAD2DEG;
if (angle > maxangle) {
end_poly();

View file

@ -22,8 +22,9 @@ export class Topo {
async generate(opt = {}) {
let { state, op, onupdate, ondone } = opt;
let { widget, settings, tabs } = opt.state;
let { widget, settings, tabs, color } = state;
let { controller, process } = settings;
let { webGPU } = controller;
let axis = op.axis.toLowerCase(),
tool = new Tool(settings, op.tool),
@ -38,9 +39,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,31 +52,36 @@ 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.units = controller.units === 'in' ? 25.4 : 1
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.lineColor = color;//controller.dark ? 0xffff00 : 0x555500;
this.offStart = op.offStart ?? 0;
this.offEnd = op.offEnd ?? 0;
this.bounds = bounds;
this.gpu = webGPU ?? false;
onupdate(0, "lathe");
const parts = webGPU ? [ 0.8, 0.2 ] : [ 0.25, 0.75 ];
const range = this.range = { min: Infinity, max: -Infinity };
const slices = this.sliced = await this.slice(scale(onupdate, 0.25, 0));
const slices = this.sliced = await this.slice(scale(onupdate, parts[0], 0));
for (let slice of slices) {
range.min = Math.min(range.min, slice.z);
range.max = Math.max(range.max, slice.z);
slice.output()
.setLayer("lathe", { line: 0x888888 })
.addPolys(slice.topPolys());
}
const lathe = await this.lathe(scale(onupdate, 0.75, 0.25));
const lathe = await this.lathe(scale(onupdate, parts[1], parts[0]));
onupdate(1, "lathe");
ondone(lathe);
@ -86,10 +91,9 @@ export class Topo {
}
async slice(onupdate) {
const { vertices, resolution, tabverts, zoff } = this;
const { vertices, resolution, tabverts, zoff, offStart, offEnd, units } = this;
const { minions } = self.kiri_worker;
const range = this.range = { min: Infinity, max: -Infinity };
const box = this.box = new THREE.Box2();
// swap XZ in shared array
for (let i = 0, l = vertices.length; i < l; i += 3) {
@ -101,9 +105,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 * units;
range.max -= offEnd * units;
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 +121,36 @@ export class Topo {
// re-create shared vertex array for workers
this.vertices = [].appendAll(vertices).appendAll(tabverts).toFloat32().toShared();
const shards = Math.ceil(Math.min(25, vertices.length / 27000));
const step = (range.max - range.min) / shards;
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++;
// rp.rasterizeMesh(vertices, resolution).then(rpo => console.log({ rpo }));
if (this.gpu) {
return await this.sliceGPU(onupdate);
}
slices.push(slice);
// console.log({ shards, range, step, slices });
const { minions } = self.kiri_worker;
const zSpan = range.max - range.min;
const shards = Math.ceil(Math.min(25, vertices.length / 27000));
const totalSteps = Math.ceil(zSpan / resolution);
const stardSteps = Math.ceil(totalSteps / shards);
const stepWidth = stardSteps * resolution;
let slices = this.slices = [];
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
});
}
// console.log({
// shards,
// step: stepWidth,
// range,
// resolution,
// slices: slices.slice(),
// minions
// });
if (minions?.running > 1) {
return await this.sliceMinions(onupdate);
} else {
@ -141,6 +158,96 @@ export class Topo {
}
}
async sliceGPU(onupdate) {
const { angle, diam, linear, offStart, offEnd, resolution, tool, units, vertices, zBottom } = this;
// invert tool Z offset for gpu code
let toolBounds = new THREE.Box3()
.expandByPoint({ x: -this.diam/2, y: -diam/2, z: 0 })
.expandByPoint({ x: this.diam/2, y: diam/2, z: 0 });
let toolPos = tool.slice();
let minz = Infinity;
for (let i=0; i<toolPos.length; i += 3) {
// toolPos[i+2] = -toolPos[i+2];
toolBounds.expandByPoint({ x: 0, y: 0, z: toolPos[i+2] });
minz = Math.min(minz,toolPos[i+2]);
}
let toolData = { positions: toolPos, bounds: toolBounds };
// console.time('swap XZ vertices');
// swap XZ vertices for gpu code
for (let i=0; i<vertices.length; i+= 3) {
let tmp = vertices[i+2];
vertices[i+2] = vertices[i+0];
vertices[i+1] = -vertices[i+1];
vertices[i+0] = tmp;
}
// console.timeEnd('swap XZ vertices');
let gpu = await self.get_raster_gpu({
mode: "radial",
resolution,
rotationStep: angle,
radialRotationOffset: 90
});
let xStep = Math.max(1, Math.round(this.step / resolution));
let boundsOverride = this.bounds.clone();
boundsOverride.min.x += offStart * units;
boundsOverride.max.x -= offEnd * units;
await gpu.loadTool({
sparseData: toolData
});
await gpu.loadTerrain({
triangles: vertices,
boundsOverride,
onProgress(pct) { onupdate(pct/100) }
});
let output = await gpu.generateToolpaths({
xStep,
yStep: 1,
zFloor: zBottom,
onProgress(i,j) { onupdate(i/j) }
});
gpu.terminate();
let { numStrips, strips } = output;
let degPerRow = 360 / numStrips;
let slices = this.gpu_slices = [];
let xmult = resolution * xStep;
let xoff = boundsOverride.min.x;
let rows = [];
for (let i=0; i<numStrips; i++) {
let points = Array.from(strips[i].pathData).map((v,j) => newPoint(j * xmult + xoff, 0, v).setA(-i * degPerRow));
rows.push(points);
}
if (linear) {
for (let i=0; i<rows.length; i++) {
let slice = newSlice(i);
slice.index = i;
slice.camLines = [ newPolygon(rows[i]).setOpen() ];
if (i % 2 === 1) slice.camLines[0].reverse();
slices.push(slice);
}
} else {
let { pointsPerLine } = strips[0];
for (let i=0; i<pointsPerLine; i++) {
let slice = newSlice(i);
let points = rows.map(row => row[i]);
if (i % 2 === 1) points.reverse();
slice.index = i;
slice.camLines = [ newPolygon(points).setOpen() ];
slices.push(slice);
}
}
for (let slice of slices) {
slice.output()
.setLayer("lathe", { line: this.lineColor })
.addPoly(slice.camLines[0].clone().applyRotations());
}
// console.log({ webGPU: output, slices });
return slices;
}
async sliceWorker(onupdate) {
const { vertices, slices, resolution } = this;
@ -205,22 +312,28 @@ export class Topo {
async lathe(onupdate) {
const { minions } = self.kiri_worker;
if (minions?.running > 1) {
if (this.gpu) {
return await this.latheGPU(onupdate);
} else if (minions?.running > 1) {
return await this.latheMinions(onupdate);
} else {
return await this.latheWorker(onupdate);
}
}
async latheGPU(onupdate) {
return this.gpu_slices;
}
lathePath(slices, tool) {
const { resolution, step, zBottom, maxo } = this;
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 +357,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 +396,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;
@ -297,8 +291,9 @@ export function fdm_export(print, online, ondone, ondebug) {
append(`; Bed type: ${bedType}`);
append(`; Target: ${filter[mode]}`);
// inject thumbnail preview
if (exportThumb && worker.snap) {
let { width, height, url } = worker.snap;
let { current } = self.kiri_worker ?? {};
if (exportThumb && current?.snap) {
let { width, height, url } = current.snap;
let data = url.substring(url.indexOf(',') + 1);
append(`; thumbnail begin ${width} ${height} ${data.length}`);
for (let i=0; i<data.length; i += 78) {
@ -470,7 +465,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;
}
@ -613,6 +608,9 @@ export function fdm_export(print, online, ondone, ondebug) {
for (pidx=0; pidx<path.length; pidx++) {
out = path[pidx];
speedMMM = (out.speed || process.outputFeedrate) * 60; // range
if (!isBelt) {
fanSpeed = out.fan ?? fanSpeed;
}
// hint to controller that we're working on a specific object
// so that gcode between start/stop comments can be cancelled
@ -634,7 +632,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 {
@ -687,10 +685,10 @@ export function fdm_export(print, online, ondone, ondebug) {
// in belt mode, fan can change per segment (for base)
if (isBelt) {
setTempFanSpeed(out.fan);
if (fanSpeed !== lastFanSpeed) {
appendAllSub(gcodeFan);
lastFanSpeed = fanSpeed;
}
}
if (fanSpeed !== lastFanSpeed) {
appendAllSub(gcodeFan);
lastFanSpeed = fanSpeed;
}
if (lastp && out.emit) {
@ -751,7 +749,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)}),
@ -135,11 +134,9 @@ export function menu() {
firstLayerFanSpeed: newInput(LANG.ou_fans_s, {title:LANG.ou_fans_l, convert:toInt, bound:bound(0,255), show:isBelt}),
firstLayerYOffset: newInput(LANG.fl_zoff_s, {title:LANG.fl_zoff_l, convert:toFloat, show:isBelt}),
firstLayerFlatten: newInput(LANG.fl_flat_s, {title:LANG.fl_flat_l, convert:toFloat, show:isBelt}),
firstLayerBeltFact: newInput(LANG.fl_mult_s, {title:LANG.fl_mult_l, convert:toFloat, bound:bound(0, 2), 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 +151,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,10 +173,13 @@ 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)}),
outputCoastDist: newInput(LANG.ad_scst_s, {title:LANG.ad_scst_l, convert:toFloat, bound:bound(0.0,10)}),
sliceCompInner: newInput(LANG.sl_ofin_s, {title:LANG.sl_ofin_l, convert:toFloat, bound:bound(-10.0,10.0)}),
sliceCompOuter: newInput(LANG.sl_ofot_s, {title:LANG.sl_ofot_l, convert:toFloat, bound:bound(-10.0,10.0)}),
zHopDistance: newInput(LANG.ad_zhop_s, {title:LANG.ad_zhop_l, convert:toFloat, bound:bound(0,3.0)}),
outputLoops: newInput(LANG.ag_loop_s, {title:LANG.ag_loop_l, convert:toInt, bound:bound(-1,1000), show:isBelt}),

View file

@ -17,12 +17,13 @@ slicer.slicePost.FDM = slicePost;
* data object. return is ignored.
*/
export function slicePost(data, options) {
const { z, lines, groups } = data;
const { useAssembly, post_args, zIndexes } = options;
const { process, isSynth, vaseMode } = post_args;
const { shellOffset, fillOffset, clipOffset } = post_args;
const { groups, z } = data;
const { post_args, useAssembly, zIndexes } = options;
const { isSynth, process, vaseMode } = post_args;
const { compInner, compOuter, pump } = post_args;
const { clipOffset, fillOffset, shellOffset } = post_args;
if (options.pump) {
if (pump) {
let nugroups = [];
let pump = shellOffset / 10;
for (let p of groups) {
@ -41,6 +42,19 @@ export function slicePost(data, options) {
data.tops = POLY.nest(groups);
}
// perimeter and inner hole compensation offsets
if (compInner || compOuter) {
let inner = POLY.inner(data.tops).flat();
let outer = POLY.outer(data.tops);
if (compInner) {
inner = POLY.flatten(POLY.offset(inner, compInner/2));
}
if (compOuter) {
outer = POLY.flatten(POLY.offset(outer, -compOuter/2));
}
data.tops = POLY.nest([...inner, ...outer]);
}
if (isSynth) {
const process = post_args.process;
if (process.sliceSupportGrow > 0) {
@ -54,6 +68,7 @@ export function slicePost(data, options) {
delete data.groups;
return;
}
const index = zIndexes.indexOf(z);
const range = getRangeParameters(process, index);
// calculate fractional shells
@ -67,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
@ -129,7 +141,7 @@ function offset_default(params) {
}
const deltaBig = 160;
const deltSmall = 20;
const deltaSmall = 20;
// look for close points on adjacent segmented polys and merge
let test_polys = first !== last ? [ ...first, ...last ] : last;
@ -139,6 +151,8 @@ function offset_default(params) {
for (let poly of test_flats)
poly.forEachSegment((p0, p1) => {
p0.angle = new Slope(p0, p1);
// todo calc vertex angle
// exclude segment vertex from merge when angle < deltaSmall
});
// segment each poly to be tested

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';
@ -46,8 +47,6 @@ export async function fdm_prepare(widgets, settings, update) {
output = [],
layerout = [];
let lastLayerStart = null;
// compute bounds if missing
if (!bounds) {
bounds = new THREE.Box3();
@ -553,12 +552,9 @@ export async function fdm_prepare(widgets, settings, update) {
offset,
layerout,
{
routeAround: process.outputAvoidGaps,
seedPoint: printPoint.sub(offset),
params, // range parameters
first: slice.index === 0,
support: slice.widget.support,
onBelt: beltStart,
params, // range parameters
pretract: (wipeDist) => {
if (!(lastLayer && lastLayer.length)) {
return;
@ -573,7 +569,11 @@ export async function fdm_prepare(widgets, settings, update) {
print.addOutput(lastLayer, endpoint, null, null, lastOut.tool);
}
}
}
},
routeAround: process.outputAvoidGaps,
seedPoint: printPoint.sub(offset),
support: slice.widget.support,
zmax,
}
);
layerout.print_time = printPoint.layer_time;
@ -682,8 +682,8 @@ export async function fdm_prepare(widgets, settings, update) {
for (let rec of layer) {
overate = rec.overate >= 0 ? rec.overate : overate;
let bmult = params.firstLayerBeltFact ?? 1;
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;
@ -695,8 +695,9 @@ 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;
// apply base flowrate multiplier to segments touching belt
rec.emit *= bmult;
minx = Math.min(minx, point.x, lastout.point.x);
maxx = Math.max(maxx, point.x, lastout.point.x);
maxy = Math.max(maxy, point.y);
@ -795,10 +796,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,
@ -807,6 +820,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),
@ -815,17 +829,20 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) {
shellOrder = {"out-in":-1,"in-out":1,"alternate":-2}[process.sliceShellOrder] || -1,
sparseMult = process.outputSparseMult,
coastDist = process.outputCoastDist || 0,
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 ? {
@ -849,11 +866,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;
@ -862,13 +874,7 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) {
// override: adapt bridge layer speed
if (slice.isBridgeLayer) {
fillSpeed /= 2;
}
// apply first layer extrusion multipliers
if (firstLayer) {
fillMult *= firstPrintMult;
shellMult *= firstPrintMult;
sparseMult *= firstPrintMult;
printSpeed /= 2;
}
function retract() {
@ -1100,9 +1106,6 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) {
coast: firstLayer ? 0 : coastDist,
extrude: numOrDefault(opt.extrude, shellMult),
nozzleSize,
rate: finishShell ? finishSpeed : printSpeed,
scarf,
tool: extruder,
onfirst: (firstPoint, preout) => {
let from = seedPoint || startPoint;
if (from.distTo2D(firstPoint) > retractDist) {
@ -1111,7 +1114,11 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) {
}
}
seedPoint = null;
}
},
rate: finishShell ? finishSpeed : printSpeed,
scarf,
tool: extruder,
zmax
});
lastPoly = slice.lastPoly = poly;
}
@ -1591,16 +1598,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 *= fact;
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 });
}
}
}
}
@ -1612,6 +1637,12 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) {
// add offset points to total print
print.addPrintPoints(preout, output);
// bridge layer fan change
if (preout.length && slice.isBridgeLayer) {
preout[0].fan = Math.round(fanSpeed / 3);
preout.peek().fan = fanSpeed;
}
// create next layer start point and annotate
// with print time for previous layer (hack to
// avoid changing return signature rn)

View file

@ -140,7 +140,8 @@ export function fdm_slice(settings, widget, onupdate, ondone) {
fillOffset = lineWidth * fillOffsetMult,
clipOffset = process.sliceSupportOffset,
sliceFillAngle = process.sliceFillAngle,
supportDensity = process.sliceSupportDensity;
supportDensity = process.sliceSupportDensity,
{ sliceCompInner, sliceCompOuter } = process;
// override globals used by vopt()
isFlat = controller.lineType === "flat";
@ -230,6 +231,8 @@ export function fdm_slice(settings, widget, onupdate, ondone) {
useAssembly,
post: 'FDM',
post_args: {
compInner: sliceCompInner,
compOuter: sliceCompOuter,
shellOffset,
fillOffset,
clipOffset,
@ -495,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;
@ -503,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) {
@ -752,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

@ -17,6 +17,7 @@ import { version } from '../../moto/license.js';
import { Widget, newWidget } from '../core/widget.js';
import { load } from "../../load/png.js";
import { JSZip } from '../../ext/jszip-esm.js';
import { RasterPath } from '../../gpu/raster.js';
import { CAM } from '../mode/cam/driver-be.js';
import { DRAG } from '../mode/drag/driver.js';
@ -64,6 +65,22 @@ self.alert = function(o) {
self.uuid = ((Math.random() * Date.now()) | 0).toString(36);
/**
* @returns {RasterPath}
*/
self.get_raster_gpu = async function({ mode, resolution, rotationStep }) {
let gpu = new RasterPath({
mode,
resolution,
rotationStep,
workerName: "/lib/gpu/raster-worker.js",
quiet: true,
debug: false,
});
await gpu.init();
return gpu;
};
function minhandler(msg) {
let data = msg.data;
let seq = data.seq;

676
src/load/step.js Normal file
View file

@ -0,0 +1,676 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { tool as MeshTool } from '../mesh/tool.js';
const { Vector3 } = THREE;
function debug() {
if (false) console.log(...arguments);
}
/**
* Convert STL triangles to STEP file format
*
* @param {Array} triangles - Array of triangle objects with vertices: [{v1, v2, v3}, ...]
* where v1, v2, v3 are {x, y, z} objects or Vector3 instances
* @param {Object} options - Optional parameters
* @param {string} options.productName - Name for the STEP product (default: 'mesh')
* @param {string} options.units - Units: 'mm', 'cm', 'm', 'inch' (default: 'mm')
* @returns {string} STEP file content as a string
*/
export function meshToSTEP(triangles, options = {}) {
const productName = options.productName || 'mesh';
const units = options.units || 'mm';
// Build vertex and triangle topology
const vertices = [];
const vertexMap = new Map(); // Map vertex coords to index
const faces = [];
// Helper to find or add vertex
function getOrAddVertex(v) {
const vec = v instanceof Vector3 ? v : new Vector3(v.x, v.y, v.z);
// Check if vertex already exists
for (let i = 0; i < vertices.length; i++) {
if (vertices[i].equals(vec)) {
return i;
}
}
// Add new vertex
vertices.push(vec);
return vertices.length - 1;
}
// Process triangles
for (const tri of triangles) {
const idx0 = getOrAddVertex(tri.v1 || tri.vertices[0]);
const idx1 = getOrAddVertex(tri.v2 || tri.vertices[1]);
const idx2 = getOrAddVertex(tri.v3 || tri.vertices[2]);
faces.push([idx0, idx1, idx2]);
}
// Generate STEP file
let step = `ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('STL to STEP Conversion'),'2;1');
FILE_NAME('${productName}.step','${new Date().toISOString()}',(''),(''),'stl-to-step','','');
FILE_SCHEMA(('AUTOMOTIVE_DESIGN'));
ENDSEC;
DATA;
`;
let id = 1;
// Units mapping
const unitMultiplier = {
'mm': '.MILLI.',
'cm': '.CENTI.',
'm': '',
'inch': '.INCH.'
};
const unitPrefix = unitMultiplier[units] || '.MILLI.';
// Units and context
const lengthUnit = id++;
const planeAngleUnit = id++;
const solidAngleUnit = id++;
step += `#${lengthUnit}=(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(${unitPrefix},.METRE.));\n`;
step += `#${planeAngleUnit}=(NAMED_UNIT(*)PLANE_ANGLE_UNIT()SI_UNIT($,.RADIAN.));\n`;
step += `#${solidAngleUnit}=(NAMED_UNIT(*)SI_UNIT($,.STERADIAN.)SOLID_ANGLE_UNIT());\n`;
const uncertainty = id++;
step += `#${uncertainty}=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#${lengthUnit},'distance_accuracy_value','confusion accuracy');\n`;
// Geometric representation context
const geomContextId = id++;
step += `#${geomContextId}=(GEOMETRIC_REPRESENTATION_CONTEXT(3)GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#${uncertainty}))GLOBAL_UNIT_ASSIGNED_CONTEXT((#${lengthUnit},#${planeAngleUnit},#${solidAngleUnit}))REPRESENTATION_CONTEXT('ID1','3D'));\n`;
// Application context
const appContext = id++;
step += `#${appContext}=APPLICATION_CONTEXT('configuration controlled 3d designs of mechanical parts and assemblies');\n`;
const appProtocol = id++;
step += `#${appProtocol}=APPLICATION_PROTOCOL_DEFINITION('international standard','automotive_design',1998,#${appContext});\n`;
// Product definition
const productId = id++;
step += `#${productId}=PRODUCT('${productName}','${productName}','',(#${appContext}));\n`;
const productDefFormId = id++;
step += `#${productDefFormId}=PRODUCT_DEFINITION_FORMATION('','',#${productId});\n`;
const productDefCtx = id++;
step += `#${productDefCtx}=PRODUCT_DEFINITION_CONTEXT('part definition',#${appContext},'design');\n`;
const productDefId = id++;
step += `#${productDefId}=PRODUCT_DEFINITION('design','',#${productDefFormId},#${productDefCtx});\n`;
// Shape representation - reserve ID
const shapeRepId = id++;
// Create cartesian points for all vertices
const vertexIds = [];
for (let i = 0; i < vertices.length; i++) {
const v = vertices[i];
const pointId = id++;
step += `#${pointId}=CARTESIAN_POINT('',(${v.x.toFixed(6)},${v.y.toFixed(6)},${v.z.toFixed(6)}));\n`;
vertexIds.push(pointId);
}
// Create vertex points
const vertexPointIds = [];
for (let i = 0; i < vertices.length; i++) {
const vpId = id++;
step += `#${vpId}=VERTEX_POINT('',#${vertexIds[i]});\n`;
vertexPointIds.push(vpId);
}
// Advanced B-rep shape representation
const manifoldId = id++;
const closedShellId = id++;
step += `#${shapeRepId}=ADVANCED_BREP_SHAPE_REPRESENTATION('',(#${manifoldId}),#${geomContextId});\n`;
step += `#${manifoldId}=MANIFOLD_SOLID_BREP('',#${closedShellId});\n`;
const faceIds = [];
for (let i = 0; i < faces.length; i++) {
faceIds.push(id++);
}
step += `#${closedShellId}=CLOSED_SHELL('',(${faceIds.map(fid => `#${fid}`).join(',')}));\n`;
// Create each triangular face
for (let faceIdx = 0; faceIdx < faces.length; faceIdx++) {
const faceVertices = faces[faceIdx];
const faceId = faceIds[faceIdx];
const faceOuterBoundId = id++;
const edgeLoopId = id++;
// Create edges for this triangular face
const orientedEdgeIds = [];
for (let i = 0; i < 3; i++) {
const v1Idx = faceVertices[i];
const v2Idx = faceVertices[(i + 1) % 3];
const orientedEdgeId = id++;
const edgeCurveId = id++;
const lineId = id++;
const vectorId = id++;
const directionId = id++;
orientedEdgeIds.push(orientedEdgeId);
// Direction vector
const v1 = vertices[v1Idx];
const v2 = vertices[v2Idx];
const dx = v2.x - v1.x;
const dy = v2.y - v1.y;
const dz = v2.z - v1.z;
const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (len < 1e-10) {
console.warn(`Warning: Degenerate edge in face ${faceIdx}`);
continue;
}
step += `#${directionId}=DIRECTION('',(${(dx / len).toFixed(6)},${(dy / len).toFixed(6)},${(dz / len).toFixed(6)}));\n`;
step += `#${vectorId}=VECTOR('',#${directionId},${len.toFixed(6)});\n`;
step += `#${lineId}=LINE('',#${vertexIds[v1Idx]},#${vectorId});\n`;
step += `#${edgeCurveId}=EDGE_CURVE('',#${vertexPointIds[v1Idx]},#${vertexPointIds[v2Idx]},#${lineId},.T.);\n`;
step += `#${orientedEdgeId}=ORIENTED_EDGE('',*,*,#${edgeCurveId},.T.);\n`;
}
step += `#${edgeLoopId}=EDGE_LOOP('',(${orientedEdgeIds.map(eid => `#${eid}`).join(',')}));\n`;
step += `#${faceOuterBoundId}=FACE_OUTER_BOUND('',#${edgeLoopId},.T.);\n`;
// Create plane for face
const planeId = id++;
const axisPlacementId = id++;
const normalDirId = id++;
const refDirId = id++;
// Calculate face normal and reference direction
const v0 = vertices[faceVertices[0]];
const v1 = vertices[faceVertices[1]];
const v2 = vertices[faceVertices[2]];
const edge1 = v1.clone().sub(v0);
const edge2 = v2.clone().sub(v0);
const normal = edge1.clone().cross(edge2).normalize();
// Calculate face center for plane origin
const centerX = (v0.x + v1.x + v2.x) / 3;
const centerY = (v0.y + v1.y + v2.y) / 3;
const centerZ = (v0.z + v1.z + v2.z) / 3;
const centerPointId = id++;
step += `#${centerPointId}=CARTESIAN_POINT('',(${centerX.toFixed(6)},${centerY.toFixed(6)},${centerZ.toFixed(6)}));\n`;
step += `#${normalDirId}=DIRECTION('',(${normal.x.toFixed(6)},${normal.y.toFixed(6)},${normal.z.toFixed(6)}));\n`;
const refDir = edge1.normalize();
step += `#${refDirId}=DIRECTION('',(${refDir.x.toFixed(6)},${refDir.y.toFixed(6)},${refDir.z.toFixed(6)}));\n`;
step += `#${axisPlacementId}=AXIS2_PLACEMENT_3D('',#${centerPointId},#${normalDirId},#${refDirId});\n`;
step += `#${planeId}=PLANE('',#${axisPlacementId});\n`;
step += `#${faceId}=ADVANCED_FACE('',(#${faceOuterBoundId}),#${planeId},.T.);\n`;
}
// Product definition shape
const prodDefShapeId = id++;
step += `#${prodDefShapeId}=PRODUCT_DEFINITION_SHAPE('','',#${productDefId});\n`;
step += `#${id++}=SHAPE_DEFINITION_REPRESENTATION(#${prodDefShapeId},#${shapeRepId});\n`;
step += `ENDSEC;\nEND-ISO-10303-21;\n`;
return step;
}
/**
* Convert STL triangles to STEP file format with face merging
* Groups co-planar connected triangles into larger faces
*
* @param {Array} triangles - Array of triangle objects with vertices: [{v1, v2, v3}, ...]
* where v1, v2, v3 are {x, y, z} objects or Vector3 instances
* @param {Object} options - Optional parameters
* @param {string} options.productName - Name for the STEP product (default: 'mesh')
* @param {string} options.units - Units: 'mm', 'cm', 'm', 'inch' (default: 'mm')
* @param {number} options.angleTolerance - Angle tolerance in degrees for co-planarity (default: 1)
* @returns {string} STEP file content as a string
*/
export function meshToSTEPWithFaces(triangles, options = {}) {
const productName = options.productName || 'mesh';
const units = options.units || 'mm';
const angleTolerance = options.angleTolerance || 1;
const radianTolerance = angleTolerance * (Math.PI / 180);
// Convert triangles to flat vertex array for MeshTool
const vertices = [];
for (const tri of triangles) {
const v1 = tri.v1 || tri.vertices[0];
const v2 = tri.v2 || tri.vertices[1];
const v3 = tri.v3 || tri.vertices[2];
vertices.push(
v1.x, v1.y, v1.z,
v2.x, v2.y, v2.z,
v3.x, v3.y, v3.z
);
}
// Use MeshTool to index faces and find adjacency
const tool = new MeshTool();
tool.index(vertices);
const totalFaces = triangles.length;
const processed = {};
const surfaces = [];
// Find all connected surfaces
for (let i = 0; i < totalFaces; i++) {
if (processed[i]) continue;
// Find all connected co-planar faces starting from this face
const connectedFaces = tool.findConnectedSurface([i], radianTolerance, undefined, processed);
if (connectedFaces.length > 0) {
// Generate outline(s) for this surface
const outlines = tool.generateOutlines(connectedFaces);
if (outlines.length > 1) {
debug(`Surface with ${connectedFaces.length} faces has ${outlines.length} outlines:`);
outlines.forEach((outline, idx) => {
debug(` Outline ${idx}: ${outline.length} vertices`);
});
}
// Sort outlines by area (largest first = outer boundary)
// Use shoelace formula to calculate signed area
const outlinesWithArea = outlines.map(outline => {
let area = 0;
for (let i = 0; i < outline.length; i++) {
const p1 = outline[i];
const p2 = outline[(i + 1) % outline.length];
area += (p1.x * p2.y - p2.x * p1.y);
}
return { outline, area: Math.abs(area) };
});
// Sort by area descending (largest = outer boundary)
outlinesWithArea.sort((a, b) => b.area - a.area);
const sortedOutlines = outlinesWithArea.map(o => o.outline);
surfaces.push({ faces: connectedFaces, outlines: sortedOutlines });
}
}
debug(`[v4] Merged ${totalFaces} triangles into ${surfaces.length} faces`);
// Generate STEP file
let step = `ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('STL to STEP Conversion with Face Merging'),'2;1');
FILE_NAME('${productName}.step','${new Date().toISOString()}',(''),(''),'stl-to-step','','');
FILE_SCHEMA(('AUTOMOTIVE_DESIGN'));
ENDSEC;
DATA;
`;
let id = 1;
// Units mapping
const unitMultiplier = {
'mm': '.MILLI.',
'cm': '.CENTI.',
'm': '',
'inch': '.INCH.'
};
const unitPrefix = unitMultiplier[units] || '.MILLI.';
// Units and context
const lengthUnit = id++;
const planeAngleUnit = id++;
const solidAngleUnit = id++;
step += `#${lengthUnit}=(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(${unitPrefix},.METRE.));\n`;
step += `#${planeAngleUnit}=(NAMED_UNIT(*)PLANE_ANGLE_UNIT()SI_UNIT($,.RADIAN.));\n`;
step += `#${solidAngleUnit}=(NAMED_UNIT(*)SI_UNIT($,.STERADIAN.)SOLID_ANGLE_UNIT());\n`;
const uncertainty = id++;
step += `#${uncertainty}=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#${lengthUnit},'distance_accuracy_value','confusion accuracy');\n`;
// Geometric representation context
const geomContextId = id++;
step += `#${geomContextId}=(GEOMETRIC_REPRESENTATION_CONTEXT(3)GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#${uncertainty}))GLOBAL_UNIT_ASSIGNED_CONTEXT((#${lengthUnit},#${planeAngleUnit},#${solidAngleUnit}))REPRESENTATION_CONTEXT('ID1','3D'));\n`;
// Application context
const appContext = id++;
step += `#${appContext}=APPLICATION_CONTEXT('configuration controlled 3d designs of mechanical parts and assemblies');\n`;
const appProtocol = id++;
step += `#${appProtocol}=APPLICATION_PROTOCOL_DEFINITION('international standard','automotive_design',1998,#${appContext});\n`;
// Product definition
const productId = id++;
step += `#${productId}=PRODUCT('${productName}','${productName}','',(#${appContext}));\n`;
const productDefFormId = id++;
step += `#${productDefFormId}=PRODUCT_DEFINITION_FORMATION('','',#${productId});\n`;
const productDefCtx = id++;
step += `#${productDefCtx}=PRODUCT_DEFINITION_CONTEXT('part definition',#${appContext},'design');\n`;
const productDefId = id++;
step += `#${productDefId}=PRODUCT_DEFINITION('design','',#${productDefFormId},#${productDefCtx});\n`;
// Shape representation - reserve ID
const shapeRepId = id++;
// Build vertex map and IDs
const vertexMap = new Map();
const cartesianPointIds = new Map();
const vertexPointIds = new Map();
function getOrCreateVertex(x, y, z) {
const key = `${x.toFixed(6)},${y.toFixed(6)},${z.toFixed(6)}`;
if (!vertexMap.has(key)) {
// Create cartesian point
const pointId = id++;
step += `#${pointId}=CARTESIAN_POINT('',(${x.toFixed(6)},${y.toFixed(6)},${z.toFixed(6)}));\n`;
cartesianPointIds.set(key, pointId);
// Create vertex point
const vpId = id++;
step += `#${vpId}=VERTEX_POINT('',#${pointId});\n`;
vertexPointIds.set(key, vpId);
vertexMap.set(key, { pointId, vpId, x, y, z });
}
return vertexMap.get(key);
}
// Build global edge map - each unique edge gets one EDGE_CURVE
const edgeMap = new Map();
function getOrCreateEdge(v1, v2, stepBuffer) {
// Create consistent key regardless of vertex order
const key1 = `${v1.pointId},${v2.pointId}`;
const key2 = `${v2.pointId},${v1.pointId}`;
let edgeData = edgeMap.get(key1) || edgeMap.get(key2);
if (!edgeData) {
const dx = v2.x - v1.x;
const dy = v2.y - v1.y;
const dz = v2.z - v1.z;
const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
const edgeCurveId = id++;
const lineId = id++;
const vectorId = id++;
const directionId = id++;
stepBuffer.append = stepBuffer.append || '';
stepBuffer.append += `#${directionId}=DIRECTION('',(${(dx / len).toFixed(6)},${(dy / len).toFixed(6)},${(dz / len).toFixed(6)}));\n`;
stepBuffer.append += `#${vectorId}=VECTOR('',#${directionId},${len.toFixed(6)});\n`;
stepBuffer.append += `#${lineId}=LINE('',#${v1.pointId},#${vectorId});\n`;
stepBuffer.append += `#${edgeCurveId}=EDGE_CURVE('',#${v1.vpId},#${v2.vpId},#${lineId},.T.);\n`;
edgeData = { edgeCurveId, v1, v2, len };
edgeMap.set(key1, edgeData);
}
return edgeData;
}
// Advanced B-rep shape representation
const manifoldId = id++;
const closedShellId = id++;
step += `#${shapeRepId}=ADVANCED_BREP_SHAPE_REPRESENTATION('',(#${manifoldId}),#${geomContextId});\n`;
step += `#${manifoldId}=MANIFOLD_SOLID_BREP('',#${closedShellId});\n`;
// We'll write the CLOSED_SHELL after creating faces
// Track which face IDs are actually created
const createdFaceIds = [];
// Helper function to merge co-linear edges in an outline
function mergeColinearEdges(outline) {
if (outline.length < 3) return outline;
const tolerance = 1e-6;
// Helper to check if three points are co-linear
function areColinear(p1, p2, p3) {
const v1 = new Vector3(p2.x - p1.x, p2.y - p1.y, p2.z - p1.z);
const v2 = new Vector3(p3.x - p2.x, p3.y - p2.y, p3.z - p2.z);
const len1 = v1.length();
const len2 = v2.length();
if (len1 < tolerance || len2 < tolerance) return true;
v1.normalize();
v2.normalize();
// Check if vectors are parallel (dot product close to 1 or -1)
const dot = Math.abs(v1.dot(v2));
return dot > 1 - tolerance;
}
// Mark vertices to keep
const keep = new Array(outline.length).fill(true);
// Check each vertex to see if it's on a straight line
for (let i = 0; i < outline.length; i++) {
const prev = outline[(i - 1 + outline.length) % outline.length];
const curr = outline[i];
const next = outline[(i + 1) % outline.length];
if (areColinear(prev, curr, next)) {
keep[i] = false;
}
}
// Build merged array with only kept vertices
const merged = [];
for (let i = 0; i < outline.length; i++) {
if (keep[i]) {
merged.push(outline[i]);
}
}
return merged.length >= 3 ? merged : outline;
}
// Buffer to collect face geometry before writing CLOSED_SHELL
let faceStepData = '';
// Create each merged face
for (let surfaceIdx = 0; surfaceIdx < surfaces.length; surfaceIdx++) {
const surface = surfaces[surfaceIdx];
if (!surface.outlines || surface.outlines.length === 0) {
console.warn(`Skipping invalid surface ${surfaceIdx} with no outlines`);
continue;
}
// Allocate face ID now (only for valid surfaces)
const faceId = id++;
createdFaceIds.push(faceId);
// Process all outlines: first is outer boundary, rest are holes
const boundIds = [];
let allVertices = [];
for (let outlineIdx = 0; outlineIdx < surface.outlines.length; outlineIdx++) {
let outline = surface.outlines[outlineIdx];
if (!outline || outline.length < 3) {
console.warn(`Skipping invalid outline ${outlineIdx} in surface ${surfaceIdx}`);
continue;
}
// Merge co-linear edges
const originalLength = outline.length;
outline = mergeColinearEdges(outline);
if (originalLength !== outline.length) {
debug(` Merged outline ${outlineIdx}: ${originalLength} -> ${outline.length} vertices`);
}
if (outline.length < 3) {
console.warn(`Outline ${outlineIdx} in surface ${surfaceIdx} has < 3 vertices after merging`);
continue;
}
const boundId = id++;
const edgeLoopId = id++;
// Create edges for this outline
const orientedEdgeIds = [];
const vertices = [];
// Get vertices and create vertex records
for (let i = 0; i < outline.length; i++) {
const pt = outline[i];
const vertex = getOrCreateVertex(pt.x, pt.y, pt.z);
vertices.push(vertex);
}
// Store vertices from first outline for normal calculation
if (outlineIdx === 0) {
allVertices = vertices;
}
// Create edges - for holes (outlineIdx > 0), use opposite orientation
const isHole = outlineIdx > 0;
// Buffer for edge geometry (needs to be written before ORIENTED_EDGEs)
const edgeBuffer = {};
const orientedEdgeData = []; // Store oriented edge info to write after EDGE_CURVEs
for (let i = 0; i < vertices.length; i++) {
const v1 = vertices[i];
const v2 = vertices[(i + 1) % vertices.length];
// Get or create shared edge
const edgeData = getOrCreateEdge(v1, v2, edgeBuffer);
if (edgeData.len < 1e-10) {
console.warn(`Warning: Degenerate edge in surface ${surfaceIdx}, outline ${outlineIdx}`);
continue;
}
// Determine orientation - check if edge is stored in same direction we're traversing
const sameDirection = (edgeData.v1.pointId === v1.pointId && edgeData.v2.pointId === v2.pointId);
let edgeOrientation;
if (isHole) {
// For holes, flip the orientation
edgeOrientation = sameDirection ? '.F.' : '.T.';
} else {
// For outer boundary, use natural orientation
edgeOrientation = sameDirection ? '.T.' : '.F.';
}
const orientedEdgeId = id++;
orientedEdgeIds.push(orientedEdgeId);
orientedEdgeData.push({ orientedEdgeId, edgeCurveId: edgeData.edgeCurveId, edgeOrientation });
}
// Write edge geometry first (EDGE_CURVEs must exist before ORIENTED_EDGEs reference them)
if (edgeBuffer.append) {
faceStepData += edgeBuffer.append;
}
// Now write ORIENTED_EDGEs
for (const oed of orientedEdgeData) {
faceStepData += `#${oed.orientedEdgeId}=ORIENTED_EDGE('',*,*,#${oed.edgeCurveId},${oed.edgeOrientation});\n`;
}
if (orientedEdgeIds.length === 0) {
console.warn(`No valid edges in outline ${outlineIdx} of surface ${surfaceIdx}`);
continue;
}
faceStepData += `#${edgeLoopId}=EDGE_LOOP('',(${orientedEdgeIds.map(eid => `#${eid}`).join(',')}));\n`;
// First outline is FACE_OUTER_BOUND, subsequent ones are holes (FACE_BOUND)
if (outlineIdx === 0) {
faceStepData += `#${boundId}=FACE_OUTER_BOUND('',#${edgeLoopId},.T.);\n`;
} else {
faceStepData += `#${boundId}=FACE_BOUND('',#${edgeLoopId},.T.);\n`;
}
boundIds.push(boundId);
}
if (boundIds.length === 0 || allVertices.length < 3) {
console.warn(`Skipping surface ${surfaceIdx} - no valid bounds`);
// Remove the face ID we allocated since we're not creating this face
createdFaceIds.pop();
continue;
}
// Calculate face normal from first three vertices of outer boundary
const v0 = new Vector3(allVertices[0].x, allVertices[0].y, allVertices[0].z);
const v1 = new Vector3(allVertices[1].x, allVertices[1].y, allVertices[1].z);
const v2 = new Vector3(allVertices[2].x, allVertices[2].y, allVertices[2].z);
const edge1 = v1.clone().sub(v0);
const edge2 = v2.clone().sub(v0);
const normal = edge1.clone().cross(edge2).normalize();
// Calculate face center for plane origin
let centerX = 0, centerY = 0, centerZ = 0;
for (let vertex of allVertices) {
centerX += vertex.x;
centerY += vertex.y;
centerZ += vertex.z;
}
centerX /= allVertices.length;
centerY /= allVertices.length;
centerZ /= allVertices.length;
const centerPointId = id++;
faceStepData += `#${centerPointId}=CARTESIAN_POINT('',(${centerX.toFixed(6)},${centerY.toFixed(6)},${centerZ.toFixed(6)}));\n`;
const planeId = id++;
const axisPlacementId = id++;
const normalDirId = id++;
const refDirId = id++;
faceStepData += `#${normalDirId}=DIRECTION('',(${normal.x.toFixed(6)},${normal.y.toFixed(6)},${normal.z.toFixed(6)}));\n`;
const refDir = edge1.normalize();
faceStepData += `#${refDirId}=DIRECTION('',(${refDir.x.toFixed(6)},${refDir.y.toFixed(6)},${refDir.z.toFixed(6)}));\n`;
faceStepData += `#${axisPlacementId}=AXIS2_PLACEMENT_3D('',#${centerPointId},#${normalDirId},#${refDirId});\n`;
faceStepData += `#${planeId}=PLANE('',#${axisPlacementId});\n`;
// Create ADVANCED_FACE with all bounds (outer + holes)
faceStepData += `#${faceId}=ADVANCED_FACE('',(${boundIds.map(bid => `#${bid}`).join(',')}),#${planeId},.T.);\n`;
}
// Now write CLOSED_SHELL with only the faces that were actually created
debug(`[v4] Created ${createdFaceIds.length} faces for CLOSED_SHELL (expected ${surfaces.length})`);
debug(`[v4] Shared ${edgeMap.size} unique edges across all faces`);
step += `#${closedShellId}=CLOSED_SHELL('',(${createdFaceIds.map(fid => `#${fid}`).join(',')}));\n`;
// Append all the face geometry
step += faceStepData;
// Product definition shape
const prodDefShapeId = id++;
step += `#${prodDefShapeId}=PRODUCT_DEFINITION_SHAPE('','',#${productDefId});\n`;
step += `#${id++}=SHAPE_DEFINITION_REPRESENTATION(#${prodDefShapeId},#${shapeRepId});\n`;
step += `ENDSEC;\nEND-ISO-10303-21;\n`;
return step;
}

View file

@ -6,12 +6,29 @@ import '../add/three.js';
import '../kiri/core/lang.js';
import '../kiri/core/lang-en.js';
import { broker } from '../moto/broker.js';
import { run } from '../kiri/core/init.js';
let traceload = location.search.indexOf('traceload') > 0;
let load = [];
if (traceload) {
broker.subscribe([
"init.one",
"init.two",
"init.lang",
"init-done",
"load-done",
], (msg, topic) => {
console.log(topic, '->', msg);
})
}
function safeExec(fn) {
try {
if (traceload) {
console.log('kiri | exec |', fn);
}
fn(kiri.api);
} catch (error) {
console.log('load error', fn, error);
@ -20,13 +37,32 @@ function safeExec(fn) {
function checkReady() {
if (document.readyState === 'complete') {
kiri.api = run();
self.$ = kiri.api.web.$;
let bootctrl = navigator.serviceWorker.controller;
console.log(`kiri | boot ctrl | ` + (bootctrl ? true : false));
let api = kiri.api = run();
self.$ = api.web.$;
for (let fn of load) {
safeExec(fn);
}
load = undefined;
kiri.api.event.emit('load-done', stats);
api.event.emit('load-done', stats);
if (api.electron) {
$('install').classList.add('hide');
$('app-quit').classList.remove('hide');
$('app-name-text').innerText = "More Info";
$('top-sep').style.display = 'flex';
} else if (bootctrl) {
$('install').classList.add('hide');
$('uninstall').classList.remove('hide');
$('uninstall').onclick = () => {
bootctrl.postMessage({ clear: true, disable: true });
location.reload();
}
} else {
$('install').onclick = () => {
location.replace('/boot');
}
}
}
}

View file

@ -99,6 +99,7 @@ function init() {
// for electron
self.mesh = { api };
self.electron = navigator.userAgent.includes('Electron');
}
// restore space layout and view from previous session
@ -187,7 +188,11 @@ async function restore_space() {
// hide loading curtain
$d('curtain','none');
// restore handles visibility
handles.setEnabled(api.prefs.map.space.bounds);
handles.setEnabled(api.prefs.map.space.bounds ?? false);
// restore script if was showing
if (api.prefs.map.space.script) {
api.script.show();
}
if (api.prefs.map.info.welcome !== false) {
api.welcome(version);
}
@ -224,7 +229,11 @@ function space_init(data) {
if (evt.key === '?') {
return api.welcome(version);
}
let { shiftKey, metaKey, ctrlKey, code } = evt;
let { shiftKey, metaKey, ctrlKey, code, target } = evt;
if (target.nodeName === 'TEXTAREA') {
api.script.changed();
return;
}
switch (code) {
case 'Digit1':
return api.mode.sketch();
@ -286,7 +295,26 @@ function space_init(data) {
}
},
'keydown', evt => {
let { shiftKey, metaKey, ctrlKey, code } = evt;
let { shiftKey, metaKey, ctrlKey, code, target } = evt;
if (target.nodeName === 'TEXTAREA') {
if (code === 'Tab') {
estop(evt);
const start = target.selectionStart;
const end = target.selectionEnd;
const val = target.value;
const upto = val.slice(0, start);
const col = start - upto.lastIndexOf('\n') - 1;
const tab = ' '.substring(col % 4);
target.value = val.slice(0, start) + tab + val.slice(end);
target.selectionStart = target.selectionEnd = start + tab.length;
}
if (code === 'Enter' && metaKey) {
api.script.execute();
} else {
api.script.changed();
}
return;
}
let once = keyOnce[code];
if (once) {
delete keyOnce[code];
@ -471,7 +499,7 @@ function space_init(data) {
} else if (sketch_item) {
sketch_item.toggle();
} else {
console.log('unknown selection');
// console.log('unknown selection');
}
} else {
// return objects upSelect can choose from

View file

@ -1,34 +0,0 @@
const service_worker = navigator.serviceWorker;
import { version } from '../moto/license.js';
async function start_service_worker() {
function debug() {
console.log(`[${version}]`, ...arguments);
}
// install service worker
debug('service worker registration');
try {
// const reg = await navigator.serviceWorker.register("/src/moto/service.js?013", { scope: "/" });
const reg = await navigator.serviceWorker.register("/code/service.js?"+version, { scope: "/" });
if (reg.installing) {
debug('service worker installing');
} else if (reg.waiting) {
debug('service worker waiting');
} else if (reg.active) {
debug('service worker active');
} else {
debug({ service_worker: reg });
}
if (service_worker.controller) {
service_worker.controller.postMessage('ctrl message');
}
} catch (err) {
debug('service worker registration failed', err);
}
}
if (service_worker && self.enable_service) start_service_worker();

View file

@ -11,6 +11,7 @@ import { edges as meshEdges } from './edges.js';
import { group as meshGroup } from './group.js';
import { model as meshModel } from './model.js';
import { sketch as meshSketch } from './sketch.js';
import { script } from './script.js';
import { newPolygon } from '../geo/polygon.js';
import { THREE } from '../ext/three.js';
import { $, $C, h } from '../moto/webui.js';
@ -79,6 +80,7 @@ const selection = {
selected = objects.map(o => o.models ? o.models : o).flat();
tools = toolist || [];
meshUtil.defer(selection.update);
return api.selection;
},
// @param group {MeshObject}
@ -770,12 +772,55 @@ let add = {
api.modal.bound.genvrt.focus();
},
plane() {
const [ x,y ] = [ ...arguments ];
const box = new THREE.PlaneGeometry(1,1).toNonIndexed();
const vert = box.attributes.position.array;
const nmdl = new meshModel({ file: "plane", mesh: vert });
nmdl._loft = function(plane) {
function uv(v) {
let m = new Set();
let g = [...v].group(3).filter(a => {
let k = a.map(v => v.round(5)).join('-');
if (m.has(k)) return false;
m.add(k); return true;
});
return [ g[0], g[1], g[3], g[2] ];
};
let s0 = uv(vert);
let s1 = uv(plane.attributes.position.array);
let v = [];
for (let i=0; i<4; i++) {
let p0 = [ s0[i], s0[(i+1)%4] ];
let p1 = [ s1[i], s1[(i+1)%4] ];
v.push(...p1[1]);
v.push(...p0[1]);
v.push(...p0[0]);
v.push(...p1[0]);
v.push(...p1[1]);
v.push(...p0[0]);
}
let pa = [...plane.attributes.position.array];
v.push(...pa.group(3).reverse().flat());
v.push(...vert);
const nmdl = new meshModel({ file: "loft", mesh: v.toFloat32() });
const ngrp = api.group.new([ nmdl ]);
selection.set([ nmdl ]);
return ngrp;
};
const ngrp = api.group.new([ nmdl ]);
ngrp.scale(x ?? 10, y ?? 10, 1).floor();
selection.set([ nmdl ]);
return ngrp;
},
cube() {
const [ x,y,z ] = [ ...arguments ].map(v => parseFloat(v) || undefined);
const box = new THREE.BoxGeometry(1,1,1).toNonIndexed();
const vert = box.attributes.position.array;
const nmdl = new meshModel({ file: "box", mesh: vert });
const ngrp = api.group.new([ nmdl ]);
ngrp.scale(10, 10, 10).floor();
ngrp.scale(x ?? 10, y ?? 10, z ?? 10).floor();
selection.set([ nmdl ]);
return ngrp;
},
@ -964,7 +1009,8 @@ let file = {
]),
h.div([
h.button({ _: "download OBJ", onclick() { doit('obj') } }),
h.button({ _: "download STL", onclick() { doit('stl') } })
h.button({ _: "download STL", onclick() { doit('stl') } }),
h.button({ _: "download STEP", onclick() { doit('step') } }),
])
]) ]
});
@ -984,7 +1030,7 @@ const tool = {
return log('nothing to merge');
}
log(`merging ${models.length} models`).pin();
worker.model_merge(models.map(m => {
return worker.model_merge(models.map(m => {
return { id: m.id, matrix: m.matrix }
}))
.then(data => {
@ -995,6 +1041,7 @@ const tool = {
api.selection.visible(false);
api.selection.set([group]);
log('merge complete').unpin();
return group;
});
},
@ -1074,7 +1121,7 @@ const tool = {
return log('nothing to subtract');
}
log(`subtract ${models.length} models`).pin();
worker.model_subtract(models.map(m => {
return worker.model_subtract(models.map(m => {
return { id: m.id, tool: m.tool() }
}))
.then(data => {
@ -1084,6 +1131,7 @@ const tool = {
})]).promote();
api.selection.delete();
api.selection.set([group]);
return group;
})
.catch(error => {
log(`subtract error: ${error}`);
@ -1106,7 +1154,7 @@ const tool = {
for (let m of models) {
nm.push(m.duplicate({ select: dup_sel, shift: dup_shift }));
}
tool.regroup(nm);
return tool.regroup(nm);
},
mirror() {
@ -1397,6 +1445,7 @@ const prefs = {
dark: false,
floor: false,
grid: true,
script: false,
select: [],
snap: 1,
snapon: false,
@ -1515,8 +1564,8 @@ const api = {
},
clear() {
for (let group of group.list()) {
group.remove(group);
for (let grp of group.list()) {
group.remove(grp);
}
},
@ -1645,6 +1694,8 @@ const api = {
prefs,
script,
selection,
sketch,

View file

@ -383,7 +383,7 @@ function menu_item(text, fn, short, id) {
// create html elements
function ui_build() {
let { bind, div, input, hr, label } = h;
let { bind, button, div, input, hr, label, span, textarea } = h;
let { file, selection, mode, tool, sketch, prefs, add } = api;
let trash = FontAwesome.icon({ prefix: "fas", iconName: "trash" }).html[0];
let eye_open = FontAwesome.icon({ prefix: "fas", iconName: "eye" }).html[0];
@ -395,13 +395,14 @@ function ui_build() {
div('File'),
div({ class: "menu-items" }, [
input({
id: "import", type: "file", class: ["hide"], multiple: true,
id: "import", type: "file", class: ["hide"], multiple: true, accept:".stl,.obj",
onchange(evt) { broker.send.load_files(evt.target.files) }
}),
menu_item('Import', file.import, 'I'),
menu_item('Export', file.export, 'X'),
hr(),
menu_item('Slicer', api.kirimoto),
menu_item('Script', api.script.toggle),
hr(),
menu_item('Close', window.close),
])
@ -579,6 +580,15 @@ function ui_build() {
div({ id: 'objecttools', class: "tools sketch-off" }),
div({ id: 'logger', onmouseover() { log.show() } }),
]),
div({ id: 'script', class: "hide" }, [
div({ id: 'script-header' }, [
span("script"),
div({ class: "grow" }),
button({ _: "run", onclick: api.script.execute }),
button({ _: "&times;", class: "close", onclick: api.script.hide })
]),
textarea({ id: 'script-editor', rows: 30, cols: 65 }),
]),
div({ id: 'grouplist' }),
div({ id: 'selectlist' }),
div({ id: 'download', class: "hide" })

View file

@ -41,6 +41,11 @@ class MeshGroup extends meshObject {
return box3;
}
rename(name) {
this.name = name;
return this;
}
// @param model {MeshModel}
add(model) {
model.group = this;

View file

@ -334,6 +334,7 @@ class MeshModel extends meshObject {
rename(file) {
this.file = file;
this.sync();
return this;
}
// sync to worker and indexeddb for page restoration or worker ops

58
src/mesh/script.js Normal file
View file

@ -0,0 +1,58 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { api } from '../mesh/api.js';
import { $, h, estop } from '../moto/webui.js';
export const script = {
running: false,
visible: false,
changed() {
localStorage.script = $('script-editor').value;
},
error(e) {
api.modal.dialog({
title: "script error",
body: [ h.div({ id: "script-error", _: e.message || e.toString() }) ]
});
},
hide() {
$('script').classList.add('hide');
api.prefs.save(api.prefs.map.space.script = script.visible = false);
},
show() {
$('script').classList.remove('hide');
$('script-editor').value = localStorage.script ?? '';
api.prefs.save(api.prefs.map.space.script = script.visible = true);
},
toggle() {
if (script.visible) {
script.hide();
} else {
script.show();
}
},
execute() {
let cmd = localStorage.script || `console.log('no script')`;
if (cmd) {
if (script.running) {
return script.error('script running');
}
cmd = [
'(async () => {',
cmd,
'})()',
'.catch(error => {',
'api.script.error(error)',
'})',
'.finally(() => {',
'api.script.running = false;',
'});'
].join('\n');
try {
eval(`{ \n ${cmd} \n }`);
} catch (e) {
script.error(e);
}
}
}
};

View file

@ -16,6 +16,7 @@ import { polygons } from '../geo/polygons.js';
import { CSG } from '../geo/csg.js';
import { util } from './util.js';
import { tool as meshTool } from './tool.js';
import { meshToSTEPWithFaces } from '../load/step.js';
const { Triangle, Vector3, BufferGeometry, BufferAttribute, computeFaceNormal } = THREE;
@ -604,6 +605,17 @@ const file = {
vtot += (vs.length / 3);
}
switch (format) {
case "step":
let varr = [];
for (let rec of recs) {
let tris = rec.varr
.group(3)
.map(r => { return { x:r[0], y:r[1], z:r[2] } })
.group(3)
.map(f => { return { v1: f[0], v2: f[1], v3: f[2] } });
varr.push(...tris);
}
return meshToSTEPWithFaces(varr);
case "obj":
let p = 1;
let obj = [header];

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.1"
VERSION: "4.5.0"
};
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();
}