如何在Gloss中绘制Picture边界矩形?能否获取/实现其最小包围盒函数?
Great question! Let's break this down step by step—since Gloss doesn't have built-in support for this out of the box, but it's totally feasible to implement yourself.
First: Does Gloss have a built-in function for picture bounding boxes?
Short answer: No, there's no standard function in Gloss that takes a Picture and returns its minimal enclosing rectangle's dimensions and coordinates. You'll need to implement this yourself by traversing Gloss's Picture algebraic data type.
Step 1: Define a Bounding Box Structure
First, let's create a simple data type to represent our bounding box—this makes it easy to store and manipulate the left/right/top/bottom bounds:
data BoundingBox = BoundingBox { bbLeft :: Float -- Leftmost x-coordinate , bbRight :: Float -- Rightmost x-coordinate , bbTop :: Float -- Topmost y-coordinate , bbBottom :: Float -- Bottommost y-coordinate } deriving (Show, Eq)
Step 2: Implement a Bounding Box Calculator
We'll write a recursive function boundingBox :: Picture -> BoundingBox that handles every constructor in the Picture type. Here's a complete implementation with explanations:
import Graphics.Gloss import Graphics.Gloss.Data.Text (textWidth, textHeight) import qualified Graphics.Gloss.Geometry.Angle as Angle -- Merge two bounding boxes into one that contains both mergeBoundingBoxes :: BoundingBox -> BoundingBox -> BoundingBox mergeBoundingBoxes b1 b2 = BoundingBox { bbLeft = min (bbLeft b1) (bbLeft b2) , bbRight = max (bbRight b1) (bbRight b2) , bbTop = max (bbTop b1) (bbTop b2) , bbBottom = min (bbBottom b1) (bbBottom b2) } -- Calculate the bounding box for any Gloss Picture boundingBox :: Picture -> BoundingBox boundingBox Blank = BoundingBox 0 0 0 0 -- Empty picture, trivial bounds boundingBox (Polygon pts) = let xs = map fst pts ys = map snd pts in BoundingBox (minimum xs) (maximum xs) (maximum ys) (minimum ys) boundingBox (Circle r) = BoundingBox (-r) r r (-r) -- Centered at origin boundingBox (ThickCircle r t) = let outerR = r + t in BoundingBox (-outerR) outerR outerR (-outerR) boundingBox (Line pts) = let xs = map fst pts ys = map snd pts in BoundingBox (minimum xs) (maximum xs) (maximum ys) (minimum ys) boundingBox (Text str) = -- Approximate bounds: text starts at (0,0), extends to (width, height) -- Note: This doesn't account for descenders (like y/g) perfectly, but works for most cases let w = textWidth str h = textHeight str in BoundingBox 0 w h 0 boundingBox (Color _ pic) = boundingBox pic -- Color doesn't affect bounds boundingBox (Translate dx dy pic) = let bb = boundingBox pic in BoundingBox (bbLeft bb + dx) (bbRight bb + dx) (bbTop bb + dy) (bbBottom bb + dy) boundingBox (Rotate theta pic) = let bb = boundingBox pic rad = Angle.degToRad theta -- Convert degrees to radians cosθ = cos rad sinθ = sin rad -- Get all four corners of the original bounding box corners = [ (bbLeft bb, bbTop bb) , (bbRight bb, bbTop bb) , (bbRight bb, bbBottom bb) , (bbLeft bb, bbBottom bb) ] -- Rotate each corner using standard 2D rotation formula rotatedCorners = map (\(x,y) -> (x*cosθ - y*sinθ, x*sinθ + y*cosθ)) corners xs = map fst rotatedCorners ys = map snd rotatedCorners in BoundingBox (minimum xs) (maximum xs) (maximum ys) (minimum ys) boundingBox (Scale sx sy pic) = let bb = boundingBox pic scaledLeft = bbLeft bb * sx scaledRight = bbRight bb * sx scaledTop = bbTop bb * sy scaledBottom = bbBottom bb * sy -- Handle negative scaling (which flips axes) by taking min/max in BoundingBox (min scaledLeft scaledRight) (max scaledLeft scaledRight) (max scaledTop scaledBottom) (min scaledTop scaledBottom) boundingBox (Pictures pics) = -- Merge bounds of all child pictures foldl mergeBoundingBoxes (BoundingBox 0 0 0 0) (map boundingBox pics)
Step 3: Draw the Bounding Rectangle
Once you have the bounding box, converting it to a Gloss Picture (to draw as a border) is straightforward. Here's a function that creates a red rectangle around the bounds:
drawBoundingBox :: BoundingBox -> Picture drawBoundingBox bb = let width = bbRight bb - bbLeft bb height = bbTop bb - bbBottom bb centerX = (bbLeft bb + bbRight bb) / 2 centerY = (bbTop bb + bbBottom bb) / 2 -- Gloss's Rectangle uses center coordinates + half-width/half-height in Color red $ Translate centerX centerY $ Rectangle (width/2) (height/2)
Example Usage
Putting it all together, here's a main function that draws a couple of pictures and their combined bounding box:
main :: IO () main = let myArt = Pictures [ Translate 100 50 (Text "Hello Gloss!") , Rotate 30 (Translate (-50) (-20) (Circle 30)) ] artBounds = boundingBox myArt in display (InWindow "Gloss Bounding Box" (400, 400) (10, 10)) white (Pictures [myArt, drawBoundingBox artBounds])
Notes to Keep in Mind
- Text Bounds: The text handling is approximate—Gloss's
textWidthandtextHeightdon't account for descenders (like the bottom of a 'g' or 'y') perfectly. For precise text bounds, you'd need a lower-level text rendering library, but this works for most casual use cases. - Transformations: The function correctly handles
Translate,Rotate, andScale(even negative scaling, which flips the picture). - Edge Cases: Empty
Blankpictures return a trivial bounding box at (0,0), but you can adjust this if needed for your use case.
内容的提问来源于stack exchange,提问作者user1023733

