Browser Support
Chrome | 51 | May 2016 |
Firefox | 54 | Jun 2017 |
Edge | 14 | Aug 2016 |
Safari | 10 | Sep 2016 |
Opera | 38 | Jun 2016 |
Internet Explorer | Not supported |
|
Installing Dependencies
Basic Usage
Configurator init with options. ?
denotes an optional setting. A div
with the id of sectional-config-container
must be provided to inject the webgl canvas.
const initOptions:MxtSectionalConfiguratorOptions = { apiKey:"TODO: place your api key here", element: document.getElementById('sectional-config-container') as HTMLDivElement, eventCommunicator: new MxtSectionalEventCommunicator(UrlUtilities.getTargetOrigin()) } const sectionalConfig = new MxtSectionalConfigurator(initOptions); await sectionalConfig.init();
Initialization Options
Below is a complete list of the configurable options that are available to you when initializing
export interface MxtSectionalConfiguratorOptions { /** private access key to sectional */ apiKey: string; /** Element to place the rendering canvas inside of */ element: HTMLElement; /** Category id to load products from */ categoryId?: string; categoryName?: string; eventCommunicator: MxtSectionalEventCommunicator; skuResolver?: ISkuResolver; colors?: { primary: IColor3, secondary: IColor3, dropShadow: IColor3 }; /** Zoom Delta - in meters; defaults to .3 */ zoomDelta?: number; /** Rotate Delta - in radians; defaults to .3 */ rotateDelta?: number; /** Multiplier; amount of space product takes up in screen by default; defaults to .75. 1 would mean that the product takes up entire screen */ fitToScreenMultiplier?: number; /** Multiplier; How much you can zoom out past the initial zoom. Defaults to 1.33. A value of 1 would mean that the initial zoom distance is the farthest you can zoom out */ maxDistanceMultiplier?: number; /** Advanced camera options */ camera?: { defaultRadius?: number, defaultMaxDistance?: number, defaultBeta?: number, defaultAlpha?: number, lowerBetaLimit?: number, upperBetaLimit?: number, lowerAlphaLimit?: number, upperAlphaLimit?: number }; renderConfig?: { unitType?: UnitType, fractionRoundTo?: number, dimensionLineColor?: IColor4 }; dimensions?: { arrowScale?: number; lineScale?: number; overrideDimensions?: boolean; }; snapPlaneBuffer?: number; plugins?: MxtSectionalConfiguratorPlugins; customText?: { /** language should be an language ISO code (https://www.metamodpro.com/browser-language-codes)*/ [language: string]: any; }; assetEnvironment?:MxtAssetEnvironment; logging?:log.LogLevelDesc; cornerProducts?:{ maxCornerWedges?: number; cornersSnapToEndsOnly?: boolean; } /** Controls whether or not skus are added and removed as user interacts with the configurator. Defaults to true*/ urlParams?:{ skus?:boolean; } }
SKU Resolution
Bidirectional SKU resolution to go between a Marxent Product and a client SKU (and vice versa). To resolve SKUs in a custom manner, provide a SKU resolver class to the following spec, at initialization.
import { Product } from '@mxt/mxt-services/lib/datatypes/Product'; import { AssemblyWizardStep } from '@mxt/mxt-services/lib/datatypes'; export interface ISkuResolver { // Used to delimit SKU values when converted between an array or string delimiter: string; /** * Convert a product to a sku (or a list of skus). Also provide back the * assembly wizard step that that sku is currently sitting on (or null if it is the root product) * @param product */ productToSkus(product: Product): Map<AssemblyWizardStep, string>; /** * Convert an array of skus to a Product. This is the direct inverse of * productToSkus. Any list of skus here should product a unique product, * and that product, when put in productToSkus, should produce the exact * same list of skus. * @param skus */ skusToProduct(skus: string[]): Promise<Product>; }
A basic example:
import { Product, ProductRouter, ProductAssembly } from "@mxt/mxt-services/lib"; import { AssemblyWizardStep } from "@mxt/mxt-services/lib/datatypes"; import { ISkuResolver } from './ISkuResolver'; import log from "loglevel"; export class SkuResolver implements ISkuResolver { productRouter = new ProductRouter(); delimiter = '|'; productToSkus(product: Product): Map<AssemblyWizardStep, string> { const skus = new Map<AssemblyWizardStep, string>(); skus.set(null, product.sku? product.sku: product.productId); product.forEachOnProductAssembly((selectedProduct, productAssembly) => { if (productAssembly && productAssembly.wizardStep.visible) { if (selectedProduct) { if (selectedProduct.sku) { skus.set(productAssembly.wizardStep, selectedProduct.sku); } else { let _sku = '$$productId$$' + selectedProduct.productId skus.set(productAssembly.wizardStep, _sku); } } } }); return skus; } async skusToProduct(skus: string[]): Promise<Product> { if(skus.length == 0) { log.error('WLASkuResolver::skusToProduct no skus provided'); return null; } const rootSku = skus.shift(); if(!rootSku) { log.error('WLASkuResolver::skusToProduct no sku provided'); return null; } const allProducts = (await this.productRouter.queryProducts({ clientSku: rootSku })); let rootProduct = allProducts.filter(rp => rp.categories.length > 0)[0]; if (!rootProduct) { rootProduct = allProducts[0]; } rootProduct = await this.productRouter.populateFullProduct(rootProduct); //by iterating in same order, we guarantee sku order const toCheck:{product:Product, productAssembly?:ProductAssembly}[] = [{product:rootProduct}]; while(toCheck.length > 0) { const p = toCheck.shift(); if(p.productAssembly && p.productAssembly.wizardStep.visible) { if(skus.length > 0) { const nextSku = skus.shift(); if(nextSku.startsWith('$$productId$$')) { const pid = nextSku.replace('$$productId$$', ''); const product = await this.productRouter.getProduct(pid); this.updateProductAssembly([product], p.productAssembly); } else if(nextSku == 'null') { p.productAssembly.selectedProduct = null; } else { const possibleProducts = await this.productRouter.queryProducts({ clientSku: nextSku }); if(this.updateProductAssembly(possibleProducts, p.productAssembly)) { p.productAssembly.selectedProduct = await this.productRouter.populateFullProduct(p.productAssembly.selectedProduct); } } //update p.product so we add correct children below p.product = p.productAssembly.selectedProduct; } else { log.info('No sku provided for visible step id: ' + p.productAssembly.wizardStep.id); } } if(p.product && p.product.productAssemblies) { for(let pa of p.product.productAssemblies) { toCheck.push({product:pa.selectedProduct, productAssembly: pa}); } } } return rootProduct; } /** * Attempts to update the product assembly and returns true if this was the correct product assembly for this product array, * false if this isnt the correct product assembly to update * @param products * @param productAssembly */ private updateProductAssembly(products:Product[], productAssembly:ProductAssembly): boolean { let found = false; for(let p of products) { if(productAssembly.wizardStep.macs.find(x=>x.name == p.mac)) { found = true; productAssembly.selectedProduct = p; break; } } if(!found) { log.error('Could not find product from sku ' + products[0].sku + ' that matches macs on assembly step ' + productAssembly.wizardStep.stepType + ' for pids ' + products.map(p=>p.productId).join(',')); } return found; } }
This basic, generic, example does not account for things like the same SKU in multiple places on an assembly. For example, if you have a fabric cover with the same sku that can be used for pillows OR the frame of a sofa, you may need explicit ordering of skus to know what step to put things on.
**Make sure to add the “Tabbed Menu” described in “Customizing Content with 3D Cloud” in order to add/remove or style products in the scene.
--
Terminology
Root Product - The top level product the contains an assembly. For example, a Chair, or a Fireplace, etc
Assembly Step - a configurable unit of the product; for example, the legs on a chair, or the type of stone used inside the fireplace
Data Customizations
Customized Automatic Styling
If you would like to automatically style other assemblies in the scene based on a style selection, provide a styling plugin on initialization (a new class that implementing IStylingPlugin
). This is also a great place to add additional analytics tracking.
Default StylingPlugin
import { IStylingPlugin } from '@mxt/mxt-sectionalconfig/lib/sectionalconfigurator/plugins/styling/IStylingPlugin'; export class CustomStylingPlugin implements IStylingPlugin { constructor(protected sectionDelegate:StyleSectionDelegate){} async onProductOptionClick(productId:string, img:string):Promise<void>{ await this.sectionDelegate.swapAllByStepType(this.sectionDelegate.stepType, this.sectionDelegate.currentProduct); } }
import { CustomStylingPlugin } from "..."; const sectionalConfig = new MxtSectionalConfigurator({ ... plugins?:{ styling?: CustomStylingPlugin } });
Using the SDK & Event Communicator
Registering Events
You may register callbacks for a variety of UI, product, and other notification events, allowing you to act on them.
onResetButtonClick(payload?:any) => { // Define what happens when the reset button is clicked. } configurator.eventsManager.addCallback( SdkProductEvent.ON_RESET_PRODUCTS, onResetButtonClick() )
Available events include:
Description | Response Data |
---|---|
| { type: “ON_FULLSCREEN_ACTION“ } |
| { type: “ON_HELP_ACTION“ } |
| { type: “ON_SELECT_PRODUCT“, data: { position: { x: number, y: number, z: number }, sku: string, } } |
| { type: “ON_RESET_PRODUCTS“ } |
| { type: "ON_SHARE_ACTION", data: { image: string, url: string } } |
| { type: “POST_ADD_PRODUCT“, payload: { activeProductsData: ChainNodeSkuPayload, menuProductsData: ValidatedProductData } } |
| { type: “POST_INIT_PRODUCTS“, payload: { activeProductsData: ChainNodeSkuPayload, menuProductsData: ValidatedProductData } } |
| { type: “POST_DELETE_ALL_PRODUCTS“, payload: { activeProductsData: ChainNodeSkuPayload, menuProductsData: ValidatedProductData } } |
| { type: “POST_DELETE_PRODUCT“, payload: { activeProductsData: ChainNodeSkuPayload, menuProductsData: ValidatedProductData } } |
Events List
export enum SdkProductEvent { ON_ADD_PRODUCT = 'ON_ADD_PRODUCT', ON_ADD_PRODUCTS = 'ON_ADD_PRODUCTS', ON_DELETE_ALL_PRODUCTS = 'ON_DELETE_ALL_PRODUCTS', ON_DELETE_PRODUCT = 'ON_DELETE_PRODUCT', ON_INIT_PRODUCTS = 'ON_INIT_PRODUCTS', ON_RESET_PRODUCTS = 'ON_RESET_PRODUCTS', ON_SELECT_PRODUCT = 'ON_SELECT_PRODUCT', POST_ADD_PRODUCT = 'POST_ADD_PRODUCT', POST_INIT_PRODUCTS = 'POST_INIT_PRODUCTS', POST_DELETE_ALL_PRODUCTS = 'POST_DELETE_ALL_PRODUCTS', POST_DELETE_PRODUCT = 'POST_DELETE_PRODUCT', } export interface IInitProductsEvent { skus: string[]; } export interface IAddProductEvent { sku: string; } export interface IAddProductsEvent { skus: string[]; } export interface ISelectProductEvent { id?: string; index?: number; sku: string; } export interface IDeleteProductEvent { index: number; } export enum SdkUiEvent { ERROR_ADD_PRODUCT = 'errors.addProductSDK', ERROR_ADD_PRODUCT_DELEGATE = 'errors.addProductDelegate', ERROR_ADD_PRODUCT_RESOLVER = 'errors.addProductResolver', ERROR_ADD_TO_CART = 'errors.addToCart', ERROR_DELETE_PRODUCT = 'errors.deleteProductSDK', ERROR_DELETE_ALL_PRODUCTS = 'errors.deleteAllProductsSDK', ERROR_INIT_CONFIGURATOR = 'errors.initConfigurator', ERROR_INIT_PRODUCTS = 'errors.initProductsSDK', ERROR_INIT_PRODUCTS_SCENE = 'errors.initProductsScene', ERROR_INVALID_PRODUCT = 'errors.invalidProduct', ERROR_MOUNT_POINT_DATA = 'errors.mountPointData', ERROR_NO_ACTION = 'errors.noActionButtonController', ERROR_NO_CANVAS = 'errors.noCanvasScene', ERROR_PIN_CLICK = 'errors.pinClickDelegate', ERROR_SELECT_PRODUCT = 'errors.selectProductSDK', ERROR_VALIDATING_PRODUCT = 'errors.validatingProductResolveManager', ON_ERROR = 'ON_ERROR', ON_FULLSCREEN_ACTION = 'ON_FULLSCREEN_ACTION', ON_HELP_ACTION = 'ON_HELP_ACTION', ON_SELECT_PRODUCT = 'ON_SELECT_PRODUCT', ON_NOTIFICATION_MESSAGE = 'ON_NOTIFICATION_MESSAGE', ON_SHARE_ACTION = 'ON_SHARE_ACTION', ON_TOOLTIP_MESSAGE = 'ON_TOOLTIP_MESSAGE', RESET = 'reset', RESET_COMPLETE = 'pubresetcomplete', SET_NEW_CATEGORY = 'updatefamily', SET_NEW_CATEGORY_COMPLETE = 'pubupdatefamilycomplete' } export enum SdkScreenshotEvent { TAKE_SCREENSHOT = 'takeScreenshot', TAKE_SCREENSHOT_ALL = 'takeScreenshotAll', SCREENSHOT_TAKEN = 'screenshotTaken', } export enum SdkNotificationEvent { DISABLED_ARM_PIECE = 'notifications.disabledArmPiece', DISABLED_INSIDE_PIECE = 'notifications.disabledInsidePiece', MAX_CORNER_WEDGES = 'notifications.maxCornerWedges', OTTOMAN_ADD = 'notifications.ottomanAdd', UNAVAILABLE_PRODUCT = 'notifications.unavailableProduct' }
Subscribing Directly to Events
If you prefer receive notification of events using event listeners instead of callbacks, that is also possible. This would be the suggested method when operating in an Iframe. The list of events below can be subscribed to.
SDK Manager Class
You may optionally provide a custom SdkManager
on init. This class contains methods that you can use to interact with the sectional configurator canvas
import { ISdkManager } from '@mxt/mxt-sectionalconfig/sectionalconfigurator/sdk/ISdkManager'; export class SdkManager implements ISdkManager { eventCommunicator: MxtSectionalEventCommunicator; constructor( public mxtConfiguratorDelegate: MxtSectionalConfiguratorDelegate, eventCommunicator: MxtSectionalEventCommunicator, ) /* Render an arrangement of products (and configurations). * example payload using pipe delimiter: * { * skus: [ * `{baseSku_a}|{visibleStepSku_a0}|{visibleStepSku_a1}...`, * `{baseSku_b}|{visibleStepSku_b0}|{visibleStepSku_b1}...`, * `{baseSku_c}|{visibleStepSku_c0}|{visibleStepSku_c1}...`, * ] * } */ async initProducts(payload: IInitProductsEvent): Promise<SdkProductsData>; async addProduct(payload: IAddProductEvent): Promise<SdkProductsData>; async deleteAllProducts(): Promise<void>; async deleteProduct(payload: IDeleteProductEvent): Promise<SdkProductsData>; async selectProduct(payload: ISelectProductEvent): Promise<SdkProductsData>; async takeScreenshotOfSingleProduct(options: IMxtSectionalConfiguratorScreenshotOptions): Promise<string>; async takeScreenshotOfAllProducts(options: IMxtSectionalConfiguratorScreenshotOptions): Promise<Array<string>>; // Get information about the active products in the scene, as well as the menu // products. getProductsData(): SdkProductsData; getSelectedProduct(payload: ISelectProductEvent): ChainNodeSkuPayload; // Register a callback to the specified SdkUiEvent or SdkProductEvent. addCallback(event: SdkUiEvent | SdkProductEvent, cb: (data?: any) => void): void; onInitProducts(data?: any): void; // Render current active products, plus an additional product (and configurations // ) at the rightmost position. onAddProduct(data?: any): void; onDeleteProduct(data?: any): void; onDeleteAllProducts(data?: any): void; async onErrorAction(data: { error: { message: string, stack: any }, type: SdkUiEvent }): Promise<void>; onFullscreenAction(data?: any): void; onHelpAction(data?: any): void; onReset(data?: any): void; onSetNewCategory(data?: any): void; async onShareAction(): Promise<void>; onNotificationMessage(data: string): void; // Get a x64 image of a single product in the current sectional arrangement. async onTakeScreenshotOfSingleProduct(message: any): void; // Get a x64 image of each product in the current sectional arrangement, returned // as an array of strings (in arrangement order). async onTakeScreenshotOfAllProducts(message: any): void; onResetProducts(): void; // Display all elegable positions for the provided product. Once a position // is clicked, render the new product at that position in the scene. // To cancel product selection, set the sku property on the payload to: null onSelectProduct(data?: { sku: string, position: IVector3 }): void; }