add dxf import
This commit is contained in:
parent
8c8afaba69
commit
3aa8b77e77
5 changed files with 418 additions and 5 deletions
|
|
@ -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' }),
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
"<div class='f-col a-center'>",
|
||||
" <h3>Import DXF</h3>",
|
||||
" <p class='t-just' style='width:300px;line-height:1.5em'>",
|
||||
" Extrude a 3D model from a 2D DXF.",
|
||||
" Supports POLYLINE, LWPOLYLINE, LINE, CIRCLE, and ARC entities.",
|
||||
" </p>",
|
||||
" <div class='f-row t-right'><table>",
|
||||
" <tr><th>z height in mm</th><td><input id='dxf-depth' value='5' size='3'></td></tr>",
|
||||
" <tr><th title='target length of each line segment when converting arcs and circles'>arc segment size in mm</th><td><input id='dxf-seg' value='1' size='3'></td></tr>",
|
||||
" <tr><th title='minimum number of segments for very small arcs to avoid degenerate geometry'>minimum arc segments</th><td><input id='dxf-min' value='4' size='3'></td></tr>",
|
||||
" <tr><th>nest shapes</th><td><input id='dxf-nest' value='1' type='checkbox' checked></td></tr>",
|
||||
" </table></div>",
|
||||
"</div>"
|
||||
]};
|
||||
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.
|
||||
|
|
|
|||
359
src/load/dxf.js
Normal file
359
src/load/dxf.js
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- 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 };
|
||||
}
|
||||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue