[canvaskit] Expose getShadowLocalBounds

Bug: skia:11146
Change-Id: Ib08a96c8d0ca0f4ca2b489b3793e45f642cb231f
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/353034
Reviewed-by: Yegor Jbanov <yjbanov@google.com>
Reviewed-by: Jim Van Verth <jvanverth@google.com>
diff --git a/modules/canvaskit/CHANGELOG.md b/modules/canvaskit/CHANGELOG.md
index 2d9cce3..75d1397 100644
--- a/modules/canvaskit/CHANGELOG.md
+++ b/modules/canvaskit/CHANGELOG.md
@@ -8,6 +8,7 @@
 
 ### Added
  - Constants for the shadow flags. Of note, some of these values can be used on previous releases.
+ - `getShadowLocalBounds()` to estimate the bounds of the shadows drawn by `Canvas.drawShadow`.
 
 ### Breaking
 - `MakeImprovedNoise` is removed.
diff --git a/modules/canvaskit/canvaskit/types/canvaskit-wasm-tests.ts b/modules/canvaskit/canvaskit/types/canvaskit-wasm-tests.ts
index bd45380..adb659c 100644
--- a/modules/canvaskit/canvaskit/types/canvaskit-wasm-tests.ts
+++ b/modules/canvaskit/canvaskit/types/canvaskit-wasm-tests.ts
@@ -351,13 +351,19 @@
     const tf = fm.makeTypefaceFromData(buff1); // $ExpectType Typeface
 }
 
