Lost Lily/Grid ToolkitDocs 1.0
Table of Contents

Display action feedback

When to use

Display action feedback when a preview or committed result should highlight affected or invalid cells. Visual feedback is presentation state: it does not mutate the board or advance its revision.

Before you start

Choose the logical request in Choose a built-in action. Connect a GridBoardView to a ready renderer with a compatible intent renderer.

Procedure

Publish preview feedback

Preview the action, then give the same action and result to the view's GridVisualState.

Excerpt — publish a preview result. Tested using public APIs.

GridActionResult preview = GridActionRunner.Preview(state, action);
boardView.VisualState.SetActionPreview(action, preview);

Apply and replace the channel

Apply only after a successful preview. SetActionResult replaces the action feedback channel with the committed result.

Excerpt — apply and publish the result. Tested using public APIs.

GridActionResult applied = GridActionRunner.Apply(state, action);
boardView.VisualState.SetActionResult(action, applied);

Choose the feedback route

Feedback Use
Generic affected or invalid cells SetActionPreview or SetActionResult
Placement footprint SetPlacementPreview
Inventory move, rotate, sort, transfer, or merge Inventory Visualization bridge
Project-specific semantics IGridActionVisualIntentContributor

A custom contributor that returns no valid intents falls back to generic action feedback. Inventory quantity changes also use generic action feedback because Inventory has no quantity-specific intent ID.

GridVisualIntentIds defines the Stable built-in intent and channel IDs:

Purpose Intent ID Channel ID
Hover grid.hover grid.visual.channel.hover
Selection grid.selection grid.visual.channel.selection
Valid placement grid.placement.valid grid.visual.channel.placement
Invalid placement grid.placement.invalid grid.visual.channel.placement
Affected action cells grid.action.affected grid.visual.channel.action-affected
Invalid action cells grid.action.invalid grid.visual.channel.action-invalid
Project overlay grid.overlay Project-owned channel

GridInventoryVisualIntentIds defines these Preview intent IDs:

Operation Accepted intent Rejected intent
Move grid.inventory.move grid.inventory.move.invalid
Transfer grid.inventory.transfer grid.inventory.transfer.invalid
Merge grid.inventory.merge grid.inventory.merge.invalid
Rotate grid.inventory.rotate grid.inventory.rotate.invalid
Sort grid.inventory.sort grid.inventory.sort.invalid

The current Inventory bridge emits grid.inventory.move, grid.inventory.rotate, and grid.inventory.sort channels for same-board actions.

Cross-board operations use one channel per participant:

  • transfers use grid.inventory.transfer.source and grid.inventory.transfer.destination;
  • merges use grid.inventory.merge.source and grid.inventory.merge.destination.

These channel strings are current Preview-tier behavior rather than Stable built-in constants.

Clear temporary feedback

Excerpt — clear the action-feedback channels. Tested using public APIs.

boardView ??= GetComponent<GridBoardView>();
if (boardView == null)
    return;
boardView.VisualState.ClearIntentChannel(
    GridVisualIntentIds.ActionAffectedChannel);
boardView.VisualState.ClearIntentChannel(
    GridVisualIntentIds.ActionInvalidChannel);
Complete tested example
using System;
using LostLily.GridToolkit.Actions;
using LostLily.GridToolkit.Authoring;
using LostLily.GridToolkit.Board;
using LostLily.GridToolkit.Topology.Square;
using LostLily.GridToolkit.Visualization;
using UnityEngine;

namespace GridToolkit.Tutorials
{
    [RequireComponent(typeof(GridBoardAuthoring), typeof(GridBoardView))]
    public sealed class ActionFeedbackExample : MonoBehaviour
    {
        [SerializeField] private GridBoardAuthoring boardAuthoring;
        [SerializeField] private GridBoardView boardView;

        public (
            bool PreviewSucceeded,
            bool ApplySucceeded,
            long RevisionBeforePreview,
            long RevisionAfterPreview,
            long RevisionAfterApply,
            int BatchesAfterPreview,
            int CommittedBatchCount,
            bool CellExists,
            bool FeedbackVisible) LastReport { get; private set; }

        private void Start()
        {
            LastReport = Run();
        }

        public (
            bool PreviewSucceeded,
            bool ApplySucceeded,
            long RevisionBeforePreview,
            long RevisionAfterPreview,
            long RevisionAfterApply,
            int BatchesAfterPreview,
            int CommittedBatchCount,
            bool CellExists,
            bool FeedbackVisible) Run()
        {
            return Run(new SquareGridCoordinate(8, 0));
        }

        public (
            bool PreviewSucceeded,
            bool ApplySucceeded,
            long RevisionBeforePreview,
            long RevisionAfterPreview,
            long RevisionAfterApply,
            int BatchesAfterPreview,
            int CommittedBatchCount,
            bool CellExists,
            bool FeedbackVisible) Run(SquareGridCoordinate coordinate)
        {
            boardAuthoring ??= GetComponent<GridBoardAuthoring>();
            boardView ??= GetComponent<GridBoardView>();
            if (boardAuthoring == null || boardAuthoring.State == null
                || boardView == null)
            {
                Debug.LogError(
                    "Action feedback needs a ready GridBoardAuthoring and GridBoardView.",
                    this);
                return default;
            }

            GridBoardState state = boardAuthoring.State;
            IGridAction action = GridActions.AddCell(coordinate);
            long revisionBeforePreview = state.Revision;
            int batchCount = 0;
            void CountBatch(GridBoardEventBatch _) => batchCount++;
            state.EventsCommitted += CountBatch;
            try
            {
                GridActionResult preview = GridActionRunner.Preview(state, action);
                boardView.VisualState.SetActionPreview(action, preview);

                long revisionAfterPreview = state.Revision;
                int batchesAfterPreview = batchCount;
                if (!preview.Success)
                {
                    Debug.LogError(preview.FormatDiagnostics(), this);
                    LastReport = (
                        false,
                        false,
                        revisionBeforePreview,
                        revisionAfterPreview,
                        state.Revision,
                        batchesAfterPreview,
                        batchCount,
                        state.ContainsCell(coordinate),
                        HasFeedback());
                    return LastReport;
                }

                GridActionResult applied = GridActionRunner.Apply(state, action);
                boardView.VisualState.SetActionResult(action, applied);

                if (!applied.Success)
                    Debug.LogError(applied.FormatDiagnostics(), this);

                LastReport = (
                    true,
                    applied.Success,
                    revisionBeforePreview,
                    revisionAfterPreview,
                    state.Revision,
                    batchesAfterPreview,
                    batchCount,
                    state.ContainsCell(coordinate),
                    HasFeedback());
                Debug.Log(
                    $"Action feedback: revision {revisionBeforePreview} -> "
                    + $"{LastReport.RevisionAfterApply}, batches "
                    + $"{LastReport.CommittedBatchCount}, cell {coordinate} exists: "
                    + $"{LastReport.CellExists}.",
                    this);
                return LastReport;
            }
            finally
            {
                state.EventsCommitted -= CountBatch;
            }
        }

        public void ClearFeedback()
        {
            boardView ??= GetComponent<GridBoardView>();
            if (boardView == null)
                return;
            boardView.VisualState.ClearIntentChannel(
                GridVisualIntentIds.ActionAffectedChannel);
            boardView.VisualState.ClearIntentChannel(
                GridVisualIntentIds.ActionInvalidChannel);
        }

        private bool HasFeedback()
        {
            return boardView.VisualState.TryGetIntentChannel(
                       GridVisualIntentIds.ActionAffectedChannel,
                       out _)
                   || boardView.VisualState.TryGetIntentChannel(
                       GridVisualIntentIds.ActionInvalidChannel,
                       out _);
        }
    }
}

Result

Preview feedback appears without a commit. Apply publishes the committed result, and the renderer keeps each feedback node under a stable key that includes its owning channel. Replacing or clearing that channel updates or removes only its own retained nodes.

Troubleshooting

Symptom Check
The action succeeds but nothing appears Assign an intent renderer whose required capabilities match the active backend.
Old feedback remains visible Clear the exact owner channel or the relevant transient visual category.
Custom feedback disappears Return at least one valid intent and keep its channel ID stable.