UNPKG

@plattar/plattar-ar-adapter

Version:

Plattar AR Adapter for interfacing with Google & Apple WebAR

509 lines (508 loc) 20.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ConfiguratorState = void 0; const plattar_api_1 = require("@plattar/plattar-api"); /** * Manages a Configuration State of multiple Products with multiple Variations * Allows easily changing */ class ConfiguratorState { constructor(state = null) { this._mappedVariationIDValues = new Map(); this._mappedVariationSKUValues = new Map(); const defaultState = { meta: { scene_product_index: 0, scene_model_index: 0, product_index: 0, product_variation_index: 1, meta_index: 2, }, states: [] }; if (state) { try { const decodedb64State = atob(state); const parsedState = JSON.parse(decodedb64State); // set the meta data if (parsedState.meta) { defaultState.meta.scene_product_index = parsedState.meta.scene_product_index || 0; defaultState.meta.scene_model_index = parsedState.meta.scene_model_index || 0; defaultState.meta.product_index = parsedState.meta.product_index || 0; defaultState.meta.product_variation_index = parsedState.meta.product_variation_index || 1; defaultState.meta.meta_index = parsedState.meta.meta_index || 2; } defaultState.states = parsedState.states || []; } catch (err) { console.error("ConfiguratorState.constructor() - there was an error parsing configurator state"); console.error(err); } } this._state = defaultState; } /** * Modifyes the SceneProduct that this Variation SKU belongs to and changes for * purposes of Configuration */ setVariationSKU(productVariationSKU) { const variationIDs = this._mappedVariationSKUValues.get(productVariationSKU); if (!variationIDs) { console.warn("ConfiguratorState.setVariationSKU() - Variation SKU of " + productVariationSKU + " is not defined in any variations"); return; } variationIDs.forEach((variationID) => { this.setVariationID(variationID); }); } /** * Modifyes the SceneProduct that this Variation belongs to and changes for * purposes of Configuration */ setVariationID(productVariationID) { const sceneProductID = this._mappedVariationIDValues.get(productVariationID); if (!sceneProductID) { console.warn("ConfiguratorState.setVariationID() - Variation ID of " + productVariationID + " is not defined in any products"); return; } this.setSceneProduct(sceneProductID, productVariationID); } /** * Adds a new Scene Product/Variation combo with meta-data into the Configurator State * * @param sceneProductID - The Scene Product ID to be used (as defined in Plattar CMS) * @param productVariationID - The Product Variation ID to be used (as defined in Plattar CMS) * @param metaData - Arbitrary meta-data that can be used against certain operaions */ setSceneProduct(sceneProductID, productVariationID, metaData = null) { this.addSceneProduct(sceneProductID, productVariationID, metaData); } /** * Adds a new SceneModel with meta-data into the Configurator State. Note that the SceneProductDataMeta will * override the isSceneModel variable and assign to "true" automatically * * @param SceneModelID - The SceneModel ID to add * @param metaData - Arbitrary meta-data that can be used against certain operaions */ setSceneModel(SceneModelID, metaData = null) { if (SceneModelID) { metaData = metaData || { augment: true, type: "scenemodel" }; metaData.type = "scenemodel"; const states = this._state.states; const meta = this._state.meta; let newData = null; const existingData = this.findSceneProductIndex(SceneModelID); if (existingData) { newData = existingData; } else { newData = []; // push the new data into the stack states.push(newData); } newData[meta.scene_product_index] = SceneModelID; newData[meta.product_variation_index] = null; newData[meta.meta_index] = metaData; } } /** * Adds a new Product with meta-data into the Configurator State * * @param productID - The Product ID to add * @param metaData - Arbitrary meta-data that can be used against certain operaions */ setProduct(productID, productVariationID, metaData = null) { if (productID && productVariationID) { metaData = metaData || { augment: true, type: "product" }; metaData.type = "product"; const states = this._state.states; const meta = this._state.meta; let newData = null; const existingData = this.findSceneProductIndex(productID); if (existingData) { newData = existingData; } else { newData = []; // push the new data into the stack states.push(newData); } newData[meta.product_index] = productID; newData[meta.product_variation_index] = productVariationID; newData[meta.meta_index] = metaData; } } /** * Adds a new Scene Product/Variation combo with meta-data into the Configurator State * * @param sceneProductID - The Scene Product ID to be used (as defined in Plattar CMS) * @param productVariationID - The Product Variation ID to be used (as defined in Plattar CMS) * @param metaData - Arbitrary meta-data that can be used against certain operaions */ addSceneProduct(sceneProductID, productVariationID, metaData = null) { if (sceneProductID && productVariationID) { metaData = metaData || { augment: true, type: "sceneproduct" }; metaData.type = "sceneproduct"; const states = this._state.states; const meta = this._state.meta; let newData = null; const existingData = this.findSceneProductIndex(sceneProductID); if (existingData) { newData = existingData; } else { newData = []; // push the new data into the stack states.push(newData); } newData[meta.scene_product_index] = sceneProductID; newData[meta.product_variation_index] = productVariationID; newData[meta.meta_index] = metaData; } } /** * Search and return the data index reference for the provided Scene Product ID * if not found, will return null * @param sceneProductID */ findSceneProductIndex(sceneProductID) { const states = this._state.states; if (states.length > 0) { const meta = this._state.meta; const found = states.find((productState) => { return productState[meta.scene_product_index] === sceneProductID; }); return found ? found : null; } return null; } /** * Search and return the data for the provided Scene Product ID * if not found, will return null * @param sceneProductID */ findSceneProduct(sceneProductID) { const found = this.findSceneProductIndex(sceneProductID); if (found) { const meta = this._state.meta; const data = { scene_product_id: found[meta.scene_product_index], product_variation_id: found[meta.product_variation_index], meta_data: { augment: true, type: "sceneproduct" } }; // include the meta-data if (found.length === 3) { data.meta_data.augment = found[meta.meta_index].augment || true; data.meta_data.type = found[meta.meta_index].type || "sceneproduct"; } return data; } return null; } /** * Iterate over the internal state data */ forEach(callback) { const states = this._state.states; const meta = this._state.meta; if (states.length > 0) { states.forEach((productState) => { if (productState.length === 2) { callback({ scene_product_id: productState[meta.scene_product_index], product_variation_id: productState[meta.product_variation_index], meta_data: { augment: true, type: "sceneproduct" } }); } else if (productState.length === 3) { callback({ scene_product_id: productState[meta.scene_product_index], product_variation_id: productState[meta.product_variation_index], meta_data: { augment: productState[meta.meta_index].augment ?? true, type: productState[meta.meta_index].type || "sceneproduct" } }); } }); } } /** * Compose and return an array of all internal objects */ array() { const array = new Array(); this.forEach((object) => { array.push(object); }); return array; } /** * @returns Returns the first reference of data in the stack, otherwise returns null */ first() { const states = this._state.states; if (states.length > 0) { const meta = this._state.meta; const found = states.find((productState) => { const check = productState[meta.scene_product_index]; // ensure the data contains valid elements return check !== null && check !== undefined; }); if (!found) { return null; } const data = { scene_product_id: found[meta.scene_product_index], product_variation_id: found[meta.product_variation_index], meta_data: { augment: true, type: "sceneproduct" } }; // include the meta-data if (found.length === 3) { data.meta_data.augment = found[meta.meta_index].augment || true; data.meta_data.type = found[meta.meta_index].type || "sceneproduct"; } return data; } return null; } /** * @returns Returns the first reference of data in the stack that matches a type, otherwise returns null */ firstOfType(type) { const states = this._state.states; if (states.length > 0) { const meta = this._state.meta; const found = states.find((productState) => { const check = productState[meta.scene_product_index]; if (check !== null && check !== undefined) { return productState.length === 3 && productState[meta.meta_index].type === type; } return false; }); if (!found) { return null; } const data = { scene_product_id: found[meta.scene_product_index], product_variation_id: found[meta.product_variation_index], meta_data: { augment: found[meta.meta_index].augment || true, type: found[meta.meta_index].type || type } }; return data; } return null; } firstActiveOfType(type) { const states = this._state.states; if (states.length > 0) { const meta = this._state.meta; const found = states.find((productState) => { const check = productState[meta.scene_product_index]; if (check !== null && check !== undefined) { return productState.length === 3 && productState[meta.meta_index].type === type && productState[meta.meta_index].augment === true; } return false; }); if (!found) { return null; } const data = { scene_product_id: found[meta.scene_product_index], product_variation_id: found[meta.product_variation_index], meta_data: { augment: found[meta.meta_index].augment || true, type: found[meta.meta_index].type || type } }; return data; } return null; } get length() { return this._state.states.length; } /** * Decodes and returns an instance of ConfiguratorState from a previously * encoded state * @param state - The previously encoded state as a Base64 String * @returns - ConfiguratorState instance */ static decode(state) { return new ConfiguratorState(state); } /** * Decodes a previously generated state * @param sceneID * @param state * @returns */ static async decodeState(sceneID = null, state = null) { if (!sceneID || !state) { throw new Error("ConfiguratorState.decodeState(sceneID, state) - sceneID and state must be defined"); } const configState = new ConfiguratorState(state); const fscene = new plattar_api_1.Scene(sceneID); fscene.include(plattar_api_1.Project); fscene.include(plattar_api_1.Product); fscene.include(plattar_api_1.SceneProduct); fscene.include(plattar_api_1.SceneModel); fscene.include(plattar_api_1.SceneProduct.include(plattar_api_1.Product.include(plattar_api_1.ProductVariation))); const scene = await fscene.get(); return { scene: scene, state: configState }; } /** * Generates a new ConfiguratorState instance from all SceneProducts and default * variations from the provided Scene ID * @param sceneID - the Scene ID to generate * @returns - Promise that resolves into a ConfiguratorState instance */ static async decodeScene(sceneID = null) { if (!sceneID) { throw new Error("ConfiguratorState.decodeScene(sceneID) - sceneID must be defined"); } const configState = new ConfiguratorState(); const fscene = new plattar_api_1.Scene(sceneID); fscene.include(plattar_api_1.Project); fscene.include(plattar_api_1.SceneProduct); fscene.include(plattar_api_1.SceneModel); fscene.include(plattar_api_1.Product); fscene.include(plattar_api_1.SceneProduct.include(plattar_api_1.Product.include(plattar_api_1.ProductVariation))); const scene = await fscene.get(); const sceneProducts = scene.relationships.filter(plattar_api_1.SceneProduct); const sceneModels = scene.relationships.filter(plattar_api_1.SceneModel); const products = scene.relationships.filter(plattar_api_1.Product); // add our scene models sceneModels.forEach((sceneModel) => { configState.setSceneModel(sceneModel.id, { augment: sceneModel.attributes.include_in_augment, type: "scenemodel" }); }); // add our products - this is used when scene == furniture products.forEach((product) => { if (product.attributes.product_variation_id) { configState.setProduct(product.id, product.attributes.product_variation_id, { augment: true, type: "product" }); } }); // add our scene products sceneProducts.forEach((sceneProduct) => { const product = sceneProduct.relationships.find(plattar_api_1.Product); if (product) { if (product.attributes.product_variation_id) { configState.setSceneProduct(sceneProduct.id, product.attributes.product_variation_id, { augment: sceneProduct.attributes.include_in_augment, type: "sceneproduct" }); } // add the variation to an acceptible range of values const variations = product.relationships.filter(plattar_api_1.ProductVariation); variations.forEach((variation) => { configState._mappedVariationIDValues.set(variation.id, sceneProduct.id); if (variation.attributes.sku) { const existingSKUs = configState._mappedVariationSKUValues.get(variation.attributes.sku); if (existingSKUs) { existingSKUs.push(variation.id); } else { configState._mappedVariationSKUValues.set(variation.attributes.sku, [variation.id]); } } }); } }); return { scene: scene, state: configState }; } /** * Encode and return the internal ConfiguratorState as a Base64 String * @returns - Base64 String */ encode() { return btoa(JSON.stringify(this._state)); } async encodeSceneGraphID() { const graph = this.sceneGraph; // some scene-graphs are very large in size, we store it remotely // this storage will expire in 10 minutes so this is a non-permanent version // and is designed for quick ar const url = `https://c.plattar.com/v3/redir/store`; // finally send our scene-graph to the backend to generate the AR file and return try { const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ data: { attributes: { data: graph } } }) }); if (!response.ok) { throw new Error(`ConfiguratorState.encodeSceneGraphID() - network response was not ok ${response.status}`); } const data = await response.json(); return data.data.id; } catch (error) { throw new Error(`ConfiguratorState.encodeSceneGraphID() - there was a request error to ${url}, error was ${error.message}`); } } /** * Compiles and returns the Dynamic Scene Graph (Updated for 2025 for DynamicAR) * NOTE: Eventually this structure should replace ConfiguratorState */ get sceneGraph() { const objects = this.array(); // in here we need to generate the schema input to be sent to the backend service const schema = { // ensure to only generate AR using files we pass into the backend strict: false, inputs: [] }; objects.forEach((object) => { if (object.meta_data.type === "scenemodel") { const data = { id: object.scene_product_id, type: 'scenemodel', visibility: object.meta_data.augment }; schema.inputs.push(data); } else { const data = { id: object.scene_product_id, type: 'sceneproduct', variation_id: object.product_variation_id, visibility: object.meta_data.augment }; schema.inputs.push(data); } }); return schema; } } exports.ConfiguratorState = ConfiguratorState;