-function globalTests(CK: CanvasKit) {
+function globalTests(CK: CanvasKit, path?: Path) {
+    if (!path) {
+        return;
+    }
     const ctx = CK.currentContext();
     CK.setCurrentContext(ctx);
     CK.deleteContext(ctx);
     const n = CK.getDecodeCacheLimitBytes();
     const u = CK.getDecodeCacheUsedBytes();
     CK.setDecodeCacheLimitBytes(1000);
+    const matr = CK.Matrix.rotated(Math.PI / 6);
+    const p = CK.getShadowLocalBounds(matr, path, [0, 0, 1], [500, 500, 20], 20,
+        CK.ShadowDirectionalLight | CK.ShadowGeometricOnly | CK.ShadowDirectionalLight);
 }
 
 function paintTests(CK: CanvasKit, colorFilter?: ColorFilter, imageFilter?: ImageFilter,
diff --git a/modules/canvaskit/canvaskit/types/index.d.ts b/modules/canvaskit/canvaskit/types/index.d.ts
index 83960f8..0bfc99d 100644
--- a/modules/canvaskit/canvaskit/types/index.d.ts
+++ b/modules/canvaskit/canvaskit/types/index.d.ts
@@ -124,6 +124,26 @@
     RRectXY(rect: InputRect, rx: number, ry: number): RRect;
 
     /**
+     * Generate bounding box for shadows relative to path. Includes both the ambient and spot
+     * shadow bounds. This pairs with Canvas.drawShadow().
+     * See SkShadowUtils.h for more details.
+     * @param ctm - Current transformation matrix to device space.
+     * @param path - The occluder used to generate the shadows.
+     * @param zPlaneParams - Values for the plane function which returns the Z offset of the
+     *                       occluder from the canvas based on local x and y values (the current
+     *                       matrix is not applied).
+     * @param lightPos - The 3D position of the light relative to the canvas plane. This is
+     *                   independent of the canvas's current matrix.
+     * @param lightRadius - The radius of the disc light.
+     * @param flags - See SkShadowFlags.h; 0 means use default options.
+     * @param dstRect - if provided, the bounds will be copied into this rect instead of allocating
+     *                  a new one.
+     * @returns The bounding rectangle or null if it could not be computed.
+     */
+    getShadowLocalBounds(ctm: InputMatrix, path: Path, zPlaneParams: Vector3, lightPos: Vector3,
+                         lightRadius: number, flags: number, dstRect?: Rect): Rect | null;
+
+    /**
      * Malloc returns a TypedArray backed by the C++ memory of the
      * given length. It should only be used by advanced users who
      * can manage memory and initialize values properly. When used
diff --git a/modules/canvaskit/canvaskit_bindings.cpp b/modules/canvaskit/canvaskit_bindings.cpp
index 1f5f60a..edf2de3 100644
--- a/modules/canvaskit/canvaskit_bindings.cpp
+++ b/modules/canvaskit/canvaskit_bindings.cpp
@@ -781,6 +781,16 @@
         return SkImage::MakeRasterData(info, pixelData, rowBytes);
     }), allow_raw_pointers());
 
+    function("_getShadowLocalBounds", optional_override([](
+            uintptr_t /* float* */ ctmPtr, const SkPath& path,
+            const SkPoint3& zPlaneParams, const SkPoint3& lightPos, SkScalar lightRadius,
+            uint32_t flags, uintptr_t /* SkRect* */ outPtr) -> bool {
+        OptionalMatrix ctm(ctmPtr);
+        SkRect* outputBounds = reinterpret_cast<SkRect*>(outPtr);
+        return SkShadowUtils::GetLocalBounds(ctm, path, zPlaneParams, lightPos, lightRadius,
+                              flags, outputBounds);
+    }));
+
 #ifdef SK_SERIALIZE_SKP
     function("_MakePicture", optional_override([](uintptr_t /* unint8_t* */ dPtr,
                                                     size_t bytes)->sk_sp<SkPicture> {
diff --git a/modules/canvaskit/externs.js b/modules/canvaskit/externs.js
index 6508b56..451041c 100644
--- a/modules/canvaskit/externs.js
+++ b/modules/canvaskit/externs.js
@@ -70,7 +70,7 @@
   parseColorString: function() {},
   setCurrentContext: function() {},
   setDecodeCacheLimitBytes: function() {},
-
+  getShadowLocalBounds: function() {},
   // Defined by emscripten.
   createContext: function() {},
 
@@ -84,6 +84,7 @@
   _decodeAnimatedImage: function() {},
   _decodeImage: function() {},
   _drawShapedText: function() {},
+  _getShadowLocalBounds: function() {},
 
   // The testing object is meant to expose internal functions
   // for more fine-grained testing, e.g. parseColor
diff --git a/modules/canvaskit/interface.js b/modules/canvaskit/interface.js
index 7a5e4c9..9c78e05 100644
--- a/modules/canvaskit/interface.js
+++ b/modules/canvaskit/interface.js
@@ -1149,7 +1149,8 @@
     this._drawRect(rPtr, paint);
   };
 
-  CanvasKit.Canvas.prototype.drawShadow = function(path, zPlaneParams, lightPos, lightRadius, ambientColor, spotColor, flags) {
+  CanvasKit.Canvas.prototype.drawShadow = function(path, zPlaneParams, lightPos, lightRadius,
+                                                   ambientColor, spotColor, flags) {
     var ambiPtr = copyColorToWasmNoScratch(ambientColor);
     var spotPtr = copyColorToWasmNoScratch(spotColor);
     this._drawShadow(path, zPlaneParams, lightPos, lightRadius, ambiPtr, spotPtr, flags);
@@ -1157,6 +1158,22 @@
     freeArraysThatAreNotMallocedByUsers(spotPtr, spotColor);
   };
 
+  CanvasKit.getShadowLocalBounds = function(ctm, path, zPlaneParams, lightPos, lightRadius,
+                                            flags, optOutputRect) {
+    var ctmPtr = copy3x3MatrixToWasm(ctm);
+    var ok = this._getShadowLocalBounds(ctmPtr, path, zPlaneParams, lightPos, lightRadius,
+                                        flags, _scratchRectPtr);
+    if (!ok) {
+      return null;
+    }
+    var ta = _scratchRect['toTypedArray']();
+    if (optOutputRect) {
+      optOutputRect.set(ta);
+      return optOutputRect;
+    }
+    return ta.slice();
+  };
+
   // getLocalToDevice returns a 4x4 matrix.
   CanvasKit.Canvas.prototype.getLocalToDevice = function() {
     // _getLocalToDevice will copy the values into the pointer.
diff --git a/modules/canvaskit/tests/core.spec.js b/modules/canvaskit/tests/core.spec.js
index 929f4d3..b723683 100644
--- a/modules/canvaskit/tests/core.spec.js
+++ b/modules/canvaskit/tests/core.spec.js
@@ -927,12 +927,23 @@
                           CanvasKit.BLACK, CanvasKit.MAGENTA, 0);
         canvas.drawText('Default Flags', 5, 250, textPaint, textFont);
 
+        const bounds = CanvasKit.getShadowLocalBounds(CanvasKit.Matrix.identity(),
+            path, zPlaneParams, lightPos, lightRadius, 0);
+        expectTypedArraysToEqual(bounds, Float32Array.of(-3.64462, -12.67541, 245.50, 242.59164));
+
         canvas.translate(250, 250);
         canvas.drawShadow(path, zPlaneParams, lightPos, lightRadius,
                           CanvasKit.BLACK, CanvasKit.MAGENTA,
                           CanvasKit.ShadowTransparentOccluder | CanvasKit.ShadowGeometricOnly | CanvasKit.ShadowDirectionalLight);
         canvas.drawText('All Flags', 5, 250, textPaint, textFont);
 
+        const outBounds = new Float32Array(4);
+        CanvasKit.getShadowLocalBounds(CanvasKit.Matrix.rotated(Math.PI / 6),
+            path, zPlaneParams, lightPos, lightRadius,
+            CanvasKit.ShadowTransparentOccluder | CanvasKit.ShadowGeometricOnly | CanvasKit.ShadowDirectionalLight,
+            outBounds);
+        expectTypedArraysToEqual(outBounds, Float32Array.of(1.52207, -6.35035, 264.03445, 261.83294));
+
         path.delete();
         textFont.delete();
         textPaint.delete();