Files
label-designer/src/elementBox.ts
2026-06-12 20:58:51 +08:00

290 lines
7.4 KiB
TypeScript

import { TemplateElement, ElementSide } from './types';
import { resolveElementBackgroundFill } from './utils';
export interface CornerRadiiPx {
tl: number;
tr: number;
br: number;
bl: number;
}
export interface BorderSides {
top: boolean;
right: boolean;
bottom: boolean;
left: boolean;
}
export interface PaddingMm {
top: number;
right: number;
bottom: number;
left: number;
}
export interface BorderSideStyle {
width: number;
color: string;
}
export type BorderConfig = Record<ElementSide, BorderSideStyle>;
export function resolvePaddingMm(elem: TemplateElement): PaddingMm {
const base = elem.padding ?? 0;
const val = (specific?: number) =>
specific !== undefined && specific !== null ? specific : base;
return {
top: val(elem.paddingTop),
right: val(elem.paddingRight),
bottom: val(elem.paddingBottom),
left: val(elem.paddingLeft),
};
}
export function getPaddedRectPx(
x: number,
y: number,
w: number,
h: number,
elem: TemplateElement,
scale: number
) {
const pad = resolvePaddingMm(elem);
const top = Math.round(pad.top * scale);
const right = Math.round(pad.right * scale);
const bottom = Math.round(pad.bottom * scale);
const left = Math.round(pad.left * scale);
return {
x: x + left,
y: y + top,
w: Math.max(1, w - left - right),
h: Math.max(1, h - top - bottom),
paddingPx: { top, right, bottom, left },
};
}
export function resolveBorderConfig(elem: TemplateElement): BorderConfig {
const baseWidth = elem.borderWidth ?? 0;
const baseColor = elem.borderColor || '#111827';
const legacy = elem.borderSides ?? {};
const resolveSide = (
side: ElementSide,
widthOverride: number | undefined,
colorOverride: string | undefined
): BorderSideStyle => {
let width: number;
if (widthOverride !== undefined && widthOverride !== null) {
width = widthOverride;
} else if (baseWidth <= 0 || legacy[side] === false) {
width = 0;
} else {
width = baseWidth;
}
return {
width,
color: colorOverride?.trim() ? colorOverride : baseColor,
};
};
return {
top: resolveSide('top', elem.borderWidthTop, elem.borderColorTop),
right: resolveSide('right', elem.borderWidthRight, elem.borderColorRight),
bottom: resolveSide('bottom', elem.borderWidthBottom, elem.borderColorBottom),
left: resolveSide('left', elem.borderWidthLeft, elem.borderColorLeft),
};
}
export function resolveCornerRadiiPx(
elem: TemplateElement,
scale: number,
w: number,
h: number
): CornerRadiiPx {
const maxR = Math.min(w, h) / 2;
const baseMm = elem.borderRadius ?? 0;
const radiusFor = (specificMm?: number): number => {
const mm = specificMm !== undefined && specificMm !== null ? specificMm : baseMm;
if (mm <= 0) return 0;
return Math.min(maxR, mm * scale);
};
return {
tl: radiusFor(elem.borderRadiusTL),
tr: radiusFor(elem.borderRadiusTR),
br: radiusFor(elem.borderRadiusBR),
bl: radiusFor(elem.borderRadiusBL),
};
}
export function resolveBorderSides(elem: TemplateElement): BorderSides {
const cfg = resolveBorderConfig(elem);
return {
top: cfg.top.width > 0,
right: cfg.right.width > 0,
bottom: cfg.bottom.width > 0,
left: cfg.left.width > 0,
};
}
/** 构建顺时针闭合圆角矩形路径 */
export function traceRoundedRect(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
w: number,
h: number,
r: CornerRadiiPx
) {
const { tl, tr, br, bl } = r;
ctx.beginPath();
ctx.moveTo(x + tl, y);
ctx.lineTo(x + w - tr, y);
if (tr > 0) ctx.arcTo(x + w, y, x + w, y + tr, tr);
else ctx.lineTo(x + w, y);
ctx.lineTo(x + w, y + h - br);
if (br > 0) ctx.arcTo(x + w, y + h, x + w - br, y + h, br);
else ctx.lineTo(x + w, y + h);
ctx.lineTo(x + bl, y + h);
if (bl > 0) ctx.arcTo(x, y + h, x, y + h - bl, bl);
else ctx.lineTo(x, y + h);
ctx.lineTo(x, y + tl);
if (tl > 0) ctx.arcTo(x, y, x + tl, y, tl);
else ctx.lineTo(x, y);
ctx.closePath();
}
export function drawElementFill(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
w: number,
h: number,
elem: TemplateElement,
radii: CornerRadiiPx
) {
const fill = resolveElementBackgroundFill(elem.backgroundColor, elem.backgroundOpacity);
if (!fill) return;
traceRoundedRect(ctx, x, y, w, h, radii);
ctx.fillStyle = fill;
ctx.fill();
}
export function drawElementBorders(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
w: number,
h: number,
radii: CornerRadiiPx,
sides: BorderSides,
elem: TemplateElement,
scale: number
) {
const border = resolveBorderConfig(elem);
const { tl, tr, br, bl } = radii;
ctx.lineCap = 'butt';
ctx.lineJoin = 'round';
if (sides.top) {
const lw = Math.max(1, border.top.width * scale);
const inset = lw / 2;
ctx.strokeStyle = border.top.color;
ctx.lineWidth = lw;
ctx.beginPath();
if (sides.left && tl > 0) {
ctx.arc(x + tl, y + tl, Math.max(0, tl - inset), Math.PI, 1.5 * Math.PI);
} else {
ctx.moveTo(x, y + inset);
}
if (sides.right && tr > 0) {
ctx.lineTo(x + w - tr, y + inset);
ctx.arc(x + w - tr, y + tr, Math.max(0, tr - inset), 1.5 * Math.PI, 2 * Math.PI);
} else {
ctx.lineTo(x + w, y + inset);
}
ctx.stroke();
}
if (sides.right) {
const lw = Math.max(1, border.right.width * scale);
const inset = lw / 2;
ctx.strokeStyle = border.right.color;
ctx.lineWidth = lw;
ctx.beginPath();
const topStart = sides.top && tr > 0;
if (topStart) {
ctx.moveTo(x + w - inset, y + tr);
} else {
ctx.moveTo(x + w - inset, y);
}
if (sides.bottom && br > 0) {
ctx.lineTo(x + w - inset, y + h - br);
ctx.arc(x + w - br, y + h - br, Math.max(0, br - inset), 0, 0.5 * Math.PI);
} else {
ctx.lineTo(x + w - inset, y + h);
}
ctx.stroke();
}
if (sides.bottom) {
const lw = Math.max(1, border.bottom.width * scale);
const inset = lw / 2;
ctx.strokeStyle = border.bottom.color;
ctx.lineWidth = lw;
ctx.beginPath();
if (sides.right && br > 0) {
ctx.moveTo(x + w - inset, y + h - br);
ctx.arc(x + w - br, y + h - br, Math.max(0, br - inset), 0, 0.5 * Math.PI);
} else {
ctx.moveTo(x + w - inset, y + h - inset);
}
if (sides.left && bl > 0) {
ctx.lineTo(x + bl, y + h - inset);
ctx.arc(x + bl, y + h - bl, Math.max(0, bl - inset), 0.5 * Math.PI, Math.PI);
} else {
ctx.lineTo(x, y + h - inset);
}
ctx.stroke();
}
if (sides.left) {
const lw = Math.max(1, border.left.width * scale);
const inset = lw / 2;
ctx.strokeStyle = border.left.color;
ctx.lineWidth = lw;
ctx.beginPath();
const bottomStart = sides.bottom && bl > 0;
if (bottomStart) {
ctx.moveTo(x + inset, y + h - bl);
} else {
ctx.moveTo(x + inset, y + h);
}
if (sides.top && tl > 0) {
ctx.lineTo(x + inset, y + tl);
ctx.arc(x + tl, y + tl, Math.max(0, tl - inset), Math.PI, 1.5 * Math.PI);
} else {
ctx.lineTo(x + inset, y);
}
ctx.stroke();
}
}
export function drawElementChrome(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
w: number,
h: number,
elem: TemplateElement,
scale: number
) {
const radii = resolveCornerRadiiPx(elem, scale, w, h);
const sides = resolveBorderSides(elem);
drawElementFill(ctx, x, y, w, h, elem, radii);
drawElementBorders(ctx, x, y, w, h, radii, sides, elem, scale);
return { radii, sides };
}