Angular 17+ Three.js 3D Model Cropper Library with cheap geometry cropping.
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.
- 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
npm install ng-three-model-cropper three
npm install -D @types/threeimport { 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);
}
}| 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 |
| 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 |
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 {}| 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.rotationis stored internally in radians (Three.js native).- For numeric inputs (native steppers), prefer
meshTransformUi.rotationso 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 |
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;
}
}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';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
The library includes a comprehensive test suite covering all modules. Code coverage is tracked via Codecov.
# 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# Run the demo app (recommended)
npm run devIn 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.
If you specifically want to validate the packaged output in dist/model-cropper:
npm run dev:distThis 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.
The core/ folder is framework-agnostic. To support Angular 19/20+:
- Create a new branch (e.g.,
angular-20) - Update workspace to Angular 20
- Create
ng/adapter-angular20/with updated components - Publish as
[email protected]with updated peer dependencies
# Build library
npm run build:lib
# Publish to npm
npm run publish:lib- Angular 17-20
- Three.js >= 0.150.0
- TypeScript 5.x
Apache License 2.0