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

@manifold-studio/wrapper

v0.3.7

Published

A synchronous TypeScript wrapper for [ManifoldCAD](https://github.com/elalish/manifold) with operation tracking, export utilities, and headless pipeline support.

Readme

@manifold-studio/wrapper

A synchronous TypeScript wrapper for ManifoldCAD with operation tracking, export utilities, and headless pipeline support.

🚀 The Problem This Solves

ManifoldCAD is an excellent 3D modeling library, but it's distributed as a WASM module that requires async initialization. This creates friction when trying to:

  • Use ManifoldCAD with the broader NPM ecosystem
  • Write clean, synchronous modeling code
  • Integrate with modern TypeScript tooling
  • Build development environments with hot reloading

@manifold-studio/wrapper solves this by providing a synchronous API that handles all the async complexity behind the scenes using top-level await.

✨ Key Features

  • 🔄 Synchronous API - Write clean modeling code without async/await
  • 📊 Operation Tracking - Debug and visualize your modeling operations
  • 📦 Export Utilities - Built-in OBJ and GLB export functions
  • ⚡ Pipeline Ready - Headless generation for CI/CD and automation
  • 🎛️ Parametric System - Type-safe parameter definitions with UI generation
  • 🔍 Full Type Safety - Complete TypeScript definitions
  • 🪶 Zero Overhead - Transparent proxy to original ManifoldCAD API

📦 Installation

npm install @manifold-studio/wrapper

🚀 Quick Start

import { Manifold, exportToOBJ } from "@manifold-studio/wrapper";

// Create 3D models with synchronous API - no async/await needed!
const box = Manifold.cube([20, 15, 10], true);
const sphere = Manifold.sphere(8);
const result = Manifold.difference(box, sphere);

// Export to file formats
const objData = exportToOBJ(result);
console.log("Generated OBJ:", objData);

That's it! No WASM initialization, no async complexity - just clean modeling code.

🔧 Core Features

Synchronous ManifoldCAD API

The wrapper provides transparent access to the complete ManifoldCAD API, but synchronously:

import { Manifold, CrossSection } from "@manifold-studio/wrapper";

// All the ManifoldCAD operations you know and love
const cube = Manifold.cube([10, 10, 10]);
const cylinder = Manifold.cylinder(5, 10);
const union = Manifold.union([cube, cylinder]);

// Cross-section operations
const square = CrossSection.square([5, 5]);
const circle = CrossSection.circle(3);
const extruded = square.extrude(10);

Operation Tracking

Track the history of operations for debugging and visualization:

import { Manifold, getOperationRegistry } from "@manifold-studio/wrapper";

// Create some operations
const box = Manifold.cube([10, 10, 10]);
const sphere = Manifold.sphere(5);
const result = Manifold.difference([box, sphere]);

// Access the operation history
const registry = getOperationRegistry();
const operations = registry.buildTree(result.getOperationId());

console.log(
  "Operation chain:",
  operations.map((op) => op.type)
);
// Output: ['cube', 'sphere', 'difference']

Export Utilities

Built-in export functions for common 3D formats:

import { Manifold, exportToOBJ, manifoldToGLB } from "@manifold-studio/wrapper";

const model = Manifold.cube([20, 20, 20]);

// Export to OBJ format
const objResult = exportToOBJ(model, { filename: "my-cube.obj" });
console.log(objResult.content); // OBJ file content

// Export to GLB format (requires @gltf-transform/core)
const glbBuffer = await manifoldToGLB(model);
// Save glbBuffer to file

Parametric Configuration System

Create type-safe parametric models with automatic UI generation:

import { Manifold, P, createConfig } from "@manifold-studio/wrapper";

// Define parameters with types and constraints
const boxConfig = createConfig(
  {
    width: P.number(20, 5, 50, 1), // value, min, max, step
    height: P.number(15, 5, 50, 1),
    depth: P.number(10, 5, 50, 1),
    centered: P.boolean(true),
    material: P.choice("plastic", ["plastic", "metal", "wood"]),
  },
  (params) => {
    // Generate model using parameters
    return Manifold.cube(
      [params.width, params.height, params.depth],
      params.centered
    );
  },
  {
    name: "Parametric Box",
    description: "A customizable box with adjustable dimensions",
  }
);

// Use in applications
const model = boxConfig.generateModel(boxConfig.getDefaults());

## 📚 API Reference

### Core Manifold API

All ManifoldCAD functions are re-exported synchronously:

```typescript
// Static construction methods
Manifold.cube(size: Vec3 | number, center?: boolean): Manifold
Manifold.sphere(radius: number, circularSegments?: number): Manifold
Manifold.cylinder(height: number, radiusLow: number, radiusHigh?: number): Manifold
Manifold.tetrahedron(): Manifold

// Boolean operations
Manifold.union(manifolds: Manifold[]): Manifold
Manifold.difference(manifolds: Manifold[]): Manifold
Manifold.intersection(manifolds: Manifold[]): Manifold

// Instance methods
manifold.translate(offset: Vec3): Manifold
manifold.rotate(angles: Vec3): Manifold
manifold.scale(factor: Vec3 | number): Manifold
manifold.getMesh(): MeshData

Export Functions

// OBJ export
exportToOBJ(manifold: Manifold, options?: ExportOptions): ExportResult
manifoldToOBJ(manifold: Manifold): string

// GLB export (async due to gltf-transform library)
manifoldToGLB(manifold: Manifold): Promise<ArrayBuffer>

// Types
interface ExportOptions {
  filename?: string;
}

interface ExportResult {
  content: string;
  filename: string;
}

Operation Tracking

// Get the global operation registry
getOperationRegistry(): OperationRegistry

// Registry methods
registry.get(id: string): OperationInfo | undefined
registry.buildTree(rootId: string): OperationInfo[]
registry.getAllOperations(): OperationInfo[]
registry.clear(): void

// Operation info structure
interface OperationInfo {
  id: string;
  type: string;
  inputIds: string[];
  metadata: Record<string, any>;
  timestamp: number;
}

// Tracked manifold methods
manifold.getOperationId(): string

Pipeline Utilities

// Parameter handling
isParametricConfig(obj: any): obj is ParametricConfig
extractDefaultParams(config: ParametricConfig): Record<string, any>
mergeParameters(defaults: Record<string, any>, userParams: Record<string, any>): Record<string, any>
parseParameterString(paramStr: string): Record<string, any>
parseParameterValue(value: string): any
validateRequiredParameters(config: ParametricConfig, params: Record<string, any>): void
getParameterInfo(config: ParametricConfig): Array<{name: string, type: string, value: any}>

Parametric Types

// Parameter builders
P.number(value: number, min: number, max: number, step?: number): TweakpaneNumberParam
P.boolean(value: boolean): TweakpaneBooleanParam
P.string(value: string): TweakpaneStringParam
P.choice<T>(value: T, options: T[]): TweakpaneParam<T>

// Configuration creator
createConfig<T>(
  parameters: T,
  generateModel: (params: ExtractParamTypes<T>) => Manifold,
  metadata?: { name?: string; description?: string }
): ParametricConfig

// Types
interface ParametricConfig {
  parameters: Record<string, ParameterConfig>;
  generateModel: (params: any) => Manifold;
  metadata?: { name?: string; description?: string };
  getDefaults(): Record<string, any>;
}

📊 Operation Tracking Deep Dive

Operation tracking allows you to understand and debug the sequence of operations that created a 3D model.

How It Works

The wrapper uses JavaScript Proxies to intercept ManifoldCAD operations and record them in a global registry:

import { Manifold, getOperationRegistry } from "@manifold-studio/wrapper";

// Each operation gets a unique ID and is tracked
const box = Manifold.cube([10, 10, 10]); // op_0: cube
const sphere = Manifold.sphere(5); // op_1: sphere
const moved = sphere.translate([5, 0, 0]); // op_2: translate (input: op_1)
const result = Manifold.difference(box, moved); // op_3: difference (inputs: op_0, op_2)

// Access the operation tree
const registry = getOperationRegistry();
const tree = registry.buildTree(result.getOperationId());

console.log("Operations in dependency order:");
tree.forEach((op) => {
  console.log(`${op.id}: ${op.type} (inputs: ${op.inputIds.join(", ")})`);
});

Use Cases

  • Debugging: Understand why a model looks unexpected
  • Visualization: Show operation history in development tools
  • Optimization: Identify redundant operations
  • Documentation: Auto-generate model creation steps

Accessing Operation Data

// Get detailed information about any operation
const registry = getOperationRegistry();
const operation = registry.get("op_3");

console.log({
  id: operation.id, // 'op_3'
  type: operation.type, // 'difference'
  inputIds: operation.inputIds, // ['op_0', 'op_2']
  timestamp: operation.timestamp,
  metadata: operation.metadata,
});

// Clear the registry (useful for testing)
registry.clear();

📦 Export Capabilities

The wrapper provides built-in export functionality for common 3D file formats.

OBJ Export

import { Manifold, exportToOBJ, manifoldToOBJ } from "@manifold-studio/wrapper";

const model = Manifold.sphere(10);

// Method 1: Full export with metadata
const result = exportToOBJ(model, { filename: "sphere.obj" });
console.log(result.filename); // 'sphere.obj'
console.log(result.content); // OBJ file content

// Method 2: Just get the OBJ string
const objString = manifoldToOBJ(model);

GLB Export

GLB export requires the @gltf-transform/core dependency and is asynchronous:

import { Manifold, manifoldToGLB } from "@manifold-studio/wrapper";

const model = Manifold.cube([20, 20, 20]);

// Export to GLB binary format
const glbBuffer = await manifoldToGLB(model);

// Save to file (Node.js)
import { writeFileSync } from "fs";
writeFileSync("model.glb", Buffer.from(glbBuffer));

// Or create download link (browser)
const blob = new Blob([glbBuffer], { type: "model/gltf-binary" });
const url = URL.createObjectURL(blob);

Export Options

interface ExportOptions {
  filename?: string; // Default filename for the export
}

interface ExportResult {
  content: string; // File content
  filename: string; // Resolved filename
}

⚡ Pipeline Integration

The wrapper includes utilities that power headless 3D model generation in command-line environments.

Parameter Processing

import {
  parseParameterString,
  mergeParameters,
  extractDefaultParams,
} from "@manifold-studio/wrapper";

// Parse command-line parameter strings
const params = parseParameterString("width=20,height=15,centered=true");
// Result: { width: 20, height: 15, centered: true }

// Merge with defaults
const defaults = { width: 10, height: 10, depth: 5 };
const merged = mergeParameters(defaults, params);
// Result: { width: 20, height: 15, depth: 5 }

Parametric Config Validation

import {
  isParametricConfig,
  validateRequiredParameters,
  getParameterInfo,
} from "@manifold-studio/wrapper";

// Check if an export is a parametric config
if (isParametricConfig(modelExport)) {
  // Extract parameter information
  const paramInfo = getParameterInfo(modelExport);
  console.log("Available parameters:", paramInfo);

  // Validate user parameters
  const userParams = { width: 25, invalidParam: "test" };
  validateRequiredParameters(modelExport, userParams);
}

## 🔧 Technical Details

### WASM Loading Strategy

The wrapper solves the "WASM async problem" using **top-level await**:

```typescript
// In src/lib/manifold.ts
import ManifoldModule from 'manifold-3d';

// Top-level await - this blocks module loading until WASM is ready
const manifoldModule = await ManifoldModule();
manifoldModule.setup();

// Now we can export synchronous functions
export function cube(size: Vec3 | number, center = false): Manifold {
  return manifoldModule.Manifold.cube(size, center); // Synchronous!
}

How it works:

  1. When JavaScript loads the wrapper module, it sees the top-level await
  2. Module loading pauses until the WASM module initializes
  3. Only after WASM is ready does the module finish loading
  4. All exported functions can now use the initialized WASM synchronously

This concentrates all async complexity at application startup, keeping your modeling code clean and synchronous.

Proxy Architecture for Operation Tracking

Operation tracking uses JavaScript Proxies to intercept method calls without modifying the original ManifoldCAD API:

// Simplified version of the tracking implementation
export function createTrackedManifold(OriginalManifold) {
  return new Proxy(OriginalManifold, {
    construct(target, args) {
      const instance = new target(...args);
      return createTrackedInstance(instance);
    },

    get(target, prop) {
      const value = target[prop];
      if (typeof value === "function") {
        return function (...args) {
          const result = value.apply(target, args);

          // If result is a Manifold, track the operation
          if (isManifoldInstance(result)) {
            const operationId = registry.generateId();
            registry.register({
              id: operationId,
              type: prop,
              inputIds: extractInputIds(args),
              metadata: {},
              timestamp: Date.now(),
            });

            // Attach operation ID to result
            result.getOperationId = () => operationId;
          }

          return result;
        };
      }
      return value;
    },
  });
}

This approach provides:

  • Zero API changes - Original ManifoldCAD API is preserved exactly
  • Transparent tracking - Operations are recorded automatically
  • Minimal overhead - Proxy calls are very fast
  • Complete coverage - All operations are tracked, including chained calls

Type Safety

The wrapper provides complete TypeScript definitions:

// All original types are preserved and re-exported
export type Vec3 = [number, number, number];
export type ManifoldType = /* original Manifold type */;

// Additional wrapper-specific types
export interface OperationInfo {
  id: string;
  type: string;
  inputIds: string[];
  metadata: Record<string, any>;
  timestamp: number;
}

// Parametric system types with full inference
export type ExtractParamTypes<T> = {
  [K in keyof T]: T[K] extends TweakpaneParam<infer U> ? U : never;
};

Performance Considerations

  • WASM Loading: One-time cost at application startup
  • Operation Tracking: Minimal overhead (~1-2% performance impact)
  • Memory Usage: Operation registry grows with model complexity
  • Export Performance: OBJ export is fast, GLB export has some overhead due to gltf-transform

Optimization tips:

  • Clear operation registry periodically: getOperationRegistry().clear()
  • Disable tracking in production if not needed
  • Use OBJ export for better performance when GLB features aren't required

🚨 Troubleshooting

Common Issues

"Module not found" or WASM loading errors

Error: Cannot resolve module 'manifold-3d'

Solution: Ensure manifold-3d is installed:

npm install manifold-3d

Top-level await not supported

SyntaxError: Unexpected reserved word 'await'

Solution: Ensure your environment supports top-level await:

  • Node.js 14.8+ with "type": "module" in package.json
  • Modern bundlers (Vite, Webpack 5+, Rollup)
  • Modern browsers (Chrome 89+, Firefox 89+, Safari 15+)

TypeScript errors with Manifold types

Property 'cube' does not exist on type 'typeof Manifold'

Solution: Ensure TypeScript can find the type definitions:

// tsconfig.json
{
  "compilerOptions": {
    "moduleResolution": "node",
    "esModuleInterop": true
  }
}

GLB export fails

Error: @gltf-transform/core not found

Solution: Install the optional GLB dependency:

npm install @gltf-transform/core

Performance Issues

Slow model generation

  • Check operation registry size: getOperationRegistry().getAllOperations().length
  • Clear registry if it's large: getOperationRegistry().clear()
  • Profile your model code for inefficient operations

Memory leaks

  • Operation registry holds references to operation metadata
  • Clear registry periodically in long-running applications
  • Consider disabling tracking in production builds

🎯 Advanced Usage

Custom Operation Metadata

You can attach custom metadata to operations for enhanced tracking:

import { Manifold, getOperationRegistry } from "@manifold-studio/wrapper";

// The registry automatically captures operation info, but you can add custom metadata
const box = Manifold.cube([10, 10, 10]);
const registry = getOperationRegistry();

// Find the operation and add metadata
const operations = registry.getAllOperations();
const cubeOp = operations.find((op) => op.type === "cube");
if (cubeOp) {
  cubeOp.metadata.purpose = "base structure";
  cubeOp.metadata.material = "steel";
}

Working with the Operation Registry

// Get all operations in chronological order
const allOps = registry.getAllOperations();

// Find operations by type
const booleanOps = allOps.filter((op) =>
  ["union", "difference", "intersection"].includes(op.type)
);

// Build dependency graph
function buildDependencyGraph(rootId: string) {
  const tree = registry.buildTree(rootId);
  const graph = new Map();

  tree.forEach((op) => {
    graph.set(op.id, {
      operation: op,
      dependencies: op.inputIds.map((id) => registry.get(id)).filter(Boolean),
    });
  });

  return graph;
}

🤝 Contributing

Development Setup

# Clone the repository
git clone https://github.com/your-org/manifold-studio.git
cd manifold-studio

# Install dependencies
npm install

# Build the wrapper package
cd packages/wrapper
npm run build

# Run tests
npm test

# Watch mode for development
npm run dev

Testing

The wrapper uses Vitest for testing:

# Run all tests
npm test

# Watch mode
npm run test:watch

# Test specific functionality
npm test -- --grep "operation tracking"

Architecture Decisions

Why Proxies for Tracking?

  • Preserves the original ManifoldCAD API exactly
  • No performance overhead for non-tracked usage
  • Automatic coverage of all operations including future additions

Why Top-Level Await?

  • Concentrates async complexity at module boundaries
  • Enables clean, synchronous modeling code
  • Works with modern JavaScript environments

Why Global Operation Registry?

  • Simple to use across different parts of an application
  • Avoids threading operation context through all function calls
  • Easy to clear/reset for testing

Code Style

  • Use TypeScript for all source files
  • Follow existing naming conventions
  • Add JSDoc comments for public APIs
  • Include tests for new functionality

📄 License

MIT License - see the LICENSE file for details.

🔗 Related Projects


Built with ❤️ for the 3D modeling community