Expose glyphIntercepts to CK

bug: skia:12000
Change-Id: I40f130b0eab21ef4a53ee88eed8f62c4f3a577c1
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/408899
Commit-Queue: Mike Reed <reed@google.com>
Reviewed-by: Yegor Jbanov <yjbanov@google.com>
diff --git a/modules/canvaskit/WasmCommon.h b/modules/canvaskit/WasmCommon.h
index 9bc7a1b..b6e3ace 100644
--- a/modules/canvaskit/WasmCommon.h
+++ b/modules/canvaskit/WasmCommon.h
@@ -41,4 +41,29 @@
     return jarray;
 }
 
+/**
+ *  Reads a JS array, and returns a copy of it in the std::vector.
+ *
+ *  It is up to the caller to ensure the T and arrayType-string match
+ *      e.g. T == uint16_t, arrayType == "Uint16Array"
+ *
+ *  Note, this checks the JS type, and if it does not match, it will still attempt
+ *  to return a copy, just by performing a 1-element-at-a-time copy
+ *  via emscripten::vecFromJSArray().
+ */
+template <typename T>
+std::vector<T> CopyTypedArray(JSArray src, const char arrayType[]) {
+    if (src.instanceof(emscripten::val::global(arrayType))) {
+        const size_t len = src["length"].as<size_t>();
+        std::vector<T> dst;
+        dst.resize(len);
+        auto dst_view = emscripten::val(typed_memory_view(len, dst.data()));
+        dst_view.call<void>("set", src);
+        return dst;
+    } else {
+        // copies one at a time
+        return emscripten::vecFromJSArray<T>(src);
+    }
+}
+
 #endif
diff --git a/modules/canvaskit/canvaskit_bindings.cpp b/modules/canvaskit/canvaskit_bindings.cpp
index 5d7a3d7..67ee1ec 100644
--- a/modules/canvaskit/canvaskit_bindings.cpp
+++ b/modules/canvaskit/canvaskit_bindings.cpp
@@ -1209,6 +1209,19 @@
             }
             return j;
         }))
+        .function("getGlyphIntercepts", optional_override([](SkFont& self,
+                                                             JSArray jglyphs,
+                                                             JSArray jpos,
+                                                             float top, float bottom) -> JSArray {
+            auto glyphs = CopyTypedArray<uint16_t>(jglyphs, "Uint16Array");
+            auto pos    = CopyTypedArray<float>(jpos, "Float32Array");
+            if (glyphs.size() > (pos.size() >> 1)) {
+                return emscripten::val("Not enough x,y position pairs for glyphs");
+            }
+            auto sects  = self.getIntercepts(glyphs.data(), SkToInt(glyphs.size()),
+                                             (const SkPoint*)pos.data(), top, bottom);
+            return MakeTypedArray(sects.size(), (const float*)sects.data(), "Float32Array");
+        }), allow_raw_pointers())
         .function("getScaleX", &SkFont::getScaleX)
         .function("getSize", &SkFont::getSize)
         .function("getSkewX", &SkFont::getSkewX)
diff --git a/modules/canvaskit/externs.js b/modules/canvaskit/externs.js
index b3e968b..ed4a462 100644
--- a/modules/canvaskit/externs.js
+++ b/modules/canvaskit/externs.js
@@ -358,6 +358,7 @@
       getGlyphBounds: function() {},
       getGlyphIDs: function() {},
       getGlyphWidths: function() {},
+      getGlyphIntercepts: function() {},
     },
 
     // private API (from C++ bindings)
diff --git a/modules/canvaskit/npm_build/extra.html b/modules/canvaskit/npm_build/extra.html
index e12ad20..40ad87c 100644
--- a/modules/canvaskit/npm_build/extra.html
+++ b/modules/canvaskit/npm_build/extra.html
@@ -368,6 +368,7 @@
 
                     case 'i': editor.applyStyleToSelection({italic:'toggle'}); return;
                     case 'b': editor.applyStyleToSelection({bold:'toggle'}); return;
+                    case 'l': editor.applyStyleToSelection({underline:'toggle'}); return;
 
                     case ']': editor.applyStyleToSelection({size_add:1}); return;
                     case '[': editor.applyStyleToSelection({size_add:-1}); return;
diff --git a/modules/canvaskit/npm_build/textapi_utils.js b/modules/canvaskit/npm_build/textapi_utils.js
index f2e429f..9f46889 100644
--- a/modules/canvaskit/npm_build/textapi_utils.js
+++ b/modules/canvaskit/npm_build/textapi_utils.js
@@ -205,6 +205,7 @@
         color: null,
         bold: null,
         italic: null,
