summaryrefslogtreecommitdiffstats
path: root/src/grid-snapper.cpp
blob: f43975c79a80f6d6917a839008b7d288f80a5c99 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
// SPDX-License-Identifier: GPL-2.0-or-later
/** @file
 * Grid Snapper for Rectangular and Axonometric Grids
 *//*
 * Authors: see git history
 *
 * Copyright (C) 2022 Authors
 * Copyright (C) Johan Engelen 2006-2007 <johan@shouraizou.nl>
 * Copyright (C) Lauris Kaplinski 2000
 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
 */
#include "grid-snapper.h"

#include "desktop.h"
#include "helper/mathfns.h"
#include "object/sp-grid.h"
#include "object/sp-namedview.h"

static int calculate_scaling_factor(double length, int major)
{
    int multiply = 1;
    int step = std::max(major, 1);
    int watchdog = 0;

    while (length * multiply < 8.0 && watchdog < 100) {
        multiply *= step;
        // First pass, go up to the major line spacing, then keep increasing by two.
        step = 2;
        watchdog++;
    }

    return multiply;
}

// Project a vector onto the given axis.
static auto proj(Geom::Point const &p, int dim)
{
    return dim == 0
         ? Geom::Point(p.x(), 0.0)
         : Geom::Point(0.0, p.y());
}

// Return the unit vector along the given axis.
static auto basis(int dim)
{
    return dim == 0
         ? Geom::Point(1.0, 0.0)
         : Geom::Point(0.0, 1.0);
}

