@thinkersteps_labs/3d-viewer-angular 0.0.0-stage → 1.0.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 CHANGED
@@ -1,3 +1,107 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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,8 @@
1
+ export { ViewerComponent } from './viewer.component';
2
+ export { ViewerModule } from './viewer.module';
3
+ export { MetalSwatchesComponent, MetalSelectComponent, } from './swatches/metal-swatches.component';
4
+ export { StoneSwatchesComponent, StoneSelectComponent, } from './swatches/stone-swatches.component';
5
+ export { JewelryCustomizerComponent, } from './swatches/jewelry-customizer.component';
6
+ export { CANONICAL_METAL_PRESETS, CANONICAL_STONE_PRESETS, METAL_PRESETS, STONE_PRESETS, PHYSICALLY_BASED_METAL_PRESETS, DIAMOND_PRESETS, GEMSTONE_PRESETS, ALL_METAL_PRESETS, ALL_STONE_PRESETS, getMetalPreset, getStonePreset, normalizeModelSource, inferFormatFromUrl, } from '@thinkersteps_labs/3d-viewer-core';
7
+ export type { MetalPreset, StonePreset, StandardMetalId, StandardStoneId, MetalColorProp, StoneColorProp, ModelSource, ViewerSourceInput, ViewerOptions, ViewerState, PartInfo, PartType, MaterialSlotInfo, MeshMaterialSlot, StateRestoreResult, Viewer, } from '@thinkersteps_labs/3d-viewer-core';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAE9C,OAAO,EACL,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,qCAAqC,CAAA;AAE5C,OAAO,EACL,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,qCAAqC,CAAA;AAE5C,OAAO,EACL,0BAA0B,GAC3B,MAAM,yCAAyC,CAAA;AAGhD,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,aAAa,EACb,aAAa,EACb,8BAA8B,EAC9B,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,mCAAmC,CAAA;AAE1C,YAAY,EACV,WAAW,EACX,WAAW,EACX,eAAe,EACf,eAAe,EACf,cAAc,EACd,cAAc,EACd,WAAW,EACX,iBAAiB,EACjB,aAAa,EACb,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,EAClB,MAAM,GACP,MAAM,mCAAmC,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export { ViewerComponent } from './viewer.component';
2
+ export { ViewerModule } from './viewer.module';
3
+ export { MetalSwatchesComponent, MetalSelectComponent, } from './swatches/metal-swatches.component';
4
+ export { StoneSwatchesComponent, StoneSelectComponent, } from './swatches/stone-swatches.component';
5
+ export { JewelryCustomizerComponent, } from './swatches/jewelry-customizer.component';
6
+ // Convenience re-exports — Angular consumers can import everything from one place.
7
+ export { CANONICAL_METAL_PRESETS, CANONICAL_STONE_PRESETS, METAL_PRESETS, STONE_PRESETS, PHYSICALLY_BASED_METAL_PRESETS, DIAMOND_PRESETS, GEMSTONE_PRESETS, ALL_METAL_PRESETS, ALL_STONE_PRESETS, getMetalPreset, getStonePreset, normalizeModelSource, inferFormatFromUrl, } from '@thinkersteps_labs/3d-viewer-core';
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAE9C,OAAO,EACL,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,qCAAqC,CAAA;AAE5C,OAAO,EACL,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,qCAAqC,CAAA;AAE5C,OAAO,EACL,0BAA0B,GAC3B,MAAM,yCAAyC,CAAA;AAEhD,mFAAmF;AACnF,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,aAAa,EACb,aAAa,EACb,8BAA8B,EAC9B,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,mCAAmC,CAAA"}
@@ -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
+ }
@@ -0,0 +1,29 @@
1
+ import { EventEmitter } from '@angular/core';
2
+ import { type MetalPreset, type StonePreset, type MetalColorProp, type StoneColorProp, type ViewerSourceInput, type PartInfo, type PartType } from '@thinkersteps_labs/3d-viewer-core';
3
+ import * as i0 from "@angular/core";
4
+ export declare class JewelryCustomizerComponent {
5
+ src?: string;
6
+ source?: ViewerSourceInput;
7
+ metal: MetalColorProp;
8
+ metalChange: EventEmitter<string>;
9
+ stone: StoneColorProp;
10
+ stoneChange: EventEmitter<string>;
11
+ height: number | string;
12
+ autoRotate: boolean;
13
+ quality: 'low' | 'medium' | 'high';
14
+ controlsVariant: 'pills' | 'dots' | 'select' | 'none';
15
+ metalOptions: MetalPreset[];
16
+ stoneOptions: StonePreset[];
17
+ loaded: EventEmitter<PartInfo[]>;
18
+ partSelected: EventEmitter<{
19
+ meshId: string;
20
+ type: PartType;
21
+ }>;
22
+ get buttonVariant(): 'pills' | 'dots';
23
+ get formattedHeight(): string;
24
+ onMetalChange(val: string): void;
25
+ onStoneChange(val: string): void;
26
+ static ɵfac: i0.ɵɵFactoryDeclaration<JewelryCustomizerComponent, never>;
27
+ static ɵcmp: i0.ɵɵComponentDeclaration<JewelryCustomizerComponent, "td-jewelry-customizer", never, { "src": { "alias": "src"; "required": false; }; "source": { "alias": "source"; "required": false; }; "metal": { "alias": "metal"; "required": false; }; "stone": { "alias": "stone"; "required": false; }; "height": { "alias": "height"; "required": false; }; "autoRotate": { "alias": "autoRotate"; "required": false; }; "quality": { "alias": "quality"; "required": false; }; "controlsVariant": { "alias": "controlsVariant"; "required": false; }; "metalOptions": { "alias": "metalOptions"; "required": false; }; "stoneOptions": { "alias": "stoneOptions"; "required": false; }; }, { "metalChange": "metalChange"; "stoneChange": "stoneChange"; "loaded": "loaded"; "partSelected": "partSelected"; }, never, never, true, never>;
28
+ }
29
+ //# sourceMappingURL=jewelry-customizer.component.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jewelry-customizer.component.d.ts","sourceRoot":"","sources":["../../src/swatches/jewelry-customizer.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,YAAY,EAEb,MAAM,eAAe,CAAA;AAEtB,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,QAAQ,EACb,KAAK,QAAQ,EACd,MAAM,mCAAmC,CAAA;;AAkC1C,qBA4Da,0BAA0B;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,iBAAiB,CAAA;IAC1B,KAAK,EAAE,cAAc,CAAoB;IACxC,WAAW,uBAA6B;IAEzC,KAAK,EAAE,cAAc,CAAc;IAClC,WAAW,uBAA6B;IAEzC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAM;IAC7B,UAAU,UAAO;IACjB,OAAO,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAS;IAC3C,eAAe,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAU;IAE/D,YAAY,EAAE,WAAW,EAAE,CAA0B;IACrD,YAAY,EAAE,WAAW,EAAE,CAA0B;IAEpD,MAAM,2BAAiC;IACvC,YAAY;gBAA8B,MAAM;cAAQ,QAAQ;OAAK;IAE/E,IAAI,aAAa,IAAI,OAAO,GAAG,MAAM,CAEpC;IAED,IAAI,eAAe,IAAI,MAAM,CAE5B;IAED,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAKhC,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;yCAjCrB,0BAA0B;2CAA1B,0BAA0B;CAqCtC"}
@@ -0,0 +1,198 @@
1
+ import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { CANONICAL_METAL_PRESETS, CANONICAL_STONE_PRESETS, } from '@thinkersteps_labs/3d-viewer-core';
4
+ import { ViewerComponent } from '../viewer.component';
5
+ import { MetalSwatchesComponent, MetalSelectComponent } from './metal-swatches.component';
6
+ import { StoneSwatchesComponent, StoneSelectComponent } from './stone-swatches.component';
7
+ import * as i0 from "@angular/core";
8
+ import * as i1 from "@angular/common";
9
+ const CUSTOMIZER_STYLES = `
10
+ :host {
11
+ display: block;
12
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
13
+ }
14
+ .td-customizer-card {
15
+ display: flex;
16
+ flex-direction: column;
17
+ gap: 16px;
18
+ background: #ffffff;
19
+ border-radius: 16px;
20
+ border: 1px solid #e2e8f0;
21
+ padding: 16px;
22
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -2px rgba(0, 0, 0, 0.05);
23
+ }
24
+ .td-customizer-viewport {
25
+ position: relative;
26
+ width: 100%;
27
+ border-radius: 12px;
28
+ overflow: hidden;
29
+ background: #f8fafc;
30
+ }
31
+ .td-customizer-controls {
32
+ display: flex;
33
+ flex-direction: column;
34
+ gap: 14px;
35
+ }
36
+ `;
37
+ export class JewelryCustomizerComponent {
38
+ constructor() {
39
+ this.metal = 'yellow-gold-18k';
40
+ this.metalChange = new EventEmitter();
41
+ this.stone = 'diamond-d';
42
+ this.stoneChange = new EventEmitter();
43
+ this.height = 520;
44
+ this.autoRotate = true;
45
+ this.quality = 'high';
46
+ this.controlsVariant = 'pills';
47
+ this.metalOptions = CANONICAL_METAL_PRESETS;
48
+ this.stoneOptions = CANONICAL_STONE_PRESETS;
49
+ this.loaded = new EventEmitter();
50
+ this.partSelected = new EventEmitter();
51
+ }
52
+ get buttonVariant() {
53
+ return this.controlsVariant === 'dots' ? 'dots' : 'pills';
54
+ }
55
+ get formattedHeight() {
56
+ return typeof this.height === 'number' ? `${this.height}px` : this.height;
57
+ }
58
+ onMetalChange(val) {
59
+ this.metal = val;
60
+ this.metalChange.emit(val);
61
+ }
62
+ onStoneChange(val) {
63
+ this.stone = val;
64
+ this.stoneChange.emit(val);
65
+ }
66
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: JewelryCustomizerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
67
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: JewelryCustomizerComponent, isStandalone: true, selector: "td-jewelry-customizer", inputs: { src: "src", source: "source", metal: "metal", stone: "stone", height: "height", autoRotate: "autoRotate", quality: "quality", controlsVariant: "controlsVariant", metalOptions: "metalOptions", stoneOptions: "stoneOptions" }, outputs: { metalChange: "metalChange", stoneChange: "stoneChange", loaded: "loaded", partSelected: "partSelected" }, ngImport: i0, template: `
68
+ <div class="td-customizer-card">
69
+ <div class="td-customizer-viewport" [style.height]="formattedHeight">
70
+ <td-viewer
71
+ [src]="src"
72
+ [source]="source"
73
+ [metalColor]="metal"
74
+ [stoneColor]="stone"
75
+ [autoRotate]="autoRotate"
76
+ [quality]="quality"
77
+ (loaded)="loaded.emit($event)"
78
+ (partSelected)="partSelected.emit($event)"
79
+ ></td-viewer>
80
+ </div>
81
+
82
+ <div class="td-customizer-controls" *ngIf="controlsVariant !== 'none'">
83
+ <ng-container *ngIf="controlsVariant === 'select'; else buttonSwatches">
84
+ <td-metal-select
85
+ [value]="metal"
86
+ (valueChange)="onMetalChange($event)"
87
+ [options]="metalOptions"
88
+ ></td-metal-select>
89
+ <td-stone-select
90
+ [value]="stone"
91
+ (valueChange)="onStoneChange($event)"
92
+ [options]="stoneOptions"
93
+ ></td-stone-select>
94
+ </ng-container>
95
+
96
+ <ng-template #buttonSwatches>
97
+ <td-metal-swatches
98
+ [value]="metal"
99
+ (valueChange)="onMetalChange($event)"
100
+ [options]="metalOptions"
101
+ [variant]="buttonVariant"
102
+ ></td-metal-swatches>
103
+ <td-stone-swatches
104
+ [value]="stone"
105
+ (valueChange)="onStoneChange($event)"
106
+ [options]="stoneOptions"
107
+ [variant]="buttonVariant"
108
+ ></td-stone-swatches>
109
+ </ng-template>
110
+ </div>
111
+ </div>
112
+ `, isInline: true, styles: ["\n :host {\n display: block;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", sans-serif;\n }\n .td-customizer-card {\n display: flex;\n flex-direction: column;\n gap: 16px;\n background: #ffffff;\n border-radius: 16px;\n border: 1px solid #e2e8f0;\n padding: 16px;\n box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -2px rgba(0, 0, 0, 0.05);\n }\n .td-customizer-viewport {\n position: relative;\n width: 100%;\n border-radius: 12px;\n overflow: hidden;\n background: #f8fafc;\n }\n .td-customizer-controls {\n display: flex;\n flex-direction: column;\n gap: 14px;\n }\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: ViewerComponent, selector: "td-viewer", inputs: ["src", "source", "metalColor", "stoneColor", "materialColors", "partColors", "height", "minHeight", "background", "metalEnvUrl", "gemEnvUrl", "fov", "shadows", "options", "autoRotate", "quality", "state"], outputs: ["loaded", "modelLoaded", "progress", "partSelected", "error", "change"] }, { kind: "component", type: MetalSwatchesComponent, selector: "td-metal-swatches", inputs: ["value", "options", "category", "variant", "showLabel"], outputs: ["valueChange"] }, { kind: "component", type: MetalSelectComponent, selector: "td-metal-select", inputs: ["value", "options", "label"], outputs: ["valueChange"] }, { kind: "component", type: StoneSwatchesComponent, selector: "td-stone-swatches", inputs: ["value", "options", "category", "variant", "showLabel"], outputs: ["valueChange"] }, { kind: "component", type: StoneSelectComponent, selector: "td-stone-select", inputs: ["value", "options", "label"], outputs: ["valueChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
113
+ }
114
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: JewelryCustomizerComponent, decorators: [{
115
+ type: Component,
116
+ args: [{ selector: 'td-jewelry-customizer', standalone: true, imports: [
117
+ CommonModule,
118
+ ViewerComponent,
119
+ MetalSwatchesComponent,
120
+ MetalSelectComponent,
121
+ StoneSwatchesComponent,
122
+ StoneSelectComponent,
123
+ ], template: `
124
+ <div class="td-customizer-card">
125
+ <div class="td-customizer-viewport" [style.height]="formattedHeight">
126
+ <td-viewer
127
+ [src]="src"
128
+ [source]="source"
129
+ [metalColor]="metal"
130
+ [stoneColor]="stone"
131
+ [autoRotate]="autoRotate"
132
+ [quality]="quality"
133
+ (loaded)="loaded.emit($event)"
134
+ (partSelected)="partSelected.emit($event)"
135
+ ></td-viewer>
136
+ </div>
137
+
138
+ <div class="td-customizer-controls" *ngIf="controlsVariant !== 'none'">
139
+ <ng-container *ngIf="controlsVariant === 'select'; else buttonSwatches">
140
+ <td-metal-select
141
+ [value]="metal"
142
+ (valueChange)="onMetalChange($event)"
143
+ [options]="metalOptions"
144
+ ></td-metal-select>
145
+ <td-stone-select
146
+ [value]="stone"
147
+ (valueChange)="onStoneChange($event)"
148
+ [options]="stoneOptions"
149
+ ></td-stone-select>
150
+ </ng-container>
151
+
152
+ <ng-template #buttonSwatches>
153
+ <td-metal-swatches
154
+ [value]="metal"
155
+ (valueChange)="onMetalChange($event)"
156
+ [options]="metalOptions"
157
+ [variant]="buttonVariant"
158
+ ></td-metal-swatches>
159
+ <td-stone-swatches
160
+ [value]="stone"
161
+ (valueChange)="onStoneChange($event)"
162
+ [options]="stoneOptions"
163
+ [variant]="buttonVariant"
164
+ ></td-stone-swatches>
165
+ </ng-template>
166
+ </div>
167
+ </div>
168
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: ["\n :host {\n display: block;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", sans-serif;\n }\n .td-customizer-card {\n display: flex;\n flex-direction: column;\n gap: 16px;\n background: #ffffff;\n border-radius: 16px;\n border: 1px solid #e2e8f0;\n padding: 16px;\n box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -2px rgba(0, 0, 0, 0.05);\n }\n .td-customizer-viewport {\n position: relative;\n width: 100%;\n border-radius: 12px;\n overflow: hidden;\n background: #f8fafc;\n }\n .td-customizer-controls {\n display: flex;\n flex-direction: column;\n gap: 14px;\n }\n"] }]
169
+ }], propDecorators: { src: [{
170
+ type: Input
171
+ }], source: [{
172
+ type: Input
173
+ }], metal: [{
174
+ type: Input
175
+ }], metalChange: [{
176
+ type: Output
177
+ }], stone: [{
178
+ type: Input
179
+ }], stoneChange: [{
180
+ type: Output
181
+ }], height: [{
182
+ type: Input
183
+ }], autoRotate: [{
184
+ type: Input
185
+ }], quality: [{
186
+ type: Input
187
+ }], controlsVariant: [{
188
+ type: Input
189
+ }], metalOptions: [{
190
+ type: Input
191
+ }], stoneOptions: [{
192
+ type: Input
193
+ }], loaded: [{
194
+ type: Output
195
+ }], partSelected: [{
196
+ type: Output
197
+ }] } });
198
+ //# sourceMappingURL=jewelry-customizer.component.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jewelry-customizer.component.js","sourceRoot":"","sources":["../../src/swatches/jewelry-customizer.component.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,KAAK,EACL,MAAM,EACN,YAAY,EACZ,uBAAuB,GACxB,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,OAAO,EACL,uBAAuB,EACvB,uBAAuB,GAQxB,MAAM,mCAAmC,CAAA;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AACrD,OAAO,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAA;AACzF,OAAO,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAA;;;AAEzF,MAAM,iBAAiB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BzB,CAAA;AA8DD,MAAM,OAAO,0BAA0B;IA5DvC;QA+DW,UAAK,GAAmB,iBAAiB,CAAA;QACxC,gBAAW,GAAG,IAAI,YAAY,EAAU,CAAA;QAEzC,UAAK,GAAmB,WAAW,CAAA;QAClC,gBAAW,GAAG,IAAI,YAAY,EAAU,CAAA;QAEzC,WAAM,GAAoB,GAAG,CAAA;QAC7B,eAAU,GAAG,IAAI,CAAA;QACjB,YAAO,GAA8B,MAAM,CAAA;QAC3C,oBAAe,GAAyC,OAAO,CAAA;QAE/D,iBAAY,GAAkB,uBAAuB,CAAA;QACrD,iBAAY,GAAkB,uBAAuB,CAAA;QAEpD,WAAM,GAAG,IAAI,YAAY,EAAc,CAAA;QACvC,iBAAY,GAAG,IAAI,YAAY,EAAsC,CAAA;KAmBhF;IAjBC,IAAI,aAAa;QACf,OAAO,IAAI,CAAC,eAAe,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAA;IAC3D,CAAC;IAED,IAAI,eAAe;QACjB,OAAO,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAA;IAC3E,CAAC;IAED,aAAa,CAAC,GAAW;QACvB,IAAI,CAAC,KAAK,GAAG,GAAG,CAAA;QAChB,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAC5B,CAAC;IAED,aAAa,CAAC,GAAW;QACvB,IAAI,CAAC,KAAK,GAAG,GAAG,CAAA;QAChB,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAC5B,CAAC;+GApCU,0BAA0B;mGAA1B,0BAA0B,gbAjD3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CT,svBApDC,YAAY,mIACZ,eAAe,gWACf,sBAAsB,0JACtB,oBAAoB,6HACpB,sBAAsB,0JACtB,oBAAoB;;4FAmDX,0BAA0B;kBA5DtC,SAAS;+BACE,uBAAuB,cACrB,IAAI,WACP;wBACP,YAAY;wBACZ,eAAe;wBACf,sBAAsB;wBACtB,oBAAoB;wBACpB,sBAAsB;wBACtB,oBAAoB;qBACrB,YACS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CT,mBAEgB,uBAAuB,CAAC,MAAM;8BAGtC,GAAG;sBAAX,KAAK;gBACG,MAAM;sBAAd,KAAK;gBACG,KAAK;sBAAb,KAAK;gBACI,WAAW;sBAApB,MAAM;gBAEE,KAAK;sBAAb,KAAK;gBACI,WAAW;sBAApB,MAAM;gBAEE,MAAM;sBAAd,KAAK;gBACG,UAAU;sBAAlB,KAAK;gBACG,OAAO;sBAAf,KAAK;gBACG,eAAe;sBAAvB,KAAK;gBAEG,YAAY;sBAApB,KAAK;gBACG,YAAY;sBAApB,KAAK;gBAEI,MAAM;sBAAf,MAAM;gBACG,YAAY;sBAArB,MAAM"}
@@ -0,0 +1,28 @@
1
+ import { EventEmitter } from '@angular/core';
2
+ import { type MetalPreset, type MetalColorProp } from '@thinkersteps_labs/3d-viewer-core';
3
+ import * as i0 from "@angular/core";
4
+ export declare class MetalSwatchesComponent {
5
+ value?: MetalColorProp;
6
+ valueChange: EventEmitter<string>;
7
+ options: MetalPreset[];
8
+ category: 'all' | 'gold' | 'platinum-silver' | 'alternative';
9
+ variant: 'pills' | 'dots';
10
+ showLabel: boolean;
11
+ get visibleOptions(): MetalPreset[];
12
+ get activePreset(): MetalPreset | undefined;
13
+ isSelected(m: MetalPreset): boolean;
14
+ selectMetal(m: MetalPreset): void;
15
+ static ɵfac: i0.ɵɵFactoryDeclaration<MetalSwatchesComponent, never>;
16
+ static ɵcmp: i0.ɵɵComponentDeclaration<MetalSwatchesComponent, "td-metal-swatches", never, { "value": { "alias": "value"; "required": false; }; "options": { "alias": "options"; "required": false; }; "category": { "alias": "category"; "required": false; }; "variant": { "alias": "variant"; "required": false; }; "showLabel": { "alias": "showLabel"; "required": false; }; }, { "valueChange": "valueChange"; }, never, never, true, never>;
17
+ }
18
+ export declare class MetalSelectComponent {
19
+ value?: MetalColorProp;
20
+ valueChange: EventEmitter<string>;
21
+ options: MetalPreset[];
22
+ label: string;
23
+ get activeValue(): string;
24
+ onSelectChange(event: Event): void;
25
+ static ɵfac: i0.ɵɵFactoryDeclaration<MetalSelectComponent, never>;
26
+ static ɵcmp: i0.ɵɵComponentDeclaration<MetalSelectComponent, "td-metal-select", never, { "value": { "alias": "value"; "required": false; }; "options": { "alias": "options"; "required": false; }; "label": { "alias": "label"; "required": false; }; }, { "valueChange": "valueChange"; }, never, never, true, never>;
27
+ }
28
+ //# sourceMappingURL=metal-swatches.component.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metal-swatches.component.d.ts","sourceRoot":"","sources":["../../src/swatches/metal-swatches.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,YAAY,EAEb,MAAM,eAAe,CAAA;AAEtB,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,cAAc,EACpB,MAAM,mCAAmC,CAAA;;AAyH1C,qBAgCa,sBAAsB;IACxB,KAAK,CAAC,EAAE,cAAc,CAAA;IACrB,WAAW,uBAA6B;IAEzC,OAAO,EAAE,WAAW,EAAE,CAA0B;IAChD,QAAQ,EAAE,KAAK,GAAG,MAAM,GAAG,iBAAiB,GAAG,aAAa,CAAQ;IACpE,OAAO,EAAE,OAAO,GAAG,MAAM,CAAU;IACnC,SAAS,UAAO;IAEzB,IAAI,cAAc,IAAI,WAAW,EAAE,CAGlC;IAED,IAAI,YAAY,IAAI,WAAW,GAAG,SAAS,CAE1C;IAED,UAAU,CAAC,CAAC,EAAE,WAAW,GAAG,OAAO;IAMnC,WAAW,CAAC,CAAC,EAAE,WAAW,GAAG,IAAI;yCAxBtB,sBAAsB;2CAAtB,sBAAsB;CA4BlC;AAED,qBAiBa,oBAAoB;IACtB,KAAK,CAAC,EAAE,cAAc,CAAA;IACrB,WAAW,uBAA6B;IACzC,OAAO,EAAE,WAAW,EAAE,CAA0B;IAChD,KAAK,SAAiB;IAE/B,IAAI,WAAW,IAAI,MAAM,CAGxB;IAED,cAAc,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;yCAXvB,oBAAoB;2CAApB,oBAAoB;CAgBhC"}