From 17574f76b962e5e0f8471a4e8236bc604f602bc3 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 20 Mar 2026 12:21:54 -0400 Subject: [PATCH] add dxf spline support --- src/kiri/app/platform.js | 4 +- src/load/dxf.js | 181 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 183 insertions(+), 2 deletions(-) diff --git a/src/kiri/app/platform.js b/src/kiri/app/platform.js index e7875fbb..2ef9ee30 100644 --- a/src/kiri/app/platform.js +++ b/src/kiri/app/platform.js @@ -1147,11 +1147,11 @@ function loadSVGDialog(doit) { */ function loadDXFDialog(doit) { const opt = {pre: [ - "
", + "
", "

Import DXF

", "

", " Extrude a 3D model from a 2D DXF.", - " Supports POLYLINE, LWPOLYLINE, LINE, CIRCLE, and ARC entities.", + " Supports POLYLINE, LWPOLYLINE, LINE, CIRCLE, ARC, and SPLINE entities.", "

", "
", " ", diff --git a/src/load/dxf.js b/src/load/dxf.js index b7b38aa8..d30d9fbd 100644 --- a/src/load/dxf.js +++ b/src/load/dxf.js @@ -87,6 +87,27 @@ export function parse(text, opt = { }) { let poly = newPolygon().addPoints(points); poly.setOpen(true); polys.push(poly); + } else if (entity.type === 'SPLINE') { + // Convert NURBS spline to polyline by sampling + if (entity.controlPoints.length < 2) { + continue; + } + + const points = evaluateSpline(entity, segmentSize, minSegments); + if (points.length < 2) { + continue; + } + + let poly = newPolygon().addPoints(points); + if (entity.closed) { + // Remove duplicate end point if closed + if (poly.appearsClosed()) { + poly.points.pop(); + } + } else { + poly.setOpen(true); + } + polys.push(poly); } } @@ -173,6 +194,13 @@ function extractEntities(lines) { i = entity.endIndex; continue; } + } else if (value === 'SPLINE') { + const entity = parseSpline(lines, i); + if (entity) { + entities.push(entity); + i = entity.endIndex; + continue; + } } } @@ -357,3 +385,156 @@ function parseArc(lines, start) { return { type: 'ARC', center, radius, startAngle, endAngle, endIndex: i }; } + +function parseSpline(lines, start) { + let i = start + 2; + let degree = 3; // default cubic + let closed = false; + const controlPoints = []; + const knots = []; + let numKnots = 0; + let numControlPoints = 0; + + while (i < lines.length - 1) { + const code = lines[i]; + const value = lines[i + 1]; + + if (code === '70') { + // Spline flag: bit 0 (1) = closed + closed = (parseInt(value) & 1) === 1; + } + if (code === '71') degree = parseInt(value); + if (code === '72') numKnots = parseInt(value); + if (code === '73') numControlPoints = parseInt(value); + if (code === '40') { + // Knot value + knots.push(parseFloat(value)); + } + if (code === '10') { + // Control point X - start new point + controlPoints.push({ x: parseFloat(value), y: 0, z: 0 }); + } + if (code === '20' && controlPoints.length > 0) { + // Control point Y + controlPoints[controlPoints.length - 1].y = parseFloat(value); + } + if (code === '30' && controlPoints.length > 0) { + // Control point Z + controlPoints[controlPoints.length - 1].z = parseFloat(value); + } + + if (code === '0') { + return { type: 'SPLINE', degree, closed, knots, controlPoints, endIndex: i }; + } + + i += 2; + } + + return { type: 'SPLINE', degree, closed, knots, controlPoints, endIndex: i }; +} + +// Evaluate NURBS B-spline curve to generate sample points +function evaluateSpline(entity, segmentSize, minSegments) { + const { degree, controlPoints, knots, closed } = entity; + + if (controlPoints.length < degree + 1 || knots.length === 0) { + // Degenerate spline, just return control points + return controlPoints.map(p => newPoint(p.x, p.y, p.z)); + } + + // Estimate curve length by summing control point distances (rough approximation) + let estimatedLength = 0; + for (let i = 1; i < controlPoints.length; i++) { + const dx = controlPoints[i].x - controlPoints[i-1].x; + const dy = controlPoints[i].y - controlPoints[i-1].y; + estimatedLength += Math.sqrt(dx * dx + dy * dy); + } + + // Calculate number of samples + const numSamples = Math.max(minSegments, Math.ceil(estimatedLength / segmentSize)); + const points = []; + + // Find parameter range (first and last non-repeated knot values) + const knotStart = knots[degree]; + const knotEnd = knots[knots.length - degree - 1]; + + if (knotStart >= knotEnd) { + // Invalid knot vector, return control points + return controlPoints.map(p => newPoint(p.x, p.y, p.z)); + } + + // Sample the curve + for (let i = 0; i <= numSamples; i++) { + const t = knotStart + (i / numSamples) * (knotEnd - knotStart); + const point = evaluateNURBS(t, degree, controlPoints, knots); + points.push(newPoint(point.x, point.y, point.z)); + } + + return points; +} + +// Evaluate a single point on a NURBS curve using De Boor's algorithm +function evaluateNURBS(t, degree, controlPoints, knots) { + const n = controlPoints.length - 1; + + // Clamp t to valid range + t = Math.max(knots[degree], Math.min(knots[n + 1], t)); + + // Find knot span (which segment t falls into) + let span = degree; + while (span <= n && knots[span + 1] <= t) { + span++; + } + if (span > n) span = n; + + // Compute basis functions using Cox-de Boor recursion + const N = []; + for (let i = 0; i <= n; i++) { + N[i] = []; + } + + // Initialize degree 0 basis functions + for (let i = 0; i <= n; i++) { + if (t >= knots[i] && t < knots[i + 1]) { + N[i][0] = 1.0; + } else { + N[i][0] = 0.0; + } + } + // Special case for last knot + if (t === knots[n + 1]) { + N[n][0] = 1.0; + } + + // Compute higher degree basis functions + for (let k = 1; k <= degree; k++) { + for (let i = 0; i <= n; i++) { + let c1 = 0, c2 = 0; + + if (N[i][k - 1] !== 0) { + if (knots[i + k] !== knots[i]) { + c1 = ((t - knots[i]) / (knots[i + k] - knots[i])) * N[i][k - 1]; + } + } + + if (i + 1 <= n && N[i + 1][k - 1] !== 0) { + if (knots[i + k + 1] !== knots[i + 1]) { + c2 = ((knots[i + k + 1] - t) / (knots[i + k + 1] - knots[i + 1])) * N[i + 1][k - 1]; + } + } + + N[i][k] = c1 + c2; + } + } + + // Compute curve point as weighted sum of control points + let x = 0, y = 0, z = 0; + for (let i = 0; i <= n; i++) { + const weight = N[i][degree] || 0; + x += controlPoints[i].x * weight; + y += controlPoints[i].y * weight; + z += controlPoints[i].z * weight; + } + + return { x, y, z }; +}
z height in mm