.pragma library

function _number(value, fallback) {
    var n = Number(value);
    return isNaN(n) ? fallback : n;
}

function snap(value, dpr) {
    var scale = dpr || 1;
    return Math.round(_number(value, 0) * scale) / scale;
}

function isHorizontal(side) {
    return side === "top" || side === "bottom";
}

function isVertical(side) {
    return side === "left" || side === "right";
}

function bodyRect(descriptor, dpr) {
    var source = descriptor && descriptor.bodyRect ? descriptor.bodyRect : descriptor || {};
    return {
        "x": snap(source.x !== undefined ? source.x : source.bodyX, dpr),
        "y": snap(source.y !== undefined ? source.y : source.bodyY, dpr),
        "width": Math.max(0, snap(source.width !== undefined ? source.width : source.bodyW, dpr)),
        "height": Math.max(0, snap(source.height !== undefined ? source.height : source.bodyH, dpr))
    };
}

function animatedBodyRect(descriptor, dpr) {
    var rect = bodyRect(descriptor, dpr);
    var offset = descriptor && descriptor.animationOffset ? descriptor.animationOffset : descriptor || {};
    var side = descriptor && descriptor.barSide ? descriptor.barSide : "bottom";
    var dx = isVertical(side) ? Math.max(-rect.width, Math.min(_number(offset.x !== undefined ? offset.x : offset.animX, 0), rect.width)) : 0;
    var dy = isHorizontal(side) ? Math.max(-rect.height, Math.min(_number(offset.y !== undefined ? offset.y : offset.animY, 0), rect.height)) : 0;

    return {
        "x": snap(rect.x + (side === "right" ? dx : 0), dpr),
        "y": snap(rect.y + (side === "bottom" ? dy : 0), dpr),
        "width": Math.max(0, snap(rect.width - Math.abs(dx), dpr)),
        "height": Math.max(0, snap(rect.height - Math.abs(dy), dpr)),
        "dx": snap(dx, dpr),
        "dy": snap(dy, dpr)
    };
}

function translatedBodyRect(descriptor, dpr) {
    var rect = bodyRect(descriptor, dpr);
    var offset = descriptor && descriptor.animationOffset ? descriptor.animationOffset : {};
    return {
        "x": snap(rect.x + _number(offset.x, 0), dpr),
        "y": snap(rect.y + _number(offset.y, 0), dpr),
        "width": rect.width,
        "height": rect.height
    };
}

function connectorRadii(descriptor, rect, connectedRadius, surfaceRadius, dpr, nearIncludesSurface) {
    var side = descriptor && descriptor.barSide ? descriptor.barSide : "bottom";
    var horizontal = isHorizontal(side);
    var extent = horizontal ? rect.height : rect.width;
    var crossSize = horizontal ? rect.width : rect.height;
    var nearLimit = nearIncludesSurface ? Math.min(connectedRadius, surfaceRadius, extent, crossSize / 2) : Math.min(connectedRadius, extent, crossSize / 2);
    var farLimit = Math.min(connectedRadius, surfaceRadius, crossSize / 2);
    var near = snap(Math.max(0, nearLimit), dpr);
    var far = snap(Math.max(0, farLimit), dpr);
    var omitStart = !!(descriptor && descriptor.omitStartConnector);
    var omitEnd = !!(descriptor && descriptor.omitEndConnector);
    return {
        "near": near,
        "far": far,
        "start": omitStart ? 0 : near,
        "end": omitEnd ? 0 : near,
        "farStart": omitStart ? far : 0,
        "farEnd": omitEnd ? far : 0,
        "farExtent": Math.max(omitStart ? far : 0, omitEnd ? far : 0)
    };
}

function _connectorWidth(side, spacing, radius) {
    return isVertical(side) ? spacing + radius : radius;
}

function _connectorHeight(side, spacing, radius) {
    return isVertical(side) ? radius : spacing + radius;
}

function connectorRect(side, rect, placement, spacing, radius, dpr) {
    var width = _connectorWidth(side, spacing, radius);
    var height = _connectorHeight(side, spacing, radius);
    var seamX = isVertical(side) ? (side === "left" ? rect.x : rect.x + rect.width) : (placement === "left" ? rect.x : rect.x + rect.width);
    var seamY = side === "top" ? rect.y : (side === "bottom" ? rect.y + rect.height : (placement === "left" ? rect.y : rect.y + rect.height));
    var x = isVertical(side) ? (side === "left" ? seamX : seamX - width) : (placement === "left" ? seamX - width : seamX);
    var y = side === "top" ? seamY : (side === "bottom" ? seamY - height : (placement === "left" ? seamY - height : seamY));
    return {
        "x": snap(x, dpr),
        "y": snap(y, dpr),
        "width": Math.max(0, snap(width, dpr)),
        "height": Math.max(0, snap(height, dpr))
    };
}

function farConnectorRect(side, rect, placement, radius, dpr) {
    var x;
    var y;
    if (isHorizontal(side)) {
        x = placement === "left" ? rect.x : rect.x + rect.width - radius;
        y = side === "top" ? rect.y + rect.height : rect.y - radius;
    } else {
        x = side === "left" ? rect.x + rect.width : rect.x - radius;
        y = placement === "left" ? rect.y : rect.y + rect.height - radius;
    }
    return {
        "x": snap(x, dpr),
        "y": snap(y, dpr),
        "width": Math.max(0, snap(radius, dpr)),
        "height": Math.max(0, snap(radius, dpr))
    };
}

