Skip to content

Commit fa4d341

Browse files
greenfire27claude
andcommitted
Frames an animation can call by name
An image asset in explicit mode cuts its sheet into cells that each carry a RegionName, and an animation on it can list those names -- block1 block2 block3 block4 -- instead of listing 0 1 2 3. The point is that a named list survives the sheet being re-cut or re-ordered, which a numbered one does not. The engine has played these correctly for as long as they have existed. ImageFrameProviderCore branches on the mode in all four places that matter, and the runtime path from validate through play and update to the frame area is mode-aware throughout. Only AUTHORING was broken, and by one character: AnimationAsset_ScriptBinding.h formatted a StringTableEntry -- a const char* -- through "%d", so getNamedAnimationFrames returned a row of pointer addresses. Nothing that asked an animation for its named frames could recover them, so the Asset Manager refused such an asset twice over: AssetInspector sent it to the stock inspector, and AssetAnimationStage::canEdit denied it a palette and a timeline. Both refusals carried comments explaining that the API did not round trip, and both were right. NAMED CELLS MODE IS NO LONGER A FLAG ANYBODY SETS. The field, its setter, its write function and the member are gone; getNamedCellsMode() is a live read of mImageAsset->getExplicitMode(). The image already held the only honest answer, and a stored copy could disagree with it the moment that image was re-cut -- and did worse than that, since a person could set the flag true on an image with no names at all and get an animation with no frames and no explanation. Nothing has to keep the two in step now because there is only one of them. The refresh cascade already reaches here: setExplicitMode ends in refreshAsset, AssetManager::updateAssetDependencies has the edge from the animation's Image field, and the drain loop is index-based over a growing vector, so the dependent animation is dispatched in the same drain that dispatched the image. The whole editor stays in INDEX space, and that is what kept this small. The palette shows cell N, the timeline holds cell N, a drag carries cell N, the range dialog builds "28 29 30"; only loading and committing know that names exist. Every gesture, the caret arithmetic, the hold detection and the undo transaction are unchanged. The one thing index space cannot carry is a name whose cell has been deleted. It resolves to no index, and EVERY such name resolves to the same -1, so a list round-tripped through indices would come back from a single edit with two broken frames merged into one and the other silently committed away. So the timeline keeps a parallel mSlotNames, always exactly the size of mSlots: mSlots stays the drawing truth and mSlotNames the authoring truth. setFrames takes indices and derives names, setNamedFrames takes names and derives indices, and both always fill both -- which is why appendFrame, insertFrameAtPoint and the range dialog needed no changes at all. A missing frame draws as an outlined empty cell in the theme's error color with the name it could not find under it, in the same ink, and says so again in its tooltip and in the inspector's warning line. It is kept rather than dropped because dropping it is a deletion the user never asked for and could not have seen happen. Cells label themselves in both grids now, so the payoff is visible where the work is done. That is one virtual on the shared base, so the palette and the timeline cannot label the same cell differently -- which would make dragging between them a guess. Long names clip with an ellipsis and the full text is in the tooltip; the clip measures and draws the same string rather than pairing getStrNWidth with dglDrawTextN, whose counts are bytes and UTF16 units respectively and disagree the moment a name is not ASCII. EVERY EXPLICIT CELL NOW HAS A NAME, which is the invariant the rest rests on. A cell stored without one is named Frame<N>, seeded at its own index and walked past anything already taken -- not hypothetical, since deleting a cell from the middle renumbers every one after it. It happens in calculateExplicitMode, the one funnel every path ends in, because the TAML read pushes straight into mExplicitFrames and never goes near addExplicitCell. What stood there before was a "repair" in four places that could not have worked: it read dSscanf FROM the empty name INTO a U32 passed by value where a pointer was required, and never assigned a name to anything. Its guard never fired either -- it compared a console or TAML buffer against StringTable->EmptyString by POINTER, and neither is ever interned. The image editor's Add Cell button now asks the engine for the name instead of building "Frame" @ index itself with no uniqueness check, which is how adding a cell after deleting one from the middle produced a duplicate that the rename box beside it would have refused. Switching an image between explicit and cell mode converts the animations on it, so the switch is a decision rather than a commitment. Both lists are kept in memory and only the one in use is written, and the conversion runs from onAssetRefresh, setImage and initializeAsset -- NOT from validateFrames, which is where it obviously belongs and where it would have destroyed data. That is called from inside both frame setters, so setAnimationFrames("") -- what the editor sends when the timeline is emptied, and what copyFieldsFrom sends on every single copy -- would have seen an empty active list beside a full one and put the frames the user had just cleared straight back. validateFrames stays a pure derivation that touches neither specified list. initializeAsset is new here: settling this after the whole file is read is what makes the result independent of TAML field order, which mattered as soon as anything depended on Image and a frame list together. Gating the write on the mode is what closes the round trip. Both lists used to be written whenever they had content, and the named one is applied last and used to force named mode on -- so an animation given numbered frames after ever having had named ones came back from its own file named. That in turn made an older landmine reachable: onTamlCustomWrite gated the Cells node on explicit mode, so saving an image with the mode off deleted every cell in the file and with them the only thing that could ever resolve those names again. The cells are authored data that outlive the mode -- copyAssetStateTo says so in as many words -- so they are written whenever there are any. Which means the file has to state the mode out loud, because the read infers explicit mode from the presence of a Cells node and must keep doing so for every file written before this. A file that states it is believed; only a file that says nothing is inferred from. Engine defects fixed on the way, all reachable before any of this: - getExplicitCellOffset returned NULL from a Vector2-returning function when not in explicit mode, which selects Vector2(const char*), which calls setString on a null pointer and dereferences it. The image editor's swap-cells path reaches it. - all four getExplicitCell accessors indexed with Vector<T>::at, which takes a U32 and only asserts -- so it is unchecked in release and at(-1) was a read at four billion. A failed name lookup is exactly what -1 means around here. - getExplicitCellName and getExplicitCellIndex refused to answer while explicit mode was off, which is precisely when a name has to be translated back into an index. The guards are off those two; the four mutators keep theirs. - getCellByName's empty-name guard was the same pointer comparison as above, so an empty name matched the first frame of any image whose cells are unnamed. - ImageFrameProviderCore::mUsingNamedFrame and mNamedImageFrame were never initialized by the constructor and never cleared by clearAssets, and validRender reads the first on the first frame of every static sprite -- an indeterminate true then dereferences an equally indeterminate name. - getNamedAnimationFrames sized its return buffer at a fixed 4096 that suits a list of integers. A region name has no length limit and dSprintf truncates in silence, so a long animation would have lost its tail and said nothing. Both it and getMissingFrames measure first. - mValidatedNameFrames was missing its VECTOR_SET_ASSOCIATION, and the dead mAnimationIntegration field is gone. Two new bindings exist to stop script having to branch. getFrameCount answers in whichever space the animation uses; getAnimationFrameCount refuses in named mode and returns -1, which read as "fewer than one" to Keep Frame Rate and as "-1 frames" on the inspector's info line. getMissingFrames returns the names no cell answers to, which is cheaper and more honest than N console calls per refresh. animationFrameConversionTests and imageAssetCellNameTests cover the arithmetic through the statics, because building a real explicit cell needs a bitmap and a unit test has no GL context to load one into: the round trip, a hold surviving it, everything that fails to resolve in either direction, and the naming search including the collision walk and the case fold. AnimationAssetCarriesNamedFrames lost its mode assertion with the field and gained a sibling asserting that BOTH lists survive a copy, which is what makes the mode switch reversible. animationFrameValidation carries the engine half -- most valuably that getNamedAnimationFrames returns four names and not four numbers -- plus the mode switch, the auto-naming, and the file keeping its cells with the mode off. assetAnimationTimeline drives the editor half through to reading the saved file back. The shot harness gains the two pictures that only a picture settles: whether a name reads at 48 pixels, and whether a missing frame is findable. The toybox 1234 image and 1234Animation are the demo pair, and the only named assets in the tree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Xh3XULt9PtdCtsaYHGnYE
1 parent 9e4ae28 commit fa4d341

