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

openskp

v1.3.0

Published

Open-source SketchUp (.skp) binary file parser, writer, and converter — extract geometry, metadata, layers, and materials; create and edit .skp files; convert to GLB, OBJ, STL, PLY, DXF, and IFC4. Works in browser and Node.js.

Readme

OpenSKP

The open-source SketchUp (.skp) file parser, writer, and converter — TypeScript / JavaScript edition.

Parse, write, and convert .skp files without SketchUp. No SDK. No license. Zero native dependencies — fflate handles ZIP extraction and a ported earcut handles triangulation, so it runs anywhere JavaScript does: Node.js or the browser.

License: MIT npm Node

🏠 openskp.com · 🌐 Try the Live Web Viewer · 📖 Docs · Changelog

[!IMPORTANT] This project was built by reverse engineering a proprietary binary format. It is not affiliated with or endorsed by Trimble Inc. or SketchUp.

What is OpenSKP?

OpenSKP is the first and only open-source, cross-platform parser for SketchUp binary files — reverse-engineered from both the modern VFF container (SketchUp 2021+) and the classic MFC CArchive container (SketchUp 2013–2020). It gives you full programmatic access to geometry, materials, components, layers, and metadata, with no SketchUp installation and no proprietary SDK required. The same parser and export API also ship as first-class packages for Python, .NET, Dart, and C++ — see the project README for the full cross-language picture.

This package can also write new .skp files from scratch, and edit existing ones, validated feature-by-feature against the real SketchUp SDK (see Writing below).

Features

  • Full-fidelity parsing — vertices, edges, faces, normals, UV coordinates, nested component hierarchies, layers/tags, materials, textures, styles, and dynamic-component attributes.
  • Both SketchUp file generations — modern VFF (2021+) and legacy MFC (2013–2020) containers, transparently, behind one parseSkp()/.parse() call.
  • Scene baking — an opt-in buildScene() pass resolves the full placed scene graph to world-space, triangulated, export-ready geometry.
  • Instancing preserved — buildInstancedScene() keeps SketchUp's own instancing instead of baking it out: unique geometry once, plus one transform per placement, so a component placed 1,000 times costs one copy of its buffers. Losslessly — no decimation or quantisation. See Choosing an API.
  • Native multi-format conversion — glTF (GLB), Wavefront OBJ/MTL, STL, PLY, AutoCAD DXF (3DFACE and Polyface Mesh), IFC4 (BIM/ISO 10303-21 STEP), ThatOpen Fragments (.frag binary for high-speed Web BIM streaming), and JSON — all written from scratch, no third-party CAD/BIM SDK involved. The DXF writer is verified against real desktop AutoCAD, not just lenient DXF readers.
  • Runs anywhere JavaScript does — zero native dependencies, works in Node.js and directly in the browser from a File/Blob.
  • Structured observability — opt-in progress reporting and structured, location-carrying parse errors.
  • Write support — build new legacy-format .skp files from scratch: geometry (including true, editable circular/arc curves, freeform polylines, faces with holes cut out, and non-planar auto-triangulation), materials (solid + PNG/JPEG textures), layers, nested component definitions and groups, instance rotation/visibility, and custom attribute dictionaries — or load and extend an existing file with openExisting(). No SDK involved; every feature validated against the real SketchUp SDK. See Writing below.

Installation

npm install openskp

Quick Start

import { SkpFile } from 'openskp';

// Node.js
const skp = SkpFile.open('model.skp');
const model = skp.parse();

// Browser: parse from a File/Blob's ArrayBuffer
import { parseSkp } from 'openskp';
const buffer = await file.arrayBuffer();
const model2 = parseSkp(buffer);

console.log(model.version, model.layers.length, 'layers');

// Inspect definitions (component geometry) - model.definitions is a Map
for (const [id, defn] of model.definitions) {
  console.log(`${defn.name}: ${defn.faces.length} faces, ${defn.vertices.length} vertices`);
}

// model.root holds whatever is placed directly in the model, not inside
// any component/group
console.log(model.root.instances.length, 'root-level instances');

// Opt-in: full placed scene graph, triangulated, world-space, GLB-ready
const scene = skp.buildScene();
console.log(scene.glbPrimitives.length, 'renderable mesh primitives');

// Or keep the file's instancing instead of baking it out: unique geometry
// once, plus a transform per placement.
const instanced = skp.buildInstancedScene();
console.log(instanced.meshResources.length, 'unique meshes');

Choosing an API

Three entry points, in increasing order of what they compute. Each one re-parses the file independently, so you only pay for the one you call.

