Skip to content

Migrate all shape gizmos to the registry-driven generic system - #4471

Draft
Ayush2k02 wants to merge 18 commits into
GraphiteEditor:masterfrom
Ayush2k02:feat/gizmo-registry-full
Draft

Migrate all shape gizmos to the registry-driven generic system#4471
Ayush2k02 wants to merge 18 commits into
GraphiteEditor:masterfrom
Ayush2k02:feat/gizmo-registry-full

Conversation

@Ayush2k02

Copy link
Copy Markdown
Contributor

No description provided.

Keavon and others added 18 commits August 23, 2026 02:54
…ter APIs

The stack was written against the 2026-07-23 fork point and does not build on
current master. Two of master's changes land on it directly:

- GraphiteEditor#4457 deleted the legacy `subpath` module. The Heart node was the only
  remaining caller of `ManipulatorGroup`/`Subpath::new`/`Vector::from_subpath`.
  Its anchor math moves to a `heart_bezpath` constructor in `shapes.rs`,
  alongside the other generators, so the node body stays as thin as its
  neighbors and the load-bearing winding order lives with the rest of the
  geometry.

- GraphiteEditor#4387 replaced raw input indices with parameter symbols, removing the
  `NodeInputDecleration` trait and `grid::SpacingInput`'s type parameter. The
  registry's remaining `parameter_index: 1` literals become the generated
  symbols, and the generic gizmos pair their runtime-chosen index back with
  their node identifier through `ParameterRef` rather than passing a bare
  `usize` to `InputConnector::node`.

No behavior change. `cargo test -p graphite-editor -p vector-nodes` passes
(225 tests), including the six registry tests.
The port from the deleted subpath API to BezPath was a manual translation of
anchor and handle positions, and nothing else in the tree exercises the heart's
geometry. These pin the properties the translation could plausibly have broken:
segment count and closure, mirror symmetry about the vertical axis, linear
scaling with radius, and honoring the center argument.
Every parametric shape's gizmo shares the same mechanics -- hit-test a handle,
run a hover/drag state machine, draw the handle, write the input -- which is what
the generic gizmos already replace. But three behaviors in the hand-written
handlers genuinely depend on a node's geometry and cannot be expressed as
registry data:

- A star's snap radii fall out of its side count and its *other* radius.
- A spiral's turns and outer radius have to move together, or the spiral changes
  tightness as you drag it.
- A star previews its outline and spokes while dragging, not just the handle.

`GizmoBehavior` carries those as optional functions supplied by the shape, so the
registry stays a declarative table and the geometry-specific math stays with the
geometry. All fifteen existing declarations opt out via `GizmoBehavior::NONE`;
nothing changes for them.

Snap resolution is extracted as `nearest_snap_target` so it can be tested without
a live document. Targets are captured when the gizmo is first hovered rather than
recomputed per frame, since they depend on parameters that are themselves in flux
during a drag.
…gration

The hand-written `NumberOfPointsDial` drew a spoke per side and the shape's
outline: the spokes at rest as a hint that the dial exists at all, and both
while dragging so the count being edited stays legible. Moving the polygon to
the generic dial dropped all of it, leaving a bare ring with no indication of
what it controls.

The overlay hook now runs in every state, including Inactive, since a resting
affordance is exactly the case the shape wants to draw for. `GizmoContext` grows
the cursor position, the interaction state, and the path editor's state, which
between them are what the ported drawing code needs -- including standing down
near an editable segment, so the hint never competes with path editing.

`gizmo_behaviors` is the new home for this shape-specific half. It also carries
the star's declarations, which are correct but not yet reachable: the star's
radius handles sit on its vertices rather than at a single point on the +X axis,
so they need a gizmo type the generic layer does not have yet.
The star's radius is not a slider. Its handles sit on the shape's own points --
`radius_1` grabbable at every outer vertex, `radius_2` at every inner one -- and
the drag runs out along whichever point the user took hold of, not along a fixed
axis. The generic slider assumed a single handle on the local +X axis, which is
why the star could not migrate with the circle and the polygon.

So the slider now asks the shape where its parameter can be grabbed. The default
is unchanged and still a single handle `value` out along +X, which is what a
circle, a heart, and a spiral all want. A shape that puts handles on its geometry
returns the whole set, and the drag projects onto the ray through the one that
was grabbed. `handle_index` records which, so the overlay can draw the right ray.

With that, the star's remaining behavior is expressible through the hooks:
snapping to the radii where its points line up, ticks marking them, the ray
extended across the viewport, and the outline of the shape being reshaped.

Deletes `PointRadiusHandle` and `NumberOfPointsDial`, whose last user this was,
along with the `ShapeGizmoHandlers::Star` variant.

Two deliberate differences from the hand-written version: the drag projects in
the layer's local space rather than mixing local deltas with viewport-space
directions, which is what it meant to do and now also holds under rotation; and
the red alignment guides drawn at the moment of snapping are not reproduced --
the ticks still mark every snap radius.
A spiral's control neither extends nor steps: it winds. Dragging an endpoint
sweeps around the origin, and the sweep has to keep counting past a full turn,
which the angle between two points cannot express. Turns alone would also change
the spiral's tightness as it grows, so the outer radius has to move with it.

