From a527d6d6cc9b7ed98b9a512f5934365e1edc7648 Mon Sep 17 00:00:00 2001 From: "yuqi.pyq" Date: Wed, 31 Aug 2022 15:17:07 +0800 Subject: [PATCH] fix: make segment size configurable when calculating path length & bbox --- __tests__/unit/path/get-path-bbox.spec.ts | 7 +++++++ .../unit/path/get-point-at-length.spec.ts | 3 +-- __tests__/unit/path/get-total-length.spec.ts | 9 +++++---- package.json | 3 ++- src/path/process/normalize-path.ts | 8 +------- src/path/types.ts | 5 +++++ src/path/util/equalize-segments.ts | 1 + src/path/util/get-path-bbox-total-length.ts | 9 ++++++--- src/path/util/get-path-bbox.ts | 6 +++--- src/path/util/get-point-at-length.ts | 10 +++++++--- src/path/util/get-total-length.ts | 6 +++--- src/path/util/path-length-factory.ts | 11 +++++++++-- src/path/util/segment-arc-factory.ts | 19 ++++++++++++++----- src/path/util/segment-cubic-factory.ts | 15 +++++++++++---- src/path/util/segment-quad-factory.ts | 15 +++++++++++---- 15 files changed, 86 insertions(+), 41 deletions(-) diff --git a/__tests__/unit/path/get-path-bbox.spec.ts b/__tests__/unit/path/get-path-bbox.spec.ts index b63c86f..1fe52d9 100644 --- a/__tests__/unit/path/get-path-bbox.spec.ts +++ b/__tests__/unit/path/get-path-bbox.spec.ts @@ -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 }); + }); }); diff --git a/__tests__/unit/path/get-point-at-length.spec.ts b/__tests__/unit/path/get-point-at-length.spec.ts index a534435..2c0e439 100644 --- a/__tests__/unit/path/get-point-at-length.spec.ts +++ b/__tests__/unit/path/get-point-at-length.spec.ts @@ -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', () => { @@ -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 }); diff --git a/__tests__/unit/path/get-total-length.spec.ts b/__tests__/unit/path/get-total-length.spec.ts index 16bc932..e8a1972 100644 --- a/__tests__/unit/path/get-total-length.spec.ts +++ b/__tests__/unit/path/get-total-length.spec.ts @@ -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', () => { @@ -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); }); }); diff --git a/package.json b/package.json index 2be4484..38b1514 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/path/process/normalize-path.ts b/src/path/process/normalize-path.ts index 4166555..af5ab22 100644 --- a/src/path/process/normalize-path.ts +++ b/src/path/process/normalize-path.ts @@ -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]; diff --git a/src/path/types.ts b/src/path/types.ts index 8e19f40..7aa32c7 100644 --- a/src/path/types.ts +++ b/src/path/types.ts @@ -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; diff --git a/src/path/util/equalize-segments.ts b/src/path/util/equalize-segments.ts index cf5b780..f68f351 100644 --- a/src/path/util/equalize-segments.ts +++ b/src/path/util/equalize-segments.ts @@ -42,6 +42,7 @@ function getCurveArray(segments: PathArray) { segmentData[6], segmentData[7], segmentData[8], + { bbox: false }, ).length : 0; diff --git a/src/path/util/get-path-bbox-total-length.ts b/src/path/util/get-path-bbox-total-length.ts index e6e4c8a..f32de43 100644 --- a/src/path/util/get-path-bbox-total-length.ts +++ b/src/path/util/get-path-bbox-total-length.ts @@ -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, +): PathBBoxTotalLength { if (!path) { return { length: 0, @@ -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; diff --git a/src/path/util/get-path-bbox.ts b/src/path/util/get-path-bbox.ts index 8552b98..adadcf9 100644 --- a/src/path/util/get-path-bbox.ts +++ b/src/path/util/get-path-bbox.ts @@ -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): PathBBox { if (!path) { return { x: 0, @@ -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; diff --git a/src/path/util/get-point-at-length.ts b/src/path/util/get-point-at-length.ts index acfca6a..6c58282 100644 --- a/src/path/util/get-point-at-length.ts +++ b/src/path/util/get-point-at-length.ts @@ -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, +) { + return pathLengthFactory(pathInput, distance, { ...options, bbox: false, length: true }).point; } diff --git a/src/path/util/get-total-length.ts b/src/path/util/get-total-length.ts index 42e9abb..51c1a73 100644 --- a/src/path/util/get-total-length.ts +++ b/src/path/util/get-total-length.ts @@ -1,4 +1,4 @@ -import type { PathArray } from '../types'; +import type { PathArray, PathLengthFactoryOptions } from '../types'; import { pathLengthFactory } from './path-length-factory'; /** @@ -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) { + return pathLengthFactory(pathInput, undefined, { ...options, bbox: false, length: true }).length; } diff --git a/src/path/util/path-length-factory.ts b/src/path/util/path-length-factory.ts index 3a40954..3811ec2 100644 --- a/src/path/util/path-length-factory.ts +++ b/src/path/util/path-length-factory.ts @@ -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'; @@ -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, +): LengthFactory { const path = normalizePath(pathInput); const distanceIsNumber = typeof distance === 'number'; let isM: boolean; @@ -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( @@ -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( @@ -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]; diff --git a/src/path/util/segment-arc-factory.ts b/src/path/util/segment-arc-factory.ts index ba14311..aca246d 100644 --- a/src/path/util/segment-arc-factory.ts +++ b/src/path/util/segment-arc-factory.ts @@ -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'; @@ -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, @@ -132,7 +134,9 @@ export function segmentArcFactory( X2: number, Y2: number, distance: number, + options: Partial, ): LengthFactory { + const { bbox = true, length = true, sampleSize = 10 } = options; const distanceIsNumber = typeof distance === 'number'; let x = X1; let y = Y1; @@ -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]) { diff --git a/src/path/util/segment-cubic-factory.ts b/src/path/util/segment-cubic-factory.ts index cfd0d34..74e1f4c 100644 --- a/src/path/util/segment-cubic-factory.ts +++ b/src/path/util/segment-cubic-factory.ts @@ -1,4 +1,4 @@ -import type { LengthFactory } from '../types'; +import type { LengthFactory, PathLengthFactoryOptions } from '../types'; import { distanceSquareRoot } from './distance-square-root'; /** @@ -37,7 +37,9 @@ export function segmentCubicFactory( x2: number, y2: number, distance: number, + options: Partial, ): LengthFactory { + const { bbox = true, length = true, sampleSize = 10 } = options; const distanceIsNumber = typeof distance === 'number'; let x = x1; let y = y1; @@ -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]) { diff --git a/src/path/util/segment-quad-factory.ts b/src/path/util/segment-quad-factory.ts index 9da66b0..dbd52b0 100644 --- a/src/path/util/segment-quad-factory.ts +++ b/src/path/util/segment-quad-factory.ts @@ -1,4 +1,4 @@ -import type { LengthFactory } from '../types'; +import type { LengthFactory, PathLengthFactoryOptions } from '../types'; import { distanceSquareRoot } from './distance-square-root'; /** @@ -35,7 +35,9 @@ export function segmentQuadFactory( x2: number, y2: number, distance: number, + options: Partial, ): LengthFactory { + const { bbox = true, length = true, sampleSize = 10 } = options; const distanceIsNumber = typeof distance === 'number'; let x = x1; let y = y1; @@ -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]) {