// SPDX-License-Identifier: GPL-2.0-or-later /* * Vanishing point for 3D perspectives * * Authors: * bulia byak * Johan Engelen * Maximilian Albert * Abhishek Sharma * Jon A. Cruz * * Copyright (C) 2005-2007 authors * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include #include "vanishing-point.h" #include "desktop.h" #include "document-undo.h" #include "perspective-line.h" #include "snap.h" #include "display/control/canvas-item-curve.h" #include "object/sp-namedview.h" #include "object/box3d.h" #include "ui/icon-names.h" #include "ui/knot/knot.h" #include "ui/shape-editor.h" #include "ui/tools/tool-base.h" using Inkscape::DocumentUndo; namespace Box3D { #define VP_KNOT_COLOR_NORMAL 0xffffff00 #define VP_KNOT_COLOR_SELECTED 0x0000ff00 // screen pixels between knots when they snap: #define SNAP_DIST 5 // absolute distance between gradient points for them to become a single dragger when the drag is created: #define MERGE_DIST 0.1 // knot shapes corresponding to GrPointType enum Inkscape::CanvasItemCtrlShape vp_knot_shapes[] = { Inkscape::CANVAS_ITEM_CTRL_SHAPE_SQUARE, // VP_FINITE Inkscape::CANVAS_ITEM_CTRL_SHAPE_CIRCLE // VP_INFINITE }; static void vp_drag_sel_changed(Inkscape::Selection * /*selection*/, gpointer data) { VPDrag *drag = (VPDrag *)data; drag->updateDraggers(); drag->updateLines(); drag->updateBoxReprs(); } static void vp_drag_sel_modified(Inkscape::Selection * /*selection*/, guint /*flags*/, gpointer data) { VPDrag *drag = (VPDrag *)data; drag->updateLines(); // drag->updateBoxReprs(); drag->updateBoxHandles(); // FIXME: Only update the handles of boxes on this dragger (not on all) drag->updateDraggers(); } static bool have_VPs_of_same_perspective(VPDragger *dr1, VPDragger *dr2) { for (auto & vp : dr1->vps) { if (dr2->hasPerspective(vp.get_perspective())) { return true; } } return false; } static void vp_knot_moved_handler(SPKnot *knot, Geom::Point const &ppointer, guint state, gpointer data) { VPDragger *dragger = (VPDragger *)data; VPDrag *drag = dragger->parent; Geom::Point p = ppointer; // FIXME: take from prefs double snap_dist = SNAP_DIST / SP_ACTIVE_DESKTOP->current_zoom(); /* * We use dragging_started to indicate if we have already checked for the need to split Draggers up. * This only has the purpose of avoiding costly checks in the routine below. */ if (!dragger->dragging_started && (state & GDK_SHIFT_MASK)) { /* with Shift; if there is more than one box linked to this VP we need to split it and create a new perspective */ if (dragger->numberOfBoxes() > 1) { // FIXME: Don't do anything if *all* boxes of a VP are selected std::set sel_vps = dragger->VPsOfSelectedBoxes(); std::list sel_boxes; for (auto sel_vp : sel_vps) { // for each VP that has selected boxes: Persp3D *old_persp = sel_vp->get_perspective(); sel_boxes = sel_vp->selectedBoxes(SP_ACTIVE_DESKTOP->getSelection()); // we create a new perspective ... Persp3D *new_persp = Persp3D::create_xml_element(dragger->parent->document); /* ... unlink the boxes from the old one and FIXME: We need to unlink the _un_selected boxes of each VP so that the correct boxes are kept with the VP being moved */ std::list bx_lst = old_persp->list_of_boxes(); for (auto & box : bx_lst) { if (std::find(sel_boxes.begin(), sel_boxes.end(), box) == sel_boxes.end()) { /* if a box in the VP is unselected, move it to the newly created perspective so that it doesn't get dragged **/ box->switch_perspectives(old_persp, new_persp); } } } // FIXME: Do we need to create a new dragger as well? dragger->updateZOrders(); DocumentUndo::done(SP_ACTIVE_DESKTOP->getDocument(), _("Split vanishing points"), INKSCAPE_ICON("draw-cuboid")); return; } } if (!(state & GDK_SHIFT_MASK)) { // without Shift; see if we need to snap to another dragger for (std::vector::const_iterator di = dragger->parent->draggers.begin(); di != dragger->parent->draggers.end(); ++di) { VPDragger *d_new = *di; if ((d_new != dragger) && (Geom::L2(d_new->point - p) < snap_dist)) { if (have_VPs_of_same_perspective(dragger, d_new)) { // this would result in degenerate boxes, which we disallow for the time being continue; } // update positions ... (this is needed so that the perspectives are detected as identical) // FIXME: This is called a bit too often, isn't it? for (auto & vp : dragger->vps) { vp.set_pos(d_new->point); } // ... join lists of VPs ... d_new->vps.merge(dragger->vps); // ... delete old dragger ... drag->draggers.erase(std::remove(drag->draggers.begin(), drag->draggers.end(), dragger), drag->draggers.end()); delete dragger; dragger = nullptr; // ... and merge any duplicate perspectives d_new->mergePerspectives(); // TODO: Update the new merged dragger d_new->updateTip(); d_new->parent->updateBoxDisplays(); // FIXME: Only update boxes in current dragger! d_new->updateZOrders(); drag->updateLines(); // TODO: Undo machinery; this doesn't work yet because perspectives must be created and // deleted according to changes in the svg representation, not based on any user input // as is currently the case. DocumentUndo::done(SP_ACTIVE_DESKTOP->getDocument(), _("Merge vanishing points"), INKSCAPE_ICON("draw-cuboid")); return; } } } // We didn't hit the return statement above, so we didn't snap to another dragger. Therefore we'll now try a regular // snap // Regardless of the status of the SHIFT key, we will try to snap; Here SHIFT does not disable snapping, as the // shift key // has a different purpose in this context (see above) SPDesktop *desktop = SP_ACTIVE_DESKTOP; SnapManager &m = desktop->namedview->snap_manager; m.setup(desktop); Inkscape::SnappedPoint s = m.freeSnap(Inkscape::SnapCandidatePoint(p, Inkscape::SNAPSOURCE_OTHER_HANDLE)); m.unSetup(); if (s.getSnapped()) { p = s.getPoint(); knot->moveto(p); } dragger->point = p; // FIXME: Is dragger->point being used at all? dragger->updateVPs(p); dragger->updateBoxDisplays(); dragger->parent->updateBoxHandles(); // FIXME: Only update the handles of boxes on this dragger (not on all) dragger->updateZOrders(); drag->updateLines(); dragger->dragging_started = true; } static void vp_knot_grabbed_handler(SPKnot * /*knot*/, unsigned int /*state*/, gpointer data) { VPDragger *dragger = (VPDragger *)data; VPDrag *drag = dragger->parent; drag->dragging = true; } static void vp_knot_ungrabbed_handler(SPKnot *knot, guint /*state*/, gpointer data) { VPDragger *dragger = (VPDragger *)data; dragger->point_original = dragger->point = knot->pos; dragger->dragging_started = false; for (auto & vp : dragger->vps) { vp.set_pos(knot->pos); vp.updateBoxReprs(); vp.updatePerspRepr(); } dragger->parent->updateDraggers(); dragger->parent->updateLines(); dragger->parent->updateBoxHandles(); // TODO: Update box's paths and svg representation dragger->parent->dragging = false; // TODO: Undo machinery!! g_return_if_fail(dragger->parent); g_return_if_fail(dragger->parent->document); DocumentUndo::done(dragger->parent->document, _("3D box: Move vanishing point"), INKSCAPE_ICON("draw-cuboid")); } unsigned int VanishingPoint::global_counter = 0; // FIXME: Rename to something more meaningful! void VanishingPoint::set_pos(Proj::Pt2 const &pt) { g_return_if_fail(_persp); _persp->perspective_impl->tmat.set_image_pt(_axis, pt); } std::list VanishingPoint::selectedBoxes(Inkscape::Selection *sel) { std::list sel_boxes; auto itemlist = sel->items(); for (auto i = itemlist.begin(); i != itemlist.end(); ++i) { SPItem *item = *i; SPBox3D *box = dynamic_cast(item); if (box && this->hasBox(box)) { sel_boxes.push_back(box); } } return sel_boxes; } VPDragger::VPDragger(VPDrag *parent, Geom::Point p, VanishingPoint &vp) : parent(parent) , knot(nullptr) , point(p) , point_original(p) , dragging_started(false) , vps() { if (vp.is_finite()) { // create the knot this->knot = new SPKnot(SP_ACTIVE_DESKTOP, "", Inkscape::CANVAS_ITEM_CTRL_TYPE_ANCHOR, "CanvasItemCtrl:VPDragger"); this->knot->setFill(VP_KNOT_COLOR_NORMAL, VP_KNOT_COLOR_NORMAL, VP_KNOT_COLOR_NORMAL, VP_KNOT_COLOR_NORMAL); this->knot->setStroke(0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff); this->knot->updateCtrl(); // move knot to the given point this->knot->setPosition(this->point, SP_KNOT_STATE_NORMAL); this->knot->show(); // connect knot's signals this->_moved_connection = this->knot->moved_signal.connect(sigc::bind(sigc::ptr_fun(vp_knot_moved_handler), this)); this->_grabbed_connection = this->knot->grabbed_signal.connect(sigc::bind(sigc::ptr_fun(vp_knot_grabbed_handler), this)); this->_ungrabbed_connection = this->knot->ungrabbed_signal.connect(sigc::bind(sigc::ptr_fun(vp_knot_ungrabbed_handler), this)); // add the initial VP (which may be NULL!) this->addVP(vp); } } VPDragger::~VPDragger() { // disconnect signals this->_moved_connection.disconnect(); this->_grabbed_connection.disconnect(); this->_ungrabbed_connection.disconnect(); /* unref should call destroy */ knot_unref(this->knot); } /** Updates the statusbar tip of the dragger knot, based on its draggables */ void VPDragger::updateTip() { if (this->knot && this->knot->tip) { g_free(this->knot->tip); this->knot->tip = nullptr; } guint num = this->numberOfBoxes(); if (this->vps.size() == 1) { if (this->vps.front().is_finite()) { this->knot->tip = g_strdup_printf(ngettext("Finite vanishing point shared by %d box", "Finite vanishing point shared by %d boxes; drag " "with Shift to separate selected box(es)", num), num); } else { // This won't make sense any more when infinite VPs are not shown on the canvas, // but currently we update the status message anyway this->knot->tip = g_strdup_printf(ngettext("Infinite vanishing point shared by the box", "Infinite vanishing point shared by %d boxes; " "drag with Shift to separate selected box(es)", num), num); } } else { int length = this->vps.size(); char const *tmpl = ngettext("Collection of %d vanishing points shared by the box; " "drag with Shift to separate", "Collection of %d vanishing points shared by %d boxes; " "drag with Shift to separate", num); this->knot->tip = g_strdup_printf(tmpl, length, num); } } /** * Adds a vanishing point to the dragger (also updates the position if necessary); * the perspective is stored separately, too, for efficiency in updating boxes. */ void VPDragger::addVP(VanishingPoint &vp, bool update_pos) { if (!vp.is_finite() || std::find(vps.begin(), vps.end(), vp) != vps.end()) { // don't add infinite VPs; don't add the same VP twice return; } if (update_pos) { vp.set_pos(this->point); } this->vps.push_front(vp); this->updateTip(); } void VPDragger::removeVP(VanishingPoint const &vp) { std::list::iterator i = std::find(this->vps.begin(), this->vps.end(), vp); if (i != this->vps.end()) { this->vps.erase(i); } this->updateTip(); } VanishingPoint *VPDragger::findVPWithBox(SPBox3D *box) { for (auto & vp : vps) { if (vp.hasBox(box)) { return &vp; } } return nullptr; } std::set VPDragger::VPsOfSelectedBoxes() { std::set sel_vps; VanishingPoint *vp; // FIXME: Should we take the selection from the parent VPDrag? I guess it shouldn't make a difference. Inkscape::Selection *sel = SP_ACTIVE_DESKTOP->getSelection(); auto itemlist = sel->items(); for (auto i = itemlist.begin(); i != itemlist.end(); ++i) { SPItem *item = *i; SPBox3D *box = dynamic_cast(item); if (box) { vp = this->findVPWithBox(box); if (vp) { sel_vps.insert(vp); } } } return sel_vps; } guint VPDragger::numberOfBoxes() { guint num = 0; for (auto & vp : vps) { num += vp.numberOfBoxes(); } return num; } bool VPDragger::hasPerspective(const Persp3D *persp) { for (auto & vp : vps) { if (persp->perspectives_coincide(vp.get_perspective())) { return true; } } return false; } void VPDragger::mergePerspectives() { Persp3D *persp1, *persp2; for (std::list::iterator i = vps.begin(); i != vps.end(); ++i) { persp1 = (*i).get_perspective(); for (std::list::iterator j = i; j != vps.end(); ++j) { persp2 = (*j).get_perspective(); if (persp1 == persp2) { /* don't merge a perspective with itself */ continue; } if (persp1->perspectives_coincide(persp2)) { /* if perspectives coincide but are not the same, merge them */ persp1->absorb(persp2); this->parent->swap_perspectives_of_VPs(persp2, persp1); persp2->deleteObject(false); } } } } void VPDragger::updateBoxDisplays() { for (auto & vp : this->vps) { vp.updateBoxDisplays(); } } void VPDragger::updateVPs(Geom::Point const &pt) { for (auto & vp : this->vps) { vp.set_pos(pt); } } void VPDragger::updateZOrders() { for (auto & vp : this->vps) { vp.get_perspective()->update_z_orders(); } } void VPDragger::printVPs() { g_print("VPDragger at position (%f, %f):\n", point[Geom::X], point[Geom::Y]); for (auto & vp : this->vps) { g_print(" VP %s\n", vp.axisString()); } } VPDrag::VPDrag(SPDocument *document) { this->document = document; this->selection = SP_ACTIVE_DESKTOP->getSelection(); this->show_lines = true; this->front_or_rear_lines = 0x1; this->dragging = false; this->sel_changed_connection = this->selection->connectChanged(sigc::bind(sigc::ptr_fun(&vp_drag_sel_changed), (gpointer) this) ); this->sel_modified_connection = this->selection->connectModified(sigc::bind(sigc::ptr_fun(&vp_drag_sel_modified), (gpointer) this)); this->updateDraggers(); this->updateLines(); } VPDrag::~VPDrag() { this->sel_changed_connection.disconnect(); this->sel_modified_connection.disconnect(); for (auto dragger : this->draggers) { delete dragger; } this->draggers.clear(); for (auto item_curve : item_curves) { delete item_curve; } item_curves.clear(); } /** * Select the dragger that has the given VP. */ VPDragger *VPDrag::getDraggerFor(VanishingPoint const &vp) { for (auto dragger : this->draggers) { for (std::list::iterator j = dragger->vps.begin(); j != dragger->vps.end(); ++j) { // TODO: Should we compare the pointers or the VPs themselves!?!?!?! if (*j == vp) { return (dragger); } } } return nullptr; } void VPDrag::printDraggers() { g_print("=== VPDrag info: =================================\n"); for (auto dragger : this->draggers) { dragger->printVPs(); g_print("========\n"); } g_print("=================================================\n"); } /** * Regenerates the draggers list from the current selection; is called when selection is changed or modified */ void VPDrag::updateDraggers() { if (this->dragging) return; // delete old draggers for (auto dragger : this->draggers) { delete dragger; } this->draggers.clear(); g_return_if_fail(this->selection != nullptr); auto itemlist = this->selection->items(); for (auto i = itemlist.begin(); i != itemlist.end(); ++i) { SPItem *item = *i; SPBox3D *box = dynamic_cast(item); if (box) { VanishingPoint vp; for (int i = 0; i < 3; ++i) { vp.set(box->get_perspective(), Proj::axes[i]); addDragger(vp); } } } } /** Regenerates the lines list from the current selection; is called on each move of a dragger, so that lines are always in sync with the actual perspective */ void VPDrag::updateLines() { // Delete old lines for (auto curve : item_curves) { delete curve; } item_curves.clear(); // do nothing if perspective lines are currently disabled if (this->show_lines == 0) return; g_return_if_fail(this->selection != nullptr); auto itemlist = this->selection->items(); for (auto i = itemlist.begin(); i != itemlist.end(); ++i) { SPItem *item = *i; SPBox3D *box = dynamic_cast(item); if (box) { this->drawLinesForFace(box, Proj::X); this->drawLinesForFace(box, Proj::Y); this->drawLinesForFace(box, Proj::Z); } } } void VPDrag::updateBoxHandles() { // FIXME: Is there a way to update the knots without accessing the // (previously) statically linked function KnotHolder::update_knots? auto sel = selection->items(); if (sel.empty()) return; // no selection if (boost::distance(sel) > 1) { // Currently we only show handles if a single box is selected return; } Inkscape::UI::Tools::ToolBase *ec = SP_ACTIVE_DESKTOP->getEventContext(); g_assert(ec != nullptr); if (ec->shape_editor != nullptr) { ec->shape_editor->update_knotholder(); } } void VPDrag::updateBoxReprs() { for (auto dragger : this->draggers) { for (auto & vp : dragger->vps) { vp.updateBoxReprs(); } } } void VPDrag::updateBoxDisplays() { for (auto dragger : this->draggers) { for (auto & vp : dragger->vps) { vp.updateBoxDisplays(); } } } /** * Depending on the value of all_lines, draw the front and/or rear perspective lines starting from the given corners. */ void VPDrag::drawLinesForFace(const SPBox3D *box, Proj::Axis axis) //, guint corner1, guint corner2, guint corner3, guint corner4) { Inkscape::CanvasItemColor type = Inkscape::CANVAS_ITEM_PRIMARY; switch (axis) { // TODO: Make color selectable by user case Proj::X: type = Inkscape::CANVAS_ITEM_SECONDARY; break; case Proj::Y: type = Inkscape::CANVAS_ITEM_PRIMARY; break; case Proj::Z: type = Inkscape::CANVAS_ITEM_TERTIARY;; break; default: g_assert_not_reached(); } const size_t NUM_CORNERS = 4; Geom::Point corners[NUM_CORNERS]; box->corners_for_PLs(axis, corners[0], corners[1], corners[2], corners[3]); g_return_if_fail(box->get_perspective()); Proj::Pt2 vp = box->get_perspective()->get_VP(axis); if (vp.is_finite()) { // draw perspective lines for finite VPs Geom::Point pt = vp.affine(); if (this->front_or_rear_lines & 0x1) { // draw 'front' perspective lines this->addCurve(corners[0], pt, type); this->addCurve(corners[1], pt, type); } if (this->front_or_rear_lines & 0x2) { // draw 'rear' perspective lines this->addCurve(corners[2], pt, type); this->addCurve(corners[3], pt, type); } } else { // draw perspective lines for infinite VPs std::optional pts[NUM_CORNERS]; Persp3D *persp = box->get_perspective(); SPDesktop *desktop = SP_ACTIVE_DESKTOP; // FIXME: Store the desktop in VPDrag for (size_t i = 0; i < NUM_CORNERS; i++) { Box3D::PerspectiveLine pl(corners[i], axis, persp); if (!(pts[i] = pl.intersection_with_viewbox(desktop))) { // some perspective lines are outside the canvas; currently we don't draw any of them return; } } if (this->front_or_rear_lines & 0x1) { // draw 'front' perspective lines this->addCurve(corners[0], *pts[0], type); this->addCurve(corners[1], *pts[1], type); } if (this->front_or_rear_lines & 0x2) { // draw 'rear' perspective lines this->addCurve(corners[2], *pts[2], type); this->addCurve(corners[3], *pts[3], type); } } } /** * If there already exists a dragger within MERGE_DIST of p, add the VP to it; * otherwise create new dragger and add it to draggers list * We also store the corresponding perspective in case it is not already present. */ void VPDrag::addDragger(VanishingPoint &vp) { if (!vp.is_finite()) { // don't create draggers for infinite vanishing points return; } Geom::Point p = vp.get_pos(); for (auto dragger : this->draggers) { if (Geom::L2(dragger->point - p) < MERGE_DIST) { // distance is small, merge this draggable into dragger, no need to create new dragger dragger->addVP(vp); return; } } VPDragger *new_dragger = new VPDragger(this, p, vp); // fixme: draggers should be added AFTER the last one: this way tabbing through them will be from begin to end. this->draggers.push_back(new_dragger); } void VPDrag::swap_perspectives_of_VPs(Persp3D *persp2, Persp3D *persp1) { // iterate over all VP in all draggers and replace persp2 with persp1 for (auto dragger : this->draggers) { for (auto & vp : dragger->vps) { if (vp.get_perspective() == persp2) { vp.set_perspective(persp1); } } } } void VPDrag::addCurve(Geom::Point const &p1, Geom::Point const &p2, Inkscape::CanvasItemColor color) { auto item_curve = new Inkscape::CanvasItemCurve(SP_ACTIVE_DESKTOP->getCanvasControls(), p1, p2); item_curve->set_name("3DBoxCurve"); item_curve->set_stroke(color); item_curves.push_back(item_curve); } } // namespace Box3D /* 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:fileencoding=utf-8:textwidth=99 :