#define __VANISHING_POINT_C__ /* * Vanishing point for 3D perspectives * * Authors: * bulia byak <buliabyak@users.sf.net> * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> * Maximilian Albert <Anhalter42@gmx.de> * * Copyright (C) 2005-2007 authors * * Released under GNU GPL, read the file 'COPYING' for more information */ #include <glibmm/i18n.h> #include "vanishing-point.h" #include "desktop-handles.h" #include "desktop.h" #include "event-context.h" #include "xml/repr.h" #include "perspective-line.h" #include "shape-editor.h" namespace Box3D { #define VP_KNOT_COLOR_NORMAL 0xffffff00 #define VP_KNOT_COLOR_SELECTED 0x0000ff00 #define VP_LINE_COLOR_FILL 0x0000ff7f #define VP_LINE_COLOR_STROKE_X 0xff00007f #define VP_LINE_COLOR_STROKE_Y 0x0000ff7f #define VP_LINE_COLOR_STROKE_Z 0xffff007f // 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 SPKnotShapeType vp_knot_shapes [] = { SP_KNOT_SHAPE_SQUARE, // VP_FINITE SP_KNOT_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 (std::list<VanishingPoint>::iterator i = dr1->vps.begin(); i != dr1->vps.end(); ++i) { if (dr2->hasPerspective ((*i).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 / inkscape_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<VanishingPoint*, less_ptr> sel_vps = dragger->VPsOfSelectedBoxes(); std::list<SPBox3D *> sel_boxes; for (std::set<VanishingPoint*, less_ptr>::iterator vp = sel_vps.begin(); vp != sel_vps.end(); ++vp) { // for each VP that has selected boxes: Persp3D *old_persp = (*vp)->get_perspective(); sel_boxes = (*vp)->selectedBoxes(sp_desktop_selection(inkscape_active_desktop())); // we create a new perspective ... Persp3D *new_persp = persp3d_create_xml_element (dragger->parent->document, old_persp); /* ... 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<SPBox3D *> bx_lst = persp3d_list_of_boxes(old_persp); for (std::list<SPBox3D *>::iterator i = bx_lst.begin(); i != bx_lst.end(); ++i) { if (std::find(sel_boxes.begin(), sel_boxes.end(), *i) == 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 **/ box3d_switch_perspectives(*i, old_persp, new_persp); } } } // FIXME: Do we need to create a new dragger as well? dragger->updateZOrders (); sp_document_done (sp_desktop_document (inkscape_active_desktop()), SP_VERB_CONTEXT_3DBOX, _("Split vanishing points")); return; } } if (!(state & GDK_SHIFT_MASK)) { // without Shift; see if we need to snap to another dragger for (GList *di = dragger->parent->draggers; di != NULL; di = di->next) { VPDragger *d_new = (VPDragger *) di->data; 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 (std::list<VanishingPoint>::iterator j = dragger->vps.begin(); j != dragger->vps.end(); ++j) { (*j).set_pos(d_new->point); } // ... join lists of VPs ... d_new->vps.merge(dragger->vps); // ... delete old dragger ... drag->draggers = g_list_remove (drag->draggers, dragger); delete dragger; dragger = NULL; // ... 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. sp_document_done (sp_desktop_document (inkscape_active_desktop()), SP_VERB_CONTEXT_3DBOX, _("Merge vanishing points")); return; } } } dragger->point = p; // FIXME: Brauchen wir dragger->point überhaupt? 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; } 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 (std::list<VanishingPoint>::iterator i = dragger->vps.begin(); i != dragger->vps.end(); ++i) { (*i).set_pos (knot->pos); (*i).updateBoxReprs(); (*i).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); sp_document_done(dragger->parent->document, SP_VERB_CONTEXT_3DBOX, _("3D box: Move vanishing point")); } 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->tmat.set_image_pt (_axis, pt); } std::list<SPBox3D *> VanishingPoint::selectedBoxes(Inkscape::Selection *sel) { std::list<SPBox3D *> sel_boxes; for (GSList const* i = sel->itemList(); i != NULL; i = i->next) { if (!SP_IS_BOX3D(i->data)) continue; SPBox3D *box = SP_BOX3D(i->data); if (this->hasBox(box)) { sel_boxes.push_back (box); } } return sel_boxes; } VPDragger::VPDragger(VPDrag *parent, Geom::Point p, VanishingPoint &vp) { this->parent = parent; this->point = p; this->point_original = p; this->dragging_started = false; if (vp.is_finite()) { // create the knot this->knot = sp_knot_new (inkscape_active_desktop(), NULL); this->knot->setMode(SP_KNOT_MODE_XOR); this->knot->setFill(VP_KNOT_COLOR_NORMAL, VP_KNOT_COLOR_NORMAL, VP_KNOT_COLOR_NORMAL); this->knot->setStroke(0x000000ff, 0x000000ff, 0x000000ff); sp_knot_update_ctrl(this->knot); // move knot to the given point sp_knot_set_position (this->knot, this->point, SP_KNOT_STATE_NORMAL); sp_knot_show (this->knot); // connect knot's signals g_signal_connect (G_OBJECT (this->knot), "moved", G_CALLBACK (vp_knot_moved_handler), this); g_signal_connect (G_OBJECT (this->knot), "grabbed", G_CALLBACK (vp_knot_grabbed_handler), this); g_signal_connect (G_OBJECT (this->knot), "ungrabbed", G_CALLBACK (vp_knot_ungrabbed_handler), this); // add the initial VP (which may be NULL!) this->addVP (vp); } } VPDragger::~VPDragger() { // disconnect signals g_signal_handlers_disconnect_by_func(G_OBJECT(this->knot), (gpointer) G_CALLBACK (vp_knot_moved_handler), this); g_signal_handlers_disconnect_by_func(G_OBJECT(this->knot), (gpointer) G_CALLBACK (vp_knot_grabbed_handler), this); g_signal_handlers_disconnect_by_func(G_OBJECT(this->knot), (gpointer) G_CALLBACK (vp_knot_ungrabbed_handler), this); /* unref should call destroy */ g_object_unref (G_OBJECT (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 = NULL; } guint num = this->numberOfBoxes(); if (this->vps.size() == 1) { if (this->vps.front().is_finite()) { this->knot->tip = g_strdup_printf (ngettext("<b>Finite</b> vanishing point shared by <b>%d</b> box", "<b>Finite</b> vanishing point shared by <b>%d</b> boxes; drag with <b>Shift</b> 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("<b>Infinite</b> vanishing point shared by <b>%d</b> box", "<b>Infinite</b> vanishing point shared by <b>%d</b> boxes; drag with <b>Shift</b> to separate selected box(es)", num), num); } } else { int length = this->vps.size(); char *desc1 = g_strdup_printf ("Collection of <b>%d</b> vanishing points ", length); char *desc2 = g_strdup_printf (ngettext("shared by <b>%d</b> box; drag with <b>Shift</b> to separate selected box(es)", "shared by <b>%d</b> boxes; drag with <b>Shift</b> to separate selected box(es)", num), num); this->knot->tip = g_strconcat(desc1, desc2, NULL); g_free (desc1); g_free (desc2); } } /** * 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<VanishingPoint>::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 (std::list<VanishingPoint>::iterator vp = vps.begin(); vp != vps.end(); ++vp) { if ((*vp).hasBox(box)) { return &(*vp); } } return NULL; } std::set<VanishingPoint*, less_ptr> VPDragger::VPsOfSelectedBoxes() { std::set<VanishingPoint*, less_ptr> 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_desktop_selection(inkscape_active_desktop()); for (GSList const* i = sel->itemList(); i != NULL; i = i->next) { if (!SP_IS_BOX3D(i->data)) continue; SPBox3D *box = SP_BOX3D(i->data); vp = this->findVPWithBox(box); if (vp) { sel_vps.insert (vp); } } return sel_vps; } guint VPDragger::numberOfBoxes () { guint num = 0; for (std::list<VanishingPoint>::iterator vp = vps.begin(); vp != vps.end(); ++vp) { num += (*vp).numberOfBoxes(); } return num; } bool VPDragger::hasPerspective (const Persp3D *persp) { for (std::list<VanishingPoint>::iterator i = vps.begin(); i != vps.end(); ++i) { if (persp3d_perspectives_coincide(persp, (*i).get_perspective())) { return true; } } return false; } void VPDragger::mergePerspectives () { Persp3D *persp1, *persp2; for (std::list<VanishingPoint>::iterator i = vps.begin(); i != vps.end(); ++i) { persp1 = (*i).get_perspective(); for (std::list<VanishingPoint>::iterator j = i; j != vps.end(); ++j) { persp2 = (*j).get_perspective(); if (persp1 == persp2) { /* don't merge a perspective with itself */ continue; } if (persp3d_perspectives_coincide(persp1,persp2)) { /* if perspectives coincide but are not the same, merge them */ persp3d_absorb(persp1, persp2); this->parent->swap_perspectives_of_VPs(persp2, persp1); SP_OBJECT(persp2)->deleteObject(false); } } } } void VPDragger::updateBoxDisplays () { for (std::list<VanishingPoint>::iterator i = this->vps.begin(); i != this->vps.end(); ++i) { (*i).updateBoxDisplays(); } } void VPDragger::updateVPs (Geom::Point const &pt) { for (std::list<VanishingPoint>::iterator i = this->vps.begin(); i != this->vps.end(); ++i) { (*i).set_pos (pt); } } void VPDragger::updateZOrders () { for (std::list<VanishingPoint>::iterator i = this->vps.begin(); i != this->vps.end(); ++i) { persp3d_update_z_orders((*i).get_perspective()); } } void VPDragger::printVPs() { g_print ("VPDragger at position (%f, %f):\n", point[Geom::X], point[Geom::Y]); for (std::list<VanishingPoint>::iterator i = this->vps.begin(); i != this->vps.end(); ++i) { g_print (" VP %s\n", (*i).axisString()); } } VPDrag::VPDrag (SPDocument *document) { this->document = document; this->selection = sp_desktop_selection(inkscape_active_desktop()); this->draggers = NULL; this->lines = NULL; 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 (GList *l = this->draggers; l != NULL; l = l->next) { delete ((VPDragger *) l->data); } g_list_free (this->draggers); this->draggers = NULL; for (GSList const *i = this->lines; i != NULL; i = i->next) { gtk_object_destroy( GTK_OBJECT (i->data)); } g_slist_free (this->lines); this->lines = NULL; } /** * Select the dragger that has the given VP. */ VPDragger * VPDrag::getDraggerFor (VanishingPoint const &vp) { for (GList const* i = this->draggers; i != NULL; i = i->next) { VPDragger *dragger = (VPDragger *) i->data; for (std::list<VanishingPoint>::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 NULL; } void VPDrag::printDraggers () { g_print ("=== VPDrag info: =================================\n"); for (GList const* i = this->draggers; i != NULL; i = i->next) { ((VPDragger *) i->data)->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 (GList const* i = this->draggers; i != NULL; i = i->next) { delete ((VPDragger *) i->data); } g_list_free (this->draggers); this->draggers = NULL; g_return_if_fail (this->selection != NULL); for (GSList const* i = this->selection->itemList(); i != NULL; i = i->next) { SPItem *item = SP_ITEM(i->data); if (!SP_IS_BOX3D (item)) continue; SPBox3D *box = SP_BOX3D (item); VanishingPoint vp; for (int i = 0; i < 3; ++i) { vp.set(box3d_get_perspective(box), 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 (GSList const *i = this->lines; i != NULL; i = i->next) { gtk_object_destroy( GTK_OBJECT (i->data)); } g_slist_free (this->lines); this->lines = NULL; // do nothing if perspective lines are currently disabled if (this->show_lines == 0) return; g_return_if_fail (this->selection != NULL); for (GSList const* i = this->selection->itemList(); i != NULL; i = i->next) { if (!SP_IS_BOX3D(i->data)) continue; SPBox3D *box = SP_BOX3D (i->data); 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? GSList *sel = (GSList *) selection->itemList(); if (!sel) return; // no selection if (g_slist_length (sel) > 1) { // Currently we only show handles if a single box is selected return; } SPEventContext *ec = inkscape_active_event_context(); g_assert (ec != NULL); if (ec->shape_editor != NULL) { ec->shape_editor->update_knotholder(); } } void VPDrag::updateBoxReprs () { for (GList *i = this->draggers; i != NULL; i = i->next) { VPDragger *dragger = (VPDragger *) i->data; for (std::list<VanishingPoint>::iterator i = dragger->vps.begin(); i != dragger->vps.end(); ++i) { (*i).updateBoxReprs(); } } } void VPDrag::updateBoxDisplays () { for (GList *i = this->draggers; i != NULL; i = i->next) { VPDragger *dragger = (VPDragger *) i->data; for (std::list<VanishingPoint>::iterator i = dragger->vps.begin(); i != dragger->vps.end(); ++i) { (*i).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) { guint color; switch (axis) { // TODO: Make color selectable by user case Proj::X: color = VP_LINE_COLOR_STROKE_X; break; case Proj::Y: color = VP_LINE_COLOR_STROKE_Y; break; case Proj::Z: color = VP_LINE_COLOR_STROKE_Z; break; default: g_assert_not_reached(); } Geom::Point corner1, corner2, corner3, corner4; box3d_corners_for_PLs (box, axis, corner1, corner2, corner3, corner4); g_return_if_fail (box3d_get_perspective(box)); Proj::Pt2 vp = persp3d_get_VP (box3d_get_perspective(box), 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->addLine (corner1, pt, color); this->addLine (corner2, pt, color); } if (this->front_or_rear_lines & 0x2) { // draw 'rear' perspective lines this->addLine (corner3, pt, color); this->addLine (corner4, pt, color); } } else { // draw perspective lines for infinite VPs boost::optional<Geom::Point> pt1, pt2, pt3, pt4; Persp3D *persp = box3d_get_perspective(box); SPDesktop *desktop = inkscape_active_desktop (); // FIXME: Store the desktop in VPDrag Box3D::PerspectiveLine pl (corner1, axis, persp); pt1 = pl.intersection_with_viewbox(desktop); pl = Box3D::PerspectiveLine (corner2, axis, persp); pt2 = pl.intersection_with_viewbox(desktop); pl = Box3D::PerspectiveLine (corner3, axis, persp); pt3 = pl.intersection_with_viewbox(desktop); pl = Box3D::PerspectiveLine (corner4, axis, persp); pt4 = pl.intersection_with_viewbox(desktop); if (!pt1 || !pt2 || !pt3 || !pt4) { // some perspective lines s 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->addLine (corner1, *pt1, color); this->addLine (corner2, *pt2, color); } if (this->front_or_rear_lines & 0x2) { // draw 'rear' perspective lines this->addLine (corner3, *pt3, color); this->addLine (corner4, *pt4, color); } } } /** * 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 (GList *i = this->draggers; i != NULL; i = i->next) { VPDragger *dragger = (VPDragger *) i->data; 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 = g_list_append (this->draggers, 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 (GList *i = this->draggers; i != NULL; i = i->next) { for (std::list<VanishingPoint>::iterator j = ((VPDragger *) (i->data))->vps.begin(); j != ((VPDragger *) (i->data))->vps.end(); ++j) { if ((*j).get_perspective() == persp2) { (*j).set_perspective(persp1); } } } } /** Create a line from p1 to p2 and add it to the lines list */ void VPDrag::addLine (Geom::Point p1, Geom::Point p2, guint32 rgba) { SPCanvasItem *line = sp_canvas_item_new(sp_desktop_controls(inkscape_active_desktop()), SP_TYPE_CTRLLINE, NULL); sp_ctrlline_set_coords(SP_CTRLLINE(line), p1, p2); if (rgba != VP_LINE_COLOR_FILL) // fill is the default, so don't set color for it to speed up redraw sp_ctrlline_set_rgba32 (SP_CTRLLINE(line), rgba); sp_canvas_item_show (line); this->lines = g_slist_append (this->lines, line); } } // 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:encoding=utf-8:textwidth=99 :