// Package d2scene defines the renderer-neutral scene representation used by // D2's raster export pipeline. // // A Document is owned by its builder and is treated as immutable once it is // handed to a renderer. Renderers may therefore read the same document // concurrently at different animation times.
package d2scene import ( ) // Point is a point or vector in logical D2 coordinates. type Point struct { X float64 Y float64 } // Box is an axis-aligned rectangle expressed as an origin and size. // Width and Height may be zero, but must not be negative in a valid scene. type Box struct { X float64 Y float64 Width float64 Height float64 } // Bounds is an axis-aligned bounding box. Valid distinguishes an empty set // from a zero-area bound such as a point or a horizontal line. type Bounds struct { Min Point Max Point Valid bool } // NewBounds constructs valid bounds and normalizes the two corners. func (, , , float64) Bounds { return Bounds{ Min: Point{X: math.Min(, ), Y: math.Min(, )}, Max: Point{X: math.Max(, ), Y: math.Max(, )}, Valid: true, } } // BoundsFromPoints returns the smallest bounds containing points. func ( ...Point) Bounds { var Bounds for , := range { = .Include() } return } // Bounds converts b to min/max form. Negative sizes are normalized here so // geometry helpers remain total; render preflight rejects them in scenes. func ( Box) () Bounds { return NewBounds(.X, .Y, .X+.Width, .Y+.Height) } func ( Bounds) () float64 { if !.Valid { return 0 } return .Max.X - .Min.X } func ( Bounds) () float64 { if !.Valid { return 0 } return .Max.Y - .Min.Y } func ( Bounds) () Box { if !.Valid { return Box{} } return Box{X: .Min.X, Y: .Min.Y, Width: .Width(), Height: .Height()} } // Include returns bounds containing b and p. func ( Bounds) ( Point) Bounds { if !.Valid { return Bounds{Min: , Max: , Valid: true} } .Min.X = math.Min(.Min.X, .X) .Min.Y = math.Min(.Min.Y, .Y) .Max.X = math.Max(.Max.X, .X) .Max.Y = math.Max(.Max.Y, .Y) return } // Union returns the smallest bounds containing both operands. func ( Bounds) ( Bounds) Bounds { if !.Valid { return } if !.Valid { return } return NewBounds( math.Min(.Min.X, .Min.X), math.Min(.Min.Y, .Min.Y), math.Max(.Max.X, .Max.X), math.Max(.Max.Y, .Max.Y), ) } // Intersect returns the common area of two bounds, or invalid bounds when // they do not overlap. func ( Bounds) ( Bounds) Bounds { if !.Valid || !.Valid { return Bounds{} } := math.Max(.Min.X, .Min.X) := math.Max(.Min.Y, .Min.Y) := math.Min(.Max.X, .Max.X) := math.Min(.Max.Y, .Max.Y) if > || > { return Bounds{} } return NewBounds(, , , ) } // Expand grows bounds in both directions. A negative amount shrinks it and // may produce invalid bounds. func ( Bounds) (, float64) Bounds { if !.Valid { return } , := .Min.X-, .Min.Y- , := .Max.X+, .Max.Y+ if > || > { return Bounds{} } return NewBounds(, , , ) } // Translate returns bounds shifted by v. func ( Bounds) ( Point) Bounds { if !.Valid { return } return Bounds{ Min: Point{X: .Min.X + .X, Y: .Min.Y + .Y}, Max: Point{X: .Max.X + .X, Y: .Max.Y + .Y}, Valid: true, } } func ( Bounds) () bool { return !.Valid || (finitePoint(.Min) && finitePoint(.Max)) } func finite( float64) bool { return !math.IsNaN() && !math.IsInf(, 0) } func finitePoint( Point) bool { return finite(.X) && finite(.Y) } // Matrix is an SVG-style affine transform: // // x' = A*x + C*y + E // y' = B*x + D*y + F // // The zero value is the zero transform, not identity. Use Identity explicitly. type Matrix struct { A float64 B float64 C float64 D float64 E float64 F float64 } func () Matrix { return Matrix{A: 1, D: 1} } func (, float64) Matrix { return Matrix{A: 1, D: 1, E: , F: } } func (, float64) Matrix { return Matrix{A: , D: } } // Rotate returns a counter-clockwise rotation in radians in Cartesian // coordinates. In D2's screen coordinate system (positive Y down), it appears // clockwise, matching SVG transform behavior. func ( float64) Matrix { , := math.Sincos() return Matrix{A: , B: , C: -, D: } } func (, , float64) Matrix { return Translate(, ).Mul(Rotate()).Mul(Translate(-, -)) } func ( float64) Matrix { return Matrix{A: 1, C: math.Tan(), D: 1} } func ( float64) Matrix { return Matrix{A: 1, B: math.Tan(), D: 1} } // Mul composes m and right. The right transform is applied to a point first. func ( Matrix) ( Matrix) Matrix { return Matrix{ A: .A*.A + .C*.B, B: .B*.A + .D*.B, C: .A*.C + .C*.D, D: .B*.C + .D*.D, E: .A*.E + .C*.F + .E, F: .B*.E + .D*.F + .F, } } func ( Matrix) ( Point) Point { return Point{ X: .A*.X + .C*.Y + .E, Y: .B*.X + .D*.Y + .F, } } // Vector transforms a vector, excluding translation. func ( Matrix) ( Point) Point { return Point{ X: .A*.X + .C*.Y, Y: .B*.X + .D*.Y, } } func ( Matrix) () float64 { return .A*.D - .B*.C } func ( Matrix) () (Matrix, error) { := .Determinant() if == 0 || !finite() { return Matrix{}, errors.New("d2scene: singular transform") } return Matrix{ A: .D / , B: -.B / , C: -.C / , D: .A / , E: (.C*.F - .D*.E) / , F: (.B*.E - .A*.F) / , }, nil } func ( Matrix) () bool { return finite(.A) && finite(.B) && finite(.C) && finite(.D) && finite(.E) && finite(.F) } // MaxScale is the largest singular value of the linear portion of m. It is // used to conservatively transform stroke and filter radii. func ( Matrix) () float64 { := math.Max(math.Max(math.Abs(.A), math.Abs(.B)), math.Max(math.Abs(.C), math.Abs(.D))) if == 0 || math.IsInf(, 0) || math.IsNaN() { return } , , , := .A/, .B/, .C/, .D/ // Eigenvalues of M^T M are (trace +/- sqrt(trace^2-4*det^2))/2. := * + * + * + * := * - * := math.Max(0, *-4**) return * math.Sqrt(math.Max(0, (+math.Sqrt())/2)) } // Transform returns the axis-aligned bounds of the transformed corners. func ( Bounds) ( Matrix) Bounds { if !.Valid { return } return BoundsFromPoints( .Point(.Min), .Point(Point{X: .Max.X, Y: .Min.Y}), .Point(.Max), .Point(Point{X: .Min.X, Y: .Max.Y}), ) }