/*
 *  $Id: tip-model.c 29754 2026-04-09 09:35:42Z yeti-dn $
 *  Copyright (C) 2003-2025 David Necas (Yeti), Petr Klapetek.
 *  E-mail: yeti@gwyddion.net, klapetek@gwyddion.net.
 *
 *  This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public
 *  License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any
 *  later version.
 *
 *  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 *  warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 *  details.
 *
 *  You should have received a copy of the GNU General Public License along with this program; if not, write to the
 *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include "config.h"
#include <string.h>
#include <glib/gi18n-lib.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/stats.h"
#include "libgwyddion/filters.h"
#include "libgwyddion/grains.h"
#include "libgwyddion/tip-model.h"

#include "libgwyddion/internal.h"

#define CONTACT_ANGLE (0.5*G_PI - atan(G_SQRT2))
#define NONCONTACT_ANGLE (0.5*G_PI - gwy_deg2rad(70.0))

typedef void (*TipModelFunc)(GwyField *tip,
                             const gdouble *params);
typedef void (*TipSizeFunc)(const gdouble *params,
                            gdouble height,
                            gdouble dx,
                            gint *xres,
                            gint *yres);

typedef struct {
    const gchar *name;
    const gchar *group_name;
    TipModelFunc model_tip;
    TipSizeFunc guess_size;
    gint nparams;
    const GwyTipParamType *params;
} TipModelBuiltin;

struct _GwyTipModelPrivate {
    const TipModelBuiltin *builtin;
};

#define DECLARE_TIP_MODEL(name) \
    static void name##_model(GwyField *tip, \
                             const gdouble *params); \
    static void name##_size(const gdouble *params, \
                            gdouble height, \
                            gdouble dx, \
                            gint *xres, \
                            gint *yres)

#define MODEL_DATA(name) name##_model, name##_size, G_N_ELEMENTS(name##_params), name##_params

DECLARE_TIP_MODEL(pyramid);
DECLARE_TIP_MODEL(contact);
DECLARE_TIP_MODEL(noncontact);
DECLARE_TIP_MODEL(delta);
DECLARE_TIP_MODEL(parabola);
DECLARE_TIP_MODEL(cone);
DECLARE_TIP_MODEL(ell_parabola);
DECLARE_TIP_MODEL(sphere);

static GwyTipModel* create_model_builtin   (const TipModelBuiltin *builtin);
static void         resource_setup_builtins(GwyInventory *inventory);
static gboolean     check_tip_zrange       (GwyField *tip,
                                            gdouble zrange,
                                            gdouble *zrange_leftright,
                                            gdouble *zrange_topbottom,
                                            guint *can_shrink_width,
                                            guint *can_shrink_height);

static const GwyTipParamType pyramid_params[] = {
    GWY_TIP_PARAM_RADIUS,
    GWY_TIP_PARAM_ROTATION,
    GWY_TIP_PARAM_NSIDES,
    GWY_TIP_PARAM_SLOPE,
};

static const GwyTipParamType contact_params[] = {
    GWY_TIP_PARAM_RADIUS,
    GWY_TIP_PARAM_ROTATION,
};

static const GwyTipParamType noncontact_params[] = {
    GWY_TIP_PARAM_RADIUS,
    GWY_TIP_PARAM_ROTATION,
};

static const GwyTipParamType delta_params[] = {
    GWY_TIP_PARAM_HEIGHT,
};

static const GwyTipParamType parabola_params[] = {
    GWY_TIP_PARAM_RADIUS,
};

static const GwyTipParamType cone_params[] = {
    GWY_TIP_PARAM_RADIUS,
    GWY_TIP_PARAM_SLOPE,
};

static const GwyTipParamType ell_parabola_params[] = {
    GWY_TIP_PARAM_RADIUS,
    GWY_TIP_PARAM_ROTATION,
    GWY_TIP_PARAM_ANISOTROPY,
};

static const GwyTipParamType sphere_params[] = {
    GWY_TIP_PARAM_RADIUS,
};

/* Must match the GwyTipTyp enum! */
static const TipModelBuiltin models[] = {
    { N_("Pyramid"),             N_("Pyramidal"),  MODEL_DATA(pyramid),      },
    { N_("Contact"),             N_("Pyramidal"),  MODEL_DATA(contact),      },
    { N_("Non-contact"),         N_("Pyramidal"),  MODEL_DATA(noncontact),   },
    { N_("Delta function"),      N_("Analytical"), MODEL_DATA(delta),        },
    { N_("Parabola"),            N_("Symmetric"),  MODEL_DATA(parabola),     },
    { N_("Cone"),                N_("Symmetric"),  MODEL_DATA(cone),         },
    { N_("Elliptical parabola"), N_("Asymmetric"), MODEL_DATA(ell_parabola), },
    { N_("Ball on stick"),       N_("Symmetric"),  MODEL_DATA(sphere),       },
};

static GwyResourceClass *parent_class = NULL;

G_DEFINE_TYPE_WITH_CODE(GwyTipModel, gwy_tip_model, GWY_TYPE_RESOURCE, G_ADD_PRIVATE(GwyTipModel))

static void
gwy_tip_model_class_init(GwyTipModelClass *klass)
{
    GwyResourceClass *res_class = GWY_RESOURCE_CLASS(klass);

    parent_class = gwy_tip_model_parent_class;
    res_class->item_type = *gwy_resource_class_get_item_type(parent_class);

    res_class->item_type.type = G_TYPE_FROM_CLASS(klass);
    res_class->name = "tipmodels";
    res_class->inventory = gwy_inventory_new(&res_class->item_type);
    gwy_inventory_set_default_item_name(res_class->inventory, models[0].name);
    gwy_inventory_forget_order(res_class->inventory);
    res_class->setup_builtins = resource_setup_builtins;
}

static void
gwy_tip_model_init(GwyTipModel *model)
{
    model->priv = gwy_tip_model_get_instance_private(model);
}

static void
resource_setup_builtins(GwyInventory *res_inventory)
{
    for (guint i = 0; i < G_N_ELEMENTS(models); i++) {
        GwyTipModel *model = create_model_builtin(models + i);
        gwy_inventory_insert_item(res_inventory, model);
        g_object_unref(model);
    }
    gwy_inventory_restore_order(res_inventory);
}

static GwyTipModel*
create_model_builtin(const TipModelBuiltin *builtin)
{
    GwyTipModel *model = g_object_new(GWY_TYPE_TIP_MODEL,
                                      "name", builtin->name,
                                      "const", TRUE,
                                      NULL);
    model->priv->builtin = builtin;

    return model;
}

static void
guess_size_symmetrical(gdouble height, gdouble radius, gdouble angle, gdouble dx,
                       gint *xres, gint *yres)
{
    /* radius*cos(angle) is the maximum lateral dimension of the spherical part of the tip – for sane angles it takes
     * little height so we do not take that into accout; height*tan(angle) is the actual width from the sloped part. */
    gdouble xreal = height*tan(angle) + radius*cos(angle);
    gint xpix = 2*GWY_ROUND(dx*xreal) + 1;
    xpix = CLAMP(xpix, 5, 1201);

    *xres = xpix;
    *yres = xpix;
}

static void
pyramid_size(const gdouble *params, gdouble height, gdouble dx,
             gint *xres, gint *yres)
{
    gdouble radius = params[0], angle = params[3];
    guess_size_symmetrical(height, radius, angle, dx, xres, yres);
}

static void
contact_size(const gdouble *params, gdouble height, gdouble dx,
             gint *xres, gint *yres)
{
    gdouble radius = params[0];
    guess_size_symmetrical(height, radius, CONTACT_ANGLE, dx, xres, yres);
}

static void
noncontact_size(const gdouble *params, gdouble height, gdouble dx,
                gint *xres, gint *yres)
{
    gdouble radius = params[0];
    guess_size_symmetrical(height, radius, NONCONTACT_ANGLE, dx, xres, yres);
}

static void
parabola_size(const gdouble *params, gdouble height, gdouble dx,
              gint *xres, gint *yres)
{
    gdouble radius = params[0];
    gdouble xreal = sqrt(2*height*radius);
    gint xpix = 2*GWY_ROUND(dx*xreal) + 1;
    xpix = CLAMP(xpix, 5, 1201);

    *xres = xpix;
    *yres = xpix;
}

