XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Numerics;
using System.Runtime.InteropServices.WindowsRuntime;
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml.Hosting;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Windows.Foundation;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Core.Rendering;
using XFEEDToolBox.Core.Services;
using XFEEDToolBox.Rendering;

namespace XFEEDToolBox.Controls;

/// <summary>Interactive system-space scene. Selection is UI-only; the explicit Go command owns all game control.</summary>
public sealed class LocalSystemMapView:UserControl
{
    private readonly Grid host=new(){Background=Brush(7,16,26)};
    private readonly Canvas labels=new(){IsHitTestVisible=false};
    private readonly RectangleGeometry clip=new();
    private readonly TextBlock empty=new(){Text="等待读取真实地点坐标",Foreground=Brush(139,159,181),FontSize=15};
    private readonly List<(Border Border,TextBlock Text)> labelPool=[];
    private readonly List<(Grid Host,GameIcon Icon,Border Badge,TextBlock Count)> iconPool=[];
    private IReadOnlyList<LocalSystemMarkerGroup> markerGroups=[];
    private readonly LocalSystemViewLock viewLock=new();
    private LocalSystemGraphics? graphics;
    private Compositor? compositor;
    private ContainerVisual? visual;
    private SpriteVisual? pixelsVisual;
    private LocalSystemScene? scene;
    private IReadOnlyList<LocalSystemProjection> projections=[];
    private LocalSystemCamera camera=LocalSystemCamera.Overview;
    private string? selected;
    private long system;
    private bool loaded,scheduled,dragging,moved,panning;
    private bool overview=true;
    private bool displayOnly;
    private Point previous,pressed;
    private double overlayLeft,overlayRight;
    private HashSet<string>? markerIdentities;
    public event EventHandler<SystemLocation>? LocationSelected;
    public event Action<string>? SceneStatusChanged;
    public event Action<string,string>? OrbitStatusChanged;
    public string GraphicsError{get;private set;}="";
    public long FrameCount=>graphics?.Frames??0;
    internal LocalSystemCamera CameraState=>camera;
    internal LocalSystemOrientation ViewOrientationForProbe=>viewLock.Orientation;
    internal IReadOnlyList<LocalSystemProjection> ProjectedLocations=>projections;
    internal double ProjectionWidth=>Math.Max(140,ActualWidth);
    private double CameraProjectionWidth=>Math.Max(140,ActualWidth-overlayLeft-overlayRight);
    internal LocalSystemCamera OverviewCameraForProbe=>LocalSystemCamera.OverviewFor(CameraProjectionWidth,ActualHeight);
    internal IReadOnlyList<LocalSystemProjection> MarkerLocations=>projections.Where(IsMarkerVisible).ToArray();
    internal IReadOnlyList<LocalSystemProjection> InteractiveLocations=>MarkerLocations.Where(point=>LocalSystemMarkerLayout.Celestial(point.Node.Location))
        .Concat(markerGroups.SelectMany(group=>group.Members.Select(point=>point with{X=group.Anchor.X,Y=group.Anchor.Y,Radius=12}))).ToArray();
    private bool IsMarkerVisible(LocalSystemProjection point)=>markerIdentities?.Contains(point.Node.Location.Identity)??true;
    internal object OrbitPresentationForProbe=>new{metadata=scene?.OrbitCatalogueCount??0,verified=scene?.VerifiedOrbitCount??0,displayed=scene?.Orbits.Count??0,
        status=scene?.OrbitStatus??"轨道参数待接入",issues=scene?.OrbitUnavailableReasons,
        paths=scene?.Orbits.Select(path=>new{child=path.Child.Identity,parent=path.ParentIdentity,path.ParentDisplayName,path.FocusPosition,path.Orbit.Source,path.Orbit.Quality,
            samples=path.Points.Count,actualMetres=path.ActualPoints,displayPoints=path.Points})};
    internal object PresentationForProbe
    {
        get
        {
            var containerSize=visual?.Size??Vector2.Zero;
            var containerRelative=visual?.RelativeSizeAdjustment??Vector2.Zero;
            var spriteSize=pixelsVisual?.Size??Vector2.Zero;
            var spriteRelative=pixelsVisual?.RelativeSizeAdjustment??Vector2.Zero;
            var effectiveContainer=containerSize+containerRelative*new Vector2((float)host.ActualWidth,(float)host.ActualHeight);
            var effectiveSprite=spriteSize+spriteRelative*effectiveContainer;
            return new
            {
                loaded,scheduled,GraphicsError,frames=FrameCount,
                control=new{width=ActualWidth,height=ActualHeight},
                host=new{width=host.ActualWidth,height=host.ActualHeight},
                container=new{exists=visual is not null,width=containerSize.X,height=containerSize.Y,relativeWidth=containerRelative.X,relativeHeight=containerRelative.Y,
                    effectiveWidth=effectiveContainer.X,effectiveHeight=effectiveContainer.Y},
                sprite=new{exists=pixelsVisual is not null,width=spriteSize.X,height=spriteSize.Y,relativeWidth=spriteRelative.X,relativeHeight=spriteRelative.Y,
                    effectiveWidth=effectiveSprite.X,effectiveHeight=effectiveSprite.Y},
                surface=new{width=graphics?.Surface.Size.Width??0,height=graphics?.Surface.Size.Height??0}
            };
        }
    }
    public LocalSystemMapView()
    {
        var root=new Grid{Clip=clip};root.Children.Add(host);root.Children.Add(labels);labels.Children.Add(empty);Content=root;
        Loaded+=(_,_)=>{loaded=true;EnsureGraphics();ResizePresentation();Invalidate();};
        Unloaded+=(_,_)=>{loaded=false;StopScheduling();dragging=false;};
        SizeChanged+=(_,_)=>{clip.Rect=new(0,0,ActualWidth,ActualHeight);ResizePresentation();Invalidate();};
        host.SizeChanged+=(_,_)=>{ResizePresentation();Invalidate();};
        IsTabStop=true;ManipulationMode=ManipulationModes.None;
        PointerPressed+=Pressed;PointerMoved+=Moved;PointerReleased+=Released;
        PointerCanceled+=(_,_)=>dragging=false;PointerCaptureLost+=(_,_)=>dragging=false;
        PointerWheelChanged+=(_,e)=>{Zoom(e.GetCurrentPoint(this).Properties.MouseWheelDelta/120d);e.Handled=true;};
        DoubleTapped+=(_,e)=>{var hit=LocalSystemScene.HitTest(InteractiveLocations,e.GetPosition(this).X,e.GetPosition(this).Y).FirstOrDefault();if(hit is not null){Select(hit.Node.Location);FocusSelection();e.Handled=true;}};
        KeyDown+=(_,e)=>
        {
            if(e.Key==Windows.System.VirtualKey.Home){ResetCamera();e.Handled=true;}
            else if(e.Key==Windows.System.VirtualKey.Add){Zoom(1);e.Handled=true;}
            else if(e.Key==Windows.System.VirtualKey.Subtract){Zoom(-1);e.Handled=true;}
        };
        Microsoft.UI.Xaml.Automation.AutomationProperties.SetName(this,"当前星系3D场景:点击选择,双击聚焦,左键拖动旋转,右键拖动平移,滚轮缩放,Home总览。");
    }
    public void SetSnapshot(CurrentSystemSnapshot? snapshot,IReadOnlyList<SystemLocation>? visible=null)
    {
        displayOnly=snapshot?.DisplayOnly==true;
        if(snapshot is null){scene=null;projections=[];markerIdentities=null;selected=null;Invalidate();return;}
        if(system!=snapshot.SystemAddress){system=snapshot.SystemAddress;camera=LocalSystemCamera.Overview;overview=true;selected=null;}
        // A verified orbital frame cancels the rotating native reference. If
        // that anchor is temporarily absent, retain the last coherent geometry.
        markerIdentities=visible?.Select(location=>location.Identity).ToHashSet(StringComparer.Ordinal);
        if(displayOnly)scene=new(snapshot);
        else if(viewLock.Observe(snapshot))scene=new(snapshot,null,viewLock.Orientation);
        Invalidate();
    }
    public void SetSelection(string? identity){if(selected==identity)return;selected=identity;Invalidate();}
    public void SetOverlayInsets(double left,double right){overlayLeft=Math.Max(0,left);overlayRight=Math.Max(0,right);Invalidate();}
    public void ResetCamera(){camera=LocalSystemCamera.Overview;overview=true;Invalidate();}
    public void FocusSelection()
    {
        if(scene?.Nodes.FirstOrDefault(node=>node.Location.Identity==selected) is not {} node)return;
        overview=false;camera=camera.Focus(node.Position);Invalidate();
    }
    public void Zoom(double steps){overview=false;camera=camera.Zoom(steps);Invalidate();}
    private void EnsureGraphics()
    {
        if(graphics is not null||GraphicsError.Length>0)return;
        try
        {
            compositor=ElementCompositionPreview.GetElementVisual(host).Compositor;graphics=new(compositor);
            visual=compositor.CreateContainerVisual();pixelsVisual=compositor.CreateSpriteVisual();pixelsVisual.RelativeSizeAdjustment=Vector2.One;
            var surface=compositor.CreateSurfaceBrush(graphics.Surface);surface.Stretch=CompositionStretch.Fill;pixelsVisual.Brush=surface;visual.Children.InsertAtTop(pixelsVisual);
            ElementCompositionPreview.SetElementChildVisual(host,visual);
            ResizePresentation();
        }
        catch(Exception error)
        {
            GraphicsError=error.Message;empty.Text="3D渲染暂不可用,可从左侧列表选择地点。";
            App.Runtime.ReportDiagnostic("current-system.graphics","星系场景",error,ActivitySeverity.Error,"3D场景初始化失败,地点列表仍可使用。");
        }
    }
    private void ResizePresentation()
    {
        // The relative-size sprite fills this container, not the XAML host directly.
        // An unsized ContainerVisual stays 0x0 even while GPU readback has valid pixels.
        if(visual is not null)visual.Size=new((float)Math.Max(0,host.ActualWidth),(float)Math.Max(0,host.ActualHeight));
    }
    private void Invalidate(){if(!loaded||scheduled)return;scheduled=true;CompositionTarget.Rendering+=RenderFrame;}
    private void StopScheduling(){if(!scheduled)return;CompositionTarget.Rendering-=RenderFrame;scheduled=false;}
    private void RenderFrame(object? sender,object e){StopScheduling();if(loaded)Draw();}
    internal void RenderNowForProbe(){StopScheduling();EnsureGraphics();Draw();}
    private static Vector4 Colour(SystemLocationCategory kind)=>kind switch
    {
        SystemLocationCategory.Star=>new(1,.41f,.14f,1),
        SystemLocationCategory.Planet=>new(.38f,.53f,.69f,1),
        SystemLocationCategory.LandablePlanet=>new(.63f,.49f,.34f,1),
        SystemLocationCategory.Station or SystemLocationCategory.Carrier=>new(.73f,.82f,.89f,1),
        SystemLocationCategory.Signal=>new(.42f,.82f,.76f,1),_=>new(.63f,.71f,.8f,1)
    };
    private void Draw()
    {
        ResizePresentation();
        if(ActualWidth<2||ActualHeight<2)return;
        var width=ProjectionWidth;var height=ActualHeight;
        if(overview)camera=OverviewCameraForProbe;
        projections=scene?.Project(camera,CameraProjectionWidth,height,selected).Select(point=>point with{X=point.X+overlayLeft}).ToArray()??[];
        markerGroups=LocalSystemMarkerLayout.Build(projections,MarkerLocations);
        if(GraphicsError.Length==0)empty.Text=scene is null?"等待读取真实地点坐标":"当前筛选没有已定位地点,可调整搜索或地点类型。";
        empty.Visibility=scene is null||scene.Nodes.Count==0||GraphicsError.Length>0?Visibility.Visible:Visibility.Collapsed;
        Canvas.SetLeft(empty,24);Canvas.SetTop(empty,60);
        var bodies=new List<LocalSystemBody>();var lines=new List<LocalSystemLine>();
        void Line(Vector3d from,Vector3d to,Vector4 colour)
        {
            if(Project(from) is not {} a||Project(to) is not {} b)return;
            lines.Add(new(new((float)a.X,(float)a.Y,0,0),colour));lines.Add(new(new((float)b.X,(float)b.Y,0,0),colour));
        }
        for(var index=-12;index<=12;index++)
        {
            var at=index*.5;var alpha=index==0?.28f:.09f;
            Line(new(at,-.08,-6),new(at,-.08,6),new(.2f,.43f,.59f,alpha));
            Line(new(-6,-.08,at),new(6,-.08,at),new(.2f,.43f,.59f,alpha));
        }
        if(scene is not null)foreach(var path in scene.Orbits.OrderBy(path=>path.Child.Identity==selected))
        {
            var colour=path.Child.Identity==selected?new Vector4(1,.68f,.28f,.95f):new Vector4(.32f,.58f,.78f,.4f);
            for(var index=1;index<path.Points.Count;index++)Line(path.Points[index-1],path.Points[index],colour);
        }
        foreach(var point in projections)
        {
            var kind=point.Node.Location.Category;
            if(kind is not (SystemLocationCategory.Star or SystemLocationCategory.Planet or SystemLocationCategory.LandablePlanet))continue;
            var style=kind==SystemLocationCategory.Star?0:1;
            var colour=Colour(kind);
            if(kind==SystemLocationCategory.Star){var spectral=LocalSystemStellarStyle.Colour(point.Node.Location.NativeStarType);colour=new(spectral.R/255f,spectral.G/255f,spectral.B/255f,1);}
            bodies.Add(new(new((float)point.X,(float)point.Y,(float)point.Radius,0),colour,
                new((float)point.Light.X,(float)point.Light.Y,(float)point.Light.Z,0),new(style,point.Selected?1:0,point.Node.Location.Key%31,(float)point.Radius)));
        }
        if(scene?.PlayerPosition is {} ship&&Project(ship) is {} player)
        {
            var angle=0d;
            if(scene.PlayerForward is {} forward){var facing=camera.RotateVector(forward);angle=-Math.Atan2(facing.X,facing.Y);}
            bodies.Add(new(new((float)player.X,(float)player.Y,9,0),new(.33f,.87f,.78f,1),default,new(4,0,0,(float)angle)));
            if(scene.Nodes.FirstOrDefault(node=>node.Location.Identity==selected) is {} destination)
                Line(ship,destination.Position,new(.94f,.63f,.24f,.38f));
        }
        if(graphics is not null)
        {
            try{graphics.Draw(bodies.ToArray(),lines.ToArray(),ActualWidth,height,XamlRoot?.RasterizationScale??1);}
            catch(Exception error)
            {
                GraphicsError=error.Message;empty.Text="3D渲染暂不可用,可从左侧列表选择地点。";empty.Visibility=Visibility.Visible;
                App.Runtime.ReportDiagnostic("current-system.graphics","星系场景",error,ActivitySeverity.Error,"3D场景绘制失败。");
            }
        }
        PlaceIcons();PlaceLabels(width,height);
        OrbitStatusChanged?.Invoke(scene?.OrbitStatus??"轨道参数待接入",scene?.OrbitDescription(selected)??"等待真实轨道参数。");
        SceneStatusChanged?.Invoke(scene is null?"等待真实地点坐标":displayOnly?$"公开目录示意 · {scene.Nodes.Count} 个地点":$"已定位 {scene.Nodes.Count} 个地点 · 显示 {MarkerLocations.Count} 个标记");
    }
    private (double X,double Y,double Depth)? Project(Vector3d point)
        =>camera.Project(point,CameraProjectionWidth,ActualHeight) is {} value?(value.X+overlayLeft,value.Y,value.Depth):null;
    private void PlaceIcons()
    {
        var count=0;
        foreach(var group in markerGroups)
        {
            var point=group.Anchor;
            // Celestial bodies retain their shaded 3D surface. All other
            // destinations use exactly the same asset resolver as the sidebar.
            if(point.Node.Location.Category is SystemLocationCategory.Star or SystemLocationCategory.Planet or SystemLocationCategory.LandablePlanet)continue;
            if(count==iconPool.Count)
            {
                var icon=new GameIcon();var container=new Grid{IsHitTestVisible=false};
                container.Children.Add(new Microsoft.UI.Xaml.Shapes.Ellipse{Stroke=Brush(255,174,59),StrokeThickness=1,Opacity=.8});
                container.Children.Add(icon);
                var number=new TextBlock{FontSize=10,Foreground=Brush(229,238,247),Margin=new(3,0,3,0)};
                var badge=new Border{Child=number,Background=Brush(18,34,48),CornerRadius=new(7),HorizontalAlignment=HorizontalAlignment.Right,VerticalAlignment=VerticalAlignment.Bottom,Margin=new(0,0,-7,-5)};
                container.Children.Add(badge);iconPool.Add((container,icon,badge,number));labels.Children.Insert(0,container);
            }
            var marker=iconPool[count++];var size=22d;
            marker.Icon.IconKey=GameUiIcons.ForLocation(point.Node.Location);marker.Icon.Width=size;marker.Icon.Height=size;
            marker.Count.Text=group.Members.Count.ToString();marker.Badge.Visibility=group.Members.Count>1?Visibility.Visible:Visibility.Collapsed;
            marker.Host.Width=size+8;marker.Host.Height=size+8;marker.Host.Visibility=Visibility.Visible;
            marker.Host.Children[0].Visibility=point.Selected?Visibility.Visible:Visibility.Collapsed;
            Canvas.SetLeft(marker.Host,point.X-(size+8)/2);Canvas.SetTop(marker.Host,point.Y-(size+8)/2);
        }
        for(var index=count;index<iconPool.Count;index++)iconPool[index].Host.Visibility=Visibility.Collapsed;
    }
    internal object[] IconsForProbe=>markerGroups.Select(group=>(object)new{group.Anchor.Node.Location.Identity,icon=GameUiIcons.ForLocation(group.Anchor.Node.Location).ToString(),
        asset=GameUiIcons.Asset(GameUiIcons.ForLocation(group.Anchor.Node.Location)),group.Anchor.X,group.Anchor.Y,members=group.Members.Select(point=>point.Node.Location.Identity)}).ToArray();
    private void PlaceLabels(double width,double height)
    {
        var boxes=new List<Rect>();var count=0;
        var candidates=MarkerLocations.Where(point=>LocalSystemMarkerLayout.Celestial(point.Node.Location)).Concat(markerGroups.Select(group=>group.Anchor))
            .OrderByDescending(point=>point.Selected).ThenBy(point=>point.Node.Location.Category==SystemLocationCategory.Star?0:1)
            .ThenBy(point=>point.Node.Location.DistanceMetres??double.MaxValue);
        foreach(var point in candidates)
        {
            if(count>=28)break;
            var text=point.Node.Location.DisplayName;var labelWidth=Math.Clamp(text.Length*7+14,90,180);var labelHeight=point.Selected?44d:26d;
            var choices=new[]{new Rect(point.X+point.Radius+8,point.Y-10,labelWidth,labelHeight),new Rect(point.X-point.Radius-labelWidth-8,point.Y-10,labelWidth,labelHeight),
                new Rect(point.X-labelWidth/2,point.Y+point.Radius+8,labelWidth,labelHeight),new Rect(point.X-labelWidth/2,point.Y-point.Radius-labelHeight-8,labelWidth,labelHeight)};
            var place=choices.FirstOrDefault(box=>box.X>=overlayLeft+12&&box.Y>=64&&box.Right<=width-overlayRight-12&&box.Bottom<=height-64
                &&!projections.Where(body=>LocalSystemMarkerLayout.Celestial(body.Node.Location)).Any(body=>
                    Math.Pow(body.X-Math.Clamp(body.X,box.X,box.Right),2)+Math.Pow(body.Y-Math.Clamp(body.Y,box.Y,box.Bottom),2)<Math.Pow(body.Radius+5,2))
                &&!boxes.Any(other=>other.X<box.Right&&other.Right>box.X&&other.Y<box.Bottom&&other.Bottom>box.Y));
            if(place.Width==0)continue;
            if(count==labelPool.Count)
            {
                var block=new TextBlock{FontSize=12,TextTrimming=TextTrimming.CharacterEllipsis,MaxLines=2};
                var border=new Border{Child=block,Background=new SolidColorBrush(Windows.UI.Color.FromArgb(185,8,18,29)),Padding=new(5,3,5,3),CornerRadius=new(4)};
                labelPool.Add((border,block));labels.Children.Add(border);
            }
            var label=labelPool[count++];label.Text.Text=point.Selected?text+"\n"+point.Node.Location.DistanceText:text;
            label.Text.Foreground=point.Selected?Brush(255,179,71):Brush(187,207,224);label.Text.FontWeight=point.Selected?Microsoft.UI.Text.FontWeights.SemiBold:Microsoft.UI.Text.FontWeights.Normal;
            label.Border.Width=place.Width;label.Border.Height=place.Height;label.Border.Visibility=Visibility.Visible;
            Canvas.SetLeft(label.Border,place.X);Canvas.SetTop(label.Border,place.Y);boxes.Add(place);
        }
        for(var i=count;i<labelPool.Count;i++)labelPool[i].Border.Visibility=Visibility.Collapsed;
    }
    private void Pressed(object sender,PointerRoutedEventArgs e)
    {
        var point=e.GetCurrentPoint(this);if(!point.Properties.IsLeftButtonPressed&&!point.Properties.IsRightButtonPressed)return;
        Focus(FocusState.Pointer);dragging=true;moved=false;panning=point.Properties.IsRightButtonPressed;previous=pressed=point.Position;CapturePointer(e.Pointer);e.Handled=true;
    }
    private void Moved(object sender,PointerRoutedEventArgs e)
    {
        if(!dragging)return;var point=e.GetCurrentPoint(this).Position;var dx=point.X-previous.X;var dy=point.Y-previous.Y;
        if(Math.Abs(point.X-pressed.X)+Math.Abs(point.Y-pressed.Y)>4)moved=true;
        overview=false;camera=panning?camera.Pan(dx,dy,CameraProjectionWidth,ActualHeight):camera.Rotate(dx,dy);previous=point;Invalidate();e.Handled=true;
    }
    private void Released(object sender,PointerRoutedEventArgs e)
    {
        if(!dragging)return;dragging=false;ReleasePointerCapture(e.Pointer);
        if(!moved&&!panning)SelectAt(e.GetCurrentPoint(this).Position);e.Handled=true;
    }
    private void Select(SystemLocation location){selected=location.Identity;LocationSelected?.Invoke(this,location);Invalidate();}
    private void SelectAt(Point point)
    {
        var hits=LocalSystemScene.HitTest(InteractiveLocations,point.X,point.Y);
        if(hits.Count==0)return;
        if(hits.Count==1){Select(hits[0].Node.Location);return;}
        var picker=new ListView{ItemsSource=hits.Select(hit=>hit.Node.Location.DisplayName+" · "+hit.Node.Location.KindText).ToArray(),MaxHeight=340,MinWidth=260,SelectionMode=ListViewSelectionMode.Single};
        var flyout=new Flyout{Content=picker};
        picker.SelectionChanged+=(_,_)=>{if(picker.SelectedIndex is var index&&index>=0&&index<hits.Count){Select(hits[index].Node.Location);flyout.Hide();}};
        flyout.ShowAt(this,new Microsoft.UI.Xaml.Controls.Primitives.FlyoutShowOptions{Position=point});
    }
    internal void SelectAtForProbe(Point point)
    {
        // Same production hit testing; a deterministic explicit first hit avoids opening a test-only flyout.
        if(LocalSystemScene.HitTest(InteractiveLocations,point.X,point.Y).FirstOrDefault() is {} hit)Select(hit.Node.Location);
    }
    internal void RotateForProbe(double dx,double dy){overview=false;camera=camera.Rotate(dx,dy);RenderNowForProbe();}
    internal void PanForProbe(double dx,double dy){overview=false;camera=camera.Pan(dx,dy,CameraProjectionWidth,ActualHeight);RenderNowForProbe();}
    internal (int Width,int Height,byte[] Pixels) CapturePixelsForProbe()=>graphics?.ReadPixels()??throw new InvalidOperationException(GraphicsError);
    internal async Task<IDisposable> FreezeForXamlProbeAsync()
    {
        RenderNowForProbe();var frame=graphics?.ReadPixels()??throw new InvalidOperationException(GraphicsError);
        var bitmap=new Microsoft.UI.Xaml.Media.Imaging.WriteableBitmap(frame.Width,frame.Height);
        using(var stream=bitmap.PixelBuffer.AsStream())await stream.WriteAsync(frame.Pixels);bitmap.Invalidate();
        var snapshot=new Image{Source=bitmap,Stretch=Stretch.Fill,IsHitTestVisible=false};ElementCompositionPreview.SetElementChildVisual(host,null);host.Children.Add(snapshot);
        return new SnapshotScope(()=>{host.Children.Remove(snapshot);ElementCompositionPreview.SetElementChildVisual(host,visual);Invalidate();});
    }
    private sealed class SnapshotScope(Action restore):IDisposable{public void Dispose()=>restore();}
    private static SolidColorBrush Brush(byte r,byte g,byte b)=>new(Windows.UI.Color.FromArgb(255,r,g,b));
}