From 5ca763cdbb4f423fc585d962607901ce55cec4e6 Mon Sep 17 00:00:00 2001 From: Daishi Kato Date: Sat, 11 Apr 2020 20:10:47 +0900 Subject: [PATCH] Calculate rotated element bounds properly (#1354) * Calculate rotated element bounds properly, fixes #1303 * prefer isLinearElement * empty commit --- src/element/bounds.ts | 102 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/src/element/bounds.ts b/src/element/bounds.ts index a188a281f..db17c7ea2 100644 --- a/src/element/bounds.ts +++ b/src/element/bounds.ts @@ -197,12 +197,114 @@ export function getArrowPoints( return [x2, y2, x3, y3, x4, y4]; } +// this function has some code in common with getLinearElementAbsoluteBounds +// there might be more efficient way +const getLinearElementRotatedBounds = ( + element: ExcalidrawLinearElement, + cx: number, + cy: number, +): [number, number, number, number] => { + if (element.points.length < 2 || !getShapeForElement(element)) { + const { minX, minY, maxX, maxY } = element.points.reduce( + (limits, [x, y]) => { + [x, y] = rotate(element.x + x, element.y + y, cx, cy, element.angle); + limits.minY = Math.min(limits.minY, y); + limits.minX = Math.min(limits.minX, x); + limits.maxX = Math.max(limits.maxX, x); + limits.maxY = Math.max(limits.maxY, y); + return limits; + }, + { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }, + ); + return [minX, minY, maxX, maxY]; + } + + const shape = getShapeForElement(element) as Drawable[]; + + // first element is always the curve + const ops = getCurvePathOps(shape[0]); + + let currentP: Point = [0, 0]; + + const { minX, minY, maxX, maxY } = ops.reduce( + (limits, { op, data }) => { + // There are only four operation types: + // move, bcurveTo, lineTo, and curveTo + if (op === "move") { + // change starting point + currentP = (data as unknown) as Point; + // move operation does not draw anything; so, it always + // returns false + } else if (op === "bcurveTo") { + // create points from bezier curve + // bezier curve stores data as a flattened array of three positions + // [x1, y1, x2, y2, x3, y3] + const p1 = [data[0], data[1]] as Point; + const p2 = [data[2], data[3]] as Point; + const p3 = [data[4], data[5]] as Point; + + const p0 = currentP; + currentP = p3; + + const equation = (t: number, idx: number) => + Math.pow(1 - t, 3) * p3[idx] + + 3 * t * Math.pow(1 - t, 2) * p2[idx] + + 3 * Math.pow(t, 2) * (1 - t) * p1[idx] + + p0[idx] * Math.pow(t, 3); + + let t = 0; + while (t <= 1.0) { + let x = equation(t, 0); + let y = equation(t, 1); + [x, y] = rotate(element.x + x, element.y + y, cx, cy, element.angle); + limits.minY = Math.min(limits.minY, y); + limits.minX = Math.min(limits.minX, x); + limits.maxX = Math.max(limits.maxX, x); + limits.maxY = Math.max(limits.maxY, y); + t += 0.1; + } + } else if (op === "lineTo") { + // TODO: Implement this + } else if (op === "qcurveTo") { + // TODO: Implement this + } + return limits; + }, + { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }, + ); + + return [minX, minY, maxX, maxY]; +}; + export const getElementBounds = ( element: ExcalidrawElement, ): [number, number, number, number] => { const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const cx = (x1 + x2) / 2; const cy = (y1 + y2) / 2; + if (isLinearElement(element)) { + return getLinearElementRotatedBounds(element, cx, cy); + } + if (element.type === "diamond") { + const [x11, y11] = rotate(cx, y1, cx, cy, element.angle); + const [x12, y12] = rotate(cx, y2, cx, cy, element.angle); + const [x22, y22] = rotate(x2, cy, cx, cy, element.angle); + const [x21, y21] = rotate(x2, cy, cx, cy, element.angle); + const minX = Math.min(x11, x12, x22, x21); + const minY = Math.min(y11, y12, y22, y21); + const maxX = Math.max(x11, x12, x22, x21); + const maxY = Math.max(y11, y12, y22, y21); + return [minX, minY, maxX, maxY]; + } + if (element.type === "ellipse") { + const w = (x2 - x1) / 2; + const h = (y2 - y1) / 2; + const cos = Math.cos(element.angle); + const sin = Math.sin(element.angle); + const ww = Math.hypot(w * cos, h * sin); + const hh = Math.hypot(h * cos, w * sin); + return [cx - ww, cy - hh, cx + ww, cy + hh]; + } const [x11, y11] = rotate(x1, y1, cx, cy, element.angle); const [x12, y12] = rotate(x1, y2, cx, cy, element.angle); const [x22, y22] = rotate(x2, y2, cx, cy, element.angle);