Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 1 addition & 11 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,7 @@ Documentation files are located in the `./docs` directory.

This directory contains the docs as-is, the deployment of the docs are handled by [apps/docs](./apps/docs). A docusaurus project that syncs the docs content to its directory. When writing docs, the root `./docs` directory is the source of truth.

| directory | name | description | active |
| ------------------------------------- | ------------- | ---------------------------------------------------------------- | ------ |
| [/docs/wg](./docs/wg) | working group | working group documents, architecture documents, todo list, etc | yes |
| [/docs/reference](./docs/reference) | reference | glossary and references (technical documents) | yes |
| [/docs/math](./docs/math) | math | Math reference, used for internal docs referencing | yes |
| [/docs/platform](./docs/platform) | platform | Grida Platform (API/Spec) documents | yes |
| [/docs/editor](./docs/editor) | editor | Grida Editor - User Documentation | yes |
| [/docs/canvas](./docs/canvas) | canvas | Grida Canvas SDK - User Documentation | no |
| [/docs/cli](./docs/cli) | cli | Grida CLI - User Documentation | yes |
| [/docs/together](./docs/together) | together | Contributing, Support, Community, etc | yes |
| [/docs/with-figma](./docs/with-figma) | with-figma | Grida with Figma - Grida <-> Figma compatilibity and user guides | yes |
See [`docs/AGENTS.md`](./docs/AGENTS.md) for the docs contribution scope (we only actively maintain `docs/wg/**` and `docs/reference/**`).

Comment thread
softmarshmallow marked this conversation as resolved.
## `/crates/*`

Expand Down
35 changes: 35 additions & 0 deletions docs/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Docs agent guide (`/docs`)

This directory is the **source of truth** for documentation content.

- **Source**: `/docs/**` (edit here)
- **Site build copy**: `/apps/docs/docs/**` (generated/synced during docs site build)
- **Published at**: `https://grida.co/docs`

## Actively maintained

We **only actively maintain** the following docs areas:

- `docs/wg/**` — working group docs (design notes, architecture, proposals, WIP)
- `docs/reference/**` — reference docs (glossary, specs, stable technical references)

## Everything else

Other folders under `/docs` are **not actively managed**.

- Unless you have a specific task, **avoid editing** content outside `docs/wg/**` and `docs/reference/**`.
- Do not edit generated artifacts under `/apps/docs/docs/**`.

## Structure

| directory | name | description | active |
| -------------------------------- | ------------- | ---------------------------------------------------------------------- | ------ |
| [/docs/wg](./wg) | working group | working group documents, architecture documents, todo list, etc | yes |
| [/docs/reference](./reference) | reference | glossary and references (technical documents) | yes |
| [/docs/math](./math) | math | Math reference, used for internal docs referencing | yes |
| [/docs/platform](./platform) | platform | Grida Platform (API/Spec) documents | yes |
| [/docs/editor](./editor) | editor | Grida Editor - User Documentation | yes |
| [/docs/canvas](./canvas) | canvas | Grida Canvas SDK - User Documentation | no |
| [/docs/cli](./cli) | cli | Grida CLI - User Documentation | yes |
| [/docs/together](./together) | together | Contributing, Support, Community, etc | yes |
| [/docs/with-figma](./with-figma) | with-figma | Grida with Figma - Grida &lt;-&gt; Figma compatibility and user guides | yes |
Comment thread
softmarshmallow marked this conversation as resolved.
40 changes: 21 additions & 19 deletions docs/wg/feat-authoring/parametric-scaling.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,19 @@
title: Scale tool (K) — parameter-space scaling (A.k.a Apply Scale or K-Scale)
---

