#region === Copyright (c) 2010 Pascal van der Heiden === using System; using System.Collections.Generic; using System.Drawing; using System.IO; using System.Runtime.InteropServices; using System.Windows.Forms; using CodeImp.DoomBuilder.Data; using CodeImp.DoomBuilder.Editing; using System.Drawing.Imaging; using CodeImp.DoomBuilder.Geometry; using CodeImp.DoomBuilder.Map; using CodeImp.DoomBuilder.Rendering; using CodeImp.DoomBuilder.Windows; #endregion namespace CodeImp.DoomBuilder.Plugins.VisplaneExplorer { [EditMode(DisplayName = "Visplane Explorer", SwitchAction = "visplaneexplorermode", ButtonImage = "Gauge.png", ButtonOrder = 300, ButtonGroup = "002_tools", Volatile = true, UseByDefault = true, SupportedMapFormats = new[] { "DoomMapSetIO", "HexenMapSetIO" }, //mxd AllowCopyPaste = false)] public class VisplaneExplorerMode : ClassicMode { #region ================== Constants #endregion #region ================== APIs [DllImport("kernel32.dll")] static extern void RtlZeroMemory(IntPtr dst, int length); #endregion #region ================== Variables // The image is the ImageData resource for Doom Builder to work with private DynamicBitmapImage image; // This is the bitmap that we will be drawing on private Bitmap canvas; private ViewStats lastviewstats; // Temporary WAD file written for the vpo.dll library private string tempfile; // Rectangle around the map private Rectangle mapbounds; // 64x64 tiles in map space. These are discarded when outside view. private Dictionary tiles = new Dictionary(); // Time when to do another update private DateTime nextupdate; // Are we processing? private bool processingenabled; #endregion #region ================== Properties #endregion #region ================== Constructor / Destructor #endregion #region ================== Methods // This cleans up anything we used for this mode private void CleanUp() { BuilderPlug.VPO.Stop(); if(processingenabled) { General.Interface.DisableProcessing(); processingenabled = false; } if(!string.IsNullOrEmpty(tempfile)) { File.Delete(tempfile); tempfile = null; } if(image != null) { image.Dispose(); image = null; } if(canvas != null) { canvas.Dispose(); canvas = null; } tiles.Clear(); BuilderPlug.InterfaceForm.HideTooltip(); BuilderPlug.InterfaceForm.RemoveFromInterface(); } // This returns the tile position for the given map coordinate private static Point TileForPoint(float x, float y) { return new Point((int)Math.Floor(x / Tile.TILE_SIZE) * Tile.TILE_SIZE, (int)Math.Floor(y / Tile.TILE_SIZE) * Tile.TILE_SIZE); } // This draws all tiles on the image // THIS MUST BE FAST! TOP PERFORMANCE REQUIRED! private unsafe void RedrawAllTiles() { if(canvas == null) return; // Determine viewport rectangle in map space Vector2D mapleftbot = Renderer.DisplayToMap(new Vector2D(0f, 0f)); Vector2D maprighttop = Renderer.DisplayToMap(new Vector2D(General.Interface.Display.ClientSize.Width, General.Interface.Display.ClientSize.Height)); Rectangle mapviewrect = new Rectangle((int)mapleftbot.x - Tile.TILE_SIZE, (int)maprighttop.y - Tile.TILE_SIZE, (int)maprighttop.x - (int)mapleftbot.x + Tile.TILE_SIZE, (int)mapleftbot.y - (int)maprighttop.y + Tile.TILE_SIZE); int viewstats = (int)BuilderPlug.InterfaceForm.ViewStats; Palette pal = (BuilderPlug.InterfaceForm.ShowHeatmap ? BuilderPlug.Palettes[(int)ViewStats.Heatmap] : BuilderPlug.Palettes[viewstats]); Size canvassize = canvas.Size; BitmapData bd = canvas.LockBits(new Rectangle(0, 0, canvassize.Width, canvassize.Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); RtlZeroMemory(bd.Scan0, bd.Width * bd.Height * 4); int* p = (int*)bd.Scan0.ToPointer(); foreach(KeyValuePair t in tiles) { if(!mapviewrect.Contains(t.Key)) continue; // Map this tile to screen space Vector2D lb = Renderer.MapToDisplay(new Vector2D(t.Value.Position.X, t.Value.Position.Y)); Vector2D rt = Renderer.MapToDisplay(new Vector2D(t.Value.Position.X + Tile.TILE_SIZE, t.Value.Position.Y + Tile.TILE_SIZE)); // Make sure the coordinates are aligned with canvas pixels float x1 = (float)Math.Round(lb.x); float x2 = (float)Math.Round(rt.x); float y1 = (float)Math.Round(rt.y); float y2 = (float)Math.Round(lb.y); // Determine width and height of the screen space area for this tile float w = x2 - x1; float h = y2 - y1; float winv = 1f / w; float hinv = 1f / h; // Loop ranges. These are relative to the left-top of the tile. float sx = 0f; float sy = 0f; float ex = w; float ey = h; int screenx = (int)x1; int screenystart = (int)y1; // Clipping the loop ranges against canvas boundary. if(x1 < 0f) { sx = -x1; screenx = 0; } if(y1 < 0f) { sy = -y1; screenystart = 0; } if(x2 > bd.Width) ex = w - (x2 - bd.Width); if(y2 > bd.Height) ey = h - (y2 - bd.Height); // Draw all pixels within this tile for(float x = sx; x < ex; x++, screenx++) { int screeny = screenystart; for(float y = sy; y < ey; y++, screeny++) { // Calculate the relative offset in map coordinates for this pixel float ux = x * winv * Tile.TILE_SIZE; float uy = y * hinv * Tile.TILE_SIZE; // Get the data and apply the color byte value = t.Value.GetPointByte((int)ux, Tile.TILE_SIZE - 1 - (int)uy, viewstats); p[screeny * bd.Width + screenx] = pal.Colors[value]; } } } canvas.UnlockBits(bd); image.UpdateTexture(); } // This queues points for all current tiles private void QueuePoints(int pointsleft) { // Determine viewport rectangle in map space Vector2D mapleftbot = Renderer.DisplayToMap(new Vector2D(0f, 0f)); Vector2D maprighttop = Renderer.DisplayToMap(new Vector2D(General.Interface.Display.ClientSize.Width, General.Interface.Display.ClientSize.Height)); Rectangle mapviewrect = new Rectangle((int)mapleftbot.x - Tile.TILE_SIZE, (int)maprighttop.y - Tile.TILE_SIZE, (int)maprighttop.x - (int)mapleftbot.x + Tile.TILE_SIZE, (int)mapleftbot.y - (int)maprighttop.y + Tile.TILE_SIZE); while(pointsleft < (VPOManager.POINTS_PER_ITERATION * BuilderPlug.VPO.NumThreads * 5)) { // Collect points from the tiles in the current view List newpoints = new List(tiles.Count); foreach(KeyValuePair t in tiles) if((!t.Value.IsComplete) && (mapviewrect.Contains(t.Key))) newpoints.Add(t.Value.GetNextPoint()); // If the current view is complete, try getting points from all tiles if(newpoints.Count == 0) { foreach(KeyValuePair t in tiles) if(!t.Value.IsComplete) newpoints.Add(t.Value.GetNextPoint()); } if(newpoints.Count == 0) break; pointsleft = BuilderPlug.VPO.EnqueuePoints(newpoints); } } // This updates the overlay private void UpdateOverlay() { // We must redraw the tiles to the canvas when the stats to view has changed if(lastviewstats != BuilderPlug.InterfaceForm.ViewStats) { RedrawAllTiles(); lastviewstats = BuilderPlug.InterfaceForm.ViewStats; } // Render the overlay if(renderer.StartOverlay(true)) { // Render the canvas to screen RectangleF r = new RectangleF(0, 0, canvas.Width, canvas.Height); renderer.RenderRectangleFilled(r, PixelColor.FromColor(Color.White), false, image); // Render any selection if(selecting) RenderMultiSelection(); // Finish our rendering to this layer. renderer.Finish(); } } #endregion #region ================== Events // Mode starts public override void OnEngage() { Cursor.Current = Cursors.WaitCursor; base.OnEngage(); General.Interface.DisplayStatus(StatusType.Busy, "Setting up test environment..."); CleanUp(); BuilderPlug.InterfaceForm.AddToInterface(); lastviewstats = BuilderPlug.InterfaceForm.ViewStats; // Export the current map to a temporary WAD file tempfile = BuilderPlug.MakeTempFilename(".wad"); if(!General.Map.ExportToFile(tempfile)) { //mxd. Abort on export fail Cursor.Current = Cursors.Default; General.Interface.DisplayStatus(StatusType.Warning, "Unable to set test environment..."); OnCancel(); return; } // Load the map in VPO_DLL BuilderPlug.VPO.Start(tempfile, General.Map.Options.LevelName); // Determine map boundary mapbounds = Rectangle.Round(MapSet.CreateArea(General.Map.Map.Vertices)); // Create tiles for all points inside the map Point lt = TileForPoint(mapbounds.Left - Tile.TILE_SIZE, mapbounds.Top - Tile.TILE_SIZE); Point rb = TileForPoint(mapbounds.Right + Tile.TILE_SIZE, mapbounds.Bottom + Tile.TILE_SIZE); Rectangle tilesrect = new Rectangle(lt.X, lt.Y, rb.X - lt.X, rb.Y - lt.Y); NearestLineBlockmap blockmap = new NearestLineBlockmap(tilesrect); for(int x = tilesrect.X; x <= tilesrect.Right; x += Tile.TILE_SIZE) { for(int y = tilesrect.Y; y <= tilesrect.Bottom; y += Tile.TILE_SIZE) { // If the tile is obviously outside the map, don't create it Vector2D pc = new Vector2D(x + (Tile.TILE_SIZE >> 1), y + (Tile.TILE_SIZE >> 1)); Linedef ld = MapSet.NearestLinedef(blockmap.GetBlockAt(pc).Lines, pc); float distancesq = ld.DistanceToSq(pc, true); if(distancesq > (Tile.TILE_SIZE * Tile.TILE_SIZE)) { float side = ld.SideOfLine(pc); if((side > 0.0f) && (ld.Back == null)) continue; } Point tp = new Point(x, y); tiles.Add(tp, new Tile(tp)); } } QueuePoints(0); // Make an image to draw on. // The BitmapImage for Doom Builder's resources must be Format32bppArgb and NOT using color correction, // otherwise DB will make a copy of the bitmap when LoadImage() is called! This is normally not a problem, // but we want to keep drawing to the same bitmap. int width = General.NextPowerOf2(General.Interface.Display.ClientSize.Width); int height = General.NextPowerOf2(General.Interface.Display.ClientSize.Height); canvas = new Bitmap(width, height, PixelFormat.Format32bppArgb); image = new DynamicBitmapImage(canvas, "_CANVAS_"); image.UseColorCorrection = false; image.MipMapLevels = 1; image.LoadImage(); image.CreateTexture(); // Make custom presentation CustomPresentation p = new CustomPresentation(); p.AddLayer(new PresentLayer(RendererLayer.Overlay, BlendingMode.Mask, 1f, false)); p.AddLayer(new PresentLayer(RendererLayer.Grid, BlendingMode.Mask)); p.AddLayer(new PresentLayer(RendererLayer.Geometry, BlendingMode.Alpha, 1f, true)); renderer.SetPresentation(p); // Setup processing nextupdate = DateTime.Now + new TimeSpan(0, 0, 0, 0, 100); General.Interface.EnableProcessing(); processingenabled = true; RedrawAllTiles(); Cursor.Current = Cursors.Default; General.Interface.SetCursor(Cursors.Cross); General.Interface.DisplayReady(); } // Mode ends public override void OnDisengage() { CleanUp(); base.OnDisengage(); } // Cancelled public override void OnCancel() { // Cancel base class base.OnCancel(); // Return to previous mode General.Editing.ChangeMode(General.Editing.PreviousStableMode.Name); } // View position/scale changed! protected override void OnViewChanged() { base.OnViewChanged(); RedrawAllTiles(); // Update the screen sooner nextupdate = DateTime.Now + new TimeSpan(0, 0, 0, 0, 100); } // Draw the display public override void OnRedrawDisplay() { base.OnRedrawDisplay(); // Render the overlay UpdateOverlay(); // Render lines and vertices if(renderer.StartPlotter(true)) { renderer.PlotLinedefSet(General.Map.Map.Linedefs); renderer.PlotVerticesSet(General.Map.Map.Vertices); renderer.Finish(); } renderer.Present(); } // Processing public override void OnProcess(long deltatime) { base.OnProcess(deltatime); if(DateTime.Now >= nextupdate) { // Get the processed points from the VPO manager List points = new List(); int pointsleft = BuilderPlug.VPO.DequeueResults(points); // Queue more points if needed QueuePoints(pointsleft); // Apply the points to the tiles foreach(PointData pd in points) { Tile t; Point tp = TileForPoint(pd.point.x, pd.point.y); if(tiles.TryGetValue(tp, out t)) t.StorePointData(pd); } // Redraw RedrawAllTiles(); General.Interface.RedrawDisplay(); nextupdate = DateTime.Now + new TimeSpan(0, 0, 0, 0, 500); } else { // Queue more points if needed QueuePoints(BuilderPlug.VPO.GetRemainingPoints()); } } // LMB pressed protected override void OnSelectBegin() { StartMultiSelection(); BuilderPlug.InterfaceForm.HideTooltip(); base.OnSelectBegin(); } // Multiselecting protected override void OnUpdateMultiSelection() { base.OnUpdateMultiSelection(); UpdateOverlay(); renderer.Present(); } // Multiselect ends protected override void OnEndMultiSelection() { base.OnEndMultiSelection(); if((selectionrect.Width < 64f) && (selectionrect.Height < 64f)) selectionrect.Inflate(64f, 64f); base.CenterOnArea(selectionrect, 0.1f); } // Mouse moves public override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); if(!selecting) { int viewstats = (int)BuilderPlug.InterfaceForm.ViewStats; // Get the tile data for the current position Point tp = TileForPoint(mousemappos.x, mousemappos.y); Tile t; if(tiles.TryGetValue(tp, out t)) { int x = (int)Math.Floor(mousemappos.x) - t.Position.X; int y = (int)Math.Floor(mousemappos.y) - t.Position.Y; byte b = t.GetPointByte(x, y, viewstats); if(b != Tile.POINT_VOID_B) { // Setup hoverlabel int value = t.GetPointValue(x, y, viewstats); Point p = new Point((int)mousepos.x + 5, (int)mousepos.y + 5); string appendoverflow = (b == Tile.POINT_OVERFLOW_B) ? "+" : ""; BuilderPlug.InterfaceForm.ShowTooltip(value + appendoverflow + " / " + Tile.STATS_LIMITS[viewstats], p); } else { // Void BuilderPlug.InterfaceForm.HideTooltip(); } } else { BuilderPlug.InterfaceForm.HideTooltip(); } } } // Mouse leaves public override void OnMouseLeave(EventArgs e) { base.OnMouseLeave(e); BuilderPlug.InterfaceForm.HideTooltip(); } #endregion } }