babylon.canvas2dLayoutEngine.ts 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724
  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. @logMethod()
  53. public updateLayout(prim: Prim2DBase) {
  54. // If this prim is layoutDiry we update its layoutArea and also the one of its direct children
  55. if (prim._isFlagSet(SmartPropertyPrim.flagLayoutDirty)) {
  56. prim._clearFlags(SmartPropertyPrim.flagLayoutDirty);
  57. for (let child of prim.children) {
  58. this._doUpdate(child);
  59. }
  60. }
  61. }
  62. @logMethod()
  63. private _doUpdate(prim: Prim2DBase) {
  64. // Canvas ?
  65. if (prim instanceof Canvas2D) {
  66. prim.layoutArea = prim.actualSize; //.multiplyByFloats(prim.scaleX, prim.scaleY);
  67. }
  68. // Direct child of Canvas ?
  69. else if (prim.parent instanceof Canvas2D) {
  70. prim.layoutArea = prim.owner.actualSize; //.multiplyByFloats(prim.owner.scaleX, prim.owner.scaleY);
  71. }
  72. // Indirect child of Canvas
  73. else {
  74. let contentArea = prim.parent.contentArea;
  75. // Can be null if the parent's content area depend of its children, the computation will be done in many passes
  76. if (contentArea) {
  77. prim.layoutArea = contentArea;
  78. }
  79. }
  80. C2DLogging.setPostMessage(() => `Prim: ${prim.id} has layoutArea: ${prim.layoutArea}`);
  81. }
  82. get isChildPositionAllowed(): boolean {
  83. return true;
  84. }
  85. }
  86. @className("StackPanelLayoutEngine", "BABYLON")
  87. /**
  88. * A stack panel layout. Primitive will be stack either horizontally or vertically.
  89. * 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.
  90. */
  91. export class StackPanelLayoutEngine extends LayoutEngineBase {
  92. constructor() {
  93. super();
  94. this.layoutDirtyOnPropertyChangedMask = Prim2DBase.sizeProperty.flagId | Prim2DBase.actualSizeProperty.flagId;
  95. }
  96. public static get Horizontal(): StackPanelLayoutEngine {
  97. if (!StackPanelLayoutEngine._horizontal) {
  98. StackPanelLayoutEngine._horizontal = new StackPanelLayoutEngine();
  99. StackPanelLayoutEngine._horizontal.isHorizontal = true;
  100. StackPanelLayoutEngine._horizontal.lock();
  101. }
  102. return StackPanelLayoutEngine._horizontal;
  103. }
  104. public static get Vertical(): StackPanelLayoutEngine {
  105. if (!StackPanelLayoutEngine._vertical) {
  106. StackPanelLayoutEngine._vertical = new StackPanelLayoutEngine();
  107. StackPanelLayoutEngine._vertical.isHorizontal = false;
  108. StackPanelLayoutEngine._vertical.lock();
  109. }
  110. return StackPanelLayoutEngine._vertical;
  111. }
  112. private static _horizontal: StackPanelLayoutEngine = null;
  113. private static _vertical: StackPanelLayoutEngine = null;
  114. get isHorizontal(): boolean {
  115. return this._isHorizontal;
  116. }
  117. set isHorizontal(val: boolean) {
  118. if (this.isLocked()) {
  119. return;
  120. }
  121. this._isHorizontal = val;
  122. }
  123. private _isHorizontal: boolean = true;
  124. private static stackPanelLayoutArea = Size.Zero();
  125. private static dstOffset = Vector4.Zero();
  126. private static dstArea = Size.Zero();
  127. private static computeCounter = 0;
  128. public updateLayout(prim: Prim2DBase) {
  129. if (prim._isFlagSet(SmartPropertyPrim.flagLayoutDirty)) {
  130. let primLayoutArea = prim.layoutArea;
  131. let isSizeAuto = prim.isSizeAuto;
  132. // If we're not in autoSize the layoutArea of the prim having the stack panel must be computed in order for us to compute the children' position.
  133. // If there's at least one auto size (Horizontal or Vertical) we will have to figure the layoutArea ourselves
  134. if (!primLayoutArea && !isSizeAuto) {
  135. return;
  136. }
  137. // console.log("Compute Stack Panel Layout " + ++StackPanelLayoutEngine.computeCounter);
  138. let x = 0;
  139. let y = 0;
  140. let horizonStackPanel = this.isHorizontal;
  141. // If the stack panel is horizontal we check if the primitive height is auto or not, if it's auto then we have to compute the required height, otherwise we just take the actualHeight. If the stack panel is vertical we do the same but with width
  142. let max = 0;
  143. let stackPanelLayoutArea = StackPanelLayoutEngine.stackPanelLayoutArea;
  144. if (horizonStackPanel) {
  145. if (prim.isVerticalSizeAuto) {
  146. max = 0;
  147. stackPanelLayoutArea.height = 0;
  148. } else {
  149. max = prim.layoutArea.height;
  150. stackPanelLayoutArea.height = prim.layoutArea.height;
  151. stackPanelLayoutArea.width = 0;
  152. }
  153. } else {
  154. if (prim.isHorizontalSizeAuto) {
  155. max = 0;
  156. stackPanelLayoutArea.width = 0;
  157. } else {
  158. max = prim.layoutArea.width;
  159. stackPanelLayoutArea.width = prim.layoutArea.width;
  160. stackPanelLayoutArea.height = 0;
  161. }
  162. }
  163. for (let child of prim.children) {
  164. if (child._isFlagSet(SmartPropertyPrim.flagNoPartOfLayout)) {
  165. continue;
  166. }
  167. if (child._hasMargin) {
  168. // Calling computeWithAlignment will return us the area taken by "child" which is its layoutArea
  169. // We also have the dstOffset which will give us the y position in horizontal mode or x position in vertical mode.
  170. // The alignment offset on the other axis is simply ignored as it doesn't make any sense (e.g. horizontal alignment is ignored in horizontal mode)
  171. child.margin.computeWithAlignment(stackPanelLayoutArea, child.actualSize, child.marginAlignment, child.actualScale, StackPanelLayoutEngine.dstOffset, StackPanelLayoutEngine.dstArea, true);
  172. child.layoutArea = StackPanelLayoutEngine.dstArea;
  173. } else {
  174. child.margin.computeArea(child.actualSize, child.actualScale, StackPanelLayoutEngine.dstArea);
  175. child.layoutArea = StackPanelLayoutEngine.dstArea;
  176. }
  177. max = Math.max(max, horizonStackPanel ? StackPanelLayoutEngine.dstArea.height : StackPanelLayoutEngine.dstArea.width);
  178. }
  179. for (let child of prim.children) {
  180. if (child._isFlagSet(SmartPropertyPrim.flagNoPartOfLayout)) {
  181. continue;
  182. }
  183. let layoutArea = child.layoutArea;
  184. if (horizonStackPanel) {
  185. child.layoutAreaPos = new Vector2(x, 0);
  186. x += layoutArea.width;
  187. child.layoutArea = new Size(layoutArea.width, max);
  188. } else {
  189. child.layoutAreaPos = new Vector2(0, y);
  190. y += layoutArea.height;
  191. child.layoutArea = new Size(max, layoutArea.height);
  192. }
  193. }
  194. prim._clearFlags(SmartPropertyPrim.flagLayoutDirty);
  195. }
  196. }
  197. get isChildPositionAllowed(): boolean {
  198. return false;
  199. }
  200. }
  201. /**
  202. * GridData is used specify what row(s) and column(s) a primitive is placed in when its parent is using a Grid Panel Layout.
  203. */
  204. export class GridData implements ILayoutData{
  205. /**
  206. * the row number of the grid
  207. **/
  208. public row:number;
  209. /**
  210. * the column number of the grid
  211. **/
  212. public column:number;
  213. /**
  214. * the number of rows a primitive will occupy
  215. **/
  216. public rowSpan:number;
  217. /**
  218. * the number of columns a primitive will occupy
  219. **/
  220. public columnSpan:number;
  221. /**
  222. * Create a Grid Data that describes where a primitive will be placed in a Grid Panel Layout.
  223. * @param row the row number of the grid
  224. * @param column the column number of the grid
  225. * @param rowSpan the number of rows a primitive will occupy
  226. * @param columnSpan the number of columns a primitive will occupy
  227. **/
  228. constructor(row:number, column:number, rowSpan?:number, columnSpan?:number){
  229. this.row = row;
  230. this.column = column;
  231. this.rowSpan = (rowSpan == null) ? 1 : rowSpan;
  232. this.columnSpan = (columnSpan == null) ? 1 : columnSpan;
  233. }
  234. }
  235. class GridDimensionDefinition {
  236. public static Pixels = 1;
  237. public static Stars = 2;
  238. public static Auto = 3;
  239. _parse(value: string, res: (v: number, vp: number, t: number) => void) {
  240. let v = value.toLocaleLowerCase().trim();
  241. if (v.indexOf("auto") === 0) {
  242. res(null, null, GridDimensionDefinition.Auto);
  243. } else if (v.indexOf("*") !== -1) {
  244. let i = v.indexOf("*");
  245. let w = 1;
  246. if(i > 0){
  247. w = parseFloat(v.substr(0, i));
  248. }
  249. res(w, null, GridDimensionDefinition.Stars);
  250. } else {
  251. let w = parseFloat(v);
  252. res(w, w, GridDimensionDefinition.Pixels);
  253. }
  254. }
  255. }
  256. class RowDefinition extends GridDimensionDefinition {
  257. heightPixels: number;
  258. height: number;
  259. heightType: number;
  260. }
  261. class ColumnDefinition extends GridDimensionDefinition {
  262. widthPixels: number;
  263. width: number;
  264. widthType: number;
  265. }
  266. @className("GridPanelLayoutEngine", "BABYLON")
  267. /**
  268. * A grid panel layout. Grid panel is a table that has rows and columns.
  269. * 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.
  270. */
  271. export class GridPanelLayoutEngine extends LayoutEngineBase {
  272. constructor(settings: { rows: [{ height: string }], columns: [{ width: string }] }) {
  273. super();
  274. this.layoutDirtyOnPropertyChangedMask = Prim2DBase.sizeProperty.flagId | Prim2DBase.actualSizeProperty.flagId;
  275. this._rows = new Array<RowDefinition>();
  276. this._columns = new Array<ColumnDefinition>();
  277. if (settings.rows) {
  278. for (let row of settings.rows) {
  279. let r = new RowDefinition();
  280. r._parse(row.height, (v, vp, t) => {
  281. r.height = v;
  282. r.heightPixels = vp;
  283. r.heightType = t;
  284. });
  285. this._rows.push(r);
  286. }
  287. }
  288. if (settings.columns) {
  289. for (let col of settings.columns) {
  290. let r = new ColumnDefinition();
  291. r._parse(col.width, (v, vp, t) => {
  292. r.width = v;
  293. r.widthPixels = vp;
  294. r.widthType = t;
  295. });
  296. this._columns.push(r);
  297. }
  298. }
  299. }
  300. private _rows: Array<RowDefinition>;
  301. private _columns: Array<ColumnDefinition>;
  302. private _children: Prim2DBase[][] = [];
  303. private _rowBottoms: Array<number> = [];
  304. private _columnLefts: Array<number> = [];
  305. private _rowHeights: Array<number> = [];
  306. private _columnWidths: Array<number> = [];
  307. private static dstOffset = Vector4.Zero();
  308. private static dstArea = Size.Zero();
  309. private static dstAreaPos = Vector2.Zero();
  310. public updateLayout(prim: Prim2DBase) {
  311. if (prim._isFlagSet(SmartPropertyPrim.flagLayoutDirty)) {
  312. if (!prim.layoutArea) {
  313. return;
  314. }
  315. for (let child of prim.children) {
  316. if (child._isFlagSet(SmartPropertyPrim.flagNoPartOfLayout)) {
  317. continue;
  318. }
  319. if (child._hasMargin) {
  320. child.margin.computeWithAlignment(prim.layoutArea, child.actualSize, child.marginAlignment, child.actualScale, GridPanelLayoutEngine.dstOffset, GridPanelLayoutEngine.dstArea, true);
  321. } else {
  322. child.margin.computeArea(child.actualSize, child.actualScale, GridPanelLayoutEngine.dstArea);
  323. }
  324. child.layoutArea = GridPanelLayoutEngine.dstArea;
  325. }
  326. this._updateGrid(prim);
  327. let _children = this._children;
  328. let rl = this._rows.length;
  329. let cl = this._columns.length;
  330. let columnWidth = 0;
  331. let rowHeight = 0;
  332. let dstArea = GridPanelLayoutEngine.dstArea;
  333. let dstAreaPos = GridPanelLayoutEngine.dstAreaPos;
  334. for(let i = 0; i < _children.length; i++){
  335. let children = _children[i];
  336. if(children){
  337. let bottom = this._rowBottoms[i];
  338. let rowHeight = this._rowHeights[i];
  339. let oBottom = bottom;
  340. let oRowHeight = rowHeight;
  341. for(let j = 0; j < children.length; j++){
  342. let left = this._columnLefts[j];
  343. let columnWidth = this._columnWidths[j];
  344. let child = children[j];
  345. if(child){
  346. let gd = <GridData>child.layoutData;
  347. if(gd.columnSpan > 1){
  348. for(let k = j+1; k < gd.columnSpan + j && k < cl; k++){
  349. columnWidth += this._columnWidths[k];
  350. }
  351. }
  352. if(gd.rowSpan > 1){
  353. for(let k = i+1; k < gd.rowSpan + i && k < rl; k++){
  354. rowHeight += this._rowHeights[k];
  355. bottom = this._rowBottoms[k];
  356. }
  357. }
  358. dstArea.width = columnWidth;
  359. dstArea.height = rowHeight;
  360. child.layoutArea = dstArea;
  361. dstAreaPos.x = left;
  362. dstAreaPos.y = bottom;
  363. child.layoutAreaPos = dstAreaPos;
  364. bottom = oBottom;
  365. rowHeight = oRowHeight;
  366. }
  367. }
  368. }
  369. }
  370. prim._clearFlags(SmartPropertyPrim.flagLayoutDirty);
  371. }
  372. }
  373. get isChildPositionAllowed(): boolean {
  374. return false;
  375. }
  376. private _getMaxChildHeightInRow(rowNum:number):number{
  377. let rows = this._rows;
  378. let cl = this._columns.length;
  379. let rl = this._rows.length;
  380. let children = this._children;
  381. let row = rows[rowNum];
  382. let maxHeight = 0;
  383. if(children && children[rowNum]){
  384. for(let i = 0; i < cl; i++){
  385. let child = children[rowNum][i];
  386. if(child){
  387. let span = (<GridData>child.layoutData).rowSpan;
  388. if(maxHeight < child.layoutArea.height/span){
  389. maxHeight = child.layoutArea.height/span;
  390. }
  391. }
  392. }
  393. }
  394. return maxHeight;
  395. }
  396. private _getMaxChildWidthInColumn(colNum:number):number{
  397. let columns = this._columns;
  398. let cl = this._columns.length;
  399. let rl = this._rows.length;
  400. let children = this._children;
  401. let column = columns[colNum];
  402. let maxWidth = 0;
  403. if(children){
  404. for(let i = 0; i < rl; i++){
  405. if(children[i]){
  406. let child = children[i][colNum];
  407. if(child){
  408. let span = (<GridData>child.layoutData).columnSpan;
  409. if(maxWidth < child.layoutArea.width/span){
  410. maxWidth = child.layoutArea.width/span;
  411. }
  412. }
  413. }
  414. }
  415. }
  416. return maxWidth;
  417. }
  418. private _updateGrid(prim:Prim2DBase){
  419. let _children = this._children;
  420. //remove prim.children from _children
  421. for(let i = 0; i < _children.length; i++){
  422. let children = _children[i];
  423. if(children){
  424. children.length = 0;
  425. }
  426. }
  427. let childrenThatSpan:Array<Prim2DBase>;
  428. //add prim.children to _children
  429. for(let child of prim.children){
  430. if(!child.layoutData){
  431. continue;
  432. }
  433. let gd = <GridData>child.layoutData;
  434. if(!_children[gd.row]){
  435. _children[gd.row] = [];
  436. }
  437. if(gd.columnSpan == 1 && gd.rowSpan == 1){
  438. _children[gd.row][gd.column] = child;
  439. }else{
  440. if(!childrenThatSpan){
  441. childrenThatSpan = [];
  442. }
  443. //when children span, we need to add them to _children whereever they span to so that
  444. //_getMaxChildHeightInRow and _getMaxChildWidthInColumn will work correctly.
  445. childrenThatSpan.push(child);
  446. for(let i = gd.row; i < gd.row + gd.rowSpan; i++){
  447. for(let j = gd.column; j < gd.column + gd.columnSpan; j++){
  448. _children[i][j] = child;
  449. }
  450. }
  451. }
  452. }
  453. let rows = this._rows;
  454. let columns = this._columns;
  455. let rl = this._rows.length;
  456. let cl = this._columns.length;
  457. //get fixed and auto row heights first
  458. var starIndexes = [];
  459. var totalStars = 0;
  460. var rowHeights = 0;
  461. let columnWidths = 0;
  462. for (let i = 0; i < rl; i++) {
  463. let row = this._rows[i];
  464. if(row.heightType == GridDimensionDefinition.Auto){
  465. this._rowHeights[i] = this._getMaxChildHeightInRow(i);
  466. rowHeights += this._rowHeights[i];
  467. }else if(row.heightType == GridDimensionDefinition.Pixels){
  468. let maxChildHeight = this._getMaxChildHeightInRow(i);
  469. this._rowHeights[i] = Math.max(row.heightPixels, maxChildHeight);
  470. rowHeights += this._rowHeights[i];
  471. }else if(row.heightType == GridDimensionDefinition.Stars){
  472. starIndexes.push(i);
  473. totalStars += row.height;
  474. }
  475. }
  476. //star row heights
  477. if(starIndexes.length > 0){
  478. let remainingHeight = prim.contentArea.height - rowHeights;
  479. for(let i = 0; i < starIndexes.length; i++){
  480. let rowIndex = starIndexes[i];
  481. let starHeight = (this._rows[rowIndex].height / totalStars) * remainingHeight;
  482. let maxChildHeight = this._getMaxChildHeightInRow(i);
  483. this._rowHeights[rowIndex] = Math.max(starHeight, maxChildHeight);
  484. }
  485. }
  486. //get fixed and auto column widths
  487. starIndexes.length = 0;
  488. totalStars = 0;
  489. for (let i = 0; i < cl; i++) {
  490. let column = this._columns[i];
  491. if(column.widthType == GridDimensionDefinition.Auto){
  492. this._columnWidths[i] = this._getMaxChildWidthInColumn(i);
  493. columnWidths += this._columnWidths[i];
  494. }else if(column.widthType == GridDimensionDefinition.Pixels){
  495. let maxChildWidth = this._getMaxChildWidthInColumn(i);
  496. this._columnWidths[i] = Math.max(column.widthPixels, maxChildWidth);
  497. columnWidths += this._columnWidths[i];
  498. }else if(column.widthType == GridDimensionDefinition.Stars){
  499. starIndexes.push(i);
  500. totalStars += column.width;
  501. }
  502. }
  503. //star column widths
  504. if(starIndexes.length > 0){
  505. let remainingWidth = prim.contentArea.width - columnWidths;
  506. for(let i = 0; i < starIndexes.length; i++){
  507. let columnIndex = starIndexes[i];
  508. let starWidth = (this._columns[columnIndex].width / totalStars) * remainingWidth;
  509. let maxChildWidth = this._getMaxChildWidthInColumn(i);
  510. this._columnWidths[columnIndex] = Math.max(starWidth, maxChildWidth);
  511. }
  512. }
  513. let y = 0;
  514. this._rowBottoms[rl - 1] = y;
  515. for (let i = rl - 2; i >= 0; i--) {
  516. y += this._rowHeights[i+1];
  517. this._rowBottoms[i] = y;
  518. }
  519. let x = 0;
  520. this._columnLefts[0] = x;
  521. for (let i = 1; i < cl; i++) {
  522. x += this._columnWidths[i-1];
  523. this._columnLefts[i] = x;
  524. }
  525. //remove duplicate references to children that span
  526. if(childrenThatSpan){
  527. for(var i = 0; i < childrenThatSpan.length; i++){
  528. let child = childrenThatSpan[i];
  529. let gd = <GridData>child.layoutData;
  530. for(let i = gd.row; i < gd.row + gd.rowSpan; i++){
  531. for(let j = gd.column; j < gd.column + gd.columnSpan; j++){
  532. if(i == gd.row && j == gd.column){
  533. continue;
  534. }
  535. if(_children[i][j] == child){
  536. _children[i][j] = null;
  537. }
  538. }
  539. }
  540. }
  541. }
  542. }
  543. }
  544. }