matrix_utils.dart 24.9 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5

6 7
import 'dart:typed_data';

8
import 'package:flutter/foundation.dart';
9 10 11 12
import 'package:vector_math/vector_math_64.dart';

import 'basic_types.dart';

13
/// Utility functions for working with matrices.
14
class MatrixUtils {
15
  // This class is not meant to be instantiated or extended; this constructor
16
  // prevents instantiation and extension.
17 18
  MatrixUtils._();

19 20
  /// Returns the given [transform] matrix as an [Offset], if the matrix is
  /// nothing but a 2D translation.
21
  ///
22
  /// Otherwise, returns null.
23
  static Offset? getAsTranslation(Matrix4 transform) {
Hixie's avatar
Hixie committed
24
    assert(transform != null);
25
    final Float64List values = transform.storage;
Florian Loitsch's avatar
Florian Loitsch committed
26
    // Values are stored in column-major order.
27
    if (values[0] == 1.0 && // col 1
28 29 30
        values[1] == 0.0 &&
        values[2] == 0.0 &&
        values[3] == 0.0 &&
31
        values[4] == 0.0 && // col 2
32 33 34
        values[5] == 1.0 &&
        values[6] == 0.0 &&
        values[7] == 0.0 &&
35
        values[8] == 0.0 && // col 3
36 37 38
        values[9] == 0.0 &&
        values[10] == 1.0 &&
        values[11] == 0.0 &&
39
        values[14] == 0.0 && // bottom of col 4 (values 12 and 13 are the x and y offsets)
40
        values[15] == 1.0) {
41
      return Offset(values[12], values[13]);
42 43 44 45
    }
    return null;
  }

46 47 48 49
  /// Returns the given [transform] matrix as a [double] describing a uniform
  /// scale, if the matrix is nothing but a symmetric 2D scale transform.
  ///
  /// Otherwise, returns null.
50
  static double? getAsScale(Matrix4 transform) {
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
    assert(transform != null);
    final Float64List values = transform.storage;
    // Values are stored in column-major order.
    if (values[1] == 0.0 && // col 1 (value 0 is the scale)
        values[2] == 0.0 &&
        values[3] == 0.0 &&
        values[4] == 0.0 && // col 2 (value 5 is the scale)
        values[6] == 0.0 &&
        values[7] == 0.0 &&
        values[8] == 0.0 && // col 3
        values[9] == 0.0 &&
        values[10] == 1.0 &&
        values[11] == 0.0 &&
        values[12] == 0.0 && // col 4
        values[13] == 0.0 &&
        values[14] == 0.0 &&
        values[15] == 1.0 &&
        values[0] == values[5]) { // uniform scale
      return values[0];
    }
    return null;
  }

Hixie's avatar
Hixie committed
74 75
  /// Returns true if the given matrices are exactly equal, and false
  /// otherwise. Null values are assumed to be the identity matrix.
76
  static bool matrixEquals(Matrix4? a, Matrix4? b) {
Hixie's avatar
Hixie committed
77 78 79 80
    if (identical(a, b))
      return true;
    assert(a != null || b != null);
    if (a == null)
81
      return isIdentity(b!);
Hixie's avatar
Hixie committed
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
    if (b == null)
      return isIdentity(a);
    assert(a != null && b != null);
    return a.storage[0] == b.storage[0]
        && a.storage[1] == b.storage[1]
        && a.storage[2] == b.storage[2]
        && a.storage[3] == b.storage[3]
        && a.storage[4] == b.storage[4]
        && a.storage[5] == b.storage[5]
        && a.storage[6] == b.storage[6]
        && a.storage[7] == b.storage[7]
        && a.storage[8] == b.storage[8]
        && a.storage[9] == b.storage[9]
        && a.storage[10] == b.storage[10]
        && a.storage[11] == b.storage[11]
        && a.storage[12] == b.storage[12]
        && a.storage[13] == b.storage[13]
        && a.storage[14] == b.storage[14]
        && a.storage[15] == b.storage[15];
  }

103
  /// Whether the given matrix is the identity matrix.
Hixie's avatar
Hixie committed
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
  static bool isIdentity(Matrix4 a) {
    assert(a != null);
    return a.storage[0] == 1.0 // col 1
        && a.storage[1] == 0.0
        && a.storage[2] == 0.0
        && a.storage[3] == 0.0
        && a.storage[4] == 0.0 // col 2
        && a.storage[5] == 1.0
        && a.storage[6] == 0.0
        && a.storage[7] == 0.0
        && a.storage[8] == 0.0 // col 3
        && a.storage[9] == 0.0
        && a.storage[10] == 1.0
        && a.storage[11] == 0.0
        && a.storage[12] == 0.0 // col 4
        && a.storage[13] == 0.0
        && a.storage[14] == 0.0
        && a.storage[15] == 1.0;
  }

124 125 126 127
  /// Applies the given matrix as a perspective transform to the given point.
  ///
  /// This function assumes the given point has a z-coordinate of 0.0. The
  /// z-coordinate of the result is ignored.
128 129 130 131 132 133 134
  ///
  /// While not common, this method may return (NaN, NaN), iff the given `point`
  /// results in a "point at infinity" in homogeneous coordinates after applying
  /// the `transform`. For example, a [RenderObject] may set its transform to
  /// the zero matrix to indicate its content is currently not visible. Trying
  /// to convert an `Offset` to its coordinate space always results in
  /// (NaN, NaN).
135
  static Offset transformPoint(Matrix4 transform, Offset point) {
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
    final Float64List storage = transform.storage;
    final double x = point.dx;
    final double y = point.dy;

    // Directly simulate the transform of the vector (x, y, 0, 1),
    // dropping the resulting Z coordinate, and normalizing only
    // if needed.

    final double rx = storage[0] * x + storage[4] * y + storage[12];
    final double ry = storage[1] * x + storage[5] * y + storage[13];
    final double rw = storage[3] * x + storage[7] * y + storage[15];
    if (rw == 1.0) {
      return Offset(rx, ry);
    } else {
      return Offset(rx / rw, ry / rw);
    }
  }