static void
ell_parabola_size(const gdouble *params, gdouble height, gdouble dx,
                  gint *xres, gint *yres)
{
    gdouble radius = params[0], anisotropy = params[2];
    gdouble r1 = radius*sqrt(anisotropy), r2 = radius/sqrt(anisotropy);
    gdouble rmax = fmax(r1, r2);
    parabola_size(&rmax, height, dx, xres, yres);
}

static void
cone_size(const gdouble *params, gdouble height, gdouble dx,
          gint *xres, gint *yres)
{
    gdouble radius = params[0], angle = params[1];
    guess_size_symmetrical(height, radius, angle, dx, xres, yres);
}

static void
delta_size(G_GNUC_UNUSED const gdouble *params, G_GNUC_UNUSED gdouble height, G_GNUC_UNUSED gdouble dx,
           gint *xres, gint *yres)
{
    *xres = *yres = 3;
}

static void
sphere_size(const gdouble *params, gdouble height, gdouble dx,
            gint *xres, gint *yres)
{
    gdouble radius = params[0];
    guess_size_symmetrical(height, radius, 1e-5, dx, xres, yres);

    gint xpix = GWY_ROUND(dx*1.05*radius) + 1;
    xpix = CLAMP(xpix, 1, 600);
    *xres += 2*xpix;
    *yres += 2*xpix;
}

static void
create_pyramid(GwyField *tip, gdouble alpha, gint n, gdouble theta)
{
    gint xres = tip->xres, yres = tip->yres;
    gdouble pxsize = sqrt(xres*xres + yres*yres)/2.0;
    gdouble add = (n == 3 ? G_PI/6 : G_PI/4);
    gdouble nangle = G_PI/n;
    gdouble height = gwy_field_get_dy(tip)*pxsize * cos(nangle)/tan(alpha);
    gint scol = xres/2, srow = yres/2;
    gdouble ca = cos(theta + add);
    gdouble sa = sin(theta + add);
    gdouble ir = 1.0/(pxsize*cos(nangle));
    gdouble *d = gwy_field_get_data(tip);
    for (gint row = 0; row < yres; row++) {
        for (gint col = 0; col < xres; col++) {
            gint ccol = col - scol;
            gint crow = row - srow;
            gdouble rcol = -ccol*ca + crow*sa;
            gdouble rrow = ccol*sa + crow*ca;
            gdouble phi = atan2(rrow, rcol) + G_PI;
            gdouble phic = floor(phi/(2*nangle))*2*nangle + nangle;
            gdouble vm = rcol*cos(phic) + rrow*sin(phic);
            d[xres*row + col] = height*(1 + vm*ir);
        }
    }
}

static void
round_pyramid(GwyField *tip, gdouble angle, gint n, gdouble ballradius)
{
    gint xres = tip->xres, yres = tip->yres;
    //gdouble pxsize = sqrt(xres*xres + yres*yres)/2.0;
    gdouble height = gwy_field_max(tip);
    gdouble dx = gwy_field_get_dx(tip), dy = gwy_field_get_dy(tip);
    //gdouble beta = atan(dy*pxsize/height);
    gdouble center_x = gwy_field_get_xreal(tip)/2;
    gdouble center_y = gwy_field_get_yreal(tip)/2;
    gdouble beta = atan(tan(angle)/cos(G_PI/n));
    gdouble center_z = height - ballradius/sin(beta);
    gwy_debug("z:%g, height=%g, ballradius=%g, cosbeta=%g, beta=%g "
              "(%g deg of %g deg)\n",
              center_z, height, ballradius, cos(beta), beta,
              gwy_rad2deg(beta), gwy_rad2deg(angle));
    gdouble *d = gwy_field_get_data(tip);
    gdouble sbeta = sin(beta);
    for (gint row = 0; row < yres; row++) {
        gdouble y = dy*row - center_y;
        for (gint col = 0; col < xres; col++) {
            if (d[xres*row + col] > center_z + ballradius*sbeta) {
                gdouble x = dx*col - center_x;
                gdouble sphere = ballradius*ballradius - (x*x + y*y);
                gdouble zd = sqrt(fmax(sphere, 0.0));
                d[xres*row + col] = fmin(d[xres*row + col], center_z + zd);
            }
        }
    }
}

static void
pyramid_model(GwyField *tip, const gdouble *params)
{
    gdouble radius = params[0], rotation = params[1], slope = params[3];
    gint nsides = GWY_ROUND(params[2]);
    create_pyramid(tip, slope, nsides, rotation);
    round_pyramid(tip, slope, nsides, radius);
}

