summaryrefslogtreecommitdiffstats
path: root/src/object/persp3d.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/object/persp3d.cpp')
-rw-r--r--src/object/persp3d.cpp605
1 files changed, 605 insertions, 0 deletions
diff --git a/src/object/persp3d.cpp b/src/object/persp3d.cpp
new file mode 100644
index 0000000..58a6d8a
--- /dev/null
+++ b/src/object/persp3d.cpp
@@ -0,0 +1,605 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Class modelling a 3D perspective as an SPObject
+ *
+ * Authors:
+ * Maximilian Albert <Anhalter42@gmx.de>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2007 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "persp3d.h"
+
+#include <glibmm/i18n.h>
+
+
+#include "attributes.h"
+#include "box3d.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "perspective-line.h"
+#include "sp-defs.h"
+#include "sp-root.h"
+#include "vanishing-point.h"
+
+#include "svg/stringstream.h"
+#include "ui/icon-names.h"
+#include "ui/tools/box3d-tool.h"
+#include "util/units.h"
+#include "xml/node-event-vector.h"
+
+using Inkscape::DocumentUndo;
+
+static void persp3d_on_repr_attr_changed (Inkscape::XML::Node * repr, const gchar *key, const gchar *oldval, const gchar *newval, bool is_interactive, void * data);
+
+static int global_counter = 0;
+
+/* Constructor/destructor for the internal class */
+
+Persp3DImpl::Persp3DImpl() :
+ tmat (Proj::TransfMat3x4 ()),
+ document (nullptr)
+{
+ my_counter = global_counter++;
+}
+
+static Inkscape::XML::NodeEventVector const persp3d_repr_events = {
+ nullptr, /* child_added */
+ nullptr, /* child_removed */
+ persp3d_on_repr_attr_changed,
+ nullptr, /* content_changed */
+ nullptr /* order_changed */
+};
+
+
+Persp3D::Persp3D() : SPObject() {
+ this->perspective_impl = new Persp3DImpl();
+}
+
+Persp3D::~Persp3D() = default;
+
+
+/**
+ * Virtual build: set persp3d attributes from its associated XML node.
+ */
+void Persp3D::build(SPDocument *document, Inkscape::XML::Node *repr) {
+ SPObject::build(document, repr);
+
+ this->readAttr(SPAttr::INKSCAPE_PERSP3D_VP_X);
+ this->readAttr(SPAttr::INKSCAPE_PERSP3D_VP_Y);
+ this->readAttr(SPAttr::INKSCAPE_PERSP3D_VP_Z);
+ this->readAttr(SPAttr::INKSCAPE_PERSP3D_ORIGIN);
+
+ if (repr) {
+ repr->addListener (&persp3d_repr_events, this);
+ }
+}
+
+/**
+ * Virtual release of Persp3D members before destruction.
+ */
+void Persp3D::release() {
+ delete this->perspective_impl;
+ this->getRepr()->removeListenerByData(this);
+}
+
+/**
+ * Apply viewBox and legacy desktop transformation to point loaded from SVG
+ */
+static Proj::Pt2 legacy_transform_forward(Proj::Pt2 pt, SPDocument const *doc) {
+ // Read values are in 'user units'.
+ auto root = doc->getRoot();
+ if (root->viewBox_set) {
+ pt[0] *= root->width.computed / root->viewBox.width();
+ pt[1] *= root->height.computed / root->viewBox.height();
+ }
+
+ // <inkscape:perspective> stores inverted y-axis coordinates
+ if (doc->is_yaxisdown()) {
+ pt[1] *= -1;
+ if (pt[2]) {
+ pt[1] += doc->getHeight().value("px");
+ }
+ }
+
+ return pt;
+}
+
+/**
+ * Apply viewBox and legacy desktop transformation to point to be written to SVG
+ */
+static Proj::Pt2 legacy_transform_backward(Proj::Pt2 pt, SPDocument const *doc) {
+ // <inkscape:perspective> stores inverted y-axis coordinates
+ if (doc->is_yaxisdown()) {
+ pt[1] *= -1;
+ if (pt[2]) {
+ pt[1] += doc->getHeight().value("px");
+ }
+ }
+
+ // Written values are in 'user units'.
+ auto root = doc->getRoot();
+ if (root->viewBox_set) {
+ pt[0] *= root->viewBox.width() / root->width.computed;
+ pt[1] *= root->viewBox.height() / root->height.computed;
+ }
+
+ return pt;
+}
+
+/**
+ * Virtual set: set attribute to value.
+ */
+// FIXME: Currently we only read the finite positions of vanishing points;
+// should we move VPs into their own repr (as it's done for SPStop, e.g.)?
+void Persp3D::set(SPAttr key, gchar const *value) {
+
+ switch (key) {
+ case SPAttr::INKSCAPE_PERSP3D_VP_X: {
+ if (value) {
+ Proj::Pt2 pt (value);
+ Proj::Pt2 ptn = legacy_transform_forward(pt, document);
+ perspective_impl->tmat.set_image_pt( Proj::X, ptn );
+ }
+ break;
+ }
+ case SPAttr::INKSCAPE_PERSP3D_VP_Y: {
+ if (value) {
+ Proj::Pt2 pt (value);
+ Proj::Pt2 ptn = legacy_transform_forward(pt, document);
+ perspective_impl->tmat.set_image_pt( Proj::Y, ptn );
+ }
+ break;
+ }
+ case SPAttr::INKSCAPE_PERSP3D_VP_Z: {
+ if (value) {
+ Proj::Pt2 pt (value);
+ Proj::Pt2 ptn = legacy_transform_forward(pt, document);
+ perspective_impl->tmat.set_image_pt( Proj::Z, ptn );
+ }
+ break;
+ }
+ case SPAttr::INKSCAPE_PERSP3D_ORIGIN: {
+ if (value) {
+ Proj::Pt2 pt (value);
+ Proj::Pt2 ptn = legacy_transform_forward(pt, document);
+ perspective_impl->tmat.set_image_pt( Proj::W, ptn );
+ }
+ break;
+ }
+ default: {
+ SPObject::set(key, value);
+ break;
+ }
+ }
+
+ // FIXME: Is this the right place for resetting the draggers? PROBABLY NOT!
+ if (!SP_ACTIVE_DESKTOP) {
+ // Maybe in commandline mode.
+ return;
+ }
+
+ Inkscape::UI::Tools::ToolBase *ec = SP_ACTIVE_DESKTOP->getEventContext();
+ if (SP_IS_BOX3D_CONTEXT(ec)) {
+ Inkscape::UI::Tools::Box3dTool *bc = SP_BOX3D_CONTEXT(ec);
+ bc->_vpdrag->updateDraggers();
+ bc->_vpdrag->updateLines();
+ bc->_vpdrag->updateBoxHandles();
+ bc->_vpdrag->updateBoxReprs();
+ }
+}
+
+void Persp3D::update(SPCtx *ctx, guint flags) {
+ if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) {
+
+ /* TODO: Should we update anything here? */
+
+ }
+
+ SPObject::update(ctx, flags);
+}
+
+Persp3D *
+Persp3D::create_xml_element(SPDocument *document) {
+ SPDefs *defs = document->getDefs();
+ Inkscape::XML::Document *xml_doc = document->getReprDoc();
+ Inkscape::XML::Node *repr;
+
+ /* if no perspective is given, create a default one */
+ repr = xml_doc->createElement("inkscape:perspective");
+ repr->setAttribute("sodipodi:type", "inkscape:persp3d");
+
+ // Use 'user-units'
+ double width = document->getWidth().value("px");
+ double height = document->getHeight().value("px");
+ if( document->getRoot()->viewBox_set ) {
+ Geom::Rect vb = document->getRoot()->viewBox;
+ width = vb.width();
+ height = vb.height();
+ }
+
+ Proj::Pt2 proj_vp_x = Proj::Pt2 (0.0, height/2.0, 1.0);
+ Proj::Pt2 proj_vp_y = Proj::Pt2 (0.0, 1000.0, 0.0);
+ Proj::Pt2 proj_vp_z = Proj::Pt2 (width, height/2.0, 1.0);
+ Proj::Pt2 proj_origin = Proj::Pt2 (width/2.0, height/3.0, 1.0 );
+
+ gchar *str = nullptr;
+ str = proj_vp_x.coord_string();
+ repr->setAttribute("inkscape:vp_x", str);
+ g_free (str);
+ str = proj_vp_y.coord_string();
+ repr->setAttribute("inkscape:vp_y", str);
+ g_free (str);
+ str = proj_vp_z.coord_string();
+ repr->setAttribute("inkscape:vp_z", str);
+ g_free (str);
+ str = proj_origin.coord_string();
+ repr->setAttribute("inkscape:persp3d-origin", str);
+ g_free (str);
+
+ /* Append the new persp3d to defs */
+ defs->getRepr()->addChild(repr, nullptr);
+ Inkscape::GC::release(repr);
+
+ return reinterpret_cast<Persp3D *>( defs->get_child_by_repr(repr) );
+}
+
+Persp3D *
+Persp3D::document_first_persp(SPDocument *document)
+{
+ Persp3D *first = nullptr;
+ for (auto& child: document->getDefs()->children) {
+ if (SP_IS_PERSP3D(&child)) {
+ first = SP_PERSP3D(&child);
+ break;
+ }
+ }
+ return first;
+}
+
+/**
+ * Virtual write: write object attributes to repr.
+ */
+Inkscape::XML::Node* Persp3D::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) {
+
+ if ((flags & SP_OBJECT_WRITE_BUILD & SP_OBJECT_WRITE_EXT) && !repr) {
+ // this is where we end up when saving as plain SVG (also in other circumstances?);
+ // hence we don't set the sodipodi:type attribute
+ repr = xml_doc->createElement("inkscape:perspective");
+ }
+
+ if (flags & SP_OBJECT_WRITE_EXT) {
+ {
+ Proj::Pt2 pt = perspective_impl->tmat.column( Proj::X );
+ Inkscape::SVGOStringStream os;
+ pt = legacy_transform_backward(pt, document);
+ os << pt[0] << " : " << pt[1] << " : " << pt[2];
+ repr->setAttribute("inkscape:vp_x", os.str());
+ }
+ {
+ Proj::Pt2 pt = perspective_impl->tmat.column( Proj::Y );
+ Inkscape::SVGOStringStream os;
+ pt = legacy_transform_backward(pt, document);
+ os << pt[0] << " : " << pt[1] << " : " << pt[2];
+ repr->setAttribute("inkscape:vp_y", os.str());
+ }
+ {
+ Proj::Pt2 pt = perspective_impl->tmat.column( Proj::Z );
+ Inkscape::SVGOStringStream os;
+ pt = legacy_transform_backward(pt, document);
+ os << pt[0] << " : " << pt[1] << " : " << pt[2];
+ repr->setAttribute("inkscape:vp_z", os.str());
+ }
+ {
+ Proj::Pt2 pt = perspective_impl->tmat.column( Proj::W );
+ Inkscape::SVGOStringStream os;
+ pt = legacy_transform_backward(pt, document);
+ os << pt[0] << " : " << pt[1] << " : " << pt[2];
+ repr->setAttribute("inkscape:persp3d-origin", os.str());
+ }
+ }
+
+ SPObject::write(xml_doc, repr, flags);
+
+ return repr;
+}
+
+/* convenience wrapper around Persp3D::get_finite_dir() and Persp3D::get_infinite_dir() */
+Geom::Point
+Persp3D::get_PL_dir_from_pt (Geom::Point const &pt, Proj::Axis axis) const {
+ if (Persp3D::VP_is_finite(this->perspective_impl, axis)) {
+ return this->get_finite_dir(pt, axis);
+ } else {
+ return this->get_infinite_dir(axis);
+ }
+}
+
+Geom::Point
+Persp3D::get_finite_dir (Geom::Point const &pt, Proj::Axis axis) const {
+ Box3D::PerspectiveLine pl(pt, axis, this);
+ return pl.direction();
+}
+
+Geom::Point
+Persp3D::get_infinite_dir (Proj::Axis axis) const {
+ Proj::Pt2 vp(this->get_VP(axis));
+ if (vp[2] != 0.0) {
+ g_print ("VP should be infinite but is (%f : %f : %f)\n", vp[0], vp[1], vp[2]);
+ g_return_val_if_fail(vp[2] != 0.0, Geom::Point(0.0, 0.0));
+ }
+ return Geom::Point(vp[0], vp[1]);
+}
+
+double
+Persp3D::get_infinite_angle (Proj::Axis axis) const {
+ return this->perspective_impl->tmat.get_infinite_angle(axis);
+}
+
+bool
+Persp3D::VP_is_finite (Persp3DImpl *persp_impl, Proj::Axis axis) {
+ return persp_impl->tmat.has_finite_image(axis);
+}
+
+void
+Persp3D::toggle_VP (Proj::Axis axis, bool set_undo) {
+ this->perspective_impl->tmat.toggle_finite(axis);
+ // FIXME: Remove this repr update and rely on vp_drag_sel_modified() to do this for us
+ // On the other hand, vp_drag_sel_modified() would update all boxes;
+ // here we can confine ourselves to the boxes of this particular perspective.
+ this->update_box_reprs();
+ this->updateRepr(SP_OBJECT_WRITE_EXT);
+ if (set_undo) {
+ DocumentUndo::done(SP_ACTIVE_DESKTOP->getDocument(), _("Toggle vanishing point"), INKSCAPE_ICON("draw-cuboid"));
+ }
+}
+
+/* toggle VPs for the same axis in all perspectives of a given list */
+void
+Persp3D::toggle_VPs (std::list<Persp3D *> list, Proj::Axis axis) {
+ for (Persp3D *persp : list) {
+ persp->toggle_VP(axis, false);
+ }
+ DocumentUndo::done(SP_ACTIVE_DESKTOP->getDocument(), _("Toggle multiple vanishing points"), INKSCAPE_ICON("draw-cuboid"));
+}
+
+void
+Persp3D::set_VP_state (Proj::Axis axis, Proj::VPState state) {
+ if (Persp3D::VP_is_finite(this->perspective_impl, axis) != (state == Proj::VP_FINITE)) {
+ this->toggle_VP(axis);
+ }
+}
+
+void
+Persp3D::rotate_VP (Proj::Axis axis, double angle, bool alt_pressed) { // angle is in degrees
+ // FIXME: Most of this functionality should be moved to trans_mat_3x4.(h|cpp)
+ if (this->perspective_impl->tmat.has_finite_image(axis)) {
+ // don't rotate anything for finite VPs
+ return;
+ }
+ Proj::Pt2 v_dir_proj (this->perspective_impl->tmat.column(axis));
+ Geom::Point v_dir (v_dir_proj[0], v_dir_proj[1]);
+ double a = Geom::atan2 (v_dir) * 180/M_PI;
+ a += alt_pressed ? 0.5 * ((angle > 0 ) - (angle < 0)) : angle; // the r.h.s. yields +/-0.5 or angle
+ this->perspective_impl->tmat.set_infinite_direction (axis, a);
+
+ this->update_box_reprs ();
+ this->updateRepr(SP_OBJECT_WRITE_EXT);
+}
+
+void
+Persp3D::apply_affine_transformation (Geom::Affine const &xform) {
+ this->perspective_impl->tmat *= xform;
+ this->update_box_reprs();
+ this->updateRepr(SP_OBJECT_WRITE_EXT);
+}
+
+void
+Persp3D::add_box (SPBox3D *box) {
+ Persp3DImpl *persp_impl = this->perspective_impl;
+
+ if (!box) {
+ return;
+ }
+ if (std::find (persp_impl->boxes.begin(), persp_impl->boxes.end(), box) != persp_impl->boxes.end()) {
+ return;
+ }
+ persp_impl->boxes.push_back(box);
+}
+
+void
+Persp3D::remove_box (SPBox3D *box) {
+ Persp3DImpl *persp_impl = this->perspective_impl;
+
+ std::vector<SPBox3D *>::iterator i = std::find (persp_impl->boxes.begin(), persp_impl->boxes.end(), box);
+ if (i != persp_impl->boxes.end())
+ persp_impl->boxes.erase(i);
+}
+
+bool
+Persp3D::has_box (SPBox3D *box) const {
+ Persp3DImpl *persp_impl = this->perspective_impl;
+
+ // FIXME: For some reason, std::find() does not seem to compare pointers "correctly" (or do we need to
+ // provide a proper comparison function?), so we manually traverse the list.
+ for (auto & boxe : persp_impl->boxes) {
+ if (boxe == box) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void
+Persp3D::update_box_displays () {
+ Persp3DImpl *persp_impl = this->perspective_impl;
+
+ if (persp_impl->boxes.empty())
+ return;
+ for (auto & boxe : persp_impl->boxes) {
+ boxe->position_set();
+ }
+}
+
+void
+Persp3D::update_box_reprs () {
+ Persp3DImpl *persp_impl = this->perspective_impl;
+
+ if (!persp_impl || persp_impl->boxes.empty())
+ return;
+ for (auto & boxe : persp_impl->boxes) {
+ boxe->updateRepr(SP_OBJECT_WRITE_EXT);
+ boxe->set_z_orders();
+ }
+}
+
+void
+Persp3D::update_z_orders () {
+ Persp3DImpl *persp_impl = this->perspective_impl;
+
+ if (!persp_impl || persp_impl->boxes.empty())
+ return;
+ for (auto & boxe : persp_impl->boxes) {
+ boxe->set_z_orders();
+ }
+}
+
+// FIXME: For some reason we seem to require a vector instead of a list in Persp3D, but in vp_knot_moved_handler()
+// we need a list of boxes. If we can store a list in Persp3D right from the start, this function becomes
+// obsolete. We should do this.
+std::list<SPBox3D *>
+Persp3D::list_of_boxes() const {
+ Persp3DImpl *persp_impl = this->perspective_impl;
+
+ std::list<SPBox3D *> bx_lst;
+ for (auto & boxe : persp_impl->boxes) {
+ bx_lst.push_back(boxe);
+ }
+ return bx_lst;
+}
+
+bool
+Persp3D::perspectives_coincide(const Persp3D *other) const
+{
+ return this->perspective_impl->tmat == other->perspective_impl->tmat;
+}
+
+void
+Persp3D::absorb(Persp3D *other) {
+ /* double check if we are called in sane situations */
+ g_return_if_fail (this->perspectives_coincide(other) && this != other);
+
+ // Note: We first need to copy the boxes of other into a separate list;
+ // otherwise the loop below gets confused when perspectives are reattached.
+ std::list<SPBox3D *> boxes_of_persp2 = other->list_of_boxes();
+
+ for (auto & box : boxes_of_persp2) {
+ box->switch_perspectives(other, this, true);
+ box->updateRepr(SP_OBJECT_WRITE_EXT); // so that undo/redo can do its job properly
+ }
+}
+
+static void
+persp3d_on_repr_attr_changed ( Inkscape::XML::Node * /*repr*/,
+ const gchar */*key*/,
+ const gchar */*oldval*/,
+ const gchar */*newval*/,
+ bool /*is_interactive*/,
+ void * data )
+{
+ if (!data)
+ return;
+
+ Persp3D *persp = (Persp3D*) data;
+ persp->update_box_displays ();
+}
+
+/* checks whether all boxes linked to this perspective are currently selected */
+bool
+Persp3D::has_all_boxes_in_selection (Inkscape::ObjectSet *set) const {
+ Persp3DImpl *persp_impl = this->perspective_impl;
+
+ std::list<SPBox3D *> selboxes = set->box3DList();
+
+ for (auto & boxe : persp_impl->boxes) {
+ if (std::find(selboxes.begin(), selboxes.end(), boxe) == selboxes.end()) {
+ // we have an unselected box in the perspective
+ return false;
+ }
+ }
+ return true;
+}
+
+/* some debugging stuff follows */
+
+void
+Persp3D::print_debugging_info () const {
+ Persp3DImpl *persp_impl = this->perspective_impl;
+ g_print ("=== Info for Persp3D %d ===\n", persp_impl->my_counter);
+ gchar * cstr;
+ for (auto & axe : Proj::axes) {
+ cstr = this->get_VP(axe).coord_string();
+ g_print (" VP %s: %s\n", Proj::string_from_axis(axe), cstr);
+ g_free(cstr);
+ }
+ cstr = this->get_VP(Proj::W).coord_string();
+ g_print (" Origin: %s\n", cstr);
+ g_free(cstr);
+
+ g_print (" Boxes: ");
+ for (auto & boxe : persp_impl->boxes) {
+ g_print ("%d (%d) ", boxe->my_counter, boxe->get_perspective()->perspective_impl->my_counter);
+ }
+ g_print ("\n");
+ g_print ("========================\n");
+}
+
+void
+Persp3D::print_debugging_info_all(SPDocument *document)
+{
+ for (auto& child: document->getDefs()->children) {
+ if (SP_IS_PERSP3D(&child)) {
+ SP_PERSP3D(&child)->print_debugging_info();
+ }
+ }
+ Persp3D::print_all_selected();
+}
+
+void
+Persp3D::print_all_selected() {
+ g_print ("\n======================================\n");
+ g_print ("Selected perspectives and their boxes:\n");
+
+ std::list<Persp3D *> sel_persps = SP_ACTIVE_DESKTOP->getSelection()->perspList();
+
+ for (auto & sel_persp : sel_persps) {
+ Persp3D *persp = SP_PERSP3D(sel_persp);
+ Persp3DImpl *persp_impl = persp->perspective_impl;
+ g_print (" %s (%d): ", persp->getRepr()->attribute("id"), persp->perspective_impl->my_counter);
+ for (auto & boxe : persp_impl->boxes) {
+ g_print ("%d ", boxe->my_counter);
+ }
+ g_print ("\n");
+ }
+ g_print ("======================================\n\n");
+ }
+
+void print_current_persp3d(gchar *func_name, Persp3D *persp) {
+ g_print ("%s: current_persp3d is now %s\n",
+ func_name,
+ persp ? persp->getRepr()->attribute("id") : "NULL");
+}
+
+/*
+ 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 :