Skip to content

Angular 17+ Three.js 3D model cropper dialog for GLB/FBX with cheap geometry pruning, GLB export, and fully customizable UI.

License

Notifications You must be signed in to change notification settings

AlexRynas/ng-three-model-cropper

Repository files navigation

ng-three-model-cropper

Angular 17+ Three.js 3D Model Cropper Library with cheap geometry cropping.

npm version npm downloads CI codecov License Angular Three.js

Overview

A highly configurable, UI-agnostic 3D model cropper component for Angular applications. Load GLB/FBX models, define a crop box, apply "cheap" triangle-pruning cropping, and export the result as GLB.

Key Features

  • Multi-format Support: Load GLB, GLTF, and FBX 3D models
  • Cheap Cropping: Triangle-pruning based cropping (no boolean CSG operations)
  • GLB Export: Export cropped models as binary GLB files
  • Visual Helpers: Configurable crop box color, grid helper, and view helper (axis indicator)
  • Angular 17-20 Compatible: Built with Angular 17, works with apps 17-20
  • Zoneless Friendly: Signal-based state management, no zone.js dependency
  • Highly Configurable: Template customization, content projection, label overrides
  • UI Agnostic: Works with MatDialog or any dialog/container system
  • Partial Ivy: Published with partial compilation for broad compatibility

Installation

npm install ng-three-model-cropper three
npm install -D @types/three

Quick Start

import { Component } from '@angular/core';
import { ModelCropperComponent, CropResult } from 'ng-three-model-cropper';

@Component({
  selector: 'app-model-editor',
  standalone: true,
  imports: [ModelCropperComponent],
  template: `
    <ntmc-model-cropper
      [srcUrl]="modelUrl"
      [downloadMode]="'download'"
      [filename]="'my-cropped-model.glb'"
      (cropApplied)="onCropApplied($event)"
      (fileReady)="onFileReady($event)"
      (loadError)="onLoadError($event)"
    />
  `,
  styles: [
    `
      :host {
        display: block;
        width: 100%;
        height: 600px;
      }
    `,
  ],
})
export class ModelEditorComponent {
  modelUrl = 'assets/models/sample.glb';

  onCropApplied(result: CropResult): void {
    console.log(`Removed ${result.trianglesRemoved} triangles`);
  }

  onFileReady(buffer: ArrayBuffer): void {
    // Handle the exported GLB ArrayBuffer (e.g., upload to server)
  }

  onLoadError(message: string): void {
    console.error('Failed to load model:', message);
  }
}

Component API

Inputs

Input Type Default Description
srcUrl string required URL to the 3D model file (GLB/GLTF/FBX)
initialCropBox CropBoxConfig auto-calculated Initial crop box bounds
initialTransform MeshTransformConfig identity Initial position/rotation
rotationUnit 'radians' | 'degrees' 'radians' Unit for rotation values passed to setRotation (UI context)
downloadMode 'download' | 'emit' 'download' Export behavior
filename string 'cropped-model.glb' Download filename
cropBoxColor string '#00ff00' Hex color for crop box visualization
showGrid boolean false Show grid helper in the scene
showViewHelper boolean false Show view helper (axis indicator)
sceneBackgroundColor string '#2a2a2a' CSS color for scene background; supports transparency (rgba, hex with alpha, named colors)
showLoadingOverlay boolean true Show the loading overlay with spinner
showErrorOverlay boolean true Show the error overlay
showLoadingProgress boolean true Show loading progress percentage
spinnerColor string '#4caf50' Hex color for the loading spinner
uiTemplate TemplateRef - Custom UI template
labelsConfig Partial<ModelCropperLabels> defaults UI label overrides

Outputs

Output Type Description
cropApplied CropResult Emitted after cropping with statistics
fileReady ArrayBuffer Emitted with GLB data (emit mode only)
loadError string Emitted when model loading fails
exportError string Emitted when export fails
loadingProgressChange LoadingProgress Emitted during model loading progress

Custom UI Template

Override the default UI panel with your own template:

@Component({
  template: `
    <ntmc-model-cropper [srcUrl]="modelUrl" [uiTemplate]="customUI">
      <ng-template #customUI let-ctx>
        <!-- ctx is ModelCropperUiContext -->
        <div class="my-custom-panel">
          <mat-slider
            [value]="ctx.cropBox.minX"
            (input)="ctx.setCropBoxValue('minX', $event.value)"
          />
          <button mat-raised-button (click)="ctx.applyCrop()">Crop Model</button>
          <button mat-button (click)="ctx.download()">Export</button>
        </div>
      </ng-template>
    </ntmc-model-cropper>
  `,
})
export class CustomUiComponent {}

UI Context API (ModelCropperUiContext)

Property/Method Description
cropBox Current crop box configuration
rotationUnit Unit used for setRotation and meshTransformUi
meshTransform Current position/rotation values
meshTransformUi Position + rotation values for UI (rotation in rotationUnit)
loadingState 'idle' | 'loading' | 'loaded' | 'error'
loadingProgress Detailed loading progress information
errorMessage Error message if any
boxVisible Crop box visibility state
cropBoxColor Current crop box color (hex string)
gridVisible Grid helper visibility state
viewHelperVisible View helper visibility state
canApplyCrop Whether cropping is available (model loaded)
canExport Whether export is available (crop applied and valid)
setCropBox(box) Set entire crop box
setCropBoxValue(key, value) Set single crop box value
setMeshTransform(transform) Set entire transform
setPosition(partial) Update position values
setRotation(partial) Update rotation values

