actionManager.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  1. import { Nullable } from "../types";
  2. import { AbstractMesh } from "../Meshes/abstractMesh";
  3. import { Scene } from "../scene";
  4. import { Vector3, Vector4 } from "../Maths/math.vector";
  5. import { Color3, Color4 } from "../Maths/math.color";
  6. import { Condition, ValueCondition } from "./condition";
  7. import { Action, IAction } from "./action";
  8. import { DoNothingAction } from "./directActions";
  9. import { EngineStore } from "../Engines/engineStore";
  10. import { IActionEvent } from "../Actions/actionEvent";
  11. import { Logger } from "../Misc/logger";
  12. import { DeepCopier } from "../Misc/deepCopier";
  13. import { _TypeStore } from "../Misc/typeStore";
  14. import { AbstractActionManager } from './abstractActionManager';
  15. import { Constants } from "../Engines/constants";
  16. /**
  17. * Action Manager manages all events to be triggered on a given mesh or the global scene.
  18. * A single scene can have many Action Managers to handle predefined actions on specific meshes.
  19. * @see http://doc.babylonjs.com/how_to/how_to_use_actions
  20. */
  21. export class ActionManager extends AbstractActionManager {
  22. /**
  23. * Nothing
  24. * @see http://doc.babylonjs.com/how_to/how_to_use_actions#triggers
  25. */
  26. public static readonly NothingTrigger = Constants.ACTION_NothingTrigger;
  27. /**
  28. * On pick
  29. * @see http://doc.babylonjs.com/how_to/how_to_use_actions#triggers
  30. */
  31. public static readonly OnPickTrigger = Constants.ACTION_OnPickTrigger;
  32. /**
  33. * On left pick
  34. * @see http://doc.babylonjs.com/how_to/how_to_use_actions#triggers
  35. */
  36. public static readonly OnLeftPickTrigger = Constants.ACTION_OnLeftPickTrigger;
  37. /**
  38. * On right pick
  39. * @see http://doc.babylonjs.com/how_to/how_to_use_actions#triggers
  40. */
  41. public static readonly OnRightPickTrigger = Constants.ACTION_OnRightPickTrigger;
  42. /**
  43. * On center pick
  44. * @see http://doc.babylonjs.com/how_to/how_to_use_actions#triggers
  45. */
  46. public static readonly OnCenterPickTrigger = Constants.ACTION_OnCenterPickTrigger;
  47. /**
  48. * On pick down
  49. * @see http://doc.babylonjs.com/how_to/how_to_use_actions#triggers
  50. */
  51. public static readonly OnPickDownTrigger = Constants.ACTION_OnPickDownTrigger;
  52. /**
  53. * On double pick
  54. * @see http://doc.babylonjs.com/how_to/how_to_use_actions#triggers
  55. */
  56. public static readonly OnDoublePickTrigger = Constants.ACTION_OnDoublePickTrigger;
  57. /**
  58. * On pick up
  59. * @see http://doc.babylonjs.com/how_to/how_to_use_actions#triggers
  60. */
  61. public static readonly OnPickUpTrigger = Constants.ACTION_OnPickUpTrigger;
  62. /**
  63. * On pick out.
  64. * This trigger will only be raised if you also declared a OnPickDown
  65. * @see http://doc.babylonjs.com/how_to/how_to_use_actions#triggers
  66. */
  67. public static readonly OnPickOutTrigger = Constants.ACTION_OnPickOutTrigger;
  68. /**
  69. * On long press
  70. * @see http://doc.babylonjs.com/how_to/how_to_use_actions#triggers
  71. */
  72. public static readonly OnLongPressTrigger = Constants.ACTION_OnLongPressTrigger;
  73. /**
  74. * On pointer over
  75. * @see http://doc.babylonjs.com/how_to/how_to_use_actions#triggers
  76. */
  77. public static readonly OnPointerOverTrigger = Constants.ACTION_OnPointerOverTrigger;
  78. /**
  79. * On pointer out
  80. * @see http://doc.babylonjs.com/how_to/how_to_use_actions#triggers
  81. */
  82. public static readonly OnPointerOutTrigger = Constants.ACTION_OnPointerOutTrigger;
  83. /**
  84. * On every frame
  85. * @see http://doc.babylonjs.com/how_to/how_to_use_actions#triggers
  86. */
  87. public static readonly OnEveryFrameTrigger = Constants.ACTION_OnEveryFrameTrigger;
  88. /**
  89. * On intersection enter
  90. * @see http://doc.babylonjs.com/how_to/how_to_use_actions#triggers
  91. */
  92. public static readonly OnIntersectionEnterTrigger = Constants.ACTION_OnIntersectionEnterTrigger;
  93. /**
  94. * On intersection exit
  95. * @see http://doc.babylonjs.com/how_to/how_to_use_actions#triggers
  96. */
  97. public static readonly OnIntersectionExitTrigger = Constants.ACTION_OnIntersectionExitTrigger;
  98. /**
  99. * On key down
  100. * @see http://doc.babylonjs.com/how_to/how_to_use_actions#triggers
  101. */
  102. public static readonly OnKeyDownTrigger = Constants.ACTION_OnKeyDownTrigger;
  103. /**
  104. * On key up
  105. * @see http://doc.babylonjs.com/how_to/how_to_use_actions#triggers
  106. */
  107. public static readonly OnKeyUpTrigger = 15;
  108. // Members
  109. private _scene: Scene;
  110. /**
  111. * Creates a new action manager
  112. * @param scene defines the hosting scene
  113. */
  114. constructor(scene: Scene) {
  115. super();
  116. this._scene = scene || EngineStore.LastCreatedScene;
  117. scene.actionManagers.push(this);
  118. }
  119. // Methods
  120. /**
  121. * Releases all associated resources
  122. */
  123. public dispose(): void {
  124. var index = this._scene.actionManagers.indexOf(this);
  125. for (var i = 0; i < this.actions.length; i++) {
  126. var action = this.actions[i];
  127. ActionManager.Triggers[action.trigger]--;
  128. if (ActionManager.Triggers[action.trigger] === 0) {
  129. delete ActionManager.Triggers[action.trigger];
  130. }
  131. }
  132. if (index > -1) {
  133. this._scene.actionManagers.splice(index, 1);
  134. }
  135. }
  136. /**
  137. * Gets hosting scene
  138. * @returns the hosting scene
  139. */
  140. public getScene(): Scene {
  141. return this._scene;
  142. }
  143. /**
  144. * Does this action manager handles actions of any of the given triggers
  145. * @param triggers defines the triggers to be tested
  146. * @return a boolean indicating whether one (or more) of the triggers is handled
  147. */
  148. public hasSpecificTriggers(triggers: number[]): boolean {
  149. for (var index = 0; index < this.actions.length; index++) {
  150. var action = this.actions[index];
  151. if (triggers.indexOf(action.trigger) > -1) {
  152. return true;
  153. }
  154. }
  155. return false;
  156. }
  157. /**
  158. * Does this action manager handles actions of any of the given triggers. This function takes two arguments for
  159. * speed.
  160. * @param triggerA defines the trigger to be tested
  161. * @param triggerB defines the trigger to be tested
  162. * @return a boolean indicating whether one (or more) of the triggers is handled
  163. */
  164. public hasSpecificTriggers2(triggerA: number, triggerB: number): boolean {
  165. for (var index = 0; index < this.actions.length; index++) {
  166. var action = this.actions[index];
  167. if (triggerA == action.trigger || triggerB == action.trigger) {
  168. return true;
  169. }
  170. }
  171. return false;
  172. }
  173. /**
  174. * Does this action manager handles actions of a given trigger
  175. * @param trigger defines the trigger to be tested
  176. * @param parameterPredicate defines an optional predicate to filter triggers by parameter
  177. * @return whether the trigger is handled
  178. */
  179. public hasSpecificTrigger(trigger: number, parameterPredicate?: (parameter: any) => boolean): boolean {
  180. for (var index = 0; index < this.actions.length; index++) {
  181. var action = this.actions[index];
  182. if (action.trigger === trigger) {
  183. if (parameterPredicate) {
  184. if (parameterPredicate(action.getTriggerParameter())) {
  185. return true;
  186. }
  187. } else {
  188. return true;
  189. }
  190. }
  191. }
  192. return false;
  193. }
  194. /**
  195. * Does this action manager has pointer triggers
  196. */
  197. public get hasPointerTriggers(): boolean {
  198. for (var index = 0; index < this.actions.length; index++) {
  199. var action = this.actions[index];
  200. if (action.trigger >= ActionManager.OnPickTrigger && action.trigger <= ActionManager.OnPointerOutTrigger) {
  201. return true;
  202. }
  203. }
  204. return false;
  205. }
  206. /**
  207. * Does this action manager has pick triggers
  208. */
  209. public get hasPickTriggers(): boolean {
  210. for (var index = 0; index < this.actions.length; index++) {
  211. var action = this.actions[index];
  212. if (action.trigger >= ActionManager.OnPickTrigger && action.trigger <= ActionManager.OnPickUpTrigger) {
  213. return true;
  214. }
  215. }
  216. return false;
  217. }
  218. /**
  219. * Registers an action to this action manager
  220. * @param action defines the action to be registered
  221. * @return the action amended (prepared) after registration
  222. */
  223. public registerAction(action: IAction): Nullable<IAction> {
  224. if (action.trigger === ActionManager.OnEveryFrameTrigger) {
  225. if (this.getScene().actionManager !== this) {
  226. Logger.Warn("OnEveryFrameTrigger can only be used with scene.actionManager");
  227. return null;
  228. }
  229. }
  230. this.actions.push(action);
  231. if (ActionManager.Triggers[action.trigger]) {
  232. ActionManager.Triggers[action.trigger]++;
  233. }
  234. else {
  235. ActionManager.Triggers[action.trigger] = 1;
  236. }
  237. action._actionManager = this;
  238. action._prepare();
  239. return action;
  240. }
  241. /**
  242. * Unregisters an action to this action manager
  243. * @param action defines the action to be unregistered
  244. * @return a boolean indicating whether the action has been unregistered
  245. */
  246. public unregisterAction(action: IAction): Boolean {
  247. var index = this.actions.indexOf(action);
  248. if (index !== -1) {
  249. this.actions.splice(index, 1);
  250. ActionManager.Triggers[action.trigger] -= 1;
  251. if (ActionManager.Triggers[action.trigger] === 0) {
  252. delete ActionManager.Triggers[action.trigger];
  253. }
  254. delete action._actionManager;
  255. return true;
  256. }
  257. return false;
  258. }
  259. /**
  260. * Process a specific trigger
  261. * @param trigger defines the trigger to process
  262. * @param evt defines the event details to be processed
  263. */
  264. public processTrigger(trigger: number, evt?: IActionEvent): void {
  265. for (var index = 0; index < this.actions.length; index++) {
  266. var action = this.actions[index];
  267. if (action.trigger === trigger) {
  268. if (evt) {
  269. if (trigger === ActionManager.OnKeyUpTrigger
  270. || trigger === ActionManager.OnKeyDownTrigger) {
  271. var parameter = action.getTriggerParameter();
  272. if (parameter && parameter !== evt.sourceEvent.keyCode) {
  273. if (!parameter.toLowerCase) {
  274. continue;
  275. }
  276. var lowerCase = parameter.toLowerCase();
  277. if (lowerCase !== evt.sourceEvent.key) {
  278. var unicode = evt.sourceEvent.charCode ? evt.sourceEvent.charCode : evt.sourceEvent.keyCode;
  279. var actualkey = String.fromCharCode(unicode).toLowerCase();
  280. if (actualkey !== lowerCase) {
  281. continue;
  282. }
  283. }
  284. }
  285. }
  286. }
  287. action._executeCurrent(evt);
  288. }
  289. }
  290. }
  291. /** @hidden */
  292. public _getEffectiveTarget(target: any, propertyPath: string): any {
  293. var properties = propertyPath.split(".");
  294. for (var index = 0; index < properties.length - 1; index++) {
  295. target = target[properties[index]];
  296. }
  297. return target;
  298. }
  299. /** @hidden */
  300. public _getProperty(propertyPath: string): string {
  301. var properties = propertyPath.split(".");
  302. return properties[properties.length - 1];
  303. }
  304. /**
  305. * Serialize this manager to a JSON object
  306. * @param name defines the property name to store this manager
  307. * @returns a JSON representation of this manager
  308. */
  309. public serialize(name: string): any {
  310. var root = {
  311. children: new Array(),
  312. name: name,
  313. type: 3, // Root node
  314. properties: new Array() // Empty for root but required
  315. };
  316. for (var i = 0; i < this.actions.length; i++) {
  317. var triggerObject = {
  318. type: 0, // Trigger
  319. children: new Array(),
  320. name: ActionManager.GetTriggerName(this.actions[i].trigger),
  321. properties: new Array()
  322. };
  323. var triggerOptions = this.actions[i].triggerOptions;
  324. if (triggerOptions && typeof triggerOptions !== "number") {
  325. if (triggerOptions.parameter instanceof Node) {
  326. triggerObject.properties.push(Action._GetTargetProperty(triggerOptions.parameter));
  327. }
  328. else {
  329. var parameter = <any>{};
  330. DeepCopier.DeepCopy(triggerOptions.parameter, parameter, ["mesh"]);
  331. if (triggerOptions.parameter && triggerOptions.parameter.mesh) {
  332. parameter._meshId = triggerOptions.parameter.mesh.id;
  333. }
  334. triggerObject.properties.push({ name: "parameter", targetType: null, value: parameter });
  335. }
  336. }
  337. // Serialize child action, recursively
  338. this.actions[i].serialize(triggerObject);
  339. // Add serialized trigger
  340. root.children.push(triggerObject);
  341. }
  342. return root;
  343. }
  344. /**
  345. * Creates a new ActionManager from a JSON data
  346. * @param parsedActions defines the JSON data to read from
  347. * @param object defines the hosting mesh
  348. * @param scene defines the hosting scene
  349. */
  350. public static Parse(parsedActions: any, object: Nullable<AbstractMesh>, scene: Scene): void {
  351. var actionManager = new ActionManager(scene);
  352. if (object === null) {
  353. scene.actionManager = actionManager;
  354. }
  355. else {
  356. object.actionManager = actionManager;
  357. }
  358. // instanciate a new object
  359. var instanciate = (name: string, params: Array<any>): any => {
  360. const internalClassType = _TypeStore.GetClass("BABYLON." + name);
  361. if (internalClassType) {
  362. var newInstance: Object = Object.create(internalClassType.prototype);
  363. newInstance.constructor.apply(newInstance, params);
  364. return newInstance;
  365. }
  366. };
  367. var parseParameter = (name: string, value: string, target: any, propertyPath: Nullable<string>): any => {
  368. if (propertyPath === null) {
  369. // String, boolean or float
  370. var floatValue = parseFloat(value);
  371. if (value === "true" || value === "false") {
  372. return value === "true";
  373. }
  374. else {
  375. return isNaN(floatValue) ? value : floatValue;
  376. }
  377. }
  378. var effectiveTarget = propertyPath.split(".");
  379. var values = value.split(",");
  380. // Get effective Target
  381. for (var i = 0; i < effectiveTarget.length; i++) {
  382. target = target[effectiveTarget[i]];
  383. }
  384. // Return appropriate value with its type
  385. if (typeof (target) === "boolean") {
  386. return values[0] === "true";
  387. }
  388. if (typeof (target) === "string") {
  389. return values[0];
  390. }
  391. // Parameters with multiple values such as Vector3 etc.
  392. var split = new Array<number>();
  393. for (var i = 0; i < values.length; i++) {
  394. split.push(parseFloat(values[i]));
  395. }
  396. if (target instanceof Vector3) {
  397. return Vector3.FromArray(split);
  398. }
  399. if (target instanceof Vector4) {
  400. return Vector4.FromArray(split);
  401. }
  402. if (target instanceof Color3) {
  403. return Color3.FromArray(split);
  404. }
  405. if (target instanceof Color4) {
  406. return Color4.FromArray(split);
  407. }
  408. return parseFloat(values[0]);
  409. };
  410. // traverse graph per trigger
  411. var traverse = (parsedAction: any, trigger: any, condition: Nullable<Condition>, action: Nullable<Action>, combineArray: Nullable<Array<Action>> = null) => {
  412. if (parsedAction.detached) {
  413. return;
  414. }
  415. var parameters = new Array<any>();
  416. var target: any = null;
  417. var propertyPath: Nullable<string> = null;
  418. var combine = parsedAction.combine && parsedAction.combine.length > 0;
  419. // Parameters
  420. if (parsedAction.type === 2) {
  421. parameters.push(actionManager);
  422. }
  423. else {
  424. parameters.push(trigger);
  425. }
  426. if (combine) {
  427. var actions = new Array<Action>();
  428. for (var j = 0; j < parsedAction.combine.length; j++) {
  429. traverse(parsedAction.combine[j], ActionManager.NothingTrigger, condition, action, actions);
  430. }
  431. parameters.push(actions);
  432. }
  433. else {
  434. for (var i = 0; i < parsedAction.properties.length; i++) {
  435. var value = parsedAction.properties[i].value;
  436. var name = parsedAction.properties[i].name;
  437. var targetType = parsedAction.properties[i].targetType;
  438. if (name === "target") {
  439. if (targetType !== null && targetType === "SceneProperties") {
  440. value = target = scene;
  441. }
  442. else {
  443. value = target = scene.getNodeByName(value);
  444. }
  445. }
  446. else if (name === "parent") {
  447. value = scene.getNodeByName(value);
  448. }
  449. else if (name === "sound") {
  450. // Can not externalize to component, so only checks for the presence off the API.
  451. if (scene.getSoundByName) {
  452. value = scene.getSoundByName(value);
  453. }
  454. }
  455. else if (name !== "propertyPath") {
  456. if (parsedAction.type === 2 && name === "operator") {
  457. value = (<any>ValueCondition)[value];
  458. }
  459. else {
  460. value = parseParameter(name, value, target, name === "value" ? propertyPath : null);
  461. }
  462. } else {
  463. propertyPath = value;
  464. }
  465. parameters.push(value);
  466. }
  467. }
  468. if (combineArray === null) {
  469. parameters.push(condition);
  470. }
  471. else {
  472. parameters.push(null);
  473. }
  474. // If interpolate value action
  475. if (parsedAction.name === "InterpolateValueAction") {
  476. var param = parameters[parameters.length - 2];
  477. parameters[parameters.length - 1] = param;
  478. parameters[parameters.length - 2] = condition;
  479. }
  480. // Action or condition(s) and not CombineAction
  481. var newAction = instanciate(parsedAction.name, parameters);
  482. if (newAction instanceof Condition && condition !== null) {
  483. var nothing = new DoNothingAction(trigger, condition);
  484. if (action) {
  485. action.then(nothing);
  486. }
  487. else {
  488. actionManager.registerAction(nothing);
  489. }
  490. action = nothing;
  491. }
  492. if (combineArray === null) {
  493. if (newAction instanceof Condition) {
  494. condition = newAction;
  495. newAction = action;
  496. } else {
  497. condition = null;
  498. if (action) {
  499. action.then(newAction);
  500. }
  501. else {
  502. actionManager.registerAction(newAction);
  503. }
  504. }
  505. }
  506. else {
  507. combineArray.push(newAction);
  508. }
  509. for (var i = 0; i < parsedAction.children.length; i++) {
  510. traverse(parsedAction.children[i], trigger, condition, newAction, null);
  511. }
  512. };
  513. // triggers
  514. for (var i = 0; i < parsedActions.children.length; i++) {
  515. var triggerParams: any;
  516. var trigger = parsedActions.children[i];
  517. if (trigger.properties.length > 0) {
  518. var param = trigger.properties[0].value;
  519. var value = trigger.properties[0].targetType === null ? param : scene.getMeshByName(param);
  520. if (value._meshId) {
  521. value.mesh = scene.getMeshByID(value._meshId);
  522. }
  523. triggerParams = { trigger: (<any>ActionManager)[trigger.name], parameter: value };
  524. }
  525. else {
  526. triggerParams = (<any>ActionManager)[trigger.name];
  527. }
  528. for (var j = 0; j < trigger.children.length; j++) {
  529. if (!trigger.detached) {
  530. traverse(trigger.children[j], triggerParams, null, null);
  531. }
  532. }
  533. }
  534. }
  535. /**
  536. * Get a trigger name by index
  537. * @param trigger defines the trigger index
  538. * @returns a trigger name
  539. */
  540. public static GetTriggerName(trigger: number): string {
  541. switch (trigger) {
  542. case 0: return "NothingTrigger";
  543. case 1: return "OnPickTrigger";
  544. case 2: return "OnLeftPickTrigger";
  545. case 3: return "OnRightPickTrigger";
  546. case 4: return "OnCenterPickTrigger";
  547. case 5: return "OnPickDownTrigger";
  548. case 6: return "OnPickUpTrigger";
  549. case 7: return "OnLongPressTrigger";
  550. case 8: return "OnPointerOverTrigger";
  551. case 9: return "OnPointerOutTrigger";
  552. case 10: return "OnEveryFrameTrigger";
  553. case 11: return "OnIntersectionEnterTrigger";
  554. case 12: return "OnIntersectionExitTrigger";
  555. case 13: return "OnKeyDownTrigger";
  556. case 14: return "OnKeyUpTrigger";
  557. case 15: return "OnPickOutTrigger";
  558. default: return "";
  559. }
  560. }
  561. }