namespace Inkscape {

GridSnapper::GridSnapper(SPGrid const *grid, SnapManager *sm, Geom::Coord const d)
    : LineSnapper(sm, d)
    , _grid(grid)
{
}

/**
 *  \return Snap tolerance (desktop coordinates); depends on current zoom so that it's always the same in screen pixels
 */
Geom::Coord GridSnapper::getSnapperTolerance() const
{
    SPDesktop const *dt = _snapmanager->getDesktop();
    double const zoom =  dt ? dt->current_zoom() : 1;
    return _snapmanager->snapprefs.getGridTolerance() / zoom;
}

bool GridSnapper::getSnapperAlwaysSnap() const
{
    return _snapmanager->snapprefs.getGridTolerance() == 10000; //TODO: Replace this threshold of 10000 by a constant; see also tolerance-slider.cpp
}

LineSnapper::LineList GridSnapper::_getSnapLines(Geom::Point const &p) const
{
    if (!_snapmanager->getNamedView() || !ThisSnapperMightSnap()) {
        return {};
    }

    switch (_grid->getType()) {
        case GridType::RECTANGULAR: return getSnapLinesXY(p);
        case GridType::AXONOMETRIC: return getSnapLinesAxonom(p);
        default: g_assert_not_reached(); return {};
    }
}

void GridSnapper::_addSnappedLine(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance,  SnapSourceType const &source, long source_num, Geom::Point const &normal_to_line, Geom::Point const &point_on_line) const
{
    isr.grid_lines.emplace_back(snapped_point, snapped_distance, source, source_num, SNAPTARGET_GRID, getSnapperTolerance(), getSnapperAlwaysSnap(), normal_to_line, point_on_line);
}

void GridSnapper::_addSnappedPoint(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const
{
    isr.points.emplace_back(snapped_point, source, source_num, SNAPTARGET_GRID, snapped_distance, getSnapperTolerance(), getSnapperAlwaysSnap(), constrained_snap, true);
}

void GridSnapper::_addSnappedLinePerpendicularly(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const
{
    isr.points.emplace_back(snapped_point, source, source_num, SNAPTARGET_GRID_PERPENDICULAR, snapped_distance, getSnapperTolerance(), getSnapperAlwaysSnap(), constrained_snap, true);
}

/**
 *  \return true if this Snapper will snap at least one kind of point.
 */
bool GridSnapper::ThisSnapperMightSnap() const
{
    return _snap_enabled && _snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_GRID);
}

LineSnapper::LineList GridSnapper::getSnapLinesXY(Geom::Point const &p) const
{
    LineList s;

    auto const *desktop = _snapmanager->getDesktop();
    auto const [origin, spacing] = _grid->getEffectiveOriginAndSpacing();

    for (int i = 0; i < 2; ++i) {
        double scaled_spacing = spacing[i];

        if (getSnapVisibleOnly() && desktop) {
            // Only snap to visible grid lines.
            auto const sw = proj(spacing, i) * desktop->d2w().withoutTranslation();
            int const mult = calculate_scaling_factor(sw.length(), _grid->getMajorLineInterval());
            scaled_spacing *= mult;
        }

        s.emplace_back(basis(i), basis(i) * Util::round_to_upper_multiple_plus(p[i], scaled_spacing, origin[i]));
        s.emplace_back(basis(i), basis(i) * Util::round_to_lower_multiple_plus(p[i], scaled_spacing, origin[i]));
    }

    return s;
}

LineSnapper::LineList GridSnapper::getSnapLinesAxonom(Geom::Point const &p) const
{
    LineList s;

    auto const *desktop = _snapmanager->getDesktop();
    auto const [origin, spacing] = _grid->getEffectiveOriginAndSpacing();

    double ta_x = tan(Geom::rad_from_deg(_grid->getAngleX()));
    double ta_z = tan(Geom::rad_from_deg(_grid->getAngleZ()));

    if (desktop && desktop->is_yaxisdown()) {
        std::swap(ta_x, ta_z);
    }

    double spacing_h = spacing.y() / (ta_x + ta_z);
    double spacing_v = spacing.y();

    if (getSnapVisibleOnly() && desktop) {
        // Only snap to visible grid lines.
        auto const lyw = spacing.y() * desktop->d2w().descrim();
        int const mult = calculate_scaling_factor(lyw, _grid->getMajorLineInterval());
        spacing_h *= mult;
        spacing_v *= mult;
    }

    // In an axonometric grid, any point will be surrounded by 6 grid lines:
    // - 2 vertical grid lines, one left and one right from the point
    // - 2 angled z grid lines, one above and one below the point
    // - 2 angled x grid lines, one above and one below the point

    // Calculate the x coordinate of the vertical grid lines
    Geom::Coord x_max = Util::round_to_upper_multiple_plus(p[Geom::X], spacing_h, origin[Geom::X]);
    Geom::Coord x_min = Util::round_to_lower_multiple_plus(p[Geom::X], spacing_h, origin[Geom::X]);

    // Calculate the y coordinate of the intersection of the angled grid lines with the y-axis
    double y_proj_along_z = p[Geom::Y] - ta_z * (p[Geom::X] - origin[Geom::X]);
    double y_proj_along_x = p[Geom::Y] + ta_x * (p[Geom::X] - origin[Geom::X]);
    double y_proj_along_z_max = Util::round_to_upper_multiple_plus(y_proj_along_z, spacing_v, origin[Geom::Y]);
    double y_proj_along_z_min = Util::round_to_lower_multiple_plus(y_proj_along_z, spacing_v, origin[Geom::Y]);
    double y_proj_along_x_max = Util::round_to_upper_multiple_plus(y_proj_along_x, spacing_v, origin[Geom::Y]);
    double y_proj_along_x_min = Util::round_to_lower_multiple_plus(y_proj_along_x, spacing_v, origin[Geom::Y]);

    // Calculate the versor for the angled grid lines
    Geom::Point vers_x = Geom::Point(1, -ta_x);
    Geom::Point vers_z = Geom::Point(1, ta_z);

    // Calculate the normal for the angled grid lines
    Geom::Point norm_x = Geom::rot90(vers_x);
    Geom::Point norm_z = Geom::rot90(vers_z);

    // The four angled grid lines form a parallelogram, enclosing the point
    // One of the two vertical grid lines divides this parallelogram in two triangles
    // We will now try to find out in which half (i.e. triangle) our point is, and return
    // only the three grid lines defining that triangle

    // The vertical grid line is at the intersection of two angled grid lines.
    // Now go find that intersection!
    Geom::Point p_x(0, y_proj_along_x_max);
    Geom::Line line_x(p_x, p_x + vers_x);
    Geom::Point p_z(0, y_proj_along_z_max);
    Geom::Line line_z(p_z, p_z + vers_z);

    Geom::OptCrossing inters = Geom::OptCrossing(); // empty by default
    try
    {
        inters = Geom::intersection(line_x, line_z);
    }
    catch (Geom::InfiniteSolutions &e)
    {
        // We're probably dealing with parallel lines; this is useless!
        return s;
    }

    // Determine which half of the parallelogram to use
    bool use_left_half = true;
    bool use_right_half = true;

    if (inters) {
        Geom::Point inters_pt = line_x.pointAt((*inters).ta);
        use_left_half = (p[Geom::X] - origin[Geom::X]) < inters_pt[Geom::X];
        use_right_half = !use_left_half;
    }

    // Return the three grid lines which define the triangle that encloses our point
    // If we didn't find an intersection above, all 6 grid lines will be returned
    if (use_left_half) {
        s.emplace_back(norm_z, Geom::Point(origin[Geom::X], y_proj_along_z_max));
        s.emplace_back(norm_x, Geom::Point(origin[Geom::X], y_proj_along_x_min));
        s.emplace_back(Geom::Point(1, 0), Geom::Point(x_max, 0));
    }

    if (use_right_half) {
        s.emplace_back(norm_z, Geom::Point(origin[Geom::X], y_proj_along_z_min));
        s.emplace_back(norm_x, Geom::Point(origin[Geom::X], y_proj_along_x_max));
        s.emplace_back(Geom::Point(1, 0), Geom::Point(x_min, 0));
    }

    return s;
}

} // namepsace Inkscape