  /// Returns a rect that bounds the result of applying the given matrix as a
  /// perspective transform to the given rect.
  ///
  /// This version of the operation is slower than the regular transformRect
  /// method, but it avoids creating infinite values from large finite values
  /// if it can.
  static Rect _safeTransformRect(Matrix4 transform, Rect rect) {
    final Float64List storage = transform.storage;
    final bool isAffine = storage[3] == 0.0 &&
        storage[7] == 0.0 &&
        storage[15] == 1.0;

    _accumulate(storage, rect.left,  rect.top,    true,  isAffine);
    _accumulate(storage, rect.right, rect.top,    false, isAffine);
    _accumulate(storage, rect.left,  rect.bottom, false, isAffine);
    _accumulate(storage, rect.right, rect.bottom, false, isAffine);

    return Rect.fromLTRB(_minMax[0], _minMax[1], _minMax[2], _minMax[3]);
  }

174
  static late final Float64List _minMax = Float64List(4);
175
  static void _accumulate(Float64List m, double x, double y, bool first, bool isAffine) {
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195
    final double w = isAffine ? 1.0 : 1.0 / (m[3] * x + m[7] * y + m[15]);
    final double tx = (m[0] * x + m[4] * y + m[12]) * w;
    final double ty = (m[1] * x + m[5] * y + m[13]) * w;
    if (first) {
      _minMax[0] = _minMax[2] = tx;
      _minMax[1] = _minMax[3] = ty;
    } else {
      if (tx < _minMax[0]) {
        _minMax[0] = tx;
      }
      if (ty < _minMax[1]) {
        _minMax[1] = ty;
      }
      if (tx > _minMax[2]) {
        _minMax[2] = tx;
      }
      if (ty > _minMax[3]) {
        _minMax[3] = ty;
      }
    }
Hixie's avatar
Hixie committed
196 197
  }

198 199 200 201 202 203 204
  /// Returns a rect that bounds the result of applying the given matrix as a
  /// perspective transform to the given rect.
  ///
  /// This function assumes the given rect is in the plane with z equals 0.0.
  /// The transformed rect is then projected back into the plane with z equals
  /// 0.0 before computing its bounding rect.
  static Rect transformRect(Matrix4 transform, Rect rect) {
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419
    final Float64List storage = transform.storage;
    final double x = rect.left;
    final double y = rect.top;
    final double w = rect.right - x;
    final double h = rect.bottom - y;

    // We want to avoid turning a finite rect into an infinite one if we can.
    if (!w.isFinite || !h.isFinite) {
      return _safeTransformRect(transform, rect);
    }

    // Transforming the 4 corners of a rectangle the straightforward way
    // incurs the cost of transforming 4 points using vector math which
    // involves 48 multiplications and 48 adds and then normalizing
    // the points using 4 inversions of the homogeneous weight factor
    // and then 12 multiplies. Once we have transformed all of the points
    // we then need to turn them into a bounding box using 4 min/max
    // operations each on 4 values yielding 12 total comparisons.
    //
    // On top of all of those operations, using the vector_math package to
    // do the work for us involves allocating several objects in order to
    // communicate the values back and forth - 4 allocating getters to extract
    // the [Offset] objects for the corners of the [Rect], 4 conversions to
    // a [Vector3] to use [Matrix4.perspectiveTransform()], and then 4 new
    // [Offset] objects allocated to hold those results, yielding 8 [Offset]
    // and 4 [Vector3] object allocations per rectangle transformed.
    //
    // But the math we really need to get our answer is actually much less
    // than that.
    //
    // First, consider that a full point transform using the vector math
    // package involves expanding it out into a vector3 with a Z coordinate
    // of 0.0 and then performing 3 multiplies and 3 adds per coordinate:
    // ```
    // xt = x*m00 + y*m10 + z*m20 + m30;
    // yt = x*m01 + y*m11 + z*m21 + m31;
    // zt = x*m02 + y*m12 + z*m22 + m32;
    // wt = x*m03 + y*m13 + z*m23 + m33;
    // ```
    // Immediately we see that we can get rid of the 3rd column of multiplies
    // since we know that Z=0.0. We can also get rid of the 3rd row because
    // we ignore the resulting Z coordinate. Finally we can get rid of the
    // last row if we don't have a perspective transform since we can verify
    // that the results are 1.0 for all points.  This gets us down to 16
    // multiplies and 16 adds in the non-perspective case and 24 of each for
    // the perspective case. (Plus the 12 comparisons to turn them back into
    // a bounding box.)
    //
    // But we can do even better than that.
    //
    // Under optimal conditions of no perspective transformation,
    // which is actually a very common condition, we can transform
    // a rectangle in as little as 3 operations:
    //
    // (rx,ry) = transform of upper left corner of rectangle
    // (wx,wy) = delta transform of the (w, 0) width relative vector
    // (hx,hy) = delta transform of the (0, h) height relative vector
    //
    // A delta transform is a transform of all elements of the matrix except
    // for the translation components. The translation components are added
    // in at the end of each transform computation so they represent a
    // constant offset for each point transformed. A delta transform of
    // a horizontal or vertical vector involves a single multiplication due
    // to the fact that it only has one non-zero coordinate and no addition
    // of the translation component.
    //
    // In the absence of a perspective transform, the transformed
    // rectangle will be mapped into a parallelogram with corners at:
    // corner1 = (rx, ry)
    // corner2 = corner1 + dTransformed width vector = (rx+wx, ry+wy)
    // corner3 = corner1 + dTransformed height vector = (rx+hx, ry+hy)
    // corner4 = corner1 + both dTransformed vectors = (rx+wx+hx, ry+wy+hy)
    // In all, this method of transforming the rectangle requires only
    // 8 multiplies and 12 additions (which we can reduce to 8 additions if
    // we only need a bounding box, see below).
    //
    // In the presence of a perspective transform, the above conditions
    // continue to hold with respect to the non-normalized coordinates so
    // we can still save a lot of multiplications by computing the 4
    // non-normalized coordinates using relative additions before we normalize
    // them and they lose their "pseudo-parallelogram" relationships.  We still
    // have to do the normalization divisions and min/max all 4 points to
    // get the resulting transformed bounding box, but we save a lot of
    // calculations over blindly transforming all 4 coordinates independently.
    // In all, we need 12 multiplies and 22 additions to construct the
    // non-normalized vectors and then 8 divisions (or 4 inversions and 8
    // multiplies) for normalization (plus the standard set of 12 comparisons
    // for the min/max bounds operations).
    //
    // Back to the non-perspective case, the optimization that lets us get
    // away with fewer additions if we only need a bounding box comes from
    // analyzing the impact of the relative vectors on expanding the
    // bounding box of the parallelogram. First, the bounding box always
    // contains the transformed upper-left corner of the rectangle. Next,
    // each relative vector either pushes on the left or right side of the
    // bounding box and also either the top or bottom side, depending on
    // whether it is positive or negative. Finally, you can consider the
    // impact of each vector on the bounding box independently. If, say,
    // wx and hx have the same sign, then the limiting point in the bounding
    // box will be the one that involves adding both of them to the origin
    // point. If they have opposite signs, then one will push one wall one
    // way and the other will push the opposite wall the other way and when
    // you combine both of them, the resulting "opposite corner" will
    // actually be between the limits they established by pushing the walls
    // away from each other, as below:
    // ```
    //         +---------(originx,originy)--------------+
    //         |            -----^----                  |
    //         |       -----          ----              |
    //         |  -----                   ----          |
    // (+hx,+hy)<                             ----      |
    //         |  ----                            ----  |
    //         |      ----                             >(+wx,+wy)
    //         |          ----                   -----  |
    //         |              ----          -----       |
    //         |                  ---- -----            |
    //         |                      v                 |
    //         +---------------(+wx+hx,+wy+hy)----------+
    // ```
    // In this diagram, consider that:
    // ```
    // wx would be a positive number
    // hx would be a negative number
    // wy and hy would both be positive numbers
    // ```
    // As a result, wx pushes out the right wall, hx pushes out the left wall,
    // and both wy and hy push down the bottom wall of the bounding box. The
    // wx,hx pair (of opposite signs) worked on opposite walls and the final
    // opposite corner had an X coordinate between the limits they established.
    // The wy,hy pair (of the same sign) both worked together to push the
    // bottom wall down by their sum.
    //
    // This relationship allows us to simply start with the point computed by
    // transforming the upper left corner of the rectangle, and then
    // conditionally adding wx, wy, hx, and hy to either the left or top
    // or right or bottom of the bounding box independently depending on sign.
    // In that case we only need 4 comparisons and 4 additions total to
    // compute the bounding box, combined with the 8 multiplications and
    // 4 additions to compute the transformed point and relative vectors
    // for a total of 8 multiplies, 8 adds, and 4 comparisons.
    //
    // An astute observer will note that we do need to do 2 subtractions at
    // the top of the method to compute the width and height.  Add those to
    // all of the relative solutions listed above.  The test for perspective
    // also adds 3 compares to the affine case and up to 3 compares to the
    // perspective case (depending on which test fails, the rest are omitted).
    //
    // The final tally:
    // basic method          = 60 mul + 48 add + 12 compare
    // optimized perspective = 12 mul + 22 add + 15 compare + 2 sub
    // optimized affine      =  8 mul +  8 add +  7 compare + 2 sub
    //
    // Since compares are essentially subtractions and subtractions are
    // the same cost as adds, we end up with:
    // basic method          = 60 mul + 60 add/sub/compare
    // optimized perspective = 12 mul + 39 add/sub/compare
    // optimized affine      =  8 mul + 17 add/sub/compare

    final double wx = storage[0] * w;
    final double hx = storage[4] * h;
    final double rx = storage[0] * x + storage[4] * y + storage[12];

    final double wy = storage[1] * w;
    final double hy = storage[5] * h;
    final double ry = storage[1] * x + storage[5] * y + storage[13];

    if (storage[3] == 0.0 && storage[7] == 0.0 && storage[15] == 1.0) {
      double left  = rx;
      double right = rx;
      if (wx < 0) {
        left  += wx;
      } else {
        right += wx;
      }
      if (hx < 0) {
        left  += hx;
      } else {
        right += hx;
      }

      double top    = ry;
      double bottom = ry;
      if (wy < 0) {
        top    += wy;
      } else {
        bottom += wy;
      }
      if (hy < 0) {
        top    += hy;
      } else {
        bottom += hy;
      }

      return Rect.fromLTRB(left, top, right, bottom);
    } else {
      final double ww = storage[3] * w;
      final double hw = storage[7] * h;
      final double rw = storage[3] * x + storage[7] * y + storage[15];

      final double ulx =  rx            /  rw;
      final double uly =  ry            /  rw;
      final double urx = (rx + wx)      / (rw + ww);
      final double ury = (ry + wy)      / (rw + ww);
      final double llx = (rx      + hx) / (rw      + hw);
      final double lly = (ry      + hy) / (rw      + hw);
      final double lrx = (rx + wx + hx) / (rw + ww + hw);
      final double lry = (ry + wy + hy) / (rw + ww + hw);

      return Rect.fromLTRB(
        _min4(ulx, urx, llx, lrx),
        _min4(uly, ury, lly, lry),
        _max4(ulx, urx, llx, lrx),
        _max4(uly, ury, lly, lry),
      );
    }
420 421
  }

Hixie's avatar
Hixie committed
422
  static double _min4(double a, double b, double c, double d) {
423 424 425
    final double e = (a < b) ? a : b;
    final double f = (c < d) ? c : d;
    return (e < f) ? e : f;
Hixie's avatar
Hixie committed
426 427
  }
  static double _max4(double a, double b, double c, double d) {
428 429 430
    final double e = (a > b) ? a : b;
    final double f = (c > d) ? c : d;
    return (e > f) ? e : f;
Hixie's avatar
Hixie committed
431 432
  }

433 434
  /// Returns a rect that bounds the result of applying the inverse of the given
  /// matrix as a perspective transform to the given rect.
435 436 437 438
  ///
  /// This function assumes the given rect is in the plane with z equals 0.0.
  /// The transformed rect is then projected back into the plane with z equals
  /// 0.0 before computing its bounding rect.
439
  static Rect inverseTransformRect(Matrix4 transform, Rect rect) {
Hixie's avatar
Hixie committed
440
    assert(rect != null);
441 442 443 444
    // As exposed by `unrelated_type_equality_checks`, this assert was a no-op.
    // Fixing it introduces a bunch of runtime failures; for more context see:
    // https://github.com/flutter/flutter/pull/31568
    // assert(transform.determinant != 0.0);
Hixie's avatar
Hixie committed
445 446
    if (isIdentity(transform))
      return rect;
447
    transform = Matrix4.copy(transform)..invert();
448
    return transformRect(transform, rect);
Hixie's avatar
Hixie committed
449
  }
450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483