| Use | When | |---|---| | parseSkp() / .parse() | You want the file's raw contents — definitions, layers, materials, metadata — with no scene-graph instancing resolved. Fastest and lightest. | | buildScene() / .buildScene() | You want a flat, world-space, triangulated scene to hand straight to a renderer or an exporter (toGLB, toOBJ, toSTL, toIFC, …). Every placement is baked into its own vertex buffers. | | buildInstancedScene() / .buildInstancedScene() | You want the same geometry, but with the file's instancing preserved: each definition triangulated once and referenced by every placement. |

The memory tradeoff

buildScene() bakes each placed instance into its own world-space buffers, so its output scales with:

definition geometry x number of placed instances

buildInstancedScene() stores each distinct definition once and puts the placement on the node, so it scales with:

unique geometry + instance transforms

For a model that reuses components heavily — furniture, facade panels, fixtures, anything repeated — that is the difference between an output that grows with the model's placement count and one that grows with its distinct content. On a synthetic scene of one 24-face component repeated 1,000 times, the geometry buffers are 1,000x smaller (3,562 KB vs 3.6 KB) and the exported GLB is 48x smaller.

The cost is that you must apply the node transforms yourself (or hand the result to toInstancedGLB(), or to any glTF/three.js-style scene graph, which do it natively). If you just want flat triangles, buildScene() is still the simpler call.

This is lossless instancing preservation, not mesh decimation. No vertices are removed, merged, quantised or approximated. The triangles are exactly the ones buildScene() produces; they are stored once and referenced N times instead of copied N times. The test suite asserts this directly, by flattening the instanced result and comparing it against the baked one on the repository's real .skp fixtures.

Coordinate systems and units

SketchUp stores geometry in inches on a Z-up axis system. Both scene builders convert to metres on glTF's Y-up axes, applying the same (x, y, z) -> (x, z, -y) swap, so:

  • LocalPrimitive.positions / normals are in metres, Y-up, and in definition-local space — no instance transform applied.
  • InstancedNode.matrix is a 16-element column-major glTF matrix, in metres, Y-up, and is relative to its parent. Compose the chain by walking the tree, exactly as glTF does. The root node's matrix is the identity.
  • InstancedNode.positionMm is the one exception: it is the node's absolute position in millimetres on SketchUp's Z-up axes, kept in that frame so it matches the baked path's InstanceNode.positionMm field for metadata comparisons.

Normals are left in local space deliberately. Transforming them is the consumer's job (glTF's own rule: inverse-transpose of the node's upper-left 3x3), and deferring it is what keeps non-uniform and mirrored (negative-determinant) scales correct without baking a per-placement copy of the normal buffer.

How material variants affect resource reuse

Two placements share a mesh resource when their effective rendered geometry is identical — which is not the same as sharing a definition ID. The same component renders differently depending on where it is placed, so resource identity also accounts for:

  • the inherited instance material (SketchUp's "paint the component"), which sets both the colour of unpainted faces and, through its texture's tile size, their UVs;
  • texture identity, not just averaged colour — two different images can average to the same RGB;
  • the effective layer's fallback colour, which is what an entirely unpainted face renders as.

So two instances of one definition painted with different materials produce two resource variants, while two instances with the same effective context share one. Front/back material resolution, per-face UV mapping and double-sided-vs-split geometry all follow deterministically from the definition plus those inputs. InstancedMeshResource.variantKey exposes the resolved context if you need to see why a definition produced more than one resource. Resource IDs (mesh_0, mesh_1, …) are stable and deterministic for a given file.

Edge and face visibility

SketchUp does not draw every edge it stores. Three flags suppress an edge: hidden (explicitly hidden), and soft/smooth — the smoothing flags that make a faceted surface read as curved. That last pair is why a rounded model carries far more edges than it appears to: every curve is triangles stitched by edges that define the shape and are never shown.

These are parsed and exposed on Edge, but acting on them is opt-in.

import { parseSkp, isDrawableEdge } from 'openskp';

const model = parseSkp(buffer);
const visible = model.root.edges.filter(isDrawableEdge);

How much that saves is strongly model-dependent — measured across this repository's fixtures, 27.3% of edges are non-drawable on aggregate, but that ranges from 0.2% on a mostly-flat model to 66.1% on a curved-surface one. Use it in wireframe/hidden-line renderers built on parseSkp() output, where drawing suppressed edges is both slower and visually wrong.

For faces, both scene builders take an opt-in flag:

const scene = buildScene(buffer, { respectEdgeVisibility: true });

This skips faces carrying SketchUp's "Hide" flag. It does not filter edges, because neither scene builder emits edges — their output is face triangles. Hidden faces are rare in practice (none in this repository's fixtures), so expect this to be correct rather than dramatic; the edge helper above is where the real saving lives. Off by default, since what SketchUp draws is a display policy rather than a parsing fact.

