Port NiimBlue label designer to Fichero D11s with local BLE protocol library

This commit is contained in:
Hamza
2026-03-01 22:47:16 +01:00
parent b1ff403594
commit 774b02bb99
102 changed files with 16333 additions and 951 deletions

View File

@@ -0,0 +1,287 @@
import * as fabric from "fabric";
import { code128b, ean13 } from "$/utils/barcode";
import { CanvasUtils } from "$/utils/canvas_utils";
import { OBJECT_DEFAULTS_TEXT } from "$/defaults";
const EAN13_LONG_BAR_INDEXES: number[] = [
0, 1, 2, 45, 46, 47, 48, 49, 92, 93, 94,
];
export type BarcodeCoding = "EAN13" | "CODE128B";
export const barcodeDefaultValues: Partial<fabric.TClassProperties<Barcode>> = {
text: "",
encoding: "EAN13",
printText: true,
scaleFactor: 1,
fontSize: 12,
fontFamily: OBJECT_DEFAULTS_TEXT.fontFamily,
};
interface UniqueBarcodeProps {
text: string;
encoding: BarcodeCoding;
printText: boolean;
scaleFactor: number;
fontSize: number;
fontFamily: string;
}
export interface BarcodeProps
extends fabric.FabricObjectProps,
UniqueBarcodeProps {}
export interface SerializedBarcodeProps
extends fabric.SerializedObjectProps,
UniqueBarcodeProps {}
const BARCODE_PROPS = [
"text",
"encoding",
"printText",
"scaleFactor",
"fontSize",
"fontFamily",
] as const;
export class Barcode<
Props extends fabric.TOptions<BarcodeProps> = Partial<BarcodeProps>,
SProps extends SerializedBarcodeProps = SerializedBarcodeProps,
EventSpec extends fabric.ObjectEvents = fabric.ObjectEvents,
>
extends fabric.FabricObject<Props, SProps, EventSpec>
implements BarcodeProps
{
static override type = "Barcode";
/**
* Barcode text
* @type string
* @default ""
*/
declare text: string;
/**
* Barcode encoding
* @type BarcodeCoding
* @default "EAN13"
*/
declare encoding: BarcodeCoding;
/**
* Print text
* @type boolean
* @default true
*/
declare printText: boolean;
/**
* Scale factor
* @type number
* @default 1
*/
declare scaleFactor: number;
/**
* Font size
* @type number
* @default 12
*/
declare fontSize: number;
/**
* Font family
* @type string
* @default "Noto Sans Variable"
*/
declare fontFamily: string;
private barcodeEncoded: string = "";
private displayText: string = "";
constructor(options?: Props) {
super();
Object.assign(this, barcodeDefaultValues);
const { text, ...other } = options ?? {};
this.setOptions(other); // Must be set separately because the encoding needs to be set first
this.set("text", text);
this.setControlsVisibility({
tl: false,
tr: false,
bl: false,
br: false,
ml: false,
mr: false,
mtr: false,
});
this.objectCaching = false;
this._createBandCode();
}
override _set(key: string, value?: any): this {
super._set(key, value);
if (key === "text" || key == "encoding") {
this._createBandCode();
}
if (
this.barcodeEncoded &&
(BARCODE_PROPS.includes(key as any) || key == "canvas")
) {
const letterWidth = this._measureLetterWidth();
let barcodeWidth = (this.scaleFactor ?? 1) * this.barcodeEncoded.length;
if (this.encoding === "EAN13") {
barcodeWidth += letterWidth * 2; // side margins
}
super.set("width", barcodeWidth);
this.setCoords();
}
return this;
}
_createBandCode() {
if (this.encoding === "EAN13") {
const { text, bandcode } = ean13(this.text);
this.displayText = text;
this.barcodeEncoded = bandcode;
} else {
this.displayText = this.text;
this.barcodeEncoded = code128b(this.text);
}
}
_getFont(): string {
return `bold ${this.fontSize}px ${this.fontFamily}`;
}
// parent canvas is needed for this operation
_measureLetterWidth(): number {
const ctx = this.canvas?.getContext();
let w = 0;
if (ctx !== undefined) {
ctx.save();
ctx.font = this._getFont();
w = ctx.measureText("0").width;
ctx.restore();
}
return Math.ceil(w);
}
override _render(ctx: CanvasRenderingContext2D) {
if (this.barcodeEncoded === "") {
super._render(ctx);
return;
}
const letterWidth = this._measureLetterWidth();
ctx.save();
ctx.translate(-this.width / 2, -this.height / 2); // make top-left origin
ctx.translate(0.5, 0.5); // blurry rendering fix
ctx.font = this._getFont();
ctx.textBaseline = "bottom";
const longBarHeight = this.height;
let shortBarHeight = this.height;
const barcodeStartPos = this.encoding === "EAN13" ? letterWidth : 0;
if (this.printText) {
shortBarHeight -= this.fontSize * 1.2;
} else if (this.encoding === "EAN13") {
shortBarHeight -= 8;
}
let blackStartPosition = -1;
let blackCount = 0;
let isLongBar = false;
// render barcode
for (let i = 0; i < this.barcodeEncoded.length; i++) {
const isBlack = this.barcodeEncoded[i] === "1";
const xPos = barcodeStartPos + i * this.scaleFactor;
if (isBlack) {
blackCount++;
if (blackStartPosition == -1) {
blackStartPosition = xPos;
}
if (this.encoding === "EAN13" && EAN13_LONG_BAR_INDEXES.includes(i)) {
isLongBar = true;
}
if (blackStartPosition != -1 && i === this.barcodeEncoded.length - 1) {
// last index
ctx.fillRect(
blackStartPosition,
0,
this.scaleFactor * blackCount,
isLongBar ? longBarHeight : shortBarHeight,
);
}
} else {
ctx.fillRect(
blackStartPosition,
0,
this.scaleFactor * blackCount,
isLongBar ? longBarHeight : shortBarHeight,
);
blackStartPosition = -1;
blackCount = 0;
isLongBar = false;
}
}
// render text
if (this.printText) {
if (this.encoding === "EAN13") {
const parts = [
this.displayText[0],
this.displayText.slice(1, 7),
this.displayText.slice(7, 13),
">",
];
const midPartWidth = 40;
const longBars1End = 4;
const longBars2End = 50;
ctx.fillText(parts[0], 0, this.height); // first digit
CanvasUtils.equalSpacingFillText(
ctx,
parts[1],
letterWidth + longBars1End * this.scaleFactor,
this.height,
midPartWidth * this.scaleFactor,
); // part 1
CanvasUtils.equalSpacingFillText(
ctx,
parts[2],
letterWidth + longBars2End * this.scaleFactor,
this.height,
midPartWidth * this.scaleFactor,
); // part 2
ctx.fillText(parts[3], this.width - letterWidth, this.height); // last digit
} else {
CanvasUtils.equalSpacingFillText(
ctx,
this.displayText,
barcodeStartPos,
this.height,
this.width,
);
}
}
ctx.restore();
super._render(ctx);
}
override toObject(propertiesToInclude: any[] = []) {
return super.toObject([...BARCODE_PROPS, ...propertiesToInclude]);
}
}
fabric.classRegistry.setClass(Barcode, "Barcode");
export default Barcode;