  /// Create a transformation matrix which mimics the effects of tangentially
  /// wrapping the plane on which this transform is applied around a cylinder
  /// and then looking at the cylinder from a point outside the cylinder.
  ///
  /// The `radius` simulates the radius of the cylinder the plane is being
  /// wrapped onto. If the transformation is applied to a 0-dimensional dot
  /// instead of a plane, the dot would simply translate by +/- `radius` pixels
  /// along the `orientation` [Axis] when rotating from 0 to +/- 90 degrees.
  ///
  /// A positive radius means the object is closest at 0 `angle` and a negative
  /// radius means the object is closest at π `angle` or 180 degrees.
  ///
  /// The `angle` argument is the difference in angle in radians between the
  /// object and the viewing point. A positive `angle` on a positive `radius`
  /// moves the object up when `orientation` is vertical and right when
  /// horizontal.
  ///
  /// The transformation is always done such that a 0 `angle` keeps the
  /// transformed object at exactly the same size as before regardless of
  /// `radius` and `perspective` when `radius` is positive.
  ///
  /// The `perspective` argument is a number between 0 and 1 where 0 means
  /// looking at the object from infinitely far with an infinitely narrow field
  /// of view and 1 means looking at the object from infinitely close with an
  /// infinitely wide field of view. Defaults to a sane but arbitrary 0.001.
  ///
  /// The `orientation` is the direction of the rotation axis.
  ///
  /// Because the viewing position is a point, it's never possible to see the
  /// outer side of the cylinder at or past +/- π / 2 or 90 degrees and it's
  /// almost always possible to end up seeing the inner side of the cylinder
  /// or the back side of the transformed plane before π / 2 when perspective > 0.
  static Matrix4 createCylindricalProjectionTransform({
484 485
    required double radius,
    required double angle,
486 487
    double perspective = 0.001,
    Axis orientation = Axis.vertical,
488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510
  }) {
    assert(radius != null);
    assert(angle != null);
    assert(perspective >= 0 && perspective <= 1.0);
    assert(orientation != null);

    // Pre-multiplied matrix of a projection matrix and a view matrix.
    //
    // Projection matrix is a simplified perspective matrix
    // http://web.iitd.ac.in/~hegde/cad/lecture/L9_persproj.pdf
    // in the form of
    // [[1.0, 0.0, 0.0, 0.0],
    //  [0.0, 1.0, 0.0, 0.0],
    //  [0.0, 0.0, 1.0, 0.0],
    //  [0.0, 0.0, -perspective, 1.0]]
    //
    // View matrix is a simplified camera view matrix.
    // Basically re-scales to keep object at original size at angle = 0 at
    // any radius in the form of
    // [[1.0, 0.0, 0.0, 0.0],
    //  [0.0, 1.0, 0.0, 0.0],
    //  [0.0, 0.0, 1.0, -radius],
    //  [0.0, 0.0, 0.0, 1.0]]
511
    Matrix4 result = Matrix4.identity()
512 513 514 515 516 517
        ..setEntry(3, 2, -perspective)
        ..setEntry(2, 3, -radius)
        ..setEntry(3, 3, perspective * radius + 1.0);

    // Model matrix by first translating the object from the origin of the world
    // by radius in the z axis and then rotating against the world.
518
    result = result * ((
519
        orientation == Axis.horizontal
520 521
            ? Matrix4.rotationY(angle)
            : Matrix4.rotationX(angle)
522
    ) * Matrix4.translationValues(0.0, 0.0, radius)) as Matrix4;
523 524 525 526

    // Essentially perspective * view * model.
    return result;
  }
527 528 529 530 531 532 533

