@canvus/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/layout.js ADDED
@@ -0,0 +1,278 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // canvus/src/layout.ts
3
+ // Layout Introspection Engine — Detects CSS layout modes and
4
+ // extracts structural metadata from live Shadow DOM elements.
5
+ //
6
+ // All functions read from `getComputedStyle()` and do not
7
+ // mutate the DOM. Results are used by the overlay renderer to
8
+ // draw layout-aware visual badges, direction arrows, and grid
9
+ // track lines.
10
+ // ─────────────────────────────────────────────────────────────
11
+ // ── Detection Functions ─────────────────────────────────────
12
+ /**
13
+ * Detects the CSS layout mode and properties of an element.
14
+ *
15
+ * Reads `getComputedStyle()` to extract the resolved display
16
+ * mode, flex direction, gap, alignment, and grid template values.
17
+ *
18
+ * @param element - The DOM element to inspect.
19
+ * @returns Complete `LayoutInfo` descriptor.
20
+ */
21
+ export function detectLayout(element) {
22
+ const cs = getComputedStyle(element);
23
+ const display = cs.display;
24
+ // Resolve display to our LayoutMode enum.
25
+ const mode = resolveLayoutMode(display);
26
+ // Flex properties.
27
+ const isFlex = mode === "flex" || mode === "inline-flex";
28
+ const direction = isFlex
29
+ ? cs.flexDirection
30
+ : null;
31
+ const wrap = isFlex
32
+ ? cs.flexWrap
33
+ : null;
34
+ // Gap (works for flex and grid).
35
+ const rowGap = parseFloat(cs.rowGap) || 0;
36
+ const columnGap = parseFloat(cs.columnGap) || 0;
37
+ // Alignment.
38
+ const alignItems = cs.alignItems;
39
+ const justifyContent = cs.justifyContent;
40
+ // Grid properties.
41
+ const isGrid = mode === "grid" || mode === "inline-grid";
42
+ const gridTemplateColumns = isGrid ? cs.gridTemplateColumns : null;
43
+ const gridTemplateRows = isGrid ? cs.gridTemplateRows : null;
44
+ const gridAutoFlow = isGrid ? cs.gridAutoFlow : null;
45
+ return {
46
+ mode,
47
+ direction,
48
+ wrap,
49
+ gap: { row: rowGap, column: columnGap },
50
+ alignItems,
51
+ justifyContent,
52
+ gridTemplateColumns,
53
+ gridTemplateRows,
54
+ gridAutoFlow,
55
+ };
56
+ }
57
+ /**
58
+ * Returns the primary flow axis for a layout container.
59
+ *
60
+ * - Flex row → "x" (horizontal)
61
+ * - Flex column → "y" (vertical)
62
+ * - Grid row (default auto-flow) → "x"
63
+ * - Block → "y" (vertical stacking)
64
+ *
65
+ * @param info - The layout info to inspect.
66
+ * @returns "x" for horizontal flow, "y" for vertical flow.
67
+ */
68
+ export function getFlowAxis(info) {
69
+ if (info.mode === "flex" || info.mode === "inline-flex") {
70
+ return info.direction === "column" || info.direction === "column-reverse"
71
+ ? "y"
72
+ : "x";
73
+ }
74
+ if (info.mode === "grid" || info.mode === "inline-grid") {
75
+ return info.gridAutoFlow?.includes("column") ? "x" : "y";
76
+ }
77
+ // Block, inline: vertical stacking.
78
+ return "y";
79
+ }
80
+ /**
81
+ * Returns the flow direction sign for a layout container.
82
+ *
83
+ * `1` for normal direction, `-1` for reverse.
84
+ * Useful for computing insertion positions.
85
+ */
86
+ export function getFlowSign(info) {
87
+ if (info.direction === "row-reverse" || info.direction === "column-reverse") {
88
+ return -1;
89
+ }
90
+ return 1;
91
+ }
92
+ /**
93
+ * Detects the position and size of each child element's slot
94
+ * within a container, measured relative to the container's
95
+ * padding box.
96
+ *
97
+ * This uses `getBoundingClientRect()` for accuracy, then
98
+ * converts to container-relative coordinates.
99
+ *
100
+ * @param container - The parent DOM element.
101
+ * @returns Array of `ChildSlot` objects, one per child element.
102
+ */
103
+ export function detectChildSlots(container) {
104
+ const slots = [];
105
+ const containerRect = container.getBoundingClientRect();
106
+ const cs = getComputedStyle(container);
107
+ const padLeft = parseFloat(cs.paddingLeft) || 0;
108
+ const padTop = parseFloat(cs.paddingTop) || 0;
109
+ const children = container.children;
110
+ for (let i = 0; i < children.length; i++) {
111
+ const child = children[i];
112
+ const childRect = child.getBoundingClientRect();
113
+ slots.push({
114
+ index: i,
115
+ rect: {
116
+ x: childRect.left - containerRect.left - padLeft,
117
+ y: childRect.top - containerRect.top - padTop,
118
+ width: childRect.width,
119
+ height: childRect.height,
120
+ },
121
+ });
122
+ }
123
+ return slots;
124
+ }
125
+ /**
126
+ * Parses grid track values from a resolved `grid-template-columns`
127
+ * or `grid-template-rows` string into an array of `GridTrack` objects.
128
+ *
129
+ * The browser resolves templates like `1fr 2fr 100px` into
130
+ * computed pixel values like `200px 400px 100px`. This function
131
+ * parses those resolved values, accounting for the layout gap between tracks.
132
+ *
133
+ * @param templateValue - The resolved CSS grid template string
134
+ * (e.g. "200px 400px 100px").
135
+ * @param gap - The layout gap in pixels between tracks.
136
+ * @returns Array of grid tracks with start offsets and sizes.
137
+ */
138
+ export function parseGridTracks(templateValue, gap = 0) {
139
+ const tracks = [];
140
+ // The resolved value is space-separated pixel values like "200px 400px 100px".
141
+ // Filter out named grid lines (e.g. "[start]") which appear in brackets.
142
+ const parts = templateValue
143
+ .replace(/\[.*?\]/g, "") // Remove grid line names.
144
+ .trim()
145
+ .split(/\s+/)
146
+ .filter(p => p.length > 0);
147
+ let offset = 0;
148
+ for (let i = 0; i < parts.length; i++) {
149
+ const part = parts[i];
150
+ const size = parseFloat(part) || 0;
151
+ if (i > 0) {
152
+ offset += gap;
153
+ }
154
+ tracks.push({ start: offset, size });
155
+ offset += size;
156
+ }
157
+ return tracks;
158
+ }
159
+ /**
160
+ * Maps a padding-box relative coordinate (x, y) to the grid cell indices (1-indexed).
161
+ *
162
+ * @param x - X coordinate relative to container padding edge.
163
+ * @param y - Y coordinate relative to container padding edge.
164
+ * @param columns - Parsed column tracks.
165
+ * @param rows - Parsed row tracks.
166
+ * @param colGap - Column gap.
167
+ * @param rowGap - Row gap.
168
+ * @returns { col: number, row: number } (1-indexed, matching CSS grid-column-start/grid-row-start).
169
+ */
170
+ export function getGridCellAt(x, y, columns, rows, colGap, rowGap) {
171
+ // Find column index (1-indexed)
172
+ let col = 1;
173
+ for (let i = 0; i < columns.length; i++) {
174
+ const c = columns[i];
175
+ const colEnd = c.start + c.size + colGap / 2;
176
+ if (x <= colEnd) {
177
+ col = i + 1;
178
+ break;
179
+ }
180
+ col = i + 1;
181
+ }
182
+ // Find row index (1-indexed)
183
+ let row = 1;
184
+ for (let i = 0; i < rows.length; i++) {
185
+ const r = rows[i];
186
+ const rowEnd = r.start + r.size + rowGap / 2;
187
+ if (y <= rowEnd) {
188
+ row = i + 1;
189
+ break;
190
+ }
191
+ row = i + 1;
192
+ }
193
+ return { col, row };
194
+ }
195
+ /**
196
+ * Computes the canvas-space bounding rect of a grid area span.
197
+ *
198
+ * @param containerRect - The parent container's canvas-space bounding box.
199
+ * @param padLeft - Container padding-left.
200
+ * @param padTop - Container padding-top.
201
+ * @param colStart - 1-based column start index.
202
+ * @param rowStart - 1-based row start index.
203
+ * @param colSpan - Column span.
204
+ * @param rowSpan - Row span.
205
+ * @param columns - Parsed column tracks.
206
+ * @param rows - Parsed row tracks.
207
+ * @returns The bounding Rect in canvas-space.
208
+ */
209
+ export function getGridAreaRect(containerRect, padLeft, padTop, colStart, rowStart, colSpan, rowSpan, columns, rows) {
210
+ const colIdx = Math.max(1, Math.min(colStart, columns.length)) - 1;
211
+ const rowIdx = Math.max(1, Math.min(rowStart, rows.length)) - 1;
212
+ const startCol = columns[colIdx] ?? { start: 0, size: 0 };
213
+ const startRow = rows[rowIdx] ?? { start: 0, size: 0 };
214
+ const endColIdx = Math.max(1, Math.min(colStart + colSpan - 1, columns.length)) - 1;
215
+ const endRowIdx = Math.max(1, Math.min(rowStart + rowSpan - 1, rows.length)) - 1;
216
+ const endCol = columns[endColIdx] ?? startCol;
217
+ const endRow = rows[endRowIdx] ?? startRow;
218
+ const x = containerRect.x + padLeft + startCol.start;
219
+ const y = containerRect.y + padTop + startRow.start;
220
+ const width = (endCol.start + endCol.size) - startCol.start;
221
+ const height = (endRow.start + endRow.size) - startRow.start;
222
+ return { x, y, width, height };
223
+ }
224
+ /**
225
+ * Returns a short, human-readable label for a layout mode.
226
+ * Used for layout badge overlays.
227
+ *
228
+ * @param info - The layout info to label.
229
+ * @returns A concise string like "FLEX →", "FLEX ↓", "GRID", "BLOCK".
230
+ */
231
+ export function getLayoutLabel(info) {
232
+ switch (info.mode) {
233
+ case "flex":
234
+ case "inline-flex": {
235
+ const arrow = info.direction === "column"
236
+ ? "↓"
237
+ : info.direction === "column-reverse"
238
+ ? "↑"
239
+ : info.direction === "row-reverse"
240
+ ? "←"
241
+ : "→";
242
+ return `FLEX ${arrow}`;
243
+ }
244
+ case "grid":
245
+ case "inline-grid":
246
+ return "GRID";
247
+ case "inline":
248
+ return "INLINE";
249
+ case "block":
250
+ return "BLOCK";
251
+ default:
252
+ return "NONE";
253
+ }
254
+ }
255
+ // ── Private Helpers ─────────────────────────────────────────
256
+ /**
257
+ * Maps a CSS `display` value to our `LayoutMode` enum.
258
+ * Handles compound values like "inline-flex", "inline-grid".
259
+ */
260
+ function resolveLayoutMode(display) {
261
+ // Normalize: CSS can return compound values like "inline flex" or "block grid".
262
+ const d = display.toLowerCase().trim();
263
+ if (d.includes("inline-flex") || d.includes("inline flex"))
264
+ return "inline-flex";
265
+ if (d.includes("inline-grid") || d.includes("inline grid"))
266
+ return "inline-grid";
267
+ if (d.includes("flex"))
268
+ return "flex";
269
+ if (d.includes("grid"))
270
+ return "grid";
271
+ if (d === "inline" || d === "inline-block" || d.includes("inline"))
272
+ return "inline";
273
+ if (d === "none")
274
+ return "none";
275
+ // Everything else (block, table, list-item, etc.) → block.
276
+ return "block";
277
+ }
278
+ //# sourceMappingURL=layout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout.js","sourceRoot":"","sources":["../src/layout.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,168 @@
1
+ import type { Rect, Vec2, ViewportMatrix } from "./types.js";
2
+ /**
3
+ * Projects a raw screen-space pointer coordinate (e.g. `MouseEvent.clientX/Y`)
4
+ * into the infinite canvas coordinate space, accounting for:
5
+ * 1. The canvas element's own position within the page (`canvasRect`).
6
+ * 2. The current pan offset (`viewport.offsetX/Y`).
7
+ * 3. The current zoom scale (`viewport.scale`).
8
+ *
9
+ * Derivation:
10
+ * screenX = canvasX * scale + offsetX + canvasRect.x
11
+ * ⟹ canvasX = (clientX - canvasRect.x - offsetX) / scale
12
+ *
13
+ * @param clientX - `MouseEvent.clientX` (viewport-relative screen pixel).
14
+ * @param clientY - `MouseEvent.clientY` (viewport-relative screen pixel).
15
+ * @param viewport - Current affine viewport transform state.
16
+ * @param canvasRect - Bounding rect of the `<canvas>` element on screen.
17
+ * @returns The corresponding point in canvas (world) space.
18
+ */
19
+ export declare function screenToCanvas(clientX: number, clientY: number, viewport: Readonly<ViewportMatrix>, canvasRect: Readonly<Rect>): Vec2;
20
+ /**
21
+ * Projects a canvas-space coordinate back to absolute screen pixels,
22
+ * the algebraic inverse of `screenToCanvas`.
23
+ *
24
+ * Derivation:
25
+ * screenX = canvasX * scale + offsetX + canvasRect.x
26
+ *
27
+ * @param canvasX - X position in canvas (world) space.
28
+ * @param canvasY - Y position in canvas (world) space.
29
+ * @param viewport - Current affine viewport transform state.
30
+ * @param canvasRect - Bounding rect of the `<canvas>` element on screen.
31
+ * @returns The corresponding point in screen (client) space.
32
+ */
33
+ export declare function canvasToScreen(canvasX: number, canvasY: number, viewport: Readonly<ViewportMatrix>, canvasRect: Readonly<Rect>): Vec2;
34
+ /**
35
+ * Computes a new `ViewportMatrix` after applying a zoom delta
36
+ * anchored precisely at the cursor's current screen position.
37
+ *
38
+ * The key invariant: the canvas-space point directly beneath the
39
+ * cursor must map to the *exact same screen pixel* before and
40
+ * after the zoom. This prevents the jarring "zoom drift" that
41
+ * occurs with naïve center-point scaling.
42
+ *
43
+ * ### Mathematical derivation
44
+ *
45
+ * Let `(cx, cy)` be the cursor in screen-space relative to the
46
+ * canvas element origin:
47
+ * cx = clientX - canvasRect.x
48
+ * cy = clientY - canvasRect.y
49
+ *
50
+ * Before zoom, the canvas-space point under the cursor is:
51
+ * worldX = (cx - offsetX) / oldScale
52
+ * worldY = (cy - offsetY) / oldScale
53
+ *
54
+ * After zoom with `newScale`, we require the same world point to
55
+ * project back to `(cx, cy)`:
56
+ * cx = worldX * newScale + newOffsetX
57
+ * cy = worldY * newScale + newOffsetY
58
+ *
59
+ * Solving for the new offsets:
60
+ * newOffsetX = cx - worldX * newScale
61
+ * = cx - ((cx - offsetX) / oldScale) * newScale
62
+ * = cx * (1 - newScale / oldScale) + offsetX * (newScale / oldScale)
63
+ *
64
+ * Equivalently (and more numerically stable):
65
+ * newOffsetX = cx - (cx - offsetX) * (newScale / oldScale)
66
+ *
67
+ * @param clientX - `MouseEvent.clientX` at the zoom gesture origin.
68
+ * @param clientY - `MouseEvent.clientY` at the zoom gesture origin.
69
+ * @param scaleDelta - Multiplicative scale factor to apply (e.g. 1.1 for zoom-in).
70
+ * @param viewport - The current viewport transform state before zooming.
71
+ * @param canvasRect - Bounding rect of the `<canvas>` element on screen.
72
+ * @returns A new `ViewportMatrix` with the anchored zoom applied.
73
+ */
74
+ export declare function calculateZoomAnchor(clientX: number, clientY: number, scaleDelta: number, viewport: Readonly<ViewportMatrix>, canvasRect: Readonly<Rect>): ViewportMatrix;
75
+ /**
76
+ * Convenience wrapper that converts a `WheelEvent.deltaY` value
77
+ * into a multiplicative scale factor and delegates to
78
+ * `calculateZoomAnchor`.
79
+ *
80
+ * Scroll-up (negative deltaY) zooms in, scroll-down zooms out.
81
+ * The sensitivity constant (0.001) yields a smooth, non-jarring
82
+ * zoom curve across trackpad and discrete-notch scroll wheels.
83
+ *
84
+ * @param clientX - `MouseEvent.clientX` at the wheel event.
85
+ * @param clientY - `MouseEvent.clientY` at the wheel event.
86
+ * @param deltaY - `WheelEvent.deltaY` (positive = scroll down = zoom out).
87
+ * @param viewport - Current viewport state.
88
+ * @param canvasRect - Canvas element bounding rect.
89
+ * @returns A new `ViewportMatrix` with the wheel-zoom applied.
90
+ */
91
+ export declare function applyWheelZoom(clientX: number, clientY: number, deltaY: number, viewport: Readonly<ViewportMatrix>, canvasRect: Readonly<Rect>, isPinch?: boolean): ViewportMatrix;
92
+ /**
93
+ * Applies a screen-space pan delta to the viewport offset.
94
+ * Typically driven by pointer-move events while spacebar is held
95
+ * or middle-mouse is pressed.
96
+ *
97
+ * @param dx - Horizontal screen-pixel delta (`e.movementX`).
98
+ * @param dy - Vertical screen-pixel delta (`e.movementY`).
99
+ * @param viewport - Current viewport state.
100
+ * @returns A new `ViewportMatrix` with the pan offset applied.
101
+ */
102
+ export declare function applyPan(dx: number, dy: number, viewport: Readonly<ViewportMatrix>): ViewportMatrix;
103
+ /**
104
+ * Axis-Aligned Bounding Box (AABB) point-inclusion test.
105
+ *
106
+ * Returns `true` if the point `(x, y)` lies within (or exactly on
107
+ * the boundary of) the rectangle described by `bounds`.
108
+ *
109
+ * Both coordinates and bounds should be in the same space
110
+ * (typically canvas-space after `screenToCanvas` conversion).
111
+ *
112
+ * @param x - Point X coordinate.
113
+ * @param y - Point Y coordinate.
114
+ * @param bounds - The axis-aligned bounding rectangle to test against.
115
+ * @returns Whether the point is inside or on the edge of `bounds`.
116
+ */
117
+ export declare function isPointInElement(x: number, y: number, bounds: Readonly<Rect>): boolean;
118
+ /**
119
+ * Determines which `WebHTMLNode` (if any) is hit by a canvas-space
120
+ * point, respecting z-order (last in the array = topmost).
121
+ *
122
+ * @param x - Canvas-space X coordinate.
123
+ * @param y - Canvas-space Y coordinate.
124
+ * @param elements - Array of elements with `id` and bounding `Rect`.
125
+ * @returns The `id` of the topmost hit element, or `null` if none.
126
+ */
127
+ export declare function hitTestElements(x: number, y: number, elements: ReadonlyArray<Readonly<{
128
+ id: string;
129
+ currentRect: Rect | null;
130
+ }>>): string | null;
131
+ /**
132
+ * Computes the screen-space positions of all 8 resize anchor
133
+ * handles for a given element bounding box.
134
+ *
135
+ * Returns an object keyed by anchor direction with the
136
+ * corresponding screen-space `Vec2` position of each handle center.
137
+ *
138
+ * @param bounds - The element's bounding rect in canvas-space.
139
+ * @param viewport - Current viewport transform.
140
+ * @param canvasRect - Canvas element bounding rect on screen.
141
+ * @returns Record mapping each anchor to its screen-space center point.
142
+ */
143
+ export declare function getAnchorPositions(bounds: Readonly<Rect>, viewport: Readonly<ViewportMatrix>, canvasRect: Readonly<Rect>): Record<"nw" | "n" | "ne" | "e" | "se" | "s" | "sw" | "w", Vec2>;
144
+ /** Clamps a scale value to the allowed zoom range. */
145
+ export declare function clampScale(s: number): number;
146
+ /**
147
+ * Linearly interpolates between two values.
148
+ * Useful for animated viewport transitions.
149
+ *
150
+ * @param a - Start value.
151
+ * @param b - End value.
152
+ * @param t - Interpolation factor in [0, 1].
153
+ */
154
+ export declare function lerp(a: number, b: number, t: number): number;
155
+ /**
156
+ * Linearly interpolates between two `ViewportMatrix` states.
157
+ * Useful for smooth animated zoom/pan transitions.
158
+ *
159
+ * @param from - Starting viewport.
160
+ * @param to - Target viewport.
161
+ * @param t - Interpolation factor in [0, 1].
162
+ */
163
+ export declare function lerpViewport(from: Readonly<ViewportMatrix>, to: Readonly<ViewportMatrix>, t: number): ViewportMatrix;
164
+ /**
165
+ * Checks if two axis-aligned bounding rectangles intersect.
166
+ */
167
+ export declare function rectsIntersect(r1: Readonly<Rect>, r2: Readonly<Rect>): boolean;
168
+ //# sourceMappingURL=matrix.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matrix.d.ts","sourceRoot":"","sources":["../src/matrix.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAK7D;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,QAAQ,CAAC,cAAc,CAAC,EAClC,UAAU,EAAE,QAAQ,CAAC,IAAI,CAAC,GACzB,IAAI,CAKN;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,QAAQ,CAAC,cAAc,CAAC,EAClC,UAAU,EAAE,QAAQ,CAAC,IAAI,CAAC,GACzB,IAAI,CAKN;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,QAAQ,CAAC,cAAc,CAAC,EAClC,UAAU,EAAE,QAAQ,CAAC,IAAI,CAAC,GACzB,cAAc,CAmBhB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,QAAQ,CAAC,cAAc,CAAC,EAClC,UAAU,EAAE,QAAQ,CAAC,IAAI,CAAC,EAC1B,OAAO,GAAE,OAAe,GACvB,cAAc,CAWhB;AAID;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CACtB,EAAE,EAAE,MAAM,EACV,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,QAAQ,CAAC,cAAc,CAAC,GACjC,cAAc,CAMhB;AAID;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAC9B,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,GACrB,OAAO,CAOT;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC7B,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,QAAQ,EAAE,aAAa,CAAC,QAAQ,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,IAAI,GAAG,IAAI,CAAA;CAAE,CAAC,CAAC,GAC1E,MAAM,GAAG,IAAI,CASf;AAID;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,EACtB,QAAQ,EAAE,QAAQ,CAAC,cAAc,CAAC,EAClC,UAAU,EAAE,QAAQ,CAAC,IAAI,CAAC,GACzB,MAAM,CAAC,IAAI,GAAG,GAAG,GAAG,IAAI,GAAG,GAAG,GAAG,IAAI,GAAG,GAAG,GAAG,IAAI,GAAG,GAAG,EAAE,IAAI,CAAC,CAsBjE;AAID,sDAAsD;AACtD,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE5C;AAED;;;;;;;GAOG;AACH,wBAAgB,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,QAAQ,CAAC,cAAc,CAAC,EAC9B,EAAE,EAAE,QAAQ,CAAC,cAAc,CAAC,EAC5B,CAAC,EAAE,MAAM,GACR,cAAc,CAMhB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,OAAO,CAO9E"}