Skip to content
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
5 changes: 5 additions & 0 deletions src/SVGPathData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ export class SVGPathData extends TransformableSVG {
return this;
}

removeCollinear() {
this.commands = SVGPathDataTransformer.REMOVE_COLLINEAR(this.commands);
return this;
}

static encode(commands: SVGCommand[]) {
return encodeSVGPath(commands);
}
Expand Down
2 changes: 2 additions & 0 deletions src/SVGPathDataTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
type Point,
} from './mathUtils.js';
import { SVGPathData } from './SVGPathData.js';
import { REMOVE_COLLINEAR } from './transformers/remove_collinear.js';
import { REVERSE_PATH } from './transformers/reverse_path.js';
import type { SVGCommand, TransformFunction } from './types.js';

Expand Down Expand Up @@ -816,4 +817,5 @@ export const SVGPathDataTransformer = {
CLONE,
CALCULATE_BOUNDS,
REVERSE_PATH,
REMOVE_COLLINEAR,
};
75 changes: 75 additions & 0 deletions src/tests/remove_collinear.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { describe, test, expect } from '@jest/globals';

import { SVGPathData } from '../SVGPathData.js';

function testRemoveCollinear(path: string): string {
const pathData = new SVGPathData(path).removeCollinear();
return pathData.encode();
}

describe('Remove collinear points', () => {
test('Horizontal line with collinear point', () => {
expect(testRemoveCollinear('M10,10 L20,10 L30,10')).toEqual('M10 10L30 10');
});

test('Diagonal line with collinear point', () => {
expect(testRemoveCollinear('M10,10 L20,20 L30,30')).toEqual('M10 10L30 30');
});

test('Vertical line with collinear point', () => {
expect(testRemoveCollinear('M10,10 L10,20 L10,30')).toEqual('M10 10L10 30');
});

test('Multiple collinear sections', () => {
expect(
testRemoveCollinear(
'M10,10 L20,10 L30,10 L40,20 L50,30 L60,40 L60,50 L60,60',
),
).toEqual('M10 10L30 10L60 40L60 60');
});

test('Preserves curves', () => {
expect(
testRemoveCollinear('M10,10 C20,20 30,20 40,10 L50,10 L60,10'),
).toEqual('M10 10C20 20 30 20 40 10L60 10');
});

test('Preserves closed paths', () => {
expect(testRemoveCollinear('M10,10 L20,10 L30,10 L30,20 L10,20 Z')).toEqual(
'M10 10L30 10L30 20L10 20z',
);
});

test('Handles multiple subpaths', () => {
expect(
testRemoveCollinear('M10,10 L20,10 L30,10 M40,40 L50,40 L60,40'),
).toEqual('M10 10L30 10M40 40L60 40');
});

// Tests for relative paths
test('Relative horizontal line with collinear point', () => {
expect(testRemoveCollinear('m10,10 l10,0 l10,0')).toEqual('m10 10l20 0');
});

test('Relative diagonal line with collinear point', () => {
expect(testRemoveCollinear('m10,10 l10,10 l10,10')).toEqual('m10 10l20 20');
});

test('Relative vertical line with collinear point', () => {
expect(testRemoveCollinear('m10,10 l0,10 l0,10')).toEqual('m10 10l0 20');
});

test('Mixed relative and absolute commands', () => {
expect(testRemoveCollinear('M10,10 L20,10 l10,0 l10,0')).toEqual(
'M10 10l30 0',
);
});

test('Relative multiple collinear sections', () => {
expect(
testRemoveCollinear(
'm10,10 l10,0 l10,0 l10,10 l10,10 l10,10 l0,10 l0,10',
),
).toEqual('m10 10l20 0l30 30l0 20');
});
});
55 changes: 55 additions & 0 deletions src/transformers/remove_collinear.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { SVGPathData } from '../SVGPathData.js';
import { SVGPathDataTransformer } from '../index.js';
import { arePointsCollinear, type Point } from '../mathUtils.js';
import type { SVGCommand } from '../types.js';

/**
* Process a path and remove collinear points
* @param commands Array of SVG path commands to process (must be absolute)
* @returns New array with collinear points removed
*/
export function REMOVE_COLLINEAR(commands: SVGCommand[]): SVGCommand[] {
if (commands.length <= 2) return commands; // exit early if there are less than 3 points

const results: SVGCommand[] = [];

const points: Point[] = commands.map(
SVGPathDataTransformer.INFO((cmd, pXAbs, pYAbs) => {
// Calculate absolute coordinates and normlise HV
const isRelatve = 'relative' in cmd && cmd.relative;
return [
'x' in cmd ? cmd.x + (isRelatve ? pXAbs : 0) : pXAbs,
'y' in cmd ? cmd.y + (isRelatve ? pYAbs : 0) : pYAbs,
];
}),
);
Copy link
Contributor Author

@armano2 armano2 Mar 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i was thinking about using TO_ABS instead but i would also need to normalize HV, and this is easier

technically i could do all of this in single loop, by skipping first 2 points and removing3ht one and updating 2nd one, and i'm not sure if SVGPathDataTransformer.INFO is even needed in here


let prevPoint = points[0];
results.push(commands[0]); // always keep the first point

for (let i = 1; i < commands.length; i++) {
const cmd = commands[i];
const nextCmd = commands[i + 1];

if (
i < commands.length - 1 &&
nextCmd &&
cmd.type & SVGPathData.LINE_COMMANDS &&
nextCmd.type & SVGPathData.LINE_COMMANDS
) {
const nextPoint = points[i + 1];
// Check triplets of points for collinearity
if (arePointsCollinear(prevPoint, points[i], nextPoint)) {
// update next point if its relative
if ('relative' in nextCmd && nextCmd.relative) {
if ('x' in nextCmd) nextCmd.x = nextPoint[0] - prevPoint[0];
if ('y' in nextCmd) nextCmd.y = nextPoint[1] - prevPoint[1];
}
continue;
}
}
results.push(cmd);
prevPoint = points[i];
}
return results;
}