babylon.canvas2dLayoutEngine.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664
  1. module BABYLON {
  2. export interface ILayoutData {
  3. }
  4. @className("LayoutEngineBase", "BABYLON")
  5. /**
  6. * This is the base class you have to extend in order to implement your own Layout Engine.
  7. * Note that for performance reason, each different Layout Engine type can be exposed as one/many singleton or must be instanced each time.
  8. * If data has to be associated to a given primitive you can use the SmartPropertyPrim.addExternalData API to do it.
  9. */
  10. export class LayoutEngineBase implements ILockable {
  11. constructor() {
  12. this.layoutDirtyOnPropertyChangedMask = 0;
  13. }
  14. public updateLayout(prim: Prim2DBase) {
  15. }
  16. public get isChildPositionAllowed(): boolean {
  17. return false;
  18. }
  19. isLocked(): boolean {
  20. return this._isLocked;
  21. }
  22. lock(): boolean {
  23. if (this._isLocked) {
  24. return false;
  25. }
  26. this._isLocked = true;
  27. return true;
  28. }
  29. public layoutDirtyOnPropertyChangedMask;
  30. private _isLocked: boolean;
  31. }
  32. @className("CanvasLayoutEngine", "BABYLON")
  33. /**
  34. * The default Layout Engine, primitive are positioning into a Canvas, using their x/y coordinates.
  35. * This layout must be used as a Singleton through the CanvasLayoutEngine.Singleton property.
  36. */
  37. export class CanvasLayoutEngine extends LayoutEngineBase {
  38. private static _singleton: CanvasLayoutEngine = null;
  39. public static get Singleton(): CanvasLayoutEngine {
  40. if (!CanvasLayoutEngine._singleton) {
  41. CanvasLayoutEngine._singleton = new CanvasLayoutEngine();
  42. }
  43. return CanvasLayoutEngine._singleton;
  44. }
  45. constructor() {
  46. super();
  47. this.layoutDirtyOnPropertyChangedMask = Prim2DBase.sizeProperty.flagId | Prim2DBase.actualSizeProperty.flagId;
  48. }
  49. // A very simple (no) layout computing...
  50. // The Canvas and its direct children gets the Canvas' size as Layout Area
  51. // Indirect children have their Layout Area to the actualSize (margin area) of their parent
  52. public updateLayout(prim: Prim2DBase) {
  53. // If this prim is layoutDiry we update its layoutArea and also the one of its direct children
  54. if (prim._isFlagSet(SmartPropertyPrim.flagLayoutDirty)) {
  55. for (let child of prim.children) {
  56. this._doUpdate(child);
  57. }
  58. prim._clearFlags(SmartPropertyPrim.flagLayoutDirty);
  59. }
  60. }
  61. private _doUpdate(prim: Prim2DBase) {
  62. // Canvas ?
  63. if (prim instanceof Canvas2D) {
  64. prim.layoutArea = prim.actualSize; //.multiplyByFloats(prim.scaleX, prim.scaleY);
  65. }
  66. // Direct child of Canvas ?
  67. else if (prim.parent instanceof Canvas2D) {
  68. prim.layoutArea = prim.owner.actualSize; //.multiplyByFloats(prim.owner.scaleX, prim.owner.scaleY);
  69. }
  70. // Indirect child of Canvas
  71. else {
  72. prim.layoutArea = prim.parent.contentArea;
  73. }
  74. }
  75. get isChildPositionAllowed(): boolean {
  76. return true;
  77. }
  78. }
  79. @className("StackPanelLayoutEngine", "BABYLON")
  80. /**
  81. * A stack panel layout. Primitive will be stack either horizontally or vertically.
  82. * This Layout type must be used as a Singleton, use the StackPanelLayoutEngine.Horizontal for an horizontal stack panel or StackPanelLayoutEngine.Vertical for a vertical one.
  83. */
  84. export class StackPanelLayoutEngine extends LayoutEngineBase {
  85. constructor() {
  86. super();
  87. this.layoutDirtyOnPropertyChangedMask = Prim2DBase.sizeProperty.flagId | Prim2DBase.actualSizeProperty.flagId;
  88. }
  89. public static get Horizontal(): StackPanelLayoutEngine {
  90. if (!StackPanelLayoutEngine._horizontal) {
  91. StackPanelLayoutEngine._horizontal = new StackPanelLayoutEngine();
  92. StackPanelLayoutEngine._horizontal.isHorizontal = true;
  93. StackPanelLayoutEngine._horizontal.lock();
  94. }
  95. return StackPanelLayoutEngine._horizontal;
  96. }
  97. public static get Vertical(): StackPanelLayoutEngine {
  98. if (!StackPanelLayoutEngine._vertical) {
  99. StackPanelLayoutEngine._vertical = new StackPanelLayoutEngine();
  100. StackPanelLayoutEngine._vertical.isHorizontal = false;
  101. StackPanelLayoutEngine._vertical.lock();
  102. }
  103. return StackPanelLayoutEngine._vertical;
  104. }
  105. private static _horizontal: StackPanelLayoutEngine = null;
  106. private static _vertical: StackPanelLayoutEngine = null;
  107. get isHorizontal(): boolean {
  108. return this._isHorizontal;
  109. }
  110. set isHorizontal(val: boolean) {
  111. if (this.isLocked()) {
  112. return;
  113. }
  114. this._isHorizontal = val;
  115. }
  116. private _isHorizontal: boolean = true;
  117. private static dstOffset = Vector4.Zero();
  118. private static dstArea = Size.Zero();
  119. public updateLayout(prim: Prim2DBase) {
  120. if (prim._isFlagSet(SmartPropertyPrim.flagLayoutDirty)) {
  121. let x = 0;
  122. let y = 0;
  123. let h = this.isHorizontal;
  124. let max = 0;
  125. for (let child of prim.children) {
  126. if (child._isFlagSet(SmartPropertyPrim.flagNoPartOfLayout)) {
  127. continue;
  128. }
  129. let layoutArea: Size;
  130. if (child._hasMargin) {
  131. child.margin.computeWithAlignment(prim.layoutArea, child.actualSize, child.marginAlignment, child.actualScale, StackPanelLayoutEngine.dstOffset, StackPanelLayoutEngine.dstArea, true);
  132. layoutArea = StackPanelLayoutEngine.dstArea.clone();
  133. child.layoutArea = layoutArea;
  134. } else {
  135. layoutArea = child.layoutArea;
  136. child.margin.computeArea(child.actualSize, layoutArea);
  137. }
  138. max = Math.max(max, h ? layoutArea.height : layoutArea.width);
  139. }
  140. for (let child of prim.children) {
  141. if (child._isFlagSet(SmartPropertyPrim.flagNoPartOfLayout)) {
  142. continue;
  143. }
  144. child.layoutAreaPos = new Vector2(x, y);
  145. let layoutArea = child.layoutArea;
  146. if (h) {
  147. x += layoutArea.width;
  148. child.layoutArea = new Size(layoutArea.width, max);
  149. } else {
  150. y += layoutArea.height;
  151. child.layoutArea = new Size(max, layoutArea.height);
  152. }
  153. }
  154. prim._clearFlags(SmartPropertyPrim.flagLayoutDirty);
  155. }
  156. }
  157. get isChildPositionAllowed(): boolean {
  158. return false;
  159. }
  160. }
  161. /**
  162. * GridData is used specify what row(s) and column(s) a primitive is placed in when its parent is using a Grid Panel Layout.
  163. */
  164. export class GridData implements ILayoutData{
  165. /**
  166. * the row number of the grid
  167. **/
  168. public row:number;
  169. /**
  170. * the column number of the grid
  171. **/
  172. public column:number;
  173. /**
  174. * the number of rows a primitive will occupy
  175. **/
  176. public rowSpan:number;
  177. /**
  178. * the number of columns a primitive will occupy
  179. **/
  180. public columnSpan:number;
  181. /**
  182. * Create a Grid Data that describes where a primitive will be placed in a Grid Panel Layout.
  183. * @param row the row number of the grid
  184. * @param column the column number of the grid
  185. * @param rowSpan the number of rows a primitive will occupy
  186. * @param columnSpan the number of columns a primitive will occupy
  187. **/
  188. constructor(row:number, column:number, rowSpan?:number, columnSpan?:number){
  189. this.row = row;
  190. this.column = column;
  191. this.rowSpan = (rowSpan == null) ? 1 : rowSpan;
  192. this.columnSpan = (columnSpan == null) ? 1 : columnSpan;
  193. }
  194. }
  195. class GridDimensionDefinition {
  196. public static Pixels = 1;
  197. public static Stars = 2;
  198. public static Auto = 3;
  199. _parse(value: string, res: (v: number, vp: number, t: number) => void) {
  200. let v = value.toLocaleLowerCase().trim();
  201. if (v.indexOf("auto") === 0) {
  202. res(null, null, GridDimensionDefinition.Auto);
  203. } else if (v.indexOf("*") !== -1) {
  204. let i = v.indexOf("*");
  205. let w = 1;
  206. if(i > 0){
  207. w = parseFloat(v.substr(0, i));
  208. }
  209. res(w, null, GridDimensionDefinition.Stars);
  210. } else {
  211. let w = parseFloat(v);
  212. res(w, w, GridDimensionDefinition.Pixels);
  213. }
  214. }
  215. }
  216. class RowDefinition extends GridDimensionDefinition {
  217. heightPixels: number;
  218. height: number;
  219. heightType: number;
  220. }
  221. class ColumnDefinition extends GridDimensionDefinition {
  222. widthPixels: number;
  223. width: number;
  224. widthType: number;
  225. }
  226. @className("GridPanelLayoutEngine", "BABYLON")
  227. /**
  228. * A grid panel layout. Grid panel is a table that has rows and columns.
  229. * When adding children to a primitive that is using grid panel layout, you must assign a GridData object to the child to indicate where the child will appear in the grid.
  230. */
  231. export class GridPanelLayoutEngine extends LayoutEngineBase {
  232. constructor(settings: { rows: [{ height: string }], columns: [{ width: string }] }) {
  233. super();
  234. this.layoutDirtyOnPropertyChangedMask = Prim2DBase.sizeProperty.flagId | Prim2DBase.actualSizeProperty.flagId;
  235. this._rows = new Array<RowDefinition>();
  236. this._columns = new Array<ColumnDefinition>();
  237. if (settings.rows) {
  238. for (let row of settings.rows) {
  239. let r = new RowDefinition();
  240. r._parse(row.height, (v, vp, t) => {
  241. r.height = v;
  242. r.heightPixels = vp;
  243. r.heightType = t;
  244. });
  245. this._rows.push(r);
  246. }
  247. }
  248. if (settings.columns) {
  249. for (let col of settings.columns) {
  250. let r = new ColumnDefinition();
  251. r._parse(col.width, (v, vp, t) => {
  252. r.width = v;
  253. r.widthPixels = vp;
  254. r.widthType = t;
  255. });
  256. this._columns.push(r);
  257. }
  258. }
  259. }
  260. private _rows: Array<RowDefinition>;
  261. private _columns: Array<ColumnDefinition>;
  262. private _children: Prim2DBase[][] = [];
  263. private _rowBottoms: Array<number> = [];
  264. private _columnLefts: Array<number> = [];
  265. private _rowHeights: Array<number> = [];
  266. private _columnWidths: Array<number> = [];
  267. private static dstOffset = Vector4.Zero();
  268. private static dstArea = Size.Zero();
  269. public updateLayout(prim: Prim2DBase) {
  270. if (prim._isFlagSet(SmartPropertyPrim.flagLayoutDirty)) {
  271. for (let child of prim.children) {
  272. if (child._isFlagSet(SmartPropertyPrim.flagNoPartOfLayout)) {
  273. continue;
  274. }
  275. if (child._hasMargin) {
  276. child.margin.computeWithAlignment(prim.layoutArea, child.actualSize, child.marginAlignment, child.actualScale, GridPanelLayoutEngine.dstOffset, GridPanelLayoutEngine.dstArea, true);
  277. child.layoutArea = GridPanelLayoutEngine.dstArea.clone();
  278. } else {
  279. child.margin.computeArea(child.actualSize, child.layoutArea);
  280. }
  281. }
  282. this._updateGrid(prim);
  283. let _children = this._children;
  284. let rl = this._rows.length;
  285. let cl = this._columns.length;
  286. let columnWidth = 0;
  287. let rowHeight = 0;
  288. let layoutArea = new BABYLON.Size(0, 0);
  289. for(let i = 0; i < _children.length; i++){
  290. let children = _children[i];
  291. if(children){
  292. let bottom = this._rowBottoms[i];
  293. let rowHeight = this._rowHeights[i];
  294. let oBottom = bottom;
  295. let oRowHeight = rowHeight;
  296. for(let j = 0; j < children.length; j++){
  297. let left = this._columnLefts[j];
  298. let columnWidth = this._columnWidths[j];
  299. let child = children[j];
  300. if(child){
  301. let gd = <GridData>child.layoutData;
  302. if(gd.columnSpan > 1){
  303. for(let k = j+1; k < gd.columnSpan + j && k < cl; k++){
  304. columnWidth += this._columnWidths[k];
  305. }
  306. }
  307. if(gd.rowSpan > 1){
  308. for(let k = i+1; k < gd.rowSpan + i && k < rl; k++){
  309. rowHeight += this._rowHeights[k];
  310. bottom = this._rowBottoms[k];
  311. }
  312. }
  313. layoutArea.width = columnWidth;
  314. layoutArea.height = rowHeight;
  315. child.margin.computeWithAlignment(layoutArea, child.actualSize, child.marginAlignment, child.actualScale, GridPanelLayoutEngine.dstOffset, GridPanelLayoutEngine.dstArea);
  316. child.layoutAreaPos = new BABYLON.Vector2(left + GridPanelLayoutEngine.dstOffset.x, bottom + GridPanelLayoutEngine.dstOffset.y);
  317. bottom = oBottom;
  318. rowHeight = oRowHeight;
  319. }
  320. }
  321. }
  322. }
  323. prim._clearFlags(SmartPropertyPrim.flagLayoutDirty);
  324. }
  325. }
  326. get isChildPositionAllowed(): boolean {
  327. return false;
  328. }
  329. private _getMaxChildHeightInRow(rowNum:number):number{
  330. let rows = this._rows;
  331. let cl = this._columns.length;
  332. let rl = this._rows.length;
  333. let children = this._children;
  334. let row = rows[rowNum];
  335. let maxHeight = 0;
  336. if(children && children[rowNum]){
  337. for(let i = 0; i < cl; i++){
  338. let child = children[rowNum][i];
  339. if(child){
  340. let span = (<GridData>child.layoutData).rowSpan;
  341. if(maxHeight < child.layoutArea.height/span){
  342. maxHeight = child.layoutArea.height/span;
  343. }
  344. }
  345. }
  346. }
  347. return maxHeight;
  348. }
  349. private _getMaxChildWidthInColumn(colNum:number):number{
  350. let columns = this._columns;
  351. let cl = this._columns.length;
  352. let rl = this._rows.length;
  353. let children = this._children;
  354. let column = columns[colNum];
  355. let maxWidth = 0;
  356. if(children){
  357. for(let i = 0; i < rl; i++){
  358. if(children[i]){
  359. let child = children[i][colNum];
  360. if(child){
  361. let span = (<GridData>child.layoutData).columnSpan;
  362. if(maxWidth < child.layoutArea.width/span){
  363. maxWidth = child.layoutArea.width/span;
  364. }
  365. }
  366. }
  367. }
  368. }
  369. return maxWidth;
  370. }
  371. private _updateGrid(prim:Prim2DBase){
  372. let _children = this._children;
  373. //remove prim.children from _children
  374. for(let i = 0; i < _children.length; i++){
  375. let children = _children[i];
  376. if(children){
  377. children.length = 0;
  378. }
  379. }
  380. let childrenThatSpan:Array<Prim2DBase>;
  381. //add prim.children to _children
  382. for(let child of prim.children){
  383. if(!child.layoutData){
  384. continue;
  385. }
  386. let gd = <GridData>child.layoutData;
  387. if(!_children[gd.row]){
  388. _children[gd.row] = [];
  389. }
  390. if(gd.columnSpan == 1 && gd.rowSpan == 1){
  391. _children[gd.row][gd.column] = child;
  392. }else{
  393. if(!childrenThatSpan){
  394. childrenThatSpan = [];
  395. }
  396. //when children span, we need to add them to _children whereever they span to so that
  397. //_getMaxChildHeightInRow and _getMaxChildWidthInColumn will work correctly.
  398. childrenThatSpan.push(child);
  399. for(let i = gd.row; i < gd.row + gd.rowSpan; i++){
  400. for(let j = gd.column; j < gd.column + gd.columnSpan; j++){
  401. _children[i][j] = child;
  402. }
  403. }
  404. }
  405. }
  406. let rows = this._rows;
  407. let columns = this._columns;
  408. let rl = this._rows.length;
  409. let cl = this._columns.length;
  410. //get fixed and auto row heights first
  411. var starIndexes = [];
  412. var totalStars = 0;
  413. var rowHeights = 0;
  414. let columnWidths = 0;
  415. for (let i = 0; i < rl; i++) {
  416. let row = this._rows[i];
  417. if(row.heightType == GridDimensionDefinition.Auto){
  418. this._rowHeights[i] = this._getMaxChildHeightInRow(i);
  419. rowHeights += this._rowHeights[i];
  420. }else if(row.heightType == GridDimensionDefinition.Pixels){
  421. let maxChildHeight = this._getMaxChildHeightInRow(i);
  422. this._rowHeights[i] = Math.max(row.heightPixels, maxChildHeight);
  423. rowHeights += this._rowHeights[i];
  424. }else if(row.heightType == GridDimensionDefinition.Stars){
  425. starIndexes.push(i);
  426. totalStars += row.height;
  427. }
  428. }
  429. //star row heights
  430. if(starIndexes.length > 0){
  431. let remainingHeight = prim.contentArea.height - rowHeights;
  432. for(let i = 0; i < starIndexes.length; i++){
  433. let rowIndex = starIndexes[i];
  434. let starHeight = (this._rows[rowIndex].height / totalStars) * remainingHeight;
  435. let maxChildHeight = this._getMaxChildHeightInRow(i);
  436. this._rowHeights[rowIndex] = Math.max(starHeight, maxChildHeight);
  437. }
  438. }
  439. //get fixed and auto column widths
  440. starIndexes.length = 0;
  441. totalStars = 0;
  442. for (let i = 0; i < cl; i++) {
  443. let column = this._columns[i];
  444. if(column.widthType == GridDimensionDefinition.Auto){
  445. this._columnWidths[i] = this._getMaxChildWidthInColumn(i);
  446. columnWidths += this._columnWidths[i];
  447. }else if(column.widthType == GridDimensionDefinition.Pixels){
  448. let maxChildWidth = this._getMaxChildWidthInColumn(i);
  449. this._columnWidths[i] = Math.max(column.widthPixels, maxChildWidth);
  450. columnWidths += this._columnWidths[i];
  451. }else if(column.widthType == GridDimensionDefinition.Stars){
  452. starIndexes.push(i);
  453. totalStars += column.width;
  454. }
  455. }
  456. //star column widths
  457. if(starIndexes.length > 0){
  458. let remainingWidth = prim.contentArea.width - columnWidths;
  459. for(let i = 0; i < starIndexes.length; i++){
  460. let columnIndex = starIndexes[i];
  461. let starWidth = (this._columns[columnIndex].width / totalStars) * remainingWidth;
  462. let maxChildWidth = this._getMaxChildWidthInColumn(i);
  463. this._columnWidths[columnIndex] = Math.max(starWidth, maxChildWidth);
  464. }
  465. }
  466. let y = 0;
  467. this._rowBottoms[rl - 1] = y;
  468. for (let i = rl - 2; i >= 0; i--) {
  469. y += this._rowHeights[i+1];
  470. this._rowBottoms[i] = y;
  471. }
  472. let x = 0;
  473. this._columnLefts[0] = x;
  474. for (let i = 1; i < cl; i++) {
  475. x += this._columnWidths[i-1];
  476. this._columnLefts[i] = x;
  477. }
  478. //remove duplicate references to children that span
  479. if(childrenThatSpan){
  480. for(var i = 0; i < childrenThatSpan.length; i++){
  481. let child = childrenThatSpan[i];
  482. let gd = <GridData>child.layoutData;
  483. for(let i = gd.row; i < gd.row + gd.rowSpan; i++){
  484. for(let j = gd.column; j < gd.column + gd.columnSpan; j++){
  485. if(i == gd.row && j == gd.column){
  486. continue;
  487. }
  488. if(_children[i][j] == child){
  489. _children[i][j] = null;
  490. }
  491. }
  492. }
  493. }
  494. }
  495. }
  496. }
  497. }