addAnimation.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. import * as React from "react";
  2. import { ButtonLineComponent } from "../../../../../sharedUiComponents/lines/buttonLineComponent";
  3. import { Observable } from "babylonjs/Misc/observable";
  4. import { PropertyChangedEvent } from "../../../../../components/propertyChangedEvent";
  5. import { Animation } from "babylonjs/Animations/animation";
  6. //import { Vector2, Vector3, Quaternion } from "babylonjs/Maths/math.vector"; // Remove comment lines when these imports are supported
  7. //import { Color3, Color4 } from "babylonjs/Maths/math.color"; // Remove comment lines when these imports are supported
  8. import { IAnimatable } from "babylonjs/Animations/animatable.interface";
  9. import { IAnimationKey } from "babylonjs/Animations/animationKey";
  10. interface IAddAnimationProps {
  11. isOpen: boolean;
  12. close: () => void;
  13. entity: IAnimatable;
  14. onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
  15. setNotificationMessage: (message: string) => void;
  16. finishedUpdate: () => void;
  17. addedNewAnimation: (animation: Animation) => void;
  18. fps: number;
  19. selectedToUpdate?: Animation | undefined;
  20. }
  21. /**
  22. * Controls the creation of a new animation.
  23. * @property {boolean} isOpen controls if the add animation pannel is open or closed in the editor controls;
  24. * @property {()=>void} close sends the message to close this panel setting isOpen to false;
  25. * @property {IAnimatable} entity is the animation object to add the animation to;
  26. * @property {Observable<PropertyChangedEvent>} onPropertyChangedObservable is the registered observable
  27. * @property {(message: string) => void} setNotificationMessage sends the message string to display errors on the message box
  28. * @property {() => void} finishedUpdate tells the parent component the update on the animation collection has completed
  29. * @property { (animation: Animation) => void} addedNewAnimation sends the animation to the editor to process how its is rendered
  30. * @property {number} fps Frames per second of the animation.
  31. * @property {Animation | undefined} selectedToUpdate the selected animation so we can update the renderer curve.
  32. */
  33. export class AddAnimation extends React.Component<
  34. IAddAnimationProps,
  35. {
  36. animationName: string;
  37. animationTargetProperty: string;
  38. animationType: number;
  39. loopMode: number;
  40. animationTargetPath: string;
  41. isUpdating: boolean;
  42. }
  43. > {
  44. constructor(props: IAddAnimationProps) {
  45. super(props);
  46. this.state = this.setInitialState(this.props.selectedToUpdate);
  47. }
  48. setInitialState(editingAnimation?: Animation) {
  49. return {
  50. animationName: editingAnimation ? editingAnimation.name : "",
  51. animationTargetPath: "",
  52. animationType: editingAnimation ? editingAnimation.dataType : Animation.ANIMATIONTYPE_FLOAT,
  53. loopMode: editingAnimation
  54. ? editingAnimation.loopMode ?? Animation.ANIMATIONLOOPMODE_CYCLE
  55. : Animation.ANIMATIONLOOPMODE_CYCLE,
  56. animationTargetProperty: editingAnimation ? editingAnimation.targetProperty : "",
  57. isUpdating: editingAnimation ? true : false,
  58. };
  59. }
  60. /**
  61. * We decide wether the animation will be added or edited
  62. * @param prevProps Previous props.
  63. * @param prevState Previous state.
  64. */
  65. componentDidUpdate(prevProps: IAddAnimationProps, prevState: any) {
  66. if (this.props.selectedToUpdate !== undefined && this.props.selectedToUpdate !== prevProps.selectedToUpdate) {
  67. this.setState(this.setInitialState(this.props.selectedToUpdate));
  68. } else {
  69. if (this.props.isOpen === true && this.props.isOpen !== prevProps.isOpen) {
  70. this.setState(this.setInitialState());
  71. }
  72. }
  73. }
  74. /**
  75. * Updates the animation with the correct properties
  76. * Updates its name, loopmode and targetProperty.
  77. * This allows the edition of the animation in the curve editor.
  78. * We can only update these props for the animation.
  79. * Keyframes of course are updated on the curve editor.
  80. */
  81. updateAnimation = () => {
  82. if (this.props.selectedToUpdate !== undefined) {
  83. const oldNameValue = this.props.selectedToUpdate.name;
  84. this.props.selectedToUpdate.name = this.state.animationName;
  85. this.raiseOnPropertyUpdated(oldNameValue, this.state.animationName, "name");
  86. const oldLoopModeValue = this.props.selectedToUpdate.loopMode;
  87. this.props.selectedToUpdate.loopMode = this.state.loopMode;
  88. this.raiseOnPropertyUpdated(oldLoopModeValue, this.state.loopMode, "loopMode");
  89. const oldTargetPropertyValue = this.props.selectedToUpdate.targetProperty;
  90. this.props.selectedToUpdate.targetProperty = this.state.animationTargetProperty;
  91. this.raiseOnPropertyUpdated(oldTargetPropertyValue, this.state.animationTargetProperty, "targetProperty");
  92. this.props.finishedUpdate();
  93. }
  94. };
  95. /**
  96. * Returns the animation type as string
  97. * @param type Type of animation so we return a string.
  98. */
  99. getTypeAsString(type: number) {
  100. switch (type) {
  101. case Animation.ANIMATIONTYPE_FLOAT:
  102. return "Float";
  103. case Animation.ANIMATIONTYPE_QUATERNION:
  104. return "Quaternion";
  105. case Animation.ANIMATIONTYPE_VECTOR3:
  106. return "Vector3";
  107. case Animation.ANIMATIONTYPE_VECTOR2:
  108. return "Vector2";
  109. case Animation.ANIMATIONTYPE_SIZE:
  110. return "Size";
  111. case Animation.ANIMATIONTYPE_COLOR3:
  112. return "Color3";
  113. case Animation.ANIMATIONTYPE_COLOR4:
  114. return "Color4";
  115. default:
  116. return "Float";
  117. }
  118. }
  119. /**
  120. * Process the creation of a new animation.
  121. * We verify is the property to animate is present on the object
  122. * We verify if the property derives from a correct class *type of animation to add an animation
  123. * At the end we create an empty animation named animation.
  124. * The animation keys array is set to empty so we can add keyframes in the editor.
  125. * We return the animation to the curve editor and update the entity animation collection
  126. */
  127. addAnimation = () => {
  128. if (this.state.animationName != "" && this.state.animationTargetProperty != "") {
  129. let matchTypeTargetProperty = this.state.animationTargetProperty.split(".");
  130. let animationDataType = this.state.animationType;
  131. let matched = false;
  132. if (matchTypeTargetProperty.length === 1) {
  133. let match = (this.props.entity as any)[matchTypeTargetProperty[0]];
  134. if (match) {
  135. switch (match.constructor.name) {
  136. case "Vector2":
  137. matched = animationDataType === Animation.ANIMATIONTYPE_VECTOR2;
  138. break;
  139. case "Vector3":
  140. matched = animationDataType === Animation.ANIMATIONTYPE_VECTOR3;
  141. break;
  142. case "Quaternion":
  143. matched = animationDataType === Animation.ANIMATIONTYPE_QUATERNION;
  144. break;
  145. case "Color3":
  146. matched = animationDataType === Animation.ANIMATIONTYPE_COLOR3;
  147. break;
  148. case "Color4":
  149. matched = animationDataType === Animation.ANIMATIONTYPE_COLOR4;
  150. break;
  151. case "Size":
  152. matched = animationDataType === Animation.ANIMATIONTYPE_SIZE;
  153. break;
  154. }
  155. } else {
  156. this.props.setNotificationMessage(
  157. `The selected entity doesn't have a ${matchTypeTargetProperty[0]} property`
  158. );
  159. }
  160. } else if (matchTypeTargetProperty.length > 1) {
  161. let matchProp = (this.props.entity as any)[matchTypeTargetProperty[0]];
  162. if (matchProp) {
  163. let match = matchProp[matchTypeTargetProperty[1]];
  164. if (typeof match === "number") {
  165. animationDataType === Animation.ANIMATIONTYPE_FLOAT ? (matched = true) : (matched = false);
  166. }
  167. }
  168. }
  169. if (matched) {
  170. let alreadyAnimatedProperty = (this.props.entity as IAnimatable).animations?.find(
  171. (anim) => anim.targetProperty === this.state.animationTargetProperty,
  172. this
  173. );
  174. let alreadyAnimationName = (this.props.entity as IAnimatable).animations?.find(
  175. (anim) => anim.name === this.state.animationName,
  176. this
  177. );
  178. if (alreadyAnimatedProperty) {
  179. this.props.setNotificationMessage(
  180. `The property "${this.state.animationTargetProperty}" already has an animation`
  181. );
  182. } else if (alreadyAnimationName) {
  183. this.props.setNotificationMessage(
  184. `There is already an animation with the name: "${this.state.animationName}"`
  185. );
  186. } else {
  187. let animation = new Animation(
  188. this.state.animationName,
  189. this.state.animationTargetProperty,
  190. this.props.fps,
  191. animationDataType
  192. );
  193. // Start with empty keyframes
  194. var keys: IAnimationKey[] = [];
  195. animation.setKeys(keys);
  196. if (this.props.entity.animations) {
  197. const store = this.props.entity.animations;
  198. const updatedCollection = [...this.props.entity.animations, animation];
  199. this.raiseOnPropertyChanged(updatedCollection, store);
  200. this.props.entity.animations = updatedCollection;
  201. this.props.addedNewAnimation(animation);
  202. //Here we clean the form fields
  203. this.setState({
  204. animationName: "",
  205. animationTargetPath: "",
  206. animationType: animationDataType,
  207. loopMode: this.state.loopMode,
  208. animationTargetProperty: "",
  209. });
  210. }
  211. }
  212. } else {
  213. this.props.setNotificationMessage(
  214. `The property "${this.state.animationTargetProperty}" is not a "${this.getTypeAsString(
  215. this.state.animationType
  216. )}" type`
  217. );
  218. }
  219. } else {
  220. this.props.setNotificationMessage(`You need to provide a name and target property.`);
  221. }
  222. };
  223. raiseOnPropertyChanged(newValue: Animation[], previousValue: Animation[]) {
  224. if (!this.props.onPropertyChangedObservable) {
  225. return;
  226. }
  227. this.props.onPropertyChangedObservable.notifyObservers({
  228. object: this.props.entity,
  229. property: "animations",
  230. value: newValue,
  231. initialValue: previousValue,
  232. });
  233. }
  234. raiseOnPropertyUpdated(newValue: string | number | undefined, previousValue: string | number, property: string) {
  235. if (!this.props.onPropertyChangedObservable) {
  236. return;
  237. }
  238. this.props.onPropertyChangedObservable.notifyObservers({
  239. object: this.props.selectedToUpdate,
  240. property: property,
  241. value: newValue,
  242. initialValue: previousValue,
  243. });
  244. }
  245. handlePathChange = (event: React.ChangeEvent<HTMLInputElement>) => {
  246. event.preventDefault();
  247. this.setState({ animationTargetPath: event.target.value.trim() });
  248. };
  249. handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
  250. event.preventDefault();
  251. this.setState({ animationName: event.target.value.trim() });
  252. };
  253. handleTypeChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
  254. event.preventDefault();
  255. this.setState({ animationType: parseInt(event.target.value) });
  256. };
  257. handlePropertyChange = (event: React.ChangeEvent<HTMLInputElement>) => {
  258. event.preventDefault();
  259. this.setState({ animationTargetProperty: event.target.value });
  260. };
  261. handleLoopModeChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
  262. event.preventDefault();
  263. this.setState({ loopMode: parseInt(event.target.value) });
  264. };
  265. render() {
  266. const confirmLabel = this.state.isUpdating ? "Update" : "Create";
  267. const confirmHandleOnClick = this.state.isUpdating ? this.updateAnimation : this.addAnimation;
  268. return (
  269. <div className="new-animation" style={{ display: this.props.isOpen ? "block" : "none" }}>
  270. <div className="sub-content">
  271. <div className="label-input">
  272. <label>Display Name</label>
  273. <input type="text" value={this.state.animationName} onChange={this.handleNameChange}></input>
  274. </div>
  275. {this.state.isUpdating ? null : (
  276. <div className="label-input">
  277. <label>Property</label>
  278. <input
  279. type="text"
  280. value={this.state.animationTargetProperty}
  281. onChange={this.handlePropertyChange}
  282. ></input>
  283. </div>
  284. )}
  285. {this.state.isUpdating ? null : (
  286. <div className="label-input">
  287. <label>Type</label>
  288. <select onChange={this.handleTypeChange} value={this.state.animationType}>
  289. {/** Uncomment the following lines when other animation types are available */}
  290. {/* <option value={Animation.ANIMATIONTYPE_COLOR3}>Color3</option>
  291. <option value={Animation.ANIMATIONTYPE_COLOR4}>Color4</option> */}
  292. <option value={Animation.ANIMATIONTYPE_FLOAT}>Float</option>
  293. {/* <option value={Animation.ANIMATIONTYPE_VECTOR3}>Vector3</option>
  294. <option value={Animation.ANIMATIONTYPE_VECTOR2}>Vector2</option>
  295. <option value={Animation.ANIMATIONTYPE_QUATERNION}>
  296. Quaternion
  297. </option> */}
  298. </select>
  299. </div>
  300. )}
  301. <div className="label-input">
  302. <label>Loop Mode</label>
  303. <select onChange={this.handleLoopModeChange} value={this.state.loopMode}>
  304. <option value={Animation.ANIMATIONLOOPMODE_CYCLE}>Cycle</option>
  305. <option value={Animation.ANIMATIONLOOPMODE_RELATIVE}>Relative</option>
  306. <option value={Animation.ANIMATIONLOOPMODE_CONSTANT}>Constant</option>
  307. </select>
  308. </div>
  309. <div className="confirm-buttons">
  310. <ButtonLineComponent label={confirmLabel} onClick={confirmHandleOnClick} />
  311. {this.props.entity.animations?.length !== 0 ? (
  312. <ButtonLineComponent label={"Cancel"} onClick={this.props.close} />
  313. ) : null}
  314. </div>
  315. </div>
  316. </div>
  317. );
  318. }
  319. }