@plattar/plattar-ar-adapter
Version:
Plattar AR Adapter for interfacing with Google & Apple WebAR
509 lines (508 loc) • 20.6 kB
JavaScript
"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;