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 };
}
+28
View File
@@ -0,0 +1,28 @@
import type { Asset } from './Asset';
import { frameLabel } from './pixelart';
import { findAsset, type Project } from './Project';
import { clipsOf, hasClips } from './sound';
/** A named piece of an asset with an id of its own: a clip of a sound or video, a frame of a picture. */
export interface AssetPart {
id: string;
name: string;
}
/**
* What the asset panel lists under an asset (#425, #430): the clips of a sound or a video, the frames
* of a texture, and those of pixel art once it has more than one – the frames a texture field offers.
*/
export function partsOf(asset: Asset): AssetPart[] {
if (hasClips(asset)) return clipsOf<AssetPart>(asset).map(({ id, name }) => ({ id, name }));
if (asset.type === 'texture') return asset.frames.map(({ id, name }) => ({ id, name }));
if (asset.type === 'pixelart' && asset.frames.length > 1)
return asset.frames.map((frame, index) => ({ id: frame.id, name: frameLabel(index) }));
return [];
}
/** The asset `id` is a listed part of; undefined for an asset's own id or anything else. */
export function partOwner(project: Project, id: string): Asset | undefined {
if (id === '' || findAsset(project, id)) return undefined;
return project.assets.find((asset) => partsOf(asset).some((part) => part.id === id));
}
-8
View File
@@ -55,14 +55,6 @@ export function assetNamedBy(assets: readonly Asset[], id: string): Asset | unde
);
}
/** The sound or video `id` is a clip of; undefined for an asset's own id or anything else. */
export function clipOwner(project: Project, id: string): ClipOwner | undefined {
if (id === '' || findAsset(project, id)) return undefined;
return project.assets.find(
(a): a is ClipOwner => hasClips(a) && clipsOf<{ id: string }>(a).some((c) => c.id === id),
);
}
/**
* Gives the clips of a copy fresh ids, since a clip's id is named across the document, and notes
* each old id with its new one in `ids` so the places that named a clip can follow.
+6 -4
View File
@@ -1,7 +1,8 @@
import { type PropertyBag, type PropertyValue, textOf } from '../model/Properties';
import { findPixelFrame } from '../model/pixelart';
import { findAsset, type Project } from '../model/Project';
import { clipOwner, findSound } from '../model/sound';
import { partOwner } from '../model/assetParts';
import { findSound } from '../model/sound';
import { findFrame } from '../model/texture';
import { findVideo } from '../model/video';
import { isKindWith } from './kinds';
@@ -28,13 +29,14 @@ export function referencedAssetId(project: Project, def: PropertyDef, value: Pro
}
/**
* What the label of a reference row links to: the asset it names, or the clip of a sound or a video
* itself (#425) – a clip has a row of its own in the asset panel and is picked in its asset's tab.
* What the label of a reference row links to: the asset it names, or the part of it named itself – a
* clip (#425), a frame (#430) – which has a row of its own in the asset panel and is picked in its
* asset's tab.
*/
export function referencedTarget(project: Project, def: PropertyDef, value: PropertyValue): string | null {
const assetId = referencedAssetId(project, def, value);
const id = textOf(value);
return assetId && clipOwner(project, id)?.id === assetId ? id : assetId;
return assetId && partOwner(project, id)?.id === assetId ? id : assetId;
}
export interface Reference {
+1 -1
View File
@@ -712,7 +712,7 @@ export class Editor implements PlayHost {
});
state.on('open', () => {
const s = state.selection;
if (s?.kind !== 'asset') return;
if (s?.kind !== 'asset' && s?.kind !== 'clip' && s?.kind !== 'frame') return;
if (findAsset(state.project, s.assetId)?.type === 'scene') this.showTab('scene');
else this.openDocument(`asset:${s.assetId}`);
});
+28 -7
View File
@@ -21,6 +21,8 @@ import { RemoveFrameCommand } from '../core/commands/frameCommands';
import { DisconnectCommand, FoldEdgeCommand, RemoveNodeCommand } from '../core/commands/graphCommands';
import { RemoveSceneObjectsCommand } from '../core/commands/sceneObjectCommands';
import { RemoveVectorShapeCommand, RemoveVectorShapesCommand } from '../core/commands/vectorArtCommands';
import type { Asset } from '../core/model/Asset';
import { partOwner } from '../core/model/assetParts';
import type { AssetType } from '../core/model/AssetTypes';
import { curveUsages } from '../core/model/curve';
import { structTypeUsages } from '../core/model/structTypes';
@@ -32,7 +34,6 @@ import { findAsset, findGraphAsset, type ObjectHolder, type Project } from '../c
import { ownerOf } from '../core/music/embedded';
import type { SceneObject } from '../core/model/SceneObject';
import { pixelArtRefIds } from '../core/model/pixelart';
import { clipOwner } from '../core/model/sound';
import { textureRefIds } from '../core/model/texture';
import { videoRefIds } from '../core/model/video';
import {
@@ -44,7 +45,7 @@ import {
} from '../core/model/usages';
import { Dialog } from '../ui';
import { removeSongItems, removeTrack } from './song/songActions';
import { type EditorState, graphSelection, objectSelection } from './state';
import { type EditorState, graphSelection, objectSelection, type Selection } from './state';
/**
* What a double-click on an asset does, wherever it happens (asset panel, "Open …" entries, a link
@@ -55,11 +56,31 @@ export function activateAsset(state: EditorState, assetId: string): void {
state.openAsset(assetId);
}
/** The same for an id that may name a clip of a sound or a video (#425): its tab opens with the clip picked. */
export function activateAssetOrClip(state: EditorState, id: string): void {
const owner = clipOwner(state.project, id);
if (owner) state.select({ kind: 'clip', assetId: owner.id, clipId: id });
else activateAsset(state, id);
/**
* The same for an id that may name a part of an asset – a clip (#425), a frame (#430): its tab opens
* with the part picked, also when one click on its row has picked it already.
*/
export function activateAssetOrPart(state: EditorState, id: string): void {
const owner = partOwner(state.project, id);
if (!owner) {
activateAsset(state, id);
return;
}
if (owner.type === 'pixelart') state.showPixelFrame(owner.id, id);
state.openSelection(partSelection(owner, id));
}
/** One click on a part's row: a look at the inspector, its tab left as it is (#425). */
export function glancePart(state: EditorState, owner: Asset, id: string): void {
if (owner.type === 'pixelart') state.showPixelFrame(owner.id, id);
state.glance(partSelection(owner, id));
}
/** A frame of pixel art is painted on rather than picked (#430), so it selects its asset. */
function partSelection(owner: Asset, id: string): Selection {
if (owner.type === 'texture') return { kind: 'frame', assetId: owner.id, frameId: id };
if (owner.type === 'pixelart') return { kind: 'asset', assetId: owner.id };
return { kind: 'clip', assetId: owner.id, clipId: id };
}
/** Groups the objects into a new container asset placed in their stead; selects the container. */
+3 -2
View File
@@ -129,8 +129,9 @@ export const COMPOSITION_COMMANDS: readonly CatalogCommand[] = [
name: 'add_composition_item',
description:
'Lays an asset on a track of a composition, from a frame. A placeable asset (a sprite, a container, a text, ...) ' +
'becomes an object of the composition, living as long as the item; a texture, pixel art, vector art, a video or a ' +
'clip of one gets a sprite of the composition to wear it; a sound, a sound clip, a pattern or a song becomes an ' +
'becomes an object of the composition, living as long as the item; a texture, pixel art, vector art, a video, a ' +
'clip of a video or a frame of a picture gets a sprite of the composition to wear it, the clip or frame as its ' +
'texture; a sound, a sound clip, a pattern or a song becomes an ' +
'audio item. Without a length it is as long as its video or what it plays, else 3 s, and keeps following it. ' +
"An item's object is moved and set with the commands of area objects, in = the composition. Items of a track may " +
'overlap. Animations go on a track with add_composition_clip.',
+5 -5
View File
@@ -1,7 +1,7 @@
import type { PropertyBag } from '../../core/model/Properties';
import type { FederatedPointerEvent } from 'pixi.js';
import { Label, Panel, pressCount, ScrollContainer, ui, type Widget } from '../../ui';
import { activateAssetOrClip } from '../actions';
import { activateAssetOrPart } from '../actions';
import { type EditorState, sameSelection, type Selection } from '../state';
import { assetSignature, buildAssetSection, refreshAssetSection } from './assetSection';
import { annotationSignature, buildAnnotationSection, refreshAnnotationSection } from './annotationSection';
@@ -66,12 +66,12 @@ export class Inspector extends Panel {
}
/**
* A link in a reference row (#221): a click pings the asset or clip (#425) in the asset panel, a
* double-click opens it. Pinging leaves the selection alone, so this panel keeps showing the object
* and both presses land on the same link.
* A link in a reference row (#221): a click pings the asset or a part of it (#425, #430) in the
* asset panel, a double-click opens it. Pinging leaves the selection alone, so this panel keeps
* showing the object and both presses land on the same link.
*/
private followReference(id: string, e: FederatedPointerEvent): void {
if (pressCount(e, `inspector-link:${id}`) === 2) activateAssetOrClip(this.state, id);
if (pressCount(e, `inspector-link:${id}`) === 2) activateAssetOrPart(this.state, id);
else this.state.pingAsset(id);
}
+51 -34
View File
@@ -18,7 +18,8 @@ import { generatesPictures } from '../../core/model/generated';
import { PALETTE_KEY, pixelArtWornId } from '../../core/model/pixelart';
import type { PropertyBag } from '../../core/model/Properties';
import { findAsset } from '../../core/model/Project';
import { clipOwner, clipRefName, clipsOf, hasClips } from '../../core/model/sound';
import { partOwner, partsOf } from '../../core/model/assetParts';
import { clipRefName } from '../../core/model/sound';
import { listedAssets, ownerOf } from '../../core/music/embedded';
import { spriteFromTexture } from '../../core/properties/aspect';
import { assetDeviates } from '../../core/properties/deviation';
@@ -43,7 +44,7 @@ import { type AssetCategory, type AssetCategoryGroup, ASSET_GROUPS, groupAssets
import { ASSET_ICON_SVG } from './assetIcons';
import { assetMenuCategories } from './assetMenu';
import { TOOLBAR_ICON_SVG } from './toolbarIcons';
import { activateAssetOrClip, confirmDeleteAsset } from '../actions';
import { activateAssetOrPart, confirmDeleteAsset, glancePart } from '../actions';
import { openReferenceItems } from '../objectMenu';
import { generalSettings, onGeneralSettings } from '../generalSettings';
import { scrollTween } from '../viewTween';
@@ -77,8 +78,16 @@ export class AssetPanel extends Panel {
private readonly unfolded = new Set<AssetType>();
/** Categories folded away, for the session; every category starts unfolded, showing its groups. */
private readonly foldedCategories = new Set<AssetCategory>();
/** Sounds and videos whose clips are shown under them (#425), for the session; all start folded. */
private readonly unfoldedClips = new Set<string>();
/**
* Assets whose clips or frames are shown under them (#425, #430), for the session; all start
* folded.
*/
private readonly unfoldedParts = new Set<string>();
/**
* The frame of pixel art last picked here (#430). It is not selected – its asset is – so the list
* remembers which row to light up while that asset stays selected.
*/
private pickedPixelFrame: string | null = null;
/** Ids of the rows as shown, headings included. */
private rows: string[] = [];
/**
@@ -114,7 +123,7 @@ export class AssetPanel extends Panel {
new Menu(this.addMenuEntries(headingId), { title: ADD_MENU_TITLE }).open(x, y);
this.list.onDragOut = (id) => this.startPlacementDrag(id);
this.list.onToggle = (id, expand) => {
if (findAsset(state.project, id)) this.setClipsFolded(id, !expand);
if (findAsset(state.project, id)) this.setPartsFolded(id, !expand);
else if (id.startsWith(CATEGORY))
this.setCategoryFolded(id.slice(CATEGORY.length) as AssetCategory, !expand);
else this.setFolded(id.slice(HEADING.length) as AssetType, !expand);
@@ -132,7 +141,8 @@ export class AssetPanel extends Panel {
state.on('load', () => {
this.unfolded.clear();
this.foldedCategories.clear();
this.unfoldedClips.clear();
this.unfoldedParts.clear();
this.pickedPixelFrame = null;
this.scrolledTo = null;
this.refresh();
});
@@ -153,24 +163,27 @@ export class AssetPanel extends Panel {
}
/**
* Unfolds and scrolls to the pinged asset or clip (#425) and lets its row light up; the selection
* stays. What a song or a composition keeps to itself has no row, so its owner lights up (#355,
* #419).
* Unfolds and scrolls to the pinged asset or part (#425, #430) and lets its row light up; the
* selection stays. What a song or a composition keeps to itself has no row, so its owner lights up
* (#355, #419).
*/
private ping(): void {
const pinged = this.state.pingedAssetId ?? '';
const asset = findAsset(this.state.project, pinged) ?? clipOwner(this.state.project, pinged);
const asset = findAsset(this.state.project, pinged) ?? partOwner(this.state.project, pinged);
if (!asset) return;
const id = ownerOf(this.state.project, asset)?.id ?? pinged;
this.reveal(id);
this.list.ping(id);
}
/** A row picked in the list: an asset, or a clip (#425), looked at in the inspector without its tab. */
/** A row picked in the list: an asset or a part (#425, #430), looked at in the inspector, not its tab. */
private pick(id: string | null): void {
const owner = id ? clipOwner(this.state.project, id) : undefined;
if (owner && id) this.state.glance({ kind: 'clip', assetId: owner.id, clipId: id });
const owner = id ? partOwner(this.state.project, id) : undefined;
this.pickedPixelFrame = owner?.type === 'pixelart' ? id : null;
if (owner && id) glancePart(this.state, owner, id);
else this.state.select(id ? { kind: 'asset', assetId: id } : null);
// Its asset may have been selected already, so nothing else says the row changed.
this.syncSelection();
}
/** The row lighting up after a ping and how brightly, or null (test hook). */
@@ -239,17 +252,17 @@ export class AssetPanel extends Panel {
});
if (!expanded) continue;
for (const asset of assets) {
const clips = hasClips(asset) ? clipsOf<{ id: string; name: string }>(asset) : [];
const clipsShown = this.unfoldedClips.has(asset.id);
const parts = partsOf(asset);
const partsShown = this.unfoldedParts.has(asset.id);
items.push({
id: asset.id,
label: asset.name,
marks: asset.type === 'scene' ? this.sceneMarks(asset.id) : undefined,
depth: 2,
overridden: assetDeviates(project, asset),
expanded: clips.length > 0 ? clipsShown : undefined,
expanded: parts.length > 0 ? partsShown : undefined,
});
if (clipsShown) for (const clip of clips) items.push({ id: clip.id, label: clip.name, depth: 3 });
if (partsShown) for (const part of parts) items.push({ id: part.id, label: part.name, depth: 3 });
}
}
}
@@ -274,10 +287,10 @@ export class AssetPanel extends Panel {
this.refresh();
}
/** Unlike a group, folding keeps a picked clip: it may be the one being worked on in its tab. */
private setClipsFolded(assetId: string, fold: boolean): void {
if (fold) this.unfoldedClips.delete(assetId);
else this.unfoldedClips.add(assetId);
/** Unlike a group, folding keeps a picked part: it may be the one being worked on in its tab. */
private setPartsFolded(assetId: string, fold: boolean): void {
if (fold) this.unfoldedParts.delete(assetId);
else this.unfoldedParts.add(assetId);
this.refresh();
}
@@ -299,7 +312,10 @@ export class AssetPanel extends Panel {
private syncSelection(): void {
const s = this.state.selection;
const id = s?.kind === 'asset' ? s.assetId : s?.kind === 'clip' ? s.clipId : null;
if (s?.kind !== 'asset' || partOwner(this.state.project, this.pickedPixelFrame ?? '')?.id !== s.assetId)
this.pickedPixelFrame = null;
const part = s?.kind === 'clip' ? s.clipId : s?.kind === 'frame' ? s.frameId : this.pickedPixelFrame;
const id = part ?? (s?.kind === 'asset' ? s.assetId : null);
// A selection made anywhere else – a graph node, a picker, a fresh asset – has to be reachable
// in the list: its category and group unfold, and with many assets the row is often below the fold.
const type = id ? findAsset(this.state.project, id)?.type : undefined;
@@ -307,8 +323,8 @@ export class AssetPanel extends Panel {
this.setFolded(type, false);
return;
}
// Only a clip picked here unfolds its sound or video: one picked in its tab leaves the list alone.
if (s?.kind === 'clip' && !this.rows.includes(s.clipId)) {
// Only a part picked here unfolds its asset: one picked in its tab leaves the list alone.
if (part && !this.rows.includes(part)) {
this.list.select(null, true);
this.scrolledTo = null;
return;
@@ -453,11 +469,11 @@ export class AssetPanel extends Panel {
}
/**
* Double-click / Enter opens the asset's tab; a scene also becomes the active one, a clip (#425) is
* picked in its tab. Placing is a drag.
* Double-click / Enter opens the asset's tab; a scene also becomes the active one, a part (#425,
* #430) is picked in its tab. Placing is a drag.
*/
private activate(id: string): void {
activateAssetOrClip(this.state, id);
activateAssetOrPart(this.state, id);
}
/**
@@ -470,14 +486,14 @@ export class AssetPanel extends Panel {
}
/**
* Unfolds the asset's group – and for a clip (#425) its sound or video – and scrolls to its row:
* Unfolds the asset's group – and for a part (#425, #430) its asset – and scrolls to its row:
* before a ping, and as a user would before clicking it (test hooks).
*/
private reveal(id: string): void {
const owner = clipOwner(this.state.project, id);
const owner = partOwner(this.state.project, id);
const type = findAsset(this.state.project, owner?.id ?? id)?.type;
if (type && !this.shows(type)) this.setFolded(type, false);
if (owner && !this.unfoldedClips.has(owner.id)) this.setClipsFolded(owner.id, false);
if (owner && !this.unfoldedParts.has(owner.id)) this.setPartsFolded(owner.id, false);
this.scrollTo(id);
}
@@ -501,13 +517,14 @@ export class AssetPanel extends Panel {
* Hands the asset to the view under the pointer: a scene places it, a song lays out a pattern or a
* sound (#334), a composition an animation as a clip (#417), a picture or video as an item (#419)
* and a sound, pattern or song as an audio item (#418). A scene has nowhere to go. A clip of a sound
* or a video goes where its asset would, to a song or a composition (#425).
* or a video goes where its asset would, to a song or a composition (#425), a frame of a picture to
* a composition (#430).
*/
private startPlacementDrag(assetId: string): void {
const owner = clipOwner(this.state.project, assetId);
const owner = partOwner(this.state.project, assetId);
if (owner) {
const clip = clipsOf<{ id: string; name: string }>(owner).find((c) => c.id === assetId)!;
startDrag({ kind: 'asset', id: assetId }, clipRefName(owner.name, clip.name));
const part = partsOf(owner).find((p) => p.id === assetId)!;
startDrag({ kind: 'asset', id: assetId }, clipRefName(owner.name, part.name));
return;
}
const asset = findAsset(this.state.project, assetId);
+11
View File
@@ -128,6 +128,17 @@ export class PixelArtCanvas extends SheetCanvas<PixelArtAsset> {
this.redraw();
});
state.on('project', () => this.redraw());
state.on('pixelFrame', () => {
const shown = state.shownPixelFrame;
if (!shown) return;
if (shown.assetId === this.assetId) this.selectFrame(shown.frameId);
else
this.places.set(shown.assetId, {
layerId: '',
...this.places.get(shown.assetId),
frameId: shown.frameId,
});
});
state.on('load', () => {
this.places.clear();
this.needsFit = true;
+23 -2
View File
@@ -242,6 +242,8 @@ export type EditorEvent =
| 'open'
/** An asset should light up where it is listed (`pingedAssetId`), without being selected. */
| 'ping'
/** A frame of pixel art should be painted on (`shownPixelFrame`). */
| 'pixelFrame'
| 'mode'
| 'save'
| 'notice'
@@ -298,7 +300,9 @@ export class EditorState {
* working rather than something the project remembers.
*/
tilemapPaint: TilemapPaint = { tool: 'select', tileId: '', layerId: '', focus: 'all' };
/** The asset or clip (#425) the last `pingAsset` pointed at. */
/** The frame of pixel art the last `showPixelFrame` asked for. */
shownPixelFrame: { assetId: string; frameId: string } | null = null;
/** The asset or part of one (#425, #430) the last `pingAsset` pointed at. */
pingedAssetId: string | null = null;
/** Set while `glance` selects: the selection stays where it is shown. */
glancing = false;
@@ -581,10 +585,27 @@ export class EditorState {
* there, and like `openValues` it works without knowing the `Editor`.
*/
openAsset(assetId: string): void {
this.select({ kind: 'asset', assetId });
this.openSelection({ kind: 'asset', assetId });
}
/**
* The same for an asset or a part of one, a clip or a frame (#430). Asked for even when the selection
* is already this one, as after one click on its row: selecting it again would bring up nothing.
*/
openSelection(selection: Selection): void {
this.select(selection);
this.emit('open');
}
/**
* Asks the Pixel art tab to paint on this frame of the asset (#430), now or once it shows the asset:
* a frame is not selected, as it has nothing of its own to show in the inspector.
*/
showPixelFrame(assetId: string, frameId: string): void {
this.shownPixelFrame = { assetId, frameId };
this.emit('pixelFrame');
}
/**
* Asks the graph showing `assetId` to bring these nodes into its picture (#429): what the agent
* just added far to one side is otherwise selected out of sight.