  /// Returns a matrix that transforms every point to [offset].
  static Matrix4 forceToPoint(Offset offset) {
    return Matrix4.identity()
      ..setRow(0, Vector4(0, 0, 0, offset.dx))
      ..setRow(1, Vector4(0, 0, 0, offset.dy));
  }
Florian Loitsch's avatar
Florian Loitsch committed
534
}
535 536 537 538 539

/// Returns a list of strings representing the given transform in a format
/// useful for [TransformProperty].
///
/// If the argument is null, returns a list with the single string "null".
540
List<String> debugDescribeTransform(Matrix4? transform) {
541 542
  if (transform == null)
    return const <String>['null'];
543 544 545 546 547 548
  return <String>[
    '[0] ${debugFormatDouble(transform.entry(0, 0))},${debugFormatDouble(transform.entry(0, 1))},${debugFormatDouble(transform.entry(0, 2))},${debugFormatDouble(transform.entry(0, 3))}',
    '[1] ${debugFormatDouble(transform.entry(1, 0))},${debugFormatDouble(transform.entry(1, 1))},${debugFormatDouble(transform.entry(1, 2))},${debugFormatDouble(transform.entry(1, 3))}',
    '[2] ${debugFormatDouble(transform.entry(2, 0))},${debugFormatDouble(transform.entry(2, 1))},${debugFormatDouble(transform.entry(2, 2))},${debugFormatDouble(transform.entry(2, 3))}',
    '[3] ${debugFormatDouble(transform.entry(3, 0))},${debugFormatDouble(transform.entry(3, 1))},${debugFormatDouble(transform.entry(3, 2))},${debugFormatDouble(transform.entry(3, 3))}',
  ];
549 550 551 552 553 554 555
}

