npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@three-ws/glb-tools

v0.1.0

Published

Inspect, re-theme, and bake GLB models from the shell or CI. The client SDK behind the three.ws 3D asset pipeline — structural inspection, token-themed mesh synthesis, and appearance baking over live three.ws endpoints.

Readme


@three-ws/glb-tools is the official client for the three.ws GLB pipeline — the same inspection, theming, and baking engine three.ws runs on every asset it serves. It wraps three live endpoints: structural model inspection (/api/x402/model-check), token-themed mesh synthesis (/api/x402/mint-to-mesh), and server-side appearance baking (/api/avatars/:id). One ergonomic call replaces a custom glTF parser, a @gltf-transform build chain, and the polling/billing plumbing around them. It pairs with @three-ws/forge — Forge makes the GLB, glb-tools measures, themes, and optimizes it.

Why

A GLB looks like one file but hides a graph: scenes, nodes, meshes, materials, textures, skins, animations, extensions. Answering "how heavy is this, is it rigged, will it choke a phone, can I ship it" means either booting a headless viewer or hand-writing a glTF-Transform pipeline — per asset, per CI run. glb-tools does it as a function call:

  • Inspect without a viewer. inspect(url) returns exact vertex/triangle counts, per-texture dimensions, per-material channel maps, extensions, and a prioritized list of optimization suggestions — no GPU, no browser, headless in CI.
  • Theme a token into a mesh. theme(mint) returns a watertight, instantly renderable GLB cube colored from a stable hash of the mint, with the token image baked on as a baseColor texture when one exists.
  • Bake an appearance once. bake(avatarId, appearance) flattens outfit morphs, color tints, bone-mounted accessories, and hidden layers into a single optimized GLB (weld → quantize → meshopt → WebP textures) that every viewer renders with zero runtime customization code.

This is the SDK twin of the inspection tools exposed on the 3D Studio MCP server — the same engine, as plain functions instead of MCP tools.

Install

npm install @three-ws/glb-tools

Zero runtime dependencies. Works in Node 18+, modern browsers, and CI runners (uses fetch). To generate the GLBs you inspect, add @three-ws/forge.

Quick start

Inspect any public GLB — exact stats, no viewer:

import { inspect } from '@three-ws/glb-tools';

const report = await inspect('https://three.ws/avatar/character-studio/sample.glb');

console.log(report.model.counts.totalTriangles); // → 24812
console.log(report.model.counts.skins > 0);       // → true  (rigged)
report.suggestions.forEach((s) => console.log(`[${s.severity}] ${s.message}`));

Turn a Solana mint into a renderable GLB:

import { theme } from '@three-ws/glb-tools';
import { writeFile } from 'node:fs/promises';

const out = await theme('FeMbDoX7R1Psc4GEcvJdsbNbZA3bfztcyDCatJVJpump'); // $THREE
console.log(out.theme.color);   // → [0.92, 0.45, 0.18]  (hashed from the mint)
await writeFile('three.glb', out.bytes); // ready for three.js / model-viewer

Bake a customized appearance into one flat, optimized GLB:

import { bake } from '@three-ws/glb-tools';

const baked = await bake('avatar_8f3a…', {
  outfit: 'streetwear-01',
  colors: { hair: '#1b1b1f', outfit: '#3b82f6' },
  accessories: ['glasses-aviator'],
}, { token: process.env.THREE_WS_TOKEN });

console.log(baked.baked_storage_key, baked.size_bytes);

API

inspect(url, options?) → Promise<InspectReport>

Fetch a public glTF/GLB by URL and return its structure plus optimization advice. Wraps GET /api/x402/model-check?url=<url>. The model is parsed server-side with @gltf-transform; only the JSON/scene graph is analyzed — fast even on large meshes. Source URL must be public HTTPS; max 16 MiB.

Options

| Option | Type | Default | Notes | |---|---|---|---| | payWith | 'x402' \| 'credits' | 'x402' | Billing lane (see Pricing). | | signal | AbortSignal | — | Cancel an in-flight inspection. |

Returns InspectReport

