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

emdash-plugin-stl-viewer

v0.3.0

Published

EmDash CMS plugin: embed interactive 3D previews of STL or 3MF files in Portable Text content

Downloads

204

Readme

emdash-plugin-stl-viewer

npm version npm downloads license

An EmDash CMS plugin that embeds interactive 3D previews of STL and 3MF files inside Portable Text content. Drop a 3D Model block into any post or page, pick a file (or paste a URL), and get a touch-friendly orbit viewer that loads three.js lazily on viewport entry.

A rendered STL model embedded in an article

Features

  • STL and 3MF — both formats supported. Format is auto-detected from the URL's file extension (.stl / .3mf); set an explicit override in the block form for opaque URLs.
  • Lazy three.js — the library (~170 KB gzipped) downloads only when a viewer is scrolled near; pages with no model embeds pay nothing.
  • Lazy 3MF decoder — the 3MFLoader (plus the bundled fflate zip decoder) loads on-demand the first time a 3MF URL is encountered. Pages with STL-only content never pay the 3MF cost.
  • Shared module — additional viewers on the same page reuse the cached module, only paying for their own scene.
  • Five materials — matte plastic, glossy plastic, brushed metal, surface-normal rainbow, unlit clay. The configured material is applied uniformly to 3MF imports (the file's embedded materials are intentionally overridden so blocks render consistently with STL).
  • Three sizes — compact / standard / tall, all responsive.
  • Auto-rotate on idle — pauses on user input, resumes after a moment.
  • Touch support — pinch-zoom, two-finger pan, one-finger orbit.
  • Progress UI — bytes/total during model download, indeterminate while three.js loads.
  • Auto Z-up detection — many printable STLs and 3MFs ship Z-up; the viewer rotates them so Y is up.
  • Auto-framing — the model is recentered around the origin and the camera distance is derived from the bounding sphere and FOV.

Installation

pnpm add emdash-plugin-stl-viewer three
# (peers: astro, emdash, three)

Register it in astro.config.mjs:

import { stlViewerPlugin } from "emdash-plugin-stl-viewer";

export default defineConfig({
	integrations: [
		emdash({
			plugins: [stlViewerPlugin()],
			// ...
		}),
	],
});

That's it. The block type stl-viewer (labeled "3D Model (STL or 3MF)" in the slash menu) is now available in the Portable Text editor.

Authoring

In the editor, type / and pick 3D Model (STL or 3MF). Fill in:

| Field | Description | | --- | --- | | Model file | Opens the EmDash media picker filtered to model/* MIME types. Pick an existing asset, upload a new .stl/.3mf, or paste a URL directly into the input. | | Format | Auto-detect from URL (default) / STL / 3MF. Override when the URL has no extension. | | Title | Optional header text. | | Caption | Optional figcaption below the viewer. | | Material | Matte plastic, glossy plastic, brushed metal, normal map, or unlit clay. | | Color | Hex color for matte/glossy/metal materials. Ignored by normal. | | Viewer height | Compact (320 px), Standard (440 px), Tall (560 px). | | Auto-rotate when idle | Off if you'd rather have a static front view. | | Show ground plane | Subtle grid beneath the model. |

Uploading STL or 3MF files

The block's Model file field uses EmDash's media_picker with mime_type_filter: "model/". In the popup you can:

  • Select an existing asset (filtered to model/* MIME types).
  • Upload a new .stl or .3mf — the file dialog's accept is scoped to model/, and EmDash's /_emdash/api/media/upload-url endpoint passes the MIME type through to R2 without a global allowlist.
  • Paste a URL directly into the input — the field value is a plain string, so external URLs (https://…) work too.

Browsers may not recognise .stl / .3mf from extension alone (the Content-Type arrives as application/octet-stream). EmDash now retains the upload's original Content-Type header, but if you're hosting your own asset server, set Content-Type: model/stl or model/3mf explicitly. The viewer itself does not require a particular MIME type — it sniffs the URL's extension or the explicit format override.

Architecture

The plugin is native format — that's required for Portable Text block types because they need Astro components for site-side rendering.

src/
├── index.ts              # PluginDescriptor + definePlugin() (Native format)
└── astro/
    ├── index.ts          # Exports `blockComponents` map
    ├── StlViewer.astro   # Server-rendered placeholder + scoped CSS
    └── viewer-client.ts  # Lazy three.js + 3MF init, OrbitControls, lifecycle

The Astro component renders a placeholder card with a CSS-animated phantom cube. When the wrapper enters the viewport (IntersectionObserver, 400 px root margin), the client script:

  1. Dynamically imports three, three/addons/controls/OrbitControls.js, and three/addons/loaders/STLLoader.js (memoised — one fetch per page).
  2. If the URL ends in .3mf (or format === "3mf"), additionally imports three/addons/loaders/3MFLoader.js lazily.
  3. Streams the model via fetch with a progress callback so large files show byte counts.
  4. STL: parses to a single BufferGeometry, recenters it, computes smooth-shaded normals. 3MF: parses to a Group (potentially multi-mesh), replaces embedded materials with the configured material, repositions the group's centroid to the origin.
  5. Mounts a WebGLRenderer canvas inside the stage, attaches OrbitControls.
  6. Hooks up ResizeObserver so the renderer follows container resizes, and a second IntersectionObserver to pause the render loop when offscreen.

A note on Astro scoped CSS

The canvas element is injected by three.js at runtime, so it doesn't carry the data-astro-cid-* attribute Astro adds to elements declared inside the component. Any selector targeting the canvas must use :global(canvas), or Astro's scope rewrite will silently leave the runtime element unstyled. The viewer hits this in two places — both are wrapped accordingly in StlViewer.astro.

Bundle layout

Built via Vite (Astro):

| Chunk | Approx size | When loaded | | --- | ---: | --- | | StlViewer.astro_…js | ~7 KB | Eagerly with the page that has a viewer | | three.module.js | ~690 KB | First viewer hits viewport | | OrbitControls.js | ~19 KB | First viewer hits viewport | | STLLoader.js | ~3 KB | First viewer hits viewport | | 3MFLoader.js (+ fflate) | ~50 KB | Only on the first 3MF viewer on a page |

Development

pnpm install
pnpm generate-fixtures   # writes cube.stl / icosahedron.stl / torus.stl / knurled.stl to public/stls/ of the host repo
pnpm typecheck

Compatibility

  • Astro >= 6.0
  • EmDash >= 0.12 (uses Block Kit's media_picker element with mime_type_filter).
  • three ^0.171.0

Changelog

  • 0.3.0 — Switch the Model URL field to a media_picker filtered to model/* MIME types. Authors get a unified pick/upload/paste-URL popup in the block form. Bumps the required EmDash version to >= 0.12. Existing content is unchanged — the field value is still a plain URL string.
  • 0.2.0 — Add 3MF support alongside STL. 3MFLoader (plus the bundled fflate zip decoder) lazy-loads only when a 3MF URL is encountered.
  • 0.1.0 — Initial release. STL viewer block with five materials, three sizes, auto-rotate, touch support, and a lazy three.js bundle.

License

MIT — see LICENSE.