@vitrosoftware/common-ui-ts 1.1.122 → 1.1.124
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/css/std/controls/checkbox/checkbox.css +4 -0
- package/css/std/controls/checkbox/img/checkbox-indeterminate.svg +4 -0
- package/css/std/controls/date-picker/date-picker.css +1 -25
- package/css/std/controls/dxf-viewer/annotation.css +85 -0
- package/css/std/controls/dxf-viewer/common.css +24 -0
- package/css/std/controls/dxf-viewer/dxf-viewer-index.css +14081 -0
- package/css/std/controls/dxf-viewer/dxf-viewer.css +194 -0
- package/css/std/controls/dxf-viewer/img/cancel-dark-grey.svg +5 -0
- package/css/std/controls/dxf-viewer/img/collapse-bottom.svg +5 -0
- package/css/std/controls/dxf-viewer/img/collapse-up-blue.svg +5 -0
- package/css/std/controls/dxf-viewer/img/delete-active.svg +11 -0
- package/css/std/controls/dxf-viewer/img/delete.svg +11 -0
- package/css/std/controls/dxf-viewer/img/draw-annotation.svg +3 -0
- package/css/std/controls/dxf-viewer/img/invisible-eye.svg +4 -0
- package/css/std/controls/dxf-viewer/img/show-annotation.svg +3 -0
- package/css/std/controls/dxf-viewer/img/sidebar-layers-toggle.svg +6 -0
- package/css/std/controls/dxf-viewer/img/sidebar-notes-toggle.svg +5 -0
- package/css/std/controls/dxf-viewer/img/sidebar-resizer.svg +6 -0
- package/css/std/controls/dxf-viewer/img/sidebar-toggle.svg +7 -0
- package/css/std/controls/dxf-viewer/img/visible-eye.svg +4 -0
- package/css/std/controls/dxf-viewer/img/zoom-in.svg +6 -0
- package/css/std/controls/dxf-viewer/img/zoom-out.svg +5 -0
- package/css/std/controls/dxf-viewer/layer-list.css +104 -0
- package/css/std/controls/dxf-viewer/panel.css +34 -0
- package/css/std/controls/dxf-viewer/prop-inspector.css +102 -0
- package/css/std/controls/dxf-viewer/select.css +111 -0
- package/css/std/controls/dxf-viewer/sidebar.css +190 -0
- package/css/std/controls/dxf-viewer/thumbnail-list.css +65 -0
- package/css/std/controls/dxf-viewer/toolbar.css +117 -0
- package/css/std/controls/dxf-viewer/treeview.css +3 -0
- package/css/std/controls/dxf-viewer/treeview.panel.css +108 -0
- package/css/std/controls/error-message/error-message.css +22 -0
- package/css/std/controls/image-picker/image-picker.css +0 -26
- package/css/std/controls/input/input.css +1 -24
- package/css/std/controls/issue-tile/issue-tile-header.css +1 -0
- package/css/std/controls/login/ntlm-authentication-form.css +9 -12
- package/css/std/controls/lookup-picker/lookup-picker-value-list.css +38 -2
- package/css/std/controls/lookup-picker/lookup-picker.css +1 -25
- package/css/std/controls/table-view/treegrid-context-menu.css +44 -18
- package/css/std/controls/table-view/treegrid-message.css +4 -4
- package/css/std/controls/time-picker/time-picker.css +1 -25
- package/dist/index.css +81 -143
- package/dist/index.js +15137 -489
- package/dist/index.js.map +1 -1
- package/dist/src/controls/Checkbox/Checkbox.d.ts +1 -0
- package/dist/src/controls/DxfViewer/DxfViewer.d.ts +6 -0
- package/dist/src/controls/DxfViewer/DxfViewerContext.d.ts +31 -0
- package/dist/src/controls/DxfViewer/Layer.d.ts +9 -0
- package/dist/src/controls/DxfViewer/LayerList.d.ts +11 -0
- package/dist/src/controls/DxfViewer/Thumbnail.d.ts +7 -0
- package/dist/src/controls/DxfViewer/ThumbnailList.d.ts +6 -0
- package/dist/src/controls/DxfViewer/Viewer.d.ts +6 -0
- package/dist/src/controls/ErrorMessage/ErrorMessage.d.ts +6 -0
- package/dist/src/controls/Login/FormRef.d.ts +3 -0
- package/dist/src/controls/Login/LoginConstants.d.ts +2 -1
- package/dist/src/controls/Login/LoginFormRef.d.ts +2 -2
- package/dist/src/controls/Login/NTLMAuthenticationForm.d.ts +5 -2
- package/dist/src/controls/LookupPicker/LookupPicker.d.ts +2 -0
- package/dist/src/controls/LookupPicker/ValueList.d.ts +2 -0
- package/dist/src/controls/TableView/TableViewConstants.d.ts +11 -0
- package/dist/src/controls/TableView/TreeGridTableViewContextImpl.d.ts +1 -0
- package/dist/src/controls/TreeView/TreeView.d.ts +4 -0
- package/dist/src/controls/TreeView/TreeViewConfig.d.ts +3 -0
- package/dist/src/controls/TreeView/TreeViewConstants.d.ts +2 -1
- package/dist/src/index.d.ts +7 -1
- package/lib/dxf-viewer/BatchingKey.js +91 -0
- package/lib/dxf-viewer/DxfFetcher.js +39 -0
- package/lib/dxf-viewer/DxfScene.js +2695 -0
- package/lib/dxf-viewer/DxfViewer.js +1056 -0
- package/lib/dxf-viewer/DxfWorker.js +229 -0
- package/lib/dxf-viewer/DynamicBuffer.js +100 -0
- package/lib/dxf-viewer/HatchCalculator.js +345 -0
- package/lib/dxf-viewer/LinearDimension.js +323 -0
- package/lib/dxf-viewer/MTextFormatParser.js +211 -0
- package/lib/dxf-viewer/MaterialKey.js +37 -0
- package/lib/dxf-viewer/OrbitControls.js +1253 -0
- package/lib/dxf-viewer/Pattern.js +94 -0
- package/lib/dxf-viewer/RBTree.js +471 -0
- package/lib/dxf-viewer/TextRenderer.js +1038 -0
- package/lib/dxf-viewer/index.js +42 -0
- package/lib/dxf-viewer/math/Matrix2.js +77 -0
- package/lib/dxf-viewer/math/utils.js +59 -0
- package/lib/dxf-viewer/parser/AutoCadColorIndex.js +265 -0
- package/lib/dxf-viewer/parser/DimStyleCodes.js +33 -0
- package/lib/dxf-viewer/parser/DxfArrayScanner.js +143 -0
- package/lib/dxf-viewer/parser/DxfParser.js +980 -0
- package/lib/dxf-viewer/parser/ExtendedDataParse-My.js +91 -0
- package/lib/dxf-viewer/parser/ExtendedDataParser.js +123 -0
- package/lib/dxf-viewer/parser/ParseHelpers.js +142 -0
- package/lib/dxf-viewer/parser/entities/3dface.js +83 -0
- package/lib/dxf-viewer/parser/entities/arc.js +38 -0
- package/lib/dxf-viewer/parser/entities/attdef.js +89 -0
- package/lib/dxf-viewer/parser/entities/attrib.js +34 -0
- package/lib/dxf-viewer/parser/entities/attribute.js +109 -0
- package/lib/dxf-viewer/parser/entities/circle.js +43 -0
- package/lib/dxf-viewer/parser/entities/dimension.js +72 -0
- package/lib/dxf-viewer/parser/entities/ellipse.js +46 -0
- package/lib/dxf-viewer/parser/entities/hatch.js +343 -0
- package/lib/dxf-viewer/parser/entities/insert.js +62 -0
- package/lib/dxf-viewer/parser/entities/leader.js +84 -0
- package/lib/dxf-viewer/parser/entities/line.js +34 -0
- package/lib/dxf-viewer/parser/entities/lwpolyline.js +100 -0
- package/lib/dxf-viewer/parser/entities/mtext.js +54 -0
- package/lib/dxf-viewer/parser/entities/point.js +35 -0
- package/lib/dxf-viewer/parser/entities/polyline.js +92 -0
- package/lib/dxf-viewer/parser/entities/solid.js +40 -0
- package/lib/dxf-viewer/parser/entities/spline.js +70 -0
- package/lib/dxf-viewer/parser/entities/text.js +47 -0
- package/lib/dxf-viewer/parser/entities/vertex.js +62 -0
- package/lib/dxf-viewer/parser/entities/viewport.js +56 -0
- package/lib/dxf-viewer/parser/objects/dictionary.js +29 -0
- package/lib/dxf-viewer/parser/objects/layout.js +35 -0
- package/lib/dxf-viewer/parser/objects/xrecord.js +29 -0
- package/lib/opentype/opentype.module.js +14571 -0
- package/lib/three/CSS2DRenderer.js +235 -0
- package/lib/three/three.module.js +49912 -0
- package/package.json +12 -10
- package/src/controls/BimViewer/js/bim-viewer.js +2 -2
- package/src/controls/DxfViewer/js/dxf-viewer.js +3580 -0
- package/src/controls/PdfViewer/js/pdf-viewer.js +1 -1
- package/css/std/controls/input/img/error-message.svg +0 -6
- package/css/std/controls/lookup-picker/img/error-message.svg +0 -6
- package/css/std/controls/time-picker/img/error-message.svg +0 -6
- /package/css/std/controls/{date-picker → error-message}/img/error-message.svg +0 -0
|
@@ -0,0 +1,1038 @@
|
|
|
1
|
+
import { ShapePath } from "/resource/dxfViewer/js/three/three.module.js"
|
|
2
|
+
import { ShapeUtils } from "/resource/dxfViewer/js/three/three.module.js"
|
|
3
|
+
import { Matrix3, Vector2 } from "/resource/dxfViewer/js/three/three.module.js"
|
|
4
|
+
import {DxfScene, Entity} from "./DxfScene.js"
|
|
5
|
+
import {MTextFormatParser} from "./MTextFormatParser.js"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
/** Regex for parsing special characters in text entities. */
|
|
9
|
+
const SPECIAL_CHARS_RE = /(?:%%([dpcou%]))|(?:\\U\+([0-9a-fA-F]{4}))/g
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parse special characters in text entities and convert them to corresponding unicode
|
|
13
|
+
* characters.
|
|
14
|
+
* https://knowledge.autodesk.com/support/autocad/learn-explore/caas/CloudHelp/cloudhelp/2019/ENU/AutoCAD-Core/files/GUID-518E1A9D-398C-4A8A-AC32-2D85590CDBE1-htm.html
|
|
15
|
+
* @param {string} text Raw string.
|
|
16
|
+
* @return {string} String with special characters replaced.
|
|
17
|
+
*/
|
|
18
|
+
export function ParseSpecialChars(text) {
|
|
19
|
+
return text.replaceAll(SPECIAL_CHARS_RE, (match, p1, p2) => {
|
|
20
|
+
if (p1 !== undefined) {
|
|
21
|
+
switch (p1) {
|
|
22
|
+
case "d":
|
|
23
|
+
return "\xb0"
|
|
24
|
+
case "p":
|
|
25
|
+
return "\xb1"
|
|
26
|
+
case "c":
|
|
27
|
+
return "\u2205"
|
|
28
|
+
case "o":
|
|
29
|
+
/* Toggles overscore mode on and off, not implemented. */
|
|
30
|
+
return ""
|
|
31
|
+
case "u":
|
|
32
|
+
/* Toggles underscore mode on and off, not implemented. */
|
|
33
|
+
return ""
|
|
34
|
+
case "%":
|
|
35
|
+
return "%"
|
|
36
|
+
}
|
|
37
|
+
} else if (p2 !== undefined) {
|
|
38
|
+
const code = parseInt(p2, 16)
|
|
39
|
+
if (isNaN(code)) {
|
|
40
|
+
return match
|
|
41
|
+
}
|
|
42
|
+
return String.fromCharCode(code)
|
|
43
|
+
}
|
|
44
|
+
return match
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Helper class for rendering text.
|
|
51
|
+
* Currently it is just basic very simplified implementation for MVP. Further work should include:
|
|
52
|
+
* * Support DXF text styles and weight.
|
|
53
|
+
* * Bitmap fonts generation in texture atlas for more optimal rendering.
|
|
54
|
+
*/
|
|
55
|
+
export class TextRenderer {
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param fontFetchers {?Function[]} List of font fetchers. Fetcher should return promise with
|
|
59
|
+
* loaded font object (opentype.js). They are invoked only when necessary. Each glyph is being
|
|
60
|
+
* searched sequentially in each provided font.
|
|
61
|
+
* @param options {?{}} See TextRenderer.DefaultOptions.
|
|
62
|
+
*/
|
|
63
|
+
constructor(fontFetchers, options = null) {
|
|
64
|
+
this.fontFetchers = fontFetchers
|
|
65
|
+
this.fonts = []
|
|
66
|
+
|
|
67
|
+
this.options = Object.create(TextRenderer.DefaultOptions)
|
|
68
|
+
if (options) {
|
|
69
|
+
Object.assign(this.options, options)
|
|
70
|
+
}
|
|
71
|
+
/* Indexed by character, value is CharShape. */
|
|
72
|
+
this.shapes = new Map()
|
|
73
|
+
this.stubShapeLoaded = false
|
|
74
|
+
/* Shape to display if no glyph found in the specified fonts. May be null if fallback
|
|
75
|
+
* character can not be rendered as well.
|
|
76
|
+
*/
|
|
77
|
+
this.stubShape = null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Fetch necessary fonts to render the provided text. Should be called for each string which
|
|
81
|
+
* will be rendered later.
|
|
82
|
+
* @param text {string}
|
|
83
|
+
* @return {Boolean} True if all characters can be rendered, false if none of the provided fonts
|
|
84
|
+
* contains glyphs for some of the specified text characters.
|
|
85
|
+
*/
|
|
86
|
+
async FetchFonts(text) {
|
|
87
|
+
if (!this.stubShapeLoaded) {
|
|
88
|
+
this.stubShapeLoaded = true
|
|
89
|
+
for (const char of Array.from(this.options.fallbackChar)) {
|
|
90
|
+
if (await this.FetchFonts(char)) {
|
|
91
|
+
this.stubShape = this._CreateCharShape(char)
|
|
92
|
+
break
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
let charMissing = false
|
|
97
|
+
for (const char of text) {
|
|
98
|
+
if (char.codePointAt(0) < 0x20) {
|
|
99
|
+
/* Control character. */
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
let found = false
|
|
103
|
+
for (const font of this.fonts) {
|
|
104
|
+
if (font.HasChar(char)) {
|
|
105
|
+
found = true
|
|
106
|
+
break
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (found) {
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
if (!this.fontFetchers) {
|
|
113
|
+
return false
|
|
114
|
+
}
|
|
115
|
+
while (this.fontFetchers.length > 0) {
|
|
116
|
+
const fetcher = this.fontFetchers.shift()
|
|
117
|
+
const font = await this._FetchFont(fetcher)
|
|
118
|
+
this.fonts.push(font)
|
|
119
|
+
if (font.HasChar(char)) {
|
|
120
|
+
found = true
|
|
121
|
+
break
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (!found) {
|
|
125
|
+
charMissing = true
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return !charMissing
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
get canRender() {
|
|
132
|
+
return this.fonts !== null && this.fonts.length > 0
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Get width in model space units for a single line of text.
|
|
136
|
+
* @param text {string}
|
|
137
|
+
* @param fontSize {number}
|
|
138
|
+
*/
|
|
139
|
+
GetLineWidth(text, fontSize) {
|
|
140
|
+
const block = new TextBlock(fontSize)
|
|
141
|
+
for (const char of text) {
|
|
142
|
+
const shape = this._GetCharShape(char)
|
|
143
|
+
if (!shape) {
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
block.PushChar(char, shape)
|
|
147
|
+
}
|
|
148
|
+
return block.GetCurrentPosition()
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @param text {string}
|
|
154
|
+
* @param startPos {{x,y}}
|
|
155
|
+
* @param endPos {?{x,y}} TEXT group second alignment point.
|
|
156
|
+
* @param rotation {?number} Rotation attribute, deg.
|
|
157
|
+
* @param widthFactor {?number} Relative X scale factor (group 41)
|
|
158
|
+
* @param hAlign {?number} Horizontal text justification type code (group 72)
|
|
159
|
+
* @param vAlign {?number} Vertical text justification type code (group 73).
|
|
160
|
+
* @param color {number}
|
|
161
|
+
* @param layer {?string}
|
|
162
|
+
* @param fontSize {number}
|
|
163
|
+
* @return {Generator<Entity>} Rendering entities. Currently just indexed triangles for each
|
|
164
|
+
* glyph.
|
|
165
|
+
*/
|
|
166
|
+
*Render({text, startPos, endPos, rotation = 0, widthFactor = 1, hAlign = 0, vAlign = 0,
|
|
167
|
+
color, layer = null, fontSize}) {
|
|
168
|
+
const block = new TextBlock(fontSize)
|
|
169
|
+
for (const char of text) {
|
|
170
|
+
const shape = this._GetCharShape(char)
|
|
171
|
+
if (!shape) {
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
block.PushChar(char, shape)
|
|
175
|
+
}
|
|
176
|
+
yield* block.Render(startPos, endPos, rotation, widthFactor, hAlign, vAlign, color, layer)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @param {MTextFormatEntity[]} formattedText Parsed formatted text.
|
|
181
|
+
* @param {{x, y}} position Insertion position.
|
|
182
|
+
* @param {Number} fontSize
|
|
183
|
+
* @param {?Number} width Text block width, no wrapping if undefined.
|
|
184
|
+
* @param {?Number} rotation Text block rotation in degrees.
|
|
185
|
+
* @param {?{x, y}} direction Text block orientation defined as direction vector. Takes a
|
|
186
|
+
* precedence over rotation if both provided.
|
|
187
|
+
* @param {number} attachment Attachment point, one of MTextAttachment values.
|
|
188
|
+
* @param {?number} lineSpacing Line spacing ratio relative to default one (5/3 of font size).
|
|
189
|
+
* @param {number} color
|
|
190
|
+
* @param {?string} layer
|
|
191
|
+
* @return {Generator<Entity>} Rendering entities. Currently just indexed triangles for each
|
|
192
|
+
* glyph.
|
|
193
|
+
*/
|
|
194
|
+
*RenderMText({formattedText, position, fontSize, width = null, rotation = 0, direction = null,
|
|
195
|
+
attachment, lineSpacing = 1, color, layer = null}) {
|
|
196
|
+
const box = new TextBox(fontSize, this._GetCharShape.bind(this))
|
|
197
|
+
box.FeedText(formattedText)
|
|
198
|
+
yield* box.Render(position, width, rotation, direction, attachment, lineSpacing, color,
|
|
199
|
+
layer)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** @return {CharShape} Shape for the specified character.
|
|
203
|
+
* Each shape is indexed triangles mesh for font size 1. They should be further transformed as
|
|
204
|
+
* needed.
|
|
205
|
+
*/
|
|
206
|
+
_GetCharShape(char) {
|
|
207
|
+
let shape = this.shapes.get(char)
|
|
208
|
+
if (shape) {
|
|
209
|
+
return shape
|
|
210
|
+
}
|
|
211
|
+
shape = this._CreateCharShape(char)
|
|
212
|
+
this.shapes.set(char, shape)
|
|
213
|
+
return shape
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
_CreateCharShape(char) {
|
|
217
|
+
for (const font of this.fonts) {
|
|
218
|
+
const path = font.GetCharPath(char)
|
|
219
|
+
if (path) {
|
|
220
|
+
return new CharShape(font, path, this.options)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return this.stubShape
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async _FetchFont(fontFetcher) {
|
|
227
|
+
return new Font(await fontFetcher())
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
TextRenderer.DefaultOptions = {
|
|
233
|
+
/** Number of segments for each curve in a glyph. Currently Three.js does not have more
|
|
234
|
+
* adequate angle-based or length-based tessellation option.
|
|
235
|
+
*/
|
|
236
|
+
curveSubdivision: 2,
|
|
237
|
+
/** Character to use when the specified fonts does not contain necessary glyph. Several ones can
|
|
238
|
+
* be specified, the first one available is used.
|
|
239
|
+
*/
|
|
240
|
+
fallbackChar: "\uFFFD?"
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** @typedef {Object} CharPath
|
|
244
|
+
* @property advance {number}
|
|
245
|
+
* @property path {?ShapePath}
|
|
246
|
+
* @property bounds {xMin: number, xMax: number, yMin: number, yMax: number}
|
|
247
|
+
*/
|
|
248
|
+
|
|
249
|
+
class CharShape {
|
|
250
|
+
/**
|
|
251
|
+
* @param font {Font}
|
|
252
|
+
* @param glyph {CharPath}
|
|
253
|
+
* @param options {{}} Renderer options.
|
|
254
|
+
*/
|
|
255
|
+
constructor(font, glyph, options) {
|
|
256
|
+
this.font = font
|
|
257
|
+
this.advance = glyph.advance
|
|
258
|
+
this.bounds = glyph.bounds
|
|
259
|
+
if (glyph.path) {
|
|
260
|
+
const shapes = glyph.path.toShapes(false)
|
|
261
|
+
this.vertices = []
|
|
262
|
+
this.indices = []
|
|
263
|
+
for (const shape of shapes) {
|
|
264
|
+
const shapePoints = shape.extractPoints(options.curveSubdivision)
|
|
265
|
+
/* Ensure proper vertices winding. */
|
|
266
|
+
if (!ShapeUtils.isClockWise(shapePoints.shape)) {
|
|
267
|
+
shapePoints.shape = shapePoints.shape.reverse()
|
|
268
|
+
for (const hole of shapePoints.holes) {
|
|
269
|
+
if (ShapeUtils.isClockWise(hole)) {
|
|
270
|
+
shapePoints.holes[h] = hole.reverse()
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/* This call also removes duplicated end vertices. */
|
|
275
|
+
const indices = ShapeUtils.triangulateShape(shapePoints.shape, shapePoints.holes)
|
|
276
|
+
|
|
277
|
+
const _this = this
|
|
278
|
+
const baseIdx = this.vertices.length
|
|
279
|
+
|
|
280
|
+
function AddVertices(vertices) {
|
|
281
|
+
for (const v of vertices) {
|
|
282
|
+
_this.vertices.push(v)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
AddVertices(shapePoints.shape)
|
|
287
|
+
for (const hole of shapePoints.holes) {
|
|
288
|
+
AddVertices(hole)
|
|
289
|
+
}
|
|
290
|
+
for (const tuple of indices) {
|
|
291
|
+
for (const idx of tuple) {
|
|
292
|
+
this.indices.push(baseIdx + idx)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
} else {
|
|
298
|
+
this.vertices = null
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Get vertices array transformed to the specified position and with the specified size.
|
|
303
|
+
* @param position {{x,y}}
|
|
304
|
+
* @param size {number}
|
|
305
|
+
* @return {Vector2[]}
|
|
306
|
+
*/
|
|
307
|
+
GetVertices(position, size) {
|
|
308
|
+
return this.vertices.map(v => v.clone().multiplyScalar(size).add(position))
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
class Font {
|
|
313
|
+
constructor(data) {
|
|
314
|
+
this.data = data
|
|
315
|
+
this.charMap = new Map()
|
|
316
|
+
for (const glyph of Object.values(data.glyphs.glyphs)) {
|
|
317
|
+
if (glyph.unicode === undefined) {
|
|
318
|
+
continue
|
|
319
|
+
}
|
|
320
|
+
this.charMap.set(String.fromCodePoint(glyph.unicode), glyph)
|
|
321
|
+
}
|
|
322
|
+
/* Scale to transform the paths to size 1. */
|
|
323
|
+
//XXX not really clear what is the resulting unit, check, review and comment it later
|
|
324
|
+
// (100px?)
|
|
325
|
+
this.scale = 100 / ((this.data.unitsPerEm || 2048) * 72)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* @param char {string} Character code point as string.
|
|
330
|
+
* @return {Boolean} True if the font has glyphs for the specified character.
|
|
331
|
+
*/
|
|
332
|
+
HasChar(char) {
|
|
333
|
+
return this.charMap.has(char)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* @param char {string} Character code point as string.
|
|
338
|
+
* @return {?CharPath} Path is scaled to size 1. Null if no glyphs for the specified characters.
|
|
339
|
+
*/
|
|
340
|
+
GetCharPath(char) {
|
|
341
|
+
const glyph = this.charMap.get(char)
|
|
342
|
+
if (!glyph) {
|
|
343
|
+
return null
|
|
344
|
+
}
|
|
345
|
+
const scale = this.scale
|
|
346
|
+
const path = new ShapePath()
|
|
347
|
+
for (const cmd of glyph.path.commands) {
|
|
348
|
+
switch (cmd.type) {
|
|
349
|
+
|
|
350
|
+
case 'M':
|
|
351
|
+
path.moveTo(cmd.x * scale, cmd.y * scale)
|
|
352
|
+
break
|
|
353
|
+
|
|
354
|
+
case 'L':
|
|
355
|
+
path.lineTo(cmd.x * scale, cmd.y * scale)
|
|
356
|
+
break
|
|
357
|
+
|
|
358
|
+
case 'Q':
|
|
359
|
+
path.quadraticCurveTo(cmd.x1 * scale, cmd.y1 * scale,
|
|
360
|
+
cmd.x * scale, cmd.y * scale)
|
|
361
|
+
break
|
|
362
|
+
|
|
363
|
+
case 'C':
|
|
364
|
+
path.bezierCurveTo(cmd.x1 * scale, cmd.y1 * scale,
|
|
365
|
+
cmd.x2 * scale, cmd.y2 * scale,
|
|
366
|
+
cmd.x * scale, cmd.y * scale)
|
|
367
|
+
break
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return {advance: glyph.advanceWidth * scale, path,
|
|
371
|
+
bounds: {xMin: glyph.xMin * scale, xMax: glyph.xMax * scale,
|
|
372
|
+
yMin: glyph.yMin * scale, yMax: glyph.yMax * scale}}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* @param c1 {string}
|
|
377
|
+
* @param c2 {string}
|
|
378
|
+
* @return {number}
|
|
379
|
+
*/
|
|
380
|
+
GetKerning(c1, c2) {
|
|
381
|
+
const i1 = this.data.charToGlyphIndex(c1)
|
|
382
|
+
if (i1 === 0) {
|
|
383
|
+
return 0
|
|
384
|
+
}
|
|
385
|
+
const i2 = this.data.charToGlyphIndex(c1)
|
|
386
|
+
if (i2 === 0) {
|
|
387
|
+
return 0
|
|
388
|
+
}
|
|
389
|
+
return this.data.getKerningValue(i1, i2) * this.scale
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** TEXT group attribute 72 values. */
|
|
394
|
+
export const HAlign = Object.freeze({
|
|
395
|
+
LEFT: 0,
|
|
396
|
+
CENTER: 1,
|
|
397
|
+
RIGHT: 2,
|
|
398
|
+
ALIGNED: 3,
|
|
399
|
+
MIDDLE: 4,
|
|
400
|
+
FIT: 5
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
/** TEXT group attribute 73 values. */
|
|
404
|
+
export const VAlign = Object.freeze({
|
|
405
|
+
BASELINE: 0,
|
|
406
|
+
BOTTOM: 1,
|
|
407
|
+
MIDDLE: 2,
|
|
408
|
+
TOP: 3
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
/** MTEXT group attribute 71 values. */
|
|
412
|
+
const MTextAttachment = Object.freeze({
|
|
413
|
+
TOP_LEFT: 1,
|
|
414
|
+
TOP_CENTER: 2,
|
|
415
|
+
TOP_RIGHT: 3,
|
|
416
|
+
MIDDLE_LEFT: 4,
|
|
417
|
+
MIDDLE_CENTER: 5,
|
|
418
|
+
MIDDLE_RIGHT: 6,
|
|
419
|
+
BOTTOM_LEFT: 7,
|
|
420
|
+
BOTTOM_CENTER: 8,
|
|
421
|
+
BOTTOM_RIGHT: 9
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
/** Encapsulates layout calculations for a multiline-line text block. */
|
|
425
|
+
class TextBox {
|
|
426
|
+
/**
|
|
427
|
+
* @param fontSize
|
|
428
|
+
* @param {Function<CharShape, String>} charShapeProvider
|
|
429
|
+
*/
|
|
430
|
+
constructor(fontSize, charShapeProvider) {
|
|
431
|
+
this.fontSize = fontSize
|
|
432
|
+
this.charShapeProvider = charShapeProvider
|
|
433
|
+
this.curParagraph = new TextBox.Paragraph(this)
|
|
434
|
+
this.paragraphs = [this.curParagraph]
|
|
435
|
+
this.spaceShape = charShapeProvider(" ")
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** Add some formatted text to the box.
|
|
439
|
+
* @param {MTextFormatEntity[]} formattedText Parsed formatted text.
|
|
440
|
+
*/
|
|
441
|
+
FeedText(formattedText) {
|
|
442
|
+
/* For now advanced formatting is not implemented so scopes are just flattened. */
|
|
443
|
+
function *FlattenItems(items) {
|
|
444
|
+
for (const item of items) {
|
|
445
|
+
if (item.type === MTextFormatParser.EntityType.SCOPE) {
|
|
446
|
+
yield *FlattenItems(item.content)
|
|
447
|
+
} else {
|
|
448
|
+
yield item
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/* Null is default alignment which depends on attachment point. */
|
|
454
|
+
let curAlignment = null
|
|
455
|
+
|
|
456
|
+
for (const item of FlattenItems(formattedText)) {
|
|
457
|
+
switch(item.type) {
|
|
458
|
+
|
|
459
|
+
case MTextFormatParser.EntityType.TEXT:
|
|
460
|
+
for (const c of item.content) {
|
|
461
|
+
if (c === " ") {
|
|
462
|
+
this.curParagraph.FeedSpace()
|
|
463
|
+
} else {
|
|
464
|
+
this.curParagraph.FeedChar(c)
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
break
|
|
468
|
+
|
|
469
|
+
case MTextFormatParser.EntityType.PARAGRAPH:
|
|
470
|
+
this.curParagraph = new TextBox.Paragraph(this)
|
|
471
|
+
this.curParagraph.SetAlignment(curAlignment)
|
|
472
|
+
this.paragraphs.push(this.curParagraph)
|
|
473
|
+
break
|
|
474
|
+
|
|
475
|
+
case MTextFormatParser.EntityType.NON_BREAKING_SPACE:
|
|
476
|
+
this.curParagraph.FeedChar(" ")
|
|
477
|
+
break
|
|
478
|
+
|
|
479
|
+
case MTextFormatParser.EntityType.PARAGRAPH_ALIGNMENT:
|
|
480
|
+
let a = null
|
|
481
|
+
switch (item.alignment) {
|
|
482
|
+
case "l":
|
|
483
|
+
a = TextBox.Paragraph.Alignment.LEFT
|
|
484
|
+
break
|
|
485
|
+
case "c":
|
|
486
|
+
a = TextBox.Paragraph.Alignment.CENTER
|
|
487
|
+
break
|
|
488
|
+
case "r":
|
|
489
|
+
a = TextBox.Paragraph.Alignment.RIGHT
|
|
490
|
+
break
|
|
491
|
+
case "d":
|
|
492
|
+
a = TextBox.Paragraph.Alignment.JUSTIFY
|
|
493
|
+
break
|
|
494
|
+
case "j":
|
|
495
|
+
a = null
|
|
496
|
+
break
|
|
497
|
+
}
|
|
498
|
+
this.curParagraph.SetAlignment(a)
|
|
499
|
+
curAlignment = a
|
|
500
|
+
break
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
*Render(position, width, rotation, direction, attachment, lineSpacing, color, layer) {
|
|
506
|
+
for (const p of this.paragraphs) {
|
|
507
|
+
p.BuildLines(width)
|
|
508
|
+
}
|
|
509
|
+
if (width === null || width === 0) {
|
|
510
|
+
/* Find maximal paragraph width which will define overall box width. */
|
|
511
|
+
width = 0
|
|
512
|
+
for (const p of this.paragraphs) {
|
|
513
|
+
const pWidth = p.GetMaxLineWidth()
|
|
514
|
+
if (pWidth > width) {
|
|
515
|
+
width = pWidth
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
let defaultAlignment = TextBox.Paragraph.Alignment.LEFT
|
|
521
|
+
switch (attachment) {
|
|
522
|
+
case MTextAttachment.TOP_CENTER:
|
|
523
|
+
case MTextAttachment.MIDDLE_CENTER:
|
|
524
|
+
case MTextAttachment.BOTTOM_CENTER:
|
|
525
|
+
defaultAlignment = TextBox.Paragraph.Alignment.CENTER
|
|
526
|
+
break
|
|
527
|
+
case MTextAttachment.TOP_RIGHT:
|
|
528
|
+
case MTextAttachment.MIDDLE_RIGHT:
|
|
529
|
+
case MTextAttachment.BOTTOM_RIGHT:
|
|
530
|
+
defaultAlignment = TextBox.Paragraph.Alignment.RIGHT
|
|
531
|
+
break
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
for (const p of this.paragraphs) {
|
|
535
|
+
p.ApplyAlignment(width, defaultAlignment)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/* Box local coordinates have top-left corner origin, so Y values are negative. The
|
|
539
|
+
* specified attachment should be used to obtain attachment point offset relatively to box
|
|
540
|
+
* CS origin.
|
|
541
|
+
*/
|
|
542
|
+
|
|
543
|
+
if (direction !== null) {
|
|
544
|
+
/* Direction takes precedence over rotation if specified. */
|
|
545
|
+
rotation = Math.atan2(direction.y, direction.x) * 180 / Math.PI
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const lineHeight = lineSpacing * 5 * this.fontSize / 3
|
|
549
|
+
|
|
550
|
+
let height = 0
|
|
551
|
+
for (const p of this.paragraphs) {
|
|
552
|
+
if (p.lines === null) {
|
|
553
|
+
/* Paragraph always occupies at least one line. */
|
|
554
|
+
height++
|
|
555
|
+
} else {
|
|
556
|
+
height += p.lines.length
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
height *= lineHeight
|
|
560
|
+
|
|
561
|
+
let origin = new Vector2()
|
|
562
|
+
switch (attachment) {
|
|
563
|
+
case MTextAttachment.TOP_LEFT:
|
|
564
|
+
break
|
|
565
|
+
case MTextAttachment.TOP_CENTER:
|
|
566
|
+
origin.x = width / 2
|
|
567
|
+
break
|
|
568
|
+
case MTextAttachment.TOP_RIGHT:
|
|
569
|
+
origin.x = width
|
|
570
|
+
break
|
|
571
|
+
case MTextAttachment.MIDDLE_LEFT:
|
|
572
|
+
origin.y = -height / 2
|
|
573
|
+
break
|
|
574
|
+
case MTextAttachment.MIDDLE_CENTER:
|
|
575
|
+
origin.x = width / 2
|
|
576
|
+
origin.y = -height / 2
|
|
577
|
+
break
|
|
578
|
+
case MTextAttachment.MIDDLE_RIGHT:
|
|
579
|
+
origin.x = width
|
|
580
|
+
origin.y = -height / 2
|
|
581
|
+
break
|
|
582
|
+
case MTextAttachment.BOTTOM_LEFT:
|
|
583
|
+
origin.y = -height
|
|
584
|
+
break
|
|
585
|
+
case MTextAttachment.BOTTOM_CENTER:
|
|
586
|
+
origin.x = width / 2
|
|
587
|
+
origin.y = -height
|
|
588
|
+
break
|
|
589
|
+
case MTextAttachment.BOTTOM_RIGHT:
|
|
590
|
+
origin.x = width
|
|
591
|
+
origin.y = -height
|
|
592
|
+
break
|
|
593
|
+
default:
|
|
594
|
+
throw new Error("Unhandled alignment")
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/* Transform for each chunk insertion point. */
|
|
598
|
+
const transform = new Matrix3().translate(-origin.x, -origin.y)
|
|
599
|
+
.rotate(-rotation * Math.PI / 180).translate(position.x, position.y)
|
|
600
|
+
|
|
601
|
+
let y = -this.fontSize
|
|
602
|
+
for (const p of this.paragraphs) {
|
|
603
|
+
if (p.lines === null) {
|
|
604
|
+
y -= lineHeight
|
|
605
|
+
continue
|
|
606
|
+
}
|
|
607
|
+
for (const line of p.lines) {
|
|
608
|
+
for (let chunkIdx = line.startChunkIdx;
|
|
609
|
+
chunkIdx < line.startChunkIdx + line.numChunks;
|
|
610
|
+
chunkIdx++) {
|
|
611
|
+
|
|
612
|
+
const chunk = p.chunks[chunkIdx]
|
|
613
|
+
let x = chunk.position
|
|
614
|
+
/* First chunk of continuation line never prepended by whitespace. */
|
|
615
|
+
if (chunkIdx === 0 || chunkIdx !== line.startChunkIdx) {
|
|
616
|
+
x += chunk.GetSpacingWidth()
|
|
617
|
+
}
|
|
618
|
+
const v = new Vector2(x, y)
|
|
619
|
+
v.applyMatrix3(transform)
|
|
620
|
+
if (chunk.block) {
|
|
621
|
+
yield* chunk.block.Render(v, null, rotation, null,
|
|
622
|
+
HAlign.LEFT, VAlign.BASELINE,
|
|
623
|
+
color, layer)
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
y -= lineHeight
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
TextBox.Paragraph = class {
|
|
633
|
+
constructor(textBox) {
|
|
634
|
+
this.textBox = textBox
|
|
635
|
+
this.chunks = []
|
|
636
|
+
this.curChunk = null
|
|
637
|
+
this.alignment = null
|
|
638
|
+
this.lines = null
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** Feed character for current chunk. Spaces should be fed by FeedSpace() method. If space
|
|
642
|
+
* character is fed into this method, it is interpreted as non-breaking space.
|
|
643
|
+
*/
|
|
644
|
+
FeedChar(c) {
|
|
645
|
+
const shape = this.textBox.charShapeProvider(c)
|
|
646
|
+
if (shape === null) {
|
|
647
|
+
return
|
|
648
|
+
}
|
|
649
|
+
if (this.curChunk === null) {
|
|
650
|
+
this._AddChunk()
|
|
651
|
+
}
|
|
652
|
+
this.curChunk.PushChar(c, shape)
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
FeedSpace() {
|
|
656
|
+
if (this.curChunk === null || this.curChunk.lastChar !== null) {
|
|
657
|
+
this._AddChunk()
|
|
658
|
+
}
|
|
659
|
+
this.curChunk.PushSpace()
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
SetAlignment(alignment) {
|
|
663
|
+
this.alignment = alignment
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/** Group chunks into lines.
|
|
667
|
+
*
|
|
668
|
+
* @param {?number} boxWidth Box width. Do not wrap lines if null (one line is created).
|
|
669
|
+
*/
|
|
670
|
+
BuildLines(boxWidth) {
|
|
671
|
+
if (this.curChunk === null) {
|
|
672
|
+
return
|
|
673
|
+
}
|
|
674
|
+
this.lines = []
|
|
675
|
+
let startChunkIdx = 0
|
|
676
|
+
let curChunkIdx = 0
|
|
677
|
+
let curWidth = 0
|
|
678
|
+
|
|
679
|
+
const CommitLine = () => {
|
|
680
|
+
this.lines.push(new TextBox.Paragraph.Line(this,
|
|
681
|
+
startChunkIdx,
|
|
682
|
+
curChunkIdx - startChunkIdx,
|
|
683
|
+
curWidth))
|
|
684
|
+
startChunkIdx = curChunkIdx
|
|
685
|
+
curWidth = 0
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
for (; curChunkIdx < this.chunks.length; curChunkIdx++) {
|
|
689
|
+
const chunk = this.chunks[curChunkIdx]
|
|
690
|
+
const chunkWidth = chunk.GetWidth(startChunkIdx === 0 || curChunkIdx !== startChunkIdx)
|
|
691
|
+
if (boxWidth !== null && boxWidth !== 0 && curWidth !== 0 &&
|
|
692
|
+
curWidth + chunkWidth > boxWidth) {
|
|
693
|
+
|
|
694
|
+
CommitLine()
|
|
695
|
+
}
|
|
696
|
+
chunk.position = curWidth
|
|
697
|
+
curWidth += chunkWidth
|
|
698
|
+
}
|
|
699
|
+
if (startChunkIdx !== curChunkIdx && curWidth !== 0) {
|
|
700
|
+
CommitLine()
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
GetMaxLineWidth() {
|
|
705
|
+
if (this.lines === null) {
|
|
706
|
+
return 0
|
|
707
|
+
}
|
|
708
|
+
let maxWidth = 0
|
|
709
|
+
for (const line of this.lines) {
|
|
710
|
+
if (line.width > maxWidth) {
|
|
711
|
+
maxWidth = line.width
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return maxWidth
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
ApplyAlignment(boxWidth, defaultAlignment) {
|
|
718
|
+
if (this.lines) {
|
|
719
|
+
for (const line of this.lines) {
|
|
720
|
+
line.ApplyAlignment(boxWidth, defaultAlignment)
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
_AddChunk() {
|
|
726
|
+
this.curChunk = new TextBox.Paragraph.Chunk(this, this.textBox.fontSize, this.curChunk)
|
|
727
|
+
this.chunks.push(this.curChunk)
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
TextBox.Paragraph.Alignment = Object.freeze({
|
|
732
|
+
LEFT: 0,
|
|
733
|
+
CENTER: 1,
|
|
734
|
+
RIGHT: 2,
|
|
735
|
+
JUSTIFY: 3
|
|
736
|
+
})
|
|
737
|
+
|
|
738
|
+
TextBox.Paragraph.Chunk = class {
|
|
739
|
+
/**
|
|
740
|
+
* @param {TextBox.Paragraph} paragraph
|
|
741
|
+
* @param {number} fontSize
|
|
742
|
+
* @param {?TextBox.Paragraph.Chunk} prevChunk
|
|
743
|
+
*/
|
|
744
|
+
constructor(paragraph, fontSize, prevChunk) {
|
|
745
|
+
this.paragraph = paragraph
|
|
746
|
+
this.fontSize = fontSize
|
|
747
|
+
this.prevChunk = prevChunk
|
|
748
|
+
this.lastChar = null
|
|
749
|
+
this.lastShape = null
|
|
750
|
+
this.leadingSpaces = 0
|
|
751
|
+
this.spaceStartKerning = null
|
|
752
|
+
this.spaceEndKerning = null
|
|
753
|
+
this.block = null
|
|
754
|
+
this.position = null
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
PushSpace() {
|
|
758
|
+
if (this.block) {
|
|
759
|
+
throw new Error("Illegal operation")
|
|
760
|
+
}
|
|
761
|
+
this.leadingSpaces++
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* @param char {string}
|
|
766
|
+
* @param shape {CharShape}
|
|
767
|
+
*/
|
|
768
|
+
PushChar(char, shape) {
|
|
769
|
+
if (this.spaceStartKerning === null) {
|
|
770
|
+
if (this.leadingSpaces === 0) {
|
|
771
|
+
this.spaceStartKerning = 0
|
|
772
|
+
this.spaceEndKerning = 0
|
|
773
|
+
} else {
|
|
774
|
+
if (this.prevChunk && this.prevChunk.lastShape &&
|
|
775
|
+
this.prevChunk.fontSize === this.fontSize &&
|
|
776
|
+
this.prevChunk.lastShape.font === this.paragraph.textBox.spaceShape.font) {
|
|
777
|
+
|
|
778
|
+
this.spaceStartKerning =
|
|
779
|
+
this.prevChunk.lastShape.font.GetKerning(this.prevChunk.lastChar, " ")
|
|
780
|
+
} else {
|
|
781
|
+
this.spaceStartKerning = 0
|
|
782
|
+
}
|
|
783
|
+
if (shape.font === this.paragraph.textBox.spaceShape.font) {
|
|
784
|
+
this.spaceEndKerning = shape.font.GetKerning(" ", char)
|
|
785
|
+
} else {
|
|
786
|
+
this.spaceEndKerning = 0
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (this.block === null) {
|
|
792
|
+
this.block = new TextBlock(this.fontSize)
|
|
793
|
+
}
|
|
794
|
+
this.block.PushChar(char, shape)
|
|
795
|
+
|
|
796
|
+
this.lastChar = char
|
|
797
|
+
this.lastShape = shape
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
GetSpacingWidth() {
|
|
801
|
+
return (this.leadingSpaces * this.paragraph.textBox.spaceShape.advance +
|
|
802
|
+
this.spaceStartKerning + this.spaceEndKerning) * this.fontSize
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
GetWidth(withSpacing) {
|
|
806
|
+
if (this.block === null) {
|
|
807
|
+
return 0
|
|
808
|
+
}
|
|
809
|
+
let width = this.block.GetCurrentPosition()
|
|
810
|
+
if (withSpacing) {
|
|
811
|
+
width += this.GetSpacingWidth()
|
|
812
|
+
}
|
|
813
|
+
return width
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
TextBox.Paragraph.Line = class {
|
|
818
|
+
constructor(paragraph, startChunkIdx, numChunks, width) {
|
|
819
|
+
this.paragraph = paragraph
|
|
820
|
+
this.startChunkIdx = startChunkIdx
|
|
821
|
+
this.numChunks = numChunks
|
|
822
|
+
this.width = width
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
ApplyAlignment(boxWidth, defaultAlignment) {
|
|
826
|
+
let alignment = this.paragraph.alignment ?? defaultAlignment
|
|
827
|
+
switch (alignment) {
|
|
828
|
+
case TextBox.Paragraph.Alignment.LEFT:
|
|
829
|
+
break
|
|
830
|
+
case TextBox.Paragraph.Alignment.CENTER: {
|
|
831
|
+
const offset = (boxWidth - this.width) / 2
|
|
832
|
+
this.ForEachChunk(chunk => chunk.position += offset)
|
|
833
|
+
break
|
|
834
|
+
}
|
|
835
|
+
case TextBox.Paragraph.Alignment.RIGHT: {
|
|
836
|
+
const offset = boxWidth - this.width
|
|
837
|
+
this.ForEachChunk(chunk => chunk.position += offset)
|
|
838
|
+
break
|
|
839
|
+
}
|
|
840
|
+
case TextBox.Paragraph.Alignment.JUSTIFY: {
|
|
841
|
+
const space = boxWidth - this.width
|
|
842
|
+
if (space <= 0 || this.numChunks === 1) {
|
|
843
|
+
break
|
|
844
|
+
}
|
|
845
|
+
const step = space / (this.numChunks - 1)
|
|
846
|
+
let offset = 0
|
|
847
|
+
this.ForEachChunk(chunk => {
|
|
848
|
+
chunk.position += offset
|
|
849
|
+
offset += step
|
|
850
|
+
})
|
|
851
|
+
break
|
|
852
|
+
}
|
|
853
|
+
default:
|
|
854
|
+
throw new Error("Unhandled alignment: " + this.paragraph.alignment)
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
ForEachChunk(handler) {
|
|
859
|
+
for (let i = 0; i < this.numChunks; i++) {
|
|
860
|
+
handler(this.paragraph.chunks[this.startChunkIdx + i])
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/** Encapsulates calculations for a single-line text block. */
|
|
866
|
+
class TextBlock {
|
|
867
|
+
constructor(fontSize) {
|
|
868
|
+
this.fontSize = fontSize
|
|
869
|
+
/* Element is {shape: CharShape, vertices: ?{Vector2}[]} */
|
|
870
|
+
this.glyphs = []
|
|
871
|
+
this.bounds = null
|
|
872
|
+
this.curX = 0
|
|
873
|
+
this.prevChar = null
|
|
874
|
+
this.prevFont = null
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* @param char {string}
|
|
879
|
+
* @param shape {CharShape}
|
|
880
|
+
*/
|
|
881
|
+
PushChar(char, shape) {
|
|
882
|
+
/* Initially store with just font size and characters position applied. Origin is the first
|
|
883
|
+
* character base point.
|
|
884
|
+
*/
|
|
885
|
+
let offset
|
|
886
|
+
if (this.prevChar !== null && this.prevFont === shape.font) {
|
|
887
|
+
offset = this.prevFont.GetKerning(this.prevChar, char)
|
|
888
|
+
} else {
|
|
889
|
+
offset = 0
|
|
890
|
+
}
|
|
891
|
+
const x = this.curX + offset * this.fontSize
|
|
892
|
+
let vertices
|
|
893
|
+
if (shape.vertices) {
|
|
894
|
+
vertices = shape.GetVertices({x, y: 0}, this.fontSize)
|
|
895
|
+
const xMin = x + shape.bounds.xMin * this.fontSize
|
|
896
|
+
const xMax = x + shape.bounds.xMax * this.fontSize
|
|
897
|
+
const yMin = shape.bounds.yMin * this.fontSize
|
|
898
|
+
const yMax = shape.bounds.yMax * this.fontSize
|
|
899
|
+
/* Leading/trailing spaces not accounted intentionally now. */
|
|
900
|
+
if (this.bounds === null) {
|
|
901
|
+
this.bounds = {xMin, xMax, yMin, yMax}
|
|
902
|
+
} else {
|
|
903
|
+
if (xMin < this.bounds.xMin) {
|
|
904
|
+
this.bounds.xMin = xMin
|
|
905
|
+
}
|
|
906
|
+
if (yMin < this.bounds.yMin) {
|
|
907
|
+
this.bounds.yMin = yMin
|
|
908
|
+
}
|
|
909
|
+
if (xMax > this.bounds.xMax) {
|
|
910
|
+
this.bounds.xMax = xMax
|
|
911
|
+
}
|
|
912
|
+
if (yMax > this.bounds.yMax) {
|
|
913
|
+
this.bounds.yMax = yMax
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
} else {
|
|
917
|
+
vertices = null
|
|
918
|
+
}
|
|
919
|
+
this.curX = x + shape.advance * this.fontSize
|
|
920
|
+
this.glyphs.push({shape, vertices})
|
|
921
|
+
this.prevChar = char
|
|
922
|
+
this.prevFont = shape.font
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
GetCurrentPosition() {
|
|
926
|
+
return this.curX
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* @param startPos {{x,y}} TEXT group first alignment point.
|
|
931
|
+
* @param endPos {?{x,y}} TEXT group second alignment point.
|
|
932
|
+
* @param rotation {?number} Rotation attribute, deg.
|
|
933
|
+
* @param widthFactor {?number} Relative X scale factor (group 41).
|
|
934
|
+
* @param hAlign {?number} Horizontal text justification type code (group 72).
|
|
935
|
+
* @param vAlign {?number} Vertical text justification type code (group 73).
|
|
936
|
+
* @param color {number}
|
|
937
|
+
* @param layer {?string}
|
|
938
|
+
* @return {Generator<Entity>} Rendering entities. Currently just indexed triangles for each
|
|
939
|
+
* glyph.
|
|
940
|
+
*/
|
|
941
|
+
*Render(startPos, endPos, rotation, widthFactor, hAlign, vAlign, color, layer) {
|
|
942
|
+
|
|
943
|
+
if (this.bounds === null) {
|
|
944
|
+
return
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
endPos = endPos ?? startPos
|
|
948
|
+
if (rotation) {
|
|
949
|
+
rotation *= -Math.PI / 180
|
|
950
|
+
} else {
|
|
951
|
+
rotation = 0
|
|
952
|
+
}
|
|
953
|
+
widthFactor = widthFactor ?? 1
|
|
954
|
+
hAlign = hAlign ?? HAlign.LEFT
|
|
955
|
+
vAlign = vAlign ?? VAlign.BASELINE
|
|
956
|
+
|
|
957
|
+
let origin = new Vector2()
|
|
958
|
+
let scale = new Vector2(widthFactor, 1)
|
|
959
|
+
let insertionPos =
|
|
960
|
+
(hAlign === HAlign.LEFT && vAlign === VAlign.BASELINE) ||
|
|
961
|
+
hAlign === HAlign.FIT || hAlign === HAlign.ALIGNED ?
|
|
962
|
+
new Vector2(startPos.x, startPos.y) : new Vector2(endPos.x, endPos.y)
|
|
963
|
+
|
|
964
|
+
const GetFitScale = () => {
|
|
965
|
+
const width = endPos.x - startPos.x
|
|
966
|
+
if (width < Number.MIN_VALUE * 2) {
|
|
967
|
+
return widthFactor
|
|
968
|
+
}
|
|
969
|
+
return width / (this.bounds.xMax - this.bounds.xMin)
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const GetFitRotation = () => {
|
|
973
|
+
return -Math.atan2(endPos.y - startPos.y, endPos.x - startPos.x)
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
switch (hAlign) {
|
|
977
|
+
case HAlign.LEFT:
|
|
978
|
+
origin.x = this.bounds.xMin
|
|
979
|
+
break
|
|
980
|
+
case HAlign.CENTER:
|
|
981
|
+
origin.x = (this.bounds.xMax - this.bounds.xMin) / 2
|
|
982
|
+
break
|
|
983
|
+
case HAlign.RIGHT:
|
|
984
|
+
origin.x = this.bounds.xMax
|
|
985
|
+
break
|
|
986
|
+
case HAlign.MIDDLE:
|
|
987
|
+
origin.x = (this.bounds.xMax - this.bounds.xMin) / 2
|
|
988
|
+
origin.y = (this.bounds.yMax - this.bounds.yMin) / 2
|
|
989
|
+
break
|
|
990
|
+
case HAlign.ALIGNED: {
|
|
991
|
+
const f = GetFitScale()
|
|
992
|
+
scale.x = f
|
|
993
|
+
scale.y = f
|
|
994
|
+
rotation = GetFitRotation()
|
|
995
|
+
break
|
|
996
|
+
}
|
|
997
|
+
case HAlign.FIT:
|
|
998
|
+
scale.x = GetFitScale()
|
|
999
|
+
rotation = GetFitRotation()
|
|
1000
|
+
break
|
|
1001
|
+
default:
|
|
1002
|
+
console.warn("Unrecognized hAlign value: " + hAlign)
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
switch (vAlign) {
|
|
1006
|
+
case VAlign.BASELINE:
|
|
1007
|
+
break
|
|
1008
|
+
case VAlign.BOTTOM:
|
|
1009
|
+
origin.y = this.bounds.yMin
|
|
1010
|
+
break
|
|
1011
|
+
case VAlign.MIDDLE:
|
|
1012
|
+
origin.y = (this.bounds.yMax - this.bounds.yMin) / 2
|
|
1013
|
+
break
|
|
1014
|
+
case VAlign.TOP:
|
|
1015
|
+
origin.y = this.bounds.yMax
|
|
1016
|
+
break
|
|
1017
|
+
default:
|
|
1018
|
+
console.warn("Unrecognized vAlign value: " + vAlign)
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
const transform = new Matrix3().translate(-origin.x, -origin.y).scale(scale.x, scale.y)
|
|
1022
|
+
.rotate(rotation).translate(insertionPos.x, insertionPos.y)
|
|
1023
|
+
|
|
1024
|
+
for (const glyph of this.glyphs) {
|
|
1025
|
+
if (glyph.vertices) {
|
|
1026
|
+
for (const v of glyph.vertices) {
|
|
1027
|
+
v.applyMatrix3(transform)
|
|
1028
|
+
}
|
|
1029
|
+
yield new Entity({
|
|
1030
|
+
type: Entity.Type.TRIANGLES,
|
|
1031
|
+
vertices: glyph.vertices,
|
|
1032
|
+
indices: glyph.shape.indices,
|
|
1033
|
+
layer, color
|
|
1034
|
+
})
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|