UNPKG

dxf-viewer

Version:
1,355 lines (1,222 loc) 104 kB
import { DynamicBuffer, NativeType } from "./DynamicBuffer.js" import { BatchingKey } from "./BatchingKey.js" import { Matrix3, Vector2 } from "three" import { TextRenderer, ParseSpecialChars, HAlign, VAlign } from "./TextRenderer.js" import { RBTree } from "./RBTree.js" import { MTextFormatParser } from "./MTextFormatParser.js" import dimStyleCodes from "./parser/DimStyleCodes.js" import { LinearDimension } from "./LinearDimension.js" import { HatchCalculator, HatchStyle } from "./HatchCalculator.js" import { LookupPattern, Pattern } from "./Pattern.js" import "./patterns/index.js" import earcut from "earcut" /** Use 16-bit indices for indexed geometry. */ const INDEXED_CHUNK_SIZE = 0x10000 /** Arc angle for tessellating point circle shape. */ const POINT_CIRCLE_TESSELLATION_ANGLE = 15 * Math.PI / 180 const POINT_SHAPE_BLOCK_NAME = "__point_shape" /** Flatten a block if its total vertices count in all instances is less than this value. */ const BLOCK_FLATTENING_VERTICES_THRESHOLD = 1024 /** Number of subdivisions per spline point. */ const SPLINE_SUBDIVISION = 4 /** Limit hatch lines number to some reasonable value to mitigate hanging and out-of-memory issues * on bad files. */ const MAX_HATCH_LINES = 20000 /** Limit hatch segments number per line to some reasonable value to mitigate hanging and * out-of-memory issues on bad files. */ const MAX_HATCH_SEGMENTS = 20000 /** Default values for system variables. Entry may be either value or function to call for obtaining * a value, the function `this` argument is DxfScene. */ const DEFAULT_VARS = { /* https://knowledge.autodesk.com/support/autocad/learn-explore/caas/CloudHelp/cloudhelp/2016/ENU/AutoCAD-Core/files/GUID-A17A69D7-25EF-4F57-B4EB-D53A56AB909C-htm.html */ DIMTXT: function() { //XXX should select value for imperial or metric units return 2.5 //XXX 0.18 for imperial }, DIMASZ: 2.5,//XXX 0.18 for imperial DIMCLRD: 0, DIMCLRE: 0, DIMCLRT: 0, DIMDEC: 2, //XXX 4 for imperial, DIMDLE: 0, DIMDSEP: ".".charCodeAt(0), //XXX "," for imperial, DIMEXE: 1.25, //XXX 0.18 for imperial DIMEXO: 0.625, // XXX 0.0625 for imperial DIMFXL: 1, DIMFXLON: false, DIMGAP: 0.625,//XXX for imperial DIMLFAC: 1, DIMRND: 0, DIMSAH: 0, DIMSCALE: 1, DIMSD1: 0, DIMSD2: 0, DIMSE1: 0, DIMSE2: 0, DIMSOXD: false, DIMTSZ: 0, DIMZIN: 8, //XXX 0 for imperial, } /** This class prepares an internal representation of a DXF file, optimized fo WebGL rendering. It * is decoupled in such a way so that it should be possible to build it in a web-worker, effectively * transfer it to the main thread, and easily apply it to a Three.js scene there. */ export class DxfScene { constructor(options) { this.options = Object.create(DxfScene.DefaultOptions) if (options) { Object.assign(this.options, options.sceneOptions) } /* Scene origin. All input coordinates are made local to this point to minimize precision * loss. */ this.origin = null /* RBTree<BatchingKey, RenderBatch> */ this.batches = new RBTree((b1, b2) => b1.key.Compare(b2.key)) /* Indexed by layer name, value is layer object from parsed DXF. */ this.layers = new Map() /* Indexed by block name, value is Block. */ this.blocks = new Map() /** Indexed by dimension style name, value is DIMSTYLE object from parsed DXF. */ this.dimStyles = new Map() /** Indexed by variable name (without leading '$'). */ this.vars = new Map() this.fontStyles = new Map() /* Indexed by entity handle. */ this.inserts = new Map() this.bounds = null this.pointShapeBlock = null this.numBlocksFlattened = 0 this.numEntitiesFiltered = 0 } /** Build the scene from the provided parsed DXF. * @param dxf {{}} Parsed DXF file. * @param fontFetchers {?Function[]} List of font fetchers. Fetcher should return promise with * loaded font object (opentype.js). They are invoked only when necessary. Each glyph is being * searched sequentially in each provided font. */ async Build(dxf, fontFetchers) { const header = dxf.header || {} for (const [name, value] of Object.entries(header)) { if (name.startsWith("$")) { this.vars.set(name.slice(1), value) } } /* Zero angle direction, 0 is +X. */ this.angBase = this.vars.get("ANGBASE") ?? 0 /* 0 - CCW, 1 - CW */ this.angDir = this.vars.get("ANGDIR") ?? 0 this.pdMode = this.vars.get("PDMODE") ?? 0 this.pdSize = this.vars.get("PDSIZE") ?? 0 this.isMetric = (this.vars.get("MEASUREMENT") ?? 1) == 1 if(dxf.tables && dxf.tables.layer) { for (const [, layer] of Object.entries(dxf.tables.layer.layers)) { layer.displayName = ParseSpecialChars(layer.name) this.layers.set(layer.name, layer) } } if(dxf.tables && dxf.tables.dimstyle) { for (const [, style] of Object.entries(dxf.tables.dimstyle.dimStyles)) { this.dimStyles.set(style.name, style) } } if (dxf.tables && dxf.tables.style) { for (const [, style] of Object.entries(dxf.tables.style.styles)) { this.fontStyles.set(style.styleName, style); } } if (dxf.blocks) { for (const [, block] of Object.entries(dxf.blocks)) { this.blocks.set(block.name, new Block(block)) } } this.textRenderer = new TextRenderer(fontFetchers, this.options.textOptions) this.hasMissingChars = false await this._FetchFonts(dxf) /* Scan all entities to analyze block usage statistics. */ for (const entity of dxf.entities) { if (!this._FilterEntity(entity)) { continue } if (entity.type === "INSERT") { this.inserts.set(entity.handle, entity) const block = this.blocks.get(entity.name) block?.RegisterInsert(entity) } else if (entity.type == "DIMENSION") { if ((entity.block ?? null) !== null) { const block = this.blocks.get(entity.block) block?.RegisterInsert(entity) } } } for (const block of this.blocks.values()) { if (block.data.hasOwnProperty("entities")) { const blockCtx = block.DefinitionContext() for (const entity of block.data.entities) { if (!this._FilterEntity(entity)) { continue } this._ProcessDxfEntity(entity, blockCtx) } } if (block.SetFlatten()) { this.numBlocksFlattened++ } } console.log(`${this.numBlocksFlattened} blocks flattened`) for (const entity of dxf.entities) { if (!this._FilterEntity(entity)) { this.numEntitiesFiltered++ continue } this._ProcessDxfEntity(entity) } console.log(`${this.numEntitiesFiltered} entities filtered`) this.scene = this._BuildScene() delete this.batches delete this.layers delete this.blocks delete this.textRenderer } /** @return False to suppress the specified entity, true to permit rendering. */ _FilterEntity(entity) { if (entity.hidden) { return false } const layerName = this._GetEntityLayer(entity) if (layerName != "0") { const layer = this.layers.get(layerName) if (layer?.frozen) { return false } } return !this.options.suppressPaperSpace || !entity.inPaperSpace } async _FetchFonts(dxf) { function IsTextEntity(entity) { return entity.type === "TEXT" || entity.type === "MTEXT" || entity.type === "DIMENSION" || entity.type === "ATTDEF" || entity.type === "ATTRIB" } const ProcessEntity = async (entity) => { if (!this._FilterEntity(entity)) { return } let ret if (entity.type === "TEXT" || entity.type === "ATTRIB" || entity.type === "ATTDEF") { ret = await this.textRenderer.FetchFonts(ParseSpecialChars(entity.text)) } else if (entity.type === "MTEXT") { const parser = new MTextFormatParser() parser.Parse(entity.text) ret = true //XXX formatted MTEXT may specify some fonts explicitly, this is not yet supported for (const text of parser.GetText()) { if (!await this.textRenderer.FetchFonts(ParseSpecialChars(text))) { ret = false break } } } else if (entity.type === "DIMENSION") { ret = true const dim = this._CreateLinearDimension(entity) if (dim) { for (const text of dim.GetTexts()) { if (!await this.textRenderer.FetchFonts(text)) { ret = false break } } } } else { throw new Error("Bad entity type") } if (!ret) { this.hasMissingChars = true } return ret } for (const entity of dxf.entities) { if (IsTextEntity(entity)) { if (!await ProcessEntity(entity)) { /* Failing to resolve some character means that all fonts have been loaded and * checked. No mean to check the rest strings. However until it is encountered, * all strings should be checked, even if all fonts already loaded. This needed * to properly set hasMissingChars which allows displaying some warning in a * viewer. */ return } } } for (const block of this.blocks.values()) { if (block.data.hasOwnProperty("entities")) { for (const entity of block.data.entities) { if (IsTextEntity(entity)) { if (!await ProcessEntity(entity)) { return } } } } } } _ProcessDxfEntity(entity, blockCtx = null) { let renderEntities switch (entity.type) { case "LINE": renderEntities = this._DecomposeLine(entity, blockCtx) break case "POLYLINE": case "LWPOLYLINE": renderEntities = this._DecomposePolyline(entity, blockCtx) break case "ARC": renderEntities = this._DecomposeArc(entity, blockCtx) break case "CIRCLE": renderEntities = this._DecomposeCircle(entity, blockCtx) break case "ELLIPSE": renderEntities = this._DecomposeEllipse(entity, blockCtx) break case "POINT": renderEntities = this._DecomposePoint(entity, blockCtx) break case "SPLINE": renderEntities = this._DecomposeSpline(entity, blockCtx) break case "INSERT": /* Works with rendering batches without intermediate entities. */ this._ProcessInsert(entity, blockCtx) return case "TEXT": renderEntities = this._DecomposeText(entity, blockCtx) break case "MTEXT": renderEntities = this._DecomposeMText(entity, blockCtx) break case "3DFACE": renderEntities = this._Decompose3DFace(entity, blockCtx) break case "SOLID": renderEntities = this._DecomposeSolid(entity, blockCtx) break case "DIMENSION": renderEntities = this._DecomposeDimension(entity, blockCtx) break case "ATTRIB": renderEntities = this._DecomposeAttribute(entity, blockCtx) break case "HATCH": renderEntities = this._DecomposeHatch(entity, blockCtx) break default: console.log("Unhandled entity type: " + entity.type) return } for (const renderEntity of renderEntities) { this._ProcessEntity(renderEntity, blockCtx) } } /** * @param entity {Entity} * @param blockCtx {?BlockContext} */ _ProcessEntity(entity, blockCtx = null) { switch (entity.type) { case Entity.Type.POINTS: this._ProcessPoints(entity, blockCtx) break case Entity.Type.LINE_SEGMENTS: this._ProcessLineSegments(entity, blockCtx) break case Entity.Type.POLYLINE: this._ProcessPolyline(entity, blockCtx) break case Entity.Type.TRIANGLES: this._ProcessTriangles(entity, blockCtx) break default: throw new Error("Unhandled entity type: " + entity.type) } } /** * @param entity * @param vertex * @param blockCtx {?BlockContext} * @return {number} */ _GetLineType(entity, vertex = null, blockCtx = null) { //XXX lookup return 0 } /** Check if start/end with are not specified. */ _IsPlainLine(entity) { //XXX until shaped polylines rendering implemented return true // return !Boolean(entity.startWidth || entity.endWidth) } *_DecomposeLine(entity, blockCtx) { /* start/end width, bulge - seems cannot be present, at least with current parser */ if (entity.vertices.length !== 2) { return } const layer = this._GetEntityLayer(entity, blockCtx) const color = this._GetEntityColor(entity, blockCtx) yield new Entity({ type: Entity.Type.LINE_SEGMENTS, vertices: entity.vertices, layer, color, lineType: this._GetLineType(entity, entity.vertices[0]) }) } /** Generate vertices for bulged line segment. * * @param vertices Generated vertices pushed here. * @param startVtx Starting vertex. Assuming it is already present in the vertices array. * @param endVtx Ending vertex. * @param bulge Bulge value (see DXF specification). */ _GenerateBulgeVertices(vertices, startVtx, endVtx, bulge) { const a = 4 * Math.atan(bulge) const aAbs = Math.abs(a) if (aAbs < this.options.arcTessellationAngle) { vertices.push(new Vector2(endVtx.x, endVtx.y)) return } const ha = a / 2 const sha = Math.sin(ha) const cha = Math.cos(ha) const d = {x: endVtx.x - startVtx.x, y: endVtx.y - startVtx.y} const dSq = d.x * d.x + d.y * d.y if (dSq < Number.MIN_VALUE * 2) { /* No vertex is pushed since end vertex is duplicate of start vertex. */ return } const D = Math.sqrt(dSq) let R = D / 2 / sha d.x /= D d.y /= D const center = { x: (d.x * sha - d.y * cha) * R + startVtx.x, y: (d.x * cha + d.y * sha) * R + startVtx.y } let numSegments = Math.floor(aAbs / this.options.arcTessellationAngle) if (numSegments < this.options.minArcTessellationSubdivisions) { numSegments = this.options.minArcTessellationSubdivisions } if (numSegments > 1) { const startAngle = Math.atan2(startVtx.y - center.y, startVtx.x - center.x) const step = a / numSegments if (a < 0) { R = -R } for (let i = 1; i < numSegments; i++) { const a = startAngle + i * step const v = new Vector2( center.x + R * Math.cos(a), center.y + R * Math.sin(a) ) vertices.push(v) } } vertices.push(new Vector2(endVtx.x, endVtx.y)) } /** Generate vertices for arc segment. * * @param vertices Generated vertices pushed here. * @param {{x, y}} center Center vector. * @param {number} radius * @param {?number} startAngle Start angle in radians. Zero if not specified. Arc is drawn in * CCW direction from start angle towards end angle. * @param {?number} endAngle Optional end angle in radians. Full circle is drawn if not * specified. * @param {?number} tessellationAngle Arc tessellation angle in radians, default value is taken * from scene options. * @param {?number} yRadius Specify to get ellipse arc. `radius` parameter used as X radius. * @param {?Matrix3} transform Optional transform matrix for the arc. Applied as last operation. * @param {?number} rotation Optional rotation angle for generated arc. Mostly for ellipses. * @param {?boolean} cwAngleDir Angles counted in clockwise direction from X positive direction. * @return {Vector2[]} List of generated vertices. */ _GenerateArcVertices({vertices, center, radius, startAngle = null, endAngle = null, tessellationAngle = null, yRadius = null, transform = null, rotation = null, ccwAngleDir = true}) { if (!center || !radius) { return } if (!tessellationAngle) { tessellationAngle = this.options.arcTessellationAngle } if (yRadius === null) { yRadius = radius } /* Normalize angles - make them starting from +X in CCW direction. End angle should be * greater than start angle. */ if (startAngle === undefined || startAngle === null) { startAngle = 0 } else { startAngle += this.angBase } let isClosed = false if (endAngle === undefined || endAngle === null) { endAngle = startAngle + 2 * Math.PI isClosed = true } else { endAngle += this.angBase } //XXX this.angDir - not clear, seem in practice it does not alter arcs rendering. if (!ccwAngleDir) { const tmp = startAngle startAngle = -endAngle endAngle = -tmp } while (endAngle <= startAngle) { endAngle += Math.PI * 2 } const arcAngle = endAngle - startAngle let numSegments = Math.floor(arcAngle / tessellationAngle) if (numSegments === 0) { numSegments = 1 } const step = arcAngle / numSegments let rotationTransform = null if (rotation) { rotationTransform = new Matrix3().makeRotation(rotation) } for (let i = 0; i <= numSegments; i++) { if (i === numSegments && isClosed) { break } let a if (ccwAngleDir) { a = startAngle + i * step } else { a = startAngle + (numSegments - i) * step } const v = new Vector2(radius * Math.cos(a), yRadius * Math.sin(a)) if (rotationTransform) { v.applyMatrix3(rotationTransform) } v.add(center) if (transform) { v.applyMatrix3(transform) } vertices.push(v) } } *_DecomposeArc(entity, blockCtx) { const color = this._GetEntityColor(entity, blockCtx) const layer = this._GetEntityLayer(entity, blockCtx) const lineType = this._GetLineType(entity, null, blockCtx) const vertices = [] this._GenerateArcVertices({vertices, center: entity.center, radius: entity.radius, startAngle: entity.startAngle, endAngle: entity.endAngle, transform: this._GetEntityExtrusionTransform(entity)}) yield new Entity({ type: Entity.Type.POLYLINE, vertices, layer, color, lineType, shape: entity.endAngle === undefined }) } *_DecomposeCircle(entity, blockCtx) { const color = this._GetEntityColor(entity, blockCtx) const layer = this._GetEntityLayer(entity, blockCtx) const lineType = this._GetLineType(entity, null, blockCtx) const vertices = [] this._GenerateArcVertices({vertices, center: entity.center, radius: entity.radius, transform: this._GetEntityExtrusionTransform(entity)}) yield new Entity({ type: Entity.Type.POLYLINE, vertices, layer, color, lineType, shape: true }) } *_DecomposeEllipse(entity, blockCtx) { const color = this._GetEntityColor(entity, blockCtx) const layer = this._GetEntityLayer(entity, blockCtx) const lineType = this._GetLineType(entity, null, blockCtx) const vertices = [] const xR = Math.sqrt(entity.majorAxisEndPoint.x * entity.majorAxisEndPoint.x + entity.majorAxisEndPoint.y * entity.majorAxisEndPoint.y) const yR = xR * entity.axisRatio const rotation = Math.atan2(entity.majorAxisEndPoint.y, entity.majorAxisEndPoint.x) const startAngle = entity.startAngle ?? 0 let endAngle = entity.endAngle ?? startAngle + 2 * Math.PI while (endAngle <= startAngle) { endAngle += Math.PI * 2 } const isClosed = (entity.endAngle ?? null) === null || Math.abs(endAngle - startAngle - 2 * Math.PI) < 1e-6 this._GenerateArcVertices({vertices, center: entity.center, radius: xR, startAngle: entity.startAngle, endAngle: isClosed ? null : entity.endAngle, yRadius: yR, rotation, /* Assuming mirror transform if present, for ellipse it just * reverses angle direction. */ ccwAngleDir: !this._GetEntityExtrusionTransform(entity)}) yield new Entity({ type: Entity.Type.POLYLINE, vertices, layer, color, lineType, shape: isClosed }) } *_DecomposePoint(entity, blockCtx) { if (this.pdMode === PdMode.NONE) { /* Points not displayed. */ return } if (this.pdMode !== PdMode.DOT && this.pdSize <= 0) { /* Currently not supported. */ return } const color = this._GetEntityColor(entity, blockCtx) const layer = this._GetEntityLayer(entity, blockCtx) const markType = this.pdMode & PdMode.MARK_MASK const isShaped = (this.pdMode & PdMode.SHAPE_MASK) !== 0 if (isShaped) { /* Shaped mark should be instanced. */ const key = new BatchingKey(layer, POINT_SHAPE_BLOCK_NAME, BatchingKey.GeometryType.POINT_INSTANCE, color, 0) const batch = this._GetBatch(key) batch.PushVertex(this._TransformVertex(entity.position)) this._CreatePointShapeBlock() return } if (markType === PdMode.DOT) { yield new Entity({ type: Entity.Type.POINTS, vertices: [entity.position], layer, color, lineType: null }) return } const vertices = [] this._CreatePointMarker(vertices, markType, entity.position) yield new Entity({ type: Entity.Type.LINE_SEGMENTS, vertices, layer, color, lineType: null }) } *_DecomposeAttribute(entity, blockCtx) { if (!this.textRenderer.canRender) { return; } const insertEntity = this.inserts.get(entity.ownerHandle) const layer = this._GetEntityLayer(insertEntity ?? entity, blockCtx) const color = this._GetEntityColor(insertEntity ?? entity, blockCtx) //XXX lookup font style attributes yield* this.textRenderer.Render({ text: ParseSpecialChars(entity.text), fontSize: entity.textHeight * entity.scale, startPos: entity.startPoint, endPos: entity.endPoint, rotation: entity.rotation, hAlign: entity.horizontalJustification, vAlign: entity.verticalJustification, color, layer }) } /** Create line segments for point marker. * @param vertices * @param markType * @param position {?{x,y}} point center position, default is zero. */ _CreatePointMarker(vertices, markType, position = null) { const _this = this function PushVertex(offsetX, offsetY) { vertices.push({ x: (position?.x ?? 0) + offsetX * _this.pdSize * 0.5, y: (position?.y ?? 0) + offsetY * _this.pdSize * 0.5 }) } switch(markType) { case PdMode.PLUS: PushVertex(0, 1.5) PushVertex(0, -1.5) PushVertex(-1.5, 0) PushVertex(1.5, 0) break case PdMode.CROSS: PushVertex(-1, 1) PushVertex(1, -1) PushVertex(1, 1) PushVertex(-1, -1) break case PdMode.TICK: PushVertex(0, 1) PushVertex(0, 0) break default: console.warn("Unsupported point display type: " + markType) } } /** Create point shape block if not yet done. */ _CreatePointShapeBlock() { if (this.pointShapeBlock) { return } /* This mimics DXF block entity. */ this.pointShapeBlock = new Block({ name: POINT_SHAPE_BLOCK_NAME, position: { x: 0, y: 0} }) /* Fix block origin at zero. */ this.pointShapeBlock.offset = new Vector2(0, 0) const blockCtx = this.pointShapeBlock.DefinitionContext() const markType = this.pdMode & PdMode.MARK_MASK if (markType !== PdMode.DOT && markType !== PdMode.NONE) { const vertices = [] this._CreatePointMarker(vertices, markType) const entity = new Entity({ type: Entity.Type.LINE_SEGMENTS, vertices, color: ColorCode.BY_BLOCK }) this._ProcessEntity(entity, blockCtx) } if (this.pdMode & PdMode.SQUARE) { const r = this.pdSize * 0.5 const vertices = [ {x: -r, y: r}, {x: r, y: r}, {x: r, y: -r}, {x: -r, y: -r} ] const entity = new Entity({ type: Entity.Type.POLYLINE, vertices, color: ColorCode.BY_BLOCK, shape: true }) this._ProcessEntity(entity, blockCtx) } if (this.pdMode & PdMode.CIRCLE) { const vertices = [] this._GenerateArcVertices({vertices, center: {x: 0, y: 0}, radius: this.pdSize * 0.5, tessellationAngle: POINT_CIRCLE_TESSELLATION_ANGLE}) const entity = new Entity({ type: Entity.Type.POLYLINE, vertices, color: ColorCode.BY_BLOCK, shape: true }) this._ProcessEntity(entity, blockCtx) } } *_Decompose3DFace(entity, blockCtx) { yield *this._DecomposeFace(entity, entity.vertices, blockCtx, this.options.wireframeMesh) } *_DecomposeSolid(entity, blockCtx) { yield *this._DecomposeFace(entity, entity.points, blockCtx, false, this._GetEntityExtrusionTransform(entity)) } *_DecomposeFace(entity, vertices, blockCtx, wireframe, transform = null) { const layer = this._GetEntityLayer(entity, blockCtx) const color = this._GetEntityColor(entity, blockCtx) function IsValidTriangle(v1, v2, v3) { const e1 = new Vector2().subVectors(v2, v1) const e2 = new Vector2().subVectors(v3, v1) const area = Math.abs(e1.cross(e2)) return area > Number.EPSILON } const v0 = new Vector2(vertices[0].x, vertices[0].y) const v1 = new Vector2(vertices[1].x, vertices[1].y) const v2 = new Vector2(vertices[2].x, vertices[2].y) let v3 = null let hasFirstTriangle = IsValidTriangle(v0, v1, v2) let hasSecondTriangle = false if (vertices.length > 3) { /* Fourth vertex may be the same as one of the previous vertices, so additional triangle * for degeneration. */ v3 = new Vector2(vertices[3].x, vertices[3].y) hasSecondTriangle = IsValidTriangle(v1, v3, v2) if (transform) { v3.applyMatrix3(transform) } } if (transform) { v0.applyMatrix3(transform) v1.applyMatrix3(transform) v2.applyMatrix3(transform) } if (!hasFirstTriangle && !hasSecondTriangle) { return } if (wireframe) { const _vertices = [] if (hasFirstTriangle && !hasSecondTriangle) { _vertices.push(v0, v1, v2) } else if (!hasFirstTriangle && hasSecondTriangle) { _vertices.push(v1, v3, v2) } else { _vertices.push(v0, v1, v3, v2) } yield new Entity({ type: Entity.Type.POLYLINE, vertices: _vertices, layer, color, shape: true }) } else { const _vertices = [] const indices = [] if (hasFirstTriangle) { _vertices.push(v0, v1, v2) indices.push(0, 1, 2) } if (hasSecondTriangle) { if (!hasFirstTriangle) { _vertices.push(v1, v2) indices.push(0, 1, 2) } else { indices.push(1, 2, 3) } _vertices.push(v3) } yield new Entity({ type: Entity.Type.TRIANGLES, vertices: _vertices, indices, layer, color }) } } *_DecomposeText(entity, blockCtx) { if (!this.textRenderer.canRender) { return } const layer = this._GetEntityLayer(entity, blockCtx) const color = this._GetEntityColor(entity, blockCtx) const style = this._GetEntityTextStyle(entity) const fixedHeight = style?.fixedTextHeight === 0 ? null : style?.fixedTextHeight yield* this.textRenderer.Render({ text: ParseSpecialChars(entity.text), fontSize: entity.textHeight ?? (fixedHeight ?? 1), startPos: entity.startPoint, endPos: entity.endPoint, rotation: entity.rotation, hAlign: entity.halign, vAlign: entity.valign, widthFactor: entity.xScale, color, layer }) } *_DecomposeMText(entity, blockCtx) { if (!this.textRenderer.canRender) { return } const layer = this._GetEntityLayer(entity, blockCtx) const color = this._GetEntityColor(entity, blockCtx) const style = this._GetEntityTextStyle(entity) const fixedHeight = style?.fixedTextHeight === 0 ? null : style?.fixedTextHeight const parser = new MTextFormatParser() parser.Parse(ParseSpecialChars(entity.text)) yield* this.textRenderer.RenderMText({ formattedText: parser.GetContent(), // May still be overwritten by inline formatting codes fontSize: entity.height ?? fixedHeight, position: entity.position, rotation: entity.rotation, direction: entity.direction, attachment: entity.attachmentPoint, lineSpacing: entity.lineSpacing, width: entity.width, color, layer }) } /** * @return {?LinearDimension} Dimension handler instance, null if not possible to create from * the provided entity. */ _CreateLinearDimension(entity) { const type = (entity.dimensionType || 0) & 0xf /* For now support linear dimensions only. */ if ((type != 0 && type != 1) || !entity.linearOrAngularPoint1 || !entity.linearOrAngularPoint2 || !entity.anchorPoint) { return null } let style = null if (entity.hasOwnProperty("styleName")) { style = this.dimStyles.get(entity.styleName) } const dim = new LinearDimension({ p1: new Vector2().copy(entity.linearOrAngularPoint1), p2: new Vector2().copy(entity.linearOrAngularPoint2), anchor: new Vector2().copy(entity.anchorPoint), isAligned: type == 1, angle: entity.angle, text: entity.text, textAnchor: entity.middleOfText ? new Vector2().copy(entity.middleOfText) : null, textRotation: entity.textRotation /* styleResolver */ }, valueName => { return this._GetDimStyleValue(valueName, entity, style) /* textWidthCalculator */ }, (text, fontSize) => { return this.textRenderer.GetLineWidth(text, fontSize) }) if (!dim.IsValid) { console.warn("Invalid dimension geometry detected for " + entity.handle) return null } return dim } *_DecomposeDimension(entity, blockCtx) { if ((entity.block ?? null) !== null && this.blocks.has(entity.block)) { /* Dimension may have pre-rendered block attached. Then just render this block instead * of synthesizing dimension geometry from parameters. * * Create dummy INSERT entity. */ const insert = { name: entity.block, position: {x: 0, y: 0}, layer: entity.layer, color: entity.color, colorIndex: entity.colorIndex } this._ProcessInsert(insert, blockCtx) return } /* https://ezdxf.readthedocs.io/en/stable/tutorials/linear_dimension.html * https://ezdxf.readthedocs.io/en/stable/tables/dimstyle_table_entry.html */ const dim = this._CreateLinearDimension(entity) if (!dim) { return } const layer = this._GetEntityLayer(entity, blockCtx) const color = this._GetEntityColor(entity, blockCtx) const transform = this._GetEntityExtrusionTransform(entity) const layout = dim.GenerateLayout() for (const line of layout.lines) { const vertices = [] if (transform) { line.start.applyMatrix3(transform) line.end.applyMatrix3(transform) } vertices.push(line.start, line.end) yield new Entity({ type: Entity.Type.LINE_SEGMENTS, vertices, layer, color: line.color ?? color }) } for (const triangle of layout.triangles) { if (transform) { for (const v of triangle.vertices) { v.applyMatrix3(transform) } } yield new Entity({ type: Entity.Type.TRIANGLES, vertices: triangle.vertices, indices: triangle.indices, layer, color: triangle.color ?? color }) } if (this.textRenderer.canRender) { for (const text of layout.texts) { if (transform) { //XXX does not affect text rotation and mirroring text.position.applyMatrix3(transform) } yield* this.textRenderer.Render({ text: text.text, fontSize: text.size, startPos: text.position, rotation: text.angle, hAlign: HAlign.CENTER, vAlign: VAlign.MIDDLE, color: text.color ?? color, layer }) } } } /** * @param {Vector2[]} loop Loop vertices. Transformed in-place if transform specified. * @param {Matrix3 | null} transform * @param {number[] | null} result Resulting coordinates appended to this array. * @return {number[]} Each pair of numbers form vertex coordinate. This format is required for * `earcut` library. */ _TransformBoundaryLoop(loop, transform, result) { if (!result) { result = [] } for (const v of loop) { if (transform) { v.applyMatrix3(transform) } result.push(v.x) result.push(v.y) } return result } *_DecomposeHatch(entity, blockCtx) { const boundaryLoops = this._GetHatchBoundaryLoops(entity) if (boundaryLoops.length == 0) { console.warn("HATCH entity with empty boundary loops array " + "(perhaps some loop types are not implemented yet)") return } const style = entity.hatchStyle ?? 0 const layer = this._GetEntityLayer(entity, blockCtx) const color = this._GetEntityColor(entity, blockCtx) const transform = this._GetEntityExtrusionTransform(entity) let filteredBoundaryLoops = null /* Make external loop first, outermost the second, all the rest in arbitrary order. Now is * required only for solid infill. */ boundaryLoops.sort((a, b) => { if (a.isExternal != b.isExternal) { return a.isExternal ? -1 : 1 } if (a.isOutermost != b.isOutermost) { return a.isOutermost ? -1 : 1 } return 0 }) if (style == HatchStyle.THROUGH_ENTIRE_AREA) { /* Leave only external loop. */ filteredBoundaryLoops = [boundaryLoops[0].vertices] } else if (style == HatchStyle.OUTERMOST) { /* Leave external and outermost loop. */ filteredBoundaryLoops = [] for (const loop of boundaryLoops) { if (loop.isExternal || loop.isOutermost) { filteredBoundaryLoops.push(loop.vertices) } } if (filteredBoundaryLoops.length == 0) { filteredBoundaryLoops = null } } if (!filteredBoundaryLoops) { /* Fall-back to full list. */ filteredBoundaryLoops = boundaryLoops.map(loop => loop.vertices) } if (entity.isSolid) { const coords = this._TransformBoundaryLoop(filteredBoundaryLoops[0], transform) const holes = [] for (let i = 1; i < filteredBoundaryLoops.length; i++) { holes.push(coords.length / 2) this._TransformBoundaryLoop(filteredBoundaryLoops[i], transform, coords) } const indices = earcut(coords, holes) const vertices = [] for (const loop of filteredBoundaryLoops) { vertices.push(...loop) } yield new Entity({ type: Entity.Type.TRIANGLES, vertices, indices, layer, color }) return } const calc = new HatchCalculator(filteredBoundaryLoops, style) let pattern = null if (entity.definitionLines) { pattern = new Pattern(entity.definitionLines, entity.patternName, false) } /* QCAD always embed ANSI31-like pattern definition. Try to detect it, and let named * pattern override the provided definition. */ if ((pattern == null || pattern.isQcadDefault) && entity.patternName) { const _pattern = LookupPattern(entity.patternName, this.isMetric) if (!_pattern) { console.log(`Hatch pattern with name ${entity.patternName} not found ` + `(metric: ${this.isMetric})`) } else { pattern = _pattern } } if (pattern == null) { pattern = LookupPattern("ANSI31") } if (!pattern) { return } const seedPoints = entity.seedPoints ? entity.seedPoints : [{x: 0, y: 0}] for (const seedPoint of seedPoints) { /* Seems pattern transform is not applied at all if using lines definition embedded into * HATCH entity (according to observation of AutoDesk viewer behavior). */ const patTransform = pattern.offsetInLineSpace ? calc.GetPatternTransform({ seedPoint, angle: entity.patternAngle, scale: entity.patternScale }) : new Matrix3() for (const line of pattern.lines) { let offsetX let offsetY if (pattern.offsetInLineSpace) { offsetX = line.offset.x offsetY = line.offset.y } else { const sin = Math.sin(-(line.angle ?? 0)) const cos = Math.cos(-(line.angle ?? 0)) offsetX = line.offset.x * cos - line.offset.y * sin offsetY = line.offset.x * sin + line.offset.y * cos } /* Normalize offset so that Y is always non-negative. Inverting offset vector * direction does not change lines positions. */ if (offsetY < 0) { offsetY = -offsetY offsetX = -offsetX } const lineTransform = calc.GetLineTransform({ patTransform, basePoint: line.base, angle: line.angle ?? 0 }) const bbox = calc.GetBoundingBox(lineTransform) const margin = (bbox.max.x - bbox.min.x) * 0.05 /* First determine range of line indices. Line with index 0 goes through base point * (which is [0; 0] in line coordinates system). Line with index `n`` starts in `n` * offset vectors added to the base point. */ let minLineIdx, maxLineIdx if (offsetY == 0) { /* Degenerated to single line. */ minLineIdx = 0 maxLineIdx = 0 } else { minLineIdx = Math.ceil(bbox.min.y / offsetY) maxLineIdx = Math.floor(bbox.max.y / offsetY) } if (maxLineIdx - minLineIdx > MAX_HATCH_LINES) { console.warn("Too many lines produced by hatching pattern") continue } let dashPatLength if (line.dashes && line.dashes.length > 1) { dashPatLength = 0 for (const dash of line.dashes) { if (dash < 0) { dashPatLength -= dash } else { dashPatLength += dash } } } else { dashPatLength = null } const ocsTransform = lineTransform.clone().invert() for (let lineIdx = minLineIdx; lineIdx <= maxLineIdx; lineIdx++) { const y = lineIdx * offsetY const xBase = lineIdx * offsetX const xStart = bbox.min.x - margin const xEnd = bbox.max.x + margin const lineLength = xEnd - xStart const start = new Vector2(xStart, y).applyMatrix3(ocsTransform) const end = new Vector2(xEnd, y).applyMatrix3(ocsTransform) const lineVec = end.clone().sub(start) const clippedSegments = calc.ClipLine([start, end]) function GetParam(x) { return (x - xStart) / lineLength } function RenderSegment(seg) { const p1 = lineVec.clone().multiplyScalar(seg[0]).add(start) const p2 = lineVec.clone().multiplyScalar(seg[1]).add(start) if (transform) { p1.applyMatrix3(transform) p2.applyMatrix3(transform) } if (seg[1] - seg[0] <= Number.EPSILON) { return new Entity({ type: Entity.Type.POINTS, vertices: [p1], layer, color }) } return new Entity({ type: Entity.Type.LINE_SEGMENTS, vertices: [p1, p2], layer, color }) } /** Clip segment against `clippedSegments`. */ function *ClipSegment(segStart, segEnd) { for (const seg of clippedSegments) { if (seg[0] >= segEnd) { return } if (seg[1] <= segStart) { continue } const _start = Math.max(segStart, seg[0]) const _end = Math.min(segEnd, seg[1]) yield [_start, _end] segStart = _end } } /* Determine range for segment indices. One segment is one full sequence of * dashes. In case there is no dashes (solid line), just use hatch bounds. */ if (dashPatLength !== null) { let minSegIdx = Math.floor((xStart - xBase) / dashPatLength) let maxSegIdx = Math.floor((xEnd - xBase) / dashPatLength) if (maxSegIdx - minSegIdx >= MAX_HATCH_SEGMENTS) { console.warn("Too many segments produced by hatching pattern line") continue } for (let segIdx = minSegIdx; segIdx <= maxSegIdx; segIdx++) { let segStartParam = GetParam(xBase + segIdx * dashPatLength) for (let dashLength of line.dashes) { const isSpace = dashLength < 0 if (isSpace) { dashLength = -dashLength } const dashLengthParam = dashLength / lineLength if (!isSpace) { for (const seg of ClipSegment(segStartParam, segStartParam + dashLengthParam)) { yield RenderSegment(seg) } } segStartParam += dashLengthParam } } } else { /* Single solid line. */ for (const seg of clippedSegments) { yield RenderSegment(seg) } } } } } } /** * @typedef {Object} HatchBoundaryLoop * @property {Vector2[]} vertices List of points in OCS coordinates. * @property {Boolean} isExternal * @property {Boolean} isOutermost */ /** @return {HatchBoundaryLoop[]} Each loop is a list of points in OCS coordinates. */ _GetHatchBoundaryLoops(entity) { if (!entity.boundaryLoops) { return [] } const result = [] const AddPoints = (vertices, points) => { const n = points.length if (n == 0) { return } if (vertices.length == 0) { vertices.push(points[0]) } else { const lastPt