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
120 changes: 120 additions & 0 deletions src/SVGShapes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { SVGPathData } from './SVGPathData.js';
import type { SVGCommand } from './types.js';

function moveTo(x: number, y: number): SVGCommand {
return { type: SVGPathData.MOVE_TO, relative: false, x, y };
}

function lineTo(x: number, y: number): SVGCommand {
return { type: SVGPathData.LINE_TO, relative: false, x, y };
}

function arcTo(
rx: number,
ry: number,
xRot: number,
largeArc: 0 | 1,
sweep: 0 | 1,
x: number,
y: number,
): SVGCommand {
return {
type: SVGPathData.ARC,
relative: false,
rX: rx,
rY: ry,
xRot,
lArcFlag: largeArc,
sweepFlag: sweep,
x,
y,
};
}

/**
* Creates an ellipse path centered at (cx,cy) with radii rx and ry
*/
function createEllipse(
rx: number,
ry: number,
cx: number,
cy: number,
): SVGPathData {
return new SVGPathData([
moveTo(cx + rx, cy),
arcTo(rx, ry, 0, 1, 1, cx - rx, cy),
arcTo(rx, ry, 0, 1, 1, cx + rx, cy),
{ type: SVGPathData.CLOSE_PATH },
]);
}

/**
* Creates a rectangle path with optional rounded corners
*/
function createRect(
x: number,
y: number,
width: number,
height: number,
rX = 0,
rY = 0,
): SVGPathData {
if (rX === 0 || rY === 0) {
return new SVGPathData([
moveTo(x, y),
lineTo(x + width, y),
lineTo(x + width, y + height),
lineTo(x, y + height),
{ type: SVGPathData.CLOSE_PATH },
]);
}

const rx = Math.min(rX, width / 2);
const ry = Math.min(rY, height / 2);

return new SVGPathData([
moveTo(x + rx, y),
lineTo(x + width - rx, y),
arcTo(rx, ry, 0, 0, 1, x + width, y + ry),
lineTo(x + width, y + height - ry),
arcTo(rx, ry, 0, 0, 1, x + width - rx, y + height),
lineTo(x + rx, y + height),
arcTo(rx, ry, 0, 0, 1, x, y + height - ry),
lineTo(x, y + ry),
arcTo(rx, ry, 0, 0, 1, x + rx, y),
{ type: SVGPathData.CLOSE_PATH },
]);
}

/**
* Creates a polyline from an array of coordinates [x1,y1,x2,y2,...]
*/
function createPolyline(coords: number[]): SVGPathData {
if (coords.length < 2) return new SVGPathData([]);

const commands: SVGCommand[] = [moveTo(coords[0], coords[1])];

for (let i = 2; i < coords.length; i += 2) {
commands.push(lineTo(coords[i], coords[i + 1]));
}

return new SVGPathData(commands);
}

/**
* Creates a closed polygon from an array of coordinates
*/
function createPolygon(coords: number[]): SVGPathData {
if (coords.length < 2) return new SVGPathData([]);

const commands = createPolyline(coords).commands;
commands.push({ type: SVGPathData.CLOSE_PATH });
return new SVGPathData(commands);
}

export const SVGShapes = {
createEllipse,
createRect,
createPolyline,
createPolygon,
};
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export type * from './types.js';
export { encodeSVGPath } from './SVGPathDataEncoder.js';
export { SVGPathDataParser } from './SVGPathDataParser.js';
export { SVGPathDataTransformer } from './SVGPathDataTransformer.js';
export { SVGShapes } from './SVGShapes.js';
75 changes: 75 additions & 0 deletions src/tests/shapes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { describe, test, expect } from '@jest/globals';
import { SVGShapes } from '../index.js';

describe('SVGShapes', () => {
describe('createEllipse', () => {
test('should create a valid ellipse path', () => {
const ellipse = SVGShapes.createEllipse(100, 50, 200, 150);
expect(ellipse.encode()).toBe(
'M300 150A100 50 0 1 1 100 150A100 50 0 1 1 300 150z',
);
});

test('should create a circle when rx equals ry', () => {
const circle = SVGShapes.createEllipse(50, 50, 100, 100);
expect(circle.encode()).toBe(
'M150 100A50 50 0 1 1 50 100A50 50 0 1 1 150 100z',
);
});
});

describe('createRect', () => {
test('should create a simple rectangle without rounded corners', () => {
const rect = SVGShapes.createRect(10, 20, 200, 100);
expect(rect.encode()).toBe('M10 20L210 20L210 120L10 120z');
});

test('should create a rectangle with rounded corners', () => {
const roundedRect = SVGShapes.createRect(10, 20, 200, 100, 15, 10);
expect(roundedRect.encode()).toBe(
'M25 20L195 20A15 10 0 0 1 210 30L210 110A15 10 0 0 1 195 120L25 120A15 10 0 0 1 10 110L10 30A15 10 0 0 1 25 20z',
);
});

test('should cap radius to half width/height if too large', () => {
const rect = SVGShapes.createRect(10, 20, 100, 60, 60, 40);
expect(rect.encode()).toBe(
'M60 20L60 20A50 30 0 0 1 110 50L110 50A50 30 0 0 1 60 80L60 80A50 30 0 0 1 10 50L10 50A50 30 0 0 1 60 20z',
);
});
});

describe('createPolyline', () => {
test('should create a valid polyline from coordinates', () => {
const polyline = SVGShapes.createPolyline([10, 20, 30, 40, 50, 10]);
expect(polyline.encode()).toBe('M10 20L30 40L50 10');
});

test('should create a valid polyline with exactly 2 points', () => {
const polyline = SVGShapes.createPolyline([10, 20, 30, 40]);
expect(polyline.encode()).toBe('M10 20L30 40');
});

test('should return empty path data for insufficient coordinates', () => {
const polyline = SVGShapes.createPolyline([10]);
expect(polyline.encode()).toBe('');
});
});

describe('createPolygon', () => {
test('should create a closed polygon from coordinates', () => {
const polygon = SVGShapes.createPolygon([10, 20, 30, 40, 50, 10]);
expect(polygon.encode()).toBe('M10 20L30 40L50 10z');
});

test('should create a valid polygon with exactly 2 points', () => {
const polygon = SVGShapes.createPolygon([10, 20, 30, 40]);
expect(polygon.encode()).toBe('M10 20L30 40z');
});

test('should return empty path data for insufficient coordinates', () => {
const polygon = SVGShapes.createPolygon([10]);
expect(polygon.encode()).toBe('');
});
});
});