private Point2 GetTileAtLocalPos(ICmpTilemapRenderer renderer, Point localPos, TilePickMode pickMode) { Component component = renderer as Component; Transform transform = component.GameObj.Transform; // Determine where the cursor is hovering in various coordinate systems Vector3 worldCursorPos = this.CameraComponent.GetSpaceCoord(new Vector3(localPos.X, localPos.Y, transform.Pos.Z)); Vector2 localCursorPos = transform.GetLocalPoint(worldCursorPos.Xy); // Determine tile coordinates of the cursor return renderer.GetTileAtLocalPos(localCursorPos, pickMode); }
private static void DrawTileHighlights(Canvas canvas, ICmpTilemapRenderer renderer, Point2 origin, IReadOnlyGrid<bool> highlight, ColorRgba fillTint, ColorRgba outlineTint, TileHighlightMode mode, List<Vector2[]> outlineCache = null) { if (highlight.Width == 0 || highlight.Height == 0) return; // Generate strippled line texture if not available yet if (strippledLineTex == null) { PixelData pixels = new PixelData(8, 1); for (int i = 0; i < pixels.Width / 2; i++) pixels[i, 0] = ColorRgba.White; for (int i = pixels.Width / 2; i < pixels.Width; i++) pixels[i, 0] = ColorRgba.TransparentWhite; using (Pixmap pixmap = new Pixmap(pixels)) { strippledLineTex = new Texture(pixmap, TextureSizeMode.Default, TextureMagFilter.Nearest, TextureMinFilter.Nearest, TextureWrapMode.Repeat, TextureWrapMode.Repeat, TexturePixelFormat.Rgba); } } BatchInfo defaultMaterial = new BatchInfo(DrawTechnique.Alpha, canvas.State.Material.MainColor); BatchInfo strippleMaterial = new BatchInfo(DrawTechnique.Alpha, canvas.State.Material.MainColor, strippledLineTex); bool uncertain = (mode & TileHighlightMode.Uncertain) != 0; bool selection = (mode & TileHighlightMode.Selection) != 0; Component component = renderer as Component; Transform transform = component.GameObj.Transform; Tilemap tilemap = renderer.ActiveTilemap; Tileset tileset = tilemap != null ? tilemap.Tileset.Res : null; Vector2 tileSize = tileset != null ? tileset.TileSize : Tileset.DefaultTileSize; Rect localRect = renderer.LocalTilemapRect; // Determine the object's local coordinate system (rotated, scaled) in world space Vector2 worldAxisX = Vector2.UnitX; Vector2 worldAxisY = Vector2.UnitY; MathF.TransformCoord(ref worldAxisX.X, ref worldAxisX.Y, transform.Angle, transform.Scale); MathF.TransformCoord(ref worldAxisY.X, ref worldAxisY.Y, transform.Angle, transform.Scale); Vector2 localOriginPos = tileSize * origin; Vector2 worldOriginPos = localOriginPos.X * worldAxisX + localOriginPos.Y * worldAxisY; canvas.PushState(); { // Configure the canvas so our shapes are properly rotated and scaled canvas.State.TransformHandle = -localRect.TopLeft; canvas.State.TransformAngle = transform.Angle; canvas.State.TransformScale = new Vector2(transform.Scale); // Fill all highlighted tiles that are currently visible { canvas.State.SetMaterial(defaultMaterial); canvas.State.ColorTint = fillTint * ColorRgba.White.WithAlpha(selection ? 0.2f : 0.375f); // Determine tile visibility Vector2 worldTilemapOriginPos = localRect.TopLeft; MathF.TransformCoord(ref worldTilemapOriginPos.X, ref worldTilemapOriginPos.Y, transform.Angle, transform.Scale); TilemapCulling.TileInput cullingIn = new TilemapCulling.TileInput { // Remember: All these transform values are in world space TilemapPos = transform.Pos + new Vector3(worldTilemapOriginPos) + new Vector3(worldOriginPos), TilemapScale = transform.Scale, TilemapAngle = transform.Angle, TileCount = new Point2(highlight.Width, highlight.Height), TileSize = tileSize }; TilemapCulling.TileOutput cullingOut = TilemapCulling.GetVisibleTileRect(canvas.DrawDevice, cullingIn); int renderedTileCount = cullingOut.VisibleTileCount.X * cullingOut.VisibleTileCount.Y; // Draw all visible highlighted tiles { Point2 tileGridPos = cullingOut.VisibleTileStart; Vector2 renderStartPos = worldOriginPos + tileGridPos.X * tileSize.X * worldAxisX + tileGridPos.Y * tileSize.Y * worldAxisY;; Vector2 renderPos = renderStartPos; Vector2 tileXStep = worldAxisX * tileSize.X; Vector2 tileYStep = worldAxisY * tileSize.Y; int lineMergeCount = 0; int totalRects = 0; for (int tileIndex = 0; tileIndex < renderedTileCount; tileIndex++) { bool current = highlight[tileGridPos.X, tileGridPos.Y]; if (current) { // Try to merge consecutive rects in the same line to reduce drawcalls / CPU load bool hasNext = (tileGridPos.X + 1 < highlight.Width) && ((tileGridPos.X + 1 - cullingOut.VisibleTileStart.X) < cullingOut.VisibleTileCount.X); bool next = hasNext ? highlight[tileGridPos.X + 1, tileGridPos.Y] : false; if (next) { lineMergeCount++; } else { totalRects++; canvas.FillRect( transform.Pos.X + renderPos.X - lineMergeCount * tileXStep.X, transform.Pos.Y + renderPos.Y - lineMergeCount * tileXStep.Y, transform.Pos.Z, tileSize.X * (1 + lineMergeCount), tileSize.Y); lineMergeCount = 0; } } tileGridPos.X++; renderPos += tileXStep; if ((tileGridPos.X - cullingOut.VisibleTileStart.X) >= cullingOut.VisibleTileCount.X) { tileGridPos.X = cullingOut.VisibleTileStart.X; tileGridPos.Y++; renderPos = renderStartPos; renderPos += tileYStep * (tileGridPos.Y - cullingOut.VisibleTileStart.Y); } } } } // Draw highlight area outlines, unless flagged as uncertain if (!uncertain) { // Determine the outlines of individual highlighted tile patches if (outlineCache == null) outlineCache = new List<Vector2[]>(); if (outlineCache.Count == 0) { GetTileAreaOutlines(highlight, tileSize, ref outlineCache); } // Draw outlines around all highlighted tile patches canvas.State.SetMaterial(selection ? strippleMaterial : defaultMaterial); canvas.State.ColorTint = outlineTint; foreach (Vector2[] outline in outlineCache) { // For strippled-line display, determine total length of outline if (selection) { float totalLength = 0.0f; for (int i = 1; i < outline.Length; i++) { totalLength += (outline[i - 1] - outline[i]).Length; } canvas.State.TextureCoordinateRect = new Rect(totalLength / strippledLineTex.PixelWidth, 1.0f); } // Draw the outline canvas.DrawPolygon( outline, transform.Pos.X + worldOriginPos.X, transform.Pos.Y + worldOriginPos.Y, transform.Pos.Z); } } // If this is an uncertain highlight, i.e. not actually reflecting the represented action, // draw a gizmo to indicate this for the user. if (uncertain) { Vector2 highlightSize = new Vector2(highlight.Width * tileSize.X, highlight.Height * tileSize.Y); Vector2 highlightCenter = highlightSize * 0.5f; Vector3 circlePos = transform.Pos + new Vector3(worldOriginPos + worldAxisX * highlightCenter + worldAxisY * highlightCenter); float circleRadius = MathF.Min(tileSize.X, tileSize.Y) * 0.2f; canvas.State.SetMaterial(defaultMaterial); canvas.State.ColorTint = outlineTint; canvas.FillCircle( circlePos.X, circlePos.Y, circlePos.Z, circleRadius); } } canvas.PopState(); }
protected override void OnMouseLeave(EventArgs e) { base.OnMouseLeave(e); this.hoveredTile = InvalidTile; this.hoveredRenderer = null; if (this.actionTool == this.toolNone) { this.activeTool = this.toolNone; this.activeTilemap = null; this.activeAreaOrigin = InvalidTile; this.activeArea.ResizeClear(0, 0); } this.UpdateCursor(); this.Invalidate(); }
private void UpdateHoverState(Point cursorPos) { Point2 lastHoveredTile = this.hoveredTile; ICmpTilemapRenderer lastHoveredRenderer = this.hoveredRenderer; // Reset hover data this.hoveredTile = InvalidTile; this.hoveredRenderer = null; // Early-out, if the cursor isn't even inside the CamView area - unless the user is performing a cursor action if (!this.View.ClientRectangle.Contains(cursorPos) && this.actionTool == this.toolNone) { if (lastHoveredTile != this.hoveredTile || lastHoveredRenderer != this.hoveredRenderer) this.Invalidate(); return; } // Early-out, if a camera action claims the cursor if (this.CamActionRequiresCursor) { if (lastHoveredTile != this.hoveredTile || lastHoveredRenderer != this.hoveredRenderer) this.Invalidate(); return; } // While doing an action, it's either the selected tilemap or none. No picking, just determine the hovered tile bool performingAction = this.actionTool != this.toolNone; if (performingAction) { this.hoveredTile = this.GetTileAtLocalPos(this.activeRenderer, cursorPos, TilePickMode.Free); this.hoveredRenderer = this.activeRenderer; } // Otherwise, perform a tile-based picking operation else { List<ICmpTilemapRenderer> visibleRenderers; bool pureZSortPicking = !(this.overrideTool ?? this.selectedTool).PickPreferSelectedLayer || this.selectedTilemap == null; // Determine which renderers we're able to see right now and sort them by their Z values visibleRenderers = this.QueryVisibleTilemapRenderers().ToList(); visibleRenderers.StableSort((a, b) => { // When prefered by the editing tool, the currently edited tilemap always prevails in picking checks if (!pureZSortPicking && a.ActiveTilemap != b.ActiveTilemap) { if (a.ActiveTilemap == this.selectedTilemap) return -1; else if (b.ActiveTilemap == this.selectedTilemap) return 1; } // Otherwise, do regular Z sorting return ((a as Component).GameObj.Transform.Pos.Z + a.BaseDepthOffset > (b as Component).GameObj.Transform.Pos.Z + b.BaseDepthOffset) ? 1 : -1; }); // Eliminate all tilemap renderers without a tile hit, so we remain with only the renderers under the cursor. for (int i = visibleRenderers.Count - 1; i >= 0; i--) { ICmpTilemapRenderer renderer = visibleRenderers[i]; Point2 tileCursorPos = this.GetTileAtLocalPos(renderer, cursorPos, TilePickMode.Reject); if (tileCursorPos == InvalidTile) { visibleRenderers.RemoveAt(i); continue; } } // Iterate over the remaining tilemap renderers to find out which one prevails for (int i = 0; i < visibleRenderers.Count; i++) { ICmpTilemapRenderer renderer = visibleRenderers[i]; Component component = renderer as Component; Point2 tileCursorPos = this.GetTileAtLocalPos(renderer, cursorPos, TilePickMode.Reject); // If the hovered tile is transparent, don't treat it as a hit unless it's the bottom renderer bool isBottomRenderer = (i == visibleRenderers.Count - 1); if (pureZSortPicking && !isBottomRenderer) { Tilemap tilemap = renderer.ActiveTilemap; int tileIndex = (tilemap != null) ? tilemap.Tiles[tileCursorPos.X, tileCursorPos.Y].Index : -1; if (tileIndex == -1 || this.IsTileTransparent(tilemap.Tileset.Res, tileIndex)) { tileCursorPos = InvalidTile; } } // If we're hovering a tile of the current renderer, we're done if (tileCursorPos != InvalidTile) { // If it's locked and not selected, treat it as blocking but don't allow editing bool isLocked = DesignTimeObjectData.Get(component.GameObj).IsLocked; if (isLocked) { bool isSelected = false; IEnumerable<Tilemap> selectedTilemaps = TilemapsEditorSelectionParser.QuerySelectedTilemaps(); foreach (Tilemap tilemap in selectedTilemaps) { if (renderer.ActiveTilemap == tilemap) { isSelected = true; break; } } if (!isSelected) break; } // Set the hovered renderer and stop picking this.hoveredTile = tileCursorPos; this.hoveredRenderer = renderer; break; } } } // If we're not doing an action, let our action begin tile just follow around if (this.actionTool == this.toolNone) this.actionBeginTile = this.hoveredTile; // If something changed, redraw the view if (lastHoveredTile != this.hoveredTile || lastHoveredRenderer != this.hoveredRenderer) { this.Invalidate(); } }
private void UpdateActiveState() { TilemapTool lastActiveTool = this.activeTool; Tilemap lastActiveTilemap = this.activeTilemap; ICmpTilemapRenderer lastActiveRenderer = this.activeRenderer; // If an action is currently being performed, that action will always be the active tool if (this.actionTool != this.toolNone) { this.activeTool = this.actionTool; } // Otherwise, determine what action the cursor would do right now else { // Determine the active tool dynamically based on user input state if (this.hoveredRenderer == null) this.activeTool = this.toolNone; else if (this.selectedTilemap == null) this.activeTool = this.toolSelect; else if (this.hoveredRenderer.ActiveTilemap != this.selectedTilemap) this.activeTool = this.toolSelect; else this.activeTool = this.overrideTool ?? this.selectedTool; // Keep in mind on what renderer and tilemap belong to the currently active tool this.activeTilemap = (this.hoveredRenderer != null) ? this.hoveredRenderer.ActiveTilemap : null; this.activeRenderer = this.hoveredRenderer; } // Determine the area that is affected by the current action this.activeTool.UpdatePreview(); // If our highlighted action changed, redraw view and update the cursor if (lastActiveTool != this.activeTool || lastActiveTilemap != this.activeTilemap || lastActiveRenderer != this.activeRenderer) { this.UpdateCursor(); this.Invalidate(); } }
public override void Perform(IEnumerable <Tilemap> objEnum) { // Since resizing is usually done for all tilemaps in a layered setup, // but users will often have only one of them selected due to previous // editing operations, check for layered setups and deal with them. List <Tilemap> tilemaps = objEnum.ToList(); if (tilemaps.Count == 1) { Tilemap baseTilemap = tilemaps[0]; GameObject baseObject = baseTilemap.GameObj; Scene baseScene = (baseObject != null) ? baseObject.ParentScene : null; // Prerequisite: We are operating on a tilemap with a proper parent and // parent scene available. Otherwise, there is nothing we could do. if (baseScene != null) { // Since a tilemap is just a data container without any relation to a // scene or layered setups, we will check tilemap renderers instead. // Only they can provide the required information for determining layered setups. ICmpTilemapRenderer[] allRenderers = baseScene.FindComponents <ICmpTilemapRenderer>().ToArray(); ICmpTilemapRenderer baseRenderer = allRenderers.FirstOrDefault(r => r.ActiveTilemap == baseTilemap); Transform baseTransform = (baseRenderer != null) ? (baseRenderer as Component).GameObj.Transform : null; if (baseTransform != null) { foreach (ICmpTilemapRenderer renderer in allRenderers) { if (renderer.ActiveTilemap == null) { continue; } if (tilemaps.Contains(renderer.ActiveTilemap)) { continue; } Transform transform = (renderer as Component).GameObj.Transform; if (baseTransform.Pos.Xy != transform.Pos.Xy) { continue; } if (baseTransform.Angle != transform.Angle) { continue; } if (baseTransform.Scale != transform.Scale) { continue; } if (baseTilemap.Size != renderer.ActiveTilemap.Size) { continue; } if (baseRenderer.LocalTilemapRect != renderer.LocalTilemapRect) { continue; } tilemaps.Add(renderer.ActiveTilemap); } } } } // Open the resize dialog. It will handle all the rest. TilemapResizeDialog resizeDialog = new TilemapResizeDialog(); resizeDialog.Tilemaps = tilemaps; resizeDialog.ShowDialog(DualityEditorApp.MainForm); }