+        underline: null,
 
         _check_toggle: function(src, dst) {
             if (src == 'toggle') {
@@ -234,6 +235,9 @@
             if (src.italic) {
                 this.italic = this._check_toggle(src.italic, this.italic);
             }
+            if (src.underline) {
+                this.underline = this._check_toggle(src.underline, this.underline);
+            }
 
             if (src.size_add) {
                 this.size += src.size_add;
@@ -496,12 +500,32 @@
                     }
                     LOG('    glyph subrange', glyph_start, glyph_end);
                     gly = gly.slice(glyph_start, glyph_end);
-                    pos = pos.slice(glyph_start*2, glyph_end*2);
+                    // +2 at the end so we can see the trailing position (esp. for underlines)
+                    pos = pos.slice(glyph_start*2, glyph_end*2 + 2);
                 } else {
                     LOG('    use entire glyph run');
                 }
                 canvas.drawGlyphs(gly, pos, 0, 0, f, p);
 
+                if (s.underline) {
+                    const gap = 2;
+                    const Y = pos[1];   // first Y
+                    const lastX = pos[gly.length*2];
+                    const sects = f.getGlyphIntercepts(gly, pos, Y+2, Y+4);
+
+                    let x = pos[0];
+                    for (let i = 0; i < sects.length; i += 2) {
+                        const end = sects[i] - gap;
+                        if (x < end) {
+                            canvas.drawRect([x, Y+2, end, Y+4], p);
+                        }
+                        x = sects[i+1] + gap;
+                    }
+                    if (x < lastX) {
+                        canvas.drawRect([x, Y+2, lastX, Y+4], p);
+                    }
+                }
+
                 start = end;
             }
 
diff --git a/modules/canvaskit/npm_build/types/canvaskit-wasm-tests.ts b/modules/canvaskit/npm_build/types/canvaskit-wasm-tests.ts
index 8c75898..e14573cf 100644
--- a/modules/canvaskit/npm_build/types/canvaskit-wasm-tests.ts
+++ b/modules/canvaskit/npm_build/types/canvaskit-wasm-tests.ts
@@ -344,6 +344,22 @@
     font.setSubpixel(true);
     font.setTypeface(null);
     font.setTypeface(face);
+
+    // try unittest for intercepts
+    {
+        font.setTypeface(null);
+        font.setSize(100);
+        const ids = font.getGlyphIDs('I');
+        console.assert(ids.length == 1, "");
+
+        // aim for the middle of the I at 100 point, expecting a hit
+        let sects = font.getGlyphIntercepts(ids, [0, 0], -60, -40);
+        console.assert(sects.length === 2, "expected one pair of intercepts");
+
+        // aim below the baseline where we expect no intercepts
+        sects = font.getGlyphIntercepts(ids, [0, 0], 20, 30);
+        console.assert(ids.length === 0, "expected no intercepts");
+    }
 }
 
 function fontMgrTests(CK: CanvasKit) {
diff --git a/modules/canvaskit/npm_build/types/index.d.ts b/modules/canvaskit/npm_build/types/index.d.ts
index 966a35d..de62a4d 100644
--- a/modules/canvaskit/npm_build/types/index.d.ts
+++ b/modules/canvaskit/npm_build/types/index.d.ts
@@ -1660,6 +1660,26 @@
                    output?: Float32Array): Float32Array;
 
     /**
+     * Computes any intersections of a thick "line" and a run of positionsed glyphs.
+     * The thick line is represented as a top and bottom coordinate (positive for
+     * below the baseline, negative for above). If there are no intersections
+     * (e.g. if this is intended as an underline, and there are no "collisions")
+     * then the returned array will be empty. If there are intersections, the array
+     * will contain pairs of X coordinates [start, end] for each segment that
+     * intersected with a glyph.
+     * 
+     * @param glyphs        the glyphs to intersect with
+     * @param positions     x,y coordinates (2 per glyph) for each glyph
+     * @param top           top of the thick "line" to use for intersection testing
+     * @param bottom        bottom of the thick "line" to use for intersection testing
+     * @param paint         optional (can be null) in case the paint affects the
+     *                      "thickness" of the glyphs (e.g. patheffect, stroking, maskfilter)
+     * @return              array of [start, end] x-coordinate pairs. Maybe be empty.
+     */
+    getGlyphIntercepts(glyphs: InputGlyphIDArray, positions: Float32Array,
+                       top: number, bottom: number): Float32Array;
+
+    /**
      * Returns text scale on x-axis. Default value is 1.
      */
     getScaleX(): number;