diff --git a/src/kiri/app/init/menu.js b/src/kiri/app/init/menu.js
index 8c936c6f..bbdcf9d8 100644
--- a/src/kiri/app/init/menu.js
+++ b/src/kiri/app/init/menu.js
@@ -126,7 +126,7 @@ function content(actions) {
menuItem(actions, { id: 'file-recent', lk: 'recent', text: 'recent', iconClass: 'fas fa-list' }),
menuItem(actions, {
id: 'file-import', lk: 'import', text: 'import', iconClass: 'fas fa-file-upload', children:
- input({ id: 'load-file', type: 'file', name: 'loadme', style: 'display:none', accept: '.km,.kmz,.stl,.obj,.svg,.png,.jpg,.jpeg,.gcode,.nc' })
+ input({ id: 'load-file', type: 'file', name: 'loadme', style: 'display:none', accept: '.km,.kmz,.stl,.obj,.svg,.dxf,.png,.jpg,.jpeg,.gcode,.nc' })
}),
hr(),
menuItem(actions, { id: 'mesh-export-obj', lk: 'export-obj', text: 'save as OBJ', iconClass: 'fas fa-dice-d20' }),
diff --git a/src/kiri/app/platform.js b/src/kiri/app/platform.js
index 8763bbae..e7875fbb 100644
--- a/src/kiri/app/platform.js
+++ b/src/kiri/app/platform.js
@@ -979,6 +979,7 @@ function load_files(files, group) {
isobj = lower.endsWith(".obj"),
is3mf = lower.endsWith(".3mf"),
issvg = lower.endsWith(".svg"),
+ isdxf = lower.endsWith(".dxf"),
ispng = lower.endsWith(".png"),
isjpg = lower.endsWith(".jpg"),
iskmz = lower.endsWith(".kmz"),
@@ -1067,7 +1068,7 @@ function load_files(files, group) {
api.function.parse(data.textDecode('utf-8'), 'gcode');
load_dec();
} else if (issvg) {
- loadSVGDialog(opt => {
+ loadSVGDialog(opt => {
group = group || [];
let svg = file_load.SVG.parse(data.textDecode('utf-8'), opt);
let ind = 0;
@@ -1080,6 +1081,19 @@ function load_files(files, group) {
}
load_dec();
});
+ } else if (isdxf) {
+ loadDXFDialog(opt => {
+ group = group || [];
+ let dxf = file_load.DXF.parse(data.textDecode('utf-8'), opt);
+ let ind = 0;
+ if (dxf.length === 0) {
+ api.show.alert(`DXF contains no supported entities`, 10);
+ }
+ for (let v of dxf) {
+ load_verts(group, dxf[ind++], ind ? `${name}-${ind}` : name);
+ }
+ load_dec();
+ });
}
else if (iskmz) api.settings.import_zip(data, true);
else if (isset) api.settings.import(data.textDecode('utf-8'), true);
@@ -1125,6 +1139,37 @@ function loadSVGDialog(doit) {
});
}
+/**
+ * Show dialog to configure DXF import settings.
+ * Prompts for extrusion depth, arc segment size, and nesting.
+ * @param {Function} doit - Callback with options: {soup, depth, segmentSize, minSegments}
+ * @private
+ */
+function loadDXFDialog(doit) {
+ const opt = {pre: [
+ "
",
+ "
Import DXF
",
+ "
",
+ " Extrude a 3D model from a 2D DXF.",
+ " Supports POLYLINE, LWPOLYLINE, LINE, CIRCLE, and ARC entities.",
+ "
",
+ "
",
+ "
"
+ ]};
+ api.uc.confirm(undefined, {convert:true, cancel:false}, undefined, opt).then((ok) => {
+ let depth = Math.max(0.1, parseFloat($('dxf-depth').value));
+ let segmentSize = Math.max(0.01, parseFloat($('dxf-seg').value));
+ let minSegments = Math.max(3, parseInt($('dxf-min').value));
+ let soup = $('dxf-nest').checked;
+ ok && doit({ soup, depth, segmentSize, minSegments });
+ });
+}
+
/**
* Expand platform bed depth to fit widgets (belt mode only).
* Finds maximum Y dimension of all widgets and expands bed if needed.
diff --git a/src/load/dxf.js b/src/load/dxf.js
new file mode 100644
index 00000000..b7b38aa8
--- /dev/null
+++ b/src/load/dxf.js
@@ -0,0 +1,359 @@
+/** Copyright Stewart Allen -- All Rights Reserved */
+
+import { newPolygon } from '../geo/polygon.js';
+import { newPoint } from '../geo/point.js';
+import { polygons } from '../geo/polygons.js';
+
+export function parseAsync(text, opt) {
+ return new Promise((resolve, reject) => {
+ try {
+ resolve(parse(text, opt));
+ } catch (e) {
+ reject(e);
+ }
+ });
+}
+
+export function parse(text, opt = { }) {
+ const justPoly = opt.flat || false;
+ const fromSoup = opt.soup !== false || justPoly;
+ const depth = parseFloat(opt.depth || 5);
+ const segmentSize = parseFloat(opt.segmentSize || 1); // default 1mm segments
+ const minSegments = parseInt(opt.minSegments || 4); // minimum segments for very small arcs
+ const objs = [];
+ const polys = [];
+
+ // Parse DXF file - normalize line endings and split
+ const lines = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n').map(l => l.trim());
+ const entities = extractEntities(lines);
+
+ // Convert entities to polygons
+ for (let entity of entities) {
+ if (entity.type === 'POLYLINE' || entity.type === 'LWPOLYLINE') {
+ if (entity.points.length < 2) {
+ continue;
+ }
+
+ let poly = newPolygon().addPoints(
+ entity.points.map(p => newPoint(p.x, p.y, p.z || 0))
+ ).clean();
+
+ // Check if closed
+ if (entity.closed && poly.appearsClosed()) {
+ poly.points.pop();
+ } else if (!entity.closed) {
+ poly.setOpen(true);
+ }
+
+ polys.push(poly);
+ } else if (entity.type === 'LINE') {
+ // Convert line to polyline
+ let poly = newPolygon().addPoints([
+ newPoint(entity.start.x, entity.start.y, entity.start.z || 0),
+ newPoint(entity.end.x, entity.end.y, entity.end.z || 0)
+ ]);
+ poly.setOpen(true);
+ polys.push(poly);
+ } else if (entity.type === 'CIRCLE') {
+ // Convert circle to polygon with points
+ // Calculate segments based on circumference and desired segment size
+ const circumference = 2 * Math.PI * entity.radius;
+ const segments = Math.max(minSegments, Math.ceil(circumference / segmentSize));
+ let points = [];
+ for (let i = 0; i < segments; i++) {
+ const angle = (i / segments) * Math.PI * 2;
+ points.push(newPoint(
+ entity.center.x + Math.cos(angle) * entity.radius,
+ entity.center.y + Math.sin(angle) * entity.radius,
+ entity.center.z || 0
+ ));
+ }
+ let poly = newPolygon().addPoints(points).clean();
+ polys.push(poly);
+ } else if (entity.type === 'ARC') {
+ // Convert arc to polyline
+ // Calculate segments based on arc length and desired segment size
+ const arcLength = Math.abs(entity.endAngle - entity.startAngle) * entity.radius;
+ const segments = Math.max(minSegments, Math.ceil(arcLength / segmentSize));
+ let points = [];
+ for (let i = 0; i <= segments; i++) {
+ const angle = entity.startAngle + (i / segments) * (entity.endAngle - entity.startAngle);
+ points.push(newPoint(
+ entity.center.x + Math.cos(angle) * entity.radius,
+ entity.center.y + Math.sin(angle) * entity.radius,
+ entity.center.z || 0
+ ));
+ }
+ let poly = newPolygon().addPoints(points);
+ poly.setOpen(true);
+ polys.push(poly);
+ }
+ }
+
+ // Nest polygons to identify holes vs outlines
+ const sub = fromSoup ? polygons.nest(polys) : polys;
+ const nest = sub.filter(p => {
+ for (let pc of polys) {
+ if (pc === p) {
+ return true;
+ } else {
+ return !pc.isEquivalent(p);
+ }
+ }
+ });
+
+ if (justPoly) {
+ return nest;
+ }
+
+ // Extrude polygons to 3D
+ for (let poly of nest) {
+ let obj = poly.extrude(depth);
+ objs.push(obj);
+ }
+
+ return objs;
+}
+
+function extractEntities(lines) {
+ const entities = [];
+ let inEntities = false;
+ let i = 0;
+
+ while (i < lines.length - 1) {
+ const code = lines[i];
+ const value = lines[i + 1];
+
+ // Check if we're in the ENTITIES section
+ if (code === '0' && value === 'SECTION') {
+ if (i + 3 < lines.length && lines[i + 2] === '2' && lines[i + 3] === 'ENTITIES') {
+ inEntities = true;
+ i += 4;
+ continue;
+ }
+ }
+
+ if (code === '0' && value === 'ENDSEC' && inEntities) {
+ break;
+ }
+
+ if (inEntities && code === '0') {
+ if (value === 'POLYLINE') {
+ const entity = parsePolyline(lines, i);
+ if (entity) {
+ entities.push(entity);
+ i = entity.endIndex;
+ continue;
+ }
+ } else if (value === 'LWPOLYLINE') {
+ const entity = parseLWPolyline(lines, i);
+ if (entity) {
+ entities.push(entity);
+ i = entity.endIndex;
+ continue;
+ }
+ } else if (value === 'LINE') {
+ const entity = parseLine(lines, i);
+ if (entity) {
+ entities.push(entity);
+ i = entity.endIndex;
+ continue;
+ }
+ } else if (value === 'CIRCLE') {
+ const entity = parseCircle(lines, i);
+ if (entity) {
+ entities.push(entity);
+ i = entity.endIndex;
+ continue;
+ }
+ } else if (value === 'ARC') {
+ const entity = parseArc(lines, i);
+ if (entity) {
+ entities.push(entity);
+ i = entity.endIndex;
+ continue;
+ }
+ }
+ }
+
+ i += 2;
+ }
+
+ return entities;
+}
+
+function parsePolyline(lines, start) {
+ let i = start + 2;
+ let closed = false;
+ const points = [];
+
+ // Read polyline flags
+ while (i < lines.length - 1) {
+ const code = lines[i];
+ const value = lines[i + 1];
+
+ if (code === '70') {
+ // Polyline flag: 1 = closed
+ closed = (parseInt(value) & 1) === 1;
+ }
+
+ if (code === '0' && value === 'VERTEX') {
+ const vertex = parseVertex(lines, i);
+ if (vertex) {
+ points.push(vertex.point);
+ i = vertex.endIndex;
+ continue;
+ }
+ }
+
+ if (code === '0' && value === 'SEQEND') {
+ return { type: 'POLYLINE', points, closed, endIndex: i + 2 };
+ }
+
+ i += 2;
+ }
+
+ return null;
+}
+
+function parseVertex(lines, start) {
+ let i = start + 2;
+ const point = { x: 0, y: 0, z: 0 };
+
+ while (i < lines.length - 1) {
+ const code = lines[i];
+ const value = lines[i + 1];
+
+ if (code === '10') point.x = parseFloat(value);
+ if (code === '20') point.y = parseFloat(value);
+ if (code === '30') point.z = parseFloat(value);
+
+ if (code === '0') {
+ return { point, endIndex: i };
+ }
+
+ i += 2;
+ }
+
+ return { point, endIndex: i };
+}
+
+function parseLWPolyline(lines, start) {
+ let i = start + 2;
+ let closed = false;
+ const points = [];
+ let currentPoint = null;
+
+ while (i < lines.length - 1) {
+ const code = lines[i];
+ const value = lines[i + 1];
+
+ if (code === '70') {
+ closed = (parseInt(value) & 1) === 1;
+ }
+
+ if (code === '10') {
+ if (currentPoint) {
+ points.push(currentPoint);
+ }
+ currentPoint = { x: parseFloat(value), y: 0, z: 0 };
+ }
+
+ if (code === '20' && currentPoint) {
+ currentPoint.y = parseFloat(value);
+ }
+
+ if (code === '0') {
+ if (currentPoint) {
+ points.push(currentPoint);
+ }
+ return { type: 'LWPOLYLINE', points, closed, endIndex: i };
+ }
+
+ i += 2;
+ }
+
+ if (currentPoint) {
+ points.push(currentPoint);
+ }
+
+ return { type: 'LWPOLYLINE', points, closed, endIndex: i };
+}
+
+function parseLine(lines, start) {
+ let i = start + 2;
+ const start_point = { x: 0, y: 0, z: 0 };
+ const end_point = { x: 0, y: 0, z: 0 };
+
+ while (i < lines.length - 1) {
+ const code = lines[i];
+ const value = lines[i + 1];
+
+ if (code === '10') start_point.x = parseFloat(value);
+ if (code === '20') start_point.y = parseFloat(value);
+ if (code === '30') start_point.z = parseFloat(value);
+ if (code === '11') end_point.x = parseFloat(value);
+ if (code === '21') end_point.y = parseFloat(value);
+ if (code === '31') end_point.z = parseFloat(value);
+
+ if (code === '0') {
+ return { type: 'LINE', start: start_point, end: end_point, endIndex: i };
+ }
+
+ i += 2;
+ }
+
+ return { type: 'LINE', start: start_point, end: end_point, endIndex: i };
+}
+
+function parseCircle(lines, start) {
+ let i = start + 2;
+ const center = { x: 0, y: 0, z: 0 };
+ let radius = 0;
+
+ while (i < lines.length - 1) {
+ const code = lines[i];
+ const value = lines[i + 1];
+
+ if (code === '10') center.x = parseFloat(value);
+ if (code === '20') center.y = parseFloat(value);
+ if (code === '30') center.z = parseFloat(value);
+ if (code === '40') radius = parseFloat(value);
+
+ if (code === '0') {
+ return { type: 'CIRCLE', center, radius, endIndex: i };
+ }
+
+ i += 2;
+ }
+
+ return { type: 'CIRCLE', center, radius, endIndex: i };
+}
+
+function parseArc(lines, start) {
+ let i = start + 2;
+ const center = { x: 0, y: 0, z: 0 };
+ let radius = 0;
+ let startAngle = 0;
+ let endAngle = 0;
+
+ while (i < lines.length - 1) {
+ const code = lines[i];
+ const value = lines[i + 1];
+
+ if (code === '10') center.x = parseFloat(value);
+ if (code === '20') center.y = parseFloat(value);
+ if (code === '30') center.z = parseFloat(value);
+ if (code === '40') radius = parseFloat(value);
+ if (code === '50') startAngle = parseFloat(value) * Math.PI / 180; // Convert to radians
+ if (code === '51') endAngle = parseFloat(value) * Math.PI / 180; // Convert to radians
+
+ if (code === '0') {
+ return { type: 'ARC', center, radius, startAngle, endAngle, endIndex: i };
+ }
+
+ i += 2;
+ }
+
+ return { type: 'ARC', center, radius, startAngle, endAngle, endIndex: i };
+}
diff --git a/src/load/file.js b/src/load/file.js
index 88962c27..82978395 100644
--- a/src/load/file.js
+++ b/src/load/file.js
@@ -6,6 +6,7 @@ import { STL } from './stl.js';
import * as OBJ from './obj.js';
import * as TMF from './3mf.js';
import * as SVG from './svg.js';
+import * as DXF from './dxf.js';
import * as GBR from './gbr.js';
import { load as pngLoad } from './png.js';
@@ -36,6 +37,11 @@ const types = {
resolve(opt.flat ? out : out.map(m => { return { mesh: m.toFloat32(), file } }));
},
+ dxf(data, file, resolve, reject, opt = {}) {
+ let out = DXF.parse(data, opt);
+ resolve(opt.flat ? out : out.map(m => { return { mesh: m.toFloat32(), file } }));
+ },
+
png(data, file, resolve, reject, opt = {}) {
pngLoad.PNG.parse(data, {
...opt,
@@ -100,6 +106,6 @@ function load_file(file, opt) {
});
}
-Object.assign(load_file, { SVG, OBJ, STL, TMF, GBR, PNG: pngLoad.PNG });
+Object.assign(load_file, { SVG, DXF, OBJ, STL, TMF, GBR, PNG: pngLoad.PNG });
export { types, as_buffer, load_data, load_file, load_file as load };
diff --git a/src/load/url.js b/src/load/url.js
index 6b481a1a..d4f5ed82 100644
--- a/src/load/url.js
+++ b/src/load/url.js
@@ -4,7 +4,7 @@
import { load_file } from './file.js';
-const { STL, OBJ, TMF, SVG } = load_file;
+const { STL, OBJ, TMF, SVG, DXF } = load_file;
const CDH = 'Content-Disposition';
export function load_url(url, options = {}) {
@@ -12,7 +12,7 @@ export function load_url(url, options = {}) {
let xhr = new XMLHttpRequest();
let file = options.file || options.filename || (((url.split('?')[0]).split('#')[0]).split('/')).pop();
let ext = file.split('.').pop().toLowerCase();
- let deftype = ext === "obj" || ext === 'svg' ? "text" : "arraybuffer";
+ let deftype = ext === "obj" || ext === 'svg' || ext === 'dxf' ? "text" : "arraybuffer";
let datatype = options.datatype || deftype;
let formdata = options.formdata;
@@ -55,6 +55,9 @@ export function load_url(url, options = {}) {
case "svg":
resolve(SVG.parse(data).map(m => { return {mesh: m.toFloat32(), file} }));
break;
+ case "dxf":
+ resolve(DXF.parse(data).map(m => { return {mesh: m.toFloat32(), file} }));
+ break;
default:
reject(`unknown file type: "${ext}" from ${url}`);
break;