/**
* @file ARgo Application Script
* @author Perikles C. Stephanidis <perikles@stephanidis.net>
* @copyright Doticca SRL 2019
* @version 1.0.0
*/
// @ts-check
// ReSharper disable UseOfImplicitGlobalInFunctionScope
// ReSharper disable PossiblyUnassignedProperty
// ReSharper disable InconsistentNaming
// #region Type Definitions
/**
* @typedef {import("AR").Error} ARError
* @typedef {import("AR").Sound} Sound
* @typedef {import("AR").Model} Model
* @typedef {import("AR").Animation} ARAnimation
* @typedef {import("AR").Drawable} Drawable
* @typedef {import("AR").BaseTracker} BaseTracker
* @typedef {import("AR").ImageTracker} ImageTracker
* @typedef {import("AR").ImageTrackable} ImageTrackable
* @typedef {import("AR").ImageDrawable} ImageDrawable
* @typedef {import("AR").ImageTarget} ImageTarget
* @typedef {import("AR").Label} Label
* @typedef {import("AR").PositionableOptions} PositionableOptions
*/
/**
* Definition of a helper callback that checks a condition
* and returns a `boolean` value.
* @callback conditionCallback
* @param {any} [data]
* @returns {boolean}
*/
// #endregion
AR.hardware.camera.enabled = false;
$.mobile.defaultPageTransition = "none";
// #region ARgoException
class ARgoException extends Error {
constructor(message) {
super(message);
// Maintains proper stack trace for where our error
// was thrown (only available on V8).
// @ts-ignore
if (Error.captureStackTrace) {
// @ts-ignore
Error.captureStackTrace(this, ARgoException);
}
this.name = 'ARgoException';
this.date = new Date();
}
}
// #endregion
// #region ARgoDefaultSound
/**
* A default empty sound.
* @type {Sound}
*/
const ARgoDefaultSound = new AR.Sound("");
// #endregion
// #region ARgoDefaultLayout
/**
* A 3D model's default layout.
* @type {ModelLayout}
*/
const ARgoDefaultLayout = {
scale: 0,
translate: {
x: 0,
y: 0,
z: 0
},
rotate: {
x: 0,
y: 0,
z: 0
},
isEmpty: true
};
// #endregion
// #region ARgoObjectManipulation
/**
* Supports user manipulation of a 3D model.
*/
class ARgoObjectManipulationData {
/**
* Creates an instance of `ARgoObjectManipulationData`.
* @param {number} initialRotationValue
* Initial rotation.
*/
constructor(initialRotationValue) {
this.previousDragValue = {
x: 0,
y: 0
};
this.previousScaleValue = 0;
this.previousRotationValue = initialRotationValue;
}
}
// #endregion
// #region ARgoAnimationGroup
class ARgoAnimationGroup extends AR.AnimationGroup {
/**
*
* @param {string} type
* @param {ARAnimation[]} animations
* @param {{ onStart?: () => void, onFinish?: () => void, loopTimes?: number }} [options]
*/
constructor(type, animations, options) {
super(type, animations, options)
/**
* Defines how many times should the animation be played. A negative value indicates an infinite looping.
* Must be a whole number.
* @type {number}
*/
this.loopTimes = options ? (typeof options.loopTimes == 'number') ? options.loopTimes : 1 : 1;
}
start() {
super.start(this.loopTimes);
}
}
// #endregion
// #region ARgoModel
/**
* A Model drawable represents an `ARObject` as a 3D Model.
* This subclass adds basic animation layouts' handling.
* @augments AR.Model
* @inheritdoc
*/
class ARgoModel extends AR.Model {
/**
* Creates an instance of an `ARgoModel` drawable that
* represents an `ARObject` as a 3D Model.
* @param {ARgoDrawableOptions} [options]
* Source and setup parameters to customize additional object properties.
*/
constructor(options) {
if (options && options.defaultLayout) {
let layout = ARgoModel.getSafeLayout(options.defaultLayout);
options.scale = {
x: layout.scale,
y: layout.scale,
z: layout.scale
};
options.translate = layout.translate;
options.rotate = layout.rotate;
}
if (options && options.allowManipulation) {
options.onScaleBegan = function ( /*scale*/) {
// @ts-ignore
if (!this._isOnPresentation && this._isSnapped) {
this.manipulationData.previousScaleValue = this.scale.x;
}
};
options.onScaleChanged = function (scale) {
// @ts-ignore
if (!this._isOnPresentation && this._isSnapped) {
var routerScale = this.manipulationData.previousScaleValue * scale;
if (routerScale >= 2.5) {
return;
}
this.scale = {
x: routerScale,
y: routerScale,
z: routerScale
};
}
};
options.onDragChanged = function (x, y) {
// @ts-ignore
if (!this._isOnPresentation && this._isSnapped) {
var movement = {
x: 0,
y: 0
};
/* Calculate the touch movement between this event and the last one */
movement.x = this.manipulationData.previousDragValue.x - x;
movement.y = this.manipulationData.previousDragValue.y - y;
if (this.canRotate()) {
/*
Rotate the model accordingly to the calculated movement values
and the current orientation of the model.
*/
this.rotate.y += (Math.cos(this.rotate.z * Math.PI / 180) * movement.x *
-1 + Math.sin(this.rotate.z * Math.PI / 180) * movement.y) * 180;
this.rotate.x += (Math.cos(this.rotate.z * Math.PI / 180) * movement.y +
Math.sin(this.rotate.z * Math.PI / 180) * movement.x) * -180;
} else {
this.translate.x -= movement.x * 4;
this.translate.y += movement.y * 4;
}
this.manipulationData.previousDragValue.x = x;
this.manipulationData.previousDragValue.y = y;
}
};
options.onDragEnded = function ( /*x, y*/) {
// @ts-ignore
if (!this._isOnPresentation && this._isSnapped) {
this.manipulationData.previousDragValue.x = 0;
this.manipulationData.previousDragValue.y = 0;
}
};
options.onRotationChanged = function (angleInDegrees) {
// @ts-ignore
if (!this.isOnPresentation && this.canRotate()) {
this.rotate.z = this.manipulationData.previousRotationValue - angleInDegrees;
}
};
options.onRotationEnded = function ( /*angleInDegrees*/) {
// @ts-ignore
if (!this.isOnPresentation && this.canRotate()) {
this.manipulationData.previousRotationValue = this.rotate.z;
}
};
}
super(options.source, options);
/**
* The default layout of the object in the AR environment.
* If the model is animated and you set this, you do not need to set
* the regular layout options.
* @type {ModelLayout}
*/
this.defaultLayout = options ? ARgoModel.getSafeLayout(options.defaultLayout) : ARgoDefaultLayout;
/**
* The layout of the object at the beginning of
* a snapping animation (if any).
* @type {ModelLayout}
*/
this.rootLayout = options ? ARgoModel.getSafeLayout(options.rootLayout) : ARgoDefaultLayout;
/**
* The layout of the object when on presentation mode.
* @type {ModelLayout}
*/
this.presentationLayout = options ? ARgoModel.getSafeLayout(options.presentationLayout) : ARgoDefaultLayout;
/**
* Allows user manipulation when snapped on screen.
* @type {boolean}
*/
this.allowManipulation = options ? options.allowManipulation : false;
/**
* Initial manipulation data.
* @type {ARgoObjectManipulationData}
*/
this.manipulationData = options ? options.manipulationData : new ARgoObjectManipulationData(0);
/**
* A callback that indicates if rotation is allowed during manipulation.
*/
this.canRotate = options ? options.canRotate : function () { return true; };
/**
* Called when the model is about to be shown.
* @type {(model: ARgoModel) => ARgoAnimationGroup}
*/
this.onCreateAppearingAnimation = options ? options.onCreateAppearingAnimation : null;
/**
* Called when the model is about to be hidden.
* @type {(model: ARgoModel) => ARgoAnimationGroup}
*/
this.onCreateHidingAnimation = options ? options.onCreateHidingAnimation : null;
/**
* Called when the model is about to go to presentation mode.
* @type {(model: ARgoModel) => ARgoAnimationGroup}
*/
this.onCreatePresentationAnimation = options ? options.onCreatePresentationAnimation : null;
/**
* Called when the model is about to resume from presentation mode.
* @type {(model: ARgoModel) => ARgoAnimationGroup}
*/
this.onCreateResetAnimation = options ? options.onCreateResetAnimation : null;
/**
* Called when the model is shown and after any appearing animations have completed.
* @type {(model: ARgoModel) => void}
*/
this.onAppearingComplete = options ? options.onAppearingComplete : null;
/**
* Called when the model is shown and after any hiding animations have completed.
* @type {(model: ARgoModel) => void}
*/
this.onHidingComplete = options ? options.onHidingComplete : null;
/**
* Called when the model has entered Presentation Mode and after any animations have completed.
* @type {(model: ARgoModel) => void}
*/
this.onPresentationComplete = options ? options.onPresentationComplete : null;
/**
* Called when the model has exited Presentation Mode and after any animations have completed.
* @type {(model: ARgoModel) => void}
*/
this.onResetComplete = options ? options.onResetComplete : null;
/**
* @private
*/
this._isSnapped = false;
/**
* @private
*/
this._isOnPresentation = false;
/**
* The animation that brings this model to and back from Presentation Mode.
* @type {ARgoAnimationGroup}
*/
this.presentationAnimation = null;
/**
* The name of the model presented in association to this model.
* @type {string}
*/
this.presentedModel = null;
/**
* An animation that initially shows or later hides the model.
* @type {ARgoAnimationGroup}
*/
this.appearanceAnimation = null;
}
show(animate = false) {
if (this._isSnapped) {
return;
}
World.destroyAnimation(this.presentationAnimation);
this.presentationAnimation = null;
World.destroyAnimation(this.appearanceAnimation);
this.appearanceAnimation = null;
if (animate) {
// We need a starting layout before we create the animation
// and that should be either a provided or the default root.
if (this.onCreateAppearingAnimation) {
if (!this.rootLayout.isEmpty) {
this.applyRootLayout();
}
this.appearanceAnimation = this.onCreateAppearingAnimation(this);
} else {
this.applyRootLayout();
this.appearanceAnimation = ARgoModel.createDefaultAnimation(this, this.defaultLayout);
}
let onFinishCallback = this.appearanceAnimation.onFinish;
let model = this;
this.appearanceAnimation.onFinish = function () {
model.applyDefaultLayout();
if (onFinishCallback) {
onFinishCallback();
}
if (model.onAppearingComplete) {
model.onAppearingComplete(model);
}
World.destroyAnimation(model.appearanceAnimation);
model.appearanceAnimation = null;
}
this.enabled = true;
this.appearanceAnimation.start();
return;
}
this.applyDefaultLayout();
this.enabled = true;
if (this.onAppearingComplete) {
this.onAppearingComplete(this);
}
}
hide(animate = false) {
World.destroyAnimation(this.presentationAnimation);
this.presentationAnimation = null;
World.destroyAnimation(this.appearanceAnimation);
this.appearanceAnimation = null;
if (animate) {
this.appearanceAnimation = this.onCreateHidingAnimation ?
this.onCreateHidingAnimation(this) :
ARgoModel.createDefaultAnimation(this, this.rootLayout);
let onFinishCallback = this.appearanceAnimation.onFinish;
let model = this;
this.appearanceAnimation.onFinish = function () {
model.applyRootLayout();
if (onFinishCallback) {
onFinishCallback();
}
if (model.onHidingComplete) {
model.onHidingComplete(model);
}
World.destroyAnimation(model.appearanceAnimation);
model.appearanceAnimation = null;
}
// Start from current layout.
this.appearanceAnimation.start();
return;
}
this.enabled = false;
if (this.onHidingComplete) {
this.onHidingComplete(this);
}
}
get isSnapped() {
return this._isSnapped;
}
/**
* Gets or sets if the model is currently in Presentation Mode.
* @type {boolean}
*/
get isOnPresentation() {
return this._isOnPresentation;
}
set isOnPresentation(value) {
if (value === this._isOnPresentation) {
return;
}
// Previous state must had been the default layout,
// irrespective of manipulation.
if (value && !this._isSnapped) {
throw new ARgoException("The model is not shown yet");
}
World.destroyAnimation(this.appearanceAnimation);
this.appearanceAnimation = null;
World.destroyAnimation(this.presentationAnimation);
this.presentationAnimation = null;
if (value) {
if (!this.presentationLayout.isEmpty) {
if (this.onCreatePresentationAnimation) {
this.presentationAnimation = this.onCreatePresentationAnimation(this);
}
this.presentationAnimation = this.presentationAnimation ||
ARgoModel.createDefaultAnimation(this, this.presentationLayout);
let onFinishCallback = this.presentationAnimation.onFinish;
let model = this;
this.presentationAnimation.onFinish = function () {
model.applyPresentationLayout();
if (onFinishCallback) {
onFinishCallback();
}
if (model.onPresentationComplete) {
model.onPresentationComplete(model);
}
World.destroyAnimation(model.presentationAnimation);
model.presentationAnimation = null;
}
this.presentationAnimation.start();
return;
}
this.applyPresentationLayout();
if (this.onPresentationComplete) {
this.onPresentationComplete(this);
}
} else { // defaultLayout cannot ever be empty.
this.presentationAnimation = this.onCreateResetAnimation ?
this.onCreateResetAnimation(this) :
ARgoModel.createDefaultAnimation(this, this.defaultLayout);
let onFinishCallback = this.presentationAnimation.onFinish;
let model = this;
this.presentationAnimation.onFinish = function () {
model.applyDefaultLayout();
if (onFinishCallback) {
onFinishCallback();
}
if (model.onResetComplete) {
model.onResetComplete(model);
}
World.destroyAnimation(model.presentationAnimation);
model.presentationAnimation = null;
}
this.presentationAnimation.start();
}
}
/**
* Applies the `defaultLayout` to the model.
*/
applyDefaultLayout() {
this.scale = {
x: this.defaultLayout.scale,
y: this.defaultLayout.scale,
z: this.defaultLayout.scale
};
this.translate = {
x: this.defaultLayout.translate.x,
y: this.defaultLayout.translate.y,
z: this.defaultLayout.translate.z
};
this.rotate = {
x: this.defaultLayout.rotate.x,
y: this.defaultLayout.rotate.y,
z: this.defaultLayout.rotate.z
};
this._isOnPresentation = false;
this._isSnapped = true;
}
/**
* Applies the `applyRootLayout` to the model.
*/
applyRootLayout() {
this.scale = {
x: this.rootLayout.scale,
y: this.rootLayout.scale,
z: this.rootLayout.scale
};
this.translate = {
x: this.rootLayout.translate.x,
y: this.rootLayout.translate.y,
z: this.rootLayout.translate.z
};
this.rotate = {
x: this.rootLayout.rotate.x,
y: this.rootLayout.rotate.y,
z: this.rootLayout.rotate.z
};
this._isOnPresentation = false;
this._isSnapped = false;
}
/**
* Applies the `presentationLayout` to the model.
*/
applyPresentationLayout() {
this.scale = {
x: this.presentationLayout.scale,
y: this.presentationLayout.scale,
z: this.presentationLayout.scale
};
this.translate = {
x: this.presentationLayout.translate.x,
y: this.presentationLayout.translate.y,
z: this.presentationLayout.translate.z
};
this.rotate = {
x: this.presentationLayout.rotate.x,
y: this.presentationLayout.rotate.y,
z: this.presentationLayout.rotate.z
};
this._isSnapped = false;
this._isOnPresentation = true;
}
/**
* Applies a specified `layout` to the model.
* @param {ModelLayout} layout
*/
applyLayout(layout) {
this.scale = {
x: layout.scale,
y: layout.scale,
z: layout.scale
};
this.translate = {
x: layout.translate.x,
y: layout.translate.y,
z: layout.translate.z
};
this.rotate = {
x: layout.rotate.x,
y: layout.rotate.y,
z: layout.rotate.z
};
}
/**
* Gets a layout that has all members initialized.
* @param {ModelLayout} layout
* @returns {ModelLayout}
* A `ModelLayout` instance that has all members initialized.
*/
static getSafeLayout(layout) {
if (!layout) {
return ARgoDefaultLayout;
}
const emptyVector = { x: 0, y: 0, z: 0 };
let scale = layout.scale || 0;
let translate = layout.translate || emptyVector;
let rotate = layout.rotate || emptyVector;
return {
scale: scale,
translate: {
x: translate.x || 0,
y: translate.y || 0,
z: translate.z || 0
},
rotate: {
x: rotate.x || 0,
y: rotate.y || 0,
z: rotate.z || 0
},
isEmpty: false
}
}
/**
* Creates a default animation for animating from the current layout to another.
* @param {ARgoModel} model
* @param {ModelLayout} layout
*/
static createDefaultAnimation(model, layout) {
var scale = layout.scale;
var duration = 2000;
/* X animations */
var routerScaleAnimationX = new AR.PropertyAnimation(model, "scale.x", model.scale.x, scale, duration, {
type: AR.CONST.EASING_CURVE_TYPE.EASE_IN_OUT_SINE
});
/* Y animations */
var routerScaleAnimationY = new AR.PropertyAnimation(model, "scale.y", model.scale.y, scale, duration, {
type: AR.CONST.EASING_CURVE_TYPE.EASE_IN_OUT_SINE
});
/* Z animations */
var routerScaleAnimationZ = new AR.PropertyAnimation(model, "scale.z", model.scale.z, scale, duration, {
type: AR.CONST.EASING_CURVE_TYPE.EASE_IN_OUT_SINE
});
/* X animations */
var routerTranslateAnimationX = new AR.PropertyAnimation(model, "translate.x", model.translate.x, layout.translate.x, duration, {
type: AR.CONST.EASING_CURVE_TYPE.EASE_IN_OUT_SINE
});
/* Y animations */
var routerTranslateAnimationY = new AR.PropertyAnimation(model, "translate.y", model.translate.y, layout.translate.y, duration, {
type: AR.CONST.EASING_CURVE_TYPE.EASE_IN_OUT_SINE
});
/* Z animations */
var routerTranslateAnimationZ = new AR.PropertyAnimation(model, "translate.z", model.translate.z, layout.translate.z, duration, {
type: AR.CONST.EASING_CURVE_TYPE.EASE_IN_OUT_SINE
});
/* X animations */
var routerRotateAnimationX = new AR.PropertyAnimation(model, "rotate.x", model.rotate.x, layout.rotate.x, duration, {
type: AR.CONST.EASING_CURVE_TYPE.EASE_IN_OUT_SINE
});
/* Y animations */
var routerRotateAnimationY = new AR.PropertyAnimation(model, "rotate.y", model.rotate.y, layout.rotate.y, duration, {
type: AR.CONST.EASING_CURVE_TYPE.EASE_IN_OUT_SINE
});
/* Z animations */
var routerRotateAnimationZ = new AR.PropertyAnimation(model, "rotate.z", model.rotate.z, layout.rotate.z, duration, {
type: AR.CONST.EASING_CURVE_TYPE.EASE_IN_OUT_SINE
});
return new ARgoAnimationGroup(AR.CONST.ANIMATION_GROUP_TYPE.PARALLEL, [
routerScaleAnimationX,
routerScaleAnimationY,
routerScaleAnimationZ,
routerTranslateAnimationX,
routerTranslateAnimationY,
routerTranslateAnimationZ,
routerRotateAnimationX,
routerRotateAnimationY,
routerRotateAnimationZ
]);
}
}
// #endregion
// #region ARgoPositionable
/**
* Allows the display of 3D models snapped to the center of a
* device's screen.
* This special object requires the use of the `argo-plugins`
* module (`libargoplugins.so`).
* By default, an `ARgoPositionable` is disabled when first created.
*/
class ARgoPositionable extends AR.Positionable {
/**
* Creates an instance of `ARgoPositionable` that allows
* the display of 3D models snapped to the center of a device's
* screen.
* @param {string} name
* A unique name for thie Positionable.
* @param {ARgoPositionableOptions} options
* Setup options for this positionable.
* @param {...[string, ARgoDrawableOptions]} models
*/
constructor(name, options, ...models) {
options = options ? options : {};
options.enabled = false;
super(name, options);
/**
* The models to be positioned by this positionable.
* @type {Map<String, ARgoDrawableOptions>}
*/
this.models = new Map(models);
/**
* Adds a default background to the device's screen.
* @type {boolean}
*/
this.showBackground = options ? options.showBackground : false;
if (this.showBackground) {
let backgroundDrawable = this.getBackgroundDrawable();
this.drawables.addCamDrawable(backgroundDrawable);
this[this.backgroundDrawableUniqueName] = backgroundDrawable;
}
}
/**
* Adds a model to be controlled by this positionable.
* Models can be loaded and unloaded at runtime.
* @param {string} modelName
* Unique name for the model.
* @param {ARgoDrawableOptions} options
* Source and setup-Parameters for the model.
* @see {ARgo.MODELS}
*/
addModel(modelName, options) {
if (modelName === this.backgroundDrawableUniqueName) {
throw new ARgoException(`The unique model name ${this.backgroundDrawableUniqueName} is reserved.`);
}
this.models.set(modelName, options);
}
/**
* @param {string} modelName
* @returns {ARgoModel}
*/
getLoadedModel(modelName) {
if (modelName in this) {
return this[modelName];
}
return undefined;
}
/**
* Gets if a model with the specified unique name,
* is currently loaded by this Positionable.
* @param {string} modelName
*/
hasLoadedModel(modelName) {
return modelName in this;
}
/**
* Shows a model maintained by this Positionable.
* @param {string} modelName
* Unique name of the model.
* @param {boolean} [animate]
* @returns {boolean}
* Returns if the model is successfully loaded and shown.
*/
showModel(modelName, animate = false) {
if (!this.models.has(modelName)) {
return false;
}
if (!this.hasLoadedModel(modelName)) {
let model = new ARgoModel(this.models.get(modelName));
this.drawables.addCamDrawable(model);
this[modelName] = model;
}
this.getLoadedModel(modelName).show(animate);
return true;
}
/**
* @private
*/
getBackgroundDrawable() {
var contentsBackgroundImage = new AR.ImageResource("assets/background-dark-grey.png");
return new AR.ImageDrawable(contentsBackgroundImage, 5, {
onLoaded: World.getObjectLoadedHandler(ARgo.LOG_MESSAGES.LOAD_BACKGROUND),
zOrder: -1,
translate: {
z: 0.9
}
});
}
/**
* Gets the unique name used for the Background Drawable.
* @returns {string}
*/
get backgroundDrawableUniqueName() {
return "argo_background";
}
/**
* Hides a model maintained and already loaded by this Positionable.
* @param {string} modelName
* Unique name of loaded the model.
* @param {boolean} [animate]
*/
hideModel(modelName, animate = false) {
if (!this.hasLoadedModel(modelName)) {
return;
}
this.getLoadedModel(modelName).hide(animate);
}
/**
* Hides and unloads a model maintained and already loaded
* by this Positionable.
* @param {string} modelName
* Unique name of loaded the model.
*/
closeModel(modelName) {
if (!this.hasLoadedModel(modelName)) {
return;
}
let model = this.getLoadedModel(modelName);
model.hide();
this.drawables.removeCamDrawable(model);
delete this[modelName];
model.destroy();
}
/**
* Hides and unloads all models maintained and currently loaded
* by this Positionable.
*/
closeAll() {
let models = Array.from(this.models.keys());
for (const modelName of models) {
this.closeModel(modelName);
}
}
enable() {
this.enabled = true;
}
disable() {
this.enabled = false;
}
}
// #endregion
// #region ARgoNativeCommand
class ARgoNativeCommand {
/**
* Creates an instance of `ARgoNativeCommand`.
* @param {string} command
* The textual representation of the command wrapped by this class.
* @param {string} message
* A message to be logged, when it applies.
* @param {Object} data
* Additional data used by the command.
*/
constructor(command, message = undefined, data = undefined) {
/**
* Textual representation of the native command
* represented by this class.
* @type {string}
*/
this.command = command;
/**
* A message to be logged, when it applies.
* @type {string}
*/
this.message = message;
/**
* Additional data used by the command.
* @type {Object}
*/
this.data = data;
}
}
// #endregion
// #region ARgo
/**
* @class
* @classdesc Provides control of the application's UI environment.
* @hideconstructor
*/
const ARgo = {
/**
* @type {string}
*/
assetsFolder: "assets/",
/**
* @private
* @type {Swiper}
*/
contentsSwiper: null,
/**
* @private
* @type {Swiper}
*/
introSwiper: null,
/**
* @private
* @type {Swiper}
*/
routerSwiper: null,
/**
* @private
* @see ARgo.CURRENT_PAGE
*/
currentPage: "welcomePage",
/**
* @private
* @see ARgo.CURRENT_PAGE
*/
previousPage: "",
/**
* @private
*/
contentsDialogShown: false,
/**
* @private
*/
snappingDialogShown: false,
/**
* Gets or sets if the app is hosted in a debuggable native client.
*/
isDebuggable: false,
/**
* Available native commands.
* @enum {string}
*/
NATIVE_COMMANDS: {
INIT: "INIT",
BACK: "BACK",
LOG: "LOG",
ERROR: "ERROR"
},
/**
* Log Messages
* @enum {string}
*/
LOG_MESSAGES: {
LOAD_BACKGROUND: "Loaded 3D Background",
LOAD_MODEL_ROUTER: "Loaded 3D Router",
LOAD_MODEL_AR_ROUTER: "Loaded AR Router Model",
LOAD_MODEL_MARKER: "Loaded 3D Marker",
LOAD_MODEL_ARROW: "Loaded 3D Arrow",
LOAD_MODEL_BLOCK: "Loaded 3D Block",
LOAD_MODEL_RJ45: "Loaded 3D RJ45",
LOAD_MODEL_AR_RJ45: "Loaded AR RJ45 Model",
LOAD_MODEL_CHARGER: "Loaded 3D Charger",
LOAD_MODEL_AR_CHARGER: "Loaded AR Charger Model",
LOAD_TRACKER_BOX: "Loaded Box Tracker",
LOAD_TRACKER_ROUTER: "Loaded Router Tracker",
LOAD_TRACKABLE_BOX: "Loaded Box Trackable",
LOAD_TRACKABLE_ROUTER: "Loaded Router Trackable",
LOAD_POSITIONABLE: "Loaded Managed Positionable",
TARGET_BOX_RECOGNIZED: "Target Box Recognized",
TARGET_ROUTER_RECOGNIZED: "Target Router Recognized",
TARGET_BOX_LOST: "Target Box Lost",
TARGET_ROUTER_LOST: "Target Router Lost",
},
/**
* Available UI pages.
* @readonly
* @enum {String}
*/
PAGES: {
WELCOME: "welcomePage",
SCENARIO: "scenarioPage",
BOX_SCENARIO: "boxScenarioPage",
ROUTER_SCENARIO: "routerScenarioPage",
MAIN: "boxARPage",
CONTENTS: "contentsPage",
CONTENTS_ITEM: "itemGraphPage",
INSTRUCTIONS_1: "instructionsPage1",
ROUTER: "routerARPage",
ROUTER_GRAPH: "routerGraphPage",
RJ45_GRAPH: "rj45GraphPage",
RJ45: "rj45ARPage",
CHARGER_GRAPH: "chargerGraphPage",
CHARGER: "chargerARPage"
},
COMMON_AR_PAGES: [
"rj45GraphPage",
"chargerGraphPage"
],
/**
* Available models.
* @readonly
* @enum {string}
*/
MODELS: {
ROUTER_ITEM: "Router_Item",
RJ45_ITEM: "RJ_45_Item",
ROUTER: "Router",
RJ45: "RJ_45",
CHARGER_ITEM: "Charger_Item",
CHARGER: "Charger",
},
/**
* Available sounds.
* @readonly
* @enum {Sound}
*/
SOUNDS: {
LOCATE: ARgoDefaultSound,
INSTRUCTION: ARgoDefaultSound,
BOX_HELP: ARgoDefaultSound,
BOX: ARgoDefaultSound,
ROUTER: ARgoDefaultSound,
ROUTER_HELP: ARgoDefaultSound,
ROUTER_GRAPH: ARgoDefaultSound,
ROUTER_BACK: ARgoDefaultSound,
RJ45_GRAPH: ARgoDefaultSound,
CABLE_VIEW: ARgoDefaultSound,
AR_OPTION: ARgoDefaultSound,
POWER_GRAPH: ARgoDefaultSound,
},
/**
* Text of available 3D labels.
* @readonly
* @enum {String}
*/
LABELS: {
ROUTER_PORTS: "\n ΘΥΡΕΣ ΣΥΝΔΕΣΗΣ ΚΑΛΩΔΙΩΝ \n\n Ακολουθούν οδηγίες για βήμα προς βήμα \n σύνδεση των καλωδίων του εξοπλισμού σας, \n στις θύρες του ρούτερ \n\n ΠΑΤΗΣΤΕ ΓΙΑ ΣΥΝΕΧΕΙΑ \n"
},
/**
* Available targets by tracker.
* @readonly
* @enum {object}
*/
TARGETS: {
BOX: {
},
/**
* Available targets in the router tracker.
* @readonly
* @enum {String}
*/
ROUTER: {
FRONT: "1576242680793R",
BACK: "1576242870970R",
BACK_2: "1576242870970",
BACK_3: "IMG_E0980",
BACK_4: "IMG_E0981",
BACK_5: "IMG_E0985",
BACK_6: "IMG_E0986",
BACK_7: "IMG_E0987"
}
},
/**
* Available 3D model parts.
* @readonly
* @enum {String}
*/
PARTS: {
M: "m",
ETHERNET: "Ethernet",
DSL: "DSL",
POWER: "Power"
},
/**
* Gets or sets the currently loaded page.
* @type {string}
* @see ARgo.PAGES
*/
get CURRENT_PAGE() {
return this.currentPage;
},
set CURRENT_PAGE(page) {
if (this.currentPage === page) {
return;
}
let isCommonAR = $.inArray(page, this.COMMON_AR_PAGES) > -1;
this.reset(isCommonAR);
switch (page) {
case this.PAGES.MAIN:
this.enableCamera();
World.createSounds(page);
World.currentTracker = World.boxTracker;
break;
case this.PAGES.ROUTER:
this.enableCamera();
World.createSounds(page);
World.currentTracker = World.routerTracker;
World.modelBlock.enabled = true;
break;
case this.PAGES.RJ45:
this.enableCamera();
World.createSounds(page);
World.currentTracker = World.routerTracker;
World.routerImageTrackable.drawables.addCamDrawable(World.modelRJ45);
World.modelRJ45.enabled = true;
break;
case this.PAGES.CHARGER:
this.enableCamera();
World.createSounds(page);
World.currentTracker = World.routerTracker;
World.routerImageTrackable.drawables.addCamDrawable(World.modelCharger);
World.modelCharger.enabled = true;
break;
case this.PAGES.CONTENTS:
break;
case this.PAGES.CONTENTS_ITEM:
World.defaultPositionable.enable();
$(document.body).css("background", "none");
World.defaultPositionable.showModel(this.MODELS.ROUTER_ITEM, true);
break;
case this.PAGES.ROUTER_GRAPH:
World.defaultPositionable.enable();
World.createSounds(page);
$(document.body).css("background", "none");
if (World.defaultPositionable.hasLoadedModel(this.MODELS.ROUTER)) {
let routerModel = World.defaultPositionable.getLoadedModel(this.MODELS.ROUTER);
if (isCommonAR) {
// We return from Presentation Mode.
World.defaultPositionable.closeModel(routerModel.presentedModel);
if (routerModel.isOnPresentation) {
// Just exit presentation mode (will play
// restore animation).
routerModel.isOnPresentation = false;
break;
}
}
routerModel.show(true);
} else {
World.defaultPositionable.showModel(this.MODELS.ROUTER, true);
}
break;
case this.PAGES.RJ45_GRAPH:
World.createSounds(page);
break;
case this.PAGES.CHARGER_GRAPH:
World.createSounds(page);
break;
default:
break;
}
this.loadPageThunk(page);
},
/**
* Gets if a valid page with the specified id, is available in the UI.
* @param {string} pageId
* HTML `id` of the page to seek for.
* @returns {boolean}
*/
hasPage(pageId) {
for (const page in this.PAGES) {
if (this.PAGES[page] === pageId) {
return true;
}
}
return false;
},
/**
* Called from HTML to proceed to a page based on a radio input choice.
* @param {String} callingPage
* The calling page containing the radio inputs.
* The radio inputs must be part of a `form` with an `id`: "_pageId**Form**_"
* and they should share a `name` as: "_pageIdForm**Choice**_"
* @see ARgo.PAGES
*/
gotoPage(callingPage) {
const form = callingPage + "Form";
const choiceSelector = `input[name='${form}Choice']:checked`;
let value = null;
if ($(form)) {
value = $(choiceSelector).val();
}
switch (callingPage) {
case this.PAGES.SCENARIO:
switch (value) {
case "1":
this.CURRENT_PAGE = this.PAGES.BOX_SCENARIO;
break;
case "2":
this.CURRENT_PAGE = this.PAGES.ROUTER_SCENARIO;
break;
case "3":
this.CURRENT_PAGE = this.PAGES.RJ45;
break;
default:
break;
}
break;
case this.PAGES.BOX_SCENARIO:
switch (value) {
case "1":
this.CURRENT_PAGE = this.PAGES.MAIN;
break;
case "2":
this.CURRENT_PAGE = this.PAGES.CONTENTS;
break;
default:
break;
}
break;
case this.PAGES.ROUTER_SCENARIO:
switch (value) {
case "1":
this.CURRENT_PAGE = this.PAGES.ROUTER;
break;
case "2":
this.CURRENT_PAGE = this.PAGES.ROUTER_GRAPH;
break;
default:
break;
}
break;
default:
break;
}
},
/**
* Called from HTML to move to a page.
* @param {PAGES} page
* The page to be loaded.
* @param {boolean} [preventARReset]
* Prevent reseting the AR environment.
* @see ARgo.PAGES
*/
loadPage(page, preventARReset) {
if (preventARReset) {
this.COMMON_AR_PAGES.push(page);
}
this.CURRENT_PAGE = page;
if (preventARReset) {
this.COMMON_AR_PAGES.pop();
}
},
/**
* @private
* @param {string} page
*/
loadPageThunk(page) {
$.mobile.changePage($("#" + page));
this.previousPage = this.currentPage;
this.currentPage = page;
},
/**
* Prepares the environment for camera usage.
*/
enableCamera() {
AR.hardware.camera.enabled = true;
$(document.body).css("background", "none");
},
/**
* Disables the device's camera.
*/
disableCamera() {
AR.hardware.camera.enabled = false;
},
/**
* Resets the AR environment to default state.
* @param {boolean} [isCommonAR]
* Indicates if the reset should retain the state of all
* Wikitude context (trackers, models, camera state etc).
*/
reset(isCommonAR) {
World.stopSounds();
ARgo.hideDialogs();
World.destroySounds();
if (isCommonAR) {
return;
}
if (World.defaultPositionable) {
World.defaultPositionable.closeAll();
World.defaultPositionable.disable();
}
World.currentTracker = null;
ARgo.disableCamera();
$(document.body).css("background", "lightgrey");
if (World.routerImageTrackable) {
World.routerImageTrackable.drawables.removeCamDrawable(World.modelRJ45);
World.routerImageTrackable.drawables.removeCamDrawable(World.modelCharger);
World.modelRJ45.enabled = false;
World.modelCharger.enabled = false;
World.modelBlock.enabled = false;
}
},
/**
* Resets and refreshes the content of pages.
*/
resetPages() {
if (this.introSwiper) {
this.introSwiper.slideTo(0);
}
if (this.routerSwiper) {
this.routerSwiper.slideTo(0);
}
if (this.contentsSwiper) {
this.contentsSwiper.slideTo(0);
}
},
/**
* Resets state of previously hidden dialogs.
*/
resetDialogs() {
this.contentsDialogShown = false;
this.snappingDialogShown = false;
},
/**
* Shows the dialog associated with the specified page.
* Its id should be of the form: "_pageId**Dialog**_"
* @param {String} dialogSelector The selector of the page (e.g. `#myPage`)
*/
showDialog(dialogSelector) {
$.mobile.changePage(`#${dialogSelector}Dialog`, { role: "dialog" });
},
/**
* Hides all dialogs currently visible.
*/
hideDialogs() {
$(".ui-dialog").dialog("close");
},
/**
* Hides elements with the `.popupText` CSS class.
*/
hidePopup() {
$(".popupText").hide();
},
/**
* Shows the element(s) with the `.popupText` CSS class
* available in the currently loaded page.
* @param {string} [id]
*/
showPopup(id) {
var selector = `#${ARgo.CURRENT_PAGE} > .popupText`;
if (id) {
selector = `${selector}.${id}`;
}
$(selector).show();
},
/**
* Hides info bar, footer buttons and any dialogs currently open.
*/
hideInfoBar() {
$(".info").hide();
$(".footerButtons").hide();
ARgo.hideDialogs();
},
/**
* Hides the loading message and shows info bar and footer buttons.
*/
showInfoBar() {
$(".info").show();
$(".footerButtons").show();
$("#loadingMessage").hide();
},
/**
* Occurs in response to the **_`pagechange`_** event.
* @param {String} pageSelector
* The selector of the page we've just moved to (e.g. `#myPage`).
* @listens Document.pagechange
*/
onPageChanged(pageSelector) {
const page = pageSelector.replace("#", "");
this.resetPages();
switch (page) {
case this.PAGES.BOX_SCENARIO:
if (!this.introSwiper) {
// Initialize swiper when the page is loaded.
this.introSwiper = new Swiper(".swiper-intro-container", {
pagination: {
el: ".swiper-pagination"
},
on: {
slideChange: function () {
// @ts-ignore
if (this.isEnd) {
$("#introButton").removeClass("ui-disabled");
} else {
$("#introButton").addClass("ui-disabled");
}
}
}
});
}
break;
case this.PAGES.ROUTER_SCENARIO:
if (!this.routerSwiper) {
// Initialize swiper when the page is loaded.
this.routerSwiper = new Swiper(".swiper-router-intro-container", {
pagination: {
el: ".swiper-pagination"
},
on: {
slideChange: function () {
// @ts-ignore
if (this.isEnd) {
$("#routerIntroButton").removeClass("ui-disabled");
} else {
$("#routerIntroButton").addClass("ui-disabled");
}
}
}
});
}
break;
case this.PAGES.MAIN:
if (World.targetRecognized) {
break;
}
World.playSound(ARgo.SOUNDS.BOX_HELP);
this.showDialog(page);
break;
case this.PAGES.ROUTER:
this.snappingDialogShown = false;
if (World.targetRecognized) {
break;
}
World.playSound(ARgo.SOUNDS.ROUTER_HELP);
this.showDialog(page);
break;
case this.PAGES.ROUTER_GRAPH:
// Sound and dialog at the end of snapping animation.
break;
case this.PAGES.RJ45_GRAPH:
World.playSound(
ARgo.SOUNDS.RJ45_GRAPH,
[ARgo.SOUNDS.CABLE_VIEW, ARgo.SOUNDS.AR_OPTION],
() => World.defaultPositionable.getLoadedModel(this.MODELS.ROUTER).isOnPresentation);
ARgo.showDialog(page);
ARgo.showInfoBar();
break;
case this.PAGES.CHARGER_GRAPH:
World.playSound(
ARgo.SOUNDS.POWER_GRAPH,
[ARgo.SOUNDS.CABLE_VIEW, ARgo.SOUNDS.AR_OPTION],
() => World.defaultPositionable.getLoadedModel(this.MODELS.ROUTER).isOnPresentation);
ARgo.showDialog(page);
ARgo.showInfoBar();
break;
case this.PAGES.RJ45:
case this.PAGES.CHARGER:
World.playSound(ARgo.SOUNDS.ROUTER_BACK);
ARgo.showDialog(page);
break;
case this.PAGES.CONTENTS:
if (!this.contentsSwiper) {
// Initialize swiper when the page is loaded.
this.contentsSwiper = new Swiper(".swiper-container", {
pagination: {
el: ".swiper-pagination"
}
});
}
if (this.contentsDialogShown) {
break;
}
this.contentsDialogShown = true;
this.showDialog(page);
break;
case this.PAGES.CONTENTS_ITEM:
// Sound and dialog at the end of snapping animation.
break;
case this.PAGES.SCENARIO:
case this.PAGES.WELCOME:
case this.PAGES.INSTRUCTIONS_1:
this.resetDialogs();
break;
default:
break;
}
},
/**
* Called by the native hosting Activity on Android devices.
*/
onAndroidGoBack() {
// No attribute at all means we should not move.
// This is the case with dialogs, so we just stop
// and close all context.
if (!$.mobile.activePage.is("[data-argo-back]")) {
World.stopSounds();
this.hideDialogs();
return;
}
const backPage = $.mobile.activePage.attr("data-argo-back");
if (this.hasPage(backPage)) {
this.CURRENT_PAGE = backPage;
} else {
// Can be just an empty attribute,
// indicating we should exit ARgo.
this.close();
}
},
/**
* Sends a predefined command to the native host.
* @param {string} command
* @param {string} [message]
* @param {Object} [data]
* @see ARgo.NATIVE_COMMANDS
*/
sendCommand(command, message, data) {
AR.platform.sendJSONObject(new ARgoNativeCommand(command, message, data));
},
/**
* Log a message to the console and the
* app's online analytics.
* @param {string} message
* @param {Object} [data]
* @see ARgo.LOG_MESSAGES
*/
log(message, data) {
message = `[ARgo]: ${message}`;
console.log(message);
this.sendCommand(ARgo.NATIVE_COMMANDS.LOG, message, data);
},
/**
* Asks the native host to close this view and return to main app.
*/
close() {
this.log("Application Exit");
// TODO: Show dialog when needed.
this.sendCommand(ARgo.NATIVE_COMMANDS.BACK);
}
};
// #endregion
/**
* @class
* @classdesc Includes the application's AR logic.
* @hideconstructor
*/
const World = {
// #region Properties
/**
* Indicates if a visual target is currently
* recognized by a tracker.
* @type {boolean}
*/
targetRecognized: false,
/**
* The application's loaded drawables.
* @type {Drawable[]}
*/
drawables: [],
/**
* The application's loaded trackers.
* @type {BaseTracker[]}
*/
trackers: [],
/**
* The application's loaded sounds.
* @type {Sound[]}
*/
sounds: [],
/**
* Gets or sets the currently active tracker.
* Can be set to `null` to deactivate all trackers.
* @type {BaseTracker}
*/
get currentTracker() {
for (let i = 0; i < World.trackers.length; i++) {
if (World.trackers[i].enabled) {
return World.trackers[i];
}
}
return null;
},
set currentTracker(value) {
for (const tracker of World.trackers) {
const state = (tracker === value);
if (tracker.enabled === state) {
continue;
}
tracker.enabled = state;
World.onTargetLost();
}
},
/**
* The 3D model of the router in AR.
* @type {ARgoModel}
*/
modelRouterAR: null,
/**
* The 3D model of the RJ45 cable.
* @type {ARgoModel}
*/
modelRJ45: null,
/**
* The 3D model of the transformer plug.
* @type {ARgoModel}
*/
modelCharger: null,
/**
* The 3D model of the ports' frame.
* @type {ARgoModel}
*/
modelBlock: null,
/**
* The 3D model of a marker.
* @type {ARgoModel}
*/
modelMarker: null,
/**
* The 3D model of an arrow.
* @type {ARgoModel}
*/
modelArrow: null,
/**
* The trackable of the router box.
* @type {ImageTrackable}
*/
boxImageTrackable: null,
/**
* The trackable of the router.
* @type {ImageTrackable}
*/
routerImageTrackable: null,
/**
* The router's ports.
* @type {Label}
*/
portsLabel: null,
/**
* @type {ARgoPositionable}
*/
defaultPositionable: null,
// #endregion
// #region Methods
/**
* Initializes the `World` singleton.
*/
init() {
AR.hardware.camera.enabled = false;
if (this.fixTheSourceOfAllEvil()) {
return;
}
$(document).on("pagechange", function (event, args) {
ARgo.onPageChanged(args.toPage.selector);
});
this.createSounds();
this.createOverlays();
this.createPositionableOverlays();
ARgo.sendCommand(ARgo.NATIVE_COMMANDS.INIT);
},
/**
* Speaks for itself!
* @returns {boolean}
*/
fixTheSourceOfAllEvil() {
var path = window.location.pathname;
var htmlPage = path.split("/").pop();
console.log(htmlPage);
// On certain devices and for an unknown reason, the initial
// href is missing the html page part. Without it, jQuery dialogs
// misbehave and cause unintended history navigations!
if (!htmlPage) {
window.location.href = `${window.location.pathname}index.html`;
return true;
}
return false;
},
/**
* Creates all AR overlays (models, trackers, trackables and animations).
*/
createOverlays() {
var scale = 0.045;
var animationScale = scale + (scale * 0.10);
// Router model shown in AR.
this.modelRouterAR = new ARgoModel({
source: "assets/router.wt3",
onLoaded: World.getObjectLoadedHandler(ARgo.LOG_MESSAGES.LOAD_MODEL_AR_ROUTER),
onError: World.onError,
defaultLayout: {
scale: 0.045
},
onClick: function () {
ARgo.CURRENT_PAGE = ARgo.PAGES.CONTENTS;
}
});
World.drawables.push(this.modelRouterAR);
this.modelRJ45 = new ARgoModel({
source: "assets/RJ45_6.wt3",
onLoaded: World.getObjectLoadedHandler(ARgo.LOG_MESSAGES.LOAD_MODEL_AR_RJ45),
onError: World.onError,
defaultLayout: {
scale: 0.055,
translate: {
x: 0.07,
y: -0.24,
z: 0.07
},
rotate: {
z: 180
}
},
enabled: false
});
World.drawables.push(this.modelRJ45);
World.addARCableConnectionAnimation(this.modelRJ45);
this.modelCharger = new ARgoModel({
source: "assets/charger.wt3",
onLoaded: World.getObjectLoadedHandler(ARgo.LOG_MESSAGES.LOAD_MODEL_AR_CHARGER),
onError: World.onError,
defaultLayout: {
scale: 0.055,
translate: {
x: -0.4,
y: -0.23,
z: 0.05
},
rotate: {
z: 180
}
},
enabled: false
});
World.drawables.push(this.modelCharger);
World.addARCableConnectionAnimation(this.modelCharger);
// Shown over router in AR.
this.modelMarker = new ARgoModel({
source: "assets/marker.wt3",
onLoaded: World.getObjectLoadedHandler(ARgo.LOG_MESSAGES.LOAD_MODEL_MARKER),
onError: World.onError,
defaultLayout: {
scale: 0.02,
translate: {
z: 0.2
}
}
});
World.drawables.push(this.modelMarker);
World.addFlashingAnimation(this.modelMarker);
// Shown over router in AR.
this.modelArrow = new ARgoModel({
source: "assets/arrow.wt3",
onLoaded: World.getObjectLoadedHandler(ARgo.LOG_MESSAGES.LOAD_MODEL_ARROW),
onError: World.onError,
defaultLayout: {
scale: 0.045,
translate: {
x: 0.7,
z: 0.2
},
rotate: {
z: 90
}
}
});
World.drawables.push(this.modelArrow);
World.addFlashingAnimation(this.modelArrow);
// Shown over router ports in AR.
this.modelBlock = new ARgoModel({
source: "assets/block5.wt3",
onLoaded: World.getObjectLoadedHandler(ARgo.LOG_MESSAGES.LOAD_MODEL_BLOCK),
onError: World.onError,
defaultLayout: {
scale: 1.65,
translate: {
x: -0.44,
y: 0.15
},
rotate: {
z: 270
}
},
onClick: function () {
ARgo.CURRENT_PAGE = ARgo.PAGES.ROUTER_GRAPH;
}
});
World.drawables.push(this.modelBlock);
this.portsLabel = new AR.Label(ARgo.LABELS.ROUTER_PORTS, 0.8, {
translate: {
x: 1
},
onError: World.onError,
onClick: function () {
ARgo.CURRENT_PAGE = ARgo.PAGES.ROUTER_GRAPH;
},
style: {
backgroundColor: "#16A085",
textColor: "#FFFFFF",
fontStyle: AR.CONST.FONT_STYLE.BOLD
},
verticalAnchor: AR.CONST.VERTICAL_ANCHOR.BOTTOM,
opacity: 0.8
});
this.appearingAnimation = this.createARAppearingAnimation(this.modelRouterAR, animationScale);
this.routerTargetCollectionResource = new AR.TargetCollectionResource("assets/tracker_router.wtc", {
onError: World.onError
});
this.routerTracker = new AR.ImageTracker(this.routerTargetCollectionResource, {
onTargetsLoaded: World.getObjectLoadedHandler(ARgo.LOG_MESSAGES.LOAD_TRACKER_ROUTER),
onError: World.onError,
extendedRangeRecognition: 1,
enabled: false
});
World.trackers.push(this.routerTracker);
this.routerImageTrackable = new AR.ImageTrackable(this.routerTracker, "*", {
onLoaded: World.getObjectLoadedHandler(ARgo.LOG_MESSAGES.LOAD_TRACKABLE_ROUTER),
onImageRecognized: World.onTargetRecognized,
onImageLost: World.onTargetLost,
onError: World.onError
});
this.boxTargetCollectionResource = new AR.TargetCollectionResource("assets/tracker_box.wtc", {
onError: World.onError
});
this.boxTracker = new AR.ImageTracker(this.boxTargetCollectionResource, {
onTargetsLoaded: World.getObjectLoadedHandler(ARgo.LOG_MESSAGES.LOAD_TRACKER_BOX),
onError: World.onError
});
World.trackers.push(this.boxTracker);
this.boxImageTrackable = new AR.ImageTrackable(this.boxTracker, "*", {
onLoaded: World.getObjectLoadedHandler(ARgo.LOG_MESSAGES.LOAD_TRACKABLE_BOX),
drawables: {
cam: [this.modelRouterAR]
},
onImageRecognized: World.onTargetRecognized,
onImageLost: World.onTargetLost,
onError: World.onError
});
},
/**
* Add our models' specifications to the default
* Positionable managed by the native plugin.
*/
createPositionableOverlays() {
World.defaultPositionable = new ARgoPositionable("argo-positionable", {
onLoaded: World.getObjectLoadedHandler(ARgo.LOG_MESSAGES.LOAD_POSITIONABLE),
showBackground: true
});
World.defaultPositionable.addModel(ARgo.MODELS.ROUTER_ITEM, {
source: `${ARgo.assetsFolder}router_2.wt3`,
onLoaded: World.getObjectLoadedHandler(ARgo.LOG_MESSAGES.LOAD_MODEL_ROUTER),
onError: World.onError,
defaultLayout: {
scale: 0.8,
rotate: {
x: -10,
z: 180
}
},
rootLayout: {
rotate: {
x: -10,
y: -90,
z: 180
}
},
onCreateAppearingAnimation: World.createAppearingAnimation,
onAppearingComplete: World.onModelAppearingComplete,
allowManipulation: true,
canRotate: World.canRotateSnappedModel,
manipulationData: new ARgoObjectManipulationData(180),
enabled: false
});
World.defaultPositionable.addModel(ARgo.MODELS.ROUTER, {
source: `${ARgo.assetsFolder}router_2.wt3`,
onLoaded: World.getObjectLoadedHandler(ARgo.LOG_MESSAGES.LOAD_MODEL_ROUTER),
onError: World.onError,
defaultLayout: {
scale: 0.8,
rotate: {
x: -10,
z: 180
}
},
rootLayout: {
rotate: {
x: -10,
y: -90,
z: 180
}
},
presentationLayout: {
scale: 1.5,
rotate: {
x: -10,
y: -20,
z: 180
}
},
onCreateAppearingAnimation: World.createAppearingAnimation,
onCreatePresentationAnimation: World.adjustPresentationTransform,
onAppearingComplete: World.onModelAppearingComplete,
allowManipulation: true,
manipulationData: new ARgoObjectManipulationData(180),
canRotate: World.canRotateSnappedModel,
onClick: World.onModelClick,
enabled: false
});
World.defaultPositionable.addModel(ARgo.MODELS.RJ45, {
source: `${ARgo.assetsFolder}RJ45_6.wt3`,
onLoaded: World.getObjectLoadedHandler(ARgo.LOG_MESSAGES.LOAD_MODEL_RJ45),
onError: World.onError,
defaultLayout: {
scale: 1.2,
translate: {
x: 3.2,
y: -7.5,
z: 1.2
},
rotate: {
x: -10,
y: -20,
z: 180
}
},
onCreateAppearingAnimation: World.createCableConnectionAnimation,
enabled: false
});
World.defaultPositionable.addModel(ARgo.MODELS.CHARGER, {
source: `${ARgo.assetsFolder}charger.wt3`,
onLoaded: World.getObjectLoadedHandler(ARgo.LOG_MESSAGES.LOAD_MODEL_CHARGER),
onError: World.onError,
defaultLayout: {
scale: 3,
translate: {
x: 2,
y: -8.2,
z: 4
},
rotate: {
x: -10,
y: -20,
z: 180
}
},
onCreateAppearingAnimation: World.createCableConnectionAnimation,
enabled: false
});
},
createObjectTrackable() {
this.rj45targetCollectionResource = new AR.TargetCollectionResource("assets/tracker.wto", {
onError: World.onError
});
this.rj45tracker = new AR.ObjectTracker(this.rj45targetCollectionResource, {
onTargetsLoaded: ARgo.showInfoBar,
onError: World.onError
});
World.trackers.push(this.rj45tracker);
this.rj45objectTrackable = new AR.ObjectTrackable(this.rj45tracker, "*", {
// @ts-ignore
// Signature for ImageTarget
onObjectRecognized: World.onTargetRecognized,
// @ts-ignore
// Signature for ImageTarget
onObjectLost: World.onTargetLost,
onError: World.onError
});
},
/**
* Creates the first appearance animation for a 3D model.
* @param {Model} model The model to animate.
* @param {number} scale The maximum scale of the animation.
* @returns {ARgoAnimationGroup}
* An instance of `ARgoAnimationGroup`
*/
createARAppearingAnimation(model, scale) {
var sx = new AR.PropertyAnimation(model, "scale.x", 0, scale, 1500, {
type: AR.CONST.EASING_CURVE_TYPE.EASE_OUT_ELASTIC
});
var sy = new AR.PropertyAnimation(model, "scale.y", 0, scale, 1500, {
type: AR.CONST.EASING_CURVE_TYPE.EASE_OUT_ELASTIC
});
var sz = new AR.PropertyAnimation(model, "scale.z", 0, scale, 1500, {
type: AR.CONST.EASING_CURVE_TYPE.EASE_OUT_ELASTIC
});
return new ARgoAnimationGroup(AR.CONST.ANIMATION_GROUP_TYPE.PARALLEL, [sx, sy, sz]);
},
/**
* Creates and starts a flashing animation for a 3D model.
* @param {Model} model
*/
addFlashingAnimation(model) {
var scaleS = 0.02;
var scaleL = 0.03;
var scaleDuration = 2000;
/* X animations */
var buttonScaleAnimationXOut = new AR.PropertyAnimation(model, "scale.x", scaleS, scaleL, scaleDuration / 2, {
type: AR.CONST.EASING_CURVE_TYPE.EASE_IN_OUT_SINE
});
var buttonScaleAnimationXIn = new AR.PropertyAnimation(model, "scale.x", scaleL, scaleS, scaleDuration / 2, {
type: AR.CONST.EASING_CURVE_TYPE.EASE_IN_OUT_SINE
});
this.buttonScaleAnimationX = new AR.AnimationGroup(
AR.CONST.ANIMATION_GROUP_TYPE.SEQUENTIAL, [buttonScaleAnimationXOut, buttonScaleAnimationXIn]);
/* Y animations */
var buttonScaleAnimationYOut = new AR.PropertyAnimation(model, "scale.y", scaleS, scaleL, scaleDuration / 2, {
type: AR.CONST.EASING_CURVE_TYPE.EASE_IN_OUT_SINE
});
var buttonScaleAnimationYIn = new AR.PropertyAnimation(model, "scale.y", scaleL, scaleS, scaleDuration / 2, {
type: AR.CONST.EASING_CURVE_TYPE.EASE_IN_OUT_SINE
});
this.buttonScaleAnimationY = new AR.AnimationGroup(
AR.CONST.ANIMATION_GROUP_TYPE.SEQUENTIAL, [buttonScaleAnimationYOut, buttonScaleAnimationYIn]);
/* Z animations */
var buttonScaleAnimationZOut = new AR.PropertyAnimation(model, "scale.z", scaleS, scaleL, scaleDuration / 2, {
type: AR.CONST.EASING_CURVE_TYPE.EASE_IN_OUT_SINE
});
var buttonScaleAnimationZIn = new AR.PropertyAnimation(model, "scale.z", scaleL, scaleS, scaleDuration / 2, {
type: AR.CONST.EASING_CURVE_TYPE.EASE_IN_OUT_SINE
});
this.buttonScaleAnimationZ = new AR.AnimationGroup(
AR.CONST.ANIMATION_GROUP_TYPE.SEQUENTIAL, [buttonScaleAnimationZOut, buttonScaleAnimationZIn]);
/* Start all animation groups. */
this.buttonScaleAnimationX.start(-1);
this.buttonScaleAnimationY.start(-1);
this.buttonScaleAnimationZ.start(-1);
},
/**
* Creates a connection animation for a model in 3D Presentation Mode.
* @param {ARgoModel} model
* @returns {ARgoAnimationGroup}
*/
createCableConnectionAnimation(model) {
var translateZS = model.translate.z;
var translateZL = translateZS + 12;
var translateXS = model.translate.x;
var translateXL = translateXS + 2;
var translateDuration = 5000;
/* Z animations */
var cableConnectionAnimationXOut = new AR.PropertyAnimation(model, "translate.x", translateXS, translateXL, translateDuration / 2, {
type: AR.CONST.EASING_CURVE_TYPE.EASE_IN_OUT_SINE
});
var cableConnectionAnimationXIn = new AR.PropertyAnimation(model, "translate.x", translateXL, translateXS, translateDuration / 2, {
type: AR.CONST.EASING_CURVE_TYPE.EASE_IN_OUT_SINE
});
var cableConnectionAnimationX = new AR.AnimationGroup(
AR.CONST.ANIMATION_GROUP_TYPE.SEQUENTIAL, [cableConnectionAnimationXOut, cableConnectionAnimationXIn]);
/* Z animations */
var cableConnectionAnimationZOut = new AR.PropertyAnimation(model, "translate.z", translateZS, translateZL, translateDuration / 2, {
type: AR.CONST.EASING_CURVE_TYPE.EASE_IN_OUT_SINE
});
var cableConnectionAnimationZIn = new AR.PropertyAnimation(model, "translate.z", translateZL, translateZS, translateDuration / 2, {
type: AR.CONST.EASING_CURVE_TYPE.EASE_IN_OUT_SINE
});
var cableConnectionAnimationZ = new AR.AnimationGroup(
AR.CONST.ANIMATION_GROUP_TYPE.SEQUENTIAL, [cableConnectionAnimationZOut, cableConnectionAnimationZIn]);
return new ARgoAnimationGroup(AR.CONST.ANIMATION_GROUP_TYPE.PARALLEL, [cableConnectionAnimationX, cableConnectionAnimationZ], {
loopTimes: -1
});
},
/**
* Creates and starts a connection animation for a model in AR Presentation Mode.
* @param {ARgoModel} model
*/
addARCableConnectionAnimation(model) {
var translateZS = model.translate.z;
var translateZL = translateZS + 1.5;
var translateDuration = 7000;
/* Z animations */
var cableConnectionAnimationZOut = new AR.PropertyAnimation(model, "translate.z", translateZS, translateZL, translateDuration / 2, {
type: AR.CONST.EASING_CURVE_TYPE.EASE_IN_OUT_SINE
});
var cableConnectionAnimationZIn = new AR.PropertyAnimation(model, "translate.z", translateZL, translateZS, translateDuration / 2, {
type: AR.CONST.EASING_CURVE_TYPE.EASE_IN_OUT_SINE
});
var cableConnectionAnimationZ = new ARgoAnimationGroup(
AR.CONST.ANIMATION_GROUP_TYPE.SEQUENTIAL, [cableConnectionAnimationZOut, cableConnectionAnimationZIn], { loopTimes: -1 });
model.appearanceAnimation = cableConnectionAnimationZ;
/* Start all animation groups. */
model.appearanceAnimation.start();
},
/**
* Adjusts the `presentationLayout` according to the Presentation Mode step.
* @param {ARgoModel} model
* @returns {ARgoAnimationGroup}
*/
adjustPresentationTransform(model) {
switch (ARgo.CURRENT_PAGE) {
case ARgo.PAGES.CHARGER_GRAPH:
model.presentationLayout.translate = ARgoModel.getSafeLayout({
translate: {
x: 10
}
}).translate;
break;
default:
model.presentationLayout.translate = ARgoModel.getSafeLayout({
translate: {}
}).translate
break;
}
return null;
},
/**
* Creates an appearing animation for a positionable 3D model.
* @param {ARgoModel} model The model to animate.
* @returns {ARgoAnimationGroup}
* An instance of `ARgoAnimationGroup`
*/
createAppearingAnimation(model) {
var beginScale = model.rootLayout.scale;
var scale = model.defaultLayout.scale;
var beginRotateY = model.rootLayout.rotate.y;
var rotateY = model.defaultLayout.rotate.y;
var duration = 2000;
/* X animations */
var routerScaleAnimationX = new AR.PropertyAnimation(model, "scale.x", beginScale, scale, duration, {
type: AR.CONST.EASING_CURVE_TYPE.EASE_IN_OUT_SINE
});
/* Y animations */
var routerScaleAnimationY = new AR.PropertyAnimation(model, "scale.y", beginScale, scale, duration, {
type: AR.CONST.EASING_CURVE_TYPE.EASE_IN_OUT_SINE
});
/* Z animations */
var routerScaleAnimationZ = new AR.PropertyAnimation(model, "scale.z", beginScale, scale, duration, {
type: AR.CONST.EASING_CURVE_TYPE.EASE_IN_OUT_SINE
});
/* Y animations */
var routerRotateAnimationY = new AR.PropertyAnimation(model, "rotate.y", beginRotateY, rotateY, duration, {
type: AR.CONST.EASING_CURVE_TYPE.EASE_IN_OUT_SINE
});
return new ARgoAnimationGroup(AR.CONST.ANIMATION_GROUP_TYPE.PARALLEL, [routerScaleAnimationX, routerScaleAnimationY, routerScaleAnimationZ, routerRotateAnimationY]);
},
/**
*
* @param {ARgoModel} model
*/
onModelAppearingComplete(model) {
if (ARgo.snappingDialogShown) {
return;
}
switch (ARgo.CURRENT_PAGE) {
case ARgo.PAGES.CONTENTS_ITEM:
ARgo.showDialog(ARgo.PAGES.CONTENTS_ITEM);
break;
case ARgo.PAGES.ROUTER_GRAPH:
World.playSound(ARgo.SOUNDS.ROUTER_GRAPH);
ARgo.showDialog(ARgo.PAGES.ROUTER_GRAPH);
break;
}
ARgo.snappingDialogShown = true;
},
/**
* Stops and destroys an animation assigned to a model.
* @param {ARgoAnimationGroup} animation
*/
destroyAnimation(animation) {
if (animation) {
if (animation.isRunning()) {
animation.stop();
}
animation.destroy();
}
},
/**
* Unloads and destroys all currently loaded sounds.
*/
destroySounds() {
for (const sound of World.sounds) {
sound.destroy();
}
World.sounds.length = 0;
for (const soundEnumItem in ARgo.SOUNDS) {
switch (soundEnumItem) {
case "INSTRUCTION":
case "LOCATE":
break;
default:
ARgo.SOUNDS[soundEnumItem] = ARgoDefaultSound;
break;
}
}
},
/**
* Creates and loads the application's sounds.
* These are accessed by `ARgo.SOUNDS` and listed in `World.sounds`.
* @param {string} [page]
* The sounds to load based on the specified app stage.
* @see ARgo.SOUNDS
* @see World.sounds
*/
createSounds(page) {
if (!page) {
ARgo.SOUNDS.LOCATE = this.createSound("assets/locate.wav", false);
ARgo.SOUNDS.INSTRUCTION = this.createSound("assets/instruction.wav", false);
return;
}
switch (page) {
case ARgo.PAGES.MAIN:
ARgo.SOUNDS.BOX = this.createSound("assets/box.mp3");
ARgo.SOUNDS.BOX_HELP = this.createSound("assets/boxHelp.mp3");
break;
case ARgo.PAGES.ROUTER:
ARgo.SOUNDS.ROUTER = this.createSound("assets/router.mp3");
ARgo.SOUNDS.ROUTER_HELP = this.createSound("assets/routerHelp.mp3");
break;
case ARgo.PAGES.ROUTER_GRAPH:
ARgo.SOUNDS.ROUTER_GRAPH = this.createSound("assets/router3D.mp3");
break;
case ARgo.PAGES.RJ45_GRAPH:
ARgo.SOUNDS.RJ45_GRAPH = this.createSound("assets/rj45.mp3");
ARgo.SOUNDS.CABLE_VIEW = this.createSound("assets/cableView.mp3");
ARgo.SOUNDS.AR_OPTION = this.createSound("assets/arOption.mp3");
break;
case ARgo.PAGES.CHARGER_GRAPH:
ARgo.SOUNDS.POWER_GRAPH = this.createSound("assets/power.mp3");
ARgo.SOUNDS.CABLE_VIEW = this.createSound("assets/cableView.mp3");
ARgo.SOUNDS.AR_OPTION = this.createSound("assets/arOption.mp3");
break;
case ARgo.PAGES.RJ45:
ARgo.SOUNDS.ROUTER_BACK = this.createSound("assets/routerBack.mp3");
ARgo.SOUNDS.RJ45_GRAPH = this.createSound("assets/rj45.mp3");
ARgo.SOUNDS.CABLE_VIEW = this.createSound("assets/cableView.mp3");
break;
case ARgo.PAGES.CHARGER:
ARgo.SOUNDS.ROUTER_BACK = this.createSound("assets/routerBack.mp3");
ARgo.SOUNDS.POWER_GRAPH = this.createSound("assets/power.mp3");
ARgo.SOUNDS.CABLE_VIEW = this.createSound("assets/cableView.mp3");
break;
default:
break;
}
},
/**
* Creates and loads an `AR.Sound`.
* @param {string} uri
* The path to the sound file.
* @param {boolean} [cache]
* Whether to cache the sound to `World.sounds`.
* For constantly loaded sounds this should be `false`.
* @returns {Sound}
*/
createSound(uri, cache = true) {
let sound = new AR.Sound(uri, {
onError: World.onError
});
sound.load();
if (cache) {
this.sounds.push(sound);
}
return sound;
},
/**
* Stops any currently playing sounds.
*/
stopSounds() {
for (let i = 0; i < World.sounds.length; i++) {
switch (World.sounds[i].state) {
case AR.CONST.STATE.LOADED:
case AR.CONST.STATE.INITIALIZED:
case AR.CONST.STATE.PAUSED:
case AR.CONST.STATE.PLAYING:
World.sounds[i].onFinishedPlaying = null;
World.sounds[i].stop();
break;
default:
break;
}
}
},
/**
* Plays an application's sound (see: {@link ARgo.SOUNDS}).
* @param {Sound} sound
* The sound to play. (see: {@link ARgo.SOUNDS})
* @param {Sound[]} [nextSounds]
* The sound to play right after `sound` has finished.
* @param {conditionCallback} [conditionCallback]
* A callback that checks whether each of `nextSound` should be
* played or not. If specified, this is called when each sound
* finishes playing.
* @see ARgo.SOUNDS
*/
playSound(sound, nextSounds, conditionCallback) {
World.stopSounds();
if (nextSounds) {
const nextSound = nextSounds.shift();
if (nextSound) {
sound.onFinishedPlaying = function () {
sound.onFinishedPlaying = null;
if (conditionCallback && !conditionCallback()) {
return;
}
World.playSound(nextSound, nextSounds, conditionCallback);
};
}
}
if (sound.state !== AR.CONST.STATE.LOADED) {
return;
}
sound.play();
},
/**
* Listens to the `AR.ImageTrackable.onImageRecognized` event.
* @param {ImageTarget} target
* @listens AR:ImageTrackable~event:onImageRecognized
*/
onTargetRecognized(target) {
World.targetRecognized = true;
ARgo.hideInfoBar();
let logMessage = ARgo.LOG_MESSAGES.TARGET_ROUTER_RECOGNIZED;
switch (ARgo.CURRENT_PAGE) {
case ARgo.PAGES.MAIN:
ARgo.showPopup();
World.resetModel();
World.appearingAnimation.start();
World.playSound(ARgo.SOUNDS.LOCATE, [ARgo.SOUNDS.BOX], World.isTargetRecognized);
logMessage = ARgo.LOG_MESSAGES.TARGET_BOX_RECOGNIZED;
break;
case ARgo.PAGES.ROUTER:
switch (target.name) {
case ARgo.TARGETS.ROUTER.FRONT:
ARgo.showPopup("argo-first");
World.playSound(ARgo.SOUNDS.LOCATE);
World.routerImageTrackable.addImageTargetCamDrawables(target, [World.modelMarker, World.modelArrow]);
break;
case ARgo.TARGETS.ROUTER.BACK:
case ARgo.TARGETS.ROUTER.BACK_2:
case ARgo.TARGETS.ROUTER.BACK_3:
case ARgo.TARGETS.ROUTER.BACK_4:
case ARgo.TARGETS.ROUTER.BACK_5:
case ARgo.TARGETS.ROUTER.BACK_6:
case ARgo.TARGETS.ROUTER.BACK_7:
console.log(target.name);
ARgo.showPopup("argo-second");
World.playSound(ARgo.SOUNDS.LOCATE, [ARgo.SOUNDS.ROUTER], World.isTargetRecognized);
World.routerImageTrackable.addImageTargetCamDrawables(target, [World.modelBlock]); //, World.portsLabel
break;
}
break;
case ARgo.PAGES.RJ45:
ARgo.showPopup();
World.playSound(ARgo.SOUNDS.INSTRUCTION, [ARgo.SOUNDS.RJ45_GRAPH, ARgo.SOUNDS.CABLE_VIEW]);
break;
case ARgo.PAGES.CHARGER:
ARgo.showPopup();
World.playSound(ARgo.SOUNDS.INSTRUCTION, [ARgo.SOUNDS.POWER_GRAPH, ARgo.SOUNDS.CABLE_VIEW]);
break;
default:
return;
}
ARgo.log(logMessage, target);
},
/**
* Listens to the `AR.ImageTrackable.onImageLost` event.
* @param {ImageTarget} [target]
* @listens AR:ImageTrackable~event:onImageLost
*/
onTargetLost(target) {
World.targetRecognized = false;
ARgo.hidePopup();
ARgo.showInfoBar();
switch (ARgo.CURRENT_PAGE) {
case ARgo.PAGES.MAIN:
ARgo.log(ARgo.LOG_MESSAGES.TARGET_BOX_LOST, target);
break;
case ARgo.PAGES.ROUTER:
case ARgo.PAGES.RJ45:
case ARgo.PAGES.CHARGER:
ARgo.log(ARgo.LOG_MESSAGES.TARGET_ROUTER_LOST, target);
break;
default:
break;
}
},
/**
* Listens to the `AR.Model.onClick` event.
* @param {Object} arObject
* @param {string} modelPart
*/
onModelClick(arObject, modelPart) {
/**
* @type {ARgoModel}
*/
// @ts-ignore
var model = this;
if (model.isOnPresentation) {
return;
}
if (modelPart === ARgo.PARTS.M) {
return;
}
if (ARgo.CURRENT_PAGE === ARgo.PAGES.ROUTER_GRAPH) {
console.log(modelPart);
switch (modelPart) {
case ARgo.PARTS.ETHERNET:
ARgo.CURRENT_PAGE = ARgo.PAGES.RJ45_GRAPH;
model.onPresentationComplete = function () {
World.onModelPresentationComplete(model, ARgo.MODELS.RJ45);
}
model.isOnPresentation = true;
break;
case ARgo.PARTS.POWER:
ARgo.CURRENT_PAGE = ARgo.PAGES.CHARGER_GRAPH;
model.onPresentationComplete = function () {
World.onModelPresentationComplete(model, ARgo.MODELS.CHARGER);
}
model.isOnPresentation = true;
break;
default:
break;
}
model.presentationAnimation.start();
}
},
/**
* Fires when a model moves to presentation mode.
* @param {ARgoModel} model
* @param {string} presentedModel
*/
onModelPresentationComplete(model, presentedModel) {
if (World.defaultPositionable.showModel(presentedModel, true)) {
model.presentedModel = presentedModel;
}
},
/**
* Helper method returning if a target is currently recognized.
* @see World.targetRecognized
* @see conditionCallback
* @returns {boolean}
*/
isTargetRecognized() {
return World.targetRecognized;
},
/**
* @private
*/
resetModel() {
if (!World.targetRecognized) {
ARgo.showInfoBar();
}
},
/**
* Gets if snapped models can be rotated.
* @returns {boolean}
*/
canRotateSnappedModel() {
return ARgo.CURRENT_PAGE == ARgo.PAGES.CONTENTS_ITEM;
},
/**
* Called by the native client on initialization.
* @param {boolean} debuggable
* Indicates if the native client is debuggable.
*/
onInit(debuggable) {
ARgo.isDebuggable = debuggable;
ARgo.log("Application Initialized", {Debuggable: debuggable});
},
/**
* Gets a callback that handles the `onLoaded` event of an AR object.
* @param {string} loggingMessage
* @returns {() => void}
*/
getObjectLoadedHandler(loggingMessage) {
return () => World.onObjectLoaded(loggingMessage);
},
/**
* Handles the `onLoaded` event of AR objects.
* @param {string} loggingMessage
*/
onObjectLoaded(loggingMessage) {
ARgo.showInfoBar();
if (loggingMessage) {
ARgo.log(loggingMessage);
}
},
/**
* Called in response to `AR` errors.
* @param {ARError} [error]
*/
onError(error) {
if (ARgo.isDebuggable) {
alert(error);
}
ARgo.log(error.toString());
ARgo.sendCommand(ARgo.NATIVE_COMMANDS.ERROR, error.toString());
}
// #endregion
};
World.init();