-
Notifications
You must be signed in to change notification settings - Fork 2.3k
feat: rectangle and ellipse shape annotations (#275) #499
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
Enriquefft
wants to merge
13
commits into
siddharthvaddem:main
Choose a base branch
from
Enriquefft:feat/shape-annotations-275
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
ff89098
feat: add kind discriminator to FigureData
Enriquefft 2716e65
feat: render rectangle in overlay and export
Enriquefft f609c4b
feat: render ellipse in overlay and export
Enriquefft c8c3fc2
feat: toolbar buttons for rectangle and ellipse
Enriquefft d768664
feat: inspector fill section for closed shapes
Enriquefft b0b5c8f
i18n: shape annotation strings across 6 locales
Enriquefft 9e114b1
refactor: make FigureData.kind optional per spec
Enriquefft cb9878a
feat: fill opacity slider with single-source hex utilities
Enriquefft 0c66445
i18n: opacity slider label across 6 locales
Enriquefft 9bd4261
fix: opacity slider preserves fill RGB instead of border color
Enriquefft 2bc3fc1
i18n: tighten shape annotation locale wording
Enriquefft 0347006
fix: validate persisted figure data and harden exhaustive fallbacks
Enriquefft e02807e
a11y: label fill toggle switch via aria-labelledby
Enriquefft File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| /** | ||
| * Hex-color utilities for the annotation Fill section. | ||
| * | ||
| * Fill is stored on `FigureData.fill` as a single canonical string in | ||
| * `#RRGGBBAA` form (8-digit, alpha required when fill is set). The opacity | ||
| * slider in the inspector is a *view* of the alpha byte; toggling fill off | ||
| * sets `figureData.fill = undefined`. There is no separate alpha field. | ||
| * | ||
| * All functions throw on malformed input — this module is internal code, not | ||
| * a user-input parser. Callers must not rely on silent fallbacks. | ||
| */ | ||
|
|
||
| /** | ||
| * Structured representation of a parsed hex color. | ||
| * | ||
| * `rgb` is always `#RRGGBB` (lowercase, with leading `#`). | ||
| * `alpha` is an integer in the closed range `[0, 255]`. When the input was a | ||
| * 6-digit hex (no alpha component), `alpha` defaults to `255` (fully opaque). | ||
| */ | ||
| export interface ParsedHexColor { | ||
| readonly rgb: string; | ||
| readonly alpha: number; | ||
| } | ||
|
|
||
| const HEX_COLOR_REGEX = /^#([0-9a-f]{6})([0-9a-f]{2})?$/i; | ||
|
|
||
| /** | ||
| * Parse a `#RRGGBB` or `#RRGGBBAA` hex color (case-insensitive) into its RGB | ||
| * and alpha components. Throws on malformed input. | ||
| */ | ||
| export function parseHexColor(hex: string): ParsedHexColor { | ||
| const match = HEX_COLOR_REGEX.exec(hex); | ||
| if (match === null) { | ||
| throw new Error(`Invalid hex color: ${hex}`); | ||
| } | ||
| const rgbHex = match[1].toLowerCase(); | ||
| const alphaHex = match[2]; | ||
| const alpha = alphaHex === undefined ? 255 : Number.parseInt(alphaHex, 16); | ||
| return { rgb: `#${rgbHex}`, alpha }; | ||
| } | ||
|
|
||
| /** | ||
| * Format an RGB hex (`#RRGGBB`) plus an integer alpha byte (0-255) into a | ||
| * canonical `#RRGGBBAA` string. Lowercase, alpha is zero-padded to 2 digits. | ||
| * | ||
| * Throws if `rgb` is not `#RRGGBB` or `alpha` is not an integer in [0, 255]. | ||
| */ | ||
| export function formatHexColor(rgb: string, alpha: number): string { | ||
| const match = HEX_COLOR_REGEX.exec(rgb); | ||
| if (match === null || match[2] !== undefined) { | ||
| throw new Error(`Invalid RGB hex (expected #RRGGBB): ${rgb}`); | ||
| } | ||
| if (!Number.isInteger(alpha) || alpha < 0 || alpha > 255) { | ||
| throw new Error(`Invalid alpha byte (expected integer 0-255): ${alpha}`); | ||
| } | ||
| const rgbLower = match[1].toLowerCase(); | ||
| const alphaHex = alpha.toString(16).padStart(2, "0"); | ||
| return `#${rgbLower}${alphaHex}`; | ||
| } | ||
|
|
||
| /** | ||
| * Convenience: take any `#RRGGBB` or `#RRGGBBAA` color, replace its alpha | ||
| * byte with the supplied one, and return the canonical `#RRGGBBAA` form. | ||
| * | ||
| * Throws if `color` is malformed or `alpha` is out of range. | ||
| */ | ||
| export function withAlpha(color: string, alpha: number): string { | ||
| const parsed = parseHexColor(color); | ||
| return formatHexColor(parsed.rgb, alpha); | ||
| } | ||
|
|
||
| /** | ||
| * Extract the alpha byte (0-255) from a `#RRGGBB` or `#RRGGBBAA` hex string. | ||
| * Throws on malformed input. A 6-digit hex resolves to `255`. | ||
| */ | ||
| export function getAlpha(fill: string): number { | ||
| return parseHexColor(fill).alpha; | ||
| } | ||
|
|
||
| /** | ||
| * Convert an alpha byte (0-255) to an integer percent (0-100). The result is | ||
| * rounded to the nearest int. Throws on non-finite or out-of-range input. | ||
| */ | ||
| export function alphaToPercent(alpha: number): number { | ||
| if (!Number.isFinite(alpha) || alpha < 0 || alpha > 255) { | ||
| throw new Error(`alphaToPercent: invalid alpha (expected finite 0-255): ${alpha}`); | ||
| } | ||
| return Math.round((alpha / 255) * 100); | ||
| } | ||
|
|
||
| /** | ||
| * Convert an integer percent (0-100) to an alpha byte (0-255). The result is | ||
| * rounded to the nearest int. Throws on non-finite or out-of-range input. | ||
| */ | ||
| export function percentToAlpha(percent: number): number { | ||
| if (!Number.isFinite(percent) || percent < 0 || percent > 100) { | ||
| throw new Error(`percentToAlpha: invalid percent (expected finite 0-100): ${percent}`); | ||
| } | ||
| return Math.round((percent / 100) * 255); | ||
| } | ||
|
|
||
| /** | ||
| * Cheap structural check for a `#RRGGBB` or `#RRGGBBAA` hex color string. | ||
| * Use this to gate untrusted input before passing to `parseHexColor` / | ||
| * `withAlpha` (which throw on malformed input). | ||
| */ | ||
| export function isValidHexColor(value: unknown): value is string { | ||
| return typeof value === "string" && HEX_COLOR_REGEX.test(value); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Closed shapes still inherit arrow-only controls.
This branch already knows when
kind !== "arrow", but rectangles/ellipses still show theArrow Directiongrid andArrow Colorlabel above it. that’s lowkey confusing: direction becomes a no-op, and the color label is wrong. Reuse the same kind check to hide arrow-only controls and rename the shared color control to something generic likeStroke/Border.🤖 Prompt for AI Agents