summaryrefslogtreecommitdiffstats
path: root/src/snapped-curve.cpp
blob: 70ad72ee0767bdfb271f6fca783d9d51def31a3b (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
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
// SPDX-License-Identifier: GPL-2.0-or-later
/**
 *    \file src/snapped-curve.cpp
 *    SnappedCurve class.
 *
 *    Authors:
 *      Diederik van Lierop <mail@diedenrezi.nl>
 *
 *    Released under GNU GPL v2+, read the file 'COPYING' for more information.
 */

#include "snapped-curve.h"
#include <2geom/path-intersection.h>

Inkscape::SnappedCurve::SnappedCurve(Geom::Point const &snapped_point, Geom::Point const &tangent, int num_path, int num_segm, Geom::Coord const &snapped_distance, Geom::Coord const &snapped_tolerance, bool const &always_snap, bool const &fully_constrained, Geom::Curve const *curve, SnapSourceType source, long source_num, SnapTargetType target, Geom::OptRect target_bbox)
{
    _num_path = num_path;
    _num_segm = num_segm;
    _distance = snapped_distance;
    _tolerance = std::max(snapped_tolerance, 1.0);
    _always_snap = always_snap;
    _curve = curve;
    _second_distance = Geom::infinity();
    _second_tolerance = 1;
    _second_always_snap = false;
    _point = snapped_point;
    _tangent = tangent;
    _at_intersection = false;
    _fully_constrained = fully_constrained;
    _source = source;
    _source_num = source_num;
    _target = target;
    _target_bbox = target_bbox;
}

Inkscape::SnappedCurve::SnappedCurve()
{
    _num_path = 0;
    _num_segm = 0;
    _distance = Geom::infinity();
    _tolerance = 1;
    _always_snap = false;
    _curve = nullptr;
    _second_distance = Geom::infinity();
    _second_tolerance = 1;
    _second_always_snap = false;
    _point = Geom::Point(0,0);
    _tangent = Geom::Point(0,0);
    _at_intersection = false;
    _fully_constrained = false;
    _source = SNAPSOURCE_UNDEFINED;
    _source_num = -1;
    _target = SNAPTARGET_UNDEFINED;
    _target_bbox = Geom::OptRect();
}

Inkscape::SnappedCurve::~SnappedCurve()
= default;

Inkscape::SnappedPoint Inkscape::SnappedCurve::intersect(SnappedCurve const &curve, Geom::Point const &p, Geom::Affine dt2doc) const
{
    // Calculate the intersections of two curves, which are both within snapping range, and
    // return only the closest intersection
    // The point of intersection should be considered for snapping, but might be outside the snapping range
    // PS: We need p (the location of the mouse pointer) to find out which intersection is the
    // closest, as there might be multiple intersections of two curves
    Geom::Crossings cs = crossings(*(this->_curve), *(curve._curve));

    if (cs.size() > 0) {
        // There might be multiple intersections: find the closest
        Geom::Coord best_dist = Geom::infinity();
        Geom::Point best_p = Geom::Point(Geom::infinity(), Geom::infinity());
        for (const auto & c : cs) {
            Geom::Point p_ix = this->_curve->pointAt(c.ta);
            Geom::Coord dist = Geom::distance(p_ix, p);

            // Test if we have two segments (curves) from the same path..
            if (this->_num_path == curve._num_path) {
                // Never try to intersect a segment with itself
                if (this->_num_segm == curve._num_segm) continue;
                // Two subsequent segments (curves) in a path will have a common node; this node is not considered to be an intersection
                if (this->_num_segm == curve._num_segm + 1 && c.ta == 0 && c.tb == 1) continue;
                if (this->_num_segm + 1 == curve._num_segm && c.ta == 1 && c.tb == 0) continue;
            }

            if (dist < best_dist) {
                best_dist = dist;
                best_p = p_ix;
            }
        }

        // Now we've found the closest intersection, return it as a SnappedPoint
        bool const use_this_as_primary = _distance < curve.getSnapDistance();
        Inkscape::SnappedCurve const *primaryC = use_this_as_primary ? this : &curve;
        Inkscape::SnappedCurve const *secondaryC = use_this_as_primary ? &curve : this;

        // The intersection should in fact be returned in desktop coordinates
        best_p = best_p * dt2doc;

        Geom::Coord primaryDist = use_this_as_primary ? Geom::L2(best_p - this->getPoint()) : Geom::L2(best_p - curve.getPoint());
        Geom::Coord secondaryDist = use_this_as_primary ? Geom::L2(best_p - curve.getPoint()) : Geom::L2(best_p - this->getPoint());
        // TODO: Investigate whether it is possible to use document coordinates everywhere
        // in the snapper code. Only the mouse position should be in desktop coordinates, I guess.
        // All paths are already in document coords and we are certainly not going to change THAT.
        return SnappedPoint(best_p, Inkscape::SNAPSOURCE_UNDEFINED, primaryC->getSourceNum(), Inkscape::SNAPTARGET_PATH_INTERSECTION, primaryDist, primaryC->getTolerance(), primaryC->getAlwaysSnap(), true, false, true,
                secondaryDist, secondaryC->getTolerance(), secondaryC->getAlwaysSnap());
    }

    // No intersection
    return SnappedPoint(Geom::Point(Geom::infinity(), Geom::infinity()), SNAPSOURCE_UNDEFINED, 0, SNAPTARGET_UNDEFINED, Geom::infinity(), 0, false, false, false, false, Geom::infinity(), 0, false);
}

Inkscape::SnappedPoint Inkscape::SnappedCurve::intersect(SnappedLine const &line, Geom::Point const &p, Geom::Affine dt2doc) const
{
    // Calculate the intersections of a curve with a line, which are both within snapping range, and
    // return only the closest intersection
    // The point of intersection should be considered for snapping, but might be outside the snapping range
    // PS: We need p (the location of the mouse pointer) to find out which intersection is the
    // closest, as there might be multiple intersections of a single curve with a line

    // 1) get a Geom::Line object from the SnappedLine
    // 2) convert to document coordinates (line and p are in desktop coordinates, but the curves are in document coordinate)
    // 3) create a Geom::LineSegment (i.e. a curve), because we cannot use a Geom::Line for calculating intersections
    //      (for this we will create a 2e6 pixels long linesegment, with t running from -1e6 to 1e6; this should be long
    //      enough for any practical purpose)
    Geom::LineSegment line_segm = line.getLine().transformed(dt2doc).segment(-1e6, 1e6); //
    const Geom::Curve *line_as_curve = dynamic_cast<Geom::Curve const*>(&line_segm);
    Geom::Crossings cs = crossings(*(this->_curve), *line_as_curve);

    if (cs.size() > 0) {
        // There might be multiple intersections: find the closest
        Geom::Coord best_dist = Geom::infinity();
        Geom::Point best_p = Geom::Point(Geom::infinity(), Geom::infinity());
        for (const auto & c : cs) {
            Geom::Point p_ix = this->_curve->pointAt(c.ta);
            Geom::Coord dist = Geom::distance(p_ix, p);

            if (dist < best_dist) {
                best_dist = dist;
                best_p = p_ix;
            }
        }

        // The intersection should in fact be returned in desktop coordinates
        best_p = best_p * dt2doc;

        // Now we've found the closest intersection, return it as a SnappedPoint
        if (_distance < line.getSnapDistance()) {
            // curve is the closest, so this is our primary snap target
            return SnappedPoint(best_p, Inkscape::SNAPSOURCE_UNDEFINED, this->getSourceNum(), Inkscape::SNAPTARGET_PATH_GUIDE_INTERSECTION,
                    Geom::L2(best_p - this->getPoint()), this->getTolerance(), this->getAlwaysSnap(), true, false, true,
                    Geom::L2(best_p - line.getPoint()), line.getTolerance(), line.getAlwaysSnap());
        } else {
            return SnappedPoint(best_p, Inkscape::SNAPSOURCE_UNDEFINED, line.getSourceNum(), Inkscape::SNAPTARGET_PATH_GUIDE_INTERSECTION,
                    Geom::L2(best_p - line.getPoint()), line.getTolerance(), line.getAlwaysSnap(), true, false, true,
                    Geom::L2(best_p - this->getPoint()), this->getTolerance(), this->getAlwaysSnap());
        }
    }

    // No intersection
    return SnappedPoint(Geom::Point(Geom::infinity(), Geom::infinity()), SNAPSOURCE_UNDEFINED, 0, SNAPTARGET_UNDEFINED, Geom::infinity(), 0, false, false, false, false, Geom::infinity(), 0, false);
}


// search for the closest snapped line
bool getClosestCurve(std::list<Inkscape::SnappedCurve> const &list, Inkscape::SnappedCurve &result, bool exclude_paths)
{
    bool success = false;

    for (std::list<Inkscape::SnappedCurve>::const_iterator i = list.begin(); i != list.end(); ++i) {
        if (exclude_paths && ((*i).getTarget() == Inkscape::SNAPTARGET_PATH)) {
            continue;
        }
        if ((i == list.begin()) || (*i).getSnapDistance() < result.getSnapDistance()) {
            result = *i;
            success = true;
        }
    }

    return success;
}

// search for the closest intersection of two snapped curves, which are both member of the same collection
bool getClosestIntersectionCS(std::list<Inkscape::SnappedCurve> const &list, Geom::Point const &p, Inkscape::SnappedPoint &result, Geom::Affine dt2doc)
{
    bool success = false;

    for (std::list<Inkscape::SnappedCurve>::const_iterator i = list.begin(); i != list.end(); ++i) {
        if ((*i).getTarget() != Inkscape::SNAPTARGET_BBOX_EDGE) { // We don't support snapping to intersections of bboxes,
            // as this would require two bboxes two be flashed in the snap indicator
            std::list<Inkscape::SnappedCurve>::const_iterator j = i;
            ++j;
            for (; j != list.end(); ++j) {
                if ((*j).getTarget() != Inkscape::SNAPTARGET_BBOX_EDGE) { // We don't support snapping to intersections of bboxes
                    Inkscape::SnappedPoint sp = (*i).intersect(*j, p, dt2doc);
                    if (sp.getAtIntersection()) {
                        // if it's the first point
                        bool const c1 = !success;
                        // or, if it's closer
                        bool const c2 = sp.getSnapDistance() < result.getSnapDistance();
                        // or, if it's just as close then look at the other distance
                        // (only relevant for snapped points which are at an intersection)
                        bool const c3 = (sp.getSnapDistance() == result.getSnapDistance()) && (sp.getSecondSnapDistance() < result.getSecondSnapDistance());
                        // then prefer this point over the previous one
                        if (c1 || c2 || c3) {
                            result = sp;
                            success = true;
                        }
                    }
                }
            }
        }
    }

    return success;
}

// search for the closest intersection of two snapped curves, which are member of two different collections
bool getClosestIntersectionCL(std::list<Inkscape::SnappedCurve> const &curve_list, std::list<Inkscape::SnappedLine> const &line_list, Geom::Point const &p, Inkscape::SnappedPoint &result, Geom::Affine dt2doc)
{
    bool success = false;

    for (const auto & i : curve_list) {
        if (i.getTarget() != Inkscape::SNAPTARGET_BBOX_EDGE) { // We don't support snapping to intersections of bboxes,
            // as this would require two bboxes two be flashed in the snap indicator
            for (const auto & j : line_list) {
                if (j.getTarget() != Inkscape::SNAPTARGET_BBOX_EDGE) { // We don't support snapping to intersections of bboxes
                    Inkscape::SnappedPoint sp = i.intersect(j, p, dt2doc);
                    if (sp.getAtIntersection()) {
                        // if it's the first point
                        bool const c1 = !success;
                        // or, if it's closer
                        bool const c2 = sp.getSnapDistance() < result.getSnapDistance();
                        // or, if it's just as close then look at the other distance
                        // (only relevant for snapped points which are at an intersection)
                        bool const c3 = (sp.getSnapDistance() == result.getSnapDistance()) && (sp.getSecondSnapDistance() < result.getSecondSnapDistance());
                        // then prefer this point over the previous one
                        if (c1 || c2 || c3) {
                            result = sp;
                            success = true;
                        }
                    }
                }
            }
        }
    }

    return success;
}

/*
  Local Variables:
  mode:c++
  c-file-style:"stroustrup"
  c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
  indent-tabs-mode:nil
  fill-column:99
  End:
*/
// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :