Merge upstream/master - resolve IIPHandler.ts const enum conflict

This commit is contained in:
Anthony Kim
2026-02-12 13:47:48 -08:00
51 changed files with 602 additions and 184 deletions
+3 -2
View File
@@ -69,9 +69,10 @@ export class FitAddon implements ITerminalAddon, IFitApi {
return undefined;
}
const scrollbarWidth = (this._terminal.options.scrollback === 0
const showScrollbar = this._terminal.options.scrollbar?.showScrollbar ?? true;
const scrollbarWidth = (this._terminal.options.scrollback === 0 || !showScrollbar
? 0
: (this._terminal.options.overviewRuler?.width || ViewportConstants.DEFAULT_SCROLL_BAR_WIDTH));
: (this._terminal.options.scrollbar?.width ?? ViewportConstants.DEFAULT_SCROLL_BAR_WIDTH));
const parentElementStyle = _getComputedStyle(this._terminal.element.parentElement);
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
+14 -11
View File
@@ -5,16 +5,19 @@
import { IImageAddonOptions, IOscHandler, IResetHandler, ITerminalExt } from './Types';
import { ImageRenderer } from './ImageRenderer';
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
import Base64Decoder, { type DecodeStatus } from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
import Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
import { HeaderParser, IHeaderFields, HeaderState } from './IIPHeaderParser';
import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics';
// limit hold memory in base64 decoder (encoded bytes)
const KEEP_DATA = 4194304;
const INITIAL_DATA = 1048576;
// Local mirror of const enum (esbuild can't inline const enums from external packages)
const DECODER_OK: DecodeStatus.OK = 0;
// Local const enum mirror - esbuild can't inline const enums from external packages
const enum DecoderConst {
// Limit held memory in base64 decoder (encoded bytes).
KEEP_DATA = 4194304,
// Initial buffer allocation for the decoder.
INITIAL_DATA = 1048576,
// Local mirror of const enum (esbuild can't inline const enums from external packages)
OK = 0
}
// default IIP header values
const DEFAULT_HEADER: IHeaderFields = {
@@ -41,8 +44,8 @@ export class IIPHandler implements IOscHandler, IResetHandler {
private readonly _coreTerminal: ITerminalExt
) {
const maxEncodedBytes = Math.ceil(this._opts.iipSizeLimit * 4 / 3);
const initialBytes = Math.min(INITIAL_DATA, maxEncodedBytes);
this._dec = new Base64Decoder(KEEP_DATA, maxEncodedBytes, initialBytes);
const initialBytes = Math.min(DecoderConst.INITIAL_DATA, maxEncodedBytes);
this._dec = new Base64Decoder(DecoderConst.KEEP_DATA, maxEncodedBytes, initialBytes);
}
public reset(): void {}
@@ -58,7 +61,7 @@ export class IIPHandler implements IOscHandler, IResetHandler {
if (this._aborted) return;
if (this._hp.state === HeaderState.END) {
if (this._dec.put(data.subarray(start, end)) !== DECODER_OK) {
if ((this._dec.put(data.subarray(start, end)) as number) !== DecoderConst.OK) {
this._dec.release();
this._aborted = true;
}
@@ -75,7 +78,7 @@ export class IIPHandler implements IOscHandler, IResetHandler {
return;
}
this._dec.init();
if (this._dec.put(data.subarray(dataPos, end)) !== DECODER_OK) {
if ((this._dec.put(data.subarray(dataPos, end)) as number) !== DecoderConst.OK) {
this._dec.release();
this._aborted = true;
}
@@ -116,7 +116,7 @@ describe('SerializeAddon', () => {
describe('underline styles', () => {
it('should serialize single underline with style', async () => {
await writeP(terminal, sgr('4:1') + 'test' + sgr('24'));
assert.equal(serializeAddon.serialize(), '\u001b[4:1mtest\u001b[0m');
assert.equal(serializeAddon.serialize(), '\u001b[4mtest\u001b[0m');
});
it('should serialize double underline', async () => {
+40 -16
View File
@@ -89,11 +89,19 @@ function equalUnderline(cell1: IBufferCell | IAttributeData, cell2: IBufferCell)
if (!cell1.isUnderline() && !cell2.isUnderline()) {
return true;
}
const cell1Data = cell1 as unknown as IAttributeData;
const cell2Data = cell2 as unknown as IAttributeData;
return cell1Data.getUnderlineStyle() === cell2Data.getUnderlineStyle()
&& cell1Data.getUnderlineColor() === cell2Data.getUnderlineColor()
&& cell1Data.getUnderlineColorMode() === cell2Data.getUnderlineColorMode();
if (cell1.getUnderlineStyle() !== cell2.getUnderlineStyle()) {
return false;
}
const cell1Default = cell1.isUnderlineColorDefault();
const cell2Default = cell2.isUnderlineColorDefault();
if (cell1Default && cell2Default) {
return true;
}
if (cell1Default !== cell2Default) {
return false;
}
return cell1.getUnderlineColor() === cell2.getUnderlineColor()
&& cell1.getUnderlineColorMode() === cell2.getUnderlineColorMode();
}
function equalFlags(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
@@ -109,6 +117,16 @@ function equalFlags(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): bo
&& cell1.isStrikethrough() === cell2.isStrikethrough();
}
function attributesEquals(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
const cell1AsBufferCell = cell1 as IBufferCell;
if (typeof cell1AsBufferCell.attributesEquals === 'function') {
return cell1AsBufferCell.attributesEquals(cell2);
}
return equalFg(cell1, cell2)
&& equalBg(cell1, cell2)
&& equalFlags(cell1, cell2);
}
class StringSerializeHandler extends BaseSerializeHandler {
private _rowIndex: number = 0;
private _allRows: string[] = new Array<string>();
@@ -258,6 +276,9 @@ class StringSerializeHandler extends BaseSerializeHandler {
private _diffStyle(cell: IBufferCell | IAttributeData, oldCell: IBufferCell): number[] {
const sgrSeq: number[] = [];
if (attributesEquals(cell, oldCell)) {
return sgrSeq;
}
const fgChanged = !equalFg(cell, oldCell);
const bgChanged = !equalBg(cell, oldCell);
const flagsChanged = !equalFlags(cell, oldCell);
@@ -290,17 +311,18 @@ class StringSerializeHandler extends BaseSerializeHandler {
if (cell.isInverse() !== oldCell.isInverse()) { sgrSeq.push(cell.isInverse() ? 7 : 27); }
if (cell.isBold() !== oldCell.isBold()) { sgrSeq.push(cell.isBold() ? 1 : 22); }
if (!equalUnderline(cell, oldCell)) {
const cellData = cell as unknown as IAttributeData;
const style = cellData.getUnderlineStyle();
const style = cell.getUnderlineStyle();
if (style === UnderlineStyle.NONE) {
sgrSeq.push(24);
} else if (style === UnderlineStyle.SINGLE && cell.isUnderlineColorDefault()) {
sgrSeq.push(4);
} else {
// Use SGR 4:X format for underline styles
sgrSeq.push('4:' + style as unknown as number);
// Handle underline color
if (!cellData.isUnderlineColorDefault()) {
const color = cellData.getUnderlineColor();
if (cellData.isUnderlineColorRGB()) {
if (!cell.isUnderlineColorDefault()) {
const color = cell.getUnderlineColor();
if (cell.isUnderlineColorRGB()) {
sgrSeq.push('58:2::' + ((color >>> 16) & 0xFF) + ':' + ((color >>> 8) & 0xFF) + ':' + (color & 0xFF) as unknown as number);
} else {
sgrSeq.push('58:5:' + color as unknown as number);
@@ -675,12 +697,11 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
}
private _getUnderlineColor(cell: IBufferCell): string | undefined {
const cellData = cell as unknown as IAttributeData;
if (cellData.isUnderlineColorDefault()) {
if (cell.isUnderlineColorDefault()) {
return undefined;
}
const color = cellData.getUnderlineColor();
if (cellData.isUnderlineColorRGB()) {
const color = cell.getUnderlineColor();
if (cell.isUnderlineColorRGB()) {
const rgb = [
(color >> 16) & 255,
(color >> 8) & 255,
@@ -693,8 +714,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
}
private _getUnderlineStyle(cell: IBufferCell): string {
const cellData = cell as unknown as IAttributeData;
switch (cellData.getUnderlineStyle()) {
switch (cell.getUnderlineStyle()) {
case UnderlineStyle.SINGLE:
return 'underline';
case UnderlineStyle.DOUBLE:
@@ -713,6 +733,10 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): string[] | undefined {
const content: string[] = [];
if (attributesEquals(cell, oldCell)) {
return undefined;
}
const fgChanged = !equalFg(cell, oldCell);
const bgChanged = !equalBg(cell, oldCell);
const flagsChanged = !equalFlags(cell, oldCell);
@@ -203,6 +203,46 @@ test.describe('SerializeAddon', () => {
strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n'));
});
test('buffer cell attributesEquals compares underline style and color', async () => {
await ctx.proxy.write(`${sgr(UNDERLINE_DOUBLE, UNDERLINE_COLOR_RED)}A${sgr(UNDERLINE_DOUBLE, UNDERLINE_COLOR_RED)}B${sgr(NORMAL)}`);
const sameAttributes = await ctx.page.evaluate(`(() => {
const line = window.term.buffer.active.getLine(0);
const cellA = line?.getCell(0);
const cellB = line?.getCell(1);
if (!cellA || !cellB) {
return undefined;
}
return cellA.attributesEquals(cellB);
})()`);
strictEqual(sameAttributes, true);
await ctx.page.evaluate(`window.term.reset()`);
await ctx.proxy.write(`${sgr(UNDERLINE_DOUBLE, UNDERLINE_COLOR_RED)}A${sgr(UNDERLINE_DOUBLE, UNDERLINE_COLOR_GREEN)}B${sgr(NORMAL)}`);
const differentColor = await ctx.page.evaluate(`(() => {
const line = window.term.buffer.active.getLine(0);
const cellA = line?.getCell(0);
const cellB = line?.getCell(1);
if (!cellA || !cellB) {
return undefined;
}
return cellA.attributesEquals(cellB);
})()`);
strictEqual(differentColor, false);
await ctx.page.evaluate(`window.term.reset()`);
await ctx.proxy.write(`${sgr(UNDERLINE_DOUBLE, UNDERLINE_COLOR_RED)}A${sgr(UNDERLINED, UNDERLINE_COLOR_RED)}B${sgr(NORMAL)}`);
const differentStyle = await ctx.page.evaluate(`(() => {
const line = window.term.buffer.active.getLine(0);
const cellA = line?.getCell(0);
const cellB = line?.getCell(1);
if (!cellA || !cellB) {
return undefined;
}
return cellA.attributesEquals(cellB);
})()`);
strictEqual(differentStyle, false);
});
test('serialize all rows of content with color256', async function(): Promise<any> {
const rows = 32;
const cols = 10;
@@ -602,6 +642,9 @@ const BOLD = '1';
const DIM = '2';
const ITALIC = '3';
const UNDERLINED = '4';
const UNDERLINE_DOUBLE = '4:2';
const UNDERLINE_COLOR_RED = '58;5;196';
const UNDERLINE_COLOR_GREEN = '58;5;46';
const BLINK = '5';
const INVERSE = '7';
const INVISIBLE = '8';
+5 -2
View File
@@ -7,6 +7,7 @@ import { ICellData } from 'common/Types';
import { Terminal } from '@xterm/xterm';
import { rgba } from 'common/Color';
import { treatGlyphAsBackgroundColor } from 'browser/renderer/shared/RendererUtils';
import { blockPatternCodepoints } from './customGlyphs/CustomGlyphDefinitions';
// Work variables to avoid garbage collection
let $fg = 0;
@@ -42,7 +43,7 @@ export class CellColorResolver {
* Resolves colors for the cell, putting the result into the shared {@link result}. This resolves
* overrides, inverse and selection for the cell which can then be used to feed into the renderer.
*/
public resolve(cell: ICellData, x: number, y: number, deviceCellWidth: number): void {
public resolve(cell: ICellData, x: number, y: number, deviceCellWidth: number, deviceCellHeight: number): void {
this.result.bg = cell.bg;
this.result.fg = cell.fg;
this.result.ext = cell.bg & BgFlags.HAS_EXTENDED ? cell.extended.ext : 0;
@@ -63,7 +64,9 @@ export class CellColorResolver {
const lineWidth = Math.max(1, Math.floor(this._optionService.rawOptions.fontSize * this._coreBrowserService.dpr / 15));
$variantOffset = x * deviceCellWidth % (Math.round(lineWidth) * 2);
}
if ($variantOffset === 0 && blockPatternCodepoints.has(code)) {
$variantOffset = ((x * deviceCellWidth) % 2) * 2 + ((y * deviceCellHeight) % 2);
}
// Apply decorations on the bottom layer
this._decorationService.forEachDecorationAtCell(x, y, 'bottom', d => {
if (d.backgroundColorRGB) {
+3 -1
View File
@@ -8,6 +8,7 @@ import { ITerminalOptions, Terminal } from '@xterm/xterm';
import { ITerminal, ReadonlyColorSet } from 'browser/Types';
import { ICharAtlasConfig, ITextureAtlas } from './Types';
import { generateConfig, configEquals } from './CharAtlasUtils';
import type { ILogService } from 'common/services/Services';
interface ITextureAtlasCacheEntry {
atlas: ITextureAtlas;
@@ -67,8 +68,9 @@ export function acquireTextureAtlas(
}
const core: ITerminal = (terminal as any)._core;
const logService = (core as any)._logService as ILogService;
const newEntry: ITextureAtlasCacheEntry = {
atlas: new TextureAtlas(document, newConfig, core.unicodeService),
atlas: new TextureAtlas(document, newConfig, core.unicodeService, logService),
config: newConfig,
ownedBy: [terminal]
};
+6 -4
View File
@@ -14,7 +14,7 @@ import { IdleTaskQueue } from 'common/TaskQueue';
import { IColor } from 'common/Types';
import { AttributeData } from 'common/buffer/AttributeData';
import { Attributes, DEFAULT_COLOR, DEFAULT_EXT, UnderlineStyle } from 'common/buffer/Constants';
import { IUnicodeService } from 'common/services/Services';
import { ILogService, IUnicodeService } from 'common/services/Services';
import { Emitter } from 'common/Event';
/**
@@ -88,7 +88,8 @@ export class TextureAtlas implements ITextureAtlas {
constructor(
private readonly _document: Document,
private readonly _config: ICharAtlasConfig,
private readonly _unicodeService: IUnicodeService
private readonly _unicodeService: IUnicodeService,
private readonly _logService: ILogService
) {
this._createNewPage();
this._tmpCanvas = createCanvas(
@@ -119,7 +120,7 @@ export class TextureAtlas implements ITextureAtlas {
private _doWarmUp(): void {
// Pre-fill with ASCII 33-126, this is not urgent and done in idle callbacks
const queue = new IdleTaskQueue();
const queue = new IdleTaskQueue(this._logService);
for (let i = 33; i < 126; i++) {
queue.enqueue(() => {
if (!this._cacheMap.get(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT)) {
@@ -525,7 +526,8 @@ export class TextureAtlas implements ITextureAtlas {
// Draw custom characters if applicable
let customGlyph = false;
if (this._config.customGlyphs !== false) {
customGlyph = tryDrawCustomGlyph(this._tmpCtx, chars, padding, padding, this._config.deviceCellWidth, this._config.deviceCellHeight, this._config.deviceCharWidth, this._config.deviceCharHeight, this._config.fontSize, this._config.devicePixelRatio, backgroundColor.css);
const variantOffset = this._workAttributeData.getUnderlineVariantOffset();
customGlyph = tryDrawCustomGlyph(this._tmpCtx, chars, padding, padding, this._config.deviceCellWidth, this._config.deviceCellHeight, this._config.deviceCharWidth, this._config.deviceCharHeight, this._config.fontSize, this._config.devicePixelRatio, backgroundColor.css, variantOffset);
}
// Whether to clear pixels based on a threshold difference between the glyph color and the
+1 -1
View File
@@ -500,7 +500,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
}
// Load colors/resolve overrides into work colors
this._cellColorResolver.resolve(cell, x, row, this.dimensions.device.cell.width);
this._cellColorResolver.resolve(cell, x, row, this.dimensions.device.cell.width, this.dimensions.device.cell.height);
// Override colors for cursor cell
if (isCursorVisible && row === cursorY) {
@@ -644,8 +644,8 @@ export const customGlyphDefinitions: { [index: string]: CustomGlyphCharacterDefi
] },
// Diagonal fill characters (1FB98-1FB99)
'\u{1FB98}': { type: CustomGlyphDefinitionType.PATH_FUNCTION, data: 'M0,0 L1,1 M0,.25 L.75,1 M0,.5 L.5,1 M0,.75 L.25,1 M.25,0 L1,.75 M.5,0 L1,.5 M.75,0 L1,.25', strokeWidth: 1 }, // UPPER LEFT TO LOWER RIGHT FILL
'\u{1FB99}': { type: CustomGlyphDefinitionType.PATH_FUNCTION, data: 'M0,.25 L.25,0 M0,.5 L.5,0 M0,.75 L.75,0 M0,1 L1,0 M.25,1 L1,.25 M.5,1 L1,.5 M.75,1 L1,.75', strokeWidth: 1 }, // UPPER RIGHT TO LOWER LEFT FILL
'\u{1FB98}': { type: CustomGlyphDefinitionType.PATH_FUNCTION, data: 'M-0.25,-0.25 L1.25,1.25 M-0.25,0 L1,1.25 M-0.25,0.25 L0.75,1.25 M-0.25,0.5 L0.5,1.25 M0,-0.25 L1.25,1 M0.25,-0.25 L1.25,0.75 M0.5,-0.25 L1.25,0.5 M-0.25,0.75 L0.25,1.25 M0.75,-0.25 L1.25,0.25', strokeWidth: 1 }, // UPPER LEFT TO LOWER RIGHT FILL
'\u{1FB99}': { type: CustomGlyphDefinitionType.PATH_FUNCTION, data: 'M-0.25,0.5 L0.5,-0.25 M-0.25,0.75 L0.75,-0.25 M-0.25,1 L1,-0.25 M-0.25,1.25 L1.25,-0.25 M0,1.25 L1.25,0 M0.25,1.25 L1.25,0.25 M0.5,1.25 L1.25,0.5 M-0.25,0.25 L0.25,-0.25 M0.75,1.25 L1.25,0.75', strokeWidth: 1 }, // UPPER RIGHT TO LOWER LEFT FILL
// Smooth mosaic terminal graphic characters (1FB9A-1FB9B)
'\u{1FB9A}': { type: CustomGlyphDefinitionType.VECTOR_SHAPE, data: { d: 'M0,0 L.5,.5 L0,1 L1,1 L.5,.5 L1,0', type: CustomGlyphVectorType.FILL } }, // UPPER AND LOWER TRIANGULAR HALF BLOCK
@@ -820,6 +820,27 @@ export const customGlyphDefinitions: { [index: string]: CustomGlyphCharacterDefi
// #endregion
};
export const blockPatternCodepoints = new Set<number>([
// Shade characters (2591-2593)
0x2591,
0x2592,
0x2593,
// Rectangular shade characters (1FB8C-1FB94)
0x1FB8C,
0x1FB8D,
0x1FB8E,
0x1FB8F,
0x1FB90,
0x1FB91,
0x1FB92,
0x1FB94,
// Triangular shade characters (1FB9C-1FB9F)
0x1FB9C,
0x1FB9D,
0x1FB9E,
0x1FB9F
]);
/**
* Generates a drawing function for sextant characters. Sextants are a 2x3 grid where each cell
* can be on or off.
@@ -22,14 +22,15 @@ export function tryDrawCustomGlyph(
deviceCharHeight: number,
fontSize: number,
devicePixelRatio: number,
backgroundColor?: string
backgroundColor?: string,
variantOffset: number = 0
): boolean {
const unifiedCharDefinition = customGlyphDefinitions[c];
if (unifiedCharDefinition) {
// Normalize to array for uniform handling
const parts = Array.isArray(unifiedCharDefinition) ? unifiedCharDefinition : [unifiedCharDefinition];
for (const part of parts) {
drawDefinitionPart(ctx, part, xOffset, yOffset, deviceCellWidth, deviceCellHeight, deviceCharWidth, deviceCharHeight, fontSize, devicePixelRatio, backgroundColor);
drawDefinitionPart(ctx, part, xOffset, yOffset, deviceCellWidth, deviceCellHeight, deviceCharWidth, deviceCharHeight, fontSize, devicePixelRatio, backgroundColor, variantOffset);
}
return true;
}
@@ -48,7 +49,8 @@ function drawDefinitionPart(
deviceCharHeight: number,
fontSize: number,
devicePixelRatio: number,
backgroundColor?: string
backgroundColor?: string,
variantOffset: number = 0
): void {
// Handle scaleType - adjust dimensions and offset when scaling to character area
let drawWidth = deviceCellWidth;
@@ -74,7 +76,7 @@ function drawDefinitionPart(
drawBlockVectorChar(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight);
break;
case CustomGlyphDefinitionType.BLOCK_PATTERN:
drawPatternChar(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight);
drawPatternChar(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight, variantOffset);
break;
case CustomGlyphDefinitionType.PATH_FUNCTION:
drawPathFunctionCharacter(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight, devicePixelRatio, part.strokeWidth);
@@ -437,7 +439,8 @@ function drawPatternChar(
xOffset: number,
yOffset: number,
deviceCellWidth: number,
deviceCellHeight: number
deviceCellHeight: number,
variantOffset: number = 0
): void {
let patternSet = cachedPatterns.get(charDefinition);
if (!patternSet) {
@@ -486,6 +489,15 @@ function drawPatternChar(
pattern = throwIfFalsy(ctx.createPattern(tmpCanvas, null));
patternSet.set(fillStyle, pattern);
}
// Apply pattern offset to ensure seamless tiling across cells when cell dimensions are odd.
// variantOffset encodes: bit 1 = x pixel shift, bit 0 = y pixel shift.
const dx = (variantOffset >> 1) & 1;
const dy = variantOffset & 1;
if (dx !== 0 || dy !== 0) {
pattern.setTransform(new DOMMatrix().translateSelf(-dx, -dy));
} else {
pattern.setTransform(new DOMMatrix());
}
ctx.fillStyle = pattern;
ctx.fillRect(xOffset, yOffset, deviceCellWidth, deviceCellHeight);
}
@@ -500,6 +512,11 @@ function drawPathFunctionCharacter(
devicePixelRatio: number,
strokeWidth?: number
): void {
ctx.save();
ctx.beginPath();
ctx.rect(xOffset, yOffset, deviceCellWidth, deviceCellHeight);
ctx.clip();
ctx.beginPath();
let actualInstructions: string;
if (typeof charDefinition === 'function') {
@@ -526,7 +543,7 @@ function drawPathFunctionCharacter(
if (!args[0] || !args[1]) {
continue;
}
f(ctx, translateArgs(args, deviceCellWidth, deviceCellHeight, xOffset, yOffset, true, devicePixelRatio), state);
f(ctx, translateArgs(args, deviceCellWidth, deviceCellHeight, xOffset, yOffset, true, devicePixelRatio, 0, 0, false), state);
state.lastCommand = type;
}
if (strokeWidth !== undefined) {
@@ -537,6 +554,7 @@ function drawPathFunctionCharacter(
ctx.fill();
}
ctx.closePath();
ctx.restore();
}
/**
@@ -685,7 +703,7 @@ const svgToCanvasInstructionMap: { [index: string]: (ctx: CanvasRenderingContext
}
};
function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number, doClamp: boolean, devicePixelRatio: number, leftPadding: number = 0, rightPadding: number = 0): number[] {
function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number, doClamp: boolean, devicePixelRatio: number, leftPadding: number = 0, rightPadding: number = 0, clampToCell: boolean = true): number[] {
const result = args.map(e => parseFloat(e) || parseInt(e));
if (result.length < 2) {
@@ -695,10 +713,11 @@ function translateArgs(args: string[], cellWidth: number, cellHeight: number, xO
for (let x = 0; x < result.length; x += 2) {
// Translate from 0-1 to 0-cellWidth
result[x] *= cellWidth - (leftPadding * devicePixelRatio) - (rightPadding * devicePixelRatio);
// Ensure coordinate doesn't escape cell bounds and round to the nearest 0.5 to ensure a crisp
// line at 100% devicePixelRatio
// Round to the nearest 0.5 to ensure a crisp line at 100% devicePixelRatio, and optionally
// clamp to the cell bounds.
if (doClamp && result[x] !== 0) {
result[x] = clamp(Math.round(result[x] + 0.5) - 0.5, cellWidth, 0);
const rounded = Math.round(result[x] + 0.5) - 0.5;
result[x] = clampToCell ? clamp(rounded, cellWidth, 0) : rounded;
}
// Apply the cell's offset (ie. x*cellWidth)
result[x] += xOffset + (leftPadding * devicePixelRatio);
@@ -707,10 +726,11 @@ function translateArgs(args: string[], cellWidth: number, cellHeight: number, xO
for (let y = 1; y < result.length; y += 2) {
// Translate from 0-1 to 0-cellHeight
result[y] *= cellHeight;
// Ensure coordinate doesn't escape cell bounds and round to the nearest 0.5 to ensure a crisp
// line at 100% devicePixelRatio
// Round to the nearest 0.5 to ensure a crisp line at 100% devicePixelRatio, and optionally
// clamp to the cell bounds.
if (doClamp && result[y] !== 0) {
result[y] = clamp(Math.round(result[y] + 0.5) - 0.5, cellHeight, 0);
const rounded = Math.round(result[y] + 0.5) - 0.5;
result[y] = clampToCell ? clamp(rounded, cellHeight, 0) : rounded;
}
// Apply the cell's offset (ie. x*cellHeight)
result[y] += yOffset;