29 files changed

Lines changed: 2063 additions & 195 deletions

cmake/EngineSources.cmake

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ set(TORQUE_ENGINE_SOURCES
343343
# ---- testing ----
344344
${TORQUE_SRC}/testing/unitTesting.cc
345345
# ---- testing/tests ----
346+
${TORQUE_SRC}/testing/tests/animationFrameConversionTests.cc
346347
${TORQUE_SRC}/testing/tests/assetStateCopyTests.cc
347348
${TORQUE_SRC}/testing/tests/bitmapFontParseTests.cc
348349
${TORQUE_SRC}/testing/tests/guiControlReparentTests.cc
@@ -355,6 +356,7 @@ set(TORQUE_ENGINE_SOURCES
355356
${TORQUE_SRC}/testing/tests/guiTextEditTests.cc
356357
${TORQUE_SRC}/testing/tests/guiTextWrapTests.cc
357358
${TORQUE_SRC}/testing/tests/guiTreeRowLayoutTests.cc
359+
${TORQUE_SRC}/testing/tests/imageAssetCellNameTests.cc
358360
${TORQUE_SRC}/testing/tests/namespaceLinkTests.cc
359361
${TORQUE_SRC}/testing/tests/platformFileIoTests.cc
360362
${TORQUE_SRC}/testing/tests/platformMemoryTests.cc

editor/AssetAdmin/Animation/AssetAnimationStage.cs

Lines changed: 66 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767

6868
function AssetAnimationStage::retainFor(%this, %animationAssetId)
6969
{
70-
if(%animationAssetId $= "" || !%this.canEdit(%animationAssetId))
70+
if(%animationAssetId $= "" || !AssetDatabase.isDeclaredAsset(%animationAssetId))
7171
{
7272
%this.teardown();
7373
return false;
@@ -76,24 +76,42 @@
7676
return true;
7777
}
7878

79-
// Named cells are out of scope for now, and the reason is not squeamishness: the
80-
// engine's named-frame API does not round-trip. Its getter formats a string
81-
// through %d, and the field joins with commas while the setter splits on
82-
// whitespace, so a named list does not survive its own TAML file. Rather than
83-
// build a timeline on top of that, such an asset keeps the plain inspector and
84-
// the old single-sprite preview, which have always worked for it.
85-
function AssetAnimationStage::canEdit(%this, %animationAssetId)
79+
// Whether the animation on show addresses its frames by name.
80+
//
81+
// Asked of the asset, which asks the image: an image in explicit mode cuts itself
82+
// into named cells, so an animation on it lists names. Nothing here sets it, and
83+
// there is no flag to set -- changing the image, or that image's explicit mode,
84+
// is what changes the answer.
85+
//
86+
// The whole editor stays in INDEX space either way. The palette shows cell N, the
87+
// timeline holds cell N, a drag carries cell N; only loading and committing know
88+
// about names at all. What that buys is that every gesture, the range dialog, the
89+
// caret arithmetic and the hold detection are written once.
90+
function AssetAnimationStage::namedMode(%this)
91+
{
92+
return isObject(%this.animationAsset) && %this.animationAsset.getNamedCellsMode();
93+
}
94+
95+
// Point the timeline at the asset's frames, in whichever space they are kept.
96+
//
97+
// One method because there were three call sites that each did it slightly
98+
// differently -- selection, a refresh, and an inspector commit -- and a fourth
99+
// spelling of it was how the named case would have been missed.
100+
function AssetAnimationStage::loadTimeline(%this)
86101
{
87-
%asset = AssetDatabase.acquireAsset(%animationAssetId);
88-
if(!isObject(%asset))
102+
if(!isObject(%this.timelinePane) || !isObject(%this.animationAsset))
89103
{
90-
return false;
104+
return;
91105
}
92106

93-
%named = %asset.getNamedCellsMode();
94-
AssetDatabase.releaseAsset(%animationAssetId);
95-
96-
return !%named;
107+
if(%this.namedMode())
108+
{
109+
%this.timelinePane.loadNamed(%this.imageAssetId, trim(%this.animationAsset.getNamedAnimationFrames()));
110+
}
111+
else
112+
{
113+
%this.timelinePane.load(%this.imageAssetId, trim(%this.animationAsset.getAnimationFrames()));
114+
}
97115
}
98116

99117
function AssetAnimationStage::select(%this, %imageAsset, %animationAsset, %assetId)
@@ -124,7 +142,7 @@
124142
%this.imageAssetId = %animationAsset.getImage();
125143

126144
%this.palettePane.load(%this.imageAssetId);
127-
%this.timelinePane.load(%this.imageAssetId, trim(%animationAsset.getAnimationFrames()));
145+
%this.loadTimeline();
128146

129147
%this.admin.transportBarContainer.setVisible(true);
130148

@@ -464,14 +482,21 @@ class = "AssetAnimationPalettePane";
464482
// for a list it already holds. The same guard shape AssetInspectorPane uses.
465483
if(%isAnimation && !%this.committing)
466484
{
467-
%this.timelinePane.load(%this.imageAssetId, trim(%this.animationAsset.getAnimationFrames()));
485+
%this.loadTimeline();
468486
}
469487

470488
// The image may have been re-cut, so the palette's frame count has moved --
471489
// and so has what the animation's frames mean.
490+
//
491+
// It may also have changed explicit mode, which moves the animation between
492+
// name space and index space entirely. The asset has already converted its own
493+
// list by the time this runs -- that is what AnimationAsset::onAssetRefresh
494+
// does -- so the timeline is reloaded here as well, from whichever list is now
495+
// the live one.
472496
if(%isImage)
473497
{
474498
%this.palettePane.reload();
499+
%this.loadTimeline();
475500
}
476501

477502
%this.resyncPreview();
@@ -558,17 +583,24 @@ class = "AssetAnimationPalettePane";
558583
// place in the editor that writes the animation's frames.
559584
//-----------------------------------------------------------------------------
560585

561-
function AssetAnimationStage::commitFrames(%this, %frames)
586+
function AssetAnimationStage::commitFrames(%this)
562587
{
563-
if(!%this.built || !isObject(%this.animationAsset))
588+
if(!%this.built || !isObject(%this.animationAsset) || !isObject(%this.timelinePane))
564589
{
565590
return;
566591
}
567592

593+
%named = %this.namedMode();
594+
568595
// How many frames there were, asked of the ASSET rather than of the strip:
569596
// the strip already holds the edited list by the time it reports, so it can
570597
// no longer say what the animation used to be.
571-
%before = %this.animationAsset.getAnimationFrameCount();
598+
//
599+
// getFrameCount rather than getAnimationFrameCount, because that one refuses
600+
// to answer in named mode and returns -1 -- which reads as "fewer than one"
601+
// to keepFrameRate below, and would have silently switched that feature off
602+
// for every named animation.
603+
%before = %this.animationAsset.getFrameCount();
572604

573605
// Where the preview is, captured BEFORE the write, because the engine
574606
// restarts playback in the middle of it: AssetManager::refreshAsset notifies
@@ -586,8 +618,20 @@ class = "AssetAnimationPalettePane";
586618
// Guarded because the change comes straight back: every asset setter ends in
587619
// refreshAsset, which announces the change and fires onRefresh synchronously,
588620
// inside this call.
621+
//
622+
// The list is asked of the strip in whichever space the asset keeps it. Both
623+
// are always available -- the strip fills its index list and its name list
624+
// together, whichever one it was given -- so this is a choice of which to hand
625+
// over, not a conversion.
589626
%this.committing = true;
590-
%this.animationAsset.setAnimationFrames(%frames);
627+
if(%named)
628+
{
629+
%this.animationAsset.setNamedAnimationFrames(%this.timelinePane.strip.getNamedFrames());
630+
}
631+
else
632+
{
633+
%this.animationAsset.setAnimationFrames(%this.timelinePane.strip.getFrames());
634+
}
591635
%this.committing = false;
592636

593637
// Before the slot is forgotten, because this changes the asset a second time
@@ -657,7 +701,7 @@ class = "AssetAnimationPalettePane";
657701

658702
%this.imageAssetId = %imageAssetId;
659703
%this.palettePane.load(%imageAssetId);
660-
%this.timelinePane.load(%imageAssetId, trim(%this.animationAsset.getAnimationFrames()));
704+
%this.loadTimeline();
661705

662706
%this.admin.transportBar.refresh();
663707
}

editor/AssetAdmin/Animation/AssetAnimationTimelinePane.cs

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,25 @@
8989
%this.scroller.add(%this.strip);
9090
}
9191

92+
// The image FIRST in both of these, and that order is load bearing.
93+
//
94+
// The strip fills its index list and its name list together, and it can only do
95+
// that by asking the image what cell N is called or which cell is called N. Given
96+
// the frames before the image, every name resolves to nothing.
9297
function AssetAnimationTimelinePane::load(%this, %imageAssetId, %frames)
9398
{
9499
%this.strip.setImageAsset(%imageAssetId);
95100
%this.strip.setFrames(%frames);
96101
%this.refreshCaption();
97102
}
98103

104+
function AssetAnimationTimelinePane::loadNamed(%this, %imageAssetId, %names)
105+
{
106+
%this.strip.setImageAsset(%imageAssetId);
107+
%this.strip.setNamedFrames(%names);
108+
%this.refreshCaption();
109+
}
110+
99111
function AssetAnimationTimelinePane::setPreviewSprite(%this, %sprite)
100112
{
101113
%this.strip.setPreviewSprite(%sprite);
@@ -119,10 +131,14 @@
119131
// pane's job, and writing it is the stage's.
120132
//-----------------------------------------------------------------------------
121133

134+
// The list is no longer handed over here. The stage reads it off the strip in
135+
// whichever space the asset keeps its frames, and only the stage knows which that
136+
// is -- passing indices from here meant a named animation was committed as a row
137+
// of numbers to a setter that refuses them.
122138
function AssetAnimationTimelinePane::commitFrames(%this)
123139
{
124140
%this.refreshCaption();
125-
%this.stage.commitFrames(%this.strip.getFrames());
141+
%this.stage.commitFrames();
126142
}
127143

128144
function AssetAnimationTimelinePane::appendFrame(%this, %frame)
@@ -137,10 +153,20 @@
137153
%this.commitFrames();
138154
}
139155

156+
// Inserted one at a time rather than concatenated onto getFrames() and set back.
157+
//
158+
// The round trip through the index list was lossy once frames could be missing: a
159+
// frame whose cell has been deleted is index -1, and rebuilding the list from
160+
// indices would have turned every such frame into the same nameless hole. Adding
161+
// to the end touches nothing that is already there.
140162
function AssetAnimationTimelinePane::appendFrames(%this, %frames)
141163
{
142-
%existing = %this.strip.getFrames();
143-
%this.strip.setFrames(%existing $= "" ? %frames : (%existing SPC %frames));
164+
%count = getWordCount(%frames);
165+
for(%i = 0; %i < %count; %i++)
166+
{
167+
%this.strip.insertFrame(%this.strip.getCellCount(), getWord(%frames, %i));
168+
}
169+
144170
%this.commitFrames();
145171
}
146172

editor/AssetAdmin/AssetInspector.cs

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -737,21 +737,14 @@ class = "DuplicateAssetDialog";
737737
%this.titlebar.setText("Animation Asset:" SPC %animationAsset.AssetName);
738738
%this.beginDocument(%animationAsset);
739739

740-
// Named cells still fall back to the generic inspector.
740+
// Named cells come here too now.
741741
//
742-
// The reason they used to is now gone: NamedAnimationFrames is a
743-
// TypeStringTableEntryVector, whose getter joins with commas, and
744-
// setNamedAnimationFrames split on whitespace alone -- so a named list did not
745-
// survive its own TAML file, and a pane built on it would have quietly lost
746-
// work. The setter accepts commas now (AnimationAsset.cc), so a named-cells
747-
// pane is buildable. It is simply not built yet, which is a job of its own
748-
// rather than a hazard.
749-
if(%animationAsset.getNamedCellsMode())
750-
{
751-
%this.inspectStock(%animationAsset);
752-
return;
753-
}
754-
742+
// They used to fall through to the generic inspector, and there were two good
743+
// reasons at the time: setNamedAnimationFrames split on whitespace while the
744+
// field joined with commas, so a named list did not survive its own TAML file;
745+
// and getNamedAnimationFrames formatted a StringTableEntry through %d, so what
746+
// came back was a row of pointers. Both are fixed, and the pane reads a named
747+
// animation the same way it reads a numbered one.
755748
%this.chooseInspector("Animation");
756749
%this.animationPane.bind(%animationAsset, %assetID);
757750
}

editor/AssetAdmin/ImageEditor/AssetImageFrameEditTool.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,18 +141,29 @@
141141
%this.startListening(%row);
142142
}
143143

