Skip to content

Design fluent position draft modifiers for snapping and constrained transforms #112

Description

@kostyafarber

Context

Continuous position edits currently lower too early into sparse position patches. That works, but it makes tools reason about GlyphLayerPositions when they are really expressing actions like move, rotate, resize, absolute handle placement, snapping, axis locking, or point-rule constrained movement.

We want a more ergonomic tool-facing API before adding snapping and locking behavior.

Settled Naming

Use edit/action language for the public API:

class LayerPositions
interface PositionEdit
class MoveEdit implements PositionEdit
class RotateEdit implements PositionEdit
class ResizeEdit implements PositionEdit
class PlaceEdit implements PositionEdit

Public verbs:

layer.positions.move(...)
layer.positions.rotate(...)
layer.positions.resize(...)
layer.positions.place(...)

Use move instead of translate in tool-facing code. Reserve translate for pure geometry/math or lower-level intent names.

Use resize instead of scale for bounding-box handle interactions. Reserve scale for pure numeric geometry/math.

Use place instead of direct for custom absolute point placement, such as pen handle mirroring.

Avoid public Draft names. The implementation may still use a preview/commit draft internally, but the tool-facing concept is a position edit.

Direction

Explore operation-specific position edits with fluent configuration. Chain order configures behavior only; execution order is fixed internally.

const move = layer.positions
  .move(subject)
  .from(PositionReference.point(hitPointId))
  .axisLockedBy(AxisLock.primary({ when: () => modifiers.shiftKey }))
  .snappedBy(snapping)
  .constrainedBy(pointRules);

move.preview(delta);
move.commit();

For rotation:

const rotate = layer.positions
  .rotate(subject, origin)
  .angleSnappedBy(AngleSnap.everyDegrees(15, { when: () => modifiers.shiftKey }));

rotate.preview(angle);
rotate.commit();

For bounding-box resizing:

const resize = layer.positions
  .resize(subject, boundsHandle)
  .uniformBy({ when: () => modifiers.shiftKey })
  .scaleSnappedBy(ScaleSnap.increment(0.1));

resize.preview(pointer);
resize.commit();

For absolute/custom placement:

const handles = layer.positions.place({ points: handlePointIds });

handles.preview([
  PositionMove.to(cpOutId, handlePos),
  PositionMove.to(cpInId, mirror),
]);

handles.commit();

Design Constraints

  • Fluent configuration methods mutate/configure the edit before first preview and return this.
  • Chain order should not define execution order.
  • Each edit should expose only relevant modifiers: move gets axis/object snapping/point rules; rotate gets angle snapping; resize gets uniform resize and scale snapping; place gets absolute position moves.
  • Preview should always evaluate from the frozen base state, not accumulate from the previous preview frame.
  • Raw GlyphLayerPositions, previewPositionPatch, and commitPositionPatch should remain lowering details or test-only surfaces, not the normal tool-facing API.
  • Point rules should integrate as a move modifier, not force tools into a generic previewResolved(...) path.
  • The rules layer should distinguish uniform expanded movement from truly absolute constrained geometry, so smooth-anchor handle-following can still commit as an efficient translation when possible.
  • Snapping needs an explicit reference concept, e.g. PositionReference.point(...), anchor(...), position(...), or bounds references.

Proposed Move Pipeline

Execution order should be fixed internally:

  1. Raw delta
  2. Axis lock
  3. Snapping via PositionReference
  4. Point rules
  5. Preview lowering
  6. Commit lowering

Open Questions

  • What snap result metadata is needed for overlay rendering?
  • Where should object/guideline/grid snapping live: a single snapping service or separate composable snap providers?
  • How should point-rule movement expose expanded-uniform vs absolute-resolved outcomes internally?
  • How should resize snapping arbitrate between snapped handle positions and snapped scale factors?
  • Should object/guideline angle snapping be supported for rotation, or should first pass stay with numeric angle snapping only?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions