import * as fabric from "fabric"; import { pathLengthPx, type ElDef, type ThreadColor } from "@/lib/tatting-ru"; export type TattingObject = fabric.Group & { elId?: string; elName?: string; knots?: number; pathPx?: number; threadName?: string; isGrid?: boolean; isRuler?: boolean; }; export const CUSTOM_PROPS = ["elId", "elName", "knots", "pathPx", "threadName", "isGrid", "isRuler"]; const strokeOf = (t: ThreadColor, size: number): string | fabric.Gradient<"linear"> => t.to ? new fabric.Gradient({ type: "linear", gradientUnits: "pixels", coords: { x1: -size, y1: -size, x2: size, y2: size }, colorStops: [ { offset: 0, color: t.value }, { offset: 1, color: t.to }, ], }) : t.value; export function buildElement(def: ElDef, thread: ThreadColor, width: number): TattingObject { const r = def.radius; if (def.kind === "text") { const text = new fabric.IText(def.id === "rownum" ? "Ряд 1" : "Подпись", { fontSize: def.radius * 1.6, fontFamily: "DM Sans, system-ui, sans-serif", fill: thread.value, originX: "center", originY: "center", }) as unknown as TattingObject; text.elId = def.id; text.elName = def.name; text.knots = 0; text.pathPx = 0; text.threadName = thread.name; return text; } const stroke = strokeOf(thread, r * 1.4); const picotR = Math.max(3, r * 0.13); const base = { fill: "", stroke, strokeWidth: width, originX: "center", originY: "center" } as const; const parts: fabric.FabricObject[] = []; const picotRing = (count: number, rad: number) => { for (let i = 0; i < count; i++) { const a = (i / count) * Math.PI * 2 - Math.PI / 2; parts.push( new fabric.Circle({ ...base, radius: picotR, left: Math.cos(a) * (rad + picotR * 0.8), top: Math.sin(a) * (rad + picotR * 0.8), }), ); } }; if (def.kind === "ring") { parts.push(new fabric.Circle({ ...base, radius: r, left: 0, top: 0 })); parts.push( new fabric.Circle({ ...base, radius: r - 5, left: 0, top: 0, strokeWidth: width * 0.4, opacity: 0.5 }), ); picotRing(def.picots, r); } else if (def.kind === "arc" || def.kind === "arc-curved") { const sweep = def.kind === "arc-curved" ? 1.35 : 1; const half = (Math.PI * sweep) / 2; const x0 = -Math.sin(half) * r; const y0 = Math.cos(half) * r; parts.push( new fabric.Path(`M ${x0} ${y0} A ${r} ${r} 0 ${sweep > 1 ? 1 : 0} 1 ${-x0} ${y0}`, { ...base, left: 0, top: 0, }), ); parts.push( new fabric.Path(`M ${x0 * 0.88} ${y0 * 0.88} A ${r - 5} ${r - 5} 0 ${sweep > 1 ? 1 : 0} 1 ${-x0 * 0.88} ${y0 * 0.88}`, { ...base, left: 0, top: 0, strokeWidth: width * 0.4, opacity: 0.5, }), ); for (let i = 0; i < def.picots; i++) { const t = (i + 1) / (def.picots + 1); const a = -half + t * half * 2; parts.push( new fabric.Circle({ ...base, radius: picotR, left: Math.sin(a) * (r + picotR * 0.8), top: -Math.cos(a) * (r + picotR * 0.8) + r, }), ); } } else if (def.kind === "picot") { parts.push(new fabric.Path(`M ${-r} 0 Q 0 ${-r * 1.8} ${r} 0`, { ...base, left: 0, top: 0 })); parts.push(new fabric.Circle({ ...base, radius: r * 0.35, left: 0, top: -r * 0.6 })); } else if (def.kind === "bead") { parts.push( new fabric.Circle({ radius: r, left: 0, top: 0, originX: "center", originY: "center", fill: stroke, stroke, strokeWidth: 1, }), ); } else { const pts = [ { x: 0, y: -r * 1.3 }, { x: r, y: 0 }, { x: 0, y: r * 1.3 }, { x: -r, y: 0 }, ]; parts.push( new fabric.Polygon(pts, { left: 0, top: 0, originX: "center", originY: "center", fill: stroke, stroke, strokeWidth: 1, }), ); parts.push(new fabric.Polygon(pts.map((p) => ({ x: p.x * 0.5, y: p.y * 0.5 })), { left: 0, top: 0, originX: "center", originY: "center", fill: "", stroke: "#ffffff", opacity: 0.6, strokeWidth: 1, })); } const group = new fabric.Group(parts, { originX: "center", originY: "center", objectCaching: false, }) as TattingObject; group.elId = def.id; group.elName = def.name; group.knots = def.knots; group.pathPx = pathLengthPx(def); group.threadName = thread.name; return group; } export function applyThread(obj: TattingObject, thread: ThreadColor) { if (typeof obj.getObjects !== "function") { obj.set("fill", thread.value); obj.threadName = thread.name; obj.dirty = true; return; } const size = Math.max(obj.width ?? 40, obj.height ?? 40) * 0.7; const filled = obj.elId === "bead" || obj.elId === "crystal"; obj.getObjects().forEach((child, i) => { child.set("stroke", strokeOf(thread, size)); if (filled && i === 0) child.set("fill", strokeOf(thread, size)); }); obj.threadName = thread.name; obj.dirty = true; } export function applyWidth(obj: TattingObject, width: number) { if (typeof obj.getObjects !== "function") return; obj.getObjects().forEach((child) => { const thin = (child.strokeWidth ?? 1) < 1.5; child.set("strokeWidth", thin ? width * 0.4 : width); }); obj.dirty = true; }