144+
// The name is the engine's to choose, and then read back.
145+
//
146+
// This used to build "Frame" @ %index itself, with no uniqueness check at all --
147+
// so adding a cell after deleting one from the middle produced a second cell
148+
// with a name that already existed, which onCellNameChange right below would have
149+
// refused had a person typed it. The engine names an unnamed cell on the way
150+
// through calculateExplicitMode, walking past any name already taken, and that is
151+
// now the only place the rule lives.
144152
function AssetImageFrameEditTool::addNewCell(%this)
145153
{
146154
%index = %this.asset.getExplicitCellCount();
147-
%name = "Frame" @ %index;
148155
%x = 0;
149156
%y = 0;
150157
%width = %this.asset.getImageWidth();
151158
%height = %this.asset.getImageHeight();
152159

153160
%this.rowChain.callOnChildrenNoRecurse("updateCellCount", %index + 1);
154161

155-
%this.asset.addExplicitCell(%x, %y, %width, %height, %name);
162+
// addExplicitCell refreshes the asset before it returns, so the name it picked
163+
// is there to be read on the next line.
164+
%this.asset.addExplicitCell(%x, %y, %width, %height, "");
165+
%name = %this.asset.getExplicitCellName(%index);
166+
156167
%this.addImageFrameRow(%name, %x SPC %y, %width, %height, %index);
157168
}
158169

editor/AssetAdmin/Inspector/AssetAnimationInspectorPane.cs

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,14 @@
3030
// the one that is only a box cannot say which frame 67 is.
3131
//
3232
// Also absent, each for a checkable reason:
33-
// NamedAnimationFrames, NamedCellsMode v1 is numeric, and the engine's named
34-
// frame API does not round-trip through
35-
// its own file -- so this pane is never
36-
// shown for such an asset rather than
37-
// offering a switch into it
33+
// NamedAnimationFrames the same field as AnimationFrames, in
34+
// name space, and the timeline owns that
35+
// one too
36+
// NamedCellsMode not a field at all any more. Whether an
37+
// animation names its frames is read from
38+
// the image -- explicit mode means named
39+
// cells -- so a switch here would be a
40+
// second, disagreeing answer
3841
// AssetInternal, AssetPrivate they exist to keep an asset out of the
3942
// editor
4043
// asset id, asset file the module and the name are on show,
@@ -206,7 +209,10 @@
206209
// answer separately: how long, how many, and therefore how fast.
207210
function AssetAnimationInspectorPane::describeAnimation(%this, %asset)
208211
{
209-
%count = %asset.getAnimationFrameCount();
212+
// getFrameCount, not getAnimationFrameCount: that one refuses to answer for an
213+
// animation using named cells and returns -1, which read as "-1 frames" on
214+
// this very line.
215+
%count = %asset.getFrameCount();
210216
%time = %asset.getAnimationTime();
211217

212218
%line = %count SPC ((%count == 1) ? "frame," : "frames,") SPC %time SPC "s";
@@ -253,18 +259,35 @@
253259
return "The image asset" SPC %imageId SPC "did not load, so there is nothing to play.";
254260
}
255261

262+
// The two spaces fail differently, so they are reported differently.
263+
//
264+
// A named frame that no cell answers to is simply not drawn, and the timeline
265+
// keeps it as an outlined gap -- so the useful thing to say is WHICH names,
266+
// because the fix is either to put the cell back or to take the frame out.
267+
if(%asset.getNamedCellsMode())
268+
{
269+
%missing = trim(%asset.getMissingFrames());
270+
if(%missing !$= "")
271+
{
272+
%plural = (getWordCount(%missing) == 1);
273+
return (%plural ? "The frame" : "The frames") SPC "\"" @ %missing @ "\"" SPC
274+
(%plural ? "names a cell" : "name cells") SPC "the image no longer has, so" SPC
275+
(%plural ? "it draws" : "they draw") SPC "nothing. Put the cell back on the " @
276+
"Explicit Frames tab, or take the frame out of the timeline.";
277+
}
278+
}
279+
// A numbered frame out of range is CLAMPED to the last one rather than
280+
// dropped, so the animation keeps playing and quietly shows the wrong art.
256281
// Specified against validated is the only comparison script can make, and it
257-
// is exactly the right one: validateNumericalFrames CLAMPS an out-of-range
258-
// frame to the last one rather than dropping it, so the animation keeps
259-
// playing and quietly shows the wrong art.
260-
if(trim(%asset.getAnimationFrames()) !$= trim(%asset.getAnimationFrames(true)))
282+
// is exactly the right one.
283+
else if(trim(%asset.getAnimationFrames()) !$= trim(%asset.getAnimationFrames(true)))
261284
{
262285
return "Some frames are outside the image's" SPC %image.getFrameCount() SPC
263286
"and are being clamped to the nearest one. The timeline shows what was asked for; " @
264287
"the preview shows what is being drawn.";
265288
}
266289

267-
if(%asset.getAnimationFrameCount() == 0)
290+
if(%asset.getFrameCount() == 0)
268291
{
269292
return "This animation has no frames yet. Drag one in from the palette, or use Frame Range.";
270293
}

editor/EditorCore/Themes/BaseTheme/BaseTheme.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1931,6 +1931,14 @@
19311931
fontColorHL = %this.color4;
19321932
fontColorSL = %this.color5;
19331933

1934+
// Errors, in the sense the console profile uses this slot for. The
1935+
// timeline outlines a frame naming a cell the image no longer has, and this
1936+
// is the color of that outline and of its label. The four FILL colors are
1937+
// all spoken for -- background, hover, selected, and about-to-be-discarded
1938+
// during a drag -- which is why a missing frame is a border rather than a
1939+
// wash.
1940+
fontColorNA = "255 0 0 255";
1941+
19341942
// The Delete key only reaches a control that can hold focus, and the
19351943
// timeline's whole keyboard depends on it.
19361944
canKeyFocus = true;

0 commit comments

Comments
 (0)