- Added a new specification for the speech style toolbar live-edit, detailing immediate style application, scoped effect demonstration, and hover feedback. - Introduced refinements to the speech style toolbar, including improved chooser sizing, exclusive stage editing during speech composition, and removal of redundant controls. - Documented new stage controls for managing how the whole speech shows, including options for bubble arrangement, word appearance, and bubble motion. - Created a new log file for speech padding syntax to track engine versioning.
152 lines
16 KiB
Markdown
152 lines
16 KiB
Markdown
# Stage Controls for How the Whole Speech Shows
|
|
|
|
Status: **Implemented; pending user verification** (2026-10-02). This documents the stage controls that extend the approved comic speech composer and replace the Inspector **Show** section. Bubble layout presets are specified in [Speech Bubble Layout Presets](speech_bubble_layout_presets.md).
|
|
|
|
> **Current dialog layout:** see the 2026-10-02 amendment at the end of this document. It supersedes older control grouping, Comic chain placement, and footer descriptions below.
|
|
|
|
## 1. User interaction
|
|
|
|
When speech editing opens, no bubble has stage focus. A compact panel beside the bubble group shows the three choices that affect the entire speech immediately. There is no **Whole Speech** button or additional mode switch.
|
|
|
|
| Label | Choices | Stored values |
|
|
| --- | --- | --- |
|
|
| 💬 **Show bubbles** | **Show all** / **One at a time** / **Comic chain** | `arrangement: "together"` / `"take_turns"` / `"comic_chain"` |
|
|
| 🔤 **Words appear** | **All at once** / **Letter by letter** / **Word by word** | action `reveal: "instant"` / `"letter"` / `"word"` |
|
|
| ✨ **Bubble motion** | **Bounce in** / **Just appear** | action `pop_in: true` / `false` |
|
|
|
|
Use large buttons with a persistent selected outline that remains visible on hover, a ribbon title such as **Show speech**, and optional small pictorial samples. A choice updates the recoverable speech draft and selection feedback immediately. Comic chain is cumulative: each bubble appears in order while earlier bubbles remain visible, and joined connectors are shown. Entering it applies the Stack positions; the separate Stack and Same spot actions only position bubbles. Choosing Show all or One at a time leaves Comic chain. **Preview speech** plays the full sequence with the chosen arrangement, reveal, and motion. The separate **Save speech** action remains the queue/rule commit point; Cancel discards the session.
|
|
|
|
Clicking a bubble or one of its tokens focuses that bubble and swaps the group panel for its existing **Speech Style** toolbar. The token is also selected when one was tapped. **Done styling** and a click on empty stage space clear bubble focus and restore the group panel. Empty stage clicks do not select, move, or edit characters or props beneath them. Adding a bubble still selects the new bubble, as previously approved; after **Done styling**, the group panel returns. A bubble drag focuses that bubble once the drag threshold is met. Clicking overlapping bubbles retains the existing temporary picker.
|
|
|
|
The group panel sits beside the visible bubble group and follows it after bubble movement, camera changes, resize, and text changes. Prefer a side with enough room; otherwise try the other side, then below or above. Clamp the panel within the usable stage rectangle and keep it off the bubbles whenever space permits. Use the existing stage theme, ribbon, 44 px minimum controls, and vertical wrapping; do not introduce horizontal stage scrolling. During a full replay or message dialog, hide or disable the group panel consistently with the current Speech Style toolbar. No group highlight overlay is required; the group title and three choice rows communicate scope.
|
|
|
|
```text
|
|
Speech editor opened / Done styling / empty stage click
|
|
┌──────────────────────┐ ┌────────────────────────────┐
|
|
│ Bubble group │ │ Show speech │
|
|
│ [Hi!] [Everyone!] │ │ 💬 Show bubbles │
|
|
│ │ │ 🔤 Words appear │
|
|
│ │ │ ✨ Bubble motion │
|
|
└──────────────────────┘ └────────────────────────────┘
|
|
│ click a bubble or token
|
|
▼
|
|
┌──────────────────────┐ ┌────────────────────────────┐
|
|
│ Selected bubble │ │ Speech Style │
|
|
│ [Hi!] │ │ Message · Timing │
|
|
│ │ │ Frame · Letters · Effects │
|
|
│ │ │ Done styling │
|
|
└──────────────────────┘ └────────────────────────────┘
|
|
```
|
|
|
|
The existing **Timing** control remains in Speech Style for this change. It already sets the action-wide `duration_mode`; this specification changes only the three former Show controls.
|
|
|
|
## 2. Ownership, nodes, and typed interfaces
|
|
|
|
Keep `SpeechComposer` (`class_name SpeechComposer extends Control`) as the owner of the draft and text editing. Its `_selected_bubble: int` remains a valid index for the Message editor and draft recovery. Add a separate transient `_stage_focus_bubble: int = -1`; `-1` means the group panel is shown. Focus is UI state and is not serialized. Add typed methods such as:
|
|
|
|
```gdscript
|
|
signal stage_focus_changed(bubble_index: int) # -1 means the whole speech
|
|
|
|
func stage_focus_bubble_index() -> int
|
|
func clear_stage_focus() -> void
|
|
func choose_speech_show(key: StringName, value: Variant) -> void
|
|
```
|
|
|
|
`choose_speech_show` accepts only `&"arrangement"`, `&"reveal"`, and `&"pop_in"`; it validates each value against `SpeechPayload` constants, updates one action-level field, synchronizes the per-bubble render fields, recalculates duration through the existing `_emit_draft()`, and emits one `draft_changed(action: Dictionary)`. Selecting Comic chain applies measured Stack centers and the arrangement together in one draft update. It must not accidentally overwrite the Message TextEdit content; preserve `_save_active_editor_state()` ordering. Existing `select_bubble_from_stage`, `select_bubble_token_from_stage`, and `_add_bubble` set stage focus. `open_for` and `set_draft` start with no stage focus; recovered `_selected_bubble` can still restore editor position independently.
|
|
|
|
Add a lightweight `SpeechShowToolbar` (`class_name SpeechShowToolbar extends Control`) as a sibling of `SpeechStyleToolbar` under the Sandbox Stage UI control. Its full-rect root ignores mouse input while its visible `PanelContainer` stops mouse input. Suggested public contract:
|
|
|
|
```gdscript
|
|
signal choice_selected(key: StringName, value: Variant)
|
|
|
|
func configure(theme_data: SandboxTheme) -> void
|
|
func set_context(action: Dictionary) -> void
|
|
func set_anchor_rect(group_viewport_rect: Rect2, stage_viewport_rect: Rect2) -> void
|
|
func set_busy(busy: bool) -> void
|
|
func contains_viewport_point(point: Vector2) -> bool
|
|
```
|
|
|
|
The toolbar only emits choices; it does not mutate a draft or save a scene. Its hierarchy is `SpeechShowToolbar / PanelContainer / VBoxContainer / ribbon Label + three labelled HFlowContainer choice rows`. Put its `PanelContainer` in the same stage UI layer as Speech Style. Reuse `SandboxTheme` colours, button sizes, and selected/hover border treatment.
|
|
|
|
`SpeechBubble` (`class_name SpeechBubble extends Node2D`) should expose `func group_frames_viewport_rect() -> Rect2`, the union of its rendered frame rectangles in viewport coordinates, using the same `_layout_frame` and canvas transform already used by `selected_frame_viewport_rect(index)`. Return an empty rect when no static editor frames exist. This keeps screen geometry in the renderer rather than recomputing it from text or actor positions in the toolbar.
|
|
|
|
`SandboxStage` owns focus presentation and pointer routing. It connects `SpeechShowToolbar.choice_selected` to the composer's `choose_speech_show`, and `SpeechComposer.stage_focus_changed` to a single refresh path that shows exactly one of the group panel and Speech Style toolbar. `_speech_composer_owns_event` must include the group panel's visible rect so a click on a choice cannot pass through to stage bubble selection. The existing `_resolve_speech_bubble_pointer_click` empty-hit path calls `clear_stage_focus()` instead of only clearing the token. The **Done styling** handler also calls `clear_stage_focus()` rather than merely hiding Speech Style. Both panels use the current busy/preview gate, and stage cleanup hides both. The current `_speech_style_toolbar_dismissed` flag should be retired or derived from focus, because an independent hidden state can disagree with the new panel swap.
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
P[Stage pointer] --> S[SandboxStage]
|
|
S -->|bubble/token click or empty click| C[SpeechComposer focus]
|
|
C -->|stage_focus_changed| S
|
|
S -->|focus = -1| G[SpeechShowToolbar]
|
|
S -->|focus >= 0| B[SpeechStyleToolbar]
|
|
G -->|choice_selected| S
|
|
S -->|choose_speech_show| C
|
|
C -->|draft_changed| S
|
|
S --> R[SpeechBubble preview]
|
|
```
|
|
|
|
## 3. Action payload and migration
|
|
|
|
Use action-level fields as the source of truth for all three Show choices. Keep the existing `arrangement` field and add `reveal` and `pop_in` at the action level. The bubble `visual.reveal` and `visual.pop_in` keys remain synchronized render copies for the existing `SpeechBubble` and `SpeechPayload.duration_for()` paths; they are never independent editing controls after this change.
|
|
|
|
```gdscript
|
|
{
|
|
"type": "speak",
|
|
"arrangement": "take_turns", # or "together" / "comic_chain"
|
|
"reveal": "letter",
|
|
"pop_in": true,
|
|
"duration_mode": "just_enough",
|
|
"bubbles": [
|
|
{"text": "Hi!", "visual": {"bubble_style": "talk", "reveal": "letter", "pop_in": true}},
|
|
{"text": "Everyone!", "visual": {"bubble_style": "shout", "reveal": "letter", "pop_in": true}}
|
|
]
|
|
}
|
|
```
|
|
|
|
`SpeechPayload.normalize(raw: Dictionary) -> Dictionary` determines shared `reveal` and `pop_in` in this order: valid action-level values; otherwise the first bubble's visual values; otherwise the legacy single-text `visual`; otherwise `"instant"` and `false`. It writes the chosen values to the action and every normalized bubble visual. The same first-bubble rule deliberately resolves older multi-bubble actions whose per-bubble values differ. Loading such an action into editing and saving it makes all bubbles use the first bubble's reveal and motion. Preserve the first bubble's other visual properties and all bubble placements. `arrangement` accepts `together`, `take_turns`, and `comic_chain`, defaulting to `together`. Older `bubble_layout: "comic_chain"` values migrate to the canonical arrangement.
|
|
|
|
`SpeechPayload.validate` accepts old actions without the new top-level keys, and validates the keys when present (`reveal` in `REVEAL_MODES`, `pop_in` a bool). The scene decoder currently validates before it normalizes, so this backward compatibility is required there. `StageSceneIO._encode_speech` includes the new action-level fields in its explicit speech allowlist and writes a normalized speech action with synchronized bubble visuals. `_decode_speech` continues to accept pre-change scenes and returns the normalized action. `ActionRegistry.to_rule_action` and `from_rule_action` add `reveal` and `pop_in` to their explicit speak field lists; this prevents event-rule speech from silently losing the choices. Draft JSON uses the full action dictionary and therefore carries the fields naturally. No `.scene` file version bump is needed because all new keys are optional to the decoder.
|
|
|
|
`SpeechComposer._new_bubble` copies action-level `reveal` and `pop_in` into its visual; `SpeechPayload.normalize` enforces the same contract for actions created elsewhere. `SpeechPayload.duration_for()` should use the action-level reveal value when present and the first bubble fallback for legacy actions, or consume its synchronized normalized bubble copies. `SpeechBubble.render_action()` already normalizes before building labels and entrance scales, so runtime and editor preview share the same choices. Avoid calling `normalize` once per frame inside each individual bubble render; normalize once at the action boundary as it does today.
|
|
|
|
## 4. Lifecycle and acceptance
|
|
|
|
1. Open a new speech or a saved speech: the group panel appears beside the group; no bubble is dimmed or highlighted as selected. Click a bubble or token: group panel hides and Speech Style appears. **Done styling** or empty stage click returns to the group panel, without changing the draft's text or positions.
|
|
2. Choose each Bubbles, Words appear, and Bubble motion option: the selected border persists on hover; all existing bubbles use that choice on **Preview speech**. Add another bubble and preview again: the new bubble uses the same reveal and motion, regardless of which bubble was previously edited.
|
|
3. Save speech, save the scene, reload it, and reopen queue editing: the three choices are selected correctly and playback matches. The same fields survive a reactive rule round trip and recoverable draft restore.
|
|
4. Load a pre-change scene with mixed per-bubble reveal or motion values: the group panel reflects the first bubble, and every bubble follows that choice when edited or replayed. Existing legacy single-text actions still load and play; old scenes are not rejected for missing action-level fields.
|
|
5. On a narrow stage, drag a bubble, pan/zoom the camera, and resize the window: the panel remains adjacent when space allows, stays within stage bounds, and does not obscure its choice labels. Its buttons do not trigger a bubble click or an object interaction behind it.
|
|
6. During a full replay and while Message is open, group choices cannot be changed accidentally. Save speech and Cancel retain their current commit/discard semantics, and the inspector no longer displays the Show card.
|
|
|
|
## 5. Tradeoffs
|
|
|
|
- Keeping synchronized `visual.reveal` and `visual.pop_in` copies avoids a broad renderer rewrite but duplicates two values in saved JSON. `SpeechPayload.normalize()` must remain the single synchronization boundary; future speech writers should use it before serialization.
|
|
- Migrating mixed legacy bubbles from the first bubble makes the new group panel truthful and deterministic. It intentionally removes later bubbles' distinct reveal/motion choices when they are normalized into this speech-wide model.
|
|
- A dedicated stage panel makes the group scope visible without adding a button or mode, at the cost of one more transient Control node and adaptive placement logic. A separate focus index preserves existing Message and draft recovery behavior.
|
|
|
|
## 6. Dialog layout amendment (2026-10-02)
|
|
|
|
Keep the whole-speech controls visually distinct from per-bubble styling:
|
|
|
|
- **Show bubbles** has only **Show all** and **One at a time**.
|
|
- **Bubble layout** has **Stack** and **Same spot**.
|
|
- **Connect bubbles** is a separate on/off switch. Turning it on selects Stack if needed; choosing Same spot turns it off. It is available only when there are at least two bubbles.
|
|
- With One at a time and Connect bubbles on, reveal one new bubble per step while keeping previous bubbles visible as part of the chain. With Show all, all connected bubbles are visible together.
|
|
- **More speech settings** starts collapsed and contains Words appear, Bubble motion, Timing, and the reactive-only Stop current action option.
|
|
- **Preview speech**, **Done speech**, and **Cancel speech** stay in a footer outside the scrolling content. Done speech commits the speech action; Cancel discards the editing session.
|
|
- The panel stays beside the bubble group when space allows, otherwise moves below or above it. Cap its width to preserve stage visibility; scroll the settings content on short stages while keeping the footer reachable.
|
|
|
|
```text
|
|
┌──────────────────────────────┐
|
|
│ Show speech │
|
|
│ Show bubbles │
|
|
│ [Show all] [One at a time] │
|
|
│ Bubble layout │
|
|
│ [Stack] [Same spot] │
|
|
│ 🔗 Connect bubbles [Off/On] │
|
|
│ ▸ More speech settings │
|
|
│ │
|
|
│ [▶ Preview] [Done] [Cancel] │
|
|
└──────────────────────────────┘
|
|
```
|