PropertyLine.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. module INSPECTOR {
  2. export class PropertyFormatter {
  3. /**
  4. * Format the value of the given property of the given object.
  5. */
  6. public static format(obj: any, prop: string): string {
  7. // Get original value;
  8. let value = obj[prop];
  9. // test if type PrimitiveAlignment is available (only included in canvas2d)
  10. if (BABYLON.PrimitiveAlignment) {
  11. if (obj instanceof BABYLON.PrimitiveAlignment) {
  12. if (prop === 'horizontal') {
  13. switch (value) {
  14. case BABYLON.PrimitiveAlignment.AlignLeft:
  15. return 'left';
  16. case BABYLON.PrimitiveAlignment.AlignRight:
  17. return 'right';
  18. case BABYLON.PrimitiveAlignment.AlignCenter:
  19. return 'center';
  20. case BABYLON.PrimitiveAlignment.AlignStretch:
  21. return 'stretch';
  22. }
  23. } else if (prop === 'vertical') {
  24. switch (value) {
  25. case BABYLON.PrimitiveAlignment.AlignTop:
  26. return 'top';
  27. case BABYLON.PrimitiveAlignment.AlignBottom:
  28. return 'bottom';
  29. case BABYLON.PrimitiveAlignment.AlignCenter:
  30. return 'center';
  31. case BABYLON.PrimitiveAlignment.AlignStretch:
  32. return 'stretch';
  33. }
  34. }
  35. }
  36. }
  37. return value;
  38. }
  39. }
  40. /**
  41. * A property line represents a line in the detail panel. This line is composed of :
  42. * - a name (the property name)
  43. * - a value if this property is of a type 'simple' : string, number, boolean, color, texture
  44. * - the type of the value if this property is of a complex type (Vector2, Size, ...)
  45. * - a ID if defined (otherwise an empty string is displayed)
  46. * The original object is sent to the value object who will update it at will.
  47. *
  48. * A property line can contain OTHER property line objects in the case of a complex type.
  49. * If this instance has no link to other instances, its type is ALWAYS a simple one (see above).
  50. *
  51. */
  52. export class PropertyLine {
  53. // The property can be of any type (Property internally can have any type), relative to this._obj
  54. private _property: Property;
  55. //The HTML element corresponding to this line
  56. private _div: HTMLElement;
  57. // The div containing the value to display. Used to update dynamically the property
  58. private _valueDiv: HTMLElement;
  59. // If the type is complex, this property will have child to update
  60. private _children: Array<PropertyLine> = [];
  61. // Array representing the simple type. All others are considered 'complex'
  62. private static _SIMPLE_TYPE = ['number', 'string', 'boolean'];
  63. // The number of pixel at each children step
  64. private static _MARGIN_LEFT = 15;
  65. // The margin-left used to display to row
  66. private _level: number;
  67. /** The list of viewer element displayed at the end of the line (color, texture...) */
  68. private _elements: Array<BasicElement> = [];
  69. /** The property parent of this one. Used to update the value of this property and to retrieve the correct object */
  70. private _parent: PropertyLine;
  71. /** The input element to display if this property is 'simple' in order to update it */
  72. private _input: HTMLInputElement;
  73. /** Display input handler (stored to be removed afterwards) */
  74. private _displayInputHandler: EventListener;
  75. /** Handler used to validate the input by pressing 'enter' */
  76. private _validateInputHandler: EventListener;
  77. /** Handler used to validate the input by pressing 'esc' */
  78. private _escapeInputHandler: EventListener;
  79. /** Handler used on focus out */
  80. private _focusOutInputHandler: EventListener;
  81. private _input_checkbox: HTMLInputElement;
  82. /** Handler used to get mouse position */
  83. private _onMouseDownHandler: EventListener;
  84. private _onMouseDragHandler: EventListener;
  85. private _onMouseUpHandler: EventListener;
  86. /** Save previous Y mouse position */
  87. private _prevY: number;
  88. /**Save value while slider is on */
  89. private _preValue: number;
  90. constructor(prop: Property, parent?: PropertyLine, level: number = 0) {
  91. this._property = prop;
  92. this._level = level;
  93. this._parent = parent;
  94. this._div = Helpers.CreateDiv('row');
  95. this._div.style.marginLeft = `${this._level}px`;
  96. // Property name
  97. let propName: HTMLElement = Helpers.CreateDiv('prop-name', this._div);
  98. propName.textContent = `${this.name}`;
  99. // Value
  100. this._valueDiv = Helpers.CreateDiv('prop-value', this._div);
  101. if (typeof this.value !== 'boolean') {
  102. this._valueDiv.textContent = this._displayValueContent() || '-'; // Init value text node
  103. }
  104. this._createElements();
  105. for (let elem of this._elements) {
  106. this._valueDiv.appendChild(elem.toHtml());
  107. }
  108. this._updateValue();
  109. // If the property type is not simple, add click event to unfold its children
  110. if (typeof this.value === 'boolean') {
  111. this._checkboxInput();
  112. } else if (!this._isSimple()) {
  113. this._valueDiv.classList.add('clickable');
  114. this._valueDiv.addEventListener('click', this._addDetails.bind(this));
  115. } else {
  116. this._initInput();
  117. this._valueDiv.addEventListener('click', this._displayInputHandler);
  118. this._input.addEventListener('focusout', this._focusOutInputHandler);
  119. this._input.addEventListener('keydown', this._validateInputHandler);
  120. this._input.addEventListener('keydown', this._escapeInputHandler);
  121. }
  122. // Add this property to the scheduler
  123. Scheduler.getInstance().add(this);
  124. }
  125. /**
  126. * Init the input element and al its handler :
  127. * - a click in the window remove the input and restore the old property value
  128. * - enters updates the property
  129. */
  130. private _initInput() {
  131. // Create the input element
  132. this._input = document.createElement('input') as HTMLInputElement;
  133. this._input.setAttribute('type', 'text');
  134. // if the property is 'simple', add an event listener to create an input
  135. this._displayInputHandler = this._displayInput.bind(this);
  136. this._validateInputHandler = this._validateInput.bind(this);
  137. this._escapeInputHandler = this._escapeInput.bind(this);
  138. this._focusOutInputHandler = this.update.bind(this);
  139. this._onMouseDownHandler = this._onMouseDown.bind(this);
  140. this._onMouseDragHandler = this._onMouseDrag.bind(this);
  141. this._onMouseUpHandler = this._onMouseUp.bind(this);
  142. }
  143. /**
  144. * On enter : validates the new value and removes the input
  145. * On escape : removes the input
  146. */
  147. private _validateInput(e: KeyboardEvent) {
  148. this._input.removeEventListener('focusout', this._focusOutInputHandler);
  149. if (e.keyCode == 13) { // Enter
  150. this.validateInput(this._input.value);
  151. } else if (e.keyCode == 9) { // Tab
  152. e.preventDefault();
  153. this.validateInput(this._input.value);
  154. } else if (e.keyCode == 27) {
  155. // Esc : remove input
  156. this.update();
  157. }
  158. }
  159. public validateInput(value: any): void {
  160. this.updateObject();
  161. if (typeof this._property.value === 'number') {
  162. this._property.value = parseFloat(value);
  163. } else {
  164. this._property.value = value;
  165. }
  166. // Remove input
  167. this.update();
  168. // resume scheduler
  169. Scheduler.getInstance().pause = false;
  170. }
  171. /**
  172. * On escape : removes the input
  173. */
  174. private _escapeInput(e: KeyboardEvent) {
  175. // Remove focus out handler
  176. this._input.removeEventListener('focusout', this._focusOutInputHandler);
  177. if (e.keyCode == 27) {
  178. // Esc : remove input
  179. this.update();
  180. }
  181. }
  182. /** Removes the input without validating the new value */
  183. private _removeInputWithoutValidating() {
  184. Helpers.CleanDiv(this._valueDiv);
  185. if (typeof this.value !== 'boolean') {
  186. this._valueDiv.textContent = "-";
  187. }
  188. // restore elements
  189. for (let elem of this._elements) {
  190. this._valueDiv.appendChild(elem.toHtml());
  191. }
  192. this._valueDiv.addEventListener('click', this._displayInputHandler);
  193. }
  194. /** Replaces the default display with an input */
  195. private _displayInput(e) {
  196. // Remove the displayInput event listener
  197. this._valueDiv.removeEventListener('click', this._displayInputHandler);
  198. // Set input value
  199. let valueTxt = this._valueDiv.textContent;
  200. this._valueDiv.textContent = "";
  201. this._input.value = valueTxt;
  202. this._valueDiv.appendChild(this._input);
  203. this._input.focus();
  204. if (typeof this.value === 'number') {
  205. // Slider
  206. // let slider = Helpers.CreateDiv('slider-number', this._valueDiv);
  207. // slider.style.background = '#303030';
  208. // slider.style.cursor = 'ew-resize';
  209. // slider.innerHTML = 'HELLO'
  210. this._input.addEventListener('mousedown', this._onMouseDownHandler);
  211. }
  212. this._input.addEventListener('focusout', this._focusOutInputHandler);
  213. // Pause the scheduler
  214. Scheduler.getInstance().pause = true;
  215. }
  216. /** Retrieve the correct object from its parent.
  217. * If no parent exists, returns the property value.
  218. * This method is used at each update in case the property object is removed from the original object
  219. * (example : mesh.position = new BABYLON.Vector3 ; the original vector3 object is deleted from the mesh).
  220. */
  221. public updateObject() {
  222. if (this._parent) {
  223. this._property.obj = this._parent.updateObject();
  224. }
  225. return this._property.value;
  226. }
  227. // Returns the property name
  228. public get name(): string {
  229. return this._property.name;
  230. }
  231. // Returns the value of the property
  232. public get value(): any {
  233. return PropertyFormatter.format(this._property.obj, this._property.name);
  234. }
  235. // Returns the type of the property
  236. public get type(): string {
  237. return this._property.type;
  238. }
  239. /**
  240. * Creates elements that wil be displayed on a property line, depending on the
  241. * type of the property.
  242. */
  243. private _createElements() {
  244. // Colors
  245. if (this.type == 'Color3' || this.type == 'Color4') {
  246. this._elements.push(new ColorPickerElement(this.value, this));
  247. //this._elements.push(new ColorElement(this.value));
  248. }
  249. // Texture
  250. if (this.type == 'Texture') {
  251. this._elements.push(new TextureElement(this.value));
  252. }
  253. // HDR Texture
  254. if (this.type == 'HDRCubeTexture') {
  255. this._elements.push(new HDRCubeTextureElement(this.value));
  256. }
  257. if (this.type == 'CubeTexture') {
  258. this._elements.push(new CubeTextureElement(this.value));
  259. }
  260. }
  261. // Returns the text displayed on the left of the property name :
  262. // - If the type is simple, display its value
  263. // - If the type is complex, but instance of Vector2, Size, display the type and its tostring
  264. // - If the type is another one, display the Type
  265. private _displayValueContent() {
  266. let value = this.value;
  267. // If the value is a number, truncate it if needed
  268. if (typeof value === 'number') {
  269. return Helpers.Trunc(value);
  270. }
  271. // If it's a string or a boolean, display its value
  272. if (typeof value === 'string' || typeof value === 'boolean') {
  273. return value;
  274. }
  275. return PROPERTIES.format(value);
  276. }
  277. /** Delete properly this property line.
  278. * Removes itself from the scheduler.
  279. * Dispose all viewer element (color, texture...)
  280. */
  281. public dispose() {
  282. // console.log('delete properties', this.name);
  283. Scheduler.getInstance().remove(this);
  284. for (let child of this._children) {
  285. // console.log('delete properties', child.name);
  286. Scheduler.getInstance().remove(child);
  287. }
  288. for (let elem of this._elements) {
  289. elem.dispose();
  290. }
  291. this._elements = [];
  292. }
  293. /** Updates the content of _valueDiv with the value of the property,
  294. * and all HTML element correpsonding to this type.
  295. * Elements are updated as well
  296. */
  297. private _updateValue() {
  298. // Update the property object first
  299. this.updateObject();
  300. // Then update its value
  301. // this._valueDiv.textContent = " "; // TOFIX this removes the elements after
  302. if (typeof this.value === 'boolean') {
  303. this._checkboxInput();
  304. } else {
  305. this._valueDiv.childNodes[0].nodeValue = this._displayValueContent();
  306. }
  307. for (let elem of this._elements) {
  308. elem.update(this.value);
  309. }
  310. }
  311. /**
  312. * Update the property division with the new property value.
  313. * If this property is complex, update its child, otherwise update its text content
  314. */
  315. public update() {
  316. this._removeInputWithoutValidating();
  317. this._updateValue();
  318. }
  319. /**
  320. * Returns true if the given instance is a simple type
  321. */
  322. private static _IS_TYPE_SIMPLE(inst: any) {
  323. let type = Helpers.GET_TYPE(inst);
  324. return PropertyLine._SIMPLE_TYPE.indexOf(type) != -1;
  325. }
  326. /**
  327. * Returns true if the type of this property is simple, false otherwise.
  328. * Returns true if the value is null
  329. */
  330. private _isSimple(): boolean {
  331. if (this.value != null && this.type !== 'type_not_defined') {
  332. if (PropertyLine._SIMPLE_TYPE.indexOf(this.type) == -1) {
  333. // complex type : return the type name
  334. return false;
  335. } else {
  336. // simple type : return value
  337. return true;
  338. }
  339. } else {
  340. return true;
  341. }
  342. }
  343. public toHtml(): HTMLElement {
  344. return this._div;
  345. }
  346. /**
  347. * Add sub properties in case of a complex type
  348. */
  349. private _addDetails() {
  350. if (this._div.classList.contains('unfolded')) {
  351. // Remove class unfolded
  352. this._div.classList.remove('unfolded');
  353. // remove html children
  354. for (let child of this._children) {
  355. this._div.parentNode.removeChild(child.toHtml());
  356. }
  357. } else {
  358. // if children does not exists, generate it
  359. this._div.classList.toggle('unfolded');
  360. if (this._children.length == 0) {
  361. let objToDetail = this.value;
  362. let propToDisplay = PROPERTIES[Helpers.GET_TYPE(objToDetail)].properties.reverse();
  363. let propertyLine = null;
  364. for (let prop of propToDisplay) {
  365. let infos = new Property(prop, this._property.value);
  366. let child = new PropertyLine(infos, this, this._level + PropertyLine._MARGIN_LEFT);
  367. this._children.push(child);
  368. }
  369. }
  370. // otherwise display it
  371. for (let child of this._children) {
  372. this._div.parentNode.insertBefore(child.toHtml(), this._div.nextSibling);
  373. }
  374. }
  375. }
  376. /**
  377. * Refresh mouse position on y axis
  378. * @param e
  379. */
  380. private _onMouseDrag(e: MouseEvent): void {
  381. const diff = this._prevY - e.clientY;
  382. this._input.value = (this._preValue + diff).toString();
  383. }
  384. /**
  385. * Save new value from slider
  386. * @param e
  387. */
  388. private _onMouseUp(e: MouseEvent): void {
  389. window.removeEventListener('mousemove', this._onMouseDragHandler);
  390. window.removeEventListener('mouseup', this._onMouseUpHandler);
  391. this._prevY = e.clientY;
  392. }
  393. /**
  394. * Start record mouse position
  395. * @param e
  396. */
  397. private _onMouseDown(e: MouseEvent): void {
  398. this._prevY = e.clientY;
  399. this._preValue = this.value;
  400. window.addEventListener('mousemove', this._onMouseDragHandler);
  401. window.addEventListener('mouseup', this._onMouseUpHandler);
  402. }
  403. /**
  404. * Create input entry
  405. */
  406. private _checkboxInput() {
  407. if(this._valueDiv.childElementCount < 1) { // Prevent display two checkbox
  408. this._input_checkbox = Helpers.CreateInput('checkbox-element', this._valueDiv);
  409. this._input_checkbox.type = 'checkbox'
  410. this._input_checkbox.checked = this.value;
  411. this._input_checkbox.addEventListener('change', () => {
  412. Scheduler.getInstance().pause = true;
  413. this.validateInput(!this.value)
  414. })
  415. }
  416. }
  417. }
  418. }