refactor: share trace, error boundary, and proxy http helpers
Lift the trace-to-shape geometry into @tts/mesh (traceToShape/ traceToUvBounds), parameterized by scale so the web token viewer and @tts/tabletop share one implementation. Move the CORS proxy HTTP helpers (assetUrl, resolveAssetUrl, traceImage) into a new @tts/http package, and make @tts/tabletop's ErrorBoundary the single source used by the web app. This removes the duplicated ErrorBoundary, assetUrl, tabletopHttp, and trace-to-shape code from apps/web and packages/tabletop.
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { resolveAssetUrl } from './asset.js';
|
||||
|
||||
describe('resolveAssetUrl', () => {
|
||||
it('passes absolute urls through unchanged', () => {
|
||||
expect(resolveAssetUrl('https://example.com/a.png', 'harbor/parts/')).toBe(
|
||||
'https://example.com/a.png',
|
||||
);
|
||||
expect(resolveAssetUrl('data:image/png;base64,AAAA', 'harbor/parts/')).toBe(
|
||||
'data:image/png;base64,AAAA',
|
||||
);
|
||||
});
|
||||
|
||||
it('resolves a relative path against baseUrl', () => {
|
||||
expect(resolveAssetUrl('./assets/tokens.png', 'harbor/parts/')).toBe(
|
||||
'harbor/parts/assets/tokens.png',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the relative path as-is without a baseUrl', () => {
|
||||
expect(resolveAssetUrl('./assets/tokens.png')).toBe('./assets/tokens.png');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Asset URL helpers for the host app's CORS-safe asset proxy (`/asset`).
|
||||
* Shared by the web viewers and `@tts/tabletop`, which both route external
|
||||
* assets (textures, models) through the proxy so three.js can load them.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolve a part's asset URL. Absolute URLs (http/https, data:, blob:) pass
|
||||
* through unchanged; relative paths are resolved against `baseUrl` — the
|
||||
* directory of the part's source file (the json/yaml/markdown that defined
|
||||
* it). Without a `baseUrl`, a relative path is returned as-is.
|
||||
*/
|
||||
export function resolveAssetUrl(url: string, baseUrl?: string): string {
|
||||
if (/^(https?:|data:|blob:)/i.test(url)) return url;
|
||||
if (!baseUrl) return url;
|
||||
// `baseUrl` is a path relative to the games root (no scheme), so join rather
|
||||
// than resolve as a URL. A fake origin lets `new URL` normalize `./` and
|
||||
// `../`; the origin is stripped from the result.
|
||||
const resolved = new URL(url, `http://localhost/${baseUrl}`).pathname;
|
||||
return resolved.replace(/^\/+/, '');
|
||||
}
|
||||
|
||||
/** Route an external asset URL through the proxy so three.js can load it. */
|
||||
export function assetUrl(url: string): string {
|
||||
return `/asset?url=${encodeURIComponent(url)}`;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { resolveAssetUrl, assetUrl } from './asset.js';
|
||||
export { traceImage } from './trace.js';
|
||||
export type { TraceResult } from '@tts/shared';
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Trace an image into a vector shape via the host app's proxy (`/trace`).
|
||||
* Shared by the web token viewer and `@tts/tabletop`, which both trace an
|
||||
* image's alpha channel into a silhouette for extrusion.
|
||||
*/
|
||||
import { deserialize } from 'bson';
|
||||
import type { TraceResult } from '@tts/shared';
|
||||
|
||||
/**
|
||||
* Trace an image into a vector shape, BSON-deserializing the proxy response.
|
||||
* `mode` controls how the region is derived (`alpha`, `bw`, `color`); a
|
||||
* non-zero `offset` insets (negative) or outsets (positive) the shape.
|
||||
*/
|
||||
export async function traceImage(
|
||||
url: string,
|
||||
mode: 'alpha' | 'bw' | 'color' = 'alpha',
|
||||
offset?: number,
|
||||
): Promise<TraceResult> {
|
||||
const params = new URLSearchParams({ url, mode });
|
||||
if (offset !== undefined) params.set('offset', String(offset));
|
||||
const res = await fetch(`/trace?${params.toString()}`);
|
||||
if (!res.ok) {
|
||||
const body = (await res.json().catch(() => null)) as { error?: string } | null;
|
||||
throw new Error(body?.error ?? `Trace failed (${res.status})`);
|
||||
}
|
||||
return deserialize(new Uint8Array(await res.arrayBuffer())) as TraceResult;
|
||||
}
|
||||
Reference in New Issue
Block a user