View File

@@ -0,0 +1,458 @@
import * as fabric from "fabric";
import { DEFAULT_LABEL_PROPS } from "$/defaults";
import type { LabelProps } from "$/types";
type LabelBounds = {
startX: number;
startY: number;
endX: number;
endY: number;
width: number;
height: number;
};
type FoldSegment = { start: number; end: number };
type FoldInfo = {
axis: "vertical" | "horizontal" | "none";
points: number[];
segments: FoldSegment[];
};
type MirrorInfo = { pos: fabric.Point; flip: boolean };
export class CustomCanvas extends fabric.Canvas {
private labelProps: LabelProps = DEFAULT_LABEL_PROPS;
private readonly SEPARATOR_LINE_WIDTH = 2;
private readonly ROUND_RADIUS = 10;
private readonly TAIL_WIDTH = 40;
private readonly GRAY = "#CFCFCF";
private readonly MIRROR_GHOST_COLOR = "rgba(0, 0, 0, 0.3)";
private customBackground: boolean = true;
private highlightMirror: boolean = true;
private virtualZoomRatio: number = 1;
constructor(
el?: string | HTMLCanvasElement,
options?: fabric.TOptions<fabric.CanvasOptions>,
) {
super(el, options);
this.setupZoom();
this.preserveObjectStacking = true;
}
private setupZoom() {
this.on("mouse:wheel", (opt) => {
const event = opt.e as WheelEvent;
event.preventDefault();
const delta = event.deltaY;
if (delta > 0) {
this.virtualZoomOut();
} else {
this.virtualZoomIn();
}
});
this.on("mouse:down:before", (opt) => {
const event = opt.e as MouseEvent;
if (event.button == 1) {
event.preventDefault();
this.resetVirtualZoom();
}
});
}
public virtualZoom(newZoom: number) {
this.virtualZoomRatio = Math.min(Math.max(0.25, newZoom), 4);
this.setDimensions(
{
width: this.virtualZoomRatio * this.getWidth() + "px",
height: this.virtualZoomRatio * this.getHeight() + "px",
},
{ cssOnly: true },
);
}
public virtualZoomIn() {
this.virtualZoom(this.virtualZoomRatio * 1.05);
}
public virtualZoomOut() {
this.virtualZoom(this.virtualZoomRatio * 0.95);
}
public getVirtualZoom(): number {
return this.virtualZoomRatio;
}
public resetVirtualZoom() {
this.virtualZoom(1);
}
setLabelProps(value: LabelProps) {
this.labelProps = value;
this.requestRenderAll();
}
setCustomBackground(value: boolean) {
this.customBackground = value;
}
setHighlightMirror(value: boolean) {
this.highlightMirror = value;
}
/** Get label bounds without tail */
getLabelBounds(): LabelBounds {
let endX = this.width ?? 1;
let endY = this.height ?? 1;
let startX = 0;
let startY = 0;
if (this.labelProps.tailPos === "right") {
endX -= this.labelProps.tailLength ?? 0;
} else if (this.labelProps.tailPos === "bottom") {
endY -= this.labelProps.tailLength ?? 0;
} else if (this.labelProps.tailPos === "left") {
startX += this.labelProps.tailLength ?? 0;
} else if (this.labelProps.tailPos === "top") {
startY += this.labelProps.tailLength ?? 0;
}
const width = endX - startX;
const height = endY - startY;
return { startX, startY, endX, endY, width, height };
}
/** Get fold line position for splitted labels */
getFoldInfo(): FoldInfo {
const bb = this.getLabelBounds();
const points: number[] = [];
const segments: FoldSegment[] = [];
const splitParts = this.labelProps.splitParts ?? 2;
if (splitParts < 2) {
return { axis: "none", points, segments };
}
if (this.labelProps.split === "horizontal") {
const segmentHeight = bb.height / splitParts;
let lastY: number = bb.startY;
for (let i = 1; i < splitParts; i++) {
const y =
bb.startY + segmentHeight * i - this.SEPARATOR_LINE_WIDTH / 2 + 1;
points.push(y);
segments.push({ start: lastY, end: y });
lastY = y;
}
segments.push({ start: lastY, end: bb.endY });
return { axis: "horizontal", points, segments };
} else if (this.labelProps.split === "vertical") {
const segmentWidth = bb.width / splitParts;
let lastX: number = bb.startX;
for (let i = 1; i < splitParts; i++) {
const x =
bb.startX + segmentWidth * i - this.SEPARATOR_LINE_WIDTH / 2 + 1;
points.push(x);
segments.push({ start: lastX, end: x });
lastX = x;
}
segments.push({ start: lastX, end: bb.endX });
return { axis: "vertical", points, segments };
}
return { axis: "none", points, segments };
}
override _renderBackground(ctx: CanvasRenderingContext2D) {
if (this.width === undefined || this.height === undefined) {
return;
}
ctx.save();
ctx.fillStyle = "white";
// Draw simple white background and exit
if (!this.customBackground) {
ctx.fillRect(0, 0, this.width, this.height);
ctx.restore();
return;
}
// Disable further actions for circle labels, just render
if (this.labelProps.shape === "circle") {
ctx.beginPath();
ctx.arc(this.width / 2, this.height / 2, this.height / 2, 0, 2 * Math.PI);
ctx.fill();
ctx.restore();
return;
}
let roundRadius = this.ROUND_RADIUS;
const bb = this.getLabelBounds();
const fold = this.getFoldInfo();
if (this.labelProps.shape !== "rounded_rect") {
roundRadius = 0;
}
// Draw tail
ctx.fillStyle = this.GRAY;
ctx.beginPath();
if (
this.labelProps.tailLength !== undefined &&
this.labelProps.tailLength > 0
) {
if (this.labelProps.tailPos === "right") {
ctx.rect(
bb.endX - roundRadius,
bb.endY / 2 - this.TAIL_WIDTH / 2,
this.width - bb.endX + roundRadius,
this.TAIL_WIDTH,
);
} else if (this.labelProps.tailPos === "bottom") {
ctx.rect(
bb.endX / 2 - this.TAIL_WIDTH / 2,
bb.endY - roundRadius,
this.TAIL_WIDTH,
this.height - bb.endY + roundRadius,
);
} else if (this.labelProps.tailPos === "left") {
ctx.rect(
0,
bb.endY / 2 - this.TAIL_WIDTH / 2,
bb.startX + roundRadius,
this.TAIL_WIDTH,
);
} else if (this.labelProps.tailPos === "top") {
ctx.rect(
bb.endX / 2 - this.TAIL_WIDTH / 2,
0,
this.TAIL_WIDTH,
bb.startY + roundRadius,
);
}
}
ctx.fill();
// Draw label(s)
ctx.fillStyle = "white";
ctx.beginPath();
const splitParts = this.labelProps.splitParts ?? 2;
if (this.labelProps.shape === "rounded_rect") {
if (this.labelProps.split === "horizontal") {
const segmentHeight = bb.height / splitParts;
ctx.roundRect(
bb.startX,
bb.startY,
bb.width,
segmentHeight,
roundRadius,
); // First part
fold.points.forEach((y) =>
ctx.roundRect(bb.startX, y, bb.width, segmentHeight, roundRadius),
); // Other parts
} else if (this.labelProps.split === "vertical") {
const segmentWidth = bb.width / splitParts;
ctx.roundRect(
bb.startX,
bb.startY,
segmentWidth,
bb.height,
roundRadius,
); // First part
fold.points.forEach((x) =>
ctx.roundRect(x, bb.startY, segmentWidth, bb.height, roundRadius),
); // Other parts
} else {
ctx.roundRect(0, 0, this.width, this.height, roundRadius);
}
} else {
ctx.rect(bb.startX, bb.startY, bb.width, bb.height);
}
ctx.fill();
// Draw separator
ctx.strokeStyle = this.GRAY;
ctx.lineWidth = this.SEPARATOR_LINE_WIDTH;
ctx.setLineDash([8, 8]);
ctx.beginPath();
if (fold.axis === "horizontal") {
fold.points.forEach((x) => {
ctx.moveTo(bb.startX + roundRadius, x);
ctx.lineTo(bb.endX - roundRadius, x);
});
} else if (fold.axis === "vertical") {
fold.points.forEach((y) => {
ctx.moveTo(y, bb.startY + roundRadius);
ctx.lineTo(y, bb.endY - roundRadius);
});
}
ctx.stroke();
ctx.restore();
}
override _renderObjects(
ctx: CanvasRenderingContext2D,
objects: fabric.FabricObject[],
) {
super._renderObjects(ctx, objects);
if (!this.highlightMirror || this.getActiveObjects().length > 1) {
return;
}
ctx.save();
objects.forEach((obj) => {
const infos = this.getMirroredObjectCoords(obj);
infos.forEach((info) => {
const bbox = obj.getBoundingRect();
ctx.fillStyle = this.MIRROR_GHOST_COLOR;
ctx.fillRect(
info.pos.x - bbox.width / 2,
info.pos.y - bbox.height / 2,
bbox.width,
bbox.height,
);
ctx.restore();
});
});
ctx.restore();
}
/**
* Return new object positions (origin is center) if object needs mirroring
**/
getMirroredObjectCoords(obj: fabric.FabricObject): MirrorInfo[] {
const fold = this.getFoldInfo();
const result: MirrorInfo[] = [];
if (
fold.axis === "none" ||
!(this.labelProps.mirror === "flip" || this.labelProps.mirror === "copy")
) {
return result;
}
const bounds = this.getLabelBounds();
if (fold.axis === "vertical") {
if (this.labelProps.mirror === "copy") {
fold.points.forEach((x) => {
const pos = obj.getPointByOrigin("center", "center");
pos.setX(x + (pos.x - bounds.startX));
result.push({ pos, flip: false });
});
} else if (
this.labelProps.mirror === "flip" &&
fold.points.length === 1
) {
// Half split only supported
const axisX = fold.points[0];
const pos = obj.getPointByOrigin("center", "center");
pos.setX(axisX + (axisX - pos.x));
pos.setY(bounds.startY + bounds.endY - pos.y);
result.push({ pos, flip: true });
}
} else if (fold.axis === "horizontal") {
if (this.labelProps.mirror === "copy") {
fold.points.forEach((y) => {
const pos = obj.getPointByOrigin("center", "center");
pos.setY(y + (pos.y - bounds.startY));
result.push({ pos, flip: false });
});
} else if (
this.labelProps.mirror === "flip" &&
fold.points.length === 1
) {
// Half split only supported
const axisY = fold.points[0];
const pos = obj.getPointByOrigin("center", "center");
pos.setY(axisY + (axisY - pos.y));
pos.setX(bounds.startX + bounds.endX - pos.x);
result.push({ pos, flip: true });
}
}
return result;
}
/** Clone mirrored objects and add them to canvas */
async createMirroredObjects() {
const objects = this.getObjects();
for (const obj of objects) {
const infos = this.getMirroredObjectCoords(obj);
for (const info of infos) {
const newObj = await obj.clone();
newObj.setPositionByOrigin(info.pos, "center", "center");
if (info.flip) {
newObj.centeredRotation = true;
newObj.rotate((newObj.angle + 180) % 360);
}
this.add(newObj);
}
}
}
/** Centers object horizontally in the canvas or label part */
override centerObjectH(object: fabric.FabricObject): void {
if ((this.labelProps.split ?? "none") !== "none") {
const pos = object.getPointByOrigin("center", "center");
const bounds = this.getLabelBounds();
const fold = this.getFoldInfo();
let centerX = bounds.startX + bounds.width / 2;
if (fold.axis !== "horizontal") {
fold.segments.forEach((seg) => {
if (pos.x >= seg.start && pos.x <= seg.end) {
centerX = seg.start + (seg.end - seg.start) / 2;
}
});
}
pos.setX(centerX);
object.setPositionByOrigin(pos, "center", "center");
return;
}
super.centerObjectH(object);
}
/** Centers object vertically in the canvas or label part */
override centerObjectV(object: fabric.FabricObject): void {
if ((this.labelProps.split ?? "none") !== "none") {
const pos = object.getPointByOrigin("center", "center");
const bounds = this.getLabelBounds();
const fold = this.getFoldInfo();
let centerY = bounds.startY + bounds.height / 2;
if (fold.axis !== "vertical") {
fold.segments.forEach((seg) => {
if (pos.y >= seg.start && pos.y <= seg.end) {
centerY = seg.start + (seg.end - seg.start) / 2;
}
});
}
pos.setY(centerY);
object.setPositionByOrigin(pos, "center", "center");
return;
}
super.centerObjectV(object);
}
}

