import { Readable } from 'stream'; import DxfArrayScanner, { IGroup } from './DxfArrayScanner'; import AUTO_CAD_COLOR_INDEX from './AutoCadColorIndex'; import Face from './entities/3dface'; import Arc from './entities/arc'; import AttDef from './entities/attdef'; import Circle from './entities/circle'; import Dimension from './entities/dimension'; import Ellipse from './entities/ellipse'; import Insert from './entities/insert'; import Line from './entities/line'; import LWPolyline from './entities/lwpolyline'; import MText from './entities/mtext'; import Point from './entities/point'; import Polyline from './entities/polyline'; import Solid from './entities/solid'; import Spline from './entities/spline'; import Text from './entities/text'; //import Vertex from './entities/'; import log from 'loglevel'; import IGeometry, { EntityName, IEntity, IPoint } from './entities/geomtry'; //log.setLevel('trace'); //log.setLevel('debug'); //log.setLevel('info'); //log.setLevel('warn'); log.setLevel('error'); //log.setLevel('silent'); export interface IBlock { entities: IEntity[]; type: number; ownerHandle: string; // entities: any[]; xrefPath: string; name: string; name2: string; handle: string; layer: string; position: IPoint; paperSpace: boolean; } export interface IViewPort { name: string; lowerLeftCorner: IPoint; upperRightCorner: IPoint; center: IPoint; snapBasePoint: IPoint; snapSpacing: IPoint; gridSpacing: IPoint; viewDirectionFromTarget: IPoint; viewTarget: IPoint; lensLength: number; frontClippingPlane: string | number | boolean; backClippingPlane: string | number | boolean; viewHeight: number; snapRotationAngle: number; viewTwistAngle: number; orthographicType: string; ucsOrigin: IPoint; ucsXAxis: IPoint; ucsYAxis: IPoint; renderMode: string; defaultLightingType: string; defaultLightingOn: string; ownerHandle: string; ambientColor: number; } export interface IViewPortTableDefinition { tableRecordsProperty: 'viewPorts'; tableName: 'viewPort'; dxfSymbolName: 'VPORT'; parseTableRecords(): IViewPort[]; } export interface ILineType { name: string; description: string; pattern: string[]; patternLength: number; } export interface ILineTypeTableDefinition { tableRecordsProperty: 'lineTypes'; tableName: 'lineType'; dxfSymbolName: 'LTYPE'; parseTableRecords(): Record; } export interface ILayer { name: string; visible: boolean; colorIndex: number; color: number; frozen: boolean; } export interface ILayerTableDefinition { tableRecordsProperty: 'layers'; tableName: 'layer'; dxfSymbolName: 'LAYER'; parseTableRecords(): Record; } export interface ITableDefinitions { VPORT: IViewPortTableDefinition; LTYPE: ILineTypeTableDefinition; LAYER: ILayerTableDefinition; } export interface IBaseTable { handle: string; ownerHandle: string; } export interface IViewPortTable extends IBaseTable { viewPorts: IViewPort[]; } export interface ILayerTypesTable extends IBaseTable { lineTypes: Record; } export interface ILayersTable extends IBaseTable { layers: Record; } export interface ITables { viewPort: IViewPortTable; lineType: ILayerTypesTable; layer: ILayersTable; } export type ITable = IViewPortTable | ILayerTypesTable | ILayersTable; export interface IDxf { header: Record; entities: IEntity[]; blocks: Record; tables: ITables; } function registerDefaultEntityHandlers(dxfParser: DxfParser) { // Supported entities here (some entity code is still being refactored into this flow) dxfParser.registerEntityHandler(Face); dxfParser.registerEntityHandler(Arc); dxfParser.registerEntityHandler(AttDef); dxfParser.registerEntityHandler(Circle); dxfParser.registerEntityHandler(Dimension); dxfParser.registerEntityHandler(Ellipse); dxfParser.registerEntityHandler(Insert); dxfParser.registerEntityHandler(Line); dxfParser.registerEntityHandler(LWPolyline); dxfParser.registerEntityHandler(MText); dxfParser.registerEntityHandler(Point); dxfParser.registerEntityHandler(Polyline); dxfParser.registerEntityHandler(Solid); dxfParser.registerEntityHandler(Spline); dxfParser.registerEntityHandler(Text); //dxfParser.registerEntityHandler(require('./entities/vertex')); } export default class DxfParser { private _entityHandlers = {} as Record; constructor() { registerDefaultEntityHandlers(this); } public parse(source: string) { if (typeof source === 'string') { return this._parse(source); } else { console.error('Cannot read dxf source of type `' + typeof (source)); return null; } } public registerEntityHandler(handlerType: new () => IGeometry) { const instance = new handlerType(); this._entityHandlers[instance.ForEntityName] = instance; } public parseSync(source: string) { return this.parse(source); } public parseStream(stream: Readable) { let dxfString = ""; const self = this; return new Promise((res, rej) => { stream.on('data', (chunk) => { dxfString += chunk; }); stream.on('end', () => { try { res(self._parse(dxfString)); } catch (err) { rej(err); } }); stream.on('error', (err) => { rej(err); }); }); } private _parse(dxfString: string) { const dxf = {} as IDxf; let lastHandle = 0; const dxfLinesArray = dxfString.split(/\r\n|\r|\n/g); const scanner = new DxfArrayScanner(dxfLinesArray); if (!scanner.hasNext()) throw Error('Empty file'); const self = this; let curr: IGroup; function parseAll() { curr = scanner.next(); while (!scanner.isEOF()) { if (curr.code === 0 && curr.value === 'SECTION') { curr = scanner.next(); // Be sure we are reading a section code if (curr.code !== 2) { console.error('Unexpected code %s after 0:SECTION', debugCode(curr)); curr = scanner.next(); continue; } if (curr.value === 'HEADER') { log.debug('> HEADER'); dxf.header = parseHeader(); log.debug('<'); } else if (curr.value === 'BLOCKS') { log.debug('> BLOCKS'); dxf.blocks = parseBlocks(); log.debug('<'); } else if (curr.value === 'ENTITIES') { log.debug('> ENTITIES'); dxf.entities = parseEntities(false); log.debug('<'); } else if (curr.value === 'TABLES') { log.debug('> TABLES'); dxf.tables = parseTables(); log.debug('<'); } else if (curr.value === 'EOF') { log.debug('EOF'); } else { log.warn('Skipping section \'%s\'', curr.value); } } else { curr = scanner.next(); } // If is a new section } } /** * * @return {object} header */ function parseHeader() { // interesting variables: // $ACADVER, $VIEWDIR, $VIEWSIZE, $VIEWCTR, $TDCREATE, $TDUPDATE // http://www.autodesk.com/techpubs/autocad/acadr14/dxf/header_section_al_u05_c.htm // Also see VPORT table entries let currVarName = null as null | string; let currVarValue = null as null | IPoint | number; const header = {} as Record; // loop through header variables curr = scanner.next(); while (true) { if (groupIs(curr, 0, 'ENDSEC')) { if (currVarName) header[currVarName as string] = currVarValue!; break; } else if (curr.code === 9) { if (currVarName) header[currVarName as string] = currVarValue!; currVarName = curr.value as string; // Filter here for particular variables we are interested in } else { if (curr.code === 10) { currVarValue = { x: curr.value as number } as IPoint; } else if (curr.code === 20) { (currVarValue as IPoint).y = curr.value as number; } else if (curr.code === 30) { (currVarValue as IPoint).z = curr.value as number; } else { currVarValue = curr.value as number; } } curr = scanner.next(); } // console.log(util.inspect(header, { colors: true, depth: null })); curr = scanner.next(); // swallow up ENDSEC return header; } /** * */ function parseBlocks() { const blocks = {} as Record; curr = scanner.next(); while (curr.value !== 'EOF') { if (groupIs(curr, 0, 'ENDSEC')) { break; } if (groupIs(curr, 0, 'BLOCK')) { log.debug('block {'); const block = parseBlock(); log.debug('}'); ensureHandle(block); if (!block.name) log.error('block with handle "' + block.handle + '" is missing a name.'); else blocks[block.name] = block; } else { logUnhandledGroup(curr); curr = scanner.next(); } } return blocks; } function parseBlock() { const block = {} as IBlock; curr = scanner.next(); while (curr.value !== 'EOF') { switch (curr.code) { case 1: block.xrefPath = curr.value as string; curr = scanner.next(); break; case 2: block.name = curr.value as string; curr = scanner.next(); break; case 3: block.name2 = curr.value as string; curr = scanner.next(); break; case 5: block.handle = curr.value as string; curr = scanner.next(); break; case 8: block.layer = curr.value as string; curr = scanner.next(); break; case 10: block.position = parsePoint(curr); curr = scanner.next(); break; case 67: block.paperSpace = (curr.value && curr.value == 1) ? true : false; curr = scanner.next(); break; case 70: if (curr.value != 0) { //if(curr.value & BLOCK_ANONYMOUS_FLAG) console.log(' Anonymous block'); //if(curr.value & BLOCK_NON_CONSTANT_FLAG) console.log(' Non-constant attributes'); //if(curr.value & BLOCK_XREF_FLAG) console.log(' Is xref'); //if(curr.value & BLOCK_XREF_OVERLAY_FLAG) console.log(' Is xref overlay'); //if(curr.value & BLOCK_EXTERNALLY_DEPENDENT_FLAG) console.log(' Is externally dependent'); //if(curr.value & BLOCK_RESOLVED_OR_DEPENDENT_FLAG) console.log(' Is resolved xref or dependent of an xref'); //if(curr.value & BLOCK_REFERENCED_XREF) console.log(' This definition is a referenced xref'); block.type = curr.value as number; } curr = scanner.next(); break; case 100: // ignore class markers curr = scanner.next(); break; case 330: block.ownerHandle = curr.value as string; curr = scanner.next(); break; case 0: if (curr.value == 'ENDBLK') break; block.entities = parseEntities(true); break; default: logUnhandledGroup(curr); curr = scanner.next(); } if (groupIs(curr, 0, 'ENDBLK')) { curr = scanner.next(); break; } } return block; } /** * parseTables * @return {Object} Object representing tables */ function parseTables() { const tables = {} as ITables; curr = scanner.next(); while (curr.value !== 'EOF') { if (groupIs(curr, 0, 'ENDSEC')) break; if (groupIs(curr, 0, 'TABLE')) { curr = scanner.next(); const tableDefinition = tableDefinitions[curr.value as keyof ITableDefinitions]; if (tableDefinition) { log.debug(curr.value + ' Table {'); tables[tableDefinitions[curr.value as keyof ITableDefinitions].tableName] = parseTable(curr); log.debug('}'); } else { log.debug('Unhandled Table ' + curr.value); } } else { // else ignored curr = scanner.next(); } } curr = scanner.next(); return tables; } const END_OF_TABLE_VALUE = 'ENDTAB'; function parseTable(group: IGroup) { const tableDefinition = tableDefinitions[group.value as keyof ITableDefinitions]; const table = {} as T; let expectedCount = 0; curr = scanner.next(); while (!groupIs(curr, 0, END_OF_TABLE_VALUE)) { switch (curr.code) { case 5: table.handle = curr.value as string; curr = scanner.next(); break; case 330: table.ownerHandle = curr.value as string; curr = scanner.next(); break; case 100: if (curr.value === 'AcDbSymbolTable') { // ignore curr = scanner.next(); } else { logUnhandledGroup(curr); curr = scanner.next(); } break; case 70: expectedCount = curr.value as number; curr = scanner.next(); break; case 0: if (curr.value === tableDefinition.dxfSymbolName) { table[tableDefinition.tableRecordsProperty] = tableDefinition.parseTableRecords(); } else { logUnhandledGroup(curr); curr = scanner.next(); } break; default: logUnhandledGroup(curr); curr = scanner.next(); } } const tableRecords = table[tableDefinition.tableRecordsProperty]; if (tableRecords) { let actualCount = (() => { if (tableRecords.constructor === Array) { return tableRecords.length; } else if (typeof (tableRecords) === 'object') { return Object.keys(tableRecords).length; } return undefined; })(); if (expectedCount !== actualCount) log.warn('Parsed ' + actualCount + ' ' + tableDefinition.dxfSymbolName + '\'s but expected ' + expectedCount); } curr = scanner.next(); return table; } function parseViewPortRecords() { const viewPorts = [] as IViewPort[]; // Multiple table entries may have the same name indicating a multiple viewport configuration let viewPort = {} as IViewPort; log.debug('ViewPort {'); curr = scanner.next(); while (!groupIs(curr, 0, END_OF_TABLE_VALUE)) { switch (curr.code) { case 2: // layer name viewPort.name = curr.value as string; curr = scanner.next(); break; case 10: viewPort.lowerLeftCorner = parsePoint(curr); curr = scanner.next(); break; case 11: viewPort.upperRightCorner = parsePoint(curr); curr = scanner.next(); break; case 12: viewPort.center = parsePoint(curr); curr = scanner.next(); break; case 13: viewPort.snapBasePoint = parsePoint(curr); curr = scanner.next(); break; case 14: viewPort.snapSpacing = parsePoint(curr); curr = scanner.next(); break; case 15: viewPort.gridSpacing = parsePoint(curr); curr = scanner.next(); break; case 16: viewPort.viewDirectionFromTarget = parsePoint(curr); curr = scanner.next(); break; case 17: viewPort.viewTarget = parsePoint(curr); curr = scanner.next(); break; case 42: viewPort.lensLength = curr.value as number; curr = scanner.next(); break; case 43: viewPort.frontClippingPlane = curr.value; curr = scanner.next(); break; case 44: viewPort.backClippingPlane = curr.value; curr = scanner.next(); break; case 45: viewPort.viewHeight = curr.value as number; curr = scanner.next(); break; case 50: viewPort.snapRotationAngle = curr.value as number; curr = scanner.next(); break; case 51: viewPort.viewTwistAngle = curr.value as number; curr = scanner.next(); break; case 79: viewPort.orthographicType = curr.value as string; curr = scanner.next(); break; case 110: viewPort.ucsOrigin = parsePoint(curr); curr = scanner.next(); break; case 111: viewPort.ucsXAxis = parsePoint(curr); curr = scanner.next(); break; case 112: viewPort.ucsYAxis = parsePoint(curr); curr = scanner.next(); break; case 110: viewPort.ucsOrigin = parsePoint(curr); curr = scanner.next(); break; case 281: viewPort.renderMode = curr.value as string; curr = scanner.next(); break; case 281: // 0 is one distant light, 1 is two distant lights viewPort.defaultLightingType = curr.value as string; curr = scanner.next(); break; case 292: viewPort.defaultLightingOn = curr.value as string; curr = scanner.next(); break; case 330: viewPort.ownerHandle = curr.value as string; curr = scanner.next(); break; case 63: // These are all ambient color. Perhaps should be a gradient when multiple are set. case 421: case 431: viewPort.ambientColor = curr.value as number; curr = scanner.next(); break; case 0: // New ViewPort if (curr.value === 'VPORT') { log.debug('}'); viewPorts.push(viewPort); log.debug('ViewPort {'); viewPort = {} as IViewPort; curr = scanner.next(); } break; default: logUnhandledGroup(curr); curr = scanner.next(); break; } } // Note: do not call scanner.next() here, // parseTable() needs the current group log.debug('}'); viewPorts.push(viewPort); return viewPorts; } function parseLineTypes() { const ltypes = {} as Record; let ltype = {} as ILineType; let length = 0; let ltypeName: string; log.debug('LType {'); curr = scanner.next(); while (!groupIs(curr, 0, 'ENDTAB')) { switch (curr.code) { case 2: ltype.name = curr.value as string; ltypeName = curr.value as string; curr = scanner.next(); break; case 3: ltype.description = curr.value as string; curr = scanner.next(); break; case 73: // Number of elements for this line type (dots, dashes, spaces); length = curr.value as number; if (length > 0) ltype.pattern = []; curr = scanner.next(); break; case 40: // total pattern length ltype.patternLength = curr.value as number; curr = scanner.next(); break; case 49: ltype.pattern.push(curr.value as string); curr = scanner.next(); break; case 0: log.debug('}'); if (length > 0 && length !== ltype.pattern.length) log.warn('lengths do not match on LTYPE pattern'); ltypes[ltypeName!] = ltype; ltype = {} as ILineType; log.debug('LType {'); curr = scanner.next(); break; default: curr = scanner.next(); } } log.debug('}'); ltypes[ltypeName!] = ltype; return ltypes; } function parseLayers() { const layers = {} as Record; let layer = {} as ILayer; let layerName: string | undefined; log.debug('Layer {'); curr = scanner.next(); while (!groupIs(curr, 0, 'ENDTAB')) { switch (curr.code) { case 2: // layer name layer.name = curr.value as string; layerName = curr.value as string; curr = scanner.next(); break; case 62: // color, visibility layer.visible = curr.value >= 0; // TODO 0 and 256 are BYBLOCK and BYLAYER respectively. Need to handle these values for layers?. layer.colorIndex = Math.abs(curr.value as number); layer.color = getAcadColor(layer.colorIndex as number); curr = scanner.next(); break; case 70: // frozen layer layer.frozen = (((curr.value as number) & 1) != 0 || ((curr.value as number) & 2) != 0); curr = scanner.next(); break; case 0: // New Layer if (curr.value === 'LAYER') { log.debug('}'); layers[layerName!] = layer; log.debug('Layer {'); layer = {} as ILayer; layerName = undefined; curr = scanner.next(); } break; default: logUnhandledGroup(curr); curr = scanner.next(); break; } } // Note: do not call scanner.next() here, // parseLayerTable() needs the current group log.debug('}'); layers[layerName!] = layer; return layers; } const tableDefinitions = { VPORT: { tableRecordsProperty: 'viewPorts', tableName: 'viewPort', dxfSymbolName: 'VPORT', parseTableRecords: parseViewPortRecords }, LTYPE: { tableRecordsProperty: 'lineTypes', tableName: 'lineType', dxfSymbolName: 'LTYPE', parseTableRecords: parseLineTypes }, LAYER: { tableRecordsProperty: 'layers', tableName: 'layer', dxfSymbolName: 'LAYER', parseTableRecords: parseLayers } } as ITableDefinitions; /** * Is called after the parser first reads the 0:ENTITIES group. The scanner * should be on the start of the first entity already. * @return {Array} the resulting entities */ function parseEntities(forBlock: boolean) { const entities = [] as IEntity[]; const endingOnValue = forBlock ? 'ENDBLK' : 'ENDSEC'; if (!forBlock) { curr = scanner.next(); } while (true) { if (curr.code === 0) { if (curr.value === endingOnValue) { break; } const handler = self._entityHandlers[curr.value as EntityName]; if (handler != null) { log.debug(curr.value + ' {'); const entity = handler.parseEntity(scanner, curr); curr = scanner.lastReadGroup!; log.debug('}'); ensureHandle(entity); entities.push(entity); } else { log.warn('Unhandled entity ' + curr.value); curr = scanner.next(); continue; } } else { // ignored lines from unsupported entity curr = scanner.next(); } } if (endingOnValue == 'ENDSEC') curr = scanner.next(); // swallow up ENDSEC, but not ENDBLK return entities; } /** * Parses a 2D or 3D point, returning it as an object with x, y, and * (sometimes) z property if it is 3D. It is assumed the current group * is x of the point being read in, and scanner.next() will return the * y. The parser will determine if there is a z point automatically. * @return {Object} The 2D or 3D point as an object with x, y[, z] */ function parsePoint(curr: IGroup) { const point = {} as IPoint; let code = curr.code; point.x = curr.value as number; code += 10; curr = scanner.next(); if (curr.code != code) throw new Error('Expected code for point value to be ' + code + ' but got ' + curr.code + '.'); point.y = curr.value as number; code += 10; curr = scanner.next(); if (curr.code != code) { scanner.rewind(); return point; } point.z = curr.value as number; return point; } function ensureHandle(entity: IEntity | IBlock) { if (!entity) throw new TypeError('entity cannot be undefined or null'); if (!entity.handle) entity.handle = lastHandle++; } parseAll(); return dxf; } } function groupIs(group: IGroup, code: number, value: string | number | boolean) { return group.code === code && group.value === value; } function logUnhandledGroup(curr: IGroup) { log.debug('unhandled group ' + debugCode(curr)); } function debugCode(curr: IGroup) { return curr.code + ':' + curr.value; } /** * Returns the truecolor value of the given AutoCad color index value * @return {Number} truecolor value as a number */ function getAcadColor(index: number) { return AUTO_CAD_COLOR_INDEX[index]; } // const BLOCK_ANONYMOUS_FLAG = 1; // const BLOCK_NON_CONSTANT_FLAG = 2; // const BLOCK_XREF_FLAG = 4; // const BLOCK_XREF_OVERLAY_FLAG = 8; // const BLOCK_EXTERNALLY_DEPENDENT_FLAG = 16; // const BLOCK_RESOLVED_OR_DEPENDENT_FLAG = 32; // const BLOCK_REFERENCED_XREF = 64; /* Notes */ // Code 6 of an entity indicates inheritance of properties (eg. color). // BYBLOCK means inherits from block // BYLAYER (default) mean inherits from layer