From 385b93ecf1f4afa856ecc17af632756cc329a6a1 Mon Sep 17 00:00:00 2001 From: RKUZNETSOV Date: Wed, 16 Sep 2026 08:43:33 +0300 Subject: [PATCH] Add DXF preview and layer selection before import --- src/kiri/app/platform.js | 87 +++++++++++++++++++++++++++++++++++----- src/load/dxf.js | 31 +++++++++++++- web/kiri/index.css | 67 +++++++++++++++++++++++++++++++ 3 files changed, 173 insertions(+), 12 deletions(-) diff --git a/src/kiri/app/platform.js b/src/kiri/app/platform.js index 5bd080a44..02b757e29 100644 --- a/src/kiri/app/platform.js +++ b/src/kiri/app/platform.js @@ -1082,7 +1082,7 @@ function load_files(files, group) { load_dec(); }); } else if (isdxf) { - loadDXFDialog(opt => { + loadDXFDialog(data.textDecode('utf-8'), opt => { group = group || []; let dxf = file_load.DXF.parse(data.textDecode('utf-8'), opt); let ind = 0; @@ -1145,12 +1145,15 @@ function loadSVGDialog(doit) { * @param {Function} doit - Callback with options: {soup, depth, segmentSize, minSegments} * @private */ -function loadDXFDialog(doit) { +function loadDXFDialog(text, doit) { const rnd = Date.now().toString(36); const host = $('mod-any'); host.innerHTML = [ - `
`, + `
`, `

Import DXF

`, + `
`, + `

`, + `

Layers

`, `

`, ` Extrude a 3D model from a 2D DXF.`, ` Supports POLYLINE, LWPOLYLINE, LINE, CIRCLE, ARC, and SPLINE entities.`, @@ -1165,7 +1168,7 @@ function loadDXFDialog(doit) { `

`, ` `, ` `, - `
`, + `
`, `
` ].join(''); @@ -1177,16 +1180,78 @@ function loadDXFDialog(doit) { const okBtn = $(`dxf-convert-ok-${rnd}`); const cancelBtn = $(`dxf-convert-cancel-${rnd}`); + const canvas = $(`dxf-preview-${rnd}`); + const status = $(`dxf-status-${rnd}`); + const layerList = $(`dxf-layers-${rnd}`); + const selected = new Set(file_load.DXF.getLayers(text)); + const options = () => ({ + soup: nest.checked, + depth: Math.max(0.1, parseFloat(depth.value) || 5), + segmentSize: Math.max(0.01, parseFloat(segmentSize.value) || 1), + minSegments: Math.max(3, parseInt(minSegments.value) || 4), + units: units.value, + layers: [...selected] + }); + function renderPreview() { + const ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, canvas.width, canvas.height); + okBtn.disabled = selected.size === 0; + status.textContent = selected.size ? '' : 'Select at least one layer'; + if (!selected.size) return; + try { + const paths = []; + function collect(poly) { + if (poly.points.length) paths.push(poly); + for (const inner of poly.inner || []) collect(inner); + } + file_load.DXF.parse(text, { ...options(), flat: true }).forEach(collect); + if (!paths.length) { + status.textContent = 'No supported geometry in selected layers'; + okBtn.disabled = true; + return; + } + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + for (const path of paths) for (const p of path.points) { + minX = Math.min(minX, p.x); maxX = Math.max(maxX, p.x); + minY = Math.min(minY, p.y); maxY = Math.max(maxY, p.y); + } + const scale = Math.min(660 / Math.max(maxX - minX, 1), 460 / Math.max(maxY - minY, 1)); + ctx.strokeStyle = '#3989ce'; + ctx.lineWidth = 1.5; + for (const path of paths) { + ctx.beginPath(); + path.points.forEach((p, i) => { + const x = 350 + (p.x - (minX + maxX) / 2) * scale; + const y = 250 - (p.y - (minY + maxY) / 2) * scale; + if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); + }); + if (!path.open) ctx.closePath(); + ctx.stroke(); + } + } catch (error) { + status.textContent = 'Unable to preview DXF: ' + error.message; + okBtn.disabled = true; + } + } + for (const layer of selected) { + const label = document.createElement('label'); + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.checked = true; + checkbox.onchange = () => { + if (checkbox.checked) selected.add(layer); else selected.delete(layer); + renderPreview(); + }; + label.append(checkbox, document.createTextNode(layer)); + layerList.append(label); + } + for (const input of [units, segmentSize, minSegments, nest]) input.onchange = renderPreview; + renderPreview(); + okBtn.onclick = () => { api.modal.hide(); setTimeout(() => { - doit({ - soup: nest.checked, - depth: Math.max(0.1, parseFloat(depth.value)), - segmentSize: Math.max(0.01, parseFloat(segmentSize.value)), - minSegments: Math.max(3, parseInt(minSegments.value)), - units: units.value - }); + doit(options()); }, 50); }; cancelBtn.onclick = () => api.modal.hide(); diff --git a/src/load/dxf.js b/src/load/dxf.js index 419443624..5624ead78 100644 --- a/src/load/dxf.js +++ b/src/load/dxf.js @@ -4,6 +4,24 @@ import { newPolygon } from '../geo/polygon.js'; import { newPoint } from '../geo/point.js'; import { polygons } from '../geo/polygons.js'; +/** List declared layers and layers used by entities, including unsupported entities. */ +export function getLayers(text) { + const lines = text.replace(/\r\n?/g, '\n').split('\n').map(line => line.trim()); + const layers = new Set(); + let section = ''; + let record = ''; + for (let i = 0; i < lines.length - 1; i += 2) { + const code = lines[i], value = lines[i + 1]; + if (code === '0') record = value; + if (record === 'SECTION' && code === '2') section = value; + if (record === 'ENDSEC') section = ''; + if (section === 'TABLES' && record === 'LAYER' && code === '2') layers.add(value); + if (section === 'ENTITIES' && code === '8') layers.add(value); + } + for (const entity of extractEntities(lines)) layers.add(entity.layer); + return [...layers].sort((a, b) => a.localeCompare(b)); +} + export function parseAsync(text, opt) { return new Promise((resolve, reject) => { try { @@ -32,7 +50,8 @@ export function parse(text, opt = { }) { const inputUnits = (!opt.units || opt.units === 'auto') ? fileUnits : opt.units; const scale = getScaleToMM(inputUnits); // convert to mm (Kiri:Moto's internal unit) - const entities = extractEntities(lines); + const selected = opt.layers === undefined ? null : new Set(opt.layers); + const entities = extractEntities(lines).filter(entity => !selected || selected.has(entity.layer)); // Scale all entities to mm BEFORE stitching scaleEntities(entities, scale); @@ -225,9 +244,14 @@ function extractEntities(lines) { } if (inEntities && code === '0') { + let layer = '0'; + for (let j = i + 2; j < lines.length - 1 && lines[j] !== '0'; j += 2) { + if (lines[j] === '8') layer = lines[j + 1]; + } if (value === 'POLYLINE') { const entity = parsePolyline(lines, i); if (entity) { + entity.layer = layer; entities.push(entity); i = entity.endIndex; continue; @@ -235,6 +259,7 @@ function extractEntities(lines) { } else if (value === 'LWPOLYLINE') { const entity = parseLWPolyline(lines, i); if (entity) { + entity.layer = layer; entities.push(entity); i = entity.endIndex; continue; @@ -242,6 +267,7 @@ function extractEntities(lines) { } else if (value === 'LINE') { const entity = parseLine(lines, i); if (entity) { + entity.layer = layer; entities.push(entity); i = entity.endIndex; continue; @@ -249,6 +275,7 @@ function extractEntities(lines) { } else if (value === 'CIRCLE') { const entity = parseCircle(lines, i); if (entity) { + entity.layer = layer; entities.push(entity); i = entity.endIndex; continue; @@ -256,6 +283,7 @@ function extractEntities(lines) { } else if (value === 'ARC') { const entity = parseArc(lines, i); if (entity) { + entity.layer = layer; entities.push(entity); i = entity.endIndex; continue; @@ -263,6 +291,7 @@ function extractEntities(lines) { } else if (value === 'SPLINE') { const entity = parseSpline(lines, i); if (entity) { + entity.layer = layer; entities.push(entity); i = entity.endIndex; continue; diff --git a/web/kiri/index.css b/web/kiri/index.css index 4a6544899..c4cb09f16 100644 --- a/web/kiri/index.css +++ b/web/kiri/index.css @@ -2545,3 +2545,70 @@ details[open] summary::after { .em20 { max-width: 20em; } + +/* DXF layer selection and live geometry preview. + * The application makes every div a flex row; explicitly define each panel. + */ +.dxf-import-dialog { + box-sizing: border-box; + width: min(1000px, calc(100vw - 80px)); + min-width: 0; + max-height: 72vh; +} +.dxf-import-dialog * { box-sizing: border-box; } +.dxf-import-body { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(260px, 340px); + align-items: start; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; +} +.dxf-preview { + display: block; + min-width: 0; + padding: 12px; + border-right: 1px solid #8886; +} +.dxf-preview canvas { + display: block; + width: 100%; + height: auto; + background: #f7f9fc; + border-radius: 4px; +} +.dxf-preview p { min-height: 1.5em; white-space: normal; } +.dxf-options { + display: flex; + flex-direction: column; + align-items: stretch; + gap: 8px; + padding: 0 12px; + min-width: 0; +} +.dxf-options h4 { margin: 0; } +.dxf-layers { + display: block; + max-height: 220px; + overflow-y: auto; + border: 1px solid #8886; + padding: 6px; +} +.dxf-layers label { + display: flex; + align-items: center; + gap: 8px; + white-space: normal; + overflow-wrap: anywhere; + padding: 4px; + cursor: pointer; +} +.dxf-layers input { flex-shrink: 0; } +.dxf-options .image-convert-copy { width: auto; text-align: left; } +.dxf-options .image-convert-fields { display: block; } +.dxf-options table { width: 100%; } +.dxf-options th { white-space: normal; } +.dxf-options td { width: 110px; } +.dxf-options td input:not([type="checkbox"]), +.dxf-options td select { width: 100%; min-width: 0; } +.dxf-options .image-convert-actions { flex: none; align-items: center; }