#version 330 core
// Magnifier "sdf"-style border (Square2 preset; docs-vn/rd/windows/plan/
// 2026-08-12-magnifier.md §2.2b, Mac §4.4). Separate draw pass from
// magnifier.fs's own "zoom"-style border -- unlike that style (one extra
// mask sample, no new state), this one samples a pre-baked signed
// distance field texture (GpuRasterPrimitive::resolveMagnifierSdfTexture)
// and colours a ring of `borderWidth * 0.83` px on BOTH sides of the zero
// crossing, matching Mac §4.4's own literal "两侧" (both sides) text.
// CAWP-19426 (2026-08-20): an earlier Windows-only outward-only variant
// (to avoid the centered stroke eating into the lens's own magnified
// content by half its width) was reverted -- it made the border always
// reach flush against the selection box at border.width=200 with zero
// gap, but a real Mac render was confirmed to leave a visible gap there,
// which only the literal two-sided formula reproduces.
//
// Drawn as its own pass composited UNDER the magnifier.fs pass (same
// "衬在原内容下方" ordering Mac's doc uses for the zoom style too, even
// though the mechanism differs) so the lens's own zoomed content still
// shows through unobstructed inside the mask cutout.
//
// Paired vertex shader: transition_dissolve.vs (plain passthrough, same
// as magnifier.fs / mosaic_pixelate.fs).

out vec4 FragColor;
in  vec2 TexCoord;   // [0,1] over the dst-rect-sized viewport

// Baked SDF texture (GpuRasterPrimitive::resolveMagnifierSdfTexture).
// Sampled with the raw TexCoord, no flip -- same row-0-first upload
// convention as u_maskTex in magnifier.fs/mosaic_pixelate.fs.
uniform sampler2D u_sdfTex;
// Must match loadMagnifierSdfTextureFromSvgContent's own normalisation
// window exactly, or every distance decoded here will be wrong by a
// constant scale factor.
uniform float u_sdfNormalizeRangePx;
// Stroke half-width from the zero crossing, i.e. borderWidth * 0.83
// (precomputed CPU-side, this shader has no MagnifierBorder uniform of
// its own -- keeps this pass's uniform set minimal and mirrors
// magnifier.fs's own "precompute ratios CPU-side" convention for
// u_borderScaleRatio). The coverage test below accepts BOTH sides of
// the zero crossing -- see the header comment above.
uniform float u_strokeHalfWidthPx;
uniform vec4  u_borderColor; // straight (non-premultiplied) RGBA
uniform float u_opacity;
// CAWP-19425/19426: the SDF is baked over a canvas PADDED beyond the
// mask's own 1000-unit authoring canvas (see kPaddedCanvasUnits in
// GpuRasterPrimitive.cpp for why), so the shape occupies only a centered
// sub-rect of this texture -- `lensUv` (which spans [0,1] over the
// shape's own 1000-unit frame == the dst rect) must be remapped into
// that sub-rect before sampling. u_sdfShapeScale is the sub-rect's size
// as a fraction of the texture (1000/1200) and u_sdfShapeOffset its
// origin ((1 - scale) / 2). Defaults of 1/0 make this a no-op for any
// caller that has not been updated.
uniform float u_sdfShapeScale;
uniform float u_sdfShapeOffset;

// Rotation support -- see magnifier.fs's own identical uniform block/
// applyOrient for the full rationale (same applyOrientationUniforms
// CPU-side helper binds both). Defaults are no-op.
uniform int   u_flipX;
uniform int   u_flipY;
uniform float u_rotRad;
uniform vec2  u_dstSize;
uniform vec2  u_unrotSize;
uniform vec2  u_rotPivot01;
uniform vec2  u_anchorPoint01;

