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 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 | Default | Description |
215
- | ----------------- | -------------------- | --------- | ------------------------------------------------------------------------------------------------ |
216
- | `hint` | `TextureHint` | `'color'` | `'color'`, `'colorWithAlpha'`, or `'normal'` |
217
- | `preferredFormat` | `'bc1'` | — | Prefer BC1 (half of BC7's size) when supported; normal selection otherwise. `hint: 'color'` only |
218
- | `colorSpace` | `'srgb' \| 'linear'` | `'srgb'` | Use the sRGB or linear variant of the chosen format |
219
- | `flipY` | `boolean` | `true` | Flip vertically (matches Three.js convention) |
220
- | `mipmaps` | `boolean` | `false` | Generate full mip chain down to 1x1 |
221
- | `device` | `GPUDevice` | — | Reuse an existing WebGPU device instead of creating one |
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
- 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 RawPixelSource, type SelectFormatOptions, TextureFormat, type TextureHint, WebGLBlockEncoder, type WebGLCapabilities, type WebGLEncodeBytesResult, type WebGLEncoderConstructor, type WebGLEncoderImageSource, type WebGLEncoderOptions, type WebGLFormatSelection, WebGPUFeature, createWebGLContext, detectCapabilities, detectWebGLCapabilities, generateMipChain, getSharedWebGLContext, isWebGLAvailable, padToBlockMultiple, selectFormat, selectWebGLFormat };
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gputex",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "dist"