Base interface and class definitions, utils classes.

TreeNode, HierarchyTreeNode, PropertyTreeNode, TraceRect
PropertiesProvider
Operation, OperationChain
TraceRect
TestNodeUtils, GeometryUtils, RawDataUtils, TransformUtils

Bug: b/311643292
Test: npm run test:unit:ci
Change-Id: I8508dff559eb8041f92982f7b74e960a5f5910ab
diff --git a/tools/winscope/src/app/components/timeline/expanded-timeline/abstract_timeline_row_component.ts b/tools/winscope/src/app/components/timeline/expanded-timeline/abstract_timeline_row_component.ts
index dd9ab56..f41a9ec 100644
--- a/tools/winscope/src/app/components/timeline/expanded-timeline/abstract_timeline_row_component.ts
+++ b/tools/winscope/src/app/components/timeline/expanded-timeline/abstract_timeline_row_component.ts
@@ -15,7 +15,7 @@
  */
 
 import {ElementRef, EventEmitter, SimpleChanges} from '@angular/core';
-import {Point} from 'common/geometry_utils';
+import {Point} from 'common/geometry_types';
 import {TraceEntry} from 'trace/trace';
 import {TracePosition} from 'trace/trace_position';
 import {CanvasDrawer} from './canvas_drawer';
diff --git a/tools/winscope/src/app/components/timeline/expanded-timeline/canvas_drawer.ts b/tools/winscope/src/app/components/timeline/expanded-timeline/canvas_drawer.ts
index 2190fca..d1f2efd 100644
--- a/tools/winscope/src/app/components/timeline/expanded-timeline/canvas_drawer.ts
+++ b/tools/winscope/src/app/components/timeline/expanded-timeline/canvas_drawer.ts
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-import {Rect} from 'common/geometry_utils';
+import {Rect} from 'common/rect';
 
 export class CanvasDrawer {
   private canvas!: HTMLCanvasElement;
diff --git a/tools/winscope/src/app/components/timeline/expanded-timeline/canvas_drawer_test.ts b/tools/winscope/src/app/components/timeline/expanded-timeline/canvas_drawer_test.ts
index b759cec..626225f 100644
--- a/tools/winscope/src/app/components/timeline/expanded-timeline/canvas_drawer_test.ts
+++ b/tools/winscope/src/app/components/timeline/expanded-timeline/canvas_drawer_test.ts
@@ -15,6 +15,7 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
+import {Rect} from 'common/rect';
 import {CanvasDrawer} from './canvas_drawer';
 
 describe('CanvasDrawer', () => {
@@ -24,7 +25,7 @@
 
     const canvasDrawer = new CanvasDrawer();
     canvasDrawer.setCanvas(actualCanvas);
-    canvasDrawer.drawRect({x: 10, y: 10, w: 10, h: 10}, '#333333', 1.0);
+    canvasDrawer.drawRect(new Rect(10, 10, 10, 10), '#333333', 1.0);
 
     expect(pixelsAllMatch(actualCanvas, expectedCanvas)).toBeFalse();
 
@@ -39,7 +40,7 @@
 
     const canvasDrawer = new CanvasDrawer();
     canvasDrawer.setCanvas(actualCanvas);
-    canvasDrawer.drawRect({x: 10, y: 10, w: 10, h: 10}, '#333333', 1.0);
+    canvasDrawer.drawRect(new Rect(10, 10, 10, 10), '#333333', 1.0);
 
     const expectedCtx = assertDefined(expectedCanvas.getContext('2d'));
     expectedCtx.fillStyle = '#333333';
@@ -55,7 +56,7 @@
 
     const canvasDrawer = new CanvasDrawer();
     canvasDrawer.setCanvas(actualCanvas);
-    canvasDrawer.drawRect({x: 10, y: 10, w: 10, h: 10}, '#333333', 0.5);
+    canvasDrawer.drawRect(new Rect(10, 10, 10, 10), '#333333', 0.5);
 
     const expectedCtx = assertDefined(expectedCanvas.getContext('2d'));
     expectedCtx.fillStyle = 'rgba(51,51,51,0.5)';
@@ -71,7 +72,7 @@
 
     const canvasDrawer = new CanvasDrawer();
     canvasDrawer.setCanvas(actualCanvas);
-    canvasDrawer.drawRectBorder({x: 10, y: 10, w: 10, h: 10});
+    canvasDrawer.drawRectBorder(new Rect(10, 10, 10, 10));
 
     const expectedCtx = assertDefined(expectedCanvas.getContext('2d'));
 
@@ -93,8 +94,8 @@
 
     const canvasDrawer = new CanvasDrawer();
     canvasDrawer.setCanvas(actualCanvas);
-    canvasDrawer.drawRect({x: 200, y: 200, w: 10, h: 10}, '#333333', 1.0);
-    canvasDrawer.drawRect({x: 95, y: 95, w: 50, h: 50}, '#333333', 1.0);
+    canvasDrawer.drawRect(new Rect(200, 200, 10, 10), '#333333', 1.0);
+    canvasDrawer.drawRect(new Rect(95, 95, 50, 50), '#333333', 1.0);
 
     const expectedCtx = assertDefined(expectedCanvas.getContext('2d'));
     expectedCtx.fillStyle = '#333333';
diff --git a/tools/winscope/src/app/components/timeline/expanded-timeline/default_timeline_row_component.ts b/tools/winscope/src/app/components/timeline/expanded-timeline/default_timeline_row_component.ts
index 1168081..9b126bf 100644
--- a/tools/winscope/src/app/components/timeline/expanded-timeline/default_timeline_row_component.ts
+++ b/tools/winscope/src/app/components/timeline/expanded-timeline/default_timeline_row_component.ts
@@ -15,7 +15,8 @@
  */
 
 import {Component, ElementRef, EventEmitter, Input, Output, ViewChild} from '@angular/core';
-import {GeometryUtils, Point, Rect} from 'common/geometry_utils';
+import {Point} from 'common/geometry_types';
+import {Rect} from 'common/rect';
 import {TimeRange, Timestamp} from 'common/time';
 import {Trace, TraceEntry} from 'trace/trace';
 import {TracePosition} from 'trace/trace_position';
@@ -102,7 +103,7 @@
     if (candidateEntry !== undefined) {
       const timestamp = candidateEntry.getTimestamp();
       const rect = this.entryRect(timestamp);
-      if (GeometryUtils.isPointInRect(mousePoint, rect)) {
+      if (rect.containsPoint(mousePoint)) {
         return candidateEntry;
       }
     }
@@ -121,12 +122,12 @@
   private entryRect(entry: Timestamp, padding = 0): Rect {
     const xPos = this.getXPosOf(entry);
 
-    return {
-      x: xPos + padding,
-      y: padding,
-      w: this.entryWidth - 2 * padding,
-      h: this.entryWidth - 2 * padding,
-    };
+    return new Rect(
+      xPos + padding,
+      padding,
+      this.entryWidth - 2 * padding,
+      this.entryWidth - 2 * padding
+    );
   }
 
   private getXPosOf(entry: Timestamp): number {
diff --git a/tools/winscope/src/app/components/timeline/expanded-timeline/default_timeline_row_component_test.ts b/tools/winscope/src/app/components/timeline/expanded-timeline/default_timeline_row_component_test.ts
index 6b3fd40..74332d5 100644
--- a/tools/winscope/src/app/components/timeline/expanded-timeline/default_timeline_row_component_test.ts
+++ b/tools/winscope/src/app/components/timeline/expanded-timeline/default_timeline_row_component_test.ts
@@ -26,6 +26,7 @@
 import {MatTooltipModule} from '@angular/material/tooltip';
 import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
 import {assertDefined} from 'common/assert_utils';
+import {Rect} from 'common/rect';
 import {RealTimestamp} from 'common/time';
 import {TraceBuilder} from 'test/unit/trace_builder';
 import {waitToBeCalled} from 'test/utils';
@@ -81,43 +82,19 @@
     const canvasWidth = component.canvasDrawer.getScaledCanvasWidth() - width;
 
     expect(drawRectSpy).toHaveBeenCalledTimes(4);
+    expect(drawRectSpy).toHaveBeenCalledWith(new Rect(0, 0, width, height), component.color, alpha);
     expect(drawRectSpy).toHaveBeenCalledWith(
-      {
-        x: 0,
-        y: 0,
-        w: width,
-        h: height,
-      },
+      new Rect(Math.floor((canvasWidth * 2) / 100), 0, width, height),
       component.color,
       alpha
     );
     expect(drawRectSpy).toHaveBeenCalledWith(
-      {
-        x: Math.floor((canvasWidth * 2) / 100),
-        y: 0,
-        w: width,
-        h: height,
-      },
+      new Rect(Math.floor((canvasWidth * 5) / 100), 0, width, height),
       component.color,
       alpha
     );
     expect(drawRectSpy).toHaveBeenCalledWith(
-      {
-        x: Math.floor((canvasWidth * 5) / 100),
-        y: 0,
-        w: width,
-        h: height,
-      },
-      component.color,
-      alpha
-    );
-    expect(drawRectSpy).toHaveBeenCalledWith(
-      {
-        x: Math.floor((canvasWidth * 60) / 100),
-        y: 0,
-        w: width,
-        h: height,
-      },
+      new Rect(Math.floor((canvasWidth * 60) / 100), 0, width, height),
       component.color,
       alpha
     );
@@ -141,12 +118,7 @@
 
     expect(drawRectSpy).toHaveBeenCalledTimes(1);
     expect(drawRectSpy).toHaveBeenCalledWith(
-      {
-        x: Math.floor((canvasWidth * 10) / 25),
-        y: 0,
-        w: width,
-        h: height,
-      },
+      new Rect(Math.floor((canvasWidth * 10) / 25), 0, width, height),
       component.color,
       alpha
     );
@@ -177,24 +149,10 @@
 
     expect(assertDefined(component.hoveringEntry).getValueNs()).toBe(10n);
     expect(drawRectSpy).toHaveBeenCalledTimes(1);
-    expect(drawRectSpy).toHaveBeenCalledWith(
-      {
-        x: 0,
-        y: 0,
-        w: 32,
-        h: 32,
-      },
-      component.color,
-      1.0
-    );
+    expect(drawRectSpy).toHaveBeenCalledWith(new Rect(0, 0, 32, 32), component.color, 1.0);
 
     expect(drawRectBorderSpy).toHaveBeenCalledTimes(1);
-    expect(drawRectBorderSpy).toHaveBeenCalledWith({
-      x: 0,
-      y: 0,
-      w: 32,
-      h: 32,
-    });
+    expect(drawRectBorderSpy).toHaveBeenCalledWith(new Rect(0, 0, 32, 32));
   });
 
   it('can draw correct entry on click of first entry', async () => {
@@ -289,12 +247,7 @@
       expectedTimestampNs
     );
 
-    const expectedRect = {
-      x: xPos + 1,
-      y: 1,
-      w: 30,
-      h: 30,
-    };
+    const expectedRect = new Rect(xPos + 1, 1, 30, 30);
 
     expect(drawRectSpy).toHaveBeenCalledTimes(rectSpyCalls);
     expect(drawRectSpy).toHaveBeenCalledWith(expectedRect, component.color, 1.0);
diff --git a/tools/winscope/src/app/components/timeline/expanded-timeline/transition_timeline_component.ts b/tools/winscope/src/app/components/timeline/expanded-timeline/transition_timeline_component.ts
index 836ab44..4f7909c 100644
--- a/tools/winscope/src/app/components/timeline/expanded-timeline/transition_timeline_component.ts
+++ b/tools/winscope/src/app/components/timeline/expanded-timeline/transition_timeline_component.ts
@@ -15,7 +15,8 @@
  */
 
 import {Component, ElementRef, EventEmitter, Input, Output, ViewChild} from '@angular/core';
-import {GeometryUtils, Point, Rect} from 'common/geometry_utils';
+import {Point} from 'common/geometry_types';
+import {Rect} from 'common/rect';
 import {ElapsedTimestamp, RealTimestamp, TimeRange, Timestamp, TimestampType} from 'common/time';
 import {Transition} from 'flickerlib/common';
 import {Trace, TraceEntry} from 'trace/trace';
@@ -150,7 +151,7 @@
           const transitionSegment = await this.getSegmentForTransition(entry);
           const rowToUse = this.getRowToUseFor(entry);
           const rect = this.getSegmentRect(transitionSegment.from, transitionSegment.to, rowToUse);
-          if (GeometryUtils.isPointInRect(mousePoint, rect)) {
+          if (rect.containsPoint(mousePoint)) {
             return entry;
           }
           return undefined;
@@ -206,7 +207,7 @@
     const padding = 5;
     const rowHeight = totalRowHeight - padding;
 
-    return {x: xPosStart, y: borderPadding + rowToUse * totalRowHeight, w: width, h: rowHeight};
+    return new Rect(xPosStart, borderPadding + rowToUse * totalRowHeight, width, rowHeight);
   }
 
   override async drawTimeline() {
diff --git a/tools/winscope/src/app/components/timeline/expanded-timeline/transition_timeline_component_test.ts b/tools/winscope/src/app/components/timeline/expanded-timeline/transition_timeline_component_test.ts
index a5bbd3e..841d4c7 100644
--- a/tools/winscope/src/app/components/timeline/expanded-timeline/transition_timeline_component_test.ts
+++ b/tools/winscope/src/app/components/timeline/expanded-timeline/transition_timeline_component_test.ts
@@ -25,6 +25,7 @@
 import {MatSelectModule} from '@angular/material/select';
 import {MatTooltipModule} from '@angular/material/tooltip';
 import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import {Rect} from 'common/rect';
 import {RealTimestamp} from 'common/time';
 import {Transition} from 'flickerlib/common';
 import {TraceBuilder} from 'test/unit/trace_builder';
@@ -96,22 +97,12 @@
 
     expect(drawRectSpy).toHaveBeenCalledTimes(2);
     expect(drawRectSpy).toHaveBeenCalledWith(
-      {
-        x: 0,
-        y: padding,
-        w: Math.floor(width / 5),
-        h: oneRowHeight,
-      },
+      new Rect(0, padding, Math.floor(width / 5), oneRowHeight),
       component.color,
       1
     );
     expect(drawRectSpy).toHaveBeenCalledWith(
-      {
-        x: Math.floor(width / 2),
-        y: padding,
-        w: Math.floor(width / 2),
-        h: oneRowHeight,
-      },
+      new Rect(Math.floor(width / 2), padding, Math.floor(width / 2), oneRowHeight),
       component.color,
       1
     );
@@ -147,22 +138,12 @@
 
     expect(drawRectSpy).toHaveBeenCalledTimes(2);
     expect(drawRectSpy).toHaveBeenCalledWith(
-      {
-        x: -Math.floor(width / 10),
-        y: padding,
-        w: Math.floor(width / 5),
-        h: oneRowHeight,
-      },
+      new Rect(-Math.floor(width / 10), padding, Math.floor(width / 5), oneRowHeight),
       component.color,
       1
     );
     expect(drawRectSpy).toHaveBeenCalledWith(
-      {
-        x: Math.floor(width / 2),
-        y: padding,
-        w: Math.floor(width),
-        h: oneRowHeight,
-      },
+      new Rect(Math.floor(width / 2), padding, Math.floor(width), oneRowHeight),
       component.color,
       1
     );
@@ -197,12 +178,12 @@
     const oneRowHeight = oneRowTotalHeight - padding;
     const width = component.canvasDrawer.getScaledCanvasWidth();
 
-    const expectedRect = {
-      x: Math.floor((width * 1) / 4),
-      y: padding,
-      w: Math.floor(width / 2),
-      h: oneRowHeight,
-    };
+    const expectedRect = new Rect(
+      Math.floor((width * 1) / 4),
+      padding,
+      Math.floor(width / 2),
+      oneRowHeight
+    );
     expect(drawRectSpy).toHaveBeenCalledTimes(2); // once drawn as a normal entry another time with rect border
     expect(drawRectSpy).toHaveBeenCalledWith(expectedRect, component.color, 0.25);
 
@@ -244,12 +225,12 @@
     await waitToBeCalled(drawRectSpy, 1);
     await waitToBeCalled(drawRectBorderSpy, 1);
 
-    const expectedRect = {
-      x: Math.floor((width * 1) / 4),
-      y: padding,
-      w: Math.floor(width / 2),
-      h: oneRowHeight,
-    };
+    const expectedRect = new Rect(
+      Math.floor((width * 1) / 4),
+      padding,
+      Math.floor(width / 2),
+      oneRowHeight
+    );
     expect(drawRectSpy).toHaveBeenCalledTimes(1);
     expect(drawRectSpy).toHaveBeenCalledWith(expectedRect, component.color, 0.25);
 
@@ -288,22 +269,17 @@
 
     expect(drawRectSpy).toHaveBeenCalledTimes(2);
     expect(drawRectSpy).toHaveBeenCalledWith(
-      {
-        x: 0,
-        y: padding,
-        w: Math.floor((width * 3) / 4),
-        h: oneRowHeight,
-      },
+      new Rect(0, padding, Math.floor((width * 3) / 4), oneRowHeight),
       component.color,
       1
     );
     expect(drawRectSpy).toHaveBeenCalledWith(
-      {
-        x: Math.floor(width / 2),
-        y: padding + oneRowTotalHeight,
-        w: Math.floor(width / 2),
-        h: oneRowHeight,
-      },
+      new Rect(
+        Math.floor(width / 2),
+        padding + oneRowTotalHeight,
+        Math.floor(width / 2),
+        oneRowHeight
+      ),
       component.color,
       1
     );
@@ -340,22 +316,17 @@
 
     expect(drawRectSpy).toHaveBeenCalledTimes(2);
     expect(drawRectSpy).toHaveBeenCalledWith(
-      {
-        x: 0,
-        y: padding,
-        w: Math.floor((width * 3) / 4),
-        h: oneRowHeight,
-      },
+      new Rect(0, padding, Math.floor((width * 3) / 4), oneRowHeight),
       component.color,
       1
     );
     expect(drawRectSpy).toHaveBeenCalledWith(
-      {
-        x: Math.floor(width / 4),
-        y: padding + oneRowTotalHeight,
-        w: Math.floor(width / 4),
-        h: oneRowHeight,
-      },
+      new Rect(
+        Math.floor(width / 4),
+        padding + oneRowTotalHeight,
+        Math.floor(width / 4),
+        oneRowHeight
+      ),
       component.color,
       1
     );
@@ -388,12 +359,7 @@
 
     expect(drawRectSpy).toHaveBeenCalledTimes(1);
     expect(drawRectSpy).toHaveBeenCalledWith(
-      {
-        x: Math.floor((width * 1) / 4),
-        y: padding,
-        w: Math.floor(width / 2),
-        h: oneRowHeight,
-      },
+      new Rect(Math.floor((width * 1) / 4), padding, Math.floor(width / 2), oneRowHeight),
       component.color,
       0.25
     );
diff --git a/tools/winscope/src/app/components/timeline/mini-timeline/drawer/canvas_mouse_handler_impl.ts b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/canvas_mouse_handler_impl.ts
index dccdd8d..28faa91 100644
--- a/tools/winscope/src/app/components/timeline/mini-timeline/drawer/canvas_mouse_handler_impl.ts
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/canvas_mouse_handler_impl.ts
@@ -15,7 +15,7 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {Point} from 'common/geometry_utils';
+import {Point} from 'common/geometry_types';
 import {CanvasMouseHandler, DragListener, DropListener} from './canvas_mouse_handler';
 import {DraggableCanvasObject} from './draggable_canvas_object';
 import {MiniTimelineDrawer} from './mini_timeline_drawer';
diff --git a/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_timeline_drawer_impl.ts b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_timeline_drawer_impl.ts
index e739128..b0d4995 100644
--- a/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_timeline_drawer_impl.ts
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_timeline_drawer_impl.ts
@@ -16,7 +16,7 @@
 
 import {Color} from 'app/colors';
 import {TRACE_INFO} from 'app/trace_info';
-import {Point} from 'common/geometry_utils';
+import {Point} from 'common/geometry_types';
 import {Padding} from 'common/padding';
 import {Timestamp} from 'common/time';
 import {CanvasMouseHandler} from './canvas_mouse_handler';
diff --git a/tools/winscope/src/app/components/timeline/mini-timeline/slider_component.ts b/tools/winscope/src/app/components/timeline/mini-timeline/slider_component.ts
index 5badc62..1c46105 100644
--- a/tools/winscope/src/app/components/timeline/mini-timeline/slider_component.ts
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/slider_component.ts
@@ -28,7 +28,7 @@
 } from '@angular/core';
 import {Color} from 'app/colors';
 import {assertDefined} from 'common/assert_utils';
-import {Point} from 'common/geometry_utils';
+import {Point} from 'common/geometry_types';
 import {TimeRange, Timestamp} from 'common/time';
 import {TracePosition} from 'trace/trace_position';
 import {Transformer} from './transformer';
diff --git a/tools/winscope/src/common/geometry_utils.ts b/tools/winscope/src/common/geometry_types.ts
similarity index 73%
rename from tools/winscope/src/common/geometry_utils.ts
rename to tools/winscope/src/common/geometry_types.ts
index d426d18..c485ce9 100644
--- a/tools/winscope/src/common/geometry_utils.ts
+++ b/tools/winscope/src/common/geometry_types.ts
@@ -19,13 +19,6 @@
   y: number;
 }
 
-export interface Rect {
-  x: number;
-  y: number;
-  w: number;
-  h: number;
-}
-
 export interface TransformMatrix {
   dsdx: number;
   dtdx: number;
@@ -36,14 +29,3 @@
 }
 
 export const IDENTITY_MATRIX = {dsdx: 1, dtdx: 0, tx: 0, dsdy: 0, dtdy: 1, ty: 0};
-
-export class GeometryUtils {
-  static isPointInRect(point: Point, rect: Rect): boolean {
-    return (
-      rect.x <= point.x &&
-      point.x <= rect.x + rect.w &&
-      rect.y <= point.y &&
-      point.y <= rect.y + rect.h
-    );
-  }
-}
diff --git a/tools/winscope/src/common/rect.ts b/tools/winscope/src/common/rect.ts
new file mode 100644
index 0000000..e703c16
--- /dev/null
+++ b/tools/winscope/src/common/rect.ts
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Point} from 'common/geometry_types';
+import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
+
+export class Rect {
+  constructor(public x: number, public y: number, public w: number, public h: number) {}
+
+  static from(node: PropertyTreeNode): Rect {
+    const left = node.getChildByName('left')?.getValue() ?? 0;
+    const top = node.getChildByName('top')?.getValue() ?? 0;
+    const right = node.getChildByName('right')?.getValue() ?? 0;
+    const bottom = node.getChildByName('bottom')?.getValue() ?? 0;
+    return new Rect(left, top, right - left, bottom - top);
+  }
+
+  containsPoint(point: Point): boolean {
+    return (
+      this.x <= point.x &&
+      point.x <= this.x + this.w &&
+      this.y <= point.y &&
+      point.y <= this.y + this.h
+    );
+  }
+
+  cropRect(other: Rect): Rect {
+    const maxLeft = Math.max(this.x, other.x);
+    const minRight = Math.min(this.x + this.w, other.x + other.w);
+    const maxTop = Math.max(this.y, other.y);
+    const minBottom = Math.min(this.y + this.h, other.y + other.h);
+    return new Rect(maxLeft, maxTop, minRight - maxLeft, minBottom - maxTop);
+  }
+
+  containsRect(other: Rect): boolean {
+    return (
+      this.w > 0 &&
+      this.h > 0 &&
+      this.x <= other.x &&
+      this.y <= other.y &&
+      this.x + this.w >= other.x + other.w &&
+      this.y + this.h >= other.y + other.h
+    );
+  }
+
+  intersectsRect(other: Rect): boolean {
+    if (
+      this.x < other.x + other.w &&
+      other.x < this.x + this.w &&
+      this.y <= other.y + other.h &&
+      other.y <= this.y + this.h
+    ) {
+      const intersectionRect = new Rect(this.x, this.y, this.w, this.h);
+
+      if (this.x < other.x) {
+        intersectionRect.x = other.x;
+      }
+      if (this.y < other.y) {
+        intersectionRect.y = other.y;
+      }
+      if (this.x + this.w > other.x + other.w) {
+        intersectionRect.w = other.w;
+      }
+      if (this.y + this.h > other.y + other.h) {
+        intersectionRect.h = other.h;
+      }
+
+      return !intersectionRect.isEmpty();
+    }
+
+    return false;
+  }
+
+  isEmpty(): boolean {
+    const [x, y, w, h] = [this.x, this.y, this.w, this.h];
+    const nullValuePresent = x === -1 || y === -1 || x + w === -1 || y + h === -1;
+    const nullHeightOrWidth = w <= 0 || h <= 0;
+    return nullValuePresent || nullHeightOrWidth;
+  }
+}
diff --git a/tools/winscope/src/parsers/parser_factory.ts b/tools/winscope/src/parsers/parser_factory.ts
index f262239..4bd566b 100644
--- a/tools/winscope/src/parsers/parser_factory.ts
+++ b/tools/winscope/src/parsers/parser_factory.ts
@@ -27,13 +27,13 @@
 import {ParserProtoLog} from './parser_protolog';
 import {ParserScreenRecording} from './parser_screen_recording';
 import {ParserScreenRecordingLegacy} from './parser_screen_recording_legacy';
-import {ParserSurfaceFlinger} from './parser_surface_flinger';
 import {ParserTransactions} from './parser_transactions';
 import {ParserTransitionsShell} from './parser_transitions_shell';
 import {ParserTransitionsWm} from './parser_transitions_wm';
 import {ParserViewCapture} from './parser_view_capture';
 import {ParserWindowManager} from './parser_window_manager';
 import {ParserWindowManagerDump} from './parser_window_manager_dump';
+import {ParserSurfaceFlinger} from './surface_flinger/parser_surface_flinger';
 
 export class ParserFactory {
   static readonly PARSERS = [
diff --git a/tools/winscope/src/parsers/raw_data_utils.ts b/tools/winscope/src/parsers/raw_data_utils.ts
new file mode 100644
index 0000000..819a887
--- /dev/null
+++ b/tools/winscope/src/parsers/raw_data_utils.ts
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Rect} from 'common/rect';
+import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
+
+export class RawDataUtils {
+  static isEmptyObj(obj: PropertyTreeNode): boolean {
+    if (RawDataUtils.isColor(obj)) {
+      return RawDataUtils.isEmptyColor(obj);
+    }
+
+    if (RawDataUtils.isRect(obj)) {
+      return Rect.from(obj).isEmpty();
+    }
+
+    return false;
+  }
+
+  static isColor(obj: PropertyTreeNode): boolean {
+    return obj.getChildByName('a') !== undefined;
+  }
+
+  static isRect(obj: PropertyTreeNode): boolean {
+    return (
+      (obj.getChildByName('right') !== undefined && obj.getChildByName('bottom') !== undefined) ||
+      (obj.getChildByName('left') !== undefined && obj.getChildByName('top') !== undefined)
+    );
+  }
+
+  static isBuffer(obj: PropertyTreeNode): boolean {
+    return obj.getChildByName('stride') !== undefined && obj.getChildByName('format') !== undefined;
+  }
+
+  static isSize(obj: PropertyTreeNode): boolean {
+    return (
+      obj.getAllChildren().length <= 2 &&
+      (obj.getChildByName('w') !== undefined || obj.getChildByName('h') !== undefined)
+    );
+  }
+
+  static isPosition(obj: PropertyTreeNode): boolean {
+    return (
+      obj.getAllChildren().length <= 2 &&
+      (obj.getChildByName('x') !== undefined || obj.getChildByName('y') !== undefined)
+    );
+  }
+
+  static isRegion(obj: PropertyTreeNode): boolean {
+    const rect = obj.getChildByName('rect');
+    return (
+      rect !== undefined &&
+      rect.getAllChildren().every((innerRect: PropertyTreeNode) => RawDataUtils.isRect(innerRect))
+    );
+  }
+
+  private static isEmptyColor(color: PropertyTreeNode): boolean {
+    const [r, g, b, a] = [
+      color.getChildByName('r')?.getValue() ?? 0,
+      color.getChildByName('g')?.getValue() ?? 0,
+      color.getChildByName('b')?.getValue() ?? 0,
+      color.getChildByName('a')?.getValue() ?? 0,
+    ];
+    if (a === 0) return true;
+    return r < 0 || g < 0 || b < 0 || (r === 0 && g === 0 && b === 0);
+  }
+}
diff --git a/tools/winscope/src/parsers/raw_data_utils_test.ts b/tools/winscope/src/parsers/raw_data_utils_test.ts
new file mode 100644
index 0000000..b64951a
--- /dev/null
+++ b/tools/winscope/src/parsers/raw_data_utils_test.ts
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {TreeNodeUtils} from 'test/unit/tree_node_utils';
+import {PropertySource, PropertyTreeNode} from 'trace/tree_node/property_tree_node';
+import {RawDataUtils} from './raw_data_utils';
+
+describe('RawDataUtils', () => {
+  it('identifies color', () => {
+    const color = TreeNodeUtils.makeColorNode(0, 0, 0, 1);
+    expect(RawDataUtils.isColor(color)).toBeTrue();
+
+    const colorOnlyA = TreeNodeUtils.makeColorNode(undefined, undefined, undefined, 1);
+    expect(RawDataUtils.isColor(colorOnlyA)).toBeTrue();
+  });
+
+  it('identifies rect', () => {
+    const rect = TreeNodeUtils.makeRectNode(0, 0, 1, 1);
+    expect(RawDataUtils.isRect(rect)).toBeTrue();
+
+    const rectLeftTop = TreeNodeUtils.makeRectNode(0, 0, undefined, undefined);
+    expect(RawDataUtils.isRect(rectLeftTop)).toBeTrue();
+
+    const rectRightBottom = TreeNodeUtils.makeRectNode(undefined, undefined, 1, 1);
+    expect(RawDataUtils.isRect(rectRightBottom)).toBeTrue();
+  });
+
+  it('identifies buffer', () => {
+    const buffer = TreeNodeUtils.makeBufferNode();
+    expect(RawDataUtils.isBuffer(buffer)).toBeTrue();
+  });
+
+  it('identifies size', () => {
+    const size = TreeNodeUtils.makeSizeNode(0, 0);
+    expect(RawDataUtils.isSize(size)).toBeTrue();
+    expect(RawDataUtils.isBuffer(size)).toBeFalse();
+
+    const sizeOnlyW = TreeNodeUtils.makeSizeNode(0, undefined);
+    expect(RawDataUtils.isSize(sizeOnlyW)).toBeTrue();
+
+    const sizeOnlyH = TreeNodeUtils.makeSizeNode(undefined, 0);
+    expect(RawDataUtils.isSize(sizeOnlyH)).toBeTrue();
+
+    const notSize = TreeNodeUtils.makeSizeNode(0, 0);
+    notSize.addChild(new PropertyTreeNode('size.x', 'x', PropertySource.PROTO, 0));
+    notSize.addChild(new PropertyTreeNode('size.y', 'y', PropertySource.PROTO, 0));
+    expect(RawDataUtils.isSize(notSize)).toBeFalse();
+  });
+
+  it('identifies position', () => {
+    const pos = TreeNodeUtils.makePositionNode(0, 0);
+    expect(RawDataUtils.isPosition(pos)).toBeTrue();
+    expect(RawDataUtils.isRect(pos)).toBeFalse();
+
+    const posOnlyX = TreeNodeUtils.makePositionNode(0, undefined);
+    expect(RawDataUtils.isPosition(posOnlyX)).toBeTrue();
+
+    const posOnlyY = TreeNodeUtils.makePositionNode(undefined, 0);
+    expect(RawDataUtils.isPosition(posOnlyY)).toBeTrue();
+
+    const notPos = TreeNodeUtils.makePositionNode(0, 0);
+    notPos.addChild(new PropertyTreeNode('pos.w', 'w', PropertySource.PROTO, 0));
+    notPos.addChild(new PropertyTreeNode('pos.h', 'h', PropertySource.PROTO, 0));
+    expect(RawDataUtils.isPosition(notPos)).toBeFalse();
+  });
+
+  it('identifies region', () => {
+    const region = new PropertyTreeNode('region', 'region', PropertySource.PROTO, undefined);
+    const rect = new PropertyTreeNode('region.rect', 'rect', PropertySource.PROTO, []);
+    region.addChild(rect);
+    expect(RawDataUtils.isRegion(region)).toBeTrue();
+
+    rect.addChild(TreeNodeUtils.makeRectNode(0, 0, 1, 1));
+    rect.addChild(TreeNodeUtils.makeRectNode(0, 0, undefined, undefined));
+    rect.addChild(TreeNodeUtils.makeRectNode(undefined, undefined, 1, 1));
+    expect(RawDataUtils.isRegion(region)).toBeTrue();
+  });
+
+  it('identifies non-empty color and rect', () => {
+    const color = TreeNodeUtils.makeColorNode(0, 8, 0, 1);
+    const rect = TreeNodeUtils.makeRectNode(0, 0, 1, 1);
+
+    const isEmptyColor = RawDataUtils.isEmptyObj(color);
+    const isEmptyRect = RawDataUtils.isEmptyObj(rect);
+    expect(isEmptyColor).toBeFalse();
+    expect(isEmptyRect).toBeFalse();
+  });
+
+  it('identifies empty color and rect', () => {
+    const color = TreeNodeUtils.makeColorNode(-1, -1, undefined, 1);
+    const rect = TreeNodeUtils.makeRectNode(0, 0, undefined, undefined);
+    const otherColor = TreeNodeUtils.makeColorNode(1, 1, 1, 0);
+    const otherRect = TreeNodeUtils.makeRectNode(0, 0, 0, 0);
+
+    expect(RawDataUtils.isEmptyObj(color)).toBeTrue();
+    expect(RawDataUtils.isEmptyObj(rect)).toBeTrue();
+    expect(RawDataUtils.isEmptyObj(otherColor)).toBeTrue();
+    expect(RawDataUtils.isEmptyObj(otherRect)).toBeTrue();
+  });
+});
diff --git a/tools/winscope/src/parsers/parser_surface_flinger.ts b/tools/winscope/src/parsers/surface_flinger/parser_surface_flinger.ts
similarity index 98%
rename from tools/winscope/src/parsers/parser_surface_flinger.ts
rename to tools/winscope/src/parsers/surface_flinger/parser_surface_flinger.ts
index 6d73ef8..be22699 100644
--- a/tools/winscope/src/parsers/parser_surface_flinger.ts
+++ b/tools/winscope/src/parsers/surface_flinger/parser_surface_flinger.ts
@@ -17,6 +17,7 @@
 import {assertDefined} from 'common/assert_utils';
 import {Timestamp, TimestampType} from 'common/time';
 import {LayerTraceEntry} from 'flickerlib/layers/LayerTraceEntry';
+import {AbstractParser} from 'parsers/abstract_parser';
 import root from 'protos/surfaceflinger/udc/json';
 import {android} from 'protos/surfaceflinger/udc/static';
 import {
@@ -27,7 +28,6 @@
 import {EntriesRange} from 'trace/trace';
 import {TraceFile} from 'trace/trace_file';
 import {TraceType} from 'trace/trace_type';
-import {AbstractParser} from './abstract_parser';
 
 class ParserSurfaceFlinger extends AbstractParser {
   private static readonly LayersTraceFileProto = root.lookupType(
diff --git a/tools/winscope/src/parsers/parser_surface_flinger_dump_test.ts b/tools/winscope/src/parsers/surface_flinger/parser_surface_flinger_dump_test.ts
similarity index 100%
rename from tools/winscope/src/parsers/parser_surface_flinger_dump_test.ts
rename to tools/winscope/src/parsers/surface_flinger/parser_surface_flinger_dump_test.ts
diff --git a/tools/winscope/src/parsers/parser_surface_flinger_test.ts b/tools/winscope/src/parsers/surface_flinger/parser_surface_flinger_test.ts
similarity index 100%
rename from tools/winscope/src/parsers/parser_surface_flinger_test.ts
rename to tools/winscope/src/parsers/surface_flinger/parser_surface_flinger_test.ts
diff --git a/tools/winscope/src/parsers/surface_flinger/transform_utils.ts b/tools/winscope/src/parsers/surface_flinger/transform_utils.ts
new file mode 100644
index 0000000..d32220b
--- /dev/null
+++ b/tools/winscope/src/parsers/surface_flinger/transform_utils.ts
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {assertDefined} from 'common/assert_utils';
+import {IDENTITY_MATRIX, TransformMatrix} from 'common/geometry_types';
+import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
+
+export enum TransformType {
+  EMPTY = 0x0,
+  TRANSLATE_VAL = 0x0001,
+  ROTATE_VAL = 0x0002,
+  SCALE_VAL = 0x0004,
+  FLIP_H_VAL = 0x0100,
+  FLIP_V_VAL = 0x0200,
+  ROT_90_VAL = 0x0400,
+  ROT_INVALID_VAL = 0x8000,
+}
+
+export class Transform {
+  static EMPTY = new Transform(TransformType.EMPTY, IDENTITY_MATRIX);
+
+  constructor(public type: TransformType, public matrix: TransformMatrix) {}
+
+  static from(transformNode: PropertyTreeNode, position?: PropertyTreeNode): Transform {
+    if (transformNode.getAllChildren().length === 0) return Transform.EMPTY;
+
+    const transformType = transformNode.getChildByName('type')?.getValue() ?? 0;
+    const matrixNode = transformNode.getChildByName('matrix');
+
+    if (matrixNode) {
+      return new Transform(transformType, {
+        dsdx: assertDefined(matrixNode.getChildByName('dsdx')).getValue(),
+        dtdx: assertDefined(matrixNode.getChildByName('dtdx')).getValue(),
+        tx: assertDefined(matrixNode.getChildByName('tx')).getValue(),
+        dsdy: assertDefined(matrixNode.getChildByName('dsdy')).getValue(),
+        dtdy: assertDefined(matrixNode.getChildByName('dtdy')).getValue(),
+        ty: assertDefined(matrixNode.getChildByName('ty')).getValue(),
+      });
+    }
+
+    const x = position?.getChildByName('x')?.getValue() ?? 0;
+    const y = position?.getChildByName('y')?.getValue() ?? 0;
+
+    if (TransformUtils.isSimpleTransform(transformType)) {
+      return TransformUtils.getDefaultTransform(transformType, x, y);
+    }
+
+    return new Transform(transformType, {
+      dsdx: transformNode.getChildByName('dsdx')?.getValue() ?? 0,
+      dtdx: transformNode.getChildByName('dtdx')?.getValue() ?? 0,
+      tx: x,
+      dsdy: transformNode.getChildByName('dsdy')?.getValue() ?? 0,
+      dtdy: transformNode.getChildByName('dtdy')?.getValue() ?? 0,
+      ty: y,
+    });
+  }
+}
+
+export class TransformUtils {
+  static isValidTransform(transform: Transform): boolean {
+    return (
+      transform.matrix.dsdx * transform.matrix.dtdy !==
+      transform.matrix.dtdx * transform.matrix.dsdy
+    );
+  }
+
+  static isSimpleRotation(type: TransformType | undefined): boolean {
+    return !(type ? TransformUtils.isFlagSet(type, TransformType.ROT_INVALID_VAL) : false);
+  }
+
+  static getTypeFlags(type: TransformType): string {
+    const typeFlags: string[] = [];
+
+    if (
+      TransformUtils.isFlagClear(
+        type,
+        TransformType.SCALE_VAL | TransformType.ROTATE_VAL | TransformType.TRANSLATE_VAL
+      )
+    ) {
+      typeFlags.push('IDENTITY');
+    }
+
+    if (TransformUtils.isFlagSet(type, TransformType.SCALE_VAL)) {
+      typeFlags.push('SCALE');
+    }
+
+    if (TransformUtils.isFlagSet(type, TransformType.TRANSLATE_VAL)) {
+      typeFlags.push('TRANSLATE');
+    }
+
+    if (TransformUtils.isFlagSet(type, TransformType.ROT_INVALID_VAL)) {
+      typeFlags.push('ROT_INVALID');
+    } else if (
+      TransformUtils.isFlagSet(
+        type,
+        TransformType.ROT_90_VAL | TransformType.FLIP_V_VAL | TransformType.FLIP_H_VAL
+      )
+    ) {
+      typeFlags.push('ROT_270');
+    } else if (
+      TransformUtils.isFlagSet(type, TransformType.FLIP_V_VAL | TransformType.FLIP_H_VAL)
+    ) {
+      typeFlags.push('ROT_180');
+    } else {
+      if (TransformUtils.isFlagSet(type, TransformType.ROT_90_VAL)) {
+        typeFlags.push('ROT_90');
+      }
+      if (TransformUtils.isFlagSet(type, TransformType.FLIP_V_VAL)) {
+        typeFlags.push('FLIP_V');
+      }
+      if (TransformUtils.isFlagSet(type, TransformType.FLIP_H_VAL)) {
+        typeFlags.push('FLIP_H');
+      }
+    }
+
+    if (typeFlags.length === 0) {
+      throw Error(`Unknown transform type ${type}`);
+    }
+    return typeFlags.join('|');
+  }
+
+  static getDefaultTransform(type: TransformType, x: number, y: number): Transform {
+    // IDENTITY
+    if (!type) {
+      return new Transform(type, {dsdx: 1, dtdx: 0, tx: x, dsdy: 0, dtdy: 1, ty: y});
+    }
+
+    // ROT_270 = ROT_90|FLIP_H|FLIP_V
+    if (
+      TransformUtils.isFlagSet(
+        type,
+        TransformType.ROT_90_VAL | TransformType.FLIP_V_VAL | TransformType.FLIP_H_VAL
+      )
+    ) {
+      return new Transform(type, {dsdx: 0, dtdx: -1, tx: x, dsdy: 1, dtdy: 0, ty: y});
+    }
+
+    // ROT_180 = FLIP_H|FLIP_V
+    if (TransformUtils.isFlagSet(type, TransformType.FLIP_V_VAL | TransformType.FLIP_H_VAL)) {
+      return new Transform(type, {dsdx: -1, dtdx: 0, tx: x, dsdy: 0, dtdy: -1, ty: y});
+    }
+
+    // ROT_90
+    if (TransformUtils.isFlagSet(type, TransformType.ROT_90_VAL)) {
+      return new Transform(type, {dsdx: 0, dtdx: 1, tx: x, dsdy: -1, dtdy: 0, ty: y});
+    }
+
+    // IDENTITY
+    if (TransformUtils.isFlagClear(type, TransformType.SCALE_VAL | TransformType.ROTATE_VAL)) {
+      return new Transform(type, {dsdx: 1, dtdx: 0, tx: x, dsdy: 0, dtdy: 1, ty: y});
+    }
+
+    throw new Error(`Unknown transform type ${type}`);
+  }
+
+  static isSimpleTransform(type: TransformType): boolean {
+    return TransformUtils.isFlagClear(
+      type,
+      TransformType.ROT_INVALID_VAL | TransformType.SCALE_VAL
+    );
+  }
+
+  private static isFlagSet(type: TransformType, bits: number): boolean {
+    type = type || 0;
+    return (type & bits) === bits;
+  }
+
+  private static isFlagClear(type: TransformType, bits: number): boolean {
+    return (type & bits) === 0;
+  }
+}
diff --git a/tools/winscope/src/parsers/transform_utils.ts b/tools/winscope/src/parsers/transform_utils.ts
deleted file mode 100644
index 6a98c7e..0000000
--- a/tools/winscope/src/parsers/transform_utils.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import {IDENTITY_MATRIX, TransformMatrix} from 'common/geometry_utils';
-
-export class Transform {
-  constructor(public type: TransformType, public matrix: TransformMatrix) {}
-}
-
-export enum TransformType {
-  EMPTY = 0x0,
-  TRANSLATE_VAL = 0x0001,
-  ROTATE_VAL = 0x0002,
-  SCALE_VAL = 0x0004,
-  FLIP_H_VAL = 0x0100,
-  FLIP_V_VAL = 0x0200,
-  ROT_90_VAL = 0x0400,
-  ROT_INVALID_VAL = 0x8000,
-}
-
-export const EMPTY_TRANSFORM = new Transform(TransformType.EMPTY, IDENTITY_MATRIX);
-
-export class TransformUtils {
-  static isValidTransform(transform: any): boolean {
-    if (!transform) return false;
-    return transform.dsdx * transform.dtdy !== transform.dtdx * transform.dsdy;
-  }
-
-  static getTransform(transform: any, position: any): Transform {
-    const transformType = transform?.type ?? 0;
-    const x = position?.x ?? 0;
-    const y = position?.y ?? 0;
-
-    if (!transform || TransformUtils.isSimpleTransform(transformType)) {
-      return TransformUtils.getDefaultTransform(transformType, x, y);
-    }
-
-    return new Transform(transformType, {
-      dsdx: transform?.matrix.dsdx ?? 0,
-      dtdx: transform?.matrix.dtdx ?? 0,
-      tx: x,
-      dsdy: transform?.matrix.dsdy ?? 0,
-      dtdy: transform?.matrix.dtdy ?? 0,
-      ty: y,
-    });
-  }
-
-  static isSimpleRotation(transform: any): boolean {
-    return !(transform?.type
-      ? TransformUtils.isFlagSet(transform.type, TransformType.ROT_INVALID_VAL)
-      : false);
-  }
-
-  private static getDefaultTransform(type: TransformType, x: number, y: number): Transform {
-    // IDENTITY
-    if (!type) {
-      return new Transform(type, {dsdx: 1, dtdx: 0, tx: x, dsdy: 0, dtdy: 1, ty: y});
-    }
-
-    // ROT_270 = ROT_90|FLIP_H|FLIP_V
-    if (
-      TransformUtils.isFlagSet(
-        type,
-        TransformType.ROT_90_VAL | TransformType.FLIP_V_VAL | TransformType.FLIP_H_VAL
-      )
-    ) {
-      return new Transform(type, {dsdx: 0, dtdx: -1, tx: x, dsdy: 1, dtdy: 0, ty: y});
-    }
-
-    // ROT_180 = FLIP_H|FLIP_V
-    if (TransformUtils.isFlagSet(type, TransformType.FLIP_V_VAL | TransformType.FLIP_H_VAL)) {
-      return new Transform(type, {dsdx: -1, dtdx: 0, tx: x, dsdy: 0, dtdy: -1, ty: y});
-    }
-
-    // ROT_90
-    if (TransformUtils.isFlagSet(type, TransformType.ROT_90_VAL)) {
-      return new Transform(type, {dsdx: 0, dtdx: 1, tx: x, dsdy: -1, dtdy: 0, ty: y});
-    }
-
-    // IDENTITY
-    if (TransformUtils.isFlagClear(type, TransformType.SCALE_VAL | TransformType.ROTATE_VAL)) {
-      return new Transform(type, {dsdx: 1, dtdx: 0, tx: x, dsdy: 0, dtdy: 1, ty: y});
-    }
-
-    throw new Error(`Unknown transform type ${type}`);
-  }
-
-  private static isFlagSet(type: number, bits: number): boolean {
-    type = type || 0;
-    return (type & bits) === bits;
-  }
-
-  private static isFlagClear(type: number, bits: number): boolean {
-    return (type & bits) === 0;
-  }
-
-  private static isSimpleTransform(type: number): boolean {
-    return TransformUtils.isFlagClear(
-      type,
-      TransformType.ROT_INVALID_VAL | TransformType.SCALE_VAL
-    );
-  }
-}
diff --git a/tools/winscope/src/test/unit/tree_node_utils.ts b/tools/winscope/src/test/unit/tree_node_utils.ts
new file mode 100644
index 0000000..acab913
--- /dev/null
+++ b/tools/winscope/src/test/unit/tree_node_utils.ts
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {TransformType} from 'parsers/surface_flinger/transform_utils';
+import {PropertySource, PropertyTreeNode} from 'trace/tree_node/property_tree_node';
+
+export class TreeNodeUtils {
+  static makeRectNode(
+    left: number | undefined,
+    top: number | undefined,
+    right: number | undefined,
+    bottom: number | undefined,
+    id = 'test node'
+  ): PropertyTreeNode {
+    const rect = new PropertyTreeNode(`${id}.rect`, 'rect', PropertySource.PROTO, undefined);
+    if (left !== undefined) {
+      rect.addChild(new PropertyTreeNode(`${id}.rect.left`, 'left', PropertySource.PROTO, left));
+    }
+    if (top !== undefined) {
+      rect.addChild(new PropertyTreeNode(`${id}.rect.top`, 'top', PropertySource.PROTO, top));
+    }
+    if (right !== undefined) {
+      rect.addChild(new PropertyTreeNode(`${id}.rect.right`, 'right', PropertySource.PROTO, right));
+    }
+    if (bottom !== undefined) {
+      rect.addChild(
+        new PropertyTreeNode(`${id}.rect.bottom`, 'bottom', PropertySource.PROTO, bottom)
+      );
+    }
+    return rect;
+  }
+
+  static makeColorNode(
+    r: number | undefined,
+    g: number | undefined,
+    b: number | undefined,
+    a: number | undefined
+  ): PropertyTreeNode {
+    const color = new PropertyTreeNode('test node.color', 'color', PropertySource.PROTO, undefined);
+    if (r !== undefined) {
+      color.addChild(new PropertyTreeNode('test node.color.r', 'r', PropertySource.PROTO, r));
+    }
+    if (g !== undefined) {
+      color.addChild(new PropertyTreeNode('test node.color.g', 'g', PropertySource.PROTO, g));
+    }
+    if (b !== undefined) {
+      color.addChild(new PropertyTreeNode('test node.color.b', 'b', PropertySource.PROTO, b));
+    }
+    if (a !== undefined) {
+      color.addChild(new PropertyTreeNode('test node.color.a', 'a', PropertySource.PROTO, a));
+    }
+    return color;
+  }
+
+  static makeBufferNode(): PropertyTreeNode {
+    const buffer = new PropertyTreeNode(
+      'test node.buffer',
+      'buffer',
+      PropertySource.PROTO,
+      undefined
+    );
+    buffer.addChild(
+      new PropertyTreeNode('test node.buffer.height', 'height', PropertySource.PROTO, 0)
+    );
+    buffer.addChild(
+      new PropertyTreeNode('test node.buffer.width', 'width', PropertySource.PROTO, 1)
+    );
+    buffer.addChild(
+      new PropertyTreeNode('test node.buffer.stride', 'stride', PropertySource.PROTO, 0)
+    );
+    buffer.addChild(
+      new PropertyTreeNode('test node.buffer.format', 'format', PropertySource.PROTO, 1)
+    );
+    return buffer;
+  }
+
+  static makeTransformNode(type: TransformType): PropertyTreeNode {
+    const transform = new PropertyTreeNode(
+      'test node.transform',
+      'transform',
+      PropertySource.PROTO,
+      undefined
+    );
+    transform.addChild(
+      new PropertyTreeNode('test node.transform.type', 'type', PropertySource.PROTO, type)
+    );
+    return transform;
+  }
+
+  static makeSizeNode(w: number | undefined, h: number | undefined): PropertyTreeNode {
+    const size = new PropertyTreeNode('test node.size', 'size', PropertySource.PROTO, undefined);
+    if (w !== undefined) {
+      size.addChild(new PropertyTreeNode('test node.size.w', 'w', PropertySource.PROTO, w));
+    }
+    if (h !== undefined) {
+      size.addChild(new PropertyTreeNode('test node.size.h', 'h', PropertySource.PROTO, h));
+    }
+    return size;
+  }
+
+  static makePositionNode(x: number | undefined, y: number | undefined): PropertyTreeNode {
+    const pos = new PropertyTreeNode('test node.pos', 'pos', PropertySource.PROTO, undefined);
+    if (x !== undefined) {
+      pos.addChild(new PropertyTreeNode('test node.pos.x', 'x', PropertySource.PROTO, x));
+    }
+    if (y !== undefined) {
+      pos.addChild(new PropertyTreeNode('test node.pos.y', 'y', PropertySource.PROTO, y));
+    }
+    return pos;
+  }
+}
diff --git a/tools/winscope/src/trace/item.ts b/tools/winscope/src/trace/item.ts
new file mode 100644
index 0000000..b57867f
--- /dev/null
+++ b/tools/winscope/src/trace/item.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export interface Item {
+  id: string;
+  name: string;
+}
diff --git a/tools/winscope/src/trace/trace_data_utils.ts b/tools/winscope/src/trace/trace_data_utils.ts
deleted file mode 100644
index a6aaff5..0000000
--- a/tools/winscope/src/trace/trace_data_utils.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import {Rect} from 'common/geometry_utils';
-import {Transform} from 'parsers/transform_utils';
-
-/* DATA INTERFACES */
-class Item {
-  constructor(public id: string, public label: string) {}
-}
-
-class TreeNode<T> extends Item {
-  constructor(id: string, label: string, public children: Array<TreeNode<T> & T> = []) {
-    super(id, label);
-  }
-}
-
-class TraceRect extends Item implements Rect {
-  constructor(
-    id: string,
-    label: string,
-    public x: number,
-    public y: number,
-    public w: number,
-    public h: number,
-    public cornerRadius: number,
-    public transform: Transform,
-    public zAbs: number,
-    public groupId: number,
-    public isVisible: boolean,
-    public isDisplay: boolean,
-    public isVirtual: boolean
-  ) {
-    super(id, label);
-  }
-}
-
-type Proto = any;
-
-/* GET PROPERTIES TYPES */
-type GetPropertiesFromProtoType = (proto: Proto) => PropertyTreeNode;
-type GetPropertiesType = () => PropertyTreeNode;
-
-/* MIXINS */
-interface PropertiesGetter {
-  getProperties: GetPropertiesType;
-}
-
-enum PropertySource {
-  PROTO,
-  DEFAULT,
-  CALCULATED,
-}
-
-interface PropertyDetails {
-  value: string;
-  source: PropertySource;
-}
-
-interface AssociatedProperty {
-  property: null | PropertyDetails;
-}
-
-interface Constructor<T = {}> {
-  new (...args: any[]): T;
-}
-
-type PropertyTreeNode = TreeNode<AssociatedProperty> & AssociatedProperty;
-type HierarchyTreeNode = TreeNode<PropertiesGetter> & PropertiesGetter;
-
-const EMPTY_OBJ_STRING = '{empty}';
-const EMPTY_ARRAY_STRING = '[empty]';
-
-export {
-  Item,
-  TreeNode,
-  TraceRect,
-  Proto,
-  GetPropertiesType,
-  GetPropertiesFromProtoType,
-  PropertiesGetter,
-  PropertySource,
-  PropertyDetails,
-  AssociatedProperty,
-  Constructor,
-  PropertyTreeNode,
-  HierarchyTreeNode,
-  EMPTY_OBJ_STRING,
-  EMPTY_ARRAY_STRING,
-};
diff --git a/tools/winscope/src/trace/trace_rect.ts b/tools/winscope/src/trace/trace_rect.ts
new file mode 100644
index 0000000..6928f00
--- /dev/null
+++ b/tools/winscope/src/trace/trace_rect.ts
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {TransformMatrix} from 'common/geometry_types';
+import {Rect} from 'common/rect';
+import {Item} from 'trace/item';
+
+export class TraceRect extends Rect implements Item {
+  constructor(
+    x: number,
+    y: number,
+    w: number,
+    h: number,
+    readonly id: string,
+    readonly name: string,
+    readonly cornerRadius: number,
+    readonly transform: TransformMatrix,
+    readonly zOrderPath: number[],
+    readonly groupId: number,
+    readonly isVisible: boolean,
+    readonly isDisplay: boolean,
+    readonly isVirtual: boolean
+  ) {
+    super(x, y, w, h);
+  }
+}
diff --git a/tools/winscope/src/trace/trace_rect_builder.ts b/tools/winscope/src/trace/trace_rect_builder.ts
new file mode 100644
index 0000000..becf14a
--- /dev/null
+++ b/tools/winscope/src/trace/trace_rect_builder.ts
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {TransformMatrix} from 'common/geometry_types';
+import {TraceRect} from './trace_rect';
+
+export class TraceRectBuilder {
+  x: number | undefined;
+  y: number | undefined;
+  w: number | undefined;
+  h: number | undefined;
+  id: string | undefined;
+  name: string | undefined;
+  cornerRadius: number | undefined;
+  transform: TransformMatrix | undefined;
+  zOrderPath: number[] | undefined;
+  groupId: number | undefined;
+  isVisible: boolean | undefined;
+  isDisplay: boolean | undefined;
+  isVirtual: boolean | undefined;
+
+  setX(value: number) {
+    this.x = value;
+    return this;
+  }
+
+  setY(value: number) {
+    this.y = value;
+    return this;
+  }
+
+  setWidth(value: number) {
+    this.w = value;
+    return this;
+  }
+
+  setHeight(value: number) {
+    this.h = value;
+    return this;
+  }
+
+  setId(value: string) {
+    this.id = value;
+    return this;
+  }
+
+  setName(value: string) {
+    this.name = value;
+    return this;
+  }
+
+  setCornerRadius(value: number) {
+    this.cornerRadius = value;
+    return this;
+  }
+
+  setTransform(value: TransformMatrix) {
+    this.transform = value;
+    return this;
+  }
+
+  setZOrderPath(value: number[]) {
+    this.zOrderPath = value;
+    return this;
+  }
+
+  setGroupId(value: number) {
+    this.groupId = value;
+    return this;
+  }
+
+  setIsVisible(value: boolean) {
+    this.isVisible = value;
+    return this;
+  }
+
+  setIsDisplay(value: boolean) {
+    this.isDisplay = value;
+    return this;
+  }
+
+  setIsVirtual(value: boolean) {
+    this.isVirtual = value;
+    return this;
+  }
+
+  build(): TraceRect {
+    if (this.x === undefined) {
+      throw Error('x not set');
+    }
+
+    if (this.y === undefined) {
+      throw Error('y not set');
+    }
+
+    if (this.w === undefined) {
+      throw Error('width not set');
+    }
+
+    if (this.h === undefined) {
+      throw Error('height not set');
+    }
+
+    if (this.id === undefined) {
+      throw Error('id not set');
+    }
+
+    if (this.name === undefined) {
+      throw Error('name not set');
+    }
+
+    if (this.cornerRadius === undefined) {
+      throw Error('cornerRadius not set');
+    }
+
+    if (this.transform === undefined) {
+      throw Error('transform not set');
+    }
+
+    if (this.zOrderPath === undefined) {
+      throw Error('zOrderPath not set');
+    }
+
+    if (this.groupId === undefined) {
+      throw Error('groupId not set');
+    }
+
+    if (this.isVisible === undefined) {
+      throw Error('isVisible not set');
+    }
+
+    if (this.isDisplay === undefined) {
+      throw Error('isDisplay not set');
+    }
+
+    if (this.isVirtual === undefined) {
+      throw Error('isVirtual not set');
+    }
+
+    return new TraceRect(
+      this.x,
+      this.y,
+      this.w,
+      this.h,
+      this.id,
+      this.name,
+      this.cornerRadius,
+      this.transform,
+      this.zOrderPath,
+      this.groupId,
+      this.isVisible,
+      this.isDisplay,
+      this.isVirtual
+    );
+  }
+}
diff --git a/tools/winscope/src/trace/trace_type.ts b/tools/winscope/src/trace/trace_type.ts
index 2a857a5..7b2bf85 100644
--- a/tools/winscope/src/trace/trace_type.ts
+++ b/tools/winscope/src/trace/trace_type.ts
@@ -13,10 +13,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 import {Cuj, Event, Transition} from 'flickerlib/common';
 import {LayerTraceEntry} from 'flickerlib/layers/LayerTraceEntry';
 import {WindowManagerState} from 'flickerlib/windows/WindowManagerState';
-import {HierarchyTreeNode, TraceRect, TreeNode} from 'trace/trace_data_utils';
 import {LogMessage} from './protolog';
 import {ScreenRecordingTraceEntry} from './screen_recording';
 
@@ -52,46 +52,32 @@
 export type ViewNode = any;
 export type FrameData = any;
 
-export interface TreeAndRects {
-  tree: HierarchyTreeNode;
-  rects: TraceRect[];
-}
-
 export interface TraceEntryTypeMap {
-  [TraceType.PROTO_LOG]: {new: LogMessage; legacy: LogMessage};
-  [TraceType.SURFACE_FLINGER]: {new: TreeAndRects; legacy: LayerTraceEntry};
-  [TraceType.SCREEN_RECORDING]: {new: ScreenRecordingTraceEntry; legacy: ScreenRecordingTraceEntry};
-  [TraceType.SYSTEM_UI]: {new: object; legacy: object};
-  [TraceType.TRANSACTIONS]: {new: TreeNode<any>; legacy: object};
-  [TraceType.TRANSACTIONS_LEGACY]: {new: TreeNode<any>; legacy: object};
-  [TraceType.WAYLAND]: {new: object; legacy: object};
-  [TraceType.WAYLAND_DUMP]: {new: object; legacy: object};
-  [TraceType.WINDOW_MANAGER]: {new: TreeAndRects; legacy: WindowManagerState};
-  [TraceType.INPUT_METHOD_CLIENTS]: {new: TreeNode<any>; legacy: object};
-  [TraceType.INPUT_METHOD_MANAGER_SERVICE]: {new: TreeNode<any>; legacy: object};
-  [TraceType.INPUT_METHOD_SERVICE]: {new: TreeNode<any>; legacy: object};
-  [TraceType.EVENT_LOG]: {new: TreeNode<any>; legacy: Event};
-  [TraceType.WM_TRANSITION]: {new: TreeNode<any>; legacy: object};
-  [TraceType.SHELL_TRANSITION]: {new: TreeNode<any>; legacy: object};
-  [TraceType.TRANSITION]: {new: TreeNode<any>; legacy: Transition};
-  [TraceType.CUJS]: {new: TreeNode<any>; legacy: Cuj};
-  [TraceType.TAG]: {new: object; legacy: object};
-  [TraceType.ERROR]: {new: object; legacy: object};
-  [TraceType.TEST_TRACE_STRING]: {new: string; legacy: string};
-  [TraceType.TEST_TRACE_NUMBER]: {new: number; legacy: number};
-  [TraceType.VIEW_CAPTURE]: {new: TreeNode<any>; legacy: object};
-  [TraceType.VIEW_CAPTURE_LAUNCHER_ACTIVITY]: {
-    new: TreeAndRects;
-    legacy: FrameData;
-  };
-  [TraceType.VIEW_CAPTURE_TASKBAR_DRAG_LAYER]: {
-    new: TreeAndRects;
-    legacy: FrameData;
-  };
-  [TraceType.VIEW_CAPTURE_TASKBAR_OVERLAY_DRAG_LAYER]: {
-    new: TreeAndRects;
-    legacy: FrameData;
-  };
+  [TraceType.PROTO_LOG]: LogMessage;
+  [TraceType.SURFACE_FLINGER]: LayerTraceEntry;
+  [TraceType.SCREEN_RECORDING]: ScreenRecordingTraceEntry;
+  [TraceType.SYSTEM_UI]: object;
+  [TraceType.TRANSACTIONS]: object;
+  [TraceType.TRANSACTIONS_LEGACY]: object;
+  [TraceType.WAYLAND]: object;
+  [TraceType.WAYLAND_DUMP]: object;
+  [TraceType.WINDOW_MANAGER]: WindowManagerState;
+  [TraceType.INPUT_METHOD_CLIENTS]: object;
+  [TraceType.INPUT_METHOD_MANAGER_SERVICE]: object;
+  [TraceType.INPUT_METHOD_SERVICE]: object;
+  [TraceType.EVENT_LOG]: Event;
+  [TraceType.WM_TRANSITION]: object;
+  [TraceType.SHELL_TRANSITION]: object;
+  [TraceType.TRANSITION]: Transition;
+  [TraceType.CUJS]: Cuj;
+  [TraceType.TAG]: object;
+  [TraceType.ERROR]: object;
+  [TraceType.TEST_TRACE_STRING]: string;
+  [TraceType.TEST_TRACE_NUMBER]: number;
+  [TraceType.VIEW_CAPTURE]: object;
+  [TraceType.VIEW_CAPTURE_LAUNCHER_ACTIVITY]: FrameData;
+  [TraceType.VIEW_CAPTURE_TASKBAR_DRAG_LAYER]: FrameData;
+  [TraceType.VIEW_CAPTURE_TASKBAR_OVERLAY_DRAG_LAYER]: FrameData;
 }
 
 export class TraceTypeUtils {
diff --git a/tools/winscope/src/trace/traces.ts b/tools/winscope/src/trace/traces.ts
index 20f2cdd..6d4a8b0 100644
--- a/tools/winscope/src/trace/traces.ts
+++ b/tools/winscope/src/trace/traces.ts
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-import {Timestamp} from '../common/time';
+import {Timestamp} from 'common/time';
 import {AbsoluteFrameIndex} from './index_types';
 import {Trace} from './trace';
 import {TraceEntryTypeMap, TraceType} from './trace_type';
@@ -22,12 +22,12 @@
 export class Traces {
   private traces = new Map<TraceType, Trace<{}>>();
 
-  setTrace<T extends TraceType>(type: T, trace: Trace<TraceEntryTypeMap[T]['legacy']>) {
+  setTrace<T extends TraceType>(type: T, trace: Trace<TraceEntryTypeMap[T]>) {
     this.traces.set(type, trace);
   }
 
-  getTrace<T extends TraceType>(type: T): Trace<TraceEntryTypeMap[T]['legacy']> | undefined {
-    return this.traces.get(type) as Trace<TraceEntryTypeMap[T]['legacy']> | undefined;
+  getTrace<T extends TraceType>(type: T): Trace<TraceEntryTypeMap[T]> | undefined {
+    return this.traces.get(type) as Trace<TraceEntryTypeMap[T]> | undefined;
   }
 
   deleteTrace<T extends TraceType>(type: T) {
diff --git a/tools/winscope/src/trace/tree_node/formatters.ts b/tools/winscope/src/trace/tree_node/formatters.ts
new file mode 100644
index 0000000..190e0e4
--- /dev/null
+++ b/tools/winscope/src/trace/tree_node/formatters.ts
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {RawDataUtils} from 'parsers/raw_data_utils';
+import {TransformUtils} from 'parsers/surface_flinger/transform_utils';
+import {PropertyTreeNode} from './property_tree_node';
+
+const EMPTY_OBJ_STRING = '{empty}';
+const EMPTY_ARRAY_STRING = '[empty]';
+
+interface PropertyFormatter {
+  format(node: PropertyTreeNode): string;
+}
+
+class DefaultPropertyFormatter implements PropertyFormatter {
+  format(node: PropertyTreeNode): string {
+    const value = node.getValue();
+    if (Array.isArray(value) && value.length === 0) {
+      return EMPTY_ARRAY_STRING;
+    }
+
+    if (value?.toString) return value.toString();
+
+    return `${value}`;
+  }
+}
+const DEFAULT_PROPERTY_FORMATTER = new DefaultPropertyFormatter();
+
+class ColorFormatter implements PropertyFormatter {
+  format(node: PropertyTreeNode): string {
+    if (RawDataUtils.isEmptyObj(node)) {
+      return `${EMPTY_OBJ_STRING}, alpha: ${node.getChildByName('a')?.getValue() ?? 'unknown'}`;
+    }
+    return `(${node.getChildByName('r')?.getValue() ?? 0}, ${
+      node.getChildByName('g')?.getValue() ?? 0
+    }, ${node.getChildByName('b')?.getValue() ?? 0}, ${node.getChildByName('a')?.getValue() ?? 0})`;
+  }
+}
+const COLOR_FORMATTER = new ColorFormatter();
+
+class RectFormatter implements PropertyFormatter {
+  format(node: PropertyTreeNode): string {
+    if (RawDataUtils.isEmptyObj(node)) {
+      return EMPTY_OBJ_STRING;
+    }
+    return `(${node.getChildByName('left')?.getValue() ?? 0}, ${
+      node.getChildByName('top')?.getValue() ?? 0
+    }) - (${node.getChildByName('right')?.getValue() ?? 0}, ${
+      node.getChildByName('bottom')?.getValue() ?? 0
+    })`;
+  }
+}
+const RECT_FORMATTER = new RectFormatter();
+
+class BufferFormatter implements PropertyFormatter {
+  format(node: PropertyTreeNode): string {
+    return `w: ${node.getChildByName('width')?.getValue() ?? 0}, h: ${
+      node.getChildByName('height')?.getValue() ?? 0
+    }, stride: ${node.getChildByName('stride')?.getValue()}, format: ${node
+      .getChildByName('format')
+      ?.getValue()}`;
+  }
+}
+const BUFFER_FORMATTER = new BufferFormatter();
+
+class LayerIdFormatter implements PropertyFormatter {
+  format(node: PropertyTreeNode): string {
+    const value = node.getValue();
+    return value === -1 || value === 0 ? 'none' : `${value}`;
+  }
+}
+const LAYER_ID_FORMATTER = new LayerIdFormatter();
+
+class TransformFormatter implements PropertyFormatter {
+  format(node: PropertyTreeNode): string {
+    const type = node.getChildByName('type');
+    return type !== undefined ? TransformUtils.getTypeFlags(type.getValue() ?? 0) : 'null';
+  }
+}
+const TRANSFORM_FORMATTER = new TransformFormatter();
+
+class SizeFormatter implements PropertyFormatter {
+  format(node: PropertyTreeNode): string {
+    return `${node.getChildByName('w')?.getValue() ?? 0} x ${
+      node.getChildByName('h')?.getValue() ?? 0
+    }`;
+  }
+}
+const SIZE_FORMATTER = new SizeFormatter();
+
+class PositionFormatter implements PropertyFormatter {
+  format(node: PropertyTreeNode): string {
+    return `x: ${node.getChildByName('x')?.getValue() ?? 0}, y: ${
+      node.getChildByName('y')?.getValue() ?? 0
+    }`;
+  }
+}
+const POSITION_FORMATTER = new PositionFormatter();
+
+class RegionFormatter implements PropertyFormatter {
+  format(node: PropertyTreeNode): string {
+    let res = 'SkRegion(';
+    node
+      .getChildByName('rect')
+      ?.getAllChildren()
+      .forEach((rectNode: PropertyTreeNode) => {
+        res += `(${rectNode.getChildByName('left')?.getValue() ?? 0}, ${
+          rectNode.getChildByName('top')?.getValue() ?? 0
+        }, ${rectNode.getChildByName('right')?.getValue() ?? 0}, ${
+          rectNode.getChildByName('bottom')?.getValue() ?? 0
+        })`;
+      });
+    return res + ')';
+  }
+}
+const REGION_FORMATTER = new RegionFormatter();
+
+class EnumFormatter implements PropertyFormatter {
+  constructor(private readonly valuesById: {[key: number]: string}) {}
+
+  format(node: PropertyTreeNode): string {
+    const value = node.getValue();
+    if (typeof value === 'number' && this.valuesById[value]) {
+      return this.valuesById[value];
+    }
+    return `${value}`;
+  }
+}
+
+class FixedStringFormatter implements PropertyFormatter {
+  constructor(private readonly fixedStringValue: string) {}
+
+  format(node: PropertyTreeNode): string {
+    return this.fixedStringValue;
+  }
+}
+
+export {
+  EMPTY_OBJ_STRING,
+  EMPTY_ARRAY_STRING,
+  PropertyFormatter,
+  DEFAULT_PROPERTY_FORMATTER,
+  COLOR_FORMATTER,
+  RECT_FORMATTER,
+  BUFFER_FORMATTER,
+  LAYER_ID_FORMATTER,
+  TRANSFORM_FORMATTER,
+  SIZE_FORMATTER,
+  POSITION_FORMATTER,
+  REGION_FORMATTER,
+  EnumFormatter,
+  FixedStringFormatter,
+};
diff --git a/tools/winscope/src/trace/tree_node/formatters_test.ts b/tools/winscope/src/trace/tree_node/formatters_test.ts
new file mode 100644
index 0000000..d6b9d78
--- /dev/null
+++ b/tools/winscope/src/trace/tree_node/formatters_test.ts
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {TransformType} from 'parsers/surface_flinger/transform_utils';
+import {TreeNodeUtils} from 'test/unit/tree_node_utils';
+import {
+  BUFFER_FORMATTER,
+  COLOR_FORMATTER,
+  DEFAULT_PROPERTY_FORMATTER,
+  EMPTY_ARRAY_STRING,
+  EMPTY_OBJ_STRING,
+  LAYER_ID_FORMATTER,
+  POSITION_FORMATTER,
+  RECT_FORMATTER,
+  REGION_FORMATTER,
+  SIZE_FORMATTER,
+  TRANSFORM_FORMATTER,
+} from './formatters';
+import {PropertySource, PropertyTreeNode} from './property_tree_node';
+
+describe('Formatters', () => {
+  describe('PropertyFormatter', () => {
+    it('translates simple values correctly', () => {
+      expect(
+        DEFAULT_PROPERTY_FORMATTER.format(new PropertyTreeNode('', '', PropertySource.PROTO, 12345))
+      ).toEqual('12345');
+      expect(
+        DEFAULT_PROPERTY_FORMATTER.format(
+          new PropertyTreeNode('', '', PropertySource.PROTO, 'test_string')
+        )
+      ).toEqual('test_string');
+      expect(
+        DEFAULT_PROPERTY_FORMATTER.format(new PropertyTreeNode('', '', PropertySource.PROTO, 0.4))
+      ).toEqual('0.4');
+    });
+
+    it('translates values with toString method correctly', () => {
+      expect(
+        DEFAULT_PROPERTY_FORMATTER.format(
+          new PropertyTreeNode('', '', PropertySource.PROTO, BigInt(123))
+        )
+      ).toEqual('123');
+    });
+
+    it('translates default values correctly', () => {
+      expect(
+        DEFAULT_PROPERTY_FORMATTER.format(new PropertyTreeNode('', '', PropertySource.PROTO, []))
+      ).toEqual(EMPTY_ARRAY_STRING);
+      expect(
+        DEFAULT_PROPERTY_FORMATTER.format(new PropertyTreeNode('', '', PropertySource.PROTO, false))
+      ).toEqual('false');
+      expect(
+        DEFAULT_PROPERTY_FORMATTER.format(new PropertyTreeNode('', '', PropertySource.PROTO, null))
+      ).toEqual('null');
+    });
+  });
+
+  describe('ColorFormatter', () => {
+    it('translates empty color to string correctly', () => {
+      expect(COLOR_FORMATTER.format(TreeNodeUtils.makeColorNode(-1, -1, -1, 1))).toEqual(
+        `${EMPTY_OBJ_STRING}, alpha: 1`
+      );
+      expect(COLOR_FORMATTER.format(TreeNodeUtils.makeColorNode(1, 1, 1, 0))).toEqual(
+        `${EMPTY_OBJ_STRING}, alpha: 0`
+      );
+    });
+
+    it('translates non-empty color to string correctly', () => {
+      expect(COLOR_FORMATTER.format(TreeNodeUtils.makeColorNode(1, 2, 3, 1))).toEqual(
+        '(1, 2, 3, 1)'
+      );
+      expect(COLOR_FORMATTER.format(TreeNodeUtils.makeColorNode(1, 2, 3, 0.5))).toEqual(
+        '(1, 2, 3, 0.5)'
+      );
+    });
+  });
+
+  describe('RectFormatter', () => {
+    it('translates empty rect to string correctly', () => {
+      expect(RECT_FORMATTER.format(TreeNodeUtils.makeRectNode(0, 0, -1, -1))).toEqual(
+        EMPTY_OBJ_STRING
+      );
+      expect(RECT_FORMATTER.format(TreeNodeUtils.makeRectNode(0, 0, 0, 0))).toEqual(
+        EMPTY_OBJ_STRING
+      );
+    });
+
+    it('translates non-empty rect to string correctly', () => {
+      expect(RECT_FORMATTER.format(TreeNodeUtils.makeRectNode(0, 0, 1, 1))).toEqual(
+        '(0, 0) - (1, 1)'
+      );
+      expect(RECT_FORMATTER.format(TreeNodeUtils.makeRectNode(0, 0, 10, 10))).toEqual(
+        '(0, 0) - (10, 10)'
+      );
+    });
+  });
+
+  describe('BufferFormatter', () => {
+    it('translates buffer to string correctly', () => {
+      const buffer = TreeNodeUtils.makeBufferNode();
+      expect(BUFFER_FORMATTER.format(buffer)).toEqual('w: 1, h: 0, stride: 0, format: 1');
+    });
+  });
+
+  describe('LayerIdFormatter', () => {
+    it('translates -1 id correctly', () => {
+      expect(
+        LAYER_ID_FORMATTER.format(new PropertyTreeNode('', '', PropertySource.PROTO, -1))
+      ).toEqual('none');
+    });
+
+    it('translates valid id correctly', () => {
+      expect(
+        LAYER_ID_FORMATTER.format(new PropertyTreeNode('', '', PropertySource.PROTO, 1))
+      ).toEqual('1');
+      expect(
+        LAYER_ID_FORMATTER.format(new PropertyTreeNode('', '', PropertySource.PROTO, -10))
+      ).toEqual('-10');
+    });
+  });
+
+  describe('TransformFormatter', () => {
+    it('translates type correctly', () => {
+      expect(
+        TRANSFORM_FORMATTER.format(TreeNodeUtils.makeTransformNode(TransformType.EMPTY))
+      ).toEqual('IDENTITY');
+      expect(
+        TRANSFORM_FORMATTER.format(TreeNodeUtils.makeTransformNode(TransformType.TRANSLATE_VAL))
+      ).toEqual('TRANSLATE');
+      expect(
+        TRANSFORM_FORMATTER.format(TreeNodeUtils.makeTransformNode(TransformType.SCALE_VAL))
+      ).toEqual('SCALE');
+      expect(
+        TRANSFORM_FORMATTER.format(TreeNodeUtils.makeTransformNode(TransformType.FLIP_H_VAL))
+      ).toEqual('IDENTITY|FLIP_H');
+      expect(
+        TRANSFORM_FORMATTER.format(TreeNodeUtils.makeTransformNode(TransformType.FLIP_V_VAL))
+      ).toEqual('IDENTITY|FLIP_V');
+      expect(
+        TRANSFORM_FORMATTER.format(TreeNodeUtils.makeTransformNode(TransformType.ROT_90_VAL))
+      ).toEqual('IDENTITY|ROT_90');
+      expect(
+        TRANSFORM_FORMATTER.format(TreeNodeUtils.makeTransformNode(TransformType.ROT_INVALID_VAL))
+      ).toEqual('IDENTITY|ROT_INVALID');
+    });
+  });
+
+  describe('SizeFormatter', () => {
+    it('translates size correctly', () => {
+      expect(SIZE_FORMATTER.format(TreeNodeUtils.makeSizeNode(1, 2))).toEqual('1 x 2');
+    });
+  });
+
+  describe('PositionFormatter', () => {
+    it('translates position correctly', () => {
+      expect(POSITION_FORMATTER.format(TreeNodeUtils.makePositionNode(1, 2))).toEqual('x: 1, y: 2');
+    });
+  });
+
+  describe('RegionFormatter', () => {
+    it('translates region correctly', () => {
+      const region = new PropertyTreeNode('region', 'region', PropertySource.PROTO, undefined);
+      const rect = new PropertyTreeNode('region.rect', 'rect', PropertySource.PROTO, []);
+      rect.addChild(TreeNodeUtils.makeRectNode(0, 0, 1080, 2340));
+      region.addChild(rect);
+      expect(REGION_FORMATTER.format(region)).toEqual('SkRegion((0, 0, 1080, 2340))');
+    });
+  });
+});
diff --git a/tools/winscope/src/trace/tree_node/hierarchy_tree_node.ts b/tools/winscope/src/trace/tree_node/hierarchy_tree_node.ts
new file mode 100644
index 0000000..4acf708
--- /dev/null
+++ b/tools/winscope/src/trace/tree_node/hierarchy_tree_node.ts
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {TraceRect} from 'trace/trace_rect';
+import {PropertiesProvider} from 'trace/tree_node/properties_provider';
+import {PropertyTreeNode} from './property_tree_node';
+import {TreeNode} from './tree_node';
+
+export class HierarchyTreeNode extends TreeNode {
+  private rects: TraceRect[] | undefined;
+  private zParent: this | undefined;
+
+  constructor(id: string, name: string, protected readonly propertiesProvider: PropertiesProvider) {
+    super(id, name);
+  }
+
+  async getAllProperties(): Promise<PropertyTreeNode> {
+    return await this.propertiesProvider.getAll();
+  }
+
+  getEagerPropertyByName(name: string): PropertyTreeNode | undefined {
+    return this.propertiesProvider.getEagerProperties().getChildById(`${this.id}.${name}`);
+  }
+
+  addEagerProperty(property: PropertyTreeNode): void {
+    this.propertiesProvider.addEagerProperty(property);
+  }
+
+  setRects(value: TraceRect[]) {
+    this.rects = value;
+  }
+
+  getRects(): TraceRect[] | undefined {
+    return this.rects;
+  }
+
+  setZParent(parent: this): void {
+    this.zParent = parent;
+  }
+
+  getZParent(): this | undefined {
+    return this.zParent;
+  }
+
+  override isRoot(): boolean {
+    return !this.zParent;
+  }
+
+  findAncestor(targetNodeFilter: (node: this) => boolean): this | undefined {
+    let ancestor = this.getZParent();
+
+    while (ancestor && !targetNodeFilter(ancestor)) {
+      ancestor = ancestor.getZParent();
+    }
+
+    return ancestor;
+  }
+}
diff --git a/tools/winscope/src/trace/tree_node/operations/operation.ts b/tools/winscope/src/trace/tree_node/operations/operation.ts
new file mode 100644
index 0000000..8d50873
--- /dev/null
+++ b/tools/winscope/src/trace/tree_node/operations/operation.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {TreeNode} from 'trace/tree_node/tree_node';
+
+export interface Operation<T extends TreeNode> {
+  apply(value: T): T;
+}
diff --git a/tools/winscope/src/trace/tree_node/operations/operation_chain.ts b/tools/winscope/src/trace/tree_node/operations/operation_chain.ts
new file mode 100644
index 0000000..b6d5852
--- /dev/null
+++ b/tools/winscope/src/trace/tree_node/operations/operation_chain.ts
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {TreeNode} from 'trace/tree_node/tree_node';
+import {Operation} from './operation';
+
+export class OperationChain<T extends TreeNode> {
+  constructor(private readonly enclosedOperations: Array<Operation<T>>) {}
+
+  apply(value: T): T {
+    this.enclosedOperations.forEach((operation: Operation<T>) => {
+      value = operation.apply(value);
+    });
+
+    return value;
+  }
+
+  push(enclosedOperation: Operation<T>) {
+    this.enclosedOperations.push(enclosedOperation);
+  }
+
+  static emptyChain<T extends TreeNode>(): OperationChain<T> {
+    return new OperationChain([]);
+  }
+}
diff --git a/tools/winscope/src/trace/tree_node/properties_provider.ts b/tools/winscope/src/trace/tree_node/properties_provider.ts
new file mode 100644
index 0000000..d83e64c
--- /dev/null
+++ b/tools/winscope/src/trace/tree_node/properties_provider.ts
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {OperationChain} from 'trace/tree_node/operations/operation_chain';
+import {PropertySource, PropertyTreeNode} from 'trace/tree_node/property_tree_node';
+
+export class PropertiesProvider {
+  private eagerPropertiesRoot: PropertyTreeNode;
+  private lazyPropertiesRoot: PropertyTreeNode | undefined;
+  private allPropertiesRoot: PropertyTreeNode | undefined;
+
+  constructor(
+    eagerPropertiesRoot: PropertyTreeNode,
+    private readonly lazyPropertiesStrategy: () => Promise<PropertyTreeNode>,
+    private readonly commonOperations: OperationChain<PropertyTreeNode>,
+    private readonly eagerOperations: OperationChain<PropertyTreeNode>,
+    private readonly lazyOperations: OperationChain<PropertyTreeNode>
+  ) {
+    this.eagerPropertiesRoot = this.commonOperations.apply(
+      this.eagerOperations.apply(eagerPropertiesRoot)
+    );
+  }
+
+  getEagerProperties(): PropertyTreeNode {
+    return this.eagerPropertiesRoot;
+  }
+
+  addEagerProperty(property: PropertyTreeNode) {
+    this.eagerPropertiesRoot.addChild(
+      this.commonOperations.apply(this.eagerOperations.apply(property))
+    );
+  }
+
+  async getAll(): Promise<PropertyTreeNode> {
+    if (this.allPropertiesRoot) return this.allPropertiesRoot;
+
+    const root = new PropertyTreeNode(
+      this.eagerPropertiesRoot.id,
+      this.eagerPropertiesRoot.name,
+      PropertySource.PROTO,
+      undefined
+    );
+    const children = [...this.eagerPropertiesRoot.getAllChildren()];
+
+    // all eager properties have already had operations applied so no need to reapply
+    if (!this.lazyPropertiesRoot) {
+      this.lazyPropertiesRoot = this.commonOperations.apply(
+        this.lazyOperations.apply(await this.lazyPropertiesStrategy())
+      );
+    }
+
+    children.push(...this.lazyPropertiesRoot.getAllChildren());
+    children.sort(this.sortChildren).forEach((child) => root.addChild(child));
+
+    root.setIsRoot(true);
+
+    this.allPropertiesRoot = root;
+    return this.allPropertiesRoot;
+  }
+
+  private sortChildren(a: PropertyTreeNode, b: PropertyTreeNode): number {
+    return a.name < b.name ? -1 : 1;
+  }
+}
diff --git a/tools/winscope/src/trace/tree_node/property_tree_node.ts b/tools/winscope/src/trace/tree_node/property_tree_node.ts
new file mode 100644
index 0000000..faee8f4
--- /dev/null
+++ b/tools/winscope/src/trace/tree_node/property_tree_node.ts
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {PropertyFormatter} from './formatters';
+import {TreeNode} from './tree_node';
+
+export class PropertyTreeNode extends TreeNode {
+  protected formatter: PropertyFormatter | undefined = undefined;
+  protected internalIsRoot = false;
+
+  constructor(
+    id: string,
+    name: string,
+    readonly source: PropertySource,
+    protected readonly value: any
+  ) {
+    super(id, name);
+  }
+
+  getChildByName(name: string): this | undefined {
+    return this.children.get(`${this.id}.${name}`);
+  }
+
+  getValue(): any {
+    return this.value;
+  }
+
+  setFormatter(formatter: PropertyFormatter): this {
+    this.formatter = formatter;
+    return this;
+  }
+
+  setIsRoot(value: boolean) {
+    this.internalIsRoot = value;
+  }
+
+  override isRoot(): boolean {
+    return this.internalIsRoot;
+  }
+
+  formattedValue(): string {
+    if (this.formatter) {
+      return this.formatter.format(this);
+    }
+
+    return '';
+  }
+}
+
+export enum PropertySource {
+  PROTO,
+  DEFAULT,
+  CALCULATED,
+}
diff --git a/tools/winscope/src/trace/tree_node/tree_node.ts b/tools/winscope/src/trace/tree_node/tree_node.ts
new file mode 100644
index 0000000..459a010
--- /dev/null
+++ b/tools/winscope/src/trace/tree_node/tree_node.ts
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Item} from 'trace/item';
+
+export abstract class TreeNode implements Item {
+  protected children = new Map<string, this>();
+
+  constructor(public id: string, public name: string) {}
+
+  addChild(child: this): void {
+    this.children.set(child.id, child);
+  }
+
+  removeChild(childId: string) {
+    this.children.delete(childId);
+  }
+
+  removeAllChildren() {
+    this.children.clear();
+  }
+
+  getChildById(id: string): this | undefined {
+    return this.children.get(id);
+  }
+
+  getAllChildren(): this[] {
+    return [...this.children.values()];
+  }
+
+  forEachNodeDfs(callback: (node: this) => void) {
+    callback(this);
+    this.children.forEach((child) => {
+      child.forEachNodeDfs(callback);
+    });
+  }
+
+  findDfs(targetNodeFilter: (node: this) => boolean): this | undefined {
+    if (targetNodeFilter(this)) {
+      return this;
+    }
+
+    for (const child of this.children.values()) {
+      const nodeFound = child.findDfs(targetNodeFilter);
+      if (nodeFound) return nodeFound;
+    }
+
+    return undefined;
+  }
+
+  abstract isRoot(): boolean;
+}
diff --git a/tools/winscope/src/viewers/common/surface_flinger_utils.ts b/tools/winscope/src/viewers/common/surface_flinger_utils.ts
index 1bca894..0b7ee1d 100644
--- a/tools/winscope/src/viewers/common/surface_flinger_utils.ts
+++ b/tools/winscope/src/viewers/common/surface_flinger_utils.ts
@@ -14,9 +14,10 @@
  * limitations under the License.
  */
 
-import {TransformMatrix} from 'common/geometry_utils';
+import {TransformMatrix} from 'common/geometry_types';
 import {Layer, LayerTraceEntry} from 'flickerlib/common';
 import {UiRect} from 'viewers/components/rects/types2d';
+import {UiRectBuilder} from 'viewers/components/rects/ui_rect_builder';
 import {UserOptions} from './user_options';
 
 export class SurfaceFlingerUtils {
@@ -46,25 +47,24 @@
       .sort(SurfaceFlingerUtils.compareLayerZ)
       .map((it: Layer) => {
         const transform: TransformMatrix = it.rect.transform?.matrix ?? it.rect.transform;
-        const rect: UiRect = {
-          x: it.rect.left,
-          y: it.rect.top,
-          w: it.rect.right - it.rect.left,
-          h: it.rect.bottom - it.rect.top,
-          label: it.rect.label,
-          transform,
-          isVisible: it.isVisible,
-          isDisplay: false,
-          id: it.stableId,
-          displayId: it.stackId,
-          isVirtual: false,
-          isClickable: true,
-          cornerRadius: it.cornerRadius,
-          hasContent: viewCapturePackageNames.includes(
-            it.rect.label.substring(0, it.rect.label.indexOf('/'))
-          ),
-        };
-        return rect;
+        return new UiRectBuilder()
+          .setX(it.rect.left)
+          .setY(it.rect.top)
+          .setWidth(it.rect.right - it.rect.left)
+          .setHeight(it.rect.bottom - it.rect.top)
+          .setLabel(it.rect.label)
+          .setTransform(transform)
+          .setIsVisible(it.isVisible)
+          .setIsDisplay(false)
+          .setId(it.stableId)
+          .setDisplayId(it.stackId)
+          .setIsVirtual(false)
+          .setIsClickable(true)
+          .setCornerRadius(it.cornerRadius)
+          .setHasContent(
+            viewCapturePackageNames.includes(it.rect.label.substring(0, it.rect.label.indexOf('/')))
+          )
+          .build();
       });
   }
 
@@ -75,23 +75,22 @@
 
     return entry.displays?.map((display: any) => {
       const transform: TransformMatrix = display.transform?.matrix ?? display.transform;
-      const rect: UiRect = {
-        x: 0,
-        y: 0,
-        w: display.size.width,
-        h: display.size.height,
-        label: 'Display',
-        transform,
-        isVisible: false,
-        isDisplay: true,
-        id: `Display - ${display.id}`,
-        displayId: display.layerStackId,
-        isVirtual: display.isVirtual ?? false,
-        isClickable: false,
-        cornerRadius: 0,
-        hasContent: false,
-      };
-      return rect;
+      return new UiRectBuilder()
+        .setX(0)
+        .setY(0)
+        .setWidth(display.size.width)
+        .setHeight(display.size.height)
+        .setLabel('Display')
+        .setTransform(transform)
+        .setIsVisible(false)
+        .setIsDisplay(true)
+        .setId(`Display - ${display.id}`)
+        .setDisplayId(display.layerStackId)
+        .setIsVirtual(display.isVirtual ?? false)
+        .setIsClickable(false)
+        .setCornerRadius(0)
+        .setHasContent(false)
+        .build();
     });
   }
 
diff --git a/tools/winscope/src/viewers/components/rects/canvas.ts b/tools/winscope/src/viewers/components/rects/canvas.ts
index e0436f8..0c89665 100644
--- a/tools/winscope/src/viewers/components/rects/canvas.ts
+++ b/tools/winscope/src/viewers/components/rects/canvas.ts
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {TransformMatrix} from 'common/geometry_utils';
+import {TransformMatrix} from 'common/geometry_types';
 import * as THREE from 'three';
 import {CSS2DObject, CSS2DRenderer} from 'three/examples/jsm/renderers/CSS2DRenderer';
 import {ViewerEvents} from 'viewers/common/viewer_events';
diff --git a/tools/winscope/src/viewers/components/rects/mapper3d.ts b/tools/winscope/src/viewers/components/rects/mapper3d.ts
index 0643669..bbad7ab 100644
--- a/tools/winscope/src/viewers/components/rects/mapper3d.ts
+++ b/tools/winscope/src/viewers/components/rects/mapper3d.ts
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-import {IDENTITY_MATRIX, TransformMatrix} from 'common/geometry_utils';
+import {IDENTITY_MATRIX, TransformMatrix} from 'common/geometry_types';
 import {Size, UiRect} from 'viewers/components/rects/types2d';
 import {Box3D, ColorType, Distance2D, Label3D, Point3D, Rect3D, Scene3D} from './types3d';
 
diff --git a/tools/winscope/src/viewers/components/rects/rects_component_test.ts b/tools/winscope/src/viewers/components/rects/rects_component_test.ts
index 55bd335..1a3f736 100644
--- a/tools/winscope/src/viewers/components/rects/rects_component_test.ts
+++ b/tools/winscope/src/viewers/components/rects/rects_component_test.ts
@@ -25,6 +25,7 @@
 import {RectsComponent} from 'viewers/components/rects/rects_component';
 import {UiRect} from 'viewers/components/rects/types2d';
 import {Canvas} from './canvas';
+import {UiRectBuilder} from './ui_rect_builder';
 
 describe('RectsComponent', () => {
   let component: TestHostComponent;
@@ -68,28 +69,28 @@
   it('draws scene when input data changes', async () => {
     spyOn(Canvas.prototype, 'draw').and.callThrough();
 
-    const inputRect: UiRect = {
-      x: 0,
-      y: 0,
-      w: 1,
-      h: 1,
-      label: 'rectangle1',
-      transform: {
+    const inputRect = new UiRectBuilder()
+      .setX(0)
+      .setY(0)
+      .setWidth(1)
+      .setHeight(1)
+      .setLabel('rectangle1')
+      .setTransform({
         dsdx: 1,
         dsdy: 0,
         dtdx: 0,
         dtdy: 1,
         tx: 0,
         ty: 0,
-      },
-      isVisible: true,
-      isDisplay: false,
-      id: 'test-id-1234',
-      displayId: 0,
-      isVirtual: false,
-      isClickable: false,
-      cornerRadius: 0,
-    };
+      })
+      .setIsVisible(true)
+      .setIsDisplay(false)
+      .setId('test-id-1234')
+      .setDisplayId(0)
+      .setIsVirtual(false)
+      .setIsClickable(false)
+      .setCornerRadius(0)
+      .build();
 
     expect(Canvas.prototype.draw).toHaveBeenCalledTimes(0);
     component.rectsComponent.rects = [inputRect];
diff --git a/tools/winscope/src/viewers/components/rects/types2d.ts b/tools/winscope/src/viewers/components/rects/types2d.ts
index fe34955..a150a36 100644
--- a/tools/winscope/src/viewers/components/rects/types2d.ts
+++ b/tools/winscope/src/viewers/components/rects/types2d.ts
@@ -14,20 +14,29 @@
  * limitations under the License.
  */
 
-import {Rect, TransformMatrix} from 'common/geometry_utils';
+import {TransformMatrix} from 'common/geometry_types';
+import {Rect} from 'common/rect';
 
-export interface UiRect extends Rect {
-  label: string;
-  transform?: TransformMatrix;
-  isVisible: boolean;
-  isDisplay: boolean;
-  id: string;
-  displayId: number;
-  isVirtual: boolean;
-  isClickable: boolean;
-  cornerRadius: number;
-  depth?: number;
-  hasContent?: boolean;
+export class UiRect extends Rect {
+  constructor(
+    x: number,
+    y: number,
+    w: number,
+    h: number,
+    readonly label: string,
+    readonly isVisible: boolean,
+    readonly isDisplay: boolean,
+    readonly id: string,
+    readonly displayId: number,
+    readonly isVirtual: boolean,
+    readonly isClickable: boolean,
+    readonly cornerRadius: number,
+    readonly transform: TransformMatrix | undefined,
+    readonly depth: number | undefined,
+    readonly hasContent: boolean | undefined
+  ) {
+    super(x, y, w, h);
+  }
 }
 
 export interface Size {
diff --git a/tools/winscope/src/viewers/components/rects/types3d.ts b/tools/winscope/src/viewers/components/rects/types3d.ts
index fa06e00..1c4f541 100644
--- a/tools/winscope/src/viewers/components/rects/types3d.ts
+++ b/tools/winscope/src/viewers/components/rects/types3d.ts
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-import {TransformMatrix} from 'common/geometry_utils';
+import {TransformMatrix} from 'common/geometry_types';
 
 export enum ColorType {
   VISIBLE,
diff --git a/tools/winscope/src/viewers/components/rects/ui_rect_builder.ts b/tools/winscope/src/viewers/components/rects/ui_rect_builder.ts
new file mode 100644
index 0000000..263294f
--- /dev/null
+++ b/tools/winscope/src/viewers/components/rects/ui_rect_builder.ts
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {TransformMatrix} from 'common/geometry_types';
+import {UiRect} from './types2d';
+
+export class UiRectBuilder {
+  x: number | undefined;
+  y: number | undefined;
+  w: number | undefined;
+  h: number | undefined;
+  label: string | undefined;
+  transform: TransformMatrix | undefined;
+  isVisible: boolean | undefined;
+  isDisplay: boolean | undefined;
+  id: string | undefined;
+  displayId: number | undefined;
+  isVirtual: boolean | undefined;
+  isClickable: boolean | undefined;
+  cornerRadius: number | undefined;
+  depth: number | undefined;
+  hasContent: boolean | undefined;
+
+  setX(value: number) {
+    this.x = value;
+    return this;
+  }
+
+  setY(value: number) {
+    this.y = value;
+    return this;
+  }
+
+  setWidth(value: number) {
+    this.w = value;
+    return this;
+  }
+
+  setHeight(value: number) {
+    this.h = value;
+    return this;
+  }
+
+  setLabel(value: string) {
+    this.label = value;
+    return this;
+  }
+
+  setTransform(value: TransformMatrix) {
+    this.transform = value;
+    return this;
+  }
+
+  setIsVisible(value: boolean) {
+    this.isVisible = value;
+    return this;
+  }
+
+  setIsDisplay(value: boolean) {
+    this.isDisplay = value;
+    return this;
+  }
+
+  setId(value: string) {
+    this.id = value;
+    return this;
+  }
+
+  setDisplayId(value: number) {
+    this.displayId = value;
+    return this;
+  }
+
+  setIsVirtual(value: boolean) {
+    this.isVirtual = value;
+    return this;
+  }
+
+  setIsClickable(value: boolean) {
+    this.isClickable = value;
+    return this;
+  }
+
+  setCornerRadius(value: number) {
+    this.cornerRadius = value;
+    return this;
+  }
+
+  setDepth(value: number) {
+    this.depth = value;
+    return this;
+  }
+
+  setHasContent(value: boolean) {
+    this.hasContent = value;
+    return this;
+  }
+
+  build(): UiRect {
+    if (this.x === undefined) {
+      throw Error('x not set');
+    }
+
+    if (this.y === undefined) {
+      throw Error('y not set');
+    }
+
+    if (this.w === undefined) {
+      throw Error('width not set');
+    }
+
+    if (this.h === undefined) {
+      throw Error('height not set');
+    }
+
+    if (this.label === undefined) {
+      throw Error('label not set');
+    }
+
+    if (this.isVisible === undefined) {
+      throw Error('isVisible not set');
+    }
+
+    if (this.isDisplay === undefined) {
+      throw Error('isDisplay not set');
+    }
+
+    if (this.id === undefined) {
+      throw Error('id not set');
+    }
+
+    if (this.displayId === undefined) {
+      throw Error('displayId not set');
+    }
+
+    if (this.isVirtual === undefined) {
+      throw Error('isVirtual not set');
+    }
+
+    if (this.isClickable === undefined) {
+      throw Error('isClickable not set');
+    }
+
+    if (this.cornerRadius === undefined) {
+      throw Error('cornerRadius not set');
+    }
+
+    return new UiRect(
+      this.x,
+      this.y,
+      this.w,
+      this.h,
+      this.label,
+      this.isVisible,
+      this.isDisplay,
+      this.id,
+      this.displayId,
+      this.isVirtual,
+      this.isClickable,
+      this.cornerRadius,
+      this.transform,
+      this.depth,
+      this.hasContent
+    );
+  }
+}
diff --git a/tools/winscope/src/viewers/viewer_view_capture/presenter.ts b/tools/winscope/src/viewers/viewer_view_capture/presenter.ts
index 97e446b..34141b5 100644
--- a/tools/winscope/src/viewers/viewer_view_capture/presenter.ts
+++ b/tools/winscope/src/viewers/viewer_view_capture/presenter.ts
@@ -29,6 +29,7 @@
 import {UserOptions} from 'viewers/common/user_options';
 import {ViewCaptureUtils} from 'viewers/common/view_capture_utils';
 import {UiRect} from 'viewers/components/rects/types2d';
+import {UiRectBuilder} from 'viewers/components/rects/ui_rect_builder';
 import {UiData} from './ui_data';
 
 export class Presenter {
@@ -171,23 +172,23 @@
     const rectangles: UiRect[] = [];
 
     function inner(node: any /* ViewNode */) {
-      const aUiRect: UiRect = {
-        x: node.boxPos.left,
-        y: node.boxPos.top,
-        w: node.boxPos.width,
-        h: node.boxPos.height,
-        label: '',
-        transform: undefined,
-        isVisible: node.isVisible,
-        isDisplay: false,
-        id: node.id,
-        displayId: 0,
-        isVirtual: false,
-        isClickable: true,
-        cornerRadius: 0,
-        depth: node.depth,
-        hasContent: node.isVisible,
-      };
+      const aUiRect = new UiRectBuilder()
+        .setX(node.boxPos.left)
+        .setY(node.boxPos.top)
+        .setWidth(node.boxPos.width)
+        .setHeight(node.boxPos.height)
+        .setLabel('')
+        .setIsVisible(node.isVisible)
+        .setIsDisplay(false)
+        .setId(node.id)
+        .setDisplayId(0)
+        .setIsVirtual(false)
+        .setIsClickable(true)
+        .setCornerRadius(0)
+        .setHasContent(node.isVisible)
+        .setDepth(node.depth)
+        .build();
+
       rectangles.push(aUiRect);
       node.children.forEach((it: any) /* ViewNode */ => inner(it));
     }
diff --git a/tools/winscope/src/viewers/viewer_window_manager/presenter.ts b/tools/winscope/src/viewers/viewer_window_manager/presenter.ts
index f5875a8..e3ee90f 100644
--- a/tools/winscope/src/viewers/viewer_window_manager/presenter.ts
+++ b/tools/winscope/src/viewers/viewer_window_manager/presenter.ts
@@ -15,7 +15,7 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {TransformMatrix} from 'common/geometry_utils';
+import {TransformMatrix} from 'common/geometry_types';
 import {PersistentStoreProxy} from 'common/persistent_store_proxy';
 import {FilterType, TreeUtils} from 'common/tree_utils';
 import {DisplayContent} from 'flickerlib/windows/DisplayContent';
@@ -31,6 +31,7 @@
 import {HierarchyTreeNode, PropertiesTreeNode} from 'viewers/common/ui_tree_utils';
 import {UserOptions} from 'viewers/common/user_options';
 import {UiRect} from 'viewers/components/rects/types2d';
+import {UiRectBuilder} from 'viewers/components/rects/ui_rect_builder';
 import {UiData} from './ui_data';
 
 type NotifyViewCallbackType = (uiData: UiData) => void;
@@ -206,21 +207,20 @@
     };
     const displayRects: UiRect[] =
       entry.displays?.map((display: DisplayContent) => {
-        const rect: UiRect = {
-          x: display.displayRect.left,
-          y: display.displayRect.top,
-          w: display.displayRect.right - display.displayRect.left,
-          h: display.displayRect.bottom - display.displayRect.top,
-          label: `Display - ${display.title}`,
-          transform: identityMatrix,
-          isVisible: false, //TODO: check if displayRect.ref.isVisible exists
-          isDisplay: true,
-          id: display.stableId,
-          displayId: display.id,
-          isVirtual: false,
-          isClickable: false,
-          cornerRadius: 0,
-        };
+        const rect = new UiRectBuilder()
+          .setX(display.displayRect.left)
+          .setY(display.displayRect.top)
+          .setWidth(display.displayRect.right - display.displayRect.left)
+          .setHeight(display.displayRect.bottom - display.displayRect.top)
+          .setLabel(`Display - ${display.title}`)
+          .setIsVisible(false)
+          .setIsDisplay(true)
+          .setId(display.stableId)
+          .setDisplayId(display.id)
+          .setIsVirtual(false)
+          .setIsClickable(false)
+          .setCornerRadius(0)
+          .build();
         return rect;
       }) ?? [];
 
@@ -228,21 +228,20 @@
       entry.windowStates
         ?.sort((a: any, b: any) => b.computedZ - a.computedZ)
         .map((it: any) => {
-          const rect: UiRect = {
-            x: it.rect.left,
-            y: it.rect.top,
-            w: it.rect.right - it.rect.left,
-            h: it.rect.bottom - it.rect.top,
-            label: it.rect.label,
-            transform: identityMatrix,
-            isVisible: it.isVisible,
-            isDisplay: false,
-            id: it.stableId,
-            displayId: it.displayId,
-            isVirtual: false, //TODO: is this correct?
-            isClickable: true,
-            cornerRadius: 0,
-          };
+          const rect = new UiRectBuilder()
+            .setX(it.rect.left)
+            .setY(it.rect.top)
+            .setWidth(it.rect.right - it.rect.left)
+            .setHeight(it.rect.bottom - it.rect.top)
+            .setLabel(it.rect.label)
+            .setIsVisible(it.isVisible)
+            .setIsDisplay(false)
+            .setId(it.stableId)
+            .setDisplayId(it.displayId)
+            .setIsVirtual(false)
+            .setIsClickable(true)
+            .setCornerRadius(0)
+            .build();
           return rect;
         }) ?? [];