static void
contact_model(GwyField *tip, const gdouble *params)
{
    gdouble radius = params[0], rotation = params[1], angle = CONTACT_ANGLE;
    create_pyramid(tip, angle, 4, rotation);
    round_pyramid(tip, angle, 4, radius);
}

static void
noncontact_model(GwyField *tip, const gdouble *params)
{
    gdouble radius = params[0], rotation = params[1], angle = NONCONTACT_ANGLE;
    create_pyramid(tip, angle, 3, rotation);
    round_pyramid(tip, angle, 3, radius);
}

static void
parabola_model(GwyField *tip, const gdouble *params)
{
    gdouble radius = params[0];
    gint xres = tip->xres, yres = tip->yres;
    gint scol = xres/2, srow = yres/2;
    gdouble dx = gwy_field_get_dx(tip), dy = gwy_field_get_dy(tip);
    gdouble a = 0.5/radius;
    gdouble x0 = scol*dx;
    gdouble z0 = 2.0*a*x0*x0;
    gdouble *d = gwy_field_get_data(tip);
    for (gint row = 0; row < tip->yres; row++) {
        gdouble y = dy*(row - srow);
        for (gint col = 0; col < tip->xres; col++) {
            gdouble x = dx*(col - scol);
            gdouble r2 = x*x + y*y;
            d[xres*row + col] = z0 - a*r2;
        }
    }
}

static void
ell_parabola_model(GwyField *tip, const gdouble *params)
{
    gdouble radius = params[0], rotation = params[1], anisotropy = params[2];
    gdouble a1 = 0.5/radius/sqrt(anisotropy), a2 = 0.5/radius*sqrt(anisotropy);
    gint xres = tip->xres, yres = tip->yres;
    gint scol = xres/2, srow = yres/2;
    gdouble dx = gwy_field_get_dx(tip), dy = gwy_field_get_dy(tip);
    gdouble ca = cos(rotation), sa = sin(rotation);
    gdouble *d = gwy_field_get_data(tip);
    for (gint row = 0; row < yres; row++) {
        gdouble y = dy*(row - srow);
        for (gint col = 0; col < xres; col++) {
            gdouble x = dx*(col - scol);
            gdouble xx = x*ca - y*sa;
            gdouble yy = x*sa + y*ca;
            d[xres*row + col] = -(a1*xx*xx + a2*yy*yy);
        }
    }
    gwy_field_add(tip, -gwy_field_min(tip));
}

static void
cone_model(GwyField *tip, const gdouble *params)
{
    gdouble radius = params[0], angle = params[1];
    gint xres = tip->xres, yres = tip->yres;
    gint scol = xres/2, srow = yres/2;
    gdouble dx = gwy_field_get_dx(tip), dy = gwy_field_get_dy(tip);
    gdouble z0 = radius/sin(angle);
    gdouble br2 = radius*cos(angle);
    br2 *= br2;
    gdouble ta = 1.0/tan(angle);
    gdouble *d = gwy_field_get_data(tip);
    for (gint row = 0; row < yres; row++) {
        gdouble y = dy*(row - srow);
        for (gint col = 0; col < tip->xres; col++) {
            gdouble x = dx*(col - scol);
            gdouble r2 = x*x + y*y;
            d[xres*row + col] = (r2 < br2 ? sqrt(radius*radius - r2) : z0 - ta*sqrt(r2));
        }
    }
    gwy_field_add(tip, -gwy_field_min(tip));
}

static void
delta_model(GwyField *tip, const gdouble *params)
{
    gdouble height = params[0];
    gint xres = tip->xres, yres = tip->yres;
    gint scol = xres/2, srow = yres/2;
    gwy_field_clear(tip);
    gwy_field_set_val(tip, scol, srow, height);
}

/* Model sphere on stick as a very sharp cone with a rounded tip. */
static void
sphere_model(GwyField *tip, const gdouble *params)
{
    gdouble radius = params[0];
    gdouble cparams[G_N_ELEMENTS(cone_params)] = { radius, 1e-5 };
    cone_model(tip, cparams);
}