| Field | Type | Notes | |---|---|---| | url | string | Canonicalized source URL. | | fetchedBytes | number | Bytes downloaded for analysis. | | model | ModelInfo | Structural summary (below). | | suggestions | Suggestion[] | Prioritized optimization advice. |

ModelInfo carries container ('glb' \| 'gltf'), generator, version, copyright, extensionsUsed, extensionsRequired, a counts object (scenes, nodes, meshes, materials, textures, animations, skins, totalVertices, totalTriangles, indexedPrimitives, nonIndexedPrimitives), primitiveModes, a per-texture textures[] array (name, mimeType, width, height, byteSize), and a per-material materials[] array (name, alphaMode, doubleSided, hasBaseColorTexture, hasNormalTexture, hasMetallicRoughnessTexture, hasEmissiveTexture, hasOcclusionTexture). A non-empty counts.skins means the model is rigged.

Each Suggestion is { id, severity: 'info' | 'warn' | 'critical', message, estimate? } — e.g. tri_budget (decimate/LOD over 500k triangles), draco, meshopt, texture_size.

theme(mint, options?) → Promise<ThemedMesh>

Synthesize a themed GLB for a Solana fungible-token mint. Wraps GET /api/x402/mint-to-mesh?mint=<base58>. The server reads on-chain Metaplex metadata, colors a unit cube from a stable hash of the mint, and — when the off-chain metadata exposes a PNG/JPEG — embeds the token image as a baseColor texture on every face. The mint's name, symbol, and a timestamp are written to asset.extras so downstream agents can introspect the model.

Inputmint is a base58 SPL address (32–44 chars).

Returns ThemedMesh

| Field | Type | Notes | |---|---|---| | mint | string | Echoed mint address. | | theme.name | string \| null | On-chain token name. | | theme.symbol | string \| null | On-chain symbol. | | theme.color | [number, number, number] | RGB in [0,1], the baseColorFactor. | | theme.imageUrl | string \| null | Source image URL, when present. | | theme.hasImage | boolean | true when an image was embedded as a texture. | | bytes | Uint8Array | Decoded GLB bytes (from the response's base64). | | glb.mimeType | 'model/gltf-binary' | — | | glb.bytes | number | GLB size in bytes. |

The raw endpoint returns the GLB base64-encoded under glb.base64; the SDK decodes it for you into bytes and leaves glb.bytes as the size.

bake(avatarId, appearance, options?) → Promise<BakeResult>

Bake an appearance into a three.ws avatar's GLB, server-side. Wraps PATCH /api/avatars/:id { appearance }, which triggers the synchronous baker. Requires an authenticated owner token (the avatar belongs to a signed-in account).

appearance accepts any combination of:

| Field | Type | Effect | |---|---|---| | outfit | string | Outfit preset id — applies its morph bindings. | | accessories | string[] | Bone-mounted accessory preset ids (hats, glasses…). | | colors | Record<slot, hex> | Tint material slots (skin, hair, outfit, glasses). | | morphs | Record<name, 0..1> | Raw morph-target overrides (win over preset bindings). | | hidden | string[] | Slots to hide, exposing the base body. |

A bakeable appearance returns BakeResult { baked_storage_key, appearance_hash, size_bytes }. An empty / cleared appearance clears the cached baked GLB (the base model is served again) and resolves with the avatar's baked_storage_key set to null.

How it works

Three independent endpoints, one client. None of the heavy lifting happens in your process:

inspect(url) ─────▶ GET /api/x402/model-check?url=…
                        │  fetch ≤16 MiB → @gltf-transform readJSON
                        ▼
                    counts · textures · materials · extensions
                        + suggestOptimizations() → InspectReport

theme(mint) ──────▶ GET /api/x402/mint-to-mesh?mint=…
                        │  Metaplex metadata → colorFromMint() (FNV-1a hash)
                        │  optional image → baseColor texture
                        ▼
                    createThemedGLB() → unit-cube GLB (base64) → ThemedMesh

bake(id, appr) ───▶ PATCH /api/avatars/:id { appearance }
                        │  applyMorphs · applyColors · merge accessories · applyHidden
                        │  unpartition → prune → dedup → weld → quantize
                        │  → meshopt → textureCompress (WebP, ≤1024px)
                        ▼
                    optimized GLB in R2 → BakeResult
  • Inspect is read-only and deterministic: same URL in, same report out. It never executes shaders or rasterizes — it walks the scene graph.
  • Theme is "mint to mesh": a fully conformant glTF 2.0 cube that any Three.js, Babylon.js, or <model-viewer> instance renders directly, with the full on-chain metadata in asset.extras.
  • Bake is a one-shot flatten + compress. Bone-name matching tolerates the common mixamorig:, CC_Base_, and rig_ prefixes, so accessories attach to any upstream rigger's skeleton without renaming.

Pricing

The inspection and theming endpoints are pay-per-call x402 lanes settled in USDC (6-decimal atomics):

| Capability | Endpoint | Lane | Price | |---|---|---|---| | inspect() | /api/x402/model-check | x402 / USDC on Solana | $0.001 per call | | theme() | /api/x402/mint-to-mesh | x402 / USDC on Base | $0.001 per call | | bake() | /api/avatars/:id | authenticated owner | included with the avatar |

Set payWith: 'x402' (default) to pay per call with USDC, or payWith: 'credits' to draw from a signed-in prepaid balance. Pair with @three-ws/x402-fetch to automate the 402 settlement. Internal/subscription/OAuth callers can be granted a payment bypass (x-payment-bypass header on the response). Both x402 routes are also exposed as MCP tools on the 3D Studio server.

Errors & edge cases

inspect(), theme(), and bake() reject with a typed GlbToolsError carrying a code that mirrors the endpoint's error contract:

| code | HTTP | Meaning | Recovery | |---|---|---|---| | missing_url / missing_mint | 400 | Required input omitted. | Pass url / mint. | | invalid_url | 400 | Not a fetchable HTTPS URL. | Use a public HTTPS GLB. | | invalid_mint | 400 | Not a base58 SPL address (32–44 chars). | Pass a valid mint. | | payment_required | 402 | x402 lane with no payment proof. | Provide an x402 payer or use credits. | | method_not_allowed | 405 | Wrong HTTP verb. | The SDK uses the correct verb; surfaces only on raw calls. | | verify_failed / settle_failed | 502 | Payment verification/settlement upstream failed. | Retry; check the payer balance. | | internal_error | 500 | Upstream parse/build error. | Retry, or check the input asset. |

Inspection rejects oversized inputs before download (the 16 MiB cap), so a runaway URL fails fast rather than streaming forever. A bake of an empty appearance is not an error — it intentionally clears the cached baked GLB. Every state is designed: a missing key returns a code, not a crash.

Examples

CI asset gate — fail the build when a model busts the triangle budget:

import { inspect } from '@three-ws/glb-tools';

const { model, suggestions } = await inspect(process.env.MODEL_URL, {
  payWith: 'credits',
});
const critical = suggestions.filter((s) => s.severity === 'critical');
if (model.counts.totalTriangles > 250_000 || critical.length) {
  console.error('Model exceeds web budget:', model.counts.totalTriangles);
  process.exit(1);
}

Forge → inspect — generate, then verify it's rig-ready before shipping:

import { forge } from '@three-ws/forge';
import { inspect } from '@three-ws/glb-tools';

const { glbUrl } = await forge('a cartoon astronaut, full body');
const { model } = await inspect(glbUrl);
console.log('rigged:', model.counts.skins > 0, '· tris:', model.counts.totalTriangles);

Theme a token, render inline in the browser:

<script type="module">
  import { theme } from '@three-ws/glb-tools';

  const { bytes } = await theme('FeMbDoX7R1Psc4GEcvJdsbNbZA3bfztcyDCatJVJpump'); // $THREE
  const url = URL.createObjectURL(new Blob([bytes], { type: 'model/gltf-binary' }));
  const el = Object.assign(document.createElement('model-viewer'), { src: url });
  document.body.append(el);
</script>

Related