@mlightcad/libredwg-web
Version:
A DWG/DXF JavaScript parser based on libredwg
538 lines • 22 kB
JavaScript
import { DwgAttachmentPoint, DwgTextHorizontalAlign, isModelSpace } from '../database';
import { Box2D } from './box2d';
import { evaluateBSpline } from './bspline';
import { Color } from './color';
import { interpolatePolyline } from './polyline';
export class SvgConverter {
blockMap = new Map();
rotate(point, angle) {
const cos = Math.cos(angle);
const sin = Math.sin(angle);
return {
x: point.x * cos - point.y * sin,
y: point.x * sin + point.y * cos
};
}
/**
* Interpolates a B-spline curve and returns the resulting polyline.
*
* @param controlPoints The control points of the B-spline.
* @param degree The degree of the B-spline.
* @param knots The knot vector.
* @param interpolationsPerSplineSegment Number of interpolated points per spline segment.
* @param weights Optional weight vector for rational B-splines.
* @returns An array of interpolated 2D points representing the polyline.
*/
interpolateBSpline(controlPoints, degree, knots, interpolationsPerSplineSegment = 25, weights) {
const polyline = [];
const controlPointsForLib = controlPoints.map((p) => [p.x, p.y]);
const segmentTs = [knots[degree]];
const domain = [
knots[degree],
knots[knots.length - 1 - degree]
];
for (let k = degree + 1; k < knots.length - degree; ++k) {
if (segmentTs[segmentTs.length - 1] !== knots[k]) {
segmentTs.push(knots[k]);
}
}
for (let i = 1; i < segmentTs.length; ++i) {
const uMin = segmentTs[i - 1];
const uMax = segmentTs[i];
for (let k = 0; k <= interpolationsPerSplineSegment; ++k) {
const u = (k / interpolationsPerSplineSegment) * (uMax - uMin) + uMin;
let t = (u - domain[0]) / (domain[1] - domain[0]);
t = Math.max(0, Math.min(1, t)); // Clamp t to [0, 1]
const p = evaluateBSpline(t, degree, controlPointsForLib, knots, weights);
polyline.push({ x: p[0], y: p[1] });
}
}
return polyline;
}
addFlipXIfApplicable(entity, { bbox, element }) {
if ('extrusionDirection' in entity &&
entity.extrusionDirection.z === -1) {
return {
bbox: new Box2D()
.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 };
}
}
line(entity) {
const bbox = new Box2D()
.expandByPoint({ x: entity.startPoint.x, y: entity.startPoint.y })
.expandByPoint({ x: entity.endPoint.x, y: entity.endPoint.y });
const element = `<line x1="${entity.startPoint.x}" y1="${entity.startPoint.y}" x2="${entity.endPoint.x}" y2="${entity.endPoint.y}" />`;
return { bbox, element };
}
ray(entity) {
const scale = 10000;
const firstPoint = entity.firstPoint;
const secondPoint = {
x: firstPoint.x + entity.unitDirection.x * scale,
y: firstPoint.y + entity.unitDirection.y * scale
};
const bbox = new Box2D()
.expandByPoint(firstPoint)
.expandByPoint(secondPoint);
const element = `<line x1="${firstPoint.x}" y1="${firstPoint.y}" x2="${secondPoint.x}" y2="${secondPoint.y}" />`;
return { bbox, element };
}
xline(entity) {
const scale = 10000;
const firstPoint = {
x: entity.firstPoint.x - entity.unitDirection.x * scale,
y: entity.firstPoint.y - entity.unitDirection.y * scale
};
const secondPoint = {
x: entity.firstPoint.x + entity.unitDirection.x * scale,
y: entity.firstPoint.y + entity.unitDirection.y * scale
};
const bbox = new Box2D()
.expandByPoint(firstPoint)
.expandByPoint(secondPoint);
const element = `<line x1="${firstPoint.x}" y1="${firstPoint.y}" x2="${secondPoint.x}" y2="${secondPoint.y}" />`;
return { bbox, element };
}
extractMTextLines(mtext) {
return (mtext
// Convert Unicode codes to characters
.replace(/\\U\+([0-9A-Fa-f]{4})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
// Preserve line breaks: replace \P with newline placeholder
.replace(/\\P/g, '\n')
// Remove underline, overline
.replace(/\\[LOlo]/g, '')
// Remove font specs like \FArial|b0|i0|c134|p49;
.replace(/\\[Ff][^;\\]*?(?:\|[^;\\]*)*;/g, '')
// Remove formatting codes like \H1.0x; \W0.5; \C7 etc.
.replace(/\\[KkCcHhWwTtAa][^;\\]*;?/g, '')
// Remove general \word; style control codes like \x; \pxqc;
.replace(/\\[a-zA-Z]+;?/g, '')
// Remove AutoCAD %% control sequences like %%d, %%p, etc.
.replace(/%%(d|p|c|%)/gi, '')
// Replace escaped backslash
.replace(/\\\\/g, '\\')
// Replace non-breaking space
.replace(/\\~/g, '\u00A0')
// Remove grouping braces
.replace(/[{}]/g, '')
// Split by preserved newlines
.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0));
}
lines(lines, fontsize, insertionPoint, extentsWidth, anchor = 'start') {
const bbox = new Box2D()
.expandByPoint({
x: insertionPoint.x,
y: insertionPoint.y
})
.expandByPoint({
x: insertionPoint.x + extentsWidth,
y: insertionPoint.y - lines.length * fontsize * 1.5
});
const texts = lines.map((line, index) => {
const x = insertionPoint.x;
const y = insertionPoint.y - index * fontsize * 1.5;
const transform = `translate(${x},${y}) scale(1,-1) translate(${-x},${-y})`;
return `<text x="${x}" y="${y}" font-size="${fontsize}" text-anchor="${anchor}" transform="${transform}">${line}</text>`;
});
return { bbox, element: texts.join('\n') };
}
mtext(entity) {
const fontsize = entity.textHeight;
const insertionPoint = entity.insertionPoint;
const lines = this.extractMTextLines(entity.text);
const attachmentPoint = entity.attachmentPoint;
let anchor = 'start';
if (attachmentPoint == DwgAttachmentPoint.BottomCenter ||
attachmentPoint == DwgAttachmentPoint.MiddleCenter ||
attachmentPoint == DwgAttachmentPoint.TopCenter) {
anchor = 'middle';
}
else if (attachmentPoint == DwgAttachmentPoint.BottomRight ||
attachmentPoint == DwgAttachmentPoint.MiddleRight ||
attachmentPoint == DwgAttachmentPoint.TopRight) {
anchor = 'end';
}
return this.lines(lines, fontsize, insertionPoint, entity.extentsWidth, anchor);
}
table(entity) {
const { rowCount, columnCount, rowHeightArr, columnWidthArr, startPoint, cells } = entity;
const originX = startPoint.x;
const originY = startPoint.y;
// Compute cell rectangles
const cellRects = [];
for (let row = 0, y = originY; row < rowCount; row++) {
const height = rowHeightArr[row];
let x = originX;
for (let col = 0; col < columnCount; col++) {
const cellIndex = row * columnCount + col;
const cell = cells[cellIndex];
const width = columnWidthArr[col];
cellRects.push({ x, y, width, height, cell, row, col });
x += width;
}
y += height;
}
// Create SVG content
const svgElements = cellRects
.map(({ x, y, width, height, cell }) => {
const lines = [];
if (cell.topBorderVisibility)
lines.push(`<line x1="${x}" y1="${y}" x2="${x + width}" y2="${y}" stroke="black" />`);
if (cell.bottomBorderVisibility)
lines.push(`<line x1="${x}" y1="${y + height}" x2="${x + width}" y2="${y + height}" stroke="black" />`);
if (cell.leftBorderVisibility)
lines.push(`<line x1="${x}" y1="${y}" x2="${x}" y2="${y + height}" stroke="black" />`);
if (cell.rightBorderVisibility)
lines.push(`<line x1="${x + width}" y1="${y}" x2="${x + width}" y2="${y + height}" stroke="black" />`);
const textX = x + width / 2;
const textY = y + height / 2 + cell.textHeight / 3;
const text = `<text x="${textX}" y="${textY}" font-size="${cell.textHeight}" text-anchor="middle" dominant-baseline="middle">${cell.text}</text>`;
return [...lines, text].join('\n');
})
.join('\n');
const totalWidth = columnWidthArr.reduce((sum, w) => sum + w, 0);
const totalHeight = rowHeightArr.reduce((sum, h) => sum + h, 0);
const bbox = new Box2D()
.expandByPoint({ x: originX, y: originY })
.expandByPoint({ x: originX + totalWidth, y: originY + totalHeight });
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="${totalWidth}" height="${totalHeight}" viewBox="${originX} ${originY} ${totalWidth} ${totalHeight}">
${svgElements}
</svg>
`.trim();
return {
bbox,
element: svg
};
}
text(entity) {
const fontsize = entity.textHeight;
const insertionPoint = entity.startPoint;
const lines = [entity.text];
let extentsWidth = entity.endPoint.x - entity.endPoint.x;
if (entity.halign == 0) {
extentsWidth = entity.text.length * fontsize + entity.startPoint.x;
}
let anchor = 'start';
if (entity.halign == DwgTextHorizontalAlign.CENTER) {
anchor = 'middle';
}
else if (entity.halign == DwgTextHorizontalAlign.RIGHT) {
anchor = 'end';
}
return this.lines(lines, fontsize, insertionPoint, extentsWidth, anchor);
}
vertices(vertices, closed = false) {
const bbox = vertices.reduce((acc, point) => acc.expandByPoint(point), new Box2D());
let d = vertices.reduce((acc, point, i) => {
acc += i === 0 ? 'M' : 'L';
acc += point.x + ',' + point.y;
return acc;
}, '');
if (closed) {
d += 'Z';
}
return { bbox, element: `<path d="${d}" />` };
}
circle(entity) {
const bbox0 = new Box2D()
.expandByPoint({
x: entity.center.x + entity.radius,
y: entity.center.y + entity.radius
})
.expandByPoint({
x: entity.center.x - entity.radius,
y: entity.center.y - entity.radius
});
const element0 = `<circle cx="${entity.center.x}" cy="${entity.center.y}" r="${entity.radius}" />`;
return {
bbox: bbox0,
element: element0
};
}
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 = this.rotate({ x: Math.cos(startAngle) * rx, y: Math.sin(startAngle) * ry }, rotationAngle);
const startPoint = { x: cx + startOffset.x, y: cy + startOffset.y };
const endOffset = this.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 Box2D());
return bbox;
}
ellipse(entity) {
const { bbox: bbox0, element: element0 } = this.ellipseOrArc(entity.center.x, entity.center.y, entity.majorAxisEndPoint.x, entity.majorAxisEndPoint.y, entity.axisRatio, entity.startAngle, entity.endAngle);
return {
bbox: bbox0,
element: element0
};
}
arc(entity) {
const { bbox: bbox0, element: element0 } = this.ellipseOrArc(entity.center.x, entity.center.y, entity.radius, 0, 1, entity.startAngle, entity.endAngle);
return {
bbox: bbox0,
element: element0
};
}
dimension(entity) {
const block = this.blockMap.get(entity.name);
if (block) {
return {
bbox: block.bbox,
element: `<use href="#${entity.name}" />`
};
}
return null;
}
insert(entity) {
const block = this.blockMap.get(entity.name);
if (block) {
// In SVG, the unit of rotate is degrees — not radians.
const insertionPoint = entity.insertionPoint;
// const basePoint = block.bbox.min
const rotation = entity.rotation * (180 / Math.PI);
const transform = `translate(${insertionPoint.x},${insertionPoint.y}) rotate(${rotation}) scale(${entity.xScale},${entity.yScale})`;
const newBBox = block.bbox
.clone()
.transform({ x: entity.xScale, y: entity.yScale }, { x: insertionPoint.x, y: insertionPoint.y })
.rotate(entity.rotation, insertionPoint);
return {
bbox: newBBox,
element: `<use href="#${entity.name}" transform="${transform}" />`
};
}
return null;
}
block(block, dwg) {
const entities = block.entities;
const { bbox, elements } = entities.reduce((acc, 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);
}
const color = this.getEntityColor(dwg.tables.LAYER.entries, entity);
const fill = entity.type == 'TEXT' || entity.type == 'MTEXT'
? color.cssColor
: 'none';
if (color.isByBlock) {
acc.elements.push(`<g id="${entity.handle}">${element}</g>`);
}
else {
acc.elements.push(`<g id="${entity.handle}" stroke="${color.cssColor}" fill="${fill}">${element}</g>`);
}
}
return acc;
}, {
bbox: new Box2D(),
elements: []
});
if (bbox.valid) {
return {
bbox,
element: `<g id="${block.name}">${elements.join('\n')}</g>`
};
}
return null;
}
entityToBoundsAndElement(entity) {
let result = null;
switch (entity.type) {
case 'ARC':
result = this.arc(entity);
break;
case 'CIRCLE':
result = this.circle(entity);
break;
case 'DIMENSION':
result = this.dimension(entity);
break;
case 'ELLIPSE':
result = this.ellipse(entity);
break;
case 'INSERT':
result = this.insert(entity);
break;
case 'LINE':
result = this.line(entity);
break;
case 'LWPOLYLINE': {
const lwpolyline = entity;
const closed = !!(lwpolyline.flag & 0x200);
const vertices = interpolatePolyline(lwpolyline, closed);
result = this.vertices(vertices, closed);
break;
}
case 'MTEXT':
result = this.mtext(entity);
break;
case 'SPLINE': {
const spline = entity;
result = this.vertices(this.interpolateBSpline(spline.controlPoints, spline.degree, spline.knots, 25, spline.weights));
break;
}
case 'POLYLINE': {
// const polyline = entity as DwgPolylineEntity
// const closed = !!(polyline.flag & 0x1)
// const vertices = interpolatePolyline(polyline, closed)
// result = this.vertices(vertices, closed)
break;
}
case 'RAY':
result = this.ray(entity);
break;
case 'TABLE':
result = this.table(entity);
break;
case 'TEXT':
result = this.text(entity);
break;
case 'XLINE':
result = this.xline(entity);
break;
default:
result = null;
break;
}
if (result) {
return this.addFlipXIfApplicable(entity, result);
}
return null;
}
getEntityColor(layers, entity) {
// Get entity color
const color = new Color();
if (entity.colorIndex != null) {
color.colorIndex = entity.colorIndex;
}
else if (entity.colorName) {
color.colorName = entity.colorName;
}
else if (entity.color != null) {
color.color = entity.color;
}
// If it is white color, convert it to black because the background of svg is white
if (color.colorIndex == 7) {
color.colorIndex = 256;
}
// If color is 'byLayer', use the layer color
if (color.isByLayer) {
const layer = layers.find((layer) => layer.name === entity.layer);
if (layer != null) {
color.colorIndex = layer.colorIndex;
}
}
if (color.color == null) {
color.color = 0xffffff;
}
return color;
}
convert(dwg) {
let modelSpace = null;
this.blockMap.clear();
let blockElements = '';
dwg.tables.BLOCK_RECORD.entries.forEach(block => {
if (isModelSpace(block.name)) {
modelSpace = block;
}
else {
const item = this.block(block, dwg);
if (item) {
blockElements += item.element;
this.blockMap.set(block.name, item);
}
}
});
const ms = this.block(modelSpace, dwg);
const viewBox = ms && ms.bbox.valid
? {
x: ms.bbox.min.x,
y: -ms.bbox.max.y,
width: ms.bbox.max.x - ms.bbox.min.x,
height: ms.bbox.max.y - ms.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%"
>
<defs>${blockElements}</defs>
<g stroke="#000000" stroke-width="0.1%" fill="none" transform="matrix(1,0,0,-1,0,0)">
${ms ? ms.element : ''}
</g>
</svg>`;
}
}
//# sourceMappingURL=svgConverter.js.map