gputex 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -9
- package/dist/index.d.ts +24 -1
- package/dist/index.js +123 -0
- package/dist/three.d.ts +22 -3
- package/dist/three.js +167 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# GPUtex | On-the-fly GPU texture encoding
|
|
2
2
|
|
|
3
|
-
Runtime GPU texture compression via WebGPU compute shaders, with a WebGL2 fragment-shader fallback. Feed it a PNG/JPG/WebP/AVIF and get back a GPU-compressed texture (BC7, BC5, ASTC 4x4, or BC1) ready for Three.js or React Three Fiber.
|
|
3
|
+
Runtime GPU texture compression via WebGPU compute shaders, with a WebGL2 fragment-shader fallback. Feed it a PNG/JPG/WebP/AVIF — or an SVG, rasterised on the fly — and get back a GPU-compressed texture (BC7, BC5, ASTC 4x4, or BC1) ready for Three.js or React Three Fiber.
|
|
4
4
|
|
|
5
5
|
⚠️ 100% vibe-coded. The code is completely unreviewed and under-tested. Do not use for anything important.
|
|
6
6
|
|
|
@@ -93,6 +93,32 @@ material.map = texture
|
|
|
93
93
|
(byte-identical on >96% of blocks; the rest are equal-error FP tie-breaks,
|
|
94
94
|
enforced by the GPU test suite).
|
|
95
95
|
|
|
96
|
+
#### SVG sources
|
|
97
|
+
|
|
98
|
+
SVGs work anywhere a raster image does — as a URL, a Blob/File, an inline
|
|
99
|
+
markup string (detected by a leading `<`), or an `<img>` element. The vector
|
|
100
|
+
is rasterised before encoding, at the SVG's intrinsic size by default
|
|
101
|
+
(absolute `width`/`height` attributes, else the `viewBox` dimensions). Use
|
|
102
|
+
`svgSize` to pick the raster size — the browser renders the vector directly
|
|
103
|
+
at that size, so upscaling stays crisp:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
// Longest side 1024, aspect ratio preserved:
|
|
107
|
+
const { texture } = await compressTexture('/logo.svg', { svgSize: 1024 })
|
|
108
|
+
|
|
109
|
+
// Exact size (aspect mismatches follow the SVG's preserveAspectRatio rules):
|
|
110
|
+
await compressTexture('/icon.svg', { svgSize: { width: 512, height: 512 } })
|
|
111
|
+
|
|
112
|
+
// Inline markup:
|
|
113
|
+
await compressTexture('<svg viewBox="0 0 32 32">…</svg>', { svgSize: 256 })
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
An SVG with no `width`/`height` **and** no `viewBox` has no intrinsic size;
|
|
117
|
+
`svgSize` is required for those. Rasterisation needs a DOM `Image`, so SVG
|
|
118
|
+
sources are main-thread only. Non-Three.js users get the same rasteriser as
|
|
119
|
+
a standalone helper: `rasterizeSvg(source, { size })` from the core `gputex`
|
|
120
|
+
entry returns an `ImageBitmap` ready for `encodeToBytes()`.
|
|
121
|
+
|
|
96
122
|
### `GputexLoader` — Three.js Loader
|
|
97
123
|
|
|
98
124
|
```ts
|
|
@@ -211,14 +237,15 @@ const tex = buildCompressedTexture([bytes], TextureFormat.BC7_SRGB)
|
|
|
211
237
|
|
|
212
238
|
### `compressTexture` options
|
|
213
239
|
|
|
214
|
-
| Option | Type
|
|
215
|
-
| ----------------- |
|
|
216
|
-
| `hint` | `TextureHint`
|
|
217
|
-
| `preferredFormat` | `'bc1'`
|
|
218
|
-
| `colorSpace` | `'srgb' \| 'linear'`
|
|
219
|
-
| `
|
|
220
|
-
| `
|
|
221
|
-
| `
|
|
240
|
+
| Option | Type | Default | Description |
|
|
241
|
+
| ----------------- | ----------------------------- | --------- | ------------------------------------------------------------------------------------------------ |
|
|
242
|
+
| `hint` | `TextureHint` | `'color'` | `'color'`, `'colorWithAlpha'`, or `'normal'` |
|
|
243
|
+
| `preferredFormat` | `'bc1'` | — | Prefer BC1 (half of BC7's size) when supported; normal selection otherwise. `hint: 'color'` only |
|
|
244
|
+
| `colorSpace` | `'srgb' \| 'linear'` | `'srgb'` | Use the sRGB or linear variant of the chosen format |
|
|
245
|
+
| `svgSize` | `number \| { width, height }` | intrinsic | Raster size for SVG sources: longest side (aspect preserved) or exact size |
|
|
246
|
+
| `flipY` | `boolean` | `true` | Flip vertically (matches Three.js convention) |
|
|
247
|
+
| `mipmaps` | `boolean` | `false` | Generate full mip chain down to 1x1 |
|
|
248
|
+
| `device` | `GPUDevice` | — | Reuse an existing WebGPU device instead of creating one |
|
|
222
249
|
|
|
223
250
|
## Benchmarks
|
|
224
251
|
|
package/dist/index.d.ts
CHANGED
|
@@ -440,4 +440,27 @@ declare function generateMipChain(level0: MipLevel): MipLevel[];
|
|
|
440
440
|
*/
|
|
441
441
|
declare function padToBlockMultiple(level: MipLevel): MipLevel;
|
|
442
442
|
|
|
443
|
-
|
|
443
|
+
/**
|
|
444
|
+
* Target raster size for an SVG source. A number scales the SVG so its
|
|
445
|
+
* longest side matches (aspect ratio preserved); an object rasterises at
|
|
446
|
+
* exactly that size. When omitted, the SVG's intrinsic size is used
|
|
447
|
+
* (absolute width/height attributes, else the viewBox dimensions).
|
|
448
|
+
*/
|
|
449
|
+
type SvgRasterSize = number | {
|
|
450
|
+
width: number;
|
|
451
|
+
height: number;
|
|
452
|
+
};
|
|
453
|
+
interface RasterizeSvgOptions {
|
|
454
|
+
/** Target raster size. Default: the SVG's intrinsic size. */
|
|
455
|
+
size?: SvgRasterSize;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Rasterise an SVG (markup string or Blob/File) to an `ImageBitmap`.
|
|
459
|
+
*
|
|
460
|
+
* Used automatically by `compressTexture()` for SVG sources; exported for
|
|
461
|
+
* callers driving the core encoders directly — the returned bitmap is a
|
|
462
|
+
* valid `EncoderImageSource`. Main-thread only (needs `Image`).
|
|
463
|
+
*/
|
|
464
|
+
declare function rasterizeSvg(source: string | Blob, options?: RasterizeSvgOptions): Promise<ImageBitmap>;
|
|
465
|
+
|
|
466
|
+
export { ASTC4x4Encoder, ASTC4x4WebGLEncoder, BC1Encoder, BC1WebGLEncoder, BC5Encoder, BC5WebGLEncoder, BC7Encoder, BC7WebGLEncoder, type Capabilities, type EncodeBytesResult, type EncodeCallOptions, type EncodeQuality, Encoder, type EncoderConstructor, type EncoderImageSource, type EncoderOptions, type ExtensionProvider, type FeatureProvider, type FormatSelection, type FormatVariant, type MipLevel, type PreferredFormat, type RasterizeSvgOptions, type RawPixelSource, type SelectFormatOptions, type SvgRasterSize, TextureFormat, type TextureHint, WebGLBlockEncoder, type WebGLCapabilities, type WebGLEncodeBytesResult, type WebGLEncoderConstructor, type WebGLEncoderImageSource, type WebGLEncoderOptions, type WebGLFormatSelection, WebGPUFeature, createWebGLContext, detectCapabilities, detectWebGLCapabilities, generateMipChain, getSharedWebGLContext, isWebGLAvailable, padToBlockMultiple, rasterizeSvg, selectFormat, selectWebGLFormat };
|
package/dist/index.js
CHANGED
|
@@ -1441,6 +1441,128 @@ function padToBlockMultiple(level) {
|
|
|
1441
1441
|
}
|
|
1442
1442
|
return { data: out, width: pw, height: ph };
|
|
1443
1443
|
}
|
|
1444
|
+
|
|
1445
|
+
// src/svg.ts
|
|
1446
|
+
var ROOT_TAG_RE = /<svg(?=[\s/>])[^>]*>/;
|
|
1447
|
+
function getAttr(tag, name) {
|
|
1448
|
+
const m = new RegExp(`\\s${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`).exec(tag);
|
|
1449
|
+
return m ? m[1] ?? m[2] ?? "" : null;
|
|
1450
|
+
}
|
|
1451
|
+
function removeAttr(tag, name) {
|
|
1452
|
+
return tag.replace(new RegExp(`\\s${name}\\s*=\\s*(?:"[^"]*"|'[^']*')`, "g"), "");
|
|
1453
|
+
}
|
|
1454
|
+
function parseAbsoluteLength(value) {
|
|
1455
|
+
if (value == null) return null;
|
|
1456
|
+
const m = /^\s*\+?(\d+(?:\.\d+)?|\.\d+)(?:px)?\s*$/i.exec(value);
|
|
1457
|
+
if (!m) return null;
|
|
1458
|
+
const n = Number(m[1]);
|
|
1459
|
+
return n > 0 ? n : null;
|
|
1460
|
+
}
|
|
1461
|
+
function parseSvgDimensions(svgText) {
|
|
1462
|
+
const m = ROOT_TAG_RE.exec(svgText);
|
|
1463
|
+
if (!m) return null;
|
|
1464
|
+
const tag = m[0];
|
|
1465
|
+
const dims = {
|
|
1466
|
+
width: parseAbsoluteLength(getAttr(tag, "width")),
|
|
1467
|
+
height: parseAbsoluteLength(getAttr(tag, "height")),
|
|
1468
|
+
viewBoxWidth: null,
|
|
1469
|
+
viewBoxHeight: null
|
|
1470
|
+
};
|
|
1471
|
+
const viewBox = getAttr(tag, "viewBox");
|
|
1472
|
+
if (viewBox) {
|
|
1473
|
+
const parts = viewBox.trim().split(/[\s,]+/).map(Number);
|
|
1474
|
+
if (parts.length === 4 && parts.every(Number.isFinite) && parts[2] > 0 && parts[3] > 0) {
|
|
1475
|
+
dims.viewBoxWidth = parts[2];
|
|
1476
|
+
dims.viewBoxHeight = parts[3];
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
return dims;
|
|
1480
|
+
}
|
|
1481
|
+
function resolveSvgRasterSize(dims, size) {
|
|
1482
|
+
if (size !== void 0 && typeof size === "object") {
|
|
1483
|
+
const width = Math.round(size.width);
|
|
1484
|
+
const height = Math.round(size.height);
|
|
1485
|
+
if (!(width >= 1) || !(height >= 1)) {
|
|
1486
|
+
throw new Error(`rasterizeSvg: svgSize must be \u22651\xD71 (got ${size.width}\xD7${size.height})`);
|
|
1487
|
+
}
|
|
1488
|
+
return { width, height };
|
|
1489
|
+
}
|
|
1490
|
+
let w = dims.width;
|
|
1491
|
+
let h = dims.height;
|
|
1492
|
+
if (dims.viewBoxWidth != null && dims.viewBoxHeight != null) {
|
|
1493
|
+
if (w == null && h != null) w = h * dims.viewBoxWidth / dims.viewBoxHeight;
|
|
1494
|
+
else if (h == null && w != null) h = w * dims.viewBoxHeight / dims.viewBoxWidth;
|
|
1495
|
+
else if (w == null && h == null) {
|
|
1496
|
+
w = dims.viewBoxWidth;
|
|
1497
|
+
h = dims.viewBoxHeight;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
if (typeof size === "number") {
|
|
1501
|
+
if (!(size >= 1)) {
|
|
1502
|
+
throw new Error(`rasterizeSvg: svgSize must be \u22651 (got ${size})`);
|
|
1503
|
+
}
|
|
1504
|
+
const aspect = w != null && h != null ? w / h : 1;
|
|
1505
|
+
return aspect >= 1 ? { width: Math.round(size), height: Math.max(1, Math.round(size / aspect)) } : { width: Math.max(1, Math.round(size * aspect)), height: Math.round(size) };
|
|
1506
|
+
}
|
|
1507
|
+
if (w == null || h == null) {
|
|
1508
|
+
throw new Error(
|
|
1509
|
+
"rasterizeSvg: the SVG has no intrinsic size (no absolute width/height attributes and no viewBox) \u2014 pass svgSize to choose a rasterisation size"
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
return { width: Math.max(1, Math.round(w)), height: Math.max(1, Math.round(h)) };
|
|
1513
|
+
}
|
|
1514
|
+
function setSvgRootSize(svgText, width, height) {
|
|
1515
|
+
const m = ROOT_TAG_RE.exec(svgText);
|
|
1516
|
+
if (!m) {
|
|
1517
|
+
throw new Error("rasterizeSvg: no <svg> root element found in source");
|
|
1518
|
+
}
|
|
1519
|
+
let tag = m[0];
|
|
1520
|
+
const origWidth = parseAbsoluteLength(getAttr(tag, "width"));
|
|
1521
|
+
const origHeight = parseAbsoluteLength(getAttr(tag, "height"));
|
|
1522
|
+
const hasViewBox = getAttr(tag, "viewBox") != null;
|
|
1523
|
+
tag = removeAttr(removeAttr(tag, "width"), "height");
|
|
1524
|
+
let inject = ` width="${width}" height="${height}"`;
|
|
1525
|
+
if (!hasViewBox && origWidth != null && origHeight != null) {
|
|
1526
|
+
inject += ` viewBox="0 0 ${origWidth} ${origHeight}"`;
|
|
1527
|
+
}
|
|
1528
|
+
tag = `<svg${inject}${tag.slice("<svg".length)}`;
|
|
1529
|
+
return svgText.slice(0, m.index) + tag + svgText.slice(m.index + m[0].length);
|
|
1530
|
+
}
|
|
1531
|
+
async function rasterizeSvg(source, options = {}) {
|
|
1532
|
+
if (typeof Image === "undefined") {
|
|
1533
|
+
throw new Error(
|
|
1534
|
+
"rasterizeSvg: SVG rasterisation needs a DOM Image element and cannot run in this environment (e.g. a worker)"
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
const svgText = typeof source === "string" ? source : await source.text();
|
|
1538
|
+
const dims = parseSvgDimensions(svgText);
|
|
1539
|
+
if (!dims) {
|
|
1540
|
+
throw new Error("rasterizeSvg: no <svg> root element found in source");
|
|
1541
|
+
}
|
|
1542
|
+
const { width, height } = resolveSvgRasterSize(dims, options.size);
|
|
1543
|
+
const sized = setSvgRootSize(svgText, width, height);
|
|
1544
|
+
const url = URL.createObjectURL(new Blob([sized], { type: "image/svg+xml;charset=utf-8" }));
|
|
1545
|
+
try {
|
|
1546
|
+
const img = new Image();
|
|
1547
|
+
img.decoding = "async";
|
|
1548
|
+
await new Promise((resolve, reject) => {
|
|
1549
|
+
img.onload = () => resolve();
|
|
1550
|
+
img.onerror = () => reject(new Error("rasterizeSvg: the browser failed to decode the SVG"));
|
|
1551
|
+
img.src = url;
|
|
1552
|
+
});
|
|
1553
|
+
await img.decode().catch(() => {
|
|
1554
|
+
});
|
|
1555
|
+
const canvas = typeof OffscreenCanvas !== "undefined" ? new OffscreenCanvas(width, height) : Object.assign(document.createElement("canvas"), { width, height });
|
|
1556
|
+
const ctx = canvas.getContext("2d");
|
|
1557
|
+
if (!ctx) {
|
|
1558
|
+
throw new Error("rasterizeSvg: no 2D context available");
|
|
1559
|
+
}
|
|
1560
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
1561
|
+
return await createImageBitmap(canvas, { colorSpaceConversion: "none", premultiplyAlpha: "none" });
|
|
1562
|
+
} finally {
|
|
1563
|
+
URL.revokeObjectURL(url);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1444
1566
|
export {
|
|
1445
1567
|
ASTC4x4Encoder,
|
|
1446
1568
|
ASTC4x4WebGLEncoder,
|
|
@@ -1461,6 +1583,7 @@ export {
|
|
|
1461
1583
|
getSharedWebGLContext,
|
|
1462
1584
|
isWebGLAvailable,
|
|
1463
1585
|
padToBlockMultiple,
|
|
1586
|
+
rasterizeSvg,
|
|
1464
1587
|
selectFormat,
|
|
1465
1588
|
selectWebGLFormat
|
|
1466
1589
|
};
|
package/dist/three.d.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import { TextureHint, PreferredFormat, EncodeQuality, TextureFormat, Encoder, EncoderImageSource } from './index.js';
|
|
2
|
-
export { ASTC4x4Encoder, ASTC4x4WebGLEncoder, BC1Encoder, BC1WebGLEncoder, BC5Encoder, BC5WebGLEncoder, BC7Encoder, BC7WebGLEncoder, Capabilities, EncodeBytesResult, EncodeCallOptions, EncoderConstructor, EncoderOptions, ExtensionProvider, FeatureProvider, FormatSelection, FormatVariant, MipLevel, RawPixelSource, SelectFormatOptions, WebGLBlockEncoder, WebGLCapabilities, WebGLEncodeBytesResult, WebGLEncoderConstructor, WebGLEncoderImageSource, WebGLEncoderOptions, WebGLFormatSelection, WebGPUFeature, createWebGLContext, detectCapabilities, detectWebGLCapabilities, generateMipChain, getSharedWebGLContext, isWebGLAvailable, padToBlockMultiple, selectFormat, selectWebGLFormat } from './index.js';
|
|
1
|
+
import { TextureHint, PreferredFormat, SvgRasterSize, EncodeQuality, TextureFormat, Encoder, EncoderImageSource } from './index.js';
|
|
2
|
+
export { ASTC4x4Encoder, ASTC4x4WebGLEncoder, BC1Encoder, BC1WebGLEncoder, BC5Encoder, BC5WebGLEncoder, BC7Encoder, BC7WebGLEncoder, Capabilities, EncodeBytesResult, EncodeCallOptions, EncoderConstructor, EncoderOptions, ExtensionProvider, FeatureProvider, FormatSelection, FormatVariant, MipLevel, RasterizeSvgOptions, RawPixelSource, SelectFormatOptions, WebGLBlockEncoder, WebGLCapabilities, WebGLEncodeBytesResult, WebGLEncoderConstructor, WebGLEncoderImageSource, WebGLEncoderOptions, WebGLFormatSelection, WebGPUFeature, createWebGLContext, detectCapabilities, detectWebGLCapabilities, generateMipChain, getSharedWebGLContext, isWebGLAvailable, padToBlockMultiple, rasterizeSvg, selectFormat, selectWebGLFormat } from './index.js';
|
|
3
3
|
import { Texture, CompressedTexture, Loader, CompressedPixelFormat } from 'three';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Everything `compressTexture()` can take as an image source. A superset
|
|
7
7
|
* of `EncoderImageSource` (see Encoder.ts) that also accepts URL strings
|
|
8
8
|
* and Blob / File objects — the common cases in a web app.
|
|
9
|
+
*
|
|
10
|
+
* SVG works through all of these: a URL to an `.svg` file, a string of
|
|
11
|
+
* inline SVG markup (detected by a leading `<`), an SVG Blob/File, or an
|
|
12
|
+
* HTMLImageElement whose src is SVG. Vector sources are rasterised to RGBA
|
|
13
|
+
* before encoding — see the `svgSize` option.
|
|
9
14
|
*/
|
|
10
15
|
type CompressTextureSource = string | Blob | File | ImageBitmap | HTMLImageElement | HTMLCanvasElement | OffscreenCanvas | ImageData;
|
|
11
16
|
interface CompressOptions {
|
|
@@ -21,6 +26,14 @@ interface CompressOptions {
|
|
|
21
26
|
preferredFormat?: PreferredFormat;
|
|
22
27
|
/** Pick the sRGB or linear variant of the chosen format. Default 'srgb'. */
|
|
23
28
|
colorSpace?: 'srgb' | 'linear';
|
|
29
|
+
/**
|
|
30
|
+
* Rasterisation size for SVG sources. A number scales the SVG so its
|
|
31
|
+
* longest side matches (aspect ratio preserved); `{ width, height }`
|
|
32
|
+
* rasterises at exactly that size. Default: the SVG's intrinsic size
|
|
33
|
+
* (absolute width/height attributes, else the viewBox dimensions).
|
|
34
|
+
* Ignored for non-SVG sources.
|
|
35
|
+
*/
|
|
36
|
+
svgSize?: SvgRasterSize;
|
|
24
37
|
/** Flip the image vertically before encoding. Default true (matches Three.js convention). */
|
|
25
38
|
flipY?: boolean;
|
|
26
39
|
/** Generate a full mip chain down to 1×1 on the CPU, encode every level. */
|
|
@@ -79,6 +92,12 @@ declare class GputexLoader extends Loader<Texture> {
|
|
|
79
92
|
preferredFormat?: PreferredFormat;
|
|
80
93
|
/** Pick the sRGB or linear variant of the chosen format. Default 'srgb'. */
|
|
81
94
|
colorSpace: 'srgb' | 'linear';
|
|
95
|
+
/**
|
|
96
|
+
* Rasterisation size for SVG URLs — a number (longest side, aspect
|
|
97
|
+
* preserved) or exact `{ width, height }`. Default: the SVG's intrinsic
|
|
98
|
+
* size. See `CompressOptions.svgSize`.
|
|
99
|
+
*/
|
|
100
|
+
svgSize?: SvgRasterSize;
|
|
82
101
|
/** Flip the image vertically before encoding. Default true (matches Three.js convention). */
|
|
83
102
|
flipY: boolean;
|
|
84
103
|
/** Generate + encode a full mip chain. Default false. */
|
|
@@ -152,4 +171,4 @@ interface EncodeToTextureOptions {
|
|
|
152
171
|
*/
|
|
153
172
|
declare function encodeToTexture(encoder: Encoder, source: EncoderImageSource, { colorSpace, quality, flipY }?: EncodeToTextureOptions): Promise<EncodeResult>;
|
|
154
173
|
|
|
155
|
-
export { type CompressOptions, type CompressResult, type CompressTextureSource, EncodeQuality, type EncodeResult, type EncodeToTextureOptions, Encoder, EncoderImageSource, GputexLoader, PreferredFormat, TextureFormat, TextureHint, buildCompressedTexture, compressTexture, encodeToTexture, threeFormatFor };
|
|
174
|
+
export { type CompressOptions, type CompressResult, type CompressTextureSource, EncodeQuality, type EncodeResult, type EncodeToTextureOptions, Encoder, EncoderImageSource, GputexLoader, PreferredFormat, SvgRasterSize, TextureFormat, TextureHint, buildCompressedTexture, compressTexture, encodeToTexture, threeFormatFor };
|
package/dist/three.js
CHANGED
|
@@ -1446,6 +1446,140 @@ function padToBlockMultiple(level) {
|
|
|
1446
1446
|
return { data: out, width: pw, height: ph };
|
|
1447
1447
|
}
|
|
1448
1448
|
|
|
1449
|
+
// src/svg.ts
|
|
1450
|
+
function isSvgMarkup(source) {
|
|
1451
|
+
return source.trimStart().startsWith("<");
|
|
1452
|
+
}
|
|
1453
|
+
function hasSvgExtension(url) {
|
|
1454
|
+
return /\.svg$/i.test(url.split(/[?#]/, 1)[0]);
|
|
1455
|
+
}
|
|
1456
|
+
function isSvgBlob(blob) {
|
|
1457
|
+
if (blob.type) {
|
|
1458
|
+
return blob.type.split(";", 1)[0].trim().toLowerCase() === "image/svg+xml";
|
|
1459
|
+
}
|
|
1460
|
+
return typeof File !== "undefined" && blob instanceof File && hasSvgExtension(blob.name);
|
|
1461
|
+
}
|
|
1462
|
+
var ROOT_TAG_RE = /<svg(?=[\s/>])[^>]*>/;
|
|
1463
|
+
function getAttr(tag, name) {
|
|
1464
|
+
const m = new RegExp(`\\s${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`).exec(tag);
|
|
1465
|
+
return m ? m[1] ?? m[2] ?? "" : null;
|
|
1466
|
+
}
|
|
1467
|
+
function removeAttr(tag, name) {
|
|
1468
|
+
return tag.replace(new RegExp(`\\s${name}\\s*=\\s*(?:"[^"]*"|'[^']*')`, "g"), "");
|
|
1469
|
+
}
|
|
1470
|
+
function parseAbsoluteLength(value) {
|
|
1471
|
+
if (value == null) return null;
|
|
1472
|
+
const m = /^\s*\+?(\d+(?:\.\d+)?|\.\d+)(?:px)?\s*$/i.exec(value);
|
|
1473
|
+
if (!m) return null;
|
|
1474
|
+
const n = Number(m[1]);
|
|
1475
|
+
return n > 0 ? n : null;
|
|
1476
|
+
}
|
|
1477
|
+
function parseSvgDimensions(svgText) {
|
|
1478
|
+
const m = ROOT_TAG_RE.exec(svgText);
|
|
1479
|
+
if (!m) return null;
|
|
1480
|
+
const tag = m[0];
|
|
1481
|
+
const dims = {
|
|
1482
|
+
width: parseAbsoluteLength(getAttr(tag, "width")),
|
|
1483
|
+
height: parseAbsoluteLength(getAttr(tag, "height")),
|
|
1484
|
+
viewBoxWidth: null,
|
|
1485
|
+
viewBoxHeight: null
|
|
1486
|
+
};
|
|
1487
|
+
const viewBox = getAttr(tag, "viewBox");
|
|
1488
|
+
if (viewBox) {
|
|
1489
|
+
const parts = viewBox.trim().split(/[\s,]+/).map(Number);
|
|
1490
|
+
if (parts.length === 4 && parts.every(Number.isFinite) && parts[2] > 0 && parts[3] > 0) {
|
|
1491
|
+
dims.viewBoxWidth = parts[2];
|
|
1492
|
+
dims.viewBoxHeight = parts[3];
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
return dims;
|
|
1496
|
+
}
|
|
1497
|
+
function resolveSvgRasterSize(dims, size) {
|
|
1498
|
+
if (size !== void 0 && typeof size === "object") {
|
|
1499
|
+
const width = Math.round(size.width);
|
|
1500
|
+
const height = Math.round(size.height);
|
|
1501
|
+
if (!(width >= 1) || !(height >= 1)) {
|
|
1502
|
+
throw new Error(`rasterizeSvg: svgSize must be \u22651\xD71 (got ${size.width}\xD7${size.height})`);
|
|
1503
|
+
}
|
|
1504
|
+
return { width, height };
|
|
1505
|
+
}
|
|
1506
|
+
let w = dims.width;
|
|
1507
|
+
let h = dims.height;
|
|
1508
|
+
if (dims.viewBoxWidth != null && dims.viewBoxHeight != null) {
|
|
1509
|
+
if (w == null && h != null) w = h * dims.viewBoxWidth / dims.viewBoxHeight;
|
|
1510
|
+
else if (h == null && w != null) h = w * dims.viewBoxHeight / dims.viewBoxWidth;
|
|
1511
|
+
else if (w == null && h == null) {
|
|
1512
|
+
w = dims.viewBoxWidth;
|
|
1513
|
+
h = dims.viewBoxHeight;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
if (typeof size === "number") {
|
|
1517
|
+
if (!(size >= 1)) {
|
|
1518
|
+
throw new Error(`rasterizeSvg: svgSize must be \u22651 (got ${size})`);
|
|
1519
|
+
}
|
|
1520
|
+
const aspect = w != null && h != null ? w / h : 1;
|
|
1521
|
+
return aspect >= 1 ? { width: Math.round(size), height: Math.max(1, Math.round(size / aspect)) } : { width: Math.max(1, Math.round(size * aspect)), height: Math.round(size) };
|
|
1522
|
+
}
|
|
1523
|
+
if (w == null || h == null) {
|
|
1524
|
+
throw new Error(
|
|
1525
|
+
"rasterizeSvg: the SVG has no intrinsic size (no absolute width/height attributes and no viewBox) \u2014 pass svgSize to choose a rasterisation size"
|
|
1526
|
+
);
|
|
1527
|
+
}
|
|
1528
|
+
return { width: Math.max(1, Math.round(w)), height: Math.max(1, Math.round(h)) };
|
|
1529
|
+
}
|
|
1530
|
+
function setSvgRootSize(svgText, width, height) {
|
|
1531
|
+
const m = ROOT_TAG_RE.exec(svgText);
|
|
1532
|
+
if (!m) {
|
|
1533
|
+
throw new Error("rasterizeSvg: no <svg> root element found in source");
|
|
1534
|
+
}
|
|
1535
|
+
let tag = m[0];
|
|
1536
|
+
const origWidth = parseAbsoluteLength(getAttr(tag, "width"));
|
|
1537
|
+
const origHeight = parseAbsoluteLength(getAttr(tag, "height"));
|
|
1538
|
+
const hasViewBox = getAttr(tag, "viewBox") != null;
|
|
1539
|
+
tag = removeAttr(removeAttr(tag, "width"), "height");
|
|
1540
|
+
let inject = ` width="${width}" height="${height}"`;
|
|
1541
|
+
if (!hasViewBox && origWidth != null && origHeight != null) {
|
|
1542
|
+
inject += ` viewBox="0 0 ${origWidth} ${origHeight}"`;
|
|
1543
|
+
}
|
|
1544
|
+
tag = `<svg${inject}${tag.slice("<svg".length)}`;
|
|
1545
|
+
return svgText.slice(0, m.index) + tag + svgText.slice(m.index + m[0].length);
|
|
1546
|
+
}
|
|
1547
|
+
async function rasterizeSvg(source, options = {}) {
|
|
1548
|
+
if (typeof Image === "undefined") {
|
|
1549
|
+
throw new Error(
|
|
1550
|
+
"rasterizeSvg: SVG rasterisation needs a DOM Image element and cannot run in this environment (e.g. a worker)"
|
|
1551
|
+
);
|
|
1552
|
+
}
|
|
1553
|
+
const svgText = typeof source === "string" ? source : await source.text();
|
|
1554
|
+
const dims = parseSvgDimensions(svgText);
|
|
1555
|
+
if (!dims) {
|
|
1556
|
+
throw new Error("rasterizeSvg: no <svg> root element found in source");
|
|
1557
|
+
}
|
|
1558
|
+
const { width, height } = resolveSvgRasterSize(dims, options.size);
|
|
1559
|
+
const sized = setSvgRootSize(svgText, width, height);
|
|
1560
|
+
const url = URL.createObjectURL(new Blob([sized], { type: "image/svg+xml;charset=utf-8" }));
|
|
1561
|
+
try {
|
|
1562
|
+
const img = new Image();
|
|
1563
|
+
img.decoding = "async";
|
|
1564
|
+
await new Promise((resolve, reject) => {
|
|
1565
|
+
img.onload = () => resolve();
|
|
1566
|
+
img.onerror = () => reject(new Error("rasterizeSvg: the browser failed to decode the SVG"));
|
|
1567
|
+
img.src = url;
|
|
1568
|
+
});
|
|
1569
|
+
await img.decode().catch(() => {
|
|
1570
|
+
});
|
|
1571
|
+
const canvas = typeof OffscreenCanvas !== "undefined" ? new OffscreenCanvas(width, height) : Object.assign(document.createElement("canvas"), { width, height });
|
|
1572
|
+
const ctx = canvas.getContext("2d");
|
|
1573
|
+
if (!ctx) {
|
|
1574
|
+
throw new Error("rasterizeSvg: no 2D context available");
|
|
1575
|
+
}
|
|
1576
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
1577
|
+
return await createImageBitmap(canvas, { colorSpaceConversion: "none", premultiplyAlpha: "none" });
|
|
1578
|
+
} finally {
|
|
1579
|
+
URL.revokeObjectURL(url);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1449
1583
|
// src/three/compressTexture.ts
|
|
1450
1584
|
import { LinearFilter as LinearFilter2, LinearSRGBColorSpace as LinearSRGBColorSpace2, RepeatWrapping as RepeatWrapping2, SRGBColorSpace as SRGBColorSpace2, Texture } from "three";
|
|
1451
1585
|
|
|
@@ -1518,27 +1652,49 @@ async function encodeToTexture(encoder, source, { colorSpace = "srgb", quality =
|
|
|
1518
1652
|
}
|
|
1519
1653
|
|
|
1520
1654
|
// src/three/compressTexture.ts
|
|
1521
|
-
async function sourceToBitmap(source) {
|
|
1655
|
+
async function sourceToBitmap(source, svgSize) {
|
|
1522
1656
|
const opts = {
|
|
1523
1657
|
colorSpaceConversion: "none",
|
|
1524
1658
|
premultiplyAlpha: "none"
|
|
1525
1659
|
};
|
|
1526
1660
|
if (typeof source === "string") {
|
|
1661
|
+
if (isSvgMarkup(source)) {
|
|
1662
|
+
return rasterizeSvg(source, { size: svgSize });
|
|
1663
|
+
}
|
|
1527
1664
|
const resp = await fetch(source);
|
|
1528
1665
|
if (!resp.ok) {
|
|
1529
1666
|
throw new Error(`compressTexture: fetch ${source} failed (${resp.status})`);
|
|
1530
1667
|
}
|
|
1531
1668
|
const blob = await resp.blob();
|
|
1669
|
+
if (isSvgBlob(blob) || !isImageMimeType(blob.type) && hasSvgExtension(source)) {
|
|
1670
|
+
return rasterizeSvg(blob, { size: svgSize });
|
|
1671
|
+
}
|
|
1532
1672
|
return createImageBitmap(blob, opts);
|
|
1533
1673
|
}
|
|
1534
1674
|
if (source instanceof Blob) {
|
|
1675
|
+
if (isSvgBlob(source)) {
|
|
1676
|
+
return rasterizeSvg(source, { size: svgSize });
|
|
1677
|
+
}
|
|
1535
1678
|
return createImageBitmap(source, opts);
|
|
1536
1679
|
}
|
|
1537
1680
|
if (source instanceof ImageBitmap) {
|
|
1538
1681
|
return source;
|
|
1539
1682
|
}
|
|
1683
|
+
if (typeof HTMLImageElement !== "undefined" && source instanceof HTMLImageElement) {
|
|
1684
|
+
const src = source.currentSrc || source.src;
|
|
1685
|
+
if (src && (hasSvgExtension(src) || /^data:image\/svg\+xml/i.test(src))) {
|
|
1686
|
+
const resp = await fetch(src);
|
|
1687
|
+
if (!resp.ok) {
|
|
1688
|
+
throw new Error(`compressTexture: fetch ${src} failed (${resp.status})`);
|
|
1689
|
+
}
|
|
1690
|
+
return rasterizeSvg(await resp.blob(), { size: svgSize });
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1540
1693
|
return createImageBitmap(source, opts);
|
|
1541
1694
|
}
|
|
1695
|
+
function isImageMimeType(type) {
|
|
1696
|
+
return /^image\//i.test(type) && !/svg/i.test(type);
|
|
1697
|
+
}
|
|
1542
1698
|
function bitmapToMipLevel(bitmap, flipY) {
|
|
1543
1699
|
const w = bitmap.width, h = bitmap.height;
|
|
1544
1700
|
const canvas = typeof OffscreenCanvas !== "undefined" ? new OffscreenCanvas(w, h) : Object.assign(document.createElement("canvas"), { width: w, height: h });
|
|
@@ -1573,6 +1729,7 @@ async function compressTexture(source, options = {}) {
|
|
|
1573
1729
|
hint = "color",
|
|
1574
1730
|
preferredFormat,
|
|
1575
1731
|
colorSpace = "srgb",
|
|
1732
|
+
svgSize,
|
|
1576
1733
|
flipY = true,
|
|
1577
1734
|
mipmaps = false,
|
|
1578
1735
|
quality = "fast",
|
|
@@ -1580,7 +1737,7 @@ async function compressTexture(source, options = {}) {
|
|
|
1580
1737
|
adapter: providedAdapter
|
|
1581
1738
|
} = options;
|
|
1582
1739
|
const srgb = colorSpace === "srgb";
|
|
1583
|
-
const bitmap = await sourceToBitmap(source);
|
|
1740
|
+
const bitmap = await sourceToBitmap(source, svgSize);
|
|
1584
1741
|
const viaWebGPU = await encodeViaWebGPU();
|
|
1585
1742
|
if (viaWebGPU) return viaWebGPU;
|
|
1586
1743
|
const viaWebGL = encodeViaWebGL();
|
|
@@ -1750,6 +1907,12 @@ var GputexLoader = class extends Loader {
|
|
|
1750
1907
|
preferredFormat;
|
|
1751
1908
|
/** Pick the sRGB or linear variant of the chosen format. Default 'srgb'. */
|
|
1752
1909
|
colorSpace = "srgb";
|
|
1910
|
+
/**
|
|
1911
|
+
* Rasterisation size for SVG URLs — a number (longest side, aspect
|
|
1912
|
+
* preserved) or exact `{ width, height }`. Default: the SVG's intrinsic
|
|
1913
|
+
* size. See `CompressOptions.svgSize`.
|
|
1914
|
+
*/
|
|
1915
|
+
svgSize;
|
|
1753
1916
|
/** Flip the image vertically before encoding. Default true (matches Three.js convention). */
|
|
1754
1917
|
flipY = true;
|
|
1755
1918
|
/** Generate + encode a full mip chain. Default false. */
|
|
@@ -1781,6 +1944,7 @@ var GputexLoader = class extends Loader {
|
|
|
1781
1944
|
hint: this.hint,
|
|
1782
1945
|
preferredFormat: this.preferredFormat,
|
|
1783
1946
|
colorSpace: this.colorSpace,
|
|
1947
|
+
svgSize: this.svgSize,
|
|
1784
1948
|
flipY: this.flipY,
|
|
1785
1949
|
mipmaps: this.mipmaps,
|
|
1786
1950
|
quality: this.quality,
|
|
@@ -1836,6 +2000,7 @@ export {
|
|
|
1836
2000
|
getSharedWebGLContext,
|
|
1837
2001
|
isWebGLAvailable,
|
|
1838
2002
|
padToBlockMultiple,
|
|
2003
|
+
rasterizeSvg,
|
|
1839
2004
|
selectFormat,
|
|
1840
2005
|
selectWebGLFormat,
|
|
1841
2006
|
threeFormatFor
|