@geastack/native-webgl-angle 0.1.11 → 0.1.12
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/geatsc-plugin-readback.mjs +25 -0
- package/geatsc-plugin.mjs +7 -1
- package/native/angle_webgl_host.mm +50 -1
- package/native/webgl_readback.h +29 -0
- package/package.json +9 -7
- package/src/inflate.ts +252 -0
- package/src/nativeWebGL.ts +24 -7
- package/src/nativeWebGLHost.ts +9 -2
- package/src/text/atlas-sans.ts +19 -178
- package/src/text/atlas-serif-italic.ts +26 -178
- package/src/text/atlas-serif.ts +22 -178
- package/src/text/native-text-types.ts +28 -0
- package/src/text/native-text.ts +154 -50
- package/src/troika-three-text.ts +49 -4
- package/NOTICE +0 -4
|
@@ -13,5 +13,33 @@ export interface GlyphAtlas {
|
|
|
13
13
|
first: number
|
|
14
14
|
glyphs: number[][]
|
|
15
15
|
kerning: number[][]
|
|
16
|
+
/** The alpha bitmap is raw DEFLATE (zlib deflateRawSync) before base64. */
|
|
17
|
+
alphaPacked: boolean
|
|
16
18
|
alphaBase64: string[]
|
|
17
19
|
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Metric rows from their packed spelling, `width` comma-separated numbers per
|
|
23
|
+
* row. A generated atlas writes its glyph and kerning tables this way because a
|
|
24
|
+
* nested array literal compiles to code building every row -- 470 KB of it for
|
|
25
|
+
* one font's 1078 kerning pairs -- while a string is plain read-only data.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* The `alphaBase64` of an atlas whose bitmap the build does not carry
|
|
29
|
+
* (native-text.ts reads it from setAtlasAlphaSource instead). One typed array
|
|
30
|
+
* rather than a `[]` literal per atlas: a bundler inlines the literal, and an
|
|
31
|
+
* empty literal says nothing about its element type.
|
|
32
|
+
*/
|
|
33
|
+
export const ALPHA_ELSEWHERE: string[] = []
|
|
34
|
+
|
|
35
|
+
export function unpackRows(width: number, packed: string): number[][] {
|
|
36
|
+
const rows: number[][] = []
|
|
37
|
+
if (packed.length === 0) return rows
|
|
38
|
+
const fields = packed.split(',')
|
|
39
|
+
for (let i = 0; i + width <= fields.length; i += width) {
|
|
40
|
+
const row: number[] = []
|
|
41
|
+
for (let k = 0; k < width; k++) row.push(Number(fields[i + k]))
|
|
42
|
+
rows.push(row)
|
|
43
|
+
}
|
|
44
|
+
return rows
|
|
45
|
+
}
|
package/src/text/native-text.ts
CHANGED
|
@@ -6,15 +6,18 @@
|
|
|
6
6
|
import { BufferAttribute } from 'three/src/core/BufferAttribute.js'
|
|
7
7
|
import { BufferGeometry } from 'three/src/core/BufferGeometry.js'
|
|
8
8
|
import { DataTexture } from 'three/src/textures/DataTexture.js'
|
|
9
|
+
import type { Texture } from 'three/src/textures/Texture.js'
|
|
9
10
|
import {
|
|
10
11
|
ClampToEdgeWrapping,
|
|
11
12
|
LinearFilter,
|
|
12
13
|
NoColorSpace,
|
|
13
|
-
|
|
14
|
+
RedFormat,
|
|
15
|
+
RGFormat,
|
|
14
16
|
UnsignedByteType,
|
|
15
17
|
UVMapping,
|
|
16
18
|
} from 'three/src/constants.js'
|
|
17
19
|
import type { GlyphAtlas } from './native-text-types'
|
|
20
|
+
import { base64Bytes, inflateRaw } from '../inflate'
|
|
18
21
|
export interface LaidOutText {
|
|
19
22
|
geometry: BufferGeometry
|
|
20
23
|
positions: Float32Array
|
|
@@ -30,66 +33,73 @@ export interface LaidOutText {
|
|
|
30
33
|
const textureCacheKeys: string[] = []
|
|
31
34
|
const textureCacheValues: DataTexture[] = []
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return 0
|
|
36
|
+
// Where an atlas's alpha bitmap comes from when the build carries none
|
|
37
|
+
// (`alphaBase64` is empty): a target can keep its atlases outside the app image
|
|
38
|
+
// -- the ESP-Mosaico on its SPI NAND -- and set this to read one by atlas name.
|
|
39
|
+
// The bytes are the bitmap as baked: raw DEFLATE when `alphaPacked`.
|
|
40
|
+
let atlasAlphaSource: (name: string) => Uint8Array = (name: string): Uint8Array => {
|
|
41
|
+
throw new Error(`text: no alpha bitmap for atlas ${name}: this build carries none and no source is set`)
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
export function setAtlasAlphaSource(source: (name: string) => Uint8Array): void {
|
|
45
|
+
atlasAlphaSource = source
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// A renderer that can sample one byte per texel as both red and green (gea-threejs:
|
|
49
|
+
// TextureUserData.redAsGreen) keeps an atlas at one byte per texel instead of two,
|
|
50
|
+
// and the bitmap read from the source is the texture's own storage (no copy).
|
|
51
|
+
let singleByteAtlases = false
|
|
52
|
+
|
|
53
|
+
export function useSingleByteAtlases(): void {
|
|
54
|
+
singleByteAtlases = true
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Decode the alpha bitmap into the glyph coverage texture, built once per atlas
|
|
58
|
+
// and shared by every text instance. It is the material's alphaMap, which
|
|
59
|
+
// reads green, so the texture is red-green (r = g = coverage): two bytes per
|
|
60
|
+
// texel where a white RGBA map took four, and a renderer that keeps byte
|
|
61
|
+
// images at their own size (gea-threejs) holds half of what it did. The
|
|
62
|
+
// bytes are dropped once uploaded -- nothing re-uploads an atlas.
|
|
44
63
|
export function atlasTexture(atlas: GlyphAtlas): DataTexture {
|
|
45
64
|
for (let i = 0; i < textureCacheKeys.length; i++) {
|
|
46
65
|
if (textureCacheKeys[i] === atlas.name) return textureCacheValues[i]
|
|
47
66
|
}
|
|
48
67
|
const total = atlas.atlasWidth * atlas.atlasHeight
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (code3 !== 61) {
|
|
79
|
-
const a2 = ((v2 & 3) << 6) | v3
|
|
80
|
-
rgba[out] = 255
|
|
81
|
-
rgba[out + 1] = 255
|
|
82
|
-
rgba[out + 2] = 255
|
|
83
|
-
rgba[out + 3] = a2
|
|
84
|
-
out += 4
|
|
85
|
-
}
|
|
86
|
-
}
|
|
68
|
+
const encoded = atlas.alphaBase64.length === 0 ? atlasAlphaSource(atlas.name) : base64Bytes(atlas.alphaBase64)
|
|
69
|
+
const alpha = atlas.alphaPacked ? inflateRaw(encoded, total) : encoded
|
|
70
|
+
if (singleByteAtlases) {
|
|
71
|
+
const single = new DataTexture(
|
|
72
|
+
alpha,
|
|
73
|
+
atlas.atlasWidth,
|
|
74
|
+
atlas.atlasHeight,
|
|
75
|
+
RedFormat,
|
|
76
|
+
UnsignedByteType,
|
|
77
|
+
UVMapping,
|
|
78
|
+
ClampToEdgeWrapping,
|
|
79
|
+
ClampToEdgeWrapping,
|
|
80
|
+
LinearFilter,
|
|
81
|
+
LinearFilter,
|
|
82
|
+
1,
|
|
83
|
+
NoColorSpace,
|
|
84
|
+
)
|
|
85
|
+
single.userData.redAsGreen = true
|
|
86
|
+
single.unpackAlignment = 1
|
|
87
|
+
single.onUpdate = releaseAtlasBytes
|
|
88
|
+
single.needsUpdate = true
|
|
89
|
+
textureCacheKeys.push(atlas.name)
|
|
90
|
+
textureCacheValues.push(single)
|
|
91
|
+
return single
|
|
92
|
+
}
|
|
93
|
+
const coverage = new Uint8Array(total * 2)
|
|
94
|
+
for (let i = 0; i < total; i++) {
|
|
95
|
+
coverage[i * 2] = alpha[i]
|
|
96
|
+
coverage[i * 2 + 1] = alpha[i]
|
|
87
97
|
}
|
|
88
98
|
const tex = new DataTexture(
|
|
89
|
-
|
|
99
|
+
coverage,
|
|
90
100
|
atlas.atlasWidth,
|
|
91
101
|
atlas.atlasHeight,
|
|
92
|
-
|
|
102
|
+
RGFormat,
|
|
93
103
|
UnsignedByteType,
|
|
94
104
|
UVMapping,
|
|
95
105
|
ClampToEdgeWrapping,
|
|
@@ -99,12 +109,22 @@ export function atlasTexture(atlas: GlyphAtlas): DataTexture {
|
|
|
99
109
|
1,
|
|
100
110
|
NoColorSpace,
|
|
101
111
|
)
|
|
112
|
+
// Rows of width * 2 bytes: tightly packed for any even width.
|
|
113
|
+
tex.unpackAlignment = 2
|
|
114
|
+
tex.onUpdate = releaseAtlasBytes
|
|
102
115
|
tex.needsUpdate = true
|
|
103
116
|
textureCacheKeys.push(atlas.name)
|
|
104
117
|
textureCacheValues.push(tex)
|
|
105
118
|
return tex
|
|
106
119
|
}
|
|
107
120
|
|
|
121
|
+
|
|
122
|
+
function releaseAtlasBytes(texture: Texture): void {
|
|
123
|
+
if (!(texture instanceof DataTexture)) return
|
|
124
|
+
const image = texture.image
|
|
125
|
+
if (image !== null) image.data = new Uint8Array(0)
|
|
126
|
+
}
|
|
127
|
+
|
|
108
128
|
const kerningCacheKeys: string[] = []
|
|
109
129
|
const kerningCacheValues: Map<number, number>[] = []
|
|
110
130
|
|
|
@@ -121,6 +141,90 @@ function kerningTable(atlas: GlyphAtlas): Map<number, number> {
|
|
|
121
141
|
return table
|
|
122
142
|
}
|
|
123
143
|
|
|
144
|
+
// layoutText's quads written into existing position (12 per quad) and uv
|
|
145
|
+
// (8 per quad) arrays, when `text` lays out to exactly as many quads as they
|
|
146
|
+
// hold: a score or FPS readout changing digits reuses its geometry instead of
|
|
147
|
+
// building a new one. Returns the laid-out width, or -1 (nothing written in a
|
|
148
|
+
// way the caller may keep) when the quad count differs.
|
|
149
|
+
export function layoutTextInPlace(
|
|
150
|
+
atlas: GlyphAtlas,
|
|
151
|
+
text: string,
|
|
152
|
+
fontSize: number,
|
|
153
|
+
letterSpacing: number,
|
|
154
|
+
positions: Float32Array,
|
|
155
|
+
uvs: Float32Array,
|
|
156
|
+
): number {
|
|
157
|
+
const quads = positions.length / 12
|
|
158
|
+
if (uvs.length !== quads * 8) return -1
|
|
159
|
+
let n = 0
|
|
160
|
+
for (let i = 0; i < text.length; i++) {
|
|
161
|
+
const code = text.charCodeAt(i)
|
|
162
|
+
if (code === 10) return -1
|
|
163
|
+
const index = code - atlas.first
|
|
164
|
+
if (index < 0 || index >= atlas.glyphs.length) continue
|
|
165
|
+
const g = atlas.glyphs[index]
|
|
166
|
+
if (g[2] > g[0] && g[3] > g[1]) n++
|
|
167
|
+
}
|
|
168
|
+
if (n !== quads) return -1
|
|
169
|
+
const scale = fontSize / atlas.pixelHeight
|
|
170
|
+
const kern = kerningTable(atlas)
|
|
171
|
+
const invW = 1 / atlas.atlasWidth
|
|
172
|
+
const invH = 1 / atlas.atlasHeight
|
|
173
|
+
let penX = 0
|
|
174
|
+
let prev = 0
|
|
175
|
+
let q = 0
|
|
176
|
+
for (let i = 0; i < text.length; i++) {
|
|
177
|
+
const code = text.charCodeAt(i)
|
|
178
|
+
const index = code - atlas.first
|
|
179
|
+
if (index < 0 || index >= atlas.glyphs.length) continue
|
|
180
|
+
if (prev !== 0) {
|
|
181
|
+
const k = kern.get(prev * 1024 + code)
|
|
182
|
+
if (k !== undefined) penX += k * scale
|
|
183
|
+
}
|
|
184
|
+
const g = atlas.glyphs[index]
|
|
185
|
+
const gx0 = g[0]
|
|
186
|
+
const gy0 = g[1]
|
|
187
|
+
const gx1 = g[2]
|
|
188
|
+
const gy1 = g[3]
|
|
189
|
+
if (gx1 > gx0 && gy1 > gy0) {
|
|
190
|
+
const x0 = penX + g[4] * scale
|
|
191
|
+
const y1 = -g[5] * scale
|
|
192
|
+
const x1 = x0 + (gx1 - gx0) * scale
|
|
193
|
+
const y0 = y1 - (gy1 - gy0) * scale
|
|
194
|
+
const p = q * 12
|
|
195
|
+
positions[p] = x0
|
|
196
|
+
positions[p + 1] = y0
|
|
197
|
+
positions[p + 2] = 0
|
|
198
|
+
positions[p + 3] = x1
|
|
199
|
+
positions[p + 4] = y0
|
|
200
|
+
positions[p + 5] = 0
|
|
201
|
+
positions[p + 6] = x1
|
|
202
|
+
positions[p + 7] = y1
|
|
203
|
+
positions[p + 8] = 0
|
|
204
|
+
positions[p + 9] = x0
|
|
205
|
+
positions[p + 10] = y1
|
|
206
|
+
positions[p + 11] = 0
|
|
207
|
+
const u0 = gx0 * invW
|
|
208
|
+
const u1 = gx1 * invW
|
|
209
|
+
const v0 = gy1 * invH
|
|
210
|
+
const v1 = gy0 * invH
|
|
211
|
+
const t = q * 8
|
|
212
|
+
uvs[t] = u0
|
|
213
|
+
uvs[t + 1] = v0
|
|
214
|
+
uvs[t + 2] = u1
|
|
215
|
+
uvs[t + 3] = v0
|
|
216
|
+
uvs[t + 4] = u1
|
|
217
|
+
uvs[t + 5] = v1
|
|
218
|
+
uvs[t + 6] = u0
|
|
219
|
+
uvs[t + 7] = v1
|
|
220
|
+
q++
|
|
221
|
+
}
|
|
222
|
+
penX += g[6] * scale + letterSpacing * fontSize
|
|
223
|
+
prev = code
|
|
224
|
+
}
|
|
225
|
+
return penX > 0 ? penX : 0
|
|
226
|
+
}
|
|
227
|
+
|
|
124
228
|
// Build quad geometry for `text` at `fontSize` world units per em.
|
|
125
229
|
// letterSpacing is in em units (troika convention). Multi-line via '\n'.
|
|
126
230
|
// The origin is the LEFT edge of the FIRST baseline; the caller applies
|
package/src/troika-three-text.ts
CHANGED
|
@@ -22,7 +22,7 @@ import { BufferGeometry } from 'three/src/core/BufferGeometry.js'
|
|
|
22
22
|
import { MeshBasicMaterial } from 'three/src/materials/MeshBasicMaterial.js'
|
|
23
23
|
import { Object3D } from 'three/src/core/Object3D.js'
|
|
24
24
|
import type { Texture } from 'three/src/textures/Texture.js'
|
|
25
|
-
import { atlasTexture, layoutText } from './text/native-text'
|
|
25
|
+
import { atlasTexture, layoutText, layoutTextInPlace } from './text/native-text'
|
|
26
26
|
import type { GlyphAtlas } from './text/native-text-types'
|
|
27
27
|
import { sansAtlas } from './text/atlas-sans'
|
|
28
28
|
import { serifAtlas } from './text/atlas-serif'
|
|
@@ -164,7 +164,13 @@ export class TroikaText extends Object3D {
|
|
|
164
164
|
return
|
|
165
165
|
}
|
|
166
166
|
const atlas = atlasForFont(this.font)
|
|
167
|
+
const sameShape = this.inPlace(atlas, atlasKey)
|
|
167
168
|
this.layoutKey = nextLayoutKey
|
|
169
|
+
if (sameShape) {
|
|
170
|
+
setMaterialHex(this.material, this.color)
|
|
171
|
+
this.material.opacity = this.fillOpacity
|
|
172
|
+
return
|
|
173
|
+
}
|
|
168
174
|
const laidOut = layoutText(atlas, this.text, this.fontSize, this.letterSpacing)
|
|
169
175
|
// Anchor offsets: troika's anchorX left|center|right and anchorY
|
|
170
176
|
// top|middle|bottom (the subset the app uses). The layout origin is the
|
|
@@ -227,10 +233,11 @@ export class TroikaText extends Object3D {
|
|
|
227
233
|
}
|
|
228
234
|
// Keep both sides of the identity comparison on the shared Texture
|
|
229
235
|
// carrier. Otherwise a derived DataTexture local can be incorrectly
|
|
230
|
-
// treated as disjoint from Material.
|
|
236
|
+
// treated as disjoint from Material.alphaMap during native constant folding.
|
|
237
|
+
this.laidAtlas = atlasKey
|
|
231
238
|
const nextMap: Texture = atlasTexture(atlas)
|
|
232
|
-
if (this.material.
|
|
233
|
-
this.material.
|
|
239
|
+
if (this.material.alphaMap !== nextMap) {
|
|
240
|
+
this.material.alphaMap = nextMap
|
|
234
241
|
// needsUpdate recompiles the shader program; only pay that when the
|
|
235
242
|
// map binding actually changed, not on every per-frame sync().
|
|
236
243
|
this.material.needsUpdate = true
|
|
@@ -239,6 +246,42 @@ export class TroikaText extends Object3D {
|
|
|
239
246
|
this.material.opacity = this.fillOpacity
|
|
240
247
|
}
|
|
241
248
|
|
|
249
|
+
// Only the text changed and lays out to as many quads as the current
|
|
250
|
+
// geometry holds (a counter changing digits): rewrite its positions and
|
|
251
|
+
// uvs in place. The index pattern of n quads does not depend on the glyphs.
|
|
252
|
+
private laidAtlas = ''
|
|
253
|
+
private inPlace(atlas: GlyphAtlas, atlasKey: string): boolean {
|
|
254
|
+
if (atlasKey !== this.laidAtlas) return false
|
|
255
|
+
const geometry: BufferGeometry = this.mesh.geometry
|
|
256
|
+
const position = geometry.getAttribute('position')
|
|
257
|
+
const uv = geometry.getAttribute('uv')
|
|
258
|
+
if (!(position instanceof BufferAttribute) || !(uv instanceof BufferAttribute)) return false
|
|
259
|
+
const positions = position.array
|
|
260
|
+
const uvs = uv.array
|
|
261
|
+
if (!(positions instanceof Float32Array) || !(uvs instanceof Float32Array) || positions.length === 0) return false
|
|
262
|
+
const width = layoutTextInPlace(atlas, this.text, this.fontSize, this.letterSpacing, positions, uvs)
|
|
263
|
+
if (width < 0) return false
|
|
264
|
+
const scale = this.fontSize / atlas.pixelHeight
|
|
265
|
+
const ascent = atlas.ascent * scale
|
|
266
|
+
const descent = atlas.descent * scale
|
|
267
|
+
let offsetX = 0
|
|
268
|
+
if (this.anchorX === 'center') offsetX = -width / 2
|
|
269
|
+
else if (this.anchorX === 'right') offsetX = -width
|
|
270
|
+
let offsetY = 0
|
|
271
|
+
if (this.anchorY === 'middle') offsetY = -(ascent + descent) / 2
|
|
272
|
+
else if (this.anchorY === 'top') offsetY = -ascent
|
|
273
|
+
else if (this.anchorY === 'bottom') offsetY = -descent
|
|
274
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
275
|
+
positions[i] += offsetX
|
|
276
|
+
positions[i + 1] += offsetY
|
|
277
|
+
}
|
|
278
|
+
position.needsUpdate = true
|
|
279
|
+
uv.needsUpdate = true
|
|
280
|
+
geometry.boundingSphere = null
|
|
281
|
+
geometry.boundingBox = null
|
|
282
|
+
return true
|
|
283
|
+
}
|
|
284
|
+
|
|
242
285
|
dispose(): void {
|
|
243
286
|
if (this.mesh.geometry) this.mesh.geometry.dispose()
|
|
244
287
|
this.material.dispose()
|
|
@@ -246,3 +289,5 @@ export class TroikaText extends Object3D {
|
|
|
246
289
|
}
|
|
247
290
|
|
|
248
291
|
export { TroikaText as Text }
|
|
292
|
+
// A target that keeps atlas bitmaps outside its image (see native-text.ts).
|
|
293
|
+
export { setAtlasAlphaSource, useSingleByteAtlases } from './text/native-text'
|
package/NOTICE
DELETED