Cataloguing models

Two things an asset browser or block library needs, without paying for a full parse or a render:

import { extractThumbnail, buildScene } from 'openskp';

// The preview image SketchUp already saved inside the file. Reads
// container metadata only - no geometry parsing, no renderer.
const thumb = extractThumbnail(buffer);
if (thumb) {
  // thumb.data (raw bytes), thumb.mimeType, thumb.width, thumb.height
  fs.writeFileSync('cover.png', thumb.data);
}

// The model's overall size, computed during the bake.
const scene = buildScene(buffer);
if (scene.bounds) {
  const [w, h, d] = scene.bounds.size;       // metres, glTF Y-up
  console.log(`${w.toFixed(2)} x ${h.toFixed(2)} x ${d.toFixed(2)} m`);
  console.log('centre:', scene.bounds.center); // e.g. to frame a camera
}

extractThumbnail() prefers SketchUp's clean model_thumbnail over preview_thumbnail, which has the red/green/blue axis lines drawn in and reads as clutter on a catalogue card; thumb.source says which was used.

It returns null rather than throwing when there is no usable preview. That includes legacy (pre-2021 MFC) files: those embed PNGs too, but the container stores them without entry names, so a thumbnail cannot be distinguished from a material's texture image without guessing.

scene.bounds is null for a model with no geometry, so an empty model stays distinguishable from one sitting at the origin. buildInstancedScene() exposes the same field, computed from the placed node transforms so both builders agree.

Exporting an instanced GLB

import { SkpFile, toInstancedGLB } from 'openskp';
import * as fs from 'fs';

const instanced = SkpFile.open('model.skp').buildInstancedScene();

// Multiple glTF nodes reference the SAME mesh - the vertex and index
// buffers are written once, not once per placement.
fs.writeFileSync('model.glb', toInstancedGLB(instanced));

// Pass { textures: true } to embed the texture images, as with toGLB().
fs.writeFileSync('textured.glb', toInstancedGLB(instanced, { textures: true }));

A definition that resolves to several materials becomes one glTF mesh with several primitives, which is glTF's normal representation — not several nodes. toGLB() and buildScene() are untouched and still produce exactly what they always have.

Exporting

import { toGLB, toInstancedGLB, toOBJ, toMTL, exportOBJ, toSTLAscii, toSTLBinary, exportSTL, toPLYAscii, toPLYBinary, exportPLY, toDXF, exportDXF, toIFC, exportIFC, toFragments, toJSON } from 'openskp';

// Serialize a built scene straight to .glb bytes (in-memory, no disk I/O).
// Index buffers are written as UNSIGNED_SHORT when every index fits, which
// is the usual case - roughly halving the index data at no loss.
const glbBytes = toGLB(scene);

// Instancing-preserving GLB: one mesh, many nodes (see Choosing an API)
const instancedGlb = toInstancedGLB(instanced);

// Export to Wavefront OBJ string, plus a companion .mtl material library
const objText = toOBJ(scene, 'output.mtl');
const mtlText = toMTL(scene);

// Node.js only: writes both output.obj and output.mtl together
exportOBJ(scene, 'output.obj');

// Export to STL ASCII or Binary string/buffer
const stlText = toSTLAscii(scene);
const stlBytes = toSTLBinary(scene);

// Export to PLY ASCII or Binary string/buffer
const plyText = toPLYAscii(scene);
const plyBytes = toPLYBinary(scene);

// Export to AutoCAD 3D DXF text format (R2000 / AC1015 compliant)
const dxfText = toDXF(scene);

// Node.js only: export directly to a .dxf file
exportDXF(scene, 'output.dxf');

// Export to IFC4 / BIM (ISO 10303-21 STEP format string / file)
const ifcText = toIFC(scene);
exportIFC(scene, 'output.ifc');

// Export to ThatOpen Fragments (.frag binary format for Web BIM 3D streaming)
const fragBytes = toFragments(instanced);
// Or directly via SkpFile:
// const fragBytes = skp.toFragments();

// Full metadata as a JSON-compatible object
const meta = toJSON(model, scene);

Writing

OpenSKP can also create new .skp files from scratch — a genuine, from-scratch binary writer for the legacy MFC CArchive format (SketchUp 2013–2020), with no SketchUp SDK involved at any point. Ports the same feature set as the Python package's writer, verified byte-identical to Python's own output on the same input: geometry, materials (solid + PNG/JPEG textures), layers (with color and default visibility), component definitions with multiple instances, groups, nested definitions and nested group instances, per-instance rotation and visibility, explicit per-side texture positioning, custom key/value attribute dictionaries, circular faces and partial arcs, freeform polyline curves, faces with holes cut out, and non-planar auto-triangulation. openExisting() loads an existing legacy-format file and rebuilds it as a new builder, so more geometry can be added before saving. See src/create.ts for the full scope notes.

import { create } from 'openskp';

const builder = create();

// Materials and layers
const red = builder.addMaterial('Red', [255, 0, 0]);
const brick = builder.addTextureMaterial('Brick', brickPngBytes);
const roofLayer = builder.addLayer('Roof', { color: [180, 60, 40] });

// All addComponentDefinition/addGroup calls must come before any
// addInstance/addFace call - placing anything locks in the file's
// internal slot numbering for everything after it
const chair = builder.addComponentDefinition('Chair', (def) => {
  def.addFace([[0, 0, 0], [20, 0, 0], [20, 20, 0], [0, 20, 0]]);
});
builder.addInstance(chair, { translation: [50, 0, 0] });
builder.addInstance(chair, { translation: [100, 0, 0], hidden: true });

builder.addFace(
  [[0, 0, 0], [100, 0, 0], [100, 100, 0], [0, 100, 0]],
  { material: red, layer: roofLayer }
);

builder.save('output.skp');   // Node.js only; use builder.toBytes() in the browser

Editing an existing file

import { openExisting } from 'openskp';

const { builder, warnings, definitions } = openExisting('building.skp');
for (const w of warnings) console.log('not fully reproduced:', w);

builder.addCircle([0, 0, 100], [0, 0, 1], 50);
builder.save('building_edited.skp');

Generating code from a file

toTypeScriptCode() takes the opposite approach: instead of a builder you keep editing, it returns a string of source code — a re-runnable transcript of create() calls that rebuilds an equivalent file when run:

import { SkpFile, toTypeScriptCode } from 'openskp';

const model = SkpFile.open('building.skp').parse();
console.log(toTypeScriptCode(model));

Useful for handing a real model to an AI coding agent as editable starting code, or for a diffable, reviewable text representation of a .skp file. Shares the same fidelity scope as openExisting() above.

warnings is the honest account of what couldn't be faithfully reproduced from that specific source file. Every material/layer the source had is reachable on builder.materialsByName/builder.layersByName without a separate lookup, and definitions maps each replayed component definition's own name to its builder for placing more instances of something the source already defined.

Observability

parse()/buildScene() accept an optional options object for progress reporting and structured errors — silent by default:

const model = skp.parse({
  onProgress: (info) => console.log(`${info.stage}: ${info.current}/${info.total}`),
  onLog: (level, message) => console.log(`[${level}] ${message}`),
});

Parse failures throw SkpParseError, carrying stage, recordIndex, totalRecords, tag, and definitionId context, with the original error preserved as .cause. See docs/OBSERVABILITY.md for the full cross-language reference.

Known limitation: large files

Memory use scales worse than the other four ports on very large files — a 113 MB file needs 8–16 GB of Node heap, and files beyond ~150-200 MB may not parse in a browser tab at all (a typical tab's heap ceiling is ~4 GB). Root cause: V8's per-object overhead on millions of small geometry objects. See docs/DEVELOPER_GUIDE.md for verified numbers before parsing very large files in this package.

Package Structure

| Module | Purpose | |---|---| | parser.ts | TLV binary parser for SketchUp's internal format | | model.ts | Interfaces for geometry, layers, materials, scenes | | legacy.ts | Legacy MFC container support (SketchUp 2013–2020) | | vff.ts | VFF/ZIP container handling (fflate-based) | | triangulator.ts | Planar polygon triangulation (ported earcut) | | transforms.ts | 3D matrix transforms and coordinate conversions | | observability.ts | Progress/log callback types | | errors.ts | SkpParseError and structured failure context | | face-groups.ts | Local-space face grouping shared by both scene builders | | instanced.ts | Instancing-preserving scene builder (buildInstancedScene) | | instanced-glb.ts | GLB exporter that reuses one mesh across many nodes | | index.ts | Public entry point — SkpFile, parseSkp, buildScene, buildInstancedScene, toGLB, toInstancedGLB, toJSON |

Requirements

Node.js ≥ 16, or any modern browser. No native dependencies.

Used in Production

OpenSKP powers the SketchUp import pipeline for FrameSmart (a 3D collaboration platform with nearly 200 active users) and IngeTrazo (a SketchUp-alternative 3D modeler with a BIM → IFC bridge). Using OpenSKP in your own project? Open an issue or a PR to get added here.

Contributing

The Python package in ../python/ and this package are both full, independent implementations at parity — neither is a stub for the other. See CONTRIBUTING.md for guidelines.

License

MIT — see LICENSE