import { type TBBox, util, type XY } from 'fabric'; const { makeBoundingBoxFromPoints } = util; /** * 计算将 `bounds` 平移多少 `{ x, y }` 后,能按选定策略与 `container` 对齐。 * * - **cover**:在宽、高均不小于容器时,避免出现「容器内侧露空」——保持内容仍盖住整个容器(类似 cover)。 * - **contain**:将 `bounds` 完全限制在容器矩形内,防止越界拖出可视区域。 * - **auto**:根据 `bounds` 与 `container` 的尺寸关系自动选择:若宽高均不小于容器(在 EPS 容差内)则按 cover,否则按 contain。 * * @param bounds - 待调整的轴对齐矩形(如内容、选区等)。 * @param container - 轴对齐的裁剪或视口矩形。 * @param mode - 夹紧策略。`auto` 为默认,按尺寸推断;`cover` / `contain` 为强制策略。 * @returns 需要施加的平移 `{ x, y }`;若无需移动则返回 `undefined`。 */ export function calcTranslationDelta(bounds: TBBox, container: TBBox, mode: 'auto' | 'cover' | 'contain' = 'auto'): XY | void { const EPS = 1e-6; const covers = mode === 'auto' ? bounds.width >= container.width - EPS && bounds.height >= container.height - EPS : mode === 'cover'; const bl = bounds.left; const bt = bounds.top; const br = bounds.left + bounds.width; const bb = bounds.top + bounds.height; const cl = container.left; const ct = container.top; const cr = container.left + container.width; const cb = container.top + container.height; const delta = { x: 0, y: 0 }; if (covers) { if (bl > cl + EPS) delta.x -= bl - cl; if (bt > ct + EPS) delta.y -= bt - ct; if (br < cr - EPS) delta.x += cr - br; if (bb < cb - EPS) delta.y += cb - bb; } else { if (bl < cl - EPS) delta.x += cl - bl; if (bt < ct - EPS) delta.y += ct - bt; if (br > cr + EPS) delta.x -= br - cr; if (bb > cb + EPS) delta.y -= bb - cb; } return delta.x === 0 && delta.y === 0 ? void 0 : delta; } /** * 计算将主体四边形约束在容器四边形(带 UV 内边)内所需的一系列修正步骤。 * * 在最多 `refineIters` 次迭代中,依次尝试:按轴对齐包围盒做 contain 平移、将越界角点沿容器四边形 UV 拉回(平移有上限)、仍不满足时相对重心等比缩小。 * 返回的步骤需按顺序应用;`translate` 的 `x/y` 为平移增量,`scale` 的 `x/y` 为相对重心的缩放系数(当前实现中二者相等)。 * * @param corners - 主体四边形顶点(至少 4 点,顺序与 `container` 一致,按平行四边形处理)。 * @param container - 容器/视口四边形顶点(至少 4 点)。 * @param options - 预留配置(当前未使用)。 * @returns 修正步骤数组;若 `corners` 非法(非数组或点数不足)则返回 `undefined`;已满足约束时可能为空数组。 */ export function calcSubjectContainerClampSteps(corners: XY[], container: XY[], options?: {step?: number}) { if (!Array.isArray(corners) || corners.length < 4) return; if (!Array.isArray(container) || container.length < 4) return; const result: Array<{ kind: 'translate' | 'scale' } & XY> = []; const containerInset = 0.35; const containerNudgeCap = 48; const refineIters = options?.step ?? 16; const shrink = 0.97; const { insetU, insetV } = parallelogramWithInset(container, containerInset); for (let step = 0; step < refineIters; step++) { if (!subjectViolations(corners, container, insetU, insetV)) break; const delta = calcTranslationDelta(makeBoundingBoxFromPoints(corners), makeBoundingBoxFromPoints(container), 'contain'); if (delta) { corners = corners.map((p) => ({ x: p.x + delta.x, y: p.y + delta.y })); result.push({ kind: 'translate', ...delta }); } const pull = subjectPullIntoBackdropQuad(corners, container, { cap: containerNudgeCap, inset: containerInset }); if (pull) { corners = corners.map((p) => ({ x: p.x + pull.x, y: p.y + pull.y })); result.push({ kind: 'translate', ...pull }); } if (!subjectViolations(corners, container, insetU, insetV)) break; const centroid = corners.reduce((c, p) => ({ x: c.x + p.x, y: c.y + p.y }), { x: 0, y: 0 }); const cx = centroid.x / corners.length; const cy = centroid.y / corners.length; corners = corners.map((p) => ({ x: cx + shrink * (p.x - cx), y: cy + shrink * (p.y - cy) })); result.push({ kind: 'scale', x: shrink, y: shrink }); } return result; } function parallelogramWithInset(corners: XY[], inset = 0.35) { const [tl, tr, br, bl] = corners; const e1x = tr.x - tl.x; const e1y = tr.y - tl.y; const e2x = bl.x - tl.x; const e2y = bl.y - tl.y; const len1 = Math.hypot(e1x, e1y) || 1; const len2 = Math.hypot(e2x, e2y) || 1; return { tl, tr, br, bl, e1x, e1y, e2x, e2y, insetU: inset / len1, insetV: inset / len2, }; } function subjectPullIntoBackdropQuad(corners: XY[], container: XY[], options?: { cap?: number; inset?: number }) { const cap = options?.cap ?? 48; const { e1x, e1y, e2x, e2y, insetV, insetU } = parallelogramWithInset(container, options?.inset); let sx = 0; let sy = 0; let n = 0; for (const p of corners) { if (pointInParallelogramInset(container, p, insetU, insetV)) continue; const { u, v } = parallelogramUV(container, p); const u0 = insetU; const u1 = 1 - insetU; const v0 = insetV; const v1 = 1 - insetV; const uc = Math.min(u1, Math.max(u0, u)); const vc = Math.min(v1, Math.max(v0, v)); const du = uc - u; const dv = vc - v; if (du === 0 && dv === 0) continue; sx += du * e1x + dv * e2x; sy += du * e1y + dv * e2y; n++; } const delta = { x: n === 0 ? 0 : sx / n, y: n === 0 ? 0 : sy / n }; const mag = Math.hypot(delta.x, delta.y); if (mag > cap) { const s = cap / mag; delta.x *= s; delta.y *= s; } return delta.x === 0 && delta.y === 0 ? void 0 : delta; } function parallelogramUV(corners: XY[], point: XY): { u: number; v: number } { const [tl, tr, br, bl] = corners; const e1x = tr.x - tl.x; const e1y = tr.y - tl.y; const e2x = bl.x - tl.x; const e2y = bl.y - tl.y; const px = point.x - tl.x; const py = point.y - tl.y; const det = e1x * e2y - e1y * e2x; if (Math.abs(det) < 1e-12) return { u: 0, v: 0 }; const u = (px * e2y - py * e2x) / det; const v = (e1x * py - e1y * px) / det; return { u, v }; } function pointInParallelogramInset(corners: XY[], point: XY, insetU: number, insetV: number): boolean { const { u, v } = parallelogramUV(corners, point); const u0 = insetU; const u1 = 1 - insetU; const v0 = insetV; const v1 = 1 - insetV; if (!(u0 <= u1 && v0 <= v1)) return u >= 0 && u <= 1 && v >= 0 && v <= 1; return u >= u0 - 1e-9 && u <= u1 + 1e-9 && v >= v0 - 1e-9 && v <= v1 + 1e-9; } function subjectViolations(corners: XY[], container: XY[], backdropInsetU: number, backdropInsetV: number): boolean { for (const p of corners) { if (!pointInParallelogramInset(container, p, backdropInsetU, backdropInsetV)) return true; } return false; }