@bitbybit-dev/base
Version:
Bit By Bit Developers Base CAD Library to Program Geometry
659 lines (658 loc) • 21.3 kB
JavaScript
export class DxfGenerator {
constructor() {
this.entityHandle = 256; // Start at 256 to avoid conflicts with system handles
this.colorFormat = "aci";
this.acadVersion = "AC1009";
}
/**
* Generate a complete DXF file content from path-based entities
*/
generateDxf(dxfInputs) {
// Set format options from input
this.colorFormat = dxfInputs.colorFormat || "aci";
this.acadVersion = dxfInputs.acadVersion || "AC1009";
const dxfContent = [];
// Header section
dxfContent.push(...this.generateHeader());
// Tables section
dxfContent.push(...this.generateTables(dxfInputs));
// Blocks section (required by many CAD programs)
dxfContent.push(...this.generateBlocks());
// Entities section
dxfContent.push(...this.generateEntities(dxfInputs));
// End of file
dxfContent.push("0", "EOF");
return dxfContent.join("\n");
}
/**
* Generate DXF header section
*/
generateHeader() {
const header = [
"0",
"SECTION",
"2",
"HEADER",
"9",
"$ACADVER",
"1",
this.acadVersion
];
if (this.acadVersion === "AC1009") {
// AC1009 (AutoCAD R12) - minimal header for maximum compatibility
header.push("9", "$DWGCODEPAGE", "3", "ascii", "9", "$HANDSEED", "5", "0");
}
else {
// AC1015 (AutoCAD 2000) - modern format
header.push("9", "$DWGCODEPAGE", "3", "ANSI_1252", "9", "$LASTSAVEDBY", "1", "bitbybit.dev", "9", "$HANDSEED", "5", "20000");
}
header.push("0", "ENDSEC");
return header;
}
/**
* Generate DXF tables section (layers, line types, etc.)
*/
generateTables(dxfInputs) {
const tables = [
"0",
"SECTION",
"2",
"TABLES"
];
// VPORT table (required for AC1009)
if (this.acadVersion === "AC1009") {
tables.push(...this.generateVportTable());
}
// Line type table (required)
tables.push(...this.generateLineTypeTable());
// Layer table
tables.push(...this.generateLayerTable(dxfInputs));
// Text style table (required for text entities, included for completeness)
tables.push(...this.generateStyleTable());
// Additional tables for AC1009
if (this.acadVersion === "AC1009") {
tables.push(...this.generateViewTable());
tables.push(...this.generateUcsTable());
tables.push(...this.generateAppidTable());
tables.push(...this.generateDimstyleTable());
}
tables.push("0", "ENDSEC");
return tables;
}
/**
* Generate line type table
*/
generateLineTypeTable() {
const ltype = [
"0",
"TABLE",
"2",
"LTYPE",
"70",
"1" // Number of line types
];
if (this.acadVersion === "AC1015") {
ltype.push("5", "5", "100", "AcDbSymbolTable");
}
ltype.push("0", "LTYPE", "2", "CONTINUOUS", "70", this.acadVersion === "AC1009" ? "64" : "0", "3", "Solid line", "72", "65", "73", "0", "40", "0.0");
if (this.acadVersion === "AC1015") {
ltype.splice(ltype.indexOf("LTYPE") + 1, 0, "5", "14", "100", "AcDbSymbolTableRecord", "100", "AcDbLinetypeTableRecord");
}
ltype.push("0", "ENDTAB");
return ltype;
}
/**
* Generate text style table
*/
generateStyleTable() {
const style = [
"0",
"TABLE",
"2",
"STYLE",
"70",
"1" // Number of styles
];
if (this.acadVersion === "AC1015") {
style.push("5", "3", "100", "AcDbSymbolTable");
}
style.push("0", "STYLE", "2", "STANDARD", "70", "0", "40", "0.0", "41", "1.0", "50", "0.0", "71", "0", "42", "0.2", "3", "txt", "4", "");
if (this.acadVersion === "AC1015") {
style.splice(style.indexOf("STYLE") + 1, 0, "5", "11", "100", "AcDbSymbolTableRecord", "100", "AcDbTextStyleTableRecord");
}
style.push("0", "ENDTAB");
return style;
}
/**
* Generate VPORT table (viewport configuration)
*/
generateVportTable() {
return [
"0",
"TABLE",
"2",
"VPORT",
"70",
"2",
"0",
"VPORT",
"2",
"*ACTIVE",
"70",
"0",
"10",
"0.0",
"20",
"0.0",
"11",
"1.0",
"21",
"1.0",
"12",
"15.0",
"22",
"11.101231",
"13",
"0.0",
"23",
"0.0",
"14",
"0.1",
"24",
"0.1",
"15",
"0.5",
"25",
"0.5",
"16",
"0.0",
"26",
"0.0",
"36",
"1.0",
"17",
"0.0",
"27",
"0.0",
"37",
"0.0",
"40",
"22.202462",
"41",
"1.351201",
"42",
"50.0",
"43",
"0.0",
"44",
"0.0",
"50",
"0.0",
"51",
"0.0",
"71",
"0",
"72",
"100",
"73",
"1",
"74",
"1",
"75",
"1",
"76",
"0",
"77",
"0",
"78",
"0",
"0",
"ENDTAB"
];
}
/**
* Generate VIEW table (empty but required for AC1009)
*/
generateViewTable() {
return [
"0",
"TABLE",
"2",
"VIEW",
"70",
"0",
"0",
"ENDTAB"
];
}
/**
* Generate UCS table (user coordinate system - empty but required for AC1009)
*/
generateUcsTable() {
return [
"0",
"TABLE",
"2",
"UCS",
"70",
"0",
"0",
"ENDTAB"
];
}
/**
* Generate APPID table (application ID - required for AC1009)
*/
generateAppidTable() {
return [
"0",
"TABLE",
"2",
"APPID",
"70",
"1",
"0",
"APPID",
"2",
"ACAD",
"70",
"64",
"0",
"ENDTAB"
];
}
/**
* Generate DIMSTYLE table (dimension style - empty but required for AC1009)
*/
generateDimstyleTable() {
return [
"0",
"TABLE",
"2",
"DIMSTYLE",
"70",
"0",
"0",
"ENDTAB"
];
}
/**
* Generate blocks section (empty but required)
*/
generateBlocks() {
return [
"0",
"SECTION",
"2",
"BLOCKS",
"0",
"ENDSEC"
];
}
/**
* Generate layer table based on unique layers in all parts
*/
generateLayerTable(dxfInputs) {
const layers = new Set();
// Collect all unique layer names
if (dxfInputs.dxfPathsParts) {
dxfInputs.dxfPathsParts.forEach(part => {
if (part.layer) {
layers.add(part.layer);
}
});
}
// Add default layer if no layers specified
if (layers.size === 0) {
layers.add("0");
}
const layerTable = [
"0",
"TABLE",
"2",
"LAYER",
"70",
layers.size.toString()
];
if (this.acadVersion === "AC1015") {
layerTable.splice(4, 0, "5", "2", "100", "AcDbSymbolTable");
}
// Generate layer entries
layers.forEach(layerName => {
layerTable.push("0", "LAYER", "2", layerName, "70", "0", "62", "7", // Default color (white)
"6", "CONTINUOUS" // Line type
);
// Add AC1015-specific subclass markers
if (this.acadVersion === "AC1015") {
const insertIdx = layerTable.lastIndexOf("LAYER") + 1;
layerTable.splice(insertIdx, 0, "5", this.getNextHandle(), "100", "AcDbSymbolTableRecord", "100", "AcDbLayerTableRecord");
}
});
layerTable.push("0", "ENDTAB");
return layerTable;
}
/**
* Generate DXF entities section with all path segments
*/
generateEntities(dxfInputs) {
const entities = [
"0",
"SECTION",
"2",
"ENTITIES"
];
if (dxfInputs.dxfPathsParts) {
dxfInputs.dxfPathsParts.forEach(part => {
if (part.paths) {
part.paths.forEach(path => {
if (path.segments) {
path.segments.forEach(segment => {
entities.push(...this.generateSegmentEntity(segment, part));
});
}
});
}
});
}
entities.push("0", "ENDSEC");
return entities;
}
/**
* Generate entity for a single segment based on its type
*/
generateSegmentEntity(segment, part) {
// Check segment type and generate appropriate entity
if (this.isLineSegment(segment)) {
return this.generateLineEntity(segment, part);
}
else if (this.isArcSegment(segment)) {
return this.generateArcEntity(segment, part);
}
else if (this.isCircleSegment(segment)) {
return this.generateCircleEntity(segment, part);
}
else if (this.isPolylineSegment(segment)) {
return this.generatePolylineEntity(segment, part);
}
else if (this.isSplineSegment(segment)) {
return this.generateSplineEntity(segment, part);
}
return [];
}
/**
* Type guard for line segments
*/
isLineSegment(segment) {
return segment.start !== undefined && segment.end !== undefined && segment.radius === undefined;
}
/**
* Type guard for arc segments
*/
isArcSegment(segment) {
return segment.center !== undefined && segment.radius !== undefined && segment.startAngle !== undefined && segment.endAngle !== undefined;
}
/**
* Type guard for circle segments
*/
isCircleSegment(segment) {
return segment.center !== undefined && segment.radius !== undefined && segment.startAngle === undefined;
}
/**
* Type guard for polyline segments
*/
isPolylineSegment(segment) {
return segment.points !== undefined && Array.isArray(segment.points);
}
/**
* Type guard for spline segments
*/
isSplineSegment(segment) {
return segment.controlPoints !== undefined && Array.isArray(segment.controlPoints);
}
/**
* Generate a LINE entity
*/
generateLineEntity(line, part) {
const entity = [
"0",
"LINE",
"8",
part.layer || "0"
];
// Add color if specified
if (part.color !== undefined) {
const colorCodes = this.convertColorToDxf(part.color);
colorCodes.forEach(cc => entity.push(cc.code, cc.value));
}
entity.push("10", line.start[0].toFixed(6), "20", line.start[1].toFixed(6), "30", "0.00", // Z coordinate (2D)
"11", line.end[0].toFixed(6), "21", line.end[1].toFixed(6), "31", "0.00" // Z coordinate (2D)
);
// Add AC1015-specific codes
if (this.acadVersion === "AC1015") {
entity.splice(2, 0, "5", this.getNextHandle(), "100", "AcDbEntity");
const coordIdx = entity.indexOf("10");
entity.splice(coordIdx, 0, "100", "AcDbLine");
}
return entity;
}
/**
* Generate a CIRCLE entity
*/
generateCircleEntity(circle, part) {
const entity = [
"0",
"CIRCLE",
"8",
part.layer || "0"
];
// Add color if specified
if (part.color !== undefined) {
const colorCodes = this.convertColorToDxf(part.color);
colorCodes.forEach(cc => entity.push(cc.code, cc.value));
}
entity.push("10", circle.center[0].toFixed(6), "20", circle.center[1].toFixed(6), "30", "0.00", "40", circle.radius.toFixed(6));
// Add AC1015-specific codes
if (this.acadVersion === "AC1015") {
entity.splice(2, 0, "5", this.getNextHandle(), "100", "AcDbEntity");
const coordIdx = entity.indexOf("10");
entity.splice(coordIdx, 0, "100", "AcDbCircle");
}
return entity;
}
/**
* Generate an ARC entity
*/
generateArcEntity(arc, part) {
const entity = [
"0",
"ARC",
"8",
part.layer || "0"
];
// Add line type for AC1009 (optional empty)
if (this.acadVersion === "AC1009") {
entity.push("6", " ");
}
// Add color if specified
if (part.color !== undefined) {
const colorCodes = this.convertColorToDxf(part.color);
colorCodes.forEach(cc => entity.push(cc.code, cc.value));
}
entity.push("10", arc.center[0].toFixed(6), "20", arc.center[1].toFixed(6), "30", this.acadVersion === "AC1009" ? "" : "0.0", "40", arc.radius.toFixed(6), "50", arc.startAngle.toFixed(6), "51", arc.endAngle.toFixed(6));
// Add AC1015-specific codes
if (this.acadVersion === "AC1015") {
entity.splice(2, 0, "5", this.getNextHandle(), "100", "AcDbEntity");
const coordIdx = entity.indexOf("10");
entity.splice(coordIdx, 0, "100", "AcDbCircle");
const angleIdx = entity.indexOf("50");
entity.splice(angleIdx, 0, "100", "AcDbArc");
}
return entity;
}
/**
* Generate a LWPOLYLINE entity
*/
generatePolylineEntity(polyline, part) {
const entity = [
"0",
"LWPOLYLINE",
"8",
part.layer || "0"
];
// Add color if specified
if (part.color !== undefined) {
const colorCodes = this.convertColorToDxf(part.color);
colorCodes.forEach(cc => entity.push(cc.code, cc.value));
}
const isClosed = polyline.closed || (polyline.points.length > 2 && this.isClosedPolyline(polyline.points));
entity.push("90", polyline.points.length.toString(), "70", isClosed ? "1" : "0");
// Add AC1015-specific codes
if (this.acadVersion === "AC1015") {
entity.splice(2, 0, "5", this.getNextHandle(), "100", "AcDbEntity");
const pointIdx = entity.indexOf("90");
entity.splice(pointIdx, 0, "100", "AcDbPolyline");
}
// Add vertices
polyline.points.forEach((point, index) => {
entity.push("10", point[0].toFixed(6), "20", point[1].toFixed(6));
// Add bulge value if specified (for arc segments)
if (polyline.bulges && polyline.bulges.length > index) {
const bulge = polyline.bulges[index];
if (bulge !== 0) {
entity.push("42", bulge.toFixed(6));
}
}
});
return entity;
}
/**
* Generate a SPLINE entity
*/
generateSplineEntity(spline, part) {
const entity = [
"0",
"SPLINE",
"8",
part.layer || "0"
];
// Add color if specified
if (part.color !== undefined) {
const colorCodes = this.convertColorToDxf(part.color);
colorCodes.forEach(cc => entity.push(cc.code, cc.value));
}
const degree = spline.degree || 3;
const numControlPoints = spline.controlPoints.length;
const numKnots = numControlPoints + degree + 1;
// Spline flags: 1 = closed, 2 = periodic, 4 = rational, 8 = planar, 16 = linear
const flags = spline.closed ? 9 : 8; // Add planar flag (8) for 2D splines
entity.push("210", "0.0", "220", "0.0", "230", "1.0", // Normal vector (Z-axis for 2D)
"70", flags.toString(), "71", degree.toString(), "72", numKnots.toString(), "73", numControlPoints.toString(), "74", "0" // Number of fit points (we're using control points)
);
// Add AC1015-specific codes
if (this.acadVersion === "AC1015") {
entity.splice(2, 0, "5", this.getNextHandle(), "100", "AcDbEntity");
const normalIdx = entity.indexOf("210");
entity.splice(normalIdx, 0, "100", "AcDbSpline");
}
// Generate knot values (uniform knot vector)
for (let i = 0; i < numKnots; i++) {
entity.push("40", i.toFixed(6));
}
// Add control points
spline.controlPoints.forEach(point => {
entity.push("10", point[0].toFixed(6), "20", point[1].toFixed(6), "30", "0.0" // Z coordinate (2D)
);
});
return entity;
}
/**
* Check if polyline should be closed (first and last points are the same)
*/
isClosedPolyline(points) {
if (points.length < 3)
return false;
const first = points[0];
const last = points[points.length - 1];
return Math.abs(first[0] - last[0]) < 1e-10 &&
Math.abs(first[1] - last[1]) < 1e-10;
}
/**
* Get next entity handle as hex string
*/
getNextHandle() {
return (++this.entityHandle).toString(16).toUpperCase();
}
/**
* Convert color to DXF format
* Accepts hex color (#RRGGBB) or ACI color index (1-255)
* Returns appropriate DXF color codes based on colorFormat setting
*/
convertColorToDxf(color) {
// If it's already a number (ACI index), use it directly
if (/^\d+$/.test(color)) {
const colorIndex = parseInt(color, 10);
if (colorIndex >= 1 && colorIndex <= 255) {
return [{ code: "62", value: color }];
}
}
// If it's a hex color, handle based on format preference
if (color.startsWith("#")) {
const hex = color.substring(1);
if (hex.length === 6) {
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
if (this.colorFormat === "truecolor") {
// Use 24-bit true color for full color spectrum (newer CAD software)
const trueColor = (r * 65536) + (g * 256) + b;
return [
{ code: "62", value: "256" },
{ code: "420", value: trueColor.toString() }
];
}
else {
// Use ACI color index for better compatibility (older CAD software)
const aciIndex = this.rgbToAciColorIndex(r, g, b);
return [{ code: "62", value: aciIndex.toString() }];
}
}
}
// Default to white (7) if color can't be parsed
return [{ code: "62", value: "7" }];
}
/**
* Convert RGB values to nearest AutoCAD Color Index (ACI)
* Uses a simplified mapping to standard ACI colors
*/
rgbToAciColorIndex(r, g, b) {
// ACI standard colors (simplified mapping)
const aciColors = {
1: [255, 0, 0],
2: [255, 255, 0],
3: [0, 255, 0],
4: [0, 255, 255],
5: [0, 0, 255],
6: [255, 0, 255],
7: [255, 255, 255],
8: [128, 128, 128],
9: [192, 192, 192] // Light gray
};
// Special case for black or very dark colors
if (r < 30 && g < 30 && b < 30) {
return 7; // Use white for visibility on dark backgrounds
}
// Find nearest color
let nearestIndex = 7;
let minDistance = Infinity;
for (const [index, [cr, cg, cb]] of Object.entries(aciColors)) {
const distance = Math.sqrt(Math.pow(r - cr, 2) +
Math.pow(g - cg, 2) +
Math.pow(b - cb, 2));
if (distance < minDistance) {
minDistance = distance;
nearestIndex = parseInt(index);
}
}
return nearestIndex;
}
}