dxf-viewer
Version:
JavaScript DXF file viewer
1,040 lines (951 loc) • 34.2 kB
JavaScript
import {Entity} from "./DxfScene.js"
import {ShapePath} from "three/src/extras/core/ShapePath.js"
import {ShapeUtils} from "three/src/extras/ShapeUtils.js"
import {Matrix3, Vector2} from "three"
import {MTextFormatParser} from "./MTextFormatParser.js"
/** Regex for parsing special characters in text entities. */
const SPECIAL_CHARS_RE = /(?:%%([dpcou%]))|(?:\\U\+([0-9a-f]{4}))/gi
/**
* Parse special characters in text entities and convert them to corresponding unicode
* characters.
* https://knowledge.autodesk.com/support/autocad/learn-explore/caas/CloudHelp/cloudhelp/2019/ENU/AutoCAD-Core/files/GUID-518E1A9D-398C-4A8A-AC32-2D85590CDBE1-htm.html
* @param {string} text Raw string.
* @return {string} String with special characters replaced.
*/
export function ParseSpecialChars(text) {
return text.replaceAll(SPECIAL_CHARS_RE, (match, p1, p2) => {
if (p1 !== undefined) {
switch (p1.toLowerCase()) {
case "d":
return "\xb0"
case "p":
return "\xb1"
case "c":
return "\u2205"
case "o":
/* Toggles overscore mode on and off, not implemented. */
return ""
case "u":
/* Toggles underscore mode on and off, not implemented. */
return ""
case "%":
return "%"
}
} else if (p2 !== undefined) {
const code = parseInt(p2, 16)
if (isNaN(code)) {
return match
}
return String.fromCharCode(code)
}
return match
})
}
/**
* Helper class for rendering text.
* Currently it is just basic very simplified implementation for MVP. Further work should include:
* * Support DXF text styles and weight.
* * Bitmap fonts generation in texture atlas for more optimal rendering.
*/
export class TextRenderer {
/**
* @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.
* @param options {?{}} See TextRenderer.DefaultOptions.
*/
constructor(fontFetchers, options = null) {
this.fontFetchers = fontFetchers
this.fonts = []
this.options = Object.create(TextRenderer.DefaultOptions)
if (options) {
Object.assign(this.options, options)
}
/* Indexed by character, value is CharShape. */
this.shapes = new Map()
this.stubShapeLoaded = false
/* Shape to display if no glyph found in the specified fonts. May be null if fallback
* character can not be rendered as well.
*/
this.stubShape = null
}
/** Fetch necessary fonts to render the provided text. Should be called for each string which
* will be rendered later.
* @param text {string}
* @return {Boolean} True if all characters can be rendered, false if none of the provided fonts
* contains glyphs for some of the specified text characters.
*/
async FetchFonts(text) {
if (!this.stubShapeLoaded) {
this.stubShapeLoaded = true
for (const char of Array.from(this.options.fallbackChar)) {
if (await this.FetchFonts(char)) {
this.stubShape = this._CreateCharShape(char)
break
}
}
}
let charMissing = false
for (const char of text) {
if (char.codePointAt(0) < 0x20) {
/* Control character. */
continue
}
let found = false
for (const font of this.fonts) {
if (font.HasChar(char)) {
found = true
break
}
}
if (found) {
continue
}
if (!this.fontFetchers) {
return false
}
while (this.fontFetchers.length > 0) {
const fetcher = this.fontFetchers.shift()
const font = await this._FetchFont(fetcher)
this.fonts.push(font)
if (font.HasChar(char)) {
found = true
break
}
}
if (!found) {
charMissing = true
}
}
return !charMissing
}
get canRender() {
return this.fonts !== null && this.fonts.length > 0
}
/** Get width in model space units for a single line of text.
* @param text {string}
* @param fontSize {number}
*/
GetLineWidth(text, fontSize) {
const block = new TextBlock(fontSize)
for (const char of text) {
const shape = this._GetCharShape(char)
if (!shape) {
continue
}
block.PushChar(char, shape)
}
return block.GetCurrentPosition()
}
/**
* @param {string} text
* @param {{x,y}} startPos
* @param {?{x,y}} endPos TEXT group second alignment point.
* @param {?number} rotation Rotation attribute, deg.
* @param {?number} widthFactor Relative X scale factor (group 41)
* @param {?number} hAlign Horizontal text justification type code (group 72)
* @param {?number} vAlign Vertical text justification type code (group 73).
* @param {number} color
* @param {?string} layer
* @param {number} fontSize Font size.
* @return {Generator<Entity>} Rendering entities. Currently just indexed triangles for each
* glyph.
*/
*Render({text, startPos, endPos, rotation = 0, widthFactor = 1, hAlign = 0, vAlign = 0,
color, layer = null, fontSize}) {
const block = new TextBlock(fontSize)
for (const char of text) {
const shape = this._GetCharShape(char)
if (!shape) {
continue
}
block.PushChar(char, shape)
}
yield* block.Render(startPos, endPos, rotation, widthFactor, hAlign, vAlign, color, layer)
}
/**
* @param {MTextFormatEntity[]} formattedText Parsed formatted text.
* @param {{x, y}} position Insertion position.
* @param {?number} fontSize If not specified, then it still may be defined by inline
* formatting codes, otherwise 1 is used as fall-back value.
* @param {?Number} width Text block width, no wrapping if undefined.
* @param {?Number} rotation Text block rotation in degrees.
* @param {?{x, y}} direction Text block orientation defined as direction vector. Takes a
* precedence over rotation if both provided.
* @param {number} attachment Attachment point, one of MTextAttachment values.
* @param {?number} lineSpacing Line spacing ratio relative to default one (5/3 of font size).
* @param {number} color
* @param {?string} layer
* @return {Generator<Entity>} Rendering entities. Currently just indexed triangles for each
* glyph.
*/
*RenderMText({formattedText, position, fontSize, width = null, rotation = 0, direction = null,
attachment, lineSpacing = 1, color, layer = null}) {
if (!fontSize) {
fontSize = 1;
}
const box = new TextBox(fontSize, this._GetCharShape.bind(this))
box.FeedText(formattedText)
yield* box.Render(position, width, rotation, direction, attachment, lineSpacing, color,
layer)
}
/** @return {CharShape} Shape for the specified character.
* Each shape is indexed triangles mesh for font size 1. They should be further transformed as
* needed.
*/
_GetCharShape(char) {
let shape = this.shapes.get(char)
if (shape) {
return shape
}
shape = this._CreateCharShape(char)
this.shapes.set(char, shape)
return shape
}
_CreateCharShape(char) {
for (const font of this.fonts) {
const path = font.GetCharPath(char)
if (path) {
return new CharShape(font, path, this.options)
}
}
return this.stubShape
}
async _FetchFont(fontFetcher) {
return new Font(await fontFetcher())
}
}
TextRenderer.DefaultOptions = {
/** Number of segments for each curve in a glyph. Currently Three.js does not have more
* adequate angle-based or length-based tessellation option.
*/
curveSubdivision: 2,
/** Character to use when the specified fonts does not contain necessary glyph. Several ones can
* be specified, the first one available is used.
*/
fallbackChar: "\uFFFD?"
}
/** @typedef {Object} CharPath
* @property advance {number}
* @property path {?ShapePath}
* @property bounds {xMin: number, xMax: number, yMin: number, yMax: number}
*/
class CharShape {
/**
* @param font {Font}
* @param glyph {CharPath}
* @param options {{}} Renderer options.
*/
constructor(font, glyph, options) {
this.font = font
this.advance = glyph.advance
this.bounds = glyph.bounds
if (glyph.path) {
const shapes = glyph.path.toShapes(false)
this.vertices = []
this.indices = []
for (const shape of shapes) {
const shapePoints = shape.extractPoints(options.curveSubdivision)
/* Ensure proper vertices winding. */
if (!ShapeUtils.isClockWise(shapePoints.shape)) {
shapePoints.shape = shapePoints.shape.reverse()
for (const hole of shapePoints.holes) {
if (ShapeUtils.isClockWise(hole)) {
shapePoints.holes[h] = hole.reverse()
}
}
}
/* This call also removes duplicated end vertices. */
const indices = ShapeUtils.triangulateShape(shapePoints.shape, shapePoints.holes)
const _this = this
const baseIdx = this.vertices.length
function AddVertices(vertices) {
for (const v of vertices) {
_this.vertices.push(v)
}
}
AddVertices(shapePoints.shape)
for (const hole of shapePoints.holes) {
AddVertices(hole)
}
for (const tuple of indices) {
for (const idx of tuple) {
this.indices.push(baseIdx + idx)
}
}
}
} else {
this.vertices = null
}
}
/** Get vertices array transformed to the specified position and with the specified size.
* @param position {{x,y}}
* @param size {number}
* @return {Vector2[]}
*/
GetVertices(position, size) {
return this.vertices.map(v => v.clone().multiplyScalar(size).add(position))
}
}
class Font {
constructor(data) {
this.data = data
this.charMap = new Map()
for (const glyph of Object.values(data.glyphs.glyphs)) {
if (glyph.unicode === undefined) {
continue
}
this.charMap.set(String.fromCodePoint(glyph.unicode), glyph)
}
/* Scale to transform the paths to size 1. */
//XXX not really clear what is the resulting unit, check, review and comment it later
// (100px?)
this.scale = 100 / ((this.data.unitsPerEm || 2048) * 72)
}
/**
* @param char {string} Character code point as string.
* @return {Boolean} True if the font has glyphs for the specified character.
*/
HasChar(char) {
return this.charMap.has(char)
}
/**
* @param char {string} Character code point as string.
* @return {?CharPath} Path is scaled to size 1. Null if no glyphs for the specified characters.
*/
GetCharPath(char) {
const glyph = this.charMap.get(char)
if (!glyph) {
return null
}
const scale = this.scale
const path = new ShapePath()
for (const cmd of glyph.path.commands) {
switch (cmd.type) {
case 'M':
path.moveTo(cmd.x * scale, cmd.y * scale)
break
case 'L':
path.lineTo(cmd.x * scale, cmd.y * scale)
break
case 'Q':
path.quadraticCurveTo(cmd.x1 * scale, cmd.y1 * scale,
cmd.x * scale, cmd.y * scale)
break
case 'C':
path.bezierCurveTo(cmd.x1 * scale, cmd.y1 * scale,
cmd.x2 * scale, cmd.y2 * scale,
cmd.x * scale, cmd.y * scale)
break
}
}
return {advance: glyph.advanceWidth * scale, path,
bounds: {xMin: glyph.xMin * scale, xMax: glyph.xMax * scale,
yMin: glyph.yMin * scale, yMax: glyph.yMax * scale}}
}
/**
* @param c1 {string}
* @param c2 {string}
* @return {number}
*/
GetKerning(c1, c2) {
const i1 = this.data.charToGlyphIndex(c1)
if (i1 === 0) {
return 0
}
const i2 = this.data.charToGlyphIndex(c1)
if (i2 === 0) {
return 0
}
return this.data.getKerningValue(i1, i2) * this.scale
}
}
/** TEXT group attribute 72 values. */
export const HAlign = Object.freeze({
LEFT: 0,
CENTER: 1,
RIGHT: 2,
ALIGNED: 3,
MIDDLE: 4,
FIT: 5
})
/** TEXT group attribute 73 values. */
export const VAlign = Object.freeze({
BASELINE: 0,
BOTTOM: 1,
MIDDLE: 2,
TOP: 3
})
/** MTEXT group attribute 71 values. */
const MTextAttachment = Object.freeze({
TOP_LEFT: 1,
TOP_CENTER: 2,
TOP_RIGHT: 3,
MIDDLE_LEFT: 4,
MIDDLE_CENTER: 5,
MIDDLE_RIGHT: 6,
BOTTOM_LEFT: 7,
BOTTOM_CENTER: 8,
BOTTOM_RIGHT: 9
})
/** Encapsulates layout calculations for a multiline-line text block. */
class TextBox {
/**
* @param fontSize
* @param {Function<CharShape, String>} charShapeProvider
*/
constructor(fontSize, charShapeProvider) {
this.fontSize = fontSize
this.charShapeProvider = charShapeProvider
this.curParagraph = new TextBox.Paragraph(this)
this.paragraphs = [this.curParagraph]
this.spaceShape = charShapeProvider(" ")
}
/** Add some formatted text to the box.
* @param {MTextFormatEntity[]} formattedText Parsed formatted text.
*/
FeedText(formattedText) {
/* For now advanced formatting is not implemented so scopes are just flattened. */
function *FlattenItems(items) {
for (const item of items) {
if (item.type === MTextFormatParser.EntityType.SCOPE) {
yield *FlattenItems(item.content)
} else {
yield item
}
}
}
/* Null is default alignment which depends on attachment point. */
let curAlignment = null
for (const item of FlattenItems(formattedText)) {
switch(item.type) {
case MTextFormatParser.EntityType.TEXT:
for (const c of item.content) {
if (c === " ") {
this.curParagraph.FeedSpace()
} else {
this.curParagraph.FeedChar(c)
}
}
break
case MTextFormatParser.EntityType.PARAGRAPH:
this.curParagraph = new TextBox.Paragraph(this)
this.curParagraph.SetAlignment(curAlignment)
this.paragraphs.push(this.curParagraph)
break
case MTextFormatParser.EntityType.NON_BREAKING_SPACE:
this.curParagraph.FeedChar(" ")
break
case MTextFormatParser.EntityType.PARAGRAPH_ALIGNMENT:
let a = null
switch (item.alignment) {
case "l":
a = TextBox.Paragraph.Alignment.LEFT
break
case "c":
a = TextBox.Paragraph.Alignment.CENTER
break
case "r":
a = TextBox.Paragraph.Alignment.RIGHT
break
case "d":
a = TextBox.Paragraph.Alignment.JUSTIFY
break
case "j":
a = null
break
}
this.curParagraph.SetAlignment(a)
curAlignment = a
break
}
}
}
*Render(position, width, rotation, direction, attachment, lineSpacing, color, layer) {
for (const p of this.paragraphs) {
p.BuildLines(width)
}
if (width === null || width === 0) {
/* Find maximal paragraph width which will define overall box width. */
width = 0
for (const p of this.paragraphs) {
const pWidth = p.GetMaxLineWidth()
if (pWidth > width) {
width = pWidth
}
}
}
let defaultAlignment = TextBox.Paragraph.Alignment.LEFT
switch (attachment) {
case MTextAttachment.TOP_CENTER:
case MTextAttachment.MIDDLE_CENTER:
case MTextAttachment.BOTTOM_CENTER:
defaultAlignment = TextBox.Paragraph.Alignment.CENTER
break
case MTextAttachment.TOP_RIGHT:
case MTextAttachment.MIDDLE_RIGHT:
case MTextAttachment.BOTTOM_RIGHT:
defaultAlignment = TextBox.Paragraph.Alignment.RIGHT
break
}
for (const p of this.paragraphs) {
p.ApplyAlignment(width, defaultAlignment)
}
/* Box local coordinates have top-left corner origin, so Y values are negative. The
* specified attachment should be used to obtain attachment point offset relatively to box
* CS origin.
*/
if (direction !== null) {
/* Direction takes precedence over rotation if specified. */
rotation = Math.atan2(direction.y, direction.x) * 180 / Math.PI
}
const lineHeight = lineSpacing * 5 * this.fontSize / 3
let height = 0
for (const p of this.paragraphs) {
if (p.lines === null) {
/* Paragraph always occupies at least one line. */
height++
} else {
height += p.lines.length
}
}
height *= lineHeight
let origin = new Vector2()
switch (attachment) {
case MTextAttachment.TOP_LEFT:
break
case MTextAttachment.TOP_CENTER:
origin.x = width / 2
break
case MTextAttachment.TOP_RIGHT:
origin.x = width
break
case MTextAttachment.MIDDLE_LEFT:
origin.y = -height / 2
break
case MTextAttachment.MIDDLE_CENTER:
origin.x = width / 2
origin.y = -height / 2
break
case MTextAttachment.MIDDLE_RIGHT:
origin.x = width
origin.y = -height / 2
break
case MTextAttachment.BOTTOM_LEFT:
origin.y = -height
break
case MTextAttachment.BOTTOM_CENTER:
origin.x = width / 2
origin.y = -height
break
case MTextAttachment.BOTTOM_RIGHT:
origin.x = width
origin.y = -height
break
default:
throw new Error("Unhandled alignment")
}
/* Transform for each chunk insertion point. */
const transform = new Matrix3().translate(-origin.x, -origin.y)
.rotate(-rotation * Math.PI / 180).translate(position.x, position.y)
let y = -this.fontSize
for (const p of this.paragraphs) {
if (p.lines === null) {
y -= lineHeight
continue
}
for (const line of p.lines) {
for (let chunkIdx = line.startChunkIdx;
chunkIdx < line.startChunkIdx + line.numChunks;
chunkIdx++) {
const chunk = p.chunks[chunkIdx]
let x = chunk.position
/* First chunk of continuation line never prepended by whitespace. */
if (chunkIdx === 0 || chunkIdx !== line.startChunkIdx) {
x += chunk.GetSpacingWidth()
}
const v = new Vector2(x, y)
v.applyMatrix3(transform)
if (chunk.block) {
yield* chunk.block.Render(v, null, rotation, null,
HAlign.LEFT, VAlign.BASELINE,
color, layer)
}
}
y -= lineHeight
}
}
}
}
TextBox.Paragraph = class {
constructor(textBox) {
this.textBox = textBox
this.chunks = []
this.curChunk = null
this.alignment = null
this.lines = null
}
/** Feed character for current chunk. Spaces should be fed by FeedSpace() method. If space
* character is fed into this method, it is interpreted as non-breaking space.
*/
FeedChar(c) {
const shape = this.textBox.charShapeProvider(c)
if (shape === null) {
return
}
if (this.curChunk === null) {
this._AddChunk()
}
this.curChunk.PushChar(c, shape)
}
FeedSpace() {
if (this.curChunk === null || this.curChunk.lastChar !== null) {
this._AddChunk()
}
this.curChunk.PushSpace()
}
SetAlignment(alignment) {
this.alignment = alignment
}
/** Group chunks into lines.
*
* @param {?number} boxWidth Box width. Do not wrap lines if null (one line is created).
*/
BuildLines(boxWidth) {
if (this.curChunk === null) {
return
}
this.lines = []
let startChunkIdx = 0
let curChunkIdx = 0
let curWidth = 0
const CommitLine = () => {
this.lines.push(new TextBox.Paragraph.Line(this,
startChunkIdx,
curChunkIdx - startChunkIdx,
curWidth))
startChunkIdx = curChunkIdx
curWidth = 0
}
for (; curChunkIdx < this.chunks.length; curChunkIdx++) {
const chunk = this.chunks[curChunkIdx]
const chunkWidth = chunk.GetWidth(startChunkIdx === 0 || curChunkIdx !== startChunkIdx)
if (boxWidth !== null && boxWidth !== 0 && curWidth !== 0 &&
curWidth + chunkWidth > boxWidth) {
CommitLine()
}
chunk.position = curWidth
curWidth += chunkWidth
}
if (startChunkIdx !== curChunkIdx && curWidth !== 0) {
CommitLine()
}
}
GetMaxLineWidth() {
if (this.lines === null) {
return 0
}
let maxWidth = 0
for (const line of this.lines) {
if (line.width > maxWidth) {
maxWidth = line.width
}
}
return maxWidth
}
ApplyAlignment(boxWidth, defaultAlignment) {
if (this.lines) {
for (const line of this.lines) {
line.ApplyAlignment(boxWidth, defaultAlignment)
}
}
}
_AddChunk() {
this.curChunk = new TextBox.Paragraph.Chunk(this, this.textBox.fontSize, this.curChunk)
this.chunks.push(this.curChunk)
}
}
TextBox.Paragraph.Alignment = Object.freeze({
LEFT: 0,
CENTER: 1,
RIGHT: 2,
JUSTIFY: 3
})
TextBox.Paragraph.Chunk = class {
/**
* @param {TextBox.Paragraph} paragraph
* @param {number} fontSize
* @param {?TextBox.Paragraph.Chunk} prevChunk
*/
constructor(paragraph, fontSize, prevChunk) {
this.paragraph = paragraph
this.fontSize = fontSize
this.prevChunk = prevChunk
this.lastChar = null
this.lastShape = null
this.leadingSpaces = 0
this.spaceStartKerning = null
this.spaceEndKerning = null
this.block = null
this.position = null
}
PushSpace() {
if (this.block) {
throw new Error("Illegal operation")
}
this.leadingSpaces++
}
/**
* @param char {string}
* @param shape {CharShape}
*/
PushChar(char, shape) {
if (this.spaceStartKerning === null) {
if (this.leadingSpaces === 0) {
this.spaceStartKerning = 0
this.spaceEndKerning = 0
} else {
if (this.prevChunk && this.prevChunk.lastShape &&
this.prevChunk.fontSize === this.fontSize &&
this.prevChunk.lastShape.font === this.paragraph.textBox.spaceShape.font) {
this.spaceStartKerning =
this.prevChunk.lastShape.font.GetKerning(this.prevChunk.lastChar, " ")
} else {
this.spaceStartKerning = 0
}
if (shape.font === this.paragraph.textBox.spaceShape.font) {
this.spaceEndKerning = shape.font.GetKerning(" ", char)
} else {
this.spaceEndKerning = 0
}
}
}
if (this.block === null) {
this.block = new TextBlock(this.fontSize)
}
this.block.PushChar(char, shape)
this.lastChar = char
this.lastShape = shape
}
GetSpacingWidth() {
return (this.leadingSpaces * this.paragraph.textBox.spaceShape.advance +
this.spaceStartKerning + this.spaceEndKerning) * this.fontSize
}
GetWidth(withSpacing) {
if (this.block === null) {
return 0
}
let width = this.block.GetCurrentPosition()
if (withSpacing) {
width += this.GetSpacingWidth()
}
return width
}
}
TextBox.Paragraph.Line = class {
constructor(paragraph, startChunkIdx, numChunks, width) {
this.paragraph = paragraph
this.startChunkIdx = startChunkIdx
this.numChunks = numChunks
this.width = width
}
ApplyAlignment(boxWidth, defaultAlignment) {
let alignment = this.paragraph.alignment ?? defaultAlignment
switch (alignment) {
case TextBox.Paragraph.Alignment.LEFT:
break
case TextBox.Paragraph.Alignment.CENTER: {
const offset = (boxWidth - this.width) / 2
this.ForEachChunk(chunk => chunk.position += offset)
break
}
case TextBox.Paragraph.Alignment.RIGHT: {
const offset = boxWidth - this.width
this.ForEachChunk(chunk => chunk.position += offset)
break
}
case TextBox.Paragraph.Alignment.JUSTIFY: {
const space = boxWidth - this.width
if (space <= 0 || this.numChunks === 1) {
break
}
const step = space / (this.numChunks - 1)
let offset = 0
this.ForEachChunk(chunk => {
chunk.position += offset
offset += step
})
break
}
default:
throw new Error("Unhandled alignment: " + this.paragraph.alignment)
}
}
ForEachChunk(handler) {
for (let i = 0; i < this.numChunks; i++) {
handler(this.paragraph.chunks[this.startChunkIdx + i])
}
}
}
/** Encapsulates calculations for a single-line text block. */
class TextBlock {
constructor(fontSize) {
this.fontSize = fontSize
/* Element is {shape: CharShape, vertices: ?{Vector2}[]} */
this.glyphs = []
this.bounds = null
this.curX = 0
this.prevChar = null
this.prevFont = null
}
/**
* @param char {string}
* @param shape {CharShape}
*/
PushChar(char, shape) {
/* Initially store with just font size and characters position applied. Origin is the first
* character base point.
*/
let offset
if (this.prevChar !== null && this.prevFont === shape.font) {
offset = this.prevFont.GetKerning(this.prevChar, char)
} else {
offset = 0
}
const x = this.curX + offset * this.fontSize
let vertices
if (shape.vertices && shape.vertices.length > 0) {
vertices = shape.GetVertices({x, y: 0}, this.fontSize)
const xMin = x + shape.bounds.xMin * this.fontSize
const xMax = x + shape.bounds.xMax * this.fontSize
const yMin = shape.bounds.yMin * this.fontSize
const yMax = shape.bounds.yMax * this.fontSize
/* Leading/trailing spaces not accounted intentionally now. */
if (this.bounds === null) {
this.bounds = {xMin, xMax, yMin, yMax}
} else {
if (xMin < this.bounds.xMin) {
this.bounds.xMin = xMin
}
if (yMin < this.bounds.yMin) {
this.bounds.yMin = yMin
}
if (xMax > this.bounds.xMax) {
this.bounds.xMax = xMax
}
if (yMax > this.bounds.yMax) {
this.bounds.yMax = yMax
}
}
} else {
vertices = null
}
this.curX = x + shape.advance * this.fontSize
this.glyphs.push({shape, vertices})
this.prevChar = char
this.prevFont = shape.font
}
GetCurrentPosition() {
return this.curX
}
/**
* @param startPos {{x,y}} TEXT group first alignment point.
* @param endPos {?{x,y}} TEXT group second alignment point.
* @param rotation {?number} Rotation attribute, deg.
* @param widthFactor {?number} Relative X scale factor (group 41).
* @param hAlign {?number} Horizontal text justification type code (group 72).
* @param vAlign {?number} Vertical text justification type code (group 73).
* @param color {number}
* @param layer {?string}
* @return {Generator<Entity>} Rendering entities. Currently just indexed triangles for each
* glyph.
*/
*Render(startPos, endPos, rotation, widthFactor, hAlign, vAlign, color, layer) {
if (this.bounds === null) {
return
}
endPos = endPos ?? startPos
if (rotation) {
rotation *= -Math.PI / 180
} else {
rotation = 0
}
widthFactor = widthFactor ?? 1
hAlign = hAlign ?? HAlign.LEFT
vAlign = vAlign ?? VAlign.BASELINE
let origin = new Vector2()
let scale = new Vector2(widthFactor, 1)
let insertionPos =
(hAlign === HAlign.LEFT && vAlign === VAlign.BASELINE) ||
hAlign === HAlign.FIT || hAlign === HAlign.ALIGNED ?
new Vector2(startPos.x, startPos.y) : new Vector2(endPos.x, endPos.y)
const GetFitScale = () => {
const width = endPos.x - startPos.x
if (width < Number.MIN_VALUE * 2) {
return widthFactor
}
return width / (this.bounds.xMax - this.bounds.xMin)
}
const GetFitRotation = () => {
return -Math.atan2(endPos.y - startPos.y, endPos.x - startPos.x)
}
switch (hAlign) {
case HAlign.LEFT:
origin.x = this.bounds.xMin
break
case HAlign.CENTER:
origin.x = (this.bounds.xMax - this.bounds.xMin) / 2
break
case HAlign.RIGHT:
origin.x = this.bounds.xMax
break
case HAlign.MIDDLE:
origin.x = (this.bounds.xMax - this.bounds.xMin) / 2
origin.y = (this.bounds.yMax - this.bounds.yMin) / 2
break
case HAlign.ALIGNED: {
const f = GetFitScale()
scale.x = f
scale.y = f
rotation = GetFitRotation()
break
}
case HAlign.FIT:
scale.x = GetFitScale()
rotation = GetFitRotation()
break
default:
console.warn("Unrecognized hAlign value: " + hAlign)
}
switch (vAlign) {
case VAlign.BASELINE:
break
case VAlign.BOTTOM:
origin.y = this.bounds.yMin
break
case VAlign.MIDDLE:
origin.y = (this.bounds.yMax - this.bounds.yMin) / 2
break
case VAlign.TOP:
origin.y = this.bounds.yMax
break
default:
console.warn("Unrecognized vAlign value: " + vAlign)
}
const transform = new Matrix3().translate(-origin.x, -origin.y).scale(scale.x, scale.y)
.rotate(rotation).translate(insertionPos.x, insertionPos.y)
for (const glyph of this.glyphs) {
if (glyph.vertices) {
for (const v of glyph.vertices) {
v.applyMatrix3(transform)
}
yield new Entity({
type: Entity.Type.TRIANGLES,
vertices: glyph.vertices,
indices: glyph.shape.indices,
layer, color
})
}
}
}
}