dxf-writer
Version:
Dead simple 2D DXF writer
593 lines (537 loc) • 17.3 kB
JavaScript
const LineType = require("./LineType");
const Layer = require("./Layer");
const Table = require("./Table");
const DimStyleTable = require("./DimStyleTable");
const TextStyle = require("./TextStyle");
const Viewport = require("./Viewport");
const AppId = require("./AppId");
const Block = require("./Block");
const BlockRecord = require("./BlockRecord");
const Dictionary = require("./Dictionary");
const Line = require("./Line");
const Line3d = require("./Line3d");
const Arc = require("./Arc");
const Circle = require("./Circle");
const Cylinder = require("./Cylinder");
const Text = require("./Text");
const Polyline = require("./Polyline");
const Polyline3d = require("./Polyline3d");
const Face = require("./Face");
const Point = require("./Point");
const Spline = require("./Spline");
const Ellipse = require("./Ellipse");
const TagsManager = require("./TagsManager");
const Handle = require("./Handle");
class Drawing {
constructor() {
this.layers = {};
this.activeLayer = null;
this.lineTypes = {};
this.headers = {};
this.tables = {};
this.blocks = {};
this.dictionary = new Dictionary();
this.setUnits("Unitless");
for (const ltype of Drawing.LINE_TYPES) {
this.addLineType(ltype.name, ltype.description, ltype.elements);
}
for (const l of Drawing.LAYERS) {
this.addLayer(l.name, l.colorNumber, l.lineTypeName);
}
this.setActiveLayer("0");
// Must call this function
this.generateAutocadExtras();
}
/**
* @param {string} name
* @param {string} description
* @param {array} elements - if elem > 0 it is a line, if elem < 0 it is gap, if elem == 0.0 it is a
*/
addLineType(name, description, elements) {
this.lineTypes[name] = new LineType(name, description, elements);
return this;
}
addLayer(name, colorNumber, lineTypeName) {
this.layers[name] = new Layer(name, colorNumber, lineTypeName);
return this;
}
setActiveLayer(name) {
this.activeLayer = this.layers[name];
return this;
}
addTable(name) {
const table = new Table(name);
this.tables[name] = table;
return table;
}
/**
*
* @param {string} name The name of the block.
* @returns {Block}
*/
addBlock(name) {
const block = new Block(name);
this.blocks[name] = block;
return block;
}
drawLine(x1, y1, x2, y2) {
this.activeLayer.addShape(new Line(x1, y1, x2, y2));
return this;
}
drawLine3d(x1, y1, z1, x2, y2, z2) {
this.activeLayer.addShape(new Line3d(x1, y1, z1, x2, y2, z2));
return this;
}
drawPoint(x, y) {
this.activeLayer.addShape(new Point(x, y));
return this;
}
drawRect(x1, y1, x2, y2, cornerLength, cornerBulge) {
const w = x2 - x1;
const h = y2 - y1;
cornerBulge = cornerBulge || 0;
let p = null;
if (!cornerLength) {
p = new Polyline(
[
[x1, y1],
[x1, y1 + h],
[x1 + w, y1 + h],
[x1 + w, y1],
],
true
);
} else {
p = new Polyline(
[
[x1 + w - cornerLength, y1, cornerBulge], // 1
[x1 + w, y1 + cornerLength], // 2
[x1 + w, y1 + h - cornerLength, cornerBulge], // 3
[x1 + w - cornerLength, y1 + h], // 4
[x1 + cornerLength, y1 + h, cornerBulge], // 5
[x1, y1 + h - cornerLength], // 6
[x1, y1 + cornerLength, cornerBulge], // 7
[x1 + cornerLength, y1], // 8
],
true
);
}
this.activeLayer.addShape(p);
return this;
}
/**
* Draw a regular convex polygon as a polyline entity.
*
* @see [Regular polygon | Wikipedia](https://en.wikipedia.org/wiki/Regular_polygon)
*
* @param {number} x - The X coordinate of the center of the polygon.
* @param {number} y - The Y coordinate of the center of the polygon.
* @param {number} numberOfSides - The number of sides.
* @param {number} radius - The radius.
* @param {number} rotation - The rotation angle (in Degrees) of the polygon. By default 0.
* @param {boolean} circumscribed - If `true` is a polygon in which each side is a tangent to a circle.
* If `false` is a polygon in which all vertices lie on a circle. By default `false`.
*
* @returns {Drawing} - The current object of {@link Drawing}.
*/
drawPolygon(
x,
y,
numberOfSides,
radius,
rotation = 0,
circumscribed = false
) {
const angle = (2 * Math.PI) / numberOfSides;
const vertices = [];
let d = radius;
const rotationRad = (rotation * Math.PI) / 180;
if (circumscribed) d = radius / Math.cos(Math.PI / numberOfSides);
for (let i = 0; i < numberOfSides; i++) {
vertices.push([
x + d * Math.sin(rotationRad + i * angle),
y + d * Math.cos(rotationRad + i * angle),
]);
}
this.activeLayer.addShape(new Polyline(vertices, true));
return this;
}
/**
* @param {number} x1 - Center x
* @param {number} y1 - Center y
* @param {number} r - radius
* @param {number} startAngle - degree
* @param {number} endAngle - degree
*/
drawArc(x1, y1, r, startAngle, endAngle) {
this.activeLayer.addShape(new Arc(x1, y1, r, startAngle, endAngle));
return this;
}
/**
* @param {number} x1 - Center x
* @param {number} y1 - Center y
* @param {number} r - radius
*/
drawCircle(x1, y1, r) {
this.activeLayer.addShape(new Circle(x1, y1, r));
return this;
}
/**
* @param {number} x1 - Center x
* @param {number} y1 - Center y
* @param {number} z1 - Center z
* @param {number} r - radius
* @param {number} thickness - thickness
* @param {number} extrusionDirectionX - Extrusion Direction x
* @param {number} extrusionDirectionY - Extrusion Direction y
* @param {number} extrusionDirectionZ - Extrusion Direction z
*/
drawCylinder(
x1,
y1,
z1,
r,
thickness,
extrusionDirectionX,
extrusionDirectionY,
extrusionDirectionZ
) {
this.activeLayer.addShape(
new Cylinder(
x1,
y1,
z1,
r,
thickness,
extrusionDirectionX,
extrusionDirectionY,
extrusionDirectionZ
)
);
return this;
}
/**
* @param {number} x1 - x
* @param {number} y1 - y
* @param {number} height - Text height
* @param {number} rotation - Text rotation
* @param {string} value - the string itself
* @param {string} [horizontalAlignment="left"] left | center | right
* @param {string} [verticalAlignment="baseline"] baseline | bottom | middle | top
*/
drawText(
x1,
y1,
height,
rotation,
value,
horizontalAlignment = "left",
verticalAlignment = "baseline"
) {
this.activeLayer.addShape(
new Text(
x1,
y1,
height,
rotation,
value,
horizontalAlignment,
verticalAlignment
)
);
return this;
}
/**
* @param {[number, number][]} points - Array of points like [ [x1, y1], [x2, y2]... ]
* @param {boolean} closed - Closed polyline flag
* @param {number} startWidth - Default start width
* @param {number} endWidth - Default end width
*/
drawPolyline(points, closed = false, startWidth = 0, endWidth = 0) {
this.activeLayer.addShape(
new Polyline(points, closed, startWidth, endWidth)
);
return this;
}
/**
* @param {[number, number, number][]} points - Array of points like [ [x1, y1, z1], [x2, y2, z1]... ]
*/
drawPolyline3d(points) {
points.forEach((point) => {
if (point.length !== 3) {
throw "Require 3D coordinates";
}
});
this.activeLayer.addShape(new Polyline3d(points));
return this;
}
/**
*
* @param {number} trueColor - Integer representing the true color, can be passed as an hexadecimal value of the form 0xRRGGBB
*/
setTrueColor(trueColor) {
this.activeLayer.setTrueColor(trueColor);
return this;
}
/**
* Draw a spline.
* @param {[Array]} controlPoints - Array of control points like [ [x1, y1], [x2, y2]... ]
* @param {number} degree - Degree of spline: 2 for quadratic, 3 for cubic. Default is 3
* @param {[number]} knots - Knot vector array. If null, will use a uniform knot vector. Default is null
* @param {[number]} weights - Control point weights. If provided, must be one weight for each control point. Default is null
* @param {[Array]} fitPoints - Array of fit points like [ [x1, y1], [x2, y2]... ]
*/
drawSpline(
controlPoints,
degree = 3,
knots = null,
weights = null,
fitPoints = []
) {
this.activeLayer.addShape(
new Spline(controlPoints, degree, knots, weights, fitPoints)
);
return this;
}
/**
* Draw an ellipse.
* @param {number} x1 - Center x
* @param {number} y1 - Center y
* @param {number} majorAxisX - Endpoint x of major axis, relative to center
* @param {number} majorAxisY - Endpoint y of major axis, relative to center
* @param {number} axisRatio - Ratio of minor axis to major axis
* @param {number} startAngle - Start angle
* @param {number} endAngle - End angle
*/
drawEllipse(
x1,
y1,
majorAxisX,
majorAxisY,
axisRatio,
startAngle = 0,
endAngle = 2 * Math.PI
) {
this.activeLayer.addShape(
new Ellipse(
x1,
y1,
majorAxisX,
majorAxisY,
axisRatio,
startAngle,
endAngle
)
);
return this;
}
/**
* @param {number} x1 - x
* @param {number} y1 - y
* @param {number} z1 - z
* @param {number} x2 - x
* @param {number} y2 - y
* @param {number} z2 - z
* @param {number} x3 - x
* @param {number} y3 - y
* @param {number} z3 - z
* @param {number} x4 - x
* @param {number} y4 - y
* @param {number} z4 - z
*/
drawFace(x1, y1, z1, x2, y2, z2, x3, y3, z3, x4, y4, z4) {
this.activeLayer.addShape(
new Face(x1, y1, z1, x2, y2, z2, x3, y3, z3, x4, y4, z4)
);
return this;
}
_ltypeTable() {
const t = new Table("LTYPE");
const ltypes = Object.values(this.lineTypes);
for (const lt of ltypes) t.add(lt);
return t;
}
_layerTable(manager) {
const t = new Table("LAYER");
const layers = Object.values(this.layers);
for (const l of layers) t.add(l);
return t;
}
/**
* @see https://www.autodesk.com/techpubs/autocad/acadr14/dxf/header_section_al_u05_c.htm
* @see https://www.autodesk.com/techpubs/autocad/acad2000/dxf/header_section_group_codes_dxf_02.htm
*
* @param {string} variable
* @param {array} values Array of "two elements arrays". [ [value1_GroupCode, value1_value], [value2_GroupCode, value2_value] ]
*/
header(variable, values) {
this.headers[variable] = values;
return this;
}
/**
*
* @param {string} unit see Drawing.UNITS
*/
setUnits(unit) {
let value =
typeof Drawing.UNITS[unit] != "undefined"
? Drawing.UNITS[unit]
: Drawing.UNITS["Unitless"];
this.header("INSUNITS", [[70, Drawing.UNITS[unit]]]);
return this;
}
/** Generate additional DXF metadata which are required to successfully open resulted document
* in AutoDesk products. Call this method before serializing the drawing to get the most
* compatible result.
*/
generateAutocadExtras() {
if (!this.headers["ACADVER"]) {
/* AutoCAD 2007 version. */
this.header("ACADVER", [[1, "AC1021"]]);
}
if (!this.lineTypes["ByBlock"]) {
this.addLineType("ByBlock", "", []);
}
if (!this.lineTypes["ByLayer"]) {
this.addLineType("ByLayer", "", []);
}
let vpTable = this.tables["VPORT"];
if (!vpTable) {
vpTable = this.addTable("VPORT");
}
let styleTable = this.tables["STYLE"];
if (!styleTable) {
styleTable = this.addTable("STYLE");
}
if (!this.tables["VIEW"]) {
this.addTable("VIEW");
}
if (!this.tables["UCS"]) {
this.addTable("UCS");
}
let appIdTable = this.tables["APPID"];
if (!appIdTable) {
appIdTable = this.addTable("APPID");
}
if (!this.tables["DIMSTYLE"]) {
const t = new DimStyleTable("DIMSTYLE");
this.tables["DIMSTYLE"] = t;
}
vpTable.add(new Viewport("*ACTIVE", 1000));
/* Non-default text alignment is not applied without this entry. */
styleTable.add(new TextStyle("standard"));
appIdTable.add(new AppId("ACAD"));
this.modelSpace = this.addBlock("*Model_Space");
this.addBlock("*Paper_Space");
const d = new Dictionary();
this.dictionary.addChildDictionary("ACAD_GROUP", d);
}
_tagsManager() {
const manager = new TagsManager();
// Setup
const blockRecordTable = new Table("BLOCK_RECORD");
const blocks = Object.values(this.blocks);
for (const b of blocks) {
const r = new BlockRecord(b.name);
blockRecordTable.add(r);
}
const ltypeTable = this._ltypeTable();
const layerTable = this._layerTable();
// Header section start.
manager.start("HEADER");
manager.addHeaderVariable("HANDSEED", [[5, Handle.peek()]]);
const variables = Object.entries(this.headers);
for (const v of variables) {
const [name, values] = v;
manager.addHeaderVariable(name, values);
}
manager.end();
// Header section end.
// Classes section start.
manager.start("CLASSES");
// Empty CLASSES section for compatibility
manager.end();
// Classes section end.
// Tables section start.
manager.start("TABLES");
ltypeTable.tags(manager);
layerTable.tags(manager);
const tables = Object.values(this.tables);
for (const t of tables) {
t.tags(manager);
}
blockRecordTable.tags(manager);
manager.end();
// Tables section end.
// Blocks section start.
manager.start("BLOCKS");
for (const b of blocks) {
b.tags(manager);
}
manager.end();
// Blocks section end.
// Entities section start.
manager.start("ENTITIES");
const layers = Object.values(this.layers);
for (const l of layers) {
l.shapesTags(this.modelSpace, manager);
}
manager.end();
// Entities section end.
// Objects section start.
manager.start("OBJECTS");
this.dictionary.tags(manager);
manager.end();
// Objects section end.
manager.push(0, "EOF");
return manager;
}
toDxfString() {
return this._tagsManager().toDxfString();
}
}
//AutoCAD Color Index (ACI)
//http://sub-atomic.com/~moses/acadcolors.html
Drawing.ACI = {
LAYER: 0,
RED: 1,
YELLOW: 2,
GREEN: 3,
CYAN: 4,
BLUE: 5,
MAGENTA: 6,
WHITE: 7,
};
Drawing.LINE_TYPES = [
{ name: "CONTINUOUS", description: "______", elements: [] },
{ name: "DASHED", description: "_ _ _ ", elements: [5.0, -5.0] },
{ name: "DOTTED", description: ". . . ", elements: [0.0, -5.0] },
];
Drawing.LAYERS = [
{ name: "0", colorNumber: Drawing.ACI.WHITE, lineTypeName: "CONTINUOUS" },
];
//https://www.autodesk.com/techpubs/autocad/acad2000/dxf/header_section_group_codes_dxf_02.htm
Drawing.UNITS = {
Unitless: 0,
Inches: 1,
Feet: 2,
Miles: 3,
Millimeters: 4,
Centimeters: 5,
Meters: 6,
Kilometers: 7,
Microinches: 8,
Mils: 9,
Yards: 10,
Angstroms: 11,
Nanometers: 12,
Microns: 13,
Decimeters: 14,
Decameters: 15,
Hectometers: 16,
Gigameters: 17,
"Astronomical units": 18,
"Light years": 19,
Parsecs: 20,
};
module.exports = Drawing;