This is page 6 of 8. Use http://codebase.md/1yhy/figma-context-mcp?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .editorconfig
├── .env.example
├── .github
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.yml
│ │ └── feature_request.yml
│ ├── PULL_REQUEST_TEMPLATE.md
│ └── workflows
│ └── ci.yml
├── .gitignore
├── .husky
│ ├── commit-msg
│ └── pre-commit
├── .lintstagedrc.json
├── .nvmrc
├── .prettierrc
├── CHANGELOG.md
├── commitlint.config.js
├── CONTRIBUTING.md
├── Dockerfile
├── docs
│ ├── en
│ │ ├── absolute-to-relative-research.md
│ │ ├── architecture.md
│ │ ├── cache-architecture.md
│ │ ├── grid-layout-research.md
│ │ ├── icon-detection.md
│ │ ├── layout-detection-research.md
│ │ └── layout-detection.md
│ └── zh-CN
│ ├── absolute-to-relative-research.md
│ ├── architecture.md
│ ├── cache-architecture.md
│ ├── grid-layout-research.md
│ ├── icon-detection.md
│ ├── layout-detection-research.md
│ ├── layout-detection.md
│ └── TODO-feature-enhancements.md
├── eslint.config.js
├── LICENSE
├── package.json
├── pnpm-lock.yaml
├── README.md
├── README.zh-CN.md
├── scripts
│ ├── fetch-test-data.ts
│ └── optimize-figma-json.ts
├── smithery.yaml
├── src
│ ├── algorithms
│ │ ├── icon
│ │ │ ├── detector.ts
│ │ │ └── index.ts
│ │ └── layout
│ │ ├── detector.ts
│ │ ├── index.ts
│ │ ├── optimizer.ts
│ │ └── spatial.ts
│ ├── config.ts
│ ├── core
│ │ ├── effects.ts
│ │ ├── layout.ts
│ │ ├── parser.ts
│ │ └── style.ts
│ ├── index.ts
│ ├── prompts
│ │ ├── design-to-code.ts
│ │ └── index.ts
│ ├── resources
│ │ ├── figma-resources.ts
│ │ └── index.ts
│ ├── server.ts
│ ├── services
│ │ ├── cache
│ │ │ ├── cache-manager.ts
│ │ │ ├── disk-cache.ts
│ │ │ ├── index.ts
│ │ │ ├── lru-cache.ts
│ │ │ └── types.ts
│ │ ├── cache.ts
│ │ ├── figma.ts
│ │ └── simplify-node-response.ts
│ ├── types
│ │ ├── figma.ts
│ │ ├── index.ts
│ │ └── simplified.ts
│ └── utils
│ ├── color.ts
│ ├── css.ts
│ ├── file.ts
│ └── validation.ts
├── tests
│ ├── fixtures
│ │ ├── expected
│ │ │ ├── node-240-32163-optimized.json
│ │ │ ├── node-402-34955-optimized.json
│ │ │ └── real-node-data-optimized.json
│ │ └── figma-data
│ │ ├── node-240-32163.json
│ │ ├── node-402-34955.json
│ │ └── real-node-data.json
│ ├── integration
│ │ ├── __snapshots__
│ │ │ ├── layout-optimization.test.ts.snap
│ │ │ └── output-quality.test.ts.snap
│ │ ├── layout-optimization.test.ts
│ │ ├── output-quality.test.ts
│ │ └── parser.test.ts
│ ├── unit
│ │ ├── algorithms
│ │ │ ├── icon-optimization.test.ts
│ │ │ ├── icon.test.ts
│ │ │ └── layout.test.ts
│ │ ├── resources
│ │ │ └── figma-resources.test.ts
│ │ └── services
│ │ └── cache.test.ts
│ └── utils
│ ├── preview-generator.ts
│ ├── preview.ts
│ ├── run-simplification.ts
│ └── viewer.html
├── tsconfig.json
├── tsup.config.ts
└── vitest.config.ts
```
# Files
--------------------------------------------------------------------------------
/tests/unit/algorithms/layout.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Layout Detection Algorithm Unit Tests
3 | *
4 | * Tests the layout detection algorithm for inferring Flexbox layouts
5 | * from absolutely positioned Figma elements.
6 | */
7 |
8 | import { describe, it, expect, beforeAll } from "vitest";
9 | import * as fs from "fs";
10 | import * as path from "path";
11 | import { fileURLToPath } from "url";
12 | import {
13 | extractBoundingBox,
14 | toElementRect,
15 | groupIntoRows,
16 | groupIntoColumns,
17 | analyzeGaps,
18 | analyzeAlignment,
19 | calculateBounds,
20 | clusterValues,
21 | detectGridLayout,
22 | calculateIoU,
23 | classifyOverlap,
24 | detectOverlappingElements,
25 | detectBackgroundElement,
26 | LayoutOptimizer,
27 | analyzeHomogeneity,
28 | filterHomogeneousForGrid,
29 | type ElementRect,
30 | type BoundingBox,
31 | } from "~/algorithms/layout/index.js";
32 | import type { SimplifiedNode } from "~/types/index.js";
33 |
34 | const __dirname = path.dirname(fileURLToPath(import.meta.url));
35 | const fixturesPath = path.join(__dirname, "../../fixtures");
36 |
37 | // Test data types
38 | interface FigmaNode {
39 | id: string;
40 | name: string;
41 | type: string;
42 | absoluteBoundingBox?: BoundingBox;
43 | children?: FigmaNode[];
44 | }
45 |
46 | // Load test fixture
47 | function loadTestData(): FigmaNode {
48 | const dataPath = path.join(fixturesPath, "figma-data/real-node-data.json");
49 | const rawData = JSON.parse(fs.readFileSync(dataPath, "utf-8"));
50 | const nodeKey = Object.keys(rawData.nodes)[0];
51 | return rawData.nodes[nodeKey].document;
52 | }
53 |
54 | // Extract child elements with bounding boxes
55 | function extractChildElements(node: FigmaNode): ElementRect[] {
56 | if (!node.children) return [];
57 |
58 | return node.children
59 | .filter((child) => child.absoluteBoundingBox)
60 | .map((child, index) => {
61 | const box = child.absoluteBoundingBox!;
62 | return toElementRect(box, index);
63 | });
64 | }
65 |
66 | describe("Layout Detection Algorithm", () => {
67 | let testData: FigmaNode;
68 |
69 | beforeAll(() => {
70 | testData = loadTestData();
71 | });
72 |
73 | describe("Bounding Box Extraction", () => {
74 | it("should extract bounding box from CSS styles", () => {
75 | const cssStyles = { left: "100px", top: "200px", width: "300px", height: "400px" };
76 | const box = extractBoundingBox(cssStyles);
77 |
78 | expect(box).toBeDefined();
79 | expect(box?.x).toBe(100);
80 | expect(box?.y).toBe(200);
81 | expect(box?.width).toBe(300);
82 | expect(box?.height).toBe(400);
83 | });
84 |
85 | it("should convert to ElementRect correctly", () => {
86 | const box: BoundingBox = { x: 100, y: 200, width: 300, height: 400 };
87 | const rect = toElementRect(box, 0);
88 |
89 | expect(rect.index).toBe(0);
90 | expect(rect.x).toBe(100);
91 | expect(rect.y).toBe(200);
92 | expect(rect.width).toBe(300);
93 | expect(rect.height).toBe(400);
94 | expect(rect.right).toBe(400);
95 | expect(rect.bottom).toBe(600);
96 | expect(rect.centerX).toBe(250);
97 | expect(rect.centerY).toBe(400);
98 | });
99 | });
100 |
101 | describe("Row Grouping (Y-axis overlap)", () => {
102 | it("should group horizontally aligned elements into same row", () => {
103 | const elements: ElementRect[] = [
104 | toElementRect({ x: 0, y: 10, width: 50, height: 30 }, 0),
105 | toElementRect({ x: 60, y: 15, width: 50, height: 30 }, 1),
106 | toElementRect({ x: 120, y: 12, width: 50, height: 30 }, 2),
107 | ];
108 |
109 | const rows = groupIntoRows(elements);
110 | expect(rows.length).toBe(1);
111 | expect(rows[0].length).toBe(3);
112 | });
113 |
114 | it("should separate vertically stacked elements into different rows", () => {
115 | const elements: ElementRect[] = [
116 | toElementRect({ x: 0, y: 0, width: 100, height: 30 }, 0),
117 | toElementRect({ x: 0, y: 50, width: 100, height: 30 }, 1),
118 | toElementRect({ x: 0, y: 100, width: 100, height: 30 }, 2),
119 | ];
120 |
121 | const rows = groupIntoRows(elements);
122 | // Elements don't overlap on Y-axis, expect multiple rows
123 | expect(rows.length).toBeGreaterThanOrEqual(1);
124 | });
125 | });
126 |
127 | describe("Column Grouping (X-axis overlap)", () => {
128 | it("should group vertically aligned elements into same column", () => {
129 | const elements: ElementRect[] = [
130 | toElementRect({ x: 10, y: 0, width: 30, height: 50 }, 0),
131 | toElementRect({ x: 15, y: 60, width: 30, height: 50 }, 1),
132 | toElementRect({ x: 12, y: 120, width: 30, height: 50 }, 2),
133 | ];
134 |
135 | const columns = groupIntoColumns(elements);
136 | expect(columns.length).toBe(1);
137 | expect(columns[0].length).toBe(3);
138 | });
139 | });
140 |
141 | describe("Gap Analysis", () => {
142 | it("should detect consistent gaps", () => {
143 | const gaps = [16, 16, 16, 16];
144 | const result = analyzeGaps(gaps);
145 |
146 | expect(result.isConsistent).toBe(true);
147 | expect(result.average).toBe(16);
148 | });
149 |
150 | it("should detect inconsistent gaps", () => {
151 | const gaps = [10, 30, 15, 40];
152 | const result = analyzeGaps(gaps);
153 |
154 | expect(result.isConsistent).toBe(false);
155 | });
156 |
157 | it("should handle gaps with small variance", () => {
158 | const gaps = [15, 16, 17, 16];
159 | const result = analyzeGaps(gaps);
160 |
161 | expect(result.isConsistent).toBe(true);
162 | });
163 | });
164 |
165 | describe("Alignment Detection", () => {
166 | it("should return alignment object with horizontal and vertical properties", () => {
167 | const elements: ElementRect[] = [
168 | toElementRect({ x: 0, y: 0, width: 100, height: 30 }, 0),
169 | toElementRect({ x: 0, y: 40, width: 150, height: 30 }, 1),
170 | toElementRect({ x: 0, y: 80, width: 80, height: 30 }, 2),
171 | ];
172 | const bounds: BoundingBox = { x: 0, y: 0, width: 200, height: 110 };
173 |
174 | const alignment = analyzeAlignment(elements, bounds);
175 | expect(alignment).toHaveProperty("horizontal");
176 | expect(alignment).toHaveProperty("vertical");
177 | });
178 |
179 | it("should analyze horizontal alignment", () => {
180 | const elements: ElementRect[] = [
181 | toElementRect({ x: 50, y: 0, width: 100, height: 30 }, 0),
182 | toElementRect({ x: 25, y: 40, width: 150, height: 30 }, 1),
183 | toElementRect({ x: 60, y: 80, width: 80, height: 30 }, 2),
184 | ];
185 | const bounds: BoundingBox = { x: 0, y: 0, width: 200, height: 110 };
186 |
187 | const alignment = analyzeAlignment(elements, bounds);
188 | expect(typeof alignment.horizontal).toBe("string");
189 | });
190 |
191 | it("should analyze vertical alignment", () => {
192 | const elements: ElementRect[] = [
193 | toElementRect({ x: 0, y: 0, width: 50, height: 100 }, 0),
194 | toElementRect({ x: 60, y: 0, width: 50, height: 80 }, 1),
195 | toElementRect({ x: 120, y: 0, width: 50, height: 120 }, 2),
196 | ];
197 | const bounds: BoundingBox = { x: 0, y: 0, width: 170, height: 120 };
198 |
199 | const alignment = analyzeAlignment(elements, bounds);
200 | expect(typeof alignment.vertical).toBe("string");
201 | });
202 | });
203 |
204 | describe("Bounds Calculation", () => {
205 | it("should calculate correct bounds for multiple elements", () => {
206 | const elements: ElementRect[] = [
207 | toElementRect({ x: 10, y: 20, width: 50, height: 30 }, 0),
208 | toElementRect({ x: 100, y: 5, width: 40, height: 60 }, 1),
209 | ];
210 |
211 | const bounds = calculateBounds(elements);
212 |
213 | expect(bounds.x).toBe(10);
214 | expect(bounds.y).toBe(5);
215 | expect(bounds.width).toBe(130);
216 | expect(bounds.height).toBe(60);
217 | });
218 |
219 | it("should handle empty array", () => {
220 | const bounds = calculateBounds([]);
221 |
222 | expect(bounds.x).toBe(0);
223 | expect(bounds.y).toBe(0);
224 | expect(bounds.width).toBe(0);
225 | expect(bounds.height).toBe(0);
226 | });
227 | });
228 |
229 | describe("Real Figma Data", () => {
230 | it("should process real Figma node data", () => {
231 | expect(testData).toBeDefined();
232 | expect(testData.type).toBe("GROUP");
233 | expect(testData.children).toBeDefined();
234 | });
235 |
236 | it("should extract child elements from real data", () => {
237 | const elements = extractChildElements(testData);
238 | expect(elements.length).toBeGreaterThan(0);
239 |
240 | elements.forEach((el) => {
241 | expect(typeof el.x).toBe("number");
242 | expect(typeof el.width).toBe("number");
243 | expect(typeof el.index).toBe("number");
244 | });
245 | });
246 | });
247 |
248 | describe("Value Clustering", () => {
249 | it("should cluster similar values together", () => {
250 | const values = [10, 12, 11, 50, 51, 52, 100, 101];
251 | const clusters = clusterValues(values, 3);
252 |
253 | expect(clusters.length).toBe(3);
254 | expect(clusters[0].count).toBe(3); // 10, 11, 12
255 | expect(clusters[1].count).toBe(3); // 50, 51, 52
256 | expect(clusters[2].count).toBe(2); // 100, 101
257 | });
258 |
259 | it("should handle empty array", () => {
260 | const clusters = clusterValues([]);
261 | expect(clusters.length).toBe(0);
262 | });
263 |
264 | it("should handle single value", () => {
265 | const clusters = clusterValues([42]);
266 | expect(clusters.length).toBe(1);
267 | expect(clusters[0].center).toBe(42);
268 | });
269 |
270 | it("should separate distant values", () => {
271 | const values = [0, 100, 200, 300];
272 | const clusters = clusterValues(values, 3);
273 |
274 | expect(clusters.length).toBe(4);
275 | });
276 | });
277 |
278 | describe("Grid Detection", () => {
279 | it("should detect a perfect 2x3 grid", () => {
280 | // 2 rows, 3 columns
281 | const elements: ElementRect[] = [
282 | // Row 1
283 | toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0),
284 | toElementRect({ x: 120, y: 0, width: 100, height: 50 }, 1),
285 | toElementRect({ x: 240, y: 0, width: 100, height: 50 }, 2),
286 | // Row 2
287 | toElementRect({ x: 0, y: 70, width: 100, height: 50 }, 3),
288 | toElementRect({ x: 120, y: 70, width: 100, height: 50 }, 4),
289 | toElementRect({ x: 240, y: 70, width: 100, height: 50 }, 5),
290 | ];
291 |
292 | const result = detectGridLayout(elements);
293 |
294 | expect(result.isGrid).toBe(true);
295 | expect(result.rowCount).toBe(2);
296 | expect(result.columnCount).toBe(3);
297 | expect(result.confidence).toBeGreaterThanOrEqual(0.6);
298 | });
299 |
300 | it("should detect a 3x2 grid", () => {
301 | // 3 rows, 2 columns
302 | const elements: ElementRect[] = [
303 | // Row 1
304 | toElementRect({ x: 0, y: 0, width: 80, height: 40 }, 0),
305 | toElementRect({ x: 100, y: 0, width: 80, height: 40 }, 1),
306 | // Row 2
307 | toElementRect({ x: 0, y: 60, width: 80, height: 40 }, 2),
308 | toElementRect({ x: 100, y: 60, width: 80, height: 40 }, 3),
309 | // Row 3
310 | toElementRect({ x: 0, y: 120, width: 80, height: 40 }, 4),
311 | toElementRect({ x: 100, y: 120, width: 80, height: 40 }, 5),
312 | ];
313 |
314 | const result = detectGridLayout(elements);
315 |
316 | expect(result.isGrid).toBe(true);
317 | expect(result.rowCount).toBe(3);
318 | expect(result.columnCount).toBe(2);
319 | });
320 |
321 | it("should calculate consistent row and column gaps", () => {
322 | const elements: ElementRect[] = [
323 | // Row 1 (gap = 20)
324 | toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0),
325 | toElementRect({ x: 120, y: 0, width: 100, height: 50 }, 1),
326 | // Row 2 (row gap = 16)
327 | toElementRect({ x: 0, y: 66, width: 100, height: 50 }, 2),
328 | toElementRect({ x: 120, y: 66, width: 100, height: 50 }, 3),
329 | ];
330 |
331 | const result = detectGridLayout(elements);
332 |
333 | expect(result.isGrid).toBe(true);
334 | expect(result.rowGap).toBe(16);
335 | expect(result.columnGap).toBe(20);
336 | });
337 |
338 | it("should generate track widths and heights", () => {
339 | const elements: ElementRect[] = [
340 | toElementRect({ x: 0, y: 0, width: 96, height: 64 }, 0), // Using common values
341 | toElementRect({ x: 120, y: 0, width: 80, height: 64 }, 1),
342 | toElementRect({ x: 0, y: 84, width: 96, height: 40 }, 2),
343 | toElementRect({ x: 120, y: 84, width: 80, height: 40 }, 3),
344 | ];
345 |
346 | const result = detectGridLayout(elements);
347 |
348 | expect(result.isGrid).toBe(true);
349 | expect(result.trackWidths.length).toBe(2);
350 | expect(result.trackHeights.length).toBe(2);
351 | // Track widths should be max of each column (rounded to common values)
352 | expect(result.trackWidths[0]).toBe(96);
353 | expect(result.trackWidths[1]).toBe(80);
354 | // Track heights should be max of each row
355 | expect(result.trackHeights[0]).toBe(64);
356 | expect(result.trackHeights[1]).toBe(40);
357 | });
358 |
359 | it("should build correct cell map", () => {
360 | const elements: ElementRect[] = [
361 | toElementRect({ x: 0, y: 0, width: 50, height: 50 }, 0),
362 | toElementRect({ x: 60, y: 0, width: 50, height: 50 }, 1),
363 | toElementRect({ x: 0, y: 60, width: 50, height: 50 }, 2),
364 | toElementRect({ x: 60, y: 60, width: 50, height: 50 }, 3),
365 | ];
366 |
367 | const result = detectGridLayout(elements);
368 |
369 | expect(result.cellMap.length).toBe(2); // 2 rows
370 | expect(result.cellMap[0].length).toBe(2); // 2 columns
371 | expect(result.cellMap[0][0]).toBe(0);
372 | expect(result.cellMap[0][1]).toBe(1);
373 | expect(result.cellMap[1][0]).toBe(2);
374 | expect(result.cellMap[1][1]).toBe(3);
375 | });
376 |
377 | it("should NOT detect grid for single row (flex row instead)", () => {
378 | const elements: ElementRect[] = [
379 | toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0),
380 | toElementRect({ x: 120, y: 0, width: 100, height: 50 }, 1),
381 | toElementRect({ x: 240, y: 0, width: 100, height: 50 }, 2),
382 | ];
383 |
384 | const result = detectGridLayout(elements);
385 |
386 | expect(result.isGrid).toBe(false);
387 | });
388 |
389 | it("should detect lower confidence for misaligned columns", () => {
390 | const elements: ElementRect[] = [
391 | // Row 1: columns at x=0, x=120
392 | toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0),
393 | toElementRect({ x: 120, y: 0, width: 100, height: 50 }, 1),
394 | // Row 2: columns at x=50, x=200 (misaligned!)
395 | toElementRect({ x: 50, y: 70, width: 100, height: 50 }, 2),
396 | toElementRect({ x: 200, y: 70, width: 100, height: 50 }, 3),
397 | ];
398 |
399 | const result = detectGridLayout(elements);
400 |
401 | // Misaligned columns should result in 4 column positions detected
402 | // and lower confidence due to alignment issues
403 | expect(result.columnCount).toBeGreaterThan(2);
404 | });
405 |
406 | it("should NOT detect grid for too few elements", () => {
407 | const elements: ElementRect[] = [
408 | toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0),
409 | toElementRect({ x: 120, y: 0, width: 100, height: 50 }, 1),
410 | toElementRect({ x: 0, y: 70, width: 100, height: 50 }, 2),
411 | ];
412 |
413 | const result = detectGridLayout(elements);
414 |
415 | // 3 elements is not enough for a meaningful grid
416 | expect(result.isGrid).toBe(false);
417 | });
418 |
419 | it("should handle grid with varying element sizes in same column", () => {
420 | const elements: ElementRect[] = [
421 | toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0),
422 | toElementRect({ x: 120, y: 0, width: 150, height: 50 }, 1), // wider
423 | toElementRect({ x: 0, y: 70, width: 100, height: 80 }, 2), // taller
424 | toElementRect({ x: 120, y: 70, width: 150, height: 80 }, 3),
425 | ];
426 |
427 | const result = detectGridLayout(elements);
428 |
429 | expect(result.isGrid).toBe(true);
430 | // Track widths should use max width in column
431 | expect(result.trackWidths[1]).toBe(150);
432 | });
433 | });
434 |
435 | describe("LayoutOptimizer Grid Integration", () => {
436 | // Helper to create SimplifiedNode from position/size
437 | function createNode(
438 | id: string,
439 | left: number,
440 | top: number,
441 | width: number,
442 | height: number,
443 | ): SimplifiedNode {
444 | return {
445 | id,
446 | name: `Node ${id}`,
447 | type: "FRAME",
448 | cssStyles: {
449 | left: `${left}px`,
450 | top: `${top}px`,
451 | width: `${width}px`,
452 | height: `${height}px`,
453 | },
454 | };
455 | }
456 |
457 | it("should detect and apply Grid CSS for 2x2 grid container", () => {
458 | const container: SimplifiedNode = {
459 | id: "container",
460 | name: "Grid Container",
461 | type: "FRAME",
462 | cssStyles: {
463 | width: "260px",
464 | height: "140px",
465 | },
466 | children: [
467 | createNode("1", 0, 0, 100, 50),
468 | createNode("2", 120, 0, 100, 50),
469 | createNode("3", 0, 70, 100, 50),
470 | createNode("4", 120, 70, 100, 50),
471 | ],
472 | };
473 |
474 | const result = LayoutOptimizer.optimizeContainer(container);
475 |
476 | expect(result.cssStyles?.display).toBe("grid");
477 | expect(result.cssStyles?.gridTemplateColumns).toBeDefined();
478 | });
479 |
480 | it("should apply gap for Grid layout", () => {
481 | const container: SimplifiedNode = {
482 | id: "container",
483 | name: "Grid Container",
484 | type: "FRAME",
485 | cssStyles: {
486 | width: "260px",
487 | height: "140px",
488 | },
489 | children: [
490 | createNode("1", 0, 0, 100, 50),
491 | createNode("2", 120, 0, 100, 50), // 20px column gap
492 | createNode("3", 0, 70, 100, 50), // 20px row gap
493 | createNode("4", 120, 70, 100, 50),
494 | ],
495 | };
496 |
497 | const result = LayoutOptimizer.optimizeContainer(container);
498 |
499 | expect(result.cssStyles?.display).toBe("grid");
500 | // Should have gap property
501 | expect(
502 | result.cssStyles?.gap || result.cssStyles?.rowGap || result.cssStyles?.columnGap,
503 | ).toBeDefined();
504 | });
505 |
506 | it("should fall back to flex for single row", () => {
507 | const container: SimplifiedNode = {
508 | id: "container",
509 | name: "Row Container",
510 | type: "FRAME",
511 | cssStyles: {
512 | width: "260px",
513 | height: "50px",
514 | },
515 | children: [
516 | createNode("1", 0, 0, 100, 50),
517 | createNode("2", 120, 0, 100, 50),
518 | createNode("3", 240, 0, 100, 50),
519 | ],
520 | };
521 |
522 | const result = LayoutOptimizer.optimizeContainer(container);
523 |
524 | // Should be flex, not grid (single row)
525 | expect(result.cssStyles?.display).toBe("flex");
526 | expect(result.cssStyles?.gridTemplateColumns).toBeUndefined();
527 | });
528 |
529 | it("should generate correct gridTemplateColumns", () => {
530 | // Use common design values (96, 80, 64) that don't get rounded
531 | const container: SimplifiedNode = {
532 | id: "container",
533 | name: "Grid Container",
534 | type: "FRAME",
535 | cssStyles: {
536 | width: "340px",
537 | height: "140px",
538 | },
539 | children: [
540 | createNode("1", 0, 0, 96, 48),
541 | createNode("2", 116, 0, 80, 48), // different width column
542 | createNode("3", 216, 0, 96, 48),
543 | createNode("4", 0, 68, 96, 48),
544 | createNode("5", 116, 68, 80, 48),
545 | createNode("6", 216, 68, 96, 48),
546 | ],
547 | };
548 |
549 | const result = LayoutOptimizer.optimizeContainer(container);
550 |
551 | expect(result.cssStyles?.display).toBe("grid");
552 | expect(result.cssStyles?.gridTemplateColumns).toContain("96px");
553 | expect(result.cssStyles?.gridTemplateColumns).toContain("80px");
554 | });
555 | });
556 |
557 | // ==================== IoU and Overlap Detection Tests ====================
558 |
559 | describe("IoU (Intersection over Union) Calculation", () => {
560 | it("should return 0 for non-overlapping elements", () => {
561 | const a = toElementRect({ x: 0, y: 0, width: 100, height: 100 }, 0);
562 | const b = toElementRect({ x: 200, y: 0, width: 100, height: 100 }, 1);
563 |
564 | const iou = calculateIoU(a, b);
565 | expect(iou).toBe(0);
566 | });
567 |
568 | it("should return 1 for identical elements", () => {
569 | const a = toElementRect({ x: 0, y: 0, width: 100, height: 100 }, 0);
570 | const b = toElementRect({ x: 0, y: 0, width: 100, height: 100 }, 1);
571 |
572 | const iou = calculateIoU(a, b);
573 | expect(iou).toBe(1);
574 | });
575 |
576 | it("should return correct IoU for partial overlap", () => {
577 | // 50% overlap on x-axis
578 | const a = toElementRect({ x: 0, y: 0, width: 100, height: 100 }, 0);
579 | const b = toElementRect({ x: 50, y: 0, width: 100, height: 100 }, 1);
580 |
581 | const iou = calculateIoU(a, b);
582 | // Intersection: 50 * 100 = 5000
583 | // Union: 100*100 + 100*100 - 5000 = 15000
584 | // IoU = 5000 / 15000 = 0.333...
585 | expect(iou).toBeCloseTo(0.333, 2);
586 | });
587 |
588 | it("should handle completely contained element", () => {
589 | const outer = toElementRect({ x: 0, y: 0, width: 200, height: 200 }, 0);
590 | const inner = toElementRect({ x: 50, y: 50, width: 100, height: 100 }, 1);
591 |
592 | const iou = calculateIoU(outer, inner);
593 | // Intersection: 100 * 100 = 10000
594 | // Union: 200*200 + 100*100 - 10000 = 40000 + 10000 - 10000 = 40000
595 | // IoU = 10000 / 40000 = 0.25
596 | expect(iou).toBeCloseTo(0.25, 2);
597 | });
598 | });
599 |
600 | describe("Overlap Classification", () => {
601 | it("should classify non-overlapping elements as 'none'", () => {
602 | const a = toElementRect({ x: 0, y: 0, width: 100, height: 100 }, 0);
603 | const b = toElementRect({ x: 200, y: 0, width: 100, height: 100 }, 1);
604 |
605 | const result = classifyOverlap(a, b);
606 | expect(result).toBe("none");
607 | });
608 |
609 | it("should classify adjacent elements as 'adjacent'", () => {
610 | const a = toElementRect({ x: 0, y: 0, width: 100, height: 100 }, 0);
611 | const b = toElementRect({ x: 101, y: 0, width: 100, height: 100 }, 1); // 1px gap
612 |
613 | const result = classifyOverlap(a, b);
614 | expect(result).toBe("adjacent");
615 | });
616 |
617 | it("should classify small overlap as 'partial'", () => {
618 | const a = toElementRect({ x: 0, y: 0, width: 100, height: 100 }, 0);
619 | const b = toElementRect({ x: 95, y: 0, width: 100, height: 100 }, 1); // 5% overlap
620 |
621 | const result = classifyOverlap(a, b);
622 | expect(result).toBe("partial");
623 | });
624 |
625 | it("should classify significant overlap as 'significant'", () => {
626 | const a = toElementRect({ x: 0, y: 0, width: 100, height: 100 }, 0);
627 | const b = toElementRect({ x: 50, y: 0, width: 100, height: 100 }, 1); // ~33% IoU
628 |
629 | const result = classifyOverlap(a, b);
630 | expect(result).toBe("significant");
631 | });
632 |
633 | it("should classify contained element as 'contained'", () => {
634 | const outer = toElementRect({ x: 0, y: 0, width: 100, height: 100 }, 0);
635 | const inner = toElementRect({ x: 10, y: 10, width: 80, height: 80 }, 1);
636 |
637 | const result = classifyOverlap(outer, inner);
638 | expect(result).toBe("contained");
639 | });
640 | });
641 |
642 | describe("Overlap Detection", () => {
643 | it("should separate overlapping elements from flow elements", () => {
644 | const elements: ElementRect[] = [
645 | toElementRect({ x: 0, y: 0, width: 100, height: 100 }, 0),
646 | toElementRect({ x: 50, y: 50, width: 100, height: 100 }, 1), // Overlaps with 0
647 | toElementRect({ x: 300, y: 0, width: 100, height: 100 }, 2), // No overlap
648 | toElementRect({ x: 450, y: 0, width: 100, height: 100 }, 3), // No overlap
649 | ];
650 |
651 | const result = detectOverlappingElements(elements, 0.1);
652 |
653 | expect(result.stackedElements.length).toBe(2); // Elements 0 and 1
654 | expect(result.flowElements.length).toBe(2); // Elements 2 and 3
655 | expect(result.stackedIndices.has(0)).toBe(true);
656 | expect(result.stackedIndices.has(1)).toBe(true);
657 | });
658 |
659 | it("should return all elements as flow when no overlap", () => {
660 | const elements: ElementRect[] = [
661 | toElementRect({ x: 0, y: 0, width: 100, height: 100 }, 0),
662 | toElementRect({ x: 150, y: 0, width: 100, height: 100 }, 1),
663 | toElementRect({ x: 300, y: 0, width: 100, height: 100 }, 2),
664 | ];
665 |
666 | const result = detectOverlappingElements(elements, 0.1);
667 |
668 | expect(result.stackedElements.length).toBe(0);
669 | expect(result.flowElements.length).toBe(3);
670 | });
671 |
672 | it("should detect multiple overlapping pairs", () => {
673 | const elements: ElementRect[] = [
674 | toElementRect({ x: 0, y: 0, width: 100, height: 100 }, 0),
675 | toElementRect({ x: 50, y: 0, width: 100, height: 100 }, 1), // Overlaps with 0
676 | toElementRect({ x: 300, y: 0, width: 100, height: 100 }, 2),
677 | toElementRect({ x: 350, y: 0, width: 100, height: 100 }, 3), // Overlaps with 2
678 | ];
679 |
680 | const result = detectOverlappingElements(elements, 0.1);
681 |
682 | expect(result.stackedElements.length).toBe(4); // All overlap with at least one
683 | });
684 |
685 | it("should use custom IoU threshold", () => {
686 | const elements: ElementRect[] = [
687 | toElementRect({ x: 0, y: 0, width: 100, height: 100 }, 0),
688 | toElementRect({ x: 80, y: 0, width: 100, height: 100 }, 1), // ~11% IoU
689 | ];
690 |
691 | // With 0.1 threshold, should detect overlap
692 | const result1 = detectOverlappingElements(elements, 0.1);
693 | expect(result1.stackedElements.length).toBe(2);
694 |
695 | // With 0.5 threshold, should NOT detect overlap
696 | const result2 = detectOverlappingElements(elements, 0.5);
697 | expect(result2.stackedElements.length).toBe(0);
698 | });
699 | });
700 |
701 | describe("Child Style Cleanup", () => {
702 | // Helper to create SimplifiedNode with absolute positioning
703 | function createAbsoluteNode(
704 | id: string,
705 | left: number,
706 | top: number,
707 | width: number,
708 | height: number,
709 | ): SimplifiedNode {
710 | return {
711 | id,
712 | name: `Node ${id}`,
713 | type: "FRAME",
714 | cssStyles: {
715 | position: "absolute",
716 | left: `${left}px`,
717 | top: `${top}px`,
718 | width: `${width}px`,
719 | height: `${height}px`,
720 | },
721 | };
722 | }
723 |
724 | it("should remove position:absolute from children when parent becomes flex", () => {
725 | const child = createAbsoluteNode("1", 0, 0, 100, 50);
726 | const cleaned = LayoutOptimizer.cleanChildStylesForLayout(child, "flex");
727 |
728 | expect(cleaned.cssStyles?.position).toBeUndefined();
729 | });
730 |
731 | it("should remove left/top from children when parent becomes flex", () => {
732 | const child = createAbsoluteNode("1", 100, 200, 100, 50);
733 | const cleaned = LayoutOptimizer.cleanChildStylesForLayout(child, "flex");
734 |
735 | expect(cleaned.cssStyles?.left).toBeUndefined();
736 | expect(cleaned.cssStyles?.top).toBeUndefined();
737 | });
738 |
739 | it("should keep width/height for flex children", () => {
740 | const child = createAbsoluteNode("1", 0, 0, 100, 50);
741 | const cleaned = LayoutOptimizer.cleanChildStylesForLayout(child, "flex");
742 |
743 | expect(cleaned.cssStyles?.width).toBe("100px");
744 | expect(cleaned.cssStyles?.height).toBe("50px");
745 | });
746 |
747 | it("should remove all position properties for grid children", () => {
748 | const child: SimplifiedNode = {
749 | id: "1",
750 | name: "Node 1",
751 | type: "FRAME",
752 | cssStyles: {
753 | position: "absolute",
754 | left: "10px",
755 | top: "20px",
756 | right: "30px",
757 | bottom: "40px",
758 | width: "100px",
759 | height: "50px",
760 | },
761 | };
762 |
763 | const cleaned = LayoutOptimizer.cleanChildStylesForLayout(child, "grid");
764 |
765 | expect(cleaned.cssStyles?.position).toBeUndefined();
766 | expect(cleaned.cssStyles?.left).toBeUndefined();
767 | expect(cleaned.cssStyles?.top).toBeUndefined();
768 | expect(cleaned.cssStyles?.right).toBeUndefined();
769 | expect(cleaned.cssStyles?.bottom).toBeUndefined();
770 | });
771 |
772 | it("should skip cleaning for stacked elements", () => {
773 | const children: SimplifiedNode[] = [
774 | createAbsoluteNode("1", 0, 0, 100, 50),
775 | createAbsoluteNode("2", 50, 0, 100, 50), // Overlapping
776 | ];
777 |
778 | // Mark index 0 and 1 as stacked
779 | const stackedIndices = new Set([0, 1]);
780 | const cleaned = LayoutOptimizer.cleanChildrenStyles(children, "flex", stackedIndices);
781 |
782 | // Stacked elements should keep their absolute positioning
783 | expect(cleaned[0].cssStyles?.position).toBe("absolute");
784 | expect(cleaned[1].cssStyles?.position).toBe("absolute");
785 | });
786 |
787 | it("should remove default CSS values", () => {
788 | const styles = {
789 | fontWeight: "400",
790 | textAlign: "left",
791 | opacity: "1",
792 | backgroundColor: "transparent",
793 | width: "100px",
794 | color: "#000",
795 | };
796 |
797 | const cleaned = LayoutOptimizer.removeDefaultValues(styles);
798 |
799 | expect(cleaned.fontWeight).toBeUndefined();
800 | expect(cleaned.textAlign).toBeUndefined();
801 | expect(cleaned.opacity).toBeUndefined();
802 | expect(cleaned.backgroundColor).toBeUndefined();
803 | expect(cleaned.width).toBe("100px"); // Keep non-default
804 | expect(cleaned.color).toBe("#000"); // Keep non-default
805 | });
806 |
807 | it("should remove 0px position values", () => {
808 | const styles = {
809 | left: "0px",
810 | top: "0",
811 | right: "10px",
812 | width: "100px",
813 | };
814 |
815 | const cleaned = LayoutOptimizer.removeDefaultValues(styles);
816 |
817 | expect(cleaned.left).toBeUndefined();
818 | expect(cleaned.top).toBeUndefined();
819 | expect(cleaned.right).toBe("10px"); // Keep non-zero
820 | expect(cleaned.width).toBe("100px");
821 | });
822 | });
823 |
824 | describe("Integrated Overlap and Cleanup in optimizeContainer", () => {
825 | function createAbsoluteNode(
826 | id: string,
827 | left: number,
828 | top: number,
829 | width: number,
830 | height: number,
831 | ): SimplifiedNode {
832 | return {
833 | id,
834 | name: `Node ${id}`,
835 | type: "FRAME",
836 | cssStyles: {
837 | position: "absolute",
838 | left: `${left}px`,
839 | top: `${top}px`,
840 | width: `${width}px`,
841 | height: `${height}px`,
842 | },
843 | };
844 | }
845 |
846 | it("should clean child styles when parent becomes flex", () => {
847 | const container: SimplifiedNode = {
848 | id: "container",
849 | name: "Flex Container",
850 | type: "FRAME",
851 | cssStyles: { width: "500px", height: "100px" },
852 | children: [
853 | createAbsoluteNode("1", 0, 0, 100, 50),
854 | createAbsoluteNode("2", 120, 0, 100, 50),
855 | createAbsoluteNode("3", 240, 0, 100, 50),
856 | ],
857 | };
858 |
859 | const result = LayoutOptimizer.optimizeContainer(container);
860 |
861 | expect(result.cssStyles?.display).toBe("flex");
862 | // Children should have position:absolute removed
863 | result.children?.forEach((child) => {
864 | expect(child.cssStyles?.position).toBeUndefined();
865 | expect(child.cssStyles?.left).toBeUndefined();
866 | expect(child.cssStyles?.top).toBeUndefined();
867 | });
868 | });
869 |
870 | it("should clean child styles when parent becomes grid", () => {
871 | const container: SimplifiedNode = {
872 | id: "container",
873 | name: "Grid Container",
874 | type: "FRAME",
875 | cssStyles: { width: "260px", height: "140px" },
876 | children: [
877 | createAbsoluteNode("1", 0, 0, 100, 50),
878 | createAbsoluteNode("2", 120, 0, 100, 50),
879 | createAbsoluteNode("3", 0, 70, 100, 50),
880 | createAbsoluteNode("4", 120, 70, 100, 50),
881 | ],
882 | };
883 |
884 | const result = LayoutOptimizer.optimizeContainer(container);
885 |
886 | expect(result.cssStyles?.display).toBe("grid");
887 | // Children should have position:absolute removed
888 | result.children?.forEach((child) => {
889 | expect(child.cssStyles?.position).toBeUndefined();
890 | expect(child.cssStyles?.left).toBeUndefined();
891 | expect(child.cssStyles?.top).toBeUndefined();
892 | });
893 | });
894 | });
895 |
896 | describe("Homogeneity Analysis", () => {
897 | it("should detect homogeneous elements with similar sizes", () => {
898 | const elements: ElementRect[] = [
899 | toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0),
900 | toElementRect({ x: 120, y: 0, width: 100, height: 50 }, 1),
901 | toElementRect({ x: 0, y: 70, width: 100, height: 50 }, 2),
902 | toElementRect({ x: 120, y: 70, width: 100, height: 50 }, 3),
903 | ];
904 |
905 | const result = analyzeHomogeneity(elements);
906 |
907 | expect(result.isHomogeneous).toBe(true);
908 | expect(result.widthCV).toBe(0);
909 | expect(result.heightCV).toBe(0);
910 | expect(result.homogeneousElements.length).toBe(4);
911 | expect(result.outlierElements.length).toBe(0);
912 | });
913 |
914 | it("should detect non-homogeneous elements with varying sizes", () => {
915 | const elements: ElementRect[] = [
916 | toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0),
917 | toElementRect({ x: 120, y: 0, width: 200, height: 100 }, 1), // Much larger
918 | toElementRect({ x: 0, y: 70, width: 50, height: 25 }, 2), // Much smaller
919 | toElementRect({ x: 120, y: 70, width: 100, height: 50 }, 3),
920 | ];
921 |
922 | const result = analyzeHomogeneity(elements);
923 |
924 | // Elements have high size variance
925 | expect(result.widthCV).toBeGreaterThan(0.2);
926 | });
927 |
928 | it("should return not homogeneous for fewer than 4 elements", () => {
929 | const elements: ElementRect[] = [
930 | toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0),
931 | toElementRect({ x: 120, y: 0, width: 100, height: 50 }, 1),
932 | toElementRect({ x: 0, y: 70, width: 100, height: 50 }, 2),
933 | ];
934 |
935 | const result = analyzeHomogeneity(elements);
936 |
937 | expect(result.isHomogeneous).toBe(false);
938 | });
939 |
940 | it("should filter by node types when provided", () => {
941 | const elements: ElementRect[] = [
942 | toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0),
943 | toElementRect({ x: 120, y: 0, width: 100, height: 50 }, 1),
944 | toElementRect({ x: 0, y: 70, width: 100, height: 50 }, 2),
945 | toElementRect({ x: 120, y: 70, width: 100, height: 50 }, 3),
946 | ];
947 | const nodeTypes = ["FRAME", "FRAME", "FRAME", "FRAME"];
948 |
949 | const result = analyzeHomogeneity(elements, nodeTypes);
950 |
951 | expect(result.isHomogeneous).toBe(true);
952 | expect(result.types).toContain("FRAME");
953 | });
954 |
955 | it("should reject incompatible node types", () => {
956 | const elements: ElementRect[] = [
957 | toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0),
958 | toElementRect({ x: 120, y: 0, width: 100, height: 50 }, 1),
959 | toElementRect({ x: 0, y: 70, width: 100, height: 50 }, 2),
960 | toElementRect({ x: 120, y: 70, width: 100, height: 50 }, 3),
961 | ];
962 | // TEXT nodes are not allowed in grid
963 | const nodeTypes = ["TEXT", "TEXT", "TEXT", "TEXT"];
964 |
965 | const result = analyzeHomogeneity(elements, nodeTypes);
966 |
967 | expect(result.isHomogeneous).toBe(false);
968 | });
969 |
970 | it("should separate outliers from homogeneous elements", () => {
971 | const elements: ElementRect[] = [
972 | // 4 similar-sized elements
973 | toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0),
974 | toElementRect({ x: 120, y: 0, width: 100, height: 50 }, 1),
975 | toElementRect({ x: 0, y: 70, width: 100, height: 50 }, 2),
976 | toElementRect({ x: 120, y: 70, width: 100, height: 50 }, 3),
977 | // 1 outlier (much larger)
978 | toElementRect({ x: 0, y: 140, width: 300, height: 200 }, 4),
979 | ];
980 |
981 | const result = analyzeHomogeneity(elements);
982 |
983 | expect(result.isHomogeneous).toBe(true);
984 | expect(result.homogeneousElements.length).toBe(4);
985 | expect(result.outlierElements.length).toBe(1);
986 | expect(result.outlierElements[0].index).toBe(4);
987 | });
988 |
989 | it("should use custom size tolerance", () => {
990 | const elements: ElementRect[] = [
991 | toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0),
992 | toElementRect({ x: 120, y: 0, width: 110, height: 55 }, 1), // 10% larger
993 | toElementRect({ x: 0, y: 70, width: 90, height: 45 }, 2), // 10% smaller
994 | toElementRect({ x: 120, y: 70, width: 100, height: 50 }, 3),
995 | ];
996 |
997 | // With strict tolerance (5%), should fail
998 | const strictResult = analyzeHomogeneity(elements, undefined, 0.05);
999 | expect(strictResult.isHomogeneous).toBe(false);
1000 |
1001 | // With relaxed tolerance (25%), should pass
1002 | const relaxedResult = analyzeHomogeneity(elements, undefined, 0.25);
1003 | expect(relaxedResult.isHomogeneous).toBe(true);
1004 | });
1005 | });
1006 |
1007 | describe("Filter Homogeneous For Grid", () => {
1008 | it("should return homogeneous elements for grid detection", () => {
1009 | const elements: ElementRect[] = [
1010 | toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0),
1011 | toElementRect({ x: 120, y: 0, width: 100, height: 50 }, 1),
1012 | toElementRect({ x: 0, y: 70, width: 100, height: 50 }, 2),
1013 | toElementRect({ x: 120, y: 70, width: 100, height: 50 }, 3),
1014 | ];
1015 |
1016 | const result = filterHomogeneousForGrid(elements);
1017 |
1018 | expect(result.elements.length).toBe(4);
1019 | expect(result.gridIndices.size).toBe(4);
1020 | });
1021 |
1022 | it("should return empty array for non-homogeneous elements", () => {
1023 | const elements: ElementRect[] = [
1024 | toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0),
1025 | toElementRect({ x: 120, y: 0, width: 300, height: 200 }, 1), // Very different
1026 | toElementRect({ x: 0, y: 70, width: 50, height: 25 }, 2), // Very different
1027 | toElementRect({ x: 120, y: 70, width: 100, height: 50 }, 3),
1028 | ];
1029 |
1030 | const result = filterHomogeneousForGrid(elements);
1031 |
1032 | // Not enough homogeneous elements
1033 | expect(result.elements.length).toBeLessThan(4);
1034 | });
1035 |
1036 | it("should return empty array for fewer than 4 elements", () => {
1037 | const elements: ElementRect[] = [
1038 | toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0),
1039 | toElementRect({ x: 120, y: 0, width: 100, height: 50 }, 1),
1040 | toElementRect({ x: 0, y: 70, width: 100, height: 50 }, 2),
1041 | ];
1042 |
1043 | const result = filterHomogeneousForGrid(elements);
1044 |
1045 | expect(result.elements.length).toBe(0);
1046 | expect(result.gridIndices.size).toBe(0);
1047 | });
1048 |
1049 | it("should filter out outliers and return only homogeneous elements", () => {
1050 | const elements: ElementRect[] = [
1051 | // 4 similar-sized elements (grid candidates)
1052 | toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0),
1053 | toElementRect({ x: 120, y: 0, width: 100, height: 50 }, 1),
1054 | toElementRect({ x: 0, y: 70, width: 100, height: 50 }, 2),
1055 | toElementRect({ x: 120, y: 70, width: 100, height: 50 }, 3),
1056 | // Outlier (header or title)
1057 | toElementRect({ x: 0, y: -30, width: 240, height: 20 }, 4),
1058 | ];
1059 |
1060 | const result = filterHomogeneousForGrid(elements);
1061 |
1062 | expect(result.elements.length).toBe(4);
1063 | // Outlier should not be included
1064 | expect(result.elements.every((e) => e.index !== 4)).toBe(true);
1065 | expect(result.gridIndices.has(4)).toBe(false);
1066 | });
1067 |
1068 | it("should work with node types filtering", () => {
1069 | const elements: ElementRect[] = [
1070 | toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0),
1071 | toElementRect({ x: 120, y: 0, width: 100, height: 50 }, 1),
1072 | toElementRect({ x: 0, y: 70, width: 100, height: 50 }, 2),
1073 | toElementRect({ x: 120, y: 70, width: 100, height: 50 }, 3),
1074 | ];
1075 | const nodeTypes = ["INSTANCE", "INSTANCE", "INSTANCE", "INSTANCE"];
1076 |
1077 | const result = filterHomogeneousForGrid(elements, nodeTypes);
1078 |
1079 | expect(result.elements.length).toBe(4);
1080 | expect(result.gridIndices.size).toBe(4);
1081 | });
1082 | });
1083 |
1084 | describe("Grid Detection with Real Data", () => {
1085 | let testData: FigmaNode;
1086 |
1087 | beforeAll(() => {
1088 | testData = loadTestData();
1089 | });
1090 |
1091 | it("should detect grid layout in real Figma data", () => {
1092 | // Find any container with 4+ children that might form a grid
1093 | function findGridCandidate(node: FigmaNode): FigmaNode | null {
1094 | if (node.children && node.children.length >= 4) {
1095 | const childBoxes = node.children
1096 | .filter((c) => c.absoluteBoundingBox)
1097 | .map((c, i) => toElementRect(c.absoluteBoundingBox!, i));
1098 |
1099 | if (childBoxes.length >= 4) {
1100 | const gridResult = detectGridLayout(childBoxes);
1101 | if (gridResult.isGrid && gridResult.rowCount >= 2) {
1102 | return node;
1103 | }
1104 | }
1105 | }
1106 |
1107 | if (node.children) {
1108 | for (const child of node.children) {
1109 | const found = findGridCandidate(child);
1110 | if (found) return found;
1111 | }
1112 | }
1113 | return null;
1114 | }
1115 |
1116 | const gridCandidate = findGridCandidate(testData);
1117 |
1118 | // Test passes if we either find a grid or don't (depends on fixture data)
1119 | if (gridCandidate) {
1120 | const childBoxes = gridCandidate
1121 | .children!.filter((c) => c.absoluteBoundingBox)
1122 | .map((c, i) => toElementRect(c.absoluteBoundingBox!, i));
1123 |
1124 | const gridResult = detectGridLayout(childBoxes);
1125 |
1126 | expect(gridResult.isGrid).toBe(true);
1127 | expect(gridResult.rowCount).toBeGreaterThanOrEqual(2);
1128 | expect(gridResult.columnCount).toBeGreaterThanOrEqual(2);
1129 | expect(gridResult.trackWidths.length).toBe(gridResult.columnCount);
1130 | }
1131 | });
1132 |
1133 | it("should filter homogeneous elements before grid detection", () => {
1134 | // Find a container with mixed children
1135 | function findMixedContainer(node: FigmaNode): FigmaNode | null {
1136 | if (node.children && node.children.length >= 5) {
1137 | const sizes = node.children
1138 | .filter((c) => c.absoluteBoundingBox)
1139 | .map((c) => c.absoluteBoundingBox!.width * c.absoluteBoundingBox!.height);
1140 |
1141 | if (sizes.length >= 5) {
1142 | const maxSize = Math.max(...sizes);
1143 | const minSize = Math.min(...sizes);
1144 | // Look for containers where max is at least 2x min (mixed sizes)
1145 | if (maxSize > minSize * 2) {
1146 | return node;
1147 | }
1148 | }
1149 | }
1150 |
1151 | if (node.children) {
1152 | for (const child of node.children) {
1153 | const found = findMixedContainer(child);
1154 | if (found) return found;
1155 | }
1156 | }
1157 | return null;
1158 | }
1159 |
1160 | const mixedContainer = findMixedContainer(testData);
1161 |
1162 | if (mixedContainer) {
1163 | const childBoxes = mixedContainer
1164 | .children!.filter((c) => c.absoluteBoundingBox)
1165 | .map((c, i) => toElementRect(c.absoluteBoundingBox!, i));
1166 |
1167 | const nodeTypes = mixedContainer.children!.map((c) => c.type);
1168 |
1169 | // Filter should reduce the set
1170 | const result = filterHomogeneousForGrid(childBoxes, nodeTypes);
1171 |
1172 | // Filtered should be less than or equal to original
1173 | expect(result.elements.length).toBeLessThanOrEqual(childBoxes.length);
1174 | }
1175 | });
1176 | });
1177 |
1178 | // ==================== Absolute to Relative Position Conversion ====================
1179 | describe("Absolute to Relative Position Conversion", () => {
1180 | describe("collectFlowChildOffsets", () => {
1181 | it("should collect offsets from flow children only", () => {
1182 | const children: SimplifiedNode[] = [
1183 | {
1184 | id: "1",
1185 | name: "child1",
1186 | type: "FRAME",
1187 | cssStyles: { left: "10px", top: "20px", width: "100px", height: "50px" },
1188 | },
1189 | {
1190 | id: "2",
1191 | name: "child2",
1192 | type: "FRAME",
1193 | cssStyles: { left: "120px", top: "20px", width: "100px", height: "50px" },
1194 | },
1195 | {
1196 | id: "3",
1197 | name: "stacked",
1198 | type: "FRAME",
1199 | cssStyles: { left: "50px", top: "30px", width: "80px", height: "40px" },
1200 | },
1201 | ];
1202 |
1203 | // Child index 2 is stacked (overlapping)
1204 | const stackedIndices = new Set([2]);
1205 |
1206 | const offsets = LayoutOptimizer.collectFlowChildOffsets(children, stackedIndices);
1207 |
1208 | expect(offsets.length).toBe(2); // Only 2 flow children
1209 | expect(offsets[0].index).toBe(0);
1210 | expect(offsets[0].left).toBe(10);
1211 | expect(offsets[0].top).toBe(20);
1212 | expect(offsets[1].index).toBe(1);
1213 | expect(offsets[1].left).toBe(120);
1214 | });
1215 |
1216 | it("should skip children without cssStyles", () => {
1217 | const children: SimplifiedNode[] = [
1218 | { id: "1", name: "child1", type: "FRAME" },
1219 | {
1220 | id: "2",
1221 | name: "child2",
1222 | type: "FRAME",
1223 | cssStyles: { left: "10px", top: "20px", width: "100px", height: "50px" },
1224 | },
1225 | ];
1226 |
1227 | const offsets = LayoutOptimizer.collectFlowChildOffsets(children, new Set());
1228 |
1229 | expect(offsets.length).toBe(1);
1230 | expect(offsets[0].index).toBe(1);
1231 | });
1232 | });
1233 |
1234 | describe("inferContainerPadding", () => {
1235 | it("should infer padding from child offsets", () => {
1236 | const offsets = [
1237 | { index: 0, left: 20, top: 15, width: 100, height: 50, right: 120, bottom: 65 },
1238 | { index: 1, left: 130, top: 15, width: 100, height: 50, right: 230, bottom: 65 },
1239 | ];
1240 |
1241 | const padding = LayoutOptimizer.inferContainerPadding(offsets, 250, 80, "row");
1242 |
1243 | expect(padding.paddingLeft).toBe(20);
1244 | expect(padding.paddingTop).toBe(15);
1245 | expect(padding.paddingRight).toBe(20); // 250 - 230 = 20
1246 | expect(padding.paddingBottom).toBe(15); // 80 - 65 = 15
1247 | });
1248 |
1249 | it("should return zero padding for small offsets (<= 2px)", () => {
1250 | const offsets = [
1251 | { index: 0, left: 1, top: 2, width: 100, height: 50, right: 101, bottom: 52 },
1252 | ];
1253 |
1254 | const padding = LayoutOptimizer.inferContainerPadding(offsets, 103, 54, "row");
1255 |
1256 | expect(padding.paddingLeft).toBe(0); // 1 <= 2
1257 | expect(padding.paddingTop).toBe(0); // 2 <= 2
1258 | expect(padding.paddingRight).toBe(0); // 103 - 101 = 2 <= 2
1259 | expect(padding.paddingBottom).toBe(0); // 54 - 52 = 2 <= 2
1260 | });
1261 |
1262 | it("should return zero padding for empty offsets", () => {
1263 | const padding = LayoutOptimizer.inferContainerPadding([], 100, 100, "row");
1264 |
1265 | expect(padding.paddingLeft).toBe(0);
1266 | expect(padding.paddingTop).toBe(0);
1267 | expect(padding.paddingRight).toBe(0);
1268 | expect(padding.paddingBottom).toBe(0);
1269 | });
1270 | });
1271 |
1272 | describe("calculateChildMargins", () => {
1273 | it("should calculate marginTop for row layout with flex-start alignment", () => {
1274 | const offsets = [
1275 | { index: 0, left: 10, top: 10, width: 100, height: 50, right: 110, bottom: 60 },
1276 | { index: 1, left: 120, top: 25, width: 100, height: 30, right: 220, bottom: 55 }, // offset 15px down
1277 | ];
1278 | const padding = { paddingTop: 10, paddingRight: 10, paddingBottom: 10, paddingLeft: 10 };
1279 |
1280 | const margins = LayoutOptimizer.calculateChildMargins(
1281 | offsets,
1282 | padding,
1283 | "row",
1284 | "flex-start",
1285 | );
1286 |
1287 | expect(margins.get(0)).toBeUndefined(); // No margin needed
1288 | expect(margins.get(1)?.marginTop).toBe(15); // 25 - 10 = 15
1289 | });
1290 |
1291 | it("should calculate marginLeft for column layout with flex-start alignment", () => {
1292 | const offsets = [
1293 | { index: 0, left: 10, top: 10, width: 100, height: 50, right: 110, bottom: 60 },
1294 | { index: 1, left: 30, top: 70, width: 80, height: 50, right: 110, bottom: 120 }, // offset 20px right
1295 | ];
1296 | const padding = { paddingTop: 10, paddingRight: 10, paddingBottom: 10, paddingLeft: 10 };
1297 |
1298 | const margins = LayoutOptimizer.calculateChildMargins(
1299 | offsets,
1300 | padding,
1301 | "column",
1302 | "flex-start",
1303 | );
1304 |
1305 | expect(margins.get(0)).toBeUndefined(); // No margin needed
1306 | expect(margins.get(1)?.marginLeft).toBe(20); // 30 - 10 = 20
1307 | });
1308 |
1309 | it("should not add margins for center alignment", () => {
1310 | const offsets = [
1311 | { index: 0, left: 10, top: 10, width: 100, height: 50, right: 110, bottom: 60 },
1312 | { index: 1, left: 120, top: 25, width: 100, height: 30, right: 220, bottom: 55 },
1313 | ];
1314 | const padding = { paddingTop: 10, paddingRight: 10, paddingBottom: 10, paddingLeft: 10 };
1315 |
1316 | const margins = LayoutOptimizer.calculateChildMargins(offsets, padding, "row", "center");
1317 |
1318 | expect(margins.size).toBe(0); // No margins for center alignment
1319 | });
1320 | });
1321 |
1322 | describe("generatePaddingCSS", () => {
1323 | it("should generate single value when all padding is equal", () => {
1324 | const padding = { paddingTop: 10, paddingRight: 10, paddingBottom: 10, paddingLeft: 10 };
1325 | const css = LayoutOptimizer.generatePaddingCSS(padding);
1326 | expect(css).toBe("10px");
1327 | });
1328 |
1329 | it("should generate two values when top/bottom and left/right are equal", () => {
1330 | const padding = { paddingTop: 10, paddingRight: 20, paddingBottom: 10, paddingLeft: 20 };
1331 | const css = LayoutOptimizer.generatePaddingCSS(padding);
1332 | expect(css).toBe("10px 20px");
1333 | });
1334 |
1335 | it("should generate three values when left/right are equal", () => {
1336 | const padding = { paddingTop: 10, paddingRight: 20, paddingBottom: 30, paddingLeft: 20 };
1337 | const css = LayoutOptimizer.generatePaddingCSS(padding);
1338 | expect(css).toBe("10px 20px 30px");
1339 | });
1340 |
1341 | it("should generate four values when all padding is different", () => {
1342 | const padding = { paddingTop: 10, paddingRight: 20, paddingBottom: 30, paddingLeft: 40 };
1343 | const css = LayoutOptimizer.generatePaddingCSS(padding);
1344 | expect(css).toBe("10px 20px 30px 40px");
1345 | });
1346 |
1347 | it("should return null when all padding is zero", () => {
1348 | const padding = { paddingTop: 0, paddingRight: 0, paddingBottom: 0, paddingLeft: 0 };
1349 | const css = LayoutOptimizer.generatePaddingCSS(padding);
1350 | expect(css).toBeNull();
1351 | });
1352 | });
1353 |
1354 | describe("convertAbsoluteToRelative", () => {
1355 | it("should convert absolute positioning to padding and clean children", () => {
1356 | const parent: SimplifiedNode = {
1357 | id: "parent",
1358 | name: "container",
1359 | type: "FRAME",
1360 | cssStyles: { width: "300px", height: "100px" },
1361 | };
1362 |
1363 | const children: SimplifiedNode[] = [
1364 | {
1365 | id: "1",
1366 | name: "child1",
1367 | type: "FRAME",
1368 | cssStyles: {
1369 | position: "absolute",
1370 | left: "20px",
1371 | top: "10px",
1372 | width: "100px",
1373 | height: "80px",
1374 | },
1375 | },
1376 | {
1377 | id: "2",
1378 | name: "child2",
1379 | type: "FRAME",
1380 | cssStyles: {
1381 | position: "absolute",
1382 | left: "140px",
1383 | top: "10px",
1384 | width: "140px",
1385 | height: "80px",
1386 | },
1387 | },
1388 | ];
1389 |
1390 | const result = LayoutOptimizer.convertAbsoluteToRelative(
1391 | parent,
1392 | children,
1393 | "flex",
1394 | "row",
1395 | new Set(),
1396 | "flex-start",
1397 | );
1398 |
1399 | // Should have padding
1400 | expect(result.parentPaddingStyle).toBe("10px 20px");
1401 |
1402 | // Children should not have position: absolute or left/top
1403 | expect(result.convertedChildren[0].cssStyles?.position).toBeUndefined();
1404 | expect(result.convertedChildren[0].cssStyles?.left).toBeUndefined();
1405 | expect(result.convertedChildren[0].cssStyles?.top).toBeUndefined();
1406 | expect(result.convertedChildren[1].cssStyles?.position).toBeUndefined();
1407 | });
1408 |
1409 | it("should keep stacked elements with absolute positioning", () => {
1410 | const parent: SimplifiedNode = {
1411 | id: "parent",
1412 | name: "container",
1413 | type: "FRAME",
1414 | cssStyles: { width: "200px", height: "100px" },
1415 | };
1416 |
1417 | const children: SimplifiedNode[] = [
1418 | {
1419 | id: "1",
1420 | name: "background",
1421 | type: "RECTANGLE",
1422 | cssStyles: {
1423 | position: "absolute",
1424 | left: "0px",
1425 | top: "0px",
1426 | width: "200px",
1427 | height: "100px",
1428 | },
1429 | },
1430 | {
1431 | id: "2",
1432 | name: "content",
1433 | type: "FRAME",
1434 | cssStyles: {
1435 | position: "absolute",
1436 | left: "20px",
1437 | top: "10px",
1438 | width: "160px",
1439 | height: "80px",
1440 | },
1441 | },
1442 | ];
1443 |
1444 | // Child 0 is stacked (background)
1445 | const stackedIndices = new Set([0]);
1446 |
1447 | const result = LayoutOptimizer.convertAbsoluteToRelative(
1448 | parent,
1449 | children,
1450 | "flex",
1451 | "row",
1452 | stackedIndices,
1453 | "flex-start",
1454 | );
1455 |
1456 | // Stacked element should keep absolute positioning
1457 | expect(result.convertedChildren[0].cssStyles?.position).toBe("absolute");
1458 | expect(result.convertedChildren[0].cssStyles?.left).toBe("0px");
1459 |
1460 | // Flow element should be cleaned
1461 | expect(result.convertedChildren[1].cssStyles?.position).toBeUndefined();
1462 | });
1463 | });
1464 | });
1465 |
1466 | // ==================== Background Element Detection Tests ====================
1467 | describe("detectBackgroundElement", () => {
1468 | it("should detect background element at origin matching parent size", () => {
1469 | const rects: ElementRect[] = [
1470 | {
1471 | x: 0,
1472 | y: 0,
1473 | width: 400,
1474 | height: 300,
1475 | index: 0,
1476 | right: 400,
1477 | bottom: 300,
1478 | centerX: 200,
1479 | centerY: 150,
1480 | },
1481 | {
1482 | x: 20,
1483 | y: 20,
1484 | width: 100,
1485 | height: 50,
1486 | index: 1,
1487 | right: 120,
1488 | bottom: 70,
1489 | centerX: 70,
1490 | centerY: 45,
1491 | },
1492 | {
1493 | x: 150,
1494 | y: 100,
1495 | width: 80,
1496 | height: 40,
1497 | index: 2,
1498 | right: 230,
1499 | bottom: 140,
1500 | centerX: 190,
1501 | centerY: 120,
1502 | },
1503 | ];
1504 |
1505 | const result = detectBackgroundElement(rects, 400, 300);
1506 |
1507 | expect(result.hasBackground).toBe(true);
1508 | expect(result.backgroundIndex).toBe(0);
1509 | expect(result.contentIndices).toEqual([1, 2]);
1510 | });
1511 |
1512 | it("should not detect background when no element matches parent size", () => {
1513 | const rects: ElementRect[] = [
1514 | {
1515 | x: 10,
1516 | y: 10,
1517 | width: 200,
1518 | height: 150,
1519 | index: 0,
1520 | right: 210,
1521 | bottom: 160,
1522 | centerX: 110,
1523 | centerY: 85,
1524 | },
1525 | {
1526 | x: 50,
1527 | y: 50,
1528 | width: 100,
1529 | height: 50,
1530 | index: 1,
1531 | right: 150,
1532 | bottom: 100,
1533 | centerX: 100,
1534 | centerY: 75,
1535 | },
1536 | ];
1537 |
1538 | const result = detectBackgroundElement(rects, 400, 300);
1539 |
1540 | expect(result.hasBackground).toBe(false);
1541 | expect(result.backgroundIndex).toBe(-1);
1542 | });
1543 |
1544 | it("should detect background with small tolerance (within 5%)", () => {
1545 | const rects: ElementRect[] = [
1546 | {
1547 | x: 0,
1548 | y: 0,
1549 | width: 395,
1550 | height: 290,
1551 | index: 0,
1552 | right: 395,
1553 | bottom: 290,
1554 | centerX: 197.5,
1555 | centerY: 145,
1556 | },
1557 | {
1558 | x: 20,
1559 | y: 20,
1560 | width: 100,
1561 | height: 50,
1562 | index: 1,
1563 | right: 120,
1564 | bottom: 70,
1565 | centerX: 70,
1566 | centerY: 45,
1567 | },
1568 | ];
1569 |
1570 | const result = detectBackgroundElement(rects, 400, 300);
1571 |
1572 | expect(result.hasBackground).toBe(true);
1573 | expect(result.backgroundIndex).toBe(0);
1574 | });
1575 |
1576 | it("should return empty result for single element", () => {
1577 | const rects: ElementRect[] = [
1578 | {
1579 | x: 0,
1580 | y: 0,
1581 | width: 400,
1582 | height: 300,
1583 | index: 0,
1584 | right: 400,
1585 | bottom: 300,
1586 | centerX: 200,
1587 | centerY: 150,
1588 | },
1589 | ];
1590 |
1591 | const result = detectBackgroundElement(rects, 400, 300);
1592 |
1593 | expect(result.hasBackground).toBe(false);
1594 | });
1595 | });
1596 |
1597 | // ==================== Background Style Extraction Tests ====================
1598 | describe("extractBackgroundStyles", () => {
1599 | it("should extract backgroundColor from background element", () => {
1600 | const bgChild: SimplifiedNode = {
1601 | id: "bg-1",
1602 | name: "Background",
1603 | type: "RECTANGLE",
1604 | cssStyles: {
1605 | backgroundColor: "rgba(255, 255, 255, 1)",
1606 | width: "400px",
1607 | height: "300px",
1608 | },
1609 | };
1610 |
1611 | const result = LayoutOptimizer.extractBackgroundStyles(bgChild);
1612 |
1613 | expect(result.backgroundColor).toBe("rgba(255, 255, 255, 1)");
1614 | expect(result.width).toBeUndefined();
1615 | expect(result.height).toBeUndefined();
1616 | });
1617 |
1618 | it("should extract borderRadius from background element", () => {
1619 | const bgChild: SimplifiedNode = {
1620 | id: "bg-1",
1621 | name: "Background",
1622 | type: "RECTANGLE",
1623 | cssStyles: {
1624 | backgroundColor: "rgba(0, 0, 0, 1)",
1625 | borderRadius: "8px",
1626 | },
1627 | };
1628 |
1629 | const result = LayoutOptimizer.extractBackgroundStyles(bgChild);
1630 |
1631 | expect(result.backgroundColor).toBe("rgba(0, 0, 0, 1)");
1632 | expect(result.borderRadius).toBe("8px");
1633 | });
1634 |
1635 | it("should extract boxShadow from background element", () => {
1636 | const bgChild: SimplifiedNode = {
1637 | id: "bg-1",
1638 | name: "Background",
1639 | type: "RECTANGLE",
1640 | cssStyles: {
1641 | backgroundColor: "white",
1642 | boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
1643 | },
1644 | };
1645 |
1646 | const result = LayoutOptimizer.extractBackgroundStyles(bgChild);
1647 |
1648 | expect(result.boxShadow).toBe("0 2px 4px rgba(0,0,0,0.1)");
1649 | });
1650 |
1651 | it("should return empty object for element without styles", () => {
1652 | const bgChild: SimplifiedNode = {
1653 | id: "bg-1",
1654 | name: "Background",
1655 | type: "RECTANGLE",
1656 | };
1657 |
1658 | const result = LayoutOptimizer.extractBackgroundStyles(bgChild);
1659 |
1660 | expect(Object.keys(result)).toHaveLength(0);
1661 | });
1662 | });
1663 |
1664 | // ==================== isBackgroundElement Tests ====================
1665 | describe("isBackgroundElement", () => {
1666 | it("should return true for valid background element", () => {
1667 | const child: SimplifiedNode = {
1668 | id: "bg-1",
1669 | name: "Background",
1670 | type: "RECTANGLE",
1671 | cssStyles: {
1672 | backgroundColor: "white",
1673 | },
1674 | };
1675 |
1676 | expect(LayoutOptimizer.isBackgroundElement(0, 0, child)).toBe(true);
1677 | });
1678 |
1679 | it("should return false when index does not match", () => {
1680 | const child: SimplifiedNode = {
1681 | id: "bg-1",
1682 | name: "Background",
1683 | type: "RECTANGLE",
1684 | cssStyles: {
1685 | backgroundColor: "white",
1686 | },
1687 | };
1688 |
1689 | expect(LayoutOptimizer.isBackgroundElement(1, 0, child)).toBe(false);
1690 | });
1691 |
1692 | it("should return false for non-visual element types", () => {
1693 | const child: SimplifiedNode = {
1694 | id: "text-1",
1695 | name: "Text",
1696 | type: "TEXT",
1697 | cssStyles: {
1698 | backgroundColor: "white",
1699 | },
1700 | };
1701 |
1702 | expect(LayoutOptimizer.isBackgroundElement(0, 0, child)).toBe(false);
1703 | });
1704 |
1705 | it("should return false for element without visual styles", () => {
1706 | const child: SimplifiedNode = {
1707 | id: "bg-1",
1708 | name: "Background",
1709 | type: "RECTANGLE",
1710 | cssStyles: {
1711 | width: "100px",
1712 | height: "50px",
1713 | },
1714 | };
1715 |
1716 | expect(LayoutOptimizer.isBackgroundElement(0, 0, child)).toBe(false);
1717 | });
1718 | });
1719 | });
1720 |
```
--------------------------------------------------------------------------------
/tests/fixtures/figma-data/real-node-data.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "Vigilkids产品站",
3 | "lastModified": "2025-12-05T09:47:37Z",
4 | "thumbnailUrl": "https://s3-alpha.figma.com/thumbnails/9a38a8e4-5a00-4c07-a053-e71c7103a167?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAQ4GOSFWCXJW6HYPC%2F20251204%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251204T000000Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=dab594780137ae82433ff7205024276c20a3abb73855f2e393c7e6373086b892",
5 | "version": "2294025171571219775",
6 | "role": "owner",
7 | "editorType": "figma",
8 | "linkAccess": "inherit",
9 | "nodes": {
10 | "2:674": {
11 | "document": {
12 | "id": "2:674",
13 | "name": "Group 1410104851",
14 | "type": "GROUP",
15 | "scrollBehavior": "SCROLLS",
16 | "children": [
17 | {
18 | "id": "2:675",
19 | "name": "Rectangle 34",
20 | "type": "RECTANGLE",
21 | "locked": true,
22 | "scrollBehavior": "SCROLLS",
23 | "blendMode": "PASS_THROUGH",
24 | "fills": [
25 | {
26 | "blendMode": "NORMAL",
27 | "type": "SOLID",
28 | "color": {
29 | "r": 1,
30 | "g": 1,
31 | "b": 1,
32 | "a": 1
33 | }
34 | }
35 | ],
36 | "strokes": [],
37 | "strokeWeight": 1,
38 | "strokeAlign": "INSIDE",
39 | "cornerRadius": 12,
40 | "cornerSmoothing": 0,
41 | "absoluteBoundingBox": {
42 | "x": 406,
43 | "y": 422,
44 | "width": 1580,
45 | "height": 895
46 | },
47 | "absoluteRenderBounds": {
48 | "x": 406,
49 | "y": 422,
50 | "width": 1580,
51 | "height": 895
52 | },
53 | "constraints": {
54 | "vertical": "TOP",
55 | "horizontal": "LEFT"
56 | },
57 | "effects": [],
58 | "interactions": []
59 | },
60 | {
61 | "id": "2:676",
62 | "name": "Group 1410104850",
63 | "type": "GROUP",
64 | "scrollBehavior": "SCROLLS",
65 | "children": [
66 | {
67 | "id": "2:677",
68 | "name": "Group 1410104480",
69 | "type": "GROUP",
70 | "scrollBehavior": "SCROLLS",
71 | "children": [
72 | {
73 | "id": "2:678",
74 | "name": "添加自定义关键词,当检测到关键词出现时您将接收警报",
75 | "type": "TEXT",
76 | "scrollBehavior": "SCROLLS",
77 | "blendMode": "PASS_THROUGH",
78 | "fills": [
79 | {
80 | "blendMode": "NORMAL",
81 | "type": "SOLID",
82 | "color": {
83 | "r": 0.20000000298023224,
84 | "g": 0.20000000298023224,
85 | "b": 0.20000000298023224,
86 | "a": 1
87 | }
88 | }
89 | ],
90 | "strokes": [],
91 | "strokeWeight": 1,
92 | "strokeAlign": "OUTSIDE",
93 | "absoluteBoundingBox": {
94 | "x": 1021,
95 | "y": 822,
96 | "width": 350,
97 | "height": 20
98 | },
99 | "absoluteRenderBounds": {
100 | "x": 1021.5040283203125,
101 | "y": 825.3939819335938,
102 | "width": 349.033935546875,
103 | "height": 14.15399169921875
104 | },
105 | "constraints": {
106 | "vertical": "TOP",
107 | "horizontal": "LEFT"
108 | },
109 | "characters": "添加自定义关键词,当检测到关键词出现时您将接收警报",
110 | "characterStyleOverrides": [],
111 | "styleOverrideTable": {},
112 | "lineTypes": ["NONE"],
113 | "lineIndentations": [0],
114 | "style": {
115 | "fontFamily": "PingFang SC",
116 | "fontPostScriptName": "PingFangSC-Medium",
117 | "fontStyle": "Medium",
118 | "fontWeight": 500,
119 | "textAutoResize": "WIDTH_AND_HEIGHT",
120 | "fontSize": 14,
121 | "textAlignHorizontal": "CENTER",
122 | "textAlignVertical": "CENTER",
123 | "letterSpacing": 0,
124 | "lineHeightPx": 19.600000381469727,
125 | "lineHeightPercent": 100,
126 | "lineHeightUnit": "INTRINSIC_%"
127 | },
128 | "layoutVersion": 4,
129 | "effects": [],
130 | "interactions": []
131 | },
132 | {
133 | "id": "2:679",
134 | "name": "Group 1410104479",
135 | "type": "GROUP",
136 | "scrollBehavior": "SCROLLS",
137 | "children": [
138 | {
139 | "id": "2:680",
140 | "name": "Group 1410086131",
141 | "type": "GROUP",
142 | "scrollBehavior": "SCROLLS",
143 | "children": [
144 | {
145 | "id": "2:681",
146 | "name": "Rectangle 34625783",
147 | "type": "RECTANGLE",
148 | "scrollBehavior": "SCROLLS",
149 | "boundVariables": {
150 | "fills": [
151 | {
152 | "type": "VARIABLE_ALIAS",
153 | "id": "VariableID:926747ab970a552aee35a6ed34090251e61d5ed8/70:4"
154 | }
155 | ]
156 | },
157 | "blendMode": "PASS_THROUGH",
158 | "fills": [
159 | {
160 | "blendMode": "NORMAL",
161 | "type": "SOLID",
162 | "color": {
163 | "r": 0.1411764770746231,
164 | "g": 0.7803921699523926,
165 | "b": 0.5647059082984924,
166 | "a": 1
167 | },
168 | "boundVariables": {
169 | "color": {
170 | "type": "VARIABLE_ALIAS",
171 | "id": "VariableID:926747ab970a552aee35a6ed34090251e61d5ed8/70:4"
172 | }
173 | }
174 | }
175 | ],
176 | "strokes": [],
177 | "strokeWeight": 1,
178 | "strokeAlign": "INSIDE",
179 | "cornerRadius": 10,
180 | "cornerSmoothing": 0,
181 | "absoluteBoundingBox": {
182 | "x": 1076,
183 | "y": 872,
184 | "width": 240,
185 | "height": 44
186 | },
187 | "absoluteRenderBounds": {
188 | "x": 1076,
189 | "y": 872,
190 | "width": 240,
191 | "height": 44
192 | },
193 | "constraints": {
194 | "vertical": "TOP",
195 | "horizontal": "LEFT"
196 | },
197 | "effects": [],
198 | "interactions": []
199 | },
200 | {
201 | "id": "2:682",
202 | "name": "Group 1410104509",
203 | "type": "GROUP",
204 | "scrollBehavior": "SCROLLS",
205 | "children": [
206 | {
207 | "id": "2:683",
208 | "name": "AI生成关键词",
209 | "type": "TEXT",
210 | "scrollBehavior": "SCROLLS",
211 | "blendMode": "PASS_THROUGH",
212 | "fills": [
213 | {
214 | "blendMode": "NORMAL",
215 | "type": "SOLID",
216 | "color": {
217 | "r": 1,
218 | "g": 1,
219 | "b": 1,
220 | "a": 1
221 | }
222 | }
223 | ],
224 | "strokes": [],
225 | "strokeWeight": 1,
226 | "strokeAlign": "OUTSIDE",
227 | "absoluteBoundingBox": {
228 | "x": 1170,
229 | "y": 886,
230 | "width": 84,
231 | "height": 16
232 | },
233 | "absoluteRenderBounds": {
234 | "x": 1170.2939453125,
235 | "y": 886.9739990234375,
236 | "width": 82.4940185546875,
237 | "height": 13.4539794921875
238 | },
239 | "constraints": {
240 | "vertical": "TOP",
241 | "horizontal": "LEFT"
242 | },
243 | "characters": "AI生成关键词",
244 | "characterStyleOverrides": [],
245 | "styleOverrideTable": {},
246 | "lineTypes": ["NONE"],
247 | "lineIndentations": [0],
248 | "style": {
249 | "fontFamily": "Roboto",
250 | "fontPostScriptName": "Roboto-Bold",
251 | "fontStyle": "Bold",
252 | "fontWeight": 700,
253 | "textCase": "UPPER",
254 | "textAutoResize": "WIDTH_AND_HEIGHT",
255 | "fontSize": 14,
256 | "textAlignHorizontal": "CENTER",
257 | "textAlignVertical": "CENTER",
258 | "letterSpacing": 0,
259 | "lineHeightPx": 16.40625,
260 | "lineHeightPercent": 100,
261 | "lineHeightUnit": "INTRINSIC_%"
262 | },
263 | "layoutVersion": 4,
264 | "effects": [],
265 | "interactions": []
266 | },
267 | {
268 | "id": "2:684",
269 | "name": "Frame",
270 | "type": "FRAME",
271 | "scrollBehavior": "SCROLLS",
272 | "children": [
273 | {
274 | "id": "2:685",
275 | "name": "Vector",
276 | "type": "VECTOR",
277 | "scrollBehavior": "SCROLLS",
278 | "blendMode": "PASS_THROUGH",
279 | "fills": [
280 | {
281 | "blendMode": "NORMAL",
282 | "type": "SOLID",
283 | "color": {
284 | "r": 1,
285 | "g": 1,
286 | "b": 1,
287 | "a": 1
288 | }
289 | }
290 | ],
291 | "strokes": [],
292 | "strokeWeight": 0.1953125,
293 | "strokeAlign": "INSIDE",
294 | "absoluteBoundingBox": {
295 | "x": 1139.833740234375,
296 | "y": 884.83251953125,
297 | "width": 18.333126068115234,
298 | "height": 18.334999084472656
299 | },
300 | "absoluteRenderBounds": {
301 | "x": 1139.833740234375,
302 | "y": 884.83251953125,
303 | "width": 18.3331298828125,
304 | "height": 18.33502197265625
305 | },
306 | "constraints": {
307 | "vertical": "SCALE",
308 | "horizontal": "SCALE"
309 | },
310 | "effects": [],
311 | "interactions": []
312 | }
313 | ],
314 | "blendMode": "PASS_THROUGH",
315 | "clipsContent": true,
316 | "background": [
317 | {
318 | "blendMode": "NORMAL",
319 | "visible": false,
320 | "type": "SOLID",
321 | "color": {
322 | "r": 1,
323 | "g": 1,
324 | "b": 1,
325 | "a": 1
326 | }
327 | }
328 | ],
329 | "fills": [
330 | {
331 | "blendMode": "NORMAL",
332 | "visible": false,
333 | "type": "SOLID",
334 | "color": {
335 | "r": 1,
336 | "g": 1,
337 | "b": 1,
338 | "a": 1
339 | }
340 | }
341 | ],
342 | "strokes": [],
343 | "strokeWeight": 1,
344 | "strokeAlign": "INSIDE",
345 | "backgroundColor": {
346 | "r": 0,
347 | "g": 0,
348 | "b": 0,
349 | "a": 0
350 | },
351 | "absoluteBoundingBox": {
352 | "x": 1139,
353 | "y": 884,
354 | "width": 20,
355 | "height": 20
356 | },
357 | "absoluteRenderBounds": {
358 | "x": 1139,
359 | "y": 884,
360 | "width": 20,
361 | "height": 20
362 | },
363 | "constraints": {
364 | "vertical": "TOP",
365 | "horizontal": "LEFT"
366 | },
367 | "effects": [],
368 | "interactions": []
369 | }
370 | ],
371 | "blendMode": "PASS_THROUGH",
372 | "clipsContent": false,
373 | "background": [],
374 | "fills": [],
375 | "strokes": [],
376 | "strokeWeight": 1,
377 | "strokeAlign": "INSIDE",
378 | "backgroundColor": {
379 | "r": 0,
380 | "g": 0,
381 | "b": 0,
382 | "a": 0
383 | },
384 | "absoluteBoundingBox": {
385 | "x": 1139,
386 | "y": 884,
387 | "width": 115,
388 | "height": 20
389 | },
390 | "absoluteRenderBounds": {
391 | "x": 1139,
392 | "y": 884,
393 | "width": 115,
394 | "height": 20
395 | },
396 | "constraints": {
397 | "vertical": "TOP",
398 | "horizontal": "LEFT"
399 | },
400 | "effects": [],
401 | "interactions": []
402 | }
403 | ],
404 | "blendMode": "PASS_THROUGH",
405 | "clipsContent": false,
406 | "background": [],
407 | "fills": [],
408 | "strokes": [],
409 | "rectangleCornerRadii": [0, 0, 0, 0],
410 | "cornerSmoothing": 0,
411 | "strokeWeight": 1,
412 | "strokeAlign": "INSIDE",
413 | "backgroundColor": {
414 | "r": 0,
415 | "g": 0,
416 | "b": 0,
417 | "a": 0
418 | },
419 | "absoluteBoundingBox": {
420 | "x": 1076,
421 | "y": 872,
422 | "width": 240,
423 | "height": 44
424 | },
425 | "absoluteRenderBounds": {
426 | "x": 1076,
427 | "y": 872,
428 | "width": 240,
429 | "height": 44
430 | },
431 | "constraints": {
432 | "vertical": "TOP",
433 | "horizontal": "CENTER"
434 | },
435 | "effects": [],
436 | "interactions": []
437 | },
438 | {
439 | "id": "2:686",
440 | "name": "Group 1410104451",
441 | "type": "GROUP",
442 | "scrollBehavior": "SCROLLS",
443 | "children": [
444 | {
445 | "id": "2:687",
446 | "name": "Rectangle 34625783",
447 | "type": "RECTANGLE",
448 | "scrollBehavior": "SCROLLS",
449 | "blendMode": "PASS_THROUGH",
450 | "fills": [
451 | {
452 | "blendMode": "NORMAL",
453 | "type": "SOLID",
454 | "color": {
455 | "r": 1,
456 | "g": 1,
457 | "b": 1,
458 | "a": 1
459 | }
460 | }
461 | ],
462 | "strokes": [
463 | {
464 | "blendMode": "NORMAL",
465 | "type": "SOLID",
466 | "color": {
467 | "r": 0.7680059671401978,
468 | "g": 0.7680059671401978,
469 | "b": 0.7680059671401978,
470 | "a": 1
471 | }
472 | }
473 | ],
474 | "strokeWeight": 0,
475 | "strokeAlign": "INSIDE",
476 | "cornerRadius": 10,
477 | "cornerSmoothing": 0,
478 | "absoluteBoundingBox": {
479 | "x": 1076,
480 | "y": 926,
481 | "width": 240,
482 | "height": 44
483 | },
484 | "absoluteRenderBounds": {
485 | "x": 1076,
486 | "y": 926,
487 | "width": 240,
488 | "height": 44
489 | },
490 | "constraints": {
491 | "vertical": "TOP",
492 | "horizontal": "LEFT"
493 | },
494 | "effects": [],
495 | "interactions": []
496 | },
497 | {
498 | "id": "2:688",
499 | "name": "添加自定义关键词",
500 | "type": "TEXT",
501 | "scrollBehavior": "SCROLLS",
502 | "blendMode": "PASS_THROUGH",
503 | "fills": [
504 | {
505 | "blendMode": "NORMAL",
506 | "type": "SOLID",
507 | "color": {
508 | "r": 0.20000000298023224,
509 | "g": 0.20000000298023224,
510 | "b": 0.20000000298023224,
511 | "a": 1
512 | }
513 | }
514 | ],
515 | "strokes": [],
516 | "strokeWeight": 1,
517 | "strokeAlign": "OUTSIDE",
518 | "absoluteBoundingBox": {
519 | "x": 1123.594970703125,
520 | "y": 940,
521 | "width": 145.82278442382812,
522 | "height": 16
523 | },
524 | "absoluteRenderBounds": {
525 | "x": 1140.8983154296875,
526 | "y": 941.0579833984375,
527 | "width": 110.64208984375,
528 | "height": 13.342041015625
529 | },
530 | "constraints": {
531 | "vertical": "TOP",
532 | "horizontal": "LEFT"
533 | },
534 | "characters": "添加自定义关键词",
535 | "characterStyleOverrides": [],
536 | "styleOverrideTable": {},
537 | "lineTypes": ["NONE"],
538 | "lineIndentations": [0],
539 | "style": {
540 | "fontFamily": "Roboto",
541 | "fontPostScriptName": "Roboto-Bold",
542 | "fontStyle": "Bold",
543 | "fontWeight": 700,
544 | "textCase": "UPPER",
545 | "textAutoResize": "HEIGHT",
546 | "fontSize": 14,
547 | "textAlignHorizontal": "CENTER",
548 | "textAlignVertical": "CENTER",
549 | "letterSpacing": 0,
550 | "lineHeightPx": 16.40625,
551 | "lineHeightPercent": 100,
552 | "lineHeightUnit": "INTRINSIC_%"
553 | },
554 | "layoutVersion": 4,
555 | "effects": [],
556 | "interactions": []
557 | }
558 | ],
559 | "blendMode": "PASS_THROUGH",
560 | "clipsContent": false,
561 | "background": [],
562 | "fills": [],
563 | "strokes": [],
564 | "cornerRadius": 10,
565 | "cornerSmoothing": 0,
566 | "strokeWeight": 1,
567 | "strokeAlign": "INSIDE",
568 | "backgroundColor": {
569 | "r": 0,
570 | "g": 0,
571 | "b": 0,
572 | "a": 0
573 | },
574 | "absoluteBoundingBox": {
575 | "x": 1076,
576 | "y": 926,
577 | "width": 240,
578 | "height": 44
579 | },
580 | "absoluteRenderBounds": {
581 | "x": 1076,
582 | "y": 926,
583 | "width": 240,
584 | "height": 44
585 | },
586 | "constraints": {
587 | "vertical": "TOP",
588 | "horizontal": "LEFT"
589 | },
590 | "exportSettings": [
591 | {
592 | "suffix": "",
593 | "format": "PNG",
594 | "constraint": {
595 | "type": "SCALE",
596 | "value": 1
597 | }
598 | }
599 | ],
600 | "effects": [],
601 | "interactions": []
602 | }
603 | ],
604 | "blendMode": "PASS_THROUGH",
605 | "clipsContent": false,
606 | "background": [],
607 | "fills": [],
608 | "strokes": [],
609 | "rectangleCornerRadii": [0, 0, 0, 0],
610 | "cornerSmoothing": 0,
611 | "strokeWeight": 1,
612 | "strokeAlign": "INSIDE",
613 | "backgroundColor": {
614 | "r": 0,
615 | "g": 0,
616 | "b": 0,
617 | "a": 0
618 | },
619 | "absoluteBoundingBox": {
620 | "x": 1076,
621 | "y": 872,
622 | "width": 240,
623 | "height": 98
624 | },
625 | "absoluteRenderBounds": {
626 | "x": 1076,
627 | "y": 872,
628 | "width": 240,
629 | "height": 98
630 | },
631 | "constraints": {
632 | "vertical": "TOP",
633 | "horizontal": "LEFT"
634 | },
635 | "effects": [],
636 | "interactions": []
637 | }
638 | ],
639 | "blendMode": "PASS_THROUGH",
640 | "clipsContent": false,
641 | "background": [],
642 | "fills": [],
643 | "strokes": [],
644 | "rectangleCornerRadii": [0, 0, 0, 0],
645 | "cornerSmoothing": 0,
646 | "strokeWeight": 1,
647 | "strokeAlign": "INSIDE",
648 | "backgroundColor": {
649 | "r": 0,
650 | "g": 0,
651 | "b": 0,
652 | "a": 0
653 | },
654 | "absoluteBoundingBox": {
655 | "x": 1021,
656 | "y": 822,
657 | "width": 350,
658 | "height": 148
659 | },
660 | "absoluteRenderBounds": {
661 | "x": 1021,
662 | "y": 822,
663 | "width": 350,
664 | "height": 148
665 | },
666 | "constraints": {
667 | "vertical": "TOP",
668 | "horizontal": "LEFT"
669 | },
670 | "effects": [],
671 | "interactions": []
672 | },
673 | {
674 | "id": "2:689",
675 | "name": "Group 1410104849",
676 | "type": "GROUP",
677 | "scrollBehavior": "SCROLLS",
678 | "children": [
679 | {
680 | "id": "2:690",
681 | "name": "Rectangle 34626114",
682 | "type": "RECTANGLE",
683 | "scrollBehavior": "SCROLLS",
684 | "blendMode": "PASS_THROUGH",
685 | "fills": [
686 | {
687 | "opacity": 0,
688 | "blendMode": "NORMAL",
689 | "type": "SOLID",
690 | "color": {
691 | "r": 1,
692 | "g": 1,
693 | "b": 1,
694 | "a": 1
695 | }
696 | }
697 | ],
698 | "strokes": [],
699 | "strokeWeight": 1.100000023841858,
700 | "strokeAlign": "INSIDE",
701 | "absoluteBoundingBox": {
702 | "x": 1086,
703 | "y": 654,
704 | "width": 220,
705 | "height": 138
706 | },
707 | "absoluteRenderBounds": null,
708 | "constraints": {
709 | "vertical": "TOP",
710 | "horizontal": "LEFT"
711 | },
712 | "effects": [],
713 | "interactions": []
714 | },
715 | {
716 | "id": "2:691",
717 | "name": "Group 1410104848",
718 | "type": "GROUP",
719 | "scrollBehavior": "SCROLLS",
720 | "children": [
721 | {
722 | "id": "2:692",
723 | "name": "Ellipse 2571",
724 | "type": "ELLIPSE",
725 | "scrollBehavior": "SCROLLS",
726 | "blendMode": "PASS_THROUGH",
727 | "fills": [],
728 | "strokes": [
729 | {
730 | "opacity": 0.10000000149011612,
731 | "blendMode": "NORMAL",
732 | "type": "SOLID",
733 | "color": {
734 | "r": 0.1411764770746231,
735 | "g": 0.7803921699523926,
736 | "b": 0.5647059082984924,
737 | "a": 1
738 | }
739 | }
740 | ],
741 | "strokeWeight": 1.9917323589324951,
742 | "strokeAlign": "INSIDE",
743 | "absoluteBoundingBox": {
744 | "x": 1113.2984619140625,
745 | "y": 765.6846313476562,
746 | "width": 10.954526901245117,
747 | "height": 10.9071044921875
748 | },
749 | "absoluteRenderBounds": {
750 | "x": 1113.2984619140625,
751 | "y": 765.6846313476562,
752 | "width": 10.9544677734375,
753 | "height": 10.9071044921875
754 | },
755 | "constraints": {
756 | "vertical": "TOP",
757 | "horizontal": "LEFT"
758 | },
759 | "effects": [],
760 | "arcData": {
761 | "startingAngle": 0,
762 | "endingAngle": 6.2831854820251465,
763 | "innerRadius": 0
764 | },
765 | "interactions": []
766 | },
767 | {
768 | "id": "2:693",
769 | "name": "Rectangle 34626109",
770 | "type": "VECTOR",
771 | "scrollBehavior": "SCROLLS",
772 | "blendMode": "PASS_THROUGH",
773 | "fills": [
774 | {
775 | "blendMode": "NORMAL",
776 | "type": "GRADIENT_LINEAR",
777 | "gradientHandlePositions": [
778 | {
779 | "x": 0.0657894648366455,
780 | "y": 7.999539142211631e-9
781 | },
782 | {
783 | "x": 0.7894736608539367,
784 | "y": 0.999999973948599
785 | },
786 | {
787 | "x": -0.4342105609220982,
788 | "y": 0.3009599985319522
789 | }
790 | ],
791 | "gradientStops": [
792 | {
793 | "color": {
794 | "r": 0.29140377044677734,
795 | "g": 0.942671537399292,
796 | "b": 0.5844742059707642,
797 | "a": 1
798 | },
799 | "position": 0
800 | },
801 | {
802 | "color": {
803 | "r": 0,
804 | "g": 0.8191801309585571,
805 | "b": 0.5427696704864502,
806 | "a": 1
807 | },
808 | "position": 1
809 | }
810 | ]
811 | }
812 | ],
813 | "strokes": [],
814 | "strokeWeight": 0.3095206916332245,
815 | "strokeAlign": "CENTER",
816 | "cornerRadius": 18.921457290649414,
817 | "cornerSmoothing": 0,
818 | "absoluteBoundingBox": {
819 | "x": 1138.195068359375,
820 | "y": 661.5670166015625,
821 | "width": 113.52873992919922,
822 | "height": 123.94437408447266
823 | },
824 | "absoluteRenderBounds": {
825 | "x": 1129.1700439453125,
826 | "y": 651.6083374023438,
827 | "width": 131.5787353515625,
828 | "height": 143.8616943359375
829 | },
830 | "constraints": {
831 | "vertical": "TOP",
832 | "horizontal": "LEFT"
833 | },
834 | "effects": [
835 | {
836 | "type": "DROP_SHADOW",
837 | "visible": true,
838 | "color": {
839 | "r": 0,
840 | "g": 0.269947350025177,
841 | "b": 0.17840000987052917,
842 | "a": 0.09000000357627869
843 | },
844 | "blendMode": "NORMAL",
845 | "offset": {
846 | "x": 0,
847 | "y": 0
848 | },
849 | "radius": 9.958662033081055,
850 | "showShadowBehindNode": false
851 | }
852 | ],
853 | "interactions": []
854 | },
855 | {
856 | "id": "2:694",
857 | "name": "Rectangle 34626110",
858 | "type": "RECTANGLE",
859 | "scrollBehavior": "SCROLLS",
860 | "blendMode": "PASS_THROUGH",
861 | "fills": [
862 | {
863 | "blendMode": "NORMAL",
864 | "type": "GRADIENT_LINEAR",
865 | "gradientHandlePositions": [
866 | {
867 | "x": 0.5221519248834344,
868 | "y": 4.985427063369147e-14
869 | },
870 | {
871 | "x": 0.5221519248834481,
872 | "y": 1.00000000000005
873 | },
874 | {
875 | "x": 0.4947324413228972,
876 | "y": -8.056334654634439e-10
877 | }
878 | ],
879 | "gradientStops": [
880 | {
881 | "color": {
882 | "r": 0.0117647061124444,
883 | "g": 0.7882353067398071,
884 | "b": 0.4745098054409027,
885 | "a": 1
886 | },
887 | "position": 0
888 | },
889 | {
890 | "color": {
891 | "r": 0,
892 | "g": 0.7350029349327087,
893 | "b": 0.447717547416687,
894 | "a": 1
895 | },
896 | "position": 1
897 | }
898 | ]
899 | }
900 | ],
901 | "strokes": [],
902 | "strokeWeight": 0.9958661794662476,
903 | "strokeAlign": "INSIDE",
904 | "cornerRadius": 18.42352294921875,
905 | "cornerSmoothing": 0,
906 | "absoluteBoundingBox": {
907 | "x": 1116.2860107421875,
908 | "y": 728.0046997070312,
909 | "width": 157.3468475341797,
910 | "height": 36.68753433227539
911 | },
912 | "absoluteRenderBounds": {
913 | "x": 1116.2860107421875,
914 | "y": 728.0046997070312,
915 | "width": 157.3468017578125,
916 | "height": 36.68756103515625
917 | },
918 | "constraints": {
919 | "vertical": "TOP",
920 | "horizontal": "LEFT"
921 | },
922 | "effects": [],
923 | "interactions": []
924 | },
925 | {
926 | "id": "2:695",
927 | "name": "Vector 601",
928 | "type": "VECTOR",
929 | "scrollBehavior": "SCROLLS",
930 | "blendMode": "PASS_THROUGH",
931 | "fills": [],
932 | "fillOverrideTable": {
933 | "1": null,
934 | "2": null,
935 | "3": null
936 | },
937 | "strokes": [
938 | {
939 | "blendMode": "NORMAL",
940 | "type": "SOLID",
941 | "color": {
942 | "r": 0.6509804129600525,
943 | "g": 0.9764705896377563,
944 | "b": 0.7607843279838562,
945 | "a": 1
946 | }
947 | }
948 | ],
949 | "strokeWeight": 2.9875986576080322,
950 | "strokeAlign": "CENTER",
951 | "strokeJoin": "ROUND",
952 | "strokeCap": "ROUND",
953 | "absoluteBoundingBox": {
954 | "x": 1158.15234375,
955 | "y": 737.9147338867188,
956 | "width": 79.68075561523438,
957 | "height": 14.171746253967285
958 | },
959 | "absoluteRenderBounds": {
960 | "x": 1156.6585693359375,
961 | "y": 736.4205932617188,
962 | "width": 82.66845703125,
963 | "height": 17.15972900390625
964 | },
965 | "constraints": {
966 | "vertical": "TOP",
967 | "horizontal": "LEFT"
968 | },
969 | "effects": [],
970 | "interactions": []
971 | },
972 | {
973 | "id": "2:696",
974 | "name": "Frame",
975 | "type": "FRAME",
976 | "scrollBehavior": "SCROLLS",
977 | "children": [
978 | {
979 | "id": "2:697",
980 | "name": "Vector",
981 | "type": "VECTOR",
982 | "scrollBehavior": "SCROLLS",
983 | "blendMode": "PASS_THROUGH",
984 | "fills": [
985 | {
986 | "blendMode": "NORMAL",
987 | "type": "SOLID",
988 | "color": {
989 | "r": 0.02860725112259388,
990 | "g": 0.5845416188240051,
991 | "b": 0.3645462095737457,
992 | "a": 1
993 | }
994 | }
995 | ],
996 | "strokes": [],
997 | "strokeWeight": 0.19450511038303375,
998 | "strokeAlign": "INSIDE",
999 | "absoluteBoundingBox": {
1000 | "x": 1224.025146484375,
1001 | "y": 728.1633911132812,
1002 | "width": 37.4700813293457,
1003 | "height": 37.30668258666992
1004 | },
1005 | "absoluteRenderBounds": {
1006 | "x": 1224.025146484375,
1007 | "y": 728.1633911132812,
1008 | "width": 37.4700927734375,
1009 | "height": 37.30670166015625
1010 | },
1011 | "constraints": {
1012 | "vertical": "SCALE",
1013 | "horizontal": "SCALE"
1014 | },
1015 | "effects": [],
1016 | "interactions": []
1017 | },
1018 | {
1019 | "id": "2:698",
1020 | "name": "Vector",
1021 | "type": "VECTOR",
1022 | "scrollBehavior": "SCROLLS",
1023 | "blendMode": "PASS_THROUGH",
1024 | "fills": [
1025 | {
1026 | "blendMode": "NORMAL",
1027 | "type": "SOLID",
1028 | "color": {
1029 | "r": 0.02860725112259388,
1030 | "g": 0.5845416188240051,
1031 | "b": 0.3645462095737457,
1032 | "a": 1
1033 | }
1034 | }
1035 | ],
1036 | "strokes": [],
1037 | "strokeWeight": 0.19450511038303375,
1038 | "strokeAlign": "INSIDE",
1039 | "absoluteBoundingBox": {
1040 | "x": 1252.0159912109375,
1041 | "y": 756.0406494140625,
1042 | "width": 15.463837623596191,
1043 | "height": 15.395621299743652
1044 | },
1045 | "absoluteRenderBounds": {
1046 | "x": 1252.0159912109375,
1047 | "y": 756.0406494140625,
1048 | "width": 15.4638671875,
1049 | "height": 15.3956298828125
1050 | },
1051 | "constraints": {
1052 | "vertical": "SCALE",
1053 | "horizontal": "SCALE"
1054 | },
1055 | "effects": [],
1056 | "interactions": []
1057 | },
1058 | {
1059 | "id": "2:699",
1060 | "name": "Ellipse 2587",
1061 | "type": "ELLIPSE",
1062 | "scrollBehavior": "SCROLLS",
1063 | "blendMode": "PASS_THROUGH",
1064 | "fills": [
1065 | {
1066 | "opacity": 0.28999999165534973,
1067 | "blendMode": "NORMAL",
1068 | "type": "SOLID",
1069 | "color": {
1070 | "r": 1,
1071 | "g": 1,
1072 | "b": 1,
1073 | "a": 1
1074 | }
1075 | }
1076 | ],
1077 | "strokes": [],
1078 | "strokeWeight": 0.9958661794662476,
1079 | "strokeAlign": "INSIDE",
1080 | "absoluteBoundingBox": {
1081 | "x": 1227.822998046875,
1082 | "y": 731.9710693359375,
1083 | "width": 29.875986099243164,
1084 | "height": 29.746652603149414
1085 | },
1086 | "absoluteRenderBounds": {
1087 | "x": 1227.822998046875,
1088 | "y": 731.9710693359375,
1089 | "width": 29.8759765625,
1090 | "height": 29.74664306640625
1091 | },
1092 | "constraints": {
1093 | "vertical": "SCALE",
1094 | "horizontal": "SCALE"
1095 | },
1096 | "effects": [
1097 | {
1098 | "type": "BACKGROUND_BLUR",
1099 | "visible": true,
1100 | "radius": 4.0830512046813965
1101 | },
1102 | {
1103 | "type": "INNER_SHADOW",
1104 | "visible": true,
1105 | "color": {
1106 | "r": 1,
1107 | "g": 1,
1108 | "b": 1,
1109 | "a": 1
1110 | },
1111 | "blendMode": "NORMAL",
1112 | "offset": {
1113 | "x": 0,
1114 | "y": 0
1115 | },
1116 | "radius": 11.751220703125
1117 | }
1118 | ],
1119 | "arcData": {
1120 | "startingAngle": 0,
1121 | "endingAngle": 6.2831854820251465,
1122 | "innerRadius": 0
1123 | },
1124 | "interactions": []
1125 | }
1126 | ],
1127 | "blendMode": "PASS_THROUGH",
1128 | "clipsContent": true,
1129 | "background": [
1130 | {
1131 | "blendMode": "NORMAL",
1132 | "visible": false,
1133 | "type": "SOLID",
1134 | "color": {
1135 | "r": 1,
1136 | "g": 1,
1137 | "b": 1,
1138 | "a": 1
1139 | }
1140 | }
1141 | ],
1142 | "fills": [
1143 | {
1144 | "blendMode": "NORMAL",
1145 | "visible": false,
1146 | "type": "SOLID",
1147 | "color": {
1148 | "r": 1,
1149 | "g": 1,
1150 | "b": 1,
1151 | "a": 1
1152 | }
1153 | }
1154 | ],
1155 | "strokes": [],
1156 | "strokeWeight": 0.9958661794662476,
1157 | "strokeAlign": "INSIDE",
1158 | "backgroundColor": {
1159 | "r": 0,
1160 | "g": 0,
1161 | "b": 0,
1162 | "a": 0
1163 | },
1164 | "absoluteBoundingBox": {
1165 | "x": 1220.85205078125,
1166 | "y": 725.0332641601562,
1167 | "width": 49.79330825805664,
1168 | "height": 49.57775115966797
1169 | },
1170 | "absoluteRenderBounds": {
1171 | "x": 1220.85205078125,
1172 | "y": 725.0332641601562,
1173 | "width": 49.7933349609375,
1174 | "height": 49.5777587890625
1175 | },
1176 | "constraints": {
1177 | "vertical": "TOP",
1178 | "horizontal": "LEFT"
1179 | },
1180 | "effects": [],
1181 | "interactions": []
1182 | },
1183 | {
1184 | "id": "2:700",
1185 | "name": "Group 1410104846",
1186 | "type": "GROUP",
1187 | "scrollBehavior": "SCROLLS",
1188 | "children": [
1189 | {
1190 | "id": "2:701",
1191 | "name": "Ellipse 2583",
1192 | "type": "ELLIPSE",
1193 | "scrollBehavior": "SCROLLS",
1194 | "blendMode": "PASS_THROUGH",
1195 | "fills": [
1196 | {
1197 | "blendMode": "NORMAL",
1198 | "type": "SOLID",
1199 | "color": {
1200 | "r": 0.9934962391853333,
1201 | "g": 0.7477474212646484,
1202 | "b": 0.2174474149942398,
1203 | "a": 1
1204 | }
1205 | }
1206 | ],
1207 | "strokes": [],
1208 | "strokeWeight": 0.9958661794662476,
1209 | "strokeAlign": "INSIDE",
1210 | "absoluteBoundingBox": {
1211 | "x": 1124.2529296875,
1212 | "y": 734.9506225585938,
1213 | "width": 22.904922485351562,
1214 | "height": 22.80576515197754
1215 | },
1216 | "absoluteRenderBounds": {
1217 | "x": 1124.2529296875,
1218 | "y": 734.9506225585938,
1219 | "width": 22.9049072265625,
1220 | "height": 22.8057861328125
1221 | },
1222 | "constraints": {
1223 | "vertical": "TOP",
1224 | "horizontal": "LEFT"
1225 | },
1226 | "effects": [],
1227 | "arcData": {
1228 | "startingAngle": 0,
1229 | "endingAngle": 6.2831854820251465,
1230 | "innerRadius": 0
1231 | },
1232 | "interactions": []
1233 | },
1234 | {
1235 | "id": "2:702",
1236 | "name": "Ellipse 2584",
1237 | "type": "ELLIPSE",
1238 | "scrollBehavior": "SCROLLS",
1239 | "blendMode": "PASS_THROUGH",
1240 | "fills": [
1241 | {
1242 | "blendMode": "NORMAL",
1243 | "type": "SOLID",
1244 | "color": {
1245 | "r": 1,
1246 | "g": 1,
1247 | "b": 1,
1248 | "a": 1
1249 | }
1250 | }
1251 | ],
1252 | "strokes": [],
1253 | "strokeWeight": 0.9958661794662476,
1254 | "strokeAlign": "INSIDE",
1255 | "absoluteBoundingBox": {
1256 | "x": 1134.211669921875,
1257 | "y": 750.8113403320312,
1258 | "width": 2.987598419189453,
1259 | "height": 2.9746649265289307
1260 | },
1261 | "absoluteRenderBounds": {
1262 | "x": 1134.211669921875,
1263 | "y": 750.8113403320312,
1264 | "width": 2.987548828125,
1265 | "height": 2.97467041015625
1266 | },
1267 | "constraints": {
1268 | "vertical": "TOP",
1269 | "horizontal": "LEFT"
1270 | },
1271 | "effects": [],
1272 | "arcData": {
1273 | "startingAngle": 0,
1274 | "endingAngle": 6.2831854820251465,
1275 | "innerRadius": 0
1276 | },
1277 | "interactions": []
1278 | },
1279 | {
1280 | "id": "2:703",
1281 | "name": "Rectangle 34626111",
1282 | "type": "RECTANGLE",
1283 | "scrollBehavior": "SCROLLS",
1284 | "blendMode": "PASS_THROUGH",
1285 | "fills": [
1286 | {
1287 | "blendMode": "NORMAL",
1288 | "type": "SOLID",
1289 | "color": {
1290 | "r": 1,
1291 | "g": 1,
1292 | "b": 1,
1293 | "a": 1
1294 | }
1295 | }
1296 | ],
1297 | "strokes": [],
1298 | "strokeWeight": 0.9958661794662476,
1299 | "strokeAlign": "INSIDE",
1300 | "cornerRadius": 2.9875986576080322,
1301 | "cornerSmoothing": 0,
1302 | "absoluteBoundingBox": {
1303 | "x": 1134.211669921875,
1304 | "y": 738.9099731445312,
1305 | "width": 2.987598419189453,
1306 | "height": 9.915549278259277
1307 | },
1308 | "absoluteRenderBounds": {
1309 | "x": 1134.211669921875,
1310 | "y": 738.9099731445312,
1311 | "width": 2.987548828125,
1312 | "height": 9.91552734375
1313 | },
1314 | "constraints": {
1315 | "vertical": "TOP",
1316 | "horizontal": "LEFT"
1317 | },
1318 | "effects": [],
1319 | "interactions": []
1320 | }
1321 | ],
1322 | "blendMode": "PASS_THROUGH",
1323 | "clipsContent": false,
1324 | "background": [],
1325 | "fills": [],
1326 | "strokes": [],
1327 | "rectangleCornerRadii": [0, 0, 0, 0],
1328 | "cornerSmoothing": 0,
1329 | "strokeWeight": 0.9958661794662476,
1330 | "strokeAlign": "INSIDE",
1331 | "backgroundColor": {
1332 | "r": 0,
1333 | "g": 0,
1334 | "b": 0,
1335 | "a": 0
1336 | },
1337 | "absoluteBoundingBox": {
1338 | "x": 1124.2529296875,
1339 | "y": 734.9506225585938,
1340 | "width": 22.904922485351562,
1341 | "height": 22.80576515197754
1342 | },
1343 | "absoluteRenderBounds": {
1344 | "x": 1124.2529296875,
1345 | "y": 734.9506225585938,
1346 | "width": 22.904922485351562,
1347 | "height": 22.8057861328125
1348 | },
1349 | "constraints": {
1350 | "vertical": "TOP",
1351 | "horizontal": "LEFT"
1352 | },
1353 | "effects": [],
1354 | "interactions": []
1355 | },
1356 | {
1357 | "id": "2:704",
1358 | "name": "Ellipse 2585",
1359 | "type": "ELLIPSE",
1360 | "scrollBehavior": "SCROLLS",
1361 | "blendMode": "PASS_THROUGH",
1362 | "fills": [
1363 | {
1364 | "blendMode": "NORMAL",
1365 | "type": "SOLID",
1366 | "color": {
1367 | "r": 0.6039215922355652,
1368 | "g": 0.9490196108818054,
1369 | "b": 0.7333333492279053,
1370 | "a": 1
1371 | }
1372 | }
1373 | ],
1374 | "strokes": [],
1375 | "strokeWeight": 0.9958661794662476,
1376 | "strokeAlign": "INSIDE",
1377 | "absoluteBoundingBox": {
1378 | "x": 1160.1041259765625,
1379 | "y": 700.2426147460938,
1380 | "width": 7.9669294357299805,
1381 | "height": 7.932440280914307
1382 | },
1383 | "absoluteRenderBounds": {
1384 | "x": 1160.1041259765625,
1385 | "y": 700.2426147460938,
1386 | "width": 7.9669189453125,
1387 | "height": 7.93243408203125
1388 | },
1389 | "constraints": {
1390 | "vertical": "TOP",
1391 | "horizontal": "LEFT"
1392 | },
1393 | "effects": [],
1394 | "arcData": {
1395 | "startingAngle": 0,
1396 | "endingAngle": 6.2831854820251465,
1397 | "innerRadius": 0
1398 | },
1399 | "interactions": []
1400 | },
1401 | {
1402 | "id": "2:705",
1403 | "name": "Ellipse 2586",
1404 | "type": "ELLIPSE",
1405 | "scrollBehavior": "SCROLLS",
1406 | "blendMode": "PASS_THROUGH",
1407 | "fills": [
1408 | {
1409 | "blendMode": "NORMAL",
1410 | "type": "SOLID",
1411 | "color": {
1412 | "r": 0.6039215922355652,
1413 | "g": 0.9490196108818054,
1414 | "b": 0.7333333492279053,
1415 | "a": 1
1416 | }
1417 | }
1418 | ],
1419 | "strokes": [],
1420 | "strokeWeight": 0.9958661794662476,
1421 | "strokeAlign": "INSIDE",
1422 | "absoluteBoundingBox": {
1423 | "x": 1160.1041259765625,
1424 | "y": 712.13623046875,
1425 | "width": 7.9669294357299805,
1426 | "height": 7.932440280914307
1427 | },
1428 | "absoluteRenderBounds": {
1429 | "x": 1160.1041259765625,
1430 | "y": 712.13623046875,
1431 | "width": 7.9669189453125,
1432 | "height": 7.93243408203125
1433 | },
1434 | "constraints": {
1435 | "vertical": "TOP",
1436 | "horizontal": "LEFT"
1437 | },
1438 | "effects": [],
1439 | "arcData": {
1440 | "startingAngle": 0,
1441 | "endingAngle": 6.2831854820251465,
1442 | "innerRadius": 0
1443 | },
1444 | "interactions": []
1445 | },
1446 | {
1447 | "id": "2:706",
1448 | "name": "Rectangle 34626112",
1449 | "type": "RECTANGLE",
1450 | "scrollBehavior": "SCROLLS",
1451 | "blendMode": "PASS_THROUGH",
1452 | "fills": [
1453 | {
1454 | "blendMode": "NORMAL",
1455 | "type": "SOLID",
1456 | "color": {
1457 | "r": 0.6039215922355652,
1458 | "g": 0.9490196108818054,
1459 | "b": 0.7333333492279053,
1460 | "a": 1
1461 | }
1462 | }
1463 | ],
1464 | "strokes": [],
1465 | "strokeWeight": 0.9958661794662476,
1466 | "strokeAlign": "INSIDE",
1467 | "cornerRadius": 2.9875986576080322,
1468 | "cornerSmoothing": 0,
1469 | "absoluteBoundingBox": {
1470 | "x": 1172.0545654296875,
1471 | "y": 700.2426147460938,
1472 | "width": 61.74370193481445,
1473 | "height": 7.932440280914307
1474 | },
1475 | "absoluteRenderBounds": {
1476 | "x": 1172.0545654296875,
1477 | "y": 700.2426147460938,
1478 | "width": 61.74365234375,
1479 | "height": 7.93243408203125
1480 | },
1481 | "constraints": {
1482 | "vertical": "TOP",
1483 | "horizontal": "LEFT"
1484 | },
1485 | "effects": [],
1486 | "interactions": []
1487 | },
1488 | {
1489 | "id": "2:707",
1490 | "name": "Rectangle 34626113",
1491 | "type": "RECTANGLE",
1492 | "scrollBehavior": "SCROLLS",
1493 | "blendMode": "PASS_THROUGH",
1494 | "fills": [
1495 | {
1496 | "blendMode": "NORMAL",
1497 | "type": "SOLID",
1498 | "color": {
1499 | "r": 0.6039215922355652,
1500 | "g": 0.9490196108818054,
1501 | "b": 0.7333333492279053,
1502 | "a": 1
1503 | }
1504 | }
1505 | ],
1506 | "strokes": [],
1507 | "strokeWeight": 0.9958661794662476,
1508 | "strokeAlign": "INSIDE",
1509 | "cornerRadius": 2.9875986576080322,
1510 | "cornerSmoothing": 0,
1511 | "absoluteBoundingBox": {
1512 | "x": 1172.0545654296875,
1513 | "y": 712.13623046875,
1514 | "width": 48.79743957519531,
1515 | "height": 7.932440280914307
1516 | },
1517 | "absoluteRenderBounds": {
1518 | "x": 1172.0545654296875,
1519 | "y": 712.13623046875,
1520 | "width": 48.7974853515625,
1521 | "height": 7.93243408203125
1522 | },
1523 | "constraints": {
1524 | "vertical": "TOP",
1525 | "horizontal": "LEFT"
1526 | },
1527 | "effects": [],
1528 | "interactions": []
1529 | },
1530 | {
1531 | "id": "2:708",
1532 | "name": "Vector 600",
1533 | "type": "VECTOR",
1534 | "scrollBehavior": "SCROLLS",
1535 | "blendMode": "PASS_THROUGH",
1536 | "fills": [],
1537 | "fillOverrideTable": {
1538 | "1": null,
1539 | "2": null,
1540 | "3": null
1541 | },
1542 | "strokes": [
1543 | {
1544 | "blendMode": "NORMAL",
1545 | "type": "SOLID",
1546 | "color": {
1547 | "r": 0.6509804129600525,
1548 | "g": 0.9764705896377563,
1549 | "b": 0.7607843279838562,
1550 | "a": 1
1551 | }
1552 | }
1553 | ],
1554 | "strokeWeight": 2.9875986576080322,
1555 | "strokeAlign": "CENTER",
1556 | "strokeJoin": "ROUND",
1557 | "strokeCap": "ROUND",
1558 | "absoluteBoundingBox": {
1559 | "x": 1156.1207275390625,
1560 | "y": 677.427734375,
1561 | "width": 79.72064208984375,
1562 | "height": 11.296095848083496
1563 | },
1564 | "absoluteRenderBounds": {
1565 | "x": 1154.626953125,
1566 | "y": 675.93359375,
1567 | "width": 82.7083740234375,
1568 | "height": 14.2840576171875
1569 | },
1570 | "constraints": {
1571 | "vertical": "TOP",
1572 | "horizontal": "LEFT"
1573 | },
1574 | "effects": [],
1575 | "interactions": []
1576 | },
1577 | {
1578 | "id": "2:709",
1579 | "name": "Star 4",
1580 | "type": "STAR",
1581 | "scrollBehavior": "SCROLLS",
1582 | "blendMode": "PASS_THROUGH",
1583 | "fills": [
1584 | {
1585 | "opacity": 0.44999998807907104,
1586 | "blendMode": "NORMAL",
1587 | "type": "SOLID",
1588 | "color": {
1589 | "r": 0.26274511218070984,
1590 | "g": 0.929411768913269,
1591 | "b": 0.5803921818733215,
1592 | "a": 1
1593 | }
1594 | }
1595 | ],
1596 | "strokes": [],
1597 | "strokeWeight": 0.9958661794662476,
1598 | "strokeAlign": "INSIDE",
1599 | "cornerRadius": 0.9958661794662476,
1600 | "cornerSmoothing": 0,
1601 | "absoluteBoundingBox": {
1602 | "x": 1099.3563232421875,
1603 | "y": 677.427734375,
1604 | "width": 21.909053802490234,
1605 | "height": 21.814208984375
1606 | },
1607 | "absoluteRenderBounds": {
1608 | "x": 1101.794189453125,
1609 | "y": 679.8384399414062,
1610 | "width": 17.0333251953125,
1611 | "height": 16.9927978515625
1612 | },
1613 | "constraints": {
1614 | "vertical": "TOP",
1615 | "horizontal": "LEFT"
1616 | },
1617 | "effects": [],
1618 | "interactions": []
1619 | },
1620 | {
1621 | "id": "2:710",
1622 | "name": "Group 1410104431",
1623 | "type": "GROUP",
1624 | "scrollBehavior": "SCROLLS",
1625 | "rotation": 5.527084202842137e-17,
1626 | "children": [
1627 | {
1628 | "id": "2:711",
1629 | "name": "Union",
1630 | "type": "BOOLEAN_OPERATION",
1631 | "scrollBehavior": "SCROLLS",
1632 | "rotation": -1.2451086677321256e-24,
1633 | "children": [
1634 | {
1635 | "id": "2:712",
1636 | "name": "Vector 561 (Stroke)",
1637 | "type": "VECTOR",
1638 | "scrollBehavior": "SCROLLS",
1639 | "rotation": -1.2451086677321256e-24,
1640 | "blendMode": "PASS_THROUGH",
1641 | "fills": [
1642 | {
1643 | "opacity": 0.10000000149011612,
1644 | "blendMode": "NORMAL",
1645 | "type": "SOLID",
1646 | "color": {
1647 | "r": 0.1411764770746231,
1648 | "g": 0.7803921699523926,
1649 | "b": 0.5647059082984924,
1650 | "a": 1
1651 | }
1652 | }
1653 | ],
1654 | "strokes": [],
1655 | "strokeWeight": 1.9917323589324951,
1656 | "strokeAlign": "CENTER",
1657 | "strokeCap": "ROUND",
1658 | "absoluteBoundingBox": {
1659 | "x": 1252.719482421875,
1660 | "y": 779.5591430664062,
1661 | "width": 9.958661079406738,
1662 | "height": 1.9831100702285767
1663 | },
1664 | "absoluteRenderBounds": null,
1665 | "constraints": {
1666 | "vertical": "TOP",
1667 | "horizontal": "LEFT"
1668 | },
1669 | "effects": [],
1670 | "interactions": []
1671 | },
1672 | {
1673 | "id": "2:713",
1674 | "name": "Vector 562 (Stroke)",
1675 | "type": "VECTOR",
1676 | "scrollBehavior": "SCROLLS",
1677 | "rotation": 1.57079641145509,
1678 | "blendMode": "PASS_THROUGH",
1679 | "fills": [
1680 | {
1681 | "opacity": 0.10000000149011612,
1682 | "blendMode": "NORMAL",
1683 | "type": "SOLID",
1684 | "color": {
1685 | "r": 0.1411764770746231,
1686 | "g": 0.7803921699523926,
1687 | "b": 0.5647059082984924,
1688 | "a": 1
1689 | }
1690 | }
1691 | ],
1692 | "strokes": [],
1693 | "strokeWeight": 1.9917323589324951,
1694 | "strokeAlign": "CENTER",
1695 | "strokeCap": "ROUND",
1696 | "absoluteBoundingBox": {
1697 | "x": 1256.7027248094278,
1698 | "y": 775.5997923133051,
1699 | "width": 1.9917331983847362,
1700 | "height": 9.915549445422926
1701 | },
1702 | "absoluteRenderBounds": null,
1703 | "constraints": {
1704 | "vertical": "TOP",
1705 | "horizontal": "LEFT"
1706 | },
1707 | "effects": [],
1708 | "interactions": []
1709 | }
1710 | ],
1711 | "blendMode": "PASS_THROUGH",
1712 | "fills": [
1713 | {
1714 | "opacity": 0.20000000298023224,
1715 | "blendMode": "NORMAL",
1716 | "type": "SOLID",
1717 | "color": {
1718 | "r": 0.1411764770746231,
1719 | "g": 0.7803921699523926,
1720 | "b": 0.5647059082984924,
1721 | "a": 1
1722 | }
1723 | }
1724 | ],
1725 | "strokes": [],
1726 | "strokeWeight": 0,
1727 | "strokeAlign": "CENTER",
1728 | "strokeCap": "ROUND",
1729 | "booleanOperation": "UNION",
1730 | "absoluteBoundingBox": {
1731 | "x": 1252.7197265625,
1732 | "y": 775.6001586914062,
1733 | "width": 9.9580078125,
1734 | "height": 9.9150390625
1735 | },
1736 | "absoluteRenderBounds": {
1737 | "x": 1252.7197265625,
1738 | "y": 775.6001586914062,
1739 | "width": 9.9580078125,
1740 | "height": 9.9150390625
1741 | },
1742 | "constraints": {
1743 | "vertical": "TOP",
1744 | "horizontal": "LEFT"
1745 | },
1746 | "effects": [],
1747 | "interactions": []
1748 | }
1749 | ],
1750 | "blendMode": "PASS_THROUGH",
1751 | "clipsContent": false,
1752 | "background": [],
1753 | "fills": [],
1754 | "strokes": [],
1755 | "strokeWeight": 0.4979330897331238,
1756 | "strokeAlign": "INSIDE",
1757 | "backgroundColor": {
1758 | "r": 0,
1759 | "g": 0,
1760 | "b": 0,
1761 | "a": 0
1762 | },
1763 | "absoluteBoundingBox": {
1764 | "x": 1252.7197265625,
1765 | "y": 775.6001586914062,
1766 | "width": 9.9580078125,
1767 | "height": 9.9150390625
1768 | },
1769 | "absoluteRenderBounds": {
1770 | "x": 1252.7197265625,
1771 | "y": 775.6001586914062,
1772 | "width": 9.9580078125,
1773 | "height": 9.9150390625
1774 | },
1775 | "constraints": {
1776 | "vertical": "TOP",
1777 | "horizontal": "LEFT"
1778 | },
1779 | "effects": [],
1780 | "interactions": []
1781 | },
1782 | {
1783 | "id": "2:714",
1784 | "name": "Group 1410104847",
1785 | "type": "GROUP",
1786 | "scrollBehavior": "SCROLLS",
1787 | "rotation": 0.7832289476437421,
1788 | "children": [
1789 | {
1790 | "id": "2:715",
1791 | "name": "Union",
1792 | "type": "BOOLEAN_OPERATION",
1793 | "scrollBehavior": "SCROLLS",
1794 | "rotation": -2.591895864600957e-10,
1795 | "children": [
1796 | {
1797 | "id": "2:716",
1798 | "name": "Vector 561 (Stroke)",
1799 | "type": "VECTOR",
1800 | "scrollBehavior": "SCROLLS",
1801 | "rotation": -2.591895864600957e-10,
1802 | "blendMode": "PASS_THROUGH",
1803 | "fills": [
1804 | {
1805 | "opacity": 0.10000000149011612,
1806 | "blendMode": "NORMAL",
1807 | "type": "SOLID",
1808 | "color": {
1809 | "r": 0.1411764770746231,
1810 | "g": 0.7803921699523926,
1811 | "b": 0.5647059082984924,
1812 | "a": 1
1813 | }
1814 | }
1815 | ],
1816 | "strokes": [],
1817 | "strokeWeight": 1.9917323589324951,
1818 | "strokeAlign": "CENTER",
1819 | "strokeCap": "ROUND",
1820 | "absoluteBoundingBox": {
1821 | "x": 1280.4326970016364,
1822 | "y": 716.9205932617188,
1823 | "width": 8.450204286549706,
1824 | "height": 8.413622949463615
1825 | },
1826 | "absoluteRenderBounds": null,
1827 | "constraints": {
1828 | "vertical": "SCALE",
1829 | "horizontal": "SCALE"
1830 | },
1831 | "effects": [],
1832 | "interactions": []
1833 | },
1834 | {
1835 | "id": "2:717",
1836 | "name": "Vector 562 (Stroke)",
1837 | "type": "VECTOR",
1838 | "scrollBehavior": "SCROLLS",
1839 | "rotation": 1.570796411347784,
1840 | "blendMode": "PASS_THROUGH",
1841 | "fills": [
1842 | {
1843 | "opacity": 0.10000000149011612,
1844 | "blendMode": "NORMAL",
1845 | "type": "SOLID",
1846 | "color": {
1847 | "r": 0.1411764770746231,
1848 | "g": 0.7803921699523926,
1849 | "b": 0.5647059082984924,
1850 | "a": 1
1851 | }
1852 | }
1853 | ],
1854 | "strokes": [],
1855 | "strokeWeight": 1.9917323589324951,
1856 | "strokeAlign": "CENTER",
1857 | "strokeCap": "ROUND",
1858 | "absoluteBoundingBox": {
1859 | "x": 1280.4326077396113,
1860 | "y": 716.9262815659645,
1861 | "width": 8.450204760388715,
1862 | "height": 8.41362247562438
1863 | },
1864 | "absoluteRenderBounds": null,
1865 | "constraints": {
1866 | "vertical": "SCALE",
1867 | "horizontal": "SCALE"
1868 | },
1869 | "effects": [],
1870 | "interactions": []
1871 | }
1872 | ],
1873 | "blendMode": "PASS_THROUGH",
1874 | "fills": [
1875 | {
1876 | "opacity": 0.20000000298023224,
1877 | "blendMode": "NORMAL",
1878 | "type": "SOLID",
1879 | "color": {
1880 | "r": 0.1411764770746231,
1881 | "g": 0.7803921699523926,
1882 | "b": 0.5647059082984924,
1883 | "a": 1
1884 | }
1885 | }
1886 | ],
1887 | "strokes": [],
1888 | "strokeWeight": 0,
1889 | "strokeAlign": "CENTER",
1890 | "strokeCap": "ROUND",
1891 | "booleanOperation": "UNION",
1892 | "absoluteBoundingBox": {
1893 | "x": 1277.613173712045,
1894 | "y": 714.1190795898438,
1895 | "width": 14.083507420669775,
1896 | "height": 14.022539245837834
1897 | },
1898 | "absoluteRenderBounds": {
1899 | "x": 1280.8450927734375,
1900 | "y": 717.3314208984375,
1901 | "width": 7.625244140625,
1902 | "height": 7.59759521484375
1903 | },
1904 | "constraints": {
1905 | "vertical": "SCALE",
1906 | "horizontal": "SCALE"
1907 | },
1908 | "effects": [],
1909 | "interactions": []
1910 | }
1911 | ],
1912 | "blendMode": "PASS_THROUGH",
1913 | "clipsContent": false,
1914 | "background": [],
1915 | "fills": [],
1916 | "strokes": [],
1917 | "strokeWeight": 0.4979330897331238,
1918 | "strokeAlign": "INSIDE",
1919 | "backgroundColor": {
1920 | "r": 0,
1921 | "g": 0,
1922 | "b": 0,
1923 | "a": 0
1924 | },
1925 | "absoluteBoundingBox": {
1926 | "x": 1277.613173712045,
1927 | "y": 714.1190795898438,
1928 | "width": 14.083507420669775,
1929 | "height": 14.022539245837834
1930 | },
1931 | "absoluteRenderBounds": {
1932 | "x": 1277.613173712045,
1933 | "y": 714.1190795898438,
1934 | "width": 14.083507420669775,
1935 | "height": 14.022539245837834
1936 | },
1937 | "constraints": {
1938 | "vertical": "TOP",
1939 | "horizontal": "LEFT"
1940 | },
1941 | "effects": [],
1942 | "interactions": []
1943 | },
1944 | {
1945 | "id": "2:718",
1946 | "name": "Ellipse 2568",
1947 | "type": "ELLIPSE",
1948 | "scrollBehavior": "SCROLLS",
1949 | "blendMode": "PASS_THROUGH",
1950 | "fills": [],
1951 | "strokes": [
1952 | {
1953 | "opacity": 0.20000000298023224,
1954 | "blendMode": "NORMAL",
1955 | "type": "SOLID",
1956 | "color": {
1957 | "r": 0.1411764770746231,
1958 | "g": 0.7803921699523926,
1959 | "b": 0.5647059082984924,
1960 | "a": 1
1961 | }
1962 | }
1963 | ],
1964 | "strokeWeight": 1.9917323589324951,
1965 | "strokeAlign": "INSIDE",
1966 | "absoluteBoundingBox": {
1967 | "x": 1268.653564453125,
1968 | "y": 677.427734375,
1969 | "width": 10.954526901245117,
1970 | "height": 10.9071044921875
1971 | },
1972 | "absoluteRenderBounds": {
1973 | "x": 1268.653564453125,
1974 | "y": 677.427734375,
1975 | "width": 10.9544677734375,
1976 | "height": 10.9071044921875
1977 | },
1978 | "constraints": {
1979 | "vertical": "TOP",
1980 | "horizontal": "LEFT"
1981 | },
1982 | "effects": [],
1983 | "arcData": {
1984 | "startingAngle": 0,
1985 | "endingAngle": 6.2831854820251465,
1986 | "innerRadius": 0
1987 | },
1988 | "interactions": []
1989 | }
1990 | ],
1991 | "blendMode": "PASS_THROUGH",
1992 | "clipsContent": false,
1993 | "background": [],
1994 | "fills": [],
1995 | "strokes": [],
1996 | "rectangleCornerRadii": [0, 0, 0, 0],
1997 | "cornerSmoothing": 0,
1998 | "strokeWeight": 0.9958661794662476,
1999 | "strokeAlign": "INSIDE",
2000 | "backgroundColor": {
2001 | "r": 0,
2002 | "g": 0,
2003 | "b": 0,
2004 | "a": 0
2005 | },
2006 | "absoluteBoundingBox": {
2007 | "x": 1099.3563232421875,
2008 | "y": 661.5670166015625,
2009 | "width": 192.34036254882812,
2010 | "height": 123.94818115234375
2011 | },
2012 | "absoluteRenderBounds": {
2013 | "x": 1099.3563232421875,
2014 | "y": 651.6083374023438,
2015 | "width": 192.34036254882812,
2016 | "height": 143.8616943359375
2017 | },
2018 | "constraints": {
2019 | "vertical": "TOP",
2020 | "horizontal": "LEFT"
2021 | },
2022 | "effects": [],
2023 | "interactions": []
2024 | }
2025 | ],
2026 | "blendMode": "PASS_THROUGH",
2027 | "clipsContent": false,
2028 | "background": [],
2029 | "fills": [],
2030 | "strokes": [],
2031 | "rectangleCornerRadii": [0, 0, 0, 0],
2032 | "cornerSmoothing": 0,
2033 | "strokeWeight": 1.100000023841858,
2034 | "strokeAlign": "INSIDE",
2035 | "backgroundColor": {
2036 | "r": 0,
2037 | "g": 0,
2038 | "b": 0,
2039 | "a": 0
2040 | },
2041 | "absoluteBoundingBox": {
2042 | "x": 1086,
2043 | "y": 654,
2044 | "width": 220,
2045 | "height": 138
2046 | },
2047 | "absoluteRenderBounds": {
2048 | "x": 1086,
2049 | "y": 651.6083374023438,
2050 | "width": 220,
2051 | "height": 143.8616943359375
2052 | },
2053 | "constraints": {
2054 | "vertical": "TOP",
2055 | "horizontal": "LEFT"
2056 | },
2057 | "exportSettings": [
2058 | {
2059 | "suffix": "",
2060 | "format": "PNG",
2061 | "constraint": {
2062 | "type": "SCALE",
2063 | "value": 2
2064 | }
2065 | }
2066 | ],
2067 | "effects": [],
2068 | "interactions": []
2069 | }
2070 | ],
2071 | "blendMode": "PASS_THROUGH",
2072 | "clipsContent": false,
2073 | "background": [],
2074 | "fills": [],
2075 | "strokes": [],
2076 | "rectangleCornerRadii": [0, 0, 0, 0],
2077 | "cornerSmoothing": 0,
2078 | "strokeWeight": 1,
2079 | "strokeAlign": "INSIDE",
2080 | "backgroundColor": {
2081 | "r": 0,
2082 | "g": 0,
2083 | "b": 0,
2084 | "a": 0
2085 | },
2086 | "absoluteBoundingBox": {
2087 | "x": 1021,
2088 | "y": 654,
2089 | "width": 350,
2090 | "height": 316
2091 | },
2092 | "absoluteRenderBounds": {
2093 | "x": 1021,
2094 | "y": 651.6083374023438,
2095 | "width": 350,
2096 | "height": 318.39166259765625
2097 | },
2098 | "constraints": {
2099 | "vertical": "TOP",
2100 | "horizontal": "LEFT"
2101 | },
2102 | "exportSettings": [
2103 | {
2104 | "suffix": "",
2105 | "format": "PNG",
2106 | "constraint": {
2107 | "type": "SCALE",
2108 | "value": 1
2109 | }
2110 | }
2111 | ],
2112 | "effects": [],
2113 | "interactions": []
2114 | }
2115 | ],
2116 | "blendMode": "PASS_THROUGH",
2117 | "clipsContent": false,
2118 | "background": [],
2119 | "fills": [],
2120 | "strokes": [],
2121 | "rectangleCornerRadii": [0, 0, 0, 0],
2122 | "cornerSmoothing": 0,
2123 | "strokeWeight": 1,
2124 | "strokeAlign": "INSIDE",
2125 | "backgroundColor": {
2126 | "r": 0,
2127 | "g": 0,
2128 | "b": 0,
2129 | "a": 0
2130 | },
2131 | "absoluteBoundingBox": {
2132 | "x": 406,
2133 | "y": 422,
2134 | "width": 1580,
2135 | "height": 895
2136 | },
2137 | "absoluteRenderBounds": {
2138 | "x": 406,
2139 | "y": 422,
2140 | "width": 1580,
2141 | "height": 895
2142 | },
2143 | "constraints": {
2144 | "vertical": "TOP",
2145 | "horizontal": "LEFT"
2146 | },
2147 | "exportSettings": [
2148 | {
2149 | "suffix": "",
2150 | "format": "PNG",
2151 | "constraint": {
2152 | "type": "SCALE",
2153 | "value": 1
2154 | }
2155 | }
2156 | ],
2157 | "effects": [],
2158 | "interactions": []
2159 | },
2160 | "components": {},
2161 | "componentSets": {},
2162 | "schemaVersion": 0,
2163 | "styles": {}
2164 | }
2165 | }
2166 | }
2167 |
```