UNPKG

@mlightcad/libredwg-web

Version:

A DWG/DXF JavaScript parser based on libredwg

235 lines 9.97 kB
import { entityToPolyline } from './util/entityToPolyline'; import { denormalise } from './util/denormalise'; import { rotate } from './util/rotate'; import { rgbToColorAttribute } from './util/rgbToColorAttribute'; import { toPiecewiseBezier, multiplicity } from './util/toPiecewiseBezier'; import { transformBoundingBoxAndElement } from './util/transformBoundingBoxAndElement'; import { BoundingBox } from './util/boundingBox'; import { Color } from './color'; export class LibreDwgSVGConverter { addFlipXIfApplicable(entity, { bbox, element }) { if (entity.extrusionZ === -1) { return { bbox: new BoundingBox() .expandByPoint({ x: -bbox.min.x, y: bbox.min.y }) .expandByPoint({ x: -bbox.max.x, y: bbox.max.y }), element: `<g transform="matrix(-1 0 0 1 0 0)">${element}</g>` }; } else { return { bbox, element }; } } polyline(entity) { const vertices = entityToPolyline(entity); const bbox = vertices.reduce((acc, [x, y]) => acc.expandByPoint({ x, y }), new BoundingBox()); const d = vertices.reduce((acc, point, i) => { acc += i === 0 ? 'M' : 'L'; acc += point[0] + ',' + point[1]; return acc; }, ''); return transformBoundingBoxAndElement(bbox, `<path d="${d}" />`, entity.transforms); } circle(entity) { const bbox0 = new BoundingBox() .expandByPoint({ x: entity.x + entity.r, y: entity.y + entity.r }) .expandByPoint({ x: entity.x - entity.r, y: entity.y - entity.r }); const element0 = `<circle cx="${entity.x}" cy="${entity.y}" r="${entity.r}" />`; const { bbox, element } = this.addFlipXIfApplicable(entity, { bbox: bbox0, element: element0 }); return transformBoundingBoxAndElement(bbox, element, entity.transforms); } ellipseOrArc(cx, cy, majorX, majorY, axisRatio, startAngle, endAngle) { const rx = Math.sqrt(majorX * majorX + majorY * majorY); const ry = axisRatio * rx; const rotationAngle = -Math.atan2(-majorY, majorX); const bbox = this.bboxEllipseOrArc(cx, cy, majorX, majorY, axisRatio, startAngle, endAngle); if (Math.abs(startAngle - endAngle) < 1e-9 || Math.abs(startAngle - endAngle + Math.PI * 2) < 1e-9) { const element = `<g transform="rotate(${(rotationAngle / Math.PI) * 180} ${cx}, ${cy})"><ellipse cx="${cx}" cy="${cy}" rx="${rx}" ry="${ry}" /></g>`; return { bbox, element }; } else { const startOffset = rotate({ x: Math.cos(startAngle) * rx, y: Math.sin(startAngle) * ry }, rotationAngle); const startPoint = { x: cx + startOffset.x, y: cy + startOffset.y }; const endOffset = rotate({ x: Math.cos(endAngle) * rx, y: Math.sin(endAngle) * ry }, rotationAngle); const endPoint = { x: cx + endOffset.x, y: cy + endOffset.y }; const adjustedEndAngle = endAngle < startAngle ? endAngle + Math.PI * 2 : endAngle; const largeArcFlag = adjustedEndAngle - startAngle < Math.PI ? 0 : 1; const d = `M ${startPoint.x} ${startPoint.y} A ${rx} ${ry} ${(rotationAngle / Math.PI) * 180} ${largeArcFlag} 1 ${endPoint.x} ${endPoint.y}`; const element = `<path d="${d}" />`; return { bbox, element }; } } bboxEllipseOrArc(cx, cy, majorX, majorY, axisRatio, startAngle, endAngle) { while (startAngle < 0) startAngle += Math.PI * 2; while (endAngle <= startAngle) endAngle += Math.PI * 2; const angles = []; if (Math.abs(majorX) < 1e-12 || Math.abs(majorY) < 1e-12) { for (let i = 0; i < 4; i++) { angles.push((i / 2) * Math.PI); } } else { angles[0] = Math.atan((-majorY * axisRatio) / majorX) - Math.PI; angles[1] = Math.atan((majorX * axisRatio) / majorY) - Math.PI; angles[2] = angles[0] - Math.PI; angles[3] = angles[1] - Math.PI; } for (let i = 4; i >= 0; i--) { while (angles[i] < startAngle) angles[i] += Math.PI * 2; if (angles[i] > endAngle) { angles.splice(i, 1); } } angles.push(startAngle); angles.push(endAngle); const pts = angles.map(a => ({ x: Math.cos(a), y: Math.sin(a) })); const M = [ [majorX, -majorY * axisRatio], [majorY, majorX * axisRatio] ]; const rotatedPts = pts.map(p => ({ x: p.x * M[0][0] + p.y * M[0][1] + cx, y: p.x * M[1][0] + p.y * M[1][1] + cy })); const bbox = rotatedPts.reduce((acc, p) => { acc.expandByPoint(p); return acc; }, new BoundingBox()); return bbox; } ellipse(entity) { const { bbox: bbox0, element: element0 } = this.ellipseOrArc(entity.x, entity.y, entity.majorX, entity.majorY, entity.axisRatio, entity.startAngle, entity.endAngle); const { bbox, element } = this.addFlipXIfApplicable(entity, { bbox: bbox0, element: element0 }); return transformBoundingBoxAndElement(bbox, element, entity.transforms); } arc(entity) { const { bbox: bbox0, element: element0 } = this.ellipseOrArc(entity.x, entity.y, entity.r, 0, 1, entity.startAngle, entity.endAngle); const { bbox, element } = this.addFlipXIfApplicable(entity, { bbox: bbox0, element: element0 }); return transformBoundingBoxAndElement(bbox, element, entity.transforms); } piecewiseToPaths(k, knots, controlPoints) { const paths = []; let controlPointIndex = 0; let knotIndex = k; while (knotIndex < knots.length - k + 1) { const m = multiplicity(knots, knotIndex); const cp = controlPoints.slice(controlPointIndex, controlPointIndex + k); if (k === 4) { paths.push(`<path d="M ${cp[0].x} ${cp[0].y} C ${cp[1].x} ${cp[1].y} ${cp[2].x} ${cp[2].y} ${cp[3].x} ${cp[3].y}" />`); } else if (k === 3) { paths.push(`<path d="M ${cp[0].x} ${cp[0].y} Q ${cp[1].x} ${cp[1].y} ${cp[2].x} ${cp[2].y}" />`); } controlPointIndex += m; knotIndex += m; } return paths; } bezier(entity) { let bbox = new BoundingBox(); entity.controlPoints.forEach((p) => { bbox = bbox.expandByPoint(p); }); const k = entity.degree + 1; const piecewise = toPiecewiseBezier(k, entity.controlPoints, entity.knots); const paths = this.piecewiseToPaths(k, piecewise.knots, piecewise.controlPoints); const element = `<g>${paths.join('')}</g>`; return transformBoundingBoxAndElement(bbox, element, entity.transforms); } entityToBoundsAndElement(entity) { switch (entity.type) { case 'CIRCLE': return this.circle(entity); case 'ELLIPSE': return this.ellipse(entity); case 'ARC': return this.arc(entity); case 'SPLINE': { const hasWeights = entity.weights && entity.weights.some((w) => w !== 1); if ((entity.degree === 2 || entity.degree === 3) && !hasWeights) { try { return this.bezier(entity); } catch (err) { return this.polyline(entity); } } else { return this.polyline(entity); } } case 'LINE': case 'LWPOLYLINE': case 'POLYLINE': return this.polyline(entity); default: return null; } } getEntityColor(layers, entity) { const layer = layers.find((layer) => layer.name === entity.layer); if (layer) { const color = new Color(layer.colorIndex); return `rgb(${color.red}, ${color.green}, ${color.blue})`; } return "rgb(0, 0, 0)"; // Default to black if layer not found } convert(dwg) { const entities = denormalise(dwg); const { bbox, elements } = entities.reduce((acc, entity) => { const rgb = this.getEntityColor(dwg.tables.LAYER.entries, entity); const boundsAndElement = this.entityToBoundsAndElement(entity); if (boundsAndElement) { const { bbox, element } = boundsAndElement; if (bbox.valid) { acc.bbox.expandByPoint(bbox.min); acc.bbox.expandByPoint(bbox.max); } acc.elements.push(`<g stroke="${rgbToColorAttribute(rgb)}">${element}</g>`); } return acc; }, { bbox: new BoundingBox(), elements: [] }); const viewBox = bbox.valid ? { x: bbox.min.x, y: -bbox.max.y, width: bbox.max.x - bbox.min.x, height: bbox.max.y - bbox.min.y } : { x: 0, y: 0, width: 0, height: 0 }; return `<?xml version="1.0"?> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" preserveAspectRatio="xMinYMin meet" viewBox="${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}" width="100%" height="100%" > <g stroke="#000000" stroke-width="0.1%" fill="none" transform="matrix(1,0,0,-1,0,0)"> ${elements.join('\n')} </g> </svg>`; } } //# sourceMappingURL=ToSVG.js.map