So the generic layer grows two things. `drag` lets a shape convert cursor motion
into node inputs itself, returning every input the motion implies rather than the
single value the default projection produces. And the slider now accumulates the
angle swept about the layer's origin frame by frame, since that is bookkeeping no
stateless hook can do for itself -- with a half-degree deadzone, because near the
origin the angle between successive cursor positions is mostly noise.

Because a multi-parameter drag is the only thing that knows how its parameters
constrain each other, supplying `drag` also bypasses clamping and snapping.

The registry entry drops to the turns control alone. The inner and outer radius
sliders it used to declare were never reachable, and a handle for either would
land at an arbitrary point on a curve that is nowhere near circular -- while
winding the spiral from its own endpoints reads immediately. Both are still
adjustable from the Properties panel.

Deletes `SpiralTurns` and `SpiralGizmoHandler`.
An arc's sweep is held to a single turn and never runs backwards, and its start
angle is kept inside [-180°, 180°]. All three limits get reached by *continuing*
a drag rather than by ending it, so the gesture does not stop at them -- dragging
the start endpoint past a full sweep hands over to the end endpoint and carries
on from there.

That means a drag has to be able to rewrite the baseline the rest of the gesture
is measured against, so `drag` now takes its `DragInput` mutably and the slider
carries whatever the shape leaves behind into the next frame. Re-anchoring is the
one thing a drag genuinely owns about itself.

`GizmoType::Angle` now builds a gizmo instead of being silently skipped. It runs
on the same handle machinery as a slider, since with a drag hook the two differ
only in what the default would have done -- and for an angle the default, a
distance along a ray, is meaningless. `angle_deadzone` moves onto the behavior
for the same reason: the spiral wants its jitter guard, the arc does not.

One entry covers the sweep rather than two. Dragging either endpoint can move the
start angle and the sweep together, so a separate start-angle gizmo would be a
second control over one gesture.

Deletes `SweepAngleGizmo`, `RadiusHandle`, and `ArcGizmoHandler`. The arc's
radius now uses the plain slider, as the circle's already did.

Not reproduced: the red guide lines drawn at the moment a sweep snaps. The sweep
still snaps to each eighth of a turn, and the angle readout still tracks it.
A grid's rows and columns are not grabbed at a point: any spot along an edge will
do, and the band the edge occupies counts too. So a shape can now supply
`hover_distances` in place of measuring to its handle positions, which is the
same escape hatch the other hooks are -- the generic layer keeps arbitrating
between overlapping gizmos, it just no longer assumes the thing being aimed at is
a point.

Dragging the top edge upward also has to move the layer. The node builds its grid
downward from the origin, so without a matching translation the new rows would
appear at the bottom and the edge would slide out from under the cursor. `drag`
therefore returns a `DragWrites` rather than a bare list of inputs, carrying an
optional transform alongside them.

Dragging past the last row does not stop at one either -- the grid turns inside
out and the opposite edge takes over -- so the gesture re-anchors its origin the
same way the arc re-anchors its baseline.

Rows and columns are declared as dials, but a declaration that brings its own
drag is hosted by the general handle gizmo rather than the dial: the dial is the
narrow one, a count stepped by horizontal drag, and none of that applies here.

The grid's edge geometry -- four edges across two layouts, rectangular and
isometric -- moves to `grid_shape.rs`. It is the grid's own geometry rather than
gizmo machinery, and it is the same shape of split as the star's snap radii.

The spacing gizmo the registry used to declare is dropped. It was a position
gizmo that was never built, and spacing is a two-axis value with no obvious
handle on the shape.

With this the `shape_gizmos` module is empty and deleted. All seven shapes now
run on the registry.
Six function-pointer fields spelled out inline made the struct hard to read and
tripped clippy's complex-type lint. The aliases also give the hooks somewhere to
be documented individually.
… grid drawing a stray handle

Auditing the migrated shapes against the handlers they replaced turned up three
regressions, two of them shipped and one of them mine.

A circle's radius was never grabbed at a point. The hand-written handler took the
whole circumference, through a band that widened with the stroke so a thick
outline stayed grabbable at its edge, and narrowed for a circle small on screen so
the band could not swallow the shape. It drew that band as a pair of dashed
ellipses rather than a handle. Moving the circle to the plain slider replaced all
of it with a single dot on the +X axis; the arc's radius lost the same when it
followed.

`CIRCULAR_RADIUS` puts it back for both. It reports the radial distance from the
circumference rather than a flat yes, so an arc's endpoints still win the cursor
where the two overlap -- which the hand-written version could not do, since its
two gizmos hovered independently.

The grid, meanwhile, was drawing a handle dot and a line to it at local
`(row_count, 0)`: a point with no meaning, left over from the default handle
position, sitting inside the grid. `draws_own_handle` marks the shapes whose
overlay already draws the thing being grabbed.

`hover_distances` now also owns its own range. A shape that answers the hover
question knows how far is too far, and the point threshold that suits a handle is
wrong for a band or an edge.
Two differences from the hand-written dial, both dating from the original
migration. It stepped every 25 pixels of drag, not 20, so the generic one turned
a quarter faster than the shape it replaced. And it disappeared once the shape
was small enough on screen that the dial would cover it -- the generic one's
check compared a single local unit against nothing meaningful and never fired, so
the dial stayed live on a shape a few pixels across and swallowed presses meant
for the layer.

The replacement measures the layer's bounding box, which is what "too small to
sit around" actually means and does not depend on the shape's own parameters.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants