@bloopjs/toodle 0.1.2 → 0.1.3
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/dist/Toodle.d.ts +1 -1
- package/dist/Toodle.d.ts.map +1 -1
- package/dist/backends/ITextShader.d.ts +15 -0
- package/dist/backends/ITextShader.d.ts.map +1 -0
- package/dist/backends/mod.d.ts +1 -0
- package/dist/backends/mod.d.ts.map +1 -1
- package/dist/backends/webgl2/WebGLTextShader.d.ts +20 -0
- package/dist/backends/webgl2/WebGLTextShader.d.ts.map +1 -0
- package/dist/backends/webgl2/mod.d.ts +1 -0
- package/dist/backends/webgl2/mod.d.ts.map +1 -1
- package/dist/{text → backends/webgpu}/FontPipeline.d.ts +1 -1
- package/dist/backends/webgpu/FontPipeline.d.ts.map +1 -0
- package/dist/{text/TextShader.d.ts → backends/webgpu/WebGPUTextShader.d.ts} +7 -7
- package/dist/backends/webgpu/WebGPUTextShader.d.ts.map +1 -0
- package/dist/backends/webgpu/mod.d.ts +2 -0
- package/dist/backends/webgpu/mod.d.ts.map +1 -1
- package/dist/backends/webgpu/wgsl/text.wgsl.d.ts.map +1 -0
- package/dist/mod.d.ts +1 -1
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +1795 -1777
- package/dist/mod.js.map +15 -14
- package/dist/{text → scene}/TextNode.d.ts +5 -5
- package/dist/scene/TextNode.d.ts.map +1 -0
- package/dist/scene/mod.d.ts +1 -0
- package/dist/scene/mod.d.ts.map +1 -1
- package/dist/text/mod.d.ts +1 -3
- package/dist/text/mod.d.ts.map +1 -1
- package/dist/textures/AssetManager.d.ts +9 -6
- package/dist/textures/AssetManager.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/Toodle.ts +1 -1
- package/src/backends/ITextShader.ts +15 -0
- package/src/backends/mod.ts +1 -0
- package/src/backends/webgl2/WebGLTextShader.ts +35 -0
- package/src/backends/webgl2/mod.ts +1 -0
- package/src/{text → backends/webgpu}/FontPipeline.ts +2 -2
- package/src/{text/TextShader.ts → backends/webgpu/WebGPUTextShader.ts} +14 -10
- package/src/backends/webgpu/mod.ts +2 -0
- package/src/mod.ts +1 -1
- package/src/{text → scene}/TextNode.ts +6 -6
- package/src/scene/mod.ts +1 -0
- package/src/text/mod.ts +1 -4
- package/src/textures/AssetManager.ts +38 -31
- package/dist/text/FontPipeline.d.ts.map +0 -1
- package/dist/text/TextNode.d.ts.map +0 -1
- package/dist/text/TextShader.d.ts.map +0 -1
- package/dist/text/text.wgsl.d.ts.map +0 -1
- /package/dist/{text → backends/webgpu/wgsl}/text.wgsl.d.ts +0 -0
- /package/src/{text → backends/webgpu/wgsl}/text.wgsl.ts +0 -0
package/dist/mod.js
CHANGED
|
@@ -17219,1917 +17219,1932 @@ class WebGPUBackend {
|
|
|
17219
17219
|
return this.#renderPass;
|
|
17220
17220
|
}
|
|
17221
17221
|
}
|
|
17222
|
-
// src/
|
|
17223
|
-
|
|
17224
|
-
|
|
17225
|
-
|
|
17226
|
-
|
|
17227
|
-
|
|
17228
|
-
|
|
17229
|
-
|
|
17230
|
-
|
|
17231
|
-
|
|
17232
|
-
|
|
17233
|
-
|
|
17234
|
-
|
|
17235
|
-
|
|
17236
|
-
mat3.rotate(base, transform.rotation, base);
|
|
17237
|
-
mat3.scale(base, [transform.scale.x, transform.scale.y], base);
|
|
17238
|
-
return base;
|
|
17239
|
-
}
|
|
17240
|
-
function convertScreenToWorld(screenCoordinates, camera, projectionMatrix, resolution) {
|
|
17241
|
-
const inverseViewProjectionMatrix = mat3.mul(mat3.inverse(camera.matrix), mat3.inverse(projectionMatrix));
|
|
17242
|
-
const normalizedDeviceCoordinates = {
|
|
17243
|
-
x: 2 * screenCoordinates.x / resolution.width - 1,
|
|
17244
|
-
y: 1 - 2 * screenCoordinates.y / resolution.height
|
|
17245
|
-
};
|
|
17246
|
-
return transformPoint(normalizedDeviceCoordinates, inverseViewProjectionMatrix);
|
|
17222
|
+
// src/backends/webgl2/WebGLTextShader.ts
|
|
17223
|
+
class WebGLTextShader {
|
|
17224
|
+
label = "text";
|
|
17225
|
+
font;
|
|
17226
|
+
maxCharCount;
|
|
17227
|
+
constructor(font, maxCharCount) {
|
|
17228
|
+
this.font = font;
|
|
17229
|
+
this.maxCharCount = maxCharCount;
|
|
17230
|
+
}
|
|
17231
|
+
startFrame(_uniform) {}
|
|
17232
|
+
processBatch(_nodes) {
|
|
17233
|
+
throw new Error("Text rendering is not supported in WebGL mode. Use WebGPU backend for text rendering.");
|
|
17234
|
+
}
|
|
17235
|
+
endFrame() {}
|
|
17247
17236
|
}
|
|
17248
|
-
|
|
17249
|
-
|
|
17250
|
-
|
|
17251
|
-
|
|
17252
|
-
|
|
17253
|
-
|
|
17254
|
-
|
|
17237
|
+
|
|
17238
|
+
// src/backends/webgpu/wgsl/text.wgsl.ts
|
|
17239
|
+
var text_wgsl_default = `
|
|
17240
|
+
// Adapted from: https://webgpu.github.io/webgpu-samples/?sample=textRenderingMsdf
|
|
17241
|
+
|
|
17242
|
+
// Quad vertex positions for a character
|
|
17243
|
+
const pos = array(
|
|
17244
|
+
vec2f(0, -1),
|
|
17245
|
+
vec2f(1, -1),
|
|
17246
|
+
vec2f(0, 0),
|
|
17247
|
+
vec2f(1, 0),
|
|
17248
|
+
);
|
|
17249
|
+
|
|
17250
|
+
// Debug colors for visualization
|
|
17251
|
+
const debugColors = array(
|
|
17252
|
+
vec4f(1, 0, 0, 1),
|
|
17253
|
+
vec4f(0, 1, 0, 1),
|
|
17254
|
+
vec4f(0, 0, 1, 1),
|
|
17255
|
+
vec4f(1, 1, 1, 1),
|
|
17256
|
+
);
|
|
17257
|
+
|
|
17258
|
+
// Vertex input from GPU
|
|
17259
|
+
struct VertexInput {
|
|
17260
|
+
@builtin(vertex_index) vertex: u32,
|
|
17261
|
+
@builtin(instance_index) instance: u32,
|
|
17262
|
+
};
|
|
17263
|
+
|
|
17264
|
+
// Output from vertex shader to fragment shader
|
|
17265
|
+
struct VertexOutput {
|
|
17266
|
+
@builtin(position) position: vec4f,
|
|
17267
|
+
@location(0) texcoord: vec2f,
|
|
17268
|
+
@location(1) debugColor: vec4f,
|
|
17269
|
+
@location(2) @interpolate(flat) instanceIndex: u32,
|
|
17270
|
+
};
|
|
17271
|
+
|
|
17272
|
+
// Metadata for a single character glyph
|
|
17273
|
+
struct Char {
|
|
17274
|
+
texOffset: vec2f, // Offset to top-left in MSDF texture (pixels)
|
|
17275
|
+
texExtent: vec2f, // Size in texture (pixels)
|
|
17276
|
+
size: vec2f, // Glyph size in ems
|
|
17277
|
+
offset: vec2f, // Position offset in ems
|
|
17278
|
+
};
|
|
17279
|
+
|
|
17280
|
+
// Metadata for a text block
|
|
17281
|
+
struct TextBlockDescriptor {
|
|
17282
|
+
transform: mat3x3f, // Text transform matrix (model matrix)
|
|
17283
|
+
color: vec4f, // Text color
|
|
17284
|
+
fontSize: f32, // Font size
|
|
17285
|
+
blockWidth: f32, // Total width of text block
|
|
17286
|
+
blockHeight: f32, // Total height of text block
|
|
17287
|
+
bufferPosition: f32 // Index and length in textBuffer
|
|
17288
|
+
};
|
|
17289
|
+
|
|
17290
|
+
// Font bindings
|
|
17291
|
+
@group(0) @binding(0) var fontTexture: texture_2d<f32>;
|
|
17292
|
+
@group(0) @binding(1) var fontSampler: sampler;
|
|
17293
|
+
@group(0) @binding(2) var<storage> chars: array<Char>;
|
|
17294
|
+
@group(0) @binding(3) var<uniform> fontData: vec4f; // Contains line height (x)
|
|
17295
|
+
|
|
17296
|
+
// Text bindings
|
|
17297
|
+
@group(1) @binding(0) var<storage> texts: array<TextBlockDescriptor>;
|
|
17298
|
+
@group(1) @binding(1) var<storage> textBuffer: array<vec4f>; // Each vec4: xy = glyph pos, z = char index
|
|
17299
|
+
|
|
17300
|
+
// Global uniforms
|
|
17301
|
+
@group(2) @binding(0) var<uniform> viewProjectionMatrix: mat3x3f;
|
|
17302
|
+
|
|
17303
|
+
// Vertex shader
|
|
17304
|
+
@vertex
|
|
17305
|
+
fn vertexMain(input: VertexInput) -> VertexOutput {
|
|
17306
|
+
// Because the instance index is used for character indexing, we are
|
|
17307
|
+
// overloading the vertex index to store the instance of the text metadata.
|
|
17308
|
+
//
|
|
17309
|
+
// I.e...
|
|
17310
|
+
// Vertex 0-4 = Instance 0, Vertex 0-4
|
|
17311
|
+
// Vertex 4-8 = Instance 1, Vertex 0-4
|
|
17312
|
+
// Vertex 8-12 = Instance 2, Vertex 0-4
|
|
17313
|
+
let vertexIndex = input.vertex % 4;
|
|
17314
|
+
let textIndex = input.vertex / 4;
|
|
17315
|
+
|
|
17316
|
+
let text = texts[textIndex];
|
|
17317
|
+
let textElement = textBuffer[u32(text.bufferPosition) + input.instance];
|
|
17318
|
+
let char = chars[u32(textElement.z)];
|
|
17319
|
+
|
|
17320
|
+
let lineHeight = fontData.x;
|
|
17321
|
+
let textWidth = text.blockWidth;
|
|
17322
|
+
let textHeight = text.blockHeight;
|
|
17323
|
+
|
|
17324
|
+
// Center text vertically; origin is mid-height
|
|
17325
|
+
let offset = vec2f(0, -textHeight / 2);
|
|
17326
|
+
|
|
17327
|
+
// Glyph position in ems (quad pos * size + per-char offset)
|
|
17328
|
+
let emPos = pos[vertexIndex] * char.size + char.offset + textElement.xy - offset;
|
|
17329
|
+
let charPos = emPos * (text.fontSize / lineHeight);
|
|
17330
|
+
|
|
17331
|
+
var output: VertexOutput;
|
|
17332
|
+
let transformedPosition = viewProjectionMatrix * text.transform * vec3f(charPos, 1);
|
|
17333
|
+
|
|
17334
|
+
output.position = vec4f(transformedPosition, 1);
|
|
17335
|
+
output.texcoord = pos[vertexIndex] * vec2f(1, -1);
|
|
17336
|
+
output.texcoord *= char.texExtent;
|
|
17337
|
+
output.texcoord += char.texOffset;
|
|
17338
|
+
output.debugColor = debugColors[vertexIndex];
|
|
17339
|
+
output.instanceIndex = textIndex;
|
|
17340
|
+
return output;
|
|
17341
|
+
|
|
17342
|
+
// To debug - hardcode quad in bottom right quarter of the screen:
|
|
17343
|
+
// output.position = vec4f(pos[input.vertex], 0, 1);
|
|
17255
17344
|
}
|
|
17256
|
-
|
|
17257
|
-
|
|
17258
|
-
|
|
17259
|
-
|
|
17260
|
-
|
|
17261
|
-
};
|
|
17345
|
+
|
|
17346
|
+
// Signed distance function sampling for MSDF font rendering
|
|
17347
|
+
fn sampleMsdf(texcoord: vec2f) -> f32 {
|
|
17348
|
+
let c = textureSample(fontTexture, fontSampler, texcoord);
|
|
17349
|
+
return max(min(c.r, c.g), min(max(c.r, c.g), c.b));
|
|
17262
17350
|
}
|
|
17263
17351
|
|
|
17264
|
-
//
|
|
17265
|
-
|
|
17266
|
-
|
|
17267
|
-
|
|
17268
|
-
|
|
17269
|
-
|
|
17270
|
-
|
|
17271
|
-
|
|
17272
|
-
|
|
17273
|
-
|
|
17274
|
-
|
|
17275
|
-
|
|
17276
|
-
|
|
17277
|
-
|
|
17278
|
-
|
|
17279
|
-
|
|
17280
|
-
|
|
17281
|
-
|
|
17282
|
-
|
|
17283
|
-
|
|
17284
|
-
|
|
17285
|
-
|
|
17286
|
-
|
|
17287
|
-
|
|
17288
|
-
if (!layer) {
|
|
17289
|
-
layer = { z: z3, pipelines: [] };
|
|
17290
|
-
this.layers.push(layer);
|
|
17291
|
-
this.layers.sort((a3, b3) => a3.z - b3.z);
|
|
17292
|
-
}
|
|
17293
|
-
return layer;
|
|
17294
|
-
}
|
|
17295
|
-
#findOrCreatePipeline(layer, shader) {
|
|
17296
|
-
let pipeline = layer.pipelines.find((p3) => p3.shader === shader);
|
|
17297
|
-
if (!pipeline) {
|
|
17298
|
-
pipeline = { shader, nodes: [] };
|
|
17299
|
-
layer.pipelines.push(pipeline);
|
|
17300
|
-
this.pipelines.push(pipeline);
|
|
17301
|
-
}
|
|
17302
|
-
return pipeline;
|
|
17352
|
+
// Fragment shader
|
|
17353
|
+
// Anti-aliasing technique by Paul Houx
|
|
17354
|
+
// more details here:
|
|
17355
|
+
// https://github.com/Chlumsky/msdfgen/issues/22#issuecomment-234958005
|
|
17356
|
+
@fragment
|
|
17357
|
+
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
|
|
17358
|
+
let text = texts[input.instanceIndex];
|
|
17359
|
+
|
|
17360
|
+
// pxRange (AKA distanceRange) comes from the msdfgen tool.
|
|
17361
|
+
let pxRange = 4.0;
|
|
17362
|
+
let texSize = vec2f(textureDimensions(fontTexture, 0));
|
|
17363
|
+
|
|
17364
|
+
let dx = texSize.x * length(vec2f(dpdxFine(input.texcoord.x), dpdyFine(input.texcoord.x)));
|
|
17365
|
+
let dy = texSize.y * length(vec2f(dpdxFine(input.texcoord.y), dpdyFine(input.texcoord.y)));
|
|
17366
|
+
|
|
17367
|
+
let toPixels = pxRange * inverseSqrt(dx * dx + dy * dy);
|
|
17368
|
+
let sigDist = sampleMsdf(input.texcoord) - 0.5;
|
|
17369
|
+
let pxDist = sigDist * toPixels;
|
|
17370
|
+
|
|
17371
|
+
let edgeWidth = 0.5;
|
|
17372
|
+
let alpha = smoothstep(-edgeWidth, edgeWidth, pxDist);
|
|
17373
|
+
|
|
17374
|
+
if (alpha < 0.001) {
|
|
17375
|
+
discard;
|
|
17303
17376
|
}
|
|
17304
|
-
}
|
|
17305
17377
|
|
|
17306
|
-
|
|
17307
|
-
|
|
17308
|
-
|
|
17309
|
-
|
|
17310
|
-
|
|
17311
|
-
return
|
|
17378
|
+
let msdfColor = vec4f(text.color.rgb, text.color.a * alpha);
|
|
17379
|
+
return msdfColor;
|
|
17380
|
+
|
|
17381
|
+
// Debug options:
|
|
17382
|
+
// return text.color;
|
|
17383
|
+
// return input.debugColor;
|
|
17384
|
+
// return vec4f(1, 0, 1, 1); // hardcoded magenta
|
|
17385
|
+
// return textureSample(fontTexture, fontSampler, input.texcoord);
|
|
17312
17386
|
}
|
|
17387
|
+
`;
|
|
17313
17388
|
|
|
17314
|
-
// src/
|
|
17315
|
-
class
|
|
17316
|
-
|
|
17317
|
-
|
|
17318
|
-
|
|
17319
|
-
|
|
17320
|
-
|
|
17321
|
-
|
|
17322
|
-
|
|
17389
|
+
// src/backends/webgpu/FontPipeline.ts
|
|
17390
|
+
class FontPipeline {
|
|
17391
|
+
pipeline;
|
|
17392
|
+
font;
|
|
17393
|
+
fontBindGroup;
|
|
17394
|
+
maxCharCount;
|
|
17395
|
+
constructor(pipeline, font, fontBindGroup, maxCharCount) {
|
|
17396
|
+
this.pipeline = pipeline;
|
|
17397
|
+
this.font = font;
|
|
17398
|
+
this.fontBindGroup = fontBindGroup;
|
|
17399
|
+
this.maxCharCount = maxCharCount;
|
|
17323
17400
|
}
|
|
17324
|
-
|
|
17325
|
-
|
|
17326
|
-
|
|
17327
|
-
|
|
17328
|
-
|
|
17329
|
-
|
|
17330
|
-
|
|
17331
|
-
|
|
17332
|
-
|
|
17333
|
-
|
|
17334
|
-
|
|
17335
|
-
|
|
17336
|
-
|
|
17337
|
-
|
|
17338
|
-
|
|
17339
|
-
|
|
17340
|
-
|
|
17341
|
-
|
|
17342
|
-
|
|
17343
|
-
|
|
17344
|
-
|
|
17345
|
-
|
|
17346
|
-
|
|
17347
|
-
|
|
17348
|
-
|
|
17349
|
-
|
|
17350
|
-
|
|
17351
|
-
|
|
17352
|
-
|
|
17353
|
-
|
|
17354
|
-
|
|
17355
|
-
|
|
17356
|
-
|
|
17357
|
-
|
|
17358
|
-
|
|
17359
|
-
|
|
17360
|
-
|
|
17361
|
-
|
|
17362
|
-
|
|
17363
|
-
|
|
17364
|
-
|
|
17401
|
+
static async create(device, font, colorFormat, maxCharCount) {
|
|
17402
|
+
const pipeline = await pipelinePromise(device, colorFormat, font.name);
|
|
17403
|
+
const texture = device.createTexture({
|
|
17404
|
+
label: `MSDF font ${font.name}`,
|
|
17405
|
+
size: [font.imageBitmap.width, font.imageBitmap.height, 1],
|
|
17406
|
+
format: "rgba8unorm",
|
|
17407
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
|
|
17408
|
+
});
|
|
17409
|
+
device.queue.copyExternalImageToTexture({ source: font.imageBitmap }, { texture }, [font.imageBitmap.width, font.imageBitmap.height]);
|
|
17410
|
+
const charsGpuBuffer = device.createBuffer({
|
|
17411
|
+
label: `MSDF font ${font.name} character layout buffer`,
|
|
17412
|
+
size: font.charCount * Float32Array.BYTES_PER_ELEMENT * 8,
|
|
17413
|
+
usage: GPUBufferUsage.STORAGE,
|
|
17414
|
+
mappedAtCreation: true
|
|
17415
|
+
});
|
|
17416
|
+
const charsArray = new Float32Array(charsGpuBuffer.getMappedRange());
|
|
17417
|
+
charsArray.set(font.charBuffer, 0);
|
|
17418
|
+
charsGpuBuffer.unmap();
|
|
17419
|
+
const fontDataBuffer = device.createBuffer({
|
|
17420
|
+
label: `MSDF font ${font.name} metadata buffer`,
|
|
17421
|
+
size: Float32Array.BYTES_PER_ELEMENT * 4,
|
|
17422
|
+
usage: GPUBufferUsage.UNIFORM,
|
|
17423
|
+
mappedAtCreation: true
|
|
17424
|
+
});
|
|
17425
|
+
const fontDataArray = new Float32Array(fontDataBuffer.getMappedRange());
|
|
17426
|
+
fontDataArray[0] = font.lineHeight;
|
|
17427
|
+
fontDataBuffer.unmap();
|
|
17428
|
+
const fontBindGroup = device.createBindGroup({
|
|
17429
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
17430
|
+
entries: [
|
|
17431
|
+
{
|
|
17432
|
+
binding: 0,
|
|
17433
|
+
resource: texture.createView()
|
|
17434
|
+
},
|
|
17435
|
+
{
|
|
17436
|
+
binding: 1,
|
|
17437
|
+
resource: device.createSampler(sampler)
|
|
17438
|
+
},
|
|
17439
|
+
{
|
|
17440
|
+
binding: 2,
|
|
17441
|
+
resource: {
|
|
17442
|
+
buffer: charsGpuBuffer
|
|
17443
|
+
}
|
|
17444
|
+
},
|
|
17445
|
+
{
|
|
17446
|
+
binding: 3,
|
|
17447
|
+
resource: {
|
|
17448
|
+
buffer: fontDataBuffer
|
|
17449
|
+
}
|
|
17450
|
+
}
|
|
17451
|
+
]
|
|
17452
|
+
});
|
|
17453
|
+
return new FontPipeline(pipeline, font, fontBindGroup, maxCharCount);
|
|
17365
17454
|
}
|
|
17366
17455
|
}
|
|
17367
|
-
|
|
17368
|
-
|
|
17369
|
-
|
|
17370
|
-
|
|
17371
|
-
|
|
17372
|
-
|
|
17373
|
-
|
|
17374
|
-
|
|
17375
|
-
|
|
17376
|
-
|
|
17377
|
-
|
|
17378
|
-
|
|
17379
|
-
|
|
17380
|
-
|
|
17381
|
-
|
|
17382
|
-
|
|
17383
|
-
|
|
17384
|
-
|
|
17385
|
-
|
|
17386
|
-
|
|
17387
|
-
|
|
17388
|
-
|
|
17389
|
-
|
|
17390
|
-
|
|
17391
|
-
|
|
17392
|
-
|
|
17393
|
-
|
|
17394
|
-
|
|
17395
|
-
|
|
17396
|
-
|
|
17397
|
-
|
|
17398
|
-
|
|
17399
|
-
|
|
17400
|
-
|
|
17401
|
-
|
|
17402
|
-
|
|
17403
|
-
|
|
17404
|
-
|
|
17405
|
-
|
|
17406
|
-
|
|
17407
|
-
for (const kid of opts?.kids ?? []) {
|
|
17408
|
-
this.add(kid);
|
|
17409
|
-
}
|
|
17410
|
-
const self = this;
|
|
17411
|
-
this.#positionProxy = {
|
|
17412
|
-
get x() {
|
|
17413
|
-
return self.#transform.position.x;
|
|
17414
|
-
},
|
|
17415
|
-
set x(value) {
|
|
17416
|
-
self.#transform.position.x = value;
|
|
17417
|
-
self.setDirty();
|
|
17418
|
-
},
|
|
17419
|
-
get y() {
|
|
17420
|
-
return self.#transform.position.y;
|
|
17421
|
-
},
|
|
17422
|
-
set y(value) {
|
|
17423
|
-
self.#transform.position.y = value;
|
|
17424
|
-
self.setDirty();
|
|
17425
|
-
}
|
|
17426
|
-
};
|
|
17427
|
-
this.#scaleProxy = {
|
|
17428
|
-
get x() {
|
|
17429
|
-
return self.#transform.scale.x;
|
|
17430
|
-
},
|
|
17431
|
-
set x(value) {
|
|
17432
|
-
self.#transform.scale.x = value;
|
|
17433
|
-
self.setDirty();
|
|
17434
|
-
},
|
|
17435
|
-
get y() {
|
|
17436
|
-
return self.#transform.scale.y;
|
|
17437
|
-
},
|
|
17438
|
-
set y(value) {
|
|
17439
|
-
self.#transform.scale.y = value;
|
|
17440
|
-
self.setDirty();
|
|
17441
|
-
}
|
|
17442
|
-
};
|
|
17443
|
-
}
|
|
17444
|
-
add(kid, index) {
|
|
17445
|
-
kid.#parent = this;
|
|
17446
|
-
if (index === undefined) {
|
|
17447
|
-
this.#kids.push(kid);
|
|
17448
|
-
} else {
|
|
17449
|
-
this.#kids.splice(index, 0, kid);
|
|
17456
|
+
function pipelinePromise(device, colorFormat, label) {
|
|
17457
|
+
const shader = device.createShaderModule({
|
|
17458
|
+
label: `${label} shader`,
|
|
17459
|
+
code: text_wgsl_default
|
|
17460
|
+
});
|
|
17461
|
+
return device.createRenderPipelineAsync({
|
|
17462
|
+
label: `${label} pipeline`,
|
|
17463
|
+
layout: device.createPipelineLayout({
|
|
17464
|
+
bindGroupLayouts: [
|
|
17465
|
+
device.createBindGroupLayout(fontBindGroupLayout),
|
|
17466
|
+
device.createBindGroupLayout(textUniformBindGroupLayout),
|
|
17467
|
+
device.createBindGroupLayout(engineUniformBindGroupLayout)
|
|
17468
|
+
]
|
|
17469
|
+
}),
|
|
17470
|
+
vertex: {
|
|
17471
|
+
module: shader,
|
|
17472
|
+
entryPoint: "vertexMain"
|
|
17473
|
+
},
|
|
17474
|
+
fragment: {
|
|
17475
|
+
module: shader,
|
|
17476
|
+
entryPoint: "fragmentMain",
|
|
17477
|
+
targets: [
|
|
17478
|
+
{
|
|
17479
|
+
format: colorFormat,
|
|
17480
|
+
blend: {
|
|
17481
|
+
color: {
|
|
17482
|
+
srcFactor: "src-alpha",
|
|
17483
|
+
dstFactor: "one-minus-src-alpha"
|
|
17484
|
+
},
|
|
17485
|
+
alpha: {
|
|
17486
|
+
srcFactor: "one",
|
|
17487
|
+
dstFactor: "one"
|
|
17488
|
+
}
|
|
17489
|
+
}
|
|
17490
|
+
}
|
|
17491
|
+
]
|
|
17492
|
+
},
|
|
17493
|
+
primitive: {
|
|
17494
|
+
topology: "triangle-strip",
|
|
17495
|
+
stripIndexFormat: "uint32"
|
|
17450
17496
|
}
|
|
17451
|
-
|
|
17452
|
-
|
|
17453
|
-
|
|
17454
|
-
|
|
17455
|
-
|
|
17456
|
-
|
|
17457
|
-
|
|
17458
|
-
|
|
17459
|
-
|
|
17460
|
-
|
|
17461
|
-
|
|
17462
|
-
|
|
17463
|
-
|
|
17464
|
-
|
|
17465
|
-
|
|
17466
|
-
|
|
17467
|
-
|
|
17468
|
-
|
|
17469
|
-
|
|
17470
|
-
|
|
17471
|
-
|
|
17472
|
-
|
|
17473
|
-
|
|
17474
|
-
|
|
17475
|
-
|
|
17476
|
-
|
|
17477
|
-
|
|
17478
|
-
|
|
17479
|
-
|
|
17480
|
-
|
|
17481
|
-
|
|
17482
|
-
}
|
|
17483
|
-
set y(value) {
|
|
17484
|
-
this.#transform.position.y = value;
|
|
17485
|
-
this.setDirty();
|
|
17486
|
-
}
|
|
17487
|
-
get y() {
|
|
17488
|
-
return this.#transform.position.y;
|
|
17489
|
-
}
|
|
17490
|
-
set rotation(value) {
|
|
17491
|
-
this.#transform.rotation = deg2rad(value);
|
|
17492
|
-
this.setDirty();
|
|
17493
|
-
}
|
|
17494
|
-
get rotation() {
|
|
17495
|
-
return rad2deg(this.#transform.rotation);
|
|
17496
|
-
}
|
|
17497
|
-
get rotationRadians() {
|
|
17498
|
-
return this.#transform.rotation;
|
|
17499
|
-
}
|
|
17500
|
-
set rotationRadians(value) {
|
|
17501
|
-
this.#transform.rotation = value;
|
|
17502
|
-
this.setDirty();
|
|
17503
|
-
}
|
|
17504
|
-
get scale() {
|
|
17505
|
-
return this.#scaleProxy;
|
|
17506
|
-
}
|
|
17507
|
-
set scale(value) {
|
|
17508
|
-
if (typeof value === "number") {
|
|
17509
|
-
this.#transform.scale = { x: value, y: value };
|
|
17510
|
-
} else {
|
|
17511
|
-
this.#transform.scale = value;
|
|
17497
|
+
});
|
|
17498
|
+
}
|
|
17499
|
+
if (typeof GPUShaderStage === "undefined") {
|
|
17500
|
+
globalThis.GPUShaderStage = {
|
|
17501
|
+
VERTEX: 1,
|
|
17502
|
+
FRAGMENT: 2,
|
|
17503
|
+
COMPUTE: 4
|
|
17504
|
+
};
|
|
17505
|
+
}
|
|
17506
|
+
var fontBindGroupLayout = {
|
|
17507
|
+
label: "MSDF font group layout",
|
|
17508
|
+
entries: [
|
|
17509
|
+
{
|
|
17510
|
+
binding: 0,
|
|
17511
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
17512
|
+
texture: {}
|
|
17513
|
+
},
|
|
17514
|
+
{
|
|
17515
|
+
binding: 1,
|
|
17516
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
17517
|
+
sampler: {}
|
|
17518
|
+
},
|
|
17519
|
+
{
|
|
17520
|
+
binding: 2,
|
|
17521
|
+
visibility: GPUShaderStage.VERTEX,
|
|
17522
|
+
buffer: { type: "read-only-storage" }
|
|
17523
|
+
},
|
|
17524
|
+
{
|
|
17525
|
+
binding: 3,
|
|
17526
|
+
visibility: GPUShaderStage.VERTEX,
|
|
17527
|
+
buffer: {}
|
|
17512
17528
|
}
|
|
17513
|
-
|
|
17514
|
-
|
|
17515
|
-
|
|
17516
|
-
|
|
17517
|
-
|
|
17518
|
-
|
|
17519
|
-
|
|
17520
|
-
|
|
17521
|
-
|
|
17522
|
-
get aspectRatio() {
|
|
17523
|
-
if (!this.#size) {
|
|
17524
|
-
console.warn("Attempted to get aspect ratio of a node with no ideal size");
|
|
17525
|
-
return 1;
|
|
17529
|
+
]
|
|
17530
|
+
};
|
|
17531
|
+
var engineUniformBindGroupLayout = {
|
|
17532
|
+
label: "Uniform bind group",
|
|
17533
|
+
entries: [
|
|
17534
|
+
{
|
|
17535
|
+
binding: 0,
|
|
17536
|
+
visibility: GPUShaderStage.VERTEX,
|
|
17537
|
+
buffer: {}
|
|
17526
17538
|
}
|
|
17527
|
-
|
|
17528
|
-
|
|
17529
|
-
|
|
17530
|
-
|
|
17531
|
-
|
|
17532
|
-
|
|
17533
|
-
|
|
17534
|
-
|
|
17535
|
-
|
|
17536
|
-
|
|
17537
|
-
|
|
17538
|
-
|
|
17539
|
+
]
|
|
17540
|
+
};
|
|
17541
|
+
var sampler = {
|
|
17542
|
+
label: "MSDF text sampler",
|
|
17543
|
+
minFilter: "linear",
|
|
17544
|
+
magFilter: "linear",
|
|
17545
|
+
mipmapFilter: "linear",
|
|
17546
|
+
maxAnisotropy: 16
|
|
17547
|
+
};
|
|
17548
|
+
var textUniformBindGroupLayout = {
|
|
17549
|
+
label: "MSDF text block uniform",
|
|
17550
|
+
entries: [
|
|
17551
|
+
{
|
|
17552
|
+
binding: 0,
|
|
17553
|
+
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
17554
|
+
buffer: { type: "read-only-storage" }
|
|
17555
|
+
},
|
|
17556
|
+
{
|
|
17557
|
+
binding: 1,
|
|
17558
|
+
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
17559
|
+
buffer: { type: "read-only-storage" }
|
|
17539
17560
|
}
|
|
17540
|
-
|
|
17541
|
-
|
|
17542
|
-
|
|
17543
|
-
|
|
17544
|
-
|
|
17561
|
+
]
|
|
17562
|
+
};
|
|
17563
|
+
// src/utils/error.ts
|
|
17564
|
+
var warnings = new Map;
|
|
17565
|
+
function warnOnce(key, msg) {
|
|
17566
|
+
if (warnings.has(key)) {
|
|
17567
|
+
return;
|
|
17545
17568
|
}
|
|
17546
|
-
|
|
17547
|
-
|
|
17548
|
-
|
|
17549
|
-
|
|
17550
|
-
|
|
17551
|
-
|
|
17552
|
-
|
|
17553
|
-
|
|
17554
|
-
|
|
17555
|
-
|
|
17556
|
-
|
|
17557
|
-
|
|
17569
|
+
warnings.set(key, true);
|
|
17570
|
+
console.warn(msg ?? key);
|
|
17571
|
+
}
|
|
17572
|
+
|
|
17573
|
+
// src/text/MsdfFont.ts
|
|
17574
|
+
class MsdfFont {
|
|
17575
|
+
id;
|
|
17576
|
+
json;
|
|
17577
|
+
imageBitmap;
|
|
17578
|
+
name;
|
|
17579
|
+
charset;
|
|
17580
|
+
charCount;
|
|
17581
|
+
lineHeight;
|
|
17582
|
+
charBuffer;
|
|
17583
|
+
#kernings;
|
|
17584
|
+
#chars;
|
|
17585
|
+
#fallbackCharCode;
|
|
17586
|
+
constructor(id, json, imageBitmap) {
|
|
17587
|
+
this.id = id;
|
|
17588
|
+
this.json = json;
|
|
17589
|
+
this.imageBitmap = imageBitmap;
|
|
17590
|
+
const charArray = Object.values(json.chars);
|
|
17591
|
+
this.charCount = charArray.length;
|
|
17592
|
+
this.lineHeight = json.common.lineHeight;
|
|
17593
|
+
this.charset = json.info.charset;
|
|
17594
|
+
this.name = json.info.face;
|
|
17595
|
+
this.#kernings = new Map;
|
|
17596
|
+
if (json.kernings) {
|
|
17597
|
+
for (const kearning of json.kernings) {
|
|
17598
|
+
let charKerning = this.#kernings.get(kearning.first);
|
|
17599
|
+
if (!charKerning) {
|
|
17600
|
+
charKerning = new Map;
|
|
17601
|
+
this.#kernings.set(kearning.first, charKerning);
|
|
17558
17602
|
}
|
|
17603
|
+
charKerning.set(kearning.second, kearning.amount);
|
|
17559
17604
|
}
|
|
17560
|
-
this.#cache.layer = 0;
|
|
17561
17605
|
}
|
|
17562
|
-
|
|
17563
|
-
|
|
17564
|
-
|
|
17565
|
-
|
|
17566
|
-
|
|
17567
|
-
|
|
17568
|
-
|
|
17569
|
-
|
|
17570
|
-
|
|
17571
|
-
|
|
17572
|
-
|
|
17573
|
-
|
|
17574
|
-
|
|
17575
|
-
|
|
17576
|
-
this
|
|
17577
|
-
|
|
17578
|
-
|
|
17579
|
-
|
|
17580
|
-
mat3.identity(this.#matrix);
|
|
17581
|
-
}
|
|
17582
|
-
this.#cache.matrix = createModelMatrix(this.transform, this.#matrix);
|
|
17606
|
+
this.#chars = new Map;
|
|
17607
|
+
const charCount = Object.values(json.chars).length;
|
|
17608
|
+
this.charBuffer = new Float32Array(charCount * 8);
|
|
17609
|
+
let offset = 0;
|
|
17610
|
+
const u3 = 1 / json.common.scaleW;
|
|
17611
|
+
const v3 = 1 / json.common.scaleH;
|
|
17612
|
+
for (const [i3, char] of json.chars.entries()) {
|
|
17613
|
+
this.#chars.set(char.id, char);
|
|
17614
|
+
this.#chars.get(char.id).charIndex = i3;
|
|
17615
|
+
this.charBuffer[offset] = char.x * u3;
|
|
17616
|
+
this.charBuffer[offset + 1] = char.y * v3;
|
|
17617
|
+
this.charBuffer[offset + 2] = char.width * u3;
|
|
17618
|
+
this.charBuffer[offset + 3] = char.height * v3;
|
|
17619
|
+
this.charBuffer[offset + 4] = char.width;
|
|
17620
|
+
this.charBuffer[offset + 5] = char.height;
|
|
17621
|
+
this.charBuffer[offset + 6] = char.xoffset;
|
|
17622
|
+
this.charBuffer[offset + 7] = -char.yoffset;
|
|
17623
|
+
offset += 8;
|
|
17583
17624
|
}
|
|
17584
|
-
return this.#cache.matrix;
|
|
17585
17625
|
}
|
|
17586
|
-
|
|
17587
|
-
|
|
17588
|
-
|
|
17589
|
-
const
|
|
17590
|
-
|
|
17591
|
-
|
|
17592
|
-
vec2.transformMat3([-width / 2, height / 2], this.matrix),
|
|
17593
|
-
vec2.transformMat3([width / 2, height / 2], this.matrix),
|
|
17594
|
-
vec2.transformMat3([-width / 2, -height / 2], this.matrix),
|
|
17595
|
-
vec2.transformMat3([width / 2, -height / 2], this.matrix)
|
|
17596
|
-
];
|
|
17597
|
-
const center = vec2.transformMat3([0, 0], this.matrix);
|
|
17598
|
-
const xValues = corners.map((c3) => c3[0]);
|
|
17599
|
-
const yValues = corners.map((c3) => c3[1]);
|
|
17600
|
-
this.#cache.bounds = {
|
|
17601
|
-
x: center[0],
|
|
17602
|
-
y: center[1],
|
|
17603
|
-
left: Math.min(xValues[0], xValues[1], xValues[2], xValues[3]),
|
|
17604
|
-
right: Math.max(xValues[0], xValues[1], xValues[2], xValues[3]),
|
|
17605
|
-
top: Math.max(yValues[0], yValues[1], yValues[2], yValues[3]),
|
|
17606
|
-
bottom: Math.min(yValues[0], yValues[1], yValues[2], yValues[3])
|
|
17607
|
-
};
|
|
17626
|
+
getChar(charCode) {
|
|
17627
|
+
const char = this.#chars.get(charCode);
|
|
17628
|
+
if (!char) {
|
|
17629
|
+
const fallbackCharacter = this.#chars.get(this.#fallbackCharCode ?? this.#chars.keys().toArray()[0]);
|
|
17630
|
+
warnOnce(`unknown_char_${this.name}`, `Couldn't find character ${charCode} in characters for font ${this.name} -- defaulting to first available character "${fallbackCharacter.char}"`);
|
|
17631
|
+
return fallbackCharacter;
|
|
17608
17632
|
}
|
|
17609
|
-
return
|
|
17610
|
-
}
|
|
17611
|
-
setBounds(bounds) {
|
|
17612
|
-
if (bounds.left !== undefined)
|
|
17613
|
-
this.left = bounds.left;
|
|
17614
|
-
if (bounds.right !== undefined)
|
|
17615
|
-
this.right = bounds.right;
|
|
17616
|
-
if (bounds.top !== undefined)
|
|
17617
|
-
this.top = bounds.top;
|
|
17618
|
-
if (bounds.bottom !== undefined)
|
|
17619
|
-
this.bottom = bounds.bottom;
|
|
17620
|
-
if (bounds.x !== undefined)
|
|
17621
|
-
this.centerX = bounds.x;
|
|
17622
|
-
if (bounds.y !== undefined)
|
|
17623
|
-
this.centerY = bounds.y;
|
|
17624
|
-
return this;
|
|
17625
|
-
}
|
|
17626
|
-
set left(value) {
|
|
17627
|
-
this.#adjustWorldPosition([value - this.bounds.left, 0]);
|
|
17628
|
-
}
|
|
17629
|
-
set bottom(value) {
|
|
17630
|
-
this.#adjustWorldPosition([0, value - this.bounds.bottom]);
|
|
17631
|
-
}
|
|
17632
|
-
set top(value) {
|
|
17633
|
-
this.#adjustWorldPosition([0, value - this.bounds.top]);
|
|
17634
|
-
}
|
|
17635
|
-
set right(value) {
|
|
17636
|
-
this.#adjustWorldPosition([value - this.bounds.right, 0]);
|
|
17637
|
-
}
|
|
17638
|
-
set centerX(value) {
|
|
17639
|
-
this.#adjustWorldPosition([value - this.bounds.x, 0]);
|
|
17633
|
+
return char;
|
|
17640
17634
|
}
|
|
17641
|
-
|
|
17642
|
-
|
|
17635
|
+
getXAdvance(charCode, nextCharCode = -1) {
|
|
17636
|
+
const char = this.getChar(charCode);
|
|
17637
|
+
if (nextCharCode >= 0) {
|
|
17638
|
+
const kerning = this.#kernings.get(charCode);
|
|
17639
|
+
if (kerning) {
|
|
17640
|
+
return char.xadvance + (kerning.get(nextCharCode) ?? 0);
|
|
17641
|
+
}
|
|
17642
|
+
}
|
|
17643
|
+
return char.xadvance;
|
|
17643
17644
|
}
|
|
17644
|
-
|
|
17645
|
-
|
|
17646
|
-
|
|
17647
|
-
|
|
17645
|
+
static async create(id, fontJsonUrl) {
|
|
17646
|
+
const response = await fetch(fontJsonUrl);
|
|
17647
|
+
const json = await response.json();
|
|
17648
|
+
const i3 = fontJsonUrl.href.lastIndexOf("/");
|
|
17649
|
+
const baseUrl = i3 !== -1 ? fontJsonUrl.href.substring(0, i3 + 1) : undefined;
|
|
17650
|
+
if (json.pages.length < 1) {
|
|
17651
|
+
throw new Error(`Can't create an msdf font without a reference to the page url in the json`);
|
|
17648
17652
|
}
|
|
17649
|
-
|
|
17650
|
-
|
|
17651
|
-
|
|
17652
|
-
|
|
17653
|
+
if (json.pages.length > 1) {
|
|
17654
|
+
throw new Error(`Can't create an msdf font with more than one page`);
|
|
17655
|
+
}
|
|
17656
|
+
const textureUrl = baseUrl + json.pages[0];
|
|
17657
|
+
const textureResponse = await fetch(textureUrl);
|
|
17658
|
+
const bitmap = await createImageBitmap(await textureResponse.blob());
|
|
17659
|
+
return new MsdfFont(id, json, bitmap);
|
|
17653
17660
|
}
|
|
17654
|
-
|
|
17655
|
-
const
|
|
17656
|
-
if (
|
|
17657
|
-
|
|
17661
|
+
set fallbackCharacter(character) {
|
|
17662
|
+
const charCode = character.charCodeAt(0);
|
|
17663
|
+
if (this.#chars.has(charCode)) {
|
|
17664
|
+
this.#fallbackCharCode = charCode;
|
|
17665
|
+
} else {
|
|
17666
|
+
const fallbackCode = this.#chars.keys().toArray()[0];
|
|
17667
|
+
console.warn(`${character} character does not exist in font ${this.name} defaulting to "${this.#chars.get(fallbackCode)?.char}".`);
|
|
17668
|
+
this.#fallbackCharCode = fallbackCode;
|
|
17658
17669
|
}
|
|
17659
|
-
this.#kids.splice(childIndex, 1);
|
|
17660
|
-
kid.#parent = null;
|
|
17661
|
-
kid.setDirty();
|
|
17662
17670
|
}
|
|
17663
|
-
|
|
17664
|
-
|
|
17665
|
-
|
|
17666
|
-
|
|
17667
|
-
|
|
17668
|
-
|
|
17669
|
-
|
|
17671
|
+
}
|
|
17672
|
+
|
|
17673
|
+
// src/text/shaping.ts
|
|
17674
|
+
var TAB_SPACES = 4;
|
|
17675
|
+
function shapeText(font, text, blockSize, fontSize, formatting, textArray, initialFloatOffset = 0, debug = false) {
|
|
17676
|
+
let offset = initialFloatOffset;
|
|
17677
|
+
const measurements = measureText(font, text, formatting.wordWrap);
|
|
17678
|
+
const alignment = formatting.align || "left";
|
|
17679
|
+
const em2px = fontSize / font.lineHeight;
|
|
17680
|
+
const hackHasExplicitBlock = blockSize.width !== measurements.width;
|
|
17681
|
+
let debugData = null;
|
|
17682
|
+
if (debug) {
|
|
17683
|
+
debugData = [];
|
|
17670
17684
|
}
|
|
17671
|
-
|
|
17672
|
-
|
|
17673
|
-
|
|
17685
|
+
for (const word of measurements.words) {
|
|
17686
|
+
for (const glyph of word.glyphs) {
|
|
17687
|
+
let lineOffset = 0;
|
|
17688
|
+
if (alignment === "center") {
|
|
17689
|
+
lineOffset = measurements.width * -0.5 - (measurements.width - measurements.lineWidths[glyph.line]) * -0.5;
|
|
17690
|
+
} else if (alignment === "right") {
|
|
17691
|
+
const blockSizeEm = blockSize.width / em2px;
|
|
17692
|
+
const delta = measurements.width - measurements.lineWidths[glyph.line];
|
|
17693
|
+
lineOffset = (hackHasExplicitBlock ? blockSizeEm / 2 : measurements.width / 2) - measurements.width + delta;
|
|
17694
|
+
} else if (alignment === "left") {
|
|
17695
|
+
const blockSizeEm = blockSize.width / em2px;
|
|
17696
|
+
lineOffset = hackHasExplicitBlock ? -blockSizeEm / 2 : -measurements.width / 2;
|
|
17697
|
+
}
|
|
17698
|
+
if (debug && debugData) {
|
|
17699
|
+
debugData.push({
|
|
17700
|
+
line: glyph.line,
|
|
17701
|
+
word: word.glyphs.map((g3) => g3.char.char).join(""),
|
|
17702
|
+
glyph: glyph.char.char,
|
|
17703
|
+
startX: word.startX,
|
|
17704
|
+
glyphX: glyph.offset[0],
|
|
17705
|
+
advance: glyph.char.xadvance,
|
|
17706
|
+
lineOffset,
|
|
17707
|
+
startY: word.startY,
|
|
17708
|
+
glyphY: glyph.offset[1]
|
|
17709
|
+
});
|
|
17710
|
+
}
|
|
17711
|
+
textArray[offset] = word.startX + glyph.offset[0] + lineOffset;
|
|
17712
|
+
textArray[offset + 1] = word.startY + glyph.offset[1];
|
|
17713
|
+
textArray[offset + 2] = glyph.char.charIndex;
|
|
17714
|
+
offset += 4;
|
|
17715
|
+
}
|
|
17674
17716
|
}
|
|
17675
|
-
|
|
17676
|
-
|
|
17677
|
-
return new SceneNode(obj);
|
|
17717
|
+
if (debug && debugData) {
|
|
17718
|
+
console.table(debugData);
|
|
17678
17719
|
}
|
|
17679
|
-
|
|
17680
|
-
|
|
17681
|
-
|
|
17682
|
-
|
|
17683
|
-
|
|
17684
|
-
|
|
17685
|
-
|
|
17686
|
-
|
|
17687
|
-
|
|
17720
|
+
}
|
|
17721
|
+
function measureText(font, text, wordWrap) {
|
|
17722
|
+
let maxWidth = 0;
|
|
17723
|
+
const lineWidths = [];
|
|
17724
|
+
let textOffsetX = 0;
|
|
17725
|
+
let textOffsetY = 0;
|
|
17726
|
+
let line = 0;
|
|
17727
|
+
let printedCharCount = 0;
|
|
17728
|
+
let nextCharCode = text.charCodeAt(0);
|
|
17729
|
+
let word = { glyphs: [], width: 0, startX: 0, startY: 0 };
|
|
17730
|
+
const words = [];
|
|
17731
|
+
for (let i3 = 0;i3 < text.length; i3++) {
|
|
17732
|
+
const isLastLetter = i3 === text.length - 1;
|
|
17733
|
+
const charCode = nextCharCode;
|
|
17734
|
+
nextCharCode = i3 < text.length - 1 ? text.charCodeAt(i3 + 1) : -1;
|
|
17735
|
+
switch (charCode) {
|
|
17736
|
+
case 9 /* HorizontalTab */:
|
|
17737
|
+
insertSpaces(TAB_SPACES);
|
|
17738
|
+
break;
|
|
17739
|
+
case 10 /* Newline */:
|
|
17740
|
+
flushLine();
|
|
17741
|
+
flushWord();
|
|
17742
|
+
break;
|
|
17743
|
+
case 13 /* CarriageReturn */:
|
|
17744
|
+
break;
|
|
17745
|
+
case 32 /* Space */:
|
|
17746
|
+
insertSpaces(1);
|
|
17747
|
+
break;
|
|
17748
|
+
default: {
|
|
17749
|
+
const advance = font.getXAdvance(charCode, nextCharCode);
|
|
17750
|
+
if (wordWrap && wordWrap.breakOn === "character" && textOffsetX + advance > wordWrap.emWidth) {
|
|
17751
|
+
if (word.startX === 0) {
|
|
17752
|
+
flushWord();
|
|
17753
|
+
} else {
|
|
17754
|
+
lineWidths.push(textOffsetX - word.width);
|
|
17755
|
+
line++;
|
|
17756
|
+
maxWidth = Math.max(maxWidth, textOffsetX);
|
|
17757
|
+
textOffsetX = word.width;
|
|
17758
|
+
textOffsetY -= font.lineHeight;
|
|
17759
|
+
word.startX = 0;
|
|
17760
|
+
word.startY = textOffsetY;
|
|
17761
|
+
word.glyphs.forEach((g3) => {
|
|
17762
|
+
g3.line = line;
|
|
17763
|
+
});
|
|
17764
|
+
}
|
|
17765
|
+
}
|
|
17766
|
+
word.glyphs.push({
|
|
17767
|
+
char: font.getChar(charCode),
|
|
17768
|
+
offset: [word.width, 0],
|
|
17769
|
+
line
|
|
17770
|
+
});
|
|
17771
|
+
if (isLastLetter) {
|
|
17772
|
+
flushWord();
|
|
17773
|
+
}
|
|
17774
|
+
word.width += advance;
|
|
17775
|
+
textOffsetX += advance;
|
|
17776
|
+
}
|
|
17777
|
+
}
|
|
17778
|
+
}
|
|
17779
|
+
lineWidths.push(textOffsetX);
|
|
17780
|
+
maxWidth = Math.max(maxWidth, textOffsetX);
|
|
17781
|
+
const lineCount = lineWidths.length;
|
|
17782
|
+
return {
|
|
17783
|
+
width: maxWidth,
|
|
17784
|
+
height: lineCount * font.lineHeight,
|
|
17785
|
+
lineWidths,
|
|
17786
|
+
lineCount,
|
|
17787
|
+
printedCharCount,
|
|
17788
|
+
words
|
|
17789
|
+
};
|
|
17790
|
+
function flushWord() {
|
|
17791
|
+
printedCharCount += word.glyphs.length;
|
|
17792
|
+
words.push(word);
|
|
17793
|
+
word = {
|
|
17794
|
+
glyphs: [],
|
|
17795
|
+
width: 0,
|
|
17796
|
+
startX: textOffsetX,
|
|
17797
|
+
startY: textOffsetY
|
|
17688
17798
|
};
|
|
17689
17799
|
}
|
|
17690
|
-
|
|
17691
|
-
|
|
17692
|
-
|
|
17693
|
-
|
|
17800
|
+
function flushLine() {
|
|
17801
|
+
lineWidths.push(textOffsetX);
|
|
17802
|
+
line++;
|
|
17803
|
+
maxWidth = Math.max(maxWidth, textOffsetX);
|
|
17804
|
+
textOffsetX = 0;
|
|
17805
|
+
textOffsetY -= font.lineHeight;
|
|
17694
17806
|
}
|
|
17695
|
-
|
|
17696
|
-
if (
|
|
17697
|
-
|
|
17698
|
-
|
|
17699
|
-
if (
|
|
17700
|
-
|
|
17807
|
+
function insertSpaces(spaces) {
|
|
17808
|
+
if (spaces < 1)
|
|
17809
|
+
spaces = 1;
|
|
17810
|
+
textOffsetX += font.getXAdvance(32 /* Space */) * spaces;
|
|
17811
|
+
if (wordWrap?.breakOn === "word" && textOffsetX >= wordWrap.emWidth) {
|
|
17812
|
+
flushLine();
|
|
17701
17813
|
}
|
|
17702
|
-
|
|
17703
|
-
|
|
17814
|
+
flushWord();
|
|
17815
|
+
}
|
|
17816
|
+
}
|
|
17817
|
+
function findLargestFontSize(font, text, size, formatting) {
|
|
17818
|
+
if (!formatting.fontSize) {
|
|
17819
|
+
throw new Error("fontSize is required for shrinkToFit");
|
|
17820
|
+
}
|
|
17821
|
+
if (!formatting.shrinkToFit) {
|
|
17822
|
+
throw new Error("shrinkToFit is required for findLargestFontSize");
|
|
17823
|
+
}
|
|
17824
|
+
const minSize = formatting.shrinkToFit.minFontSize;
|
|
17825
|
+
const maxSize = formatting.shrinkToFit.maxFontSize ?? formatting.fontSize;
|
|
17826
|
+
const maxLines = formatting.shrinkToFit.maxLines ?? Number.POSITIVE_INFINITY;
|
|
17827
|
+
const threshold = 0.5;
|
|
17828
|
+
let low = minSize;
|
|
17829
|
+
let high = maxSize;
|
|
17830
|
+
while (high - low > threshold) {
|
|
17831
|
+
const testSize = (low + high) / 2;
|
|
17832
|
+
const testMeasure = measureText(font, text, formatting.wordWrap);
|
|
17833
|
+
const padding = formatting.shrinkToFit.padding ?? 0;
|
|
17834
|
+
const scaledWidth = testMeasure.width * (testSize / font.lineHeight);
|
|
17835
|
+
const scaledHeight = testMeasure.height * (testSize / font.lineHeight);
|
|
17836
|
+
const fitsWidth = scaledWidth <= size.width - size.width * padding;
|
|
17837
|
+
const fitsHeight = scaledHeight <= size.height - size.height * padding;
|
|
17838
|
+
const fitsLines = testMeasure.lineCount <= maxLines;
|
|
17839
|
+
if (fitsWidth && fitsHeight && fitsLines) {
|
|
17840
|
+
low = testSize;
|
|
17841
|
+
} else {
|
|
17842
|
+
high = testSize;
|
|
17704
17843
|
}
|
|
17705
17844
|
}
|
|
17706
|
-
return
|
|
17845
|
+
return low;
|
|
17707
17846
|
}
|
|
17708
17847
|
|
|
17709
|
-
// src/
|
|
17710
|
-
|
|
17711
|
-
|
|
17712
|
-
|
|
17713
|
-
|
|
17714
|
-
|
|
17715
|
-
|
|
17716
|
-
width: 0,
|
|
17717
|
-
height: 0
|
|
17718
|
-
};
|
|
17848
|
+
// src/math/angle.ts
|
|
17849
|
+
function deg2rad(degrees) {
|
|
17850
|
+
return degrees * (Math.PI / 180);
|
|
17851
|
+
}
|
|
17852
|
+
function rad2deg(radians) {
|
|
17853
|
+
return radians * (180 / Math.PI);
|
|
17854
|
+
}
|
|
17719
17855
|
|
|
17720
|
-
|
|
17721
|
-
|
|
17722
|
-
|
|
17723
|
-
|
|
17724
|
-
|
|
17725
|
-
|
|
17726
|
-
|
|
17727
|
-
|
|
17728
|
-
|
|
17729
|
-
|
|
17730
|
-
|
|
17731
|
-
|
|
17732
|
-
|
|
17733
|
-
|
|
17734
|
-
|
|
17735
|
-
|
|
17736
|
-
|
|
17737
|
-
|
|
17738
|
-
|
|
17739
|
-
|
|
17740
|
-
|
|
17741
|
-
|
|
17742
|
-
|
|
17743
|
-
|
|
17744
|
-
|
|
17745
|
-
|
|
17746
|
-
|
|
17747
|
-
|
|
17748
|
-
|
|
17749
|
-
|
|
17750
|
-
|
|
17751
|
-
|
|
17752
|
-
|
|
17753
|
-
|
|
17856
|
+
// src/math/matrix.ts
|
|
17857
|
+
function createProjectionMatrix(resolution, dst) {
|
|
17858
|
+
const { width, height } = resolution;
|
|
17859
|
+
return mat3.scaling([2 / width, 2 / height], dst);
|
|
17860
|
+
}
|
|
17861
|
+
function createViewMatrix(camera, target) {
|
|
17862
|
+
const matrix = mat3.identity(target);
|
|
17863
|
+
mat3.scale(matrix, [camera.zoom, camera.zoom], matrix);
|
|
17864
|
+
mat3.rotate(matrix, camera.rotationRadians, matrix);
|
|
17865
|
+
mat3.translate(matrix, [-camera.x, -camera.y], matrix);
|
|
17866
|
+
return matrix;
|
|
17867
|
+
}
|
|
17868
|
+
function createModelMatrix(transform, base) {
|
|
17869
|
+
mat3.translate(base, [transform.position.x, transform.position.y], base);
|
|
17870
|
+
mat3.rotate(base, transform.rotation, base);
|
|
17871
|
+
mat3.scale(base, [transform.scale.x, transform.scale.y], base);
|
|
17872
|
+
return base;
|
|
17873
|
+
}
|
|
17874
|
+
function convertScreenToWorld(screenCoordinates, camera, projectionMatrix, resolution) {
|
|
17875
|
+
const inverseViewProjectionMatrix = mat3.mul(mat3.inverse(camera.matrix), mat3.inverse(projectionMatrix));
|
|
17876
|
+
const normalizedDeviceCoordinates = {
|
|
17877
|
+
x: 2 * screenCoordinates.x / resolution.width - 1,
|
|
17878
|
+
y: 1 - 2 * screenCoordinates.y / resolution.height
|
|
17879
|
+
};
|
|
17880
|
+
return transformPoint(normalizedDeviceCoordinates, inverseViewProjectionMatrix);
|
|
17881
|
+
}
|
|
17882
|
+
function convertWorldToScreen(worldCoordinates, camera, projectionMatrix, resolution) {
|
|
17883
|
+
const viewProjectionMatrix = mat3.mul(projectionMatrix, camera.matrix);
|
|
17884
|
+
const ndcPoint = transformPoint(worldCoordinates, viewProjectionMatrix);
|
|
17885
|
+
return {
|
|
17886
|
+
x: (ndcPoint.x + 1) * resolution.width / 2,
|
|
17887
|
+
y: (1 - ndcPoint.y) * resolution.height / 2
|
|
17888
|
+
};
|
|
17889
|
+
}
|
|
17890
|
+
function transformPoint(point, matrix) {
|
|
17891
|
+
const result = vec2.transformMat3([point.x, point.y], matrix);
|
|
17892
|
+
return {
|
|
17893
|
+
x: result[0],
|
|
17894
|
+
y: result[1]
|
|
17895
|
+
};
|
|
17896
|
+
}
|
|
17897
|
+
|
|
17898
|
+
// src/scene/SceneNode.ts
|
|
17899
|
+
class SceneNode {
|
|
17900
|
+
static nextId = 1;
|
|
17901
|
+
id;
|
|
17902
|
+
label;
|
|
17903
|
+
#isActive = true;
|
|
17904
|
+
#layer = null;
|
|
17905
|
+
#parent = null;
|
|
17906
|
+
#key = null;
|
|
17907
|
+
#kids = [];
|
|
17908
|
+
#transform;
|
|
17909
|
+
#matrix = mat3.identity();
|
|
17910
|
+
#renderComponent = null;
|
|
17911
|
+
#size = null;
|
|
17912
|
+
#positionProxy;
|
|
17913
|
+
#scaleProxy;
|
|
17914
|
+
#cache = null;
|
|
17915
|
+
constructor(opts) {
|
|
17916
|
+
this.id = opts?.id ?? SceneNode.nextId++;
|
|
17917
|
+
if (opts?.rotation && opts?.rotationRadians) {
|
|
17918
|
+
throw new Error(`Cannot set both rotation and rotationRadians for node ${opts?.label ?? this.id}`);
|
|
17754
17919
|
}
|
|
17755
|
-
this.#
|
|
17756
|
-
|
|
17757
|
-
|
|
17758
|
-
|
|
17759
|
-
|
|
17760
|
-
this.#cropRatio = !this.#atlasCoords.uvScaleCropped ? { width: 1, height: 1 } : {
|
|
17761
|
-
width: this.#atlasCoords.uvScaleCropped.width / this.#atlasCoords.uvScale.width,
|
|
17762
|
-
height: this.#atlasCoords.uvScaleCropped.height / this.#atlasCoords.uvScale.height
|
|
17920
|
+
this.#transform = {
|
|
17921
|
+
position: opts?.position ?? { x: 0, y: 0 },
|
|
17922
|
+
scale: { x: 1, y: 1 },
|
|
17923
|
+
size: opts?.size ?? { width: 1, height: 1 },
|
|
17924
|
+
rotation: opts?.rotationRadians ?? 0
|
|
17763
17925
|
};
|
|
17764
|
-
|
|
17765
|
-
|
|
17766
|
-
|
|
17767
|
-
|
|
17768
|
-
|
|
17769
|
-
|
|
17770
|
-
this.#
|
|
17771
|
-
|
|
17772
|
-
|
|
17773
|
-
|
|
17774
|
-
|
|
17775
|
-
|
|
17926
|
+
if (opts?.scale)
|
|
17927
|
+
this.scale = opts.scale;
|
|
17928
|
+
if (opts?.rotation)
|
|
17929
|
+
this.rotation = opts.rotation;
|
|
17930
|
+
this.#matrix = mat3.identity();
|
|
17931
|
+
this.#renderComponent = opts?.render ?? null;
|
|
17932
|
+
this.#layer = opts?.layer ?? null;
|
|
17933
|
+
this.#isActive = opts?.isActive ?? true;
|
|
17934
|
+
this.label = opts?.label ?? undefined;
|
|
17935
|
+
this.#size = opts?.size ?? null;
|
|
17936
|
+
this.#key = opts?.key ?? null;
|
|
17937
|
+
for (const kid of opts?.kids ?? []) {
|
|
17938
|
+
this.add(kid);
|
|
17776
17939
|
}
|
|
17777
|
-
|
|
17778
|
-
|
|
17779
|
-
|
|
17780
|
-
|
|
17781
|
-
|
|
17782
|
-
|
|
17783
|
-
|
|
17784
|
-
|
|
17785
|
-
|
|
17786
|
-
|
|
17787
|
-
|
|
17788
|
-
|
|
17789
|
-
|
|
17790
|
-
|
|
17791
|
-
|
|
17792
|
-
|
|
17793
|
-
|
|
17794
|
-
|
|
17795
|
-
|
|
17796
|
-
|
|
17797
|
-
|
|
17798
|
-
|
|
17799
|
-
|
|
17800
|
-
|
|
17801
|
-
|
|
17802
|
-
|
|
17803
|
-
|
|
17804
|
-
|
|
17805
|
-
|
|
17806
|
-
|
|
17807
|
-
|
|
17808
|
-
|
|
17809
|
-
|
|
17810
|
-
get cropOffset() {
|
|
17811
|
-
return this.#cropOffset;
|
|
17812
|
-
}
|
|
17813
|
-
set cropOffset(value) {
|
|
17814
|
-
this.#cropOffset = value;
|
|
17815
|
-
}
|
|
17816
|
-
get textureId() {
|
|
17817
|
-
return this.#textureId;
|
|
17818
|
-
}
|
|
17819
|
-
get isPrimitive() {
|
|
17820
|
-
return this.#textureId === PRIMITIVE_TEXTURE;
|
|
17821
|
-
}
|
|
17822
|
-
get isCircle() {
|
|
17823
|
-
return this.#atlasCoords.atlasIndex === CIRCLE_INDEX;
|
|
17940
|
+
const self = this;
|
|
17941
|
+
this.#positionProxy = {
|
|
17942
|
+
get x() {
|
|
17943
|
+
return self.#transform.position.x;
|
|
17944
|
+
},
|
|
17945
|
+
set x(value) {
|
|
17946
|
+
self.#transform.position.x = value;
|
|
17947
|
+
self.setDirty();
|
|
17948
|
+
},
|
|
17949
|
+
get y() {
|
|
17950
|
+
return self.#transform.position.y;
|
|
17951
|
+
},
|
|
17952
|
+
set y(value) {
|
|
17953
|
+
self.#transform.position.y = value;
|
|
17954
|
+
self.setDirty();
|
|
17955
|
+
}
|
|
17956
|
+
};
|
|
17957
|
+
this.#scaleProxy = {
|
|
17958
|
+
get x() {
|
|
17959
|
+
return self.#transform.scale.x;
|
|
17960
|
+
},
|
|
17961
|
+
set x(value) {
|
|
17962
|
+
self.#transform.scale.x = value;
|
|
17963
|
+
self.setDirty();
|
|
17964
|
+
},
|
|
17965
|
+
get y() {
|
|
17966
|
+
return self.#transform.scale.y;
|
|
17967
|
+
},
|
|
17968
|
+
set y(value) {
|
|
17969
|
+
self.#transform.scale.y = value;
|
|
17970
|
+
self.setDirty();
|
|
17971
|
+
}
|
|
17972
|
+
};
|
|
17824
17973
|
}
|
|
17825
|
-
|
|
17826
|
-
|
|
17827
|
-
|
|
17828
|
-
|
|
17829
|
-
|
|
17830
|
-
|
|
17831
|
-
},
|
|
17832
|
-
atlasSize: () => {
|
|
17833
|
-
return this.#atlasSize;
|
|
17974
|
+
add(kid, index) {
|
|
17975
|
+
kid.#parent = this;
|
|
17976
|
+
if (index === undefined) {
|
|
17977
|
+
this.#kids.push(kid);
|
|
17978
|
+
} else {
|
|
17979
|
+
this.#kids.splice(index, 0, kid);
|
|
17834
17980
|
}
|
|
17835
|
-
|
|
17836
|
-
|
|
17837
|
-
function writeQuadInstance(node, array, offset) {
|
|
17838
|
-
if (!(node instanceof QuadNode)) {
|
|
17839
|
-
throw new Error("QuadNode.writeInstance can only be called on QuadNodes");
|
|
17981
|
+
kid.setDirty();
|
|
17982
|
+
return kid;
|
|
17840
17983
|
}
|
|
17841
|
-
|
|
17842
|
-
|
|
17843
|
-
const region = node.region;
|
|
17844
|
-
if (node.textureId === PRIMITIVE_TEXTURE) {
|
|
17845
|
-
array.set([
|
|
17846
|
-
node.atlasCoords.uvOffset.x,
|
|
17847
|
-
node.atlasCoords.uvOffset.y,
|
|
17848
|
-
node.atlasCoords.uvScale.width,
|
|
17849
|
-
node.atlasCoords.uvScale.height
|
|
17850
|
-
], offset + 16);
|
|
17851
|
-
} else {
|
|
17852
|
-
const atlasSize = node.extra.atlasSize();
|
|
17853
|
-
array.set([
|
|
17854
|
-
node.atlasCoords.uvOffset.x + region.x / atlasSize.width,
|
|
17855
|
-
node.atlasCoords.uvOffset.y + region.y / atlasSize.height,
|
|
17856
|
-
region.width / atlasSize.width,
|
|
17857
|
-
region.height / atlasSize.height
|
|
17858
|
-
], offset + 16);
|
|
17984
|
+
get kids() {
|
|
17985
|
+
return this.#kids;
|
|
17859
17986
|
}
|
|
17860
|
-
|
|
17861
|
-
|
|
17862
|
-
node.cropOffset.y / 2 / (node.atlasCoords.originalSize.height || 1),
|
|
17863
|
-
node.extra.cropRatio().width,
|
|
17864
|
-
node.extra.cropRatio().height
|
|
17865
|
-
], offset + 20);
|
|
17866
|
-
new DataView(array.buffer).setUint32(array.byteOffset + (offset + 24) * Float32Array.BYTES_PER_ELEMENT, node.atlasCoords.atlasIndex, true);
|
|
17867
|
-
node.writeInstance?.(array, offset + 28);
|
|
17868
|
-
return 1;
|
|
17869
|
-
}
|
|
17870
|
-
|
|
17871
|
-
// src/scene/JumboQuadNode.ts
|
|
17872
|
-
var MAT3_SIZE = 12;
|
|
17873
|
-
var VEC4F_SIZE = 4;
|
|
17874
|
-
|
|
17875
|
-
class JumboQuadNode extends QuadNode {
|
|
17876
|
-
#tiles;
|
|
17877
|
-
#matrixPool;
|
|
17878
|
-
constructor(options, matrixPool) {
|
|
17879
|
-
assert(options.shader, "JumboQuadNode requires a shader to be explicitly provided");
|
|
17880
|
-
assert(options.tiles && options.tiles.length > 0, "JumboQuadNode requires at least one tile to be provided");
|
|
17881
|
-
options.render ??= {
|
|
17882
|
-
shader: options.shader,
|
|
17883
|
-
writeInstance: writeJumboQuadInstance
|
|
17884
|
-
};
|
|
17885
|
-
super({
|
|
17886
|
-
...options,
|
|
17887
|
-
atlasCoords: options.tiles[0].atlasCoords
|
|
17888
|
-
}, matrixPool);
|
|
17889
|
-
this.#matrixPool = matrixPool;
|
|
17890
|
-
this.#tiles = [];
|
|
17891
|
-
for (const tile of options.tiles) {
|
|
17892
|
-
assert(tile.atlasCoords, "JumboQuadNode requires atlas coords to be provided");
|
|
17893
|
-
assert(tile.size, "JumboQuadNode requires a size to be provided");
|
|
17894
|
-
this.#tiles.push({
|
|
17895
|
-
textureId: tile.textureId,
|
|
17896
|
-
offset: tile.offset,
|
|
17897
|
-
size: tile.size,
|
|
17898
|
-
atlasCoords: tile.atlasCoords
|
|
17899
|
-
});
|
|
17900
|
-
}
|
|
17987
|
+
get children() {
|
|
17988
|
+
return this.#kids;
|
|
17901
17989
|
}
|
|
17902
|
-
get
|
|
17903
|
-
|
|
17990
|
+
get transform() {
|
|
17991
|
+
return this.#transform;
|
|
17904
17992
|
}
|
|
17905
|
-
get
|
|
17906
|
-
return this.#
|
|
17993
|
+
get key() {
|
|
17994
|
+
return this.#key ?? "";
|
|
17907
17995
|
}
|
|
17908
|
-
|
|
17909
|
-
|
|
17910
|
-
const originalSize = {
|
|
17911
|
-
width: Math.max(...this.#tiles.map((t3) => t3.offset.x + t3.size.width)),
|
|
17912
|
-
height: Math.max(...this.#tiles.map((t3) => t3.offset.y + t3.size.height))
|
|
17913
|
-
};
|
|
17914
|
-
const proportionalSize = {
|
|
17915
|
-
width: this.size.width / originalSize.width,
|
|
17916
|
-
height: this.size.height / originalSize.height
|
|
17917
|
-
};
|
|
17918
|
-
const centerOffset = {
|
|
17919
|
-
x: tile.offset.x + tile.size.width / 2 - originalSize.width / 2,
|
|
17920
|
-
y: -(tile.offset.y + tile.size.height / 2 - originalSize.height / 2)
|
|
17921
|
-
};
|
|
17922
|
-
mat3.translate(matrix, [
|
|
17923
|
-
centerOffset.x * proportionalSize.width,
|
|
17924
|
-
centerOffset.y * proportionalSize.height
|
|
17925
|
-
], matrix);
|
|
17926
|
-
mat3.scale(matrix, [
|
|
17927
|
-
tile.size.width * proportionalSize.width * (this.flipX ? -1 : 1),
|
|
17928
|
-
tile.size.height * proportionalSize.height * (this.flipY ? -1 : 1)
|
|
17929
|
-
], matrix);
|
|
17930
|
-
return matrix;
|
|
17996
|
+
get parent() {
|
|
17997
|
+
return this.#parent;
|
|
17931
17998
|
}
|
|
17932
|
-
|
|
17933
|
-
|
|
17934
|
-
|
|
17935
|
-
throw new Error("JumboQuadNode.writeJumboQuadInstance can only be called on JumboQuadNodes");
|
|
17999
|
+
set position(value) {
|
|
18000
|
+
this.#transform.position = value;
|
|
18001
|
+
this.setDirty();
|
|
17936
18002
|
}
|
|
17937
|
-
|
|
17938
|
-
|
|
17939
|
-
const coord = tile.atlasCoords;
|
|
17940
|
-
const matrix = node.getTileMatrix(tile);
|
|
17941
|
-
array.set(matrix, offset + tileOffset);
|
|
17942
|
-
tileOffset += MAT3_SIZE;
|
|
17943
|
-
array.set([node.color.r, node.color.g, node.color.b, node.color.a], offset + tileOffset);
|
|
17944
|
-
tileOffset += VEC4F_SIZE;
|
|
17945
|
-
array.set([
|
|
17946
|
-
coord.uvOffset.x,
|
|
17947
|
-
coord.uvOffset.y,
|
|
17948
|
-
coord.uvScale.width,
|
|
17949
|
-
coord.uvScale.height
|
|
17950
|
-
], offset + tileOffset);
|
|
17951
|
-
tileOffset += VEC4F_SIZE;
|
|
17952
|
-
const cropRatio = !coord.uvScaleCropped ? { width: 1, height: 1 } : {
|
|
17953
|
-
width: coord.uvScaleCropped.width / coord.uvScale.width,
|
|
17954
|
-
height: coord.uvScaleCropped.height / coord.uvScale.height
|
|
17955
|
-
};
|
|
17956
|
-
array.set([
|
|
17957
|
-
tile.atlasCoords.cropOffset.x / 2 / (tile.atlasCoords.originalSize.width || 1),
|
|
17958
|
-
tile.atlasCoords.cropOffset.y / 2 / (tile.atlasCoords.originalSize.height || 1),
|
|
17959
|
-
cropRatio.width,
|
|
17960
|
-
cropRatio.height
|
|
17961
|
-
], offset + tileOffset);
|
|
17962
|
-
tileOffset += VEC4F_SIZE;
|
|
17963
|
-
new DataView(array.buffer).setUint32(array.byteOffset + (offset + tileOffset) * Float32Array.BYTES_PER_ELEMENT, coord.atlasIndex, true);
|
|
17964
|
-
tileOffset += VEC4F_SIZE;
|
|
18003
|
+
get position() {
|
|
18004
|
+
return this.#positionProxy;
|
|
17965
18005
|
}
|
|
17966
|
-
|
|
17967
|
-
|
|
17968
|
-
|
|
17969
|
-
|
|
17970
|
-
// src/utils/error.ts
|
|
17971
|
-
var warnings = new Map;
|
|
17972
|
-
function warnOnce(key, msg) {
|
|
17973
|
-
if (warnings.has(key)) {
|
|
17974
|
-
return;
|
|
18006
|
+
set x(value) {
|
|
18007
|
+
this.#transform.position.x = value;
|
|
18008
|
+
this.setDirty();
|
|
17975
18009
|
}
|
|
17976
|
-
|
|
17977
|
-
|
|
17978
|
-
}
|
|
17979
|
-
|
|
17980
|
-
// src/text/MsdfFont.ts
|
|
17981
|
-
class MsdfFont {
|
|
17982
|
-
id;
|
|
17983
|
-
json;
|
|
17984
|
-
imageBitmap;
|
|
17985
|
-
name;
|
|
17986
|
-
charset;
|
|
17987
|
-
charCount;
|
|
17988
|
-
lineHeight;
|
|
17989
|
-
charBuffer;
|
|
17990
|
-
#kernings;
|
|
17991
|
-
#chars;
|
|
17992
|
-
#fallbackCharCode;
|
|
17993
|
-
constructor(id, json, imageBitmap) {
|
|
17994
|
-
this.id = id;
|
|
17995
|
-
this.json = json;
|
|
17996
|
-
this.imageBitmap = imageBitmap;
|
|
17997
|
-
const charArray = Object.values(json.chars);
|
|
17998
|
-
this.charCount = charArray.length;
|
|
17999
|
-
this.lineHeight = json.common.lineHeight;
|
|
18000
|
-
this.charset = json.info.charset;
|
|
18001
|
-
this.name = json.info.face;
|
|
18002
|
-
this.#kernings = new Map;
|
|
18003
|
-
if (json.kernings) {
|
|
18004
|
-
for (const kearning of json.kernings) {
|
|
18005
|
-
let charKerning = this.#kernings.get(kearning.first);
|
|
18006
|
-
if (!charKerning) {
|
|
18007
|
-
charKerning = new Map;
|
|
18008
|
-
this.#kernings.set(kearning.first, charKerning);
|
|
18009
|
-
}
|
|
18010
|
-
charKerning.set(kearning.second, kearning.amount);
|
|
18011
|
-
}
|
|
18012
|
-
}
|
|
18013
|
-
this.#chars = new Map;
|
|
18014
|
-
const charCount = Object.values(json.chars).length;
|
|
18015
|
-
this.charBuffer = new Float32Array(charCount * 8);
|
|
18016
|
-
let offset = 0;
|
|
18017
|
-
const u3 = 1 / json.common.scaleW;
|
|
18018
|
-
const v3 = 1 / json.common.scaleH;
|
|
18019
|
-
for (const [i3, char] of json.chars.entries()) {
|
|
18020
|
-
this.#chars.set(char.id, char);
|
|
18021
|
-
this.#chars.get(char.id).charIndex = i3;
|
|
18022
|
-
this.charBuffer[offset] = char.x * u3;
|
|
18023
|
-
this.charBuffer[offset + 1] = char.y * v3;
|
|
18024
|
-
this.charBuffer[offset + 2] = char.width * u3;
|
|
18025
|
-
this.charBuffer[offset + 3] = char.height * v3;
|
|
18026
|
-
this.charBuffer[offset + 4] = char.width;
|
|
18027
|
-
this.charBuffer[offset + 5] = char.height;
|
|
18028
|
-
this.charBuffer[offset + 6] = char.xoffset;
|
|
18029
|
-
this.charBuffer[offset + 7] = -char.yoffset;
|
|
18030
|
-
offset += 8;
|
|
18031
|
-
}
|
|
18010
|
+
get x() {
|
|
18011
|
+
return this.#transform.position.x;
|
|
18032
18012
|
}
|
|
18033
|
-
|
|
18034
|
-
|
|
18035
|
-
|
|
18036
|
-
const fallbackCharacter = this.#chars.get(this.#fallbackCharCode ?? this.#chars.keys().toArray()[0]);
|
|
18037
|
-
warnOnce(`unknown_char_${this.name}`, `Couldn't find character ${charCode} in characters for font ${this.name} -- defaulting to first available character "${fallbackCharacter.char}"`);
|
|
18038
|
-
return fallbackCharacter;
|
|
18039
|
-
}
|
|
18040
|
-
return char;
|
|
18013
|
+
set y(value) {
|
|
18014
|
+
this.#transform.position.y = value;
|
|
18015
|
+
this.setDirty();
|
|
18041
18016
|
}
|
|
18042
|
-
|
|
18043
|
-
|
|
18044
|
-
if (nextCharCode >= 0) {
|
|
18045
|
-
const kerning = this.#kernings.get(charCode);
|
|
18046
|
-
if (kerning) {
|
|
18047
|
-
return char.xadvance + (kerning.get(nextCharCode) ?? 0);
|
|
18048
|
-
}
|
|
18049
|
-
}
|
|
18050
|
-
return char.xadvance;
|
|
18017
|
+
get y() {
|
|
18018
|
+
return this.#transform.position.y;
|
|
18051
18019
|
}
|
|
18052
|
-
|
|
18053
|
-
|
|
18054
|
-
|
|
18055
|
-
const i3 = fontJsonUrl.href.lastIndexOf("/");
|
|
18056
|
-
const baseUrl = i3 !== -1 ? fontJsonUrl.href.substring(0, i3 + 1) : undefined;
|
|
18057
|
-
if (json.pages.length < 1) {
|
|
18058
|
-
throw new Error(`Can't create an msdf font without a reference to the page url in the json`);
|
|
18059
|
-
}
|
|
18060
|
-
if (json.pages.length > 1) {
|
|
18061
|
-
throw new Error(`Can't create an msdf font with more than one page`);
|
|
18062
|
-
}
|
|
18063
|
-
const textureUrl = baseUrl + json.pages[0];
|
|
18064
|
-
const textureResponse = await fetch(textureUrl);
|
|
18065
|
-
const bitmap = await createImageBitmap(await textureResponse.blob());
|
|
18066
|
-
return new MsdfFont(id, json, bitmap);
|
|
18020
|
+
set rotation(value) {
|
|
18021
|
+
this.#transform.rotation = deg2rad(value);
|
|
18022
|
+
this.setDirty();
|
|
18067
18023
|
}
|
|
18068
|
-
|
|
18069
|
-
|
|
18070
|
-
|
|
18071
|
-
|
|
18024
|
+
get rotation() {
|
|
18025
|
+
return rad2deg(this.#transform.rotation);
|
|
18026
|
+
}
|
|
18027
|
+
get rotationRadians() {
|
|
18028
|
+
return this.#transform.rotation;
|
|
18029
|
+
}
|
|
18030
|
+
set rotationRadians(value) {
|
|
18031
|
+
this.#transform.rotation = value;
|
|
18032
|
+
this.setDirty();
|
|
18033
|
+
}
|
|
18034
|
+
get scale() {
|
|
18035
|
+
return this.#scaleProxy;
|
|
18036
|
+
}
|
|
18037
|
+
set scale(value) {
|
|
18038
|
+
if (typeof value === "number") {
|
|
18039
|
+
this.#transform.scale = { x: value, y: value };
|
|
18072
18040
|
} else {
|
|
18073
|
-
|
|
18074
|
-
console.warn(`${character} character does not exist in font ${this.name} defaulting to "${this.#chars.get(fallbackCode)?.char}".`);
|
|
18075
|
-
this.#fallbackCharCode = fallbackCode;
|
|
18041
|
+
this.#transform.scale = value;
|
|
18076
18042
|
}
|
|
18043
|
+
this.setDirty();
|
|
18077
18044
|
}
|
|
18078
|
-
|
|
18079
|
-
|
|
18080
|
-
|
|
18081
|
-
var TAB_SPACES = 4;
|
|
18082
|
-
function shapeText(font, text, blockSize, fontSize, formatting, textArray, initialFloatOffset = 0, debug = false) {
|
|
18083
|
-
let offset = initialFloatOffset;
|
|
18084
|
-
const measurements = measureText(font, text, formatting.wordWrap);
|
|
18085
|
-
const alignment = formatting.align || "left";
|
|
18086
|
-
const em2px = fontSize / font.lineHeight;
|
|
18087
|
-
const hackHasExplicitBlock = blockSize.width !== measurements.width;
|
|
18088
|
-
let debugData = null;
|
|
18089
|
-
if (debug) {
|
|
18090
|
-
debugData = [];
|
|
18045
|
+
set size(value) {
|
|
18046
|
+
this.#size = value;
|
|
18047
|
+
this.setDirty();
|
|
18091
18048
|
}
|
|
18092
|
-
|
|
18093
|
-
|
|
18094
|
-
|
|
18095
|
-
|
|
18096
|
-
|
|
18097
|
-
|
|
18098
|
-
|
|
18099
|
-
|
|
18100
|
-
|
|
18101
|
-
|
|
18102
|
-
|
|
18103
|
-
|
|
18104
|
-
}
|
|
18105
|
-
|
|
18106
|
-
|
|
18107
|
-
|
|
18108
|
-
|
|
18109
|
-
|
|
18110
|
-
startX: word.startX,
|
|
18111
|
-
glyphX: glyph.offset[0],
|
|
18112
|
-
advance: glyph.char.xadvance,
|
|
18113
|
-
lineOffset,
|
|
18114
|
-
startY: word.startY,
|
|
18115
|
-
glyphY: glyph.offset[1]
|
|
18116
|
-
});
|
|
18049
|
+
get size() {
|
|
18050
|
+
return this.#size;
|
|
18051
|
+
}
|
|
18052
|
+
get aspectRatio() {
|
|
18053
|
+
if (!this.#size) {
|
|
18054
|
+
console.warn("Attempted to get aspect ratio of a node with no ideal size");
|
|
18055
|
+
return 1;
|
|
18056
|
+
}
|
|
18057
|
+
return this.#size.width / this.#size.height;
|
|
18058
|
+
}
|
|
18059
|
+
get isActive() {
|
|
18060
|
+
if (!this.#cache?.isActive) {
|
|
18061
|
+
this.#cache ??= {};
|
|
18062
|
+
let parent = this;
|
|
18063
|
+
let isActive = this.#isActive;
|
|
18064
|
+
while (isActive && parent.#parent) {
|
|
18065
|
+
parent = parent.#parent;
|
|
18066
|
+
isActive = isActive && parent.#isActive;
|
|
18117
18067
|
}
|
|
18118
|
-
|
|
18119
|
-
textArray[offset + 1] = word.startY + glyph.offset[1];
|
|
18120
|
-
textArray[offset + 2] = glyph.char.charIndex;
|
|
18121
|
-
offset += 4;
|
|
18068
|
+
this.#cache.isActive = isActive;
|
|
18122
18069
|
}
|
|
18070
|
+
return this.#cache.isActive;
|
|
18123
18071
|
}
|
|
18124
|
-
|
|
18125
|
-
|
|
18072
|
+
set isActive(value) {
|
|
18073
|
+
this.#isActive = value;
|
|
18074
|
+
this.setDirty();
|
|
18126
18075
|
}
|
|
18127
|
-
|
|
18128
|
-
|
|
18129
|
-
|
|
18130
|
-
|
|
18131
|
-
|
|
18132
|
-
|
|
18133
|
-
|
|
18134
|
-
|
|
18135
|
-
|
|
18136
|
-
|
|
18137
|
-
|
|
18138
|
-
|
|
18139
|
-
const isLastLetter = i3 === text.length - 1;
|
|
18140
|
-
const charCode = nextCharCode;
|
|
18141
|
-
nextCharCode = i3 < text.length - 1 ? text.charCodeAt(i3 + 1) : -1;
|
|
18142
|
-
switch (charCode) {
|
|
18143
|
-
case 9 /* HorizontalTab */:
|
|
18144
|
-
insertSpaces(TAB_SPACES);
|
|
18145
|
-
break;
|
|
18146
|
-
case 10 /* Newline */:
|
|
18147
|
-
flushLine();
|
|
18148
|
-
flushWord();
|
|
18149
|
-
break;
|
|
18150
|
-
case 13 /* CarriageReturn */:
|
|
18151
|
-
break;
|
|
18152
|
-
case 32 /* Space */:
|
|
18153
|
-
insertSpaces(1);
|
|
18154
|
-
break;
|
|
18155
|
-
default: {
|
|
18156
|
-
const advance = font.getXAdvance(charCode, nextCharCode);
|
|
18157
|
-
if (wordWrap && wordWrap.breakOn === "character" && textOffsetX + advance > wordWrap.emWidth) {
|
|
18158
|
-
if (word.startX === 0) {
|
|
18159
|
-
flushWord();
|
|
18160
|
-
} else {
|
|
18161
|
-
lineWidths.push(textOffsetX - word.width);
|
|
18162
|
-
line++;
|
|
18163
|
-
maxWidth = Math.max(maxWidth, textOffsetX);
|
|
18164
|
-
textOffsetX = word.width;
|
|
18165
|
-
textOffsetY -= font.lineHeight;
|
|
18166
|
-
word.startX = 0;
|
|
18167
|
-
word.startY = textOffsetY;
|
|
18168
|
-
word.glyphs.forEach((g3) => {
|
|
18169
|
-
g3.line = line;
|
|
18170
|
-
});
|
|
18171
|
-
}
|
|
18172
|
-
}
|
|
18173
|
-
word.glyphs.push({
|
|
18174
|
-
char: font.getChar(charCode),
|
|
18175
|
-
offset: [word.width, 0],
|
|
18176
|
-
line
|
|
18177
|
-
});
|
|
18178
|
-
if (isLastLetter) {
|
|
18179
|
-
flushWord();
|
|
18076
|
+
get layer() {
|
|
18077
|
+
if (this.#layer != null) {
|
|
18078
|
+
return this.#layer;
|
|
18079
|
+
}
|
|
18080
|
+
if (!this.#cache?.layer) {
|
|
18081
|
+
this.#cache ??= {};
|
|
18082
|
+
let parent = this;
|
|
18083
|
+
while (parent.#parent) {
|
|
18084
|
+
parent = parent.#parent;
|
|
18085
|
+
if (parent.hasExplicitLayer) {
|
|
18086
|
+
this.#cache.layer = parent.#layer;
|
|
18087
|
+
return this.#cache.layer;
|
|
18180
18088
|
}
|
|
18181
|
-
word.width += advance;
|
|
18182
|
-
textOffsetX += advance;
|
|
18183
18089
|
}
|
|
18090
|
+
this.#cache.layer = 0;
|
|
18184
18091
|
}
|
|
18092
|
+
return this.#cache.layer;
|
|
18185
18093
|
}
|
|
18186
|
-
|
|
18187
|
-
|
|
18188
|
-
const lineCount = lineWidths.length;
|
|
18189
|
-
return {
|
|
18190
|
-
width: maxWidth,
|
|
18191
|
-
height: lineCount * font.lineHeight,
|
|
18192
|
-
lineWidths,
|
|
18193
|
-
lineCount,
|
|
18194
|
-
printedCharCount,
|
|
18195
|
-
words
|
|
18196
|
-
};
|
|
18197
|
-
function flushWord() {
|
|
18198
|
-
printedCharCount += word.glyphs.length;
|
|
18199
|
-
words.push(word);
|
|
18200
|
-
word = {
|
|
18201
|
-
glyphs: [],
|
|
18202
|
-
width: 0,
|
|
18203
|
-
startX: textOffsetX,
|
|
18204
|
-
startY: textOffsetY
|
|
18205
|
-
};
|
|
18094
|
+
get hasExplicitLayer() {
|
|
18095
|
+
return this.#layer != null;
|
|
18206
18096
|
}
|
|
18207
|
-
|
|
18208
|
-
|
|
18209
|
-
|
|
18210
|
-
maxWidth = Math.max(maxWidth, textOffsetX);
|
|
18211
|
-
textOffsetX = 0;
|
|
18212
|
-
textOffsetY -= font.lineHeight;
|
|
18097
|
+
set layer(value) {
|
|
18098
|
+
this.#layer = value;
|
|
18099
|
+
this.setDirty();
|
|
18213
18100
|
}
|
|
18214
|
-
|
|
18215
|
-
|
|
18216
|
-
|
|
18217
|
-
|
|
18218
|
-
if (
|
|
18219
|
-
|
|
18101
|
+
get renderComponent() {
|
|
18102
|
+
return this.#renderComponent;
|
|
18103
|
+
}
|
|
18104
|
+
get matrix() {
|
|
18105
|
+
if (!this.#cache?.matrix) {
|
|
18106
|
+
this.#cache ??= {};
|
|
18107
|
+
if (this.#parent) {
|
|
18108
|
+
mat3.clone(this.#parent.matrix, this.#matrix);
|
|
18109
|
+
} else {
|
|
18110
|
+
mat3.identity(this.#matrix);
|
|
18111
|
+
}
|
|
18112
|
+
this.#cache.matrix = createModelMatrix(this.transform, this.#matrix);
|
|
18113
|
+
}
|
|
18114
|
+
return this.#cache.matrix;
|
|
18115
|
+
}
|
|
18116
|
+
get bounds() {
|
|
18117
|
+
if (!this.#cache?.bounds) {
|
|
18118
|
+
this.#cache ??= {};
|
|
18119
|
+
const height = this.size?.height ?? 0;
|
|
18120
|
+
const width = this.size?.width ?? 0;
|
|
18121
|
+
const corners = [
|
|
18122
|
+
vec2.transformMat3([-width / 2, height / 2], this.matrix),
|
|
18123
|
+
vec2.transformMat3([width / 2, height / 2], this.matrix),
|
|
18124
|
+
vec2.transformMat3([-width / 2, -height / 2], this.matrix),
|
|
18125
|
+
vec2.transformMat3([width / 2, -height / 2], this.matrix)
|
|
18126
|
+
];
|
|
18127
|
+
const center = vec2.transformMat3([0, 0], this.matrix);
|
|
18128
|
+
const xValues = corners.map((c3) => c3[0]);
|
|
18129
|
+
const yValues = corners.map((c3) => c3[1]);
|
|
18130
|
+
this.#cache.bounds = {
|
|
18131
|
+
x: center[0],
|
|
18132
|
+
y: center[1],
|
|
18133
|
+
left: Math.min(xValues[0], xValues[1], xValues[2], xValues[3]),
|
|
18134
|
+
right: Math.max(xValues[0], xValues[1], xValues[2], xValues[3]),
|
|
18135
|
+
top: Math.max(yValues[0], yValues[1], yValues[2], yValues[3]),
|
|
18136
|
+
bottom: Math.min(yValues[0], yValues[1], yValues[2], yValues[3])
|
|
18137
|
+
};
|
|
18220
18138
|
}
|
|
18221
|
-
|
|
18139
|
+
return this.#cache.bounds;
|
|
18222
18140
|
}
|
|
18223
|
-
|
|
18224
|
-
|
|
18225
|
-
|
|
18226
|
-
|
|
18141
|
+
setBounds(bounds) {
|
|
18142
|
+
if (bounds.left !== undefined)
|
|
18143
|
+
this.left = bounds.left;
|
|
18144
|
+
if (bounds.right !== undefined)
|
|
18145
|
+
this.right = bounds.right;
|
|
18146
|
+
if (bounds.top !== undefined)
|
|
18147
|
+
this.top = bounds.top;
|
|
18148
|
+
if (bounds.bottom !== undefined)
|
|
18149
|
+
this.bottom = bounds.bottom;
|
|
18150
|
+
if (bounds.x !== undefined)
|
|
18151
|
+
this.centerX = bounds.x;
|
|
18152
|
+
if (bounds.y !== undefined)
|
|
18153
|
+
this.centerY = bounds.y;
|
|
18154
|
+
return this;
|
|
18227
18155
|
}
|
|
18228
|
-
|
|
18229
|
-
|
|
18156
|
+
set left(value) {
|
|
18157
|
+
this.#adjustWorldPosition([value - this.bounds.left, 0]);
|
|
18230
18158
|
}
|
|
18231
|
-
|
|
18232
|
-
|
|
18233
|
-
const maxLines = formatting.shrinkToFit.maxLines ?? Number.POSITIVE_INFINITY;
|
|
18234
|
-
const threshold = 0.5;
|
|
18235
|
-
let low = minSize;
|
|
18236
|
-
let high = maxSize;
|
|
18237
|
-
while (high - low > threshold) {
|
|
18238
|
-
const testSize = (low + high) / 2;
|
|
18239
|
-
const testMeasure = measureText(font, text, formatting.wordWrap);
|
|
18240
|
-
const padding = formatting.shrinkToFit.padding ?? 0;
|
|
18241
|
-
const scaledWidth = testMeasure.width * (testSize / font.lineHeight);
|
|
18242
|
-
const scaledHeight = testMeasure.height * (testSize / font.lineHeight);
|
|
18243
|
-
const fitsWidth = scaledWidth <= size.width - size.width * padding;
|
|
18244
|
-
const fitsHeight = scaledHeight <= size.height - size.height * padding;
|
|
18245
|
-
const fitsLines = testMeasure.lineCount <= maxLines;
|
|
18246
|
-
if (fitsWidth && fitsHeight && fitsLines) {
|
|
18247
|
-
low = testSize;
|
|
18248
|
-
} else {
|
|
18249
|
-
high = testSize;
|
|
18250
|
-
}
|
|
18159
|
+
set bottom(value) {
|
|
18160
|
+
this.#adjustWorldPosition([0, value - this.bounds.bottom]);
|
|
18251
18161
|
}
|
|
18252
|
-
|
|
18253
|
-
|
|
18254
|
-
|
|
18255
|
-
// src/text/TextNode.ts
|
|
18256
|
-
var DEFAULT_FONT_SIZE = 14;
|
|
18257
|
-
|
|
18258
|
-
class TextNode extends SceneNode {
|
|
18259
|
-
#text;
|
|
18260
|
-
#formatting;
|
|
18261
|
-
#font;
|
|
18262
|
-
constructor(shader, text, opts = {}) {
|
|
18263
|
-
const { width, height } = measureText(shader.font, text, opts.wordWrap);
|
|
18264
|
-
if (text.length > shader.maxCharCount) {
|
|
18265
|
-
throw new Error(`Text: ${text} exceeds ${shader.maxCharCount} characters. Try using fewer characters or increase the limit in Toodle.attach.`);
|
|
18266
|
-
}
|
|
18267
|
-
const em2px = shader.font.lineHeight / (opts.fontSize ?? DEFAULT_FONT_SIZE);
|
|
18268
|
-
if (!opts.shrinkToFit && !opts.size) {
|
|
18269
|
-
opts.size = { width: width / em2px, height: height / em2px };
|
|
18270
|
-
}
|
|
18271
|
-
super({
|
|
18272
|
-
...opts,
|
|
18273
|
-
render: {
|
|
18274
|
-
shader,
|
|
18275
|
-
writeInstance: (_node, _array, _offset) => {
|
|
18276
|
-
throw new Error("not implemented - needs access to text uniform buffer, dimensions and a model matrix");
|
|
18277
|
-
}
|
|
18278
|
-
}
|
|
18279
|
-
});
|
|
18280
|
-
this.#font = shader.font;
|
|
18281
|
-
this.#text = text;
|
|
18282
|
-
this.#formatting = opts;
|
|
18162
|
+
set top(value) {
|
|
18163
|
+
this.#adjustWorldPosition([0, value - this.bounds.top]);
|
|
18283
18164
|
}
|
|
18284
|
-
|
|
18285
|
-
|
|
18165
|
+
set right(value) {
|
|
18166
|
+
this.#adjustWorldPosition([value - this.bounds.right, 0]);
|
|
18286
18167
|
}
|
|
18287
|
-
|
|
18288
|
-
|
|
18168
|
+
set centerX(value) {
|
|
18169
|
+
this.#adjustWorldPosition([value - this.bounds.x, 0]);
|
|
18289
18170
|
}
|
|
18290
|
-
|
|
18291
|
-
|
|
18171
|
+
set centerY(value) {
|
|
18172
|
+
this.#adjustWorldPosition([0, value - this.bounds.y]);
|
|
18292
18173
|
}
|
|
18293
|
-
|
|
18294
|
-
|
|
18295
|
-
|
|
18174
|
+
delete() {
|
|
18175
|
+
this.#parent?.remove(this);
|
|
18176
|
+
for (const child of this.#kids) {
|
|
18177
|
+
child.delete();
|
|
18296
18178
|
}
|
|
18297
|
-
this.#
|
|
18298
|
-
this
|
|
18179
|
+
this.#kids = [];
|
|
18180
|
+
this.#isActive = false;
|
|
18181
|
+
this.#layer = null;
|
|
18182
|
+
this.#renderComponent = null;
|
|
18299
18183
|
}
|
|
18300
|
-
|
|
18301
|
-
|
|
18184
|
+
remove(kid) {
|
|
18185
|
+
const childIndex = this.#kids.findIndex((child) => child.id === kid.id);
|
|
18186
|
+
if (childIndex <= -1) {
|
|
18187
|
+
throw new Error(`${kid.label ?? kid.id} is not a child of ${this.label ?? this.id}`);
|
|
18188
|
+
}
|
|
18189
|
+
this.#kids.splice(childIndex, 1);
|
|
18190
|
+
kid.#parent = null;
|
|
18191
|
+
kid.setDirty();
|
|
18302
18192
|
}
|
|
18303
|
-
|
|
18304
|
-
this.#
|
|
18193
|
+
#adjustWorldPosition(delta) {
|
|
18194
|
+
const inverseMatrix = mat3.inverse(this.#parent?.matrix ?? mat3.identity());
|
|
18195
|
+
inverseMatrix[8] = inverseMatrix[9] = 0;
|
|
18196
|
+
const localDelta = vec2.transformMat3(delta, inverseMatrix);
|
|
18197
|
+
this.#transform.position.x += localDelta[0];
|
|
18198
|
+
this.#transform.position.y += localDelta[1];
|
|
18305
18199
|
this.setDirty();
|
|
18306
18200
|
}
|
|
18307
|
-
|
|
18308
|
-
this.#
|
|
18309
|
-
this.setDirty();
|
|
18201
|
+
setDirty() {
|
|
18202
|
+
this.#cache = null;
|
|
18203
|
+
this.#kids.forEach((kid) => kid.setDirty());
|
|
18204
|
+
}
|
|
18205
|
+
static parse(json) {
|
|
18206
|
+
const obj = JSON.parse(json, reviver);
|
|
18207
|
+
return new SceneNode(obj);
|
|
18208
|
+
}
|
|
18209
|
+
toJSON() {
|
|
18210
|
+
return {
|
|
18211
|
+
id: this.id,
|
|
18212
|
+
label: this.label,
|
|
18213
|
+
transform: this.#transform,
|
|
18214
|
+
layer: this.#layer,
|
|
18215
|
+
isActive: this.#isActive,
|
|
18216
|
+
kids: this.#kids,
|
|
18217
|
+
render: this.#renderComponent
|
|
18218
|
+
};
|
|
18310
18219
|
}
|
|
18311
18220
|
}
|
|
18312
|
-
|
|
18313
|
-
|
|
18314
|
-
|
|
18315
|
-
|
|
18316
|
-
|
|
18317
|
-
|
|
18318
|
-
|
|
18319
|
-
// Input texture from which to compute the non-transparent bounding box
|
|
18320
|
-
@group(0) @binding(0)
|
|
18321
|
-
var input_texture: texture_2d<f32>;
|
|
18322
|
-
|
|
18323
|
-
// Atomic bounding box storage structure
|
|
18324
|
-
struct bounding_box_atomic {
|
|
18325
|
-
min_x: atomic<u32>,
|
|
18326
|
-
min_y: atomic<u32>,
|
|
18327
|
-
max_x: atomic<u32>,
|
|
18328
|
-
max_y: atomic<u32>,
|
|
18329
|
-
};
|
|
18330
|
-
|
|
18331
|
-
// Storage buffer to hold atomic bounding box updates
|
|
18332
|
-
@group(0) @binding(1)
|
|
18333
|
-
var<storage, read_write> bounds: bounding_box_atomic;
|
|
18334
|
-
|
|
18335
|
-
// Compute shader to find the bounding box of non-transparent pixels
|
|
18336
|
-
@compute @workgroup_size(8, 8)
|
|
18337
|
-
fn find_bounds(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
18338
|
-
let size = textureDimensions(input_texture).xy;
|
|
18339
|
-
if (gid.x >= size.x || gid.y >= size.y) {
|
|
18340
|
-
return;
|
|
18341
|
-
}
|
|
18342
|
-
|
|
18343
|
-
let pixel = textureLoad(input_texture, vec2<i32>(gid.xy), 0);
|
|
18344
|
-
if (pixel.a > 0.0) {
|
|
18345
|
-
atomicMin(&bounds.min_x, gid.x);
|
|
18346
|
-
atomicMin(&bounds.min_y, gid.y);
|
|
18347
|
-
atomicMax(&bounds.max_x, gid.x);
|
|
18348
|
-
atomicMax(&bounds.max_y, gid.y);
|
|
18221
|
+
function reviver(key, value) {
|
|
18222
|
+
if (key === "kids") {
|
|
18223
|
+
return value.map((kid) => new SceneNode(kid));
|
|
18224
|
+
}
|
|
18225
|
+
if (Array.isArray(value) && value.every((v3) => typeof v3 === "number")) {
|
|
18226
|
+
if (value.length === 2) {
|
|
18227
|
+
return value;
|
|
18349
18228
|
}
|
|
18350
|
-
|
|
18351
|
-
|
|
18352
|
-
// ==============================
|
|
18353
|
-
// === CROP + OUTPUT PASS ======
|
|
18354
|
-
// ==============================
|
|
18355
|
-
|
|
18356
|
-
// Input texture from which cropped data is read
|
|
18357
|
-
@group(0) @binding(0)
|
|
18358
|
-
var input_texture_crop: texture_2d<f32>;
|
|
18359
|
-
|
|
18360
|
-
// Output texture where cropped image is written
|
|
18361
|
-
@group(0) @binding(1)
|
|
18362
|
-
var output_texture: texture_storage_2d<rgba8unorm, write>;
|
|
18363
|
-
|
|
18364
|
-
// Bounding box passed in as a uniform (not atomic anymore)
|
|
18365
|
-
struct bounding_box_uniform {
|
|
18366
|
-
min_x: u32,
|
|
18367
|
-
min_y: u32,
|
|
18368
|
-
max_x: u32,
|
|
18369
|
-
max_y: u32,
|
|
18370
|
-
};
|
|
18371
|
-
|
|
18372
|
-
@group(0) @binding(2)
|
|
18373
|
-
var<uniform> bounds_uniform: bounding_box_uniform;
|
|
18374
|
-
|
|
18375
|
-
// Struct to store both original and cropped texture dimensions
|
|
18376
|
-
struct image_dimensions {
|
|
18377
|
-
original_width: u32,
|
|
18378
|
-
original_height: u32,
|
|
18379
|
-
cropped_width: u32,
|
|
18380
|
-
cropped_height: u32,
|
|
18381
|
-
};
|
|
18382
|
-
|
|
18383
|
-
// Storage buffer to output the result dimensions
|
|
18384
|
-
@group(0) @binding(3)
|
|
18385
|
-
var<storage, read_write> dimensions_out: image_dimensions;
|
|
18386
|
-
|
|
18387
|
-
// Compute shader to crop the input texture to the bounding box and output it
|
|
18388
|
-
@compute @workgroup_size(8, 8)
|
|
18389
|
-
fn crop_and_output(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
18390
|
-
let size = textureDimensions(input_texture_crop).xy;
|
|
18391
|
-
|
|
18392
|
-
let crop_width = bounds_uniform.max_x - bounds_uniform.min_x + 1u;
|
|
18393
|
-
let crop_height = bounds_uniform.max_y - bounds_uniform.min_y + 1u;
|
|
18394
|
-
|
|
18395
|
-
if (gid.x >= crop_width || gid.y >= crop_height) {
|
|
18396
|
-
return;
|
|
18229
|
+
if (value.length === 3) {
|
|
18230
|
+
return value;
|
|
18397
18231
|
}
|
|
18398
|
-
|
|
18399
|
-
|
|
18400
|
-
i32(bounds_uniform.min_x + gid.x),
|
|
18401
|
-
i32(bounds_uniform.min_y + gid.y)
|
|
18402
|
-
);
|
|
18403
|
-
|
|
18404
|
-
let dst_coord = vec2<i32>(i32(gid.x), i32(gid.y));
|
|
18405
|
-
let pixel = textureLoad(input_texture_crop, src_coord, 0);
|
|
18406
|
-
textureStore(output_texture, dst_coord, pixel);
|
|
18407
|
-
|
|
18408
|
-
// Output dimensions from workgroup (0,0) only
|
|
18409
|
-
if (gid.x == 0u && gid.y == 0u) {
|
|
18410
|
-
dimensions_out.original_width = size.x;
|
|
18411
|
-
dimensions_out.original_height = size.y;
|
|
18412
|
-
dimensions_out.cropped_width = crop_width;
|
|
18413
|
-
dimensions_out.cropped_height = crop_height;
|
|
18232
|
+
if (value.length === 4) {
|
|
18233
|
+
return value;
|
|
18414
18234
|
}
|
|
18235
|
+
}
|
|
18236
|
+
return value;
|
|
18415
18237
|
}
|
|
18416
18238
|
|
|
18417
|
-
//
|
|
18418
|
-
|
|
18419
|
-
// ==============================
|
|
18420
|
-
|
|
18421
|
-
// Output texture to draw a fallback checkerboard
|
|
18422
|
-
@group(0) @binding(0)
|
|
18423
|
-
var checker_texture: texture_storage_2d<rgba8unorm, write>;
|
|
18239
|
+
// src/scene/TextNode.ts
|
|
18240
|
+
var DEFAULT_FONT_SIZE = 14;
|
|
18424
18241
|
|
|
18425
|
-
|
|
18426
|
-
|
|
18427
|
-
|
|
18428
|
-
|
|
18429
|
-
|
|
18430
|
-
|
|
18242
|
+
class TextNode extends SceneNode {
|
|
18243
|
+
#text;
|
|
18244
|
+
#formatting;
|
|
18245
|
+
#font;
|
|
18246
|
+
constructor(shader, text, opts = {}) {
|
|
18247
|
+
const { width, height } = measureText(shader.font, text, opts.wordWrap);
|
|
18248
|
+
if (text.length > shader.maxCharCount) {
|
|
18249
|
+
throw new Error(`Text: ${text} exceeds ${shader.maxCharCount} characters. Try using fewer characters or increase the limit in Toodle.attach.`);
|
|
18431
18250
|
}
|
|
18432
|
-
|
|
18433
|
-
|
|
18434
|
-
|
|
18435
|
-
|
|
18436
|
-
|
|
18437
|
-
|
|
18438
|
-
|
|
18439
|
-
|
|
18440
|
-
|
|
18441
|
-
|
|
18442
|
-
|
|
18443
|
-
}
|
|
18444
|
-
`;
|
|
18445
|
-
|
|
18446
|
-
// src/backends/webgpu/TextureComputeShader.ts
|
|
18447
|
-
var BOUNDING_BOX_SIZE = 4 * Uint32Array.BYTES_PER_ELEMENT;
|
|
18448
|
-
var WORKGROUP_SIZE = 8;
|
|
18449
|
-
var MAX_BOUND = 4294967295;
|
|
18450
|
-
var MIN_BOUND = 0;
|
|
18451
|
-
|
|
18452
|
-
class TextureComputeShader {
|
|
18453
|
-
#device;
|
|
18454
|
-
#boundingBuffer;
|
|
18455
|
-
#cropPipeline;
|
|
18456
|
-
#boundPipeline;
|
|
18457
|
-
#missingTexturePipeline;
|
|
18458
|
-
constructor(device, cropPipeline, boundPipeline, missingTexturePipeline) {
|
|
18459
|
-
this.#device = device;
|
|
18460
|
-
this.#boundPipeline = boundPipeline;
|
|
18461
|
-
this.#cropPipeline = cropPipeline;
|
|
18462
|
-
this.#missingTexturePipeline = missingTexturePipeline;
|
|
18463
|
-
this.#boundingBuffer = this.#device.createBuffer({
|
|
18464
|
-
size: BOUNDING_BOX_SIZE,
|
|
18465
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST
|
|
18251
|
+
const em2px = shader.font.lineHeight / (opts.fontSize ?? DEFAULT_FONT_SIZE);
|
|
18252
|
+
if (!opts.shrinkToFit && !opts.size) {
|
|
18253
|
+
opts.size = { width: width / em2px, height: height / em2px };
|
|
18254
|
+
}
|
|
18255
|
+
super({
|
|
18256
|
+
...opts,
|
|
18257
|
+
render: {
|
|
18258
|
+
shader,
|
|
18259
|
+
writeInstance: (_node, _array, _offset) => {
|
|
18260
|
+
throw new Error("not implemented - needs access to text uniform buffer, dimensions and a model matrix");
|
|
18261
|
+
}
|
|
18262
|
+
}
|
|
18466
18263
|
});
|
|
18264
|
+
this.#font = shader.font;
|
|
18265
|
+
this.#text = text;
|
|
18266
|
+
this.#formatting = opts;
|
|
18467
18267
|
}
|
|
18468
|
-
|
|
18469
|
-
|
|
18470
|
-
return new TextureComputeShader(device, pipelines.cropPipeline, pipelines.boundPipeline, pipelines.missingTexturePipeline);
|
|
18268
|
+
get text() {
|
|
18269
|
+
return this.#text;
|
|
18471
18270
|
}
|
|
18472
|
-
|
|
18473
|
-
|
|
18474
|
-
|
|
18475
|
-
|
|
18476
|
-
|
|
18477
|
-
|
|
18478
|
-
|
|
18479
|
-
|
|
18480
|
-
|
|
18481
|
-
MIN_BOUND,
|
|
18482
|
-
MIN_BOUND
|
|
18483
|
-
]);
|
|
18484
|
-
this.#device.queue.writeBuffer(this.#boundingBuffer, 0, boundsInit.buffer, 0, BOUNDING_BOX_SIZE);
|
|
18485
|
-
passEncoder.setPipeline(this.#boundPipeline);
|
|
18486
|
-
passEncoder.setBindGroup(0, boundsBindGroup);
|
|
18487
|
-
passEncoder.dispatchWorkgroups(dispatchX, dispatchY);
|
|
18488
|
-
passEncoder.end();
|
|
18489
|
-
this.#device.queue.submit([commandEncoder.finish()]);
|
|
18490
|
-
const { texelX, texelY, texelWidth, texelHeight, computeBuffer } = await this.#getBoundingBox();
|
|
18491
|
-
if (texelX === MAX_BOUND || texelY === MAX_BOUND) {
|
|
18492
|
-
return await this.#createMissingTexture(textureWrapper.texture);
|
|
18271
|
+
get formatting() {
|
|
18272
|
+
return this.#formatting;
|
|
18273
|
+
}
|
|
18274
|
+
get font() {
|
|
18275
|
+
return this.#font;
|
|
18276
|
+
}
|
|
18277
|
+
set text(text) {
|
|
18278
|
+
if (!text) {
|
|
18279
|
+
throw new Error("text cannot be empty");
|
|
18493
18280
|
}
|
|
18494
|
-
|
|
18495
|
-
|
|
18496
|
-
const rightCrop = textureWrapper.originalSize.width - texelX - texelWidth;
|
|
18497
|
-
const topCrop = texelY;
|
|
18498
|
-
const bottomCrop = textureWrapper.originalSize.height - texelY - texelHeight;
|
|
18499
|
-
textureWrapper = {
|
|
18500
|
-
texture: croppedTexture,
|
|
18501
|
-
cropOffset: { x: leftCrop - rightCrop, y: bottomCrop - topCrop },
|
|
18502
|
-
originalSize: textureWrapper.originalSize
|
|
18503
|
-
};
|
|
18504
|
-
return textureWrapper;
|
|
18281
|
+
this.#text = text;
|
|
18282
|
+
this.setDirty();
|
|
18505
18283
|
}
|
|
18506
|
-
|
|
18507
|
-
|
|
18508
|
-
label: "AABB Compute Buffer",
|
|
18509
|
-
size: BOUNDING_BOX_SIZE,
|
|
18510
|
-
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
18511
|
-
});
|
|
18512
|
-
const copyEncoder = this.#device.createCommandEncoder();
|
|
18513
|
-
copyEncoder.copyBufferToBuffer(this.#boundingBuffer, 0, readBuffer, 0, BOUNDING_BOX_SIZE);
|
|
18514
|
-
this.#device.queue.submit([copyEncoder.finish()]);
|
|
18515
|
-
await readBuffer.mapAsync(GPUMapMode.READ);
|
|
18516
|
-
const computeBuffer = new Uint32Array(readBuffer.getMappedRange().slice(0));
|
|
18517
|
-
readBuffer.unmap();
|
|
18518
|
-
const [minX, minY, maxX, maxY] = computeBuffer;
|
|
18519
|
-
return {
|
|
18520
|
-
texelX: minX,
|
|
18521
|
-
texelY: minY,
|
|
18522
|
-
texelWidth: maxX - minX + 1,
|
|
18523
|
-
texelHeight: maxY - minY + 1,
|
|
18524
|
-
computeBuffer
|
|
18525
|
-
};
|
|
18284
|
+
get tint() {
|
|
18285
|
+
return this.#formatting.color || { r: 1, g: 1, b: 1, a: 1 };
|
|
18526
18286
|
}
|
|
18527
|
-
|
|
18528
|
-
|
|
18529
|
-
|
|
18530
|
-
size: BOUNDING_BOX_SIZE,
|
|
18531
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
18532
|
-
});
|
|
18533
|
-
this.#device.queue.writeBuffer(boundsUniform, 0, computeBuffer);
|
|
18534
|
-
const outputTexture = this.#device.createTexture({
|
|
18535
|
-
label: "Cropped Texture",
|
|
18536
|
-
size: [croppedWidth, croppedHeight],
|
|
18537
|
-
format: "rgba8unorm",
|
|
18538
|
-
usage: GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC
|
|
18539
|
-
});
|
|
18540
|
-
const dimensionsOutBuffer = this.#device.createBuffer({
|
|
18541
|
-
label: "Cropping Dimensions Output Buffer",
|
|
18542
|
-
size: BOUNDING_BOX_SIZE,
|
|
18543
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
|
|
18544
|
-
});
|
|
18545
|
-
const bindGroup = this.#croppingBindGroup(inputTexture, outputTexture, boundsUniform, dimensionsOutBuffer);
|
|
18546
|
-
const encoder = this.#device.createCommandEncoder();
|
|
18547
|
-
const pass = encoder.beginComputePass();
|
|
18548
|
-
pass.setPipeline(this.#cropPipeline);
|
|
18549
|
-
pass.setBindGroup(0, bindGroup);
|
|
18550
|
-
pass.dispatchWorkgroups(Math.ceil(croppedWidth / WORKGROUP_SIZE), Math.ceil(croppedHeight / WORKGROUP_SIZE));
|
|
18551
|
-
pass.end();
|
|
18552
|
-
this.#device.queue.submit([encoder.finish()]);
|
|
18553
|
-
return outputTexture;
|
|
18287
|
+
set tint(tint) {
|
|
18288
|
+
this.#formatting.color = tint;
|
|
18289
|
+
this.setDirty();
|
|
18554
18290
|
}
|
|
18555
|
-
|
|
18556
|
-
|
|
18557
|
-
|
|
18558
|
-
size: [inputTexture.width, inputTexture.height],
|
|
18559
|
-
format: "rgba8unorm",
|
|
18560
|
-
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING
|
|
18561
|
-
});
|
|
18562
|
-
const encoder = this.#device.createCommandEncoder();
|
|
18563
|
-
const pass = encoder.beginComputePass();
|
|
18564
|
-
pass.setPipeline(this.#missingTexturePipeline);
|
|
18565
|
-
pass.setBindGroup(0, this.#missingTextureBindGroup(placeholder));
|
|
18566
|
-
pass.dispatchWorkgroups(placeholder.width / 8, placeholder.height / 8);
|
|
18567
|
-
pass.end();
|
|
18568
|
-
this.#device.queue.submit([encoder.finish()]);
|
|
18569
|
-
return {
|
|
18570
|
-
texture: placeholder,
|
|
18571
|
-
cropOffset: { x: 0, y: 0 },
|
|
18572
|
-
originalSize: { width: inputTexture.width, height: inputTexture.height }
|
|
18573
|
-
};
|
|
18291
|
+
set formatting(formatting) {
|
|
18292
|
+
this.#formatting = formatting;
|
|
18293
|
+
this.setDirty();
|
|
18574
18294
|
}
|
|
18575
|
-
|
|
18576
|
-
|
|
18577
|
-
|
|
18295
|
+
}
|
|
18296
|
+
|
|
18297
|
+
// src/backends/webgpu/WebGPUTextShader.ts
|
|
18298
|
+
var deets = new _t2(text_wgsl_default);
|
|
18299
|
+
var struct = deets.structs.find((s3) => s3.name === "TextBlockDescriptor");
|
|
18300
|
+
if (!struct) {
|
|
18301
|
+
throw new Error("FormattedText struct not found");
|
|
18302
|
+
}
|
|
18303
|
+
var textDescriptorInstanceSize = struct.size;
|
|
18304
|
+
|
|
18305
|
+
class WebGPUTextShader {
|
|
18306
|
+
label = "text";
|
|
18307
|
+
#backend;
|
|
18308
|
+
#pipeline;
|
|
18309
|
+
#bindGroups = [];
|
|
18310
|
+
#font;
|
|
18311
|
+
#maxCharCount;
|
|
18312
|
+
#engineUniformsBuffer;
|
|
18313
|
+
#descriptorBuffer;
|
|
18314
|
+
#textBlockBuffer;
|
|
18315
|
+
#cpuDescriptorBuffer;
|
|
18316
|
+
#cpuTextBlockBuffer;
|
|
18317
|
+
#instanceIndex = 0;
|
|
18318
|
+
#textBlockOffset = 0;
|
|
18319
|
+
constructor(backend, pipeline, font, _colorFormat, instanceCount) {
|
|
18320
|
+
this.#backend = backend;
|
|
18321
|
+
const device = backend.device;
|
|
18322
|
+
this.#font = font;
|
|
18323
|
+
this.#pipeline = pipeline.pipeline;
|
|
18324
|
+
this.#maxCharCount = pipeline.maxCharCount;
|
|
18325
|
+
this.#descriptorBuffer = device.createBuffer({
|
|
18326
|
+
label: "msdf text descriptor buffer",
|
|
18327
|
+
size: textDescriptorInstanceSize * instanceCount,
|
|
18328
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
18329
|
+
});
|
|
18330
|
+
this.#cpuDescriptorBuffer = new Float32Array(instanceCount * textDescriptorInstanceSize / Float32Array.BYTES_PER_ELEMENT);
|
|
18331
|
+
this.#cpuTextBlockBuffer = new Float32Array(instanceCount * this.maxCharCount * 4);
|
|
18332
|
+
this.#engineUniformsBuffer = device.createBuffer({
|
|
18333
|
+
label: "msdf view projection matrix",
|
|
18334
|
+
size: Float32Array.BYTES_PER_ELEMENT * 12,
|
|
18335
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
18336
|
+
});
|
|
18337
|
+
this.#textBlockBuffer = device.createBuffer({
|
|
18338
|
+
label: "msdf text buffer",
|
|
18339
|
+
size: instanceCount * this.maxCharCount * 4 * Float32Array.BYTES_PER_ELEMENT,
|
|
18340
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
18341
|
+
});
|
|
18342
|
+
this.#bindGroups.push(pipeline.fontBindGroup);
|
|
18343
|
+
this.#bindGroups.push(device.createBindGroup({
|
|
18344
|
+
label: "msdf text bind group",
|
|
18345
|
+
layout: pipeline.pipeline.getBindGroupLayout(1),
|
|
18578
18346
|
entries: [
|
|
18579
|
-
{
|
|
18580
|
-
|
|
18347
|
+
{
|
|
18348
|
+
binding: 0,
|
|
18349
|
+
resource: { buffer: this.#descriptorBuffer }
|
|
18350
|
+
},
|
|
18351
|
+
{
|
|
18352
|
+
binding: 1,
|
|
18353
|
+
resource: { buffer: this.#textBlockBuffer }
|
|
18354
|
+
}
|
|
18581
18355
|
]
|
|
18582
|
-
});
|
|
18583
|
-
|
|
18584
|
-
|
|
18585
|
-
|
|
18586
|
-
layout: this.#cropPipeline.getBindGroupLayout(0),
|
|
18356
|
+
}));
|
|
18357
|
+
const engineUniformsBindGroup = device.createBindGroup({
|
|
18358
|
+
label: "msdf text uniforms bind group",
|
|
18359
|
+
layout: pipeline.pipeline.getBindGroupLayout(2),
|
|
18587
18360
|
entries: [
|
|
18588
|
-
{
|
|
18589
|
-
|
|
18590
|
-
|
|
18591
|
-
|
|
18361
|
+
{
|
|
18362
|
+
binding: 0,
|
|
18363
|
+
resource: { buffer: this.#engineUniformsBuffer }
|
|
18364
|
+
}
|
|
18592
18365
|
]
|
|
18593
18366
|
});
|
|
18367
|
+
this.#bindGroups.push(engineUniformsBindGroup);
|
|
18594
18368
|
}
|
|
18595
|
-
|
|
18596
|
-
|
|
18597
|
-
|
|
18598
|
-
|
|
18599
|
-
|
|
18369
|
+
startFrame(uniform) {
|
|
18370
|
+
const device = this.#backend.device;
|
|
18371
|
+
device.queue.writeBuffer(this.#engineUniformsBuffer, 0, uniform.viewProjectionMatrix);
|
|
18372
|
+
this.#instanceIndex = 0;
|
|
18373
|
+
this.#textBlockOffset = 0;
|
|
18600
18374
|
}
|
|
18601
|
-
|
|
18602
|
-
|
|
18603
|
-
|
|
18604
|
-
|
|
18605
|
-
|
|
18606
|
-
|
|
18607
|
-
|
|
18608
|
-
|
|
18609
|
-
|
|
18610
|
-
{
|
|
18611
|
-
|
|
18612
|
-
|
|
18613
|
-
texture: { sampleType: "float", viewDimension: "2d" }
|
|
18614
|
-
},
|
|
18615
|
-
{
|
|
18616
|
-
binding: 1,
|
|
18617
|
-
visibility: GPUShaderStage.COMPUTE,
|
|
18618
|
-
buffer: { type: "storage" }
|
|
18619
|
-
}
|
|
18620
|
-
]
|
|
18621
|
-
});
|
|
18622
|
-
const cropBindGroupLayout = device.createBindGroupLayout({
|
|
18623
|
-
label: "Cropping Layout",
|
|
18624
|
-
entries: [
|
|
18625
|
-
{ binding: 0, visibility: GPUShaderStage.COMPUTE, texture: {} },
|
|
18626
|
-
{
|
|
18627
|
-
binding: 1,
|
|
18628
|
-
visibility: GPUShaderStage.COMPUTE,
|
|
18629
|
-
storageTexture: {
|
|
18630
|
-
access: "write-only",
|
|
18631
|
-
format: "rgba8unorm",
|
|
18632
|
-
viewDimension: "2d"
|
|
18633
|
-
}
|
|
18634
|
-
},
|
|
18635
|
-
{
|
|
18636
|
-
binding: 2,
|
|
18637
|
-
visibility: GPUShaderStage.COMPUTE,
|
|
18638
|
-
buffer: { type: "uniform" }
|
|
18639
|
-
},
|
|
18640
|
-
{
|
|
18641
|
-
binding: 3,
|
|
18642
|
-
visibility: GPUShaderStage.COMPUTE,
|
|
18643
|
-
buffer: { type: "storage" }
|
|
18644
|
-
}
|
|
18645
|
-
]
|
|
18646
|
-
});
|
|
18647
|
-
const missingTextureBindGroupLayout = device.createBindGroupLayout({
|
|
18648
|
-
label: "Missing Texture Layout",
|
|
18649
|
-
entries: [
|
|
18650
|
-
{
|
|
18651
|
-
binding: 0,
|
|
18652
|
-
visibility: GPUShaderStage.COMPUTE,
|
|
18653
|
-
storageTexture: {
|
|
18654
|
-
access: "write-only",
|
|
18655
|
-
format: "rgba8unorm",
|
|
18656
|
-
viewDimension: "2d"
|
|
18657
|
-
}
|
|
18375
|
+
processBatch(nodes) {
|
|
18376
|
+
if (nodes.length === 0)
|
|
18377
|
+
return 0;
|
|
18378
|
+
const renderPass = this.#backend.renderPass;
|
|
18379
|
+
renderPass.setPipeline(this.#pipeline);
|
|
18380
|
+
for (let i3 = 0;i3 < this.#bindGroups.length; i3++) {
|
|
18381
|
+
renderPass.setBindGroup(i3, this.#bindGroups[i3]);
|
|
18382
|
+
}
|
|
18383
|
+
for (const node of nodes) {
|
|
18384
|
+
if (!(node instanceof TextNode)) {
|
|
18385
|
+
console.error(node);
|
|
18386
|
+
throw new Error(`Tried to use WebGPUTextShader on something that isn't a TextNode: ${node}`);
|
|
18658
18387
|
}
|
|
18659
|
-
|
|
18660
|
-
|
|
18661
|
-
|
|
18662
|
-
|
|
18663
|
-
|
|
18664
|
-
|
|
18665
|
-
|
|
18666
|
-
|
|
18667
|
-
|
|
18668
|
-
|
|
18669
|
-
|
|
18670
|
-
|
|
18671
|
-
|
|
18672
|
-
|
|
18673
|
-
|
|
18674
|
-
|
|
18675
|
-
|
|
18676
|
-
|
|
18677
|
-
|
|
18678
|
-
|
|
18679
|
-
|
|
18680
|
-
|
|
18681
|
-
|
|
18682
|
-
|
|
18683
|
-
|
|
18684
|
-
|
|
18685
|
-
|
|
18686
|
-
|
|
18687
|
-
|
|
18688
|
-
|
|
18689
|
-
|
|
18690
|
-
// Quad vertex positions for a character
|
|
18691
|
-
const pos = array(
|
|
18692
|
-
vec2f(0, -1),
|
|
18693
|
-
vec2f(1, -1),
|
|
18694
|
-
vec2f(0, 0),
|
|
18695
|
-
vec2f(1, 0),
|
|
18696
|
-
);
|
|
18697
|
-
|
|
18698
|
-
// Debug colors for visualization
|
|
18699
|
-
const debugColors = array(
|
|
18700
|
-
vec4f(1, 0, 0, 1),
|
|
18701
|
-
vec4f(0, 1, 0, 1),
|
|
18702
|
-
vec4f(0, 0, 1, 1),
|
|
18703
|
-
vec4f(1, 1, 1, 1),
|
|
18704
|
-
);
|
|
18705
|
-
|
|
18706
|
-
// Vertex input from GPU
|
|
18707
|
-
struct VertexInput {
|
|
18708
|
-
@builtin(vertex_index) vertex: u32,
|
|
18709
|
-
@builtin(instance_index) instance: u32,
|
|
18710
|
-
};
|
|
18711
|
-
|
|
18712
|
-
// Output from vertex shader to fragment shader
|
|
18713
|
-
struct VertexOutput {
|
|
18714
|
-
@builtin(position) position: vec4f,
|
|
18715
|
-
@location(0) texcoord: vec2f,
|
|
18716
|
-
@location(1) debugColor: vec4f,
|
|
18717
|
-
@location(2) @interpolate(flat) instanceIndex: u32,
|
|
18718
|
-
};
|
|
18719
|
-
|
|
18720
|
-
// Metadata for a single character glyph
|
|
18721
|
-
struct Char {
|
|
18722
|
-
texOffset: vec2f, // Offset to top-left in MSDF texture (pixels)
|
|
18723
|
-
texExtent: vec2f, // Size in texture (pixels)
|
|
18724
|
-
size: vec2f, // Glyph size in ems
|
|
18725
|
-
offset: vec2f, // Position offset in ems
|
|
18726
|
-
};
|
|
18727
|
-
|
|
18728
|
-
// Metadata for a text block
|
|
18729
|
-
struct TextBlockDescriptor {
|
|
18730
|
-
transform: mat3x3f, // Text transform matrix (model matrix)
|
|
18731
|
-
color: vec4f, // Text color
|
|
18732
|
-
fontSize: f32, // Font size
|
|
18733
|
-
blockWidth: f32, // Total width of text block
|
|
18734
|
-
blockHeight: f32, // Total height of text block
|
|
18735
|
-
bufferPosition: f32 // Index and length in textBuffer
|
|
18736
|
-
};
|
|
18737
|
-
|
|
18738
|
-
// Font bindings
|
|
18739
|
-
@group(0) @binding(0) var fontTexture: texture_2d<f32>;
|
|
18740
|
-
@group(0) @binding(1) var fontSampler: sampler;
|
|
18741
|
-
@group(0) @binding(2) var<storage> chars: array<Char>;
|
|
18742
|
-
@group(0) @binding(3) var<uniform> fontData: vec4f; // Contains line height (x)
|
|
18743
|
-
|
|
18744
|
-
// Text bindings
|
|
18745
|
-
@group(1) @binding(0) var<storage> texts: array<TextBlockDescriptor>;
|
|
18746
|
-
@group(1) @binding(1) var<storage> textBuffer: array<vec4f>; // Each vec4: xy = glyph pos, z = char index
|
|
18747
|
-
|
|
18748
|
-
// Global uniforms
|
|
18749
|
-
@group(2) @binding(0) var<uniform> viewProjectionMatrix: mat3x3f;
|
|
18750
|
-
|
|
18751
|
-
// Vertex shader
|
|
18752
|
-
@vertex
|
|
18753
|
-
fn vertexMain(input: VertexInput) -> VertexOutput {
|
|
18754
|
-
// Because the instance index is used for character indexing, we are
|
|
18755
|
-
// overloading the vertex index to store the instance of the text metadata.
|
|
18756
|
-
//
|
|
18757
|
-
// I.e...
|
|
18758
|
-
// Vertex 0-4 = Instance 0, Vertex 0-4
|
|
18759
|
-
// Vertex 4-8 = Instance 1, Vertex 0-4
|
|
18760
|
-
// Vertex 8-12 = Instance 2, Vertex 0-4
|
|
18761
|
-
let vertexIndex = input.vertex % 4;
|
|
18762
|
-
let textIndex = input.vertex / 4;
|
|
18763
|
-
|
|
18764
|
-
let text = texts[textIndex];
|
|
18765
|
-
let textElement = textBuffer[u32(text.bufferPosition) + input.instance];
|
|
18766
|
-
let char = chars[u32(textElement.z)];
|
|
18767
|
-
|
|
18768
|
-
let lineHeight = fontData.x;
|
|
18769
|
-
let textWidth = text.blockWidth;
|
|
18770
|
-
let textHeight = text.blockHeight;
|
|
18771
|
-
|
|
18772
|
-
// Center text vertically; origin is mid-height
|
|
18773
|
-
let offset = vec2f(0, -textHeight / 2);
|
|
18774
|
-
|
|
18775
|
-
// Glyph position in ems (quad pos * size + per-char offset)
|
|
18776
|
-
let emPos = pos[vertexIndex] * char.size + char.offset + textElement.xy - offset;
|
|
18777
|
-
let charPos = emPos * (text.fontSize / lineHeight);
|
|
18778
|
-
|
|
18779
|
-
var output: VertexOutput;
|
|
18780
|
-
let transformedPosition = viewProjectionMatrix * text.transform * vec3f(charPos, 1);
|
|
18781
|
-
|
|
18782
|
-
output.position = vec4f(transformedPosition, 1);
|
|
18783
|
-
output.texcoord = pos[vertexIndex] * vec2f(1, -1);
|
|
18784
|
-
output.texcoord *= char.texExtent;
|
|
18785
|
-
output.texcoord += char.texOffset;
|
|
18786
|
-
output.debugColor = debugColors[vertexIndex];
|
|
18787
|
-
output.instanceIndex = textIndex;
|
|
18788
|
-
return output;
|
|
18789
|
-
|
|
18790
|
-
// To debug - hardcode quad in bottom right quarter of the screen:
|
|
18791
|
-
// output.position = vec4f(pos[input.vertex], 0, 1);
|
|
18388
|
+
const text = node.text;
|
|
18389
|
+
const formatting = node.formatting;
|
|
18390
|
+
const measurements = measureText(this.#font, text, formatting.wordWrap);
|
|
18391
|
+
const textBlockSize = 4 * text.length;
|
|
18392
|
+
const textDescriptorOffset = this.#instanceIndex * textDescriptorInstanceSize / Float32Array.BYTES_PER_ELEMENT;
|
|
18393
|
+
this.#cpuDescriptorBuffer.set(node.matrix, textDescriptorOffset);
|
|
18394
|
+
this.#cpuDescriptorBuffer.set([node.tint.r, node.tint.g, node.tint.b, node.tint.a], textDescriptorOffset + 12);
|
|
18395
|
+
const size = node.size ?? measurements;
|
|
18396
|
+
const fontSize = formatting.shrinkToFit ? findLargestFontSize(this.#font, text, size, formatting) : formatting.fontSize;
|
|
18397
|
+
const actualFontSize = fontSize || DEFAULT_FONT_SIZE;
|
|
18398
|
+
this.#cpuDescriptorBuffer[textDescriptorOffset + 16] = actualFontSize;
|
|
18399
|
+
this.#cpuDescriptorBuffer[textDescriptorOffset + 17] = formatting.align === "center" ? 0 : measurements.width;
|
|
18400
|
+
this.#cpuDescriptorBuffer[textDescriptorOffset + 18] = measurements.height;
|
|
18401
|
+
this.#cpuDescriptorBuffer[textDescriptorOffset + 19] = this.#textBlockOffset / 4;
|
|
18402
|
+
shapeText(this.#font, text, size, actualFontSize, formatting, this.#cpuTextBlockBuffer, this.#textBlockOffset);
|
|
18403
|
+
this.#backend.device.queue.writeBuffer(this.#descriptorBuffer, textDescriptorOffset * Float32Array.BYTES_PER_ELEMENT, this.#cpuDescriptorBuffer, textDescriptorOffset, textDescriptorInstanceSize / Float32Array.BYTES_PER_ELEMENT);
|
|
18404
|
+
this.#backend.device.queue.writeBuffer(this.#textBlockBuffer, this.#textBlockOffset * Float32Array.BYTES_PER_ELEMENT, this.#cpuTextBlockBuffer, this.#textBlockOffset, textBlockSize);
|
|
18405
|
+
this.#textBlockOffset += textBlockSize;
|
|
18406
|
+
renderPass.draw(4, measurements.printedCharCount, 4 * this.#instanceIndex, 0);
|
|
18407
|
+
this.#instanceIndex++;
|
|
18408
|
+
}
|
|
18409
|
+
return nodes.length;
|
|
18410
|
+
}
|
|
18411
|
+
endFrame() {}
|
|
18412
|
+
get font() {
|
|
18413
|
+
return this.#font;
|
|
18414
|
+
}
|
|
18415
|
+
get maxCharCount() {
|
|
18416
|
+
return this.#maxCharCount;
|
|
18417
|
+
}
|
|
18792
18418
|
}
|
|
18793
18419
|
|
|
18794
|
-
//
|
|
18795
|
-
|
|
18796
|
-
|
|
18797
|
-
|
|
18420
|
+
// src/scene/Batcher.ts
|
|
18421
|
+
class Batcher {
|
|
18422
|
+
nodes = [];
|
|
18423
|
+
layers = [];
|
|
18424
|
+
pipelines = [];
|
|
18425
|
+
enqueue(node) {
|
|
18426
|
+
if (node.renderComponent && node.isActive) {
|
|
18427
|
+
this.nodes.push(node);
|
|
18428
|
+
const z3 = node.layer;
|
|
18429
|
+
const layer = this.#findOrCreateLayer(z3);
|
|
18430
|
+
const pipeline = this.#findOrCreatePipeline(layer, node.renderComponent.shader);
|
|
18431
|
+
pipeline.nodes.push(node);
|
|
18432
|
+
}
|
|
18433
|
+
for (const kid of node.kids) {
|
|
18434
|
+
this.enqueue(kid);
|
|
18435
|
+
}
|
|
18436
|
+
}
|
|
18437
|
+
flush() {
|
|
18438
|
+
this.nodes = [];
|
|
18439
|
+
this.layers = [];
|
|
18440
|
+
this.pipelines = [];
|
|
18441
|
+
}
|
|
18442
|
+
#findOrCreateLayer(z3) {
|
|
18443
|
+
let layer = this.layers.find((l3) => l3.z === z3);
|
|
18444
|
+
if (!layer) {
|
|
18445
|
+
layer = { z: z3, pipelines: [] };
|
|
18446
|
+
this.layers.push(layer);
|
|
18447
|
+
this.layers.sort((a3, b3) => a3.z - b3.z);
|
|
18448
|
+
}
|
|
18449
|
+
return layer;
|
|
18450
|
+
}
|
|
18451
|
+
#findOrCreatePipeline(layer, shader) {
|
|
18452
|
+
let pipeline = layer.pipelines.find((p3) => p3.shader === shader);
|
|
18453
|
+
if (!pipeline) {
|
|
18454
|
+
pipeline = { shader, nodes: [] };
|
|
18455
|
+
layer.pipelines.push(pipeline);
|
|
18456
|
+
this.pipelines.push(pipeline);
|
|
18457
|
+
}
|
|
18458
|
+
return pipeline;
|
|
18459
|
+
}
|
|
18798
18460
|
}
|
|
18799
18461
|
|
|
18800
|
-
//
|
|
18801
|
-
|
|
18802
|
-
|
|
18803
|
-
|
|
18804
|
-
|
|
18805
|
-
|
|
18806
|
-
|
|
18807
|
-
|
|
18808
|
-
|
|
18809
|
-
|
|
18810
|
-
|
|
18811
|
-
|
|
18812
|
-
|
|
18813
|
-
|
|
18814
|
-
|
|
18815
|
-
|
|
18816
|
-
|
|
18817
|
-
|
|
18818
|
-
|
|
18819
|
-
|
|
18820
|
-
|
|
18821
|
-
|
|
18822
|
-
|
|
18823
|
-
|
|
18462
|
+
// src/scene/Camera.ts
|
|
18463
|
+
class Camera {
|
|
18464
|
+
#position = { x: 0, y: 0 };
|
|
18465
|
+
#zoom = 1;
|
|
18466
|
+
#rotation = 0;
|
|
18467
|
+
#isDirty = true;
|
|
18468
|
+
#matrix = mat3.create();
|
|
18469
|
+
get zoom() {
|
|
18470
|
+
return this.#zoom;
|
|
18471
|
+
}
|
|
18472
|
+
set zoom(value) {
|
|
18473
|
+
this.#zoom = value;
|
|
18474
|
+
this.setDirty();
|
|
18475
|
+
}
|
|
18476
|
+
get rotation() {
|
|
18477
|
+
return rad2deg(this.#rotation);
|
|
18478
|
+
}
|
|
18479
|
+
set rotation(value) {
|
|
18480
|
+
this.#rotation = deg2rad(value);
|
|
18481
|
+
this.setDirty();
|
|
18482
|
+
}
|
|
18483
|
+
get rotationRadians() {
|
|
18484
|
+
return this.#rotation;
|
|
18485
|
+
}
|
|
18486
|
+
set rotationRadians(value) {
|
|
18487
|
+
this.#rotation = value;
|
|
18488
|
+
this.setDirty();
|
|
18489
|
+
}
|
|
18490
|
+
get x() {
|
|
18491
|
+
return this.#position.x;
|
|
18492
|
+
}
|
|
18493
|
+
get y() {
|
|
18494
|
+
return this.#position.y;
|
|
18495
|
+
}
|
|
18496
|
+
set x(value) {
|
|
18497
|
+
this.#position.x = value;
|
|
18498
|
+
this.setDirty();
|
|
18499
|
+
}
|
|
18500
|
+
set y(value) {
|
|
18501
|
+
this.#position.y = value;
|
|
18502
|
+
this.setDirty();
|
|
18503
|
+
}
|
|
18504
|
+
get matrix() {
|
|
18505
|
+
if (this.#isDirty) {
|
|
18506
|
+
this.#isDirty = false;
|
|
18507
|
+
this.#matrix = createViewMatrix(this, this.#matrix);
|
|
18508
|
+
}
|
|
18509
|
+
return this.#matrix;
|
|
18510
|
+
}
|
|
18511
|
+
setDirty() {
|
|
18512
|
+
this.#isDirty = true;
|
|
18824
18513
|
}
|
|
18825
|
-
|
|
18826
|
-
let msdfColor = vec4f(text.color.rgb, text.color.a * alpha);
|
|
18827
|
-
return msdfColor;
|
|
18828
|
-
|
|
18829
|
-
// Debug options:
|
|
18830
|
-
// return text.color;
|
|
18831
|
-
// return input.debugColor;
|
|
18832
|
-
// return vec4f(1, 0, 1, 1); // hardcoded magenta
|
|
18833
|
-
// return textureSample(fontTexture, fontSampler, input.texcoord);
|
|
18834
18514
|
}
|
|
18835
|
-
`;
|
|
18836
18515
|
|
|
18837
|
-
// src/
|
|
18838
|
-
|
|
18839
|
-
|
|
18840
|
-
|
|
18841
|
-
|
|
18842
|
-
|
|
18843
|
-
|
|
18844
|
-
|
|
18845
|
-
|
|
18846
|
-
|
|
18847
|
-
|
|
18516
|
+
// src/scene/QuadNode.ts
|
|
18517
|
+
var PRIMITIVE_TEXTURE = "__primitive__";
|
|
18518
|
+
var RESERVED_PRIMITIVE_INDEX_START = 1000;
|
|
18519
|
+
var CIRCLE_INDEX = 1001;
|
|
18520
|
+
var DEFAULT_REGION = {
|
|
18521
|
+
x: 0,
|
|
18522
|
+
y: 0,
|
|
18523
|
+
width: 0,
|
|
18524
|
+
height: 0
|
|
18525
|
+
};
|
|
18526
|
+
|
|
18527
|
+
class QuadNode extends SceneNode {
|
|
18528
|
+
assetManager;
|
|
18529
|
+
#color;
|
|
18530
|
+
#atlasCoords;
|
|
18531
|
+
#region;
|
|
18532
|
+
#matrixPool;
|
|
18533
|
+
#flip;
|
|
18534
|
+
#cropOffset;
|
|
18535
|
+
#cropRatio;
|
|
18536
|
+
#atlasSize;
|
|
18537
|
+
#textureId;
|
|
18538
|
+
#writeInstance;
|
|
18539
|
+
constructor(options, matrixPool) {
|
|
18540
|
+
assert(options.shader, "QuadNode requires a shader to be explicitly provided");
|
|
18541
|
+
assert(options.size, "QuadNode requires a size to be explicitly provided");
|
|
18542
|
+
assert(options.atlasCoords, "QuadNode requires atlas coords to be explicitly provided");
|
|
18543
|
+
options.render ??= {
|
|
18544
|
+
shader: options.shader,
|
|
18545
|
+
writeInstance: writeQuadInstance
|
|
18546
|
+
};
|
|
18547
|
+
super(options);
|
|
18548
|
+
assert(options.assetManager, "QuadNode requires an asset manager");
|
|
18549
|
+
this.assetManager = options.assetManager;
|
|
18550
|
+
if (options.atlasCoords && options.atlasCoords.atlasIndex >= RESERVED_PRIMITIVE_INDEX_START) {
|
|
18551
|
+
this.#textureId = PRIMITIVE_TEXTURE;
|
|
18552
|
+
this.#region = DEFAULT_REGION;
|
|
18553
|
+
this.#atlasSize = DEFAULT_REGION;
|
|
18554
|
+
} else {
|
|
18555
|
+
assert(options.textureId, "QuadNode requires texture id to be explicitly provided");
|
|
18556
|
+
this.#textureId = options.textureId;
|
|
18557
|
+
assert(options.region, "QuadNode requires a region to be explicitly provided");
|
|
18558
|
+
this.#region = options.region;
|
|
18559
|
+
assert(options.atlasSize, "QuadNode requires atlas size to be explicitly provided");
|
|
18560
|
+
this.#atlasSize = options.atlasSize;
|
|
18561
|
+
}
|
|
18562
|
+
this.#atlasCoords = options.atlasCoords;
|
|
18563
|
+
this.#color = options.color ?? { r: 1, g: 1, b: 1, a: 1 };
|
|
18564
|
+
this.#matrixPool = matrixPool;
|
|
18565
|
+
this.#flip = { x: options.flipX ? -1 : 1, y: options.flipY ? -1 : 1 };
|
|
18566
|
+
this.#cropOffset = options.cropOffset ?? { x: 0, y: 0 };
|
|
18567
|
+
this.#cropRatio = !this.#atlasCoords.uvScaleCropped ? { width: 1, height: 1 } : {
|
|
18568
|
+
width: this.#atlasCoords.uvScaleCropped.width / this.#atlasCoords.uvScale.width,
|
|
18569
|
+
height: this.#atlasCoords.uvScaleCropped.height / this.#atlasCoords.uvScale.height
|
|
18570
|
+
};
|
|
18571
|
+
this.#writeInstance = options.writeInstance;
|
|
18572
|
+
}
|
|
18573
|
+
get color() {
|
|
18574
|
+
return this.#color;
|
|
18575
|
+
}
|
|
18576
|
+
set color(value) {
|
|
18577
|
+
this.#color = value;
|
|
18578
|
+
}
|
|
18579
|
+
get size() {
|
|
18580
|
+
const size = super.size;
|
|
18581
|
+
if (!size) {
|
|
18582
|
+
throw new Error("QuadNode requires a size");
|
|
18583
|
+
}
|
|
18584
|
+
return size;
|
|
18585
|
+
}
|
|
18586
|
+
set size(val) {
|
|
18587
|
+
super.size = val;
|
|
18588
|
+
}
|
|
18589
|
+
get matrixWithSize() {
|
|
18590
|
+
const matrix = mat3.clone(this.matrix, this.#matrixPool.get());
|
|
18591
|
+
mat3.scale(matrix, [this.size.width * this.#flip.x, this.size.height * this.#flip.y], matrix);
|
|
18592
|
+
return matrix;
|
|
18593
|
+
}
|
|
18594
|
+
get atlasCoords() {
|
|
18595
|
+
return this.#atlasCoords;
|
|
18596
|
+
}
|
|
18597
|
+
get region() {
|
|
18598
|
+
return this.#region;
|
|
18599
|
+
}
|
|
18600
|
+
get writeInstance() {
|
|
18601
|
+
return this.#writeInstance;
|
|
18602
|
+
}
|
|
18603
|
+
get flipX() {
|
|
18604
|
+
return this.#flip.x === -1;
|
|
18605
|
+
}
|
|
18606
|
+
set flipX(value) {
|
|
18607
|
+
this.#flip.x = value ? -1 : 1;
|
|
18608
|
+
this.setDirty();
|
|
18609
|
+
}
|
|
18610
|
+
get flipY() {
|
|
18611
|
+
return this.#flip.y === -1;
|
|
18848
18612
|
}
|
|
18849
|
-
|
|
18850
|
-
|
|
18851
|
-
|
|
18852
|
-
label: `MSDF font ${font.name}`,
|
|
18853
|
-
size: [font.imageBitmap.width, font.imageBitmap.height, 1],
|
|
18854
|
-
format: "rgba8unorm",
|
|
18855
|
-
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
|
|
18856
|
-
});
|
|
18857
|
-
device.queue.copyExternalImageToTexture({ source: font.imageBitmap }, { texture }, [font.imageBitmap.width, font.imageBitmap.height]);
|
|
18858
|
-
const charsGpuBuffer = device.createBuffer({
|
|
18859
|
-
label: `MSDF font ${font.name} character layout buffer`,
|
|
18860
|
-
size: font.charCount * Float32Array.BYTES_PER_ELEMENT * 8,
|
|
18861
|
-
usage: GPUBufferUsage.STORAGE,
|
|
18862
|
-
mappedAtCreation: true
|
|
18863
|
-
});
|
|
18864
|
-
const charsArray = new Float32Array(charsGpuBuffer.getMappedRange());
|
|
18865
|
-
charsArray.set(font.charBuffer, 0);
|
|
18866
|
-
charsGpuBuffer.unmap();
|
|
18867
|
-
const fontDataBuffer = device.createBuffer({
|
|
18868
|
-
label: `MSDF font ${font.name} metadata buffer`,
|
|
18869
|
-
size: Float32Array.BYTES_PER_ELEMENT * 4,
|
|
18870
|
-
usage: GPUBufferUsage.UNIFORM,
|
|
18871
|
-
mappedAtCreation: true
|
|
18872
|
-
});
|
|
18873
|
-
const fontDataArray = new Float32Array(fontDataBuffer.getMappedRange());
|
|
18874
|
-
fontDataArray[0] = font.lineHeight;
|
|
18875
|
-
fontDataBuffer.unmap();
|
|
18876
|
-
const fontBindGroup = device.createBindGroup({
|
|
18877
|
-
layout: pipeline.getBindGroupLayout(0),
|
|
18878
|
-
entries: [
|
|
18879
|
-
{
|
|
18880
|
-
binding: 0,
|
|
18881
|
-
resource: texture.createView()
|
|
18882
|
-
},
|
|
18883
|
-
{
|
|
18884
|
-
binding: 1,
|
|
18885
|
-
resource: device.createSampler(sampler)
|
|
18886
|
-
},
|
|
18887
|
-
{
|
|
18888
|
-
binding: 2,
|
|
18889
|
-
resource: {
|
|
18890
|
-
buffer: charsGpuBuffer
|
|
18891
|
-
}
|
|
18892
|
-
},
|
|
18893
|
-
{
|
|
18894
|
-
binding: 3,
|
|
18895
|
-
resource: {
|
|
18896
|
-
buffer: fontDataBuffer
|
|
18897
|
-
}
|
|
18898
|
-
}
|
|
18899
|
-
]
|
|
18900
|
-
});
|
|
18901
|
-
return new FontPipeline(pipeline, font, fontBindGroup, maxCharCount);
|
|
18613
|
+
set flipY(value) {
|
|
18614
|
+
this.#flip.y = value ? -1 : 1;
|
|
18615
|
+
this.setDirty();
|
|
18902
18616
|
}
|
|
18903
|
-
|
|
18904
|
-
|
|
18905
|
-
|
|
18906
|
-
|
|
18907
|
-
|
|
18908
|
-
}
|
|
18909
|
-
|
|
18910
|
-
|
|
18911
|
-
|
|
18912
|
-
|
|
18913
|
-
|
|
18914
|
-
|
|
18915
|
-
|
|
18916
|
-
|
|
18917
|
-
|
|
18918
|
-
|
|
18919
|
-
|
|
18920
|
-
|
|
18617
|
+
get cropOffset() {
|
|
18618
|
+
return this.#cropOffset;
|
|
18619
|
+
}
|
|
18620
|
+
set cropOffset(value) {
|
|
18621
|
+
this.#cropOffset = value;
|
|
18622
|
+
}
|
|
18623
|
+
get textureId() {
|
|
18624
|
+
return this.#textureId;
|
|
18625
|
+
}
|
|
18626
|
+
get isPrimitive() {
|
|
18627
|
+
return this.#textureId === PRIMITIVE_TEXTURE;
|
|
18628
|
+
}
|
|
18629
|
+
get isCircle() {
|
|
18630
|
+
return this.#atlasCoords.atlasIndex === CIRCLE_INDEX;
|
|
18631
|
+
}
|
|
18632
|
+
extra = {
|
|
18633
|
+
setAtlasCoords: (value) => {
|
|
18634
|
+
this.#atlasCoords = value;
|
|
18921
18635
|
},
|
|
18922
|
-
|
|
18923
|
-
|
|
18924
|
-
entryPoint: "fragmentMain",
|
|
18925
|
-
targets: [
|
|
18926
|
-
{
|
|
18927
|
-
format: colorFormat,
|
|
18928
|
-
blend: {
|
|
18929
|
-
color: {
|
|
18930
|
-
srcFactor: "src-alpha",
|
|
18931
|
-
dstFactor: "one-minus-src-alpha"
|
|
18932
|
-
},
|
|
18933
|
-
alpha: {
|
|
18934
|
-
srcFactor: "one",
|
|
18935
|
-
dstFactor: "one"
|
|
18936
|
-
}
|
|
18937
|
-
}
|
|
18938
|
-
}
|
|
18939
|
-
]
|
|
18636
|
+
cropRatio: () => {
|
|
18637
|
+
return this.#cropRatio;
|
|
18940
18638
|
},
|
|
18941
|
-
|
|
18942
|
-
|
|
18943
|
-
stripIndexFormat: "uint32"
|
|
18639
|
+
atlasSize: () => {
|
|
18640
|
+
return this.#atlasSize;
|
|
18944
18641
|
}
|
|
18945
|
-
});
|
|
18946
|
-
}
|
|
18947
|
-
if (typeof GPUShaderStage === "undefined") {
|
|
18948
|
-
globalThis.GPUShaderStage = {
|
|
18949
|
-
VERTEX: 1,
|
|
18950
|
-
FRAGMENT: 2,
|
|
18951
|
-
COMPUTE: 4
|
|
18952
18642
|
};
|
|
18953
18643
|
}
|
|
18954
|
-
|
|
18955
|
-
|
|
18956
|
-
|
|
18957
|
-
|
|
18958
|
-
|
|
18959
|
-
|
|
18960
|
-
|
|
18961
|
-
|
|
18962
|
-
|
|
18963
|
-
|
|
18964
|
-
|
|
18965
|
-
|
|
18966
|
-
|
|
18967
|
-
|
|
18968
|
-
|
|
18969
|
-
|
|
18970
|
-
|
|
18971
|
-
|
|
18972
|
-
|
|
18973
|
-
|
|
18974
|
-
|
|
18975
|
-
|
|
18644
|
+
function writeQuadInstance(node, array, offset) {
|
|
18645
|
+
if (!(node instanceof QuadNode)) {
|
|
18646
|
+
throw new Error("QuadNode.writeInstance can only be called on QuadNodes");
|
|
18647
|
+
}
|
|
18648
|
+
array.set(node.matrixWithSize, offset);
|
|
18649
|
+
array.set([node.color.r, node.color.g, node.color.b, node.color.a], offset + 12);
|
|
18650
|
+
const region = node.region;
|
|
18651
|
+
if (node.textureId === PRIMITIVE_TEXTURE) {
|
|
18652
|
+
array.set([
|
|
18653
|
+
node.atlasCoords.uvOffset.x,
|
|
18654
|
+
node.atlasCoords.uvOffset.y,
|
|
18655
|
+
node.atlasCoords.uvScale.width,
|
|
18656
|
+
node.atlasCoords.uvScale.height
|
|
18657
|
+
], offset + 16);
|
|
18658
|
+
} else {
|
|
18659
|
+
const atlasSize = node.extra.atlasSize();
|
|
18660
|
+
array.set([
|
|
18661
|
+
node.atlasCoords.uvOffset.x + region.x / atlasSize.width,
|
|
18662
|
+
node.atlasCoords.uvOffset.y + region.y / atlasSize.height,
|
|
18663
|
+
region.width / atlasSize.width,
|
|
18664
|
+
region.height / atlasSize.height
|
|
18665
|
+
], offset + 16);
|
|
18666
|
+
}
|
|
18667
|
+
array.set([
|
|
18668
|
+
node.cropOffset.x / 2 / (node.atlasCoords.originalSize.width || 1),
|
|
18669
|
+
node.cropOffset.y / 2 / (node.atlasCoords.originalSize.height || 1),
|
|
18670
|
+
node.extra.cropRatio().width,
|
|
18671
|
+
node.extra.cropRatio().height
|
|
18672
|
+
], offset + 20);
|
|
18673
|
+
new DataView(array.buffer).setUint32(array.byteOffset + (offset + 24) * Float32Array.BYTES_PER_ELEMENT, node.atlasCoords.atlasIndex, true);
|
|
18674
|
+
node.writeInstance?.(array, offset + 28);
|
|
18675
|
+
return 1;
|
|
18676
|
+
}
|
|
18677
|
+
|
|
18678
|
+
// src/scene/JumboQuadNode.ts
|
|
18679
|
+
var MAT3_SIZE = 12;
|
|
18680
|
+
var VEC4F_SIZE = 4;
|
|
18681
|
+
|
|
18682
|
+
class JumboQuadNode extends QuadNode {
|
|
18683
|
+
#tiles;
|
|
18684
|
+
#matrixPool;
|
|
18685
|
+
constructor(options, matrixPool) {
|
|
18686
|
+
assert(options.shader, "JumboQuadNode requires a shader to be explicitly provided");
|
|
18687
|
+
assert(options.tiles && options.tiles.length > 0, "JumboQuadNode requires at least one tile to be provided");
|
|
18688
|
+
options.render ??= {
|
|
18689
|
+
shader: options.shader,
|
|
18690
|
+
writeInstance: writeJumboQuadInstance
|
|
18691
|
+
};
|
|
18692
|
+
super({
|
|
18693
|
+
...options,
|
|
18694
|
+
atlasCoords: options.tiles[0].atlasCoords
|
|
18695
|
+
}, matrixPool);
|
|
18696
|
+
this.#matrixPool = matrixPool;
|
|
18697
|
+
this.#tiles = [];
|
|
18698
|
+
for (const tile of options.tiles) {
|
|
18699
|
+
assert(tile.atlasCoords, "JumboQuadNode requires atlas coords to be provided");
|
|
18700
|
+
assert(tile.size, "JumboQuadNode requires a size to be provided");
|
|
18701
|
+
this.#tiles.push({
|
|
18702
|
+
textureId: tile.textureId,
|
|
18703
|
+
offset: tile.offset,
|
|
18704
|
+
size: tile.size,
|
|
18705
|
+
atlasCoords: tile.atlasCoords
|
|
18706
|
+
});
|
|
18976
18707
|
}
|
|
18977
|
-
|
|
18708
|
+
}
|
|
18709
|
+
get atlasCoords() {
|
|
18710
|
+
throw new Error("JumboQuadNode does not have a single atlas coords");
|
|
18711
|
+
}
|
|
18712
|
+
get tiles() {
|
|
18713
|
+
return this.#tiles;
|
|
18714
|
+
}
|
|
18715
|
+
getTileMatrix(tile) {
|
|
18716
|
+
const matrix = mat3.clone(this.matrix, this.#matrixPool.get());
|
|
18717
|
+
const originalSize = {
|
|
18718
|
+
width: Math.max(...this.#tiles.map((t3) => t3.offset.x + t3.size.width)),
|
|
18719
|
+
height: Math.max(...this.#tiles.map((t3) => t3.offset.y + t3.size.height))
|
|
18720
|
+
};
|
|
18721
|
+
const proportionalSize = {
|
|
18722
|
+
width: this.size.width / originalSize.width,
|
|
18723
|
+
height: this.size.height / originalSize.height
|
|
18724
|
+
};
|
|
18725
|
+
const centerOffset = {
|
|
18726
|
+
x: tile.offset.x + tile.size.width / 2 - originalSize.width / 2,
|
|
18727
|
+
y: -(tile.offset.y + tile.size.height / 2 - originalSize.height / 2)
|
|
18728
|
+
};
|
|
18729
|
+
mat3.translate(matrix, [
|
|
18730
|
+
centerOffset.x * proportionalSize.width,
|
|
18731
|
+
centerOffset.y * proportionalSize.height
|
|
18732
|
+
], matrix);
|
|
18733
|
+
mat3.scale(matrix, [
|
|
18734
|
+
tile.size.width * proportionalSize.width * (this.flipX ? -1 : 1),
|
|
18735
|
+
tile.size.height * proportionalSize.height * (this.flipY ? -1 : 1)
|
|
18736
|
+
], matrix);
|
|
18737
|
+
return matrix;
|
|
18738
|
+
}
|
|
18739
|
+
}
|
|
18740
|
+
function writeJumboQuadInstance(node, array, offset) {
|
|
18741
|
+
if (!(node instanceof JumboQuadNode)) {
|
|
18742
|
+
throw new Error("JumboQuadNode.writeJumboQuadInstance can only be called on JumboQuadNodes");
|
|
18743
|
+
}
|
|
18744
|
+
let tileOffset = 0;
|
|
18745
|
+
for (const tile of node.tiles) {
|
|
18746
|
+
const coord = tile.atlasCoords;
|
|
18747
|
+
const matrix = node.getTileMatrix(tile);
|
|
18748
|
+
array.set(matrix, offset + tileOffset);
|
|
18749
|
+
tileOffset += MAT3_SIZE;
|
|
18750
|
+
array.set([node.color.r, node.color.g, node.color.b, node.color.a], offset + tileOffset);
|
|
18751
|
+
tileOffset += VEC4F_SIZE;
|
|
18752
|
+
array.set([
|
|
18753
|
+
coord.uvOffset.x,
|
|
18754
|
+
coord.uvOffset.y,
|
|
18755
|
+
coord.uvScale.width,
|
|
18756
|
+
coord.uvScale.height
|
|
18757
|
+
], offset + tileOffset);
|
|
18758
|
+
tileOffset += VEC4F_SIZE;
|
|
18759
|
+
const cropRatio = !coord.uvScaleCropped ? { width: 1, height: 1 } : {
|
|
18760
|
+
width: coord.uvScaleCropped.width / coord.uvScale.width,
|
|
18761
|
+
height: coord.uvScaleCropped.height / coord.uvScale.height
|
|
18762
|
+
};
|
|
18763
|
+
array.set([
|
|
18764
|
+
tile.atlasCoords.cropOffset.x / 2 / (tile.atlasCoords.originalSize.width || 1),
|
|
18765
|
+
tile.atlasCoords.cropOffset.y / 2 / (tile.atlasCoords.originalSize.height || 1),
|
|
18766
|
+
cropRatio.width,
|
|
18767
|
+
cropRatio.height
|
|
18768
|
+
], offset + tileOffset);
|
|
18769
|
+
tileOffset += VEC4F_SIZE;
|
|
18770
|
+
new DataView(array.buffer).setUint32(array.byteOffset + (offset + tileOffset) * Float32Array.BYTES_PER_ELEMENT, coord.atlasIndex, true);
|
|
18771
|
+
tileOffset += VEC4F_SIZE;
|
|
18772
|
+
}
|
|
18773
|
+
node.writeInstance?.(array, offset + tileOffset);
|
|
18774
|
+
return node.tiles.length;
|
|
18775
|
+
}
|
|
18776
|
+
|
|
18777
|
+
// src/backends/webgpu/wgsl/pixel-scraping.wgsl.ts
|
|
18778
|
+
var pixel_scraping_wgsl_default = `
|
|
18779
|
+
// ==============================
|
|
18780
|
+
// === BOUNDING BOX PASS =======
|
|
18781
|
+
// ==============================
|
|
18782
|
+
|
|
18783
|
+
// Input texture from which to compute the non-transparent bounding box
|
|
18784
|
+
@group(0) @binding(0)
|
|
18785
|
+
var input_texture: texture_2d<f32>;
|
|
18786
|
+
|
|
18787
|
+
// Atomic bounding box storage structure
|
|
18788
|
+
struct bounding_box_atomic {
|
|
18789
|
+
min_x: atomic<u32>,
|
|
18790
|
+
min_y: atomic<u32>,
|
|
18791
|
+
max_x: atomic<u32>,
|
|
18792
|
+
max_y: atomic<u32>,
|
|
18978
18793
|
};
|
|
18979
|
-
|
|
18980
|
-
|
|
18981
|
-
|
|
18982
|
-
|
|
18983
|
-
|
|
18984
|
-
|
|
18985
|
-
|
|
18794
|
+
|
|
18795
|
+
// Storage buffer to hold atomic bounding box updates
|
|
18796
|
+
@group(0) @binding(1)
|
|
18797
|
+
var<storage, read_write> bounds: bounding_box_atomic;
|
|
18798
|
+
|
|
18799
|
+
// Compute shader to find the bounding box of non-transparent pixels
|
|
18800
|
+
@compute @workgroup_size(8, 8)
|
|
18801
|
+
fn find_bounds(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
18802
|
+
let size = textureDimensions(input_texture).xy;
|
|
18803
|
+
if (gid.x >= size.x || gid.y >= size.y) {
|
|
18804
|
+
return;
|
|
18986
18805
|
}
|
|
18987
|
-
|
|
18806
|
+
|
|
18807
|
+
let pixel = textureLoad(input_texture, vec2<i32>(gid.xy), 0);
|
|
18808
|
+
if (pixel.a > 0.0) {
|
|
18809
|
+
atomicMin(&bounds.min_x, gid.x);
|
|
18810
|
+
atomicMin(&bounds.min_y, gid.y);
|
|
18811
|
+
atomicMax(&bounds.max_x, gid.x);
|
|
18812
|
+
atomicMax(&bounds.max_y, gid.y);
|
|
18813
|
+
}
|
|
18814
|
+
}
|
|
18815
|
+
|
|
18816
|
+
// ==============================
|
|
18817
|
+
// === CROP + OUTPUT PASS ======
|
|
18818
|
+
// ==============================
|
|
18819
|
+
|
|
18820
|
+
// Input texture from which cropped data is read
|
|
18821
|
+
@group(0) @binding(0)
|
|
18822
|
+
var input_texture_crop: texture_2d<f32>;
|
|
18823
|
+
|
|
18824
|
+
// Output texture where cropped image is written
|
|
18825
|
+
@group(0) @binding(1)
|
|
18826
|
+
var output_texture: texture_storage_2d<rgba8unorm, write>;
|
|
18827
|
+
|
|
18828
|
+
// Bounding box passed in as a uniform (not atomic anymore)
|
|
18829
|
+
struct bounding_box_uniform {
|
|
18830
|
+
min_x: u32,
|
|
18831
|
+
min_y: u32,
|
|
18832
|
+
max_x: u32,
|
|
18833
|
+
max_y: u32,
|
|
18988
18834
|
};
|
|
18989
|
-
|
|
18990
|
-
|
|
18991
|
-
|
|
18992
|
-
|
|
18993
|
-
|
|
18994
|
-
|
|
18835
|
+
|
|
18836
|
+
@group(0) @binding(2)
|
|
18837
|
+
var<uniform> bounds_uniform: bounding_box_uniform;
|
|
18838
|
+
|
|
18839
|
+
// Struct to store both original and cropped texture dimensions
|
|
18840
|
+
struct image_dimensions {
|
|
18841
|
+
original_width: u32,
|
|
18842
|
+
original_height: u32,
|
|
18843
|
+
cropped_width: u32,
|
|
18844
|
+
cropped_height: u32,
|
|
18995
18845
|
};
|
|
18996
|
-
|
|
18997
|
-
|
|
18998
|
-
|
|
18999
|
-
|
|
19000
|
-
|
|
19001
|
-
|
|
19002
|
-
|
|
19003
|
-
|
|
19004
|
-
|
|
19005
|
-
|
|
19006
|
-
|
|
19007
|
-
|
|
18846
|
+
|
|
18847
|
+
// Storage buffer to output the result dimensions
|
|
18848
|
+
@group(0) @binding(3)
|
|
18849
|
+
var<storage, read_write> dimensions_out: image_dimensions;
|
|
18850
|
+
|
|
18851
|
+
// Compute shader to crop the input texture to the bounding box and output it
|
|
18852
|
+
@compute @workgroup_size(8, 8)
|
|
18853
|
+
fn crop_and_output(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
18854
|
+
let size = textureDimensions(input_texture_crop).xy;
|
|
18855
|
+
|
|
18856
|
+
let crop_width = bounds_uniform.max_x - bounds_uniform.min_x + 1u;
|
|
18857
|
+
let crop_height = bounds_uniform.max_y - bounds_uniform.min_y + 1u;
|
|
18858
|
+
|
|
18859
|
+
if (gid.x >= crop_width || gid.y >= crop_height) {
|
|
18860
|
+
return;
|
|
19008
18861
|
}
|
|
19009
|
-
]
|
|
19010
|
-
};
|
|
19011
18862
|
|
|
19012
|
-
|
|
19013
|
-
|
|
19014
|
-
|
|
19015
|
-
|
|
19016
|
-
|
|
18863
|
+
let src_coord = vec2<i32>(
|
|
18864
|
+
i32(bounds_uniform.min_x + gid.x),
|
|
18865
|
+
i32(bounds_uniform.min_y + gid.y)
|
|
18866
|
+
);
|
|
18867
|
+
|
|
18868
|
+
let dst_coord = vec2<i32>(i32(gid.x), i32(gid.y));
|
|
18869
|
+
let pixel = textureLoad(input_texture_crop, src_coord, 0);
|
|
18870
|
+
textureStore(output_texture, dst_coord, pixel);
|
|
18871
|
+
|
|
18872
|
+
// Output dimensions from workgroup (0,0) only
|
|
18873
|
+
if (gid.x == 0u && gid.y == 0u) {
|
|
18874
|
+
dimensions_out.original_width = size.x;
|
|
18875
|
+
dimensions_out.original_height = size.y;
|
|
18876
|
+
dimensions_out.cropped_width = crop_width;
|
|
18877
|
+
dimensions_out.cropped_height = crop_height;
|
|
18878
|
+
}
|
|
19017
18879
|
}
|
|
19018
|
-
var textDescriptorInstanceSize = struct.size;
|
|
19019
18880
|
|
|
19020
|
-
|
|
19021
|
-
|
|
19022
|
-
|
|
19023
|
-
|
|
19024
|
-
|
|
19025
|
-
|
|
19026
|
-
|
|
19027
|
-
|
|
19028
|
-
|
|
19029
|
-
|
|
19030
|
-
|
|
19031
|
-
|
|
19032
|
-
|
|
19033
|
-
|
|
19034
|
-
|
|
19035
|
-
|
|
19036
|
-
|
|
19037
|
-
|
|
19038
|
-
|
|
19039
|
-
|
|
19040
|
-
|
|
19041
|
-
|
|
19042
|
-
|
|
19043
|
-
|
|
18881
|
+
// ==============================
|
|
18882
|
+
// === MISSING TEXTURE FILL ====
|
|
18883
|
+
// ==============================
|
|
18884
|
+
|
|
18885
|
+
// Output texture to draw a fallback checkerboard
|
|
18886
|
+
@group(0) @binding(0)
|
|
18887
|
+
var checker_texture: texture_storage_2d<rgba8unorm, write>;
|
|
18888
|
+
|
|
18889
|
+
// Compute shader to fill a texture with a purple & green checkerboard
|
|
18890
|
+
@compute @workgroup_size(8, 8)
|
|
18891
|
+
fn missing_texture(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
18892
|
+
let size = textureDimensions(checker_texture);
|
|
18893
|
+
if (id.x >= size.x || id.y >= size.y) {
|
|
18894
|
+
return;
|
|
18895
|
+
}
|
|
18896
|
+
|
|
18897
|
+
let checker_size = 25u;
|
|
18898
|
+
let on_color = ((id.x / checker_size + id.y / checker_size) % 2u) == 0u;
|
|
18899
|
+
|
|
18900
|
+
let color = select(
|
|
18901
|
+
vec4<f32>(0.5, 0.0, 0.5, 1.0), // Purple
|
|
18902
|
+
vec4<f32>(0.0, 1.0, 0.0, 1.0), // Green
|
|
18903
|
+
on_color
|
|
18904
|
+
);
|
|
18905
|
+
|
|
18906
|
+
textureStore(checker_texture, vec2<i32>(id.xy), color);
|
|
18907
|
+
}
|
|
18908
|
+
`;
|
|
18909
|
+
|
|
18910
|
+
// src/backends/webgpu/TextureComputeShader.ts
|
|
18911
|
+
var BOUNDING_BOX_SIZE = 4 * Uint32Array.BYTES_PER_ELEMENT;
|
|
18912
|
+
var WORKGROUP_SIZE = 8;
|
|
18913
|
+
var MAX_BOUND = 4294967295;
|
|
18914
|
+
var MIN_BOUND = 0;
|
|
18915
|
+
|
|
18916
|
+
class TextureComputeShader {
|
|
18917
|
+
#device;
|
|
18918
|
+
#boundingBuffer;
|
|
18919
|
+
#cropPipeline;
|
|
18920
|
+
#boundPipeline;
|
|
18921
|
+
#missingTexturePipeline;
|
|
18922
|
+
constructor(device, cropPipeline, boundPipeline, missingTexturePipeline) {
|
|
18923
|
+
this.#device = device;
|
|
18924
|
+
this.#boundPipeline = boundPipeline;
|
|
18925
|
+
this.#cropPipeline = cropPipeline;
|
|
18926
|
+
this.#missingTexturePipeline = missingTexturePipeline;
|
|
18927
|
+
this.#boundingBuffer = this.#device.createBuffer({
|
|
18928
|
+
size: BOUNDING_BOX_SIZE,
|
|
18929
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST
|
|
18930
|
+
});
|
|
18931
|
+
}
|
|
18932
|
+
static create(device) {
|
|
18933
|
+
const pipelines = createPipelines(device, "TextureComputeShader");
|
|
18934
|
+
return new TextureComputeShader(device, pipelines.cropPipeline, pipelines.boundPipeline, pipelines.missingTexturePipeline);
|
|
18935
|
+
}
|
|
18936
|
+
async processTexture(textureWrapper) {
|
|
18937
|
+
const boundsBindGroup = this.#boundsBindGroup(textureWrapper.texture);
|
|
18938
|
+
const commandEncoder = this.#device.createCommandEncoder();
|
|
18939
|
+
const passEncoder = commandEncoder.beginComputePass();
|
|
18940
|
+
const dispatchX = Math.ceil(textureWrapper.texture.width / WORKGROUP_SIZE);
|
|
18941
|
+
const dispatchY = Math.ceil(textureWrapper.texture.height / WORKGROUP_SIZE);
|
|
18942
|
+
const boundsInit = new Uint32Array([
|
|
18943
|
+
MAX_BOUND,
|
|
18944
|
+
MAX_BOUND,
|
|
18945
|
+
MIN_BOUND,
|
|
18946
|
+
MIN_BOUND
|
|
18947
|
+
]);
|
|
18948
|
+
this.#device.queue.writeBuffer(this.#boundingBuffer, 0, boundsInit.buffer, 0, BOUNDING_BOX_SIZE);
|
|
18949
|
+
passEncoder.setPipeline(this.#boundPipeline);
|
|
18950
|
+
passEncoder.setBindGroup(0, boundsBindGroup);
|
|
18951
|
+
passEncoder.dispatchWorkgroups(dispatchX, dispatchY);
|
|
18952
|
+
passEncoder.end();
|
|
18953
|
+
this.#device.queue.submit([commandEncoder.finish()]);
|
|
18954
|
+
const { texelX, texelY, texelWidth, texelHeight, computeBuffer } = await this.#getBoundingBox();
|
|
18955
|
+
if (texelX === MAX_BOUND || texelY === MAX_BOUND) {
|
|
18956
|
+
return await this.#createMissingTexture(textureWrapper.texture);
|
|
18957
|
+
}
|
|
18958
|
+
const croppedTexture = await this.#cropTexture(texelWidth, texelHeight, computeBuffer, textureWrapper.texture);
|
|
18959
|
+
const leftCrop = texelX;
|
|
18960
|
+
const rightCrop = textureWrapper.originalSize.width - texelX - texelWidth;
|
|
18961
|
+
const topCrop = texelY;
|
|
18962
|
+
const bottomCrop = textureWrapper.originalSize.height - texelY - texelHeight;
|
|
18963
|
+
textureWrapper = {
|
|
18964
|
+
texture: croppedTexture,
|
|
18965
|
+
cropOffset: { x: leftCrop - rightCrop, y: bottomCrop - topCrop },
|
|
18966
|
+
originalSize: textureWrapper.originalSize
|
|
18967
|
+
};
|
|
18968
|
+
return textureWrapper;
|
|
18969
|
+
}
|
|
18970
|
+
async#getBoundingBox() {
|
|
18971
|
+
const readBuffer = this.#device.createBuffer({
|
|
18972
|
+
label: "AABB Compute Buffer",
|
|
18973
|
+
size: BOUNDING_BOX_SIZE,
|
|
18974
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
19044
18975
|
});
|
|
19045
|
-
|
|
19046
|
-
this.#
|
|
19047
|
-
this.#
|
|
19048
|
-
|
|
19049
|
-
|
|
18976
|
+
const copyEncoder = this.#device.createCommandEncoder();
|
|
18977
|
+
copyEncoder.copyBufferToBuffer(this.#boundingBuffer, 0, readBuffer, 0, BOUNDING_BOX_SIZE);
|
|
18978
|
+
this.#device.queue.submit([copyEncoder.finish()]);
|
|
18979
|
+
await readBuffer.mapAsync(GPUMapMode.READ);
|
|
18980
|
+
const computeBuffer = new Uint32Array(readBuffer.getMappedRange().slice(0));
|
|
18981
|
+
readBuffer.unmap();
|
|
18982
|
+
const [minX, minY, maxX, maxY] = computeBuffer;
|
|
18983
|
+
return {
|
|
18984
|
+
texelX: minX,
|
|
18985
|
+
texelY: minY,
|
|
18986
|
+
texelWidth: maxX - minX + 1,
|
|
18987
|
+
texelHeight: maxY - minY + 1,
|
|
18988
|
+
computeBuffer
|
|
18989
|
+
};
|
|
18990
|
+
}
|
|
18991
|
+
async#cropTexture(croppedWidth, croppedHeight, computeBuffer, inputTexture) {
|
|
18992
|
+
const boundsUniform = this.#device.createBuffer({
|
|
18993
|
+
label: "Cropping Bounds Uniform Buffer",
|
|
18994
|
+
size: BOUNDING_BOX_SIZE,
|
|
19050
18995
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
19051
18996
|
});
|
|
19052
|
-
this.#
|
|
19053
|
-
|
|
19054
|
-
|
|
19055
|
-
|
|
18997
|
+
this.#device.queue.writeBuffer(boundsUniform, 0, computeBuffer);
|
|
18998
|
+
const outputTexture = this.#device.createTexture({
|
|
18999
|
+
label: "Cropped Texture",
|
|
19000
|
+
size: [croppedWidth, croppedHeight],
|
|
19001
|
+
format: "rgba8unorm",
|
|
19002
|
+
usage: GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC
|
|
19056
19003
|
});
|
|
19057
|
-
this.#
|
|
19058
|
-
|
|
19059
|
-
|
|
19060
|
-
|
|
19004
|
+
const dimensionsOutBuffer = this.#device.createBuffer({
|
|
19005
|
+
label: "Cropping Dimensions Output Buffer",
|
|
19006
|
+
size: BOUNDING_BOX_SIZE,
|
|
19007
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
|
|
19008
|
+
});
|
|
19009
|
+
const bindGroup = this.#croppingBindGroup(inputTexture, outputTexture, boundsUniform, dimensionsOutBuffer);
|
|
19010
|
+
const encoder = this.#device.createCommandEncoder();
|
|
19011
|
+
const pass = encoder.beginComputePass();
|
|
19012
|
+
pass.setPipeline(this.#cropPipeline);
|
|
19013
|
+
pass.setBindGroup(0, bindGroup);
|
|
19014
|
+
pass.dispatchWorkgroups(Math.ceil(croppedWidth / WORKGROUP_SIZE), Math.ceil(croppedHeight / WORKGROUP_SIZE));
|
|
19015
|
+
pass.end();
|
|
19016
|
+
this.#device.queue.submit([encoder.finish()]);
|
|
19017
|
+
return outputTexture;
|
|
19018
|
+
}
|
|
19019
|
+
async#createMissingTexture(inputTexture) {
|
|
19020
|
+
const placeholder = this.#device.createTexture({
|
|
19021
|
+
label: "Missing Placeholder Texture",
|
|
19022
|
+
size: [inputTexture.width, inputTexture.height],
|
|
19023
|
+
format: "rgba8unorm",
|
|
19024
|
+
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING
|
|
19025
|
+
});
|
|
19026
|
+
const encoder = this.#device.createCommandEncoder();
|
|
19027
|
+
const pass = encoder.beginComputePass();
|
|
19028
|
+
pass.setPipeline(this.#missingTexturePipeline);
|
|
19029
|
+
pass.setBindGroup(0, this.#missingTextureBindGroup(placeholder));
|
|
19030
|
+
pass.dispatchWorkgroups(placeholder.width / 8, placeholder.height / 8);
|
|
19031
|
+
pass.end();
|
|
19032
|
+
this.#device.queue.submit([encoder.finish()]);
|
|
19033
|
+
return {
|
|
19034
|
+
texture: placeholder,
|
|
19035
|
+
cropOffset: { x: 0, y: 0 },
|
|
19036
|
+
originalSize: { width: inputTexture.width, height: inputTexture.height }
|
|
19037
|
+
};
|
|
19038
|
+
}
|
|
19039
|
+
#boundsBindGroup(inputTexture) {
|
|
19040
|
+
return this.#device.createBindGroup({
|
|
19041
|
+
layout: this.#boundPipeline.getBindGroupLayout(0),
|
|
19061
19042
|
entries: [
|
|
19062
|
-
{
|
|
19063
|
-
|
|
19064
|
-
resource: { buffer: this.#descriptorBuffer }
|
|
19065
|
-
},
|
|
19066
|
-
{
|
|
19067
|
-
binding: 1,
|
|
19068
|
-
resource: { buffer: this.#textBlockBuffer }
|
|
19069
|
-
}
|
|
19043
|
+
{ binding: 0, resource: inputTexture.createView() },
|
|
19044
|
+
{ binding: 1, resource: { buffer: this.#boundingBuffer } }
|
|
19070
19045
|
]
|
|
19071
|
-
})
|
|
19072
|
-
|
|
19073
|
-
|
|
19074
|
-
|
|
19046
|
+
});
|
|
19047
|
+
}
|
|
19048
|
+
#croppingBindGroup(inputTexture, outputTexture, boundsUniform, dimensionsOutBuffer) {
|
|
19049
|
+
return this.#device.createBindGroup({
|
|
19050
|
+
layout: this.#cropPipeline.getBindGroupLayout(0),
|
|
19075
19051
|
entries: [
|
|
19076
|
-
{
|
|
19077
|
-
|
|
19078
|
-
|
|
19079
|
-
}
|
|
19052
|
+
{ binding: 0, resource: inputTexture.createView() },
|
|
19053
|
+
{ binding: 1, resource: outputTexture.createView() },
|
|
19054
|
+
{ binding: 2, resource: { buffer: boundsUniform } },
|
|
19055
|
+
{ binding: 3, resource: { buffer: dimensionsOutBuffer } }
|
|
19080
19056
|
]
|
|
19081
19057
|
});
|
|
19082
|
-
this.#bindGroups.push(engineUniformsBindGroup);
|
|
19083
19058
|
}
|
|
19084
|
-
|
|
19085
|
-
|
|
19086
|
-
|
|
19087
|
-
|
|
19088
|
-
|
|
19059
|
+
#missingTextureBindGroup(outputTexture) {
|
|
19060
|
+
return this.#device.createBindGroup({
|
|
19061
|
+
layout: this.#missingTexturePipeline.getBindGroupLayout(0),
|
|
19062
|
+
entries: [{ binding: 0, resource: outputTexture.createView() }]
|
|
19063
|
+
});
|
|
19089
19064
|
}
|
|
19090
|
-
|
|
19091
|
-
|
|
19092
|
-
|
|
19093
|
-
|
|
19094
|
-
|
|
19095
|
-
|
|
19096
|
-
|
|
19097
|
-
|
|
19098
|
-
|
|
19099
|
-
|
|
19100
|
-
|
|
19101
|
-
|
|
19065
|
+
}
|
|
19066
|
+
function createPipelines(device, label) {
|
|
19067
|
+
const shader = device.createShaderModule({
|
|
19068
|
+
label: `${label} Shader`,
|
|
19069
|
+
code: pixel_scraping_wgsl_default
|
|
19070
|
+
});
|
|
19071
|
+
const findBoundsBindGroupLayout = device.createBindGroupLayout({
|
|
19072
|
+
label: "Bounds Detection Layout",
|
|
19073
|
+
entries: [
|
|
19074
|
+
{
|
|
19075
|
+
binding: 0,
|
|
19076
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
19077
|
+
texture: { sampleType: "float", viewDimension: "2d" }
|
|
19078
|
+
},
|
|
19079
|
+
{
|
|
19080
|
+
binding: 1,
|
|
19081
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
19082
|
+
buffer: { type: "storage" }
|
|
19102
19083
|
}
|
|
19103
|
-
|
|
19104
|
-
|
|
19105
|
-
|
|
19106
|
-
|
|
19107
|
-
|
|
19108
|
-
|
|
19109
|
-
|
|
19110
|
-
|
|
19111
|
-
|
|
19112
|
-
|
|
19113
|
-
|
|
19114
|
-
|
|
19115
|
-
|
|
19116
|
-
|
|
19117
|
-
|
|
19118
|
-
|
|
19119
|
-
|
|
19120
|
-
|
|
19121
|
-
|
|
19122
|
-
|
|
19123
|
-
|
|
19124
|
-
|
|
19125
|
-
|
|
19126
|
-
|
|
19127
|
-
|
|
19128
|
-
|
|
19129
|
-
}
|
|
19130
|
-
|
|
19131
|
-
|
|
19132
|
-
|
|
19084
|
+
]
|
|
19085
|
+
});
|
|
19086
|
+
const cropBindGroupLayout = device.createBindGroupLayout({
|
|
19087
|
+
label: "Cropping Layout",
|
|
19088
|
+
entries: [
|
|
19089
|
+
{ binding: 0, visibility: GPUShaderStage.COMPUTE, texture: {} },
|
|
19090
|
+
{
|
|
19091
|
+
binding: 1,
|
|
19092
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
19093
|
+
storageTexture: {
|
|
19094
|
+
access: "write-only",
|
|
19095
|
+
format: "rgba8unorm",
|
|
19096
|
+
viewDimension: "2d"
|
|
19097
|
+
}
|
|
19098
|
+
},
|
|
19099
|
+
{
|
|
19100
|
+
binding: 2,
|
|
19101
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
19102
|
+
buffer: { type: "uniform" }
|
|
19103
|
+
},
|
|
19104
|
+
{
|
|
19105
|
+
binding: 3,
|
|
19106
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
19107
|
+
buffer: { type: "storage" }
|
|
19108
|
+
}
|
|
19109
|
+
]
|
|
19110
|
+
});
|
|
19111
|
+
const missingTextureBindGroupLayout = device.createBindGroupLayout({
|
|
19112
|
+
label: "Missing Texture Layout",
|
|
19113
|
+
entries: [
|
|
19114
|
+
{
|
|
19115
|
+
binding: 0,
|
|
19116
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
19117
|
+
storageTexture: {
|
|
19118
|
+
access: "write-only",
|
|
19119
|
+
format: "rgba8unorm",
|
|
19120
|
+
viewDimension: "2d"
|
|
19121
|
+
}
|
|
19122
|
+
}
|
|
19123
|
+
]
|
|
19124
|
+
});
|
|
19125
|
+
return {
|
|
19126
|
+
boundPipeline: device.createComputePipeline({
|
|
19127
|
+
label: `${label} - Find Bounds Pipeline`,
|
|
19128
|
+
layout: device.createPipelineLayout({
|
|
19129
|
+
bindGroupLayouts: [findBoundsBindGroupLayout]
|
|
19130
|
+
}),
|
|
19131
|
+
compute: { module: shader, entryPoint: "find_bounds" }
|
|
19132
|
+
}),
|
|
19133
|
+
cropPipeline: device.createComputePipeline({
|
|
19134
|
+
label: `${label} - Crop Pipeline`,
|
|
19135
|
+
layout: device.createPipelineLayout({
|
|
19136
|
+
bindGroupLayouts: [cropBindGroupLayout]
|
|
19137
|
+
}),
|
|
19138
|
+
compute: { module: shader, entryPoint: "crop_and_output" }
|
|
19139
|
+
}),
|
|
19140
|
+
missingTexturePipeline: device.createComputePipeline({
|
|
19141
|
+
label: `${label} - Missing Texture Pipeline`,
|
|
19142
|
+
layout: device.createPipelineLayout({
|
|
19143
|
+
bindGroupLayouts: [missingTextureBindGroupLayout]
|
|
19144
|
+
}),
|
|
19145
|
+
compute: { module: shader, entryPoint: "missing_texture" }
|
|
19146
|
+
})
|
|
19147
|
+
};
|
|
19133
19148
|
}
|
|
19134
19149
|
|
|
19135
19150
|
// src/textures/Bundles.ts
|
|
@@ -19697,18 +19712,20 @@ class AssetManager {
|
|
|
19697
19712
|
this.bundles.unloadBundle(bundleId);
|
|
19698
19713
|
}
|
|
19699
19714
|
async loadFont(id, url, fallbackCharacter = "_") {
|
|
19700
|
-
if (this.#backend.type !== "webgpu") {
|
|
19701
|
-
throw new Error("loadFont is only supported with WebGPU backend. Text rendering is not available in WebGL mode.");
|
|
19702
|
-
}
|
|
19703
|
-
const webgpuBackend = this.#backend;
|
|
19704
|
-
const device = webgpuBackend.device;
|
|
19705
|
-
const presentationFormat = webgpuBackend.presentationFormat;
|
|
19706
19715
|
const limits = this.#backend.limits;
|
|
19707
19716
|
const font = await MsdfFont.create(id, url);
|
|
19708
19717
|
font.fallbackCharacter = fallbackCharacter;
|
|
19709
|
-
|
|
19710
|
-
|
|
19711
|
-
|
|
19718
|
+
if (this.#backend.type === "webgpu") {
|
|
19719
|
+
const webgpuBackend = this.#backend;
|
|
19720
|
+
const device = webgpuBackend.device;
|
|
19721
|
+
const presentationFormat = webgpuBackend.presentationFormat;
|
|
19722
|
+
const fontPipeline = await FontPipeline.create(device, font, presentationFormat, limits.maxTextLength);
|
|
19723
|
+
const textShader = new WebGPUTextShader(webgpuBackend, fontPipeline, font, presentationFormat, limits.instanceCount);
|
|
19724
|
+
this.#fonts.set(id, textShader);
|
|
19725
|
+
} else {
|
|
19726
|
+
const textShader = new WebGLTextShader(font, limits.maxTextLength);
|
|
19727
|
+
this.#fonts.set(id, textShader);
|
|
19728
|
+
}
|
|
19712
19729
|
return id;
|
|
19713
19730
|
}
|
|
19714
19731
|
getFont(id) {
|
|
@@ -20394,8 +20411,10 @@ __export(exports_mod4, {
|
|
|
20394
20411
|
// src/scene/mod.ts
|
|
20395
20412
|
var exports_mod5 = {};
|
|
20396
20413
|
__export(exports_mod5, {
|
|
20414
|
+
TextNode: () => TextNode,
|
|
20397
20415
|
SceneNode: () => SceneNode,
|
|
20398
20416
|
QuadNode: () => QuadNode,
|
|
20417
|
+
DEFAULT_FONT_SIZE: () => DEFAULT_FONT_SIZE,
|
|
20399
20418
|
Camera: () => Camera
|
|
20400
20419
|
});
|
|
20401
20420
|
// src/screen/mod.ts
|
|
@@ -20403,8 +20422,7 @@ var exports_mod6 = {};
|
|
|
20403
20422
|
// src/text/mod.ts
|
|
20404
20423
|
var exports_mod7 = {};
|
|
20405
20424
|
__export(exports_mod7, {
|
|
20406
|
-
TextShader: () =>
|
|
20407
|
-
TextNode: () => TextNode
|
|
20425
|
+
TextShader: () => WebGPUTextShader
|
|
20408
20426
|
});
|
|
20409
20427
|
// src/textures/mod.ts
|
|
20410
20428
|
var exports_mod8 = {};
|
|
@@ -20426,5 +20444,5 @@ export {
|
|
|
20426
20444
|
AssetManager
|
|
20427
20445
|
};
|
|
20428
20446
|
|
|
20429
|
-
//# debugId=
|
|
20447
|
+
//# debugId=7918C9E3F46FD35864756E2164756E21
|
|
20430
20448
|
//# sourceMappingURL=mod.js.map
|