dxf-viewer
Advanced tools
Comparing version 1.0.11 to 1.0.12
{ | ||
"name": "dxf-viewer", | ||
"version": "1.0.11", | ||
"version": "1.0.12", | ||
"description": "JavaScript DXF file viewer", | ||
@@ -5,0 +5,0 @@ "main": "src/index.js", |
@@ -48,6 +48,6 @@ # DXF viewer | ||
waste for the buffer). | ||
* Multiline text blocks (MTEXT group). | ||
* Text styling. Currently, text rendering is using just the specified fonts in the specified order. | ||
DXF style and font attributes are ignored. Text glyphs are always rendered infilled. | ||
* [Special 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) in text. | ||
* Advanced formatting support for MTEXT (fonts, coloring, stacking). | ||
* Line patterns - all lines are rendered in continuous style for now. I am going to use 1-D texture | ||
@@ -65,4 +65,8 @@ generated on preparation stage, texture coordinates (which should account pattern continuity flag | ||
* Dimensions | ||
* Leaders | ||
* Non-UTF-8 file encoding support. Currently, such files are displayed incorrectly. `$DWGCODEPAGE` | ||
parameter is ignored. | ||
* Full OCS support. Currently, it is assumed that entity extrusion direction is either +Z or -Z | ||
(which is commonly used for features mirroring in CAD). Arbitrary directions is not properly | ||
processed. | ||
* Many less commonly used DXF features. |
@@ -6,2 +6,3 @@ import {DynamicBuffer, NativeType} from "./DynamicBuffer" | ||
import {RBTree} from "./RBTree" | ||
import {MTextFormatParser} from "./MTextFormatParser"; | ||
@@ -110,5 +111,30 @@ /** Use 16-bit indices for indexed geometry. */ | ||
async _FetchFonts(dxf) { | ||
const ProcessEntity = async (entity) => { | ||
let ret | ||
if (entity.type === "TEXT") { | ||
ret = await this.textRenderer.FetchFonts(entity.text) | ||
} else if (entity.type === "MTEXT") { | ||
const parser = new MTextFormatParser() | ||
parser.Parse(entity.text) | ||
//XXX formatted MTEXT may specify some fonts explicitly, this is not yet supported | ||
for (const text of parser.GetText()) { | ||
if (!await this.textRenderer.FetchFonts(text)) { | ||
ret = false | ||
break | ||
} | ||
} | ||
ret = true | ||
} else { | ||
throw new Error("Bad entity type") | ||
} | ||
if (!ret) { | ||
this.hasMissingChars = true | ||
} | ||
return ret | ||
} | ||
for (const entity of dxf.entities) { | ||
if (entity.type === "TEXT" || entity.type === "MTEXT") { | ||
if (!await this.textRenderer.FetchFonts(entity.text)) { | ||
if (!await ProcessEntity(entity)) { | ||
/* Failing to resolve some character means that all fonts have been loaded and | ||
@@ -120,3 +146,2 @@ * checked. No mean to check the rest strings. However until it is encountered, | ||
*/ | ||
this.hasMissingChars = true | ||
return | ||
@@ -130,4 +155,3 @@ } | ||
if (entity.type === "TEXT" || entity.type === "MTEXT") { | ||
if (!await this.textRenderer.FetchFonts(entity.text)) { | ||
this.hasMissingChars = true | ||
if (!await ProcessEntity(entity)) { | ||
return | ||
@@ -173,2 +197,5 @@ } | ||
break | ||
case "MTEXT": | ||
renderEntities = this._DecomposeMText(entity, blockCtx) | ||
break | ||
default: | ||
@@ -302,4 +329,3 @@ console.log("Unhandled entity type: " + entity.type) | ||
* @param yRadius {?number} Specify to get ellipse arc. `radius` parameter used as X radius. | ||
* @param transform {?Matrix3} Optional transform matrix for the arc. Applied before the arc is | ||
* positioned to the specified center. | ||
* @param transform {?Matrix3} Optional transform matrix for the arc. Applied as last operation. | ||
*/ | ||
@@ -353,6 +379,6 @@ _GenerateArcVertices({vertices, center, radius, startAngle = null, endAngle = null, | ||
const v = new Vector2(radius * Math.cos(a), yRadius * Math.sin(a)) | ||
v.add(center) | ||
if (transform) { | ||
v.applyMatrix3(transform) | ||
} | ||
v.add(center) | ||
vertices.push(v) | ||
@@ -416,3 +442,2 @@ } | ||
} | ||
//XXX extrusion direction mirror | ||
yield new Entity({ | ||
@@ -567,3 +592,3 @@ type: Entity.Type.POLYLINE, | ||
text: entity.text, | ||
size: entity.textHeight, | ||
fontSize: entity.textHeight, | ||
startPos: entity.startPoint, | ||
@@ -579,2 +604,23 @@ endPos: entity.endPoint, | ||
*_DecomposeMText(entity, blockCtx) { | ||
if (!this.textRenderer.canRender) { | ||
return | ||
} | ||
const layer = this._GetEntityLayer(entity, blockCtx) | ||
const color = this._GetEntityColor(entity, blockCtx) | ||
const parser = new MTextFormatParser() | ||
parser.Parse(entity.text) | ||
yield* this.textRenderer.RenderMText({ | ||
formattedText: parser.GetContent(), | ||
fontSize: entity.height, | ||
position: entity.position, | ||
rotation: entity.rotation, | ||
direction: entity.direction, | ||
attachment: entity.attachmentPoint, | ||
lineSpacing: entity.lineSpacing, | ||
width: entity.width, | ||
color, layer | ||
}) | ||
} | ||
/** | ||
@@ -1079,4 +1125,4 @@ * Updates batches directly. | ||
_GetEntityExtrusionTransform(entity) { | ||
//XXX For now just mirror Y axis if extrusion Z is negative. Should be investigated for | ||
// proper calculation. | ||
//XXX For now just mirror X axis if extrusion Z is negative. No full support for arbitrary | ||
// OCS yet. | ||
if (!entity.hasOwnProperty("extrusionDirection")) { | ||
@@ -1088,3 +1134,3 @@ return null | ||
} | ||
return new Matrix3().scale(1, -1) | ||
return new Matrix3().scale(-1, 1) | ||
} | ||
@@ -1478,13 +1524,12 @@ | ||
const yScale = entity.yScale || 1 | ||
let xScale = entity.xScale || 1 | ||
const xScale = entity.xScale || 1 | ||
const rotation = -(entity.rotation || 0) * Math.PI / 180 | ||
let x = entity.position.x | ||
let y = entity.position.y | ||
if (entity.extrusionDirection && entity.extrusionDirection.z < 0) { | ||
xScale = -xScale | ||
x = -x | ||
} | ||
const y = entity.position.y | ||
mInsert.scale(xScale, yScale) | ||
mInsert.rotate(rotation) | ||
mInsert.translate(x, y) | ||
if (entity.extrusionDirection && entity.extrusionDirection.z < 0) { | ||
mInsert.scale(-1, 1) | ||
} | ||
if (this.type !== BlockContext.Type.INSTANTIATION) { | ||
@@ -1491,0 +1536,0 @@ return mInsert |
@@ -9,2 +9,8 @@ import * as three from "three" | ||
/** Level in "message" events. */ | ||
const MessageLevel = Object.freeze({ | ||
INFO: "info", | ||
WARN: "warn", | ||
ERROR: "error" | ||
}) | ||
@@ -197,4 +203,14 @@ /** The representation class for the viewer, based on Three.js WebGL renderer. */ | ||
this.FitView(scene.bounds.minX - scene.origin.x, scene.bounds.maxX - scene.origin.x, | ||
scene.bounds.minY - scene.origin.y, scene.bounds.maxY - scene.origin.y) | ||
if (scene.bounds) { | ||
this.FitView(scene.bounds.minX - scene.origin.x, scene.bounds.maxX - scene.origin.x, | ||
scene.bounds.minY - scene.origin.y, scene.bounds.maxY - scene.origin.y) | ||
} else { | ||
this._Message("Empty document", MessageLevel.WARN) | ||
} | ||
if (this.hasMissingChars) { | ||
this._Message("Some characters cannot be properly displayed due to missing fonts", | ||
MessageLevel.WARN) | ||
} | ||
this._CreateControls() | ||
@@ -332,2 +348,3 @@ this.Render() | ||
* * "viewChanged" | ||
* * "message" - Some message from the viewer. {message: string, level: string}. | ||
* | ||
@@ -381,2 +398,6 @@ * @param eventName {string} | ||
_Message(message, level = MessageLevel.INFO) { | ||
this._Emit("message", {message, level}) | ||
} | ||
_OnPointerEvent(e) { | ||
@@ -612,2 +633,4 @@ const canvasRect = e.target.getBoundingClientRect() | ||
DxfViewer.MessageLevel = MessageLevel | ||
DxfViewer.DefaultOptions = { | ||
@@ -614,0 +637,0 @@ canvasWidth: 400, |
@@ -38,3 +38,3 @@ /** | ||
group.value = parseGroupValue(group.code, this._data[this._pointer].trim()); | ||
group.value = parseGroupValue(group.code, this._data[this._pointer]); | ||
@@ -62,3 +62,3 @@ this._pointer++; | ||
group.value = parseGroupValue(group.code, this._data[this._pointer + 1].trim()); | ||
group.value = parseGroupValue(group.code, this._data[this._pointer + 1]); | ||
@@ -109,24 +109,24 @@ return group; | ||
if(code <= 9) return value; | ||
if(code >= 10 && code <= 59) return parseFloat(value); | ||
if(code >= 60 && code <= 99) return parseInt(value); | ||
if(code >= 10 && code <= 59) return parseFloat(value.trim()); | ||
if(code >= 60 && code <= 99) return parseInt(value.trim()); | ||
if(code >= 100 && code <= 109) return value; | ||
if(code >= 110 && code <= 149) return parseFloat(value); | ||
if(code >= 160 && code <= 179) return parseInt(value); | ||
if(code >= 210 && code <= 239) return parseFloat(value); | ||
if(code >= 270 && code <= 289) return parseInt(value); | ||
if(code >= 290 && code <= 299) return parseBoolean(value); | ||
if(code >= 110 && code <= 149) return parseFloat(value.trim()); | ||
if(code >= 160 && code <= 179) return parseInt(value.trim()); | ||
if(code >= 210 && code <= 239) return parseFloat(value.trim()); | ||
if(code >= 270 && code <= 289) return parseInt(value.trim()); | ||
if(code >= 290 && code <= 299) return parseBoolean(value.trim()); | ||
if(code >= 300 && code <= 369) return value; | ||
if(code >= 370 && code <= 389) return parseInt(value); | ||
if(code >= 370 && code <= 389) return parseInt(value.trim()); | ||
if(code >= 390 && code <= 399) return value; | ||
if(code >= 400 && code <= 409) return parseInt(value); | ||
if(code >= 400 && code <= 409) return parseInt(value.trim()); | ||
if(code >= 410 && code <= 419) return value; | ||
if(code >= 420 && code <= 429) return parseInt(value); | ||
if(code >= 420 && code <= 429) return parseInt(value.trim()); | ||
if(code >= 430 && code <= 439) return value; | ||
if(code >= 440 && code <= 459) return parseInt(value); | ||
if(code >= 460 && code <= 469) return parseFloat(value); | ||
if(code >= 440 && code <= 459) return parseInt(value.trim()); | ||
if(code >= 460 && code <= 469) return parseFloat(value.trim()); | ||
if(code >= 470 && code <= 481) return value; | ||
if(code === 999) return value; | ||
if(code >= 1000 && code <= 1009) return value; | ||
if(code >= 1010 && code <= 1059) return parseFloat(value); | ||
if(code >= 1060 && code <= 1071) return parseInt(value); | ||
if(code >= 1010 && code <= 1059) return parseFloat(value.trim()); | ||
if(code >= 1060 && code <= 1071) return parseInt(value.trim()); | ||
@@ -133,0 +133,0 @@ console.log('WARNING: Group code does not have a defined type: %j', { code: code, value: value }); |
@@ -76,2 +76,5 @@ | ||
break; | ||
case 101: | ||
helpers.skipEmbeddedObject(scanner); | ||
break; | ||
case 210: | ||
@@ -78,0 +81,0 @@ entity.extrusionDirection = helpers.parsePoint(scanner); |
@@ -16,4 +16,2 @@ | ||
case 3: | ||
entity.text ? entity.text += curr.value : entity.text = curr.value; | ||
break; | ||
case 1: | ||
@@ -25,2 +23,5 @@ entity.text ? entity.text += curr.value : entity.text = curr.value; | ||
break; | ||
case 11: | ||
entity.direction = helpers.parsePoint(scanner); | ||
break; | ||
case 40: | ||
@@ -33,2 +34,5 @@ //Note: this is the text height | ||
break; | ||
case 44: | ||
entity.lineSpacing = curr.value; | ||
break; | ||
case 50: | ||
@@ -43,2 +47,5 @@ entity.rotation = curr.value; | ||
break; | ||
case 101: | ||
helpers.skipEmbeddedObject(scanner); | ||
break; | ||
default: | ||
@@ -45,0 +52,0 @@ helpers.checkCommonEntityProperties(entity, curr); |
@@ -46,2 +46,20 @@ import AUTO_CAD_COLOR_INDEX from './AutoCadColorIndex'; | ||
/** Some entities may contain embedded object which is started by group 101. All the rest data until | ||
* end of entity should not be interpreted as entity attributes. There is no documentation for this | ||
* feature. | ||
* @param scanner | ||
*/ | ||
export function skipEmbeddedObject(scanner) { | ||
/* Ensure proper start group. */ | ||
scanner.rewind() | ||
let curr = scanner.next() | ||
if (curr.code !== 101) { | ||
throw new Error("Bad call for skipEmbeddedObject()") | ||
} | ||
do { | ||
curr = scanner.next() | ||
} while (curr.code !== 0) | ||
scanner.rewind() | ||
} | ||
/** | ||
@@ -48,0 +66,0 @@ * Attempts to parse codes common to all entities. Returns true if the group |
@@ -5,2 +5,3 @@ import {DxfScene, Entity} from "./DxfScene" | ||
import {Matrix3, Vector2} from "three" | ||
import {MTextFormatParser} from "./MTextFormatParser"; | ||
@@ -103,3 +104,3 @@ /** | ||
* @param layer {?string} | ||
* @param size {number} | ||
* @param fontSize {number} | ||
* @return {Generator<Entity>} Rendering entities. Currently just indexed triangles for each | ||
@@ -109,4 +110,4 @@ * glyph. | ||
*Render({text, startPos, endPos, rotation = 0, widthFactor = 1, hAlign = 0, vAlign = 0, | ||
color, layer = null, size}) { | ||
const block = new TextBlock(size) | ||
color, layer = null, fontSize}) { | ||
const block = new TextBlock(fontSize) | ||
for (const char of text) { | ||
@@ -122,2 +123,25 @@ const shape = this._GetCharShape(char) | ||
/** | ||
* @param {MTextFormatEntity[]} formattedText Parsed formatted text. | ||
* @param {{x, y}} position Insertion position. | ||
* @param {Number} fontSize | ||
* @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}) { | ||
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. | ||
@@ -266,6 +290,4 @@ * Each shape is indexed triangles mesh for font size 1. They should be further transformed as | ||
} | ||
let path = null | ||
let x, y, cpx, cpy, cpx1, cpy1, cpx2, cpy2 | ||
const scale = this.scale | ||
path = new ShapePath() | ||
const path = new ShapePath() | ||
for (const cmd of glyph.path.commands) { | ||
@@ -335,7 +357,460 @@ switch (cmd.type) { | ||
/** Encapsulates calculations for a text block. */ | ||
//XXX multiline text | ||
/** 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(size) { | ||
this.size = size | ||
constructor(fontSize) { | ||
this.fontSize = fontSize | ||
/* Element is {shape: CharShape, vertices: ?{Vector2}[]} */ | ||
@@ -363,10 +838,10 @@ this.glyphs = [] | ||
} | ||
const x = this.curX + offset * this.size | ||
const x = this.curX + offset * this.fontSize | ||
let vertices | ||
if (shape.vertices) { | ||
vertices = shape.GetVertices({x, y: 0}, this.size) | ||
const xMin = x + shape.bounds.xMin * this.size | ||
const xMax = x + shape.bounds.xMax * this.size | ||
const yMin = shape.bounds.yMin * this.size | ||
const yMax = shape.bounds.yMax * this.size | ||
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. */ | ||
@@ -392,3 +867,3 @@ if (this.bounds === null) { | ||
} | ||
this.curX = x + shape.advance * this.size | ||
this.curX = x + shape.advance * this.fontSize | ||
this.glyphs.push({shape, vertices}) | ||
@@ -399,2 +874,6 @@ this.prevChar = char | ||
GetCurrentPosition() { | ||
return this.curX | ||
} | ||
/** | ||
@@ -401,0 +880,0 @@ * @param startPos {{x,y}} TEXT group first alignment point. |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
277445
38
7211
71