flexily 0.0.1 → 0.2.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.
Files changed (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +200 -0
  3. package/dist/classic/layout.d.ts +57 -0
  4. package/dist/classic/layout.d.ts.map +1 -0
  5. package/dist/classic/layout.js +1558 -0
  6. package/dist/classic/layout.js.map +1 -0
  7. package/dist/classic/node.d.ts +648 -0
  8. package/dist/classic/node.d.ts.map +1 -0
  9. package/dist/classic/node.js +1002 -0
  10. package/dist/classic/node.js.map +1 -0
  11. package/dist/constants.d.ts +58 -0
  12. package/dist/constants.d.ts.map +1 -0
  13. package/dist/constants.js +70 -0
  14. package/dist/constants.js.map +1 -0
  15. package/dist/index-classic.d.ts +30 -0
  16. package/dist/index-classic.d.ts.map +1 -0
  17. package/dist/index-classic.js +57 -0
  18. package/dist/index-classic.js.map +1 -0
  19. package/dist/index.d.ts +30 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +57 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/layout-flex-lines.d.ts +77 -0
  24. package/dist/layout-flex-lines.d.ts.map +1 -0
  25. package/dist/layout-flex-lines.js +317 -0
  26. package/dist/layout-flex-lines.js.map +1 -0
  27. package/dist/layout-helpers.d.ts +48 -0
  28. package/dist/layout-helpers.d.ts.map +1 -0
  29. package/dist/layout-helpers.js +108 -0
  30. package/dist/layout-helpers.js.map +1 -0
  31. package/dist/layout-measure.d.ts +25 -0
  32. package/dist/layout-measure.d.ts.map +1 -0
  33. package/dist/layout-measure.js +231 -0
  34. package/dist/layout-measure.js.map +1 -0
  35. package/dist/layout-stats.d.ts +19 -0
  36. package/dist/layout-stats.d.ts.map +1 -0
  37. package/dist/layout-stats.js +37 -0
  38. package/dist/layout-stats.js.map +1 -0
  39. package/dist/layout-traversal.d.ts +28 -0
  40. package/dist/layout-traversal.d.ts.map +1 -0
  41. package/dist/layout-traversal.js +65 -0
  42. package/dist/layout-traversal.js.map +1 -0
  43. package/dist/layout-zero.d.ts +26 -0
  44. package/dist/layout-zero.d.ts.map +1 -0
  45. package/dist/layout-zero.js +1601 -0
  46. package/dist/layout-zero.js.map +1 -0
  47. package/dist/logger.d.ts +14 -0
  48. package/dist/logger.d.ts.map +1 -0
  49. package/dist/logger.js +61 -0
  50. package/dist/logger.js.map +1 -0
  51. package/dist/node-zero.d.ts +702 -0
  52. package/dist/node-zero.d.ts.map +1 -0
  53. package/dist/node-zero.js +1268 -0
  54. package/dist/node-zero.js.map +1 -0
  55. package/dist/testing.d.ts +69 -0
  56. package/dist/testing.d.ts.map +1 -0
  57. package/dist/testing.js +179 -0
  58. package/dist/testing.js.map +1 -0
  59. package/dist/trace.d.ts +74 -0
  60. package/dist/trace.d.ts.map +1 -0
  61. package/dist/trace.js +191 -0
  62. package/dist/trace.js.map +1 -0
  63. package/dist/types.d.ts +170 -0
  64. package/dist/types.d.ts.map +1 -0
  65. package/dist/types.js +43 -0
  66. package/dist/types.js.map +1 -0
  67. package/dist/utils.d.ts +41 -0
  68. package/dist/utils.d.ts.map +1 -0
  69. package/dist/utils.js +197 -0
  70. package/dist/utils.js.map +1 -0
  71. package/package.json +58 -3
  72. package/src/CLAUDE.md +512 -0
  73. package/src/beorn-logger.d.ts +10 -0
  74. package/src/classic/layout.ts +1783 -0
  75. package/src/classic/node.ts +1121 -0
  76. package/src/constants.ts +81 -0
  77. package/src/index-classic.ts +110 -0
  78. package/src/index.ts +110 -0
  79. package/src/layout-flex-lines.ts +346 -0
  80. package/src/layout-helpers.ts +140 -0
  81. package/src/layout-measure.ts +259 -0
  82. package/src/layout-stats.ts +43 -0
  83. package/src/layout-traversal.ts +70 -0
  84. package/src/layout-zero.ts +1792 -0
  85. package/src/logger.ts +67 -0
  86. package/src/node-zero.ts +1412 -0
  87. package/src/testing.ts +209 -0
  88. package/src/trace.ts +252 -0
  89. package/src/types.ts +229 -0
  90. package/src/utils.ts +217 -0
@@ -0,0 +1,1412 @@
1
+ /**
2
+ * Flexily Node
3
+ *
4
+ * Yoga-compatible Node class for flexbox layout.
5
+ */
6
+
7
+ import * as C from "./constants.js"
8
+ import { computeLayout, countNodes, markSubtreeLayoutSeen } from "./layout-zero.js"
9
+ import {
10
+ type BaselineFunc,
11
+ type FlexInfo,
12
+ type Layout,
13
+ type LayoutCacheEntry,
14
+ type MeasureEntry,
15
+ type MeasureFunc,
16
+ type Style,
17
+ type Value,
18
+ createDefaultStyle,
19
+ } from "./types.js"
20
+ import { setEdgeValue, setEdgeBorder, getEdgeValue, getEdgeBorderValue, traversalStack } from "./utils.js"
21
+ import { log } from "./logger.js"
22
+
23
+ /**
24
+ * A layout node in the flexbox tree.
25
+ */
26
+ export class Node {
27
+ // Tree structure
28
+ private _parent: Node | null = null
29
+ private _children: Node[] = []
30
+
31
+ // Style
32
+ private _style: Style = createDefaultStyle()
33
+
34
+ // Measure function for intrinsic sizing
35
+ private _measureFunc: MeasureFunc | null = null
36
+
37
+ // Baseline function for baseline alignment
38
+ private _baselineFunc: BaselineFunc | null = null
39
+
40
+ // Measure cache - 4-entry numeric cache (faster than Map<string,...>)
41
+ // Each entry stores: w, wm, h, hm, rw, rh
42
+ // Cleared when markDirty() is called since content may have changed
43
+ private _m0?: MeasureEntry
44
+ private _m1?: MeasureEntry
45
+ private _m2?: MeasureEntry
46
+ private _m3?: MeasureEntry
47
+
48
+ // Layout cache - 2-entry cache for sizing pass (availW, availH -> computedW, computedH)
49
+ // Cleared at start of each calculateLayout pass via resetLayoutCache()
50
+ // This avoids redundant recursive layout calls during intrinsic sizing
51
+ private _lc0?: LayoutCacheEntry
52
+ private _lc1?: LayoutCacheEntry
53
+
54
+ // Stable result objects for zero-allocation cache returns
55
+ // These are mutated in place instead of creating new objects on each cache hit
56
+ private _measureResult: { width: number; height: number } = {
57
+ width: 0,
58
+ height: 0,
59
+ }
60
+ private _layoutResult: { width: number; height: number } = {
61
+ width: 0,
62
+ height: 0,
63
+ }
64
+
65
+ // Static counters for cache statistics (reset per layout pass)
66
+ static measureCalls = 0
67
+ static measureCacheHits = 0
68
+
69
+ /**
70
+ * Reset measure statistics (call before calculateLayout).
71
+ */
72
+ static resetMeasureStats(): void {
73
+ Node.measureCalls = 0
74
+ Node.measureCacheHits = 0
75
+ }
76
+
77
+ // Computed layout
78
+ private _layout: Layout = { left: 0, top: 0, width: 0, height: 0 }
79
+
80
+ // Per-node flex calculation state (reused across layout passes to avoid allocation)
81
+ private _flex: FlexInfo = {
82
+ mainSize: 0,
83
+ baseSize: 0,
84
+ mainMargin: 0,
85
+ flexGrow: 0,
86
+ flexShrink: 0,
87
+ minMain: 0,
88
+ maxMain: Infinity,
89
+ mainStartMarginAuto: false,
90
+ mainEndMarginAuto: false,
91
+ mainStartMarginValue: 0,
92
+ mainEndMarginValue: 0,
93
+ marginL: 0,
94
+ marginT: 0,
95
+ marginR: 0,
96
+ marginB: 0,
97
+ frozen: false,
98
+ lineIndex: 0,
99
+ relativeIndex: -1,
100
+ baseline: 0,
101
+ // Constraint fingerprinting
102
+ lastAvailW: NaN,
103
+ lastAvailH: NaN,
104
+ lastOffsetX: NaN,
105
+ lastOffsetY: NaN,
106
+ layoutValid: false,
107
+ lastDir: 0,
108
+ }
109
+
110
+ // Dirty flags
111
+ private _isDirty = true
112
+ private _hasNewLayout = false
113
+
114
+ // Last calculateLayout() inputs (for constraint-aware skip)
115
+ private _lastCalcW: number = NaN
116
+ private _lastCalcH: number = NaN
117
+ private _lastCalcDir: number = 0
118
+
119
+ // ============================================================================
120
+ // Static Factory
121
+ // ============================================================================
122
+
123
+ /**
124
+ * Create a new layout node.
125
+ *
126
+ * @returns A new Node instance
127
+ * @example
128
+ * ```typescript
129
+ * const root = Node.create();
130
+ * root.setWidth(100);
131
+ * root.setHeight(200);
132
+ * ```
133
+ */
134
+ static create(): Node {
135
+ return new Node()
136
+ }
137
+
138
+ // ============================================================================
139
+ // Tree Operations
140
+ // ============================================================================
141
+
142
+ /**
143
+ * Get the number of child nodes.
144
+ *
145
+ * @returns The number of children
146
+ */
147
+ getChildCount(): number {
148
+ return this._children.length
149
+ }
150
+
151
+ /**
152
+ * Get a child node by index.
153
+ *
154
+ * @param index - Zero-based child index
155
+ * @returns The child node at the given index, or undefined if index is out of bounds
156
+ */
157
+ getChild(index: number): Node | undefined {
158
+ return this._children[index]
159
+ }
160
+
161
+ /**
162
+ * Get the parent node.
163
+ *
164
+ * @returns The parent node, or null if this is a root node
165
+ */
166
+ getParent(): Node | null {
167
+ return this._parent
168
+ }
169
+
170
+ /**
171
+ * Insert a child node at the specified index.
172
+ * If the child already has a parent, it will be removed from that parent first.
173
+ * Marks the node as dirty to trigger layout recalculation.
174
+ *
175
+ * @param child - The child node to insert
176
+ * @param index - The index at which to insert the child
177
+ * @example
178
+ * ```typescript
179
+ * const parent = Node.create();
180
+ * const child1 = Node.create();
181
+ * const child2 = Node.create();
182
+ * parent.insertChild(child1, 0);
183
+ * parent.insertChild(child2, 1);
184
+ * ```
185
+ */
186
+ insertChild(child: Node, index: number): void {
187
+ if (child._parent !== null) {
188
+ child._parent.removeChild(child)
189
+ }
190
+ child._parent = this
191
+ // Clamp index to valid range to ensure deterministic behavior
192
+ const clampedIndex = Math.max(0, Math.min(index, this._children.length))
193
+ this._children.splice(clampedIndex, 0, child)
194
+ // Invalidate layoutValid for siblings after the insertion point
195
+ // Their positions may change due to the insertion
196
+ for (let i = clampedIndex + 1; i < this._children.length; i++) {
197
+ this._children[i]!._flex.layoutValid = false
198
+ }
199
+ this.markDirty()
200
+ }
201
+
202
+ /**
203
+ * Remove a child node from this node.
204
+ * The child's parent reference will be cleared.
205
+ * Marks the node as dirty to trigger layout recalculation.
206
+ * Invalidates layout validity of remaining siblings whose positions may change.
207
+ *
208
+ * @param child - The child node to remove
209
+ */
210
+ removeChild(child: Node): void {
211
+ const index = this._children.indexOf(child)
212
+ if (index !== -1) {
213
+ this._children.splice(index, 1)
214
+ child._parent = null
215
+ // Invalidate layoutValid for remaining siblings after the removal point
216
+ // Their positions may change due to the removal
217
+ for (let i = index; i < this._children.length; i++) {
218
+ this._children[i]!._flex.layoutValid = false
219
+ }
220
+ this.markDirty()
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Free this node and clean up all references.
226
+ * Removes the node from its parent, clears all children, and removes the measure function.
227
+ * This does not recursively free child nodes.
228
+ */
229
+ free(): void {
230
+ // Remove from parent
231
+ if (this._parent !== null) {
232
+ this._parent.removeChild(this)
233
+ }
234
+ // Clear children
235
+ for (const child of this._children) {
236
+ child._parent = null
237
+ }
238
+ this._children = []
239
+ this._measureFunc = null
240
+ this._baselineFunc = null
241
+ }
242
+
243
+ /**
244
+ * Dispose the node (calls free)
245
+ */
246
+ [Symbol.dispose](): void {
247
+ this.free()
248
+ }
249
+
250
+ // ============================================================================
251
+ // Measure Function
252
+ // ============================================================================
253
+
254
+ /**
255
+ * Set a measure function for intrinsic sizing.
256
+ * The measure function is called during layout to determine the node's natural size.
257
+ * Typically used for text nodes or other content that has an intrinsic size.
258
+ * Marks the node as dirty to trigger layout recalculation.
259
+ *
260
+ * @param measureFunc - Function that returns width and height given available space and constraints
261
+ * @example
262
+ * ```typescript
263
+ * const textNode = Node.create();
264
+ * textNode.setMeasureFunc((width, widthMode, height, heightMode) => {
265
+ * // Measure text and return dimensions
266
+ * return { width: 50, height: 20 };
267
+ * });
268
+ * ```
269
+ */
270
+ setMeasureFunc(measureFunc: MeasureFunc): void {
271
+ this._measureFunc = measureFunc
272
+ this.markDirty()
273
+ }
274
+
275
+ /**
276
+ * Remove the measure function from this node.
277
+ * Marks the node as dirty to trigger layout recalculation.
278
+ */
279
+ unsetMeasureFunc(): void {
280
+ this._measureFunc = null
281
+ this.markDirty()
282
+ }
283
+
284
+ /**
285
+ * Check if this node has a measure function.
286
+ *
287
+ * @returns True if a measure function is set
288
+ */
289
+ hasMeasureFunc(): boolean {
290
+ return this._measureFunc !== null
291
+ }
292
+
293
+ // ============================================================================
294
+ // Baseline Function
295
+ // ============================================================================
296
+
297
+ /**
298
+ * Set a baseline function to determine where this node's text baseline is.
299
+ * Used for ALIGN_BASELINE to align text across siblings with different heights.
300
+ *
301
+ * @param baselineFunc - Function that returns baseline offset from top given width and height
302
+ * @example
303
+ * ```typescript
304
+ * textNode.setBaselineFunc((width, height) => {
305
+ * // For a text node, baseline might be at 80% of height
306
+ * return height * 0.8;
307
+ * });
308
+ * ```
309
+ */
310
+ setBaselineFunc(baselineFunc: BaselineFunc): void {
311
+ this._baselineFunc = baselineFunc
312
+ this.markDirty()
313
+ }
314
+
315
+ /**
316
+ * Remove the baseline function from this node.
317
+ * Marks the node as dirty to trigger layout recalculation.
318
+ */
319
+ unsetBaselineFunc(): void {
320
+ this._baselineFunc = null
321
+ this.markDirty()
322
+ }
323
+
324
+ /**
325
+ * Check if this node has a baseline function.
326
+ *
327
+ * @returns True if a baseline function is set
328
+ */
329
+ hasBaselineFunc(): boolean {
330
+ return this._baselineFunc !== null
331
+ }
332
+
333
+ /**
334
+ * Call the measure function with caching.
335
+ * Uses a 4-entry numeric cache for fast lookup without allocations.
336
+ * Cache is cleared when markDirty() is called.
337
+ *
338
+ * @returns Measured dimensions or null if no measure function
339
+ */
340
+ cachedMeasure(w: number, wm: number, h: number, hm: number): { width: number; height: number } | null {
341
+ if (!this._measureFunc) return null
342
+
343
+ Node.measureCalls++
344
+
345
+ // Check 4-entry cache (most recent first)
346
+ // Returns stable _measureResult object to avoid allocation on cache hit
347
+ const m0 = this._m0
348
+ if (m0 && m0.w === w && m0.wm === wm && m0.h === h && m0.hm === hm) {
349
+ Node.measureCacheHits++
350
+ this._measureResult.width = m0.rw
351
+ this._measureResult.height = m0.rh
352
+ return this._measureResult
353
+ }
354
+ const m1 = this._m1
355
+ if (m1 && m1.w === w && m1.wm === wm && m1.h === h && m1.hm === hm) {
356
+ Node.measureCacheHits++
357
+ this._measureResult.width = m1.rw
358
+ this._measureResult.height = m1.rh
359
+ return this._measureResult
360
+ }
361
+ const m2 = this._m2
362
+ if (m2 && m2.w === w && m2.wm === wm && m2.h === h && m2.hm === hm) {
363
+ Node.measureCacheHits++
364
+ this._measureResult.width = m2.rw
365
+ this._measureResult.height = m2.rh
366
+ return this._measureResult
367
+ }
368
+ const m3 = this._m3
369
+ if (m3 && m3.w === w && m3.wm === wm && m3.h === h && m3.hm === hm) {
370
+ Node.measureCacheHits++
371
+ this._measureResult.width = m3.rw
372
+ this._measureResult.height = m3.rh
373
+ return this._measureResult
374
+ }
375
+
376
+ // Call actual measure function
377
+ const result = this._measureFunc(w, wm, h, hm)
378
+
379
+ // Zero-allocation: rotate entries by copying values, lazily allocate on first use
380
+ // Rotate: m3 <- m2 <- m1 <- m0 <- new values
381
+ if (this._m2) {
382
+ if (!this._m3) this._m3 = { w: 0, wm: 0, h: 0, hm: 0, rw: 0, rh: 0 }
383
+ this._m3.w = this._m2.w
384
+ this._m3.wm = this._m2.wm
385
+ this._m3.h = this._m2.h
386
+ this._m3.hm = this._m2.hm
387
+ this._m3.rw = this._m2.rw
388
+ this._m3.rh = this._m2.rh
389
+ }
390
+ if (this._m1) {
391
+ if (!this._m2) this._m2 = { w: 0, wm: 0, h: 0, hm: 0, rw: 0, rh: 0 }
392
+ this._m2.w = this._m1.w
393
+ this._m2.wm = this._m1.wm
394
+ this._m2.h = this._m1.h
395
+ this._m2.hm = this._m1.hm
396
+ this._m2.rw = this._m1.rw
397
+ this._m2.rh = this._m1.rh
398
+ }
399
+ if (this._m0) {
400
+ if (!this._m1) this._m1 = { w: 0, wm: 0, h: 0, hm: 0, rw: 0, rh: 0 }
401
+ this._m1.w = this._m0.w
402
+ this._m1.wm = this._m0.wm
403
+ this._m1.h = this._m0.h
404
+ this._m1.hm = this._m0.hm
405
+ this._m1.rw = this._m0.rw
406
+ this._m1.rh = this._m0.rh
407
+ }
408
+ if (!this._m0) this._m0 = { w: 0, wm: 0, h: 0, hm: 0, rw: 0, rh: 0 }
409
+ this._m0.w = w
410
+ this._m0.wm = wm
411
+ this._m0.h = h
412
+ this._m0.hm = hm
413
+ this._m0.rw = result.width
414
+ this._m0.rh = result.height
415
+
416
+ // Return stable result object (same as cache hits)
417
+ this._measureResult.width = result.width
418
+ this._measureResult.height = result.height
419
+ return this._measureResult
420
+ }
421
+
422
+ // ============================================================================
423
+ // Layout Caching (for intrinsic sizing pass)
424
+ // ============================================================================
425
+
426
+ /**
427
+ * Check layout cache for a previously computed size with same available dimensions.
428
+ * Returns cached (width, height) or null if not found.
429
+ *
430
+ * NaN dimensions are handled specially via Object.is (NaN === NaN is false, but Object.is(NaN, NaN) is true).
431
+ */
432
+ getCachedLayout(availW: number, availH: number): { width: number; height: number } | null {
433
+ // Never return cached layout for dirty nodes - content may have changed
434
+ if (this._isDirty) {
435
+ return null
436
+ }
437
+ // Returns stable _layoutResult object to avoid allocation on cache hit
438
+ const lc0 = this._lc0
439
+ if (lc0 && Object.is(lc0.availW, availW) && Object.is(lc0.availH, availH)) {
440
+ this._layoutResult.width = lc0.computedW
441
+ this._layoutResult.height = lc0.computedH
442
+ return this._layoutResult
443
+ }
444
+ const lc1 = this._lc1
445
+ if (lc1 && Object.is(lc1.availW, availW) && Object.is(lc1.availH, availH)) {
446
+ this._layoutResult.width = lc1.computedW
447
+ this._layoutResult.height = lc1.computedH
448
+ return this._layoutResult
449
+ }
450
+ return null
451
+ }
452
+
453
+ /**
454
+ * Cache a computed layout result for the given available dimensions.
455
+ * Zero-allocation: lazily allocates cache entries once, then reuses.
456
+ */
457
+ setCachedLayout(availW: number, availH: number, computedW: number, computedH: number): void {
458
+ // Rotate entries: copy _lc0 values to _lc1, then update _lc0
459
+ if (this._lc0) {
460
+ // Lazily allocate _lc1 on first rotation
461
+ if (!this._lc1) {
462
+ this._lc1 = { availW: NaN, availH: NaN, computedW: 0, computedH: 0 }
463
+ }
464
+ this._lc1.availW = this._lc0.availW
465
+ this._lc1.availH = this._lc0.availH
466
+ this._lc1.computedW = this._lc0.computedW
467
+ this._lc1.computedH = this._lc0.computedH
468
+ }
469
+ // Lazily allocate _lc0 on first use
470
+ if (!this._lc0) {
471
+ this._lc0 = { availW: 0, availH: 0, computedW: 0, computedH: 0 }
472
+ }
473
+ this._lc0.availW = availW
474
+ this._lc0.availH = availH
475
+ this._lc0.computedW = computedW
476
+ this._lc0.computedH = computedH
477
+ }
478
+
479
+ /**
480
+ * Clear layout cache for this node and all descendants.
481
+ * Called at the start of each calculateLayout pass.
482
+ * Zero-allocation: invalidates entries (availW = NaN) rather than deallocating.
483
+ * Uses iterative traversal to avoid stack overflow on deep trees.
484
+ */
485
+ resetLayoutCache(): void {
486
+ traversalStack.length = 0
487
+ traversalStack.push(this)
488
+ while (traversalStack.length > 0) {
489
+ const node = traversalStack.pop() as Node
490
+ // Invalidate using -1 sentinel (not NaN — NaN is a legitimate "unconstrained" query
491
+ // value and Object.is(NaN, NaN) === true would cause false cache hits)
492
+ if (node._lc0) node._lc0.availW = -1
493
+ if (node._lc1) node._lc1.availW = -1
494
+ for (const child of node._children) {
495
+ traversalStack.push(child)
496
+ }
497
+ }
498
+ }
499
+
500
+ // ============================================================================
501
+ // Dirty Tracking
502
+ // ============================================================================
503
+
504
+ /**
505
+ * Check if this node needs layout recalculation.
506
+ *
507
+ * @returns True if the node is dirty and needs layout
508
+ */
509
+ isDirty(): boolean {
510
+ return this._isDirty
511
+ }
512
+
513
+ /**
514
+ * Mark this node and all ancestors as dirty.
515
+ * A dirty node needs layout recalculation.
516
+ * This is automatically called by all style setters and tree operations.
517
+ * Uses iterative approach to avoid stack overflow on deep trees.
518
+ */
519
+ markDirty(): void {
520
+ let current: Node | null = this
521
+ while (current !== null) {
522
+ // Always clear caches - even if already dirty, a child's content change
523
+ // may invalidate cached layout results that used the old child size
524
+ current._m0 = current._m1 = current._m2 = current._m3 = undefined
525
+ current._lc0 = current._lc1 = undefined
526
+ // Skip setting dirty flag if already dirty (but still cleared caches above)
527
+ if (current._isDirty) break
528
+ current._isDirty = true
529
+ // Invalidate layout fingerprint
530
+ current._flex.layoutValid = false
531
+ current = current._parent
532
+ }
533
+ }
534
+
535
+ /**
536
+ * Check if this node has new layout results since the last check.
537
+ *
538
+ * @returns True if layout was recalculated since the last call to markLayoutSeen
539
+ */
540
+ hasNewLayout(): boolean {
541
+ return this._hasNewLayout
542
+ }
543
+
544
+ /**
545
+ * Mark that the current layout has been seen/processed.
546
+ * Clears the hasNewLayout flag.
547
+ */
548
+ markLayoutSeen(): void {
549
+ this._hasNewLayout = false
550
+ }
551
+
552
+ // ============================================================================
553
+ // Layout Calculation
554
+ // ============================================================================
555
+
556
+ /**
557
+ * Calculate layout for this node and all descendants.
558
+ * This runs the flexbox layout algorithm to compute positions and sizes.
559
+ * Only recalculates if the node is marked as dirty.
560
+ *
561
+ * @param width - Available width for layout
562
+ * @param height - Available height for layout
563
+ * @param _direction - Text direction (LTR or RTL), defaults to LTR
564
+ * @example
565
+ * ```typescript
566
+ * const root = Node.create();
567
+ * root.setFlexDirection(FLEX_DIRECTION_ROW);
568
+ * root.setWidth(100);
569
+ * root.setHeight(50);
570
+ *
571
+ * const child = Node.create();
572
+ * child.setFlexGrow(1);
573
+ * root.insertChild(child, 0);
574
+ *
575
+ * root.calculateLayout(100, 50, DIRECTION_LTR);
576
+ *
577
+ * // Now you can read computed layout
578
+ * console.log(child.getComputedWidth());
579
+ * ```
580
+ */
581
+ calculateLayout(width?: number, height?: number, direction: number = C.DIRECTION_LTR): void {
582
+ // Treat undefined as unconstrained (NaN signals content-based sizing)
583
+ const availableWidth = width ?? NaN
584
+ const availableHeight = height ?? NaN
585
+
586
+ // Skip if not dirty AND constraints unchanged (use Object.is for NaN equality)
587
+ if (
588
+ !this._isDirty &&
589
+ Object.is(this._lastCalcW, availableWidth) &&
590
+ Object.is(this._lastCalcH, availableHeight) &&
591
+ this._lastCalcDir === direction
592
+ ) {
593
+ log.debug?.("layout skip (not dirty, constraints unchanged)")
594
+ return
595
+ }
596
+
597
+ // Track constraints for future skip check
598
+ this._lastCalcW = availableWidth
599
+ this._lastCalcH = availableHeight
600
+ this._lastCalcDir = direction
601
+
602
+ const start = Date.now()
603
+ const nodeCount = countNodes(this)
604
+
605
+ // Reset measure statistics for this layout pass
606
+ Node.resetMeasureStats()
607
+
608
+ // Run the layout algorithm
609
+ computeLayout(this, availableWidth, availableHeight, direction)
610
+
611
+ // Mark layout computed
612
+ this._isDirty = false
613
+ this._hasNewLayout = true
614
+ markSubtreeLayoutSeen(this)
615
+
616
+ log.debug?.(
617
+ "layout: %dx%d, %d nodes in %dms (measure: calls=%d hits=%d)",
618
+ width,
619
+ height,
620
+ nodeCount,
621
+ Date.now() - start,
622
+ Node.measureCalls,
623
+ Node.measureCacheHits,
624
+ )
625
+ }
626
+
627
+ // ============================================================================
628
+ // Layout Results
629
+ // ============================================================================
630
+
631
+ /**
632
+ * Get the computed left position after layout.
633
+ *
634
+ * @returns The left position in points
635
+ */
636
+ getComputedLeft(): number {
637
+ return this._layout.left
638
+ }
639
+
640
+ /**
641
+ * Get the computed top position after layout.
642
+ *
643
+ * @returns The top position in points
644
+ */
645
+ getComputedTop(): number {
646
+ return this._layout.top
647
+ }
648
+
649
+ /**
650
+ * Get the computed width after layout.
651
+ *
652
+ * @returns The width in points
653
+ */
654
+ getComputedWidth(): number {
655
+ return this._layout.width
656
+ }
657
+
658
+ /**
659
+ * Get the computed height after layout.
660
+ *
661
+ * @returns The height in points
662
+ */
663
+ getComputedHeight(): number {
664
+ return this._layout.height
665
+ }
666
+
667
+ // ============================================================================
668
+ // Internal Accessors (for layout algorithm)
669
+ // ============================================================================
670
+
671
+ get children(): readonly Node[] {
672
+ return this._children
673
+ }
674
+
675
+ get style(): Style {
676
+ return this._style
677
+ }
678
+
679
+ get layout(): Layout {
680
+ return this._layout
681
+ }
682
+
683
+ get measureFunc(): MeasureFunc | null {
684
+ return this._measureFunc
685
+ }
686
+
687
+ get baselineFunc(): BaselineFunc | null {
688
+ return this._baselineFunc
689
+ }
690
+
691
+ get flex(): FlexInfo {
692
+ return this._flex
693
+ }
694
+
695
+ // ============================================================================
696
+ // Width Setters
697
+ // ============================================================================
698
+
699
+ /**
700
+ * Set the width to a fixed value in points.
701
+ *
702
+ * @param value - Width in points
703
+ */
704
+ setWidth(value: number): void {
705
+ // NaN means "auto" in Yoga API
706
+ if (Number.isNaN(value)) {
707
+ this._style.width = { value: 0, unit: C.UNIT_AUTO }
708
+ } else {
709
+ this._style.width = { value, unit: C.UNIT_POINT }
710
+ }
711
+ this.markDirty()
712
+ }
713
+
714
+ /**
715
+ * Set the width as a percentage of the parent's width.
716
+ *
717
+ * @param value - Width as a percentage (0-100)
718
+ */
719
+ setWidthPercent(value: number): void {
720
+ this._style.width = { value, unit: C.UNIT_PERCENT }
721
+ this.markDirty()
722
+ }
723
+
724
+ /**
725
+ * Set the width to auto (determined by layout algorithm).
726
+ */
727
+ setWidthAuto(): void {
728
+ this._style.width = { value: 0, unit: C.UNIT_AUTO }
729
+ this.markDirty()
730
+ }
731
+
732
+ // ============================================================================
733
+ // Height Setters
734
+ // ============================================================================
735
+
736
+ /**
737
+ * Set the height to a fixed value in points.
738
+ *
739
+ * @param value - Height in points
740
+ */
741
+ setHeight(value: number): void {
742
+ // NaN means "auto" in Yoga API
743
+ if (Number.isNaN(value)) {
744
+ this._style.height = { value: 0, unit: C.UNIT_AUTO }
745
+ } else {
746
+ this._style.height = { value, unit: C.UNIT_POINT }
747
+ }
748
+ this.markDirty()
749
+ }
750
+
751
+ /**
752
+ * Set the height as a percentage of the parent's height.
753
+ *
754
+ * @param value - Height as a percentage (0-100)
755
+ */
756
+ setHeightPercent(value: number): void {
757
+ this._style.height = { value, unit: C.UNIT_PERCENT }
758
+ this.markDirty()
759
+ }
760
+
761
+ /**
762
+ * Set the height to auto (determined by layout algorithm).
763
+ */
764
+ setHeightAuto(): void {
765
+ this._style.height = { value: 0, unit: C.UNIT_AUTO }
766
+ this.markDirty()
767
+ }
768
+
769
+ // ============================================================================
770
+ // Min/Max Size Setters
771
+ // ============================================================================
772
+
773
+ /**
774
+ * Set the minimum width in points.
775
+ *
776
+ * @param value - Minimum width in points
777
+ */
778
+ setMinWidth(value: number): void {
779
+ this._style.minWidth = { value, unit: C.UNIT_POINT }
780
+ this.markDirty()
781
+ }
782
+
783
+ /**
784
+ * Set the minimum width as a percentage of the parent's width.
785
+ *
786
+ * @param value - Minimum width as a percentage (0-100)
787
+ */
788
+ setMinWidthPercent(value: number): void {
789
+ this._style.minWidth = { value, unit: C.UNIT_PERCENT }
790
+ this.markDirty()
791
+ }
792
+
793
+ /**
794
+ * Set the minimum height in points.
795
+ *
796
+ * @param value - Minimum height in points
797
+ */
798
+ setMinHeight(value: number): void {
799
+ this._style.minHeight = { value, unit: C.UNIT_POINT }
800
+ this.markDirty()
801
+ }
802
+
803
+ /**
804
+ * Set the minimum height as a percentage of the parent's height.
805
+ *
806
+ * @param value - Minimum height as a percentage (0-100)
807
+ */
808
+ setMinHeightPercent(value: number): void {
809
+ this._style.minHeight = { value, unit: C.UNIT_PERCENT }
810
+ this.markDirty()
811
+ }
812
+
813
+ /**
814
+ * Set the maximum width in points.
815
+ *
816
+ * @param value - Maximum width in points
817
+ */
818
+ setMaxWidth(value: number): void {
819
+ this._style.maxWidth = { value, unit: C.UNIT_POINT }
820
+ this.markDirty()
821
+ }
822
+
823
+ /**
824
+ * Set the maximum width as a percentage of the parent's width.
825
+ *
826
+ * @param value - Maximum width as a percentage (0-100)
827
+ */
828
+ setMaxWidthPercent(value: number): void {
829
+ this._style.maxWidth = { value, unit: C.UNIT_PERCENT }
830
+ this.markDirty()
831
+ }
832
+
833
+ /**
834
+ * Set the maximum height in points.
835
+ *
836
+ * @param value - Maximum height in points
837
+ */
838
+ setMaxHeight(value: number): void {
839
+ this._style.maxHeight = { value, unit: C.UNIT_POINT }
840
+ this.markDirty()
841
+ }
842
+
843
+ /**
844
+ * Set the maximum height as a percentage of the parent's height.
845
+ *
846
+ * @param value - Maximum height as a percentage (0-100)
847
+ */
848
+ setMaxHeightPercent(value: number): void {
849
+ this._style.maxHeight = { value, unit: C.UNIT_PERCENT }
850
+ this.markDirty()
851
+ }
852
+
853
+ /**
854
+ * Set the aspect ratio of the node.
855
+ * When set, the node's width/height relationship is constrained.
856
+ * If width is defined, height = width / aspectRatio.
857
+ * If height is defined, width = height * aspectRatio.
858
+ *
859
+ * @param value - Aspect ratio (width/height). Use NaN to unset.
860
+ */
861
+ setAspectRatio(value: number): void {
862
+ this._style.aspectRatio = value
863
+ this.markDirty()
864
+ }
865
+
866
+ // ============================================================================
867
+ // Flex Setters
868
+ // ============================================================================
869
+
870
+ /**
871
+ * Set the flex grow factor.
872
+ * Determines how much the node will grow relative to siblings when there is extra space.
873
+ *
874
+ * @param value - Flex grow factor (typically 0 or 1+)
875
+ * @example
876
+ * ```typescript
877
+ * const child = Node.create();
878
+ * child.setFlexGrow(1); // Will grow to fill available space
879
+ * ```
880
+ */
881
+ setFlexGrow(value: number): void {
882
+ this._style.flexGrow = value
883
+ this.markDirty()
884
+ }
885
+
886
+ /**
887
+ * Set the flex shrink factor.
888
+ * Determines how much the node will shrink relative to siblings when there is insufficient space.
889
+ *
890
+ * @param value - Flex shrink factor (default is 1)
891
+ */
892
+ setFlexShrink(value: number): void {
893
+ this._style.flexShrink = value
894
+ this.markDirty()
895
+ }
896
+
897
+ /**
898
+ * Set the flex basis to a fixed value in points.
899
+ * The initial size of the node before flex grow/shrink is applied.
900
+ *
901
+ * @param value - Flex basis in points
902
+ */
903
+ setFlexBasis(value: number): void {
904
+ this._style.flexBasis = { value, unit: C.UNIT_POINT }
905
+ this.markDirty()
906
+ }
907
+
908
+ /**
909
+ * Set the flex basis as a percentage of the parent's size.
910
+ *
911
+ * @param value - Flex basis as a percentage (0-100)
912
+ */
913
+ setFlexBasisPercent(value: number): void {
914
+ this._style.flexBasis = { value, unit: C.UNIT_PERCENT }
915
+ this.markDirty()
916
+ }
917
+
918
+ /**
919
+ * Set the flex basis to auto (based on the node's width/height).
920
+ */
921
+ setFlexBasisAuto(): void {
922
+ this._style.flexBasis = { value: 0, unit: C.UNIT_AUTO }
923
+ this.markDirty()
924
+ }
925
+
926
+ /**
927
+ * Set the flex direction (main axis direction).
928
+ *
929
+ * @param direction - FLEX_DIRECTION_ROW, FLEX_DIRECTION_COLUMN, FLEX_DIRECTION_ROW_REVERSE, or FLEX_DIRECTION_COLUMN_REVERSE
930
+ * @example
931
+ * ```typescript
932
+ * const container = Node.create();
933
+ * container.setFlexDirection(FLEX_DIRECTION_ROW); // Lay out children horizontally
934
+ * ```
935
+ */
936
+ setFlexDirection(direction: number): void {
937
+ this._style.flexDirection = direction
938
+ this.markDirty()
939
+ }
940
+
941
+ /**
942
+ * Set the flex wrap behavior.
943
+ *
944
+ * @param wrap - WRAP_NO_WRAP, WRAP_WRAP, or WRAP_WRAP_REVERSE
945
+ */
946
+ setFlexWrap(wrap: number): void {
947
+ this._style.flexWrap = wrap
948
+ this.markDirty()
949
+ }
950
+
951
+ // ============================================================================
952
+ // Alignment Setters
953
+ // ============================================================================
954
+
955
+ /**
956
+ * Set how children are aligned along the cross axis.
957
+ *
958
+ * @param align - ALIGN_FLEX_START, ALIGN_CENTER, ALIGN_FLEX_END, ALIGN_STRETCH, or ALIGN_BASELINE
959
+ * @example
960
+ * ```typescript
961
+ * const container = Node.create();
962
+ * container.setFlexDirection(FLEX_DIRECTION_ROW);
963
+ * container.setAlignItems(ALIGN_CENTER); // Center children vertically
964
+ * ```
965
+ */
966
+ setAlignItems(align: number): void {
967
+ this._style.alignItems = align
968
+ this.markDirty()
969
+ }
970
+
971
+ /**
972
+ * Set how this node is aligned along the parent's cross axis.
973
+ * Overrides the parent's alignItems for this specific child.
974
+ *
975
+ * @param align - ALIGN_AUTO, ALIGN_FLEX_START, ALIGN_CENTER, ALIGN_FLEX_END, ALIGN_STRETCH, or ALIGN_BASELINE
976
+ */
977
+ setAlignSelf(align: number): void {
978
+ this._style.alignSelf = align
979
+ this.markDirty()
980
+ }
981
+
982
+ /**
983
+ * Set how lines are aligned in a multi-line flex container.
984
+ * Only affects containers with wrap enabled and multiple lines.
985
+ *
986
+ * @param align - ALIGN_FLEX_START, ALIGN_CENTER, ALIGN_FLEX_END, ALIGN_STRETCH, ALIGN_SPACE_BETWEEN, or ALIGN_SPACE_AROUND
987
+ */
988
+ setAlignContent(align: number): void {
989
+ this._style.alignContent = align
990
+ this.markDirty()
991
+ }
992
+
993
+ /**
994
+ * Set how children are distributed along the main axis.
995
+ *
996
+ * @param justify - JUSTIFY_FLEX_START, JUSTIFY_CENTER, JUSTIFY_FLEX_END, JUSTIFY_SPACE_BETWEEN, JUSTIFY_SPACE_AROUND, or JUSTIFY_SPACE_EVENLY
997
+ * @example
998
+ * ```typescript
999
+ * const container = Node.create();
1000
+ * container.setJustifyContent(JUSTIFY_SPACE_BETWEEN); // Space children evenly with edges at start/end
1001
+ * ```
1002
+ */
1003
+ setJustifyContent(justify: number): void {
1004
+ this._style.justifyContent = justify
1005
+ this.markDirty()
1006
+ }
1007
+
1008
+ // ============================================================================
1009
+ // Spacing Setters
1010
+ // ============================================================================
1011
+
1012
+ /**
1013
+ * Set padding for one or more edges.
1014
+ *
1015
+ * @param edge - EDGE_LEFT, EDGE_TOP, EDGE_RIGHT, EDGE_BOTTOM, EDGE_HORIZONTAL, EDGE_VERTICAL, or EDGE_ALL
1016
+ * @param value - Padding in points
1017
+ * @example
1018
+ * ```typescript
1019
+ * node.setPadding(EDGE_ALL, 10); // Set 10pt padding on all edges
1020
+ * node.setPadding(EDGE_HORIZONTAL, 5); // Set 5pt padding on left and right
1021
+ * ```
1022
+ */
1023
+ setPadding(edge: number, value: number): void {
1024
+ setEdgeValue(this._style.padding, edge, value, C.UNIT_POINT)
1025
+ this.markDirty()
1026
+ }
1027
+
1028
+ /**
1029
+ * Set padding as a percentage of the parent's width.
1030
+ * Per CSS spec, percentage padding always resolves against the containing block's width.
1031
+ *
1032
+ * @param edge - EDGE_LEFT, EDGE_TOP, EDGE_RIGHT, EDGE_BOTTOM, EDGE_HORIZONTAL, EDGE_VERTICAL, or EDGE_ALL
1033
+ * @param value - Padding as a percentage (0-100)
1034
+ */
1035
+ setPaddingPercent(edge: number, value: number): void {
1036
+ setEdgeValue(this._style.padding, edge, value, C.UNIT_PERCENT)
1037
+ this.markDirty()
1038
+ }
1039
+
1040
+ /**
1041
+ * Set margin for one or more edges.
1042
+ *
1043
+ * @param edge - EDGE_LEFT, EDGE_TOP, EDGE_RIGHT, EDGE_BOTTOM, EDGE_HORIZONTAL, EDGE_VERTICAL, or EDGE_ALL
1044
+ * @param value - Margin in points
1045
+ * @example
1046
+ * ```typescript
1047
+ * node.setMargin(EDGE_ALL, 5); // Set 5pt margin on all edges
1048
+ * node.setMargin(EDGE_TOP, 10); // Set 10pt margin on top only
1049
+ * ```
1050
+ */
1051
+ setMargin(edge: number, value: number): void {
1052
+ setEdgeValue(this._style.margin, edge, value, C.UNIT_POINT)
1053
+ this.markDirty()
1054
+ }
1055
+
1056
+ /**
1057
+ * Set margin as a percentage of the parent's size.
1058
+ *
1059
+ * @param edge - EDGE_LEFT, EDGE_TOP, EDGE_RIGHT, EDGE_BOTTOM, EDGE_HORIZONTAL, EDGE_VERTICAL, or EDGE_ALL
1060
+ * @param value - Margin as a percentage (0-100)
1061
+ */
1062
+ setMarginPercent(edge: number, value: number): void {
1063
+ setEdgeValue(this._style.margin, edge, value, C.UNIT_PERCENT)
1064
+ this.markDirty()
1065
+ }
1066
+
1067
+ /**
1068
+ * Set margin to auto (for centering items with margin: auto).
1069
+ *
1070
+ * @param edge - EDGE_LEFT, EDGE_TOP, EDGE_RIGHT, EDGE_BOTTOM, EDGE_HORIZONTAL, EDGE_VERTICAL, or EDGE_ALL
1071
+ */
1072
+ setMarginAuto(edge: number): void {
1073
+ setEdgeValue(this._style.margin, edge, 0, C.UNIT_AUTO)
1074
+ this.markDirty()
1075
+ }
1076
+
1077
+ /**
1078
+ * Set border width for one or more edges.
1079
+ *
1080
+ * @param edge - EDGE_LEFT, EDGE_TOP, EDGE_RIGHT, EDGE_BOTTOM, EDGE_HORIZONTAL, EDGE_VERTICAL, or EDGE_ALL
1081
+ * @param value - Border width in points
1082
+ */
1083
+ setBorder(edge: number, value: number): void {
1084
+ setEdgeBorder(this._style.border, edge, value)
1085
+ this.markDirty()
1086
+ }
1087
+
1088
+ /**
1089
+ * Set gap between flex items.
1090
+ *
1091
+ * @param gutter - GUTTER_COLUMN (horizontal gap), GUTTER_ROW (vertical gap), or GUTTER_ALL (both)
1092
+ * @param value - Gap size in points
1093
+ * @example
1094
+ * ```typescript
1095
+ * container.setGap(GUTTER_ALL, 8); // Set 8pt gap between all items
1096
+ * container.setGap(GUTTER_COLUMN, 10); // Set 10pt horizontal gap only
1097
+ * ```
1098
+ */
1099
+ setGap(gutter: number, value: number): void {
1100
+ if (gutter === C.GUTTER_COLUMN) {
1101
+ this._style.gap[0] = value
1102
+ } else if (gutter === C.GUTTER_ROW) {
1103
+ this._style.gap[1] = value
1104
+ } else if (gutter === C.GUTTER_ALL) {
1105
+ this._style.gap[0] = value
1106
+ this._style.gap[1] = value
1107
+ }
1108
+ this.markDirty()
1109
+ }
1110
+
1111
+ // ============================================================================
1112
+ // Position Setters
1113
+ // ============================================================================
1114
+
1115
+ /**
1116
+ * Set the position type.
1117
+ *
1118
+ * @param positionType - POSITION_TYPE_STATIC, POSITION_TYPE_RELATIVE, or POSITION_TYPE_ABSOLUTE
1119
+ * @example
1120
+ * ```typescript
1121
+ * node.setPositionType(POSITION_TYPE_ABSOLUTE);
1122
+ * node.setPosition(EDGE_LEFT, 10);
1123
+ * node.setPosition(EDGE_TOP, 20);
1124
+ * ```
1125
+ */
1126
+ setPositionType(positionType: number): void {
1127
+ this._style.positionType = positionType
1128
+ this.markDirty()
1129
+ }
1130
+
1131
+ /**
1132
+ * Set position offset for one or more edges.
1133
+ * Only applies when position type is ABSOLUTE or RELATIVE.
1134
+ *
1135
+ * @param edge - EDGE_LEFT, EDGE_TOP, EDGE_RIGHT, EDGE_BOTTOM, EDGE_HORIZONTAL, EDGE_VERTICAL, or EDGE_ALL
1136
+ * @param value - Position offset in points
1137
+ */
1138
+ setPosition(edge: number, value: number): void {
1139
+ // NaN means "auto" (unset) in Yoga API
1140
+ if (Number.isNaN(value)) {
1141
+ setEdgeValue(this._style.position, edge, 0, C.UNIT_UNDEFINED)
1142
+ } else {
1143
+ setEdgeValue(this._style.position, edge, value, C.UNIT_POINT)
1144
+ }
1145
+ this.markDirty()
1146
+ }
1147
+
1148
+ /**
1149
+ * Set position offset as a percentage.
1150
+ *
1151
+ * @param edge - EDGE_LEFT, EDGE_TOP, EDGE_RIGHT, EDGE_BOTTOM, EDGE_HORIZONTAL, EDGE_VERTICAL, or EDGE_ALL
1152
+ * @param value - Position offset as a percentage of parent's corresponding dimension
1153
+ */
1154
+ setPositionPercent(edge: number, value: number): void {
1155
+ setEdgeValue(this._style.position, edge, value, C.UNIT_PERCENT)
1156
+ this.markDirty()
1157
+ }
1158
+
1159
+ // ============================================================================
1160
+ // Other Setters
1161
+ // ============================================================================
1162
+
1163
+ /**
1164
+ * Set the display type.
1165
+ *
1166
+ * @param display - DISPLAY_FLEX or DISPLAY_NONE
1167
+ */
1168
+ setDisplay(display: number): void {
1169
+ this._style.display = display
1170
+ this.markDirty()
1171
+ }
1172
+
1173
+ /**
1174
+ * Set the overflow behavior.
1175
+ *
1176
+ * @param overflow - OVERFLOW_VISIBLE, OVERFLOW_HIDDEN, or OVERFLOW_SCROLL
1177
+ */
1178
+ setOverflow(overflow: number): void {
1179
+ this._style.overflow = overflow
1180
+ this.markDirty()
1181
+ }
1182
+
1183
+ // ============================================================================
1184
+ // Style Getters
1185
+ // ============================================================================
1186
+
1187
+ /**
1188
+ * Get the width style value.
1189
+ *
1190
+ * @returns Width value with unit (points, percent, or auto)
1191
+ */
1192
+ getWidth(): Value {
1193
+ return this._style.width
1194
+ }
1195
+
1196
+ /**
1197
+ * Get the height style value.
1198
+ *
1199
+ * @returns Height value with unit (points, percent, or auto)
1200
+ */
1201
+ getHeight(): Value {
1202
+ return this._style.height
1203
+ }
1204
+
1205
+ /**
1206
+ * Get the minimum width style value.
1207
+ *
1208
+ * @returns Minimum width value with unit
1209
+ */
1210
+ getMinWidth(): Value {
1211
+ return this._style.minWidth
1212
+ }
1213
+
1214
+ /**
1215
+ * Get the minimum height style value.
1216
+ *
1217
+ * @returns Minimum height value with unit
1218
+ */
1219
+ getMinHeight(): Value {
1220
+ return this._style.minHeight
1221
+ }
1222
+
1223
+ /**
1224
+ * Get the maximum width style value.
1225
+ *
1226
+ * @returns Maximum width value with unit
1227
+ */
1228
+ getMaxWidth(): Value {
1229
+ return this._style.maxWidth
1230
+ }
1231
+
1232
+ /**
1233
+ * Get the maximum height style value.
1234
+ *
1235
+ * @returns Maximum height value with unit
1236
+ */
1237
+ getMaxHeight(): Value {
1238
+ return this._style.maxHeight
1239
+ }
1240
+
1241
+ /**
1242
+ * Get the aspect ratio.
1243
+ *
1244
+ * @returns Aspect ratio value (NaN if not set)
1245
+ */
1246
+ getAspectRatio(): number {
1247
+ return this._style.aspectRatio
1248
+ }
1249
+
1250
+ /**
1251
+ * Get the flex grow factor.
1252
+ *
1253
+ * @returns Flex grow value
1254
+ */
1255
+ getFlexGrow(): number {
1256
+ return this._style.flexGrow
1257
+ }
1258
+
1259
+ /**
1260
+ * Get the flex shrink factor.
1261
+ *
1262
+ * @returns Flex shrink value
1263
+ */
1264
+ getFlexShrink(): number {
1265
+ return this._style.flexShrink
1266
+ }
1267
+
1268
+ /**
1269
+ * Get the flex basis style value.
1270
+ *
1271
+ * @returns Flex basis value with unit
1272
+ */
1273
+ getFlexBasis(): Value {
1274
+ return this._style.flexBasis
1275
+ }
1276
+
1277
+ /**
1278
+ * Get the flex direction.
1279
+ *
1280
+ * @returns Flex direction constant
1281
+ */
1282
+ getFlexDirection(): number {
1283
+ return this._style.flexDirection
1284
+ }
1285
+
1286
+ /**
1287
+ * Get the flex wrap setting.
1288
+ *
1289
+ * @returns Flex wrap constant
1290
+ */
1291
+ getFlexWrap(): number {
1292
+ return this._style.flexWrap
1293
+ }
1294
+
1295
+ /**
1296
+ * Get the align items setting.
1297
+ *
1298
+ * @returns Align items constant
1299
+ */
1300
+ getAlignItems(): number {
1301
+ return this._style.alignItems
1302
+ }
1303
+
1304
+ /**
1305
+ * Get the align self setting.
1306
+ *
1307
+ * @returns Align self constant
1308
+ */
1309
+ getAlignSelf(): number {
1310
+ return this._style.alignSelf
1311
+ }
1312
+
1313
+ /**
1314
+ * Get the align content setting.
1315
+ *
1316
+ * @returns Align content constant
1317
+ */
1318
+ getAlignContent(): number {
1319
+ return this._style.alignContent
1320
+ }
1321
+
1322
+ /**
1323
+ * Get the justify content setting.
1324
+ *
1325
+ * @returns Justify content constant
1326
+ */
1327
+ getJustifyContent(): number {
1328
+ return this._style.justifyContent
1329
+ }
1330
+
1331
+ /**
1332
+ * Get the padding for a specific edge.
1333
+ *
1334
+ * @param edge - EDGE_LEFT, EDGE_TOP, EDGE_RIGHT, or EDGE_BOTTOM
1335
+ * @returns Padding value with unit
1336
+ */
1337
+ getPadding(edge: number): Value {
1338
+ return getEdgeValue(this._style.padding, edge)
1339
+ }
1340
+
1341
+ /**
1342
+ * Get the margin for a specific edge.
1343
+ *
1344
+ * @param edge - EDGE_LEFT, EDGE_TOP, EDGE_RIGHT, or EDGE_BOTTOM
1345
+ * @returns Margin value with unit
1346
+ */
1347
+ getMargin(edge: number): Value {
1348
+ return getEdgeValue(this._style.margin, edge)
1349
+ }
1350
+
1351
+ /**
1352
+ * Get the border width for a specific edge.
1353
+ *
1354
+ * @param edge - EDGE_LEFT, EDGE_TOP, EDGE_RIGHT, or EDGE_BOTTOM
1355
+ * @returns Border width in points
1356
+ */
1357
+ getBorder(edge: number): number {
1358
+ return getEdgeBorderValue(this._style.border, edge)
1359
+ }
1360
+
1361
+ /**
1362
+ * Get the position offset for a specific edge.
1363
+ *
1364
+ * @param edge - EDGE_LEFT, EDGE_TOP, EDGE_RIGHT, or EDGE_BOTTOM
1365
+ * @returns Position value with unit
1366
+ */
1367
+ getPosition(edge: number): Value {
1368
+ return getEdgeValue(this._style.position, edge)
1369
+ }
1370
+
1371
+ /**
1372
+ * Get the position type.
1373
+ *
1374
+ * @returns Position type constant
1375
+ */
1376
+ getPositionType(): number {
1377
+ return this._style.positionType
1378
+ }
1379
+
1380
+ /**
1381
+ * Get the display type.
1382
+ *
1383
+ * @returns Display constant
1384
+ */
1385
+ getDisplay(): number {
1386
+ return this._style.display
1387
+ }
1388
+
1389
+ /**
1390
+ * Get the overflow setting.
1391
+ *
1392
+ * @returns Overflow constant
1393
+ */
1394
+ getOverflow(): number {
1395
+ return this._style.overflow
1396
+ }
1397
+
1398
+ /**
1399
+ * Get the gap for column or row.
1400
+ *
1401
+ * @param gutter - GUTTER_COLUMN or GUTTER_ROW
1402
+ * @returns Gap size in points
1403
+ */
1404
+ getGap(gutter: number): number {
1405
+ if (gutter === C.GUTTER_COLUMN) {
1406
+ return this._style.gap[0]
1407
+ } else if (gutter === C.GUTTER_ROW) {
1408
+ return this._style.gap[1]
1409
+ }
1410
+ return this._style.gap[0] // Default to column gap
1411
+ }
1412
+ }