| feature id | status | description | PRs |
| -------------------- | -------- | -------------------------------------------- | --- |
| `parametric-scaling` | proposed | Parameter-space scaling operation for Grida. | - |
| feature id | status | description | PRs |
| -------------------- | -------- | -------------------------------------------- | ------------------------------------------------- |
| `parametric-scaling` | proposed | Parameter-space scaling operation for Grida. | [#471](https://github.com/gridaco/grida/pull/471) |

## Key principles

- **Visual accuracy over “clean” values**: Scale (K) is an authoring-time operation whose primary goal is that the post-scale render is visually consistent with a uniform similarity transform. As a result, it is normal (and expected) for authored values to become fractional / “dirty” after repeated scaling.

- **No extra quantization / optimization in the core rewrite**: The core scaling rules should apply the exact factor $s$ to existing numeric values without “cleaning up” the result. Any additional rounding/optimization risks accumulating error over repeated operations or round trips. (Gesture-input quantization may exist for UX stability, but is intentionally out of scope for this specification.)

- **Round-trip consistency (best-effort)**: Perfect round-trip guarantees are not always possible across multi-step edits, but for simple numeric cases the rewrite should behave consistently within the limits of JavaScript number precision. Example: $1 \\to 0.01x \\to 100x \\to 1$ should return to (approximately) the original value.

- **Deterministic, minimal rewrite (do not change the nature of properties)**: Scaling MUST NOT reinterpret or “bake” non-numeric authored intent into numeric values. For example, a property that is `auto`, `undefined`, or otherwise non-numeric must remain so. Scale (K) **bakes existing length values**, not “bake-all”.

## Context

Expand Down Expand Up @@ -55,7 +65,7 @@ Simple resize is insufficient because it changes box geometry but leaves geometr

When applied with multiplier $s$:

- **I1. Geometry update**: box geometry parameters MUST be updated as described under “Anchor / origin”.
- **I1. Layout geometry update**: numeric layout geometry fields (e.g. `left/top/right/bottom/width/height`) MUST be scaled by $s$ without reinterpreting author intent (non-numeric values like `"auto"` MUST be preserved).
- **I2. Parameter rewrite**: all tracked, geometry-contributing parameters MUST be multiplied by $s$ (or scaled according to their field-level rules).
- **I3. Invariants preserved**: unitless ratios, enums, IDs, and content MUST remain unchanged.
- **I4. No layout reflow**: the operation MUST NOT attempt to resolve constraints or reflow layout. It only scales stored values.
Expand All @@ -73,27 +83,19 @@ This specification defines **uniform parameter-space scaling** as the baseline b

(If we later support non-uniform scaling $s_x, s_y$, we must define how to map two factors into a single “thickness scale” for strokes/effects; see “Future extensions”.)

### Anchor / origin

Scaling is performed around an **anchor point** in the selection bounds (e.g. top-left, center, etc.).

For a node with an absolute box (`left`, `top`, `width`, `height`):
### Layout geometry (coordinate-space; no anchor)

- Compute the anchor point $A$ in parent coordinates.
- Compute the node’s reference point $P$ (typically its top-left corner at (`left`,`top`)).
- Scale the vector $\overrightarrow{AP}$ by $s$:
For parameter-space scaling we treat layout geometry fields as authored numeric values, and scale them **just like other length values**.

$$
P' = A + (P - A) \cdot s
$$
For a node with layout fields (`left`, `top`, `right`, `bottom`, `width`, `height`):

- Set `left/top` from $P'$
- Set `width/height` to `width * s`, `height * s`
- If a field is a **number**, multiply it by $s$.
- If a field is **non-numeric** (e.g. `"auto"`), preserve it as-is (do not bake or resolve it).

Notes:

- For nodes using `position: "relative"`, `left/top/right/bottom` are still lengths but their meaning depends on layout context. K-scale should still scale the stored values when present, but should not attempt to reflow layout.
- For container flex layout, K-scale ignores constraints/reflow (matching the intent of a proportional scale tool).
- For nodes using `position: "relative"`, offsets (`left/top/right/bottom`) are still lengths but their meaning depends on layout context. K-scale scales the stored values when present, but does not attempt to reflow layout.
- The editor UI may expose an “origin” control for interactive workflows, but anchor-based geometry rewriting is an implementation detail and is not required for the core parameter-space rewrite.

## Examples

Expand Down
52 changes: 43 additions & 9 deletions editor/components/cursor/cursor-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@ export namespace cursors {
css: pngsetcss(_default_png_url, 28, 28),
};

const _ew_resize_png_url =
"/assets/css-cursors-grida/ew-resize-64-x32y32-000000.png";
const _ns_resize_png_url =
"/assets/css-cursors-grida/ns-resize-64-x32y32-000000.png";
const _nesw_resize_png_url =
"/assets/css-cursors-grida/nesw-resize-64-x32y32-000000.png";
const _nwse_resize_png_url =
"/assets/css-cursors-grida/nwse-resize-64-x32y32-000000.png";

export const default_svg = {
data: (
width: number = 32,
Expand Down Expand Up @@ -77,16 +86,41 @@ export namespace cursors {
return `data:image/svg+xml;base64,${btoa(svgData)}`;
};

/**
* Scale tool (K) cursor set — uses designed resize cursors.
*
* Files live under `editor/public/assets/css-cursors-grida/`.
*/
const _ew_resize_scale_png_url =
"/assets/css-cursors-grida/ew-resize-scale-64-x32y32-000000.png";
const _ns_resize_scale_png_url =
"/assets/css-cursors-grida/ns-resize-scale-64-x32y32-000000.png";
const _nesw_resize_scale_png_url =
"/assets/css-cursors-grida/nesw-resize-scale-64-x32y32-000000.png";
const _nwse_resize_scale_png_url =
"/assets/css-cursors-grida/nwse-resize-scale-64-x32y32-000000.png";

export const resize_handle_scale_cursor_map = {
nw: pngsetcss(_nwse_resize_scale_png_url, 32, 32, "nwse-resize"),
n: pngsetcss(_ns_resize_scale_png_url, 32, 32, "ns-resize"),
ne: pngsetcss(_nesw_resize_scale_png_url, 32, 32, "nesw-resize"),
e: pngsetcss(_ew_resize_scale_png_url, 32, 32, "ew-resize"),
se: pngsetcss(_nwse_resize_scale_png_url, 32, 32, "nwse-resize"),
s: pngsetcss(_ns_resize_scale_png_url, 32, 32, "ns-resize"),
sw: pngsetcss(_nesw_resize_scale_png_url, 32, 32, "nesw-resize"),
w: pngsetcss(_ew_resize_scale_png_url, 32, 32, "ew-resize"),
} as const;

export const resize_handle_cursor_map = {
nw: "nwse-resize",
n: "ns-resize",
ne: "nesw-resize",
e: "ew-resize",
se: "nwse-resize",
s: "ns-resize",
sw: "nesw-resize",
w: "ew-resize",
};
nw: pngsetcss(_nwse_resize_png_url, 32, 32, "nwse-resize"),
n: pngsetcss(_ns_resize_png_url, 32, 32, "ns-resize"),
ne: pngsetcss(_nesw_resize_png_url, 32, 32, "nesw-resize"),
e: pngsetcss(_ew_resize_png_url, 32, 32, "ew-resize"),
se: pngsetcss(_nwse_resize_png_url, 32, 32, "nwse-resize"),
s: pngsetcss(_ns_resize_png_url, 32, 32, "ns-resize"),
sw: pngsetcss(_nesw_resize_png_url, 32, 32, "nesw-resize"),
w: pngsetcss(_ew_resize_png_url, 32, 32, "ew-resize"),
} as const;

function pngsetcss(
url: string,
Expand Down
1 change: 1 addition & 0 deletions editor/grida-canvas-hosted/playground/uxhost-toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export function PlaygroundToolbar() {
options={[
{ value: "cursor", label: "Cursor", shortcut: "V" },
{ value: "hand", label: "Hand tool", shortcut: "H" },
{ value: "scale", label: "Scale tool", shortcut: "K" },
]}
onValueChange={(v) => {
editor.surface.surfaceSetTool(
Expand Down
22 changes: 22 additions & 0 deletions editor/grida-canvas-react-starter-kit/starterkit-icons/upscale.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as React from "react";

export const UpscaleIcon = ({ ...props }: React.SVGProps<SVGSVGElement>) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M16 3h5v5M17 21h2a2 2 0 0 0 2-2M21 12v3M21 3l-5 5M3 7V5a2 2 0 0 1 2-2M9 3h3M12 11H4a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-8a1 1 0 0 0-1-1Z"
/>
</svg>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,12 @@ import {
StarIcon,
} from "@radix-ui/react-icons";
import { BrushIcon, LassoIcon, PenToolIcon, TriangleIcon } from "lucide-react";
import { UpscaleIcon } from "../starterkit-icons/upscale";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuCheckboxItem,
DropdownMenuItem,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
Expand Down Expand Up @@ -148,6 +147,7 @@ export default function Toolbar() {
options={[
{ value: "cursor", label: "Cursor", shortcut: "V" },
{ value: "hand", label: "Hand tool", shortcut: "H" },
{ value: "scale", label: "Scale tool", shortcut: "K" },
]}
onValueChange={(v) => {
editor.surface.surfaceSetTool(
Expand Down Expand Up @@ -258,7 +258,7 @@ export function ToolsGroup({
{options.map((option) => (
<DropdownMenuRadioItem value={option.value} key={option.value}>
{/* <div className="w-full flex items-center gap-2"> */}
<ToolIcon type={option.value} />
<ToolIcon type={option.value} className="size-4" />
<span>{option.label}</span>
{option.shortcut && (
<DropdownMenuShortcut>
Expand All @@ -283,6 +283,8 @@ export function ToolIcon({
switch (type) {
case "cursor":
return <CursorArrowIcon {...props} />;
case "scale":
return <UpscaleIcon {...props} />;
case "hand":
return <HandIcon {...props} />;
case "container":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { editor } from "@/grida-canvas";

export type ToolbarToolType =
| "cursor"
| "scale"
| "hand"
| "rectangle"
| "ellipse"
Expand All @@ -27,6 +28,8 @@ export function toolmode_to_toolbar_value(
case "cursor":
case "zoom":
return "cursor";
case "scale":
return "scale";
case "hand":
return "hand";
case "insert":
Expand Down Expand Up @@ -56,6 +59,8 @@ export function toolbar_value_to_cursormode(
switch (tt) {
case "cursor":
return { type: "cursor" };
case "scale":
return { type: "scale" };
case "hand":
return { type: "hand" };
case "container":
Expand Down
1 change: 1 addition & 0 deletions editor/grida-canvas-react/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,7 @@ export function useEventTargetCSSCursor() {
}
switch (tool.type) {
case "cursor":
case "scale":
return cursors.default_png.css;
case "hand":
return "grab";
Expand Down
18 changes: 18 additions & 0 deletions editor/grida-canvas-react/viewport/hotkeys.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,11 @@ export const keybindings_sheet = [
description: "Select tool",
keys: ["v"],
},
{
name: "scale",
description: "Scale tool (parametric scaling)",
keys: ["k"],
},
{
name: "lasso",
description: "Lasso tool (vector mode)",
Expand Down Expand Up @@ -897,6 +902,19 @@ export function useEditorHotKeys() {
editor.surface.surfaceSetTool({ type: "cursor" });
});

useHotkeys(
"k",
() => {
editor.surface.surfaceSetTool({ type: "scale" });
},
// need below, k might open a ui with autofocus input, below prevents "k" being typed in to the input.
{
preventDefault: true,
enableOnFormTags: false,
enableOnContentEditable: false,
}
);

useHotkeys("q", () => {
if (content_edit_mode?.type === "vector") {
editor.surface.surfaceSetTool({ type: "lasso" });
Expand Down
Loading