KoSnapStrategy.cpp 18.7 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
/* This file is part of the KDE project
 * Copyright (C) 2008 Jan Hambrecht <jaham@gmx.net>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public License
 * along with this library; see the file COPYING.LIB.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

20
#include "KoSnapStrategy.h"
21
#include "KoSnapGuide.h"
22 23
#include <KoPathShape.h>
#include <KoPathPoint.h>
24
#include <KoCanvasBase.h>
25
#include <KoViewConverter.h>
Jan Hambrecht's avatar
Jan Hambrecht committed
26
#include <KoGuidesData.h>
27 28

#include <QtGui/QPainter>
29

30
//#include <kdebug.h>
31 32 33
#include <math.h>


34 35
KoSnapStrategy::KoSnapStrategy(KoSnapStrategy::SnapType type)
        : m_snapType(type)
36 37 38
{
}

39
QPointF KoSnapStrategy::snappedPosition() const
40 41 42 43
{
    return m_snappedPosition;
}

44
void KoSnapStrategy::setSnappedPosition(const QPointF &position)
45
{
46
    m_snappedPosition = position;
47 48
}

49
KoSnapStrategy::SnapType KoSnapStrategy::type() const
50 51 52 53
{
    return m_snapType;
}

54
qreal KoSnapStrategy::fastDistance(const QPointF &p1, const QPointF &p2)
55
{
56 57
    qreal dx = p1.x() - p2.x();
    qreal dy = p1.y() - p2.y();
58 59 60
    return dx*dx + dy*dy;
}

61
qreal KoSnapStrategy::scalarProduct(const QPointF &p1, const QPointF &p2)
62 63 64 65
{
    return p1.x() * p2.x() + p1.y() * p2.y();
}

66
OrthogonalSnapStrategy::OrthogonalSnapStrategy()
67
        : KoSnapStrategy(KoSnapStrategy::Orthogonal)
68 69 70
{
}

71
bool OrthogonalSnapStrategy::snap(const QPointF &mousePosition, KoSnapProxy * proxy, qreal maxSnapDistance)
72 73
{
    QPointF horzSnap, vertSnap;
74 75
    qreal minVertDist = HUGE_VAL;
    qreal minHorzDist = HUGE_VAL;
76 77

    QList<KoShape*> shapes = proxy->shapes();
78 79
    foreach(KoShape * shape, shapes) {
        QList<QPointF> points = proxy->pointsFromShape(shape);
Jan Hambrecht's avatar
Jan Hambrecht committed
80
        foreach(const QPointF & point, points) {
81 82
            qreal dx = fabs(point.x() - mousePosition.x());
            if (dx < minHorzDist && dx < maxSnapDistance) {
83 84 85
                minHorzDist = dx;
                horzSnap = point;
            }
86 87
            qreal dy = fabs(point.y() - mousePosition.y());
            if (dy < minVertDist && dy < maxSnapDistance) {
88 89 90 91 92 93 94 95
                minVertDist = dy;
                vertSnap = point;
            }
        }
    }

    QPointF snappedPoint = mousePosition;

96 97 98 99
    if (minHorzDist < HUGE_VAL)
        snappedPoint.setX(horzSnap.x());
    if (minVertDist < HUGE_VAL)
        snappedPoint.setY(vertSnap.y());
100

101 102
    if (minHorzDist < HUGE_VAL)
        m_hLine = QLineF(horzSnap, snappedPoint);
103 104
    else
        m_hLine = QLineF();
105

106 107
    if (minVertDist < HUGE_VAL)
        m_vLine = QLineF(vertSnap, snappedPoint);
108 109
    else
        m_vLine = QLineF();
110

111
    setSnappedPosition(snappedPoint);
112 113 114 115

    return (minHorzDist < HUGE_VAL || minVertDist < HUGE_VAL);
}

116
QPainterPath OrthogonalSnapStrategy::decoration(const KoViewConverter &converter) const
117
{
118
    Q_UNUSED(converter);
119 120

    QPainterPath decoration;
121 122 123
    if (! m_hLine.isNull()) {
        decoration.moveTo(m_hLine.p1());
        decoration.lineTo(m_hLine.p2());
124
    }
125 126 127
    if (! m_vLine.isNull()) {
        decoration.moveTo(m_vLine.p1());
        decoration.lineTo(m_vLine.p2());
128 129 130 131
    }
    return decoration;
}

132
NodeSnapStrategy::NodeSnapStrategy()
133
        : KoSnapStrategy(KoSnapStrategy::Node)
134 135 136
{
}

137
bool NodeSnapStrategy::snap(const QPointF &mousePosition, KoSnapProxy * proxy, qreal maxSnapDistance)
138
{
139
    qreal maxDistance = maxSnapDistance * maxSnapDistance;
140
    qreal minDistance = HUGE_VAL;
141

142 143 144
    QRectF rect(-maxSnapDistance, -maxSnapDistance, maxSnapDistance, maxSnapDistance);
    rect.moveCenter(mousePosition);
    QList<QPointF> points = proxy->pointsInRect(rect);
145 146 147

    QPointF snappedPoint = mousePosition;

Jan Hambrecht's avatar
Jan Hambrecht committed
148
    foreach(const QPointF & point, points) {
149 150
        qreal distance = fastDistance(mousePosition, point);
        if (distance < maxDistance && distance < minDistance) {
151 152 153 154 155
            snappedPoint = point;
            minDistance = distance;
        }
    }

156
    setSnappedPosition(snappedPoint);
157 158 159

    return (minDistance < HUGE_VAL);
}
160

161
QPainterPath NodeSnapStrategy::decoration(const KoViewConverter &converter) const
162
{
163 164
    QRectF unzoomedRect = converter.viewToDocument(QRectF(0, 0, 11, 11));
    unzoomedRect.moveCenter(snappedPosition());
165
    QPainterPath decoration;
166
    decoration.addEllipse(unzoomedRect);
167 168 169
    return decoration;
}

170
ExtensionSnapStrategy::ExtensionSnapStrategy()
171
        : KoSnapStrategy(KoSnapStrategy::Extension)
172 173 174
{
}

175
bool ExtensionSnapStrategy::snap(const QPointF &mousePosition, KoSnapProxy * proxy, qreal maxSnapDistance)
176
{
177
    qreal maxDistance = maxSnapDistance * maxSnapDistance;
178
    qreal minDistance = HUGE_VAL;
179 180 181 182

    QPointF snappedPoint = mousePosition;
    QPointF startPoint;

183 184 185 186
    QList<KoShape*> shapes = proxy->shapes(true);
    foreach(KoShape * shape, shapes) {
        KoPathShape * path = dynamic_cast<KoPathShape*>(shape);
        if (! path)
187 188 189 190 191
            continue;

        QMatrix matrix = path->absoluteTransformation(0);

        int subpathCount = path->subpathCount();
192 193
        for (int subpathIndex = 0; subpathIndex < subpathCount; ++subpathIndex) {
            if (path->isClosedSubpath(subpathIndex))
194 195
                continue;

196
            int pointCount = path->pointCountSubpath(subpathIndex);
197 198

            // check the extension from the start point
199
            KoPathPoint * first = path->pointByIndex(KoPathPointIndex(subpathIndex, 0));
200
            QPointF firstSnapPosition = mousePosition;
201 202 203
            if (snapToExtension(firstSnapPosition, first, matrix)) {
                qreal distance = fastDistance(firstSnapPosition, mousePosition);
                if (distance < maxDistance && distance < minDistance) {
204 205
                    minDistance = distance;
                    snappedPoint = firstSnapPosition;
206
                    startPoint = matrix.map(first->point());
207 208 209 210
                }
            }

            // now check the extension from the last point
211
            KoPathPoint * last = path->pointByIndex(KoPathPointIndex(subpathIndex, pointCount - 1));
212
            QPointF lastSnapPosition = mousePosition;
213 214 215
            if (snapToExtension(lastSnapPosition, last, matrix)) {
                qreal distance = fastDistance(lastSnapPosition, mousePosition);
                if (distance < maxDistance && distance < minDistance) {
216 217
                    minDistance = distance;
                    snappedPoint = lastSnapPosition;
218
                    startPoint = matrix.map(last->point());
219 220 221 222 223
                }
            }
        }
    }

224 225
    if (minDistance < HUGE_VAL)
        m_line = QLineF(startPoint, snappedPoint);
226 227
    else
        m_line = QLineF();
228

229
    setSnappedPosition(snappedPoint);
230 231 232 233

    return (minDistance < HUGE_VAL);
}

234
QPainterPath ExtensionSnapStrategy::decoration(const KoViewConverter &converter) const
235
{
236
    Q_UNUSED(converter);
237 238

    QPainterPath decoration;
239 240 241
    if (! m_line.isNull()) {
        decoration.moveTo(m_line.p1());
        decoration.lineTo(m_line.p2());
242 243 244 245
    }
    return decoration;
}

246
bool ExtensionSnapStrategy::snapToExtension(QPointF &position, KoPathPoint * point, const QMatrix &matrix)
247
{
248 249
    QPointF direction = extensionDirection(point, matrix);
    if (direction.isNull())
Jan Hambrecht's avatar
Jan Hambrecht committed
250 251
        return false;

252 253 254 255
    QPointF extensionStart = matrix.map(point->point());
    QPointF extensionStop = matrix.map(point->point()) + direction;
    float posOnExtension = project(extensionStart, extensionStop, position);
    if (posOnExtension < 0.0)
256 257 258 259 260 261
        return false;

    position = extensionStart + posOnExtension * direction;
    return true;
}

262
qreal ExtensionSnapStrategy::project(const QPointF &lineStart, const QPointF &lineEnd, const QPointF &point)
263 264 265
{
    QPointF diff = lineEnd - lineStart;
    QPointF relPoint = point - lineStart;
266 267
    qreal diffLength = sqrt(diff.x() * diff.x() + diff.y() * diff.y());
    if (diffLength == 0.0)
Jan Hambrecht's avatar
Jan Hambrecht committed
268 269
        return 0.0;

270 271
    diff /= diffLength;
    // project mouse position relative to stop position on extension line
272
    qreal scalar = relPoint.x() * diff.x() + relPoint.y() * diff.y();
273 274 275
    return scalar /= diffLength;
}

276
QPointF ExtensionSnapStrategy::extensionDirection(KoPathPoint * point, const QMatrix &matrix)
277 278
{
    KoPathShape * path = point->parent();
279
    KoPathPointIndex index = path->pathPointIndex(point);
280 281

    /// check if it is a start point
282 283
    if (point->properties() & KoPathPoint::StartSubpath) {
        if (point->activeControlPoint2()) {
284
            return matrix.map(point->point()) - matrix.map(point->controlPoint2());
285 286 287
        } else {
            KoPathPoint * next = path->pointByIndex(KoPathPointIndex(index.first, index.second + 1));
            if (! next)
288
                return QPointF();
289
            else if (next->activeControlPoint1())
290 291 292 293
                return matrix.map(point->point()) - matrix.map(next->controlPoint1());
            else
                return matrix.map(point->point()) - matrix.map(next->point());
        }
294 295
    } else {
        if (point->activeControlPoint1()) {
296
            return matrix.map(point->point()) - matrix.map(point->controlPoint1());
297 298 299
        } else {
            KoPathPoint * prev = path->pointByIndex(KoPathPointIndex(index.first, index.second - 1));
            if (! prev)
300
                return QPointF();
301
            else if (prev->activeControlPoint2())
302 303 304 305 306 307
                return matrix.map(point->point()) - matrix.map(prev->controlPoint2());
            else
                return matrix.map(point->point()) - matrix.map(prev->point());
        }
    }
}
308 309

IntersectionSnapStrategy::IntersectionSnapStrategy()
310
        : KoSnapStrategy(KoSnapStrategy::Intersection)
311 312 313
{
}

314
bool IntersectionSnapStrategy::snap(const QPointF &mousePosition, KoSnapProxy * proxy, qreal maxSnapDistance)
315
{
316
    qreal maxDistance = maxSnapDistance * maxSnapDistance;
317
    qreal minDistance = HUGE_VAL;
318

319 320
    QRectF rect(-maxSnapDistance, -maxSnapDistance, maxSnapDistance, maxSnapDistance);
    rect.moveCenter(mousePosition);
321 322
    QPointF snappedPoint = mousePosition;

323
    QList<KoPathSegment> segments = proxy->segmentsInRect(rect);
324 325 326
    //kDebug() << "found" << segments.count() << "segments in roi";

    int segmentCount = segments.count();
327
    for (int i = 0; i < segmentCount; ++i) {
328
        const KoPathSegment& s1 = segments[i];
329 330
        for (int j = i + 1; j < segmentCount; ++j) {
            QList<QPointF> isects = s1.intersections(segments[j]);
331
            //kDebug() << isects.count() << "intersections found";
332 333
            foreach(const QPointF &point, isects) {
                if (! rect.contains(point))
334
                    continue;
335 336
                qreal distance = fastDistance(mousePosition, point);
                if (distance < maxDistance && distance < minDistance) {
337 338 339 340 341 342 343
                    snappedPoint = point;
                    minDistance = distance;
                }
            }
        }
    }

344
    setSnappedPosition(snappedPoint);
345 346 347

    return (minDistance < HUGE_VAL);
}
348

349
QPainterPath IntersectionSnapStrategy::decoration(const KoViewConverter &converter) const
350
{
351 352
    QRectF unzoomedRect = converter.viewToDocument(QRectF(0, 0, 11, 11));
    unzoomedRect.moveCenter(snappedPosition());
353
    QPainterPath decoration;
354
    decoration.addRect(unzoomedRect);
355 356 357
    return decoration;
}

358
GridSnapStrategy::GridSnapStrategy()
359
        : KoSnapStrategy(KoSnapStrategy::Grid)
360 361 362
{
}

363
bool GridSnapStrategy::snap(const QPointF &mousePosition, KoSnapProxy * proxy, qreal maxSnapDistance)
364
{
365
    if (! proxy->canvas()->snapToGrid())
366 367 368
        return false;

    // The 1e-10 here is a workaround for some weird division problem.
369
    // 360.00062366 / 2.83465058 gives 127 'exactly' when shown as a qreal,
370
    // but when casting into an int, we get 126. In fact it's 127 - 5.64e-15 !
371
    qreal gridX, gridY;
372 373 374 375
    proxy->canvas()->gridSize(&gridX, &gridY);

    // we want to snap to the nearest grid point, so calculate
    // the grid rows/columns before and after the points position
376 377 378
    int col = static_cast<int>(mousePosition.x() / gridX + 1e-10);
    int nextCol = col + 1;
    int row = static_cast<int>(mousePosition.y() / gridY + 1e-10);
379 380 381
    int nextRow = row + 1;

    // now check which grid line has less distance to the point
382 383 384
    qreal distToCol = qAbs(col * gridX - mousePosition.x());
    qreal distToNextCol = qAbs(nextCol * gridX - mousePosition.x());
    if (distToCol > distToNextCol) {
385 386 387 388
        col = nextCol;
        distToCol = distToNextCol;
    }

389 390 391
    qreal distToRow = qAbs(row * gridY - mousePosition.y());
    qreal distToNextRow = qAbs(nextRow * gridY - mousePosition.y());
    if (distToRow > distToNextRow) {
392 393 394 395 396 397
        row = nextRow;
        distToRow = distToNextRow;
    }

    QPointF snappedPoint = mousePosition;

398 399
    qreal distance = distToCol * distToCol + distToRow * distToRow;
    qreal maxDistance = maxSnapDistance * maxSnapDistance;
400
    // now check if we are inside the snap distance
401 402
    if (distance < maxDistance) {
        snappedPoint = QPointF(col * gridX, row * gridY);
403 404
    }

405
    setSnappedPosition(snappedPoint);
406

407
    return (distance < maxDistance);
408
}
409

410
QPainterPath GridSnapStrategy::decoration(const KoViewConverter &converter) const
411
{
412
    QSizeF unzoomedSize = converter.viewToDocument(QSizeF(5, 5));
413
    QPainterPath decoration;
414 415 416 417
    decoration.moveTo(snappedPosition() - QPointF(unzoomedSize.width(), 0));
    decoration.lineTo(snappedPosition() + QPointF(unzoomedSize.width(), 0));
    decoration.moveTo(snappedPosition() - QPointF(0, unzoomedSize.height()));
    decoration.lineTo(snappedPosition() + QPointF(0, unzoomedSize.height()));
418 419
    return decoration;
}
420 421

BoundingBoxSnapStrategy::BoundingBoxSnapStrategy()
422
        : KoSnapStrategy(KoSnapStrategy::BoundingBox)
423 424 425
{
}

426
bool BoundingBoxSnapStrategy::snap(const QPointF &mousePosition, KoSnapProxy * proxy, qreal maxSnapDistance)
427
{
428
    qreal maxDistance = maxSnapDistance * maxSnapDistance;
429
    qreal minDistance = HUGE_VAL;
430

431 432
    QRectF rect(-maxSnapDistance, -maxSnapDistance, maxSnapDistance, maxSnapDistance);
    rect.moveCenter(mousePosition);
433 434 435 436 437 438 439 440 441 442
    QPointF snappedPoint = mousePosition;

    KoFlake::Position pointId[5] = {
        KoFlake::TopLeftCorner,
        KoFlake::TopRightCorner,
        KoFlake::BottomRightCorner,
        KoFlake::BottomLeftCorner,
        KoFlake::CenteredPosition
    };

443 444
    QList<KoShape*> shapes = proxy->shapesInRect(rect, true);
    foreach(KoShape * shape, shapes) {
445
        qreal shapeMinDistance = HUGE_VAL;
446
        // first check the corner and center points
447 448 449 450
        for (int i = 0; i < 5; ++i) {
            m_boxPoints[i] = shape->absolutePosition(pointId[i]);
            qreal d = fastDistance(mousePosition, m_boxPoints[i]);
            if (d < minDistance && d < maxDistance) {
451 452 453 454 455 456
                shapeMinDistance = d;
                minDistance = d;
                snappedPoint = m_boxPoints[i];
            }
        }
        // prioritize points over edges
457
        if (shapeMinDistance < maxDistance)
458 459 460
            continue;

        // now check distances to edges of bounding box
461
        for (int i = 0; i < 4; ++i) {
462
            QPointF pointOnLine;
463 464
            qreal d = squareDistanceToLine(m_boxPoints[i], m_boxPoints[(i+1)%4], mousePosition, pointOnLine);
            if (d < minDistance && d < maxDistance) {
465 466 467 468 469 470
                minDistance = d;
                snappedPoint = pointOnLine;
            }
        }
    }

471
    setSnappedPosition(snappedPoint);
472

473
    return (minDistance < maxDistance);
474 475 476

}

477
qreal BoundingBoxSnapStrategy::squareDistanceToLine(const QPointF &lineA, const QPointF &lineB, const QPointF &point, QPointF &pointOnLine)
478 479
{
    QPointF diff = lineB - lineA;
480 481
    qreal diffLength = sqrt(diff.x() * diff.x() + diff.y() * diff.y());
    if (diffLength == 0.0f)
482 483
        return HUGE_VAL;
    // project mouse position relative to start position on line
484 485
    qreal scalar = KoSnapStrategy::scalarProduct(point - lineA, diff / diffLength);
    if (scalar < 0.0 || scalar > diffLength)
486 487
        return HUGE_VAL;
    // calculate vector between relative mouse position and projected mouse position
488
    pointOnLine = lineA + scalar / diffLength * diff;
489 490 491 492
    QPointF distVec = pointOnLine - point;
    return distVec.x()*distVec.x() + distVec.y()*distVec.y();
}

493
QPainterPath BoundingBoxSnapStrategy::decoration(const KoViewConverter &converter) const
494
{
495
    QSizeF unzoomedSize = converter.viewToDocument(QSizeF(5, 5));
496 497

    QPainterPath decoration;
498 499 500 501
    decoration.moveTo(snappedPosition() - QPointF(unzoomedSize.width(), unzoomedSize.height()));
    decoration.lineTo(snappedPosition() + QPointF(unzoomedSize.width(), unzoomedSize.height()));
    decoration.moveTo(snappedPosition() - QPointF(unzoomedSize.width(), -unzoomedSize.height()));
    decoration.lineTo(snappedPosition() + QPointF(unzoomedSize.width(), -unzoomedSize.height()));
502 503 504

    return decoration;
}
Jan Hambrecht's avatar
Jan Hambrecht committed
505 506

LineGuideSnapStrategy::LineGuideSnapStrategy()
507
        : KoSnapStrategy(KoSnapStrategy::GuideLine)
Jan Hambrecht's avatar
Jan Hambrecht committed
508 509 510
{
}

511
bool LineGuideSnapStrategy::snap(const QPointF &mousePosition, KoSnapProxy * proxy, qreal maxSnapDistance)
Jan Hambrecht's avatar
Jan Hambrecht committed
512 513
{
    KoGuidesData * guidesData = proxy->canvas()->guidesData();
514 515
    if (! guidesData || ! guidesData->showGuideLines())
        return false;
Jan Hambrecht's avatar
Jan Hambrecht committed
516 517

    QPointF snappedPoint = mousePosition;
518
    m_orientation = 0;
Jan Hambrecht's avatar
Jan Hambrecht committed
519

520
    qreal minHorzDistance = maxSnapDistance;
521 522 523 524
    foreach(qreal guidePos, guidesData->horizontalGuideLines()) {
        qreal distance = qAbs(guidePos - mousePosition.y());
        if (distance < minHorzDistance) {
            snappedPoint.setY(guidePos);
525 526
            minHorzDistance = distance;
            m_orientation |= Qt::Horizontal;
Jan Hambrecht's avatar
Jan Hambrecht committed
527 528
        }
    }
529
    qreal minVertSnapDistance = maxSnapDistance;
530 531 532 533
    foreach(qreal guidePos, guidesData->verticalGuideLines()) {
        qreal distance = qAbs(guidePos - mousePosition.x());
        if (distance < minVertSnapDistance) {
            snappedPoint.setX(guidePos);
534 535
            minVertSnapDistance = distance;
            m_orientation |= Qt::Vertical;
Jan Hambrecht's avatar
Jan Hambrecht committed
536 537 538
        }
    }

539
    setSnappedPosition(snappedPoint);
Jan Hambrecht's avatar
Jan Hambrecht committed
540

541
    return (minHorzDistance < maxSnapDistance || minVertSnapDistance < maxSnapDistance);
Jan Hambrecht's avatar
Jan Hambrecht committed
542 543
}

544
QPainterPath LineGuideSnapStrategy::decoration(const KoViewConverter &converter) const
Jan Hambrecht's avatar
Jan Hambrecht committed
545
{
546
    QSizeF unzoomedSize = converter.viewToDocument(QSizeF(5, 5));
Jan Hambrecht's avatar
Jan Hambrecht committed
547
    QPainterPath decoration;
548 549 550
    if (m_orientation & Qt::Horizontal) {
        decoration.moveTo(snappedPosition() - QPointF(unzoomedSize.width(), 0));
        decoration.lineTo(snappedPosition() + QPointF(unzoomedSize.width(), 0));
Jan Hambrecht's avatar
Jan Hambrecht committed
551
    }
552 553 554
    if (m_orientation & Qt::Vertical) {
        decoration.moveTo(snappedPosition() - QPointF(0, unzoomedSize.height()));
        decoration.lineTo(snappedPosition() + QPointF(0, unzoomedSize.height()));
Jan Hambrecht's avatar
Jan Hambrecht committed
555 556 557
    }
    return decoration;
}