Add Ctrl+Z/Ctrl+Y undo/redo with command-based network sync

Pointer-based per-painter history of the canonical maps with copy-on-write
sharing (only maps touched by a stroke are cloned). A stroke (press..release)
is one step; a Load resets the history to the loaded maps as the sole baseline.

Network sync replicates undo/redo as tiny checkpoint/restore commands instead
of broadcasting full map snapshots: every peer runs the same COW history keyed
by owner-assigned checkpoint ids and reproduces restores locally. A peer that
cannot reproduce a target (undo across its join boundary) requests a targeted
snapshot from the owner as fallback.

Hardening: ordered send FIFO keeps stamps + checkpoints + restores in
production order (a marker never overtakes the stamps it finalizes); stamp
overflow carries over to the next frame instead of being decimated, so remote
content matches the owner; remote always inserts the checkpoint id to stay
1:1 aligned. Owner-authoritative snapshot fallback replaces the silent
host no-op. Adds a logSync toggle (default on) tracing when each
checkpoint/restore/snapshot is sent, applied, or falls back.

Also includes paintings-panel layout tuning in PaintHudPanel.prefab
(inspector edits).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 4.8 committed 2026-06-27 20:19:36 +02:00
1 parent ee482905f9
commit 301f7e64cb
8 files changed
+619 -78

No files matched your search

+16
View File
@@ -303,6 +303,7 @@ public partial class MeshPainter : MonoBehaviour
// HUD library (MeshPainter.Persistence), never auto-loaded on start.
SetupBackend(useGpu, _seedCanonical);
ConfigureMaterialPbr();
InitUndoHistory(); // baseline = material seed, captured before any stroke
EnsureProceduralMask();
ResolveActiveMask();
@@ -592,10 +593,22 @@ public partial class MeshPainter : MonoBehaviour
// Painting disabled by the game mode (e.g. player-control mode): do nothing.
if (!PaintingEnabled)
{
EndStrokeIfActive(); // finalize an in-progress stroke if the mode was left mid-paint
_hasLastScreen = false;
return;
}
// Undo / redo (Ctrl+Z / Ctrl+Y): only here, since PaintingEnabled is true only on the local
// owner in paint mode. Commit any in-progress stroke first so it becomes its own history step.
GetUndoRedoInput(out bool wantUndo, out bool wantRedo);
if (wantUndo || wantRedo)
{
EndStrokeIfActive();
if (wantUndo) Undo();
else Redo();
return;
}
if (updateColliderEveryFrame && _skinned)
{
BakeColliderMesh();
@@ -617,6 +630,7 @@ public partial class MeshPainter : MonoBehaviour
// entirely (no painting, no brush preview ring while active).
if (pipette)
{
EndStrokeIfActive(); // switching to the eyedropper ends any active stroke
if (pressed)
{
if (!_pickHeld) PickAtScreen(screenPos);
@@ -635,6 +649,7 @@ public partial class MeshPainter : MonoBehaviour
if (!pressed)
{
EndStrokeIfActive(); // mouse released -> commit the stroke as one undo step
_hasLastScreen = false;
return;
}
@@ -717,6 +732,7 @@ public partial class MeshPainter : MonoBehaviour
else PaintStamp3D(pLocal, radiusLocal, u, v, nLocal);
EmitStamp(pLocal, radiusLocal, u, v, nLocal);
MarkStrokePainted(); // a stamp landed -> this stroke is undoable
}
/// <summary>