commit
daebb07ee1
18 changed files with 792 additions and 44 deletions
|
|
@ -54,6 +54,21 @@
|
||||||
"camDrillLift": 2,
|
"camDrillLift": 2,
|
||||||
"camDrillingOn": false,
|
"camDrillingOn": false,
|
||||||
"camRegisterSpeed": 1000,
|
"camRegisterSpeed": 1000,
|
||||||
|
"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",
|
"camFlipAxis": "X",
|
||||||
"camFlipOther": "",
|
"camFlipOther": "",
|
||||||
"camTabsWidth": 20,
|
"camTabsWidth": 20,
|
||||||
|
|
|
||||||
|
|
@ -538,6 +538,10 @@ export function arcToPath(start, end, arcdivs = 24, opts) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(arcdivs <= 2){
|
||||||
|
return [start.clone(), end.clone()];
|
||||||
|
}
|
||||||
|
|
||||||
if (center) {
|
if (center) {
|
||||||
// center = center.add(start);
|
// center = center.add(start);
|
||||||
center.r = center.distTo2D(start);
|
center.r = center.distTo2D(start);
|
||||||
|
|
@ -569,27 +573,17 @@ export function arcToPath(start, end, arcdivs = 24, opts) {
|
||||||
let a2 = Math.atan2(center.y - end.y, center.x - end.x) + Math.PI;
|
let a2 = Math.atan2(center.y - end.y, center.x - end.x) + Math.PI;
|
||||||
let ad = base.util.thetaDiff(a1, a2, clockwise); // angle difference in radians
|
let ad = base.util.thetaDiff(a1, a2, clockwise); // angle difference in radians
|
||||||
let samePoint = Math.abs(ad) < 0.001
|
let samePoint = Math.abs(ad) < 0.001
|
||||||
let ofFull = Math.abs(ad) / (2 * Math.PI);
|
let ofFull = Math.abs(ad)/(2*Math.PI);
|
||||||
let steps = samePoint ? arcdivs : Math.max(Math.floor(arcdivs * ofFull), 4);
|
let steps = samePoint? arcdivs : Math.max(Math.floor( arcdivs * ofFull),4);
|
||||||
let step = (samePoint ? (Math.PI * 2) : ad) / steps;
|
let step = (samePoint? (Math.PI*2)*(clockwise? -1 : 1) : ad) / steps;
|
||||||
let numPoints = steps / step;
|
|
||||||
let zStart = start.z;
|
let zStart = start.z;
|
||||||
let zStep = dz / numPoints;
|
let zStep = -dz / steps;
|
||||||
let rot = a1 + step;
|
let rot = a1;
|
||||||
|
|
||||||
//unused deltas
|
//unused deltas
|
||||||
let da = Math.abs(a1 - a2);
|
let da = Math.abs(a1 - a2);
|
||||||
let dd = Math.sqrt(dx * dx + dy * dy);
|
let dd = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
// LOG({index, da, dd, first: pos, last: rec, center, a1, a2, ad, step, steps, rot, line});
|
|
||||||
// G0G1(false, [`X${center.x}`, `Y${center.y}`, `E1`]);
|
|
||||||
|
|
||||||
// under 1 degree arc and 5mm, convert to straight line
|
|
||||||
// if (da < 0.005 && dd < 5) {
|
|
||||||
// G0G1(false, [`X${end.x}`, `Y${end.y}`, `E1`]);
|
|
||||||
// return ;
|
|
||||||
// }
|
|
||||||
|
|
||||||
let arr = [] // point accumulator
|
let arr = [] // point accumulator
|
||||||
for (let i = 0; i <= steps - 2; i++) {
|
for (let i = 0; i <= steps - 2; i++) {
|
||||||
if (isNaN(center.r) || isNaN(center.x) || isNaN(center.y)) {
|
if (isNaN(center.r) || isNaN(center.x) || isNaN(center.y)) {
|
||||||
|
|
|
||||||
|
|
@ -518,6 +518,21 @@ export const conf = {
|
||||||
camDrillingOn: false,
|
camDrillingOn: false,
|
||||||
camRegisterSpeed: 1000,
|
camRegisterSpeed: 1000,
|
||||||
camRegisterThru: 5,
|
camRegisterThru: 5,
|
||||||
|
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",
|
camFlipAxis: "X",
|
||||||
camFlipOther: "",
|
camFlipOther: "",
|
||||||
camLaserEnable: ["M321"],
|
camLaserEnable: ["M321"],
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,11 @@ const LISTS = {
|
||||||
{ name: "inside" },
|
{ name: "inside" },
|
||||||
{ name: "outside" }
|
{ name: "outside" }
|
||||||
],
|
],
|
||||||
|
helicaloff:[
|
||||||
|
{ name: "auto" },
|
||||||
|
{ name: "inside" },
|
||||||
|
{ name: "outside" },
|
||||||
|
],
|
||||||
zanchor: [
|
zanchor: [
|
||||||
{ name: "top" },
|
{ name: "top" },
|
||||||
{ name: "middle" },
|
{ name: "middle" },
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ export const UI = {
|
||||||
bound,
|
bound,
|
||||||
toInt,
|
toInt,
|
||||||
toFloat,
|
toFloat,
|
||||||
|
toDegsFloat,
|
||||||
isSticky,
|
isSticky,
|
||||||
setSticky,
|
setSticky,
|
||||||
newBoolean,
|
newBoolean,
|
||||||
|
|
@ -334,6 +335,18 @@ function toFloat() {
|
||||||
return nv;
|
return nv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toDegsFloat(){
|
||||||
|
let nv = this.value !== '' ? parseFloat(this.value) : null;
|
||||||
|
if (nv !== null && this.bound) nv = this.bound(nv);
|
||||||
|
nv = (nv+360)% 360; // bound the val to 0-359.99
|
||||||
|
if (this.setv) {
|
||||||
|
return this.setv(nv);
|
||||||
|
} else {
|
||||||
|
this.value = nv;
|
||||||
|
}
|
||||||
|
return nv;
|
||||||
|
}
|
||||||
|
|
||||||
function raw() {
|
function raw() {
|
||||||
return this.value !== '' ? this.value : null;
|
return this.value !== '' ? this.value : null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
63
src/kiri/mode/cam/cl-helical.js
Normal file
63
src/kiri/mode/cam/cl-helical.js
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { api } from "../../core/api.js";
|
||||||
|
import { CAM } from "./driver-fe.js";
|
||||||
|
import { clearPops, env } from "./client.js";
|
||||||
|
|
||||||
|
let alert, lastWidget;
|
||||||
|
export let helicalOn = false;
|
||||||
|
export function selectHelical() {
|
||||||
|
if (helicalOn) {
|
||||||
|
return helicalDone();
|
||||||
|
}
|
||||||
|
clearPops();
|
||||||
|
alert = api.show.alert("analyzing surfaces...", 1000);
|
||||||
|
let cylinders = env.poppedRec.cylinders;
|
||||||
|
|
||||||
|
CAM.surface_prep(false, () => {
|
||||||
|
api.hide.alert(alert);
|
||||||
|
alert = api.show.alert("[esc] cancels surface selection");
|
||||||
|
for (let [wid, arr] of Object.entries(cylinders)) {
|
||||||
|
let widget = api.widgets.forid(wid);
|
||||||
|
if ( widget && arr?.length )
|
||||||
|
for (let faceid of arr) {
|
||||||
|
CAM.cylinderToggle(widget, faceid, (faceids) => {
|
||||||
|
// cylinders[widget.id] = faceids;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
helicalOn = env.hoveredOp;
|
||||||
|
helicalOn.classList.add("editing");
|
||||||
|
api.feature.on_mouse_up = (obj, ev) => {
|
||||||
|
let { face } = obj;
|
||||||
|
let min = Math.min(face.a, face.b, face.c);
|
||||||
|
let faceid = min / 3;
|
||||||
|
let widget = (lastWidget = obj.object.widget);
|
||||||
|
CAM.cylinderToggle(widget, faceid, ({ faces, error }) => {
|
||||||
|
if ( error ) {
|
||||||
|
api.show.alert(error, 3000);
|
||||||
|
console.log( error );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cylinders[widget.id] = faces;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function helicalDone(){
|
||||||
|
if (!( helicalOn && env.poppedRec && env.poppedRec.cylinders )) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let { cylinders } = env.poppedRec;
|
||||||
|
for (let wid of Object.keys(cylinders)) {
|
||||||
|
let widget = api.widgets.forid(wid);
|
||||||
|
if (widget) {
|
||||||
|
CAM.cylinderClear(widget);
|
||||||
|
} else {
|
||||||
|
delete cylinders[wid];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
api.hide.alert(alert);
|
||||||
|
api.feature.on_mouse_up = undefined;
|
||||||
|
helicalOn.classList.remove("editing");
|
||||||
|
helicalOn = false;
|
||||||
|
};
|
||||||
|
|
@ -140,17 +140,7 @@ export async function selectHoles(individual) {
|
||||||
|
|
||||||
export function selectHolesHover(data) {
|
export function selectHolesHover(data) {
|
||||||
//not used right now. may be useful in the future
|
//not used right now. may be useful in the future
|
||||||
if (lastTrace) {
|
|
||||||
let { color, colorSave } = lastTrace.material[0] || lastTrace.material;
|
|
||||||
color.r = colorSave.r;
|
|
||||||
color.g = colorSave.g;
|
|
||||||
color.b = colorSave.b;
|
|
||||||
lastTrace.position.z -= 0.01;
|
|
||||||
}
|
|
||||||
if (data.type === 'platform') {
|
|
||||||
setLastTrace();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectHolesHoverUp(int, ev) {
|
export function selectHolesHoverUp(int, ev) {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { env } from './client.js';
|
||||||
import { Tool } from './tool.js';
|
import { Tool } from './tool.js';
|
||||||
import { opFlip } from './cl-flip.js';
|
import { opFlip } from './cl-flip.js';
|
||||||
import { selectHoles } from './cl-hole.js';
|
import { selectHoles } from './cl-hole.js';
|
||||||
|
import { selectHelical } from './cl-helical.js';
|
||||||
import { surfaceAdd } from './cl-surface.js';
|
import { surfaceAdd } from './cl-surface.js';
|
||||||
import { traceAdd } from './cl-trace.js';
|
import { traceAdd } from './cl-trace.js';
|
||||||
|
|
||||||
|
|
@ -524,6 +525,49 @@ export function createPopOps() {
|
||||||
thru: UC.newInput(LANG.cd_thru_s, { title: LANG.cd_thru_l, convert: UC.toFloat, units: true }),
|
thru: UC.newInput(LANG.cd_thru_s, { title: LANG.cd_thru_l, convert: UC.toFloat, units: true }),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
createPopOp('helical', {
|
||||||
|
tool: 'camHelicalTool',
|
||||||
|
offset: 'camHelicalOffset',
|
||||||
|
spindle: 'camHelicalSpindle',
|
||||||
|
rate: 'camHelicalDownSpeed',
|
||||||
|
feed: 'camHelicalSpeed',
|
||||||
|
down: 'camHelicalDown',
|
||||||
|
finish: 'camHelicalBottomFinish',
|
||||||
|
startAng:'camHelicalStartAngle',
|
||||||
|
offOver: 'camHelicalOffsetOverride',
|
||||||
|
entry: 'camHelicalEntry',
|
||||||
|
entryOffset: 'camHelicalEntryOffset',
|
||||||
|
reverse: 'camHelicalReverse',
|
||||||
|
clockwise:'camHelicalClockwise',
|
||||||
|
thru: 'camHelicalThru',
|
||||||
|
forceStartAng:'camHelicalForceStartAngle',
|
||||||
|
fromTop: 'camHelicalFromStockTop',
|
||||||
|
}).inputs = {
|
||||||
|
tool: UC.newSelect(LANG.cc_tool, {}, "tools"),
|
||||||
|
offset: UC.newSelect(LANG.cc_offs_s, {title: LANG.cc_offs_l,}, "helicaloff"),
|
||||||
|
sep: UC.newBlank({class:"pop-sep"}),
|
||||||
|
spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}),
|
||||||
|
rate: UC.newInput(LANG.cc_plng_s, {title:LANG.cc_plng_l, convert:UC.toFloat, units:true}),
|
||||||
|
feed: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toFloat, units:true}),
|
||||||
|
down: UC.newInput(LANG.ch_sdwn_s, {title:LANG.ch_sdwn_l, convert:UC.toFloat, units:true}),
|
||||||
|
startAng: UC.newInput(LANG.ch_stra_s, {title:LANG.ch_stra_l, convert:UC.toDegsFloat, bound:UC.bound(-360,360),show:() => env.poppedRec.forceStartAng}),
|
||||||
|
offOver: UC.newInput(LANG.cc_offd_s, {title:LANG.cc_offd_l, convert:UC.toFloat, units:true, bound:UC.bound(0,Infinity)}),
|
||||||
|
sep: UC.newBlank({class:"pop-sep"}),
|
||||||
|
entry: UC.newBoolean(LANG.ch_entr_s,undefined, {title:LANG.ch_entr_l}),
|
||||||
|
entryOffset: UC.newInput(LANG.ch_ento_s, {title:LANG.ch_ento_l, convert:UC.toFloat, units:true, show:() => env.poppedRec.entry}),
|
||||||
|
reverse: UC.newBoolean(LANG.ch_rvrs_s,undefined, {title:LANG.ch_rvrs_l}),
|
||||||
|
clockwise:UC.newBoolean(LANG.ch_clkw_s,undefined, {title:LANG.ch_clkw_l}),
|
||||||
|
sep: UC.newBlank({class:"pop-sep"}),
|
||||||
|
finish: UC.newBoolean(LANG.ch_fini_s,undefined, { title:LANG.ch_fini_l ,show: ()=>!env.poppedRec.reverse}),
|
||||||
|
forceStartAng: UC.newBoolean(LANG.ch_fsta_s, undefined, {title:LANG.ch_fsta_l }),
|
||||||
|
fromTop: UC.newBoolean(LANG.cd_ftop_s,undefined, {title:LANG.cd_ftop_l}),
|
||||||
|
sep: UC.newBlank({class:"pop-sep"}),
|
||||||
|
thru: UC.newInput(LANG.cd_thru_s, {title:LANG.cd_thru_l, convert:UC.toFloat, units:true}),
|
||||||
|
actions: UC.newRow([
|
||||||
|
UC.newButton(LANG.select, selectHelical, {title:LANG.cd_seli_l}),
|
||||||
|
], {class:"ext-buttons f-col"})
|
||||||
|
};
|
||||||
|
|
||||||
createPopOp('flip', {
|
createPopOp('flip', {
|
||||||
axis: 'camFlipAxis',
|
axis: 'camFlipAxis',
|
||||||
invert: 'camFlipInvert'
|
invert: 'camFlipInvert'
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { tabAdd, tabDone, tabClear, restoreTabs, rotateTabs, updateTabs, clearTa
|
||||||
import { traceOn, traceDone, unselectTraces } from './cl-trace.js';
|
import { traceOn, traceDone, unselectTraces } from './cl-trace.js';
|
||||||
import { holeSelOn, selectHolesDone, clearHolesRec } from './cl-hole.js';
|
import { holeSelOn, selectHolesDone, clearHolesRec } from './cl-hole.js';
|
||||||
import { surfaceOn, surfaceDone } from './cl-surface.js';
|
import { surfaceOn, surfaceDone } from './cl-surface.js';
|
||||||
|
import { helicalOn, helicalDone } from './cl-helical.js';
|
||||||
import { originSelectDone } from './cl-origin.js';
|
import { originSelectDone } from './cl-origin.js';
|
||||||
|
|
||||||
const DEG2RAD = Math.PI / 180;
|
const DEG2RAD = Math.PI / 180;
|
||||||
|
|
@ -60,6 +61,8 @@ export function clearPops() {
|
||||||
surfaceDone();
|
surfaceDone();
|
||||||
tabDone();
|
tabDone();
|
||||||
traceDone();
|
traceDone();
|
||||||
|
selectHolesDone();
|
||||||
|
helicalDone();
|
||||||
}
|
}
|
||||||
|
|
||||||
function animFn() {
|
function animFn() {
|
||||||
|
|
@ -172,6 +175,14 @@ const opAddPocket = () => {
|
||||||
opAdd(rec);
|
opAdd(rec);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const opAddHelical = () => {
|
||||||
|
traceDone();
|
||||||
|
surfaceDone();
|
||||||
|
let rec = env.popOp.helical.new();
|
||||||
|
rec.cylinders = { /* widget.id: [ faces... ] */ };
|
||||||
|
opAdd(rec);
|
||||||
|
};
|
||||||
|
|
||||||
const opAddContour = (axis) => {
|
const opAddContour = (axis) => {
|
||||||
let rec = env.popOp.contour.new();
|
let rec = env.popOp.contour.new();
|
||||||
rec.axis = axis.toUpperCase();
|
rec.axis = axis.toUpperCase();
|
||||||
|
|
@ -728,6 +739,7 @@ export function init() {
|
||||||
case "drill": return opAddDrill();
|
case "drill": return opAddDrill();
|
||||||
case "trace": return opAddTrace();
|
case "trace": return opAddTrace();
|
||||||
case "pocket": return opAddPocket();
|
case "pocket": return opAddPocket();
|
||||||
|
case "helical": return opAddHelical();
|
||||||
case "flip":
|
case "flip":
|
||||||
// only one flip op permitted
|
// only one flip op permitted
|
||||||
for (let op of env.current.process.ops) {
|
for (let op of env.current.process.ops) {
|
||||||
|
|
@ -793,6 +805,9 @@ export function init() {
|
||||||
if (traceOn) {
|
if (traceOn) {
|
||||||
traceDone();
|
traceDone();
|
||||||
}
|
}
|
||||||
|
if(helicalOn){
|
||||||
|
helicalDone();
|
||||||
|
}
|
||||||
unselectTraces(widget);
|
unselectTraces(widget);
|
||||||
if (env.flipping) {
|
if (env.flipping) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -816,6 +831,9 @@ export function init() {
|
||||||
if (holeSelOn) {
|
if (holeSelOn) {
|
||||||
selectHolesDone();
|
selectHolesDone();
|
||||||
}
|
}
|
||||||
|
if(helicalOn){
|
||||||
|
helicalDone();
|
||||||
|
}
|
||||||
clearHolesRec(widget)
|
clearHolesRec(widget)
|
||||||
unselectTraces(widget);
|
unselectTraces(widget);
|
||||||
if (env.flipping) {
|
if (env.flipping) {
|
||||||
|
|
@ -836,6 +854,9 @@ export function init() {
|
||||||
if (holeSelOn) {
|
if (holeSelOn) {
|
||||||
selectHolesDone();
|
selectHolesDone();
|
||||||
}
|
}
|
||||||
|
if(helicalOn){
|
||||||
|
helicalDone();
|
||||||
|
}
|
||||||
if (env.flipping) {
|
if (env.flipping) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { cam_slice, holes, traces } from './slice.js';
|
||||||
import { cam_prepare } from './prepare.js';
|
import { cam_prepare } from './prepare.js';
|
||||||
import { cam_export } from './export.js';
|
import { cam_export } from './export.js';
|
||||||
import { tool as mesh_tool } from '../../../mesh/tool.js';
|
import { tool as mesh_tool } from '../../../mesh/tool.js';
|
||||||
|
import { cylinder_poly_find } from './slice.js';
|
||||||
|
|
||||||
import { init as init_2d } from './anim-2d-be.js';
|
import { init as init_2d } from './anim-2d-be.js';
|
||||||
import { init as init_3d } from './anim-3d-be.js';
|
import { init as init_3d } from './anim-3d-be.js';
|
||||||
|
|
@ -97,6 +98,16 @@ function init(worker) {
|
||||||
shadowed: widget.shadowedDrills
|
shadowed: widget.shadowedDrills
|
||||||
} } )));
|
} } )));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispatch.cam_cylinder_find = async function(data,send){
|
||||||
|
const { id, face, settings } = data;
|
||||||
|
const widget = worker.cache[id];
|
||||||
|
try{
|
||||||
|
send.done(cylinder_poly_find(widget, face));
|
||||||
|
}catch(error){
|
||||||
|
send.done({error});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CAM = {
|
export const CAM = {
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,10 @@ function surface_show(widget) {
|
||||||
widget.selectFaces(Object.values(widget._surfaces).flat());
|
widget.selectFaces(Object.values(widget._surfaces).flat());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function cylinder_show(widget) {
|
||||||
|
widget.selectFaces(Object.values(widget._cylinders).flat());
|
||||||
|
};
|
||||||
|
|
||||||
function surface_toggle(widget, face, radians, ondone) {
|
function surface_toggle(widget, face, radians, ondone) {
|
||||||
let surfaces = widget._surfaces = widget._surfaces || {};
|
let surfaces = widget._surfaces = widget._surfaces || {};
|
||||||
for (let [root, faces] of Object.entries(surfaces)) {
|
for (let [root, faces] of Object.entries(surfaces)) {
|
||||||
|
|
@ -78,9 +82,44 @@ function holes(indiv, rec, onProgress, onDone) {
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function cylinderShow(onProgress,onDone){
|
||||||
|
kiri.client.sync();
|
||||||
|
const settings = api.conf.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cylinderToggle(widget, face, onDone){
|
||||||
|
let cyls = widget._cylinders = widget._cylinders || {};
|
||||||
|
for (let [root, faces] of Object.entries(cyls)) {
|
||||||
|
if (faces.contains(face)) { // if a face group contains the selected face
|
||||||
|
// delete this face group
|
||||||
|
delete cyls[root];
|
||||||
|
CAM.cylinder_show(widget);
|
||||||
|
onDone(Object.keys(cyls).map(v => parseInt(v)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//send
|
||||||
|
api.client.send("cam_cylinder_find", { id: widget.id, face }, ({faces,error}) => {
|
||||||
|
if (faces?.length) {
|
||||||
|
cyls[face] = faces;
|
||||||
|
CAM.cylinder_show(widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDone({faces:Object.keys(cyls).map(v => parseInt(v)),error});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function cylinderClear(widget){
|
||||||
|
widget.selectFaces([]);
|
||||||
|
widget._cylinders = {};
|
||||||
|
}
|
||||||
|
|
||||||
export const CAM = {
|
export const CAM = {
|
||||||
surface_prep,
|
surface_prep,
|
||||||
surface_show,
|
surface_show,
|
||||||
|
cylinder_show,
|
||||||
|
cylinderToggle,
|
||||||
|
cylinderClear,
|
||||||
surface_toggle,
|
surface_toggle,
|
||||||
surface_clear,
|
surface_clear,
|
||||||
traces,
|
traces,
|
||||||
|
|
|
||||||
278
src/kiri/mode/cam/op-helical.js
Normal file
278
src/kiri/mode/cam/op-helical.js
Normal file
|
|
@ -0,0 +1,278 @@
|
||||||
|
import { arcToPath } from "../../../geo/paths.js";
|
||||||
|
import { newPolygon } from "../../../geo/polygon.js";
|
||||||
|
import { CamOp } from "./op.js";
|
||||||
|
import { Tool } from "./tool.js";
|
||||||
|
import { cylinder_poly_find } from "./slice.js";
|
||||||
|
import { newPoint } from "../../../geo/point.js";
|
||||||
|
import { newSlice } from "../../core/slice.js";
|
||||||
|
|
||||||
|
export class OpHelical extends CamOp {
|
||||||
|
constructor(state, op) {
|
||||||
|
super(state, op);
|
||||||
|
}
|
||||||
|
async slice(progress) {
|
||||||
|
let { op, state } = this;
|
||||||
|
let { settings, addSlices, widget, updateToolDiams, slicer } = state;
|
||||||
|
let { color } = state;
|
||||||
|
let {
|
||||||
|
cylinders,
|
||||||
|
forceStartAng,
|
||||||
|
startAng,
|
||||||
|
fromTop,
|
||||||
|
offOver,
|
||||||
|
offset,
|
||||||
|
down,
|
||||||
|
thru,
|
||||||
|
finish,
|
||||||
|
clockwise,
|
||||||
|
reverse,
|
||||||
|
entry,
|
||||||
|
entryOffset,
|
||||||
|
} = op;
|
||||||
|
let {stock} = settings
|
||||||
|
|
||||||
|
let tool = new Tool(settings, op.tool),
|
||||||
|
toolDiam = tool.fluteDiameter(),
|
||||||
|
sliceOut = (this.sliceOut = []),
|
||||||
|
startAngle = forceStartAng ? startAng : 0;
|
||||||
|
|
||||||
|
updateToolDiams(toolDiam);
|
||||||
|
// console.log({cylinders})
|
||||||
|
let faces = cylinders[widget.id] ?? [];
|
||||||
|
if (faces.faces) faces = faces.faces;
|
||||||
|
let polys = [];
|
||||||
|
|
||||||
|
//iterate over selected faces
|
||||||
|
for (let [i,face] of faces.entries()) {
|
||||||
|
//get poly info for the cylinder
|
||||||
|
let res
|
||||||
|
try{
|
||||||
|
res = cylinder_poly_find(
|
||||||
|
widget,
|
||||||
|
face
|
||||||
|
);
|
||||||
|
}catch(error){
|
||||||
|
// don't quit if a cylinder errors out, just skip
|
||||||
|
console.error(error)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let { zmin, zmax, diam, center, interior, faces } = res;
|
||||||
|
// console.log({ zmin, zmax, diam, center, interior });
|
||||||
|
let radius = diam >> 1;
|
||||||
|
zmax = fromTop ? stock.z: zmax;
|
||||||
|
|
||||||
|
let radAdd,
|
||||||
|
zBottom = zmin - thru,
|
||||||
|
numSegs = faces.length / 2,
|
||||||
|
poly = newPolygon().setOpen();
|
||||||
|
|
||||||
|
if (offOver) {
|
||||||
|
//apply offset override
|
||||||
|
if (offset == "auto") {
|
||||||
|
radAdd = interior ? -offOver : offOver;
|
||||||
|
} else if (offset == "inside") {
|
||||||
|
radAdd = -offOver;
|
||||||
|
} else if (offset == "outside") {
|
||||||
|
radAdd = offOver;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//otherwise, calculate based on offset type
|
||||||
|
if (offset == "auto") {
|
||||||
|
radAdd = (interior ? -toolDiam : toolDiam) / 2;
|
||||||
|
} else if (offset == "inside") {
|
||||||
|
radAdd = -toolDiam / 2;
|
||||||
|
} else if (offset == "outside") {
|
||||||
|
radAdd = toolDiam / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
radius += radAdd;
|
||||||
|
|
||||||
|
if( radius <= 0 ){
|
||||||
|
//if negative radius, skip this cylinder
|
||||||
|
console.log('negative radius', radius);
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let startPoint = center
|
||||||
|
.clone()
|
||||||
|
.setZ(zmax)
|
||||||
|
.add(
|
||||||
|
newPoint(
|
||||||
|
Math.cos(startAngle) * radius,
|
||||||
|
Math.sin(startAngle) * radius,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
let currentZ = zmax;
|
||||||
|
for (;;) {
|
||||||
|
let bottom = currentZ - down;
|
||||||
|
if ( bottom > zBottom ) {
|
||||||
|
//if not at the bottom, do a whole helix
|
||||||
|
let start = startPoint.setZ(currentZ),
|
||||||
|
end = startPoint.clone().setZ(bottom);
|
||||||
|
// console.log(currentZ, bottom, [start.z, end.z]);
|
||||||
|
let path = arcToPath(start, end, numSegs, {
|
||||||
|
clockwise,
|
||||||
|
radius,
|
||||||
|
center,
|
||||||
|
});
|
||||||
|
// console.log({ path, start, end, radius, center });
|
||||||
|
poly.addPoints(path);
|
||||||
|
} else {
|
||||||
|
//if at the bottom, do a partial helix
|
||||||
|
let toBottom = currentZ - zBottom,
|
||||||
|
ofFull = toBottom / down,
|
||||||
|
ofCircle = ofFull * (2 * Math.PI),
|
||||||
|
cwMultiplier = clockwise ? -1 : 1,
|
||||||
|
finalAngle = startAngle + (ofCircle*cwMultiplier),
|
||||||
|
finalEnd = center
|
||||||
|
.clone()
|
||||||
|
.setZ(zBottom)
|
||||||
|
.add(
|
||||||
|
newPoint(
|
||||||
|
Math.cos(finalAngle) * radius,
|
||||||
|
Math.sin(finalAngle) * radius,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
// console.log({down,toBottom,ofFull,ofCircle,startAngle,finalAngle,finalEnd});
|
||||||
|
|
||||||
|
//circle down to final point
|
||||||
|
poly.addPoints(
|
||||||
|
arcToPath(startPoint.setZ(currentZ), finalEnd, numSegs , {
|
||||||
|
clockwise,
|
||||||
|
radius,
|
||||||
|
center,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
//if starting at the bottom, reverse poly
|
||||||
|
if ( reverse ){
|
||||||
|
poly.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( entry ){
|
||||||
|
//get first z of poly to use for entry
|
||||||
|
let firstZ = poly.first().z;
|
||||||
|
//create new pointArray
|
||||||
|
let entry =[newPoint(0,0,0)];
|
||||||
|
//add center point
|
||||||
|
let entryCenter = center.clone().setZ(firstZ)
|
||||||
|
|
||||||
|
//if doing a curved enter, add more
|
||||||
|
if ( entryOffset > 0 ){
|
||||||
|
if ( entryOffset > radius ){
|
||||||
|
console.error("entryOffset must be less than radius of helix");
|
||||||
|
}else{
|
||||||
|
//calculate entry center
|
||||||
|
let lineOut = radius - entryOffset;
|
||||||
|
let centerOffset = (entryOffset**2 + 2*entryOffset*lineOut) / (2*(entryOffset + lineOut));
|
||||||
|
//add entry points
|
||||||
|
entry.push(...
|
||||||
|
newPoint(0,-lineOut,0),
|
||||||
|
arcToPath(
|
||||||
|
newPoint(0,-lineOut,0),
|
||||||
|
newPoint(radius,0,0),
|
||||||
|
128,
|
||||||
|
{
|
||||||
|
clockwise:false,
|
||||||
|
radius:lineOut,
|
||||||
|
center:newPoint(centerOffset,0,0)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
newPoint(radius,0,0),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//for each point
|
||||||
|
entry = entry.map(p => {
|
||||||
|
//rotate by start angle
|
||||||
|
// add to hole center
|
||||||
|
// TODO: there's totally a way to do this with a matrix
|
||||||
|
p.y *= clockwise ? 1 : -1
|
||||||
|
p.rotate(startAngle)
|
||||||
|
return p.add(entryCenter)
|
||||||
|
})
|
||||||
|
//append to start of poly
|
||||||
|
poly = newPolygon().setOpen().addPoints(entry).addPoints(poly.points);
|
||||||
|
}
|
||||||
|
//if doing a finish pass, do a full circle at the bottom
|
||||||
|
if ( finish && !reverse ){
|
||||||
|
poly.addPoints(
|
||||||
|
arcToPath(finalEnd, finalEnd, numSegs, {
|
||||||
|
clockwise,
|
||||||
|
radius,
|
||||||
|
center,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
poly.append(
|
||||||
|
finalEnd.clone().setZ(zBottom),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
currentZ = bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress(i/faces.length, "Helical intrerpolation");
|
||||||
|
polys.push(poly);
|
||||||
|
}
|
||||||
|
|
||||||
|
let slice = newSlice(0);
|
||||||
|
|
||||||
|
slice.camTrace = { tool: op.tool, rate: op.feed, plunge: op.rate };
|
||||||
|
slice.camLines = polys;
|
||||||
|
|
||||||
|
slice
|
||||||
|
.output()
|
||||||
|
.setLayer("Helical", { face: color, line: color })
|
||||||
|
.addPolys(slice.camLines);
|
||||||
|
addSlices(slice);
|
||||||
|
sliceOut.push(slice);
|
||||||
|
}
|
||||||
|
|
||||||
|
async prepare(ops, progress) {
|
||||||
|
let { op, state } = this;
|
||||||
|
let { settings, widget, addSlices, updateToolDiams } = state;
|
||||||
|
let {
|
||||||
|
setTool,
|
||||||
|
setSpindle,
|
||||||
|
polyEmit,
|
||||||
|
camOut,
|
||||||
|
setPrintPoint,
|
||||||
|
getPrintPoint,
|
||||||
|
} = ops;
|
||||||
|
let { tool, spindle, rate, feed } = op;
|
||||||
|
setTool(tool, feed, rate);
|
||||||
|
setSpindle(spindle);
|
||||||
|
|
||||||
|
let [polys] = this.sliceOut.map((s) => s.camLines);
|
||||||
|
let pp = getPrintPoint();
|
||||||
|
|
||||||
|
polys = polys.slice();
|
||||||
|
for (;;) {
|
||||||
|
let closestDist = Infinity,
|
||||||
|
closestI,
|
||||||
|
closest = null,
|
||||||
|
dist,
|
||||||
|
poly;
|
||||||
|
|
||||||
|
for (let i = 0; i < polys.length; i++) {
|
||||||
|
if (!polys[i]) continue;
|
||||||
|
if ((dist = polys[i].first().distTo2D(pp)) < closestDist) {
|
||||||
|
closestDist = dist;
|
||||||
|
closest = polys[i];
|
||||||
|
closestI = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!closest) break;
|
||||||
|
poly = polys[closestI]
|
||||||
|
polys[closestI] = null;
|
||||||
|
//emit
|
||||||
|
pp = polyEmit(poly,0,1,poly.points[0])
|
||||||
|
}
|
||||||
|
setPrintPoint(pp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,7 @@ import { OpLaserOff } from './op-laser-off.js';
|
||||||
import { OpGCode } from './op-gcode.js';
|
import { OpGCode } from './op-gcode.js';
|
||||||
import { OpIndex } from './op-index.js';
|
import { OpIndex } from './op-index.js';
|
||||||
import { CamOp } from './op.js';
|
import { CamOp } from './op.js';
|
||||||
|
import { OpHelical } from './op-helical.js';
|
||||||
|
|
||||||
export const ops = {
|
export const ops = {
|
||||||
"xray": OpXRay,
|
"xray": OpXRay,
|
||||||
|
|
@ -29,6 +30,7 @@ export const ops = {
|
||||||
"trace": OpTrace,
|
"trace": OpTrace,
|
||||||
"drill": OpDrill,
|
"drill": OpDrill,
|
||||||
"register": OpRegister,
|
"register": OpRegister,
|
||||||
|
"helical": OpHelical,
|
||||||
"laser on": OpLaserOn,
|
"laser on": OpLaserOn,
|
||||||
"laser off": OpLaserOff,
|
"laser off": OpLaserOff,
|
||||||
"gcode": OpGCode,
|
"gcode": OpGCode,
|
||||||
|
|
|
||||||
|
|
@ -332,7 +332,7 @@ export function prepEach(widget, settings, print, firstPoint, update) {
|
||||||
* @param {number} opts.factor speed scale factor
|
* @param {number} opts.factor speed scale factor
|
||||||
*/
|
*/
|
||||||
function camOut(point, emit, opts) {
|
function camOut(point, emit, opts) {
|
||||||
// console.log({point, emit, opts})
|
// console.trace({point, emit, opts})
|
||||||
let {
|
let {
|
||||||
center = {},
|
center = {},
|
||||||
clockwise = true,
|
clockwise = true,
|
||||||
|
|
@ -742,7 +742,8 @@ export function prepEach(widget, settings, print, firstPoint, update) {
|
||||||
function polyEmit(poly, index, count, fromPoint) {
|
function polyEmit(poly, index, count, fromPoint) {
|
||||||
let arcQ = [],
|
let arcQ = [],
|
||||||
arcMax = Infinity, // no max arc radius
|
arcMax = Infinity, // no max arc radius
|
||||||
lineTolerance = 0.001; // do not consider points under 0.001mm for arcs
|
lineTolerance = 0.001, // do not consider points under 0.001mm for arcs
|
||||||
|
zTolerance = 0.1; // allow zs of Points to be off by 0.001mm per radian of rotation
|
||||||
|
|
||||||
fromPoint = fromPoint || printPoint;
|
fromPoint = fromPoint || printPoint;
|
||||||
arcQ.angle = []
|
arcQ.angle = []
|
||||||
|
|
@ -818,6 +819,18 @@ export function prepEach(widget, settings, print, firstPoint, update) {
|
||||||
radFault = true;
|
radFault = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let endDelta = e1.distTo2D(e3)
|
||||||
|
if(endDelta < 0.01){
|
||||||
|
console.log("circleCompleteFault")
|
||||||
|
let last = arcQ.peek()
|
||||||
|
drainQ(true)
|
||||||
|
arcQ.push(last)
|
||||||
|
return e3
|
||||||
|
}
|
||||||
|
|
||||||
|
let ddz = Math.abs((e3.z - e4.z) - (e4.z - e5.z)) // take second derivitive of z
|
||||||
|
let zFault = Math.abs(ddz) > zTolerance
|
||||||
|
|
||||||
if (cc) {
|
if (cc) {
|
||||||
if ([cc.x, cc.y, cc.z, cc.r].hasNaN()) {
|
if ([cc.x, cc.y, cc.z, cc.r].hasNaN()) {
|
||||||
// console.log({cc, e1, e2, e3});
|
// console.log({cc, e1, e2, e3});
|
||||||
|
|
@ -829,15 +842,15 @@ export function prepEach(widget, settings, print, firstPoint, update) {
|
||||||
arcQ.rSum = cc.r;
|
arcQ.rSum = cc.r;
|
||||||
|
|
||||||
// check if first angles should have caused radFault
|
// check if first angles should have caused radFault
|
||||||
let a = toRadians(arcQ[0].slopeTo(cc).angle)
|
let angle = toRadians(arcQ[0].slopeTo(cc).angleDiff(arcQ[1].slopeTo(cc)).angle)
|
||||||
let b = toRadians(arcQ[1].slopeTo(cc).angle)
|
radFault = Math.abs(angle) > arcRes
|
||||||
let angle = Math.abs(b - a)
|
if (radFault) {
|
||||||
|
|
||||||
if (Math.abs(angle) > arcRes) {
|
|
||||||
// if so, remove first point
|
// if so, remove first point
|
||||||
|
|
||||||
|
console.log("secondary radfault,",structuredClone(arcQ),{angle,arcRes,a,b})
|
||||||
camOut(arcQ.shift(), 1)
|
camOut(arcQ.shift(), 1)
|
||||||
radFault = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// check center point delta
|
// check center point delta
|
||||||
arcQ.xSum = arcQ.center.reduce(function (t, v) { return t + v.x }, 0);
|
arcQ.xSum = arcQ.center.reduce(function (t, v) { return t + v.x }, 0);
|
||||||
|
|
@ -848,10 +861,10 @@ export function prepEach(widget, settings, print, firstPoint, update) {
|
||||||
dc = Math.hypot(dx, dy); // delta center distance
|
dc = Math.hypot(dx, dy); // delta center distance
|
||||||
}
|
}
|
||||||
// if new point is off the arc
|
// if new point is off the arc
|
||||||
// if (deem || depm || desp || dc > arcTolerance || cc.r < arcMin || cc.r > arcMax || dist > cc.r) {
|
// if point is off-center, or too far from center, or too large of a radius
|
||||||
if (dc * arcQ.center.length / arcQ.rSum > arcTolerance || dist > cc.r || cc.r > arcMax || radFault) {
|
if (dc * arcQ.center.length / arcQ.rSum > arcTolerance || dist > cc.r || cc.r > arcMax || radFault || zFault) {
|
||||||
// let debug = [deem, depm, desp, dc * arcQ.center.length / arcQ.rSum > arcTolerance, dist > cc.r, cc.r > arcMax, radFault];
|
// let debug = [ dc * arcQ.center.length / arcQ.rSum > arcTolerance, dist > cc.r, cc.r > arcMax, radFault];
|
||||||
// console.log("point off the arc,",structuredClone(arcQ),);
|
// console.log("point off the arc,",structuredClone(arcQ),radFault,zFault,[dc * arcQ.center.length / arcQ.rSum > arcTolerance , dist > cc.r , cc.r > arcMax]);
|
||||||
if (arcQ.length === 4) {
|
if (arcQ.length === 4) {
|
||||||
// not enough points for an arc, drop first point and recalc center
|
// not enough points for an arc, drop first point and recalc center
|
||||||
camOut(arcQ.shift(), 1);
|
camOut(arcQ.shift(), 1);
|
||||||
|
|
@ -899,7 +912,12 @@ export function prepEach(widget, settings, print, firstPoint, update) {
|
||||||
return point;
|
return point;
|
||||||
}
|
}
|
||||||
|
|
||||||
function drainQ() {
|
/**
|
||||||
|
* Emits arcs and/or lines from the arcQ to the current point set.
|
||||||
|
* @param {boolean} forceCircle emits a single circle iven if poly is not closed.
|
||||||
|
*/
|
||||||
|
function drainQ(forceCircle = false) {
|
||||||
|
// console.trace("draining")
|
||||||
let arcPreviewRes = 64
|
let arcPreviewRes = 64
|
||||||
|
|
||||||
if (!arcTolerance) {
|
if (!arcTolerance) {
|
||||||
|
|
@ -915,6 +933,7 @@ export function prepEach(widget, settings, print, firstPoint, update) {
|
||||||
let from = arcQ[0];
|
let from = arcQ[0];
|
||||||
let to = arcQ.peek();
|
let to = arcQ.peek();
|
||||||
let delta = from.distTo2D(to)
|
let delta = from.distTo2D(to)
|
||||||
|
let closed = poly.isClosed()
|
||||||
arcQ.xSum = arcQ.center.reduce((t, v) => t + v.x, 0);
|
arcQ.xSum = arcQ.center.reduce((t, v) => t + v.x, 0);
|
||||||
arcQ.ySum = arcQ.center.reduce((t, v) => t + v.y, 0);
|
arcQ.ySum = arcQ.center.reduce((t, v) => t + v.y, 0);
|
||||||
arcQ.rSum = arcQ.center.reduce((t, v) => t + v.r, 0);
|
arcQ.rSum = arcQ.center.reduce((t, v) => t + v.r, 0);
|
||||||
|
|
@ -925,15 +944,20 @@ export function prepEach(widget, settings, print, firstPoint, update) {
|
||||||
)
|
)
|
||||||
|
|
||||||
// console.log("draining")
|
// console.log("draining")
|
||||||
if (arcQ.length == poly.points.length) {
|
if (closed && arcQ.length == poly.points.length || forceCircle ) {
|
||||||
//if is a circle
|
//if is a circle
|
||||||
// generate circle
|
// generate circle
|
||||||
// console.log("circle",{from, to,center});
|
// console.log("circle",{from, to,center});
|
||||||
let arcPoints = arcToPath(from, from, arcPreviewRes, { clockwise, center })
|
to = forceCircle? from.clone().setZ(to.z) : from
|
||||||
.map(applyWidgetMovement);
|
|
||||||
|
|
||||||
|
let arcPoints = arcToPath(from, to, arcPreviewRes, { clockwise, center })
|
||||||
|
.map(applyWidgetMovement);
|
||||||
|
|
||||||
|
// console.log({arcPoints})
|
||||||
|
|
||||||
camOut(from, 1);
|
camOut(from, 1);
|
||||||
camOut(from, gc, { center: center.sub(from), clockwise, arcPoints });
|
camOut(to, gc, { center: center.sub(from), clockwise, arcPoints });
|
||||||
// lastPoint = to.clone();
|
|
||||||
} else {
|
} else {
|
||||||
//if a non-circle arc
|
//if a non-circle arc
|
||||||
let arcPoints = arcToPath(from, to, arcPreviewRes, { clockwise, center })
|
let arcPoints = arcToPath(from, to, arcPreviewRes, { clockwise, center })
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { setSliceTracker } from '../../core/slice.js';
|
||||||
import { ops as OPS } from './ops.js';
|
import { ops as OPS } from './ops.js';
|
||||||
import { Tool } from './tool.js';
|
import { Tool } from './tool.js';
|
||||||
import { Slicer as cam_slicer } from './slicer.js';
|
import { Slicer as cam_slicer } from './slicer.js';
|
||||||
|
import { CAM } from './driver-be.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DRIVER SLICE CONTRACT
|
* DRIVER SLICE CONTRACT
|
||||||
|
|
@ -508,6 +509,123 @@ export async function traces(settings, widget) {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of arrays of perpindicular triangles in the mesh.
|
||||||
|
* Each sub-array is a list of triangle data that are part of the same
|
||||||
|
* cylinder.
|
||||||
|
*
|
||||||
|
* @param {object} settings - settings object
|
||||||
|
* @param {object} widget - widget object
|
||||||
|
* @param {object} opts - options object
|
||||||
|
* @return {array} - array of arrays of triangle data
|
||||||
|
*/
|
||||||
|
export async function cylinders(settings, widget, opts){
|
||||||
|
|
||||||
|
let {} = opts ?? {}
|
||||||
|
|
||||||
|
let {array:verts} = widget.mesh.geometry.attributes.position
|
||||||
|
let perpTriangles = []
|
||||||
|
|
||||||
|
//iterate over all triangles
|
||||||
|
for(let i = 0; i < verts.length; i+=9){
|
||||||
|
let a = [verts[i], verts[i+1], verts[i+2]],
|
||||||
|
b = [verts[i+3], verts[i+4], verts[i+5]],
|
||||||
|
c = [verts[i+6], verts[i+7], verts[i+8]];
|
||||||
|
|
||||||
|
//calculate normal
|
||||||
|
let normal = new THREE.Vector3(a[0]-b[0], a[1]-b[1], a[2]-b[2])
|
||||||
|
.cross(new THREE.Vector3(b[0]-c[0], b[1]-c[1], b[2]-c[2]))
|
||||||
|
.normalize()
|
||||||
|
|
||||||
|
// if perpindicular normal, and at least 2 Zs are the same
|
||||||
|
if(normal.z.round(5) == 0 && ! (a[2] != b[2] && b[2] != c[2])){
|
||||||
|
let minZ = Math.min(a[2], b[2], c[2]),
|
||||||
|
maxZ = Math.max(a[2], b[2], c[2]);
|
||||||
|
if(minZ == maxZ){ // all Zs are the same indicated malformed geometry
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
perpTriangles.push({...[a,b,c], normal,minZ,maxZ,i:index})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
//map where zmax, zmin -> triangleData
|
||||||
|
let cylinderTriangles = new Map()
|
||||||
|
for(let t of perpTriangles){
|
||||||
|
let hash = `${t.minZ.round(5)},${t.maxZ.round(5)}`
|
||||||
|
if(!cylinderTriangles.has(hash)){
|
||||||
|
cylinderTriangles.set(hash,[])
|
||||||
|
}
|
||||||
|
cylinderTriangles.get(hash).push(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(cylinderTriangles.values())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all triangles that are part of the same cylinder as the given triangle
|
||||||
|
* @param {Widget} widget - widget object
|
||||||
|
* @param {number} face - index of face in triangle vertex data
|
||||||
|
* @return {array} - array of all triangle data that belong to the same cylinder
|
||||||
|
* @requires surface_prep must be called first
|
||||||
|
*/
|
||||||
|
export function cylinder_find (widget, face){
|
||||||
|
CAM.surface_prep(widget,false);
|
||||||
|
return widget.tool.findCylinderSurface(face);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cylinder_poly_find(widget, face){
|
||||||
|
let faces = cylinder_find(widget, face);
|
||||||
|
let vert = widget.getGeoVertices({ unroll: true, translate: true }).map(v => v.round(4));
|
||||||
|
let slicer = new cam_slicer(widget,{})
|
||||||
|
let opts = {
|
||||||
|
dedup:false,
|
||||||
|
edges: false,
|
||||||
|
over:true
|
||||||
|
}
|
||||||
|
let firstOffset = faces[0]*9
|
||||||
|
let [x1,y1,z1,x2,y2,z2,x3,y3,z3]= Array.from(vert.subarray(firstOffset,firstOffset+9))
|
||||||
|
let zs = [z1,z2,z3].map(z => z.round(5))
|
||||||
|
let zmin = Math.min(...zs);
|
||||||
|
let zmax = Math.max(...zs);
|
||||||
|
let zmid = (zmin+zmax)/2
|
||||||
|
let cylVerts = new Float32Array(faces.length*9)
|
||||||
|
|
||||||
|
let i = 0
|
||||||
|
//for each cylinder face index
|
||||||
|
for(let index of faces){
|
||||||
|
let off = index*9
|
||||||
|
//copy the 9 triangle vertices floats in
|
||||||
|
for(let j = 0;j < 9;j++){
|
||||||
|
cylVerts[i] = vert[off+j]
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//slice the poly at the midpoint
|
||||||
|
let poly = slicer.sliceZ(zmid, cylVerts,opts)?.polys?.at(0)
|
||||||
|
if(!poly) throw "slicing returned no poly"
|
||||||
|
let circular = poly.circularity() > 0.98;
|
||||||
|
//throw error if not circular
|
||||||
|
if(!circular) throw "faces must be circular"
|
||||||
|
|
||||||
|
//calculate circle properties
|
||||||
|
let area = poly.area(),
|
||||||
|
diam = Math.sqrt(area / Math.PI)* 2,
|
||||||
|
center = poly.calcCircleCenter();
|
||||||
|
|
||||||
|
//find direction using normal to center point
|
||||||
|
|
||||||
|
let point = newPoint(x1, y1, z1),
|
||||||
|
delta = point.setZ(center.z).sub(center),
|
||||||
|
normal = new THREE.Vector3(x1-x2, y1-y2, z1-z2)
|
||||||
|
.cross(new THREE.Vector3(x2-x3, y2-y3, z2-z3)),
|
||||||
|
dotProd = normal.dot(delta),
|
||||||
|
interior = dotProd < 0
|
||||||
|
|
||||||
|
return {faces, zmin, zmax, poly, circular, area, diam, center,interior}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a list of holes in the model based on the given diameter.
|
* Generate a list of holes in the model based on the given diameter.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -233,11 +233,20 @@ class MeshTool {
|
||||||
fn++;
|
fn++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//faces are the normals in groups of 6 [nx,]
|
||||||
|
|
||||||
this.indexed = {
|
this.indexed = {
|
||||||
faces, sides, sideExt
|
faces, sides, sideExt
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the adjacent faces for a given face index.
|
||||||
|
*
|
||||||
|
* @param {number} face - The index of the face for which adjacent faces are to be found.
|
||||||
|
* @returns {number[]} An array of face indices that are adjacent to the given face.
|
||||||
|
*/
|
||||||
|
|
||||||
getAdjacentFaces(face) {
|
getAdjacentFaces(face) {
|
||||||
const { faces, sides } = this.getIndex();
|
const { faces, sides } = this.getIndex();
|
||||||
const foff = face * 6;
|
const foff = face * 6;
|
||||||
|
|
@ -294,6 +303,88 @@ class MeshTool {
|
||||||
return faces;
|
return faces;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds all faces in a cylinder surface connected to the given face.
|
||||||
|
*
|
||||||
|
* @param {number} face - the face index to start from
|
||||||
|
* @return {number[]} face indices in the cylinder surface
|
||||||
|
*
|
||||||
|
* @throws {string} if the face has only one Z value
|
||||||
|
* @throws {string} if the face normal is not perpendicular to Z axis
|
||||||
|
*
|
||||||
|
* The algorithm works by:
|
||||||
|
* 1. Finding the two distinct Z values of the given face
|
||||||
|
* 2. Filtering adjacent faces to those with the same Z values
|
||||||
|
* 3. Filtering faces with normals perpendicular to Z axis
|
||||||
|
* 4. Recursively adding adjacent faces with the same Z values and normal perpendicular to Z axis
|
||||||
|
*/
|
||||||
|
findCylinderSurface(face){
|
||||||
|
face = Number(face)
|
||||||
|
|
||||||
|
const radianTolerance = 8 * (Math.PI/180);
|
||||||
|
const vertexTolerance = 1e-4;
|
||||||
|
//helper function
|
||||||
|
const dissimilar = (a,b,c) => Math.abs(a-b) > vertexTolerance &&
|
||||||
|
Math.abs(b-c) > vertexTolerance &&
|
||||||
|
Math.abs(a-c) > vertexTolerance;
|
||||||
|
|
||||||
|
let found = {},
|
||||||
|
out = [face],
|
||||||
|
{vertices} = this,
|
||||||
|
{faces} = this.getIndex(),
|
||||||
|
faceOffset = face*9,
|
||||||
|
zs = [2,5,8].map(i => vertices[faceOffset+i].round(5)), //get zs from face vertices
|
||||||
|
lowZ = Math.min(zs),
|
||||||
|
highZ = Math.max(zs),
|
||||||
|
checked = {},
|
||||||
|
check = [face];
|
||||||
|
|
||||||
|
//check for errors, and throw if found
|
||||||
|
if(dissimilar(...zs)){
|
||||||
|
throw `face must have only 2 Z values. found: ${zs.join(", ")}`
|
||||||
|
}else if(Math.abs( faces[face * 6 + 2] > 1e-6 )){
|
||||||
|
throw "face's normal must be perpendicular to Z axis"
|
||||||
|
}
|
||||||
|
|
||||||
|
found[face] = 1;
|
||||||
|
while (check.length) {
|
||||||
|
const face = check.shift();
|
||||||
|
const froot = face * 6;
|
||||||
|
const vroot = face * 9
|
||||||
|
|
||||||
|
// filter faces to those perpendicular to +z axis
|
||||||
|
if ( Math.abs( faces[froot + 2] > 1e-6 )) continue;
|
||||||
|
//filter faces with verts don't share the z values of the first face
|
||||||
|
if(
|
||||||
|
!(vertices[vroot + 2] !== lowZ || vertices[vroot + 2] !== highZ) &&
|
||||||
|
!(vertices[vroot + 5] !== lowZ || vertices[vroot + 5] !== highZ) &&
|
||||||
|
!(vertices[vroot + 8] !== lowZ || vertices[vroot + 8] !== highZ)
|
||||||
|
) continue;
|
||||||
|
|
||||||
|
const fadj = this.getAdjacentFaces(face);
|
||||||
|
for (let f of fadj) {
|
||||||
|
if (found[f] || checked[f]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const aroot = f * 6;
|
||||||
|
let sum = 0;
|
||||||
|
for (let i=0; i<3; i++) {
|
||||||
|
sum += Math.pow(faces[froot + i] - faces[aroot + i], 2);
|
||||||
|
}
|
||||||
|
const fn = Math.sqrt(sum);
|
||||||
|
if (fn <= radianTolerance) {
|
||||||
|
out.push(f);
|
||||||
|
check.push(f)
|
||||||
|
checked[f] = 1;
|
||||||
|
found[f] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// given a list of faces (indices), return an array of closed
|
||||||
|
// polylines that represent the outlines of each discrete island
|
||||||
generateOutlines(list) {
|
generateOutlines(list) {
|
||||||
const { faces, sides } = this.getIndex();
|
const { faces, sides } = this.getIndex();
|
||||||
const prec = this.precision;
|
const prec = this.precision;
|
||||||
|
|
|
||||||
|
|
@ -437,6 +437,7 @@
|
||||||
<div title="clean up after roughing or perform a part cutout">outline</div>
|
<div title="clean up after roughing or perform a part cutout">outline</div>
|
||||||
<div title="2.5D tracing along X or Y axis used for complex part features">contour</div>
|
<div title="2.5D tracing along X or Y axis used for complex part features">contour</div>
|
||||||
<div title="clear areas above selected surfaces">pocket</div>
|
<div title="clear areas above selected surfaces">pocket</div>
|
||||||
|
<div title="follow a helix around circular paths">helical</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -608,6 +608,26 @@ self.lang['en-us'] = {
|
||||||
cd_sela_s: "Select Matching Diameter",
|
cd_sela_s: "Select Matching Diameter",
|
||||||
cd_sela_l: ["select all holes that", "match selected tool diameter"],
|
cd_sela_l: ["select all holes that", "match selected tool diameter"],
|
||||||
|
|
||||||
|
// CNC HELICAL tolpath
|
||||||
|
ch_menu: "helical",
|
||||||
|
ch_sdwn_s: "step down (pitch)",
|
||||||
|
ch_sdwn_l: ["z-distance","per revolution","in workspace units","(inverse of threads per unit)", "0 to disable"],
|
||||||
|
ch_fsta_s: "force start angle",
|
||||||
|
ch_fsta_l: ["override start angle","defaults to closest point to tool"],
|
||||||
|
ch_stra_s: "start angle",
|
||||||
|
ch_stra_l: ["start angle of helix","in degrees ccw of +x axis"],
|
||||||
|
ch_selc_l: ["select a cylinder face", "inner holes that are too small","will be ignored."],
|
||||||
|
ch_fini_s: "bottom finish pass",
|
||||||
|
ch_fini_l: ["trace a circle at bottom of helix","used for hole clearing"],
|
||||||
|
ch_entr_s: "enter from center",
|
||||||
|
ch_entr_l: ["start cut from center of cylinder","gently arcing in horisontaly", "instead of plunging"],
|
||||||
|
ch_ento_s: "center entry offset",
|
||||||
|
ch_ento_l: ["offset from path for curved entry","usually thread depth if threadmilling","in workspace units","0 for a linear entry"],
|
||||||
|
ch_rvrs_s: "reverse",
|
||||||
|
ch_rvrs_l: ["reverse helical z-axis direction","low-Z to high-Z when checked","useful for thread milling","not recomended for other operations"],
|
||||||
|
ch_clkw_s: "clockwise",
|
||||||
|
ch_clkw_l: ["reverse helical rotation around cylinder","clockwise when checked"],
|
||||||
|
|
||||||
// CNC FLIP
|
// CNC FLIP
|
||||||
cf_menu: "flip",
|
cf_menu: "flip",
|
||||||
cf_nvrt_s: "z bottom",
|
cf_nvrt_s: "z bottom",
|
||||||
|
|
@ -643,6 +663,10 @@ self.lang['en-us'] = {
|
||||||
cl_maxz_s: "max z height",
|
cl_maxz_s: "max z height",
|
||||||
cl_maxz_l: ["highest Z band position. when set to 0, uses the highest Z point of the workspace"],
|
cl_maxz_l: ["highest Z band position. when set to 0, uses the highest Z point of the workspace"],
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// CNC CUTOUT TABS
|
// CNC CUTOUT TABS
|
||||||
ct_menu: "tabs",
|
ct_menu: "tabs",
|
||||||
ct_angl_s: "angle",
|
ct_angl_s: "angle",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue