cudag 0.3.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. cudag/__init__.py +334 -0
  2. cudag/annotation/__init__.py +77 -0
  3. cudag/annotation/codegen.py +648 -0
  4. cudag/annotation/config.py +545 -0
  5. cudag/annotation/loader.py +342 -0
  6. cudag/annotation/scaffold.py +121 -0
  7. cudag/annotation/transcription.py +296 -0
  8. cudag/cli/__init__.py +5 -0
  9. cudag/cli/main.py +315 -0
  10. cudag/cli/new.py +873 -0
  11. cudag/core/__init__.py +364 -0
  12. cudag/core/button.py +137 -0
  13. cudag/core/canvas.py +222 -0
  14. cudag/core/config.py +70 -0
  15. cudag/core/coords.py +233 -0
  16. cudag/core/data_grid.py +804 -0
  17. cudag/core/dataset.py +678 -0
  18. cudag/core/distribution.py +136 -0
  19. cudag/core/drawing.py +75 -0
  20. cudag/core/fonts.py +156 -0
  21. cudag/core/generator.py +163 -0
  22. cudag/core/grid.py +367 -0
  23. cudag/core/grounding_task.py +247 -0
  24. cudag/core/icon.py +207 -0
  25. cudag/core/iconlist_task.py +301 -0
  26. cudag/core/models.py +1251 -0
  27. cudag/core/random.py +130 -0
  28. cudag/core/renderer.py +190 -0
  29. cudag/core/screen.py +402 -0
  30. cudag/core/scroll_task.py +254 -0
  31. cudag/core/scrollable_grid.py +447 -0
  32. cudag/core/state.py +110 -0
  33. cudag/core/task.py +293 -0
  34. cudag/core/taskbar.py +350 -0
  35. cudag/core/text.py +212 -0
  36. cudag/core/utils.py +82 -0
  37. cudag/data/surnames.txt +5000 -0
  38. cudag/modal_apps/__init__.py +4 -0
  39. cudag/modal_apps/archive.py +103 -0
  40. cudag/modal_apps/extract.py +138 -0
  41. cudag/modal_apps/preprocess.py +529 -0
  42. cudag/modal_apps/upload.py +317 -0
  43. cudag/prompts/SYSTEM_PROMPT.txt +104 -0
  44. cudag/prompts/__init__.py +33 -0
  45. cudag/prompts/system.py +43 -0
  46. cudag/prompts/tools.py +382 -0
  47. cudag/py.typed +0 -0
  48. cudag/schemas/filesystem.json +90 -0
  49. cudag/schemas/test_record.schema.json +113 -0
  50. cudag/schemas/train_record.schema.json +90 -0
  51. cudag/server/__init__.py +21 -0
  52. cudag/server/app.py +232 -0
  53. cudag/server/services/__init__.py +9 -0
  54. cudag/server/services/generator.py +128 -0
  55. cudag/templates/scripts/archive.sh +35 -0
  56. cudag/templates/scripts/build.sh +13 -0
  57. cudag/templates/scripts/extract.sh +54 -0
  58. cudag/templates/scripts/generate.sh +116 -0
  59. cudag/templates/scripts/pre-commit.sh +44 -0
  60. cudag/templates/scripts/preprocess.sh +46 -0
  61. cudag/templates/scripts/upload.sh +63 -0
  62. cudag/templates/scripts/verify.py +428 -0
  63. cudag/validation/__init__.py +35 -0
  64. cudag/validation/validate.py +508 -0
  65. cudag-0.3.10.dist-info/METADATA +570 -0
  66. cudag-0.3.10.dist-info/RECORD +69 -0
  67. cudag-0.3.10.dist-info/WHEEL +4 -0
  68. cudag-0.3.10.dist-info/entry_points.txt +2 -0
  69. cudag-0.3.10.dist-info/licenses/LICENSE +66 -0
