Move, turn and scale an item in the composition picture live

Dragging an object in the Composition tab's picture writes the copy
of the composition at the playhead and asks for a preview once a
frame. The tab answered every such preview by building that copy
again from the composition, which still held the old value. So the
object stood still while dragged, and a move often did nothing at
all, because its end was read from a copy that had just been rebuilt.
Values typed in the inspector were not affected.

- SceneViewport gets editing(), which says whether a gesture of this
  view is under way: a drag, a size or turn handle, a polygon corner,
  a joint anchor or a tilemap stroke.
- CompositionCanvas leaves its copy alone while such a gesture sends
  its frame previews. The command at the end writes the composition
  as before, and the copy is built again from it.

Tests:
- tsc, eslint and Prettier are clean.
- e2e 69 on melektaus-atx: ALL PASSED, 16 checks, 69l is new. An item
  dragged 60 px to the right stands at +60 px while the button is
  still down (590 to 650) and has x = 60 in the model afterwards.
- Counter-check: rebuilding the copy on every preview again turns 69l
  red. The item stays put while dragged and x is 0 after letting go.
- Gate on melektaus-atx: green, 305 files, 4714 tests.
- Turning and scaling by the handles were not tried in the browser.
  They take the same path and editing() covers them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-12 19:52:11 +02:00
1 parent b16fe8c718
commit 33891acfb3
4 files changed
+48 -3

No files matched your search

+1 -1
View File
@@ -47,7 +47,7 @@ npm run build # writes dist/ and regenerates index.html; the folder is wh
The browser suite and the three example games need a machine with a GPU:
```bash
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 2048 checks (GM_STEPS=20 for one step)
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 2049 checks (GM_STEPS=20 for one step)
# every tool signs in first – see the Accounts page
node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs
node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs
+6 -1
View File
@@ -4,6 +4,7 @@ import { findAssetOf } from '../../core/model/Project';
import { compositionVideosAhead } from '../../viewport/items';
import type { VideoHearing } from '../../viewport/VideoLibrary';
import type { Frame } from '../../viewport/WorldViewport';
import { isDragFrame } from '../framePreview';
import type { EditorState } from '../state';
import { SceneViewport } from '../viewport/SceneViewport';
@@ -24,7 +25,11 @@ export class CompositionCanvas extends SceneViewport {
private readonly playhead: () => number,
) {
super(state);
state.on('project', () => this.rebuildPicture());
// A gesture here writes the copy shown and asks for a preview once a frame. Building the copy
// again from the composition then would drop what it moved until the pointer lets go.
state.on('project', () => {
if (!(isDragFrame() && this.editing())) this.rebuildPicture();
});
state.on('load', () => this.rebuildPicture());
}
+11
View File
@@ -240,6 +240,17 @@ export class SceneViewport extends FreeViewport {
if (this.collectFrame().items.length > 0) this.frameScene();
}
/** Whether a gesture of this view is under way: a drag, a handle, a corner, an anchor or a stroke. */
protected editing(): boolean {
return (
this.dragging !== null ||
this.sizer.active ||
this.corners.active ||
this.anchors.active ||
this.painter.active
);
}
/** The Container tab keeps the view of each container instead (#171). */
protected framesOnOpen(): boolean {
return true;
+30 -1
View File
@@ -6,7 +6,8 @@
* takes an item with its object, and Play runs the playhead to the end. The video of an item about
* to begin is opened ahead, so the cut to it shows its picture at once, and Play waits for a video
* whose player is still loading. A copied item is pasted under the pointer or at the playhead. A
* video dragged in wears a sprite the composition keeps to itself.
* video dragged in wears a sprite the composition keeps to itself. An item dragged in the picture
* follows the pointer while the button is down.
*
* Everything it adds carries a `ct69` id and goes again at the end.
*/
@@ -348,6 +349,34 @@ export async function compositionTab(t) {
JSON.stringify({ videoDrop, one, row, two, undone }),
);
// 69l. An item dragged in the picture follows the pointer while the button is down, not only when
// it is let go: the drag's preview each frame does not build the picture again from the document.
await evaluate('__gm.composition.scrub(15)');
await sleep(100);
const front = (await json('__gm.composition.shown()')).at(-1)?.id ?? '';
const startAt = await json(`__gm.composition.objectScreen('${front}')`);
const objectX = () =>
json(
`__gm.state.project.assets.find((a) => a.id === '${CUT}').objects.find((o) => o.id === '${front}').position.x`,
);
const xBefore = await objectX();
await mouse('mouseMoved', startAt.x, startAt.y);
await mouse('mousePressed', startAt.x, startAt.y);
for (let i = 1; i <= 6; i++) {
await mouse('mouseMoved', startAt.x + i * 10, startAt.y, 'left', { buttons: 1 });
await sleep(16);
}
await sleep(100);
const during = await json(`__gm.composition.objectScreen('${front}')`);
await mouse('mouseReleased', startAt.x + 60, startAt.y);
await sleep(150);
const xAfter = await objectX();
check(
'an item dragged in the picture follows the pointer while dragged and keeps the place let go',
Math.abs(during.x - startAt.x - 60) < 2 && xAfter > xBefore,
JSON.stringify({ front, startAt, during, xBefore, xAfter }),
);
await showTab('scene');
await mutate(`
const p = __gm.state.project;