feat: complete TASK-WP4-03 text templates

This commit is contained in:
suyx
2026-08-03 11:40:45 +08:00
parent d16802c164
commit dd7a23a281
24 changed files with 1934 additions and 20 deletions
+40 -2
View File
@@ -35,6 +35,34 @@ interface StaticStickerInput {
const snapThreshold = 0.015;
const hitHalfExtent = 0.08;
function styleNumber(element: CanvasElement, key: string, fallback: number) {
const value = element.style_parameters?.[key];
return typeof value === "number" ? value : fallback;
}
function textLineUnits(line: string) {
return Array.from(line).reduce((total, character) => total + (/^[\x00-\x7F]$/.test(character) ? 0.62 : 1), 0);
}
export function elementHalfExtents(state: CanvasState, element: CanvasElement): CanvasPoint {
if (element.type !== "text_template") {
return { x: hitHalfExtent * element.scale.x, y: hitHalfExtent * element.scale.y };
}
const lines = (element.content ?? "").split("\n");
const fontSize = element.font_size ?? 48;
const letterSpacing = styleNumber(element, "letter_spacing", 1);
const lineHeight = styleNumber(element, "line_height", 1.2);
const strokeWidth = styleNumber(element, "stroke_width", 0);
const longestLine = Math.max(1, ...lines.map((line) => textLineUnits(line)));
const longestCharacterCount = Math.max(1, ...lines.map((line) => Array.from(line).length));
const widthPixels = longestLine * fontSize + (longestCharacterCount - 1) * letterSpacing + 32 + strokeWidth * 2;
const heightPixels = Math.max(1, lines.length) * fontSize * lineHeight + 32 + strokeWidth * 2;
return {
x: Math.max(hitHalfExtent, widthPixels / state.pixel_width / 2) * element.scale.x,
y: Math.max(hitHalfExtent, heightPixels / state.pixel_height / 2) * element.scale.y,
};
}
function cloneState(state: CanvasState) {
return structuredClone(state);
}
@@ -132,8 +160,7 @@ export class CanvasElementController {
candidatesAt(point: CanvasPoint) {
return this.current.elements
.filter((element) => {
const halfWidth = hitHalfExtent * element.scale.x;
const halfHeight = hitHalfExtent * element.scale.y;
const { x: halfWidth, y: halfHeight } = elementHalfExtents(this.current, element);
return point.x >= element.position.x - halfWidth && point.x <= element.position.x + halfWidth
&& point.y >= element.position.y - halfHeight && point.y <= element.position.y + halfHeight;
})
@@ -186,6 +213,17 @@ export class CanvasElementController {
return this.value;
}
replaceElement(element: CanvasElement) {
const index = this.current.elements.findIndex((candidate) => candidate.element_id === element.element_id);
if (index < 0) throw new Error("canvas_element_not_found");
const elements = [...this.current.elements];
elements[index] = structuredClone(element);
this.current = requireState({ ...this.current, elements });
this.selection = [element.element_id];
this.pointerMoved();
return this.value;
}
private updateSelected(mapper: (element: CanvasElement) => CanvasElement) {
const selected = new Set(this.selection);
this.current = requireState({