@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.
Files changed (124) hide show
  1. package/css/std/controls/checkbox/checkbox.css +4 -0
  2. package/css/std/controls/checkbox/img/checkbox-indeterminate.svg +4 -0
  3. package/css/std/controls/date-picker/date-picker.css +1 -25
  4. package/css/std/controls/dxf-viewer/annotation.css +85 -0
  5. package/css/std/controls/dxf-viewer/common.css +24 -0
  6. package/css/std/controls/dxf-viewer/dxf-viewer-index.css +14081 -0
  7. package/css/std/controls/dxf-viewer/dxf-viewer.css +194 -0
  8. package/css/std/controls/dxf-viewer/img/cancel-dark-grey.svg +5 -0
  9. package/css/std/controls/dxf-viewer/img/collapse-bottom.svg +5 -0
  10. package/css/std/controls/dxf-viewer/img/collapse-up-blue.svg +5 -0
  11. package/css/std/controls/dxf-viewer/img/delete-active.svg +11 -0
  12. package/css/std/controls/dxf-viewer/img/delete.svg +11 -0
  13. package/css/std/controls/dxf-viewer/img/draw-annotation.svg +3 -0
  14. package/css/std/controls/dxf-viewer/img/invisible-eye.svg +4 -0
  15. package/css/std/controls/dxf-viewer/img/show-annotation.svg +3 -0
  16. package/css/std/controls/dxf-viewer/img/sidebar-layers-toggle.svg +6 -0
  17. package/css/std/controls/dxf-viewer/img/sidebar-notes-toggle.svg +5 -0
  18. package/css/std/controls/dxf-viewer/img/sidebar-resizer.svg +6 -0
  19. package/css/std/controls/dxf-viewer/img/sidebar-toggle.svg +7 -0
  20. package/css/std/controls/dxf-viewer/img/visible-eye.svg +4 -0
  21. package/css/std/controls/dxf-viewer/img/zoom-in.svg +6 -0
  22. package/css/std/controls/dxf-viewer/img/zoom-out.svg +5 -0
  23. package/css/std/controls/dxf-viewer/layer-list.css +104 -0
  24. package/css/std/controls/dxf-viewer/panel.css +34 -0
  25. package/css/std/controls/dxf-viewer/prop-inspector.css +102 -0
  26. package/css/std/controls/dxf-viewer/select.css +111 -0
  27. package/css/std/controls/dxf-viewer/sidebar.css +190 -0
  28. package/css/std/controls/dxf-viewer/thumbnail-list.css +65 -0
  29. package/css/std/controls/dxf-viewer/toolbar.css +117 -0
  30. package/css/std/controls/dxf-viewer/treeview.css +3 -0
  31. package/css/std/controls/dxf-viewer/treeview.panel.css +108 -0
  32. package/css/std/controls/error-message/error-message.css +22 -0
  33. package/css/std/controls/image-picker/image-picker.css +0 -26
  34. package/css/std/controls/input/input.css +1 -24
  35. package/css/std/controls/issue-tile/issue-tile-header.css +1 -0
  36. package/css/std/controls/login/ntlm-authentication-form.css +9 -12
  37. package/css/std/controls/lookup-picker/lookup-picker-value-list.css +38 -2
  38. package/css/std/controls/lookup-picker/lookup-picker.css +1 -25
  39. package/css/std/controls/table-view/treegrid-context-menu.css +44 -18
  40. package/css/std/controls/table-view/treegrid-message.css +4 -4
  41. package/css/std/controls/time-picker/time-picker.css +1 -25
  42. package/dist/index.css +81 -143
  43. package/dist/index.js +15137 -489
  44. package/dist/index.js.map +1 -1
  45. package/dist/src/controls/Checkbox/Checkbox.d.ts +1 -0
  46. package/dist/src/controls/DxfViewer/DxfViewer.d.ts +6 -0
  47. package/dist/src/controls/DxfViewer/DxfViewerContext.d.ts +31 -0
  48. package/dist/src/controls/DxfViewer/Layer.d.ts +9 -0
  49. package/dist/src/controls/DxfViewer/LayerList.d.ts +11 -0
  50. package/dist/src/controls/DxfViewer/Thumbnail.d.ts +7 -0
  51. package/dist/src/controls/DxfViewer/ThumbnailList.d.ts +6 -0
  52. package/dist/src/controls/DxfViewer/Viewer.d.ts +6 -0
  53. package/dist/src/controls/ErrorMessage/ErrorMessage.d.ts +6 -0
  54. package/dist/src/controls/Login/FormRef.d.ts +3 -0
  55. package/dist/src/controls/Login/LoginConstants.d.ts +2 -1
  56. package/dist/src/controls/Login/LoginFormRef.d.ts +2 -2
  57. package/dist/src/controls/Login/NTLMAuthenticationForm.d.ts +5 -2
  58. package/dist/src/controls/LookupPicker/LookupPicker.d.ts +2 -0
  59. package/dist/src/controls/LookupPicker/ValueList.d.ts +2 -0
  60. package/dist/src/controls/TableView/TableViewConstants.d.ts +11 -0
  61. package/dist/src/controls/TableView/TreeGridTableViewContextImpl.d.ts +1 -0
  62. package/dist/src/controls/TreeView/TreeView.d.ts +4 -0
  63. package/dist/src/controls/TreeView/TreeViewConfig.d.ts +3 -0
  64. package/dist/src/controls/TreeView/TreeViewConstants.d.ts +2 -1
  65. package/dist/src/index.d.ts +7 -1
  66. package/lib/dxf-viewer/BatchingKey.js +91 -0
  67. package/lib/dxf-viewer/DxfFetcher.js +39 -0
  68. package/lib/dxf-viewer/DxfScene.js +2695 -0
  69. package/lib/dxf-viewer/DxfViewer.js +1056 -0
  70. package/lib/dxf-viewer/DxfWorker.js +229 -0
  71. package/lib/dxf-viewer/DynamicBuffer.js +100 -0
  72. package/lib/dxf-viewer/HatchCalculator.js +345 -0
  73. package/lib/dxf-viewer/LinearDimension.js +323 -0
  74. package/lib/dxf-viewer/MTextFormatParser.js +211 -0
  75. package/lib/dxf-viewer/MaterialKey.js +37 -0
  76. package/lib/dxf-viewer/OrbitControls.js +1253 -0
  77. package/lib/dxf-viewer/Pattern.js +94 -0
  78. package/lib/dxf-viewer/RBTree.js +471 -0
  79. package/lib/dxf-viewer/TextRenderer.js +1038 -0
  80. package/lib/dxf-viewer/index.js +42 -0
  81. package/lib/dxf-viewer/math/Matrix2.js +77 -0
  82. package/lib/dxf-viewer/math/utils.js +59 -0
  83. package/lib/dxf-viewer/parser/AutoCadColorIndex.js +265 -0
  84. package/lib/dxf-viewer/parser/DimStyleCodes.js +33 -0
  85. package/lib/dxf-viewer/parser/DxfArrayScanner.js +143 -0
  86. package/lib/dxf-viewer/parser/DxfParser.js +980 -0
  87. package/lib/dxf-viewer/parser/ExtendedDataParse-My.js +91 -0
  88. package/lib/dxf-viewer/parser/ExtendedDataParser.js +123 -0
  89. package/lib/dxf-viewer/parser/ParseHelpers.js +142 -0
  90. package/lib/dxf-viewer/parser/entities/3dface.js +83 -0
  91. package/lib/dxf-viewer/parser/entities/arc.js +38 -0
  92. package/lib/dxf-viewer/parser/entities/attdef.js +89 -0
  93. package/lib/dxf-viewer/parser/entities/attrib.js +34 -0
  94. package/lib/dxf-viewer/parser/entities/attribute.js +109 -0
  95. package/lib/dxf-viewer/parser/entities/circle.js +43 -0
  96. package/lib/dxf-viewer/parser/entities/dimension.js +72 -0
  97. package/lib/dxf-viewer/parser/entities/ellipse.js +46 -0
  98. package/lib/dxf-viewer/parser/entities/hatch.js +343 -0
  99. package/lib/dxf-viewer/parser/entities/insert.js +62 -0
  100. package/lib/dxf-viewer/parser/entities/leader.js +84 -0
  101. package/lib/dxf-viewer/parser/entities/line.js +34 -0
  102. package/lib/dxf-viewer/parser/entities/lwpolyline.js +100 -0
  103. package/lib/dxf-viewer/parser/entities/mtext.js +54 -0
  104. package/lib/dxf-viewer/parser/entities/point.js +35 -0
  105. package/lib/dxf-viewer/parser/entities/polyline.js +92 -0
  106. package/lib/dxf-viewer/parser/entities/solid.js +40 -0
  107. package/lib/dxf-viewer/parser/entities/spline.js +70 -0
  108. package/lib/dxf-viewer/parser/entities/text.js +47 -0
  109. package/lib/dxf-viewer/parser/entities/vertex.js +62 -0
  110. package/lib/dxf-viewer/parser/entities/viewport.js +56 -0
  111. package/lib/dxf-viewer/parser/objects/dictionary.js +29 -0
  112. package/lib/dxf-viewer/parser/objects/layout.js +35 -0
  113. package/lib/dxf-viewer/parser/objects/xrecord.js +29 -0
  114. package/lib/opentype/opentype.module.js +14571 -0
  115. package/lib/three/CSS2DRenderer.js +235 -0
  116. package/lib/three/three.module.js +49912 -0
  117. package/package.json +12 -10
  118. package/src/controls/BimViewer/js/bim-viewer.js +2 -2
  119. package/src/controls/DxfViewer/js/dxf-viewer.js +3580 -0
  120. package/src/controls/PdfViewer/js/pdf-viewer.js +1 -1
  121. package/css/std/controls/input/img/error-message.svg +0 -6
  122. package/css/std/controls/lookup-picker/img/error-message.svg +0 -6
  123. package/css/std/controls/time-picker/img/error-message.svg +0 -6
  124. /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
+ }