@thinkersteps_labs/3d-viewer-angular 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -107
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/viewer.component.d.ts +96 -0
- package/dist/src/viewer.module.d.ts +18 -0
- package/dist/swatches/jewelry-customizer.component.js +117 -117
- package/dist/swatches/metal-swatches.component.js +183 -183
- package/dist/swatches/stone-swatches.component.js +183 -183
- package/package.json +49 -49
package/README.md
CHANGED
|
@@ -1,107 +1,107 @@
|
|
|
1
|
-
# @thinkersteps_labs/3d-viewer-angular
|
|
2
|
-
|
|
3
|
-
Official Angular component wrapper for `@thinkersteps_labs/3d-viewer-core`. Renders photorealistic 3D jewelry and customizer canvases in Angular 18+ applications (including standalone components and legacy `NgModule` architectures).
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Installation
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
pnpm add @thinkersteps_labs/3d-viewer-angular @thinkersteps_labs/3d-viewer-core three
|
|
11
|
-
# or with npm
|
|
12
|
-
npm install @thinkersteps_labs/3d-viewer-angular @thinkersteps_labs/3d-viewer-core three
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
> **Peer Dependencies**: Requires `@angular/core >= 18.0.0` and `three >= 0.150.0`.
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## 1. Turnkey Customizer (`<td-jewelry-customizer>`)
|
|
20
|
-
|
|
21
|
-
```typescript
|
|
22
|
-
import { Component } from '@angular/core'
|
|
23
|
-
import { JewelryCustomizerComponent } from '@thinkersteps_labs/3d-viewer-angular'
|
|
24
|
-
|
|
25
|
-
@Component({
|
|
26
|
-
selector: 'app-product-page',
|
|
27
|
-
standalone: true,
|
|
28
|
-
imports: [JewelryCustomizerComponent],
|
|
29
|
-
template: `
|
|
30
|
-
<td-jewelry-customizer
|
|
31
|
-
[src]="'https://cdn.example.com/models/solitaire-ring.glb'"
|
|
32
|
-
[(metal)]="activeMetal"
|
|
33
|
-
[(stone)]="activeStone"
|
|
34
|
-
[height]="520"
|
|
35
|
-
[autoRotate]="true"
|
|
36
|
-
></td-jewelry-customizer>
|
|
37
|
-
`
|
|
38
|
-
})
|
|
39
|
-
export class ProductPageComponent {
|
|
40
|
-
activeMetal = 'yellow-gold-18k'
|
|
41
|
-
activeStone = 'diamond-d'
|
|
42
|
-
}
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
---
|
|
46
|
-
|
|
47
|
-
## 2. Composable Primitives (`<td-viewer>` + Swatches)
|
|
48
|
-
|
|
49
|
-
```typescript
|
|
50
|
-
import { Component } from '@angular/core'
|
|
51
|
-
import {
|
|
52
|
-
ViewerComponent,
|
|
53
|
-
MetalSwatchesComponent,
|
|
54
|
-
StoneSwatchesComponent,
|
|
55
|
-
type MetalColorProp,
|
|
56
|
-
type StoneColorProp,
|
|
57
|
-
} from '@thinkersteps_labs/3d-viewer-angular'
|
|
58
|
-
|
|
59
|
-
@Component({
|
|
60
|
-
selector: 'app-product-detail',
|
|
61
|
-
standalone: true,
|
|
62
|
-
imports: [ViewerComponent, MetalSwatchesComponent, StoneSwatchesComponent],
|
|
63
|
-
template: `
|
|
64
|
-
<div style="max-width: 840px; margin: 0 auto;">
|
|
65
|
-
<td-viewer
|
|
66
|
-
[src]="modelUrl"
|
|
67
|
-
[metalColor]="activeMetal"
|
|
68
|
-
[stoneColor]="activeStone"
|
|
69
|
-
[height]="500"
|
|
70
|
-
[autoRotate]="true"
|
|
71
|
-
></td-viewer>
|
|
72
|
-
|
|
73
|
-
<div style="margin-top: 16px; display: flex; flex-direction: column; gap: 14px;">
|
|
74
|
-
<td-metal-swatches [(value)]="activeMetal" [variant]="'pills'"></td-metal-swatches>
|
|
75
|
-
<td-stone-swatches [(value)]="activeStone" [variant]="'pills'"></td-stone-swatches>
|
|
76
|
-
</div>
|
|
77
|
-
</div>
|
|
78
|
-
`
|
|
79
|
-
})
|
|
80
|
-
export class ProductDetailComponent {
|
|
81
|
-
modelUrl = 'https://cdn.example.com/models/solitaire-ring.glb'
|
|
82
|
-
activeMetal: MetalColorProp = 'yellow-gold-18k'
|
|
83
|
-
activeStone: StoneColorProp = 'diamond-d'
|
|
84
|
-
}
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
---
|
|
88
|
-
|
|
89
|
-
## 3. NgModule Apps (e.g. Angular ERP)
|
|
90
|
-
|
|
91
|
-
Import `ViewerModule` in your feature module to get access to `<td-viewer>`, `<td-metal-swatches>`, `<td-stone-swatches>`, and `<td-jewelry-customizer>`:
|
|
92
|
-
|
|
93
|
-
```typescript
|
|
94
|
-
import { NgModule } from '@angular/core'
|
|
95
|
-
import { ViewerModule } from '@thinkersteps_labs/3d-viewer-angular'
|
|
96
|
-
|
|
97
|
-
@NgModule({
|
|
98
|
-
imports: [ViewerModule],
|
|
99
|
-
})
|
|
100
|
-
export class ProductModule {}
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
---
|
|
104
|
-
|
|
105
|
-
## License
|
|
106
|
-
|
|
107
|
-
MIT
|
|
1
|
+
# @thinkersteps_labs/3d-viewer-angular
|
|
2
|
+
|
|
3
|
+
Official Angular component wrapper for `@thinkersteps_labs/3d-viewer-core`. Renders photorealistic 3D jewelry and customizer canvases in Angular 18+ applications (including standalone components and legacy `NgModule` architectures).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @thinkersteps_labs/3d-viewer-angular @thinkersteps_labs/3d-viewer-core three
|
|
11
|
+
# or with npm
|
|
12
|
+
npm install @thinkersteps_labs/3d-viewer-angular @thinkersteps_labs/3d-viewer-core three
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
> **Peer Dependencies**: Requires `@angular/core >= 18.0.0` and `three >= 0.150.0`.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 1. Turnkey Customizer (`<td-jewelry-customizer>`)
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { Component } from '@angular/core'
|
|
23
|
+
import { JewelryCustomizerComponent } from '@thinkersteps_labs/3d-viewer-angular'
|
|
24
|
+
|
|
25
|
+
@Component({
|
|
26
|
+
selector: 'app-product-page',
|
|
27
|
+
standalone: true,
|
|
28
|
+
imports: [JewelryCustomizerComponent],
|
|
29
|
+
template: `
|
|
30
|
+
<td-jewelry-customizer
|
|
31
|
+
[src]="'https://cdn.example.com/models/solitaire-ring.glb'"
|
|
32
|
+
[(metal)]="activeMetal"
|
|
33
|
+
[(stone)]="activeStone"
|
|
34
|
+
[height]="520"
|
|
35
|
+
[autoRotate]="true"
|
|
36
|
+
></td-jewelry-customizer>
|
|
37
|
+
`
|
|
38
|
+
})
|
|
39
|
+
export class ProductPageComponent {
|
|
40
|
+
activeMetal = 'yellow-gold-18k'
|
|
41
|
+
activeStone = 'diamond-d'
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 2. Composable Primitives (`<td-viewer>` + Swatches)
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { Component } from '@angular/core'
|
|
51
|
+
import {
|
|
52
|
+
ViewerComponent,
|
|
53
|
+
MetalSwatchesComponent,
|
|
54
|
+
StoneSwatchesComponent,
|
|
55
|
+
type MetalColorProp,
|
|
56
|
+
type StoneColorProp,
|
|
57
|
+
} from '@thinkersteps_labs/3d-viewer-angular'
|
|
58
|
+
|
|
59
|
+
@Component({
|
|
60
|
+
selector: 'app-product-detail',
|
|
61
|
+
standalone: true,
|
|
62
|
+
imports: [ViewerComponent, MetalSwatchesComponent, StoneSwatchesComponent],
|
|
63
|
+
template: `
|
|
64
|
+
<div style="max-width: 840px; margin: 0 auto;">
|
|
65
|
+
<td-viewer
|
|
66
|
+
[src]="modelUrl"
|
|
67
|
+
[metalColor]="activeMetal"
|
|
68
|
+
[stoneColor]="activeStone"
|
|
69
|
+
[height]="500"
|
|
70
|
+
[autoRotate]="true"
|
|
71
|
+
></td-viewer>
|
|
72
|
+
|
|
73
|
+
<div style="margin-top: 16px; display: flex; flex-direction: column; gap: 14px;">
|
|
74
|
+
<td-metal-swatches [(value)]="activeMetal" [variant]="'pills'"></td-metal-swatches>
|
|
75
|
+
<td-stone-swatches [(value)]="activeStone" [variant]="'pills'"></td-stone-swatches>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
`
|
|
79
|
+
})
|
|
80
|
+
export class ProductDetailComponent {
|
|
81
|
+
modelUrl = 'https://cdn.example.com/models/solitaire-ring.glb'
|
|
82
|
+
activeMetal: MetalColorProp = 'yellow-gold-18k'
|
|
83
|
+
activeStone: StoneColorProp = 'diamond-d'
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 3. NgModule Apps (e.g. Angular ERP)
|
|
90
|
+
|
|
91
|
+
Import `ViewerModule` in your feature module to get access to `<td-viewer>`, `<td-metal-swatches>`, `<td-stone-swatches>`, and `<td-jewelry-customizer>`:
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { NgModule } from '@angular/core'
|
|
95
|
+
import { ViewerModule } from '@thinkersteps_labs/3d-viewer-angular'
|
|
96
|
+
|
|
97
|
+
@NgModule({
|
|
98
|
+
imports: [ViewerModule],
|
|
99
|
+
})
|
|
100
|
+
export class ProductModule {}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const i=require("@angular/core"),s=require("@3d-viewer/core");function p(l){const e=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(l){for(const t in l)if(t!=="default"){const h=Object.getOwnPropertyDescriptor(l,t);Object.defineProperty(e,t,h.get?h:{enumerable:!0,get:()=>l[t]})}}return e.default=l,Object.freeze(e)}const r=p(i),a=class a{constructor(){this.source=null,this.loaded=new i.EventEmitter,this.progress=new i.EventEmitter,this.partSelected=new i.EventEmitter,this.error=new i.EventEmitter,this.change=new i.EventEmitter,this.viewer=null,this.abortCtrl=null,this.initRafId=null,this.pendingSource=null}getViewer(){return this.viewer}ngAfterViewInit(){this.initRafId=requestAnimationFrame(()=>{this.initRafId=null;const e={...this.options,...this.background!==void 0&&{background:this.background},...this.fov!==void 0&&{fov:this.fov},...this.shadows!==void 0&&{shadows:this.shadows},...this.autoRotate!==void 0&&{autoRotate:this.autoRotate},...this.quality!==void 0&&{quality:this.quality},...this.metalEnvUrl!==void 0&&{metalEnvUrl:this.metalEnvUrl},...this.gemEnvUrl!==void 0&&{gemEnvUrl:this.gemEnvUrl}};this.viewer=s.createViewer(this.containerRef.nativeElement,e),this.viewer.on("loaded",({parts:t})=>this.loaded.emit(t)),this.viewer.on("loading",({progress:t})=>this.progress.emit(t)),this.viewer.on("error",t=>this.error.emit(t)),this.viewer.on("partSelected",t=>this.partSelected.emit(t)),this.viewer.on("change",t=>this.change.emit(t)),this.pendingSource&&(this._loadSource(this.pendingSource),this.pendingSource=null),this.state?this.viewer.setState(this.state):(this.metalColor&&this.viewer.setMetalColor(this.metalColor),this.stoneColor&&this.viewer.setStoneColor(this.stoneColor)),this.autoRotate!==void 0&&(this.autoRotate?this.viewer.startRotation():this.viewer.stopRotation()),this.quality&&this.viewer.setQuality(this.quality)})}ngOnChanges(e){if(!this.viewer){e.source&&(this.pendingSource=this.source);return}if(e.source){const t=e.source.previousValue;this._isSameSource(t,this.source)||this._loadSource(this.source)}e.state&&this.state?this.viewer.setState(this.state):(e.metalColor&&this.metalColor&&this.viewer.setMetalColor(this.metalColor),e.stoneColor&&this.stoneColor&&this.viewer.setStoneColor(this.stoneColor)),e.autoRotate&&this.autoRotate!==void 0&&(this.autoRotate?this.viewer.startRotation():this.viewer.stopRotation()),e.quality&&this.quality&&this.viewer.setQuality(this.quality)}ngOnDestroy(){var e,t;this.initRafId!==null&&(cancelAnimationFrame(this.initRafId),this.initRafId=null),(e=this.abortCtrl)==null||e.abort(),(t=this.viewer)==null||t.dispose(),this.viewer=null}_loadSource(e){var t;if((t=this.abortCtrl)==null||t.abort(),!!this.viewer){if(!e){this.viewer.unload();return}this.abortCtrl=new AbortController,this.viewer.load(e,{signal:this.abortCtrl.signal}).catch(()=>{})}}_isSameSource(e,t){return e===t?!0:!e||!t?!1:"url"in e&&"url"in t?e.url===t.url&&e.format===t.format&&e.mtlUrl===t.mtlUrl:"data"in e&&"data"in t?e.data===t.data&&e.format===t.format:!1}};a.ɵfac=r.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"18.2.14",ngImport:r,type:a,deps:[],target:r.ɵɵFactoryTarget.Component}),a.ɵcmp=r.ɵɵngDeclareComponent({minVersion:"14.0.0",version:"18.2.14",type:a,isStandalone:!0,selector:"td-viewer",inputs:{source:"source",metalColor:"metalColor",stoneColor:"stoneColor",background:"background",metalEnvUrl:"metalEnvUrl",gemEnvUrl:"gemEnvUrl",fov:"fov",shadows:"shadows",options:"options",autoRotate:"autoRotate",quality:"quality",state:"state"},outputs:{loaded:"loaded",progress:"progress",partSelected:"partSelected",error:"error",change:"change"},viewQueries:[{propertyName:"containerRef",first:!0,predicate:["container"],descendants:!0}],usesOnChanges:!0,ngImport:r,template:'<div #container style="width:100%;height:100%;display:block;"></div>',isInline:!0,styles:[""],changeDetection:r.ChangeDetectionStrategy.OnPush});let n=a;r.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"18.2.14",ngImport:r,type:n,decorators:[{type:i.Component,args:[{selector:"td-viewer",standalone:!0,template:'<div #container style="width:100%;height:100%;display:block;"></div>',changeDetection:i.ChangeDetectionStrategy.OnPush}]}],propDecorators:{source:[{type:i.Input}],metalColor:[{type:i.Input}],stoneColor:[{type:i.Input}],background:[{type:i.Input}],metalEnvUrl:[{type:i.Input}],gemEnvUrl:[{type:i.Input}],fov:[{type:i.Input}],shadows:[{type:i.Input}],options:[{type:i.Input}],autoRotate:[{type:i.Input}],quality:[{type:i.Input}],state:[{type:i.Input}],loaded:[{type:i.Output}],progress:[{type:i.Output}],partSelected:[{type:i.Output}],error:[{type:i.Output}],change:[{type:i.Output}],containerRef:[{type:i.ViewChild,args:["container"]}]}});const o=class o{};o.ɵfac=r.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"18.2.14",ngImport:r,type:o,deps:[],target:r.ɵɵFactoryTarget.NgModule}),o.ɵmod=r.ɵɵngDeclareNgModule({minVersion:"14.0.0",version:"18.2.14",ngImport:r,type:o,imports:[n],exports:[n]}),o.ɵinj=r.ɵɵngDeclareInjector({minVersion:"12.0.0",version:"18.2.14",ngImport:r,type:o});let u=o;r.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"18.2.14",ngImport:r,type:u,decorators:[{type:i.NgModule,args:[{imports:[n],exports:[n]}]}]});Object.defineProperty(exports,"ALL_METAL_PRESETS",{enumerable:!0,get:()=>s.ALL_METAL_PRESETS});Object.defineProperty(exports,"ALL_STONE_PRESETS",{enumerable:!0,get:()=>s.ALL_STONE_PRESETS});Object.defineProperty(exports,"DIAMOND_PRESETS",{enumerable:!0,get:()=>s.DIAMOND_PRESETS});Object.defineProperty(exports,"GEMSTONE_PRESETS",{enumerable:!0,get:()=>s.GEMSTONE_PRESETS});Object.defineProperty(exports,"METAL_PRESETS",{enumerable:!0,get:()=>s.METAL_PRESETS});Object.defineProperty(exports,"PHYSICALLY_BASED_METAL_PRESETS",{enumerable:!0,get:()=>s.PHYSICALLY_BASED_METAL_PRESETS});Object.defineProperty(exports,"STONE_PRESETS",{enumerable:!0,get:()=>s.STONE_PRESETS});exports.ViewerComponent=n;exports.ViewerModule=u;
|
|
2
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../src/viewer.component.ts","../src/viewer.module.ts"],"sourcesContent":["import {\n Component,\n Input,\n Output,\n EventEmitter,\n ElementRef,\n ViewChild,\n OnChanges,\n AfterViewInit,\n OnDestroy,\n SimpleChanges,\n ChangeDetectionStrategy,\n} from '@angular/core'\nimport {\n createViewer,\n type Viewer,\n type ModelSource,\n type ViewerOptions,\n type ViewerState,\n type PartInfo,\n type PartType,\n} from '@3d-viewer/core'\n\n/**\n * Angular wrapper for @3d-viewer/core.\n *\n * Usage (standalone):\n * import { ViewerComponent } from '@3d-viewer/angular'\n *\n * Usage (NgModule — for Angular 18 apps like AngularJMS):\n * import { ViewerModule } from '@3d-viewer/angular'\n *\n * @example\n * ```html\n * <td-viewer\n * [source]=\"modelSource\"\n * [metalColor]=\"'18k-yellow-gold'\"\n * (loaded)=\"onLoaded($event)\"\n * (error)=\"onError($event)\"\n * ></td-viewer>\n * ```\n */\n@Component({\n selector: 'td-viewer',\n standalone: true,\n template: `<div #container style=\"width:100%;height:100%;display:block;\"></div>`,\n styles: [`\n :host {\n display: block;\n width: 100%;\n height: 100%;\n position: relative;\n }\n `],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class ViewerComponent implements AfterViewInit, OnChanges, OnDestroy {\n\n // ── Inputs ────────────────────────────────────────────────────────────────\n\n /** The 3D model to load. Supports URL or raw ArrayBuffer/Blob. Set null to unload. */\n @Input() source: ModelSource | null = null\n\n /** Metal preset ID or hex color string. Reactive post-init. */\n @Input() metalColor?: string\n\n /** Stone preset ID or hex color string. Reactive post-init. */\n @Input() stoneColor?: string\n\n /** Background color as hex string or number. */\n @Input() background?: string | number\n\n /** Metal HDR environment URL or buffer. */\n @Input() metalEnvUrl?: string | ArrayBuffer\n\n /** Gemstone HDR environment URL or buffer. */\n @Input() gemEnvUrl?: string | ArrayBuffer\n\n /** Camera field of view. */\n @Input() fov?: number\n\n /** Enable shadow rendering. */\n @Input() shadows?: boolean\n\n /**\n * Init-time viewer options (background, fov, shadows, etc.).\n * Changes after mount are ignored — use dedicated reactive inputs instead.\n */\n @Input() options?: ViewerOptions\n\n /**\n * Reactive auto-rotation toggle. Calls startRotation() / stopRotation() on change.\n * Overrides the autoRotate flag in `options` after initialization.\n */\n @Input() autoRotate?: boolean\n\n /**\n * Reactive render quality. Calls setQuality() on change.\n * Overrides the quality flag in `options` after initialization.\n */\n @Input() quality?: 'low' | 'medium' | 'high'\n\n /**\n * Full viewer state object. When changed, calls viewer.setState() — a COMPLETE\n * replace of metalColor, stoneColor, camera, and part overrides. Not a merge.\n * Takes precedence over individual metalColor / stoneColor inputs.\n */\n @Input() state?: ViewerState\n\n // ── Outputs ───────────────────────────────────────────────────────────────\n\n /** Emits classified part list when model finishes loading. $event = PartInfo[] */\n @Output() loaded = new EventEmitter<PartInfo[]>()\n\n /** Emits loading progress 0-1. $event = number */\n @Output() progress = new EventEmitter<number>()\n\n /** Emits clicked part info. $event = { meshId: string, type: PartType } */\n @Output() partSelected = new EventEmitter<{ meshId: string; type: PartType }>()\n\n /** Emits Error when model loading fails. */\n @Output() error = new EventEmitter<Error>()\n\n /**\n * Emits full ViewerState on every metal/stone/camera change.\n * Use for persistence, undo history, or sharing a viewer URL.\n * Note: OrbitControls dragging emits state updates continuously.\n */\n @Output() change = new EventEmitter<ViewerState>()\n\n // ── Private ───────────────────────────────────────────────────────────────\n\n @ViewChild('container') private containerRef!: ElementRef<HTMLDivElement>\n\n private viewer: Viewer | null = null\n private abortCtrl: AbortController | null = null\n private initRafId: number | null = null\n\n /**\n * Source received before viewer is initialized. Applied in ngAfterViewInit.\n * Needed because ngOnChanges fires before ngAfterViewInit on first render.\n */\n private pendingSource: ModelSource | null = null\n\n // ── Public Access ─────────────────────────────────────────────────────────\n\n /** Direct access to underlying @3d-viewer/core instance (null before mount/after destroy). */\n getViewer(): Viewer | null {\n return this.viewer\n }\n\n // ── Lifecycle ─────────────────────────────────────────────────────────────\n\n ngAfterViewInit(): void {\n // requestAnimationFrame ensures the container has real layout dimensions\n // before createViewer initializes the WebGLRenderer (matches AngularJMS pattern).\n this.initRafId = requestAnimationFrame(() => {\n this.initRafId = null\n const viewerOptions: ViewerOptions = {\n ...this.options,\n ...(this.background !== undefined && { background: this.background }),\n ...(this.fov !== undefined && { fov: this.fov }),\n ...(this.shadows !== undefined && { shadows: this.shadows }),\n ...(this.autoRotate !== undefined && { autoRotate: this.autoRotate }),\n ...(this.quality !== undefined && { quality: this.quality }),\n ...(this.metalEnvUrl !== undefined && { metalEnvUrl: this.metalEnvUrl }),\n ...(this.gemEnvUrl !== undefined && { gemEnvUrl: this.gemEnvUrl }),\n }\n this.viewer = createViewer(this.containerRef.nativeElement, viewerOptions)\n\n this.viewer.on('loaded', ({ parts }) => this.loaded.emit(parts))\n this.viewer.on('loading', ({ progress }) => this.progress.emit(progress))\n this.viewer.on('error', (err) => this.error.emit(err))\n this.viewer.on('partSelected', (info) => this.partSelected.emit(info))\n this.viewer.on('change', (state) => this.change.emit(state))\n\n // Apply initial model source if one was queued.\n if (this.pendingSource) {\n this._loadSource(this.pendingSource)\n this.pendingSource = null\n }\n\n // Precedence: full state snapshot overrides individual color inputs\n if (this.state) {\n this.viewer.setState(this.state)\n } else {\n if (this.metalColor) this.viewer.setMetalColor(this.metalColor)\n if (this.stoneColor) this.viewer.setStoneColor(this.stoneColor)\n }\n\n if (this.autoRotate !== undefined) {\n if (this.autoRotate) this.viewer.startRotation()\n else this.viewer.stopRotation()\n }\n if (this.quality) this.viewer.setQuality(this.quality)\n })\n }\n\n ngOnChanges(changes: SimpleChanges): void {\n // Guard: viewer is null until ngAfterViewInit + rAF fires.\n if (!this.viewer) {\n if (changes['source']) {\n this.pendingSource = this.source\n }\n return\n }\n\n if (changes['source']) {\n const prev = changes['source'].previousValue as ModelSource | null | undefined\n if (!this._isSameSource(prev, this.source)) {\n this._loadSource(this.source)\n }\n }\n\n // Precedence: if 'state' changed in this cycle, setState handles everything.\n // Individual color inputs in the same cycle are ignored.\n if (changes['state'] && this.state) {\n this.viewer.setState(this.state)\n } else {\n if (changes['metalColor'] && this.metalColor) this.viewer.setMetalColor(this.metalColor)\n if (changes['stoneColor'] && this.stoneColor) this.viewer.setStoneColor(this.stoneColor)\n }\n\n if (changes['autoRotate'] && this.autoRotate !== undefined) {\n if (this.autoRotate) this.viewer.startRotation()\n else this.viewer.stopRotation()\n }\n if (changes['quality'] && this.quality) this.viewer.setQuality(this.quality)\n }\n\n ngOnDestroy(): void {\n if (this.initRafId !== null) {\n cancelAnimationFrame(this.initRafId)\n this.initRafId = null\n }\n this.abortCtrl?.abort()\n this.viewer?.dispose()\n this.viewer = null\n }\n\n // ── Helpers ───────────────────────────────────────────────────────────────\n\n private _loadSource(source: ModelSource | null): void {\n this.abortCtrl?.abort()\n if (!this.viewer) return\n\n if (!source) {\n this.viewer.unload()\n return\n }\n\n this.abortCtrl = new AbortController()\n this.viewer.load(source, { signal: this.abortCtrl.signal }).catch(() => {\n // Error is emitted via the viewer's 'error' event — no duplicate handling needed.\n })\n }\n\n private _isSameSource(a: ModelSource | null | undefined, b: ModelSource | null | undefined): boolean {\n if (a === b) return true\n if (!a || !b) return false\n if ('url' in a && 'url' in b) {\n return a.url === b.url && a.format === b.format && a.mtlUrl === b.mtlUrl\n }\n if ('data' in a && 'data' in b) {\n return a.data === b.data && a.format === b.format\n }\n return false\n }\n}\n","import { NgModule } from '@angular/core'\nimport { ViewerComponent } from './viewer.component'\n\n/**\n * Import this module in NgModule-based Angular apps (Angular 18 style, like AngularJMS).\n *\n * @example\n * ```typescript\n * import { ViewerModule } from '@3d-viewer/angular'\n *\n * @NgModule({ imports: [ViewerModule] })\n * export class YourFeatureModule {}\n * ```\n */\n@NgModule({\n imports: [ViewerComponent],\n exports: [ViewerComponent],\n})\nexport class ViewerModule {}\n"],"names":["_ViewerComponent","EventEmitter","viewerOptions","createViewer","parts","progress","err","info","state","changes","prev","_a","_b","source","a","b","i0","ViewerComponent","Component","ChangeDetectionStrategy","Input","Output","_ViewerModule","ViewerModule"],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { ViewerComponent } from './viewer.component';
|
|
2
|
+
export { ViewerModule } from './viewer.module';
|
|
3
|
+
export { METAL_PRESETS, STONE_PRESETS, PHYSICALLY_BASED_METAL_PRESETS, DIAMOND_PRESETS, GEMSTONE_PRESETS, ALL_METAL_PRESETS, ALL_STONE_PRESETS, } from '@3d-viewer/core';
|
|
4
|
+
export type { MetalPreset, StonePreset } from '@3d-viewer/core';
|
|
5
|
+
export type { ModelSource, ViewerOptions, ViewerState, PartInfo, PartType, Viewer, } from '@3d-viewer/core';
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { EventEmitter, OnChanges, AfterViewInit, OnDestroy, SimpleChanges } from '@angular/core';
|
|
2
|
+
import { type Viewer, type ModelSource, type ViewerOptions, type ViewerState, type PartInfo, type PartType } from '@3d-viewer/core';
|
|
3
|
+
import * as i0 from "@angular/core";
|
|
4
|
+
/**
|
|
5
|
+
* Angular wrapper for @3d-viewer/core.
|
|
6
|
+
*
|
|
7
|
+
* Usage (standalone):
|
|
8
|
+
* import { ViewerComponent } from '@3d-viewer/angular'
|
|
9
|
+
*
|
|
10
|
+
* Usage (NgModule — for Angular 18 apps like AngularJMS):
|
|
11
|
+
* import { ViewerModule } from '@3d-viewer/angular'
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```html
|
|
15
|
+
* <td-viewer
|
|
16
|
+
* [source]="modelSource"
|
|
17
|
+
* [metalColor]="'18k-yellow-gold'"
|
|
18
|
+
* (loaded)="onLoaded($event)"
|
|
19
|
+
* (error)="onError($event)"
|
|
20
|
+
* ></td-viewer>
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare class ViewerComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
24
|
+
/** The 3D model to load. Supports URL or raw ArrayBuffer/Blob. Set null to unload. */
|
|
25
|
+
source: ModelSource | null;
|
|
26
|
+
/** Metal preset ID or hex color string. Reactive post-init. */
|
|
27
|
+
metalColor?: string;
|
|
28
|
+
/** Stone preset ID or hex color string. Reactive post-init. */
|
|
29
|
+
stoneColor?: string;
|
|
30
|
+
/** Background color as hex string or number. */
|
|
31
|
+
background?: string | number;
|
|
32
|
+
/** Metal HDR environment URL or buffer. */
|
|
33
|
+
metalEnvUrl?: string | ArrayBuffer;
|
|
34
|
+
/** Gemstone HDR environment URL or buffer. */
|
|
35
|
+
gemEnvUrl?: string | ArrayBuffer;
|
|
36
|
+
/** Camera field of view. */
|
|
37
|
+
fov?: number;
|
|
38
|
+
/** Enable shadow rendering. */
|
|
39
|
+
shadows?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Init-time viewer options (background, fov, shadows, etc.).
|
|
42
|
+
* Changes after mount are ignored — use dedicated reactive inputs instead.
|
|
43
|
+
*/
|
|
44
|
+
options?: ViewerOptions;
|
|
45
|
+
/**
|
|
46
|
+
* Reactive auto-rotation toggle. Calls startRotation() / stopRotation() on change.
|
|
47
|
+
* Overrides the autoRotate flag in `options` after initialization.
|
|
48
|
+
*/
|
|
49
|
+
autoRotate?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Reactive render quality. Calls setQuality() on change.
|
|
52
|
+
* Overrides the quality flag in `options` after initialization.
|
|
53
|
+
*/
|
|
54
|
+
quality?: 'low' | 'medium' | 'high';
|
|
55
|
+
/**
|
|
56
|
+
* Full viewer state object. When changed, calls viewer.setState() — a COMPLETE
|
|
57
|
+
* replace of metalColor, stoneColor, camera, and part overrides. Not a merge.
|
|
58
|
+
* Takes precedence over individual metalColor / stoneColor inputs.
|
|
59
|
+
*/
|
|
60
|
+
state?: ViewerState;
|
|
61
|
+
/** Emits classified part list when model finishes loading. $event = PartInfo[] */
|
|
62
|
+
loaded: EventEmitter<PartInfo[]>;
|
|
63
|
+
/** Emits loading progress 0-1. $event = number */
|
|
64
|
+
progress: EventEmitter<number>;
|
|
65
|
+
/** Emits clicked part info. $event = { meshId: string, type: PartType } */
|
|
66
|
+
partSelected: EventEmitter<{
|
|
67
|
+
meshId: string;
|
|
68
|
+
type: PartType;
|
|
69
|
+
}>;
|
|
70
|
+
/** Emits Error when model loading fails. */
|
|
71
|
+
error: EventEmitter<Error>;
|
|
72
|
+
/**
|
|
73
|
+
* Emits full ViewerState on every metal/stone/camera change.
|
|
74
|
+
* Use for persistence, undo history, or sharing a viewer URL.
|
|
75
|
+
* Note: OrbitControls dragging emits state updates continuously.
|
|
76
|
+
*/
|
|
77
|
+
change: EventEmitter<ViewerState>;
|
|
78
|
+
private containerRef;
|
|
79
|
+
private viewer;
|
|
80
|
+
private abortCtrl;
|
|
81
|
+
private initRafId;
|
|
82
|
+
/**
|
|
83
|
+
* Source received before viewer is initialized. Applied in ngAfterViewInit.
|
|
84
|
+
* Needed because ngOnChanges fires before ngAfterViewInit on first render.
|
|
85
|
+
*/
|
|
86
|
+
private pendingSource;
|
|
87
|
+
/** Direct access to underlying @3d-viewer/core instance (null before mount/after destroy). */
|
|
88
|
+
getViewer(): Viewer | null;
|
|
89
|
+
ngAfterViewInit(): void;
|
|
90
|
+
ngOnChanges(changes: SimpleChanges): void;
|
|
91
|
+
ngOnDestroy(): void;
|
|
92
|
+
private _loadSource;
|
|
93
|
+
private _isSameSource;
|
|
94
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<ViewerComponent, never>;
|
|
95
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<ViewerComponent, "td-viewer", never, { "source": { "alias": "source"; "required": false; }; "metalColor": { "alias": "metalColor"; "required": false; }; "stoneColor": { "alias": "stoneColor"; "required": false; }; "background": { "alias": "background"; "required": false; }; "metalEnvUrl": { "alias": "metalEnvUrl"; "required": false; }; "gemEnvUrl": { "alias": "gemEnvUrl"; "required": false; }; "fov": { "alias": "fov"; "required": false; }; "shadows": { "alias": "shadows"; "required": false; }; "options": { "alias": "options"; "required": false; }; "autoRotate": { "alias": "autoRotate"; "required": false; }; "quality": { "alias": "quality"; "required": false; }; "state": { "alias": "state"; "required": false; }; }, { "loaded": "loaded"; "progress": "progress"; "partSelected": "partSelected"; "error": "error"; "change": "change"; }, never, never, true, never>;
|
|
96
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as i0 from "@angular/core";
|
|
2
|
+
import * as i1 from "./viewer.component";
|
|
3
|
+
/**
|
|
4
|
+
* Import this module in NgModule-based Angular apps (Angular 18 style, like AngularJMS).
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { ViewerModule } from '@3d-viewer/angular'
|
|
9
|
+
*
|
|
10
|
+
* @NgModule({ imports: [ViewerModule] })
|
|
11
|
+
* export class YourFeatureModule {}
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export declare class ViewerModule {
|
|
15
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<ViewerModule, never>;
|
|
16
|
+
static ɵmod: i0.ɵɵNgModuleDeclaration<ViewerModule, never, [typeof i1.ViewerComponent], [typeof i1.ViewerComponent]>;
|
|
17
|
+
static ɵinj: i0.ɵɵInjectorDeclaration<ViewerModule>;
|
|
18
|
+
}
|