fix dxf open line reconnects with shared endpoints
This commit is contained in:
parent
3593be43b5
commit
c14ea71c8c
3 changed files with 413 additions and 18 deletions
|
|
@ -1156,8 +1156,9 @@ function loadDXFDialog(doit) {
|
||||||
` Supports POLYLINE, LWPOLYLINE, LINE, CIRCLE, ARC, and SPLINE entities.`,
|
` Supports POLYLINE, LWPOLYLINE, LINE, CIRCLE, ARC, and SPLINE entities.`,
|
||||||
` </p>`,
|
` </p>`,
|
||||||
` <div class="f-row t-right image-convert-fields"><table>`,
|
` <div class="f-row t-right image-convert-fields"><table>`,
|
||||||
` <tr><th>z height in mm</th><td><input id="dxf-depth-${rnd}" value="5" size="3"></td></tr>`,
|
` <tr><th>units</th><td><select id="dxf-units-${rnd}"><option value="mm">millimeters</option><option value="inch">inches</option></select></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-${rnd}" value="1" size="3"></td></tr>`,
|
` <tr><th>z height</th><td><input id="dxf-depth-${rnd}" value="5" size="3"></td></tr>`,
|
||||||
|
` <tr><th title="target length of each line segment when converting arcs and circles">arc segment size</th><td><input id="dxf-seg-${rnd}" 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-${rnd}" value="4" 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-${rnd}" value="4" size="3"></td></tr>`,
|
||||||
` <tr><th>nest shapes</th><td><input id="dxf-nest-${rnd}" type="checkbox" checked></td></tr>`,
|
` <tr><th>nest shapes</th><td><input id="dxf-nest-${rnd}" type="checkbox" checked></td></tr>`,
|
||||||
` </table></div>`,
|
` </table></div>`,
|
||||||
|
|
@ -1168,6 +1169,7 @@ function loadDXFDialog(doit) {
|
||||||
`</div>`
|
`</div>`
|
||||||
].join('');
|
].join('');
|
||||||
|
|
||||||
|
const units = $(`dxf-units-${rnd}`);
|
||||||
const depth = $(`dxf-depth-${rnd}`);
|
const depth = $(`dxf-depth-${rnd}`);
|
||||||
const segmentSize = $(`dxf-seg-${rnd}`);
|
const segmentSize = $(`dxf-seg-${rnd}`);
|
||||||
const minSegments = $(`dxf-min-${rnd}`);
|
const minSegments = $(`dxf-min-${rnd}`);
|
||||||
|
|
@ -1182,7 +1184,8 @@ function loadDXFDialog(doit) {
|
||||||
soup: nest.checked,
|
soup: nest.checked,
|
||||||
depth: Math.max(0.1, parseFloat(depth.value)),
|
depth: Math.max(0.1, parseFloat(depth.value)),
|
||||||
segmentSize: Math.max(0.01, parseFloat(segmentSize.value)),
|
segmentSize: Math.max(0.01, parseFloat(segmentSize.value)),
|
||||||
minSegments: Math.max(3, parseInt(minSegments.value))
|
minSegments: Math.max(3, parseInt(minSegments.value)),
|
||||||
|
units: units.value
|
||||||
});
|
});
|
||||||
}, 50);
|
}, 50);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,7 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSpindle(speed) {
|
function setSpindle(speed) {
|
||||||
|
// console.trace({ setSpindle: speed });
|
||||||
spindle = Math.min(speed, spindleMax);
|
spindle = Math.min(speed, spindleMax);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
421
src/load/dxf.js
421
src/load/dxf.js
|
|
@ -25,11 +25,52 @@ export function parse(text, opt = { }) {
|
||||||
|
|
||||||
// Parse DXF file - normalize line endings and split
|
// 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 lines = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n').map(l => l.trim());
|
||||||
|
|
||||||
|
// Parse header for units (informational, can be overridden by user)
|
||||||
|
const fileUnits = extractUnits(lines);
|
||||||
|
const inputUnits = opt.units || fileUnits; // user can override file's declared units
|
||||||
|
const scale = getScaleToMM(inputUnits); // convert to mm (Kiri:Moto's internal unit)
|
||||||
|
|
||||||
const entities = extractEntities(lines);
|
const entities = extractEntities(lines);
|
||||||
|
|
||||||
// Convert entities to polygons
|
// Scale all entities to mm BEFORE stitching
|
||||||
for (let entity of entities) {
|
scaleEntities(entities, scale);
|
||||||
if (entity.type === 'POLYLINE' || entity.type === 'LWPOLYLINE') {
|
|
||||||
|
// Stitch together open paths that share endpoints (tolerance in mm now)
|
||||||
|
const tolerance = Math.max(0.001, segmentSize * 0.001); // 0.1% of segment size, min 0.001mm
|
||||||
|
const stitchedEntities = stitchPaths(entities, tolerance);
|
||||||
|
|
||||||
|
// Convert entities to polygons (no scaling needed, already in mm)
|
||||||
|
for (let entity of stitchedEntities) {
|
||||||
|
if (entity.type === 'STITCHED') {
|
||||||
|
// Convert stitched path parts into a single polyline
|
||||||
|
const points = [];
|
||||||
|
for (const part of entity.parts) {
|
||||||
|
const partPoints = convertEntityToPoints(part, segmentSize, minSegments);
|
||||||
|
if (partPoints.length > 0) {
|
||||||
|
if (points.length === 0) {
|
||||||
|
points.push(...partPoints);
|
||||||
|
} else {
|
||||||
|
// Skip first point if it's the same as our last point (avoid duplicates)
|
||||||
|
points.push(...partPoints.slice(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (points.length < 2) continue;
|
||||||
|
|
||||||
|
let poly = newPolygon().addPoints(
|
||||||
|
points.map(p => newPoint(p.x, p.y, p.z || 0))
|
||||||
|
).clean();
|
||||||
|
|
||||||
|
if (entity.closed && poly.appearsClosed()) {
|
||||||
|
poly.points.pop();
|
||||||
|
} else if (!entity.closed) {
|
||||||
|
poly.setOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
polys.push(poly);
|
||||||
|
} else if (entity.type === 'POLYLINE' || entity.type === 'LWPOLYLINE') {
|
||||||
if (entity.points.length < 2) {
|
if (entity.points.length < 2) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -56,7 +97,7 @@ export function parse(text, opt = { }) {
|
||||||
polys.push(poly);
|
polys.push(poly);
|
||||||
} else if (entity.type === 'CIRCLE') {
|
} else if (entity.type === 'CIRCLE') {
|
||||||
// Convert circle to polygon with points
|
// Convert circle to polygon with points
|
||||||
// Calculate segments based on circumference and desired segment size
|
// Calculate segments based on circumference and desired segment size (already in mm)
|
||||||
const circumference = 2 * Math.PI * entity.radius;
|
const circumference = 2 * Math.PI * entity.radius;
|
||||||
const segments = Math.max(minSegments, Math.ceil(circumference / segmentSize));
|
const segments = Math.max(minSegments, Math.ceil(circumference / segmentSize));
|
||||||
let points = [];
|
let points = [];
|
||||||
|
|
@ -72,23 +113,47 @@ export function parse(text, opt = { }) {
|
||||||
polys.push(poly);
|
polys.push(poly);
|
||||||
} else if (entity.type === 'ARC') {
|
} else if (entity.type === 'ARC') {
|
||||||
// Convert arc to polyline
|
// Convert arc to polyline
|
||||||
// Calculate segments based on arc length and desired segment size
|
// DXF arcs always go counterclockwise. Handle angle wrapping.
|
||||||
const arcLength = Math.abs(entity.endAngle - entity.startAngle) * entity.radius;
|
let startAngle = entity.startAngle;
|
||||||
|
let endAngle = entity.endAngle;
|
||||||
|
let angleDiff = endAngle - startAngle;
|
||||||
|
|
||||||
|
// If endAngle < startAngle, arc wraps through 0/360
|
||||||
|
if (angleDiff < 0) {
|
||||||
|
angleDiff += Math.PI * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const arcLength = angleDiff * entity.radius;
|
||||||
const segments = Math.max(minSegments, Math.ceil(arcLength / segmentSize));
|
const segments = Math.max(minSegments, Math.ceil(arcLength / segmentSize));
|
||||||
let points = [];
|
let points = [];
|
||||||
for (let i = 0; i <= segments; i++) {
|
|
||||||
const angle = entity.startAngle + (i / segments) * (entity.endAngle - entity.startAngle);
|
if (entity.reversed) {
|
||||||
points.push(newPoint(
|
// Sample backwards from end to start
|
||||||
entity.center.x + Math.cos(angle) * entity.radius,
|
for (let i = 0; i <= segments; i++) {
|
||||||
entity.center.y + Math.sin(angle) * entity.radius,
|
const angle = endAngle - (i / segments) * angleDiff;
|
||||||
entity.center.z || 0
|
points.push(newPoint(
|
||||||
));
|
entity.center.x + Math.cos(angle) * entity.radius,
|
||||||
|
entity.center.y + Math.sin(angle) * entity.radius,
|
||||||
|
entity.center.z || 0
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Sample forward from start to end
|
||||||
|
for (let i = 0; i <= segments; i++) {
|
||||||
|
const angle = startAngle + (i / segments) * angleDiff;
|
||||||
|
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);
|
let poly = newPolygon().addPoints(points);
|
||||||
poly.setOpen(true);
|
poly.setOpen(true);
|
||||||
polys.push(poly);
|
polys.push(poly);
|
||||||
} else if (entity.type === 'SPLINE') {
|
} else if (entity.type === 'SPLINE') {
|
||||||
// Convert NURBS spline to polyline by sampling
|
// Convert NURBS spline to polyline by sampling (already in mm)
|
||||||
if (entity.controlPoints.length < 2) {
|
if (entity.controlPoints.length < 2) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -433,7 +498,7 @@ function parseSpline(lines, start) {
|
||||||
return { type: 'SPLINE', degree, closed, knots, controlPoints, endIndex: i };
|
return { type: 'SPLINE', degree, closed, knots, controlPoints, endIndex: i };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluate NURBS B-spline curve to generate sample points
|
// Evaluate NURBS B-spline curve to generate sample points (entities already scaled to mm)
|
||||||
function evaluateSpline(entity, segmentSize, minSegments) {
|
function evaluateSpline(entity, segmentSize, minSegments) {
|
||||||
const { degree, controlPoints, knots, closed } = entity;
|
const { degree, controlPoints, knots, closed } = entity;
|
||||||
|
|
||||||
|
|
@ -538,3 +603,329 @@ function evaluateNURBS(t, degree, controlPoints, knots) {
|
||||||
|
|
||||||
return { x, y, z };
|
return { x, y, z };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scale all entity coordinates to millimeters
|
||||||
|
function scaleEntities(entities, scale) {
|
||||||
|
if (scale === 1) return; // no scaling needed
|
||||||
|
|
||||||
|
for (let entity of entities) {
|
||||||
|
if (entity.type === 'LINE') {
|
||||||
|
entity.start.x *= scale;
|
||||||
|
entity.start.y *= scale;
|
||||||
|
entity.start.z = (entity.start.z || 0) * scale;
|
||||||
|
entity.end.x *= scale;
|
||||||
|
entity.end.y *= scale;
|
||||||
|
entity.end.z = (entity.end.z || 0) * scale;
|
||||||
|
} else if (entity.type === 'CIRCLE') {
|
||||||
|
entity.center.x *= scale;
|
||||||
|
entity.center.y *= scale;
|
||||||
|
entity.center.z = (entity.center.z || 0) * scale;
|
||||||
|
entity.radius *= scale;
|
||||||
|
} else if (entity.type === 'ARC') {
|
||||||
|
entity.center.x *= scale;
|
||||||
|
entity.center.y *= scale;
|
||||||
|
entity.center.z = (entity.center.z || 0) * scale;
|
||||||
|
entity.radius *= scale;
|
||||||
|
} else if (entity.type === 'POLYLINE' || entity.type === 'LWPOLYLINE') {
|
||||||
|
for (let point of entity.points) {
|
||||||
|
point.x *= scale;
|
||||||
|
point.y *= scale;
|
||||||
|
point.z = (point.z || 0) * scale;
|
||||||
|
}
|
||||||
|
} else if (entity.type === 'SPLINE') {
|
||||||
|
for (let point of entity.controlPoints) {
|
||||||
|
point.x *= scale;
|
||||||
|
point.y *= scale;
|
||||||
|
point.z = (point.z || 0) * scale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract units from DXF header
|
||||||
|
function extractUnits(lines) {
|
||||||
|
let i = 0;
|
||||||
|
let inHeader = false;
|
||||||
|
|
||||||
|
while (i < lines.length - 1) {
|
||||||
|
const code = lines[i];
|
||||||
|
const value = lines[i + 1];
|
||||||
|
|
||||||
|
if (code === '0' && value === 'SECTION') {
|
||||||
|
if (i + 3 < lines.length && lines[i + 2] === '2' && lines[i + 3] === 'HEADER') {
|
||||||
|
inHeader = true;
|
||||||
|
i += 4;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code === '0' && value === 'ENDSEC' && inHeader) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inHeader && code === '9' && value === '$INSUNITS') {
|
||||||
|
// Next line should be 70, followed by the unit code
|
||||||
|
if (i + 3 < lines.length && lines[i + 2] === '70') {
|
||||||
|
const unitCode = parseInt(lines[i + 3]);
|
||||||
|
// DXF INSUNITS codes: 0=unitless, 1=inches, 2=feet, 4=mm, 5=cm, 6=meters
|
||||||
|
switch (unitCode) {
|
||||||
|
case 1: return 'inch';
|
||||||
|
case 2: return 'foot';
|
||||||
|
case 4: return 'mm';
|
||||||
|
case 5: return 'cm';
|
||||||
|
case 6: return 'meter';
|
||||||
|
default: return 'mm'; // default to mm for unitless
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'mm'; // default to millimeters
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get scale factor to convert from input units to millimeters
|
||||||
|
function getScaleToMM(inputUnits) {
|
||||||
|
// Scale factors to convert to mm (Kiri:Moto's internal unit)
|
||||||
|
const toMM = {
|
||||||
|
'mm': 1,
|
||||||
|
'cm': 10,
|
||||||
|
'meter': 1000,
|
||||||
|
'inch': 25.4,
|
||||||
|
'foot': 304.8
|
||||||
|
};
|
||||||
|
|
||||||
|
return toMM[inputUnits] || 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stitch together open paths that share endpoints
|
||||||
|
function stitchPaths(entities, tolerance = 0.01) {
|
||||||
|
const stitched = [];
|
||||||
|
const used = new Set();
|
||||||
|
|
||||||
|
// Helper to check if two points are within tolerance
|
||||||
|
const pointsMatch = (p1, p2) => {
|
||||||
|
const dx = p1.x - p2.x;
|
||||||
|
const dy = p1.y - p2.y;
|
||||||
|
const dz = (p1.z || 0) - (p2.z || 0);
|
||||||
|
return Math.sqrt(dx * dx + dy * dy + dz * dz) < tolerance;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to get endpoints of an entity
|
||||||
|
const getEndpoints = (entity) => {
|
||||||
|
if (entity.type === 'LINE') {
|
||||||
|
return { start: entity.start, end: entity.end };
|
||||||
|
} else if (entity.type === 'ARC') {
|
||||||
|
// Calculate actual arc endpoints
|
||||||
|
const startX = entity.center.x + Math.cos(entity.startAngle) * entity.radius;
|
||||||
|
const startY = entity.center.y + Math.sin(entity.startAngle) * entity.radius;
|
||||||
|
const endX = entity.center.x + Math.cos(entity.endAngle) * entity.radius;
|
||||||
|
const endY = entity.center.y + Math.sin(entity.endAngle) * entity.radius;
|
||||||
|
const start = { x: startX, y: startY, z: entity.center.z || 0 };
|
||||||
|
const end = { x: endX, y: endY, z: entity.center.z || 0 };
|
||||||
|
// If arc is reversed, swap the endpoints
|
||||||
|
if (entity.reversed) {
|
||||||
|
return { start: end, end: start };
|
||||||
|
}
|
||||||
|
return { start, end };
|
||||||
|
} else if (entity.type === 'POLYLINE' || entity.type === 'LWPOLYLINE') {
|
||||||
|
if (entity.closed || entity.points.length < 2) return null;
|
||||||
|
return {
|
||||||
|
start: entity.points[0],
|
||||||
|
end: entity.points[entity.points.length - 1]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to convert entity to points
|
||||||
|
const entityToPoints = (entity) => {
|
||||||
|
if (entity.type === 'LINE') {
|
||||||
|
return [entity.start, entity.end];
|
||||||
|
} else if (entity.type === 'POLYLINE' || entity.type === 'LWPOLYLINE') {
|
||||||
|
return [...entity.points];
|
||||||
|
}
|
||||||
|
// For ARC and other types, return null (will be converted later in main loop)
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// First, identify stitchable entities (LINE, ARC, open POLYLINE)
|
||||||
|
const stitchable = [];
|
||||||
|
for (let i = 0; i < entities.length; i++) {
|
||||||
|
const entity = entities[i];
|
||||||
|
if (entity.type === 'LINE' || entity.type === 'ARC' ||
|
||||||
|
((entity.type === 'POLYLINE' || entity.type === 'LWPOLYLINE') && !entity.closed)) {
|
||||||
|
stitchable.push({ entity, index: i });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to stitch paths together
|
||||||
|
for (let i = 0; i < stitchable.length; i++) {
|
||||||
|
if (used.has(i)) continue;
|
||||||
|
|
||||||
|
const { entity, index } = stitchable[i];
|
||||||
|
const endpoints = getEndpoints(entity);
|
||||||
|
if (!endpoints) {
|
||||||
|
stitched.push(entity);
|
||||||
|
used.add(i);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a new path
|
||||||
|
const path = [entity];
|
||||||
|
used.add(i);
|
||||||
|
let currentEnd = endpoints.end;
|
||||||
|
let currentStart = endpoints.start;
|
||||||
|
let foundMatch = true;
|
||||||
|
|
||||||
|
// Keep extending the path
|
||||||
|
while (foundMatch) {
|
||||||
|
foundMatch = false;
|
||||||
|
|
||||||
|
for (let j = 0; j < stitchable.length; j++) {
|
||||||
|
if (used.has(j)) continue;
|
||||||
|
|
||||||
|
const nextEndpoints = getEndpoints(stitchable[j].entity);
|
||||||
|
if (!nextEndpoints) continue;
|
||||||
|
|
||||||
|
// Check if this entity connects to current end
|
||||||
|
if (pointsMatch(currentEnd, nextEndpoints.start)) {
|
||||||
|
path.push(stitchable[j].entity);
|
||||||
|
currentEnd = nextEndpoints.end;
|
||||||
|
used.add(j);
|
||||||
|
foundMatch = true;
|
||||||
|
break;
|
||||||
|
} else if (pointsMatch(currentEnd, nextEndpoints.end)) {
|
||||||
|
// Need to reverse this entity
|
||||||
|
const reversed = reverseEntity(stitchable[j].entity);
|
||||||
|
path.push(reversed);
|
||||||
|
// After reversing, the start becomes the new end
|
||||||
|
const reversedEndpoints = getEndpoints(reversed);
|
||||||
|
currentEnd = reversedEndpoints.end;
|
||||||
|
used.add(j);
|
||||||
|
foundMatch = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Check if this entity connects to current start (prepend)
|
||||||
|
else if (pointsMatch(currentStart, nextEndpoints.end)) {
|
||||||
|
path.unshift(stitchable[j].entity);
|
||||||
|
currentStart = nextEndpoints.start;
|
||||||
|
used.add(j);
|
||||||
|
foundMatch = true;
|
||||||
|
break;
|
||||||
|
} else if (pointsMatch(currentStart, nextEndpoints.start)) {
|
||||||
|
// Need to reverse and prepend
|
||||||
|
const reversed = reverseEntity(stitchable[j].entity);
|
||||||
|
path.unshift(reversed);
|
||||||
|
// After reversing, the end becomes the new start
|
||||||
|
const reversedEndpoints = getEndpoints(reversed);
|
||||||
|
currentStart = reversedEndpoints.start;
|
||||||
|
used.add(j);
|
||||||
|
foundMatch = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert path to a single stitched entity
|
||||||
|
if (path.length === 1) {
|
||||||
|
stitched.push(path[0]);
|
||||||
|
} else {
|
||||||
|
// Combine into stitched polyline - mark for later conversion
|
||||||
|
const stitchedEntity = {
|
||||||
|
type: 'STITCHED',
|
||||||
|
parts: path,
|
||||||
|
closed: pointsMatch(currentStart, currentEnd)
|
||||||
|
};
|
||||||
|
stitched.push(stitchedEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add non-stitchable entities (CIRCLE, SPLINE, closed POLYLINE)
|
||||||
|
for (let i = 0; i < entities.length; i++) {
|
||||||
|
const entity = entities[i];
|
||||||
|
if (entity.type === 'CIRCLE' || entity.type === 'SPLINE' ||
|
||||||
|
((entity.type === 'POLYLINE' || entity.type === 'LWPOLYLINE') && entity.closed)) {
|
||||||
|
stitched.push(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stitched;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse an entity's direction
|
||||||
|
function reverseEntity(entity) {
|
||||||
|
if (entity.type === 'LINE') {
|
||||||
|
return {
|
||||||
|
type: 'LINE',
|
||||||
|
start: entity.end,
|
||||||
|
end: entity.start
|
||||||
|
};
|
||||||
|
} else if (entity.type === 'ARC') {
|
||||||
|
// Mark the arc as reversed so it gets sampled in reverse
|
||||||
|
return {
|
||||||
|
type: 'ARC',
|
||||||
|
center: entity.center,
|
||||||
|
radius: entity.radius,
|
||||||
|
startAngle: entity.startAngle,
|
||||||
|
endAngle: entity.endAngle,
|
||||||
|
reversed: true
|
||||||
|
};
|
||||||
|
} else if (entity.type === 'POLYLINE' || entity.type === 'LWPOLYLINE') {
|
||||||
|
return {
|
||||||
|
type: entity.type,
|
||||||
|
points: [...entity.points].reverse(),
|
||||||
|
closed: entity.closed
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert entity to array of points (entities already scaled to mm)
|
||||||
|
function convertEntityToPoints(entity, segmentSize, minSegments) {
|
||||||
|
if (entity.type === 'LINE') {
|
||||||
|
return [entity.start, entity.end];
|
||||||
|
} else if (entity.type === 'POLYLINE' || entity.type === 'LWPOLYLINE') {
|
||||||
|
return [...entity.points];
|
||||||
|
} else if (entity.type === 'ARC') {
|
||||||
|
// DXF arcs always go counterclockwise. Handle angle wrapping.
|
||||||
|
let startAngle = entity.startAngle;
|
||||||
|
let endAngle = entity.endAngle;
|
||||||
|
let angleDiff = endAngle - startAngle;
|
||||||
|
|
||||||
|
// If endAngle < startAngle, arc wraps through 0/360
|
||||||
|
if (angleDiff < 0) {
|
||||||
|
angleDiff += Math.PI * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const arcLength = angleDiff * entity.radius;
|
||||||
|
const segments = Math.max(minSegments, Math.ceil(arcLength / segmentSize));
|
||||||
|
const points = [];
|
||||||
|
|
||||||
|
if (entity.reversed) {
|
||||||
|
// Sample backwards from end to start
|
||||||
|
for (let i = 0; i <= segments; i++) {
|
||||||
|
const angle = endAngle - (i / segments) * angleDiff;
|
||||||
|
points.push({
|
||||||
|
x: entity.center.x + Math.cos(angle) * entity.radius,
|
||||||
|
y: entity.center.y + Math.sin(angle) * entity.radius,
|
||||||
|
z: entity.center.z || 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Sample forward from start to end
|
||||||
|
for (let i = 0; i <= segments; i++) {
|
||||||
|
const angle = startAngle + (i / segments) * angleDiff;
|
||||||
|
points.push({
|
||||||
|
x: entity.center.x + Math.cos(angle) * entity.radius,
|
||||||
|
y: entity.center.y + Math.sin(angle) * entity.radius,
|
||||||
|
z: entity.center.z || 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue