Editor/Canvas/DesignerCanvas.cs
using Editor;
using Grains.RazorDesigner.Document;
using Sandbox;
namespace Grains.RazorDesigner.Canvas;
// Same shape as ShaderGraph PreviewPanel: override PreFrame to advance the scene.
public class DesignerCanvas : SceneRenderingWidget
{
private const string LogPrefix = "[Grains.RazorDesigner]";
private readonly DesignerScene _designerScene;
public DesignerCanvas( Widget parent ) : base( parent )
{
Log.Info( $"{LogPrefix} DesignerCanvas ctor" );
_designerScene = new DesignerScene();
Scene = _designerScene.Scene;
Camera = _designerScene.Camera;
HorizontalSizeMode = SizeMode.Default | SizeMode.Expand;
VerticalSizeMode = SizeMode.Default | SizeMode.Expand;
// Without AcceptDrops, OnDragHover/OnDragDrop are never invoked.
AcceptDrops = true;
MouseTracking = true;
}
public DesignerScene DesignerScene => _designerScene;
// Assigned by DesignerWindow after construction (mirrors how _viewportFrame.Canvas is wired).
public OverlayController Overlay { get; set; }
public CanvasViewportFrame ViewportFrame { get; set; }
public event System.Action<Vector2, bool, Sandbox.KeyboardModifiers> CanvasClicked;
public event System.Action<Vector2, Sandbox.KeyboardModifiers> CanvasMoved;
public event System.Action<Vector2> CanvasReleased;
public event System.Action<Vector2> CanvasPanDragged; // delta, screen px
// Fires when the cursor leaves the canvas widget. Used to clear hover-pick state.
public event System.Action CanvasHoverEnded;
public event System.Action<ControlType, Vector2> RecordDropped;
// Mirrors RecordDropped but for saved palette templates.
public event System.Action<Grains.RazorDesigner.Templates.PaletteTemplate, Vector2> TemplateDropped;
private bool _middlePanning;
private Vector2 _lastPanScreen;
private const bool ProbeFrameCost = false;
private const int ProbeWindow = 120;
private readonly System.Diagnostics.Stopwatch _probeSw = new();
private double _probeTickMs;
private double _probeUpdateMs;
private int _probeFrames;
public override void OnDragHover( DragEvent ev )
{
base.OnDragHover( ev );
if ( ev.Data.Object is ControlType || ev.Data.Object is Grains.RazorDesigner.Templates.PaletteTemplate )
{
ev.Action = DropAction.Copy;
}
}
public override void OnDragDrop( DragEvent ev )
{
base.OnDragDrop( ev );
if ( ev.Data.Object is ControlType type )
{
Log.Info( $"{LogPrefix} DesignerCanvas drop: {type} at widget ({ev.LocalPosition.x:F0}, {ev.LocalPosition.y:F0})" );
RecordDropped?.Invoke( type, ev.LocalPosition );
}
else if ( ev.Data.Object is Grains.RazorDesigner.Templates.PaletteTemplate template )
{
Log.Info( $"{LogPrefix} DesignerCanvas drop: template \"{template.Name}\" at widget ({ev.LocalPosition.x:F0}, {ev.LocalPosition.y:F0})" );
TemplateDropped?.Invoke( template, ev.LocalPosition );
}
}
protected override void OnMousePress( MouseEvent e )
{
if ( e.MiddleMouseButton )
{
// Swallow entirely — don't let base (SceneRenderingWidget) see the middle drag.
_middlePanning = true;
_lastPanScreen = e.ScreenPosition;
e.Accepted = true;
return;
}
base.OnMousePress( e );
if ( e.LeftMouseButton || e.RightMouseButton )
{
var pos = e.LocalPosition;
Log.Info( $"{LogPrefix} DesignerCanvas click at widget ({pos.x:F0}, {pos.y:F0}) right={e.RightMouseButton}" );
CanvasClicked?.Invoke( pos, e.RightMouseButton, e.KeyboardModifiers );
}
}
protected override void OnMouseMove( MouseEvent e )
{
if ( _middlePanning )
{
var s = e.ScreenPosition;
CanvasPanDragged?.Invoke( s - _lastPanScreen );
_lastPanScreen = s;
e.Accepted = true;
return;
}
base.OnMouseMove( e );
CanvasMoved?.Invoke( e.LocalPosition, e.KeyboardModifiers );
}
protected override void OnMouseReleased( MouseEvent e )
{
if ( _middlePanning )
{
_middlePanning = false;
e.Accepted = true;
return;
}
base.OnMouseReleased( e );
CanvasReleased?.Invoke( e.LocalPosition );
}
protected override void OnMouseLeave()
{
base.OnMouseLeave();
CanvasHoverEnded?.Invoke();
}
protected override void PreFrame()
{
base.PreFrame();
if ( !_designerScene.Scene.IsValid() )
return;
if ( ProbeFrameCost ) _probeSw.Restart();
using ( _designerScene.Scene.Push() )
{
// EditorTick (not GameTick): preview scene, no SceneNetworkUpdate / fixed physics.
_designerScene.Scene.EditorTick( RealTime.Now, RealTime.Delta );
}
if ( ProbeFrameCost ) { _probeTickMs += _probeSw.Elapsed.TotalMilliseconds; _probeSw.Restart(); }
// Layout in framebuffer space (Size * DpiScale); DesignerScene sets Scale = DpiScale so authoring stays in logical px.
_designerScene.Update( Size.x, Size.y, DpiScale );
Overlay?.Tick( _designerScene, DpiScale );
if ( ProbeFrameCost )
{
_probeUpdateMs += _probeSw.Elapsed.TotalMilliseconds;
if ( ++_probeFrames >= ProbeWindow )
{
Log.Info( $"{LogPrefix} probe: EditorTick avg {_probeTickMs / _probeFrames:F3}ms | Update avg {_probeUpdateMs / _probeFrames:F3}ms (over {_probeFrames} frames, canvas {Size.x:F0}x{Size.y:F0})" );
_probeTickMs = 0;
_probeUpdateMs = 0;
_probeFrames = 0;
}
}
}
protected override void OnKeyPress( KeyEvent e )
{
base.OnKeyPress( e );
if ( ViewportFrame is not { CanPanZoom: true } frame ) return;
var mods = e.KeyboardModifiers;
var ctrl = (mods & Sandbox.KeyboardModifiers.Ctrl) != 0;
var shift = (mods & Sandbox.KeyboardModifiers.Shift) != 0;
var alt = (mods & Sandbox.KeyboardModifiers.Alt) != 0;
if ( !ctrl || alt ) return; // every shortcut is Ctrl-based; Alt clears it.
switch ( e.Key )
{
case KeyCode.Num0:
if ( shift )
{
Log.Info( $"{LogPrefix} OnKeyPress Ctrl+Shift+0 → ZoomToSelection" );
ZoomToSelectionViaFrame( frame );
}
else
{
Log.Info( $"{LogPrefix} OnKeyPress Ctrl+0 → ApplyFit" );
frame.ApplyFit();
}
e.Accepted = true;
break;
case KeyCode.Num1:
if ( shift ) return;
Log.Info( $"{LogPrefix} OnKeyPress Ctrl+1 → ResetZoomOnly" );
frame.ResetZoomOnly();
e.Accepted = true;
break;
case KeyCode.Equal:
if ( shift ) return;
frame.ZoomBy( 1.25f );
e.Accepted = true;
break;
case KeyCode.Minus:
if ( shift ) return;
frame.ZoomBy( 1f / 1.25f );
e.Accepted = true;
break;
}
}
private void ZoomToSelectionViaFrame( CanvasViewportFrame frame )
{
var record = frame.Selection?.Selected;
var live = record?.LivePanel;
if ( live is null || !live.IsValid )
{
Log.Info( $"{LogPrefix} Ctrl+Shift+0: no valid selection" );
return;
}
frame.ApplyZoomToRect( live.Box.Rect, /* maxZoom */ 4f, /* padding */ 0.10f );
}
public override void OnDestroyed()
{
Log.Info( $"{LogPrefix} DesignerCanvas.OnDestroyed" );
base.OnDestroyed();
_designerScene.Dispose();
}
}