Skip to content

Pivot Crosshair - #28

Closed
drakeraz09 wants to merge 3 commits into
PenguinMod:developfrom
drakeraz09:feature/pivot-crosshair
Closed

drakeraz09 wants to merge 3 commits into
PenguinMod:developfrom
drakeraz09:feature/pivot-crosshair

Conversation

@drakeraz09

@drakeraz09 drakeraz09 commented Sep 21, 2026 •

Copy link
Copy Markdown

Pivot!!!!!

The bounding box already drew a crosshair in the middle of your selection, but it did literally nothing. Everything rotated and scaled around the center of the bounds no matter what.

So now the crosshair actually does something:

  • Drag it wherever you want the selection to transform from.
  • Snaps to corners, edge midpoints, the selection center, and the costume's rotation center.
  • Double-click (or drop it back on the center) to get rid of it.
  • Saves with the costume, because losing your custom pivot will leave a hole in your heart..

(Bitmap mode gets the crosshair as a plain marker. Bitmap costumes have nowhere to put an anchor because they are foolish and ephemeral).

Its

Paper.js handles the heavy lifting via item.pivot, serializing it into PAPER PROJECT JSON so undo/redo works.

  • For the selection: It belongs to the selection, not individual items. Grouping moves the anchor onto the new group and clears it off the members so there's one definitive answer instead of five.
  • Serialization: It is stored as data-pivot on the item's element. DOMPurify keeps data-* attributes safe, so it survives scratch-vm's round-trip sanitizer.
  • Coordinates are sane: It crosses the svg boundary as an inert copy on item.data, stored relative to the artboard center so it doesn't get killed and ljkadgsfjlkgdlreffd when the svg export origin shifts.

Bugs we befriended along the way

  • The Jolt: RotateTool used to assume the rotation handle was always directly below the center, causing the drawing to JOOOLTTTT the instant you started dragging. Now it uses where the handle actually is.
  • Hover Outlines: Fixed to ACTAULLY respect the transformation matrix instead of throwing shit and mud at the wall to guess item.position.

Files changed

  • New:

  • src/helper/pivot.js (Anchor logic, agreement, & snapping)

  • src/helper/pivot-data.js (svg saving and loading)

  • src/helper/selection-tools/pivot-tool.js (Crosshair rendering & dragging)

  • Modified:

  • bounding-box-tool.js, rotate-tool.js, scale-tool.js, select-tool.js, guides.js, group.js, paper-canvas.jsx, and update-image-hoc.jsx.

Tests

26 unit tests included. I don't wanna write documentation about them. Just take them.

drakeraz09 and others added 3 commits September 21, 2026 13:08
The bounding box already drew a crosshair in the middle of the selection, but it
did nothing. Everything rotated and scaled around the center of the bounds no
matter what, so swinging an arm around its elbow meant moving the art, rotating
it, and moving it back. Every single time.

Now you can drag it. It snaps to the corners, edge midpoints and center of the
selection, and to the costume's rotation center. Drop it back on the center, or
double click, and it goes away.

The anchor sits on paper's own `item.pivot`, which already transforms with the
item and gets serialized into the project JSON so undo works.

It belongs to the selection rather than one item. A group is a single item and
answers for itself, and a loose multi-selection only counts as having an anchor
while every member agrees on it. Grouping moves the anchor onto the new group
and clears it off the members so there is one answer instead of five.

The old static glyph and the selectionAnchor bookkeeping ScaleTool did to keep
it upright are gone, since the crosshair is drawn on the guide layer now and
rebuilt on every update.

Also fixes the hover outline landing in the wrong spot once an item has an
anchor. It was placed with `item.position`, which IS the anchor as soon as you
set one. Uses the matrix instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RotateTool always span the selection about the center of its bounds. Now that
the selection can carry an anchor, spin around that when everyone agrees on
one, and fall back to the bounds center otherwise.

This also kills a jolt that was there before any of this existed. `prevRot` got
initialized to 90 degrees on mouse down, which assumes the rotation handle sits
directly below whatever you are spinning around. Only true when that happens to
be the bounds center. So the first drag event applied the gap between the real
angle and the assumed one as a rotation, and the drawing jumped the instant you
touched the handle. With an anchor set it was bad enough to throw the art off
screen.

Just use where the handle actually was.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`item.pivot` survives undo because paper serializes it into the project JSON,
but it does not survive the SVG round trip a costume goes through on save. An
anchor that quietly vanishes when you reopen the project is worse than no
anchor at all.

It goes on the item's own element as `data-pivot`. The element is the key, so
there is no id to hand out and nothing to clean up later, and copying a subtree
brings the anchor along for free. DOMPurify's svg profile keeps `data-*`
attributes, so it survives the round trip scratch-vm puts costumes through.

It crosses the SVG boundary as an inert copy on `item.data`, written before the
export scales the layer, read back on import, and applied only once
initializeSvg has finished shoving everything around. That is what stops it
getting mangled.

Stored relative to the art board center rather than in SVG coordinates, because
the SVG export origin depends on the current content bounds and moves whenever
the art does. An anchor written in SVG space would drift every time you nudged
the drawing.

Bitmap mode gets the crosshair as a plain marker. A bitmap costume has nowhere
to save an anchor, and setting one would change what `position` means on the
raster the bitmap tools move around.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@drakeraz09

Copy link
Copy Markdown
Author

I FORGOT TO SAY: all the commits are very new Because i'm a lazy sack of shit that cant use git for anything so i had to do it all just now

@KylomaskGamer

Copy link
Copy Markdown

did you test it
does it function

@drakeraz09

Copy link
Copy Markdown
Author

did you test it does it function

8mb.video-EOu-JbRcSzSP.mp4

Im pretty sure it should work on PenguinMod-Paint This is it working on my pm fork so

@Ianyourgod

Copy link
Copy Markdown
Member

how is the pivot stored in the project.json?

@drakeraz09

drakeraz09 commented Sep 22, 2026 •

Copy link
Copy Markdown
Author

Pivot is stored using the svg itself using attribute "data-pivot", rather than project.json serialization... so the pivot is put in the svg itself (survives sanitization since the sanitizer will spare data-* tags on svgs)

@KylomaskGamer

KylomaskGamer commented Sep 22, 2026 via email

Copy link
Copy Markdown

@drakeraz09

drakeraz09 commented Sep 22, 2026 •

Copy link
Copy Markdown
Author

is there a way to reset the pivot perhaps Sent from my phone :D

yesss You can just double click the crosshair or hover it over the center

@drakeraz09

Copy link
Copy Markdown
Author

TO CLARIFY ON THE PROJECT JSON THING I meant the PAPER.JS LIVE PROJECT JSON that tracks state and what not. i realize now that that is very easily mistaken with an sb3 project json

@RedMan13 RedMan13 closed this Sep 22, 2026
@RedMan13

Copy link
Copy Markdown
Collaborator

reimplemented without ai, the feature itself is added to port though

@KylomaskGamer

Copy link
Copy Markdown

reimplemented without ai, the feature itself is added to port though

very kind :)

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.

4 participants