feat: complete TASK-WP4-03 text templates
This commit is contained in:
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user