Skip to content

Commit

Permalink
arc bounding box
Browse files Browse the repository at this point in the history
  • Loading branch information
mzusin committed Apr 14, 2023
1 parent d237186 commit 8b167d5
Show file tree
Hide file tree
Showing 14 changed files with 396 additions and 48 deletions.
4 changes: 2 additions & 2 deletions dist/mz-svg.esm.js

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions dist/mz-svg.esm.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions dist/mz-svg.min.js

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions dist/mz-svg.min.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions dist/mz-svg.node.cjs

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions dist/mz-svg.node.cjs.map

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion src/index-esm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export * from './main/io-browser';
export * from './main/containers';
export * from './main/helpers';
export * from './main/path';
export * from './main/path/transform';
export * from './main/path/transform';
export * from './main/path/bbox';
5 changes: 4 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as ioBrowser from './main/io-browser';
import * as helpers from './main/helpers';
import * as pathUtilities from './main/path';
import * as pathTransform from './main/path/transform';
import * as bbox from './main/path/bbox';

const api = {
...core,
Expand All @@ -20,6 +21,7 @@ const api = {
...helpers,
...pathUtilities,
...pathTransform,
...bbox,
};

declare global {
Expand All @@ -39,4 +41,5 @@ export * from './main/shapes/shape-paths';
export * from './main/containers';
export * from './main/helpers';
export * from './main/path';
export * from './main/path/transform';
export * from './main/path/transform';
export * from './main/path/bbox';
286 changes: 284 additions & 2 deletions src/main/path/bbox.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { parsePath } from './index';
import { maximizeAbsolutePath, pathDataToAbsolute } from './convert';
import { EPathDataCommand } from './interfaces';
import { setDecimalPlaces, v2CubicBezierBBox, v2QuadraticBezierBBox, Vector2 } from 'mz-math';
import {
degreesToRadians,
Matrix2, mMulVector,
setDecimalPlaces,
v2CubicBezierBBox, v2MulScalar,
v2QuadraticBezierBBox, v2Sum,
Vector2
} from 'mz-math';

export interface IBBox {
x: number;
Expand All @@ -12,6 +19,263 @@ export interface IBBox {
y2: number;
}

/**
* https://www.w3.org/TR/SVG11/implnote.html#ArcConversionEndpointToCenter
*/
export const getSVGArcCenter = (
startX: number,
startY: number,
rx: number,
ry: number,
angleRad: number,
largeArcFlag: number,
sweepFlag: number,
endX: number,
endY: number
) : Vector2 | null => {
// rx ry x-axis-rotation large-arc-flag sweep-flag x y

// F.6.5: Step 1 ---------------------------------------
const cos = Math.cos(angleRad);
const sin = Math.sin(angleRad);

const rotationMatrix: Matrix2 = [
[cos, -sin],
[sin, cos],
];
const posVector1: Vector2 = [
(startX - endX) / 2,
(startY - endY) / 2,
];

// Compute (x1′, y1′)
const posVector2 = mMulVector(rotationMatrix, posVector1);

// F.6.5: Step 2 ---------------------------------------
const rx2 = rx ** 2;
const ry2 = ry ** 2;
const posx2 = posVector2[0] ** 2;
const posy2 = posVector2[1] ** 2;

const t1 = rx2 * ry2 - rx2 * posy2 - ry2 * posx2;
const t2 = rx2 * posy2 + ry2 * posx2;
if(t2 === 0) return null;

const t3 = t1 / t2;
if(t3 < 0) return null;

let t4 = Math.sqrt(t3);

const posVector3: Vector2 = [
rx * posVector2[1] / ry,
-ry * posVector2[0] / rx,
];

if(largeArcFlag === sweepFlag){
t4 = -t4;
}

const centerVector1: Vector2 = v2MulScalar(posVector3, t4);

// F.6.5: Step 3 ---------------------------------------
const rotationMatrix2: Matrix2 = [
[cos, sin],
[-sin, cos],
];

const centerVector2: Vector2 = mMulVector(rotationMatrix2, centerVector1) as Vector2;
const posVector4: Vector2 = [
(startX + endX) / 2,
(startY + endY) / 2,
];

return v2Sum(centerVector2, posVector4);


/*if (rx < 0) {
rx *= -1;
}
if (ry < 0) {
ry *= -1;
}
if (rx === 0 || ry === 0) {
xmin = x1 < x2 ? x1 : x2;
xmax = x1 > x2 ? x1 : x2;
ymin = y1 < y2 ? y1 : y2;
ymax = y1 > y2 ? y1 : y2;
return formatBBox(xmin, xmax, ymin, ymax);
}
const x1prime: number = Math.cos(phi) * (x1 - x2) / 2 + Math.sin(phi) * (y1 - y2) / 2;
const y1prime: number = -Math.sin(phi) * (x1 - x2) / 2 + Math.cos(phi) * (y1 - y2) / 2;
let radicant: number = (rx * rx * ry * ry - rx * rx * y1prime * y1prime - ry * ry * x1prime * x1prime);
radicant /= (rx * rx * y1prime * y1prime + ry * ry * x1prime * x1prime);
let cxPrime = 0;
let cyPrime = 0;
if (radicant < 0) {
const ratio: number = rx / ry;
radicant = y1prime * y1prime + x1prime * x1prime / (ratio * ratio);
if (radicant < 0) {
xmin = (x1 < x2 ? x1 : x2);
xmax = (x1 > x2 ? x1 : x2);
ymin = (y1 < y2 ? y1 : y2);
ymax = (y1 > y2 ? y1 : y2);
return formatBBox(xmin, xmax, ymin, ymax);
}
ry = Math.sqrt(radicant);
rx = ratio * ry;
}
else {
const factor = (largeArc == sweep ? -1 : 1) * Math.sqrt(radicant);
cxPrime = factor * rx * y1prime / ry;
cyPrime = -factor * ry * x1prime / rx;
}
const cx = cxPrime * Math.cos(phi) - cyPrime * Math.sin(phi) + (x1 + x2) / 2;
const cy = cxPrime * Math.sin(phi) + cyPrime * Math.cos(phi) + (y1 + y2) / 2;*/
};

const getAngle = (bx: number, by: number): number => {
const PI2 = 2 * Math.PI;
const t1 = by > 0 ? 1 : -1;
return ((PI2 + t1 * Math.acos(bx / Math.sqrt(bx * bx + by * by))) % PI2);
};

const formatBBox = (xmin: number, xmax: number, ymin: number, ymax: number) : IBBox => {
return {
x: xmin,
y: ymin,
w: Math.abs(ymax - ymin),
h: Math.abs(xmax - xmin),
x2: xmax,
y2: ymax,
};
};

const getArcBoundingBox = (
x1: number, y1: number,
rx: number, ry: number,
angleRad: number, largeArc: boolean,
sweep: boolean,
x2: number, y2: number
) : IBBox => {

let xmin, xmax, ymin, ymax;

const center = getSVGArcCenter(
x1,
y1,
rx,
ry,
angleRad,
largeArc ? 1 : 0,
sweep ? 1 : 0,
x2,
y2
);
if(!center) return formatBBox(0, 0, 0, 0);

const cx = center[0];
const cy = center[1];

let txMin: number, txMax: number, tyMin: number, tyMax: number;

if (angleRad === 0 || angleRad === Math.PI) {
xmin = cx - rx;
txMin = getAngle(-rx, 0);
xmax = cx + rx;
txMax = getAngle(rx, 0);
ymin = cy - ry;
tyMin = getAngle(0, -ry);
ymax = cy + ry;
tyMax = getAngle(0, ry);
}
else if (angleRad === Math.PI / 2 || angleRad === 3.0 * Math.PI / 2) {
xmin = cx - ry;
txMin = getAngle(-ry, 0);
xmax = cx + ry;
txMax = getAngle(ry, 0);
ymin = cy - rx;
tyMin = getAngle(0, -rx);
ymax = cy + rx;
tyMax = getAngle(0, rx);
}
else {
txMin = -Math.atan(ry * Math.tan(angleRad) / rx);
txMax = Math.PI - Math.atan(ry * Math.tan(angleRad) / rx);

xmin = cx + rx * Math.cos(txMax) * Math.cos(angleRad) - ry * Math.sin(txMin) * Math.sin(angleRad);
xmax = cx + rx * Math.cos(txMax) * Math.cos(angleRad) - ry * Math.sin(txMax) * Math.sin(angleRad);

// swap ------------------------
if (xmin > xmax) {
[xmin, xmax] = [xmax, xmin];
[txMin, txMax] = [txMax, txMin];
}

let tmpY = cy + rx * Math.cos(txMin) * Math.sin(angleRad) + ry * Math.sin(txMin) * Math.cos(angleRad);
txMin = getAngle(xmin - cx, tmpY - cy);
tmpY = cy + rx * Math.cos(txMax) * Math.sin(angleRad) + ry * Math.sin(txMax) * Math.cos(angleRad);
txMax = getAngle(xmax - cx, tmpY - cy);

tyMin = Math.atan(ry / (Math.tan(angleRad) * rx));
tyMax = Math.atan(ry / (Math.tan(angleRad) * rx)) + Math.PI;
ymin = cy + rx * Math.cos(tyMin) * Math.sin(angleRad) + ry * Math.sin(tyMin) * Math.cos(angleRad);
ymax = cy + rx * Math.cos(tyMax) * Math.sin(angleRad) + ry * Math.sin(tyMax) * Math.cos(angleRad);

// swap ------------------------
if (ymin > ymax) {
[ymin, ymax] = [ymax, ymin];
[tyMin, tyMax] = [tyMax, tyMin];
}

let tmpX = cx + rx * Math.cos(tyMin) * Math.cos(angleRad) - ry * Math.sin(tyMin) * Math.sin(angleRad);
tyMin = getAngle(tmpX - cx, ymin - cy);
tmpX = cx + rx * Math.cos(tyMax) * Math.cos(angleRad) - ry * Math.sin(tyMax) * Math.sin(angleRad);
tyMax = getAngle(tmpX - cx, ymax - cy);
}

let angle1 = getAngle(x1 - cx, y1 - cy);
let angle2 = getAngle(x2 - cx, y2 - cy);

if (!sweep){
[angle1, angle2] = [angle2, angle1];
}

let otherArc = false;

if (angle1 > angle2) {
// swap ------------------------
[angle1, angle2] = [angle2, angle1];
otherArc = true;
}

if ((!otherArc && (angle1 > txMin || angle2 < txMin)) || (otherArc && !(angle1 > txMin || angle2 < txMin))) {
xmin = Math.min(x1, x2);
}

if ((!otherArc && (angle1 > txMax || angle2 < txMax)) || (otherArc && !(angle1 > txMax || angle2 < txMax))) {
xmax = Math.max(x1, x2);
}

if ((!otherArc && (angle1 > tyMin || angle2 < tyMin)) || (otherArc && !(angle1 > tyMin || angle2 < tyMin))) {
ymin = Math.min(y1, y2);
}

if ((!otherArc && (angle1 > tyMax || angle2 < tyMax)) || (otherArc && !(angle1 > tyMax || angle2 < tyMax))) {
ymax = Math.max(y1, y2);
}

return formatBBox(xmin, xmax, ymin, ymax);
}

/**
* Determine the coordinates of the smallest rectangle in which the path fits.
*/
Expand Down Expand Up @@ -108,7 +372,25 @@ export const getPathBBox = (d?: string, decimalPlaces = 2) : IBBox|null => {
}

case EPathDataCommand.ArcAbs:{
//parseCommand(7, token.tokenType, isRelative);
// rx ry x-axis-rotation large-arc-flag sweep-flag x y
const rx = item.params[0];
const ry = item.params[1];
const angleDeg = item.params[2];
const largeArcFlag = item.params[3];
const sweepFlag = item.params[4];
const endX = item.params[5];
const endY = item.params[6];

//const arcCenter = getSVGArcCenter(x, y, rx, ry, angleDeg, largeArcFlag, sweepFlag, endX, endY);
//console.log('arcCenter', arcCenter)

const bbox = getArcBoundingBox(x, y, rx, ry, degreesToRadians(angleDeg), largeArcFlag === 1, sweepFlag === 1, endX, endY);

minX = bbox?.x ?? 0;
minY = bbox?.y ?? 0;

maxX = bbox?.x2 ?? 0;
maxY = bbox?.y2 ?? 0;

x = item.params[5];
y = item.params[6];
Expand Down
Binary file added src/main/path/svg-path-arc.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 0 additions & 6 deletions test-browser/examples/path/rotate-cubic-bezier.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,6 @@
<body>
<script src="../../../dist/mz-svg.min.js"></script>
<script>

// console.log(mzSVG.pathToAbs('M400 300h50v50h-50z'))

const cx = 425;
const cy = 325;

const $svg = mzSVG.createSVG({
width: 800,
height: 600
Expand Down
Loading

0 comments on commit 8b167d5

Please sign in to comment.