dxf-viewer
Version:
JavaScript DXF file viewer
1,355 lines (1,222 loc) • 104 kB
JavaScript
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