From deb3e0faa9ab73f38103ca47d5c76d532da0fec3 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Tue, 23 Dec 2025 20:12:47 -0500 Subject: [PATCH 001/154] extract poly utils into their own static class --- src/geo/poly/arcs.js | 325 +++++++++++++++ src/geo/poly/geo.js | 280 +++++++++++++ src/geo/poly/mesh.js | 276 ++++++++++++ src/geo/poly/path.js | 374 +++++++++++++++++ src/geo/polygon.js | 967 ++----------------------------------------- 5 files changed, 1280 insertions(+), 942 deletions(-) create mode 100644 src/geo/poly/arcs.js create mode 100644 src/geo/poly/geo.js create mode 100644 src/geo/poly/mesh.js create mode 100644 src/geo/poly/path.js diff --git a/src/geo/poly/arcs.js b/src/geo/poly/arcs.js new file mode 100644 index 00000000..8697a6d6 --- /dev/null +++ b/src/geo/poly/arcs.js @@ -0,0 +1,325 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +"use strict"; + +import { base, util } from '../base.js'; +import { newPoint } from '../point.js'; + +/** + * Arc detection utility functions for polygons. + * These utilities detect and annotate circular arc sequences in polygon points. + */ + +/** + * Calculate the center point of a circle defined by three points. + * Using perimeter length, finds 3 equally spaced points to compute circle center. + * + * @param {Polygon} poly - polygon assumed to be roughly circular + * @returns {Point} center point of the circle + */ +export function calcCircleCenter(poly) { + let pe = poly.perimeter(), + pe1 = pe / 3, // point 2 + pe2 = pe1 * 2, // point 3 + p = poly.points, + l = p.length, + i = 1, + td = 0, + lp = p[0], // first point + ap = [ lp ]; // array of 3 points + while (i < l) { + let np = p[i++]; + let d = lp.distTo2D(np); + td += d; + if (ap.length === 1 && td >= pe1) { + ap.push(np); + } else if (ap.length === 2 && (td >= pe2 || i >= l)) { + ap.push(np); + break; + } + lp = np; + } + let center = util.center2d(...ap); + return newPoint(center.x, center.y, p[0].z, null); +} + +/** + * Iterate over polygon points and find sequences of 5 or more points + * with a common center point and collect as midpoint/radius. + * + * @param {Polygon} poly - polygon to analyze + * @param {Object} opt - options + * @param {number} opt.tolerance - center matching tolerance (default 1e-2) + * @param {boolean} opt.inside - require centers be inside polygon (default true) + * @returns {Array} array of arc center records + */ +export function findArcCenters(poly, opt = {}) { + if (poly.length < 6) return []; + let tolerance = opt.tolerance || 1e-2, + inside = opt.inside ?? true, + seq = poly.points.slice(), + util = base.util; + if (poly.isClosed()) seq.appendAll(seq.slice(0,5)); + let recs = [], // accumulated arc points + cand = []; // candidate center array + for (let pos=3; pos= 3) { + // emit record on 5 points + recs.push(cand.peek()); + } + // reset candidate array + cand = [ next ]; + } + if (cand.length >= 3) { + recs.push(cand.peek()); + } + // filter dups + recs.sort((a,b) => { + return util.dist2D(a,b); + }); + recs = recs.filter((r,i) => { + if (i > 0) { + return util.dist2D(r, recs[i-1]) < tolerance*10 ? null : r; + } else { + return r; + } + }); + return recs; +} + +/** + * Detect and annotate arc sequences in polygon points. + * When an arc is detected, the first point is annotated with arc metadata + * and intermediate points remain in the array but should be skipped during emission. + * + * @param {Polygon} poly - polygon to analyze (will be mutated with arc annotations) + * @param {Object} opts - detection options + * @param {number} opts.tolerance - arc detection tolerance (default 0.1) + * @param {number} opts.arcRes - arc resolution in radians (default 0.1) + * @param {number} opts.minPoints - minimum points to consider an arc (default 4) + * @returns {Polygon} this polygon with arc annotations + */ +export function detectArcs(poly, opts = {}) { + const { + tolerance = 0.1, + arcRes = 0.1, + minPoints = 4 + } = opts; + + const points = poly.points; + const length = points.length; + + if (length < minPoints) { + return poly; + } + + // Clear any existing arc annotations + for (let p of points) { + delete p.arc; + } + + let i = 0; + // Process all points except we can't start an arc at the last point + // because we need at least minPoints to form an arc + while (i < length - minPoints + 1) { + // Try to detect arc starting at position i + const arcData = detectArcAt(poly, i, points, length, tolerance, arcRes, minPoints); + + if (arcData) { + // Annotate the starting point + points[i].arc = arcData; + // Skip past the arc points + i += arcData.skip + 1; // +1 to move to point after arc end + } else { + i++; + } + } + + return poly; +} + +/** + * Try to detect an arc starting at the given index. + * Uses a greedy approach: grow the arc as long as possible, then validate. + * + * @private + * @param {Polygon} poly - polygon being analyzed + * @param {number} startIdx - starting index + * @param {Point[]} points - polygon points + * @param {number} length - points length + * @param {number} tolerance - detection tolerance + * @param {number} arcRes - arc resolution + * @param {number} minPoints - minimum points for arc + * @returns {Object|null} arc data or null if no arc detected + */ +export function detectArcAt(poly, startIdx, points, length, tolerance, arcRes, minPoints) { + // Greedy approach: collect as many points as possible that might form an arc + let candidates = []; + + for (let idx = startIdx; idx < length; idx++) { + const p1 = points[idx]; + + // Last point - add and done + if (idx >= length - 1) { + candidates.push(p1); + break; + } + + const p2 = points[idx + 1]; + + // Skip duplicate points + if (p1.distTo2D(p2) < 0.001) { + break; + } + + candidates.push(p1); + + // Stop if we've collected enough to test + if (candidates.length >= minPoints) { + // Try to validate the arc with current candidates + const arcData = validateArc(poly, candidates, tolerance, arcRes, minPoints); + + if (!arcData) { + // Current set doesn't form valid arc, back up one point + candidates.pop(); + break; + } + } + } + + // Final validation with all collected candidates + return validateArc(poly, candidates, tolerance, arcRes, minPoints); +} + +/** + * Validate if a set of points forms a valid arc. + * + * @private + * @param {Polygon} poly - polygon being analyzed + * @param {Point[]} arcPoints - candidate arc points + * @param {number} tolerance - detection tolerance + * @param {number} arcRes - arc resolution + * @param {number} minPoints - minimum points for arc + * @returns {Object|null} arc data or null if invalid + */ +export function validateArc(poly, arcPoints, tolerance, arcRes, minPoints) { + if (arcPoints.length < minPoints) { + return null; + } + + // Calculate center using well-distributed points + const center = findBestCenter(arcPoints, tolerance); + if (!center) { + return null; + } + + // Check if all points lie on the circle within tolerance + let maxRadiusError = 0; + let sumRadiusError = 0; + + for (let i = 0; i < arcPoints.length; i++) { + const p = arcPoints[i]; + const radius = Math.hypot(p.x - center.x, p.y - center.y); + const radiusError = Math.abs(radius - center.r); + + maxRadiusError = Math.max(maxRadiusError, radiusError); + sumRadiusError += radiusError; + + // Hard limit on any single point + if (radiusError > tolerance * 2) { + return null; + } + } + + // Check average error is reasonable + const avgRadiusError = sumRadiusError / arcPoints.length; + if (avgRadiusError > tolerance) { + return null; + } + + // Check angular resolution (don't want points too far apart) + for (let i = 0; i < arcPoints.length - 1; i++) { + const curr = arcPoints[i]; + const next = arcPoints[i + 1]; + const dist = curr.distTo2D(next); + const radius = Math.hypot(curr.x - center.x, curr.y - center.y); + + if (radius > 0) { + const angle = 2 * Math.asin(Math.min(1, dist / (2 * radius))); + if (Math.abs(angle) > arcRes) { + return null; + } + } + } + + // Determine arc direction + const p0 = arcPoints[0]; + const p1 = arcPoints[Math.min(1, arcPoints.length - 1)]; + const vec1 = { x: p1.x - p0.x, y: p1.y - p0.y }; + const vec2 = { x: center.x - p0.x, y: center.y - p0.y }; + const cross = vec1.x * vec2.y - vec1.y * vec2.x; + const clockwise = cross < 0; + + return { + center: newPoint(center.x, center.y, p0.z), + clockwise, + skip: arcPoints.length - 1 + }; +} + +/** + * Find the best-fit center for a set of arc points. + * + * @private + * @param {Point[]} arcPoints - candidate arc points + * @param {number} tolerance - detection tolerance + * @returns {Object|null} center with x, y, r properties or null + */ +export function findBestCenter(arcPoints, tolerance) { + const len = arcPoints.length; + + // Use 3 well-spaced points for initial center calculation + let idx1 = 0; + let idx2 = Math.floor(len / 2); + let idx3 = len - 1; + + // If first and last are very close (near-circle), use different points + if (arcPoints[idx1].distTo2D(arcPoints[idx3]) < tolerance * 2) { + idx1 = Math.floor(len * 0.25); + idx2 = Math.floor(len * 0.5); + idx3 = Math.floor(len * 0.75); + } + + const center = util.center2d( + arcPoints[idx1], + arcPoints[idx2], + arcPoints[idx3], + 1 + ); + + if (!center || center.hasNaN?.() || !isFinite(center.r)) { + return null; + } + + return center; +} diff --git a/src/geo/poly/geo.js b/src/geo/poly/geo.js new file mode 100644 index 00000000..0a69a951 --- /dev/null +++ b/src/geo/poly/geo.js @@ -0,0 +1,280 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +"use strict"; + +import { base, config, util } from '../base.js'; +import { calc_normal, calc_vertex } from '../paths.js'; +import { newPolygon, slopeDiff } from '../polygon.js'; + +/** + * Geometric utility functions for polygons. + * These are extracted from the Polygon class to improve modularity. + * All functions take a polygon as the first parameter. + */ + +/** + * Remove points that are closer than `dist` to the previous point. + * Returns a new polygon with filtered points, or null if result would have < 2 points. + * + * @param {Polygon} poly - source polygon + * @param {number} dist - minimum distance between points (defaults to precision_merge) + * @returns {Polygon|null} new filtered polygon or null + */ +export function debur(poly, dist) { + if (poly.length < 2) { + return null; + } + const pa = poly.points, + pln = pa.length, + open = poly.open, + newp = newPolygon().copyZ(poly.getZ()), + min = dist || base.config.precision_merge; + let lo; + newp.push(lo = pa[0]); + for (let i = 1; i < pln; i++) { + if (lo.distTo2D(pa[i]) >= min) { + newp.push(lo = pa[i]); + } + } + newp.open = open; + newp.parent = poly.parent; + if (newp.length < 2) { + return null; + } + return newp; +} + +/** + * Calculate miter joins for polygon points where angle exceeds 90 degrees. + * Returns either the original polygon or a new one with miter adjustments. + * + * @param {Polygon} poly - source polygon + * @param {boolean} debug - debug flag + * @returns {Polygon} polygon with miters applied + */ +export function miter(poly, debug) { + if (poly.length < 3) return poly; + + const slo = [], + pa = poly.points, + pln = pa.length, + open = poly.open; + let last; + for (let i = 1; i < pln; i++) { + slo.push(pa[i - 1].slopeTo(last = pa[i])); + } + if (!open) { + slo.push(last.slopeTo(pa[0])); + } + + const ang = new Array(pln).fill(0); + let redo = false; + const aln = open ? pln - 1 : pln; + for (let i = 1; i < aln; i++) { + ang[i] = slopeDiff(slo[i - 1], slo[i]); + redo |= ang[i] > 90; + } + if (!open) { + ang[0] = slopeDiff(slo[pln - 1], slo[0]); + redo |= ang[pln - 1] > 90; + redo |= ang[0] > 90; + } + if (redo) { + const newp = newPolygon().copyZ(poly.z); + newp.open = open; + for (let i = 0; i < pln; i++) { + const p = pa[(i + pln) % pln]; + const d = ang[(i + pln) % pln]; + if (d > 179) { + const s = slo[(i + pln) % pln]; + const pp = pa[(i + pln - 1) % pln]; + const ps = slo[(i + pln - 1) % pln]; + newp.push(p.follow(p.slopeTo(pp).normal(), 0.001)); + newp.push(p.follow(s.clone().normal().invert(), 0.001)); + } else if (d > 90) { + const s = slo[(i + pln) % pln]; + const pp = pa[(i + pln - 1) % pln]; + const ps = slo[(i + pln - 1) % pln]; + newp.push(p.follow(p.slopeTo(pp), 0.001)); + newp.push(p.follow(s, 0.001)); + } else { + p.parent = newp; + newp.push(p); + } + } + return newp; + } + return poly; +} + +/** + * Create convex hull from points and add to polygon (mutates polygon). + * Only used in fdm/slice.js for enclosing supports. + * + * @param {Polygon} poly - target polygon (will be mutated) + * @param {Point[]} points - points to form convex hull + * @returns {Polygon} the mutated polygon + */ +export function createConvexHull(poly, points) { + function removeMiddle(a, b, c) { + let cross = (a.x - b.x) * (c.y - b.y) - (a.y - b.y) * (c.x - b.x); + let dot = (a.x - b.x) * (c.x - b.x) + (a.y - b.y) * (c.y - b.y); + return cross < 0 || cross == 0 && dot <= 0; + } + + points.sort(function(a, b) { + return a.x != b.x ? a.x - b.x : a.y - b.y; + }); + + let n = points.length; + let hull = []; + + for (let i = 0; i < 2 * n; i++) { + let j = i < n ? i : 2 * n - 1 - i; + while (hull.length >= 2 && removeMiddle(hull[hull.length - 2], hull[hull.length - 1], points[j])) + hull.pop(); + hull.push(points[j]); + } + + hull.pop(); + poly.addPoints(hull); + + return poly; +} + +/** + * Find all intersections of a line segment with polygon edges. + * Results are sorted by distance from lp1. + * + * @param {Polygon} poly - polygon to test + * @param {Point} lp1 - line segment start + * @param {Point} lp2 - line segment end + * @param {boolean} deep - recurse into inner polygons + * @returns {Point[]} array of intersection points + */ +export function intersections(poly, lp1, lp2, deep) { + let list = []; + poly.forEachSegment(function(pp1, pp2, ip1, ip2) { + let int = util.intersect(lp1, lp2, pp1, pp2, base.key.SEGINT, false); + if (int) { + list.push(int); + pp1.pos = ip1; + pp2.pos = ip2; + } + }); + if (deep && poly.inner) { + poly.inner.forEach(p => { + let ints = intersections(p, lp1, lp2); + if (ints) list.appendAll(ints); + }); + } + list.sort(function(p1, p2) { + return util.distSq(lp1, p1) - util.distSq(lp1, p2); + }); + return list; +} + +/** + * Walk points noting z deltas and smoothing z sawtooth patterns. + * Used to smooth low-rez surface contouring (mutates polygon). + * + * @param {Polygon} poly - polygon to smooth (mutated) + * @param {number} passes - number of smoothing passes + * @returns {Polygon} the mutated polygon + */ +export function refine(poly, passes = 0) { + for (let j = 0; j < passes; j++) { + let points = poly.points, + length = points.length, + sn = [], // segment normals + vn = []; // vertex normals + for (let i = 0; i < length; i++) { + let p1 = points[i]; + let p2 = points[(i + 1) % length]; + sn.push(calc_normal(p1, p2)); + } + for (let i = 0; i < length; i++) { + let n1 = sn[(i + length - 1) % length]; + let n2 = sn[i]; + let vi = calc_vertex(n1, n2, 1); + vn.push(vi); + let vl = Math.abs(1 - vi.vl).round(2); + // vl should be close to zero on smooth / continuous curves + // factoring out hard turns, we smooth the z using the weighted + // z values of the points before and after the current point + if (vl === 0) { + let p0 = points[(i + length - 1) % length]; + let p1 = points[i]; + let p2 = points[(i + 1) % length]; + p1.z = (p0.z + p2.z + p1.z) / 3; + } + } + } + return poly; +} + +/** + * Test if two polygons are essentially equivalent in shape and position. + * Checks area, bounds, circularity, and point-to-segment distances. + * + * @param {Polygon} poly1 - first polygon + * @param {Polygon} poly2 - second polygon + * @param {boolean} recurse - also check inner polygons + * @param {number} precision - tolerance for comparisons + * @returns {boolean} true if polygons are equivalent + */ +export function isEquivalent(poly1, poly2, recurse, precision) { + let area1 = Math.abs(poly1.area()); + let area2 = Math.abs(poly2.area()); + if (util.isCloseTo(area1, area2, precision || config.precision_poly_area) && + poly1.bounds.equals(poly2.bounds, precision || config.precision_poly_bounds)) { + // use circularity near 1 to eliminate the extensive check below + let c1 = poly1.circularity(), + c2 = poly2.circularity(); + if (Math.abs(c1 - c2) < config.precision_circularity && ((1 - c1) < config.precision_circularity)) { + return true; + } + + if (recurse) { + let i, ai = poly1.inner, + bi = poly2.inner; + if (ai !== bi) { + if (ai === null || bi === null || ai.length != bi.length) { + return false; + } + for (i = 0; i < ai.length; i++) { + if (!isEquivalent(ai[i], bi[i])) { + return false; + } + } + } + } + + let exit = true, + pointok, + dist, + min; + + poly1.forEachPoint(i2p => { + pointok = false; + poly2.forEachSegment((i1p1, i1p2) => { + // if point is close to poly, terminate search, go to next point + if ((dist = i2p.distToLine(i1p1, i1p2)) < config.precision_poly_merge) { + return pointok = true; + } + // otherwise track min and keep searching + min = Math.min(min, dist); + }); + // fail poly if one point is bad + if (!pointok) { + exit = false; + // terminate search + return true; + } + }); + return exit; + + } + + return false; +} diff --git a/src/geo/poly/mesh.js b/src/geo/poly/mesh.js new file mode 100644 index 00000000..a6f9bef3 --- /dev/null +++ b/src/geo/poly/mesh.js @@ -0,0 +1,276 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +"use strict"; + +import { earcut as earcutLib } from '../base.js'; +import { newPolygon } from '../polygon.js'; +import { polygons as POLY } from '../polygons.js'; + +/** + * Mesh generation utility functions for polygons. + * These utilities convert 2D polygons into 3D meshes and geometry. + */ + +/** + * Triangulate polygon using earcut library. + * Returns array of triangular polygons with preserved alignment/swap info. + * + * @param {Polygon} poly - polygon to triangulate + * @returns {Polygon[]} array of triangular polygons + */ +export function earcut(poly) { + // gather all points into a single array including inner polys + // keeping track of array offset indices for inners + let out = []; + let holes = []; + + // flatten points into array for earcut() + poly.points.forEach(p => { + out.push(p.x, p.y, p.z); + }); + + // add hole offsets for inner polygons + if (poly.inner) { + poly.inner.forEach(p => { + holes.push(out.length / 3); + p.points.forEach(p => { + out.push(p.x, p.y, p.z); + }) + }); + } + + // perform earcut() + let cut = earcutLib(out, holes, 3); + let ret = []; + + // preserve swaps in new polys + for (let i = 0; i < cut.length; i += 3) { + let p = new (poly.constructor)(); + p._aligned = poly._aligned; + p._swapped = poly._swapped; + for (let j = 0; j < 3; j++) { + let n = cut[i + j] * 3; + p.add(out[n], out[n + 1], out[n + 2]); + } + ret.push(p); + } + + return ret; +} + +/** + * Turn 2D polygon into a 2.5D ribbon extruded in Z. + * Returns flat array of vertices for mesh rendering. + * + * @param {Polygon} poly - polygon to extrude as ribbon + * @param {number} z - Z height (default 1) + * @param {number} zadd - Z offset (default 0) + * @param {boolean} rev - reverse winding (default false) + * @returns {number[]} flat array of triangle vertices + */ +export function ribbonZ(poly, z = 1, zadd = 0, rev) { + let clone = poly.clone().setClockwise(); + let faces = []; + let points = clone.points; + let length = points.length; + if (rev) { + points = points.slice().reverse(); + } + for (let i=0; i face.points.map(p => [ p.x, p.y, p.z ])).flat().flat(); + } + + let inv = z < 0; + + if (inv) { + z = -z; + } + + let chamfer = opt.chamfer || 0; + let chamfer_top = opt.chamfer_top || chamfer; + let chamfer_bottom = opt.chamfer_bottom || chamfer; + + if (inv) { + let tmp = chamfer_top; + chamfer_top = chamfer_bottom; + chamfer_bottom = tmp; + } + + let zadd = (typeof opt === 'number' ? opt : opt.zadd || 0); // z bottom + let obj = []; // flat output vertex array (float-x,float-y,float-z,...) + let top_face = earcutFaces; + let bottom_face = earcutFaces; + let z_top = z + zadd; + let z_bottom = zadd; + let z_side_top = z; + let z_side_bottom = z_bottom; + + // chamfer bottom only on negative chamfer + if (chamfer < 0) { + chamfer_top = 0; + chamfer_bottom = -chamfer; + } + + // create chamfers (when defined) + if (chamfer_top) { + let inset = poly.offset(chamfer_top); + if (inset.length === 1) { + inset[0].setZ(z_top); + top_face = earcut(inset[0]); + z_side_top -= chamfer_top; + let renest = POLY.renest([poly.clone(true).setZ(z_side_top), inset[0]]); + for (let rnpoly of renest) { + obj.appendAll(ribbonMesh(rnpoly, true)); + } + } + } + + if (chamfer_bottom) { + let inset = poly.offset(chamfer_bottom); + if (inset.length === 1) { + inset[0].setZ(0); + bottom_face = earcut(inset[0]); + z_side_top -= chamfer_top || chamfer_bottom; + z_side_bottom += chamfer_bottom; + let renest = POLY.renest([poly.clone(true).setZ(z_side_bottom), inset[0]]); + for (let rnpoly of renest) { + obj.appendAll(ribbonMesh(rnpoly, false)); + } + } + } + + for (let p of top_face) { + for (let point of p.points) { + obj.push(point.x, point.y, z_top); + } + } + + // bottom face (reversed to reverse normals) + for (let p of bottom_face) { + for (let point of p.points.reverse()) { + obj.push(point.x, point.y, z_bottom); + } + } + + // outside wall + let rib = z - chamfer_top - chamfer_bottom; + obj.appendAll(ribbonZ(poly, rib, z_side_bottom)); + for (let inner of poly.inner || []) { + // inside wall(s) + obj.appendAll(ribbonZ(inner, rib, z_side_bottom, true)); + } + + if (inv) { + for (let i=2; i -- All Rights Reserved */ + +"use strict"; + +import { util } from '../base.js'; +import { newPoint } from '../point.js'; +import { newPolygon } from '../polygon.js'; +import { newSlopeFromAngle } from '../slope.js'; +import { polygons as POLY } from '../polygons.js'; + +/** + * Toolpath and CNC utility functions for polygons. + * These utilities handle path generation, segmentation, and CNC-specific operations. + */ + +/** + * Generate a trace path around the inside of a polygon including inner polys. + * Returns the noodle and the remainder of the polygon with the noodle removed. + * TODO: relocate this code to fdm/post.js + * + * @param {Polygon} poly - polygon to trace + * @param {number} width - trace width + * @returns {Object} { noodle: Polygon[], remain: Polygon[] } + */ +export function noodle(poly, width) { + let clone = poly.clone(true); + let ins = clone.offset(width) ?? []; + let remain = ins.clone(true); + let nood = POLY.nest(POLY.flatten([ clone, ...ins ], [], true)); + return { noodle: nood, remain }; +} + +/** + * Generate center crossing point cloud for thin wall filling. + * Only used for fdm thin-wall type 1 (fdm/post.js). + * TODO: relocate this code to fdm/post.js + * + * @param {Polygon} poly - polygon to analyze + * @param {number} step - step size for sampling + * @param {number} z - Z height + * @param {number} min - minimum distance threshold + * @param {number} max - maximum distance threshold + * @param {Object} opt - options + * @param {boolean} opt.lines - return lines instead of points (default false) + * @param {number} opt.mindist - minimum distance for point joining (default step * 1.5) + * @returns {Polygon[]|Point[]} array of polygons or points + */ +export function centers(poly, step, z, min, max, opt = {}) { + let cloud = [], + bounds = poly.bounds, + lines = opt.lines || false, + stepoff = step / 2, + set = [poly.points]; + + if (poly.inner) { + for (let inner of poly.inner) { + set.push(inner.points); + } + } + + for (let y of util.lerp(bounds.miny + stepoff, bounds.maxy - stepoff, step, true)) { + let ints = []; + for (let points of set) { + let length = points.length; + for (let i = 0; i < length; i++) { + let p1 = points[i % length]; + let p2 = points[(i + 1) % length]; + if ( + (p1.y <= y && p2.y > y) || + (p1.y > y && p2.y <= y) + ) ints.push([p1, p2]); + } + } + let cntr = []; + if (ints.length && ints.length % 2 === 0) { + for (let int of ints) { + let [p1, p2] = int; + if (p2.y < p1.y) { + let tp = p1; + p1 = p2; + p2 = tp; + } + let minx = Math.min(p1.x, p2.x); + let maxx = Math.max(p1.x, p2.x); + let miny = Math.min(p1.y, p2.y); + let maxy = Math.max(p1.y, p2.y); + let dx = p2.x - p1.x; + let dy = maxy - miny; + let pct = (y - miny) / dy; + let xpo = p1.x + pct * dx; + cntr.push(xpo); + } + } + cntr.sort((a, b) => { + return b - a; + }); + let lp, eo = 0; + for (let x of cntr) { + let p = newPoint(x, y, z); + if (eo++ % 2) { + let d = lp.distTo2D(p); + if (d >= min && d <= max) { + if (lines) { + cloud.push(lp); + cloud.push(p); + } else { + cloud.push(newPoint( + (lp.x + p.x) / 2, y, z + )); + } + } + } else { + lp = p; + } + } + } + + for (let x of util.lerp(bounds.minx + stepoff, bounds.maxx - stepoff, step, true)) { + let ints = []; + for (let points of set) { + let length = points.length; + for (let i = 0; i < length; i++) { + let p1 = points[i % length]; + let p2 = points[(i + 1) % length]; + if ( + (p1.x <= x && p2.x > x) || + (p1.x > x && p2.x <= x) + ) ints.push([p1, p2]); + } + } + let cntr = []; + if (ints.length && ints.length % 2 === 0) { + for (let int of ints) { + let [p1, p2] = int; + if (p2.x < p1.x) { + let tp = p1; + p1 = p2; + p2 = tp; + } + let minx = Math.min(p1.x, p2.x); + let maxx = Math.max(p1.x, p2.x); + let miny = Math.min(p1.y, p2.y); + let maxy = Math.max(p1.y, p2.y); + let dx = maxx - minx; + let dy = p2.y - p1.y; + let pct = (x - minx) / dx; + let ypo = p1.y + pct * dy; + cntr.push(ypo); + } + } + cntr.sort((a, b) => { + return b - a; + }); + let lp, eo = 0; + for (let y of cntr) { + let p = newPoint(x, y, z); + if (eo++ % 2) { + let d = lp.distTo2D(p); + if (d >= min && d <= max) { + if (lines) { + cloud.push(lp); + cloud.push(p); + } else { + cloud.push(newPoint( + x, (lp.y + p.y) / 2, z + )); + } + } + } else { + lp = p; + } + } + } + + if (lines) { + return cloud; + } + + let mindist = opt.mindist || step * 1.5; + + function build(polyPoints) { + let lastp = polyPoints.last(); + let minp; + let mind = Infinity; + for (let point of cloud) { + let dist = point.distTo2D(lastp); + if (dist < mindist && dist < mind) { + mind = dist; + minp = point; + } + } + if (minp) { + cloud = cloud.filter(p => p !== minp); + polyPoints.push(minp); + return true; + } + return false; + } + + // join points into polys + let polys = []; + let polyPoints = []; + while (cloud.length) { + if (polyPoints.length === 0) { + polyPoints = [cloud.shift()]; + polys.push(polyPoints); + continue; + } + if (build(polyPoints)) { + continue; + } + if (!polyPoints.flip) { + polyPoints.reverse(); + polyPoints.flip = true; + continue; + } + if (polyPoints.length) { + polyPoints = []; + } else { + throw "whoop there it is"; + } + } + + return polys + .filter(polyPoints => polyPoints.length > 1) + .map(polyPoints => { + let np = newPolygon().setOpen(); + for (let p of polyPoints) { + np.push(p); + } + if (np.last().distTo2D(np.first()) <= max) { + np.setClosed(); + } + np = np.clean(); + return np; + }); +} + +/** + * Split long straight lines into segments no longer than `max`. + * Returns a new polygon. Optionally `mark` points with their derived + * segment start point (hinting for thin walls), `wrap` the poly first + * point by appending to the end (adaptive walls), or stop segmentation at `maxoff`. + * + * @param {Polygon} poly - polygon to segment + * @param {number} max - maximum segment length (default 1) + * @param {boolean} mark - mark points with segment start (default false) + * @param {boolean} wrap - wrap first point to end (default false) + * @param {number} maxoff - maximum offset for segmentation (default Infinity) + * @returns {Polygon} new segmented polygon + */ +export function segment(poly, max = 1, mark = false, wrap = false, maxoff = Infinity) { + const newp = []; + const points = poly.points; + const length = points.length; + const l0 = poly.open ? length - 1 : length; + for (let i=0, p=points, l1=length, l2=l1+1; i length) { + if (wrap) { + newp.push(newp[0]); + } + return newPolygon().addPoints(newp.map(p => p.clone(['segment']))).setOpenValue(poly.open); + } + return poly; +} + +/** + * For any poly points closer than `dist`, replace them with their midpoint. + * Only used by cam/op-pocket.js. + * + * @param {Polygon} poly - polygon to process + * @param {number} dist - distance threshold (default 0.01) + * @returns {Polygon} new polygon with midpoints or original if no changes + */ +export function midpoints(poly, dist = 0.01) { + const newp = []; + const points = poly.points; + const length = points.length; + const l0 = poly.open ? length - 1 : length; + let mod = 0; + for (let i=0, p=points; i p.clone())).setOpenValue(poly.open); + } + return poly; +} + +/** + * Add dogbone joints for CNC operations at sharp interior corners. + * Mutates the polygon in place. Recursively processes inner polygons. + * + * @param {Polygon} poly - polygon to modify (mutated) + * @param {number} dist - dogbone distance/size + * @param {boolean} reverse - reverse winding direction + * @returns {Polygon} the mutated polygon + */ +export function addDogbones(poly, dist, reverse) { + let open = poly.open; + let isCW = poly.isClockwise(); + if (reverse || poly.parent) isCW = !isCW; + let oldpts = poly.points.slice(); + let lastpt = oldpts[oldpts.length - 1]; + let lastsl = lastpt.slopeTo(oldpts[0]).toUnit(); + let length = oldpts.length + (open ? 0 : 1); + let newpts = []; + for (let i = 0; i < length; i++) { + let nextpt = oldpts[i % oldpts.length]; + let nextsl = lastpt.slopeTo(nextpt).toUnit(); + let adiff = lastsl.angleDiff(nextsl, true); + let bdiff = ((adiff < 0 ? (180 - adiff) : (180 + adiff)) / 2) + 180; + if (!open || (i > 1 && i < length)) { + if (isCW && adiff > 45) { + let newa = newSlopeFromAngle(lastsl.angle + bdiff); + newpts.push(lastpt.projectOnSlope(newa, dist)); + newpts.push(lastpt.clone()); + } else if (!isCW && adiff < -45) { + let newa = newSlopeFromAngle(lastsl.angle - bdiff); + newpts.push(lastpt.projectOnSlope(newa, dist)); + newpts.push(lastpt.clone()); + } + } + lastsl = nextsl; + lastpt = nextpt; + if (i < oldpts.length) { + newpts.push(nextpt); + } + } + poly.points = newpts; + if (poly.inner) { + poly.inner.forEach(inner => addDogbones(inner, dist, true)); + } + return poly; +} diff --git a/src/geo/polygon.js b/src/geo/polygon.js index 1349371f..070e1887 100644 --- a/src/geo/polygon.js +++ b/src/geo/polygon.js @@ -9,6 +9,10 @@ import { newBounds } from './bounds.js'; import { newPoint, pointFromClipper } from './point.js'; import { newSlopeFromAngle } from './slope.js'; import { polygons as POLY } from './polygons.js'; +import * as geo from './poly/geo.js'; +import * as arcs from './poly/arcs.js'; +import * as mesh from './poly/mesh.js'; +import * as path from './poly/path.js'; const { Vector3 } = THREE; @@ -222,43 +226,7 @@ export class Polygon { } earcut() { - // gather all points into a single array including inner polys - // keeping track of array offset indices for inners - let out = []; - let holes = []; - - // flatten points into array for earcut() - this.points.forEach(p => { - out.push(p.x, p.y, p.z); - }); - - // add hole offsets for inner polygons - if (this.inner) { - this.inner.forEach(p => { - holes.push(out.length / 3); - p.points.forEach(p => { - out.push(p.x, p.y, p.z); - }) - }); - } - - // perform earcut() - let cut = earcut(out, holes, 3); - let ret = []; - - // preserve swaps in new polys - for (let i = 0; i < cut.length; i += 3) { - let p = new Polygon(); - p._aligned = this._aligned; - p._swapped = this._swapped; - for (let j = 0; j < 3; j++) { - let n = cut[i + j] * 3; - p.add(out[n], out[n + 1], out[n + 2]); - } - ret.push(p); - } - - return ret; + return mesh.earcut(this); } setInner(inner) { @@ -271,314 +239,27 @@ export class Polygon { // of the polygon with the noodle removed (for the next pass) // todo: relocate this code to post.js noodle(width) { - let clone = this.clone(true); - let ins = clone.offset(width) ?? []; - let remain = ins.clone(true); - let nood = POLY.nest(POLY.flatten([ clone, ...ins ], [], true)); - return { noodle: nood, remain }; + return path.noodle(this, width); } // generate center crossing point cloud // only used for fdm thin-wall type 1 (fdm/post.js) // todo: relocate this code to post.js centers(step, z, min, max, opt = {}) { - let cloud = [], - bounds = this.bounds, - lines = opt.lines || false, - stepoff = step / 2, - set = [this.points]; - - if (this.inner) { - for (let inner of this.inner) { - set.push(inner.points); - } - } - - for (let y of util.lerp(bounds.miny + stepoff, bounds.maxy - stepoff, step, true)) { - let ints = []; - for (let points of set) { - let length = points.length; - for (let i = 0; i < length; i++) { - let p1 = points[i % length]; - let p2 = points[(i + 1) % length]; - if ( - (p1.y <= y && p2.y > y) || - (p1.y > y && p2.y <= y) - ) ints.push([p1, p2]); - } - } - let cntr = []; - if (ints.length && ints.length % 2 === 0) { - for (let int of ints) { - let [p1, p2] = int; - if (p2.y < p1.y) { - let tp = p1; - p1 = p2; - p2 = tp; - } - let minx = Math.min(p1.x, p2.x); - let maxx = Math.max(p1.x, p2.x); - let miny = Math.min(p1.y, p2.y); - let maxy = Math.max(p1.y, p2.y); - let dx = p2.x - p1.x; - let dy = maxy - miny; - let pct = (y - miny) / dy; - let xpo = p1.x + pct * dx; - cntr.push(xpo); - } - } - cntr.sort((a, b) => { - return b - a; - }); - let lp, eo = 0; - for (let x of cntr) { - let p = newPoint(x, y, z); - if (eo++ % 2) { - let d = lp.distTo2D(p); - if (d >= min && d <= max) { - if (lines) { - cloud.push(lp); - cloud.push(p); - } else { - cloud.push(newPoint( - (lp.x + p.x) / 2, y, z - )); - } - } - } else { - lp = p; - } - } - } - - for (let x of util.lerp(bounds.minx + stepoff, bounds.maxx - stepoff, step, true)) { - let ints = []; - for (let points of set) { - let length = points.length; - for (let i = 0; i < length; i++) { - let p1 = points[i % length]; - let p2 = points[(i + 1) % length]; - if ( - (p1.x <= x && p2.x > x) || - (p1.x > x && p2.x <= x) - ) ints.push([p1, p2]); - } - } - let cntr = []; - if (ints.length && ints.length % 2 === 0) { - for (let int of ints) { - let [p1, p2] = int; - if (p2.x < p1.x) { - let tp = p1; - p1 = p2; - p2 = tp; - } - let minx = Math.min(p1.x, p2.x); - let maxx = Math.max(p1.x, p2.x); - let miny = Math.min(p1.y, p2.y); - let maxy = Math.max(p1.y, p2.y); - let dx = maxx - minx; - let dy = p2.y - p1.y; - let pct = (x - minx) / dx; - let ypo = p1.y + pct * dy; - cntr.push(ypo); - } - } - cntr.sort((a, b) => { - return b - a; - }); - let lp, eo = 0; - for (let y of cntr) { - let p = newPoint(x, y, z); - if (eo++ % 2) { - let d = lp.distTo2D(p); - if (d >= min && d <= max) { - if (lines) { - cloud.push(lp); - cloud.push(p); - } else { - cloud.push(newPoint( - x, (lp.y + p.y) / 2, z - )); - } - } - } else { - lp = p; - } - } - } - - if (lines) { - return cloud; - } - - let mindist = opt.mindist || step * 1.5; - - function build(poly) { - let lastp = poly.last(); - let minp; - let mind = Infinity; - for (let point of cloud) { - let dist = point.distTo2D(lastp); - if (dist < mindist && dist < mind) { - mind = dist; - minp = point; - } - } - if (minp) { - cloud = cloud.filter(p => p !== minp); - poly.push(minp); - return true; - } - return false; - } - - // join points into polys - let polys = []; - let poly = []; - while (cloud.length) { - if (poly.length === 0) { - poly = [cloud.shift()]; - polys.push(poly); - continue; - } - if (build(poly)) { - continue; - } - if (!poly.flip) { - poly.reverse(); - poly.flip = true; - continue; - } - if (poly.length) { - poly = []; - } else { - throw "whoop there it is"; - } - } - - return polys - .filter(poly => poly.length > 1) - .map(poly => { - let np = newPolygon().setOpen(); - for (let p of poly) { - np.push(p); - } - if (np.last().distTo2D(np.first()) <= max) { - np.setClosed(); - } - np = np.clean(); - return np; - }); + return path.centers(this, step, z, min, max, opt); } debur(dist) { - if (this.len < 2) { - return null; - } - const pa = this.points, - pln = pa.length, - open = this.open, - newp = newPolygon().copyZ(this.z), - min = dist || base.config.precision_merge; - let lo; - newp.push(lo = pa[0]); - for (let i = 1; i < pln; i++) { - if (lo.distTo2D(pa[i]) >= min) { - newp.push(lo = pa[i]); - } - } - newp.open = open; - newp.parent = this.parent; - if (newp.length < 2) { - return null; - } - return newp; + return geo.debur(this, dist); } miter(debug) { - if (this.length < 3) return this; - - const slo = [], - pa = this.points, - pln = pa.length, - open = this.open; - let last; - for (let i = 1; i < pln; i++) { - slo.push(pa[i - 1].slopeTo(last = pa[i])); - } - if (!open) { - slo.push(last.slopeTo(pa[0])); - } - - const ang = new Array(pln).fill(0); - let redo = false; - const aln = open ? pln - 1 : pln; - for (let i = 1; i < aln; i++) { - ang[i] = slopeDiff(slo[i - 1], slo[i]); - redo |= ang[i] > 90; - } - if (!open) { - // ang[pln-1] = slopeDiff(slo[pln-2], slo[pln-1]); - ang[0] = slopeDiff(slo[pln - 1], slo[0]); - redo |= ang[pln - 1] > 90; - redo |= ang[0] > 90; - } - if (redo) { - const newp = newPolygon().copyZ(this.z); - // newp.debug = this.debug = true; - newp.open = open; - for (let i = 0; i < pln; i++) { - const p = pa[(i + pln) % pln]; - const d = ang[(i + pln) % pln]; - if (d > 179) { - const s = slo[(i + pln) % pln]; - const pp = pa[(i + pln - 1) % pln]; - const ps = slo[(i + pln - 1) % pln]; - newp.push(p.follow(p.slopeTo(pp).normal(), 0.001)); - newp.push(p.follow(s.clone().normal().invert(), 0.001)); - } else if (d > 90) { - const s = slo[(i + pln) % pln]; - const pp = pa[(i + pln - 1) % pln]; - const ps = slo[(i + pln - 1) % pln]; - newp.push(p.follow(p.slopeTo(pp), 0.001)); - newp.push(p.follow(s, 0.001)); - } else { - p.parent = newp; - newp.push(p); - } - } - return newp; - } - return this; + return geo.miter(this, debug); } // only used in fdm/slice.js for enclosing supports createConvexHull(points) { - function removeMiddle(a, b, c) { - let cross = (a.x - b.x) * (c.y - b.y) - (a.y - b.y) * (c.x - b.x); - let dot = (a.x - b.x) * (c.x - b.x) + (a.y - b.y) * (c.y - b.y); - return cross < 0 || cross == 0 && dot <= 0; - } - - points.sort(function(a, b) { - return a.x != b.x ? a.x - b.x : a.y - b.y; - }); - - let n = points.length; - let hull = []; - - for (let i = 0; i < 2 * n; i++) { - let j = i < n ? i : 2 * n - 1 - i; - while (hull.length >= 2 && removeMiddle(hull[hull.length - 2], hull[hull.length - 1], points[j])) - hull.pop(); - hull.push(points[j]); - } - - hull.pop(); - this.addPoints(hull); - - return this; + return geo.createConvexHull(this, points); } stepsFromRoot() { @@ -653,29 +334,7 @@ export class Polygon { * @returns {Point} center of a polygon assuming it's a circle */ calcCircleCenter() { - let pe = this.perimeter(), - pe1 = pe / 3, // point 2 - pe2 = pe1 * 2, // point 3 - p = this.points, - l = p.length, - i = 1, - td = 0, - lp = p[0], // first point - ap = [ lp ]; // array of 3 points - while (i < l) { - let np = p[i++]; - let d = lp.distTo2D(np); - td += d; - if (ap.length === 1 && td >= pe1) { - ap.push(np); - } else if (ap.length === 2 && (td >= pe2 || i >= l)) { - ap.push(np); - break; - } - lp = np; - } - let center = util.center2d(...ap); - return newPoint(center.x, center.y, p[0].z, null); + return arcs.calcCircleCenter(this); } /** @@ -683,60 +342,7 @@ export class Polygon { * with a common center point and collect as a midpoint/radius */ findArcCenters(opt = {}) { - if (this.length < 6) return []; - let tolerance = opt.tolerance || 1e-2, - inside = opt.inside ?? true, - seq = this.points.slice(), - util = base.util; - if (this.isClosed()) seq.appendAll(seq.slice(0,5)); - let recs = [], // accumulated arc points - cand = []; // candidate center array - for (let pos=3; pos= 3) { - // emit record on 5 points - recs.push(cand.peek()); - } - // reset candidate array - cand = [ next ]; - } - if (cand.length >= 3) { - recs.push(cand.peek()); - } - // filter dups - recs.sort((a,b) => { - return util.dist2D(a,b); - }); - recs = recs.filter((r,i) => { - if (i > 0) { - return util.dist2D(r, recs[i-1]) < tolerance*10 ? null : r; - } else { - return r; - } - }); - return recs; + return arcs.findArcCenters(this, opt); } /** @@ -751,42 +357,7 @@ export class Polygon { * @returns {Polygon} this polygon with arc annotations */ detectArcs(opts = {}) { - const { - tolerance = 0.1, - arcRes = 0.1, - minPoints = 4 - } = opts; - - const points = this.points; - const length = points.length; - - if (length < minPoints) { - return this; - } - - // Clear any existing arc annotations - for (let p of points) { - delete p.arc; - } - - let i = 0; - // Process all points except we can't start an arc at the last point - // because we need at least minPoints to form an arc - while (i < length - minPoints + 1) { - // Try to detect arc starting at position i - const arcData = this._detectArcAt(i, points, length, tolerance, arcRes, minPoints); - - if (arcData) { - // Annotate the starting point - points[i].arc = arcData; - // Skip past the arc points - i += arcData.skip + 1; // +1 to move to point after arc end - } else { - i++; - } - } - - return this; + return arcs.detectArcs(this, opts); } /** @@ -796,42 +367,7 @@ export class Polygon { * @returns {Object|null} arc data or null if no arc detected */ _detectArcAt(startIdx, points, length, tolerance, arcRes, minPoints) { - // Greedy approach: collect as many points as possible that might form an arc - let candidates = []; - - for (let idx = startIdx; idx < length; idx++) { - const p1 = points[idx]; - - // Last point - add and done - if (idx >= length - 1) { - candidates.push(p1); - break; - } - - const p2 = points[idx + 1]; - - // Skip duplicate points - if (p1.distTo2D(p2) < 0.001) { - break; - } - - candidates.push(p1); - - // Stop if we've collected enough to test - if (candidates.length >= minPoints) { - // Try to validate the arc with current candidates - const arcData = this._validateArc(candidates, tolerance, arcRes, minPoints); - - if (!arcData) { - // Current set doesn't form valid arc, back up one point - candidates.pop(); - break; - } - } - } - - // Final validation with all collected candidates - return this._validateArc(candidates, tolerance, arcRes, minPoints); + return arcs.detectArcAt(this, startIdx, points, length, tolerance, arcRes, minPoints); } /** @@ -839,68 +375,7 @@ export class Polygon { * @private */ _validateArc(arcPoints, tolerance, arcRes, minPoints) { - if (arcPoints.length < minPoints) { - return null; - } - - // Calculate center using well-distributed points - const center = this._findBestCenter(arcPoints, tolerance); - if (!center) { - return null; - } - - // Check if all points lie on the circle within tolerance - let maxRadiusError = 0; - let sumRadiusError = 0; - - for (let i = 0; i < arcPoints.length; i++) { - const p = arcPoints[i]; - const radius = Math.hypot(p.x - center.x, p.y - center.y); - const radiusError = Math.abs(radius - center.r); - - maxRadiusError = Math.max(maxRadiusError, radiusError); - sumRadiusError += radiusError; - - // Hard limit on any single point - if (radiusError > tolerance * 2) { - return null; - } - } - - // Check average error is reasonable - const avgRadiusError = sumRadiusError / arcPoints.length; - if (avgRadiusError > tolerance) { - return null; - } - - // Check angular resolution (don't want points too far apart) - for (let i = 0; i < arcPoints.length - 1; i++) { - const curr = arcPoints[i]; - const next = arcPoints[i + 1]; - const dist = curr.distTo2D(next); - const radius = Math.hypot(curr.x - center.x, curr.y - center.y); - - if (radius > 0) { - const angle = 2 * Math.asin(Math.min(1, dist / (2 * radius))); - if (Math.abs(angle) > arcRes) { - return null; - } - } - } - - // Determine arc direction - const p0 = arcPoints[0]; - const p1 = arcPoints[Math.min(1, arcPoints.length - 1)]; - const vec1 = { x: p1.x - p0.x, y: p1.y - p0.y }; - const vec2 = { x: center.x - p0.x, y: center.y - p0.y }; - const cross = vec1.x * vec2.y - vec1.y * vec2.x; - const clockwise = cross < 0; - - return { - center: newPoint(center.x, center.y, p0.z), - clockwise, - skip: arcPoints.length - 1 - }; + return arcs.validateArc(this, arcPoints, tolerance, arcRes, minPoints); } /** @@ -908,32 +383,7 @@ export class Polygon { * @private */ _findBestCenter(arcPoints, tolerance) { - const len = arcPoints.length; - - // Use 3 well-spaced points for initial center calculation - let idx1 = 0; - let idx2 = Math.floor(len / 2); - let idx3 = len - 1; - - // If first and last are very close (near-circle), use different points - if (arcPoints[idx1].distTo2D(arcPoints[idx3]) < tolerance * 2) { - idx1 = Math.floor(len * 0.25); - idx2 = Math.floor(len * 0.5); - idx3 = Math.floor(len * 0.75); - } - - const center = util.center2d( - arcPoints[idx1], - arcPoints[idx2], - arcPoints[idx3], - 1 - ); - - if (!center || center.hasNaN?.() || !isFinite(center.r)) { - return null; - } - - return center; + return arcs.findBestCenter(arcPoints, tolerance); } /** @@ -1461,27 +911,7 @@ export class Polygon { * find all intersections sorted by closest to lp1 */ intersections(lp1, lp2, deep) { - let list = []; - this.forEachSegment(function(pp1, pp2, ip1, ip2) { - let int = util.intersect(lp1, lp2, pp1, pp2, base.key.SEGINT, false); - if (int) { - list.push(int); - // console.log('pp1.pos',pp1.pos,'to',ip1); - // console.log('pp2.pos',pp2.pos,'to',ip2); - pp1.pos = ip1; - pp2.pos = ip2; - } - }); - if (deep && this.inner) { - this.inner.forEach(p => { - let ints = p.intersections(lp1, lp2); - if (ints) list.appendAll(ints); - }); - } - list.sort(function(p1, p2) { - return util.distSq(lp1, p1) - util.distSq(lp1, p2); - }); - return list; + return geo.intersections(this, lp1, lp2, deep); } // return true if any line segments on either poly crosses the other @@ -1943,60 +1373,7 @@ export class Polygon { * @returns {boolean} true if polygons are, essentially, the same */ isEquivalent(poly, recurse, precision) { - // throw new Error("isEquivalent"); - let area1 = Math.abs(this.area()); - let area2 = Math.abs(poly.area()); - if (util.isCloseTo(area1, area2, precision || config.precision_poly_area) && - this.bounds.equals(poly.bounds, precision || config.precision_poly_bounds)) { - // use circularity near 1 to eliminate the extensive check below - let c1 = this.circularity(), - c2 = poly.circularity(); - if (Math.abs(c1 - c2) < config.precision_circularity && ((1 - c1) < config.precision_circularity)) { - return true; - } - - if (recurse) { - let i, ai = this.inner, - bi = poly.inner; - if (ai !== bi) { - if (ai === null || bi === null || ai.length != bi.length) { - return false; - } - for (i = 0; i < ai.length; i++) { - if (!ai[i].isEquivalent(bi[i])) { - return false; - } - } - } - } - - let exit = true, - pointok, - dist, - min; - - this.forEachPoint(i2p => { - pointok = false; - poly.forEachSegment((i1p1, i1p2) => { - // if point is close to poly, terminate search, go to next point - if ((dist = i2p.distToLine(i1p1, i1p2)) < config.precision_poly_merge) { - return pointok = true; - } - // otherwise track min and keep searching - min = Math.min(min, dist); - }); - // fail poly if one point is bad - if (!pointok) { - exit = false; - // terminate search - return true; - } - }); - return exit; - - } - - return false; + return geo.isEquivalent(this, poly, recurse, precision); } /** @@ -2311,24 +1688,7 @@ export class Polygon { // turn 2d polygon into a 2.5D ribbon extruded in Z ribbonZ(z = 1, zadd = 0, rev) { - let poly = this.clone().setClockwise(); - let faces = []; - let points = poly.points; - let length = points.length; - if (rev) { - points = points.slice().reverse(); - } - for (let i=0; i face.points.map(p => [ p.x, p.y, p.z ])).flat().flat(); - } - - let inv = z < 0; - - if (inv) { - z = -z; - } - - let chamfer = opt.chamfer || 0; - let chamfer_top = opt.chamfer_top || chamfer; - let chamfer_bottom = opt.chamfer_bottom || chamfer; - - if (inv) { - let tmp = chamfer_top; - chamfer_top = chamfer_bottom; - chamfer_bottom = tmp; - } - - let zadd = (typeof opt === 'number' ? opt : opt.zadd || 0); // z bottom - let obj = []; // flat output vertex array (float-x,float-y,float-z,...) - let top_face = earcut; - let bottom_face = earcut; - let z_top = z + zadd; - let z_bottom = zadd; - let z_side_top = z; - let z_side_bottom = z_bottom; - - // chamfer bottom only on negative chamfer - if (chamfer < 0) { - chamfer_top = 0; - chamfer_bottom = -chamfer; - } - - // create chamfers (when defined) - if (chamfer_top) { - let inset = this.offset(chamfer_top); - if (inset.length === 1) { - inset[0].setZ(z_top); - top_face = inset[0].earcut(); - z_side_top -= chamfer_top; - let renest = POLY.renest([this.clone(true).setZ(z_side_top), inset[0]]); - for (let rnpoly of renest) { - obj.appendAll(rnpoly.ribbonMesh(true)); - } - } - } - - if (chamfer_bottom) { - let inset = this.offset(chamfer_bottom); - if (inset.length === 1) { - inset[0].setZ(0); - bottom_face = inset[0].earcut(); - z_side_top -= chamfer_top || chamfer_bottom; - z_side_bottom += chamfer_bottom; - let renest = POLY.renest([this.clone(true).setZ(z_side_bottom), inset[0]]); - for (let rnpoly of renest) { - obj.appendAll(rnpoly.ribbonMesh(false)); - } - } - } - - for (let poly of top_face) { - for (let point of poly.points) { - obj.push(point.x, point.y, z_top); - } - } - - // bottom face (reversed to reverse normals) - for (let poly of bottom_face) { - for (let point of poly.points.reverse()) { - obj.push(point.x, point.y, z_bottom); - } - } - - // outside wall - let rib = z - chamfer_top - chamfer_bottom; - obj.appendAll(this.ribbonZ(rib, z_side_bottom)); - for (let inner of this.inner || []) { - // inside wall(s) - obj.appendAll(inner.ribbonZ(rib, z_side_bottom, true)); - } - - if (inv) { - for (let i=2; i length) { - if (wrap) { - newp.push(newp[0]); - } - return newPolygon().addPoints(newp.map(p => p.clone(['segment']))).setOpenValue(this.open); - } - return this; + return path.segment(this, max, mark, wrap, maxoff); } // only used by cam/op-pocket.js // for any poly points closer than `dist`, replace them with their midpoint midpoints(dist = 0.01) { - const newp = []; - const points = this.points; - const length = points.length; - const l0 = this.open ? length - 1 : length; - let mod = 0; - for (let i=0, p=points; i p.clone())).setOpenValue(this.open); - } - return this; + return path.midpoints(this, dist); } // walk points noting z deltas and smoothing z sawtooth patterns // used to smooth low-rez surface contouring refine(passes = 0) { - for (let j = 0; j < passes; j++) { - let points = this.points, - length = points.length, - sn = [], // segment normals - vn = []; // vertex normals - for (let i = 0; i < length; i++) { - let p1 = points[i]; - let p2 = points[(i + 1) % length]; - sn.push(calc_normal(p1, p2)); - } - for (let i = 0; i < length; i++) { - let n1 = sn[(i + length - 1) % length]; - let n2 = sn[i]; - let vi = calc_vertex(n1, n2, 1); - vn.push(vi); - let vl = Math.abs(1 - vi.vl).round(2); - // vl should be close to zero on smooth / continuous curves - // factoring out hard turns, we smooth the z using the weighted - // z values of the points before and after the current point - if (vl === 0) { - let p0 = points[(i + length - 1) % length]; - let p1 = points[i]; - let p2 = points[(i + 1) % length]; - p1.z = (p0.z + p2.z + p1.z) / 3; - } - } - } + return geo.refine(this, passes); } addDogbones(dist, reverse) { - let poly = this; - let open = poly.open; - let isCW = poly.isClockwise(); - if (reverse || poly.parent) isCW = !isCW; - let oldpts = poly.points.slice(); - let lastpt = oldpts[oldpts.length - 1]; - let lastsl = lastpt.slopeTo(oldpts[0]).toUnit(); - let length = oldpts.length + (open ? 0 : 1); - let newpts = []; - for (let i = 0; i < length; i++) { - let nextpt = oldpts[i % oldpts.length]; - let nextsl = lastpt.slopeTo(nextpt).toUnit(); - let adiff = lastsl.angleDiff(nextsl, true); - let bdiff = ((adiff < 0 ? (180 - adiff) : (180 + adiff)) / 2) + 180; - if (!open || (i > 1 && i < length)) { - if (isCW && adiff > 45) { - let newa = newSlopeFromAngle(lastsl.angle + bdiff); - newpts.push(lastpt.projectOnSlope(newa, dist)); - newpts.push(lastpt.clone()); - } else if (!isCW && adiff < -45) { - let newa = newSlopeFromAngle(lastsl.angle - bdiff); - newpts.push(lastpt.projectOnSlope(newa, dist)); - newpts.push(lastpt.clone()); - } - } - lastsl = nextsl; - lastpt = nextpt; - if (i < oldpts.length) { - newpts.push(nextpt); - } - } - poly.points = newpts; - if (poly.inner) { - poly.inner.forEach(inner => inner.addDogbones(dist, true)); - } + return path.addDogbones(this, dist, reverse); } } From 66bdcfa607430f200842980c492e3a6984a6aa1d Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Tue, 23 Dec 2025 20:16:34 -0500 Subject: [PATCH 002/154] remove unused helpers that were migrated --- src/geo/polygon.js | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/src/geo/polygon.js b/src/geo/polygon.js index 070e1887..0617d77e 100644 --- a/src/geo/polygon.js +++ b/src/geo/polygon.js @@ -360,32 +360,6 @@ export class Polygon { return arcs.detectArcs(this, opts); } - /** - * Try to detect an arc starting at the given index - * Uses a greedy approach: grow the arc as long as possible, then validate - * @private - * @returns {Object|null} arc data or null if no arc detected - */ - _detectArcAt(startIdx, points, length, tolerance, arcRes, minPoints) { - return arcs.detectArcAt(this, startIdx, points, length, tolerance, arcRes, minPoints); - } - - /** - * Validate if a set of points forms a valid arc - * @private - */ - _validateArc(arcPoints, tolerance, arcRes, minPoints) { - return arcs.validateArc(this, arcPoints, tolerance, arcRes, minPoints); - } - - /** - * Find the best-fit center for a set of arc points - * @private - */ - _findBestCenter(arcPoints, tolerance) { - return arcs.findBestCenter(arcPoints, tolerance); - } - /** * add points forming a rectangle around a center point * From 49eaef231b03b995f94675c785a72f4b25997937 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Tue, 23 Dec 2025 20:20:26 -0500 Subject: [PATCH 003/154] remove unused imports --- src/geo/poly/mesh.js | 1 - src/geo/polygon.js | 5 ++--- src/kiri/mode/cam/prepare.js | 1 - 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/geo/poly/mesh.js b/src/geo/poly/mesh.js index a6f9bef3..3a2768bb 100644 --- a/src/geo/poly/mesh.js +++ b/src/geo/poly/mesh.js @@ -3,7 +3,6 @@ "use strict"; import { earcut as earcutLib } from '../base.js'; -import { newPolygon } from '../polygon.js'; import { polygons as POLY } from '../polygons.js'; /** diff --git a/src/geo/polygon.js b/src/geo/polygon.js index 0617d77e..81257832 100644 --- a/src/geo/polygon.js +++ b/src/geo/polygon.js @@ -2,12 +2,11 @@ "use strict"; -import { base, config, earcut, util } from './base.js'; -import { calc_normal, calc_vertex, paths } from './paths.js'; +import { base, config, util } from './base.js'; +import { paths } from './paths.js'; import { ClipperLib } from '../ext/clip2.esm.js'; import { newBounds } from './bounds.js'; import { newPoint, pointFromClipper } from './point.js'; -import { newSlopeFromAngle } from './slope.js'; import { polygons as POLY } from './polygons.js'; import * as geo from './poly/geo.js'; import * as arcs from './poly/arcs.js'; diff --git a/src/kiri/mode/cam/prepare.js b/src/kiri/mode/cam/prepare.js index 2f51f51f..d77245f4 100644 --- a/src/kiri/mode/cam/prepare.js +++ b/src/kiri/mode/cam/prepare.js @@ -1,6 +1,5 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { base, util } from '../../../geo/base.js'; import { tip2tipEmit, poly2polyEmit } from '../../../geo/paths.js'; import { newPoint } from '../../../geo/point.js'; import { polygons as POLY } from '../../../geo/polygons.js'; From 2a45ad87dc00001bc2a7408741089a72016d69b1 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Tue, 23 Dec 2025 20:32:09 -0500 Subject: [PATCH 004/154] rename mode client to init-ui for clarity and consistency --- src/kiri/core/export.js | 2 +- src/kiri/core/init.js | 14 +++++++------- src/kiri/mode/cam/cl-flip.js | 2 +- src/kiri/mode/cam/cl-helical.js | 2 +- src/kiri/mode/cam/cl-hole.js | 2 +- src/kiri/mode/cam/cl-ops.js | 2 +- src/kiri/mode/cam/cl-origin.js | 2 +- src/kiri/mode/cam/cl-stock.js | 2 +- src/kiri/mode/cam/cl-surface.js | 2 +- src/kiri/mode/cam/cl-tab.js | 2 +- src/kiri/mode/cam/cl-trace.js | 2 +- src/kiri/mode/cam/{client.js => init-ui.js} | 0 src/kiri/mode/drag/{client.js => init-ui.js} | 0 src/kiri/mode/fdm/{client.js => init-ui.js} | 0 src/kiri/mode/laser/{client.js => init-ui.js} | 0 src/kiri/mode/sla/{client.js => init-ui.js} | 0 src/kiri/mode/wedm/{client.js => init-ui.js} | 0 src/kiri/mode/wjet/{client.js => init-ui.js} | 0 18 files changed, 17 insertions(+), 17 deletions(-) rename src/kiri/mode/cam/{client.js => init-ui.js} (100%) rename src/kiri/mode/drag/{client.js => init-ui.js} (100%) rename src/kiri/mode/fdm/{client.js => init-ui.js} (100%) rename src/kiri/mode/laser/{client.js => init-ui.js} (100%) rename src/kiri/mode/sla/{client.js => init-ui.js} (100%) rename src/kiri/mode/wedm/{client.js => init-ui.js} (100%) rename src/kiri/mode/wjet/{client.js => init-ui.js} (100%) diff --git a/src/kiri/core/export.js b/src/kiri/core/export.js index 28ba7927..7b69b91a 100644 --- a/src/kiri/core/export.js +++ b/src/kiri/core/export.js @@ -7,7 +7,7 @@ import { local } from '../../data/local.js'; import { util } from '../../geo/base.js'; import { MODES } from './consts.js'; import { LASER as laser_driver } from '../mode/laser/driver.js'; -import { SLA as sla_client } from '../mode/sla/client.js'; +import { SLA as sla_client } from '../mode/sla/init-ui.js'; import { hash } from '../../ext/md5.js'; let printSeq = parseInt(local['kiri-print-seq'] || local['print-seq'] || "0") + 1; diff --git a/src/kiri/core/init.js b/src/kiri/core/init.js index c311a06d..5459b4e4 100644 --- a/src/kiri/core/init.js +++ b/src/kiri/core/init.js @@ -9,13 +9,13 @@ import { LOCAL, SETUP } from './main.js'; import { VIEWS, MODES, SEED } from './consts.js'; import { beta, version } from '../../moto/license.js'; import { settings as set_ctrl } from './settings.js'; -import { init as initCAM } from '../mode/cam/client.js'; -import { init as initDRAG } from '../mode/drag/client.js'; -import { init as initFDM } from '../mode/fdm/client.js'; -import { init as initLaser } from '../mode/laser/client.js'; -import { init as initSLA } from '../mode/sla/client.js'; -import { init as initWEDM } from '../mode/wedm/client.js'; -import { init as initWJET } from '../mode/wjet/client.js'; +import { init as initCAM } from '../mode/cam/init-ui.js'; +import { init as initDRAG } from '../mode/drag/init-ui.js'; +import { init as initFDM } from '../mode/fdm/init-ui.js'; +import { init as initLaser } from '../mode/laser/init-ui.js'; +import { init as initSLA } from '../mode/sla/init-ui.js'; +import { init as initWEDM } from '../mode/wedm/init-ui.js'; +import { init as initWJET } from '../mode/wjet/init-ui.js'; import { menu as menuCAM } from '../mode/cam/init-menu.js'; import { menu as menuFDM } from '../mode/fdm/init-menu.js'; import { menu as menuLaser } from '../mode/laser/init-menu.js'; diff --git a/src/kiri/mode/cam/cl-flip.js b/src/kiri/mode/cam/cl-flip.js index 166d8b4a..ec51b302 100644 --- a/src/kiri/mode/cam/cl-flip.js +++ b/src/kiri/mode/cam/cl-flip.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { api } from '../../core/api.js'; -import { env, opAdd, opRender } from './client.js'; +import { env, opAdd, opRender } from './init-ui.js'; import { CAM } from './driver-fe.js'; import { clearTabs, restoreTabs } from './cl-tab.js'; import { updateStock } from './cl-stock.js'; diff --git a/src/kiri/mode/cam/cl-helical.js b/src/kiri/mode/cam/cl-helical.js index bf64098e..dc8cda1d 100644 --- a/src/kiri/mode/cam/cl-helical.js +++ b/src/kiri/mode/cam/cl-helical.js @@ -1,6 +1,6 @@ import { api } from "../../core/api.js"; import { CAM } from "./driver-fe.js"; -import { clearPops, env } from "./client.js"; +import { clearPops, env } from "./init-ui.js"; let alert, lastWidget; diff --git a/src/kiri/mode/cam/cl-hole.js b/src/kiri/mode/cam/cl-hole.js index 5a2125ed..ecf4abd9 100644 --- a/src/kiri/mode/cam/cl-hole.js +++ b/src/kiri/mode/cam/cl-hole.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { api } from '../../core/api.js'; -import { env, clearPops } from './client.js'; +import { env, clearPops } from './init-ui.js'; import { CAM } from './driver-fe.js'; export let holeSelOn = false; diff --git a/src/kiri/mode/cam/cl-ops.js b/src/kiri/mode/cam/cl-ops.js index 68d2e971..15a6b660 100644 --- a/src/kiri/mode/cam/cl-ops.js +++ b/src/kiri/mode/cam/cl-ops.js @@ -2,7 +2,7 @@ import { $, h } from '../../../moto/webui.js'; import { api } from '../../core/api.js'; -import { env, opRender } from './client.js'; +import { env, opRender } from './init-ui.js'; import { Tool } from './tool.js'; import { opFlip } from './cl-flip.js'; import { selectHoles } from './cl-hole.js'; diff --git a/src/kiri/mode/cam/cl-origin.js b/src/kiri/mode/cam/cl-origin.js index 73fca4d9..9c3e485b 100644 --- a/src/kiri/mode/cam/cl-origin.js +++ b/src/kiri/mode/cam/cl-origin.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { api } from '../../core/api.js'; -import { env, clearPops } from './client.js'; +import { env, clearPops } from './init-ui.js'; import { CAM } from './driver-fe.js'; import { addbox, clearboxes } from '../../core/boxes.js'; diff --git a/src/kiri/mode/cam/cl-stock.js b/src/kiri/mode/cam/cl-stock.js index dbb1777d..d4b71b79 100644 --- a/src/kiri/mode/cam/cl-stock.js +++ b/src/kiri/mode/cam/cl-stock.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { api } from '../../core/api.js'; -import { env, isDark } from './client.js'; +import { env, isDark } from './init-ui.js'; import { space as SPACE } from '../../../moto/space.js'; const { ui: UI } = api; diff --git a/src/kiri/mode/cam/cl-surface.js b/src/kiri/mode/cam/cl-surface.js index baf6a2cc..0c168432 100644 --- a/src/kiri/mode/cam/cl-surface.js +++ b/src/kiri/mode/cam/cl-surface.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { api } from '../../core/api.js'; -import { env, clearPops } from './client.js'; +import { env, clearPops } from './init-ui.js'; import { CAM } from './driver-fe.js'; const DEG2RAD = Math.PI / 180; diff --git a/src/kiri/mode/cam/cl-tab.js b/src/kiri/mode/cam/cl-tab.js index 9abcd4af..c30fd266 100644 --- a/src/kiri/mode/cam/cl-tab.js +++ b/src/kiri/mode/cam/cl-tab.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { api } from '../../core/api.js'; -import { env, isDark } from './client.js'; +import { env, isDark } from './init-ui.js'; import { addbox, delbox, clearboxes } from '../../core/boxes.js'; import { traceDone } from './cl-trace.js'; import { space as SPACE } from '../../../moto/space.js'; diff --git a/src/kiri/mode/cam/cl-trace.js b/src/kiri/mode/cam/cl-trace.js index 3af4e19d..bd2e9aff 100644 --- a/src/kiri/mode/cam/cl-trace.js +++ b/src/kiri/mode/cam/cl-trace.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { api } from '../../core/api.js'; -import { env, clearPops, isDark } from './client.js'; +import { env, clearPops, isDark } from './init-ui.js'; import { CAM } from './driver-fe.js'; import { Layers } from '../../core/layers.js'; import { Stack } from '../../core/stack.js'; diff --git a/src/kiri/mode/cam/client.js b/src/kiri/mode/cam/init-ui.js similarity index 100% rename from src/kiri/mode/cam/client.js rename to src/kiri/mode/cam/init-ui.js diff --git a/src/kiri/mode/drag/client.js b/src/kiri/mode/drag/init-ui.js similarity index 100% rename from src/kiri/mode/drag/client.js rename to src/kiri/mode/drag/init-ui.js diff --git a/src/kiri/mode/fdm/client.js b/src/kiri/mode/fdm/init-ui.js similarity index 100% rename from src/kiri/mode/fdm/client.js rename to src/kiri/mode/fdm/init-ui.js diff --git a/src/kiri/mode/laser/client.js b/src/kiri/mode/laser/init-ui.js similarity index 100% rename from src/kiri/mode/laser/client.js rename to src/kiri/mode/laser/init-ui.js diff --git a/src/kiri/mode/sla/client.js b/src/kiri/mode/sla/init-ui.js similarity index 100% rename from src/kiri/mode/sla/client.js rename to src/kiri/mode/sla/init-ui.js diff --git a/src/kiri/mode/wedm/client.js b/src/kiri/mode/wedm/init-ui.js similarity index 100% rename from src/kiri/mode/wedm/client.js rename to src/kiri/mode/wedm/init-ui.js diff --git a/src/kiri/mode/wjet/client.js b/src/kiri/mode/wjet/init-ui.js similarity index 100% rename from src/kiri/mode/wjet/client.js rename to src/kiri/mode/wjet/init-ui.js From 69ef0ff54b4ebc342adcb620abaca20f3214b767 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Tue, 23 Dec 2025 21:20:11 -0500 Subject: [PATCH 005/154] consolidate stl/obj load/encode into src/load classes --- src/kiri/core/selection.js | 68 +++++++++------------------ src/load/obj.js | 22 +++++++++ src/load/stl.js | 70 ++++++++++++++++++++++++++++ src/mesh/work.js | 94 +++----------------------------------- 4 files changed, 119 insertions(+), 135 deletions(-) diff --git a/src/kiri/core/selection.js b/src/kiri/core/selection.js index 6ef28db1..2cc97abf 100644 --- a/src/kiri/core/selection.js +++ b/src/kiri/core/selection.js @@ -4,6 +4,8 @@ import { api } from './api.js'; import { space } from '../../moto/space.js'; import { THREE } from '../../ext/three.js'; import { tool as MeshTool } from '../../mesh/tool.js'; +import { encode as objEncode } from '../../load/obj.js'; +import { encode as stlEncode } from '../../load/stl.js'; const selectedMeshes = []; @@ -258,61 +260,33 @@ function exportWidgets(format = "stl") { facets += geo.attributes.position.count; }); if (format === "obj") { - let obj = []; - let vpad = 0; - for (let out of outs) { + let recs = outs.map(out => { let meta = out.widget.meta; let name = meta.file || 'unnamed'; - obj.push(`g ${name}`); let { position } = out.geo.attributes; - let pvals = position.array; - for (let i=0, il=position.count; i { let { position } = out.geo.attributes; let pvals = position.array; - for (let i=0, il=position.count; i 1) { - let {x, y, z} = out.widget.track.pos; - xo = x; - yo = y; - zo = z; + let varr = pvals; + + // Apply position offset if exporting multiple widgets + if (outs.length > 1) { + let {x, y, z} = out.widget.track.pos; + varr = new Float32Array(pvals.length); + for (let i = 0; i < pvals.length; i += 3) { + varr[i] = pvals[i] + x; + varr[i+1] = pvals[i+1] + y; + varr[i+2] = pvals[i+2] + z; } - dat.setFloat32(pos + 0, norm.x, true); - dat.setFloat32(pos + 4, norm.y, true); - dat.setFloat32(pos + 8, norm.z, true); - dat.setFloat32(pos + 12, p0.x + xo, true); - dat.setFloat32(pos + 16, p0.y + yo, true); - dat.setFloat32(pos + 20, p0.z + zo, true); - dat.setFloat32(pos + 24, p1.x + xo, true); - dat.setFloat32(pos + 28, p1.y + yo, true); - dat.setFloat32(pos + 32, p1.z + zo, true); - dat.setFloat32(pos + 36, p2.x + xo, true); - dat.setFloat32(pos + 40, p2.y + yo, true); - dat.setFloat32(pos + 44, p2.z + zo, true); - pos += 50; } - } - return stl; + + return { file: out.widget.meta.file || 'unnamed', varr }; + }); + return stlEncode(recs); } function widgets(orall) { diff --git a/src/load/obj.js b/src/load/obj.js index 4a8b1f01..ec767b0e 100644 --- a/src/load/obj.js +++ b/src/load/obj.js @@ -4,6 +4,28 @@ import { util } from '../geo/base.js'; +/** + * Export vertex array(s) to OBJ format string + * @param {Array} recs - array of {file: string, varr: Float32Array} records + * @param {String} header - optional header comment + * @returns {String} OBJ format text + */ +export function encode(recs, header = '# Generated @ https://grid.space') { + let p = 1; + let obj = [header]; + for (let rec of recs) { + let { file, varr } = rec; + obj.push(`g ${file || 'unnamed'}`); + for (let i=0; i -- All Rights Reserved */ +import { THREE } from '../ext/three.js'; + const CDH = 'Content-Disposition'; +const { Vector3, computeFaceNormal } = THREE; + +/** + * Encode vertex array(s) to binary STL format + * Automatically computes normals from triangle vertices + * @param {Array} recs - array of {file: string, varr: Float32Array} records + * @param {String} header - optional header text (max 80 chars) + * @returns {Uint8Array} binary STL data + */ +export function encode(recs, header = '') { + // Calculate total vertices + let vtot = 0; + for (let rec of recs) { + vtot += (rec.varr.length / 3); + } + + // Create STL buffer: 80 byte header + 4 byte count + (50 bytes per triangle) + let stl = new Uint8Array(80 + 4 + vtot/3 * 50); + let dat = new DataView(stl.buffer); + let pos = 84; + + // Write header text (first 80 bytes) + if (header) { + header.substring(0, 80).split('').forEach((c, i) => { + dat.setUint8(i, c.charCodeAt(0)); + }); + } + + // Write triangle count at byte 80 + dat.setInt32(80, vtot/3, true); + + // Write triangles + for (let rec of recs) { + let { varr } = rec; + for (let i = 0, l = varr.length; i < l;) { + // Read three vertices + let p0 = new Vector3(varr[i++], varr[i++], varr[i++]); + let p1 = new Vector3(varr[i++], varr[i++], varr[i++]); + let p2 = new Vector3(varr[i++], varr[i++], varr[i++]); + + // Compute face normal + let norm = computeFaceNormal(p0, p1, p2); + + // Write normal (3 floats) + dat.setFloat32(pos + 0, norm.x, true); + dat.setFloat32(pos + 4, norm.y, true); + dat.setFloat32(pos + 8, norm.z, true); + + // Write vertices (9 floats) + dat.setFloat32(pos + 12, p0.x, true); + dat.setFloat32(pos + 16, p0.y, true); + dat.setFloat32(pos + 20, p0.z, true); + dat.setFloat32(pos + 24, p1.x, true); + dat.setFloat32(pos + 28, p1.y, true); + dat.setFloat32(pos + 32, p1.z, true); + dat.setFloat32(pos + 36, p2.x, true); + dat.setFloat32(pos + 40, p2.y, true); + dat.setFloat32(pos + 44, p2.z, true); + + // Attribute byte count (2 bytes) - typically 0 + dat.setUint16(pos + 48, 0, true); + + pos += 50; + } + } + + return stl; +} export class STL { constructor() { diff --git a/src/mesh/work.js b/src/mesh/work.js index 214854d4..091a0faa 100644 --- a/src/mesh/work.js +++ b/src/mesh/work.js @@ -17,6 +17,8 @@ import { CSG } from '../geo/csg.js'; import { util } from './util.js'; import { tool as meshTool } from './tool.js'; import { meshToSTEPWithFaces } from '../load/step.js'; +import { encode as objEncode } from '../load/obj.js'; +import { encode as stlEncode } from '../load/stl.js'; const { Triangle, Vector3, BufferGeometry, BufferAttribute, computeFaceNormal } = THREE; @@ -517,51 +519,9 @@ const model = { } switch (format) { case "obj": - let p = 1; - let obj = [header]; - for (let rec of recs) { - let { file, varr } = rec; - obj.push(`g ${file}`); - for (let i=0; i { - dat.setUint8(i, c.charCodeAt(0)); - }); - // todo put Mesh:Tool info in header - dat.setInt32(80, vtot/3, true); - for (let rec of recs) { - let { id, matrix, file, varr } = rec; - for (let i=0, l=varr.length; i { - dat.setUint8(i, c.charCodeAt(0)); - }); - // todo put Mesh:Tool info in header - dat.setInt32(80, vtot/3, true); - for (let rec of recs) { - let { id, matrix, file, varr } = rec; - for (let i=0, l=varr.length; i Date: Tue, 23 Dec 2025 21:35:30 -0500 Subject: [PATCH 006/154] move unionTris into geo/polygons as unionFaces --- src/geo/polygons.js | 73 +++++++++++++++++++++++++++++++++++++++++ src/kiri/core/widget.js | 69 +------------------------------------- 2 files changed, 74 insertions(+), 68 deletions(-) diff --git a/src/geo/polygons.js b/src/geo/polygons.js index 7f9c35f2..41102373 100644 --- a/src/geo/polygons.js +++ b/src/geo/polygons.js @@ -71,6 +71,7 @@ const POLYS = { toClipper, trimTo, union, + unionFaces, xor, }; @@ -487,6 +488,78 @@ export function union(polys, minarea, all, opt = {}) { return uset; } +/** + * Recursively process triangle soup from mesh by bucketing them + * by grid cell locality with max depth. This minimizes the number + * of progressive small unions. Results in a few unions of larger polys. + * Optimized for unioning 1000+ triangular faces from mesh slicing. + * + * @param {Polygon[]} polys - array of polygons (typically triangular faces) + * @param {number} depth - recursion depth (internal, default 0) + * @returns {Polygon[]} unioned polygons + */ +export function unionFaces(polys, depth = 0) { + if (depth < 5 && polys.length > 1000) { + // Calculate bounding box of all triangles + let minX = Infinity, maxX = -Infinity; + let minY = Infinity, maxY = -Infinity; + for (let tri of polys) { + for (let pt of tri.points) { + minX = Math.min(minX, pt.x); + maxX = Math.max(maxX, pt.x); + minY = Math.min(minY, pt.y); + maxY = Math.max(maxY, pt.y); + } + } + + // Create NxN grid + const gridSize = 3; + const cellWidth = (maxX - minX) / gridSize; + const cellHeight = (maxY - minY) / gridSize; + + // Partition polygons into grid cells + const grid = Array(gridSize).fill(0).map(() => Array(gridSize).fill(0).map(() => [])); + + for (let i = 0; i < polys.length; i++) { + const poly = polys[i]; + // Use polygon centroid to assign to cell + const bounds = poly.bounds; + const cx = (bounds.minx + bounds.maxx) / 2; + const cy = (bounds.miny + bounds.maxy) / 2; + + // Clamp to grid bounds + let gx = Math.floor((cx - minX) / cellWidth); + let gy = Math.floor((cy - minY) / cellHeight); + gx = Math.max(0, Math.min(gridSize - 1, gx)); + gy = Math.max(0, Math.min(gridSize - 1, gy)); + + grid[gx][gy].push(poly); + } + + // Union within each cell + const cellResults = []; + let totalInCell = 0; + for (let gx = 0; gx < gridSize; gx++) { + for (let gy = 0; gy < gridSize; gy++) { + const cellPolys = grid[gx][gy]; + if (cellPolys.length > 0) { + totalInCell += cellPolys.length; + const cellUnion = unionFaces(cellPolys, depth + 1); + cellResults.push(...cellUnion); + } + } + } + + // Union the cell results + polys = unionFaces(cellResults, depth + 1); + + } else { + polys = union(polys, 0, true, { wasm: false }); + } + + return polys; +} + /** * @param {Polygon} poly clipping mask * @returns {?Polygon[]} diff --git a/src/kiri/core/widget.js b/src/kiri/core/widget.js index 24e475cb..1640ce1c 100644 --- a/src/kiri/core/widget.js +++ b/src/kiri/core/widget.js @@ -965,7 +965,7 @@ class Widget { }); // recursively merge grid constrained subsets of polygons - polys = unionTris(polys); + polys = POLY.unionFaces(polys); // for a more perfect union, pump shadows to merge very close lines // todo: create clipper only version that avoids round trip thru geo classes @@ -976,73 +976,6 @@ class Widget { } } -/* - * recursively process triangle soup from mesh by bucketing them - * by grid cell locality with max depth. this minimizes the number - * of progressive small unions. results in a few unions of larger polys. - */ -function unionTris(polys, depth = 0) { - if (depth < 5 && polys.length > 1000) { - // Calculate bounding box of all triangles - let minX = Infinity, maxX = -Infinity; - let minY = Infinity, maxY = -Infinity; - for (let tri of polys) { - for (let pt of tri.points) { - minX = Math.min(minX, pt.x); - maxX = Math.max(maxX, pt.x); - minY = Math.min(minY, pt.y); - maxY = Math.max(maxY, pt.y); - } - } - - // Create NxN grid - const gridSize = 3; - const cellWidth = (maxX - minX) / gridSize; - const cellHeight = (maxY - minY) / gridSize; - - // Partition polygons into grid cells - const grid = Array(gridSize).fill(0).map(() => Array(gridSize).fill(0).map(() => [])); - - for (let i = 0; i < polys.length; i++) { - const poly = polys[i]; - // Use polygon centroid to assign to cell - const bounds = poly.bounds; - const cx = (bounds.minx + bounds.maxx) / 2; - const cy = (bounds.miny + bounds.maxy) / 2; - - // Clamp to grid bounds - let gx = Math.floor((cx - minX) / cellWidth); - let gy = Math.floor((cy - minY) / cellHeight); - gx = Math.max(0, Math.min(gridSize - 1, gx)); - gy = Math.max(0, Math.min(gridSize - 1, gy)); - - grid[gx][gy].push(poly); - } - - // Union within each cell - const cellResults = []; - let totalInCell = 0; - for (let gx = 0; gx < gridSize; gx++) { - for (let gy = 0; gy < gridSize; gy++) { - const cellPolys = grid[gx][gy]; - if (cellPolys.length > 0) { - totalInCell += cellPolys.length; - const cellUnion = unionTris(cellPolys, depth + 1); - cellResults.push(...cellUnion); - } - } - } - - // Union the cell results - polys = unionTris(cellResults, depth + 1); - - } else { - polys = POLY.union(polys, 0, true, { wasm: false }); - } - - return polys; -} - // Widget Grouping API const Group = Widget.Groups = { list() { From d957c4ce7fb9ac7c7c773d62569dca8cf414c308 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Tue, 23 Dec 2025 21:43:09 -0500 Subject: [PATCH 007/154] remove FloatPacker and cutPolys --- src/geo/paths.js | 57 +------------------------------------- src/kiri/mode/cam/slice.js | 7 ----- 2 files changed, 1 insertion(+), 63 deletions(-) diff --git a/src/geo/paths.js b/src/geo/paths.js index 139759f7..a6f3822f 100644 --- a/src/geo/paths.js +++ b/src/geo/paths.js @@ -691,65 +691,10 @@ export function arcToPath(start, end, arcdivs = 24, opts) { return arr } -/** - * Dynamically growing Float32Array with efficient memory management - * Used for accumulating vertex/normal data when final size is unknown - * Automatically expands when capacity reached, optimizes final output - * @class - */ -export class FloatPacker { - /** - * Create new float packer with initial size - * @param {number} size - Initial array size - * @param {number} [factor=1.2] - Growth factor when expanding (clamped to max 1.2) - */ - constructor(size, factor) { - this.size = size; - this.factor = Math.min(factor || 1.2, 1.1); - this.array = new Float32Array(size); - this.pos = 0; - } - - /** - * Push multiple float values onto array - * Automatically expands array if needed - * @param {...number} values - Float values to append - */ - push() { - const array = this.array; - const size = this.size; - const args = arguments.length; - if (this.pos + args >= size) { - let nusize = ((size * this.factor) | 0) + args; - let nuarray = new Float32Array(nusize); - nuarray.set(array); - this.array = nuarray; - this.size = nusize; - } - for (let i = 0; i < args; i++) { - array[this.pos++] = arguments[i]; - } - } - - /** - * Get final array trimmed to actual size - * Uses subarray (view) if >90% full, otherwise copies to save memory - * @returns {Float32Array} Array containing only pushed values - */ - finalize() { - if (this.pos / this.size >= 0.9) { - return this.array.subarray(0, this.pos); - } else { - return this.array.slice(0, this.pos); - } - } -} - export const paths = { tip2tipEmit, poly2polyEmit, shapeToPath, pointsToPath, - pathTo3D, - FloatPacker + pathTo3D } diff --git a/src/kiri/mode/cam/slice.js b/src/kiri/mode/cam/slice.js index 2e633be1..74675635 100644 --- a/src/kiri/mode/cam/slice.js +++ b/src/kiri/mode/cam/slice.js @@ -183,7 +183,6 @@ export async function cam_slice(settings, widget, onupdate, ondone) { color, computeShadows, contourPolys, - cutPolys, cutTabs, dark, isIndexed, @@ -784,12 +783,6 @@ function cutTabs(tabs, offset) { return out; } -function cutPolys(polys, offset) { - let noff = []; - offset.forEach(op => noff.appendAll( op.cut(POLY.union(polys, 0, true), true) )); - return POLY.reconnect(noff); -} - function contourPolys(widget, polys) { const raycaster = new THREE.Raycaster(); raycaster.ray.direction.set(0, 0, -1); // ray pointing down Z From 35b76802d183acb6c7fda177f1488b4ecce79a12 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Tue, 23 Dec 2025 22:21:29 -0500 Subject: [PATCH 008/154] threading now always on. alt code path remains for now --- src/kiri/core/init.js | 9 +++++---- src/kiri/core/settings.js | 13 +++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/kiri/core/init.js b/src/kiri/core/init.js index 5459b4e4..8d845f90 100644 --- a/src/kiri/core/init.js +++ b/src/kiri/core/init.js @@ -131,7 +131,7 @@ function setThreaded(bool) { // upon restore, seed presets api.event.emit('preset', api.conf.dbo()); -api.event.on("set.threaded", bool => setThreaded(bool)); +// api.event.on("set.threaded", bool => setThreaded(bool)); function booleanSave() { let control = settings().controller; @@ -161,7 +161,7 @@ function booleanSave() { control.showOrigin = ui.showOrigin.checked; control.showRulers = ui.showRulers.checked; control.spaceRandoX = ui.spaceRandoX.checked; - control.threaded = setThreaded(ui.threaded.checked); + // control.threaded = setThreaded(ui.threaded.checked); control.webGPU = ui.webGPU.checked; space.view.setZoom(control.reverseZoom, control.zoomSpeed); // platform.layout(); @@ -1044,7 +1044,7 @@ function init_one() { _____: newGroup(LANG.pt_menu, $('prefs-prt'), {inline}), detail: newSelect(LANG.pt_qual_s, {title: LANG.pt_qual_l, action: detailSave}, "detail"), 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}), + // 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}), @@ -1619,9 +1619,10 @@ function init_two() { ui.showRulers.checked = control.showRulers; ui.showSpeeds.checked = control.showSpeeds; ui.spaceRandoX.checked = control.spaceRandoX; - ui.threaded.checked = setThreaded(control.threaded); + // ui.threaded.checked = setThreaded(control.threaded); ui.webGPU.checked = control.webGPU; + setThreaded(true); lineTypeSave(); detailSave(); updateStats(); diff --git a/src/kiri/core/settings.js b/src/kiri/core/settings.js index c269a7b4..7630bad7 100644 --- a/src/kiri/core/settings.js +++ b/src/kiri/core/settings.js @@ -756,11 +756,11 @@ function settingsPrusaConvert(data) { }); } -function setThreaded(bool) { - bool = bool ? true : false; - settings.controller.threaded = bool; - api.event.emit("set.threaded", bool); -} +// function setThreaded(bool) { +// bool = bool ? true : false; +// settings.controller.threaded = bool; +// api.event.emit("set.threaded", bool); +// } function setEnableWASM(bool) { bool = bool ? true : false; @@ -786,7 +786,8 @@ const apiSet = { update_fields: updateFieldsFromSettings, update_fields_from_range: updateFieldsFromRange, set_enable_wasm: setEnableWASM, - set_threaded: setThreaded, + // set_threaded: setThreaded, + set_threaded() { console.trace('set_threaded deprecated' )}, restore: restoreSettings, export: settingsExport, import: settingsImport, From 209f5cae9c6bd2cb66cc9e3135e1a810cb2de751 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Wed, 24 Dec 2025 14:15:47 -0500 Subject: [PATCH 009/154] add quickjs eval engine for macros --- bin/webpack-quickjs-bundle.js | 28 ++++ bin/webpack-quickjs-esm.js | 50 ++++++ package.json | 4 +- src/kiri/core/print.js | 23 ++- src/kiri/mode/cam/prepare.js | 3 + src/kiri/mode/fdm/prepare.js | 3 + src/kiri/run/worker.js | 19 ++- src/moto/quickjs.js | 280 ++++++++++++++++++++++++++++++++++ web/kiri/index.html | 4 +- 9 files changed, 404 insertions(+), 10 deletions(-) create mode 100644 bin/webpack-quickjs-bundle.js create mode 100644 bin/webpack-quickjs-esm.js create mode 100644 src/moto/quickjs.js diff --git a/bin/webpack-quickjs-bundle.js b/bin/webpack-quickjs-bundle.js new file mode 100644 index 00000000..7ec6d58b --- /dev/null +++ b/bin/webpack-quickjs-bundle.js @@ -0,0 +1,28 @@ +import * as QuickJSModule from "../node_modules/quickjs-emscripten/dist/index.js"; + +// Re-export all the named exports +export const getQuickJS = QuickJSModule.getQuickJS; +export const getQuickJSSync = QuickJSModule.getQuickJSSync; +export const newQuickJSWASMModule = QuickJSModule.newQuickJSWASMModule; +export const newQuickJSAsyncWASMModule = QuickJSModule.newQuickJSAsyncWASMModule; +export const newAsyncRuntime = QuickJSModule.newAsyncRuntime; +export const newAsyncContext = QuickJSModule.newAsyncContext; +export const DEBUG_SYNC = QuickJSModule.DEBUG_SYNC; +export const DEBUG_ASYNC = QuickJSModule.DEBUG_ASYNC; +export const RELEASE_SYNC = QuickJSModule.RELEASE_SYNC; +export const RELEASE_ASYNC = QuickJSModule.RELEASE_ASYNC; +export const errors = QuickJSModule.errors; +export const memoizePromiseFactory = QuickJSModule.memoizePromiseFactory; +export const Lifetime = QuickJSModule.Lifetime; +export const Scope = QuickJSModule.Scope; +export const WeakLifetime = QuickJSModule.WeakLifetime; +export const StaticLifetime = QuickJSModule.StaticLifetime; +export const TestQuickJSWASMModule = QuickJSModule.TestQuickJSWASMModule; +export const isFail = QuickJSModule.isFail; +export const isSuccess = QuickJSModule.isSuccess; +export const assertSync = QuickJSModule.assertSync; +export const DeferredPromise = QuickJSModule.DeferredPromise; +export const shouldInterruptAfterDeadline = QuickJSModule.shouldInterruptAfterDeadline; + +// Default export +export default QuickJSModule; diff --git a/bin/webpack-quickjs-esm.js b/bin/webpack-quickjs-esm.js new file mode 100644 index 00000000..4012a5ad --- /dev/null +++ b/bin/webpack-quickjs-esm.js @@ -0,0 +1,50 @@ +const path = require('path'); +const webpack = require('webpack'); + +module.exports = { + mode: 'production', + entry: path.resolve(__dirname, './webpack-quickjs-bundle.js'), + output: { + path: path.resolve(__dirname, '../src/ext'), + filename: 'quickjs.js', + library: { + type: 'module' + }, + module: true + }, + experiments: { + outputModule: true + }, + resolve: { + extensions: ['.mjs', '.js'], + fallback: { + 'path': false, + 'fs': false, + 'crypto': false + } + }, + plugins: [ + new webpack.DefinePlugin({ + 'process.env.QTS_DEBUG': JSON.stringify(false), + 'process.env.NODE_ENV': JSON.stringify('production') + }) + ], + module: { + rules: [ + { + test: /\.m?js$/, + resolve: { + fullySpecified: false, + }, + }, + ], + }, + optimization: { + minimize: false + }, + performance: { + hints: false, + maxAssetSize: 2 * 1024 * 1024, + maxEntrypointSize: 2 * 1024 * 1024, + }, +}; diff --git a/package.json b/package.json index facdcf06..6dced782 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "mqtt": "^5.10.3", "npm": "^11.4.2", "prettier": "^3.5.3", + "quickjs-emscripten": "^0.20.0", "react-responsive-carousel": "^3.2.23", "serve-static": "^2.2.0", "three": "^0.182.0", @@ -120,7 +121,8 @@ "start-ddb": "npm run prebuild && electron . --devel --debugg", "start-dev": "npm run prebuild prod && electron . --devel", "start": "npm run prebuild && electron .", - "webpack-ext": "npm run webpack-three && npm run webpack-zip", + "webpack-ext": "npm run webpack-three && npm run webpack-zip && npm run webpack-qjs", + "webpack-qjs": "npx webpack --config bin/webpack-quickjs-esm.js", "webpack-src": "node bin/esbuild.config.mjs", "webpack-three": "npx webpack --config bin/webpack-three-esm.js", "webpack-zip": "npx webpack --config bin/webpack-jszip-esm.js" diff --git a/src/kiri/core/print.js b/src/kiri/core/print.js index 11419606..a090a0c6 100644 --- a/src/kiri/core/print.js +++ b/src/kiri/core/print.js @@ -2,6 +2,7 @@ import { arcToPath } from '../../geo/paths.js'; import { consts } from './consts.js'; +import { createVM } from '../../moto/quickjs.js'; import { newPoint } from '../../geo/point.js'; import { util } from '../../geo/base.js'; @@ -24,6 +25,7 @@ class Print { this.tools = {}; // set to 1 to enable flow rate analysis (console) this.debugE = settings ? (settings.controller.devel ? 1 : 0) : 0; + this._ready = this.createSafeEval(); } setType(type) { @@ -237,7 +239,26 @@ class Print { return output.last().point; } + async ready() { + await this._ready; + } + + async createSafeEval() { + this.safeEval = await createVM(); + this.safeEval.eval("function range(a,b) { return (a + (layer / layers) * (b-a)) }"); + } + + disposeSafeEval() { + if (this.safeEval) { + this.safeEval.dispose(); + } + } + constReplace(str, consts, start, pad, short) { + let safeEval = this.safeEval; + if (safeEval) { + safeEval.setContext(consts); + } function tryeval(str) { try { return eval(`{ ${str} }`) @@ -269,7 +290,7 @@ class Print { } eva.push(`function range(a,b) { return (a + (layer / layers) * (b-a)).round(4) }`); eva.push(`try {( ${tok} )} catch (e) {console.log(e);0}`); - let evl = tryeval(eva.join('')); + let evl = safeEval ? safeEval.eval(tok) : tryeval(eva.join('')); nutok = evl; if (pad === 666) { return evl; diff --git a/src/kiri/mode/cam/prepare.js b/src/kiri/mode/cam/prepare.js index d77245f4..7c63c418 100644 --- a/src/kiri/mode/cam/prepare.js +++ b/src/kiri/mode/cam/prepare.js @@ -30,6 +30,9 @@ export async function cam_prepare(widgets, settings, update) { const print = self.kiri_worker.current.print = newPrint(settings, active); const { origin } = settings; + // wait for safe eval setup + await print.ready(); + // cam-specific storage print.output = []; diff --git a/src/kiri/mode/fdm/prepare.js b/src/kiri/mode/fdm/prepare.js index 8fd835b3..4dfe09fa 100644 --- a/src/kiri/mode/fdm/prepare.js +++ b/src/kiri/mode/fdm/prepare.js @@ -47,6 +47,9 @@ export async function fdm_prepare(widgets, settings, update) { output = [], layerout = []; + // wait for safe eval setup + await print.ready(); + // compute bounds if missing if (!bounds) { bounds = new THREE.Box3(); diff --git a/src/kiri/run/worker.js b/src/kiri/run/worker.js index a63fbeeb..82f422fc 100644 --- a/src/kiri/run/worker.js +++ b/src/kiri/run/worker.js @@ -58,6 +58,13 @@ function debug() { console.log(...arguments); } +function setPrint(print) { + if (current.print && current.print !== print) { + current.print.disposeSafeEval(); + } + return current.print = print; +} + // catch clipper alerts and convert to console messages self.alert = function(o) { console.log(o); @@ -66,7 +73,7 @@ self.alert = function(o) { self.uuid = ((Math.random() * Date.now()) | 0).toString(36); /** - * @returns {RasterPath} + * @returns {RasterPath} instantiated class */ self.get_raster_gpu = async function({ mode, resolution, rotationStep }) { let gpu = new RasterPath({ @@ -314,7 +321,7 @@ const dispatch = { // purge all sync data clear(data, send) { // current.snap = null; - current.print = null; + setPrint(null); dispatch.group = wgroup = {}; dispatch.cache = worker.cache = wcache = {}; Widget.Groups.clear(); @@ -427,7 +434,7 @@ const dispatch = { let last = time(), now; - current.print = null; + setPrint(null); current.mode = settings.mode.toUpperCase(); widget.anno = data.anno || widget.anno; @@ -496,7 +503,7 @@ const dispatch = { send.data(emit, state.zeros); }).then(() => { const unitScale = settings.controller.units === 'in' ? (1 / 25.4) : 1; - const print = current.print || {}; + const print = setPrint(current.print || {}); const minSpeed = (print.minSpeed || 0) * unitScale; const maxSpeed = (print.maxSpeed || 0) * unitScale; @@ -571,7 +578,7 @@ const dispatch = { z: origin.z - (process.camOriginOffZ ?? 0) }; const device = settings.device; - const print = current.print = newPrint(settings, Object.values(wcache)); + const print = setPrint(newPrint(settings, Object.values(wcache))); const tools = device.extruders; const mode = settings.mode; const thin = settings.controller.lineType === 'line' || mode !== 'FDM'; @@ -601,7 +608,7 @@ const dispatch = { out.point = newPoint(x,y,z || 0); }); }); - const print = current.print = newPrint(null, Object.values(wcache)); + const print = setPrint(newPrint(null, Object.values(wcache))); render.path(parsed, progress => { send.data({ progress }); }, { thin: true }) diff --git a/src/moto/quickjs.js b/src/moto/quickjs.js new file mode 100644 index 00000000..002effd3 --- /dev/null +++ b/src/moto/quickjs.js @@ -0,0 +1,280 @@ +/** + * QuickJS VM Wrapper + * Provides a simplified interface for working with QuickJS contexts + */ + +import { getQuickJS } from '../ext/quickjs.js'; + +let quickJSInstance = null; + +/** + * Get or initialize the QuickJS singleton instance + */ +async function getQuickJSInstance() { + if (!quickJSInstance) { + quickJSInstance = await getQuickJS(); + } + return quickJSInstance; +} + +/** + * QuickJS VM wrapper with helper methods + * Automatically manages a QuickJS context lifecycle + */ +class QuickJSVM { + /** + * Create a new QuickJS VM context + * @param {Object} options - Optional VM configuration + */ + constructor(options = {}) { + this.vm = null; + this.disposed = false; + this.initPromise = this._init(options); + } + + /** + * Internal initialization (async) + */ + async _init(options) { + const QuickJS = await getQuickJSInstance(); + this.vm = QuickJS.newContext(options); + } + + /** + * Ensure VM is initialized before use + */ + async ready() { + await this.initPromise; + if (this.disposed) { + throw new Error('VM has been disposed'); + } + return this; + } + + /** + * Set multiple variables in the global context + * @param {Object} obj - Object with key/value pairs to set as globals + * @example + * vm.setContext({ + * x: 123, + * config: { width: 800, height: 600 }, + * items: [1, 2, 3] + * }); + */ + setContext(obj) { + for (const [key, value] of Object.entries(obj)) { + const handle = this.jsToVm(value); + this.vm.setProp(this.vm.global, key, handle); + handle.dispose(); + } + return this; + } + + /** + * Set a single global variable + * @param {string} name - Variable name + * @param {*} value - JavaScript value to set + * @example + * vm.set('x', 123); + * vm.set('config', { width: 800 }); + */ + set(name, value) { + const handle = this.jsToVm(value); + this.vm.setProp(this.vm.global, name, handle); + handle.dispose(); + return this; + } + + /** + * Get a global variable value + * @param {string} name - Variable name + * @returns {*} JavaScript value + * @example + * const x = vm.get('x'); // 123 + */ + get(name) { + const handle = this.vm.getProp(this.vm.global, name); + const value = this.vm.dump(handle); + handle.dispose(); + return value; + } + + /** + * Convert JavaScript value to QuickJS handle + * @param {*} value - JavaScript value + * @returns {QuickJSHandle} QuickJS handle (caller must dispose) + */ + jsToVm(value) { + // Handle primitives + if (value === null) { + return this.vm.null; + } + + if (value === undefined) { + return this.vm.undefined; + } + + if (typeof value === 'number') { + return this.vm.newNumber(value); + } + + if (typeof value === 'string') { + return this.vm.newString(value); + } + + if (typeof value === 'boolean') { + return value ? this.vm.true : this.vm.false; + } + + // Handle arrays + if (Array.isArray(value)) { + const arr = this.vm.newArray(); + for (let i = 0; i < value.length; i++) { + const itemHandle = this.jsToVm(value[i]); + this.vm.setProp(arr, i, itemHandle); + itemHandle.dispose(); + } + return arr; + } + + // Handle objects + if (typeof value === 'object') { + const obj = this.vm.newObject(); + for (const [k, v] of Object.entries(value)) { + const propHandle = this.jsToVm(v); + this.vm.setProp(obj, k, propHandle); + propHandle.dispose(); + } + return obj; + } + + // Fallback for unsupported types + console.warn(`Unsupported type for VM: ${typeof value}, setting to undefined`); + return this.vm.undefined; + } + + /** + * Evaluate JavaScript code in the VM + * @param {string} code - JavaScript code to evaluate + * @returns {*} Result value or throws error + * @example + * const result = vm.eval('1 + 2'); // 3 + */ + eval(code) { + const result = this.vm.evalCode(code); + + if (result.error) { + const error = this.vm.dump(result.error); + result.error.dispose(); + throw new Error(`VM Error: ${error}`); + } + + const value = this.vm.dump(result.value); + result.value.dispose(); + return value; + } + + /** + * Evaluate code and return raw result handle (caller must dispose) + * @param {string} code - JavaScript code to evaluate + * @returns {Object} { error?: handle, value?: handle } + */ + evalRaw(code) { + return this.vm.evalCode(code); + } + + /** + * Create a JavaScript function that can be called from VM code + * @param {string} name - Function name in VM + * @param {Function} fn - JavaScript function to wrap + * @example + * vm.setFunction('add', (a, b) => a + b); + * vm.eval('add(2, 3)'); // 5 + */ + setFunction(name, fn) { + const fnHandle = this.vm.newFunction(name, (...args) => { + // Convert VM args to JS + const jsArgs = args.map(arg => this.vm.dump(arg)); + + // Call JS function + const result = fn(...jsArgs); + + // Convert result back to VM + return this.jsToVm(result); + }); + + this.vm.setProp(this.vm.global, name, fnHandle); + fnHandle.dispose(); + return this; + } + + /** + * Create an object with JSON-like structure + * @param {Object} data - JavaScript object to convert + * @returns {QuickJSHandle} Handle to VM object (caller must dispose) + */ + newJSON(data) { + return this.vm.unwrapResult( + this.vm.evalCode(`(${JSON.stringify(data)})`) + ); + } + + /** + * Execute a function in the VM with arguments + * @param {string} funcName - Function name in VM + * @param {...*} args - Arguments to pass + * @returns {*} Result value + * @example + * vm.eval('function add(a, b) { return a + b; }'); + * vm.call('add', 2, 3); // 5 + */ + call(funcName, ...args) { + const argsStr = args.map(a => JSON.stringify(a)).join(', '); + return this.eval(`${funcName}(${argsStr})`); + } + + /** + * Check if VM has been disposed + */ + isDisposed() { + return this.disposed; + } + + /** + * Get direct access to underlying QuickJS context + * Use with caution - you're responsible for handle management + */ + getContext() { + return this.vm; + } + + /** + * Dispose the VM and free all resources + * Must be called when done to prevent memory leaks + */ + dispose() { + if (!this.disposed && this.vm) { + this.vm.dispose(); + this.disposed = true; + this.vm = null; + } + } +} + +/** + * Create and initialize a new QuickJS VM + * @param {Object} options - Optional VM configuration + * @returns {Promise} Initialized VM instance + * @example + * const vm = await createVM(); + * vm.set('x', 123); + * const result = vm.eval('x * 2'); // 246 + * vm.dispose(); + */ +async function createVM(options = {}) { + const vm = new QuickJSVM(options); + await vm.ready(); + return vm; +} + +export { QuickJSVM, createVM }; diff --git a/web/kiri/index.html b/web/kiri/index.html index b7003067..ae9e1a88 100644 --- a/web/kiri/index.html +++ b/web/kiri/index.html @@ -22,8 +22,8 @@ - + + From 8349fcb65ee7ef23ac91e889288eec9ac3e3019b Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Wed, 24 Dec 2025 15:55:44 -0500 Subject: [PATCH 010/154] remove unused imports. prevent multiple interval timers --- src/kiri/core/init.js | 24 +++++++++--------------- src/kiri/core/main.js | 5 ----- src/moto/quickjs.js | 2 ++ 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/src/kiri/core/init.js b/src/kiri/core/init.js index 8d845f90..ce3ea20f 100644 --- a/src/kiri/core/init.js +++ b/src/kiri/core/init.js @@ -38,8 +38,7 @@ let { CAM, SLA, FDM, LASER, DRAG, WJET, WEDM } = MODES, FDM_CAM = [ FDM, CAM ], proto = location.protocol, platformColor, - separator = true, - hideable = true, + statsTimer, inline = true, driven = true, trigger = true, @@ -183,9 +182,11 @@ function updateStats() { if (self.debug !== true) { return; } + console.log('updateStats()'); let { div, fps, rms, rnfo } = ui.stats; div.style.display = 'flex'; - setInterval(() => { + clearTimeout(statsTimer); + statsTimer = setInterval(() => { const nrms = space.view.getRMS().toFixed(1); const nfps = space.view.getFPS().toFixed(1); const rend = space.renderInfo(); @@ -1414,18 +1415,6 @@ function init_one() { // default show gcode pre ui.gcodePre.button.click(); - function mksvg(src) { - let svg = DOC.createElement('svg'); - svg.innerHTML = src; - return svg; - } - - function mklbl(src) { - let lbl = DOC.createElement('label'); - lbl.innerText = src; - return lbl; - } - api.platform.update_size(); function mouseOnHover(int, event, ints) { @@ -1444,6 +1433,11 @@ function init_one() { space.platform.onHover(enable ? platformOnHover : undefined); }); + // prevent safari from exiting full screen mode + DOC.onkeydown = function (evt) { + if (evt.code == 27) evt.preventDefault() + } + // block standard browser context menu DOC.oncontextmenu = (event) => { let et = event.target; diff --git a/src/kiri/core/main.js b/src/kiri/core/main.js index 279aed3b..8c01ec0e 100644 --- a/src/kiri/core/main.js +++ b/src/kiri/core/main.js @@ -3,7 +3,6 @@ import './frame.js'; import '../../ext/base64.js'; -import lang from './lang.js'; import STACKS from './stacks.js'; import { $ } from '../../moto/webui.js'; @@ -26,7 +25,6 @@ import { showTools } from '../mode/cam/tools.js'; let { parseOpt, o2js, js2o, ls2o } = utils, { COLOR, MODES, VIEWS } = consts, - LANG = lang.current, WIN = self.window, DOC = self.document, LOC = self.location, @@ -912,7 +910,4 @@ function downloadBlob(data, filename) { $('_dexport_').click(); } -// prevent safari from exiting full screen mode -DOC.onkeydown = function (evt) { if (evt.keyCode == 27) evt.preventDefault() } - export { FILES as catalog, LOCAL, SETUP, SECURE }; diff --git a/src/moto/quickjs.js b/src/moto/quickjs.js index 002effd3..f2db2b44 100644 --- a/src/moto/quickjs.js +++ b/src/moto/quickjs.js @@ -1,3 +1,5 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + /** * QuickJS VM Wrapper * Provides a simplified interface for working with QuickJS contexts From 667a7fad78a1c94d3a6a5749027f087d3802d6e6 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Wed, 24 Dec 2025 16:51:42 -0500 Subject: [PATCH 011/154] extract slider control from init into slider.js core singleton --- package.json | 2 +- src/kiri/core/init.js | 263 +++++++++++++-------------------- src/kiri/core/main.js | 51 +++---- src/kiri/core/settings.js | 6 +- src/kiri/core/slider.js | 302 ++++++++++++++++++++++++++++++++++++++ src/moto/license.js | 2 +- web/kiri/manifest.json | 2 +- 7 files changed, 440 insertions(+), 188 deletions(-) create mode 100644 src/kiri/core/slider.js diff --git a/package.json b/package.json index 6dced782..65e77421 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "grid-apps", - "version": "4.5.0", + "version": "4.6.0", "description": "grid.space 3d slicing & modeling tools", "author": "Stewart Allen ", "license": "MIT", diff --git a/src/kiri/core/init.js b/src/kiri/core/init.js index ce3ea20f..0ac072b5 100644 --- a/src/kiri/core/init.js +++ b/src/kiri/core/init.js @@ -4,6 +4,7 @@ import { $, $c } from '../../moto/webui.js'; import { api } from './api.js'; import { base } from '../../geo/base.js'; import { local as sdb } from '../../data/local.js'; +import { slider } from './slider.js'; import { space } from '../../moto/space.js'; import { LOCAL, SETUP } from './main.js'; import { VIEWS, MODES, SEED } from './consts.js'; @@ -20,6 +21,7 @@ import { menu as menuCAM } from '../mode/cam/init-menu.js'; import { menu as menuFDM } from '../mode/fdm/init-menu.js'; import { menu as menuLaser } from '../mode/laser/init-menu.js'; import { menu as menuSLA } from '../mode/sla/init-menu.js'; +import STACKS from './stacks.js'; import './tools.js'; let { CAM, SLA, FDM, LASER, DRAG, WJET, WEDM } = MODES, @@ -304,14 +306,20 @@ function keyDownHandler(evt) { break; case 38: // up arrow if (inputHasFocus()) return false; - if (evt.metaKey) return api.show.layer(api.var.layer_at+1); + if (evt.metaKey) { + const { hi } = slider.getRange(); + return api.show.layer(hi + 1); + } if (deg) selection.rotate(deg, 0, 0); if (move > 0) selection.move(0, move, 0); evt.preventDefault(); break; case 40: // down arrow if (inputHasFocus()) return false; - if (evt.metaKey) return api.show.layer(api.var.layer_at-1); + if (evt.metaKey) { + const { hi } = slider.getRange(); + return api.show.layer(hi - 1); + } if (deg) selection.rotate(-deg, 0, 0); if (move > 0) selection.move(0, -move, 0); evt.preventDefault(); @@ -364,16 +372,56 @@ function keyHandler(evt) { } switch (evt.charCode) { case cca('`'): api.show.slices(0); break; - case cca('0'): api.show.slices(api.var.layer_max); break; - case cca('1'): api.show.slices(api.var.layer_max/10); break; - case cca('2'): api.show.slices(api.var.layer_max*2/10); break; - case cca('3'): api.show.slices(api.var.layer_max*3/10); break; - case cca('4'): api.show.slices(api.var.layer_max*4/10); break; - case cca('5'): api.show.slices(api.var.layer_max*5/10); break; - case cca('6'): api.show.slices(api.var.layer_max*6/10); break; - case cca('7'): api.show.slices(api.var.layer_max*7/10); break; - case cca('8'): api.show.slices(api.var.layer_max*8/10); break; - case cca('9'): api.show.slices(api.var.layer_max*9/10); break; + case cca('0'): { + const { max } = slider.getRange(); + api.show.slices(max); + break; + } + case cca('1'): { + const { max } = slider.getRange(); + api.show.slices(max/10); + break; + } + case cca('2'): { + const { max } = slider.getRange(); + api.show.slices(max*2/10); + break; + } + case cca('3'): { + const { max } = slider.getRange(); + api.show.slices(max*3/10); + break; + } + case cca('4'): { + const { max } = slider.getRange(); + api.show.slices(max*4/10); + break; + } + case cca('5'): { + const { max } = slider.getRange(); + api.show.slices(max*5/10); + break; + } + case cca('6'): { + const { max } = slider.getRange(); + api.show.slices(max*6/10); + break; + } + case cca('7'): { + const { max } = slider.getRange(); + api.show.slices(max*7/10); + break; + } + case cca('8'): { + const { max } = slider.getRange(); + api.show.slices(max*8/10); + break; + } + case cca('9'): { + const { max } = slider.getRange(); + api.show.slices(max*9/10); + break; + } case cca('?'): api.help.show(); break; @@ -434,10 +482,11 @@ function keyHandler(evt) { if (api.view.get() === VIEWS.ARRANGE) { api.space.set_focus(selection.widgets()); } - if (api.var.layer_hi == api.var.layer_lo) { - api.var.layer_lo = 0; + const { lo, hi } = slider.getRange(); + if (hi === lo) { + slider.setRange(0, hi); } else { - api.var.layer_lo = api.var.layer_hi; + slider.setRange(hi, hi); } api.show.slices(); break; @@ -804,25 +853,28 @@ function init_one() { space.sky.setColor(controller.dark ? 0 : 0xffffff); space.setAntiAlias(controller.antiAlias); space.init(container, function (delta) { - let vars = api.var; - if (vars.layer_max === 0 || !delta) return; + const { lo, hi, max } = slider.getRange(); + if (max === 0 || !delta) return; if (controller.reverseZoom) delta = -delta; - let same = vars.layer_hi === vars.layer_lo; - let track = vars.layer_lo > 0; + let same = hi === lo; + let track = lo > 0; + let newHi = hi; + let newLo = lo; if (delta > 0) { - vars.layer_hi = Math.max(same ? 0 : vars.layer_lo, vars.layer_hi - 1); + newHi = Math.max(same ? 0 : lo, hi - 1); if (track) { - vars.layer_lo = Math.max(0, vars.layer_lo - 1); + newLo = Math.max(0, lo - 1); } } else if (delta < 0) { - vars.layer_hi = Math.min(vars.layer_max, vars.layer_hi + 1); + newHi = Math.min(max, hi + 1); if (track) { - vars.layer_lo = Math.min(vars.layer_hi, vars.layer_lo + 1); + newLo = Math.min(newHi, lo + 1); } } if (same) { - vars.layer_lo = vars.layer_hi; + newLo = newHi; } + slider.setRange(newLo, newHi); view.update_slider(); show.slices(); }, controller.ortho); @@ -1086,148 +1138,45 @@ function init_one() { }; // slider setup - const mobile = space.info.mob; - const slbar = mobile ? 80 : 30; - const slbar2 = slbar * 2; - const slider = ui.sliderRange; - const drag = { }; + slider.init({ + ui: { + layers: ui.layers, + slider: ui.slider, + sliderRange: ui.sliderRange, + sliderHold: ui.sliderHold, + sliderMid: ui.sliderMid, + sliderLo: ui.sliderLo, + sliderHi: ui.sliderHi, + sliderMin: ui.sliderMin, + sliderMax: ui.sliderMax, + sliderZero: $('slider-zero'), + }, + tracker: tracker, + mobile: space.info.mob, + onLayerChange: (hi, lo) => { + api.show.layer(hi, lo); + }, + onStackUpdate: (lo, hi) => { + STACKS.setRange(lo, hi); + }, + onSceneUpdate: () => { + space.scene.active(); + } + }); - if (mobile) { - ui.slider.classList.add('slider-mobile'); - ui.sliderLo.classList.add('slider-mobile'); - ui.sliderHi.classList.add('slider-mobile'); - // add css style for mobile devices + // expose slider to API + api.slider = slider; + + // add mobile class to body if needed + if (space.info.mob) { DOC.body.classList.add('mobile'); } - function pxToInt(txt) { - return txt ? parseInt(txt.substring(0,txt.length-2)) : 0; - } - - function sliderUpdate() { - let start = drag.low / drag.maxval; - let end = (drag.low + drag.mid - slbar) / drag.maxval; - api.event.emit('slider.pos', { start, end }); - api.var.layer_lo = Math.round(start * api.var.layer_max); - api.var.layer_hi = Math.round(end * api.var.layer_max); - api.show.layer(); - space.scene.active(); - } - - function dragit(el, delta) { - el.ontouchstart = el.onmousedown = (ev) => { - // el.classList.add('sli-drag-el'); - tracker.style.display = 'block'; - ev.stopPropagation(); - let obj = (ev.touches ? ev.touches[0] : ev); - drag.width = slider.clientWidth; - drag.maxval = drag.width - slbar2; - drag.start = obj.screenX; - drag.loat = drag.low = pxToInt(ui.sliderHold.style.marginLeft); - drag.mdat = drag.mid = ui.sliderMid.clientWidth; - drag.hiat = pxToInt(ui.sliderHold.style.marginRight); - drag.mdmax = drag.width - slbar - drag.loat; - drag.himax = drag.width - slbar - drag.mdat; - let cancel_drag = tracker.ontouchend = tracker.onmouseup = (ev) => { - // el.classList.remove('sli-drag-el'); - if (ev) { - ev.stopPropagation(); - ev.preventDefault(); - } - slider.onmousemove = undefined; - tracker.style.display = 'none'; - }; - el.ontouchend = cancel_drag; - el.ontouchmove = tracker.ontouchmove = tracker.onmousemove = (ev) => { - ev.stopPropagation(); - ev.preventDefault(); - if (ev.buttons === 0) { - return cancel_drag(); - } - if (delta) { - let obj = (ev.touches ? ev.touches[0] : ev); - delta(obj.screenX - drag.start); - } - }; - }; - } - - dragit(ui.sliderLo, (delta) => { - let midval = drag.mdat - delta; - let lowval = drag.loat + delta; - if (midval < slbar || lowval < 0) { - return; - } - ui.sliderHold.style.marginLeft = `${lowval}px`; - ui.sliderMid.style.width = `${midval}px`; - drag.low = lowval; - drag.mid = midval; - sliderUpdate(); - }); - dragit(ui.sliderMid, (delta) => { - let loval = drag.loat + delta; - let hival = drag.hiat - delta; - if (loval < 0 || hival < 0) return; - ui.sliderHold.style.marginLeft = `${loval}px`; - ui.sliderHold.style.marginRight = `${hival}px`; - drag.low = loval; - sliderUpdate(); - }); - dragit(ui.sliderHi, (delta) => { - let midval = drag.mdat + delta; - let hival = drag.hiat - delta; - if (midval < slbar || midval > drag.mdmax || hival < 0) return; - ui.sliderMid.style.width = `${midval}px`; - ui.sliderHold.style.marginRight = `${hival}px`; - drag.mid = midval; - sliderUpdate(); - }); - ui.load.onchange = function(event) { api.platform.load_files(event.target.files); ui.load.value = ''; // reset so you can re-import the same filee }; - ui.sliderMin.onclick = () => { - api.show.layer(0,0); - } - - ui.sliderMax.onclick = () => { - api.show.layer(api.var.layer_max,0); - } - - ui.slider.onmouseover = (ev) => { - api.event.emit('slider.label'); - }; - - ui.slider.onmouseleave = (ev) => { - if (!ev.buttons) api.event.emit('slider.unlabel'); - }; - - api.event.on('slider.unlabel', (values) => { - }); - - api.event.on('slider.label', (values) => { - let digits = api.var.layer_max.toString().length; - $('slider-zero').style.width = `${digits}em`; - $('slider-max').style.width = `${digits}em`; - $('slider-zero').innerText = api.var.layer_lo; - $('slider-max').innerText = api.var.layer_hi; - }); - - api.event.on('slider.set', (values) => { - let width = slider.clientWidth; - let maxval = width - slbar2; - let start = Math.max(0, Math.min(1, values.start)); - let end = Math.max(start, Math.min(1, values.end)); - let lowval = start * maxval; - let midval = ((end - start) * maxval) + slbar; - let hival = maxval - end * maxval; - ui.sliderHold.style.marginLeft = `${lowval}px`; - ui.sliderMid.style.width = `${midval}px`; - ui.sliderHold.style.marginRight = `${hival}px`; - }); - // store layer preferences api.event.on('stack.show', label => { let mode = api.mode.get(); diff --git a/src/kiri/core/main.js b/src/kiri/core/main.js index 8c01ec0e..d222094e 100644 --- a/src/kiri/core/main.js +++ b/src/kiri/core/main.js @@ -314,20 +314,21 @@ function updateSpeeds(maxSpeed, minSpeed) { } function updateSlider() { - api.event.emit("slider.set", { - start: (api.var.layer_lo / api.var.layer_max), - end: (api.var.layer_hi / api.var.layer_max) - }); + const { lo, hi, max } = api.slider.getRange(); + if (max > 0) { + api.slider.updatePosition(lo / max, hi / max); + } api.conf.update_fields_from_range(); } function setVisibleLayer(h, l) { - h = h >= 0 ? h : api.var.layer_hi; - l = l >= 0 ? l : api.var.layer_lo; - api.var.layer_hi = bound(h, 0, api.var.layer_max); - api.var.layer_lo = bound(l, 0, h); - api.event.emit("slider.label"); - updateSlider(); + const { lo, hi, max } = api.slider.getRange(); + h = h >= 0 ? h : hi; + l = l >= 0 ? l : lo; + const newHi = bound(h, 0, max); + const newLo = bound(l, 0, newHi); + api.slider.setRange(newLo, newHi); + api.slider.showLabels(); showSlices(); } @@ -349,11 +350,11 @@ function setEdges(bool) { function updateSliderMax(set) { let max = STACKS.getRange().tallest - 1; - api.var.layer_max = api.ui.sliderMax.innerText = max; - if (set || max < api.var.layer_hi) { - api.var.layer_hi = api.var.layer_max; - api.event.emit("slider.label"); - updateSlider(); + api.slider.setMax(max); + const { hi } = api.slider.getRange(); + if (set || max < hi) { + api.slider.setRange(max, max); + api.slider.showLabels(); } } @@ -387,31 +388,31 @@ function showSlices(layer) { showSlider(); + const { lo, hi, max } = api.slider.getRange(); + if (typeof(layer) === 'string' || typeof(layer) === 'number') { layer = parseInt(layer); } else { - layer = api.var.layer_hi; + layer = hi; } - layer = bound(layer, 0, api.var.layer_max); - if (layer < api.var.layer_lo) api.var.layer_lo = layer; - api.var.layer_hi = layer; - api.event.emit("slider.label"); + layer = bound(layer, 0, max); + const newLo = layer < lo ? layer : lo; + api.slider.setRange(newLo, layer); + api.slider.showLabels(); updateSlider(); - STACKS.setRange(api.var.layer_lo, api.var.layer_hi); + // STACKS.setRange is called by slider callback SPACE.update(); } function showSlider() { - api.ui.layers.style.display = 'flex'; - api.ui.slider.style.display = 'flex'; + api.slider.show(); } function hideSlider() { - api.ui.layers.style.display = 'none'; - api.ui.slider.style.display = 'none'; + api.slider.hide(); api.ui.speeds.style.display = 'none'; } diff --git a/src/kiri/core/settings.js b/src/kiri/core/settings.js index 7630bad7..51bb27e2 100644 --- a/src/kiri/core/settings.js +++ b/src/kiri/core/settings.js @@ -115,7 +115,7 @@ function updateSettings(opt = {}) { if (settings.mode === 'FDM' && api.view.is_slice()) { let changes = {}; let values = process; - let { layer_lo, layer_hi, layer_max } = api.var; + const { lo: layer_lo, hi: layer_hi, max: layer_max } = api.slider.getRange(); let range = { lo: layer_lo, hi: layer_hi }; let add = false; if (layer_lo > 0 || layer_hi < layer_max) { @@ -450,8 +450,8 @@ function updateFieldsFromRange() { let match = 0; let values = {}; let restores = Object.clone(overrides); - let { layer_lo, layer_hi } = api.var; - for (let range of getOverlappingRanges(api.var.layer_lo, api.var.layer_hi)) { + const { lo: layer_lo, hi: layer_hi } = api.slider.getRange(); + for (let range of getOverlappingRanges(layer_lo, layer_hi)) { for (let key of Object.keys(range.fields)) { values[key] = range.fields[key]; overrides[key] = settings.process[key]; diff --git a/src/kiri/core/slider.js b/src/kiri/core/slider.js new file mode 100644 index 00000000..d7db613f --- /dev/null +++ b/src/kiri/core/slider.js @@ -0,0 +1,302 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +/** + * Layer Slider Controller (Singleton) + * Manages layer range selection and visualization for slice/preview views + */ + +class SliderControl { + #layerLo = 0; + #layerHi = 0; + #layerMax = 0; + #ui; + #tracker; + #mobile; + #sliderBar; + #sliderBar2; + #drag = {}; + + // Callbacks to replace event emissions + #onLayerChange; + #onStackUpdate; + #onSceneUpdate; + + #initialized = false; + + /** + * Initialize the slider with configuration + * @param {Object} config + * @param {Object} config.ui - UI element references + * @param {HTMLElement} config.tracker - Drag tracker element + * @param {boolean} config.mobile - Mobile device flag + * @param {Function} config.onLayerChange - Called when layer range changes + * @param {Function} config.onStackUpdate - Called to update stack visibility + * @param {Function} config.onSceneUpdate - Called to refresh scene + */ + init({ ui, tracker, mobile, onLayerChange, onStackUpdate, onSceneUpdate }) { + if (this.#initialized) { + throw new Error('Slider already initialized'); + } + + this.#ui = ui; + this.#tracker = tracker; + this.#mobile = mobile; + this.#sliderBar = mobile ? 80 : 30; + this.#sliderBar2 = this.#sliderBar * 2; + this.#onLayerChange = onLayerChange; + this.#onStackUpdate = onStackUpdate; + this.#onSceneUpdate = onSceneUpdate; + + this.#setupUI(); + this.#bindEvents(); + this.#initialized = true; + } + + /** + * Get current layer range + * @returns {{lo: number, hi: number, max: number}} + */ + getRange() { + return { + lo: this.#layerLo, + hi: this.#layerHi, + max: this.#layerMax + }; + } + + /** + * Set layer range (bounds are enforced) + * @param {number} lo - Lower layer + * @param {number} hi - Upper layer + */ + setRange(lo, hi) { + this.#layerLo = Math.max(0, Math.min(hi, lo)); + this.#layerHi = Math.max(this.#layerLo, Math.min(this.#layerMax, hi)); + this.#updateVisuals(); + this.showLabels(); + this.#notifyChange(); + } + + /** + * Set maximum layer count + * @param {number} max - Maximum layer value + */ + setMax(max) { + this.#layerMax = max; + this.#ui.sliderMax.innerText = max; + if (max < this.#layerHi) { + this.#layerHi = max; + this.showLabels(); + this.#updateVisuals(); + } + } + + /** + * Update slider visual position from normalized values + * @param {number} startPercent - Start position (0-1) + * @param {number} endPercent - End position (0-1) + */ + updatePosition(startPercent, endPercent) { + const width = this.#ui.sliderRange.clientWidth; + const maxval = width - this.#sliderBar2; + const start = Math.max(0, Math.min(1, startPercent)); + const end = Math.max(start, Math.min(1, endPercent)); + + const lowval = start * maxval; + const midval = ((end - start) * maxval) + this.#sliderBar; + const hival = maxval - end * maxval; + + this.#ui.sliderHold.style.marginLeft = `${lowval}px`; + this.#ui.sliderMid.style.width = `${midval}px`; + this.#ui.sliderHold.style.marginRight = `${hival}px`; + } + + /** + * Show layer number labels + */ + showLabels() { + const digits = this.#layerMax.toString().length; + this.#ui.sliderZero.style.width = `${digits}em`; + this.#ui.sliderMax.style.width = `${digits}em`; + this.#ui.sliderZero.innerText = this.#layerLo; + this.#ui.sliderMax.innerText = this.#layerHi; + } + + /** + * Show slider UI + */ + show() { + this.#ui.layers.style.display = 'flex'; + this.#ui.slider.style.display = 'flex'; + } + + /** + * Hide slider UI + */ + hide() { + this.#ui.layers.style.display = 'none'; + this.#ui.slider.style.display = 'none'; + } + + #setupUI() { + if (this.#mobile) { + this.#ui.slider.classList.add('slider-mobile'); + this.#ui.sliderLo.classList.add('slider-mobile'); + this.#ui.sliderHi.classList.add('slider-mobile'); + } + } + + #bindEvents() { + // Min/Max buttons + this.#ui.sliderMin.onclick = () => { + this.#layerLo = 0; + this.#layerHi = 0; + this.showLabels(); + this.#updateVisuals(); + this.#notifyChange(); + }; + + this.#ui.sliderMax.onclick = () => { + this.#layerLo = this.#layerMax; + this.#layerHi = this.#layerMax; + this.showLabels(); + this.#updateVisuals(); + this.#notifyChange(); + }; + + // Mouse hover for labels + this.#ui.slider.onmouseover = () => { + this.showLabels(); + }; + + this.#ui.slider.onmouseleave = (ev) => { + if (!ev.buttons) { + this.hideLabels(); + } + }; + + // Drag handlers for the three slider sections + this.#bindDrag(this.#ui.sliderLo, this.#handleLoDrag.bind(this)); + this.#bindDrag(this.#ui.sliderMid, this.#handleMidDrag.bind(this)); + this.#bindDrag(this.#ui.sliderHi, this.#handleHiDrag.bind(this)); + } + + #bindDrag(el, deltaHandler) { + const slider = this.#ui.sliderRange; + + el.ontouchstart = el.onmousedown = (ev) => { + this.#tracker.style.display = 'block'; + ev.stopPropagation(); + + let obj = (ev.touches ? ev.touches[0] : ev); + this.#drag.width = slider.clientWidth; + this.#drag.maxval = this.#drag.width - this.#sliderBar2; + this.#drag.start = obj.screenX; + this.#drag.loat = this.#drag.low = this.#pxToInt(this.#ui.sliderHold.style.marginLeft); + this.#drag.mdat = this.#drag.mid = this.#ui.sliderMid.clientWidth; + this.#drag.hiat = this.#pxToInt(this.#ui.sliderHold.style.marginRight); + this.#drag.mdmax = this.#drag.width - this.#sliderBar - this.#drag.loat; + this.#drag.himax = this.#drag.width - this.#sliderBar - this.#drag.mdat; + + const cancelDrag = this.#tracker.ontouchend = this.#tracker.onmouseup = (ev) => { + if (ev) { + ev.stopPropagation(); + ev.preventDefault(); + } + slider.onmousemove = undefined; + this.#tracker.style.display = 'none'; + }; + + el.ontouchend = cancelDrag; + el.ontouchmove = this.#tracker.ontouchmove = this.#tracker.onmousemove = (ev) => { + ev.stopPropagation(); + ev.preventDefault(); + if (ev.buttons === 0) { + return cancelDrag(); + } + if (deltaHandler) { + let obj = (ev.touches ? ev.touches[0] : ev); + deltaHandler(obj.screenX - this.#drag.start); + } + }; + }; + } + + #handleLoDrag(delta) { + let midval = this.#drag.mdat - delta; + let lowval = this.#drag.loat + delta; + + if (midval < this.#sliderBar || lowval < 0) { + return; + } + + this.#ui.sliderHold.style.marginLeft = `${lowval}px`; + this.#ui.sliderMid.style.width = `${midval}px`; + this.#drag.low = lowval; + this.#drag.mid = midval; + this.#updateFromDrag(); + } + + #handleMidDrag(delta) { + let loval = this.#drag.loat + delta; + let hival = this.#drag.hiat - delta; + + if (loval < 0 || hival < 0) { + return; + } + + this.#ui.sliderHold.style.marginLeft = `${loval}px`; + this.#ui.sliderHold.style.marginRight = `${hival}px`; + this.#drag.low = loval; + this.#updateFromDrag(); + } + + #handleHiDrag(delta) { + let midval = this.#drag.mdat + delta; + let hival = this.#drag.hiat - delta; + + if (midval < this.#sliderBar || midval > this.#drag.mdmax || hival < 0) { + return; + } + + this.#ui.sliderMid.style.width = `${midval}px`; + this.#ui.sliderHold.style.marginRight = `${hival}px`; + this.#drag.mid = midval; + this.#updateFromDrag(); + } + + #updateFromDrag() { + const start = this.#drag.low / this.#drag.maxval; + const end = (this.#drag.low + this.#drag.mid - this.#sliderBar) / this.#drag.maxval; + + this.#layerLo = Math.round(start * this.#layerMax); + this.#layerHi = Math.round(end * this.#layerMax); + + this.#notifyChange(); + } + + #updateVisuals() { + if (this.#layerMax === 0) return; + const start = this.#layerLo / this.#layerMax; + const end = this.#layerHi / this.#layerMax; + this.updatePosition(start, end); + } + + #notifyChange() { + // Notify callbacks of layer change + this.#onLayerChange?.(this.#layerHi, this.#layerLo); + this.#onStackUpdate?.(this.#layerLo, this.#layerHi); + this.#onSceneUpdate?.(); + } + + #pxToInt(txt) { + return txt ? parseInt(txt.substring(0, txt.length - 2)) : 0; + } + + hideLabels() { + // Labels hide themselves on mouse leave, but can be called explicitly + } +} + +// Export singleton instance +export const slider = new SliderControl(); diff --git a/src/moto/license.js b/src/moto/license.js index 33e5ebca..83c6af45 100644 --- a/src/moto/license.js +++ b/src/moto/license.js @@ -3,7 +3,7 @@ const terms = { COPYRIGHT: "Copyright (C) Stewart Allen - All Rights Reserved", LICENSE: "See the license.md file included with the source distribution", - VERSION: "4.5.0" + VERSION: "4.6.0" }; export const beta = 0; diff --git a/web/kiri/manifest.json b/web/kiri/manifest.json index 09fe05cc..b7ba182c 100644 --- a/web/kiri/manifest.json +++ b/web/kiri/manifest.json @@ -1,5 +1,5 @@ { - "name": "Kiri:Moto 4.5.0", + "name": "Kiri:Moto 4.6.0", "short_name": "Kiri:Moto", "description": "Slicer for 3D printers, CNC mills, laser cutters and more", "start_url": "/boot/index.html", From 41edcd408b70a2bf970d9b3d4028649ea3fbc180 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Wed, 24 Dec 2025 17:53:09 -0500 Subject: [PATCH 012/154] extract keyboard handling from init --- .gitignore | 5 +- src/kiri/core/init.js | 322 +++--------------------------- src/kiri/core/keyboard.js | 398 ++++++++++++++++++++++++++++++++++++++ src/kiri/core/main.js | 2 +- src/kiri/core/slider.js | 7 +- 5 files changed, 433 insertions(+), 301 deletions(-) create mode 100644 src/kiri/core/keyboard.js diff --git a/.gitignore b/.gitignore index 7abc853e..de5b572a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,12 +7,13 @@ app.json build data dist -jszip-esm.js logs mod node_modules pack package-lock.json -three.js +src/ext/jszip-esm.js +src/ext/quickjs.js +src/ext/three.js tmp web/boot/bundle* diff --git a/src/kiri/core/init.js b/src/kiri/core/init.js index 0ac072b5..ef338077 100644 --- a/src/kiri/core/init.js +++ b/src/kiri/core/init.js @@ -5,6 +5,7 @@ import { api } from './api.js'; import { base } from '../../geo/base.js'; import { local as sdb } from '../../data/local.js'; import { slider } from './slider.js'; +import { keyboard } from './keyboard.js'; import { space } from '../../moto/space.js'; import { LOCAL, SETUP } from './main.js'; import { VIEWS, MODES, SEED } from './consts.js'; @@ -237,296 +238,6 @@ function onButtonClick(ev) { api.event.on('click.button', onButtonClick); -function inputHasFocus() { - let active = DOC.activeElement; - return active && (active.nodeName === "INPUT" || active.nodeName === "TEXTAREA"); -} - -function cca(c) { - return c.charCodeAt(0); -} - -function keyUpHandler(evt) { - if (api.feature.on_key) { - if (api.feature.on_key({up:evt})) return; - } - for (let handler of api.feature.on_key2) { - if (handler({up:evt})) return; - } - switch (evt.keyCode) { - // escape - case 27: - // blur text input focus - DOC.activeElement.blur(); - // dismiss modals - api.modal.hide(); - // deselect widgets - platform.deselect(); - // hide all dialogs - api.dialog.hide(); - // cancel slicing - api.function.cancel(); - // and send an event (used by FDM client) - api.event.emit("key.esc"); - break; - } - return false; -} - -function keyDownHandler(evt) { - if (api.modal.visible()) { - return false; - } - if (api.feature.on_key) { - if (api.feature.on_key({down:evt})) return; - } - for (let handler of api.feature.on_key2) { - if (handler({down:evt})) return; - } - let move = evt.altKey ? 5 : 0, - deg = move ? 0 : -Math.PI / (evt.shiftKey ? 36 : 2); - switch (evt.keyCode) { - case 8: // apple: delete/backspace - case 46: // others: delete - if (inputHasFocus()) return false; - platform.delete(selection.meshes()); - evt.preventDefault(); - break; - case 37: // left arrow - if (inputHasFocus()) return false; - if (deg) selection.rotate(0, 0, -deg); - if (move > 0) selection.move(-move, 0, 0); - evt.preventDefault(); - break; - case 39: // right arrow - if (inputHasFocus()) return false; - if (deg) selection.rotate(0, 0, deg); - if (move > 0) selection.move(move, 0, 0); - evt.preventDefault(); - break; - case 38: // up arrow - if (inputHasFocus()) return false; - if (evt.metaKey) { - const { hi } = slider.getRange(); - return api.show.layer(hi + 1); - } - if (deg) selection.rotate(deg, 0, 0); - if (move > 0) selection.move(0, move, 0); - evt.preventDefault(); - break; - case 40: // down arrow - if (inputHasFocus()) return false; - if (evt.metaKey) { - const { hi } = slider.getRange(); - return api.show.layer(hi - 1); - } - if (deg) selection.rotate(-deg, 0, 0); - if (move > 0) selection.move(0, -move, 0); - evt.preventDefault(); - break; - case 65: // 'a' for select all - if (evt.metaKey || evt.ctrlKey) { - if (inputHasFocus()) return false; - evt.preventDefault(); - platform.deselect(); - platform.select_all(); - } - break; - case 83: // 's' for save workspace - if (evt.ctrlKey) { - evt.preventDefault(); - api.conf.save(); - console.log("settings saved"); - } else - if (evt.metaKey) { - evt.preventDefault(); - api.space.save(); - set_ctrl.sync.put(); - } - break; - case 76: // 'l' for restore workspace - if (evt.metaKey) { - evt.preventDefault(); - api.space.restore(); - } - break; - } -} - -function keyHandler(evt) { - let handled = true; - if (api.modal.visible() || inputHasFocus()) { - return false; - } - if (api.feature.on_key) { - if (api.feature.on_key({key:evt})) return; - } - for (let handler of api.feature.on_key2) { - if (handler({key:evt})) return; - } - if (evt.ctrlKey) { - switch (evt.key) { - case 'g': return api.group.merge(); - case 'u': return api.group.split(); - } - } - switch (evt.charCode) { - case cca('`'): api.show.slices(0); break; - case cca('0'): { - const { max } = slider.getRange(); - api.show.slices(max); - break; - } - case cca('1'): { - const { max } = slider.getRange(); - api.show.slices(max/10); - break; - } - case cca('2'): { - const { max } = slider.getRange(); - api.show.slices(max*2/10); - break; - } - case cca('3'): { - const { max } = slider.getRange(); - api.show.slices(max*3/10); - break; - } - case cca('4'): { - const { max } = slider.getRange(); - api.show.slices(max*4/10); - break; - } - case cca('5'): { - const { max } = slider.getRange(); - api.show.slices(max*5/10); - break; - } - case cca('6'): { - const { max } = slider.getRange(); - api.show.slices(max*6/10); - break; - } - case cca('7'): { - const { max } = slider.getRange(); - api.show.slices(max*7/10); - break; - } - case cca('8'): { - const { max } = slider.getRange(); - api.show.slices(max*8/10); - break; - } - case cca('9'): { - const { max } = slider.getRange(); - api.show.slices(max*9/10); - break; - } - case cca('?'): - api.help.show(); - break; - case cca('Z'): // reset stored state - uc.confirm('clear all settings and preferences?').then(yes => { - if (yes) { - sdb.clear(); - WIN.location.reload(); - } - }); - break; - case cca('C'): // refresh catalog - catalog.refresh(); - break; - case cca('i'): // file import - api.event.import(); - break; - case cca('S'): // slice - case cca('s'): // slice - api.function.slice(); - break; - case cca('P'): // prepare - case cca('p'): // prepare - if (api.mode.get() !== 'SLA') { - // hidden in SLA mode - api.function.prepare(); - } - break; - case cca('X'): // export - case cca('x'): // export - api.function.export(); - break; - case cca('g'): // CAM animate - api.function.animate(); - break; - case cca('O'): // manual rotation - rotateInputSelection(); - break; - case cca('r'): // recent files - api.modal.show('files'); - break; - case cca('q'): // preferences - api.modal.show('prefs'); - break; - case cca('l'): // device - settingsLoad(); - break; - case cca('e'): // device - api.show.devices(); - break; - case cca('o'): // tools - api.show.tools(); - break; - case cca('c'): // local devices - api.show.local(); - break; - case cca('v'): // toggle single slice view mode - if (api.view.get() === VIEWS.ARRANGE) { - api.space.set_focus(selection.widgets()); - } - const { lo, hi } = slider.getRange(); - if (hi === lo) { - slider.setRange(0, hi); - } else { - slider.setRange(hi, hi); - } - api.show.slices(); - break; - case cca('d'): // duplicate object - duplicateSelection(); - break; - case cca('m'): // mirror object - mirrorSelection(); - break; - case cca('a'): - if (api.view.get() === VIEWS.ARRANGE) { - // auto arrange items on platform - platform.layout(); - if (!api.conf.get().controller.spaceRandoX) { - api.space.set_focus(selection.widgets()); - } - } else { - // go to arrange view - api.view.set(VIEWS.ARRANGE); - } - break; - default: - api.event.emit('keypress', evt); - handled = false; - break; - } - if (handled) { - evt.preventDefault(); - evt.stopPropagation(); - } - return false; -} - -function duplicateSelection() { - selection.duplicate(); -} - -function mirrorSelection() { - selection.mirror(); -} function keys(o) { let key, list = []; @@ -1154,7 +865,8 @@ function init_one() { tracker: tracker, mobile: space.info.mob, onLayerChange: (hi, lo) => { - api.show.layer(hi, lo); + // Visualization is handled by the code that calls setRange() + // Don't call api.show.layer here as it creates circular dependency }, onStackUpdate: (lo, hi) => { STACKS.setRange(lo, hi); @@ -1167,6 +879,27 @@ function init_one() { // expose slider to API api.slider = slider; + // initialize keyboard controller + keyboard.init({ + api, + platform, + selection, + slider, + space, + catalog, + sdb, + uc, + setCtrl: set_ctrl, + VIEWS, + DOC, + WIN, + rotateInputSelection, + settingsLoad + }); + + // expose keyboard to API + api.keyboard = keyboard; + // add mobile class to body if needed if (space.info.mob) { DOC.body.classList.add('mobile'); @@ -1225,9 +958,6 @@ function init_one() { }; space.event.addHandlers(self, [ - 'keyup', keyUpHandler, - 'keydown', keyDownHandler, - 'keypress', keyHandler, 'dragover', dragOverHandler, 'dragleave', dragLeave, 'drop', dropHandler @@ -1704,8 +1434,8 @@ function init_two() { $('mesh-export-obj').onclick = () => { objectsExport('obj') }; $('mesh-merge').onclick = selection.merge; $('mesh-split').onclick = selection.isolateBodies; - $('context-duplicate').onclick = duplicateSelection; - $('context-mirror').onclick = mirrorSelection; + $('context-duplicate').onclick = selection.duplicate; + $('context-mirror').onclick = selection.mirror; $('context-layflat').onclick = () => { api.event.emit("tool.mesh.lay-flat") }; $('context-lefty').onclick = () => { api.event.emit("tool.mesh.lefty") }; $('context-setfocus').onclick = () => { diff --git a/src/kiri/core/keyboard.js b/src/kiri/core/keyboard.js new file mode 100644 index 00000000..87d54a9a --- /dev/null +++ b/src/kiri/core/keyboard.js @@ -0,0 +1,398 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +/** + * Keyboard Controller (Singleton) + * Manages keyboard event handling and shortcuts + */ + +class KeyboardControl { + #api; + #platform; + #selection; + #slider; + #space; + #catalog; + #sdb; + #uc; + #setCtrl; + #VIEWS; + #DOC; + #WIN; + #rotateInputSelection; + #settingsLoad; + #initialized = false; + + /** + * Initialize the keyboard controller with dependencies + * @param {Object} config + * @param {Object} config.api - API object + * @param {Object} config.platform - Platform object + * @param {Object} config.selection - Selection object + * @param {Object} config.slider - Slider controller + * @param {Object} config.space - Space object + * @param {Object} config.catalog - Catalog object + * @param {Object} config.sdb - Local storage + * @param {Object} config.uc - User confirmation utility + * @param {Object} config.setCtrl - Settings controller + * @param {Object} config.VIEWS - View constants + * @param {Document} config.DOC - Document object + * @param {Window} config.WIN - Window object + * @param {Function} config.rotateInputSelection - Rotate input callback + * @param {Function} config.settingsLoad - Settings load callback + */ + init({ api, platform, selection, slider, space, catalog, sdb, uc, setCtrl, VIEWS, DOC, WIN, rotateInputSelection, settingsLoad }) { + if (this.#initialized) { + throw new Error('Keyboard already initialized'); + } + + this.#api = api; + this.#platform = platform; + this.#selection = selection; + this.#slider = slider; + this.#space = space; + this.#catalog = catalog; + this.#sdb = sdb; + this.#uc = uc; + this.#setCtrl = setCtrl; + this.#VIEWS = VIEWS; + this.#DOC = DOC; + this.#WIN = WIN; + this.#rotateInputSelection = rotateInputSelection; + this.#settingsLoad = settingsLoad; + + this.#bindEvents(); + this.#initialized = true; + } + + /** + * Check if an input element has focus + * @returns {boolean} + */ + inputHasFocus() { + let active = this.#DOC.activeElement; + return active && (active.nodeName === "INPUT" || active.nodeName === "TEXTAREA"); + } + + /** + * Get character code at position 0 + * @param {string} c - Character + * @returns {number} + */ + #cca(c) { + return c.charCodeAt(0); + } + + #bindEvents() { + this.#space.event.addHandlers(self, [ + 'keyup', this.#handleKeyUp.bind(this), + 'keydown', this.#handleKeyDown.bind(this), + 'keypress', this.#handleKeyPress.bind(this) + ]); + } + + #handleKeyUp(evt) { + // Allow feature hooks to intercept + if (this.#api.feature.on_key) { + if (this.#api.feature.on_key({up:evt})) return; + } + for (let handler of this.#api.feature.on_key2) { + if (handler({up:evt})) return; + } + + switch (evt.keyCode) { + // escape + case 27: + // blur text input focus + this.#DOC.activeElement.blur(); + // dismiss modals + this.#api.modal.hide(); + // deselect widgets + this.#platform.deselect(); + // hide all dialogs + this.#api.dialog.hide(); + // cancel slicing + this.#api.function.cancel(); + // trigger escape handlers (used by FDM mode for support editing) + this.#onEscape(); + break; + } + return false; + } + + #handleKeyDown(evt) { + if (this.#api.modal.visible()) { + return false; + } + + // Allow feature hooks to intercept + if (this.#api.feature.on_key) { + if (this.#api.feature.on_key({down:evt})) return; + } + for (let handler of this.#api.feature.on_key2) { + if (handler({down:evt})) return; + } + + let move = evt.altKey ? 5 : 0, + deg = move ? 0 : -Math.PI / (evt.shiftKey ? 36 : 2); + + switch (evt.keyCode) { + case 8: // apple: delete/backspace + case 46: // others: delete + if (this.inputHasFocus()) return false; + this.#platform.delete(this.#selection.meshes()); + evt.preventDefault(); + break; + case 37: // left arrow + if (this.inputHasFocus()) return false; + if (deg) this.#selection.rotate(0, 0, -deg); + if (move > 0) this.#selection.move(-move, 0, 0); + evt.preventDefault(); + break; + case 39: // right arrow + if (this.inputHasFocus()) return false; + if (deg) this.#selection.rotate(0, 0, deg); + if (move > 0) this.#selection.move(move, 0, 0); + evt.preventDefault(); + break; + case 38: // up arrow + if (this.inputHasFocus()) return false; + if (evt.metaKey) { + const { hi } = this.#slider.getRange(); + return this.#api.show.layer(hi + 1); + } + if (deg) this.#selection.rotate(deg, 0, 0); + if (move > 0) this.#selection.move(0, move, 0); + evt.preventDefault(); + break; + case 40: // down arrow + if (this.inputHasFocus()) return false; + if (evt.metaKey) { + const { hi } = this.#slider.getRange(); + return this.#api.show.layer(hi - 1); + } + if (deg) this.#selection.rotate(-deg, 0, 0); + if (move > 0) this.#selection.move(0, -move, 0); + evt.preventDefault(); + break; + case 65: // 'a' for select all + if (evt.metaKey || evt.ctrlKey) { + if (this.inputHasFocus()) return false; + evt.preventDefault(); + this.#platform.deselect(); + this.#platform.select_all(); + } + break; + case 83: // 's' for save workspace + if (evt.ctrlKey) { + evt.preventDefault(); + this.#api.conf.save(); + console.log("settings saved"); + } else + if (evt.metaKey) { + evt.preventDefault(); + this.#api.space.save(); + this.#setCtrl.sync.put(); + } + break; + case 76: // 'l' for restore workspace + if (evt.metaKey) { + evt.preventDefault(); + this.#api.space.restore(); + } + break; + } + } + + #handleKeyPress(evt) { + let handled = true; + if (this.#api.modal.visible() || this.inputHasFocus()) { + return false; + } + + // Allow feature hooks to intercept + if (this.#api.feature.on_key) { + if (this.#api.feature.on_key({key:evt})) return; + } + for (let handler of this.#api.feature.on_key2) { + if (handler({key:evt})) return; + } + + // Handle Ctrl key combinations + if (evt.ctrlKey) { + switch (evt.key) { + case 'g': return this.#api.group.merge(); + case 'u': return this.#api.group.split(); + } + } + + switch (evt.charCode) { + case this.#cca('`'): this.#api.show.slices(0); break; + case this.#cca('0'): { + const { max } = this.#slider.getRange(); + this.#api.show.slices(max); + break; + } + case this.#cca('1'): { + const { max } = this.#slider.getRange(); + this.#api.show.slices(max/10); + break; + } + case this.#cca('2'): { + const { max } = this.#slider.getRange(); + this.#api.show.slices(max*2/10); + break; + } + case this.#cca('3'): { + const { max } = this.#slider.getRange(); + this.#api.show.slices(max*3/10); + break; + } + case this.#cca('4'): { + const { max } = this.#slider.getRange(); + this.#api.show.slices(max*4/10); + break; + } + case this.#cca('5'): { + const { max } = this.#slider.getRange(); + this.#api.show.slices(max*5/10); + break; + } + case this.#cca('6'): { + const { max } = this.#slider.getRange(); + this.#api.show.slices(max*6/10); + break; + } + case this.#cca('7'): { + const { max } = this.#slider.getRange(); + this.#api.show.slices(max*7/10); + break; + } + case this.#cca('8'): { + const { max } = this.#slider.getRange(); + this.#api.show.slices(max*8/10); + break; + } + case this.#cca('9'): { + const { max } = this.#slider.getRange(); + this.#api.show.slices(max*9/10); + break; + } + case this.#cca('?'): + this.#api.help.show(); + break; + case this.#cca('Z'): // reset stored state + this.#uc.confirm('clear all settings and preferences?').then(yes => { + if (yes) { + this.#sdb.clear(); + this.#WIN.location.reload(); + } + }); + break; + case this.#cca('C'): // refresh catalog + this.#catalog.refresh(); + break; + case this.#cca('i'): // file import + this.#api.event.import(); + break; + case this.#cca('S'): // slice + case this.#cca('s'): // slice + this.#api.function.slice(); + break; + case this.#cca('P'): // prepare + case this.#cca('p'): // prepare + if (this.#api.mode.get() !== 'SLA') { + // hidden in SLA mode + this.#api.function.prepare(); + } + break; + case this.#cca('X'): // export + case this.#cca('x'): // export + this.#api.function.export(); + break; + case this.#cca('g'): // CAM animate + this.#api.function.animate(); + break; + case this.#cca('O'): // manual rotation + this.#rotateInputSelection(); + break; + case this.#cca('r'): // recent files + this.#api.modal.show('files'); + break; + case this.#cca('q'): // preferences + this.#api.modal.show('prefs'); + break; + case this.#cca('l'): // device + this.#settingsLoad(); + break; + case this.#cca('e'): // device + this.#api.show.devices(); + break; + case this.#cca('o'): // tools + this.#api.show.tools(); + break; + case this.#cca('c'): // local devices + this.#api.show.local(); + break; + case this.#cca('v'): // toggle single slice view mode + if (this.#api.view.get() === this.#VIEWS.ARRANGE) { + this.#api.space.set_focus(this.#selection.widgets()); + } + const { lo, hi } = this.#slider.getRange(); + if (hi === lo) { + this.#slider.setRange(0, hi); + } else { + this.#slider.setRange(hi, hi); + } + this.#api.show.slices(); + break; + case this.#cca('d'): // duplicate object + this.#selection.duplicate(); + break; + case this.#cca('m'): // mirror object + this.#selection.mirror(); + break; + case this.#cca('a'): + if (this.#api.view.get() === this.#VIEWS.ARRANGE) { + // auto arrange items on platform + this.#platform.layout(); + if (!this.#api.conf.get().controller.spaceRandoX) { + this.#api.space.set_focus(this.#selection.widgets()); + } + } else { + // go to arrange view + this.#api.view.set(this.#VIEWS.ARRANGE); + } + break; + default: + this.#onUnhandledKey(evt); + handled = false; + break; + } + if (handled) { + evt.preventDefault(); + evt.stopPropagation(); + } + return false; + } + + /** + * Called when escape key is pressed + * Replaces api.event.emit('key.esc') + */ + #onEscape() { + // Notify listeners (currently used by FDM mode for support editing) + this.#api.event.emit("key.esc"); + } + + /** + * Called when an unhandled key is pressed + * Replaces api.event.emit('keypress', evt) + */ + #onUnhandledKey(evt) { + // Notify listeners for custom key handling + this.#api.event.emit('keypress', evt); + } +} + +// Export singleton instance +export const keyboard = new KeyboardControl(); diff --git a/src/kiri/core/main.js b/src/kiri/core/main.js index d222094e..0ce2af7e 100644 --- a/src/kiri/core/main.js +++ b/src/kiri/core/main.js @@ -327,7 +327,7 @@ function setVisibleLayer(h, l) { l = l >= 0 ? l : lo; const newHi = bound(h, 0, max); const newLo = bound(l, 0, newHi); - api.slider.setRange(newLo, newHi); + api.slider.setRange(newLo, newHi); // Notify callbacks (STACKS, scene updates) api.slider.showLabels(); showSlices(); } diff --git a/src/kiri/core/slider.js b/src/kiri/core/slider.js index d7db613f..f588340b 100644 --- a/src/kiri/core/slider.js +++ b/src/kiri/core/slider.js @@ -68,13 +68,16 @@ class SliderControl { * Set layer range (bounds are enforced) * @param {number} lo - Lower layer * @param {number} hi - Upper layer + * @param {boolean} notify - Whether to trigger callbacks (default: true) */ - setRange(lo, hi) { + setRange(lo, hi, notify = true) { this.#layerLo = Math.max(0, Math.min(hi, lo)); this.#layerHi = Math.max(this.#layerLo, Math.min(this.#layerMax, hi)); this.#updateVisuals(); this.showLabels(); - this.#notifyChange(); + if (notify) { + this.#notifyChange(); + } } /** From 79f7a338231e718e45d208ce36d78148854bf15e Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Wed, 24 Dec 2025 18:01:58 -0500 Subject: [PATCH 013/154] consolidate modal handling into a new core file --- src/kiri/core/init.js | 18 ++++++++ src/kiri/core/main.js | 73 ++++-------------------------- src/kiri/core/modal.js | 100 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 64 deletions(-) create mode 100644 src/kiri/core/modal.js diff --git a/src/kiri/core/init.js b/src/kiri/core/init.js index ef338077..6df587d7 100644 --- a/src/kiri/core/init.js +++ b/src/kiri/core/init.js @@ -6,6 +6,7 @@ import { base } from '../../geo/base.js'; import { local as sdb } from '../../data/local.js'; import { slider } from './slider.js'; import { keyboard } from './keyboard.js'; +import { modal } from './modal.js'; import { space } from '../../moto/space.js'; import { LOCAL, SETUP } from './main.js'; import { VIEWS, MODES, SEED } from './consts.js'; @@ -848,6 +849,23 @@ function init_one() { settingsSave(undefined, ui.settingsName.value); }; + // initialize modal controller + modal.init({ + ui: { + modal: ui.modal, + modals: ui.modals + }, + onShow: (which) => { + api.event.emit('modal.show', which); + }, + onHide: () => { + api.event.emit('modal.hide'); + } + }); + + // expose modal to API + api.modal = modal; + // slider setup slider.init({ ui: { diff --git a/src/kiri/core/main.js b/src/kiri/core/main.js index 0ce2af7e..46367ca1 100644 --- a/src/kiri/core/main.js +++ b/src/kiri/core/main.js @@ -4,6 +4,7 @@ import './frame.js'; import '../../ext/base64.js'; import STACKS from './stacks.js'; +import { modal } from './modal.js'; import { $ } from '../../moto/webui.js'; import { api } from './api.js'; @@ -49,8 +50,8 @@ stats.set('kiri', version); self.kiri_catalog = FILES; export const dialog = { - show: showModal, - hide: hideModal, + show: (which) => modal.show(which), + hide: () => modal.hide(), update_process_list: updateProcessList }; @@ -84,11 +85,8 @@ export const image = { convert: loadImageConvert }; -export const modal = { - show: showModal, - hide: hideModal, - visible: modalShowing -}; +// Re-export modal singleton methods +export { modal }; export const mode = { get_id() { return MODE }, @@ -172,7 +170,7 @@ export const view = { }; // add show() to catalog for API -FILES.show = showCatalog; +FILES.show = () => modal.show('files'); // patch broker for api backward compatibility EVENT.on = (topic, listener) => { @@ -604,59 +602,6 @@ function clearWorkspace() { platform.delete(selection.meshes()); } -function modalShowing() { - return api.ui.modal.style.display === 'flex'; -} - -function showModal(which) { - let mod = api.ui.modal, - style = mod.style, - visible = modalShowing(), - info = { pct: 0 }; - - // hide all modals befroe showing another - Object.keys(api.ui.modals).forEach(name => { - api.ui.modals[name].style.display = name === which ? 'flex' : ''; - }); - - function ondone() { - api.event.emit('modal.show', which); - } - - if (visible) { - return ondone(); - } - - style.height = '0'; - style.display = 'flex'; - - new TWEEN.Tween(info). - easing(TWEEN.Easing.Quadratic.InOut). - to({ pct: 100 }, 100). - onUpdate(() => { style.height = `${info.pct}%` }). - onComplete(ondone). - start(); -} - -function hideModal() { - if (!modalShowing()) { - return; - } - let mod = api.ui.modal, style = mod.style, info={pct:100}; - new TWEEN.Tween(info). - easing(TWEEN.Easing.Quadratic.InOut). - to({pct:0}, 100). - onUpdate(() => { style.height = `${info.pct}%` }). - onComplete(() => { - style.display = ''; - api.event.emit('modal.hide'); - }). - start(); -} - -function showCatalog() { - showModal("files"); -} function editSettings(e) { let current = settings.get(), @@ -729,7 +674,7 @@ function updateProcessList() { load.onclick = (ev) => { api.conf.load(undefined, sk); updateProcessList(); - hideModal(); + modal.hide(); } load.appendChild(DOC.createTextNode(sk)); if (sk == settings.proc().processName) { @@ -772,7 +717,7 @@ function showHelpFile(local,then) { } const LANG = api.language.current; $('kiri-version').innerHTML = `${LANG.version} ${version}`; - showModal('help'); + modal.show('help'); api.event.emit('help.show', local); } @@ -881,7 +826,7 @@ function setMode(mode, lock, then) { selection.update_bounds(api.widgets.all()); api.conf.update_fields(); // because device dialog, if showing, needs to be updated - if (modalShowing()) { + if (modal.visible()) { api.show.devices(); } api.space.restore(null, true); diff --git a/src/kiri/core/modal.js b/src/kiri/core/modal.js new file mode 100644 index 00000000..9cf49b7a --- /dev/null +++ b/src/kiri/core/modal.js @@ -0,0 +1,100 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +/** + * Modal Manager (Singleton) + * Manages modal dialog display and animations + */ + +class ModalControl { + #ui; + #onShow; + #onHide; + #initialized = false; + + /** + * Initialize the modal controller with dependencies + * @param {Object} config + * @param {Object} config.ui - UI element references + * @param {HTMLElement} config.ui.modal - Modal container element + * @param {Object} config.ui.modals - Object containing all modal elements by name + * @param {Function} config.onShow - Optional callback when modal is shown + * @param {Function} config.onHide - Optional callback when modal is hidden + */ + init({ ui, onShow, onHide }) { + if (this.#initialized) { + throw new Error('Modal already initialized'); + } + + this.#ui = ui; + this.#onShow = onShow; + this.#onHide = onHide; + this.#initialized = true; + } + + /** + * Check if modal is currently visible + * @returns {boolean} + */ + visible() { + return this.#ui.modal.style.display === 'flex'; + } + + /** + * Show a specific modal with animation + * @param {string} which - Name of the modal to show + */ + show(which) { + let mod = this.#ui.modal, + style = mod.style, + visible = this.visible(), + info = { pct: 0 }; + + // hide all modals before showing another + Object.keys(this.#ui.modals).forEach(name => { + this.#ui.modals[name].style.display = name === which ? 'flex' : ''; + }); + + const ondone = () => { + this.#onShow?.(which); + }; + + if (visible) { + return ondone(); + } + + style.height = '0'; + style.display = 'flex'; + + new TWEEN.Tween(info). + easing(TWEEN.Easing.Quadratic.InOut). + to({ pct: 100 }, 100). + onUpdate(() => { style.height = `${info.pct}%` }). + onComplete(ondone). + start(); + } + + /** + * Hide the modal with animation + */ + hide() { + if (!this.visible()) { + return; + } + let mod = this.#ui.modal, + style = mod.style, + info = { pct: 100 }; + + new TWEEN.Tween(info). + easing(TWEEN.Easing.Quadratic.InOut). + to({ pct: 0 }, 100). + onUpdate(() => { style.height = `${info.pct}%` }). + onComplete(() => { + style.display = ''; + this.#onHide?.(); + }). + start(); + } +} + +// Export singleton instance +export const modal = new ModalControl(); From 5dbe9066295f517e8ae34b4f9f7ef819e031f56e Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Wed, 24 Dec 2025 19:53:02 -0500 Subject: [PATCH 014/154] move mouse, hover, click, drag into interact module --- src/kiri/core/init.js | 129 ++++++------------------- src/kiri/core/interact.js | 192 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 220 insertions(+), 101 deletions(-) create mode 100644 src/kiri/core/interact.js diff --git a/src/kiri/core/init.js b/src/kiri/core/init.js index 6df587d7..efc25321 100644 --- a/src/kiri/core/init.js +++ b/src/kiri/core/init.js @@ -7,6 +7,7 @@ import { local as sdb } from '../../data/local.js'; import { slider } from './slider.js'; import { keyboard } from './keyboard.js'; import { modal } from './modal.js'; +import { interact } from './interact.js'; import { space } from '../../moto/space.js'; import { LOCAL, SETUP } from './main.js'; import { VIEWS, MODES, SEED } from './consts.js'; @@ -1114,22 +1115,35 @@ function init_one() { api.platform.update_size(); - function mouseOnHover(int, event, ints) { - if (!api.feature.hover) return; - if (!int) return api.feature.hovers || api.widgets.meshes(); - api.event.emit('mouse.hover', {int, ints, event, point: int.point, type: 'widget'}); - } - - function platformOnHover(int, event) { - if (!api.feature.hover) return; - if (int) api.event.emit('mouse.hover', {point: int, event, type: 'platform'}); - } - - api.event.on("feature.hover", enable => { - space.mouse.onHover(enable ? mouseOnHover : undefined); - space.platform.onHover(enable ? platformOnHover : undefined); + // initialize interaction controller + interact.init({ + api, + space, + platform, + selection, + ui, + settings, + VIEWS, + onHover: (data) => { + api.event.emit('mouse.hover', data); + }, + onHoverDown: (data) => { + api.event.emit('mouse.hover.down', data); + }, + onHoverUp: (data) => { + api.event.emit('mouse.hover.up', data); + }, + onDragDone: (offset) => { + api.event.emit('mouse.drag.done', offset); + }, + onSelectionDrag: (delta) => { + api.event.emit('selection.drag', delta); + } }); + // expose interact to API + api.interact = interact; + // prevent safari from exiting full screen mode DOC.onkeydown = function (evt) { if (evt.code == 27) evt.preventDefault() @@ -1145,93 +1159,6 @@ function init_one() { } }; - space.mouse.downSelect((int, event) => { - if (api.feature.on_mouse_down) { - if (int) { - api.feature.on_mouse_down(int, event); - return; - } - } - if (api.feature.hover) { - if (int) { - return api.event.emit('mouse.hover.down', {int, point: int.point}); - } else { - return selection.meshes(); - } - } - // lay flat with meta or ctrl clicking a selected face - if (int && (event.ctrlKey || event.metaKey || api.feature.on_face_select)) { - let q = new THREE.Quaternion(); - // find intersecting point, look "up" on Z and rotate to face that - q.setFromUnitVectors(int.face.normal, new THREE.Vector3(0,0,-1)); - selection.rotate(q); - } - if (api.view.get() !== VIEWS.ARRANGE) { - // return no selection in modes other than arrange - return null; - } else { - // return selected meshes for further mouse processing - return api.feature.hovers || selection.meshes(); - } - }); - - space.mouse.upSelect((object, event) => { - if (api.feature.on_mouse_up) { - if (event && object) { - return api.feature.on_mouse_up(object, event); - } else { - return api.widgets.meshes(); - } - } - if (event && api.feature.hover) { - api.event.emit('mouse.hover.up', { object, event }); - return; - } - if (event && event.target.nodeName === "CANVAS") { - if (object && object.object) { - if (object.object.widget) { - platform.select(object.object.widget, event.shiftKey, false); - } - } else { - platform.deselect(); - } - } else { - return api.feature.hovers || api.widgets.meshes(); - } - }); - - space.mouse.onDrag(function(delta, offset, up = false) { - if (api.feature.hover) { - return; - } - if (up) { - api.event.emit('mouse.drag.done', offset); - } - if (delta && ui.freeLayout.checked) { - let set = settings(); - let dev = set.device; - let bound = set.bounds_sel; - let width = dev.bedWidth/2; - let depth = dev.bedDepth/2; - let isout = ( - bound.min.x <= -width || - bound.min.y <= -depth || - bound.max.x >= width || - bound.max.y >= depth - ); - if (!isout) { - if (bound.min.x + delta.x <= -width) return; - if (bound.min.y + delta.y <= -depth) return; - if (bound.max.x + delta.x >= width) return; - if (bound.max.y + delta.y >= depth) return; - } - selection.move(delta.x, delta.y, 0); - api.event.emit('selection.drag', delta); - } else { - return selection.meshes().length > 0; - } - }); - api.space.restore(init_two) || checkSeed(init_two) || init_two(); }; diff --git a/src/kiri/core/interact.js b/src/kiri/core/interact.js new file mode 100644 index 00000000..59a2348f --- /dev/null +++ b/src/kiri/core/interact.js @@ -0,0 +1,192 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +/** + * Interaction Controller (Singleton) + * Manages mouse hover, click, and drag interactions with 3D objects + */ + +class InteractionControl { + #api; + #space; + #platform; + #selection; + #ui; + #settings; + #VIEWS; + #initialized = false; + + // Callbacks to replace event emissions + #onHover; + #onHoverDown; + #onHoverUp; + #onDragDone; + #onSelectionDrag; + + /** + * Initialize the interaction controller with dependencies + * @param {Object} config + * @param {Object} config.api - API object + * @param {Object} config.space - Space object (3D scene) + * @param {Object} config.platform - Platform object + * @param {Object} config.selection - Selection object + * @param {Object} config.ui - UI element references + * @param {Function} config.settings - Settings getter function + * @param {Object} config.VIEWS - View constants + * @param {Function} config.onHover - Called when hovering over objects + * @param {Function} config.onHoverDown - Called on mouse down over object + * @param {Function} config.onHoverUp - Called on mouse up over object + * @param {Function} config.onDragDone - Called when drag is complete + * @param {Function} config.onSelectionDrag - Called during selection drag + */ + init({ api, space, platform, selection, ui, settings, VIEWS, onHover, onHoverDown, onHoverUp, onDragDone, onSelectionDrag }) { + if (this.#initialized) { + throw new Error('Interactions already initialized'); + } + + this.#api = api; + this.#space = space; + this.#platform = platform; + this.#selection = selection; + this.#ui = ui; + this.#settings = settings; + this.#VIEWS = VIEWS; + this.#onHover = onHover; + this.#onHoverDown = onHoverDown; + this.#onHoverUp = onHoverUp; + this.#onDragDone = onDragDone; + this.#onSelectionDrag = onSelectionDrag; + + this.#bindHoverHandlers(); + this.#bindMouseHandlers(); + this.#initialized = true; + } + + #bindHoverHandlers() { + // Set up hover handlers + this.#api.event.on("feature.hover", enable => { + this.#space.mouse.onHover(enable ? this.#mouseOnHover.bind(this) : undefined); + this.#space.platform.onHover(enable ? this.#platformOnHover.bind(this) : undefined); + }); + } + + #mouseOnHover(int, event, ints) { + if (!this.#api.feature.hover) return; + if (!int) return this.#api.feature.hovers || this.#api.widgets.meshes(); + this.#onHover?.({int, ints, event, point: int.point, type: 'widget'}); + } + + #platformOnHover(int, event) { + if (!this.#api.feature.hover) return; + if (int) this.#onHover?.({point: int, event, type: 'platform'}); + } + + #bindMouseHandlers() { + // Mouse down handler + this.#space.mouse.downSelect((int, event) => { + // Feature hook for custom mouse down handling + if (this.#api.feature.on_mouse_down) { + if (int) { + this.#api.feature.on_mouse_down(int, event); + return; + } + } + + // Hover mode handling + if (this.#api.feature.hover) { + if (int) { + this.#onHoverDown?.({int, point: int.point}); + return; + } else { + return this.#selection.meshes(); + } + } + + // Lay flat with meta or ctrl clicking a selected face + if (int && (event.ctrlKey || event.metaKey || this.#api.feature.on_face_select)) { + let q = new THREE.Quaternion(); + // find intersecting point, look "up" on Z and rotate to face that + q.setFromUnitVectors(int.face.normal, new THREE.Vector3(0,0,-1)); + this.#selection.rotate(q); + } + + if (this.#api.view.get() !== this.#VIEWS.ARRANGE) { + // return no selection in modes other than arrange + return null; + } else { + // return selected meshes for further mouse processing + return this.#api.feature.hovers || this.#selection.meshes(); + } + }); + + // Mouse up handler + this.#space.mouse.upSelect((object, event) => { + // Feature hook for custom mouse up handling + if (this.#api.feature.on_mouse_up) { + if (event && object) { + return this.#api.feature.on_mouse_up(object, event); + } else { + return this.#api.widgets.meshes(); + } + } + + // Hover mode handling + if (event && this.#api.feature.hover) { + this.#onHoverUp?.({ object, event }); + return; + } + + // Regular selection handling + if (event && event.target.nodeName === "CANVAS") { + if (object && object.object) { + if (object.object.widget) { + this.#platform.select(object.object.widget, event.shiftKey, false); + } + } else { + this.#platform.deselect(); + } + } else { + return this.#api.feature.hovers || this.#api.widgets.meshes(); + } + }); + + // Drag handler + this.#space.mouse.onDrag((delta, offset, up = false) => { + if (this.#api.feature.hover) { + return; + } + + if (up) { + this.#onDragDone?.(offset); + } + + if (delta && this.#ui.freeLayout.checked) { + let set = this.#settings(); + let dev = set.device; + let bound = set.bounds_sel; + let width = dev.bedWidth/2; + let depth = dev.bedDepth/2; + let isout = ( + bound.min.x <= -width || + bound.min.y <= -depth || + bound.max.x >= width || + bound.max.y >= depth + ); + + if (!isout) { + if (bound.min.x + delta.x <= -width) return; + if (bound.min.y + delta.y <= -depth) return; + if (bound.max.x + delta.x >= width) return; + if (bound.max.y + delta.y >= depth) return; + } + + this.#selection.move(delta.x, delta.y, 0); + this.#onSelectionDrag?.(delta); + } else { + return this.#selection.meshes().length > 0; + } + }); + } +} + +// Export singleton instance +export const interact = new InteractionControl(); From a752c188553d273e147815d3d05ac14feca83f57 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Wed, 24 Dec 2025 21:14:46 -0500 Subject: [PATCH 015/154] start ui code re-organization --- src/kiri/core/api.js | 10 +++++----- src/kiri/core/codec.js | 2 +- src/kiri/core/init.js | 11 +++++------ src/kiri/core/main.js | 10 +++++----- src/kiri/core/platform.js | 2 +- src/kiri/core/render.js | 2 +- src/kiri/core/slice.js | 2 +- src/kiri/mode/cam/cl-trace.js | 4 ++-- src/kiri/mode/cam/init-ui.js | 4 ++-- src/kiri/mode/fdm/init-ui.js | 4 ++-- src/kiri/mode/laser/driver.js | 2 +- src/kiri/{core => ui}/alerts.js | 2 +- src/kiri/{core/ui.js => ui/component.js} | 2 +- src/kiri/{core => ui}/do.js | 2 +- src/kiri/{core => ui}/files.js | 0 src/kiri/{core => ui}/frame.js | 8 ++++---- src/kiri/{core => ui}/interact.js | 0 src/kiri/{core => ui}/keyboard.js | 0 src/kiri/{core => ui}/lang.js | 0 src/kiri/{core => ui}/layers.js | 0 src/kiri/{core => ui}/modal.js | 0 src/kiri/{core => ui}/pack.js | 0 src/kiri/{core => ui}/slider.js | 0 src/kiri/{core => ui}/stack.js | 0 src/kiri/{core => ui}/stacks.js | 2 +- src/kiri/{core => ui}/stats.js | 2 +- src/kiri/{core => ui}/tools.js | 0 src/main/kiri.js | 2 +- 28 files changed, 36 insertions(+), 37 deletions(-) rename src/kiri/{core => ui}/alerts.js (97%) rename src/kiri/{core/ui.js => ui/component.js} (99%) rename src/kiri/{core => ui}/do.js (98%) rename src/kiri/{core => ui}/files.js (100%) rename src/kiri/{core => ui}/frame.js (95%) rename src/kiri/{core => ui}/interact.js (100%) rename src/kiri/{core => ui}/keyboard.js (100%) rename src/kiri/{core => ui}/lang.js (100%) rename src/kiri/{core => ui}/layers.js (100%) rename src/kiri/{core => ui}/modal.js (100%) rename src/kiri/{core => ui}/pack.js (100%) rename src/kiri/{core => ui}/slider.js (100%) rename src/kiri/{core => ui}/stack.js (100%) rename src/kiri/{core => ui}/stacks.js (99%) rename src/kiri/{core => ui}/stats.js (96%) rename src/kiri/{core => ui}/tools.js (100%) diff --git a/src/kiri/core/api.js b/src/kiri/core/api.js index 4d8b328f..b5064edd 100644 --- a/src/kiri/core/api.js +++ b/src/kiri/core/api.js @@ -1,8 +1,8 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import alerts from './alerts.js'; +import alerts from '../ui/alerts.js'; import settings from './settings.js' -import STACKS from './stacks.js'; +import STACKS from '../ui/stacks.js'; import { broker } from '../../moto/broker.js'; import { client as work } from './client.js'; @@ -15,7 +15,7 @@ import { noop, ajax, o2js, js2o } from './utils.js'; import { functions } from './function.js'; import { platform } from './platform.js'; import { selection } from './selection.js'; -import { stats } from './stats.js'; +import { stats } from '../ui/stats.js'; import { newWidget } from './widget.js'; import { widgets } from './widgets.js'; import { updateTool } from '../mode/cam/tools.js'; @@ -23,9 +23,9 @@ import { beta, version } from '../../moto/license.js'; import { space as SPACE } from '../../moto/space.js'; import { types as load } from '../../load/file.js'; -import { LANG } from './lang.js'; +import { LANG } from '../ui/lang.js'; import { LOCAL, SETUP, SECURE } from './main.js'; -import { UI } from './ui.js'; +import { UI } from '../ui/component.js'; import web from '../../moto/webui.js'; diff --git a/src/kiri/core/codec.js b/src/kiri/core/codec.js index ec0da847..fa625720 100644 --- a/src/kiri/core/codec.js +++ b/src/kiri/core/codec.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { Layers } from './layers.js'; +import { Layers } from '../ui/layers.js'; import { newPoint } from '../../geo/point.js'; import { Polygon, newPolygon } from '../../geo/polygon.js'; import { Slice, newSlice, Top, newTop } from './slice.js'; diff --git a/src/kiri/core/init.js b/src/kiri/core/init.js index efc25321..b363c334 100644 --- a/src/kiri/core/init.js +++ b/src/kiri/core/init.js @@ -4,15 +4,16 @@ import { $, $c } from '../../moto/webui.js'; import { api } from './api.js'; import { base } from '../../geo/base.js'; import { local as sdb } from '../../data/local.js'; -import { slider } from './slider.js'; -import { keyboard } from './keyboard.js'; -import { modal } from './modal.js'; -import { interact } from './interact.js'; +import { slider } from '../ui/slider.js'; +import { keyboard } from '../ui/keyboard.js'; +import { modal } from '../ui/modal.js'; +import { interact } from '../ui/interact.js'; import { space } from '../../moto/space.js'; import { LOCAL, SETUP } from './main.js'; import { VIEWS, MODES, SEED } from './consts.js'; import { beta, version } from '../../moto/license.js'; import { settings as set_ctrl } from './settings.js'; +import STACKS from '../ui/stacks.js'; import { init as initCAM } from '../mode/cam/init-ui.js'; import { init as initDRAG } from '../mode/drag/init-ui.js'; import { init as initFDM } from '../mode/fdm/init-ui.js'; @@ -24,8 +25,6 @@ import { menu as menuCAM } from '../mode/cam/init-menu.js'; import { menu as menuFDM } from '../mode/fdm/init-menu.js'; import { menu as menuLaser } from '../mode/laser/init-menu.js'; import { menu as menuSLA } from '../mode/sla/init-menu.js'; -import STACKS from './stacks.js'; -import './tools.js'; let { CAM, SLA, FDM, LASER, DRAG, WJET, WEDM } = MODES, { client, catalog, platform, selection, stats } = api, diff --git a/src/kiri/core/main.js b/src/kiri/core/main.js index 46367ca1..29450427 100644 --- a/src/kiri/core/main.js +++ b/src/kiri/core/main.js @@ -1,10 +1,10 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import './frame.js'; +import '../ui/frame.js'; import '../../ext/base64.js'; -import STACKS from './stacks.js'; -import { modal } from './modal.js'; +import STACKS from '../ui/stacks.js'; +import { modal } from '../ui/modal.js'; import { $ } from '../../moto/webui.js'; import { api } from './api.js'; @@ -12,13 +12,13 @@ import { broker } from '../../moto/broker.js'; import { consts } from './consts.js'; import { Index } from '../../data/index.js'; import { local as dataLocal } from '../../data/local.js'; -import { openFiles } from './files.js'; +import { openFiles } from '../ui/files.js'; import { platform } from './platform.js'; import { selection } from './selection.js'; import { settings } from './settings.js'; import { showDevices } from './devices.js'; import { space as SPACE } from '../../moto/space.js'; -import { stats } from './stats.js'; +import { stats } from '../ui/stats.js'; import { noop, utils } from './utils.js'; import { version } from '../../moto/license.js'; import { Widget, newWidget } from './widget.js'; diff --git a/src/kiri/core/platform.js b/src/kiri/core/platform.js index e349dfb2..dae6cd2b 100644 --- a/src/kiri/core/platform.js +++ b/src/kiri/core/platform.js @@ -7,7 +7,7 @@ import { base } from '../../geo/base.js'; import { load } from '../../load/file.js'; import { space } from '../../moto/space.js'; import { Widget, newWidget } from './widget.js'; -import { Packer } from './pack.js'; +import { Packer } from '../ui/pack.js'; import { COLOR, MODES } from './consts.js'; import { THREE } from '../../ext/three.js'; diff --git a/src/kiri/core/render.js b/src/kiri/core/render.js index 7dfb7e97..456f3146 100644 --- a/src/kiri/core/render.js +++ b/src/kiri/core/render.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { Layers } from './layers.js'; +import { Layers } from '../ui/layers.js'; import { newPoint } from '../../geo/point.js'; import { newPolygon } from '../../geo/polygon.js'; import { newSlopeFromAngle } from '../../geo/slope.js'; diff --git a/src/kiri/core/slice.js b/src/kiri/core/slice.js index d684d046..6604399b 100644 --- a/src/kiri/core/slice.js +++ b/src/kiri/core/slice.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { Layers } from './layers.js'; +import { Layers } from '../ui/layers.js'; import { newPoint } from '../../geo/point.js'; import { polygons as POLY } from '../../geo/polygons.js'; diff --git a/src/kiri/mode/cam/cl-trace.js b/src/kiri/mode/cam/cl-trace.js index bd2e9aff..8a77995c 100644 --- a/src/kiri/mode/cam/cl-trace.js +++ b/src/kiri/mode/cam/cl-trace.js @@ -3,8 +3,8 @@ import { api } from '../../core/api.js'; import { env, clearPops, isDark } from './init-ui.js'; import { CAM } from './driver-fe.js'; -import { Layers } from '../../core/layers.js'; -import { Stack } from '../../core/stack.js'; +import { Layers } from '../../ui/layers.js'; +import { Stack } from '../../ui/stack.js'; export let traceOn = false; export let lastTrace; diff --git a/src/kiri/mode/cam/init-ui.js b/src/kiri/mode/cam/init-ui.js index 5b9cea05..b8839152 100644 --- a/src/kiri/mode/cam/init-ui.js +++ b/src/kiri/mode/cam/init-ui.js @@ -6,8 +6,8 @@ import { load } from '../../../load/file.js'; import { animate as anim_2d, animate_clear as anim_2d_clear } from './anim-2d-fe.js'; import { animate2 as anim_3d, animate_clear2 as anim_3d_clear } from './anim-3d-fe.js'; import { space as SPACE } from '../../../moto/space.js'; -import { Layers } from '../../core/layers.js'; -import { Stack } from '../../core/stack.js'; +import { Layers } from '../../ui/layers.js'; +import { Stack } from '../../ui/stack.js'; import { updateStock } from './cl-stock.js'; import { createPopOps } from './cl-ops.js'; import { tabAdd, tabDone, tabClear, restoreTabs, rotateTabs, updateTabs, clearTabs, mirrorTabs } from './cl-tab.js'; diff --git a/src/kiri/mode/fdm/init-ui.js b/src/kiri/mode/fdm/init-ui.js index 466856f9..ea9187a5 100644 --- a/src/kiri/mode/fdm/init-ui.js +++ b/src/kiri/mode/fdm/init-ui.js @@ -6,8 +6,8 @@ import { conf } from '../../core/conf.js'; import { util } from '../../../geo/base.js'; import { client } from '../../core/client.js'; import { addbox, delbox, getlastbox } from '../../core/boxes.js'; -import { Layers } from '../../core/layers.js'; -import { Stack } from '../../core/stack.js'; +import { Layers } from '../../ui/layers.js'; +import { Stack } from '../../ui/stack.js'; import { Widget, newWidget } from '../../core/widget.js'; import { space } from '../../../moto/space.js'; diff --git a/src/kiri/mode/laser/driver.js b/src/kiri/mode/laser/driver.js index c07e4007..9605d838 100644 --- a/src/kiri/mode/laser/driver.js +++ b/src/kiri/mode/laser/driver.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { base } from '../../../geo/base.js'; -import { Packer } from '../../core/pack.js'; +import { Packer } from '../../ui/pack.js'; import { newBounds } from '../../../geo/bounds.js'; import { newPoint } from '../../../geo/point.js'; import { newPrint } from '../../core/print.js'; diff --git a/src/kiri/core/alerts.js b/src/kiri/ui/alerts.js similarity index 97% rename from src/kiri/core/alerts.js rename to src/kiri/ui/alerts.js index 5cf3c46d..e596b985 100644 --- a/src/kiri/core/alerts.js +++ b/src/kiri/ui/alerts.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from './api.js'; +import { api } from '../core/api.js'; let alerts = []; diff --git a/src/kiri/core/ui.js b/src/kiri/ui/component.js similarity index 99% rename from src/kiri/core/ui.js rename to src/kiri/ui/component.js index d4edf9e2..994484e9 100644 --- a/src/kiri/core/ui.js +++ b/src/kiri/ui/component.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { $ } from '../../moto/webui.js'; -import { api } from './api.js'; +import { api } from '../core/api.js'; let DOC = self.document, inputAction = null, diff --git a/src/kiri/core/do.js b/src/kiri/ui/do.js similarity index 98% rename from src/kiri/core/do.js rename to src/kiri/ui/do.js index b3b998c6..a9f03c81 100644 --- a/src/kiri/core/do.js +++ b/src/kiri/ui/do.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from './api.js'; +import { api } from '../core/api.js'; import { space } from '../../moto/space.js'; const { event } = api; diff --git a/src/kiri/core/files.js b/src/kiri/ui/files.js similarity index 100% rename from src/kiri/core/files.js rename to src/kiri/ui/files.js diff --git a/src/kiri/core/frame.js b/src/kiri/ui/frame.js similarity index 95% rename from src/kiri/core/frame.js rename to src/kiri/ui/frame.js index 11f97f69..634b5857 100644 --- a/src/kiri/core/frame.js +++ b/src/kiri/ui/frame.js @@ -1,10 +1,10 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from './api.js'; +import { api } from '../core/api.js'; import { load } from '../../load/file.js'; -import { newWidget } from './widget.js'; -import { VIEWS } from './consts.js'; -import { widgets } from './widgets.js'; +import { newWidget } from '../core/widget.js'; +import { VIEWS } from '../core/consts.js'; +import { widgets } from '../core/widgets.js'; // add frame message api listener window.addEventListener('message', msg => { diff --git a/src/kiri/core/interact.js b/src/kiri/ui/interact.js similarity index 100% rename from src/kiri/core/interact.js rename to src/kiri/ui/interact.js diff --git a/src/kiri/core/keyboard.js b/src/kiri/ui/keyboard.js similarity index 100% rename from src/kiri/core/keyboard.js rename to src/kiri/ui/keyboard.js diff --git a/src/kiri/core/lang.js b/src/kiri/ui/lang.js similarity index 100% rename from src/kiri/core/lang.js rename to src/kiri/ui/lang.js diff --git a/src/kiri/core/layers.js b/src/kiri/ui/layers.js similarity index 100% rename from src/kiri/core/layers.js rename to src/kiri/ui/layers.js diff --git a/src/kiri/core/modal.js b/src/kiri/ui/modal.js similarity index 100% rename from src/kiri/core/modal.js rename to src/kiri/ui/modal.js diff --git a/src/kiri/core/pack.js b/src/kiri/ui/pack.js similarity index 100% rename from src/kiri/core/pack.js rename to src/kiri/ui/pack.js diff --git a/src/kiri/core/slider.js b/src/kiri/ui/slider.js similarity index 100% rename from src/kiri/core/slider.js rename to src/kiri/ui/slider.js diff --git a/src/kiri/core/stack.js b/src/kiri/ui/stack.js similarity index 100% rename from src/kiri/core/stack.js rename to src/kiri/ui/stack.js diff --git a/src/kiri/core/stacks.js b/src/kiri/ui/stacks.js similarity index 99% rename from src/kiri/core/stacks.js rename to src/kiri/ui/stacks.js index a45f862e..541686fb 100644 --- a/src/kiri/core/stacks.js +++ b/src/kiri/ui/stacks.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { $ } from '../../moto/webui.js'; -import { api } from './api.js'; +import { api } from '../core/api.js'; import { Stack } from './stack.js'; let freeMem = true, diff --git a/src/kiri/core/stats.js b/src/kiri/ui/stats.js similarity index 96% rename from src/kiri/core/stats.js rename to src/kiri/ui/stats.js index d7fcb1a9..5f36f536 100644 --- a/src/kiri/core/stats.js +++ b/src/kiri/ui/stats.js @@ -2,7 +2,7 @@ import { local } from '../../data/local.js'; import { broker } from '../../moto/broker.js'; -import { js2o, o2js } from './utils.js'; +import { js2o, o2js } from '../core/utils.js'; class Stats { constructor(db) { diff --git a/src/kiri/core/tools.js b/src/kiri/ui/tools.js similarity index 100% rename from src/kiri/core/tools.js rename to src/kiri/ui/tools.js diff --git a/src/main/kiri.js b/src/main/kiri.js index 918898a5..a733c879 100644 --- a/src/main/kiri.js +++ b/src/main/kiri.js @@ -3,7 +3,7 @@ import '../add/array.js'; import '../add/class.js'; import '../add/three.js'; -import '../kiri/core/lang.js'; +import '../kiri/ui/lang.js'; import '../kiri/core/lang-en.js'; import { broker } from '../moto/broker.js'; From a0091b8abb125c083a4acc5462e0fe39b59a6d58 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Wed, 24 Dec 2025 21:37:03 -0500 Subject: [PATCH 016/154] move devices and export into ui --- src/kiri/core/api.js | 2 +- src/kiri/core/function.js | 2 +- src/kiri/core/main.js | 2 +- src/kiri/{core => ui}/devices.js | 6 +++--- src/kiri/{core => ui}/export.js | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) rename src/kiri/{core => ui}/devices.js (98%) rename src/kiri/{core => ui}/export.js (99%) diff --git a/src/kiri/core/api.js b/src/kiri/core/api.js index b5064edd..416fdcbb 100644 --- a/src/kiri/core/api.js +++ b/src/kiri/core/api.js @@ -7,7 +7,7 @@ import STACKS from '../ui/stacks.js'; import { broker } from '../../moto/broker.js'; 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 { device, devices } from '../ui/devices.js'; import { catalog, dialog, event, group, help, hide, image } from './main.js'; import { modal, mode, process, show, space, util, view } from './main.js'; import { local as dataLocal } from '../../data/local.js'; diff --git a/src/kiri/core/function.js b/src/kiri/core/function.js index 47fb4d06..9b078e8e 100644 --- a/src/kiri/core/function.js +++ b/src/kiri/core/function.js @@ -5,7 +5,7 @@ import { client } from './client.js'; import { codec } from './codec.js'; import { space } from '../../moto/space.js'; import { COLOR, PMODES } from './consts.js'; -import { exportFile } from './export.js'; +import { exportFile } from '../ui/export.js'; let complete = {}; diff --git a/src/kiri/core/main.js b/src/kiri/core/main.js index 29450427..f9d0b55f 100644 --- a/src/kiri/core/main.js +++ b/src/kiri/core/main.js @@ -16,7 +16,7 @@ import { openFiles } from '../ui/files.js'; import { platform } from './platform.js'; import { selection } from './selection.js'; import { settings } from './settings.js'; -import { showDevices } from './devices.js'; +import { showDevices } from '../ui/devices.js'; import { space as SPACE } from '../../moto/space.js'; import { stats } from '../ui/stats.js'; import { noop, utils } from './utils.js'; diff --git a/src/kiri/core/devices.js b/src/kiri/ui/devices.js similarity index 98% rename from src/kiri/core/devices.js rename to src/kiri/ui/devices.js index 82a867d7..20a080a2 100644 --- a/src/kiri/core/devices.js +++ b/src/kiri/ui/devices.js @@ -1,11 +1,11 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { $, h } from '../../moto/webui.js'; -import { api } from './api.js'; -import { conf } from './conf.js'; +import { api } from '../core/api.js'; +import { conf } from '../core/conf.js'; import { space } from '../../moto/space.js'; import { devices as devlist } from '../../pack/kiri-devs.js'; -import { settings, conf as setconf } from './settings.js'; +import { settings, conf as setconf } from '../core/settings.js'; export const device = { clone: cloneDevice, diff --git a/src/kiri/core/export.js b/src/kiri/ui/export.js similarity index 99% rename from src/kiri/core/export.js rename to src/kiri/ui/export.js index 7b69b91a..36511442 100644 --- a/src/kiri/core/export.js +++ b/src/kiri/ui/export.js @@ -1,11 +1,11 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { $ } from '../../moto/webui.js'; -import { api } from './api.js'; -import { client } from './client.js'; +import { api } from '../core/api.js'; +import { client } from '../core/client.js'; import { local } from '../../data/local.js'; import { util } from '../../geo/base.js'; -import { MODES } from './consts.js'; +import { MODES } from '../core/consts.js'; import { LASER as laser_driver } from '../mode/laser/driver.js'; import { SLA as sla_client } from '../mode/sla/init-ui.js'; import { hash } from '../../ext/md5.js'; From b1d4750ed1e5151f2e46eff18f770fc9052696e4 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Wed, 24 Dec 2025 21:55:35 -0500 Subject: [PATCH 017/154] move client, conf to ui --- src/kiri/core/api.js | 4 ++-- src/kiri/core/function.js | 2 +- src/kiri/core/init.js | 2 +- src/kiri/core/main.js | 2 +- src/kiri/mode/cam/tools.js | 2 +- src/kiri/mode/fdm/init-ui.js | 4 ++-- src/kiri/run/engine.js | 4 ++-- src/kiri/{core => ui}/client.js | 10 +++++----- .../{core/conf.js => ui/config/defaults.js} | 0 .../settings.js => ui/config/manager.js} | 20 +++++++++---------- src/kiri/ui/devices.js | 4 ++-- src/kiri/ui/export.js | 2 +- 12 files changed, 28 insertions(+), 28 deletions(-) rename src/kiri/{core => ui}/client.js (97%) rename src/kiri/{core/conf.js => ui/config/defaults.js} (100%) rename src/kiri/{core/settings.js => ui/config/manager.js} (98%) diff --git a/src/kiri/core/api.js b/src/kiri/core/api.js index 416fdcbb..8ed318c5 100644 --- a/src/kiri/core/api.js +++ b/src/kiri/core/api.js @@ -1,11 +1,11 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import alerts from '../ui/alerts.js'; -import settings from './settings.js' +import settings from '../ui/config/manager.js'; import STACKS from '../ui/stacks.js'; import { broker } from '../../moto/broker.js'; -import { client as work } from './client.js'; +import { client as work } from '../ui/client.js'; import { consts, COLOR as color, LISTS as lists } from './consts.js'; import { device, devices } from '../ui/devices.js'; import { catalog, dialog, event, group, help, hide, image } from './main.js'; diff --git a/src/kiri/core/function.js b/src/kiri/core/function.js index 9b078e8e..72e7385d 100644 --- a/src/kiri/core/function.js +++ b/src/kiri/core/function.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { api } from './api.js'; -import { client } from './client.js'; +import { client } from '../ui/client.js'; import { codec } from './codec.js'; import { space } from '../../moto/space.js'; import { COLOR, PMODES } from './consts.js'; diff --git a/src/kiri/core/init.js b/src/kiri/core/init.js index b363c334..19ae5441 100644 --- a/src/kiri/core/init.js +++ b/src/kiri/core/init.js @@ -12,7 +12,7 @@ import { space } from '../../moto/space.js'; import { LOCAL, SETUP } from './main.js'; import { VIEWS, MODES, SEED } from './consts.js'; import { beta, version } from '../../moto/license.js'; -import { settings as set_ctrl } from './settings.js'; +import { settings as set_ctrl } from '../ui/config/manager.js'; import STACKS from '../ui/stacks.js'; import { init as initCAM } from '../mode/cam/init-ui.js'; import { init as initDRAG } from '../mode/drag/init-ui.js'; diff --git a/src/kiri/core/main.js b/src/kiri/core/main.js index f9d0b55f..2679bd8b 100644 --- a/src/kiri/core/main.js +++ b/src/kiri/core/main.js @@ -15,7 +15,7 @@ import { local as dataLocal } from '../../data/local.js'; import { openFiles } from '../ui/files.js'; import { platform } from './platform.js'; import { selection } from './selection.js'; -import { settings } from './settings.js'; +import { settings } from '../ui/config/manager.js'; import { showDevices } from '../ui/devices.js'; import { space as SPACE } from '../../moto/space.js'; import { stats } from '../ui/stats.js'; diff --git a/src/kiri/mode/cam/tools.js b/src/kiri/mode/cam/tools.js index 16dc117e..b542415a 100644 --- a/src/kiri/mode/cam/tools.js +++ b/src/kiri/mode/cam/tools.js @@ -3,7 +3,7 @@ import { $ } from '../../../moto/webui.js'; import { api } from '../../core/api.js'; import { consts } from '../../core/consts.js'; -import { settings as setconf } from '../../core/settings.js'; +import { settings as setconf } from '../../ui/config/manager.js'; import { Tool, calcTaperAngle, calcTaperBallExtent, calcTaperLength } from './tool.js'; const DEG2RAD = Math.PI / 180; diff --git a/src/kiri/mode/fdm/init-ui.js b/src/kiri/mode/fdm/init-ui.js index ea9187a5..b403ad8d 100644 --- a/src/kiri/mode/fdm/init-ui.js +++ b/src/kiri/mode/fdm/init-ui.js @@ -2,9 +2,9 @@ import { $ } from '../../../moto/webui.js'; import { api } from '../../core/api.js'; -import { conf } from '../../core/conf.js'; +import { conf } from '../../ui/config/defaults.js'; import { util } from '../../../geo/base.js'; -import { client } from '../../core/client.js'; +import { client } from '../../ui/client.js'; import { addbox, delbox, getlastbox } from '../../core/boxes.js'; import { Layers } from '../../ui/layers.js'; import { Stack } from '../../ui/stack.js'; diff --git a/src/kiri/run/engine.js b/src/kiri/run/engine.js index 5b9d7008..f1b49fc7 100644 --- a/src/kiri/run/engine.js +++ b/src/kiri/run/engine.js @@ -4,8 +4,8 @@ import '../../add/array.js'; import '../../add/class.js'; import '../../add/three.js'; import { api } from '../core/api.js'; -import { conf } from '../core/conf.js'; -import { client } from '../core/client.js'; +import { conf } from '../ui/config/defaults.js'; +import { client } from '../ui/client.js'; import { load } from '../../load/file.js'; import { newWidget } from '../core/widget.js'; diff --git a/src/kiri/core/client.js b/src/kiri/ui/client.js similarity index 97% rename from src/kiri/core/client.js rename to src/kiri/ui/client.js index a6a5a116..84208ed2 100644 --- a/src/kiri/core/client.js +++ b/src/kiri/ui/client.js @@ -1,10 +1,10 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from './api.js'; -import { noop } from './utils.js'; -import { codec } from './codec.js'; -import { Widget } from './widget.js'; -import { newPrint } from './print.js'; +import { api } from '../core/api.js'; +import { noop } from '../core/utils.js'; +import { codec } from '../core/codec.js'; +import { Widget } from '../core/widget.js'; +import { newPrint } from '../core/print.js'; // this code runs in kiri's main loop let debug = self.debug === true, diff --git a/src/kiri/core/conf.js b/src/kiri/ui/config/defaults.js similarity index 100% rename from src/kiri/core/conf.js rename to src/kiri/ui/config/defaults.js diff --git a/src/kiri/core/settings.js b/src/kiri/ui/config/manager.js similarity index 98% rename from src/kiri/core/settings.js rename to src/kiri/ui/config/manager.js index 51bb27e2..edf14e67 100644 --- a/src/kiri/core/settings.js +++ b/src/kiri/ui/config/manager.js @@ -1,15 +1,15 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { $, h } from '../../moto/webui.js'; -import { api } from './api.js'; -import { conf } from './conf.js'; -import { codec } from './codec.js'; -import { consts } from './consts.js'; -import { space } from '../../moto/space.js'; -import { local } from '../../data/local.js'; -import { utils } from './utils.js'; -import { version } from '../../moto/license.js'; -import { JSZip } from '../../ext/jszip-esm.js'; +import { $, h } from '../../../moto/webui.js'; +import { api } from '../../core/api.js'; +import { conf } from './defaults.js'; +import { codec } from '../../core/codec.js'; +import { consts } from '../../core/consts.js'; +import { space } from '../../../moto/space.js'; +import { local } from '../../../data/local.js'; +import { utils } from '../../core/utils.js'; +import { version } from '../../../moto/license.js'; +import { JSZip } from '../../../ext/jszip-esm.js'; const { areEqual, ls2o, js2o } = utils; const { COLOR } = consts; diff --git a/src/kiri/ui/devices.js b/src/kiri/ui/devices.js index 20a080a2..7586687a 100644 --- a/src/kiri/ui/devices.js +++ b/src/kiri/ui/devices.js @@ -2,10 +2,10 @@ import { $, h } from '../../moto/webui.js'; import { api } from '../core/api.js'; -import { conf } from '../core/conf.js'; +import { conf } from './config/defaults.js'; import { space } from '../../moto/space.js'; import { devices as devlist } from '../../pack/kiri-devs.js'; -import { settings, conf as setconf } from '../core/settings.js'; +import { settings, conf as setconf } from './config/manager.js'; export const device = { clone: cloneDevice, diff --git a/src/kiri/ui/export.js b/src/kiri/ui/export.js index 36511442..02fa1367 100644 --- a/src/kiri/ui/export.js +++ b/src/kiri/ui/export.js @@ -2,7 +2,7 @@ import { $ } from '../../moto/webui.js'; import { api } from '../core/api.js'; -import { client } from '../core/client.js'; +import { client } from './client.js'; import { local } from '../../data/local.js'; import { util } from '../../geo/base.js'; import { MODES } from '../core/consts.js'; From b7f7d87563f352b8217f04dbb8b03b452996c460 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Wed, 24 Dec 2025 22:38:04 -0500 Subject: [PATCH 018/154] more shuffling of ui files --- src/kiri/core/widgets.js | 2 +- src/kiri/mode/cam/anim-2d-fe.js | 2 +- src/kiri/mode/cam/anim-3d-fe.js | 2 +- src/kiri/mode/cam/cl-flip.js | 2 +- src/kiri/mode/cam/cl-helical.js | 2 +- src/kiri/mode/cam/cl-hole.js | 2 +- src/kiri/mode/cam/cl-ops.js | 2 +- src/kiri/mode/cam/cl-origin.js | 2 +- src/kiri/mode/cam/cl-stock.js | 2 +- src/kiri/mode/cam/cl-surface.js | 2 +- src/kiri/mode/cam/cl-tab.js | 2 +- src/kiri/mode/cam/cl-trace.js | 2 +- src/kiri/mode/cam/driver-fe.js | 2 +- src/kiri/mode/cam/init-menu.js | 2 +- src/kiri/mode/cam/init-ui.js | 2 +- src/kiri/mode/cam/tools.js | 2 +- src/kiri/mode/fdm/init-menu.js | 2 +- src/kiri/mode/fdm/init-ui.js | 2 +- src/kiri/mode/laser/init-menu.js | 2 +- src/kiri/mode/sla/init-menu.js | 2 +- src/kiri/mode/sla/init-ui.js | 2 +- src/kiri/mode/wedm/init-ui.js | 2 +- src/kiri/run/engine.js | 2 +- src/kiri/ui/alerts.js | 2 +- src/kiri/{core => ui}/api.js | 26 ++++++++++---------- src/kiri/ui/client.js | 2 +- src/kiri/ui/component.js | 2 +- src/kiri/ui/config/manager.js | 2 +- src/kiri/ui/devices.js | 2 +- src/kiri/ui/do.js | 2 +- src/kiri/ui/export.js | 2 +- src/kiri/ui/frame.js | 2 +- src/kiri/{core => ui}/function.js | 8 +++--- src/kiri/{core => ui}/init.js | 14 +++++------ src/kiri/{core => ui}/main.js | 22 ++++++++--------- src/kiri/{core => ui}/platform.js | 8 +++--- src/kiri/{core/selection.js => ui/select.js} | 0 src/kiri/ui/stacks.js | 2 +- src/main/kiri.js | 2 +- 39 files changed, 72 insertions(+), 72 deletions(-) rename src/kiri/{core => ui}/api.js (89%) rename src/kiri/{core => ui}/function.js (98%) rename src/kiri/{core => ui}/init.js (99%) rename src/kiri/{core => ui}/main.js (98%) rename src/kiri/{core => ui}/platform.js (99%) rename src/kiri/{core/selection.js => ui/select.js} (100%) diff --git a/src/kiri/core/widgets.js b/src/kiri/core/widgets.js index 10541e74..5bc90476 100644 --- a/src/kiri/core/widgets.js +++ b/src/kiri/core/widgets.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { $ } from '../../moto/webui.js'; -import { api } from './api.js'; +import { api } from '../ui/api.js'; import { load } from '../../load/file.js'; import { Widget, newWidget } from './widget.js'; diff --git a/src/kiri/mode/cam/anim-2d-fe.js b/src/kiri/mode/cam/anim-2d-fe.js index 5e13d220..4c65321c 100644 --- a/src/kiri/mode/cam/anim-2d-fe.js +++ b/src/kiri/mode/cam/anim-2d-fe.js @@ -2,7 +2,7 @@ // UI FRONT END ANIMATION CODE for 2D -import { api } from '../../core/api.js'; +import { api } from '../../ui/api.js'; import { space } from '../../../moto/space.js'; const { client } = api; diff --git a/src/kiri/mode/cam/anim-3d-fe.js b/src/kiri/mode/cam/anim-3d-fe.js index 5c36e2e8..99d4ffab 100644 --- a/src/kiri/mode/cam/anim-3d-fe.js +++ b/src/kiri/mode/cam/anim-3d-fe.js @@ -2,7 +2,7 @@ // UI FRONT END ANIMATION CODE for 3D (indexed) -import { api } from '../../core/api.js'; +import { api } from '../../ui/api.js'; import { space } from '../../../moto/space.js'; const { client } = api; diff --git a/src/kiri/mode/cam/cl-flip.js b/src/kiri/mode/cam/cl-flip.js index ec51b302..457cc5a3 100644 --- a/src/kiri/mode/cam/cl-flip.js +++ b/src/kiri/mode/cam/cl-flip.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../core/api.js'; +import { api } from '../../ui/api.js'; import { env, opAdd, opRender } from './init-ui.js'; import { CAM } from './driver-fe.js'; import { clearTabs, restoreTabs } from './cl-tab.js'; diff --git a/src/kiri/mode/cam/cl-helical.js b/src/kiri/mode/cam/cl-helical.js index dc8cda1d..4ca086d6 100644 --- a/src/kiri/mode/cam/cl-helical.js +++ b/src/kiri/mode/cam/cl-helical.js @@ -1,4 +1,4 @@ -import { api } from "../../core/api.js"; +import { api } from '../../ui/api.js'; import { CAM } from "./driver-fe.js"; import { clearPops, env } from "./init-ui.js"; diff --git a/src/kiri/mode/cam/cl-hole.js b/src/kiri/mode/cam/cl-hole.js index ecf4abd9..80f1c292 100644 --- a/src/kiri/mode/cam/cl-hole.js +++ b/src/kiri/mode/cam/cl-hole.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../core/api.js'; +import { api } from '../../ui/api.js'; import { env, clearPops } from './init-ui.js'; import { CAM } from './driver-fe.js'; diff --git a/src/kiri/mode/cam/cl-ops.js b/src/kiri/mode/cam/cl-ops.js index 15a6b660..5a235e95 100644 --- a/src/kiri/mode/cam/cl-ops.js +++ b/src/kiri/mode/cam/cl-ops.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { $, h } from '../../../moto/webui.js'; -import { api } from '../../core/api.js'; +import { api } from '../../ui/api.js'; import { env, opRender } from './init-ui.js'; import { Tool } from './tool.js'; import { opFlip } from './cl-flip.js'; diff --git a/src/kiri/mode/cam/cl-origin.js b/src/kiri/mode/cam/cl-origin.js index 9c3e485b..187ec947 100644 --- a/src/kiri/mode/cam/cl-origin.js +++ b/src/kiri/mode/cam/cl-origin.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../core/api.js'; +import { api } from '../../ui/api.js'; import { env, clearPops } from './init-ui.js'; import { CAM } from './driver-fe.js'; import { addbox, clearboxes } from '../../core/boxes.js'; diff --git a/src/kiri/mode/cam/cl-stock.js b/src/kiri/mode/cam/cl-stock.js index d4b71b79..b9fc429e 100644 --- a/src/kiri/mode/cam/cl-stock.js +++ b/src/kiri/mode/cam/cl-stock.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../core/api.js'; +import { api } from '../../ui/api.js'; import { env, isDark } from './init-ui.js'; import { space as SPACE } from '../../../moto/space.js'; diff --git a/src/kiri/mode/cam/cl-surface.js b/src/kiri/mode/cam/cl-surface.js index 0c168432..670ad823 100644 --- a/src/kiri/mode/cam/cl-surface.js +++ b/src/kiri/mode/cam/cl-surface.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../core/api.js'; +import { api } from '../../ui/api.js'; import { env, clearPops } from './init-ui.js'; import { CAM } from './driver-fe.js'; diff --git a/src/kiri/mode/cam/cl-tab.js b/src/kiri/mode/cam/cl-tab.js index c30fd266..a406920d 100644 --- a/src/kiri/mode/cam/cl-tab.js +++ b/src/kiri/mode/cam/cl-tab.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../core/api.js'; +import { api } from '../../ui/api.js'; import { env, isDark } from './init-ui.js'; import { addbox, delbox, clearboxes } from '../../core/boxes.js'; import { traceDone } from './cl-trace.js'; diff --git a/src/kiri/mode/cam/cl-trace.js b/src/kiri/mode/cam/cl-trace.js index 8a77995c..9f89cd47 100644 --- a/src/kiri/mode/cam/cl-trace.js +++ b/src/kiri/mode/cam/cl-trace.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../core/api.js'; +import { api } from '../../ui/api.js'; import { env, clearPops, isDark } from './init-ui.js'; import { CAM } from './driver-fe.js'; import { Layers } from '../../ui/layers.js'; diff --git a/src/kiri/mode/cam/driver-fe.js b/src/kiri/mode/cam/driver-fe.js index ed3d1c46..3ae2cf34 100644 --- a/src/kiri/mode/cam/driver-fe.js +++ b/src/kiri/mode/cam/driver-fe.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../core/api.js'; +import { api } from '../../ui/api.js'; import { codec } from '../../core/codec.js'; function surface_prep(index, ondone) { diff --git a/src/kiri/mode/cam/init-menu.js b/src/kiri/mode/cam/init-menu.js index f179aa7b..7efb902b 100644 --- a/src/kiri/mode/cam/init-menu.js +++ b/src/kiri/mode/cam/init-menu.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../core/api.js'; +import { api } from '../../ui/api.js'; import { originReset, originSelect } from './cl-origin.js'; import { updateTool } from './tools.js'; diff --git a/src/kiri/mode/cam/init-ui.js b/src/kiri/mode/cam/init-ui.js index b8839152..1b44574c 100644 --- a/src/kiri/mode/cam/init-ui.js +++ b/src/kiri/mode/cam/init-ui.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { $ } from '../../../moto/webui.js'; -import { api } from '../../core/api.js'; +import { api } from '../../ui/api.js'; import { load } from '../../../load/file.js'; import { animate as anim_2d, animate_clear as anim_2d_clear } from './anim-2d-fe.js'; import { animate2 as anim_3d, animate_clear2 as anim_3d_clear } from './anim-3d-fe.js'; diff --git a/src/kiri/mode/cam/tools.js b/src/kiri/mode/cam/tools.js index b542415a..8e6c6de5 100644 --- a/src/kiri/mode/cam/tools.js +++ b/src/kiri/mode/cam/tools.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { $ } from '../../../moto/webui.js'; -import { api } from '../../core/api.js'; +import { api } from '../../ui/api.js'; import { consts } from '../../core/consts.js'; import { settings as setconf } from '../../ui/config/manager.js'; import { Tool, calcTaperAngle, calcTaperBallExtent, calcTaperLength } from './tool.js'; diff --git a/src/kiri/mode/fdm/init-menu.js b/src/kiri/mode/fdm/init-menu.js index e04dc093..f052348b 100644 --- a/src/kiri/mode/fdm/init-menu.js +++ b/src/kiri/mode/fdm/init-menu.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../core/api.js'; +import { api } from '../../ui/api.js'; let LANG = api.language.current; let { FDM } = api.const.MODES, diff --git a/src/kiri/mode/fdm/init-ui.js b/src/kiri/mode/fdm/init-ui.js index b403ad8d..9ccbeb7a 100644 --- a/src/kiri/mode/fdm/init-ui.js +++ b/src/kiri/mode/fdm/init-ui.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { $ } from '../../../moto/webui.js'; -import { api } from '../../core/api.js'; +import { api } from '../../ui/api.js'; import { conf } from '../../ui/config/defaults.js'; import { util } from '../../../geo/base.js'; import { client } from '../../ui/client.js'; diff --git a/src/kiri/mode/laser/init-menu.js b/src/kiri/mode/laser/init-menu.js index 824fe6aa..16f141e5 100644 --- a/src/kiri/mode/laser/init-menu.js +++ b/src/kiri/mode/laser/init-menu.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../core/api.js'; +import { api } from '../../ui/api.js'; let LANG = api.language.current; let { LASER, DRAG, WJET, WEDM } = api.const.MODES, diff --git a/src/kiri/mode/sla/init-menu.js b/src/kiri/mode/sla/init-menu.js index fa45d671..c275596a 100644 --- a/src/kiri/mode/sla/init-menu.js +++ b/src/kiri/mode/sla/init-menu.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../core/api.js'; +import { api } from '../../ui/api.js'; let LANG = api.language.current; let { SLA } = api.const.MODES, diff --git a/src/kiri/mode/sla/init-ui.js b/src/kiri/mode/sla/init-ui.js index 0d1f849d..cc2a361e 100644 --- a/src/kiri/mode/sla/init-ui.js +++ b/src/kiri/mode/sla/init-ui.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { $ } from '../../../moto/webui.js'; -import { api } from '../../core/api.js'; +import { api } from '../../ui/api.js'; export const SLA = { init, diff --git a/src/kiri/mode/wedm/init-ui.js b/src/kiri/mode/wedm/init-ui.js index cf5db035..1a358f1b 100644 --- a/src/kiri/mode/wedm/init-ui.js +++ b/src/kiri/mode/wedm/init-ui.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../core/api.js'; +import { api } from '../../ui/api.js'; const state = { api: undefined, diff --git a/src/kiri/run/engine.js b/src/kiri/run/engine.js index f1b49fc7..67a93d8c 100644 --- a/src/kiri/run/engine.js +++ b/src/kiri/run/engine.js @@ -3,7 +3,7 @@ import '../../add/array.js'; import '../../add/class.js'; import '../../add/three.js'; -import { api } from '../core/api.js'; +import { api } from '../ui/api.js'; import { conf } from '../ui/config/defaults.js'; import { client } from '../ui/client.js'; import { load } from '../../load/file.js'; diff --git a/src/kiri/ui/alerts.js b/src/kiri/ui/alerts.js index e596b985..5cf3c46d 100644 --- a/src/kiri/ui/alerts.js +++ b/src/kiri/ui/alerts.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../core/api.js'; +import { api } from './api.js'; let alerts = []; diff --git a/src/kiri/core/api.js b/src/kiri/ui/api.js similarity index 89% rename from src/kiri/core/api.js rename to src/kiri/ui/api.js index 8ed318c5..4536f468 100644 --- a/src/kiri/core/api.js +++ b/src/kiri/ui/api.js @@ -1,31 +1,31 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import alerts from '../ui/alerts.js'; -import settings from '../ui/config/manager.js'; -import STACKS from '../ui/stacks.js'; +import alerts from './alerts.js'; +import settings from './config/manager.js'; +import STACKS from './stacks.js'; import { broker } from '../../moto/broker.js'; -import { client as work } from '../ui/client.js'; -import { consts, COLOR as color, LISTS as lists } from './consts.js'; -import { device, devices } from '../ui/devices.js'; +import { client as work } from './client.js'; +import { consts, COLOR as color, LISTS as lists } from '../core/consts.js'; +import { device, devices } from './devices.js'; import { catalog, dialog, event, group, help, hide, image } 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 { noop, ajax, o2js, js2o } from '../core/utils.js'; import { functions } from './function.js'; import { platform } from './platform.js'; -import { selection } from './selection.js'; -import { stats } from '../ui/stats.js'; -import { newWidget } from './widget.js'; -import { widgets } from './widgets.js'; +import { selection } from './select.js'; +import { stats } from './stats.js'; +import { newWidget } from '../core/widget.js'; +import { widgets } from '../core/widgets.js'; import { updateTool } from '../mode/cam/tools.js'; import { beta, version } from '../../moto/license.js'; import { space as SPACE } from '../../moto/space.js'; import { types as load } from '../../load/file.js'; -import { LANG } from '../ui/lang.js'; +import { LANG } from './lang.js'; import { LOCAL, SETUP, SECURE } from './main.js'; -import { UI } from '../ui/component.js'; +import { UI } from './component.js'; import web from '../../moto/webui.js'; diff --git a/src/kiri/ui/client.js b/src/kiri/ui/client.js index 84208ed2..2f6a564f 100644 --- a/src/kiri/ui/client.js +++ b/src/kiri/ui/client.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../core/api.js'; +import { api } from './api.js'; import { noop } from '../core/utils.js'; import { codec } from '../core/codec.js'; import { Widget } from '../core/widget.js'; diff --git a/src/kiri/ui/component.js b/src/kiri/ui/component.js index 994484e9..d4edf9e2 100644 --- a/src/kiri/ui/component.js +++ b/src/kiri/ui/component.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { $ } from '../../moto/webui.js'; -import { api } from '../core/api.js'; +import { api } from './api.js'; let DOC = self.document, inputAction = null, diff --git a/src/kiri/ui/config/manager.js b/src/kiri/ui/config/manager.js index edf14e67..a5cbe5cc 100644 --- a/src/kiri/ui/config/manager.js +++ b/src/kiri/ui/config/manager.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { $, h } from '../../../moto/webui.js'; -import { api } from '../../core/api.js'; +import { api } from '../api.js'; import { conf } from './defaults.js'; import { codec } from '../../core/codec.js'; import { consts } from '../../core/consts.js'; diff --git a/src/kiri/ui/devices.js b/src/kiri/ui/devices.js index 7586687a..a3366f72 100644 --- a/src/kiri/ui/devices.js +++ b/src/kiri/ui/devices.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { $, h } from '../../moto/webui.js'; -import { api } from '../core/api.js'; +import { api } from './api.js'; import { conf } from './config/defaults.js'; import { space } from '../../moto/space.js'; import { devices as devlist } from '../../pack/kiri-devs.js'; diff --git a/src/kiri/ui/do.js b/src/kiri/ui/do.js index a9f03c81..b3b998c6 100644 --- a/src/kiri/ui/do.js +++ b/src/kiri/ui/do.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../core/api.js'; +import { api } from './api.js'; import { space } from '../../moto/space.js'; const { event } = api; diff --git a/src/kiri/ui/export.js b/src/kiri/ui/export.js index 02fa1367..7409581e 100644 --- a/src/kiri/ui/export.js +++ b/src/kiri/ui/export.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { $ } from '../../moto/webui.js'; -import { api } from '../core/api.js'; +import { api } from './api.js'; import { client } from './client.js'; import { local } from '../../data/local.js'; import { util } from '../../geo/base.js'; diff --git a/src/kiri/ui/frame.js b/src/kiri/ui/frame.js index 634b5857..41727b39 100644 --- a/src/kiri/ui/frame.js +++ b/src/kiri/ui/frame.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../core/api.js'; +import { api } from './api.js'; import { load } from '../../load/file.js'; import { newWidget } from '../core/widget.js'; import { VIEWS } from '../core/consts.js'; diff --git a/src/kiri/core/function.js b/src/kiri/ui/function.js similarity index 98% rename from src/kiri/core/function.js rename to src/kiri/ui/function.js index 72e7385d..c3268eb3 100644 --- a/src/kiri/core/function.js +++ b/src/kiri/ui/function.js @@ -1,11 +1,11 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { api } from './api.js'; -import { client } from '../ui/client.js'; -import { codec } from './codec.js'; +import { client } from './client.js'; +import { codec } from '../core/codec.js'; import { space } from '../../moto/space.js'; -import { COLOR, PMODES } from './consts.js'; -import { exportFile } from '../ui/export.js'; +import { COLOR, PMODES } from '../core/consts.js'; +import { exportFile } from './export.js'; let complete = {}; diff --git a/src/kiri/core/init.js b/src/kiri/ui/init.js similarity index 99% rename from src/kiri/core/init.js rename to src/kiri/ui/init.js index 19ae5441..2626f868 100644 --- a/src/kiri/core/init.js +++ b/src/kiri/ui/init.js @@ -4,16 +4,16 @@ import { $, $c } from '../../moto/webui.js'; import { api } from './api.js'; import { base } from '../../geo/base.js'; import { local as sdb } from '../../data/local.js'; -import { slider } from '../ui/slider.js'; -import { keyboard } from '../ui/keyboard.js'; -import { modal } from '../ui/modal.js'; -import { interact } from '../ui/interact.js'; +import { slider } from './slider.js'; +import { keyboard } from './keyboard.js'; +import { modal } from './modal.js'; +import { interact } from './interact.js'; import { space } from '../../moto/space.js'; import { LOCAL, SETUP } from './main.js'; -import { VIEWS, MODES, SEED } from './consts.js'; +import { VIEWS, MODES, SEED } from '../core/consts.js'; import { beta, version } from '../../moto/license.js'; -import { settings as set_ctrl } from '../ui/config/manager.js'; -import STACKS from '../ui/stacks.js'; +import { settings as set_ctrl } from './config/manager.js'; +import STACKS from './stacks.js'; import { init as initCAM } from '../mode/cam/init-ui.js'; import { init as initDRAG } from '../mode/drag/init-ui.js'; import { init as initFDM } from '../mode/fdm/init-ui.js'; diff --git a/src/kiri/core/main.js b/src/kiri/ui/main.js similarity index 98% rename from src/kiri/core/main.js rename to src/kiri/ui/main.js index 2679bd8b..546edad3 100644 --- a/src/kiri/core/main.js +++ b/src/kiri/ui/main.js @@ -1,27 +1,27 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import '../ui/frame.js'; +import './frame.js'; import '../../ext/base64.js'; -import STACKS from '../ui/stacks.js'; -import { modal } from '../ui/modal.js'; +import STACKS from './stacks.js'; +import { modal } from './modal.js'; import { $ } from '../../moto/webui.js'; import { api } from './api.js'; import { broker } from '../../moto/broker.js'; -import { consts } from './consts.js'; +import { consts } from '../core/consts.js'; import { Index } from '../../data/index.js'; import { local as dataLocal } from '../../data/local.js'; -import { openFiles } from '../ui/files.js'; +import { openFiles } from './files.js'; import { platform } from './platform.js'; -import { selection } from './selection.js'; -import { settings } from '../ui/config/manager.js'; -import { showDevices } from '../ui/devices.js'; +import { selection } from './select.js'; +import { settings } from './config/manager.js'; +import { showDevices } from './devices.js'; import { space as SPACE } from '../../moto/space.js'; -import { stats } from '../ui/stats.js'; -import { noop, utils } from './utils.js'; +import { stats } from './stats.js'; +import { noop, utils } from '../core/utils.js'; import { version } from '../../moto/license.js'; -import { Widget, newWidget } from './widget.js'; +import { Widget, newWidget } from '../core/widget.js'; import { showTools } from '../mode/cam/tools.js'; let { parseOpt, o2js, js2o, ls2o } = utils, diff --git a/src/kiri/core/platform.js b/src/kiri/ui/platform.js similarity index 99% rename from src/kiri/core/platform.js rename to src/kiri/ui/platform.js index dae6cd2b..46864d0e 100644 --- a/src/kiri/core/platform.js +++ b/src/kiri/ui/platform.js @@ -2,14 +2,14 @@ import { $, h } from '../../moto/webui.js'; import { api } from './api.js'; -import { ajax, js2o } from './utils.js'; +import { ajax, js2o } from '../core/utils.js'; import { base } from '../../geo/base.js'; import { load } from '../../load/file.js'; import { space } from '../../moto/space.js'; -import { Widget, newWidget } from './widget.js'; -import { Packer } from '../ui/pack.js'; +import { Widget, newWidget } from '../core/widget.js'; +import { Packer } from './pack.js'; -import { COLOR, MODES } from './consts.js'; +import { COLOR, MODES } from '../core/consts.js'; import { THREE } from '../../ext/three.js'; const V0 = new THREE.Vector3(0,0,0); diff --git a/src/kiri/core/selection.js b/src/kiri/ui/select.js similarity index 100% rename from src/kiri/core/selection.js rename to src/kiri/ui/select.js diff --git a/src/kiri/ui/stacks.js b/src/kiri/ui/stacks.js index 541686fb..a45f862e 100644 --- a/src/kiri/ui/stacks.js +++ b/src/kiri/ui/stacks.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { $ } from '../../moto/webui.js'; -import { api } from '../core/api.js'; +import { api } from './api.js'; import { Stack } from './stack.js'; let freeMem = true, diff --git a/src/main/kiri.js b/src/main/kiri.js index a733c879..3827e559 100644 --- a/src/main/kiri.js +++ b/src/main/kiri.js @@ -7,7 +7,7 @@ import '../kiri/ui/lang.js'; import '../kiri/core/lang-en.js'; import { broker } from '../moto/broker.js'; -import { run } from '../kiri/core/init.js'; +import { run } from '../kiri/ui/init.js'; let traceload = location.search.indexOf('traceload') > 0; let load = []; From 8af2fec11bc19dee387aef556fccc11f547033c1 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Wed, 24 Dec 2025 23:38:22 -0500 Subject: [PATCH 019/154] splitting functionality out of main --- src/kiri/ui/api.js | 2 + src/kiri/ui/groups.js | 17 + src/kiri/ui/help.js | 28 ++ src/kiri/ui/image.js | 84 +++++ src/kiri/ui/init.js | 13 +- src/kiri/ui/main.js | 659 ++------------------------------------ src/kiri/ui/mode.js | 111 +++++++ src/kiri/ui/view-state.js | 77 +++++ src/kiri/ui/visuals.js | 181 +++++++++++ src/kiri/ui/workspace.js | 177 ++++++++++ 10 files changed, 722 insertions(+), 627 deletions(-) create mode 100644 src/kiri/ui/groups.js create mode 100644 src/kiri/ui/help.js create mode 100644 src/kiri/ui/image.js create mode 100644 src/kiri/ui/mode.js create mode 100644 src/kiri/ui/view-state.js create mode 100644 src/kiri/ui/visuals.js create mode 100644 src/kiri/ui/workspace.js diff --git a/src/kiri/ui/api.js b/src/kiri/ui/api.js index 4536f468..e882474c 100644 --- a/src/kiri/ui/api.js +++ b/src/kiri/ui/api.js @@ -3,6 +3,7 @@ import alerts from './alerts.js'; import settings from './config/manager.js'; import STACKS from './stacks.js'; +import { visuals } from './visuals.js'; import { broker } from '../../moto/broker.js'; import { client as work } from './client.js'; @@ -191,6 +192,7 @@ export const api = { }, version, view, + visuals, web, widgets, work, diff --git a/src/kiri/ui/groups.js b/src/kiri/ui/groups.js new file mode 100644 index 00000000..935b347d --- /dev/null +++ b/src/kiri/ui/groups.js @@ -0,0 +1,17 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { selection } from './select.js'; +import { Widget } from '../core/widget.js'; + +function groupMerge() { + Widget.Groups.merge(selection.widgets(true)); +} + +function groupSplit() { + Widget.Groups.split(selection.widgets(false)); +} + +export const group = { + merge: groupMerge, + split: groupSplit, +}; diff --git a/src/kiri/ui/help.js b/src/kiri/ui/help.js new file mode 100644 index 00000000..aa01c6a0 --- /dev/null +++ b/src/kiri/ui/help.js @@ -0,0 +1,28 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { $ } from '../../moto/webui.js'; +import { api } from './api.js'; +import { modal } from './modal.js'; +import { version } from '../../moto/license.js'; + +const WIN = self.window; + +function showHelp() { + showHelpFile(`local`,() => {}); +} + +function showHelpFile(local,then) { + if (!local) { + WIN.open("//docs.grid.space/", "_help"); + return; + } + const LANG = api.language.current; + $('kiri-version').innerHTML = `${LANG.version} ${version}`; + modal.show('help'); + api.event.emit('help.show', local); +} + +export const help = { + show: showHelp, + file: showHelpFile +}; diff --git a/src/kiri/ui/image.js b/src/kiri/ui/image.js new file mode 100644 index 00000000..98397779 --- /dev/null +++ b/src/kiri/ui/image.js @@ -0,0 +1,84 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { $ } from '../../moto/webui.js'; +import { api } from './api.js'; +import { newWidget } from '../core/widget.js'; +import { platform } from './platform.js'; +import { settings } from './config/manager.js'; + +function loadImageDialog(image, name, force) { + if (!force && image.byteLength > 2500000) { + return api.uc.confirm("Large images may fail to import
Consider resizing under 1000 x 1000
Proceed with import?").then(ok => { + if (ok) { + loadImageDialog(image, name, true); + } + }); + } + const opt = {pre: [ + "
", + "

Image Conversion

", + "

", + " This will create a 3D model from a 2D PNG image. Photos must", + " be blurred to be usable. Values from 0=off to 50=high are suggested.", + " Higher values incur more processing time.", + "

", + "
", + " ", + " ", + " ", + " ", + " ", + " ", + "
blur value invert image
base size invert alpha
border size
", + "
" + ]}; + api.uc.confirm(undefined, {convert:true, cancel:false}, undefined, opt).then((ok) => { + if (ok) { + loadImage(image, { + file: name, + blur: parseInt($('png-blur').value) || 0, + base: parseInt($('png-base').value) || 0, + border: parseInt($('png-border').value) || 0, + inv_image: $('png-inv').checked, + inv_alpha: $('alpha-inv').checked + }); + } + }); +} + +function loadImage(image, opt = {}) { + const info = Object.assign({settings: settings.get(), png:image}, opt); + api.client.image2mesh(info, progress => { + api.show.progress(progress, "converting"); + }, vertices => { + api.show.progress(0); + const widget = newWidget().loadVertices(vertices); + widget.meta.file = opt.file; + platform.add(widget); + }); +} + +// convert any image type to png +function loadImageConvert(res, name) { + let url = URL.createObjectURL(new Blob([res])); + + $('mod-any').innerHTML = ``; + + let img = $('xsrc'); + let can = $('xdst'); + + img.onload = () => { + can.width = img.width; + can.height = img.height; + let ctx = can.getContext('2d'); + ctx.drawImage(img, 0, 0); + fetch(can.toDataURL()).then(r => r.arrayBuffer()).then(data => { + loadImageDialog(data, name); + }); + }; +} + +export const image = { + dialog: loadImageDialog, + convert: loadImageConvert +}; diff --git a/src/kiri/ui/init.js b/src/kiri/ui/init.js index 2626f868..e392d5f0 100644 --- a/src/kiri/ui/init.js +++ b/src/kiri/ui/init.js @@ -534,13 +534,20 @@ function isNotBelt() { // MAIN INITIALIZATION FUNCTION function init_one() { let { event, conf, view, show } = api, - { bound, toInt, toFloat } = uc, { newBlank, newButton, newBoolean, newGroup, newInput } = uc, - { newSelect, newLabel, newValue, newRow, newGCode, newDiv } = uc; + { newSelect, newRow, newGCode, newDiv, toInt, toFloat } = uc; event.emit('init.one'); - // ensure we have settings from last session + // restore kiri-init vars + let inits = parseInt(sdb.getItem('kiri-init') || stats.get('init') || 0) + 1; + + // update version and init count + sdb.setItem('kiri-init', inits); + stats.set('init', inits); + stats.set('kiri', version); + + // restore settings from last saved session conf.restore(); let container = $('container'), diff --git a/src/kiri/ui/main.js b/src/kiri/ui/main.js index 546edad3..88a9ec58 100644 --- a/src/kiri/ui/main.js +++ b/src/kiri/ui/main.js @@ -1,31 +1,30 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import './frame.js'; import '../../ext/base64.js'; -import STACKS from './stacks.js'; import { modal } from './modal.js'; import { $ } from '../../moto/webui.js'; import { api } from './api.js'; import { broker } from '../../moto/broker.js'; -import { consts } from '../core/consts.js'; import { Index } from '../../data/index.js'; -import { local as dataLocal } from '../../data/local.js'; import { openFiles } from './files.js'; -import { platform } from './platform.js'; -import { selection } from './select.js'; import { settings } from './config/manager.js'; import { showDevices } from './devices.js'; -import { space as SPACE } from '../../moto/space.js'; -import { stats } from './stats.js'; import { noop, utils } from '../core/utils.js'; import { version } from '../../moto/license.js'; -import { Widget, newWidget } from '../core/widget.js'; import { showTools } from '../mode/cam/tools.js'; -let { parseOpt, o2js, js2o, ls2o } = utils, - { COLOR, MODES, VIEWS } = consts, +// Import new modules +import { workspace } from './workspace.js'; +import { mode as modeModule, process as processModule } from './mode.js'; +import { help as helpModule } from './help.js'; +import { group as groupModule } from './groups.js'; +import { image as imageModule } from './image.js'; +import { view as viewModule } from './view-state.js'; +import { visuals } from './visuals.js'; + +let { parseOpt } = utils, WIN = self.window, DOC = self.document, LOC = self.location, @@ -33,18 +32,7 @@ let { parseOpt, o2js, js2o, ls2o } = utils, SECURE = isSecure(LOC.protocol), LOCAL = self.debug && !SETUP.remote, EVENT = broker, - SDB = dataLocal, - FILES = openFiles(new Index(SETUP.d ? SETUP.d[0] : 'kiri')), - clone = Object.clone, - MODE = MODES.FDM, - viewMode = VIEWS.ARRANGE, - autoSaveTimer = null, - inits = parseInt(SDB.getItem('kiri-init') || stats.get('init') || 0) + 1; - -// update version and init count -SDB.setItem('kiri-init', inits); -stats.set('init', inits); -stats.set('kiri', version); + FILES = openFiles(new Index(SETUP.d ? SETUP.d[0] : 'kiri')); // allow widget to straddle client / worker FOR NOW self.kiri_catalog = FILES; @@ -55,10 +43,8 @@ export const dialog = { update_process_list: updateProcessList }; -export const help = { - show: showHelp, - file: showHelpFile -}; +// Re-export help from new module +export const help = helpModule; export const event = { on(t,l) { return EVENT.on(t,l) }, @@ -69,74 +55,39 @@ export const event = { settings: triggerSettingsEvent }; -export const group = { - merge: groupMerge, - split: groupSplit, -}; +// Re-export group from new module +export const group = groupModule; export const hide = { alert(rec, recs) { api.alerts.hide(...arguments) }, import: noop, - slider: hideSlider + slider: visuals.hide_slider }; -export const image = { - dialog: loadImageDialog, - convert: loadImageConvert -}; +// Re-export image from new module +export const image = imageModule; // Re-export modal singleton methods export { modal }; -export const mode = { - get_id() { return MODE }, - get_lower: getModeLower, - get: getMode, - set: setMode, - switch: switchMode, - set_expert: noop, - is_fdm() { return MODE === MODES.FDM }, - is_cam() { return MODE === MODES.CAM }, - is_sla() { return MODE === MODES.SLA }, - is_drag() { return MODE === MODES.DRAG }, - is_wedm() { return MODE === MODES.WEDM }, - is_wjet() { return MODE === MODES.WJET }, - is_laser() { return MODE === MODES.LASER }, - is_2d() { return false || - api.mode.is_drag() || - api.mode.is_wedm() || - api.mode.is_wjet() || - api.mode.is_laser() - } -}; - -export const process = { - code: currentProcessCode, - get: currentProcessName -}; +// Re-export mode and process from new module +export const mode = modeModule; +export const process = processModule; export const show = { alert() { return api.alerts.show(...arguments) }, - controls: setControlsVisible, + controls() { console.trace('deprecated') }, devices: showDevices, import() { api.ui.import.style.display = '' }, - layer: setVisibleLayer, - local: showLocal, + layer: visuals.set_visible_layer, + local() { console.trace('deprecated') }, progress: setProgress, - slices: showSlices, + slices: visuals.show_slices, tools: showTools }; -export const space = { - reload, - auto_save, - restore: restoreWorkspace, - clear: clearWorkspace, - save: saveWorkspace, - set_focus: setFocus, - update: SPACE.update, - is_dark() { return settings.ctrl().dark } -}; +// Re-export space (workspace) from new module +export const space = workspace; export const util = { isSecure, @@ -147,26 +98,11 @@ export const util = { b64dec(obj) { return JSON.parse(new TextDecoder().decode(base64js.toByteArray(obj))) } }; +// Combine view-state and visuals modules export const view = { - get() { return viewMode }, - set() { setViewMode(...arguments) }, - set_arrange() { api.view.set(VIEWS.ARRANGE) }, - set_slice() { api.view.set(VIEWS.SLICE) }, - set_preview() { api.view.set(VIEWS.PREVIEW) }, - set_animate() { api.view.set(VIEWS.ANIMATE) }, - is_arrange() { return viewMode === VIEWS.ARRANGE }, - is_slice() { return viewMode === VIEWS.SLICE }, - is_preview() { return viewMode === VIEWS.PREVIEW }, - is_animate() { return viewMode === VIEWS.ANIMATE }, - update_stack_labels: updateStackLabelState, - update_slider_max: updateSliderMax, - update_slider: updateSlider, - update_speeds: updateSpeeds, - hide_slices: hideSlices, - snapshot: null, - edges: setEdges, - unit_scale: unitScale, - wireframe: setWireframe + ...viewModule, + ...visuals, + snapshot: null }; // add show() to catalog for API @@ -178,80 +114,10 @@ EVENT.on = (topic, listener) => { return EVENT; }; -function updateStackLabelState() { - const settings = api.conf.get(); - // match label checkboxes to preference - for (let label of api.stacks.getLabels()) { - let check = `${settings.mode}-${api.view.get()}-${label}`; - api.stacks.setVisible(label, settings.labels[check] !== false); - } -} - -function setFocus(sel, point) { - if (point) { - SPACE.platform.setCenter(point.x, point.z, point.y); - SPACE.view.setFocus(new THREE.Vector3(point.x, point.y, point.z)); - return; - } - if (sel === undefined) { - sel = api.widgets.all(); - } else if (!Array.isArray) { - sel = [ sel ]; - } else if (sel.length === 0) { - sel = api.widgets.all(); - } - let pos = { x:0, y:0, z:0 }; - for (let widget of sel) { - pos.x += widget.track.pos.x; - pos.y += widget.track.pos.y; - pos.z += widget.track.pos.z; - } - if (sel.length) { - pos.x /= sel.length; - pos.y /= sel.length; - pos.z /= sel.length; - } - let cam_index = api.conf.get().process.camStockIndexed || false; - let focus_z = cam_index ? 0 : platform.top_z() / 2; - SPACE.platform.setCenter(pos.x, -pos.y, focus_z); - SPACE.view.setFocus(new THREE.Vector3(pos.x, focus_z, -pos.y)); -} - -function reload() { - api.event.emit('reload'); - do_reload(100); -} - -function do_reload(time) { - // allow time for async saves to complete and busy to to to zero - setTimeout(() => { - if (api.busy.val() === 0) { - LOC.reload(); - } else { - console.log(`reload deferred on busy=${api.busy.val()}`); - do_reload(250); - } - }, time || 100); -} - -function auto_save() { - if (!settings.ctrl().autoSave) { - return; - } - clearTimeout(autoSaveTimer); - autoSaveTimer = setTimeout(() => { - api.space.save(true); - }, 1000); -} - /** ****************************************************************** * Utility Functions ******************************************************************* */ -function unitScale() { - return api.mode.is_cam() && settings.ctrl().units === 'in' ? 25.4 : 1; -} - function triggerSettingsEvent() { api.event.emit('settings', settings.get()); } @@ -274,234 +140,6 @@ function setProgress(value = 0, msg) { } } -function bound(v,min,max) { - return Math.max(min,Math.min(max,v)); -} - -function updateSpeeds(maxSpeed, minSpeed) { - const { ui } = api; - ui.speeds.style.display = - maxSpeed && - settings.mode !== 'SLA' && - settings.mode !== 'LASER' && - viewMode === VIEWS.PREVIEW && - ui.showSpeeds.checked ? 'block' : ''; - if (maxSpeed) { - const colors = []; - for (let i = 0; i <= maxSpeed; i += maxSpeed / 20) { - colors.push(Math.round(Math.max(i, 1))); - } - api.client.colors(colors, maxSpeed, speedColors => { - const list = []; - Object.keys(speedColors).map(v => parseInt(v)).sort((a, b) => b - a).forEach(speed => { - const color = speedColors[speed]; - const hex = color.toString(16).padStart(6, 0); - const r = (color >> 16) & 0xff; - const g = (color >> 8) & 0xff; - const b = (color >> 0) & 0xff; - const style = `background-color:#${hex}`; - list.push(``); - }); - ui.speedbar.innerHTML = list.join(''); - }); - api.event.emit('preview.speeds', { - min: minSpeed, - max: maxSpeed - }); - } -} - -function updateSlider() { - const { lo, hi, max } = api.slider.getRange(); - if (max > 0) { - api.slider.updatePosition(lo / max, hi / max); - } - api.conf.update_fields_from_range(); -} - -function setVisibleLayer(h, l) { - const { lo, hi, max } = api.slider.getRange(); - h = h >= 0 ? h : hi; - l = l >= 0 ? l : lo; - const newHi = bound(h, 0, max); - const newLo = bound(l, 0, newHi); - api.slider.setRange(newLo, newHi); // Notify callbacks (STACKS, scene updates) - api.slider.showLabels(); - showSlices(); -} - -function setWireframe(bool, color, opacity) { - api.widgets.each(function(w) { w.setWireframe(bool, color, opacity) }); - SPACE.update(); -} - -function setEdges(bool) { - if (bool && bool.toggle) { - api.local.toggle('model.edges'); - } else { - api.local.set('model.edges', bool); - } - bool = api.local.getBoolean('model.edges'); - api.widgets.each(w => w.setEdges(bool)); - SPACE.update(); -} - -function updateSliderMax(set) { - let max = STACKS.getRange().tallest - 1; - api.slider.setMax(max); - const { hi } = api.slider.getRange(); - if (set || max < hi) { - api.slider.setRange(max, max); - api.slider.showLabels(); - } -} - -function hideSlices() { - STACKS.clear(); - api.widgets.setOpacity(COLOR.model_opacity); - api.widgets.each(function(widget) { - widget.setWireframe(false); - }); -} - -function setWidgetVisibility(bool) { - api.widgets.each(w => { - if (bool) { - w.show(); - } else { - w.hide(); - } - }); -} - -/** - * hide or show slice-layers and their sub-elements - * - * @param {number} [layer] - */ -function showSlices(layer) { - if (viewMode === VIEWS.ARRANGE) { - return; - } - - showSlider(); - - const { lo, hi, max } = api.slider.getRange(); - - if (typeof(layer) === 'string' || typeof(layer) === 'number') { - layer = parseInt(layer); - } else { - layer = hi; - } - - layer = bound(layer, 0, max); - const newLo = layer < lo ? layer : lo; - api.slider.setRange(newLo, layer); - api.slider.showLabels(); - - updateSlider(); - // STACKS.setRange is called by slider callback - - SPACE.update(); -} - -function showSlider() { - api.slider.show(); -} - -function hideSlider() { - api.slider.hide(); - api.ui.speeds.style.display = 'none'; -} - -function loadImageDialog(image, name, force) { - if (!force && image.byteLength > 2500000) { - return api.uc.confirm("Large images may fail to import
Consider resizing under 1000 x 1000
Proceed with import?").then(ok => { - if (ok) { - loadImageDialog(image, name, true); - } - }); - } - const opt = {pre: [ - "
", - "

Image Conversion

", - "

", - " This will create a 3D model from a 2D PNG image. Photos must", - " be blurred to be usable. Values from 0=off to 50=high are suggested.", - " Higher values incur more processing time.", - "

", - "
", - " ", - " ", - " ", - " ", - " ", - " ", - "
blur value invert image
base size invert alpha
border size
", - "
" - ]}; - api.uc.confirm(undefined, {convert:true, cancel:false}, undefined, opt).then((ok) => { - if (ok) { - loadImage(image, { - file: name, - blur: parseInt($('png-blur').value) || 0, - base: parseInt($('png-base').value) || 0, - border: parseInt($('png-border').value) || 0, - inv_image: $('png-inv').checked, - inv_alpha: $('alpha-inv').checked - }); - } - }); -} - -function loadImage(image, opt = {}) { - const info = Object.assign({settings: settings.get(), png:image}, opt); - api.client.image2mesh(info, progress => { - api.show.progress(progress, "converting"); - }, vertices => { - api.show.progress(0); - const widget = newWidget().loadVertices(vertices); - widget.meta.file = opt.file; - platform.add(widget); - }); -} - -/** ****************************************************************** - * Selection Functions - ******************************************************************* */ - -function groupMerge() { - Widget.Groups.merge(selection.widgets(true)); -} - -function groupSplit() { - Widget.Groups.split(selection.widgets(false)); -} - -/** ****************************************************************** - * Settings Functions - ******************************************************************* */ - -// convert any image type to png -function loadImageConvert(res, name) { - let url = URL.createObjectURL(new Blob([res])); - - $('mod-any').innerHTML = ``; - - let img = $('xsrc'); - let can = $('xdst'); - - img.onload = () => { - can.width = img.width; - can.height = img.height; - let ctx = can.getContext('2d'); - ctx.drawImage(img, 0, 0); - fetch(can.toDataURL()).then(r => r.arrayBuffer()).then(data => { - loadImageDialog(data, name); - }); - }; -} - function loadFile(ev) { // use modern Filesystem api when available if (false && window.showOpenFilePicker) { @@ -517,92 +155,6 @@ function loadFile(ev) { api.ui.load.click(); } -function saveWorkspace(quiet) { - api.conf.save(); - const newWidgets = []; - const oldWidgets = js2o(SDB.getItem('ws-widgets'), []); - api.widgets.each(function(widget) { - if (widget.synth) return; - newWidgets.push(widget.id); - oldWidgets.remove(widget.id); - widget.saveState(); - let ann = api.widgets.annotate(widget.id); - ann.file = widget.meta.file; - ann.url = widget.meta.url; - }); - SDB.setItem('ws-widgets', o2js(newWidgets)); - oldWidgets.forEach(wid => { - Widget.deleteFromState(wid); - }); - // eliminate dangling saved widgets - FILES.deleteFilter(key => newWidgets.indexOf(key.substring(8)) < 0, "ws-save-", "ws-savf"); - if (!quiet) { - api.show.alert("workspace saved", 1); - } -} - -function restoreWorkspace(ondone, skip_widget_load) { - let newset = api.conf.restore(false), - camera = newset.controller.view, - toload = ls2o('ws-widgets',[]), - loaded = 0, - position = true; - - api.conf.update_fields(); - platform.update_size(); - - SPACE.view.reset(); - if (camera) { - SPACE.view.load(camera); - } else { - SPACE.view.home(); - } - - if (skip_widget_load) { - if (ondone) { - ondone(); - } - return; - } - - // remove any widgets from platform - api.widgets.each(function(widget) { - platform.delete(widget); - }); - - // load any widget by name that was saved to the workspace - toload.forEach(function(widgetid) { - Widget.loadFromState(widgetid, function(widget) { - if (widget) { - platform.add(widget, 0, position, true); - let ann = api.widgets.annotate(widgetid); - widget.meta.file = ann.file; - widget.meta.url = ann.url; - } - if (++loaded === toload.length) { - platform.deselect(); - if (ondone) { - ondone(); - setTimeout(() => { - platform.update_bounds(); - SPACE.update(); - }, 1); - } - } - }, position); - }); - - return toload.length > 0; -} - -function clearWorkspace() { - // free up worker cache/mem - api.client.clear(); - platform.select_all(); - platform.delete(selection.meshes()); -} - - function editSettings(e) { let current = settings.get(), mode = getMode(), @@ -657,7 +209,7 @@ function deleteSettings(e) { function updateProcessList() { let current = settings.get(); - let list = [], s = current, sp = s.sproc[getMode()] || {}, table = api.ui.settingsList; + let list = [], s = current, sp = s.sproc[settings.mode()] || {}, table = api.ui.settingsList; table.innerHTML = ''; for (let k in sp) { if (sp.hasOwnProperty(k)) list.push(k); @@ -706,150 +258,6 @@ function updateProcessList() { }); } -function showHelp() { - showHelpFile(`local`,() => {}); -} - -function showHelpFile(local,then) { - if (!local) { - WIN.open("//docs.grid.space/", "_help"); - return; - } - const LANG = api.language.current; - $('kiri-version').innerHTML = `${LANG.version} ${version}`; - modal.show('help'); - api.event.emit('help.show', local); -} - -function showLocal() { - // deprecated -} - -function setViewMode(mode) { - const isCAM = settings.mode() === 'CAM'; - viewMode = mode; - platform.deselect(); - selection.update_info(); - // clear any bounds forced by, for example, WireEDM previews - api.platform.set_bounds(); - // disable clear in non-arrange modes - ['view-arrange','act-slice','act-preview','act-animate'].forEach(el => { - $(el).classList.remove('selected') - }); - $('render-tools').classList.add('hide'); - switch (mode) { - case VIEWS.ARRANGE: - $('view-arrange').classList.add('selected'); - $('render-tools').classList.remove('hide'); - api.function.clear_progress(); - api.client.clear(); - STACKS.clear(); - hideSlider(); - updateSpeeds(); - setVisibleLayer(); - setWidgetVisibility(true); - api.widgets.setOpacity(1); - api.view.edges(api.local.getBoolean('model.edges')); - break; - case VIEWS.SLICE: - $('act-slice').classList.add('selected'); - updateSpeeds(); - updateSliderMax(); - setWidgetVisibility(true); - !isCAM && api.view.edges(false); - break; - case VIEWS.PREVIEW: - $('act-preview').classList.add('selected'); - setWidgetVisibility(true); - !isCAM && api.view.edges(false); - break; - case VIEWS.ANIMATE: - $('act-animate').classList.add('selected'); - !isCAM && api.view.edges(false); - break; - default: - console.log("invalid view mode: "+mode); - return; - } - api.event.emit('view.set', mode); - DOC.activeElement.blur(); -} - -function getMode() { - return settings.mode(); -} - -function getModeLower() { - return getMode().toLowerCase(); -} - -function switchMode(mode) { - setMode(mode, null, platform.update_size); -} - -function setMode(mode, lock, then) { - if (!MODES[mode]) { - console.log("invalid mode: "+mode); - mode = 'FDM'; - } - const current = settings.get(); - // change mode constants - current.mode = mode; - MODE = MODES[mode]; - // gcode edit area for any non-SLA mode - api.uc.setVisible($('gcode-edit'), mode !== 'SLA'); - // highlight selected mode menu item - ["FDM","CAM","SLA","LASER","DRAG","WJET","WEDM"].forEach(sm => { - const cl = $(`mode-${sm.toLowerCase()}`).classList; - if (sm === mode) { - cl.add('selected'); - } else { - cl.remove('selected'); - } - }); - // restore cached device profile for this mode - if (current.cdev[mode]) { - current.device = clone(current.cdev[mode]); - api.event.emit('device.select', api.device.get()); - } - // hide/show - api.uc.setVisible($('set-tools'), mode === 'CAM'); - // updates right-hand menu by enabling/disabling fields - setViewMode(VIEWS.ARRANGE); - api.uc.setMode(MODE); - // sanitize and persist settings - api.conf.load(); - api.conf.save(); - // other housekeeping - triggerSettingsEvent(); - platform.update_selected(); - selection.update_bounds(api.widgets.all()); - api.conf.update_fields(); - // because device dialog, if showing, needs to be updated - if (modal.visible()) { - api.show.devices(); - } - api.space.restore(null, true); - api.event.emit("mode.set", mode); - if (then) { - then(); - } -} - -function currentProcessName() { - return settings.get().cproc[getMode()]; -} - -function currentProcessCode() { - return settings.get().sproc[getMode()][currentProcessName()]; -} - -function setControlsVisible(show) { - // TODO fix - // $('mid-left').style.display = show ? 'flex' : 'none'; - // $('mid-right').style.display = show ? 'flex' : 'none'; -} - function downloadBlob(data, filename) { let url = WIN.URL.createObjectURL(new Blob([data], {type: "octet/stream"})); $('mod-any').innerHTML = `x`; @@ -857,3 +265,6 @@ function downloadBlob(data, filename) { } export { FILES as catalog, LOCAL, SETUP, SECURE }; + +// Import frame.js last to avoid circular dependency issues +// import './frame.js'; diff --git a/src/kiri/ui/mode.js b/src/kiri/ui/mode.js new file mode 100644 index 00000000..5fdfc174 --- /dev/null +++ b/src/kiri/ui/mode.js @@ -0,0 +1,111 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { $ } from '../../moto/webui.js'; +import { api } from './api.js'; +import { consts } from '../core/consts.js'; +import { modal } from './modal.js'; +import { platform } from './platform.js'; +import { selection } from './select.js'; +import { settings } from './config/manager.js'; + +const { MODES, VIEWS } = consts; +const clone = Object.clone; + +let MODE = MODES.FDM; + +function getMode() { + return settings.mode(); +} + +function getModeLower() { + return getMode().toLowerCase(); +} + +function switchMode(mode) { + setMode(mode, null, platform.update_size); +} + +function setMode(mode, lock, then) { + if (!MODES[mode]) { + console.log("invalid mode: "+mode); + mode = 'FDM'; + } + const current = settings.get(); + // change mode constants + current.mode = mode; + MODE = MODES[mode]; + // gcode edit area for any non-SLA mode + api.uc.setVisible($('gcode-edit'), mode !== 'SLA'); + // highlight selected mode menu item + ["FDM","CAM","SLA","LASER","DRAG","WJET","WEDM"].forEach(sm => { + const cl = $(`mode-${sm.toLowerCase()}`).classList; + if (sm === mode) { + cl.add('selected'); + } else { + cl.remove('selected'); + } + }); + // restore cached device profile for this mode + if (current.cdev[mode]) { + current.device = clone(current.cdev[mode]); + api.event.emit('device.select', api.device.get()); + } + // hide/show + api.uc.setVisible($('set-tools'), mode === 'CAM'); + // updates right-hand menu by enabling/disabling fields + api.view.set(VIEWS.ARRANGE); + api.uc.setMode(MODE); + // sanitize and persist settings + api.conf.load(); + api.conf.save(); + // trigger settings event + api.event.emit('settings', settings.get()); + // other housekeeping + platform.update_selected(); + selection.update_bounds(api.widgets.all()); + api.conf.update_fields(); + // because device dialog, if showing, needs to be updated + if (modal.visible()) { + api.show.devices(); + } + api.space.restore(null, true); + api.event.emit("mode.set", mode); + if (then) { + then(); + } +} + +function currentProcessName() { + return settings.get().cproc[getMode()]; +} + +function currentProcessCode() { + return settings.get().sproc[getMode()][currentProcessName()]; +} + +export const mode = { + get_id() { return MODE }, + get_lower: getModeLower, + get: getMode, + set: setMode, + switch: switchMode, + set_expert() {}, // noop + is_fdm() { return MODE === MODES.FDM }, + is_cam() { return MODE === MODES.CAM }, + is_sla() { return MODE === MODES.SLA }, + is_drag() { return MODE === MODES.DRAG }, + is_wedm() { return MODE === MODES.WEDM }, + is_wjet() { return MODE === MODES.WJET }, + is_laser() { return MODE === MODES.LASER }, + is_2d() { return false || + mode.is_drag() || + mode.is_wedm() || + mode.is_wjet() || + mode.is_laser() + } +}; + +export const process = { + code: currentProcessCode, + get: currentProcessName +}; diff --git a/src/kiri/ui/view-state.js b/src/kiri/ui/view-state.js new file mode 100644 index 00000000..7a1024dc --- /dev/null +++ b/src/kiri/ui/view-state.js @@ -0,0 +1,77 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { $ } from '../../moto/webui.js'; +import { api } from './api.js'; +import { consts } from '../core/consts.js'; +import { platform } from './platform.js'; +import { selection } from './select.js'; +import { settings } from './config/manager.js'; +import STACKS from './stacks.js'; + +const { VIEWS } = consts; +const DOC = self.document; + +let viewMode = VIEWS.ARRANGE; + +function setViewMode(mode) { + const isCAM = settings.mode() === 'CAM'; + viewMode = mode; + platform.deselect(); + selection.update_info(); + // clear any bounds forced by, for example, WireEDM previews + api.platform.set_bounds(); + // disable clear in non-arrange modes + ['view-arrange','act-slice','act-preview','act-animate'].forEach(el => { + $(el).classList.remove('selected') + }); + $('render-tools').classList.add('hide'); + switch (mode) { + case VIEWS.ARRANGE: + $('view-arrange').classList.add('selected'); + $('render-tools').classList.remove('hide'); + api.function.clear_progress(); + api.client.clear(); + STACKS.clear(); + api.visuals.hide_slider(); + api.visuals.update_speeds(); + api.visuals.set_visible_layer(); + api.visuals.set_widget_visibility(true); + api.widgets.setOpacity(1); + api.view.edges(api.local.getBoolean('model.edges')); + break; + case VIEWS.SLICE: + $('act-slice').classList.add('selected'); + api.visuals.update_speeds(); + api.visuals.update_slider_max(); + api.visuals.set_widget_visibility(true); + !isCAM && api.view.edges(false); + break; + case VIEWS.PREVIEW: + $('act-preview').classList.add('selected'); + api.visuals.set_widget_visibility(true); + !isCAM && api.view.edges(false); + break; + case VIEWS.ANIMATE: + $('act-animate').classList.add('selected'); + !isCAM && api.view.edges(false); + break; + default: + console.log("invalid view mode: "+mode); + return; + } + api.event.emit('view.set', mode); + DOC.activeElement.blur(); +} + +export const view = { + get() { return viewMode }, + set() { setViewMode(...arguments) }, + set_arrange() { setViewMode(VIEWS.ARRANGE) }, + set_slice() { setViewMode(VIEWS.SLICE) }, + set_preview() { setViewMode(VIEWS.PREVIEW) }, + set_animate() { setViewMode(VIEWS.ANIMATE) }, + is_arrange() { return viewMode === VIEWS.ARRANGE }, + is_slice() { return viewMode === VIEWS.SLICE }, + is_preview() { return viewMode === VIEWS.PREVIEW }, + is_animate() { return viewMode === VIEWS.ANIMATE } +}; diff --git a/src/kiri/ui/visuals.js b/src/kiri/ui/visuals.js new file mode 100644 index 00000000..9b9ea0d5 --- /dev/null +++ b/src/kiri/ui/visuals.js @@ -0,0 +1,181 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { api } from './api.js'; +import { consts } from '../core/consts.js'; +import { settings } from './config/manager.js'; +import { space as SPACE } from '../../moto/space.js'; +import STACKS from './stacks.js'; + +const { COLOR, VIEWS } = consts; + +// Note: viewMode is imported dynamically via api.view.get() +function bound(v,min,max) { + return Math.max(min,Math.min(max,v)); +} + +function unitScale() { + return api.mode.is_cam() && settings.ctrl().units === 'in' ? 25.4 : 1; +} + +function updateSpeeds(maxSpeed, minSpeed) { + const { ui } = api; + const viewMode = api.view.get(); + ui.speeds.style.display = + maxSpeed && + settings.mode !== 'SLA' && + settings.mode !== 'LASER' && + viewMode === VIEWS.PREVIEW && + ui.showSpeeds.checked ? 'block' : ''; + if (maxSpeed) { + const colors = []; + for (let i = 0; i <= maxSpeed; i += maxSpeed / 20) { + colors.push(Math.round(Math.max(i, 1))); + } + api.client.colors(colors, maxSpeed, speedColors => { + const list = []; + Object.keys(speedColors).map(v => parseInt(v)).sort((a, b) => b - a).forEach(speed => { + const color = speedColors[speed]; + const hex = color.toString(16).padStart(6, 0); + const r = (color >> 16) & 0xff; + const g = (color >> 8) & 0xff; + const b = (color >> 0) & 0xff; + const style = `background-color:#${hex}`; + list.push(``); + }); + ui.speedbar.innerHTML = list.join(''); + }); + api.event.emit('preview.speeds', { + min: minSpeed, + max: maxSpeed + }); + } +} + +function updateSlider() { + const { lo, hi, max } = api.slider.getRange(); + if (max > 0) { + api.slider.updatePosition(lo / max, hi / max); + } + api.conf.update_fields_from_range(); +} + +function setVisibleLayer(h, l) { + const { lo, hi, max } = api.slider.getRange(); + h = h >= 0 ? h : hi; + l = l >= 0 ? l : lo; + const newHi = bound(h, 0, max); + const newLo = bound(l, 0, newHi); + api.slider.setRange(newLo, newHi); // Notify callbacks (STACKS, scene updates) + api.slider.showLabels(); + showSlices(); +} + +function setWireframe(bool, color, opacity) { + api.widgets.each(function(w) { w.setWireframe(bool, color, opacity) }); + SPACE.update(); +} + +function setEdges(bool) { + if (bool && bool.toggle) { + api.local.toggle('model.edges'); + } else { + api.local.set('model.edges', bool); + } + bool = api.local.getBoolean('model.edges'); + api.widgets.each(w => w.setEdges(bool)); + SPACE.update(); +} + +function updateSliderMax(set) { + let max = STACKS.getRange().tallest - 1; + api.slider.setMax(max); + const { hi } = api.slider.getRange(); + if (set || max < hi) { + api.slider.setRange(max, max); + api.slider.showLabels(); + } +} + +function hideSlices() { + STACKS.clear(); + api.widgets.setOpacity(COLOR.model_opacity); + api.widgets.each(function(widget) { + widget.setWireframe(false); + }); +} + +function setWidgetVisibility(bool) { + api.widgets.each(w => { + if (bool) { + w.show(); + } else { + w.hide(); + } + }); +} + +/** + * hide or show slice-layers and their sub-elements + * + * @param {number} [layer] + */ +function showSlices(layer) { + const viewMode = api.view.get(); + if (viewMode === VIEWS.ARRANGE) { + return; + } + + showSlider(); + + const { lo, hi, max } = api.slider.getRange(); + + if (typeof(layer) === 'string' || typeof(layer) === 'number') { + layer = parseInt(layer); + } else { + layer = hi; + } + + layer = bound(layer, 0, max); + const newLo = layer < lo ? layer : lo; + api.slider.setRange(newLo, layer); + api.slider.showLabels(); + + updateSlider(); + // STACKS.setRange is called by slider callback + + SPACE.update(); +} + +function showSlider() { + api.slider.show(); +} + +function hideSlider() { + api.slider.hide(); + api.ui.speeds.style.display = 'none'; +} + +function updateStackLabelState() { + const settings = api.conf.get(); + // match label checkboxes to preference + for (let label of api.stacks.getLabels()) { + let check = `${settings.mode}-${api.view.get()}-${label}`; + api.stacks.setVisible(label, settings.labels[check] !== false); + } +} + +export const visuals = { + wireframe: setWireframe, + edges: setEdges, + unit_scale: unitScale, + update_speeds: updateSpeeds, + update_slider: updateSlider, + update_slider_max: updateSliderMax, + update_stack_labels: updateStackLabelState, + show_slices: showSlices, + hide_slices: hideSlices, + show_slider: showSlider, + hide_slider: hideSlider, + set_visible_layer: setVisibleLayer, + set_widget_visibility: setWidgetVisibility +}; diff --git a/src/kiri/ui/workspace.js b/src/kiri/ui/workspace.js new file mode 100644 index 00000000..3ac3e44c --- /dev/null +++ b/src/kiri/ui/workspace.js @@ -0,0 +1,177 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { THREE } from '../../ext/three.js'; +import { api } from './api.js'; +import { local as SDB } from '../../data/local.js'; +import { openFiles } from './files.js'; +import { platform } from './platform.js'; +import { selection } from './select.js'; +import { settings } from './config/manager.js'; +import { space as SPACE } from '../../moto/space.js'; +import { utils } from '../core/utils.js'; +import { Widget } from '../core/widget.js'; +import { Index } from '../../data/index.js'; + +const { o2js, js2o, ls2o, parseOpt } = utils; +const LOC = self.location; +const SETUP = parseOpt(LOC.search.substring(1)); +const FILES = openFiles(new Index(SETUP.d ? SETUP.d[0] : 'kiri')); + +let autoSaveTimer = null; + +function reload() { + api.event.emit('reload'); + do_reload(100); +} + +function do_reload(time) { + // allow time for async saves to complete and busy to to to zero + setTimeout(() => { + if (api.busy.val() === 0) { + LOC.reload(); + } else { + console.log(`reload deferred on busy=${api.busy.val()}`); + do_reload(250); + } + }, time || 100); +} + +function auto_save() { + if (!settings.ctrl().autoSave) { + return; + } + clearTimeout(autoSaveTimer); + autoSaveTimer = setTimeout(() => { + api.space.save(true); + }, 1000); +} + +function setFocus(sel, point) { + if (point) { + SPACE.platform.setCenter(point.x, point.z, point.y); + SPACE.view.setFocus(new THREE.Vector3(point.x, point.y, point.z)); + return; + } + if (sel === undefined) { + sel = api.widgets.all(); + } else if (!Array.isArray) { + sel = [ sel ]; + } else if (sel.length === 0) { + sel = api.widgets.all(); + } + let pos = { x:0, y:0, z:0 }; + for (let widget of sel) { + pos.x += widget.track.pos.x; + pos.y += widget.track.pos.y; + pos.z += widget.track.pos.z; + } + if (sel.length) { + pos.x /= sel.length; + pos.y /= sel.length; + pos.z /= sel.length; + } + let cam_index = api.conf.get().process.camStockIndexed || false; + let focus_z = cam_index ? 0 : platform.top_z() / 2; + SPACE.platform.setCenter(pos.x, -pos.y, focus_z); + SPACE.view.setFocus(new THREE.Vector3(pos.x, focus_z, -pos.y)); +} + +function saveWorkspace(quiet) { + api.conf.save(); + const newWidgets = []; + const oldWidgets = js2o(SDB.getItem('ws-widgets'), []); + api.widgets.each(function(widget) { + if (widget.synth) return; + newWidgets.push(widget.id); + oldWidgets.remove(widget.id); + widget.saveState(); + let ann = api.widgets.annotate(widget.id); + ann.file = widget.meta.file; + ann.url = widget.meta.url; + }); + SDB.setItem('ws-widgets', o2js(newWidgets)); + oldWidgets.forEach(wid => { + Widget.deleteFromState(wid); + }); + // eliminate dangling saved widgets + FILES.deleteFilter(key => newWidgets.indexOf(key.substring(8)) < 0, "ws-save-", "ws-savf"); + if (!quiet) { + api.show.alert("workspace saved", 1); + } +} + +function restoreWorkspace(ondone, skip_widget_load) { + let newset = api.conf.restore(false), + camera = newset.controller.view, + toload = ls2o('ws-widgets',[]), + loaded = 0, + position = true; + + api.conf.update_fields(); + platform.update_size(); + + SPACE.view.reset(); + if (camera) { + SPACE.view.load(camera); + } else { + SPACE.view.home(); + } + + if (skip_widget_load) { + if (ondone) { + ondone(); + } + return; + } + + // remove any widgets from platform + api.widgets.each(function(widget) { + platform.delete(widget); + }); + + // load any widget by name that was saved to the workspace + toload.forEach(function(widgetid) { + Widget.loadFromState(widgetid, function(widget) { + if (widget) { + platform.add(widget, 0, position, true); + let ann = api.widgets.annotate(widgetid); + widget.meta.file = ann.file; + widget.meta.url = ann.url; + } + if (++loaded === toload.length) { + platform.deselect(); + if (ondone) { + ondone(); + setTimeout(() => { + platform.update_bounds(); + SPACE.update(); + }, 1); + } + } + }, position); + }); + + return toload.length > 0; +} + +function clearWorkspace() { + // free up worker cache/mem + api.client.clear(); + platform.select_all(); + platform.delete(selection.meshes()); +} + +function is_dark() { + return settings.ctrl().dark; +} + +export const workspace = { + reload, + auto_save, + restore: restoreWorkspace, + clear: clearWorkspace, + save: saveWorkspace, + set_focus: setFocus, + update: SPACE.update, + is_dark +}; From 255da52a688f5bf0eebf8a827d0743aff336ad8a Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 25 Dec 2025 00:32:55 -0500 Subject: [PATCH 020/154] stepwise refinement of init, remove / move main funcs --- src/kiri/core/widget.js | 2 +- src/kiri/ui/api.js | 288 +++++++------- src/kiri/ui/config/dialog.js | 123 ++++++ src/kiri/ui/file-ops.js | 129 +++++++ src/kiri/ui/init.js | 681 ++++----------------------------- src/kiri/ui/local-storage.js | 50 +++ src/kiri/ui/main.js | 270 ------------- src/kiri/ui/navigation.js | 89 +++++ src/kiri/ui/preferences.js | 160 ++++++++ src/kiri/ui/selection-tools.js | 161 ++++++++ src/kiri/ui/settings-ops.js | 138 +++++++ src/kiri/ui/util.js | 52 +++ 12 files changed, 1128 insertions(+), 1015 deletions(-) create mode 100644 src/kiri/ui/config/dialog.js create mode 100644 src/kiri/ui/file-ops.js create mode 100644 src/kiri/ui/local-storage.js delete mode 100644 src/kiri/ui/main.js create mode 100644 src/kiri/ui/navigation.js create mode 100644 src/kiri/ui/preferences.js create mode 100644 src/kiri/ui/selection-tools.js create mode 100644 src/kiri/ui/settings-ops.js create mode 100644 src/kiri/ui/util.js diff --git a/src/kiri/core/widget.js b/src/kiri/core/widget.js index 1640ce1c..5206e58f 100644 --- a/src/kiri/core/widget.js +++ b/src/kiri/core/widget.js @@ -21,8 +21,8 @@ let nextId = 0; function newWidget(id,group) { return new Widget(id,group) } +// todo: fix ui-only path thus this unorthodox binding vs importing api function catalog() { return self.kiri_catalog }; - function index() { return self.kiri_catalog.index } class Widget { diff --git a/src/kiri/ui/api.js b/src/kiri/ui/api.js index e882474c..9d705a1a 100644 --- a/src/kiri/ui/api.js +++ b/src/kiri/ui/api.js @@ -1,80 +1,90 @@ /** Copyright Stewart Allen -- All Rights Reserved */ +import '../../ext/base64.js'; + import alerts from './alerts.js'; import settings from './config/manager.js'; import STACKS from './stacks.js'; -import { visuals } from './visuals.js'; +import web from '../../moto/webui.js'; +import { beta, version } from '../../moto/license.js'; import { broker } from '../../moto/broker.js'; import { client as work } from './client.js'; import { consts, COLOR as color, LISTS as lists } from '../core/consts.js'; import { device, devices } from './devices.js'; -import { catalog, dialog, event, group, help, hide, image } 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 '../core/utils.js'; import { functions } from './function.js'; +import { group as groupModule } from './groups.js'; +import { help as helpModule } from './help.js'; +import { image as imageModule } from './image.js'; +import { Index } from '../../data/index.js'; +import { LANG } from './lang.js'; +import { local } from './local-storage.js'; +import { local as dataLocal } from '../../data/local.js'; +import { modal } from './modal.js'; +import { mode as modeModule, process as processModule } from './mode.js'; +import { newWidget } from '../core/widget.js'; +import { noop, ajax, o2js, js2o, utils } from '../core/utils.js'; +import { openFiles } from './files.js'; import { platform } from './platform.js'; import { selection } from './select.js'; -import { stats } from './stats.js'; -import { newWidget } from '../core/widget.js'; -import { widgets } from '../core/widgets.js'; -import { updateTool } from '../mode/cam/tools.js'; -import { beta, version } from '../../moto/license.js'; +import { settingsUI } from './config/dialog.js'; +import { showDevices } from './devices.js'; +import { showTools } from '../mode/cam/tools.js'; import { space as SPACE } from '../../moto/space.js'; +import { stats } from './stats.js'; import { types as load } from '../../load/file.js'; - -import { LANG } from './lang.js'; -import { LOCAL, SETUP, SECURE } from './main.js'; import { UI } from './component.js'; +import { updateTool } from '../mode/cam/tools.js'; +import { util as utilModule } from './util.js'; +import { view as viewModule } from './view-state.js'; +import { visuals } from './visuals.js'; +import { widgets } from '../core/widgets.js'; +import { workspace } from './workspace.js'; -import web from '../../moto/webui.js'; +// From main.js - environment setup +let LOC = self.location, + SETUP = utils.parseOpt(LOC.search.substring(1)), + SECURE = utilModule.isSecure(LOC.protocol), + LOCAL = self.debug && !SETUP.remote, + EVENT = broker, + FILES = openFiles(new Index(SETUP.d ? SETUP.d[0] : 'kiri')); -let UC = UI.prefix('kiri').inputAction(settings.conf.update), - und = undefined, - clone = Object.clone, +// todo: fix in widget.js b/c front-end and back-end do not share api +self.kiri_catalog = FILES; +FILES.show = () => modal.show('files'); + +// Broker compatibility patch +EVENT.on = (topic, listener) => { + EVENT.subscribe(topic, listener); + return EVENT; +}; + +let busyVal = 0, isHover = false, - feature = { - seed: true, // seed profiles on first use - meta: true, // show selected widget metadata - frame: true, // receive frame events - alert_event: false, // emit alerts as events instead of display - controls: true, // show or not side menus - device_filter: und, // function to limit devices shown - drop_group: und, // optional array to group multi drop - drop_layout: true, // layout on new drop - hoverAdds: false, // when true only searches widget additions - on_key: und, // function override default key handlers - on_key2: [], // allows for multiple key handlers - on_load: und, // function override file drop loads - on_add_stl: und, // legacy override stl drop loads - on_mouse_up: und, // function intercepts mouse up select - on_mouse_down: und, // function intercepts mouse down - work_alerts: true, // allow disabling work progress alerts - pmode: consts.PMODES.SPEED, // preview modes - // hover: false, // when true fires mouse hover events - get hover() { - return isHover; - }, - set hover(b) { - isHover = b; - broker.publish("feature.hover", b); - } - }, - busyVal = 0, - busy = { + undef = undefined; + +// the big kahuna +export const api = { + ajax, + beta, + alerts, + busy: { val() { return busyVal }, inc() { api.event.emit("busy", ++busyVal) }, dec() { api.event.emit("busy", --busyVal) } }, - onkey = (fn) => { - api.feature.on_key2.push(fn); + catalog: FILES, + client: work, + clip(text) { + navigator.clipboard + .writeText(text) + .catch(err => console.error('Clipboard Error:', err)); }, - doit = { - undo: noop, // do.js - redo: noop // do.js - }, - devel = { + clone: Object.clone, + color, + conf: settings.conf, + const: { LANG, LOCAL, SETUP, SECURE, STACKS, ...consts }, + devel: { get enabled() { return settings.ctrl().devel; }, @@ -88,110 +98,128 @@ let UC = UI.prefix('kiri').inputAction(settings.conf.update), api.function.slice(); } }, - local = { - get: (key) => localGet(key), - getItem: (key) => localGet(key), - getInt: (key) => parseInt(localGet(key)), - getFloat: (key) => parseFloat(localGet(key)), - getBoolean: (key, def = true) => { - let val = localGet(key); - return val === true || val === 'true' || val === def; - }, - toggle: (key, val, def) => localSet(key, val ?? !api.local.getBoolean(key, def)), - 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 } }) }, - gcode_decimals(v) { api.work.config({ base: { gcode_decimals: v } }) } - }; - -function clip(text) { - navigator.clipboard - .writeText(text) - .catch(err => console.error('Clipboard Error:', err)); -} - -function localGet(key) { - let sloc = api.conf.get().local; - return sloc[key] || api.sdb[key]; -} - -function localSet(key, val) { - let sloc = api.conf.get().local; - sloc[key] = api.sdb[key] = val; - return val; -} - -function localRemove(key) { - let sloc = api.conf.get().local; - return delete sloc[key]; -} - -export const api = { - ajax, - beta, - alerts, - busy, - catalog, - client: work, - clip, - clone, - color, - conf: settings.conf, - const: { LANG, LOCAL, SETUP, SECURE, STACKS, ...consts }, - devel, device, devices, - dialog, - doit, - event, + dialog: { + show: (which) => modal.show(which), + hide: () => modal.hide(), + update_process_list: settingsUI.update_list + }, + doit: { + undo: noop, // do.js + redo: noop // do.js + }, + event: { + on(t,l) { return EVENT.on(t,l) }, + emit(t,m,o) { return EVENT.publish(t,m,o) }, + bind(t,m,o) { return EVENT.bind(t,m,o) }, + alerts(clr) { alerts.update(clr) }, + import: utilModule.loadFile, + settings: settingsUI.trigger_event + }, electron: navigator.userAgent.includes('Electron'), - feature, + feature: { + seed: true, // seed profiles on first use + meta: true, // show selected widget metadata + frame: true, // receive frame events + alert_event: false, // emit alerts as events instead of display + controls: true, // show or not side menus + device_filter: undef, // function to limit devices shown + drop_group: undef, // optional array to group multi drop + drop_layout: true, // layout on new drop + hoverAdds: false, // when true only searches widget additions + on_key: undef, // function override default key handlers + on_key2: [], // allows for multiple key handlers + on_load: undef, // function override file drop loads + on_add_stl: undef, // legacy override stl drop loads + on_mouse_up: undef, // function intercepts mouse up select + on_mouse_down: undef, // function intercepts mouse down + work_alerts: true, // allow disabling work progress alerts + pmode: consts.PMODES.SPEED, // preview modes + // hover: false, // when true fires mouse hover events + get hover() { + return isHover; + }, + set hover(b) { + isHover = b; + broker.publish("feature.hover", b); + } + }, function: functions, - group, - help, - hide, - image, + group: groupModule, + help: helpModule, + hide: { + alert(rec, recs) { alerts.hide(...arguments) }, + import: noop, + slider: visuals.hide_slider + }, + image: imageModule, js2o, language: LANG, lists, load, local, + LOCAL, modal, - mode, + mode: modeModule, new: { widget: newWidget }, noop, o2js, - onkey, + onkey(fn) { + api.feature.on_key2.push(fn); + }, platform, - process, + process: processModule, sdb: dataLocal, + SECURE, selection, settings, - show, - space, + SETUP, + show: { + alert() { return alerts.show(...arguments) }, + controls() { console.trace('deprecated') }, + devices: showDevices, + import() { api.ui.import.style.display = '' }, + layer: visuals.set_visible_layer, + local() { console.trace('deprecated') }, + progress: utilModule.setProgress, + slices: visuals.show_slices, + tools: showTools + }, + space: workspace, SPACE, stacks: STACKS, stats, tool: { update: updateTool }, - tweak, - uc: UC, - ui: {}, - util, - var: { - layer_lo: 0, - layer_hi: 0, - layer_max: 0 + tweak: { + line_precision(v) { api.work.config({ base: { clipperClean: v } }) }, + gcode_decimals(v) { api.work.config({ base: { gcode_decimals: v } }) } }, + uc: UI.prefix('kiri').inputAction(settings.conf.update), + ui: {}, + util: { + isSecure: utilModule.isSecure, + download: utilModule.download, + ui2rec() { api.conf.update_from(...arguments) }, + rec2ui() { api.conf.update_fields(...arguments) }, + b64enc(obj) { return base64js.fromByteArray(new TextEncoder().encode(JSON.stringify(obj))) }, + b64dec(obj) { return JSON.parse(new TextDecoder().decode(base64js.toByteArray(obj))) } + }, + // var: { + // layer_lo: 0, + // layer_hi: 0, + // layer_max: 0 + // }, version, - view, + view: { + ...viewModule, + ...visuals, + snapshot: null + }, visuals, web, widgets, diff --git a/src/kiri/ui/config/dialog.js b/src/kiri/ui/config/dialog.js new file mode 100644 index 00000000..042ceffe --- /dev/null +++ b/src/kiri/ui/config/dialog.js @@ -0,0 +1,123 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { api } from '../api.js'; +import { modal } from '../modal.js'; +import { settings } from './manager.js'; +import { version } from '../../../moto/license.js'; + +const DOC = self.document; + +function triggerSettingsEvent() { + api.event.emit('settings', settings.get()); +} + +function editSettings(e) { + let current = settings.get(), + mode = settings.mode(), + name = e.target.getAttribute("name"), + load = current.sproc[mode][name], + loadstr = JSON.stringify(load,null,4).split('\n'); + api.uc.prompt(`settings for "${name}"`, loadstr).then(edit => { + if (edit) { + try { + current.sproc[mode][name] = JSON.parse(edit); + if (name === settings.proc().processName) { + api.conf.load(null, name); + } + api.conf.save(); + api.settings.sync.put(); + } catch (e) { + console.log({ malformed_settings: e }); + api.uc.alert('malformed settings object'); + } + } + }); +} + +function exportSettings(e) { + let current = settings.get(), + mode = settings.mode(), + name = e.target.getAttribute("name"), + data = api.util.b64enc({ + process: current.sproc[mode][name], + version, + moto: moto.id, + time: Date.now(), + mode, + name + }); + api.uc.prompt("Export Process Filename", name).then(name => { + if (name) { + api.util.download(data, `${name}.km`); + } + }); +} + +function deleteSettings(e) { + let current = settings.get(); + let name = e.target.getAttribute("del"); + delete current.sproc[settings.mode()][name]; + api.settings.sync.put(); + updateProcessList(); + api.conf.save(); + triggerSettingsEvent(); +} + +function updateProcessList() { + let current = settings.get(); + let list = [], s = current, sp = s.sproc[settings.mode()] || {}, table = api.ui.settingsList; + table.innerHTML = ''; + for (let k in sp) { + if (sp.hasOwnProperty(k)) list.push(k); + } + list.filter(n => n !=='default').sort().forEach(function(sk) { + let row = DOC.createElement('div'), + load = DOC.createElement('button'), + edit = DOC.createElement('button'), + xprt = DOC.createElement('button'), + del = DOC.createElement('button'), + name = sk; + + load.setAttribute('load', sk); + load.onclick = (ev) => { + api.conf.load(undefined, sk); + updateProcessList(); + modal.hide(); + } + load.appendChild(DOC.createTextNode(sk)); + if (sk == settings.proc().processName) { + load.setAttribute('class', 'selected') + } + api.ui.settingsName.value = settings.proc().processName; + + del.setAttribute('del', sk); + del.setAttribute('title', "remove '"+sk+"'"); + del.innerHTML = ''; + del.onclick = deleteSettings; + + edit.setAttribute('name', sk); + edit.setAttribute('title', 'edit'); + edit.innerHTML = ''; + edit.onclick = editSettings; + + xprt.setAttribute('name', sk); + xprt.setAttribute('title', 'export'); + xprt.innerHTML = ''; + xprt.onclick = exportSettings; + + row.setAttribute("class", "flow-row"); + row.appendChild(edit); + row.appendChild(load); + row.appendChild(xprt); + row.appendChild(del); + table.appendChild(row); + }); +} + +export const settingsUI = { + edit: editSettings, + export: exportSettings, + delete: deleteSettings, + update_list: updateProcessList, + trigger_event: triggerSettingsEvent +}; diff --git a/src/kiri/ui/file-ops.js b/src/kiri/ui/file-ops.js new file mode 100644 index 00000000..1b0a0f81 --- /dev/null +++ b/src/kiri/ui/file-ops.js @@ -0,0 +1,129 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { api } from './api.js'; +import { base } from '../../geo/base.js'; +import { space } from '../../moto/space.js'; + +const DOC = self.document; +let platformColor; + +function dragOverHandler(evt) { + evt.stopPropagation(); + evt.preventDefault(); + + // prevent drop actions when a dialog is open + if (api.modal.visible()) { + return; + } + + evt.dataTransfer.dropEffect = 'copy'; + let oldcolor = space.platform.setColor(0x00ff00); + if (oldcolor !== 0x00ff00) platformColor = oldcolor; +} + +function dragLeave() { + space.platform.setColor(platformColor); +} + +function dropHandler(evt) { + evt.stopPropagation(); + evt.preventDefault(); + + // prevent drop actions when a dialog is open + if (api.modal.visible()) { + return; + } + + space.platform.setColor(platformColor); + + let files = evt.dataTransfer.files; + + switch (api.feature.drop_group) { + case true: + return api.platform.load_files(files, []); + case false: + return api.platform.load_files(files, undefined); + } + + if (files.length === 1) { + api.platform.load_files(files); + } else if (files.length > 1) { + api.uc.confirm(`group ${files.length} files?`).then(yes => { + api.platform.load_files(files, yes ? [] : undefined); + }); + } +} + +function loadCatalogFile(e) { + api.widgets.load(e.target.getAttribute('load'), function(widget) { + api.platform.add(widget); + api.dialog.hide(); + }); +} + +function updateCatalog(files) { + let table = api.ui.catalogList, + list = []; + table.innerHTML = ''; + for (let name in files) { + list.push({n:name, ln:name.toLowerCase(), v:files[name].vertices, t:files[name].updated}); + } + list.sort(function(a,b) { + return a.ln < b.ln ? -1 : 1; + }); + for (let i=0; i { + api.uc.prompt(`rename file`, short).then(newname => { + if (newname && newname !== short) { + api.catalog.rename(name, `${newname}${ext}`, then => { + api.modal.show('files'); + }); + } + }); + }; + + load.setAttribute('load', name); + load.setAttribute('title', `file: ${name}\nvertices: ${file.v}\ndate: ${date}`); + load.onclick = loadCatalogFile; + load.appendChild(DOC.createTextNode(short)); + + del.setAttribute('del', name); + del.setAttribute('title', "remove '"+name+"'"); + del.onclick = () => { api.catalog.deleteFile(name) }; + del.innerHTML = ''; + + size.setAttribute("disabled", true); + size.setAttribute("class", "label"); + size.appendChild(DOC.createTextNode(base.util.comma(file.v))); + + row.setAttribute("class", "f-row a-center"); + row.appendChild(renm); + row.appendChild(load); + row.appendChild(size); + row.appendChild(del); + table.appendChild(row); + } +} + +export const fileOps = { + dragOverHandler, + dragLeave, + dropHandler, + loadCatalogFile, + updateCatalog +}; diff --git a/src/kiri/ui/init.js b/src/kiri/ui/init.js index e392d5f0..dbb7e70e 100644 --- a/src/kiri/ui/init.js +++ b/src/kiri/ui/init.js @@ -1,19 +1,9 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { $, $c } from '../../moto/webui.js'; +import { $ } from '../../moto/webui.js'; import { api } from './api.js'; -import { base } from '../../geo/base.js'; -import { local as sdb } from '../../data/local.js'; -import { slider } from './slider.js'; -import { keyboard } from './keyboard.js'; -import { modal } from './modal.js'; -import { interact } from './interact.js'; -import { space } from '../../moto/space.js'; -import { LOCAL, SETUP } from './main.js'; -import { VIEWS, MODES, SEED } from '../core/consts.js'; import { beta, version } from '../../moto/license.js'; -import { settings as set_ctrl } from './config/manager.js'; -import STACKS from './stacks.js'; +import { fileOps } from './file-ops.js'; import { init as initCAM } from '../mode/cam/init-ui.js'; import { init as initDRAG } from '../mode/drag/init-ui.js'; import { init as initFDM } from '../mode/fdm/init-ui.js'; @@ -21,12 +11,27 @@ import { init as initLaser } from '../mode/laser/init-ui.js'; import { init as initSLA } from '../mode/sla/init-ui.js'; import { init as initWEDM } from '../mode/wedm/init-ui.js'; import { init as initWJET } from '../mode/wjet/init-ui.js'; +import { interact } from './interact.js'; +import { keyboard } from './keyboard.js'; +import { local as sdb } from '../../data/local.js'; import { menu as menuCAM } from '../mode/cam/init-menu.js'; import { menu as menuFDM } from '../mode/fdm/init-menu.js'; import { menu as menuLaser } from '../mode/laser/init-menu.js'; import { menu as menuSLA } from '../mode/sla/init-menu.js'; +import { modal } from './modal.js'; +import { navigation } from './navigation.js'; +import { preferences } from './preferences.js'; +import { selectionTools } from './selection-tools.js'; +import { settings as set_ctrl } from './config/manager.js'; +import { settingsOps } from './settings-ops.js'; +import { slider } from './slider.js'; +import { space } from '../../moto/space.js'; +import { VIEWS, MODES, SEED } from '../core/consts.js'; -let { CAM, SLA, FDM, LASER, DRAG, WJET, WEDM } = MODES, +import STACKS from './stacks.js'; + +let { LOCAL, SETUP } = api, + { CAM, SLA, FDM, LASER, DRAG, WJET, WEDM } = MODES, { client, catalog, platform, selection, stats } = api, LANG = api.language.current, WIN = self.window, @@ -80,135 +85,11 @@ function checkSeed(then) { return false; } -function unitsSave() { - api.conf.update({ controller: true }); - platform.update_size(); -} - -function aniMeshSave() { - api.conf.update({ controller: true }); - api.conf.save(); -} - -function lineTypeSave() { - const sel = ui.lineType.options[ui.lineType.selectedIndex]; - if (sel) { - settings().controller.lineType = sel.value; - api.conf.save(); - } -} - -function detailSave() { - let level = ui.detail.options[ui.detail.selectedIndex]; - if (level) { - level = level.value; - let rez = base.config.clipperClean; - switch (level) { - case '100': rez = 50; break; - case '75': rez = base.config.clipperClean; break; - case '50': rez = 500; break; - case '25': rez = 1000; break; - } - client.config({ - base: { clipperClean: rez } - }); - settings().controller.detail = level; - api.conf.save(); - } -} - -function speedSave() { - settings().controller.showSpeeds = ui.showSpeeds.checked; - api.view.update_speeds(); -} - -function setThreaded(bool) { - if (bool) { - client.pool.start(); - } else { - client.pool.stop(); - } - return bool; -} - // upon restore, seed presets api.event.emit('preset', api.conf.dbo()); // api.event.on("set.threaded", bool => setThreaded(bool)); -function booleanSave() { - let control = settings().controller; - if (control.assembly != ui.assembly.checked) { - client.wasm(ui.assembly.checked); - } - if (control.antiAlias != ui.antiAlias.checked) { - api.show.alert('Page Reload Required to Change Aliasing'); - } - 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.spaceRandoX = ui.spaceRandoX.checked; - // control.threaded = setThreaded(ui.threaded.checked); - control.webGPU = ui.webGPU.checked; - space.view.setZoom(control.reverseZoom, control.zoomSpeed); - // platform.layout(); - api.conf.save(); - api.platform.update_size(); - updateStats(); - updateDrawer(); - api.event.emit('boolean.update'); - space.view.setProjection(control.ortho ? 'orthographic' : 'perspective'); -} - -function updateDrawer() { - const { drawer, scrolls } = settings().controller; - $c('app', drawer ? 'slideshow' : '', drawer ? '' : 'slideshow'); - $c('app', scrolls ? '' : 'hide-scroll', scrolls ? 'hide-scroll' : ''); -} - -function updateStats() { - if (self.debug !== true) { - return; - } - console.log('updateStats()'); - let { div, fps, rms, rnfo } = ui.stats; - div.style.display = 'flex'; - clearTimeout(statsTimer); - statsTimer = setInterval(() => { - const nrms = space.view.getRMS().toFixed(1); - const nfps = space.view.getFPS().toFixed(1); - const rend = space.renderInfo(); - const { memory, render } = rend; - if (nfps !== fps.innerText) { - fps.innerText = nfps; - } - if (nrms !== rms.innerText) { - rms.innerText = nrms; - } - if (rnfo.offsetParent !== null) { - rnfo.innerHTML = Object.entries({ ...memory, ...render, render_ms: nrms, frames_sec: nfps }).map(row => { - return `
${row[0]}
` - }).join(''); - } - }, 100); -} - function onBooleanClick(el) { // copy some ui elements to target settings let settings = api.conf.get(); @@ -239,7 +120,6 @@ function onButtonClick(ev) { api.event.on('click.button', onButtonClick); - function keys(o) { let key, list = []; for (key in o) { if (o.hasOwnProperty(key)) list.push(key) } @@ -252,277 +132,6 @@ function clearSelected(children) { } } -function rotateInputSelection() { - if (selection.meshes().length === 0) { - api.show.alert("select object to rotate"); - return; - } - api.uc.prompt("Enter X,Y,Z degrees of rotation","").then(coord => { - coord = (coord || '').split(','); - let prod = Math.PI / 180, - x = parseFloat(coord[0] || 0.0) * prod, - y = parseFloat(coord[1] || 0.0) * prod, - z = parseFloat(coord[2] || 0.0) * prod; - selection.rotate(x, y, z); - }); -} - -function positionSelection() { - if (selection.meshes().length === 0) { - api.show.alert("select object to position"); - return; - } - let current = settings(), - { device, process} = current, - center = process.ctOriginCenter || process.camOriginCenter || device.bedRound || device.originCenter, - bounds = boundsSelection(); - - api.uc.prompt("Enter X,Y coordinates for selection","").then(coord => { - coord = (coord || '').split(','); - let x = parseFloat(coord[0] || 0.0), - y = parseFloat(coord[1] || 0.0), - z = parseFloat(coord[2] || 0.0); - - if (!center) { - x = x - device.bedWidth/2 + (bounds.max.x - bounds.min.x)/2; - y = y - device.bedDepth/2 + (bounds.max.y - bounds.min.y)/2 - } - - selection.move(x, y, z, true); - }); -} - -function deviceExport(exp, name) { - name = (name || "device") - .toLowerCase() - .replace(/ /g,'_') - .replace(/\./g,'_'); - uc.prompt("Export Device Filename", name).then(name => { - if (name) { - api.util.download(exp, `${name}.km`); - } - }); -} - -function objectsExport(format = "stl") { - // return selection.export(); - uc.confirm("Export Filename", {ok:true, cancel: false}, `selected.${format}`).then(name => { - if (!name) return; - if (name.toLowerCase().indexOf(`.${format}`) < 0) { - name = `${name}.${format}`; - } - api.util.download(selection.export(format), name); - }); -} - -function workspaceNew() { - uc.confirm("Clear Workspace?", {ok:true, cancel: false}).then(value => { - if (value === true) { - let proc = set_ctrl.proc(); - proc.ops && (proc.ops.length = 0); - proc.op2 && (proc.op2.length = 0); - api.platform.clear(); - } - }); -} - -function profileExport() { - const opt = {pre: [ - "
", - "

Workspace Export

", - " ", - " ", - "  Exclude meshes", - "
" - ]}; - let suggestion = "workspace"; - let file = api.widgets.all()[0]?.meta.file || ''; - if (file) { - suggestion = `${suggestion}_${file.split('.')[0]}`.replaceAll(' ','_'); - }; - uc.confirm("Filename", {ok:true, cancel: false}, suggestion, opt).then(name => { - if (!name) return; - - let work = !$('excwork').checked; - let json = api.conf.export({work, clear:true}); - - client.zip([ - {name:"workspace.json", data:JSON.stringify(json)} - ], progress => { - api.show.progress(progress.percent/100, "compressing workspace"); - }, output => { - api.show.progress(0); - api.util.download(output, `${name}.kmz`); - }); - }); -} - -function settingsSave(ev, name) { - if (ev) { - ev.stopPropagation(); - ev.preventDefault(); - } - - api.dialog.hide(); - let mode = api.mode.get(), - s = settings(), - def = "default", - cp = s.process, - pl = s.sproc[mode], - lp = s.cproc[mode], - saveAs = (name) => { - if (!name) { - return; - } - let np = pl[name] = {}; - cp.processName = name; - pl[name] = Object.clone(cp); - for (let k in cp) { - if (!cp.hasOwnProperty(k)) continue; - np[k] = cp[k]; - } - s.cproc[mode] = name; - s.devproc[s.device.deviceName] = name; - api.conf.save(); - api.conf.update(); - api.event.settings(); - sync_put(); - }; - - if (name) { - saveAs(name); - } else { - uc.prompt("Save Settings As", cp ? lp || def : def).then(saveAs); - } -} - -function settingsLoad() { - api.conf.show(); -} - -function updateDeviceSize() { - api.conf.update(); - platform.update_size(); - platform.update_origin(); -} - -async function sync_put() { - await set_ctrl.sync.put(); -} - -function dragOverHandler(evt) { - evt.stopPropagation(); - evt.preventDefault(); - - // prevent drop actions when a dialog is open - if (api.modal.visible()) { - return; - } - - evt.dataTransfer.dropEffect = 'copy'; - let oldcolor = space.platform.setColor(0x00ff00); - if (oldcolor !== 0x00ff00) platformColor = oldcolor; -} - -function dragLeave() { - space.platform.setColor(platformColor); -} - -function dropHandler(evt) { - evt.stopPropagation(); - evt.preventDefault(); - - // prevent drop actions when a dialog is open - if (api.modal.visible()) { - return; - } - - space.platform.setColor(platformColor); - - let files = evt.dataTransfer.files; - - switch (api.feature.drop_group) { - case true: - return api.platform.load_files(files, []); - case false: - return api.platform.load_files(files, undefined); - } - - if (files.length === 1) { - api.platform.load_files(files); - } else if (files.length > 1) { - uc.confirm(`group ${files.length} files?`).then(yes => { - api.platform.load_files(files, yes ? [] : undefined); - }); - } -} - -function loadCatalogFile(e) { - api.widgets.load(e.target.getAttribute('load'), function(widget) { - platform.add(widget); - api.dialog.hide(); - }); -} - -function updateCatalog(files) { - let table = ui.catalogList, - list = []; - table.innerHTML = ''; - for (let name in files) { - list.push({n:name, ln:name.toLowerCase(), v:files[name].vertices, t:files[name].updated}); - } - list.sort(function(a,b) { - return a.ln < b.ln ? -1 : 1; - }); - for (let i=0; i { - api.uc.prompt(`rename file`, short).then(newname => { - if (newname && newname !== short) { - catalog.rename(name, `${newname}${ext}`, then => { - api.modal.show('files'); - }); - } - }); - }; - - load.setAttribute('load', name); - load.setAttribute('title', `file: ${name}\nvertices: ${file.v}\ndate: ${date}`); - load.onclick = loadCatalogFile; - load.appendChild(DOC.createTextNode(short)); - - del.setAttribute('del', name); - del.setAttribute('title', "remove '"+name+"'"); - del.onclick = () => { catalog.deleteFile(name) }; - del.innerHTML = ''; - - size.setAttribute("disabled", true); - size.setAttribute("class", "label"); - size.appendChild(DOC.createTextNode(base.util.comma(file.v))); - - row.setAttribute("class", "f-row a-center"); - row.appendChild(renm); - row.appendChild(load); - row.appendChild(size); - row.appendChild(del); - table.appendChild(row); - } -} - function isBelt() { return api.device.isBelt(); } @@ -600,10 +209,10 @@ function init_one() { space.platform.onMove(conf.save); space.platform.setRound(true); space.useDefaultKeys(api.feature.on_key === undefined || api.feature.on_key_defaults); - updateDrawer(); + preferences.updateDrawer(); // api augmentation with local functions - api.device.export = deviceExport; + api.device.export = settingsOps.deviceExport; Object.assign(ui, { tracker: tracker, @@ -726,9 +335,9 @@ function init_one() { device: newGroup(LANG.dv_gr_dev, null, {group:"ddev", inline, class:"noshow"}), _____: newGroup("workspace", null, {group:"dext", inline}), - bedWidth: newInput('X (width)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:updateDeviceSize}), - bedDepth: newInput('Y (depth)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:updateDeviceSize}), - maxHeight: newInput('Z (height)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:updateDeviceSize}), + bedWidth: newInput('X (width)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:settingsOps.updateDeviceSize}), + bedDepth: newInput('Y (depth)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:settingsOps.updateDeviceSize}), + maxHeight: newInput('Z (height)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:settingsOps.updateDeviceSize}), resolutionX: newInput(LANG.dv_rezx_s, {title:LANG.dv_rezx_l, convert:toInt, size:6, modes:SLA}), resolutionY: newInput(LANG.dv_rezy_s, {title:LANG.dv_rezy_l, convert:toInt, size:6, modes:SLA}), _____: newDiv({ class: "f-col t-body t-inset", addto: $('dev-config'), set:true, modes:NO_WEDM }), @@ -786,39 +395,39 @@ function init_one() { /** Preferences Menu */ _____: newGroup(LANG.op_menu, $('prefs-gen1'), {inline}), - antiAlias: newBoolean(LANG.op_anta_s, booleanSave, {title:LANG.op_anta_l}), - reverseZoom: newBoolean(LANG.op_invr_s, booleanSave, {title:LANG.op_invr_l}), - ortho: newBoolean(LANG.op_orth_s, booleanSave, {title:LANG.op_orth_l}), - dark: newBoolean(LANG.op_dark_s, booleanSave, {title:LANG.op_dark_l}), - drawer: newBoolean('slide out', booleanSave, {title:'slide out settings drawer'}), - scrolls: newBoolean('scrollbars', booleanSave, {title:'show scrollbars'}), - devel: newBoolean(LANG.op_devl_s, booleanSave, {title:LANG.op_devl_l}), + antiAlias: newBoolean(LANG.op_anta_s, preferences.booleanSave, {title:LANG.op_anta_l}), + reverseZoom: newBoolean(LANG.op_invr_s, preferences.booleanSave, {title:LANG.op_invr_l}), + ortho: newBoolean(LANG.op_orth_s, preferences.booleanSave, {title:LANG.op_orth_l}), + dark: newBoolean(LANG.op_dark_s, preferences.booleanSave, {title:LANG.op_dark_l}), + drawer: newBoolean('slide out', preferences.booleanSave, {title:'slide out settings drawer'}), + scrolls: newBoolean('scrollbars', preferences.booleanSave, {title:'show scrollbars'}), + devel: newBoolean(LANG.op_devl_s, preferences.booleanSave, {title:LANG.op_devl_l}), _____: newGroup(LANG.op_disp, $('prefs-gen2'), {inline}), - showOrigin: newBoolean(LANG.op_shor_s, booleanSave, {title:LANG.op_shor_l}), - showRulers: newBoolean(LANG.op_shru_s, booleanSave, {title:LANG.op_shru_l}), - 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"), + showOrigin: newBoolean(LANG.op_shor_s, preferences.booleanSave, {title:LANG.op_shor_l}), + showRulers: newBoolean(LANG.op_shru_s, preferences.booleanSave, {title:LANG.op_shru_l}), + showSpeeds: newBoolean(LANG.op_sped_s, preferences.speedSave, {title:LANG.op_sped_l}), + shiny: newBoolean(LANG.op_shny_s, preferences.booleanSave, {title:LANG.op_shny_l, modes:FDM}), + lineType: newSelect(LANG.op_line_s, {title: LANG.op_line_l, action: preferences.lineTypeSave, modes:FDM}, "linetype"), + manifold: newBoolean(LANG.op_mani_s, preferences.booleanSave, {title: LANG.op_mani_l, modes:CAM}, "manifold"), + animesh: newSelect(LANG.op_anim_s, {title: LANG.op_anim_l, action: preferences.aniMeshSave, modes:CAM}, "animesh"), + units: newSelect(LANG.op_unit_s, {title: LANG.op_unit_l, action: preferences.unitsSave, modes:CAM}, "units"), edgeangle: newInput(LANG.op_spoa_s, {title:LANG.op_spoa_l, convert:toFloat, size:3}), _____: newGroup(LANG.lo_menu, $('prefs-lay'), {inline}), - autoSave: newBoolean(LANG.op_save_s, booleanSave, {title:LANG.op_save_l}), - autoLayout: newBoolean(LANG.op_auto_s, booleanSave, {title:LANG.op_auto_l}), - freeLayout: newBoolean(LANG.op_free_s, booleanSave, {title:LANG.op_free_l}), - spaceRandoX: newBoolean(LANG.op_spcx_s, booleanSave, {title:LANG.op_spcx_l, show:isBelt}), + autoSave: newBoolean(LANG.op_save_s, preferences.booleanSave, {title:LANG.op_save_l}), + autoLayout: newBoolean(LANG.op_auto_s, preferences.booleanSave, {title:LANG.op_auto_l}), + freeLayout: newBoolean(LANG.op_free_s, preferences.booleanSave, {title:LANG.op_free_l}), + spaceRandoX: newBoolean(LANG.op_spcx_s, preferences.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}), - exportOcto: newBoolean(`OctoPrint`, booleanSave, {title:LANG.op_exop_l}), - exportThumb: newBoolean(`Thumbnail`, booleanSave, {modes:FDM}), - exportPreview: newBoolean(`Code Preview`, booleanSave), + exportOcto: newBoolean(`OctoPrint`, preferences.booleanSave, {title:LANG.op_exop_l}), + exportThumb: newBoolean(`Thumbnail`, preferences.booleanSave, {modes:FDM}), + exportPreview: newBoolean(`Code Preview`, preferences.booleanSave), _____: newGroup(LANG.pt_menu, $('prefs-prt'), {inline}), - detail: newSelect(LANG.pt_qual_s, {title: LANG.pt_qual_l, action: detailSave}, "detail"), - 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}), + detail: newSelect(LANG.pt_qual_s, {title: LANG.pt_qual_l, action: preferences.detailSave}, "detail"), + healMesh: newBoolean(LANG.pt_heal_s, preferences.booleanSave, {title: LANG.pt_heal_l}), + // threaded: newBoolean(LANG.pt_thrd_s, preferences.booleanSave, {title: LANG.pt_thrd_l, modes:THREED}), + assembly: newBoolean(LANG.pt_assy_s, preferences.booleanSave, {title: LANG.pt_assy_l, modes:THREED}), + webGPU: newBoolean(LANG.pt_wgpu_s, preferences.booleanSave, {title: LANG.pt_wgpu_l, modes:THREED}), prefadd: uc.checkpoint($('prefs-add')), @@ -853,7 +462,7 @@ function init_one() { // override old style settings two-button menu ui.settingsSave.onclick = () => { - settingsSave(undefined, ui.settingsName.value); + settingsOps.settingsSave(undefined, ui.settingsName.value); }; // initialize modal controller @@ -918,8 +527,8 @@ function init_one() { VIEWS, DOC, WIN, - rotateInputSelection, - settingsLoad + rotateInputSelection: selectionTools.rotateInputSelection, + settingsLoad: settingsOps.settingsLoad }); // expose keyboard to API @@ -983,97 +592,13 @@ function init_one() { }; space.event.addHandlers(self, [ - 'dragover', dragOverHandler, - 'dragleave', dragLeave, - 'drop', dropHandler + 'dragover', fileOps.dragOverHandler, + 'dragleave', fileOps.dragLeave, + 'drop', fileOps.dropHandler ]); - function selectionSize(e) { - let dv = parseFloat(e.target.value || 1), - pv = parseFloat(e.target.was || 1), - ra = dv / pv, - xv = parseFloat(ui.sizeX.was ?? ui.scaleX.value) || 1, - yv = parseFloat(ui.sizeY.was ?? ui.scaleY.value) || 1, - zv = parseFloat(ui.sizeZ.was ?? ui.scaleZ.value) || 1, - ta = e.target, - xc = ui.lockX.checked, - yc = ui.lockY.checked, - zc = ui.lockZ.checked, - xt = ta === ui.sizeX, - yt = ta === ui.sizeY, - zt = ta === ui.sizeZ, - tl = (xt && xc) || (yt && yc) || (zt && zc), - xr = ((tl && xc) || (!tl && xt) ? ra : 1), - yr = ((tl && yc) || (!tl && yt) ? ra : 1), - zr = ((tl && zc) || (!tl && zt) ? ra : 1); - // prevent null scale - if (xv * xr < 0.1 || yv * yr < 0.1 || zv * zr < 0.1) { - api.alerts.show('invalid scale value'); - return; - } - selection.scale(xr,yr,zr); - ui.sizeX.was = ui.sizeX.value = xv * xr; - ui.sizeY.was = ui.sizeY.value = yv * yr; - ui.sizeZ.was = ui.sizeZ.value = zv * zr; - } - - function selectionScale(e) { - let dv = parseFloat(e.target.value || 1), - pv = parseFloat(e.target.was || 1), - ra = dv / pv, - xv = parseFloat(ui.scaleX.was ?? ui.scaleX.value) || 1, - yv = parseFloat(ui.scaleY.was ?? ui.scaleY.value) || 1, - zv = parseFloat(ui.scaleZ.was ?? ui.scaleY.value) || 1, - ta = e.target, - xc = ui.lockX.checked, - yc = ui.lockY.checked, - zc = ui.lockZ.checked, - xt = ta === ui.scaleX, - yt = ta === ui.scaleY, - zt = ta === ui.scaleZ, - tl = (xt && xc) || (yt && yc) || (zt && zc), - xr = ((tl && xc) || (!tl && xt) ? ra : 1), - yr = ((tl && yc) || (!tl && yt) ? ra : 1), - zr = ((tl && zc) || (!tl && zt) ? ra : 1); - // prevent null scale - if (xv * xr < 0.1 || yv * yr < 0.1 || zv * zr < 0.1) { - api.alerts.show('invalid scale value'); - return; - } - selection.scale(xr,yr,zr); - ui.scaleX.was = ui.scaleX.value = xv * xr; - ui.scaleY.was = ui.scaleY.value = yv * yr; - ui.scaleZ.was = ui.scaleZ.value = zv * zr; - } - - function selectionRotate(e) { - let val = parseFloat(e.target.value) || 0; - e.target.value = val; - } - - // on enter but not on blur - space.event.onEnterKey([ - ui.scaleX, selectionScale, - ui.scaleY, selectionScale, - ui.scaleZ, selectionScale, - ui.sizeX, selectionSize, - ui.sizeY, selectionSize, - ui.sizeZ, selectionSize, - ]); - // on enter and blur - space.event.onEnterKey([ - ui.toolName, updateTool, - ui.toolNum, updateTool, - ui.toolFluteDiam, updateTool, - ui.toolFluteLen, updateTool, - ui.toolShaftDiam, updateTool, - ui.toolShaftLen, updateTool, - ui.toolTaperTip, updateTool, - ui.toolTaperAngle, updateTool, - $('rot_x'), selectionRotate, - $('rot_y'), selectionRotate, - $('rot_z'), selectionRotate - ], true); + // Setup selection transformation bindings + selectionTools.setupSelectionBindings(ui); $('lab-axis').onclick = () => { ui.lockX.checked = @@ -1222,7 +747,7 @@ function init_two() { } platform.deselect(); - catalog.addFileListener(updateCatalog); + catalog.addFileListener(fileOps.updateCatalog); space.view.setZoom(control.reverseZoom, control.zoomSpeed); space.platform.setGridZOff(undefined); space.platform.setZOff(0.05); @@ -1246,10 +771,10 @@ function init_two() { // ui.threaded.checked = setThreaded(control.threaded); ui.webGPU.checked = control.webGPU; - setThreaded(true); - lineTypeSave(); - detailSave(); - updateStats(); + preferences.setThreaded(true); + preferences.lineTypeSave(); + preferences.detailSave(); + preferences.updateStats(); // optional set-and-lock mode (hides mode menu) let SETMODE = SETUP.mode ? SETUP.mode[0] : null; @@ -1269,8 +794,8 @@ function init_two() { // default to ARRANGE view mode api.view.set(VIEWS.ARRANGE); - // add ability to override - api.show.controls(api.feature.controls); + // add ability to override (todo: restore?) + // api.show.controls(api.feature.controls); // update everything dependent on the platform size platform.update_size(); @@ -1324,80 +849,8 @@ function init_two() { sdb.gdpr = Date.now(); }; - // bind interface action elements - $('export-support-a').onclick = (ev) => { ev.stopPropagation(); api.modal.show('don8') }; - $('mode-device').onclick = api.show.devices; - $('mode-profile').onclick = settingsLoad; - $('mode-fdm').onclick = () => api.mode.set('FDM'); - $('mode-cam').onclick = () => api.mode.set('CAM'); - $('mode-sla').onclick = () => api.mode.set('SLA'); - $('mode-laser').onclick = () => api.mode.set('LASER'); - $('mode-drag').onclick = () => api.mode.set('DRAG'); - $('mode-wjet').onclick = () => api.mode.set('WJET'); - $('mode-wedm').onclick = () => api.mode.set('WEDM'); - $('set-device').onclick = (ev) => { ev.stopPropagation(); api.show.devices() }; - $('set-profs').onclick = (ev) => { ev.stopPropagation(); api.conf.show() }; - $('set-tools').onclick = (ev) => { ev.stopPropagation(); api.show.tools() }; - $('set-prefs').onclick = (ev) => { ev.stopPropagation(); api.modal.show('prefs') }; - ui.acct.help.onclick = (ev) => { ev.stopPropagation(); api.help.show() }; - ui.acct.don8.onclick = (ev) => { ev.stopPropagation(); api.modal.show('don8') }; - ui.acct.mesh.onclick = (ev) => { ev.stopPropagation(); WIN.location = "/mesh" }; - ui.acct.export.onclick = (ev) => { ev.stopPropagation(); profileExport() }; - ui.acct.export.title = LANG.acct_xpo; - $('file-new').onclick = (ev) => { ev.stopPropagation(); workspaceNew() }; - $('file-recent').onclick = () => { api.modal.show('files') }; - $('file-import').onclick = (ev) => { api.event.import(ev); }; - ui.func.slice.onclick = (ev) => { ev.stopPropagation(); api.function.slice() }; - ui.func.preview.onclick = (ev) => { ev.stopPropagation(); api.function.print() }; - ui.func.animate.onclick = (ev) => { ev.stopPropagation(); api.function.animate() }; - ui.func.export.onclick = (ev) => { ev.stopPropagation(); api.function.export() }; - $('view-arrange').onclick = api.platform.layout; - $('view-top').onclick = space.view.top; - $('view-home').onclick = space.view.home; - $('view-front').onclick = space.view.front; - $('view-back').onclick = space.view.back; - $('view-left').onclick = space.view.left; - $('view-right').onclick = space.view.right; - $('unrotate').onclick = () => { - api.widgets.for(w => w.unrotate()); - selection.update_info(); - }; - // attach button handlers to support targets - for (let btn of ["don8pt","don8gh","don8pp"]) { - $(btn).onclick = (ev) => { - window.open(ev.target.children[0].href); - } - } - // rotation buttons - let d = (Math.PI / 180); - $('rot_x_lt').onclick = () => { selection.rotate(-d * $('rot_x').value,0,0) }; - $('rot_x_gt').onclick = () => { selection.rotate( d * $('rot_x').value,0,0) }; - $('rot_y_lt').onclick = () => { selection.rotate(0,-d * $('rot_y').value,0) }; - $('rot_y_gt').onclick = () => { selection.rotate(0, d * $('rot_y').value,0) }; - $('rot_z_lt').onclick = () => { selection.rotate(0,0, d * $('rot_z').value) }; - $('rot_z_gt').onclick = () => { selection.rotate(0,0,-d * $('rot_z').value) }; - // rendering options - $('render-edges').onclick = () => { api.view.edges({ toggle: true }); api.conf.save() }; - $('render-ghost').onclick = () => { api.view.wireframe(false, 0, api.view.is_arrange() ? 0.4 : 0.25); }; - $('render-wire').onclick = () => { api.view.wireframe(true, 0, api.space.is_dark() ? 0.25 : 0.5); }; - $('render-solid').onclick = () => { api.view.wireframe(false, 0, 1); }; - $('mesh-export-stl').onclick = () => { objectsExport('stl') }; - $('mesh-export-obj').onclick = () => { objectsExport('obj') }; - $('mesh-merge').onclick = selection.merge; - $('mesh-split').onclick = selection.isolateBodies; - $('context-duplicate').onclick = selection.duplicate; - $('context-mirror').onclick = selection.mirror; - $('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", - ev => api.space.set_focus(undefined, ev.object.point) - ); - }; - $('context-contents').onclick = () => { api.SPACE.view.fit() }; - $('view-fit').onclick = () => { api.SPACE.view.fit() }; - $('wassup').onmouseover = () => { $('suppopp').classList.remove('hide') }; + // Setup navigation button bindings + navigation.setupNavigation(ui, WIN, LANG); // ui.modal.onclick = api.modal.hide; ui.modalBox.onclick = (ev) => { ev.stopPropagation() }; diff --git a/src/kiri/ui/local-storage.js b/src/kiri/ui/local-storage.js new file mode 100644 index 00000000..9d5d3335 --- /dev/null +++ b/src/kiri/ui/local-storage.js @@ -0,0 +1,50 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +// Lazily access api to avoid circular dependency at module load time +let _api; + +function getAPI() { + if (!_api) { + _api = self.kiri_api; + } + return _api; +} + +function localGet(key) { + const api = getAPI(); + let sloc = api.conf.get().local; + return sloc[key] || api.sdb[key]; +} + +function localSet(key, val) { + const api = getAPI(); + let sloc = api.conf.get().local; + sloc[key] = api.sdb[key] = val; + return val; +} + +function localRemove(key) { + const api = getAPI(); + let sloc = api.conf.get().local; + return delete sloc[key]; +} + +function localGetBoolean(key, def = true) { + let val = localGet(key); + return val === true || val === 'true' || val === def; +} + +export const local = { + get: (key) => localGet(key), + getItem: (key) => localGet(key), + getInt: (key) => parseInt(localGet(key)), + getFloat: (key) => parseFloat(localGet(key)), + getBoolean: localGetBoolean, + toggle: (key, val, def) => { + return localSet(key, val ?? !localGetBoolean(key, def)); + }, + put: (key, val) => localSet(key, val), + set: (key, val) => localSet(key, val), + setItem: (key, val) => localSet(key, val), + removeItem: (key) => localRemove(key) +}; diff --git a/src/kiri/ui/main.js b/src/kiri/ui/main.js deleted file mode 100644 index 88a9ec58..00000000 --- a/src/kiri/ui/main.js +++ /dev/null @@ -1,270 +0,0 @@ -/** Copyright Stewart Allen -- All Rights Reserved */ - -import '../../ext/base64.js'; - -import { modal } from './modal.js'; - -import { $ } from '../../moto/webui.js'; -import { api } from './api.js'; -import { broker } from '../../moto/broker.js'; -import { Index } from '../../data/index.js'; -import { openFiles } from './files.js'; -import { settings } from './config/manager.js'; -import { showDevices } from './devices.js'; -import { noop, utils } from '../core/utils.js'; -import { version } from '../../moto/license.js'; -import { showTools } from '../mode/cam/tools.js'; - -// Import new modules -import { workspace } from './workspace.js'; -import { mode as modeModule, process as processModule } from './mode.js'; -import { help as helpModule } from './help.js'; -import { group as groupModule } from './groups.js'; -import { image as imageModule } from './image.js'; -import { view as viewModule } from './view-state.js'; -import { visuals } from './visuals.js'; - -let { parseOpt } = utils, - WIN = self.window, - DOC = self.document, - LOC = self.location, - SETUP = parseOpt(LOC.search.substring(1)), - SECURE = isSecure(LOC.protocol), - LOCAL = self.debug && !SETUP.remote, - EVENT = broker, - FILES = openFiles(new Index(SETUP.d ? SETUP.d[0] : 'kiri')); - -// allow widget to straddle client / worker FOR NOW -self.kiri_catalog = FILES; - -export const dialog = { - show: (which) => modal.show(which), - hide: () => modal.hide(), - update_process_list: updateProcessList -}; - -// Re-export help from new module -export const help = helpModule; - -export const event = { - on(t,l) { return EVENT.on(t,l) }, - emit(t,m,o) { return EVENT.publish(t,m,o) }, - bind(t,m,o) { return EVENT.bind(t,m,o) }, - alerts(clr) { api.alerts.update(clr) }, - import: loadFile, - settings: triggerSettingsEvent -}; - -// Re-export group from new module -export const group = groupModule; - -export const hide = { - alert(rec, recs) { api.alerts.hide(...arguments) }, - import: noop, - slider: visuals.hide_slider -}; - -// Re-export image from new module -export const image = imageModule; - -// Re-export modal singleton methods -export { modal }; - -// Re-export mode and process from new module -export const mode = modeModule; -export const process = processModule; - -export const show = { - alert() { return api.alerts.show(...arguments) }, - controls() { console.trace('deprecated') }, - devices: showDevices, - import() { api.ui.import.style.display = '' }, - layer: visuals.set_visible_layer, - local() { console.trace('deprecated') }, - progress: setProgress, - slices: visuals.show_slices, - tools: showTools -}; - -// Re-export space (workspace) from new module -export const space = workspace; - -export const util = { - isSecure, - download: downloadBlob, - ui2rec() { api.conf.update_from(...arguments) }, - rec2ui() { api.conf.update_fields(...arguments) }, - b64enc(obj) { return base64js.fromByteArray(new TextEncoder().encode(JSON.stringify(obj))) }, - b64dec(obj) { return JSON.parse(new TextDecoder().decode(base64js.toByteArray(obj))) } -}; - -// Combine view-state and visuals modules -export const view = { - ...viewModule, - ...visuals, - snapshot: null -}; - -// add show() to catalog for API -FILES.show = () => modal.show('files'); - -// patch broker for api backward compatibility -EVENT.on = (topic, listener) => { - EVENT.subscribe(topic, listener); - return EVENT; -}; - -/** ****************************************************************** - * Utility Functions - ******************************************************************* */ - -function triggerSettingsEvent() { - api.event.emit('settings', settings.get()); -} - -function isSecure(proto) { - return proto.toLowerCase().indexOf("https") === 0; -} - -function setProgress(value = 0, msg) { - value = (value * 100).round(4); - api.ui.progress.width = value+'%'; - if (self.debug) { - // console.log(msg, value.round(2)); - api.ui.prostatus.style.display = 'flex'; - if (msg) { - api.ui.prostatus.innerHTML = msg; - } else { - api.ui.prostatus.innerHTML = ''; - } - } -} - -function loadFile(ev) { - // use modern Filesystem api when available - if (false && window.showOpenFilePicker) { - window.showOpenFilePicker().then(files => { - return Promise.all(files.map(fh => fh.getFile())) - }).then(files => { - if (files.length) { - api.platform.load_files(files); - } - }).catch(e => { /* ignore cancel */ }); - return; - } - api.ui.load.click(); -} - -function editSettings(e) { - let current = settings.get(), - mode = getMode(), - name = e.target.getAttribute("name"), - load = current.sproc[mode][name], - loadstr = JSON.stringify(load,null,4).split('\n'); - api.uc.prompt(`settings for "${name}"`, loadstr).then(edit => { - if (edit) { - try { - current.sproc[mode][name] = JSON.parse(edit); - if (name === settings.proc().processName) { - api.conf.load(null, name); - } - api.conf.save(); - api.settings.sync.put(); - } catch (e) { - console.log({ malformed_settings: e }); - api.uc.alert('malformed settings object'); - } - } - }); -} - -function exportSettings(e) { - let current = settings.get(), - mode = getMode(), - name = e.target.getAttribute("name"), - data = api.util.b64enc({ - process: current.sproc[mode][name], - version, - moto: moto.id, - time: Date.now(), - mode, - name - }); - api.uc.prompt("Export Process Filename", name).then(name => { - if (name) { - api.util.download(data, `${name}.km`); - } - }); -} - -function deleteSettings(e) { - let current = settings.get(); - let name = e.target.getAttribute("del"); - delete current.sproc[getMode()][name]; - api.settings.sync.put(); - updateProcessList(); - api.conf.save(); - triggerSettingsEvent(); -} - -function updateProcessList() { - let current = settings.get(); - let list = [], s = current, sp = s.sproc[settings.mode()] || {}, table = api.ui.settingsList; - table.innerHTML = ''; - for (let k in sp) { - if (sp.hasOwnProperty(k)) list.push(k); - } - list.filter(n => n !=='default').sort().forEach(function(sk) { - let row = DOC.createElement('div'), - load = DOC.createElement('button'), - edit = DOC.createElement('button'), - xprt = DOC.createElement('button'), - del = DOC.createElement('button'), - name = sk; - - load.setAttribute('load', sk); - load.onclick = (ev) => { - api.conf.load(undefined, sk); - updateProcessList(); - modal.hide(); - } - load.appendChild(DOC.createTextNode(sk)); - if (sk == settings.proc().processName) { - load.setAttribute('class', 'selected') - } - api.ui.settingsName.value = settings.proc().processName; - - del.setAttribute('del', sk); - del.setAttribute('title', "remove '"+sk+"'"); - del.innerHTML = ''; - del.onclick = deleteSettings; - - edit.setAttribute('name', sk); - edit.setAttribute('title', 'edit'); - edit.innerHTML = ''; - edit.onclick = editSettings; - - xprt.setAttribute('name', sk); - xprt.setAttribute('title', 'export'); - xprt.innerHTML = ''; - xprt.onclick = exportSettings; - - row.setAttribute("class", "flow-row"); - row.appendChild(edit); - row.appendChild(load); - row.appendChild(xprt); - row.appendChild(del); - table.appendChild(row); - }); -} - -function downloadBlob(data, filename) { - let url = WIN.URL.createObjectURL(new Blob([data], {type: "octet/stream"})); - $('mod-any').innerHTML = `x`; - $('_dexport_').click(); -} - -export { FILES as catalog, LOCAL, SETUP, SECURE }; - -// Import frame.js last to avoid circular dependency issues -// import './frame.js'; diff --git a/src/kiri/ui/navigation.js b/src/kiri/ui/navigation.js new file mode 100644 index 00000000..b25b56ea --- /dev/null +++ b/src/kiri/ui/navigation.js @@ -0,0 +1,89 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { $ } from '../../moto/webui.js'; +import { api } from './api.js'; +import { space } from '../../moto/space.js'; +import { settingsOps } from './settings-ops.js'; + +const { selection } = api; + +function setupNavigation(ui, WIN, LANG) { + // bind interface action elements + $('export-support-a').onclick = (ev) => { ev.stopPropagation(); api.modal.show('don8') }; + $('mode-device').onclick = api.show.devices; + $('mode-profile').onclick = settingsOps.settingsLoad; + $('mode-fdm').onclick = () => api.mode.set('FDM'); + $('mode-cam').onclick = () => api.mode.set('CAM'); + $('mode-sla').onclick = () => api.mode.set('SLA'); + $('mode-laser').onclick = () => api.mode.set('LASER'); + $('mode-drag').onclick = () => api.mode.set('DRAG'); + $('mode-wjet').onclick = () => api.mode.set('WJET'); + $('mode-wedm').onclick = () => api.mode.set('WEDM'); + $('set-device').onclick = (ev) => { ev.stopPropagation(); api.show.devices() }; + $('set-profs').onclick = (ev) => { ev.stopPropagation(); api.conf.show() }; + $('set-tools').onclick = (ev) => { ev.stopPropagation(); api.show.tools() }; + $('set-prefs').onclick = (ev) => { ev.stopPropagation(); api.modal.show('prefs') }; + ui.acct.help.onclick = (ev) => { ev.stopPropagation(); api.help.show() }; + ui.acct.don8.onclick = (ev) => { ev.stopPropagation(); api.modal.show('don8') }; + ui.acct.mesh.onclick = (ev) => { ev.stopPropagation(); WIN.location = "/mesh" }; + ui.acct.export.onclick = (ev) => { ev.stopPropagation(); settingsOps.profileExport() }; + ui.acct.export.title = LANG.acct_xpo; + $('file-new').onclick = (ev) => { ev.stopPropagation(); settingsOps.workspaceNew() }; + $('file-recent').onclick = () => { api.modal.show('files') }; + $('file-import').onclick = (ev) => { api.event.import(ev); }; + ui.func.slice.onclick = (ev) => { ev.stopPropagation(); api.function.slice() }; + ui.func.preview.onclick = (ev) => { ev.stopPropagation(); api.function.print() }; + ui.func.animate.onclick = (ev) => { ev.stopPropagation(); api.function.animate() }; + ui.func.export.onclick = (ev) => { ev.stopPropagation(); api.function.export() }; + $('view-arrange').onclick = api.platform.layout; + $('view-top').onclick = space.view.top; + $('view-home').onclick = space.view.home; + $('view-front').onclick = space.view.front; + $('view-back').onclick = space.view.back; + $('view-left').onclick = space.view.left; + $('view-right').onclick = space.view.right; + $('unrotate').onclick = () => { + api.widgets.for(w => w.unrotate()); + selection.update_info(); + }; + // attach button handlers to support targets + for (let btn of ["don8pt","don8gh","don8pp"]) { + $(btn).onclick = (ev) => { + window.open(ev.target.children[0].href); + } + } + // rotation buttons + let d = (Math.PI / 180); + $('rot_x_lt').onclick = () => { selection.rotate(-d * $('rot_x').value,0,0) }; + $('rot_x_gt').onclick = () => { selection.rotate( d * $('rot_x').value,0,0) }; + $('rot_y_lt').onclick = () => { selection.rotate(0,-d * $('rot_y').value,0) }; + $('rot_y_gt').onclick = () => { selection.rotate(0, d * $('rot_y').value,0) }; + $('rot_z_lt').onclick = () => { selection.rotate(0,0, d * $('rot_z').value) }; + $('rot_z_gt').onclick = () => { selection.rotate(0,0,-d * $('rot_z').value) }; + // rendering options + $('render-edges').onclick = () => { api.view.edges({ toggle: true }); api.conf.save() }; + $('render-ghost').onclick = () => { api.view.wireframe(false, 0, api.view.is_arrange() ? 0.4 : 0.25); }; + $('render-wire').onclick = () => { api.view.wireframe(true, 0, api.space.is_dark() ? 0.25 : 0.5); }; + $('render-solid').onclick = () => { api.view.wireframe(false, 0, 1); }; + $('mesh-export-stl').onclick = () => { settingsOps.objectsExport('stl') }; + $('mesh-export-obj').onclick = () => { settingsOps.objectsExport('obj') }; + $('mesh-merge').onclick = selection.merge; + $('mesh-split').onclick = selection.isolateBodies; + $('context-duplicate').onclick = selection.duplicate; + $('context-mirror').onclick = selection.mirror; + $('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", + ev => api.space.set_focus(undefined, ev.object.point) + ); + }; + $('context-contents').onclick = () => { api.SPACE.view.fit() }; + $('view-fit').onclick = () => { api.SPACE.view.fit() }; + $('wassup').onmouseover = () => { $('suppopp').classList.remove('hide') }; +} + +export const navigation = { + setupNavigation +}; diff --git a/src/kiri/ui/preferences.js b/src/kiri/ui/preferences.js new file mode 100644 index 00000000..c3e3cc01 --- /dev/null +++ b/src/kiri/ui/preferences.js @@ -0,0 +1,160 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { $c } from '../../moto/webui.js'; +import { api } from './api.js'; +import { base } from '../../geo/base.js'; +import { space } from '../../moto/space.js'; + +let { client, platform } = api; +let ui, statsTimer; + +// Lazy initialization to avoid circular dependencies +function getUI() { + if (!ui) ui = api.ui; + return ui; +} + +function settings() { + return api.conf.get(); +} + +function unitsSave() { + api.conf.update({ controller: true }); + platform.update_size(); +} + +function aniMeshSave() { + api.conf.update({ controller: true }); + api.conf.save(); +} + +function lineTypeSave() { + const ui = getUI(); + const sel = ui.lineType.options[ui.lineType.selectedIndex]; + if (sel) { + settings().controller.lineType = sel.value; + api.conf.save(); + } +} + +function detailSave() { + const ui = getUI(); + let level = ui.detail.options[ui.detail.selectedIndex]; + if (level) { + level = level.value; + let rez = base.config.clipperClean; + switch (level) { + case '100': rez = 50; break; + case '75': rez = base.config.clipperClean; break; + case '50': rez = 500; break; + case '25': rez = 1000; break; + } + client.config({ + base: { clipperClean: rez } + }); + settings().controller.detail = level; + api.conf.save(); + } +} + +function speedSave() { + const ui = getUI(); + settings().controller.showSpeeds = ui.showSpeeds.checked; + api.view.update_speeds(); +} + +function setThreaded(bool) { + if (bool) { + client.pool.start(); + } else { + client.pool.stop(); + } + return bool; +} + +function booleanSave() { + const ui = getUI(); + let control = settings().controller; + if (control.assembly != ui.assembly.checked) { + client.wasm(ui.assembly.checked); + } + if (control.antiAlias != ui.antiAlias.checked) { + api.show.alert('Page Reload Required to Change Aliasing'); + } + 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.spaceRandoX = ui.spaceRandoX.checked; + // control.threaded = setThreaded(ui.threaded.checked); + control.webGPU = ui.webGPU.checked; + space.view.setZoom(control.reverseZoom, control.zoomSpeed); + // platform.layout(); + api.conf.save(); + api.platform.update_size(); + updateStats(); + updateDrawer(); + api.event.emit('boolean.update'); + space.view.setProjection(control.ortho ? 'orthographic' : 'perspective'); +} + +function updateDrawer() { + const { drawer, scrolls } = settings().controller; + $c('app', drawer ? 'slideshow' : '', drawer ? '' : 'slideshow'); + $c('app', scrolls ? '' : 'hide-scroll', scrolls ? 'hide-scroll' : ''); +} + +function updateStats() { + if (self.debug !== true) { + return; + } + console.log('updateStats()'); + const ui = getUI(); + let { div, fps, rms, rnfo } = ui.stats; + div.style.display = 'flex'; + clearTimeout(statsTimer); + statsTimer = setInterval(() => { + const nrms = space.view.getRMS().toFixed(1); + const nfps = space.view.getFPS().toFixed(1); + const rend = space.renderInfo(); + const { memory, render } = rend; + if (nfps !== fps.innerText) { + fps.innerText = nfps; + } + if (nrms !== rms.innerText) { + rms.innerText = nrms; + } + if (rnfo.offsetParent !== null) { + rnfo.innerHTML = Object.entries({ ...memory, ...render, render_ms: nrms, frames_sec: nfps }).map(row => { + return `
${row[0]}
` + }).join(''); + } + }, 100); +} + +export const preferences = { + unitsSave, + aniMeshSave, + lineTypeSave, + detailSave, + speedSave, + setThreaded, + booleanSave, + updateDrawer, + updateStats +}; diff --git a/src/kiri/ui/selection-tools.js b/src/kiri/ui/selection-tools.js new file mode 100644 index 00000000..bcb21e1d --- /dev/null +++ b/src/kiri/ui/selection-tools.js @@ -0,0 +1,161 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { $ } from '../../moto/webui.js'; +import { api } from './api.js'; +import { space } from '../../moto/space.js'; +import { updateTool } from '../mode/cam/tools.js'; + +const { selection } = api; + +function settings() { + return api.conf.get(); +} + +function boundsSelection() { + // Helper to compute bounds of selected meshes + const THREE = self.THREE; + let bounds = new THREE.Box3(); + selection.for_meshes(mesh => { + let box = mesh.getBoundingBox().clone(); + let pos = mesh.widget.mesh.position; + box.min.add(pos); + box.max.add(pos); + bounds = bounds.union(box); + }); + return bounds; +} + +function rotateInputSelection() { + if (selection.meshes().length === 0) { + api.show.alert("select object to rotate"); + return; + } + api.uc.prompt("Enter X,Y,Z degrees of rotation","").then(coord => { + coord = (coord || '').split(','); + let prod = Math.PI / 180, + x = parseFloat(coord[0] || 0.0) * prod, + y = parseFloat(coord[1] || 0.0) * prod, + z = parseFloat(coord[2] || 0.0) * prod; + selection.rotate(x, y, z); + }); +} + +function positionSelection() { + if (selection.meshes().length === 0) { + api.show.alert("select object to position"); + return; + } + let current = settings(), + { device, process} = current, + center = process.ctOriginCenter || process.camOriginCenter || device.bedRound || device.originCenter, + bounds = boundsSelection(); + + api.uc.prompt("Enter X,Y coordinates for selection","").then(coord => { + coord = (coord || '').split(','); + let x = parseFloat(coord[0] || 0.0), + y = parseFloat(coord[1] || 0.0), + z = parseFloat(coord[2] || 0.0); + + if (!center) { + x = x - device.bedWidth/2 + (bounds.max.x - bounds.min.x)/2; + y = y - device.bedDepth/2 + (bounds.max.y - bounds.min.y)/2 + } + + selection.move(x, y, z, true); + }); +} + +function selectionSize(e, ui) { + let dv = parseFloat(e.target.value || 1), + pv = parseFloat(e.target.was || 1), + ra = dv / pv, + xv = parseFloat(ui.sizeX.was ?? ui.scaleX.value) || 1, + yv = parseFloat(ui.sizeY.was ?? ui.scaleY.value) || 1, + zv = parseFloat(ui.sizeZ.was ?? ui.scaleZ.value) || 1, + ta = e.target, + xc = ui.lockX.checked, + yc = ui.lockY.checked, + zc = ui.lockZ.checked, + xt = ta === ui.sizeX, + yt = ta === ui.sizeY, + zt = ta === ui.sizeZ, + tl = (xt && xc) || (yt && yc) || (zt && zc), + xr = ((tl && xc) || (!tl && xt) ? ra : 1), + yr = ((tl && yc) || (!tl && yt) ? ra : 1), + zr = ((tl && zc) || (!tl && zt) ? ra : 1); + // prevent null scale + if (xv * xr < 0.1 || yv * yr < 0.1 || zv * zr < 0.1) { + api.alerts.show('invalid scale value'); + return; + } + selection.scale(xr,yr,zr); + ui.sizeX.was = ui.sizeX.value = xv * xr; + ui.sizeY.was = ui.sizeY.value = yv * yr; + ui.sizeZ.was = ui.sizeZ.value = zv * zr; +} + +function selectionScale(e, ui) { + let dv = parseFloat(e.target.value || 1), + pv = parseFloat(e.target.was || 1), + ra = dv / pv, + xv = parseFloat(ui.scaleX.was ?? ui.scaleX.value) || 1, + yv = parseFloat(ui.scaleY.was ?? ui.scaleY.value) || 1, + zv = parseFloat(ui.scaleZ.was ?? ui.scaleY.value) || 1, + ta = e.target, + xc = ui.lockX.checked, + yc = ui.lockY.checked, + zc = ui.lockZ.checked, + xt = ta === ui.scaleX, + yt = ta === ui.scaleY, + zt = ta === ui.scaleZ, + tl = (xt && xc) || (yt && yc) || (zt && zc), + xr = ((tl && xc) || (!tl && xt) ? ra : 1), + yr = ((tl && yc) || (!tl && yt) ? ra : 1), + zr = ((tl && zc) || (!tl && zt) ? ra : 1); + // prevent null scale + if (xv * xr < 0.1 || yv * yr < 0.1 || zv * zr < 0.1) { + api.alerts.show('invalid scale value'); + return; + } + selection.scale(xr,yr,zr); + ui.scaleX.was = ui.scaleX.value = xv * xr; + ui.scaleY.was = ui.scaleY.value = yv * yr; + ui.scaleZ.was = ui.scaleZ.value = zv * zr; +} + +function selectionRotate(e) { + let val = parseFloat(e.target.value) || 0; + e.target.value = val; +} + +function setupSelectionBindings(ui) { + // on enter but not on blur + space.event.onEnterKey([ + ui.scaleX, (e) => selectionScale(e, ui), + ui.scaleY, (e) => selectionScale(e, ui), + ui.scaleZ, (e) => selectionScale(e, ui), + ui.sizeX, (e) => selectionSize(e, ui), + ui.sizeY, (e) => selectionSize(e, ui), + ui.sizeZ, (e) => selectionSize(e, ui), + ]); + // on enter and blur + space.event.onEnterKey([ + ui.toolName, updateTool, + ui.toolNum, updateTool, + ui.toolFluteDiam, updateTool, + ui.toolFluteLen, updateTool, + ui.toolShaftDiam, updateTool, + ui.toolShaftLen, updateTool, + ui.toolTaperTip, updateTool, + ui.toolTaperAngle, updateTool, + $('rot_x'), selectionRotate, + $('rot_y'), selectionRotate, + $('rot_z'), selectionRotate + ], true); +} + +export const selectionTools = { + rotateInputSelection, + positionSelection, + setupSelectionBindings +}; diff --git a/src/kiri/ui/settings-ops.js b/src/kiri/ui/settings-ops.js new file mode 100644 index 00000000..e32b1cfd --- /dev/null +++ b/src/kiri/ui/settings-ops.js @@ -0,0 +1,138 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { $ } from '../../moto/webui.js'; +import { api } from './api.js'; +import { settings as set_ctrl } from './config/manager.js'; + +function settings() { + return api.conf.get(); +} + +function deviceExport(exp, name) { + name = (name || "device") + .toLowerCase() + .replace(/ /g,'_') + .replace(/\./g,'_'); + api.uc.prompt("Export Device Filename", name).then(name => { + if (name) { + api.util.download(exp, `${name}.km`); + } + }); +} + +function objectsExport(format = "stl") { + // return selection.export(); + api.uc.confirm("Export Filename", {ok:true, cancel: false}, `selected.${format}`).then(name => { + if (!name) return; + if (name.toLowerCase().indexOf(`.${format}`) < 0) { + name = `${name}.${format}`; + } + api.util.download(api.selection.export(format), name); + }); +} + +function workspaceNew() { + api.uc.confirm("Clear Workspace?", {ok:true, cancel: false}).then(value => { + if (value === true) { + let proc = set_ctrl.proc(); + proc.ops && (proc.ops.length = 0); + proc.op2 && (proc.op2.length = 0); + api.platform.clear(); + } + }); +} + +function profileExport() { + const opt = {pre: [ + "
", + "

Workspace Export

", + " ", + " ", + "  Exclude meshes", + "
" + ]}; + let suggestion = "workspace"; + let file = api.widgets.all()[0]?.meta.file || ''; + if (file) { + suggestion = `${suggestion}_${file.split('.')[0]}`.replaceAll(' ','_'); + }; + api.uc.confirm("Filename", {ok:true, cancel: false}, suggestion, opt).then(name => { + if (!name) return; + + let work = !$('excwork').checked; + let json = api.conf.export({work, clear:true}); + + api.client.zip([ + {name:"workspace.json", data:JSON.stringify(json)} + ], progress => { + api.show.progress(progress.percent/100, "compressing workspace"); + }, output => { + api.show.progress(0); + api.util.download(output, `${name}.kmz`); + }); + }); +} + +function settingsSave(ev, name) { + if (ev) { + ev.stopPropagation(); + ev.preventDefault(); + } + + api.dialog.hide(); + let mode = api.mode.get(), + s = settings(), + def = "default", + cp = s.process, + pl = s.sproc[mode], + lp = s.cproc[mode], + saveAs = (name) => { + if (!name) { + return; + } + let np = pl[name] = {}; + cp.processName = name; + pl[name] = Object.clone(cp); + for (let k in cp) { + if (!cp.hasOwnProperty(k)) continue; + np[k] = cp[k]; + } + s.cproc[mode] = name; + s.devproc[s.device.deviceName] = name; + api.conf.save(); + api.conf.update(); + api.event.settings(); + sync_put(); + }; + + if (name) { + saveAs(name); + } else { + api.uc.prompt("Save Settings As", cp ? lp || def : def).then(saveAs); + } +} + +function settingsLoad() { + api.conf.show(); +} + +function updateDeviceSize() { + api.conf.update(); + api.platform.update_size(); + api.platform.update_origin(); +} + +async function sync_put() { + await set_ctrl.sync.put(); +} + +export const settingsOps = { + deviceExport, + objectsExport, + workspaceNew, + profileExport, + settingsSave, + settingsLoad, + updateDeviceSize, + sync_put +}; diff --git a/src/kiri/ui/util.js b/src/kiri/ui/util.js new file mode 100644 index 00000000..1d022cc1 --- /dev/null +++ b/src/kiri/ui/util.js @@ -0,0 +1,52 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { $ } from '../../moto/webui.js'; +import { api } from './api.js'; + +const WIN = self.window; + +function isSecure(proto) { + return proto.toLowerCase().indexOf("https") === 0; +} + +function setProgress(value = 0, msg) { + value = (value * 100).round(4); + api.ui.progress.width = value+'%'; + if (self.debug) { + // console.log(msg, value.round(2)); + api.ui.prostatus.style.display = 'flex'; + if (msg) { + api.ui.prostatus.innerHTML = msg; + } else { + api.ui.prostatus.innerHTML = ''; + } + } +} + +function loadFile(ev) { + // use modern Filesystem api when available + if (false && window.showOpenFilePicker) { + window.showOpenFilePicker().then(files => { + return Promise.all(files.map(fh => fh.getFile())) + }).then(files => { + if (files.length) { + api.platform.load_files(files); + } + }).catch(e => { /* ignore cancel */ }); + return; + } + api.ui.load.click(); +} + +function downloadBlob(data, filename) { + let url = WIN.URL.createObjectURL(new Blob([data], {type: "octet/stream"})); + $('mod-any').innerHTML = `x`; + $('_dexport_').click(); +} + +export const util = { + isSecure, + download: downloadBlob, + setProgress, + loadFile +}; From 2bb67ab457388155d45987c7e910acfcc8d694a9 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 25 Dec 2025 00:50:59 -0500 Subject: [PATCH 021/154] asyncify init_* --- src/kiri/ui/init.js | 90 ++++++++++++++++++++++----------------------- src/main/kiri.js | 4 +- 2 files changed, 46 insertions(+), 48 deletions(-) diff --git a/src/kiri/ui/init.js b/src/kiri/ui/init.js index dbb7e70e..1b5afe2e 100644 --- a/src/kiri/ui/init.js +++ b/src/kiri/ui/init.js @@ -33,9 +33,9 @@ import STACKS from './stacks.js'; let { LOCAL, SETUP } = api, { CAM, SLA, FDM, LASER, DRAG, WJET, WEDM } = MODES, { client, catalog, platform, selection, stats } = api, - LANG = api.language.current, - WIN = self.window, DOC = self.document, + WIN = self.window, + LANG = api.language.current, STARTMODE = SETUP.sm && SETUP.sm.length === 1 ? SETUP.sm[0] : null, TWOD = [ LASER, DRAG, WJET, WEDM ], TWONED = [ LASER, DRAG, WJET ], @@ -46,8 +46,6 @@ let { LOCAL, SETUP } = api, NO_WEDM = [ FDM, CAM, SLA, LASER, DRAG, WJET ], FDM_CAM = [ FDM, CAM ], proto = location.protocol, - platformColor, - statsTimer, inline = true, driven = true, trigger = true, @@ -120,17 +118,11 @@ function onButtonClick(ev) { api.event.on('click.button', onButtonClick); -function keys(o) { - let key, list = []; - for (key in o) { if (o.hasOwnProperty(key)) list.push(key) } - return list.sort(); -} - -function clearSelected(children) { - for (let i=0; i { + api.space.restore(resolve) || checkSeed(resolve) || resolve(); + }); }; // SECOND STAGE INIT AFTER UI RESTORED -function init_two() { +async function init_two() { api.event.emit('init.two'); // load script extensions @@ -906,40 +900,44 @@ function lang_rewrite() { // init lang must happen before all other init functions function init_lang() { - // if a language needs to load, the script is injected and loaded - // first. once this loads, or doesn't, the initialization begins - let lang = SETUP.ln ? SETUP.ln[0] : sdb.getItem('kiri-lang') || api.language.get(); - api.event.emit('init.lang', lang); - // inject language script if not english - if (lang && lang !== 'en' && lang !== 'en-us') { - let map = api.language.map(lang); - let scr = DOC.createElement('script'); - // scr.setAttribute('defer',true); - scr.setAttribute('src',`/kiri/lang/${map}.js`); - (DOC.body || DOC.head).appendChild(scr); - stats.set('ll',lang); - scr.onload = function() { - api.language.set(map); - lang_rewrite(); - init_one(); - }; - scr.onerror = function(err) { - console.log({language_load_error: err, lang}) + return new Promise(resolve => { + // if a language needs to load, the script is injected and loaded + // first. once this loads, or doesn't, the initialization begins + let lang = SETUP.ln ? SETUP.ln[0] : sdb.getItem('kiri-lang') || api.language.get(); + api.event.emit('init.lang', lang); + // inject language script if not english + if (lang && lang !== 'en' && lang !== 'en-us') { + let map = api.language.map(lang); + let scr = DOC.createElement('script'); + // scr.setAttribute('defer',true); + scr.setAttribute('src',`/kiri/lang/${map}.js`); + (DOC.body || DOC.head).appendChild(scr); + stats.set('ll',lang); + scr.onload = function() { + api.language.set(map); + lang_rewrite(); + resolve(); + }; + scr.onerror = function(err) { + console.log({language_load_error: err, lang}) + api.language.set(); + lang_rewrite(); + resolve(); + } + } else { + // set to browser default which will be overridden + // by any future script loads (above) api.language.set(); lang_rewrite(); - init_one(); + resolve(); } - } else { - // set to browser default which will be overridden - // by any future script loads (above) - api.language.set(); - lang_rewrite(); - init_one(); - } + }); } -export function run() { +export async function run() { client.start(); - init_lang(); + await init_lang(); + await init_one(); + await init_two(); return api; } diff --git a/src/main/kiri.js b/src/main/kiri.js index 3827e559..ab6e00f7 100644 --- a/src/main/kiri.js +++ b/src/main/kiri.js @@ -35,11 +35,11 @@ function safeExec(fn) { } } -function checkReady() { +async function checkReady() { if (document.readyState === 'complete') { let bootctrl = navigator.serviceWorker.controller; console.log(`kiri | boot ctrl | ` + (bootctrl ? true : false)); - let api = kiri.api = run(); + let api = kiri.api = await run(); self.$ = api.web.$; for (let fn of load) { safeExec(fn); From bda2cdda57d9801224772c02b84d29384a449324 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 25 Dec 2025 01:12:58 -0500 Subject: [PATCH 022/154] split init into three startup functions --- src/kiri/ui/init-lang.js | 69 ++++++++ src/kiri/ui/{init.js => init-one.js} | 256 +-------------------------- src/kiri/ui/init-two.js | 212 ++++++++++++++++++++++ src/main/kiri.js | 26 ++- 4 files changed, 293 insertions(+), 270 deletions(-) create mode 100644 src/kiri/ui/init-lang.js rename src/kiri/ui/{init.js => init-one.js} (76%) create mode 100644 src/kiri/ui/init-two.js diff --git a/src/kiri/ui/init-lang.js b/src/kiri/ui/init-lang.js new file mode 100644 index 00000000..f94f8a13 --- /dev/null +++ b/src/kiri/ui/init-lang.js @@ -0,0 +1,69 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { api } from './api.js'; +import { local as sdb } from '../../data/local.js'; + +const DOC = self.document; +const { SETUP } = api; +const stats = api.stats; + +// update static html elements with language overrides +function lang_rewrite() { + const LANG = api.language.current; + // lk attribute causes inner text to be replaced with lang value + for (let el of [...DOC.querySelectorAll("[lk]")]) { + let key = el.getAttribute('lk'); + let val = LANG[key]; + if (val) { + el.innerText = val; + } else { + console.log({missing_ln: key}); + } + } + // lkt attribute causes a title attribute to be set from lang value + for (let el of [...DOC.querySelectorAll("[lkt]")]) { + let key = el.getAttribute('lkt'); + let val = LANG[key]; + if (val) { + el.setAttribute("title", val); + } else { + console.log({missing_ln: key}); + } + } +} + +// init lang must happen before all other init functions +export function init_lang() { + return new Promise(resolve => { + // if a language needs to load, the script is injected and loaded + // first. once this loads, or doesn't, the initialization begins + let lang = SETUP.ln ? SETUP.ln[0] : sdb.getItem('kiri-lang') || api.language.get(); + api.event.emit('init.lang', lang); + // inject language script if not english + if (lang && lang !== 'en' && lang !== 'en-us') { + let map = api.language.map(lang); + let scr = DOC.createElement('script'); + // scr.setAttribute('defer',true); + scr.setAttribute('src',`/kiri/lang/${map}.js`); + (DOC.body || DOC.head).appendChild(scr); + stats.set('ll',lang); + scr.onload = function() { + api.language.set(map); + lang_rewrite(); + resolve(); + }; + scr.onerror = function(err) { + console.log({language_load_error: err, lang}) + api.language.set(); + lang_rewrite(); + resolve(); + } + } else { + // set to browser default which will be overridden + // by any future script loads (above) + api.language.set(); + lang_rewrite(); + resolve(); + } + }); +} diff --git a/src/kiri/ui/init.js b/src/kiri/ui/init-one.js similarity index 76% rename from src/kiri/ui/init.js rename to src/kiri/ui/init-one.js index 1b5afe2e..e03bb769 100644 --- a/src/kiri/ui/init.js +++ b/src/kiri/ui/init-one.js @@ -19,7 +19,6 @@ import { menu as menuFDM } from '../mode/fdm/init-menu.js'; import { menu as menuLaser } from '../mode/laser/init-menu.js'; import { menu as menuSLA } from '../mode/sla/init-menu.js'; import { modal } from './modal.js'; -import { navigation } from './navigation.js'; import { preferences } from './preferences.js'; import { selectionTools } from './selection-tools.js'; import { settings as set_ctrl } from './config/manager.js'; @@ -687,257 +686,4 @@ function init_one() { }); }; -// SECOND STAGE INIT AFTER UI RESTORED -async function init_two() { - api.event.emit('init.two'); - - // load script extensions - if (SETUP.s) SETUP.s.forEach(function(lib) { - let scr = DOC.createElement('script'); - scr.setAttribute('async', true); - scr.setAttribute('defer', true); - scr.setAttribute('src',`/code/${lib}.js`); - DOC.body.appendChild(scr); - stats.add('load_'+lib); - api.event.emit('load.lib', lib); - }); - - // override stored settings - if (SETUP.v) SETUP.v.forEach(function(kv) { - kv = kv.split('='); - sdb.setItem(kv[0],kv[1]); - }); - - // import octoprint settings - if (SETUP.ophost) { - let ohost = api.const.OCTO = { - host: SETUP.ophost[0], - apik: SETUP.opkey ? SETUP.opkey[0] : '' - }; - sdb['octo-host'] = ohost.host; - sdb['octo-apik'] = ohost.apik; - console.log({octoprint:ohost}); - } - - // load workspace from url - if (SETUP.wrk) { - set_ctrl.import_url(`${proto}//${SETUP.wrk[0]}`, false); - } - - // load an object from url - if (SETUP.load) { - console.log({load:SETUP}); - api.platform.load_url(`${proto}//${SETUP.load[0]}`); - } - - // bind this to UI so main can call it on settings import - ui.sync = function() { - const current = settings(); - const control = current.controller; - - if (!control.devel) { - // TODO: hide thin type 3 during development - api.const.LISTS.thin.length = 3; - } - - platform.deselect(); - catalog.addFileListener(fileOps.updateCatalog); - space.view.setZoom(control.reverseZoom, control.zoomSpeed); - space.platform.setGridZOff(undefined); - space.platform.setZOff(0.05); - space.view.setProjection(control.ortho ? 'orthographic' : 'perspective'); - - // 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.spaceRandoX.checked = control.spaceRandoX; - // ui.threaded.checked = setThreaded(control.threaded); - ui.webGPU.checked = control.webGPU; - - preferences.setThreaded(true); - preferences.lineTypeSave(); - preferences.detailSave(); - preferences.updateStats(); - - // optional set-and-lock mode (hides mode menu) - let SETMODE = SETUP.mode ? SETUP.mode[0] : null; - - // optional set-and-lock device (hides device menu) - let DEVNAME = SETUP.dev ? SETUP.dev[0] : null; - - // setup default mode and enable mode locking, if set - api.mode.set(SETMODE || STARTMODE || current.mode, SETMODE); - - // fill device list - api.devices.refresh(); - - // update ui fields from settings - api.conf.update_fields(); - - // default to ARRANGE view mode - api.view.set(VIEWS.ARRANGE); - - // add ability to override (todo: restore?) - // api.show.controls(api.feature.controls); - - // update everything dependent on the platform size - platform.update_size(); - - // load wasm if indicated - client.wasm(control.assembly === true); - }; - - ui.sync(); - - // clear alerts as they build up - setInterval(api.event.alerts, 1000); - - // add hide-alerts-on-alert-click - ui.alert.dialog.onclick = function() { - api.event.alerts(true); - }; - - // enable modal hiding - $('mod-x').onclick = api.modal.hide; - - if (!SETUP.s) console.log(`kiri | init main | ${version}`); - - // send init-done event - api.event.emit('init-done', stats); - - // show gdpr if it's never been seen and we're not iframed - const isLocal = LOCAL || WIN.location.host.split(':')[0] === 'localhost'; - if (!sdb.gdpr && WIN.self === WIN.top && !SETUP.debug && !isLocal) { - $('gdpr').style.display = 'flex'; - } - - // warn of degraded functionality when SharedArrayBuffers are missing - if (api.feature.work_alerts && !window.SharedArrayBuffer) { - api.alerts.show("The security context of this", 10); - api.alerts.show("Window blocks important functionality.", 10); - api.alerts.show("Try a Chromium-base Browser instead", 10); - } - - // add keyboard focus handler (must use for iframes) - WIN.addEventListener('load', function () { - WIN.focus(); - DOC.body.addEventListener('click', function() { - WIN.focus(); - },false); - }); - - // dismiss gdpr alert - $('gotit').onclick = () => { - $('gdpr').style.display = 'none'; - sdb.gdpr = Date.now(); - }; - - // Setup navigation button bindings - navigation.setupNavigation(ui, WIN, LANG); - - // ui.modal.onclick = api.modal.hide; - ui.modalBox.onclick = (ev) => { ev.stopPropagation() }; - - // add app name hover info - $('app-info').innerText = version; - - // show topline separator when iframed - try { if (WIN.self !== WIN.top) $('top-sep').style.display = 'flex' } catch (e) { console.log(e) } - - // warn users they are running a beta release - if (beta && beta > 0 && sdb.kiri_beta != beta) { - api.show.alert("CAUTION"); - api.show.alert("this is a development release"); - api.show.alert("and may not function properly"); - sdb.kiri_beta = beta; - } - - // hide url params but preserve version root (when present) - let wlp = WIN.location.pathname; - let kio = wlp.indexOf('/kiri/'); - if (kio >= 0) { - history.replaceState({}, '', wlp.substring(0,kio + 6)); - } - - // lift curtain - $('curtain').style.display = 'none'; -} - -// update static html elements with language overrides -function lang_rewrite() { - // lk attribute causes inner text to be replaced with lang value - for (let el of [...DOC.querySelectorAll("[lk]")]) { - let key = el.getAttribute('lk'); - let val = LANG[key]; - if (val) { - el.innerText = val; - } else { - console.log({missing_ln: key}); - } - } - // lkt attribute causes a title attribute to be set from lang value - for (let el of [...DOC.querySelectorAll("[lkt]")]) { - let key = el.getAttribute('lkt'); - let val = LANG[key]; - if (val) { - el.setAttribute("title", val); - } else { - console.log({missing_ln: key}); - } - } -} - -// init lang must happen before all other init functions -function init_lang() { - return new Promise(resolve => { - // if a language needs to load, the script is injected and loaded - // first. once this loads, or doesn't, the initialization begins - let lang = SETUP.ln ? SETUP.ln[0] : sdb.getItem('kiri-lang') || api.language.get(); - api.event.emit('init.lang', lang); - // inject language script if not english - if (lang && lang !== 'en' && lang !== 'en-us') { - let map = api.language.map(lang); - let scr = DOC.createElement('script'); - // scr.setAttribute('defer',true); - scr.setAttribute('src',`/kiri/lang/${map}.js`); - (DOC.body || DOC.head).appendChild(scr); - stats.set('ll',lang); - scr.onload = function() { - api.language.set(map); - lang_rewrite(); - resolve(); - }; - scr.onerror = function(err) { - console.log({language_load_error: err, lang}) - api.language.set(); - lang_rewrite(); - resolve(); - } - } else { - // set to browser default which will be overridden - // by any future script loads (above) - api.language.set(); - lang_rewrite(); - resolve(); - } - }); -} - -export async function run() { - client.start(); - await init_lang(); - await init_one(); - await init_two(); - return api; -} +export { isBelt, isNotBelt, onBooleanClick, updateTool, init_one }; diff --git a/src/kiri/ui/init-two.js b/src/kiri/ui/init-two.js new file mode 100644 index 00000000..0e0133b6 --- /dev/null +++ b/src/kiri/ui/init-two.js @@ -0,0 +1,212 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { $ } from '../../moto/webui.js'; +import { api } from './api.js'; +import { beta, version } from '../../moto/license.js'; +import { fileOps } from './file-ops.js'; +import { local as sdb } from '../../data/local.js'; +import { navigation } from './navigation.js'; +import { preferences } from './preferences.js'; +import { settings as set_ctrl } from './config/manager.js'; +import { space } from '../../moto/space.js'; +import { VIEWS } from '../core/consts.js'; + +const DOC = self.document; +const WIN = self.window; +const proto = location.protocol; +const { SETUP, LOCAL } = api; +const { platform, client, catalog, stats } = api; +const LANG = api.language.current; +const ui = api.ui; +const STARTMODE = SETUP.sm && SETUP.sm.length === 1 ? SETUP.sm[0] : null; + +function settings() { + return api.conf.get(); +} + +// SECOND STAGE INIT AFTER UI RESTORED +export async function init_two() { + api.event.emit('init.two'); + + // load script extensions + if (SETUP.s) SETUP.s.forEach(function(lib) { + let scr = DOC.createElement('script'); + scr.setAttribute('async', true); + scr.setAttribute('defer', true); + scr.setAttribute('src',`/code/${lib}.js`); + DOC.body.appendChild(scr); + stats.add('load_'+lib); + api.event.emit('load.lib', lib); + }); + + // override stored settings + if (SETUP.v) SETUP.v.forEach(function(kv) { + kv = kv.split('='); + sdb.setItem(kv[0],kv[1]); + }); + + // import octoprint settings + if (SETUP.ophost) { + let ohost = api.const.OCTO = { + host: SETUP.ophost[0], + apik: SETUP.opkey ? SETUP.opkey[0] : '' + }; + sdb['octo-host'] = ohost.host; + sdb['octo-apik'] = ohost.apik; + console.log({octoprint:ohost}); + } + + // load workspace from url + if (SETUP.wrk) { + set_ctrl.import_url(`${proto}//${SETUP.wrk[0]}`, false); + } + + // load an object from url + if (SETUP.load) { + console.log({load:SETUP}); + api.platform.load_url(`${proto}//${SETUP.load[0]}`); + } + + // bind this to UI so main can call it on settings import + ui.sync = function() { + const current = settings(); + const control = current.controller; + + if (!control.devel) { + // TODO: hide thin type 3 during development + api.const.LISTS.thin.length = 3; + } + + platform.deselect(); + catalog.addFileListener(fileOps.updateCatalog); + space.view.setZoom(control.reverseZoom, control.zoomSpeed); + space.platform.setGridZOff(undefined); + space.platform.setZOff(0.05); + space.view.setProjection(control.ortho ? 'orthographic' : 'perspective'); + + // 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.spaceRandoX.checked = control.spaceRandoX; + // ui.threaded.checked = setThreaded(control.threaded); + ui.webGPU.checked = control.webGPU; + + preferences.setThreaded(true); + preferences.lineTypeSave(); + preferences.detailSave(); + preferences.updateStats(); + + // optional set-and-lock mode (hides mode menu) + let SETMODE = SETUP.mode ? SETUP.mode[0] : null; + + // optional set-and-lock device (hides device menu) + let DEVNAME = SETUP.dev ? SETUP.dev[0] : null; + + // setup default mode and enable mode locking, if set + api.mode.set(SETMODE || STARTMODE || current.mode, SETMODE); + + // fill device list + api.devices.refresh(); + + // update ui fields from settings + api.conf.update_fields(); + + // default to ARRANGE view mode + api.view.set(VIEWS.ARRANGE); + + // add ability to override (todo: restore?) + // api.show.controls(api.feature.controls); + + // update everything dependent on the platform size + platform.update_size(); + + // load wasm if indicated + client.wasm(control.assembly === true); + }; + + ui.sync(); + + // clear alerts as they build up + setInterval(api.event.alerts, 1000); + + // add hide-alerts-on-alert-click + ui.alert.dialog.onclick = function() { + api.event.alerts(true); + }; + + // enable modal hiding + $('mod-x').onclick = api.modal.hide; + + if (!SETUP.s) console.log(`kiri | init main | ${version}`); + + // send init-done event + api.event.emit('init-done', stats); + + // show gdpr if it's never been seen and we're not iframed + const isLocal = LOCAL || WIN.location.host.split(':')[0] === 'localhost'; + if (!sdb.gdpr && WIN.self === WIN.top && !SETUP.debug && !isLocal) { + $('gdpr').style.display = 'flex'; + } + + // warn of degraded functionality when SharedArrayBuffers are missing + if (api.feature.work_alerts && !window.SharedArrayBuffer) { + api.alerts.show("The security context of this", 10); + api.alerts.show("Window blocks important functionality.", 10); + api.alerts.show("Try a Chromium-base Browser instead", 10); + } + + // add keyboard focus handler (must use for iframes) + WIN.addEventListener('load', function () { + WIN.focus(); + DOC.body.addEventListener('click', function() { + WIN.focus(); + },false); + }); + + // dismiss gdpr alert + $('gotit').onclick = () => { + $('gdpr').style.display = 'none'; + sdb.gdpr = Date.now(); + }; + + // Setup navigation button bindings + navigation.setupNavigation(ui, WIN, LANG); + + // ui.modal.onclick = api.modal.hide; + ui.modalBox.onclick = (ev) => { ev.stopPropagation() }; + + // add app name hover info + $('app-info').innerText = version; + + // show topline separator when iframed + try { if (WIN.self !== WIN.top) $('top-sep').style.display = 'flex' } catch (e) { console.log(e) } + + // warn users they are running a beta release + if (beta && beta > 0 && sdb.kiri_beta != beta) { + api.show.alert("CAUTION"); + api.show.alert("this is a development release"); + api.show.alert("and may not function properly"); + sdb.kiri_beta = beta; + } + + // hide url params but preserve version root (when present) + let wlp = WIN.location.pathname; + let kio = wlp.indexOf('/kiri/'); + if (kio >= 0) { + history.replaceState({}, '', wlp.substring(0,kio + 6)); + } + + // lift curtain + $('curtain').style.display = 'none'; +} diff --git a/src/main/kiri.js b/src/main/kiri.js index ab6e00f7..a7f97343 100644 --- a/src/main/kiri.js +++ b/src/main/kiri.js @@ -6,24 +6,14 @@ import '../add/three.js'; import '../kiri/ui/lang.js'; import '../kiri/core/lang-en.js'; -import { broker } from '../moto/broker.js'; -import { run } from '../kiri/ui/init.js'; +import { api } from '../kiri/ui/api.js'; +import { init_lang } from '../kiri/ui/init-lang.js'; +import { init_one } from '../kiri/ui/init-one.js'; +import { init_two } from '../kiri/ui/init-two.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) { @@ -39,8 +29,14 @@ async function checkReady() { if (document.readyState === 'complete') { let bootctrl = navigator.serviceWorker.controller; console.log(`kiri | boot ctrl | ` + (bootctrl ? true : false)); - let api = kiri.api = await run(); + kiri.api = api; self.$ = api.web.$; + { + api.client.start(); + await init_lang(); + await init_one(); + await init_two(); + } for (let fn of load) { safeExec(fn); } From 4792ec6fb4798a92b5be1c93e606044d26491c5b Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 25 Dec 2025 01:24:35 -0500 Subject: [PATCH 023/154] clean up api and init exports --- src/kiri/mode/cam/cl-origin.js | 2 +- src/kiri/ui/api.js | 8 ++------ src/kiri/ui/init-lang.js | 2 +- src/kiri/ui/init-one.js | 12 +++++------- src/kiri/ui/init-two.js | 2 +- web/kiri/lang/en.js | 3 ++- 6 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/kiri/mode/cam/cl-origin.js b/src/kiri/mode/cam/cl-origin.js index 187ec947..3a99a0bf 100644 --- a/src/kiri/mode/cam/cl-origin.js +++ b/src/kiri/mode/cam/cl-origin.js @@ -7,7 +7,7 @@ import { addbox, clearboxes } from '../../core/boxes.js'; export let originSelectOn = false; -let { world } = api.SPACE; +let { world } = api.const.SPACE; let alert, lastHover, lastPoints; export function originSelect() { diff --git a/src/kiri/ui/api.js b/src/kiri/ui/api.js index 9d705a1a..beb33cba 100644 --- a/src/kiri/ui/api.js +++ b/src/kiri/ui/api.js @@ -41,7 +41,7 @@ import { visuals } from './visuals.js'; import { widgets } from '../core/widgets.js'; import { workspace } from './workspace.js'; -// From main.js - environment setup +// environment setup let LOC = self.location, SETUP = utils.parseOpt(LOC.search.substring(1)), SECURE = utilModule.isSecure(LOC.protocol), @@ -83,7 +83,7 @@ export const api = { clone: Object.clone, color, conf: settings.conf, - const: { LANG, LOCAL, SETUP, SECURE, STACKS, ...consts }, + const: { LANG, LOCAL, SETUP, SECURE, SPACE, STACKS, ...consts }, devel: { get enabled() { return settings.ctrl().devel; @@ -159,7 +159,6 @@ export const api = { lists, load, local, - LOCAL, modal, mode: modeModule, new: { @@ -173,10 +172,8 @@ export const api = { platform, process: processModule, sdb: dataLocal, - SECURE, selection, settings, - SETUP, show: { alert() { return alerts.show(...arguments) }, controls() { console.trace('deprecated') }, @@ -189,7 +186,6 @@ export const api = { tools: showTools }, space: workspace, - SPACE, stacks: STACKS, stats, tool: { diff --git a/src/kiri/ui/init-lang.js b/src/kiri/ui/init-lang.js index f94f8a13..550fb501 100644 --- a/src/kiri/ui/init-lang.js +++ b/src/kiri/ui/init-lang.js @@ -3,8 +3,8 @@ import { api } from './api.js'; import { local as sdb } from '../../data/local.js'; +const { SETUP } = api.const; const DOC = self.document; -const { SETUP } = api; const stats = api.stats; // update static html elements with language overrides diff --git a/src/kiri/ui/init-one.js b/src/kiri/ui/init-one.js index e03bb769..77a39179 100644 --- a/src/kiri/ui/init-one.js +++ b/src/kiri/ui/init-one.js @@ -2,7 +2,7 @@ import { $ } from '../../moto/webui.js'; import { api } from './api.js'; -import { beta, version } from '../../moto/license.js'; +import { version } from '../../moto/license.js'; import { fileOps } from './file-ops.js'; import { init as initCAM } from '../mode/cam/init-ui.js'; import { init as initDRAG } from '../mode/drag/init-ui.js'; @@ -29,13 +29,12 @@ import { VIEWS, MODES, SEED } from '../core/consts.js'; import STACKS from './stacks.js'; -let { LOCAL, SETUP } = api, +let { SETUP } = api.const, { CAM, SLA, FDM, LASER, DRAG, WJET, WEDM } = MODES, - { client, catalog, platform, selection, stats } = api, + { catalog, platform, selection, stats } = api, DOC = self.document, WIN = self.window, LANG = api.language.current, - STARTMODE = SETUP.sm && SETUP.sm.length === 1 ? SETUP.sm[0] : null, TWOD = [ LASER, DRAG, WJET, WEDM ], TWONED = [ LASER, DRAG, WJET ], THREED = [ FDM, CAM, SLA ], @@ -44,7 +43,6 @@ let { LOCAL, SETUP } = api, FDM_LZN = [ FDM, ...TWONED ], NO_WEDM = [ FDM, CAM, SLA, LASER, DRAG, WJET ], FDM_CAM = [ FDM, CAM ], - proto = location.protocol, inline = true, driven = true, trigger = true, @@ -131,7 +129,7 @@ function isNotBelt() { return !isBelt(); } -// MAIN INITIALIZATION FUNCTION +// MAIN UI INITIALIZATION FUNCTION function init_one() { let { event, conf, view, show } = api, { newBlank, newButton, newBoolean, newGroup, newInput } = uc, @@ -686,4 +684,4 @@ function init_one() { }); }; -export { isBelt, isNotBelt, onBooleanClick, updateTool, init_one }; +export { onBooleanClick, updateTool, init_one }; diff --git a/src/kiri/ui/init-two.js b/src/kiri/ui/init-two.js index 0e0133b6..77bca345 100644 --- a/src/kiri/ui/init-two.js +++ b/src/kiri/ui/init-two.js @@ -14,7 +14,7 @@ import { VIEWS } from '../core/consts.js'; const DOC = self.document; const WIN = self.window; const proto = location.protocol; -const { SETUP, LOCAL } = api; +const { SETUP, LOCAL } = api.const; const { platform, client, catalog, stats } = api; const LANG = api.language.current; const ui = api.ui; diff --git a/web/kiri/lang/en.js b/web/kiri/lang/en.js index 82816abe..d70cf351 100644 --- a/web/kiri/lang/en.js +++ b/web/kiri/lang/en.js @@ -8,8 +8,9 @@ self.lang['en-us'] = { axis: "axis", // left object scale pop menu back: "back", clear: "clear", // clear workspace (remove all objects) - copy: "copy", clone: "clone", // create duplicate + contents: "contents", + copy: "copy", delete: "delete", detail: "detail", done: "done", From d8ddb45bcb463e741045e6a031ccab3f02b2f324 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 25 Dec 2025 01:44:20 -0500 Subject: [PATCH 024/154] moved kiri/ui to kiri/app --- src/kiri/{ui => app}/alerts.js | 0 src/kiri/{ui => app}/api.js | 0 src/kiri/{ui => app}/client.js | 0 src/kiri/{ui => app}/component.js | 0 src/kiri/{ui => app}/config/defaults.js | 0 src/kiri/{ui => app}/config/dialog.js | 0 src/kiri/{ui => app}/config/manager.js | 0 src/kiri/{ui => app}/devices.js | 0 src/kiri/{ui => app}/do.js | 0 src/kiri/{ui => app}/export.js | 0 src/kiri/{ui => app}/file-ops.js | 0 src/kiri/{ui => app}/files.js | 0 src/kiri/{ui => app}/frame.js | 0 src/kiri/{ui => app}/function.js | 0 src/kiri/{ui => app}/groups.js | 0 src/kiri/{ui => app}/help.js | 0 src/kiri/{ui => app}/image.js | 0 src/kiri/{ui => app}/init-lang.js | 0 src/kiri/{ui => app}/init-one.js | 0 src/kiri/{ui => app}/init-two.js | 0 src/kiri/{ui => app}/interact.js | 0 src/kiri/{ui => app}/keyboard.js | 0 src/kiri/{ui => app}/lang.js | 0 src/kiri/{ui => app}/layers.js | 0 src/kiri/{ui => app}/local-storage.js | 0 src/kiri/{ui => app}/modal.js | 0 src/kiri/{ui => app}/mode.js | 0 src/kiri/{ui => app}/navigation.js | 0 src/kiri/{ui => app}/pack.js | 0 src/kiri/{ui => app}/platform.js | 0 src/kiri/{ui => app}/preferences.js | 0 src/kiri/{ui => app}/select.js | 0 src/kiri/{ui => app}/selection-tools.js | 0 src/kiri/{ui => app}/settings-ops.js | 0 src/kiri/{ui => app}/slider.js | 0 src/kiri/{ui => app}/stack.js | 0 src/kiri/{ui => app}/stacks.js | 0 src/kiri/{ui => app}/stats.js | 0 src/kiri/{ui => app}/tools.js | 0 src/kiri/{ui => app}/util.js | 0 src/kiri/{ui => app}/view-state.js | 0 src/kiri/{ui => app}/visuals.js | 0 src/kiri/{ui => app}/workspace.js | 0 src/kiri/core/codec.js | 2 +- src/kiri/core/render.js | 2 +- src/kiri/core/slice.js | 2 +- src/kiri/core/widgets.js | 2 +- src/kiri/mode/cam/anim-2d-fe.js | 2 +- src/kiri/mode/cam/anim-3d-fe.js | 2 +- src/kiri/mode/cam/cl-flip.js | 2 +- src/kiri/mode/cam/cl-helical.js | 2 +- src/kiri/mode/cam/cl-hole.js | 2 +- src/kiri/mode/cam/cl-ops.js | 2 +- src/kiri/mode/cam/cl-origin.js | 2 +- src/kiri/mode/cam/cl-stock.js | 2 +- src/kiri/mode/cam/cl-surface.js | 2 +- src/kiri/mode/cam/cl-tab.js | 2 +- src/kiri/mode/cam/cl-trace.js | 6 +++--- src/kiri/mode/cam/driver-fe.js | 2 +- src/kiri/mode/cam/init-menu.js | 2 +- src/kiri/mode/cam/init-ui.js | 6 +++--- src/kiri/mode/cam/tools.js | 4 ++-- src/kiri/mode/fdm/init-menu.js | 2 +- src/kiri/mode/fdm/init-ui.js | 10 +++++----- src/kiri/mode/laser/driver.js | 2 +- src/kiri/mode/laser/init-menu.js | 2 +- src/kiri/mode/sla/init-menu.js | 2 +- src/kiri/mode/sla/init-ui.js | 2 +- src/kiri/mode/wedm/init-ui.js | 2 +- src/kiri/run/engine.js | 6 +++--- src/main/kiri.js | 10 +++++----- 71 files changed, 43 insertions(+), 43 deletions(-) rename src/kiri/{ui => app}/alerts.js (100%) rename src/kiri/{ui => app}/api.js (100%) rename src/kiri/{ui => app}/client.js (100%) rename src/kiri/{ui => app}/component.js (100%) rename src/kiri/{ui => app}/config/defaults.js (100%) rename src/kiri/{ui => app}/config/dialog.js (100%) rename src/kiri/{ui => app}/config/manager.js (100%) rename src/kiri/{ui => app}/devices.js (100%) rename src/kiri/{ui => app}/do.js (100%) rename src/kiri/{ui => app}/export.js (100%) rename src/kiri/{ui => app}/file-ops.js (100%) rename src/kiri/{ui => app}/files.js (100%) rename src/kiri/{ui => app}/frame.js (100%) rename src/kiri/{ui => app}/function.js (100%) rename src/kiri/{ui => app}/groups.js (100%) rename src/kiri/{ui => app}/help.js (100%) rename src/kiri/{ui => app}/image.js (100%) rename src/kiri/{ui => app}/init-lang.js (100%) rename src/kiri/{ui => app}/init-one.js (100%) rename src/kiri/{ui => app}/init-two.js (100%) rename src/kiri/{ui => app}/interact.js (100%) rename src/kiri/{ui => app}/keyboard.js (100%) rename src/kiri/{ui => app}/lang.js (100%) rename src/kiri/{ui => app}/layers.js (100%) rename src/kiri/{ui => app}/local-storage.js (100%) rename src/kiri/{ui => app}/modal.js (100%) rename src/kiri/{ui => app}/mode.js (100%) rename src/kiri/{ui => app}/navigation.js (100%) rename src/kiri/{ui => app}/pack.js (100%) rename src/kiri/{ui => app}/platform.js (100%) rename src/kiri/{ui => app}/preferences.js (100%) rename src/kiri/{ui => app}/select.js (100%) rename src/kiri/{ui => app}/selection-tools.js (100%) rename src/kiri/{ui => app}/settings-ops.js (100%) rename src/kiri/{ui => app}/slider.js (100%) rename src/kiri/{ui => app}/stack.js (100%) rename src/kiri/{ui => app}/stacks.js (100%) rename src/kiri/{ui => app}/stats.js (100%) rename src/kiri/{ui => app}/tools.js (100%) rename src/kiri/{ui => app}/util.js (100%) rename src/kiri/{ui => app}/view-state.js (100%) rename src/kiri/{ui => app}/visuals.js (100%) rename src/kiri/{ui => app}/workspace.js (100%) diff --git a/src/kiri/ui/alerts.js b/src/kiri/app/alerts.js similarity index 100% rename from src/kiri/ui/alerts.js rename to src/kiri/app/alerts.js diff --git a/src/kiri/ui/api.js b/src/kiri/app/api.js similarity index 100% rename from src/kiri/ui/api.js rename to src/kiri/app/api.js diff --git a/src/kiri/ui/client.js b/src/kiri/app/client.js similarity index 100% rename from src/kiri/ui/client.js rename to src/kiri/app/client.js diff --git a/src/kiri/ui/component.js b/src/kiri/app/component.js similarity index 100% rename from src/kiri/ui/component.js rename to src/kiri/app/component.js diff --git a/src/kiri/ui/config/defaults.js b/src/kiri/app/config/defaults.js similarity index 100% rename from src/kiri/ui/config/defaults.js rename to src/kiri/app/config/defaults.js diff --git a/src/kiri/ui/config/dialog.js b/src/kiri/app/config/dialog.js similarity index 100% rename from src/kiri/ui/config/dialog.js rename to src/kiri/app/config/dialog.js diff --git a/src/kiri/ui/config/manager.js b/src/kiri/app/config/manager.js similarity index 100% rename from src/kiri/ui/config/manager.js rename to src/kiri/app/config/manager.js diff --git a/src/kiri/ui/devices.js b/src/kiri/app/devices.js similarity index 100% rename from src/kiri/ui/devices.js rename to src/kiri/app/devices.js diff --git a/src/kiri/ui/do.js b/src/kiri/app/do.js similarity index 100% rename from src/kiri/ui/do.js rename to src/kiri/app/do.js diff --git a/src/kiri/ui/export.js b/src/kiri/app/export.js similarity index 100% rename from src/kiri/ui/export.js rename to src/kiri/app/export.js diff --git a/src/kiri/ui/file-ops.js b/src/kiri/app/file-ops.js similarity index 100% rename from src/kiri/ui/file-ops.js rename to src/kiri/app/file-ops.js diff --git a/src/kiri/ui/files.js b/src/kiri/app/files.js similarity index 100% rename from src/kiri/ui/files.js rename to src/kiri/app/files.js diff --git a/src/kiri/ui/frame.js b/src/kiri/app/frame.js similarity index 100% rename from src/kiri/ui/frame.js rename to src/kiri/app/frame.js diff --git a/src/kiri/ui/function.js b/src/kiri/app/function.js similarity index 100% rename from src/kiri/ui/function.js rename to src/kiri/app/function.js diff --git a/src/kiri/ui/groups.js b/src/kiri/app/groups.js similarity index 100% rename from src/kiri/ui/groups.js rename to src/kiri/app/groups.js diff --git a/src/kiri/ui/help.js b/src/kiri/app/help.js similarity index 100% rename from src/kiri/ui/help.js rename to src/kiri/app/help.js diff --git a/src/kiri/ui/image.js b/src/kiri/app/image.js similarity index 100% rename from src/kiri/ui/image.js rename to src/kiri/app/image.js diff --git a/src/kiri/ui/init-lang.js b/src/kiri/app/init-lang.js similarity index 100% rename from src/kiri/ui/init-lang.js rename to src/kiri/app/init-lang.js diff --git a/src/kiri/ui/init-one.js b/src/kiri/app/init-one.js similarity index 100% rename from src/kiri/ui/init-one.js rename to src/kiri/app/init-one.js diff --git a/src/kiri/ui/init-two.js b/src/kiri/app/init-two.js similarity index 100% rename from src/kiri/ui/init-two.js rename to src/kiri/app/init-two.js diff --git a/src/kiri/ui/interact.js b/src/kiri/app/interact.js similarity index 100% rename from src/kiri/ui/interact.js rename to src/kiri/app/interact.js diff --git a/src/kiri/ui/keyboard.js b/src/kiri/app/keyboard.js similarity index 100% rename from src/kiri/ui/keyboard.js rename to src/kiri/app/keyboard.js diff --git a/src/kiri/ui/lang.js b/src/kiri/app/lang.js similarity index 100% rename from src/kiri/ui/lang.js rename to src/kiri/app/lang.js diff --git a/src/kiri/ui/layers.js b/src/kiri/app/layers.js similarity index 100% rename from src/kiri/ui/layers.js rename to src/kiri/app/layers.js diff --git a/src/kiri/ui/local-storage.js b/src/kiri/app/local-storage.js similarity index 100% rename from src/kiri/ui/local-storage.js rename to src/kiri/app/local-storage.js diff --git a/src/kiri/ui/modal.js b/src/kiri/app/modal.js similarity index 100% rename from src/kiri/ui/modal.js rename to src/kiri/app/modal.js diff --git a/src/kiri/ui/mode.js b/src/kiri/app/mode.js similarity index 100% rename from src/kiri/ui/mode.js rename to src/kiri/app/mode.js diff --git a/src/kiri/ui/navigation.js b/src/kiri/app/navigation.js similarity index 100% rename from src/kiri/ui/navigation.js rename to src/kiri/app/navigation.js diff --git a/src/kiri/ui/pack.js b/src/kiri/app/pack.js similarity index 100% rename from src/kiri/ui/pack.js rename to src/kiri/app/pack.js diff --git a/src/kiri/ui/platform.js b/src/kiri/app/platform.js similarity index 100% rename from src/kiri/ui/platform.js rename to src/kiri/app/platform.js diff --git a/src/kiri/ui/preferences.js b/src/kiri/app/preferences.js similarity index 100% rename from src/kiri/ui/preferences.js rename to src/kiri/app/preferences.js diff --git a/src/kiri/ui/select.js b/src/kiri/app/select.js similarity index 100% rename from src/kiri/ui/select.js rename to src/kiri/app/select.js diff --git a/src/kiri/ui/selection-tools.js b/src/kiri/app/selection-tools.js similarity index 100% rename from src/kiri/ui/selection-tools.js rename to src/kiri/app/selection-tools.js diff --git a/src/kiri/ui/settings-ops.js b/src/kiri/app/settings-ops.js similarity index 100% rename from src/kiri/ui/settings-ops.js rename to src/kiri/app/settings-ops.js diff --git a/src/kiri/ui/slider.js b/src/kiri/app/slider.js similarity index 100% rename from src/kiri/ui/slider.js rename to src/kiri/app/slider.js diff --git a/src/kiri/ui/stack.js b/src/kiri/app/stack.js similarity index 100% rename from src/kiri/ui/stack.js rename to src/kiri/app/stack.js diff --git a/src/kiri/ui/stacks.js b/src/kiri/app/stacks.js similarity index 100% rename from src/kiri/ui/stacks.js rename to src/kiri/app/stacks.js diff --git a/src/kiri/ui/stats.js b/src/kiri/app/stats.js similarity index 100% rename from src/kiri/ui/stats.js rename to src/kiri/app/stats.js diff --git a/src/kiri/ui/tools.js b/src/kiri/app/tools.js similarity index 100% rename from src/kiri/ui/tools.js rename to src/kiri/app/tools.js diff --git a/src/kiri/ui/util.js b/src/kiri/app/util.js similarity index 100% rename from src/kiri/ui/util.js rename to src/kiri/app/util.js diff --git a/src/kiri/ui/view-state.js b/src/kiri/app/view-state.js similarity index 100% rename from src/kiri/ui/view-state.js rename to src/kiri/app/view-state.js diff --git a/src/kiri/ui/visuals.js b/src/kiri/app/visuals.js similarity index 100% rename from src/kiri/ui/visuals.js rename to src/kiri/app/visuals.js diff --git a/src/kiri/ui/workspace.js b/src/kiri/app/workspace.js similarity index 100% rename from src/kiri/ui/workspace.js rename to src/kiri/app/workspace.js diff --git a/src/kiri/core/codec.js b/src/kiri/core/codec.js index fa625720..7f6de9ea 100644 --- a/src/kiri/core/codec.js +++ b/src/kiri/core/codec.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { Layers } from '../ui/layers.js'; +import { Layers } from '../app/layers.js'; import { newPoint } from '../../geo/point.js'; import { Polygon, newPolygon } from '../../geo/polygon.js'; import { Slice, newSlice, Top, newTop } from './slice.js'; diff --git a/src/kiri/core/render.js b/src/kiri/core/render.js index 456f3146..66e39b82 100644 --- a/src/kiri/core/render.js +++ b/src/kiri/core/render.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { Layers } from '../ui/layers.js'; +import { Layers } from '../app/layers.js'; import { newPoint } from '../../geo/point.js'; import { newPolygon } from '../../geo/polygon.js'; import { newSlopeFromAngle } from '../../geo/slope.js'; diff --git a/src/kiri/core/slice.js b/src/kiri/core/slice.js index 6604399b..4db028c5 100644 --- a/src/kiri/core/slice.js +++ b/src/kiri/core/slice.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { Layers } from '../ui/layers.js'; +import { Layers } from '../app/layers.js'; import { newPoint } from '../../geo/point.js'; import { polygons as POLY } from '../../geo/polygons.js'; diff --git a/src/kiri/core/widgets.js b/src/kiri/core/widgets.js index 5bc90476..5db6f325 100644 --- a/src/kiri/core/widgets.js +++ b/src/kiri/core/widgets.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { $ } from '../../moto/webui.js'; -import { api } from '../ui/api.js'; +import { api } from '../app/api.js'; import { load } from '../../load/file.js'; import { Widget, newWidget } from './widget.js'; diff --git a/src/kiri/mode/cam/anim-2d-fe.js b/src/kiri/mode/cam/anim-2d-fe.js index 4c65321c..98286cc0 100644 --- a/src/kiri/mode/cam/anim-2d-fe.js +++ b/src/kiri/mode/cam/anim-2d-fe.js @@ -2,7 +2,7 @@ // UI FRONT END ANIMATION CODE for 2D -import { api } from '../../ui/api.js'; +import { api } from '../../app/api.js'; import { space } from '../../../moto/space.js'; const { client } = api; diff --git a/src/kiri/mode/cam/anim-3d-fe.js b/src/kiri/mode/cam/anim-3d-fe.js index 99d4ffab..478ed13e 100644 --- a/src/kiri/mode/cam/anim-3d-fe.js +++ b/src/kiri/mode/cam/anim-3d-fe.js @@ -2,7 +2,7 @@ // UI FRONT END ANIMATION CODE for 3D (indexed) -import { api } from '../../ui/api.js'; +import { api } from '../../app/api.js'; import { space } from '../../../moto/space.js'; const { client } = api; diff --git a/src/kiri/mode/cam/cl-flip.js b/src/kiri/mode/cam/cl-flip.js index 457cc5a3..cf3097f1 100644 --- a/src/kiri/mode/cam/cl-flip.js +++ b/src/kiri/mode/cam/cl-flip.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../ui/api.js'; +import { api } from '../../app/api.js'; import { env, opAdd, opRender } from './init-ui.js'; import { CAM } from './driver-fe.js'; import { clearTabs, restoreTabs } from './cl-tab.js'; diff --git a/src/kiri/mode/cam/cl-helical.js b/src/kiri/mode/cam/cl-helical.js index 4ca086d6..294f38e9 100644 --- a/src/kiri/mode/cam/cl-helical.js +++ b/src/kiri/mode/cam/cl-helical.js @@ -1,4 +1,4 @@ -import { api } from '../../ui/api.js'; +import { api } from '../../app/api.js'; import { CAM } from "./driver-fe.js"; import { clearPops, env } from "./init-ui.js"; diff --git a/src/kiri/mode/cam/cl-hole.js b/src/kiri/mode/cam/cl-hole.js index 80f1c292..cf20aece 100644 --- a/src/kiri/mode/cam/cl-hole.js +++ b/src/kiri/mode/cam/cl-hole.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../ui/api.js'; +import { api } from '../../app/api.js'; import { env, clearPops } from './init-ui.js'; import { CAM } from './driver-fe.js'; diff --git a/src/kiri/mode/cam/cl-ops.js b/src/kiri/mode/cam/cl-ops.js index 5a235e95..f0550ed0 100644 --- a/src/kiri/mode/cam/cl-ops.js +++ b/src/kiri/mode/cam/cl-ops.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { $, h } from '../../../moto/webui.js'; -import { api } from '../../ui/api.js'; +import { api } from '../../app/api.js'; import { env, opRender } from './init-ui.js'; import { Tool } from './tool.js'; import { opFlip } from './cl-flip.js'; diff --git a/src/kiri/mode/cam/cl-origin.js b/src/kiri/mode/cam/cl-origin.js index 3a99a0bf..5255d084 100644 --- a/src/kiri/mode/cam/cl-origin.js +++ b/src/kiri/mode/cam/cl-origin.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../ui/api.js'; +import { api } from '../../app/api.js'; import { env, clearPops } from './init-ui.js'; import { CAM } from './driver-fe.js'; import { addbox, clearboxes } from '../../core/boxes.js'; diff --git a/src/kiri/mode/cam/cl-stock.js b/src/kiri/mode/cam/cl-stock.js index b9fc429e..acadfd79 100644 --- a/src/kiri/mode/cam/cl-stock.js +++ b/src/kiri/mode/cam/cl-stock.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../ui/api.js'; +import { api } from '../../app/api.js'; import { env, isDark } from './init-ui.js'; import { space as SPACE } from '../../../moto/space.js'; diff --git a/src/kiri/mode/cam/cl-surface.js b/src/kiri/mode/cam/cl-surface.js index 670ad823..4a56ef63 100644 --- a/src/kiri/mode/cam/cl-surface.js +++ b/src/kiri/mode/cam/cl-surface.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../ui/api.js'; +import { api } from '../../app/api.js'; import { env, clearPops } from './init-ui.js'; import { CAM } from './driver-fe.js'; diff --git a/src/kiri/mode/cam/cl-tab.js b/src/kiri/mode/cam/cl-tab.js index a406920d..eecbc623 100644 --- a/src/kiri/mode/cam/cl-tab.js +++ b/src/kiri/mode/cam/cl-tab.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../ui/api.js'; +import { api } from '../../app/api.js'; import { env, isDark } from './init-ui.js'; import { addbox, delbox, clearboxes } from '../../core/boxes.js'; import { traceDone } from './cl-trace.js'; diff --git a/src/kiri/mode/cam/cl-trace.js b/src/kiri/mode/cam/cl-trace.js index 9f89cd47..3fd4666b 100644 --- a/src/kiri/mode/cam/cl-trace.js +++ b/src/kiri/mode/cam/cl-trace.js @@ -1,10 +1,10 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../ui/api.js'; +import { api } from '../../app/api.js'; import { env, clearPops, isDark } from './init-ui.js'; import { CAM } from './driver-fe.js'; -import { Layers } from '../../ui/layers.js'; -import { Stack } from '../../ui/stack.js'; +import { Layers } from '../../app/layers.js'; +import { Stack } from '../../app/stack.js'; export let traceOn = false; export let lastTrace; diff --git a/src/kiri/mode/cam/driver-fe.js b/src/kiri/mode/cam/driver-fe.js index 3ae2cf34..c5fdf906 100644 --- a/src/kiri/mode/cam/driver-fe.js +++ b/src/kiri/mode/cam/driver-fe.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../ui/api.js'; +import { api } from '../../app/api.js'; import { codec } from '../../core/codec.js'; function surface_prep(index, ondone) { diff --git a/src/kiri/mode/cam/init-menu.js b/src/kiri/mode/cam/init-menu.js index 7efb902b..c83999b6 100644 --- a/src/kiri/mode/cam/init-menu.js +++ b/src/kiri/mode/cam/init-menu.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../ui/api.js'; +import { api } from '../../app/api.js'; import { originReset, originSelect } from './cl-origin.js'; import { updateTool } from './tools.js'; diff --git a/src/kiri/mode/cam/init-ui.js b/src/kiri/mode/cam/init-ui.js index 1b44574c..55fdd744 100644 --- a/src/kiri/mode/cam/init-ui.js +++ b/src/kiri/mode/cam/init-ui.js @@ -1,13 +1,13 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { $ } from '../../../moto/webui.js'; -import { api } from '../../ui/api.js'; +import { api } from '../../app/api.js'; import { load } from '../../../load/file.js'; import { animate as anim_2d, animate_clear as anim_2d_clear } from './anim-2d-fe.js'; import { animate2 as anim_3d, animate_clear2 as anim_3d_clear } from './anim-3d-fe.js'; import { space as SPACE } from '../../../moto/space.js'; -import { Layers } from '../../ui/layers.js'; -import { Stack } from '../../ui/stack.js'; +import { Layers } from '../../app/layers.js'; +import { Stack } from '../../app/stack.js'; import { updateStock } from './cl-stock.js'; import { createPopOps } from './cl-ops.js'; import { tabAdd, tabDone, tabClear, restoreTabs, rotateTabs, updateTabs, clearTabs, mirrorTabs } from './cl-tab.js'; diff --git a/src/kiri/mode/cam/tools.js b/src/kiri/mode/cam/tools.js index 8e6c6de5..ea0457d8 100644 --- a/src/kiri/mode/cam/tools.js +++ b/src/kiri/mode/cam/tools.js @@ -1,9 +1,9 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { $ } from '../../../moto/webui.js'; -import { api } from '../../ui/api.js'; +import { api } from '../../app/api.js'; import { consts } from '../../core/consts.js'; -import { settings as setconf } from '../../ui/config/manager.js'; +import { settings as setconf } from '../../app/config/manager.js'; import { Tool, calcTaperAngle, calcTaperBallExtent, calcTaperLength } from './tool.js'; const DEG2RAD = Math.PI / 180; diff --git a/src/kiri/mode/fdm/init-menu.js b/src/kiri/mode/fdm/init-menu.js index f052348b..593e7387 100644 --- a/src/kiri/mode/fdm/init-menu.js +++ b/src/kiri/mode/fdm/init-menu.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../ui/api.js'; +import { api } from '../../app/api.js'; let LANG = api.language.current; let { FDM } = api.const.MODES, diff --git a/src/kiri/mode/fdm/init-ui.js b/src/kiri/mode/fdm/init-ui.js index 9ccbeb7a..ce197e85 100644 --- a/src/kiri/mode/fdm/init-ui.js +++ b/src/kiri/mode/fdm/init-ui.js @@ -1,13 +1,13 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { $ } from '../../../moto/webui.js'; -import { api } from '../../ui/api.js'; -import { conf } from '../../ui/config/defaults.js'; +import { api } from '../../app/api.js'; +import { conf } from '../../app/config/defaults.js'; import { util } from '../../../geo/base.js'; -import { client } from '../../ui/client.js'; +import { client } from '../../app/client.js'; import { addbox, delbox, getlastbox } from '../../core/boxes.js'; -import { Layers } from '../../ui/layers.js'; -import { Stack } from '../../ui/stack.js'; +import { Layers } from '../../app/layers.js'; +import { Stack } from '../../app/stack.js'; import { Widget, newWidget } from '../../core/widget.js'; import { space } from '../../../moto/space.js'; diff --git a/src/kiri/mode/laser/driver.js b/src/kiri/mode/laser/driver.js index 9605d838..bbd7f60f 100644 --- a/src/kiri/mode/laser/driver.js +++ b/src/kiri/mode/laser/driver.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { base } from '../../../geo/base.js'; -import { Packer } from '../../ui/pack.js'; +import { Packer } from '../../app/pack.js'; import { newBounds } from '../../../geo/bounds.js'; import { newPoint } from '../../../geo/point.js'; import { newPrint } from '../../core/print.js'; diff --git a/src/kiri/mode/laser/init-menu.js b/src/kiri/mode/laser/init-menu.js index 16f141e5..5e5bac27 100644 --- a/src/kiri/mode/laser/init-menu.js +++ b/src/kiri/mode/laser/init-menu.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../ui/api.js'; +import { api } from '../../app/api.js'; let LANG = api.language.current; let { LASER, DRAG, WJET, WEDM } = api.const.MODES, diff --git a/src/kiri/mode/sla/init-menu.js b/src/kiri/mode/sla/init-menu.js index c275596a..d1a01f45 100644 --- a/src/kiri/mode/sla/init-menu.js +++ b/src/kiri/mode/sla/init-menu.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../ui/api.js'; +import { api } from '../../app/api.js'; let LANG = api.language.current; let { SLA } = api.const.MODES, diff --git a/src/kiri/mode/sla/init-ui.js b/src/kiri/mode/sla/init-ui.js index cc2a361e..1e5104ad 100644 --- a/src/kiri/mode/sla/init-ui.js +++ b/src/kiri/mode/sla/init-ui.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { $ } from '../../../moto/webui.js'; -import { api } from '../../ui/api.js'; +import { api } from '../../app/api.js'; export const SLA = { init, diff --git a/src/kiri/mode/wedm/init-ui.js b/src/kiri/mode/wedm/init-ui.js index 1a358f1b..871adec7 100644 --- a/src/kiri/mode/wedm/init-ui.js +++ b/src/kiri/mode/wedm/init-ui.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../ui/api.js'; +import { api } from '../../app/api.js'; const state = { api: undefined, diff --git a/src/kiri/run/engine.js b/src/kiri/run/engine.js index 67a93d8c..c57b8984 100644 --- a/src/kiri/run/engine.js +++ b/src/kiri/run/engine.js @@ -3,9 +3,9 @@ import '../../add/array.js'; import '../../add/class.js'; import '../../add/three.js'; -import { api } from '../ui/api.js'; -import { conf } from '../ui/config/defaults.js'; -import { client } from '../ui/client.js'; +import { api } from '../app/api.js'; +import { conf } from '../app/config/defaults.js'; +import { client } from '../app/client.js'; import { load } from '../../load/file.js'; import { newWidget } from '../core/widget.js'; diff --git a/src/main/kiri.js b/src/main/kiri.js index a7f97343..4727612f 100644 --- a/src/main/kiri.js +++ b/src/main/kiri.js @@ -3,13 +3,13 @@ import '../add/array.js'; import '../add/class.js'; import '../add/three.js'; -import '../kiri/ui/lang.js'; +import '../kiri/app/lang.js'; import '../kiri/core/lang-en.js'; -import { api } from '../kiri/ui/api.js'; -import { init_lang } from '../kiri/ui/init-lang.js'; -import { init_one } from '../kiri/ui/init-one.js'; -import { init_two } from '../kiri/ui/init-two.js'; +import { api } from '../kiri/app/api.js'; +import { init_lang } from '../kiri/app/init-lang.js'; +import { init_one } from '../kiri/app/init-one.js'; +import { init_two } from '../kiri/app/init-two.js'; let traceload = location.search.indexOf('traceload') > 0; let load = []; From 5bd67545dc74b8d675f84b5812c6bcbe53de1de2 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 25 Dec 2025 10:35:49 -0500 Subject: [PATCH 025/154] kiri/app renames and reorgs --- .gitignore | 5 +- src/kiri/app/api.js | 16 +- .../{settings-ops.js => config/settings.js} | 6 +- src/kiri/app/export.js | 2 +- src/kiri/app/function.js | 2 +- src/kiri/app/groups.js | 2 +- src/kiri/app/{init-one.js => init-input.js} | 12 +- src/kiri/app/init-sync.js | 292 ++++++++++++++++++ src/kiri/app/init-two.js | 212 ------------- src/kiri/app/{component.js => inputs.js} | 0 src/kiri/app/{lang.js => language.js} | 0 src/kiri/app/{local-storage.js => local.js} | 0 src/kiri/app/mode.js | 2 +- src/kiri/app/{interact.js => mouse.js} | 0 src/kiri/app/navigation.js | 89 ------ src/kiri/app/{select.js => selected.js} | 166 +++++++++- src/kiri/app/selection-tools.js | 161 ---------- src/kiri/app/{view-state.js => viewmode.js} | 2 +- src/kiri/app/{client.js => workers.js} | 0 src/kiri/app/workspace.js | 2 +- src/kiri/mode/fdm/init-ui.js | 2 +- src/kiri/run/engine.js | 2 +- src/main/kiri.js | 10 +- 23 files changed, 486 insertions(+), 499 deletions(-) rename src/kiri/app/{settings-ops.js => config/settings.js} (96%) rename src/kiri/app/{init-one.js => init-input.js} (98%) create mode 100644 src/kiri/app/init-sync.js delete mode 100644 src/kiri/app/init-two.js rename src/kiri/app/{component.js => inputs.js} (100%) rename src/kiri/app/{lang.js => language.js} (100%) rename src/kiri/app/{local-storage.js => local.js} (100%) rename src/kiri/app/{interact.js => mouse.js} (100%) delete mode 100644 src/kiri/app/navigation.js rename src/kiri/app/{select.js => selected.js} (66%) delete mode 100644 src/kiri/app/selection-tools.js rename src/kiri/app/{view-state.js => viewmode.js} (98%) rename src/kiri/app/{client.js => workers.js} (100%) diff --git a/.gitignore b/.gitignore index de5b572a..2c523bd1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ +*.pem .DS_Store -.bache +.bcache +.claude .docusaurus .env alt @@ -16,4 +18,5 @@ src/ext/jszip-esm.js src/ext/quickjs.js src/ext/three.js tmp +tmp_*/ web/boot/bundle* diff --git a/src/kiri/app/api.js b/src/kiri/app/api.js index beb33cba..9f1c122f 100644 --- a/src/kiri/app/api.js +++ b/src/kiri/app/api.js @@ -9,7 +9,7 @@ import web from '../../moto/webui.js'; import { beta, version } from '../../moto/license.js'; import { broker } from '../../moto/broker.js'; -import { client as work } from './client.js'; +import { client as workers } from './workers.js'; import { consts, COLOR as color, LISTS as lists } from '../core/consts.js'; import { device, devices } from './devices.js'; import { functions } from './function.js'; @@ -17,8 +17,8 @@ import { group as groupModule } from './groups.js'; import { help as helpModule } from './help.js'; import { image as imageModule } from './image.js'; import { Index } from '../../data/index.js'; -import { LANG } from './lang.js'; -import { local } from './local-storage.js'; +import { LANG } from './language.js'; +import { local } from './local.js'; import { local as dataLocal } from '../../data/local.js'; import { modal } from './modal.js'; import { mode as modeModule, process as processModule } from './mode.js'; @@ -26,17 +26,17 @@ import { newWidget } from '../core/widget.js'; import { noop, ajax, o2js, js2o, utils } from '../core/utils.js'; import { openFiles } from './files.js'; import { platform } from './platform.js'; -import { selection } from './select.js'; +import { selection } from './selected.js'; import { settingsUI } from './config/dialog.js'; import { showDevices } from './devices.js'; import { showTools } from '../mode/cam/tools.js'; import { space as SPACE } from '../../moto/space.js'; import { stats } from './stats.js'; import { types as load } from '../../load/file.js'; -import { UI } from './component.js'; +import { UI } from './inputs.js'; import { updateTool } from '../mode/cam/tools.js'; import { util as utilModule } from './util.js'; -import { view as viewModule } from './view-state.js'; +import { view as viewModule } from './viewmode.js'; import { visuals } from './visuals.js'; import { widgets } from '../core/widgets.js'; import { workspace } from './workspace.js'; @@ -74,7 +74,7 @@ export const api = { dec() { api.event.emit("busy", --busyVal) } }, catalog: FILES, - client: work, + client: workers, clip(text) { navigator.clipboard .writeText(text) @@ -219,7 +219,7 @@ export const api = { visuals, web, widgets, - work, + work: workers, }; // allow widget to straddle client / worker FOR NOW diff --git a/src/kiri/app/settings-ops.js b/src/kiri/app/config/settings.js similarity index 96% rename from src/kiri/app/settings-ops.js rename to src/kiri/app/config/settings.js index e32b1cfd..627b5e79 100644 --- a/src/kiri/app/settings-ops.js +++ b/src/kiri/app/config/settings.js @@ -1,8 +1,8 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { $ } from '../../moto/webui.js'; -import { api } from './api.js'; -import { settings as set_ctrl } from './config/manager.js'; +import { $ } from '../../../moto/webui.js'; +import { api } from '../api.js'; +import { settings as set_ctrl } from './manager.js'; function settings() { return api.conf.get(); diff --git a/src/kiri/app/export.js b/src/kiri/app/export.js index 7409581e..53439bff 100644 --- a/src/kiri/app/export.js +++ b/src/kiri/app/export.js @@ -2,7 +2,7 @@ import { $ } from '../../moto/webui.js'; import { api } from './api.js'; -import { client } from './client.js'; +import { client } from './workers.js'; import { local } from '../../data/local.js'; import { util } from '../../geo/base.js'; import { MODES } from '../core/consts.js'; diff --git a/src/kiri/app/function.js b/src/kiri/app/function.js index c3268eb3..3751504c 100644 --- a/src/kiri/app/function.js +++ b/src/kiri/app/function.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { api } from './api.js'; -import { client } from './client.js'; +import { client } from './workers.js'; import { codec } from '../core/codec.js'; import { space } from '../../moto/space.js'; import { COLOR, PMODES } from '../core/consts.js'; diff --git a/src/kiri/app/groups.js b/src/kiri/app/groups.js index 935b347d..a0ab659e 100644 --- a/src/kiri/app/groups.js +++ b/src/kiri/app/groups.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { selection } from './select.js'; +import { selection } from './selected.js'; import { Widget } from '../core/widget.js'; function groupMerge() { diff --git a/src/kiri/app/init-one.js b/src/kiri/app/init-input.js similarity index 98% rename from src/kiri/app/init-one.js rename to src/kiri/app/init-input.js index 77a39179..48d67502 100644 --- a/src/kiri/app/init-one.js +++ b/src/kiri/app/init-input.js @@ -11,7 +11,7 @@ import { init as initLaser } from '../mode/laser/init-ui.js'; import { init as initSLA } from '../mode/sla/init-ui.js'; import { init as initWEDM } from '../mode/wedm/init-ui.js'; import { init as initWJET } from '../mode/wjet/init-ui.js'; -import { interact } from './interact.js'; +import { interact } from './mouse.js'; import { keyboard } from './keyboard.js'; import { local as sdb } from '../../data/local.js'; import { menu as menuCAM } from '../mode/cam/init-menu.js'; @@ -20,9 +20,8 @@ import { menu as menuLaser } from '../mode/laser/init-menu.js'; import { menu as menuSLA } from '../mode/sla/init-menu.js'; import { modal } from './modal.js'; import { preferences } from './preferences.js'; -import { selectionTools } from './selection-tools.js'; import { settings as set_ctrl } from './config/manager.js'; -import { settingsOps } from './settings-ops.js'; +import { settingsOps } from './config/settings.js'; import { slider } from './slider.js'; import { space } from '../../moto/space.js'; import { VIEWS, MODES, SEED } from '../core/consts.js'; @@ -32,6 +31,7 @@ import STACKS from './stacks.js'; let { SETUP } = api.const, { CAM, SLA, FDM, LASER, DRAG, WJET, WEDM } = MODES, { catalog, platform, selection, stats } = api, + { input_binding, input_position, input_rotate } = selection, DOC = self.document, WIN = self.window, LANG = api.language.current, @@ -516,7 +516,7 @@ function init_one() { VIEWS, DOC, WIN, - rotateInputSelection: selectionTools.rotateInputSelection, + rotateInputSelection: input_rotate, settingsLoad: settingsOps.settingsLoad }); @@ -587,7 +587,7 @@ function init_one() { ]); // Setup selection transformation bindings - selectionTools.setupSelectionBindings(ui); + input_binding(ui); $('lab-axis').onclick = () => { ui.lockX.checked = @@ -684,4 +684,4 @@ function init_one() { }); }; -export { onBooleanClick, updateTool, init_one }; +export { onBooleanClick, init_one as init_input }; diff --git a/src/kiri/app/init-sync.js b/src/kiri/app/init-sync.js new file mode 100644 index 00000000..a4753ab7 --- /dev/null +++ b/src/kiri/app/init-sync.js @@ -0,0 +1,292 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { $ } from '../../moto/webui.js'; +import { api } from './api.js'; +import { beta, version } from '../../moto/license.js'; +import { fileOps } from './file-ops.js'; +import { local as sdb } from '../../data/local.js'; +import { preferences } from './preferences.js'; +import { settings as set_ctrl } from './config/manager.js'; +import { settingsOps } from './config/settings.js'; +import { space } from '../../moto/space.js'; +import { VIEWS } from '../core/consts.js'; + +const DOC = self.document; +const WIN = self.window; +const proto = location.protocol; +const { SETUP, LOCAL } = api.const; +const { platform, client, catalog, stats } = api; +const LANG = api.language.current; +const ui = api.ui; +const STARTMODE = SETUP.sm && SETUP.sm.length === 1 ? SETUP.sm[0] : null; + +function settings() { + return api.conf.get(); +} + +// SECOND STAGE INIT AFTER UI RESTORED +export async function init_sync() { + api.event.emit('init.two'); + + // load script extensions + if (SETUP.s) SETUP.s.forEach(function(lib) { + let scr = DOC.createElement('script'); + scr.setAttribute('async', true); + scr.setAttribute('defer', true); + scr.setAttribute('src',`/code/${lib}.js`); + DOC.body.appendChild(scr); + stats.add('load_'+lib); + api.event.emit('load.lib', lib); + }); + + // override stored settings + if (SETUP.v) SETUP.v.forEach(function(kv) { + kv = kv.split('='); + sdb.setItem(kv[0],kv[1]); + }); + + // import octoprint settings + if (SETUP.ophost) { + let ohost = api.const.OCTO = { + host: SETUP.ophost[0], + apik: SETUP.opkey ? SETUP.opkey[0] : '' + }; + sdb['octo-host'] = ohost.host; + sdb['octo-apik'] = ohost.apik; + console.log({octoprint:ohost}); + } + + // load workspace from url + if (SETUP.wrk) { + set_ctrl.import_url(`${proto}//${SETUP.wrk[0]}`, false); + } + + // load an object from url + if (SETUP.load) { + console.log({load:SETUP}); + api.platform.load_url(`${proto}//${SETUP.load[0]}`); + } + + // bind this to UI so main can call it on settings import + ui.sync = ui_sync; + ui_sync(); + + // clear alerts as they build up + setInterval(api.event.alerts, 1000); + + // add hide-alerts-on-alert-click + ui.alert.dialog.onclick = function() { + api.event.alerts(true); + }; + + // enable modal hiding + $('mod-x').onclick = api.modal.hide; + + if (!SETUP.s) console.log(`kiri | init main | ${version}`); + + // send init-done event + api.event.emit('init-done', stats); + + // show gdpr if it's never been seen and we're not iframed + const isLocal = LOCAL || WIN.location.host.split(':')[0] === 'localhost'; + if (!sdb.gdpr && WIN.self === WIN.top && !SETUP.debug && !isLocal) { + $('gdpr').style.display = 'flex'; + } + + // warn of degraded functionality when SharedArrayBuffers are missing + if (api.feature.work_alerts && !window.SharedArrayBuffer) { + api.alerts.show("The security context of this", 10); + api.alerts.show("Window blocks important functionality.", 10); + api.alerts.show("Try a Chromium-base Browser instead", 10); + } + + // add keyboard focus handler (must use for iframes) + WIN.addEventListener('load', function () { + WIN.focus(); + DOC.body.addEventListener('click', function() { + WIN.focus(); + },false); + }); + + // dismiss gdpr alert + $('gotit').onclick = () => { + $('gdpr').style.display = 'none'; + sdb.gdpr = Date.now(); + }; + + // Setup navigation button bindings + setup_keybd_nav(ui, WIN, LANG); + + // ui.modal.onclick = api.modal.hide; + ui.modalBox.onclick = (ev) => { ev.stopPropagation() }; + + // add app name hover info + $('app-info').innerText = version; + + // show topline separator when iframed + try { if (WIN.self !== WIN.top) $('top-sep').style.display = 'flex' } catch (e) { console.log(e) } + + // warn users they are running a beta release + if (beta && beta > 0 && sdb.kiri_beta != beta) { + api.show.alert("CAUTION"); + api.show.alert("this is a development release"); + api.show.alert("and may not function properly"); + sdb.kiri_beta = beta; + } + + // hide url params but preserve version root (when present) + let wlp = WIN.location.pathname; + let kio = wlp.indexOf('/kiri/'); + if (kio >= 0) { + history.replaceState({}, '', wlp.substring(0,kio + 6)); + } + + // lift curtain + $('curtain').style.display = 'none'; +} + +function ui_sync() { + const current = settings(); + const control = current.controller; + + if (!control.devel) { + // TODO: hide thin type 3 during development + api.const.LISTS.thin.length = 3; + } + + platform.deselect(); + catalog.addFileListener(fileOps.updateCatalog); + space.view.setZoom(control.reverseZoom, control.zoomSpeed); + space.platform.setGridZOff(undefined); + space.platform.setZOff(0.05); + space.view.setProjection(control.ortho ? 'orthographic' : 'perspective'); + + // 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.spaceRandoX.checked = control.spaceRandoX; + // ui.threaded.checked = setThreaded(control.threaded); + ui.webGPU.checked = control.webGPU; + + preferences.setThreaded(true); + preferences.lineTypeSave(); + preferences.detailSave(); + preferences.updateStats(); + + // optional set-and-lock mode (hides mode menu) + let SETMODE = SETUP.mode ? SETUP.mode[0] : null; + + // optional set-and-lock device (hides device menu) + let DEVNAME = SETUP.dev ? SETUP.dev[0] : null; + + // setup default mode and enable mode locking, if set + api.mode.set(SETMODE || STARTMODE || current.mode, SETMODE); + + // fill device list + api.devices.refresh(); + + // update ui fields from settings + api.conf.update_fields(); + + // default to ARRANGE view mode + api.view.set(VIEWS.ARRANGE); + + // add ability to override (todo: restore?) + // api.show.controls(api.feature.controls); + + // update everything dependent on the platform size + platform.update_size(); + + // load wasm if indicated + client.wasm(control.assembly === true); +} + +function setup_keybd_nav(ui, WIN, LANG) { + const { selection } = api; + + // bind interface action elements + $('export-support-a').onclick = (ev) => { ev.stopPropagation(); api.modal.show('don8') }; + $('mode-device').onclick = api.show.devices; + $('mode-profile').onclick = settingsOps.settingsLoad; + $('mode-fdm').onclick = () => api.mode.set('FDM'); + $('mode-cam').onclick = () => api.mode.set('CAM'); + $('mode-sla').onclick = () => api.mode.set('SLA'); + $('mode-laser').onclick = () => api.mode.set('LASER'); + $('mode-drag').onclick = () => api.mode.set('DRAG'); + $('mode-wjet').onclick = () => api.mode.set('WJET'); + $('mode-wedm').onclick = () => api.mode.set('WEDM'); + $('set-device').onclick = (ev) => { ev.stopPropagation(); api.show.devices() }; + $('set-profs').onclick = (ev) => { ev.stopPropagation(); api.conf.show() }; + $('set-tools').onclick = (ev) => { ev.stopPropagation(); api.show.tools() }; + $('set-prefs').onclick = (ev) => { ev.stopPropagation(); api.modal.show('prefs') }; + ui.acct.help.onclick = (ev) => { ev.stopPropagation(); api.help.show() }; + ui.acct.don8.onclick = (ev) => { ev.stopPropagation(); api.modal.show('don8') }; + ui.acct.mesh.onclick = (ev) => { ev.stopPropagation(); WIN.location = "/mesh" }; + ui.acct.export.onclick = (ev) => { ev.stopPropagation(); settingsOps.profileExport() }; + ui.acct.export.title = LANG.acct_xpo; + $('file-new').onclick = (ev) => { ev.stopPropagation(); settingsOps.workspaceNew() }; + $('file-recent').onclick = () => { api.modal.show('files') }; + $('file-import').onclick = (ev) => { api.event.import(ev); }; + ui.func.slice.onclick = (ev) => { ev.stopPropagation(); api.function.slice() }; + ui.func.preview.onclick = (ev) => { ev.stopPropagation(); api.function.print() }; + ui.func.animate.onclick = (ev) => { ev.stopPropagation(); api.function.animate() }; + ui.func.export.onclick = (ev) => { ev.stopPropagation(); api.function.export() }; + $('view-arrange').onclick = api.platform.layout; + $('view-top').onclick = space.view.top; + $('view-home').onclick = space.view.home; + $('view-front').onclick = space.view.front; + $('view-back').onclick = space.view.back; + $('view-left').onclick = space.view.left; + $('view-right').onclick = space.view.right; + $('unrotate').onclick = () => { + api.widgets.for(w => w.unrotate()); + selection.update_info(); + }; + // attach button handlers to support targets + for (let btn of ["don8pt","don8gh","don8pp"]) { + $(btn).onclick = (ev) => { + window.open(ev.target.children[0].href); + } + } + // rotation buttons + let d = (Math.PI / 180); + $('rot_x_lt').onclick = () => { selection.rotate(-d * $('rot_x').value,0,0) }; + $('rot_x_gt').onclick = () => { selection.rotate( d * $('rot_x').value,0,0) }; + $('rot_y_lt').onclick = () => { selection.rotate(0,-d * $('rot_y').value,0) }; + $('rot_y_gt').onclick = () => { selection.rotate(0, d * $('rot_y').value,0) }; + $('rot_z_lt').onclick = () => { selection.rotate(0,0, d * $('rot_z').value) }; + $('rot_z_gt').onclick = () => { selection.rotate(0,0,-d * $('rot_z').value) }; + // rendering options + $('render-edges').onclick = () => { api.view.edges({ toggle: true }); api.conf.save() }; + $('render-ghost').onclick = () => { api.view.wireframe(false, 0, api.view.is_arrange() ? 0.4 : 0.25); }; + $('render-wire').onclick = () => { api.view.wireframe(true, 0, api.space.is_dark() ? 0.25 : 0.5); }; + $('render-solid').onclick = () => { api.view.wireframe(false, 0, 1); }; + $('mesh-export-stl').onclick = () => { settingsOps.objectsExport('stl') }; + $('mesh-export-obj').onclick = () => { settingsOps.objectsExport('obj') }; + $('mesh-merge').onclick = selection.merge; + $('mesh-split').onclick = selection.isolateBodies; + $('context-duplicate').onclick = selection.duplicate; + $('context-mirror').onclick = selection.mirror; + $('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", + ev => api.space.set_focus(undefined, ev.object.point) + ); + }; + $('context-contents').onclick = () => { api.SPACE.view.fit() }; + $('view-fit').onclick = () => { api.SPACE.view.fit() }; + $('wassup').onmouseover = () => { $('suppopp').classList.remove('hide') }; +} diff --git a/src/kiri/app/init-two.js b/src/kiri/app/init-two.js deleted file mode 100644 index 77bca345..00000000 --- a/src/kiri/app/init-two.js +++ /dev/null @@ -1,212 +0,0 @@ -/** Copyright Stewart Allen -- All Rights Reserved */ - -import { $ } from '../../moto/webui.js'; -import { api } from './api.js'; -import { beta, version } from '../../moto/license.js'; -import { fileOps } from './file-ops.js'; -import { local as sdb } from '../../data/local.js'; -import { navigation } from './navigation.js'; -import { preferences } from './preferences.js'; -import { settings as set_ctrl } from './config/manager.js'; -import { space } from '../../moto/space.js'; -import { VIEWS } from '../core/consts.js'; - -const DOC = self.document; -const WIN = self.window; -const proto = location.protocol; -const { SETUP, LOCAL } = api.const; -const { platform, client, catalog, stats } = api; -const LANG = api.language.current; -const ui = api.ui; -const STARTMODE = SETUP.sm && SETUP.sm.length === 1 ? SETUP.sm[0] : null; - -function settings() { - return api.conf.get(); -} - -// SECOND STAGE INIT AFTER UI RESTORED -export async function init_two() { - api.event.emit('init.two'); - - // load script extensions - if (SETUP.s) SETUP.s.forEach(function(lib) { - let scr = DOC.createElement('script'); - scr.setAttribute('async', true); - scr.setAttribute('defer', true); - scr.setAttribute('src',`/code/${lib}.js`); - DOC.body.appendChild(scr); - stats.add('load_'+lib); - api.event.emit('load.lib', lib); - }); - - // override stored settings - if (SETUP.v) SETUP.v.forEach(function(kv) { - kv = kv.split('='); - sdb.setItem(kv[0],kv[1]); - }); - - // import octoprint settings - if (SETUP.ophost) { - let ohost = api.const.OCTO = { - host: SETUP.ophost[0], - apik: SETUP.opkey ? SETUP.opkey[0] : '' - }; - sdb['octo-host'] = ohost.host; - sdb['octo-apik'] = ohost.apik; - console.log({octoprint:ohost}); - } - - // load workspace from url - if (SETUP.wrk) { - set_ctrl.import_url(`${proto}//${SETUP.wrk[0]}`, false); - } - - // load an object from url - if (SETUP.load) { - console.log({load:SETUP}); - api.platform.load_url(`${proto}//${SETUP.load[0]}`); - } - - // bind this to UI so main can call it on settings import - ui.sync = function() { - const current = settings(); - const control = current.controller; - - if (!control.devel) { - // TODO: hide thin type 3 during development - api.const.LISTS.thin.length = 3; - } - - platform.deselect(); - catalog.addFileListener(fileOps.updateCatalog); - space.view.setZoom(control.reverseZoom, control.zoomSpeed); - space.platform.setGridZOff(undefined); - space.platform.setZOff(0.05); - space.view.setProjection(control.ortho ? 'orthographic' : 'perspective'); - - // 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.spaceRandoX.checked = control.spaceRandoX; - // ui.threaded.checked = setThreaded(control.threaded); - ui.webGPU.checked = control.webGPU; - - preferences.setThreaded(true); - preferences.lineTypeSave(); - preferences.detailSave(); - preferences.updateStats(); - - // optional set-and-lock mode (hides mode menu) - let SETMODE = SETUP.mode ? SETUP.mode[0] : null; - - // optional set-and-lock device (hides device menu) - let DEVNAME = SETUP.dev ? SETUP.dev[0] : null; - - // setup default mode and enable mode locking, if set - api.mode.set(SETMODE || STARTMODE || current.mode, SETMODE); - - // fill device list - api.devices.refresh(); - - // update ui fields from settings - api.conf.update_fields(); - - // default to ARRANGE view mode - api.view.set(VIEWS.ARRANGE); - - // add ability to override (todo: restore?) - // api.show.controls(api.feature.controls); - - // update everything dependent on the platform size - platform.update_size(); - - // load wasm if indicated - client.wasm(control.assembly === true); - }; - - ui.sync(); - - // clear alerts as they build up - setInterval(api.event.alerts, 1000); - - // add hide-alerts-on-alert-click - ui.alert.dialog.onclick = function() { - api.event.alerts(true); - }; - - // enable modal hiding - $('mod-x').onclick = api.modal.hide; - - if (!SETUP.s) console.log(`kiri | init main | ${version}`); - - // send init-done event - api.event.emit('init-done', stats); - - // show gdpr if it's never been seen and we're not iframed - const isLocal = LOCAL || WIN.location.host.split(':')[0] === 'localhost'; - if (!sdb.gdpr && WIN.self === WIN.top && !SETUP.debug && !isLocal) { - $('gdpr').style.display = 'flex'; - } - - // warn of degraded functionality when SharedArrayBuffers are missing - if (api.feature.work_alerts && !window.SharedArrayBuffer) { - api.alerts.show("The security context of this", 10); - api.alerts.show("Window blocks important functionality.", 10); - api.alerts.show("Try a Chromium-base Browser instead", 10); - } - - // add keyboard focus handler (must use for iframes) - WIN.addEventListener('load', function () { - WIN.focus(); - DOC.body.addEventListener('click', function() { - WIN.focus(); - },false); - }); - - // dismiss gdpr alert - $('gotit').onclick = () => { - $('gdpr').style.display = 'none'; - sdb.gdpr = Date.now(); - }; - - // Setup navigation button bindings - navigation.setupNavigation(ui, WIN, LANG); - - // ui.modal.onclick = api.modal.hide; - ui.modalBox.onclick = (ev) => { ev.stopPropagation() }; - - // add app name hover info - $('app-info').innerText = version; - - // show topline separator when iframed - try { if (WIN.self !== WIN.top) $('top-sep').style.display = 'flex' } catch (e) { console.log(e) } - - // warn users they are running a beta release - if (beta && beta > 0 && sdb.kiri_beta != beta) { - api.show.alert("CAUTION"); - api.show.alert("this is a development release"); - api.show.alert("and may not function properly"); - sdb.kiri_beta = beta; - } - - // hide url params but preserve version root (when present) - let wlp = WIN.location.pathname; - let kio = wlp.indexOf('/kiri/'); - if (kio >= 0) { - history.replaceState({}, '', wlp.substring(0,kio + 6)); - } - - // lift curtain - $('curtain').style.display = 'none'; -} diff --git a/src/kiri/app/component.js b/src/kiri/app/inputs.js similarity index 100% rename from src/kiri/app/component.js rename to src/kiri/app/inputs.js diff --git a/src/kiri/app/lang.js b/src/kiri/app/language.js similarity index 100% rename from src/kiri/app/lang.js rename to src/kiri/app/language.js diff --git a/src/kiri/app/local-storage.js b/src/kiri/app/local.js similarity index 100% rename from src/kiri/app/local-storage.js rename to src/kiri/app/local.js diff --git a/src/kiri/app/mode.js b/src/kiri/app/mode.js index 5fdfc174..1938cb2d 100644 --- a/src/kiri/app/mode.js +++ b/src/kiri/app/mode.js @@ -5,7 +5,7 @@ import { api } from './api.js'; import { consts } from '../core/consts.js'; import { modal } from './modal.js'; import { platform } from './platform.js'; -import { selection } from './select.js'; +import { selection } from './selected.js'; import { settings } from './config/manager.js'; const { MODES, VIEWS } = consts; diff --git a/src/kiri/app/interact.js b/src/kiri/app/mouse.js similarity index 100% rename from src/kiri/app/interact.js rename to src/kiri/app/mouse.js diff --git a/src/kiri/app/navigation.js b/src/kiri/app/navigation.js deleted file mode 100644 index b25b56ea..00000000 --- a/src/kiri/app/navigation.js +++ /dev/null @@ -1,89 +0,0 @@ -/** Copyright Stewart Allen -- All Rights Reserved */ - -import { $ } from '../../moto/webui.js'; -import { api } from './api.js'; -import { space } from '../../moto/space.js'; -import { settingsOps } from './settings-ops.js'; - -const { selection } = api; - -function setupNavigation(ui, WIN, LANG) { - // bind interface action elements - $('export-support-a').onclick = (ev) => { ev.stopPropagation(); api.modal.show('don8') }; - $('mode-device').onclick = api.show.devices; - $('mode-profile').onclick = settingsOps.settingsLoad; - $('mode-fdm').onclick = () => api.mode.set('FDM'); - $('mode-cam').onclick = () => api.mode.set('CAM'); - $('mode-sla').onclick = () => api.mode.set('SLA'); - $('mode-laser').onclick = () => api.mode.set('LASER'); - $('mode-drag').onclick = () => api.mode.set('DRAG'); - $('mode-wjet').onclick = () => api.mode.set('WJET'); - $('mode-wedm').onclick = () => api.mode.set('WEDM'); - $('set-device').onclick = (ev) => { ev.stopPropagation(); api.show.devices() }; - $('set-profs').onclick = (ev) => { ev.stopPropagation(); api.conf.show() }; - $('set-tools').onclick = (ev) => { ev.stopPropagation(); api.show.tools() }; - $('set-prefs').onclick = (ev) => { ev.stopPropagation(); api.modal.show('prefs') }; - ui.acct.help.onclick = (ev) => { ev.stopPropagation(); api.help.show() }; - ui.acct.don8.onclick = (ev) => { ev.stopPropagation(); api.modal.show('don8') }; - ui.acct.mesh.onclick = (ev) => { ev.stopPropagation(); WIN.location = "/mesh" }; - ui.acct.export.onclick = (ev) => { ev.stopPropagation(); settingsOps.profileExport() }; - ui.acct.export.title = LANG.acct_xpo; - $('file-new').onclick = (ev) => { ev.stopPropagation(); settingsOps.workspaceNew() }; - $('file-recent').onclick = () => { api.modal.show('files') }; - $('file-import').onclick = (ev) => { api.event.import(ev); }; - ui.func.slice.onclick = (ev) => { ev.stopPropagation(); api.function.slice() }; - ui.func.preview.onclick = (ev) => { ev.stopPropagation(); api.function.print() }; - ui.func.animate.onclick = (ev) => { ev.stopPropagation(); api.function.animate() }; - ui.func.export.onclick = (ev) => { ev.stopPropagation(); api.function.export() }; - $('view-arrange').onclick = api.platform.layout; - $('view-top').onclick = space.view.top; - $('view-home').onclick = space.view.home; - $('view-front').onclick = space.view.front; - $('view-back').onclick = space.view.back; - $('view-left').onclick = space.view.left; - $('view-right').onclick = space.view.right; - $('unrotate').onclick = () => { - api.widgets.for(w => w.unrotate()); - selection.update_info(); - }; - // attach button handlers to support targets - for (let btn of ["don8pt","don8gh","don8pp"]) { - $(btn).onclick = (ev) => { - window.open(ev.target.children[0].href); - } - } - // rotation buttons - let d = (Math.PI / 180); - $('rot_x_lt').onclick = () => { selection.rotate(-d * $('rot_x').value,0,0) }; - $('rot_x_gt').onclick = () => { selection.rotate( d * $('rot_x').value,0,0) }; - $('rot_y_lt').onclick = () => { selection.rotate(0,-d * $('rot_y').value,0) }; - $('rot_y_gt').onclick = () => { selection.rotate(0, d * $('rot_y').value,0) }; - $('rot_z_lt').onclick = () => { selection.rotate(0,0, d * $('rot_z').value) }; - $('rot_z_gt').onclick = () => { selection.rotate(0,0,-d * $('rot_z').value) }; - // rendering options - $('render-edges').onclick = () => { api.view.edges({ toggle: true }); api.conf.save() }; - $('render-ghost').onclick = () => { api.view.wireframe(false, 0, api.view.is_arrange() ? 0.4 : 0.25); }; - $('render-wire').onclick = () => { api.view.wireframe(true, 0, api.space.is_dark() ? 0.25 : 0.5); }; - $('render-solid').onclick = () => { api.view.wireframe(false, 0, 1); }; - $('mesh-export-stl').onclick = () => { settingsOps.objectsExport('stl') }; - $('mesh-export-obj').onclick = () => { settingsOps.objectsExport('obj') }; - $('mesh-merge').onclick = selection.merge; - $('mesh-split').onclick = selection.isolateBodies; - $('context-duplicate').onclick = selection.duplicate; - $('context-mirror').onclick = selection.mirror; - $('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", - ev => api.space.set_focus(undefined, ev.object.point) - ); - }; - $('context-contents').onclick = () => { api.SPACE.view.fit() }; - $('view-fit').onclick = () => { api.SPACE.view.fit() }; - $('wassup').onmouseover = () => { $('suppopp').classList.remove('hide') }; -} - -export const navigation = { - setupNavigation -}; diff --git a/src/kiri/app/select.js b/src/kiri/app/selected.js similarity index 66% rename from src/kiri/app/select.js rename to src/kiri/app/selected.js index 2cc97abf..87a25422 100644 --- a/src/kiri/app/select.js +++ b/src/kiri/app/selected.js @@ -9,6 +9,10 @@ import { encode as stlEncode } from '../../load/stl.js'; const selectedMeshes = []; +function updateTool(ev) { + api.tool.update(ev); +} + function for_groups(fn) { let groups = widgets(true).map(w => w.group).uniq(); for (let group of groups) { @@ -47,6 +51,20 @@ function move(x, y, z, abs) { api.space.auto_save(); } +function get_bounds() { + // Helper to compute bounds of selected meshes + const THREE = self.THREE; + let bounds = new THREE.Box3(); + selection.for_meshes(mesh => { + let box = mesh.getBoundingBox().clone(); + let pos = mesh.widget.mesh.position; + box.min.add(pos); + box.max.add(pos); + bounds = bounds.union(box); + }); + return bounds; +} + function update_info() { const ui = api.ui; let bounds = new THREE.Box3(), track; @@ -299,19 +317,155 @@ function setDisabled(bool) { api.platform.update_selected(); } +function settings() { + return api.conf.get(); +} + +function input_rotate() { + if (selection.meshes().length === 0) { + api.show.alert("select object to rotate"); + return; + } + api.uc.prompt("Enter X,Y,Z degrees of rotation","").then(coord => { + coord = (coord || '').split(','); + let prod = Math.PI / 180, + x = parseFloat(coord[0] || 0.0) * prod, + y = parseFloat(coord[1] || 0.0) * prod, + z = parseFloat(coord[2] || 0.0) * prod; + selection.rotate(x, y, z); + }); +} + +function input_position() { + if (selection.meshes().length === 0) { + api.show.alert("select object to position"); + return; + } + let current = settings(), + { device, process} = current, + center = process.ctOriginCenter || process.camOriginCenter || device.bedRound || device.originCenter, + bounds = selection.get_bounds(); + + api.uc.prompt("Enter X,Y coordinates for selection","").then(coord => { + coord = (coord || '').split(','); + let x = parseFloat(coord[0] || 0.0), + y = parseFloat(coord[1] || 0.0), + z = parseFloat(coord[2] || 0.0); + + if (!center) { + x = x - device.bedWidth/2 + (bounds.max.x - bounds.min.x)/2; + y = y - device.bedDepth/2 + (bounds.max.y - bounds.min.y)/2 + } + + selection.move(x, y, z, true); + }); +} + +function input_resize(e, ui) { + let dv = parseFloat(e.target.value || 1), + pv = parseFloat(e.target.was || 1), + ra = dv / pv, + xv = parseFloat(ui.sizeX.was ?? ui.scaleX.value) || 1, + yv = parseFloat(ui.sizeY.was ?? ui.scaleY.value) || 1, + zv = parseFloat(ui.sizeZ.was ?? ui.scaleZ.value) || 1, + ta = e.target, + xc = ui.lockX.checked, + yc = ui.lockY.checked, + zc = ui.lockZ.checked, + xt = ta === ui.sizeX, + yt = ta === ui.sizeY, + zt = ta === ui.sizeZ, + tl = (xt && xc) || (yt && yc) || (zt && zc), + xr = ((tl && xc) || (!tl && xt) ? ra : 1), + yr = ((tl && yc) || (!tl && yt) ? ra : 1), + zr = ((tl && zc) || (!tl && zt) ? ra : 1); + // prevent null scale + if (xv * xr < 0.1 || yv * yr < 0.1 || zv * zr < 0.1) { + api.alerts.show('invalid scale value'); + return; + } + selection.scale(xr,yr,zr); + ui.sizeX.was = ui.sizeX.value = xv * xr; + ui.sizeY.was = ui.sizeY.value = yv * yr; + ui.sizeZ.was = ui.sizeZ.value = zv * zr; +} + +function input_scale(e, ui) { + let dv = parseFloat(e.target.value || 1), + pv = parseFloat(e.target.was || 1), + ra = dv / pv, + xv = parseFloat(ui.scaleX.was ?? ui.scaleX.value) || 1, + yv = parseFloat(ui.scaleY.was ?? ui.scaleY.value) || 1, + zv = parseFloat(ui.scaleZ.was ?? ui.scaleY.value) || 1, + ta = e.target, + xc = ui.lockX.checked, + yc = ui.lockY.checked, + zc = ui.lockZ.checked, + xt = ta === ui.scaleX, + yt = ta === ui.scaleY, + zt = ta === ui.scaleZ, + tl = (xt && xc) || (yt && yc) || (zt && zc), + xr = ((tl && xc) || (!tl && xt) ? ra : 1), + yr = ((tl && yc) || (!tl && yt) ? ra : 1), + zr = ((tl && zc) || (!tl && zt) ? ra : 1); + // prevent null scale + if (xv * xr < 0.1 || yv * yr < 0.1 || zv * zr < 0.1) { + api.alerts.show('invalid scale value'); + return; + } + selection.scale(xr,yr,zr); + ui.scaleX.was = ui.scaleX.value = xv * xr; + ui.scaleY.was = ui.scaleY.value = yv * yr; + ui.scaleZ.was = ui.scaleZ.value = zv * zr; +} + +function parse_as_float(e) { + e.target.value = parseFloat(e.target.value) || 0; +} + +function input_binding(ui) { + // on enter but not on blur + space.event.onEnterKey([ + ui.scaleX, (e) => input_scale(e, ui), + ui.scaleY, (e) => input_scale(e, ui), + ui.scaleZ, (e) => input_scale(e, ui), + ui.sizeX, (e) => input_resize(e, ui), + ui.sizeY, (e) => input_resize(e, ui), + ui.sizeZ, (e) => input_resize(e, ui), + ]); + // on enter and blur + space.event.onEnterKey([ + ui.toolName, updateTool, + ui.toolNum, updateTool, + ui.toolFluteDiam, updateTool, + ui.toolFluteLen, updateTool, + ui.toolShaftDiam, updateTool, + ui.toolShaftLen, updateTool, + ui.toolTaperTip, updateTool, + ui.toolTaperAngle, updateTool, + $('rot_x'), parse_as_float, + $('rot_y'), parse_as_float, + $('rot_z'), parse_as_float + ], true); +} + // extend API (api.selection) export const selection = { - move, - merge, - isolateBodies, - scale, - rotate, - mirror, duplicate, for_groups, for_meshes, for_status, for_widgets, + get_bounds, + input_binding, + input_position, + input_rotate, + isolateBodies, + merge, + mirror, + move, + rotate, + scale, update_bounds, update_info, widgets, diff --git a/src/kiri/app/selection-tools.js b/src/kiri/app/selection-tools.js deleted file mode 100644 index bcb21e1d..00000000 --- a/src/kiri/app/selection-tools.js +++ /dev/null @@ -1,161 +0,0 @@ -/** Copyright Stewart Allen -- All Rights Reserved */ - -import { $ } from '../../moto/webui.js'; -import { api } from './api.js'; -import { space } from '../../moto/space.js'; -import { updateTool } from '../mode/cam/tools.js'; - -const { selection } = api; - -function settings() { - return api.conf.get(); -} - -function boundsSelection() { - // Helper to compute bounds of selected meshes - const THREE = self.THREE; - let bounds = new THREE.Box3(); - selection.for_meshes(mesh => { - let box = mesh.getBoundingBox().clone(); - let pos = mesh.widget.mesh.position; - box.min.add(pos); - box.max.add(pos); - bounds = bounds.union(box); - }); - return bounds; -} - -function rotateInputSelection() { - if (selection.meshes().length === 0) { - api.show.alert("select object to rotate"); - return; - } - api.uc.prompt("Enter X,Y,Z degrees of rotation","").then(coord => { - coord = (coord || '').split(','); - let prod = Math.PI / 180, - x = parseFloat(coord[0] || 0.0) * prod, - y = parseFloat(coord[1] || 0.0) * prod, - z = parseFloat(coord[2] || 0.0) * prod; - selection.rotate(x, y, z); - }); -} - -function positionSelection() { - if (selection.meshes().length === 0) { - api.show.alert("select object to position"); - return; - } - let current = settings(), - { device, process} = current, - center = process.ctOriginCenter || process.camOriginCenter || device.bedRound || device.originCenter, - bounds = boundsSelection(); - - api.uc.prompt("Enter X,Y coordinates for selection","").then(coord => { - coord = (coord || '').split(','); - let x = parseFloat(coord[0] || 0.0), - y = parseFloat(coord[1] || 0.0), - z = parseFloat(coord[2] || 0.0); - - if (!center) { - x = x - device.bedWidth/2 + (bounds.max.x - bounds.min.x)/2; - y = y - device.bedDepth/2 + (bounds.max.y - bounds.min.y)/2 - } - - selection.move(x, y, z, true); - }); -} - -function selectionSize(e, ui) { - let dv = parseFloat(e.target.value || 1), - pv = parseFloat(e.target.was || 1), - ra = dv / pv, - xv = parseFloat(ui.sizeX.was ?? ui.scaleX.value) || 1, - yv = parseFloat(ui.sizeY.was ?? ui.scaleY.value) || 1, - zv = parseFloat(ui.sizeZ.was ?? ui.scaleZ.value) || 1, - ta = e.target, - xc = ui.lockX.checked, - yc = ui.lockY.checked, - zc = ui.lockZ.checked, - xt = ta === ui.sizeX, - yt = ta === ui.sizeY, - zt = ta === ui.sizeZ, - tl = (xt && xc) || (yt && yc) || (zt && zc), - xr = ((tl && xc) || (!tl && xt) ? ra : 1), - yr = ((tl && yc) || (!tl && yt) ? ra : 1), - zr = ((tl && zc) || (!tl && zt) ? ra : 1); - // prevent null scale - if (xv * xr < 0.1 || yv * yr < 0.1 || zv * zr < 0.1) { - api.alerts.show('invalid scale value'); - return; - } - selection.scale(xr,yr,zr); - ui.sizeX.was = ui.sizeX.value = xv * xr; - ui.sizeY.was = ui.sizeY.value = yv * yr; - ui.sizeZ.was = ui.sizeZ.value = zv * zr; -} - -function selectionScale(e, ui) { - let dv = parseFloat(e.target.value || 1), - pv = parseFloat(e.target.was || 1), - ra = dv / pv, - xv = parseFloat(ui.scaleX.was ?? ui.scaleX.value) || 1, - yv = parseFloat(ui.scaleY.was ?? ui.scaleY.value) || 1, - zv = parseFloat(ui.scaleZ.was ?? ui.scaleY.value) || 1, - ta = e.target, - xc = ui.lockX.checked, - yc = ui.lockY.checked, - zc = ui.lockZ.checked, - xt = ta === ui.scaleX, - yt = ta === ui.scaleY, - zt = ta === ui.scaleZ, - tl = (xt && xc) || (yt && yc) || (zt && zc), - xr = ((tl && xc) || (!tl && xt) ? ra : 1), - yr = ((tl && yc) || (!tl && yt) ? ra : 1), - zr = ((tl && zc) || (!tl && zt) ? ra : 1); - // prevent null scale - if (xv * xr < 0.1 || yv * yr < 0.1 || zv * zr < 0.1) { - api.alerts.show('invalid scale value'); - return; - } - selection.scale(xr,yr,zr); - ui.scaleX.was = ui.scaleX.value = xv * xr; - ui.scaleY.was = ui.scaleY.value = yv * yr; - ui.scaleZ.was = ui.scaleZ.value = zv * zr; -} - -function selectionRotate(e) { - let val = parseFloat(e.target.value) || 0; - e.target.value = val; -} - -function setupSelectionBindings(ui) { - // on enter but not on blur - space.event.onEnterKey([ - ui.scaleX, (e) => selectionScale(e, ui), - ui.scaleY, (e) => selectionScale(e, ui), - ui.scaleZ, (e) => selectionScale(e, ui), - ui.sizeX, (e) => selectionSize(e, ui), - ui.sizeY, (e) => selectionSize(e, ui), - ui.sizeZ, (e) => selectionSize(e, ui), - ]); - // on enter and blur - space.event.onEnterKey([ - ui.toolName, updateTool, - ui.toolNum, updateTool, - ui.toolFluteDiam, updateTool, - ui.toolFluteLen, updateTool, - ui.toolShaftDiam, updateTool, - ui.toolShaftLen, updateTool, - ui.toolTaperTip, updateTool, - ui.toolTaperAngle, updateTool, - $('rot_x'), selectionRotate, - $('rot_y'), selectionRotate, - $('rot_z'), selectionRotate - ], true); -} - -export const selectionTools = { - rotateInputSelection, - positionSelection, - setupSelectionBindings -}; diff --git a/src/kiri/app/view-state.js b/src/kiri/app/viewmode.js similarity index 98% rename from src/kiri/app/view-state.js rename to src/kiri/app/viewmode.js index 7a1024dc..dfdcde79 100644 --- a/src/kiri/app/view-state.js +++ b/src/kiri/app/viewmode.js @@ -4,7 +4,7 @@ import { $ } from '../../moto/webui.js'; import { api } from './api.js'; import { consts } from '../core/consts.js'; import { platform } from './platform.js'; -import { selection } from './select.js'; +import { selection } from './selected.js'; import { settings } from './config/manager.js'; import STACKS from './stacks.js'; diff --git a/src/kiri/app/client.js b/src/kiri/app/workers.js similarity index 100% rename from src/kiri/app/client.js rename to src/kiri/app/workers.js diff --git a/src/kiri/app/workspace.js b/src/kiri/app/workspace.js index 3ac3e44c..2f391489 100644 --- a/src/kiri/app/workspace.js +++ b/src/kiri/app/workspace.js @@ -5,7 +5,7 @@ import { api } from './api.js'; import { local as SDB } from '../../data/local.js'; import { openFiles } from './files.js'; import { platform } from './platform.js'; -import { selection } from './select.js'; +import { selection } from './selected.js'; import { settings } from './config/manager.js'; import { space as SPACE } from '../../moto/space.js'; import { utils } from '../core/utils.js'; diff --git a/src/kiri/mode/fdm/init-ui.js b/src/kiri/mode/fdm/init-ui.js index ce197e85..e36d9769 100644 --- a/src/kiri/mode/fdm/init-ui.js +++ b/src/kiri/mode/fdm/init-ui.js @@ -4,7 +4,7 @@ import { $ } from '../../../moto/webui.js'; import { api } from '../../app/api.js'; import { conf } from '../../app/config/defaults.js'; import { util } from '../../../geo/base.js'; -import { client } from '../../app/client.js'; +import { client } from '../../app/workers.js'; import { addbox, delbox, getlastbox } from '../../core/boxes.js'; import { Layers } from '../../app/layers.js'; import { Stack } from '../../app/stack.js'; diff --git a/src/kiri/run/engine.js b/src/kiri/run/engine.js index c57b8984..67543b3a 100644 --- a/src/kiri/run/engine.js +++ b/src/kiri/run/engine.js @@ -5,7 +5,7 @@ import '../../add/class.js'; import '../../add/three.js'; import { api } from '../app/api.js'; import { conf } from '../app/config/defaults.js'; -import { client } from '../app/client.js'; +import { client } from '../app/workers.js'; import { load } from '../../load/file.js'; import { newWidget } from '../core/widget.js'; diff --git a/src/main/kiri.js b/src/main/kiri.js index 4727612f..e8c7723b 100644 --- a/src/main/kiri.js +++ b/src/main/kiri.js @@ -3,13 +3,13 @@ import '../add/array.js'; import '../add/class.js'; import '../add/three.js'; -import '../kiri/app/lang.js'; +import '../kiri/app/language.js'; import '../kiri/core/lang-en.js'; import { api } from '../kiri/app/api.js'; import { init_lang } from '../kiri/app/init-lang.js'; -import { init_one } from '../kiri/app/init-one.js'; -import { init_two } from '../kiri/app/init-two.js'; +import { init_input } from '../kiri/app/init-input.js'; +import { init_sync } from '../kiri/app/init-sync.js'; let traceload = location.search.indexOf('traceload') > 0; let load = []; @@ -34,8 +34,8 @@ async function checkReady() { { api.client.start(); await init_lang(); - await init_one(); - await init_two(); + await init_input(); + await init_sync(); } for (let fn of load) { safeExec(fn); From 42a933e2de65c88b10ee6d1aeb6872d8bee8ff31 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 25 Dec 2025 11:05:31 -0500 Subject: [PATCH 026/154] rename platform internals, organize ui bindings (a little) --- src/kiri/app/init-sync.js | 30 ++++---- src/kiri/app/platform.js | 154 +++++++++++++++++++------------------- 2 files changed, 92 insertions(+), 92 deletions(-) diff --git a/src/kiri/app/init-sync.js b/src/kiri/app/init-sync.js index a4753ab7..ec0ba350 100644 --- a/src/kiri/app/init-sync.js +++ b/src/kiri/app/init-sync.js @@ -79,9 +79,6 @@ export async function init_sync() { api.event.alerts(true); }; - // enable modal hiding - $('mod-x').onclick = api.modal.hide; - if (!SETUP.s) console.log(`kiri | init main | ${version}`); // send init-done event @@ -108,21 +105,9 @@ export async function init_sync() { },false); }); - // dismiss gdpr alert - $('gotit').onclick = () => { - $('gdpr').style.display = 'none'; - sdb.gdpr = Date.now(); - }; - // Setup navigation button bindings setup_keybd_nav(ui, WIN, LANG); - // ui.modal.onclick = api.modal.hide; - ui.modalBox.onclick = (ev) => { ev.stopPropagation() }; - - // add app name hover info - $('app-info').innerText = version; - // show topline separator when iframed try { if (WIN.self !== WIN.top) $('top-sep').style.display = 'flex' } catch (e) { console.log(e) } @@ -289,4 +274,19 @@ function setup_keybd_nav(ui, WIN, LANG) { $('context-contents').onclick = () => { api.SPACE.view.fit() }; $('view-fit').onclick = () => { api.SPACE.view.fit() }; $('wassup').onmouseover = () => { $('suppopp').classList.remove('hide') }; + + // prevent modal input from propagating to parents + ui.modalBox.onclick = (ev) => { ev.stopPropagation() }; + + // enable modal hiding + $('mod-x').onclick = api.modal.hide; + + // dismiss gdpr alert + $('gotit').onclick = () => { + $('gdpr').style.display = 'none'; + sdb.gdpr = Date.now(); + }; + + // add app name hover info + $('app-info').innerText = version; } diff --git a/src/kiri/app/platform.js b/src/kiri/app/platform.js index 46864d0e..e4db4401 100644 --- a/src/kiri/app/platform.js +++ b/src/kiri/app/platform.js @@ -1,16 +1,15 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { $, h } from '../../moto/webui.js'; -import { api } from './api.js'; import { ajax, js2o } from '../core/utils.js'; +import { api } from './api.js'; import { base } from '../../geo/base.js'; -import { load } from '../../load/file.js'; -import { space } from '../../moto/space.js'; -import { Widget, newWidget } from '../core/widget.js'; -import { Packer } from './pack.js'; - import { COLOR, MODES } from '../core/consts.js'; +import { load as file_load } from '../../load/file.js'; +import { Packer } from './pack.js'; +import { space } from '../../moto/space.js'; import { THREE } from '../../ext/three.js'; +import { Widget, newWidget } from '../core/widget.js'; const V0 = new THREE.Vector3(0,0,0); @@ -26,7 +25,7 @@ function get_mode() { return api.mode.get_id(); } -function platformUpdateOrigin(update_bounds = true) { +function update_origin(update_bounds = true) { if (update_bounds) { platform.update_bounds(); } @@ -106,7 +105,7 @@ function platformUpdateOrigin(update_bounds = true) { } } -function platformUpdateSize(updateDark = true) { +function update_size(updateDark = true) { const { process, device, controller } = current(); const { showRulers, units } = controller; @@ -154,7 +153,7 @@ function platformUpdateMidZ() { space.platform.setMaxZ(topZ); } -function platformUpdateTopZ() { +function update_top_z() { const { process, stock } = current(); const MODE = get_mode(); api.widgets.each(widget => { @@ -209,12 +208,12 @@ function platformUpdateStock() { } } -function platformSetBounds(bounds) { +function set_bounds(bounds) { setbounds = bounds; - return platformUpdateBounds(); + return update_bounds(); } -function platformUpdateBounds() { +function update_bounds() { const bounds = setbounds || new THREE.Box3(); if (!setbounds) api.widgets.each(widget => { @@ -231,17 +230,17 @@ function platformUpdateBounds() { } current().bounds = bounds; platformUpdateStock(); - platformUpdateTopZ(); + update_top_z(); platformUpdateMidZ(); - platformUpdateOrigin(false); + update_origin(false); return bounds; } -function platformSelectedCount() { +function selected_count() { return api.view.is_arrange() ? api.selection.count() : 0; } -function platformUpdateSelected() { +function update_selected() { const settings = current(); const { device } = settings; @@ -267,7 +266,7 @@ function platformUpdateSelected() { } } -function platformSelect(widget, shift, recurse = true) { +function select(widget, shift, recurse = true) { const { event, selection, view } = api; if (!view.is_arrange()) { @@ -277,7 +276,7 @@ function platformSelect(widget, shift, recurse = true) { // apply select to entire group if (recurse && widget && widget.group.length > 1) { for (let w of widget.group) { - platformSelect(w, true, false); + select(w, true, false); } return; } @@ -308,7 +307,7 @@ function platformSelect(widget, shift, recurse = true) { space.update(); } -function platformDeselect(widget, recurse = true) { +function deselect(widget, recurse = true) { const { selection, view } = api; if (!view.is_arrange()) { @@ -320,7 +319,7 @@ function platformDeselect(widget, recurse = true) { // apply deselect to entire group if (recurse && widget && widget.group.length > 1) { for (let w of widget.group) { - platformDeselect(w, false); + deselect(w, false); } return; } @@ -343,7 +342,7 @@ function platformDeselect(widget, recurse = true) { space.update(); } -function platformLoad(url, onload) { +function load(url, onload) { if (url.toLowerCase().indexOf(".stl") > 0) { platform.load_stl(url, onload); } else { @@ -357,8 +356,8 @@ function platformLoad(url, onload) { } } -function platformLoadSTL(url, onload, formdata, credentials, headers) { - new load.STL().load(url, (vertices, filename) => { +function load_stl(url, onload, formdata, credentials, headers) { + new file_load.STL().load(url, (vertices, filename) => { if (vertices) { let widget = newWidget().loadVertices(vertices); widget.meta.file = filename; @@ -370,9 +369,9 @@ function platformLoadSTL(url, onload, formdata, credentials, headers) { }, formdata, 1 / api.view.unit_scale(), credentials, headers); } -function platformLoadURL(url, options = {}) { +function load_url(url, options = {}) { platform.group(); - load.URL.load(url, options).then(objects => { + file_load.URL.load(url, options).then(objects => { let widgets = []; for (let object of objects) { let widget = newWidget(undefined, options.group).loadVertices(object.mesh); @@ -391,12 +390,12 @@ function platformLoadURL(url, options = {}) { }); } -function platformGroup() { +function group() { grouping = true; } // called after all new widgets are loaded to update group positions -function platformGroupDone(skipLayout) { +function group_done(skipLayout) { grouping = false; Widget.Groups.loadDone(); if (api.feature.drop_layout && !skipLayout) { @@ -407,7 +406,7 @@ function platformGroupDone(skipLayout) { let deferred = []; let deferTimeout; -function platformAdd(widget, shift, nolayout, defer) { +function add(widget, shift, nolayout, defer) { api.widgets.add(widget); space.world.add(widget.mesh); widget.anno = widget.anno || {}; @@ -420,13 +419,13 @@ function platformAdd(widget, shift, nolayout, defer) { platform.select(widget, shift); platform.update_bounds(); api.space.auto_save(); - platformChanged(); + changed(); api.event.emit('widget.add', widget); if (nolayout) { return; } if (!grouping) { - platformGroupDone(nolayout); + group_done(nolayout); if (!current().controller.autoLayout) { positionNewWidget(widget); } @@ -445,12 +444,12 @@ function platformAddDeferred() { } } if (!grouping) { - platformGroupDone(skiplayout); + group_done(skiplayout); } api.event.emit('widget.add', deferred.map(r => r.widget)); platform.update_bounds(); api.space.auto_save(); - platformChanged(); + changed(); deferred = []; } @@ -511,7 +510,7 @@ function platformDelete(widget, defer) { for (let i = 0; i < mc.length; i++) { platform.delete(mc[i].widget || mc[i], true); } - platformDeletePost(); + delete_post(); api.event.emit('widget.delete', widget); return; } @@ -521,12 +520,12 @@ function platformDelete(widget, defer) { Widget.Groups.remove(widget); space.world.remove(widget.mesh); if (!defer) { - platformDeletePost(); + delete_post(); api.event.emit('widget.delete', widget); } } -function platformDeletePost() { +function delete_post() { api.view.update_slider_max(); platform.update_bounds(); if (get_mode() !== MODES.FDM) { @@ -538,11 +537,11 @@ function platformDeletePost() { platform.layout(); } api.space.auto_save(); - platformChanged(); + changed(); } // render list of current widgets -function platformChanged() { +function changed() { h.bind($('ws-widgets'), api.widgets.all().map(w => { let color; let file = w.meta.file || 'no name'; @@ -619,7 +618,7 @@ function platformChanged() { // w.setColor(color, null, false); // }, onclick() { - platformSelect(w, true, false); + select(w, true, false); color = w.getColor(); } }) @@ -628,13 +627,13 @@ function platformChanged() { })); } -function platformSelectAll() { +function select_all() { api.widgets.each(widget => { platform.select(widget, true, false) }); } -function platformLayout() { +function layout() { const MODE = get_mode(); const settings = current(); const { process, device, controller } = settings; @@ -659,7 +658,7 @@ function platformLayout() { gap = gap || ((process.sliceSupportExtra || 0) * 2) + 1; // auto resize device to support a larger object if (isBelt) { - fitDeviceToWidgets(); + fit(); } break; } @@ -686,7 +685,7 @@ function platformLayout() { if (isBelt) { gap += process.firstLayerBeltLead || 0; const WIDGETS = api.widgets.all(); - let bounds = platformUpdateBounds(), + let bounds = update_bounds(), movey = -(device.bedDepth / 2); for (let widget of WIDGETS) { let ylen = widget.track.box.h; @@ -737,15 +736,15 @@ function platformLayout() { api.event.emit('platform.layout'); } -function platformLoadWidget(group, vertices, filename) { +function load_verts(group, vertices, filename) { const widget = newWidget(undefined, group).loadVertices(vertices.toFloat32(), true); widget.meta.file = filename; if (filename) widget.saveToCatalog(filename); - platformAdd(widget); + add(widget); return widget; } -function platformLoadFiles(files, group) { +function load_files(files, group) { platform.group(); let loading = files.length; for (let file of files) { @@ -773,7 +772,7 @@ function platformLoadFiles(files, group) { } } if (israw) { - platformLoadWidget(group, JSON.parse(data)); + load_verts(group, JSON.parse(data)); load_dec(); } else if (api.feature.on_load && (isstl || isobj || is3mf)) { api.feature.on_load(data, file); @@ -782,14 +781,14 @@ function platformLoadFiles(files, group) { api.feature.on_add_stl(data, file); load_dec(); } else if (isstl) { - const stl = new load.STL().parse(data, api.view.unit_scale()); - platformLoadWidget(group, stl, name); + const stl = new file_load.STL().parse(data, api.view.unit_scale()); + load_verts(group, stl, name); load_dec(); } else if (isobj) { - const objs = load.OBJ.parse(data.textDecode('utf-8')); + const objs = file_load.OBJ.parse(data.textDecode('utf-8')); const ondn = function() { for (let obj of objs) { - platformLoadWidget(group, obj, obj.name ? `${obj.name}-${name}` : name); + load_verts(group, obj, obj.name ? `${obj.name}-${name}` : name); } load_dec(); }; @@ -810,7 +809,7 @@ function platformLoadFiles(files, group) { api.show.progress(progress, "converting"); }, vertices => { api.show.progress(0); - let wid = platformLoadWidget(group, vertices, name); + let wid = load_verts(group, vertices, name); if (api.mode.is_cam()) { // attach raw illustration // api.event.emit('cam.parse.gerber', { data: text, mesh: wid.mesh }); @@ -821,13 +820,13 @@ function platformLoadFiles(files, group) { let odon = function(models) { let msg = api.show.alert('Adding Objects'); for (let model of models) { - platformLoadWidget(group, model.faces, model.name ? `${model.name}-${name}` : name); + load_verts(group, model.faces, model.name ? `${model.name}-${name}` : name); } load_dec(); api.hide.alert(msg); } let msg = api.show.alert('Decoding 3MF'); - load.TMF.parseAsync(data).then(models => { + file_load.TMF.parseAsync(data).then(models => { api.hide.alert(msg); if (models.length > 1 && !group) { api.uc.confirm(`group ${models.length} objects?`).then(ok => { @@ -846,14 +845,14 @@ function platformLoadFiles(files, group) { } else if (issvg) { loadSVGDialog(opt => { group = group || []; - let svg = load.SVG.parse(data.textDecode('utf-8'), opt); + let svg = file_load.SVG.parse(data.textDecode('utf-8'), opt); let ind = 0; if (svg.length === 0) { api.show.alert(`SVG contains no polylines`, 10); api.show.alert(`Fonts must be converted to paths`, 10); } for (let v of svg) { - platformLoadWidget(group, svg[ind++], ind ? `${name}-${ind}` : name); + load_verts(group, svg[ind++], ind ? `${name}-${ind}` : name); } load_dec(); }); @@ -896,7 +895,8 @@ function loadSVGDialog(doit) { }); } -function fitDeviceToWidgets() { +// resize platform (expand) to fit widgets (belt mode) +function fit() { let maxy = 0; api.widgets.each(widget => { let wb = widget.mesh.getBoundingBox().clone(); @@ -919,28 +919,28 @@ function fitDeviceToWidgets() { // extend API (api.platform) export const platform = { - fit: fitDeviceToWidgets, - add: platformAdd, - changed: platformChanged, + fit, + add, + changed, delete: platformDelete, - layout: platformLayout, - group: platformGroup, - group_done: platformGroupDone, - load: platformLoad, - load_stl: platformLoadSTL, - load_url: platformLoadURL, - load_files: platformLoadFiles, - load_verts: platformLoadWidget, - deselect: platformDeselect, - select: platformSelect, - select_all: platformSelectAll, - selected_count: platformSelectedCount, - set_bounds: platformSetBounds, - update_origin: platformUpdateOrigin, - update_bounds: platformUpdateBounds, - update_size: platformUpdateSize, - update_top_z: platformUpdateTopZ, - update_selected: platformUpdateSelected, + layout: layout, + group, + group_done, + load, + load_stl, + load_url, + load_files, + load_verts, + deselect, + select, + select_all, + selected_count, + set_bounds, + update_origin, + update_bounds, + update_size, + update_top_z, + update_selected, update: space.platform.update, set_font: space.platform.setFont, show_axes: space.platform.showAxes, From 0e0710e85b0b382a9e0a7228c8fd96a2aecbd892 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 25 Dec 2025 11:37:12 -0500 Subject: [PATCH 027/154] group one-time inits in app/init dir --- src/kiri/app/api.js | 4 +- src/kiri/app/{config => conf}/defaults.js | 0 src/kiri/app/{config => conf}/dialog.js | 0 src/kiri/app/{config => conf}/manager.js | 0 src/kiri/app/{config => conf}/settings.js | 0 src/kiri/app/devices.js | 4 +- src/kiri/app/file-ops.js | 2 +- src/kiri/app/image.js | 2 +- src/kiri/app/{init-input.js => init/input.js} | 80 ++++++++++--------- src/kiri/app/{init-lang.js => init/lang.js} | 4 +- src/kiri/app/{init-sync.js => init/syn.js} | 20 ++--- src/kiri/app/mode.js | 2 +- src/kiri/app/viewmode.js | 2 +- src/kiri/app/visuals.js | 2 +- src/kiri/app/workspace.js | 2 +- src/kiri/mode/cam/tools.js | 2 +- src/kiri/mode/fdm/init-ui.js | 2 +- src/kiri/run/engine.js | 2 +- src/main/kiri.js | 6 +- 19 files changed, 69 insertions(+), 67 deletions(-) rename src/kiri/app/{config => conf}/defaults.js (100%) rename src/kiri/app/{config => conf}/dialog.js (100%) rename src/kiri/app/{config => conf}/manager.js (100%) rename src/kiri/app/{config => conf}/settings.js (100%) rename src/kiri/app/{init-input.js => init/input.js} (92%) rename src/kiri/app/{init-lang.js => init/lang.js} (96%) rename src/kiri/app/{init-sync.js => init/syn.js} (95%) diff --git a/src/kiri/app/api.js b/src/kiri/app/api.js index 9f1c122f..4a2db7dd 100644 --- a/src/kiri/app/api.js +++ b/src/kiri/app/api.js @@ -3,7 +3,7 @@ import '../../ext/base64.js'; import alerts from './alerts.js'; -import settings from './config/manager.js'; +import settings from './conf/manager.js'; import STACKS from './stacks.js'; import web from '../../moto/webui.js'; @@ -27,7 +27,7 @@ import { noop, ajax, o2js, js2o, utils } from '../core/utils.js'; import { openFiles } from './files.js'; import { platform } from './platform.js'; import { selection } from './selected.js'; -import { settingsUI } from './config/dialog.js'; +import { settingsUI } from './conf/dialog.js'; import { showDevices } from './devices.js'; import { showTools } from '../mode/cam/tools.js'; import { space as SPACE } from '../../moto/space.js'; diff --git a/src/kiri/app/config/defaults.js b/src/kiri/app/conf/defaults.js similarity index 100% rename from src/kiri/app/config/defaults.js rename to src/kiri/app/conf/defaults.js diff --git a/src/kiri/app/config/dialog.js b/src/kiri/app/conf/dialog.js similarity index 100% rename from src/kiri/app/config/dialog.js rename to src/kiri/app/conf/dialog.js diff --git a/src/kiri/app/config/manager.js b/src/kiri/app/conf/manager.js similarity index 100% rename from src/kiri/app/config/manager.js rename to src/kiri/app/conf/manager.js diff --git a/src/kiri/app/config/settings.js b/src/kiri/app/conf/settings.js similarity index 100% rename from src/kiri/app/config/settings.js rename to src/kiri/app/conf/settings.js diff --git a/src/kiri/app/devices.js b/src/kiri/app/devices.js index a3366f72..142333a7 100644 --- a/src/kiri/app/devices.js +++ b/src/kiri/app/devices.js @@ -2,10 +2,10 @@ import { $, h } from '../../moto/webui.js'; import { api } from './api.js'; -import { conf } from './config/defaults.js'; +import { conf } from './conf/defaults.js'; import { space } from '../../moto/space.js'; import { devices as devlist } from '../../pack/kiri-devs.js'; -import { settings, conf as setconf } from './config/manager.js'; +import { settings, conf as setconf } from './conf/manager.js'; export const device = { clone: cloneDevice, diff --git a/src/kiri/app/file-ops.js b/src/kiri/app/file-ops.js index 1b0a0f81..5fec8d55 100644 --- a/src/kiri/app/file-ops.js +++ b/src/kiri/app/file-ops.js @@ -62,7 +62,7 @@ function loadCatalogFile(e) { } function updateCatalog(files) { - let table = api.ui.catalogList, + let table = api.ui.catalog.list, list = []; table.innerHTML = ''; for (let name in files) { diff --git a/src/kiri/app/image.js b/src/kiri/app/image.js index 98397779..6b21f4fb 100644 --- a/src/kiri/app/image.js +++ b/src/kiri/app/image.js @@ -4,7 +4,7 @@ import { $ } from '../../moto/webui.js'; import { api } from './api.js'; import { newWidget } from '../core/widget.js'; import { platform } from './platform.js'; -import { settings } from './config/manager.js'; +import { settings } from './conf/manager.js'; function loadImageDialog(image, name, force) { if (!force && image.byteLength > 2500000) { diff --git a/src/kiri/app/init-input.js b/src/kiri/app/init/input.js similarity index 92% rename from src/kiri/app/init-input.js rename to src/kiri/app/init/input.js index 48d67502..90b0c023 100644 --- a/src/kiri/app/init-input.js +++ b/src/kiri/app/init/input.js @@ -1,32 +1,32 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { $ } from '../../moto/webui.js'; -import { api } from './api.js'; -import { version } from '../../moto/license.js'; -import { fileOps } from './file-ops.js'; -import { init as initCAM } from '../mode/cam/init-ui.js'; -import { init as initDRAG } from '../mode/drag/init-ui.js'; -import { init as initFDM } from '../mode/fdm/init-ui.js'; -import { init as initLaser } from '../mode/laser/init-ui.js'; -import { init as initSLA } from '../mode/sla/init-ui.js'; -import { init as initWEDM } from '../mode/wedm/init-ui.js'; -import { init as initWJET } from '../mode/wjet/init-ui.js'; -import { interact } from './mouse.js'; -import { keyboard } from './keyboard.js'; -import { local as sdb } from '../../data/local.js'; -import { menu as menuCAM } from '../mode/cam/init-menu.js'; -import { menu as menuFDM } from '../mode/fdm/init-menu.js'; -import { menu as menuLaser } from '../mode/laser/init-menu.js'; -import { menu as menuSLA } from '../mode/sla/init-menu.js'; -import { modal } from './modal.js'; -import { preferences } from './preferences.js'; -import { settings as set_ctrl } from './config/manager.js'; -import { settingsOps } from './config/settings.js'; -import { slider } from './slider.js'; -import { space } from '../../moto/space.js'; -import { VIEWS, MODES, SEED } from '../core/consts.js'; +import { $ } from '../../../moto/webui.js'; +import { api } from '../api.js'; +import { version } from '../../../moto/license.js'; +import { fileOps } from '../file-ops.js'; +import { init as initCAM } from '../../mode/cam/init-ui.js'; +import { init as initDRAG } from '../../mode/drag/init-ui.js'; +import { init as initFDM } from '../../mode/fdm/init-ui.js'; +import { init as initLaser } from '../../mode/laser/init-ui.js'; +import { init as initSLA } from '../../mode/sla/init-ui.js'; +import { init as initWEDM } from '../../mode/wedm/init-ui.js'; +import { init as initWJET } from '../../mode/wjet/init-ui.js'; +import { interact } from '../mouse.js'; +import { keyboard } from '../keyboard.js'; +import { local as sdb } from '../../../data/local.js'; +import { menu as menuCAM } from '../../mode/cam/init-menu.js'; +import { menu as menuFDM } from '../../mode/fdm/init-menu.js'; +import { menu as menuLaser } from '../../mode/laser/init-menu.js'; +import { menu as menuSLA } from '../../mode/sla/init-menu.js'; +import { modal } from '../modal.js'; +import { preferences } from '../preferences.js'; +import { settings as set_ctrl } from '../conf/manager.js'; +import { settingsOps } from '../conf/settings.js'; +import { slider } from '../slider.js'; +import { space } from '../../../moto/space.js'; +import { VIEWS, MODES, SEED } from '../../core/consts.js'; -import STACKS from './stacks.js'; +import STACKS from '../stacks.js'; let { SETUP } = api.const, { CAM, SLA, FDM, LASER, DRAG, WJET, WEDM } = MODES, @@ -262,21 +262,23 @@ function init_one() { modal: $('modal'), modalBox: $('modal-box'), modals: { - help: $('mod-help'), - setup: $('mod-setup'), - prefs: $('mod-prefs'), - files: $('mod-files'), - saves: $('mod-saves'), - tools: $('mod-tools'), - xany: $('mod-x-any'), - xsla: $('mod-x-sla'), - xlaser: $('mod-x-laser'), - don8: $('mod-don8'), - any: $('mod-any'), + help: $('mod-help'), + setup: $('mod-setup'), + prefs: $('mod-prefs'), + files: $('mod-files'), + saves: $('mod-saves'), + tools: $('mod-tools'), + xany: $('mod-x-any'), + xsla: $('mod-x-sla'), + xlaser: $('mod-x-laser'), + don8: $('mod-don8'), + any: $('mod-any'), }, - catalogBody: $('catalogBody'), - catalogList: $('catalogList'), + catalog: { + body: $('catalogBody'), + list: $('catalogList'), + }, devices: $('devices'), deviceAdd: $('device-add'), diff --git a/src/kiri/app/init-lang.js b/src/kiri/app/init/lang.js similarity index 96% rename from src/kiri/app/init-lang.js rename to src/kiri/app/init/lang.js index 550fb501..5e33c039 100644 --- a/src/kiri/app/init-lang.js +++ b/src/kiri/app/init/lang.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from './api.js'; -import { local as sdb } from '../../data/local.js'; +import { api } from '../api.js'; +import { local as sdb } from '../../../data/local.js'; const { SETUP } = api.const; const DOC = self.document; diff --git a/src/kiri/app/init-sync.js b/src/kiri/app/init/syn.js similarity index 95% rename from src/kiri/app/init-sync.js rename to src/kiri/app/init/syn.js index ec0ba350..7e8b529c 100644 --- a/src/kiri/app/init-sync.js +++ b/src/kiri/app/init/syn.js @@ -1,15 +1,15 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { $ } from '../../moto/webui.js'; -import { api } from './api.js'; -import { beta, version } from '../../moto/license.js'; -import { fileOps } from './file-ops.js'; -import { local as sdb } from '../../data/local.js'; -import { preferences } from './preferences.js'; -import { settings as set_ctrl } from './config/manager.js'; -import { settingsOps } from './config/settings.js'; -import { space } from '../../moto/space.js'; -import { VIEWS } from '../core/consts.js'; +import { $ } from '../../../moto/webui.js'; +import { api } from '../api.js'; +import { beta, version } from '../../../moto/license.js'; +import { fileOps } from '../file-ops.js'; +import { local as sdb } from '../../../data/local.js'; +import { preferences } from '../preferences.js'; +import { settings as set_ctrl } from '../conf/manager.js'; +import { settingsOps } from '../conf/settings.js'; +import { space } from '../../../moto/space.js'; +import { VIEWS } from '../../core/consts.js'; const DOC = self.document; const WIN = self.window; diff --git a/src/kiri/app/mode.js b/src/kiri/app/mode.js index 1938cb2d..dcd15f2a 100644 --- a/src/kiri/app/mode.js +++ b/src/kiri/app/mode.js @@ -6,7 +6,7 @@ import { consts } from '../core/consts.js'; import { modal } from './modal.js'; import { platform } from './platform.js'; import { selection } from './selected.js'; -import { settings } from './config/manager.js'; +import { settings } from './conf/manager.js'; const { MODES, VIEWS } = consts; const clone = Object.clone; diff --git a/src/kiri/app/viewmode.js b/src/kiri/app/viewmode.js index dfdcde79..c7f3731a 100644 --- a/src/kiri/app/viewmode.js +++ b/src/kiri/app/viewmode.js @@ -5,7 +5,7 @@ import { api } from './api.js'; import { consts } from '../core/consts.js'; import { platform } from './platform.js'; import { selection } from './selected.js'; -import { settings } from './config/manager.js'; +import { settings } from './conf/manager.js'; import STACKS from './stacks.js'; const { VIEWS } = consts; diff --git a/src/kiri/app/visuals.js b/src/kiri/app/visuals.js index 9b9ea0d5..a16166e8 100644 --- a/src/kiri/app/visuals.js +++ b/src/kiri/app/visuals.js @@ -2,7 +2,7 @@ import { api } from './api.js'; import { consts } from '../core/consts.js'; -import { settings } from './config/manager.js'; +import { settings } from './conf/manager.js'; import { space as SPACE } from '../../moto/space.js'; import STACKS from './stacks.js'; diff --git a/src/kiri/app/workspace.js b/src/kiri/app/workspace.js index 2f391489..f5df2ea1 100644 --- a/src/kiri/app/workspace.js +++ b/src/kiri/app/workspace.js @@ -6,7 +6,7 @@ import { local as SDB } from '../../data/local.js'; import { openFiles } from './files.js'; import { platform } from './platform.js'; import { selection } from './selected.js'; -import { settings } from './config/manager.js'; +import { settings } from './conf/manager.js'; import { space as SPACE } from '../../moto/space.js'; import { utils } from '../core/utils.js'; import { Widget } from '../core/widget.js'; diff --git a/src/kiri/mode/cam/tools.js b/src/kiri/mode/cam/tools.js index ea0457d8..e8b533a8 100644 --- a/src/kiri/mode/cam/tools.js +++ b/src/kiri/mode/cam/tools.js @@ -3,7 +3,7 @@ import { $ } from '../../../moto/webui.js'; import { api } from '../../app/api.js'; import { consts } from '../../core/consts.js'; -import { settings as setconf } from '../../app/config/manager.js'; +import { settings as setconf } from '../../app/conf/manager.js'; import { Tool, calcTaperAngle, calcTaperBallExtent, calcTaperLength } from './tool.js'; const DEG2RAD = Math.PI / 180; diff --git a/src/kiri/mode/fdm/init-ui.js b/src/kiri/mode/fdm/init-ui.js index e36d9769..d0da4218 100644 --- a/src/kiri/mode/fdm/init-ui.js +++ b/src/kiri/mode/fdm/init-ui.js @@ -2,7 +2,7 @@ import { $ } from '../../../moto/webui.js'; import { api } from '../../app/api.js'; -import { conf } from '../../app/config/defaults.js'; +import { conf } from '../../app/conf/defaults.js'; import { util } from '../../../geo/base.js'; import { client } from '../../app/workers.js'; import { addbox, delbox, getlastbox } from '../../core/boxes.js'; diff --git a/src/kiri/run/engine.js b/src/kiri/run/engine.js index 67543b3a..eb5aa5a5 100644 --- a/src/kiri/run/engine.js +++ b/src/kiri/run/engine.js @@ -4,7 +4,7 @@ import '../../add/array.js'; import '../../add/class.js'; import '../../add/three.js'; import { api } from '../app/api.js'; -import { conf } from '../app/config/defaults.js'; +import { conf } from '../app/conf/defaults.js'; import { client } from '../app/workers.js'; import { load } from '../../load/file.js'; import { newWidget } from '../core/widget.js'; diff --git a/src/main/kiri.js b/src/main/kiri.js index e8c7723b..7411dfa3 100644 --- a/src/main/kiri.js +++ b/src/main/kiri.js @@ -7,9 +7,9 @@ import '../kiri/app/language.js'; import '../kiri/core/lang-en.js'; import { api } from '../kiri/app/api.js'; -import { init_lang } from '../kiri/app/init-lang.js'; -import { init_input } from '../kiri/app/init-input.js'; -import { init_sync } from '../kiri/app/init-sync.js'; +import { init_lang } from '../kiri/app/init/lang.js'; +import { init_input } from '../kiri/app/init/input.js'; +import { init_sync } from '../kiri/app/init/syn.js'; let traceload = location.search.indexOf('traceload') > 0; let load = []; From b79c8da8e217eae93376272dc9289cf6334c76b2 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 25 Dec 2025 12:27:13 -0500 Subject: [PATCH 028/154] separate init bindings --- src/kiri/app/devices.js | 38 ++++---- src/kiri/app/init/bind.js | 133 ++++++++++++++++++++++++++ src/kiri/app/init/input.js | 133 +++----------------------- src/kiri/app/init/{syn.js => sync.js} | 41 ++++---- src/kiri/app/selected.js | 78 +++++++-------- src/kiri/app/slider.js | 64 +++++++------ src/kiri/app/tools.js | 38 ++++++-- src/main/kiri.js | 2 +- src/moto/space.js | 2 +- 9 files changed, 288 insertions(+), 241 deletions(-) create mode 100644 src/kiri/app/init/bind.js rename src/kiri/app/init/{syn.js => sync.js} (96%) diff --git a/src/kiri/app/devices.js b/src/kiri/app/devices.js index 142333a7..c7c7e1cf 100644 --- a/src/kiri/app/devices.js +++ b/src/kiri/app/devices.js @@ -213,24 +213,24 @@ function setDeviceCode(code, devicename) { e.disabled = !local; }); - ui.deviceSave.disabled = !local; - ui.deviceDelete.disabled = !local; - ui.deviceRename.disabled = !local; - ui.deviceExport.disabled = !local; - ui.deviceAdd.style.display = mode === 'SLA' ? 'none' : ''; + ui.devices.save.disabled = !local; + ui.devices.delete.disabled = !local; + ui.devices.rename.disabled = !local; + ui.devices.export.disabled = !local; + ui.devices.add.style.display = mode === 'SLA' ? 'none' : ''; if (local) { - ui.deviceAdd.innerText = "copy"; - ui.deviceDelete.style.display = ''; - ui.deviceRename.style.display = ''; - ui.deviceExport.style.display = ''; + ui.devices.add.innerText = "copy"; + ui.devices.delete.style.display = ''; + ui.devices.rename.style.display = ''; + ui.devices.export.style.display = ''; } else { - ui.deviceAdd.innerText = "customize"; - ui.deviceDelete.style.display = 'none'; - ui.deviceRename.style.display = 'none'; - ui.deviceExport.style.display = 'none'; + ui.devices.add.innerText = "customize"; + ui.devices.delete.style.display = 'none'; + ui.devices.rename.style.display = 'none'; + ui.devices.export.style.display = 'none'; } - ui.deviceAdd.disabled = dev.noclone; + ui.devices.add.disabled = dev.noclone; setconf.update_fields(); space.platform.setBelt(isBelt()); @@ -301,7 +301,7 @@ function renderDevices(devices) { event.emit('devices.render', devices); - ui.deviceSave.onclick = function() { + ui.devices.save.onclick = function() { event.emit('device.save'); api.function.clear(); setconf.save(); @@ -309,18 +309,18 @@ function renderDevices(devices) { showDevices(); api.modal.hide(); }; - ui.deviceAdd.onclick = function() { + ui.devices.add.onclick = function() { api.function.clear(); cloneDevice(); showDevices(); }; - ui.deviceDelete.onclick = function() { + ui.devices.delete.onclick = function() { api.function.clear(); removeLocalDevice(getSelectedDevice()); selectDevice(getModeDevices()[0]); showDevices(); }; - ui.deviceRename.onclick = function() { + ui.devices.rename.onclick = function() { api.uc.prompt(`Rename "${selected}`, selected).then(newname => { if (newname) { updateDeviceName(newname); @@ -332,7 +332,7 @@ function renderDevices(devices) { } }); }; - ui.deviceExport.onclick = function(event) { + ui.devices.export.onclick = function(event) { const record = { version: api.version, device: selected, diff --git a/src/kiri/app/init/bind.js b/src/kiri/app/init/bind.js new file mode 100644 index 00000000..468ce17f --- /dev/null +++ b/src/kiri/app/init/bind.js @@ -0,0 +1,133 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { $ } from '../../../moto/webui.js'; +import { api } from '../api.js'; + +export function bind() { + return { + alert: { + dialog: $('alert-area'), + text: $('alert-text') + }, + func: { + slice: $('act-slice'), + preview: $('act-preview'), + animate: $('act-animate'), + export: $('act-export') + }, + label: { + slice: $('label-slice'), + preview: $('label-preview'), + animate: $('label-animate'), + export: $('label-export'), + }, + acct: { + help: $('app-help'), + don8: $('app-don8'), + mesh: $('app-mesh'), + export: $('app-export') + }, + dev: { + header: $('dev-header'), + search: $('dev-search'), + filter: $('dev-filter') + }, + mesh: { + name: $('mesh-name'), + points: $('mesh-points'), + faces: $('mesh-faces'), + }, + + stats: { + fps: $('fps'), + rms: $('rms'), + div: $('stats'), + rnfo: $('rnfo'), + }, + + load: $('load-file'), + speeds: $('speeds'), + speedbar: $('speedbar'), + + context: $('context-menu'), + + // ltsetup: $('lt-setup'), + // ltfile: $('lt-file'), + // ltview: $('lt-view'), + // ltact: $('lt-start'), + // edit: $('lt-tools'), + nozzle: $('menu-nozzle'), + + modal: $('modal'), + modalBox: $('modal-box'), + modals: { + help: $('mod-help'), + setup: $('mod-setup'), + prefs: $('mod-prefs'), + files: $('mod-files'), + saves: $('mod-saves'), + tools: $('mod-tools'), + xany: $('mod-x-any'), + xsla: $('mod-x-sla'), + xlaser: $('mod-x-laser'), + don8: $('mod-don8'), + any: $('mod-any'), + }, + + catalog: { + body: $('catalogBody'), + list: $('catalogList'), + }, + + devices: { + div: $('devices'), + add: $('device-add'), + delete: $('device-del'), + export: $('device-exp'), + rename: $('device-ren'), + save: $('device-save'), + }, + + setMenu: $('set-menu'), + settings: $('settings'), + settingsBody: $('settingsBody'), + settingsList: $('settingsList'), + + slider: { + div: $('slider'), + hi: $('slider-hi'), + hold: $('slider-hold'), + lo: $('slider-lo'), + max: $('slider-max'), + mid: $('slider-mid'), + min: $('slider-zero'), + range: $('slider-center'), + }, + + loading: $('progress').style, + progress: $('progbar').style, + prostatus: $('progtxt'), + selection: $('selection'), + + size: { + X: $('size_x'), + Y: $('size_y'), + Z: $('size_z'), + }, + scale: { + X: $('scale_x'), + Y: $('scale_y'), + Z: $('scale_z'), + }, + lock: { + X: $('lock_x'), + Y: $('lock_y'), + Z: $('lock_z'), + }, + + stock: $('stock'), + stockWidth: $('stock-width'), + stockDepth: $('stock-width'), + stockHeight: $('stock-width'), + }; +} diff --git a/src/kiri/app/init/input.js b/src/kiri/app/init/input.js index 90b0c023..971950e8 100644 --- a/src/kiri/app/init/input.js +++ b/src/kiri/app/init/input.js @@ -2,6 +2,7 @@ import { $ } from '../../../moto/webui.js'; import { api } from '../api.js'; +import { bind } from './bind.js'; import { version } from '../../../moto/license.js'; import { fileOps } from '../file-ops.js'; import { init as initCAM } from '../../mode/cam/init-ui.js'; @@ -207,118 +208,8 @@ function init_one() { tracker: tracker, container: container, - alert: { - dialog: $('alert-area'), - text: $('alert-text') - }, - func: { - slice: $('act-slice'), - preview: $('act-preview'), - animate: $('act-animate'), - export: $('act-export') - }, - label: { - slice: $('label-slice'), - preview: $('label-preview'), - animate: $('label-animate'), - export: $('label-export'), - }, - acct: { - help: $('app-help'), - don8: $('app-don8'), - mesh: $('app-mesh'), - export: $('app-export') - }, - dev: { - header: $('dev-header'), - search: $('dev-search'), - filter: $('dev-filter') - }, - mesh: { - name: $('mesh-name'), - points: $('mesh-points'), - faces: $('mesh-faces'), - }, - - stats: { - fps: $('fps'), - rms: $('rms'), - div: $('stats'), - rnfo: $('rnfo'), - }, - - load: $('load-file'), - speeds: $('speeds'), - speedbar: $('speedbar'), - context: $('context-menu'), - - ltsetup: $('lt-setup'), - ltfile: $('lt-file'), - ltview: $('lt-view'), - ltact: $('lt-start'), - edit: $('lt-tools'), - nozzle: $('menu-nozzle'), - - modal: $('modal'), - modalBox: $('modal-box'), - modals: { - help: $('mod-help'), - setup: $('mod-setup'), - prefs: $('mod-prefs'), - files: $('mod-files'), - saves: $('mod-saves'), - tools: $('mod-tools'), - xany: $('mod-x-any'), - xsla: $('mod-x-sla'), - xlaser: $('mod-x-laser'), - don8: $('mod-don8'), - any: $('mod-any'), - }, - - catalog: { - body: $('catalogBody'), - list: $('catalogList'), - }, - - devices: $('devices'), - deviceAdd: $('device-add'), - deviceDelete: $('device-del'), - deviceRename: $('device-ren'), - deviceExport: $('device-exp'), - deviceSave: $('device-save'), - - setMenu: $('set-menu'), - settings: $('settings'), - settingsBody: $('settingsBody'), - settingsList: $('settingsList'), - - slider: $('slider'), - sliderMax: $('slider-max'), - sliderMin: $('slider-zero'), - sliderLo: $('slider-lo'), - sliderMid: $('slider-mid'), - sliderHi: $('slider-hi'), - sliderHold: $('slider-hold'), - sliderRange: $('slider-center'), - - loading: $('progress').style, - progress: $('progbar').style, - prostatus: $('progtxt'), - - selection: $('selection'), - sizeX: $('size_x'), - sizeY: $('size_y'), - sizeZ: $('size_z'), - scaleX: $('scale_x'), - scaleY: $('scale_y'), - scaleZ: $('scale_z'), - lockX: $('lock_x'), - lockY: $('lock_y'), - lockZ: $('lock_z'), - stock: $('stock'), - stockWidth: $('stock-width'), - stockDepth: $('stock-width'), - stockHeight: $('stock-width'), + // static html --> ui bindings + ...bind(), /** Device Browser / Editor */ @@ -592,19 +483,19 @@ function init_one() { input_binding(ui); $('lab-axis').onclick = () => { - ui.lockX.checked = - ui.lockY.checked = - ui.lockZ.checked = !( - ui.lockX.checked || - ui.lockY.checked || - ui.lockZ.checked + ui.lock.X.checked = + ui.lock.Y.checked = + ui.lock.Z.checked = !( + ui.lock.X.checked || + ui.lock.Y.checked || + ui.lock.Z.checked ); }; $('scale-reset').onclick = $('lab-scale').onclick = () => { - selection.scale(1 / ui.scaleX.was, 1 / ui.scaleY.was, 1 / ui.scaleZ.was); - ui.scaleX.value = ui.scaleY.value = ui.scaleZ.value = - ui.scaleX.was = ui.scaleY.was = ui.scaleZ.was = 1; + selection.scale(1 / ui.scale.X.was, 1 / ui.scale.Y.was, 1 / ui.scale.Z.was); + ui.scale.X.value = ui.scale.Y.value = ui.scale.Z.value = + ui.scale.X.was = ui.scale.Y.was = ui.scale.Z.was = 1; }; $('app-xpnd').onclick = () => { diff --git a/src/kiri/app/init/syn.js b/src/kiri/app/init/sync.js similarity index 96% rename from src/kiri/app/init/syn.js rename to src/kiri/app/init/sync.js index 7e8b529c..cae72456 100644 --- a/src/kiri/app/init/syn.js +++ b/src/kiri/app/init/sync.js @@ -10,6 +10,7 @@ import { settings as set_ctrl } from '../conf/manager.js'; import { settingsOps } from '../conf/settings.js'; import { space } from '../../../moto/space.js'; import { VIEWS } from '../../core/consts.js'; +import * as view_tools from '../tools.js'; const DOC = self.document; const WIN = self.window; @@ -201,6 +202,18 @@ function setup_keybd_nav(ui, WIN, LANG) { const { selection } = api; // bind interface action elements + ui.acct.help.onclick = (ev) => { ev.stopPropagation(); api.help.show() }; + ui.acct.don8.onclick = (ev) => { ev.stopPropagation(); api.modal.show('don8') }; + ui.acct.mesh.onclick = (ev) => { ev.stopPropagation(); WIN.location = "/mesh" }; + ui.acct.export.onclick = (ev) => { ev.stopPropagation(); settingsOps.profileExport() }; + ui.acct.export.title = LANG.acct_xpo; + ui.func.slice.onclick = (ev) => { ev.stopPropagation(); api.function.slice() }; + ui.func.preview.onclick = (ev) => { ev.stopPropagation(); api.function.print() }; + ui.func.animate.onclick = (ev) => { ev.stopPropagation(); api.function.animate() }; + ui.func.export.onclick = (ev) => { ev.stopPropagation(); api.function.export() }; + // prevent modal input from propagating to parents + ui.modalBox.onclick = (ev) => { ev.stopPropagation() }; + $('export-support-a').onclick = (ev) => { ev.stopPropagation(); api.modal.show('don8') }; $('mode-device').onclick = api.show.devices; $('mode-profile').onclick = settingsOps.settingsLoad; @@ -215,18 +228,9 @@ function setup_keybd_nav(ui, WIN, LANG) { $('set-profs').onclick = (ev) => { ev.stopPropagation(); api.conf.show() }; $('set-tools').onclick = (ev) => { ev.stopPropagation(); api.show.tools() }; $('set-prefs').onclick = (ev) => { ev.stopPropagation(); api.modal.show('prefs') }; - ui.acct.help.onclick = (ev) => { ev.stopPropagation(); api.help.show() }; - ui.acct.don8.onclick = (ev) => { ev.stopPropagation(); api.modal.show('don8') }; - ui.acct.mesh.onclick = (ev) => { ev.stopPropagation(); WIN.location = "/mesh" }; - ui.acct.export.onclick = (ev) => { ev.stopPropagation(); settingsOps.profileExport() }; - ui.acct.export.title = LANG.acct_xpo; $('file-new').onclick = (ev) => { ev.stopPropagation(); settingsOps.workspaceNew() }; $('file-recent').onclick = () => { api.modal.show('files') }; $('file-import').onclick = (ev) => { api.event.import(ev); }; - ui.func.slice.onclick = (ev) => { ev.stopPropagation(); api.function.slice() }; - ui.func.preview.onclick = (ev) => { ev.stopPropagation(); api.function.print() }; - ui.func.animate.onclick = (ev) => { ev.stopPropagation(); api.function.animate() }; - ui.func.export.onclick = (ev) => { ev.stopPropagation(); api.function.export() }; $('view-arrange').onclick = api.platform.layout; $('view-top').onclick = space.view.top; $('view-home').onclick = space.view.home; @@ -238,12 +242,14 @@ function setup_keybd_nav(ui, WIN, LANG) { api.widgets.for(w => w.unrotate()); selection.update_info(); }; + // attach button handlers to support targets for (let btn of ["don8pt","don8gh","don8pp"]) { $(btn).onclick = (ev) => { window.open(ev.target.children[0].href); } } + // rotation buttons let d = (Math.PI / 180); $('rot_x_lt').onclick = () => { selection.rotate(-d * $('rot_x').value,0,0) }; @@ -252,6 +258,7 @@ function setup_keybd_nav(ui, WIN, LANG) { $('rot_y_gt').onclick = () => { selection.rotate(0, d * $('rot_y').value,0) }; $('rot_z_lt').onclick = () => { selection.rotate(0,0, d * $('rot_z').value) }; $('rot_z_gt').onclick = () => { selection.rotate(0,0,-d * $('rot_z').value) }; + // rendering options $('render-edges').onclick = () => { api.view.edges({ toggle: true }); api.conf.save() }; $('render-ghost').onclick = () => { api.view.wireframe(false, 0, api.view.is_arrange() ? 0.4 : 0.25); }; @@ -263,21 +270,15 @@ function setup_keybd_nav(ui, WIN, LANG) { $('mesh-split').onclick = selection.isolateBodies; $('context-duplicate').onclick = selection.duplicate; $('context-mirror').onclick = selection.mirror; - $('context-layflat').onclick = () => { api.event.emit("tool.mesh.lay-flat") }; - $('context-lefty').onclick = () => { api.event.emit("tool.mesh.lefty") }; + $('context-layflat').onclick = view_tools.startLayFlat; + $('context-lefty').onclick = view_tools.startLeftAlign; $('context-setfocus').onclick = () => { - api.event.emit( - "tool.camera.focus", - ev => api.space.set_focus(undefined, ev.object.point) - ); + view_tools.startFocus(ev => api.space.set_focus(undefined, ev.object.point)); }; - $('context-contents').onclick = () => { api.SPACE.view.fit() }; - $('view-fit').onclick = () => { api.SPACE.view.fit() }; + $('context-contents').onclick = api.const.SPACE.view.fit; + $('view-fit').onclick = api.const.SPACE.view.fit; $('wassup').onmouseover = () => { $('suppopp').classList.remove('hide') }; - // prevent modal input from propagating to parents - ui.modalBox.onclick = (ev) => { ev.stopPropagation() }; - // enable modal hiding $('mod-x').onclick = api.modal.hide; diff --git a/src/kiri/app/selected.js b/src/kiri/app/selected.js index 87a25422..be494f20 100644 --- a/src/kiri/app/selected.js +++ b/src/kiri/app/selected.js @@ -78,12 +78,12 @@ function update_info() { }); if (bounds.min.x === Infinity) { if (selectedMeshes.length === 0) { - ui.sizeX.value = 0; - ui.sizeY.value = 0; - ui.sizeZ.value = 0; - ui.scaleX.value = 1; - ui.scaleY.value = 1; - ui.scaleZ.value = 1; + ui.size.X.value = 0; + ui.size.Y.value = 0; + ui.size.Z.value = 0; + ui.scale.X.value = 1; + ui.scale.Y.value = 1; + ui.scale.Z.value = 1; } return; } @@ -91,12 +91,12 @@ function update_info() { dy = bounds.max.y - bounds.min.y, dz = bounds.max.z - bounds.min.z, scale = api.view.unit_scale(); - ui.sizeX.value = ui.sizeX.was = (dx / scale).round(2) - ui.sizeY.value = ui.sizeY.was = (dy / scale).round(2) - ui.sizeZ.value = ui.sizeZ.was = (dz / scale).round(2) - ui.scaleX.value = ui.scaleX.was = track.scale.x.round(2); - ui.scaleY.value = ui.scaleY.was = track.scale.y.round(2); - ui.scaleZ.value = ui.scaleZ.was = track.scale.z.round(2); + ui.size.X.value = ui.size.X.was = (dx / scale).round(2) + ui.size.Y.value = ui.size.Y.was = (dy / scale).round(2) + ui.size.Z.value = ui.size.Z.was = (dz / scale).round(2) + ui.scale.X.value = ui.scale.X.was = track.scale.x.round(2); + ui.scale.Y.value = ui.scale.Y.was = track.scale.y.round(2); + ui.scale.Z.value = ui.scale.Z.was = track.scale.z.round(2); update_bounds(); } @@ -365,13 +365,13 @@ function input_resize(e, ui) { let dv = parseFloat(e.target.value || 1), pv = parseFloat(e.target.was || 1), ra = dv / pv, - xv = parseFloat(ui.sizeX.was ?? ui.scaleX.value) || 1, - yv = parseFloat(ui.sizeY.was ?? ui.scaleY.value) || 1, - zv = parseFloat(ui.sizeZ.was ?? ui.scaleZ.value) || 1, + xv = parseFloat(ui.size.X.was ?? ui.scale.X.value) || 1, + yv = parseFloat(ui.size.Y.was ?? ui.scale.Y.value) || 1, + zv = parseFloat(ui.size.Z.was ?? ui.scale.Z.value) || 1, ta = e.target, - xc = ui.lockX.checked, - yc = ui.lockY.checked, - zc = ui.lockZ.checked, + xc = ui.lock.X.checked, + yc = ui.lock.Y.checked, + zc = ui.lock.Z.checked, xt = ta === ui.sizeX, yt = ta === ui.sizeY, zt = ta === ui.sizeZ, @@ -385,25 +385,25 @@ function input_resize(e, ui) { return; } selection.scale(xr,yr,zr); - ui.sizeX.was = ui.sizeX.value = xv * xr; - ui.sizeY.was = ui.sizeY.value = yv * yr; - ui.sizeZ.was = ui.sizeZ.value = zv * zr; + ui.size.X.was = ui.size.X.value = xv * xr; + ui.size.Y.was = ui.size.Y.value = yv * yr; + ui.size.Z.was = ui.size.Z.value = zv * zr; } function input_scale(e, ui) { let dv = parseFloat(e.target.value || 1), pv = parseFloat(e.target.was || 1), ra = dv / pv, - xv = parseFloat(ui.scaleX.was ?? ui.scaleX.value) || 1, - yv = parseFloat(ui.scaleY.was ?? ui.scaleY.value) || 1, - zv = parseFloat(ui.scaleZ.was ?? ui.scaleY.value) || 1, + xv = parseFloat(ui.scale.X.was ?? ui.scale.X.value) || 1, + yv = parseFloat(ui.scale.Y.was ?? ui.scale.Y.value) || 1, + zv = parseFloat(ui.scale.Z.was ?? ui.scale.Y.value) || 1, ta = e.target, - xc = ui.lockX.checked, - yc = ui.lockY.checked, - zc = ui.lockZ.checked, - xt = ta === ui.scaleX, - yt = ta === ui.scaleY, - zt = ta === ui.scaleZ, + xc = ui.lock.X.checked, + yc = ui.lock.Y.checked, + zc = ui.lock.Z.checked, + xt = ta === ui.scale.X, + yt = ta === ui.scale.Y, + zt = ta === ui.scale.Z, tl = (xt && xc) || (yt && yc) || (zt && zc), xr = ((tl && xc) || (!tl && xt) ? ra : 1), yr = ((tl && yc) || (!tl && yt) ? ra : 1), @@ -414,9 +414,9 @@ function input_scale(e, ui) { return; } selection.scale(xr,yr,zr); - ui.scaleX.was = ui.scaleX.value = xv * xr; - ui.scaleY.was = ui.scaleY.value = yv * yr; - ui.scaleZ.was = ui.scaleZ.value = zv * zr; + ui.scale.X.was = ui.scale.X.value = xv * xr; + ui.scale.Y.was = ui.scale.Y.value = yv * yr; + ui.scale.Z.was = ui.scale.Z.value = zv * zr; } function parse_as_float(e) { @@ -426,12 +426,12 @@ function parse_as_float(e) { function input_binding(ui) { // on enter but not on blur space.event.onEnterKey([ - ui.scaleX, (e) => input_scale(e, ui), - ui.scaleY, (e) => input_scale(e, ui), - ui.scaleZ, (e) => input_scale(e, ui), - ui.sizeX, (e) => input_resize(e, ui), - ui.sizeY, (e) => input_resize(e, ui), - ui.sizeZ, (e) => input_resize(e, ui), + ui.scale.X, (e) => input_scale(e, ui), + ui.scale.Y, (e) => input_scale(e, ui), + ui.scale.Z, (e) => input_scale(e, ui), + ui.size.X, (e) => input_resize(e, ui), + ui.size.Y, (e) => input_resize(e, ui), + ui.size.Z, (e) => input_resize(e, ui), ]); // on enter and blur space.event.onEnterKey([ diff --git a/src/kiri/app/slider.js b/src/kiri/app/slider.js index f588340b..2b7bb974 100644 --- a/src/kiri/app/slider.js +++ b/src/kiri/app/slider.js @@ -10,6 +10,7 @@ class SliderControl { #layerHi = 0; #layerMax = 0; #ui; + #slider; #tracker; #mobile; #sliderBar; @@ -39,6 +40,7 @@ class SliderControl { } this.#ui = ui; + this.#slider = ui.slider; this.#tracker = tracker; this.#mobile = mobile; this.#sliderBar = mobile ? 80 : 30; @@ -86,7 +88,7 @@ class SliderControl { */ setMax(max) { this.#layerMax = max; - this.#ui.sliderMax.innerText = max; + this.#slider.max.innerText = max; if (max < this.#layerHi) { this.#layerHi = max; this.showLabels(); @@ -100,7 +102,7 @@ class SliderControl { * @param {number} endPercent - End position (0-1) */ updatePosition(startPercent, endPercent) { - const width = this.#ui.sliderRange.clientWidth; + const width = this.#slider.range.clientWidth; const maxval = width - this.#sliderBar2; const start = Math.max(0, Math.min(1, startPercent)); const end = Math.max(start, Math.min(1, endPercent)); @@ -109,9 +111,9 @@ class SliderControl { const midval = ((end - start) * maxval) + this.#sliderBar; const hival = maxval - end * maxval; - this.#ui.sliderHold.style.marginLeft = `${lowval}px`; - this.#ui.sliderMid.style.width = `${midval}px`; - this.#ui.sliderHold.style.marginRight = `${hival}px`; + this.#slider.hold.style.marginLeft = `${lowval}px`; + this.#slider.mid.style.width = `${midval}px`; + this.#slider.hold.style.marginRight = `${hival}px`; } /** @@ -119,10 +121,10 @@ class SliderControl { */ showLabels() { const digits = this.#layerMax.toString().length; - this.#ui.sliderZero.style.width = `${digits}em`; - this.#ui.sliderMax.style.width = `${digits}em`; - this.#ui.sliderZero.innerText = this.#layerLo; - this.#ui.sliderMax.innerText = this.#layerHi; + this.#slider.min.style.width = `${digits}em`; + this.#slider.max.style.width = `${digits}em`; + this.#slider.min.innerText = this.#layerLo; + this.#slider.max.innerText = this.#layerHi; } /** @@ -130,7 +132,7 @@ class SliderControl { */ show() { this.#ui.layers.style.display = 'flex'; - this.#ui.slider.style.display = 'flex'; + this.#slider.div.style.display = 'flex'; } /** @@ -138,20 +140,20 @@ class SliderControl { */ hide() { this.#ui.layers.style.display = 'none'; - this.#ui.slider.style.display = 'none'; + this.#slider.div.style.display = 'none'; } #setupUI() { if (this.#mobile) { - this.#ui.slider.classList.add('slider-mobile'); - this.#ui.sliderLo.classList.add('slider-mobile'); - this.#ui.sliderHi.classList.add('slider-mobile'); + this.#slider.div.classList.add('slider-mobile'); + this.#slider.lo.classList.add('slider-mobile'); + this.#slider.hi.classList.add('slider-mobile'); } } #bindEvents() { // Min/Max buttons - this.#ui.sliderMin.onclick = () => { + this.#slider.min.onclick = () => { this.#layerLo = 0; this.#layerHi = 0; this.showLabels(); @@ -159,7 +161,7 @@ class SliderControl { this.#notifyChange(); }; - this.#ui.sliderMax.onclick = () => { + this.#slider.max.onclick = () => { this.#layerLo = this.#layerMax; this.#layerHi = this.#layerMax; this.showLabels(); @@ -168,24 +170,24 @@ class SliderControl { }; // Mouse hover for labels - this.#ui.slider.onmouseover = () => { + this.#slider.div.onmouseover = () => { this.showLabels(); }; - this.#ui.slider.onmouseleave = (ev) => { + this.#slider.div.onmouseleave = (ev) => { if (!ev.buttons) { this.hideLabels(); } }; // Drag handlers for the three slider sections - this.#bindDrag(this.#ui.sliderLo, this.#handleLoDrag.bind(this)); - this.#bindDrag(this.#ui.sliderMid, this.#handleMidDrag.bind(this)); - this.#bindDrag(this.#ui.sliderHi, this.#handleHiDrag.bind(this)); + this.#bindDrag(this.#slider.lo, this.#handleLoDrag.bind(this)); + this.#bindDrag(this.#slider.mid, this.#handleMidDrag.bind(this)); + this.#bindDrag(this.#slider.hi, this.#handleHiDrag.bind(this)); } #bindDrag(el, deltaHandler) { - const slider = this.#ui.sliderRange; + const slider = this.#slider.range; el.ontouchstart = el.onmousedown = (ev) => { this.#tracker.style.display = 'block'; @@ -195,9 +197,9 @@ class SliderControl { this.#drag.width = slider.clientWidth; this.#drag.maxval = this.#drag.width - this.#sliderBar2; this.#drag.start = obj.screenX; - this.#drag.loat = this.#drag.low = this.#pxToInt(this.#ui.sliderHold.style.marginLeft); - this.#drag.mdat = this.#drag.mid = this.#ui.sliderMid.clientWidth; - this.#drag.hiat = this.#pxToInt(this.#ui.sliderHold.style.marginRight); + this.#drag.loat = this.#drag.low = this.#pxToInt(this.#slider.hold.style.marginLeft); + this.#drag.mdat = this.#drag.mid = this.#slider.mid.clientWidth; + this.#drag.hiat = this.#pxToInt(this.#slider.hold.style.marginRight); this.#drag.mdmax = this.#drag.width - this.#sliderBar - this.#drag.loat; this.#drag.himax = this.#drag.width - this.#sliderBar - this.#drag.mdat; @@ -233,8 +235,8 @@ class SliderControl { return; } - this.#ui.sliderHold.style.marginLeft = `${lowval}px`; - this.#ui.sliderMid.style.width = `${midval}px`; + this.#slider.hold.style.marginLeft = `${lowval}px`; + this.#slider.mid.style.width = `${midval}px`; this.#drag.low = lowval; this.#drag.mid = midval; this.#updateFromDrag(); @@ -248,8 +250,8 @@ class SliderControl { return; } - this.#ui.sliderHold.style.marginLeft = `${loval}px`; - this.#ui.sliderHold.style.marginRight = `${hival}px`; + this.#slider.hold.style.marginLeft = `${loval}px`; + this.#slider.hold.style.marginRight = `${hival}px`; this.#drag.low = loval; this.#updateFromDrag(); } @@ -262,8 +264,8 @@ class SliderControl { return; } - this.#ui.sliderMid.style.width = `${midval}px`; - this.#ui.sliderHold.style.marginRight = `${hival}px`; + this.#slider.mid.style.width = `${midval}px`; + this.#slider.hold.style.marginRight = `${hival}px`; this.#drag.mid = midval; this.#updateFromDrag(); } diff --git a/src/kiri/app/tools.js b/src/kiri/app/tools.js index 6802b718..ea3cdee4 100644 --- a/src/kiri/app/tools.js +++ b/src/kiri/app/tools.js @@ -105,29 +105,37 @@ function cleanup() { api.event.on('key.esc', endIt); -api.event.on('tool.camera.focus', (fn) => { +function startFocus(fn) { opName = 'camera focus'; onDone = fn; startIt(); -}); +} -api.event.on('tool.mesh.face-up', () => { +function startFaceUp() { opName = 'face select'; onDone = onFaceUpSelect; startIt(); -}); +} -api.event.on('tool.mesh.lay-flat', () => { +function startLayFlat() { opName = 'lay flat'; onDone = onLayFlatSelect; startIt(); -}); +} -api.event.on('tool.mesh.lefty', () => { +function startLeftAlign() { opName = 'align y axis'; onDone = onLeftySelect; startIt(); -}); +} + +api.event.on('tool.camera.focus', startFocus); + +api.event.on('tool.mesh.face-up', startFaceUp); + +api.event.on('tool.mesh.lay-flat', startLayFlat); + +api.event.on('tool.mesh.lefty', startLeftAlign); function scale() { let cam = space.internals().camera; @@ -187,4 +195,16 @@ api.event.on('mouse.hover.up', (ev) => { }); -export { onLayFlatSelect, onFaceUpSelect, startIt, endIt, cleanup, scale, onDone }; +export { + cleanup, + endIt, + onDone, + onFaceUpSelect, + onLayFlatSelect, + scale, + startFaceUp, + startFocus, + startIt, + startLeftAlign, + startLayFlat, +}; diff --git a/src/main/kiri.js b/src/main/kiri.js index 7411dfa3..17a1b119 100644 --- a/src/main/kiri.js +++ b/src/main/kiri.js @@ -9,7 +9,7 @@ import '../kiri/core/lang-en.js'; import { api } from '../kiri/app/api.js'; import { init_lang } from '../kiri/app/init/lang.js'; import { init_input } from '../kiri/app/init/input.js'; -import { init_sync } from '../kiri/app/init/syn.js'; +import { init_sync } from '../kiri/app/init/sync.js'; let traceload = location.search.indexOf('traceload') > 0; let load = []; diff --git a/src/moto/space.js b/src/moto/space.js index 524c07e7..1c3202b3 100644 --- a/src/moto/space.js +++ b/src/moto/space.js @@ -1335,7 +1335,7 @@ let Space = { } viewControl.update(); - if (then) then(); + if (typeof(then) === 'function') then(); }, setCtrl: (name) => { if (name === 'onshape') { From 66c915a1eda0fcce464bc5b76825c6b515bbae24 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 25 Dec 2025 12:48:55 -0500 Subject: [PATCH 029/154] input setup cleanup --- src/kiri/app/init/bind.js | 2 + src/kiri/app/init/input.js | 124 +++++++++++++------------------------ src/kiri/app/init/sync.js | 21 +++---- src/kiri/app/keyboard.js | 2 + src/kiri/app/modal.js | 2 + src/kiri/app/slider.js | 2 + 6 files changed, 58 insertions(+), 95 deletions(-) diff --git a/src/kiri/app/init/bind.js b/src/kiri/app/init/bind.js index 468ce17f..826c5ac6 100644 --- a/src/kiri/app/init/bind.js +++ b/src/kiri/app/init/bind.js @@ -92,6 +92,8 @@ export function bind() { settings: $('settings'), settingsBody: $('settingsBody'), settingsList: $('settingsList'), + settingsName: $('settingsName'), + settingsSave: $('settingsSave'), slider: { div: $('slider'), diff --git a/src/kiri/app/init/input.js b/src/kiri/app/init/input.js index 971950e8..ef054233 100644 --- a/src/kiri/app/init/input.js +++ b/src/kiri/app/init/input.js @@ -86,7 +86,7 @@ api.event.emit('preset', api.conf.dbo()); // api.event.on("set.threaded", bool => setThreaded(bool)); -function onBooleanClick(el) { +export function onBooleanClick(el) { // copy some ui elements to target settings let settings = api.conf.get(); settings.device.bedBelt = ui.deviceBelt.checked && api.mode.is_fdm(); @@ -130,8 +130,17 @@ function isNotBelt() { return !isBelt(); } +function onResize() { + if (WIN.innerHeight < 800) { + ui.modalBox.classList.add('mh85'); + } else { + ui.modalBox.classList.remove('mh85'); + } + api.view.update_slider(); +} + // MAIN UI INITIALIZATION FUNCTION -function init_one() { +export function init_input() { let { event, conf, view, show } = api, { newBlank, newButton, newBoolean, newGroup, newInput } = uc, { newSelect, newRow, newGCode, newDiv, toInt, toFloat } = uc; @@ -154,19 +163,11 @@ function init_one() { tracker = $('tracker'), controller = settings().controller; - WIN.addEventListener("resize", () => { - event.emit('resize'); - }); - - event.on('resize', () => { - if (WIN.innerHeight < 800) { - ui.modalBox.classList.add('mh85'); - } else { - ui.modalBox.classList.remove('mh85'); - } - view.update_slider(); - }); + // window resize handlers + WIN.addEventListener("resize", onResize); + event.on('resize', onResize); + // configure moto.space space.sky.showGrid(false); space.sky.setColor(controller.dark ? 0 : 0xffffff); space.setAntiAlias(controller.antiAlias); @@ -326,9 +327,6 @@ function init_one() { ...menuSLA(), layers: uc.setGroup($("layers")), - - settingsName: $('settingsName'), - settingsSave: $('settingsSave'), }); // run client mode initializations right after UI is bound @@ -347,8 +345,8 @@ function init_one() { settingsOps.settingsSave(undefined, ui.settingsName.value); }; - // initialize modal controller - modal.init({ + // initialize and expose modal to API + api.modal = modal.init({ ui: { modal: ui.modal, modals: ui.modals @@ -361,23 +359,9 @@ function init_one() { } }); - // expose modal to API - api.modal = modal; - - // slider setup - slider.init({ - ui: { - layers: ui.layers, - slider: ui.slider, - sliderRange: ui.sliderRange, - sliderHold: ui.sliderHold, - sliderMid: ui.sliderMid, - sliderLo: ui.sliderLo, - sliderHi: ui.sliderHi, - sliderMin: ui.sliderMin, - sliderMax: ui.sliderMax, - sliderZero: $('slider-zero'), - }, + // initialize and expose slider to API + api.slider = slider.init({ + ui, tracker: tracker, mobile: space.info.mob, onLayerChange: (hi, lo) => { @@ -392,11 +376,8 @@ function init_one() { } }); - // expose slider to API - api.slider = slider; - - // initialize keyboard controller - keyboard.init({ + // initialize and expose keyboard to API + api.keyboard = keyboard.init({ api, platform, selection, @@ -413,65 +394,46 @@ function init_one() { settingsLoad: settingsOps.settingsLoad }); - // expose keyboard to API - api.keyboard = keyboard; - // add mobile class to body if needed if (space.info.mob) { DOC.body.classList.add('mobile'); } - ui.load.onchange = function(event) { + ui.load.onchange = (event) => { api.platform.load_files(event.target.files); ui.load.value = ''; // reset so you can re-import the same filee }; // store layer preferences - api.event.on('stack.show', label => { + api.event.on('stack.show', (label) => { let mode = api.mode.get(); let view = api.view.get(); api.conf.get().labels[`${mode}-${view}-${label}`] = true; }); - api.event.on('stack.hide', label => { + api.event.on('stack.hide', (label) => { let mode = api.mode.get(); let view = api.view.get(); api.conf.get().labels[`${mode}-${view}-${label}`] = false; }); - // bind language choices - $('lset-en').onclick = function() { - sdb.setItem('kiri-lang', 'en-us'); - api.space.reload(); - }; - $('lset-da').onclick = function() { - sdb.setItem('kiri-lang', 'da-dk'); - api.space.reload(); - }; - $('lset-de').onclick = function() { - sdb.setItem('kiri-lang', 'de-de'); - api.space.reload(); - }; - $('lset-fr').onclick = function() { - sdb.setItem('kiri-lang', 'fr-fr'); - api.space.reload(); - }; - $('lset-pl').onclick = function() { - sdb.setItem('kiri-lang', 'pl-pl'); - api.space.reload(); - }; - $('lset-pt').onclick = function() { - sdb.setItem('kiri-lang', 'pt-pt'); - api.space.reload(); - }; - $('lset-es').onclick = function() { - sdb.setItem('kiri-lang', 'es-es'); - api.space.reload(); - }; - $('lset-zh').onclick = function() { - sdb.setItem('kiri-lang', 'zh'); - api.space.reload(); - }; + // bind menu language choices + let lang_map = [ + [ 'lset-en', 'en-us' ], + [ 'lset-da', 'da-dk' ], + [ 'lset-de', 'de-de' ], + [ 'lset-fr', 'fr-fr' ], + [ 'lset-pl', 'pl-pl' ], + [ 'lset-pt', 'pt-pt' ], + [ 'lset-es', 'es-es' ], + [ 'lset-zh', 'zh' ], + ] + for (let [ btn, lang ] of lang_map) { + $(btn).onclick = () => { + sdb.setItem('kiri-lang', lang); + api.space.reload(); + }; + } space.event.addHandlers(self, [ 'dragover', fileOps.dragOverHandler, @@ -576,5 +538,3 @@ function init_one() { api.space.restore(resolve) || checkSeed(resolve) || resolve(); }); }; - -export { onBooleanClick, init_one as init_input }; diff --git a/src/kiri/app/init/sync.js b/src/kiri/app/init/sync.js index cae72456..4756fbe6 100644 --- a/src/kiri/app/init/sync.js +++ b/src/kiri/app/init/sync.js @@ -12,21 +12,18 @@ import { space } from '../../../moto/space.js'; import { VIEWS } from '../../core/consts.js'; import * as view_tools from '../tools.js'; +const { SETUP, LOCAL } = api.const; +const { catalog, client, platform, selection, stats, ui } = api; + const DOC = self.document; const WIN = self.window; -const proto = location.protocol; -const { SETUP, LOCAL } = api.const; -const { platform, client, catalog, stats } = api; const LANG = api.language.current; -const ui = api.ui; const STARTMODE = SETUP.sm && SETUP.sm.length === 1 ? SETUP.sm[0] : null; -function settings() { - return api.conf.get(); -} - // SECOND STAGE INIT AFTER UI RESTORED export async function init_sync() { + const proto = location.protocol; + api.event.emit('init.two'); // load script extensions @@ -107,7 +104,7 @@ export async function init_sync() { }); // Setup navigation button bindings - setup_keybd_nav(ui, WIN, LANG); + setup_keybd_nav(); // show topline separator when iframed try { if (WIN.self !== WIN.top) $('top-sep').style.display = 'flex' } catch (e) { console.log(e) } @@ -132,7 +129,7 @@ export async function init_sync() { } function ui_sync() { - const current = settings(); + const current = api.conf.get(); const control = current.controller; if (!control.devel) { @@ -198,9 +195,7 @@ function ui_sync() { client.wasm(control.assembly === true); } -function setup_keybd_nav(ui, WIN, LANG) { - const { selection } = api; - +function setup_keybd_nav() { // bind interface action elements ui.acct.help.onclick = (ev) => { ev.stopPropagation(); api.help.show() }; ui.acct.don8.onclick = (ev) => { ev.stopPropagation(); api.modal.show('don8') }; diff --git a/src/kiri/app/keyboard.js b/src/kiri/app/keyboard.js index 87d54a9a..32b41152 100644 --- a/src/kiri/app/keyboard.js +++ b/src/kiri/app/keyboard.js @@ -62,6 +62,8 @@ class KeyboardControl { this.#bindEvents(); this.#initialized = true; + + return this; } /** diff --git a/src/kiri/app/modal.js b/src/kiri/app/modal.js index 9cf49b7a..6dbf4013 100644 --- a/src/kiri/app/modal.js +++ b/src/kiri/app/modal.js @@ -29,6 +29,8 @@ class ModalControl { this.#onShow = onShow; this.#onHide = onHide; this.#initialized = true; + + return this; } /** diff --git a/src/kiri/app/slider.js b/src/kiri/app/slider.js index 2b7bb974..4a84bedd 100644 --- a/src/kiri/app/slider.js +++ b/src/kiri/app/slider.js @@ -52,6 +52,8 @@ class SliderControl { this.#setupUI(); this.#bindEvents(); this.#initialized = true; + + return this; } /** From 195bbc1964fb6f0482e800b991d3a3d849ae6738 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 25 Dec 2025 12:54:21 -0500 Subject: [PATCH 030/154] remove unused event bindings --- src/kiri/app/init/bind.js | 1 - src/kiri/app/tools.js | 11 ----------- src/kiri/mode/cam/cl-ops.js | 7 ++----- 3 files changed, 2 insertions(+), 17 deletions(-) diff --git a/src/kiri/app/init/bind.js b/src/kiri/app/init/bind.js index 826c5ac6..0debc56e 100644 --- a/src/kiri/app/init/bind.js +++ b/src/kiri/app/init/bind.js @@ -1,7 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { $ } from '../../../moto/webui.js'; -import { api } from '../api.js'; export function bind() { return { diff --git a/src/kiri/app/tools.js b/src/kiri/app/tools.js index ea3cdee4..3ec7bac6 100644 --- a/src/kiri/app/tools.js +++ b/src/kiri/app/tools.js @@ -129,14 +129,6 @@ function startLeftAlign() { startIt(); } -api.event.on('tool.camera.focus', startFocus); - -api.event.on('tool.mesh.face-up', startFaceUp); - -api.event.on('tool.mesh.lay-flat', startLayFlat); - -api.event.on('tool.mesh.lefty', startLeftAlign); - function scale() { let cam = space.internals().camera; let dist = cam.position.distanceTo(pmesh.position); @@ -145,8 +137,6 @@ function scale() { pmesh.scale.set(scale,scale,scale); } -api.event.on("space.view.zoom", scale); - api.event.on('mouse.hover', (ev) => { if (!enabled) { return; @@ -194,7 +184,6 @@ api.event.on('mouse.hover.up', (ev) => { endIt(); }); - export { cleanup, endIt, diff --git a/src/kiri/mode/cam/cl-ops.js b/src/kiri/mode/cam/cl-ops.js index f0550ed0..106efbf9 100644 --- a/src/kiri/mode/cam/cl-ops.js +++ b/src/kiri/mode/cam/cl-ops.js @@ -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 { startFaceUp } from '../../app/tools.js'; const { MODES } = api.const; const { uc: UC } = api; @@ -592,17 +593,13 @@ export function createPopOps() { action: UC.newRow([UC.newButton(LANG.edit, gcodeEditor())], { class: "ext-buttons f-row" }) }; - function angleTowardZUp() { - api.event.emit('tool.mesh.face-up'); - } - createPopOp('index', { degrees: 'camIndexAxis', absolute: 'camIndexAbs' }).inputs = { degrees: UC.newInput(LANG.ci_degr_s, { title: LANG.ci_degr_l, convert: toFloat, bound: UC.bound(-360, 360.0) }), absolute: UC.newBoolean(LANG.ci_abso_s, undefined, { title: LANG.ci_abso_l }), - select: UC.newRow([UC.newButton(LANG.ci_face_s, angleTowardZUp)], { class: "ext-buttons f-row" }) + select: UC.newRow([UC.newButton(LANG.ci_face_s, startFaceUp)], { class: "ext-buttons f-row" }) }; const editEnable = gcodeEditor('Laser Enable Script', 'enable'); From 40f992f12b6dcba1b5bd19de2351cf642df2c95c Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 25 Dec 2025 13:05:51 -0500 Subject: [PATCH 031/154] rename settings ops --- src/kiri/app/conf/settings.js | 59 +++++++++++++++-------------------- src/kiri/app/init/input.js | 12 +++---- src/kiri/app/init/sync.js | 10 +++--- 3 files changed, 36 insertions(+), 45 deletions(-) diff --git a/src/kiri/app/conf/settings.js b/src/kiri/app/conf/settings.js index 627b5e79..97e13395 100644 --- a/src/kiri/app/conf/settings.js +++ b/src/kiri/app/conf/settings.js @@ -4,11 +4,7 @@ import { $ } from '../../../moto/webui.js'; import { api } from '../api.js'; import { settings as set_ctrl } from './manager.js'; -function settings() { - return api.conf.get(); -} - -function deviceExport(exp, name) { +function export_device(exp, name) { name = (name || "device") .toLowerCase() .replace(/ /g,'_') @@ -20,7 +16,7 @@ function deviceExport(exp, name) { }); } -function objectsExport(format = "stl") { +function export_objects(format = "stl") { // return selection.export(); api.uc.confirm("Export Filename", {ok:true, cancel: false}, `selected.${format}`).then(name => { if (!name) return; @@ -31,18 +27,7 @@ function objectsExport(format = "stl") { }); } -function workspaceNew() { - api.uc.confirm("Clear Workspace?", {ok:true, cancel: false}).then(value => { - if (value === true) { - let proc = set_ctrl.proc(); - proc.ops && (proc.ops.length = 0); - proc.op2 && (proc.op2.length = 0); - api.platform.clear(); - } - }); -} - -function profileExport() { +function export_profile() { const opt = {pre: [ "
", "

Workspace Export

", @@ -73,7 +58,18 @@ function profileExport() { }); } -function settingsSave(ev, name) { +function new_workspace() { + api.uc.confirm("Clear Workspace?", {ok:true, cancel: false}).then(value => { + if (value === true) { + let proc = set_ctrl.proc(); + proc.ops && (proc.ops.length = 0); + proc.op2 && (proc.op2.length = 0); + api.platform.clear(); + } + }); +} + +function settings_save(ev, name) { if (ev) { ev.stopPropagation(); ev.preventDefault(); @@ -81,7 +77,7 @@ function settingsSave(ev, name) { api.dialog.hide(); let mode = api.mode.get(), - s = settings(), + s = api.conf.get(), def = "default", cp = s.process, pl = s.sproc[mode], @@ -112,27 +108,22 @@ function settingsSave(ev, name) { } } -function settingsLoad() { +function settings_load() { api.conf.show(); } -function updateDeviceSize() { +function update_platform_size() { api.conf.update(); api.platform.update_size(); api.platform.update_origin(); } -async function sync_put() { - await set_ctrl.sync.put(); -} - export const settingsOps = { - deviceExport, - objectsExport, - workspaceNew, - profileExport, - settingsSave, - settingsLoad, - updateDeviceSize, - sync_put + export_device, + export_objects, + export_profile, + new_workspace, + settings_save, + settings_load, + update_device: update_platform_size, }; diff --git a/src/kiri/app/init/input.js b/src/kiri/app/init/input.js index ef054233..c1e761aa 100644 --- a/src/kiri/app/init/input.js +++ b/src/kiri/app/init/input.js @@ -203,7 +203,7 @@ export function init_input() { preferences.updateDrawer(); // api augmentation with local functions - api.device.export = settingsOps.deviceExport; + api.device.export = settingsOps.export_device; Object.assign(ui, { tracker: tracker, @@ -218,9 +218,9 @@ export function init_input() { device: newGroup(LANG.dv_gr_dev, null, {group:"ddev", inline, class:"noshow"}), _____: newGroup("workspace", null, {group:"dext", inline}), - bedWidth: newInput('X (width)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:settingsOps.updateDeviceSize}), - bedDepth: newInput('Y (depth)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:settingsOps.updateDeviceSize}), - maxHeight: newInput('Z (height)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:settingsOps.updateDeviceSize}), + bedWidth: newInput('X (width)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:settingsOps.update_device}), + bedDepth: newInput('Y (depth)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:settingsOps.update_device}), + maxHeight: newInput('Z (height)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:settingsOps.update_device}), resolutionX: newInput(LANG.dv_rezx_s, {title:LANG.dv_rezx_l, convert:toInt, size:6, modes:SLA}), resolutionY: newInput(LANG.dv_rezy_s, {title:LANG.dv_rezy_l, convert:toInt, size:6, modes:SLA}), _____: newDiv({ class: "f-col t-body t-inset", addto: $('dev-config'), set:true, modes:NO_WEDM }), @@ -342,7 +342,7 @@ export function init_input() { // override old style settings two-button menu ui.settingsSave.onclick = () => { - settingsOps.settingsSave(undefined, ui.settingsName.value); + settingsOps.settings_save(undefined, ui.settingsName.value); }; // initialize and expose modal to API @@ -391,7 +391,7 @@ export function init_input() { DOC, WIN, rotateInputSelection: input_rotate, - settingsLoad: settingsOps.settingsLoad + settingsLoad: settingsOps.settings_load }); // add mobile class to body if needed diff --git a/src/kiri/app/init/sync.js b/src/kiri/app/init/sync.js index 4756fbe6..75f3b554 100644 --- a/src/kiri/app/init/sync.js +++ b/src/kiri/app/init/sync.js @@ -200,7 +200,7 @@ function setup_keybd_nav() { ui.acct.help.onclick = (ev) => { ev.stopPropagation(); api.help.show() }; ui.acct.don8.onclick = (ev) => { ev.stopPropagation(); api.modal.show('don8') }; ui.acct.mesh.onclick = (ev) => { ev.stopPropagation(); WIN.location = "/mesh" }; - ui.acct.export.onclick = (ev) => { ev.stopPropagation(); settingsOps.profileExport() }; + ui.acct.export.onclick = (ev) => { ev.stopPropagation(); settingsOps.export_profile() }; ui.acct.export.title = LANG.acct_xpo; ui.func.slice.onclick = (ev) => { ev.stopPropagation(); api.function.slice() }; ui.func.preview.onclick = (ev) => { ev.stopPropagation(); api.function.print() }; @@ -211,7 +211,7 @@ function setup_keybd_nav() { $('export-support-a').onclick = (ev) => { ev.stopPropagation(); api.modal.show('don8') }; $('mode-device').onclick = api.show.devices; - $('mode-profile').onclick = settingsOps.settingsLoad; + $('mode-profile').onclick = settingsOps.settings_load; $('mode-fdm').onclick = () => api.mode.set('FDM'); $('mode-cam').onclick = () => api.mode.set('CAM'); $('mode-sla').onclick = () => api.mode.set('SLA'); @@ -223,7 +223,7 @@ function setup_keybd_nav() { $('set-profs').onclick = (ev) => { ev.stopPropagation(); api.conf.show() }; $('set-tools').onclick = (ev) => { ev.stopPropagation(); api.show.tools() }; $('set-prefs').onclick = (ev) => { ev.stopPropagation(); api.modal.show('prefs') }; - $('file-new').onclick = (ev) => { ev.stopPropagation(); settingsOps.workspaceNew() }; + $('file-new').onclick = (ev) => { ev.stopPropagation(); settingsOps.new_workspace() }; $('file-recent').onclick = () => { api.modal.show('files') }; $('file-import').onclick = (ev) => { api.event.import(ev); }; $('view-arrange').onclick = api.platform.layout; @@ -259,8 +259,8 @@ function setup_keybd_nav() { $('render-ghost').onclick = () => { api.view.wireframe(false, 0, api.view.is_arrange() ? 0.4 : 0.25); }; $('render-wire').onclick = () => { api.view.wireframe(true, 0, api.space.is_dark() ? 0.25 : 0.5); }; $('render-solid').onclick = () => { api.view.wireframe(false, 0, 1); }; - $('mesh-export-stl').onclick = () => { settingsOps.objectsExport('stl') }; - $('mesh-export-obj').onclick = () => { settingsOps.objectsExport('obj') }; + $('mesh-export-stl').onclick = () => { settingsOps.export_objects('stl') }; + $('mesh-export-obj').onclick = () => { settingsOps.export_objects('obj') }; $('mesh-merge').onclick = selection.merge; $('mesh-split').onclick = selection.isolateBodies; $('context-duplicate').onclick = selection.duplicate; From dbde5969035c838b3fa2d43a90344de1e07ef333 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 25 Dec 2025 14:03:37 -0500 Subject: [PATCH 032/154] push to get friendly jsdoc for future devs --- src/kiri/app/alerts.js | 20 ++++ src/kiri/app/api.js | 38 ++++++- src/kiri/app/devices.js | 24 ++++- src/kiri/app/do.js | 25 +++++ src/kiri/app/export.js | 60 +++++++++++ src/kiri/app/file-ops.js | 30 ++++++ src/kiri/app/files.js | 46 +++++++++ src/kiri/app/frame.js | 28 ++++- src/kiri/app/function.js | 54 ++++++++++ src/kiri/app/groups.js | 7 ++ src/kiri/app/help.js | 8 ++ src/kiri/app/image.js | 21 +++- src/kiri/app/inputs.js | 154 ++++++++++++++++++++++++++- src/kiri/app/keyboard.js | 34 ++++++ src/kiri/app/language.js | 24 +++++ src/kiri/app/layers.js | 98 ++++++++++++++++-- src/kiri/app/mode.js | 37 +++++++ src/kiri/app/mouse.js | 28 +++++ src/kiri/app/platform.js | 218 ++++++++++++++++++++++++++++++++++++++- src/kiri/app/selected.js | 120 +++++++++++++++++++++ 20 files changed, 1054 insertions(+), 20 deletions(-) diff --git a/src/kiri/app/alerts.js b/src/kiri/app/alerts.js index 5cf3c46d..a54f1dac 100644 --- a/src/kiri/app/alerts.js +++ b/src/kiri/app/alerts.js @@ -2,8 +2,18 @@ import { api } from './api.js'; +/** + * Active alert records. Each record is an array: [message, timestamp, duration, active] + * @type {Array<[string, number, number, boolean]>} + */ let alerts = []; +/** + * Display an alert message to the user + * @param {string} message - The message to display + * @param {number} time - Duration in seconds to show the alert + * @returns {Array|undefined} Alert record [message, timestamp, duration, active] or result of update() + */ function show(message, time) { if (message === undefined || message === null) { return update(true); @@ -19,6 +29,11 @@ function show(message, time) { return rec; } +/** + * Hide one or more alerts + * @param {Array|Array} rec - Single alert record or array of records to hide + * @param {Array} [recs] - Optional array of alert records (deprecated parameter style) + */ function hide(rec, recs) { if (Array.isArray(recs)) { for (let r of recs) { @@ -36,6 +51,11 @@ function hide(rec, recs) { } } +/** + * Update the alert display by filtering expired/inactive alerts and rendering active ones. + * Filters alerts by age (based on duration) and active flag, limits display to 5 alerts. + * @param {boolean} [clear] - If true, clears all alerts before updating + */ function update(clear) { if (clear) { alerts = []; diff --git a/src/kiri/app/api.js b/src/kiri/app/api.js index 4a2db7dd..48d11c2e 100644 --- a/src/kiri/app/api.js +++ b/src/kiri/app/api.js @@ -53,14 +53,19 @@ let LOC = self.location, self.kiri_catalog = FILES; FILES.show = () => modal.show('files'); -// Broker compatibility patch +/** + * Broker compatibility patch - adds 'on' method as alias for subscribe + */ EVENT.on = (topic, listener) => { EVENT.subscribe(topic, listener); return EVENT; }; +/** Busy state counter for tracking active operations */ let busyVal = 0, + /** Hover feature flag */ isHover = false, + /** Explicit undefined for object defaults */ undef = undefined; // the big kahuna @@ -68,6 +73,10 @@ export const api = { ajax, beta, alerts, + /** + * Busy state management for tracking active operations. + * Emits 'busy' events when state changes. + */ busy: { val() { return busyVal }, inc() { api.event.emit("busy", ++busyVal) }, @@ -84,10 +93,19 @@ export const api = { color, conf: settings.conf, const: { LANG, LOCAL, SETUP, SECURE, SPACE, STACKS, ...consts }, + /** + * Development/debug utilities + */ devel: { get enabled() { return settings.ctrl().devel; }, + /** + * Enable X-ray view of specific layers for debugging slicing. + * Converts layer indices to Z-heights and triggers re-slice. + * @param {number|number[]} layers - Layer index or array of indices to view + * @param {boolean} [raw] - If true, use raw values without height calculation + */ xray(layers, raw) { let proc = api.conf.get().process, size = proc.sliceHeight || proc.slaSlice || 1, @@ -118,6 +136,10 @@ export const api = { settings: settingsUI.trigger_event }, electron: navigator.userAgent.includes('Electron'), + /** + * Feature flags and hook functions for customizing application behavior. + * Many of these are hook points for external integrations or plugins. + */ feature: { seed: true, // seed profiles on first use meta: true, // show selected widget metadata @@ -136,7 +158,9 @@ export const api = { on_mouse_down: undef, // function intercepts mouse down work_alerts: true, // allow disabling work progress alerts pmode: consts.PMODES.SPEED, // preview modes - // hover: false, // when true fires mouse hover events + /** + * Hover feature flag. Setting this publishes a "feature.hover" event. + */ get hover() { return isHover; }, @@ -202,7 +226,17 @@ export const api = { download: utilModule.download, ui2rec() { api.conf.update_from(...arguments) }, rec2ui() { api.conf.update_fields(...arguments) }, + /** + * Encode object to base64 string via JSON serialization + * @param {*} obj - Object to encode + * @returns {string} Base64 encoded string + */ b64enc(obj) { return base64js.fromByteArray(new TextEncoder().encode(JSON.stringify(obj))) }, + /** + * Decode base64 string to object via JSON parsing + * @param {string} obj - Base64 encoded string + * @returns {*} Decoded object + */ b64dec(obj) { return JSON.parse(new TextDecoder().decode(base64js.toByteArray(obj))) } }, // var: { diff --git a/src/kiri/app/devices.js b/src/kiri/app/devices.js index c7c7e1cf..77b8f1e7 100644 --- a/src/kiri/app/devices.js +++ b/src/kiri/app/devices.js @@ -92,7 +92,11 @@ function selectDevice(devicename) { } } -// only for local filters +/** + * Clone the current device to create a customizable local copy. + * Naming logic: if device already has "My" prefix, appends " copy", otherwise adds "My" prefix. + * Only works for local devices (not stock devices). + */ function cloneDevice() { let name = `${getSelectedDevice().replace(/\./g,' ')}`; let code = api.clone(setconf.get().device); @@ -113,6 +117,18 @@ function updateLaserState() { $('laser-off').style.display = dev.useLaser ? 'flex' : 'none'; } +/** + * Set device configuration from code and initialize device state. + * Complex initialization that: + * - Parses device code (string or object) + * - Fills missing device fields with defaults + * - Handles first-time device setup with profiles + * - Updates UI elements and platform configuration + * - Manages device/process associations + * - Emits device.select and device.selected events + * @param {string|object} code - Device configuration code or object + * @param {string} devicename - Name of the device being set + */ function setDeviceCode(code, devicename) { api.event.emit('device.select', devicename); try { @@ -277,6 +293,12 @@ function setDeviceCode(code, devicename) { api.event.settings(); } +/** + * Render device selection UI with both stock and custom devices. + * Builds device list dropdown, sets up event handlers for save/add/delete/rename/export, + * and separates local "My Devices" from "Stock Devices" in the UI. + * @param {string[]} devices - Array of stock device names for current mode + */ function renderDevices(devices) { let selected = api.device.get() || devices[0], features = api.feature, diff --git a/src/kiri/app/do.js b/src/kiri/app/do.js index b3b998c6..644a1586 100644 --- a/src/kiri/app/do.js +++ b/src/kiri/app/do.js @@ -5,9 +5,19 @@ import { space } from '../../moto/space.js'; const { event } = api; +/** + * Undo/Redo action stack. Each entry contains {undo, redo} action records. + * @type {Array<{undo: object, redo: object}>} + */ let stack = []; + +/** Current position in the undo/redo stack */ let stpos = 0; + +/** Accumulator for drag movements - tracks cumulative x,y offset during selection drag */ let moved = { x: 0, y: 0 }; + +/** Message ID for the current undo/redo alert message */ let msgid; event.on("init-done", () => { @@ -31,6 +41,9 @@ let redo = api.doit.redo = function() { } }; +/** + * Clear the entire undo/redo stack and reset moved accumulator + */ let clear = api.doit.clear = function() { stack = []; stpos = 0; @@ -52,6 +65,13 @@ function message(txt) { msgid = api.show.alert(txt); } +/** + * Execute an action record (undo or redo). + * Dispatches based on action type: move, rotate, or scale. + * @param {object} rec - Action record with type and action-specific properties + * @param {string} rec.type - Action type: 'move', 'rotate', or 'scale' + * @param {Array} rec.widgets - Widgets to operate on + */ function action(rec) { switch (rec.type) { case 'move': @@ -75,6 +95,11 @@ function action(rec) { updateButtons(); } +/** + * Push an undo/redo action pair onto the stack. + * Truncates stack at current position (clearing any "future" when adding new action). + * @param {object} ur - Action pair with {undo, redo} records + */ function pushActions(ur) { stack.length = stpos++; stack.push(ur); diff --git a/src/kiri/app/export.js b/src/kiri/app/export.js index 53439bff..a44b7f0d 100644 --- a/src/kiri/app/export.js +++ b/src/kiri/app/export.js @@ -10,6 +10,10 @@ import { LASER as laser_driver } from '../mode/laser/driver.js'; import { SLA as sla_client } from '../mode/sla/init-ui.js'; import { hash } from '../../ext/md5.js'; +/** + * Sequential print counter for generating unique export filenames. + * Increments with each export and persists in local storage. + */ let printSeq = parseInt(local['kiri-print-seq'] || local['print-seq'] || "0") + 1; function localGet(key) { @@ -20,6 +24,12 @@ function localSet(key, val) { return api.local.set(key, val); } +/** + * Main export entry point. Dispatches to mode-specific export handlers. + * Extracts widget filenames to use for export filename suggestions. + * @param {object} options - Export options (mode-specific) + * @returns {*} Result from mode-specific export handler + */ export function exportFile(options) { let mode = api.mode.get(); let names = api.widgets.all().map(w => w.meta ? w.meta.file : undefined) @@ -39,6 +49,13 @@ export function exportFile(options) { } } +/** + * Export gcode for FDM or CAM modes. + * Calls worker to generate gcode, then presents dialog or invokes callback. + * @param {function} callback - Optional callback(gcode_string, output_info) + * @param {string} mode - Current mode ('FDM' or 'CAM') + * @param {string[]} names - Widget filenames for export naming + */ function callExport(callback, mode, names) { let alert = api.feature.work_alerts ? api.show.alert("Exporting") : null; let gcode = []; @@ -65,6 +82,12 @@ function callExport(callback, mode, names) { }); } +/** + * Export laser/waterjet/drag knife toolpaths. + * Worker generates output, then presents export dialog with SVG/DXF/STL/GCode options. + * @param {object} options - Export options + * @param {string[]} names - Widget filenames for export naming + */ function callExportLaser(options, names) { client.export(api.conf.get(), (line) => { // engine export uses lines @@ -79,6 +102,12 @@ function callExportLaser(options, names) { }); } +/** + * Export SLA print data. + * Worker generates layer images, then delegates to SLA client for download. + * @param {object} options - Export options + * @param {string[]} names - Widget filenames for export naming + */ function callExportSLA(options, names) { client.export(api.conf.get(), (line) => { api.show.progress(line.progress, "exporting"); @@ -92,6 +121,12 @@ function callExportSLA(options, names) { }); } +/** + * Present laser export dialog with format options (SVG, DXF, STL, GCode). + * Sets up UI handlers for downloading in each supported format. + * @param {Array} data - Layer data from export worker + * @param {string[]} names - Widget filenames for naming suggestion + */ function exportLaserDialog(data, names) { localSet('kiri-print-seq', printSeq++); @@ -147,6 +182,11 @@ function exportLaserDialog(data, names) { $('print-lg').onclick = download_gcode; } +/** + * Bind an input field to persist its value to local storage on blur. + * @param {string} field - Element ID of the input field + * @param {string} varname - Local storage key to save value under + */ function bindField(field, varname) { $(field).onblur = function() { console.log('save', field, 'to', varname); @@ -154,6 +194,19 @@ function bindField(field, varname) { }; } +/** + * Present gcode export dialog with download/send options. + * Complex UI setup that handles: + * - Local download of gcode, zip (CAM operations), or 3MF (FDM) + * - OctoPrint integration for remote printing + * - Bambu printer integration (via api.bambu) + * - Print statistics (time, filament, weight) + * - Gcode preview + * @param {string[]} gcode - Array of gcode lines + * @param {object} [sections] - CAM operation sections for zip export + * @param {object} info - Export metadata (time, distance, bytes, etc.) + * @param {string[]} names - Widget filenames for naming suggestion + */ function exportGCodeDialog(gcode, sections, info, names) { localSet('kiri-print-seq', printSeq++); @@ -336,6 +389,13 @@ function exportGCodeDialog(gcode, sections, info, names) { // let bnds = settings.bounds; // console.log({ wids, bnds }); + /** + * Generate 3MF file for Bambu/BambuStudio printers. + * Creates ZIP archive with gcode, thumbnails, and metadata. + * @param {function} then - Callback to receive generated 3MF blob + * @param {string} [ptype='unknown'] - Printer type identifier + * @param {number[]} [ams=[0]] - AMS (filament) slot assignments + */ function gen3mf(then, ptype = 'unknown', ams = [0]) { let now = new Date(); let ymd = [ diff --git a/src/kiri/app/file-ops.js b/src/kiri/app/file-ops.js index 5fec8d55..e0cd1054 100644 --- a/src/kiri/app/file-ops.js +++ b/src/kiri/app/file-ops.js @@ -5,8 +5,16 @@ import { base } from '../../geo/base.js'; import { space } from '../../moto/space.js'; const DOC = self.document; + +/** Cached platform color before drag-over highlight */ let platformColor; +/** + * Handle file drag-over event. + * Changes platform color to green to indicate drop target. + * Prevents drop when modal dialogs are open. + * @param {DragEvent} evt - Browser drag event + */ function dragOverHandler(evt) { evt.stopPropagation(); evt.preventDefault(); @@ -21,10 +29,22 @@ function dragOverHandler(evt) { if (oldcolor !== 0x00ff00) platformColor = oldcolor; } +/** + * Handle drag-leave event by restoring original platform color. + */ function dragLeave() { space.platform.setColor(platformColor); } +/** + * Handle file drop event. + * Restores platform color and loads dropped files. + * Handles multi-file drops with optional grouping: + * - Single file: loads immediately + * - Multiple files: prompts user whether to group + * - api.feature.drop_group can override behavior (true=always group, false=never group) + * @param {DragEvent} evt - Browser drop event + */ function dropHandler(evt) { evt.stopPropagation(); evt.preventDefault(); @@ -54,6 +74,10 @@ function dropHandler(evt) { } } +/** + * Load a file from the catalog when clicked. + * @param {Event} e - Click event from catalog item + */ function loadCatalogFile(e) { api.widgets.load(e.target.getAttribute('load'), function(widget) { api.platform.add(widget); @@ -61,6 +85,12 @@ function loadCatalogFile(e) { }); } +/** + * Update the catalog UI with current file list. + * Builds interactive list with rename, load, and delete buttons for each file. + * Sorts files alphabetically (case-insensitive). + * @param {object} files - Dictionary of filename -> {vertices, updated} file metadata + */ function updateCatalog(files) { let table = api.ui.catalog.list, list = []; diff --git a/src/kiri/app/files.js b/src/kiri/app/files.js index 0067915e..978e5886 100644 --- a/src/kiri/app/files.js +++ b/src/kiri/app/files.js @@ -1,6 +1,15 @@ /** Copyright Stewart Allen -- All Rights Reserved */ +/** + * Manages file storage in IndexedDB for 3D model vertices. + * Supports file operations (put, get, delete, rename), listener notifications, + * and deferred loading for externally-stored files. + */ class Files { + /** + * Create a new Files store + * @param {object} indexdb - IndexedDB wrapper instance + */ constructor(indexdb) { let store = this; this.db = indexdb; @@ -47,10 +56,21 @@ class Files { this.listeners.remove(listener); }; + /** + * Set handler for deferred file loading. + * Deferred files are not stored locally but fetched on-demand. + * @param {function} handler - Function(mark, name, callback) to load deferred file + */ setDeferredHandler(handler) { this.deferredHandler = handler; }; + /** + * Register a deferred file placeholder. + * File data is not stored locally; the mark is used by the deferred handler to fetch it later. + * @param {string} name - Filename + * @param {*} mark - Identifier for deferred handler to fetch file (e.g., URL, storage key) + */ putDeferred(name, mark) { // triggers refresh callback this.files[name] = { @@ -91,6 +111,13 @@ class Files { .catch(ondone); }; + /** + * Rename a file in the catalog. + * Copies vertex data to new key, updates file list, and removes old key. + * @param {string} name - Current filename + * @param {string} newname - New filename + * @param {function} callback - Function({error}) called when complete + */ rename(name, newname, callback) { if (!this.files[name]) return callback({error: 'no such file'}); if (!newname || newname == name) return callback({error: 'invalid new name'}); @@ -146,6 +173,12 @@ class Files { if (callback) callback(false); }; + /** + * Delete files matching a filter function. + * @param {function} fn - Filter function(key) returning true to delete + * @param {*} [from] - Optional start key for range query + * @param {*} [to] - Optional end key for range query + */ deleteFilter(fn, from, to) { this.db.keys(keys => { for (let key of keys) { @@ -157,17 +190,30 @@ class Files { } } +/** + * Save file list to IndexedDB and notify listeners. + * @param {Files} store - Files instance + */ function saveFileList(store) { store.db.put('files', store.files); notifyFileListeners(store); } +/** + * Notify all registered listeners of file list changes. + * @param {Files} store - Files instance + */ function notifyFileListeners(store) { for (let i=0; i -- All Rights Reserved */ +/** + * Frame Message API for iframe integration. + * + * Enables parent windows to control embedded Kiri:Moto instances via postMessage. + * Controlled by api.feature.frame flag. + * + * Supported message types: + * - mode: Set operating mode (FDM, CAM, SLA, LASER, etc.) + * - view: Set view mode (ARRANGE, SLICE, PREVIEW, etc.) + * - function: Call api.function methods (slice, print, export, etc.) + * - event: Subscribe to events + * - emit: Emit events + * - get: Query state (mode, device, process, widgets) + * - set: Update state + * - features: Update feature flags + * - device/process/controller: Update settings + * - parse: Parse and load file data (STL, OBJ, 3MF, SVG) + * - load: Load URL + * - clear: Clear platform + * - alert: Show alert + * - progress: Update progress bar + */ + import { api } from './api.js'; import { load } from '../../load/file.js'; import { newWidget } from '../core/widget.js'; import { VIEWS } from '../core/consts.js'; import { widgets } from '../core/widgets.js'; -// add frame message api listener +/** + * Frame message API listener. + * Handles postMessage communication between parent window and embedded Kiri:Moto. + */ window.addEventListener('message', msg => { const { conf, event, feature, platform, settings, show } = api; diff --git a/src/kiri/app/function.js b/src/kiri/app/function.js index 3751504c..d7939c9d 100644 --- a/src/kiri/app/function.js +++ b/src/kiri/app/function.js @@ -7,8 +7,26 @@ import { space } from '../../moto/space.js'; import { COLOR, PMODES } from '../core/consts.js'; import { exportFile } from './export.js'; +/** + * Tracks completion state of operations to prevent redundant work. + * Properties: slice, preview, export + */ let complete = {}; +/** + * Prepare and execute slicing for all widgets on the platform. + * Main slicing function that: + * - Takes screenshots for export/preview + * - Handles belt mode layout + * - Slices each widget sequentially via worker + * - Tracks progress across all widgets + * - Renders sliced layers to stacks + * - Emits slice.begin, slice, slice.end, slice.error events + * + * @param {function} [callback] - Called when slicing completes + * @param {number} [scale=1] - Progress bar scale factor (for chaining operations) + * @param {number} [offset=0] - Progress bar offset (for chaining operations) + */ function prepareSlices(callback, scale = 1, offset = 0) { const { conf, event, feature, hide, mode, view, platform, show, stacks } = api; @@ -289,6 +307,17 @@ function prepareSlices(callback, scale = 1, offset = 0) { sliceNext(); } +/** + * Prepare preview/print visualization. + * Generates toolpaths and renders them as 3D lines. + * Handles multiple preview modes (speed, filament, layer) via feature.pmode. + * Auto-runs slicing first if not already complete. + * Emits preview.begin, print, preview.end, preview.error events. + * + * @param {function} [callback] - Called when preview completes + * @param {number} [scale=1] - Progress bar scale factor (for chaining operations) + * @param {number} [offset=0] - Progress bar offset (for chaining operations) + */ function preparePreview(callback, scale = 1, offset = 0) { const { conf, event, feature, hide, mode, view, platform, show, stacks } = api; const widgets = api.widgets.all(); @@ -427,6 +456,10 @@ function preparePreview(callback, scale = 1, offset = 0) { }); } +/** + * Prepare animation (requires SharedArrayBuffer support). + * Checks for browser support and emits function.animate event. + */ function prepareAnimation() { if (!window.SharedArrayBuffer) { api.alerts.show("The security context of this"); @@ -437,6 +470,12 @@ function prepareAnimation() { api.event.emit("function.animate", {mode: api.conf.get().mode}); } +/** + * Prepare and trigger export. + * Auto-runs preview first if not already complete. + * Delegates to exportFile() for mode-specific export. + * @param {...*} args - Arguments passed through to exportFile() + */ function prepareExport() { const settings = api.conf.get(); const argsave = arguments; @@ -449,12 +488,23 @@ function prepareExport() { exportFile(...argsave); } +/** + * Cancel running worker operation by restarting the worker. + */ function cancelWorker() { if (client.isBusy()) { client.restart(); } } +/** + * Parse and visualize gcode or other toolpath code. + * Sends code to worker for parsing, renders result as 3D preview. + * Emits code.load and code.loaded events. + * + * @param {string} code - Gcode or toolpath text + * @param {string} type - Code type identifier (gcode, etc.) + */ function parseCode(code, type) { const { conf, event, show, stacks, widgets, view } = api; const settings = conf.get(); @@ -479,6 +529,10 @@ function parseCode(code, type) { }); } +/** + * Clear operation completion tracking. + * Resets complete state so operations can run again. + */ function clear_progress() { complete = {}; } diff --git a/src/kiri/app/groups.js b/src/kiri/app/groups.js index a0ab659e..92434c69 100644 --- a/src/kiri/app/groups.js +++ b/src/kiri/app/groups.js @@ -3,10 +3,17 @@ import { selection } from './selected.js'; import { Widget } from '../core/widget.js'; +/** + * Merge selected widgets into a group. + * Grouped widgets move together as a unit. + */ function groupMerge() { Widget.Groups.merge(selection.widgets(true)); } +/** + * Split grouped widgets back into individual widgets. + */ function groupSplit() { Widget.Groups.split(selection.widgets(false)); } diff --git a/src/kiri/app/help.js b/src/kiri/app/help.js index aa01c6a0..aec228f7 100644 --- a/src/kiri/app/help.js +++ b/src/kiri/app/help.js @@ -7,10 +7,18 @@ import { version } from '../../moto/license.js'; const WIN = self.window; +/** + * Show local help dialog. + */ function showHelp() { showHelpFile(`local`,() => {}); } +/** + * Show help dialog or open external docs. + * @param {string} local - If truthy, shows local help modal; otherwise opens docs.grid.space + * @param {function} then - Callback after help shown + */ function showHelpFile(local,then) { if (!local) { WIN.open("//docs.grid.space/", "_help"); diff --git a/src/kiri/app/image.js b/src/kiri/app/image.js index 6b21f4fb..2867436d 100644 --- a/src/kiri/app/image.js +++ b/src/kiri/app/image.js @@ -6,6 +6,14 @@ import { newWidget } from '../core/widget.js'; import { platform } from './platform.js'; import { settings } from './conf/manager.js'; +/** + * Show image import dialog with conversion options. + * Prompts for blur, inversion, base size, and border settings. + * Large images (>2.5MB) show a warning before proceeding. + * @param {ArrayBuffer} image - PNG image data + * @param {string} name - Filename + * @param {boolean} [force] - Skip size warning if true + */ function loadImageDialog(image, name, force) { if (!force && image.byteLength > 2500000) { return api.uc.confirm("Large images may fail to import
Consider resizing under 1000 x 1000
Proceed with import?").then(ok => { @@ -46,6 +54,12 @@ function loadImageDialog(image, name, force) { }); } +/** + * Convert PNG image to 3D mesh using worker. + * Creates height map from pixel brightness and generates vertices. + * @param {ArrayBuffer} image - PNG image data + * @param {object} [opt={}] - Options: file, blur, base, border, inv_image, inv_alpha + */ function loadImage(image, opt = {}) { const info = Object.assign({settings: settings.get(), png:image}, opt); api.client.image2mesh(info, progress => { @@ -58,7 +72,12 @@ function loadImage(image, opt = {}) { }); } -// convert any image type to png +/** + * Convert any image format to PNG before loading. + * Uses canvas to convert image blob to PNG data URL. + * @param {ArrayBuffer} res - Image data in any format + * @param {string} name - Filename + */ function loadImageConvert(res, name) { let url = URL.createObjectURL(new Blob([res])); diff --git a/src/kiri/app/inputs.js b/src/kiri/app/inputs.js index d4edf9e2..9525762b 100644 --- a/src/kiri/app/inputs.js +++ b/src/kiri/app/inputs.js @@ -1,31 +1,62 @@ /** Copyright Stewart Allen -- All Rights Reserved */ +/** + * UI component factory system. + * Creates form elements, dialogs, and manages UI state/visibility. + * Supports mode-specific visibility, unit conversion, and hierarchical grouping. + */ + import { $ } from '../../moto/webui.js'; import { api } from './api.js'; let DOC = self.document, + /** Callback for input changes */ inputAction = null, + /** Previous addTo value for nesting */ lastAddTo = null, + /** Current group array */ lastGroup = null, + /** Last created div container */ lastDiv = null, + /** Current container to add elements to */ addTo = null, + /** Alternative binding target for input actions */ bindTo = null, + /** Map of group name to array of elements */ groups = {}, + /** Sticky state prevents auto-hide on blur */ groupSticky = false, + /** Current group name */ groupName = undefined, - heads = {}, // hideable group heads (clickable label) - hidden = {}, // hidden groups (by name) + /** Collapsible group headers (clickable label) */ + heads = {}, + /** Hidden groups by name */ + hidden = {}, + /** Elements with mode visibility rules */ hasModes = [], + /** Elements with unit conversion setters */ setters = [], + /** Last mode set for visibility filtering */ lastMode = null, + /** Last expert mode state */ lastExpert = true, + /** Prefix for element IDs */ prefix = "tab", + /** Unit scale multiplier for conversions */ units = 1, + /** Last changed input element */ lastChange = null, + /** Last clicked button */ lastBtn = null, + /** Last clicked text element */ lastTxt = null, + /** Last shown popup */ lastPop = null; +/** + * UI component factory and utilities. + * Provides chainable builder pattern and component creation functions. + */ export const UI = { prefix: function(pre) { prefix = pre; return UI }, inputAction: function(fn) { inputAction = fn; return UI }, @@ -84,6 +115,10 @@ export const UI = { } }; +/** + * Set hidden group map and refresh visibility. + * @param {object} map - Map of group name to hidden boolean + */ function setHidden(map) { hidden = map; refresh(); @@ -92,6 +127,11 @@ function setHidden(map) { } } +/** + * Attach blur event listener to element(s). + * @param {HTMLElement|Array} obj - Element or array of elements + * @param {function} fn - Blur handler function + */ function onBlur(obj, fn) { if (Array.isArray(obj)) { for (let o of obj) onBlur(o, fn); @@ -100,14 +140,34 @@ function onBlur(obj, fn) { obj.addEventListener('blur', fn); } +/** + * Show alert dialog with OK button. + * @param {string} message - Alert message + * @returns {Promise} Resolves when OK clicked + */ function alert(message) { return confirm(message, {ok:true}); } +/** + * Show prompt dialog with text input. + * @param {string} message - Prompt message + * @param {string} value - Default input value + * @returns {Promise} Resolves with entered text or undefined if cancelled + */ function prompt(message, value) { return confirm(message, {ok:true, cancel:undefined}, value); } +/** + * Show modal dialog with custom buttons and optional input. + * Blocks keyboard events while open. + * @param {string} message - Dialog message + * @param {object} buttons - Button labels mapped to return values (e.g., {yes: true, no: false}) + * @param {string|Array} [input] - Optional input field. Array creates textarea with lines. + * @param {object} [opt={}] - Options: {pre, post} for additional HTML content + * @returns {Promise} Resolves with button value or input value if provided + */ function confirm(message, buttons, input, opt = {}) { return new Promise((resolve, reject) => { let { feature } = api; @@ -307,6 +367,12 @@ function addCollapsableElement(parent, options = {}) { return row; } +/** + * Create a value bounding function. + * @param {number} low - Minimum value + * @param {number} high - Maximum value + * @returns {function} Function that clamps value to [low, high] range + */ function bound(low,high) { return function(v) { if (isNaN(v)) return low; @@ -314,6 +380,11 @@ function bound(low,high) { }; } +/** + * Convert input value to integer. + * Bound to input element as `this`. Applies bounds and unit conversion if configured. + * @returns {number} Integer value + */ function toInt() { let nv = this.value !== '' ? parseInt(this.value) : null; if (isNaN(nv)) nv = 0; @@ -325,6 +396,11 @@ function toInt() { return nv; } +/** + * Convert input value to float. + * Bound to input element as `this`. Applies bounds and unit conversion if configured. + * @returns {number} Float value + */ function toFloat() { let nv = this.value !== '' ? parseFloat(this.value) : null; if (nv !== null && this.bound) nv = this.bound(nv); @@ -335,6 +411,11 @@ function toFloat() { return nv; } +/** + * Convert comma-separated input to float array. + * Bound to input element as `this`. Applies bounds to each value. + * @returns {Array} Float array + */ function toFloatArray() { let nv = this.value !== '' ? this.value.split(',').map(v => parseFloat(v)) : null; console.log({ toFloatArray: nv }); @@ -343,6 +424,11 @@ function toFloatArray() { return nv; } +/** + * Convert input value to degrees float (0-360). + * Bound to input element as `this`. Normalizes to 0-359.99 range. + * @returns {number} Degrees float + */ function toDegsFloat(){ let nv = this.value !== '' ? parseFloat(this.value) : null; if (nv !== null && this.bound) nv = this.bound(nv); @@ -375,6 +461,12 @@ function newLabel(text, opt = {}) { return label; } +/** + * Create a read-only value display element. + * @param {number} [size=6] - Input size attribute + * @param {object} [opt={}] - Options: {class} + * @returns {HTMLInputElement} Read-only input element + */ function newValue(size = 6, opt = {}) { let value = DOC.createElement('input'); value.setAttribute("size", size); @@ -452,6 +544,13 @@ function newDiv(opt = {}) { return div; } +/** + * Create a collapsible details/summary element. + * Changes addTo context to the details element until endExpand() called. + * @param {string} label - Summary label text + * @param {object} [opt={}] - Options: {class, open, modes, show, etc.} + * @returns {HTMLElement} Details element with collapse() method + */ function newExpand(label, opt = {}) { let div = DOC.createElement('details'); div.setAttribute('class', opt.class || 'f-col'); @@ -474,6 +573,10 @@ function newExpand(label, opt = {}) { return div; } +/** + * End expand context, restoring previous addTo. + * @returns {HTMLElement} Restored addTo element + */ function endExpand() { addTo = lastAddTo; return addTo; @@ -612,6 +715,15 @@ function newText(label, options) { return txt; } +/** + * Create a labeled input field with automatic validation and unit conversion. + * Supports text input or textarea (if height > 1). + * Filters non-numeric keys unless opt.text=true. + * Triggers action on blur. + * @param {string} label - Input label text + * @param {object} [opt={}] - Options: {size, height, action, convert, bound, units, round, text, comma, disabled, title, id, trigger, hide} + * @returns {HTMLInputElement|HTMLTextAreaElement} Input element with setVisible() method + */ function newInput(label, opt = {}) { let row = newDiv(opt), hide = opt.hide, @@ -706,6 +818,12 @@ function addUnits(input, round) { return input; } +/** + * Create a labeled range slider. + * @param {string} label - Slider label text + * @param {object} options - Options: {min, max, hide, title, action, modes, etc.} + * @returns {HTMLInputElement} Range input with setVisible() method + */ function newRange(label, options) { let row = newDiv(options), ip = DOC.createElement('input'), @@ -732,6 +850,14 @@ function newRange(label, options) { return ip; } +/** + * Create a labeled select dropdown. + * Triggers action on change. + * @param {string} label - Select label text + * @param {object} [options={}] - Options: {hide, id, convert, disabled, title, action, trigger, post, modes, etc.} + * @param {Array|string} source - Option source array or source attribute value + * @returns {HTMLSelectElement} Select element with setVisible() method + */ function newSelect(label, options = {}, source) { let row = newDiv(options), ip = DOC.createElement('select'), @@ -774,6 +900,14 @@ function newSelect(label, options = {}, source) { return ip; } +/** + * Create a labeled checkbox. + * Triggers action on click. + * @param {string} label - Checkbox label text + * @param {function} [action=bindTo] - Click handler function + * @param {object} [opt={}] - Options: {hide, disabled, title, trigger, modes, etc.} + * @returns {HTMLInputElement} Checkbox element with setVisible() method + */ function newBoolean(label, action = bindTo, opt = {}) { let row = newDiv(opt), ip = DOC.createElement('input'), @@ -828,7 +962,15 @@ function newBlank(options) { return row; } -// unlike other elements, does not auto-add to a row +/** + * Create a button element. + * Unlike other elements, does not auto-add to a row. + * Action can be a function or event name string. + * @param {string} label - Button label text + * @param {function|string} action - Click handler or event name to emit + * @param {object} [opt={}] - Options: {class, icon, title, id, modes, etc.} + * @returns {HTMLButtonElement} Button element with mode visibility + */ function newButton(label, action, opt = {}) { let b = DOC.createElement('button'); @@ -866,6 +1008,12 @@ function newButton(label, action, opt = {}) { return b; } +/** + * Create a row container with child elements. + * @param {Array} children - Child elements to append + * @param {object} options - Options: {class, noadd, modes, etc.} + * @returns {HTMLElement} Row element + */ function newRow(children, options) { let row = addCollapsableElement((options && options.noadd) ? null : addTo); if (children) children.forEach(function (c) { row.appendChild(c) }); diff --git a/src/kiri/app/keyboard.js b/src/kiri/app/keyboard.js index 32b41152..13b399db 100644 --- a/src/kiri/app/keyboard.js +++ b/src/kiri/app/keyboard.js @@ -84,6 +84,10 @@ class KeyboardControl { return c.charCodeAt(0); } + /** + * Bind keyboard event handlers to space.event + * @private + */ #bindEvents() { this.#space.event.addHandlers(self, [ 'keyup', this.#handleKeyUp.bind(this), @@ -92,6 +96,13 @@ class KeyboardControl { ]); } + /** + * Handle keyup events. + * Primarily handles Escape key for dismissing modals/dialogs/selections. + * @private + * @param {KeyboardEvent} evt - Keyboard event + * @returns {boolean} False to prevent default + */ #handleKeyUp(evt) { // Allow feature hooks to intercept if (this.#api.feature.on_key) { @@ -121,6 +132,14 @@ class KeyboardControl { return false; } + /** + * Handle keydown events. + * Handles arrow keys for movement/rotation, delete key, and Cmd/Ctrl shortcuts. + * Arrow keys: rotate (default) or move (with Alt). Shift = smaller rotation. + * Meta + Up/Down: navigate layers. + * @private + * @param {KeyboardEvent} evt - Keyboard event + */ #handleKeyDown(evt) { if (this.#api.modal.visible()) { return false; @@ -205,6 +224,21 @@ class KeyboardControl { } } + /** + * Handle keypress events. + * Main keyboard shortcuts for application commands: + * - Numbers 0-9: show slice layers (0=all, 1-9=10-90%) + * - s/S: slice, p/P: prepare, x/X: export + * - a: arrange/layout, v: toggle single slice view + * - d: duplicate, m: mirror + * - i: import, r: recent files + * - e: devices, o: tools, l: load settings + * - Z: clear all settings, C: refresh catalog + * - Ctrl+g: group, Ctrl+u: ungroup + * @private + * @param {KeyboardEvent} evt - Keyboard event + * @returns {boolean} False to prevent default + */ #handleKeyPress(evt) { let handled = true; if (this.#api.modal.visible() || this.inputHasFocus()) { diff --git a/src/kiri/app/language.js b/src/kiri/app/language.js index 187f590b..7e955f30 100644 --- a/src/kiri/app/language.js +++ b/src/kiri/app/language.js @@ -1,10 +1,23 @@ /** Copyright Stewart Allen -- All Rights Reserved */ +/** + * Global language/translation system. + * Stores language maps and tracks the current language selection. + */ const LANG = self.lang = { current: {} }; + +/** Default language code */ const KDFL = 'en-us'; +/** Currently selected language code from browser */ let lset = navigator.language.toLocaleLowerCase(); +/** + * Map language codes to supported language identifiers. + * Normalizes short codes (en, fr, de) to full language codes (en-us, fr-fr, de-de). + * @param {string} key - Language code (e.g., 'en', 'fr', 'de-de') + * @returns {string} Normalized language code or default 'en-us' + */ LANG.map = function(key) { if (!key) { return KDFL; @@ -23,10 +36,21 @@ LANG.map = function(key) { return KDFL; }; +/** + * Get current browser language setting. + * @returns {string} Browser language code + */ LANG.get = function() { return lset; }; +/** + * Set language by trying a list of language codes in order. + * Falls back to browser language, then English if no arguments provided. + * Populates LANG.current with translations, filling missing keys from English. + * @param {...string} keys - Language codes to try in order + * @returns {string|undefined} Selected language code or undefined if none found + */ LANG.set = function() { let map, key, keys = [...arguments]; // provide default if none given diff --git a/src/kiri/app/layers.js b/src/kiri/app/layers.js index 6a585825..efbaf061 100644 --- a/src/kiri/app/layers.js +++ b/src/kiri/app/layers.js @@ -3,11 +3,19 @@ import { newPolygon, Polygon } from '../../geo/polygon.js'; import { polygons as POLY } from '../../geo/polygons.js'; +/** + * Layer management system for 3D visualization. + * Organizes geometry (lines, polygons, faces, paths) by layer with color/opacity. + * Supports multiple rendering styles: basic lines, webgl lines, filled areas, 3D extrusion paths. + */ export class Layers { constructor() { this.init(); } + /** + * Initialize or reset layers, profiles, and statistics. + */ init() { this.layers = {}; this.profiles = {}; @@ -20,21 +28,47 @@ export class Layers { }; } + /** + * Get layer data by layer index. + * @param {number} layer - Layer index + * @returns {object} Layer data or undefined + */ getLayer(layer) { return this.layers[layer]; } - // in radians + /** + * Set rotation for subsequent layers. + * @param {number} [x=0] - X rotation in radians + * @param {number} [y=0] - Y rotation in radians + * @param {number} [z=0] - Z rotation in radians + * @returns {Layers} This instance for chaining + */ setRotation(x = 0, y = 0, z = 0) { this.rotation = { x, y, z }; return this; } + /** + * Set position offset for subsequent layers. + * @param {number} [x=0] - X position + * @param {number} [y=0] - Y position + * @param {number} [z=0] - Z position + * @returns {Layers} This instance for chaining + */ setPosition(x = 0, y = 0, z = 0) { this.position = { x, y, z }; return this; } + /** + * Set current layer for subsequent geometry additions. + * Creates layer if it doesn't exist. + * @param {number} layer - Layer index + * @param {number|object} colors - Single color number or {line, face, opacity} + * @param {boolean} [off] - If true, marks layer as off/hidden + * @returns {Layers} This instance for chaining + */ setLayer(layer, colors, off) { let layers = this.layers; if (typeof(colors) === 'number') { @@ -65,13 +99,24 @@ export class Layers { return this; } - // add a line segment (two points) + /** + * Add a single line segment to current layer. + * @param {object} p1 - First point {x, y, z} + * @param {object} p2 - Second point {x, y, z} + * @returns {Layers} This instance for chaining + */ addLine(p1, p2) { this.current.lines.push(p1, p2); return this; } - // add an array of line segments + /** + * Add multiple line segments to current layer. + * If options provided, converts to open polygons via addPolys. + * @param {Array} lines - Array of points (alternating pairs for line segments) + * @param {object} [options] - If provided, creates open polygons instead + * @returns {Layers} This instance for chaining + */ addLines(lines, options) { if (options) { // the open option encodes lines as open polygons @@ -92,12 +137,23 @@ export class Layers { return this; } - // an open or closed polygon + /** + * Add a single polygon to current layer. + * @param {Polygon} poly - Polygon to add + * @param {object} [options] - Rendering options + * @returns {Layers} This instance for chaining + */ addPoly(poly, options) { return this.addPolys([ poly ], options); } - // a polygon rendered as a webgl line + /** + * Add polygons rendered as WebGL lines. + * Dispatches to addFlats or addPaths based on options. + * @param {Array} polys - Array of polygons + * @param {object} [options] - Options: {clean, flat, thin, z, color} + * @returns {Layers} This instance for chaining + */ addPolys(polys, options) { if (!polys) { return this; @@ -134,8 +190,13 @@ export class Layers { return this; } - // add an enclosed 3D polygon earcut into faces - // used for FDM solids, bridges, flats debug, CAM hole generation & SLA slice visualization + /** + * Add enclosed 3D polygon earcut into triangular faces. + * Used for FDM solids, bridges, flats debug, CAM hole generation, SLA slice visualization. + * @param {Polygon|Array} polys - Polygon(s) to tessellate + * @param {object} [options] - Options: {outline} to also draw polygon outline + * @returns {Layers} This instance for chaining + */ addAreas(polys, options) { const faces = this.current.faces; polys = Array.isArray(polys) ? polys : [ polys ]; @@ -151,7 +212,13 @@ export class Layers { } } - // add a 2D polyline path (usually FDM extrusion paths) + /** + * Add 2D polyline paths with width (usually FDM extrusion paths). + * Creates flat ribbons with vertex normals for lighting. + * @param {Array} polys - Polygons to render as flat ribbons + * @param {object} [options] - Options: {offset, outline, color} + * @returns {Layers} This instance for chaining + */ addFlats(polys, options) { const opts = options || {}; const offset = opts.offset || 1; @@ -200,7 +267,14 @@ export class Layers { return this; } - // add 3D polyline path (FDM extrusion paths) + /** + * Add 3D polyline paths with extrusion (FDM extrusion paths). + * Creates 3D tubes with vertex normals for lighting. + * Supports incremental merging and per-segment color changes. + * @param {Array} polys - Polygons to render as 3D tubes + * @param {object} [options] - Options: {height, offset, z, color} + * @returns {Layers} This instance for chaining + */ addPaths(polys, options) { const opts = options || {}; const height = opts.height || 1; @@ -259,6 +333,12 @@ export class Layers { } } +/** + * Flatten polygon array or single polygon for rendering. + * Clones and flattens nested polygon structures. + * @param {Polygon|Array} polys - Polygon(s) to flatten + * @returns {Array} Flattened polygon array + */ function flat(polys) { if (Array.isArray(polys)) { return POLY.flatten(polys.clone(true), [], true); diff --git a/src/kiri/app/mode.js b/src/kiri/app/mode.js index dcd15f2a..40b4c394 100644 --- a/src/kiri/app/mode.js +++ b/src/kiri/app/mode.js @@ -11,20 +11,49 @@ import { settings } from './conf/manager.js'; const { MODES, VIEWS } = consts; const clone = Object.clone; +/** Current operating mode ID */ let MODE = MODES.FDM; +/** + * Get current mode name as string. + * @returns {string} Mode name (FDM, CAM, SLA, LASER, etc.) + */ function getMode() { return settings.mode(); } +/** + * Get current mode name in lowercase. + * @returns {string} Lowercase mode name (fdm, cam, sla, etc.) + */ function getModeLower() { return getMode().toLowerCase(); } +/** + * Switch to a different operating mode. + * Convenience wrapper around setMode() that updates platform size after switch. + * @param {string} mode - Target mode name (FDM, CAM, SLA, LASER, DRAG, WJET, WEDM) + */ function switchMode(mode) { setMode(mode, null, platform.update_size); } +/** + * Set operating mode with full initialization. + * Complex function that: + * - Validates and sets mode constant + * - Updates UI to show/hide mode-specific controls + * - Restores cached device for the mode + * - Resets view to ARRANGE + * - Saves settings + * - Updates platform and selection + * - Emits mode.set event + * + * @param {string} mode - Target mode name (FDM, CAM, SLA, LASER, DRAG, WJET, WEDM) + * @param {*} lock - Currently unused parameter + * @param {function} [then] - Optional callback after mode set complete + */ function setMode(mode, lock, then) { if (!MODES[mode]) { console.log("invalid mode: "+mode); @@ -75,10 +104,18 @@ function setMode(mode, lock, then) { } } +/** + * Get the name of the current process profile for this mode. + * @returns {string} Process profile name + */ function currentProcessName() { return settings.get().cproc[getMode()]; } +/** + * Get the process configuration object for the current mode and profile. + * @returns {object} Process configuration + */ function currentProcessCode() { return settings.get().sproc[getMode()][currentProcessName()]; } diff --git a/src/kiri/app/mouse.js b/src/kiri/app/mouse.js index 59a2348f..d4807d6c 100644 --- a/src/kiri/app/mouse.js +++ b/src/kiri/app/mouse.js @@ -61,6 +61,11 @@ class InteractionControl { this.#initialized = true; } + /** + * Bind hover event handlers for mouse and platform interactions. + * Listens to "feature.hover" event to enable/disable hover functionality. + * @private + */ #bindHoverHandlers() { // Set up hover handlers this.#api.event.on("feature.hover", enable => { @@ -69,17 +74,40 @@ class InteractionControl { }); } + /** + * Handle mouse hover over widgets. + * Returns widget meshes if no intersection, otherwise triggers hover callback. + * @private + * @param {object} int - Intersection data + * @param {Event} event - Mouse event + * @param {Array} ints - All intersections + */ #mouseOnHover(int, event, ints) { if (!this.#api.feature.hover) return; if (!int) return this.#api.feature.hovers || this.#api.widgets.meshes(); this.#onHover?.({int, ints, event, point: int.point, type: 'widget'}); } + /** + * Handle mouse hover over platform. + * Triggers hover callback with platform intersection point. + * @private + * @param {object} int - Intersection point + * @param {Event} event - Mouse event + */ #platformOnHover(int, event) { if (!this.#api.feature.hover) return; if (int) this.#onHover?.({point: int, event, type: 'platform'}); } + /** + * Bind mouse click and drag handlers. + * Handles: + * - Mouse down: lay-flat (Ctrl/Cmd+click), custom hooks, hover mode + * - Mouse up: widget selection/deselection, custom hooks + * - Drag: move selected widgets with boundary checking + * @private + */ #bindMouseHandlers() { // Mouse down handler this.#space.mouse.downSelect((int, event) => { diff --git a/src/kiri/app/platform.js b/src/kiri/app/platform.js index e4db4401..701c67d6 100644 --- a/src/kiri/app/platform.js +++ b/src/kiri/app/platform.js @@ -13,18 +13,42 @@ import { Widget, newWidget } from '../core/widget.js'; const V0 = new THREE.Vector3(0,0,0); +/** Bounds update callback */ let setbounds = undefined; + +/** Flag indicating if grouping operation is in progress */ let grouping = false; + +/** Maximum Z height of all widgets on platform */ let topZ = 0; +/** + * Get current settings object. + * @returns {object} Current configuration + */ function current() { return api.conf.get(); } +/** + * Get current mode ID constant. + * @returns {number} Mode ID + */ function get_mode() { return api.mode.get_id(); } +/** + * Update platform origin position and rulers. + * Calculates origin based on mode, device, and process settings: + * - FDM: Corner or center based on device.originCenter/bedRound + * - CAM: Relative to stock, with optional top origin + * - 2D modes (LASER, DRAG, WJET, WEDM): Corner, center, or bounds-based + * - Belt: Special Y offset handling + * Applies mode-specific origin offsets and updates visual rulers. + * + * @param {boolean} [update_bounds=true] - Whether to recalculate bounds first + */ function update_origin(update_bounds = true) { if (update_bounds) { platform.update_bounds(); @@ -105,6 +129,13 @@ function update_origin(update_bounds = true) { } } +/** + * Update platform size and visual appearance. + * Sets platform dimensions from device settings, updates grid/rulers, + * and applies dark mode styling if enabled. + * + * @param {boolean} [updateDark=true] - Whether to update dark mode styling + */ function update_size(updateDark = true) { const { process, device, controller } = current(); const { showRulers, units } = controller; @@ -145,6 +176,10 @@ function update_size(updateDark = true) { platform.update_origin(); } +/** + * Update topZ to track maximum Z height of all widgets. + * Used for belt mode and other Z-dependent calculations. + */ function platformUpdateMidZ() { topZ = 0; api.widgets.each(widget => { @@ -153,6 +188,11 @@ function platformUpdateMidZ() { space.platform.setMaxZ(topZ); } +/** + * Update widget Z positioning based on CAM anchoring settings. + * In CAM mode, anchors widgets to top/middle/bottom of stock with offset. + * In other modes, resets topZ to 0. + */ function update_top_z() { const { process, stock } = current(); const MODE = get_mode(); @@ -182,6 +222,13 @@ function update_top_z() { }); } +/** + * Calculate and update stock dimensions for CAM mode. + * Stock dimensions can be absolute or relative (offset from bounds). + * Falls back to offset mode if any stock dimension is 0. + * Calculates stock center point for origin calculations. + * In non-CAM modes, clears stock object. + */ function platformUpdateStock() { const settings = current(); const { bounds, process, mode } = settings; @@ -208,11 +255,22 @@ function platformUpdateStock() { } } +/** + * Set explicit platform bounds and trigger update. + * @param {THREE.Box3} bounds - Bounding box to set + * @returns {THREE.Box3} Updated bounds + */ function set_bounds(bounds) { setbounds = bounds; return update_bounds(); } +/** + * Calculate platform bounds from all widgets or use explicit bounds. + * Unions all widget bounding boxes translated by their positions. + * Updates stock, top Z, midZ, and origin after calculation. + * @returns {THREE.Box3} Calculated or explicit bounds + */ function update_bounds() { const bounds = setbounds || new THREE.Box3(); if (!setbounds) @@ -236,10 +294,19 @@ function update_bounds() { return bounds; } +/** + * Get count of selected widgets in arrange view. + * @returns {number} Selection count (0 if not in arrange view) + */ function selected_count() { return api.view.is_arrange() ? api.selection.count() : 0; } +/** + * Update visual appearance of selected widgets. + * Sets selected color and highlights extruder buttons for FDM multi-extruder devices. + * Saves widget state after selection. + */ function update_selected() { const settings = current(); @@ -266,6 +333,15 @@ function update_selected() { } } +/** + * Select a widget on the platform. + * Only works in arrange view. Handles group selection recursively. + * With shift key, toggles selection. Without shift, replaces selection. + * Emits 'widget.select' event and updates UI. + * @param {Widget} widget - Widget to select + * @param {boolean} shift - Whether shift key is pressed (multi-select) + * @param {boolean} [recurse=true] - Whether to recursively select group members + */ function select(widget, shift, recurse = true) { const { event, selection, view } = api; @@ -307,6 +383,14 @@ function select(widget, shift, recurse = true) { space.update(); } +/** + * Deselect widget(s) on the platform. + * Only works in arrange view. Handles group deselection recursively. + * If no widget provided, deselects all widgets. + * Emits 'widget.deselect' event and updates UI. + * @param {Widget} [widget] - Widget to deselect, or undefined to deselect all + * @param {boolean} [recurse=true] - Whether to recursively deselect group members + */ function deselect(widget, recurse = true) { const { selection, view } = api; @@ -342,6 +426,12 @@ function deselect(widget, recurse = true) { space.update(); } +/** + * Load mesh from URL (.stl or raw vertex data). + * Detects file type and delegates to load_stl or ajax loader. + * @param {string} url - URL to load from + * @param {Function} [onload] - Callback(vertices, widget) on load complete + */ function load(url, onload) { if (url.toLowerCase().indexOf(".stl") > 0) { platform.load_stl(url, onload); @@ -356,6 +446,14 @@ function load(url, onload) { } } +/** + * Load STL file from URL and add to platform. + * @param {string} url - URL to STL file + * @param {Function} [onload] - Callback(vertices, widget) on load complete + * @param {FormData} [formdata] - Optional form data for POST request + * @param {boolean} [credentials] - Include credentials in request + * @param {object} [headers] - Additional HTTP headers + */ function load_stl(url, onload, formdata, credentials, headers) { new file_load.STL().load(url, (vertices, filename) => { if (vertices) { @@ -369,6 +467,13 @@ function load_stl(url, onload, formdata, credentials, headers) { }, formdata, 1 / api.view.unit_scale(), credentials, headers); } +/** + * Load mesh(es) from URL via file loader. + * Supports multiple file formats. Groups loading to prevent intermediate layouts. + * Emits 'load.url' event with loaded widgets. + * @param {string} url - URL to load from + * @param {object} [options={}] - Load options including optional group array + */ function load_url(url, options = {}) { platform.group(); file_load.URL.load(url, options).then(objects => { @@ -390,11 +495,20 @@ function load_url(url, options = {}) { }); } +/** + * Begin group loading mode. + * Defers layout and group position updates until group_done() is called. + */ function group() { grouping = true; } -// called after all new widgets are loaded to update group positions +/** + * Complete group loading and finalize widget positions. + * Called after all widgets in a group are loaded. + * Triggers layout if drop_layout feature is enabled. + * @param {boolean} [skipLayout] - Whether to skip auto-layout + */ function group_done(skipLayout) { grouping = false; Widget.Groups.loadDone(); @@ -403,9 +517,23 @@ function group_done(skipLayout) { } } +/** Deferred widget additions pending batch processing */ let deferred = []; + +/** Timeout handle for deferred widget batch processing */ let deferTimeout; +/** + * Add widget to platform and 3D scene. + * Can defer addition for batch processing to improve performance. + * Initializes widget annotation (extruder assignment). + * Updates bounds, triggers save, and positions widget if autoLayout is disabled. + * Emits 'widget.add' event. + * @param {Widget} widget - Widget to add + * @param {boolean} shift - Whether to multi-select (shift key pressed) + * @param {boolean} nolayout - Skip layout/positioning + * @param {boolean} defer - Batch multiple adds for better performance + */ function add(widget, shift, nolayout, defer) { api.widgets.add(widget); space.world.add(widget.mesh); @@ -433,6 +561,12 @@ function add(widget, shift, nolayout, defer) { } } +/** + * Process batch of deferred widget additions. + * Called after 150ms timeout to batch multiple rapid additions. + * Positions widgets if autoLayout is disabled, then completes grouping. + * @private + */ function platformAddDeferred() { let skiplayout = false; for (let rec of deferred) { @@ -453,6 +587,14 @@ function platformAddDeferred() { deferred = []; } +/** + * Find non-colliding position for newly added widget. + * Uses spiral search pattern radiating from center (0,0). + * Tests 360 positions at each radius (10, 20, 30...200mm). + * Skips if this is the first widget on platform. + * @param {Widget} widget - Widget to position + * @private + */ function positionNewWidget(widget) { if (api.widgets.count() <= 1) { return; @@ -501,6 +643,15 @@ function positionNewWidget(widget) { } } +/** + * Delete widget(s) from platform. + * Handles single widget, array of widgets, or widget records. + * Removes from API widget collection, selection, groups, and 3D scene. + * Can defer post-processing for batch deletions. + * Emits 'widget.delete' event. + * @param {Widget|Array} widget - Widget(s) to delete + * @param {boolean} [defer] - Skip post-processing (for batch operations) + */ function platformDelete(widget, defer) { if (!widget) { return; @@ -525,6 +676,11 @@ function platformDelete(widget, defer) { } } +/** + * Post-deletion cleanup and updates. + * Updates slider, bounds, layout, selection, triggers save. + * @private + */ function delete_post() { api.view.update_slider_max(); platform.update_bounds(); @@ -540,7 +696,11 @@ function delete_post() { changed(); } -// render list of current widgets +/** + * Render widget list UI with action buttons. + * Creates interactive widget cards with save/rename/replace/disable/delete buttons. + * Highlights widgets on hover. Updates when widgets are added/removed/modified. + */ function changed() { h.bind($('ws-widgets'), api.widgets.all().map(w => { let color; @@ -627,12 +787,36 @@ function changed() { })); } +/** + * Select all widgets on platform. + * Adds each widget to selection without recursing into groups. + */ function select_all() { api.widgets.each(widget => { platform.select(widget, true, false) }); } +/** + * Arrange widgets on platform using automatic layout. + * Behavior varies by mode: + * - FDM: 2D bin packing with support spacing. Belt mode uses linear Y layout with optional X randomization. + * - SLA: 2D bin packing with support spacing + * - CAM/LASER: 2D bin packing with configurable tile spacing + * + * Belt mode special handling: + * - Positions widgets linearly along Y axis + * - Adds belt lead spacing + * - Optional X randomization for better adhesion + * - Auto-expands bed depth if needed + * + * Standard mode: + * - Uses 2D bin packing algorithm (Packer class) + * - Grows packing area by 10% if widgets don't fit + * - Centers packed layout on platform + * + * Emits 'platform.layout' event when complete. + */ function layout() { const MODE = get_mode(); const settings = current(); @@ -736,6 +920,14 @@ function layout() { api.event.emit('platform.layout'); } +/** + * Create widget from vertex data and add to platform. + * Optionally saves to catalog and adds to group. + * @param {Array} [group] - Optional group array for grouping multiple widgets + * @param {Float32Array|Array} vertices - Vertex data (converted to Float32Array if needed) + * @param {string} [filename] - Optional filename for metadata and catalog + * @returns {Widget} Created widget + */ function load_verts(group, vertices, filename) { const widget = newWidget(undefined, group).loadVertices(vertices.toFloat32(), true); widget.meta.file = filename; @@ -744,6 +936,15 @@ function load_verts(group, vertices, filename) { return widget; } +/** + * Load multiple files from File objects. + * Supports: STL, OBJ, 3MF, SVG, PNG, JPG, KMZ, Gerber (.gbr), gcode, raw vertex data. + * Also handles settings import (.b64, .km, .ini files). + * Groups loading to prevent intermediate layouts. + * Prompts user for grouping when multiple objects detected. + * @param {Array} files - Array of File objects from file input or drag/drop + * @param {Array} [group] - Optional group array for grouping loaded widgets + */ function load_files(files, group) { platform.group(); let loading = files.length; @@ -868,6 +1069,12 @@ function load_files(files, group) { } } +/** + * Show dialog to configure SVG import settings. + * Prompts for extrusion depth, arc resolution, DPI, and nesting. + * @param {Function} doit - Callback with options: {soup, resolution, segmin, depth, dpi} + * @private + */ function loadSVGDialog(doit) { const opt = {pre: [ "
", @@ -895,7 +1102,12 @@ function loadSVGDialog(doit) { }); } -// resize platform (expand) to fit widgets (belt mode) +/** + * Expand platform bed depth to fit widgets (belt mode only). + * Finds maximum Y dimension of all widgets and expands bed if needed. + * Adds 10mm padding. Saves original bed depth. + * @returns {boolean} True if bed was expanded + */ function fit() { let maxy = 0; api.widgets.each(widget => { diff --git a/src/kiri/app/selected.js b/src/kiri/app/selected.js index be494f20..b78548f0 100644 --- a/src/kiri/app/selected.js +++ b/src/kiri/app/selected.js @@ -7,12 +7,21 @@ import { tool as MeshTool } from '../../mesh/tool.js'; import { encode as objEncode } from '../../load/obj.js'; import { encode as stlEncode } from '../../load/stl.js'; +/** + * Array of currently selected widget meshes. + * @type {Array} + */ const selectedMeshes = []; function updateTool(ev) { api.tool.update(ev); } +/** + * Execute function for each unique group represented in selection. + * Passes the first widget from each group to the function. + * @param {function} fn - Function(widget) to execute for each group + */ function for_groups(fn) { let groups = widgets(true).map(w => w.group).uniq(); for (let group of groups) { @@ -20,6 +29,12 @@ function for_groups(fn) { } } +/** + * Execute function for each selected widget. + * If nothing selected, defaults to all widgets unless noauto=true. + * @param {function} fn - Function(widget) to execute + * @param {boolean} [noauto] - If true, don't auto-select all widgets when none selected + */ function for_widgets(fn, noauto) { let m = selectedMeshes; let w = api.widgets.all(); @@ -29,16 +44,32 @@ function for_widgets(fn, noauto) { m.slice().forEach(mesh => { fn(mesh.widget) }); } +/** + * Execute function for all widgets with selection status. + * @param {function} fn - Function(widget, is_selected) to execute + */ function for_status(fn) { api.widgets.all().forEach(w => { fn(w, selectedMeshes.contains(w.mesh)); }); } +/** + * Execute function for each selected mesh. + * @param {function} fn - Function(mesh) to execute + */ function for_meshes(fn) { selectedMeshes.slice().forEach(mesh => { fn(mesh) }); } +/** + * Move selected widget groups. + * Only works in ARRANGE view. + * @param {number} x - X offset + * @param {number} y - Y offset + * @param {number} z - Z offset + * @param {boolean} [abs] - If true, use absolute positioning instead of relative + */ function move(x, y, z, abs) { if (!api.view.is_arrange()) { return; @@ -51,6 +82,10 @@ function move(x, y, z, abs) { api.space.auto_save(); } +/** + * Compute bounding box of all selected meshes. + * @returns {THREE.Box3} Bounding box + */ function get_bounds() { // Helper to compute bounds of selected meshes const THREE = self.THREE; @@ -65,6 +100,10 @@ function get_bounds() { return bounds; } +/** + * Update UI size/scale info fields based on selected mesh bounds. + * Calculates combined bounding box and displays dimensions/scale in UI. + */ function update_info() { const ui = api.ui; let bounds = new THREE.Box3(), track; @@ -100,6 +139,12 @@ function update_info() { update_bounds(); } +/** + * Update platform bounds and constrain widgets to bed limits. + * For belt mode, fits widgets and updates origin. + * Automatically moves widgets back into bounds if they exceed bed size. + * @param {Array} [widgets] - Widgets to bound. Defaults to selected widgets. + */ function update_bounds(widgets) { const settings = api.conf.get(); // update bounds on selection for drag limiting @@ -149,6 +194,11 @@ function update_bounds(widgets) { settings.bounds_sel = bounds_sel; } +/** + * Duplicate selected widgets with offset. + * Only works in ARRANGE view. + * New widgets offset by bounding box width + 1 in X direction. + */ function duplicate() { if (!api.view.is_arrange()) { return; @@ -167,6 +217,10 @@ function duplicate() { }); } +/** + * Mirror selected widgets along X axis. + * Only works in ARRANGE view. + */ function mirror() { if (!api.view.is_arrange()) { return; @@ -178,6 +232,15 @@ function mirror() { api.space.auto_save(); } +/** + * Scale selected widget groups. + * Only works in ARRANGE view. + * Updates bounds, saves workspace, refreshes info and display. + * @param {number} x - X scale factor + * @param {number} y - Y scale factor + * @param {number} z - Z scale factor + * @param {boolean} [last] - If strictly false, skips auto-save + */ function scale() { if (!api.view.is_arrange()) { return; @@ -196,6 +259,14 @@ function scale() { space.update(); } +/** + * Rotate selected widget groups. + * Only works in ARRANGE view. + * Updates bounds, saves workspace, refreshes info and display. + * @param {number} x - X rotation in radians + * @param {number} y - Y rotation in radians + * @param {number} z - Z rotation in radians + */ function rotate(x, y, z) { if (!api.view.is_arrange()) { return; @@ -209,6 +280,12 @@ function rotate(x, y, z) { space.update(); } +/** + * Merge selected widgets into a single widget. + * Combines all vertex data from selected widgets into one mesh. + * @param {object} options - Options + * @param {boolean} [options.deleteMerged=true] - If true, deletes original widgets after merge + */ function merge({ deleteMerged = true }) { let sel = widgets(); if (sel.length === 0) { @@ -264,6 +341,12 @@ function isolateBodies(){ } } +/** + * Export selected widgets to STL or OBJ format. + * If multiple widgets, applies position offsets in STL format. + * @param {string} [format="stl"] - Export format: "stl" or "obj" + * @returns {ArrayBuffer|string} Encoded file data + */ function exportWidgets(format = "stl") { let sel = widgets(); if (sel.length === 0) { @@ -307,6 +390,11 @@ function exportWidgets(format = "stl") { return stlEncode(recs); } +/** + * Get selected widgets. + * @param {boolean} [orall] - If true and nothing selected, returns all widgets + * @returns {Array} Selected widgets or empty array + */ function widgets(orall) { let sel = selectedMeshes.slice().map(m => m.widget); return sel.length ? sel : orall ? api.widgets.all() : [] @@ -321,6 +409,11 @@ function settings() { return api.conf.get(); } +/** + * Prompt user for rotation angles and rotate selection. + * Shows alert if nothing selected. + * Accepts comma-separated X,Y,Z degrees. + */ function input_rotate() { if (selection.meshes().length === 0) { api.show.alert("select object to rotate"); @@ -336,6 +429,12 @@ function input_rotate() { }); } +/** + * Prompt user for position coordinates and move selection. + * Shows alert if nothing selected. + * Accepts comma-separated X,Y coordinates. + * Adjusts for center vs corner origin modes. + */ function input_position() { if (selection.meshes().length === 0) { api.show.alert("select object to position"); @@ -361,6 +460,14 @@ function input_position() { }); } +/** + * Handle size input change event. + * Calculates scale ratio from size change and applies to selection. + * Respects lock checkboxes for proportional scaling. + * Prevents scales below 0.1. + * @param {Event} e - Input change event + * @param {object} ui - UI elements object with size/lock fields + */ function input_resize(e, ui) { let dv = parseFloat(e.target.value || 1), pv = parseFloat(e.target.was || 1), @@ -390,6 +497,14 @@ function input_resize(e, ui) { ui.size.Z.was = ui.size.Z.value = zv * zr; } +/** + * Handle scale input change event. + * Calculates scale ratio from scale change and applies to selection. + * Respects lock checkboxes for proportional scaling. + * Prevents scales below 0.1. + * @param {Event} e - Input change event + * @param {object} ui - UI elements object with scale/lock fields + */ function input_scale(e, ui) { let dv = parseFloat(e.target.value || 1), pv = parseFloat(e.target.was || 1), @@ -423,6 +538,11 @@ function parse_as_float(e) { e.target.value = parseFloat(e.target.value) || 0; } +/** + * Bind input handlers to UI elements. + * Sets up Enter key handlers for scale, size, tool, and rotation inputs. + * @param {object} ui - UI elements object + */ function input_binding(ui) { // on enter but not on blur space.event.onEnterKey([ From bcd2449b895f8862bd758c1759e2fc37001b91bc Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 25 Dec 2025 16:17:44 -0500 Subject: [PATCH 033/154] fold tiny util into api, clean up modal corner cases, add more jsdoc --- src/kiri/app/alerts.js | 26 +++++++++++- src/kiri/app/api.js | 43 ++++++++++++++----- src/kiri/app/init/sync.js | 8 ++-- src/kiri/app/modal.js | 33 +++++++++++---- src/kiri/app/mode.js | 4 +- src/kiri/app/tools.js | 82 ++++++++++++++++++++++++++++++++++- src/kiri/app/util.js | 52 ----------------------- src/kiri/app/viewmode.js | 40 ++++++++++++++++-- src/kiri/app/visuals.js | 87 +++++++++++++++++++++++++++++++++++--- src/kiri/app/workspace.js | 55 ++++++++++++++++++++++++ src/kiri/mode/cam/slice.js | 1 + 11 files changed, 340 insertions(+), 91 deletions(-) delete mode 100644 src/kiri/app/util.js diff --git a/src/kiri/app/alerts.js b/src/kiri/app/alerts.js index a54f1dac..6fa72f1f 100644 --- a/src/kiri/app/alerts.js +++ b/src/kiri/app/alerts.js @@ -82,14 +82,36 @@ function update(clear) { } } +/** + * Update progress bar display. + * In debug mode, also shows progress status message. + * @param {number} [value=0] - Progress value (0.0 to 1.0) + * @param {string} [msg] - Optional status message to display + */ +function progress(value = 0, msg) { + value = (value * 100).round(4); + api.ui.progress.width = value+'%'; + if (self.debug) { + // console.log(msg, value.round(2)); + api.ui.prostatus.style.display = 'flex'; + if (msg) { + api.ui.prostatus.innerHTML = msg; + } else { + api.ui.prostatus.innerHTML = ''; + } + } +} + export { hide, show, - update + update, + progress }; export default { hide, show, - update + update, + progress }; diff --git a/src/kiri/app/api.js b/src/kiri/app/api.js index 48d11c2e..d87f99d6 100644 --- a/src/kiri/app/api.js +++ b/src/kiri/app/api.js @@ -35,7 +35,6 @@ import { stats } from './stats.js'; import { types as load } from '../../load/file.js'; import { UI } from './inputs.js'; import { updateTool } from '../mode/cam/tools.js'; -import { util as utilModule } from './util.js'; import { view as viewModule } from './viewmode.js'; import { visuals } from './visuals.js'; import { widgets } from '../core/widgets.js'; @@ -43,11 +42,11 @@ import { workspace } from './workspace.js'; // environment setup let LOC = self.location, - SETUP = utils.parseOpt(LOC.search.substring(1)), - SECURE = utilModule.isSecure(LOC.protocol), - LOCAL = self.debug && !SETUP.remote, EVENT = broker, - FILES = openFiles(new Index(SETUP.d ? SETUP.d[0] : 'kiri')); + SETUP = utils.parseOpt(LOC.search.substring(1)), + FILES = openFiles(new Index(SETUP.d ? SETUP.d[0] : 'kiri')), + LOCAL = self.debug && !SETUP.remote, + SECURE = isSecure(LOC.protocol); // todo: fix in widget.js b/c front-end and back-end do not share api self.kiri_catalog = FILES; @@ -61,6 +60,27 @@ EVENT.on = (topic, listener) => { return EVENT; }; +/** + * Check if protocol is secure (HTTPS). + * @param {string} proto - Protocol string (e.g., "https:", "http:") + * @returns {boolean} True if protocol starts with "https" + */ +function isSecure(proto) { + return proto.toLowerCase().indexOf("https") === 0; +} + +/** + * Download data as a file using browser download mechanism. + * Creates temporary object URL and triggers download via hidden link click. + * @param {Blob|ArrayBuffer|string} data - Data to download + * @param {string} filename - Filename for download + */ +function download(data, filename) { + let url = WIN.URL.createObjectURL(new Blob([data], {type: "octet/stream"})); + $('mod-any').innerHTML = `x`; + $('_dexport_').click(); +} + /** Busy state counter for tracking active operations */ let busyVal = 0, /** Hover feature flag */ @@ -68,7 +88,10 @@ let busyVal = 0, /** Explicit undefined for object defaults */ undef = undefined; -// the big kahuna +/** + * organize modules for easy use in other modules + * without having to know the paths to all of them + */ export const api = { ajax, beta, @@ -132,7 +155,7 @@ export const api = { emit(t,m,o) { return EVENT.publish(t,m,o) }, bind(t,m,o) { return EVENT.bind(t,m,o) }, alerts(clr) { alerts.update(clr) }, - import: utilModule.loadFile, + import() { api.ui.load.click() }, settings: settingsUI.trigger_event }, electron: navigator.userAgent.includes('Electron'), @@ -205,7 +228,7 @@ export const api = { import() { api.ui.import.style.display = '' }, layer: visuals.set_visible_layer, local() { console.trace('deprecated') }, - progress: utilModule.setProgress, + progress: alerts.progress, slices: visuals.show_slices, tools: showTools }, @@ -222,8 +245,8 @@ export const api = { uc: UI.prefix('kiri').inputAction(settings.conf.update), ui: {}, util: { - isSecure: utilModule.isSecure, - download: utilModule.download, + isSecure, + download, ui2rec() { api.conf.update_from(...arguments) }, rec2ui() { api.conf.update_fields(...arguments) }, /** diff --git a/src/kiri/app/init/sync.js b/src/kiri/app/init/sync.js index 75f3b554..90a4b842 100644 --- a/src/kiri/app/init/sync.js +++ b/src/kiri/app/init/sync.js @@ -255,10 +255,10 @@ function setup_keybd_nav() { $('rot_z_gt').onclick = () => { selection.rotate(0,0,-d * $('rot_z').value) }; // rendering options - $('render-edges').onclick = () => { api.view.edges({ toggle: true }); api.conf.save() }; - $('render-ghost').onclick = () => { api.view.wireframe(false, 0, api.view.is_arrange() ? 0.4 : 0.25); }; - $('render-wire').onclick = () => { api.view.wireframe(true, 0, api.space.is_dark() ? 0.25 : 0.5); }; - $('render-solid').onclick = () => { api.view.wireframe(false, 0, 1); }; + $('render-edges').onclick = () => { api.view.set_edges({ toggle: true }); api.conf.save() }; + $('render-ghost').onclick = () => { api.view.set_wireframe(false, 0, api.view.is_arrange() ? 0.4 : 0.25); }; + $('render-wire').onclick = () => { api.view.set_wireframe(true, 0, api.space.is_dark() ? 0.25 : 0.5); }; + $('render-solid').onclick = () => { api.view.set_wireframe(false, 0, 1); }; $('mesh-export-stl').onclick = () => { settingsOps.export_objects('stl') }; $('mesh-export-obj').onclick = () => { settingsOps.export_objects('obj') }; $('mesh-merge').onclick = selection.merge; diff --git a/src/kiri/app/modal.js b/src/kiri/app/modal.js index 6dbf4013..2dacd74a 100644 --- a/src/kiri/app/modal.js +++ b/src/kiri/app/modal.js @@ -9,6 +9,7 @@ class ModalControl { #ui; #onShow; #onHide; + #showing; #initialized = false; /** @@ -38,7 +39,15 @@ class ModalControl { * @returns {boolean} */ visible() { - return this.#ui.modal.style.display === 'flex'; + return modal.#ui.modal.style.display === 'flex'; + } + + /** + * Check if a specific modal is currently visible + * @returns {boolean} + */ + is(name) { + return modal.#showing === name; } /** @@ -46,18 +55,21 @@ class ModalControl { * @param {string} which - Name of the modal to show */ show(which) { - let mod = this.#ui.modal, + let mod = modal.#ui.modal, style = mod.style, - visible = this.visible(), + visible = modal.visible(), info = { pct: 0 }; // hide all modals before showing another - Object.keys(this.#ui.modals).forEach(name => { - this.#ui.modals[name].style.display = name === which ? 'flex' : ''; + Object.keys(modal.#ui.modals).forEach(name => { + console.log({ name, which }); + modal.#ui.modals[name].style.display = name === which ? 'flex' : ''; }); + modal.#showing = which; + const ondone = () => { - this.#onShow?.(which); + modal.#onShow?.(which); }; if (visible) { @@ -79,10 +91,13 @@ class ModalControl { * Hide the modal with animation */ hide() { - if (!this.visible()) { + if (!modal.visible()) { return; } - let mod = this.#ui.modal, + + modal.#showing = undefined; + + let mod = modal.#ui.modal, style = mod.style, info = { pct: 100 }; @@ -92,7 +107,7 @@ class ModalControl { onUpdate(() => { style.height = `${info.pct}%` }). onComplete(() => { style.display = ''; - this.#onHide?.(); + modal.#onHide?.(); }). start(); } diff --git a/src/kiri/app/mode.js b/src/kiri/app/mode.js index 40b4c394..22caa361 100644 --- a/src/kiri/app/mode.js +++ b/src/kiri/app/mode.js @@ -93,8 +93,8 @@ function setMode(mode, lock, then) { platform.update_selected(); selection.update_bounds(api.widgets.all()); api.conf.update_fields(); - // because device dialog, if showing, needs to be updated - if (modal.visible()) { + // if device dialog showing, needs to be refreshed + if (modal.is('setup')) { api.show.devices(); } api.space.restore(null, true); diff --git a/src/kiri/app/tools.js b/src/kiri/app/tools.js index 3ec7bac6..07df927f 100644 --- a/src/kiri/app/tools.js +++ b/src/kiri/app/tools.js @@ -1,20 +1,39 @@ /** Copyright Stewart Allen -- All Rights Reserved */ +/** + * Face selection tools for mesh manipulation. + * Provides interactive tools for: + * - Lay flat: Rotate selected face to be parallel with build plate + * - Left align: Rotate to align face normal with -X axis + * - Face up: Select face normal for custom operations + * - Camera focus: Select point for camera focus + * + * Tools use hover mode to display visual indicator on face under cursor. + * User clicks face to apply operation. ESC to cancel. + */ + import { api } from './api.js'; import { THREE } from '../../ext/three.js'; import { space } from '../../moto/space.js'; const { Vector3, Quaternion } = THREE; +/** X-axis unit vector for rotation calculations */ const XAXIS = new THREE.Vector3(1,0,0); + +/** Z-axis unit vector for rotation calculations */ const ZAXIS = new THREE.Vector3(0,0,1); -// circle +/** Circle mesh geometry for face indicator */ let pgeo = new THREE.CircleGeometry(8, 30); + +/** Semi-transparent material for face indicator */ let pmat = new THREE.MeshBasicMaterial({color: 0xddeeff, opacity: 0.6, transparent: true}); + +/** Combined mesh for face indicator (circle + outline) */ let pmesh = new THREE.Mesh(pgeo, pmat); -// circle outline +// Build circle outline from geometry indices let pi = pgeo.index.array; let pp = pgeo.attributes.position.array; let np = []; @@ -36,19 +55,39 @@ let wmat = new THREE.LineBasicMaterial({ color: 0x6699cc }); let wmesh = new THREE.LineSegments(wgeo, wmat); pmesh.add(wmesh); +/** Alert handle for "ESC to end" message */ let alert; + +/** Last object (widget mesh) the indicator was attached to */ let lastobj; + +/** Last face that was hovered over */ let lastface; + +/** Whether a tool is currently active */ let enabled = false; + +/** Name of current operation for display */ let opName = 'lay flat'; + +/** Callback to execute when face is selected */ let onDone = onLayFlatSelect; +/** + * Lay flat operation: Rotate widget so selected face is parallel to build plate. + * Calculates quaternion to align face normal with -Z axis (down). + */ function onLayFlatSelect() { let q = new Quaternion().setFromUnitVectors(lastface.normal, new Vector3(0,0,-1)); api.selection.rotate(q); // endIt(); } +/** + * Left align operation: Rotate widget so selected face normal aligns with -X axis. + * Projects face normal onto XY plane and calculates Z-axis rotation. + * Used for orienting parts for belt printers or side printing. + */ 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(); @@ -67,11 +106,21 @@ function onLeftySelect() { api.selection.rotate(q); } +/** + * Face select operation: Emit face normal for custom handling. + * Emits 'tool.mesh.face-normal' event with selected face normal. + */ function onFaceUpSelect() { api.event.emit('tool.mesh.face-normal', lastface.normal); // endIt(); } +/** + * Start interactive face selection tool. + * Enables hover mode, displays alert, and begins tracking mouse. + * If already enabled, ends the current operation. + * Cannot start if another hover mode is active. + */ function startIt() { if (enabled) { endIt(); @@ -86,6 +135,10 @@ function startIt() { alert = api.show.alert(`[ESC] to end ${opName}`, 600000); } +/** + * End current face selection tool. + * Disables hover mode, hides alert, and cleans up indicator mesh. + */ function endIt() { if (enabled) { api.hide.alert(alert); @@ -96,6 +149,9 @@ function endIt() { } } +/** + * Remove face indicator mesh from scene. + */ function cleanup() { if (lastobj) { lastobj.remove(pmesh); @@ -105,30 +161,52 @@ function cleanup() { api.event.on('key.esc', endIt); +/** + * Start camera focus tool. + * User clicks face to set camera focus point. + * @param {Function} fn - Callback to execute with focus point + */ function startFocus(fn) { opName = 'camera focus'; onDone = fn; startIt(); } +/** + * Start face selection tool. + * User clicks face to select its normal vector. + */ function startFaceUp() { opName = 'face select'; onDone = onFaceUpSelect; startIt(); } +/** + * Start lay flat tool. + * User clicks face to rotate widget so that face is parallel to build plate. + */ function startLayFlat() { opName = 'lay flat'; onDone = onLayFlatSelect; startIt(); } +/** + * Start left align tool. + * User clicks face to align its normal with -X axis. + */ function startLeftAlign() { opName = 'align y axis'; onDone = onLeftySelect; startIt(); } +/** + * Scale face indicator based on camera distance. + * For camera focus, scales inversely (closer = smaller). + * For other tools, scales with distance for consistent visual size. + */ function scale() { let cam = space.internals().camera; let dist = cam.position.distanceTo(pmesh.position); diff --git a/src/kiri/app/util.js b/src/kiri/app/util.js deleted file mode 100644 index 1d022cc1..00000000 --- a/src/kiri/app/util.js +++ /dev/null @@ -1,52 +0,0 @@ -/** Copyright Stewart Allen -- All Rights Reserved */ - -import { $ } from '../../moto/webui.js'; -import { api } from './api.js'; - -const WIN = self.window; - -function isSecure(proto) { - return proto.toLowerCase().indexOf("https") === 0; -} - -function setProgress(value = 0, msg) { - value = (value * 100).round(4); - api.ui.progress.width = value+'%'; - if (self.debug) { - // console.log(msg, value.round(2)); - api.ui.prostatus.style.display = 'flex'; - if (msg) { - api.ui.prostatus.innerHTML = msg; - } else { - api.ui.prostatus.innerHTML = ''; - } - } -} - -function loadFile(ev) { - // use modern Filesystem api when available - if (false && window.showOpenFilePicker) { - window.showOpenFilePicker().then(files => { - return Promise.all(files.map(fh => fh.getFile())) - }).then(files => { - if (files.length) { - api.platform.load_files(files); - } - }).catch(e => { /* ignore cancel */ }); - return; - } - api.ui.load.click(); -} - -function downloadBlob(data, filename) { - let url = WIN.URL.createObjectURL(new Blob([data], {type: "octet/stream"})); - $('mod-any').innerHTML = `x`; - $('_dexport_').click(); -} - -export const util = { - isSecure, - download: downloadBlob, - setProgress, - loadFile -}; diff --git a/src/kiri/app/viewmode.js b/src/kiri/app/viewmode.js index c7f3731a..35da8ee0 100644 --- a/src/kiri/app/viewmode.js +++ b/src/kiri/app/viewmode.js @@ -11,8 +11,26 @@ import STACKS from './stacks.js'; const { VIEWS } = consts; const DOC = self.document; +/** Current view mode (ARRANGE, SLICE, PREVIEW, or ANIMATE) */ let viewMode = VIEWS.ARRANGE; +/** + * Set application view mode and update UI accordingly. + * Modes: + * - ARRANGE: Widget positioning and arrangement + * - SLICE: Sliced layer visualization + * - PREVIEW: Toolpath/gcode preview + * - ANIMATE: Animated toolpath playback + * + * Each mode: + * - Updates UI button states + * - Clears selections and bounds + * - Shows/hides relevant tools and sliders + * - Configures widget visibility and rendering + * - Emits 'view.set' event + * + * @param {number} mode - View mode constant from VIEWS + */ function setViewMode(mode) { const isCAM = settings.mode() === 'CAM'; viewMode = mode; @@ -37,23 +55,23 @@ function setViewMode(mode) { api.visuals.set_visible_layer(); api.visuals.set_widget_visibility(true); api.widgets.setOpacity(1); - api.view.edges(api.local.getBoolean('model.edges')); + api.view.set_edges(api.local.getBoolean('model.edges')); break; case VIEWS.SLICE: $('act-slice').classList.add('selected'); api.visuals.update_speeds(); api.visuals.update_slider_max(); api.visuals.set_widget_visibility(true); - !isCAM && api.view.edges(false); + !isCAM && api.view.set_edges(false); break; case VIEWS.PREVIEW: $('act-preview').classList.add('selected'); api.visuals.set_widget_visibility(true); - !isCAM && api.view.edges(false); + !isCAM && api.view.set_edges(false); break; case VIEWS.ANIMATE: $('act-animate').classList.add('selected'); - !isCAM && api.view.edges(false); + !isCAM && api.view.set_edges(false); break; default: console.log("invalid view mode: "+mode); @@ -63,15 +81,29 @@ function setViewMode(mode) { DOC.activeElement.blur(); } +/** + * View mode management API. + * Provides getters/setters and mode checking utilities. + */ export const view = { + /** Get current view mode */ get() { return viewMode }, + /** Set view mode */ set() { setViewMode(...arguments) }, + /** Switch to arrange mode */ set_arrange() { setViewMode(VIEWS.ARRANGE) }, + /** Switch to slice mode */ set_slice() { setViewMode(VIEWS.SLICE) }, + /** Switch to preview mode */ set_preview() { setViewMode(VIEWS.PREVIEW) }, + /** Switch to animate mode */ set_animate() { setViewMode(VIEWS.ANIMATE) }, + /** Check if in arrange mode */ is_arrange() { return viewMode === VIEWS.ARRANGE }, + /** Check if in slice mode */ is_slice() { return viewMode === VIEWS.SLICE }, + /** Check if in preview mode */ is_preview() { return viewMode === VIEWS.PREVIEW }, + /** Check if in animate mode */ is_animate() { return viewMode === VIEWS.ANIMATE } }; diff --git a/src/kiri/app/visuals.js b/src/kiri/app/visuals.js index a16166e8..d9ef410f 100644 --- a/src/kiri/app/visuals.js +++ b/src/kiri/app/visuals.js @@ -8,15 +8,35 @@ import STACKS from './stacks.js'; const { COLOR, VIEWS } = consts; -// Note: viewMode is imported dynamically via api.view.get() +/** + * Constrain value to min/max bounds. + * @param {number} v - Value to constrain + * @param {number} min - Minimum value + * @param {number} max - Maximum value + * @returns {number} Constrained value + */ function bound(v,min,max) { return Math.max(min,Math.min(max,v)); } +/** + * Get unit scale factor for coordinate conversion. + * Returns 25.4 for CAM mode with inches, 1.0 otherwise (mm). + * @returns {number} Unit scale factor + */ function unitScale() { return api.mode.is_cam() && settings.ctrl().units === 'in' ? 25.4 : 1; } +/** + * Update speed color legend for preview mode. + * Shows color-coded speed visualization in preview mode for FDM. + * Generates 20 color swatches from worker and displays in UI. + * Only visible in PREVIEW mode for FDM (not SLA/LASER) when enabled. + * Emits 'preview.speeds' event with min/max speeds. + * @param {number} [maxSpeed] - Maximum speed value + * @param {number} [minSpeed] - Minimum speed value + */ function updateSpeeds(maxSpeed, minSpeed) { const { ui } = api; const viewMode = api.view.get(); @@ -51,6 +71,11 @@ function updateSpeeds(maxSpeed, minSpeed) { } } +/** + * Update slider visual position and sync settings. + * Updates slider thumb positions based on current range. + * Synchronizes configuration fields from range values. + */ function updateSlider() { const { lo, hi, max } = api.slider.getRange(); if (max > 0) { @@ -59,6 +84,13 @@ function updateSlider() { api.conf.update_fields_from_range(); } +/** + * Set visible layer range and update visualization. + * Bounds values to valid range and updates slider. + * Triggers slice visualization update. + * @param {number} [h] - High layer (defaults to current hi) + * @param {number} [l] - Low layer (defaults to current lo) + */ function setVisibleLayer(h, l) { const { lo, hi, max } = api.slider.getRange(); h = h >= 0 ? h : hi; @@ -70,11 +102,25 @@ function setVisibleLayer(h, l) { showSlices(); } +/** + * Set wireframe rendering mode for all widgets. + * Updates 3D scene after applying to all widgets. + * @param {boolean} bool - Enable/disable wireframe + * @param {number} [color] - Optional wireframe color + * @param {number} [opacity] - Optional wireframe opacity + */ function setWireframe(bool, color, opacity) { api.widgets.each(function(w) { w.setWireframe(bool, color, opacity) }); SPACE.update(); } +/** + * Toggle or set edge rendering for all widgets. + * If bool.toggle is truthy, toggles the current state. + * Otherwise sets edge visibility to bool value. + * Persists state to local storage and updates scene. + * @param {boolean|object} bool - Enable/disable edges, or {toggle: true} to toggle + */ function setEdges(bool) { if (bool && bool.toggle) { api.local.toggle('model.edges'); @@ -86,6 +132,12 @@ function setEdges(bool) { SPACE.update(); } +/** + * Update slider maximum from stack height. + * Sets slider max to tallest stack layer - 1. + * If set=true or current hi exceeds new max, resets range to show top layer. + * @param {boolean} [set] - Force reset range to max + */ function updateSliderMax(set) { let max = STACKS.getRange().tallest - 1; api.slider.setMax(max); @@ -96,6 +148,10 @@ function updateSliderMax(set) { } } +/** + * Hide slice visualization and restore widget rendering. + * Clears stack display, restores model opacity, disables wireframe. + */ function hideSlices() { STACKS.clear(); api.widgets.setOpacity(COLOR.model_opacity); @@ -104,6 +160,10 @@ function hideSlices() { }); } +/** + * Show or hide all widgets in 3D scene. + * @param {boolean} bool - True to show, false to hide + */ function setWidgetVisibility(bool) { api.widgets.each(w => { if (bool) { @@ -115,9 +175,13 @@ function setWidgetVisibility(bool) { } /** - * hide or show slice-layers and their sub-elements - * - * @param {number} [layer] + * Show slice layer visualization. + * Only works in SLICE/PREVIEW/ANIMATE views (not ARRANGE). + * Updates slider range to show specified layer(s). + * If no layer specified, shows current hi layer. + * Expands range downward if needed (newLo = min(layer, lo)). + * Updates slider position and triggers stack visualization. + * @param {number|string} [layer] - Layer to show (defaults to current hi) */ function showSlices(layer) { const viewMode = api.view.get(); @@ -146,15 +210,26 @@ function showSlices(layer) { SPACE.update(); } +/** + * Show layer slider UI element. + */ function showSlider() { api.slider.show(); } +/** + * Hide layer slider and speed legend UI elements. + */ function hideSlider() { api.slider.hide(); api.ui.speeds.style.display = 'none'; } +/** + * Synchronize stack label visibility with saved preferences. + * Reads visibility state from settings.labels for current mode/view combination. + * Applies visibility to all stack labels. + */ function updateStackLabelState() { const settings = api.conf.get(); // match label checkboxes to preference @@ -165,8 +240,8 @@ function updateStackLabelState() { } export const visuals = { - wireframe: setWireframe, - edges: setEdges, + set_wireframe: setWireframe, + set_edges: setEdges, unit_scale: unitScale, update_speeds: updateSpeeds, update_slider: updateSlider, diff --git a/src/kiri/app/workspace.js b/src/kiri/app/workspace.js index f5df2ea1..6c97faa4 100644 --- a/src/kiri/app/workspace.js +++ b/src/kiri/app/workspace.js @@ -17,13 +17,25 @@ const LOC = self.location; const SETUP = parseOpt(LOC.search.substring(1)); const FILES = openFiles(new Index(SETUP.d ? SETUP.d[0] : 'kiri')); +/** Auto-save timer handle */ let autoSaveTimer = null; +/** + * Reload the application page. + * Emits 'reload' event and defers reload until busy count reaches zero. + */ function reload() { api.event.emit('reload'); do_reload(100); } +/** + * Perform page reload after ensuring async operations complete. + * Checks busy state and defers reload until idle. + * Retries with exponential backoff (100ms -> 250ms). + * @param {number} [time=100] - Initial delay in milliseconds + * @private + */ function do_reload(time) { // allow time for async saves to complete and busy to to to zero setTimeout(() => { @@ -36,6 +48,11 @@ function do_reload(time) { }, time || 100); } +/** + * Trigger delayed auto-save if enabled. + * Debounces saves with 1-second delay. + * Only saves if controller.autoSave is enabled. + */ function auto_save() { if (!settings.ctrl().autoSave) { return; @@ -46,6 +63,14 @@ function auto_save() { }, 1000); } +/** + * Set camera focus on widgets or point. + * If point provided, focuses on that point. + * Otherwise calculates centroid of selection (or all widgets if sel undefined/empty). + * For CAM indexed mode, uses Z=0 focus height. Otherwise uses half of top_z. + * @param {Widget|Array} [sel] - Widget(s) to focus on (defaults to all) + * @param {object} [point] - Optional point {x, y, z} to focus on directly + */ function setFocus(sel, point) { if (point) { SPACE.platform.setCenter(point.x, point.z, point.y); @@ -76,6 +101,16 @@ function setFocus(sel, point) { SPACE.view.setFocus(new THREE.Vector3(pos.x, focus_z, -pos.y)); } +/** + * Save workspace state to local storage. + * Saves: + * - Configuration settings + * - Widget list and metadata + * - Widget positions and states + * Removes stale widget data for deleted widgets. + * Shows alert unless quiet=true. + * @param {boolean} [quiet] - Suppress "workspace saved" alert + */ function saveWorkspace(quiet) { api.conf.save(); const newWidgets = []; @@ -100,6 +135,18 @@ function saveWorkspace(quiet) { } } +/** + * Restore workspace from local storage. + * Restores: + * - Configuration settings + * - Camera view position + * - All saved widgets from storage + * Clears existing platform widgets unless skip_widget_load=true. + * Calls ondone callback when complete. + * @param {Function} [ondone] - Callback when restore completes + * @param {boolean} [skip_widget_load] - Skip loading widgets (config only) + * @returns {boolean} True if widgets were queued for loading + */ function restoreWorkspace(ondone, skip_widget_load) { let newset = api.conf.restore(false), camera = newset.controller.view, @@ -154,6 +201,10 @@ function restoreWorkspace(ondone, skip_widget_load) { return toload.length > 0; } +/** + * Clear all widgets from workspace. + * Clears worker cache/memory, selects all widgets, and deletes them. + */ function clearWorkspace() { // free up worker cache/mem api.client.clear(); @@ -161,6 +212,10 @@ function clearWorkspace() { platform.delete(selection.meshes()); } +/** + * Check if dark mode is enabled. + * @returns {boolean} True if dark mode active + */ function is_dark() { return settings.ctrl().dark; } diff --git a/src/kiri/mode/cam/slice.js b/src/kiri/mode/cam/slice.js index 74675635..6e79d525 100644 --- a/src/kiri/mode/cam/slice.js +++ b/src/kiri/mode/cam/slice.js @@ -23,6 +23,7 @@ export async function cam_slice(settings, widget, onupdate, ondone) { // get tab widgets let tabW = widget.group.filter(w => w != widget); + // merge overlapping tabs for (let i=0; i Date: Thu, 25 Dec 2025 16:34:17 -0500 Subject: [PATCH 034/154] update file names, jsdoc --- src/kiri/app/api.js | 2 +- src/kiri/app/{tools.js => face-tool.js} | 0 src/kiri/app/init/sync.js | 2 +- src/kiri/app/{viewmode.js => view-mode.js} | 0 src/kiri/app/workers.js | 2 +- src/kiri/app/workspace.js | 20 ++++++++++---------- src/kiri/mode/cam/cl-ops.js | 2 +- 7 files changed, 14 insertions(+), 14 deletions(-) rename src/kiri/app/{tools.js => face-tool.js} (100%) rename src/kiri/app/{viewmode.js => view-mode.js} (100%) diff --git a/src/kiri/app/api.js b/src/kiri/app/api.js index d87f99d6..cc0ff6e2 100644 --- a/src/kiri/app/api.js +++ b/src/kiri/app/api.js @@ -35,7 +35,7 @@ import { stats } from './stats.js'; import { types as load } from '../../load/file.js'; import { UI } from './inputs.js'; import { updateTool } from '../mode/cam/tools.js'; -import { view as viewModule } from './viewmode.js'; +import { view as viewModule } from './view-mode.js'; import { visuals } from './visuals.js'; import { widgets } from '../core/widgets.js'; import { workspace } from './workspace.js'; diff --git a/src/kiri/app/tools.js b/src/kiri/app/face-tool.js similarity index 100% rename from src/kiri/app/tools.js rename to src/kiri/app/face-tool.js diff --git a/src/kiri/app/init/sync.js b/src/kiri/app/init/sync.js index 90a4b842..93b10010 100644 --- a/src/kiri/app/init/sync.js +++ b/src/kiri/app/init/sync.js @@ -10,7 +10,7 @@ import { settings as set_ctrl } from '../conf/manager.js'; import { settingsOps } from '../conf/settings.js'; import { space } from '../../../moto/space.js'; import { VIEWS } from '../../core/consts.js'; -import * as view_tools from '../tools.js'; +import * as view_tools from '../face-tool.js'; const { SETUP, LOCAL } = api.const; const { catalog, client, platform, selection, stats, ui } = api; diff --git a/src/kiri/app/viewmode.js b/src/kiri/app/view-mode.js similarity index 100% rename from src/kiri/app/viewmode.js rename to src/kiri/app/view-mode.js diff --git a/src/kiri/app/workers.js b/src/kiri/app/workers.js index 2f6a564f..e0a9871d 100644 --- a/src/kiri/app/workers.js +++ b/src/kiri/app/workers.js @@ -48,7 +48,7 @@ function send(fn, data, onreply, zerocopy) { // code is running in the browser / client context export const client = { - send: send, + send, setWorkPath(path) { workpath = path; diff --git a/src/kiri/app/workspace.js b/src/kiri/app/workspace.js index 6c97faa4..c8ea427e 100644 --- a/src/kiri/app/workspace.js +++ b/src/kiri/app/workspace.js @@ -71,7 +71,7 @@ function auto_save() { * @param {Widget|Array} [sel] - Widget(s) to focus on (defaults to all) * @param {object} [point] - Optional point {x, y, z} to focus on directly */ -function setFocus(sel, point) { +function set_focus(sel, point) { if (point) { SPACE.platform.setCenter(point.x, point.z, point.y); SPACE.view.setFocus(new THREE.Vector3(point.x, point.y, point.z)); @@ -111,7 +111,7 @@ function setFocus(sel, point) { * Shows alert unless quiet=true. * @param {boolean} [quiet] - Suppress "workspace saved" alert */ -function saveWorkspace(quiet) { +function save(quiet) { api.conf.save(); const newWidgets = []; const oldWidgets = js2o(SDB.getItem('ws-widgets'), []); @@ -147,7 +147,7 @@ function saveWorkspace(quiet) { * @param {boolean} [skip_widget_load] - Skip loading widgets (config only) * @returns {boolean} True if widgets were queued for loading */ -function restoreWorkspace(ondone, skip_widget_load) { +function restore(ondone, skip_widget_load) { let newset = api.conf.restore(false), camera = newset.controller.view, toload = ls2o('ws-widgets',[]), @@ -205,7 +205,7 @@ function restoreWorkspace(ondone, skip_widget_load) { * Clear all widgets from workspace. * Clears worker cache/memory, selects all widgets, and deletes them. */ -function clearWorkspace() { +function clear() { // free up worker cache/mem api.client.clear(); platform.select_all(); @@ -221,12 +221,12 @@ function is_dark() { } export const workspace = { - reload, auto_save, - restore: restoreWorkspace, - clear: clearWorkspace, - save: saveWorkspace, - set_focus: setFocus, + clear, + is_dark, + reload, + restore, + save, + set_focus, update: SPACE.update, - is_dark }; diff --git a/src/kiri/mode/cam/cl-ops.js b/src/kiri/mode/cam/cl-ops.js index 106efbf9..0f90ba55 100644 --- a/src/kiri/mode/cam/cl-ops.js +++ b/src/kiri/mode/cam/cl-ops.js @@ -9,7 +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 { startFaceUp } from '../../app/tools.js'; +import { startFaceUp } from '../../app/face-tool.js'; const { MODES } = api.const; const { uc: UC } = api; From 7290103b22c0b2df9c325060e3717970147b84a3 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 25 Dec 2025 17:58:45 -0500 Subject: [PATCH 035/154] fix dialog key override / restore, add named module loading --- mods/bambu/bambu.js | 4 +--- mods/devel/kiri.js | 2 +- src/kiri/app/api.js | 20 +++++++++++++++----- src/kiri/app/inputs.js | 4 +++- src/kiri/app/keyboard.js | 8 ++++---- src/kiri/app/modal.js | 1 - src/main/kiri.js | 16 +++++++++------- src/moto/broker.js | 4 ++++ 8 files changed, 37 insertions(+), 22 deletions(-) diff --git a/mods/bambu/bambu.js b/mods/bambu/bambu.js index 76b43dfa..53faa3eb 100644 --- a/mods/bambu/bambu.js +++ b/mods/bambu/bambu.js @@ -1,7 +1,5 @@ self.kiri.load(api => { - console.log('BAMBU MODULE RUNNING'); - const { uc: ui } = api; const { $, h } = api.web; const defams = ";; DEFINE BAMBU-AMS "; @@ -1327,4 +1325,4 @@ self.kiri.load(api => { setInterval(monitor_keepalive, 5000); let bblapi = api.bambu = { send, prep_export }; -}); +}, 'Bambu'); diff --git a/mods/devel/kiri.js b/mods/devel/kiri.js index a4562540..514a25bb 100644 --- a/mods/devel/kiri.js +++ b/mods/devel/kiri.js @@ -20,4 +20,4 @@ self.kiri.load(api => { } }; }); -}); +}, 'Developer'); diff --git a/src/kiri/app/api.js b/src/kiri/app/api.js index cc0ff6e2..7ecceb8b 100644 --- a/src/kiri/app/api.js +++ b/src/kiri/app/api.js @@ -115,7 +115,15 @@ export const api = { clone: Object.clone, color, conf: settings.conf, - const: { LANG, LOCAL, SETUP, SECURE, SPACE, STACKS, ...consts }, + const: { + LANG, LOCAL, SETUP, SECURE, SPACE, STACKS, + ...consts, + URL: { + path: LOC.pathname, + hash: LOC.hash.substring(1), + query: LOC.search.substring(1) + } + }, /** * Development/debug utilities */ @@ -151,12 +159,14 @@ export const api = { redo: noop // do.js }, event: { - on(t,l) { return EVENT.on(t,l) }, - emit(t,m,o) { return EVENT.publish(t,m,o) }, - bind(t,m,o) { return EVENT.bind(t,m,o) }, alerts(clr) { alerts.update(clr) }, + bind(t,m,o) { return EVENT.bind(t,m,o) }, + emit(t,m,o) { return EVENT.publish(t,m,o) }, import() { api.ui.load.click() }, - settings: settingsUI.trigger_event + listeners(topic) { return EVENT.targets(topic) }, + on(t,l) { return EVENT.on(t,l) }, + settings: settingsUI.trigger_event, + topics() { return EVENT.topics() } }, electron: navigator.userAgent.includes('Electron'), /** diff --git a/src/kiri/app/inputs.js b/src/kiri/app/inputs.js index 9525762b..a2204af7 100644 --- a/src/kiri/app/inputs.js +++ b/src/kiri/app/inputs.js @@ -208,9 +208,11 @@ function confirm(message, buttons, input, opt = {}) { html.appendAll(opt.post); } dialog.innerHTML = html.join(''); + dialog.onclose = () => { + feature.on_key = onkey_save; + }; function done(value) { dialog.close(); - feature.on_key = onkey_save; if (value !== undefined) { setTimeout(() => { resolve(value) }, 150); } diff --git a/src/kiri/app/keyboard.js b/src/kiri/app/keyboard.js index 13b399db..d0e825c9 100644 --- a/src/kiri/app/keyboard.js +++ b/src/kiri/app/keyboard.js @@ -253,15 +253,15 @@ class KeyboardControl { if (handler({key:evt})) return; } - // Handle Ctrl key combinations + // Handle widget grouping (disabled: broken, conflicts with hash settings) if (evt.ctrlKey) { switch (evt.key) { - case 'g': return this.#api.group.merge(); - case 'u': return this.#api.group.split(); + // case 'g': return this.#api.group.merge(); + // case 'u': return this.#api.group.split(); } } - switch (evt.charCode) { + switch (evt.keyCode) { case this.#cca('`'): this.#api.show.slices(0); break; case this.#cca('0'): { const { max } = this.#slider.getRange(); diff --git a/src/kiri/app/modal.js b/src/kiri/app/modal.js index 2dacd74a..893e0ab2 100644 --- a/src/kiri/app/modal.js +++ b/src/kiri/app/modal.js @@ -62,7 +62,6 @@ class ModalControl { // hide all modals before showing another Object.keys(modal.#ui.modals).forEach(name => { - console.log({ name, which }); modal.#ui.modals[name].style.display = name === which ? 'flex' : ''; }); diff --git a/src/main/kiri.js b/src/main/kiri.js index 17a1b119..ad798760 100644 --- a/src/main/kiri.js +++ b/src/main/kiri.js @@ -14,10 +14,12 @@ import { init_sync } from '../kiri/app/init/sync.js'; let traceload = location.search.indexOf('traceload') > 0; let load = []; -function safeExec(fn) { +function safeExec(fn, name) { try { if (traceload) { - console.log('kiri | exec |', fn); + console.log('kiri | exec |', name, fn); + } else if (name) { + console.log('kiri | load mods |', name); } fn(kiri.api); } catch (error) { @@ -37,8 +39,8 @@ async function checkReady() { await init_input(); await init_sync(); } - for (let fn of load) { - safeExec(fn); + for (let [fn, name] of load) { + safeExec(fn, name); } load = undefined; api.event.emit('load-done', stats); @@ -63,12 +65,12 @@ async function checkReady() { } self.kiri = { - load(fn) { + load(fn, name) { // console.log('KIRI LOAD', [...arguments]); if (load) { - load.push(fn); + load.push([fn, name]); } else { - safeExec(fn); + safeExec(fn, name); } } }; diff --git a/src/moto/broker.js b/src/moto/broker.js index 08ca8734..7682e30b 100644 --- a/src/moto/broker.js +++ b/src/moto/broker.js @@ -18,6 +18,10 @@ class Broker { return Object.keys(this.#topics); } + targets(topic) { + return this.#topics[topic]; + } + get send() { return this.#send; } From 1dbe7fe29b5586750c257e2599e7142aa9d1421e Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 25 Dec 2025 20:00:27 -0500 Subject: [PATCH 036/154] fix rendering performance stats in developer mode --- src/kiri/app/alerts.js | 26 ++------------- src/kiri/app/api.js | 2 +- src/kiri/app/init/sync.js | 2 +- src/kiri/app/preferences.js | 37 ++-------------------- src/kiri/app/visuals.js | 63 ++++++++++++++++++++++++++++++++++--- src/kiri/mode/fdm/slice.js | 9 +++--- 6 files changed, 71 insertions(+), 68 deletions(-) diff --git a/src/kiri/app/alerts.js b/src/kiri/app/alerts.js index 6fa72f1f..a54f1dac 100644 --- a/src/kiri/app/alerts.js +++ b/src/kiri/app/alerts.js @@ -82,36 +82,14 @@ function update(clear) { } } -/** - * Update progress bar display. - * In debug mode, also shows progress status message. - * @param {number} [value=0] - Progress value (0.0 to 1.0) - * @param {string} [msg] - Optional status message to display - */ -function progress(value = 0, msg) { - value = (value * 100).round(4); - api.ui.progress.width = value+'%'; - if (self.debug) { - // console.log(msg, value.round(2)); - api.ui.prostatus.style.display = 'flex'; - if (msg) { - api.ui.prostatus.innerHTML = msg; - } else { - api.ui.prostatus.innerHTML = ''; - } - } -} - export { hide, show, - update, - progress + update }; export default { hide, show, - update, - progress + update }; diff --git a/src/kiri/app/api.js b/src/kiri/app/api.js index 7ecceb8b..d7520350 100644 --- a/src/kiri/app/api.js +++ b/src/kiri/app/api.js @@ -238,7 +238,7 @@ export const api = { import() { api.ui.import.style.display = '' }, layer: visuals.set_visible_layer, local() { console.trace('deprecated') }, - progress: alerts.progress, + progress: visuals.set_progress, slices: visuals.show_slices, tools: showTools }, diff --git a/src/kiri/app/init/sync.js b/src/kiri/app/init/sync.js index 93b10010..bff4f9c9 100644 --- a/src/kiri/app/init/sync.js +++ b/src/kiri/app/init/sync.js @@ -165,7 +165,7 @@ function ui_sync() { preferences.setThreaded(true); preferences.lineTypeSave(); preferences.detailSave(); - preferences.updateStats(); + api.visuals.update_stats(); // optional set-and-lock mode (hides mode menu) let SETMODE = SETUP.mode ? SETUP.mode[0] : null; diff --git a/src/kiri/app/preferences.js b/src/kiri/app/preferences.js index c3e3cc01..8facac53 100644 --- a/src/kiri/app/preferences.js +++ b/src/kiri/app/preferences.js @@ -6,12 +6,10 @@ import { base } from '../../geo/base.js'; import { space } from '../../moto/space.js'; let { client, platform } = api; -let ui, statsTimer; // Lazy initialization to avoid circular dependencies function getUI() { - if (!ui) ui = api.ui; - return ui; + return api.ui; } function settings() { @@ -107,7 +105,7 @@ function booleanSave() { // platform.layout(); api.conf.save(); api.platform.update_size(); - updateStats(); + api.visuals.update_stats(); updateDrawer(); api.event.emit('boolean.update'); space.view.setProjection(control.ortho ? 'orthographic' : 'perspective'); @@ -119,34 +117,6 @@ function updateDrawer() { $c('app', scrolls ? '' : 'hide-scroll', scrolls ? 'hide-scroll' : ''); } -function updateStats() { - if (self.debug !== true) { - return; - } - console.log('updateStats()'); - const ui = getUI(); - let { div, fps, rms, rnfo } = ui.stats; - div.style.display = 'flex'; - clearTimeout(statsTimer); - statsTimer = setInterval(() => { - const nrms = space.view.getRMS().toFixed(1); - const nfps = space.view.getFPS().toFixed(1); - const rend = space.renderInfo(); - const { memory, render } = rend; - if (nfps !== fps.innerText) { - fps.innerText = nfps; - } - if (nrms !== rms.innerText) { - rms.innerText = nrms; - } - if (rnfo.offsetParent !== null) { - rnfo.innerHTML = Object.entries({ ...memory, ...render, render_ms: nrms, frames_sec: nfps }).map(row => { - return `
${row[0]}
` - }).join(''); - } - }, 100); -} - export const preferences = { unitsSave, aniMeshSave, @@ -155,6 +125,5 @@ export const preferences = { speedSave, setThreaded, booleanSave, - updateDrawer, - updateStats + updateDrawer }; diff --git a/src/kiri/app/visuals.js b/src/kiri/app/visuals.js index d9ef410f..3e7201a5 100644 --- a/src/kiri/app/visuals.js +++ b/src/kiri/app/visuals.js @@ -1,9 +1,11 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { api } from './api.js'; +import { base } from '../../geo/base.js'; import { consts } from '../core/consts.js'; import { settings } from './conf/manager.js'; -import { space as SPACE } from '../../moto/space.js'; +import { space } from '../../moto/space.js'; + import STACKS from './stacks.js'; const { COLOR, VIEWS } = consts; @@ -111,7 +113,7 @@ function setVisibleLayer(h, l) { */ function setWireframe(bool, color, opacity) { api.widgets.each(function(w) { w.setWireframe(bool, color, opacity) }); - SPACE.update(); + space.update(); } /** @@ -129,7 +131,7 @@ function setEdges(bool) { } bool = api.local.getBoolean('model.edges'); api.widgets.each(w => w.setEdges(bool)); - SPACE.update(); + space.update(); } /** @@ -207,7 +209,7 @@ function showSlices(layer) { updateSlider(); // STACKS.setRange is called by slider callback - SPACE.update(); + space.update(); } /** @@ -239,7 +241,59 @@ function updateStackLabelState() { } } +/** + * Update progress bar display. + * In debug mode, also shows progress status message. + * @param {number} [value=0] - Progress value (0.0 to 1.0) + * @param {string} [msg] - Optional status message to display + */ +function setProgress(value = 0, msg) { + value = (value * 100).round(4); + api.ui.progress.width = value+'%'; + if (self.debug) { + // console.log(msg, value.round(2)); + api.ui.prostatus.style.display = 'flex'; + if (msg) { + api.ui.prostatus.innerHTML = msg; + } else { + api.ui.prostatus.innerHTML = ''; + } + } +} + +let statsTimer; + +function updateStats() { + clearTimeout(statsTimer); + let { div, fps, rms, rnfo } = api.ui.stats; + if (api.devel.enabled === false) { + fps.innerText = ''; + rms.innerText = ''; + rnfo.innerHTML = ''; + return; + } + div.style.display = 'flex'; + statsTimer = setInterval(() => { + const nrms = space.view.getRMS().toFixed(1); + const nfps = space.view.getFPS().toFixed(1); + const rend = space.renderInfo(); + const { memory, render } = rend; + if (nfps !== fps.innerText) { + fps.innerText = nfps; + } + if (nrms !== rms.innerText) { + rms.innerText = nrms; + } + if (rnfo.offsetParent !== null) { + rnfo.innerHTML = Object.entries({ ...memory, ...render, render_ms: nrms, frames_sec: nfps }).map(row => { + return `
${row[0]}
` + }).join(''); + } + }, 100); +} + export const visuals = { + set_progress: setProgress, set_wireframe: setWireframe, set_edges: setEdges, unit_scale: unitScale, @@ -247,6 +301,7 @@ export const visuals = { update_slider: updateSlider, update_slider_max: updateSliderMax, update_stack_labels: updateStackLabelState, + update_stats: updateStats, show_slices: showSlices, hide_slices: hideSlices, show_slider: showSlider, diff --git a/src/kiri/mode/fdm/slice.js b/src/kiri/mode/fdm/slice.js index 5bc215da..b6505093 100644 --- a/src/kiri/mode/fdm/slice.js +++ b/src/kiri/mode/fdm/slice.js @@ -119,12 +119,13 @@ export function fdm_slice(settings, widget, onupdate, ondone) { let render = settings.render !== false, { minions } = self.kiri_worker, { process, device, controller } = settings, + { devel, assembly, threaded } = controller, isBelt = device.bedBelt, - isBrick = controller.devel && process.sliceZInterleave, + isBrick = devel && process.sliceZInterleave, isSynth = widget.track.synth, isSupport = widget.track.support, - useAssembly = controller.assembly, - isConcurrent = controller.threaded && minions.concurrent, + useAssembly = assembly, + isConcurrent = threaded && minions.concurrent, topLayers = process.sliceTopLayers || 0, bottomLayers = process.sliceBottomLayers || 0, vaseMode = process.sliceFillType === 'vase' && !isSynth, @@ -343,7 +344,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) { if (changes && !isSynth) { healed = true; slice.changes = changes; - if (self.debug) { + if (devel) { console.log('slice healed', slice.index, slice.z, changes); } } From 080d7a23e290308e7ac5e5216711849682c23dd4 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 25 Dec 2025 20:30:06 -0500 Subject: [PATCH 037/154] add slicing/preview stats to dev hover stats --- src/kiri/app/function.js | 2 ++ src/kiri/app/visuals.js | 16 +++++++++++++--- web/kiri/index.css | 3 +++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/kiri/app/function.js b/src/kiri/app/function.js index d7939c9d..e51b04ba 100644 --- a/src/kiri/app/function.js +++ b/src/kiri/app/function.js @@ -291,6 +291,7 @@ function prepareSlices(callback, scale = 1, offset = 0) { // print stats segtimes.total = Date.now() - mark; console.log(segtimes); + api.visuals.update_stats(segtimes); if (callback && typeof callback === 'function') { callback(); } @@ -428,6 +429,7 @@ function preparePreview(callback, scale = 1, offset = 0) { // print stats segtimes.total = Date.now() - mark; console.log(segtimes); + api.visuals.update_stats(segtimes); event.emit('print', pMode); event.emit('preview.end', pMode); diff --git a/src/kiri/app/visuals.js b/src/kiri/app/visuals.js index 3e7201a5..a2f0d9da 100644 --- a/src/kiri/app/visuals.js +++ b/src/kiri/app/visuals.js @@ -262,8 +262,10 @@ function setProgress(value = 0, msg) { } let statsTimer; +let statsAdd; -function updateStats() { +function updateStats(add) { + statsAdd = add ? { ...add, 'hr': 1 } : {}; clearTimeout(statsTimer); let { div, fps, rms, rnfo } = api.ui.stats; if (api.devel.enabled === false) { @@ -285,8 +287,16 @@ function updateStats() { rms.innerText = nrms; } if (rnfo.offsetParent !== null) { - rnfo.innerHTML = Object.entries({ ...memory, ...render, render_ms: nrms, frames_sec: nfps }).map(row => { - return `
${row[0]}
` + rnfo.innerHTML = Object.entries({ + ...statsAdd, + ...memory, + ...render, + render_ms: nrms, + frames_sec: nfps + }).map(row => { + return row[0] === 'hr' ? + '

' : + `
${row[0]}
` ; }).join(''); } }, 100); diff --git a/web/kiri/index.css b/web/kiri/index.css index 344acd70..6d09ee00 100644 --- a/web/kiri/index.css +++ b/web/kiri/index.css @@ -2536,6 +2536,9 @@ details[open] summary::after { flex-direction: column; align-items: flex-end; } +#stats hr { + width: 100%; +} #stats:hover #rnfo { display: grid; } From 70c15876670143ae30ef6e73d1e44a411fb047e6 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 25 Dec 2025 20:47:38 -0500 Subject: [PATCH 038/154] switch laser stack 3d output to obj --- src/kiri/app/api.js | 2 +- src/kiri/app/export.js | 12 ++++++------ src/kiri/mode/laser/driver.js | 14 ++++++++------ web/kiri/index.html | 2 +- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/kiri/app/api.js b/src/kiri/app/api.js index d7520350..360775cd 100644 --- a/src/kiri/app/api.js +++ b/src/kiri/app/api.js @@ -76,7 +76,7 @@ function isSecure(proto) { * @param {string} filename - Filename for download */ function download(data, filename) { - let url = WIN.URL.createObjectURL(new Blob([data], {type: "octet/stream"})); + let url = window.URL.createObjectURL(new Blob([data], {type: "octet/stream"})); $('mod-any').innerHTML = `x`; $('_dexport_').click(); } diff --git a/src/kiri/app/export.js b/src/kiri/app/export.js index a44b7f0d..4e44302d 100644 --- a/src/kiri/app/export.js +++ b/src/kiri/app/export.js @@ -136,9 +136,9 @@ function exportLaserDialog(data, names) { const driver = laser_driver; const { process } = settings; if (process.ctOutStack) { - $('print-stl').classList.remove('hide'); + $('print-obj').classList.remove('hide'); } else { - $('print-stl').classList.add('hide'); + $('print-obj').classList.add('hide'); } function download_svg() { @@ -155,10 +155,10 @@ function exportLaserDialog(data, names) { ); } - function download_stl() { + function download_obj() { api.util.download( - driver.exportSTL(settings, data), - $('print-filename-laser').value + ".stl" + driver.exportOBJ(settings, data), + $('print-filename-laser').value + ".obj" ); } @@ -178,7 +178,7 @@ function exportLaserDialog(data, names) { $('print-lines').value = util.comma(segments); $('print-svg').onclick = download_svg; $('print-dxf').onclick = download_dxf; - $('print-stl').onclick = download_stl; + $('print-obj').onclick = download_obj; $('print-lg').onclick = download_gcode; } diff --git a/src/kiri/mode/laser/driver.js b/src/kiri/mode/laser/driver.js index bbd7f60f..c097bfa4 100644 --- a/src/kiri/mode/laser/driver.js +++ b/src/kiri/mode/laser/driver.js @@ -11,7 +11,7 @@ import { render } from '../../core/render.js'; import { slicer } from '../../../geo/slicer.js'; import { newSlice } from '../../core/slice.js'; import { newPolygon } from '../../../geo/polygon.js'; -import { STL } from '../../../load/stl.js'; +import { encode as encodeOBJ } from '../../../load/obj.js'; export const TYPE = { LASER: 0, @@ -610,7 +610,7 @@ function exportElements(settings, output, onpre, onpoly, onpost, onpoint, onlaye onpost(); }; -function exportSTL(settings, data) { +function exportOBJ(settings, data) { let { ctSliceHeight, ctOutSmooth } = settings.process; let polys = []; for (let array of data) { @@ -633,7 +633,7 @@ function exportSTL(settings, data) { }).flat().filter(v => v); } let layers = {}; - let mesh = []; + let obj = []; for (let poly of polys) { let z = poly.getZ(); let layer = layers[z]; @@ -644,13 +644,15 @@ function exportSTL(settings, data) { } for (let rec of Object.entries(layers)) { let [ z, polys ] = rec; + let layer = []; polys = POLY.nest(polys); for (let poly of polys) { let ext = poly.setZ(0).extrude(ctSliceHeight, { zadd: parseFloat(z) }); - mesh = mesh.concat(ext); + layer = layer.concat(ext); } + obj.push({ file:`z-${z}`, varr: layer }); } - return new STL().encode(mesh); + return encodeOBJ(obj); } /** @@ -933,5 +935,5 @@ export const LASER = { exportGCode, exportDXF, exportSVG, - exportSTL + exportOBJ }; diff --git a/web/kiri/index.html b/web/kiri/index.html index ae9e1a88..523b86e9 100644 --- a/web/kiri/index.html +++ b/web/kiri/index.html @@ -716,7 +716,7 @@
- +
From 1f663f3cc970d25bc52c845b40a8509b65d4865d Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 25 Dec 2025 22:16:13 -0500 Subject: [PATCH 039/154] properly handle expanding preview in developer mode --- mods/bambu/bambu.js | 2 +- src/kiri/app/export.js | 7 ++++--- src/kiri/app/modal.js | 3 +++ web/kiri/index.css | 14 ++++++++++++++ 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/mods/bambu/bambu.js b/mods/bambu/bambu.js index 53faa3eb..ebc6f9c7 100644 --- a/mods/bambu/bambu.js +++ b/mods/bambu/bambu.js @@ -1253,7 +1253,7 @@ self.kiri.load(api => { } function prep_export(gen3mf, gcode, info, settings) { - if (!settings.device.extras?.bbl || api.ui.deviceSave.disabled) { + if (!settings.device.extras?.bbl || api.ui.devices.save.disabled) { $('bambu-output').style.display = 'none'; return; } diff --git a/src/kiri/app/export.js b/src/kiri/app/export.js index 4e44302d..8e682a35 100644 --- a/src/kiri/app/export.js +++ b/src/kiri/app/export.js @@ -328,7 +328,8 @@ function exportGCodeDialog(gcode, sections, info, names) { api.modal.show('xany'); let set = api.conf.get(); let fdm = MODE === MODES.FDM; - let octo = set.controller.exportOcto && MODE !== MODES.CAM; + let cam = MODE === MODES.CAM; + let octo = set.controller.exportOcto && !cam; let preview = set.controller.exportPreview; $('code-preview-head').style.display = preview ? '' : 'none'; $('code-preview').style.display = preview ? '' : 'none'; @@ -337,8 +338,8 @@ function exportGCodeDialog(gcode, sections, info, names) { $('print-filename').value = filename; $('print-filesize').value = util.comma(info.bytes); $('print-length').value = Math.round(info.distance); - if (set.controller.devel) { - $('code-preview-textarea').style.height = "30em"; + if ((fdm || cam) && preview && set.controller.devel) { + document.body.classList.add('devel'); } calcTime(); if (fdm) { diff --git a/src/kiri/app/modal.js b/src/kiri/app/modal.js index 893e0ab2..74d19fa8 100644 --- a/src/kiri/app/modal.js +++ b/src/kiri/app/modal.js @@ -60,6 +60,9 @@ class ModalControl { visible = modal.visible(), info = { pct: 0 }; + // if the dialog needs it, it will re-add it + document.body.classList.remove('devel'); + // hide all modals before showing another Object.keys(modal.#ui.modals).forEach(name => { modal.#ui.modals[name].style.display = name === which ? 'flex' : ''; diff --git a/web/kiri/index.css b/web/kiri/index.css index 6d09ee00..4da46c58 100644 --- a/web/kiri/index.css +++ b/web/kiri/index.css @@ -2615,3 +2615,17 @@ details[open] summary::after { .range-detail span { flex-grow: 1; } + +.devel #modal-box { + height: 80%; +} +.devel #mod-x-any { + height: 100%; +} +.devel #code-preview { + flex-grow: 1; +} +.devel #code-preview > div, +.devel #code-preview-textarea { + height: 100%; +} \ No newline at end of file From 1505d94fc0a6834f44681c410c60ba77b8e08afe Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 25 Dec 2025 22:27:26 -0500 Subject: [PATCH 040/154] prevent text wrap in stats --- src/kiri/app/function.js | 8 ++++---- web/kiri/index.css | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/kiri/app/function.js b/src/kiri/app/function.js index e51b04ba..c4956a7f 100644 --- a/src/kiri/app/function.js +++ b/src/kiri/app/function.js @@ -152,8 +152,8 @@ function prepareSlices(callback, scale = 1, offset = 0) { let mark = Date.now(); if (lastMsg) { let key = slicing.length > 1 ? - `${widget.id}_${segNumber++}_${lastMsg}` : - `${segNumber++}_${lastMsg}` + `${widget.id} ${segNumber++} ${lastMsg}` : + `${segNumber++} ${lastMsg}` segtimes[key] = mark - startTime; } lastMsg = msg; @@ -177,8 +177,8 @@ function prepareSlices(callback, scale = 1, offset = 0) { // update segment time if (lastMsg) { let key = slicing.length > 1 ? - `${widget.id}_${segNumber++}_${lastMsg}` : - `${segNumber++}_${lastMsg}` + `${widget.id} ${segNumber++} ${lastMsg}` : + `${segNumber++} ${lastMsg}` segtimes[`${key}`] = mark - startTime; } event.emit('slice', settings.mode); diff --git a/web/kiri/index.css b/web/kiri/index.css index 4da46c58..1536e407 100644 --- a/web/kiri/index.css +++ b/web/kiri/index.css @@ -2535,6 +2535,7 @@ details[open] summary::after { color: #000; flex-direction: column; align-items: flex-end; + white-space: nowrap; } #stats hr { width: 100%; From 97a3edf0398b4e051a6359e9d62f11ba3a39b14c Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 25 Dec 2025 22:57:14 -0500 Subject: [PATCH 041/154] add strict closing rule for fdm, laser slicing --- src/geo/slicer.js | 5 +++++ src/kiri/mode/fdm/slice.js | 1 + src/kiri/mode/laser/driver.js | 1 + 3 files changed, 7 insertions(+) diff --git a/src/geo/slicer.js b/src/geo/slicer.js index 3ce82017..88e575c5 100644 --- a/src/geo/slicer.js +++ b/src/geo/slicer.js @@ -565,6 +565,11 @@ export function sliceConnect(input, z, opt = {}) { // emit a polygon if it can be cleaned and still have 2 or more points function emit(poly) { + let gap = poly.first().distTo2D(poly.last()); + if (opt.strict && gap > bridgeMax) { + if (debug) console.log('drop', gap, poly); + return; + } emitted += poly.length; if (!opt.dirty) poly = poly.clean(); if (poly.length > 2 || true) output.push(poly); diff --git a/src/kiri/mode/fdm/slice.js b/src/kiri/mode/fdm/slice.js index b6505093..2a99d529 100644 --- a/src/kiri/mode/fdm/slice.js +++ b/src/kiri/mode/fdm/slice.js @@ -222,6 +222,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) { } slice(points, { + strict: true, debug: process.xray, xray: process.xray, zMin: bounds.min.z, diff --git a/src/kiri/mode/laser/driver.js b/src/kiri/mode/laser/driver.js index c097bfa4..daedf895 100644 --- a/src/kiri/mode/laser/driver.js +++ b/src/kiri/mode/laser/driver.js @@ -191,6 +191,7 @@ function laser_slice(settings, widget, onupdate, ondone) { let indices = []; slicer.slice(points, { + strict: true, zMin: bounds.min.z, zMax: bounds.max.z, zGen(zopt) { From caa986e9a2e78eba3b0800e774aac22b4a549849 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 25 Dec 2025 23:15:38 -0500 Subject: [PATCH 042/154] collapsible ops list --- web/kiri/index.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/kiri/index.html b/web/kiri/index.html index 523b86e9..a06b6b89 100644 --- a/web/kiri/index.html +++ b/web/kiri/index.html @@ -447,7 +447,8 @@
- +
+ operation list
@@ -474,6 +475,7 @@
+
From de41b25fbaeec81b895d769fd754229d933a6d62 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 26 Dec 2025 00:00:04 -0500 Subject: [PATCH 043/154] add optional slice ordering via selection --- src/kiri/app/function.js | 6 ++++++ src/kiri/mode/cam/prepare.js | 8 ++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/kiri/app/function.js b/src/kiri/app/function.js index c4956a7f..7e75c8fc 100644 --- a/src/kiri/app/function.js +++ b/src/kiri/app/function.js @@ -12,6 +12,7 @@ import { exportFile } from './export.js'; * Properties: slice, preview, export */ let complete = {}; +let order; /** * Prepare and execute slicing for all widgets on the platform. @@ -46,6 +47,10 @@ function prepareSlices(callback, scale = 1, offset = 0) { callback = preparePreview; } + order = api.selection.count() ? + Object.assign({}, ...api.selection.widgets().map((w,i) => ({[w.id]:i}))) : + undefined; + const widgets = api.widgets.all(); const settings = conf.get(); const { device, process, controller } = settings; @@ -371,6 +376,7 @@ function preparePreview(callback, scale = 1, offset = 0) { // pass preview mode to worker settings.pmode = feature.pmode; settings.render = true; + settings.order = order; client.prepare(settings, (progress, message, layer) => { if (layer) { diff --git a/src/kiri/mode/cam/prepare.js b/src/kiri/mode/cam/prepare.js index 7c63c418..8da97d71 100644 --- a/src/kiri/mode/cam/prepare.js +++ b/src/kiri/mode/cam/prepare.js @@ -28,7 +28,7 @@ export async function cam_prepare(widgets, settings, update) { const count = active.length; const weight = 1 / count; const print = self.kiri_worker.current.print = newPrint(settings, active); - const { origin } = settings; + const { order, origin } = settings; // wait for safe eval setup await print.ready(); @@ -37,7 +37,11 @@ export async function cam_prepare(widgets, settings, update) { print.output = []; // sort output by distance to origin - if (origin) { + if (order) { + active.sort((a,b) => { + return (order[a.id] ?? Infinity) - (order[b.id] ?? Infinity); + }); + } else if (origin) { let point = newPoint().move(origin); active.sort((w0,w1) => newPoint().move(w0.track.pos).distTo2D(point) - From 27293271de84aa7a25b888de4cf471b2da95ca18 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 26 Dec 2025 00:19:54 -0500 Subject: [PATCH 044/154] missed a couple of segtimes _ separators --- src/kiri/app/function.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/kiri/app/function.js b/src/kiri/app/function.js index 7e75c8fc..a5f027fc 100644 --- a/src/kiri/app/function.js +++ b/src/kiri/app/function.js @@ -264,8 +264,8 @@ function prepareSlices(callback, scale = 1, offset = 0) { for (let widget of slicing) { // on done let key = slicing.length > 1 ? - `${widget.id}_${segNumber++}_draw` : - `${segNumber++}_draw` + `${widget.id}_${segNumber++} draw` : + `${segNumber++} draw` segtimes[`${key}`] = widget.render(widget.stack); // rotate stack for belt beds if (widget.belt) { @@ -385,7 +385,7 @@ function preparePreview(callback, scale = 1, offset = 0) { if (message && message !== lastMsg) { const mark = Date.now(); if (lastMsg) { - segtimes[`${segNumber++}_${lastMsg}`] = mark - startTime; + segtimes[`${segNumber++} ${lastMsg}`] = mark - startTime; } lastMsg = message; startTime = mark; @@ -403,7 +403,7 @@ function preparePreview(callback, scale = 1, offset = 0) { } if (lastMsg) { - segtimes[`${segNumber++}_${lastMsg}`] = Date.now() - startTime; + segtimes[`${segNumber++} ${lastMsg}`] = Date.now() - startTime; } show.progress(0); @@ -429,7 +429,7 @@ function preparePreview(callback, scale = 1, offset = 0) { stack.obj.rotate(widgets[0].belt); } api.hide.alert(alert); - segtimes[`${segNumber}_draw`] = Date.now() - startTime; + segtimes[`${segNumber} draw`] = Date.now() - startTime; } // print stats From fc6ea03f8d24e3232bb074d94f868c2770d74a96 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 26 Dec 2025 10:06:53 -0500 Subject: [PATCH 045/154] start reworking platform axis labels --- mods/bambu/bambu.js | 2 +- src/moto/space.js | 205 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 181 insertions(+), 26 deletions(-) diff --git a/mods/bambu/bambu.js b/mods/bambu/bambu.js index ebc6f9c7..e27bf4e6 100644 --- a/mods/bambu/bambu.js +++ b/mods/bambu/bambu.js @@ -1230,7 +1230,7 @@ self.kiri.load(api => { if (!bound) { return; } - if (devsel.extras?.bbl && !api.ui.deviceSave.disabled) { + if (devsel.extras?.bbl && !api.ui.devices.save.disabled) { device = devsel; printers = devsel.extras.bbl; bound.bblman.classList.remove('hide'); diff --git a/src/moto/space.js b/src/moto/space.js index 1c3202b3..310f1806 100644 --- a/src/moto/space.js +++ b/src/moto/space.js @@ -444,6 +444,129 @@ function canvasInMesh(w, h, textAlign, textBaseline, color, size) { return { context, mesh }; } +/** + * Font atlas for 3D text labels. + * Generates a texture atlas with characters 0-9, -, X, Y at startup. + */ +let fontAtlas = null; + +/** + * Create bitmap font atlas for grid labels. + * Generates texture with characters: 0-9, -, X, Y + * @returns {object} Atlas data with texture and UV mapping + */ +function createFontAtlas() { + const chars = '0123456789-XY'; + const canvas = DOC.createElement('canvas'); + const ctx = canvas.getContext('2d'); + const charSize = 64; + + canvas.width = charSize * chars.length; + canvas.height = charSize; + + // Clear to transparent + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Load Russo One font or fallback to sans-serif + ctx.font = `bold ${charSize * 0.8}px 'Russo One', sans-serif`; + ctx.fillStyle = 'white'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + + const uvMap = {}; + for (let i = 0; i < chars.length; i++) { + ctx.fillText(chars[i], i * charSize + charSize/2, charSize/2); + uvMap[chars[i]] = { + uStart: i / chars.length, + uEnd: (i + 1) / chars.length + }; + } + + const texture = new THREE.CanvasTexture(canvas); + texture.minFilter = THREE.LinearFilter; + texture.magFilter = THREE.LinearFilter; + texture.needsUpdate = true; + + console.log('Font atlas created', { width: canvas.width, height: canvas.height, chars: chars.length }); + + return { texture, uvMap, charSize }; +} + +/** + * Create 3D text label using bitmap font atlas. + * @param {string} text - Text to render + * @param {number} size - Character size + * @param {string|number} color - Text color (CSS string or hex number) + * @param {string} align - Horizontal alignment ('left', 'center', 'right') + * @returns {THREE.Group} Group containing character meshes + */ +function create3DTextLabel(text, size, color, align = 'center') { + if (!fontAtlas) { + fontAtlas = createFontAtlas(); + } + + const { texture, uvMap, charSize } = fontAtlas; + const group = new THREE.Group(); + const charWidth = size * 0.6; // Narrower spacing for better appearance + const totalWidth = text.length * charWidth; + + // Convert CSS color string to THREE.Color if needed + let threeColor = 0x333333; // default gray + if (color) { + if (typeof color === 'string') { + threeColor = new THREE.Color(color); + } else { + threeColor = color; + } + } + + // Calculate starting X based on alignment + let startX = 0; + if (align === 'center') { + startX = -totalWidth / 2; + } else if (align === 'right') { + startX = -totalWidth; + } + + // Create a mesh for each character + for (let i = 0; i < text.length; i++) { + const char = text[i]; + const uvData = uvMap[char]; + + if (!uvData) { + console.warn('Character not in atlas:', char); + continue; + } + + // Create plane geometry for this character + const geometry = new THREE.PlaneGeometry(charWidth, size); + const uvAttr = geometry.attributes.uv; + + // Set UV coordinates to show only this character from atlas + // PlaneGeometry UVs: bottom-left, bottom-right, top-left, top-right + uvAttr.setXY(0, uvData.uStart, 0); // bottom-left + uvAttr.setXY(1, uvData.uEnd, 0); // bottom-right + uvAttr.setXY(2, uvData.uStart, 1); // top-left + uvAttr.setXY(3, uvData.uEnd, 1); // top-right + + const material = new THREE.MeshBasicMaterial({ + map: texture, + transparent: true, + opacity: 0.9, + color: threeColor, + depthWrite: false, + side: THREE.DoubleSide + }); + + const mesh = new THREE.Mesh(geometry, material); + mesh.position.x = startX + i * charWidth + charWidth / 2; + + group.add(mesh); + } + + return group; +} + function setRulers( xon = ruler.xon, @@ -475,50 +598,82 @@ function updateRulers() { oldView = ruler.view, view = ruler.view = new THREE.Group(); - if (xon && axesOn) { - let xPadding = labelSize * 4, - canvas = canvasInMesh(x + xPadding, labelSize * 3, 'center', 'top', rulerColor, labelSize), - context = canvas.context, - mesh = canvas.mesh; - if (!(context && mesh)) return; + console.log('updateRulers called', { + xon, yon, axesOn, labelSize, rulerColor, factor, + x1: ruler.x1, x2: ruler.x2, y1: ruler.y1, y2: ruler.y2, + xo: ruler.xo, yo: ruler.yo, + originX: grid.origin.x, originY: grid.origin.y + }); + if (xon && axesOn) { + // Create X-axis numeric labels + // Original canvas code draws at canvas X: ruler.xo + i + xPadding/2 + // Canvas is centered at world X=0, canvas width = x + xPadding + // So world X = (canvasX - canvasWidth/2) = (ruler.xo + i + xPadding/2) - (x + xPadding)/2 + // = ruler.xo + i - x/2 = ruler.xo + i - w if (platformBelt) { for (let i = 0; i <= ruler.x2; i += grid.unitMajor) { - context.fillText((i * factor).round(1).toString(), ruler.x2 - i + xPadding / 2, 0); + const value = (i * factor).round(1).toString(); + const label = create3DTextLabel(value, labelSize, rulerColor, 'center'); + + label.position.set(ruler.x2 - i, -h - labelSize * 2, zp); + label.rotation.x = Math.PI; + view.add(label); } } else { for (let i = 0; i >= ruler.x1; i -= grid.unitMajor) { - context.fillText((i * factor).round(1).toString(), ruler.xo + i + xPadding / 2, 0); + const value = (i * factor).round(1).toString(); + const label = create3DTextLabel(value, labelSize, rulerColor, 'center'); + + label.position.set(ruler.xo + i - w, -h - labelSize * 2, zp); + label.rotation.x = Math.PI; + view.add(label); } for (let i = 0; i <= ruler.x2; i += grid.unitMajor) { - context.fillText((i * factor).round(1).toString(), ruler.xo + i + xPadding / 2, 0); + const value = (i * factor).round(1).toString(); + const label = create3DTextLabel(value, labelSize, rulerColor, 'center'); + + label.position.set(ruler.xo + i - w, -h - labelSize * 2, zp); + label.rotation.x = Math.PI; + view.add(label); } } - context.font = (labelSize * 0.75) + 'px sans-serif'; - context.fillText(xlabel, (x + xPadding) / 2, labelSize * 1.5); - mesh.position.set(0, - h - labelSize * 2, zp); - view.add(mesh); + // Create X-axis label + const xLabel = create3DTextLabel(xlabel, labelSize * 0.75, rulerColor, 'center'); + xLabel.position.set(0, -h - labelSize * 3.5, zp); + xLabel.rotation.x = Math.PI; + view.add(xLabel); } if (yon && axesOn) { - let yPadding = labelSize, - canvas = canvasInMesh(labelSize * 4, y + yPadding, 'end', 'middle', rulerColor, labelSize), - context = canvas.context, - mesh = canvas.mesh; - if (!(context && mesh)) return; - + // Create Y-axis numeric labels + // Original canvas code draws at canvas Y: y - (ruler.yo + i) + yPadding/2 + // Canvas is centered at world Y=0, canvas height = y + yPadding + // So world Y = (canvasY - canvasHeight/2) = [y - (ruler.yo + i) + yPadding/2] - (y + yPadding)/2 + // = y/2 - ruler.yo - i = h - ruler.yo - i for (let i = 0; i >= ruler.y1; i -= grid.unitMajor) { - context.fillText((i * factor).round(1), labelSize * 4, y - (ruler.yo + i) + yPadding / 2); + const value = (i * factor).round(1).toString(); + const label = create3DTextLabel(value, labelSize, rulerColor, 'right'); + + label.position.set(-w - labelSize * 2 - 5, h - ruler.yo - i, zp); + label.rotation.x = Math.PI; + view.add(label); } for (let i = 0; i <= ruler.y2; i += grid.unitMajor) { - context.fillText((i * factor).round(1), labelSize * 4, y - (ruler.yo + i) + yPadding / 2); + const value = (i * factor).round(1).toString(); + const label = create3DTextLabel(value, labelSize, rulerColor, 'right'); + + label.position.set(-w - labelSize * 2 - 5, h - ruler.yo - i, zp); + label.rotation.x = Math.PI; + view.add(label); } - context.font = (labelSize * 0.75) + 'px sans-serif'; - context.fillText(ylabel, labelSize * 1.25, (y + yPadding) / 2); - mesh.position.set(-w - labelSize * 2 - 5, 0, zp); - view.add(mesh); + // Create Y-axis label + const yLabel = create3DTextLabel(ylabel, labelSize * 0.75, rulerColor, 'center'); + yLabel.position.set(-w - labelSize * 5, 0, zp); + yLabel.rotation.x = Math.PI; + view.add(yLabel); } Space.scene.remove(oldView); From dda980b1000861975d553640244b03024b460df9 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 26 Dec 2025 10:19:27 -0500 Subject: [PATCH 046/154] encapsulate new axis labels into text3 class --- src/moto/space.js | 153 +++++--------------------------- src/moto/text3d.js | 213 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 234 insertions(+), 132 deletions(-) create mode 100644 src/moto/text3d.js diff --git a/src/moto/space.js b/src/moto/space.js index 310f1806..2d127d18 100644 --- a/src/moto/space.js +++ b/src/moto/space.js @@ -3,6 +3,7 @@ import { broker } from './broker.js'; import { THREE } from '../ext/three.js'; import { Orbit } from './orbit.js'; +import { Text3D } from './text3d.js'; import '../ext/tween.js'; const { @@ -445,127 +446,15 @@ function canvasInMesh(w, h, textAlign, textBaseline, color, size) { } /** - * Font atlas for 3D text labels. - * Generates a texture atlas with characters 0-9, -, X, Y at startup. + * 3D text renderer for grid labels. + * Uses bitmap font atlas with shared geometries for memory efficiency. */ -let fontAtlas = null; - -/** - * Create bitmap font atlas for grid labels. - * Generates texture with characters: 0-9, -, X, Y - * @returns {object} Atlas data with texture and UV mapping - */ -function createFontAtlas() { - const chars = '0123456789-XY'; - const canvas = DOC.createElement('canvas'); - const ctx = canvas.getContext('2d'); - const charSize = 64; - - canvas.width = charSize * chars.length; - canvas.height = charSize; - - // Clear to transparent - ctx.clearRect(0, 0, canvas.width, canvas.height); - - // Load Russo One font or fallback to sans-serif - ctx.font = `bold ${charSize * 0.8}px 'Russo One', sans-serif`; - ctx.fillStyle = 'white'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - - const uvMap = {}; - for (let i = 0; i < chars.length; i++) { - ctx.fillText(chars[i], i * charSize + charSize/2, charSize/2); - uvMap[chars[i]] = { - uStart: i / chars.length, - uEnd: (i + 1) / chars.length - }; - } - - const texture = new THREE.CanvasTexture(canvas); - texture.minFilter = THREE.LinearFilter; - texture.magFilter = THREE.LinearFilter; - texture.needsUpdate = true; - - console.log('Font atlas created', { width: canvas.width, height: canvas.height, chars: chars.length }); - - return { texture, uvMap, charSize }; -} - -/** - * Create 3D text label using bitmap font atlas. - * @param {string} text - Text to render - * @param {number} size - Character size - * @param {string|number} color - Text color (CSS string or hex number) - * @param {string} align - Horizontal alignment ('left', 'center', 'right') - * @returns {THREE.Group} Group containing character meshes - */ -function create3DTextLabel(text, size, color, align = 'center') { - if (!fontAtlas) { - fontAtlas = createFontAtlas(); - } - - const { texture, uvMap, charSize } = fontAtlas; - const group = new THREE.Group(); - const charWidth = size * 0.6; // Narrower spacing for better appearance - const totalWidth = text.length * charWidth; - - // Convert CSS color string to THREE.Color if needed - let threeColor = 0x333333; // default gray - if (color) { - if (typeof color === 'string') { - threeColor = new THREE.Color(color); - } else { - threeColor = color; - } - } - - // Calculate starting X based on alignment - let startX = 0; - if (align === 'center') { - startX = -totalWidth / 2; - } else if (align === 'right') { - startX = -totalWidth; - } - - // Create a mesh for each character - for (let i = 0; i < text.length; i++) { - const char = text[i]; - const uvData = uvMap[char]; - - if (!uvData) { - console.warn('Character not in atlas:', char); - continue; - } - - // Create plane geometry for this character - const geometry = new THREE.PlaneGeometry(charWidth, size); - const uvAttr = geometry.attributes.uv; - - // Set UV coordinates to show only this character from atlas - // PlaneGeometry UVs: bottom-left, bottom-right, top-left, top-right - uvAttr.setXY(0, uvData.uStart, 0); // bottom-left - uvAttr.setXY(1, uvData.uEnd, 0); // bottom-right - uvAttr.setXY(2, uvData.uStart, 1); // top-left - uvAttr.setXY(3, uvData.uEnd, 1); // top-right - - const material = new THREE.MeshBasicMaterial({ - map: texture, - transparent: true, - opacity: 0.9, - color: threeColor, - depthWrite: false, - side: THREE.DoubleSide - }); - - const mesh = new THREE.Mesh(geometry, material); - mesh.position.x = startX + i * charWidth + charWidth / 2; - - group.add(mesh); - } - - return group; -} +let text3d = new Text3D({ + chars: '0123456789-XY', + atlasCharSize: 64, + kerning: 0.5, // Tighter spacing than default + fontFamily: "'Russo One', sans-serif" +}); function setRulers( @@ -598,7 +487,7 @@ function updateRulers() { oldView = ruler.view, view = ruler.view = new THREE.Group(); - console.log('updateRulers called', { + if (false) console.log('updateRulers called', { xon, yon, axesOn, labelSize, rulerColor, factor, x1: ruler.x1, x2: ruler.x2, y1: ruler.y1, y2: ruler.y2, xo: ruler.xo, yo: ruler.yo, @@ -614,7 +503,7 @@ function updateRulers() { if (platformBelt) { for (let i = 0; i <= ruler.x2; i += grid.unitMajor) { const value = (i * factor).round(1).toString(); - const label = create3DTextLabel(value, labelSize, rulerColor, 'center'); + const label = text3d.createLabel(value, labelSize, rulerColor, 'center'); label.position.set(ruler.x2 - i, -h - labelSize * 2, zp); label.rotation.x = Math.PI; @@ -623,7 +512,7 @@ function updateRulers() { } else { for (let i = 0; i >= ruler.x1; i -= grid.unitMajor) { const value = (i * factor).round(1).toString(); - const label = create3DTextLabel(value, labelSize, rulerColor, 'center'); + const label = text3d.createLabel(value, labelSize, rulerColor, 'center'); label.position.set(ruler.xo + i - w, -h - labelSize * 2, zp); label.rotation.x = Math.PI; @@ -631,7 +520,7 @@ function updateRulers() { } for (let i = 0; i <= ruler.x2; i += grid.unitMajor) { const value = (i * factor).round(1).toString(); - const label = create3DTextLabel(value, labelSize, rulerColor, 'center'); + const label = text3d.createLabel(value, labelSize, rulerColor, 'center'); label.position.set(ruler.xo + i - w, -h - labelSize * 2, zp); label.rotation.x = Math.PI; @@ -640,7 +529,7 @@ function updateRulers() { } // Create X-axis label - const xLabel = create3DTextLabel(xlabel, labelSize * 0.75, rulerColor, 'center'); + const xLabel = text3d.createLabel(xlabel, labelSize * 0.75, rulerColor, 'center'); xLabel.position.set(0, -h - labelSize * 3.5, zp); xLabel.rotation.x = Math.PI; view.add(xLabel); @@ -650,27 +539,27 @@ function updateRulers() { // Create Y-axis numeric labels // Original canvas code draws at canvas Y: y - (ruler.yo + i) + yPadding/2 // Canvas is centered at world Y=0, canvas height = y + yPadding - // So world Y = (canvasY - canvasHeight/2) = [y - (ruler.yo + i) + yPadding/2] - (y + yPadding)/2 - // = y/2 - ruler.yo - i = h - ruler.yo - i + // Canvas Y goes down, but world Y goes up, so we need to negate + // World Y = -(canvasY - canvasHeight/2) = -(h - ruler.yo - i) = -h + ruler.yo + i for (let i = 0; i >= ruler.y1; i -= grid.unitMajor) { const value = (i * factor).round(1).toString(); - const label = create3DTextLabel(value, labelSize, rulerColor, 'right'); + const label = text3d.createLabel(value, labelSize, rulerColor, 'right'); - label.position.set(-w - labelSize * 2 - 5, h - ruler.yo - i, zp); + label.position.set(-w - labelSize * 2 - 5, -h + ruler.yo + i, zp); label.rotation.x = Math.PI; view.add(label); } for (let i = 0; i <= ruler.y2; i += grid.unitMajor) { const value = (i * factor).round(1).toString(); - const label = create3DTextLabel(value, labelSize, rulerColor, 'right'); + const label = text3d.createLabel(value, labelSize, rulerColor, 'right'); - label.position.set(-w - labelSize * 2 - 5, h - ruler.yo - i, zp); + label.position.set(-w - labelSize * 2 - 5, -h + ruler.yo + i, zp); label.rotation.x = Math.PI; view.add(label); } // Create Y-axis label - const yLabel = create3DTextLabel(ylabel, labelSize * 0.75, rulerColor, 'center'); + const yLabel = text3d.createLabel(ylabel, labelSize * 0.75, rulerColor, 'center'); yLabel.position.set(-w - labelSize * 5, 0, zp); yLabel.rotation.x = Math.PI; view.add(yLabel); diff --git a/src/moto/text3d.js b/src/moto/text3d.js new file mode 100644 index 00000000..4a4f6aff --- /dev/null +++ b/src/moto/text3d.js @@ -0,0 +1,213 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { THREE } from '../ext/three.js'; + +/** + * 3D Bitmap Text Renderer using font atlas. + * Creates a texture atlas from specified characters and renders text using + * individual character meshes with shared geometries and material. + */ +class Text3D { + /** + * Create a 3D text renderer. + * @param {object} options - Configuration options + * @param {string} options.chars - Characters to include in atlas (default: '0123456789-XY') + * @param {number} options.atlasCharSize - Size of each character in atlas texture (default: 64) + * @param {number} options.kerning - Character spacing multiplier (default: 0.5, lower = tighter) + * @param {string} options.fontFamily - CSS font family string (default: 'Russo One, sans-serif') + */ + constructor(options = {}) { + this.chars = options.chars || '0123456789-XY'; + this.atlasCharSize = options.atlasCharSize || 64; + this.kerning = options.kerning !== undefined ? options.kerning : 0.5; + this.fontFamily = options.fontFamily || "'Russo One', sans-serif"; + + this.atlas = null; + this.geometries = {}; // Cached geometries per character + this.material = null; // Shared material for all characters + + this._initialize(); + } + + /** + * Initialize atlas, geometries, and material. + * @private + */ + _initialize() { + this._createAtlas(); + this._createGeometries(); + this._createMaterial(); + } + + /** + * Create bitmap font atlas texture. + * @private + */ + _createAtlas() { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + const charSize = this.atlasCharSize; + + canvas.width = charSize * this.chars.length; + canvas.height = charSize; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.font = `bold ${charSize * 0.8}px ${this.fontFamily}`; + ctx.fillStyle = 'white'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + + const uvMap = {}; + for (let i = 0; i < this.chars.length; i++) { + const char = this.chars[i]; + ctx.fillText(char, i * charSize + charSize/2, charSize/2); + uvMap[char] = { + uStart: i / this.chars.length, + uEnd: (i + 1) / this.chars.length + }; + } + + const texture = new THREE.CanvasTexture(canvas); + texture.minFilter = THREE.LinearFilter; + texture.magFilter = THREE.LinearFilter; + texture.needsUpdate = true; + + this.atlas = { texture, uvMap, canvas }; + + if (self.debug) { + console.log('Text3D atlas created', { + width: canvas.width, + height: canvas.height, + chars: this.chars.length + }); + } + } + + /** + * Create cached geometries for each character. + * Each character gets one geometry with UVs mapped to atlas position. + * These geometries are reused across all labels. + * @private + */ + _createGeometries() { + const { uvMap } = this.atlas; + + for (let char of this.chars) { + const uvData = uvMap[char]; + if (!uvData) continue; + + // Create unit-sized plane (will be scaled when used) + const geometry = new THREE.PlaneGeometry(1, 1); + const uvAttr = geometry.attributes.uv; + + // Set UV coordinates to sample this character from atlas + uvAttr.setXY(0, uvData.uStart, 0); // bottom-left + uvAttr.setXY(1, uvData.uEnd, 0); // bottom-right + uvAttr.setXY(2, uvData.uStart, 1); // top-left + uvAttr.setXY(3, uvData.uEnd, 1); // top-right + + this.geometries[char] = geometry; + } + } + + /** + * Create shared material for all characters. + * @private + */ + _createMaterial() { + this.material = new THREE.MeshBasicMaterial({ + map: this.atlas.texture, + transparent: true, + opacity: 0.9, + depthWrite: false, + side: THREE.DoubleSide + }); + } + + /** + * Create a 3D text label. + * @param {string} text - Text to render + * @param {number} size - Character height in world units + * @param {string|number} color - Text color (CSS string or hex number) + * @param {string} align - Horizontal alignment ('left', 'center', 'right') + * @param {number} kerning - Optional kerning override for this label + * @returns {THREE.Group} Group containing character meshes + */ + createLabel(text, size, color = 0x333333, align = 'center', kerning) { + const group = new THREE.Group(); + const spacing = kerning !== undefined ? kerning : this.kerning; + const charWidth = size * spacing; + const totalWidth = text.length * charWidth; + + // Convert CSS color string to THREE.Color if needed + let threeColor = color; + if (typeof color === 'string') { + threeColor = new THREE.Color(color); + } + + // Calculate starting X based on alignment + let startX = 0; + if (align === 'center') { + startX = -totalWidth / 2; + } else if (align === 'right') { + startX = -totalWidth; + } + + // Create mesh for each character using cached geometry + for (let i = 0; i < text.length; i++) { + const char = text[i]; + const geometry = this.geometries[char]; + + if (!geometry) { + if (self.debug) { + console.warn('Text3D: Character not in atlas:', char); + } + continue; + } + + // Clone material to set per-label color + const material = this.material.clone(); + material.color = threeColor; + + const mesh = new THREE.Mesh(geometry, material); + + // Scale and position the unit-sized geometry + mesh.scale.set(charWidth, size, 1); + mesh.position.x = startX + i * charWidth + charWidth / 2; + + group.add(mesh); + } + + return group; + } + + /** + * Update kerning for future labels. + * @param {number} kerning - New kerning value + */ + setKerning(kerning) { + this.kerning = kerning; + } + + /** + * Clean up resources. + */ + dispose() { + // Dispose texture + if (this.atlas && this.atlas.texture) { + this.atlas.texture.dispose(); + } + + // Dispose geometries + for (let char in this.geometries) { + this.geometries[char].dispose(); + } + + // Dispose material + if (this.material) { + this.material.dispose(); + } + } +} + +export { Text3D }; From c6dd4c304c085d5ec95209e940e2d11823e9dee5 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 26 Dec 2025 10:28:54 -0500 Subject: [PATCH 047/154] add text scaling, kerning --- src/moto/space.js | 10 +++----- src/moto/text3d.js | 58 +++++++++++++++++++++++++++++++++++----------- 2 files changed, 48 insertions(+), 20 deletions(-) diff --git a/src/moto/space.js b/src/moto/space.js index 2d127d18..927b7722 100644 --- a/src/moto/space.js +++ b/src/moto/space.js @@ -451,8 +451,9 @@ function canvasInMesh(w, h, textAlign, textBaseline, color, size) { */ let text3d = new Text3D({ chars: '0123456789-XY', - atlasCharSize: 64, - kerning: 0.5, // Tighter spacing than default + charSize: 64, + kerning: 0.6, + scaleX: 0.7, fontFamily: "'Russo One', sans-serif" }); @@ -504,7 +505,6 @@ function updateRulers() { for (let i = 0; i <= ruler.x2; i += grid.unitMajor) { const value = (i * factor).round(1).toString(); const label = text3d.createLabel(value, labelSize, rulerColor, 'center'); - label.position.set(ruler.x2 - i, -h - labelSize * 2, zp); label.rotation.x = Math.PI; view.add(label); @@ -513,7 +513,6 @@ function updateRulers() { for (let i = 0; i >= ruler.x1; i -= grid.unitMajor) { const value = (i * factor).round(1).toString(); const label = text3d.createLabel(value, labelSize, rulerColor, 'center'); - label.position.set(ruler.xo + i - w, -h - labelSize * 2, zp); label.rotation.x = Math.PI; view.add(label); @@ -521,7 +520,6 @@ function updateRulers() { for (let i = 0; i <= ruler.x2; i += grid.unitMajor) { const value = (i * factor).round(1).toString(); const label = text3d.createLabel(value, labelSize, rulerColor, 'center'); - label.position.set(ruler.xo + i - w, -h - labelSize * 2, zp); label.rotation.x = Math.PI; view.add(label); @@ -544,7 +542,6 @@ function updateRulers() { for (let i = 0; i >= ruler.y1; i -= grid.unitMajor) { const value = (i * factor).round(1).toString(); const label = text3d.createLabel(value, labelSize, rulerColor, 'right'); - label.position.set(-w - labelSize * 2 - 5, -h + ruler.yo + i, zp); label.rotation.x = Math.PI; view.add(label); @@ -552,7 +549,6 @@ function updateRulers() { for (let i = 0; i <= ruler.y2; i += grid.unitMajor) { const value = (i * factor).round(1).toString(); const label = text3d.createLabel(value, labelSize, rulerColor, 'right'); - label.position.set(-w - labelSize * 2 - 5, -h + ruler.yo + i, zp); label.rotation.x = Math.PI; view.add(label); diff --git a/src/moto/text3d.js b/src/moto/text3d.js index 4a4f6aff..81d26a3d 100644 --- a/src/moto/text3d.js +++ b/src/moto/text3d.js @@ -12,20 +12,22 @@ class Text3D { * Create a 3D text renderer. * @param {object} options - Configuration options * @param {string} options.chars - Characters to include in atlas (default: '0123456789-XY') - * @param {number} options.atlasCharSize - Size of each character in atlas texture (default: 64) + * @param {number} options.charSize - Size of each character in atlas texture (default: 64) * @param {number} options.kerning - Character spacing multiplier (default: 0.5, lower = tighter) - * @param {string} options.fontFamily - CSS font family string (default: 'Russo One, sans-serif') + * @param {number} options.scaleX - Default horizontal scale (default: 1.0) + * @param {number} options.scaleY - Default vertical scale (default: 1.0) + * @param {string} options.fontFamily - CSS font family string (default: 'sans-serif') */ constructor(options = {}) { this.chars = options.chars || '0123456789-XY'; - this.atlasCharSize = options.atlasCharSize || 64; + this.charSize = options.charSize || 64; this.kerning = options.kerning !== undefined ? options.kerning : 0.5; - this.fontFamily = options.fontFamily || "'Russo One', sans-serif"; - + this.scaleX = options.scaleX !== undefined ? options.scaleX : 1.0; + this.scaleY = options.scaleY !== undefined ? options.scaleY : 1.0; + this.fontFamily = options.fontFamily || "sans-serif"; this.atlas = null; this.geometries = {}; // Cached geometries per character this.material = null; // Shared material for all characters - this._initialize(); } @@ -46,7 +48,7 @@ class Text3D { _createAtlas() { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); - const charSize = this.atlasCharSize; + const charSize = this.charSize; canvas.width = charSize * this.chars.length; canvas.height = charSize; @@ -131,13 +133,17 @@ class Text3D { * @param {string|number} color - Text color (CSS string or hex number) * @param {string} align - Horizontal alignment ('left', 'center', 'right') * @param {number} kerning - Optional kerning override for this label + * @param {number} scaleX - Optional horizontal scale override for this label + * @param {number} scaleY - Optional vertical scale override for this label * @returns {THREE.Group} Group containing character meshes */ - createLabel(text, size, color = 0x333333, align = 'center', kerning) { + createLabel(text, size, color = 0x333333, align = 'center', kerning, scaleX, scaleY) { const group = new THREE.Group(); const spacing = kerning !== undefined ? kerning : this.kerning; - const charWidth = size * spacing; - const totalWidth = text.length * charWidth; + const sx = scaleX !== undefined ? scaleX : this.scaleX; + const sy = scaleY !== undefined ? scaleY : this.scaleY; + const charSpacing = size * spacing * sx; // Distance between character centers (account for width scale) + const totalWidth = text.length * charSpacing; // Convert CSS color string to THREE.Color if needed let threeColor = color; @@ -171,9 +177,9 @@ class Text3D { const mesh = new THREE.Mesh(geometry, material); - // Scale and position the unit-sized geometry - mesh.scale.set(charWidth, size, 1); - mesh.position.x = startX + i * charWidth + charWidth / 2; + // Apply size and scale + mesh.scale.set(size * sx, size * sy, 1); + mesh.position.x = startX + i * charSpacing + charSpacing / 2; group.add(mesh); } @@ -189,6 +195,32 @@ class Text3D { this.kerning = kerning; } + /** + * Update horizontal scale for future labels. + * @param {number} scaleX - New horizontal scale value + */ + setScaleX(scaleX) { + this.scaleX = scaleX; + } + + /** + * Update vertical scale for future labels. + * @param {number} scaleY - New vertical scale value + */ + setScaleY(scaleY) { + this.scaleY = scaleY; + } + + /** + * Update both scales for future labels. + * @param {number} scaleX - New horizontal scale value + * @param {number} scaleY - New vertical scale value + */ + setScale(scaleX, scaleY) { + this.scaleX = scaleX; + this.scaleY = scaleY; + } + /** * Clean up resources. */ From f7edbad51dbc778d08541e4121079ac9a7203291 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 26 Dec 2025 10:40:14 -0500 Subject: [PATCH 048/154] rm dead code, workaround space+text3 inclusion in worker --- src/moto/space.js | 43 ++----------------------------------------- web/kiri/index.css | 6 ++++-- 2 files changed, 6 insertions(+), 43 deletions(-) diff --git a/src/moto/space.js b/src/moto/space.js index 927b7722..be6f84fa 100644 --- a/src/moto/space.js +++ b/src/moto/space.js @@ -407,56 +407,17 @@ function showVolume(bool) { requestRefresh(); } -function canvasInMesh(w, h, textAlign, textBaseline, color, size) { - let canvas = document.createElement('canvas'), - canvasTexture = new THREE.CanvasTexture(canvas), - plane = new THREE.PlaneGeometry(w, h), - context = canvas.getContext('2d'), - scale = 8; - - canvas.width = w * scale; - canvas.height = h * scale; - - canvas.addEventListener("webglcontextlost", event => { - console.log('WEB GL CONTEXT LOST'); - event.preventDefault(); - }, false); - - canvas.addEventListener("webglcontextrestored", event => { - console.log('WEB GL CONTEXT RESTORED'); - }, false); - - try { - context.scale(scale, scale); - } catch (e) { - console.log('unable to create label canvas', e ? e.name : 'unknown'); - return {}; - } - context.fillStyle = color || fontColor; - context.font = `${size}px sans-serif`; - context.textAlign = textAlign; - context.textBaseline = textBaseline; - canvasTexture.minFilter = THREE.LinearFilter; - - // set 'transparent' to false to debug mesh bounds - let material = new THREE.MeshBasicMaterial({transparent: true, map: canvasTexture}); - let mesh = new THREE.Mesh(plane, material); - - return { context, mesh }; -} - /** * 3D text renderer for grid labels. * Uses bitmap font atlas with shared geometries for memory efficiency. */ -let text3d = new Text3D({ +let text3d = self.document ? new Text3D({ chars: '0123456789-XY', charSize: 64, kerning: 0.6, scaleX: 0.7, fontFamily: "'Russo One', sans-serif" -}); - +}) : undefined; function setRulers( xon = ruler.xon, diff --git a/web/kiri/index.css b/web/kiri/index.css index 1536e407..5f2491de 100644 --- a/web/kiri/index.css +++ b/web/kiri/index.css @@ -420,6 +420,8 @@ details[open] summary::after { } .dark #rnfo { color: #aaa; + border: 1px solid rgba(255,255,255,0.25); + background-color: rgba(0,0,0,0.75); } .dark #rnfo label { color: #ada; @@ -2550,8 +2552,8 @@ details[open] summary::after { bottom: 100%; gap: 3px; right: 0; - background-color: rgba(255,255,255,0.05); - border: 1px solid black; + background-color: rgba(255,255,255,0.75); + border: 1px solid rgba(0,0,0,0.5); border-radius: 3px; padding: 5px; } From ba294c34b107c5ac600b376a428cce6520b119fc Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 26 Dec 2025 12:54:23 -0500 Subject: [PATCH 049/154] tighten label to platform, remove belt specific render --- src/moto/space.js | 47 ++++++++++++++++++----------------------------- 1 file changed, 18 insertions(+), 29 deletions(-) diff --git a/src/moto/space.js b/src/moto/space.js index be6f84fa..90122faf 100644 --- a/src/moto/space.js +++ b/src/moto/space.js @@ -1,6 +1,5 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { broker } from './broker.js'; import { THREE } from '../ext/three.js'; import { Orbit } from './orbit.js'; import { Text3D } from './text3d.js'; @@ -462,33 +461,23 @@ function updateRulers() { // Canvas is centered at world X=0, canvas width = x + xPadding // So world X = (canvasX - canvasWidth/2) = (ruler.xo + i + xPadding/2) - (x + xPadding)/2 // = ruler.xo + i - x/2 = ruler.xo + i - w - if (platformBelt) { - for (let i = 0; i <= ruler.x2; i += grid.unitMajor) { - const value = (i * factor).round(1).toString(); - const label = text3d.createLabel(value, labelSize, rulerColor, 'center'); - label.position.set(ruler.x2 - i, -h - labelSize * 2, zp); - label.rotation.x = Math.PI; - view.add(label); - } - } else { - for (let i = 0; i >= ruler.x1; i -= grid.unitMajor) { - const value = (i * factor).round(1).toString(); - const label = text3d.createLabel(value, labelSize, rulerColor, 'center'); - label.position.set(ruler.xo + i - w, -h - labelSize * 2, zp); - label.rotation.x = Math.PI; - view.add(label); - } - for (let i = 0; i <= ruler.x2; i += grid.unitMajor) { - const value = (i * factor).round(1).toString(); - const label = text3d.createLabel(value, labelSize, rulerColor, 'center'); - label.position.set(ruler.xo + i - w, -h - labelSize * 2, zp); - label.rotation.x = Math.PI; - view.add(label); - } + for (let i = 0; i >= ruler.x1; i -= grid.unitMajor) { + const value = (i * factor).round(1).toString(); + const label = text3d.createLabel(value, labelSize, rulerColor, 'center'); + label.position.set(ruler.xo + i - w, -h - labelSize, zp); + label.rotation.x = Math.PI; + view.add(label); + } + for (let i = 0; i <= ruler.x2; i += grid.unitMajor) { + const value = (i * factor).round(1).toString(); + const label = text3d.createLabel(value, labelSize, rulerColor, 'center'); + label.position.set(ruler.xo + i - w, -h - labelSize, zp); + label.rotation.x = Math.PI; + view.add(label); } // Create X-axis label - const xLabel = text3d.createLabel(xlabel, labelSize * 0.75, rulerColor, 'center'); + const xLabel = text3d.createLabel(xlabel, labelSize, rulerColor, 'center'); xLabel.position.set(0, -h - labelSize * 3.5, zp); xLabel.rotation.x = Math.PI; view.add(xLabel); @@ -503,21 +492,21 @@ function updateRulers() { for (let i = 0; i >= ruler.y1; i -= grid.unitMajor) { const value = (i * factor).round(1).toString(); const label = text3d.createLabel(value, labelSize, rulerColor, 'right'); - label.position.set(-w - labelSize * 2 - 5, -h + ruler.yo + i, zp); + label.position.set(-w - labelSize + 3, -h + ruler.yo + i, zp); label.rotation.x = Math.PI; view.add(label); } for (let i = 0; i <= ruler.y2; i += grid.unitMajor) { const value = (i * factor).round(1).toString(); const label = text3d.createLabel(value, labelSize, rulerColor, 'right'); - label.position.set(-w - labelSize * 2 - 5, -h + ruler.yo + i, zp); + label.position.set(-w - labelSize + 3, -h + ruler.yo + i, zp); label.rotation.x = Math.PI; view.add(label); } // Create Y-axis label - const yLabel = text3d.createLabel(ylabel, labelSize * 0.75, rulerColor, 'center'); - yLabel.position.set(-w - labelSize * 5, 0, zp); + const yLabel = text3d.createLabel(ylabel, labelSize, rulerColor, 'center'); + yLabel.position.set(-w - labelSize * 4, 0, zp); yLabel.rotation.x = Math.PI; view.add(yLabel); } From f7125661ef12c63ac8859378818a1a29732e8f34 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 26 Dec 2025 19:02:05 -0500 Subject: [PATCH 050/154] add dev server and beta warnings --- mods/devel/kiri.js | 6 ++++++ src/kiri/app/init/sync.js | 4 +--- src/moto/license.js | 2 +- web/kiri/index.css | 3 +++ 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/mods/devel/kiri.js b/mods/devel/kiri.js index 514a25bb..47ad33cb 100644 --- a/mods/devel/kiri.js +++ b/mods/devel/kiri.js @@ -1,6 +1,12 @@ // provides a device export to local disk option self.kiri.load(api => { api.event.on('load-done', () => { + let devwarn = api.sdb.kiri_dev; + if (location.host === 'dev.grid.space' && devwarn !== api.version) { + api.alerts.show('this is a development server', 10); + api.alerts.show('use grid.space for production', 10); + api.sdb.kiri_dev = api.version; + } let deviceExport = api.device.export; api.device.export = (exp, name, opt = {}) => { const { event, record } = opt; diff --git a/src/kiri/app/init/sync.js b/src/kiri/app/init/sync.js index bff4f9c9..fcf42801 100644 --- a/src/kiri/app/init/sync.js +++ b/src/kiri/app/init/sync.js @@ -111,9 +111,7 @@ export async function init_sync() { // warn users they are running a beta release if (beta && beta > 0 && sdb.kiri_beta != beta) { - api.show.alert("CAUTION"); - api.show.alert("this is a development release"); - api.show.alert("and may not function properly"); + api.show.alert('caution: beta code ahead', 10); sdb.kiri_beta = beta; } diff --git a/src/moto/license.js b/src/moto/license.js index 83c6af45..d307e578 100644 --- a/src/moto/license.js +++ b/src/moto/license.js @@ -6,6 +6,6 @@ const terms = { VERSION: "4.6.0" }; -export const beta = 0; +export const beta = 4600; export const license = terms; export const version = terms.VERSION; diff --git a/web/kiri/index.css b/web/kiri/index.css index 5f2491de..60b2a680 100644 --- a/web/kiri/index.css +++ b/web/kiri/index.css @@ -279,6 +279,9 @@ details[open] summary::after { background-color: var(--blue-2); color: black; } +.dark #alert-border:hover { + background-color: rgba(255,255,255,0.7); +} .dark #progress { background-color: #333; border-top: 1px solid #555; From 89eac1edcbaae7a359e02c2e770c3486b762db2b Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 26 Dec 2025 19:07:32 -0500 Subject: [PATCH 051/154] move dev server warning into mainline --- mods/devel/kiri.js | 6 ------ src/kiri/app/init/sync.js | 8 ++++++++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/mods/devel/kiri.js b/mods/devel/kiri.js index 47ad33cb..514a25bb 100644 --- a/mods/devel/kiri.js +++ b/mods/devel/kiri.js @@ -1,12 +1,6 @@ // provides a device export to local disk option self.kiri.load(api => { api.event.on('load-done', () => { - let devwarn = api.sdb.kiri_dev; - if (location.host === 'dev.grid.space' && devwarn !== api.version) { - api.alerts.show('this is a development server', 10); - api.alerts.show('use grid.space for production', 10); - api.sdb.kiri_dev = api.version; - } let deviceExport = api.device.export; api.device.export = (exp, name, opt = {}) => { const { event, record } = opt; diff --git a/src/kiri/app/init/sync.js b/src/kiri/app/init/sync.js index fcf42801..2d34f670 100644 --- a/src/kiri/app/init/sync.js +++ b/src/kiri/app/init/sync.js @@ -115,6 +115,14 @@ export async function init_sync() { sdb.kiri_beta = beta; } + // warn users they are using a development server + let devwarn = sdb.kiri_dev; + if (location.host === 'dev.grid.space' && devwarn !== version) { + api.alerts.show('this is a development server', 10); + api.alerts.show('use grid.space for production', 10); + sdb.kiri_dev = version; + } + // hide url params but preserve version root (when present) let wlp = WIN.location.pathname; let kio = wlp.indexOf('/kiri/'); From b735a3abefa91e8ee0d85aba132c133954b76744 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 26 Dec 2025 21:46:49 -0500 Subject: [PATCH 052/154] fix starting offset with multiple widgets --- src/kiri/app/platform.js | 6 +++--- src/kiri/mode/cam/export.js | 1 + src/kiri/mode/cam/prepare.js | 30 +++++++++++++++++++++++++++--- web/kiri/index.html | 2 +- 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/kiri/app/platform.js b/src/kiri/app/platform.js index 701c67d6..8523632c 100644 --- a/src/kiri/app/platform.js +++ b/src/kiri/app/platform.js @@ -3,9 +3,9 @@ import { $, h } from '../../moto/webui.js'; import { ajax, js2o } from '../core/utils.js'; import { api } from './api.js'; -import { base } from '../../geo/base.js'; import { COLOR, MODES } from '../core/consts.js'; import { load as file_load } from '../../load/file.js'; +import { newBounds } from '../../geo/bounds.js'; import { Packer } from './pack.js'; import { space } from '../../moto/space.js'; import { THREE } from '../../ext/three.js'; @@ -611,8 +611,8 @@ function positionNewWidget(widget) { x: dim.x / 2, y: dim.y / 2 }; - const bounds = base.newBounds(); - const target = base.newBounds(); + const bounds = newBounds(); + const target = newBounds(); const DEG2RAD = Math.PI / 180; const WIDGETS = api.widgets.all(); // look for best position for new widget that doesn't collide diff --git a/src/kiri/mode/cam/export.js b/src/kiri/mode/cam/export.js index 7dfb657c..b9206f9b 100644 --- a/src/kiri/mode/cam/export.js +++ b/src/kiri/mode/cam/export.js @@ -416,6 +416,7 @@ export function cam_export(print, online) { const isOffset = offset && (offset.x || offset.y || offset.z); // remap points as necessary for origins, offsets, inversions print.output.forEach(layer => { + // console.log(layer); if (!Array.isArray(layer)) { return; } diff --git a/src/kiri/mode/cam/prepare.js b/src/kiri/mode/cam/prepare.js index 8da97d71..e1ecbac2 100644 --- a/src/kiri/mode/cam/prepare.js +++ b/src/kiri/mode/cam/prepare.js @@ -82,7 +82,8 @@ export async function cam_prepare(widgets, settings, update) { // process `prepare` paths for a single widget export async function prepare_one(widget, settings, print, firstPoint, update) { - let { device, process } = settings, + let { device, process, stock: set_stock } = settings, + { center } = set_stock, { alignTop } = settings.controller, { camArcEnabled, camArcResolution, camArcTolerance } = process, { camDepthFirst, camEaseAngle, camEaseDown } = process, @@ -144,6 +145,21 @@ export async function prepare_one(widget, settings, print, firstPoint, update) { if (debug) console.log({ zSafe, wmx, wmy, wmz }); + // function d(o) { + // console.log('<-------------------'); + // for (let [k,v] of Object.entries(o)) console.log(k,v); + // console.log('------------------->'); + // } + + // d({ + // stock, + // sstck: set_stock, + // center, + // origin, + // wmx, + // wmy + // }); + function newLayer(op) { if (layerOut.length || layerOut.mode) { newOutput.push(layerOut); @@ -850,7 +866,15 @@ export async function prepare_one(widget, settings, print, firstPoint, update) { // coming from a previous widget, use previous last point as starting point // make top start offset configurable - printPoint = firstPoint || origin; + if (firstPoint) { + // we're coming from another widget. offset compensated below + printPoint = firstPoint; + // console.log('coming from another widget', { printPoint }); + } else { + // we're the first widget output. offset is center + printPoint = origin.clone().move({ x: center.x, y: center.y }); + // console.log('first widget output', { printPoint }); + } let ops = { addGCode, @@ -910,7 +934,7 @@ export async function prepare_one(widget, settings, print, firstPoint, update) { update((opSum + (progress * weight)) / opTot, message || op.type(), message); }); opSum += weight; - if (tool && printPoint) { + if (tool && printPoint && cop.type !== 'shadow') { newLayer(); if (!isIndex) { layerPush(printPoint.clone().setZ(stockZClear), 0, 0, tool); diff --git a/web/kiri/index.html b/web/kiri/index.html index a06b6b89..c0890b03 100644 --- a/web/kiri/index.html +++ b/web/kiri/index.html @@ -692,7 +692,7 @@
From 4a472699725d351904549a8070f9158a2622e00e Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 26 Dec 2025 21:53:58 -0500 Subject: [PATCH 053/154] fix import slider, click on slider max --- src/kiri/app/slider.js | 2 +- src/kiri/app/visuals.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/kiri/app/slider.js b/src/kiri/app/slider.js index 4a84bedd..2d5d2bd8 100644 --- a/src/kiri/app/slider.js +++ b/src/kiri/app/slider.js @@ -164,7 +164,7 @@ class SliderControl { }; this.#slider.max.onclick = () => { - this.#layerLo = this.#layerMax; + this.#layerLo = 0; this.#layerHi = this.#layerMax; this.showLabels(); this.#updateVisuals(); diff --git a/src/kiri/app/visuals.js b/src/kiri/app/visuals.js index a2f0d9da..1b988120 100644 --- a/src/kiri/app/visuals.js +++ b/src/kiri/app/visuals.js @@ -145,7 +145,7 @@ function updateSliderMax(set) { api.slider.setMax(max); const { hi } = api.slider.getRange(); if (set || max < hi) { - api.slider.setRange(max, max); + api.slider.setRange(0, max); api.slider.showLabels(); } } From c279e1140b1d8afab14ef21f8ae1032479d9c5bf Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sat, 27 Dec 2025 00:16:39 -0500 Subject: [PATCH 054/154] clip-to stock only for non-gpu topo3 contouring --- src/kiri/app/conf/defaults.js | 1 - src/kiri/mode/cam/cl-ops.js | 6 ++++++ src/kiri/mode/cam/init-menu.js | 2 -- src/kiri/mode/cam/slice.js | 6 +++--- src/kiri/mode/cam/topo3.js | 2 +- web/kiri/lang/en.js | 4 ++-- 6 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/kiri/app/conf/defaults.js b/src/kiri/app/conf/defaults.js index 3e3a3c89..b8f7f438 100644 --- a/src/kiri/app/conf/defaults.js +++ b/src/kiri/app/conf/defaults.js @@ -614,7 +614,6 @@ export const conf = { camZBottom: 0, camZClearance: 1, camZOffset: 0, - // camZThru: 0, camZTop: 0, op2: [], // flip ops ops: [], // current ops diff --git a/src/kiri/mode/cam/cl-ops.js b/src/kiri/mode/cam/cl-ops.js index 0f90ba55..df9c612b 100644 --- a/src/kiri/mode/cam/cl-ops.js +++ b/src/kiri/mode/cam/cl-ops.js @@ -28,6 +28,10 @@ function hasSpindle() { return env.current.device.spindleMax > 0; } +function isWebGPU() { + return api.conf.get().controller.webGPU; +} + function zTop() { return api.conf.get().process.camZTop > 0; } @@ -317,6 +321,7 @@ export function createPopOps() { bottom: 'camContourBottom', curves: 'camContourCurves', inside: 'camContourIn', + clipto: 'camStockClipTo', filter: 'camContourFilter', axis: 'X' }).inputs = { @@ -338,6 +343,7 @@ export function createPopOps() { curves: UC.newBoolean(LANG.cf_curv_s, undefined, { title: LANG.cf_curv_l }), inside: UC.newBoolean(LANG.cf_olin_s, undefined, { title: LANG.cf_olin_l }), bottom: UC.newBoolean(LANG.cf_botm_s, undefined, { title: LANG.cf_botm_l, show: (op, conf) => conf ? conf.process.camZBottom : 0 }), + clipto: UC.newBoolean(LANG.cf_clip_s, undefined, { title:LANG.cf_clip_l, show: () => !isWebGPU() }), filter: UC.newRow([UC.newButton(LANG.filter, contourFilter)], { class: "ext-buttons f-row" }) }; diff --git a/src/kiri/mode/cam/init-menu.js b/src/kiri/mode/cam/init-menu.js index c83999b6..5b660b2e 100644 --- a/src/kiri/mode/cam/init-menu.js +++ b/src/kiri/mode/cam/init-menu.js @@ -112,7 +112,6 @@ export function menu() { camStockZ: newInput(LANG.cs_hght_s, {title:LANG.cs_hght_l, convert:toFloat, bound:bound(0,9999), units}), separator: newBlank({ class:"set-sep", driven }), camStockOffset: newBoolean(LANG.cs_offs_s, onBooleanClick, {title:LANG.cs_offs_l}), - camStockClipTo: newBoolean(LANG.cs_clip_s, onBooleanClick, {title:LANG.cs_clip_l}), camStockIndexed: newBoolean(LANG.cs_indx_s, onBooleanClick, {title:LANG.cs_indx_l}), camStockIndexGrid: newBoolean(LANG.cs_ishg_s, onBooleanClick, {title:LANG.cs_ishg_l, show:() => ui.camStockIndexed.checked}), _____: newGroup(LANG.cc_menu, $('cam-limits'), { modes:CAM, driven, separator }), @@ -120,7 +119,6 @@ export function menu() { camZOffset: newInput(LANG.ou_ztof_s, {title:LANG.ou_ztof_l, convert:toFloat, units}), camZTop: newInput(LANG.ou_ztop_s, {title:LANG.ou_ztop_l, convert:toFloat, units, trigger}), camZBottom: newInput(LANG.ou_zbot_s, {title:LANG.ou_zbot_l, convert:toFloat, units, trigger}), - // camZThru: newInput(LANG.ou_ztru_s, {title:LANG.ou_ztru_l, convert:toFloat, bound:bound(0.0,100), units }), camZClearance: newInput(LANG.ou_zclr_s, {title:LANG.ou_zclr_l, convert:toFloat, bound:bound(0.01,100), units }), separator: newBlank({ class:"set-sep", driven }), camFastFeed: newInput(LANG.cc_rapd_s, {title:LANG.cc_rapd_l, convert:toFloat, units}), diff --git a/src/kiri/mode/cam/slice.js b/src/kiri/mode/cam/slice.js index 6e79d525..70597045 100644 --- a/src/kiri/mode/cam/slice.js +++ b/src/kiri/mode/cam/slice.js @@ -52,7 +52,7 @@ export async function cam_slice(settings, widget, onupdate, ondone) { let axisRotation, axisIndex, bounds, dark, color, stock, tabs, track, tool, unsafe, units, workarea, - camZTop, camZBottom, camZThru, minToolDiam, maxToolDiam, + camZTop, camZBottom, minToolDiam, maxToolDiam, bottom_gap, bottom_part, bottom_stock, bottom_z, top_stock, top_part, top_gap, top_z, zBottom, zMin, zMax, zTop, @@ -70,7 +70,7 @@ export async function cam_slice(settings, widget, onupdate, ondone) { // allow recomputing later if widget or settings changes const var_compute = () => { let { camStockX, camStockY, camStockZ, camStockOffset } = proc; - ({ camZTop, camZBottom, camZThru } = proc); + ({ camZTop, camZBottom } = proc); bounds = widget.getBoundingBox(); let pos = widget.track.pos; stock = camStockOffset ? { @@ -637,7 +637,7 @@ export async function holes(settings, widget, individual, rec, onProgress) { stock = settings.stock || {}, isIndexed = proc.camStockIndexed, track = widget.track, - { camZTop, camZBottom, camZThru } = proc, + { camZBottom } = proc, // widget top z as defined by setTopz() wztop = track.top, // distance between top of part and top of stock diff --git a/src/kiri/mode/cam/topo3.js b/src/kiri/mode/cam/topo3.js index 1253cdf1..5a34b1eb 100644 --- a/src/kiri/mode/cam/topo3.js +++ b/src/kiri/mode/cam/topo3.js @@ -77,7 +77,7 @@ export class Topo { gridDelta = Math.floor(partOff / resolution), debug_clips = true; - if (process.camStockClipTo) { + if (contour.clipto) { let { stock } = settings; let { center, x, y } = stock; clipTo.push(newPolygon().centerRectangle(center, x, y)); diff --git a/web/kiri/lang/en.js b/web/kiri/lang/en.js index d70cf351..709ee39c 100644 --- a/web/kiri/lang/en.js +++ b/web/kiri/lang/en.js @@ -585,6 +585,8 @@ self.lang['en-us'] = { cf_linx_l: "linear y-axis finishing", cf_liny_s: "enable x pass", cf_liny_l: "linear x-axis finishing", + cf_clip_s: "clip to stock", + cf_clip_l: ["contour op only","clip cutting paths","to defined stock"], // CNC TRACE cu_menu: "trace", @@ -838,8 +840,6 @@ self.lang['en-us'] = { cs_hght_l: ["height (z) in workspace units","0 defaults to part size"], cs_offs_s: "offset", cs_offs_l: ["use width, depth, height","as offsets from max","part size on platform"], - cs_clip_s: "clip to", - cs_clip_l: ["contour op only","clip cutting paths","to defined stock"], cs_ishg_s: "show grid", cs_ishg_l: ["show platform grid in indexed mode"], cs_indx_s: "indexed", From c46f88b412115b70ef19a43ea86a9b2f33b6d7c6 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sat, 27 Dec 2025 10:28:07 -0500 Subject: [PATCH 055/154] add optional alternating/zigzag for area linear surfacing --- src/geo/paths.js | 44 ++++++++++++++++++++++++++++++--- src/kiri/app/conf/defaults.js | 1 + src/kiri/mode/cam/cl-ops.js | 2 ++ src/kiri/mode/cam/op-area.js | 36 ++++++++++++++++----------- src/kiri/mode/cam/op-contour.js | 3 +-- 5 files changed, 66 insertions(+), 20 deletions(-) diff --git a/src/geo/paths.js b/src/geo/paths.js index a6f3822f..1e72369b 100644 --- a/src/geo/paths.js +++ b/src/geo/paths.js @@ -2,6 +2,7 @@ import { base } from './base.js'; import { newPoint } from './point.js'; +import { newPolygon } from './polygon.js'; /** * Emit each element in array based on next closest endpoint @@ -18,23 +19,26 @@ export function tip2tipEmit(array, startPoint, emitter) { for (;;) { found = null; mindist = Infinity; - array.forEach(function (el) { - if (el.delete) return; + for (let el of array) { + if (el.delete) continue; dist = startPoint.distTo2D(el.first); if (dist < mindist) { found = { el: el, first: el.first, last: el.last }; mindist = dist; } + // when heads is set, it's first to first + // not first/last to closest first/last + if (el.heads) continue; dist = startPoint.distTo2D(el.last); if (dist < mindist) { found = { el: el, first: el.last, last: el.first }; mindist = dist; } - }); + } if (found) { found.el.delete = true; startPoint = found.last; - emitter(found.el, found.first, ++count); + emitter(found.el, found.first, ++count, mindist); } else { break; } @@ -42,6 +46,38 @@ export function tip2tipEmit(array, startPoint, emitter) { return startPoint; } +/** + * Emit each element in array based on next closest endpoint + * Uses greedy nearest-neighbor algorithm to minimize travel distance + * When polygons are close, a new polygon is injected that joins them + * and the next polygon is reversed so that tips would merge on output + * @param {Array} paths - Array of elements with { first, last } Point properties + * @param {Point} startPoint - Starting point for path ordering + * @param {Number} mergeDist - threshold for merging Polygons + * @returns {Array} reduced merged Polygon array + */ +export function tip2tipJoin(paths, startPoint, mergeDist) { + let newp = []; + tip2tipEmit(paths.map(path => ({ + path, + last: path.last(), + first: path.first() + })), startPoint, (next, point, count, dist) => { + if (next.last === point) { + next.path.reverse(); + } + if (newp.length && dist < mergeDist) { + let lastpath = newp.peek(); + newp.push(newPolygon().addPoints([ + lastpath.last(), + next.path.first() + ])); + } + newp.push(next.path); + }); + return newp; +} + /** * Emit polygons in optimal order based on nearest point * Like tip2tipEmit but accepts polygons where next closest point can be anywhere diff --git a/src/kiri/app/conf/defaults.js b/src/kiri/app/conf/defaults.js index b8f7f438..a829966e 100644 --- a/src/kiri/app/conf/defaults.js +++ b/src/kiri/app/conf/defaults.js @@ -446,6 +446,7 @@ export const conf = { camAreaDogbones: false, camAreaRevbones: false, camAreaOutline: false, + camAreaZigZag: true, camContourAngle: 85, camContourBottom: false, camContourBridge: 0, diff --git a/src/kiri/mode/cam/cl-ops.js b/src/kiri/mode/cam/cl-ops.js index df9c612b..fc03e33f 100644 --- a/src/kiri/mode/cam/cl-ops.js +++ b/src/kiri/mode/cam/cl-ops.js @@ -678,6 +678,7 @@ export function createPopOps() { tr_type: 'camAreaTrace', sr_type: 'camAreaSurface', sr_angle: 'camAreaAngle', + sr_alter: 'camAreaZigZag', over: 'camAreaOver', down: 'camAreaDown', rate: 'camAreaSpeed', @@ -713,6 +714,7 @@ export function createPopOps() { over: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: toFloat, bound: UC.bound(0.001, 100.0), show: () => isClear() || isSurface() }), down: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: toFloat, bound: UC.bound(0, 100.0), units, show: () => isClear() || isTrace() }), refine: UC.newInput(LANG.cp_refi_s, { title: LANG.cp_refi_l, convert: toInt, show: isSurface }), + sr_alter: UC.newBoolean("alternate", undefined, { title: "alternate cutting direction", show: isSurfaceLinear }), dogbones: UC.newBoolean(LANG.co_dogb_s, undefined, { title: LANG.co_dogb_l, show: isTrace }), revbones: UC.newBoolean(LANG.co_dogr_s, undefined, { title: LANG.co_dogr_l, show: () => env.poppedRec.dogbones }), exp_end: UC.endExpand(), diff --git a/src/kiri/mode/cam/op-area.js b/src/kiri/mode/cam/op-area.js index 35c518e4..7764f07d 100644 --- a/src/kiri/mode/cam/op-area.js +++ b/src/kiri/mode/cam/op-area.js @@ -10,6 +10,7 @@ import { newPoint } from '../../../geo/point.js'; import { newPolygon } from '../../../geo/polygon.js'; import { polygons as POLY } from '../../../geo/polygons.js'; import { util as base_util } from '../../../geo/base.js'; +import { tip2tipJoin } from '../../../geo/paths.js'; import { CAM } from './driver-be.js'; const DEG2RAD = Math.PI / 180; @@ -41,6 +42,11 @@ class OpArea extends CamOp { let thruHoles = state.shadow.holes; let roundSharps = settings.process.camRoundCorners; + // get these once and re-use for each area + let vertices = mode === 'surface' ? + widget.getGeoVertices({ unroll: true, translate: true }) : + undefined; + // also updates tab offsets setToolDiam(toolDiam); @@ -302,7 +308,7 @@ class OpArea extends CamOp { progress(proc, 'trace'); } else if (mode === 'surface') { - let { sr_type, sr_angle, tolerance } = op; + let { sr_type, sr_angle, sr_alter, tolerance } = op; let resolution = tolerance || 0.05; let raster = await self.get_raster_gpu({ mode: "tracing", resolution }); @@ -327,8 +333,14 @@ class OpArea extends CamOp { paths.push(POLY.fromClipperNode(node, 0)); } } - // convert resulting poly lines to raster float32 array groups - paths = paths.map(poly => poly.points.map(p => [ p.x, p.y ]).flat().toFloat32()); + // in surfacing mode, direction is simply reversal + if (direction === 'climb') { + paths.forEach(path => path.reverse()); + } + // optional alternating paths + if (sr_alter) { + paths = tip2tipJoin(paths, paths[0].first(), toolDiam * 4); + } } else if (sr_type === 'offset') { // progressive inset from perimeter @@ -336,9 +348,12 @@ class OpArea extends CamOp { count: 999, outs: paths, flat: true, z: 0, minArea: 0 }); paths.forEach(poly => poly.isClosed() && poly.push(poly.first())); - paths = paths.map(poly => poly.points.map(p => [ p.x, p.y ]).flat().toFloat32()); + POLY.setWinding(paths.filter(p => p.isClosed()), direction === 'climb'); } + // convert resulting poly lines to raster float32 array groups + paths = paths.map(poly => poly.points.map(p => [ p.x, p.y ]).flat().toFloat32()); + // prepare tool mesh points let toolBounds = new THREE.Box3() .expandByPoint({ x: -toolDiam/2, y: -toolDiam/2, z: 0 }) @@ -350,7 +365,6 @@ class OpArea extends CamOp { let toolData = { positions: toolPos, bounds: toolBounds }; // prepare terrain and raster paths over terrain - let vertices = widget.getGeoVertices({ unroll: true, translate: true }); let wbounds = bounds.clone().expandByVector({ x: toolDiam/2, y: toolDiam/2, z: 0 }); wbounds.min.z = zBottom; wbounds.max.z = zTop; @@ -408,17 +422,11 @@ class OpArea extends CamOp { if (surfaces.length) { setContouring(true); for (let surface of surfaces) { - let array = surface.map(poly => { return { - el: poly, - first: poly.first(), - last: poly.last() - } }); - tip2tipEmit(array, printPoint, (next, point) => { + for (let poly of surface) { setNextIsMove(); - if (next.last === point) next.el.reverse(); - printPoint = polyEmit(next.el); + printPoint = polyEmit(poly); newLayer(); - }); + } } setContouring(false); // skip areas when processing surfaces diff --git a/src/kiri/mode/cam/op-contour.js b/src/kiri/mode/cam/op-contour.js index baff1a70..b9116c86 100644 --- a/src/kiri/mode/cam/op-contour.js +++ b/src/kiri/mode/cam/op-contour.js @@ -122,10 +122,9 @@ class OpContour extends CamOp { let { process } = settings; let { polyEmit, setContouring, setTolerance, setTool } = ops; - let { widget, camOut, newLayer, zmax } = ops; + let { widget, newLayer, zmax } = ops; let bounds = widget.getBoundingBox(); - let toolDiam = this.toolDiam; let depthData = []; setTool(op.tool, op.rate, process.camFastFeedZ); From b45e2ba9b4d593b617ff242a41b15219ac909598 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sat, 27 Dec 2025 12:39:28 -0500 Subject: [PATCH 056/154] optimize load path. present menu on version change for changelog --- src/kiri/app/init/input.js | 8 ++++++-- src/kiri/app/workspace.js | 16 +++++++++------- src/kiri/core/consts.js | 4 +++- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/kiri/app/init/input.js b/src/kiri/app/init/input.js index c1e761aa..bb9abe08 100644 --- a/src/kiri/app/init/input.js +++ b/src/kiri/app/init/input.js @@ -25,7 +25,7 @@ import { settings as set_ctrl } from '../conf/manager.js'; import { settingsOps } from '../conf/settings.js'; import { slider } from '../slider.js'; import { space } from '../../../moto/space.js'; -import { VIEWS, MODES, SEED } from '../../core/consts.js'; +import { LAST, MODES, SEED, VIEWS } from '../../core/consts.js'; import STACKS from '../stacks.js'; @@ -77,7 +77,11 @@ function checkSeed(then) { }); return true; } + } else if (sdb[LAST] !== version) { + sdb[LAST] = version; + api.help.show(); } + then(); return false; } @@ -535,6 +539,6 @@ export function init_input() { }; return new Promise(resolve => { - api.space.restore(resolve) || checkSeed(resolve) || resolve(); + api.space.restore(() => checkSeed(resolve)); }); }; diff --git a/src/kiri/app/workspace.js b/src/kiri/app/workspace.js index c8ea427e..fd0f8806 100644 --- a/src/kiri/app/workspace.js +++ b/src/kiri/app/workspace.js @@ -165,9 +165,7 @@ function restore(ondone, skip_widget_load) { } if (skip_widget_load) { - if (ondone) { - ondone(); - } + ondone && ondone(); return; } @@ -177,8 +175,9 @@ function restore(ondone, skip_widget_load) { }); // load any widget by name that was saved to the workspace - toload.forEach(function(widgetid) { - Widget.loadFromState(widgetid, function(widget) { + // after the last widget is loaded, call ondone() + for (let widgetid of toload) { + Widget.loadFromState(widgetid, (widget) => { if (widget) { platform.add(widget, 0, position, true); let ann = api.widgets.annotate(widgetid); @@ -196,9 +195,12 @@ function restore(ondone, skip_widget_load) { } } }, position); - }); + }; - return toload.length > 0; + // when there is nothing async to load, call ondone() + if (toload.length === 0 && ondone) { + ondone(); + } } /** diff --git a/src/kiri/core/consts.js b/src/kiri/core/consts.js index fa4e45af..365f6a7c 100644 --- a/src/kiri/core/consts.js +++ b/src/kiri/core/consts.js @@ -176,10 +176,12 @@ const PATHS = { pool: "./minion.js" }; +const LAST = 'kiri-last'; const SEED = 'kiri-seed'; export let consts = { COLOR, + LAST, LISTS, MODES, PATHS, @@ -189,4 +191,4 @@ export let consts = { beltfact: Math.cos(Math.PI / 4) }; -export { COLOR, LISTS, MODES, VIEWS, PATHS, PMODES, SEED }; +export { COLOR, LAST, LISTS, MODES, VIEWS, PATHS, PMODES, SEED }; From 24c0316cf8f66c31db5002d97adeb9dcbf5fec38 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sat, 27 Dec 2025 13:24:04 -0500 Subject: [PATCH 057/154] split widget into core and app-ui subclass --- src/kiri/app/api.js | 2 +- src/kiri/app/frame.js | 2 +- src/kiri/app/groups.js | 2 +- src/kiri/app/image.js | 2 +- src/kiri/app/platform.js | 2 +- src/kiri/app/widget.js | 253 +++++++++++++++++++++++++++++++++++ src/kiri/app/workers.js | 2 +- src/kiri/app/workspace.js | 2 +- src/kiri/core/widget.js | 221 ++---------------------------- src/kiri/core/widgets.js | 2 +- src/kiri/mode/cam/init-ui.js | 2 +- src/kiri/mode/fdm/init-ui.js | 2 +- src/kiri/run/engine.js | 2 +- 13 files changed, 275 insertions(+), 221 deletions(-) create mode 100644 src/kiri/app/widget.js diff --git a/src/kiri/app/api.js b/src/kiri/app/api.js index 360775cd..49b5f11e 100644 --- a/src/kiri/app/api.js +++ b/src/kiri/app/api.js @@ -22,7 +22,7 @@ import { local } from './local.js'; import { local as dataLocal } from '../../data/local.js'; import { modal } from './modal.js'; import { mode as modeModule, process as processModule } from './mode.js'; -import { newWidget } from '../core/widget.js'; +import { newWidget } from './widget.js'; import { noop, ajax, o2js, js2o, utils } from '../core/utils.js'; import { openFiles } from './files.js'; import { platform } from './platform.js'; diff --git a/src/kiri/app/frame.js b/src/kiri/app/frame.js index ee8ddc37..ca814ae3 100644 --- a/src/kiri/app/frame.js +++ b/src/kiri/app/frame.js @@ -25,7 +25,7 @@ import { api } from './api.js'; import { load } from '../../load/file.js'; -import { newWidget } from '../core/widget.js'; +import { newWidget } from './widget.js'; import { VIEWS } from '../core/consts.js'; import { widgets } from '../core/widgets.js'; diff --git a/src/kiri/app/groups.js b/src/kiri/app/groups.js index 92434c69..fd189742 100644 --- a/src/kiri/app/groups.js +++ b/src/kiri/app/groups.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { selection } from './selected.js'; -import { Widget } from '../core/widget.js'; +import { Widget } from './widget.js'; /** * Merge selected widgets into a group. diff --git a/src/kiri/app/image.js b/src/kiri/app/image.js index 2867436d..97fda375 100644 --- a/src/kiri/app/image.js +++ b/src/kiri/app/image.js @@ -2,7 +2,7 @@ import { $ } from '../../moto/webui.js'; import { api } from './api.js'; -import { newWidget } from '../core/widget.js'; +import { newWidget } from './widget.js'; import { platform } from './platform.js'; import { settings } from './conf/manager.js'; diff --git a/src/kiri/app/platform.js b/src/kiri/app/platform.js index 8523632c..3d8efc81 100644 --- a/src/kiri/app/platform.js +++ b/src/kiri/app/platform.js @@ -9,7 +9,7 @@ import { newBounds } from '../../geo/bounds.js'; import { Packer } from './pack.js'; import { space } from '../../moto/space.js'; import { THREE } from '../../ext/three.js'; -import { Widget, newWidget } from '../core/widget.js'; +import { Widget, newWidget } from './widget.js'; const V0 = new THREE.Vector3(0,0,0); diff --git a/src/kiri/app/widget.js b/src/kiri/app/widget.js new file mode 100644 index 00000000..b55dd548 --- /dev/null +++ b/src/kiri/app/widget.js @@ -0,0 +1,253 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { Widget as WidgetCore } from '../core/widget.js'; +import { util } from '../../geo/base.js'; +import { avgc } from '../core/utils.js'; + +const solid_opacity = 1.0; + +function catalog() { return self.kiri_catalog }; +function index() { return self.kiri_catalog.index } + +/** + * UI-enhanced Widget subclass for main thread + * Adds rendering, persistence, and visual state management + */ +class Widget extends WidgetCore { + /** + * Save widget geometry to catalog + */ + saveToCatalog(filename, overwrite) { + if (!filename) { + filename = this.meta.file; + } + if (this.grouped && !overwrite) { + return this; + } + const widget = this; + const mark = Date.now(); + const vertices = widget.getGeoVertices({ unroll: true }).slice(); + widget.meta.file = filename; + widget.meta.save = mark; + catalog().putFile(filename, vertices, () => { + console.log("saved mesh ["+(vertices.length/3)+"] time ["+(Date.now()-mark)+"]"); + }); + return this; + } + + /** + * Save widget state to index + */ + saveState(ondone) { + if (!ondone) { + clearTimeout(this._save_timer); + this._save_timer = setTimeout(() => { + this._save_timer = undefined; + this.saveState(() => {}); + }, 1500); + return; + } + const widget = this; + index().put('ws-save-'+this.id, { + geo: widget.getGeoVertices({ unroll: false }).slice(), + track: widget.track, + group: this.group.id, + meta: this.meta, + anno: this.annotations() + }, result => { + widget.meta.saved = Date.now(); + if (ondone) ondone(); + }); + } + + /** + * Set mesh material color + */ + setColor(color, settings, save = true) { + if (settings) { + console.trace('legacy call with settings'); + } + if (Array.isArray(color)) { + color = color[this.getExtruder() % color.length]; + } + if (save) { + this.color = color; + } + let material = this.getMaterial(); + material.color.set(this.meta.disabled ? avgc(0x888888, color, 3) : color); + } + + /** + * Get current visual state + */ + getVisualState() { + return { + edges: this.outline ? true : false, + wires: this.wire ? true : false, + opacity: this.getMaterial().opacity + }; + } + + /** + * Set visual state + */ + setVisualState({ edges, wires, opacity}) { + this.cache.vizstate = this.getVisualState(); + this.setEdges(edges ?? false); + this.setWireframe(wires ?? false); + this.setOpacity(opacity ?? 1); + } + + /** + * Refresh visual state + */ + refreshVisualState() { + this.setVisualState(this.getVisualState()); + } + + /** + * Restore previous visual state + */ + restoreVisualState() { + if (this.cache.vizstate) { + this.setVisualState(this.cache.vizstate); + } + } + + /** + * Set material opacity + */ + setOpacity(value) { + const mat = this.getMaterial(); + if (value <= 0.0) { + mat.transparent = solid_opacity < 1.0; + mat.opacity = solid_opacity; + mat.visible = false; + } else if (util.inRange(value, 0.0, solid_opacity)) { + mat.transparent = value < 1.0; + mat.opacity = value; + mat.visible = true; + } + } + + /** + * Set edge rendering + */ + setEdges(set) { + if (!(this.api && this.api.conf)) { + // missing api features in engine mode + return; + } + let mesh = this.mesh; + if (set && set.toggle) { + set = this.outline ? false : true; + } + if (this.outline) { + mesh.remove(this.outline); + this.outline = null; + + } + if (set) { + let dark = this.api.space.is_dark(); + let cam = this.api.mode.is_cam(); + let color = dark ? 0x444444 : 0x888888; + let angle = this.api.conf.get().controller.edgeangle || 20; + let edges = new THREE.EdgesGeometry(mesh.geometry, angle); + let material = new THREE.LineBasicMaterial({ color }); + this.outline = new THREE.LineSegments(edges, material); + this.outline.renderOrder = -20; + mesh.add(this.outline); + } + } + + /** + * Set wireframe rendering + */ + setWireframe(set, color, opacity) { + if (!(this.api && this.api.conf)) { + // missing api features in engine mode + return; + } + let mesh = this.mesh, + widget = this; + if (this.wire) { + this.setOpacity(solid_opacity); + mesh.remove(this.wire); + this.wire = null; + } + if (set) { + let dark = this.api.space.is_dark(); + let mat = new THREE.MeshBasicMaterial({ + wireframe: true, + color: dark ? 0xaaaaaa : 0, + opacity: 0.5, + transparent: true + }) + let wire = widget.wire = new THREE.Mesh(mesh.geometry.shallowClone(), mat); + mesh.add(wire); + } + if (this.api.view.is_arrange()) { + this.setColor(this.color); + } else { + this.setColor(0x888888,undefined,false); + } + if (opacity !== undefined) { + widget.setOpacity(opacity); + } + } + + /** + * Load widget from catalog + */ + static loadFromCatalog(filename, ondone) { + catalog().getFile(filename, function(data) { + let widget = new Widget().loadVertices(data); + widget.meta.file = filename; + ondone(widget); + }); + } + + /** + * Load widget from saved state + */ + static loadFromState(id, ondone, move) { + const Group = Widget.Groups; + index().get('ws-save-'+id, function(data) { + if (data) { + let vertices = data.geo || data, + track = data.track || undefined, + group = data.group || id, + anno = data.anno || undefined, + widget = new Widget(id, Group.forid(group)), + meta = data.meta || widget.meta, + ptr = widget.loadVertices(vertices); + widget.meta = meta; + widget.anno = anno || widget.anno; + // restore widget position if specified + if (move && track && track.pos) { + widget.track = track; + widget.move(track.pos.x, track.pos.y, track.pos.z, true); + } + ondone(ptr); + } else { + ondone(null); + } + }); + } + + /** + * Delete widget from saved state + */ + static deleteFromState(id,ondone) { + index().remove('ws-save-'+id, ondone); + } +} + +/** + * Factory function to create UI-enhanced widgets + */ +function newWidget(id, group) { + return new Widget(id, group); +} + +export { Widget, newWidget }; diff --git a/src/kiri/app/workers.js b/src/kiri/app/workers.js index e0a9871d..c508d10e 100644 --- a/src/kiri/app/workers.js +++ b/src/kiri/app/workers.js @@ -3,7 +3,7 @@ import { api } from './api.js'; import { noop } from '../core/utils.js'; import { codec } from '../core/codec.js'; -import { Widget } from '../core/widget.js'; +import { Widget } from './widget.js'; import { newPrint } from '../core/print.js'; // this code runs in kiri's main loop diff --git a/src/kiri/app/workspace.js b/src/kiri/app/workspace.js index fd0f8806..92dd3d4b 100644 --- a/src/kiri/app/workspace.js +++ b/src/kiri/app/workspace.js @@ -9,7 +9,7 @@ import { selection } from './selected.js'; import { settings } from './conf/manager.js'; import { space as SPACE } from '../../moto/space.js'; import { utils } from '../core/utils.js'; -import { Widget } from '../core/widget.js'; +import { Widget } from './widget.js'; import { Index } from '../../data/index.js'; const { o2js, js2o, ls2o, parseOpt } = utils; diff --git a/src/kiri/core/widget.js b/src/kiri/core/widget.js index 5206e58f..a1035977 100644 --- a/src/kiri/core/widget.js +++ b/src/kiri/core/widget.js @@ -1,7 +1,5 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { base } from '../../geo/base.js'; -import { avgc } from './utils.js'; import { util as mesh_util } from '../../mesh/util.js'; import { tool as mesh_tool } from '../../mesh/tool.js'; import { verticesToPoints } from '../../geo/points.js'; @@ -10,21 +8,13 @@ import { newPolygon } from '../../geo/polygon.js'; import { polygons as POLY } from '../../geo/polygons.js'; import { checkOverUnderOn, intersectPoints } from '../../geo/slicer.js'; -const { inRange, time } = base.util; +const sharedArrayClass = self.SharedArrayBuffer || undefined; +const hasSharedArrays = sharedArrayClass ? true : false; const solid_opacity = 1.0; const groups = []; -const sharedArrayClass = self.SharedArrayBuffer || undefined; -const hasSharedArrays = sharedArrayClass ? true : false; - let nextId = 0; -function newWidget(id,group) { return new Widget(id,group) } - -// todo: fix ui-only path thus this unorthodox binding vs importing api -function catalog() { return self.kiri_catalog }; -function index() { return self.kiri_catalog.index } - class Widget { constructor(id, group) { this.api = self.kiri_api; @@ -98,46 +88,6 @@ class Widget { this.support = false; } - saveToCatalog(filename, overwrite) { - if (!filename) { - filename = this.meta.file; - } - if (this.grouped && !overwrite) { - return this; - } - const widget = this; - const mark = time(); - const vertices = widget.getGeoVertices({ unroll: true }).slice(); - widget.meta.file = filename; - widget.meta.save = mark; - catalog().putFile(filename, vertices, () => { - console.log("saved mesh ["+(vertices.length/3)+"] time ["+(time()-mark)+"]"); - }); - return this; - } - - saveState(ondone) { - if (!ondone) { - clearTimeout(this._save_timer); - this._save_timer = setTimeout(() => { - this._save_timer = undefined; - this.saveState(() => {}); - }, 1500); - return; - } - const widget = this; - index().put('ws-save-'+this.id, { - geo: widget.getGeoVertices({ unroll: false }).slice(), - track: widget.track, - group: this.group.id, - meta: this.meta, - anno: this.annotations() - }, result => { - widget.meta.saved = time(); - if (ondone) ondone(); - }); - } - annotations() { let o = Object.clone(this.anno); if (o.support) { @@ -311,23 +261,6 @@ class Widget { this.slices = null; } - /** - * @param {number} color - */ - setColor(color, settings, save = true) { - if (settings) { - console.trace('legacy call with settings'); - } - if (Array.isArray(color)) { - color = color[this.getExtruder() % color.length]; - } - if (save) { - this.color = color; - } - let material = this.getMaterial(); - material.color.set(this.meta.disabled ? avgc(0x888888, color, 3) : color); - } - getColor() { return this.color; } @@ -362,47 +295,6 @@ class Widget { } } - getVisualState() { - return { - edges: this.outline ? true : false, - wires: this.wire ? true : false, - opacity: this.getMaterial().opacity - }; - } - - setVisualState({ edges, wires, opacity}) { - this.cache.vizstate = this.getVisualState(); - this.setEdges(edges ?? false); - this.setWireframe(wires ?? false); - this.setOpacity(opacity ?? 1); - } - - refreshVisualState() { - this.setVisualState(this.getVisualState()); - } - - restoreVisualState() { - if (this.cache.vizstate) { - this.setVisualState(this.cache.vizstate); - } - } - - /** - * @param {number} value - */ - setOpacity(value) { - const mesh = this.mesh; - const mat = this.getMaterial(); - if (value <= 0.0) { - mat.transparent = solid_opacity < 1.0; - mat.opacity = solid_opacity; - mat.visible = false; - } else if (inRange(value, 0.0, solid_opacity)) { - mat.transparent = value < 1.0; - mat.opacity = value; - mat.visible = true; - } - } toggleVisibility(bool) { const mat = this.getMaterial(); @@ -574,10 +466,10 @@ class Widget { let mesh = this.mesh, scale = this.track.scale; this.bounds = null; - this.setWireframe(false); + if (this.setWireframe) this.setWireframe(false); this.clearSlices(); mesh.geometry.applyMatrix4(new THREE.Matrix4().makeScale(x, y, z)); - if (this.outline) { + if (this.outline && this.setEdges) { this.setEdges(true); } scale.x *= (x || 1.0); @@ -593,7 +485,7 @@ class Widget { if (center) { this.center(false); } - if (this.outline) { + if (this.outline && this.setEdges) { this.setEdges(true); } if ((x || y || z) && this.api && this.api.event) { @@ -604,7 +496,7 @@ class Widget { _rotate(x, y, z, temp) { if (!temp) { this.bounds = null; - this.setWireframe(false); + if (this.setWireframe) this.setWireframe(false); this.clearSlices(); } let m4 = new THREE.Matrix4(); @@ -633,7 +525,7 @@ class Widget { this.roto = []; this.center(); this.setModified(); - this.refreshVisualState(); + if (this.refreshVisualState) this.refreshVisualState(); } mirror() { @@ -648,7 +540,7 @@ class Widget { _mirror() { this.clearSlices(); - this.setWireframe(false); + if (this.setWireframe) this.setWireframe(false); let geo = this.mesh.geometry, ot = this.track; let pos = geo.attributes.position; let arr = pos.array; @@ -782,65 +674,6 @@ class Widget { return Date.now() - mark; } - setEdges(set) { - if (!(this.api && this.api.conf)) { - // missing api features in engine mode - return; - } - let mesh = this.mesh; - if (set && set.toggle) { - set = this.outline ? false : true; - } - if (this.outline) { - mesh.remove(this.outline); - this.outline = null; - - } - if (set) { - let dark = this.api.space.is_dark(); - let cam = this.api.mode.is_cam(); - let color = dark ? 0x444444 : 0x888888; - let angle = this.api.conf.get().controller.edgeangle || 20; - let edges = new THREE.EdgesGeometry(mesh.geometry, angle); - let material = new THREE.LineBasicMaterial({ color }); - this.outline = new THREE.LineSegments(edges, material); - this.outline.renderOrder = -20; - mesh.add(this.outline); - } - } - - setWireframe(set, color, opacity) { - if (!(this.api && this.api.conf)) { - // missing api features in engine mode - return; - } - let mesh = this.mesh, - widget = this; - if (this.wire) { - this.setOpacity(solid_opacity); - mesh.remove(this.wire); - this.wire = null; - } - if (set) { - let dark = this.api.space.is_dark(); - let mat = new THREE.MeshBasicMaterial({ - wireframe: true, - color: dark ? 0xaaaaaa : 0, - opacity: 0.5, - transparent: true - }) - let wire = widget.wire = new THREE.Mesh(mesh.geometry.shallowClone(), mat); - mesh.add(wire); - } - if (this.api.view.is_arrange()) { - this.setColor(this.color); - } else { - this.setColor(0x888888,undefined,false); - } - if (opacity !== undefined) { - widget.setOpacity(opacity); - } - } show() { this.mesh.visible = true; @@ -1076,40 +909,8 @@ const Group = Widget.Groups = { } }; -Widget.loadFromCatalog = function(filename, ondone) { - catalog().getFile(filename, function(data) { - let widget = newWidget().loadVertices(data); - widget.meta.file = filename; - ondone(widget); - }); -}; - -Widget.loadFromState = function(id, ondone, move) { - index().get('ws-save-'+id, function(data) { - if (data) { - let vertices = data.geo || data, - track = data.track || undefined, - group = data.group || id, - anno = data.anno || undefined, - widget = newWidget(id, Group.forid(group)), - meta = data.meta || widget.meta, - ptr = widget.loadVertices(vertices); - widget.meta = meta; - widget.anno = anno || widget.anno; - // restore widget position if specified - if (move && track && track.pos) { - widget.track = track; - widget.move(track.pos.x, track.pos.y, track.pos.z, true); - } - ondone(ptr); - } else { - ondone(null); - } - }); -}; - -Widget.deleteFromState = function(id,ondone) { - index().remove('ws-save-'+id, ondone); -}; +function newWidget(id,group) { + return new Widget(id,group); +} export { Widget, newWidget }; diff --git a/src/kiri/core/widgets.js b/src/kiri/core/widgets.js index 5db6f325..39d621ee 100644 --- a/src/kiri/core/widgets.js +++ b/src/kiri/core/widgets.js @@ -3,7 +3,7 @@ import { $ } from '../../moto/webui.js'; import { api } from '../app/api.js'; import { load } from '../../load/file.js'; -import { Widget, newWidget } from './widget.js'; +import { Widget, newWidget } from '../app/widget.js'; let WIDGETS = []; diff --git a/src/kiri/mode/cam/init-ui.js b/src/kiri/mode/cam/init-ui.js index 55fdd744..706cb456 100644 --- a/src/kiri/mode/cam/init-ui.js +++ b/src/kiri/mode/cam/init-ui.js @@ -16,7 +16,7 @@ import { holeSelOn, selectHolesDone, clearHolesRec } from './cl-hole.js'; import { surfaceOn, surfaceDone } from './cl-surface.js'; import { helicalOn, helicalDone } from './cl-helical.js'; import { originSelectDone } from './cl-origin.js'; -import { Widget, newWidget } from '../../core/widget.js'; +import { Widget, newWidget } from '../../app/widget.js'; import { space } from '../../../moto/space.js'; import { recreateTabs } from './cl-tab.js'; diff --git a/src/kiri/mode/fdm/init-ui.js b/src/kiri/mode/fdm/init-ui.js index d0da4218..f7e4d0ff 100644 --- a/src/kiri/mode/fdm/init-ui.js +++ b/src/kiri/mode/fdm/init-ui.js @@ -8,7 +8,7 @@ import { client } from '../../app/workers.js'; import { addbox, delbox, getlastbox } from '../../core/boxes.js'; import { Layers } from '../../app/layers.js'; import { Stack } from '../../app/stack.js'; -import { Widget, newWidget } from '../../core/widget.js'; +import { Widget, newWidget } from '../../app/widget.js'; import { space } from '../../../moto/space.js'; const { Matrix4, Vector3, BufferAttribute, BufferGeometryUtils, Raycaster, Euler } = THREE; diff --git a/src/kiri/run/engine.js b/src/kiri/run/engine.js index eb5aa5a5..deb29d57 100644 --- a/src/kiri/run/engine.js +++ b/src/kiri/run/engine.js @@ -7,7 +7,7 @@ import { api } from '../app/api.js'; import { conf } from '../app/conf/defaults.js'; import { client } from '../app/workers.js'; import { load } from '../../load/file.js'; -import { newWidget } from '../core/widget.js'; +import { newWidget } from '../app/widget.js'; class Engine { constructor({ workURL, poolURL } = {}) { From c3638262d02bad8da6ee96edb0b38b0a7de36a9b Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sat, 27 Dec 2025 19:11:55 -0500 Subject: [PATCH 058/154] filter shadow slivers on high poly models, add W ghost/wire hotkeys --- src/geo/polygons.js | 6 +++++- src/kiri/app/keyboard.js | 10 +++++++--- src/kiri/app/visuals.js | 26 +++++++++++++------------- src/kiri/core/widget.js | 13 ++++++++++++- src/kiri/core/widgets.js | 2 +- 5 files changed, 38 insertions(+), 19 deletions(-) diff --git a/src/geo/polygons.js b/src/geo/polygons.js index 41102373..8a5d7947 100644 --- a/src/geo/polygons.js +++ b/src/geo/polygons.js @@ -481,7 +481,11 @@ export function union(polys, minarea, all, opt = {}) { } for (i=0; i= minarea) { + uset.push(out[i]); + } + } } opt.changes = length(uset) - lpre; diff --git a/src/kiri/app/keyboard.js b/src/kiri/app/keyboard.js index d0e825c9..22bb4be1 100644 --- a/src/kiri/app/keyboard.js +++ b/src/kiri/app/keyboard.js @@ -258,6 +258,7 @@ class KeyboardControl { switch (evt.key) { // case 'g': return this.#api.group.merge(); // case 'u': return this.#api.group.split(); + case 'w': return $('render-edges').onclick(); } } @@ -366,9 +367,6 @@ class KeyboardControl { case this.#cca('o'): // tools this.#api.show.tools(); break; - case this.#cca('c'): // local devices - this.#api.show.local(); - break; case this.#cca('v'): // toggle single slice view mode if (this.#api.view.get() === this.#VIEWS.ARRANGE) { this.#api.space.set_focus(this.#selection.widgets()); @@ -399,6 +397,12 @@ class KeyboardControl { this.#api.view.set(this.#VIEWS.ARRANGE); } break; + case this.#cca('w'): // set ghost + $('render-ghost').onclick(); + break; + case this.#cca('W'): // set wireframe + $('render-wire').onclick(); + break; default: this.#onUnhandledKey(evt); handled = false; diff --git a/src/kiri/app/visuals.js b/src/kiri/app/visuals.js index 1b988120..7a351f06 100644 --- a/src/kiri/app/visuals.js +++ b/src/kiri/app/visuals.js @@ -112,7 +112,7 @@ function setVisibleLayer(h, l) { * @param {number} [opacity] - Optional wireframe opacity */ function setWireframe(bool, color, opacity) { - api.widgets.each(function(w) { w.setWireframe(bool, color, opacity) }); + api.widgets.each((w) => { w.setWireframe(bool, color, opacity) }); space.update(); } @@ -303,19 +303,19 @@ function updateStats(add) { } export const visuals = { - set_progress: setProgress, - set_wireframe: setWireframe, - set_edges: setEdges, - unit_scale: unitScale, - update_speeds: updateSpeeds, - update_slider: updateSlider, - update_slider_max: updateSliderMax, - update_stack_labels: updateStackLabelState, - update_stats: updateStats, - show_slices: showSlices, hide_slices: hideSlices, - show_slider: showSlider, hide_slider: hideSlider, + set_edges: setEdges, + set_progress: setProgress, set_visible_layer: setVisibleLayer, - set_widget_visibility: setWidgetVisibility + set_widget_visibility: setWidgetVisibility, + set_wireframe: setWireframe, + show_slices: showSlices, + show_slider: showSlider, + unit_scale: unitScale, + update_slider_max: updateSliderMax, + update_slider: updateSlider, + update_speeds: updateSpeeds, + update_stack_labels: updateStackLabelState, + update_stats: updateStats }; diff --git a/src/kiri/core/widget.js b/src/kiri/core/widget.js index a1035977..20874102 100644 --- a/src/kiri/core/widget.js +++ b/src/kiri/core/widget.js @@ -703,7 +703,18 @@ class Widget { let minZabove = Math.min(Infinity, ...zover); let shadow = this.#computeShadowAt(z - 0.005, minZabove); if (minZabove < Infinity) { - shadow = POLY.union([...shadow, ...shadows[minZabove]], 0.001, true, { wasm: false }); + shadow = POLY.union([...shadow, ...shadows[minZabove]], 0, true, { wasm: false }); + // cull interior sliver voids + for (let poly of shadow) { + if (poly.inner) { + poly.inner = poly.inner.filter(inr => { + let A = inr.area(); + let P = inr.perimeter(); + let R = (2 * A / P); + return R > 0.05; + }); + } + } } return shadows[z] = POLY.setZ(shadow, z); } diff --git a/src/kiri/core/widgets.js b/src/kiri/core/widgets.js index 39d621ee..94adf087 100644 --- a/src/kiri/core/widgets.js +++ b/src/kiri/core/widgets.js @@ -105,9 +105,9 @@ export const widgets = { replace, opacity(o) { setOpacity(o); console.trace('opacity() deprecated') }, annotate, - setIndexed, setAxisIndex, setColor(c,s) { api.widgets.each(widget => widget.setColor(c,undefined,s) )}, + setIndexed, setOpacity, all() { return WIDGETS.slice() }, add(w) { return WIDGETS.push(w) }, From 341c9711463d74610c9a23cf084a1d9c5e3d622b Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sun, 28 Dec 2025 00:18:24 -0500 Subject: [PATCH 059/154] persist widget transparency --- src/kiri/app/platform.js | 6 ++++ src/kiri/app/view-mode.js | 6 +++- src/kiri/app/visuals.js | 76 +++++++++++++++++++++++++++++++++++---- 3 files changed, 81 insertions(+), 7 deletions(-) diff --git a/src/kiri/app/platform.js b/src/kiri/app/platform.js index 3d8efc81..1fd78121 100644 --- a/src/kiri/app/platform.js +++ b/src/kiri/app/platform.js @@ -549,6 +549,8 @@ function add(widget, shift, nolayout, defer) { api.space.auto_save(); changed(); api.event.emit('widget.add', widget); + // Apply saved visual state AFTER selection/color to avoid being overwritten + api.visuals.apply_visual_state(widget); if (nolayout) { return; } @@ -581,6 +583,10 @@ function platformAddDeferred() { group_done(skiplayout); } api.event.emit('widget.add', deferred.map(r => r.widget)); + // Apply visual state to all deferred widgets after they're positioned + for (let rec of deferred) { + api.visuals.apply_visual_state(rec.widget); + } platform.update_bounds(); api.space.auto_save(); changed(); diff --git a/src/kiri/app/view-mode.js b/src/kiri/app/view-mode.js index 35da8ee0..3cb3adc6 100644 --- a/src/kiri/app/view-mode.js +++ b/src/kiri/app/view-mode.js @@ -54,8 +54,12 @@ function setViewMode(mode) { api.visuals.update_speeds(); api.visuals.set_visible_layer(); api.visuals.set_widget_visibility(true); - api.widgets.setOpacity(1); api.view.set_edges(api.local.getBoolean('model.edges')); + api.view.set_wireframe(api.local.getBoolean('model.wireframe')); + // Only set opacity to 1 if wireframe is disabled + if (!api.local.getBoolean('model.wireframe')) { + api.widgets.setOpacity(1); + } break; case VIEWS.SLICE: $('act-slice').classList.add('selected'); diff --git a/src/kiri/app/visuals.js b/src/kiri/app/visuals.js index 7a351f06..5fe7d357 100644 --- a/src/kiri/app/visuals.js +++ b/src/kiri/app/visuals.js @@ -107,11 +107,33 @@ function setVisibleLayer(h, l) { /** * Set wireframe rendering mode for all widgets. * Updates 3D scene after applying to all widgets. - * @param {boolean} bool - Enable/disable wireframe + * Persists state to local storage. + * @param {boolean|object} bool - Enable/disable wireframe, or {toggle: true} to toggle * @param {number} [color] - Optional wireframe color * @param {number} [opacity] - Optional wireframe opacity */ function setWireframe(bool, color, opacity) { + if (bool && bool.toggle) { + api.local.toggle('model.wireframe'); + } else if (typeof bool === 'boolean') { + api.local.set('model.wireframe', bool); + // Persist color and opacity when explicitly setting wireframe + if (color !== undefined) { + api.local.set('model.wireframe.color', color); + } + if (opacity !== undefined) { + api.local.set('model.wireframe.opacity', opacity); + } + } + bool = api.local.getBoolean('model.wireframe'); + // Retrieve saved color/opacity if not provided + if (color === undefined) { + color = api.local.getInt('model.wireframe.color') || 0; + } + if (opacity === undefined) { + const saved = api.local.getFloat('model.wireframe.opacity'); + opacity = saved !== null ? saved : (api.space.is_dark() ? 0.25 : 0.5); + } api.widgets.each((w) => { w.setWireframe(bool, color, opacity) }); space.update(); } @@ -152,14 +174,22 @@ function updateSliderMax(set) { /** * Hide slice visualization and restore widget rendering. - * Clears stack display, restores model opacity, disables wireframe. + * Clears stack display, restores saved wireframe/opacity state. */ function hideSlices() { STACKS.clear(); - api.widgets.setOpacity(COLOR.model_opacity); - api.widgets.each(function(widget) { - widget.setWireframe(false); - }); + // Restore saved wireframe state instead of forcing wireframe off + const wireframe = api.local.getBoolean('model.wireframe'); + const color = api.local.getInt('model.wireframe.color') || 0; + const saved = api.local.getFloat('model.wireframe.opacity'); + const opacity = saved !== null ? saved : COLOR.model_opacity; + + if (wireframe) { + api.widgets.each(widget => widget.setWireframe(wireframe, color, opacity)); + } else { + api.widgets.setOpacity(opacity); + api.widgets.each(widget => widget.setWireframe(false)); + } } /** @@ -241,6 +271,39 @@ function updateStackLabelState() { } } +/** + * Apply saved visual state to a widget. + * Restores edges and wireframe settings from local storage. + * Only applies in ARRANGE view mode. + * @param {Widget} widget - Widget to apply visual state to + */ +function applyVisualState(widget) { + const viewMode = api.view.get(); + // Only apply visual state in ARRANGE mode + if (viewMode !== VIEWS.ARRANGE) { + return; + } + const edges = api.local.getBoolean('model.edges'); + const wireframe = api.local.getBoolean('model.wireframe'); + + // Always apply edges setting + if (edges) { + widget.setEdges(edges); + } + + // Apply wireframe and its associated opacity + const color = api.local.getInt('model.wireframe.color') || 0; + const saved = api.local.getFloat('model.wireframe.opacity'); + const opacity = saved !== null ? saved : 1; + + if (wireframe) { + widget.setWireframe(wireframe, color, opacity); + } else if (saved !== null) { + // Even if wireframe is off, restore the saved opacity + widget.setOpacity(opacity); + } +} + /** * Update progress bar display. * In debug mode, also shows progress status message. @@ -303,6 +366,7 @@ function updateStats(add) { } export const visuals = { + apply_visual_state: applyVisualState, hide_slices: hideSlices, hide_slider: hideSlider, set_edges: setEdges, From d73f72586a92c804d8a26b95ff472c5eefcdc46d Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sun, 28 Dec 2025 19:55:44 -0500 Subject: [PATCH 060/154] add codec factory option, improve geo vertex caching, fix surface find in indexed mode --- src/kiri/app/conf/defaults.js | 1 + src/kiri/app/conf/manager.js | 2 ++ src/kiri/core/codec.js | 13 +++++++++- src/kiri/core/widget.js | 45 ++++++++++++++++++++++------------ src/kiri/mode/cam/cl-ops.js | 6 +++-- src/kiri/mode/cam/driver-be.js | 4 +-- src/kiri/mode/cam/init-menu.js | 4 +++ src/kiri/mode/cam/init-ui.js | 18 +++++++++++++- src/kiri/mode/cam/slice.js | 1 - 9 files changed, 71 insertions(+), 23 deletions(-) diff --git a/src/kiri/app/conf/defaults.js b/src/kiri/app/conf/defaults.js index a829966e..6ce92a9e 100644 --- a/src/kiri/app/conf/defaults.js +++ b/src/kiri/app/conf/defaults.js @@ -518,6 +518,7 @@ export const conf = { camLatheAngle: 1, camLatheLinear: true, camLatheOver: 0.1, + camLatheDown: 0, camLatheSpeed: 500, camLatheSpindle: 1000, camLatheTool: 1000, diff --git a/src/kiri/app/conf/manager.js b/src/kiri/app/conf/manager.js index a5cbe5cc..81fb7339 100644 --- a/src/kiri/app/conf/manager.js +++ b/src/kiri/app/conf/manager.js @@ -7,6 +7,7 @@ import { codec } from '../../core/codec.js'; import { consts } from '../../core/consts.js'; import { space } from '../../../moto/space.js'; import { local } from '../../../data/local.js'; +import { newWidget } from '../widget.js'; import { utils } from '../../core/utils.js'; import { version } from '../../../moto/license.js'; import { JSZip } from '../../../ext/jszip-esm.js'; @@ -608,6 +609,7 @@ function settingsImport(data, ask) { work.type = 100; } } + codec.setFactory(codec.TYPE.WIDGET, newWidget); codec.decode(data.work).forEach(widget => { api.platform.add(widget, 0, true, true); }); diff --git a/src/kiri/core/codec.js b/src/kiri/core/codec.js index 7f6de9ea..ee102444 100644 --- a/src/kiri/core/codec.js +++ b/src/kiri/core/codec.js @@ -7,6 +7,7 @@ import { Slice, newSlice, Top, newTop } from './slice.js'; import { Widget, newWidget } from './widget.js'; const decoders = {}; +const factories = {}; const freeMem = true; const TYPE = { @@ -213,6 +214,11 @@ function decodePointArray2D(array, z, fn) { return points; } +function setFactory(type, factory) { + factories[type] = factory; + return codec; +} + // ----- Widget Codec ----- Widget.prototype.encode = function(state) { @@ -236,7 +242,8 @@ 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)), + factory = factories[TYPE.WIDGET] ?? newWidget, + widget = factory(id, Widget.Groups.forid(group)), meta= v.meta || widget.meta, anno = v.anno || widget.anno; @@ -480,6 +487,7 @@ registerDecoder(TYPE.LAYERS, function(v, state) { }); export const codec = { + TYPE, encode, decode, registerDecoder, @@ -488,10 +496,12 @@ export const codec = { decodePointArray, encodePointArray2D, decodePointArray2D, + setFactory, toCodable }; export { + TYPE, encode, decode, registerDecoder, @@ -500,6 +510,7 @@ export { decodePointArray, encodePointArray2D, decodePointArray2D, + setFactory, toCodable }; diff --git a/src/kiri/core/widget.js b/src/kiri/core/widget.js index 20874102..78591324 100644 --- a/src/kiri/core/widget.js +++ b/src/kiri/core/widget.js @@ -181,9 +181,13 @@ class Widget { return this.loadVertices(...arguments); } - setModified() { + setModified(reason) { this.modified = true; this.boundingBoxNeedsUpdate = true; + this.clearShadows(); + if (reason !== 'axis') { + this.cache.geo = undefined; + } if (this.mesh && this.mesh.geometry) { // this fixes ray intersections after the mesh is modified this.mesh.geometry.boundingSphere = null; @@ -355,7 +359,7 @@ class Widget { // invalidate cached points if (x || y || z) { this.points = null; - this.setModified(); + this.setModified('moveMesh'); } } @@ -372,11 +376,11 @@ class Widget { } setAxisIndex(deg) { + // console.trace(deg); let rad = deg * (Math.PI / 180); if (rad !== this.track.indexRad) { this.track.indexRad = rad; - this.clearShadows(); - this.setModified(); + this.setModified('axis'); this._updateMeshPosition(); } } @@ -429,7 +433,7 @@ class Widget { pos.z += (z || 0); } if (x || y || z) { - this.setModified(); + this.setModified('_move'); this._updateMeshPosition(); } } @@ -475,7 +479,7 @@ class Widget { scale.x *= (x || 1.0); scale.y *= (y || 1.0); scale.z *= (z || 1.0); - this.setModified(); + this.setModified('_scale'); } rotate(x, y, z, temp, center = true) { @@ -514,7 +518,7 @@ class Widget { rot.y += (y || 0); rot.z += (z || 0); } - this.setModified(); + this.setModified('_rotate'); } // undo all accumulated rotations @@ -524,7 +528,7 @@ class Widget { }); this.roto = []; this.center(); - this.setModified(); + this.setModified('unrotate'); if (this.refreshVisualState) this.refreshVisualState(); } @@ -563,7 +567,7 @@ class Widget { } pos.needsUpdate = true; ot.mirror = !ot.mirror; - this.setModified(); + this.setModified('_mirror'); this.points = null; } @@ -586,6 +590,17 @@ class Widget { getGeoVertices(opt = {}) { const { unroll, translate } = opt; + let cacheKey = [ + unroll ? 1 : 0, + translate ? 1 : 0, + ((this.track.indexRad ?? 0) * 100000) | 0 + ].join(','); + let marked = Date.now(); + let geoCache = this.cache.geo = (this.cache.geo || {}); + let cached = geoCache[cacheKey]; + if (cached) { + return cached.pos; + } let geo = this.mesh.geometry; let pos = geo.getAttribute('position'); // if indexed, return points rotated about X and then offset @@ -595,6 +610,7 @@ class Widget { .applyMatrix4( new THREE.Matrix4().makeRotationX(-track.indexRad) ); } pos = pos.array; + // unroll indexed geometry if (geo.index && unroll !== false) { let idx = geo.index.array; let len = idx.length; @@ -608,12 +624,8 @@ class Widget { pp2[inc++] = pos[ip]; } pos = pp2; - } else if (translate) { - pos = pos.slice(); } - // used by CAM.shadowAt() - this.cache.geo = pos; - delete this.cache.shadow; + geoCache[cacheKey] = { pos, marked }; return pos; } @@ -631,7 +643,7 @@ class Widget { if (!this.bounds || refresh || this.boundingBoxNeedsUpdate) { this.bounds = new THREE.Box3().setFromArray(this.getGeoVertices({ translate: true, - unroll: false + unroll: true })); this.boundingBoxNeedsUpdate = false; } @@ -701,6 +713,7 @@ class Widget { // find closest shadow above and use to speed up delta shadow gen let zover = Object.keys(shadows).map(v => parseFloat(v)).filter(v => v > z); let minZabove = Math.min(Infinity, ...zover); + // shift shadow probeline down a fraction to capture z flats with FP noise let shadow = this.#computeShadowAt(z - 0.005, minZabove); if (minZabove < Infinity) { shadow = POLY.union([...shadow, ...shadows[minZabove]], 0, true, { wasm: false }); @@ -725,7 +738,7 @@ class Widget { if (this.cache.shadow) { return; } - const geo = this.cache.geo; + const geo = this.getGeoVertices({ unroll: true, translate: true }); const length = geo.length; const bounds = this.getBoundingBox(); const stack = {}; diff --git a/src/kiri/mode/cam/cl-ops.js b/src/kiri/mode/cam/cl-ops.js index fc03e33f..fb1ae0a6 100644 --- a/src/kiri/mode/cam/cl-ops.js +++ b/src/kiri/mode/cam/cl-ops.js @@ -350,8 +350,9 @@ export function createPopOps() { createPopOp('lathe', { tool: 'camLatheTool', spindle: 'camLatheSpindle', - step: 'camLatheOver', angle: 'camLatheAngle', + step: 'camLatheOver', + down: 'camLatheDown', rate: 'camLatheSpeed', tolerance: 'camTolerance', filter: 'camContourFilter', @@ -365,8 +366,9 @@ export function createPopOps() { spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: toInt, show: hasSpindle }), rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: toInt, units }), sep: UC.newBlank({ class: "pop-sep" }), - step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: toFloat, bound: UC.bound(0.01, 100.0) }), angle: UC.newInput(LANG.cc_sang_s, { title: LANG.cc_sang_l, convert: toFloat, bound: UC.bound(0.01, 180.0) }), + step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: toFloat, bound: UC.bound(0.01, 100.0) }), + // down: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: toFloat, units }), sep: UC.newBlank({ class: "pop-sep" }), offStart: UC.newInput(LANG.ci_laso_s, { title: LANG.ci_laso_l, convert: toFloat}), offEnd: UC.newInput(LANG.ci_laeo_s, { title: LANG.ci_laeo_l, convert: toFloat}), diff --git a/src/kiri/mode/cam/driver-be.js b/src/kiri/mode/cam/driver-be.js index d4d6b39c..2212908e 100644 --- a/src/kiri/mode/cam/driver-be.js +++ b/src/kiri/mode/cam/driver-be.js @@ -18,9 +18,9 @@ function surface_prep(widget, index) { } }; -function surface_find(widget, faces, radians) { +function surface_find(widget, faces, radians, filterZ) { surface_prep(widget); - return widget.tool.findConnectedSurface(faces, radians || 0, 0.0); + return widget.tool.findConnectedSurface(faces, radians, filterZ); }; function init(worker) { diff --git a/src/kiri/mode/cam/init-menu.js b/src/kiri/mode/cam/init-menu.js index 5b660b2e..d061b1c0 100644 --- a/src/kiri/mode/cam/init-menu.js +++ b/src/kiri/mode/cam/init-menu.js @@ -114,6 +114,10 @@ export function menu() { camStockOffset: newBoolean(LANG.cs_offs_s, onBooleanClick, {title:LANG.cs_offs_l}), camStockIndexed: newBoolean(LANG.cs_indx_s, onBooleanClick, {title:LANG.cs_indx_l}), camStockIndexGrid: newBoolean(LANG.cs_ishg_s, onBooleanClick, {title:LANG.cs_ishg_l, show:() => ui.camStockIndexed.checked}), + // separator: newBlank({ class:"set-sep", driven }), + // camStockManual: newRow([ + // (ui.stockPlace = newButton('position', onButtonClick, { })), + // ], {class:"ext-buttons f-row"}), _____: newGroup(LANG.cc_menu, $('cam-limits'), { modes:CAM, driven, separator }), camZAnchor: newSelect(LANG.ou_zanc_s, {title: LANG.ou_zanc_l, action:zAnchorSave, show:() => !ui.camStockIndexed.checked}, "zanchor"), camZOffset: newInput(LANG.ou_ztof_s, {title:LANG.ou_ztof_l, convert:toFloat, units}), diff --git a/src/kiri/mode/cam/init-ui.js b/src/kiri/mode/cam/init-ui.js index 706cb456..81a2ed94 100644 --- a/src/kiri/mode/cam/init-ui.js +++ b/src/kiri/mode/cam/init-ui.js @@ -792,8 +792,24 @@ export function init() { // TAB/TRACE BUTTON HANDLERS api.event.on("button.click", target => { - let process = api.conf.get().process; + let { process, stock } = api.conf.get(); switch (target) { + case api.ui.stockPlace: + api.uc.prompt("stock x,y,z position", "").then(csv => { + if (csv) { + let [x,y,z] = csv.split(',').map(v => parseInt(v) || 0); + let center = { + x: x ?? 0, + y: y ?? 0, + z: z ?? 0 + } + // move widgets based on postion + // more complex than just moving stock + // because stock is based on contents, not the other way + console.log({ x,y,z }); + } + }); + return; case api.ui.tabAdd: return tabAdd(); case api.ui.tabDun: diff --git a/src/kiri/mode/cam/slice.js b/src/kiri/mode/cam/slice.js index 70597045..4c260399 100644 --- a/src/kiri/mode/cam/slice.js +++ b/src/kiri/mode/cam/slice.js @@ -241,7 +241,6 @@ export async function cam_slice(settings, widget, onupdate, ondone) { } async function computeShadows() { - // console.log('(re)compute shadows'); await new OPS.shadow(state, { type: "shadow", silent: true }).slice(progress => { // console.log('reshadowing', progress.round(3)); }); From 69493a0dafff4f171d4ff38bd1b559ff468acf8b Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sun, 28 Dec 2025 23:11:44 -0500 Subject: [PATCH 061/154] start lathe step down --- src/geo/polygon.js | 10 +++++++++ src/kiri/app/conf/defaults.js | 1 + src/kiri/mode/cam/cl-ops.js | 9 +++++++- src/kiri/mode/cam/topo4.js | 39 ++++++++++++++++++++++++++++++++++- web/kiri/lang/en.js | 4 ++++ 5 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/geo/polygon.js b/src/geo/polygon.js index 81257832..4b2a52f6 100644 --- a/src/geo/polygon.js +++ b/src/geo/polygon.js @@ -629,6 +629,16 @@ export class Polygon { return minZ; } + maxZ() { + let maxZ = Math.max(...this.points.map(p => p.z)); + if (this.inner) { + for (let i of this.inner) { + maxZ = Math.max(minZ, i.maxZ()); + } + } + return maxZ; + } + avgZ() { return [...this.points.map(p => p.z)].reduce((a,v) => a+v) / this.points.length; } diff --git a/src/kiri/app/conf/defaults.js b/src/kiri/app/conf/defaults.js index 6ce92a9e..bf30c871 100644 --- a/src/kiri/app/conf/defaults.js +++ b/src/kiri/app/conf/defaults.js @@ -519,6 +519,7 @@ export const conf = { camLatheLinear: true, camLatheOver: 0.1, camLatheDown: 0, + camLatheExpand: 0, camLatheSpeed: 500, camLatheSpindle: 1000, camLatheTool: 1000, diff --git a/src/kiri/mode/cam/cl-ops.js b/src/kiri/mode/cam/cl-ops.js index fb1ae0a6..8bcb2da4 100644 --- a/src/kiri/mode/cam/cl-ops.js +++ b/src/kiri/mode/cam/cl-ops.js @@ -139,7 +139,11 @@ export function createPopOp(type, map) { for (let inp of Object.values(op.inputs)) { let parent = inp.parentElement; if (parent && parent.setVisible && parent.__opt.show) { + // for normal inputs parent.setVisible(parent.__opt.show(op, api.conf.get())); + } else if (inp.__opt?.show) { + // for blanks + inp.setVisible(inp.__opt.show(op, api.conf.get())); } } }, @@ -353,6 +357,7 @@ export function createPopOps() { angle: 'camLatheAngle', step: 'camLatheOver', down: 'camLatheDown', + expand: 'camLatheExpand', rate: 'camLatheSpeed', tolerance: 'camTolerance', filter: 'camContourFilter', @@ -368,7 +373,9 @@ export function createPopOps() { sep: UC.newBlank({ class: "pop-sep" }), angle: UC.newInput(LANG.cc_sang_s, { title: LANG.cc_sang_l, convert: toFloat, bound: UC.bound(0.01, 180.0) }), step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: toFloat, bound: UC.bound(0.01, 100.0) }), - // down: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: toFloat, units }), + sepDn: UC.newBlank({ class: "pop-sep", show: () => env.poppedRec.down > 0 }), + down: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: toFloat, units }), + expand: UC.newInput(LANG.cl_xpnd_s, { title: LANG.cl_xpnd_l, convert: toFloat, units, show: () => env.poppedRec.down > 0 }), sep: UC.newBlank({ class: "pop-sep" }), offStart: UC.newInput(LANG.ci_laso_s, { title: LANG.ci_laso_l, convert: toFloat}), offEnd: UC.newInput(LANG.ci_laeo_s, { title: LANG.ci_laeo_l, convert: toFloat}), diff --git a/src/kiri/mode/cam/topo4.js b/src/kiri/mode/cam/topo4.js index 6352a609..5b7a925f 100644 --- a/src/kiri/mode/cam/topo4.js +++ b/src/kiri/mode/cam/topo4.js @@ -42,7 +42,9 @@ export class Topo { tolerance = op.tolerance, 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; + angle = this.angle = op.angle || 1, + down = this.down = op.down, + expand = this.expand = op.expand; if (tool.isTaperMill() && step === 0) { step = this.step = op.step * tool.unitScale(); @@ -160,6 +162,7 @@ export class Topo { async sliceGPU(onupdate) { const { angle, diam, leave, linear, offStart, offEnd, resolution, tool, units, vertices, zBottom } = this; + const { down, expand } = this; // invert tool Z offset for gpu code let toolBounds = new THREE.Box3() @@ -240,6 +243,40 @@ export class Topo { slices.push(slice); } } + let maxZ = 0; + if (down) { + for (let r of rows) + for (let p of r) { + maxZ = Math.max(maxZ, p.z); + } + console.log({ maxZ, down, expand }); + maxZ += expand; + } + if (down) { + for (let slice of slices) { + let z = slice.z; + let poly = slice.camLines[0].clone(); + let pMinZ = poly.minZ(); + let steps = Math.floor((maxZ - pMinZ) / down); + let stack = [ poly.clone() ]; + for (let i=0; i { + let slice = newSlice(z); + slice.camLines = [ poly ]; + return slice; + }); + // todo: add line to line reversals + // todo: add move to maxZ after stack is run + } + slices = this.gpu_slices = slices.map(slice => slice.stack).flat(); + slices.forEach((slice,index) => slice.index = index); + } for (let slice of slices) { slice.output() .setLayer("lathe", { line: this.lineColor }) diff --git a/web/kiri/lang/en.js b/web/kiri/lang/en.js index 709ee39c..93eba674 100644 --- a/web/kiri/lang/en.js +++ b/web/kiri/lang/en.js @@ -714,6 +714,10 @@ self.lang['en-us'] = { ct_nabl_s: "auto", ct_nabl_l: ["auto generate radial tabs","projected from part center","using count and angle offset"], + // CNC LATHE + cl_xpnd_s: "expand", + cl_xpnd_l: ["grow starting radius for step down. radius is defined as the highest Z point measured from the X axis center of the part"], + // OUTPUT ou_menu: "output", From 07da144aee4e23d999c68017f393ee9508937fb6 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Mon, 29 Dec 2025 17:53:17 -0500 Subject: [PATCH 062/154] apply axis rotations to trace lines in op area --- src/geo/polygon.js | 7 ++++--- src/kiri/mode/cam/op-area.js | 6 +++++- src/kiri/mode/cam/slice.js | 6 ++++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/geo/polygon.js b/src/geo/polygon.js index 4b2a52f6..add36b53 100644 --- a/src/geo/polygon.js +++ b/src/geo/polygon.js @@ -794,11 +794,12 @@ export class Polygon { return [this.first().key, this.last().key, this.length].join('~~'); } - applyRotations() { + applyRotations(deg) { for (let point of this.points) { - if (point.a) { + let angleDeg = deg ?? point.a; + if (angleDeg) { let p2 = new Vector3(point.x, point.y, point.z) - .applyAxisAngle(XAXIS, point.a * DEG2RAD); + .applyAxisAngle(XAXIS, angleDeg * DEG2RAD); point.x = p2.x; point.y = p2.y; point.z = p2.z; diff --git a/src/kiri/mode/cam/op-area.js b/src/kiri/mode/cam/op-area.js index 7764f07d..4e1aad80 100644 --- a/src/kiri/mode/cam/op-area.js +++ b/src/kiri/mode/cam/op-area.js @@ -29,7 +29,7 @@ class OpArea extends CamOp { let { op, state } = this; let { direction, down, expand, flats, flatOff, follow } = op; let { mode, outline, over, rename, smooth, tool } = op; - let { addSlices, color, cutTabs, settings } = state; + let { addSlices, axisIndex, color, cutTabs, settings } = state; let { shadowAt, setToolDiam, tabs, widget, workarea } = state; let areaTool = new Tool(settings, tool); @@ -75,6 +75,10 @@ class OpArea extends CamOp { let poly = newPolygon().fromArray(arr); aminz = Math.min(aminz, poly.minZ()); polys.push(poly); + if (axisIndex) { + // traces come from unrotatead widget edges + poly.applyRotations(axisIndex); + } } // connect open poly edge segments into closed loops (when possible) diff --git a/src/kiri/mode/cam/slice.js b/src/kiri/mode/cam/slice.js index 4c260399..9cd12de4 100644 --- a/src/kiri/mode/cam/slice.js +++ b/src/kiri/mode/cam/slice.js @@ -180,6 +180,8 @@ export async function cam_slice(settings, widget, onupdate, ondone) { let slicer; let state = { addSlices, + axisIndex, + axisRotation, bounds, color, computeShadows, @@ -261,8 +263,8 @@ export async function cam_slice(settings, widget, onupdate, ondone) { } async function setAxisIndex(degrees = 0, absolute = true) { - axisIndex = absolute ? degrees : (axisIndex || 0) + degrees; - axisRotation = (Math.PI / 180) * axisIndex; + state.axisIndex = axisIndex = absolute ? degrees : (axisIndex || 0) + degrees; + state.axisRotation = axisRotation = (Math.PI / 180) * axisIndex; widget.setAxisIndex(isIndexed ? -axisIndex : 0); state.tabs = tabs = []; for (let tab of tabW) { From 841082c4850ce635bc445977b534b84eba5115d1 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Mon, 29 Dec 2025 22:21:03 -0500 Subject: [PATCH 063/154] fix frame api --- src/kiri/app/frame.js | 3 +++ src/kiri/app/init/input.js | 1 + src/kiri/mode/cam/prepare.js | 5 ++++- src/kiri/mode/cam/topo3.js | 2 +- src/kiri/run/engine.js | 4 +++- src/kiri/run/frame.js | 2 +- 6 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/kiri/app/frame.js b/src/kiri/app/frame.js index ca814ae3..c1232b1a 100644 --- a/src/kiri/app/frame.js +++ b/src/kiri/app/frame.js @@ -115,6 +115,9 @@ window.addEventListener('message', msg => { let bin = data.parse; let widget; switch ((data.type || 'stl').toLowerCase()) { + case 'gcode': + api.function.parse(bin, 'gcode'); + break; case 'stl': if (!bin.buffer) bin = new Float32Array(bin).buffer; new load.STL().parse(bin, vertices => { diff --git a/src/kiri/app/init/input.js b/src/kiri/app/init/input.js index bb9abe08..fecbf8e2 100644 --- a/src/kiri/app/init/input.js +++ b/src/kiri/app/init/input.js @@ -28,6 +28,7 @@ import { space } from '../../../moto/space.js'; import { LAST, MODES, SEED, VIEWS } from '../../core/consts.js'; import STACKS from '../stacks.js'; +import '../frame.js'; let { SETUP } = api.const, { CAM, SLA, FDM, LASER, DRAG, WJET, WEDM } = MODES, diff --git a/src/kiri/mode/cam/prepare.js b/src/kiri/mode/cam/prepare.js index e1ecbac2..e22f009f 100644 --- a/src/kiri/mode/cam/prepare.js +++ b/src/kiri/mode/cam/prepare.js @@ -870,10 +870,13 @@ export async function prepare_one(widget, settings, print, firstPoint, update) { // we're coming from another widget. offset compensated below printPoint = firstPoint; // console.log('coming from another widget', { printPoint }); - } else { + } else if (center) { // we're the first widget output. offset is center printPoint = origin.clone().move({ x: center.x, y: center.y }); // console.log('first widget output', { printPoint }); + } else { + console.log({ missing_center_using_origin: origin }); + printPoint = origin.clone(); } let ops = { diff --git a/src/kiri/mode/cam/topo3.js b/src/kiri/mode/cam/topo3.js index 5a34b1eb..c53aa0eb 100644 --- a/src/kiri/mode/cam/topo3.js +++ b/src/kiri/mode/cam/topo3.js @@ -241,7 +241,7 @@ export class Topo { } } // raise output points when inside tab boundaries - if (!inside && tabsOn && clipTab.length) + if (tabsOn && clipTab.length) for (let poly of lines) { for (let p of poly.points) { for (let clip of clipTab) { diff --git a/src/kiri/run/engine.js b/src/kiri/run/engine.js index deb29d57..c8d995cd 100644 --- a/src/kiri/run/engine.js +++ b/src/kiri/run/engine.js @@ -123,6 +123,7 @@ class Engine { process.camStockX = stock.x; process.camStockY = stock.y; process.camStockZ = stock.z; + if (this.origin) settings.stock.center = origin; return this; } @@ -131,7 +132,8 @@ class Engine { } setOrigin(x, y, z) { - this.settings.origin = { x, y, z }; + this.origin = { x, y, z }; + if (this.settings.stock) this.settings.stock.center = { x, y, z }; return this; } diff --git a/src/kiri/run/frame.js b/src/kiri/run/frame.js index b363f2c1..61fdefeb 100644 --- a/src/kiri/run/frame.js +++ b/src/kiri/run/frame.js @@ -47,7 +47,7 @@ window.addEventListener('message', onMessage); }, - send: send, + send, load: (load) => { send({ load }) }, From 3e56c3e965f9d53422d546285028ef9395712f2f Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Tue, 30 Dec 2025 11:39:47 -0500 Subject: [PATCH 064/154] hide retract when fw retract enabled. fix sizeX rescale --- src/geo/slicer.js | 2 +- src/kiri/app/init/input.js | 2 +- src/kiri/app/selected.js | 6 +++--- src/kiri/mode/fdm/init-menu.js | 8 ++++++-- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/geo/slicer.js b/src/geo/slicer.js index 88e575c5..f040ca8b 100644 --- a/src/geo/slicer.js +++ b/src/geo/slicer.js @@ -566,7 +566,7 @@ export function sliceConnect(input, z, opt = {}) { // emit a polygon if it can be cleaned and still have 2 or more points function emit(poly) { let gap = poly.first().distTo2D(poly.last()); - if (opt.strict && gap > bridgeMax) { + if (poly.open && opt.strict && gap > bridgeMax) { if (debug) console.log('drop', gap, poly); return; } diff --git a/src/kiri/app/init/input.js b/src/kiri/app/init/input.js index fecbf8e2..53d23801 100644 --- a/src/kiri/app/init/input.js +++ b/src/kiri/app/init/input.js @@ -230,7 +230,7 @@ export function init_input() { resolutionY: newInput(LANG.dv_rezy_s, {title:LANG.dv_rezy_l, convert:toInt, size:6, modes:SLA}), _____: newDiv({ class: "f-col t-body t-inset", addto: $('dev-config'), set:true, modes:NO_WEDM }), _____: newGroup("firmware", null, {group:"dext", inline, modes:NO_WEDM}), - fwRetract: newBoolean(LANG.dv_retr_s, onBooleanClick, {title:LANG.dv_retr_l, modes:FDM}), + fwRetract: newBoolean(LANG.dv_retr_s, onBooleanClick, {title:LANG.dv_retr_l, modes:FDM, trigger}), deviceOrigin: newBoolean(LANG.dv_orgc_s, onBooleanClick, {title:LANG.dv_orgc_l, modes:FDM_LZN, show:() => !ui.deviceRound.checked}), deviceRound: newBoolean(LANG.dv_bedc_s, onBooleanClick, {title:LANG.dv_bedc_l, modes:FDM, trigger, show:isNotBelt}), deviceBelt: newBoolean(LANG.dv_belt_s, onBooleanClick, {title:LANG.dv_belt_l, modes:FDM, trigger, show:() => !ui.deviceRound.checked}), diff --git a/src/kiri/app/selected.js b/src/kiri/app/selected.js index b78548f0..b4421645 100644 --- a/src/kiri/app/selected.js +++ b/src/kiri/app/selected.js @@ -479,9 +479,9 @@ function input_resize(e, ui) { xc = ui.lock.X.checked, yc = ui.lock.Y.checked, zc = ui.lock.Z.checked, - xt = ta === ui.sizeX, - yt = ta === ui.sizeY, - zt = ta === ui.sizeZ, + xt = ta === ui.size.X, + yt = ta === ui.size.Y, + zt = ta === ui.size.Z, tl = (xt && xc) || (yt && yc) || (zt && zc), xr = ((tl && xc) || (!tl && xt) ? ra : 1), yr = ((tl && yc) || (!tl && yt) ? ra : 1), diff --git a/src/kiri/mode/fdm/init-menu.js b/src/kiri/mode/fdm/init-menu.js index 593e7387..121a87be 100644 --- a/src/kiri/mode/fdm/init-menu.js +++ b/src/kiri/mode/fdm/init-menu.js @@ -61,6 +61,10 @@ function isNotBelt() { return !isBelt(); } +function notFwRetract() { + return !api.conf.get().device.fwRetract; +} + export function menu() { return { @@ -160,8 +164,8 @@ export function menu() { outputFillMult: newInput(LANG.ou_flml_s, {title:LANG.ou_exml_l, convert:toFloat, bound:bound(0.0,2.0)}), outputSparseMult: newInput(LANG.ou_spml_s, {title:LANG.ou_exml_l, convert:toFloat, bound:bound(0.0,2.0)}), separator: newBlank({ class:"set-sep", driven }), - outputRetractDist: newInput(LANG.ad_rdst_s, {title:LANG.ad_rdst_l, convert:toFloat}), - outputRetractSpeed: newInput(LANG.ad_rrat_s, {title:LANG.ad_rrat_l, convert:toInt}), + outputRetractDist: newInput(LANG.ad_rdst_s, {title:LANG.ad_rdst_l, convert:toFloat, show:notFwRetract}), + outputRetractSpeed: newInput(LANG.ad_rrat_s, {title:LANG.ad_rrat_l, convert:toInt, show:notFwRetract}), outputRetractWipe: newInput(LANG.ad_wpln_s, {title:LANG.ad_wpln_l, convert:toFloat, bound:bound(0.0,10)}), separator: newBlank({ class:"set-sep", driven }), outputAvoidGaps: newBoolean(LANG.ad_agap_s, onBooleanClick, {title:LANG.ad_agap_l}), From c7bd1075671cb08640b98f9f8c0441439d83135d Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Tue, 30 Dec 2025 12:06:19 -0500 Subject: [PATCH 065/154] filter slice shells with miniscule areas --- src/kiri/core/slice.js | 19 +++++++++++++++---- src/kiri/mode/fdm/slice.js | 2 +- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/kiri/core/slice.js b/src/kiri/core/slice.js index 4db028c5..01cd5cd5 100644 --- a/src/kiri/core/slice.js +++ b/src/kiri/core/slice.js @@ -123,9 +123,9 @@ class Slice { return slice ? POLY.fingerprintCompare(this.fingerprint(), slice.fingerprint()) : false; }; - addTops(polys) { + addTops(polys, opt) { polys.forEach(p => { - this.addTop(p); + this.addTop(p, opt); }); return this; } @@ -134,16 +134,23 @@ class Slice { * @param {Object | Polygon} data * @returns Slice.Top */ - addTop(data) { + addTop(data, opt) { + let minArea = opt.minArea ?? 0; if (data.length) { // standard legacy polygon let top = new Top(data); + if (top.area() < minArea) { + return null; + } this.tops.push(top); top.simple = data; return top; } else { // create top object from object bundle passed back by slicePost() let top = new Top(data.poly); + if (top.area() < minArea) { + return null; + } top.thin_fill = data.thin_fill?.map(p => newPoint(p.x,p.y,p.z)); top.thin_wall = data.thin_wall; top.thin_sort = data.thin_sort; @@ -152,7 +159,7 @@ class Slice { top.fill_off = data.fill_off; top.last = data.last; top.gaps = data.gaps; - top.shells = data.shells; + top.shells = data.shells.filter(p => p.area() > minArea); top.simple = data.simple; this.tops.push(top); return top; @@ -208,6 +215,10 @@ class Top { this.poly = polygon; // outline poly } + area() { + return this.poly.area(); + } + clone(deep) { let top = new Top(this.poly.clone(deep)); return top; diff --git a/src/kiri/mode/fdm/slice.js b/src/kiri/mode/fdm/slice.js index 2a99d529..285d18ea 100644 --- a/src/kiri/mode/fdm/slice.js +++ b/src/kiri/mode/fdm/slice.js @@ -337,7 +337,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) { return output.slices.map(data => { let { z, clip, lines, groups, changes } = data; if (!data.tops) return null; - let slice = newSlice(z).addTops(data.tops); + let slice = newSlice(z).addTops(data.tops, { minArea: 0.1 }); slice.index = indices.indexOf(z); slice.height = heights[slice.index]; slice.clips = clip; From d5062880e64daccaecf559a4d2e967b03d62d217 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Tue, 30 Dec 2025 13:38:39 -0500 Subject: [PATCH 066/154] add lathe step down line reversal --- src/kiri/core/slice.js | 2 +- src/kiri/mode/cam/topo4.js | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/kiri/core/slice.js b/src/kiri/core/slice.js index 01cd5cd5..606692a4 100644 --- a/src/kiri/core/slice.js +++ b/src/kiri/core/slice.js @@ -134,7 +134,7 @@ class Slice { * @param {Object | Polygon} data * @returns Slice.Top */ - addTop(data, opt) { + addTop(data, opt = {}) { let minArea = opt.minArea ?? 0; if (data.length) { // standard legacy polygon diff --git a/src/kiri/mode/cam/topo4.js b/src/kiri/mode/cam/topo4.js index 5b7a925f..9d8d4468 100644 --- a/src/kiri/mode/cam/topo4.js +++ b/src/kiri/mode/cam/topo4.js @@ -228,7 +228,7 @@ export class Topo { let slice = newSlice(i); slice.index = i; slice.camLines = [ newPolygon(rows[i]).setOpen() ]; - if (i % 2 === 1) slice.camLines[0].reverse(); + if (!down && i % 2 === 1) slice.camLines[0].reverse(); slices.push(slice); } } else { @@ -237,7 +237,7 @@ export class Topo { let slice = newSlice(i); let points = rows.map(row => row[i]); points.push(points[0].clone().setA(-360)); - if (i % 2 === 1) points.reverse(); + if (!down && i % 2 === 1) points.reverse(); slice.index = i; slice.camLines = [ newPolygon(points).setOpen() ]; slices.push(slice); @@ -249,10 +249,12 @@ export class Topo { for (let p of r) { maxZ = Math.max(maxZ, p.z); } - console.log({ maxZ, down, expand }); maxZ += expand; } if (down) { + // step "up" points on trace until they max out + // then reverse the stack to cut top down + let reverse = 0; for (let slice of slices) { let z = slice.z; let poly = slice.camLines[0].clone(); @@ -267,12 +269,17 @@ export class Topo { } stack.reverse(); slice.stack = stack.map(poly => { + if (reverse++ % 2 === 1) poly.reverse(); let slice = newSlice(z); slice.camLines = [ poly ]; return slice; }); - // todo: add line to line reversals - // todo: add move to maxZ after stack is run + // first move of new stack is to maxZ (problematic) + // let first = stack[0].points; + // first.splice(0,0,first[0].clone().setZ(maxZ)); + // last move of stack is to maxZ + let last = stack.peek(); + last.push(last.last().clone().setZ(maxZ)); } slices = this.gpu_slices = slices.map(slice => slice.stack).flat(); slices.forEach((slice,index) => slice.index = index); From 60f00ae145a4dc3b95197fef42c4bb8eb819d8d3 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Tue, 30 Dec 2025 13:39:56 -0500 Subject: [PATCH 067/154] only show lathe step down when gpu enabled --- src/kiri/mode/cam/cl-ops.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kiri/mode/cam/cl-ops.js b/src/kiri/mode/cam/cl-ops.js index 8bcb2da4..c1a970b7 100644 --- a/src/kiri/mode/cam/cl-ops.js +++ b/src/kiri/mode/cam/cl-ops.js @@ -374,7 +374,7 @@ export function createPopOps() { angle: UC.newInput(LANG.cc_sang_s, { title: LANG.cc_sang_l, convert: toFloat, bound: UC.bound(0.01, 180.0) }), step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: toFloat, bound: UC.bound(0.01, 100.0) }), sepDn: UC.newBlank({ class: "pop-sep", show: () => env.poppedRec.down > 0 }), - down: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: toFloat, units }), + down: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: toFloat, units, show: isWebGPU }), expand: UC.newInput(LANG.cl_xpnd_s, { title: LANG.cl_xpnd_l, convert: toFloat, units, show: () => env.poppedRec.down > 0 }), sep: UC.newBlank({ class: "pop-sep" }), offStart: UC.newInput(LANG.ci_laso_s, { title: LANG.ci_laso_l, convert: toFloat}), From 88be7567034e5cbaa6da51ec973426c2408b3361 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Tue, 30 Dec 2025 21:09:09 -0500 Subject: [PATCH 068/154] re-design and refactor slice line reconnection --- src/geo/base.js | 4 +- src/geo/slicer.js | 271 ++++++++++++++++++++++++------------- src/kiri/app/api.js | 4 +- src/kiri/mode/fdm/slice.js | 4 +- 4 files changed, 187 insertions(+), 96 deletions(-) diff --git a/src/geo/base.js b/src/geo/base.js index 5bc1b0d1..6cfe986d 100644 --- a/src/geo/base.js +++ b/src/geo/base.js @@ -676,8 +676,8 @@ export const config = { // default # of decimal places in generated gcode gcode_decimals: 4, // heal disjoint polygons in slicing (experimental) - bridgeLineGapDistance: 0.05, - bridgeLineGapDistanceMax: 25, + bridgeLineGapDistance: 0.05, // may no longer be used + bridgeLineGapDistanceMax: 3, // Bounds default margin nearTo // Polygon.offset mindist2 offset precision precision_offset: 0.05, diff --git a/src/geo/slicer.js b/src/geo/slicer.js index f040ca8b..4889a8cb 100644 --- a/src/geo/slicer.js +++ b/src/geo/slicer.js @@ -380,7 +380,7 @@ export async function sliceZ(z, points, options = {}) { let rval = { z, lines }; if (groupFn) { - let groups = groupFn(lines, z, options); + let groups = groupFn(lines, options); if (options.xor) { groups = polygons.xor(groups); } @@ -448,12 +448,12 @@ export async function sliceZ(z, points, options = {}) { * interesection. Eliminate used points and repeat. Unjoined lines * are permitted and handled after all other cases are handled. * - * @param {Line[]} input - * @param {number} [index] + * @param {Line[]} input array of lines (soup) + * @param {Object} opt options * @returns {Array} */ -export function sliceConnect(input, z, opt = {}) { - let { debug, both } = opt; +export function sliceConnect(input, opt = {}) { + let { both, debug, dirty, strict } = opt; if (both) { if (debug) console.log('unable to connect lines sliced with "both" option'); @@ -466,8 +466,8 @@ export function sliceConnect(input, z, opt = {}) { output = [], connect = [], emitted = 0, - forks = false, - frays = false, + forks = [], // points which contain forks + stubs = [], // dead end points bridge = config.bridgeLineGapDistance, bridgeMax = config.bridgeLineGapDistanceMax, p1, p2, gl; @@ -480,11 +480,17 @@ export function sliceConnect(input, z, opt = {}) { return p; } + // add p2 to p1's group array function addConnected(p1, p2) { + if (p1 === p2) { + // console.log('drop same point connect'); + return; + } if (!p1.group) p1.group = [ p2 ]; - else p1.group.push(p2); + else p1.group.addOnce(p2); } + // calculate and store array perimeter length function perimeter(array) { if (!array.perimeter) { array.perimeter = newPolygon().addPoints(array).perimeter(); @@ -492,147 +498,230 @@ export function sliceConnect(input, z, opt = {}) { return array.perimeter; } - /** - * follow points through connected lines to form candidate output paths - */ - function findNextPath(point, current, branches, depth = 1) { - let path = []; - if (current) { - current.push(path); + // calculate and store array area + function area(array) { + if (!array.area) { + array.area = newPolygon().addPoints(array).area(); } + return array.area; + } + + function debug_points() { + let clone = points.map(p => { + let pc = p.clone(); + pc.group = p.group.slice(); + return pc; + }); + clone.sort((a,b) => a._key < b._key ? -1 : 1); + console.log({ points: clone }); + } + + /** + * construct a new path starting at point (may be a branch) + * @param {Point} point current point we're following + * @param {Point} base first point for start of all paths + * @param {Array} branches collected branches when recursing + * @param {Integer} depth recursion depth + */ + function findNextPath(point, { base, branches, depth, path } = { }) { + let marked = []; + + base = base ?? point; + branches = branches ?? []; + depth = depth ?? 1; + path = path ?? []; + + branches.push(path); for (;;) { - // prevent point re-use + // if the next point is the root, emit closed path + if (point === base && (path.length || branches.length > 1)) { + if (debug) console.log('term at start'); + break; + } + + // we can't pass thru a used point + if (point.del) { + if (debug) console.log('term at used point'); + break; + } + + // prevent passing through the same point twice in a search point.del = true; + // add point to path path.push(point); + // track marked points so they can be cleared + marked.push(point); + + // find unused "next" points let links = point.group.filter(p => !p.del); - // no need to recurse at the start - if (links.length === 2 && depth === 1) { - point = links[0]; - // if (debug) console.log({start_mid: point, depth}); - continue; - } + if (debug) console.log({ point, depth, path, links: links.length }); - // if fork in the road, follow all paths to their end - // and find the longest path - let root = !current, nc; - if (links.length > 1) { - // if (debug) console.log('fork!', {links: links.length, depth, root}); - if (root) { - current = [ path ]; - branches = [ ]; - } + if (links.length === 0) { + // for dangling paths, terminate at any junction + if (debug) console.log('term at stub or no branch') + break; + } else if (links.length === 1) { + // one choice is easy, keep going + point = links[0]; + } else if (links.length > 1) { + // if fork in the road, follow all paths to their end + // then find the path representing the shortest perimeter + if (debug) console.log('fork!', { base, links: links.length, depth }); if (branches.length < 500) for (let p of links) { - branches.push(nc = current.slice()); - let rpath = findNextPath(p, nc, branches, depth + 1); - // allow point re-use in other path searches - for (let p of rpath) p.del = false; + findNextPath(p, { + base, + branches, + depth: depth + 1, + marked, + path: path.slice() + }); } - // flatten and sort in ascending perimeter - let flat = branches.map(b => b.flat()).sort((a,b) => { - return perimeter(b) - perimeter(a); - }); - let npath = flat[0]; - if (debug) console.log({ - root, - branches: branches.slice(), - flat, path, npath - }); - if (root) { - for (let p of npath) p.del = true; - return npath; - } else { - return path; - } - // return root ? npath : path; - } else { - // choose next (unused) point - point = links[0]; - } - - // hit an open end or branch - if (!point || point.del) { - return path; + break; } } - throw "invalid state"; + for (let point of marked) { + point.del = false; + } + + // we only return data at the start of recursion + if (depth === 1) { + // flatten and sort branch paths in ascending perimeter + branches.sort((a,b) => { + return area(b) - area(a); + }); + let npath = branches[0]; + if (debug) console.log({ + branches, + npath, + path, + base + }); + for (let p of npath) { + p.del = true; + } + return npath; + } else { + if (debug) console.log('end branch', { depth }); + } } // emit a polygon if it can be cleaned and still have 2 or more points function emit(poly) { let gap = poly.first().distTo2D(poly.last()); - if (poly.open && opt.strict && gap > bridgeMax) { + if (poly.open && strict && gap > bridgeMax) { if (debug) console.log('drop', gap, poly); return; } emitted += poly.length; - if (!opt.dirty) poly = poly.clean(); - if (poly.length > 2 || true) output.push(poly); - if (debug) console.log('xray',poly); + if (!dirty) poly = poly.clean(); + if (poly.length > 2) output.push(poly); + if (debug) console.log('xray', poly); } // given an array of paths, emit longest to shortest // eliminating points from the paths as they are emitted // shorter paths any point eliminated are eliminated as candidates. function emitPath(path) { + // test if last point is in the first point's connected list let closed = path[0].group.indexOf(path.peek()) >= 0; if (closed && path.length > 2) { - if (debug) console.log({ closed: path.length, path }); + if (debug) console.log('emit', { closed: path.length, path }); emit(newPolygon().addPoints(path)); - } else if (path.length > 1) { + } else if (path.length > 2) { let gap = path[0].distTo2D(path.peek()).round(4); - if (debug) console.log({ open: path.length, gap, path }); - connect.push(path); + if (gap < bridgeMax) { + connect.push(path); + } + if (debug) console.log('emit', { open: path.length, gap, path }); + } else { + if (debug) console.log('emit', { drop: path }); } + if (debug) debug_points(); } // create point map, unique point list and point group arrays - input.forEach(function(line) { + for (let line of input) { p1 = cachedPoint(line.p1); p2 = cachedPoint(line.p2); addConnected(p1,p2); addConnected(p2,p1); - }); + } + + // exclude dangling points (from lines with the same endpoints) + points = points.filter(point => point.group); // console.log({points, forks: points.filter(p => p.group.length !== 2)}); // for each unused point, find the longest non-intersecting path for (let point of points) { gl = point.group.length; - forks = forks || gl > 2; - frays = frays || gl < 2; + if (gl > 2) forks.push(point); + if (gl < 2) stubs.push(point); + } + + // show found forks and stubs in debug/xray mode + if (debug) { + console.log({ forks, stubs }); + debug_points(); + } + + // for each dangling endpoint, try to find the closest dangling point + // in the soup which is less than the max bridging distance. + // if a candidate is found, synthesize a line between the endpoint + // and the candidate. + if (stubs.length) { + for (let i=0; i 2) forks.push(pi); + if (pj.group.length > 2) forks.push(pj); + stubs[i] = undefined; + stubs[min.j] = undefined; + } + } + stubs = stubs.filter(p => p); + if (debug) console.log({ filter_stubs: stubs }); } - if (debug && (forks || frays)) console.log({forks, frays}); // process paths starting with forks - if (forks) { + // find the path back to the starting point + // representing the greatest enclosed area if (debug) console.log('process forks'); - for (let point of points) { + for (let point of forks) { // must not have been used and be a dangling end if (!point.del && point.group.length > 2) { let path = findNextPath(point); if (path) emitPath(path); } - } } + } - // process paths with dangling endpoints - if (frays) { - if (debug) console.log('process frays'); - for (let point of points) { - // must not have been used and be a dangling end - if (!point.del && point.group.length === 1) { - let path = findNextPath(point); - if (path) emitPath(path); - } - } } - - // process normal paths - if (debug) console.log('process mids'); + // process remaining paths + // find the path back to the starting point + // representing the greatest enclosed area + if (debug) console.log('process norms'); for (let point of points) { // must not have been used and be a dangling end if (!point.del) { @@ -654,7 +743,7 @@ export function sliceConnect(input, z, opt = {}) { // progressively connect open polygons within a bridge distance let iter = 1000; let mingap; - if (true) do { + if (false) do { mingap = Infinity; outer: for (let i=0; i raw ? l : base + l * size - size / 2); proc.xrayi = layers.slice(); - api.function.slice(); + api.function.slice( + // () => SPACE.platform.showGrid(false) + ); } }, device, diff --git a/src/kiri/mode/fdm/slice.js b/src/kiri/mode/fdm/slice.js index 285d18ea..59b7d1da 100644 --- a/src/kiri/mode/fdm/slice.js +++ b/src/kiri/mode/fdm/slice.js @@ -125,7 +125,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) { isSynth = widget.track.synth, isSupport = widget.track.support, useAssembly = assembly, - isConcurrent = threaded && minions.concurrent, + isConcurrent = threaded && minions.concurrent && !process.xray, topLayers = process.sliceTopLayers || 0, bottomLayers = process.sliceBottomLayers || 0, vaseMode = process.sliceFillType === 'vase' && !isSynth, @@ -353,7 +353,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) { slice.index = process.xray.shift(); slice.lines = lines; slice.groups = groups; - slice.xray = process.xray; + slice.xray = slice.index; } return slice; }).filter(s => s); From ce79edbf930582a4baba1d88d89cb6c071671403 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Tue, 30 Dec 2025 22:29:30 -0500 Subject: [PATCH 069/154] fix widget edges updated on mesh move --- src/kiri/core/widget.js | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/kiri/core/widget.js b/src/kiri/core/widget.js index 78591324..33adf667 100644 --- a/src/kiri/core/widget.js +++ b/src/kiri/core/widget.js @@ -70,7 +70,8 @@ class Widget { }, top: 0, // z top mirror: false, - indexed: false + indexed: false, + indexRad: 0 }, // used to cache shadow geo this.cache = {}; @@ -446,14 +447,20 @@ class Widget { tz = -tzoff, { x, y, z } = track.pos; if (track.indexed) { - let rad = track.indexRad; - this.mesh.rotation.x = -rad; + this.mesh.rotation.x = -track.indexRad; z = top; } else { this.mesh.rotation.x = 0; z += tz; } mesh.position.set(x, y, z); + this._updateEdges(); + } + + _updateEdges() { + if (this.outline && this.setEdges) { + this.setEdges(true); + } } scale(x, y, z) { @@ -473,9 +480,7 @@ class Widget { if (this.setWireframe) this.setWireframe(false); this.clearSlices(); mesh.geometry.applyMatrix4(new THREE.Matrix4().makeScale(x, y, z)); - if (this.outline && this.setEdges) { - this.setEdges(true); - } + this._updateEdges(); scale.x *= (x || 1.0); scale.y *= (y || 1.0); scale.z *= (z || 1.0); @@ -489,9 +494,7 @@ class Widget { if (center) { this.center(false); } - if (this.outline && this.setEdges) { - this.setEdges(true); - } + this._updateEdges(); if ((x || y || z) && this.api && this.api.event) { this.api.event.emit('widget.rotate', {widget: this, x, y, z}); } From aa96c26c736fdda09a333512a5d0dd3e56a1f94b Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Wed, 31 Dec 2025 14:27:29 -0500 Subject: [PATCH 070/154] move drag knife login into separate driver --- src/kiri/mode/drag/driver.js | 245 +++++++++++++++++++++++++++++++++- src/kiri/mode/laser/driver.js | 94 +++---------- src/kiri/run/worker.js | 7 +- 3 files changed, 260 insertions(+), 86 deletions(-) diff --git a/src/kiri/mode/drag/driver.js b/src/kiri/mode/drag/driver.js index a6b5c853..2e586d16 100644 --- a/src/kiri/mode/drag/driver.js +++ b/src/kiri/mode/drag/driver.js @@ -1,13 +1,252 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { LASER, TYPE } from '../laser/driver.js'; +import { LASER, TYPE, FUNC } from '../laser/driver.js'; +import { newPoint } from '../../../geo/point.js'; +import { newPolygon } from '../../../geo/polygon.js'; +import { newSlopeFromAngle } from '../../../geo/slope.js'; function init(worker) { - // console.log({ DRAGKNIFE_INIT: worker }); + FUNC.generateDragKnifePath = generateDragKnifePath; +} + +/** + * Generate drag knife toolpath for a polygon. + * The knife blade trails behind the rotation axis by knifeOffset distance. + * This function creates compensated paths with pivot arcs at corners. + * + * @param {Polygon} polygon - Input polygon to cut + * @param {number} knifeOffset - Distance from Z-axis center to blade tip + * @param {Object} options - Configuration options + * @param {number} [options.angleThreshold=10] - Angle (degrees) below which vertices are treated as curves + * @param {number} [options.lookahead=3] - Number of vertices for direction smoothing on curves + * @param {boolean} [options.overcut=false] - Add slight overcut at sharp corners + * @param {number} [options.overcutDistance=0.1] - Distance to overcut in mm + * @returns {Polygon} New polygon with drag knife compensation applied + */ +function generateDragKnifePath(polygon, knifeOffset, options = {}) { + return addKnifeRadii(polygon, knifeOffset); + const { + angleThreshold = 10, // degrees + lookahead = 3, // vertices + overcut = false, + overcutDistance = 0.1 + } = options; + + polygon.setClockwise(); + const points = polygon.points; + const len = points.length; + + if (len < 2) { + return polygon.clone(); + } + + const DEG2RAD = Math.PI / 180; + const thresholdRad = angleThreshold * DEG2RAD; + + // Helper: Get direction vector from point i to i+1 + function getDirection(i) { + const curr = points[i]; + const next = points[(i + 1) % len]; + const dx = next.x - curr.x; + const dy = next.y - curr.y; + const length = Math.sqrt(dx * dx + dy * dy); + return length > 0 ? { x: dx / length, y: dy / length } : { x: 1, y: 0 }; + } + + // Helper: Get perpendicular vector (90° counter-clockwise, to the left) + function perpLeft(dir) { + return { x: -dir.y, y: dir.x }; + } + + // Helper: Calculate angle change at vertex i + function getAngleChange(i) { + const prev = points[(i - 1 + len) % len]; + const curr = points[i]; + const next = points[(i + 1) % len]; + + const v1x = curr.x - prev.x; + const v1y = curr.y - prev.y; + const v2x = next.x - curr.x; + const v2y = next.y - curr.y; + + const len1 = Math.sqrt(v1x * v1x + v1y * v1y); + const len2 = Math.sqrt(v2x * v2x + v2y * v2y); + + if (len1 === 0 || len2 === 0) return 0; + + const dot = (v1x * v2x + v1y * v2y) / (len1 * len2); + const cross = (v1x * v2y - v1y * v2x) / (len1 * len2); + + return Math.atan2(cross, dot); + } + + // Helper: Get smoothed direction using lookahead window + function getSmoothedDirection(i) { + let sumX = 0, sumY = 0, count = 0; + + for (let j = -lookahead; j <= lookahead; j++) { + const idx = (i + j + len) % len; + const dir = getDirection(idx); + sumX += dir.x; + sumY += dir.y; + count++; + } + + const length = Math.sqrt(sumX * sumX + sumY * sumY); + return length > 0 ? + { x: sumX / length, y: sumY / length } : + { x: 1, y: 0 }; + } + + // Classify each vertex as CURVE or CORNER + const types = []; + for (let i = 0; i < len; i++) { + const angleChange = Math.abs(getAngleChange(i)); + types[i] = angleChange < thresholdRad ? 'CURVE' : 'CORNER'; + } + + // Generate compensated toolpath + const outputPoints = []; + + for (let i = 0; i < len; i++) { + const curr = points[i]; + const type = types[i]; + + if (type === 'CURVE') { + // Smooth curve section - use averaged direction for offset + const smoothDir = getSmoothedDirection(i); + const offset = perpLeft(smoothDir); + const toolX = curr.x + offset.x * knifeOffset; + const toolY = curr.y + offset.y * knifeOffset; + outputPoints.push(newPoint(toolX, toolY, curr.z)); + + } else { + // Sharp corner - need pivot arc + const angleChange = getAngleChange(i); + const dirIn = getDirection((i - 1 + len) % len); + const dirOut = getDirection(i); + + const offsetIn = perpLeft(dirIn); + const offsetOut = perpLeft(dirOut); + + // Tool positions entering and exiting corner + const toolInX = curr.x + offsetIn.x * knifeOffset; + const toolInY = curr.y + offsetIn.y * knifeOffset; + const toolOutX = curr.x + offsetOut.x * knifeOffset; + const toolOutY = curr.y + offsetOut.y * knifeOffset; + + // Add entry point + outputPoints.push(newPoint(toolInX, toolInY, curr.z)); + + // For right turns (positive angle), add arc points + if (angleChange > 0) { + const arcSteps = Math.max(3, Math.ceil(Math.abs(angleChange) / (15 * DEG2RAD))); + + // Optional overcut + if (overcut) { + const ocX = curr.x + offsetIn.x * (knifeOffset + overcutDistance); + const ocY = curr.y + offsetIn.y * (knifeOffset + overcutDistance); + outputPoints.push(newPoint(ocX, ocY, curr.z)); + } + + // Generate arc from offsetIn to offsetOut around curr point + for (let step = 1; step <= arcSteps; step++) { + const t = step / arcSteps; + const angle = Math.atan2(offsetIn.y, offsetIn.x) + angleChange * t; + const arcX = curr.x + Math.cos(angle) * knifeOffset; + const arcY = curr.y + Math.sin(angle) * knifeOffset; + outputPoints.push(newPoint(arcX, arcY, curr.z)); + } + } else { + // Left turn - blade is on inside, simpler handling + // Just move directly to exit position + outputPoints.push(newPoint(toolOutX, toolOutY, curr.z)); + } + } + } + + // Create new polygon with compensated points + const result = newPolygon(outputPoints); + result.open = polygon.open; + + return result; +} + +// start to the "left" of the first point +function addKnifeRadii(poly, tipoff) { + poly.setClockwise(); + let oldpts = poly.points.slice(); + + // find leftpoint and make that the first point + let start = oldpts[0]; + let startI = 0; + let inner = tipoff < 0; + tipoff = Math.abs(tipoff); + for (let i=1; i start.x || (pt.x == start.x && pt.y < start.y))) { + start = pt; + startI = i; + } else if (!inner && (pt.x < start.x || (pt.x == start.x && pt.y > start.y))) { + start = pt; + startI = i; + } + } + if (startI > 0) { + oldpts = oldpts.slice(startI,oldpts.length).appendAll(oldpts.slice(0,startI)); + } + + let lastpt = oldpts[0].clone().move({x:-tipoff,y:0,z:0}); + let lastsl = lastpt.slopeTo(oldpts[0]).toUnit(); + let newpts = [ lastpt, lastpt = oldpts[0].clone() ]; + let tmp; + for (let i=1; i= 10) { + if (tipoff && lastpt.distTo2D(nextpt) >= tipoff) { + arc(lastpt, tipoff, lastsl, nextsl, newpts); + } else { + // todo handle short segments + // newpts.push(lastpt.projectOnSlope(lastsl, tipoff) ); + // newpts.push( lastpt.projectOnSlope(nextsl, tipoff) ); + } + } + newpts.push(nextpt); + lastsl = nextsl; + lastpt = nextpt; + } + newpts.push( tmp = lastpt.projectOnSlope(lastsl, tipoff) ); + // newpts.push( tmp.clone().move({x:tipoff, y:0, z: 0}) ); + poly.open = true; + poly.points = newpts; + + return poly; +} + +// convert arc into line segments +function arc(center, rad, s1, s2, out) { + let a1 = s1.angle; + let step = 5; + let diff = s1.angleDiff(s2, true); + let ticks = Math.abs(Math.floor(diff / step)); + let dir = Math.sign(diff); + let off = (diff % step) / 2; + if (off == 0) { + ticks++; + } else { + out.push( center.projectOnSlope(s1, rad) ); + } + while (ticks-- > 0) { + out.push( center.projectOnSlope(newSlopeFromAngle(a1 + off), rad) ); + a1 += step * dir; + } + out.push( center.projectOnSlope(s2, rad) ); } export const DRAG = Object.assign({}, LASER, { type: TYPE.DRAG, name: 'DragKnife', - init + init, + generateDragKnifePath }); diff --git a/src/kiri/mode/laser/driver.js b/src/kiri/mode/laser/driver.js index daedf895..edcd186a 100644 --- a/src/kiri/mode/laser/driver.js +++ b/src/kiri/mode/laser/driver.js @@ -5,7 +5,6 @@ import { Packer } from '../../app/pack.js'; import { newBounds } from '../../../geo/bounds.js'; import { newPoint } from '../../../geo/point.js'; import { newPrint } from '../../core/print.js'; -import { newSlopeFromAngle } from '../../../geo/slope.js'; import { polygons as POLY } from '../../../geo/polygons.js'; import { render } from '../../core/render.js'; import { slicer } from '../../../geo/slicer.js'; @@ -20,6 +19,8 @@ export const TYPE = { WEDM: 3 }; +export const FUNC = {}; + function init(worker) { // console.log({ LASER_INIT: worker }); } @@ -98,76 +99,6 @@ function sliceEmitObjects(print, slice, groups, opt = { }) { return emit; }; -// convert arc into line segments -function arc(center, rad, s1, s2, out) { - let a1 = s1.angle; - let step = 5; - let diff = s1.angleDiff(s2, true); - let ticks = Math.abs(Math.floor(diff / step)); - let dir = Math.sign(diff); - let off = (diff % step) / 2; - if (off == 0) { - ticks++; - } else { - out.push( center.projectOnSlope(s1, rad) ); - } - while (ticks-- > 0) { - out.push( center.projectOnSlope(newSlopeFromAngle(a1 + off), rad) ); - a1 += step * dir; - } - out.push( center.projectOnSlope(s2, rad) ); -} - -// start to the "left" of the first point -function addKnifeRadii(poly, tipoff) { - poly.setClockwise(); - let oldpts = poly.points.slice(); - - // find leftpoint and make that the first point - let start = oldpts[0]; - let startI = 0; - let inner = tipoff < 0; - tipoff = Math.abs(tipoff); - for (let i=1; i start.x || (pt.x == start.x && pt.y < start.y))) { - start = pt; - startI = i; - } else if (!inner && (pt.x < start.x || (pt.x == start.x && pt.y > start.y))) { - start = pt; - startI = i; - } - } - if (startI > 0) { - oldpts = oldpts.slice(startI,oldpts.length).appendAll(oldpts.slice(0,startI)); - } - - let lastpt = oldpts[0].clone().move({x:-tipoff,y:0,z:0}); - let lastsl = lastpt.slopeTo(oldpts[0]).toUnit(); - let newpts = [ lastpt, lastpt = oldpts[0].clone() ]; - let tmp; - for (let i=1; i= 10) { - if (lastpt.distTo2D(nextpt) >= tipoff) { - arc(lastpt, tipoff, lastsl, nextsl, newpts); - } else { - // todo handle short segments - // newpts.push(lastpt.projectOnSlope(lastsl, tipoff) ); - // newpts.push( lastpt.projectOnSlope(nextsl, tipoff) ); - } - } - newpts.push(nextpt); - lastsl = nextsl; - lastpt = nextpt; - } - newpts.push( tmp = lastpt.projectOnSlope(lastsl, tipoff) ); - // newpts.push( tmp.clone().move({x:tipoff, y:0, z: 0}) ); - poly.open = true; - poly.points = newpts; -} - /** * DRIVER SLICE CONTRACT * @@ -288,7 +219,8 @@ async function laser_prepare(widgets, settings, update) { { ctOutLayer, ctOutTileSpacing, ctSliceSingle } = process, { ctOriginCenter, ctOriginBounds } = process, { ctOriginOffX, ctOriginOffY } = process, - { ctOutStack, ctOutMerged, ctOutKnifeTip, ctOutShaper } = process; + { ctOutStack, ctOutMerged, ctOutKnifeTip, ctOutShaper } = process, + { generateDragKnifePath } = FUNC; ctOutStack = ctOutStack && isLaser; print.widgets = widgets; @@ -366,7 +298,8 @@ async function laser_prepare(widgets, settings, update) { let gather = []; for (let poly of merged) { if (isKnife) { - addKnifeRadii(poly, ctOutKnifeTip); + poly = generateDragKnifePath(poly, ctOutKnifeTip); + // addKnifeRadii(poly, ctOutKnifeTip); } print.PPP(poly, gather, { extrude: poly.depth, @@ -378,12 +311,15 @@ async function laser_prepare(widgets, settings, update) { // output a layer for each slice if (isKnife) { for (let slice of widget.slices) { - for (let poly of slice.offset) { - addKnifeRadii(poly, ctOutKnifeTip); - if (poly.inner) poly.inner.forEach(ip => { - addKnifeRadii(ip, -ctOutKnifeTip); - }); - } + slice.offset = POLY + .flatten(slice.offset) + .map(poly => generateDragKnifePath(poly, ctOutKnifeTip)); + // for (let poly of slice.offset) { + // addKnifeRadii(poly, ctOutKnifeTip); + // if (poly.inner) poly.inner.forEach(ip => { + // addKnifeRadii(ip, -ctOutKnifeTip); + // }); + // } } } let lastEmit; diff --git a/src/kiri/run/worker.js b/src/kiri/run/worker.js index 82f422fc..84d4f339 100644 --- a/src/kiri/run/worker.js +++ b/src/kiri/run/worker.js @@ -775,7 +775,6 @@ const worker = self.kiri_worker = { }; // initilize driver mode handlers -CAM.init(worker); -FDM.init(worker); -LASER.init(worker); -SLA.init(worker); +for (let driver of Object.values(drivers)) { + driver.init(worker); +} From debe4fb192491f904b347babce520414e27bc0cf Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Wed, 31 Dec 2025 22:59:09 -0500 Subject: [PATCH 071/154] stub in coastline moves for contouring --- src/kiri/mode/cam/op-contour.js | 6 ++-- src/kiri/mode/cam/prepare.js | 54 +++++++++++++++++++++++++++++++-- src/kiri/mode/cam/topo3.js | 11 +++++++ 3 files changed, 66 insertions(+), 5 deletions(-) diff --git a/src/kiri/mode/cam/op-contour.js b/src/kiri/mode/cam/op-contour.js index b9116c86..5939ef80 100644 --- a/src/kiri/mode/cam/op-contour.js +++ b/src/kiri/mode/cam/op-contour.js @@ -93,7 +93,7 @@ class OpContour extends CamOp { // we need topo for safe travel moves when roughing and outlining // not generated when drilling-only. then all z moves use bounds max. // also generates x and y contouring when selected - let topo = await Topo({ + let topo = this.topo = await Topo({ // onupdate: (update, msg) => { onupdate: (index, total, msg) => { progress(index / total, msg); @@ -117,7 +117,7 @@ class OpContour extends CamOp { } prepare(ops, progress) { - let { op, state, sliceOut, toolStep } = this; + let { op, sliceOut, state, toolStep, topo } = this; let { settings } = state; let { process } = settings; @@ -128,7 +128,7 @@ class OpContour extends CamOp { let depthData = []; setTool(op.tool, op.rate, process.camFastFeedZ); - setContouring(true, toolStep * 1.5); + setContouring(true, toolStep * 1.5, topo.coastline); setTolerance(this.tolerance); let printPoint = newPoint(bounds.min.x, bounds.min.y, zmax); diff --git a/src/kiri/mode/cam/prepare.js b/src/kiri/mode/cam/prepare.js index e22f009f..8a06ec9a 100644 --- a/src/kiri/mode/cam/prepare.js +++ b/src/kiri/mode/cam/prepare.js @@ -114,6 +114,7 @@ export async function prepare_one(widget, settings, print, firstPoint, update) { originx = (camOriginCenter ? 0 : -stock.x / 2) + (camOriginOffX || 0), originy = (camOriginCenter ? 0 : -stock.y / 2) + (camOriginOffY || 0), origin = newPoint(originx, originy, zSafe), + coastline, contouring = false, currentOp, drillDown = 0, @@ -184,10 +185,17 @@ export async function prepare_one(widget, settings, print, firstPoint, update) { } } - function setContouring(bool, step) { + function setContouring(bool, step, coast) { + coastline = coast; contouring = bool; toolDiamMove = step ?? tool.getStepSize(currentOp.step) * 2; if (bool) setTravelBoundary(); + if (coast) { + for (let poly of coast) { + // poly.points = poly.points.map(pt => toWorkCoords(pt)); + } + console.log({ coast_to_work: coast }); + } } function setSpindle(speed) { @@ -377,6 +385,45 @@ export async function prepare_one(widget, settings, print, firstPoint, update) { .annotate({ slice: p.slice }); } + function coastlineMove(point) { + let from = toWidgetCoords(printPoint); + let to = toWidgetCoords(point); + if (!coastline || from.distTo2D(to) < 0.01) { + return false; + } + let start = { dist: 1, poly: 0, pt: from }; + let end = { dist: 1, poly: 1, pt: to }; + for (let poly of coastline) { + let { points } = poly; + for (let i=0; i toolDiamMove) { + if (coastline && deltaXY < toolDiamMove * 5 && coastlineMove(point)) { + // console.log('coastline move'); + } else if (deltaXY > toolDiamMove) { upAndOver = true; } else if (absDeltaZ < 0.01) { if (debug) console.log('contour move as cut'); diff --git a/src/kiri/mode/cam/topo3.js b/src/kiri/mode/cam/topo3.js index c53aa0eb..021e9385 100644 --- a/src/kiri/mode/cam/topo3.js +++ b/src/kiri/mode/cam/topo3.js @@ -166,6 +166,17 @@ export class Topo { zFloor: zBottom - 1, onProgress(pct) { console.log({ pct }); onupdate(pct/100, 100) } }); + gpu.mode = 'tracing'; + // create coastline path around part for tip-to-tip travels + // convert shadow/clip poly lines to raster float32 array groups + let paths = POLY.flatten(clipTo).map(poly => poly.points.map(p => [ p.x, p.y ]).flat().toFloat32()); + let coastline = await gpu.generateToolpaths({ + paths, + step: toolStep / 2, + zFloor: zBottom - 1 + }); + this.coastline = coastline.paths.map(arr => newPolygon().fromArray([0,...arr])); + console.log({ coastline: this.coastline, clipTo }); gpu.terminate(); let { numScanlines, pointsPerLine, pathData } = output; From a24c1b6653fe45ea1d011bfc325bd7e6c1e877ec Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 1 Jan 2026 00:22:37 -0500 Subject: [PATCH 072/154] add coastline move routes --- src/kiri/mode/cam/op-contour.js | 9 +++++++ src/kiri/mode/cam/prepare.js | 45 ++++++++++++++++++++++++--------- src/kiri/mode/cam/topo3.js | 5 ++-- 3 files changed, 44 insertions(+), 15 deletions(-) diff --git a/src/kiri/mode/cam/op-contour.js b/src/kiri/mode/cam/op-contour.js index 5939ef80..b396782d 100644 --- a/src/kiri/mode/cam/op-contour.js +++ b/src/kiri/mode/cam/op-contour.js @@ -5,6 +5,7 @@ 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 { newSlice } from '../../core/slice.js'; import { tip2tipEmit } from '../../../geo/paths.js'; function createFilter(op, origin, axis) { @@ -112,6 +113,14 @@ class OpContour extends CamOp { state }); + if (this.debug && topo.coastline) { + console.log('coastline', topo.coastline); + const dbs = newSlice(-1); + const dbo = dbs.output(); + dbo.setLayer("coastline", { line: 0x0000dd }).addPolys(topo.coastline); + addSlices([ dbs ]) + } + // computed if set to 0 this.tolerance = topo.tolerance; } diff --git a/src/kiri/mode/cam/prepare.js b/src/kiri/mode/cam/prepare.js index 8a06ec9a..43a0794e 100644 --- a/src/kiri/mode/cam/prepare.js +++ b/src/kiri/mode/cam/prepare.js @@ -190,12 +190,6 @@ export async function prepare_one(widget, settings, print, firstPoint, update) { contouring = bool; toolDiamMove = step ?? tool.getStepSize(currentOp.step) * 2; if (bool) setTravelBoundary(); - if (coast) { - for (let poly of coast) { - // poly.points = poly.points.map(pt => toWorkCoords(pt)); - } - console.log({ coast_to_work: coast }); - } } function setSpindle(speed) { @@ -385,6 +379,11 @@ export async function prepare_one(widget, settings, print, firstPoint, update) { .annotate({ slice: p.slice }); } + /** + * when moving between contour endpoints, check if we can + * instead route around the bounding area of the contour + * whih we call the coastline. + */ function coastlineMove(point) { let from = toWidgetCoords(printPoint); let to = toWidgetCoords(point); @@ -397,9 +396,6 @@ export async function prepare_one(widget, settings, print, firstPoint, update) { let { points } = poly; for (let i=0; i sp ? + sp + (pl - ep): + ep + (pl - sp); + let dir = 1; + let dist; + if (adist < bdist) { + dist = adist; + if (ep < sp) { + dir = -1; + } + } else { + dist = bdist; + if (ep < sp) { + ep += pl; + } else { + sp += pl; + dir = -1; + } + } + for (let i=sp, d=0; d < dist; i += dir, d++) { + layerPush(toWorkCoords(points[i % pl]), 1, 0, tool); + } return true; } @@ -509,7 +530,7 @@ export async function prepare_one(widget, settings, print, firstPoint, update) { // contouring logic if (isMove && contouring) { - if (coastline && deltaXY < toolDiamMove * 5 && coastlineMove(point)) { + if (coastline && deltaXY < 5 && coastlineMove(point)) { // console.log('coastline move'); } else if (deltaXY > toolDiamMove) { upAndOver = true; diff --git a/src/kiri/mode/cam/topo3.js b/src/kiri/mode/cam/topo3.js index 021e9385..196ddf88 100644 --- a/src/kiri/mode/cam/topo3.js +++ b/src/kiri/mode/cam/topo3.js @@ -172,11 +172,10 @@ export class Topo { let paths = POLY.flatten(clipTo).map(poly => poly.points.map(p => [ p.x, p.y ]).flat().toFloat32()); let coastline = await gpu.generateToolpaths({ paths, - step: toolStep / 2, - zFloor: zBottom - 1 + step: 1, + zFloor: zBottom }); this.coastline = coastline.paths.map(arr => newPolygon().fromArray([0,...arr])); - console.log({ coastline: this.coastline, clipTo }); gpu.terminate(); let { numScanlines, pointsPerLine, pathData } = output; From 2d3042ce601cd34bc15d532c207c856639d47ffa Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 1 Jan 2026 10:14:10 -0500 Subject: [PATCH 073/154] attempt 2 reworking drag knife --- src/kiri/mode/cam/op-contour.js | 1 + src/kiri/mode/drag/driver.js | 178 +++++++++++++------------------- 2 files changed, 71 insertions(+), 108 deletions(-) diff --git a/src/kiri/mode/cam/op-contour.js b/src/kiri/mode/cam/op-contour.js index b396782d..c3d95682 100644 --- a/src/kiri/mode/cam/op-contour.js +++ b/src/kiri/mode/cam/op-contour.js @@ -84,6 +84,7 @@ class OpContour extends CamOp { async slice(progress) { let { op, state } = this; let { color, addSlices, settings, updateToolDiams } = state; + let conTool = new Tool(settings, op.tool); let filter = createFilter(op, settings.origin, op.axis.toLowerCase()); let toolDiam = this.toolDiam = conTool.fluteDiameter(); diff --git a/src/kiri/mode/drag/driver.js b/src/kiri/mode/drag/driver.js index 2e586d16..109fb769 100644 --- a/src/kiri/mode/drag/driver.js +++ b/src/kiri/mode/drag/driver.js @@ -24,14 +24,16 @@ function init(worker) { * @returns {Polygon} New polygon with drag knife compensation applied */ function generateDragKnifePath(polygon, knifeOffset, options = {}) { - return addKnifeRadii(polygon, knifeOffset); const { - angleThreshold = 10, // degrees - lookahead = 3, // vertices - overcut = false, - overcutDistance = 0.1 + angleThreshold = 5, // degrees - minimum angle change to insert arc + arcSegmentAngle = 5 // degrees - angle between arc segments } = options; + // Validate inputs + if (!knifeOffset || Math.abs(knifeOffset) < 0.001) { + return polygon.clone(); + } + polygon.setClockwise(); const points = polygon.points; const len = points.length; @@ -42,131 +44,91 @@ function generateDragKnifePath(polygon, knifeOffset, options = {}) { const DEG2RAD = Math.PI / 180; const thresholdRad = angleThreshold * DEG2RAD; + const arcSegRad = arcSegmentAngle * DEG2RAD; - // Helper: Get direction vector from point i to i+1 - function getDirection(i) { - const curr = points[i]; - const next = points[(i + 1) % len]; - const dx = next.x - curr.x; - const dy = next.y - curr.y; + // For closed polygons vs open paths + const isClosed = !polygon.open; + const numSegments = isClosed ? len : len - 1; + + // Calculate segment directions and angles + const segments = []; + for (let i = 0; i < numSegments; i++) { + const p1 = points[i]; + const p2 = points[(i + 1) % len]; + const dx = p2.x - p1.x; + const dy = p2.y - p1.y; const length = Math.sqrt(dx * dx + dy * dy); - return length > 0 ? { x: dx / length, y: dy / length } : { x: 1, y: 0 }; - } - // Helper: Get perpendicular vector (90° counter-clockwise, to the left) - function perpLeft(dir) { - return { x: -dir.y, y: dir.x }; - } - - // Helper: Calculate angle change at vertex i - function getAngleChange(i) { - const prev = points[(i - 1 + len) % len]; - const curr = points[i]; - const next = points[(i + 1) % len]; - - const v1x = curr.x - prev.x; - const v1y = curr.y - prev.y; - const v2x = next.x - curr.x; - const v2y = next.y - curr.y; - - const len1 = Math.sqrt(v1x * v1x + v1y * v1y); - const len2 = Math.sqrt(v2x * v2x + v2y * v2y); - - if (len1 === 0 || len2 === 0) return 0; - - const dot = (v1x * v2x + v1y * v2y) / (len1 * len2); - const cross = (v1x * v2y - v1y * v2x) / (len1 * len2); - - return Math.atan2(cross, dot); - } - - // Helper: Get smoothed direction using lookahead window - function getSmoothedDirection(i) { - let sumX = 0, sumY = 0, count = 0; - - for (let j = -lookahead; j <= lookahead; j++) { - const idx = (i + j + len) % len; - const dir = getDirection(idx); - sumX += dir.x; - sumY += dir.y; - count++; + if (length > 0.001) { + segments.push({ + index: i, + point: p1, + angle: Math.atan2(dy, dx), // Direction of travel + length: length + }); } - - const length = Math.sqrt(sumX * sumX + sumY * sumY); - return length > 0 ? - { x: sumX / length, y: sumY / length } : - { x: 1, y: 0 }; } - // Classify each vertex as CURVE or CORNER - const types = []; - for (let i = 0; i < len; i++) { - const angleChange = Math.abs(getAngleChange(i)); - types[i] = angleChange < thresholdRad ? 'CURVE' : 'CORNER'; + if (segments.length === 0) { + return polygon.clone(); } - // Generate compensated toolpath - const outputPoints = []; + const output = []; - for (let i = 0; i < len; i++) { - const curr = points[i]; - const type = types[i]; + // Process each segment + for (let i = 0; i < segments.length; i++) { + const seg = segments[i]; + const isLastSegment = i === segments.length - 1; + const nextSeg = isClosed ? segments[(i + 1) % segments.length] : + isLastSegment ? null : segments[i + 1]; - if (type === 'CURVE') { - // Smooth curve section - use averaged direction for offset - const smoothDir = getSmoothedDirection(i); - const offset = perpLeft(smoothDir); - const toolX = curr.x + offset.x * knifeOffset; - const toolY = curr.y + offset.y * knifeOffset; - outputPoints.push(newPoint(toolX, toolY, curr.z)); + // Key insight from dragknife-repath: + // Offset the point in the DIRECTION OF TRAVEL by knife offset + // The knife blade trails behind the rotation axis + const offsetX = seg.point.x + Math.cos(seg.angle) * knifeOffset; + const offsetY = seg.point.y + Math.sin(seg.angle) * knifeOffset; + output.push(newPoint(offsetX, offsetY, seg.point.z)); - } else { - // Sharp corner - need pivot arc - const angleChange = getAngleChange(i); - const dirIn = getDirection((i - 1 + len) % len); - const dirOut = getDirection(i); + // Check if we need a swivel arc at the end of this segment + if (nextSeg) { + // Calculate angle change between this segment and the next + let angleDiff = nextSeg.angle - seg.angle; - const offsetIn = perpLeft(dirIn); - const offsetOut = perpLeft(dirOut); + // Normalize angle difference to [-PI, PI] + while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI; + while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI; - // Tool positions entering and exiting corner - const toolInX = curr.x + offsetIn.x * knifeOffset; - const toolInY = curr.y + offsetIn.y * knifeOffset; - const toolOutX = curr.x + offsetOut.x * knifeOffset; - const toolOutY = curr.y + offsetOut.y * knifeOffset; + // If angle change exceeds threshold, insert arc to rotate knife + if (Math.abs(angleDiff) > thresholdRad) { + // The arc is centered at the vertex between segments + const vertex = points[(seg.index + 1) % len]; - // Add entry point - outputPoints.push(newPoint(toolInX, toolInY, curr.z)); + // Number of arc segments based on angle change + const arcSteps = Math.max(2, Math.ceil(Math.abs(angleDiff) / arcSegRad)); - // For right turns (positive angle), add arc points - if (angleChange > 0) { - const arcSteps = Math.max(3, Math.ceil(Math.abs(angleChange) / (15 * DEG2RAD))); - - // Optional overcut - if (overcut) { - const ocX = curr.x + offsetIn.x * (knifeOffset + overcutDistance); - const ocY = curr.y + offsetIn.y * (knifeOffset + overcutDistance); - outputPoints.push(newPoint(ocX, ocY, curr.z)); - } - - // Generate arc from offsetIn to offsetOut around curr point + // Generate arc from current angle to next angle for (let step = 1; step <= arcSteps; step++) { const t = step / arcSteps; - const angle = Math.atan2(offsetIn.y, offsetIn.x) + angleChange * t; - const arcX = curr.x + Math.cos(angle) * knifeOffset; - const arcY = curr.y + Math.sin(angle) * knifeOffset; - outputPoints.push(newPoint(arcX, arcY, curr.z)); + const angle = seg.angle + angleDiff * t; + const arcX = vertex.x + Math.cos(angle) * knifeOffset; + const arcY = vertex.y + Math.sin(angle) * knifeOffset; + output.push(newPoint(arcX, arcY, vertex.z)); } - } else { - // Left turn - blade is on inside, simpler handling - // Just move directly to exit position - outputPoints.push(newPoint(toolOutX, toolOutY, curr.z)); } } } - // Create new polygon with compensated points - const result = newPolygon(outputPoints); + // For open paths, add the final point offset in the last segment's direction + if (!isClosed && segments.length > 0) { + const lastSeg = segments[segments.length - 1]; + const lastPoint = points[len - 1]; + const offsetX = lastPoint.x + Math.cos(lastSeg.angle) * knifeOffset; + const offsetY = lastPoint.y + Math.sin(lastSeg.angle) * knifeOffset; + output.push(newPoint(offsetX, offsetY, lastPoint.z)); + } + + // Create new polygon with compensated path + const result = newPolygon(output); result.open = polygon.open; return result; From 9da2368a7afdab4d5f44915552f092bce344cb1e Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 1 Jan 2026 11:52:20 -0500 Subject: [PATCH 074/154] add optional shadow selection to area op --- src/kiri/app/conf/defaults.js | 1 + src/kiri/mode/cam/cl-ops.js | 21 +++++++++++---- src/kiri/mode/cam/op-area.js | 48 ++++++++++++++++++++------------- src/kiri/mode/cam/op-outline.js | 10 ------- web/kiri/lang/en.js | 8 ++++++ 5 files changed, 54 insertions(+), 34 deletions(-) diff --git a/src/kiri/app/conf/defaults.js b/src/kiri/app/conf/defaults.js index bf30c871..662fbc76 100644 --- a/src/kiri/app/conf/defaults.js +++ b/src/kiri/app/conf/defaults.js @@ -443,6 +443,7 @@ export const conf = { camAreaExpand: 0, camAreaRefine: 0, camAreaSmooth: 1, + camAreaShadow: false, camAreaDogbones: false, camAreaRevbones: false, camAreaOutline: false, diff --git a/src/kiri/mode/cam/cl-ops.js b/src/kiri/mode/cam/cl-ops.js index c1a970b7..7849a713 100644 --- a/src/kiri/mode/cam/cl-ops.js +++ b/src/kiri/mode/cam/cl-ops.js @@ -669,6 +669,10 @@ export function createPopOps() { return env.poppedRec.mode === 'trace'; } + function isShadow() { + return env.poppedRec.shadow; + } + function isSurface() { return env.poppedRec.mode === 'surface'; } @@ -697,6 +701,7 @@ export function createPopOps() { follow: 'camAreaFollow', refine: 'camAreaRefine', outline: 'camAreaOutline', + shadow: 'camAreaShadow', tolerance: 'camTolerance', dogbones: 'camAreaDogbones', revbones: 'camAreaRevbones', @@ -707,33 +712,39 @@ export function createPopOps() { tr_type: UC.newSelect(LANG.cc_offs_s, { title: LANG.cc_offs_l, show: isTrace }, "traceoff"), sr_type: UC.newSelect("pattern", { title: "pattern", show: isSurface }, "surftyp"), sep: UC.newBlank({ class: "pop-sep" }), + exp: UC.newExpand("area selection", { open }), menu: UC.newRow([ UC.newButton("edge", traceAdd), UC.newButton("surface", surfaceAdd), - ], { class: "ext-buttons f-row" }), + ], { class: "ext-buttons f-row", show: () => !isShadow() }), + shadow: UC.newBoolean(LANG.cp_shad_s, undefined, { title: LANG.cp_shad_l }), outline: UC.newBoolean(LANG.cp_outl_s, undefined, { title: LANG.cp_outl_l }), + exp_end: UC.endExpand(), + sep: UC.newBlank({ class: "pop-sep" }), + exp: UC.newExpand("area modifiers", { }), expand: UC.newInput(LANG.cp_xpnd_s, { title: LANG.cp_xpnd_l, convert: toFloat, units }), smooth: UC.newInput(LANG.cp_smoo_s, { title: LANG.cp_smoo_l, convert: toInt, xshow: isSurface }), follow: UC.newInput(LANG.cp_foll_s, { title: LANG.cp_foll_l, convert: toFloat }), tolerance: UC.newInput(LANG.ou_toll_s, { title: LANG.ou_toll_l, convert: toFloat, bound: UC.bound(0, 10.0), units, round: 4, show: isSurface }), + exp_end: UC.endExpand(), exp: UC.newExpand("tool & stepping", { open }), tool: UC.newSelect(LANG.cc_tool, {}, "tools"), direction: UC.newSelect(LANG.ou_dire_s, { title: LANG.ou_dire_l }, "direction"), - sr_angle: UC.newInput("step angle", { title: "step angle", convert: toFloat, bound: UC.bound(0, 360), show: isSurfaceLinear }), + sr_angle: UC.newInput(LANG.ca_sang_s, { title: LANG.ca_sang_l, convert: toFloat, bound: UC.bound(0, 360), show: isSurfaceLinear }), over: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: toFloat, bound: UC.bound(0.001, 100.0), show: () => isClear() || isSurface() }), down: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: toFloat, bound: UC.bound(0, 100.0), units, show: () => isClear() || isTrace() }), refine: UC.newInput(LANG.cp_refi_s, { title: LANG.cp_refi_l, convert: toInt, show: isSurface }), - sr_alter: UC.newBoolean("alternate", undefined, { title: "alternate cutting direction", show: isSurfaceLinear }), + sr_alter: UC.newBoolean(LANG.ca_altr_s, undefined, { title: LANG.ca_altr_l, show: isSurfaceLinear }), dogbones: UC.newBoolean(LANG.co_dogb_s, undefined, { title: LANG.co_dogb_l, show: isTrace }), revbones: UC.newBoolean(LANG.co_dogr_s, undefined, { title: LANG.co_dogr_l, show: () => env.poppedRec.dogbones }), exp_end: UC.endExpand(), - exp: UC.newExpand("feeds & speeds", { open }), + exp: UC.newExpand("feeds & speeds", { }), sep: UC.newBlank({ class: "pop-sep" }), spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: toInt, show: hasSpindle }), rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: toInt, units }), plunge: UC.newInput(LANG.cc_plng_s, { title: LANG.cc_plng_l, convert: toInt, units }), exp_end: UC.endExpand(), - exp: UC.newExpand("bounds", { open }), + exp: UC.newExpand("bounds", { }), sep: UC.newBlank({ class: "pop-sep" }), ov_topz: UC.newInput(LANG.ou_ztop_s, { title: LANG.ou_ztop_l, convert: toFloat, units }), ov_botz: UC.newInput(LANG.ou_zbot_s, { title: LANG.ou_zbot_l, convert: toFloat, units }), diff --git a/src/kiri/mode/cam/op-area.js b/src/kiri/mode/cam/op-area.js index 4e1aad80..a7eae0c3 100644 --- a/src/kiri/mode/cam/op-area.js +++ b/src/kiri/mode/cam/op-area.js @@ -35,7 +35,7 @@ class OpArea extends CamOp { let areaTool = new Tool(settings, tool); let smoothVal = (smooth ?? 0) / 10; let toolDiam = areaTool.fluteDiameter(); - let toolOver = areaTool.getStepSize(over); + let toolOver = this.toolOver = areaTool.getStepSize(over); let zTop = workarea.top_z; let zBottom = workarea.bottom_z; let shadowBase = state.shadow.base; @@ -71,6 +71,7 @@ class OpArea extends CamOp { } // gather area selections + if (!op.shadow) for (let arr of (op.areas[widget.id] ?? [])) { let poly = newPolygon().fromArray(arr); aminz = Math.min(aminz, poly.minZ()); @@ -86,22 +87,30 @@ class OpArea extends CamOp { polys = POLY.nest(POLY.reconnect(polys, false)); // gather surface selections - let vert = widget.getGeoVertices({ unroll: true, translate: true }).map(v => v.round(4)); - let faces = CAM.surface_find(widget, (op.surfaces[widget.id] ?? []), (follow ?? edgeangle ?? 5) * DEG2RAD); - let fpoly = []; - for (let face of faces) { - let i = face * 9; - fpoly.push(newPolygon() - .add(vert[i++], vert[i++], aminz = Math.min(aminz, vert[i++])) - .add(vert[i++], vert[i++], aminz = Math.min(aminz, vert[i++])) - .add(vert[i++], vert[i++], aminz = Math.min(aminz, vert[i++])) - ); - } - // remove invalid edges (eg. when vertical walls are the only selection) - fpoly = fpoly.filter(p => p.area() > 0.001); + if (!op.shadow) { + let vert = widget.getGeoVertices({ unroll: true, translate: true }).map(v => v.round(4)); + let faces = CAM.surface_find(widget, (op.surfaces[widget.id] ?? []), (follow ?? edgeangle ?? 5) * DEG2RAD); + let fpoly = []; + for (let face of faces) { + let i = face * 9; + fpoly.push(newPolygon() + .add(vert[i++], vert[i++], aminz = Math.min(aminz, vert[i++])) + .add(vert[i++], vert[i++], aminz = Math.min(aminz, vert[i++])) + .add(vert[i++], vert[i++], aminz = Math.min(aminz, vert[i++])) + ); + } + // remove invalid edges (eg. when vertical walls are the only selection) + fpoly = fpoly.filter(p => p.area() > 0.001); - // add in unioned surface areas - polys.push(...POLY.setZ(POLY.union(fpoly, 0.00001, true), aminz)); + // add in unioned surface areas + polys.push(...POLY.setZ(POLY.union(fpoly, 0.00001, true), aminz)); + } + + // use part shadow instead of areas or surfaces + if (op.shadow) { + console.log({ use_shadow_base: op.shadow }); + polys.push(...shadowBase.clone(true)); + } // smoothing for jaggies usually caused by vertical walls if (smoothVal) { @@ -343,7 +352,7 @@ class OpArea extends CamOp { } // optional alternating paths if (sr_alter) { - paths = tip2tipJoin(paths, paths[0].first(), toolDiam * 4); + paths = tip2tipJoin(paths, paths[0].first(), toolOver * 10); } } else if (sr_type === 'offset') { @@ -392,6 +401,7 @@ class OpArea extends CamOp { raster.terminate(); // convert terrain raster output back to open polylines + // todo: add leave_z support for (let path of output.paths) { path = newPolygon().fromArray([1, ...path]); if (op.refine) path.refine(op.refine); @@ -417,14 +427,14 @@ class OpArea extends CamOp { } prepare(ops, progress) { - let { op, state, areas, surfaces } = this; + let { op, state, areas, surfaces, toolOver } = this; let { newLayer, pocket, polyEmit, printPoint, tip2tipEmit } = ops; let { setContouring, setNextIsMove } = ops; let { process } = state.settings; // process surface paths if (surfaces.length) { - setContouring(true); + setContouring(true, toolOver * 2); for (let surface of surfaces) { for (let poly of surface) { setNextIsMove(); diff --git a/src/kiri/mode/cam/op-outline.js b/src/kiri/mode/cam/op-outline.js index 69fcf9f2..1d995719 100644 --- a/src/kiri/mode/cam/op-outline.js +++ b/src/kiri/mode/cam/op-outline.js @@ -2,16 +2,12 @@ import { CamOp } from './op.js'; import { OpArea } from './op-area.js'; -import { polygons as POLY } from '../../../geo/polygons.js'; -import { newPolygon } from '../../../geo/polygon.js'; class OpOutline extends CamOp { constructor(state, op) { super(state, op); } - // todo: wide cutout, dogbones - async slice(progress) { let { op, state } = this; let { direction, dogbones, down, inside, omitthru, omitvoid, outside } = op; @@ -26,7 +22,6 @@ class OpOutline extends CamOp { let ops_list = this.ops_list = [ ]; if (outside) { - // let areas = POLY.flatten(POLY.expand(shadow.base, state.tool.fluteDiameter() / 2 - 0.001)); let areas = shadow.base.clone(true); ops_list.push(new OpArea(state, { areas: { [widget.id]: areas.map(p => p.toArray()) }, @@ -37,7 +32,6 @@ class OpOutline extends CamOp { mode: 'trace', omitinner: omitvoid, omitthru, - // outline: omitthru, ov_botz, ov_topz, over: op.wide ? op.step : 0, @@ -66,7 +60,6 @@ class OpOutline extends CamOp { omitinner: omitvoid, omitouter: inside, omitthru, - // outline: omitthru, ov_botz, ov_topz, plunge, @@ -80,9 +73,6 @@ class OpOutline extends CamOp { thru: true, tr_type: 'outside', })); - if (!inside) { - // add outline - } } for (let op of this.ops_list) { diff --git a/web/kiri/lang/en.js b/web/kiri/lang/en.js index 93eba674..1eba625c 100644 --- a/web/kiri/lang/en.js +++ b/web/kiri/lang/en.js @@ -606,6 +606,8 @@ self.lang['en-us'] = { cp_cont_l: ["ignore interior voids and features"], cp_outl_s: "outline only", cp_outl_l: ["ignore interior voids and features"], + cp_shad_s: "shadow", + cp_shad_l: ["select part shadow as work area. overrides selected edges and surface areas"], // CNC DRILLING cd_menu: "drill", @@ -718,6 +720,12 @@ self.lang['en-us'] = { cl_xpnd_s: "expand", cl_xpnd_l: ["grow starting radius for step down. radius is defined as the highest Z point measured from the X axis center of the part"], + // CNC AREA OP + ca_altr_s: "alternate", + ca_altr_l: "alternate direction of travel lines (zigzag)", + ca_sang_s: "step angle", + ca_sang_l: "crossing angle on the XY plane for contour lines", + // OUTPUT ou_menu: "output", From f2f5081669bc70ae1c47cbc20a2ad523a5a81ae8 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 1 Jan 2026 13:06:44 -0500 Subject: [PATCH 075/154] add mesh:tool 3mf object output --- src/load/3mf.js | 130 ++++++++++++++++++++++++++++++++++++++++++++++- src/mesh/api.js | 7 +-- src/mesh/work.js | 5 ++ 3 files changed, 138 insertions(+), 4 deletions(-) diff --git a/src/load/3mf.js b/src/load/3mf.js index bf392988..3a7554ac 100644 --- a/src/load/3mf.js +++ b/src/load/3mf.js @@ -212,6 +212,133 @@ function extractItems(records) { return outItems; } +/** + * Export vertex array(s) to 3MF format + * @param {Array} recs - array of {file: string, varr: Float32Array} records + * @param {Object} options - encoding options + * @param {String} [options.unit='millimeter'] - unit of measurement + * @param {String} [options.title='Model'] - model title + * @returns {Promise} 3MF file as a Blob + */ +export async function encode(recs, options = {}) { + const { + unit = 'millimeter', + title = 'Model' + } = options; + + // Build the 3D model XML + let objectId = 1; + let resources = []; + let buildItems = []; + + for (let rec of recs) { + let { file, varr } = rec; + let name = file || `object-${objectId}`; + + // Extract unique vertices and build triangle indices + let vertices = []; + let triangles = []; + let vertexMap = new Map(); // map "x,y,z" -> vertex index + let vertexIndex = 0; + + // Process triangles (every 9 floats = 3 vertices = 1 triangle) + for (let i = 0; i < varr.length; i += 9) { + let triIndices = []; + + // Process 3 vertices per triangle + for (let j = 0; j < 3; j++) { + let vi = i + j * 3; + let x = varr[vi]; + let y = varr[vi + 1]; + let z = varr[vi + 2]; + + // Create vertex key for deduplication + let key = `${x},${y},${z}`; + let idx = vertexMap.get(key); + + if (idx === undefined) { + idx = vertexIndex++; + vertexMap.set(key, idx); + vertices.push({ x, y, z }); + } + + triIndices.push(idx); + } + + triangles.push(triIndices); + } + + // Generate object XML + let objectXml = [` `]; + objectXml.push(' '); + objectXml.push(' '); + + for (let v of vertices) { + objectXml.push(` `); + } + + objectXml.push(' '); + objectXml.push(' '); + + for (let tri of triangles) { + objectXml.push(` `); + } + + objectXml.push(' '); + objectXml.push(' '); + objectXml.push(' '); + + resources.push(objectXml.join('\n')); + buildItems.push(` `); + + objectId++; + } + + // Construct the complete 3D model XML + let modelXml = [ + '', + ``, + ' ', + resources.join('\n'), + ' ', + ' ', + buildItems.join('\n'), + ' ', + '' + ].join('\n'); + + // Create [Content_Types].xml + let contentTypes = [ + '', + '', + ' ', + ' ', + '' + ].join('\n'); + + // Create _rels/.rels + let rels = [ + '', + '', + ' ', + '' + ].join('\n'); + + // Create ZIP archive + let zip = new JSZip(); + zip.file('[Content_Types].xml', contentTypes); + zip.file('_rels/.rels', rels); + zip.file('3D/3dmodel.model', modelXml); + + // Generate the 3MF file as a blob + return await zip.generateAsync({ + type: "uint8array", + compression: 'DEFLATE', + compressionOptions: { level: 6 }, + streamFiles: true + }); +} + /** * @param {Object} data binary file * @returns {Array} vertex face array @@ -229,4 +356,5 @@ export function parseAsync(data) { } resolve(extractItems(models)); }); -} \ No newline at end of file +} + diff --git a/src/mesh/api.js b/src/mesh/api.js index 080ab478..de17b7ac 100644 --- a/src/mesh/api.js +++ b/src/mesh/api.js @@ -1008,9 +1008,10 @@ let file = { h.input({ id: "filename", value: "mesh_export" }) ]), h.div([ - h.button({ _: "download OBJ", onclick() { doit('obj') } }), - h.button({ _: "download STL", onclick() { doit('stl') } }), - h.button({ _: "download STEP", onclick() { doit('step') } }), + h.button({ _: "3MF", onclick() { doit('3mf') } }), + h.button({ _: "OBJ", onclick() { doit('obj') } }), + h.button({ _: "STL", onclick() { doit('stl') } }), + h.button({ _: "STEP", onclick() { doit('step') } }), ]) ]) ] }); diff --git a/src/mesh/work.js b/src/mesh/work.js index 091a0faa..ccbd0d54 100644 --- a/src/mesh/work.js +++ b/src/mesh/work.js @@ -17,6 +17,7 @@ import { CSG } from '../geo/csg.js'; import { util } from './util.js'; import { tool as meshTool } from './tool.js'; import { meshToSTEPWithFaces } from '../load/step.js'; +import { encode as tmfEncode } from '../load/3mf.js'; import { encode as objEncode } from '../load/obj.js'; import { encode as stlEncode } from '../load/stl.js'; @@ -580,6 +581,10 @@ const file = { return objEncode(recs, header); case "stl": return stlEncode(recs, header); + case "3mf": + send.async(); + tmfEncode(recs, header).then(data => send.done(data)); + return; default: throw `invalid format "${format}"`; } From 3efbf958c0ca02e039e477c6964f1f40e212cc9a Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 1 Jan 2026 13:18:40 -0500 Subject: [PATCH 076/154] fix broken api sync link --- src/kiri/app/conf/settings.js | 2 +- src/kiri/app/do.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/kiri/app/conf/settings.js b/src/kiri/app/conf/settings.js index 97e13395..dadf0782 100644 --- a/src/kiri/app/conf/settings.js +++ b/src/kiri/app/conf/settings.js @@ -98,7 +98,7 @@ function settings_save(ev, name) { api.conf.save(); api.conf.update(); api.event.settings(); - sync_put(); + api.settings.sync.put(); }; if (name) { diff --git a/src/kiri/app/do.js b/src/kiri/app/do.js index 644a1586..bf9836f3 100644 --- a/src/kiri/app/do.js +++ b/src/kiri/app/do.js @@ -20,7 +20,7 @@ let moved = { x: 0, y: 0 }; /** Message ID for the current undo/redo alert message */ let msgid; -event.on("init-done", () => { +event.on("load-done", () => { $('undo').onclick = undo; $('redo').onclick = redo; }); From 989997c9ee900fe4413b84264e06763fa1a9cdce Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 1 Jan 2026 16:07:29 -0500 Subject: [PATCH 077/154] fix z base layer height for parts no on the bed --- src/kiri/mode/fdm/export.js | 7 +++++-- src/kiri/mode/fdm/slice.js | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/kiri/mode/fdm/export.js b/src/kiri/mode/fdm/export.js index 5c5c305b..79e051ee 100644 --- a/src/kiri/mode/fdm/export.js +++ b/src/kiri/mode/fdm/export.js @@ -305,7 +305,7 @@ export function fdm_export(print, online, ondone, ondebug) { for (let pk in process) { appendTok("; ", pk, " = ", process[pk]); } - append("; --- startup ---"); + if (pre < 3) append("; --- startup ---"); } // looking for ";; PREAMBLE " comment @@ -316,6 +316,7 @@ export function fdm_export(print, online, ondone, ondebug) { if (line.indexOf(";; PREAMBLE") === 0) { if (line === ';; PREAMBLE OFF') pre = 1; else if (line === ';; PREAMBLE END') pre = 2; + else if (line === ';; PREAMBLE POST') pre = 3; else gcpre.push(pre = 123); } else if (line.indexOf(";; AXISMAP ") === 0) { let axmap = JSON.parse(line.substring(11).trim()); @@ -984,11 +985,13 @@ export function fdm_export(print, online, ondone, ondebug) { subst.time = util.round(time,2); subst['print_time'] = subst.time; - append("; --- shutdown ---"); + // append("; --- shutdown ---"); appendAllSub(device.gcodePost); append(`; --- filament used: ${subst.material} mm ---`); append(`; --- print time: ${time.toFixed(0)}s ---`); + if (pre === 3) preamble(); + // force emit of buffer append(); // console.log({segments, emitted, outputLength}); diff --git a/src/kiri/mode/fdm/slice.js b/src/kiri/mode/fdm/slice.js index 59b7d1da..4aec0763 100644 --- a/src/kiri/mode/fdm/slice.js +++ b/src/kiri/mode/fdm/slice.js @@ -250,7 +250,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) { } let { zMin, zMax } = zopt; let h1 = sliceHeight; - let h0 = sliceHeightBase || h1; + let h0 = Math.abs(zMin) < 0.0001 ? (sliceHeightBase || h1) : h1; let hm = sliceMinHeight || 0; let h = h0; let z = h0; From 29e303b7f970fdf09f28400d0aa8ce71dad451fb Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 1 Jan 2026 21:01:44 -0500 Subject: [PATCH 078/154] checkpoint fdm arc conversion --- src/kiri/app/conf/defaults.js | 4 +- src/kiri/core/print.js | 48 +++++- src/kiri/mode/fdm/export.js | 300 ++++++--------------------------- src/kiri/mode/fdm/init-menu.js | 5 +- src/kiri/mode/fdm/post.js | 1 + src/kiri/mode/fdm/prepare.js | 6 + src/kiri/mode/fdm/slice.js | 81 --------- web/kiri/lang/en.js | 2 - 8 files changed, 106 insertions(+), 341 deletions(-) diff --git a/src/kiri/app/conf/defaults.js b/src/kiri/app/conf/defaults.js index 662fbc76..dcc37957 100644 --- a/src/kiri/app/conf/defaults.js +++ b/src/kiri/app/conf/defaults.js @@ -261,7 +261,9 @@ export const conf = { // process defaults FDM:Process p:{ antiBacklash: 0, - arcTolerance: 0, + fdmArcEnabled: false, + fdmArcResolution: 1, + fdmArcTolerance: 0.005, firstLayerBedTemp: 0, firstLayerBeltBump: 0, firstLayerBeltLead: 3, diff --git a/src/kiri/core/print.js b/src/kiri/core/print.js index a090a0c6..a1653804 100644 --- a/src/kiri/core/print.js +++ b/src/kiri/core/print.js @@ -4,6 +4,7 @@ import { arcToPath } from '../../geo/paths.js'; import { consts } from './consts.js'; import { createVM } from '../../moto/quickjs.js'; import { newPoint } from '../../geo/point.js'; +import { newPolygon } from '../../geo/polygon.js'; import { util } from '../../geo/base.js'; const { numOrDefault } = util; @@ -42,7 +43,7 @@ class Print { * addOutput - add a new point to the output gcode array * @param {any[]} array - the output gcode array * @param {Point} point - the new point - * @param {number} emit - the extrusion value + * @param {number} emit - the extrusion value (G? value for cnc) * @param {number} speed - the feed rate * @param {string} tool - the tool id * @param {"lerp"|string} opts.type - the output type @@ -52,7 +53,7 @@ class Print { * @return {Output} - the new output object */ addOutput(array, point, emit, speed, tool, opts) { - const { type, retract, center, arcPoints} = opts ?? {}; + const { type, retract, center, arcPoints, clockwise, distance } = opts ?? {}; let { lastPoint, lastEmit, lastOut } = this; let arc = emit == 2 || emit == 3; // drop duplicates (usually intruced by FDM bisections) @@ -69,7 +70,9 @@ class Print { this.lastOut = lastOut = new Output(point, emit, speed, tool, { type: type ?? this.nextType, center, + clockwise, arcPoints, + distance }); if (tool !== undefined) { this.tools[tool] = true; @@ -130,10 +133,9 @@ class Print { perimeter = poly.perimeter(), close = !options.open, scarf = !poly.open ? (options.scarf ?? 0) : false, - tool = options.tool, - zmax = options.zmax, last = startPoint, - first = true; + first = true, + { arcOpts, tool, zmax } = options; // if short, use calculated print speed based on sliding scale if (perimeter < process.outputShortPoly) { @@ -193,6 +195,9 @@ class Print { } pp.push(...esp); scarf = true; + } else if (arcOpts) { + // annotate arc points + pp = newPolygon(pp).detectArcs(arcOpts).points; } // scarf manages its own close point @@ -201,8 +206,34 @@ class Print { } let lpo; + let arcInfo; + let arcDist = 0; for (let point of pp) { + let { arc } = point; + if (arc) { + arcInfo = arc; + // non-emit arc points get -1 for rendering + scope.addOutput(output, point, -1, moveSpeed, tool); + arcDist += last.distTo2D(point); + last = point; + continue; + } else if (arcInfo) { + arcDist += last.distTo2D(point); + if (arcInfo.skip-- === 0) { + // last point of arc is arcTo? + arcInfo.distance = arcDist; + scope.addOutput(output, point, shellMult, moveSpeed, tool, arcInfo); + arcInfo = undefined; + arcDist = 0; + } else { + // non-emit arc points get -1 for rendering + scope.addOutput(output, point, -1, moveSpeed, tool); + } + last = point; + continue; + } if (point.skip && lpo?.skip) { + // basic thin wall skips are converted to moves scope.addOutput(output, point, 0, moveSpeed, tool); } else if (first) { // if (point.skip) console.log({ skip: point }); @@ -746,15 +777,16 @@ class Output { */ constructor(point, emit, speed, tool, options) { - const { type, center, arcPoints } = (options ?? {}); - //speed, tool, type, center, arcPoints + const { type, center, arcPoints, clockwise, distance } = (options ?? {}); + this.point = point; this.emit = Number(emit); //convert bools into 0/1 this.speed = speed; this.tool = tool; this.type = type; this.center = center; - this.arcPoints = arcPoints; + this.clockwise = clockwise; + this.distance = distance; // this.where = new Error().stack.split("\n"); } diff --git a/src/kiri/mode/fdm/export.js b/src/kiri/mode/fdm/export.js index 79e051ee..5b432854 100644 --- a/src/kiri/mode/fdm/export.js +++ b/src/kiri/mode/fdm/export.js @@ -57,10 +57,6 @@ export function fdm_export(print, online, ondone, ondebug) { retSpeed = process.outputRetractSpeed * 60 || 1, // range retDwell = process.outputRetractDwell || 0, // range scarfZ = process.outputScarfLength ? true : false, - arcDist = isBelt || scarfZ ? 0 : (process.arcTolerance || 0), - arcRes = 20, - arcDev = 0.5, - arcMax = 40, originCenter = device.originCenter || bedRound, offset = originCenter ? { x: 0, @@ -119,7 +115,6 @@ export function fdm_export(print, online, ondone, ondebug) { bcos = Math.cos(Math.PI / 4), icos = 1 / bcos, inloops = 0, - arcQ = [], minz = { x: Infinity, y: Infinity, z: Infinity }, // lenghts of each filament (by nozzle) consumed segments = []; @@ -411,7 +406,6 @@ export function fdm_export(print, online, ondone, ondebug) { if (!retracted) { return; } - drainQ(); // console.log({engage:zhop}); // when enabled, resume previous Z if (zhop && pos.z != zpos) moveTo({z:zpos}, seekMMM, "z-hop end"); @@ -445,8 +439,10 @@ export function fdm_export(print, online, ondone, ondebug) { moveTo(savePos, rate, comment); } - function moveTo(newpos, rate, comment) { - let o = [!rate && !newpos.e ? 'G0' : 'G1']; + function moveTo(newpos, rate, comment, arc) { + let o = arc ? + [arc.clockwise ? 'G2' : 'G3'] : + [!rate && !newpos.e ? 'G0' : 'G1']; let emit = { x: false, y: false, z: false }; if (typeof newpos.x === 'number' && newpos.x !== pos.x) { pos.x = newpos.x; @@ -473,6 +469,11 @@ export function fdm_export(print, online, ondone, ondebug) { if (emit.x) o.append(axis.X).append(epos.x.toFixed(decimals)); if (emit.y) o.append(axis.Y).append(epos.y.toFixed(decimals)); if (emit.z) o.append(axis.Z).append(epos.z.toFixed(decimals)); + if (arc) { + let { x, y } = arc.center; + o.push(' I', (x + offset.x - pos.x).toFixed(decimals)); + o.push(' J', (y + offset.y - pos.y).toFixed(decimals)); + } if (debug) { if (emit.x) minz.x = Math.min(minz.x, epos.x); if (emit.y) minz.y = Math.min(minz.y, epos.y); @@ -608,36 +609,39 @@ export function fdm_export(print, online, ondone, ondebug) { // iterate through layer outputs for (pidx=0; pidx 1) { - let el = arcQ.length; - deem = arcQ[0].e !== rec.e; - depm = arcQ[0].emitPerMM !== rec.emitPerMM; - desp = arcQ[0].speedMMM !== rec.speedMMM; - } - // ondebug({arcQ}); - if (arcQ.length > 2) { - let el = arcQ.length; - let e1 = arcQ[0]; // first in arcQ - let e2 = arcQ[Math.floor(el/2)]; // mid in arcQ - let e3 = arcQ[el-1]; // last in arcQ - let e4 = arcQ[el-2]; // second last in arcQ - let e5 = arcQ[el-3]; // third last in arcQ - let cc = util.center2d(e1, e2, e3, 1); // find center - let lr = util.center2d(e3, e4, e5, 1); // find local radius - let dc = 0; - - let radFault = false; - if (lr) { - let angle = 2 * Math.asin(dist/(2*lr.r)); - radFault = Math.abs(angle) > Math.PI * 2 / arcRes; // enforce arcRes(olution) - // if (arcQ.center) { - // arcQ.rSum = arcQ.center.reduce( function (t, v) { return t + v.r }, 0 ); - // let avg = arcQ.rSum / arcQ.center.length; - // radFault = radFault || Math.abs(avg - lr.r) / avg > arcDev; // eliminate sharps and flats when local rad is out of arcDev(iation) - // } - } else { - radFault = true; - } - - if (cc) { - if ([cc.x,cc.y,cc.z,cc.r].hasNaN()) { - console.log({cc, e1, e2, e3}); - } - if (arcQ.length === 3) { - arcQ.center = [ cc ]; - arcQ.xSum = cc.x; - arcQ.ySum = cc.y; - arcQ.rSum = cc.r; - } else { - // check center point delta - arcQ.xSum = arcQ.center.reduce( function (t, v) { return t + v.x }, 0 ); - arcQ.ySum = arcQ.center.reduce( function (t, v) { return t + v.y }, 0 ); - arcQ.rSum = arcQ.center.reduce( function (t, v) { return t + v.r }, 0 ); - let dx = cc.x - arcQ.xSum / arcQ.center.length; - let dy = cc.y - arcQ.ySum / arcQ.center.length; - dc = Math.sqrt(dx * dx + dy * dy); - } - - // if new point is off the arc - 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) { - // not enough points for an arc, drop first point and recalc center - emitQrec(arcQ.shift()); - let tc = util.center2d(arcQ[0], arcQ[1], arcQ[2], 1); - // the new center is invalid as well. drop the first point - if (!tc) { - emitQrec(arcQ.shift()); - } else { - arcQ.center = [ tc ]; - let angle = 2 * Math.asin(arcQ[1].dist/(2*tc.r)); - if (Math.abs(angle) > Math.PI * 2 / arcRes) { // enforce arcRes on initial angle - emitQrec(arcQ.shift()); - } - } - } else { - // enough to consider an arc, emit and start new arc - let defer = arcQ.pop(); - drainQ(); - // re-add point that was off the last arc - arcQ.push(defer); - } - } else { - // new point is on the arc - arcQ.center.push(cc); - } - } else { - // drainQ on invalid center - drainQ(); - } - } + if (lastp && emit) { + // emitMM = emitPerMM * emit * dist; + emitMM = extrudeMM(dist, emitPerMM, emit); + if (scarfZ) { + moveTo({x, y, z:z + path.height/2, e:emitMM}, speedMMM); } else { - // emitMM = emitPerMM * out.emit * dist; - emitMM = extrudeMM(dist, emitPerMM, out.emit); - if (scarfZ) { - moveTo({x, y, z:z + path.height/2, e:emitMM}, speedMMM); - } else { - moveTo({x, y, e:emitMM}, speedMMM); - } - emitted += emitMM; + moveTo({x, y, e:emitMM}, speedMMM); } + emitted += emitMM; } else { - drainQ(); if (scarfZ) { moveTo({x, y, z:z + path.height/2}, speedMMM); } else { @@ -808,7 +734,6 @@ export function fdm_export(print, online, ondone, ondebug) { // retract filament if point retract flag set if (out.retract) { - drainQ(); retract(zhop); } @@ -829,8 +754,8 @@ export function fdm_export(print, online, ondone, ondebug) { lastProgress = progress; } - lastp = out.point; - laste = out.emit; + lastp = point; + laste = emit; } layer++; if (cwidget) { @@ -843,7 +768,6 @@ export function fdm_export(print, online, ondone, ondebug) { while (endloop-- > 0) { append(`M808`); } - drainQ(); print.total_time -= path.print_time; subst.remain_time = Math.ceil(print.total_time / 60); @@ -856,126 +780,6 @@ export function fdm_export(print, online, ondone, ondebug) { emitted += emitMM; } - function drainQ() { - if (!arcDist) { - return; - } - if (arcQ.length > 4) { - // ondebug({arcQ}); - let vec1 = new THREE.Vector2(arcQ[1].x - arcQ[0].x, arcQ[1].y - arcQ[0].y); - let vec2 = new THREE.Vector2(arcQ.center[0].x - arcQ[0].x, arcQ.center[0].y - arcQ[0].y); - let gc = vec1.cross(vec2) < 0 ? 'G2' : 'G3'; - let from = arcQ[0]; - let to = arcQ.peek(); - arcQ.xSum = arcQ.center.reduce( function (t, v) { return t + v.x }, 0 ); - arcQ.ySum = arcQ.center.reduce( function (t, v) { return t + v.y }, 0 ); - arcQ.rSum = arcQ.center.reduce( function (t, v) { return t + v.r }, 0 ); - let cl = arcQ.center.length; - let cc; - - let angle = util.thetaDiff( - Math.atan2((from.y - arcQ.ySum / cl), (from.x - arcQ.xSum / cl)), - Math.atan2((to.y - arcQ.ySum / cl), (to.x - arcQ.xSum / cl)), - gc === "G2" - ); - - if (Math.abs(angle) <= 3 * Math.PI / 4) { - cc = util.center2pr(from, to, arcQ.rSum / cl, gc === "G3"); - } - - if (!cc) { - cc = {x:arcQ.xSum/cl, y:arcQ.ySum/cl, z:arcQ[0].z, r:arcQ.rSum/cl}; - } - - // first arc point - emitQrec(from); - // console.log(arcQ.slice(), arcQ.center); - // console.log({first: from, last: arcQ.peek(), center: cc}); - // rest of arc to final point - let dist = arcQ.slice(1).map(v => v.dist).reduce((a,v) => a+v); - let emit = from.e;//arcQ.slice(1).map(v => v.e).reduce((a,v) => a+v); - emit = (from.emitPerMM * emit * dist); - outputLength += emit; - emitted += emit; - if (extrudeAbs) { - emit = outputLength; - } - // XYR form - // let pre = `${gc} X${to.x.toFixed(decimals)} Y${to.y.toFixed(decimals)} R${cc.r.toFixed(decimals)} E${emit.toFixed(decimals)}`; - // XYIJ form - let pre = `${gc} X${to.x.toFixed(decimals)} Y${to.y.toFixed(decimals)} I${(cc.x - pos.x).toFixed(decimals)} J${(cc.y - pos.y).toFixed(decimals)} E${emit.toFixed(decimals)}`; - let add = pos.f !== from.speedMMM ? ` E${from.speedMMM}` : ''; - append(`${pre}${add} ; merged=${cl-1} len=${dist.toFixed(decimals)} cp=${cc.x.round(2)},${cc.y.round(2)}`); - pos.x = to.x; - pos.y = to.y; - pos.z = to.z; - } else { - for (let rec of arcQ) { - emitQrec(rec); - } - } - arcQ.length = 0; - arcQ.center = undefined; - } - - // comprehensive arc validator - function arcValid() { - if (arcQ.length < 3) { - return false; - } - - let globalCenters = []; // see how a point first the curve within the context of the arc's end points. - - for (let i = 0; i < arcQ.length - 2; i++) { - let cc = util.center2d(arcQ[0], arcQ[i+1], arcQ[arcQ.length - 1], 1); - if (!cc) { - return false; - } - globalCenters.push(cc); - } - - let ac = { // average center - x:globalCenters.reduce( (t,v) => t + v.x , 0) / (globalCenters.length), - y:globalCenters.reduce( (t,v) => t + v.y , 0) / (globalCenters.length), - z:globalCenters.reduce( (t,v) => t + v.z , 0) / (globalCenters.length), - r:globalCenters.reduce( (t,v) => t + v.r , 0) / (globalCenters.length) - }; - - // make sure centers are within specified tolerance - for (let cc of globalCenters) { - let dc = Math.sqrt(Math.pow(cc.x - ac.x, 2) + Math.pow(cc.y - ac.y, 2)); - if (dc / ac.r > arcDist) { - return false; - } - } - - // make sure radii are within specified tolerance - for (let point of arcQ) { - let rad = Math.sqrt(Math.pow(point.x - ac.x, 2) + Math.pow(point.y - ac.y, 2)); - if (Math.abs(rad - ac.r) > ac.r * arcDist) { - return false; - } - } - - // enforce arcRes(olution) - for (let i = 1; i < arcQ.length; i++) { - let angle = 2 * Math.asin(arcQ[i].dist/(2*ac.r)); - if (Math.abs(angle) > Math.PI * 2 / arcRes) { - return false; - } - } - - // check points in the context of neighbors - for (let i = 0; i < arcQ.length - 2; i++) { - let cc = util.center2d(arcQ[i], arcQ[i+1], arcQ[i+2], 1); - if (!cc || Math.abs((cc.r - ac.r) / cc.r) > arcDev) { - return false; - } - } - - return true; - } - if (inloops) { append(`M808`); } diff --git a/src/kiri/mode/fdm/init-menu.js b/src/kiri/mode/fdm/init-menu.js index 121a87be..88084ea8 100644 --- a/src/kiri/mode/fdm/init-menu.js +++ b/src/kiri/mode/fdm/init-menu.js @@ -168,6 +168,10 @@ export function menu() { outputRetractSpeed: newInput(LANG.ad_rrat_s, {title:LANG.ad_rrat_l, convert:toInt, show:notFwRetract}), outputRetractWipe: newInput(LANG.ad_wpln_s, {title:LANG.ad_wpln_l, convert:toFloat, bound:bound(0.0,10)}), separator: newBlank({ class:"set-sep", driven }), + fdmArcEnabled: newBoolean(LANG.cx_arce_s, onBooleanClick, { title:LANG.cx_arce_l }), + fdmArcTolerance: newInput(LANG.cx_arct_s, {title:LANG.cx_arct_l, convert:toFloat, bound:bound(0,100), trigger, show:() => ui.fdmArcEnabled.checked}), + fdmArcResolution: newInput(LANG.cx_arcr_s, {title:LANG.cx_arcr_l, convert:toFloat, bound:bound(0,180), trigger, show:() => ui.fdmArcEnabled.checked}), + separator: newBlank({ class:"set-sep", driven, show:() => ui.fdmArcEnabled.checked}), outputAvoidGaps: newBoolean(LANG.ad_agap_s, onBooleanClick, {title:LANG.ad_agap_l}), sliceSolidify: newBoolean(LANG.ad_sold_s, onBooleanClick, {title:LANG.ad_sold_l}), separator: newBlank({ class:"set-sep", driven, show:isBelt }), @@ -175,7 +179,6 @@ export function menu() { _____: newGroup(LANG.ad_menu, $('fdm-expert'), { modes:FDM, driven, hideable, separator, group:"fdm-xprt" }), sliceAngle: newInput(LANG.sl_angl_s, {title:LANG.sl_angl_l, convert:toFloat, show:isBelt}), antiBacklash: newInput(LANG.ad_abkl_s, {title:LANG.ad_abkl_l, convert:toFloat, bound:bound(0,3)}), - 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)}), diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index a8e3fda7..a6b0bec6 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -145,6 +145,7 @@ function offset_default(params) { return { last, gaps }; } + // basic thin wall testing const deltaBig = 160; const deltaSmall = 20; diff --git a/src/kiri/mode/fdm/prepare.js b/src/kiri/mode/fdm/prepare.js index 4dfe09fa..480c1b84 100644 --- a/src/kiri/mode/fdm/prepare.js +++ b/src/kiri/mode/fdm/prepare.js @@ -848,6 +848,11 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { maxFlowRate = process.outputMaxFlowrate, wipeDist = process.outputRetractWipe || 0, isBelt = device.bedBelt, + { fdmArcEnabled, fdmArcTolerance, fdmArcResolution } = process, + arcOpts = { + tolerance: fdmArcTolerance, + arcRes: fdmArcResolution + }, bedOffset = originCenter ? { x: 0, y: 0, @@ -1105,6 +1110,7 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { let scarf = (slice.index > 0 && traceNo === 0 && z - startPoint?.z > 1e-3) ? scarfLength : 0; startPoint = print.polyPrintPath(poly, startPoint, preout, { accel: finishShell, + arcOpts: fdmArcEnabled ? arcOpts : undefined, ccw: opt.shell && process.outputAlternating && slice.index % 2, coast: firstLayer ? 0 : coastDist, extrude: numOrDefault(opt.extrude, shellMult), diff --git a/src/kiri/mode/fdm/slice.js b/src/kiri/mode/fdm/slice.js index 4aec0763..efe91db0 100644 --- a/src/kiri/mode/fdm/slice.js +++ b/src/kiri/mode/fdm/slice.js @@ -958,7 +958,6 @@ export function fdm_slice(settings, widget, onupdate, ondone) { widget.belt.midy = (bounds.miny + bounds.maxy) / 2; } } - } function connect_lines(lines, maxd = Infinity) { @@ -1878,84 +1877,4 @@ export function supports(settings, widget) { console.log(`support generated in ${Date.now() - now} ms`); widget.supports = add; return add.length > 0; -}; - -class Vector3Cache { - constructor() { - this.cache = {}; - } - - get(x, y, z) { - let key = [x.round(4),y.round(4),z.round(4)].join(','); - let val = this.cache[key]; - if (!val) { - val = new THREE.Vector3(x, y, z); - this.cache[key] = val; - } - return val; - } -} - -class Coplanars { - constructor() { - this.cache = {}; - } - - put(a, b, c, norm) { - let key = norm.round(7).toString(); - let arr = this.cache[key]; - if (!arr) { - arr = []; - this.cache[key] = arr; - } - arr.push([a,b,c]); - } - - group(union) { - let out = {}; - for (let norm in this.cache) { - let arr = this.cache[norm]; - let groups = []; - for (let face of arr) { - let match = undefined; - // see if face matches vertices in any group - outer: for (let group of groups) { - for (let el of group) { - if ( - el.indexOf(face[0]) >= 0 || - el.indexOf(face[1]) >= 0 || - el.indexOf(face[2]) >= 0 - ) { - match = group; - break outer; - } - } - } - if (match) { - match.push(face); - } else { - groups.push([face]); - } - } - if (union) { - // convert groups of faces to contiguous polygon groups - groups = groups.map(group => { - let parr = group.map(arr => { - return newPolygon() - .add(arr[0].x, arr[0].y, arr[0].z) - .add(arr[1].x, arr[1].y, arr[1].z) - .add(arr[2].x, arr[2].y, arr[2].z); - }); - let union = parr.length === 1 ? parr : - POLY.union(parr, 0, true, {wasm:false}); - union.merged = parr.length; - union.face = group[0]; - return union; - }); - } - out[norm] = groups; - } - // console.log(out); - return out; - } } diff --git a/web/kiri/lang/en.js b/web/kiri/lang/en.js index 1eba625c..f5f12250 100644 --- a/web/kiri/lang/en.js +++ b/web/kiri/lang/en.js @@ -902,8 +902,6 @@ self.lang['en-us'] = { ad_scar_l: ["ease up length for scarf joint. disables arc detection","0 = disabled"], ad_spol_s: "short path", ad_spol_l: ["polygons shorter than this","will have their print speed","scaled down to min speed","in millimeters"], - ad_arct_s: "arc tolerance", - ad_arct_l: ["convert faceted lines to arcs","center point drift tolerance","when matching arc points","consider values around 0.15","value in millimeters","0 or scarf disables this"], ad_zhop_s: "z hop distance", ad_zhop_l: ["amount to raise z","on retraction moves","in millimeters","0 to disable"], ad_abkl_s: "anti-backlash", From 36a634e805f5b4b544d83633b5792c572d850ca2 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 1 Jan 2026 22:27:57 -0500 Subject: [PATCH 079/154] fix fdm arc center output --- src/kiri/core/print.js | 7 +++---- src/kiri/mode/fdm/export.js | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/kiri/core/print.js b/src/kiri/core/print.js index a1653804..17bdf87e 100644 --- a/src/kiri/core/print.js +++ b/src/kiri/core/print.js @@ -213,14 +213,13 @@ class Print { if (arc) { arcInfo = arc; // non-emit arc points get -1 for rendering - scope.addOutput(output, point, -1, moveSpeed, tool); - arcDist += last.distTo2D(point); + scope.addOutput(output, point, shellMult, moveSpeed, tool); + arcInfo.center.move({ x: -point.x, y: -point.y, z: 0 }); last = point; continue; } else if (arcInfo) { arcDist += last.distTo2D(point); - if (arcInfo.skip-- === 0) { - // last point of arc is arcTo? + if (--arcInfo.skip === 0) { arcInfo.distance = arcDist; scope.addOutput(output, point, shellMult, moveSpeed, tool, arcInfo); arcInfo = undefined; diff --git a/src/kiri/mode/fdm/export.js b/src/kiri/mode/fdm/export.js index 5b432854..c84fa007 100644 --- a/src/kiri/mode/fdm/export.js +++ b/src/kiri/mode/fdm/export.js @@ -26,6 +26,7 @@ export function fdm_export(print, online, ondone, ondebug) { { extrudeAbs } = device, { exportThumb } = controller, { extrudeMM, extrudePerMM } = FDM, + { outputInvertX, outputInvertY } = process, timeFactor = (device.gcodeTime || 1) * 1.5, decimals = config.gcode_decimals || 4, zMoveMax = device.deviceZMax || 0, @@ -471,8 +472,8 @@ export function fdm_export(print, online, ondone, ondebug) { if (emit.z) o.append(axis.Z).append(epos.z.toFixed(decimals)); if (arc) { let { x, y } = arc.center; - o.push(' I', (x + offset.x - pos.x).toFixed(decimals)); - o.push(' J', (y + offset.y - pos.y).toFixed(decimals)); + o.push(' I', x.toFixed(decimals)); + o.push(' J', y.toFixed(decimals)); } if (debug) { if (emit.x) minz.x = Math.min(minz.x, epos.x); @@ -668,20 +669,19 @@ export function fdm_export(print, online, ondone, ondebug) { continue; } - let x = point.x + offset_x, - y = point.y + offset_y, + // translate point to workspace coordinates + let x = point.x + offset_x + offset.x, + y = point.y + offset_y + offset.y, z = point.z; // adjust for inversions and origin offsets - if (process.outputInvertX) x = -x; - if (process.outputInvertY) y = -y; - if (offset) { - x += offset.x; - y += offset.y; - } + if (outputInvertX) x = -x; + if (outputInvertY) y = -y; // G2,G3 arcs if (center) { + if (outputInvertX) center.x = -center.x; + if (outputInvertY) center.y = -center.y; emitMM = extrudeMM(out.distance, emitPerMM, emit); moveTo({ x, y, e: emitMM }, speedMMM, 'arc', { clockwise: out.clockwise, From 76fabfa7f5192b87dd9fd522f3dc5d1097655bcc Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 1 Jan 2026 22:43:35 -0500 Subject: [PATCH 080/154] arc notes. rm dead code --- src/kiri/core/print.js | 3 ++- src/kiri/mode/cam/prepare.js | 1 + src/kiri/mode/fdm/export.js | 17 ++++++----------- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/kiri/core/print.js b/src/kiri/core/print.js index 17bdf87e..3b19b106 100644 --- a/src/kiri/core/print.js +++ b/src/kiri/core/print.js @@ -212,12 +212,13 @@ class Print { let { arc } = point; if (arc) { arcInfo = arc; - // non-emit arc points get -1 for rendering scope.addOutput(output, point, shellMult, moveSpeed, tool); + // arc center is relative to first point arcInfo.center.move({ x: -point.x, y: -point.y, z: 0 }); last = point; continue; } else if (arcInfo) { + // accumulate and pass to last point to calc FDM E value arcDist += last.distTo2D(point); if (--arcInfo.skip === 0) { arcInfo.distance = arcDist; diff --git a/src/kiri/mode/cam/prepare.js b/src/kiri/mode/cam/prepare.js index 43a0794e..179108f8 100644 --- a/src/kiri/mode/cam/prepare.js +++ b/src/kiri/mode/cam/prepare.js @@ -866,6 +866,7 @@ export async function prepare_one(widget, settings, print, firstPoint, update) { let { arc } = point; skip = arc.skip; type = arc.clockwise ? 2 : 3; + // arc center is relative to first point center = arc.center.clone().move({ x: -point.x, y: -point.y }); xfactors.push(xfactors.shift()); } diff --git a/src/kiri/mode/fdm/export.js b/src/kiri/mode/fdm/export.js index c84fa007..0ebd97ff 100644 --- a/src/kiri/mode/fdm/export.js +++ b/src/kiri/mode/fdm/export.js @@ -120,6 +120,7 @@ export function fdm_export(print, online, ondone, ondebug) { // lenghts of each filament (by nozzle) consumed segments = []; + // build tool use list for this job let tools_used = Object.keys(tools); for (let tool of tools_used) { subst[`tool_used_${tool}`] = true; @@ -694,6 +695,7 @@ export function fdm_export(print, online, ondone, ondebug) { continue; } + // distance to last point for accumulation and emit calc dist = lastp ? lastp.distTo2D(point) : 0; // re-engage post-retraction before new extrusion @@ -705,6 +707,8 @@ export function fdm_export(print, online, ondone, ondebug) { if (isBelt) { setTempFanSpeed(out.fan); } + + // run fan speed macro when value changes if (fanSpeed !== lastFanSpeed) { appendAllSub(gcodeFan); lastFanSpeed = fanSpeed; @@ -741,10 +745,8 @@ export function fdm_export(print, online, ondone, ondebug) { if (dist && speedMMM) { time += (dist / speedMMM) * 60 * timeFactor; } - // if (isNaN(time)) { - // console.log({ layer, path, dist, speedMMM, timeFactor }); - // throw "NaN"; - // } + + // accumulate distance traveled and update macro var distance += dist; subst.progress = progress = Math.round((distance / totaldistance) * 100); @@ -773,13 +775,6 @@ export function fdm_export(print, online, ondone, ondebug) { subst.remain_time = Math.ceil(print.total_time / 60); } - function emitQrec(rec) { - let {e, x, y, dist, emitPerMM, speedMMM} = rec; - emitMM = extrudeMM(dist, emitPerMM, e); - moveTo({x:x, y:y, e:emitMM}, speedMMM); - emitted += emitMM; - } - if (inloops) { append(`M808`); } From 45803664f1067a64a480df2f16c5bed48bc0b3ce Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 1 Jan 2026 23:43:46 -0500 Subject: [PATCH 081/154] add function to force op change when chaining ops internally like roughing does --- src/kiri/mode/cam/op-rough.js | 4 +++- src/kiri/mode/cam/prepare.js | 9 ++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/kiri/mode/cam/op-rough.js b/src/kiri/mode/cam/op-rough.js index 11349a1f..a83e4d28 100644 --- a/src/kiri/mode/cam/op-rough.js +++ b/src/kiri/mode/cam/op-rough.js @@ -106,10 +106,12 @@ class OpRough extends CamOp { await op.slice(progress); } } - +s async prepare(ops, progress) { + let { setChangeOp } = ops; for (let op of this.ops_list) { await op.prepare(ops, progress); + setChangeOp(); } } } diff --git a/src/kiri/mode/cam/prepare.js b/src/kiri/mode/cam/prepare.js index 179108f8..4973c239 100644 --- a/src/kiri/mode/cam/prepare.js +++ b/src/kiri/mode/cam/prepare.js @@ -131,6 +131,7 @@ export async function prepare_one(widget, settings, print, firstPoint, update) { lastTravelBounds, newOutput = print.output || [], nextIsMove = true, + nextIsNewOp = false, plungeRate = camFastFeedZ, printPoint, tool, @@ -354,6 +355,10 @@ export async function prepare_one(widget, settings, print, firstPoint, update) { nextIsMove = true; } + function setChangeOp() { + nextIsNewOp = true; + } + /** * Move a point by the widget's movement offset. * @param {Point} p - point to move @@ -475,11 +480,12 @@ export async function prepare_one(widget, settings, print, firstPoint, update) { // 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 !== currentOp) { + if (nextIsNewOp || lop !== currentOp) { layerPush(printPoint.clone().setZ(zSafe).setA(printPoint.a), 0, feedRate, tool); layerPush(point.clone().setZ(zSafe).setA(printPoint.a), 0, feedRate, tool); layerPush(point.clone().setZ(zSafe), 0, feedRate, tool); newLayer(); + nextIsNewOp = false; } // consume forced next move flag and convert to move @@ -965,6 +971,7 @@ export async function prepare_one(widget, settings, print, firstPoint, update) { poly2polyEmit, polyEmit, printPoint, + setChangeOp, setContouring, setDrill, setLasering, From fc9b27fcfda89907ae3266f5598566c444ecf7f0 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 1 Jan 2026 23:49:12 -0500 Subject: [PATCH 082/154] add zflatup slicer option to restrict flats to those facing up --- src/kiri/mode/cam/op-rough.js | 2 +- src/kiri/mode/cam/slicer_cam.js | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/kiri/mode/cam/op-rough.js b/src/kiri/mode/cam/op-rough.js index 11349a1f..a40dbd7d 100644 --- a/src/kiri/mode/cam/op-rough.js +++ b/src/kiri/mode/cam/op-rough.js @@ -53,7 +53,7 @@ class OpRough extends CamOp { })); if (op.flats) { - let slicer = newSlicer(); + let slicer = newSlicer({ zflatup: true }); ops_list.push(new OpArea(state, { rename: op.rename ?? "flats", spindle: op.spindle, diff --git a/src/kiri/mode/cam/slicer_cam.js b/src/kiri/mode/cam/slicer_cam.js index 4a4109ea..862914fb 100644 --- a/src/kiri/mode/cam/slicer_cam.js +++ b/src/kiri/mode/cam/slicer_cam.js @@ -101,6 +101,11 @@ export class Slicer { // use co-flat and co-line detection to adjust slice 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 + if (opt.zflatup) { + // filter to up-facing flats (more expensive) + let norm = THREE.computeFaceNormal(p1,p2,p3); + if (norm.z < -0.00001) continue; + } let zkey = p1.z.toFixed(zDecimal), area = Math.abs(util.area2(p1, p2, p3)) / 2; if (!zFlat[zkey]) { From 1bda9ecfd43e820391cae5f88406f590a0a594f6 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 2 Jan 2026 12:16:46 -0500 Subject: [PATCH 083/154] update raster-path version dep --- package.json | 2 +- src/kiri/mode/cam/op-area.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 65e77421..3dec9879 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@gridspace/app-server": "^0.0.20", "@gridspace/basic-ftp": "github:gridspace/basic-ftp#v5.0.5-gridspace", "@gridspace/net-level-client": "^0.2.3", - "@gridspace/raster-path": "^1.0.8", + "@gridspace/raster-path": "^1.0.9", "@tracespace/parser": "^5.0.0-next.0", "@tweenjs/tween.js": "^16.6.0", "aedes": "^0.51.3", diff --git a/src/kiri/mode/cam/op-area.js b/src/kiri/mode/cam/op-area.js index a7eae0c3..35a6de58 100644 --- a/src/kiri/mode/cam/op-area.js +++ b/src/kiri/mode/cam/op-area.js @@ -108,7 +108,6 @@ class OpArea extends CamOp { // use part shadow instead of areas or surfaces if (op.shadow) { - console.log({ use_shadow_base: op.shadow }); polys.push(...shadowBase.clone(true)); } From f25e75f557f3a2563ec22fc34c92da53f8d449ef Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 2 Jan 2026 21:25:56 -0500 Subject: [PATCH 084/154] stub in loop operation --- src/kiri/app/inputs.js | 3 +++ src/kiri/mode/cam/cl-ops.js | 8 +++++++ src/kiri/mode/cam/init-ui.js | 30 +++++++++++++++++++++++ src/kiri/mode/cam/op-loop.js | 17 +++++++++++++ src/kiri/mode/cam/ops.js | 2 ++ src/kiri/mode/cam/prepare.js | 8 +++++++ src/kiri/mode/cam/slice.js | 46 ++++++++++++++++++++++++++++++++++++ web/kiri/index.css | 15 ++++++++++++ web/kiri/index.html | 1 + 9 files changed, 130 insertions(+) create mode 100644 src/kiri/mode/cam/op-loop.js diff --git a/src/kiri/app/inputs.js b/src/kiri/app/inputs.js index a2204af7..d87dd2f0 100644 --- a/src/kiri/app/inputs.js +++ b/src/kiri/app/inputs.js @@ -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); diff --git a/src/kiri/mode/cam/cl-ops.js b/src/kiri/mode/cam/cl-ops.js index 7849a713..199533b8 100644 --- a/src/kiri/mode/cam/cl-ops.js +++ b/src/kiri/mode/cam/cl-ops.js @@ -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'; } diff --git a/src/kiri/mode/cam/init-ui.js b/src/kiri/mode/cam/init-ui.js index 81a2ed94..19d1d7b4 100644 --- a/src/kiri/mode/cam/init-ui.js +++ b/src/kiri/mode/cam/init-ui.js @@ -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": diff --git a/src/kiri/mode/cam/op-loop.js b/src/kiri/mode/cam/op-loop.js new file mode 100644 index 00000000..06e16fe9 --- /dev/null +++ b/src/kiri/mode/cam/op-loop.js @@ -0,0 +1,17 @@ +/** Copyright Stewart Allen -- 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 + } +} diff --git a/src/kiri/mode/cam/ops.js b/src/kiri/mode/cam/ops.js index 79e4c7f0..3685fd1e 100644 --- a/src/kiri/mode/cam/ops.js +++ b/src/kiri/mode/cam/ops.js @@ -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, diff --git a/src/kiri/mode/cam/prepare.js b/src/kiri/mode/cam/prepare.js index 4973c239..a1d645c7 100644 --- a/src/kiri/mode/cam/prepare.js +++ b/src/kiri/mode/cam/prepare.js @@ -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; diff --git a/src/kiri/mode/cam/slice.js b/src/kiri/mode/cam/slice.js index 9cd12de4..2fcf67ad 100644 --- a/src/kiri/mode/cam/slice.js +++ b/src/kiri/mode/cam/slice.js @@ -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 === '|') { diff --git a/web/kiri/index.css b/web/kiri/index.css index 60b2a680..d3520e7a 100644 --- a/web/kiri/index.css +++ b/web/kiri/index.css @@ -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; } diff --git a/web/kiri/index.html b/web/kiri/index.html index c0890b03..795e25da 100644 --- a/web/kiri/index.html +++ b/web/kiri/index.html @@ -471,6 +471,7 @@
rough
outline
area
+
loop
From 0bdadd7294df14fdd47c6d36019dd2a3aad62988 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 2 Jan 2026 22:49:36 -0500 Subject: [PATCH 085/154] fix op loop expansion, rendering --- src/kiri/mode/cam/init-ui.js | 28 +++++++++------------- src/kiri/mode/cam/slice.js | 45 +++++++++++------------------------- 2 files changed, 25 insertions(+), 48 deletions(-) diff --git a/src/kiri/mode/cam/init-ui.js b/src/kiri/mode/cam/init-ui.js index 19d1d7b4..5dde25e7 100644 --- a/src/kiri/mode/cam/init-ui.js +++ b/src/kiri/mode/cam/init-ui.js @@ -95,7 +95,8 @@ function updateIndex() { if (op.absolute) { index = op.degrees } else { - index += op.degrees; + let numloop = op._numloop ?? 1; + index += (op.degrees * numloop); } } } @@ -288,6 +289,8 @@ export function opRender() { let bind = {}; let scale = api.view.unit_scale(); let notime = false; + let inloop = 0; + let numloop; oplist.forEach((rec, i) => { let title = ''; let clock = rec.type === '|'; @@ -300,22 +303,12 @@ export function opRender() { 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) { + inloop = rec.count; + numloop = rec.repeat; + } else if (inloop && !clock) { clazz.push('in-loop'); + rec._numloop = numloop; + inloop--; } let notable = rec.note ? rec.note.split(' ').filter(v => v.charAt(0) === '#') : undefined; @@ -375,7 +368,8 @@ export function opRender() { let popped = false; let poprec = env.popOp[rec.type]; if (type === 'index' && indexing && !rec.disabled) { - index = rec.absolute ? rec.degrees : index + rec.degrees; + let numloop = rec._numloop ?? 1; + index = rec.absolute ? rec.degrees : index + (rec.degrees * numloop); } el.rec = rec; el.unpop = () => { diff --git a/src/kiri/mode/cam/slice.js b/src/kiri/mode/cam/slice.js index 2fcf67ad..49a75492 100644 --- a/src/kiri/mode/cam/slice.js +++ b/src/kiri/mode/cam/slice.js @@ -309,6 +309,7 @@ export async function cam_slice(settings, widget, onupdate, ondone) { opList.push(new OPS.xray(state, { type: "xray" })); } + // find index ops let activeOps = proc.ops.filter(op => !op.disabled); if (isIndexed) { @@ -326,45 +327,27 @@ export async function cam_slice(settings, widget, onupdate, ondone) { // LOOP EXPANSION - duplicate next N operations M times let expandedOps = []; - let i = 0; - while (i < activeOps.length) { + for (let i=0; i= activeOps.length) { - return error(`Loop operation requires ${opCount} following operation(s), but only ${activeOps.length - i - 1} available`); + let { count, repeat } = op; + let remain = activeOps.slice(i+1).filter(op => op.type !== '|'); + if (remain < count) { + return error(`Loop operation requires ${count} following operation(s), but only ${remain.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"); + for (let r=1; r Date: Sat, 3 Jan 2026 00:44:50 -0500 Subject: [PATCH 086/154] add vertical wall clearing option for op-area like outline --- src/kiri/app/conf/defaults.js | 1 + src/kiri/mode/cam/cl-ops.js | 2 ++ src/kiri/mode/cam/op-area.js | 2 +- web/kiri/lang/en.js | 2 ++ 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/kiri/app/conf/defaults.js b/src/kiri/app/conf/defaults.js index dcc37957..0038ba8d 100644 --- a/src/kiri/app/conf/defaults.js +++ b/src/kiri/app/conf/defaults.js @@ -449,6 +449,7 @@ export const conf = { camAreaDogbones: false, camAreaRevbones: false, camAreaOutline: false, + camAreaWalls: false, camAreaZigZag: true, camContourAngle: 85, camContourBottom: false, diff --git a/src/kiri/mode/cam/cl-ops.js b/src/kiri/mode/cam/cl-ops.js index 199533b8..d3a35c67 100644 --- a/src/kiri/mode/cam/cl-ops.js +++ b/src/kiri/mode/cam/cl-ops.js @@ -705,6 +705,7 @@ export function createPopOps() { rate: 'camAreaSpeed', plunge: 'camAreaPlunge', expand: 'camAreaExpand', + walls: 'camAreaWalls', smooth: 'camAreaSmooth', follow: 'camAreaFollow', refine: 'camAreaRefine', @@ -730,6 +731,7 @@ export function createPopOps() { exp_end: UC.endExpand(), sep: UC.newBlank({ class: "pop-sep" }), exp: UC.newExpand("area modifiers", { }), + walls: UC.newBoolean(LANG.ca_wall_s, undefined, { title: LANG.ca_wall_l }), expand: UC.newInput(LANG.cp_xpnd_s, { title: LANG.cp_xpnd_l, convert: toFloat, units }), smooth: UC.newInput(LANG.cp_smoo_s, { title: LANG.cp_smoo_l, convert: toInt, xshow: isSurface }), follow: UC.newInput(LANG.cp_foll_s, { title: LANG.cp_foll_l, convert: toFloat }), diff --git a/src/kiri/mode/cam/op-area.js b/src/kiri/mode/cam/op-area.js index 35a6de58..f03c1bfd 100644 --- a/src/kiri/mode/cam/op-area.js +++ b/src/kiri/mode/cam/op-area.js @@ -186,7 +186,7 @@ class OpArea extends CamOp { let firstOff = -(toolDiam / 2 + (op.leave_xy ?? 0)); POLY.subtract([ area ], shadow, clip, undefined, undefined, 0); POLY.offset(clip, [ firstOff, -toolOver ], { - count: op.steps ?? 999, outs, flat: true, z: z - zMov, ...offopt + count: op.walls ? 1 : (op.steps ?? 999), outs, flat: true, z: z - zMov, ...offopt }); // if we see no offsets, re-check the mesh bottom Z then exit if (outs.length === 0) { diff --git a/web/kiri/lang/en.js b/web/kiri/lang/en.js index f5f12250..150fe28b 100644 --- a/web/kiri/lang/en.js +++ b/web/kiri/lang/en.js @@ -725,6 +725,8 @@ self.lang['en-us'] = { ca_altr_l: "alternate direction of travel lines (zigzag)", ca_sang_s: "step angle", ca_sang_l: "crossing angle on the XY plane for contour lines", + ca_wall_s: "walls", + ca_wall_l: ["perform a single pass along vertical faces. leave interior spaces. similar to outline operation"], // OUTPUT ou_menu: "output", From 130a1c498b7ac13e5fe010c238f6811bb1605810 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sat, 3 Jan 2026 00:58:22 -0500 Subject: [PATCH 087/154] fix ghost/wire/solid toggle states --- src/kiri/app/keyboard.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/kiri/app/keyboard.js b/src/kiri/app/keyboard.js index 22bb4be1..c68df9f6 100644 --- a/src/kiri/app/keyboard.js +++ b/src/kiri/app/keyboard.js @@ -262,6 +262,10 @@ class KeyboardControl { } } + let api = this.#api; + let isWireframe = api.local.getBoolean('model.wireframe'); + let isGhost = api.local.getFloat('model.wireframe.opacity') < 1; + switch (evt.keyCode) { case this.#cca('`'): this.#api.show.slices(0); break; case this.#cca('0'): { @@ -398,10 +402,18 @@ class KeyboardControl { } break; case this.#cca('w'): // set ghost - $('render-ghost').onclick(); + if (isGhost && !isWireframe) { + $('render-solid').onclick(); + } else { + $('render-ghost').onclick(); + } break; case this.#cca('W'): // set wireframe - $('render-wire').onclick(); + if (isWireframe) { + $('render-solid').onclick(); + } else { + $('render-wire').onclick(); + } break; default: this.#onUnhandledKey(evt); From 8adbc38990641ed60088701614feec3089231bf9 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sat, 3 Jan 2026 01:16:54 -0500 Subject: [PATCH 088/154] hide loop op unless indexing enabled --- src/kiri/mode/cam/init-ui.js | 1 + web/kiri/index.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/kiri/mode/cam/init-ui.js b/src/kiri/mode/cam/init-ui.js index 5dde25e7..c846141a 100644 --- a/src/kiri/mode/cam/init-ui.js +++ b/src/kiri/mode/cam/init-ui.js @@ -125,6 +125,7 @@ function updateAxisMode(refresh) { const showNonIndexed = env.isIndexed ? 'none' : ''; $('cam-index').style.display = showIndexed; $('cam-lathe').style.display = showIndexed; + $('cam-loop').style.display = showIndexed; $('cam-flip').style.display = showNonIndexed; $('cam-reg').style.display = showNonIndexed; if (!changed) { diff --git a/web/kiri/index.html b/web/kiri/index.html index 795e25da..7291bdb1 100644 --- a/web/kiri/index.html +++ b/web/kiri/index.html @@ -471,7 +471,7 @@
rough
outline
area
-
loop
+
loop
From 14593b6267669d9946b338c6a3bfb4ff6276c353 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sat, 3 Jan 2026 01:44:48 -0500 Subject: [PATCH 089/154] disable loop and looped ops when switching to non-indexed mode --- src/kiri/mode/cam/init-ui.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/kiri/mode/cam/init-ui.js b/src/kiri/mode/cam/init-ui.js index c846141a..aa667ec5 100644 --- a/src/kiri/mode/cam/init-ui.js +++ b/src/kiri/mode/cam/init-ui.js @@ -356,6 +356,9 @@ export function opRender() { if (!env.isIndexed && type === 'lathe') { rec.disabled = true; } + if (!env.isIndexed && (type === 'loop' || rec._numloop)) { + rec.disabled = true; + } if (!hasSharedArrays && (type === 'contour' || type === 'lathe')) { rec.disabled = true; } From 87e4a32092570ac7db95d342a763c4c0af55bf58 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sat, 3 Jan 2026 11:42:37 -0500 Subject: [PATCH 090/154] add global slicePre similar to sliceAll (which should be renamed slicePost) add shared cam operational state so index+stock can run once for all instances --- src/kiri/app/function.js | 9 +++++++-- src/kiri/app/workers.js | 7 +++++++ src/kiri/mode/cam/driver-be.js | 3 ++- src/kiri/mode/cam/op-level.js | 24 +++++++++++++++++++++--- src/kiri/mode/cam/slice.js | 28 +++++++++++++++++++++------- src/kiri/run/worker.js | 12 ++++++++++++ 6 files changed, 70 insertions(+), 13 deletions(-) diff --git a/src/kiri/app/function.js b/src/kiri/app/function.js index a5f027fc..0b3cb9e8 100644 --- a/src/kiri/app/function.js +++ b/src/kiri/app/function.js @@ -137,6 +137,10 @@ function prepareSlices(callback, scale = 1, offset = 0) { client.rotate(settings); } + function sliceSetup() { + client.slicePre(settings, sliceNext); + } + function sliceNext() { if (toSlice.length) { // while more widgets to slice @@ -309,8 +313,9 @@ function prepareSlices(callback, scale = 1, offset = 0) { space.refresh(); } - // kick off slicing chain - sliceNext(); + // do any global setup / state management + // then kick off slicing chain + sliceSetup(); } /** diff --git a/src/kiri/app/workers.js b/src/kiri/app/workers.js index c508d10e..06503bdb 100644 --- a/src/kiri/app/workers.js +++ b/src/kiri/app/workers.js @@ -211,6 +211,13 @@ export const client = { }); }, + + // called once at the beginning of slice operations + // setup any global shared state + slicePre(settings, callback) { + send("slicePre", { settings }, callback); + }, + // called once for each widget slice(settings, widget, callback) { send("slice", { diff --git a/src/kiri/mode/cam/driver-be.js b/src/kiri/mode/cam/driver-be.js index 2212908e..35a2a9e4 100644 --- a/src/kiri/mode/cam/driver-be.js +++ b/src/kiri/mode/cam/driver-be.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { codec } from '../../core/codec.js'; -import { cam_slice, holes, traces } from './slice.js'; +import { cam_slice_pre, cam_slice, holes, traces } from './slice.js'; import { cam_prepare } from './prepare.js'; import { cam_export } from './export.js'; import { tool as mesh_tool } from '../../../mesh/tool.js'; @@ -114,6 +114,7 @@ export const CAM = { init, surface_prep, surface_find, + slicePre: cam_slice_pre, slice: cam_slice, prepare: cam_prepare, export: cam_export diff --git a/src/kiri/mode/cam/op-level.js b/src/kiri/mode/cam/op-level.js index 39260ba6..2ef30a70 100644 --- a/src/kiri/mode/cam/op-level.js +++ b/src/kiri/mode/cam/op-level.js @@ -14,9 +14,11 @@ class OpLevel extends CamOp { async slice(progress) { let { op, state } = this; - let { addSlices, color, settings, shadow, updateToolDiams, zMax, ztOff } = state; + let { addSlices, color, settings, shadow } = state; + let { share, updateToolDiams, zMax, ztOff } = state; let { down, tool, step, stepz, inset } = op; let { stock } = settings; + let { center } = stock; let toolDiam = new Tool(settings, tool).fluteDiameter(); let stepOver = this.stepOver = toolDiam * step; @@ -25,11 +27,23 @@ class OpLevel extends CamOp { let zBot = zTop - down; let zList = stepz && down ? util.lerp(zTop, zBot, stepz) : [ zBot ]; + if (share.ran) { + console.log('skip'); + this.skip = true; + return; + } else if (op.stock) { + share.ran = true; + } + updateToolDiams(toolDiam); let points = []; let clear = op.stock ? - [ newPolygon().centerRectangle({x:-wpos.x,y:-wpos.y,z:wpos.z}, stock.x + toolDiam/2, stock.y) ] : + [ newPolygon().centerRectangle({ + x: -wpos.x + center.x, + y: -wpos.y + center.y, + z: wpos.z + center.z + }, stock.x + toolDiam/2, stock.y) ] : POLY.outer(POLY.offset(shadow.base, toolDiam * (inset || 0))); POLY.fillArea(clear, 1090, stepOver, points); @@ -50,10 +64,14 @@ class OpLevel extends CamOp { } prepare(ops, progress) { - let { layers, stepOver } = this; + let { layers, skip, stepOver } = this; let { printPoint } = ops; let { newLayer, tip2tipEmit, camOut } = ops; + if (skip) { + return; + } + for (let lines of layers) { lines = lines.map(p => { return { first: p.first(), last: p.last(), poly: p } }); printPoint = tip2tipEmit(lines, printPoint, (el, point, count) => { diff --git a/src/kiri/mode/cam/slice.js b/src/kiri/mode/cam/slice.js index 49a75492..8007959a 100644 --- a/src/kiri/mode/cam/slice.js +++ b/src/kiri/mode/cam/slice.js @@ -11,6 +11,13 @@ import { Slicer as cam_slicer } from './slicer_cam.js'; import { Tool } from './tool.js'; import { util } from '../../../geo/base.js'; +// state shared across all widget slicing +let shared; + +export function cam_slice_pre(settings) { + shared = { ops: { } }; +} + /** * DRIVER SLICE CONTRACT * @@ -306,7 +313,7 @@ export async function cam_slice(settings, widget, onupdate, ondone) { } if (false) { - opList.push(new OPS.xray(state, { type: "xray" })); + opList.push({ op: new OPS.xray(state, { type: "xray" }) }); } // find index ops @@ -315,14 +322,14 @@ export async function cam_slice(settings, widget, onupdate, ondone) { if (isIndexed) { // preface op list with OpIndex if (activeOps.length === 0 || activeOps[0].type !== 'index') { - opList.push(new OPS.index(state, { type: "index", degrees: 0, absolute: true })); - opTot += opList.peek().weight(); + opList.push({ op: new OPS.index(state, { type: "index", degrees: 0, absolute: true }) }); + opTot += opList.peek().op.weight(); } } else { // preface op list with OpShadow - opList.push(new OPS.shadow(state, { type: "shadow", silent: true })); - opTot += opList.peek().weight(); + opList.push({ op: new OPS.shadow(state, { type: "shadow", silent: true }) }); + opTot += opList.peek().op.weight(); } // LOOP EXPANSION - duplicate next N operations M times @@ -354,6 +361,7 @@ export async function cam_slice(settings, widget, onupdate, ondone) { activeOps = expandedOps; // determine # of steps and step weighting for progress bar + let opos = 0; for (let op of activeOps) { if (op.type === '|') { break; @@ -361,15 +369,20 @@ export async function cam_slice(settings, widget, onupdate, ondone) { let opfn = OPS[op.type]; if (opfn) { let opin = new opfn(state, op); - opList.push(opin); opTot += opin.weight(); + opos++; + let share = shared.ops[opos]; + if (!share) { + share = shared.ops[opos] = {}; + } + opList.push({ op: opin, share }); } } // call slice() function on all ops in order await setAxisIndex(); updateSlicer(); - for (let op of opList) { + for (let { op, share } of opList) { let weight = op.weight(); // apply operation override vars let workover = var_compute(); @@ -392,6 +405,7 @@ export async function cam_slice(settings, widget, onupdate, ondone) { let layername = named.length ? named : (note ? `${type} (${note})` : type); Object.assign(state, { layername, + share, stock, tool, zBottom, diff --git a/src/kiri/run/worker.js b/src/kiri/run/worker.js index 84d4f339..e5a5f974 100644 --- a/src/kiri/run/worker.js +++ b/src/kiri/run/worker.js @@ -420,6 +420,18 @@ const dispatch = { send.done({}); }, + slicePre(data, send) { + const { settings } = data; + const { mode } = settings; + const driver = drivers[mode]; + + if (driver.slicePre) { + driver.slicePre(settings); + } + + send.done(); + }, + slice(data, send) { send.data({ update:0.001, updateStatus:"slicing" }); From de58d4ddabc0368f144a68c18a7e793f8f858d3b Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sat, 3 Jan 2026 12:36:21 -0500 Subject: [PATCH 091/154] fix area surfacing progress reporting --- src/kiri/mode/cam/op-area.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/kiri/mode/cam/op-area.js b/src/kiri/mode/cam/op-area.js index f03c1bfd..ed80252e 100644 --- a/src/kiri/mode/cam/op-area.js +++ b/src/kiri/mode/cam/op-area.js @@ -395,7 +395,7 @@ class OpArea extends CamOp { paths, step: toolOver / 2, zFloor: zBottom - 1, - onProgress(pct) { console.log({ pct }); onupdate(pct/100, 100) } + onProgress: pct => progress(proc + (pinc * (pct/100))) }); raster.terminate(); @@ -414,6 +414,9 @@ class OpArea extends CamOp { // output this surface surfaces.push(surface); + + proc += pinc; + progress(proc); } } // filter out empty slices From d6e68c23488fed10773ff1ec6f1a4cc184bba45a Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sat, 3 Jan 2026 12:39:02 -0500 Subject: [PATCH 092/154] update raster-path dependency --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3dec9879..7691eeb8 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@gridspace/app-server": "^0.0.20", "@gridspace/basic-ftp": "github:gridspace/basic-ftp#v5.0.5-gridspace", "@gridspace/net-level-client": "^0.2.3", - "@gridspace/raster-path": "^1.0.9", + "@gridspace/raster-path": "^1.1.0", "@tracespace/parser": "^5.0.0-next.0", "@tweenjs/tween.js": "^16.6.0", "aedes": "^0.51.3", From dda51078109b4dbace05a27b7035a6dd9b90ad7e Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sat, 3 Jan 2026 16:28:18 -0500 Subject: [PATCH 093/154] drop redundant or unused index operations --- src/kiri/mode/cam/op-index.js | 20 ++++++++++++++++++-- src/kiri/mode/cam/op-rough.js | 19 ++++++++++++++----- src/kiri/mode/cam/op-shadow.js | 3 +-- src/kiri/mode/cam/slice.js | 23 +++++++++++++++++++++-- 4 files changed, 54 insertions(+), 11 deletions(-) diff --git a/src/kiri/mode/cam/op-index.js b/src/kiri/mode/cam/op-index.js index 21ad46a4..46e04025 100644 --- a/src/kiri/mode/cam/op-index.js +++ b/src/kiri/mode/cam/op-index.js @@ -13,14 +13,30 @@ export class OpIndex extends CamOp { async slice() { let { op, state } = this; + if (!state.isIndexed) { throw 'index op requires indexed stock'; } - let { widget, updateSlicer, computeShadows, setAxisIndex } = state; - this.degrees = await setAxisIndex(op.degrees, op.absolute); + + let { lastAxisIndex, widget, updateSlicer, computeShadows, setAxisIndex } = state; + let { degrees, absolute } = op; + + if (absolute && lastAxisIndex === degrees) { + return console.log('skip redundant absolute index'); + } else if (!absolute && degrees === 0) { + return console.log('skip redundant relative index'); + } + + this.degrees = await setAxisIndex(degrees, absolute); + state.lastAxisIndex = this.degrees; + // force recompute of topo widget.topo = undefined; + + // update slicer for new widget geometry updateSlicer(); + + // recompute shadow from new widget geometry await computeShadows(); } diff --git a/src/kiri/mode/cam/op-rough.js b/src/kiri/mode/cam/op-rough.js index a4ce6c6f..8a1800bc 100644 --- a/src/kiri/mode/cam/op-rough.js +++ b/src/kiri/mode/cam/op-rough.js @@ -102,15 +102,24 @@ class OpRough extends CamOp { })); } - for (let op of this.ops_list) { - await op.slice(progress); + let len = ops_list.length; + let per = 1 / len; + let bas = 0; + for (let op of ops_list) { + await op.slice(pct => progress(bas + pct * per)); + bas += per; } } -s + async prepare(ops, progress) { + let { ops_list } = this; let { setChangeOp } = ops; - for (let op of this.ops_list) { - await op.prepare(ops, progress); + let len = ops_list.length; + let per = 1 / len; + let bas = 0; + for (let op of ops_list) { + await op.prepare(ops, pct => progress(bas + pct * per)); + bas += per; setChangeOp(); } } diff --git a/src/kiri/mode/cam/op-shadow.js b/src/kiri/mode/cam/op-shadow.js index 5891e7b9..d06ce3ab 100644 --- a/src/kiri/mode/cam/op-shadow.js +++ b/src/kiri/mode/cam/op-shadow.js @@ -27,6 +27,7 @@ class OpShadow extends CamOp { async slice(progress) { let state = this.state; + let { addSlices, settings, shadowAt, unsafe, widget, workarea } = state; let { devel } = settings.controller; @@ -36,12 +37,10 @@ class OpShadow extends CamOp { let skipTerrain = unsafe; let terrain = []; let tzindex = []; - let { bottom_z, top_z } = workarea; let minZ = Math.floor(bounds.min.z); let maxZ = Math.floor(bounds.max.z); for (let z = maxZ; z >= minZ; z--) { - // if (z <= top_z && z >= bottom_z) tzindex.push(z); } diff --git a/src/kiri/mode/cam/slice.js b/src/kiri/mode/cam/slice.js index 8007959a..943bc300 100644 --- a/src/kiri/mode/cam/slice.js +++ b/src/kiri/mode/cam/slice.js @@ -55,7 +55,8 @@ export async function cam_slice(settings, widget, onupdate, ondone) { let proc = settings.process, camOps = widget.camops = [], sliceAll = widget.slices = [], - isIndexed = proc.camStockIndexed; + isIndexed = proc.camStockIndexed, + lastTime = Date.now(); let axisRotation, axisIndex, bounds, dark, color, stock, tabs, track, tool, unsafe, units, workarea, @@ -241,6 +242,17 @@ export async function cam_slice(settings, widget, onupdate, ondone) { } } + function updateProgress(percent, message) { + const mark = Date.now(); + if (false) console.log({ + time: ((mark - lastTime)/1000).toFixed(2), + pct: ((percent ?? 0) * 100).toFixed(2), + msg: message + }); + onupdate(percent, message); + lastTime = mark; + } + function newSlicer(opts) { return new cam_slicer(widget, opts); } @@ -250,9 +262,11 @@ export async function cam_slice(settings, widget, onupdate, ondone) { } async function computeShadows() { + console.time('computeShadows'); await new OPS.shadow(state, { type: "shadow", silent: true }).slice(progress => { // console.log('reshadowing', progress.round(3)); }); + console.timeEnd('computeShadows'); } function setToolDiam(toolDiam) { @@ -379,6 +393,11 @@ export async function cam_slice(settings, widget, onupdate, ondone) { } } + // drop last op if it's type index + if (opList.peek().op.op.type === 'index') { + opList.pop(); + } + // call slice() function on all ops in order await setAxisIndex(); updateSlicer(); @@ -416,7 +435,7 @@ export async function cam_slice(settings, widget, onupdate, ondone) { }); let operr; await op.slice((progress, message) => { - onupdate((opSum + (progress * weight)) / opTot, message || op.type()); + updateProgress((opSum + (progress * weight)) / opTot, message || op.type()); }).catch(e => { operr = e; console.trace(e); From f81e00276653f488576b639efcc3ad214b471d41 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sat, 3 Jan 2026 19:43:51 -0500 Subject: [PATCH 094/154] parallelize widget shadow generation 2x faster --- src/kiri/core/widget.js | 53 ++++++++++++++++++++++++++++++---- src/kiri/mode/cam/op-index.js | 6 ++-- src/kiri/mode/cam/op-shadow.js | 7 +++-- src/kiri/mode/cam/slice.js | 10 +++---- src/kiri/run/minion.js | 14 +++++++++ 5 files changed, 74 insertions(+), 16 deletions(-) diff --git a/src/kiri/core/widget.js b/src/kiri/core/widget.js index 33adf667..90ea0625 100644 --- a/src/kiri/core/widget.js +++ b/src/kiri/core/widget.js @@ -1,5 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ +import { decode } from '../core/codec.js'; import { util as mesh_util } from '../../mesh/util.js'; import { tool as mesh_tool } from '../../mesh/tool.js'; import { verticesToPoints } from '../../geo/points.js'; @@ -12,6 +13,7 @@ const sharedArrayClass = self.SharedArrayBuffer || undefined; const hasSharedArrays = sharedArrayClass ? true : false; const solid_opacity = 1.0; const groups = []; +const debug_shadow = false; let nextId = 0; @@ -739,8 +741,9 @@ class Widget { // the stacks are then used to produce shadowlines #ensureShadowCache() { if (this.cache.shadow) { - return; + return this.cache.shadow; } + if (debug_shadow) console.time('shadow buckets'); const geo = this.getGeoVertices({ unroll: true, translate: true }); const length = geo.length; const bounds = this.getBoundingBox(); @@ -762,15 +765,54 @@ class Widget { stack[z].push(a, b, c); } } - this.cache.shadow = stack; + if (debug_shadow) console.timeEnd('shadow buckets'); + return this.cache.shadow = stack; + } + + async computeShadowStack(zlist, progress) { + let shadow_stack = this.cache.shadow_stack; + if (!shadow_stack) { + shadow_stack = this.cache.shadow_stack = {}; + } + let work = self.kiri_worker; + let stack = this.#ensureShadowCache(); + let plist = []; + if (debug_shadow) console.time('seed minion buckets'); + work.minions.broadcast('cam_shadow_stack', stack); + if (debug_shadow) console.timeEnd('seed minion buckets'); + let pinc = 1 / zlist.length; + let pval = 0; + for (let z of zlist) { + let p = work.minions.queueAsync({ + cmd: 'cam_shadow_z', + z: z - 0.005, + t: z + 1 + }).then(reply => { + shadow_stack[z - 0.005] = decode(reply.data); + pval += pinc; + progress(pval); + }); + plist.push(p); + } + await Promise.all(plist); + work.minions.broadcast('cam_shadow_stack', { clear: true }); + } + + computeShadowAtZ(z, ztop, cached) { + return this.#computeShadowAt(z, ztop, cached); } // union triangles > z (opt cap < ztop) into polygon(s) // slice the triangle stack matchingg z then union the results - #computeShadowAt(z, ztop) { - this.#ensureShadowCache(); + #computeShadowAt(z, ztop, cached) { + let shadow_stack = this.cache.shadow_stack; + if (shadow_stack && shadow_stack[z]) { + return shadow_stack[z]; + } + let label = `compute shadow ${z.round(2)}`; + if (debug_shadow) console.time(label); const found = []; - const stack = this.cache.shadow; + const stack = cached ?? this.#ensureShadowCache(); let minZ = Math.floor(z); let maxZ = Math.ceil(z); let slices = []; @@ -832,6 +874,7 @@ class Widget { polys = POLY.offset(polys, 0.01); polys = POLY.offset(polys, -0.01); + if (debug_shadow) console.timeEnd(label); return polys; } } diff --git a/src/kiri/mode/cam/op-index.js b/src/kiri/mode/cam/op-index.js index 46e04025..9714a0a8 100644 --- a/src/kiri/mode/cam/op-index.js +++ b/src/kiri/mode/cam/op-index.js @@ -8,10 +8,10 @@ export class OpIndex extends CamOp { } weight() { - return 0.1; + return 3; } - async slice() { + async slice(progress) { let { op, state } = this; if (!state.isIndexed) { @@ -37,7 +37,7 @@ export class OpIndex extends CamOp { updateSlicer(); // recompute shadow from new widget geometry - await computeShadows(); + await computeShadows(progress); } prepare(ops, progress) { diff --git a/src/kiri/mode/cam/op-shadow.js b/src/kiri/mode/cam/op-shadow.js index d06ce3ab..349b0c4c 100644 --- a/src/kiri/mode/cam/op-shadow.js +++ b/src/kiri/mode/cam/op-shadow.js @@ -22,7 +22,7 @@ class OpShadow extends CamOp { } weight() { - return 0.1; + return 3; } async slice(progress) { @@ -50,6 +50,9 @@ class OpShadow extends CamOp { tzindex = [ ]; } + // distributed pre-fill shadow layer cache + await widget.computeShadowStack(tzindex, prog => progress(prog / 2, 'shadow')); + // terrain is the "shadow stack" where index 0 = top of part // thus array.length -1 = bottom of part for (let i=0; i { - // console.log('reshadowing', progress.round(3)); - }); - console.timeEnd('computeShadows'); + async function computeShadows(progress) { + console.time('compute shadow'); + await new OPS.shadow(state, { type: "shadow", xsilent: true }).slice(progress); + console.timeEnd('compute shadow'); } function setToolDiam(toolDiam) { diff --git a/src/kiri/run/minion.js b/src/kiri/run/minion.js index d8436f87..5186e6ee 100644 --- a/src/kiri/run/minion.js +++ b/src/kiri/run/minion.js @@ -10,6 +10,7 @@ import { base } from '../../geo/base.js'; import { codec, encode, encodePointArray } from '../core/codec.js'; import { doTopShells } from '../mode/fdm/post.js'; import { newPoint } from '../../geo/point.js'; +import { newWidget } from '../core/widget.js'; import { polygons as POLY } from '../../geo/polygons.js'; import { sliceZ, sliceConnect } from '../../geo/slicer.js'; import { Slicer as cam_slicer } from '../mode/cam/slicer_cam.js'; @@ -317,5 +318,18 @@ const funcs = self.minion = { const topo4 = Object.assign(new Topo4(), cache.lathe); const heights = topo4.lathePath(stmp, tool); reply({ seq, heights }); + }, + + // cam widget shadow + + cam_shadow_stack(data, seq) { + cache.shadow_stack = data.clear ? undefined : data; + }, + + cam_shadow_z(data, seq) { + let polys = newWidget().computeShadowAtZ(data.z, data.t, cache.shadow_stack); + let coded = codec.encode(polys); + reply({ seq, data: coded }); } + }; From 68ff3391ce1731c97eceeb47a327f82f9734534c Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sat, 3 Jan 2026 20:01:23 -0500 Subject: [PATCH 095/154] fix shadow and lathe progress reporting --- src/kiri/mode/cam/op-index.js | 4 ---- src/kiri/mode/cam/op-shadow.js | 6 +----- src/kiri/mode/cam/topo4.js | 4 ++-- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/kiri/mode/cam/op-index.js b/src/kiri/mode/cam/op-index.js index 9714a0a8..a56555b8 100644 --- a/src/kiri/mode/cam/op-index.js +++ b/src/kiri/mode/cam/op-index.js @@ -7,10 +7,6 @@ export class OpIndex extends CamOp { super(state, op); } - weight() { - return 3; - } - async slice(progress) { let { op, state } = this; diff --git a/src/kiri/mode/cam/op-shadow.js b/src/kiri/mode/cam/op-shadow.js index 349b0c4c..e0289a11 100644 --- a/src/kiri/mode/cam/op-shadow.js +++ b/src/kiri/mode/cam/op-shadow.js @@ -21,10 +21,6 @@ class OpShadow extends CamOp { super(state, op); } - weight() { - return 3; - } - async slice(progress) { let state = this.state; @@ -68,7 +64,7 @@ class OpShadow extends CamOp { .setLayer("shadow", {line: 0x888800, thin: true }) .addPolys(shadow, { thin: true }); } - progress(0.5 + i / tzindex.length, 'shadow'); + progress(0.5 + (i / tzindex.length) * 0.5, 'shadow'); } if (devel && slices.length) { diff --git a/src/kiri/mode/cam/topo4.js b/src/kiri/mode/cam/topo4.js index 9d8d4468..f78ec3ec 100644 --- a/src/kiri/mode/cam/topo4.js +++ b/src/kiri/mode/cam/topo4.js @@ -203,13 +203,13 @@ export class Topo { await gpu.loadTerrain({ triangles: vertices, boundsOverride, - onProgress(pct) { onupdate(pct/100) } + onProgress: (pct) => onupdate(pct / 1000) }); let output = await gpu.generateToolpaths({ xStep, yStep: 1, zFloor: zBottom, - onProgress(i,j) { onupdate(i/j) } + onProgress: (i,j) => onupdate(0.1 + i / j) }); gpu.terminate(); From 1193c3313e155d9519553f630b4954523499e2df Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sun, 4 Jan 2026 11:24:26 -0500 Subject: [PATCH 096/154] fix op-area rotated vs unrotated area input (auto vs user) --- src/kiri/mode/cam/op-area.js | 4 +++- src/kiri/mode/cam/op-outline.js | 2 ++ src/kiri/mode/cam/op-rough.js | 3 +++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/kiri/mode/cam/op-area.js b/src/kiri/mode/cam/op-area.js index ed80252e..ee274dd0 100644 --- a/src/kiri/mode/cam/op-area.js +++ b/src/kiri/mode/cam/op-area.js @@ -76,7 +76,9 @@ class OpArea extends CamOp { let poly = newPolygon().fromArray(arr); aminz = Math.min(aminz, poly.minZ()); polys.push(poly); - if (axisIndex) { + // user-selected traces are not rotated + // those based on shadow (outline, rough) are + if (!op.rotated && axisIndex) { // traces come from unrotatead widget edges poly.applyRotations(axisIndex); } diff --git a/src/kiri/mode/cam/op-outline.js b/src/kiri/mode/cam/op-outline.js index 1d995719..19c17fb2 100644 --- a/src/kiri/mode/cam/op-outline.js +++ b/src/kiri/mode/cam/op-outline.js @@ -39,6 +39,7 @@ class OpOutline extends CamOp { rate, rename: rename ?? "outline", revbones, + rotated: true, smooth: 0, spindle, steps: op.wide ? op.steps + 1 : 1, @@ -66,6 +67,7 @@ class OpOutline extends CamOp { rate, rename: rename ?? "outline", revbones, + rotated: true, smooth: 0, spindle, surfaces: {}, diff --git a/src/kiri/mode/cam/op-rough.js b/src/kiri/mode/cam/op-rough.js index 8a1800bc..153a206c 100644 --- a/src/kiri/mode/cam/op-rough.js +++ b/src/kiri/mode/cam/op-rough.js @@ -48,6 +48,7 @@ class OpRough extends CamOp { leave_z: op.leavez, ov_botz: op.ov_botz, ov_topz: op.ov_topz, + rotated: true, areas: { [widget.id]: areas.map(p => p.toArray()) }, surfaces: {} })); @@ -72,6 +73,7 @@ class OpRough extends CamOp { leave_z: op.leavez, ov_botz: op.ov_botz, ov_topz: op.ov_topz, + rotated: true, areas: { [widget.id]: areas.map(p => p.toArray()) }, surfaces: {}, flats: Object.keys(slicer.zFlat).map(v => parseFloat(v)).sort((a,b) => b-a), @@ -96,6 +98,7 @@ class OpRough extends CamOp { outline: !op.omitthru, ov_botz: op.ov_botz, ov_topz: op.ov_topz, + rotated: true, areas: { [widget.id]: areas.map(p => p.toArray()) }, surfaces: {}, thru: true From ca4ae077b2aec9f1fe4a7ea14c4de42258f7fa3f Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sun, 4 Jan 2026 14:53:14 -0500 Subject: [PATCH 097/154] add visual selection of z top and bottom --- src/kiri/app/api.js | 2 +- src/kiri/app/inputs.js | 8 +++ src/kiri/mode/cam/cl-ops.js | 119 ++++++++++++++++++++------------ src/kiri/mode/cam/cl-stock.js | 121 ++++++++++++++++++--------------- src/kiri/mode/cam/cl-zplane.js | 33 +++++++++ src/kiri/mode/cam/init-ui.js | 82 +++++++++++++++------- web/kiri/index.css | 5 ++ 7 files changed, 245 insertions(+), 125 deletions(-) create mode 100644 src/kiri/mode/cam/cl-zplane.js diff --git a/src/kiri/app/api.js b/src/kiri/app/api.js index f131c0ed..2d9c8ccd 100644 --- a/src/kiri/app/api.js +++ b/src/kiri/app/api.js @@ -166,7 +166,7 @@ export const api = { emit(t,m,o) { return EVENT.publish(t,m,o) }, import() { api.ui.load.click() }, listeners(topic) { return EVENT.targets(topic) }, - on(t,l) { return EVENT.on(t,l) }, + on(t,l) { EVENT.on(t,l); return api.event }, settings: settingsUI.trigger_event, topics() { return EVENT.topics() } }, diff --git a/src/kiri/app/inputs.js b/src/kiri/app/inputs.js index d87dd2f0..883ffdde 100644 --- a/src/kiri/app/inputs.js +++ b/src/kiri/app/inputs.js @@ -735,6 +735,14 @@ function newInput(label, opt = {}) { ip = height > 1 ? DOC.createElement('textarea') : DOC.createElement('input'); row.appendChild(newLabel(label)); + if (opt.selector) { + let sel = DOC.createElement('button'); + sel.classList.add('z-select'); + sel.addEventListener('click', event => opt.selector('click', { input: ip, event, opt })); + sel.addEventListener('mouseenter', event => opt.selector('enter', { input: ip, event, opt })); + sel.addEventListener('mouseleave', event => opt.selector('leave', { input: ip, event, opt })); + row.appendChild(sel); + } row.appendChild(ip); row.setAttribute("class", opt.class || "var-row"); if (height > 1) { diff --git a/src/kiri/mode/cam/cl-ops.js b/src/kiri/mode/cam/cl-ops.js index d3a35c67..c98541a3 100644 --- a/src/kiri/mode/cam/cl-ops.js +++ b/src/kiri/mode/cam/cl-ops.js @@ -203,6 +203,70 @@ export function createPopOp(type, map) { } export function createPopOps() { + + const bottom = true; + const open = true; + const top = true; + + function selector(type, { input, event, opt }) { + let which = opt?.top ? 'top' : opt?.bottom ? 'bottom' : 'unknown'; + let value = parseFloat(input.value); + switch (type) { + case 'click': + let rec = env.poppedRec; + let { field } = opt; + api.event.emit('cam.zplane.select', { which, onselect(value) { + rec[field] = value; + input.focus(); + input.value = value; + input.blur(); + } } ); + break; + case 'enter': + api.event.emit('cam.zplane.show', { which, value }); + break; + case 'leave': + api.event.emit('cam.zplane.hide', { which }); + break; + } + } + + function isClear() { + return env.poppedRec.mode === 'clear'; + } + + function isTrace() { + return env.poppedRec.mode === 'trace'; + } + + function isShadow() { + return env.poppedRec.shadow; + } + + function isSurface() { + return env.poppedRec.mode === 'surface'; + } + + function isSurfaceLinear() { + return env.poppedRec.mode === 'surface' && env.poppedRec.sr_type === 'linear'; + } + + function canDogBones() { + if (!env.poppedRec) return false; + return env.poppedRec.mode === 'follow'; + } + + function canDogBonesRev() { + return canDogBones() && env.poppedRec.dogbone; + } + + function zDogSep() { + return canDogBones() || zBottom(); + } + + const ov_topz = { title: LANG.ou_ztop_l, convert: toFloat, units, selector, top, field: "ov_topz" }; + const ov_botz = { title: LANG.ou_zbot_l, convert: toFloat, units, selector, bottom, field: "ov_botz" }; + createPopOp('level', { tool: 'camLevelTool', spindle: 'camLevelSpindle', @@ -260,8 +324,8 @@ export function createPopOps() { omitthru: UC.newBoolean(LANG.co_omit_s, undefined, { title: LANG.co_omit_l }), sep: UC.newBlank({ class: "pop-sep" }), exp: UC.newExpand("overrides"), - ov_topz: UC.newInput(LANG.ou_ztop_s, { title: LANG.ou_ztop_l, convert: toFloat, units }), - ov_botz: UC.newInput(LANG.ou_zbot_s, { title: LANG.ou_zbot_l, convert: toFloat, units }), + ov_topz: UC.newInput(LANG.ou_ztop_s, ov_topz), + ov_botz: UC.newInput(LANG.ou_zbot_s, ov_botz), exp_end: UC.endExpand(), }; @@ -304,8 +368,8 @@ export function createPopOps() { revbones: UC.newBoolean(LANG.co_dogr_s, undefined, { title: LANG.co_dogr_l, show: () => env.poppedRec.dogbones }), sep: UC.newBlank({ class: "pop-sep" }), exp: UC.newExpand("overrides"), - ov_topz: UC.newInput(LANG.ou_ztop_s, { title: LANG.ou_ztop_l, convert: toFloat, units }), - ov_botz: UC.newInput(LANG.ou_zbot_s, { title: LANG.ou_zbot_l, convert: toFloat, units }), + ov_topz: UC.newInput(LANG.ou_ztop_s, ov_topz), + ov_botz: UC.newInput(LANG.ou_zbot_s, ov_botz), exp_end: UC.endExpand(), }; @@ -386,19 +450,6 @@ export function createPopOps() { linear: UC.newBoolean(LANG.ci_line_s, undefined, { title: LANG.ci_line_l }), }; - function canDogBones() { - if (!env.poppedRec) return false; - return env.poppedRec.mode === 'follow'; - } - - function canDogBonesRev() { - return canDogBones() && env.poppedRec.dogbone; - } - - function zDogSep() { - return canDogBones() || zBottom(); - } - createPopOp('trace', { mode: 'camTraceType', offset: 'camTraceOffset', @@ -437,8 +488,8 @@ export function createPopOps() { revbone: UC.newBoolean(LANG.co_dogr_s, undefined, { title: LANG.co_dogr_l, show: canDogBonesRev }), exp: UC.newExpand("overrides"), sep: UC.newBlank({ class: "pop-sep" }), - ov_topz: UC.newInput(LANG.ou_ztop_s, { title: LANG.ou_ztop_l, convert: toFloat, units }), - ov_botz: UC.newInput(LANG.ou_zbot_s, { title: LANG.ou_zbot_l, convert: toFloat, units }), + ov_topz: UC.newInput(LANG.ou_ztop_s, ov_topz), + ov_botz: UC.newInput(LANG.ou_zbot_s, ov_botz), exp_end: UC.endExpand(), sep: UC.newBlank({ class: "pop-sep" }), menu: UC.newRow([UC.newButton("select", traceAdd)], { class: "ext-buttons f-row" }), @@ -483,8 +534,8 @@ export function createPopOps() { outline: UC.newBoolean(LANG.cp_outl_s, undefined, { title: LANG.cp_outl_l }), exp: UC.newExpand("overrides"), sep: UC.newBlank({ class: "pop-sep" }), - ov_topz: UC.newInput(LANG.ou_ztop_s, { title: LANG.ou_ztop_l, convert: toFloat, units }), - ov_botz: UC.newInput(LANG.ou_zbot_s, { title: LANG.ou_zbot_l, convert: toFloat, units }), + ov_topz: UC.newInput(LANG.ou_ztop_s, ov_topz), + ov_botz: UC.newInput(LANG.ou_zbot_s, ov_botz), exp_end: UC.endExpand(), sep: UC.newBlank({ class: "pop-sep" }), menu: UC.newRow([UC.newButton("select", surfaceAdd)], { class: "ext-buttons f-row" }), @@ -669,28 +720,6 @@ export function createPopOps() { count: UC.newInput('operations', { title: "num operations", convert: toInt, bound: UC.bound(1, 20), post: opRender }) }; - function isClear() { - return env.poppedRec.mode === 'clear'; - } - - function isTrace() { - return env.poppedRec.mode === 'trace'; - } - - function isShadow() { - return env.poppedRec.shadow; - } - - function isSurface() { - return env.poppedRec.mode === 'surface'; - } - - function isSurfaceLinear() { - return env.poppedRec.mode === 'surface' && env.poppedRec.sr_type === 'linear'; - } - - const open = true; - createPopOp('area', { spindle: 'camAreaSpindle', tool: 'camAreaTool', @@ -756,8 +785,8 @@ export function createPopOps() { exp_end: UC.endExpand(), exp: UC.newExpand("bounds", { }), sep: UC.newBlank({ class: "pop-sep" }), - ov_topz: UC.newInput(LANG.ou_ztop_s, { title: LANG.ou_ztop_l, convert: toFloat, units }), - ov_botz: UC.newInput(LANG.ou_zbot_s, { title: LANG.ou_zbot_l, convert: toFloat, units }), + ov_topz: UC.newInput(LANG.ou_ztop_s, ov_topz), + ov_botz: UC.newInput(LANG.ou_zbot_s, ov_botz), exp_end: UC.endExpand(), }; diff --git a/src/kiri/mode/cam/cl-stock.js b/src/kiri/mode/cam/cl-stock.js index acadfd79..a62045b0 100644 --- a/src/kiri/mode/cam/cl-stock.js +++ b/src/kiri/mode/cam/cl-stock.js @@ -8,6 +8,70 @@ const { ui: UI } = api; let meshZTop, meshZBottom; +{ + let geo = new THREE.PlaneGeometry(1, 1); + let mat = new THREE.MeshBasicMaterial({ + color: 0x779977, + opacity: 0.55, + transparent: true, + side: THREE.DoubleSide + }); + meshZTop = new THREE.Mesh(geo, mat); + meshZTop.renderOrder = 1; +} + +{ + let geo = new THREE.PlaneGeometry(1, 1); + let mat = new THREE.MeshBasicMaterial({ + color: 0x997777, + opacity: 0.55, + transparent: true, + side: THREE.DoubleSide + }); + meshZBottom = new THREE.Mesh(geo, mat); + meshZBottom.renderOrder = 1; +} + +function getBounds() { + const settings = api.conf.get(); + const { stock } = settings; + const { x, y, z, center } = stock; + const widgets = api.widgets.all(); + let max = { x, y, z }; + for (let w of widgets) { + max.x = Math.max(max.x, w.track.box.w); + max.y = Math.max(max.y, w.track.box.h); + max.z = Math.max(max.z, w.track.box.d); + } + return widgets.length ? { max, center } : {}; +} + +export function showZTop(zval) { + SPACE.world.remove(meshZTop); + let { max, center } = getBounds(); + if (zval && max && center) { + meshZTop.position.x = center.x; + meshZTop.position.y = center.y; + meshZTop.position.z = zval; + meshZTop.scale.x = max.x; + meshZTop.scale.y = max.y; + SPACE.world.add(meshZTop); + } +} + +export function showZBottom(zval) { + SPACE.world.remove(meshZBottom); + let { max, center } = getBounds(); + if (zval && max && center) { + meshZBottom.position.x = center.x; + meshZBottom.position.y = center.y; + meshZBottom.position.z = zval; + meshZBottom.scale.x = max.x; + meshZBottom.scale.y = max.y; + SPACE.world.add(meshZBottom); + } +} + export function updateStock() { if (env.isAnimate) { if (env.isIndexed) { @@ -22,16 +86,12 @@ export function updateStock() { SPACE.world.remove(meshZBottom); SPACE.world.remove(env.camStock); env.camStock = null; - meshZTop = null; - meshZBottom = null; return; } api.platform.update_bounds(); const settings = api.conf.get(); - const widgets = api.widgets.all(); - const { stock, process } = settings; const { x, y, z, center } = stock; @@ -86,57 +146,8 @@ export function updateStock() { lines.material.color = new THREE.Color(isDark() ? 0x555555 : 0xaaaaaa); } - SPACE.world.remove(meshZTop); - if (process.camZTop && widgets.length) { - let max = { x, y, z }; - for (let w of widgets) { - max.x = Math.max(max.x, w.track.box.w); - max.y = Math.max(max.y, w.track.box.h); - max.z = Math.max(max.z, w.track.box.d); - } - let geo = new THREE.PlaneGeometry(max.x, max.y); - let mat = new THREE.MeshBasicMaterial({ - color: 0x777777, - opacity: 0.55, - transparent: true, - side: THREE.DoubleSide - }); - meshZTop = new THREE.Mesh(geo, mat); - meshZTop._max = max; - meshZTop.renderOrder = 1; - meshZTop.position.x = center.x; - meshZTop.position.y = center.y; - meshZTop.position.z = process.camZTop; - SPACE.world.add(meshZTop); - } else { - meshZTop = undefined; - } - - SPACE.world.remove(meshZBottom); - if (process.camZBottom && widgets.length) { - let max = { x, y, z }; - for (let w of widgets) { - max.x = Math.max(max.x, w.track.box.w); - max.y = Math.max(max.y, w.track.box.h); - max.z = Math.max(max.z, w.track.box.d); - } - let geo = new THREE.PlaneGeometry(max.x, max.y); - let mat = new THREE.MeshBasicMaterial({ - color: 0x777777, - opacity: 0.55, - transparent: true, - side: THREE.DoubleSide - }); - meshZBottom = new THREE.Mesh(geo, mat); - meshZBottom._max = max; - meshZBottom.renderOrder = 1; - meshZBottom.position.x = center.x; - meshZBottom.position.y = center.y; - meshZBottom.position.z = process.camZBottom; - SPACE.world.add(meshZBottom); - } else { - meshZBottom = undefined; - } + showZTop(process.camZTop); + showZBottom(process.camZBottom); SPACE.update(); } diff --git a/src/kiri/mode/cam/cl-zplane.js b/src/kiri/mode/cam/cl-zplane.js new file mode 100644 index 00000000..03155141 --- /dev/null +++ b/src/kiri/mode/cam/cl-zplane.js @@ -0,0 +1,33 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { api } from '../../app/api.js'; +import { clearPops, hoverStart } from './init-ui.js'; +import { showZTop, showZBottom, updateStock } from './cl-stock.js'; + +let alert; +let plane; +let ondone; +let lastY; + +export function zPlaneStart(which, onselect) { + alert = api.show.alert("[esc] cancels z plane selection"); + plane = which; + ondone = onselect; + hoverStart(onHover, onHoverUp); +} + +export function zPlaneDone() { + api.hide.alert(alert); + updateStock(); +} + +function onHover(data) { + const { int, type, point } = data; + if (plane === 'top') showZTop(lastY = point.y); else + if (plane === 'bottom') showZBottom(lastY = point.y); +} + +function onHoverUp(int) { + ondone(lastY); + clearPops(); +} diff --git a/src/kiri/mode/cam/init-ui.js b/src/kiri/mode/cam/init-ui.js index aa667ec5..ea57bb41 100644 --- a/src/kiri/mode/cam/init-ui.js +++ b/src/kiri/mode/cam/init-ui.js @@ -2,23 +2,24 @@ import { $ } from '../../../moto/webui.js'; import { api } from '../../app/api.js'; -import { load } from '../../../load/file.js'; import { animate as anim_2d, animate_clear as anim_2d_clear } from './anim-2d-fe.js'; import { animate2 as anim_3d, animate_clear2 as anim_3d_clear } from './anim-3d-fe.js'; -import { space as SPACE } from '../../../moto/space.js'; -import { Layers } from '../../app/layers.js'; -import { Stack } from '../../app/stack.js'; -import { updateStock } from './cl-stock.js'; import { createPopOps } from './cl-ops.js'; +import { helicalOn, helicalDone } from './cl-helical.js'; +import { holeSelOn, selectHolesDone, clearHolesRec } from './cl-hole.js'; +import { load } from '../../../load/file.js'; +import { Layers } from '../../app/layers.js'; +import { originSelectDone } from './cl-origin.js'; +import { recreateTabs } from './cl-tab.js'; +import { showZTop, showZBottom, updateStock } from './cl-stock.js'; +import { space as SPACE } from '../../../moto/space.js'; +import { space } from '../../../moto/space.js'; +import { Stack } from '../../app/stack.js'; +import { surfaceOn, surfaceDone } from './cl-surface.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'; -import { helicalOn, helicalDone } from './cl-helical.js'; -import { originSelectDone } from './cl-origin.js'; import { Widget, newWidget } from '../../app/widget.js'; -import { space } from '../../../moto/space.js'; -import { recreateTabs } from './cl-tab.js'; +import { zPlaneStart, zPlaneDone } from './cl-zplane.js'; const { BufferGeometryUtils } = THREE; @@ -62,13 +63,28 @@ export function isDark() { export function clearPops() { if (env.func.unpop) env.func.unpop(); + helicalDone(); originSelectDone(); selectHolesDone(); surfaceDone(); tabDone(); traceDone(); - selectHolesDone(); - helicalDone(); + zPlaneDone(); + // unbind + hoverDone(); +} + +export function hoverStart(onhover, onhoverup) { + clearPops(); + env.hover = onhover; + env.hoverUp = onhoverup; + api.feature.hover = true; +} + +export function hoverDone() { + env.hover = noop; + env.hoverUp = noop; + api.feature.hover = false; } function animFn() { @@ -288,7 +304,6 @@ export function opRender() { let mark = Date.now(); let html = []; let bind = {}; - let scale = api.view.unit_scale(); let notime = false; let inloop = 0; let numloop; @@ -556,10 +571,26 @@ export function opRender() { updateStock(); } +export function zPlaneShow({ which, value }) { + if (!value) return; + if (which === 'top') showZTop(value); else + if (which === 'bottom') showZBottom(value); +} + +export function zPlaneHide({ which }) { + updateStock(); +} + +export function zPlaneSelect({ which, onselect }) { + clearPops(); + zPlaneStart(which, value => { + onselect(parseFloat(value)); + }); +} + export function init() { api.event.on('tool.mesh.face-normal', normal => { - // console.log({ env.poppedRec }); env.poppedRec.degrees = (Math.atan2(normal.y, normal.z) * RAD2DEG).round(2); env.poppedRec.absolute = true; opRender(); @@ -850,17 +881,20 @@ export function init() { } }); - api.event.on("cam.op.add", opAdd); + api.event + .on("cam.op.add", opAdd) + .on("cam.op.del", opDel) + .on("cam.op.render", opRender); - api.event.on("cam.op.del", opDel); + api.event + .on("cam.tabs.add", tabAdd) + .on("cam.tabs.done", tabDone) + .on("cam.tabs.clear", tabClear); - api.event.on("cam.op.render", opRender); - - api.event.on("cam.tabs.add", tabAdd); - - api.event.on("cam.tabs.done", tabDone); - - api.event.on("cam.tabs.clear", tabClear); + api.event + .on("cam.zplane.show", zPlaneShow) + .on("cam.zplane.hide", zPlaneHide) + .on("cam.zplane.select", zPlaneSelect); // COMMON TAB/TRACE EVENT HANDLERS api.event.on("slice.begin", () => { diff --git a/web/kiri/index.css b/web/kiri/index.css index d3520e7a..c3b6f387 100644 --- a/web/kiri/index.css +++ b/web/kiri/index.css @@ -1431,6 +1431,11 @@ details[open] summary::after { word-break: break-word; white-space: normal; } +.z-select { + aspect-ratio: 1; + border-radius: 50%; + border-color: rgba(128,128,128,0.5) !important; +} #oplist { gap: 3px; margin-bottom: 3px; From c889491aebe81e7769106734abc2b4dff3c28589 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sun, 4 Jan 2026 15:02:37 -0500 Subject: [PATCH 098/154] show z top and bottom overrides on expandable summary hover --- src/kiri/app/inputs.js | 5 +++++ src/kiri/mode/cam/cl-ops.js | 23 ++++++++++++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/kiri/app/inputs.js b/src/kiri/app/inputs.js index 883ffdde..cb876b1f 100644 --- a/src/kiri/app/inputs.js +++ b/src/kiri/app/inputs.js @@ -568,6 +568,11 @@ function newExpand(label, opt = {}) { div.removeAttribute('open'); }; + if (opt.hover) { + div.addEventListener('mouseenter', event => opt.hover('enter', event)); + div.addEventListener('mouseleave', event => opt.hover('leave', event)); + } + lastAddTo = addTo; addTo.appendChild(div); addTo = div; diff --git a/src/kiri/mode/cam/cl-ops.js b/src/kiri/mode/cam/cl-ops.js index c98541a3..b141323b 100644 --- a/src/kiri/mode/cam/cl-ops.js +++ b/src/kiri/mode/cam/cl-ops.js @@ -10,6 +10,7 @@ import { selectHelical } from './cl-helical.js'; import { surfaceAdd } from './cl-surface.js'; import { traceAdd } from './cl-trace.js'; import { startFaceUp } from '../../app/face-tool.js'; +import { showZBottom, showZTop, updateStock } from './cl-stock.js'; const { MODES } = api.const; const { uc: UC } = api; @@ -265,8 +266,20 @@ export function createPopOps() { } const ov_topz = { title: LANG.ou_ztop_l, convert: toFloat, units, selector, top, field: "ov_topz" }; + const ov_botz = { title: LANG.ou_zbot_l, convert: toFloat, units, selector, bottom, field: "ov_botz" }; + const ov_hover = { + hover(ie) { + if (ie === 'enter') { + showZTop(env.poppedRec.ov_topz); + showZBottom(env.poppedRec.ov_botz); + } else { + updateStock(); + } + } + }; + createPopOp('level', { tool: 'camLevelTool', spindle: 'camLevelSpindle', @@ -323,7 +336,7 @@ export function createPopOps() { inside: UC.newBoolean(LANG.cr_olin_s, undefined, { title: LANG.cr_olin_l, show: () => !env.poppedRec.all || env.poppedRec.inside }), omitthru: UC.newBoolean(LANG.co_omit_s, undefined, { title: LANG.co_omit_l }), sep: UC.newBlank({ class: "pop-sep" }), - exp: UC.newExpand("overrides"), + exp: UC.newExpand("overrides", ov_hover), ov_topz: UC.newInput(LANG.ou_ztop_s, ov_topz), ov_botz: UC.newInput(LANG.ou_zbot_s, ov_botz), exp_end: UC.endExpand(), @@ -367,7 +380,7 @@ export function createPopOps() { dogbones: UC.newBoolean(LANG.co_dogb_s, undefined, { title: LANG.co_dogb_l, show: (op) => { return !op.inputs.wide.checked } }), revbones: UC.newBoolean(LANG.co_dogr_s, undefined, { title: LANG.co_dogr_l, show: () => env.poppedRec.dogbones }), sep: UC.newBlank({ class: "pop-sep" }), - exp: UC.newExpand("overrides"), + exp: UC.newExpand("overrides", ov_hover), ov_topz: UC.newInput(LANG.ou_ztop_s, ov_topz), ov_botz: UC.newInput(LANG.ou_zbot_s, ov_botz), exp_end: UC.endExpand(), @@ -486,7 +499,7 @@ export function createPopOps() { merge: UC.newBoolean(LANG.co_merg_s, undefined, { title: LANG.co_merg_l, show: () => !env.popOp.trace.rec.down }), dogbone: UC.newBoolean(LANG.co_dogb_s, undefined, { title: LANG.co_dogb_l, show: canDogBones }), revbone: UC.newBoolean(LANG.co_dogr_s, undefined, { title: LANG.co_dogr_l, show: canDogBonesRev }), - exp: UC.newExpand("overrides"), + exp: UC.newExpand("overrides", ov_hover), sep: UC.newBlank({ class: "pop-sep" }), ov_topz: UC.newInput(LANG.ou_ztop_s, ov_topz), ov_botz: UC.newInput(LANG.ou_zbot_s, ov_botz), @@ -532,7 +545,7 @@ export function createPopOps() { sep: UC.newBlank({ class: "pop-sep" }), contour: UC.newBoolean(LANG.cp_cont_s, undefined, { title: LANG.cp_cont_s }), outline: UC.newBoolean(LANG.cp_outl_s, undefined, { title: LANG.cp_outl_l }), - exp: UC.newExpand("overrides"), + exp: UC.newExpand("overrides", ov_hover), sep: UC.newBlank({ class: "pop-sep" }), ov_topz: UC.newInput(LANG.ou_ztop_s, ov_topz), ov_botz: UC.newInput(LANG.ou_zbot_s, ov_botz), @@ -783,7 +796,7 @@ export function createPopOps() { rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: toInt, units }), plunge: UC.newInput(LANG.cc_plng_s, { title: LANG.cc_plng_l, convert: toInt, units }), exp_end: UC.endExpand(), - exp: UC.newExpand("bounds", { }), + exp: UC.newExpand("bounds", ov_hover), sep: UC.newBlank({ class: "pop-sep" }), ov_topz: UC.newInput(LANG.ou_ztop_s, ov_topz), ov_botz: UC.newInput(LANG.ou_zbot_s, ov_botz), From 96577c4b7b68e626a308ee5812d54f2d4e2bcf87 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sun, 4 Jan 2026 15:31:38 -0500 Subject: [PATCH 099/154] allow z top/bottom visual select in left menu. double select cancels --- src/kiri/mode/cam/cl-zplane.js | 16 ++++++++++++---- src/kiri/mode/cam/init-menu.js | 28 ++++++++++++++++++++++++++-- src/kiri/mode/cam/init-ui.js | 7 ++++--- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/src/kiri/mode/cam/cl-zplane.js b/src/kiri/mode/cam/cl-zplane.js index 03155141..8dda0811 100644 --- a/src/kiri/mode/cam/cl-zplane.js +++ b/src/kiri/mode/cam/cl-zplane.js @@ -10,24 +10,32 @@ let ondone; let lastY; export function zPlaneStart(which, onselect) { - alert = api.show.alert("[esc] cancels z plane selection"); plane = which; ondone = onselect; hoverStart(onHover, onHoverUp); + alert = api.show.alert("[esc] cancels z plane selection", 1000); } export function zPlaneDone() { api.hide.alert(alert); updateStock(); + alert = undefined; +} + +export function zPlaneSelecting() { + return alert ? true : false; } function onHover(data) { - const { int, type, point } = data; + const { int, point } = data; + if (!int) return; if (plane === 'top') showZTop(lastY = point.y); else if (plane === 'bottom') showZBottom(lastY = point.y); } function onHoverUp(int) { - ondone(lastY); - clearPops(); + if (int) { + ondone(lastY); + clearPops(); + } } diff --git a/src/kiri/mode/cam/init-menu.js b/src/kiri/mode/cam/init-menu.js index d061b1c0..83316ba0 100644 --- a/src/kiri/mode/cam/init-menu.js +++ b/src/kiri/mode/cam/init-menu.js @@ -30,6 +30,30 @@ function zAnchorSave() { api.platform.update_top_z(); } +const bottom = true; +const top = true; + +function selector(type, { input, event, opt }) { + let which = opt?.top ? 'top' : opt?.bottom ? 'bottom' : 'unknown'; + let value = parseFloat(input.value); + switch (type) { + case 'click': + api.event.emit('cam.zplane.select', { which, onselect(value) { + input.focus(); + input.value = value; + input.blur(); + } } ); + break; + case 'enter': + api.event.emit('cam.zplane.show', { which, value }); + break; + case 'leave': + api.event.emit('cam.zplane.hide', { which }); + break; + } +} + + export function menu() { let anim = ui.anim = {}; @@ -121,8 +145,8 @@ export function menu() { _____: newGroup(LANG.cc_menu, $('cam-limits'), { modes:CAM, driven, separator }), camZAnchor: newSelect(LANG.ou_zanc_s, {title: LANG.ou_zanc_l, action:zAnchorSave, show:() => !ui.camStockIndexed.checked}, "zanchor"), camZOffset: newInput(LANG.ou_ztof_s, {title:LANG.ou_ztof_l, convert:toFloat, units}), - camZTop: newInput(LANG.ou_ztop_s, {title:LANG.ou_ztop_l, convert:toFloat, units, trigger}), - camZBottom: newInput(LANG.ou_zbot_s, {title:LANG.ou_zbot_l, convert:toFloat, units, trigger}), + camZTop: newInput(LANG.ou_ztop_s, {title:LANG.ou_ztop_l, convert:toFloat, units, trigger, selector, top }), + camZBottom: newInput(LANG.ou_zbot_s, {title:LANG.ou_zbot_l, convert:toFloat, units, trigger, selector, bottom }), camZClearance: newInput(LANG.ou_zclr_s, {title:LANG.ou_zclr_l, convert:toFloat, bound:bound(0.01,100), units }), separator: newBlank({ class:"set-sep", driven }), camFastFeed: newInput(LANG.cc_rapd_s, {title:LANG.cc_rapd_l, convert:toFloat, units}), diff --git a/src/kiri/mode/cam/init-ui.js b/src/kiri/mode/cam/init-ui.js index ea57bb41..6ee3e694 100644 --- a/src/kiri/mode/cam/init-ui.js +++ b/src/kiri/mode/cam/init-ui.js @@ -19,7 +19,7 @@ import { surfaceOn, surfaceDone } from './cl-surface.js'; import { tabAdd, tabDone, tabClear, restoreTabs, rotateTabs, updateTabs, clearTabs, mirrorTabs } from './cl-tab.js'; import { traceOn, traceDone, unselectTraces } from './cl-trace.js'; import { Widget, newWidget } from '../../app/widget.js'; -import { zPlaneStart, zPlaneDone } from './cl-zplane.js'; +import { zPlaneStart, zPlaneDone, zPlaneSelecting } from './cl-zplane.js'; const { BufferGeometryUtils } = THREE; @@ -582,6 +582,9 @@ export function zPlaneHide({ which }) { } export function zPlaneSelect({ which, onselect }) { + if (zPlaneSelecting()) { + return zPlaneDone(); + } clearPops(); zPlaneStart(which, value => { onselect(parseFloat(value)); @@ -805,8 +808,6 @@ export function init() { }; $('op-add-list').onclick = (ev) => { - let settings = api.conf.get(); - let { process, device } = settings; switch (ev.target.innerText.toLowerCase()) { case "index": return opAddIndex(); case "laser on": return opAddLaserOn(); From 5a5c0f0131639829e4d48e56cfc973eddb99573d Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sun, 4 Jan 2026 23:15:58 -0500 Subject: [PATCH 100/154] update keyboard / mouse docs --- docs/kiri-moto/controls.md | 171 +++++++++++++++++++++++++------------ package.json | 8 +- 2 files changed, 120 insertions(+), 59 deletions(-) diff --git a/docs/kiri-moto/controls.md b/docs/kiri-moto/controls.md index 517f84ac..5f07edc8 100644 --- a/docs/kiri-moto/controls.md +++ b/docs/kiri-moto/controls.md @@ -4,58 +4,119 @@ description: Keyboard Shortcuts and Mouse Controls # Controls -| Key | Action | Notes | -| ---------------- | ----------------------------------------- | --------------------------------------- | -| e | Device Dialog | Select and customize devices | -| o | CNC Tool Dialog | CNC mode only | -| c | Grid.Local Dialog | Find GridBot devices on your network | -| q | Preference Dialog | Change application behaviors | -| l | Work Profile Dialog | Load, Save, Rename work profiles | -| | | | -| a | Arrange | Switch to layout view from other views | -| | | or perform an automatic layout when | -| | | already in layout view | -| s | Slice Object | Slice workspace objects | -| p | Preview Paths | Do route planning for sliced objects | -| g | CNC Animation | Enter milling animation mode | -| x | Export Paths as GCode, SVG, DXF | Produce downloadable files | -| | | | -| i | File Import Dialog | | -| r | Recent Files Dialog | | -| | | | -| h | Home View | Default 45 degree view of objects | -| t | Top View | Top-down view of objects | -| f | Front View | Direct side-view (front) | -| z | Reset View | Default zoom and Home | -| < > | Cycle Side Views | Front, Right, Back, Left | -| v | Focus on Selection (arrange mode) | Camera points to object center | -| v | Toggle Single Slice View (preview mode) | Toggle single vs range of slices | -| | | | -| \~,1-9,0 | Select layer in 10% increments | Slice and Preview modes | -| | | | -| d | Duplicate Selected Objects | Create a copy of selected objects | -| m | Mirror Selected Objects | Mirror vertices of selected objects | -| \[delete] | Delete Selected Objects | Remove selected object from workspace | -| \[arrow] | Rotate 90 degrees | Rotate selected object | -| \[shift + arrow] | Rotate 5 degrees | Rotate selected object | -| \[ctrl + a] | Select All Objects | Select all objects | -| | | | -| \[shit + click] | Toggle Selection | Select or Deselect a single object | -| \[shift + wheel] | Cycle through layers (preview mode) | Use mouse wheel as range slider | -| \[meta + drag] | Pan View | Move focus of camera | -| \[right + drag] | Pan View | | -| \[left + drag] | Rotate | Move camera around a focal point | -| \[mouse wheel] | Zoom | Move camera closer / farther from focus | -| \[middle + drag] | Zoom | | -| | | | -| \[ctrl + click] | Align face of selected object to platform | Rotate object face toward work surface | -| \[meta + click] | Align face of selected object to platform | | -| | | | -| \[shift + z] | Reset all preferences to stock | Reload page afterward | -| | | | -| **Mouse** | **FDM Support Controls** | **Version Notes** | -| \[left click] | Toggle add / remove column | | -| \[ctrl + drag] | Erase support columns | 2.7+ | -| \[cmd + drag] | Erase support columns | 2.7+ | -| \[opt + drag] | Draw on support columns | 2.7+ | -| \[alt + drag] | Draw on support columns | 2.7+ | +## Layer Navigation + +| Key | Action | Notes | +| --- | ------ | ----- | +| ` (backtick) | Show layer 0 | First layer | +| 1-9 | Show layers at 10-90% | In 10% increments | +| 0 | Show all layers (100%) | Show maximum layer | +| Meta+Up | Next layer | Navigate up one layer | +| Meta+Down | Previous layer | Navigate down one layer | +| [shift + wheel] | Cycle through layers | Preview mode only | +| v | Toggle single slice view | Preview mode: toggle single vs range | + +## View Navigation + +| Key | Action | Notes | +| --- | ------ | ----- | +| h | Home View | Default 45° view | +| t | Top View | Top-down view | +| f | Front View | Direct front side-view | +| b | Back View | Direct back side-view | +| z | Reset View | Default zoom and home | +| F | Fit View to Contents | Zoom to fit all objects | +| `<` | Previous Side View | Cycle: left → back → right → front | +| `>` | Next Side View | Cycle: front → right → back → left | +| v | Focus on Selection | Arrange mode: center camera on object | + +## File Operations + +| Key | Action | Notes | +| --- | ------ | ----- | +| i | File Import Dialog | Import 3D models | +| r | Recent Files Dialog | Open recently used files | + +## Workflow Actions + +| Key | Action | Notes | +| --- | ------ | ----- | +| s / S | Slice Object | Process workspace objects | +| p / P | Preview Paths | Route planning (hidden in SLA mode) | +| g | CNC Animation | Enter milling animation mode (CNC only) | +| x / X | Export | Export as GCode, SVG, or DXF | +| a | Arrange | Switch to ARRANGE view, or auto-layout if already in ARRANGE | + +## Object Manipulation + +| Key | Action | Notes | +| --- | ------ | ----- | +| d | Duplicate Selected | Create copy of selected objects | +| m | Mirror Selected | Mirror vertices of selected objects | +| O | Manual Rotation Input | Enter rotation values manually | +| [delete] | Delete Selected | Remove selected objects | +| [ctrl + a] / [cmd + a] | Select All | Select all objects in workspace | +| [shift + click] | Toggle Selection | Select or deselect individual object | + +## Rotation & Movement + +| Key | Action | Notes | +| --- | ------ | ----- | +| [arrow] | Rotate 90° | Arrow keys rotate on respective axes | +| [shift + arrow] | Rotate 5° | Fine rotation control | +| [alt + arrow] | Move 5mm | Move object in X/Y plane | + +## Rendering Modes + +| Key | Action | Notes | +| --- | ------ | ----- | +| w | Toggle Ghost Rendering | Cycle between solid and semi-transparent | +| W | Toggle Wireframe | Cycle between solid and wireframe | +| Ctrl+W / Cmd+W | Toggle Edge Rendering | Show/hide model edges | + +## Settings & Dialogs + +| Key | Action | Notes | +| --- | ------ | ----- | +| e | Device Dialog | Select and customize devices | +| o | Tool Dialog | CNC mode only | +| q | Preferences Dialog | Change application behaviors | +| l | Work Profile Dialog | Load, save, rename work profiles | +| ? | Help Dialog | Show help and documentation | +| C | Refresh Catalog | Reload device catalog | + +## Workspace Management + +| Key | Action | Notes | +| --- | ------ | ----- | +| Ctrl+S | Save Settings | Save current settings | +| Cmd+S | Save Workspace | Save workspace state (macOS) | +| Cmd+L | Restore Workspace | Restore saved workspace (macOS) | +| Z | Reset All Preferences | Clear all settings (requires confirmation) | + +## Mouse Controls + +### General Navigation + +| Input | Action | Notes | +| ----- | ------ | ----- | +| [left + drag] | Rotate View | Orbit camera around focus point | +| [right + drag] | Pan View | Move camera focus | +| [meta + drag] | Pan View | Alternative pan (macOS) | +| [middle + drag] | Zoom | Mouse wheel click + drag | +| [mouse wheel] | Zoom | Scroll to zoom in/out | + +### Object Interaction + +| Input | Action | Notes | +| ----- | ------ | ----- | +| [ctrl + click] | Lay Flat | Rotate clicked face toward platform | +| [meta + click] | Lay Flat | Alternative (macOS) | + +### FDM Support Mode + +| Input | Action | Notes | +| ----- | ------ | ----- | +| [left click] | Toggle Support Column | Add or remove support column | +| [ctrl + drag] / [cmd + drag] | Erase Columns | Remove support columns | +| [alt + drag] / [opt + drag] | Draw Columns | Add support columns | diff --git a/package.json b/package.json index 7691eeb8..223143b6 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "node": ">=22.0.0" }, "dependencies": { - "@docusaurus/plugin-client-redirects": "^3.9.0", + "@docusaurus/plugin-client-redirects": "3.9.1", "@fortawesome/fontawesome-free": "^6.1.1", "@gridspace/app-server": "^0.0.20", "@gridspace/basic-ftp": "github:gridspace/basic-ftp#v5.0.5-gridspace", @@ -70,8 +70,8 @@ "ws": "^8.18.2" }, "devDependencies": { - "@docusaurus/core": "^3.9.0", - "@docusaurus/preset-classic": "^3.9.0", + "@docusaurus/core": "3.9.1", + "@docusaurus/preset-classic": "3.9.1", "@electron/notarize": "^3.0.0", "copy-webpack-plugin": "^12.0.2", "cross-env": "^10.1.0", @@ -105,7 +105,7 @@ "docs-build": "docusaurus build --config conf/docusaurus.config.js", "docs-check": "prettier --config ./conf/prettier.config.js ./docs --check", "docs-dev": "docusaurus start --config conf/docusaurus.config.js", - "docs-serve": "docusaurus serve --config conf/docusaurus.config.js", + "docs-serve": "docusaurus serve --config conf/docusaurus.config.js --port 4004", "dryrun": "gs-app-server --debug --single --dryrun", "dryrun-prod": "gs-app-server --single --dryrun", "mac-verify": "spctl --assess -vv --type install dist/*/*.app", From 0ff04f71616f441341745036e8b47740fcf4ecda Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sun, 4 Jan 2026 23:31:17 -0500 Subject: [PATCH 101/154] add shader for painting on meshes --- src/kiri/app/paint.js | 378 ++++++++++++++++++++++++++++++++++ src/kiri/mode/cam/cl-stock.js | 2 +- 2 files changed, 379 insertions(+), 1 deletion(-) create mode 100644 src/kiri/app/paint.js diff --git a/src/kiri/app/paint.js b/src/kiri/app/paint.js new file mode 100644 index 00000000..93abbe33 --- /dev/null +++ b/src/kiri/app/paint.js @@ -0,0 +1,378 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +/** + * Paint overlay shaders for mesh painting functionality. + * + * Provides two implementations for visualizing painted regions on mesh surfaces: + * 1. Simple uniform-based (up to ~256 paint points) + * 2. Texture-based (unlimited paint points) + * + * Paint points are spheres defined by center position and radius. + * The shader checks if each fragment falls within any paint sphere and + * blends the paint color accordingly. + * + * PAINT POINT DATA STRUCTURE: + * --------------------------- + * Paint points must be an array of objects with this structure: + * + * { + * point: { x: number, y: number, z: number }, // World-space center of paint sphere + * radius: number // Sphere radius in mm + * } + * + * Example: + * const paintPoints = [ + * { point: { x: 10.5, y: 20.3, z: 15.0 }, radius: 5.0 }, + * { point: { x: 12.0, y: 21.0, z: 15.5 }, radius: 5.0 }, + * ... + * ]; + * + * USAGE: + * ------ + * // Initial setup - enable paint overlay: + * widget._origMaterial = widget.mesh.material; + * widget.mesh.material = widget.mesh.material.clone(); + * addPaintOverlayAuto(widget.mesh.material, paintPoints, new THREE.Color(0x4488ff)); + * widget.mesh.material.needsUpdate = true; + * + * // Real-time updates while painting (see updatePaintOverlay function below) + * + * // Cleanup - restore original material: + * widget.mesh.material = widget._origMaterial; + * delete widget._origMaterial; + * + * REAL-TIME UPDATES: + * ------------------ + * When actively painting (user dragging mouse), paint points are added continuously. + * Use updatePaintOverlay() to efficiently update the shader without recreating material: + * + * // Add new paint point + * paintPoints.push({ point: { x, y, z }, radius: 5 }); + * + * // Update shader to show new point immediately + * updatePaintOverlay(widget.mesh.material, paintPoints); + * + * Note: After onBeforeCompile runs once, the shader program is compiled and cached. + * Subsequent updates only need to update the uniform values, which is very fast. + */ + +import { THREE } from '../../ext/three.js'; + +/** + * Add paint overlay to material using uniform array storage. + * Suitable for up to ~256 paint points (GPU uniform limit varies). + * + * Uses sphere-based distance checks to determine if a fragment is painted. + * Paint color is blended at 50% opacity with the underlying material color. + * + * @param {THREE.Material} material - Material to modify (will be mutated via onBeforeCompile) + * @param {Array<{point: {x,y,z}, radius: number}>} paintPoints - Array of paint spheres + * @param {THREE.Color} [paintColor] - Color for painted regions (default: light blue) + * @returns {THREE.Material} The modified material + */ +function addPaintOverlaySimple(material, paintPoints, paintColor = new THREE.Color(0x4488ff)) { + material.onBeforeCompile = (shader) => { + // Store shader reference for later updates + material.userData.shader = shader; + + // Convert paint points to vec4 array (x, y, z, radius) + const points = paintPoints.map(p => + new THREE.Vector4(p.point.x, p.point.y, p.point.z, p.radius) + ); + + // Add uniforms for paint data + shader.uniforms.paintPoints = { value: points }; + shader.uniforms.paintCount = { value: points.length }; + shader.uniforms.paintColor = { value: paintColor }; + + // Pass world position from vertex shader to fragment shader + shader.vertexShader = shader.vertexShader.replace( + `#include `, + ` + #include + vWorldPosition = vec3(transformed); + ` + ); + + shader.vertexShader = ` + varying vec3 vWorldPosition; + ` + shader.vertexShader; + + // Fragment shader: check distance to each paint sphere + shader.fragmentShader = ` + varying vec3 vWorldPosition; + uniform vec4 paintPoints[256]; // (x, y, z, radius) + uniform int paintCount; + uniform vec3 paintColor; + ` + shader.fragmentShader; + + shader.fragmentShader = shader.fragmentShader.replace( + `#include `, + ` + #include + + // Check if this fragment is within any paint sphere + bool painted = false; + + for (int i = 0; i < 256; i++) { + if (i >= paintCount) break; + + vec3 center = paintPoints[i].xyz; + float radius = paintPoints[i].w; + float dist = distance(vWorldPosition, center); + + if (dist <= radius) { + painted = true; + break; // Early exit once painted + } + } + + if (painted) { + // Blend paint color with existing fragment color + gl_FragColor.rgb = mix(gl_FragColor.rgb, paintColor, 0.5); + } + ` + ); + }; + + return material; +} + +/** + * Add paint overlay using texture-based point storage. + * Scales to unlimited paint points by encoding them in a DataTexture. + * + * Each texel in the data texture stores one paint point as RGBA float: + * - R channel: x coordinate + * - G channel: y coordinate + * - B channel: z coordinate + * - A channel: radius + * + * @param {THREE.Material} material - Material to modify (will be mutated via onBeforeCompile) + * @param {Array<{point: {x,y,z}, radius: number}>} paintPoints - Array of paint spheres + * @param {THREE.Color} [paintColor] - Color for painted regions (default: light blue) + * @returns {THREE.Material} The modified material + */ +function addPaintOverlayTexture(material, paintPoints, paintColor = new THREE.Color(0x4488ff)) { + material.onBeforeCompile = (shader) => { + // Store shader reference for later updates + material.userData.shader = shader; + + // Create data texture from paint points + // Texture is laid out as square grid: width x height >= point count + const pointCount = paintPoints.length; + const texWidth = Math.ceil(Math.sqrt(pointCount)); + const texHeight = Math.ceil(pointCount / texWidth); + const data = new Float32Array(texWidth * texHeight * 4); + + // Pack paint points into texture data + // Each point = 4 floats (x, y, z, radius) + for (let i = 0; i < paintPoints.length; i++) { + const p = paintPoints[i]; + data[i * 4 + 0] = p.point.x; + data[i * 4 + 1] = p.point.y; + data[i * 4 + 2] = p.point.z; + data[i * 4 + 3] = p.radius; + } + + // Create texture with float data + const texture = new THREE.DataTexture( + data, + texWidth, + texHeight, + THREE.RGBAFormat, + THREE.FloatType + ); + texture.needsUpdate = true; + + // Add uniforms for paint data + shader.uniforms.paintTexture = { value: texture }; + shader.uniforms.paintCount = { value: pointCount }; + shader.uniforms.paintTexSize = { value: new THREE.Vector2(texWidth, texHeight) }; + shader.uniforms.paintColor = { value: paintColor }; + + // Pass world position from vertex shader to fragment shader + shader.vertexShader = shader.vertexShader.replace( + `#include `, + ` + #include + vWorldPosition = vec3(transformed); + ` + ); + + shader.vertexShader = ` + varying vec3 vWorldPosition; + ` + shader.vertexShader; + + // Fragment shader: sample paint points from texture and check distances + shader.fragmentShader = ` + varying vec3 vWorldPosition; + uniform sampler2D paintTexture; + uniform int paintCount; + uniform vec2 paintTexSize; + uniform vec3 paintColor; + ` + shader.fragmentShader; + + shader.fragmentShader = shader.fragmentShader.replace( + `#include `, + ` + #include + + bool painted = false; + + // Iterate through paint points stored in texture + // Loop limit must be constant, but we break early when i >= paintCount + for (int i = 0; i < 10000; i++) { + if (i >= paintCount) break; + + // Calculate texture coordinates for this point index + // Points are stored row-major in the texture + float u = (float(i % int(paintTexSize.x)) + 0.5) / paintTexSize.x; + float v = (float(i / int(paintTexSize.x)) + 0.5) / paintTexSize.y; + + // Sample point data from texture (x, y, z, radius) + vec4 pointData = texture2D(paintTexture, vec2(u, v)); + vec3 center = pointData.xyz; + float radius = pointData.w; + + // Check if fragment is within this paint sphere + float dist = distance(vWorldPosition, center); + + if (dist <= radius) { + painted = true; + break; // Early exit once painted + } + } + + if (painted) { + // Blend paint color with existing fragment color + gl_FragColor.rgb = mix(gl_FragColor.rgb, paintColor, 0.5); + } + ` + ); + }; + + return material; +} + +/** + * Helper function to choose appropriate paint overlay method based on point count. + * Automatically selects simple or scalable version. + * + * @param {THREE.Material} material - Material to modify + * @param {Array<{point: {x,y,z}, radius: number}>} paintPoints - Array of paint spheres + * @param {THREE.Color} [paintColor] - Color for painted regions + * @returns {THREE.Material} The modified material + */ +function addPaintOverlayAuto(material, paintPoints, paintColor = new THREE.Color(0x4488ff)) { + if (paintPoints.length <= 256) { + return addPaintOverlaySimple(material, paintPoints, paintColor); + } else { + return addPaintOverlayTexture(material, paintPoints, paintColor); + } +} + +/** + * Update paint overlay with new paint points (for real-time painting). + * Call this after adding new points to the paintPoints array. + * + * This function updates the shader uniforms without recompiling the shader, + * making it suitable for real-time updates during mouse drag painting. + * + * IMPORTANT: The material must have already been initialized with one of the + * addPaintOverlay functions before calling this update function. + * + * @param {THREE.Material} material - Material with paint shader already applied + * @param {Array<{point: {x,y,z}, radius: number}>} paintPoints - Updated array of paint spheres + */ +function updatePaintOverlay(material, paintPoints) { + // Access the compiled shader uniforms + // After onBeforeCompile runs once, userData.shader contains the compiled program + const shader = material.userData?.shader; + + if (!shader || !shader.uniforms) { + console.warn('updatePaintOverlay: material does not have compiled paint shader'); + return; + } + + // Determine which shader type based on available uniforms + if (shader.uniforms.paintPoints) { + // Simple uniform-based shader + updatePaintOverlaySimple(shader, paintPoints); + } else if (shader.uniforms.paintTexture) { + // Texture-based shader + updatePaintOverlayTexture(shader, paintPoints); + } else { + console.warn('updatePaintOverlay: unrecognized paint shader type'); + } +} + +/** + * Update simple uniform-based paint shader. + * @private + */ +function updatePaintOverlaySimple(shader, paintPoints) { + const uniforms = shader.uniforms; + + // Convert paint points to vec4 array (x, y, z, radius) + const points = paintPoints.map(p => + new THREE.Vector4(p.point.x, p.point.y, p.point.z, p.radius) + ); + + // Update uniform values + uniforms.paintPoints.value = points; + uniforms.paintCount.value = points.length; + + // Note: No need to set needsUpdate on the material + // Uniform updates are automatically detected by THREE.js +} + +/** + * Update texture-based paint shader. + * @private + */ +function updatePaintOverlayTexture(shader, paintPoints) { + const uniforms = shader.uniforms; + + // Recreate texture with new paint points + const pointCount = paintPoints.length; + const texWidth = Math.ceil(Math.sqrt(pointCount)); + const texHeight = Math.ceil(pointCount / texWidth); + const data = new Float32Array(texWidth * texHeight * 4); + + // Pack paint points into texture data + for (let i = 0; i < paintPoints.length; i++) { + const p = paintPoints[i]; + data[i * 4 + 0] = p.point.x; + data[i * 4 + 1] = p.point.y; + data[i * 4 + 2] = p.point.z; + data[i * 4 + 3] = p.radius; + } + + // Create new texture + const texture = new THREE.DataTexture( + data, + texWidth, + texHeight, + THREE.RGBAFormat, + THREE.FloatType + ); + texture.needsUpdate = true; + + // Dispose old texture to prevent memory leak + if (uniforms.paintTexture.value) { + uniforms.paintTexture.value.dispose(); + } + + // Update uniform values + uniforms.paintTexture.value = texture; + uniforms.paintCount.value = pointCount; + uniforms.paintTexSize.value.set(texWidth, texHeight); +} + +export { + addPaintOverlaySimple, + addPaintOverlayTexture, + addPaintOverlayAuto, + updatePaintOverlay +}; diff --git a/src/kiri/mode/cam/cl-stock.js b/src/kiri/mode/cam/cl-stock.js index a62045b0..7f2aae0f 100644 --- a/src/kiri/mode/cam/cl-stock.js +++ b/src/kiri/mode/cam/cl-stock.js @@ -32,7 +32,7 @@ let meshZTop, meshZBottom; meshZBottom.renderOrder = 1; } -function getBounds() { +export function getBounds() { const settings = api.conf.get(); const { stock } = settings; const { x, y, z, center } = stock; From 7642aaab7b72c68d5293f3d31bbf131d9d521782 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Mon, 5 Jan 2026 00:14:19 -0500 Subject: [PATCH 102/154] rewrite gpu flatness detection to use same algorithm as cpu code --- src/kiri/mode/cam/topo3.js | 42 ++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/src/kiri/mode/cam/topo3.js b/src/kiri/mode/cam/topo3.js index 196ddf88..e5a7dbd7 100644 --- a/src/kiri/mode/cam/topo3.js +++ b/src/kiri/mode/cam/topo3.js @@ -265,24 +265,40 @@ export class Topo { // drop interior points along a continuous slope for (let poly of lines) { let { points } = poly; + if (points.length < 2) continue; let merged = [ points[0] ]; - let lp = points[1]; - let dz = points[0].z - lp.z; - for (let i=2; i flatness) { - // slope changed - merged.push(lp); - dz = ndz; - } else if (curvesOnly && Math.abs(ndz) < flatness) { + let lastP = points[0]; + let latent = null; + let lastSlope = undefined; + for (let i=1; i Date: Mon, 5 Jan 2026 00:31:27 -0500 Subject: [PATCH 103/154] allow contour only op chains with shadow disabled --- src/kiri/mode/cam/op-shadow.js | 15 ++++++++++++++- src/kiri/mode/cam/slice.js | 6 +++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/kiri/mode/cam/op-shadow.js b/src/kiri/mode/cam/op-shadow.js index e0289a11..a72b72d6 100644 --- a/src/kiri/mode/cam/op-shadow.js +++ b/src/kiri/mode/cam/op-shadow.js @@ -41,9 +41,22 @@ class OpShadow extends CamOp { } if (skipTerrain) { + let { stock } = settings; + let { center } = stock; + console.log("skipping terrain generation"); - shadowBase = [ newPolygon() ]; + shadowBase = [ newPolygon().centerRectangle(center, stock.x, stock.y) ]; tzindex = [ ]; + + state.shadow = { + base: shadowBase, // computed shadow union at base of part + holes: [], + skip: skipTerrain, + slices: [], // legacy / transitional + stack: [], // stack of shadow slices + }; + + return; } // distributed pre-fill shadow layer cache diff --git a/src/kiri/mode/cam/slice.js b/src/kiri/mode/cam/slice.js index 0b10a804..d06d2e10 100644 --- a/src/kiri/mode/cam/slice.js +++ b/src/kiri/mode/cam/slice.js @@ -150,6 +150,10 @@ export async function cam_slice(settings, widget, onupdate, ondone) { onupdate(null,null,msg); } + function shadowAt(z) { + return widget.shadowAt(z); + } + if (unsafe) { console.log("disabling overhang safeties"); } @@ -202,7 +206,7 @@ export async function cam_slice(settings, widget, onupdate, ondone) { setAxisIndex, setToolDiam, settings, - shadowAt(z) { return widget.shadowAt(z) }, + shadowAt, slicer, stock, tabs, From 9531fafd1d1149a11c58d9c70f8227985c901b62 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Mon, 5 Jan 2026 09:44:44 -0500 Subject: [PATCH 104/154] fix area op exit with empty z list --- src/kiri/app/init/input.js | 1 + src/kiri/mode/cam/op-area.js | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/kiri/app/init/input.js b/src/kiri/app/init/input.js index 53d23801..55d0c46a 100644 --- a/src/kiri/app/init/input.js +++ b/src/kiri/app/init/input.js @@ -75,6 +75,7 @@ function checkSeed(then) { setTimeout(() => { api.space.save(true) },500); then(); api.help.show(); + sdb[LAST] = version; }); return true; } diff --git a/src/kiri/mode/cam/op-area.js b/src/kiri/mode/cam/op-area.js index ee274dd0..528195be 100644 --- a/src/kiri/mode/cam/op-area.js +++ b/src/kiri/mode/cam/op-area.js @@ -169,6 +169,8 @@ class OpArea extends CamOp { let zinc = 1 / zs.length; let lzo; + if (!zs.length) break; + outer: for (;;) for (let z of zs) { let slice = newLayer(z); From 5ef26bef03a0039142e0f41821484f219162aeca Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Mon, 5 Jan 2026 11:21:21 -0500 Subject: [PATCH 105/154] rename sliceAll to slicePost for driver contract --- src/kiri/app/function.js | 4 ++-- src/kiri/app/workers.js | 4 ++-- src/kiri/mode/cam/driver-be.js | 1 + src/kiri/mode/cam/slice.js | 7 +++++++ src/kiri/mode/fdm/driver-be.js | 5 +++-- src/kiri/mode/fdm/slice.js | 11 ++++++++--- src/kiri/run/worker.js | 24 ++++++++++++------------ 7 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/kiri/app/function.js b/src/kiri/app/function.js index 0b3cb9e8..a2f69cc6 100644 --- a/src/kiri/app/function.js +++ b/src/kiri/app/function.js @@ -146,8 +146,8 @@ function prepareSlices(callback, scale = 1, offset = 0) { // while more widgets to slice sliceWidget(toSlice.shift()) } else { - // once all slicing done, run sliceAll() once for all widgets - client.sliceAll(settings, sliceDone); + // once all slicing done, run slicePost() once for all widgets + client.slicePost(settings, sliceDone); } } diff --git a/src/kiri/app/workers.js b/src/kiri/app/workers.js index 06503bdb..d8b2049a 100644 --- a/src/kiri/app/workers.js +++ b/src/kiri/app/workers.js @@ -237,8 +237,8 @@ export const client = { // called once after each widget is sliced // performs merged actions that consider all widget slice data - sliceAll(settings, callback) { - send("sliceAll", { settings }, callback); + slicePost(settings, callback) { + send("slicePost", { settings }, callback); }, // turn widget slices into unified path output diff --git a/src/kiri/mode/cam/driver-be.js b/src/kiri/mode/cam/driver-be.js index 35a2a9e4..7f850aef 100644 --- a/src/kiri/mode/cam/driver-be.js +++ b/src/kiri/mode/cam/driver-be.js @@ -116,6 +116,7 @@ export const CAM = { surface_find, slicePre: cam_slice_pre, slice: cam_slice, + slicePost: undefined, prepare: cam_prepare, export: cam_export }; diff --git a/src/kiri/mode/cam/slice.js b/src/kiri/mode/cam/slice.js index d06d2e10..5cd48143 100644 --- a/src/kiri/mode/cam/slice.js +++ b/src/kiri/mode/cam/slice.js @@ -14,6 +14,11 @@ import { util } from '../../../geo/base.js'; // state shared across all widget slicing let shared; +/** + * DRIVER SLICE CONTRACT + * + * global slice operation run before individual widget slice() operations + */ export function cam_slice_pre(settings) { shared = { ops: { } }; } @@ -21,6 +26,8 @@ export function cam_slice_pre(settings) { /** * DRIVER SLICE CONTRACT * + * slice operations run per-widget between slicePre and sliceAll + * * @param {Object} settings * @param {Widget} widget * @param {Function} output diff --git a/src/kiri/mode/fdm/driver-be.js b/src/kiri/mode/fdm/driver-be.js index 7a14bb39..3abdb04f 100644 --- a/src/kiri/mode/fdm/driver-be.js +++ b/src/kiri/mode/fdm/driver-be.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { util } from '../../../geo/base.js'; -import { fdm_slice, sliceAll, supports } from './slice.js'; +import { fdm_slice, slicePost, supports } from './slice.js'; import { fdm_prepare } from './prepare.js'; import { fdm_export } from './export.js'; import { codec } from '../../core/codec.js'; @@ -40,8 +40,9 @@ export const FDM = { init, extrudePerMM, extrudeMM, + slicePre: undefined, slice: fdm_slice, - sliceAll: sliceAll, + slicePost: slicePost, prepare: fdm_prepare, export: fdm_export }; diff --git a/src/kiri/mode/fdm/slice.js b/src/kiri/mode/fdm/slice.js index efe91db0..e549834d 100644 --- a/src/kiri/mode/fdm/slice.js +++ b/src/kiri/mode/fdm/slice.js @@ -19,7 +19,7 @@ let tracker = util.pwait, fat = 1.5, COLOR = { anchor: { check: 0x999933, face: 0x999933, line: 0x999933, opacity, lopacity, fat }, - fill: { check: 0x00bb77, face: 0x00bb77, line: 0x00bb77, opacity, lopacity, fat }, + fill: { check: 0x005588, face: 0x005588, line: 0x005588, opacity, lopacity, fat }, gaps: { check: 0xaa3366, face: 0xaa3366, line: 0xaa3366, opacity, lopacity, fat }, infill: { check: 0x3322bb, face: 0x3322bb, line: 0x3322bb, opacity, lopacity, fat }, inset: { line: 0xaaaaaa, check: 0xaaaaaa, face: 0 }, @@ -29,7 +29,7 @@ let tracker = util.pwait, thin: { check: 0xbb8800, face: 0xbb8800, line: 0xbb8800, opacity, lopacity, fat }, }, COLOR_DARK = Object.assign({}, COLOR, { - fill: { check: 0x008855, face: 0x008855, line: 0x008855, opacity, lopacity, fat }, + fill: { check: 0x005588, face: 0x005588, line: 0x005588, opacity, lopacity, fat }, infill: { check: 0x3322bb, face: 0x3322bb, line: 0x3322bb, opacity, lopacity, fat }, inset: { line: 0x555555, check: 0x555555 }, part: { line: 0x777777, check: 0x777777, face: 0 }, @@ -58,7 +58,12 @@ function vopt(opt) { return opt; } -export function sliceAll(settings, onupdate) { +/** + * DRIVER SLICE CONTRACT + * + * global slice operation run after all individual widget slice() complete + */ +export function slicePost(settings, onupdate) { // future home of brim and anchor generation let widgets = Object.values(self.kiri_worker.cache) .filter(w => !w.meta.disabled) diff --git a/src/kiri/run/worker.js b/src/kiri/run/worker.js index e5a5f974..9f394ddb 100644 --- a/src/kiri/run/worker.js +++ b/src/kiri/run/worker.js @@ -7,17 +7,17 @@ import '../../add/three.js'; import { base } from '../../geo/base.js'; import { codec } from '../core/codec.js'; import { consts } from '../core/consts.js'; -import { util } from '../../geo/base.js'; -import { newPoint } from '../../geo/point.js'; -import { polygons as POLY } from '../../geo/polygons.js'; -import { newPrint } from '../core/print.js'; -import { render } from '../core/render.js'; -import { wasm_ctrl } from '../../geo/wasm.js'; -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 { load } from "../../load/png.js"; +import { newPoint } from '../../geo/point.js'; +import { newPrint } from '../core/print.js'; +import { polygons as POLY } from '../../geo/polygons.js'; import { RasterPath } from '../../gpu/raster.js'; +import { render } from '../core/render.js'; +import { util } from '../../geo/base.js'; +import { version } from '../../moto/license.js'; +import { wasm_ctrl } from '../../geo/wasm.js'; +import { Widget, newWidget } from '../core/widget.js'; import { CAM } from '../mode/cam/driver-be.js'; import { DRAG } from '../mode/drag/driver.js'; @@ -482,13 +482,13 @@ const dispatch = { }); }, - sliceAll(data, send) { + slicePost(data, send) { const { settings } = data; const { mode } = settings; const driver = drivers[mode]; - if (driver.sliceAll) { - driver.sliceAll(settings, send.data); + if (driver.slicePost) { + driver.slicePost(settings, send.data); } send.done({done: true}); From 4043553012c61d3eff6ae2e0c3124d61a11b41b1 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Mon, 5 Jan 2026 11:51:12 -0500 Subject: [PATCH 106/154] start fdm support refactor --- src/kiri/app/conf/defaults.js | 8 +++++--- src/kiri/core/consts.js | 5 +++++ src/kiri/mode/fdm/init-menu.js | 21 ++++++++++++++------- web/kiri/lang/en.js | 24 ++++++++++++++---------- 4 files changed, 38 insertions(+), 20 deletions(-) diff --git a/src/kiri/app/conf/defaults.js b/src/kiri/app/conf/defaults.js index 0038ba8d..c178c642 100644 --- a/src/kiri/app/conf/defaults.js +++ b/src/kiri/app/conf/defaults.js @@ -340,15 +340,17 @@ export const conf = { sliceSupportAngle: 50, sliceSupportArea: 0.1, sliceSupportDensity: 0.1, - sliceSupportEnable: false, + // sliceSupportEnable: false, sliceSupportExtra: 0, sliceSupportGap: 1, - sliceSupportGrow: 0, + // sliceSupportGrow: 0, sliceSupportNozzle: 0, sliceSupportOffset: 1.0, sliceSupportOutline: true, - sliceSupportSize: 5, + // sliceSupportSize: 5, sliceSupportSpan: 5, + sliceSupportType: "automatic", + sliceSupportTree: false, sliceTopLayers: 3, sliceZInterleave: false, zHopDistance: 0.2, diff --git a/src/kiri/core/consts.js b/src/kiri/core/consts.js index 365f6a7c..a6491f68 100644 --- a/src/kiri/core/consts.js +++ b/src/kiri/core/consts.js @@ -23,6 +23,11 @@ const LISTS = { { name: "out-in" }, { name: "alternate" } ], + support: [ + { name: "disabled" }, + { name: "automatic" }, + { name: "manual" }, + ], start: [ { name: "last" }, { name: "center" }, diff --git a/src/kiri/mode/fdm/init-menu.js b/src/kiri/mode/fdm/init-menu.js index 88084ea8..c81bfdd5 100644 --- a/src/kiri/mode/fdm/init-menu.js +++ b/src/kiri/mode/fdm/init-menu.js @@ -61,6 +61,10 @@ function isNotBelt() { return !isBelt(); } +function isTree() { + return api.conf.proc().sliceSupportTree; +} + function notFwRetract() { return !api.conf.get().device.fwRetract; } @@ -107,23 +111,26 @@ export function menu() { outputFanSpeed: newInput(LANG.ou_fans_s, {title:LANG.ou_fans_l, convert:toInt, bound:bound(0,255)}), outputMinLayerTime: newInput(LANG.ou_layt_s, { title:LANG.ou_layt_l, convert:toInt, bound:bound(0,200) }), _____: newGroup(LANG.sp_menu, $('fdm-support'), { modes:FDM, driven, hideable, separator, group:"fdm-supp" }), + sliceSupportType: newSelect(LANG.sp_type_s, {title:LANG.sp_type_l}, "support"), sliceSupportNozzle: newSelect(LANG.sp_nozl_s, {title:LANG.sp_nozl_l, show:isMultiHead}, "extruders"), + separator: newBlank({ class:"set-sep", driven }), + sliceSupportAngle: newInput(LANG.sp_angl_s, {title:LANG.sp_angl_l, convert:toFloat, bound:bound(0.0,90.0)}), sliceSupportDensity: newInput(LANG.sp_dens_s, {title:LANG.sp_dens_l, convert:toFloat, bound:bound(0.0,1.0)}), - sliceSupportSize: newInput(LANG.sp_size_s, {title:LANG.sp_size_l, convert:toFloat, bound:bound(1.0,200.0)}), - sliceSupportOffset: newInput(LANG.sp_offs_s, {title:LANG.sp_offs_l, convert:toFloat, bound:bound(0.0,200.0)}), + // sliceSupportSize: newInput(LANG.sp_size_s, {title:LANG.sp_size_l, convert:toFloat, bound:bound(1.0,200.0)}), sliceSupportGap: newInput(LANG.sp_gaps_s, {title:LANG.sp_gaps_l, convert:toInt, bound:bound(0,5)}), + sliceSupportOffset: newInput(LANG.sp_offs_s, {title:LANG.sp_offs_l, convert:toFloat, bound:bound(0.0,200.0)}), sliceSupportArea: newInput(LANG.sp_area_s, {title:LANG.sp_area_l, convert:toFloat, bound:bound(0.0,200.0)}), sliceSupportExtra: newInput(LANG.sp_xpnd_s, {title:LANG.sp_xpnd_l, convert:toFloat, bound:bound(0.0,10.0)}), - sliceSupportGrow: newInput(LANG.sp_grow_s, {title:LANG.sp_grow_l, convert:toFloat, bound:bound(0.0,10.0)}), - sliceSupportAngle: newInput(LANG.sp_angl_s, {title:LANG.sp_angl_l, convert:toFloat, bound:bound(0.0,90.0)}), + // sliceSupportGrow: newInput(LANG.sp_grow_s, {title:LANG.sp_grow_l, convert:toFloat, bound:bound(0.0,10.0)}), sliceSupportSpan: newInput(LANG.sp_span_s, {title:LANG.sp_span_l, convert:toFloat, bound:bound(0.0,200.0), show:() => ui.sliceSupportEnable.checked }), separator: newBlank({ class:"set-sep", driven }), - sliceSupportEnable: newBoolean(LANG.sp_auto_s, onBooleanClick, {title:LANG.sp_auto_l, show:isNotBelt}), - sliceSupportOutline: newBoolean(LANG.sp_outl_s, onBooleanClick, {title:LANG.sp_outl_l}), + sliceSupportTree: newBoolean(LANG.sp_tree_s, onBooleanClick, {title:LANG.sp_tree_s, trigger, show:isNotBelt}), + // sliceSupportEnable: newBoolean(LANG.sp_auto_s, onBooleanClick, {title:LANG.sp_auto_l, show:isNotBelt}), + sliceSupportOutline: newBoolean(LANG.sp_outl_s, onBooleanClick, {title:LANG.sp_outl_l, show: () => !isTree() }), separator: newBlank({ class:"set-sep", driven }), sliceSupportGen: newRow([ ui.ssaGen = newButton(LANG.sp_detect, onButtonClick, {class: "f-col grow a-center"}) - ], { modes: FDM, class: "ext-buttons f-row grow" }), + ], { modes: FDM, class: "ext-buttons f-row grow", show: () => !ui.sliceSupportTree.checked }), separator: newBlank({ class:"set-sep", driven }), sliceSupportManual: newRow([ (ui.ssmAdd = newButton(undefined, onButtonClick, {icon:''})), diff --git a/web/kiri/lang/en.js b/web/kiri/lang/en.js index 150fe28b..56a8f511 100644 --- a/web/kiri/lang/en.js +++ b/web/kiri/lang/en.js @@ -464,30 +464,34 @@ self.lang['en-us'] = { // FDM SUPPORT sp_menu: "support", sp_detect: "detect", + sp_type_s: "type", + sp_type_l: ["automatic = detect","manual = paint on"], sp_dens_s: "density", sp_dens_l: ["fraction 0.0 - 1.0","recommended 0.15","0 to disable"], - sp_size_s: "pillar size", - sp_size_l: ["pillar width in millimeters","smaller values exponentially","increase detection times"], + // sp_size_s: "pillar size", + // sp_size_l: ["pillar width in millimeters","smaller values exponentially","increase detection times"], sp_offs_s: "part offset", sp_offs_l: ["offset from part","in millimeters"], - sp_gaps_s: "gap layers", + sp_gaps_s: "layer gap", sp_gaps_l: ["number of layers","offset from part"], sp_span_s: "max span", sp_span_l: ["unsupported span that causes","a new support to be generated","in millimeters"], - sp_angl_s: "max angle", + sp_angl_s: "angle", sp_angl_l: ["max overhang angle before","supporting pillar is generated","normal values 45-60","belt values 0-10"], sp_area_s: "min area", sp_area_l: ["minimum area for","a support column","in millimeters","smaller values will","slow detection"], sp_xpnd_s: "expand", sp_xpnd_l: ["expand support area","beyond part boundary","in millimeters"], - sp_grow_s: "grow", - sp_grow_l: ["grow support column to fill small gaps","in millimeters"], - sp_nozl_s: "extruder", - sp_nozl_l: ["in multi-extruder systems","the extruder to use for","support material"], + // sp_grow_s: "grow", + // sp_grow_l: ["grow support column to fill small gaps","in millimeters"], + sp_nozl_s: "tool", + sp_nozl_l: ["in multi-extruder systems","the toolhead to use for","support material"], sp_outl_s: "enclosed", sp_outl_l: ["enclose support pattern","inside a perimeter shell"], - sp_auto_s: "automatic", - sp_auto_l: ["enable generated supports","using at-slice-time geometry","supports will only appear","after slicing completes"], + // sp_auto_s: "automatic", + // sp_auto_l: ["enable generated supports","using at-slice-time geometry","supports will only appear","after slicing completes"], + sp_tree_s: "tree type", + sp_tree_l: ["use tree type support structure"], // CNC COMMON terms cc_tool: "tool", From 9e816dd14b1cd6a022473c75f7d962793d44e99b Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Mon, 5 Jan 2026 15:33:29 -0500 Subject: [PATCH 107/154] reduce edges recalculation when enabled --- src/kiri/app/widget.js | 13 +++++++++---- src/kiri/core/widget.js | 18 ++++++++++-------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/kiri/app/widget.js b/src/kiri/app/widget.js index b55dd548..b27309fc 100644 --- a/src/kiri/app/widget.js +++ b/src/kiri/app/widget.js @@ -130,10 +130,16 @@ class Widget extends WidgetCore { } } - /** - * Set edge rendering - */ setEdges(set) { + clearTimeout(this._setimer); + if (set === true && this.outline) { + this._setimer = setTimeout(() => this._setEdges(set), 10); + } else { + this._setEdges(set); + } + } + + _setEdges(set) { if (!(this.api && this.api.conf)) { // missing api features in engine mode return; @@ -145,7 +151,6 @@ class Widget extends WidgetCore { if (this.outline) { mesh.remove(this.outline); this.outline = null; - } if (set) { let dark = this.api.space.is_dark(); diff --git a/src/kiri/core/widget.js b/src/kiri/core/widget.js index 90ea0625..57f5e100 100644 --- a/src/kiri/core/widget.js +++ b/src/kiri/core/widget.js @@ -442,12 +442,10 @@ class Widget { } _updateMeshPosition() { - let mesh = this.mesh, - track = this.track, - tzoff = track.tzoff, - top = track.top, - tz = -tzoff, - { x, y, z } = track.pos; + let { cache, mesh, track } = this, + { top, tzoff } = track, + { x, y, z } = track.pos, + tz = -tzoff; if (track.indexed) { this.mesh.rotation.x = -track.indexRad; z = top; @@ -455,8 +453,12 @@ class Widget { this.mesh.rotation.x = 0; z += tz; } - mesh.position.set(x, y, z); - this._updateEdges(); + let { pos } = cache; + if (!pos || pos.x !== x || pos.y !==y || pos.z !== z) { + mesh.position.set(x, y, z); + this._updateEdges(); + cache.pos = { x, y, z }; + } } _updateEdges() { From dff3cf9b1eb90555995b6868895fdebc129d2f85 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Mon, 5 Jan 2026 20:09:00 -0500 Subject: [PATCH 108/154] checkpoint material painting --- src/kiri/app/init/input.js | 4 +- src/kiri/app/mouse.js | 10 +- src/kiri/app/paint.js | 118 ++++++-- src/kiri/app/widget.js | 4 +- src/kiri/app/workers.js | 16 +- src/kiri/app/workspace.js | 2 +- src/kiri/core/widget.js | 37 +-- src/kiri/mode/fdm/driver-be.js | 20 +- src/kiri/mode/fdm/init-menu.js | 14 +- src/kiri/mode/fdm/init-ui.js | 486 ++++++--------------------------- src/kiri/mode/fdm/post.js | 20 +- src/kiri/mode/fdm/prepare.js | 36 +-- src/kiri/mode/fdm/slice.js | 205 +------------- src/moto/space.js | 15 +- 14 files changed, 271 insertions(+), 716 deletions(-) diff --git a/src/kiri/app/init/input.js b/src/kiri/app/init/input.js index 55d0c46a..6b4c881a 100644 --- a/src/kiri/app/init/input.js +++ b/src/kiri/app/init/input.js @@ -177,7 +177,7 @@ export function init_input() { space.sky.showGrid(false); space.sky.setColor(controller.dark ? 0 : 0xffffff); space.setAntiAlias(controller.antiAlias); - space.init(container, function (delta) { + space.init(container, (delta) => { const { lo, hi, max } = slider.getRange(); if (max === 0 || !delta) return; if (controller.reverseZoom) delta = -delta; @@ -471,7 +471,7 @@ export function init_input() { DOC.body.requestFullscreen(); } catch (e) { event.emit('resize'); - moto.space.event.onResize(); + space.event.onResize(); } }; diff --git a/src/kiri/app/mouse.js b/src/kiri/app/mouse.js index d4807d6c..836f19c9 100644 --- a/src/kiri/app/mouse.js +++ b/src/kiri/app/mouse.js @@ -113,10 +113,7 @@ class InteractionControl { this.#space.mouse.downSelect((int, event) => { // Feature hook for custom mouse down handling if (this.#api.feature.on_mouse_down) { - if (int) { - this.#api.feature.on_mouse_down(int, event); - return; - } + return this.#api.feature.on_mouse_down(int, event); } // Hover mode handling @@ -178,9 +175,10 @@ class InteractionControl { }); // Drag handler - this.#space.mouse.onDrag((delta, offset, up = false) => { + this.#space.mouse.onDrag((delta, offset, up = false, int) => { if (this.#api.feature.hover) { - return; + return this.#api.feature?.on_mouse_drag({ delta, offset, up, int }); + // return; } if (up) { diff --git a/src/kiri/app/paint.js b/src/kiri/app/paint.js index 93abbe33..367fffe2 100644 --- a/src/kiri/app/paint.js +++ b/src/kiri/app/paint.js @@ -71,21 +71,45 @@ import { THREE } from '../../ext/three.js'; * @returns {THREE.Material} The modified material */ function addPaintOverlaySimple(material, paintPoints, paintColor = new THREE.Color(0x4488ff)) { + // Initialize userData if it doesn't exist + if (!material.userData) { + material.userData = {}; + } + + // Store initial paint points reference + material.userData.paintPoints = paintPoints; + material.userData.paintColor = paintColor; + material.onBeforeCompile = (shader) => { // Store shader reference for later updates material.userData.shader = shader; + // Use updated paint points if they were modified before first compile + const currentPoints = material.userData.paintPoints || paintPoints; + // Convert paint points to vec4 array (x, y, z, radius) - const points = paintPoints.map(p => - new THREE.Vector4(p.point.x, p.point.y, p.point.z, p.radius) - ); + // Filter out invalid entries + const validPoints = currentPoints + .filter(p => p && p.point && typeof p.radius === 'number') + .map(p => new THREE.Vector4(p.point.x, p.point.y, p.point.z, p.radius)); + + // Pad array to exactly 256 elements (shader uniform array size) + // THREE.js requires the array length to match the shader declaration + const points = new Array(256); + for (let i = 0; i < 256; i++) { + points[i] = i < validPoints.length ? validPoints[i] : new THREE.Vector4(0, 0, 0, 0); + } + + console.log('[Paint Shader] onBeforeCompile called with', validPoints.length, 'valid points'); + console.log('[Paint Shader] First point:', currentPoints[0]); // Add uniforms for paint data shader.uniforms.paintPoints = { value: points }; - shader.uniforms.paintCount = { value: points.length }; + shader.uniforms.paintCount = { value: validPoints.length }; shader.uniforms.paintColor = { value: paintColor }; - // Pass world position from vertex shader to fragment shader + // Pass model position from vertex shader to fragment shader + // Paint points are in widget/model space, so we compare against transformed (model space) shader.vertexShader = shader.vertexShader.replace( `#include `, ` @@ -128,9 +152,14 @@ function addPaintOverlaySimple(material, paintPoints, paintColor = new THREE.Col } if (painted) { - // Blend paint color with existing fragment color - gl_FragColor.rgb = mix(gl_FragColor.rgb, paintColor, 0.5); + // Blend paint color with existing fragment color (increased to 0.7 for visibility) + gl_FragColor.rgb = mix(gl_FragColor.rgb, paintColor, 0.7); } + + // DEBUG: Uncomment to tint entire mesh when shader is active (proves shader is running) + // if (paintCount > 0) { + // gl_FragColor.rgb += vec3(0.3, 0.0, 0.0); // Add red tint to entire mesh + // } ` ); }; @@ -154,21 +183,36 @@ function addPaintOverlaySimple(material, paintPoints, paintColor = new THREE.Col * @returns {THREE.Material} The modified material */ function addPaintOverlayTexture(material, paintPoints, paintColor = new THREE.Color(0x4488ff)) { + // Initialize userData if it doesn't exist + if (!material.userData) { + material.userData = {}; + } + + // Store initial paint points reference + material.userData.paintPoints = paintPoints; + material.userData.paintColor = paintColor; + material.onBeforeCompile = (shader) => { // Store shader reference for later updates material.userData.shader = shader; + // Use updated paint points if they were modified before first compile + const currentPoints = material.userData.paintPoints || paintPoints; + + // Filter out invalid entries + const validPoints = currentPoints.filter(p => p && p.point && typeof p.radius === 'number'); + // Create data texture from paint points // Texture is laid out as square grid: width x height >= point count - const pointCount = paintPoints.length; + const pointCount = validPoints.length; const texWidth = Math.ceil(Math.sqrt(pointCount)); const texHeight = Math.ceil(pointCount / texWidth); const data = new Float32Array(texWidth * texHeight * 4); // Pack paint points into texture data // Each point = 4 floats (x, y, z, radius) - for (let i = 0; i < paintPoints.length; i++) { - const p = paintPoints[i]; + for (let i = 0; i < validPoints.length; i++) { + const p = validPoints[i]; data[i * 4 + 0] = p.point.x; data[i * 4 + 1] = p.point.y; data[i * 4 + 2] = p.point.z; @@ -191,7 +235,8 @@ function addPaintOverlayTexture(material, paintPoints, paintColor = new THREE.Co shader.uniforms.paintTexSize = { value: new THREE.Vector2(texWidth, texHeight) }; shader.uniforms.paintColor = { value: paintColor }; - // Pass world position from vertex shader to fragment shader + // Pass model position from vertex shader to fragment shader + // Paint points are in widget/model space, so we compare against transformed (model space) shader.vertexShader = shader.vertexShader.replace( `#include `, ` @@ -245,9 +290,14 @@ function addPaintOverlayTexture(material, paintPoints, paintColor = new THREE.Co } if (painted) { - // Blend paint color with existing fragment color - gl_FragColor.rgb = mix(gl_FragColor.rgb, paintColor, 0.5); + // Blend paint color with existing fragment color (increased to 0.7 for visibility) + gl_FragColor.rgb = mix(gl_FragColor.rgb, paintColor, 0.7); } + + // DEBUG: Uncomment to tint entire mesh when shader is active (proves shader is running) + // if (paintCount > 0) { + // gl_FragColor.rgb += vec3(0.3, 0.0, 0.0); // Add red tint to entire mesh + // } ` ); }; @@ -282,16 +332,32 @@ function addPaintOverlayAuto(material, paintPoints, paintColor = new THREE.Color * IMPORTANT: The material must have already been initialized with one of the * addPaintOverlay functions before calling this update function. * + * NOTE: If the shader hasn't been compiled yet (first render not complete), + * this will trigger a material update to force recompilation with new points. + * * @param {THREE.Material} material - Material with paint shader already applied * @param {Array<{point: {x,y,z}, radius: number}>} paintPoints - Updated array of paint spheres */ function updatePaintOverlay(material, paintPoints) { + // Initialize userData if it doesn't exist + if (!material.userData) { + material.userData = {}; + } + + // Store paint points reference in userData for access during shader compilation + if (!material.userData.paintPoints) { + material.userData.paintPoints = paintPoints; + } + // Access the compiled shader uniforms // After onBeforeCompile runs once, userData.shader contains the compiled program const shader = material.userData?.shader; if (!shader || !shader.uniforms) { - console.warn('updatePaintOverlay: material does not have compiled paint shader'); + // Shader hasn't compiled yet (first render not complete) + // Store the new points and trigger recompile + material.userData.paintPoints = paintPoints; + material.needsUpdate = true; return; } @@ -315,13 +381,20 @@ function updatePaintOverlaySimple(shader, paintPoints) { const uniforms = shader.uniforms; // Convert paint points to vec4 array (x, y, z, radius) - const points = paintPoints.map(p => - new THREE.Vector4(p.point.x, p.point.y, p.point.z, p.radius) - ); + // Filter out invalid entries + const validPoints = paintPoints + .filter(p => p && p.point && typeof p.radius === 'number') + .map(p => new THREE.Vector4(p.point.x, p.point.y, p.point.z, p.radius)); + + // Pad array to exactly 256 elements (shader uniform array size) + const points = new Array(256); + for (let i = 0; i < 256; i++) { + points[i] = i < validPoints.length ? validPoints[i] : new THREE.Vector4(0, 0, 0, 0); + } // Update uniform values uniforms.paintPoints.value = points; - uniforms.paintCount.value = points.length; + uniforms.paintCount.value = validPoints.length; // Note: No need to set needsUpdate on the material // Uniform updates are automatically detected by THREE.js @@ -334,15 +407,18 @@ function updatePaintOverlaySimple(shader, paintPoints) { function updatePaintOverlayTexture(shader, paintPoints) { const uniforms = shader.uniforms; + // Filter out invalid entries + const validPoints = paintPoints.filter(p => p && p.point && typeof p.radius === 'number'); + // Recreate texture with new paint points - const pointCount = paintPoints.length; + const pointCount = validPoints.length; const texWidth = Math.ceil(Math.sqrt(pointCount)); const texHeight = Math.ceil(pointCount / texWidth); const data = new Float32Array(texWidth * texHeight * 4); // Pack paint points into texture data - for (let i = 0; i < paintPoints.length; i++) { - const p = paintPoints[i]; + for (let i = 0; i < validPoints.length; i++) { + const p = validPoints[i]; data[i * 4 + 0] = p.point.x; data[i * 4 + 1] = p.point.y; data[i * 4 + 2] = p.point.z; diff --git a/src/kiri/app/widget.js b/src/kiri/app/widget.js index b27309fc..9aae365f 100644 --- a/src/kiri/app/widget.js +++ b/src/kiri/app/widget.js @@ -49,11 +49,11 @@ class Widget extends WidgetCore { } const widget = this; index().put('ws-save-'+this.id, { + anno: this.annotations(), geo: widget.getGeoVertices({ unroll: false }).slice(), - track: widget.track, group: this.group.id, meta: this.meta, - anno: this.annotations() + track: widget.track }, result => { widget.meta.saved = Date.now(); if (ondone) ondone(); diff --git a/src/kiri/app/workers.js b/src/kiri/app/workers.js index d8b2049a..808a654b 100644 --- a/src/kiri/app/workers.js +++ b/src/kiri/app/workers.js @@ -19,7 +19,7 @@ let debug = self.debug === true, poolpath; /** - * @param {Function} fn name of function in kiri.worker + * @param {Function} fn name of function in worker * @param {Object} data to send to server * @param {Function} onreply function to call on reply messages * @param {Object[]} zerocopy array of objects to pass using zerocopy @@ -31,14 +31,17 @@ function send(fn, data, onreply, zerocopy) { // track it only when we expect and can handle a reply running[seq] = { fn:onreply }; } + let msg = { seq: seq, task: fn, time: time(), data: data }; + // console.log('client send', msg); // if (!fn) { console.trace({empty_fn: data}) }; + try { worker.postMessage(msg, zerocopy); } catch (error) { @@ -46,10 +49,19 @@ function send(fn, data, onreply, zerocopy) { } } +// Promise wrapper around send() +function sendp(fn, data, zerocopy) { + return new Promise(resolve => { + send(fn, data, resolve, zerocopy); + }); +} + // code is running in the browser / client context export const client = { send, + sendp, + setWorkPath(path) { workpath = path; return client; @@ -269,7 +281,7 @@ export const client = { ondone(null, reply.error); } if (reply.debug) { - api.event.emit("export.debug", reply.debug); + console.log('worker debug', reply.debug); } }); }, diff --git a/src/kiri/app/workspace.js b/src/kiri/app/workspace.js index 92dd3d4b..faae03d9 100644 --- a/src/kiri/app/workspace.js +++ b/src/kiri/app/workspace.js @@ -115,7 +115,7 @@ function save(quiet) { api.conf.save(); const newWidgets = []; const oldWidgets = js2o(SDB.getItem('ws-widgets'), []); - api.widgets.each(function(widget) { + api.widgets.each(widget => { if (widget.synth) return; newWidgets.push(widget.id); oldWidgets.remove(widget.id); diff --git a/src/kiri/core/widget.js b/src/kiri/core/widget.js index 57f5e100..d7791888 100644 --- a/src/kiri/core/widget.js +++ b/src/kiri/core/widget.js @@ -23,6 +23,7 @@ class Widget { this.id = id || Date.now().toString(36)+(nextId++); this.grouped = group ? true : false; + // persisted this.group = group || []; this.group.push(this); if (!this.group.id) { @@ -33,10 +34,8 @@ class Widget { } // rotation stack (for undo) this.roto = []; - // added meshes (supports, tabs, etc) + // overlay meshes (supports, tabs, etc) this.adds = []; - // persisted client annotations (cam tabs, fdm supports) - this.anno = {}; // THREE Mesh and points this.mesh = null; this.points = null; @@ -48,6 +47,24 @@ class Widget { this.settings = null; // used?? this.modified = true; this.boundingBoxNeedsUpdate = true + // cache shadow geo + this.cache = {}; + this.stats = { + slice_time: 0, + load_time: 0, + progress: 0 + }; + // if this is a synthesized support widget + this.support = false; + // persisted: client annotations (cam tabs, fdm supports) + this.anno = {}; + // persisted: file meta-data + this.meta = { + url: null, + file: null, + saved: false + }; + // persisted: location state this.track = { // box size for packer box: { @@ -74,21 +91,7 @@ class Widget { mirror: false, indexed: false, indexRad: 0 - }, - // used to cache shadow geo - this.cache = {}; - this.stats = { - slice_time: 0, - load_time: 0, - progress: 0 }; - this.meta = { - url: null, - file: null, - saved: false - }; - // if this is a synthesized support widget - this.support = false; } annotations() { diff --git a/src/kiri/mode/fdm/driver-be.js b/src/kiri/mode/fdm/driver-be.js index 3abdb04f..8f308c12 100644 --- a/src/kiri/mode/fdm/driver-be.js +++ b/src/kiri/mode/fdm/driver-be.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { util } from '../../../geo/base.js'; -import { fdm_slice, slicePost, supports } from './slice.js'; +import { fdm_slice, slicePost } from './slice.js'; import { fdm_prepare } from './prepare.js'; import { fdm_export } from './export.js'; import { codec } from '../../core/codec.js'; @@ -24,15 +24,15 @@ function extrudeMM(dist, perMM, factor) { // defer loading until client and worker exist function init(worker) { - worker.dispatch.fdm_support_generate = function(data, send) { - const { settings } = data; - const widgets = Object.values(worker.cache); - const fresh = widgets.filter(widget => supports(settings, widget)); - send.done(codec.encode(fresh.map(widget => { return { - id: widget.id, - supports: widget.supports, - } } ))); - }; + // worker.dispatch.fdm_support_generate = function(data, send) { + // const { settings } = data; + // const widgets = Object.values(worker.cache); + // const fresh = widgets.filter(widget => supports(settings, widget)); + // send.done(codec.encode(fresh.map(widget => { return { + // id: widget.id, + // supports: widget.supports, + // } } ))); + // }; } diff --git a/src/kiri/mode/fdm/init-menu.js b/src/kiri/mode/fdm/init-menu.js index c81bfdd5..9dc84e2f 100644 --- a/src/kiri/mode/fdm/init-menu.js +++ b/src/kiri/mode/fdm/init-menu.js @@ -41,6 +41,10 @@ function optSelected(sel) { return opt ? opt.value : undefined; } +function manualSupport() { + return api.conf.proc().sliceSupportType === 'manual'; +} + function hasInfill() { return optSelected(ui.sliceFillType) !== 'none' } @@ -111,7 +115,7 @@ export function menu() { outputFanSpeed: newInput(LANG.ou_fans_s, {title:LANG.ou_fans_l, convert:toInt, bound:bound(0,255)}), outputMinLayerTime: newInput(LANG.ou_layt_s, { title:LANG.ou_layt_l, convert:toInt, bound:bound(0,200) }), _____: newGroup(LANG.sp_menu, $('fdm-support'), { modes:FDM, driven, hideable, separator, group:"fdm-supp" }), - sliceSupportType: newSelect(LANG.sp_type_s, {title:LANG.sp_type_l}, "support"), + sliceSupportType: newSelect(LANG.sp_type_s, {title:LANG.sp_type_l, trigger}, "support"), sliceSupportNozzle: newSelect(LANG.sp_nozl_s, {title:LANG.sp_nozl_l, show:isMultiHead}, "extruders"), separator: newBlank({ class:"set-sep", driven }), sliceSupportAngle: newInput(LANG.sp_angl_s, {title:LANG.sp_angl_l, convert:toFloat, bound:bound(0.0,90.0)}), @@ -127,16 +131,16 @@ export function menu() { sliceSupportTree: newBoolean(LANG.sp_tree_s, onBooleanClick, {title:LANG.sp_tree_s, trigger, show:isNotBelt}), // sliceSupportEnable: newBoolean(LANG.sp_auto_s, onBooleanClick, {title:LANG.sp_auto_l, show:isNotBelt}), sliceSupportOutline: newBoolean(LANG.sp_outl_s, onBooleanClick, {title:LANG.sp_outl_l, show: () => !isTree() }), - separator: newBlank({ class:"set-sep", driven }), + separator: newBlank({ class:"set-sep", driven, show:manualSupport }), sliceSupportGen: newRow([ ui.ssaGen = newButton(LANG.sp_detect, onButtonClick, {class: "f-col grow a-center"}) - ], { modes: FDM, class: "ext-buttons f-row grow", show: () => !ui.sliceSupportTree.checked }), - separator: newBlank({ class:"set-sep", driven }), + ], { modes: FDM, class: "ext-buttons f-row grow", show:manualSupport }), + // separator: newBlank({ class:"set-sep", driven }), sliceSupportManual: newRow([ (ui.ssmAdd = newButton(undefined, onButtonClick, {icon:''})), (ui.ssmDun = newButton(undefined, onButtonClick, {icon:''})), (ui.ssmClr = newButton(undefined, onButtonClick, {icon:''})) - ], {class:"ext-buttons f-row"}), + ], {class:"ext-buttons f-row", show:manualSupport}), _____: newGroup(LANG.fl_menu, $('fdm-base'), { modes:FDM, driven, hideable, separator, group:"fdm-base" }), firstSliceHeight: newInput(LANG.fl_lahi_s, {title:LANG.fl_lahi_l, convert:toFloat, show:isNotBelt}), firstLayerNozzleTemp:newInput(LANG.fl_nozl_s, {title:LANG.fl_nozl_l, convert:toInt, show:isNotBelt}), diff --git a/src/kiri/mode/fdm/init-ui.js b/src/kiri/mode/fdm/init-ui.js index f7e4d0ff..60accf7b 100644 --- a/src/kiri/mode/fdm/init-ui.js +++ b/src/kiri/mode/fdm/init-ui.js @@ -3,24 +3,16 @@ import { $ } from '../../../moto/webui.js'; import { api } from '../../app/api.js'; import { conf } from '../../app/conf/defaults.js'; -import { util } from '../../../geo/base.js'; -import { client } from '../../app/workers.js'; -import { addbox, delbox, getlastbox } from '../../core/boxes.js'; -import { Layers } from '../../app/layers.js'; -import { Stack } from '../../app/stack.js'; -import { Widget, newWidget } from '../../app/widget.js'; -import { space } from '../../../moto/space.js'; +import { addPaintOverlayTexture, updatePaintOverlay } from '../../app/paint.js'; const { Matrix4, Vector3, BufferAttribute, BufferGeometryUtils, Raycaster, Euler } = THREE; const { VIEWS } = api.const; const LANG = api.language.current; -let lastBox, lastMode, lastView, lastPillar, fromPillar, +let lastView, addingSupports = false, isFdmMode = false, - nextID = Date.now(), - p1, p2, iw, alert = [], func = {}; @@ -80,16 +72,6 @@ export function init() { } } - function filterSynth() { - api.widgets.filter((widget) => { - if (widget.track.synth) { - space.world.remove(widget.mesh); - Widget.Groups.remove(widget); - } - return !widget.track.synth - }); - } - function clearRanges() { api.conf.get().process.ranges = []; ui.fdmRanges.innerHTML = ''; @@ -140,19 +122,15 @@ export function init() { api.event.on("mode.set", mode => { isFdmMode = mode === 'FDM'; - lastMode = mode; // hide/show fdm mode elements for (let el of [...document.getElementsByClassName('mode-fdm')]) { api.uc.setClass(el, 'hide', !isFdmMode); } - updateSupportVisiblity(); }); api.event.on("view.set", view => { $('top-slider').style.display = 'none'; lastView = view; - updateSupportVisiblity(); - filterSynth(); func.sdone(); // let ranges = api.conf.get().process.ranges; if (isFdmMode) { @@ -174,7 +152,6 @@ export function init() { api.event.on("settings.load", (settings) => { if (settings.mode !== 'FDM') return; - restoreSupports(api.widgets.all()); updateRanges(settings.process.ranges); }); @@ -191,96 +168,89 @@ export function init() { } }); - api.event.on("fdm.supports.detect", func.sgen = () => { + // auto-detect supports + func.sgen = () => { let alerts = []; - let { process, device } = api.conf.get(); - if (!device.bedBelt) { - if (process.sliceSupportAngle < 10) { - alerts.push(api.show.alert("angles below 10 degrees may fail")); - } - } alerts.push(api.show.alert("analyzing part(s)...", 1000)); - support_generate(array => { - func.sclear(); - api.hide.alert(undefined,alerts); - for (let rec of array) { - let { widget, supports } = rec; - let wa = api.widgets.annotate(widget.id); - let ws = wa.support || []; - for (let support of supports) { - let { from, to, mid } = support; - let dw = api.conf.get().process.sliceSupportSize / 2; - let dh = from.z - to.z; - let rec = { - x: mid.x, - y: mid.y, - z: mid.z, - dw, - dh, - id: Math.random() * 0xffffffffff - }; - addWidgetSupport(widget, rec); - ws.push(Object.clone(rec)); - } - wa.support = ws; - } - api.event.emit("fdm.supports.detected"); - }); - }); + setTimeout(() => api.hide.alert(undefined,alerts), 2000); + } - api.event.on("fdm.supports.add", func.sadd = () => { - alert = api.show.alert("[esc] key cancels support editing"); - api.feature.hover = addingSupports = true; - }); - - api.event.on("fdm.supports.done", func.sdone = () => { - delbox('intZ'); - delbox('intW'); - delbox('supp'); - api.hide.alert(alert); - api.feature.hover = addingSupports = false; - fromPillar = undefined; - }); - - api.event.on("fdm.supports.clear", func.sclear = () => { - func.sdone(); - if (clearAllWidgetSupports()) { - api.conf.save(); + // manual add supports + func.sadd = () => { + if (addingSupports) { + return func.sdone(); } - }); + alert = api.show.alert("[esc] key cancels support editing", 1000); + let down; + api.feature.hover = addingSupports = true; + api.feature.on_mouse_down = (int) => { + if (int) { + let { point } = int; + down = { point, widget: int.object.widget }; + } + return api.widgets.meshes(); + }; + api.feature.on_mouse_drag = ({ int }) => { + if (int?.length) { + func.sadd_point(int[0].point, down.widget); + } + return [ down.widget.mesh ]; + // return api.widgets.meshes(); + }; + api.widgets.each(w => { + let mat = w.cache.mat = w.mesh.material[0]; + mat = w.mesh.material[0] = mat.clone(); + mat.needsUpdate = true; + if (!w.anno.paint) { + w.anno.paint = []; + } + addPaintOverlayTexture(mat, w.anno.paint, new THREE.Color(0x4488ff)); + }) + api.space.update(); + } + + func.sadd_point = (point, widget) => { + let { x, y, z } = point; + let rec = { + point: { x, y:-z, z:y }, + radius: 2 + }; + // return console.log({ on, widget, point }); + let paint = widget.anno.paint; + paint.push(rec); + updatePaintOverlay(widget.mesh.material[0], paint); + api.space.update(); + // api.conf.save(); + + }; + + // manual add done + func.sdone = () => { + if (!addingSupports) { + return; + } + api.feature.hover = addingSupports = false; + api.feature.on_mouse_down = undefined; + api.feature.on_mouse_drag = undefined; + api.hide.alert(alert); + api.widgets.each(w => { + w.mesh.material[0] = w.cache.mat; + delete w.cache.mat; + }); + } + + // manual supports clear + func.sclear = () => { + func.sdone(); + api.widgets.each(w => clearWidgetSupports(w)); + api.conf.save(); + } api.event.on("slice.begin", () => { if (!isFdmMode) { return; } func.sdone(); - updateSupportVisiblity(); - // synth support widget for each widget group - // iow, create support meshes based on support widget annotation - for (let group of Widget.Groups.list()) { - let merge = group.filter(w => w.sups).map(w => Object.values(w.sups)).flat(); - if (!merge.length) { - continue; - } - let boxen = merge.map(m => { - let geo = m.box.geometry.clone(); - if (geo.index) geo = geo.toNonIndexed(); - return geo.translate(m.x, m.y, m.z); - }); - for (let box of boxen) { - // eliminate / normalize uv to allow other widget merge - box.setAttribute('uv',new BufferAttribute(new Float32Array(0), 3)); - } - let bbg = BufferGeometryUtils.mergeGeometries(boxen); - let sw = newWidget(null, group); - let fwp = group[0].track.pos; - sw.loadGeometry(bbg); - sw._move(fwp.x, fwp.y, fwp.z); - api.widgets.add(sw); - sw.track.synth = true; - sw.track.support = true; - space.world.add(sw.mesh); - } }); api.event.on("slice.end", () => { @@ -312,25 +282,12 @@ export function init() { if (!isFdmMode) { return; } - let ann = api.widgets.annotate(widget.id); - if (ann.support) { - for (let supp of Object.values(ann.support)) { - supp.id = Math.abs(Math.random() * 0xffffffffffff); - } - restoreSupports([widget]); - } }); api.event.on("widget.mirror", widget => { if (!isFdmMode) { return; } - let ann = api.widgets.annotate(widget.id); - let sups = ann.support || []; - sups.forEach(sup => { - let wsup = widget.sups[sup.id]; - wsup.box.position.x = wsup.x = sup.x = -sup.x; - }); }); api.event.on("widget.rotate", rot => { @@ -341,305 +298,30 @@ export function init() { if (x || y) { clearWidgetSupports(widget); } else { - let ann = api.widgets.annotate(widget.id); - let sups = ann.support || []; - sups.forEach(sup => { - let wsup = widget.sups[sup.id]; - let vc = new Vector3(sup.x, sup.y, sup.z); - let m4 = new Matrix4(); - m4 = m4.makeRotationFromEuler(new Euler(x || 0, y || 0, z || 0)); - vc.applyMatrix4(m4); - wsup.box.position.x = wsup.x = sup.x = vc.x; - wsup.box.position.y = wsup.y = sup.y = vc.y; - wsup.box.position.z = wsup.z = sup.z = vc.z; - }); + // todo rotate supports when it's only a Z rotation } }); api.event.on("mouse.hover.up", func.hoverup = (on = {}) => { - let { event } = on; - if (!isFdmMode) { + if (!(addingSupports && isFdmMode)) { return; } - if (!addingSupports) { - return; + let { object } = on; + if (object) { + let { point } = object; + let { widget } = object.object; + func.sadd_point(point, widget); } - delbox('supp'); - if (lastPillar) { - removeWidgetSupport(lastPillar.widget, lastPillar); - return; - } - lastBox = getlastbox(); - if (!(iw && lastBox)) return; - let { point, dim } = lastBox; - p1.y = Math.max(0, p1.y); - p2.y = Math.max(0, p2.y); - let rz = dim.rz; - let dh = dim.z; - let dw = api.conf.get().process.sliceSupportSize / 2; - let ip = iw.track.pos; - let wa = api.widgets.annotate(iw.id); - let ws = (wa.support = wa.support || []); - let id = ++nextID; - let rec = {x: point.x - ip.x, y: -point.z - ip.y, z: point.y, rz, dw, dh, id}; - if (fromPillar && event && event.shiftKey) { - let targets = api.widgets.meshes().append(space.internals().platform); - let from = fromPillar, - d = { - x: rec.x - from.x, - y: rec.y - from.y, - z: rec.z - from.z - }; - let dist = Math.sqrt(d.x*d.x + d.y*d.y + d.z*d.z); - let lerp = util.lerp(0,dist,dw).map(v => v/dist); - let angle = rec.rz = Math.atan2(d.y,d.x); - let fromrec = ws.filter(r => r.id === from.id)[0]; - if (fromrec) { - from.rz = fromrec.rz = angle; - removeWidgetSupport(iw, fromPillar); - ws.push(Object.clone(fromrec)); - addWidgetSupport(iw, fromrec); - } - lerp.pop(); - let seed = Math.random(); - for (let pct of lerp) { - let point = new Vector3(from.x + d.x * pct, from.z + d.z * pct, from.y + d.y * pct); - addSupportAtPoint(targets, point, ip, angle, seed++); - } - } - ws.push(Object.clone(rec)); - fromPillar = addWidgetSupport(iw, rec); - api.conf.save(); }); - function addSupportAtPoint(targets, point, ip, angle, id) { - let up = new Vector3(0,1,0) - let dn = new Vector3(0,-1,0) - let rp = new Vector3(point.x + ip.x, point.y, -point.z - ip.y); - let iup = new Raycaster(rp, up) - .intersectObjects(targets, false) - .filter(t => !t.object.pillar); - if (iup.length && iup.length % 2 === 0) { - let idn = new Raycaster(rp, dn) - .intersectObjects(targets, false) - .filter(t => !t.object.pillar); - if (idn.length) { - iw = iup[0].object.widget || iw; - let wa = api.widgets.annotate(iw.id); - let ws = (wa.support = wa.support || []); - let phi = iup[0].point; - let plo = idn[0].point; - let ply = Math.max(0, plo.y); - let mp = (phi.y + ply) / 2; - let dy = Math.abs(phi.y - ply); - let dw = api.conf.get().process.sliceSupportSize / 2; - let rec = { rz:angle, x:point.x, y:point.z, z:mp, dw, dh:dy, id }; - ws.push(Object.clone(rec)); - fromPillar = addWidgetSupport(iw, rec); - } - } - } - api.event.on("mouse.hover", data => { - if (!isFdmMode) { + if (!(addingSupports && isFdmMode)) { return; } - if (!addingSupports) { - return; - } - delbox('supp'); const { int, type, point, event } = data; - const pillar = int ? int.object.pillar : undefined; - if (lastPillar) { - lastPillar.box.material.color.r = 0; - lastPillar = null; - } - if (pillar) { - pillar.box.material.color.r = 0.5; - lastPillar = pillar; - if (event && (event.metaKey || event.ctrlKey)) { - return func.hoverup(); - } - return; - } - if (int && type === 'widget') { - iw = int.object.widget || iw; - } else { - iw = null; - } - p1 = point; - // looking up by default (assumes platform intersect) - let dir = new Vector3(0,1,0) - let ray = new Raycaster(point, dir); - let rz = int && int.face ? Math.atan2(int.face.normal.y, int.face.normal.x) : 0; - // when intersecting underside of object, project down - if (int && int.face && int.face.normal.z < -0.1) { - dir.y = -1; - } - let targets = api.widgets.meshes().append(space.internals().platform); - let i2 = ray.intersectObjects(targets, false) - .filter(t => !t.object.pillar) // eliminate other pillars - .filter(i => i.distance > 0.1); // false matches close to origin of ray - if (i2.length) { - p2 = i2[0].point; - iw = i2[0].object.widget || iw; - let p1y = Math.max(0, p1.y); - let p2y = Math.max(0, p2.y); - let hy = (p1y + p2y) / 2; - let dy = Math.abs(p1y - p2y); - let dw = api.conf.get().process.sliceSupportSize / 2; - addbox({x:p1.x, y:hy, z:p1.z}, 0x0000dd, 'supp', { - x:dw, y:dw, z:dy, rz - }); - } else { - lastBox = null; - } - if (event && event.altKey) { - return func.hoverup(); - } }); - - api.event.on("export.debug", msg => { - if (msg.arcQ && msg.arcQ.length > 2) { - if (xpdebug) { - xpdebug.destroy(); - xpdebug = undefined; - } - let poly = base.newPolygon().setOpen(); - for (let rec of msg.arcQ) { - rec.z += 1; - poly.addObj(rec); - } - - let layers = new Layers(); - let layer = layers.setLayer("arcq", { line: 0xff00ff, face: 0xff00ff, opacity: 1 }); - layer.addPoly(poly, {thin:true}); - - layer = layers.setLayer("arcc", { line: 0xff0000, face: 0xff0000, opacity: 1 }); - for (let center of msg.arcQ.center) { - center.z += 0.5; - layer.addPoly(base.newPolygon().centerCircle( - center, center.r, 20 - )); - } - - xpdebug = new Stack(space.world, false); - xpdebug.addLayers(layers); - xpdebug.setVisible(0,Infinity); - xpdebug.show(); - - space.refresh(); - - console.log(msg, poly); - } - }); -} - -let xpdebug; -let supsave = {}; -let suptimer; - -function support_generate(ondone) { - client.clear(); - client.sync(); - const settings = api.conf.get(); - const widgets = api.widgets.map(); - client.send("fdm_support_generate", { settings }, (gen) => { - if (gen && gen.error) { - api.show.alert('support generation canceled'); - return ondone([]); - } - for (let g of gen) { - g.widget = widgets[g.id]; - } - ondone(gen); - }); -}; - -function scheduleSupportSave(widget) { - clearTimeout(suptimer); - supsave[widget.id] = widget; - suptimer = setTimeout(() => { - for (let w of Object.values(supsave)) { - w.saveState(); - } - }, 50); -} - -export function restoreSupports(widgets) { - widgets.forEach(widget => { - const supports = api.widgets.annotate(widget.id).support || []; - supports.forEach(pos => { - addWidgetSupport(widget, pos); - }); - }); -} - -function addWidgetSupport(widget, pos) { - const { x, y, z, rz, dw, dh, id } = pos; - const sups = widget.sups = (widget.sups || {}); - // prevent duplicate restore from repeated settings load calls - if (!sups[id]) { - pos.box = addbox( - { x, y, z }, 0x0000dd, id, - { x:dw, y:dw, z:dh, rz }, { group: widget.mesh } - ); - pos.box.pillar = Object.assign({widget}, pos); - sups[id] = pos; - widget.adds.push(pos.box); - scheduleSupportSave(widget); - } - return sups[id]; -} - -function removeWidgetSupport(widget, rec) { - const { box, id } = rec; - widget.adds.remove(box); - widget.mesh.remove(box); - delete widget.sups[id]; - let sa = api.widgets.annotate(widget.id).support; - let ix = 0; - sa.forEach((rec,i) => { - if (rec.id === id) { - ix = i; - } - }); - sa.splice(ix,1); - api.conf.save(); - scheduleSupportSave(widget); - fromPillar = undefined; -} - -function updateSupportVisiblity() { - const visible = (lastMode === 'FDM' && lastView === VIEWS.ARRANGE); - for (let w of api.widgets.all()) { - setSupportVisiblity(w, visible); - } -} - -function setSupportVisiblity(widget, bool) { - Object.values(widget.sups || {}).forEach(support => { - support.box.visible = bool; - }); -} - -function clearAllWidgetSupports() { - let cleared = 0; - api.widgets.all().forEach(widget => { - cleared += clearWidgetSupports(widget); - }); - return cleared; } function clearWidgetSupports(widget) { - let cleared = 0; - Object.values(widget.sups || {}).forEach(support => { - widget.adds.remove(support.box); - widget.mesh.remove(support.box); - cleared++; - }); - widget.sups = {}; - delete api.widgets.annotate(widget.id).support; - scheduleSupportSave(widget); - return cleared; + widget.anno.paint = []; } diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index a6b0bec6..299c189c 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -19,7 +19,7 @@ slicer.slicePost.FDM = slicePost; export function slicePost(data, options) { const { groups, z } = data; const { post_args, useAssembly, zIndexes } = options; - const { isSynth, process, vaseMode } = post_args; + const { process, vaseMode } = post_args; const { compInner, compOuter, pump } = post_args; const { clipOffset, fillOffset, shellOffset } = post_args; @@ -60,20 +60,6 @@ export function slicePost(data, options) { data.tops = POLY.nest([...inner, ...outer]); } - if (isSynth) { - const process = post_args.process; - if (process.sliceSupportGrow > 0) { - // expand synth supports here so they can be clipped later - data.tops = POLY.expand(data.tops, process.sliceSupportGrow, data.z, []); - } - // do not shell synth widgets because - // they will be clipped against peers later - // which requires shelling post-clip - // but we still need to generate tops - delete data.groups; - return; - } - const index = zIndexes.indexOf(z); const range = getRangeParameters(process, index); // calculate fractional shells @@ -87,10 +73,10 @@ export function slicePost(data, options) { let trg = shellFrac > 0.5 ? 1 : parts - 1; sliceShells += rem >= trg ? 1 : 0; } - const count = isSynth ? 1 : sliceShells; + const count = sliceShells; const offset = shellOffset; const fillOff = fillOffset; - const thinType = isSynth ? undefined : process.sliceDetectThin; + const thinType = process.sliceDetectThin; const nutops = []; // co-locate shell processing with top generation in slicer for (let top of data.tops) { diff --git a/src/kiri/mode/fdm/prepare.js b/src/kiri/mode/fdm/prepare.js index 480c1b84..f3d3110e 100644 --- a/src/kiri/mode/fdm/prepare.js +++ b/src/kiri/mode/fdm/prepare.js @@ -9,7 +9,6 @@ import { newPolygon, Polygon } from '../../../geo/polygon.js'; import { polygons as POLY, fillArea } from '../../../geo/polygons.js'; import { newPrint } from '../../core/print.js'; import { newSlice } from '../../core/slice.js'; -import { newWidget } from '../../core/widget.js'; import { render } from '../../core/render.js'; import { getRangeParameters } from './driver.js'; @@ -116,7 +115,7 @@ export async function fdm_prepare(widgets, settings, update) { brims = POLY.expand(brims, -extra, zheight, null, 1); } - // if raft is specified + // create raft when specified if (useRaft) { let offset = newPoint(0,0,0), height = nozzle, @@ -217,39 +216,6 @@ export async function fdm_prepare(widgets, settings, update) { bounds.max.y = Math.max(bounds.max.y, bbounds.maxy); } - // synthesize support widgets when needed - // so that they can use a separate extruder - // compute zmin for belt purge towers - for (let widget of widgets.slice()) { - let sslices = []; - if (!widget.slices) { - console.log('invalid widget', widget); - continue; - } - for (let slice of widget.slices) { - zmin = Math.min(zmin, slice.z); - zmax = Math.max(zmax, slice.z); - if (!slice.supports) { - continue; - } - let sslice = newSlice(slice.z); - sslice.extruder = process.sliceSupportNozzle; - sslice.supports = slice.supports.slice(); - sslice.height = slice.height; - sslices.push(sslice); - } - if (sslices.length) { - let swidget = newWidget(null,widget.group); - swidget.slices = sslices; - swidget.support = true; - swidget.belt = widget.belt; - swidget.track = Object.clone(widget.track); - swidget.mesh = { widget: swidget, position: swidget.track.pos }; - widget.anno.extruder = process.sliceSupportNozzle; - widgets.push(swidget); - } - } - let lastPoly; let lastLayer; let lastOffset; diff --git a/src/kiri/mode/fdm/slice.js b/src/kiri/mode/fdm/slice.js index e549834d..d880e7a5 100644 --- a/src/kiri/mode/fdm/slice.js +++ b/src/kiri/mode/fdm/slice.js @@ -127,16 +127,14 @@ export function fdm_slice(settings, widget, onupdate, ondone) { { devel, assembly, threaded } = controller, isBelt = device.bedBelt, isBrick = devel && process.sliceZInterleave, - isSynth = widget.track.synth, - isSupport = widget.track.support, useAssembly = assembly, isConcurrent = threaded && minions.concurrent && !process.xray, topLayers = process.sliceTopLayers || 0, bottomLayers = process.sliceBottomLayers || 0, - vaseMode = process.sliceFillType === 'vase' && !isSynth, + vaseMode = process.sliceFillType === 'vase', metadata = widget.anno, maxtruder = Math.max(0, device.extruders.length - 1), - extruder = Math.min(maxtruder, parseInt(isSynth ? process.sliceSupportNozzle : metadata.extruder || 0)), + extruder = Math.min(maxtruder, parseInt(metadata.extruder || 0)), sliceHeight = process.sliceHeight, sliceHeightBase = (isBelt ? sliceHeight : process.firstSliceHeight) || sliceHeight, lineWidth = process.sliceLineWidth || device.extruders[extruder].extNozzle, @@ -233,7 +231,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) { zMin: bounds.min.z, zMax: bounds.max.z - zCut, // support/synth usually has overlapping boxes - union: controller.healMesh || isSynth, + union: controller.healMesh, indices: process.indices || process.xray, useAssembly, post: 'FDM', @@ -245,7 +243,6 @@ export function fdm_slice(settings, widget, onupdate, ondone) { clipOffset, lineWidth, vaseMode, - isSynth, process, }, // z index generator @@ -331,7 +328,6 @@ export function fdm_slice(settings, widget, onupdate, ondone) { }, // slicer function (worker local or minion distributed) slicer(z, points, opts) { - // opts.debug = opts.debug || isSynth; return (isConcurrent ? minions.sliceZ : sliceZ)(z, points, opts); }, onupdate(update) { @@ -347,7 +343,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) { slice.height = heights[slice.index]; slice.clips = clip; // do not warn on merging supports - if (changes && !isSynth) { + if (changes) { healed = true; slice.changes = changes; if (devel) { @@ -431,49 +427,10 @@ export function fdm_slice(settings, widget, onupdate, ondone) { } // create shadow for non-belt supports - if (!isBelt && (isSynth || (!isSynth && supportDensity && process.sliceSupportEnable))) { + if (!isBelt && supportDensity && process.sliceSupportEnable) { await doShadow(slices); } - // for synth support widgets, clip/offset to other widgets in group - if (isSynth) { - for (let slice of slices) { - let gap = sliceHeight * process.sliceSupportGap; - // clip tops to other widgets in group - let tops = slice.topPolys(); - for (let peer of widget.group) { - // skip self - if (peer === widget || !peer.slices) { - continue; - } - for (let pslice of peer.slices) { - if (Math.abs(Math.abs(pslice.z - slice.z) - gap) > 0.1) { - continue; - } - let clipto = pslice.clips; - if (pslice.supportOutline) { - // merge support outlines into slice clips which - // should only happen when automatic and manual - // supports are used together - clipto.appendAll(pslice.supportOutline); - } - let ntops = []; - POLY.subtract(tops, clipto, ntops, null, slice.z, 0); - tops = ntops; - } - // trim to group's shadow if not in belt mode - if (!isBelt) { - tops = POLY.setZ(POLY.trimTo(tops, widget.shadow), slice.z); - } - } - slice.tops = []; - for (let t of tops) { - slice.addTop(t); - } - doShells(slice, 1, shellOffset / 2); - } - } - // calculate % complete and call onupdate() function doupdate(index, from, to, msg) { trackupdate(index / slices.length, from, to, msg); @@ -509,7 +466,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) { let isTop = topLayers && slice.index > slices.length - topLayers - 1; let isTopBase = isTop && slice.index === slices.length - topLayers; let isDense = range.sliceFillSparse > 0.995; - let isSolid = (isBottom || ((isTop || isDense) && !vaseMode)) && !isSynth; + let isSolid = (isBottom || ((isTop || isDense) && !vaseMode)); let solidWidth = isSolid ? range.sliceFillWidth || 1 : 0; if (solidWidth) { let fillSpace = fillSpacing * solidWidth; @@ -526,7 +483,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) { }, "solid layers"); // add lead in anchor when specified in belt mode (but not for synths) - if (isBelt && !isSynth) { + if (isBelt) { let { cosf, slope } = widget.belt; // find adjusted zero point from slices let smin = Infinity; @@ -737,7 +694,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) { // layer boolean diffs need to be computed to find flat areas to fill // and overhangs that need to be supported. these are stored in flats // and bridges, projected up/down, and merged into an array of solids - if (!vaseMode && !isSynth) { + if (!vaseMode) { profileStart("delta"); forSlices(0.2, 0.34, slice => { let params = slice.params || process; @@ -778,7 +735,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) { // for "real" objects, fill the remaining voids with sparse fill // sparse layers only present when non-vase mose and sparse % > 0 - if (!isSynth && !vaseMode) { + if (!vaseMode) { let lastType; let promises = isConcurrent ? [] : undefined; forSlices(0.5, promises ? 0.55 : 0.7, slice => { @@ -817,44 +774,10 @@ export function fdm_slice(settings, widget, onupdate, ondone) { } } } - } else if (isSynth && isSupport) { - // convert synth support widgets into support structure - let outline = process.sliceSupportOutline || false; - let promises = isConcurrent ? [] : undefined; - let resolve = []; - forSlices(0.5, promises ? 0.6 : 0.7, slice => { - let params = slice.params || process; - let density = params.sliceSupportDensity; - let supports = slice.topShells(); - slice.supports = supports; - slice.tops = undefined; // remove outline leave only supports - let polys = []; - if (density) { - if (!outline) { - POLY.expand(supports, lineWidth, slice.z, polys); - } else { - POLY.expand(supports, -lineWidth/4, slice.z, polys); - } - } - fillSupportPolys({ - promises, polys, lineWidth, density, z: slice.z, isBelt, - angle: process.sliceSupportFill - }); - resolve.push({ slice, polys }); - }, "infill"); - if (promises) { - await tracker(promises, (i, t) => { - trackupdate(i / t, 0.6, 0.7); - }); - } - for (let rec of resolve) { - let { slice, polys } = rec; - rec.supports = polys; - } } // auto support generation - if (!isBelt && !isSynth && supportDensity && process.sliceSupportEnable) { + if (!isBelt && supportDensity && process.sliceSupportEnable) { doShadow(slices); profileStart("support"); let promises = []; @@ -940,7 +863,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) { if (render) { forSlices(0.9, 1.0, slice => { let params = slice.params || process; - doRender(slice, isSynth, params, { + doRender(slice, params, { dark: controller.dark, devel: controller.devel }); @@ -999,7 +922,7 @@ function bound(v,min,max) { return Math.max(min,Math.min(max,v)); } -function doRender(slice, isSynth, params, opt = {}) { +function doRender(slice, params, opt = {}) { const { dark, devel } = opt; const Color = dark ? COLOR_DARK : COLOR; const output = slice.output(); @@ -1013,7 +936,7 @@ function doRender(slice, isSynth, params, opt = {}) { .addPolys([top.poly]); if (top.shells?.length) output - .setLayer(isSynth ? "support" : "shells", isSynth ? Color.support : Color.shell) + .setLayer("shells", Color.shell) .addPolys(top.shells || [], vopt({ offset, height, clean: true })); if (isThin && top.thin_wall?.length) output @@ -1031,7 +954,7 @@ function doRender(slice, isSynth, params, opt = {}) { ), vopt({ offset, height })); if (top.fill_lines?.length) output - .setLayer("solid fill", isSynth ? Color.support : Color.fill) + .setLayer("solid fill", Color.fill) .addLines(top.fill_lines || [], vopt({ offset: offset * solidWidth, height, z:slice.z })); if (!(slice.belt?.anchor)) output @@ -1783,103 +1706,3 @@ function projectSolid(slice, polys, count, up, first) { else projectSolid(slice.down, polys, count-1, false, false); } } - -export function supports(settings, widget) { - let isBelt = settings.device.bedBelt; - let process = settings.process; - let size = process.sliceSupportSize; - let s9 = size / 9; - let s4 = size / 4; - let s2 = size * 0.45; - let min = 0.01; - let geo = new THREE.BufferGeometry(); - geo.setAttribute('position', new THREE.BufferAttribute(widget.vertices, 3)); - let rad = (Math.PI / 180); - let deg = (180 / Math.PI); - let angle = rad * settings.process.sliceSupportAngle; - let thresh = -Math.sin(angle); - let dir = new THREE.Vector3(0,0,-1) - let add = []; - let mat = new THREE.MeshBasicMaterial(); - let mesh = new THREE.Mesh(geo, mat); - let platform = new THREE.Mesh( - new THREE.PlaneGeometry(10000,10000,1), mat - ); - const now = Date.now(); - // test point - function tp(point) { - if (point.added) { - return; - } - // omit pillars close to existing pillars - for (let added of add) { - let p2 = new THREE.Vector2(point.x, point.y); - let pm = new THREE.Vector2(added.mid.x, added.mid.y); - if (Math.abs(point.z - added.from.z) < s2 && p2.distanceTo(pm) < s4) { - return; - } - } - let ray = new THREE.Raycaster(point, dir); - let int = ray.intersectObjects([ mesh, platform ], false); - if (int && int.length && int[0].distance > 0.5) { - let mid = new THREE.Vector3().add(point).add(int[0].point).divideScalar(2); - add.push({from: point, to: int[0].point, mid}); - point.added = true; - } - } - function tf(a, b, c) { - let dab = a.distanceTo(b); - let dbc = b.distanceTo(c); - let dca = c.distanceTo(a); - let max = Math.max(dab, dbc, dca); - if (max < size) { - let min = Math.min(dab, dbc, dca); - if (min < s9 && Math.random() < 0.5) { - return; - } - // test midpoint of tri face - return tp(new THREE.Vector3().add(a).add(b).add(c).divideScalar(3)); - } - if (dab === max) { - let mp = new THREE.Vector3().add(a).add(b).divideScalar(2); - tf(mp, b, c); - tf(a, mp, c); - } else if (dbc === max) { - let mp = new THREE.Vector3().add(b).add(c).divideScalar(2); - tf(a, mp, c); - tf(a, b, mp); - } else { - let mp = new THREE.Vector3().add(c).add(a).divideScalar(2); - tf(a, b, mp); - tf(mp, b, c); - } - } - - let filter = isBelt ? (norm) => { - return norm.z <= thresh && norm.y < 0; - } : (norm) => { - return norm.z < thresh; - }; - let { position } = geo.attributes; - let { itemSize, count, array } = position; - for (let i = 0; i 0; -} diff --git a/src/moto/space.js b/src/moto/space.js index 90122faf..b74074f0 100644 --- a/src/moto/space.js +++ b/src/moto/space.js @@ -969,7 +969,7 @@ function onMouseUp(event) { function onMouseMove(event) { updateLastAction(); - let int, vis; + let int, vis, dragTrack; const mv = new THREE.Vector2(); mv.x = ( event.clientX / window.innerWidth ) * 2 - 1; @@ -990,13 +990,18 @@ function onMouseMove(event) { platform.visible = vis; if (int && int.length > 0) platformHover(int[0].point); } - } else if (mouseDragPoint && mouseDrag && mouseDrag()) { + } else if (mouseDragPoint && mouseDrag && (dragTrack = mouseDrag())) { event.preventDefault(); let trackTo = alignedTracking ? trackPlane : platform; let vis = trackTo.visible; trackTo.visible = true; - int = intersect([trackTo], false); - trackTo.visible = vis; + if (dragTrack.length) { + int = intersect(dragTrack, false); + trackTo = dragTrack[0]; + } else { + int = intersect([trackTo], false); + trackTo.visible = vis; + } if (int.length > 0 && int[0].object === trackTo) { let delta = mouseDragPoint.clone().sub(int[0].point); let offset = mouseDragStart.clone().sub(int[0].point); @@ -1010,7 +1015,7 @@ function onMouseMove(event) { x: -offset.x, y: offset.z, z: 0 - }); + }, false, int); requestRefresh(); } } From 824770c070a9be8253bfb687fd334f6152946616 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Tue, 6 Jan 2026 06:57:01 -0500 Subject: [PATCH 109/154] fix duplicate output with trace outside and drape option --- src/kiri/mode/cam/op-area.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/kiri/mode/cam/op-area.js b/src/kiri/mode/cam/op-area.js index 528195be..1dd68fee 100644 --- a/src/kiri/mode/cam/op-area.js +++ b/src/kiri/mode/cam/op-area.js @@ -322,6 +322,11 @@ class OpArea extends CamOp { } proc += pinc; progress(proc, 'trace'); + // legacy outline early termination b/c use of + // shadow will cause duplicate output with area + if (tr_type === 'outside' && op.drape) { + break; + } } else if (mode === 'surface') { let { sr_type, sr_angle, sr_alter, tolerance } = op; From 29947815cdd80827300d006920248621d2f8e262 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Tue, 6 Jan 2026 00:00:29 -0500 Subject: [PATCH 110/154] add pocket shadow support --- src/kiri/core/widget.js | 22 ++++++++++++---------- src/kiri/mode/fdm/slice.js | 19 +++++++++++++++++-- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/kiri/core/widget.js b/src/kiri/core/widget.js index 57f5e100..7a363c6f 100644 --- a/src/kiri/core/widget.js +++ b/src/kiri/core/widget.js @@ -708,7 +708,7 @@ class Widget { return this; } - async shadowAt(z) { + async shadowAt(z, pocket) { let shadows = this.cache.shadows; if (!shadows) { shadows = this.cache.shadows = {}; @@ -721,10 +721,11 @@ class Widget { let zover = Object.keys(shadows).map(v => parseFloat(v)).filter(v => v > z); let minZabove = Math.min(Infinity, ...zover); // shift shadow probeline down a fraction to capture z flats with FP noise - let shadow = this.#computeShadowAt(z - 0.005, minZabove); + let shadow = this.#computeShadowAt(z - 0.005, minZabove, undefined, pocket); if (minZabove < Infinity) { shadow = POLY.union([...shadow, ...shadows[minZabove]], 0, true, { wasm: false }); // cull interior sliver voids + if (!pocket) for (let poly of shadow) { if (poly.inner) { poly.inner = poly.inner.filter(inr => { @@ -741,7 +742,7 @@ class Widget { // create a stack of faces in 1mm increments // the stacks are then used to produce shadowlines - #ensureShadowCache() { + #ensureShadowCache(pocket) { if (this.cache.shadow) { return this.cache.shadow; } @@ -758,7 +759,7 @@ class Widget { const b = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]); const c = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]); const n = THREE.computeFaceNormal(a, b, c); - if (n.z < 0.001) { + if ((pocket && n.z > -0.001) || (!pocket && n.z < 0.001)) { continue; } const minZ = Math.floor(Math.min(a.z, b.z, c.z)); @@ -771,13 +772,13 @@ class Widget { return this.cache.shadow = stack; } - async computeShadowStack(zlist, progress) { + async computeShadowStack(zlist, progress, pocket) { let shadow_stack = this.cache.shadow_stack; if (!shadow_stack) { shadow_stack = this.cache.shadow_stack = {}; } let work = self.kiri_worker; - let stack = this.#ensureShadowCache(); + let stack = this.#ensureShadowCache(pocket); let plist = []; if (debug_shadow) console.time('seed minion buckets'); work.minions.broadcast('cam_shadow_stack', stack); @@ -800,13 +801,14 @@ class Widget { work.minions.broadcast('cam_shadow_stack', { clear: true }); } - computeShadowAtZ(z, ztop, cached) { - return this.#computeShadowAt(z, ztop, cached); + // called from minion + computeShadowAtZ(z, ztop, cached, pocket) { + return this.#computeShadowAt(z, ztop, cached, pocket); } // union triangles > z (opt cap < ztop) into polygon(s) // slice the triangle stack matchingg z then union the results - #computeShadowAt(z, ztop, cached) { + #computeShadowAt(z, ztop, cached, pocket) { let shadow_stack = this.cache.shadow_stack; if (shadow_stack && shadow_stack[z]) { return shadow_stack[z]; @@ -814,7 +816,7 @@ class Widget { let label = `compute shadow ${z.round(2)}`; if (debug_shadow) console.time(label); const found = []; - const stack = cached ?? this.#ensureShadowCache(); + const stack = cached ?? this.#ensureShadowCache(pocket); let minZ = Math.floor(z); let maxZ = Math.ceil(z); let slices = []; diff --git a/src/kiri/mode/fdm/slice.js b/src/kiri/mode/fdm/slice.js index e549834d..ba7cae81 100644 --- a/src/kiri/mode/fdm/slice.js +++ b/src/kiri/mode/fdm/slice.js @@ -396,6 +396,22 @@ export function fdm_slice(settings, widget, onupdate, ondone) { } async function onSliceDone(slices) { + + // new auto support demonstrator using cast shadow + if (false) { + let indices = slices.map(s => s.z);//.sort((a,b) => b-a); + + await widget.computeShadowStack(indices, prog => {}, true); + + for (let slice of slices.slice().sort((a,b) => a.z - b.z)) { + let shadow = await widget.shadowAt(slice.z, true); + // slice.supports = shadow; + slice.output() + .setLayer('shadow', 0xff0000) + .addPolys(shadow); + } + } + // alert non-manifold parts if (healed) { onupdate(null, null, "part may not be manifold"); @@ -1735,8 +1751,7 @@ function doSupportFill(args) { slice.supports = supports; }; -function fillSupportPolys(args) { - const { promises, polys, lineWidth, density, z, isBelt, angle, outline } = args; +function fillSupportPolys({ promises, polys, lineWidth, density, z, isBelt, angle, outline }) { // calculate fill density let spacing = lineWidth * (1 / density); polys.forEach(function (poly) { From 4d963cc06c6c54db74f5f9a1a437b0b203ea5819 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Tue, 6 Jan 2026 14:06:12 -0500 Subject: [PATCH 111/154] reimplement angle auto support, remove dead code, disable unused params --- src/kiri/app/conf/defaults.js | 2 +- src/kiri/core/widget.js | 8 +- src/kiri/mode/fdm/init-menu.js | 2 +- src/kiri/mode/fdm/post.js | 2 + src/kiri/mode/fdm/slice.js | 284 ++++++--------------------------- web/kiri/lang/en.js | 4 +- 6 files changed, 58 insertions(+), 244 deletions(-) diff --git a/src/kiri/app/conf/defaults.js b/src/kiri/app/conf/defaults.js index c178c642..dbdf5b7c 100644 --- a/src/kiri/app/conf/defaults.js +++ b/src/kiri/app/conf/defaults.js @@ -338,7 +338,7 @@ export const conf = { sliceSolidify: false, sliceSolidMinArea: 1, sliceSupportAngle: 50, - sliceSupportArea: 0.1, + // sliceSupportArea: 0.1, sliceSupportDensity: 0.1, // sliceSupportEnable: false, sliceSupportExtra: 0, diff --git a/src/kiri/core/widget.js b/src/kiri/core/widget.js index ff23f145..5d11b2df 100644 --- a/src/kiri/core/widget.js +++ b/src/kiri/core/widget.js @@ -711,6 +711,11 @@ class Widget { return this; } + /** + * @param {number} z height for shadow computation + * @param {number | undefined} pocket normal value to match faces + * @returns {Polygon[]} + */ async shadowAt(z, pocket) { let shadows = this.cache.shadows; if (!shadows) { @@ -762,7 +767,8 @@ class Widget { const b = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]); const c = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]); const n = THREE.computeFaceNormal(a, b, c); - if ((pocket && n.z > -0.001) || (!pocket && n.z < 0.001)) { + // todo: use pocket to match normal values when set + if ((pocket && n.z > -pocket) || (!pocket && n.z < 0.001)) { continue; } const minZ = Math.floor(Math.min(a.z, b.z, c.z)); diff --git a/src/kiri/mode/fdm/init-menu.js b/src/kiri/mode/fdm/init-menu.js index 9dc84e2f..f8622836 100644 --- a/src/kiri/mode/fdm/init-menu.js +++ b/src/kiri/mode/fdm/init-menu.js @@ -123,7 +123,7 @@ export function menu() { // sliceSupportSize: newInput(LANG.sp_size_s, {title:LANG.sp_size_l, convert:toFloat, bound:bound(1.0,200.0)}), sliceSupportGap: newInput(LANG.sp_gaps_s, {title:LANG.sp_gaps_l, convert:toInt, bound:bound(0,5)}), sliceSupportOffset: newInput(LANG.sp_offs_s, {title:LANG.sp_offs_l, convert:toFloat, bound:bound(0.0,200.0)}), - sliceSupportArea: newInput(LANG.sp_area_s, {title:LANG.sp_area_l, convert:toFloat, bound:bound(0.0,200.0)}), + // sliceSupportArea: newInput(LANG.sp_area_s, {title:LANG.sp_area_l, convert:toFloat, bound:bound(0.0,200.0)}), sliceSupportExtra: newInput(LANG.sp_xpnd_s, {title:LANG.sp_xpnd_l, convert:toFloat, bound:bound(0.0,10.0)}), // sliceSupportGrow: newInput(LANG.sp_grow_s, {title:LANG.sp_grow_l, convert:toFloat, bound:bound(0.0,10.0)}), sliceSupportSpan: newInput(LANG.sp_span_s, {title:LANG.sp_span_l, convert:toFloat, bound:bound(0.0,200.0), show:() => ui.sliceSupportEnable.checked }), diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index 299c189c..68ac94d2 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -23,6 +23,7 @@ export function slicePost(data, options) { const { compInner, compOuter, pump } = post_args; const { clipOffset, fillOffset, shellOffset } = post_args; + // pumping the shells will round sharp edges if (pump) { let nugroups = []; let pump = shellOffset / 10; @@ -86,6 +87,7 @@ export function slicePost(data, options) { useAssembly })); } + // create a fast clip offset polygon based on a simplified part outline data.clip = clipOffset ? POLY.offset(nutops.map(t => t.simple), clipOffset) : undefined; data.tops = nutops; delete data.groups; diff --git a/src/kiri/mode/fdm/slice.js b/src/kiri/mode/fdm/slice.js index b202d042..b5734d85 100644 --- a/src/kiri/mode/fdm/slice.js +++ b/src/kiri/mode/fdm/slice.js @@ -24,7 +24,7 @@ let tracker = util.pwait, infill: { check: 0x3322bb, face: 0x3322bb, line: 0x3322bb, opacity, lopacity, fat }, inset: { line: 0xaaaaaa, check: 0xaaaaaa, face: 0 }, shell: { check: 0x0077bb, face: 0x0077bb, line: 0x0077bb, opacity, lopacity, fat }, - support: { check: 0xaa5533, face: 0xaa5533, line: 0xaa5533, opacity, lopacity, fat }, + support: { check: 0xbbbb00, face: 0xbbbb00, line: 0xbbbb00, opacity, lopacity, fat }, part: { line: 0x333333, check: 0x333333 }, thin: { check: 0xbb8800, face: 0xbb8800, line: 0xbb8800, opacity, lopacity, fat }, }, @@ -239,8 +239,8 @@ export function fdm_slice(settings, widget, onupdate, ondone) { compInner: sliceCompInner, compOuter: sliceCompOuter, shellOffset, - fillOffset, - clipOffset, + fillOffset, // distance: top inset for sparse/solid infill + clipOffset, // distance: create simpler clip offset for supports lineWidth, vaseMode, process, @@ -362,49 +362,31 @@ export function fdm_slice(settings, widget, onupdate, ondone) { return onSliceDone(slices); }).then(ondone); - // shadow used to clip supports in non-belt mode - async function doShadow(slices) { - if (widget.shadow) { - return; - } - let root = widget.group[0]; - if (root.shadow) { - widget.shadow = root.shadow; - return; - } - // console.log({ doShadow: widget, slices }); - // create shadow for clipping supports - let alltops = widget.group - .filter(w => !w.track.synth) // no supports in shadow - .map(w => w.slices).flat() - .map(s => s.tops).flat().map(t => t.simple); - let shadow = isConcurrent ? - await minions.union(alltops, 0.1) : - POLY.union(alltops, 0.1, true); - // expand shadow when requested (support clipping) - if (process.sliceSupportExtra) { - shadow = POLY.offset(shadow, process.sliceSupportExtra); - } - widget.shadow = root.shadow = POLY.setZ(shadow, 0); - // slices[0].output() - // .setLayer('shadow', { line: 0xff0000, check: 0xff0000 }) - // .addPolys(shadow); - } - async function onSliceDone(slices) { // new auto support demonstrator using cast shadow - if (false) { - let indices = slices.map(s => s.z);//.sort((a,b) => b-a); + if (process.sliceSupportType === 'automatic') { + let stack = slices.slice(); + let indices = stack.map(s => s.z); + let zAngNorm = Math.sin(process.sliceSupportAngle * Math.PI / 180); - await widget.computeShadowStack(indices, prog => {}, true); + await widget.computeShadowStack(indices, progress => {}, zAngNorm); - for (let slice of slices.slice().sort((a,b) => a.z - b.z)) { + if (process.sliceSupportTree) { + stack.sort((a,b) => a.z - b.z); // deltas only + } else { + stack.sort((a,b) => b.z - a.z); // accumulate deltas + } + + for (let slice of stack) { let shadow = await widget.shadowAt(slice.z, true); - // slice.supports = shadow; - slice.output() - .setLayer('shadow', 0xff0000) - .addPolys(shadow); + if (process.sliceSupportExtra) { + shadow = POLY.offset(shadow, process.sliceSupportExtra); + } + slice.supports = shadow; + // slice.output() + // .setLayer('shadow', 0xff0000) + // .addPolys(shadow); } } @@ -442,11 +424,6 @@ export function fdm_slice(settings, widget, onupdate, ondone) { slice.params = getRangeParameters(process, slice.index); } - // create shadow for non-belt supports - if (!isBelt && supportDensity && process.sliceSupportEnable) { - await doShadow(slices); - } - // calculate % complete and call onupdate() function doupdate(index, from, to, msg) { trackupdate(index / slices.length, from, to, msg); @@ -792,22 +769,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) { } } - // auto support generation - if (!isBelt && supportDensity && process.sliceSupportEnable) { - doShadow(slices); - profileStart("support"); - let promises = []; - forSlices(0.7, 0.75, slice => { - promises.push(doSupport(slice, process, widget.shadow, { })); - }, "support"); - await tracker(promises, (i, t) => { - trackupdate(i / t, 0.75, 0.8); - }); - profileEnd(); - } - // fill all supports (auto and manual) - // if (!isBelt && supportDensity) { if (supportDensity) { profileStart("support-fill"); let promises = false && isConcurrent ? [] : undefined; @@ -815,10 +777,15 @@ export function fdm_slice(settings, widget, onupdate, ondone) { let params = slice.params || process; let density = params.sliceSupportDensity; doSupportFill({ - promises, slice, lineWidth, density, - minArea: process.sliceSupportArea, isBelt, angle: process.sliceSupportFill, - outline: process.sliceSupportOutline !== false + density, + gap: process.sliceSupportGap, + isBelt, + lineWidth, + minArea: 0, + outline: process.sliceSupportOutline !== false, + promises, + slice, }); }, "support"); if (promises) { @@ -1457,14 +1424,11 @@ function doSolidsFill(slice, spacing, angle, minArea, fillQ) { } if (tofill.length > 0) { doFillArea(fillQ, tofill, angle, spacing, newfill); - // top.fill_lines_norm = {angle:angle,spacing:spacing}; } if (angfill.length > 0) { top.fill_lines_ang = {spacing:spacing,list:[],poly:[]}; for (let af of angfill) { doFillArea(fillQ, [af], af.fillang.angle + 45, spacing, newfill); - // top.fill_lines_ang.list.push(af.fillang.angle + 45); - // top.fill_lines_ang.poly.push(af.clone()); } } } @@ -1478,175 +1442,11 @@ function doFillArea(fillQ, polys, angle, spacing, output, minLen, maxLen) { } } -/** - * calculate external overhangs requiring support - */ -async function doSupport(slice, proc, shadow, opt = {}) { - let { minions } = self.kiri_worker, - maxBridge = proc.sliceSupportSpan || 5, - minArea = proc.supportMinArea || 0.1, - pillarSize = proc.sliceSupportSize, - offset = proc.sliceSupportOffset || 0, - gap = proc.sliceSupportGap, - size = (pillarSize || 1), - tops = slice.topPolys(), - trimTo = tops; - - let traces = POLY.flatten(slice.topShells().clone(true)), - fill = slice.topFill(), - points = [], - down = slice.down, - down_tops = down ? down.topPolys() : null, - down_traces = down ? POLY.flatten(down.topShells().clone(true)) : null; - - if (opt.exp && down_tops) { - let points = down_tops.map(p => p.deepLength).reduce((a,v)=>a+v); - if (points > 200) { - // use de-rez'd top shadow instead - down_tops = down.topSimples(); - // de-rez trace polys because it's not that important for supports - down_traces = down_traces.map(p => p.clean(true, undefined, config.clipper / 10)); - } - } - - // DEBUG code - let SDBG = false; - let cks = SDBG ? [] : undefined; - let pip = SDBG ? [] : undefined; - let pcl = SDBG ? [] : undefined; - - // check if point is supported by layer below - function checkPointSupport(point) { - if (SDBG) cks.push(point); // DEBUG - // skip points close to other support points - for (let i=0; i= maxBridge) { - let slope = p1.slopeTo(p2).factor(1/dist), - segs = Math.floor(dist / maxBridge) + 1, - seglen = dist / segs; - while (i < segs) { - checkPointSupport(p1.projectOnSlope(slope, i++ * seglen)); - } - } - if (poly) checkPointSupport(p2); - } - - let supports = []; - - // generate support polys from unsupported points - if (slice.down) (function() { - // check trace line support needs - traces.forEach(function(trace) { - trace.forEachSegment(function(p1, p2) { checkLineSupport(p1, p2, true) }); - }); - - // add offset solids to supports (or fill depending) - fill.forEachPair(function(p1,p2) { checkLineSupport(p1, p2, false) }); - - // skip the rest if no points or supports - if (!(points.length || supports.length)) return; - - let pillars = []; - - // for each point, create a bounding rectangle - points.forEach(function(point) { - pillars.push(newPolygon().centerRectangle(point, size/2, size/2)); - }); - - supports.appendAll(POLY.union(pillars, null, true, { wasm: false })); - // merge pillars and replace with convex hull of outer points (aka smoothing) - pillars = POLY.union(pillars, null, true, { wasm: false }).forEach(function(pillar) { - supports.push(newPolygon().createConvexHull(pillar.points)); - }); - })(); - - // DEBUG code - if (SDBG && down_traces) slice.output() - .setLayer('cks', { line: 0xee5533, check: 0xee5533 }) - .addPolys(cks.map(p => newPolygon().centerRectangle(p, 0.25, 0.25))) - .setLayer('pip', { line: 0xdd4422, check: 0xdd4422 }) - .addPolys(pip.map(p => newPolygon().centerRectangle(p, 0.4, 0.4))) - .setLayer('pcl', { line: 0xcc3311, check: 0xcc3311 }) - .addPolys(pcl.map(p => newPolygon().centerRectangle(p, 0.3, 0.3))) - .setLayer('pts', { line: 0xdd33dd, check: 0xdd33dd }) - .addPolys(points.map(p => newPolygon().centerRectangle(p, 0.8, 0.8))) - .setLayer('dtr', { line: 0x0, check: 0x0 }) - .addPolys(POLY.setZ(down_traces.clone(true),slice.z)); - ; - - if (supports.length === 0) { - return; - } - - // then union supports - if (supports.length > 10) { - supports = await minions.union(supports); - } else { - supports = POLY.union(supports, null, true, { wasm: false }); - } - - // clip to top polys - supports = POLY.trimTo(supports, shadow); - - let depth = 0; - while (down && supports.length > 0) { - down.supports = down.supports || []; - - let trimmed = [], culled = []; - - // culled = supports; - // clip supports to shell offsets - POLY.subtract(supports, down.topSimples(), trimmed, null, slice.z, minArea, { wasm: false }); - - // set depth hint on support polys for infill density - trimmed.forEach(function(trim) { - if (trim.area() < minArea) return; - culled.push(trim.setZ(down.z)); - }); - - // exit when no more support polys exist - if (culled.length === 0) break; - - // new bridge polys for next pass (skip first layer below) - if (depth >= gap) { - down.supports.appendAll(culled); - } - - supports = culled; - down = down.down; - depth++; - } - -} - function doSupportFill(args) { - const { promises, slice, lineWidth, density, minArea, isBelt, angle, outline } = args; + const { promises, slice, lineWidth, density, minArea, isBelt, angle, outline, gap } = args; let supports = slice.supports, - nsB = [], - nsC = [], - min = minArea || 0.1; + min = minArea || 0.1, + sub; if (!supports) return; @@ -1654,13 +1454,19 @@ function doSupportFill(args) { supports = POLY.setZ(POLY.union(supports, undefined, true, { wasm: false }), slice.z); // clip supports to slice clip offset (or shell if none) - POLY.subtract(supports, slice.clips, nsB, null, slice.z, min, { wasm: false }); - supports = nsB; + POLY.subtract(supports, slice.clips, sub = [], null, slice.z, min, { wasm: false }); + supports = sub; - // also trim to lower offsets, if they exist - if (slice.down && slice.down.clips) { - POLY.subtract(nsB, slice.down.clips, nsC, null, slice.z, min, { wasm: false }); - supports = nsC; + // trim to upper offsets, if they exist + if (gap && slice?.up?.clips) { + POLY.subtract(supports, slice.up.clips, sub = [], null, slice.z, min, { wasm: false }); + supports = sub; + } + + // trim to lower offsets, if they exist + if (gap && slice?.down?.clips) { + POLY.subtract(supports, slice.down.clips, sub = [], null, slice.z, min, { wasm: false }); + supports = sub; } if (supports) { diff --git a/web/kiri/lang/en.js b/web/kiri/lang/en.js index 56a8f511..c477fda1 100644 --- a/web/kiri/lang/en.js +++ b/web/kiri/lang/en.js @@ -478,8 +478,8 @@ self.lang['en-us'] = { sp_span_l: ["unsupported span that causes","a new support to be generated","in millimeters"], sp_angl_s: "angle", sp_angl_l: ["max overhang angle before","supporting pillar is generated","normal values 45-60","belt values 0-10"], - sp_area_s: "min area", - sp_area_l: ["minimum area for","a support column","in millimeters","smaller values will","slow detection"], + // sp_area_s: "min area", + // sp_area_l: ["minimum area for","a support column","in millimeters","smaller values will","slow detection"], sp_xpnd_s: "expand", sp_xpnd_l: ["expand support area","beyond part boundary","in millimeters"], // sp_grow_s: "grow", From 97fbcb109edfd78d4a0b3dc643e80dffc2fb6e19 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Tue, 6 Jan 2026 14:47:14 -0500 Subject: [PATCH 112/154] clean up and document fdm slicing --- src/kiri/mode/fdm/belt.js | 309 +++++++++ src/kiri/mode/fdm/driver-be.js | 4 +- src/kiri/mode/fdm/slice.js | 1086 ++++++++++++++++++-------------- 3 files changed, 931 insertions(+), 468 deletions(-) create mode 100644 src/kiri/mode/fdm/belt.js diff --git a/src/kiri/mode/fdm/belt.js b/src/kiri/mode/fdm/belt.js new file mode 100644 index 00000000..922f49d7 --- /dev/null +++ b/src/kiri/mode/fdm/belt.js @@ -0,0 +1,309 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +/** + * Belt printer specific functions for FDM slicing + * Handles anchor generation, text embossing, and belt-specific bounds + */ + +import { newPolygon } from '../../../geo/polygon.js'; +import { newBounds } from '../../../geo/bounds.js'; +import { newSlice } from '../../core/slice.js'; +import { getRangeParameters } from './driver.js'; + +// Import constants from slice.js +// These would ideally be shared, but for now we'll define locally +const BELT = { + PRECISION_ROUNDING: 3, + PEEK_LAYERS: 5, + ANCHOR_BUMP_SPACING_MULT: 2, + MIN_WIDTH_FOR_HATCH: 10, + HATCH_SPACING: 3, + POOCH_Y_RANGE_MIN: 0, + POOCH_Y_RANGE_MAX: 3, + POOCH_FLATNESS_TOLERANCE: 0.01, + POOCH_WIDTH_TOLERANCE: 1, + POOCH_DY_TOLERANCE: 0.1, + POOCH_DZ_TOLERANCE: 1, + POOCH_TEXT_SCALE_X: 1.2, + POOCH_TEXT_SCALE_Y: 1, + POOCH_FONT_SIZE: 24, + POOCH_PIXEL_THRESHOLD: 30, +}; + +/** + * Generate belt anchor for lead-in adhesion + * + * Creates a lead-in anchor section before the actual part begins printing. + * The anchor helps adhesion on belt printers by providing a larger contact area. + * Optionally adds "bumps" to the anchor for improved adhesion. + * + * @param {Object} options - Configuration options + * @param {Array} options.slices - Array of slices to process + * @param {Object} options.widget - Widget being sliced + * @param {Object} options.process - Process parameters + * @param {number} options.lineWidth - Extrusion width in mm + * @param {number} options.sliceHeight - Layer height in mm + * @param {number} options.sliceHeightBase - First layer height in mm + * @param {number} options.extruder - Extruder index + * @returns {void} Modifies slices array in place + */ +export function generateBeltAnchor({ slices, widget, process, lineWidth, sliceHeight, sliceHeightBase, extruder }) { + let { cosf, slope } = widget.belt; + + // find adjusted zero point from slices + let smin = Infinity; + for (let slice of slices) { + let miny = Infinity; + for (let poly of slice.topPolys()) { + let y = poly.bounds.maxy; + let z = slice.z; + // at 45 degrees, 1mm in Z is 1mm in Y + let by = (slope * z) - y; + if (by < miny) miny = by; + if (by < smin) smin = by; + } + // mark slices with tops touching belt + slice.belt = { miny, touch: miny.round(BELT.PRECISION_ROUNDING) < sliceHeightBase }; + } + + // find max width of first N layers for brim additions + let start; + let minx = Infinity, maxx = -Infinity; + let peek = 0; + for (let slice of slices) { + if (slice.tops.length && peek++ < BELT.PEEK_LAYERS) { + for (let poly of slice.topPolys()) { + minx = Math.min(minx, poly.bounds.minx); + maxx = Math.max(maxx, poly.bounds.maxx); + } + } + // find first slice touching belt for start of anchor + if (!start && slice.belt.touch) { + start = slice; + } + } + + // ensure we start against a layer with shells + while (start && start.up && start.topShells().length === 0) { + start = start.up; + } + + // if a brim applies, add that width to anchor + let brim = getRangeParameters(process, 0).firstLayerBrim || 0; + if (brim) { + minx -= brim; + maxx += brim; + } + + // array of added top.fill_sparse arrays + let adds = []; + let step = sliceHeight; + let anchorlen = (process.beltAnchor || process.firstLayerBeltLead) * cosf; + + while (anchorlen && start && anchorlen >= sliceHeight) { + let addto = start.down; + if (!addto) { + addto = newSlice(start.z - step); + addto.extruder = extruder; + addto.belt = { }; + addto.height = start.height; + addto.up = start; + start.down = addto; + slices.splice(0,0,addto); + } else if (!addto.belt) { + console.log({addto_missing_belt: addto}); + addto.belt = {}; + } + addto.index = -1; + addto.belt.anchor = true; + // this allows the anchor to print bi-directionally + // by removing the forced start-point in print.js + addto.belt.touch = false; + let z = addto.z; + let y = (slope * z) - smin - (lineWidth / 2); + let splat = newPolygon().add(minx, y, z).add(maxx, y, z).setOpen(); + let snew = addto.addTop(splat).fill_sparse = [ splat ]; + adds.push(snew); + start = addto; + anchorlen -= (step * slope); + } + + // add anchor bump + let bump = process.firstLayerBeltBump; + if (bump) { + adds = adds.reverse().slice(1, adds.length - 1); + let count = 1; + for (let add of adds) { + let poly = add[0]; + let y = count++ * -start.height * BELT.ANCHOR_BUMP_SPACING_MULT; + if (-y > bump) { + count--; + } + let first = poly.first(); + // add up/over/down to anchor line (close = down) + // which completes the bump perimeter + poly.push(poly.last().add({x:0, y, z:0})); + poly.push(poly.first().add({x:0, y, z:0})); + poly.setClosed(); + if (count > 2 && maxx - minx > BELT.MIN_WIDTH_FOR_HATCH) { + // add vertical hatch lines inside bump shell + let mp = (maxx + minx) / 2; + let dx = (maxx - minx - 2); + dx = (Math.floor(dx / BELT.HATCH_SPACING) * BELT.HATCH_SPACING) / 2; + let fy = first.y; + let fz = first.z; + let n2 = lineWidth / 2; + for (let x = mp - dx; x <= mp + dx ; x += BELT.HATCH_SPACING) { + add.push(newPolygon().add(x, fy - n2, fz).add(x, fy + y + n2, fz).setOpen() ); + } + } + } + } +} + +/** + * Experimental: Emboss text on flat underside of belt-printed object + * + * Scans for flat underside surfaces and embosses specified text using + * canvas rendering and pixel detection. The text is converted to support + * patterns that create raised or recessed text on the bottom surface. + * + * @param {Object} options - Configuration options + * @param {Array} options.slices - Array of slices to process + * @param {Object} options.widget - Widget being sliced + * @param {Object} options.process - Process parameters + * @param {number} options.lineWidth - Extrusion width in mm + * @param {number} options.smin - Minimum belt Y offset + * @returns {void} Modifies slice.supports arrays + */ +export function embossBeltPooch({ slices, widget, process, lineWidth, smin }) { + if (!process.pooch || !self.OffscreenCanvas) { + return; + } + + const { length, width, height, text } = process.pooch; + const { slope } = widget.belt; + let firstZ, firstI, lastZ, lastI, minX = 0, maxX = 0, maxY = 0; + + // locate suitable flat spot + for (let slice of slices) { + let { belt } = slice; + if (!belt.touch) { + continue; + } + let index = slice.index; + let z = slice.z; + outer: for (let poly of slice.topPolys()) { + for (let i=0, p=poly.points, l=p.length; i BELT.POOCH_Y_RANGE_MIN && p0y < BELT.POOCH_Y_RANGE_MAX && + Math.abs(p0y - p1y) < BELT.POOCH_FLATNESS_TOLERANCE; + let x_ok = Math.abs(width - Math.abs(p1.x - p0.x)) < BELT.POOCH_WIDTH_TOLERANCE + if (y_ok && x_ok) { + if (i_ok) { + firstZ = firstZ || z; + firstI = firstI || index; + lastZ = z; + lastI = index; + minX = Math.min(p0.x, p1.x); + maxX = Math.max(p0.x, p1.x); + maxY = p0y; + break outer; + } else { + firstZ = lastI = 0; + } + } + } + } + } + + let dy = Math.abs(height - maxY * (1 / Math.sqrt(2))); + let dz = Math.abs(length - ((lastZ - firstZ) * Math.sqrt(2))); + if (dy < BELT.POOCH_DY_TOLERANCE && dz < BELT.POOCH_DZ_TOLERANCE) { + // console.log('FOUND', { firstI, lastI, minX, maxX, maxY }); + let span = lastI - firstI - 2; // x = down the belt + let tall = width * 2; + let can = new self.OffscreenCanvas(span, tall); + let ctx = can.getContext("2d"); + ctx.scale(BELT.POOCH_TEXT_SCALE_X, BELT.POOCH_TEXT_SCALE_Y); + ctx.font = `${BELT.POOCH_FONT_SIZE}px sans-serif`; + ctx.textBaseline = "bottom"; + ctx.fillText(text, 1, tall - 1); + let img = ctx.getImageData(0, 0, span, tall).data.buffer; + let rgb = new Uint32Array(img); + + for (let x=0; x=0; y--) { + let pix = rgb[y * span + x]; + pix = ( + ((pix >> 24) & 0xff) + + ((pix >> 16) & 0xff) + + ((pix >> 8) & 0xff) + ) / 3; + str += pix > BELT.POOCH_PIXEL_THRESHOLD ? '*' : '-'; + maxp = Math.max(maxp, pix); + if (pix > BELT.POOCH_PIXEL_THRESHOLD) { + if (start >= 0) { + end = tall - y; + } else { + start = tall - y; + } + } else { + if (start >= 0 && end > start) { + lines.push({ start, end }); + } + start = end = undefined; + } + } + console.log((x).toString().padStart(2,0),str,maxp | 0,lines); + if (lines.length) { + let slice = slices[firstI + x + 1]; + let supps = slice.supports = slice.supports || []; + let z = slice.z; + let y = z - smin - (lineWidth / 2); + for (let line of lines) { + supps.push(newPolygon() + .add(minX + line.start / 2, y, z) + .add(minX + line.end / 2, y, z) + .setOpen() + ); + } + } + } + } +} + +/** + * Finalize belt-specific bounds for widget tracking + * + * Calculates final Y-axis bounds for belt-printed parts, + * used for positioning and collision detection. + * + * @param {Object} options - Configuration options + * @param {Array} options.slices - Array of processed slices + * @param {Object} options.widget - Widget being sliced + * @returns {void} Modifies widget.belt properties + */ +export function finalizeBeltBounds({ slices, widget }) { + let bounds = newBounds(); + let slice = slices[0]; + if (slice.tops) { + for (let top of slice.tops) { + bounds.merge(top.poly.bounds); + } + } else if (slice.supports) { + for (let poly of slice.supports) { + bounds.merge(poly); + } + } + widget.belt.miny = -bounds.miny; + widget.belt.midy = (bounds.miny + bounds.maxy) / 2; +} diff --git a/src/kiri/mode/fdm/driver-be.js b/src/kiri/mode/fdm/driver-be.js index 8f308c12..72df30ac 100644 --- a/src/kiri/mode/fdm/driver-be.js +++ b/src/kiri/mode/fdm/driver-be.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { util } from '../../../geo/base.js'; -import { fdm_slice, slicePost } from './slice.js'; +import { sliceOne, slicePost } from './slice.js'; import { fdm_prepare } from './prepare.js'; import { fdm_export } from './export.js'; import { codec } from '../../core/codec.js'; @@ -41,7 +41,7 @@ export const FDM = { extrudePerMM, extrudeMM, slicePre: undefined, - slice: fdm_slice, + slice: sliceOne, slicePost: slicePost, prepare: fdm_prepare, export: fdm_export diff --git a/src/kiri/mode/fdm/slice.js b/src/kiri/mode/fdm/slice.js index b5734d85..0493515e 100644 --- a/src/kiri/mode/fdm/slice.js +++ b/src/kiri/mode/fdm/slice.js @@ -1,18 +1,131 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { base, util } from '../../../geo/base.js'; -import { newBounds } from '../../../geo/bounds.js'; -import { newPolygon } from '../../../geo/polygon.js'; -import { polygons as POLY } from '../../../geo/polygons.js'; -import { newPoint } from '../../../geo/point.js'; -import { slice, sliceZ } from '../../../geo/slicer.js'; -import { newSlice } from '../../core/slice.js'; -import { fill, fill_fixed } from './fill.js'; -import { getRangeParameters } from './driver.js'; import { doTopShells } from './post.js'; +import { fill, fill_fixed } from './fill.js'; +import { generateBeltAnchor, embossBeltPooch, finalizeBeltBounds } from './belt.js'; +import { getRangeParameters } from './driver.js'; +import { newPoint } from '../../../geo/point.js'; +import { newPolygon } from '../../../geo/polygon.js'; +import { newSlice } from '../../core/slice.js'; +import { polygons as POLY } from '../../../geo/polygons.js'; +import { slice, sliceZ } from '../../../geo/slicer.js'; const { config } = base; +const CONSTANTS = { + // Support fill + SUPPORT_FILL_LEGACY_OFFSET: 1000, // hack to allow old code path detection (line 158) + SUPPORT_AUTO_ANGLE_WIDE: 1090, // auto-angle for wide support polygons (90°) + SUPPORT_AUTO_ANGLE_TALL: 1000, // auto-angle for tall support polygons (0°) + SUPPORT_INSET_RATIO: 1/3, // inset support fill by 33% of line width + SUPPORT_CONNECT_DISTANCE_MULT: 2, // connect fill lines within 2x spacing + + // Minimum areas and thresholds + MIN_POLY_AREA: 0.1, // minimum polygon area in mm² + DENSE_INFILL_THRESHOLD: 0.995, // 99.5% infill triggers solid layer + SOLID_LAYER_COVERAGE_THRESHOLD: 0.5, // 50% solid coverage triggers full layer + + // Adaptive slicing + ADAPTIVE_SLICE_TOLERANCE: 0.01, // threshold for adaptive layer height adjustments + + // Belt mode + BELT_PRECISION_ROUNDING: 3, // decimal places for belt touch detection + BELT_PEEK_LAYERS: 5, // layers to check for brim width calculation + BELT_ANCHOR_BUMP_SPACING_MULT: 2, // anchor bump spacing multiplier + BELT_MIN_WIDTH_FOR_HATCH: 10, // minimum width in mm for hatch lines in bumps + BELT_HATCH_SPACING: 3, // hatch line spacing in mm for bumps + + // Pooch (belt text embossing) + POOCH_Y_RANGE_MIN: 0, // minimum Y offset for pooch detection + POOCH_Y_RANGE_MAX: 3, // maximum Y offset for pooch detection + POOCH_FLATNESS_TOLERANCE: 0.01, // flatness tolerance for pooch surface + POOCH_WIDTH_TOLERANCE: 1, // width matching tolerance in mm + POOCH_DY_TOLERANCE: 0.1, // height delta tolerance + POOCH_DZ_TOLERANCE: 1, // length delta tolerance + POOCH_TEXT_SCALE_X: 1.2, // text horizontal scale factor + POOCH_TEXT_SCALE_Y: 1, // text vertical scale factor + POOCH_FONT_SIZE: 24, // font size in pixels + POOCH_PIXEL_THRESHOLD: 30, // brightness threshold for pixel detection + + // Angled fill + ANGLED_FILL_ROTATION: 45, // rotation offset for angled fills +}; + +/** + * @typedef {Object} SliceParams + * @property {number} sliceHeight - Layer height in mm + * @property {number} sliceLineWidth - Extrusion width in mm + * @property {number} sliceTopLayers - Number of solid top layers + * @property {number} sliceBottomLayers - Number of solid bottom layers + * @property {number} sliceFillSparse - Infill density (0.0-1.0) + * @property {string} sliceFillType - Infill pattern: 'hex', 'grid', 'gyroid', 'triangle', etc. + * @property {number} sliceFillAngle - Base infill angle in degrees + * @property {number} sliceFillWidth - Solid fill line width multiplier + * @property {number} sliceFillGrow - Fill boundary expansion in mm + * @property {number} sliceFillOverlap - Fill overlap with shells (0.0-0.8) + * @property {number} sliceSolidMinArea - Minimum area in mm² for solid regions + * @property {number} sliceSupportDensity - Support fill density (0.0-1.0) + * @property {number} sliceSupportFill - Support fill angle (or auto if >= 1000) + * @property {number} sliceSupportGap - Gap between support and part in mm + * @property {boolean} sliceSupportOutline - Include support perimeter + * @property {number} sliceSupportOffset - Support clip offset in mm + * @property {number} sliceAdaptive - Enable adaptive layer heights + * @property {number} sliceMinHeight - Minimum adaptive layer height in mm + * @property {number} firstSliceHeight - First layer height in mm + * @property {number} firstLayerBrim - Brim width in mm + * @property {number} beltAnchor - Belt anchor length in mm + * @property {number} firstLayerBeltLead - Belt lead-in length in mm + * @property {number} firstLayerBeltBump - Belt anchor bump height in mm + * @property {number} firstLayerFlatten - Belt base flattening height in mm + */ + +/** + * @typedef {Object} RenderContext + * @property {boolean} isThin - Force thin line rendering + * @property {boolean} isFlat - Force flat polygon rendering + * @property {number} offset - Line generation offset (typically lineWidth/2) + */ + +/** + * @typedef {Object} FillOptions + * @property {Object} settings - Global settings object + * @property {SliceParams} process - Process parameters + * @property {Object} device - Device configuration + * @property {number} lineWidth - Extrusion width in mm + * @property {number} spacing - Fill line spacing in mm + * @property {number} density - Fill density (0.0-1.0) + * @property {Object} bounds - Widget bounding box + * @property {number} height - Layer height in mm + * @property {string} type - Fill pattern type + * @property {boolean} cache - Enable fill caching + * @property {Promise[]} [promises] - Array for async operations + */ + +/** + * @typedef {Object} SupportFillOptions + * @property {Slice} slice - Slice to process + * @property {number} lineWidth - Extrusion width in mm + * @property {number} density - Support density (0.0-1.0) + * @property {number} minArea - Minimum polygon area in mm² + * @property {boolean} isBelt - Belt printer mode + * @property {number} [angle] - Fill angle in degrees (auto if >= 1000) + * @property {boolean} outline - Include support outline + * @property {number} [gap] - Gap from part surfaces in mm + * @property {Promise[]} [promises] - Array for async operations + */ + +/** + * @typedef {Object} BeltData + * @property {number} slope - Belt angle slope factor + * @property {number} cosf - Cosine of belt angle + * @property {number} sinf - Sine of belt angle + * @property {number} miny - Minimum Y coordinate + * @property {number} midy - Middle Y coordinate + * @property {boolean} [touch] - Whether slice touches belt + * @property {boolean} [anchor] - Whether slice is part of anchor + */ + let tracker = util.pwait, lopacity = 0.6, opacity = 1, @@ -42,18 +155,21 @@ let tracker = util.pwait, profileEnd = profile ? console.profileEnd : noop, debug = false; -let isThin = false, // force line rendering - isFlat = false, // force flat rendering - offset = 0; // poly line generation offsets - -function vopt(opt) { +/** + * Apply visualization options based on render context + * + * @param {Object} opt - Base options + * @param {RenderContext} ctx - Render context + * @returns {Object|null} Modified options or null + */ +function vopt(opt, ctx) { if (opt) { - if (isFlat) { + if (ctx.isFlat) { opt.flat = true; opt.outline = true; return opt; } - if (isThin) return null; + if (ctx.isThin) return null; } return opt; } @@ -120,7 +236,7 @@ export function slicePost(settings, onupdate) { * @param {Function} onupdate (called with % complete and optional message) * @param {Function} ondone (called when complete with an array of Slice objects) */ -export function fdm_slice(settings, widget, onupdate, ondone) { +export function sliceOne(settings, widget, onupdate, ondone) { let render = settings.render !== false, { minions } = self.kiri_worker, { process, device, controller } = settings, @@ -147,20 +263,24 @@ export function fdm_slice(settings, widget, onupdate, ondone) { supportDensity = process.sliceSupportDensity, { sliceCompInner, sliceCompOuter } = process; - // override globals used by vopt() - isFlat = controller.lineType === "flat"; - isThin = !isFlat && controller.lineType === "line"; - offset = lineWidth / 2; + // create render context for visualization + /** @type {RenderContext} */ + const renderContext = { + isFlat: controller.lineType === "flat", + isThin: controller.lineType === "line", + offset: lineWidth / 2 + }; + renderContext.isThin = !renderContext.isFlat && renderContext.isThin; // allow overriding support fill auto angle algorithm // also causes support fill to be aligned on start boundaries // best with angles that are a multiple of 90 degrees if (process.sliceSupportFill >= 0) { // yes, an ugly hack to allow it to pass through old code paths - process.sliceSupportFill += 1000; + process.sliceSupportFill += CONSTANTS.SUPPORT_FILL_LEGACY_OFFSET; } - if (isFlat) { + if (renderContext.isFlat) { Object.values(COLOR).forEach(color => { color.flat = true; color.line = 1 @@ -230,7 +350,6 @@ export function fdm_slice(settings, widget, onupdate, ondone) { xray: process.xray, zMin: bounds.min.z, zMax: bounds.max.z - zCut, - // support/synth usually has overlapping boxes union: controller.healMesh, indices: process.indices || process.xray, useAssembly, @@ -245,87 +364,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) { vaseMode, process, }, - // z index generator - zGen(zopt) { - if (process.xray) { - return zopt.zIndexes; - } - let { zMin, zMax } = zopt; - let h1 = sliceHeight; - let h0 = Math.abs(zMin) < 0.0001 ? (sliceHeightBase || h1) : h1; - let hm = sliceMinHeight || 0; - let h = h0; - let z = h0; - let zi = indices; // indices - let zh = heights; // heights - if (hm) { - // adaptive increments based on z indices (var map to legacy code) - let zIncFirst = h0; - let zInc = h1; - let zIncMin = hm; - let zHeights = heights; - let zIndexes = indices; - let zOrdered = Object.values(zopt.zIndexes).map(v => parseFloat(v)); - // console.log('adaptive slicing', zIncMin, ':', zInc, 'from', zMin, 'to', zMax); - let zPos = zIncFirst, - zOI = 0, - zDelta, - zDivMin, - zDivMax, - zStep, - nextZ, - lzp = zPos; - // adaptive slice height - // first slice/height is fixed from base - zHeights.push(zIncFirst); - zIndexes.push(zIncFirst); - // console.log({zIncFirst, zOrdered}) - while (zPos < zMax && zOI < zOrdered.length) { - nextZ = zOrdered[zOI++]; - if (zPos >= nextZ) { - // console.log('skip',{zPos},'>=',{nextZ}); - continue; - } - zDelta = nextZ - zPos; - if (zDelta < zIncMin) { - // console.log('skip',{zDelta},'<',{zIncMin}); - continue; - } - zDivMin = Math.floor(zDelta / zIncMin); - zDivMax = Math.floor(zDelta / zInc); - if (zDivMax && zDivMax <= zDivMin) { - if (zDelta % zInc > 0.01) zDivMax++; - zStep = zDelta / zDivMax; - // console.log(`--- zDivMax <= zDivMin ---`, zStep, zDelta % zInc) - } else { - zStep = zDelta; - } - // console.log({nextZ, zPos, zDelta, zStep, zDivMin, zDivMax}) - while (zPos < nextZ) { - zHeights.push(zStep); - zIndexes.push(zPos + zStep); - zPos += zStep; - // console.log({D: zPos - lzp, zPos}) - // lzp = zPos; - } - } - // console.log({zIndexes, zHeights}); - } else { - // simple based + fixed increment - while (true) { - // reduce slice position by half layer height - let realz = (z - (h / 2)).round(3); - if (realz > zMax) { - break; - } - zh.push(h); - zi.push(realz); - h = h1; - z += h; - } - } - return zi; - }, + zGen, // slicer function (worker local or minion distributed) slicer(z, points, opts) { return (isConcurrent ? minions.sliceZ : sliceZ)(z, points, opts); @@ -338,7 +377,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) { return output.slices.map(data => { let { z, clip, lines, groups, changes } = data; if (!data.tops) return null; - let slice = newSlice(z).addTops(data.tops, { minArea: 0.1 }); + let slice = newSlice(z).addTops(data.tops, { minArea: CONSTANTS.MIN_POLY_AREA }); slice.index = indices.indexOf(z); slice.height = heights[slice.index]; slice.clips = clip; @@ -362,10 +401,97 @@ export function fdm_slice(settings, widget, onupdate, ondone) { return onSliceDone(slices); }).then(ondone); - async function onSliceDone(slices) { + // z index generator + function zGen(zopt) { + if (process.xray) { + return zopt.zIndexes; + } + let { zMin, zMax } = zopt; + let h1 = sliceHeight; + let h0 = Math.abs(zMin) < 0.0001 ? (sliceHeightBase || h1) : h1; + let hm = sliceMinHeight || 0; + let h = h0; + let z = h0; + let zi = indices; // indices + let zh = heights; // heights + if (hm) { + // adaptive increments based on z indices (var map to legacy code) + let zIncFirst = h0; + let zInc = h1; + let zIncMin = hm; + let zHeights = heights; + let zIndexes = indices; + let zOrdered = Object.values(zopt.zIndexes).map(v => parseFloat(v)); + // console.log('adaptive slicing', zIncMin, ':', zInc, 'from', zMin, 'to', zMax); + let zPos = zIncFirst, + zOI = 0, + zDelta, + zDivMin, + zDivMax, + zStep, + nextZ, + lzp = zPos; + // adaptive slice height + // first slice/height is fixed from base + zHeights.push(zIncFirst); + zIndexes.push(zIncFirst); + // console.log({zIncFirst, zOrdered}) + while (zPos < zMax && zOI < zOrdered.length) { + nextZ = zOrdered[zOI++]; + if (zPos >= nextZ) { + // console.log('skip',{zPos},'>=',{nextZ}); + continue; + } + zDelta = nextZ - zPos; + if (zDelta < zIncMin) { + // console.log('skip',{zDelta},'<',{zIncMin}); + continue; + } + zDivMin = Math.floor(zDelta / zIncMin); + zDivMax = Math.floor(zDelta / zInc); + if (zDivMax && zDivMax <= zDivMin) { + if (zDelta % zInc > CONSTANTS.ADAPTIVE_SLICE_TOLERANCE) zDivMax++; + zStep = zDelta / zDivMax; + // console.log(`--- zDivMax <= zDivMin ---`, zStep, zDelta % zInc) + } else { + zStep = zDelta; + } + // console.log({nextZ, zPos, zDelta, zStep, zDivMin, zDivMax}) + while (zPos < nextZ) { + zHeights.push(zStep); + zIndexes.push(zPos + zStep); + zPos += zStep; + // console.log({D: zPos - lzp, zPos}) + // lzp = zPos; + } + } + // console.log({zIndexes, zHeights}); + } else { + // simple based + fixed increment + while (true) { + // reduce slice position by half layer height + let realz = (z - (h / 2)).round(3); + if (realz > zMax) { + break; + } + zh.push(h); + zi.push(realz); + h = h1; + z += h; + } + } + return zi; + } + + async function onSliceDone(slices) { + /** + * Process automatic shadow-based support generation + */ + async function processAutoSupport() { + if (process.sliceSupportType !== 'automatic') { + return; + } - // new auto support demonstrator using cast shadow - if (process.sliceSupportType === 'automatic') { let stack = slices.slice(); let indices = stack.map(s => s.z); let zAngNorm = Math.sin(process.sliceSupportAngle * Math.PI / 180); @@ -384,310 +510,40 @@ export function fdm_slice(settings, widget, onupdate, ondone) { shadow = POLY.offset(shadow, process.sliceSupportExtra); } slice.supports = shadow; - // slice.output() - // .setLayer('shadow', 0xff0000) - // .addPolys(shadow); } } - // alert non-manifold parts - if (healed) { - onupdate(null, null, "part may not be manifold"); - } - - // remove all empty slices above part but leave below - // for multi-part (multi-extruder) setups where the void is ok - // also reverse because slicing occurs bottom-up - let found = false; - slices = slices.reverse().filter(slice => { - if (slice.tops.length) { - return found = true; - } else { - return found; - } - }).reverse(); - - // connect slices into linked list for island/bridge projections - for (let i=1; i { - fn(slice); - doupdate(slice.index, from, to, msg) - }); - } - - // do not hint polygon fill longer than a max span length - config.hint_len_max = util.sqr(process.sliceBridgeMax); - - // reset for solids, support projections - // and other annotations - slices.forEach(slice => { - slice.widget = widget; - slice.extruder = extruder; - slice.solids = []; - }); - - // just the top/bottom special solid layers or range defined solid layers - forSlices(0.15, 0.2, slice => { - let range = slice.params; - let isBottom = slice.index < bottomLayers; - let isTop = topLayers && slice.index > slices.length - topLayers - 1; - let isTopBase = isTop && slice.index === slices.length - topLayers; - let isDense = range.sliceFillSparse > 0.995; - let isSolid = (isBottom || ((isTop || isDense) && !vaseMode)); - let solidWidth = isSolid ? range.sliceFillWidth || 1 : 0; - if (solidWidth) { - let fillSpace = fillSpacing * solidWidth; - doSolidLayerFill(slice, fillSpace, sliceFillAngle); - } - if (slice.index === slices.length - 1) { - slice.isFlatsLayer = true; - } - if (isTopBase) { - // mark the first top solid supporting layer as a bridge - slice.isBridgeLayer = true; - } - sliceFillAngle = (sliceFillAngle + 90.0) % 360; - }, "solid layers"); - - // add lead in anchor when specified in belt mode (but not for synths) - if (isBelt) { - let { cosf, slope } = widget.belt; - // find adjusted zero point from slices - let smin = Infinity; - for (let slice of slices) { - let miny = Infinity; - for (let poly of slice.topPolys()) { - let y = poly.bounds.maxy; - let z = slice.z; - // at 45 degrees, 1mm in Z is 1mm in Y - let by = (slope * z) - y; - if (by < miny) miny = by; - if (by < smin) smin = by; + /** + * Process solid layer fills (top/bottom layers) + */ + function processSolidLayers() { + forSlices(0.15, 0.2, slice => { + let range = slice.params; + let isBottom = slice.index < bottomLayers; + let isTop = topLayers && slice.index > slices.length - topLayers - 1; + let isTopBase = isTop && slice.index === slices.length - topLayers; + let isDense = range.sliceFillSparse > CONSTANTS.DENSE_INFILL_THRESHOLD; + let isSolid = (isBottom || ((isTop || isDense) && !vaseMode)); + let solidWidth = isSolid ? range.sliceFillWidth || 1 : 0; + if (solidWidth) { + let fillSpace = fillSpacing * solidWidth; + doSolidLayerFill({ slice, spacing: fillSpace, angle: sliceFillAngle }); } - // mark slices with tops touching belt - slice.belt = { miny, touch: miny.round(3) < sliceHeightBase }; - } - // find max width of first 5 layers for brim additions - let start; - let minx = Infinity, maxx = -Infinity; - let peek = 0; - for (let slice of slices) { - if (slice.tops.length && peek++ < 5) { - for (let poly of slice.topPolys()) { - minx = Math.min(minx, poly.bounds.minx); - maxx = Math.max(maxx, poly.bounds.maxx); - } + if (slice.index === slices.length - 1) { + slice.isFlatsLayer = true; } - // find first slice touching belt for start of anchor - if (!start && slice.belt.touch) { - start = slice; + if (isTopBase) { + // mark the first top solid supporting layer as a bridge + slice.isBridgeLayer = true; } - } - // ensure we start against a layer with shells - while (start && start.up && start.topShells().length === 0) { - start = start.up; - } - // if a brim applies, add that width to anchor - let brim = getRangeParameters(process, 0).firstLayerBrim || 0; - if (brim) { - minx -= brim; - maxx += brim; - } - // array of added top.fill_sparse arrays - let adds = []; - let step = sliceHeight; - let anchorlen = (process.beltAnchor || process.firstLayerBeltLead) * cosf; - while (anchorlen && start && anchorlen >= sliceHeight) { - let addto = start.down; - if (!addto) { - addto = newSlice(start.z - step); - addto.extruder = extruder; - addto.belt = { }; - addto.height = start.height; - addto.up = start; - start.down = addto; - slices.splice(0,0,addto); - } else if (!addto.belt) { - console.log({addto_missing_belt: addto}); - addto.belt = {}; - } - addto.index = -1; - addto.belt.anchor = true; - // this allows the anchor to print bi-directionally - // by removing the forced start-point in print.js - addto.belt.touch = false; - let z = addto.z; - let y = (slope * z) - smin - (lineWidth / 2); - let splat = newPolygon().add(minx, y, z).add(maxx, y, z).setOpen(); - let snew = addto.addTop(splat).fill_sparse = [ splat ]; - adds.push(snew); - start = addto; - anchorlen -= (step * slope); - } - // add anchor bump - let bump = process.firstLayerBeltBump; - if (bump) { - adds = adds.reverse().slice(1, adds.length - 1); - let count = 1; - for (let add of adds) { - let poly = add[0]; - let y = count++ * -start.height * 2; - if (-y > bump) { - count--; - // break; - } - let first = poly.first(); - // add up/over/down to anchor line (close = down) - // which completes the bump perimeter - poly.push(poly.last().add({x:0, y, z:0})); - poly.push(poly.first().add({x:0, y, z:0})); - poly.setClosed(); - if (count > 2 && maxx - minx > 10) { - // add vertical hatch lines insibe bump shell - let mp = (maxx + minx) / 2; - let dx = (maxx - minx - 2); - dx = (Math.floor(dx / 3) * 3) / 2; - let fy = first.y; - let fz = first.z; - let n2 = lineWidth / 2; - for (let x = mp - dx; x <= mp + dx ; x += 3) { - add.push(newPolygon().add(x, fy - n2, fz).add(x, fy + y + n2, fz).setOpen() ); - } - } - } - } - // experimental emboss text on a flat underside of an object - // in belt mode only - if (process.pooch && self.OffscreenCanvas) { - const { length, width, height, text } = process.pooch; - let firstZ, firstI, lastZ, lastI, minX = 0, maxX = 0, maxY = 0; - // locate suitable flat spot - for (let slice of slices) { - let { belt } = slice; - if (!belt.touch) { - continue; - } - let index = slice.index; - let z = slice.z; - outer: for (let poly of slice.topPolys()) { - for (let i=0, p=poly.points, l=p.length; i 0 && p0y < 3 && Math.abs(p0y - p1y) < 0.01; - let x_ok = Math.abs(width - Math.abs(p1.x - p0.x)) < 1 - if (y_ok && x_ok) { - if (i_ok) { - firstZ = firstZ || z; - firstI = firstI || index; - lastZ = z; - lastI = index; - minX = Math.min(p0.x, p1.x); - maxX = Math.max(p0.x, p1.x); - maxY = p0y; - break outer; - } else { - firstZ = lastI = 0; - } - } - } - } - } - let dy = Math.abs(height - maxY * (1 / Math.sqrt(2))); - let dz = Math.abs(length - ((lastZ - firstZ) * Math.sqrt(2))); - if (dy < 0.1 && dz < 1) { - // console.log('FOUND', { firstI, lastI, minX, maxX, maxY }); - let span = lastI - firstI - 2; // x = down the belt - let tall = width * 2; - let can = new self.OffscreenCanvas(span, tall); - let ctx = can.getContext("2d"); - ctx.scale(1.2, 1); - ctx.font = '24px sans-serif'; - // ctx.fillStyle = 'black'; - ctx.textBaseline = "bottom"; - ctx.fillText(text, 1, tall - 1); - let img = ctx.getImageData(0, 0, span, tall).data.buffer; - let rgb = new Uint32Array(img); - // console.log({ img, rgb, p: rgb.filter(v => v) }); - for (let x=0; x=0; y--) { - let pix = rgb[y * span + x]; - pix = ( - ((pix >> 24) & 0xff) + - ((pix >> 16) & 0xff) + - ((pix >> 8) & 0xff) - ) / 3; - str += pix > 30 ? '*' : '-'; - maxp = Math.max(maxp, pix); - if (pix > 30) { - if (start >= 0) { - end = tall - y; - } else { - start = tall - y; - } - } else { - if (start >= 0 && end > start) { - lines.push({ start, end }); - } - start = end = undefined; - } - } - console.log((x).toString().padStart(2,0),str,maxp | 0,lines); - if (lines.length) { - let slice = slices[firstI + x + 1]; - let supps = slice.supports = slice.supports || []; - let z = slice.z; - let y = z - smin - (lineWidth / 2); - for (let line of lines) { - supps.push(newPolygon() - .add(minX + line.start / 2, y, z) - .add(minX + line.end / 2, y, z) - .setOpen() - ); - } - } - } - } - } + sliceFillAngle = (sliceFillAngle + 90.0) % 360; + }, "solid layers"); } - // calculations only relevant when solid layers are used - // layer boolean diffs need to be computed to find flat areas to fill - // and overhangs that need to be supported. these are stored in flats - // and bridges, projected up/down, and merged into an array of solids - if (!vaseMode) { + /** + * Process layer diffs and project solid areas + */ + function processLayerDiffs() { profileStart("delta"); forSlices(0.2, 0.34, slice => { let params = slice.params || process; @@ -696,6 +552,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) { doDiff(slice, { min: solidMinArea, grow: sliceFillGrow }); }, "layer deltas"); profileEnd(); + profileStart("delta-project"); forSlices(0.34, 0.35, slice => { let params = slice.params || process; @@ -705,6 +562,12 @@ export function fdm_slice(settings, widget, onupdate, ondone) { if (bottomLayers) projectBridges(slice, bottomLayers); }, "layer deltas"); profileEnd(); + } + + /** + * Process solid fill patterns + */ + async function processSolidFills() { profileStart("solid-fill") let promises = isConcurrent ? [] : undefined; forSlices(0.35, promises ? 0.4 : 0.5, slice => { @@ -713,7 +576,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) { let solidWidth = params.sliceFillWidth || 1; let fillSpace = fillSpacing * solidWidth; let solidMinArea = params.sliceSolidMinArea; - doSolidsFill(slice, fillSpace, sliceFillAngle, solidMinArea, promises); + doSolidsFill({ slice, spacing: fillSpace, angle: sliceFillAngle, minArea: solidMinArea, promises }); sliceFillAngle = (sliceFillAngle + 90.0) % 360; }, "fill solids"); // very last layer (top) is set to finish solid rate @@ -726,9 +589,10 @@ export function fdm_slice(settings, widget, onupdate, ondone) { profileEnd(); } - // for "real" objects, fill the remaining voids with sparse fill - // sparse layers only present when non-vase mose and sparse % > 0 - if (!vaseMode) { + /** + * Process sparse infill patterns + */ + async function processSparseInfill() { let lastType; let promises = isConcurrent ? [] : undefined; forSlices(0.5, promises ? 0.55 : 0.7, slice => { @@ -769,8 +633,10 @@ export function fdm_slice(settings, widget, onupdate, ondone) { } } - // fill all supports (auto and manual) - if (supportDensity) { + /** + * Process support structure fills + */ + async function processSupportFills() { profileStart("support-fill"); let promises = false && isConcurrent ? [] : undefined; forSlices(0.8, promises ? 0.88 : 0.9, slice => { @@ -796,7 +662,10 @@ export function fdm_slice(settings, widget, onupdate, ondone) { profileEnd(); } - if (isBrick) { + /** + * Process brick/interleave mode slicing + */ + function processBrickMode() { let indices = slices.map(s => s.index); let first = indices[1]; let last = indices[indices.length - 2]; @@ -842,35 +711,151 @@ export function fdm_slice(settings, widget, onupdate, ondone) { slices.forEach((s,i) => s.index = i); } + // calculate % complete and call onupdate() + function doupdate(index, from, to, msg) { + trackupdate(index / slices.length, from, to, msg); + } + + function trackupdate(pct, from, to, msg) { + onupdate(0.5 + (from + (pct * (to - from))) * 0.5, msg); + } + + // for each slice, performe a function and call doupdate() + function forSlices(from, to, fn, msg) { + slices.forEach(slice => { + fn(slice); + doupdate(slice.index, from, to, msg) + }); + } + + // new auto support demonstrator using cast shadow + await processAutoSupport(); + + // alert non-manifold parts + if (healed) { + onupdate(null, null, "part may not be manifold"); + } + + // remove all empty slices above part but leave below + // for multi-part (multi-extruder) setups where the void is ok + // also reverse because slicing occurs bottom-up + let found = false; + slices = slices.reverse().filter(slice => { + if (slice.tops.length) { + return found = true; + } else { + return found; + } + }).reverse(); + + // connect slices into linked list for island/bridge projections + for (let i=1; i { + slice.widget = widget; + slice.extruder = extruder; + slice.solids = []; + }); + + // process solid layers (top/bottom) + processSolidLayers(); + + // add lead in anchor when specified in belt mode (but not for synths) + if (isBelt) { + let { slope } = widget.belt; + + // Generate belt anchor + generateBeltAnchor({ + slices, + widget, + process, + lineWidth, + sliceHeight, + sliceHeightBase, + extruder + }); + + // Calculate smin for pooch text embossing + let smin = Infinity; + for (let slice of slices) { + if (slice.belt && slice.belt.miny < smin) { + smin = slice.belt.miny; + } + } + + // Experimental emboss text on flat underside + embossBeltPooch({ + slices, + widget, + process, + lineWidth, + smin + }); + } + + // calculations only relevant when solid layers are used + // layer boolean diffs need to be computed to find flat areas to fill + // and overhangs that need to be supported. these are stored in flats + // and bridges, projected up/down, and merged into an array of solids + if (!vaseMode) { + processLayerDiffs(); + await processSolidFills(); + } + + // for "real" objects, fill the remaining voids with sparse fill + // sparse layers only present when non-vase mode and sparse % > 0 + if (!vaseMode) { + await processSparseInfill(); + } + + // fill all supports (auto and manual) + if (supportDensity) { + await processSupportFills(); + } + + // brick/interleave mode processing + if (isBrick) { + processBrickMode(); + } + // render if not explicitly disabled if (render) { forSlices(0.9, 1.0, slice => { let params = slice.params || process; doRender(slice, params, { dark: controller.dark, - devel: controller.devel + devel: controller.devel, + renderContext }); }, "render"); } if (isBelt) { - let bounds = newBounds(); - let slice = slices[0]; - if (slice.tops) { - for (let top of slice.tops) { - bounds.merge(top.poly.bounds); - } - } else if (slice.supports) { - for (let poly of slice.supports) { - bounds.merge(poly); - } - } - widget.belt.miny = -bounds.miny; - widget.belt.midy = (bounds.miny + bounds.maxy) / 2; + finalizeBeltBounds({ slices, widget }); } } } +// replace with tip2tipEmit? function connect_lines(lines, maxd = Infinity) { const newlines = []; let op2; @@ -905,12 +890,22 @@ function bound(v,min,max) { return Math.max(min,Math.min(max,v)); } +/** + * Render slice visualization for preview + * @param {Slice} slice - Slice to render + * @param {SliceParams} params - Slice parameters + * @param {Object} opt - Render options + * @param {boolean} opt.dark - Use dark color scheme + * @param {boolean} opt.devel - Enable development visualization + * @param {RenderContext} opt.renderContext - Rendering context + */ function doRender(slice, params, opt = {}) { - const { dark, devel } = opt; + const { dark, devel, renderContext: ctx } = opt; const Color = dark ? COLOR_DARK : COLOR; const output = slice.output(); const height = slice.height / 2; const solidWidth = params.sliceFillWidth || 1; + const { offset, isThin, isFlat } = ctx; if (slice.tops) // missing for supports slice.tops.forEach(top => { @@ -920,11 +915,11 @@ function doRender(slice, params, opt = {}) { if (top.shells?.length) output .setLayer("shells", Color.shell) - .addPolys(top.shells || [], vopt({ offset, height, clean: true })); + .addPolys(top.shells || [], vopt({ offset, height, clean: true }, ctx)); if (isThin && top.thin_wall?.length) output .setLayer("walls", Color.thin) - .addPolys(top.thin_wall.map(a => a.map(p => newPolygon().centerCircle(p, p.r, 12).setZ(slice.z))).flat(), vopt({ offset, height })); + .addPolys(top.thin_wall.map(a => a.map(p => newPolygon().centerCircle(p, p.r, 12).setZ(slice.z))).flat(), vopt({ offset, height }, ctx)); if (!isThin && top.thin_wall?.length) output .setLayer("walls", Color.thin) @@ -934,27 +929,27 @@ function doRender(slice, params, opt = {}) { newPolygon(a.map(p => newPoint(p.x, p.y))) .setZ(slice.z) .closeIf(height) - ), vopt({ offset, height })); + ), vopt({ offset, height }, ctx)); if (top.fill_lines?.length) output .setLayer("solid fill", Color.fill) - .addLines(top.fill_lines || [], vopt({ offset: offset * solidWidth, height, z:slice.z })); + .addLines(top.fill_lines || [], vopt({ offset: offset * solidWidth, height, z:slice.z }, ctx)); if (!(slice.belt?.anchor)) output .setLayer("sparse fill", Color.infill) - .addPolys(top.fill_sparse || [], vopt({ offset, height, outline: true, trace:true })) + .addPolys(top.fill_sparse || [], vopt({ offset, height, outline: true, trace:true }, ctx)) if (slice.belt?.anchor) output .setLayer("anchor", Color.anchor) - .addPolys(top.fill_sparse || [], vopt({ offset, height, outline: true, trace:true })) + .addPolys(top.fill_sparse || [], vopt({ offset, height, outline: true, trace:true }, ctx)) if (top.thin_fill?.length) output .setLayer("thin fill", Color.thin) - .addLines(top.thin_fill, vopt({ offset, height })); + .addLines(top.thin_fill, vopt({ offset, height }, ctx)); if (top.gaps?.length && devel) output .setLayer("gaps", Color.gaps) - .addPolys(top.gaps, vopt({ offset, height, thin: true })); + .addPolys(top.gaps, vopt({ offset, height, thin: true }, ctx)); if (isThin && devel && top.fill_off?.length) { slice.output() @@ -981,12 +976,12 @@ function doRender(slice, params, opt = {}) { if (slice.supports && params.sliceSupportOutline) output .setLayer("support", Color.support) - .addPolys(slice.supports, vopt({ offset, height })); + .addPolys(slice.supports, vopt({ offset, height }, ctx)); if (slice.supports) slice.supports.forEach(poly => { if (poly.fill) output .setLayer("support", Color.support) - .addLines(poly.fill, vopt({ offset, height })); + .addLines(poly.fill, vopt({ offset, height }, ctx)); }); if (slice.xray) { @@ -1037,7 +1032,16 @@ export function doShells(slice, count, offset1, offsetN, fillOffset, opt = {}) { * @param {number} angle * @param {number} density */ - function doSolidLayerFill(slice, spacing, angle) { +/** + * Create solid fill layer by filling all top polygons with alternating pattern + * @param {Object} args - Options object + * @param {Slice} args.slice - Slice to fill + * @param {number} args.spacing - Fill line spacing in mm + * @param {number} args.angle - Fill angle in degrees + */ +function doSolidLayerFill(args) { + const { slice, spacing, angle } = args; + if (slice.xray) { return; } @@ -1298,7 +1302,7 @@ export function projectFlats(slice, count, expand) { slice.finishSolids = true; slice.isFlatsLayer = true; const flats = expand ? POLY.expand(slice.flats, expand) : slice.flats; - projectSolid(slice, flats, count, false, true); + projectSolid({ slice, polys: flats, count, up: false, first: true }); } }; @@ -1312,14 +1316,22 @@ export function projectBridges(slice, count) { slice.finishSolids = true; slice.isBridgeLayer = true; } - projectSolid(slice, slice.bridges, count, true, true); + projectSolid({ slice, polys: slice.bridges, count, up: true, first: true }); }; /** - * fill projected areas and store line data + * Fill projected solid areas and store line data + * + * @param {Object} args - Options object + * @param {Slice} args.slice - Slice to process + * @param {number} args.spacing - Fill line spacing in mm + * @param {number} args.angle - Fill angle in degrees + * @param {number} [args.minArea] - Minimum polygon area in mm² (default: 1) + * @param {Promise[]} [args.promises] - Array for async operations * @return {boolean} true if filled, false if not */ -function doSolidsFill(slice, spacing, angle, minArea, fillQ) { +function doSolidsFill(args) { + const { slice, spacing, angle, minArea, promises: fillQ } = args; let minarea = minArea || 1, tops = slice.tops, solids = slice.solids; @@ -1381,8 +1393,8 @@ function doSolidsFill(slice, spacing, angle, minArea, fillQ) { if (false && stop.length) { let top_area = top.poly.areaDeep(); let stop_area = stop.map(p => p.areaDeep()).reduce((a,v) => a + v); - // if the solid area > 50% of the top area, make entire layer solid - if (stop_area / tops_area > 0.5) { + // if the solid area > threshold of the top area, make entire layer solid + if (stop_area / tops_area > CONSTANTS.SOLID_LAYER_COVERAGE_THRESHOLD) { make_solid_layer = true; } } @@ -1394,7 +1406,7 @@ function doSolidsFill(slice, spacing, angle, minArea, fillQ) { for (let top of tops) { top.solids = []; } - doSolidLayerFill(slice, spacing, angle); + doSolidLayerFill({ slice, spacing, angle }); return; } @@ -1423,18 +1435,37 @@ function doSolidsFill(slice, spacing, angle, minArea, fillQ) { } } if (tofill.length > 0) { - doFillArea(fillQ, tofill, angle, spacing, newfill); + doFillArea({ promises: fillQ, polys: tofill, angle, spacing, output: newfill }); } if (angfill.length > 0) { top.fill_lines_ang = {spacing:spacing,list:[],poly:[]}; for (let af of angfill) { - doFillArea(fillQ, [af], af.fillang.angle + 45, spacing, newfill); + doFillArea({ + promises: fillQ, + polys: [af], + angle: af.fillang.angle + CONSTANTS.ANGLED_FILL_ROTATION, + spacing, + output: newfill + }); } } } } -function doFillArea(fillQ, polys, angle, spacing, output, minLen, maxLen) { +/** + * Fill area with line pattern + * + * @param {Object} args - Options object + * @param {Promise[]} [args.promises] - Array for async operations + * @param {Polygon[]} args.polys - Polygons to fill + * @param {number} args.angle - Fill angle in degrees + * @param {number} args.spacing - Fill line spacing in mm + * @param {Array} args.output - Output array for fill lines + * @param {number} [args.minLen] - Minimum line length + * @param {number} [args.maxLen] - Maximum line length + */ +function doFillArea(args) { + const { promises: fillQ, polys, angle, spacing, output, minLen, maxLen } = args; if (fillQ) { fillQ.push(self.kiri_worker.minions.fill(polys, angle, spacing, output, minLen, maxLen)); } else { @@ -1442,7 +1473,52 @@ function doFillArea(fillQ, polys, angle, spacing, output, minLen, maxLen) { } } -function doSupportFill(args) { + /** + * Generate support structure fill patterns for a slice + * + * This function processes support polygons by: + * 1. Unioning all support polygons to eliminate overlaps + * 2. Clipping supports to avoid collision with the part (using clip offsets) + * 3. Applying optional gap spacing from upper/lower layers + * 4. Generating hatching fill patterns based on density + * 5. Optionally connecting fill lines to reduce travel moves + * + * Process flow: + * - Union: Merge overlapping support regions into unified polygons + * - Clip: Subtract part geometry (slice.clips) to prevent support/part collision + * - Gap: If gap specified, also subtract clips from adjacent layers for clearance + * - Fill: Generate hatching pattern with auto or specified angle + * - Connect: Optionally link nearby fill line endpoints to reduce travels + * + * @param {SupportFillOptions} args - Configuration object + * @param {Promise[]} [args.promises] - Array for async fill operations (concurrent mode) + * @param {Slice} args.slice - Current slice to process + * @param {number} args.lineWidth - Extrusion width in mm (typically nozzle diameter) + * @param {number} args.density - Fill density from 0.0 (0%) to 1.0 (100%) + * Typical support density is 0.10-0.25 (10-25%) + * @param {number} args.minArea - Minimum polygon area in mm² to keep (default: 0.1) + * Smaller polygons are filtered out as noise + * @param {boolean} args.isBelt - True for belt printer mode (affects auto-angle calculation) + * @param {number} [args.angle] - Fill angle in degrees. Special values: + * - undefined/null: Auto-calculate based on polygon shape + * - >= 1000: Auto-calculate (1000=0° for tall, 1090=90° for wide) + * - 0-360: Use specified angle + * @param {boolean} args.outline - Include support perimeter outline in output + * If false, only interior fill lines are generated + * @param {number} [args.gap] - Gap distance in mm between support and part surfaces + * When specified, subtracts adjacent layer clips for clearance + * + * @modifies {Slice} slice.supports - Updated with filled support polygons (array of Polygon) + * Each polygon has a .fill property with fill lines + * @modifies {Slice} slice.supportOutline - Stores support outline polygons + * + * @see fillSupportPolys - Called internally to generate fill patterns + * @see CONSTANTS.SUPPORT_INSET_RATIO - Inset ratio (1/3 of line width) + * @see CONSTANTS.SUPPORT_AUTO_ANGLE_WIDE - Auto angle for wide polygons (1090 = 90°) + * @see CONSTANTS.SUPPORT_AUTO_ANGLE_TALL - Auto angle for tall polygons (1000 = 0°) + * @see CONSTANTS.SUPPORT_CONNECT_DISTANCE_MULT - Distance multiplier for line connection (2x) + */ + function doSupportFill(args) { const { promises, slice, lineWidth, density, minArea, isBelt, angle, outline, gap } = args; let supports = slice.supports, min = minArea || 0.1, @@ -1480,35 +1556,113 @@ function doSupportFill(args) { slice.supports = supports; }; +/** + * Generate fill patterns for individual support polygons + * + * This is an internal helper function called by doSupportFill() that processes each support + * polygon individually to generate the hatching fill patterns. It handles: + * - Automatic fill angle calculation based on polygon aspect ratio + * - Intelligent inset to prevent perimeter over-extrusion + * - Optional line connection to reduce travel moves when no outline is needed + * + * ## Algorithm Details + * + * 1. **Spacing Calculation**: Converts density percentage to actual spacing distance + * - Formula: spacing = lineWidth * (1 / density) + * - Example: 0.4mm line × (1 / 0.15) = 2.67mm spacing for 15% density + * + * 2. **Auto-Angle Selection**: Chooses fill direction based on polygon shape + * - Wide polygons (width/height > 1): Use 1090 (90° horizontal lines) + * - Tall polygons (width/height ≤ 1): Use 1000 (0° vertical lines) + * - Belt mode: Always use 1090 (90°) regardless of aspect ratio + * - Rationale: Aligns fill lines with longest dimension for structural strength + * + * 3. **Inset Operation**: Shrinks polygon before filling + * - Inset distance: lineWidth × (1/3) = 0.33× line width + * - Purpose: Prevents over-extrusion where fill lines meet perimeter + * - Creates ~0.13mm gap for typical 0.4mm nozzle + * + * 4. **Fill Generation**: Creates parallel hatching lines at calculated angle + * - Uses doFillArea() to generate scan lines through inset polygon + * - Stores result in poly.fill array + * + * 5. **Line Connection** (optional): Reduces travel moves + * - Only when outline=false (no support perimeter needed) + * - Connects nearby line endpoints within 2× spacing distance + * - Converts many short lines into fewer longer continuous paths + * - Reduces print time and stringing + * + * @param {Object} args - Configuration object + * @param {Promise[]} [args.promises] - Array for async fill operations (concurrent mode) + * Pass same array through all calls for parallelism + * @param {Polygon[]} args.polys - Array of support polygons to fill + * Must have valid .bounds property for aspect ratio calculation + * @param {number} args.lineWidth - Extrusion width in mm (typically 0.4mm for 0.4mm nozzle) + * @param {number} args.density - Fill density from 0.0 (0%) to 1.0 (100%) + * Typical support values: 0.10-0.25 (10-25%) + * Higher density = stronger support but harder to remove + * @param {number} args.z - Current Z height in mm (for polygon operations) + * @param {boolean} args.isBelt - True for belt printer mode + * Forces use of SUPPORT_AUTO_ANGLE_WIDE (1090/90°) + * @param {number} [args.angle] - Fill angle in degrees. Special values: + * - undefined/null: Auto-calculate based on polygon aspect ratio + * - >= 1000: Auto-calculate (1000=0° for tall, 1090=90° for wide) + * - 0-360: Use specified angle + * @param {boolean} args.outline - Controls line connection behavior: + * - true: Keep individual fill lines (for support with perimeter) + * - false: Connect nearby lines to reduce travel moves + * + * @modifies {Polygon} Each poly in polys array gets a new .fill property containing: + * - Array of Polygon objects representing fill lines + * - Lines are either individual segments (outline=true) or + * connected paths (outline=false) for travel reduction + * + * @see doSupportFill - Parent function that calls this after clipping/union + * @see doFillArea - Lower-level function that generates the actual scan lines + * @see connect_lines - Utility that connects nearby line segments + * @see CONSTANTS.SUPPORT_AUTO_ANGLE_WIDE - Auto angle for wide polygons (1090 = 90°) + * @see CONSTANTS.SUPPORT_AUTO_ANGLE_TALL - Auto angle for tall polygons (1000 = 0°) + * @see CONSTANTS.SUPPORT_INSET_RATIO - Inset ratio for fill area (1/3) + * @see CONSTANTS.SUPPORT_CONNECT_DISTANCE_MULT - Distance multiplier for line connection (2x) + */ + function fillSupportPolys({ promises, polys, lineWidth, density, z, isBelt, angle, outline }) { // calculate fill density let spacing = lineWidth * (1 / density); - polys.forEach(function (poly) { + for (let poly of polys) { // calculate angle based on width/height ratio - let auto = isBelt || (poly.bounds.width() / poly.bounds.height() > 1) ? 1090 : 1000; - // inset support poly for fill lines 33% of nozzle width - let inset = POLY.offset([poly], -lineWidth/3, {flat: true, z, wasm: true}); + let auto = isBelt || (poly.bounds.width() / poly.bounds.height() > 1) ? CONSTANTS.SUPPORT_AUTO_ANGLE_WIDE : CONSTANTS.SUPPORT_AUTO_ANGLE_TALL; + // inset support poly for fill lines + let inset = POLY.offset([poly], -lineWidth * CONSTANTS.SUPPORT_INSET_RATIO, {flat: true, z, wasm: true}); // do the fill if (inset && inset.length > 0) { - doFillArea(promises, inset, angle || auto, spacing, poly.fill = []); + doFillArea({ + angle: angle || auto, + output: poly.fill = [], + polys: inset, + promises, + spacing + }); if (!outline && poly.fill.length) { - poly.fill = connect_lines(poly.fill, spacing * 2); + poly.fill = connect_lines(poly.fill, spacing * CONSTANTS.SUPPORT_CONNECT_DISTANCE_MULT); } } - return true; - }); + } } /** - * - * @param {Slice} slice - * @param {Polygon[]} polys - * @param {number} count - * @param {boolean} up - * @param {boolean} first - * @returns {*} + * Project solid areas up or down through slices + * + * @param {Object} args - Options object + * @param {Slice} args.slice - Current slice + * @param {Polygon[]} args.polys - Polygons to project + * @param {number} args.count - Number of layers to project + * @param {boolean} args.up - Project upward (true) or downward (false) + * @param {boolean} args.first - First projection (hint fill angles) */ -function projectSolid(slice, polys, count, up, first) { +function projectSolid(args) { + const { slice, polys, count, up, first } = args; + if (slice && !up && count === 1) { slice.isBridgeLayer = true; } @@ -1523,7 +1677,7 @@ function projectSolid(slice, polys, count, up, first) { } addSolidFills(slice, clones); if (count > 0) { - if (up) projectSolid(slice.up, polys, count-1, true, false); - else projectSolid(slice.down, polys, count-1, false, false); + if (up) projectSolid({ slice: slice.up, polys, count: count-1, up: true, first: false }); + else projectSolid({ slice: slice.down, polys, count: count-1, up: false, first: false }); } } From 9a463881d1c20c519e992513060a0393bed93ad5 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Tue, 6 Jan 2026 19:16:11 -0500 Subject: [PATCH 113/154] reflow / rename / document fdm slicing --- src/kiri/mode/fdm/post.js | 11 +- src/kiri/mode/fdm/slice.js | 315 +++++++++++++++++++------------------ src/kiri/mode/sla/slice.js | 8 +- src/kiri/run/minion.js | 4 +- 4 files changed, 178 insertions(+), 160 deletions(-) diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index 68ac94d2..10cdbe40 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -9,14 +9,17 @@ import { slicer } from '../../../geo/slicer.js'; import { Point } from '../../../geo/point.js'; import { Slope } from '../../../geo/slope.js'; -slicer.slicePost.FDM = slicePost; +// tell the slicer what to do in FDM mode +// with slices (post processing). the post +// processor may depend on worker / minion context +slicer.slicePost.FDM = slicerPostProcessor; /** * may run in minion or worker context. do not create objects * that will not quickly encode in threaded mode. add to existing * data object. return is ignored. */ -export function slicePost(data, options) { +function slicerPostProcessor(data, options) { const { groups, z } = data; const { post_args, useAssembly, zIndexes } = options; const { process, vaseMode } = post_args; @@ -81,7 +84,7 @@ export function slicePost(data, options) { const nutops = []; // co-locate shell processing with top generation in slicer for (let top of data.tops) { - nutops.push(doTopShells(z, top, count, offset/2, offset, fillOff, { + nutops.push(layerProcessTop(z, top, count, offset/2, offset, fillOff, { thinType, vaseMode, useAssembly @@ -687,7 +690,7 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { * may run in minion or worker context. performs shell offsetting * including thin wall detection when enabled */ -export function doTopShells(z, top, count, offset1, offsetN, fillOffset, opt = {}) { +export function layerProcessTop(z, top, count, offset1, offsetN, fillOffset, opt = {}) { // pretend we're a top object in minions if (!top.poly) { top = { poly: top }; diff --git a/src/kiri/mode/fdm/slice.js b/src/kiri/mode/fdm/slice.js index 0493515e..d79f3066 100644 --- a/src/kiri/mode/fdm/slice.js +++ b/src/kiri/mode/fdm/slice.js @@ -1,7 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { base, util } from '../../../geo/base.js'; -import { doTopShells } from './post.js'; +import { layerProcessTop } from './post.js'; import { fill, fill_fixed } from './fill.js'; import { generateBeltAnchor, embossBeltPooch, finalizeBeltBounds } from './belt.js'; import { getRangeParameters } from './driver.js'; @@ -10,8 +9,7 @@ import { newPolygon } from '../../../geo/polygon.js'; import { newSlice } from '../../core/slice.js'; import { polygons as POLY } from '../../../geo/polygons.js'; import { slice, sliceZ } from '../../../geo/slicer.js'; - -const { config } = base; +import { util } from '../../../geo/base.js'; const CONSTANTS = { // Support fill @@ -174,55 +172,6 @@ function vopt(opt, ctx) { return opt; } -/** - * DRIVER SLICE CONTRACT - * - * global slice operation run after all individual widget slice() complete - */ -export function slicePost(settings, onupdate) { - // future home of brim and anchor generation - let widgets = Object.values(self.kiri_worker.cache) - .filter(w => !w.meta.disabled) - .sort((a,b) => { - return a.slices[0].z - b.slices[0].z - }); - // assign grid_id which can be embedded in gcode and - // used by the controller to cancel objects during print - let { bounds } = settings; - for (let widget of widgets) { - let { pos, box } = widget.track; - // calculate top/left coordinate for widget - // relative to bounding box for all widgets - let tl = { - x: Math.round((pos.x - box.w/2 - bounds.min.x) / 10) + 1, - y: Math.round((pos.y - box.h/2 - bounds.min.y) / 10) + 1 - }; - widget.track.grid_id = tl.x * 100 + tl.y; - } - // count extruders used - let ext = []; - for (let w of widgets) { - if (w.anno && w.anno.extruder >= 0) { - let e = w.anno.extruder; - if (ext.indexOf(e) < 0) { - ext.push(e); - } - } - } - // sort widgets by first slice Z - widgets.sort((a,b) => { - return a.slices[0].z - b.slices[0].z; - }); - // give first widget a pass since it should have the anchor - widgets.shift(); - // remove anchor slices from other widgets (only with multi-material) - if (ext.length > 1) { - for (let w of widgets) { - w.slices = w.slices.filter(s => s.index >= 0); - } - } -}; - /** * DRIVER SLICE CONTRACT * @@ -258,7 +207,7 @@ export function sliceOne(settings, widget, onupdate, ondone) { shellOffset = lineWidth, fillSpacing = lineWidth, fillOffset = lineWidth * fillOffsetMult, - clipOffset = process.sliceSupportOffset, + clipOffset = process.sliceSupportOffset + lineWidth / 2, sliceFillAngle = process.sliceFillAngle, supportDensity = process.sliceSupportDensity, { sliceCompInner, sliceCompOuter } = process; @@ -344,6 +293,8 @@ export function sliceOne(settings, widget, onupdate, ondone) { } } + // create Slice objects for specified list of Z heights + // zGen() produces the list (or empty for slicer auto-detected) slice(points, { strict: true, debug: process.xray, @@ -372,7 +323,13 @@ export function sliceOne(settings, widget, onupdate, ondone) { onupdate(update) { return onupdate(0.0 + update * 0.5) } - }).then((output) => { + }) + .then(decodeSlices) + .then(processSlices) + .then(ondone); + + // turn slicer raw output into Slice objects + function decodeSlices(output) { // post process slices and re-incorporate missing meta-data return output.slices.map(data => { let { z, clip, lines, groups, changes } = data; @@ -397,9 +354,7 @@ export function sliceOne(settings, widget, onupdate, ondone) { } return slice; }).filter(s => s); - }).then(slices => { - return onSliceDone(slices); - }).then(ondone); + } // z index generator function zGen(zopt) { @@ -483,9 +438,9 @@ export function sliceOne(settings, widget, onupdate, ondone) { return zi; } - async function onSliceDone(slices) { + async function processSlices(slices) { /** - * Process automatic shadow-based support generation + * Process shadow-based support generation */ async function processAutoSupport() { if (process.sliceSupportType !== 'automatic') { @@ -504,17 +459,22 @@ export function sliceOne(settings, widget, onupdate, ondone) { stack.sort((a,b) => b.z - a.z); // accumulate deltas } + // 1. accumulate / union shadow coverage top down + // 2. trim to area outside slice.clips + for (let slice of stack) { let shadow = await widget.shadowAt(slice.z, true); if (process.sliceSupportExtra) { shadow = POLY.offset(shadow, process.sliceSupportExtra); } slice.supports = shadow; + slice.output().setLayer("shadow", 0xff0000).addPolys(shadow); } } /** - * Process solid layer fills (top/bottom layers) + * Process top and bottom layers or any other + * layers detected and marked for solid fill */ function processSolidLayers() { forSlices(0.15, 0.2, slice => { @@ -527,7 +487,7 @@ export function sliceOne(settings, widget, onupdate, ondone) { let solidWidth = isSolid ? range.sliceFillWidth || 1 : 0; if (solidWidth) { let fillSpace = fillSpacing * solidWidth; - doSolidLayerFill({ slice, spacing: fillSpace, angle: sliceFillAngle }); + layerMakeSolid({ slice, spacing: fillSpace, angle: sliceFillAngle }); } if (slice.index === slices.length - 1) { slice.isFlatsLayer = true; @@ -544,15 +504,16 @@ export function sliceOne(settings, widget, onupdate, ondone) { * Process layer diffs and project solid areas */ function processLayerDiffs() { + // boolean diff layers to detect bridges and flats profileStart("delta"); forSlices(0.2, 0.34, slice => { let params = slice.params || process; let solidMinArea = params.sliceSolidMinArea; let sliceFillGrow = params.sliceFillGrow; - doDiff(slice, { min: solidMinArea, grow: sliceFillGrow }); + layerDiff(slice, { min: solidMinArea, grow: sliceFillGrow }); }, "layer deltas"); profileEnd(); - + // project bridges and flats up and down into part profileStart("delta-project"); forSlices(0.34, 0.35, slice => { let params = slice.params || process; @@ -560,6 +521,7 @@ export function sliceOne(settings, widget, onupdate, ondone) { bottomLayers = params.sliceBottomLayers || 0; if (topLayers) projectFlats(slice, topLayers); if (bottomLayers) projectBridges(slice, bottomLayers); + if (slice.flats) POLY.setZ(slice.flats, slice.z); }, "layer deltas"); profileEnd(); } @@ -572,11 +534,10 @@ export function sliceOne(settings, widget, onupdate, ondone) { let promises = isConcurrent ? [] : undefined; forSlices(0.35, promises ? 0.4 : 0.5, slice => { let params = slice.params || process; - let first = slice.index === 0; let solidWidth = params.sliceFillWidth || 1; let fillSpace = fillSpacing * solidWidth; let solidMinArea = params.sliceSolidMinArea; - doSolidsFill({ slice, spacing: fillSpace, angle: sliceFillAngle, minArea: solidMinArea, promises }); + layerFillSolids({ slice, spacing: fillSpace, angle: sliceFillAngle, minArea: solidMinArea, promises }); sliceFillAngle = (sliceFillAngle + 90.0) % 360; }, "fill solids"); // very last layer (top) is set to finish solid rate @@ -601,7 +562,7 @@ export function sliceOne(settings, widget, onupdate, ondone) { return; } let newType = params.sliceFillType; - doSparseLayerFill(slice, { + layerSparseFill(slice, { settings, process, device, @@ -642,7 +603,7 @@ export function sliceOne(settings, widget, onupdate, ondone) { forSlices(0.8, promises ? 0.88 : 0.9, slice => { let params = slice.params || process; let density = params.sliceSupportDensity; - doSupportFill({ + layerSupportFill({ angle: process.sliceSupportFill, density, gap: process.sliceSupportGap, @@ -765,9 +726,6 @@ export function sliceOne(settings, widget, onupdate, ondone) { slice.params = getRangeParameters(process, slice.index); } - // do not hint polygon fill longer than a max span length - config.hint_len_max = util.sqr(process.sliceBridgeMax); - // reset for solids, support projections // and other annotations slices.forEach(slice => { @@ -781,8 +739,6 @@ export function sliceOne(settings, widget, onupdate, ondone) { // add lead in anchor when specified in belt mode (but not for synths) if (isBelt) { - let { slope } = widget.belt; - // Generate belt anchor generateBeltAnchor({ slices, @@ -841,7 +797,7 @@ export function sliceOne(settings, widget, onupdate, ondone) { if (render) { forSlices(0.9, 1.0, slice => { let params = slice.params || process; - doRender(slice, params, { + layerRender(slice, params, { dark: controller.dark, devel: controller.devel, renderContext @@ -899,7 +855,7 @@ function bound(v,min,max) { * @param {boolean} opt.devel - Enable development visualization * @param {RenderContext} opt.renderContext - Rendering context */ -function doRender(slice, params, opt = {}) { +function layerRender(slice, params, opt = {}) { const { dark, devel, renderContext: ctx } = opt; const Color = dark ? COLOR_DARK : COLOR; const output = slice.output(); @@ -961,15 +917,15 @@ function doRender(slice, params, opt = {}) { }); if (isThin && devel) { - if (slice.solids && slice.solids.length) output + if (slice.solids?.length) output .setLayer("solids", { face: 0xbbbb00, check: 0xbbbb00 }) .addAreas(slice.solids); - if (slice.bridges && slice.bridges.length) output + if (slice.bridges?.length) output .setLayer("bridges", { face: 0x00cccc, line: 0x00cccc, check: 0x00cccc }) .addAreas(slice.bridges); - if (slice.flats && slice.flats.length) output + if (slice.flats?.length) output .setLayer("flats", { face: 0xaa00aa, line: 0xaa00aa, check: 0xaa00aa }) .addAreas(slice.flats); } @@ -1004,6 +960,55 @@ function doRender(slice, params, opt = {}) { // console.log(slice.index, slice.render.stats); } +/** + * DRIVER SLICE CONTRACT + * + * global slice operation run after all individual widget slice() complete + */ +export function slicePost(settings, onupdate) { + // future home of brim and anchor generation + let widgets = Object.values(self.kiri_worker.cache) + .filter(w => !w.meta.disabled) + .sort((a,b) => { + return a.slices[0].z - b.slices[0].z + }); + // assign grid_id which can be embedded in gcode and + // used by the controller to cancel objects during print + let { bounds } = settings; + for (let widget of widgets) { + let { pos, box } = widget.track; + // calculate top/left coordinate for widget + // relative to bounding box for all widgets + let tl = { + x: Math.round((pos.x - box.w/2 - bounds.min.x) / 10) + 1, + y: Math.round((pos.y - box.h/2 - bounds.min.y) / 10) + 1 + }; + widget.track.grid_id = tl.x * 100 + tl.y; + } + // count extruders used + let ext = []; + for (let w of widgets) { + if (w.anno && w.anno.extruder >= 0) { + let e = w.anno.extruder; + if (ext.indexOf(e) < 0) { + ext.push(e); + } + } + } + // sort widgets by first slice Z + widgets.sort((a,b) => { + return a.slices[0].z - b.slices[0].z; + }); + // give first widget a pass since it should have the anchor + widgets.shift(); + // remove anchor slices from other widgets (only with multi-material) + if (ext.length > 1) { + for (let w of widgets) { + w.slices = w.slices.filter(s => s.index >= 0); + } + } +} + /** * Compute offset shell polygons. For FDM, the first offset is usually half * of the nozzle width. Each subsequent offset is a full nozzle width. User @@ -1018,9 +1023,9 @@ function doRender(slice, params, opt = {}) { * @param {number} fillOffset * @param {Obejct} options */ -export function doShells(slice, count, offset1, offsetN, fillOffset, opt = {}) { +export function layerProcessTops(slice, count, offset1, offsetN, fillOffset, opt = {}) { for (let top of slice.tops) { - doTopShells(slice.z, top, count, offset1, offsetN, fillOffset, opt); + layerProcessTop(slice.z, top, count, offset1, offsetN, fillOffset, opt); } } @@ -1039,9 +1044,7 @@ export function doShells(slice, count, offset1, offsetN, fillOffset, opt = {}) { * @param {number} args.spacing - Fill line spacing in mm * @param {number} args.angle - Fill angle in degrees */ -function doSolidLayerFill(args) { - const { slice, spacing, angle } = args; - +function layerMakeSolid({ slice, spacing, angle }) { if (slice.xray) { return; } @@ -1064,7 +1067,7 @@ function doSolidLayerFill(args) { * Take output from pluggable sparse infill algorithm and clip to * the bounds of the top polygons and their inner solid areas. */ -function doSparseLayerFill(slice, options = {}) { +function layerSparseFill(slice, options = {}) { if (slice.xray || options.type === 'none') { return; } @@ -1239,7 +1242,7 @@ function doSparseLayerFill(slice, options = {}) { * Used to calculate bridges, flats and then solid projections. * 'expand' is used for top offsets in SLA mode */ -export function doDiff(slice, options = {}) { +export function layerDiff(slice, options = {}) { const { sla, fakedown, grow, min } = options; if ((slice.index <= 0 && !fakedown) || slice.xray) { return; @@ -1277,47 +1280,7 @@ export function doDiff(slice, options = {}) { bridges.appendAll(newBridges); flats.appendAll(newFlats); -}; - -/** - * - * - * @param {Polygon[]} polys - */ -function addSolidFills(slice, polys) { - if (slice.solids) { - slice.solids.appendAll(polys); - } else if (polys && polys.length) { - console.log({no_solids_in: slice, for: polys}) - } -}; - -/** - * project bottom flats down - */ -export function projectFlats(slice, count, expand) { - if (!slice.down || !slice.flats) return; - // these flats are marked for finishing print speed - if (slice.flats?.length) { - slice.finishSolids = true; - slice.isFlatsLayer = true; - const flats = expand ? POLY.expand(slice.flats, expand) : slice.flats; - projectSolid({ slice, polys: flats, count, up: false, first: true }); - } -}; - -/** - * project top bridges up - */ -export function projectBridges(slice, count) { - if (!slice.up || !slice.bridges) return; - // these flats are marked for finishing print speed - if (slice.bridges?.length) { - slice.finishSolids = true; - slice.isBridgeLayer = true; - } - projectSolid({ slice, polys: slice.bridges, count, up: true, first: true }); -}; +} /** * Fill projected solid areas and store line data @@ -1330,8 +1293,7 @@ export function projectBridges(slice, count) { * @param {Promise[]} [args.promises] - Array for async operations * @return {boolean} true if filled, false if not */ -function doSolidsFill(args) { - const { slice, spacing, angle, minArea, promises: fillQ } = args; +function layerFillSolids({ slice, spacing, angle, minArea, promises: fillQ }) { let minarea = minArea || 1, tops = slice.tops, solids = slice.solids; @@ -1406,7 +1368,7 @@ function doSolidsFill(args) { for (let top of tops) { top.solids = []; } - doSolidLayerFill({ slice, spacing, angle }); + layerMakeSolid({ slice, spacing, angle }); return; } @@ -1435,12 +1397,12 @@ function doSolidsFill(args) { } } if (tofill.length > 0) { - doFillArea({ promises: fillQ, polys: tofill, angle, spacing, output: newfill }); + areaFill({ promises: fillQ, polys: tofill, angle, spacing, output: newfill }); } if (angfill.length > 0) { top.fill_lines_ang = {spacing:spacing,list:[],poly:[]}; for (let af of angfill) { - doFillArea({ + areaFill({ promises: fillQ, polys: [af], angle: af.fillang.angle + CONSTANTS.ANGLED_FILL_ROTATION, @@ -1464,8 +1426,7 @@ function doSolidsFill(args) { * @param {number} [args.minLen] - Minimum line length * @param {number} [args.maxLen] - Maximum line length */ -function doFillArea(args) { - const { promises: fillQ, polys, angle, spacing, output, minLen, maxLen } = args; +function areaFill({ promises: fillQ, polys, angle, spacing, output, minLen, maxLen }) { if (fillQ) { fillQ.push(self.kiri_worker.minions.fill(polys, angle, spacing, output, minLen, maxLen)); } else { @@ -1512,14 +1473,13 @@ function doFillArea(args) { * Each polygon has a .fill property with fill lines * @modifies {Slice} slice.supportOutline - Stores support outline polygons * - * @see fillSupportPolys - Called internally to generate fill patterns + * @see supportPolyFill - Called internally to generate fill patterns * @see CONSTANTS.SUPPORT_INSET_RATIO - Inset ratio (1/3 of line width) * @see CONSTANTS.SUPPORT_AUTO_ANGLE_WIDE - Auto angle for wide polygons (1090 = 90°) * @see CONSTANTS.SUPPORT_AUTO_ANGLE_TALL - Auto angle for tall polygons (1000 = 0°) * @see CONSTANTS.SUPPORT_CONNECT_DISTANCE_MULT - Distance multiplier for line connection (2x) */ - function doSupportFill(args) { - const { promises, slice, lineWidth, density, minArea, isBelt, angle, outline, gap } = args; + function layerSupportFill({ promises, slice, lineWidth, density, minArea, isBelt, angle, outline, gap }) { let supports = slice.supports, min = minArea || 0.1, sub; @@ -1546,7 +1506,7 @@ function doFillArea(args) { } if (supports) { - fillSupportPolys({ + supportPolyFill({ promises, polys: supports, lineWidth, density, z: slice.z, isBelt, angle, outline }); } @@ -1617,8 +1577,8 @@ function doFillArea(args) { * - Lines are either individual segments (outline=true) or * connected paths (outline=false) for travel reduction * - * @see doSupportFill - Parent function that calls this after clipping/union - * @see doFillArea - Lower-level function that generates the actual scan lines + * @see layerSupportFill - Parent function that calls this after clipping/union + * @see areaFill - Lower-level function that generates the actual scan lines * @see connect_lines - Utility that connects nearby line segments * @see CONSTANTS.SUPPORT_AUTO_ANGLE_WIDE - Auto angle for wide polygons (1090 = 90°) * @see CONSTANTS.SUPPORT_AUTO_ANGLE_TALL - Auto angle for tall polygons (1000 = 0°) @@ -1626,7 +1586,7 @@ function doFillArea(args) { * @see CONSTANTS.SUPPORT_CONNECT_DISTANCE_MULT - Distance multiplier for line connection (2x) */ -function fillSupportPolys({ promises, polys, lineWidth, density, z, isBelt, angle, outline }) { +function supportPolyFill({ promises, polys, lineWidth, density, z, isBelt, angle, outline }) { // calculate fill density let spacing = lineWidth * (1 / density); for (let poly of polys) { @@ -1636,7 +1596,7 @@ function fillSupportPolys({ promises, polys, lineWidth, density, z, isBelt, angl let inset = POLY.offset([poly], -lineWidth * CONSTANTS.SUPPORT_INSET_RATIO, {flat: true, z, wasm: true}); // do the fill if (inset && inset.length > 0) { - doFillArea({ + areaFill({ angle: angle || auto, output: poly.fill = [], polys: inset, @@ -1660,24 +1620,79 @@ function fillSupportPolys({ promises, polys, lineWidth, density, z, isBelt, angl * @param {boolean} args.up - Project upward (true) or downward (false) * @param {boolean} args.first - First projection (hint fill angles) */ -function projectSolid(args) { - const { slice, polys, count, up, first } = args; - +function projectSolid({ slice, polys, count, up, first }) { if (slice && !up && count === 1) { slice.isBridgeLayer = true; } + if (!slice || count <= 0) { return; } + let clones = polys.clone(true); if (first) { - clones.forEach(function(p) { - p.hintFillAngle(); - }); + for (let clone of clones) { + clone.hintFillAngle(); + } } - addSolidFills(slice, clones); + + if (slice.solids) { + slice.solids.appendAll(clones); + } else if (clones && clones.length) { + console.log({no_solids_in: slice, for: clones}) + } + if (count > 0) { - if (up) projectSolid({ slice: slice.up, polys, count: count-1, up: true, first: false }); - else projectSolid({ slice: slice.down, polys, count: count-1, up: false, first: false }); + projectSolid({ + count: count - 1, + first: false, + polys, + slice: up ? slice.up : slice.down, + up + }); + // if (up) { + // projectSolid({ + // count: count-1, + // first: false, + // polys, + // slice: slice.up, + // up: true + // }); + // } else { + // projectSolid({ + // count: count-1, + // first: false, + // polys, + // slice: slice.down, + // up: false + // }); + // } } } + +/** + * project bottom flats down into part + */ +export function projectFlats(slice, count, expand) { + if (!slice.down || !slice.flats) return; + // these flats are marked for finishing print speed + if (slice.flats?.length) { + slice.finishSolids = true; + slice.isFlatsLayer = true; + const flats = expand ? POLY.expand(slice.flats, expand) : slice.flats; + projectSolid({ slice, polys: flats, count, up: false, first: true }); + } +} + +/** + * project top bridges up into part + */ +export function projectBridges(slice, count) { + if (!slice.up || !slice.bridges) return; + // these flats are marked for finishing print speed + if (slice.bridges?.length) { + slice.finishSolids = true; + slice.isBridgeLayer = true; + } + projectSolid({ slice, polys: slice.bridges, count, up: true, first: true }); +} diff --git a/src/kiri/mode/sla/slice.js b/src/kiri/mode/sla/slice.js index 2c2e3d4e..f51caad5 100644 --- a/src/kiri/mode/sla/slice.js +++ b/src/kiri/mode/sla/slice.js @@ -5,7 +5,7 @@ import { slicer } from '../../../geo/slicer.js'; import { newPolygon } from '../../../geo/polygon.js'; import { polygons as POLY } from '../../../geo/polygons.js'; import { newSlice, newTop } from '../../core/slice.js'; -import { doShells, doDiff, projectFlats, projectBridges } from '../fdm/slice.js'; +import { layerProcessTops, layerDiff, projectFlats, projectBridges } from '../fdm/slice.js'; import { PNG } from '../../../ext/pngjs.esm.js'; import { SLA } from './driver.js'; @@ -149,14 +149,14 @@ export function sla_slice(settings, widget, onupdate, ondone) { work_remain = work_total; forSlices(slices, 5, (slice,index) => { if (process.slaShell) { - doShells(slice, 2, 0, process.slaShell); + layerProcessTops(slice, 2, 0, process.slaShell); } else { - doShells(slice, 1, 0); + layerProcessTops(slice, 1, 0); } }, "shells"); forSlices(slices, 10, (slice) => { if (slice.synth) return; - doDiff(slice, 0.000001, { + layerDiff(slice, 0.000001, { sla: true, fakedown: !process.slaOpenBase }); diff --git a/src/kiri/run/minion.js b/src/kiri/run/minion.js index 5186e6ee..c125f38d 100644 --- a/src/kiri/run/minion.js +++ b/src/kiri/run/minion.js @@ -8,7 +8,7 @@ import '../../add/three.js'; import { base } from '../../geo/base.js'; import { codec, encode, encodePointArray } from '../core/codec.js'; -import { doTopShells } from '../mode/fdm/post.js'; +import { layerProcessTop } from '../mode/fdm/post.js'; import { newPoint } from '../../geo/point.js'; import { newWidget } from '../core/widget.js'; import { polygons as POLY } from '../../geo/polygons.js'; @@ -92,7 +92,7 @@ const funcs = self.minion = { topShells(data, seq) { let top = codec.decode(data.top, {full: true}); let {z, count, offset1, offsetN, fillOffset, opt} = data; - doTopShells(z, top, count, offset1, offsetN, fillOffset, opt); + layerProcessTop(z, top, count, offset1, offsetN, fillOffset, opt); let state = { zeros: [] }; reply({ seq, top: codec.encode(top, {full: true}) }, state.zeros); }, From 6bbc67d72722feecc661245ea2510afee10944c0 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Tue, 6 Jan 2026 21:37:49 -0500 Subject: [PATCH 114/154] re-attach support to fdm prepare pipeline --- src/kiri/mode/fdm/prepare.js | 6 +- src/kiri/mode/fdm/slice.js | 220 +++++++++++++---------------------- 2 files changed, 84 insertions(+), 142 deletions(-) diff --git a/src/kiri/mode/fdm/prepare.js b/src/kiri/mode/fdm/prepare.js index f3d3110e..5bdb7f66 100644 --- a/src/kiri/mode/fdm/prepare.js +++ b/src/kiri/mode/fdm/prepare.js @@ -87,7 +87,7 @@ export async function fdm_prepare(widgets, settings, update) { }); // collect support polygons if (slice.supports) - slice.supports.forEach(function(support) { + slice.supports.forEach((support) => { tops.push(support.clone()); }); } @@ -95,7 +95,7 @@ export async function fdm_prepare(widgets, settings, update) { tops = POLY.union(tops,1,true); } // nest and offset tops - POLY.nest(tops).forEach(function(poly) { + POLY.nest(tops).forEach((poly) => { let off = poly.offset(-offset + nozzle / 2); if (off) off.forEach(function(brim) { brim.move(widget.track.pos); @@ -541,7 +541,7 @@ export async function fdm_prepare(widgets, settings, update) { }, routeAround: process.outputAvoidGaps, seedPoint: printPoint.sub(offset), - support: slice.widget.support, + support: slice.supports, zmax, } ); diff --git a/src/kiri/mode/fdm/slice.js b/src/kiri/mode/fdm/slice.js index d79f3066..70e980e0 100644 --- a/src/kiri/mode/fdm/slice.js +++ b/src/kiri/mode/fdm/slice.js @@ -451,22 +451,49 @@ export function sliceOne(settings, widget, onupdate, ondone) { let indices = stack.map(s => s.z); let zAngNorm = Math.sin(process.sliceSupportAngle * Math.PI / 180); - await widget.computeShadowStack(indices, progress => {}, zAngNorm); + await widget.computeShadowStack(indices, progress => { + trackupdate(progress, 0.05, 0.15, "support gen"); + }, zAngNorm); - if (process.sliceSupportTree) { - stack.sort((a,b) => a.z - b.z); // deltas only - } else { - stack.sort((a,b) => b.z - a.z); // accumulate deltas - } + stack.sort((a,b) => a.z - b.z); // deltas only // 1. accumulate / union shadow coverage top down // 2. trim to area outside slice.clips for (let slice of stack) { - let shadow = await widget.shadowAt(slice.z, true); + slice.shadow = await widget.shadowAt(slice.z, true); + } + + let minArea = lineWidth; + let shadowSum; + for (let slice of stack.reverse()) { + let { shadow } = slice; if (process.sliceSupportExtra) { shadow = POLY.offset(shadow, process.sliceSupportExtra); } + // shadows accumulate down + if (shadowSum) { + shadow = POLY.union([...shadow, ...shadowSum], minArea, true); + } + // trim to slice.clips + if (true) { + let rem = []; + let clips = [ + slice.up?.clips, + slice.clips, + slice.down?.clips + ].filter(v => v).flat(); + clips = POLY.union(clips, minArea, true); + POLY.subtract(shadow, clips, rem, null, slice.z, minArea, { wasm: false }); + shadow = rem; + } + // pump shadow to clean to clean it up + if (true) { + let bump = lineWidth * 2; + shadow = POLY.offset(shadow, bump, { z: slice.z }); + shadow = POLY.offset(shadow, -bump, { z: slice.z }); + } + shadowSum = shadow; slice.supports = shadow; slice.output().setLayer("shadow", 0xff0000).addPolys(shadow); } @@ -606,15 +633,13 @@ export function sliceOne(settings, widget, onupdate, ondone) { layerSupportFill({ angle: process.sliceSupportFill, density, - gap: process.sliceSupportGap, isBelt, lineWidth, - minArea: 0, outline: process.sliceSupportOutline !== false, promises, slice, }); - }, "support"); + }, "support fill"); if (promises) { await tracker(promises, (i, t) => { trackupdate(i / t, 0.88, 0.9); @@ -689,9 +714,6 @@ export function sliceOne(settings, widget, onupdate, ondone) { }); } - // new auto support demonstrator using cast shadow - await processAutoSupport(); - // alert non-manifold parts if (healed) { onupdate(null, null, "part may not be manifold"); @@ -728,11 +750,14 @@ export function sliceOne(settings, widget, onupdate, ondone) { // reset for solids, support projections // and other annotations - slices.forEach(slice => { + for (let slice of slices) { slice.widget = widget; slice.extruder = extruder; slice.solids = []; - }); + } + + // new auto support demonstrator using cast shadow + await processAutoSupport(); // process solid layers (top/bottom) processSolidLayers(); @@ -1434,87 +1459,47 @@ function areaFill({ promises: fillQ, polys, angle, spacing, output, minLen, maxL } } - /** - * Generate support structure fill patterns for a slice - * - * This function processes support polygons by: - * 1. Unioning all support polygons to eliminate overlaps - * 2. Clipping supports to avoid collision with the part (using clip offsets) - * 3. Applying optional gap spacing from upper/lower layers - * 4. Generating hatching fill patterns based on density - * 5. Optionally connecting fill lines to reduce travel moves - * - * Process flow: - * - Union: Merge overlapping support regions into unified polygons - * - Clip: Subtract part geometry (slice.clips) to prevent support/part collision - * - Gap: If gap specified, also subtract clips from adjacent layers for clearance - * - Fill: Generate hatching pattern with auto or specified angle - * - Connect: Optionally link nearby fill line endpoints to reduce travels - * - * @param {SupportFillOptions} args - Configuration object - * @param {Promise[]} [args.promises] - Array for async fill operations (concurrent mode) - * @param {Slice} args.slice - Current slice to process - * @param {number} args.lineWidth - Extrusion width in mm (typically nozzle diameter) - * @param {number} args.density - Fill density from 0.0 (0%) to 1.0 (100%) - * Typical support density is 0.10-0.25 (10-25%) - * @param {number} args.minArea - Minimum polygon area in mm² to keep (default: 0.1) - * Smaller polygons are filtered out as noise - * @param {boolean} args.isBelt - True for belt printer mode (affects auto-angle calculation) - * @param {number} [args.angle] - Fill angle in degrees. Special values: - * - undefined/null: Auto-calculate based on polygon shape - * - >= 1000: Auto-calculate (1000=0° for tall, 1090=90° for wide) - * - 0-360: Use specified angle - * @param {boolean} args.outline - Include support perimeter outline in output - * If false, only interior fill lines are generated - * @param {number} [args.gap] - Gap distance in mm between support and part surfaces - * When specified, subtracts adjacent layer clips for clearance - * - * @modifies {Slice} slice.supports - Updated with filled support polygons (array of Polygon) - * Each polygon has a .fill property with fill lines - * @modifies {Slice} slice.supportOutline - Stores support outline polygons - * - * @see supportPolyFill - Called internally to generate fill patterns - * @see CONSTANTS.SUPPORT_INSET_RATIO - Inset ratio (1/3 of line width) - * @see CONSTANTS.SUPPORT_AUTO_ANGLE_WIDE - Auto angle for wide polygons (1090 = 90°) - * @see CONSTANTS.SUPPORT_AUTO_ANGLE_TALL - Auto angle for tall polygons (1000 = 0°) - * @see CONSTANTS.SUPPORT_CONNECT_DISTANCE_MULT - Distance multiplier for line connection (2x) - */ - function layerSupportFill({ promises, slice, lineWidth, density, minArea, isBelt, angle, outline, gap }) { - let supports = slice.supports, - min = minArea || 0.1, - sub; - - if (!supports) return; - - // union supports - supports = POLY.setZ(POLY.union(supports, undefined, true, { wasm: false }), slice.z); - - // clip supports to slice clip offset (or shell if none) - POLY.subtract(supports, slice.clips, sub = [], null, slice.z, min, { wasm: false }); - supports = sub; - - // trim to upper offsets, if they exist - if (gap && slice?.up?.clips) { - POLY.subtract(supports, slice.up.clips, sub = [], null, slice.z, min, { wasm: false }); - supports = sub; - } - - // trim to lower offsets, if they exist - if (gap && slice?.down?.clips) { - POLY.subtract(supports, slice.down.clips, sub = [], null, slice.z, min, { wasm: false }); - supports = sub; - } - - if (supports) { +/** + * Generate support structure fill patterns for a slice + * + * @param {SupportFillOptions} args - Configuration object + * @param {Promise[]} [args.promises] - Array for async fill operations (concurrent mode) + * @param {Slice} args.slice - Current slice to process + * @param {number} args.lineWidth - Extrusion width in mm (typically nozzle diameter) + * @param {number} args.density - Fill density from 0.0 (0%) to 1.0 (100%) + * Typical support density is 0.10-0.25 (10-25%) + * @param {boolean} args.isBelt - True for belt printer mode (affects auto-angle calculation) + * @param {number} [args.angle] - Fill angle in degrees. Special values: + * - undefined/null: Auto-calculate based on polygon shape + * - >= 1000: Auto-calculate (1000=0° for tall, 1090=90° for wide) + * - 0-360: Use specified angle + * @param {boolean} args.outline - Include support perimeter outline in output + * If false, only interior fill lines are generated + * + * @modifies {Slice} slice.supports - Updated with filled support polygons (array of Polygon) + * Each polygon has a .fill property with fill lines + * + * @see supportPolyFill - Called internally to generate fill patterns + * @see CONSTANTS.SUPPORT_INSET_RATIO - Inset ratio (1/3 of line width) + * @see CONSTANTS.SUPPORT_AUTO_ANGLE_WIDE - Auto angle for wide polygons (1090 = 90°) + * @see CONSTANTS.SUPPORT_AUTO_ANGLE_TALL - Auto angle for tall polygons (1000 = 0°) + * @see CONSTANTS.SUPPORT_CONNECT_DISTANCE_MULT - Distance multiplier for line connection (2x) + */ +function layerSupportFill({ promises, slice, lineWidth, density, isBelt, angle, outline, gap }) { + let polys = slice.supports; + if (polys) { supportPolyFill({ - promises, polys: supports, lineWidth, density, z: slice.z, isBelt, angle, outline + angle, + density, + isBelt, + lineWidth, + outline, + polys, + promises, + z: slice.z }); } - - // re-assign new supports back to slice - slice.supportOutline = supports; - slice.supports = supports; -}; +} /** * Generate fill patterns for individual support polygons @@ -1525,33 +1510,6 @@ function areaFill({ promises: fillQ, polys, angle, spacing, output, minLen, maxL * - Intelligent inset to prevent perimeter over-extrusion * - Optional line connection to reduce travel moves when no outline is needed * - * ## Algorithm Details - * - * 1. **Spacing Calculation**: Converts density percentage to actual spacing distance - * - Formula: spacing = lineWidth * (1 / density) - * - Example: 0.4mm line × (1 / 0.15) = 2.67mm spacing for 15% density - * - * 2. **Auto-Angle Selection**: Chooses fill direction based on polygon shape - * - Wide polygons (width/height > 1): Use 1090 (90° horizontal lines) - * - Tall polygons (width/height ≤ 1): Use 1000 (0° vertical lines) - * - Belt mode: Always use 1090 (90°) regardless of aspect ratio - * - Rationale: Aligns fill lines with longest dimension for structural strength - * - * 3. **Inset Operation**: Shrinks polygon before filling - * - Inset distance: lineWidth × (1/3) = 0.33× line width - * - Purpose: Prevents over-extrusion where fill lines meet perimeter - * - Creates ~0.13mm gap for typical 0.4mm nozzle - * - * 4. **Fill Generation**: Creates parallel hatching lines at calculated angle - * - Uses doFillArea() to generate scan lines through inset polygon - * - Stores result in poly.fill array - * - * 5. **Line Connection** (optional): Reduces travel moves - * - Only when outline=false (no support perimeter needed) - * - Connects nearby line endpoints within 2× spacing distance - * - Converts many short lines into fewer longer continuous paths - * - Reduces print time and stringing - * * @param {Object} args - Configuration object * @param {Promise[]} [args.promises] - Array for async fill operations (concurrent mode) * Pass same array through all calls for parallelism @@ -1585,13 +1543,14 @@ function areaFill({ promises: fillQ, polys, angle, spacing, output, minLen, maxL * @see CONSTANTS.SUPPORT_INSET_RATIO - Inset ratio for fill area (1/3) * @see CONSTANTS.SUPPORT_CONNECT_DISTANCE_MULT - Distance multiplier for line connection (2x) */ - function supportPolyFill({ promises, polys, lineWidth, density, z, isBelt, angle, outline }) { // calculate fill density let spacing = lineWidth * (1 / density); for (let poly of polys) { // calculate angle based on width/height ratio - let auto = isBelt || (poly.bounds.width() / poly.bounds.height() > 1) ? CONSTANTS.SUPPORT_AUTO_ANGLE_WIDE : CONSTANTS.SUPPORT_AUTO_ANGLE_TALL; + let auto = isBelt || (poly.bounds.width() / poly.bounds.height() > 1) ? + CONSTANTS.SUPPORT_AUTO_ANGLE_WIDE : + CONSTANTS.SUPPORT_AUTO_ANGLE_TALL; // inset support poly for fill lines let inset = POLY.offset([poly], -lineWidth * CONSTANTS.SUPPORT_INSET_RATIO, {flat: true, z, wasm: true}); // do the fill @@ -1650,23 +1609,6 @@ function projectSolid({ slice, polys, count, up, first }) { slice: up ? slice.up : slice.down, up }); - // if (up) { - // projectSolid({ - // count: count-1, - // first: false, - // polys, - // slice: slice.up, - // up: true - // }); - // } else { - // projectSolid({ - // count: count-1, - // first: false, - // polys, - // slice: slice.down, - // up: false - // }); - // } } } From 7d888a1af257ab7b5d4d0d0e5598098e9c15c5e8 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Tue, 6 Jan 2026 22:17:25 -0500 Subject: [PATCH 115/154] constrain support paint on to downward faces --- src/kiri/mode/fdm/init-ui.js | 18 +++++++++++++----- src/kiri/mode/fdm/slice.js | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/kiri/mode/fdm/init-ui.js b/src/kiri/mode/fdm/init-ui.js index 60accf7b..db1b7761 100644 --- a/src/kiri/mode/fdm/init-ui.js +++ b/src/kiri/mode/fdm/init-ui.js @@ -1,5 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ +import { newPoint } from '../../../geo/point.js'; import { $ } from '../../../moto/webui.js'; import { api } from '../../app/api.js'; import { conf } from '../../app/conf/defaults.js'; @@ -14,7 +15,8 @@ let lastView, addingSupports = false, isFdmMode = false, alert = [], - func = {}; + func = {}, + down; export function init() { const { ui } = api; @@ -181,17 +183,16 @@ export function init() { return func.sdone(); } alert = api.show.alert("[esc] key cancels support editing", 1000); - let down; api.feature.hover = addingSupports = true; api.feature.on_mouse_down = (int) => { if (int) { let { point } = int; - down = { point, widget: int.object.widget }; + down = { point, widget: int.object.widget, z: int.face.normal.z }; } return api.widgets.meshes(); }; api.feature.on_mouse_drag = ({ int }) => { - if (int?.length) { + if (int?.length && int[0].face.normal.z < 0) { func.sadd_point(int[0].point, down.widget); } return [ down.widget.mesh ]; @@ -217,6 +218,13 @@ export function init() { }; // return console.log({ on, widget, point }); let paint = widget.anno.paint; + let pt = newPoint(x, -z, y); + for (let r of paint) { + let { point } = r; + if (newPoint(point.x, point.y, point.z).distTo2D(pt) < 1) { + return; + } + } paint.push(rec); updatePaintOverlay(widget.mesh.material[0], paint); api.space.update(); @@ -307,7 +315,7 @@ export function init() { return; } let { object } = on; - if (object) { + if (object && down.z < 0) { let { point } = object; let { widget } = object.object; func.sadd_point(point, widget); diff --git a/src/kiri/mode/fdm/slice.js b/src/kiri/mode/fdm/slice.js index 70e980e0..f70de66f 100644 --- a/src/kiri/mode/fdm/slice.js +++ b/src/kiri/mode/fdm/slice.js @@ -443,7 +443,7 @@ export function sliceOne(settings, widget, onupdate, ondone) { * Process shadow-based support generation */ async function processAutoSupport() { - if (process.sliceSupportType !== 'automatic') { + if (process.sliceSupportType === 'disabled') { return; } From dcf432c5cd7f0330a63e10e68516d000260d06e9 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Tue, 6 Jan 2026 22:22:50 -0500 Subject: [PATCH 116/154] disable detect and tree until implemented --- src/kiri/mode/fdm/init-menu.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/kiri/mode/fdm/init-menu.js b/src/kiri/mode/fdm/init-menu.js index f8622836..90189778 100644 --- a/src/kiri/mode/fdm/init-menu.js +++ b/src/kiri/mode/fdm/init-menu.js @@ -128,13 +128,13 @@ export function menu() { // sliceSupportGrow: newInput(LANG.sp_grow_s, {title:LANG.sp_grow_l, convert:toFloat, bound:bound(0.0,10.0)}), sliceSupportSpan: newInput(LANG.sp_span_s, {title:LANG.sp_span_l, convert:toFloat, bound:bound(0.0,200.0), show:() => ui.sliceSupportEnable.checked }), separator: newBlank({ class:"set-sep", driven }), - sliceSupportTree: newBoolean(LANG.sp_tree_s, onBooleanClick, {title:LANG.sp_tree_s, trigger, show:isNotBelt}), + // sliceSupportTree: newBoolean(LANG.sp_tree_s, onBooleanClick, {title:LANG.sp_tree_s, trigger, show:isNotBelt}), // sliceSupportEnable: newBoolean(LANG.sp_auto_s, onBooleanClick, {title:LANG.sp_auto_l, show:isNotBelt}), sliceSupportOutline: newBoolean(LANG.sp_outl_s, onBooleanClick, {title:LANG.sp_outl_l, show: () => !isTree() }), separator: newBlank({ class:"set-sep", driven, show:manualSupport }), - sliceSupportGen: newRow([ - ui.ssaGen = newButton(LANG.sp_detect, onButtonClick, {class: "f-col grow a-center"}) - ], { modes: FDM, class: "ext-buttons f-row grow", show:manualSupport }), + // sliceSupportGen: newRow([ + // ui.ssaGen = newButton(LANG.sp_detect, onButtonClick, {class: "f-col grow a-center"}) + // ], { modes: FDM, class: "ext-buttons f-row grow", show:manualSupport }), // separator: newBlank({ class:"set-sep", driven }), sliceSupportManual: newRow([ (ui.ssmAdd = newButton(undefined, onButtonClick, {icon:''})), From da6561093fd481802a5eb8836f358efbf3d0e782 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Wed, 7 Jan 2026 00:41:47 -0500 Subject: [PATCH 117/154] add z down shader filter --- src/kiri/app/paint.js | 50 ++++++++++++++++++++++++---------- src/kiri/mode/fdm/init-menu.js | 2 +- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/src/kiri/app/paint.js b/src/kiri/app/paint.js index 367fffe2..48a75a31 100644 --- a/src/kiri/app/paint.js +++ b/src/kiri/app/paint.js @@ -118,6 +118,8 @@ function addPaintOverlaySimple(material, paintPoints, paintColor = new THREE.Col ` ); + // Don't pass normals from vertex shader at all - we'll use fragment shader's normal + shader.vertexShader = ` varying vec3 vWorldPosition; ` + shader.vertexShader; @@ -135,10 +137,17 @@ function addPaintOverlaySimple(material, paintPoints, paintColor = new THREE.Col ` #include - // Check if this fragment is within any paint sphere - bool painted = false; + // Compute world-space normal from position derivatives + vec3 posDx = dFdx(vWorldPosition); + vec3 posDy = dFdy(vWorldPosition); + vec3 worldNormal = normalize(cross(posDx, posDy)); - for (int i = 0; i < 256; i++) { + // HARDCODED TEST: only paint if normal.z < 0 (downward facing) + if (worldNormal.z < 0.0) { + // Check if this fragment is within any paint sphere + bool painted = false; + + for (int i = 0; i < 256; i++) { if (i >= paintCount) break; vec3 center = paintPoints[i].xyz; @@ -151,10 +160,11 @@ function addPaintOverlaySimple(material, paintPoints, paintColor = new THREE.Col } } - if (painted) { - // Blend paint color with existing fragment color (increased to 0.7 for visibility) - gl_FragColor.rgb = mix(gl_FragColor.rgb, paintColor, 0.7); - } + if (painted) { + // Blend paint color with existing fragment color (increased to 0.7 for visibility) + gl_FragColor.rgb = mix(gl_FragColor.rgb, paintColor, 0.7); + } + } // End normal check // DEBUG: Uncomment to tint entire mesh when shader is active (proves shader is running) // if (paintCount > 0) { @@ -245,6 +255,8 @@ function addPaintOverlayTexture(material, paintPoints, paintColor = new THREE.Co ` ); + // Don't pass normals from vertex shader at all - we'll use fragment shader's normal + shader.vertexShader = ` varying vec3 vWorldPosition; ` + shader.vertexShader; @@ -263,11 +275,18 @@ function addPaintOverlayTexture(material, paintPoints, paintColor = new THREE.Co ` #include - bool painted = false; + // Compute world-space normal from position derivatives + vec3 posDx = dFdx(vWorldPosition); + vec3 posDy = dFdy(vWorldPosition); + vec3 worldNormal = normalize(cross(posDx, posDy)); - // Iterate through paint points stored in texture - // Loop limit must be constant, but we break early when i >= paintCount - for (int i = 0; i < 10000; i++) { + // HARDCODED TEST: only paint if normal.z < 0 (downward facing) + if (worldNormal.z < 0.0) { + bool painted = false; + + // Iterate through paint points stored in texture + // Loop limit must be constant, but we break early when i >= paintCount + for (int i = 0; i < 10000; i++) { if (i >= paintCount) break; // Calculate texture coordinates for this point index @@ -289,10 +308,11 @@ function addPaintOverlayTexture(material, paintPoints, paintColor = new THREE.Co } } - if (painted) { - // Blend paint color with existing fragment color (increased to 0.7 for visibility) - gl_FragColor.rgb = mix(gl_FragColor.rgb, paintColor, 0.7); - } + if (painted) { + // Blend paint color with existing fragment color (increased to 0.7 for visibility) + gl_FragColor.rgb = mix(gl_FragColor.rgb, paintColor, 0.7); + } + } // End normal check // DEBUG: Uncomment to tint entire mesh when shader is active (proves shader is running) // if (paintCount > 0) { diff --git a/src/kiri/mode/fdm/init-menu.js b/src/kiri/mode/fdm/init-menu.js index 90189778..071400d1 100644 --- a/src/kiri/mode/fdm/init-menu.js +++ b/src/kiri/mode/fdm/init-menu.js @@ -130,7 +130,7 @@ export function menu() { separator: newBlank({ class:"set-sep", driven }), // sliceSupportTree: newBoolean(LANG.sp_tree_s, onBooleanClick, {title:LANG.sp_tree_s, trigger, show:isNotBelt}), // sliceSupportEnable: newBoolean(LANG.sp_auto_s, onBooleanClick, {title:LANG.sp_auto_l, show:isNotBelt}), - sliceSupportOutline: newBoolean(LANG.sp_outl_s, onBooleanClick, {title:LANG.sp_outl_l, show: () => !isTree() }), + sliceSupportOutline: newBoolean(LANG.sp_outl_s, onBooleanClick, {title:LANG.sp_outl_l, xshow: () => !isTree() }), separator: newBlank({ class:"set-sep", driven, show:manualSupport }), // sliceSupportGen: newRow([ // ui.ssaGen = newButton(LANG.sp_detect, onButtonClick, {class: "f-col grow a-center"}) From 4203146baeac82595c50fd890bafaabc280c8358 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Wed, 7 Jan 2026 00:51:44 -0500 Subject: [PATCH 118/154] document surface filtering --- src/kiri/app/paint.js | 50 ++++++++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/src/kiri/app/paint.js b/src/kiri/app/paint.js index 48a75a31..ab15256a 100644 --- a/src/kiri/app/paint.js +++ b/src/kiri/app/paint.js @@ -11,6 +11,12 @@ * The shader checks if each fragment falls within any paint sphere and * blends the paint color accordingly. * + * SURFACE FILTERING: + * By default, painting is constrained to downward-facing surfaces only (z-normal < 0). + * This prevents paint "spill" onto adjacent upward-facing surfaces when painting + * support regions. For multi-material coloring or other uses, set + * options.onlyDownwardFacing = false to paint all surfaces. + * * PAINT POINT DATA STRUCTURE: * --------------------------- * Paint points must be an array of objects with this structure: @@ -29,12 +35,15 @@ * * USAGE: * ------ - * // Initial setup - enable paint overlay: + * // Initial setup - enable paint overlay (for support painting, downward-facing only): * widget._origMaterial = widget.mesh.material; * widget.mesh.material = widget.mesh.material.clone(); * addPaintOverlayAuto(widget.mesh.material, paintPoints, new THREE.Color(0x4488ff)); * widget.mesh.material.needsUpdate = true; * + * // For multi-material coloring (paint all surfaces): + * addPaintOverlayAuto(widget.mesh.material, paintPoints, new THREE.Color(0xff0000), { onlyDownwardFacing: false }); + * * // Real-time updates while painting (see updatePaintOverlay function below) * * // Cleanup - restore original material: @@ -68,14 +77,19 @@ import { THREE } from '../../ext/three.js'; * @param {THREE.Material} material - Material to modify (will be mutated via onBeforeCompile) * @param {Array<{point: {x,y,z}, radius: number}>} paintPoints - Array of paint spheres * @param {THREE.Color} [paintColor] - Color for painted regions (default: light blue) + * @param {Object} [options] - Optional configuration + * @param {boolean} [options.onlyDownwardFacing=true] - Only paint on downward-facing surfaces (z-normal < 0) * @returns {THREE.Material} The modified material */ -function addPaintOverlaySimple(material, paintPoints, paintColor = new THREE.Color(0x4488ff)) { +function addPaintOverlaySimple(material, paintPoints, paintColor = new THREE.Color(0x4488ff), options = {}) { // Initialize userData if it doesn't exist if (!material.userData) { material.userData = {}; } + // Default: only paint downward-facing surfaces (for support painting) + const onlyDownwardFacing = options.onlyDownwardFacing !== undefined ? options.onlyDownwardFacing : true; + // Store initial paint points reference material.userData.paintPoints = paintPoints; material.userData.paintColor = paintColor; @@ -100,13 +114,11 @@ function addPaintOverlaySimple(material, paintPoints, paintColor = new THREE.Col points[i] = i < validPoints.length ? validPoints[i] : new THREE.Vector4(0, 0, 0, 0); } - console.log('[Paint Shader] onBeforeCompile called with', validPoints.length, 'valid points'); - console.log('[Paint Shader] First point:', currentPoints[0]); - // Add uniforms for paint data shader.uniforms.paintPoints = { value: points }; shader.uniforms.paintCount = { value: validPoints.length }; shader.uniforms.paintColor = { value: paintColor }; + shader.uniforms.onlyDownwardFacing = { value: onlyDownwardFacing }; // Pass model position from vertex shader to fragment shader // Paint points are in widget/model space, so we compare against transformed (model space) @@ -130,6 +142,7 @@ function addPaintOverlaySimple(material, paintPoints, paintColor = new THREE.Col uniform vec4 paintPoints[256]; // (x, y, z, radius) uniform int paintCount; uniform vec3 paintColor; + uniform bool onlyDownwardFacing; ` + shader.fragmentShader; shader.fragmentShader = shader.fragmentShader.replace( @@ -142,8 +155,9 @@ function addPaintOverlaySimple(material, paintPoints, paintColor = new THREE.Col vec3 posDy = dFdy(vWorldPosition); vec3 worldNormal = normalize(cross(posDx, posDy)); - // HARDCODED TEST: only paint if normal.z < 0 (downward facing) - if (worldNormal.z < 0.0) { + // Only paint downward-facing surfaces if option is enabled + bool shouldPaint = !onlyDownwardFacing || worldNormal.z < 0.0; + if (shouldPaint) { // Check if this fragment is within any paint sphere bool painted = false; @@ -190,14 +204,19 @@ function addPaintOverlaySimple(material, paintPoints, paintColor = new THREE.Col * @param {THREE.Material} material - Material to modify (will be mutated via onBeforeCompile) * @param {Array<{point: {x,y,z}, radius: number}>} paintPoints - Array of paint spheres * @param {THREE.Color} [paintColor] - Color for painted regions (default: light blue) + * @param {Object} [options] - Optional configuration + * @param {boolean} [options.onlyDownwardFacing=true] - Only paint on downward-facing surfaces (z-normal < 0) * @returns {THREE.Material} The modified material */ -function addPaintOverlayTexture(material, paintPoints, paintColor = new THREE.Color(0x4488ff)) { +function addPaintOverlayTexture(material, paintPoints, paintColor = new THREE.Color(0x4488ff), options = {}) { // Initialize userData if it doesn't exist if (!material.userData) { material.userData = {}; } + // Default: only paint downward-facing surfaces (for support painting) + const onlyDownwardFacing = options.onlyDownwardFacing !== undefined ? options.onlyDownwardFacing : true; + // Store initial paint points reference material.userData.paintPoints = paintPoints; material.userData.paintColor = paintColor; @@ -244,6 +263,7 @@ function addPaintOverlayTexture(material, paintPoints, paintColor = new THREE.Co shader.uniforms.paintCount = { value: pointCount }; shader.uniforms.paintTexSize = { value: new THREE.Vector2(texWidth, texHeight) }; shader.uniforms.paintColor = { value: paintColor }; + shader.uniforms.onlyDownwardFacing = { value: onlyDownwardFacing }; // Pass model position from vertex shader to fragment shader // Paint points are in widget/model space, so we compare against transformed (model space) @@ -268,6 +288,7 @@ function addPaintOverlayTexture(material, paintPoints, paintColor = new THREE.Co uniform int paintCount; uniform vec2 paintTexSize; uniform vec3 paintColor; + uniform bool onlyDownwardFacing; ` + shader.fragmentShader; shader.fragmentShader = shader.fragmentShader.replace( @@ -280,8 +301,9 @@ function addPaintOverlayTexture(material, paintPoints, paintColor = new THREE.Co vec3 posDy = dFdy(vWorldPosition); vec3 worldNormal = normalize(cross(posDx, posDy)); - // HARDCODED TEST: only paint if normal.z < 0 (downward facing) - if (worldNormal.z < 0.0) { + // Only paint downward-facing surfaces if option is enabled + bool shouldPaint = !onlyDownwardFacing || worldNormal.z < 0.0; + if (shouldPaint) { bool painted = false; // Iterate through paint points stored in texture @@ -332,13 +354,15 @@ function addPaintOverlayTexture(material, paintPoints, paintColor = new THREE.Co * @param {THREE.Material} material - Material to modify * @param {Array<{point: {x,y,z}, radius: number}>} paintPoints - Array of paint spheres * @param {THREE.Color} [paintColor] - Color for painted regions + * @param {Object} [options] - Optional configuration + * @param {boolean} [options.onlyDownwardFacing=true] - Only paint on downward-facing surfaces (z-normal < 0) * @returns {THREE.Material} The modified material */ -function addPaintOverlayAuto(material, paintPoints, paintColor = new THREE.Color(0x4488ff)) { +function addPaintOverlayAuto(material, paintPoints, paintColor = new THREE.Color(0x4488ff), options = {}) { if (paintPoints.length <= 256) { - return addPaintOverlaySimple(material, paintPoints, paintColor); + return addPaintOverlaySimple(material, paintPoints, paintColor, options); } else { - return addPaintOverlayTexture(material, paintPoints, paintColor); + return addPaintOverlayTexture(material, paintPoints, paintColor, options); } } From ce8abaa3e0ca5b576275df229b371737acb176c3 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Wed, 7 Jan 2026 11:50:55 -0500 Subject: [PATCH 119/154] complete and re-enable manual supports based on painting --- src/kiri/mode/fdm/init-ui.js | 3 ++- src/kiri/mode/fdm/slice.js | 40 +++++++++++++++++++++++++++++++----- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/kiri/mode/fdm/init-ui.js b/src/kiri/mode/fdm/init-ui.js index db1b7761..957ab9c8 100644 --- a/src/kiri/mode/fdm/init-ui.js +++ b/src/kiri/mode/fdm/init-ui.js @@ -228,7 +228,7 @@ export function init() { paint.push(rec); updatePaintOverlay(widget.mesh.material[0], paint); api.space.update(); - // api.conf.save(); + api.conf.save(); }; @@ -331,5 +331,6 @@ export function init() { } function clearWidgetSupports(widget) { + delete widget.anno.support; widget.anno.paint = []; } diff --git a/src/kiri/mode/fdm/slice.js b/src/kiri/mode/fdm/slice.js index f70de66f..b37c85d1 100644 --- a/src/kiri/mode/fdm/slice.js +++ b/src/kiri/mode/fdm/slice.js @@ -450,24 +450,52 @@ export function sliceOne(settings, widget, onupdate, ondone) { let stack = slices.slice(); let indices = stack.map(s => s.z); let zAngNorm = Math.sin(process.sliceSupportAngle * Math.PI / 180); + let manual = process.sliceSupportType === 'manual'; await widget.computeShadowStack(indices, progress => { - trackupdate(progress, 0.05, 0.15, "support gen"); + trackupdate(progress, 0.05, 0.10, "shadow"); }, zAngNorm); - stack.sort((a,b) => a.z - b.z); // deltas only + // sort bottom up so shadows do not accumulate + // since that is done later and clipped to slice.clips + stack.sort((a,b) => a.z - b.z); - // 1. accumulate / union shadow coverage top down - // 2. trim to area outside slice.clips + // process manual supports if they exist + // convert paint points to circles on matching slices + let { paint } = widget.anno; + if (manual && paint?.length) { + let hpi = Math.PI/2; + for (let slice of stack) { + let polys = []; + for (let rec of paint) { + let { point, radius } = rec; + let dz = Math.abs(slice.z - point.z); + if (dz < radius) { + // scale radius by distance from point.z + radius = (Math.acos(dz/radius) / hpi) * radius; + polys.push(newPolygon().centerCircle(point, radius, 10)); + } + } + slice.shadow = POLY.union(polys, 0, true); + } + } + // create automatic shadows supports when on manual paint + if (!manual) for (let slice of stack) { slice.shadow = await widget.shadowAt(slice.z, true); } + // 1. accumulate / union shadow coverage top down + // 2. trim to area outside slice.clips let minArea = lineWidth; let shadowSum; + let length = stack.length; + let count = 0; + + // perform accumulation top down for (let slice of stack.reverse()) { - let { shadow } = slice; + let shadow = slice.shadow ?? []; if (process.sliceSupportExtra) { shadow = POLY.offset(shadow, process.sliceSupportExtra); } @@ -496,7 +524,9 @@ export function sliceOne(settings, widget, onupdate, ondone) { shadowSum = shadow; slice.supports = shadow; slice.output().setLayer("shadow", 0xff0000).addPolys(shadow); + trackupdate((++count/length), 0.10, 0.15, "support"); } + } /** From 12448e6523511a6272debf3c1ee120008e15468a Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Wed, 7 Jan 2026 12:15:37 -0500 Subject: [PATCH 120/154] add support z rotation, remove unused lang keys and process settings --- src/kiri/app/conf/defaults.js | 6 +----- src/kiri/mode/fdm/init-menu.js | 9 --------- src/kiri/mode/fdm/init-ui.js | 23 +++++++++++++++++++++-- src/kiri/mode/fdm/slice.js | 21 +++++++++++---------- web/kiri/lang/en.js | 10 ---------- 5 files changed, 33 insertions(+), 36 deletions(-) diff --git a/src/kiri/app/conf/defaults.js b/src/kiri/app/conf/defaults.js index dbdf5b7c..81513b08 100644 --- a/src/kiri/app/conf/defaults.js +++ b/src/kiri/app/conf/defaults.js @@ -338,18 +338,14 @@ export const conf = { sliceSolidify: false, 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, - sliceSupportType: "automatic", + sliceSupportType: "disabled", sliceSupportTree: false, sliceTopLayers: 3, sliceZInterleave: false, diff --git a/src/kiri/mode/fdm/init-menu.js b/src/kiri/mode/fdm/init-menu.js index 071400d1..ffaf931f 100644 --- a/src/kiri/mode/fdm/init-menu.js +++ b/src/kiri/mode/fdm/init-menu.js @@ -120,22 +120,13 @@ export function menu() { separator: newBlank({ class:"set-sep", driven }), sliceSupportAngle: newInput(LANG.sp_angl_s, {title:LANG.sp_angl_l, convert:toFloat, bound:bound(0.0,90.0)}), sliceSupportDensity: newInput(LANG.sp_dens_s, {title:LANG.sp_dens_l, convert:toFloat, bound:bound(0.0,1.0)}), - // sliceSupportSize: newInput(LANG.sp_size_s, {title:LANG.sp_size_l, convert:toFloat, bound:bound(1.0,200.0)}), sliceSupportGap: newInput(LANG.sp_gaps_s, {title:LANG.sp_gaps_l, convert:toInt, bound:bound(0,5)}), sliceSupportOffset: newInput(LANG.sp_offs_s, {title:LANG.sp_offs_l, convert:toFloat, bound:bound(0.0,200.0)}), - // sliceSupportArea: newInput(LANG.sp_area_s, {title:LANG.sp_area_l, convert:toFloat, bound:bound(0.0,200.0)}), sliceSupportExtra: newInput(LANG.sp_xpnd_s, {title:LANG.sp_xpnd_l, convert:toFloat, bound:bound(0.0,10.0)}), - // sliceSupportGrow: newInput(LANG.sp_grow_s, {title:LANG.sp_grow_l, convert:toFloat, bound:bound(0.0,10.0)}), sliceSupportSpan: newInput(LANG.sp_span_s, {title:LANG.sp_span_l, convert:toFloat, bound:bound(0.0,200.0), show:() => ui.sliceSupportEnable.checked }), separator: newBlank({ class:"set-sep", driven }), - // sliceSupportTree: newBoolean(LANG.sp_tree_s, onBooleanClick, {title:LANG.sp_tree_s, trigger, show:isNotBelt}), - // sliceSupportEnable: newBoolean(LANG.sp_auto_s, onBooleanClick, {title:LANG.sp_auto_l, show:isNotBelt}), sliceSupportOutline: newBoolean(LANG.sp_outl_s, onBooleanClick, {title:LANG.sp_outl_l, xshow: () => !isTree() }), separator: newBlank({ class:"set-sep", driven, show:manualSupport }), - // sliceSupportGen: newRow([ - // ui.ssaGen = newButton(LANG.sp_detect, onButtonClick, {class: "f-col grow a-center"}) - // ], { modes: FDM, class: "ext-buttons f-row grow", show:manualSupport }), - // separator: newBlank({ class:"set-sep", driven }), sliceSupportManual: newRow([ (ui.ssmAdd = newButton(undefined, onButtonClick, {icon:''})), (ui.ssmDun = newButton(undefined, onButtonClick, {icon:''})), diff --git a/src/kiri/mode/fdm/init-ui.js b/src/kiri/mode/fdm/init-ui.js index 957ab9c8..3a7819c1 100644 --- a/src/kiri/mode/fdm/init-ui.js +++ b/src/kiri/mode/fdm/init-ui.js @@ -306,7 +306,8 @@ export function init() { if (x || y) { clearWidgetSupports(widget); } else { - // todo rotate supports when it's only a Z rotation + // rotate supports when rotation is constrained to Z axis + rotateWidgetPaint(widget, z); } }); @@ -326,7 +327,6 @@ export function init() { if (!(addingSupports && isFdmMode)) { return; } - const { int, type, point, event } = data; }); } @@ -334,3 +334,22 @@ function clearWidgetSupports(widget) { delete widget.anno.support; widget.anno.paint = []; } + +function rotateWidgetPaint(widget, angle) { + if (!widget.anno.paint?.length) { + return; + } + + // Z rotation angle in radians + const cos = Math.cos(angle); + const sin = Math.sin(angle); + + // Rotate each paint point around the Z axis + for (let paintPoint of widget.anno.paint) { + const { point } = paintPoint; + const x = point.x; + const y = point.y; + point.x = x * cos - y * sin; + point.y = x * sin + y * cos; + } +} \ No newline at end of file diff --git a/src/kiri/mode/fdm/slice.js b/src/kiri/mode/fdm/slice.js index b37c85d1..714a5e93 100644 --- a/src/kiri/mode/fdm/slice.js +++ b/src/kiri/mode/fdm/slice.js @@ -440,9 +440,9 @@ export function sliceOne(settings, widget, onupdate, ondone) { async function processSlices(slices) { /** - * Process shadow-based support generation + * Process automatic and manual shadow-based support generation */ - async function processAutoSupport() { + async function processSupports() { if (process.sliceSupportType === 'disabled') { return; } @@ -452,10 +452,6 @@ export function sliceOne(settings, widget, onupdate, ondone) { let zAngNorm = Math.sin(process.sliceSupportAngle * Math.PI / 180); let manual = process.sliceSupportType === 'manual'; - await widget.computeShadowStack(indices, progress => { - trackupdate(progress, 0.05, 0.10, "shadow"); - }, zAngNorm); - // sort bottom up so shadows do not accumulate // since that is done later and clipped to slice.clips stack.sort((a,b) => a.z - b.z); @@ -481,9 +477,14 @@ export function sliceOne(settings, widget, onupdate, ondone) { } // create automatic shadows supports when on manual paint - if (!manual) - for (let slice of stack) { - slice.shadow = await widget.shadowAt(slice.z, true); + if (!manual) { + await widget.computeShadowStack(indices, progress => { + trackupdate(progress, 0.05, 0.10, "shadow"); + }, zAngNorm); + + for (let slice of stack) { + slice.shadow = await widget.shadowAt(slice.z, true); + } } // 1. accumulate / union shadow coverage top down @@ -787,7 +788,7 @@ export function sliceOne(settings, widget, onupdate, ondone) { } // new auto support demonstrator using cast shadow - await processAutoSupport(); + await processSupports(); // process solid layers (top/bottom) processSolidLayers(); diff --git a/web/kiri/lang/en.js b/web/kiri/lang/en.js index c477fda1..77bd7e5e 100644 --- a/web/kiri/lang/en.js +++ b/web/kiri/lang/en.js @@ -137,8 +137,6 @@ self.lang['en-us'] = { dv_xtab_l: "extrusion moves absolute", dv_orgc_s: "origin center", dv_orgc_l: "bed origin center", - // dv_orgt_s: "origin top", - // dv_orgt_l: "part z origin top", dv_bedc_s: "circular bed", dv_bedc_l: "device bed is circular", dv_belt_s: "belt bed", @@ -468,8 +466,6 @@ self.lang['en-us'] = { sp_type_l: ["automatic = detect","manual = paint on"], sp_dens_s: "density", sp_dens_l: ["fraction 0.0 - 1.0","recommended 0.15","0 to disable"], - // sp_size_s: "pillar size", - // sp_size_l: ["pillar width in millimeters","smaller values exponentially","increase detection times"], sp_offs_s: "part offset", sp_offs_l: ["offset from part","in millimeters"], sp_gaps_s: "layer gap", @@ -478,18 +474,12 @@ self.lang['en-us'] = { sp_span_l: ["unsupported span that causes","a new support to be generated","in millimeters"], sp_angl_s: "angle", sp_angl_l: ["max overhang angle before","supporting pillar is generated","normal values 45-60","belt values 0-10"], - // sp_area_s: "min area", - // sp_area_l: ["minimum area for","a support column","in millimeters","smaller values will","slow detection"], sp_xpnd_s: "expand", sp_xpnd_l: ["expand support area","beyond part boundary","in millimeters"], - // sp_grow_s: "grow", - // sp_grow_l: ["grow support column to fill small gaps","in millimeters"], sp_nozl_s: "tool", sp_nozl_l: ["in multi-extruder systems","the toolhead to use for","support material"], sp_outl_s: "enclosed", sp_outl_l: ["enclose support pattern","inside a perimeter shell"], - // sp_auto_s: "automatic", - // sp_auto_l: ["enable generated supports","using at-slice-time geometry","supports will only appear","after slicing completes"], sp_tree_s: "tree type", sp_tree_l: ["use tree type support structure"], From f0ee714404de9eca6a98e935ae3321ab2f095a06 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Wed, 7 Jan 2026 13:04:13 -0500 Subject: [PATCH 121/154] add paint erase supports --- src/kiri/mode/fdm/init-menu.js | 2 +- src/kiri/mode/fdm/init-ui.js | 34 +++++++++++++++++++++++----------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/kiri/mode/fdm/init-menu.js b/src/kiri/mode/fdm/init-menu.js index ffaf931f..8131a6da 100644 --- a/src/kiri/mode/fdm/init-menu.js +++ b/src/kiri/mode/fdm/init-menu.js @@ -129,7 +129,7 @@ export function menu() { separator: newBlank({ class:"set-sep", driven, show:manualSupport }), sliceSupportManual: newRow([ (ui.ssmAdd = newButton(undefined, onButtonClick, {icon:''})), - (ui.ssmDun = newButton(undefined, onButtonClick, {icon:''})), + (ui.ssmDel = newButton(undefined, onButtonClick, {icon:''})), (ui.ssmClr = newButton(undefined, onButtonClick, {icon:''})) ], {class:"ext-buttons f-row", show:manualSupport}), _____: newGroup(LANG.fl_menu, $('fdm-base'), { modes:FDM, driven, hideable, separator, group:"fdm-base" }), diff --git a/src/kiri/mode/fdm/init-ui.js b/src/kiri/mode/fdm/init-ui.js index 3a7819c1..1f955c7d 100644 --- a/src/kiri/mode/fdm/init-ui.js +++ b/src/kiri/mode/fdm/init-ui.js @@ -13,6 +13,7 @@ const LANG = api.language.current; let lastView, addingSupports = false, + removingSupports = false, isFdmMode = false, alert = [], func = {}, @@ -164,8 +165,8 @@ export function init() { api.event.on("button.click", target => { switch (target) { case api.ui.ssaGen: return func.sgen(); - case api.ui.ssmAdd: return func.sadd(); - case api.ui.ssmDun: return func.sdone(); + case api.ui.ssmAdd: return func.sadd({ remove: false }); + case api.ui.ssmDel: return func.sadd({ remove: true }); case api.ui.ssmClr: return func.sclear(); } }); @@ -178,11 +179,15 @@ export function init() { } // manual add supports - func.sadd = () => { + func.sadd = ({ remove } = { remove: false }) => { if (addingSupports) { - return func.sdone(); + func.sdone(); + if (removingSupports === remove) { + return; + } } - alert = api.show.alert("[esc] key cancels support editing", 1000); + removingSupports = remove; + alert = api.show.alert(`[esc] key cancels ${remove?'removing':'adding'} supports`, 1000); api.feature.hover = addingSupports = true; api.feature.on_mouse_down = (int) => { if (int) { @@ -193,7 +198,7 @@ export function init() { }; api.feature.on_mouse_drag = ({ int }) => { if (int?.length && int[0].face.normal.z < 0) { - func.sadd_point(int[0].point, down.widget); + func.sadd_point(int[0].point, down.widget, remove); } return [ down.widget.mesh ]; // return api.widgets.meshes(); @@ -210,7 +215,7 @@ export function init() { api.space.update(); } - func.sadd_point = (point, widget) => { + func.sadd_point = (point, widget, remove) => { let { x, y, z } = point; let rec = { point: { x, y:-z, z:y }, @@ -221,15 +226,21 @@ export function init() { let pt = newPoint(x, -z, y); for (let r of paint) { let { point } = r; - if (newPoint(point.x, point.y, point.z).distTo2D(pt) < 1) { + let dist = newPoint(point.x, point.y, point.z).distTo2D(pt); + if (remove && dist < 2) { + r.delete = true; + } else if (dist < 1) { return; } } - paint.push(rec); + if (remove) { + paint = widget.anno.paint = paint.filter(rec => !rec.delete); + } else { + paint.push(rec); + } updatePaintOverlay(widget.mesh.material[0], paint); api.space.update(); api.conf.save(); - }; // manual add done @@ -309,6 +320,7 @@ export function init() { // rotate supports when rotation is constrained to Z axis rotateWidgetPaint(widget, z); } + api.conf.save(); }); api.event.on("mouse.hover.up", func.hoverup = (on = {}) => { @@ -319,7 +331,7 @@ export function init() { if (object && down.z < 0) { let { point } = object; let { widget } = object.object; - func.sadd_point(point, widget); + func.sadd_point(point, widget, removingSupports); } }); From ce55f395ae33a5d9e21fe80ba193e4568ddcff51 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Wed, 7 Jan 2026 14:53:18 -0500 Subject: [PATCH 122/154] update raster-path dependency to pick up large array fix --- package.json | 2 +- src/kiri/mode/fdm/post.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 223143b6..e269f823 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@gridspace/app-server": "^0.0.20", "@gridspace/basic-ftp": "github:gridspace/basic-ftp#v5.0.5-gridspace", "@gridspace/net-level-client": "^0.2.3", - "@gridspace/raster-path": "^1.1.0", + "@gridspace/raster-path": "^1.1.1", "@tracespace/parser": "^5.0.0-next.0", "@tweenjs/tween.js": "^16.6.0", "aedes": "^0.51.3", diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index 10cdbe40..07811c40 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -117,7 +117,7 @@ function offset_default(params) { // mark each poly with depth (offset #) starting at 0 for (let p of polys) { p.depth = count - onCount; - if (p.fill_off) p.fill_off.forEach(function(pi) { + if (p.fill_off) p.fill_off.forEach((pi) => { // use negative offset for inners pi.depth = -(count - onCount); }); From ecef9c4479fe5a2fc6e7ca1f2494c6645fd27b3a Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Wed, 7 Jan 2026 19:17:12 -0500 Subject: [PATCH 123/154] reflow cam op + list --- web/kiri/index.css | 5 +++-- web/kiri/index.html | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/web/kiri/index.css b/web/kiri/index.css index c3b6f387..6dc1e276 100644 --- a/web/kiri/index.css +++ b/web/kiri/index.css @@ -1534,9 +1534,10 @@ details[open] summary::after { font-size: larger; } #op-add-pop { + pointer-events: all !important; position: absolute; display: none; - top: 0; + top: 50%; right: 100%; transform: translateY(-50%); } @@ -1545,7 +1546,7 @@ details[open] summary::after { } .op-grid { display: grid !important; - grid-template-columns: 1fr 1fr; + grid-template-columns: 1fr 1fr 1fr; grid-template-rows: auto; align-items: center; gap: 5px; diff --git a/web/kiri/index.html b/web/kiri/index.html index 7291bdb1..7afcc4a5 100644 --- a/web/kiri/index.html +++ b/web/kiri/index.html @@ -455,14 +455,14 @@
-
index
-
lathe
laser on
laser off
+
index
+
lathe
flip
register
-
drill
level
+
drill
helical
gcode
contour
From 6ec01bbdbc526a6403b15b2f14577835cd582078 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Wed, 7 Jan 2026 21:56:26 -0500 Subject: [PATCH 124/154] implement svg trace path loading --- src/kiri/app/inputs.js | 6 +- src/kiri/mode/cam/cl-ops.js | 62 +++++++++----- src/kiri/mode/cam/cl-trace.js | 153 ++++++++++++++++++++++------------ web/kiri/index.css | 4 +- web/kiri/index.html | 4 +- 5 files changed, 150 insertions(+), 79 deletions(-) diff --git a/src/kiri/app/inputs.js b/src/kiri/app/inputs.js index cb876b1f..2a3a05a1 100644 --- a/src/kiri/app/inputs.js +++ b/src/kiri/app/inputs.js @@ -1034,7 +1034,11 @@ function newButton(label, action, opt = {}) { */ function newRow(children, options) { let row = addCollapsableElement((options && options.noadd) ? null : addTo); - if (children) children.forEach(function (c) { row.appendChild(c) }); + if (children) { + children.forEach((c) => { + row.appendChild(c); + }); + }; addModeControls(row, options); if (options && options.class) { options.class.split(' ').forEach(ce => { diff --git a/src/kiri/mode/cam/cl-ops.js b/src/kiri/mode/cam/cl-ops.js index b141323b..713b5b00 100644 --- a/src/kiri/mode/cam/cl-ops.js +++ b/src/kiri/mode/cam/cl-ops.js @@ -8,7 +8,7 @@ import { opFlip } from './cl-flip.js'; 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 { traceAdd, traceClear, traceLoad } from './cl-trace.js'; import { startFaceUp } from '../../app/face-tool.js'; import { showZBottom, showZTop, updateStock } from './cl-stock.js'; @@ -137,14 +137,21 @@ export function createPopOp(type, map) { return rec; }, hideshow: () => { + let conf = api.conf.get(); for (let inp of Object.values(op.inputs)) { let parent = inp.parentElement; if (parent && parent.setVisible && parent.__opt.show) { // for normal inputs - parent.setVisible(parent.__opt.show(op, api.conf.get())); + parent.setVisible(parent.__opt.show(op, conf)); } else if (inp.__opt?.show) { // for blanks - inp.setVisible(inp.__opt.show(op, api.conf.get())); + inp.setVisible(inp.__opt.show(op, conf)); + } else if (inp.children) { + for (let c of inp.children) { + if (c.__opt?.show) { + c.setVisible(c.__opt.show(op, conf)); + } + } } } }, @@ -322,10 +329,6 @@ export function createPopOps() { tool: UC.newSelect(LANG.cc_tool, {}, "tools"), direction: UC.newSelect(LANG.ou_dire_s, { title: LANG.ou_dire_l }, "direction"), sep: UC.newBlank({ class: "pop-sep" }), - spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: toInt, show: hasSpindle }), - rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: toInt, units }), - plunge: UC.newInput(LANG.cc_plng_s, { title: LANG.cc_plng_l, convert: toInt, units }), - sep: UC.newBlank({ class: "pop-sep" }), step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: toFloat, bound: UC.bound(0.01, 1.0) }), down: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: toFloat, units }), leave: UC.newInput(LANG.cr_lsto_s, { title: LANG.cr_lsto_l, convert: toFloat, units }), @@ -336,7 +339,12 @@ export function createPopOps() { inside: UC.newBoolean(LANG.cr_olin_s, undefined, { title: LANG.cr_olin_l, show: () => !env.poppedRec.all || env.poppedRec.inside }), omitthru: UC.newBoolean(LANG.co_omit_s, undefined, { title: LANG.co_omit_l }), sep: UC.newBlank({ class: "pop-sep" }), - exp: UC.newExpand("overrides", ov_hover), + exp: UC.newExpand("feeds & speeds", { }), + spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: toInt, show: hasSpindle }), + rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: toInt, units }), + plunge: UC.newInput(LANG.cc_plng_s, { title: LANG.cc_plng_l, convert: toInt, units }), + exp_end: UC.endExpand(), + exp: UC.newExpand("top & bottom", ov_hover), ov_topz: UC.newInput(LANG.ou_ztop_s, ov_topz), ov_botz: UC.newInput(LANG.ou_zbot_s, ov_botz), exp_end: UC.endExpand(), @@ -364,9 +372,6 @@ export function createPopOps() { tool: UC.newSelect(LANG.cc_tool, {}, "tools"), direction: UC.newSelect(LANG.ou_dire_s, { title: LANG.ou_dire_l }, "direction"), sep: UC.newBlank({ class: "pop-sep" }), - spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: toInt, show: hasSpindle }), - rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: toInt, units }), - plunge: UC.newInput(LANG.cc_plng_s, { title: LANG.cc_plng_l, convert: toInt, units }), down: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: toFloat, units }), step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: 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: toInt, bound: UC.bound(1, 500), show: () => env.popOp.outline.rec.wide }), @@ -380,7 +385,12 @@ export function createPopOps() { dogbones: UC.newBoolean(LANG.co_dogb_s, undefined, { title: LANG.co_dogb_l, show: (op) => { return !op.inputs.wide.checked } }), revbones: UC.newBoolean(LANG.co_dogr_s, undefined, { title: LANG.co_dogr_l, show: () => env.poppedRec.dogbones }), sep: UC.newBlank({ class: "pop-sep" }), - exp: UC.newExpand("overrides", ov_hover), + exp: UC.newExpand("feeds & speeds", { }), + spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: toInt, show: hasSpindle }), + rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: toInt, units }), + plunge: UC.newInput(LANG.cc_plng_s, { title: LANG.cc_plng_l, convert: toInt, units }), + exp_end: UC.endExpand(), + exp: UC.newExpand("top & bottom", ov_hover), ov_topz: UC.newInput(LANG.ou_ztop_s, ov_topz), ov_botz: UC.newInput(LANG.ou_zbot_s, ov_botz), exp_end: UC.endExpand(), @@ -487,10 +497,6 @@ export function createPopOps() { offset: UC.newSelect(LANG.cc_offs_s, { title: LANG.cc_offs_l, show: () => (env.poppedRec.mode === 'follow') }, "traceoff"), direction: UC.newSelect(LANG.ou_dire_s, { title: LANG.ou_dire_l }, "direction"), sep: UC.newBlank({ class: "pop-sep" }), - spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: toInt, show: hasSpindle }), - rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: toInt, units }), - plunge: UC.newInput(LANG.cc_plng_s, { title: LANG.cc_plng_l, convert: toInt, units }), - sep: UC.newBlank({ class: "pop-sep" }), step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: toFloat, bound: UC.bound(0.01, 1.0), show: (op) => env.popOp.trace.rec.mode === "clear" }), down: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: toFloat, units }), offover: UC.newInput(LANG.cc_offd_s, { title: LANG.cc_offd_l, convert: toFloat, units, show: () => env.poppedRec.offset !== "none" || env.poppedRec.mode === "clear" }), @@ -499,13 +505,22 @@ export function createPopOps() { merge: UC.newBoolean(LANG.co_merg_s, undefined, { title: LANG.co_merg_l, show: () => !env.popOp.trace.rec.down }), dogbone: UC.newBoolean(LANG.co_dogb_s, undefined, { title: LANG.co_dogb_l, show: canDogBones }), revbone: UC.newBoolean(LANG.co_dogr_s, undefined, { title: LANG.co_dogr_l, show: canDogBonesRev }), - exp: UC.newExpand("overrides", ov_hover), + exp: UC.newExpand("feeds & speeds", { }), + spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: toInt, show: hasSpindle }), + rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: toInt, units }), + plunge: UC.newInput(LANG.cc_plng_s, { title: LANG.cc_plng_l, convert: toInt, units }), + exp_end: UC.endExpand(), + exp: UC.newExpand("top & bottom", ov_hover), sep: UC.newBlank({ class: "pop-sep" }), ov_topz: UC.newInput(LANG.ou_ztop_s, ov_topz), ov_botz: UC.newInput(LANG.ou_zbot_s, ov_botz), exp_end: UC.endExpand(), sep: UC.newBlank({ class: "pop-sep" }), - menu: UC.newRow([UC.newButton("select", traceAdd)], { class: "ext-buttons f-row" }), + menu: UC.newRow([ + UC.newButton("select", traceAdd), + // UC.newButton("load", traceLoad, { show: () => env.poppedRec.mode === "follow" }), + // UC.newButton("", traceClear, { icon: '' }), + ], { class: "ext-buttons f-row", row: true }), }; createPopOp('pocket', { @@ -530,10 +545,6 @@ export function createPopOps() { tool: UC.newSelect(LANG.cc_tool, {}, "tools"), direction: UC.newSelect(LANG.ou_dire_s, { title: LANG.ou_dire_l }, "direction"), sep: UC.newBlank({ class: "pop-sep" }), - spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: toInt, show: hasSpindle }), - rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: toInt, units }), - plunge: UC.newInput(LANG.cc_plng_s, { title: LANG.cc_plng_l, convert: toInt, units }), - sep: UC.newBlank({ class: "pop-sep" }), step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: toFloat, bound: UC.bound(0.01, 1.0) }), down: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: toFloat, units, show: () => !env.poppedRec.contour }), sep: UC.newBlank({ class: "pop-sep" }), @@ -545,7 +556,12 @@ export function createPopOps() { sep: UC.newBlank({ class: "pop-sep" }), contour: UC.newBoolean(LANG.cp_cont_s, undefined, { title: LANG.cp_cont_s }), outline: UC.newBoolean(LANG.cp_outl_s, undefined, { title: LANG.cp_outl_l }), - exp: UC.newExpand("overrides", ov_hover), + exp: UC.newExpand("feeds & speeds", { }), + spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: toInt, show: hasSpindle }), + rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: toInt, units }), + plunge: UC.newInput(LANG.cc_plng_s, { title: LANG.cc_plng_l, convert: toInt, units }), + exp_end: UC.endExpand(), + exp: UC.newExpand("top & bottom", ov_hover), sep: UC.newBlank({ class: "pop-sep" }), ov_topz: UC.newInput(LANG.ou_ztop_s, ov_topz), ov_botz: UC.newInput(LANG.ou_zbot_s, ov_botz), diff --git a/src/kiri/mode/cam/cl-trace.js b/src/kiri/mode/cam/cl-trace.js index 3fd4666b..31203c63 100644 --- a/src/kiri/mode/cam/cl-trace.js +++ b/src/kiri/mode/cam/cl-trace.js @@ -2,9 +2,11 @@ import { api } from '../../app/api.js'; import { env, clearPops, isDark } from './init-ui.js'; +import { parse } from '../../../load/svg.js'; import { CAM } from './driver-fe.js'; import { Layers } from '../../app/layers.js'; import { Stack } from '../../app/stack.js'; +import { newPolygon } from '../../../geo/polygon.js'; export let traceOn = false; export let lastTrace; @@ -13,6 +15,95 @@ export function setLastTrace(trace) { lastTrace = trace; } +export function traceLoad(ev) { + let widget = api.widgets.all()[0]; + if (!widget) return; + window.showOpenFilePicker({ + types: [{ + description: 'SVG Files', + accept: { 'text/svg': ['.svg'] } + }], + multiple: false + }).then(async fileHandle => { + const file = await fileHandle[0].getFile() + const text = await file.text(); // string + // const buf = await file.arrayBuffer(); // binary + // const blob = await file.slice(); + const polys = parse(text, { flat: true, soup: true }); + const rec = env.poppedRec; + rec.areas[widget.id] = polys.map(p => p.toArray()); + rec.svg = true; + traceAdd(); + }).catch(error => { + console.log('no file load', error); + }); +} + +export function traceClear(ev) { + return; + api.widgets.for(widget => { + unselectTraces(widget, true); + widget.traces = []; + widget.adds = []; + }); + env.poppedRec.areas = []; + env.poppedRec.svg = false; + traceDone(); +} + +function startTraceAdd(ids) { + api.hide.alert(alert); + alert = api.show.alert("[esc] cancels trace editing"); + api.widgets.for(widget => { + if (ids.indexOf(widget.id) >= 0) { + unselectTraces(widget, true); + widget.trace_stack = null; + } + if (widget.trace_stack) { + widget.adds.appendAll(widget.trace_stack.meshes); + widget.trace_stack.show(); + return; + } + let areas = (env.poppedRec.areas[widget.id] || []); + let stack = new Stack(widget.mesh); + widget.trace_stack = stack; + widget.traces?.forEach(poly => { + let match = areas.filter(arr => poly.matches(arr)); + let layers = new Layers(); + layers.setLayer("trace", { line: 0xaaaa55, fat: 4, order: -10 }, false).addPoly(poly); + stack.addLayers(layers); + stack.new_meshes.forEach(mesh => { + mesh.trace = { widget, poly }; + // ensure trace poly singleton from matches + if (match.length > 0) { + poly._trace = match[0]; + } else { + poly._trace = poly.toArray(); + } + }); + widget.adds.appendAll(stack.new_meshes); + // console.log(widget) + }); + }); + // ensure appropriate traces are toggled matching current record + api.widgets.for(widget => { + widget.setVisualState({ opacity: 0.25 }); + let areas = (env.poppedRec.areas[widget.id] || []); + let stack = widget.trace_stack; + stack.meshes.forEach(mesh => { + let { poly } = mesh.trace; + let match = areas.filter(arr => poly.matches(arr)); + if (match.length > 0) { + if (!mesh.selected) { + traceToggle(mesh, true); + } + } else if (mesh.selected) { + traceToggle(mesh, true); + } + }); + }); +} + export function traceAdd(ev) { if (traceOn) { return traceDone(); @@ -25,58 +116,16 @@ export function traceAdd(ev) { api.feature.hoverAdds = true; env.hover = traceHover; env.hoverUp = traceHoverUp; - CAM.traces((ids) => { - api.hide.alert(alert); - alert = api.show.alert("[esc] cancels trace editing"); - api.widgets.for(widget => { - if (ids.indexOf(widget.id) >= 0) { - unselectTraces(widget, true); - widget.trace_stack = null; - } - if (widget.trace_stack) { - widget.adds.appendAll(widget.trace_stack.meshes); - widget.trace_stack.show(); - return; - } - let areas = (env.poppedRec.areas[widget.id] || []); - let stack = new Stack(widget.mesh); - widget.trace_stack = stack; - widget.traces?.forEach(poly => { - let match = areas.filter(arr => poly.matches(arr)); - let layers = new Layers(); - layers.setLayer("trace", { line: 0xaaaa55, fat: 4, order: -10 }, false).addPoly(poly); - stack.addLayers(layers); - stack.new_meshes.forEach(mesh => { - mesh.trace = { widget, poly }; - // ensure trace poly singleton from matches - if (match.length > 0) { - poly._trace = match[0]; - } else { - poly._trace = poly.toArray(); - } - }); - widget.adds.appendAll(stack.new_meshes); - // console.log(widget) - }); - }); - // ensure appropriate traces are toggled matching current record - api.widgets.for(widget => { - widget.setVisualState({ opacity: 0.25 }); - let areas = (env.poppedRec.areas[widget.id] || []); - let stack = widget.trace_stack; - stack.meshes.forEach(mesh => { - let { poly } = mesh.trace; - let match = areas.filter(arr => poly.matches(arr)); - if (match.length > 0) { - if (!mesh.selected) { - traceToggle(mesh, true); - } - } else if (mesh.selected) { - traceToggle(mesh, true); - } - }); - }); - }, env.poppedRec.select === 'lines'); + if (env.poppedRec.svg === true) { + let widgets = api.widgets.all(); + for (let [ id, areas ] of Object.entries(env.poppedRec.areas)) { + let widget = widgets.filter(w => w.id === id)[0]; + widget.traces = areas.map(arr => newPolygon().fromArray(arr)); + } + startTraceAdd(Object.keys(env.poppedRec.areas)); + } else { + CAM.traces(startTraceAdd, env.poppedRec.select === 'lines'); + } } export function traceDone() { diff --git a/web/kiri/index.css b/web/kiri/index.css index 6dc1e276..6150a59a 100644 --- a/web/kiri/index.css +++ b/web/kiri/index.css @@ -1346,10 +1346,12 @@ details[open] summary::after { cursor: pointer; } .ext-buttons button { + display: inline-flex; + line-height: 1; justify-content: center; align-items: center; text-align: center; - flex-grow: 1; + flex: 1 1 0; /* equal width */ margin-top: 3px; height: 75%; } diff --git a/web/kiri/index.html b/web/kiri/index.html index 7afcc4a5..c90fac76 100644 --- a/web/kiri/index.html +++ b/web/kiri/index.html @@ -110,8 +110,8 @@
- - + +

From 6e09b9125e25d35b17b316bff6b65516a78b6ac8 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Wed, 7 Jan 2026 22:12:00 -0500 Subject: [PATCH 125/154] add missing camTraceOffset default value --- src/kiri/app/conf/defaults.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/kiri/app/conf/defaults.js b/src/kiri/app/conf/defaults.js index 81513b08..7867d695 100644 --- a/src/kiri/app/conf/defaults.js +++ b/src/kiri/app/conf/defaults.js @@ -606,6 +606,7 @@ export const conf = { camTraceLines: false, camTraceMerge: false, camTraceOffOver: 0, + camTraceOffset: "none", camTraceOver: 0.5, camTracePlunge: 200, camTraceSpeed: 250, From edff2228f720a6295bbeff45c9eff52d336da54c Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Wed, 7 Jan 2026 22:32:44 -0500 Subject: [PATCH 126/154] handle null path return for area surface --- src/kiri/mode/cam/op-area.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kiri/mode/cam/op-area.js b/src/kiri/mode/cam/op-area.js index 1dd68fee..2e498e83 100644 --- a/src/kiri/mode/cam/op-area.js +++ b/src/kiri/mode/cam/op-area.js @@ -359,7 +359,7 @@ class OpArea extends CamOp { paths.forEach(path => path.reverse()); } // optional alternating paths - if (sr_alter) { + if (paths.length && sr_alter) { paths = tip2tipJoin(paths, paths[0].first(), toolOver * 10); } } else From a13bb79f4b3b10592b85d23912f592428f0d4f09 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 8 Jan 2026 07:35:41 -0500 Subject: [PATCH 127/154] reflow fdm client ui events, fix paint on near vertical surfaces --- src/kiri/app/paint.js | 4 +- src/kiri/mode/fdm/init-ui.js | 261 +++++++++++++++-------------------- 2 files changed, 116 insertions(+), 149 deletions(-) diff --git a/src/kiri/app/paint.js b/src/kiri/app/paint.js index ab15256a..872df5b8 100644 --- a/src/kiri/app/paint.js +++ b/src/kiri/app/paint.js @@ -156,7 +156,7 @@ function addPaintOverlaySimple(material, paintPoints, paintColor = new THREE.Col vec3 worldNormal = normalize(cross(posDx, posDy)); // Only paint downward-facing surfaces if option is enabled - bool shouldPaint = !onlyDownwardFacing || worldNormal.z < 0.0; + bool shouldPaint = !onlyDownwardFacing || worldNormal.z < -0.001; if (shouldPaint) { // Check if this fragment is within any paint sphere bool painted = false; @@ -302,7 +302,7 @@ function addPaintOverlayTexture(material, paintPoints, paintColor = new THREE.Co vec3 worldNormal = normalize(cross(posDx, posDy)); // Only paint downward-facing surfaces if option is enabled - bool shouldPaint = !onlyDownwardFacing || worldNormal.z < 0.0; + bool shouldPaint = !onlyDownwardFacing || worldNormal.z < -0.001; if (shouldPaint) { bool painted = false; diff --git a/src/kiri/mode/fdm/init-ui.js b/src/kiri/mode/fdm/init-ui.js index 1f955c7d..0625fc18 100644 --- a/src/kiri/mode/fdm/init-ui.js +++ b/src/kiri/mode/fdm/init-ui.js @@ -6,7 +6,6 @@ import { api } from '../../app/api.js'; import { conf } from '../../app/conf/defaults.js'; import { addPaintOverlayTexture, updatePaintOverlay } from '../../app/paint.js'; -const { Matrix4, Vector3, BufferAttribute, BufferGeometryUtils, Raycaster, Euler } = THREE; const { VIEWS } = api.const; const LANG = api.language.current; @@ -16,7 +15,6 @@ let lastView, removingSupports = false, isFdmMode = false, alert = [], - func = {}, down; export function init() { @@ -80,6 +78,13 @@ export function init() { ui.fdmRanges.innerHTML = ''; } + function onEventFDM(topic, fn) { + return api.event.on(topic, function() { + if (!isFdmMode) return; + fn(...arguments); + }); + } + // re-render ranges menu function updateRanges(ranges = []) { let html = []; @@ -134,7 +139,7 @@ export function init() { api.event.on("view.set", view => { $('top-slider').style.display = 'none'; lastView = view; - func.sdone(); + supportDone(); // let ranges = api.conf.get().process.ranges; if (isFdmMode) { if (lastView === VIEWS.SLICE) { @@ -164,155 +169,30 @@ export function init() { api.event.on("button.click", target => { switch (target) { - case api.ui.ssaGen: return func.sgen(); - case api.ui.ssmAdd: return func.sadd({ remove: false }); - case api.ui.ssmDel: return func.sadd({ remove: true }); - case api.ui.ssmClr: return func.sclear(); + case api.ui.ssmAdd: return supportStart({ remove: false }); + case api.ui.ssmDel: return supportStart({ remove: true }); + case api.ui.ssmClr: return supportClear(); } }); - // auto-detect supports - func.sgen = () => { - let alerts = []; - alerts.push(api.show.alert("analyzing part(s)...", 1000)); - setTimeout(() => api.hide.alert(undefined,alerts), 2000); - } + onEventFDM("key.esc", supportDone); - // manual add supports - func.sadd = ({ remove } = { remove: false }) => { - if (addingSupports) { - func.sdone(); - if (removingSupports === remove) { - return; - } - } - removingSupports = remove; - alert = api.show.alert(`[esc] key cancels ${remove?'removing':'adding'} supports`, 1000); - api.feature.hover = addingSupports = true; - api.feature.on_mouse_down = (int) => { - if (int) { - let { point } = int; - down = { point, widget: int.object.widget, z: int.face.normal.z }; - } - return api.widgets.meshes(); - }; - api.feature.on_mouse_drag = ({ int }) => { - if (int?.length && int[0].face.normal.z < 0) { - func.sadd_point(int[0].point, down.widget, remove); - } - return [ down.widget.mesh ]; - // return api.widgets.meshes(); - }; - api.widgets.each(w => { - let mat = w.cache.mat = w.mesh.material[0]; - mat = w.mesh.material[0] = mat.clone(); - mat.needsUpdate = true; - if (!w.anno.paint) { - w.anno.paint = []; - } - addPaintOverlayTexture(mat, w.anno.paint, new THREE.Color(0x4488ff)); - }) - api.space.update(); - } + onEventFDM("slice.begin", supportDone); - func.sadd_point = (point, widget, remove) => { - let { x, y, z } = point; - let rec = { - point: { x, y:-z, z:y }, - radius: 2 - }; - // return console.log({ on, widget, point }); - let paint = widget.anno.paint; - let pt = newPoint(x, -z, y); - for (let r of paint) { - let { point } = r; - let dist = newPoint(point.x, point.y, point.z).distTo2D(pt); - if (remove && dist < 2) { - r.delete = true; - } else if (dist < 1) { - return; - } - } - if (remove) { - paint = widget.anno.paint = paint.filter(rec => !rec.delete); - } else { - paint.push(rec); - } - updatePaintOverlay(widget.mesh.material[0], paint); - api.space.update(); - api.conf.save(); - }; + // onEventFDM("slice.end", api.noop); - // manual add done - func.sdone = () => { - if (!addingSupports) { - return; - } - api.feature.hover = addingSupports = false; - api.feature.on_mouse_down = undefined; - api.feature.on_mouse_drag = undefined; - api.hide.alert(alert); - api.widgets.each(w => { - w.mesh.material[0] = w.cache.mat; - delete w.cache.mat; - }); - } - - // manual supports clear - func.sclear = () => { - func.sdone(); - api.widgets.each(w => clearWidgetSupports(w)); - api.conf.save(); - } - - api.event.on("slice.begin", () => { - if (!isFdmMode) { - return; - } - func.sdone(); + onEventFDM("selection.scale", () => { + clearRanges(); + supportClear() }); - api.event.on("slice.end", () => { - if (!isFdmMode) { - return; - } - }); + // onEventFDM("widget.delete", api.noop); - api.event.on("key.esc", () => { - if (isFdmMode) { - func.sdone() - } - }); + // onEventFDM("widget.duplicate", api.noop); - api.event.on("selection.scale", () => { - if (isFdmMode) { - clearRanges(); - func.sclear(); - } - }); + // onEventFDM("widget.mirror", api.noop); - api.event.on("widget.delete", widget => { - if (isFdmMode) { - clearRanges(); - } - }); - - api.event.on("widget.duplicate", (widget, oldwidget) => { - if (!isFdmMode) { - return; - } - }); - - api.event.on("widget.mirror", widget => { - if (!isFdmMode) { - return; - } - }); - - api.event.on("widget.rotate", rot => { - if (!isFdmMode) { - return; - } + onEventFDM("widget.rotate", rot => { let {widget, x, y, z} = rot; if (x || y) { clearWidgetSupports(widget); @@ -323,20 +203,20 @@ export function init() { api.conf.save(); }); - api.event.on("mouse.hover.up", func.hoverup = (on = {}) => { - if (!(addingSupports && isFdmMode)) { + onEventFDM("mouse.hover.up", (on = {}) => { + if (!addingSupports) { return; } let { object } = on; if (object && down.z < 0) { let { point } = object; let { widget } = object.object; - func.sadd_point(point, widget, removingSupports); + supportUpdate(point, widget, removingSupports); } }); - api.event.on("mouse.hover", data => { - if (!(addingSupports && isFdmMode)) { + onEventFDM("mouse.hover", data => { + if (!addingSupports) { return; } }); @@ -364,4 +244,91 @@ function rotateWidgetPaint(widget, angle) { point.x = x * cos - y * sin; point.y = x * sin + y * cos; } -} \ No newline at end of file +} + +// start manual add supports +function supportStart({ remove } = { remove: false }) { + if (addingSupports) { + supportDone(); + if (removingSupports === remove) { + return; + } + } + removingSupports = remove; + alert = api.show.alert(`[esc] key cancels ${remove?'removing':'adding'} supports`, 1000); + api.feature.hover = addingSupports = true; + api.feature.on_mouse_down = (int) => { + if (int) { + let { point } = int; + down = { point, widget: int.object.widget, z: int.face.normal.z }; + } + return api.widgets.meshes(); + }; + api.feature.on_mouse_drag = ({ int }) => { + if (int?.length && int[0].face.normal.z < 0) { + supportUpdate(int[0].point, down.widget, remove); + } + return [ down.widget.mesh ]; + // return api.widgets.meshes(); + }; + api.widgets.each(w => { + let mat = w.cache.mat = w.mesh.material[0]; + mat = w.mesh.material[0] = mat.clone(); + mat.needsUpdate = true; + if (!w.anno.paint) { + w.anno.paint = []; + } + addPaintOverlayTexture(mat, w.anno.paint, new THREE.Color(0x4488ff)); + }) + api.space.update(); +} + +function supportUpdate(point, widget, remove) { + let { x, y, z } = point; + let rec = { + point: { x, y:-z, z:y }, + radius: 2 + }; + // return console.log({ on, widget, point }); + let paint = widget.anno.paint; + let pt = newPoint(x, -z, y); + for (let r of paint) { + let { point } = r; + let dist = newPoint(point.x, point.y, point.z).distTo2D(pt); + if (remove && dist < 2) { + r.delete = true; + } else if (dist < 1) { + return; + } + } + if (remove) { + paint = widget.anno.paint = paint.filter(rec => !rec.delete); + } else { + paint.push(rec); + } + updatePaintOverlay(widget.mesh.material[0], paint); + api.space.update(); + api.conf.save(); +}; + +// manual add done +function supportDone() { + if (!addingSupports) { + return; + } + api.feature.hover = addingSupports = false; + api.feature.on_mouse_down = undefined; + api.feature.on_mouse_drag = undefined; + api.hide.alert(alert); + api.widgets.each(w => { + w.mesh.material[0] = w.cache.mat; + delete w.cache.mat; + }); +} + +// manual supports clear +function supportClear() { + supportDone(); + api.widgets.each(w => clearWidgetSupports(w)); + api.conf.save(); +} From 9900161dcdb9640be2b2b85aea634d8bc7d1ec0e Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 8 Jan 2026 14:35:58 -0500 Subject: [PATCH 128/154] refactor color and opacity/edge management --- src/kiri/app/api.js | 3 +- src/kiri/app/color/base.js | 69 +++++++++++ src/kiri/app/color/cam.js | 58 +++++++++ src/kiri/app/color/fdm.js | 42 +++++++ src/kiri/app/color/laser.js | 28 +++++ src/kiri/app/color/schemes.js | 145 +++++++++++++++++++++++ src/kiri/app/color/sla.js | 23 ++++ src/kiri/app/conf/manager.js | 17 ++- src/kiri/app/function.js | 33 ++++-- src/kiri/app/platform.js | 32 +++-- src/kiri/app/state/visual.js | 215 ++++++++++++++++++++++++++++++++++ src/kiri/app/visuals.js | 15 ++- src/kiri/app/widget.js | 123 ++++++++++++++++--- src/kiri/core/consts.js | 20 +--- src/kiri/core/widget.js | 3 + src/kiri/mode/cam/cl-tab.js | 11 +- src/kiri/mode/fdm/init-ui.js | 22 +++- 17 files changed, 795 insertions(+), 64 deletions(-) create mode 100644 src/kiri/app/color/base.js create mode 100644 src/kiri/app/color/cam.js create mode 100644 src/kiri/app/color/fdm.js create mode 100644 src/kiri/app/color/laser.js create mode 100644 src/kiri/app/color/schemes.js create mode 100644 src/kiri/app/color/sla.js create mode 100644 src/kiri/app/state/visual.js diff --git a/src/kiri/app/api.js b/src/kiri/app/api.js index 2d9c8ccd..d37a80e0 100644 --- a/src/kiri/app/api.js +++ b/src/kiri/app/api.js @@ -10,7 +10,7 @@ import web from '../../moto/webui.js'; import { beta, version } from '../../moto/license.js'; import { broker } from '../../moto/broker.js'; import { client as workers } from './workers.js'; -import { consts, COLOR as color, LISTS as lists } from '../core/consts.js'; +import { consts, LISTS as lists } from '../core/consts.js'; import { device, devices } from './devices.js'; import { functions } from './function.js'; import { group as groupModule } from './groups.js'; @@ -113,7 +113,6 @@ export const api = { .catch(err => console.error('Clipboard Error:', err)); }, clone: Object.clone, - color, conf: settings.conf, const: { LANG, LOCAL, SETUP, SECURE, SPACE, STACKS, diff --git a/src/kiri/app/color/base.js b/src/kiri/app/color/base.js new file mode 100644 index 00000000..7143df03 --- /dev/null +++ b/src/kiri/app/color/base.js @@ -0,0 +1,69 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +/** + * BASE_SCHEME: Shared colors used across all device modes + * All mode-specific schemes inherit from this base + */ +const BASE_SCHEME = { + // Widget colors for selected/deselected states + widget: { + selected: { + light: [ 0xbbff00, 0xbbee00, 0xbbdd00, 0xbb9900 ], + dark: [ 0xbbff00, 0xbbee00, 0xbbdd00, 0xbb9900 ] + }, + deselected: { + light: [ 0xffff00, 0xffdd00, 0xffbb00, 0xff9900 ], + dark: [ 0xffff00, 0xffdd00, 0xffbb00, 0xff9900 ] + }, + disabled: { + // Computed via avgc(0x888888, baseColor, 3) + mixWith: 0x888888, + mixRatio: 3 + } + }, + + // Edge rendering settings + edges: { + color: { + light: 0x888888, + dark: 0x444444 + }, + angle: 20 // Default edge detection angle threshold + }, + + // Wireframe rendering settings + wireframe: { + color: { + light: 0x000000, + dark: 0xaaaaaa + }, + opacity: { + light: 0.5, + dark: 0.25 + } + }, + + // Grid colors for platform + grid: { + major: { + light: 0x999999, + dark: 0x666666 + }, + minor: { + light: 0xcccccc, + dark: 0x333333 + } + }, + + // View-specific opacity defaults (common across modes) + views: { + ARRANGE: { + model_opacity: 1.0 + }, + SLICE: { + slicing_opacity: 0.5 + } + } +}; + +export { BASE_SCHEME }; diff --git a/src/kiri/app/color/cam.js b/src/kiri/app/color/cam.js new file mode 100644 index 00000000..874562d1 --- /dev/null +++ b/src/kiri/app/color/cam.js @@ -0,0 +1,58 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { BASE_SCHEME } from './base.js'; + +/** + * CAM_SCHEME: Colors specific to CAM (3-axis milling/machining) mode + * Inherits from BASE_SCHEME and overrides/extends with CAM-specific values + */ +const CAM_SCHEME = { + // Inherit all base colors + ...BASE_SCHEME, + + // CAM-specific view overrides + views: { + ...BASE_SCHEME.views, + SLICE: { + ...BASE_SCHEME.views.SLICE, + sliced_opacity: 0.2 // CAM shows more during slice (vs FDM 0.0) + }, + PREVIEW: { + preview: { + light: 0xdddddd, + dark: 0x888888 + }, + preview_opacity: { + light: 0.2, + dark: 0.2 + } + } + }, + + // CAM-specific operations + operations: { + // Tab visualization (persistent geometry) + tabs: { + color: { + light: 0x0000dd, // Blue in light mode + dark: 0x00ddff // Cyan in dark mode + }, + opacity: { + light: 0.6, + dark: 0.75 + } + } + }, + + // Gcode preview colors (same as FDM, but documented for CAM) + gcode: { + head: 0x888888, + move: { + light: 0xaaaaaa, + dark: 0x666666 + }, + print: 0x777700 + } +}; + +export { CAM_SCHEME }; diff --git a/src/kiri/app/color/fdm.js b/src/kiri/app/color/fdm.js new file mode 100644 index 00000000..9a7f761d --- /dev/null +++ b/src/kiri/app/color/fdm.js @@ -0,0 +1,42 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { BASE_SCHEME } from './base.js'; + +/** + * FDM_SCHEME: Colors specific to FDM (Fused Deposition Modeling) mode + * Inherits from BASE_SCHEME and overrides/extends with FDM-specific values + */ +const FDM_SCHEME = { + // Inherit all base colors + ...BASE_SCHEME, + + // FDM-specific view overrides + views: { + ...BASE_SCHEME.views, + PREVIEW: { + preview_opacity: 0.0, // FDM hides widget in preview + sliced_opacity: 0.0 + } + }, + + // FDM-specific operations + operations: { + // Support painting overlay + paint: { + overlay: 0x4488ff, // Light blue for painted support regions + opacity: 0.7 + } + }, + + // Gcode preview colors + gcode: { + head: 0x888888, + move: { + light: 0xaaaaaa, + dark: 0x666666 + }, + print: 0x777700 + } +}; + +export { FDM_SCHEME }; diff --git a/src/kiri/app/color/laser.js b/src/kiri/app/color/laser.js new file mode 100644 index 00000000..29143a22 --- /dev/null +++ b/src/kiri/app/color/laser.js @@ -0,0 +1,28 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { BASE_SCHEME } from './base.js'; + +/** + * LASER_SCHEME: Colors for LASER, DRAG (knife), WJET (waterjet), and WEDM (wire-edm) modes + * All 2D device types share similar visual characteristics + * Inherits from BASE_SCHEME with minimal overrides + */ +const LASER_SCHEME = { + // Inherit all base colors + ...BASE_SCHEME, + + // Laser-specific view overrides (similar to CAM) + views: { + ...BASE_SCHEME.views, + PREVIEW: { + preview_opacity: 0.2 + } + } +}; + +// DRAG, WJET, and WEDM all use the same scheme as LASER +const DRAG_SCHEME = LASER_SCHEME; +const WJET_SCHEME = LASER_SCHEME; +const WEDM_SCHEME = LASER_SCHEME; + +export { LASER_SCHEME, DRAG_SCHEME, WJET_SCHEME, WEDM_SCHEME }; diff --git a/src/kiri/app/color/schemes.js b/src/kiri/app/color/schemes.js new file mode 100644 index 00000000..a139a3f2 --- /dev/null +++ b/src/kiri/app/color/schemes.js @@ -0,0 +1,145 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { FDM_SCHEME } from './fdm.js'; +import { CAM_SCHEME } from './cam.js'; +import { SLA_SCHEME } from './sla.js'; +import { LASER_SCHEME, DRAG_SCHEME, WJET_SCHEME, WEDM_SCHEME } from './laser.js'; + +/** + * ColorSchemeRegistry: Central registry for all mode-specific color schemes + * Provides theme-aware color resolution for UI rendering + */ +class ColorSchemeRegistry { + constructor() { + // Map mode IDs to their schemes + this.schemes = { + 1: FDM_SCHEME, // FDM mode + 2: LASER_SCHEME, // LASER mode + 3: CAM_SCHEME, // CAM mode + 4: SLA_SCHEME, // SLA mode + 5: DRAG_SCHEME, // DRAG knife mode + 6: WJET_SCHEME, // Waterjet mode + 7: WEDM_SCHEME // Wire-EDM mode + }; + } + + /** + * Get the complete scheme for a given mode and theme + * @param {number|string} mode - Mode ID (1=FDM, 2=LASER, 3=CAM, etc) or name + * @param {string} theme - 'light' or 'dark' + * @returns {object} Resolved scheme object + */ + getScheme(mode, theme = 'light') { + // Convert mode name to ID if needed + if (typeof mode === 'string') { + mode = this._modeNameToId(mode); + } + + const scheme = this.schemes[mode]; + if (!scheme) { + console.warn(`Unknown mode: ${mode}, using FDM scheme as fallback`); + return this._resolveTheme(this.schemes[1], theme); + } + + return this._resolveTheme(scheme, theme); + } + + /** + * Get a specific color value from a scheme + * @param {number|string} mode - Mode ID or name + * @param {string} theme - 'light' or 'dark' + * @param {string} path - Dot-separated path (e.g., 'widget.selected', 'edges.color') + * @returns {*} Color value or object + */ + getColor(mode, theme, path) { + const scheme = this.getScheme(mode, theme); + return this._getNestedValue(scheme, path); + } + + /** + * Compute disabled color by mixing base color with gray + * Implements: avgc(0x888888, baseColor, 3) + * @param {number} baseColor - Base color hex value + * @returns {number} Mixed color hex value + */ + computeDisabledColor(baseColor) { + // Extract RGB components + const r1 = (0x888888 >> 16) & 0xFF; + const g1 = (0x888888 >> 8) & 0xFF; + const b1 = 0x888888 & 0xFF; + + const r2 = (baseColor >> 16) & 0xFF; + const g2 = (baseColor >> 8) & 0xFF; + const b2 = baseColor & 0xFF; + + // Average with ratio 3:1 (3 parts gray, 1 part base) + const r = Math.floor((r1 * 3 + r2) / 4); + const g = Math.floor((g1 * 3 + g2) / 4); + const b = Math.floor((b1 * 3 + b2) / 4); + + return (r << 16) | (g << 8) | b; + } + + /** + * Resolve theme-specific values throughout the scheme + * @private + */ + _resolveTheme(scheme, theme) { + const resolved = JSON.parse(JSON.stringify(scheme)); // Deep clone + + this._resolveThemeRecursive(resolved, theme); + + return resolved; + } + + /** + * Recursively resolve theme values in an object + * @private + */ + _resolveThemeRecursive(obj, theme) { + for (const key in obj) { + const value = obj[key]; + + if (value && typeof value === 'object') { + // Check if this object has light/dark properties + if ('light' in value && 'dark' in value) { + // Replace the object with the theme-specific value + obj[key] = value[theme]; + } else { + // Recurse into nested objects + this._resolveThemeRecursive(value, theme); + } + } + } + } + + /** + * Get nested value from object using dot-separated path + * @private + */ + _getNestedValue(obj, path) { + return path.split('.').reduce((current, key) => current?.[key], obj); + } + + /** + * Convert mode name to ID + * @private + */ + _modeNameToId(name) { + const modes = { + 'FDM': 1, + 'LASER': 2, + 'CAM': 3, + 'SLA': 4, + 'DRAG': 5, + 'WJET': 6, + 'WEDM': 7 + }; + return modes[name.toUpperCase()] || 1; // Default to FDM + } +} + +// Create singleton instance +const colorSchemeRegistry = new ColorSchemeRegistry(); + +export { colorSchemeRegistry, ColorSchemeRegistry }; diff --git a/src/kiri/app/color/sla.js b/src/kiri/app/color/sla.js new file mode 100644 index 00000000..455ce244 --- /dev/null +++ b/src/kiri/app/color/sla.js @@ -0,0 +1,23 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { BASE_SCHEME } from './base.js'; + +/** + * SLA_SCHEME: Colors specific to SLA (stereolithography/resin) mode + * Inherits from BASE_SCHEME with minimal overrides + */ +const SLA_SCHEME = { + // Inherit all base colors + ...BASE_SCHEME, + + // SLA-specific view overrides (similar to FDM) + views: { + ...BASE_SCHEME.views, + PREVIEW: { + preview_opacity: 0.0, + sliced_opacity: 0.0 + } + } +}; + +export { SLA_SCHEME }; diff --git a/src/kiri/app/conf/manager.js b/src/kiri/app/conf/manager.js index 81fb7339..f1d9ebac 100644 --- a/src/kiri/app/conf/manager.js +++ b/src/kiri/app/conf/manager.js @@ -12,10 +12,20 @@ import { utils } from '../../core/utils.js'; import { version } from '../../../moto/license.js'; import { JSZip } from '../../../ext/jszip-esm.js'; +import { colorSchemeRegistry } from '../color/schemes.js'; + const { areEqual, ls2o, js2o } = utils; -const { COLOR } = consts; const { clone } = Object; +/** + * Get current color scheme + */ +function getColorScheme() { + const mode = api.mode.get_id(); + const theme = api.space.is_dark() ? 'dark' : 'light'; + return colorSchemeRegistry.getScheme(mode, theme); +} + const localFilterKey ='kiri-gcode-filters'; const localFilters = js2o(local.getItem(localFilterKey)) || []; @@ -322,9 +332,10 @@ function updateExtruderFields(device) { onclick() { api.selection.for_widgets(w => { w.anno.extruder = i; + const scheme = getColorScheme(); w.setColor( api.selection.contains(w) ? - COLOR.selected : COLOR.deselected, + scheme.widget.selected : scheme.widget.deselected, undefined, false); }); @@ -332,7 +343,7 @@ function updateExtruderFields(device) { }), h.div({ class: "splat", - style: `background-color: #${COLOR.deselected[i].toString(16)}` + style: `background-color: #${getColorScheme().widget.deselected[i].toString(16)}` }) ]))); } else { diff --git a/src/kiri/app/function.js b/src/kiri/app/function.js index a2f69cc6..e3832c80 100644 --- a/src/kiri/app/function.js +++ b/src/kiri/app/function.js @@ -4,9 +4,19 @@ import { api } from './api.js'; import { client } from './workers.js'; import { codec } from '../core/codec.js'; import { space } from '../../moto/space.js'; -import { COLOR, PMODES } from '../core/consts.js'; +import { PMODES } from '../core/consts.js'; +import { colorSchemeRegistry } from './color/schemes.js'; import { exportFile } from './export.js'; +/** + * Get current color scheme + */ +function getColorScheme() { + const mode = api.mode.get_id(); + const theme = api.space.is_dark() ? 'dark' : 'light'; + return colorSchemeRegistry.getScheme(mode, theme); +} + /** * Tracks completion state of operations to prevent redundant work. * Properties: slice, preview, export @@ -96,7 +106,8 @@ function prepareSlices(callback, scale = 1, offset = 0) { let totalProgress; - api.widgets.setOpacity(COLOR.slicing_opacity); + const scheme = getColorScheme(); + api.widgets.setOpacity(scheme.views.SLICE.slicing_opacity); let segtimes = {}, segNumber = 0, @@ -110,7 +121,8 @@ function prepareSlices(callback, scale = 1, offset = 0) { for (let widget of toSlice) { widget.belt = null; widget.stats.progress = 0; - widget.setColor(COLOR.slicing); + const scheme = getColorScheme(); + widget.setColor(0xffaaaa); // slicing color (from old COLOR.slicing) let { extruder } = widget.anno; if (extruder >= 0) { extruders[extruder] = extruder; @@ -277,9 +289,10 @@ function prepareSlices(callback, scale = 1, offset = 0) { } if (scale === 1) { // clear wireframe - widget.setWireframe(false, COLOR.wireframe, COLOR.wireframe_opacity); - widget.setOpacity(camOrLaser ? COLOR.cam_sliced_opacity : COLOR.sliced_opacity); - widget.setColor(COLOR.deselected); + const scheme = getColorScheme(); + widget.setWireframe(false, scheme.wireframe.color, scheme.wireframe.opacity); + widget.setOpacity(camOrLaser ? scheme.views.SLICE.sliced_opacity ?? 0.2 : scheme.views.PREVIEW?.sliced_opacity ?? 0.0); + widget.setColor(scheme.widget.deselected); api.hide.alert(alert); } } @@ -365,10 +378,12 @@ function preparePreview(callback, scale = 1, offset = 0) { event.emit('preview.begin', pMode); if (isCam) { - api.widgets.setOpacity(isDark ? COLOR.cam_preview_opacity_dark : COLOR.cam_preview_opacity); - api.widgets.setColor(isDark ? COLOR.cam_preview_dark : COLOR.cam_preview); + const scheme = getColorScheme(); + api.widgets.setOpacity(scheme.views.PREVIEW.preview_opacity); + api.widgets.setColor(scheme.views.PREVIEW.preview); } else if (offset === 0) { - api.widgets.setOpacity(COLOR.preview_opacity); + const scheme = getColorScheme(); + api.widgets.setOpacity(scheme.views.PREVIEW.preview_opacity); } let mark = Date.now(), diff --git a/src/kiri/app/platform.js b/src/kiri/app/platform.js index 1fd78121..080f0440 100644 --- a/src/kiri/app/platform.js +++ b/src/kiri/app/platform.js @@ -3,7 +3,8 @@ import { $, h } from '../../moto/webui.js'; import { ajax, js2o } from '../core/utils.js'; import { api } from './api.js'; -import { COLOR, MODES } from '../core/consts.js'; +import { MODES } from '../core/consts.js'; +import { colorSchemeRegistry } from './color/schemes.js'; import { load as file_load } from '../../load/file.js'; import { newBounds } from '../../geo/bounds.js'; import { Packer } from './pack.js'; @@ -13,6 +14,15 @@ import { Widget, newWidget } from './widget.js'; const V0 = new THREE.Vector3(0,0,0); +/** + * Get current color scheme + */ +function getColorScheme() { + const mode = api.mode.get_id(); + const theme = api.space.is_dark() ? 'dark' : 'light'; + return colorSchemeRegistry.getScheme(mode, theme); +} + /** Bounds update callback */ let setbounds = undefined; @@ -154,17 +164,18 @@ function update_size(updateDark = true) { gridMinor = unitMM ? 5 : 25.4 / 10; if (updateDark) { + const scheme = getColorScheme(); if (controller.dark) { space.platform.set({ light: 0.08 }); space.platform.setFont({rulerColor:'#888888'}); - space.platform.setGrid(gridMajor, gridMinor, 0x666666, 0x333333); + space.platform.setGrid(gridMajor, gridMinor, scheme.grid.major, scheme.grid.minor); space.platform.opacity(0.05); space.sky.set({ color: 0, ambient: { intensity: 0.6 } }); document.body.classList.add('dark'); } else { space.platform.set({ light: 0.08 }); space.platform.setFont({rulerColor:'#333333'}); - space.platform.setGrid(gridMajor, gridMinor, 0x999999, 0xcccccc); + space.platform.setGrid(gridMajor, gridMinor, scheme.grid.major, scheme.grid.minor); space.platform.opacity(0.2); space.sky.set({ color: 0xffffff, ambient: { intensity: 1.1 } }); document.body.classList.remove('dark'); @@ -319,16 +330,18 @@ function update_selected() { let b = $(`sel-ext-${i}`); if (b) b.classList.remove('pop-sel'); } + const scheme = getColorScheme(); selection.for_widgets(w => { - w.setColor(COLOR.selected); + w.setColor(scheme.widget.selected); let ext = api.widgets.annotate(w.id).extruder || 0; let b = $(`sel-ext-${ext}`); if (b) b.classList.add('pop-sel'); w.saveState(); }, true); } else { + const scheme = getColorScheme(); selection.for_widgets(w => { - w.setColor(COLOR.selected); + w.setColor(scheme.widget.selected); }, true); } } @@ -374,7 +387,8 @@ function select(widget, shift, recurse = true) { } selection.add(widget); event.emit('widget.select', widget); - widget.setColor(COLOR.selected); + const scheme = getColorScheme(); + widget.setColor(scheme.widget.selected); $(`ws-${widget.id}`)?.classList.add('selected'); selection.update_info(); } @@ -420,7 +434,8 @@ function deselect(widget, recurse = true) { } $(`ws-${widget.id}`)?.classList.remove('selected'); - widget.setColor(COLOR.deselected); + const scheme = getColorScheme(); + widget.setColor(scheme.widget.deselected); platform.update_selected(); selection.update_info(); space.update(); @@ -753,8 +768,9 @@ function changed() { onclick() { let dis = w.meta.disabled = !w.meta.disabled; let sel = api.selection.contains(w); + const scheme = getColorScheme(); api.uc.setClass($(`w-en-${w.id}`), "disabled", dis); - w.setColor(sel ? COLOR.selected : COLOR.deselected, null, false); + w.setColor(sel ? scheme.widget.selected : scheme.widget.deselected, null, false); } }, [ h.i({ class:"fas fa-ban" }) ] diff --git a/src/kiri/app/state/visual.js b/src/kiri/app/state/visual.js new file mode 100644 index 00000000..81807d0e --- /dev/null +++ b/src/kiri/app/state/visual.js @@ -0,0 +1,215 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +/** + * VisualStateManager: Manages 3-layer visual state hierarchy for widgets + * + * Layer hierarchy (bottom to top): + * 1. Base Layer: User persistent preferences (edges, wireframe, opacity) + * 2. Mode Layer: Mode+view specific defaults (FDM/ARRANGE vs CAM/PREVIEW) + * 3. Operation Layer: Temporary context overrides (paint, selection) + * + * Context types: + * - 'paint': OVERRIDE context (FDM support painting, completely replaces material) + * - 'select': TEMPORARY context (selection highlighting) + * - null: Normal state changes following hierarchy + */ +class VisualStateManager { + constructor(widget, scheme, api) { + this.widget = widget; + this.scheme = scheme; + this.api = api; + + // State stack (base → mode → operation) + this.stack = []; + + // Active operation states + this.operations = {}; // { context: state } + } + + /** + * Load user preferences from localStorage (base layer) + */ + loadPreferences() { + const edges = this.api.local.getBoolean('model.edges'); + const wireframe = this.api.local.getBoolean('model.wireframe'); + const wireframeColor = this.api.local.getInt('model.wireframe.color') || 0; + const wireframeOpacity = this.api.local.getFloat('model.wireframe.opacity'); + + this.baseState = { + edges: edges || false, + wireframe: wireframe || false, + wireframeColor: wireframeColor, + wireframeOpacity: wireframeOpacity !== null ? wireframeOpacity : this.scheme.wireframe.opacity, + opacity: 1.0 + }; + + return this.baseState; + } + + /** + * Save user preferences to localStorage + */ + savePreferences() { + if (!this.baseState) return; + + this.api.local.set('model.edges', this.baseState.edges); + this.api.local.set('model.wireframe', this.baseState.wireframe); + if (this.baseState.wireframeColor !== undefined) { + this.api.local.set('model.wireframe.color', this.baseState.wireframeColor); + } + if (this.baseState.wireframeOpacity !== undefined) { + this.api.local.set('model.wireframe.opacity', this.baseState.wireframeOpacity); + } + } + + /** + * Apply mode-specific defaults (mode layer) + * @param {number} mode - Mode ID + * @param {number} viewMode - View mode ID (ARRANGE=1, SLICE=2, PREVIEW=3, ANIMATE=4) + */ + applyModeState(mode, viewMode) { + const viewName = this._viewModeToName(viewMode); + const viewScheme = this.scheme.views[viewName]; + + this.modeState = { + mode, + viewMode, + opacity: viewScheme?.model_opacity ?? 1.0, + previewOpacity: viewScheme?.preview_opacity, + slicingOpacity: viewScheme?.slicing_opacity, + slicedOpacity: viewScheme?.sliced_opacity + }; + + return this.modeState; + } + + /** + * Push a temporary operation state (operation layer) + * @param {string} context - Operation context ('paint', 'select', etc) + * @param {object} state - State to push + * @param {function} state.restoreCallback - Optional callback to restore state on pop + */ + pushState(context, state) { + if (!context) { + console.warn('pushState called without context'); + return; + } + + this.operations[context] = state; + this.stack.push({ layer: 'operation', context, state }); + + return state; + } + + /** + * Pop a temporary operation state + * @param {string} context - Operation context to pop + */ + popState(context) { + if (!context) { + console.warn('popState called without context'); + return; + } + + const operation = this.operations[context]; + if (!operation) { + console.warn(`No operation found for context: ${context}`); + return; + } + + // Call restore callback if provided + if (operation.restoreCallback) { + operation.restoreCallback(); + } + + // Remove from operations and stack + delete this.operations[context]; + this.stack = this.stack.filter(s => s.context !== context); + } + + /** + * Get the current effective state (topmost in hierarchy) + */ + getCurrentState() { + if (this.stack.length > 0) { + return this.stack[this.stack.length - 1].state; + } + if (this.modeState) { + return this.modeState; + } + return this.baseState; + } + + /** + * Check if an operation context is currently active + * @param {string} context - Operation context to check + * @returns {boolean} + */ + isOperationActive(context) { + return context in this.operations; + } + + /** + * Get all active operation contexts + * @returns {Array} + */ + getActiveContexts() { + return Object.keys(this.operations); + } + + /** + * Clear all operation states (e.g., on mode switch) + */ + clearOperations() { + // Call restore callbacks for all active operations + for (const context in this.operations) { + const operation = this.operations[context]; + if (operation.restoreCallback) { + operation.restoreCallback(); + } + } + + this.operations = {}; + this.stack = this.stack.filter(s => s.layer !== 'operation'); + } + + /** + * Get visual state snapshot for undo/redo + */ + getVisualState() { + return { + edges: this.widget.outline ? true : false, + wires: this.widget.wire ? true : false, + opacity: this.widget.getMaterial()?.opacity ?? 1.0 + }; + } + + /** + * Restore visual state from snapshot + * @param {object} state - State snapshot + */ + setVisualState({ edges, wires, opacity }) { + this.widget.cache.vizstate = this.getVisualState(); + this.widget.setEdges(edges ?? false); + this.widget.setWireframe(wires ?? false); + if (opacity !== undefined) { + this.widget.setOpacity(opacity); + } + } + + /** + * Convert view mode ID to name + * @private + */ + _viewModeToName(viewMode) { + const views = { + 1: 'ARRANGE', + 2: 'SLICE', + 3: 'PREVIEW', + 4: 'ANIMATE' + }; + return views[viewMode] || 'ARRANGE'; + } +} + +export { VisualStateManager }; diff --git a/src/kiri/app/visuals.js b/src/kiri/app/visuals.js index 5fe7d357..9334c306 100644 --- a/src/kiri/app/visuals.js +++ b/src/kiri/app/visuals.js @@ -3,12 +3,22 @@ import { api } from './api.js'; import { base } from '../../geo/base.js'; import { consts } from '../core/consts.js'; +import { colorSchemeRegistry } from './color/schemes.js'; import { settings } from './conf/manager.js'; import { space } from '../../moto/space.js'; import STACKS from './stacks.js'; -const { COLOR, VIEWS } = consts; +const { VIEWS } = consts; + +/** + * Get current color scheme + */ +function getColorScheme() { + const mode = api.mode.get_id(); + const theme = api.space.is_dark() ? 'dark' : 'light'; + return colorSchemeRegistry.getScheme(mode, theme); +} /** * Constrain value to min/max bounds. @@ -182,7 +192,8 @@ function hideSlices() { const wireframe = api.local.getBoolean('model.wireframe'); const color = api.local.getInt('model.wireframe.color') || 0; const saved = api.local.getFloat('model.wireframe.opacity'); - const opacity = saved !== null ? saved : COLOR.model_opacity; + const scheme = getColorScheme(); + const opacity = saved !== null ? saved : (scheme.views.ARRANGE?.model_opacity ?? 1.0); if (wireframe) { api.widgets.each(widget => widget.setWireframe(wireframe, color, opacity)); diff --git a/src/kiri/app/widget.js b/src/kiri/app/widget.js index 9aae365f..a6ae0a8a 100644 --- a/src/kiri/app/widget.js +++ b/src/kiri/app/widget.js @@ -3,6 +3,8 @@ import { Widget as WidgetCore } from '../core/widget.js'; import { util } from '../../geo/base.js'; import { avgc } from '../core/utils.js'; +import { colorSchemeRegistry } from './color/schemes.js'; +import { VisualStateManager } from './state/visual.js'; const solid_opacity = 1.0; @@ -14,6 +16,59 @@ function index() { return self.kiri_catalog.index } * Adds rendering, persistence, and visual state management */ class Widget extends WidgetCore { + constructor(id, group) { + super(id, group); + // Visual state manager will be initialized when api is available + this.visualStateManager = null; + } + + /** + * Initialize visual state manager (called when api is available) + * @private + */ + _initVisualStateManager() { + if (this.visualStateManager || !this.api) { + return; + } + + const mode = this.api.mode.get_id(); + const theme = this.api.space.is_dark() ? 'dark' : 'light'; + const scheme = colorSchemeRegistry.getScheme(mode, theme); + + this.visualStateManager = new VisualStateManager(this, scheme, this.api); + this.visualStateManager.loadPreferences(); + } + + /** + * Get current color scheme + * @private + */ + _getScheme() { + if (!this.api) return null; + const mode = this.api.mode.get_id(); + const theme = this.api.space.is_dark() ? 'dark' : 'light'; + return colorSchemeRegistry.getScheme(mode, theme); + } + + /** + * Push a temporary visual state (for operations like paint) + */ + pushVisualState(context, state) { + this._initVisualStateManager(); + if (this.visualStateManager) { + return this.visualStateManager.pushState(context, state); + } + } + + /** + * Pop a temporary visual state + */ + popVisualState(context) { + if (this.visualStateManager) { + this.visualStateManager.popState(context); + } + } + /** * Save widget geometry to catalog */ @@ -144,25 +199,57 @@ class Widget extends WidgetCore { // missing api features in engine mode return; } - let mesh = this.mesh; + const mesh = this.mesh; + + // Handle toggle if (set && set.toggle) { set = this.outline ? false : true; } + + // Remove outline (but keep cache) if (this.outline) { mesh.remove(this.outline); this.outline = null; } - if (set) { - let dark = this.api.space.is_dark(); - let cam = this.api.mode.is_cam(); - let color = dark ? 0x444444 : 0x888888; - let angle = this.api.conf.get().controller.edgeangle || 20; - let edges = new THREE.EdgesGeometry(mesh.geometry, angle); - let material = new THREE.LineBasicMaterial({ color }); - this.outline = new THREE.LineSegments(edges, material); - this.outline.renderOrder = -20; - mesh.add(this.outline); + + if (!set) { + return; // Just hide, keep cache } + + // Resolve current angle and color + const scheme = this._getScheme(); + const angle = scheme?.edges?.angle ?? this.api.conf.get().controller.edgeangle ?? 20; + const color = scheme?.edges?.color ?? 0x888888; + + // Initialize cache if needed + if (!this.cache.edges) { + this.cache.edges = { geometry: null, angle: null, version: 0 }; + } + + const cache = this.cache.edges; + let edgesGeometry; + + // Check if we can reuse cached geometry + if (cache.geometry && + cache.angle === angle && + cache.version === this.geomVersion) { + // Reuse cached geometry + edgesGeometry = cache.geometry; + } else { + // Regenerate edges + edgesGeometry = new THREE.EdgesGeometry(mesh.geometry, angle); + + // Update cache + cache.geometry = edgesGeometry; + cache.angle = angle; + cache.version = this.geomVersion; + } + + // Create outline with cached/new geometry + const material = new THREE.LineBasicMaterial({ color }); + this.outline = new THREE.LineSegments(edgesGeometry, material); + this.outline.renderOrder = -20; + mesh.add(this.outline); } /** @@ -181,14 +268,16 @@ class Widget extends WidgetCore { this.wire = null; } if (set) { - let dark = this.api.space.is_dark(); - let mat = new THREE.MeshBasicMaterial({ + const scheme = this._getScheme(); + const wireColor = scheme?.wireframe?.color ?? 0xaaaaaa; + const wireOpacity = scheme?.wireframe?.opacity ?? 0.5; + const mat = new THREE.MeshBasicMaterial({ wireframe: true, - color: dark ? 0xaaaaaa : 0, - opacity: 0.5, + color: wireColor, + opacity: wireOpacity, transparent: true - }) - let wire = widget.wire = new THREE.Mesh(mesh.geometry.shallowClone(), mat); + }); + const wire = widget.wire = new THREE.Mesh(mesh.geometry.shallowClone(), mat); mesh.add(wire); } if (this.api.view.is_arrange()) { diff --git a/src/kiri/core/consts.js b/src/kiri/core/consts.js index a6491f68..ac3caaff 100644 --- a/src/kiri/core/consts.js +++ b/src/kiri/core/consts.js @@ -1,21 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -const COLOR = { - wireframe: 0x444444, - wireframe_opacity: 0.25, - selected: [ 0xbbff00, 0xbbee00, 0xbbdd00, 0xbb9900 ], - deselected: [ 0xffff00, 0xffdd00, 0xffbb00, 0xff9900 ], - model_opacity: 1.0, - preview_opacity: 0.0, - slicing: 0xffaaaa, - slicing_opacity: 0.5, - sliced_opacity: 0.0, - cam_sliced_opacity: 0.2, - cam_preview: 0xdddddd, - cam_preview_opacity: 0.2, - cam_preview_dark: 0x888888, - cam_preview_opacity_dark: 0.2, -}; +// COLOR table has been migrated to src/kiri/app/color/ scheme system const LISTS = { shell: [ @@ -185,7 +170,6 @@ const LAST = 'kiri-last'; const SEED = 'kiri-seed'; export let consts = { - COLOR, LAST, LISTS, MODES, @@ -196,4 +180,4 @@ export let consts = { beltfact: Math.cos(Math.PI / 4) }; -export { COLOR, LAST, LISTS, MODES, VIEWS, PATHS, PMODES, SEED }; +export { LAST, LISTS, MODES, VIEWS, PATHS, PMODES, SEED }; diff --git a/src/kiri/core/widget.js b/src/kiri/core/widget.js index 5d11b2df..26721942 100644 --- a/src/kiri/core/widget.js +++ b/src/kiri/core/widget.js @@ -49,6 +49,8 @@ class Widget { this.boundingBoxNeedsUpdate = true // cache shadow geo this.cache = {}; + // geometry version for edge cache invalidation + this.geomVersion = 0; this.stats = { slice_time: 0, load_time: 0, @@ -193,6 +195,7 @@ class Widget { this.clearShadows(); if (reason !== 'axis') { this.cache.geo = undefined; + this.geomVersion++; // Increment version to invalidate edge cache } if (this.mesh && this.mesh.geometry) { // this fixes ray intersections after the mesh is modified diff --git a/src/kiri/mode/cam/cl-tab.js b/src/kiri/mode/cam/cl-tab.js index eecbc623..ae86f4b2 100644 --- a/src/kiri/mode/cam/cl-tab.js +++ b/src/kiri/mode/cam/cl-tab.js @@ -1,6 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { api } from '../../app/api.js'; +import { colorSchemeRegistry } from '../../app/color/schemes.js'; import { env, isDark } from './init-ui.js'; import { addbox, delbox, clearboxes } from '../../core/boxes.js'; import { traceDone } from './cl-trace.js'; @@ -225,9 +226,15 @@ export function tabClear() { } function boxColor() { - return isDark() ? 0x00ddff : 0x0000dd; + const mode = 3; // CAM mode ID + const theme = isDark() ? 'dark' : 'light'; + const scheme = colorSchemeRegistry.getScheme(mode, theme); + return scheme.operations?.tabs?.color ?? (isDark() ? 0x00ddff : 0x0000dd); } function boxOpacity() { - return isDark() ? 0.75 : 0.6; + const mode = 3; // CAM mode ID + const theme = isDark() ? 'dark' : 'light'; + const scheme = colorSchemeRegistry.getScheme(mode, theme); + return scheme.operations?.tabs?.opacity ?? (isDark() ? 0.75 : 0.6); } diff --git a/src/kiri/mode/fdm/init-ui.js b/src/kiri/mode/fdm/init-ui.js index 0625fc18..b7255bd9 100644 --- a/src/kiri/mode/fdm/init-ui.js +++ b/src/kiri/mode/fdm/init-ui.js @@ -3,6 +3,7 @@ import { newPoint } from '../../../geo/point.js'; import { $ } from '../../../moto/webui.js'; import { api } from '../../app/api.js'; +import { colorSchemeRegistry } from '../../app/color/schemes.js'; import { conf } from '../../app/conf/defaults.js'; import { addPaintOverlayTexture, updatePaintOverlay } from '../../app/paint.js'; @@ -271,14 +272,29 @@ function supportStart({ remove } = { remove: false }) { return [ down.widget.mesh ]; // return api.widgets.meshes(); }; + // Get paint color from scheme + const mode = api.mode.get_id(); + const theme = api.space.is_dark() ? 'dark' : 'light'; + const scheme = colorSchemeRegistry.getScheme(mode, theme); + const paintColor = scheme.operations?.paint?.overlay ?? 0x4488ff; + api.widgets.each(w => { + // Use pushVisualState to track the paint operation + w.pushVisualState('paint', { + material: w.mesh.material[0], + restoreCallback: () => { + w.mesh.material[0] = w.cache.mat; + delete w.cache.mat; + } + }); + let mat = w.cache.mat = w.mesh.material[0]; mat = w.mesh.material[0] = mat.clone(); mat.needsUpdate = true; if (!w.anno.paint) { w.anno.paint = []; } - addPaintOverlayTexture(mat, w.anno.paint, new THREE.Color(0x4488ff)); + addPaintOverlayTexture(mat, w.anno.paint, new THREE.Color(paintColor)); }) api.space.update(); } @@ -321,8 +337,8 @@ function supportDone() { api.feature.on_mouse_drag = undefined; api.hide.alert(alert); api.widgets.each(w => { - w.mesh.material[0] = w.cache.mat; - delete w.cache.mat; + // Use popVisualState to restore original material + w.popVisualState('paint'); }); } From 87eadfd2b5107d6ceab0d83877b4cee13f650a8f Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 8 Jan 2026 14:49:03 -0500 Subject: [PATCH 129/154] move consts from core to app --- src/kiri/app/api.js | 2 +- src/kiri/app/conf/manager.js | 2 +- src/kiri/{core => app}/consts.js | 8 +------- src/kiri/app/export.js | 2 +- src/kiri/app/frame.js | 2 +- src/kiri/app/function.js | 2 +- src/kiri/app/init/input.js | 2 +- src/kiri/app/init/sync.js | 2 +- src/kiri/app/mode.js | 2 +- src/kiri/app/platform.js | 2 +- src/kiri/app/view-mode.js | 2 +- src/kiri/app/visuals.js | 2 +- src/kiri/app/workers.js | 4 +++- src/kiri/core/print.js | 8 +++----- src/kiri/mode/cam/tools.js | 2 +- src/kiri/run/worker.js | 4 ++-- 16 files changed, 21 insertions(+), 27 deletions(-) rename src/kiri/{core => app}/consts.js (96%) diff --git a/src/kiri/app/api.js b/src/kiri/app/api.js index d37a80e0..ccb80965 100644 --- a/src/kiri/app/api.js +++ b/src/kiri/app/api.js @@ -10,7 +10,7 @@ import web from '../../moto/webui.js'; import { beta, version } from '../../moto/license.js'; import { broker } from '../../moto/broker.js'; import { client as workers } from './workers.js'; -import { consts, LISTS as lists } from '../core/consts.js'; +import { consts, LISTS as lists } from './consts.js'; import { device, devices } from './devices.js'; import { functions } from './function.js'; import { group as groupModule } from './groups.js'; diff --git a/src/kiri/app/conf/manager.js b/src/kiri/app/conf/manager.js index f1d9ebac..c15f57ec 100644 --- a/src/kiri/app/conf/manager.js +++ b/src/kiri/app/conf/manager.js @@ -4,7 +4,7 @@ import { $, h } from '../../../moto/webui.js'; import { api } from '../api.js'; import { conf } from './defaults.js'; import { codec } from '../../core/codec.js'; -import { consts } from '../../core/consts.js'; +import { consts } from '../consts.js'; import { space } from '../../../moto/space.js'; import { local } from '../../../data/local.js'; import { newWidget } from '../widget.js'; diff --git a/src/kiri/core/consts.js b/src/kiri/app/consts.js similarity index 96% rename from src/kiri/core/consts.js rename to src/kiri/app/consts.js index ac3caaff..ac59923b 100644 --- a/src/kiri/core/consts.js +++ b/src/kiri/app/consts.js @@ -161,11 +161,6 @@ const PMODES = { TOOLS: 2 }; -const PATHS = { - work: "../lib/kiri/run/worker.js", - pool: "./minion.js" -}; - const LAST = 'kiri-last'; const SEED = 'kiri-seed'; @@ -173,11 +168,10 @@ export let consts = { LAST, LISTS, MODES, - PATHS, PMODES, VIEWS, SEED, beltfact: Math.cos(Math.PI / 4) }; -export { LAST, LISTS, MODES, VIEWS, PATHS, PMODES, SEED }; +export { LAST, LISTS, MODES, VIEWS, PMODES, SEED }; diff --git a/src/kiri/app/export.js b/src/kiri/app/export.js index 8e682a35..05831716 100644 --- a/src/kiri/app/export.js +++ b/src/kiri/app/export.js @@ -5,7 +5,7 @@ import { api } from './api.js'; import { client } from './workers.js'; import { local } from '../../data/local.js'; import { util } from '../../geo/base.js'; -import { MODES } from '../core/consts.js'; +import { MODES } from './consts.js'; import { LASER as laser_driver } from '../mode/laser/driver.js'; import { SLA as sla_client } from '../mode/sla/init-ui.js'; import { hash } from '../../ext/md5.js'; diff --git a/src/kiri/app/frame.js b/src/kiri/app/frame.js index c1232b1a..32fd2431 100644 --- a/src/kiri/app/frame.js +++ b/src/kiri/app/frame.js @@ -26,7 +26,7 @@ import { api } from './api.js'; import { load } from '../../load/file.js'; import { newWidget } from './widget.js'; -import { VIEWS } from '../core/consts.js'; +import { VIEWS } from './consts.js'; import { widgets } from '../core/widgets.js'; /** diff --git a/src/kiri/app/function.js b/src/kiri/app/function.js index e3832c80..ac3c4904 100644 --- a/src/kiri/app/function.js +++ b/src/kiri/app/function.js @@ -4,7 +4,7 @@ import { api } from './api.js'; import { client } from './workers.js'; import { codec } from '../core/codec.js'; import { space } from '../../moto/space.js'; -import { PMODES } from '../core/consts.js'; +import { PMODES } from './consts.js'; import { colorSchemeRegistry } from './color/schemes.js'; import { exportFile } from './export.js'; diff --git a/src/kiri/app/init/input.js b/src/kiri/app/init/input.js index 6b4c881a..6909ce40 100644 --- a/src/kiri/app/init/input.js +++ b/src/kiri/app/init/input.js @@ -25,7 +25,7 @@ import { settings as set_ctrl } from '../conf/manager.js'; import { settingsOps } from '../conf/settings.js'; import { slider } from '../slider.js'; import { space } from '../../../moto/space.js'; -import { LAST, MODES, SEED, VIEWS } from '../../core/consts.js'; +import { LAST, MODES, SEED, VIEWS } from '../consts.js'; import STACKS from '../stacks.js'; import '../frame.js'; diff --git a/src/kiri/app/init/sync.js b/src/kiri/app/init/sync.js index 2d34f670..b507c121 100644 --- a/src/kiri/app/init/sync.js +++ b/src/kiri/app/init/sync.js @@ -9,7 +9,7 @@ import { preferences } from '../preferences.js'; import { settings as set_ctrl } from '../conf/manager.js'; import { settingsOps } from '../conf/settings.js'; import { space } from '../../../moto/space.js'; -import { VIEWS } from '../../core/consts.js'; +import { VIEWS } from '../consts.js'; import * as view_tools from '../face-tool.js'; const { SETUP, LOCAL } = api.const; diff --git a/src/kiri/app/mode.js b/src/kiri/app/mode.js index 22caa361..7f992d9d 100644 --- a/src/kiri/app/mode.js +++ b/src/kiri/app/mode.js @@ -2,7 +2,7 @@ import { $ } from '../../moto/webui.js'; import { api } from './api.js'; -import { consts } from '../core/consts.js'; +import { consts } from './consts.js'; import { modal } from './modal.js'; import { platform } from './platform.js'; import { selection } from './selected.js'; diff --git a/src/kiri/app/platform.js b/src/kiri/app/platform.js index 080f0440..a1529fb6 100644 --- a/src/kiri/app/platform.js +++ b/src/kiri/app/platform.js @@ -3,7 +3,7 @@ import { $, h } from '../../moto/webui.js'; import { ajax, js2o } from '../core/utils.js'; import { api } from './api.js'; -import { MODES } from '../core/consts.js'; +import { MODES } from './consts.js'; import { colorSchemeRegistry } from './color/schemes.js'; import { load as file_load } from '../../load/file.js'; import { newBounds } from '../../geo/bounds.js'; diff --git a/src/kiri/app/view-mode.js b/src/kiri/app/view-mode.js index 3cb3adc6..dac5e95d 100644 --- a/src/kiri/app/view-mode.js +++ b/src/kiri/app/view-mode.js @@ -2,7 +2,7 @@ import { $ } from '../../moto/webui.js'; import { api } from './api.js'; -import { consts } from '../core/consts.js'; +import { consts } from './consts.js'; import { platform } from './platform.js'; import { selection } from './selected.js'; import { settings } from './conf/manager.js'; diff --git a/src/kiri/app/visuals.js b/src/kiri/app/visuals.js index 9334c306..b34e8c41 100644 --- a/src/kiri/app/visuals.js +++ b/src/kiri/app/visuals.js @@ -2,7 +2,7 @@ import { api } from './api.js'; import { base } from '../../geo/base.js'; -import { consts } from '../core/consts.js'; +import { consts } from './consts.js'; import { colorSchemeRegistry } from './color/schemes.js'; import { settings } from './conf/manager.js'; import { space } from '../../moto/space.js'; diff --git a/src/kiri/app/workers.js b/src/kiri/app/workers.js index 808a654b..8f71b39d 100644 --- a/src/kiri/app/workers.js +++ b/src/kiri/app/workers.js @@ -6,6 +6,8 @@ import { codec } from '../core/codec.js'; import { Widget } from './widget.js'; import { newPrint } from '../core/print.js'; +const WORKPATH = "../lib/kiri/run/worker.js"; + // this code runs in kiri's main loop let debug = self.debug === true, time = Date.now, @@ -92,7 +94,7 @@ export const client = { const blob = new Blob([ work ], { type: 'application/javascript' }); return new Worker(URL.createObjectURL(blob)); } else { - let worker = new Worker(workpath || api.const.PATHS.work, { type: 'module' }); + let worker = new Worker(workpath || WORKPATH, { type: 'module' }); worker.onerror = (error) => { console.log({ WORKER_ERROR: error }); error.preventDefault(); diff --git a/src/kiri/core/print.js b/src/kiri/core/print.js index 3b19b106..d0243ead 100644 --- a/src/kiri/core/print.js +++ b/src/kiri/core/print.js @@ -1,16 +1,14 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { arcToPath } from '../../geo/paths.js'; -import { consts } from './consts.js'; import { createVM } from '../../moto/quickjs.js'; import { newPoint } from '../../geo/point.js'; import { newPolygon } from '../../geo/polygon.js'; import { util } from '../../geo/base.js'; const { numOrDefault } = util; -const { beltfact } = consts; -const XAXIS = new THREE.Vector3(1,0,0); +const BELTFACT = Math.cos(Math.PI / 4); const DEG2RAD = Math.PI / 180; class Print { @@ -590,8 +588,8 @@ class Print { let { point, prevPoint } = processLine(line, axes); if (morph && belt) { - point.y -= point.z * beltfact; - point.z *= beltfact; + point.y -= point.z * BELTFACT; + point.z *= BELTFACT; } const retract = (fdm && pos.E < 0) || undefined; diff --git a/src/kiri/mode/cam/tools.js b/src/kiri/mode/cam/tools.js index e8b533a8..8146973a 100644 --- a/src/kiri/mode/cam/tools.js +++ b/src/kiri/mode/cam/tools.js @@ -2,7 +2,7 @@ import { $ } from '../../../moto/webui.js'; import { api } from '../../app/api.js'; -import { consts } from '../../core/consts.js'; +import { consts } from '../../app/consts.js'; import { settings as setconf } from '../../app/conf/manager.js'; import { Tool, calcTaperAngle, calcTaperBallExtent, calcTaperLength } from './tool.js'; diff --git a/src/kiri/run/worker.js b/src/kiri/run/worker.js index 9f394ddb..cb812002 100644 --- a/src/kiri/run/worker.js +++ b/src/kiri/run/worker.js @@ -6,7 +6,6 @@ import '../../add/three.js'; import { base } from '../../geo/base.js'; import { codec } from '../core/codec.js'; -import { consts } from '../core/consts.js'; import { JSZip } from '../../ext/jszip-esm.js'; import { load } from "../../load/png.js"; import { newPoint } from '../../geo/point.js'; @@ -28,6 +27,7 @@ import { WEDM } from '../mode/wedm/driver.js'; import { WJET } from '../mode/wjet/driver.js'; const { time } = util; +const POOLPATH = "./minion.js"; let drivers = { DRAG, @@ -114,7 +114,7 @@ const minwork = { return; } for (let i=0; i < concurrent; i++) { - let minion = new Worker(poolpath || consts.PATHS.pool, { type: 'module' }); + let minion = new Worker(poolpath || POOLPATH, { type: 'module' }); minion.onerror = (error) => { debug({ MINION_ERROR: error }); error.preventDefault(); From 02e59b84e2d3bec89310b7a2998cf7e1745a579e Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 8 Jan 2026 15:33:58 -0500 Subject: [PATCH 130/154] move some assets from core to app --- src/kiri/app/api.js | 2 +- src/kiri/{core => app}/boxes.js | 0 src/kiri/app/frame.js | 2 +- src/kiri/{core => app}/widgets.js | 0 src/kiri/mode/cam/cl-origin.js | 2 +- src/kiri/mode/cam/cl-tab.js | 2 +- 6 files changed, 4 insertions(+), 4 deletions(-) rename src/kiri/{core => app}/boxes.js (100%) rename src/kiri/{core => app}/widgets.js (100%) diff --git a/src/kiri/app/api.js b/src/kiri/app/api.js index ccb80965..41f644e3 100644 --- a/src/kiri/app/api.js +++ b/src/kiri/app/api.js @@ -37,7 +37,7 @@ import { UI } from './inputs.js'; import { updateTool } from '../mode/cam/tools.js'; import { view as viewModule } from './view-mode.js'; import { visuals } from './visuals.js'; -import { widgets } from '../core/widgets.js'; +import { widgets } from './widgets.js'; import { workspace } from './workspace.js'; // environment setup diff --git a/src/kiri/core/boxes.js b/src/kiri/app/boxes.js similarity index 100% rename from src/kiri/core/boxes.js rename to src/kiri/app/boxes.js diff --git a/src/kiri/app/frame.js b/src/kiri/app/frame.js index 32fd2431..4cc9dedd 100644 --- a/src/kiri/app/frame.js +++ b/src/kiri/app/frame.js @@ -27,7 +27,7 @@ import { api } from './api.js'; import { load } from '../../load/file.js'; import { newWidget } from './widget.js'; import { VIEWS } from './consts.js'; -import { widgets } from '../core/widgets.js'; +import { widgets } from './widgets.js'; /** * Frame message API listener. diff --git a/src/kiri/core/widgets.js b/src/kiri/app/widgets.js similarity index 100% rename from src/kiri/core/widgets.js rename to src/kiri/app/widgets.js diff --git a/src/kiri/mode/cam/cl-origin.js b/src/kiri/mode/cam/cl-origin.js index 5255d084..ee6349f9 100644 --- a/src/kiri/mode/cam/cl-origin.js +++ b/src/kiri/mode/cam/cl-origin.js @@ -3,7 +3,7 @@ import { api } from '../../app/api.js'; import { env, clearPops } from './init-ui.js'; import { CAM } from './driver-fe.js'; -import { addbox, clearboxes } from '../../core/boxes.js'; +import { addbox, clearboxes } from '../../app/boxes.js'; export let originSelectOn = false; diff --git a/src/kiri/mode/cam/cl-tab.js b/src/kiri/mode/cam/cl-tab.js index ae86f4b2..6ee3c6df 100644 --- a/src/kiri/mode/cam/cl-tab.js +++ b/src/kiri/mode/cam/cl-tab.js @@ -3,7 +3,7 @@ import { api } from '../../app/api.js'; import { colorSchemeRegistry } from '../../app/color/schemes.js'; import { env, isDark } from './init-ui.js'; -import { addbox, delbox, clearboxes } from '../../core/boxes.js'; +import { addbox, delbox, clearboxes } from '../../app/boxes.js'; import { traceDone } from './cl-trace.js'; import { space as SPACE } from '../../../moto/space.js'; From 7f50bafe9e741ce83cda969e36912612129ecf46 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 8 Jan 2026 15:37:14 -0500 Subject: [PATCH 131/154] move default lang link from core to app --- src/kiri/{core => app}/lang-en.js | 0 src/main/kiri.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/kiri/{core => app}/lang-en.js (100%) diff --git a/src/kiri/core/lang-en.js b/src/kiri/app/lang-en.js similarity index 100% rename from src/kiri/core/lang-en.js rename to src/kiri/app/lang-en.js diff --git a/src/main/kiri.js b/src/main/kiri.js index ad798760..8e360c8d 100644 --- a/src/main/kiri.js +++ b/src/main/kiri.js @@ -4,7 +4,7 @@ import '../add/array.js'; import '../add/class.js'; import '../add/three.js'; import '../kiri/app/language.js'; -import '../kiri/core/lang-en.js'; +import '../kiri/app/lang-en.js'; import { api } from '../kiri/app/api.js'; import { init_lang } from '../kiri/app/init/lang.js'; From 67f405fd7ba7d9fdd0a81843197a065d6abd1e9c Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 8 Jan 2026 16:52:41 -0500 Subject: [PATCH 132/154] reorganize and rename driver files for consistency and clarity --- src/kiri/app/api.js | 4 +- src/kiri/app/export.js | 4 +- src/kiri/app/init/input.js | 12 +++--- .../cam/{anim-2d-fe.js => app/anim-2d.js} | 4 +- .../cam/{anim-3d-fe.js => app/anim-3d.js} | 4 +- src/kiri/mode/cam/{ => app}/cl-flip.js | 4 +- src/kiri/mode/cam/{ => app}/cl-helical.js | 4 +- src/kiri/mode/cam/{ => app}/cl-hole.js | 4 +- src/kiri/mode/cam/{ => app}/cl-ops.js | 8 ++-- src/kiri/mode/cam/{ => app}/cl-origin.js | 6 +-- src/kiri/mode/cam/{ => app}/cl-stock.js | 4 +- src/kiri/mode/cam/{ => app}/cl-surface.js | 4 +- src/kiri/mode/cam/{ => app}/cl-tab.js | 8 ++-- src/kiri/mode/cam/{ => app}/cl-trace.js | 12 +++--- src/kiri/mode/cam/{ => app}/cl-zplane.js | 2 +- .../mode/cam/{driver-fe.js => app/driver.js} | 4 +- src/kiri/mode/cam/{ => app}/init-menu.js | 2 +- src/kiri/mode/cam/{ => app}/init-ui.js | 20 ++++----- src/kiri/mode/cam/{ => app}/tools.js | 10 ++--- src/kiri/mode/cam/{ => core}/op.js | 0 src/kiri/mode/cam/core/ops.js | 43 +++++++++++++++++++ src/kiri/mode/cam/{ => core}/tool.js | 0 src/kiri/mode/cam/ops.js | 43 ------------------- .../cam/{anim-2d-be.js => work/anim-2d.js} | 2 +- .../cam/{anim-3d-be.js => work/anim-3d.js} | 4 +- src/kiri/mode/cam/{ => work}/export.js | 4 +- .../cam/{driver-be.js => work/init-work.js} | 8 ++-- src/kiri/mode/cam/{ => work}/op-area.js | 18 ++++---- src/kiri/mode/cam/{ => work}/op-contour.js | 12 +++--- src/kiri/mode/cam/{ => work}/op-drill.js | 10 ++--- src/kiri/mode/cam/{ => work}/op-gcode.js | 2 +- src/kiri/mode/cam/{ => work}/op-helical.js | 12 +++--- src/kiri/mode/cam/{ => work}/op-index.js | 2 +- src/kiri/mode/cam/{ => work}/op-laser-off.js | 2 +- src/kiri/mode/cam/{ => work}/op-laser-on.js | 2 +- src/kiri/mode/cam/{ => work}/op-lathe.js | 4 +- src/kiri/mode/cam/{ => work}/op-level.js | 12 +++--- src/kiri/mode/cam/{ => work}/op-loop.js | 2 +- src/kiri/mode/cam/{ => work}/op-outline.js | 2 +- src/kiri/mode/cam/{ => work}/op-pocket.js | 2 +- src/kiri/mode/cam/{ => work}/op-register.js | 12 +++--- src/kiri/mode/cam/{ => work}/op-rough.js | 6 +-- src/kiri/mode/cam/{ => work}/op-shadow.js | 6 +-- src/kiri/mode/cam/{ => work}/op-trace.js | 2 +- src/kiri/mode/cam/{ => work}/op-xray.js | 6 +-- src/kiri/mode/cam/{ => work}/prepare.js | 14 +++--- src/kiri/mode/cam/{ => work}/slice.js | 20 ++++----- .../cam/{slicer_cam.js => work/slicer-cam.js} | 14 +++--- .../{slicer_topo.js => work/slicer-topo.js} | 4 +- src/kiri/mode/cam/{ => work}/topo3.js | 14 +++--- src/kiri/mode/cam/{ => work}/topo4.js | 14 +++--- .../mode/drag/{driver.js => init-work.js} | 2 +- src/kiri/mode/fdm/{ => app}/init-menu.js | 2 +- src/kiri/mode/fdm/{ => app}/init-ui.js | 12 +++--- .../mode/fdm/{driver.js => core/params.js} | 0 src/kiri/mode/fdm/{ => work}/belt.js | 8 ++-- src/kiri/mode/fdm/{ => work}/export.js | 8 ++-- src/kiri/mode/fdm/{ => work}/fill.js | 4 +- .../fdm/{driver-be.js => work/init-work.js} | 4 +- src/kiri/mode/fdm/{ => work}/post.js | 16 +++---- src/kiri/mode/fdm/{ => work}/prepare.js | 22 +++++----- src/kiri/mode/fdm/{ => work}/slice.js | 14 +++--- .../mode/laser/{driver.js => init-work.js} | 0 src/kiri/mode/sla/{ => app}/init-menu.js | 2 +- src/kiri/mode/sla/{ => app}/init-ui.js | 4 +- src/kiri/mode/sla/{ => work}/export.js | 2 +- .../mode/sla/{driver.js => work/init-work.js} | 2 +- src/kiri/mode/sla/{ => work}/slice.js | 16 +++---- src/kiri/mode/sla/{ => work}/x_cxdlp.js | 0 src/kiri/mode/sla/{ => work}/x_photon.js | 2 +- .../mode/wedm/{driver.js => init-work.js} | 2 +- .../mode/wjet/{driver.js => init-work.js} | 2 +- src/kiri/run/minion.js | 10 ++--- src/kiri/run/worker.js | 14 +++--- 74 files changed, 283 insertions(+), 283 deletions(-) rename src/kiri/mode/cam/{anim-2d-fe.js => app/anim-2d.js} (98%) rename src/kiri/mode/cam/{anim-3d-fe.js => app/anim-3d.js} (99%) rename src/kiri/mode/cam/{ => app}/cl-flip.js (96%) rename src/kiri/mode/cam/{ => app}/cl-helical.js (96%) rename src/kiri/mode/cam/{ => app}/cl-hole.js (98%) rename src/kiri/mode/cam/{ => app}/cl-ops.js (99%) rename src/kiri/mode/cam/{ => app}/cl-origin.js (96%) rename src/kiri/mode/cam/{ => app}/cl-stock.js (97%) rename src/kiri/mode/cam/{ => app}/cl-surface.js (96%) rename src/kiri/mode/cam/{ => app}/cl-tab.js (96%) rename src/kiri/mode/cam/{ => app}/cl-trace.js (96%) rename src/kiri/mode/cam/{ => app}/cl-zplane.js (95%) rename src/kiri/mode/cam/{driver-fe.js => app/driver.js} (97%) rename src/kiri/mode/cam/{ => app}/init-menu.js (99%) rename src/kiri/mode/cam/{ => app}/init-ui.js (98%) rename src/kiri/mode/cam/{ => app}/tools.js (97%) rename src/kiri/mode/cam/{ => core}/op.js (100%) create mode 100644 src/kiri/mode/cam/core/ops.js rename src/kiri/mode/cam/{ => core}/tool.js (100%) delete mode 100644 src/kiri/mode/cam/ops.js rename src/kiri/mode/cam/{anim-2d-be.js => work/anim-2d.js} (99%) rename src/kiri/mode/cam/{anim-3d-be.js => work/anim-3d.js} (99%) rename src/kiri/mode/cam/{ => work}/export.js (99%) rename src/kiri/mode/cam/{driver-be.js => work/init-work.js} (94%) rename src/kiri/mode/cam/{ => work}/op-area.js (98%) rename src/kiri/mode/cam/{ => work}/op-contour.js (95%) rename src/kiri/mode/cam/{ => work}/op-drill.js (89%) rename src/kiri/mode/cam/{ => work}/op-gcode.js (86%) rename src/kiri/mode/cam/{ => work}/op-helical.js (97%) rename src/kiri/mode/cam/{ => work}/op-index.js (97%) rename src/kiri/mode/cam/{ => work}/op-laser-off.js (91%) rename src/kiri/mode/cam/{ => work}/op-laser-on.js (91%) rename src/kiri/mode/cam/{ => work}/op-lathe.js (96%) rename src/kiri/mode/cam/{ => work}/op-level.js (90%) rename src/kiri/mode/cam/{ => work}/op-loop.js (89%) rename src/kiri/mode/cam/{ => work}/op-outline.js (98%) rename src/kiri/mode/cam/{ => work}/op-pocket.js (96%) rename src/kiri/mode/cam/{ => work}/op-register.js (95%) rename src/kiri/mode/cam/{ => work}/op-rough.js (96%) rename src/kiri/mode/cam/{ => work}/op-shadow.js (95%) rename src/kiri/mode/cam/{ => work}/op-trace.js (97%) rename src/kiri/mode/cam/{ => work}/op-xray.js (87%) rename src/kiri/mode/cam/{ => work}/prepare.js (98%) rename src/kiri/mode/cam/{ => work}/slice.js (98%) rename src/kiri/mode/cam/{slicer_cam.js => work/slicer-cam.js} (97%) rename src/kiri/mode/cam/{slicer_topo.js => work/slicer-topo.js} (98%) rename src/kiri/mode/cam/{ => work}/topo3.js (98%) rename src/kiri/mode/cam/{ => work}/topo4.js (98%) rename src/kiri/mode/drag/{driver.js => init-work.js} (99%) rename src/kiri/mode/fdm/{ => app}/init-menu.js (99%) rename src/kiri/mode/fdm/{ => app}/init-ui.js (97%) rename src/kiri/mode/fdm/{driver.js => core/params.js} (100%) rename src/kiri/mode/fdm/{ => work}/belt.js (98%) rename src/kiri/mode/fdm/{ => work}/export.js (99%) rename src/kiri/mode/fdm/{ => work}/fill.js (98%) rename src/kiri/mode/fdm/{driver-be.js => work/init-work.js} (93%) rename src/kiri/mode/fdm/{ => work}/post.js (98%) rename src/kiri/mode/fdm/{ => work}/prepare.js (99%) rename src/kiri/mode/fdm/{ => work}/slice.js (99%) rename src/kiri/mode/laser/{driver.js => init-work.js} (100%) rename src/kiri/mode/sla/{ => app}/init-menu.js (98%) rename src/kiri/mode/sla/{ => app}/init-ui.js (96%) rename src/kiri/mode/sla/{ => work}/export.js (99%) rename src/kiri/mode/sla/{driver.js => work/init-work.js} (96%) rename src/kiri/mode/sla/{ => work}/slice.js (98%) rename src/kiri/mode/sla/{ => work}/x_cxdlp.js (100%) rename src/kiri/mode/sla/{ => work}/x_photon.js (99%) rename src/kiri/mode/wedm/{driver.js => init-work.js} (89%) rename src/kiri/mode/wjet/{driver.js => init-work.js} (82%) diff --git a/src/kiri/app/api.js b/src/kiri/app/api.js index 41f644e3..9c687c4d 100644 --- a/src/kiri/app/api.js +++ b/src/kiri/app/api.js @@ -29,12 +29,12 @@ import { platform } from './platform.js'; import { selection } from './selected.js'; import { settingsUI } from './conf/dialog.js'; import { showDevices } from './devices.js'; -import { showTools } from '../mode/cam/tools.js'; +import { showTools } from '../mode/cam/app/tools.js'; import { space as SPACE } from '../../moto/space.js'; import { stats } from './stats.js'; import { types as load } from '../../load/file.js'; import { UI } from './inputs.js'; -import { updateTool } from '../mode/cam/tools.js'; +import { updateTool } from '../mode/cam/app/tools.js'; import { view as viewModule } from './view-mode.js'; import { visuals } from './visuals.js'; import { widgets } from './widgets.js'; diff --git a/src/kiri/app/export.js b/src/kiri/app/export.js index 05831716..999795ef 100644 --- a/src/kiri/app/export.js +++ b/src/kiri/app/export.js @@ -6,8 +6,8 @@ import { client } from './workers.js'; import { local } from '../../data/local.js'; import { util } from '../../geo/base.js'; import { MODES } from './consts.js'; -import { LASER as laser_driver } from '../mode/laser/driver.js'; -import { SLA as sla_client } from '../mode/sla/init-ui.js'; +import { LASER as laser_driver } from '../mode/laser/init-work.js'; +import { SLA as sla_client } from '../mode/sla/app/init-ui.js'; import { hash } from '../../ext/md5.js'; /** diff --git a/src/kiri/app/init/input.js b/src/kiri/app/init/input.js index 6909ce40..05eef309 100644 --- a/src/kiri/app/init/input.js +++ b/src/kiri/app/init/input.js @@ -5,20 +5,20 @@ import { api } from '../api.js'; import { bind } from './bind.js'; import { version } from '../../../moto/license.js'; import { fileOps } from '../file-ops.js'; -import { init as initCAM } from '../../mode/cam/init-ui.js'; +import { init as initCAM } from '../../mode/cam/app/init-ui.js'; import { init as initDRAG } from '../../mode/drag/init-ui.js'; -import { init as initFDM } from '../../mode/fdm/init-ui.js'; +import { init as initFDM } from '../../mode/fdm/app/init-ui.js'; import { init as initLaser } from '../../mode/laser/init-ui.js'; -import { init as initSLA } from '../../mode/sla/init-ui.js'; +import { init as initSLA } from '../../mode/sla/app/init-ui.js'; import { init as initWEDM } from '../../mode/wedm/init-ui.js'; import { init as initWJET } from '../../mode/wjet/init-ui.js'; import { interact } from '../mouse.js'; import { keyboard } from '../keyboard.js'; import { local as sdb } from '../../../data/local.js'; -import { menu as menuCAM } from '../../mode/cam/init-menu.js'; -import { menu as menuFDM } from '../../mode/fdm/init-menu.js'; +import { menu as menuCAM } from '../../mode/cam/app/init-menu.js'; +import { menu as menuFDM } from '../../mode/fdm/app/init-menu.js'; import { menu as menuLaser } from '../../mode/laser/init-menu.js'; -import { menu as menuSLA } from '../../mode/sla/init-menu.js'; +import { menu as menuSLA } from '../../mode/sla/app/init-menu.js'; import { modal } from '../modal.js'; import { preferences } from '../preferences.js'; import { settings as set_ctrl } from '../conf/manager.js'; diff --git a/src/kiri/mode/cam/anim-2d-fe.js b/src/kiri/mode/cam/app/anim-2d.js similarity index 98% rename from src/kiri/mode/cam/anim-2d-fe.js rename to src/kiri/mode/cam/app/anim-2d.js index 98286cc0..5d3312e5 100644 --- a/src/kiri/mode/cam/anim-2d-fe.js +++ b/src/kiri/mode/cam/app/anim-2d.js @@ -2,8 +2,8 @@ // UI FRONT END ANIMATION CODE for 2D -import { api } from '../../app/api.js'; -import { space } from '../../../moto/space.js'; +import { api } from '../../../app/api.js'; +import { space } from '../../../../moto/space.js'; const { client } = api; diff --git a/src/kiri/mode/cam/anim-3d-fe.js b/src/kiri/mode/cam/app/anim-3d.js similarity index 99% rename from src/kiri/mode/cam/anim-3d-fe.js rename to src/kiri/mode/cam/app/anim-3d.js index 478ed13e..8e26a1ac 100644 --- a/src/kiri/mode/cam/anim-3d-fe.js +++ b/src/kiri/mode/cam/app/anim-3d.js @@ -2,8 +2,8 @@ // UI FRONT END ANIMATION CODE for 3D (indexed) -import { api } from '../../app/api.js'; -import { space } from '../../../moto/space.js'; +import { api } from '../../../app/api.js'; +import { space } from '../../../../moto/space.js'; const { client } = api; diff --git a/src/kiri/mode/cam/cl-flip.js b/src/kiri/mode/cam/app/cl-flip.js similarity index 96% rename from src/kiri/mode/cam/cl-flip.js rename to src/kiri/mode/cam/app/cl-flip.js index cf3097f1..40dd842e 100644 --- a/src/kiri/mode/cam/cl-flip.js +++ b/src/kiri/mode/cam/app/cl-flip.js @@ -1,8 +1,8 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../app/api.js'; +import { api } from '../../../app/api.js'; import { env, opAdd, opRender } from './init-ui.js'; -import { CAM } from './driver-fe.js'; +import { CAM } from './driver.js'; import { clearTabs, restoreTabs } from './cl-tab.js'; import { updateStock } from './cl-stock.js'; diff --git a/src/kiri/mode/cam/cl-helical.js b/src/kiri/mode/cam/app/cl-helical.js similarity index 96% rename from src/kiri/mode/cam/cl-helical.js rename to src/kiri/mode/cam/app/cl-helical.js index 294f38e9..debbab7f 100644 --- a/src/kiri/mode/cam/cl-helical.js +++ b/src/kiri/mode/cam/app/cl-helical.js @@ -1,5 +1,5 @@ -import { api } from '../../app/api.js'; -import { CAM } from "./driver-fe.js"; +import { api } from '../../../app/api.js'; +import { CAM } from "./driver.js"; import { clearPops, env } from "./init-ui.js"; let alert, lastWidget; diff --git a/src/kiri/mode/cam/cl-hole.js b/src/kiri/mode/cam/app/cl-hole.js similarity index 98% rename from src/kiri/mode/cam/cl-hole.js rename to src/kiri/mode/cam/app/cl-hole.js index cf20aece..7c1c9d1a 100644 --- a/src/kiri/mode/cam/cl-hole.js +++ b/src/kiri/mode/cam/app/cl-hole.js @@ -1,8 +1,8 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../app/api.js'; +import { api } from '../../../app/api.js'; import { env, clearPops } from './init-ui.js'; -import { CAM } from './driver-fe.js'; +import { CAM } from './driver.js'; export let holeSelOn = false; export let lastSelHoles; diff --git a/src/kiri/mode/cam/cl-ops.js b/src/kiri/mode/cam/app/cl-ops.js similarity index 99% rename from src/kiri/mode/cam/cl-ops.js rename to src/kiri/mode/cam/app/cl-ops.js index 713b5b00..9fe753cf 100644 --- a/src/kiri/mode/cam/cl-ops.js +++ b/src/kiri/mode/cam/app/cl-ops.js @@ -1,15 +1,15 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { $, h } from '../../../moto/webui.js'; -import { api } from '../../app/api.js'; +import { $, h } from '../../../../moto/webui.js'; +import { api } from '../../../app/api.js'; import { env, opRender } from './init-ui.js'; -import { Tool } from './tool.js'; +import { Tool } from '../core/tool.js'; import { opFlip } from './cl-flip.js'; import { selectHoles } from './cl-hole.js'; import { selectHelical } from './cl-helical.js'; import { surfaceAdd } from './cl-surface.js'; import { traceAdd, traceClear, traceLoad } from './cl-trace.js'; -import { startFaceUp } from '../../app/face-tool.js'; +import { startFaceUp } from '../../../app/face-tool.js'; import { showZBottom, showZTop, updateStock } from './cl-stock.js'; const { MODES } = api.const; diff --git a/src/kiri/mode/cam/cl-origin.js b/src/kiri/mode/cam/app/cl-origin.js similarity index 96% rename from src/kiri/mode/cam/cl-origin.js rename to src/kiri/mode/cam/app/cl-origin.js index ee6349f9..625bfef5 100644 --- a/src/kiri/mode/cam/cl-origin.js +++ b/src/kiri/mode/cam/app/cl-origin.js @@ -1,9 +1,9 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../app/api.js'; +import { api } from '../../../app/api.js'; import { env, clearPops } from './init-ui.js'; -import { CAM } from './driver-fe.js'; -import { addbox, clearboxes } from '../../app/boxes.js'; +import { CAM } from './driver.js'; +import { addbox, clearboxes } from '../../../app/boxes.js'; export let originSelectOn = false; diff --git a/src/kiri/mode/cam/cl-stock.js b/src/kiri/mode/cam/app/cl-stock.js similarity index 97% rename from src/kiri/mode/cam/cl-stock.js rename to src/kiri/mode/cam/app/cl-stock.js index 7f2aae0f..550fdfb4 100644 --- a/src/kiri/mode/cam/cl-stock.js +++ b/src/kiri/mode/cam/app/cl-stock.js @@ -1,8 +1,8 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../app/api.js'; +import { api } from '../../../app/api.js'; import { env, isDark } from './init-ui.js'; -import { space as SPACE } from '../../../moto/space.js'; +import { space as SPACE } from '../../../../moto/space.js'; const { ui: UI } = api; diff --git a/src/kiri/mode/cam/cl-surface.js b/src/kiri/mode/cam/app/cl-surface.js similarity index 96% rename from src/kiri/mode/cam/cl-surface.js rename to src/kiri/mode/cam/app/cl-surface.js index 4a56ef63..be831764 100644 --- a/src/kiri/mode/cam/cl-surface.js +++ b/src/kiri/mode/cam/app/cl-surface.js @@ -1,8 +1,8 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../app/api.js'; +import { api } from '../../../app/api.js'; import { env, clearPops } from './init-ui.js'; -import { CAM } from './driver-fe.js'; +import { CAM } from './driver.js'; const DEG2RAD = Math.PI / 180; const RAD2DEG = 180 / Math.PI; diff --git a/src/kiri/mode/cam/cl-tab.js b/src/kiri/mode/cam/app/cl-tab.js similarity index 96% rename from src/kiri/mode/cam/cl-tab.js rename to src/kiri/mode/cam/app/cl-tab.js index 6ee3c6df..3306436b 100644 --- a/src/kiri/mode/cam/cl-tab.js +++ b/src/kiri/mode/cam/app/cl-tab.js @@ -1,11 +1,11 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../app/api.js'; -import { colorSchemeRegistry } from '../../app/color/schemes.js'; +import { api } from '../../../app/api.js'; +import { colorSchemeRegistry } from '../../../app/color/schemes.js'; import { env, isDark } from './init-ui.js'; -import { addbox, delbox, clearboxes } from '../../app/boxes.js'; +import { addbox, delbox, clearboxes } from '../../../app/boxes.js'; import { traceDone } from './cl-trace.js'; -import { space as SPACE } from '../../../moto/space.js'; +import { space as SPACE } from '../../../../moto/space.js'; let showTab, lastTab, tab, iw, ic; diff --git a/src/kiri/mode/cam/cl-trace.js b/src/kiri/mode/cam/app/cl-trace.js similarity index 96% rename from src/kiri/mode/cam/cl-trace.js rename to src/kiri/mode/cam/app/cl-trace.js index 31203c63..7327bb71 100644 --- a/src/kiri/mode/cam/cl-trace.js +++ b/src/kiri/mode/cam/app/cl-trace.js @@ -1,12 +1,12 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../app/api.js'; +import { api } from '../../../app/api.js'; import { env, clearPops, isDark } from './init-ui.js'; -import { parse } from '../../../load/svg.js'; -import { CAM } from './driver-fe.js'; -import { Layers } from '../../app/layers.js'; -import { Stack } from '../../app/stack.js'; -import { newPolygon } from '../../../geo/polygon.js'; +import { parse } from '../../../../load/svg.js'; +import { CAM } from './driver.js'; +import { Layers } from '../../../app/layers.js'; +import { Stack } from '../../../app/stack.js'; +import { newPolygon } from '../../../../geo/polygon.js'; export let traceOn = false; export let lastTrace; diff --git a/src/kiri/mode/cam/cl-zplane.js b/src/kiri/mode/cam/app/cl-zplane.js similarity index 95% rename from src/kiri/mode/cam/cl-zplane.js rename to src/kiri/mode/cam/app/cl-zplane.js index 8dda0811..890403f9 100644 --- a/src/kiri/mode/cam/cl-zplane.js +++ b/src/kiri/mode/cam/app/cl-zplane.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../app/api.js'; +import { api } from '../../../app/api.js'; import { clearPops, hoverStart } from './init-ui.js'; import { showZTop, showZBottom, updateStock } from './cl-stock.js'; diff --git a/src/kiri/mode/cam/driver-fe.js b/src/kiri/mode/cam/app/driver.js similarity index 97% rename from src/kiri/mode/cam/driver-fe.js rename to src/kiri/mode/cam/app/driver.js index c5fdf906..cbfc2177 100644 --- a/src/kiri/mode/cam/driver-fe.js +++ b/src/kiri/mode/cam/app/driver.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../app/api.js'; -import { codec } from '../../core/codec.js'; +import { api } from '../../../app/api.js'; +import { codec } from '../../../core/codec.js'; function surface_prep(index, ondone) { api.client.sync(); diff --git a/src/kiri/mode/cam/init-menu.js b/src/kiri/mode/cam/app/init-menu.js similarity index 99% rename from src/kiri/mode/cam/init-menu.js rename to src/kiri/mode/cam/app/init-menu.js index 83316ba0..6f59d4bc 100644 --- a/src/kiri/mode/cam/init-menu.js +++ b/src/kiri/mode/cam/app/init-menu.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../app/api.js'; +import { api } from '../../../app/api.js'; import { originReset, originSelect } from './cl-origin.js'; import { updateTool } from './tools.js'; diff --git a/src/kiri/mode/cam/init-ui.js b/src/kiri/mode/cam/app/init-ui.js similarity index 98% rename from src/kiri/mode/cam/init-ui.js rename to src/kiri/mode/cam/app/init-ui.js index 6ee3e694..2f315981 100644 --- a/src/kiri/mode/cam/init-ui.js +++ b/src/kiri/mode/cam/app/init-ui.js @@ -1,24 +1,24 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { $ } from '../../../moto/webui.js'; -import { api } from '../../app/api.js'; -import { animate as anim_2d, animate_clear as anim_2d_clear } from './anim-2d-fe.js'; -import { animate2 as anim_3d, animate_clear2 as anim_3d_clear } from './anim-3d-fe.js'; +import { $ } from '../../../../moto/webui.js'; +import { api } from '../../../app/api.js'; +import { animate as anim_2d, animate_clear as anim_2d_clear } from './anim-2d.js'; +import { animate2 as anim_3d, animate_clear2 as anim_3d_clear } from './anim-3d.js'; import { createPopOps } from './cl-ops.js'; import { helicalOn, helicalDone } from './cl-helical.js'; import { holeSelOn, selectHolesDone, clearHolesRec } from './cl-hole.js'; -import { load } from '../../../load/file.js'; -import { Layers } from '../../app/layers.js'; +import { load } from '../../../../load/file.js'; +import { Layers } from '../../../app/layers.js'; import { originSelectDone } from './cl-origin.js'; import { recreateTabs } from './cl-tab.js'; import { showZTop, showZBottom, updateStock } from './cl-stock.js'; -import { space as SPACE } from '../../../moto/space.js'; -import { space } from '../../../moto/space.js'; -import { Stack } from '../../app/stack.js'; +import { space as SPACE } from '../../../../moto/space.js'; +import { space } from '../../../../moto/space.js'; +import { Stack } from '../../../app/stack.js'; import { surfaceOn, surfaceDone } from './cl-surface.js'; import { tabAdd, tabDone, tabClear, restoreTabs, rotateTabs, updateTabs, clearTabs, mirrorTabs } from './cl-tab.js'; import { traceOn, traceDone, unselectTraces } from './cl-trace.js'; -import { Widget, newWidget } from '../../app/widget.js'; +import { Widget, newWidget } from '../../../app/widget.js'; import { zPlaneStart, zPlaneDone, zPlaneSelecting } from './cl-zplane.js'; const { BufferGeometryUtils } = THREE; diff --git a/src/kiri/mode/cam/tools.js b/src/kiri/mode/cam/app/tools.js similarity index 97% rename from src/kiri/mode/cam/tools.js rename to src/kiri/mode/cam/app/tools.js index 8146973a..eec36c20 100644 --- a/src/kiri/mode/cam/tools.js +++ b/src/kiri/mode/cam/app/tools.js @@ -1,10 +1,10 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { $ } from '../../../moto/webui.js'; -import { api } from '../../app/api.js'; -import { consts } from '../../app/consts.js'; -import { settings as setconf } from '../../app/conf/manager.js'; -import { Tool, calcTaperAngle, calcTaperBallExtent, calcTaperLength } from './tool.js'; +import { $ } from '../../../../moto/webui.js'; +import { api } from '../../../app/api.js'; +import { consts } from '../../../app/consts.js'; +import { settings as setconf } from '../../../app/conf/manager.js'; +import { Tool, calcTaperAngle, calcTaperBallExtent, calcTaperLength } from '../core/tool.js'; const DEG2RAD = Math.PI / 180; diff --git a/src/kiri/mode/cam/op.js b/src/kiri/mode/cam/core/op.js similarity index 100% rename from src/kiri/mode/cam/op.js rename to src/kiri/mode/cam/core/op.js diff --git a/src/kiri/mode/cam/core/ops.js b/src/kiri/mode/cam/core/ops.js new file mode 100644 index 00000000..bfb6e1d4 --- /dev/null +++ b/src/kiri/mode/cam/core/ops.js @@ -0,0 +1,43 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { CamOp } from './op.js'; +import { OpArea } from '../work/op-area.js'; +import { OpContour } from '../work/op-contour.js'; +import { OpDrill } from '../work/op-drill.js'; +import { OpGCode } from '../work/op-gcode.js'; +import { OpHelical } from '../work/op-helical.js'; +import { OpIndex } from '../work/op-index.js'; +import { OpLaserOff } from '../work/op-laser-off.js'; +import { OpLaserOn } from '../work/op-laser-on.js'; +import { OpLathe } from '../work/op-lathe.js'; +import { OpLevel } from '../work/op-level.js'; +import { OpLoop } from '../work/op-loop.js'; +import { OpOutline } from '../work/op-outline.js'; +import { OpPocket } from '../work/op-pocket.js'; +import { OpRegister } from '../work/op-register.js'; +import { OpRough } from '../work/op-rough.js'; +import { OpShadow } from '../work/op-shadow.js'; +import { OpTrace } from '../work/op-trace.js'; +import { OpXRay } from '../work/op-xray.js'; + +export const ops = { + "area": OpArea, + "contour": OpContour, + "drill": OpDrill, + "flip": CamOp, + "gcode": OpGCode, + "helical": OpHelical, + "index": OpIndex, + "laser off": OpLaserOff, + "laser on": OpLaserOn, + "lathe": OpLathe, + "level": OpLevel, + "loop": OpLoop, + "outline": OpOutline, + "pocket": OpPocket, + "register": OpRegister, + "rough": OpRough, + "shadow": OpShadow, + "trace": OpTrace, + "xray": OpXRay +}; diff --git a/src/kiri/mode/cam/tool.js b/src/kiri/mode/cam/core/tool.js similarity index 100% rename from src/kiri/mode/cam/tool.js rename to src/kiri/mode/cam/core/tool.js diff --git a/src/kiri/mode/cam/ops.js b/src/kiri/mode/cam/ops.js deleted file mode 100644 index 3685fd1e..00000000 --- a/src/kiri/mode/cam/ops.js +++ /dev/null @@ -1,43 +0,0 @@ -/** Copyright Stewart Allen -- All Rights Reserved */ - -import { CamOp } from './op.js'; -import { OpArea } from './op-area.js'; -import { OpContour } from './op-contour.js'; -import { OpDrill } from './op-drill.js'; -import { OpGCode } from './op-gcode.js'; -import { OpHelical } from './op-helical.js'; -import { OpIndex } from './op-index.js'; -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'; -import { OpRough } from './op-rough.js'; -import { OpShadow } from './op-shadow.js'; -import { OpTrace } from './op-trace.js'; -import { OpXRay } from './op-xray.js'; - -export const ops = { - "area": OpArea, - "contour": OpContour, - "drill": OpDrill, - "flip": CamOp, - "gcode": OpGCode, - "helical": OpHelical, - "index": OpIndex, - "laser off": OpLaserOff, - "laser on": OpLaserOn, - "lathe": OpLathe, - "level": OpLevel, - "loop": OpLoop, - "outline": OpOutline, - "pocket": OpPocket, - "register": OpRegister, - "rough": OpRough, - "shadow": OpShadow, - "trace": OpTrace, - "xray": OpXRay -}; diff --git a/src/kiri/mode/cam/anim-2d-be.js b/src/kiri/mode/cam/work/anim-2d.js similarity index 99% rename from src/kiri/mode/cam/anim-2d-be.js rename to src/kiri/mode/cam/work/anim-2d.js index 92c0f6e0..09255c97 100644 --- a/src/kiri/mode/cam/anim-2d-be.js +++ b/src/kiri/mode/cam/work/anim-2d.js @@ -2,7 +2,7 @@ // WORKER BACK END ANIMATION CODE for 2D -import { Tool } from './tool.js'; +import { Tool } from '../core/tool.js'; const asPoints = false; const asLines = false; diff --git a/src/kiri/mode/cam/anim-3d-be.js b/src/kiri/mode/cam/work/anim-3d.js similarity index 99% rename from src/kiri/mode/cam/anim-3d-be.js rename to src/kiri/mode/cam/work/anim-3d.js index b8c23ff7..1c8a6a03 100644 --- a/src/kiri/mode/cam/anim-3d-be.js +++ b/src/kiri/mode/cam/work/anim-3d.js @@ -2,8 +2,8 @@ // WORKER BACK END ANIMATION CODE for 3D (indexed) -import { CSG } from '../../../geo/csg.js'; -import { Tool } from './tool.js'; +import { CSG } from '../../../../geo/csg.js'; +import { Tool } from '../core/tool.js'; let nextMeshID = 1, stock, diff --git a/src/kiri/mode/cam/export.js b/src/kiri/mode/cam/work/export.js similarity index 99% rename from src/kiri/mode/cam/export.js rename to src/kiri/mode/cam/work/export.js index b9206f9b..f8bd0b2a 100644 --- a/src/kiri/mode/cam/export.js +++ b/src/kiri/mode/cam/work/export.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { base } from '../../../geo/base.js'; -import { version } from '../../../moto/license.js'; +import { base } from '../../../../geo/base.js'; +import { version } from '../../../../moto/license.js'; const { util } = base; diff --git a/src/kiri/mode/cam/driver-be.js b/src/kiri/mode/cam/work/init-work.js similarity index 94% rename from src/kiri/mode/cam/driver-be.js rename to src/kiri/mode/cam/work/init-work.js index 7f850aef..c57b1971 100644 --- a/src/kiri/mode/cam/driver-be.js +++ b/src/kiri/mode/cam/work/init-work.js @@ -1,14 +1,14 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { codec } from '../../core/codec.js'; +import { codec } from '../../../core/codec.js'; import { cam_slice_pre, cam_slice, holes, traces } from './slice.js'; import { cam_prepare } from './prepare.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_3d } from './anim-3d-be.js'; +import { init as init_2d } from './anim-2d.js'; +import { init as init_3d } from './anim-3d.js'; function surface_prep(widget, index) { if (!widget.tool) { diff --git a/src/kiri/mode/cam/op-area.js b/src/kiri/mode/cam/work/op-area.js similarity index 98% rename from src/kiri/mode/cam/op-area.js rename to src/kiri/mode/cam/work/op-area.js index 2e498e83..03a4a9fe 100644 --- a/src/kiri/mode/cam/op-area.js +++ b/src/kiri/mode/cam/work/op-area.js @@ -3,15 +3,15 @@ // todo: surface offset pattern // todo: trace dogbones, merge overlap -import { CamOp } from './op.js'; -import { Tool } from './tool.js'; -import { newSlice } from '../../core/slice.js'; -import { newPoint } from '../../../geo/point.js'; -import { newPolygon } from '../../../geo/polygon.js'; -import { polygons as POLY } from '../../../geo/polygons.js'; -import { util as base_util } from '../../../geo/base.js'; -import { tip2tipJoin } from '../../../geo/paths.js'; -import { CAM } from './driver-be.js'; +import { CamOp } from '../core/op.js'; +import { Tool } from '../core/tool.js'; +import { newSlice } from '../../../core/slice.js'; +import { newPoint } from '../../../../geo/point.js'; +import { newPolygon } from '../../../../geo/polygon.js'; +import { polygons as POLY } from '../../../../geo/polygons.js'; +import { util as base_util } from '../../../../geo/base.js'; +import { tip2tipJoin } from '../../../../geo/paths.js'; +import { CAM } from './init-work.js'; const DEG2RAD = Math.PI / 180; const clib = self.ClipperLib; diff --git a/src/kiri/mode/cam/op-contour.js b/src/kiri/mode/cam/work/op-contour.js similarity index 95% rename from src/kiri/mode/cam/op-contour.js rename to src/kiri/mode/cam/work/op-contour.js index c3d95682..e51ce6bd 100644 --- a/src/kiri/mode/cam/op-contour.js +++ b/src/kiri/mode/cam/work/op-contour.js @@ -1,12 +1,12 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { CamOp } from './op.js'; -import { Tool } from './tool.js'; +import { CamOp } from '../core/op.js'; +import { Tool } from '../core/tool.js'; import { generate as Topo } from './topo3.js'; -import { newPoint } from '../../../geo/point.js'; -import { newPolygon } from '../../../geo/polygon.js'; -import { newSlice } from '../../core/slice.js'; -import { tip2tipEmit } from '../../../geo/paths.js'; +import { newPoint } from '../../../../geo/point.js'; +import { newPolygon } from '../../../../geo/polygon.js'; +import { newSlice } from '../../../core/slice.js'; +import { tip2tipEmit } from '../../../../geo/paths.js'; function createFilter(op, origin, axis) { // console.log({ origin, axis }); diff --git a/src/kiri/mode/cam/op-drill.js b/src/kiri/mode/cam/work/op-drill.js similarity index 89% rename from src/kiri/mode/cam/op-drill.js rename to src/kiri/mode/cam/work/op-drill.js index 319d9ebb..3189e6d5 100644 --- a/src/kiri/mode/cam/op-drill.js +++ b/src/kiri/mode/cam/work/op-drill.js @@ -1,10 +1,10 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { CamOp } from './op.js'; -import { Tool } from './tool.js'; -import { newPolygon } from '../../../geo/polygon.js'; -import { newSlice } from '../../core/slice.js'; -import { newPoint } from '../../../geo/point.js'; +import { CamOp } from '../core/op.js'; +import { Tool } from '../core/tool.js'; +import { newPolygon } from '../../../../geo/polygon.js'; +import { newSlice } from '../../../core/slice.js'; +import { newPoint } from '../../../../geo/point.js'; class OpDrill extends CamOp { constructor(state, op) { diff --git a/src/kiri/mode/cam/op-gcode.js b/src/kiri/mode/cam/work/op-gcode.js similarity index 86% rename from src/kiri/mode/cam/op-gcode.js rename to src/kiri/mode/cam/work/op-gcode.js index a3ed9bfb..3881718a 100644 --- a/src/kiri/mode/cam/op-gcode.js +++ b/src/kiri/mode/cam/work/op-gcode.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { CamOp } from './op.js'; +import { CamOp } from '../core/op.js'; export class OpGCode extends CamOp { constructor(state, op) { diff --git a/src/kiri/mode/cam/op-helical.js b/src/kiri/mode/cam/work/op-helical.js similarity index 97% rename from src/kiri/mode/cam/op-helical.js rename to src/kiri/mode/cam/work/op-helical.js index 3458c45b..299ae493 100644 --- a/src/kiri/mode/cam/op-helical.js +++ b/src/kiri/mode/cam/work/op-helical.js @@ -1,10 +1,10 @@ -import { arcToPath } from "../../../geo/paths.js"; -import { newPolygon } from "../../../geo/polygon.js"; -import { CamOp } from "./op.js"; -import { Tool } from "./tool.js"; +import { arcToPath } from "../../../../geo/paths.js"; +import { newPolygon } from "../../../../geo/polygon.js"; +import { CamOp } from "../core/op.js"; +import { Tool } from "../core/tool.js"; import { cylinder_poly_find } from "./slice.js"; -import { newPoint } from "../../../geo/point.js"; -import { newSlice } from "../../core/slice.js"; +import { newPoint } from "../../../../geo/point.js"; +import { newSlice } from "../../../core/slice.js"; export class OpHelical extends CamOp { constructor(state, op) { diff --git a/src/kiri/mode/cam/op-index.js b/src/kiri/mode/cam/work/op-index.js similarity index 97% rename from src/kiri/mode/cam/op-index.js rename to src/kiri/mode/cam/work/op-index.js index a56555b8..e8a285ae 100644 --- a/src/kiri/mode/cam/op-index.js +++ b/src/kiri/mode/cam/work/op-index.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { CamOp } from './op.js'; +import { CamOp } from '../core/op.js'; export class OpIndex extends CamOp { constructor(state, op) { diff --git a/src/kiri/mode/cam/op-laser-off.js b/src/kiri/mode/cam/work/op-laser-off.js similarity index 91% rename from src/kiri/mode/cam/op-laser-off.js rename to src/kiri/mode/cam/work/op-laser-off.js index a474e933..fcdb8118 100644 --- a/src/kiri/mode/cam/op-laser-off.js +++ b/src/kiri/mode/cam/work/op-laser-off.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { CamOp } from './op.js'; +import { CamOp } from '../core/op.js'; export class OpLaserOff extends CamOp { constructor(state, op) { diff --git a/src/kiri/mode/cam/op-laser-on.js b/src/kiri/mode/cam/work/op-laser-on.js similarity index 91% rename from src/kiri/mode/cam/op-laser-on.js rename to src/kiri/mode/cam/work/op-laser-on.js index 076009c9..f67a47ae 100644 --- a/src/kiri/mode/cam/op-laser-on.js +++ b/src/kiri/mode/cam/work/op-laser-on.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { CamOp } from './op.js'; +import { CamOp } from '../core/op.js'; export class OpLaserOn extends CamOp { constructor(state, op) { diff --git a/src/kiri/mode/cam/op-lathe.js b/src/kiri/mode/cam/work/op-lathe.js similarity index 96% rename from src/kiri/mode/cam/op-lathe.js rename to src/kiri/mode/cam/work/op-lathe.js index fcc69e63..15c751d5 100644 --- a/src/kiri/mode/cam/op-lathe.js +++ b/src/kiri/mode/cam/work/op-lathe.js @@ -1,8 +1,8 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { CamOp } from './op.js'; +import { CamOp } from '../core/op.js'; import { generate as topo4_generate } from './topo4.js'; -import { newPoint } from '../../../geo/point.js'; +import { newPoint } from '../../../../geo/point.js'; function createFilter(op) { let ok = () => true; diff --git a/src/kiri/mode/cam/op-level.js b/src/kiri/mode/cam/work/op-level.js similarity index 90% rename from src/kiri/mode/cam/op-level.js rename to src/kiri/mode/cam/work/op-level.js index 2ef30a70..2f90b81e 100644 --- a/src/kiri/mode/cam/op-level.js +++ b/src/kiri/mode/cam/work/op-level.js @@ -1,11 +1,11 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { CamOp } from './op.js'; -import { Tool } from './tool.js'; -import { newPolygon } from '../../../geo/polygon.js'; -import { polygons as POLY } from '../../../geo/polygons.js'; -import { newSlice } from '../../core/slice.js'; -import { util } from '../../../geo/base.js'; +import { CamOp } from '../core/op.js'; +import { Tool } from '../core/tool.js'; +import { newPolygon } from '../../../../geo/polygon.js'; +import { polygons as POLY } from '../../../../geo/polygons.js'; +import { newSlice } from '../../../core/slice.js'; +import { util } from '../../../../geo/base.js'; class OpLevel extends CamOp { constructor(state, op) { diff --git a/src/kiri/mode/cam/op-loop.js b/src/kiri/mode/cam/work/op-loop.js similarity index 89% rename from src/kiri/mode/cam/op-loop.js rename to src/kiri/mode/cam/work/op-loop.js index 06e16fe9..def7bc73 100644 --- a/src/kiri/mode/cam/op-loop.js +++ b/src/kiri/mode/cam/work/op-loop.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { CamOp } from './op.js'; +import { CamOp } from '../core/op.js'; export class OpLoop extends CamOp { weight() { diff --git a/src/kiri/mode/cam/op-outline.js b/src/kiri/mode/cam/work/op-outline.js similarity index 98% rename from src/kiri/mode/cam/op-outline.js rename to src/kiri/mode/cam/work/op-outline.js index 19c17fb2..198eab31 100644 --- a/src/kiri/mode/cam/op-outline.js +++ b/src/kiri/mode/cam/work/op-outline.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { CamOp } from './op.js'; +import { CamOp } from '../core/op.js'; import { OpArea } from './op-area.js'; class OpOutline extends CamOp { diff --git a/src/kiri/mode/cam/op-pocket.js b/src/kiri/mode/cam/work/op-pocket.js similarity index 96% rename from src/kiri/mode/cam/op-pocket.js rename to src/kiri/mode/cam/work/op-pocket.js index 6bc35a2e..f225ea72 100644 --- a/src/kiri/mode/cam/op-pocket.js +++ b/src/kiri/mode/cam/work/op-pocket.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { CamOp } from './op.js'; +import { CamOp } from '../core/op.js'; import { OpArea } from './op-area.js'; class OpPocket extends CamOp { diff --git a/src/kiri/mode/cam/op-register.js b/src/kiri/mode/cam/work/op-register.js similarity index 95% rename from src/kiri/mode/cam/op-register.js rename to src/kiri/mode/cam/work/op-register.js index d9c5d85c..69f7199d 100644 --- a/src/kiri/mode/cam/op-register.js +++ b/src/kiri/mode/cam/work/op-register.js @@ -1,11 +1,11 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { CamOp } from './op.js'; -import { Tool } from './tool.js'; -import { newPolygon } from '../../../geo/polygon.js'; -import { newSlice } from '../../core/slice.js'; -import { newPoint } from '../../../geo/point.js'; -import { util } from '../../../geo/base.js'; +import { CamOp } from '../core/op.js'; +import { Tool } from '../core/tool.js'; +import { newPolygon } from '../../../../geo/polygon.js'; +import { newSlice } from '../../../core/slice.js'; +import { newPoint } from '../../../../geo/point.js'; +import { util } from '../../../../geo/base.js'; class OpRegister extends CamOp { constructor(state, op) { diff --git a/src/kiri/mode/cam/op-rough.js b/src/kiri/mode/cam/work/op-rough.js similarity index 96% rename from src/kiri/mode/cam/op-rough.js rename to src/kiri/mode/cam/work/op-rough.js index 153a206c..fbe50235 100644 --- a/src/kiri/mode/cam/op-rough.js +++ b/src/kiri/mode/cam/work/op-rough.js @@ -1,9 +1,9 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { CamOp } from './op.js'; +import { CamOp } from '../core/op.js'; import { OpArea } from './op-area.js'; -import { newPolygon } from '../../../geo/polygon.js'; -import { polygons as POLY } from '../../../geo/polygons.js'; +import { newPolygon } from '../../../../geo/polygon.js'; +import { polygons as POLY } from '../../../../geo/polygons.js'; class OpRough extends CamOp { constructor(state, op) { diff --git a/src/kiri/mode/cam/op-shadow.js b/src/kiri/mode/cam/work/op-shadow.js similarity index 95% rename from src/kiri/mode/cam/op-shadow.js rename to src/kiri/mode/cam/work/op-shadow.js index a72b72d6..c33cbb8f 100644 --- a/src/kiri/mode/cam/op-shadow.js +++ b/src/kiri/mode/cam/work/op-shadow.js @@ -1,8 +1,8 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { CamOp } from './op.js'; -import { newSlice } from '../../core/slice.js'; -import { newPolygon } from '../../../geo/polygon.js'; +import { CamOp } from '../core/op.js'; +import { newSlice } from '../../../core/slice.js'; +import { newPolygon } from '../../../../geo/polygon.js'; /** * Computes the Part "shadow" and attaches relevant data to the "state" object diff --git a/src/kiri/mode/cam/op-trace.js b/src/kiri/mode/cam/work/op-trace.js similarity index 97% rename from src/kiri/mode/cam/op-trace.js rename to src/kiri/mode/cam/work/op-trace.js index e315bd44..25ae2065 100644 --- a/src/kiri/mode/cam/op-trace.js +++ b/src/kiri/mode/cam/work/op-trace.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { CamOp } from './op.js'; +import { CamOp } from '../core/op.js'; import { OpArea } from './op-area.js'; class OpTrace extends CamOp { diff --git a/src/kiri/mode/cam/op-xray.js b/src/kiri/mode/cam/work/op-xray.js similarity index 87% rename from src/kiri/mode/cam/op-xray.js rename to src/kiri/mode/cam/work/op-xray.js index 12245c6c..b32ffe0f 100644 --- a/src/kiri/mode/cam/op-xray.js +++ b/src/kiri/mode/cam/work/op-xray.js @@ -1,8 +1,8 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { CamOp } from './op.js'; -import { newSlice } from '../../core/slice.js'; -import { Slicer } from './slicer_cam.js'; +import { CamOp } from '../core/op.js'; +import { newSlice } from '../../../core/slice.js'; +import { Slicer } from './slicer-cam.js'; class OpXRay extends CamOp { constructor(state, op) { diff --git a/src/kiri/mode/cam/prepare.js b/src/kiri/mode/cam/work/prepare.js similarity index 98% rename from src/kiri/mode/cam/prepare.js rename to src/kiri/mode/cam/work/prepare.js index a1d645c7..04610e6b 100644 --- a/src/kiri/mode/cam/prepare.js +++ b/src/kiri/mode/cam/work/prepare.js @@ -1,12 +1,12 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { tip2tipEmit, poly2polyEmit } from '../../../geo/paths.js'; -import { newPoint } from '../../../geo/point.js'; -import { polygons as POLY } from '../../../geo/polygons.js'; -import { render } from '../../core/render.js'; -import { newPrint } from '../../core/print.js'; -import { Tool } from './tool.js'; -import { newPolygon } from '../../../geo/polygon.js'; +import { tip2tipEmit, poly2polyEmit } from '../../../../geo/paths.js'; +import { newPoint } from '../../../../geo/point.js'; +import { polygons as POLY } from '../../../../geo/polygons.js'; +import { render } from '../../../core/render.js'; +import { newPrint } from '../../../core/print.js'; +import { Tool } from '../core/tool.js'; +import { newPolygon } from '../../../../geo/polygon.js'; const debug = false; const debug_push = false; diff --git a/src/kiri/mode/cam/slice.js b/src/kiri/mode/cam/work/slice.js similarity index 98% rename from src/kiri/mode/cam/slice.js rename to src/kiri/mode/cam/work/slice.js index 5cd48143..c9735b9d 100644 --- a/src/kiri/mode/cam/slice.js +++ b/src/kiri/mode/cam/work/slice.js @@ -1,15 +1,15 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { CAM } from './driver-be.js'; -import { newLine } from '../../../geo/line.js'; -import { newPoint } from '../../../geo/point.js'; -import { newPolygon } from '../../../geo/polygon.js'; -import { ops as OPS } from './ops.js'; -import { polygons as POLY } from '../../../geo/polygons.js'; -import { setSliceTracker } from '../../core/slice.js'; -import { Slicer as cam_slicer } from './slicer_cam.js'; -import { Tool } from './tool.js'; -import { util } from '../../../geo/base.js'; +import { CAM } from './init-work.js'; +import { newLine } from '../../../../geo/line.js'; +import { newPoint } from '../../../../geo/point.js'; +import { newPolygon } from '../../../../geo/polygon.js'; +import { ops as OPS } from '../core/ops.js'; +import { polygons as POLY } from '../../../../geo/polygons.js'; +import { setSliceTracker } from '../../../core/slice.js'; +import { Slicer as cam_slicer } from './slicer-cam.js'; +import { Tool } from '../core/tool.js'; +import { util } from '../../../../geo/base.js'; // state shared across all widget slicing let shared; diff --git a/src/kiri/mode/cam/slicer_cam.js b/src/kiri/mode/cam/work/slicer-cam.js similarity index 97% rename from src/kiri/mode/cam/slicer_cam.js rename to src/kiri/mode/cam/work/slicer-cam.js index 862914fb..3f68d5c2 100644 --- a/src/kiri/mode/cam/slicer_cam.js +++ b/src/kiri/mode/cam/work/slicer-cam.js @@ -1,18 +1,18 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { util } from '../../../geo/base.js'; -import { decode, decodePointArray } from '../../core/codec.js'; -import { newLine } from '../../../geo/line.js'; -import { newPoint } from '../../../geo/point.js'; -import { newSlice } from '../../core/slice.js'; -import { polygons as POLY } from '../../../geo/polygons.js'; +import { util } from '../../../../geo/base.js'; +import { decode, decodePointArray } from '../../../core/codec.js'; +import { newLine } from '../../../../geo/line.js'; +import { newPoint } from '../../../../geo/point.js'; +import { newSlice } from '../../../core/slice.js'; +import { polygons as POLY } from '../../../../geo/polygons.js'; import { checkOverUnderOn, intersectPoints, makeZLine, removeDuplicateLines, sliceConnect, -} from '../../../geo/slicer.js'; +} from '../../../../geo/slicer.js'; const timing = false; const zDecimal = 3; diff --git a/src/kiri/mode/cam/slicer_topo.js b/src/kiri/mode/cam/work/slicer-topo.js similarity index 98% rename from src/kiri/mode/cam/slicer_topo.js rename to src/kiri/mode/cam/work/slicer-topo.js index 7aeeec3b..3c80e4b3 100644 --- a/src/kiri/mode/cam/slicer_topo.js +++ b/src/kiri/mode/cam/work/slicer-topo.js @@ -1,12 +1,12 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { newPoint } from '../../../geo/point.js'; +import { newPoint } from '../../../../geo/point.js'; import { checkOverUnderOn, makeZLine, removeDuplicateLines, intersectPoints -} from '../../../geo/slicer.js'; +} from '../../../../geo/slicer.js'; /** Slicer used in Topo3 and Topo4 to find contour lines */ export class Slicer { diff --git a/src/kiri/mode/cam/topo3.js b/src/kiri/mode/cam/work/topo3.js similarity index 98% rename from src/kiri/mode/cam/topo3.js rename to src/kiri/mode/cam/work/topo3.js index e5a7dbd7..495c827c 100644 --- a/src/kiri/mode/cam/topo3.js +++ b/src/kiri/mode/cam/work/topo3.js @@ -1,12 +1,12 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { codec } from '../../core/codec.js'; -import { newPoint } from '../../../geo/point.js'; -import { newPolygon } from '../../../geo/polygon.js'; -import { newSlice } from '../../core/slice.js'; -import { Slicer as topo_slicer } from './slicer_topo.js'; -import { polygons as POLY } from '../../../geo/polygons.js'; -import { Tool } from './tool.js'; +import { codec } from '../../../core/codec.js'; +import { newPoint } from '../../../../geo/point.js'; +import { newPolygon } from '../../../../geo/polygon.js'; +import { newSlice } from '../../../core/slice.js'; +import { Slicer as topo_slicer } from './slicer-topo.js'; +import { polygons as POLY } from '../../../../geo/polygons.js'; +import { Tool } from '../core/tool.js'; const RAD2DEG = 180 / Math.PI; const DEG2RAD = Math.PI / 180; diff --git a/src/kiri/mode/cam/topo4.js b/src/kiri/mode/cam/work/topo4.js similarity index 98% rename from src/kiri/mode/cam/topo4.js rename to src/kiri/mode/cam/work/topo4.js index f78ec3ec..c73d027c 100644 --- a/src/kiri/mode/cam/topo4.js +++ b/src/kiri/mode/cam/work/topo4.js @@ -1,12 +1,12 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { codec } from '../../core/codec.js'; -import { newPoint } from '../../../geo/point.js'; -import { newPolygon } from '../../../geo/polygon.js'; -import { sliceConnect } from '../../../geo/slicer.js'; -import { newSlice } from '../../core/slice.js'; -import { Tool } from './tool.js'; -import { Slicer as topo_slicer } from './slicer_topo.js'; +import { codec } from '../../../core/codec.js'; +import { newPoint } from '../../../../geo/point.js'; +import { newPolygon } from '../../../../geo/polygon.js'; +import { sliceConnect } from '../../../../geo/slicer.js'; +import { newSlice } from '../../../core/slice.js'; +import { Tool } from '../core/tool.js'; +import { Slicer as topo_slicer } from './slicer-topo.js'; const RAD2DEG = 180 / Math.PI; const DEG2RAD = Math.PI / 180; diff --git a/src/kiri/mode/drag/driver.js b/src/kiri/mode/drag/init-work.js similarity index 99% rename from src/kiri/mode/drag/driver.js rename to src/kiri/mode/drag/init-work.js index 109fb769..801318d9 100644 --- a/src/kiri/mode/drag/driver.js +++ b/src/kiri/mode/drag/init-work.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { LASER, TYPE, FUNC } from '../laser/driver.js'; +import { LASER, TYPE, FUNC } from '../laser/init-work.js'; import { newPoint } from '../../../geo/point.js'; import { newPolygon } from '../../../geo/polygon.js'; import { newSlopeFromAngle } from '../../../geo/slope.js'; diff --git a/src/kiri/mode/fdm/init-menu.js b/src/kiri/mode/fdm/app/init-menu.js similarity index 99% rename from src/kiri/mode/fdm/init-menu.js rename to src/kiri/mode/fdm/app/init-menu.js index 8131a6da..799ace08 100644 --- a/src/kiri/mode/fdm/init-menu.js +++ b/src/kiri/mode/fdm/app/init-menu.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../app/api.js'; +import { api } from '../../../app/api.js'; let LANG = api.language.current; let { FDM } = api.const.MODES, diff --git a/src/kiri/mode/fdm/init-ui.js b/src/kiri/mode/fdm/app/init-ui.js similarity index 97% rename from src/kiri/mode/fdm/init-ui.js rename to src/kiri/mode/fdm/app/init-ui.js index b7255bd9..ab590713 100644 --- a/src/kiri/mode/fdm/init-ui.js +++ b/src/kiri/mode/fdm/app/init-ui.js @@ -1,11 +1,11 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { newPoint } from '../../../geo/point.js'; -import { $ } from '../../../moto/webui.js'; -import { api } from '../../app/api.js'; -import { colorSchemeRegistry } from '../../app/color/schemes.js'; -import { conf } from '../../app/conf/defaults.js'; -import { addPaintOverlayTexture, updatePaintOverlay } from '../../app/paint.js'; +import { newPoint } from '../../../../geo/point.js'; +import { $ } from '../../../../moto/webui.js'; +import { api } from '../../../app/api.js'; +import { colorSchemeRegistry } from '../../../app/color/schemes.js'; +import { conf } from '../../../app/conf/defaults.js'; +import { addPaintOverlayTexture, updatePaintOverlay } from '../../../app/paint.js'; const { VIEWS } = api.const; diff --git a/src/kiri/mode/fdm/driver.js b/src/kiri/mode/fdm/core/params.js similarity index 100% rename from src/kiri/mode/fdm/driver.js rename to src/kiri/mode/fdm/core/params.js diff --git a/src/kiri/mode/fdm/belt.js b/src/kiri/mode/fdm/work/belt.js similarity index 98% rename from src/kiri/mode/fdm/belt.js rename to src/kiri/mode/fdm/work/belt.js index 922f49d7..0d3bbef6 100644 --- a/src/kiri/mode/fdm/belt.js +++ b/src/kiri/mode/fdm/work/belt.js @@ -5,10 +5,10 @@ * Handles anchor generation, text embossing, and belt-specific bounds */ -import { newPolygon } from '../../../geo/polygon.js'; -import { newBounds } from '../../../geo/bounds.js'; -import { newSlice } from '../../core/slice.js'; -import { getRangeParameters } from './driver.js'; +import { newPolygon } from '../../../../geo/polygon.js'; +import { newBounds } from '../../../../geo/bounds.js'; +import { newSlice } from '../../../core/slice.js'; +import { getRangeParameters } from '../core/params.js'; // Import constants from slice.js // These would ideally be shared, but for now we'll define locally diff --git a/src/kiri/mode/fdm/export.js b/src/kiri/mode/fdm/work/export.js similarity index 99% rename from src/kiri/mode/fdm/export.js rename to src/kiri/mode/fdm/work/export.js index 0ebd97ff..07ac27b2 100644 --- a/src/kiri/mode/fdm/export.js +++ b/src/kiri/mode/fdm/work/export.js @@ -1,9 +1,9 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { base, util } from '../../../geo/base.js'; -import { getRangeParameters } from './driver.js'; -import { FDM } from './driver-be.js'; -import { version } from '../../../moto/license.js'; +import { base, util } from '../../../../geo/base.js'; +import { getRangeParameters } from '../core/params.js'; +import { FDM } from './init-work.js'; +import { version } from '../../../../moto/license.js'; const { config } = base; const debug = false; diff --git a/src/kiri/mode/fdm/fill.js b/src/kiri/mode/fdm/work/fill.js similarity index 98% rename from src/kiri/mode/fdm/fill.js rename to src/kiri/mode/fdm/work/fill.js index 830dec06..d7545b87 100644 --- a/src/kiri/mode/fdm/fill.js +++ b/src/kiri/mode/fdm/work/fill.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { newPolygon } from '../../../geo/polygon.js'; -import { slice as gyroid_slice } from '../../../geo/gyroid.js'; +import { newPolygon } from '../../../../geo/polygon.js'; +import { slice as gyroid_slice } from '../../../../geo/gyroid.js'; export const fill_fixed = { hex: fillHexFull, diff --git a/src/kiri/mode/fdm/driver-be.js b/src/kiri/mode/fdm/work/init-work.js similarity index 93% rename from src/kiri/mode/fdm/driver-be.js rename to src/kiri/mode/fdm/work/init-work.js index 72df30ac..a3d699f7 100644 --- a/src/kiri/mode/fdm/driver-be.js +++ b/src/kiri/mode/fdm/work/init-work.js @@ -1,10 +1,10 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { util } from '../../../geo/base.js'; +import { util } from '../../../../geo/base.js'; import { sliceOne, slicePost } from './slice.js'; import { fdm_prepare } from './prepare.js'; import { fdm_export } from './export.js'; -import { codec } from '../../core/codec.js'; +import { codec } from '../../../core/codec.js'; // noz = nozzle diameter // fil = filament diameter diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/work/post.js similarity index 98% rename from src/kiri/mode/fdm/post.js rename to src/kiri/mode/fdm/work/post.js index 07811c40..4cdd6110 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/work/post.js @@ -1,13 +1,13 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import '../../../ext/jspoly.js'; -import { base } from '../../../geo/base.js'; -import { newPolygon, Polygon } from '../../../geo/polygon.js'; -import { polygons as POLY, fillArea } from '../../../geo/polygons.js'; -import { getRangeParameters } from './driver.js'; -import { slicer } from '../../../geo/slicer.js'; -import { Point } from '../../../geo/point.js'; -import { Slope } from '../../../geo/slope.js'; +import '../../../../ext/jspoly.js'; +import { base } from '../../../../geo/base.js'; +import { newPolygon, Polygon } from '../../../../geo/polygon.js'; +import { polygons as POLY, fillArea } from '../../../../geo/polygons.js'; +import { getRangeParameters } from '../core/params.js'; +import { slicer } from '../../../../geo/slicer.js'; +import { Point } from '../../../../geo/point.js'; +import { Slope } from '../../../../geo/slope.js'; // tell the slicer what to do in FDM mode // with slices (post processing). the post diff --git a/src/kiri/mode/fdm/prepare.js b/src/kiri/mode/fdm/work/prepare.js similarity index 99% rename from src/kiri/mode/fdm/prepare.js rename to src/kiri/mode/fdm/work/prepare.js index 5bdb7f66..9058f3ab 100644 --- a/src/kiri/mode/fdm/prepare.js +++ b/src/kiri/mode/fdm/work/prepare.js @@ -1,16 +1,16 @@ /** Copyright Stewart Allen -- 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'; -import { newPoint, Point } from '../../../geo/point.js'; -import { newPolygon, Polygon } from '../../../geo/polygon.js'; -import { polygons as POLY, fillArea } from '../../../geo/polygons.js'; -import { newPrint } from '../../core/print.js'; -import { newSlice } from '../../core/slice.js'; -import { render } from '../../core/render.js'; -import { getRangeParameters } from './driver.js'; +import { FDM } from './init-work.js'; +import { base, util } from '../../../../geo/base.js'; +import { poly2polyEmit, tip2tipEmit } from '../../../../geo/paths.js'; +import { newBounds } from '../../../../geo/bounds.js'; +import { newPoint, Point } from '../../../../geo/point.js'; +import { newPolygon, Polygon } from '../../../../geo/polygon.js'; +import { polygons as POLY, fillArea } from '../../../../geo/polygons.js'; +import { newPrint } from '../../../core/print.js'; +import { newSlice } from '../../../core/slice.js'; +import { render } from '../../../core/render.js'; +import { getRangeParameters } from '../core/params.js'; const { numOrDefault } = util; diff --git a/src/kiri/mode/fdm/slice.js b/src/kiri/mode/fdm/work/slice.js similarity index 99% rename from src/kiri/mode/fdm/slice.js rename to src/kiri/mode/fdm/work/slice.js index 714a5e93..47f6a4f5 100644 --- a/src/kiri/mode/fdm/slice.js +++ b/src/kiri/mode/fdm/work/slice.js @@ -3,13 +3,13 @@ import { layerProcessTop } from './post.js'; import { fill, fill_fixed } from './fill.js'; import { generateBeltAnchor, embossBeltPooch, finalizeBeltBounds } from './belt.js'; -import { getRangeParameters } from './driver.js'; -import { newPoint } from '../../../geo/point.js'; -import { newPolygon } from '../../../geo/polygon.js'; -import { newSlice } from '../../core/slice.js'; -import { polygons as POLY } from '../../../geo/polygons.js'; -import { slice, sliceZ } from '../../../geo/slicer.js'; -import { util } from '../../../geo/base.js'; +import { getRangeParameters } from '../core/params.js'; +import { newPoint } from '../../../../geo/point.js'; +import { newPolygon } from '../../../../geo/polygon.js'; +import { newSlice } from '../../../core/slice.js'; +import { polygons as POLY } from '../../../../geo/polygons.js'; +import { slice, sliceZ } from '../../../../geo/slicer.js'; +import { util } from '../../../../geo/base.js'; const CONSTANTS = { // Support fill diff --git a/src/kiri/mode/laser/driver.js b/src/kiri/mode/laser/init-work.js similarity index 100% rename from src/kiri/mode/laser/driver.js rename to src/kiri/mode/laser/init-work.js diff --git a/src/kiri/mode/sla/init-menu.js b/src/kiri/mode/sla/app/init-menu.js similarity index 98% rename from src/kiri/mode/sla/init-menu.js rename to src/kiri/mode/sla/app/init-menu.js index d1a01f45..6e3e1bb3 100644 --- a/src/kiri/mode/sla/init-menu.js +++ b/src/kiri/mode/sla/app/init-menu.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from '../../app/api.js'; +import { api } from '../../../app/api.js'; let LANG = api.language.current; let { SLA } = api.const.MODES, diff --git a/src/kiri/mode/sla/init-ui.js b/src/kiri/mode/sla/app/init-ui.js similarity index 96% rename from src/kiri/mode/sla/init-ui.js rename to src/kiri/mode/sla/app/init-ui.js index 1e5104ad..48a20bbd 100644 --- a/src/kiri/mode/sla/init-ui.js +++ b/src/kiri/mode/sla/app/init-ui.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { $ } from '../../../moto/webui.js'; -import { api } from '../../app/api.js'; +import { $ } from '../../../../moto/webui.js'; +import { api } from '../../../app/api.js'; export const SLA = { init, diff --git a/src/kiri/mode/sla/export.js b/src/kiri/mode/sla/work/export.js similarity index 99% rename from src/kiri/mode/sla/export.js rename to src/kiri/mode/sla/work/export.js index 82867ea4..d296aea7 100644 --- a/src/kiri/mode/sla/export.js +++ b/src/kiri/mode/sla/work/export.js @@ -2,7 +2,7 @@ import { CXDLP } from './x_cxdlp.js'; import { photon } from './x_photon.js'; -import { SLA } from './driver.js'; +import { SLA } from './init-work.js'; /** * DRIVER CONTRACT - runs in worker diff --git a/src/kiri/mode/sla/driver.js b/src/kiri/mode/sla/work/init-work.js similarity index 96% rename from src/kiri/mode/sla/driver.js rename to src/kiri/mode/sla/work/init-work.js index 1a160945..6994cc22 100644 --- a/src/kiri/mode/sla/driver.js +++ b/src/kiri/mode/sla/work/init-work.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { newPrint } from '../../core/print.js'; +import { newPrint } from '../../../core/print.js'; import { sla_slice } from './slice.js'; import { sla_export } from './export.js'; diff --git a/src/kiri/mode/sla/slice.js b/src/kiri/mode/sla/work/slice.js similarity index 98% rename from src/kiri/mode/sla/slice.js rename to src/kiri/mode/sla/work/slice.js index f51caad5..2ec33eca 100644 --- a/src/kiri/mode/sla/slice.js +++ b/src/kiri/mode/sla/work/slice.js @@ -1,13 +1,13 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { util } from '../../../geo/base.js'; -import { slicer } from '../../../geo/slicer.js'; -import { newPolygon } from '../../../geo/polygon.js'; -import { polygons as POLY } from '../../../geo/polygons.js'; -import { newSlice, newTop } from '../../core/slice.js'; -import { layerProcessTops, layerDiff, projectFlats, projectBridges } from '../fdm/slice.js'; -import { PNG } from '../../../ext/pngjs.esm.js'; -import { SLA } from './driver.js'; +import { util } from '../../../../geo/base.js'; +import { slicer } from '../../../../geo/slicer.js'; +import { newPolygon } from '../../../../geo/polygon.js'; +import { polygons as POLY } from '../../../../geo/polygons.js'; +import { newSlice, newTop } from '../../../core/slice.js'; +import { layerProcessTops, layerDiff, projectFlats, projectBridges } from '../../fdm/work/slice.js'; +import { PNG } from '../../../../ext/pngjs.esm.js'; +import { SLA } from './init-work.js'; const tracker = util.pwait; diff --git a/src/kiri/mode/sla/x_cxdlp.js b/src/kiri/mode/sla/work/x_cxdlp.js similarity index 100% rename from src/kiri/mode/sla/x_cxdlp.js rename to src/kiri/mode/sla/work/x_cxdlp.js diff --git a/src/kiri/mode/sla/x_photon.js b/src/kiri/mode/sla/work/x_photon.js similarity index 99% rename from src/kiri/mode/sla/x_photon.js rename to src/kiri/mode/sla/work/x_photon.js index 968bfa5c..67af34ea 100644 --- a/src/kiri/mode/sla/x_photon.js +++ b/src/kiri/mode/sla/work/x_photon.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { SLA } from './driver.js'; +import { SLA } from './init-work.js'; function generatePhoton(print, conf, progress) { let printset = print.settings, diff --git a/src/kiri/mode/wedm/driver.js b/src/kiri/mode/wedm/init-work.js similarity index 89% rename from src/kiri/mode/wedm/driver.js rename to src/kiri/mode/wedm/init-work.js index 3342abfd..18905591 100644 --- a/src/kiri/mode/wedm/driver.js +++ b/src/kiri/mode/wedm/init-work.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { LASER, TYPE } from '../laser/driver.js'; +import { LASER, TYPE } from '../laser/init-work.js'; const DEG2RAD = Math.PI / 180; diff --git a/src/kiri/mode/wjet/driver.js b/src/kiri/mode/wjet/init-work.js similarity index 82% rename from src/kiri/mode/wjet/driver.js rename to src/kiri/mode/wjet/init-work.js index b11c54fd..0c8ac31c 100644 --- a/src/kiri/mode/wjet/driver.js +++ b/src/kiri/mode/wjet/init-work.js @@ -1,6 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { LASER, TYPE } from '../laser/driver.js'; +import { LASER, TYPE } from '../laser/init-work.js'; function init(worker) { // console.log({ WATERJET_INIT: worker }); diff --git a/src/kiri/run/minion.js b/src/kiri/run/minion.js index c125f38d..ab6edb7f 100644 --- a/src/kiri/run/minion.js +++ b/src/kiri/run/minion.js @@ -8,15 +8,15 @@ import '../../add/three.js'; import { base } from '../../geo/base.js'; import { codec, encode, encodePointArray } from '../core/codec.js'; -import { layerProcessTop } from '../mode/fdm/post.js'; +import { layerProcessTop } from '../mode/fdm/work/post.js'; import { newPoint } from '../../geo/point.js'; import { newWidget } from '../core/widget.js'; import { polygons as POLY } from '../../geo/polygons.js'; import { sliceZ, sliceConnect } from '../../geo/slicer.js'; -import { Slicer as cam_slicer } from '../mode/cam/slicer_cam.js'; -import { Slicer as topo_slicer } from '../mode/cam/slicer_topo.js'; -import { Probe, Trace, raster_slice } from '../mode/cam/topo3.js'; -import { Topo as Topo4, rotatePoints } from '../mode/cam/topo4.js'; +import { Slicer as cam_slicer } from '../mode/cam/work/slicer-cam.js'; +import { Slicer as topo_slicer } from '../mode/cam/work/slicer-topo.js'; +import { Probe, Trace, raster_slice } from '../mode/cam/work/topo3.js'; +import { Topo as Topo4, rotatePoints } from '../mode/cam/work/topo4.js'; import { wasm_ctrl } from '../../geo/wasm.js'; const clib = self.ClipperLib; diff --git a/src/kiri/run/worker.js b/src/kiri/run/worker.js index cb812002..cff94946 100644 --- a/src/kiri/run/worker.js +++ b/src/kiri/run/worker.js @@ -18,13 +18,13 @@ import { version } from '../../moto/license.js'; import { wasm_ctrl } from '../../geo/wasm.js'; import { Widget, newWidget } from '../core/widget.js'; -import { CAM } from '../mode/cam/driver-be.js'; -import { DRAG } from '../mode/drag/driver.js'; -import { FDM } from '../mode/fdm/driver-be.js'; -import { LASER } from '../mode/laser/driver.js'; -import { SLA } from '../mode/sla/driver.js'; -import { WEDM } from '../mode/wedm/driver.js'; -import { WJET } from '../mode/wjet/driver.js'; +import { CAM } from '../mode/cam/work/init-work.js'; +import { DRAG } from '../mode/drag/init-work.js'; +import { FDM } from '../mode/fdm/work/init-work.js'; +import { LASER } from '../mode/laser/init-work.js'; +import { SLA } from '../mode/sla/work/init-work.js'; +import { WEDM } from '../mode/wedm/init-work.js'; +import { WJET } from '../mode/wjet/init-work.js'; const { time } = util; const POOLPATH = "./minion.js"; From 8d6a2351f6e43468cc675f0367d5a399d13279ff Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 8 Jan 2026 17:03:19 -0500 Subject: [PATCH 133/154] rename cam driver as dispatch --- src/kiri/mode/cam/app/cl-flip.js | 2 +- src/kiri/mode/cam/app/cl-helical.js | 2 +- src/kiri/mode/cam/app/cl-hole.js | 2 +- src/kiri/mode/cam/app/cl-origin.js | 2 +- src/kiri/mode/cam/app/cl-surface.js | 2 +- src/kiri/mode/cam/app/cl-trace.js | 2 +- src/kiri/mode/cam/app/{driver.js => dispatch.js} | 0 7 files changed, 6 insertions(+), 6 deletions(-) rename src/kiri/mode/cam/app/{driver.js => dispatch.js} (100%) diff --git a/src/kiri/mode/cam/app/cl-flip.js b/src/kiri/mode/cam/app/cl-flip.js index 40dd842e..2b11cdf2 100644 --- a/src/kiri/mode/cam/app/cl-flip.js +++ b/src/kiri/mode/cam/app/cl-flip.js @@ -2,7 +2,7 @@ import { api } from '../../../app/api.js'; import { env, opAdd, opRender } from './init-ui.js'; -import { CAM } from './driver.js'; +import { CAM } from './dispatch.js'; import { clearTabs, restoreTabs } from './cl-tab.js'; import { updateStock } from './cl-stock.js'; diff --git a/src/kiri/mode/cam/app/cl-helical.js b/src/kiri/mode/cam/app/cl-helical.js index debbab7f..4472efe4 100644 --- a/src/kiri/mode/cam/app/cl-helical.js +++ b/src/kiri/mode/cam/app/cl-helical.js @@ -1,5 +1,5 @@ import { api } from '../../../app/api.js'; -import { CAM } from "./driver.js"; +import { CAM } from "./dispatch.js"; import { clearPops, env } from "./init-ui.js"; let alert, lastWidget; diff --git a/src/kiri/mode/cam/app/cl-hole.js b/src/kiri/mode/cam/app/cl-hole.js index 7c1c9d1a..46b2ab8f 100644 --- a/src/kiri/mode/cam/app/cl-hole.js +++ b/src/kiri/mode/cam/app/cl-hole.js @@ -2,7 +2,7 @@ import { api } from '../../../app/api.js'; import { env, clearPops } from './init-ui.js'; -import { CAM } from './driver.js'; +import { CAM } from './dispatch.js'; export let holeSelOn = false; export let lastSelHoles; diff --git a/src/kiri/mode/cam/app/cl-origin.js b/src/kiri/mode/cam/app/cl-origin.js index 625bfef5..9ebc6efc 100644 --- a/src/kiri/mode/cam/app/cl-origin.js +++ b/src/kiri/mode/cam/app/cl-origin.js @@ -2,7 +2,7 @@ import { api } from '../../../app/api.js'; import { env, clearPops } from './init-ui.js'; -import { CAM } from './driver.js'; +import { CAM } from './dispatch.js'; import { addbox, clearboxes } from '../../../app/boxes.js'; export let originSelectOn = false; diff --git a/src/kiri/mode/cam/app/cl-surface.js b/src/kiri/mode/cam/app/cl-surface.js index be831764..8560b452 100644 --- a/src/kiri/mode/cam/app/cl-surface.js +++ b/src/kiri/mode/cam/app/cl-surface.js @@ -2,7 +2,7 @@ import { api } from '../../../app/api.js'; import { env, clearPops } from './init-ui.js'; -import { CAM } from './driver.js'; +import { CAM } from './dispatch.js'; const DEG2RAD = Math.PI / 180; const RAD2DEG = 180 / Math.PI; diff --git a/src/kiri/mode/cam/app/cl-trace.js b/src/kiri/mode/cam/app/cl-trace.js index 7327bb71..45ca81d0 100644 --- a/src/kiri/mode/cam/app/cl-trace.js +++ b/src/kiri/mode/cam/app/cl-trace.js @@ -3,7 +3,7 @@ import { api } from '../../../app/api.js'; import { env, clearPops, isDark } from './init-ui.js'; import { parse } from '../../../../load/svg.js'; -import { CAM } from './driver.js'; +import { CAM } from './dispatch.js'; import { Layers } from '../../../app/layers.js'; import { Stack } from '../../../app/stack.js'; import { newPolygon } from '../../../../geo/polygon.js'; diff --git a/src/kiri/mode/cam/app/driver.js b/src/kiri/mode/cam/app/dispatch.js similarity index 100% rename from src/kiri/mode/cam/app/driver.js rename to src/kiri/mode/cam/app/dispatch.js From c9aa37831a4b3270960111f24320df879436d1a7 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 8 Jan 2026 19:34:56 -0500 Subject: [PATCH 134/154] fix more visual state anomalies --- src/kiri/app/conf/defaults.js | 1 + src/kiri/app/function.js | 2 +- src/kiri/app/init/sync.js | 36 ++++++++++++++-- src/kiri/app/keyboard.js | 2 +- src/kiri/app/local.js | 7 +++- src/kiri/app/state/visual.js | 4 +- src/kiri/app/view-mode.js | 17 ++++---- src/kiri/app/visuals.js | 70 +++++++++++++++---------------- src/kiri/app/widget.js | 77 ++++++++++++++++++++++++++++++----- 9 files changed, 151 insertions(+), 65 deletions(-) diff --git a/src/kiri/app/conf/defaults.js b/src/kiri/app/conf/defaults.js index 7867d695..f5be2e4c 100644 --- a/src/kiri/app/conf/defaults.js +++ b/src/kiri/app/conf/defaults.js @@ -1003,6 +1003,7 @@ export const conf = { widget: {}, // legacy localStorage settings (like octo print) local: { + 'model.opacity': 1.0, 'model.edges': false, 'cam.anim.trans': true, 'cam.anim.model': false, diff --git a/src/kiri/app/function.js b/src/kiri/app/function.js index ac3c4904..2a6f5e0d 100644 --- a/src/kiri/app/function.js +++ b/src/kiri/app/function.js @@ -290,7 +290,7 @@ function prepareSlices(callback, scale = 1, offset = 0) { if (scale === 1) { // clear wireframe const scheme = getColorScheme(); - widget.setWireframe(false, scheme.wireframe.color, scheme.wireframe.opacity); + widget._setWireframe(false, scheme.wireframe.color, scheme.wireframe.opacity); widget.setOpacity(camOrLaser ? scheme.views.SLICE.sliced_opacity ?? 0.2 : scheme.views.PREVIEW?.sliced_opacity ?? 0.0); widget.setColor(scheme.widget.deselected); api.hide.alert(alert); diff --git a/src/kiri/app/init/sync.js b/src/kiri/app/init/sync.js index b507c121..a5ed5915 100644 --- a/src/kiri/app/init/sync.js +++ b/src/kiri/app/init/sync.js @@ -54,6 +54,19 @@ export async function init_sync() { console.log({octoprint:ohost}); } + // One-time migration: Initialize model.opacity from existing ghosting state + if (api.local.getFloat('model.opacity') === null) { + const wireframeOpacity = api.local.getFloat('model.wireframe.opacity'); + const wireframe = api.local.getBoolean('model.wireframe', false); + + // If ghosting was enabled (wireframe off, opacity < 1), preserve it + if (!wireframe && wireframeOpacity !== null && wireframeOpacity < 1) { + api.local.set('model.opacity', wireframeOpacity); + } else { + api.local.set('model.opacity', 1.0); + } + } + // load workspace from url if (SETUP.wrk) { set_ctrl.import_url(`${proto}//${SETUP.wrk[0]}`, false); @@ -262,9 +275,26 @@ function setup_keybd_nav() { // rendering options $('render-edges').onclick = () => { api.view.set_edges({ toggle: true }); api.conf.save() }; - $('render-ghost').onclick = () => { api.view.set_wireframe(false, 0, api.view.is_arrange() ? 0.4 : 0.25); }; - $('render-wire').onclick = () => { api.view.set_wireframe(true, 0, api.space.is_dark() ? 0.25 : 0.5); }; - $('render-solid').onclick = () => { api.view.set_wireframe(false, 0, 1); }; + $('render-ghost').onclick = () => { + const opacity = api.view.is_arrange() ? 0.4 : 0.25; + api.view.set_wireframe(false); + api.visuals.set_opacity(opacity); + // Also save to old key for backwards compatibility + api.local.set('model.wireframe.opacity', opacity); + api.conf.save(); + }; + $('render-wire').onclick = () => { + api.view.set_wireframe(true, 0, api.space.is_dark() ? 0.25 : 0.5); + api.visuals.set_opacity(1.0); + api.conf.save(); + }; + $('render-solid').onclick = () => { + api.view.set_wireframe(false); + api.visuals.set_opacity(1.0); + // Also save to old key for backwards compatibility + api.local.set('model.wireframe.opacity', 1.0); + api.conf.save(); + }; $('mesh-export-stl').onclick = () => { settingsOps.export_objects('stl') }; $('mesh-export-obj').onclick = () => { settingsOps.export_objects('obj') }; $('mesh-merge').onclick = selection.merge; diff --git a/src/kiri/app/keyboard.js b/src/kiri/app/keyboard.js index c68df9f6..7d6e49fc 100644 --- a/src/kiri/app/keyboard.js +++ b/src/kiri/app/keyboard.js @@ -264,7 +264,7 @@ class KeyboardControl { let api = this.#api; let isWireframe = api.local.getBoolean('model.wireframe'); - let isGhost = api.local.getFloat('model.wireframe.opacity') < 1; + let isGhost = !isWireframe && (api.local.getFloat('model.opacity') ?? 1.0) < 1; switch (evt.keyCode) { case this.#cca('`'): this.#api.show.slices(0); break; diff --git a/src/kiri/app/local.js b/src/kiri/app/local.js index 9d5d3335..3afba384 100644 --- a/src/kiri/app/local.js +++ b/src/kiri/app/local.js @@ -38,7 +38,12 @@ export const local = { get: (key) => localGet(key), getItem: (key) => localGet(key), getInt: (key) => parseInt(localGet(key)), - getFloat: (key) => parseFloat(localGet(key)), + getFloat: (key) => { + const val = localGet(key); + if (val === undefined || val === null) return null; + const parsed = parseFloat(val); + return isNaN(parsed) ? null : parsed; + }, getBoolean: localGetBoolean, toggle: (key, val, def) => { return localSet(key, val ?? !localGetBoolean(key, def)); diff --git a/src/kiri/app/state/visual.js b/src/kiri/app/state/visual.js index 81807d0e..ca2f6429 100644 --- a/src/kiri/app/state/visual.js +++ b/src/kiri/app/state/visual.js @@ -190,8 +190,8 @@ class VisualStateManager { */ setVisualState({ edges, wires, opacity }) { this.widget.cache.vizstate = this.getVisualState(); - this.widget.setEdges(edges ?? false); - this.widget.setWireframe(wires ?? false); + this.widget._setEdges(edges ?? false); + this.widget._setWireframe(wires ?? false); if (opacity !== undefined) { this.widget.setOpacity(opacity); } diff --git a/src/kiri/app/view-mode.js b/src/kiri/app/view-mode.js index dac5e95d..dbb87dc9 100644 --- a/src/kiri/app/view-mode.js +++ b/src/kiri/app/view-mode.js @@ -54,28 +54,27 @@ function setViewMode(mode) { api.visuals.update_speeds(); api.visuals.set_visible_layer(); api.visuals.set_widget_visibility(true); - api.view.set_edges(api.local.getBoolean('model.edges')); - api.view.set_wireframe(api.local.getBoolean('model.wireframe')); - // Only set opacity to 1 if wireframe is disabled - if (!api.local.getBoolean('model.wireframe')) { - api.widgets.setOpacity(1); - } + // Restore global visual state (opacity, edges, wireframe) + api.widgets.each(w => w.applyGlobalVisualState()); break; case VIEWS.SLICE: $('act-slice').classList.add('selected'); api.visuals.update_speeds(); api.visuals.update_slider_max(); api.visuals.set_widget_visibility(true); - !isCAM && api.view.set_edges(false); + // Hide edges without persisting (preserve user preference) + !isCAM && api.view.set_edges(false, false); break; case VIEWS.PREVIEW: $('act-preview').classList.add('selected'); api.visuals.set_widget_visibility(true); - !isCAM && api.view.set_edges(false); + // Hide edges without persisting (preserve user preference) + !isCAM && api.view.set_edges(false, false); break; case VIEWS.ANIMATE: $('act-animate').classList.add('selected'); - !isCAM && api.view.set_edges(false); + // Hide edges without persisting (preserve user preference) + !isCAM && api.view.set_edges(false, false); break; default: console.log("invalid view mode: "+mode); diff --git a/src/kiri/app/visuals.js b/src/kiri/app/visuals.js index b34e8c41..a5f0af73 100644 --- a/src/kiri/app/visuals.js +++ b/src/kiri/app/visuals.js @@ -114,6 +114,25 @@ function setVisibleLayer(h, l) { showSlices(); } +/** + * Set global opacity for all widgets. + * Updates material opacity for all widgets and persists to localStorage. + * This is the primary opacity control - all widgets share the same opacity. + * @param {number} value - Opacity value (0.0 to 1.0) + */ +function setOpacity(value) { + api.local.set('model.opacity', value); + // Also update old key for backwards compatibility + api.local.set('model.wireframe.opacity', value); + api.widgets.each(w => { + const mat = w.getMaterial(); + mat.opacity = value; + mat.transparent = value < 1.0; + mat.visible = value > 0.0; + }); + space.update(); +} + /** * Set wireframe rendering mode for all widgets. * Updates 3D scene after applying to all widgets. @@ -144,7 +163,7 @@ function setWireframe(bool, color, opacity) { const saved = api.local.getFloat('model.wireframe.opacity'); opacity = saved !== null ? saved : (api.space.is_dark() ? 0.25 : 0.5); } - api.widgets.each((w) => { w.setWireframe(bool, color, opacity) }); + api.widgets.each((w) => { w._setWireframe(bool, color, opacity) }); space.update(); } @@ -154,15 +173,18 @@ function setWireframe(bool, color, opacity) { * Otherwise sets edge visibility to bool value. * Persists state to local storage and updates scene. * @param {boolean|object} bool - Enable/disable edges, or {toggle: true} to toggle + * @param {boolean} [persist=true] - Whether to save state to local storage */ -function setEdges(bool) { +function setEdges(bool, persist = true) { if (bool && bool.toggle) { api.local.toggle('model.edges'); + bool = api.local.getBoolean('model.edges'); } else { - api.local.set('model.edges', bool); + if (persist) { + api.local.set('model.edges', bool); + } } - bool = api.local.getBoolean('model.edges'); - api.widgets.each(w => w.setEdges(bool)); + api.widgets.each(w => w._setEdges(bool)); space.update(); } @@ -188,19 +210,8 @@ function updateSliderMax(set) { */ function hideSlices() { STACKS.clear(); - // Restore saved wireframe state instead of forcing wireframe off - const wireframe = api.local.getBoolean('model.wireframe'); - const color = api.local.getInt('model.wireframe.color') || 0; - const saved = api.local.getFloat('model.wireframe.opacity'); - const scheme = getColorScheme(); - const opacity = saved !== null ? saved : (scheme.views.ARRANGE?.model_opacity ?? 1.0); - - if (wireframe) { - api.widgets.each(widget => widget.setWireframe(wireframe, color, opacity)); - } else { - api.widgets.setOpacity(opacity); - api.widgets.each(widget => widget.setWireframe(false)); - } + // Restore global visual state + api.widgets.each(widget => widget.applyGlobalVisualState()); } /** @@ -284,7 +295,7 @@ function updateStackLabelState() { /** * Apply saved visual state to a widget. - * Restores edges and wireframe settings from local storage. + * Delegates to widget's applyGlobalVisualState method. * Only applies in ARRANGE view mode. * @param {Widget} widget - Widget to apply visual state to */ @@ -294,25 +305,7 @@ function applyVisualState(widget) { if (viewMode !== VIEWS.ARRANGE) { return; } - const edges = api.local.getBoolean('model.edges'); - const wireframe = api.local.getBoolean('model.wireframe'); - - // Always apply edges setting - if (edges) { - widget.setEdges(edges); - } - - // Apply wireframe and its associated opacity - const color = api.local.getInt('model.wireframe.color') || 0; - const saved = api.local.getFloat('model.wireframe.opacity'); - const opacity = saved !== null ? saved : 1; - - if (wireframe) { - widget.setWireframe(wireframe, color, opacity); - } else if (saved !== null) { - // Even if wireframe is off, restore the saved opacity - widget.setOpacity(opacity); - } + widget.applyGlobalVisualState(); } /** @@ -381,6 +374,7 @@ export const visuals = { hide_slices: hideSlices, hide_slider: hideSlider, set_edges: setEdges, + set_opacity: setOpacity, set_progress: setProgress, set_visible_layer: setVisibleLayer, set_widget_visibility: setWidgetVisibility, diff --git a/src/kiri/app/widget.js b/src/kiri/app/widget.js index a6ae0a8a..487fded3 100644 --- a/src/kiri/app/widget.js +++ b/src/kiri/app/widget.js @@ -148,9 +148,12 @@ class Widget extends WidgetCore { */ setVisualState({ edges, wires, opacity}) { this.cache.vizstate = this.getVisualState(); - this.setEdges(edges ?? false); - this.setWireframe(wires ?? false); - this.setOpacity(opacity ?? 1); + this._setEdges(edges ?? false); + this._setWireframe(wires ?? false); + // Only set opacity if explicitly provided (don't default to 1) + if (opacity !== undefined) { + this.setOpacity(opacity); + } } /** @@ -169,6 +172,39 @@ class Widget extends WidgetCore { } } + /** + * Apply global visual state from localStorage to this widget. + * Reads opacity, edges, and wireframe settings from global config. + * This is the primary method for syncing widget appearance with global preferences. + */ + applyGlobalVisualState() { + if (!this.api) { + return; + } + + const material = this.getMaterial(); + + // Apply global opacity + const opacity = this.api.local.getFloat('model.opacity') ?? 1.0; + material.opacity = opacity; + material.transparent = opacity < 1.0; + material.visible = opacity > 0.0; + + // Apply global edges + const edges = this.api.local.getBoolean('model.edges', false); + this._setEdges(edges); + + // Apply global wireframe + const wireframe = this.api.local.getBoolean('model.wireframe', false); + if (wireframe) { + const color = this.api.local.getInt('model.wireframe.color') || 0; + const wfOpacity = this.api.local.getFloat('model.wireframe.opacity') ?? 0.5; + this._setWireframe(true, color, wfOpacity); + } else { + this._setWireframe(false); + } + } + /** * Set material opacity */ @@ -185,16 +221,23 @@ class Widget extends WidgetCore { } } + /** + * Set edge rendering - public interface for core widget rotation/scale + * Delegates to internal _setEdges method + */ setEdges(set) { - clearTimeout(this._setimer); - if (set === true && this.outline) { - this._setimer = setTimeout(() => this._setEdges(set), 10); - } else { - this._setEdges(set); - } + this._setEdges(set); } + /** + * Set edge rendering (internal method - use api.view.set_edges for global control) + * @private + */ _setEdges(set) { + this.__setEdges(set); + } + + __setEdges(set) { if (!(this.api && this.api.conf)) { // missing api features in engine mode return; @@ -253,15 +296,26 @@ class Widget extends WidgetCore { } /** - * Set wireframe rendering + * Set wireframe rendering - public interface for core widget rotation/scale + * Delegates to internal _setWireframe method */ setWireframe(set, color, opacity) { + this._setWireframe(set, color, opacity); + } + + /** + * Set wireframe rendering (internal method - use api.view.set_wireframe for global control) + * @private + */ + _setWireframe(set, color, opacity) { if (!(this.api && this.api.conf)) { // missing api features in engine mode return; } let mesh = this.mesh, widget = this; + // Save current opacity before removing wire + const currentOpacity = this.getMaterial().opacity; if (this.wire) { this.setOpacity(solid_opacity); mesh.remove(this.wire); @@ -287,6 +341,9 @@ class Widget extends WidgetCore { } if (opacity !== undefined) { widget.setOpacity(opacity); + } else if (!set && this.wire === null) { + // Restore original opacity when removing wireframe without explicit opacity + widget.setOpacity(currentOpacity); } } From 9f74b8439a9d9b2ac4ef2eb8557fdd62475b9b4d Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 8 Jan 2026 20:18:56 -0500 Subject: [PATCH 135/154] fix widget cloning --- src/kiri/core/widget.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/kiri/core/widget.js b/src/kiri/core/widget.js index 26721942..c518ac29 100644 --- a/src/kiri/core/widget.js +++ b/src/kiri/core/widget.js @@ -237,6 +237,8 @@ class Widget { ]); mesh.renderOrder = 1; // geometry.computeVertexNormals(); + // Clear existing groups (e.g., from cloned geometry) before adding new one + geometry.clearGroups(); geometry.addGroup(0, Infinity, 0); // mesh.castShadow = true; // mesh.receiveShadow = true; From d51894fb75dbcaa306711268e0e28527f1dc0453 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 8 Jan 2026 23:44:31 -0500 Subject: [PATCH 136/154] add send to moonraker option --- src/kiri/app/export.js | 60 ++++++++++++++++++++++++-------------- src/kiri/app/init/input.js | 6 ++-- web/kiri/index.html | 38 +++++++++++++++++++----- web/kiri/lang/en.js | 11 ++++--- 4 files changed, 79 insertions(+), 36 deletions(-) diff --git a/src/kiri/app/export.js b/src/kiri/app/export.js index 999795ef..fa6604ef 100644 --- a/src/kiri/app/export.js +++ b/src/kiri/app/export.js @@ -192,6 +192,7 @@ function bindField(field, varname) { console.log('save', field, 'to', varname); localSet(varname, $(field).value.trim()); }; + $(field).onchange = $(field).onblur; } /** @@ -219,12 +220,7 @@ function exportGCodeDialog(gcode, sections, info, names) { codeproc = settings.device.gcodeProc, octo_host, octo_apik, - grid_host, - grid_apik, - grid_target, - grid_targets = {}, - grid_local, - grid_uuid; + octo_type; // join gcode array into a string gcode = gcode.join('\r\n') + '\r\n'; @@ -240,13 +236,18 @@ function exportGCodeDialog(gcode, sections, info, names) { {type:"application/octet-stream"}); } - function sendto_octoprint() { - if (!(octo_host && octo_apik)) return; + function printToHost() { + sendToHost(true); + } + + function sendToHost(start) { + if (!(octo_host && octo_apik && octo_type)) return; let form = new FormData(), ajax = new XMLHttpRequest(), host = octo_host.value.toLowerCase(), - apik = octo_apik.value; + apik = octo_apik.value, + type = octo_type.value; if (host.indexOf("http") !== 0) { api.show.alert("host missing protocol (http:// or https://)"); @@ -259,26 +260,37 @@ function exportGCodeDialog(gcode, sections, info, names) { localSet('octo-host', host.trim()); localSet('octo-apik', apik.trim()); + localSet('octo-type', type.trim()); filename = $('print-filename').value; form.append("file", getBlob(), filename+"."+fileext); + // moonraker upload params + // form.append("root", "gcodes"); // optional but recommended + // form.append("path", ""); // optional subfolder + if (start) form.append("print", "true"); // <-- auto-start ajax.onreadystatechange = function() { + console.log({ ajax_readyState: ajax.readyState }); if (ajax.readyState === 4) { let status = ajax.status; - api.stats.add(`ua_${api.mode.get_lower()}_print_octo_${status}`); + api.stats.add(`ua_${api.mode.get_lower()}_print_send_${status}`); if (status >= 200 && status < 300) { api.modal.hide(); } else { - api.show.alert("octoprint error\nstatus: "+status+"\nmessage: "+ajax.responseText); + api.show.alert("send error\nstatus: "+status+"\nmessage: "+ajax.responseText); } api.show.progress(0); } api.show.progress(0); }; - ajax.upload.addEventListener('progress', function(evt) { + ajax.upload.addEventListener('progress', (evt) => { + console.log({ ajax_progress: evt }); api.show.progress(evt.loaded/evt.total, "sending"); }); - ajax.open("POST", host+"/api/files/local"); + if (type === 'moonraker') { + ajax.open("POST", host + "/server/files/upload"); + } else { + ajax.open("POST", host + "/api/files/local"); + } if (apik) { ajax.setRequestHeader("X-Api-Key", apik); } @@ -349,6 +361,7 @@ function exportGCodeDialog(gcode, sections, info, names) { // persist fields when changed bindField('octo-host', 'octo-host'); bindField('octo-apik', 'octo-apik'); + bindField('octo-type', 'octo-type'); // in cam mode, show zip file option let downloadZip = $('print-zip'); @@ -550,21 +563,23 @@ function exportGCodeDialog(gcode, sections, info, names) { }) }; - // octoprint setup - $('send-to-octohead').style.display = octo ? '' : 'none'; - $('send-to-octoprint').style.display = octo ? '' : 'none'; + // remote host (klipper fluidd / octoprint) + $('send-remote-head').style.display = octo ? '' : 'none'; + $('send-remote-print').style.display = octo ? '' : 'none'; if (octo) try { - $('print-octoprint').onclick = sendto_octoprint; + $('send-remote').onclick = sendToHost; + $('print-remote').onclick = printToHost; octo_host = $('octo-host'); octo_apik = $('octo-apik'); + octo_type = $('octo-type'); if (MODE === MODES.CAM) { - $('send-to-octohead').style.display = 'none'; - $('send-to-octoprint').style.display = 'none'; + $('send-remote-head').style.display = 'none'; + $('send-remote-print').style.display = 'none'; } else { - $('send-to-octohead').style.display = ''; - $('send-to-octoprint').style.display = ''; + $('send-remote-head').style.display = ''; + $('send-remote-print').style.display = ''; } - // hide octoprint when hard-coded in the url + // hide remote host option when hard-coded in the url if (api.const.OCTO) { $('ophost').style.display = 'none'; $('opapik').style.display = 'none'; @@ -572,6 +587,7 @@ function exportGCodeDialog(gcode, sections, info, names) { } octo_host.value = localGet('octo-host') || ''; octo_apik.value = localGet('octo-apik') || ''; + octo_type.value = localGet('octo-type') || ''; } catch (e) { console.log(e) } // preview of the generated GCODE (first 64k max) diff --git a/src/kiri/app/init/input.js b/src/kiri/app/init/input.js index 05eef309..040e2dce 100644 --- a/src/kiri/app/init/input.js +++ b/src/kiri/app/init/input.js @@ -308,9 +308,9 @@ export function init_input() { spaceRandoX: newBoolean(LANG.op_spcx_s, preferences.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}), - exportOcto: newBoolean(`OctoPrint`, preferences.booleanSave, {title:LANG.op_exop_l}), - exportThumb: newBoolean(`Thumbnail`, preferences.booleanSave, {modes:FDM}), - exportPreview: newBoolean(`Code Preview`, preferences.booleanSave), + exportPreview: newBoolean(LANG.op_excp_s, preferences.booleanSave, {title:LANG.op_excp_l}), + exportOcto: newBoolean(LANG.op_exop_s, preferences.booleanSave, {title:LANG.op_exop_l}), + exportThumb: newBoolean(LANG.op_exth_s, preferences.booleanSave, {title:LANG.op_exth_l, modes:FDM}), _____: newGroup(LANG.pt_menu, $('prefs-prt'), {inline}), detail: newSelect(LANG.pt_qual_s, {title: LANG.pt_qual_l, action: preferences.detailSave}, "detail"), healMesh: newBoolean(LANG.pt_heal_s, preferences.booleanSave, {title: LANG.pt_heal_l}), diff --git a/web/kiri/index.html b/web/kiri/index.html index c90fac76..46e2ca1c 100644 --- a/web/kiri/index.html +++ b/web/kiri/index.html @@ -678,19 +678,39 @@ - +
-
-
+
+
-
-
+
+ + +
+
+ + +
+
+ + +
- + + +
+
+
-
this project needs your support! @@ -757,3 +777,7 @@
+
+ + +
diff --git a/web/kiri/lang/en.js b/web/kiri/lang/en.js index 77bd7e5e..a292599a 100644 --- a/web/kiri/lang/en.js +++ b/web/kiri/lang/en.js @@ -324,9 +324,12 @@ self.lang['en-us'] = { op_anim_l: ["animation mesh density","higher is more dense","takes more memory","and is slower"], op_mani_s: "manifold", op_mani_l: ["in stock indexed mode","use boolean visualiation","this is very expensive","and likely very slow"], - op_exgl_l: ["show Grid:Local devices in export dialog","includes Grid:Bots and OctoPrint plugins"], - op_exgh_l: ["legacy Grid:Host local network support"], - op_exop_l: ["legacy OctoPrint local network support","Grid:Space OctoPrint plugin recommended"], + op_exop_s: "remote host", + op_exop_l: ["enable sending files to remote hosts","moonraker = klipper","octoprint"], + op_excp_s: "code preview", + op_excp_l: ["show start of gcode","in export dialog"], + op_exth_s: "thumbnail", + op_exth_l: ["embed print thumbnail in gcode"], lo_menu: "layout", @@ -1014,7 +1017,7 @@ self.lang['en-us'] = { ex_gcpv: "gcode preview", ex_dwnl: "download", ex_bamb: "send to bambu", - ex_octo: "octoprint", + ex_octo: "remote host", ex_host: "host", ex_akey: "key", ex_rsnm: "resin used ML", From 7e92845662a9b76938788b61fecd18ebab74bf3f Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 9 Jan 2026 15:18:44 -0500 Subject: [PATCH 137/154] fix arc detect radian / degree add polygon thickness() ratio helper add fdm slice delta thickness filter parameter add fdm print to moonraker auto-start option fix fdm finish speed ratio for flats and bridges reflow fdm slice.js code for better readability --- src/geo/poly/arcs.js | 13 +- src/geo/polygon.js | 15 + src/kiri/app/conf/defaults.js | 1 + src/kiri/app/export.js | 18 +- src/kiri/core/print.js | 18 +- src/kiri/mode/fdm/app/init-menu.js | 1 + src/kiri/mode/fdm/app/init-ui.js | 1 + src/kiri/mode/fdm/work/prepare.js | 12 +- src/kiri/mode/fdm/work/slice.js | 755 +++++++++++++++-------------- web/kiri/index.html | 4 - web/kiri/lang/en.js | 6 +- 11 files changed, 442 insertions(+), 402 deletions(-) diff --git a/src/geo/poly/arcs.js b/src/geo/poly/arcs.js index 8697a6d6..2c67efb5 100644 --- a/src/geo/poly/arcs.js +++ b/src/geo/poly/arcs.js @@ -115,19 +115,20 @@ export function findArcCenters(poly, opt = {}) { * @param {Polygon} poly - polygon to analyze (will be mutated with arc annotations) * @param {Object} opts - detection options * @param {number} opts.tolerance - arc detection tolerance (default 0.1) - * @param {number} opts.arcRes - arc resolution in radians (default 0.1) + * @param {number} opts.arcRes - arc resolution in degrees (default 1) * @param {number} opts.minPoints - minimum points to consider an arc (default 4) * @returns {Polygon} this polygon with arc annotations */ export function detectArcs(poly, opts = {}) { const { tolerance = 0.1, - arcRes = 0.1, + arcRes = 1, minPoints = 4 } = opts; const points = poly.points; const length = points.length; + const arcResRadians = arcRes * (Math.PI / 180); if (length < minPoints) { return poly; @@ -143,7 +144,7 @@ export function detectArcs(poly, opts = {}) { // because we need at least minPoints to form an arc while (i < length - minPoints + 1) { // Try to detect arc starting at position i - const arcData = detectArcAt(poly, i, points, length, tolerance, arcRes, minPoints); + const arcData = detectArcAt(poly, i, points, length, tolerance, arcResRadians, minPoints); if (arcData) { // Annotate the starting point @@ -172,7 +173,7 @@ export function detectArcs(poly, opts = {}) { * @param {number} minPoints - minimum points for arc * @returns {Object|null} arc data or null if no arc detected */ -export function detectArcAt(poly, startIdx, points, length, tolerance, arcRes, minPoints) { +function detectArcAt(poly, startIdx, points, length, tolerance, arcRes, minPoints) { // Greedy approach: collect as many points as possible that might form an arc let candidates = []; @@ -222,7 +223,7 @@ export function detectArcAt(poly, startIdx, points, length, tolerance, arcRes, m * @param {number} minPoints - minimum points for arc * @returns {Object|null} arc data or null if invalid */ -export function validateArc(poly, arcPoints, tolerance, arcRes, minPoints) { +function validateArc(poly, arcPoints, tolerance, arcRes, minPoints) { if (arcPoints.length < minPoints) { return null; } @@ -295,7 +296,7 @@ export function validateArc(poly, arcPoints, tolerance, arcRes, minPoints) { * @param {number} tolerance - detection tolerance * @returns {Object|null} center with x, y, r properties or null */ -export function findBestCenter(arcPoints, tolerance) { +function findBestCenter(arcPoints, tolerance) { const len = arcPoints.length; // Use 3 well-spaced points for initial center calculation diff --git a/src/geo/polygon.js b/src/geo/polygon.js index add36b53..198b7f9b 100644 --- a/src/geo/polygon.js +++ b/src/geo/polygon.js @@ -1221,6 +1221,21 @@ export class Polygon { return a; } + /** + * find the ratio of area to perimeter which can give provide + * guidance if this polygon is likely to have a thin cross-section + * + * @param {*} deep whether to include inner holes in the calculation + * @returns ratio + */ + thickness(deep) { + if (deep) { + return 2 * this.areaDeep() / this.perimeterDeep(); + } else { + return 2 * this.area() / this.perimeter(); + } + } + /** * @param {Polygon} poly * @returns {boolean} diff --git a/src/kiri/app/conf/defaults.js b/src/kiri/app/conf/defaults.js index f5be2e4c..6ccd3143 100644 --- a/src/kiri/app/conf/defaults.js +++ b/src/kiri/app/conf/defaults.js @@ -337,6 +337,7 @@ export const conf = { sliceShells: 3, sliceSolidify: false, sliceSolidMinArea: 1, + sliceSolidMinThick: 0.4, sliceSupportAngle: 50, sliceSupportDensity: 0.1, sliceSupportExtra: 0, diff --git a/src/kiri/app/export.js b/src/kiri/app/export.js index fa6604ef..1479f36f 100644 --- a/src/kiri/app/export.js +++ b/src/kiri/app/export.js @@ -262,12 +262,8 @@ function exportGCodeDialog(gcode, sections, info, names) { localSet('octo-apik', apik.trim()); localSet('octo-type', type.trim()); - filename = $('print-filename').value; - form.append("file", getBlob(), filename+"."+fileext); - // moonraker upload params - // form.append("root", "gcodes"); // optional but recommended - // form.append("path", ""); // optional subfolder - if (start) form.append("print", "true"); // <-- auto-start + filename = $('print-filename').value + "." + fileext; + form.append("file", getBlob(), filename); ajax.onreadystatechange = function() { console.log({ ajax_readyState: ajax.readyState }); if (ajax.readyState === 4) { @@ -279,6 +275,16 @@ function exportGCodeDialog(gcode, sections, info, names) { api.show.alert("send error\nstatus: "+status+"\nmessage: "+ajax.responseText); } api.show.progress(0); + if (start && type === 'moonraker') { + fetch(`${host}/printer/print/start`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ filename }) + }) + .then(r => r.json()) + .then(data => console.log('print start', data)) + .catch(err => console.error('print start error', err)) + } } api.show.progress(0); }; diff --git a/src/kiri/core/print.js b/src/kiri/core/print.js index d0243ead..a0d59783 100644 --- a/src/kiri/core/print.js +++ b/src/kiri/core/print.js @@ -133,7 +133,7 @@ class Print { scarf = !poly.open ? (options.scarf ?? 0) : false, last = startPoint, first = true, - { arcOpts, tool, zmax } = options; + { arcOpts, tool, zmax, onfirst, onfirstout } = options; // if short, use calculated print speed based on sliding scale if (perimeter < process.outputShortPoly) { @@ -169,7 +169,7 @@ class Print { if (scarf) { let epz = Math.max(...poly.points.map(p => p.z)); let spz = startPoint.z; - poly = poly.segment(options.nozzleSize ?? 0.4, false, false, scarf * 2); + poly = poly.segment(nozzleSize ?? 0.4, false, false, scarf * 2); pp = poly.points; let lp, sp = []; for (let p of pp) { @@ -196,6 +196,7 @@ class Print { } else if (arcOpts) { // annotate arc points pp = newPolygon(pp).detectArcs(arcOpts).points; + // console.log(arcOpts, pp.map(p => p.arc?.skip).filter(v => v)); } // scarf manages its own close point @@ -210,10 +211,13 @@ class Print { let { arc } = point; if (arc) { arcInfo = arc; - scope.addOutput(output, point, shellMult, moveSpeed, tool); + if (first && onfirst) onfirst(point, output); + let out = scope.addOutput(output, point, shellMult, moveSpeed, tool); + if (first && onfirstout) onfirstout(out); // arc center is relative to first point arcInfo.center.move({ x: -point.x, y: -point.y, z: 0 }); last = point; + if (first) first = false; continue; } else if (arcInfo) { // accumulate and pass to last point to calc FDM E value @@ -235,14 +239,10 @@ class Print { scope.addOutput(output, point, 0, moveSpeed, tool); } else if (first) { // if (point.skip) console.log({ skip: point }); - if (options.onfirst) { - options.onfirst(point, output); - } + onfirst && onfirst(point, output); // move to first output point on poly let out = scope.addOutput(output, point, 0, moveSpeed, tool); - if (options.onfirstout) { - options.onfirstout(out); - } + onfirstout && onfirstout(out); first = false; } else { let seglen = last.distTo2D(point); diff --git a/src/kiri/mode/fdm/app/init-menu.js b/src/kiri/mode/fdm/app/init-menu.js index 799ace08..d0dbd6cb 100644 --- a/src/kiri/mode/fdm/app/init-menu.js +++ b/src/kiri/mode/fdm/app/init-menu.js @@ -101,6 +101,7 @@ export function menu() { sliceFillAngle: newInput(LANG.fi_angl_s, {title:LANG.fi_angl_l, convert:toFloat}), 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}), + sliceSolidMinThick: newInput(LANG.ad_mthk_s, {title:LANG.ad_mthk_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}), diff --git a/src/kiri/mode/fdm/app/init-ui.js b/src/kiri/mode/fdm/app/init-ui.js index ab590713..3576ecb5 100644 --- a/src/kiri/mode/fdm/app/init-ui.js +++ b/src/kiri/mode/fdm/app/init-ui.js @@ -30,6 +30,7 @@ export function init() { "sliceFillSparse": LANG.fi_pcnt_s, "sliceFillGrow": LANG.fi_grow_s, "sliceSolidMinArea": LANG.ad_msol_s, + "sliceSolidMinThick": LANG.ad_mthk_s, "sliceTopLayers": LANG.sl_ltop_s, "sliceBottomLayers": LANG.sl_lbot_s, "firstLayerRate": LANG.fl_rate_s, diff --git a/src/kiri/mode/fdm/work/prepare.js b/src/kiri/mode/fdm/work/prepare.js index 9058f3ab..82575f0f 100644 --- a/src/kiri/mode/fdm/work/prepare.js +++ b/src/kiri/mode/fdm/work/prepare.js @@ -541,7 +541,6 @@ export async function fdm_prepare(widgets, settings, update) { }, routeAround: process.outputAvoidGaps, seedPoint: printPoint.sub(offset), - support: slice.supports, zmax, } ); @@ -780,7 +779,6 @@ 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, @@ -790,7 +788,7 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { minLayerTime = process.outputMinLayerTime || 0, nozzleSize = process.sliceLineWidth || device.extruders[extruder].extNozzle, nozzles = device.extruders.map(e => e.extNozzle), - firstLayer = (opt.first || false) && !opt.support, + firstLayer = (opt.first || false), thinWall = nozzleSize * (opt.thinWall || 1.75), retractDist = opt.retractOver || (nozzleSize * 5), fillMult = opt.mult || process.outputFillMult, @@ -845,10 +843,12 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { printSpeed = finishSpeed || printSpeed; } - // override: adapt bridge layer speed - if (slice.isBridgeLayer) { + // override: adapt bridge layer speed and flats + // but not on the first layer which has it's own settings + if (slice.index > 0 && (slice.isBridgeLayer || slice.isFlatsLayer)) { fillSpeed /= 2; printSpeed /= 2; + finishSpeed /= 2; } function retract() { @@ -1444,7 +1444,7 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { if (slice.tops) { out.appendAll(slice.tops); } - if (opt.support && slice.supports) { + if (slice.supports) { out.appendAll(slice.supports); } diff --git a/src/kiri/mode/fdm/work/slice.js b/src/kiri/mode/fdm/work/slice.js index 47f6a4f5..f3f122b4 100644 --- a/src/kiri/mode/fdm/work/slice.js +++ b/src/kiri/mode/fdm/work/slice.js @@ -63,6 +63,7 @@ const CONSTANTS = { * @property {number} sliceFillGrow - Fill boundary expansion in mm * @property {number} sliceFillOverlap - Fill overlap with shells (0.0-0.8) * @property {number} sliceSolidMinArea - Minimum area in mm² for solid regions + * @property {number} sliceSolidMinThick - Miminum "thickness" ratio for culling thin solids * @property {number} sliceSupportDensity - Support fill density (0.0-1.0) * @property {number} sliceSupportFill - Support fill angle (or auto if >= 1000) * @property {number} sliceSupportGap - Gap between support and part in mm @@ -266,6 +267,7 @@ export function sliceOne(settings, widget, onupdate, ondone) { let indices = []; let heights = []; let healed = false; + let slices; // set by decodeSlices() // handle z cutting (floor method) and base flattening let zPress = isBelt ? process.firstLayerFlatten || 0 : 0; @@ -328,35 +330,7 @@ export function sliceOne(settings, widget, onupdate, ondone) { .then(processSlices) .then(ondone); - // turn slicer raw output into Slice objects - function decodeSlices(output) { - // post process slices and re-incorporate missing meta-data - return output.slices.map(data => { - let { z, clip, lines, groups, changes } = data; - if (!data.tops) return null; - let slice = newSlice(z).addTops(data.tops, { minArea: CONSTANTS.MIN_POLY_AREA }); - slice.index = indices.indexOf(z); - slice.height = heights[slice.index]; - slice.clips = clip; - // do not warn on merging supports - if (changes) { - healed = true; - slice.changes = changes; - if (devel) { - console.log('slice healed', slice.index, slice.z, changes); - } - } - if (process.xray) { - slice.index = process.xray.shift(); - slice.lines = lines; - slice.groups = groups; - slice.xray = slice.index; - } - return slice; - }).filter(s => s); - } - - // z index generator + // z index generator (bottom up) function zGen(zopt) { if (process.xray) { return zopt.zIndexes; @@ -438,321 +412,365 @@ export function sliceOne(settings, widget, onupdate, ondone) { return zi; } - async function processSlices(slices) { - /** - * Process automatic and manual shadow-based support generation - */ - async function processSupports() { - if (process.sliceSupportType === 'disabled') { - return; + // turn slicer raw output into Slice objects + // z index order should be bottom up at this point + async function decodeSlices(output) { + // post process slices and re-incorporate missing meta-data + slices = output.slices.map(data => { + let { z, clip, lines, groups, changes } = data; + if (!data.tops) return null; + let slice = newSlice(z).addTops(data.tops, { minArea: CONSTANTS.MIN_POLY_AREA }); + slice.index = indices.indexOf(z); + slice.height = heights[slice.index]; + slice.clips = clip; + // do not warn on merging supports + if (changes) { + healed = true; + slice.changes = changes; + if (devel) { + console.log('slice healed', slice.index, slice.z, changes); + } } + if (process.xray) { + slice.index = process.xray.shift(); + slice.lines = lines; + slice.groups = groups; + slice.xray = slice.index; + } + return slice; + }).filter(s => s); + } - let stack = slices.slice(); - let indices = stack.map(s => s.z); - let zAngNorm = Math.sin(process.sliceSupportAngle * Math.PI / 180); - let manual = process.sliceSupportType === 'manual'; + // calculate % complete and call onupdate() + function doupdate(index, from, to, msg) { + trackupdate(index / slices.length, from, to, msg); + } - // sort bottom up so shadows do not accumulate - // since that is done later and clipped to slice.clips - stack.sort((a,b) => a.z - b.z); + // slicing is the first 50% of the update "time" + function trackupdate(pct, from, to, msg) { + onupdate(0.5 + (from + (pct * (to - from))) * 0.5, msg); + } - // process manual supports if they exist - // convert paint points to circles on matching slices - let { paint } = widget.anno; - if (manual && paint?.length) { - let hpi = Math.PI/2; - for (let slice of stack) { - let polys = []; - for (let rec of paint) { - let { point, radius } = rec; - let dz = Math.abs(slice.z - point.z); - if (dz < radius) { - // scale radius by distance from point.z - radius = (Math.acos(dz/radius) / hpi) * radius; - polys.push(newPolygon().centerCircle(point, radius, 10)); - } + // for each slice, performe a function and call doupdate() + function forSlices(from, to, fn, msg) { + slices.forEach(slice => { + fn(slice); + doupdate(slice.index, from, to, msg) + }); + } + + /** + * Process automatic and manual shadow-based support generation + */ + async function processSupports() { + if (process.sliceSupportType === 'disabled') { + return; + } + + let stack = slices.slice(); + let indices = stack.map(s => s.z); + let zAngNorm = Math.sin(process.sliceSupportAngle * Math.PI / 180); + let manual = process.sliceSupportType === 'manual'; + + // sort bottom up so shadows do not accumulate + // since that is done later and clipped to slice.clips + stack.sort((a,b) => a.z - b.z); + + // process manual supports if they exist + // convert paint points to circles on matching slices + let { paint } = widget.anno; + if (manual && paint?.length) { + let hpi = Math.PI/2; + for (let slice of stack) { + let polys = []; + for (let rec of paint) { + let { point, radius } = rec; + let dz = Math.abs(slice.z - point.z); + if (dz < radius) { + // scale radius by distance from point.z + radius = (Math.acos(dz/radius) / hpi) * radius; + polys.push(newPolygon().centerCircle(point, radius, 10)); } - slice.shadow = POLY.union(polys, 0, true); } + slice.shadow = POLY.union(polys, 0, true); } - - // create automatic shadows supports when on manual paint - if (!manual) { - await widget.computeShadowStack(indices, progress => { - trackupdate(progress, 0.05, 0.10, "shadow"); - }, zAngNorm); - - for (let slice of stack) { - slice.shadow = await widget.shadowAt(slice.z, true); - } - } - - // 1. accumulate / union shadow coverage top down - // 2. trim to area outside slice.clips - let minArea = lineWidth; - let shadowSum; - let length = stack.length; - let count = 0; - - // perform accumulation top down - for (let slice of stack.reverse()) { - let shadow = slice.shadow ?? []; - if (process.sliceSupportExtra) { - shadow = POLY.offset(shadow, process.sliceSupportExtra); - } - // shadows accumulate down - if (shadowSum) { - shadow = POLY.union([...shadow, ...shadowSum], minArea, true); - } - // trim to slice.clips - if (true) { - let rem = []; - let clips = [ - slice.up?.clips, - slice.clips, - slice.down?.clips - ].filter(v => v).flat(); - clips = POLY.union(clips, minArea, true); - POLY.subtract(shadow, clips, rem, null, slice.z, minArea, { wasm: false }); - shadow = rem; - } - // pump shadow to clean to clean it up - if (true) { - let bump = lineWidth * 2; - shadow = POLY.offset(shadow, bump, { z: slice.z }); - shadow = POLY.offset(shadow, -bump, { z: slice.z }); - } - shadowSum = shadow; - slice.supports = shadow; - slice.output().setLayer("shadow", 0xff0000).addPolys(shadow); - trackupdate((++count/length), 0.10, 0.15, "support"); - } - } - /** - * Process top and bottom layers or any other - * layers detected and marked for solid fill - */ - function processSolidLayers() { - forSlices(0.15, 0.2, slice => { - let range = slice.params; - let isBottom = slice.index < bottomLayers; - let isTop = topLayers && slice.index > slices.length - topLayers - 1; - let isTopBase = isTop && slice.index === slices.length - topLayers; - let isDense = range.sliceFillSparse > CONSTANTS.DENSE_INFILL_THRESHOLD; - let isSolid = (isBottom || ((isTop || isDense) && !vaseMode)); - let solidWidth = isSolid ? range.sliceFillWidth || 1 : 0; - if (solidWidth) { - let fillSpace = fillSpacing * solidWidth; - layerMakeSolid({ slice, spacing: fillSpace, angle: sliceFillAngle }); - } - if (slice.index === slices.length - 1) { - slice.isFlatsLayer = true; - } - if (isTopBase) { - // mark the first top solid supporting layer as a bridge - slice.isBridgeLayer = true; - } - sliceFillAngle = (sliceFillAngle + 90.0) % 360; - }, "solid layers"); + // create automatic shadows supports when on manual paint + if (!manual) { + await widget.computeShadowStack(indices, progress => { + trackupdate(progress, 0.05, 0.10, "shadow"); + }, zAngNorm); + + for (let slice of stack) { + slice.shadow = await widget.shadowAt(slice.z, true); + } } - /** - * Process layer diffs and project solid areas - */ - function processLayerDiffs() { - // boolean diff layers to detect bridges and flats - profileStart("delta"); - forSlices(0.2, 0.34, slice => { - let params = slice.params || process; - let solidMinArea = params.sliceSolidMinArea; - let sliceFillGrow = params.sliceFillGrow; - layerDiff(slice, { min: solidMinArea, grow: sliceFillGrow }); - }, "layer deltas"); - profileEnd(); - // project bridges and flats up and down into part - profileStart("delta-project"); - forSlices(0.34, 0.35, slice => { - let params = slice.params || process; - topLayers = params.sliceTopLayers || 0; - bottomLayers = params.sliceBottomLayers || 0; - if (topLayers) projectFlats(slice, topLayers); - if (bottomLayers) projectBridges(slice, bottomLayers); - if (slice.flats) POLY.setZ(slice.flats, slice.z); - }, "layer deltas"); - profileEnd(); - } + // 1. accumulate / union shadow coverage top down + // 2. trim to area outside slice.clips + let minArea = lineWidth; + let shadowSum; + let length = stack.length; + let count = 0; - /** - * Process solid fill patterns - */ - async function processSolidFills() { - profileStart("solid-fill") - let promises = isConcurrent ? [] : undefined; - forSlices(0.35, promises ? 0.4 : 0.5, slice => { - let params = slice.params || process; - let solidWidth = params.sliceFillWidth || 1; + // perform accumulation top down + for (let slice of stack.reverse()) { + let shadow = slice.shadow ?? []; + if (process.sliceSupportExtra) { + shadow = POLY.offset(shadow, process.sliceSupportExtra); + } + // shadows accumulate down + if (shadowSum) { + shadow = POLY.union([...shadow, ...shadowSum], minArea, true); + } + // trim to slice.clips + if (true) { + let rem = []; + let clips = [ + slice.up?.clips, + slice.clips, + slice.down?.clips + ].filter(v => v).flat(); + clips = POLY.union(clips, minArea, true); + POLY.subtract(shadow, clips, rem, null, slice.z, minArea, { wasm: false }); + shadow = rem; + } + // pump shadow to clean to clean it up + if (true) { + let bump = lineWidth * 2; + shadow = POLY.offset(shadow, bump, { z: slice.z }); + shadow = POLY.offset(shadow, -bump, { z: slice.z }); + } + shadowSum = shadow; + slice.supports = shadow; + slice.output().setLayer("shadow", 0xff0000).addPolys(shadow); + trackupdate((++count/length), 0.10, 0.15, "support"); + } + } + + /** + * Process top and bottom layers or any other + * layers detected and marked for solid fill + */ + async function processSolidLayers() { + forSlices(0.15, 0.2, slice => { + let range = slice.params; + let isBottom = slice.index < bottomLayers; + let isTop = topLayers && slice.index > slices.length - topLayers - 1; + let isDense = range.sliceFillSparse > CONSTANTS.DENSE_INFILL_THRESHOLD; + let isSolid = (isBottom || ((isTop || isDense) && !vaseMode)); + let solidWidth = isSolid ? range.sliceFillWidth || 1 : 0; + if (solidWidth) { let fillSpace = fillSpacing * solidWidth; - let solidMinArea = params.sliceSolidMinArea; - layerFillSolids({ slice, spacing: fillSpace, angle: sliceFillAngle, minArea: solidMinArea, promises }); - sliceFillAngle = (sliceFillAngle + 90.0) % 360; - }, "fill solids"); - // very last layer (top) is set to finish solid rate - slices.last().finishSolids = true - if (promises) { - await tracker(promises, (i, t) => { - trackupdate(i / t, 0.4, 0.5); - }); + layerMakeSolid({ slice, spacing: fillSpace, angle: sliceFillAngle }); } - profileEnd(); - } - - /** - * Process sparse infill patterns - */ - async function processSparseInfill() { - let lastType; - let promises = isConcurrent ? [] : undefined; - forSlices(0.5, promises ? 0.55 : 0.7, slice => { - let params = slice.params || process; - if (!params.sliceFillSparse) { - return; - } - let newType = params.sliceFillType; - layerSparseFill(slice, { - settings, - process, - device, - lineWidth, - spacing: fillOffset, - density: params.sliceFillSparse, - bounds: widget.getBoundingBox(), - height: sliceHeight, - type: newType, - cache: params._range !== true && lastType === newType, - promises - }); - lastType = newType; - }, "infill"); - if (promises) { - await tracker(promises, (i, t) => { - trackupdate(i / t, 0.55, 0.7); - }); + if (slice.index === slices.length - 1) { + slice.isFlatsLayer = true; } - // back-fill slices marked for infill cloning - for (let slice of slices) { - if (slice._clone_sparse) { - let tops = slice.tops; - let down = slice.down.tops; - for (let i=0; i p.cloneZ(slice.z)); - } - } + sliceFillAngle = (sliceFillAngle + 90.0) % 360; + }, "solid layers"); + } + + /** + * Process layer diffs and project solid areas + */ + async function processLayerDiffs() { + // boolean diff layers to detect bridges and flats + profileStart("delta"); + forSlices(0.2, 0.33, slice => { + let params = slice.params || process; + let solidMinArea = params.sliceSolidMinArea; + let sliceMinThick = params.sliceSolidMinThick; + let sliceFillGrow = params.sliceFillGrow; + layerDiff(slice, { area: solidMinArea, grow: sliceFillGrow, thick: sliceMinThick }); + }, "layer deltas"); + profileEnd(); + // project bridges and flats up and down into part + profileStart("delta-project"); + forSlices(0.33, 0.34, slice => { + let params = slice.params || process; + topLayers = params.sliceTopLayers || 0; + bottomLayers = params.sliceBottomLayers || 0; + if (topLayers) projectFlats(slice, topLayers); + if (bottomLayers) projectBridges(slice, bottomLayers); + if (slice.flats) POLY.setZ(slice.flats, slice.z); + }, "layer deltas"); + profileEnd(); + // union solid areas + profileStart("solid-union"); + forSlices(0.34, 0.35, slice => { + if (slice.solids) { + slice.solids = POLY.union(slice.solids, 0, true); } - } + }); + profileEnd(); + } - /** - * Process support structure fills - */ - async function processSupportFills() { - profileStart("support-fill"); - let promises = false && isConcurrent ? [] : undefined; - forSlices(0.8, promises ? 0.88 : 0.9, slice => { - let params = slice.params || process; - let density = params.sliceSupportDensity; - layerSupportFill({ - angle: process.sliceSupportFill, - density, - isBelt, - lineWidth, - outline: process.sliceSupportOutline !== false, - promises, - slice, - }); - }, "support fill"); - if (promises) { - await tracker(promises, (i, t) => { - trackupdate(i / t, 0.88, 0.9); - }); - } - profileEnd(); - } - - /** - * Process brick/interleave mode slicing - */ - function processBrickMode() { - let indices = slices.map(s => s.index); - let first = indices[1]; - let last = indices[indices.length - 2]; - let nu = []; - for (let slice of slices) { - if (slice.index < first || slice.index > last) { - continue; - } - let nuSlice = slice.clone(); - nuSlice.z -= slice.height / 2; - if (slice.index === first) { - nuSlice.z = slice.z - slice.height / 4; - nuSlice.height = slice.height / 2; - } else { - nuSlice.height = slice.height; - } - nu.push(nuSlice); - let ti = 0; - for (let top of slice.tops || []) { - let nuTop = nuSlice.tops[ti++]; - nuTop.shells = []; - top.shells = top.shells.filter((s,i) => { - if (i % 2 === 0) { - return true; - } else { - nuTop.shells.push(s); - return false; - } - }); - } - if (slice.index === last) { - let cap = nuSlice.clone(); - cap.z += (slice.height * 0.75); - cap.height = (slice.height / 2); - nu.push(cap); - cap.tops?.forEach((top, i) => { - top.shells = nuSlice.tops[i].shells.clone(); - }); - } - } - slices.appendAll(nu); - slices.sort((a,b) => a.z - b.z); - slices.forEach((s,i) => s.index = i); - } - - // calculate % complete and call onupdate() - function doupdate(index, from, to, msg) { - trackupdate(index / slices.length, from, to, msg); - } - - function trackupdate(pct, from, to, msg) { - onupdate(0.5 + (from + (pct * (to - from))) * 0.5, msg); - } - - // for each slice, performe a function and call doupdate() - function forSlices(from, to, fn, msg) { - slices.forEach(slice => { - fn(slice); - doupdate(slice.index, from, to, msg) + /** + * Process solid fill patterns + */ + async function processSolidFills() { + profileStart("solid-fill") + let promises = isConcurrent ? [] : undefined; + forSlices(0.35, promises ? 0.4 : 0.5, slice => { + let params = slice.params || process; + let solidWidth = params.sliceFillWidth || 1; + let fillSpace = fillSpacing * solidWidth; + let solidMinArea = params.sliceSolidMinArea; + layerFillSolids({ slice, spacing: fillSpace, angle: sliceFillAngle, minArea: solidMinArea, promises }); + sliceFillAngle = (sliceFillAngle + 90.0) % 360; + }, "fill solids"); + // very last layer (top) is set to finish solid rate + slices.last().finishSolids = true + if (promises) { + await tracker(promises, (i, t) => { + trackupdate(i / t, 0.4, 0.5); }); } + profileEnd(); + } + /** + * Process sparse infill patterns + */ + async function processSparseInfill() { + let lastType; + let promises = isConcurrent ? [] : undefined; + forSlices(0.5, promises ? 0.55 : 0.7, slice => { + let params = slice.params || process; + if (!params.sliceFillSparse) { + return; + } + let newType = params.sliceFillType; + layerSparseFill(slice, { + settings, + process, + device, + lineWidth, + spacing: fillOffset, + density: params.sliceFillSparse, + bounds: widget.getBoundingBox(), + height: sliceHeight, + type: newType, + cache: params._range !== true && lastType === newType, + promises + }); + lastType = newType; + }, "infill"); + if (promises) { + await tracker(promises, (i, t) => { + trackupdate(i / t, 0.55, 0.7); + }); + } + // back-fill slices marked for infill cloning + for (let slice of slices) { + if (slice._clone_sparse) { + let tops = slice.tops; + let down = slice.down.tops; + for (let i=0; i p.cloneZ(slice.z)); + } + } + } + } + + /** + * Process support structure fills + */ + async function processSupportFills() { + profileStart("support-fill"); + let promises = false && isConcurrent ? [] : undefined; + forSlices(0.8, promises ? 0.88 : 0.9, slice => { + let params = slice.params || process; + let density = params.sliceSupportDensity; + layerSupportFill({ + angle: process.sliceSupportFill, + density, + isBelt, + lineWidth, + outline: process.sliceSupportOutline !== false, + promises, + slice, + }); + }, "support fill"); + if (promises) { + await tracker(promises, (i, t) => { + trackupdate(i / t, 0.88, 0.9); + }); + } + profileEnd(); + } + + /** + * Process brick/interleave mode slicing + */ + async function processBrickMode() { + let indices = slices.map(s => s.index); + let first = indices[1]; + let last = indices[indices.length - 2]; + let nu = []; + for (let slice of slices) { + if (slice.index < first || slice.index > last) { + continue; + } + let nuSlice = slice.clone(); + nuSlice.z -= slice.height / 2; + if (slice.index === first) { + nuSlice.z = slice.z - slice.height / 4; + nuSlice.height = slice.height / 2; + } else { + nuSlice.height = slice.height; + } + nu.push(nuSlice); + let ti = 0; + for (let top of slice.tops || []) { + let nuTop = nuSlice.tops[ti++]; + nuTop.shells = []; + top.shells = top.shells.filter((s,i) => { + if (i % 2 === 0) { + return true; + } else { + nuTop.shells.push(s); + return false; + } + }); + } + if (slice.index === last) { + let cap = nuSlice.clone(); + cap.z += (slice.height * 0.75); + cap.height = (slice.height / 2); + nu.push(cap); + cap.tops?.forEach((top, i) => { + top.shells = nuSlice.tops[i].shells.clone(); + }); + } + } + slices.appendAll(nu); + slices.sort((a,b) => a.z - b.z); + slices.forEach((s,i) => s.index = i); + } + + async function renderSlices() { + forSlices(0.9, 1.0, slice => { + let params = slice.params || process; + layerRender(slice, params, { + dark: controller.dark, + devel: controller.devel, + renderContext + }); + }, "render"); + } + + async function processSlices() { // alert non-manifold parts if (healed) { onupdate(null, null, "part may not be manifold"); } + // reverse slices to perform these ops top-down // remove all empty slices above part but leave below // for multi-part (multi-extruder) setups where the void is ok - // also reverse because slicing occurs bottom-up let found = false; slices = slices.reverse().filter(slice => { if (slice.tops.length) { @@ -762,40 +780,48 @@ export function sliceOne(settings, widget, onupdate, ondone) { } }).reverse(); + // inject one empty slice at the top so that + // top layer flats are detected properly + slices.push(newSlice(slices.peek().index + 1)); + + // attach slices to widget since the + // variable will not be replaced after this + widget.slices = slices; + + // exit if no slices detected with tops + if (!slices || slices.length === 0) { + return; + } + // connect slices into linked list for island/bridge projections for (let i=1; i 0 if (!vaseMode) { + await processLayerDiffs(); + await processSolidFills(); await processSparseInfill(); } @@ -846,19 +869,12 @@ export function sliceOne(settings, widget, onupdate, ondone) { // brick/interleave mode processing if (isBrick) { - processBrickMode(); + await processBrickMode(); } // render if not explicitly disabled if (render) { - forSlices(0.9, 1.0, slice => { - let params = slice.params || process; - layerRender(slice, params, { - dark: controller.dark, - devel: controller.devel, - renderContext - }); - }, "render"); + await renderSlices(); } if (isBelt) { @@ -913,14 +929,15 @@ function bound(v,min,max) { */ function layerRender(slice, params, opt = {}) { const { dark, devel, renderContext: ctx } = opt; + const { offset, isThin, isFlat } = ctx; + const Color = dark ? COLOR_DARK : COLOR; const output = slice.output(); const height = slice.height / 2; const solidWidth = params.sliceFillWidth || 1; - const { offset, isThin, isFlat } = ctx; - if (slice.tops) // missing for supports - slice.tops.forEach(top => { + if (slice.tops?.length) + for (let top of slice.tops) { if (isThin) output .setLayer('part', Color.part) .addPolys([top.poly]); @@ -970,7 +987,7 @@ function layerRender(slice, params, opt = {}) { // .setLayer('last', { face: 0, line: 0x008888, check: 0x008888 }) // .addPolys(top.last); } - }); + } if (isThin && devel) { if (slice.solids?.length) output @@ -1088,13 +1105,7 @@ export function layerProcessTops(slice, count, offset1, offsetN, fillOffset, opt /** * Create an entirely solid layer by filling all top polygons * with an alternating pattern. - * - * @param {number} linewidth - * @param {number} angle - * @param {number} density - */ -/** - * Create solid fill layer by filling all top polygons with alternating pattern + * * @param {Object} args - Options object * @param {Slice} args.slice - Slice to fill * @param {number} args.spacing - Fill line spacing in mm @@ -1299,33 +1310,38 @@ function layerSparseFill(slice, options = {}) { * 'expand' is used for top offsets in SLA mode */ export function layerDiff(slice, options = {}) { - const { sla, fakedown, grow, min } = options; - if ((slice.index <= 0 && !fakedown) || slice.xray) { + const { sla, grow, area, thick } = options; + + if (slice.index < 0 || slice.xray) { return; } - const top = slice, - down = slice.down || (fakedown ? newSlice(-1) : null), + + let boundary = !(slice.up && slice.down), + top = slice, + down = slice.down || newSlice(-1), topInner = sla ? top.topPolys() : top.topInners(), downInner = sla ? down.topPolys() : down.topInners(), bridges = top.bridges = [], flats = down.flats = []; // skip diffing layers that are identical - if (slice.fingerprintSame(down)) { + if (!boundary && slice.fingerprintSame(down)) { top.bridges = bridges; down.flats = flats; + // console.log(slice.z, 'layer fingerprint match'); return; } let newBridges = []; let newFlats = []; - POLY.subtract(topInner, downInner, newBridges, newFlats, slice.z, min, { + POLY.subtract(topInner, downInner, newBridges, newFlats, slice.z, area, { wasm: true }); - newBridges = newBridges.filter(p => p.areaDeep() >= min); - newFlats = newFlats.filter(p => p.areaDeep() >= min); + // console.log(slice.z, { newBridges, newFlats }); + newBridges = newBridges.filter(p => p.areaDeep() >= area && p.thickness(true) >= thick); + newFlats = newFlats.filter(p => p.areaDeep() >= area && p.thickness(true) >= thick); if (grow > 0 && newBridges.length) { newBridges = POLY.offset(newBridges, grow); @@ -1652,20 +1668,21 @@ export function projectFlats(slice, count, expand) { if (slice.flats?.length) { slice.finishSolids = true; slice.isFlatsLayer = true; - const flats = expand ? POLY.expand(slice.flats, expand) : slice.flats; - projectSolid({ slice, polys: flats, count, up: false, first: true }); + const polys = expand ? POLY.expand(slice.flats, expand) : slice.flats; + projectSolid({ slice, polys, count, up: false, first: true }); } } /** * project top bridges up into part */ -export function projectBridges(slice, count) { +export function projectBridges(slice, count, expand) { if (!slice.up || !slice.bridges) return; - // these flats are marked for finishing print speed + // these bridges are marked for finishing print speed if (slice.bridges?.length) { slice.finishSolids = true; slice.isBridgeLayer = true; + const polys = expand ? POLY.expand(slice.bridges, expand) : slice.bridges; + projectSolid({ slice, polys, count, up: true, first: true }); } - projectSolid({ slice, polys: slice.bridges, count, up: true, first: true }); } diff --git a/web/kiri/index.html b/web/kiri/index.html index 46e2ca1c..ad439837 100644 --- a/web/kiri/index.html +++ b/web/kiri/index.html @@ -777,7 +777,3 @@
-
- - -
diff --git a/web/kiri/lang/en.js b/web/kiri/lang/en.js index a292599a..fb8c709b 100644 --- a/web/kiri/lang/en.js +++ b/web/kiri/lang/en.js @@ -396,7 +396,7 @@ self.lang['en-us'] = { fi_over_l: ["overlap between shells and solid or sparse infill. can improve bonding to shells","as fraction of line width","0.0 - 1.0"], fi_angl_s: "start angle", fi_angl_l: ["starting angle in degrees","90 degrees added to ","each following layer","applies only to solid layers"], - fi_grow_s: "area expand", + fi_grow_s: "expand", fi_grow_l: ["expand projected solids","can help with solid areas","over sparse infill areas","units in millimeters"], // FDM FILL @@ -891,8 +891,10 @@ self.lang['en-us'] = { ad_rdwl_l: ["time between re-engaging","filament and movement","in milliseconds"], ad_scst_s: "shell coast", ad_scst_l: ["non-printing end","of perimeter shells","in millimeters"], - ad_msol_s: "area filter", + ad_msol_s: "min area", ad_msol_l: ["minimum area (mm^2)","bridges and flats less than this are culled"], + ad_mthk_s: "min thick", + ad_mthk_l: ["minimum area thickness","which is a ratio of area to perimeter","for layer differences which are used","to find bridges and flat areas","values from 0.0 - 0.5 work best"], ad_mins_s: "min speed", ad_mins_l: ["minimum speed","for short segments"], ad_maxf_s: "max flowrate", From 02f405b3d3a07a161466db41b9e22c9c266024f5 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 9 Jan 2026 15:45:41 -0500 Subject: [PATCH 138/154] add missing lang key for remote printing --- src/geo/polygon.js | 2 +- src/kiri/mode/fdm/work/slice.js | 2 +- web/kiri/lang/en.js | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/geo/polygon.js b/src/geo/polygon.js index 198b7f9b..634b2984 100644 --- a/src/geo/polygon.js +++ b/src/geo/polygon.js @@ -1224,7 +1224,7 @@ export class Polygon { /** * find the ratio of area to perimeter which can give provide * guidance if this polygon is likely to have a thin cross-section - * + * * @param {*} deep whether to include inner holes in the calculation * @returns ratio */ diff --git a/src/kiri/mode/fdm/work/slice.js b/src/kiri/mode/fdm/work/slice.js index f3f122b4..0e11d587 100644 --- a/src/kiri/mode/fdm/work/slice.js +++ b/src/kiri/mode/fdm/work/slice.js @@ -1105,7 +1105,7 @@ export function layerProcessTops(slice, count, offset1, offsetN, fillOffset, opt /** * Create an entirely solid layer by filling all top polygons * with an alternating pattern. - * + * * @param {Object} args - Options object * @param {Slice} args.slice - Slice to fill * @param {number} args.spacing - Fill line spacing in mm diff --git a/web/kiri/lang/en.js b/web/kiri/lang/en.js index fb8c709b..58546d42 100644 --- a/web/kiri/lang/en.js +++ b/web/kiri/lang/en.js @@ -1022,6 +1022,7 @@ self.lang['en-us'] = { ex_octo: "remote host", ex_host: "host", ex_akey: "key", + ex_type: "remote print type", ex_rsnm: "resin used ML", ex_layr: "layers", ex_ptim: "print time", From 02b900a8f0ff9131f9d187579f36f0e2b8b4d9c5 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 9 Jan 2026 16:37:39 -0500 Subject: [PATCH 139/154] fix nested top fill offsets --- src/geo/polygons.js | 10 ++++++++++ src/kiri/mode/fdm/work/post.js | 5 ++++- src/kiri/mode/fdm/work/slice.js | 6 +++--- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/geo/polygons.js b/src/geo/polygons.js index 8a5d7947..5f06696f 100644 --- a/src/geo/polygons.js +++ b/src/geo/polygons.js @@ -61,6 +61,7 @@ const POLYS = { offset, outer, points, + print, rayIntersect, reconnect, renest, @@ -212,6 +213,15 @@ export function renest(polygons, deep) { return nest(flatten(polygons, [], true), deep); } +export function print(polys, pad = ' ', buf) { + buf = buf || []; + for (let poly of polys) { + buf.push(`${pad} - ${poly.points.length}`); + if (poly.inner) print(poly.inner, pad + pad, buf); + } + return buf; +} + /** * todo use clipper polytree? * diff --git a/src/kiri/mode/fdm/work/post.js b/src/kiri/mode/fdm/work/post.js index 4cdd6110..5271b5ac 100644 --- a/src/kiri/mode/fdm/work/post.js +++ b/src/kiri/mode/fdm/work/post.js @@ -758,6 +758,9 @@ export function layerProcessTop(z, top, count, offset1, offsetN, fillOffset, opt last = [top.poly]; } + // renest deep because last[] could be nested + last = POLY.renest(last, true); + // generate fill offset poly set from last offset to top.fill_off if (fillOffset && last.length > 0) { // if gaps present, remove that area from fill inset @@ -766,7 +769,7 @@ export function layerProcessTop(z, top, count, offset1, offsetN, fillOffset, opt POLY.subtract(last, gaps, nulast, null, z); last = nulast; } - last.forEach(function(inner) { + last.forEach((inner) => { POLY.offset([inner], -fillOffset, {outs: top.fill_off, flat: true, z}); }); } diff --git a/src/kiri/mode/fdm/work/slice.js b/src/kiri/mode/fdm/work/slice.js index 0e11d587..76c56e71 100644 --- a/src/kiri/mode/fdm/work/slice.js +++ b/src/kiri/mode/fdm/work/slice.js @@ -983,9 +983,9 @@ function layerRender(slice, params, opt = {}) { if (isThin && devel && top.fill_off?.length) { slice.output() .setLayer('fill inset', Color.inset) - .addPolys(top.fill_off); - // .setLayer('last', { face: 0, line: 0x008888, check: 0x008888 }) - // .addPolys(top.last); + .addPolys(top.fill_off) + .setLayer('last', { face: 0, line: 0x008888, check: 0x008888 }) + .addPolys(top.last); } } From 002a0cf9c998f061f351a8cc596b09f538510bd7 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 9 Jan 2026 18:53:05 -0500 Subject: [PATCH 140/154] check for missing params --- src/kiri/mode/fdm/work/export.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kiri/mode/fdm/work/export.js b/src/kiri/mode/fdm/work/export.js index 07ac27b2..06fff1b5 100644 --- a/src/kiri/mode/fdm/work/export.js +++ b/src/kiri/mode/fdm/work/export.js @@ -156,7 +156,7 @@ export function fdm_export(print, online, ondone, ondebug) { // smallish band-aid. refactor above to remove redundancy function updateParams(layer, params) { - // params = getRangeParameters(process, layer); + if (!params) params = getRangeParameters(process, layer); zhop = params.zHopDistance || 0; // range retDist = params.outputRetractDist || 0; // range retSpeed = params.outputRetractSpeed * 60 || 1; // range From fca2bb7a59149f098111d4d7a30b60fa63d9c8cf Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 9 Jan 2026 19:50:03 -0500 Subject: [PATCH 141/154] fix NaN estimates with non printing layers --- src/kiri/mode/fdm/work/export.js | 30 ++++++++--------------------- src/kiri/mode/fdm/work/init-work.js | 8 ++++++-- src/kiri/mode/fdm/work/prepare.js | 2 ++ 3 files changed, 16 insertions(+), 24 deletions(-) diff --git a/src/kiri/mode/fdm/work/export.js b/src/kiri/mode/fdm/work/export.js index 06fff1b5..4ba3cd24 100644 --- a/src/kiri/mode/fdm/work/export.js +++ b/src/kiri/mode/fdm/work/export.js @@ -349,6 +349,7 @@ export function fdm_export(print, online, ondone, ondebug) { if (line.indexOf('G92') === 0) { line.split(";")[0].split(' ').forEach(function (tok) { let val = parseFloat(tok.substring(1) || 0) || 0; + if (isNaN(val)) console.log(val, line); switch (tok[0]) { case 'X': pos.x = val; break; case 'Y': pos.y = val; break; @@ -357,15 +358,17 @@ export function fdm_export(print, online, ondone, ondebug) { } }); } - if (line.indexOf('E') > 0) { - line.split(";")[0].split(' ').forEach(function (tok) { + if (line.indexOf('E') > 0 && line[0] === 'G') { + line.split(";")[0].split(' ').forEach((tok) => { // use max E position from gcode-preamble if (tok[0] == 'E') { + let etok = parseFloat(tok.substring(1) || 0); + if (isNaN(etok)) console.log(line); if (extrudeAbs) { - outputLength = Math.max(outputLength, parseFloat(tok.substring(1)) || 0); + outputLength = Math.max(outputLength, etok); emitted = outputLength; } else { - emitted += parseFloat(tok.substring(1) || 0); + emitted += etok; } } }); @@ -423,24 +426,6 @@ export function fdm_export(print, online, ondone, ondebug) { time += (retDist / retSpeed) * 60 * 2; // retraction time } - let taxis = new THREE.Vector3( 1, 0, 0 ); - let tcent = new THREE.Vector2( 0, 0 ); - let angle = -Math.PI / 4; - let savePos = pos; - - function pushPos(newpos, rate, comment) { - savePos = Object.clone(pos); - moveTo({ - x: newpos.x + offset.x, - y: newpos.y + offset.y, - z: newpos.z + offset.z - }, rate, comment); - } - - function popPos(rate, comment) { - moveTo(savePos, rate, comment); - } - function moveTo(newpos, rate, comment, arc) { let o = arc ? [arc.clockwise ? 'G2' : 'G3'] : @@ -656,6 +641,7 @@ export function fdm_export(print, online, ondone, ondebug) { extruder = extruders[tool]; offset_x = extruder.extOffsetX; offset_y = extruder.extOffsetY; + emitPerMM = extrudePerMM( lineWidth || extruder.extNozzle, extruder.extFilament, diff --git a/src/kiri/mode/fdm/work/init-work.js b/src/kiri/mode/fdm/work/init-work.js index a3d699f7..36686765 100644 --- a/src/kiri/mode/fdm/work/init-work.js +++ b/src/kiri/mode/fdm/work/init-work.js @@ -10,6 +10,9 @@ import { codec } from '../../../core/codec.js'; // fil = filament diameter // slice = slice height function extrudePerMM(noz, fil, slice) { + // if (isNaN(noz)) console.trace({ noz }); + // if (isNaN(fil)) console.trace({ fil }); + // if (isNaN(slice)) console.trace({ 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); }; @@ -18,12 +21,14 @@ function extrudePerMM(noz, fil, slice) { // perMM = amount extruded per MM (from extrudePerMM) // factor = scaling factor (usually 1.0) function extrudeMM(dist, perMM, factor) { + // if (isNaN(dist)) console.trace({ dist }); + // if (isNaN(perMM)) console.trace({ perMM }); + // if (isNaN(factor)) console.trace({ factor }); return dist * perMM * factor; } // defer loading until client and worker exist function init(worker) { - // worker.dispatch.fdm_support_generate = function(data, send) { // const { settings } = data; // const widgets = Object.values(worker.cache); @@ -33,7 +38,6 @@ function init(worker) { // supports: widget.supports, // } } ))); // }; - } export const FDM = { diff --git a/src/kiri/mode/fdm/work/prepare.js b/src/kiri/mode/fdm/work/prepare.js index 82575f0f..5ab342a5 100644 --- a/src/kiri/mode/fdm/work/prepare.js +++ b/src/kiri/mode/fdm/work/prepare.js @@ -608,7 +608,9 @@ export async function fdm_prepare(widgets, settings, update) { layerout.layer = layerno++; update((layerno / cake.length) * 0.5, "prepare"); + // clear for next layer layerout = []; + layerout.height = sliceHeight; } print.output = output; From e565426ff6151bbfeb7163d7f6e2055d985d620b Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 9 Jan 2026 22:08:18 -0500 Subject: [PATCH 142/154] start synthesizing a different support style widget to allow for alternate tool use --- src/kiri/mode/fdm/work/init-work.js | 1 - src/kiri/mode/fdm/work/prepare.js | 52 +++++++++++++++++++++-------- src/kiri/mode/fdm/work/slice.js | 9 +++-- 3 files changed, 45 insertions(+), 17 deletions(-) diff --git a/src/kiri/mode/fdm/work/init-work.js b/src/kiri/mode/fdm/work/init-work.js index 36686765..8e08a9ff 100644 --- a/src/kiri/mode/fdm/work/init-work.js +++ b/src/kiri/mode/fdm/work/init-work.js @@ -4,7 +4,6 @@ import { util } from '../../../../geo/base.js'; import { sliceOne, slicePost } from './slice.js'; import { fdm_prepare } from './prepare.js'; import { fdm_export } from './export.js'; -import { codec } from '../../../core/codec.js'; // noz = nozzle diameter // fil = filament diameter diff --git a/src/kiri/mode/fdm/work/prepare.js b/src/kiri/mode/fdm/work/prepare.js index 5ab342a5..f7f83f75 100644 --- a/src/kiri/mode/fdm/work/prepare.js +++ b/src/kiri/mode/fdm/work/prepare.js @@ -9,6 +9,7 @@ import { newPolygon, Polygon } from '../../../../geo/polygon.js'; import { polygons as POLY, fillArea } from '../../../../geo/polygons.js'; import { newPrint } from '../../../core/print.js'; import { newSlice } from '../../../core/slice.js'; +import { newWidget } from '../../../core/widget.js'; import { render } from '../../../core/render.js'; import { getRangeParameters } from '../core/params.js'; @@ -25,7 +26,7 @@ export async function fdm_prepare(widgets, settings, update) { widgets = widgets.filter(w => !w.track.ignore && !w.meta.disabled); let { device, process, controller, bounds } = settings, - { sliceHeight, firstSliceHeight, firstLayerRate } = process, + { sliceHeight, firstSliceHeight, firstLayerRate, sliceSupportNozzle } = process, { outputSeekrate, outputDraftShield, outputPurgeTower } = process, { bedWidth, bedDepth } = device, { lineType } = controller, @@ -85,11 +86,6 @@ export async function fdm_prepare(widgets, settings, update) { slice.tops.forEach(function(top) { tops.push(top.poly.clone()); }); - // collect support polygons - if (slice.supports) - slice.supports.forEach((support) => { - tops.push(support.clone()); - }); } if (outputDraftShield) { tops = POLY.union(tops,1,true); @@ -205,6 +201,7 @@ export async function fdm_prepare(widgets, settings, update) { preout.last().retract = true; } } + // recompute bounds for purge block offsets let bbounds = newBounds(); brims.forEach(brim => { @@ -224,13 +221,17 @@ export async function fdm_prepare(widgets, settings, update) { // find max layers (for updates) // generate list of used extruders for purge blocks - for (let widget of widgets) { - let extruder = widget.anno.extruder ?? 0; + for (let extruder of widgets.map(w => w.anno.extruder ?? 0)) { if (!extruders[extruder]) { extruders[extruder] = {}; extcount++; } } + // add support tool, if not already used + if (!extruders[sliceSupportNozzle]) { + extruders[sliceSupportNozzle] = {}; + extcount++; + } // determine size/location of purge blocks let blokw = Math.sqrt(outputPurgeTower || 0); @@ -295,10 +296,18 @@ export async function fdm_prepare(widgets, settings, update) { mkblok(blokw, blokh); // allocate tower space for extruders-1 locations + // for example, with colors, you only to swap once + // on each layer then start the next layer on the + // currently selected tool. let towers = extruders.slice(1).map((ext,i) => { return ext ? mkrec(i) : ext; }); + // console.log({ + // extruders: extruders.slice(), + // towers: towers.slice() + // }); + function linesToPoly(points) { let poly = newPolygon().setOpen(); let ping = 0; @@ -413,14 +422,31 @@ export async function fdm_prepare(widgets, settings, update) { widget.offset = offset; } + // for any widget with supports, create a new widget for them + for (let widget of widgets.slice()) { + let slices = []; + for (let slice of widget.slices) { + if (slice.supports) { + let nslice = newSlice(slice.z); + nslice.index = slice.index; + nslice.supports = slice.supports; + nslice.extruder = sliceSupportNozzle; + slices.push(nslice); + } + } + if (slices.length) { + let nwid = newWidget(null, widget.group); + nwid.support = true; + nwid.slices = slices; + nwid.offset = widget.offset; + widgets.push(nwid); + } + } + // create shuffled slice cake by z offset (slice.z + offset.z) let cake = []; let zrec = {}; for (let widget of widgets) { - // skip synthesized support widget(s) - if (!widget.mesh) { - continue; - } for (let slice of widget.slices) { slice.widget = widget; let z = (slice.z + widget.offset.z).round(2); @@ -1446,7 +1472,7 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { if (slice.tops) { out.appendAll(slice.tops); } - if (slice.supports) { + if (slice.supports && slice.widget.support) { out.appendAll(slice.supports); } diff --git a/src/kiri/mode/fdm/work/slice.js b/src/kiri/mode/fdm/work/slice.js index 76c56e71..6f751317 100644 --- a/src/kiri/mode/fdm/work/slice.js +++ b/src/kiri/mode/fdm/work/slice.js @@ -1045,6 +1045,7 @@ export function slicePost(settings, onupdate) { .sort((a,b) => { return a.slices[0].z - b.slices[0].z }); + // assign grid_id which can be embedded in gcode and // used by the controller to cancel objects during print let { bounds } = settings; @@ -1058,6 +1059,7 @@ export function slicePost(settings, onupdate) { }; widget.track.grid_id = tl.x * 100 + tl.y; } + // count extruders used let ext = []; for (let w of widgets) { @@ -1068,15 +1070,16 @@ export function slicePost(settings, onupdate) { } } } + // sort widgets by first slice Z widgets.sort((a,b) => { return a.slices[0].z - b.slices[0].z; }); - // give first widget a pass since it should have the anchor - widgets.shift(); + // remove anchor slices from other widgets (only with multi-material) if (ext.length > 1) { - for (let w of widgets) { + // give first widget a pass since it should have the anchor + for (let w of widgets.slice(1)) { w.slices = w.slices.filter(s => s.index >= 0); } } From 397f5272395ab190abb1f76887601220f651147a Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 9 Jan 2026 22:34:55 -0500 Subject: [PATCH 143/154] fix purge towers for non-adjacent tools --- src/kiri/mode/fdm/work/prepare.js | 46 +++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/src/kiri/mode/fdm/work/prepare.js b/src/kiri/mode/fdm/work/prepare.js index f7f83f75..bf2408fa 100644 --- a/src/kiri/mode/fdm/work/prepare.js +++ b/src/kiri/mode/fdm/work/prepare.js @@ -238,6 +238,21 @@ export async function fdm_prepare(widgets, settings, update) { let blokh = blokw; let blokpos, walkpos, blok; + /** + * mkblok() - Calculate purge tower block positions and layout + * + * Sets up the geometric layout for purge tower blocks. Each extruder swap requires + * a purge block, so we create (extcount-1) blocks arranged either horizontally or + * vertically depending on the print bed orientation. + * + * @param {number} w - width of each purge block + * @param {number} h - height of each purge block + * + * Sets module-level variables: + * - blokpos: starting position {x,y} for first block + * - walkpos: step vector {x,y} between blocks + * - blok: dimensions {x,y} of each block + */ function mkblok(w,h) { let count = extcount - 1; let gap = nozzle; @@ -271,6 +286,23 @@ export async function fdm_prepare(widgets, settings, update) { } } + /** + * mkrec() - Create a purge tower block record for a specific extruder + * + * Generates the geometry for one purge block including outline, dense fill, + * and sparse fill patterns. Position is calculated based on the block index. + * + * @param {number} i - extruder index in device.extruders array (NOT sparse extruders index) + * @param {number} angle - fill pattern angle in degrees (default 45) + * @param {number} thin - spacing multiplier for sparse fill (default 6) + * @returns {Object} purge block record with: + * - extruder: extruder index + * - diameter: nozzle diameter + * - rect: outer rectangle polygon + * - full: dense fill pattern for first layer or tool changes + * - sparse: sparse fill pattern for maintaining tower structure + * - pause: position for any pause commands + */ function mkrec(i, angle = 45, thin = 6) { let exi = device.extruders[i], noz = exi.extNozzle, @@ -296,11 +328,19 @@ export async function fdm_prepare(widgets, settings, update) { mkblok(blokw, blokh); // allocate tower space for extruders-1 locations - // for example, with colors, you only to swap once + // for example, with colors, you only need to swap once // on each layer then start the next layer on the // currently selected tool. - let towers = extruders.slice(1).map((ext,i) => { - return ext ? mkrec(i) : ext; + // IMPORTANT: must use actual extruder indices, not array indices, + // to handle non-contiguous tool usage (e.g., tool 0 + tool 2) + let usedExtruderIndices = []; + for (let i = 0; i < extruders.length; i++) { + if (extruders[i]) { + usedExtruderIndices.push(i); + } + } + let towers = usedExtruderIndices.slice(1).map((extruderIndex, blockIndex) => { + return mkrec(extruderIndex, 45 + blockIndex * 90); }); // console.log({ From 38d040b190469c5b41db1d950402d41b98f16efe Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 9 Jan 2026 22:50:51 -0500 Subject: [PATCH 144/154] add missing support slice height, add comments for critical code paths --- src/kiri/mode/fdm/work/prepare.js | 179 ++++++++++++++++++++++++------ 1 file changed, 145 insertions(+), 34 deletions(-) diff --git a/src/kiri/mode/fdm/work/prepare.js b/src/kiri/mode/fdm/work/prepare.js index bf2408fa..ace9ccb8 100644 --- a/src/kiri/mode/fdm/work/prepare.js +++ b/src/kiri/mode/fdm/work/prepare.js @@ -368,25 +368,64 @@ export async function fdm_prepare(widgets, settings, update) { let purgedFirst = false; let lastPurgeTool; - // generate purge block for given nozzle + /** + * purge() - Generate purge tower block output for a given tool + * + * Called in two contexts: + * 1. Real purge: When switching tools during printing (using=undefined) + * 2. Fill-in: At layer end to maintain unused blocks (using=actual tool index) + * + * The function consumes one purge block from the track array and generates: + * - Outline perimeter (with extrusion) + * - Fill pattern (dense for real purges, sparse for fill-ins) + * - Retract + * - Optional wipe move (outline without extrusion) + * + * @param {number} nozzle - The extruder index that needs purging/filling + * @param {Array} track - Array of available purge block records (mutated via shift()) + * @param {Array} layer - Output layer array to append print moves to + * @param {Point} start - Current print head position + * @param {number} z - Z height for this layer + * @param {number} using - Optional override tool index: + * - undefined/negative: real purge (use nozzle param) + * - >= 0: fill-in mode (use this tool instead) + * @param {Object} offset - Position offset for belt printers + * @returns {Point} Updated print head position after purge block + * + * Fill pattern selection: + * - Dense fill (rec.full): Used for first layer or real tool changes + * - Sparse fill (rec.sparse): Used for fill-ins or consecutive same-tool purges + */ function purge(nozzle, track, layer, start, z, using, offset) { if (!outputPurgeTower || extcount < 2) { return start; } + // Get next available purge block (consumes from track array) let rec = track.shift(); + + // Use sparse fill if: filling someone else's block OR same tool as last purge let thin = using >= 0 || lastPurgeTool === nozzle; + + // Actual tool to use: override tool for fill-ins, otherwise the nozzle being purged let tool = using >= 0 ? using : nozzle; + + // Detect first layer for special handling let first = isBelt ? !purgedFirst && layer.slice.index >= 0 : layer.slice.index === 0; let rate = first ? process.firstLayerRate : process.outputFeedrate; let wipe = true; lastPurgeTool = tool; + if (rec) { print.setType('purge tower'); if (layer.last()) { layer.last().retract = true; } purgedFirst = purgedFirst || first; + + // Use dense fill for first layer or real purges (when thin=false) let purgeOn = first || !thin; + + // Belt printer: scale tower size for negative Z (before belt contact) if (isBelt && z < 0) { let scale = 1 - ((z / zmin)); mkblok(blokw * scale, blokh); @@ -394,9 +433,12 @@ export async function fdm_prepare(widgets, settings, update) { rate = (process.outputFeedrate - process.firstLayerRate) * scale + process.firstLayerRate; wipe = false; } + // Prepare geometry: outline, pause point, and fill pattern (dense or sparse) let box = rec.rect.clone().setZ(z); let pause = rec.pause.clone().setZ(z); let fill = (z >= 0 && purgeOn ? rec.full : rec.sparse).clone().setZ(z); + + // Belt printer: apply Y/Z coordinate transformations if (isBelt) { let bmove = {x:0, y:z, z:0}; box.move(bmove); @@ -411,6 +453,8 @@ export async function fdm_prepare(widgets, settings, update) { if (fill) fill.move(bo); } } + + // Output sequence: 1. Outline perimeter (with extrusion) start = print.polyPrintPath(box, start, layer, { tool, rate, @@ -418,8 +462,9 @@ export async function fdm_prepare(widgets, settings, update) { open: false, onfirstout: (out => out.overate = (isBelt ? rate : 0)) }); + + // Output sequence: 2. Fill pattern (with extrusion, dense or sparse) if (fill && fill.length) { - // for pings, split path at 20mm start = print.polyPrintPath(fill, start, layer, { tool, rate, @@ -428,8 +473,11 @@ export async function fdm_prepare(widgets, settings, update) { onfirst: (point) => { point.purgeOn = purgeOn ? pause : undefined } }); } + + // Output sequence: 3. Retract after fill layer.last().retract = true; - // experimental post-retract wipe + + // Output sequence: 4. Post-retract wipe (outline without extrusion) if (wipe) start = print.polyPrintPath(box, start, layer, { tool, rate, @@ -468,6 +516,7 @@ export async function fdm_prepare(widgets, settings, update) { for (let slice of widget.slices) { if (slice.supports) { let nslice = newSlice(slice.z); + nslice.height = slice.height; nslice.index = slice.index; nslice.supports = slice.supports; nslice.extruder = sliceSupportNozzle; @@ -501,26 +550,55 @@ export async function fdm_prepare(widgets, settings, update) { return a.z - b.z; }); - let firstTool; - let lastWidget; - let lastExt; - let lastOut; + let firstTool; // First extruder used (determines starting extruder) + let lastWidget; // Last widget printed (for retraction decisions) + let lastExt; // Last extruder used (for minimizing tool changes) + let lastOut; // Last output slice (for tracking state) print.zmax = zmax; print.total_time = 0; - // walk cake layers bottom up + /** + * MAIN PRINT LOOP: Walk cake layers bottom up + * + * The "cake" is a sorted array of layers where each layer contains all slices + * at that Z height from all widgets. This loop processes layers in Z order. + * + * For each layer: + * 1. Process all slices using greedy nearest-neighbor with extruder penalties + * 2. Generate purge tower blocks on extruder changes + * 3. Add optional draft shield around perimeter + * 4. Fill any unused purge blocks to maintain tower structure + * 5. Append completed layer to output and update progress + * + * Key optimization: Minimize travel and extruder changes by: + * - Prioritizing slices using the same extruder (10000x distance penalty for changes) + * - Choosing nearest slice within same-extruder group + * - Starting first object with extruder 0 when possible + */ for (let layer of cake) { - // track purge blocks generated for each layer + // Clone towers array to track available purge blocks for this layer + // Each purge block can only be used once per layer let track = towers.slice(); - // iterate over layer slices, find closest widget, print, eliminate + /** + * INNER LOOP: Process all slices in this layer + * + * Uses greedy nearest-neighbor algorithm with extruder awareness: + * - Builds a priority queue of unprinted slices + * - Heavily penalizes extruder changes (10000x distance multiplier) + * - Selects closest slice with preferred extruder + * - Marks as prep'd and removes from consideration + * - Repeats until all slices processed (order.length === 0) + */ for (;;) { + // Build priority queue of unprinted slices with weighted distances let order = []; - // select slices of the same extruder type first then distance + + // Evaluate each unprinted slice in this layer for (let slice of layer.slices) { if (slice.prep) { - continue; + continue; // Skip already processed slices } let offset = lastOffset = slice.widget.offset; let find = slice.findClosestPointTo(printPoint.sub(offset)); @@ -528,69 +606,91 @@ export async function fdm_prepare(widgets, settings, update) { let ext = slice.extruder; let lex = lastExt; let dst = Math.abs(find.distance); - // penalize extruder swaps + + // CRITICAL: Heavily penalize extruder changes to minimize purging + // 10000x multiplier makes any same-extruder slice preferred over a swap if (ext !== lex) { dst *= 10000; } - // for first object, penalize extruders other than first + + // CRITICAL: For first slice in print, prefer extruder 0 + // Ensures print starts with primary extruder when possible if (lastExt === undefined && ext > 0) { dst *= 10000; } + order.push({dst, slice, offset, z: layer.z}); } } + + // All slices processed for this layer - exit inner loop if (order.length === 0) { break; } + + // Sort by weighted distance (extruder penalty + travel distance) order.sort((a,b) => { return a.dst - b.dst; }); + + // Select nearest slice (considering extruder penalties) let { z, slice, offset } = order[0]; + // Track the first tool used to determine print starting extruder if (firstTool === undefined) { firstTool = slice.extruder; } - // when layers switch between widgets, force retraction + // Force retraction when switching between different widgets + // Prevents stringing during travel moves between objects let forceRetract = lastOut && lastOut.widget !== slice.widget; if (forceRetract && output.length) { - // selecet last output of last layer output.last().last().retract = true; } + // Get any layer-specific parameter overrides for this slice let params = getRangeParameters(process, slice.index); + + // Mark slice as processed so it won't be selected again slice.prep = true; - // retract between widgets or layers (when set) + + // Retract between widgets within the same layer if (layerout.length && slice.widget !== lastWidget) { layerout.last().retract = true; } + + // Update layer metadata for this slice lastWidget = slice.widget; layerout.z = z + slice.height / 2; layerout.height = layerout.height || slice.height; layerout.slice = slice; layerout.params = params; - // mark layer as anchor if slice is belt and flag set - layerout.anchor = slice.belt && slice.belt.anchor; - // detect extruder change and print purge block + layerout.anchor = slice.belt && slice.belt.anchor; // Belt anchor flag + + // PURGE TOWER: Generate purge block on extruder change + // Only purge when switching to a different extruder if (!lastOut || lastOut.extruder !== slice.extruder) { if (slice.extruder >= 0) printPoint = purge(slice.extruder, track, layerout, printPoint, slice.z, undefined, offset); } let wtb = slice.widget.track.box; - let beltStart = slice.belt && slice.belt.touch;// && (widgets.length === 1); - // output seek to start point between mesh slices if previous data + let beltStart = slice.belt && slice.belt.touch; + + // PRINT THIS SLICE: Generate all toolpaths (shells, infill, etc.) print.setType('layer'); print.setWidget(lastWidget); printPoint = slicePrintPath( print, slice, + // Belt prints start from far corner, others from current position beltStart ? newPoint(-5000, 5000, 0) : printPoint.sub(offset), offset, layerout, { first: slice.index === 0, onBelt: beltStart, - params, // range parameters + params, // layer-specific parameter overrides pretract: (wipeDist) => { + // Optional wipe move before retraction if (!(lastLayer && lastLayer.length)) { return; } @@ -610,32 +710,42 @@ export async function fdm_prepare(widgets, settings, update) { zmax, } ); + + // Update print time statistics layerout.print_time = printPoint.layer_time; print.total_time += printPoint.layer_time; print.setWidget(null); + // Update state for next iteration lastOut = slice; lastExt = lastOut.extruder; lastPoly = slice.lastPoly; lastLayer = layerout; + // Optional retraction after every layer if (params.outputLayerRetract && layerout.length) { layerout.last().retract = true; } } + // End of inner loop - all slices in this layer have been processed - // clear slice.prep so it can be re-previewed in a different mode + /** + * POST-PROCESSING: Layer finalization steps + */ + + // Clear prep flags to allow re-preview in different modes + // Only clear real widget slices, not synthesized support widgets for (let widget of widgets) { - // skip synthesized support widget(s) if (!widget.mesh) { - continue; + continue; // Skip synthesized support widgets } for (let slice of widget.slices) { slice.prep = false; } } - // draft shield + // DRAFT SHIELD: Print perimeter around all objects (layers 1+) + // Helps contain heat and protects from drafts for better print quality if (layerno > 0 && shield && outputDraftShield) { print.setType('shield'); shield = POLY.setZ(shield.clone(), printPoint.z); @@ -644,7 +754,6 @@ export async function fdm_prepare(widgets, settings, update) { return print.polyPrintPath(poly, startPoint, preout, { onfirst: function(point) { if (preout.length && point.distTo2D(startPoint) > 2) { - // retract between part and shield preout.last().retract = true; } } @@ -654,27 +763,29 @@ export async function fdm_prepare(widgets, settings, update) { layerout.appendAll(preout); } - // if a declared extruder isn't used in a layer, use selected - // extruder to fill the relevant purge blocks for later support + // FILL UNUSED PURGE BLOCKS: Maintain structural integrity of purge tower + // Any purge blocks not used for real tool changes get filled with sparse + // infill using the current extruder. This keeps the tower solid for support. if (lastOut) track.slice().forEach(ext => { + // using=lastExt means "fill with current tool" (sparse fill mode) printPoint = purge(ext.extruder, track, layerout, printPoint, lastOut.z, lastExt, lastOffset); }); - // if layer produced output, append to output array + // Append completed layer to output array if (layerout.length) { output.append(layerout); } - // retract after last layer + // Final retraction after last layer of print if (layerno === cake.length - 1 && layerout.length) { layerout.last().retract = true; } - // notify progress + // Update progress callback (prepare phase is 0-50%) layerout.layer = layerno++; update((layerno / cake.length) * 0.5, "prepare"); - // clear for next layer + // Reset for next layer layerout = []; layerout.height = sliceHeight; } From dce90fab075356629e57c6f3812683195b14e3ab Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sat, 10 Jan 2026 12:33:40 -0500 Subject: [PATCH 145/154] add belt docs and preserve belt transformations --- src/kiri/mode/fdm/work/prepare.js | 36 ++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/src/kiri/mode/fdm/work/prepare.js b/src/kiri/mode/fdm/work/prepare.js index ace9ccb8..08c439d8 100644 --- a/src/kiri/mode/fdm/work/prepare.js +++ b/src/kiri/mode/fdm/work/prepare.js @@ -493,15 +493,25 @@ export async function fdm_prepare(widgets, settings, update) { } } - // establish offsets + /** + * Establish widget offsets + * + * For belt printers: Apply trigonometric transformation from Y-axis position + * to inclined belt coordinate system using widget.belt properties (cosf, sinf, ypos) + * + * For normal printers: Use widget position plus any raft offset + * + * Note: This must run BEFORE support widgets are created, as they inherit + * both the offset and belt properties from their parent widget. + */ for (let widget of widgets) { let { belt } = widget; let offset = Object.clone(widget.track.pos); if (isBelt) { offset = { x: belt.xpos, - y: belt.ypos * belt.cosf, - z: belt.ypos * belt.sinf + y: belt.ypos * belt.cosf, // Y component of inclined belt + z: belt.ypos * belt.sinf // Z component of inclined belt }; } else { // when rafts used this is non-zero @@ -510,12 +520,27 @@ export async function fdm_prepare(widgets, settings, update) { widget.offset = offset; } - // for any widget with supports, create a new widget for them + /** + * Extract support structures into separate synthetic widgets + * + * Support structures use a different extruder (sliceSupportNozzle) and need + * to be processed independently. This creates new "virtual" widgets containing + * only support geometry. + * + * CRITICAL for belt mode: Must copy belt properties to maintain proper positioning + * - widget.belt: Required for offset calculations if widget list is reprocessed + * - slice.belt.anchor: Used to mark layers that touch the belt (line 667) + * - slice.belt.touch: Affects print start position logic (line 676) + * + * Without belt property copying, supports render in incorrect positions because + * the belt coordinate transformations (cosf, sinf, ypos) are not applied. + */ for (let widget of widgets.slice()) { let slices = []; for (let slice of widget.slices) { if (slice.supports) { let nslice = newSlice(slice.z); + nslice.belt = slice.belt; nslice.height = slice.height; nslice.index = slice.index; nslice.supports = slice.supports; @@ -527,7 +552,8 @@ export async function fdm_prepare(widgets, settings, update) { let nwid = newWidget(null, widget.group); nwid.support = true; nwid.slices = slices; - nwid.offset = widget.offset; + nwid.offset = widget.offset; // inherit calculated offset from parent + nwid.belt = widget.belt; // inherit any belt transformations widgets.push(nwid); } } From d581db53c038071c21c139bb64fa191eed4dd206 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sat, 10 Jan 2026 13:48:54 -0500 Subject: [PATCH 146/154] fix paint on support for translated widgets, apply belt transforms to manual paint points at slice time --- src/geo/point.js | 1 + src/kiri/app/workers.js | 6 +++--- src/kiri/mode/fdm/app/init-ui.js | 8 +++++++- src/kiri/mode/fdm/work/slice.js | 16 +++++++++++++++- src/kiri/run/worker.js | 3 ++- 5 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/geo/point.js b/src/geo/point.js index f922b06f..04e78ff7 100644 --- a/src/geo/point.js +++ b/src/geo/point.js @@ -877,6 +877,7 @@ class Point { const { x, y } = this; this.x = x * Math.cos(angle) - y * Math.sin(angle); this.y = y * Math.cos(angle) + x * Math.sin(angle); + return this; } } diff --git a/src/kiri/app/workers.js b/src/kiri/app/workers.js index 8f71b39d..8e7148df 100644 --- a/src/kiri/app/workers.js +++ b/src/kiri/app/workers.js @@ -200,11 +200,12 @@ export const client = { let vertices = widget.getGeoVertices(); send("sync", { id: widget.id, - meta: widget.meta, + anno: widget.annotations(), group: widget.group.id, + meta: widget.meta, + position: widget.mesh.position, track: widget.track, vertices, - position: widget.mesh.position, }, done => { widget.modified = false; }); @@ -236,7 +237,6 @@ export const client = { slice(settings, widget, callback) { send("slice", { id: widget.id, - anno: widget.annotations(), settings: settings }, reply => { callback(reply); diff --git a/src/kiri/mode/fdm/app/init-ui.js b/src/kiri/mode/fdm/app/init-ui.js index 3576ecb5..2045f0ac 100644 --- a/src/kiri/mode/fdm/app/init-ui.js +++ b/src/kiri/mode/fdm/app/init-ui.js @@ -302,8 +302,14 @@ function supportStart({ remove } = { remove: false }) { function supportUpdate(point, widget, remove) { let { x, y, z } = point; + let pos = widget.track.pos; + // translate from three coordinates to widget coordinates let rec = { - point: { x, y:-z, z:y }, + point: { + x: x - pos.x, + y: -z - pos.y, + z: y - pos.z + }, radius: 2 }; // return console.log({ on, widget, point }); diff --git a/src/kiri/mode/fdm/work/slice.js b/src/kiri/mode/fdm/work/slice.js index 6f751317..857de8ac 100644 --- a/src/kiri/mode/fdm/work/slice.js +++ b/src/kiri/mode/fdm/work/slice.js @@ -477,8 +477,22 @@ export function sliceOne(settings, widget, onupdate, ondone) { stack.sort((a,b) => a.z - b.z); // process manual supports if they exist - // convert paint points to circles on matching slices let { paint } = widget.anno; + let { belt } = widget; + // apply belt transformations, if needed + if (belt && paint) { + let { dy, angle } = belt; + // make a copy we can modify + paint = structuredClone(paint); + for (let rec of paint) { + let { point } = rec; + // convert yz plane to xy plane to re-use point utility function + let np = newPoint(point.y + dy, point.z, 0).rotate(angle * (Math.PI/180)); + point.y = np.x; + point.z = np.y; + } + } + // convert paint points to circles on matching slices if (manual && paint?.length) { let hpi = Math.PI/2; for (let slice of stack) { diff --git a/src/kiri/run/worker.js b/src/kiri/run/worker.js index cff94946..fa115dc0 100644 --- a/src/kiri/run/worker.js +++ b/src/kiri/run/worker.js @@ -349,6 +349,8 @@ const dispatch = { widget.vertices = vertices; // restore meta widget.meta = data.meta; + // restore annotations + widget.anno = data.anno; // restore tracking object widget.track = data.track; send.done(data.id); @@ -449,7 +451,6 @@ const dispatch = { setPrint(null); current.mode = settings.mode.toUpperCase(); - widget.anno = data.anno || widget.anno; widget.settings = settings; widget.clearSlices(); From 60c14cf08a9a95feac4702dd3e3745094d60862c Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sat, 10 Jan 2026 15:20:14 -0500 Subject: [PATCH 147/154] for belt, clip supports to areas above platform --- src/kiri/mode/fdm/work/slice.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/kiri/mode/fdm/work/slice.js b/src/kiri/mode/fdm/work/slice.js index 857de8ac..aa2b99d8 100644 --- a/src/kiri/mode/fdm/work/slice.js +++ b/src/kiri/mode/fdm/work/slice.js @@ -538,7 +538,7 @@ export function sliceOne(settings, widget, onupdate, ondone) { if (shadowSum) { shadow = POLY.union([...shadow, ...shadowSum], minArea, true); } - // trim to slice.clips + // subtract slice.clips areas (widget boundaries) from shadow projection if (true) { let rem = []; let clips = [ @@ -557,6 +557,17 @@ export function sliceOne(settings, widget, onupdate, ondone) { shadow = POLY.offset(shadow, -bump, { z: slice.z }); } shadowSum = shadow; + // if belt, clip shadow to belt angle projection along platform + if (belt) { + let boundsx = bounds.dim.x * 1.1; + let boundsy = bounds.dim.y * 1.1; + let skewy = slice.z * belt.slope; + let clip = newPolygon() + .centerRectangle(newPoint(0, 0, slice.z), boundsx, boundsy) + .move({ x: 0, y: -boundsy / 2 + skewy, z: 0 }); + shadow = POLY.trimTo(shadow, [ clip ]); + // slice.output().setLayer("belt clip", 0xffff00).addPolys([ clip ]); + } slice.supports = shadow; slice.output().setLayer("shadow", 0xff0000).addPolys(shadow); trackupdate((++count/length), 0.10, 0.15, "support"); From c5e4bac1cb2fd54dff15649d1723a7f270dd3779 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sat, 10 Jan 2026 15:53:46 -0500 Subject: [PATCH 148/154] add support point compensation for belt anchors --- src/kiri/mode/fdm/work/belt.js | 9 ++++++++- src/kiri/mode/fdm/work/slice.js | 14 +++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/kiri/mode/fdm/work/belt.js b/src/kiri/mode/fdm/work/belt.js index 0d3bbef6..b3979a6a 100644 --- a/src/kiri/mode/fdm/work/belt.js +++ b/src/kiri/mode/fdm/work/belt.js @@ -48,7 +48,8 @@ const BELT = { * @returns {void} Modifies slices array in place */ export function generateBeltAnchor({ slices, widget, process, lineWidth, sliceHeight, sliceHeightBase, extruder }) { - let { cosf, slope } = widget.belt; + let { belt } = widget; + let { cosf, slope } = belt; // find adjusted zero point from slices let smin = Infinity; @@ -100,6 +101,12 @@ export function generateBeltAnchor({ slices, widget, process, lineWidth, sliceHe let step = sliceHeight; let anchorlen = (process.beltAnchor || process.firstLayerBeltLead) * cosf; + // offset used in support generation + belt.anchor = { + added: adds, + len: anchorlen + }; + while (anchorlen && start && anchorlen >= sliceHeight) { let addto = start.down; if (!addto) { diff --git a/src/kiri/mode/fdm/work/slice.js b/src/kiri/mode/fdm/work/slice.js index aa2b99d8..b52ba4a8 100644 --- a/src/kiri/mode/fdm/work/slice.js +++ b/src/kiri/mode/fdm/work/slice.js @@ -481,7 +481,7 @@ export function sliceOne(settings, widget, onupdate, ondone) { let { belt } = widget; // apply belt transformations, if needed if (belt && paint) { - let { dy, angle } = belt; + let { anchor, angle, dy, slope } = belt; // make a copy we can modify paint = structuredClone(paint); for (let rec of paint) { @@ -490,6 +490,10 @@ export function sliceOne(settings, widget, onupdate, ondone) { let np = newPoint(point.y + dy, point.z, 0).rotate(angle * (Math.PI/180)); point.y = np.x; point.z = np.y; + if (anchor) { + point.y += anchor.len; + point.z += anchor.len; + } } } // convert paint points to circles on matching slices @@ -837,10 +841,6 @@ export function sliceOne(settings, widget, onupdate, ondone) { slice.widget = widget; } - // support generation using either - // enclosed shadow or manual painted supports - await processSupports(); - // process solid layers (top/bottom) await processSolidLayers(); @@ -875,6 +875,10 @@ export function sliceOne(settings, widget, onupdate, ondone) { }); } + // support generation using either + // enclosed shadow or manual painted supports + await processSupports(); + // calculations only relevant when solid layers are used // layer boolean diffs need to be computed to find flat areas to fill // and overhangs that need to be supported. these are stored in flats From eb660aa20bff18efa9d29741ac19dd559f37d60a Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sat, 10 Jan 2026 16:16:49 -0500 Subject: [PATCH 149/154] add belt mode for support painting --- src/kiri/app/paint.js | 61 ++++++++++++++++++++++++++++++-- src/kiri/mode/fdm/app/init-ui.js | 37 +++++++++++++++++-- 2 files changed, 94 insertions(+), 4 deletions(-) diff --git a/src/kiri/app/paint.js b/src/kiri/app/paint.js index 872df5b8..7b76ac5d 100644 --- a/src/kiri/app/paint.js +++ b/src/kiri/app/paint.js @@ -17,6 +17,11 @@ * support regions. For multi-material coloring or other uses, set * options.onlyDownwardFacing = false to paint all surfaces. * + * BELT PRINTER MODE: + * For belt printers, surfaces need support when facing "down the belt slope" rather + * than straight down. Set options.beltMode = true and provide options.beltAngle (in radians) + * to paint surfaces that face down the angled belt direction. + * * PAINT POINT DATA STRUCTURE: * --------------------------- * Paint points must be an array of objects with this structure: @@ -79,6 +84,9 @@ import { THREE } from '../../ext/three.js'; * @param {THREE.Color} [paintColor] - Color for painted regions (default: light blue) * @param {Object} [options] - Optional configuration * @param {boolean} [options.onlyDownwardFacing=true] - Only paint on downward-facing surfaces (z-normal < 0) + * @param {boolean} [options.beltMode=false] - Enable belt printer mode (paints surfaces facing down belt slope) + * @param {number} [options.beltAngle=0] - Belt angle in radians (only used if beltMode is true) + * @param {number} [options.supportAngle=45] - Support angle in degrees (cone around down direction) * @returns {THREE.Material} The modified material */ function addPaintOverlaySimple(material, paintPoints, paintColor = new THREE.Color(0x4488ff), options = {}) { @@ -89,6 +97,10 @@ function addPaintOverlaySimple(material, paintPoints, paintColor = new THREE.Col // Default: only paint downward-facing surfaces (for support painting) const onlyDownwardFacing = options.onlyDownwardFacing !== undefined ? options.onlyDownwardFacing : true; + const beltMode = options.beltMode || false; + const beltAngle = options.beltAngle || 0; + const supportAngle = options.supportAngle || 45; + const supportThreshold = Math.cos((90 - supportAngle) * Math.PI / 180); // Store initial paint points reference material.userData.paintPoints = paintPoints; @@ -119,6 +131,9 @@ function addPaintOverlaySimple(material, paintPoints, paintColor = new THREE.Col shader.uniforms.paintCount = { value: validPoints.length }; shader.uniforms.paintColor = { value: paintColor }; shader.uniforms.onlyDownwardFacing = { value: onlyDownwardFacing }; + shader.uniforms.beltMode = { value: beltMode }; + shader.uniforms.beltAngle = { value: beltAngle }; + shader.uniforms.supportThreshold = { value: supportThreshold }; // Pass model position from vertex shader to fragment shader // Paint points are in widget/model space, so we compare against transformed (model space) @@ -143,6 +158,9 @@ function addPaintOverlaySimple(material, paintPoints, paintColor = new THREE.Col uniform int paintCount; uniform vec3 paintColor; uniform bool onlyDownwardFacing; + uniform bool beltMode; + uniform float beltAngle; + uniform float supportThreshold; ` + shader.fragmentShader; shader.fragmentShader = shader.fragmentShader.replace( @@ -156,7 +174,20 @@ function addPaintOverlaySimple(material, paintPoints, paintColor = new THREE.Col vec3 worldNormal = normalize(cross(posDx, posDy)); // Only paint downward-facing surfaces if option is enabled - bool shouldPaint = !onlyDownwardFacing || worldNormal.z < -0.001; + bool shouldPaint; + if (beltMode) { + // Belt mode: mesh is rotated, check if normal points toward world down + // World down (0,0,-1) appears as (0, -sin(θ), -cos(θ)) in rotated mesh space + // Dot product: normal · (0, -sin(θ), -cos(θ)) = -sin(θ)*ny - cos(θ)*nz + float sinAngle = sin(beltAngle); + float cosAngle = cos(beltAngle); + float rotatedZ = worldNormal.y * sinAngle + worldNormal.z * cosAngle; + // Check if within support angle cone from down direction (rotatedZ < -threshold) + shouldPaint = !onlyDownwardFacing || rotatedZ < -supportThreshold; + } else { + // Normal mode: check if normal points downward within support angle cone + shouldPaint = !onlyDownwardFacing || worldNormal.z < -supportThreshold; + } if (shouldPaint) { // Check if this fragment is within any paint sphere bool painted = false; @@ -206,6 +237,9 @@ function addPaintOverlaySimple(material, paintPoints, paintColor = new THREE.Col * @param {THREE.Color} [paintColor] - Color for painted regions (default: light blue) * @param {Object} [options] - Optional configuration * @param {boolean} [options.onlyDownwardFacing=true] - Only paint on downward-facing surfaces (z-normal < 0) + * @param {boolean} [options.beltMode=false] - Enable belt printer mode (paints surfaces facing down belt slope) + * @param {number} [options.beltAngle=0] - Belt angle in radians (only used if beltMode is true) + * @param {number} [options.supportAngle=45] - Support angle in degrees (cone around down direction) * @returns {THREE.Material} The modified material */ function addPaintOverlayTexture(material, paintPoints, paintColor = new THREE.Color(0x4488ff), options = {}) { @@ -216,6 +250,10 @@ function addPaintOverlayTexture(material, paintPoints, paintColor = new THREE.Co // Default: only paint downward-facing surfaces (for support painting) const onlyDownwardFacing = options.onlyDownwardFacing !== undefined ? options.onlyDownwardFacing : true; + const beltMode = options.beltMode || false; + const beltAngle = options.beltAngle || 0; + const supportAngle = options.supportAngle || 45; + const supportThreshold = Math.cos((90 - supportAngle) * Math.PI / 180); // Store initial paint points reference material.userData.paintPoints = paintPoints; @@ -264,6 +302,9 @@ function addPaintOverlayTexture(material, paintPoints, paintColor = new THREE.Co shader.uniforms.paintTexSize = { value: new THREE.Vector2(texWidth, texHeight) }; shader.uniforms.paintColor = { value: paintColor }; shader.uniforms.onlyDownwardFacing = { value: onlyDownwardFacing }; + shader.uniforms.beltMode = { value: beltMode }; + shader.uniforms.beltAngle = { value: beltAngle }; + shader.uniforms.supportThreshold = { value: supportThreshold }; // Pass model position from vertex shader to fragment shader // Paint points are in widget/model space, so we compare against transformed (model space) @@ -289,6 +330,9 @@ function addPaintOverlayTexture(material, paintPoints, paintColor = new THREE.Co uniform vec2 paintTexSize; uniform vec3 paintColor; uniform bool onlyDownwardFacing; + uniform bool beltMode; + uniform float beltAngle; + uniform float supportThreshold; ` + shader.fragmentShader; shader.fragmentShader = shader.fragmentShader.replace( @@ -302,7 +346,20 @@ function addPaintOverlayTexture(material, paintPoints, paintColor = new THREE.Co vec3 worldNormal = normalize(cross(posDx, posDy)); // Only paint downward-facing surfaces if option is enabled - bool shouldPaint = !onlyDownwardFacing || worldNormal.z < -0.001; + bool shouldPaint; + if (beltMode) { + // Belt mode: mesh is rotated, check if normal points toward world down + // World down (0,0,-1) appears as (0, -sin(θ), -cos(θ)) in rotated mesh space + // Dot product: normal · (0, -sin(θ), -cos(θ)) = -sin(θ)*ny - cos(θ)*nz + float sinAngle = sin(beltAngle); + float cosAngle = cos(beltAngle); + float rotatedZ = worldNormal.y * sinAngle + worldNormal.z * cosAngle; + // Check if within support angle cone from down direction (rotatedZ < -threshold) + shouldPaint = !onlyDownwardFacing || rotatedZ < -supportThreshold; + } else { + // Normal mode: check if normal points downward within support angle cone + shouldPaint = !onlyDownwardFacing || worldNormal.z < -supportThreshold; + } if (shouldPaint) { bool painted = false; diff --git a/src/kiri/mode/fdm/app/init-ui.js b/src/kiri/mode/fdm/app/init-ui.js index 2045f0ac..d74c7c74 100644 --- a/src/kiri/mode/fdm/app/init-ui.js +++ b/src/kiri/mode/fdm/app/init-ui.js @@ -267,7 +267,30 @@ function supportStart({ remove } = { remove: false }) { return api.widgets.meshes(); }; api.feature.on_mouse_drag = ({ int }) => { - if (int?.length && int[0].face.normal.z < 0) { + // Calculate if surface needs support based on belt mode or normal mode + let needsSupport = false; + if (int?.length) { + const settings = api.conf.get(); + const normal = int[0].face.normal; + + // Get support angle threshold (how steep before needing support) + const supportAngle = settings.process.sliceSupportAngle || 45; + const supportThreshold = Math.cos((90 - supportAngle) * Math.PI / 180); + + if (settings.device.bedBelt) { + // Belt mode: check if surface will face downward after belt rotation + // After rotating by angle θ around X: nz' = ny*sin(θ) + nz*cos(θ) + // We want nz' < -threshold (within cone of support angle from down) + const angle = (settings.process.sliceAngle || 45) * Math.PI / 180; + const rotatedZ = normal.y * Math.sin(angle) + normal.z * Math.cos(angle); + needsSupport = rotatedZ < -supportThreshold; + } else { + // Normal mode: check if normal points downward within support angle cone + needsSupport = normal.z < -supportThreshold; + } + } + + if (needsSupport) { supportUpdate(int[0].point, down.widget, remove); } return [ down.widget.mesh ]; @@ -279,6 +302,12 @@ function supportStart({ remove } = { remove: false }) { const scheme = colorSchemeRegistry.getScheme(mode, theme); const paintColor = scheme.operations?.paint?.overlay ?? 0x4488ff; + // Get belt configuration for paint constraint + const settings = api.conf.get(); + const isBelt = settings.device.bedBelt; + const beltAngle = isBelt ? (settings.process.sliceAngle || 45) : 0; + const supportAngle = settings.process.sliceSupportAngle || 45; + api.widgets.each(w => { // Use pushVisualState to track the paint operation w.pushVisualState('paint', { @@ -295,7 +324,11 @@ function supportStart({ remove } = { remove: false }) { if (!w.anno.paint) { w.anno.paint = []; } - addPaintOverlayTexture(mat, w.anno.paint, new THREE.Color(paintColor)); + addPaintOverlayTexture(mat, w.anno.paint, new THREE.Color(paintColor), { + beltMode: isBelt, + beltAngle: beltAngle * Math.PI / 180, // Convert to radians + supportAngle: settings.process.sliceSupportAngle || 45 + }); }) api.space.update(); } From a070b2c1340f5b7c2db69ff6a3ef4c946a6cfd62 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sat, 10 Jan 2026 19:38:36 -0500 Subject: [PATCH 150/154] add snapmaker u1. cull sparse fill dots --- ...Creality.Ender.7 => Creality.Ender.7.json} | 0 src/kiri/dev/fdm/Snapmaker.U1.json | 400 ++++++++++++++++++ src/kiri/mode/fdm/work/slice.js | 12 +- 3 files changed, 410 insertions(+), 2 deletions(-) rename src/kiri/dev/fdm/{Creality.Ender.7 => Creality.Ender.7.json} (100%) create mode 100644 src/kiri/dev/fdm/Snapmaker.U1.json diff --git a/src/kiri/dev/fdm/Creality.Ender.7 b/src/kiri/dev/fdm/Creality.Ender.7.json similarity index 100% rename from src/kiri/dev/fdm/Creality.Ender.7 rename to src/kiri/dev/fdm/Creality.Ender.7.json diff --git a/src/kiri/dev/fdm/Snapmaker.U1.json b/src/kiri/dev/fdm/Snapmaker.U1.json new file mode 100644 index 00000000..653fd140 --- /dev/null +++ b/src/kiri/dev/fdm/Snapmaker.U1.json @@ -0,0 +1,400 @@ +{ + "mode": "FDM", + "internal": 0, + "bedHeight": 2.5, + "bedWidth": 270, + "bedDepth": 270, + "bedRound": false, + "maxHeight": 270, + "originCenter": false, + "gcodeFan": [ + ";; >-----> FAN >------------------------>", + "M106 P1 S{fan_speed} ; set fan speed", + ";; <-----< FAN <------------------------<" + ], + "gcodeTrack": [ + ";; >-----> PROGRESS >------------------->", + "M73 P{progress} R{remain_time} ; report progress % and remaining time", + ";; <-----< PROGRESS <-------------------<" + ], + "gcodeLayer": [ + ";; >-----> LAYER >---------------------->", + ";BEFORE_LAYER_CHANGE", + ";[layer_z]", + "G92 E0", + "TIMELAPSE_TAKE_FRAME", + "DEFECT_DETECTION_DETECT", + ";AFTER_LAYER_CHANGE", + ";[layer_z]", + "SET_PRINT_STATS_INFO TOTAL_LAYER={layers}", + "SET_PRINT_STATS_INFO CURRENT_LAYER={layer}", + ";; <-----< LAYER <----------------------<" + ], + "gcodePre": [ + ";; >-----> HEADER >--------------------->", + ";; PREAMBLE POST", + "; HEADER_BLOCK_START", + "; generated by Kiri:Moto", + "; total layer number: {layers}", + "; filament_density: 1.24,1.24,1.24,1.24", + "; filament_diameter: 1.75,1.75,1.75,1.75", + "; max_z_height: {z_max}", + "; HEADER_BLOCK_END", + "; EXECUTABLE_BLOCK_START", + "M73 P0 R1 ; set print progress to 0%, remaining time 1 min", + "M106 S0 ; turn off primary cooling fan", + "M106 P2 S0 ; turn off auxiliary/part fan (fan index 2)", + "PRINT_START ; firmware macro: begin print sequence", + "DEFECT_DETECTION_START ; start bed surface defect detection system", + "SET_PRINT_STATS_INFO TOTAL_LAYER=3 ; set total layer count to 3", + "SET_PRINT_STATS_INFO CURRENT_LAYER=0 ; initialize current layer to 0", + "TIMELAPSE_START ; start timelapse capture", + "M140 S65 ; set bed temperature to 65C (no wait)", + "M104 T{tool} S140 ; set tool 0 nozzle temp to 140C (no wait)", + "M204 S10000 ; set default acceleration", + "G28 X Y ; home X and Y axes", + ";===== Bed surface foreign-object detection ========", + "T{tool} ; select tool 0", + "G90 ; absolute positioning", + "DEFECT_DETECTION_DETECT_BED ; scan bed for debris or foreign objects", + ";===== Tool pickup / toolhead detection =================", + "SM_PRINT_CHECK_SWITCH_EXTRUDER ; verify extruder/tool switch state", + ";===== Auto feed, flow calibration & pre-extrusion ======================", + "SM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140 ; preheat extruder 1", + "SM_PRINT_AUTO_FEED EXTRUDER=0 ; auto-load filament for extruder 0", + "SM_PRINT_FLOW_CALIBRATE EXTRUDER=0 ; calibrate flow for extruder 0", + "SM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140 ; preheat extruder 2", + "SM_PRINT_AUTO_FEED EXTRUDER=1 ; auto-load filament for extruder 1", + "SM_PRINT_FLOW_CALIBRATE EXTRUDER=1 ; calibrate flow for extruder 1", + "SM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140 ; preheat extruder 3", + "SM_PRINT_AUTO_FEED EXTRUDER=2 ; auto-load filament for extruder 2", + "SM_PRINT_FLOW_CALIBRATE EXTRUDER=2 ; calibrate flow for extruder 2", + "SM_PRINT_AUTO_FEED EXTRUDER=3 ; auto-load filament for extruder 3", + "SM_PRINT_FLOW_CALIBRATE EXTRUDER=3 ; calibrate flow for extruder 3", + "M104 S0 T0 A0 ; disable heater on tool 0", + "M104 S0 T1 A0 ; disable heater on tool 1", + "M104 S0 T2 A0 ; disable heater on tool 2", + "M104 S0 T3 A0 ; disable heater on tool 3", + "M104 T{tool} S130 ; set first tool nozzle temp to 130C", + ";===== Coarse homing =================", + "T{tool} ; select first tool", + "M106 S255 ; turn primary fan to full speed", + "M106 P2 S0 ; ensure auxiliary fan is off", + "MOVE_TO_DISCARD_FILAMENT_POSITION ; move to filament discard / purge location", + "M109 T{tool} S130 ; wait for nozzle to reach 130C", + "ROUGHLY_CLEAN_NOZZLE_WITH_DISCARD ; coarse nozzle cleaning with discard", + "MOVE_TO_XY_IDLE_POSITION_EXTRUDER ; move toolhead to idle XY position", + "G28 Z I140 J140 ; home Z axis with specified probe offsets", + ";===== Build plate detection =================", + "DETECT_BED_PLATE ; detect presence/type of build plate", + ";===== Deep nozzle cleaning =================", + "G90 ; absolute positioning", + "G0 Z5 F10000 ; raise Z to safe height", + "MOVE_TO_DISCARD_FILAMENT_POSITION ; move to discard position", + "M109 S170 ; heat nozzle to deep-clean temperature", + "ROUGHLY_CLEAN_NOZZLE ; coarse nozzle cleaning", + "MOVE_TO_XY_IDLE_POSITION_EXTRUDER ; move to idle position", + "FINELY_CLEAN_NOZZLE_STAGE_1 ; fine nozzle cleaning stage 1", + "M104 S130 ; lower nozzle temperature", + "G0 Z5 F10000 ; raise Z", + "MOVE_TO_DISCARD_FILAMENT_POSITION ; move to discard position again", + "ROUGHLY_CLEAN_NOZZLE ; repeat coarse cleaning", + "MOVE_TO_XY_IDLE_POSITION_EXTRUDER ; move to idle position", + "FINELY_CLEAN_NOZZLE_STAGE_2 ; fine nozzle cleaning stage 2", + ";===== Precision homing =================", + "M106 S255 ; set primary fan to full speed", + "M109 S130 ; wait for nozzle at 130C", + "M190 S65 ; wait for bed to reach 65C", + "M107 P2 ; turn off auxiliary fan", + "G90 ; absolute positioning", + "G0 Z5 F10000 ; lift Z before homing", + "G28 Z ; precise Z homing", + ";===== Heated bed leveling =================", + "BED_MESH_CALIBRATE PROBE_COUNT=11,11 ; probe bed mesh with 11x11 grid", + ";===== Draw purge / start line =================", + "G90 ; absolute positioning", + "G1 Z1.5 ; move nozzle near bed", + "G0 X10 Y3 Z2 F18000 ; move to purge start position", + "M109 S220 ; heat nozzle to print temperature", + "G1 Z0.2 ; move to purge height", + "M83 ; relative extrusion mode", + "G1 X110 E15 F360 ; extrude purge line", + "G1 Z1.5 ; lift nozzle", + "M73 P20 R1 ; update progress to 20%", + "G90 ; absolute positioning", + "M106 S0 ; ensure primary fan off", + "G90 ; absolute positioning (redundant safety)", + "G21 ; set units to millimeters", + "M83 ; relative extrusion mode", + "; filament start gcode", + "SET_PRESSURE_ADVANCE ADVANCE=0.02 ; override pressure advance value", + "T{tool} ; select first tool", + "M106 S0 ; ensure primary fan off", + "M106 P2 S0 ; ensure auxiliary fan off", + ";; <-----< HEADER <---------------------<" + ], + "gcodePost": [ + ";; >-----> FOOTER >--------------------->", + "M106 S0 ; turn off primary cooling fan", + "M106 P2 S0 ; turn off auxiliary/part fan (fan index 2)", + "PRINT_END", + "TIMELAPSE_STOP", + "M73 P100 R0 ; report progress complete", + ";; <-----< FOOTER <---------------------<" + ], + "gcodeProc": "", + "gcodeFExt": "gcode", + "extruders": [ + { + "extFilament": 1.75, + "extNozzle": 0.4, + "extSelect": [ + "; BEGIN tool change from {last_tool} to {tool}", + "M620 S{tool}A ; prepare tool change", + "M204 S9000 ; Set acceleration to 9000 mm/s^2", + "M620.11 S0 ; Reset filament retraction state", + "M400 ; wait for moves to complete", + "M620.1 E F187 T240 ; set purge rate 187 temp 240", + "T{tool} ; initiate tool change", + "M620.1 E F374 T240 ; set purge rate 374 temp 240", + "M620.11 S0 ; Reset filament retraction state", + "G92 E0 ; reset extruder position", + "M400 ; wait for moves to complete", + " ; -- PURGE --", + "M109 S240 ; Set nozzle temperature to 240°C", + "G1 E23.7 F187 ; Extrude 23.7mm of filament at flowrate 187", + "G1 E0.914611 F50 ; Pulsatile extrusion", + "G1 E10.518 F187 ; Continue extrusion at flowrate 187", + "G1 E0.914611 F50 ; Pulsatile extrusion", + "G1 E10.518 F374 ; Continue extrusion at flowrate 374", + "G1 E0.914611 F50 ; Pulsatile extrusion", + "G1 E10.518 F374 ; Continue extrusion at flowrate 374", + "G1 E0.914611 F50 ; Pulsatile extrusion", + "G1 E10.518 F374 ; Continue extrusion at flowrate 374", + "G1 E-2 F1800 ; Retract filament by 2mm", + "G1 E2 F300 ; Recover retraction slowly", + "M400 ; Wait for all movements to complete", + "M109 S{temp} ; Set nozzle temperature back to print temp", + " ; -- SHAKE --", + "M106 P1 S255 ; Turn on nozzle cooling fan to full speed", + "M400 S3 ; Wait for all movements to complete", + "G1 X70 F5000 ; Move to X70", + "G1 X90 F3000 ; Move to X90", + "G1 Y255 F4000 ; Move to Y255", + "G1 X105 F5000 ; Move to X105", + "G1 Y265 ; Move to Y265", + "G1 X70 F10000 ; Rapid move to X70", + "G1 X100 F5000 ; Move to X100", + "G1 X70 F10000 ; Rapid move back to X70", + "G1 X100 F5000 ; Move to X100", + "G1 X70 F10000 ; Shake filament residue", + "G1 X80 F15000", + "G1 X60", + "G1 X80", + "G1 X60", + "G1 X80 ; Shake to put down garbage", + "G1 X100 F5000 ; Move to X100", + "G1 X165 F15000 ; Wipe and shake", + "G1 Y256 ; Move Y aside to prevent collision", + "M400 ; Wait for all movements to complete", + "M204 S10000 ; Set acceleration to 10000 mm/s^2", + "M621 S{tool}A ; complete tool change", + "; END tool change" + ], + "extOffsetX": 0, + "extOffsetY": 0, + "extDeselect": [] + }, + { + "extFilament": 1.75, + "extNozzle": 0.4, + "extSelect": [], + "extOffsetX": 0, + "extOffsetY": 0, + "extDeselect": [] + }, + { + "extFilament": 1.75, + "extNozzle": 0.4, + "extSelect": [], + "extOffsetX": 0, + "extOffsetY": 0, + "extDeselect": [] + }, + { + "extFilament": 1.75, + "extNozzle": 0.4, + "extSelect": [], + "extOffsetX": 0, + "extOffsetY": 0, + "extDeselect": [] + } + ], + "new": false, + "deviceName": "My Snapmaker U1", + "bedBelt": false, + "fwRetract": false, + "filamentSource": "direct", + "extras": { + "bbl": { + "luke": { + "host": "10.20.20.75", + "code": "20709223", + "serial": "01P09C482401767", + "modified": true, + "selected": false, + "type": "C12" + }, + "leia": { + "host": "10.20.20.76", + "code": "35129782", + "serial": "01P09C461603301", + "modified": true, + "selected": false, + "type": "C12" + }, + "vader": { + "host": "10.20.20.77", + "code": "17711811", + "serial": "01P00A413100516", + "modified": true, + "selected": false, + "type": "C12" + }, + "Scrappy": { + "host": "10.10.2.5", + "serial": "01S00A292500224", + "code": "38373335", + "modified": true, + "type": "C11", + "selected": true + } + } + }, + "imageURL": "", + "deviceZMax": 0, + "gcodeTime": 1, + "gcodeFeature": [], + "profiles": [ + { + "processName": "U1 PLA", + "sliceHeight": 0.2, + "sliceShells": 3, + "sliceShellOrder": "out-in", + "sliceLayerStart": "last", + "sliceFillAngle": 45, + "sliceFillOverlap": 0.4, + "sliceFillSparse": 0.2, + "sliceFillType": "linear", + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.2, + "sliceSupportOffset": 0.2, + "sliceSupportGap": 1, + "sliceSupportExtra": 0, + "sliceSupportAngle": 50, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 1, + "sliceBottomLayers": 2, + "sliceTopLayers": 3, + "firstLayerRate": 20, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBeltLead": 0, + "firstLayerFanSpeed": 0, + "outputTemp": 220, + "outputBedTemp": 65, + "outputFanSpeed": 255, + "outputFeedrate": 100, + "outputFinishrate": 90, + "outputSeekrate": 200, + "outputShellMult": 1.2, + "outputFillMult": 1.18, + "outputSparseMult": 1.25, + "outputRetractDist": 1.3, + "outputRetractSpeed": 30, + "outputRetractWipe": 0, + "outputRetractDwell": 5, + "outputShortPoly": 0, + "outputMinSpeed": 1, + "outputCoastDist": 0, + "outputLayerRetract": false, + "zHopDistance": 0, + "antiBacklash": 0, + "sliceFillWidth": 1, + "firstSliceHeight": 0.2, + "firstLayerFillRate": 40, + "firstLayerNozzleTemp": 220, + "firstLayerBedTemp": 65, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputBrimCount": 0, + "outputBrimOffset": 1, + "outputPurgeTower": 0, + "outputInvertX": false, + "outputInvertY": false, + "ranges": [], + "sliceLineWidth": 0, + "sliceFillRepeat": 2, + "firstLayerBrimIn": 0, + "firstLayerBeltBump": 0, + "outputBeltFirst": false, + "outputLoops": 0, + "sliceFillGrow": 0.2, + "sliceSupportSpan": 5, + "sliceSupportOutline": true, + "firstLayerFlatten": 0, + "outputDraftShield": false, + "outputAvoidGaps": true, + "sliceDetectThin": "off", + "outputAlternating": false, + "sliceLayerStartX": 0, + "sliceLayerStartY": 0, + "outputFanLayer": 1, + "sliceAngle": 45, + "sliceZInterleave": false, + "outputScarfLength": 0, + "outputMinLayerTime": 9, + "sliceCompInner": 0, + "sliceCompOuter": 0, + "outputMaxFlowrate": 15, + "firstLayerBeltFact": 1, + "sliceSolidify": false, + "fdmArcEnabled": false, + "fdmArcResolution": 1, + "fdmArcTolerance": 0.005, + "sliceSupportType": "manual", + "sliceSupportTree": false, + "sliceSolidMinThick": 0.4 + } + ], + "gcodeChange": [ + ";; >-----> TOOL >----------------------->", + "; Change Tool{last_tool} -> Tool{tool} (layer {layer}) ; tool change", + "M104 S70 T{last_tool} ; cooldown previous tool", + "G91 ; set relative positioning", + "G1 Z1.5 ; lift nozzle 1.5 mm to avoid collision", + "G90 ; return to absolute positioning", + "G1 F21000 ; set high travel feedrate", + "M104 S{temp} T{tool} ; heat new tool and wait", + "M400 ; wait for all queued moves to finish", + "T{tool} ; select tool {tool}", + "SM_PRINT_PREEXTRUDE_FILAMENT INDEX={tool} ; Snapmaker macro: pre-extrude tool filament", + "G90 ; ensure absolute positioning", + "M106 P2 S178 ; set auxiliary/part fan to ~70%", + "M106 S255 ; set primary cooling fan to full speed", + "M109 S{temp} T{tool} ; set new tool nozzle temperature", + "SET_PRESSURE_ADVANCE ADVANCE=0.02 ; override pressure advance value for tool 1", + "G1 F30000 ; set printing feedrate", + ";ADD WIPE TOWER HERE", + "M220 R ; reset feedrate override to default (100%) / restore", + "G1 F30000 ; set travel feedrate to 30000 mm/min", + "G4 S0 ; dwell 0s (no-op / sync point)", + "G92 E0 ; zero extruder position after purge (prevents huge E values)", + "G1 X{pos_x} Y{pos_y} ; restore XY position", + "G1 Z{pos_z} ; restore Z position", + ";; <-----< TOOL <-----------------------<" + ] +} \ No newline at end of file diff --git a/src/kiri/mode/fdm/work/slice.js b/src/kiri/mode/fdm/work/slice.js index b52ba4a8..86055429 100644 --- a/src/kiri/mode/fdm/work/slice.js +++ b/src/kiri/mode/fdm/work/slice.js @@ -570,10 +570,10 @@ export function sliceOne(settings, widget, onupdate, ondone) { .centerRectangle(newPoint(0, 0, slice.z), boundsx, boundsy) .move({ x: 0, y: -boundsy / 2 + skewy, z: 0 }); shadow = POLY.trimTo(shadow, [ clip ]); - // slice.output().setLayer("belt clip", 0xffff00).addPolys([ clip ]); + if (devel) slice.output().setLayer("belt clip", 0xffff00).addPolys([ clip ]); } slice.supports = shadow; - slice.output().setLayer("shadow", 0xff0000).addPolys(shadow); + if (devel) slice.output().setLayer("shadow", 0xff0000).addPolys(shadow); trackupdate((++count/length), 0.10, 0.15, "support"); } } @@ -692,6 +692,14 @@ export function sliceOne(settings, widget, onupdate, ondone) { trackupdate(i / t, 0.55, 0.7); }); } + // filter out tiny fill points less than nozzle diameter + // if (false) + for (let slice of slices) { + for (let top of slice.tops) { + if (top.fill_sparse) + top.fill_sparse = top.fill_sparse.filter(p => p.perimeter() >= lineWidth); + } + } // back-fill slices marked for infill cloning for (let slice of slices) { if (slice._clone_sparse) { From 371372a195dbb25e8fa108c6c455972e8c6e20bb Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sat, 10 Jan 2026 20:11:28 -0500 Subject: [PATCH 151/154] belt angle code cleanup. rm old gridbots devices --- src/kiri/dev/fdm/GridBot.One.json | 43 ------------------------------- src/kiri/dev/fdm/GridBot.Two.json | 43 ------------------------------- src/kiri/mode/fdm/app/init-ui.js | 6 ++--- 3 files changed, 3 insertions(+), 89 deletions(-) delete mode 100644 src/kiri/dev/fdm/GridBot.One.json delete mode 100644 src/kiri/dev/fdm/GridBot.Two.json diff --git a/src/kiri/dev/fdm/GridBot.One.json b/src/kiri/dev/fdm/GridBot.One.json deleted file mode 100644 index 21ccb365..00000000 --- a/src/kiri/dev/fdm/GridBot.One.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "pre":[ - "M117 Heating ; announce heating", - "M107 ; turn off filament cooling fan", - "M104 S{temp} T0 ; set extruder temperature", - "M140 S{bed_temp} T0 ; set bed temperature", - "G90 ; set absolute positioning mode", - "M83 ; set relative positioning for extruder", - "G28 ; home axes", - "G1 F3000 ; set default move speed", - "M190 S{bed_temp} T0 ; wait for bed to reach target temp", - "M109 S{temp} T0 ; wait for extruder to reach target temp", - "M117 Purge ; announce purge", - "G92 E0 ; set extruder position to 0", - "G1 E20 F250 ; purge 20mm from extruder", - "G1 X0 Y0 Z0 E1 F3000 ; move to origin / wipe", - "M117 Start Print ; announce start of print" - ], - "post":[ - "M117 Done ; announce done", - "M107 ; turn off filament cooling fan", - "M104 S0 T0 ; turn off extruder", - "M140 S0 T0 ; turn off bed", - "G91 ; relative movement", - "G1 Z10 F1200 ; drop bed 10mm", - "G90 ; absolute positioning", - "G28 X0 Y0 ; home XY axes", - "M84 ; disable stepper motors" - ], - "cmd":{ - "fan_power": "M106 S{fan_speed}", - "progress": "M117 {progress}% layer {layer} / {layers}" - }, - "settings":{ - "image_url": "/decal/gridbot.png", - "image_anchor": 0, - "image_scale": 1, - "origin_center": false, - "bed_width": 300, - "bed_depth": 300, - "build_height": 300 - } -} diff --git a/src/kiri/dev/fdm/GridBot.Two.json b/src/kiri/dev/fdm/GridBot.Two.json deleted file mode 100644 index 21ccb365..00000000 --- a/src/kiri/dev/fdm/GridBot.Two.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "pre":[ - "M117 Heating ; announce heating", - "M107 ; turn off filament cooling fan", - "M104 S{temp} T0 ; set extruder temperature", - "M140 S{bed_temp} T0 ; set bed temperature", - "G90 ; set absolute positioning mode", - "M83 ; set relative positioning for extruder", - "G28 ; home axes", - "G1 F3000 ; set default move speed", - "M190 S{bed_temp} T0 ; wait for bed to reach target temp", - "M109 S{temp} T0 ; wait for extruder to reach target temp", - "M117 Purge ; announce purge", - "G92 E0 ; set extruder position to 0", - "G1 E20 F250 ; purge 20mm from extruder", - "G1 X0 Y0 Z0 E1 F3000 ; move to origin / wipe", - "M117 Start Print ; announce start of print" - ], - "post":[ - "M117 Done ; announce done", - "M107 ; turn off filament cooling fan", - "M104 S0 T0 ; turn off extruder", - "M140 S0 T0 ; turn off bed", - "G91 ; relative movement", - "G1 Z10 F1200 ; drop bed 10mm", - "G90 ; absolute positioning", - "G28 X0 Y0 ; home XY axes", - "M84 ; disable stepper motors" - ], - "cmd":{ - "fan_power": "M106 S{fan_speed}", - "progress": "M117 {progress}% layer {layer} / {layers}" - }, - "settings":{ - "image_url": "/decal/gridbot.png", - "image_anchor": 0, - "image_scale": 1, - "origin_center": false, - "bed_width": 300, - "bed_depth": 300, - "build_height": 300 - } -} diff --git a/src/kiri/mode/fdm/app/init-ui.js b/src/kiri/mode/fdm/app/init-ui.js index d74c7c74..00837bd5 100644 --- a/src/kiri/mode/fdm/app/init-ui.js +++ b/src/kiri/mode/fdm/app/init-ui.js @@ -305,7 +305,7 @@ function supportStart({ remove } = { remove: false }) { // Get belt configuration for paint constraint const settings = api.conf.get(); const isBelt = settings.device.bedBelt; - const beltAngle = isBelt ? (settings.process.sliceAngle || 45) : 0; + const beltAngle = isBelt ? (settings.process.sliceAngle || 45) * Math.PI / 180 : 0; const supportAngle = settings.process.sliceSupportAngle || 45; api.widgets.each(w => { @@ -326,8 +326,8 @@ function supportStart({ remove } = { remove: false }) { } addPaintOverlayTexture(mat, w.anno.paint, new THREE.Color(paintColor), { beltMode: isBelt, - beltAngle: beltAngle * Math.PI / 180, // Convert to radians - supportAngle: settings.process.sliceSupportAngle || 45 + beltAngle, + supportAngle }); }) api.space.update(); From 3e85a2c8f8a23b7c5e3b369c973b22ed6a56cf1e Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sat, 10 Jan 2026 20:50:16 -0500 Subject: [PATCH 152/154] add cors to all /lib paths to make safari happy in dev mode --- app.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/app.js b/app.js index bf380607..758da462 100644 --- a/app.js +++ b/app.js @@ -579,16 +579,19 @@ function cookieValue(cookie,key) { } function addAppHeaders(req, res, next) { + if (req.app.path.indexOf("/lib") === 0) { + addCorsHeaders(req, res); + } else if ([ "/kiri/", "/mesh/", - "/lib/gpu/raster.js", - "/lib/gpu/raster-worker.js", - "/lib/mesh/work.js", - "/lib/kiri/run/worker.js", - "/lib/kiri/run/minion.js", - "/lib/kiri/run/engine.js", - "/lib/kiri/run/frame.js" + // "/lib/gpu/raster.js", + // "/lib/gpu/raster-worker.js", + // "/lib/mesh/work.js", + // "/lib/kiri/run/worker.js", + // "/lib/kiri/run/minion.js", + // "/lib/kiri/run/engine.js", + // "/lib/kiri/run/frame.js" ].indexOf(req.app.path) >= 0) { addCorsHeaders(req, res); } From 4c79aac48faa1150d1b4ef1d2ed525089b46162d Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sat, 10 Jan 2026 21:01:20 -0500 Subject: [PATCH 153/154] normalize dolly in/out across browsers --- app.js | 10 +--------- src/moto/orbit.js | 33 +++++++++++++++++++++++++++++---- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/app.js b/app.js index 758da462..fb6f1459 100644 --- a/app.js +++ b/app.js @@ -581,17 +581,9 @@ function cookieValue(cookie,key) { function addAppHeaders(req, res, next) { if (req.app.path.indexOf("/lib") === 0) { addCorsHeaders(req, res); - } else - if ([ + } else if ([ "/kiri/", "/mesh/", - // "/lib/gpu/raster.js", - // "/lib/gpu/raster-worker.js", - // "/lib/mesh/work.js", - // "/lib/kiri/run/worker.js", - // "/lib/kiri/run/minion.js", - // "/lib/kiri/run/engine.js", - // "/lib/kiri/run/frame.js" ].indexOf(req.app.path) >= 0) { addCorsHeaders(req, res); } diff --git a/src/moto/orbit.js b/src/moto/orbit.js index 74dd86b7..54158ce5 100644 --- a/src/moto/orbit.js +++ b/src/moto/orbit.js @@ -354,7 +354,7 @@ class Orbit extends EventDispatcher { function getZoomScale(factor = 1) { // Use slower zoom for orthographic mode - let base = mode === MODE.ORTHOGRAPHIC ? 0.98 : 0.95; + let base = mode === MODE.ORTHOGRAPHIC ? 0.98 : 0.97; return Math.pow(base, factor); } @@ -461,7 +461,31 @@ class Orbit extends EventDispatcher { slider(null); - let delta = -(event.deltaY || event.wheelDelta || event.detail || 0); + // Normalize delta across browsers + let delta = 0; + if (event.wheelDelta !== undefined) { + // Chrome/Safari use wheelDelta (positive = scroll up, zoom out) + delta = -event.wheelDelta; + } else if (event.detail !== undefined) { + // Old Firefox DOMMouseScroll uses detail + delta = event.detail * 40; // Normalize to pixel values + } else if (event.deltaY !== undefined) { + // Modern browsers (including Firefox) use deltaY + // Firefox can send deltas in different units (pixels, lines, pages) + delta = event.deltaY; + // Firefox's deltaMode indicates the unit of deltaY + // DOM_DELTA_PIXEL (0x00) - pixels + // DOM_DELTA_LINE (0x01) - lines (default for Firefox, ~3 units per notch) + // DOM_DELTA_PAGE (0x02) - pages + if (event.deltaMode === 1) { + // LINE mode: Firefox typically sends values around 3 per scroll notch + // Multiply by a smaller factor to make it less sensitive + delta = delta * 10; + } else if (event.deltaMode === 2) { + // PAGE mode: very large deltas + delta = delta * 120; + } + } if (delta === 0) return; @@ -657,8 +681,9 @@ class Orbit extends EventDispatcher { domEl.addEventListener('contextmenu', function (event) { event.preventDefault() }, false); domEl.addEventListener('mousedown', onMouseDown, false); - domEl.addEventListener('mousewheel', onMouseWheel, false); - domEl.addEventListener('DOMMouseScroll', onMouseWheel, false); // firefox + domEl.addEventListener('wheel', onMouseWheel, false); // Modern standard (Chrome, Safari, Firefox) + domEl.addEventListener('mousewheel', onMouseWheel, false); // Legacy Chrome/Safari + domEl.addEventListener('DOMMouseScroll', onMouseWheel, false); // Legacy Firefox domEl.addEventListener('touchstart', touchstart, false); domEl.addEventListener('touchend', touchend, false); domEl.addEventListener('touchmove', touchmove, false); From 8d647140e8398851851d6d33a68a2a1557595cbe Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sun, 11 Jan 2026 15:29:05 -0500 Subject: [PATCH 154/154] add scroll/dolly notes --- src/moto/orbit.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/moto/orbit.js b/src/moto/orbit.js index 54158ce5..4e7204e8 100644 --- a/src/moto/orbit.js +++ b/src/moto/orbit.js @@ -462,16 +462,18 @@ class Orbit extends EventDispatcher { slider(null); // Normalize delta across browsers + // Goal: scroll up = negative delta = zoom in, scroll down = positive delta = zoom out let delta = 0; if (event.wheelDelta !== undefined) { - // Chrome/Safari use wheelDelta (positive = scroll up, zoom out) + // Chrome/Safari wheelDelta: scroll up = +120, scroll down = -120 + // Negate to match deltaY convention delta = -event.wheelDelta; } else if (event.detail !== undefined) { - // Old Firefox DOMMouseScroll uses detail + // Old Firefox DOMMouseScroll detail: scroll up = -3, scroll down = +3 delta = event.detail * 40; // Normalize to pixel values } else if (event.deltaY !== undefined) { - // Modern browsers (including Firefox) use deltaY - // Firefox can send deltas in different units (pixels, lines, pages) + // Modern browsers deltaY: scroll up = negative, scroll down = positive + // Already matches our convention delta = event.deltaY; // Firefox's deltaMode indicates the unit of deltaY // DOM_DELTA_PIXEL (0x00) - pixels