feat(proxy): add offset param to inset or outset traced shapes

Trace results can now be inset (negative) or outset (positive) by a
pixel amount. Offset the outline and holes in opposite directions with
clipper-lib's miter joins, then recombine with a boolean difference so
holes grow on inset and shrink on outset. Collapsed shapes return an
empty outline; a split outline keeps the largest ring. Document the
parameter and the new dependency.
This commit is contained in:
2026-08-08 15:37:27 +08:00
parent c7f4bd03fd
commit 9094ad58e0
12 changed files with 284 additions and 17 deletions
+44
View File
@@ -100,4 +100,48 @@ describe('trace route', () => {
expect(result.mode).toBe('bw');
expect(result.shape.outline.length).toBeGreaterThan(3);
});
it('applies an inset offset to the shape', async () => {
stubFetch(await makePng());
const res = await trace.request(
'/?url=https%3A%2F%2Fexample.com%2Fa.png&offset=-2',
);
expect(res.status).toBe(200);
const result = deserialize(new Uint8Array(await res.arrayBuffer()));
expect(result.offset).toBe(-2);
expect(result.shape.outline.length).toBeGreaterThan(3);
// An inset of 2px on a 20px circle must be visibly smaller.
const extent = (pts: number[][]) => [
Math.min(...pts.map((p) => p[0]!)),
Math.max(...pts.map((p) => p[0]!)),
];
const [minX, maxX] = extent(result.shape.outline);
expect((maxX ?? 0) - (minX ?? 0)).toBeLessThan(20);
});
it('applies an outset offset to the shape', async () => {
stubFetch(await makePng());
const res = await trace.request(
'/?url=https%3A%2F%2Fexample.com%2Fa.png&offset=2',
);
expect(res.status).toBe(200);
const result = deserialize(new Uint8Array(await res.arrayBuffer()));
expect(result.offset).toBe(2);
expect(result.shape.outline.length).toBeGreaterThan(3);
});
it('rejects a non-numeric offset', async () => {
stubFetch(await makePng());
const res = await trace.request(
'/?url=https%3A%2F%2Fexample.com%2Fa.png&offset=abc',
);
expect(res.status).toBe(400);
});
it('omits offset when not requested', async () => {
stubFetch(await makePng());
const res = await trace.request('/?url=https%3A%2F%2Fexample.com%2Fa.png');
const result = deserialize(new Uint8Array(await res.arrayBuffer()));
expect(result.offset).toBeUndefined();
});
});