/**
 * gwy_tip_model_get_group_name:
 * @model: Tip model.
 *
 * Gets group name of of a tip model.
 *
 * Model names, such as "Analytical" or "Pyramid", are informative.
 *
 * Returns: Group name.
 **/
const gchar*
gwy_tip_model_get_group_name(GwyTipModel *model)
{
    g_return_val_if_fail(GWY_IS_TIP_MODEL(model), NULL);
    return model->priv->builtin->group_name;
}

/**
 * gwy_tip_model_get_n_params:
 * @model: Tip model.
 *
 * Get number of tip model parameters.
 *
 * The parameters are from a predefined set enumerated in #GwyTipParamType. Use gwy_tip_model_get_params() to get
 * the parameter list.
 *
 * Returns: Number of model parameters.
 **/
gint
gwy_tip_model_get_n_params(GwyTipModel *model)
{
    g_return_val_if_fail(GWY_IS_TIP_MODEL(model), 0);
    return model->priv->builtin->nparams;
}

/**
 * gwy_tip_model_get_params:
 * @model: Tip model.
 *
 * Gets the list of parameters of a tip model.
 *
 * All tip models have parameters from a predefined set given by the #GwyTipParamType enum. Use
 * gwy_tip_model_get_n_params() to get the number of parameters.
 *
 * Note that further items may be in principle added to the enum in the future so you may want to avoid tip models
 * that have parameters with an unknown (higher than known) id.
 *
 * Returns: List of all tip model parameter ids in ascending order.  The array is owned by the library and must not be
 *          modified nor freed.
 **/
const GwyTipParamType*
gwy_tip_model_get_params(GwyTipModel *model)
{
    g_return_val_if_fail(GWY_IS_TIP_MODEL(model), NULL);
    return model->priv->builtin->params;
}

/**
 * gwy_tip_model_fill_param:
 * @model: Tip model.
 * @params: Array of parameters of @model where to fill one specific value. The number of elements must be at least
 *          the number returned by gwy_tip_model_get_n_params().
 * @type: The parameter to fill.
 * @value: Value to set the parameter to.
 *
 * Fills one value in an array of tip model parameters.
 *
 * This helper function simplifies the lookup when setting the values of specific parameters. Instead of getting the
 * list of parameters using gwy_tip_model_get_params() and searching for the position of each parameter, you can do
 * |[
 * gdouble *params = g_new(gdouble, gwy_tip_model_get_n_params(model));
 * gwy_tip_model_fill_param(model, params, GWY_TIP_PARAM_RADIUS, 6e-9);
 * gwy_tip_model_fill_param(model, params, GWY_TIP_PARAM_ROTATION, gwy_deg2rad(60));
 * ]|
 * provided you know that the model has parameters %GWY_TIP_PARAM_RADIUS and %GWY_TIP_PARAM_ROTATION. It is an error
 * to try to set a non-existent parameter.
 **/
void
gwy_tip_model_fill_param(GwyTipModel* model,
                         gdouble *params,
                         GwyTipParamType type,
                         gdouble value)
{
    g_return_if_fail(GWY_IS_TIP_MODEL(model));
    g_return_if_fail(params);
    const TipModelBuiltin *builtin = model->priv->builtin;
    for (guint i = 0; i < builtin->nparams; i++) {
        if (type == builtin->params[i]) {
            params[i] = value;
            return;
        }
    }
    g_assert_not_reached();
}

/**
 * gwy_tip_model_create:
 * @model: Tip model.
 * @tip: Data field to fill with the tip model.
 * @params: Parameters of the tip model.
 *
 * Fills a data field with a model tip model.
 *
 * Both pixel and physical dimensions of @tip are preserved by this function.  Ensure that before using this function
 * @tip has the same physical pixel size as target data field you want to use the tip model with.
 *
 * The number of parameters is the true full number of parameters as reported by gwy_tip_model_get_n_params() and
 * gwy_tip_model_get_params(). Exacly these parameters are passed in @params, in the reported order.
 **/
void
gwy_tip_model_create(GwyTipModel *model,
                     GwyField *tip,
                     const gdouble *params)
{
    g_return_if_fail(GWY_IS_TIP_MODEL(model));
    g_return_if_fail(GWY_IS_FIELD(tip));
    g_return_if_fail(params);
    model->priv->builtin->model_tip(tip, params);
}