cudag/core/__init__.py ADDED
@@ -0,0 +1,364 @@
1
+ # Copyright (c) 2025 Tylt LLC. All rights reserved.
2
+ # CONFIDENTIAL AND PROPRIETARY. Unauthorized use, copying, or distribution
3
+ # is strictly prohibited. For licensing inquiries: hello@claimhawk.app
4
+
5
+ """Core framework classes and DSL functions."""
6
+
7
+ from cudag.core.coords import (
8
+ RU_MAX,
9
+ bounds_to_tolerance,
10
+ calculate_tolerance_ru,
11
+ clamp_coord,
12
+ coord_distance,
13
+ coord_within_tolerance,
14
+ get_normalized_bounds,
15
+ normalize_coord,
16
+ pixel_from_normalized,
17
+ tolerance_to_ru,
18
+ )
19
+ from cudag.core.button import (
20
+ DIALOG_CANCEL,
21
+ DIALOG_OK,
22
+ LARGE_RECT,
23
+ LARGE_SQUARE,
24
+ MEDIUM_RECT,
25
+ MEDIUM_SQUARE,
26
+ NAV_BUTTON,
27
+ SMALL_RECT,
28
+ SMALL_SQUARE,
29
+ TOOLBAR_BUTTON,
30
+ ButtonPlacement,
31
+ ButtonShape,
32
+ ButtonSpec,
33
+ )
34
+ from cudag.core.canvas import CanvasConfig, RegionConfig
35
+ from cudag.core.grid import Grid, GridCell, GridGeometry
36
+ from cudag.core.scrollable_grid import (
37
+ ColumnDef,
38
+ RowLayout,
39
+ ScrollableGrid,
40
+ ScrollableGridGeometry,
41
+ ScrollState as GridScrollState,
42
+ )
43
+ from cudag.core.data_grid import (
44
+ ColumnDef as DataColumnDef,
45
+ Grid as DataGrid,
46
+ GridGeometry as DataGridGeometry,
47
+ RowLayout as DataRowLayout,
48
+ ScrollableGrid as DataScrollableGrid,
49
+ ScrollState as DataScrollState,
50
+ SelectableRowGrid,
51
+ SelectionState,
52
+ wrap_text as grid_wrap_text,
53
+ )
54
+ from cudag.core.icon import (
55
+ APP_ICON_LARGE,
56
+ APP_ICON_SMALL,
57
+ DESKTOP_ICON,
58
+ TASKBAR_ICON,
59
+ TOOLBAR_ICON,
60
+ IconLayout,
61
+ IconPlacement,
62
+ IconSpec,
63
+ )
64
+ from cudag.core.taskbar import TaskbarRenderer, TaskbarState
65
+ from cudag.core.dataset import DatasetBuilder, DatasetConfig
66
+ from cudag.core.distribution import DistributionSampler
67
+ from cudag.core.scroll_task import ScrollTaskBase, ScrollTaskConfig
68
+ from cudag.core.iconlist_task import IconListTaskBase, make_tool_call
69
+ from cudag.core.grounding_task import GroundingTaskBase, bbox_to_ru, scale_bbox
70
+ from cudag.core.models import (
71
+ # Classes
72
+ Attachment,
73
+ BelongsToRel,
74
+ BoolField,
75
+ ChoiceField,
76
+ Claim,
77
+ ComputedField,
78
+ DateField,
79
+ Field,
80
+ FloatField,
81
+ HasManyRel,
82
+ HasOneRel,
83
+ IntField,
84
+ ListField,
85
+ Model,
86
+ ModelGenerator,
87
+ MoneyField,
88
+ Patient,
89
+ Procedure,
90
+ Provider,
91
+ Relationship,
92
+ StringField,
93
+ TimeField,
94
+ # DSL functions
95
+ attribute,
96
+ belongs_to,
97
+ boolean,
98
+ choice,
99
+ computed,
100
+ date_field,
101
+ decimal,
102
+ get_first_name,
103
+ get_last_name,
104
+ has_many,
105
+ has_one,
106
+ integer,
107
+ list_of,
108
+ money,
109
+ string,
110
+ time_field,
111
+ years_since,
112
+ # Semantic field types
113
+ City,
114
+ ClaimNumber,
115
+ ClaimStatus,
116
+ DOB,
117
+ Email,
118
+ Fee,
119
+ FirstName,
120
+ FullName,
121
+ LastName,
122
+ LicenseNumber,
123
+ MemberID,
124
+ NPI,
125
+ Phone,
126
+ ProcedureCode,
127
+ SSN,
128
+ Specialty,
129
+ State,
130
+ Street,
131
+ ZipCode,
132
+ )
133
+ from cudag.core.renderer import BaseRenderer
134
+ from cudag.core.screen import (
135
+ # Classes
136
+ Bounds,
137
+ ButtonRegion,
138
+ ClickRegion,
139
+ DropdownRegion,
140
+ GridRegion,
141
+ Region,
142
+ Screen,
143
+ ScreenBase,
144
+ ScreenMeta,
145
+ ScrollRegion,
146
+ # DSL functions
147
+ button,
148
+ dropdown,
149
+ grid,
150
+ region,
151
+ scrollable,
152
+ )
153
+ from cudag.core.config import get_config_path, load_yaml_config
154
+ from cudag.core.drawing import render_scrollbar
155
+ from cudag.core.fonts import SYSTEM_FONTS, load_font, load_font_family
156
+ from cudag.core.generator import run_generator
157
+ from cudag.core.random import amount, choose, date_in_range, weighted_choice
158
+ from cudag.core.state import BaseState, ScrollState
159
+ from cudag.core.task import BaseTask, TaskContext, TaskSample, TestCase
160
+ from cudag.core.text import (
161
+ center_text_position,
162
+ draw_centered_text,
163
+ measure_text,
164
+ ordinal_suffix,
165
+ truncate_text,
166
+ wrap_text,
167
+ )
168
+ from cudag.core.utils import check_script_invocation, get_researcher_name
169
+
170
+ __all__ = [
171
+ # Coordinates
172
+ "RU_MAX",
173
+ "normalize_coord",
174
+ "pixel_from_normalized",
175
+ "get_normalized_bounds",
176
+ "clamp_coord",
177
+ "coord_distance",
178
+ "coord_within_tolerance",
179
+ "tolerance_to_ru",
180
+ "bounds_to_tolerance",
181
+ "calculate_tolerance_ru",
182
+ # Screen DSL - classes
183
+ "Screen",
184
+ "ScreenBase",
185
+ "ScreenMeta",
186
+ "Region",
187
+ "Bounds",
188
+ "ClickRegion",
189
+ "ButtonRegion",
190
+ "GridRegion",
191
+ "ScrollRegion",
192
+ "DropdownRegion",
193
+ # Screen DSL - functions
194
+ "region",
195
+ "button",
196
+ "grid",
197
+ "scrollable",
198
+ "dropdown",
199
+ # Canvas/Region
200
+ "CanvasConfig",
201
+ "RegionConfig",
202
+ # Grid
203
+ "Grid",
204
+ "GridCell",
205
+ "GridGeometry",
206
+ # Scrollable Grid (legacy)
207
+ "ScrollableGrid",
208
+ "ScrollableGridGeometry",
209
+ "ColumnDef",
210
+ "RowLayout",
211
+ "GridScrollState",
212
+ # Data Grid (composable)
213
+ "DataGrid",
214
+ "DataGridGeometry",
215
+ "DataColumnDef",
216
+ "DataRowLayout",
217
+ "DataScrollableGrid",
218
+ "DataScrollState",
219
+ "SelectableRowGrid",
220
+ "SelectionState",
221
+ "grid_wrap_text",
222
+ # Icons
223
+ "IconSpec",
224
+ "IconPlacement",
225
+ "IconLayout",
226
+ "DESKTOP_ICON",
227
+ "TASKBAR_ICON",
228
+ "TOOLBAR_ICON",
229
+ "APP_ICON_LARGE",
230
+ "APP_ICON_SMALL",
231
+ # Taskbar
232
+ "TaskbarState",
233
+ "TaskbarRenderer",
234
+ # Buttons
235
+ "ButtonSpec",
236
+ "ButtonPlacement",
237
+ "ButtonShape",
238
+ "SMALL_SQUARE",
239
+ "MEDIUM_SQUARE",
240
+ "LARGE_SQUARE",
241
+ "SMALL_RECT",
242
+ "MEDIUM_RECT",
243
+ "LARGE_RECT",
244
+ "NAV_BUTTON",
245
+ "TOOLBAR_BUTTON",
246
+ "DIALOG_OK",
247
+ "DIALOG_CANCEL",
248
+ # State
249
+ "BaseState",
250
+ "ScrollState",
251
+ # Renderer
252
+ "BaseRenderer",
253
+ # Task
254
+ "BaseTask",
255
+ "TaskSample",
256
+ "TaskContext",
257
+ "TestCase",
258
+ # Dataset
259
+ "DatasetBuilder",
260
+ "DatasetConfig",
261
+ # Distribution
262
+ "DistributionSampler",
263
+ # Scroll Tasks
264
+ "ScrollTaskBase",
265
+ "ScrollTaskConfig",
266
+ # IconList Tasks
267
+ "IconListTaskBase",
268
+ "make_tool_call",
269
+ # Grounding Tasks
270
+ "GroundingTaskBase",
271
+ "bbox_to_ru",
272
+ "scale_bbox",
273
+ # Model DSL - classes
274
+ "Model",
275
+ "ModelGenerator",
276
+ "Field",
277
+ "StringField",
278
+ "IntField",
279
+ "FloatField",
280
+ "BoolField",
281
+ "DateField",
282
+ "TimeField",
283
+ "ChoiceField",
284
+ "ListField",
285
+ "MoneyField",
286
+ "ComputedField",
287
+ # Model DSL - functions
288
+ "attribute",
289
+ "string",
290
+ "integer",
291
+ "decimal",
292
+ "money",
293
+ "date_field",
294
+ "time_field",
295
+ "boolean",
296
+ "choice",
297
+ "list_of",
298
+ "computed",
299
+ "years_since",
300
+ # Name generation functions
301
+ "get_first_name",
302
+ "get_last_name",
303
+ # Relationship DSL - classes
304
+ "Relationship",
305
+ "HasManyRel",
306
+ "BelongsToRel",
307
+ "HasOneRel",
308
+ # Relationship DSL - functions
309
+ "has_many",
310
+ "belongs_to",
311
+ "has_one",
312
+ # Common healthcare models
313
+ "Patient",
314
+ "Provider",
315
+ "Procedure",
316
+ "Claim",
317
+ "Attachment",
318
+ # Semantic field types
319
+ "FirstName",
320
+ "LastName",
321
+ "FullName",
322
+ "DOB",
323
+ "NPI",
324
+ "SSN",
325
+ "Phone",
326
+ "Email",
327
+ "Street",
328
+ "City",
329
+ "State",
330
+ "ZipCode",
331
+ "MemberID",
332
+ "ClaimNumber",
333
+ "ProcedureCode",
334
+ "LicenseNumber",
335
+ "Specialty",
336
+ "ClaimStatus",
337
+ "Fee",
338
+ # Utils
339
+ "check_script_invocation",
340
+ "get_researcher_name",
341
+ # Generator
342
+ "run_generator",
343
+ # Fonts
344
+ "load_font",
345
+ "load_font_family",
346
+ "SYSTEM_FONTS",
347
+ # Random utilities
348
+ "choose",
349
+ "date_in_range",
350
+ "amount",
351
+ "weighted_choice",
352
+ # Text utilities
353
+ "measure_text",
354
+ "center_text_position",
355
+ "draw_centered_text",
356
+ "wrap_text",
357
+ "truncate_text",
358
+ "ordinal_suffix",
359
+ # Drawing utilities
360
+ "render_scrollbar",
361
+ # Config utilities
362
+ "load_yaml_config",
363
+ "get_config_path",
364
+ ]
cudag/core/button.py ADDED
@@ -0,0 +1,137 @@
1
+ # Copyright (c) 2025 Tylt LLC. All rights reserved.
2
+ # CONFIDENTIAL AND PROPRIETARY. Unauthorized use, copying, or distribution
3
+ # is strictly prohibited. For licensing inquiries: hello@claimhawk.app
4
+
5
+ """Button abstractions for UI elements.
6
+
7
+ Provides reusable button definitions for different UI button types:
8
+ - Square buttons (equal width/height)
9
+ - Rectangular buttons (different width/height)
10
+ - Text buttons (with label)
11
+ - Icon buttons (with icon)
12
+
13
+ Each button type includes natural tolerance calculation (70% of dimensions).
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from dataclasses import dataclass
19
+ from enum import Enum
20
+
21
+
22
+ class ButtonShape(Enum):
23
+ """Button shape types."""
24
+
25
+ SQUARE = "square"
26
+ RECT = "rect"
27
+ PILL = "pill" # Rounded ends
28
+
29
+
30
+ @dataclass
31
+ class ButtonSpec:
32
+ """Specification for a button type.
33
+
34
+ Defines dimensions and characteristics for buttons in UIs.
35
+ Buttons can be square (equal dimensions) or rectangular.
36
+
37
+ Attributes:
38
+ width: Button width in pixels.
39
+ height: Button height in pixels.
40
+ shape: Button shape (square, rect, pill).
41
+ has_label: Whether button displays text label.
42
+ has_icon: Whether button displays an icon.
43
+ """
44
+
45
+ width: int
46
+ height: int
47
+ shape: ButtonShape = ButtonShape.RECT
48
+ has_label: bool = False
49
+ has_icon: bool = False
50
+
51
+ @property
52
+ def is_square(self) -> bool:
53
+ """Check if button is square."""
54
+ return self.width == self.height
55
+
56
+ @property
57
+ def tolerance_pixels(self) -> tuple[int, int]:
58
+ """Calculate natural click tolerance in pixels.
59
+
60
+ Returns 70% of dimensions (15% padding on each side).
61
+ """
62
+ return (int(self.width * 0.7), int(self.height * 0.7))
63
+
64
+ def tolerance_ru(self, image_size: tuple[int, int]) -> tuple[int, int]:
65
+ """Calculate natural tolerance in RU (Resolution Units).
66
+
67
+ Args:
68
+ image_size: (width, height) of the full image.
69
+
70
+ Returns:
71
+ Tuple of (x_tolerance_ru, y_tolerance_ru) in 0-1000 range.
72
+ """
73
+ tol_pixels = self.tolerance_pixels
74
+ x_ru = (tol_pixels[0] / image_size[0]) * 1000
75
+ y_ru = (tol_pixels[1] / image_size[1]) * 1000
76
+ return (int(x_ru), int(y_ru))
77
+
78
+
79
+ @dataclass
80
+ class ButtonPlacement:
81
+ """A button placed at a specific location.
82
+
83
+ Attributes:
84
+ spec: The button specification.
85
+ x: X position (left edge) in pixels.
86
+ y: Y position (top edge) in pixels.
87
+ label: Optional text label for the button.
88
+ description: Human-readable description of button action.
89
+ """
90
+
91
+ spec: ButtonSpec
92
+ x: int
93
+ y: int
94
+ label: str = ""
95
+ description: str = ""
96
+
97
+ @property
98
+ def bounds(self) -> tuple[int, int, int, int]:
99
+ """Get button bounds as (x, y, width, height)."""
100
+ return (self.x, self.y, self.spec.width, self.spec.height)
101
+
102
+ @property
103
+ def center(self) -> tuple[int, int]:
104
+ """Get button center coordinates."""
105
+ return (
106
+ self.x + self.spec.width // 2,
107
+ self.y + self.spec.height // 2,
108
+ )
109
+
110
+ @property
111
+ def tolerance_pixels(self) -> tuple[int, int]:
112
+ """Get natural tolerance in pixels."""
113
+ return self.spec.tolerance_pixels
114
+
115
+ def tolerance_ru(self, image_size: tuple[int, int]) -> tuple[int, int]:
116
+ """Get natural tolerance in RU units."""
117
+ return self.spec.tolerance_ru(image_size)
118
+
119
+
120
+ # Common button presets
121
+ SMALL_SQUARE = ButtonSpec(width=16, height=16, shape=ButtonShape.SQUARE, has_icon=True)
122
+ MEDIUM_SQUARE = ButtonSpec(width=24, height=24, shape=ButtonShape.SQUARE, has_icon=True)
123
+ LARGE_SQUARE = ButtonSpec(width=32, height=32, shape=ButtonShape.SQUARE, has_icon=True)
124
+
125
+ SMALL_RECT = ButtonSpec(width=60, height=24, shape=ButtonShape.RECT, has_label=True)
126
+ MEDIUM_RECT = ButtonSpec(width=80, height=28, shape=ButtonShape.RECT, has_label=True)
127
+ LARGE_RECT = ButtonSpec(width=120, height=32, shape=ButtonShape.RECT, has_label=True)
128
+
129
+ # Navigation buttons (like calendar back/forward)
130
+ NAV_BUTTON = ButtonSpec(width=20, height=12, shape=ButtonShape.RECT, has_icon=True)
131
+
132
+ # Toolbar buttons
133
+ TOOLBAR_BUTTON = ButtonSpec(width=24, height=24, shape=ButtonShape.SQUARE, has_icon=True)
134
+
135
+ # Dialog buttons
136
+ DIALOG_OK = ButtonSpec(width=75, height=23, shape=ButtonShape.RECT, has_label=True)
137
+ DIALOG_CANCEL = ButtonSpec(width=75, height=23, shape=ButtonShape.RECT, has_label=True)
cudag/core/canvas.py ADDED
@@ -0,0 +1,222 @@
1
+ # Copyright (c) 2025 Tylt LLC. All rights reserved.
2
+ # CONFIDENTIAL AND PROPRIETARY. Unauthorized use, copying, or distribution
3
+ # is strictly prohibited. For licensing inquiries: hello@claimhawk.app
4
+
5
+ """Canvas and Region abstractions for screen composition.
6
+
7
+ Provides a declarative way to define screenshot layouts:
8
+ - Canvas: full screenshot dimensions with base blank image
9
+ - Region: subsection of canvas with optional overlay and generator
10
+
11
+ Regions can have associated generators for:
12
+ - Grids (calendars, data tables)
13
+ - Icons (desktop icons, toolbars)
14
+ - Content (text, form fields)
15
+
16
+ Supports loading from YAML/JSON configuration.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from dataclasses import dataclass, field
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ import yaml
26
+
27
+
28
+ @dataclass
29
+ class RegionConfig:
30
+ """Configuration for a region within a canvas."""
31
+
32
+ name: str
33
+ """Unique region identifier."""
34
+
35
+ bounds: tuple[int, int, int, int]
36
+ """Region bounds as (x, y, width, height)."""
37
+
38
+ z_index: int = 0
39
+ """Layer order (higher = on top)."""
40
+
41
+ blank_image: str | Path = ""
42
+ """Optional overlay/blank image for this region."""
43
+
44
+ generator_type: str = ""
45
+ """Generator type: 'grid', 'icons', 'content', etc."""
46
+
47
+ generator_config: dict[str, Any] = field(default_factory=dict)
48
+ """Configuration for the generator."""
49
+
50
+ @property
51
+ def x(self) -> int:
52
+ return self.bounds[0]
53
+
54
+ @property
55
+ def y(self) -> int:
56
+ return self.bounds[1]
57
+
58
+ @property
59
+ def width(self) -> int:
60
+ return self.bounds[2]
61
+
62
+ @property
63
+ def height(self) -> int:
64
+ return self.bounds[3]
65
+
66
+ @property
67
+ def center(self) -> tuple[int, int]:
68
+ return (self.x + self.width // 2, self.y + self.height // 2)
69
+
70
+ def tolerance_ru(self, image_size: tuple[int, int]) -> tuple[int, int]:
71
+ """Get natural tolerance in RU units based on region size."""
72
+ x_ru = (self.width * 0.7 / image_size[0]) * 1000
73
+ y_ru = (self.height * 0.7 / image_size[1]) * 1000
74
+ return (int(x_ru), int(y_ru))
75
+
76
+
77
+ @dataclass
78
+ class CanvasConfig:
79
+ """Configuration for a full screenshot canvas."""
80
+
81
+ name: str
82
+ """Canvas identifier."""
83
+
84
+ size: tuple[int, int]
85
+ """Canvas dimensions as (width, height)."""
86
+
87
+ blank_image: str | Path
88
+ """Base blank image for the canvas."""
89
+
90
+ regions: list[RegionConfig] = field(default_factory=list)
91
+ """Regions on this canvas."""
92
+
93
+ task_types: list[str] = field(default_factory=list)
94
+ """Supported task types for this canvas."""
95
+
96
+ def get_region(self, name: str) -> RegionConfig | None:
97
+ """Get a region by name."""
98
+ for region in self.regions:
99
+ if region.name == name:
100
+ return region
101
+ return None
102
+
103
+ def regions_by_z(self) -> list[RegionConfig]:
104
+ """Get regions sorted by z-index (bottom to top)."""
105
+ return sorted(self.regions, key=lambda r: r.z_index)
106
+
107
+ @classmethod
108
+ def from_yaml(cls, path: Path) -> CanvasConfig:
109
+ """Load canvas configuration from YAML file."""
110
+ with open(path) as f:
111
+ data = yaml.safe_load(f)
112
+ return cls.from_dict(data)
113
+
114
+ @classmethod
115
+ def from_dict(cls, data: dict[str, Any]) -> CanvasConfig:
116
+ """Create from dictionary."""
117
+ regions = []
118
+ for r in data.get("regions", []):
119
+ regions.append(
120
+ RegionConfig(
121
+ name=r["name"],
122
+ bounds=tuple(r["bounds"]),
123
+ z_index=r.get("z_index", 0),
124
+ blank_image=r.get("blank_image", ""),
125
+ generator_type=r.get("generator", ""),
126
+ generator_config=r.get("config", {}),
127
+ )
128
+ )
129
+
130
+ return cls(
131
+ name=data["name"],
132
+ size=tuple(data["size"]),
133
+ blank_image=data["blank_image"],
134
+ regions=regions,
135
+ task_types=data.get("task_types", []),
136
+ )
137
+
138
+ def to_dict(self) -> dict[str, Any]:
139
+ """Convert to dictionary for serialization."""
140
+ return {
141
+ "name": self.name,
142
+ "size": list(self.size),
143
+ "blank_image": str(self.blank_image),
144
+ "task_types": self.task_types,
145
+ "regions": [
146
+ {
147
+ "name": r.name,
148
+ "bounds": list(r.bounds),
149
+ "z_index": r.z_index,
150
+ "blank_image": str(r.blank_image) if r.blank_image else "",
151
+ "generator": r.generator_type,
152
+ "config": r.generator_config,
153
+ }
154
+ for r in self.regions
155
+ ],
156
+ }
157
+
158
+
159
+ # Example YAML format:
160
+ """
161
+ # canvas.yaml
162
+ name: desktop
163
+ size: [1920, 1080]
164
+ blank_image: assets/blanks/desktop-blank.png
165
+ task_types:
166
+ - click-desktop-icon
167
+ - click-taskbar-icon
168
+
169
+ regions:
170
+ - name: desktop_area
171
+ bounds: [0, 0, 1920, 1032]
172
+ z_index: 0
173
+ generator: icons
174
+ config:
175
+ icon_type: desktop
176
+ layout: grid
177
+ cols: 1
178
+ padding: 20
179
+
180
+ - name: taskbar
181
+ bounds: [0, 1032, 1920, 48]
182
+ z_index: 1
183
+ blank_image: assets/blanks/taskbar.png
184
+ generator: icons
185
+ config:
186
+ icon_type: taskbar
187
+ layout: horizontal
188
+ start_x: 946
189
+ padding: 8
190
+
191
+ # calendar.yaml
192
+ name: calendar
193
+ size: [224, 208]
194
+ blank_image: assets/blanks/calendar-blank.png
195
+ task_types:
196
+ - click-day
197
+ - click-back-month
198
+ - click-forward-month
199
+
200
+ regions:
201
+ - name: day_grid
202
+ bounds: [2, 72, 219, 90]
203
+ generator: grid
204
+ config:
205
+ rows: 6
206
+ cols: 7
207
+ cell_width: 24
208
+ cell_height: 15
209
+ col_gap: 8
210
+
211
+ - name: back_button
212
+ bounds: [7, 192, 20, 12]
213
+ generator: button
214
+ config:
215
+ label: "Back Month"
216
+
217
+ - name: forward_button
218
+ bounds: [197, 192, 20, 12]
219
+ generator: button
220
+ config:
221
+ label: "Forward Month"
222
+ """