UNPKG

monaco-editor

Version:
130 lines (127 loc) 5.9 kB
import { BugIndicatingError } from '../../../../base/common/errors.js'; import { ensureNonNullable } from '../gpuUtils.js'; /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ /** * The shelf allocator is a simple allocator that places glyphs in rows, starting a new row when the * current row is full. Due to its simplicity, it can waste space but it is very fast. */ class TextureAtlasShelfAllocator { constructor(_canvas, _textureIndex) { this._canvas = _canvas; this._textureIndex = _textureIndex; this._currentRow = { x: 0, y: 0, h: 0 }; /** A set of all glyphs allocated, this is only tracked to enable debug related functionality */ this._allocatedGlyphs = new Set(); this._nextIndex = 0; this._ctx = ensureNonNullable(this._canvas.getContext('2d', { willReadFrequently: true })); } allocate(rasterizedGlyph) { // The glyph does not fit into the atlas page const glyphWidth = rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1; const glyphHeight = rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1; if (glyphWidth > this._canvas.width || glyphHeight > this._canvas.height) { throw new BugIndicatingError('Glyph is too large for the atlas page'); } // Finalize and increment row if it doesn't fix horizontally if (rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1 > this._canvas.width - this._currentRow.x) { this._currentRow.x = 0; this._currentRow.y += this._currentRow.h; this._currentRow.h = 1; } // Return undefined if there isn't any room left if (this._currentRow.y + rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1 > this._canvas.height) { return undefined; } // Draw glyph this._ctx.drawImage(rasterizedGlyph.source, // source rasterizedGlyph.boundingBox.left, rasterizedGlyph.boundingBox.top, glyphWidth, glyphHeight, // destination this._currentRow.x, this._currentRow.y, glyphWidth, glyphHeight); // Create glyph object const glyph = { pageIndex: this._textureIndex, glyphIndex: this._nextIndex++, x: this._currentRow.x, y: this._currentRow.y, w: glyphWidth, h: glyphHeight, originOffsetX: rasterizedGlyph.originOffset.x, originOffsetY: rasterizedGlyph.originOffset.y, fontBoundingBoxAscent: rasterizedGlyph.fontBoundingBoxAscent, fontBoundingBoxDescent: rasterizedGlyph.fontBoundingBoxDescent, }; // Shift current row this._currentRow.x += glyphWidth; this._currentRow.h = Math.max(this._currentRow.h, glyphHeight); // Set the glyph this._allocatedGlyphs.add(glyph); return glyph; } getUsagePreview() { const w = this._canvas.width; const h = this._canvas.height; const canvas = new OffscreenCanvas(w, h); const ctx = ensureNonNullable(canvas.getContext('2d')); ctx.fillStyle = "#808080" /* UsagePreviewColors.Unused */; ctx.fillRect(0, 0, w, h); const rowHeight = new Map(); // y -> h const rowWidth = new Map(); // y -> w for (const g of this._allocatedGlyphs) { rowHeight.set(g.y, Math.max(rowHeight.get(g.y) ?? 0, g.h)); rowWidth.set(g.y, Math.max(rowWidth.get(g.y) ?? 0, g.x + g.w)); } for (const g of this._allocatedGlyphs) { ctx.fillStyle = "#4040FF" /* UsagePreviewColors.Used */; ctx.fillRect(g.x, g.y, g.w, g.h); ctx.fillStyle = "#FF0000" /* UsagePreviewColors.Wasted */; ctx.fillRect(g.x, g.y + g.h, g.w, rowHeight.get(g.y) - g.h); } for (const [rowY, rowW] of rowWidth.entries()) { if (rowY !== this._currentRow.y) { ctx.fillStyle = "#FF0000" /* UsagePreviewColors.Wasted */; ctx.fillRect(rowW, rowY, w - rowW, rowHeight.get(rowY)); } } return canvas.convertToBlob(); } getStats() { const w = this._canvas.width; const h = this._canvas.height; let usedPixels = 0; let wastedPixels = 0; const totalPixels = w * h; const rowHeight = new Map(); // y -> h const rowWidth = new Map(); // y -> w for (const g of this._allocatedGlyphs) { rowHeight.set(g.y, Math.max(rowHeight.get(g.y) ?? 0, g.h)); rowWidth.set(g.y, Math.max(rowWidth.get(g.y) ?? 0, g.x + g.w)); } for (const g of this._allocatedGlyphs) { usedPixels += g.w * g.h; wastedPixels += g.w * (rowHeight.get(g.y) - g.h); } for (const [rowY, rowW] of rowWidth.entries()) { if (rowY !== this._currentRow.y) { wastedPixels += (w - rowW) * rowHeight.get(rowY); } } return [ `page${this._textureIndex}:`, ` Total: ${totalPixels} (${w}x${h})`, ` Used: ${usedPixels} (${((usedPixels / totalPixels) * 100).toPrecision(2)}%)`, ` Wasted: ${wastedPixels} (${((wastedPixels / totalPixels) * 100).toPrecision(2)}%)`, `Efficiency: ${((usedPixels / (usedPixels + wastedPixels)) * 100).toPrecision(2)}%`, ].join('\n'); } } export { TextureAtlasShelfAllocator };