@meonode/canvas 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,275 +1,363 @@
1
1
  import { RowNode } from './layout.canvas.util.js';
2
2
  import { Style } from '../constant/common.const.js';
3
+ import { parsePercentage } from './canvas.helper.js';
3
4
 
4
- // TODO: Add comprehensive unit tests for this file.
5
5
  /**
6
- * Grid layout node that arranges children in a configurable number of columns or rows.
7
- * Uses Yoga's flexbox capabilities with wrapping and gap properties to simulate a grid.
8
- * @extends RowNode
6
+ * Grid layout node that arranges children in a 2D grid.
7
+ * Implements a simplified version of the CSS Grid Layout algorithm.
9
8
  */
10
9
  class GridNode extends RowNode {
11
- columns;
12
- columnGapValue;
13
- rowGapValue;
14
- isVertical; // True if the main axis is vertical (flexDirection: column or column-reverse)
15
10
  /**
16
11
  * Creates a new grid layout node
17
12
  * @param props Grid configuration properties
18
13
  */
19
14
  constructor(props) {
20
- const columns = Math.max(1, props.columns || 1);
21
- const direction = props.direction || 'row'; // Default to horizontal row
22
- const isVertical = direction === 'column' || direction === 'column-reverse';
23
- // Map direction string to Yoga FlexDirection
24
- let flexDirection;
25
- switch (direction) {
26
- case 'row':
27
- flexDirection = Style.FlexDirection.Row;
28
- break;
29
- case 'column':
30
- flexDirection = Style.FlexDirection.Column;
31
- break;
32
- case 'row-reverse':
33
- flexDirection = Style.FlexDirection.RowReverse;
34
- break;
35
- case 'column-reverse':
36
- flexDirection = Style.FlexDirection.ColumnReverse;
37
- break;
38
- default:
39
- console.warn(`[GridNode] Invalid direction "${direction}". Defaulting to "row".`);
40
- flexDirection = Style.FlexDirection.Row;
41
- }
42
- // Determine the column and row gap values from props
43
- let columnGap = 0;
44
- let rowGap = 0;
45
- if (typeof props.gap === 'number' || (typeof props.gap === 'string' && props.gap.trim() !== '')) {
46
- // Single value applies to both row and column gaps
47
- columnGap = props.gap;
48
- rowGap = props.gap;
49
- }
50
- else if (props.gap && typeof props.gap === 'object') {
51
- // Object format: prioritize a specific direction (Column/Row), then All
52
- columnGap = props.gap.Column ?? props.gap.All ?? 0;
53
- rowGap = props.gap.Row ?? props.gap.All ?? 0;
54
- }
55
15
  super({
56
- name: 'Grid',
57
- flexWrap: Style.Wrap.Wrap, // Essential for grid behavior
58
- flexDirection,
59
16
  ...props,
60
- // Explicitly remove the 'direction' prop passed to super, as it's handled by flexDirection
61
- direction: undefined,
62
- // Pass undefined for gap to prevent BoxNode from trying to parse it
63
- gap: undefined,
17
+ name: props.name || 'Grid',
18
+ flexWrap: Style.Wrap.Wrap,
64
19
  });
65
- this.columns = columns;
66
- this.columnGapValue = columnGap;
67
- this.rowGapValue = rowGap;
68
- this.isVertical = isVertical;
69
- // Explicitly set gaps on this.node after super() call
70
- // These will be updated again in updateLayoutBasedOnComputedSize, but this ensures initial setup
71
- if (typeof columnGap === 'number') {
72
- this.node.setGap(Style.Gutter.Column, columnGap);
73
- }
74
- else if (typeof columnGap === 'string' && columnGap.endsWith('%')) {
75
- this.node.setGapPercent(Style.Gutter.Column, parseFloat(columnGap));
20
+ }
21
+ /**
22
+ * Helper to parse a track size definition.
23
+ */
24
+ parseTrack(track, availableSpace) {
25
+ if (typeof track === 'number') {
26
+ return { type: 'px', value: track };
76
27
  }
77
- if (typeof rowGap === 'number') {
78
- this.node.setGap(Style.Gutter.Row, rowGap);
28
+ if (track === 'auto') {
29
+ return { type: 'auto', value: 0 };
79
30
  }
80
- else if (typeof rowGap === 'string' && rowGap.endsWith('%')) {
81
- this.node.setGapPercent(Style.Gutter.Row, parseFloat(rowGap));
31
+ if (typeof track === 'string') {
32
+ if (track.endsWith('fr')) {
33
+ return { type: 'fr', value: parseFloat(track) };
34
+ }
35
+ if (track.endsWith('%')) {
36
+ return { type: '%', value: parsePercentage(track, availableSpace) };
37
+ }
38
+ // Try parsing as number (px) if just string "100"
39
+ const num = parseFloat(track);
40
+ if (!isNaN(num))
41
+ return { type: 'px', value: num };
82
42
  }
43
+ return { type: 'auto', value: 0 };
83
44
  }
84
45
  /**
85
- * Appends a child node to this grid.
86
- * Overridden primarily for documentation/clarity, functionality is inherited.
87
- * @param child Child node to append
88
- * @param index Index at which to insert the child
46
+ * Parses the gap property into pixels.
89
47
  */
90
- appendChild(child, index) {
91
- super.appendChild(child, index);
48
+ getGapPixels(gap, width, height) {
49
+ let rowGap = 0;
50
+ let colGap = 0;
51
+ if (typeof gap === 'number') {
52
+ rowGap = colGap = gap;
53
+ }
54
+ else if (typeof gap === 'string') {
55
+ const val = parsePercentage(gap, width); // Use width as base for simplicity if %
56
+ rowGap = colGap = val;
57
+ }
58
+ else if (gap && typeof gap === 'object') {
59
+ const colVal = gap.Column ?? gap.All ?? 0;
60
+ const rowVal = gap.Row ?? gap.All ?? 0;
61
+ colGap = parsePercentage(colVal, width);
62
+ rowGap = parsePercentage(rowVal, height);
63
+ }
64
+ return { rowGap, colGap };
92
65
  }
93
66
  /**
94
67
  * Update layout calculations after the initial layout is computed.
95
- * This method calculates the appropriate flex-basis for children based on the
96
- * number of columns and gaps, respecting the container's padding,
97
- * and applies the gaps using Yoga's built-in properties.
98
68
  */
99
69
  updateLayoutBasedOnComputedSize() {
100
- // Step 1: Early return if the grid is empty or invalid
101
- if (this.columns <= 0 || this.children.length === 0) {
102
- return;
103
- }
104
- // Step 2: Get container dimensions and padding after the initial layout
70
+ // 1. Get Container Dimensions
105
71
  const width = this.node.getComputedWidth();
106
- const height = this.node.getComputedHeight();
107
72
  const paddingLeft = this.node.getComputedPadding(Style.Edge.Left);
108
73
  const paddingRight = this.node.getComputedPadding(Style.Edge.Right);
109
74
  const paddingTop = this.node.getComputedPadding(Style.Edge.Top);
110
75
  const paddingBottom = this.node.getComputedPadding(Style.Edge.Bottom);
111
- // Calculate content box dimensions
112
76
  const contentWidth = Math.max(0, width - paddingLeft - paddingRight);
113
- const contentHeight = Math.max(0, height - paddingTop - paddingBottom);
114
- // Step 3: Validate dimensions needed for calculations
115
- if (!this.isVertical && contentWidth <= 0 && width > 0) {
116
- console.warn(`[GridNode ${this.props.key} - Finalize] Grid content width (${contentWidth}) is zero or negative after accounting for padding (${paddingLeft}+${paddingRight}) on total width ${width}. Cannot calculate basis.`);
117
- if (this.columns > 1)
118
- return;
119
- }
120
- if (this.isVertical && contentHeight <= 0 && height > 0) {
121
- console.warn(`[GridNode ${this.props.key} - Finalize] Grid content height (${contentHeight}) is zero or negative after accounting for padding (${paddingTop}+${paddingBottom}) on total height ${height}. Cannot calculate basis.`);
122
- if (this.columns > 1)
123
- return;
77
+ const computedHeight = this.node.getComputedHeight();
78
+ const contentHeight = Math.max(0, computedHeight - paddingTop - paddingBottom);
79
+ const { templateColumns, templateRows, autoRows = 'auto', gap, columns } = this.props;
80
+ // 2. Resolve Gaps
81
+ const { rowGap, colGap } = this.getGapPixels(gap, contentWidth, contentHeight);
82
+ // 3. Resolve Columns (Tracks)
83
+ let explicitColTracks = templateColumns || [];
84
+ if (explicitColTracks.length === 0 && columns) {
85
+ explicitColTracks = Array(columns).fill('1fr');
124
86
  }
125
- // Step 4: Calculate Gap Values in Pixels
126
- let columnGapPixels = 0;
127
- if (typeof this.columnGapValue === 'number') {
128
- columnGapPixels = this.columnGapValue;
87
+ if (explicitColTracks.length === 0)
88
+ explicitColTracks = ['1fr'];
89
+ const resolvedColTracks = this.resolveTracks(explicitColTracks, contentWidth, colGap);
90
+ // Pre-calculate Col Offsets needed for placement/width
91
+ const colOffsetsValues = [0];
92
+ for (let i = 0; i < resolvedColTracks.length; i++) {
93
+ colOffsetsValues.push(colOffsetsValues[i] + resolvedColTracks[i] + colGap);
129
94
  }
130
- else if (typeof this.columnGapValue === 'string' && this.columnGapValue.trim().endsWith('%')) {
131
- try {
132
- const percent = parseFloat(this.columnGapValue);
133
- if (!isNaN(percent) && contentWidth > 0) {
134
- columnGapPixels = (percent / 100) * contentWidth;
135
- }
136
- else if (isNaN(percent)) {
137
- console.warn(`[GridNode ${this.props.key}] Invalid percentage column gap format: "${this.columnGapValue}". Using 0px.`);
95
+ // 4. Place Items & Resolve Explicit Row Tracks
96
+ const explicitRowTracks = templateRows || [];
97
+ const resolvedExplicitRowTracks = this.resolveTracks(explicitRowTracks, contentHeight, rowGap);
98
+ const cells = []; // true if occupied
99
+ const items = [];
100
+ const isOccupied = (r, c) => {
101
+ if (!cells[r])
102
+ return false;
103
+ return cells[r][c] === true;
104
+ };
105
+ const setOccupied = (r, c) => {
106
+ if (!cells[r])
107
+ cells[r] = [];
108
+ cells[r][c] = true;
109
+ };
110
+ let cursorRow = 0;
111
+ let cursorCol = 0;
112
+ for (const child of this.children) {
113
+ const childProps = child.props;
114
+ const { gridColumn, gridRow } = childProps;
115
+ let colStart;
116
+ let colEnd;
117
+ let colSpan = 1;
118
+ let rowStart;
119
+ let rowEnd;
120
+ let rowSpan = 1;
121
+ // ... Grid Placement Logic ...
122
+ if (gridColumn) {
123
+ const parts = gridColumn.split('/').map(s => s.trim());
124
+ if (parts[0]) {
125
+ if (parts[0].startsWith('span')) {
126
+ colSpan = parseInt(parts[0].replace('span', '')) || 1;
127
+ }
128
+ else {
129
+ colStart = parseInt(parts[0]) - 1;
130
+ }
138
131
  }
139
- else if (contentWidth <= 0) {
140
- console.warn(`[GridNode ${this.props.key}] Cannot calculate percentage column gap (${this.columnGapValue}) because content width is zero. Using 0px.`);
132
+ if (parts[1]) {
133
+ if (parts[1].startsWith('span')) {
134
+ const span = parseInt(parts[1].replace('span', '')) || 1;
135
+ if (colStart !== undefined) {
136
+ colEnd = colStart + span;
137
+ colSpan = span;
138
+ }
139
+ else {
140
+ // If start is undefined but end is span? Unusual. Treat as span.
141
+ colSpan = span;
142
+ }
143
+ }
144
+ else {
145
+ colEnd = parseInt(parts[1]) - 1;
146
+ if (colStart !== undefined) {
147
+ colSpan = colEnd - colStart;
148
+ }
149
+ }
141
150
  }
142
151
  }
143
- catch (e) {
144
- console.warn(`[GridNode ${this.props.key}] Error parsing percentage column gap: "${this.columnGapValue}". Using 0px.`, e);
152
+ if (gridRow) {
153
+ const parts = gridRow.split('/').map(s => s.trim());
154
+ if (parts[0]) {
155
+ if (parts[0].startsWith('span')) {
156
+ rowSpan = parseInt(parts[0].replace('span', '')) || 1;
157
+ }
158
+ else {
159
+ rowStart = parseInt(parts[0]) - 1;
160
+ }
161
+ }
162
+ if (parts[1]) {
163
+ if (parts[1].startsWith('span')) {
164
+ const span = parseInt(parts[1].replace('span', '')) || 1;
165
+ if (rowStart !== undefined) {
166
+ rowEnd = rowStart + span;
167
+ rowSpan = span;
168
+ }
169
+ else {
170
+ rowSpan = span;
171
+ }
172
+ }
173
+ else {
174
+ rowEnd = parseInt(parts[1]) - 1;
175
+ if (rowStart !== undefined) {
176
+ rowSpan = rowEnd - rowStart;
177
+ }
178
+ }
179
+ }
145
180
  }
146
- }
147
- else if (typeof this.columnGapValue === 'string' && this.columnGapValue.trim() !== '') {
148
- console.warn(`[GridNode ${this.props.key}] Unsupported string column gap format: "${this.columnGapValue}". Using 0px. Only numbers and percentages ('%') are supported.`);
149
- }
150
- let rowGapPixels = 0;
151
- if (typeof this.rowGapValue === 'number') {
152
- rowGapPixels = this.rowGapValue;
153
- }
154
- else if (typeof this.rowGapValue === 'string' && this.rowGapValue.trim().endsWith('%')) {
155
- try {
156
- const percent = parseFloat(this.rowGapValue);
157
- if (!isNaN(percent) && contentHeight > 0) {
158
- rowGapPixels = (percent / 100) * contentHeight;
181
+ if (colStart !== undefined && rowStart !== undefined) ;
182
+ else {
183
+ // Auto placement
184
+ let placed = false;
185
+ while (!placed) {
186
+ if (!cells[cursorRow])
187
+ cells[cursorRow] = [];
188
+ if (colStart !== undefined)
189
+ cursorCol = colStart;
190
+ let fits = true;
191
+ for (let r = 0; r < rowSpan; r++) {
192
+ for (let c = 0; c < colSpan; c++) {
193
+ if (isOccupied(cursorRow + r, cursorCol + c)) {
194
+ fits = false;
195
+ break;
196
+ }
197
+ }
198
+ if (!fits)
199
+ break;
200
+ }
201
+ if (fits) {
202
+ rowStart = cursorRow;
203
+ colStart = cursorCol;
204
+ placed = true;
205
+ }
206
+ else {
207
+ cursorCol++;
208
+ if (cursorCol + colSpan > resolvedColTracks.length) {
209
+ cursorCol = 0;
210
+ cursorRow++;
211
+ }
212
+ }
159
213
  }
160
- else if (isNaN(percent)) {
161
- console.warn(`[GridNode ${this.props.key}] Invalid percentage row gap format: "${this.rowGapValue}". Using 0px.`);
214
+ cursorCol += colSpan;
215
+ if (cursorCol >= resolvedColTracks.length) {
216
+ cursorCol = 0;
217
+ cursorRow++;
162
218
  }
163
- else if (contentHeight <= 0) {
164
- console.warn(`[GridNode ${this.props.key}] Cannot calculate percentage row gap (${this.rowGapValue}) because content height is zero. Using 0px.`);
219
+ }
220
+ rowEnd = (rowStart ?? 0) + rowSpan;
221
+ colEnd = (colStart ?? 0) + colSpan;
222
+ for (let r = rowStart; r < rowEnd; r++) {
223
+ for (let c = colStart; c < colEnd; c++) {
224
+ setOccupied(r, c);
165
225
  }
166
226
  }
167
- catch (e) {
168
- console.warn(`[GridNode ${this.props.key}] Error parsing percentage row gap: "${this.rowGapValue}". Using 0px.`, e);
227
+ // CRITICAL FIX: Pre-set width on item to ensure height calculation is accurate later
228
+ const itemColStart = colStart;
229
+ const itemColEnd = colEnd;
230
+ // Extend local offsets if needed for spanned columns beyond track count (rare but safe)
231
+ while (colOffsetsValues.length <= itemColEnd) {
232
+ colOffsetsValues.push(colOffsetsValues[colOffsetsValues.length - 1] + 0 + colGap);
169
233
  }
234
+ const cs = Math.min(itemColStart, colOffsetsValues.length - 1);
235
+ const ce = Math.min(itemColEnd, colOffsetsValues.length - 1);
236
+ const targetWidth = Math.max(0, colOffsetsValues[ce] - colOffsetsValues[cs] - colGap);
237
+ child.node.setWidth(targetWidth);
238
+ child.node.calculateLayout(targetWidth, Number.NaN, Style.Direction.LTR);
239
+ items.push({ node: child, rowStart: rowStart, rowEnd: rowEnd, colStart: itemColStart, colEnd: itemColEnd });
170
240
  }
171
- else if (typeof this.rowGapValue === 'string' && this.rowGapValue.trim() !== '') {
172
- console.warn(`[GridNode ${this.props.key}] Unsupported string row gap format: "${this.rowGapValue}". Using 0px. Only numbers and percentages ('%') are supported.`);
173
- }
174
- // Ensure gaps are not negative
175
- columnGapPixels = Math.max(0, columnGapPixels);
176
- rowGapPixels = Math.max(0, rowGapPixels);
177
- // Step 5: Calculate flex-basis percentage for children
178
- const mainAxisGapPixels = this.isVertical ? rowGapPixels : columnGapPixels;
179
- const mainAxisContentSize = this.isVertical ? contentHeight : contentWidth;
180
- let childWidth = 0;
181
- if (mainAxisContentSize > 0 && this.columns > 0) {
182
- // Total space taken up by gaps on the main axis
183
- const totalGapSpaceOnMainAxis = this.columns > 1 ? mainAxisGapPixels * (this.columns - 1) : 0;
184
- // Calculate the space available *only* for the items themselves
185
- const availableSpaceOnMainAxis = Math.max(0, mainAxisContentSize - totalGapSpaceOnMainAxis);
186
- // Calculate the exact pixel of the total content size that each item should occupy
187
- const exactItemWidth = availableSpaceOnMainAxis / this.columns;
188
- // Ensure it's not negative (shouldn't happen, but safety)
189
- childWidth = Math.max(0, exactItemWidth - 0.5); // Slightly reduce to avoid rounding issues
190
- }
191
- else if (this.columns === 1) {
192
- // If only one column, it takes up the full basis (gaps don't apply)
193
- childWidth = mainAxisContentSize;
194
- }
195
- // Clamp basis percentage between 0 and 100 (mostly redundant after floor/max(0) but safe)
196
- childWidth = Math.max(0, Math.min(mainAxisContentSize, childWidth));
197
- // Step 6: Apply layout properties to children
198
- let childrenNeedRecalculation = false;
199
- for (const child of this.children) {
200
- let childChanged = false;
201
- const currentLayoutWidth = child.node.getWidth();
202
- const currentWidthValue = currentLayoutWidth.value;
203
- const currentWidthUnit = currentLayoutWidth.unit;
204
- let widthNeedsUpdate = false;
205
- if (currentWidthUnit === Style.Unit.Point) {
206
- // If current width is in points, check if the value is significantly different
207
- if (Math.abs(currentWidthValue - childWidth) > 0.01) {
208
- widthNeedsUpdate = true;
241
+ // 6. Finalize Rows (Implicit)
242
+ const totalRowsNeeded = Math.max(resolvedExplicitRowTracks.length, ...items.map(i => i.rowEnd));
243
+ const resolvedRowTracks = [...resolvedExplicitRowTracks];
244
+ // Fill implicit rows
245
+ for (let r = resolvedExplicitRowTracks.length; r < totalRowsNeeded; r++) {
246
+ let rowSize = 0;
247
+ // Better 'auto' handling:
248
+ if (autoRows === 'auto') {
249
+ const rowItems = items.filter(i => i.rowStart === r && i.rowEnd - i.rowStart === 1);
250
+ for (const item of rowItems) {
251
+ rowSize = Math.max(rowSize, item.node.node.getComputedHeight());
209
252
  }
210
253
  }
211
254
  else {
212
- // If current width is not in points (e.g., Auto, Percent, Undefined), it needs to be set to points
213
- widthNeedsUpdate = true;
214
- }
215
- if (widthNeedsUpdate) {
216
- child.node.setWidth(childWidth);
217
- childChanged = true;
255
+ const parsed = this.parseTrack(autoRows, contentHeight);
256
+ rowSize = parsed.value;
218
257
  }
219
- // Ensure grow/shrink are set correctly for grid items
220
- if (child.node.getFlexGrow() !== 0) {
221
- child.node.setFlexGrow(0);
222
- childChanged = true;
258
+ resolvedRowTracks.push(rowSize);
259
+ }
260
+ // 6. Calculate Offsets (Rows) & Final Layout Application
261
+ const colOffsets = colOffsetsValues; // Re-use
262
+ const rowOffsets = [0];
263
+ for (let i = 0; i < resolvedRowTracks.length; i++) {
264
+ let size = resolvedRowTracks[i];
265
+ // Re-check auto-sized explicit rows (value 0)
266
+ if (size === 0) {
267
+ const rowItems = items.filter(it => it.rowStart === i && it.rowEnd - it.rowStart === 1);
268
+ for (const item of rowItems) {
269
+ size = Math.max(size, item.node.node.getComputedHeight());
270
+ }
271
+ resolvedRowTracks[i] = size;
223
272
  }
224
- if (child.node.getFlexShrink() !== 1) {
225
- child.node.setFlexShrink(1); // Allow shrinking
226
- childChanged = true;
273
+ rowOffsets.push(rowOffsets[i] + size + rowGap);
274
+ }
275
+ // 7. Apply Positions
276
+ let childrenChanged = false;
277
+ for (const item of items) {
278
+ const x = colOffsets[item.colStart] + paddingLeft;
279
+ while (colOffsets.length <= item.colEnd) {
280
+ colOffsets.push(colOffsets[colOffsets.length - 1] + 0 + colGap);
227
281
  }
228
- // Remove margins that might interfere with gap property
229
- if (child.node.getMargin(Style.Edge.Bottom).unit !== Style.Unit.Undefined) {
230
- child.node.setMargin(Style.Edge.Bottom, undefined);
231
- childChanged = true;
282
+ const widthStart = colOffsets[item.colStart];
283
+ const widthEnd = colOffsets[item.colEnd];
284
+ const totalWidth = Math.max(0, widthEnd - widthStart - colGap);
285
+ const y = rowOffsets[item.rowStart] + paddingTop;
286
+ const heightStart = rowOffsets[item.rowStart];
287
+ const heightEnd = rowOffsets[item.rowEnd];
288
+ const totalHeight = Math.max(0, heightEnd - heightStart - rowGap);
289
+ const childNode = item.node.node;
290
+ if (childNode.getPositionType() !== Style.PositionType.Absolute) {
291
+ childNode.setPositionType(Style.PositionType.Absolute);
292
+ childrenChanged = true;
232
293
  }
233
- if (child.node.getMargin(Style.Edge.Right).unit !== Style.Unit.Undefined) {
234
- child.node.setMargin(Style.Edge.Right, undefined);
235
- childChanged = true;
294
+ if (childNode.getPosition(Style.Edge.Left).value !== x) {
295
+ childNode.setPosition(Style.Edge.Left, x);
296
+ childrenChanged = true;
236
297
  }
237
- if (child.node.getMargin(Style.Edge.Top).unit !== Style.Unit.Undefined) {
238
- child.node.setMargin(Style.Edge.Top, undefined);
239
- childChanged = true;
298
+ if (childNode.getPosition(Style.Edge.Top).value !== y) {
299
+ childNode.setPosition(Style.Edge.Top, y);
300
+ childrenChanged = true;
240
301
  }
241
- if (child.node.getMargin(Style.Edge.Left).unit !== Style.Unit.Undefined) {
242
- child.node.setMargin(Style.Edge.Left, undefined);
243
- childChanged = true;
302
+ if (childNode.getWidth().unit !== Style.Unit.Point || Math.abs(childNode.getWidth().value - totalWidth) > 0.1) {
303
+ childNode.setWidth(totalWidth);
304
+ childrenChanged = true;
244
305
  }
245
- if (childChanged && !child.node.isDirty()) {
246
- child.node.markDirty();
247
- childrenNeedRecalculation = true;
306
+ if (childNode.getHeight().unit !== Style.Unit.Point || Math.abs(childNode.getHeight().value - totalHeight) > 0.1) {
307
+ childNode.setHeight(totalHeight);
308
+ childrenChanged = true;
248
309
  }
249
310
  }
250
- // Step 7: Apply gaps using Yoga's built-in gap properties
251
- const currentColumnGap = this.node.getGap(Style.Gutter.Column).value;
252
- const currentRowGap = this.node.getGap(Style.Gutter.Row).value;
253
- let gapsChanged = false;
254
- // Use a small tolerance for comparing gap pixels
255
- if (Math.abs(currentColumnGap - columnGapPixels) > 0.001) {
256
- this.node.setGap(Style.Gutter.Column, columnGapPixels);
257
- gapsChanged = true;
311
+ // 9. Update Grid Height
312
+ const totalGridHeight = Math.max(0, rowOffsets[rowOffsets.length - 1] - rowGap);
313
+ const currentHeightStyle = this.node.getHeight();
314
+ if (currentHeightStyle.unit === Style.Unit.Auto || currentHeightStyle.unit === Style.Unit.Undefined) {
315
+ const targetTotalHeight = totalGridHeight + paddingTop + paddingBottom;
316
+ this.node.setHeight(targetTotalHeight);
317
+ childrenChanged = true;
258
318
  }
259
- if (Math.abs(currentRowGap - rowGapPixels) > 0.001) {
260
- this.node.setGap(Style.Gutter.Row, rowGapPixels);
261
- gapsChanged = true;
262
- }
263
- // Step 8: Mark the grid node itself as dirty if gaps changed or children changed
264
- if ((gapsChanged || childrenNeedRecalculation) && !this.node.isDirty()) {
319
+ if (childrenChanged && !this.node.isDirty()) {
265
320
  this.node.markDirty();
266
321
  }
267
322
  }
323
+ /**
324
+ * Resolves track sizes to pixels.
325
+ */
326
+ resolveTracks(tracks, availableSpace, gap) {
327
+ const resolved = [];
328
+ let usedSpace = 0;
329
+ let totalFr = 0;
330
+ const frIndices = [];
331
+ tracks.forEach((t, i) => {
332
+ const parsed = this.parseTrack(t, availableSpace);
333
+ if (parsed.type === 'px' || parsed.type === '%') {
334
+ resolved[i] = parsed.value;
335
+ usedSpace += parsed.value;
336
+ }
337
+ else if (parsed.type === 'fr') {
338
+ totalFr += parsed.value;
339
+ resolved[i] = 0;
340
+ frIndices.push(i);
341
+ }
342
+ else {
343
+ resolved[i] = 0;
344
+ }
345
+ });
346
+ const totalGaps = Math.max(0, tracks.length - 1) * gap;
347
+ usedSpace += totalGaps;
348
+ const remainingSpace = Math.max(0, availableSpace - usedSpace);
349
+ if (totalFr > 0) {
350
+ frIndices.forEach(i => {
351
+ const parsed = this.parseTrack(tracks[i], availableSpace);
352
+ const share = (parsed.value / totalFr) * remainingSpace;
353
+ resolved[i] = share;
354
+ });
355
+ }
356
+ return resolved;
357
+ }
268
358
  }
269
359
  /**
270
360
  * Factory function to create a new GridNode instance.
271
- * @param props Grid configuration properties.
272
- * @returns A new GridNode instance.
273
361
  */
274
362
  const Grid = (props) => new GridNode(props);
275
363
 
@@ -1 +1 @@
1
- {"version":3,"file":"text.canvas.util.d.ts","sourceRoot":"","sources":["../../../src/canvas/text.canvas.util.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,yBAAyB,CAAA;AACrE,OAAO,EAAU,KAAK,wBAAwB,EAA2B,MAAM,aAAa,CAAA;AAC5F,OAAO,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAA;AAGxD;;;GAGG;AACH,qBAAa,QAAS,SAAQ,OAAO;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoB;IAC7C,OAAO,CAAC,KAAK,CAAsB;IACnC,OAAO,CAAC,MAAM,CAAC,kBAAkB,CAAwC;IACzE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAU;IACxC,OAAO,CAAC,WAAW,CAAe;IAClC,OAAO,CAAC,WAAW,CAAe;IAClC,OAAO,CAAC,kBAAkB,CAAe;IAEjC,KAAK,EAAE,SAAS,GAAG;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;gBAElC,IAAI,GAAE,MAAM,GAAG,MAAW,EAAE,KAAK,GAAE,SAAc;IAuB7D;;;;;;;;OAQG;WACW,gBAAgB,CAC5B,GAAG,EAAE,wBAAwB,EAC7B,IAAI,EAAE,MAAM,EACZ,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,KAAK,GAAE;QACL,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,UAAU,CAAC,EAAE,SAAS,CAAC,YAAY,CAAC,CAAA;QACpC,SAAS,CAAC,EAAE,SAAS,CAAC,WAAW,CAAC,CAAA;QAClC,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,SAAS,CAAC,EAAE,wBAAwB,CAAC,WAAW,CAAC,CAAA;QACjD,YAAY,CAAC,EAAE,wBAAwB,CAAC,cAAc,CAAC,CAAA;KACnD;cAwBW,aAAa,IAAI,IAAI;IAoDxC;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,sBAAsB;IA8B9B;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,aAAa;IA+ErB,OAAO,CAAC,aAAa;IAKrB,OAAO,CAAC,gBAAgB;IAyBxB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,aAAa;IAiCrB;;OAEG;IACH,OAAO,CAAC,yBAAyB;IAQjC;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,WAAW;IA8NnB;;;;;;;;;OASG;IACH,OAAO,CAAC,YAAY;IAuKpB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;IAmErB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAQzB;;;;;;;;;;;;;;;;OAgBG;cACgB,cAAc,CAAC,GAAG,EAAE,wBAAwB,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CA8UrH;AAED;;GAEG;AACH,eAAO,MAAM,IAAI,GAAI,MAAM,MAAM,GAAG,MAAM,EAAE,QAAQ,SAAS,aAA8B,CAAA"}
1
+ {"version":3,"file":"text.canvas.util.d.ts","sourceRoot":"","sources":["../../../src/canvas/text.canvas.util.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,yBAAyB,CAAA;AACrE,OAAO,EAAU,KAAK,wBAAwB,EAA2B,MAAM,aAAa,CAAA;AAC5F,OAAO,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAA;AAGxD;;;GAGG;AACH,qBAAa,QAAS,SAAQ,OAAO;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoB;IAC7C,OAAO,CAAC,KAAK,CAAsB;IACnC,OAAO,CAAC,MAAM,CAAC,kBAAkB,CAAwC;IACzE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAU;IACxC,OAAO,CAAC,WAAW,CAAe;IAClC,OAAO,CAAC,WAAW,CAAe;IAClC,OAAO,CAAC,kBAAkB,CAAe;IAEjC,KAAK,EAAE,SAAS,GAAG;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;gBAElC,IAAI,GAAE,MAAM,GAAG,MAAW,EAAE,KAAK,GAAE,SAAc;IAuB7D;;;;;;;;OAQG;WACW,gBAAgB,CAC5B,GAAG,EAAE,wBAAwB,EAC7B,IAAI,EAAE,MAAM,EACZ,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,KAAK,GAAE;QACL,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,UAAU,CAAC,EAAE,SAAS,CAAC,YAAY,CAAC,CAAA;QACpC,SAAS,CAAC,EAAE,SAAS,CAAC,WAAW,CAAC,CAAA;QAClC,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,SAAS,CAAC,EAAE,wBAAwB,CAAC,WAAW,CAAC,CAAA;QACjD,YAAY,CAAC,EAAE,wBAAwB,CAAC,cAAc,CAAC,CAAA;KACnD;cAwBW,aAAa,IAAI,IAAI;IAoDxC;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,sBAAsB;IA8B9B;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,aAAa;IA+ErB,OAAO,CAAC,aAAa;IAKrB,OAAO,CAAC,gBAAgB;IAyBxB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,aAAa;IAiCrB;;OAEG;IACH,OAAO,CAAC,yBAAyB;IAQjC;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,WAAW;IA8NnB;;;;;;;;;OASG;IACH,OAAO,CAAC,YAAY;IAuKpB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;IAmErB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAQzB;;;;;;;;;;;;;;;;OAgBG;cACgB,cAAc,CAAC,GAAG,EAAE,wBAAwB,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAiXrH;AAED;;GAEG;AACH,eAAO,MAAM,IAAI,GAAI,MAAM,MAAM,GAAG,MAAM,EAAE,QAAQ,SAAS,aAA8B,CAAA"}