/// Property which handles [Matrix4] that represent transforms.
class TransformProperty extends DiagnosticsProperty<Matrix4> {
  /// Create a diagnostics property for [Matrix4] objects.
  ///
  /// The [showName] and [level] arguments must not be null.
556 557
  TransformProperty(
    String name,
558
    Matrix4? value, {
559
    bool showName = true,
560
    Object? defaultValue = kNoDefaultValue,
561
    DiagnosticLevel level = DiagnosticLevel.info,
562 563 564 565 566 567 568 569 570 571 572
  }) : assert(showName != null),
       assert(level != null),
       super(
         name,
         value,
         showName: showName,
         defaultValue: defaultValue,
         level: level,
       );

  @override
573
  String valueToString({ TextTreeConfiguration? parentConfiguration }) {
574 575 576
    if (parentConfiguration != null && !parentConfiguration.lineBreakProperties) {
      // Format the value on a single line to be compatible with the parent's
      // style.
577
      final List<String> values = <String>[
578 579 580 581
        '${debugFormatDouble(value!.entry(0, 0))},${debugFormatDouble(value!.entry(0, 1))},${debugFormatDouble(value!.entry(0, 2))},${debugFormatDouble(value!.entry(0, 3))}',
        '${debugFormatDouble(value!.entry(1, 0))},${debugFormatDouble(value!.entry(1, 1))},${debugFormatDouble(value!.entry(1, 2))},${debugFormatDouble(value!.entry(1, 3))}',
        '${debugFormatDouble(value!.entry(2, 0))},${debugFormatDouble(value!.entry(2, 1))},${debugFormatDouble(value!.entry(2, 2))},${debugFormatDouble(value!.entry(2, 3))}',
        '${debugFormatDouble(value!.entry(3, 0))},${debugFormatDouble(value!.entry(3, 1))},${debugFormatDouble(value!.entry(3, 2))},${debugFormatDouble(value!.entry(3, 3))}',
582
      ];
583
      return '[${values.join('; ')}]';
584 585 586 587
    }
    return debugDescribeTransform(value).join('\n');
  }
}