@mlightcad/libredwg-web
Version:
A DWG/DXF JavaScript parser based on libredwg
235 lines • 9.97 kB
JavaScript
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