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
+4 -3
View File
@@ -86,6 +86,7 @@ apps/web ──► apps/proxy ──► packages/tts ──► packages/shared
| `@hono/cors` | CORS middleware | `apps/proxy` |
| `bson` | BSON deserialization of TTS save files | `packages/tts` |
| `@visioncortex/vtracer` | Raster-to-SVG vectorization (wasm) | `apps/proxy` |
| `clipper-lib` | Polygon offsetting (inset/outset) for traced shapes | `apps/proxy` |
| `sharp` | Image decoding to RGBA | `apps/proxy` |
| `svgpath` | SVG path parsing for traced shapes | `apps/proxy` |
| `cheerio` | Workshop browse page scraping | `apps/proxy` |
@@ -101,9 +102,9 @@ apps/web ──► apps/proxy ──► packages/tts ──► packages/shared
must stay isomorphic.
- **`bson` is Node-only** — used by the fetcher and the trace route, not the
analysis layer.
- **`@visioncortex/vtracer`, `sharp`, `svgpath` are backend-only** — the trace
route lives in `apps/proxy`; they never appear in `packages/extract`, which
must stay isomorphic.
- **`@visioncortex/vtracer`, `sharp`, `svgpath`, `clipper-lib` are
backend-only** — the trace route lives in `apps/proxy`; they never appear in
`packages/extract`, which must stay isomorphic.
- **`packages/extract` has zero external runtime deps** — it relies only on
platform `fetch` / `Blob`, keeping it portable to a future frontend.
+9 -4
View File
@@ -164,9 +164,11 @@ CORS failures on common Workshop hosts would break the viewers.
**Decision:** The proxy exposes `GET /trace?url=...`, which fetches an image,
traces it into a vector shape, and returns the result BSON-encoded. Tracing is
configurable: `mode` (`alpha` default, `bw`, `color`) selects how the region is
derived, and `format` (`shape` default, `svg`) selects the response. The
`shape` format returns a parsed `{ outline, holes }` polygon matching
`@tts/mesh`'s `Shape` interface.
derived, `format` (`shape` default, `svg`) selects the response, and `offset`
(optional, in pixels) insets (negative) or outsets (positive) the resulting
shape — used by the token viewer to shave the anti-aliased fringe off a traced
silhouette. The `shape` format returns a parsed `{ outline, holes }` polygon
matching `@tts/mesh`'s `Shape` interface.
**Context:** The user wants to build a mesh from an image (e.g. a token or tile
art) using `@tts/mesh`. vtracer only returns SVG, but the mesh package consumes
@@ -178,4 +180,7 @@ feeding vtracer's `convertPixels`.
**Alternatives considered:** Returning only the raw SVG and parsing in the web
app near `@tts/mesh`; tracing by color only (no alpha). Rejected — server-side
parsing yields a shape the mesh package can consume directly, and alpha-based
tracing (the default) is the common case for token/tile art.
tracing (the default) is the common case for token/tile art. For shape
inset/outset, `clipper-lib` (Angus Johnson's Clipper ported to JS) was chosen
over the `polygon-offset` package because the latter crashes on degenerate
cases (collapse, hole closure) via a bug in its pinned Martinez dependency.
+13 -6
View File
@@ -241,16 +241,23 @@ Hono server exposing search + fetch.
headers, which would block three.js loaders in the browser; routing through
the proxy makes those assets loadable. Only `http(s)` URLs are allowed.
- `routes/trace.ts`
- `GET /trace?url=...&mode=alpha&threshold=128&format=shape` — fetch an
image, trace it into a vector shape, and return the result BSON-encoded.
`mode` is `alpha` (default), `bw`, or `color`; `format` is `shape`
(default) or `svg`. `shape` returns a parsed `{ outline, holes }` polygon
matching `@tts/mesh`'s `Shape` interface, ready to extrude.
- `GET /trace?url=...&mode=alpha&threshold=128&format=shape&offset=...` —
fetch an image, trace it into a vector shape, and return the result
BSON-encoded. `mode` is `alpha` (default), `bw`, or `color`; `format` is
`shape` (default) or `svg`; `offset` (optional) insets (negative) or
outsets (positive) the shape in pixels. `shape` returns a parsed
`{ outline, holes }` polygon matching `@tts/mesh`'s `Shape` interface,
ready to extrude.
- `routes/svgShape.ts`
- `parseSvgShape(svg)` — parse a vtracer SVG into a `TracedShape`
(`{ outline, holes }`): flatten beziers to polylines, split subpaths into
rings, classify by winding (CCW outline / CW hole), and assign holes to
their containing outline.
- `offsetShape(shape, delta)` — inset/outset a `TracedShape` via
`clipper-lib` (Clipper miter joins); outline and holes offset in opposite
directions and are recombined 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.
- `routes/health.ts`
- `GET /health` — liveness.
- `env.ts` — zod validation of `STEAM_API_KEY`, `PORT`.
@@ -309,7 +316,7 @@ proxy API and `packages/extract` directly for analysis.
| GET | `/items/:id` | Full parsed `TTSMod` (`?fileUrl=` skips key) | key* |
| GET | `/items/:id/file` | Raw save bytes, filename from header | key* |
| GET | `/asset?url=` | CORS-safe proxy for external assets | — |
| GET | `/trace?url=&mode=&format=` | Trace an image into a vector shape (BSON) | — |
| GET | `/trace?url=&mode=&format=&offset=` | Trace an image into a vector shape (BSON) | — |
\* `STEAM_API_KEY` is optional; `/items/*` works without it when a `fileUrl`
query param is supplied.