Notes:

  • meshTransform.rotation is stored internally in radians (Three.js native).
  • For numeric inputs (native steppers), prefer meshTransformUi.rotation so degrees/radians display stays stable and step-aligned. | toggleBoxVisibility(visible) | Show/hide crop box | | setCropBoxColor(color) | Set crop box color (hex string) | | toggleGridVisibility(visible) | Show/hide grid helper | | toggleViewHelperVisibility(visible) | Show/hide view helper | | applyCrop() | Execute cropping | | download() | Trigger export | | resetCropBox() | Reset crop box to defaults | | resetTransform() | Reset transform to identity |

Using with MatDialog

import { MatDialog } from '@angular/material/dialog';
import { ModelCropperComponent } from 'ng-three-model-cropper';

@Component({...})
export class AppComponent {
  constructor(private dialog: MatDialog) {}

  openCropper(): void {
    const dialogRef = this.dialog.open(ModelCropperDialogComponent, {
      width: '90vw',
      height: '80vh',
      data: { modelUrl: 'assets/model.glb' }
    });

    dialogRef.afterClosed().subscribe(result => {
      if (result?.fileBuffer) {
        // Handle exported GLB
      }
    });
  }
}

@Component({
  standalone: true,
  imports: [ModelCropperComponent, MatDialogModule],
  template: `
    <mat-dialog-content>
      <ntmc-model-cropper
        [srcUrl]="data.modelUrl"
        [downloadMode]="'emit'"
        (fileReady)="onFileReady($event)"
        (cropApplied)="onCropApplied($event)"
      />
    </mat-dialog-content>
    <mat-dialog-actions>
      <button mat-button mat-dialog-close>Cancel</button>
      <button mat-button [mat-dialog-close]="result">Save</button>
    </mat-dialog-actions>
  `
})
export class ModelCropperDialogComponent {
  result: { fileBuffer?: ArrayBuffer; cropResult?: CropResult } = {};

  constructor(@Inject(MAT_DIALOG_DATA) public data: { modelUrl: string }) {}

  onFileReady(buffer: ArrayBuffer): void {
    this.result.fileBuffer = buffer;
  }

  onCropApplied(cropResult: CropResult): void {
    this.result.cropResult = cropResult;
  }
}

Types

interface CropBoxConfig {
  minX: number;
  minY: number;
  minZ: number;
  maxX: number;
  maxY: number;
  maxZ: number;
}

interface MeshTransformConfig {
  position: { x: number; y: number; z: number };
  rotation: { x: number; y: number; z: number };
}

/**
 * Angle unit for rotation values
 */
type AngleUnit = 'radians' | 'degrees';

interface CropResult {
  success: boolean;
  trianglesRemoved: number;
  trianglesKept: number;
  meshesProcessed: number;
}

interface LoadingProgress {
  state: LoadingState;
  percentage: number;
  loaded: number;
  total: number;
  message: string;
}

type DownloadMode = 'download' | 'emit';
type LoadingState = 'idle' | 'loading' | 'loaded' | 'error';

Architecture

The library is organized for multi-version compatibility:

src/lib/
├── core/                 # Framework-agnostic (Three.js only)
│   ├── types.ts          # Interfaces and type definitions
│   ├── ui-context.ts     # UI context interface
│   ├── model-crop-engine.ts  # Main Three.js engine
│   └── cheap-cropper.ts  # Triangle-pruning cropper
└── ng/                   # Angular 17 adapter
    ├── model-cropper.service.ts   # Angular service wrapper
    └── model-cropper.component.ts # Standalone component

Testing

The library includes a comprehensive test suite covering all modules. Code coverage is tracked via Codecov.

Running Tests

# Run library tests (headless, single run)
npm run test:lib

# Run library tests (watch mode for development)
npm run test:lib:watch

# Run demo app tests
npm test

Development (Demo App)

# Run the demo app (recommended)
npm run dev

In development mode, the demo app resolves ng-three-model-cropper directly from the library source (projects/model-cropper/src). This avoids intermittent Vite pre-transform resolution failures that can happen when using the built dist/ output while it is being regenerated.

Optional: Test the built dist/ output

If you specifically want to validate the packaged output in dist/model-cropper:

npm run dev:dist

This runs a one-time library build first. If you want live rebuilds of the library output, run npm run watch:lib in a separate terminal.

Future Angular Versions

The core/ folder is framework-agnostic. To support Angular 19/20+:

  1. Create a new branch (e.g., angular-20)
  2. Update workspace to Angular 20
  3. Create ng/adapter-angular20/ with updated components
  4. Publish as [email protected] with updated peer dependencies

Build & Publish

# Build library
npm run build:lib

# Publish to npm
npm run publish:lib

Requirements

  • Angular 17-20
  • Three.js >= 0.150.0
  • TypeScript 5.x

License

Apache License 2.0

About

Angular 17+ Three.js 3D model cropper dialog for GLB/FBX with cheap geometry pruning, GLB export, and fully customizable UI.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published