Download core/doc.py from FineEnvs/DesignGym: direct link, hf CLI and curl.
- Browser
- Download file 8.79 kB
-
https://huggingface.co/spaces/FineEnvs/DesignGym/resolve/main/core/doc.py
- Command line
-
hf download hf://spaces/FineEnvs/DesignGym/core/doc.py
-
curl -L -o doc.py https://huggingface.co/spaces/FineEnvs/DesignGym/resolve/main/core/doc.py
8.79 kB
| """The design document: the one piece of state every surface edits. | |
| A document is a canvas size, a background colour and a flat, back-to-front list | |
| of elements. That is the whole of Crello's model once its pre-rendered layers | |
| are treated as image assets, and it is deliberately all this environment | |
| supports: no groups, no constraints, no auto-layout. Every field here maps to a | |
| control in the editor and to a parameter of a tool, and nothing else exists. | |
| Three element kinds: | |
| - `image`: a placed asset (photo, illustration, decorative shape), stretched to | |
| its box. | |
| - `text`: a text box. Lines are explicit (`\\n`), never wrapped automatically, | |
| so the renderer and the browser cannot disagree about where a line breaks. | |
| Height is derived from the line count, like Figma's auto-height text. | |
| - `shape`: a rectangle or ellipse with fill and stroke. Crello has none of these | |
| (its shapes arrive as rendered assets); they exist so the editor has a shape | |
| tool, as every real editor does. | |
| """ | |
| from __future__ import annotations | |
| import hashlib | |
| import json | |
| from typing import Literal | |
| from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator | |
| from .colors import parse_color | |
| Kind = Literal["image", "text", "shape"] | |
| Align = Literal["left", "center", "right"] | |
| ShapeType = Literal["rect", "ellipse"] | |
| class Run(BaseModel): | |
| """A styled stretch of a text element. Runs concatenate to the element's text.""" | |
| model_config = ConfigDict(extra="forbid") | |
| text: str | |
| color: str | None = None | |
| bold: bool | None = None | |
| italic: bool | None = None | |
| def _color(cls, v): | |
| return None if v is None else parse_color(v) | |
| class Element(BaseModel): | |
| model_config = ConfigDict(extra="forbid", validate_assignment=True) | |
| id: str | |
| kind: Kind | |
| name: str = "" | |
| x: float = 0.0 | |
| y: float = 0.0 | |
| width: float = Field(default=100.0, gt=0) | |
| height: float = Field(default=100.0, gt=0) | |
| angle: float = 0.0 | |
| opacity: float = Field(default=1.0, ge=0.0, le=1.0) | |
| visible: bool = True | |
| locked: bool = False | |
| flip_x: bool = False | |
| flip_y: bool = False | |
| # image | |
| asset_id: str | None = None | |
| # shape | |
| shape: ShapeType | None = None | |
| fill: str | None = None | |
| stroke: str | None = None | |
| stroke_width: float = Field(default=0.0, ge=0.0) | |
| radius: float = Field(default=0.0, ge=0.0) | |
| # text | |
| text: str = "" | |
| font_family: str | None = None | |
| font_size: float = Field(default=48.0, gt=0) | |
| color: str | None = None | |
| bold: bool = False | |
| italic: bool = False | |
| underline: bool = False | |
| align: Align = "left" | |
| line_height: float = Field(default=1.0, gt=0) | |
| letter_spacing: float = 0.0 | |
| uppercase: bool = False | |
| auto_width: bool = False | |
| runs: list[Run] | None = None | |
| def _colors(cls, v): | |
| return None if v is None else parse_color(v) | |
| def _angle(cls, v): | |
| a = float(v) % 360.0 | |
| return a - 360.0 if a > 180.0 else a | |
| def _kind_fields(self): | |
| if self.kind == "image" and not self.asset_id: | |
| raise ValueError("an image element needs an asset_id") | |
| if self.kind == "shape" and self.shape is None: | |
| object.__setattr__(self, "shape", "rect") | |
| if self.kind == "text" and self.runs: | |
| joined = "".join(r.text for r in self.runs) | |
| if joined != self.text: | |
| object.__setattr__(self, "text", joined) | |
| return self | |
| # ---------------------------------------------------------- helpers | |
| def lines(self) -> list[str]: | |
| return self.text.split("\n") | |
| def display_text(self) -> str: | |
| return self.text.upper() if self.uppercase else self.text | |
| def styled_runs(self) -> list[Run]: | |
| """Runs with every style field filled from the element defaults.""" | |
| base_color = self.color or "#000000" | |
| if not self.runs: | |
| return [Run(text=self.text, color=base_color, bold=self.bold, italic=self.italic)] | |
| return [ | |
| Run( | |
| text=r.text, | |
| color=r.color or base_color, | |
| bold=self.bold if r.bold is None else r.bold, | |
| italic=self.italic if r.italic is None else r.italic, | |
| ) | |
| for r in self.runs | |
| ] | |
| def public(self) -> dict: | |
| """The fields that mean something for this kind, for tool output and HTML.""" | |
| common = ["id", "kind", "name", "x", "y", "width", "height", "angle", "opacity"] | |
| flags = ["visible", "locked", "flip_x", "flip_y"] | |
| per_kind = { | |
| "image": ["asset_id"], | |
| "shape": ["shape", "fill", "stroke", "stroke_width", "radius"], | |
| "text": [ | |
| "text", | |
| "font_family", | |
| "font_size", | |
| "color", | |
| "bold", | |
| "italic", | |
| "underline", | |
| "align", | |
| "line_height", | |
| "letter_spacing", | |
| "uppercase", | |
| "runs", | |
| ], | |
| }[self.kind] | |
| out = {} | |
| for key in common + per_kind: | |
| value = getattr(self, key) | |
| if key == "runs": | |
| if value: | |
| out[key] = [r.model_dump(exclude_none=True) for r in value] | |
| continue | |
| if isinstance(value, float): | |
| value = round(value, 2) | |
| out[key] = value | |
| for key in flags: | |
| value = getattr(self, key) | |
| default = key == "visible" | |
| if value != default: | |
| out[key] = value | |
| return out | |
| class DesignDoc(BaseModel): | |
| model_config = ConfigDict(extra="forbid") | |
| width: int = Field(gt=0, le=10000) | |
| height: int = Field(gt=0, le=10000) | |
| background: str = "#ffffff" | |
| elements: list[Element] = Field(default_factory=list) | |
| next_id: int = 1 | |
| def _bg(cls, v): | |
| return parse_color(v, "#ffffff") | |
| # ---------------------------------------------------------- helpers | |
| def find(self, element_id: str) -> Element: | |
| for el in self.elements: | |
| if el.id == element_id: | |
| return el | |
| known = ", ".join(e.id for e in self.elements) or "none" | |
| raise KeyError(f"no element {element_id!r} (elements: {known})") | |
| def index_of(self, element_id: str) -> int: | |
| for i, el in enumerate(self.elements): | |
| if el.id == element_id: | |
| return i | |
| raise KeyError(f"no element {element_id!r}") | |
| def new_id(self) -> str: | |
| eid = f"e{self.next_id}" | |
| self.next_id += 1 | |
| return eid | |
| def copy_deep(self) -> DesignDoc: | |
| return self.model_copy(deep=True) | |
| def fingerprint(self) -> str: | |
| """Stable hash of what the document looks like, for render caching.""" | |
| payload = self.model_dump(exclude={"next_id"}) | |
| blob = json.dumps(payload, sort_keys=True, separators=(",", ":")) | |
| return hashlib.sha1(blob.encode()).hexdigest()[:16] | |
| def public(self) -> dict: | |
| return { | |
| "width": self.width, | |
| "height": self.height, | |
| "background": self.background, | |
| "elements": [e.public() for e in self.elements], | |
| } | |
| def summary(self) -> str: | |
| """One line per element, the text a model reads after every edit.""" | |
| lines = [f"canvas {self.width}x{self.height}, background {self.background}"] | |
| if not self.elements: | |
| lines.append("(no elements yet)") | |
| for i, el in enumerate(self.elements): | |
| box = f"x={el.x:.0f} y={el.y:.0f} w={el.width:.0f} h={el.height:.0f}" | |
| extra = [] | |
| if el.angle: | |
| extra.append(f"angle={el.angle:.0f}") | |
| if el.opacity < 1: | |
| extra.append(f"opacity={el.opacity:.2f}") | |
| if not el.visible: | |
| extra.append("hidden") | |
| if el.kind == "image": | |
| what = f"image {el.asset_id}" | |
| elif el.kind == "shape": | |
| what = f"{el.shape} fill={el.fill}" | |
| else: | |
| snippet = el.text.replace("\n", "\\n") | |
| if len(snippet) > 40: | |
| snippet = snippet[:37] + "..." | |
| what = ( | |
| f'text "{snippet}" {el.font_family} {el.font_size:.0f}px {el.color} {el.align}' | |
| ) | |
| tail = (" " + " ".join(extra)) if extra else "" | |
| lines.append(f"[{i}] {el.id}: {what} @ {box}{tail}") | |
| return "\n".join(lines) | |