mirror of
https://github.com/vegu-ai/talemate.git
synced 2026-08-29 10:08:58 +02:00
scene owns the backdrop asset selection
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
0.39.0.dev:
|
||||
features:
|
||||
- "Scene Illustration Background Mode: Scene illustrations gained a new 'Background' display size that renders the most recent illustration as a backdrop filling the whole scene view instead of showing it inline with messages. Message text sits on translucent blurred panels with a drop shadow, and the message input, control chips and buttons turn opaque so everything stays legible against the image; panel opacity and the text shadow are configurable. Scene Backgrounds ('Visualize Scene (Background)') and Scene Illustrations ('Visualize Moment') are individually configurable, so environmental backgrounds can fill the backdrop while moment illustrations keep rendering inline — when both use Background, the most recent image is the active backdrop. An 'Immersive' quick-toggle chip in the scene tools flips the mode whenever the scene has a suitable background image, a small marker icon shows which message set the current backdrop (click it for the image menu), and an Illustration chip on the message hover toolbar keeps the image menu (view, regenerate, edit, select) reachable. Configure under Settings → Appearance → Message Visuals."
|
||||
- "Scene Backdrop: Any scene illustration can now be set as the scene backdrop — an image that fills the whole scene view behind the messages instead of rendering inline. The backdrop belongs to the scene and is saved with it, so it survives reloads and history edits. Set it via 'Set as scene backdrop' on any illustration's image menu, the 'Set backdrop' button in the Visual Library, or enable 'Auto Backdrop' per visual type (Settings → Appearance → Message Visuals) to have newly generated Scene Backgrounds ('Visualize Scene (Background)') and/or Scene Illustrations ('Visualize Moment') promoted automatically. An 'Immersive' quick-toggle chip in the scene tools turns the backdrop on and off without forgetting the chosen image. Message text sits on translucent panels with a drop shadow for legibility — panel opacity and the text shadow are configurable — and a small marker icon shows which message's image is the current backdrop (click it for the image menu, which stays reachable via an Illustration chip on the message hover toolbar)."
|
||||
- "Visual Prompt Finalization: The Visualizer agent gained a Prompt Finalization settings tab defining post-processing actions (exact, fuzzy or regex match and replace, or an AI instruction) that rewrite image prompts right before they are sent to the image generation backend. Actions can target positive and/or negative prompts, be restricted to specific visual types, and be overridden per scene. Characters can define their own actions under World Editor → Characters → Visuals → Prompt Finalization, which run after the agent's. Reusable action sets are managed as a new 'Visual prompt finalizer' template type, including a shipped Ideogram JSON preset that converts the positive prompt into an Ideogram 4.0 structured JSON prompt. Prompt-only generation output is finalized as well, and a new FinalizePrompt node exposes the step to custom node graphs."
|
||||
improvements:
|
||||
- "Uniform Settings Framework: Agent settings and client settings now share one field-definition schema on the backend and one field renderer on the frontend. Client-specific settings gain the full widget set previously exclusive to agents (sliders, autocompletes, selects with rich choices, per-value notes) plus conditional visibility, and choice lists are delivered to the frontend in a single normalized shape."
|
||||
@@ -9,7 +9,7 @@
|
||||
- "OpenRouter Client: A failed model/provider list fetch at startup no longer sticks until the server is restarted — later config saves and client status refreshes now retry it. Setting the API key for the first time during initial setup also correctly triggers the provider fetch."
|
||||
- "Generation Error Dialog: When several generations failed at the same time (e.g. an API throttle hitting both a summarization and a background task), only the most recent error could be answered — the earlier generation was never resumed and its result (such as a scene summary) was silently lost. Error dialogs are now queued and answered one after another, and pending dialogs are cancelled cleanly when the scene is unloaded or the frontend disconnects."
|
||||
|
||||
0.38.0.dev:
|
||||
0.38.0:
|
||||
features:
|
||||
- "Event Module Auto-Register: Event Modules gained an `auto_register` toggle so a module subscribes to its event as soon as it's registered with the scene, without having to be placed in the scene loop graph."
|
||||
- "Per-Scene Agent Overrides: Agent configuration can now be overridden per scene without changing the global config. The Agent Modal gains a Global / Scene mode switch for toggling and editing overrides; choose, swap, or opt out of the override file under World Editor → Scene → Settings."
|
||||
|
||||
@@ -595,15 +595,18 @@ class MarkupMessageStyle(HistoryMessageStyle):
|
||||
|
||||
class MessageAssetCadenceConfig(pydantic.BaseModel):
|
||||
cadence: Literal["always", "never", "on_change"] = "always"
|
||||
# "background" is only meaningful for scene_illustration and
|
||||
# scene_background assets
|
||||
size: Literal["small", "medium", "big", "background"] = "medium"
|
||||
# opacity of the text panels rendered over the backdrop ("background"
|
||||
# size mode only)
|
||||
background_panel_opacity: float = pydantic.Field(default=0.8, ge=0.0, le=1.0)
|
||||
# drop shadow on message text over the backdrop ("background" size mode
|
||||
# only)
|
||||
background_text_shadow: bool = True
|
||||
size: Literal["small", "medium", "big"] = "medium"
|
||||
# automatically promote newly generated assets of this kind to the scene
|
||||
# backdrop (only meaningful for scene_illustration and scene_background)
|
||||
auto_backdrop: bool = False
|
||||
|
||||
|
||||
def default_message_asset_config(kind: str) -> MessageAssetCadenceConfig:
|
||||
# scene backgrounds are environmental full-frame images — full width
|
||||
# by default
|
||||
if kind == "scene_background":
|
||||
return MessageAssetCadenceConfig(size="big")
|
||||
return MessageAssetCadenceConfig()
|
||||
|
||||
|
||||
class SceneAppearance(pydantic.BaseModel):
|
||||
@@ -625,12 +628,17 @@ class SceneAppearance(pydantic.BaseModel):
|
||||
# individually configurable
|
||||
message_assets: Dict[str, MessageAssetCadenceConfig] = pydantic.Field(
|
||||
default_factory=lambda: {
|
||||
kind: MessageAssetCadenceConfig() for kind in MESSAGE_ASSET_KINDS
|
||||
kind: default_message_asset_config(kind) for kind in MESSAGE_ASSET_KINDS
|
||||
}
|
||||
)
|
||||
|
||||
auto_attach_assets: bool = True
|
||||
|
||||
# opacity of the message text panels rendered over the scene backdrop
|
||||
backdrop_panel_opacity: float = pydantic.Field(default=0.8, ge=0.0, le=1.0)
|
||||
# drop shadow on message text over the scene backdrop
|
||||
backdrop_text_shadow: bool = True
|
||||
|
||||
@pydantic.field_validator("message_assets", mode="after")
|
||||
@classmethod
|
||||
def ensure_default_message_asset_entries(cls, value):
|
||||
@@ -638,7 +646,7 @@ class SceneAppearance(pydantic.BaseModel):
|
||||
# in without it — fill the gaps so consumers can rely on all keys
|
||||
for key in MESSAGE_ASSET_KINDS:
|
||||
if key not in value:
|
||||
value[key] = MessageAssetCadenceConfig()
|
||||
value[key] = default_message_asset_config(key)
|
||||
return value
|
||||
|
||||
|
||||
|
||||
@@ -373,8 +373,11 @@ async def load_scene_from_data(
|
||||
scene.layered_history = []
|
||||
scene.intent_state.reset()
|
||||
|
||||
scene.assets.cover_image = scene_data.get("assets", {}).get("cover_image", None)
|
||||
scene.assets.load_assets(scene_data.get("assets", {}).get("assets", {}))
|
||||
asset_data = scene_data.get("assets", {})
|
||||
scene.assets.cover_image = asset_data.get("cover_image", None)
|
||||
scene.assets.backdrop = asset_data.get("backdrop", None)
|
||||
scene.assets.backdrop_enabled = asset_data.get("backdrop_enabled", True)
|
||||
scene.assets.load_assets(asset_data.get("assets", {}))
|
||||
|
||||
# Clean up cover images and message avatars that reference non-existent assets
|
||||
scene.assets.cleanup_cover_images()
|
||||
|
||||
@@ -63,6 +63,13 @@ VIS_TYPE_TO_ASSET_TYPE = {
|
||||
VIS_TYPE.UNSPECIFIED: None,
|
||||
}
|
||||
|
||||
# vis_types eligible for the scene backdrop, mapped to their
|
||||
# appearance.scene.message_assets config entry
|
||||
VIS_TYPE_TO_MESSAGE_ASSET_KIND = {
|
||||
VIS_TYPE.SCENE_BACKGROUND: "scene_background",
|
||||
VIS_TYPE.SCENE_ILLUSTRATION: "scene_illustration",
|
||||
}
|
||||
|
||||
|
||||
def validate_image_data_url(image_data: str) -> None:
|
||||
"""
|
||||
@@ -373,6 +380,16 @@ async def _handle_asset_saved(payload: AssetSavedPayload):
|
||||
message_ids=asset_attachment_context.message_ids,
|
||||
)
|
||||
|
||||
# scene backdrop auto-promotion — a newly generated scene illustration /
|
||||
# background becomes the backdrop when its kind's appearance config opts
|
||||
# in; backdrop_enabled is left untouched so an explicit "Immersive" off
|
||||
# isn't overridden
|
||||
|
||||
if payload.new_asset:
|
||||
kind = VIS_TYPE_TO_MESSAGE_ASSET_KIND.get(asset.meta.vis_type)
|
||||
if kind and config.appearance.scene.message_assets[kind].auto_backdrop:
|
||||
await scene.assets.set_scene_backdrop(asset_id=asset.id)
|
||||
|
||||
# cover image (scene and character)
|
||||
|
||||
if asset_attachment_context.scene_cover:
|
||||
@@ -434,6 +451,10 @@ class SceneAssets:
|
||||
self.scene = scene
|
||||
self._assets_cache = None
|
||||
self.cover_image = None
|
||||
# scene backdrop: which asset renders behind the scene text, and
|
||||
# whether it currently renders at all
|
||||
self.backdrop: str | None = None
|
||||
self.backdrop_enabled: bool = True
|
||||
|
||||
def _signal_asset_saved(
|
||||
self,
|
||||
@@ -586,12 +607,16 @@ class SceneAssets:
|
||||
def dict(self, *args, **kwargs):
|
||||
return {
|
||||
"cover_image": self.cover_image,
|
||||
"backdrop": self.backdrop,
|
||||
"backdrop_enabled": self.backdrop_enabled,
|
||||
"assets": {asset.id: asset.model_dump() for asset in self.assets.values()},
|
||||
}
|
||||
|
||||
def scene_info(self) -> dict:
|
||||
return {
|
||||
"cover_image": self.cover_image,
|
||||
"backdrop": self.backdrop,
|
||||
"backdrop_enabled": self.backdrop_enabled,
|
||||
}
|
||||
|
||||
def load_assets(self, assets_dict: dict):
|
||||
@@ -942,13 +967,22 @@ class SceneAssets:
|
||||
|
||||
def cleanup_cover_images(self) -> bool:
|
||||
"""
|
||||
Checks character cover images and the scene cover image and if they
|
||||
no longer exist as assets, unsets them.
|
||||
Checks character cover images, the scene cover image and the scene
|
||||
backdrop and if they no longer exist as assets, unsets them.
|
||||
|
||||
Returns True if any cover images were cleaned up, False otherwise.
|
||||
Returns True if anything was cleaned up, False otherwise.
|
||||
"""
|
||||
cleaned = False
|
||||
|
||||
# Check scene backdrop
|
||||
if self.backdrop and not self.validate_asset_id(self.backdrop):
|
||||
log.debug(
|
||||
"Cleaning up scene backdrop",
|
||||
asset_id=self.backdrop,
|
||||
)
|
||||
self.backdrop = None
|
||||
cleaned = True
|
||||
|
||||
# Check scene cover image
|
||||
if self.cover_image and not self.validate_asset_id(self.cover_image):
|
||||
log.debug(
|
||||
@@ -1632,6 +1666,31 @@ class SceneAssets:
|
||||
|
||||
return asset_id
|
||||
|
||||
async def set_scene_backdrop(
|
||||
self, asset_id: str | None = None, enabled: bool | None = None
|
||||
) -> str | None:
|
||||
"""
|
||||
Updates the scene backdrop.
|
||||
|
||||
Either argument may be omitted to leave that aspect untouched:
|
||||
asset_id selects which asset renders behind the scene text,
|
||||
enabled toggles whether it renders at all.
|
||||
"""
|
||||
log.debug("set_scene_backdrop", asset_id=asset_id, enabled=enabled)
|
||||
if asset_id is not None:
|
||||
if not self.validate_asset_id(asset_id):
|
||||
log.error("Invalid asset id", asset_id=asset_id)
|
||||
return None
|
||||
self.backdrop = asset_id
|
||||
if enabled is not None:
|
||||
self.backdrop_enabled = enabled
|
||||
|
||||
# scene status carries the backdrop state to the frontend
|
||||
if self.scene.active:
|
||||
self.scene.emit_status()
|
||||
|
||||
return self.backdrop
|
||||
|
||||
async def set_character_cover_image_from_bytes(
|
||||
self, character: "Character", bytes: bytes, override: bool = False
|
||||
) -> str:
|
||||
|
||||
@@ -46,6 +46,11 @@ class SetSceneCoverImagePayload(pydantic.BaseModel):
|
||||
asset_id: str
|
||||
|
||||
|
||||
class SetSceneBackdropPayload(pydantic.BaseModel):
|
||||
asset_id: str | None = None
|
||||
enabled: bool | None = None
|
||||
|
||||
|
||||
class SetCharacterCoverImagePayload(pydantic.BaseModel):
|
||||
asset_id: str
|
||||
character_name: str
|
||||
@@ -219,6 +224,30 @@ class SceneAssetsPlugin(Plugin):
|
||||
log.error("set_scene_cover_image_failed", error=e)
|
||||
await self.signal_operation_failed(f"Failed to set scene cover image: {e}")
|
||||
|
||||
async def handle_set_scene_backdrop(self, data: dict):
|
||||
payload = SetSceneBackdropPayload(**data)
|
||||
|
||||
try:
|
||||
if payload.asset_id is not None and not self.scene.assets.validate_asset_id(
|
||||
payload.asset_id
|
||||
):
|
||||
await self.signal_operation_failed("Invalid asset_id")
|
||||
return
|
||||
|
||||
await self.scene.assets.set_scene_backdrop(
|
||||
asset_id=payload.asset_id, enabled=payload.enabled
|
||||
)
|
||||
|
||||
# Request the asset for frontend
|
||||
if payload.asset_id:
|
||||
self.websocket_handler.request_scene_assets([payload.asset_id])
|
||||
|
||||
await self.scene.attempt_auto_save()
|
||||
await self.signal_operation_done()
|
||||
except Exception as e:
|
||||
log.error("set_scene_backdrop_failed", error=e)
|
||||
await self.signal_operation_failed(f"Failed to set scene backdrop: {e}")
|
||||
|
||||
async def handle_set_character_cover_image(self, data: dict):
|
||||
payload = SetCharacterCoverImagePayload(**data)
|
||||
asset_id = payload.asset_id
|
||||
|
||||
@@ -67,6 +67,13 @@ export default {
|
||||
}
|
||||
config.scene.auto_attach_assets = this.$refs.assets.get_auto_attach_assets();
|
||||
}
|
||||
// Include shared backdrop legibility settings from Assets component
|
||||
if(this.$refs.assets && this.$refs.assets.get_backdrop_settings) {
|
||||
if(!config.scene) {
|
||||
config.scene = {};
|
||||
}
|
||||
Object.assign(config.scene, this.$refs.assets.get_backdrop_settings());
|
||||
}
|
||||
return config;
|
||||
},
|
||||
onChildChanged() {
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
<th class="text-left" style="padding: 8px 12px;">Visual Type</th>
|
||||
<th class="text-left" style="padding: 8px 12px;">Render Cadence</th>
|
||||
<th class="text-left" style="padding: 8px 12px;">Display Size</th>
|
||||
<th class="text-left" style="padding: 8px 12px;">Auto Backdrop</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -58,6 +59,7 @@
|
||||
style="max-width: 200px;"
|
||||
></v-select>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 12px;">
|
||||
@@ -86,6 +88,7 @@
|
||||
style="max-width: 200px;"
|
||||
></v-select>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 12px;">
|
||||
@@ -107,13 +110,21 @@
|
||||
<td style="padding: 4px 12px;">
|
||||
<v-select
|
||||
v-model="config.scene_illustration.size"
|
||||
:items="sceneIllustrationSizeOptions"
|
||||
:items="sizeOptions"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
style="max-width: 200px;"
|
||||
></v-select>
|
||||
</td>
|
||||
<td style="padding: 4px 12px;">
|
||||
<v-checkbox
|
||||
v-model="config.scene_illustration.auto_backdrop"
|
||||
color="primary"
|
||||
density="compact"
|
||||
hide-details
|
||||
></v-checkbox>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 12px;">
|
||||
@@ -135,22 +146,30 @@
|
||||
<td style="padding: 4px 12px;">
|
||||
<v-select
|
||||
v-model="config.scene_background.size"
|
||||
:items="sceneIllustrationSizeOptions"
|
||||
:items="sizeOptions"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
style="max-width: 200px;"
|
||||
></v-select>
|
||||
</td>
|
||||
<td style="padding: 4px 12px;">
|
||||
<v-checkbox
|
||||
v-model="config.scene_background.auto_backdrop"
|
||||
color="primary"
|
||||
density="compact"
|
||||
hide-details
|
||||
></v-checkbox>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
|
||||
<v-row v-for="kind in backgroundConfiguredKinds" :key="kind" class="mt-3">
|
||||
<v-row class="mt-3">
|
||||
<v-col cols="12" md="6">
|
||||
<v-slider
|
||||
v-model="config[kind].background_panel_opacity"
|
||||
:label="`Message panel opacity (${kindLabels[kind]})`"
|
||||
v-model="backdropPanelOpacity"
|
||||
label="Backdrop message panel opacity"
|
||||
color="primary"
|
||||
:min="0"
|
||||
:max="1"
|
||||
@@ -162,8 +181,8 @@
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-checkbox
|
||||
v-model="config[kind].background_text_shadow"
|
||||
:label="`Message text shadow (${kindLabels[kind]})`"
|
||||
v-model="backdropTextShadow"
|
||||
label="Backdrop message text shadow"
|
||||
color="primary"
|
||||
density="compact"
|
||||
hide-details
|
||||
@@ -178,7 +197,8 @@
|
||||
<strong>Never:</strong> Never show visual inline with messages<br>
|
||||
<strong>On change:</strong> Only show when visual changes (portraits: tracked per character)<br><br>
|
||||
<strong>Scene Illustration</strong> covers images of the current moment ("Visualize Moment"), <strong>Scene Background</strong> covers purely environmental images ("Visualize Scene (Background)").<br>
|
||||
<strong>Sizes:</strong> Big = full width above message, Small/Medium = inline with text, Background = fills behind the scene text. When both types use Background, the most recent image is the active backdrop.
|
||||
<strong>Sizes:</strong> Big = full width above message, Small/Medium = inline with text.<br>
|
||||
<strong>Auto Backdrop:</strong> Newly generated images of this type automatically become the scene backdrop (rendered behind the scene text). Any illustration can also be set as the backdrop manually via its image menu, and the scene-tools Immersive chip toggles the backdrop on and off.
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
@@ -186,8 +206,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { BACKDROP_ASSET_KINDS } from '@/constants/visual';
|
||||
|
||||
function defaultAssetConfig() {
|
||||
return {
|
||||
avatar: {
|
||||
@@ -201,14 +219,12 @@ function defaultAssetConfig() {
|
||||
scene_illustration: {
|
||||
cadence: 'always',
|
||||
size: 'medium',
|
||||
background_panel_opacity: 0.8,
|
||||
background_text_shadow: true,
|
||||
auto_backdrop: false,
|
||||
},
|
||||
scene_background: {
|
||||
cadence: 'always',
|
||||
size: 'medium',
|
||||
background_panel_opacity: 0.8,
|
||||
background_text_shadow: true,
|
||||
size: 'big',
|
||||
auto_backdrop: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -223,11 +239,9 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
autoAttachAssets: true,
|
||||
backdropPanelOpacity: 0.8,
|
||||
backdropTextShadow: true,
|
||||
config: defaultAssetConfig(),
|
||||
kindLabels: {
|
||||
scene_illustration: 'Scene Illustration',
|
||||
scene_background: 'Scene Background',
|
||||
},
|
||||
cadenceOptions: [
|
||||
{ title: 'Always', value: 'always' },
|
||||
{ title: 'Never', value: 'never' },
|
||||
@@ -245,19 +259,6 @@ export default {
|
||||
isHydrating: false, // Flag to suppress changed events during initialization
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// "background" only makes sense for scene illustrations/backgrounds
|
||||
sceneIllustrationSizeOptions() {
|
||||
return [...this.sizeOptions, { title: 'Background', value: 'background' }];
|
||||
},
|
||||
// kinds currently set to the Background display size — each gets its
|
||||
// own panel-opacity / text-shadow controls
|
||||
backgroundConfiguredKinds() {
|
||||
return BACKDROP_ASSET_KINDS.filter(
|
||||
kind => this.config[kind].size === 'background'
|
||||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
immutableConfig: {
|
||||
handler: function(newVal) {
|
||||
@@ -275,6 +276,10 @@ export default {
|
||||
|
||||
// Load auto_attach_assets setting
|
||||
this.autoAttachAssets = sceneConfig.auto_attach_assets !== undefined ? sceneConfig.auto_attach_assets : true;
|
||||
|
||||
// Backdrop legibility settings
|
||||
this.backdropPanelOpacity = sceneConfig.backdrop_panel_opacity ?? 0.8;
|
||||
this.backdropTextShadow = sceneConfig.backdrop_text_shadow ?? true;
|
||||
|
||||
// Overlay stored values onto the defaults (?? so 0 / false
|
||||
// survive)
|
||||
@@ -312,6 +317,20 @@ export default {
|
||||
}
|
||||
},
|
||||
},
|
||||
backdropPanelOpacity: {
|
||||
handler: function(newVal, oldVal) {
|
||||
if (oldVal !== undefined && !this.isHydrating) {
|
||||
this.$emit('changed');
|
||||
}
|
||||
},
|
||||
},
|
||||
backdropTextShadow: {
|
||||
handler: function(newVal, oldVal) {
|
||||
if (oldVal !== undefined && !this.isHydrating) {
|
||||
this.$emit('changed');
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// Expose config for parent component
|
||||
@@ -322,6 +341,13 @@ export default {
|
||||
get_auto_attach_assets() {
|
||||
return this.autoAttachAssets;
|
||||
},
|
||||
// Expose shared backdrop legibility settings for parent component
|
||||
get_backdrop_settings() {
|
||||
return {
|
||||
backdrop_panel_opacity: this.backdropPanelOpacity,
|
||||
backdrop_text_shadow: this.backdropTextShadow,
|
||||
};
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -19,6 +19,8 @@ export default {
|
||||
// Provided by SceneMessages — splits scene_illustration assets into the
|
||||
// scene_background / scene_illustration config entries by vis_type
|
||||
resolveMessageAssetConfigKey: { default: null },
|
||||
// Provided by SceneMessages — asset id of the active scene backdrop
|
||||
getSceneBackdropAssetId: { default: null },
|
||||
},
|
||||
computed: {
|
||||
messageAssetDisplaySize() {
|
||||
@@ -35,15 +37,17 @@ export default {
|
||||
isSceneIllustrationAbove() {
|
||||
return this.assetType === 'scene_illustration' && this.messageAssetDisplaySize === 'big';
|
||||
},
|
||||
// "background" mode: the illustration is rendered as a backdrop behind the
|
||||
// scene text (handled by SceneMessages), not inline with the message
|
||||
// The illustration is the active scene backdrop, rendered behind the
|
||||
// scene text (painted by TalemateApp), not inline with the message
|
||||
isSceneIllustrationBackground() {
|
||||
return this.assetType === 'scene_illustration' && this.messageAssetDisplaySize === 'background';
|
||||
return this.assetType === 'scene_illustration' &&
|
||||
!!this.assetId &&
|
||||
this.getSceneBackdropAssetId?.() === this.assetId;
|
||||
},
|
||||
// In background mode there is no inline image to click, so the message
|
||||
// toolbar offers a chip to reach the asset menu instead
|
||||
// The backdrop has no inline image to click, so the message toolbar
|
||||
// offers a chip to reach the asset menu instead
|
||||
illustrationMenuAvailable() {
|
||||
return this.isSceneIllustrationBackground && !!this.assetId;
|
||||
return this.isSceneIllustrationBackground;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -155,8 +155,8 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// offer the illustration asset menu (used in "background" display mode,
|
||||
// where no inline image is rendered to click on)
|
||||
// offer the illustration asset menu (used when the message's image is
|
||||
// the active scene backdrop, where no inline image is rendered to click on)
|
||||
showIllustrationMenu: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
||||
@@ -115,6 +115,16 @@
|
||||
Choose from existing scene illustrations
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
v-if="assetMenu.context.asset_type === 'scene_illustration'"
|
||||
prepend-icon="mdi-image-area"
|
||||
@click="handleSetSceneBackdrop"
|
||||
>
|
||||
<v-list-item-title>Set as scene backdrop</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-wrap">
|
||||
Render this image behind the scene text
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-divider v-if="assetMenu.context.asset_type === 'card'"></v-divider>
|
||||
<v-list-item
|
||||
v-if="assetMenu.context.asset_type === 'card'"
|
||||
@@ -599,7 +609,7 @@ export default {
|
||||
// cleared on the editor `operation_done` envelope, which has no id)
|
||||
revisionPendingId: null,
|
||||
// fallback image viewer for asset-menu "View Image" when the menu
|
||||
// wasn't opened from a MessageAssetImage (background display mode);
|
||||
// wasn't opened from a MessageAssetImage (backdrop indicator);
|
||||
// tracks the asset id so the src resolves reactively from the
|
||||
// cache (the asset may still be in flight when the dialog opens)
|
||||
assetViewShow: false,
|
||||
@@ -611,31 +621,30 @@ export default {
|
||||
return this.appearanceConfig?.scene?.message_assets || null;
|
||||
},
|
||||
sceneBackdropPanelOpacity() {
|
||||
const kind = this.sceneBackdrop?.kind || 'scene_illustration';
|
||||
return this.messageAssetsConfig?.[kind]?.background_panel_opacity ?? 0.8;
|
||||
return this.appearanceConfig?.scene?.backdrop_panel_opacity ?? 0.8;
|
||||
},
|
||||
sceneBackdropTextShadow() {
|
||||
const kind = this.sceneBackdrop?.kind || 'scene_illustration';
|
||||
return this.messageAssetsConfig?.[kind]?.background_text_shadow ?? true;
|
||||
return this.appearanceConfig?.scene?.backdrop_text_shadow ?? true;
|
||||
},
|
||||
assetViewSrc() {
|
||||
return this.assetDataUrl(this.assetViewAssetId);
|
||||
},
|
||||
sceneBackdrop() {
|
||||
// Most recent illustration whose config entry (scene_background
|
||||
// or scene_illustration, resolved by vis_type) is in "background"
|
||||
// display mode becomes the backdrop
|
||||
for (let i = this.messages.length - 1; i >= 0; i--) {
|
||||
const msg = this.messages[i];
|
||||
if (msg.asset_type !== 'scene_illustration' || !msg.asset_id) {
|
||||
continue;
|
||||
}
|
||||
const kind = this.messageAssetConfigKey(msg.asset_id, msg.asset_type);
|
||||
if (this.messageAssetsConfig?.[kind]?.size === 'background') {
|
||||
return { assetId: msg.asset_id, messageId: msg.id, kind };
|
||||
}
|
||||
// The scene object is the source of truth: assets.backdrop names
|
||||
// the asset, assets.backdrop_enabled gates rendering. messageId
|
||||
// (when the asset is message-attached) drives the indicator icon
|
||||
const assets = this.scene?.data?.assets;
|
||||
if (!assets?.backdrop || !assets.backdrop_enabled) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
const assetId = assets.backdrop;
|
||||
const message = this.messages.find(
|
||||
(msg) => msg.asset_id === assetId && msg.asset_type === 'scene_illustration'
|
||||
);
|
||||
return {
|
||||
assetId,
|
||||
messageId: message?.id ?? null,
|
||||
};
|
||||
},
|
||||
sceneBackdropAssetId() {
|
||||
return this.sceneBackdrop?.assetId || null;
|
||||
@@ -643,14 +652,17 @@ export default {
|
||||
sceneBackdropSrc() {
|
||||
return this.assetDataUrl(this.sceneBackdropAssetId);
|
||||
},
|
||||
// Whether the scene has any message-attached asset the scene-tools
|
||||
// "Immersive" chip could promote to a backdrop (independent of the
|
||||
// current display mode)
|
||||
sceneBackdropCandidateAvailable() {
|
||||
return this.messages.some(
|
||||
(msg) => msg.asset_id && msg.asset_type === 'scene_illustration' &&
|
||||
this.messageAssetConfigKey(msg.asset_id, msg.asset_type) === 'scene_background'
|
||||
);
|
||||
// Most recent message-attached scene background the scene-tools
|
||||
// "Immersive" chip could promote to a backdrop when none is set yet
|
||||
sceneBackdropCandidateAssetId() {
|
||||
for (let i = this.messages.length - 1; i >= 0; i--) {
|
||||
const msg = this.messages[i];
|
||||
if (msg.asset_id && msg.asset_type === 'scene_illustration' &&
|
||||
this.messageAssetConfigKey(msg.asset_id, msg.asset_type) === 'scene_background') {
|
||||
return msg.asset_id;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
editorRevisionsEnabled() {
|
||||
return this.agentStatus && this.agentStatus.editor && this.agentStatus.editor.actions && this.agentStatus.editor.actions["revision"] && this.agentStatus.editor.actions["revision"].enabled;
|
||||
@@ -724,6 +736,9 @@ export default {
|
||||
visualizeMessage: this.visualizeMessage,
|
||||
isMessageVisualizing: this.isMessageVisualizing,
|
||||
resolveMessageAssetConfigKey: this.messageAssetConfigKey,
|
||||
// Active backdrop asset id — message components hide the inline
|
||||
// image for the asset currently rendered as the backdrop
|
||||
getSceneBackdropAssetId: () => this.sceneBackdropAssetId,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -1347,9 +1362,9 @@ export default {
|
||||
|
||||
// Menu opened from an inline MessageAssetImage: its callback
|
||||
// opens the component's own AssetView. Otherwise (e.g. the
|
||||
// toolbar chip in "background" display mode) use the shared
|
||||
// fallback viewer; assetViewSrc resolves from the cache so it
|
||||
// fills in once the asset arrives
|
||||
// toolbar chip when the image is the active backdrop) use the
|
||||
// shared fallback viewer; assetViewSrc resolves from the cache
|
||||
// so it fills in once the asset arrives
|
||||
if (this.assetMenu.context.onViewImage) {
|
||||
this.assetMenu.context.onViewImage();
|
||||
} else if (this.assetMenu.context.asset_id) {
|
||||
@@ -1374,9 +1389,27 @@ export default {
|
||||
return visType === VIS_TYPE.SCENE_BACKGROUND ? 'scene_background' : 'scene_illustration';
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle "Set as scene backdrop" menu option — makes this asset the
|
||||
* scene's backdrop (scene-persisted) and enables rendering
|
||||
*/
|
||||
handleSetSceneBackdrop() {
|
||||
this.assetMenu.show = false;
|
||||
const assetId = this.assetMenu.context.asset_id;
|
||||
if (!assetId) {
|
||||
return;
|
||||
}
|
||||
this.getWebsocket().send(JSON.stringify({
|
||||
type: 'scene_assets',
|
||||
action: 'set_scene_backdrop',
|
||||
asset_id: assetId,
|
||||
enabled: true,
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Open the shared asset menu from the backdrop indicator icon —
|
||||
* in background mode there is no inline image to click on
|
||||
* the active backdrop has no inline image to click on
|
||||
*/
|
||||
openBackdropIndicatorMenu(event, message) {
|
||||
this.showAssetMenu(event, {
|
||||
@@ -2143,11 +2176,12 @@ export default {
|
||||
this.$emit('scene-backdrop', src);
|
||||
},
|
||||
},
|
||||
// Drives visibility of the scene-tools "Immersive" toggle chip
|
||||
sceneBackdropCandidateAvailable: {
|
||||
// Gives the scene-tools "Immersive" toggle chip an asset to promote
|
||||
// when the scene has no backdrop set yet
|
||||
sceneBackdropCandidateAssetId: {
|
||||
immediate: true,
|
||||
handler(available) {
|
||||
this.$emit('scene-backdrop-candidate', available);
|
||||
handler(assetId) {
|
||||
this.$emit('scene-backdrop-candidate', assetId);
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -2170,10 +2204,9 @@ export default {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Scene illustration "background" display mode: the backdrop itself is
|
||||
painted by TalemateApp on the scene column (.scene-backdrop-active);
|
||||
here each message gets a translucent panel so the text stays legible
|
||||
against an unknown image. */
|
||||
/* Scene backdrop: the backdrop itself is painted by TalemateApp on the
|
||||
scene column (.scene-backdrop-active); here each message gets a
|
||||
translucent panel so the text stays legible against an unknown image. */
|
||||
.scene-backdrop-active .message {
|
||||
background-color: rgba(var(--v-theme-surface), var(--scene-backdrop-panel-opacity, 0.8));
|
||||
backdrop-filter: blur(4px);
|
||||
|
||||
@@ -228,7 +228,6 @@ import SceneToolsSave from './SceneToolsSave.vue';
|
||||
import SceneToolsTime from './SceneToolsTime.vue';
|
||||
import RequestInput from './RequestInput.vue';
|
||||
import { isPrimaryModifier, primaryModifierLabel } from '@/utils/keyboardModifiers';
|
||||
import { BACKDROP_ASSET_KINDS } from '@/constants/visual';
|
||||
|
||||
export default {
|
||||
|
||||
@@ -260,12 +259,9 @@ export default {
|
||||
agentStatus: Object,
|
||||
scene: Object,
|
||||
visualAgentReady: Boolean,
|
||||
// scene has a message-attached background-type asset the
|
||||
// "Immersive" chip could promote to a backdrop
|
||||
sceneBackdropCandidate: Boolean,
|
||||
// a backdrop is currently rendering (any kind) — keeps the chip
|
||||
// reachable so the mode can be toggled off
|
||||
sceneBackdropActive: Boolean,
|
||||
// most recent message-attached background asset the "Immersive"
|
||||
// chip promotes when the scene has no backdrop set yet
|
||||
sceneBackdropCandidate: String,
|
||||
audioPlayedForMessageId: [Number, String],
|
||||
},
|
||||
computed: {
|
||||
@@ -301,9 +297,12 @@ export default {
|
||||
return ttsAgent && ttsAgent.available;
|
||||
},
|
||||
|
||||
sceneBackdropAssetId() {
|
||||
return this.scene?.data?.assets?.backdrop || null;
|
||||
},
|
||||
|
||||
immersiveActive() {
|
||||
const messageAssets = this.appConfig()?.appearance?.scene?.message_assets;
|
||||
return BACKDROP_ASSET_KINDS.some(kind => messageAssets?.[kind]?.size === 'background');
|
||||
return !!(this.sceneBackdropAssetId && this.scene?.data?.assets?.backdrop_enabled);
|
||||
},
|
||||
|
||||
visibleQuickSettings() {
|
||||
@@ -333,12 +332,8 @@ export default {
|
||||
quickSettings: [
|
||||
{"value": "toggleAutoSave", "title": "Auto Save", "icon": "mdi-content-save", "description": "Automatically save after each game-loop", "status": () => { return this.canAutoSave ? this.autoSave : "Manually save scene for auto-save to be available"; }},
|
||||
{"value": "toggleAutoProgress", "title": "Auto Progress", "icon": "mdi-robot", "description": "AI automatically progresses after player turn.", "status": () => { return this.autoProgress }},
|
||||
{"value": "toggleImmersive", "title": "Immersive", "icon": "mdi-image-area", "description": "Render the most recent scene background image as a backdrop behind the scene", "condition": () => { return this.sceneBackdropCandidate || this.sceneBackdropActive }, "status": () => { return this.immersiveActive }},
|
||||
{"value": "toggleImmersive", "title": "Immersive", "icon": "mdi-image-area", "description": "Render the scene backdrop image behind the scene", "condition": () => { return !!(this.sceneBackdropAssetId || this.sceneBackdropCandidate) }, "status": () => { return this.immersiveActive }},
|
||||
],
|
||||
// per-kind inline sizes to restore when Immersive is toggled off
|
||||
immersiveInlineSizes: {},
|
||||
// kinds to flip back to background when toggled on again
|
||||
immersiveKinds: ['scene_background'],
|
||||
}
|
||||
},
|
||||
inject: [
|
||||
@@ -348,7 +343,6 @@ export default {
|
||||
'isWaitingForInput',
|
||||
'creativeEditor',
|
||||
'appConfig',
|
||||
'setMessageAssetDisplaySizes',
|
||||
'getTrackedCharacterState',
|
||||
'getTrackedWorldState',
|
||||
'getPlayerCharacterName',
|
||||
@@ -386,27 +380,18 @@ export default {
|
||||
},
|
||||
|
||||
toggleImmersive() {
|
||||
const messageAssets = this.appConfig()?.appearance?.scene?.message_assets || {};
|
||||
const sizes = {};
|
||||
if (this.immersiveActive) {
|
||||
// restore every kind currently in background mode to its
|
||||
// remembered inline size
|
||||
for (const kind of BACKDROP_ASSET_KINDS) {
|
||||
if (messageAssets[kind]?.size === 'background') {
|
||||
sizes[kind] = this.immersiveInlineSizes[kind] || 'medium';
|
||||
}
|
||||
}
|
||||
this.immersiveKinds = Object.keys(sizes);
|
||||
// the scene owns the backdrop: toggle rendering when one is set,
|
||||
// otherwise promote the most recent scene background image
|
||||
const message = { type: 'scene_assets', action: 'set_scene_backdrop' };
|
||||
if (this.sceneBackdropAssetId) {
|
||||
message.enabled = !this.immersiveActive;
|
||||
} else if (this.sceneBackdropCandidate) {
|
||||
message.asset_id = this.sceneBackdropCandidate;
|
||||
message.enabled = true;
|
||||
} else {
|
||||
for (const kind of this.immersiveKinds) {
|
||||
const currentSize = messageAssets[kind]?.size;
|
||||
if (currentSize && currentSize !== 'background') {
|
||||
this.immersiveInlineSizes[kind] = currentSize;
|
||||
}
|
||||
sizes[kind] = 'background';
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.setMessageAssetDisplaySizes(sizes);
|
||||
this.getWebsocket().send(JSON.stringify(message));
|
||||
},
|
||||
|
||||
openWorldStateManager(tab, sub1, sub2, sub3) {
|
||||
|
||||
@@ -287,7 +287,6 @@
|
||||
:activeCharacters="activeCharacters"
|
||||
:visual-agent-ready="visualAgentReady"
|
||||
:scene-backdrop-candidate="sceneBackdropCandidate"
|
||||
:scene-backdrop-active="!!sceneBackdropSrc"
|
||||
:audioPlayedForMessageId="audioPlayedForMessageId" />
|
||||
<SceneMessageInput
|
||||
ref="sceneMessageInput"
|
||||
@@ -459,11 +458,12 @@ export default {
|
||||
return {
|
||||
appearancePreview: null, // Preview config while editing settings (null = use saved config)
|
||||
// data-url of the scene illustration acting as the scene backdrop
|
||||
// ("background" display mode), reported up by SceneMessages
|
||||
// (scene.assets.backdrop), reported up by SceneMessages which owns
|
||||
// the asset cache
|
||||
sceneBackdropSrc: null,
|
||||
// scene has a message-attached asset eligible for backdrop promotion
|
||||
// (drives the scene-tools "Immersive" toggle chip)
|
||||
sceneBackdropCandidate: false,
|
||||
// asset id of the most recent message-attached background image the
|
||||
// scene-tools "Immersive" chip promotes when no backdrop is set
|
||||
sceneBackdropCandidate: null,
|
||||
tab: 'home',
|
||||
tabs: [
|
||||
{
|
||||
@@ -834,7 +834,6 @@ export default {
|
||||
appConfig: () => this.appConfig,
|
||||
openAppConfig: this.openAppConfig,
|
||||
openAgentActionOverrides: () => this.$refs.agentActionOverrides?.open(),
|
||||
setMessageAssetDisplaySizes: this.setMessageAssetDisplaySizes,
|
||||
configurationRequired: () => this.configurationRequired(),
|
||||
getTrackedCharacterState: (name, question) => this.$refs.worldState.trackedCharacterState(name, question),
|
||||
getTrackedCharacterStates: (name) => this.$refs.worldState.trackedCharacterStates(name),
|
||||
@@ -1355,24 +1354,6 @@ export default {
|
||||
}
|
||||
this.websocket.send(JSON.stringify({ type: 'configure_clients', clients: saveData }));
|
||||
},
|
||||
setMessageAssetDisplaySizes(sizes) {
|
||||
// Shortcut used by the scene-tools "Immersive" chip to flip
|
||||
// message-asset display modes ({kind: size}, applied in one save)
|
||||
// without opening the appearance settings.
|
||||
// IMPORTANT: the save handler validates the payload as the FULL config
|
||||
// model — a partial payload gets its missing sections replaced with
|
||||
// defaults. Always send the complete config (same as AppConfig.vue).
|
||||
if (!this.appConfig) {
|
||||
return;
|
||||
}
|
||||
// appConfig is a full backend model dump — the nested structure
|
||||
// always exists
|
||||
const config = JSON.parse(JSON.stringify(this.appConfig));
|
||||
for (const [assetKind, size] of Object.entries(sizes)) {
|
||||
config.appearance.scene.message_assets[assetKind].size = size;
|
||||
}
|
||||
this.websocket.send(JSON.stringify({ type: 'config', action: 'save', config }));
|
||||
},
|
||||
saveAgents(agents) {
|
||||
const saveData = {}
|
||||
|
||||
@@ -1773,9 +1754,9 @@ export default {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* Scene illustration "background" display mode: the most recent scene
|
||||
illustration fills the whole scene column (messages, tools and input);
|
||||
SceneMessages gives each message a translucent panel for legibility. */
|
||||
/* Scene backdrop: the scene's backdrop image (scene.assets.backdrop) fills
|
||||
the whole scene column (messages, tools and input); SceneMessages gives
|
||||
each message a translucent panel for legibility. */
|
||||
.scene-backdrop-active {
|
||||
background-image: var(--scene-backdrop-image);
|
||||
background-size: cover;
|
||||
|
||||
@@ -62,10 +62,10 @@
|
||||
</v-tooltip>
|
||||
Cancel Analysis
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="text"
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="onSetSceneCoverImage({ assetId: selectedId })"
|
||||
prepend-icon="mdi-image-frame"
|
||||
prepend-icon="mdi-image-frame"
|
||||
color="primary"
|
||||
>
|
||||
<v-tooltip activator="parent" location="top">
|
||||
@@ -73,6 +73,17 @@
|
||||
</v-tooltip>
|
||||
Set cover
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="onSetSceneBackdrop({ assetId: selectedId })"
|
||||
prepend-icon="mdi-image-area"
|
||||
color="primary"
|
||||
>
|
||||
<v-tooltip activator="parent" location="top">
|
||||
Set this image as the scene backdrop and render it behind the scene text
|
||||
</v-tooltip>
|
||||
Set backdrop
|
||||
</v-btn>
|
||||
<v-btn color="primary" variant="text" :disabled="!selectedId" @click="onOpenGenerate" prepend-icon="mdi-play">Use as reference</v-btn>
|
||||
<v-btn color="primary" variant="text" :disabled="!selectedId" @click="onOpenIterate" prepend-icon="mdi-repeat">Iterate</v-btn>
|
||||
<v-btn v-if="canSaveValue" variant="text" @click="resetForm" prepend-icon="mdi-cancel" color="cancel">Reset</v-btn>
|
||||
@@ -368,6 +379,15 @@ export default {
|
||||
asset_id: payload.assetId,
|
||||
}));
|
||||
},
|
||||
onSetSceneBackdrop(payload) {
|
||||
if (!payload || !payload.assetId) return;
|
||||
this.getWebsocket().send(JSON.stringify({
|
||||
type: 'scene_assets',
|
||||
action: 'set_scene_backdrop',
|
||||
asset_id: payload.assetId,
|
||||
enabled: true,
|
||||
}));
|
||||
},
|
||||
saveMeta() {
|
||||
const ref = this.$refs.visualImageView;
|
||||
if (!ref) {
|
||||
|
||||
@@ -22,11 +22,6 @@ export const VIS_TYPE_OPTIONS = [
|
||||
VIS_TYPE.UNSPECIFIED,
|
||||
];
|
||||
|
||||
// message_assets appearance-config kinds whose "background" display size
|
||||
// renders the asset as the scene backdrop (keys in
|
||||
// config.appearance.scene.message_assets, see config/schema.py)
|
||||
export const BACKDROP_ASSET_KINDS = ['scene_illustration', 'scene_background'];
|
||||
|
||||
// Must match FORMAT_TYPE in src/talemate/agents/visual/schema.py
|
||||
export const FORMAT_TYPE = Object.freeze({
|
||||
LANDSCAPE: 'LANDSCAPE',
|
||||
|
||||
@@ -488,6 +488,9 @@ class TestSceneAppearanceMessageAssets:
|
||||
def test_defaults_include_all_entries(self):
|
||||
appearance = SceneAppearance()
|
||||
assert set(appearance.message_assets.keys()) == self.DEFAULT_KEYS
|
||||
# scene backgrounds default to full-width display
|
||||
assert appearance.message_assets["scene_background"].size == "big"
|
||||
assert appearance.message_assets["scene_illustration"].size == "medium"
|
||||
|
||||
def test_missing_entries_are_filled(self):
|
||||
# configs saved before scene_background existed omit it — the
|
||||
@@ -496,11 +499,11 @@ class TestSceneAppearanceMessageAssets:
|
||||
message_assets={
|
||||
"avatar": {"cadence": "never", "size": "small"},
|
||||
"card": {},
|
||||
"scene_illustration": {"size": "background"},
|
||||
"scene_illustration": {"size": "big"},
|
||||
}
|
||||
)
|
||||
assert set(appearance.message_assets.keys()) == self.DEFAULT_KEYS
|
||||
assert appearance.message_assets["avatar"].cadence == "never"
|
||||
assert appearance.message_assets["avatar"].size == "small"
|
||||
assert appearance.message_assets["scene_illustration"].size == "background"
|
||||
assert appearance.message_assets["scene_background"].size == "medium"
|
||||
assert appearance.message_assets["scene_illustration"].size == "big"
|
||||
assert appearance.message_assets["scene_background"].size == "big"
|
||||
|
||||
@@ -441,10 +441,14 @@ class TestSceneAssetsDictAndSceneInfo:
|
||||
assert result["cover_image"] == "cover-id"
|
||||
assert "a1" in result["assets"]
|
||||
|
||||
def test_scene_info_only_returns_cover_image(self, scene):
|
||||
def test_scene_info_returns_cover_image_and_backdrop(self, scene):
|
||||
scene.assets.cover_image = "cv"
|
||||
info = scene.assets.scene_info()
|
||||
assert info == {"cover_image": "cv"}
|
||||
assert info == {
|
||||
"cover_image": "cv",
|
||||
"backdrop": None,
|
||||
"backdrop_enabled": True,
|
||||
}
|
||||
|
||||
def test_load_assets_is_a_noop(self, scene):
|
||||
# Legacy method -- must not raise and must not modify state.
|
||||
@@ -806,6 +810,45 @@ class TestSceneCoverImage:
|
||||
assert scene.assets.cover_image == b.id
|
||||
|
||||
|
||||
class TestSceneBackdrop:
|
||||
async def test_set_backdrop_with_valid_id(self, scene):
|
||||
a = await scene.assets.add_asset(b"x", "png", "image/png")
|
||||
result = await scene.assets.set_scene_backdrop(asset_id=a.id)
|
||||
assert result == a.id
|
||||
assert scene.assets.backdrop == a.id
|
||||
# enabled defaults to True and is untouched by asset-only updates
|
||||
assert scene.assets.backdrop_enabled is True
|
||||
|
||||
async def test_set_backdrop_invalid_returns_none(self, scene):
|
||||
result = await scene.assets.set_scene_backdrop(asset_id="nope")
|
||||
assert result is None
|
||||
assert scene.assets.backdrop is None
|
||||
|
||||
async def test_toggle_enabled_keeps_asset(self, scene):
|
||||
a = await scene.assets.add_asset(b"x", "png", "image/png")
|
||||
await scene.assets.set_scene_backdrop(asset_id=a.id)
|
||||
result = await scene.assets.set_scene_backdrop(enabled=False)
|
||||
assert result == a.id
|
||||
assert scene.assets.backdrop == a.id
|
||||
assert scene.assets.backdrop_enabled is False
|
||||
|
||||
async def test_set_asset_does_not_reenable(self, scene):
|
||||
# a new backdrop image must not override an explicit "off"
|
||||
a = await scene.assets.add_asset(b"a", "png", "image/png")
|
||||
b = await scene.assets.add_asset(b"b", "png", "image/png")
|
||||
await scene.assets.set_scene_backdrop(asset_id=a.id, enabled=False)
|
||||
await scene.assets.set_scene_backdrop(asset_id=b.id)
|
||||
assert scene.assets.backdrop == b.id
|
||||
assert scene.assets.backdrop_enabled is False
|
||||
|
||||
async def test_backdrop_in_dict_and_scene_info(self, scene):
|
||||
a = await scene.assets.add_asset(b"x", "png", "image/png")
|
||||
await scene.assets.set_scene_backdrop(asset_id=a.id, enabled=True)
|
||||
for data in (scene.assets.dict(), scene.assets.scene_info()):
|
||||
assert data["backdrop"] == a.id
|
||||
assert data["backdrop_enabled"] is True
|
||||
|
||||
|
||||
class TestSceneCoverImageFromSources:
|
||||
async def test_from_bytes(self, scene):
|
||||
rid = await scene.assets.set_scene_cover_image_from_bytes(_png_bytes())
|
||||
@@ -995,6 +1038,19 @@ class TestCleanupCoverImages:
|
||||
assert cleaned is True
|
||||
assert char.cover_image is None
|
||||
|
||||
def test_cleans_dangling_backdrop(self, scene):
|
||||
scene.assets.backdrop = "ghost"
|
||||
cleaned = scene.assets.cleanup_cover_images()
|
||||
assert cleaned is True
|
||||
assert scene.assets.backdrop is None
|
||||
|
||||
async def test_keeps_valid_backdrop(self, scene):
|
||||
a = await scene.assets.add_asset(b"x", "png", "image/png")
|
||||
scene.assets.backdrop = a.id
|
||||
cleaned = scene.assets.cleanup_cover_images()
|
||||
assert cleaned is False
|
||||
assert scene.assets.backdrop == a.id
|
||||
|
||||
|
||||
class TestCleanupCharacterAvatars:
|
||||
def test_cleans_dangling_default_and_current_avatar(self, scene):
|
||||
|
||||
Reference in New Issue
Block a user