Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: make segment size configurable when calculating path length & bbox #93

Merged
merged 1 commit into from
Aug 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions __tests__/unit/path/get-path-bbox.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,11 @@ describe('get path bbox', () => {
const { length, ...rest } = getPathBBoxTotalLength(segments);
expect(rest).toEqual({ cx: 8, cy: 8, cz: 24, height: 16, width: 16, x: 0, x2: 16, y: 0, y2: 16 });
});

it('should calc circle path with fewer segements correctly', () => {
const str: PathArray = getCirclePath(0, 0, 100, 100);
const bbox = getPathBBox(str, { sampleSize: 8 });

expect(bbox).toEqual({ cx: 0, cy: 100, cz: 300, height: 200, width: 200, x: -100, x2: 100, y: 0, y2: 200 });
});
});
3 changes: 1 addition & 2 deletions __tests__/unit/path/get-point-at-length.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ describe('get point at length', () => {
it('should get point in rounded rect correctly', () => {
const segments = parsePathString('M2 0a2 2 0 00-2 2v12a2 2 0 002 2h12a2 2 0 002-2V2a2 2 0 00-2-2H2z') as PathArray;
const pt = getPointAtLength(segments, 25);
expect(pt).toEqual({ x: 8.716879289030931, y: 16 });
expect(pt).toEqual({ x: 8.723272341772404, y: 16 });
});

it('should get point in arc correctly', () => {
Expand All @@ -19,7 +19,6 @@ describe('get point at length', () => {

it('should get point in quad bezier correctly', () => {
const segments = parsePathString('M168 250 Q113 250 58 250') as PathArray;
console.log(segments);

let pt = getPointAtLength(segments, 0);
expect(pt).toEqual({ x: 168, y: 250 });
Expand Down
9 changes: 5 additions & 4 deletions __tests__/unit/path/get-total-length.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,22 @@ describe('get total length', () => {
it('should calc the length of circle correctly', () => {
const length = getTotalLength(getCirclePath(0, 0, 100, 100));

expect(length).toBeCloseTo(628.292692472827); // 2 * Math.PI * 100
expect(length).toBeCloseTo(625.7378601609234); // 2 * Math.PI * 100
});

it('should calc the length of rounded rect correctly', () => {
const length = getTotalLength(
parsePathString('M2 0a2 2 0 00-2 2v12a2 2 0 002 2h12a2 2 0 002-2V2a2 2 0 00-2-2H2z') as PathArray,
);
expect(length).toBeCloseTo(60.56635625960637);
expect(length).toBeCloseTo(60.55345531645519);
});

it('should calc the length of rounded rect correctly', () => {
const length = getTotalLength(
parsePathString('M2 0a2 2 0 00-2 2v12a2 2 0 002 2h12a2 2 0 002-2V2a2 2 0 00-2-2H2z') as PathArray,
);
expect(length).toBeCloseTo(60.56635625960637);

expect(length).toBeCloseTo(60.55345531645519);
});

it('should calc the length of Q commands correctly', () => {
Expand All @@ -53,6 +54,6 @@ describe('get total length', () => {
['Q', 25, 25, 10, 50],
];
const length = getTotalLength(reversed);
expect(length).toBeCloseTo(244.20588053509607);
expect(length).toBeCloseTo(243.8244865343346);
});
});
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
{
"name": "@antv/util",
"version": "3.2.3",
"version": "3.2.4",
"license": "MIT",
"sideEffects": false,
"main": "lib/index.js",
"module": "esm/index.js",
"unpkg": "dist/util.min.js",
"types": "lib/index.d.ts",
"files": [
"src",
Expand Down
8 changes: 1 addition & 7 deletions src/path/process/normalize-path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,9 @@ export function normalizePath(pathInput: string | PathArray): NormalArray {

const path = path2Absolute(pathInput);
const params = { ...paramsParser };
const allPathCommands = [];
const ii = path.length;
let pathCommand = '';

for (let i = 0; i < ii; i += 1) {
[pathCommand] = path[i];

for (let i = 0; i < path.length; i += 1) {
// Save current path command
allPathCommands[i] = pathCommand;
path[i] = normalizeSegment(path[i], params);

const segment = path[i];
Expand Down
5 changes: 5 additions & 0 deletions src/path/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@ export interface PathBBox {
export interface PathBBoxTotalLength extends PathBBox {
length: number;
}
export interface PathLengthFactoryOptions {
bbox: boolean;
length: boolean;
sampleSize: number;
}
export interface SegmentLimits {
min: Point;
max: Point;
Expand Down
1 change: 1 addition & 0 deletions src/path/util/equalize-segments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ function getCurveArray(segments: PathArray) {
segmentData[6],
segmentData[7],
segmentData[8],
{ bbox: false },
).length
: 0;

Expand Down
9 changes: 6 additions & 3 deletions src/path/util/get-path-bbox-total-length.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import type { PathArray, PathBBoxTotalLength } from '../types';
import type { PathArray, PathBBoxTotalLength, PathLengthFactoryOptions } from '../types';
import { pathLengthFactory } from './path-length-factory';

/**
* Returns the bounding box of a shape.
*/
export function getPathBBoxTotalLength(path: PathArray): PathBBoxTotalLength {
export function getPathBBoxTotalLength(
path: PathArray,
options?: Partial<PathLengthFactoryOptions>,
): PathBBoxTotalLength {
if (!path) {
return {
length: 0,
Expand All @@ -24,7 +27,7 @@ export function getPathBBoxTotalLength(path: PathArray): PathBBoxTotalLength {
length,
min: { x: xMin, y: yMin },
max: { x: xMax, y: yMax },
} = pathLengthFactory(path);
} = pathLengthFactory(path, undefined, { ...options, bbox: true, length: true });

const width = xMax - xMin;
const height = yMax - yMin;
Expand Down
6 changes: 3 additions & 3 deletions src/path/util/get-path-bbox.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { PathArray, PathBBox } from '../types';
import type { PathArray, PathBBox, PathLengthFactoryOptions } from '../types';
import { pathLengthFactory } from './path-length-factory';

/**
* Returns the bounding box of a shape.
*/
export function getPathBBox(path: string | PathArray): PathBBox {
export function getPathBBox(path: string | PathArray, options?: Partial<PathLengthFactoryOptions>): PathBBox {
if (!path) {
return {
x: 0,
Expand All @@ -22,7 +22,7 @@ export function getPathBBox(path: string | PathArray): PathBBox {
const {
min: { x: xMin, y: yMin },
max: { x: xMax, y: yMax },
} = pathLengthFactory(path);
} = pathLengthFactory(path, undefined, { ...options, length: false });

const width = xMax - xMin;
const height = yMax - yMin;
Expand Down
10 changes: 7 additions & 3 deletions src/path/util/get-point-at-length.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import type { PathArray } from '../types';
import type { PathArray, PathLengthFactoryOptions } from '../types';
import { pathLengthFactory } from './path-length-factory';

/**
* Returns [x,y] coordinates of a point at a given length of a shape.
*/
export function getPointAtLength(pathInput: string | PathArray, distance: number) {
return pathLengthFactory(pathInput, distance).point;
export function getPointAtLength(
pathInput: string | PathArray,
distance: number,
options?: Partial<PathLengthFactoryOptions>,
) {
return pathLengthFactory(pathInput, distance, { ...options, bbox: false, length: true }).point;
}
6 changes: 3 additions & 3 deletions src/path/util/get-total-length.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { PathArray } from '../types';
import type { PathArray, PathLengthFactoryOptions } from '../types';
import { pathLengthFactory } from './path-length-factory';

/**
Expand All @@ -7,6 +7,6 @@ import { pathLengthFactory } from './path-length-factory';
* The `normalizePath` version is lighter, faster, more efficient and more accurate
* with paths that are not `curveArray`.
*/
export function getTotalLength(pathInput: string | PathArray) {
return pathLengthFactory(pathInput).length;
export function getTotalLength(pathInput: string | PathArray, options?: Partial<PathLengthFactoryOptions>) {
return pathLengthFactory(pathInput, undefined, { ...options, bbox: false, length: true }).length;
}
11 changes: 9 additions & 2 deletions src/path/util/path-length-factory.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { normalizePath } from '../process/normalize-path';
import type { PathCommand, PathArray, LengthFactory } from '../types';
import type { PathCommand, PathArray, LengthFactory, PathLengthFactoryOptions } from '../types';
import { segmentLineFactory } from './segment-line-factory';
import { segmentArcFactory } from './segment-arc-factory';
import { segmentCubicFactory } from './segment-cubic-factory';
Expand All @@ -10,7 +10,11 @@ import { segmentQuadFactory } from './segment-quad-factory';
* of a shape, the shape total length and
* the shape minimum and maximum {x,y} coordinates.
*/
export function pathLengthFactory(pathInput: string | PathArray, distance?: number): LengthFactory {
export function pathLengthFactory(
pathInput: string | PathArray,
distance?: number,
options?: Partial<PathLengthFactoryOptions>,
): LengthFactory {
const path = normalizePath(pathInput);
const distanceIsNumber = typeof distance === 'number';
let isM: boolean;
Expand Down Expand Up @@ -62,6 +66,7 @@ export function pathLengthFactory(pathInput: string | PathArray, distance?: numb
data[7],
data[8],
(distance || 0) - LENGTH,
options || {},
));
} else if (pathCommand === 'C') {
({ length, min, max, point } = segmentCubicFactory(
Expand All @@ -74,6 +79,7 @@ export function pathLengthFactory(pathInput: string | PathArray, distance?: numb
data[6],
data[7],
(distance || 0) - LENGTH,
options || {},
));
} else if (pathCommand === 'Q') {
({ length, min, max, point } = segmentQuadFactory(
Expand All @@ -84,6 +90,7 @@ export function pathLengthFactory(pathInput: string | PathArray, distance?: numb
data[4],
data[5],
(distance || 0) - LENGTH,
options || {},
));
} else if (pathCommand === 'Z') {
data = [x, y, mx, my];
Expand Down
19 changes: 14 additions & 5 deletions src/path/util/segment-arc-factory.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Point, LengthFactory } from '../types';
import type { Point, LengthFactory, PathLengthFactoryOptions } from '../types';
import { segmentLineFactory } from './segment-line-factory';
import { distanceSquareRoot } from './distance-square-root';

Expand Down Expand Up @@ -120,6 +120,8 @@ function getPointAtArcSegmentLength(
/**
* Returns a {x,y} point at a given length, the total length and
* the shape minimum and maximum {x,y} coordinates of an A (arc-to) segment.
*
* For better performance, it can skip calculate bbox or length in some scenario.
*/
export function segmentArcFactory(
X1: number,
Expand All @@ -132,7 +134,9 @@ export function segmentArcFactory(
X2: number,
Y2: number,
distance: number,
options: Partial<PathLengthFactoryOptions>,
): LengthFactory {
const { bbox = true, length = true, sampleSize = 10 } = options;
const distanceIsNumber = typeof distance === 'number';
let x = X1;
let y = Y1;
Expand All @@ -147,14 +151,19 @@ export function segmentArcFactory(
POINT = { x, y };
}

// bad perf when size = 300
const sampleSize = 100;
// bad perf when size > 100
for (let j = 0; j <= sampleSize; j += 1) {
t = j / sampleSize;

({ x, y } = getPointAtArcSegmentLength(X1, Y1, RX, RY, angle, LAF, SF, X2, Y2, t));
POINTS = POINTS.concat({ x, y });
LENGTH += distanceSquareRoot(cur, [x, y]);

if (bbox) {
POINTS.push({ x, y });
}

if (length) {
LENGTH += distanceSquareRoot(cur, [x, y]);
}
cur = [x, y];

if (distanceIsNumber && LENGTH >= distance && distance > prev[2]) {
Expand Down
15 changes: 11 additions & 4 deletions src/path/util/segment-cubic-factory.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { LengthFactory } from '../types';
import type { LengthFactory, PathLengthFactoryOptions } from '../types';
import { distanceSquareRoot } from './distance-square-root';

/**
Expand Down Expand Up @@ -37,7 +37,9 @@ export function segmentCubicFactory(
x2: number,
y2: number,
distance: number,
options: Partial<PathLengthFactoryOptions>,
): LengthFactory {
const { bbox = true, length = true, sampleSize = 10 } = options;
const distanceIsNumber = typeof distance === 'number';
let x = x1;
let y = y1;
Expand All @@ -53,13 +55,18 @@ export function segmentCubicFactory(
}

// bad perf when size = 300
const sampleSize = 30;
for (let j = 0; j <= sampleSize; j += 1) {
t = j / sampleSize;

({ x, y } = getPointAtCubicSegmentLength(x1, y1, c1x, c1y, c2x, c2y, x2, y2, t));
POINTS = POINTS.concat({ x, y });
LENGTH += distanceSquareRoot(cur, [x, y]);

if (bbox) {
POINTS.push({ x, y });
}

if (length) {
LENGTH += distanceSquareRoot(cur, [x, y]);
}
cur = [x, y];

if (distanceIsNumber && LENGTH >= distance && distance > prev[2]) {
Expand Down
15 changes: 11 additions & 4 deletions src/path/util/segment-quad-factory.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { LengthFactory } from '../types';
import type { LengthFactory, PathLengthFactoryOptions } from '../types';
import { distanceSquareRoot } from './distance-square-root';

/**
Expand Down Expand Up @@ -35,7 +35,9 @@ export function segmentQuadFactory(
x2: number,
y2: number,
distance: number,
options: Partial<PathLengthFactoryOptions>,
): LengthFactory {
const { bbox = true, length = true, sampleSize = 10 } = options;
const distanceIsNumber = typeof distance === 'number';
let x = x1;
let y = y1;
Expand All @@ -50,13 +52,18 @@ export function segmentQuadFactory(
POINT = { x, y };
}

const sampleSize = 30;
for (let j = 0; j <= sampleSize; j += 1) {
t = j / sampleSize;

({ x, y } = getPointAtQuadSegmentLength(x1, y1, qx, qy, x2, y2, t));
POINTS = POINTS.concat({ x, y });
LENGTH += distanceSquareRoot(cur, [x, y]);

if (bbox) {
POINTS.push({ x, y });
}

if (length) {
LENGTH += distanceSquareRoot(cur, [x, y]);
}
cur = [x, y];

if (distanceIsNumber && LENGTH >= distance && distance > prev[2]) {
Expand Down