stub in loop operation

This commit is contained in:
Stewart Allen 2026-01-02 21:25:56 -05:00
commit f25e75f557
9 changed files with 130 additions and 0 deletions

View file

@ -794,6 +794,9 @@ function newInput(label, opt = {}) {
if (opt.trigger) {
refresh(opt.trigger === 1 || opt.trigger === true);
}
if (opt.post) {
opt.post();
}
});
if (opt.units) {
addUnits(ip, opt.round || 3);

View file

@ -661,6 +661,14 @@ export function createPopOps() {
createPopOp('|', {}).inputs = {};
createPopOp('loop', {
repeat: 2,
count: 1
}).inputs = {
repeat: UC.newInput('repeat', { title: "repeat count", convert: toInt, bound: UC.bound(1, 100), post: opRender }),
count: UC.newInput('operations', { title: "num operations", convert: toInt, bound: UC.bound(1, 20), post: opRender })
};
function isClear() {
return env.poppedRec.mode === 'clear';
}

View file

@ -165,6 +165,10 @@ const opAddLevel = () => {
opAdd(env.popOp.level.new());
};
const opAddLoop = () => {
opAdd(env.popOp.loop.new());
};
const opAddRough = () => {
opAdd(env.popOp.rough.new());
};
@ -287,8 +291,33 @@ export function opRender() {
oplist.forEach((rec, i) => {
let title = '';
let clock = rec.type === '|';
let isLoop = rec.type === 'loop';
let label = clock ? `` : (rec.mode ? rec.type + ' ' + rec.mode : rec.type);
let clazz = notime ? ["draggable", "notime"] : ["draggable"];
// Loop operation label and styling
if (isLoop) {
label = `loop ${rec.repeat || 2}x`;
clazz.push('loop');
title = ` title="loop: repeat next ${rec.count || 1} operation(s) ${rec.repeat || 2} time(s)"`;
}
// Check if this operation is within N operations after a loop
let inLoopRange = false;
for (let j = i - 1; j >= 0; j--) {
if (oplist[j].type === 'loop') {
let loopCount = oplist[j].count || 1;
let distanceFromLoop = i - j;
if (distanceFromLoop <= loopCount && distanceFromLoop > 0) {
inLoopRange = true;
break;
}
}
}
if (inLoopRange) {
clazz.push('in-loop');
}
let notable = rec.note ? rec.note.split(' ').filter(v => v.charAt(0) === '#') : undefined;
if (clock) { clazz.push('clock'); title = ` title="end of ops chain\ndrag/drop like an op\nops after this are disabled"` }
if (notable?.length) {
@ -755,6 +784,7 @@ export function init() {
case "laser off": return opAddLaserOff();
case "gcode": return opAddGCode();
case "level": return opAddLevel();
case "loop": return opAddLoop();
case "rough": return opAddRough();
case "outline": return opAddOutline();
case "contour":

View file

@ -0,0 +1,17 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from './op.js';
export class OpLoop extends CamOp {
weight() {
return 0; // No weight, acts as marker
}
async slice() {
// No-op: expansion happens in slice.js
}
prepare() {
// No-op: ignored during g-code generation
}
}

View file

@ -11,6 +11,7 @@ import { OpLaserOff } from './op-laser-off.js';
import { OpLaserOn } from './op-laser-on.js';
import { OpLathe } from './op-lathe.js';
import { OpLevel } from './op-level.js';
import { OpLoop } from './op-loop.js';
import { OpOutline } from './op-outline.js';
import { OpPocket } from './op-pocket.js';
import { OpRegister } from './op-register.js';
@ -31,6 +32,7 @@ export const ops = {
"laser on": OpLaserOn,
"lathe": OpLathe,
"level": OpLevel,
"loop": OpLoop,
"outline": OpOutline,
"pocket": OpPocket,
"register": OpRegister,

View file

@ -990,6 +990,10 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
// pre-flight check of ops
for (let op of widget.camops) {
// Skip loop operations (they're expanded during slice)
if (op.op.type === 'loop') {
continue;
}
opTot += op.weight();
// ensure tool related parameters are available
// for the first index call when no tool is specified
@ -999,6 +1003,10 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
}
for (let op of widget.camops) {
// Skip loop operations (they're expanded during slice)
if (op.op.type === 'loop') {
continue;
}
contouring = false;
lasering = false;
let cop = currentOp = op.op;

View file

@ -324,6 +324,52 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
opTot += opList.peek().weight();
}
// LOOP EXPANSION - duplicate next N operations M times
let expandedOps = [];
let i = 0;
while (i < activeOps.length) {
let op = activeOps[i];
if (op.type === 'loop') {
let repeatCount = op.repeat || 2; // M times
let opCount = op.count || 1; // N operations
// Validate we have enough operations
if (i + opCount >= activeOps.length) {
return error(`Loop operation requires ${opCount} following operation(s), but only ${activeOps.length - i - 1} available`);
}
// Extract next N operations
let loopOps = activeOps.slice(i + 1, i + 1 + opCount);
// Check for nested loops
for (let loopOp of loopOps) {
if (loopOp.type === 'loop') {
return error("Nested loops are not supported");
}
}
// Duplicate operations M times
for (let r = 0; r < repeatCount; r++) {
for (let loopOp of loopOps) {
let clonedOp = Object.clone(loopOp);
expandedOps.push(clonedOp);
}
}
// Skip the loop operation itself and the N operations
i += 1 + opCount;
} else if (op.type === '|') {
break; // Clock operation
} else {
expandedOps.push(op);
i++;
}
}
activeOps = expandedOps;
// determine # of steps and step weighting for progress bar
for (let op of activeOps) {
if (op.type === '|') {

View file

@ -1448,6 +1448,21 @@ details[open] summary::after {
#oplist .clock:hover {
background-color: var(--blue-2);
}
#oplist .loop:hover {
background-color: rgba(100, 150, 255, 0.35);
}
#oplist .in-loop {
margin-left: 10px;
}
#oplist .in-loop::before {
content: "";
border: 1px solid var(--blue-2);
border-radius: 3px;
width: 2px;
height: 80%;
position: absolute;
left: -10px;
}
#oplist > div {
padding: 4px 20px 4px 7px !important;
}

View file

@ -471,6 +471,7 @@
<div title="remove an area of material&#13;inside a defined boundary&#13;sometimes called pocketing">rough</div>
<div title="cutout parts using their shadow outline">outline</div>
<div title="perform operations on using outlines or surfaces&#13;many operations are simple wrappers around this">area</div>
<div id="loop" title="loop next N operations M times">loop</div>
</div>
</div>
</div>