vec2 applyOrient(vec2 uv) {
    if (u_rotRad != 0.0) {
        if (u_unrotSize.x > 0.0 && u_unrotSize.y > 0.0) {
            vec2 dPx = (uv - u_rotPivot01) * u_dstSize;
            float s = sin(-u_rotRad);
            float co = cos(-u_rotRad);
            vec2 sPx = vec2(co * dPx.x - s * dPx.y, s * dPx.x + co * dPx.y);
            uv = sPx / u_unrotSize + u_anchorPoint01;
        } else {
            vec2 c = uv - vec2(0.5);
            float s = sin(u_rotRad);
            float co = cos(u_rotRad);
            uv = vec2(co * c.x - s * c.y, s * c.x + co * c.y) + vec2(0.5);
        }
    }
    if (u_flipX != 0) uv.x = 1.0 - uv.x;
    if (u_flipY != 0) uv.y = 1.0 - uv.y;
    return uv;
}

void main() {
    vec2 lensUv = applyOrient(TexCoord);

    // CAWP-19426: a rotated layer's dst rect is expanded to the rotated
    // AABB (see applyOrient's own header comment), so TexCoord -- and
    // therefore lensUv -- covers area OUTSIDE the shape's own [0,1]
    // frame at the AABB's corners. u_sdfTex is GL_CLAMP_TO_EDGE, and an
    // SDF's edge texels encode "distance ~= 0" (near the shape boundary,
    // not "far away") -- sampling past [0,1] keeps reading small
    // distances indefinitely, which fell inside the stroke's coverage
    // band and painted border colour into those AABB corners even
    // though they are nowhere near the actual rotated shape. Discard
    // outside the shape's own frame before doing any of that math.
    if (lensUv.x < 0.0 || lensUv.x > 1.0 || lensUv.y < 0.0 || lensUv.y > 1.0) {
        FragColor = vec4(0.0);
        return;
    }

    // Remap from the shape's own [0,1] frame into the padded texture's
    // own UV space -- see u_sdfShapeScale's comment. Without this the
    // sample lands on the padding instead of the shape, which both
    // shrank/warped the silhouette and left almost no visible ring.
    vec2 sdfUv = lensUv * u_sdfShapeScale + vec2(u_sdfShapeOffset);

    // Decode the [0,255]-normalised texture sample back into signed
    // pixels: inverse of loadMagnifierSdfTextureFromSvgContent's own
    // `(clamped / range * 0.5 + 0.5) * 255` forward mapping.
    float normalized01 = texture(u_sdfTex, sdfUv).r;
    float signedDistPx  = (normalized01 * 2.0 - 1.0) * u_sdfNormalizeRangePx;

    // Stroke coverage: 1.0 for the band [-strokeHalfWidthPx, +strokeHalfWidthPx]
    // straddling the zero crossing on BOTH sides (Mac §4.4's own "两侧").
    // AA uses a FIXED one-raster-texel falloff rather than
    // fwidth(absDist): the derivative-based width ballooned exactly where
    // the distance field bends hardest (Square2's rounded corners),
    // widening the fade there and making those corners read fainter and
    // thinner than the straight edges they should match.
    //
    // CAWP-19425: a gradient-normalisation pass (dividing the decoded
    // distance by its own measured |grad| to undo the 8SSEDT's diagonal
    // over-estimation) was tried here and REVERTED -- with a GL_R8 field
    // normalised over +-256px, one quantisation step is ~2px, so adjacent
    // texels frequently decode to the SAME value, making dFdx/dFdy zero
    // and the division explode. That produced a double ring plus heavy
    // speckle at the corners. Precision is instead addressed at the bake
    // (see kSdfNormalizeRangePx in GpuRasterPrimitive.cpp), which keeps
    // this consumer arithmetic simple and stable.
    float absDistPx = abs(signedDistPx);
    // In raster texels of the baked field (kSdfTexSize=1024), so ~2 here
    // is the same on-screen softness ~1 was at the old 512 bake.
    const float kAaTexels = 2.0;
    float coverage = 1.0 - smoothstep(u_strokeHalfWidthPx - kAaTexels,
                                       u_strokeHalfWidthPx + kAaTexels, absDistPx);

    float alpha = coverage * u_borderColor.a * u_opacity;
    FragColor = vec4(u_borderColor.rgb * alpha, alpha);
}