/**
 * gwy_tip_model_create_for_zrange:
 * @model: Tip model.
 * @tip: Data field to fill with the tip model.
 * @zrange: Range of height values in the data determining the required height of the tip model.
 * @square: %TRUE to enforce a square data field (with @xres and @yres equal).
 * @params: Parameters of the tip model.
 *
 * Fills a data field with a model tip model, resizing it to make it suitable for the given value range.
 *
 * The physical dimensions of a pixel in @tip is preserved by this function.  Ensure that before using this function
 * the @tip data field has the same pixel size as target data field you want to use the tip model with.
 *
 * However, its dimensions (@xres and @yres) will generally be changed to ensure it is optimal for @zrange.  This
 * means it is guaranteed the height difference between the apex and any border pixel in @tip is at least @zrange,
 * while simultaneously the smallest such difference is not much larger than @zrange.
 *
 * The number of parameters is the true full number of parameters as reported by gwy_tip_model_get_n_params() and
 * gwy_tip_model_get_params(). Exacly these parameters are passed in @params, in the reported order.
 **/
void
gwy_tip_model_create_for_zrange(GwyTipModel *model,
                                GwyField *tip,
                                gdouble zrange,
                                gboolean square,
                                const gdouble *params)
{
    g_return_if_fail(GWY_IS_TIP_MODEL(model));
    g_return_if_fail(GWY_IS_FIELD(tip));
    g_return_if_fail(params);

    gdouble dx = gwy_field_get_dx(tip), dy = gwy_field_get_dy(tip);
    gint xres, yres;
    const TipModelBuiltin *builtin = model->priv->builtin;
    builtin->guess_size(params, zrange, dx, &xres, &yres);
    if (square)
        xres = yres = MAX(xres, yres);

    gwy_field_resize(tip, xres, yres);
    tip->xreal = xres*dx;
    tip->yreal = yres*dy;
    builtin->model_tip(tip, params);
    if (gwy_strequal(builtin->name, "Delta function"))
        return;

    /* Enlarge the tip while it is too small. */
    guint xres_good, yres_good, redw, redh, iter = 0;
    gdouble zrange_lr, zrange_tb;
    while (!check_tip_zrange(tip, 1.1*zrange, &zrange_lr, &zrange_tb, &redw, &redh)) {
        if (zrange_tb <= 1.1*zrange)
            yres = (4*yres/3 + 1) | 1;
        if (zrange_lr <= 1.1*zrange)
            xres = (4*xres/3 + 1) | 1;
        if (square)
            xres = yres = MAX(xres, yres);

        gwy_field_resize(tip, xres, yres);
        tip->xreal = xres*dx;
        tip->yreal = yres*dy;
        builtin->model_tip(tip, params);

        /* This means about 10 times larger tip than estimated. */
        if (iter++ == 8) {
            g_warning("Failed to guarantee zrange by enlagring tip model.");
            break;
        }
    }
    xres_good = xres;
    yres_good = yres;

    if (square)
        redw = redh = MIN(redw, redh);
    if (redw)
        redw--;
    if (redh)
        redh--;

    if (!MAX(redw, redh))
        return;

    /* The tip seems too large so try cropping it a bit. */
    xres -= 2*redw;
    yres -= 2*redh;
    gwy_field_resize(tip, xres, yres);
    tip->xreal = xres*dx;
    tip->yreal = yres*dy;
    builtin->model_tip(tip, params);
    if (check_tip_zrange(tip, 1.01*zrange, &zrange_lr, &zrange_tb, &redw, &redh))
        return;

    /* If it fails the check after size reduction try two things: first adding just one pixel to each side, then just
     * reverting to the last know good tip. */
    xres += 2;
    yres += 2;
    gwy_field_resize(tip, xres, yres);
    tip->xreal = xres*dx;
    tip->yreal = yres*dy;
    builtin->model_tip(tip, params);
    if (check_tip_zrange(tip, 1.01*zrange, &zrange_lr, &zrange_tb, &redw, &redh))
        return;

    xres = xres_good;
    yres = yres_good;
    gwy_field_resize(tip, xres, yres);
    tip->xreal = xres*dx;
    tip->yreal = yres*dy;
    builtin->model_tip(tip, params);
}

