feat(proxy): add image tracing endpoint
Add GET /trace which fetches an image, traces it into a vector shape, and returns the result BSON-encoded. Tracing is configurable via mode (alpha, bw, color) and format (shape, svg). The shape format parses the vtracer SVG into an outline/holes polygon matching @tts/mesh's Shape interface.
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
itemIdSchema,
|
||||
searchQuerySchema,
|
||||
searchResultSchema,
|
||||
traceRequestSchema,
|
||||
workshopItemSchema,
|
||||
} from './schemas.js';
|
||||
|
||||
@@ -132,4 +133,56 @@ describe('searchResultSchema', () => {
|
||||
const result = { items: [], page: 1, hasMore: false };
|
||||
expect(searchResultSchema.parse(result)).toEqual(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('traceRequestSchema', () => {
|
||||
it('defaults mode, threshold, and format', () => {
|
||||
expect(traceRequestSchema.parse({ url: 'https://example.com/a.png' })).toEqual({
|
||||
url: 'https://example.com/a.png',
|
||||
mode: 'alpha',
|
||||
threshold: 128,
|
||||
format: 'shape',
|
||||
});
|
||||
});
|
||||
|
||||
it('coerces threshold and accepts optional params', () => {
|
||||
expect(
|
||||
traceRequestSchema.parse({
|
||||
url: 'https://example.com/a.png',
|
||||
mode: 'bw',
|
||||
threshold: '200',
|
||||
format: 'svg',
|
||||
simplify: 1.5,
|
||||
maxColors: '8',
|
||||
}),
|
||||
).toEqual({
|
||||
url: 'https://example.com/a.png',
|
||||
mode: 'bw',
|
||||
threshold: 200,
|
||||
format: 'svg',
|
||||
simplify: 1.5,
|
||||
maxColors: 8,
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects an invalid url', () => {
|
||||
expect(traceRequestSchema.safeParse({ url: 'not-a-url' }).success).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects an unknown mode or format', () => {
|
||||
expect(
|
||||
traceRequestSchema.safeParse({
|
||||
url: 'https://example.com/a.png',
|
||||
mode: 'nope',
|
||||
}).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
traceRequestSchema.safeParse({
|
||||
url: 'https://example.com/a.png',
|
||||
format: 'nope',
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -51,4 +51,18 @@ export const searchResultSchema = z.object({
|
||||
items: z.array(workshopItemSchema),
|
||||
page: z.number(),
|
||||
hasMore: z.boolean(),
|
||||
});
|
||||
|
||||
export const traceModeSchema = z.enum(['alpha', 'bw', 'color']);
|
||||
|
||||
export const traceFormatSchema = z.enum(['svg', 'shape']);
|
||||
|
||||
/** Query params for `GET /trace`. */
|
||||
export const traceRequestSchema = z.object({
|
||||
url: z.string().url('url must be a valid URL'),
|
||||
mode: traceModeSchema.default('alpha'),
|
||||
threshold: z.coerce.number().int().min(0).max(255).default(128),
|
||||
format: traceFormatSchema.default('shape'),
|
||||
simplify: z.coerce.number().min(0).optional(),
|
||||
maxColors: z.coerce.number().int().min(1).optional(),
|
||||
});
|
||||
@@ -109,4 +109,33 @@ export interface ExtractedObject {
|
||||
parentGuid?: string;
|
||||
childrenGuids: string[];
|
||||
refs: AssetRef[];
|
||||
}
|
||||
|
||||
/** How to derive the traced region from the source image. */
|
||||
export type TraceMode = 'alpha' | 'bw' | 'color';
|
||||
|
||||
/** Output format for a traced result. */
|
||||
export type TraceFormat = 'svg' | 'shape';
|
||||
|
||||
/**
|
||||
* A closed 2D polygon described by its outline, matching the `Shape`
|
||||
* interface in `@tts/mesh` so a traced result can be extruded directly.
|
||||
*/
|
||||
export interface TracedShape {
|
||||
/** Outline vertices in order, each `[x, y]`. */
|
||||
outline: number[][];
|
||||
/** Optional holes, each a list of `[x, y]`. */
|
||||
holes?: number[][][];
|
||||
}
|
||||
|
||||
/** A traced image, BSON-serialized by the proxy. */
|
||||
export interface TraceResult {
|
||||
width: number;
|
||||
height: number;
|
||||
mode: TraceMode;
|
||||
format: TraceFormat;
|
||||
/** Raw SVG produced by vtracer, kept as a reference. */
|
||||
svg: string;
|
||||
/** Parsed shape, present when `format` is `shape`. */
|
||||
shape?: TracedShape;
|
||||
}
|
||||
Reference in New Issue
Block a user