function farBodyCapRect(side, rect, placement, radius, dpr) {
    var x;
    var y;
    if (isHorizontal(side)) {
        x = placement === "left" ? rect.x : rect.x + rect.width - radius;
        y = side === "top" ? rect.y + rect.height - radius : rect.y;
    } else {
        x = side === "left" ? rect.x + rect.width - radius : rect.x;
        y = placement === "left" ? rect.y : rect.y + rect.height - radius;
    }
    return {
        "x": snap(x, dpr),
        "y": snap(y, dpr),
        "width": Math.max(0, snap(radius, dpr)),
        "height": Math.max(0, snap(radius, dpr))
    };
}

function chromeBounds(rect, side, startRadius, endRadius, farExtent, dpr) {
    var horizontal = isHorizontal(side);
    var bodyOffsetX = horizontal ? startRadius : (side === "right" ? farExtent : 0);
    var bodyOffsetY = horizontal ? (side === "bottom" ? farExtent : 0) : startRadius;
    return {
        "x": snap(rect.x - bodyOffsetX, dpr),
        "y": snap(rect.y - bodyOffsetY, dpr),
        "width": Math.max(0, snap(horizontal ? rect.width + startRadius + endRadius : rect.width + farExtent, dpr)),
        "height": Math.max(0, snap(horizontal ? rect.height + farExtent : rect.height + startRadius + endRadius, dpr)),
        "bodyOffsetX": snap(bodyOffsetX, dpr),
        "bodyOffsetY": snap(bodyOffsetY, dpr)
    };
}

function fillBounds(rect, side, seamOverlap, dpr) {
    var overlapX = isHorizontal(side) ? seamOverlap : 0;
    var overlapY = isVertical(side) ? seamOverlap : 0;
    return {
        "x": snap(rect.x - overlapX, dpr),
        "y": snap(rect.y - overlapY, dpr),
        "width": Math.max(0, snap(rect.width + overlapX * 2, dpr)),
        "height": Math.max(0, snap(rect.height + overlapY * 2, dpr))
    };
}

function clipEnvelope(rect, side, radii, seamOverlap, dpr) {
    var fill = fillBounds(rect, side, seamOverlap, dpr);
    var chrome = chromeBounds(fill, side, radii.start, radii.end, radii.farExtent, dpr);
    return {
        "x": chrome.x,
        "y": chrome.y,
        "width": chrome.width,
        "height": chrome.height,
        "bodyX": snap(fill.x - chrome.x, dpr),
        "bodyY": snap(fill.y - chrome.y, dpr),
        "bodyWidth": fill.width,
        "bodyHeight": fill.height
    };
}

function blurRegions(descriptor, rect, radii, dpr) {
    var side = descriptor.barSide;
    var regions = [bodyRect(rect, dpr)];
    if (radii.start > 0)
        regions.push(connectorRect(side, rect, "left", 0, radii.start, dpr));
    if (radii.end > 0)
        regions.push(connectorRect(side, rect, "right", 0, radii.end, dpr));
    if (radii.farStart > 0) {
        regions.push(farConnectorRect(side, rect, "left", radii.farStart, dpr));
        regions.push(farBodyCapRect(side, rect, "left", radii.farStart, dpr));
    }
    if (radii.farEnd > 0) {
        regions.push(farConnectorRect(side, rect, "right", radii.farEnd, dpr));
        regions.push(farBodyCapRect(side, rect, "right", radii.farEnd, dpr));
    }
    return regions;
}

function unionBounds(rects, padding, dpr) {
    var minX = Infinity;
    var minY = Infinity;
    var maxX = -Infinity;
    var maxY = -Infinity;
    for (var i = 0; i < rects.length; i++) {
        var rect = rects[i];
        if (!rect || rect.width <= 0 || rect.height <= 0)
            continue;
        minX = Math.min(minX, rect.x);
        minY = Math.min(minY, rect.y);
        maxX = Math.max(maxX, rect.x + rect.width);
        maxY = Math.max(maxY, rect.y + rect.height);
    }
    if (minX === Infinity)
        return {"x": 0, "y": 0, "width": 0, "height": 0};
    var pad = Math.max(0, _number(padding, 0));
    return {
        "x": snap(minX - pad, dpr),
        "y": snap(minY - pad, dpr),
        "width": Math.max(0, snap(maxX - minX + pad * 2, dpr)),
        "height": Math.max(0, snap(maxY - minY + pad * 2, dpr))
    };
}

function shadowSourceBounds(descriptor, rect, radii, padding, dpr) {
    return unionBounds(blurRegions(descriptor, rect, radii, dpr), padding, dpr);
}

function stableEqual(a, b, dpr) {
    if (!a || !b)
        return false;
    var threshold = 0.5 / (dpr || 1);
    return Math.abs(a.x - b.x) < threshold && Math.abs(a.y - b.y) < threshold && Math.abs(a.width - b.width) < threshold && Math.abs(a.height - b.height) < threshold;
}