static gboolean
check_tip_zrange(GwyField *tip, gdouble zrange,
                 gdouble *zrange_leftright, gdouble *zrange_topbottom,
                 guint *can_shrink_width, guint *can_shrink_height)
{
    gdouble max = gwy_field_max(tip);
    guint xres = tip->xres, yres = tip->yres;
    gdouble hmax, vmax;

    vmax = fmax(gwy_field_area_max(tip, NULL, GWY_MASK_IGNORE, 0, 0, xres, 1),
                gwy_field_area_max(tip, NULL, GWY_MASK_IGNORE, 0, yres-1, xres, 1));
    *zrange_topbottom = max - vmax;
    hmax = fmax(gwy_field_area_max(tip, NULL, GWY_MASK_IGNORE, 0, 0, 1, yres),
                gwy_field_area_max(tip, NULL, GWY_MASK_IGNORE, xres-1, 0, 1, yres));
    *zrange_leftright = max - hmax;
    *can_shrink_height = *can_shrink_width = 0;

    if (!(fmax(*zrange_topbottom, *zrange_leftright) >= zrange))
        return FALSE;

    GwyNield *mask = gwy_field_new_nield_alike(tip);
    gwy_nield_mark_by_threshold(mask, tip, max - zrange, TRUE);
    guint left, right, up, down;
    if (gwy_nield_autocrop(mask, TRUE, &left, &right, &up, &down)) {
        /* There were some empty rows, i.e. rows with too small values. */
        *can_shrink_width = left;
        *can_shrink_height = up;
    }
    g_object_unref(mask);

    return TRUE;
}

/**
 * gwy_tip_models:
 *
 * Gets inventory with all the predefined tip models.
 *
 * Returns: (transfer none): tip model inventory.
 **/
GwyInventory*
gwy_tip_models(void)
{
    GTypeClass *klass = g_type_class_peek(GWY_TYPE_TIP_MODEL);
    return GWY_RESOURCE_CLASS(klass)->inventory;
}

/**
 * SECTION:tip-model
 * @title: GwyTipModel
 * @short_description: SPM tip modelling
 *
 * The follwing tip shapes are defined:
 * <simplelist type='vert'>
 * <member><literal>"Pyramid"</literal> – general pyramid with an arbitrary number of sides</member>
 * <member><literal>"Contact"</literal> – four-sided pyramid with atan(√2) side slope</member>
 * <member><literal>"Non-contact"</literal> – three-sided pyramid with 70° side slope</member>
 * <member><literal>"Delta function"</literal> – single pixel tip</member>
 * <member><literal>"Parabola"</literal> – parabolic tip</member>
 * <member><literal>"Cone"</literal> – conical tip</member>
 * <member><literal>"Elliptical parabola"</literal> – asymmetrical parabolic typ</member>
 * <member><literal>"Ball on stick"</literal> – spherical tip at the end of almost-cyliner</member>
 * </simplelist>
 *
 * The delta-function tip is a single pixel, but usually in a 3×3 or even larger tip image. Therefore, it may or may
 * behave as infinitely sharp, depending on the height.
 **/

/**
 * GwyTipParamType:
 * @GWY_TIP_PARAM_HEIGHT: Total tip height.  This is used only in the delta function tip; for all others it is
 *                        implied.
 * @GWY_TIP_PARAM_RADIUS: Radius of curvature of the tip apex.
 * @GWY_TIP_PARAM_ROTATION: Rotation angle (in radians).
 * @GWY_TIP_PARAM_NSIDES: The number of sides for pyramidal tips (an integer ≥ 3, even though it is passed as
 *                        a double).
 * @GWY_TIP_PARAM_SLOPE: Half-angle of the apex (complement of the side slope for straight sides), in radians. A
 *                       small slope angle means a sharp tip.
 * @GWY_TIP_PARAM_ANISOTROPY: Ratio between larger and smaller tip width in two orthotonal directions.
 *
 * Type of tip model parameter.
 *
 * This enum is used with the new tip model functions gwy_tip_model_get_params(),
 * gwy_tip_model_create(), gwy_tip_model_create_for_zrange().
 **/

/* vim: set cin columns=120 tw=118 et ts=4 sw=4 cino=>1s,e0,n0,f0,{0,}0,^0,\:1s,=0,g1s,h0,t0,+1s,c3,(0,u0 : */
