Open clips and frames from the asset panel by double click (#430)
feuerwerk / gate (push) Successful in 4m58s
feuerwerk / suite (push) Failing after 19m37s

A double click on a clip row did not open its tab. The first click of
the double click had already selected the clip for the inspector, so
the second click selected the same thing again and the editor saw no
change. Frames of textures and pixel art had no rows at all.

- A double click now asks for the tab with openSelection, even when
  the selection is already right.
- Textures with frames and pixel art with more than one frame get a
  chevron and one row per frame, like sounds and videos. Which rows an
  asset has is decided in one place, src/core/model/assetParts.ts,
  used by the panel and the inspector link.
- A texture frame shows in the inspector on click and opens the
  texture with the frame selected on double click.
- A pixel art frame selects the pixel art on click, so an open tab
  paints on that frame. A double click opens the tab on the frame.
- A frame can be dragged onto a composition track like a video clip.
  The item carries the sprite of the whole image, the frame becomes
  the texture of its object, and it is 3 s long. The agent can do the
  same with add_composition_item.
- A texture label that names a frame now points at the frame row.

Found on the way: the e2e check from #425 only double clicked the
texture label, where the first click is a ping, so the bug never
showed there.

Tests:
- Unit: asset-parts (new, 6), composition-sprites and references
  extended, related tests 303/303, agent tests 21/21
- 5 mutations, all red in the matching test
- e2e 69 (atx): ALL PASSED, 26 checks, 69t new
- Gate (atx): CHECK EXIT 0, 325 files, 5017 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-18 19:30:29 +02:00
1 parent 37779a2070
commit 2e4c0a09ea
15 files changed
+405 -77

No files matched your search

+23 -11
View File
@@ -9,9 +9,10 @@ import { newId } from '../ids';
import type { Asset, CompositionItem, SpriteAsset } from '../model/Asset';
import { isCompositionSprite } from '../model/composition';
import { generatesPictures } from '../model/generated';
import { pixelArtWornId } from '../model/pixelart';
import { findPixelFrame, pixelArtWornId } from '../model/pixelart';
import type { PropertyBag } from '../model/Properties';
import { findAsset, objectHolders, type Project } from '../model/Project';
import { findFrame } from '../model/texture';
import { findVideo } from '../model/video';
import { spriteFromTexture } from '../properties/aspect';
import { type Removed, removeWhere, restoreAt } from '../util/list';
@@ -32,27 +33,38 @@ export function wornPicture(asset: Asset): string | null {
: null;
}
/** What an item dropped on a track wears: the picture its sprite is made of, and a clip of it (#425). */
/** What an item dropped on a track wears: the picture its sprite is made of, and a part (#425, #430). */
export interface DroppedPicture {
picture: string;
/** The clip of a video the item plays, as an override on its object; null for the whole picture. */
clip: string | null;
/**
* The clip of a video the item plays or the frame it shows, as an override on its object; null for
* what its sprite wears.
*/
part: string | null;
/** The name a new sprite for the picture gets. */
name: string;
}
/**
* What an asset or a clip of a video (#425) dragged onto a track wears. A clip shares the sprite of
* its video, so one composition keeps one sprite per video. Null for anything that is no picture.
* What an asset, a clip of a video (#425) or a frame of a picture (#430) dragged onto a track wears. A
* part shares the sprite of its asset, so one composition keeps one sprite per video or picture. Null
* for anything that is no picture.
*/
export function droppedPicture(project: Project, id: string): DroppedPicture | null {
const asset = findAsset(project, id);
if (asset) {
const picture = wornPicture(asset);
return picture === null ? null : { picture, clip: null, name: asset.name };
return picture === null ? null : { picture, part: null, name: asset.name };
}
const video = findVideo(project, id);
return video?.clip ? { picture: video.asset.id, clip: video.clip.id, name: video.asset.name } : null;
if (video?.clip) return { picture: video.asset.id, part: video.clip.id, name: video.asset.name };
const frame = findFrame(project, id);
if (frame) return { picture: frame.asset.id, part: id, name: frame.asset.name };
const pixel = findPixelFrame(project, id);
if (!pixel) return null;
// Its first frame is what the sprite of a sequence wears already.
const picture = pixelArtWornId(pixel.asset);
return { picture, part: id === picture ? null : id, name: pixel.asset.name };
}
/** The sprite the composition already keeps for this picture: one serves every item wearing it. */
@@ -74,8 +86,8 @@ function newCompositionSprite(
}
/**
* A texture, a drawn picture, a video or a clip of one laid on a track: an item wearing the sprite the
* composition keeps for that picture, made first when it has none yet, in one undo step. A clip is
* A texture, a drawn picture, a video or a part of one laid on a track: an item wearing the sprite the
* composition keeps for that picture, made first when it has none yet, in one undo step. A part is
* the texture of the item's object.
*/
export function addPictureItem(
@@ -90,7 +102,7 @@ export function addPictureItem(
const steps = [
...(kept ? [] : [new AddSpriteCommand(sprite)]),
command,
...(dropped.clip ? [new SetOverrideCommand(compId, objectId, 'texture', dropped.clip)] : []),
...(dropped.part ? [new SetOverrideCommand(compId, objectId, 'texture', dropped.part)] : []),
];
return { command: steps.length === 1 ? command : oneStep('Add item', steps), objectId };
}