View File

@@ -0,0 +1,159 @@
import QRCodeFactory from "qrcode-generator";
import * as fabric from "fabric";
import { OBJECT_DEFAULTS_TEXT, OBJECT_SIZE_DEFAULTS } from "$/defaults";
import { Range } from "$/types";
export type ErrorCorrectionLevel = "L" | "M" | "Q" | "H";
export type Mode = "Numeric" | "Alphanumeric" | "Byte" /* Default */ | "Kanji";
export type QrVersion = Range<41>; // 0-40, 0 is automatic
export const qrCodeDefaultValues: Partial<fabric.TClassProperties<QRCode>> = {
text: "Text",
ecl: "M",
stroke: "#000000",
fill: "#ffffff",
mode: "Byte",
qrVersion: 0,
...OBJECT_SIZE_DEFAULTS,
};
interface UniqueQRCodeProps {
text: string;
ecl: ErrorCorrectionLevel;
mode: Mode;
qrVersion: QrVersion;
}
export interface QRCodeProps
extends fabric.FabricObjectProps,
UniqueQRCodeProps {}
export interface SerializedQRCodeProps
extends fabric.SerializedObjectProps,
UniqueQRCodeProps {}
const QRCODE_PROPS = ["text", "ecl", "size", "mode", "qrVersion"] as const;
export class QRCode<
Props extends fabric.TOptions<QRCodeProps> = Partial<QRCodeProps>,
SProps extends SerializedQRCodeProps = SerializedQRCodeProps,
EventSpec extends fabric.ObjectEvents = fabric.ObjectEvents,
>
extends fabric.FabricObject<Props, SProps, EventSpec>
implements QRCodeProps
{
static override readonly type = "QRCode";
/**
* QRCode text
* @type string
* @default "Text"
*/
declare text: string;
/**
* Error Correction Level
* @type ErrorCorrectionLevel
* @default "M"
*/
declare ecl: ErrorCorrectionLevel;
/**
* Mode
* @type Mode
* @default "M"
*/
declare mode: Mode;
/**
* Version
* @type Mode
* @default "M"
*/
declare qrVersion: QrVersion;
constructor(options?: Props) {
super();
Object.assign(this, qrCodeDefaultValues);
this.setOptions(options);
this.lockScalingFlip = true;
this.setControlsVisibility({
ml: false,
mt: false,
mr: false,
mb: false,
tl: false,
tr: false,
bl: false,
});
}
override _set(key: string, value: any): this {
super._set(key, value);
if (key === "text" || key === "ecl") {
this.dirty = true;
}
return this;
}
renderError(ctx: CanvasRenderingContext2D): void {
ctx.save();
ctx.fillStyle = "black";
ctx.translate(-this.width / 2, -this.height / 2); // make top-left origin
ctx.translate(-0.5, -0.5); // blurry rendering fix
ctx.fillRect(0, 0, this.width + 1, this.height + 1);
ctx.restore();
ctx.save();
ctx.fillStyle = "white";
ctx.textAlign = "center";
ctx.font = `16px ${OBJECT_DEFAULTS_TEXT.fontFamily}`;
ctx.fillText("ERR", 0, 0);
ctx.restore();
}
override _render(ctx: CanvasRenderingContext2D): void {
if (!this.text) {
this.renderError(ctx);
super._render(ctx);
return;
}
const qr = QRCodeFactory(this.qrVersion, this.ecl);
try {
qr.addData(this.text, this.mode);
qr.make();
} catch (e) {
console.error(e);
this.renderError(ctx);
super._render(ctx);
return;
}
const qrScale = Math.floor(this.width / qr.getModuleCount());
let qrWidth = qrScale * qr.getModuleCount();
qrWidth -= qrWidth % 2; // avoid half-pixel rendering
if (qrScale < 1 || qrWidth > this.width) {
this.renderError(ctx);
super._render(ctx);
return;
}
ctx.save();
ctx.translate(-qrWidth / 2, -qrWidth / 2); // make top-left origin
ctx.translate(-0.5, -0.5); // blurry rendering fix
qr.renderTo2dContext(ctx, qrScale);
ctx.restore();
super._render(ctx);
}
override toObject(propertiesToInclude: any[] = []) {
return super.toObject([...QRCODE_PROPS, ...propertiesToInclude]);
}
}
fabric.classRegistry.setClass(QRCode, "QRCode");
export default QRCode;

View File

@@ -0,0 +1,89 @@
import * as fabric from "fabric";
interface UniqueTextboxExtProps {
fontAutoSize: boolean;
}
const TEXTBOX_PROPS: Array<keyof UniqueTextboxExtProps> = ["fontAutoSize"];
export const textboxExtDefaultValues: Partial<fabric.TClassProperties<TextboxExt>> = {
fontAutoSize: false,
};
export interface TextboxExtProps extends fabric.TextboxProps, UniqueTextboxExtProps {}
export interface SerializedTextboxExtProps extends fabric.SerializedTextboxProps, UniqueTextboxExtProps {}
export class TextboxExt<
Props extends fabric.TOptions<TextboxExtProps> = Partial<TextboxExtProps>,
SProps extends SerializedTextboxExtProps = SerializedTextboxExtProps,
EventSpec extends fabric.ITextEvents = fabric.ITextEvents,
>
extends fabric.Textbox<Props, SProps, EventSpec>
implements UniqueTextboxExtProps
{
declare fontAutoSize: boolean;
private widthBeforeEditing?: number;
constructor(text: string, options?: Props) {
super(text, options);
Object.assign(this, textboxExtDefaultValues);
this.setOptions(options);
this.setControlsVisibility({
mb: false,
mt: false,
});
}
/** Set text and reduce fontSize until text fits to the given width */
setAndShrinkText(text: string, maxWidth: number, maxLines?: number) {
const linesLimit = maxLines ?? this._splitTextIntoLines(this.text).lines.length;
let linesCount = this._splitTextIntoLines(text).lines.length;
this.set({ text });
while ((linesCount > linesLimit || this.width > maxWidth) && this.fontSize > 2) {
this.fontSize -= 1;
this.set({ text, width: maxWidth });
linesCount = this._splitTextIntoLines(text).lines.length;
}
}
/** Reduce fontSize until text fits to the given width */
shrinkText(maxWidth: number, maxLines: number) {
let linesCount = this._splitTextIntoLines(this.text).lines.length;
while ((linesCount > maxLines || this.width > maxWidth) && this.fontSize > 2) {
this.fontSize -= 1;
this.set({ width: maxWidth });
linesCount = this._splitTextIntoLines(this.text).lines.length;
}
}
override enterEditingImpl() {
super.enterEditingImpl();
this.widthBeforeEditing = this.width;
}
override exitEditingImpl() {
super.exitEditingImpl();
this.widthBeforeEditing = undefined;
}
override updateFromTextArea(): void {
super.updateFromTextArea();
if (this.widthBeforeEditing !== undefined && this.fontAutoSize) {
const lines = this.text.split("\n").length;
this.shrinkText(this.widthBeforeEditing, lines);
}
}
override toObject<T extends Omit<Props & fabric.TClassProperties<this>, keyof SProps>, K extends keyof T = never>(
propertiesToInclude: K[] = [],
): Pick<T, K> & SProps {
return super.toObject([...propertiesToInclude, ...TEXTBOX_PROPS] as (keyof T)[]);
}
}