From 855eb0a1505d99ff262b657690237f9d362e7a0c Mon Sep 17 00:00:00 2001 From: Ell Date: Mon, 6 Jan 2020 16:39:56 +0200 Subject: [PATCH] app, cursors: add GimpToolTransform3DGrid tool widget Add a new GimpToolTransform3DGrid tool widget, subclassed from GimpToolTransformGrid, which can be used to perform 3D transformations. The widget can be in one of three modes: CAMERA - allows adjusting the primary vanishing point by moving a handle. MOVE - allows moving the object through dragging. ROTATE - allows rotating the object through dragging. By default, controlling the transformation through dragging applies to the X and Y axes. Holding Shift (or setting the "constrain- axis" property) restricts the motion to only one of the axes. For the MOVE and ROTATE mode, holding Ctrl (or setting the "z-axis" property) allows controlling the Z axis instead. For the same modes, holding Alt (or setting the "local-frame" property), applies the adjustments in the object's local frame of reference, instead of the display's global frame of reference. --- app/display/Makefile.am | 2 + app/display/display-enums.c | 31 + app/display/display-enums.h | 12 + app/display/gimptooltransform3dgrid.c | 1110 +++++++++++++++++++++++ app/display/gimptooltransform3dgrid.h | 65 ++ app/widgets/gimpcursor.c | 1 + app/widgets/widgets-enums.h | 1 + cursors/Makefile.am | 2 + cursors/gimp-tool-cursors-x2.xcf | Bin 167642 -> 172862 bytes cursors/gimp-tool-cursors.xcf | Bin 78349 -> 81587 bytes cursors/tool-transform-3d-camera-x2.png | Bin 0 -> 396 bytes cursors/tool-transform-3d-camera.png | Bin 0 -> 333 bytes po/POTFILES.in | 1 + 13 files changed, 1225 insertions(+) create mode 100644 app/display/gimptooltransform3dgrid.c create mode 100644 app/display/gimptooltransform3dgrid.h create mode 100644 cursors/tool-transform-3d-camera-x2.png create mode 100644 cursors/tool-transform-3d-camera.png diff --git a/app/display/Makefile.am b/app/display/Makefile.am index f5585917bf..0f20ca7ec5 100644 --- a/app/display/Makefile.am +++ b/app/display/Makefile.am @@ -196,6 +196,8 @@ libappdisplay_a_sources = \ gimptoolrotategrid.h \ gimptoolsheargrid.c \ gimptoolsheargrid.h \ + gimptooltransform3dgrid.c \ + gimptooltransform3dgrid.h \ gimptooltransformgrid.c \ gimptooltransformgrid.h \ gimptoolwidget.c \ diff --git a/app/display/display-enums.c b/app/display/display-enums.c index ec3d941791..47e94251be 100644 --- a/app/display/display-enums.c +++ b/app/display/display-enums.c @@ -389,6 +389,37 @@ gimp_rectangle_precision_get_type (void) return type; } +GType +gimp_transform_3d_mode_get_type (void) +{ + static const GEnumValue values[] = + { + { GIMP_TRANSFORM_3D_MODE_CAMERA, "GIMP_TRANSFORM_3D_MODE_CAMERA", "camera" }, + { GIMP_TRANSFORM_3D_MODE_MOVE, "GIMP_TRANSFORM_3D_MODE_MOVE", "move" }, + { GIMP_TRANSFORM_3D_MODE_ROTATE, "GIMP_TRANSFORM_3D_MODE_ROTATE", "rotate" }, + { 0, NULL, NULL } + }; + + static const GimpEnumDesc descs[] = + { + { GIMP_TRANSFORM_3D_MODE_CAMERA, "GIMP_TRANSFORM_3D_MODE_CAMERA", NULL }, + { GIMP_TRANSFORM_3D_MODE_MOVE, "GIMP_TRANSFORM_3D_MODE_MOVE", NULL }, + { GIMP_TRANSFORM_3D_MODE_ROTATE, "GIMP_TRANSFORM_3D_MODE_ROTATE", NULL }, + { 0, NULL, NULL } + }; + + static GType type = 0; + + if (G_UNLIKELY (! type)) + { + type = g_enum_register_static ("GimpTransform3DMode", values); + gimp_type_set_translation_context (type, "transform3-dmode"); + gimp_enum_set_value_descriptions (type, descs); + } + + return type; +} + GType gimp_transform_function_get_type (void) { diff --git a/app/display/display-enums.h b/app/display/display-enums.h index 72456cb82a..b805952b35 100644 --- a/app/display/display-enums.h +++ b/app/display/display-enums.h @@ -171,6 +171,18 @@ typedef enum } GimpRectanglePrecision; +#define GIMP_TYPE_TRANSFORM_3D_MODE (gimp_transform_3d_mode_get_type ()) + +GType gimp_transform_3d_mode_get_type (void) G_GNUC_CONST; + +typedef enum /*< lowercase_name=gimp_transform_3d_mode >*/ +{ + GIMP_TRANSFORM_3D_MODE_CAMERA, + GIMP_TRANSFORM_3D_MODE_MOVE, + GIMP_TRANSFORM_3D_MODE_ROTATE +} GimpTransform3DMode; + + #define GIMP_TYPE_TRANSFORM_FUNCTION (gimp_transform_function_get_type ()) GType gimp_transform_function_get_type (void) G_GNUC_CONST; diff --git a/app/display/gimptooltransform3dgrid.c b/app/display/gimptooltransform3dgrid.c new file mode 100644 index 0000000000..41a10b9012 --- /dev/null +++ b/app/display/gimptooltransform3dgrid.c @@ -0,0 +1,1110 @@ +/* GIMP - The GNU Image Manipulation Program + * Copyright (C) 1995 Spencer Kimball and Peter Mattis + * + * gimptool3dtransformgrid.c + * Copyright (C) 2019 Ell + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "config.h" + +#include +#include + +#include "libgimpbase/gimpbase.h" +#include "libgimpmath/gimpmath.h" + +#include "display-types.h" + +#include "widgets/gimpwidgets-utils.h" + +#include "core/gimp-transform-3d-utils.h" +#include "core/gimp-utils.h" + +#include "gimpdisplayshell.h" +#include "gimpdisplayshell-transform.h" +#include "gimptooltransform3dgrid.h" + +#include "gimp-intl.h" + + +#define CONSTRAINT_MIN_DIST 8.0 +#define PIXELS_PER_REVOLUTION 1000 + + +enum +{ + PROP_0, + PROP_MODE, + PROP_CONSTRAIN_AXIS, + PROP_Z_AXIS, + PROP_LOCAL_FRAME, + PROP_CAMERA_X, + PROP_CAMERA_Y, + PROP_CAMERA_Z, + PROP_OFFSET_X, + PROP_OFFSET_Y, + PROP_OFFSET_Z, + PROP_ROTATION_ORDER, + PROP_ANGLE_X, + PROP_ANGLE_Y, + PROP_ANGLE_Z, + PROP_PIVOT_3D_X, + PROP_PIVOT_3D_Y, + PROP_PIVOT_3D_Z +}; + +typedef enum +{ + AXIS_NONE, + AXIS_X, + AXIS_Y +} Axis; + +struct _GimpToolTransform3DGridPrivate +{ + GimpTransform3DMode mode; + + gboolean constrain_axis; + gboolean z_axis; + gboolean local_frame; + + gdouble camera_x; + gdouble camera_y; + gdouble camera_z; + + gdouble offset_x; + gdouble offset_y; + gdouble offset_z; + + gint rotation_order; + gdouble angle_x; + gdouble angle_y; + gdouble angle_z; + + gdouble pivot_x; + gdouble pivot_y; + gdouble pivot_z; + + GimpTransformHandle handle; + + gdouble orig_x; + gdouble orig_y; + gdouble orig_offset_x; + gdouble orig_offset_y; + gdouble orig_offset_z; + GimpMatrix3 orig_transform; + + gdouble last_x; + gdouble last_y; + + Axis constrained_axis; +}; + + +/* local function prototypes */ + +static void gimp_tool_transform_3d_grid_constructed (GObject *object); +static void gimp_tool_transform_3d_grid_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec); +static void gimp_tool_transform_3d_grid_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec); + +static gint gimp_tool_transform_3d_grid_button_press (GimpToolWidget *widget, + const GimpCoords *coords, + guint32 time, + GdkModifierType state, + GimpButtonPressType press_type); +static void gimp_tool_transform_3d_grid_motion (GimpToolWidget *widget, + const GimpCoords *coords, + guint32 time, + GdkModifierType state); +static void gimp_tool_transform_3d_grid_hover (GimpToolWidget *widget, + const GimpCoords *coords, + GdkModifierType state, + gboolean proximity); +static void gimp_tool_transform_3d_grid_hover_modifier (GimpToolWidget *widget, + GdkModifierType key, + gboolean press, + GdkModifierType state); +static gboolean gimp_tool_transform_3d_grid_get_cursor (GimpToolWidget *widget, + const GimpCoords *coords, + GdkModifierType state, + GimpCursorType *cursor, + GimpToolCursorType *tool_cursor, + GimpCursorModifier *modifier); + +static void gimp_tool_transform_3d_grid_set_mode (GimpToolTransform3DGrid *grid, + GimpTransform3DMode mode); +static void gimp_tool_transform_3d_grid_reset_motion (GimpToolTransform3DGrid *grid); +static gboolean gimp_tool_transform_3d_grid_constrain (GimpToolTransform3DGrid *grid, + gdouble x, + gdouble y, + gdouble ox, + gdouble oy, + gdouble *tx, + gdouble *ty); + +static gboolean gimp_tool_transform_3d_grid_motion_vanishing_point (GimpToolTransform3DGrid *grid, + gdouble x, + gdouble y); +static gboolean gimp_tool_transform_3d_grid_motion_move (GimpToolTransform3DGrid *grid, + gdouble x, + gdouble y); +static gboolean gimp_tool_transform_3d_grid_motion_rotate (GimpToolTransform3DGrid *grid, + gdouble x, + gdouble y); + + +G_DEFINE_TYPE_WITH_PRIVATE (GimpToolTransform3DGrid, gimp_tool_transform_3d_grid, + GIMP_TYPE_TOOL_TRANSFORM_GRID) + +#define parent_class gimp_tool_transform_3d_grid_parent_class + + +static void +gimp_tool_transform_3d_grid_class_init (GimpToolTransform3DGridClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GimpToolWidgetClass *widget_class = GIMP_TOOL_WIDGET_CLASS (klass); + + object_class->constructed = gimp_tool_transform_3d_grid_constructed; + object_class->set_property = gimp_tool_transform_3d_grid_set_property; + object_class->get_property = gimp_tool_transform_3d_grid_get_property; + + widget_class->button_press = gimp_tool_transform_3d_grid_button_press; + widget_class->motion = gimp_tool_transform_3d_grid_motion; + widget_class->hover = gimp_tool_transform_3d_grid_hover; + widget_class->hover_modifier = gimp_tool_transform_3d_grid_hover_modifier; + widget_class->get_cursor = gimp_tool_transform_3d_grid_get_cursor; + + g_object_class_install_property (object_class, PROP_MODE, + g_param_spec_enum ("mode", + NULL, NULL, + GIMP_TYPE_TRANSFORM_3D_MODE, + GIMP_TRANSFORM_3D_MODE_CAMERA, + GIMP_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); + + g_object_class_install_property (object_class, PROP_CONSTRAIN_AXIS, + g_param_spec_boolean ("constrain-axis", + NULL, NULL, + FALSE, + GIMP_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); + + g_object_class_install_property (object_class, PROP_Z_AXIS, + g_param_spec_boolean ("z-axis", + NULL, NULL, + FALSE, + GIMP_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); + + g_object_class_install_property (object_class, PROP_LOCAL_FRAME, + g_param_spec_boolean ("local-frame", + NULL, NULL, + FALSE, + GIMP_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); + + g_object_class_install_property (object_class, PROP_CAMERA_X, + g_param_spec_double ("camera-x", + NULL, NULL, + -G_MAXDOUBLE, + G_MAXDOUBLE, + 0.0, + GIMP_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); + + g_object_class_install_property (object_class, PROP_CAMERA_Y, + g_param_spec_double ("camera-y", + NULL, NULL, + -G_MAXDOUBLE, + G_MAXDOUBLE, + 0.0, + GIMP_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); + + g_object_class_install_property (object_class, PROP_CAMERA_Z, + g_param_spec_double ("camera-z", + NULL, NULL, + -(1.0 / 0.0), + 1.0 / 0.0, + 0.0, + GIMP_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); + + g_object_class_install_property (object_class, PROP_OFFSET_X, + g_param_spec_double ("offset-x", + NULL, NULL, + -G_MAXDOUBLE, + G_MAXDOUBLE, + 0.0, + GIMP_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); + + g_object_class_install_property (object_class, PROP_OFFSET_Y, + g_param_spec_double ("offset-y", + NULL, NULL, + -G_MAXDOUBLE, + G_MAXDOUBLE, + 0.0, + GIMP_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); + + g_object_class_install_property (object_class, PROP_OFFSET_Z, + g_param_spec_double ("offset-z", + NULL, NULL, + -G_MAXDOUBLE, + G_MAXDOUBLE, + 0.0, + GIMP_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); + + g_object_class_install_property (object_class, PROP_ROTATION_ORDER, + g_param_spec_int ("rotation-order", + NULL, NULL, + 0, 6, 0, + GIMP_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); + + g_object_class_install_property (object_class, PROP_ANGLE_X, + g_param_spec_double ("angle-x", + NULL, NULL, + -G_MAXDOUBLE, + G_MAXDOUBLE, + 0.0, + GIMP_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); + + g_object_class_install_property (object_class, PROP_ANGLE_Y, + g_param_spec_double ("angle-y", + NULL, NULL, + -G_MAXDOUBLE, + G_MAXDOUBLE, + 0.0, + GIMP_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); + + g_object_class_install_property (object_class, PROP_ANGLE_Z, + g_param_spec_double ("angle-z", + NULL, NULL, + -G_MAXDOUBLE, + G_MAXDOUBLE, + 0.0, + GIMP_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); + + g_object_class_install_property (object_class, PROP_PIVOT_3D_X, + g_param_spec_double ("pivot-3d-x", + NULL, NULL, + -G_MAXDOUBLE, + G_MAXDOUBLE, + 0.0, + GIMP_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); + + g_object_class_install_property (object_class, PROP_PIVOT_3D_Y, + g_param_spec_double ("pivot-3d-y", + NULL, NULL, + -G_MAXDOUBLE, + G_MAXDOUBLE, + 0.0, + GIMP_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); + + g_object_class_install_property (object_class, PROP_PIVOT_3D_Z, + g_param_spec_double ("pivot-3d-z", + NULL, NULL, + -G_MAXDOUBLE, + G_MAXDOUBLE, + 0.0, + GIMP_PARAM_READWRITE | + G_PARAM_CONSTRUCT)); +} + +static void +gimp_tool_transform_3d_grid_init (GimpToolTransform3DGrid *grid) +{ + grid->priv = gimp_tool_transform_3d_grid_get_instance_private (grid); +} + +static void +gimp_tool_transform_3d_grid_constructed (GObject *object) +{ + G_OBJECT_CLASS (parent_class)->constructed (object); + + g_object_set (object, + "clip-guides", TRUE, + "dynamic-handle-size", FALSE, + NULL); +} + +static void +gimp_tool_transform_3d_grid_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + GimpToolTransform3DGrid *grid = GIMP_TOOL_TRANSFORM_3D_GRID (object); + GimpToolTransform3DGridPrivate *priv = grid->priv; + + switch (property_id) + { + case PROP_MODE: + gimp_tool_transform_3d_grid_set_mode (grid, g_value_get_enum (value)); + break; + + case PROP_CONSTRAIN_AXIS: + priv->constrain_axis = g_value_get_boolean (value); + gimp_tool_transform_3d_grid_reset_motion (grid); + break; + case PROP_Z_AXIS: + priv->z_axis = g_value_get_boolean (value); + gimp_tool_transform_3d_grid_reset_motion (grid); + break; + case PROP_LOCAL_FRAME: + priv->local_frame = g_value_get_boolean (value); + gimp_tool_transform_3d_grid_reset_motion (grid); + break; + + case PROP_CAMERA_X: + priv->camera_x = g_value_get_double (value); + g_object_set (grid, + "pivot-x", priv->camera_x, + NULL); + break; + case PROP_CAMERA_Y: + priv->camera_y = g_value_get_double (value); + g_object_set (grid, + "pivot-y", priv->camera_y, + NULL); + break; + case PROP_CAMERA_Z: + priv->camera_z = g_value_get_double (value); + break; + + case PROP_OFFSET_X: + priv->offset_x = g_value_get_double (value); + break; + case PROP_OFFSET_Y: + priv->offset_y = g_value_get_double (value); + break; + case PROP_OFFSET_Z: + priv->offset_z = g_value_get_double (value); + break; + + case PROP_ROTATION_ORDER: + priv->rotation_order = g_value_get_int (value); + break; + case PROP_ANGLE_X: + priv->angle_x = g_value_get_double (value); + break; + case PROP_ANGLE_Y: + priv->angle_y = g_value_get_double (value); + break; + case PROP_ANGLE_Z: + priv->angle_z = g_value_get_double (value); + break; + + case PROP_PIVOT_3D_X: + priv->pivot_x = g_value_get_double (value); + break; + case PROP_PIVOT_3D_Y: + priv->pivot_y = g_value_get_double (value); + break; + case PROP_PIVOT_3D_Z: + priv->pivot_z = g_value_get_double (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +gimp_tool_transform_3d_grid_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + GimpToolTransform3DGrid *grid = GIMP_TOOL_TRANSFORM_3D_GRID (object); + GimpToolTransform3DGridPrivate *priv = grid->priv; + + switch (property_id) + { + case PROP_MODE: + g_value_set_enum (value, priv->mode); + break; + + case PROP_CONSTRAIN_AXIS: + g_value_set_boolean (value, priv->constrain_axis); + break; + case PROP_Z_AXIS: + g_value_set_boolean (value, priv->z_axis); + break; + case PROP_LOCAL_FRAME: + g_value_set_boolean (value, priv->local_frame); + break; + + case PROP_CAMERA_X: + g_value_set_double (value, priv->camera_x); + break; + case PROP_CAMERA_Y: + g_value_set_double (value, priv->camera_y); + break; + case PROP_CAMERA_Z: + g_value_set_double (value, priv->camera_z); + break; + + case PROP_OFFSET_X: + g_value_set_double (value, priv->offset_x); + break; + case PROP_OFFSET_Y: + g_value_set_double (value, priv->offset_y); + break; + case PROP_OFFSET_Z: + g_value_set_double (value, priv->offset_z); + break; + + case PROP_ROTATION_ORDER: + g_value_set_int (value, priv->rotation_order); + break; + case PROP_ANGLE_X: + g_value_set_double (value, priv->angle_x); + break; + case PROP_ANGLE_Y: + g_value_set_double (value, priv->angle_y); + break; + case PROP_ANGLE_Z: + g_value_set_double (value, priv->angle_z); + break; + + case PROP_PIVOT_3D_X: + g_value_set_double (value, priv->pivot_x); + break; + case PROP_PIVOT_3D_Y: + g_value_set_double (value, priv->pivot_y); + break; + case PROP_PIVOT_3D_Z: + g_value_set_double (value, priv->pivot_z); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static gint +gimp_tool_transform_3d_grid_button_press (GimpToolWidget *widget, + const GimpCoords *coords, + guint32 time, + GdkModifierType state, + GimpButtonPressType press_type) +{ + GimpToolTransform3DGrid *grid = GIMP_TOOL_TRANSFORM_3D_GRID (widget); + GimpToolTransform3DGridPrivate *priv = grid->priv; + + priv->handle = GIMP_TOOL_WIDGET_CLASS (parent_class)->button_press ( + widget, coords, time, state, press_type); + + priv->orig_x = coords->x; + priv->orig_y = coords->y; + priv->orig_offset_x = priv->offset_x; + priv->orig_offset_y = priv->offset_y; + priv->orig_offset_z = priv->offset_z; + priv->last_x = coords->x; + priv->last_y = coords->y; + + gimp_tool_transform_3d_grid_reset_motion (grid); + + return priv->handle; +} + +void +gimp_tool_transform_3d_grid_motion (GimpToolWidget *widget, + const GimpCoords *coords, + guint32 time, + GdkModifierType state) +{ + GimpToolTransform3DGrid *grid = GIMP_TOOL_TRANSFORM_3D_GRID (widget); + GimpToolTransform3DGridPrivate *priv = grid->priv; + GimpMatrix3 transform; + gboolean update = TRUE; + + switch (priv->handle) + { + case GIMP_TRANSFORM_HANDLE_PIVOT: + update = gimp_tool_transform_3d_grid_motion_vanishing_point ( + grid, coords->x, coords->y); + break; + + case GIMP_TRANSFORM_HANDLE_CENTER: + update = gimp_tool_transform_3d_grid_motion_move ( + grid, coords->x, coords->y); + break; + + case GIMP_TRANSFORM_HANDLE_ROTATION: + update = gimp_tool_transform_3d_grid_motion_rotate ( + grid, coords->x, coords->y); + break; + + default: + g_return_if_reached (); + } + + if (update) + { + gimp_transform_3d_matrix (&transform, + + priv->camera_x, + priv->camera_y, + priv->camera_z, + + priv->offset_x, + priv->offset_y, + priv->offset_z, + + priv->rotation_order, + priv->angle_x, + priv->angle_y, + priv->angle_z, + + priv->pivot_x, + priv->pivot_y, + priv->pivot_z); + + g_object_set (widget, + "transform", &transform, + NULL); + } +} + +static void +gimp_tool_transform_3d_grid_hover (GimpToolWidget *widget, + const GimpCoords *coords, + GdkModifierType state, + gboolean proximity) +{ + GIMP_TOOL_WIDGET_CLASS (parent_class)->hover (widget, + coords, state, proximity); + + if (proximity && + gimp_tool_transform_grid_get_handle (GIMP_TOOL_TRANSFORM_GRID (widget)) == + GIMP_TRANSFORM_HANDLE_PIVOT) + { + gimp_tool_widget_set_status (widget, + _("Click-Drag to move the vanishing point")); + } +} + +static void +gimp_tool_transform_3d_grid_hover_modifier (GimpToolWidget *widget, + GdkModifierType key, + gboolean press, + GdkModifierType state) +{ + GimpToolTransform3DGrid *grid = GIMP_TOOL_TRANSFORM_3D_GRID (widget); + GimpToolTransform3DGridPrivate *priv = grid->priv; + + GIMP_TOOL_WIDGET_CLASS (parent_class)->hover_modifier (widget, + key, press, state); + + priv->local_frame = (state & gimp_get_extend_selection_mask ()) != 0; +} + +static gboolean +gimp_tool_transform_3d_grid_get_cursor (GimpToolWidget *widget, + const GimpCoords *coords, + GdkModifierType state, + GimpCursorType *cursor, + GimpToolCursorType *tool_cursor, + GimpCursorModifier *modifier) +{ + if (! GIMP_TOOL_WIDGET_CLASS (parent_class)->get_cursor (widget, + coords, + state, + cursor, + tool_cursor, + modifier)) + { + return FALSE; + } + + if (gimp_tool_transform_grid_get_handle (GIMP_TOOL_TRANSFORM_GRID (widget)) == + GIMP_TRANSFORM_HANDLE_PIVOT) + { + *tool_cursor = GIMP_TOOL_CURSOR_TRANSFORM_3D_CAMERA; + } + + return TRUE; +} + +static void +gimp_tool_transform_3d_grid_set_mode (GimpToolTransform3DGrid *grid, + GimpTransform3DMode mode) +{ + GimpToolTransform3DGridPrivate *priv = grid->priv; + + priv->mode = mode; + + switch (mode) + { + case GIMP_TRANSFORM_3D_MODE_CAMERA: + g_object_set (grid, + "inside-function", GIMP_TRANSFORM_FUNCTION_NONE, + "outside-function", GIMP_TRANSFORM_FUNCTION_NONE, + "use-pivot-handle", TRUE, + NULL); + break; + + case GIMP_TRANSFORM_3D_MODE_MOVE: + g_object_set (grid, + "inside-function", GIMP_TRANSFORM_FUNCTION_MOVE, + "outside-function", GIMP_TRANSFORM_FUNCTION_MOVE, + "use-pivot-handle", FALSE, + NULL); + break; + + case GIMP_TRANSFORM_3D_MODE_ROTATE: + g_object_set (grid, + "inside-function", GIMP_TRANSFORM_FUNCTION_ROTATE, + "outside-function", GIMP_TRANSFORM_FUNCTION_ROTATE, + "use-pivot-handle", FALSE, + NULL); + break; + } +} + +static void +gimp_tool_transform_3d_grid_reset_motion (GimpToolTransform3DGrid *grid) +{ + GimpToolTransform3DGridPrivate *priv = grid->priv; + GimpMatrix3 *transform; + + priv->constrained_axis = AXIS_NONE; + + g_object_get (grid, + "transform", &transform, + NULL); + + priv->orig_transform = *transform; + + g_free (transform); +} + +static gboolean +gimp_tool_transform_3d_grid_constrain (GimpToolTransform3DGrid *grid, + gdouble x, + gdouble y, + gdouble ox, + gdouble oy, + gdouble *tx, + gdouble *ty) +{ + GimpToolTransform3DGridPrivate *priv = grid->priv; + + if (! priv->constrain_axis) + return TRUE; + + if (priv->constrained_axis == AXIS_NONE) + { + GimpDisplayShell *shell; + gdouble x1, y1; + gdouble x2, y2; + + shell = gimp_tool_widget_get_shell (GIMP_TOOL_WIDGET (grid)); + + gimp_display_shell_transform_xy_f (shell, + priv->last_x, priv->last_y, + &x1, &y1); + gimp_display_shell_transform_xy_f (shell, + x, y, + &x2, &y2); + + if (hypot (x2 - x1, y2 - y1) < CONSTRAINT_MIN_DIST) + return FALSE; + + if (fabs (*tx - ox) >= fabs (*ty - oy)) + priv->constrained_axis = AXIS_X; + else + priv->constrained_axis = AXIS_Y; + } + + if (priv->constrained_axis == AXIS_X) + *ty = oy; + else + *tx = ox; + + return TRUE; +} + +static gboolean +gimp_tool_transform_3d_grid_motion_vanishing_point (GimpToolTransform3DGrid *grid, + gdouble x, + gdouble y) +{ + GimpToolTransform3DGridPrivate *priv = grid->priv; + GimpCoords c = {}; + gdouble pivot_x; + gdouble pivot_y; + + if (! gimp_tool_transform_3d_grid_constrain (grid, + x, y, + priv->last_x, priv->last_y, + &x, &y)) + { + return FALSE; + } + + c.x = x; + c.y = y; + + GIMP_TOOL_WIDGET_CLASS (parent_class)->motion (GIMP_TOOL_WIDGET (grid), + &c, 0, 0); + + g_object_get (grid, + "pivot-x", &pivot_x, + "pivot-y", &pivot_y, + NULL); + + g_object_set (grid, + "camera-x", pivot_x, + "camera-y", pivot_y, + NULL); + + priv->last_x = c.x; + priv->last_y = c.y; + + return TRUE; +} + +static gboolean +gimp_tool_transform_3d_grid_motion_move (GimpToolTransform3DGrid *grid, + gdouble x, + gdouble y) +{ + GimpToolTransform3DGridPrivate *priv = grid->priv; + GimpMatrix4 matrix; + + if (! priv->z_axis) + { + gdouble x1, y1, z1, w1; + gdouble x2, y2, z2, w2; + + if (! priv->local_frame) + { + gimp_matrix4_identity (&matrix); + } + else + { + GimpMatrix3 transform_inv = priv->orig_transform;; + + gimp_matrix3_invert (&transform_inv); + + gimp_transform_3d_matrix3_to_matrix4 (&transform_inv, &matrix, 2); + } + + w1 = gimp_matrix4_transform_point (&matrix, + priv->last_x, priv->last_y, 0.0, + &x1, &y1, &z1); + w2 = gimp_matrix4_transform_point (&matrix, + x, y, 0.0, + &x2, &y2, &z2); + + if (w1 <= 0.0) + return FALSE; + + if (! gimp_tool_transform_3d_grid_constrain (grid, + x, y, + x1, y1, + &x2, &y2)) + { + return FALSE; + } + + if (priv->local_frame) + { + gimp_matrix4_identity (&matrix); + + gimp_transform_3d_matrix4_rotate_euler (&matrix, + priv->rotation_order, + priv->angle_x, + priv->angle_y, + priv->angle_z, + 0.0, 0.0, 0.0); + + gimp_matrix4_transform_point (&matrix, + x1, y1, z1, + &x1, &y1, &z1); + gimp_matrix4_transform_point (&matrix, + x2, y2, z2, + &x2, &y2, &z2); + } + + if (w2 > 0.0) + { + g_object_set (grid, + "offset-x", priv->offset_x + (x2 - x1), + "offset-y", priv->offset_y + (y2 - y1), + "offset-z", priv->offset_z + (z2 - z1), + NULL); + + priv->last_x = x; + priv->last_y = y; + } + else + { + g_object_set (grid, + "offset-x", priv->orig_offset_x, + "offset-y", priv->orig_offset_y, + "offset-z", priv->orig_offset_z, + NULL); + + priv->last_x = priv->orig_x; + priv->last_y = priv->orig_y; + } + } + else + { + GimpVector3 axis; + gdouble amount; + + if (! priv->local_frame) + { + axis.x = 0.0; + axis.y = 0.0; + axis.z = 1.0; + } + else + { + gimp_matrix4_identity (&matrix); + + gimp_transform_3d_matrix4_rotate_euler (&matrix, + priv->rotation_order, + priv->angle_x, + priv->angle_y, + priv->angle_z, + 0.0, 0.0, 0.0); + + axis.x = matrix.coeff[0][2]; + axis.y = matrix.coeff[1][2]; + axis.z = matrix.coeff[2][2]; + + if (axis.x < 0.0) + gimp_vector3_neg (&axis); + } + + amount = x - priv->last_x; + + g_object_set (grid, + "offset-x", priv->offset_x + axis.x * amount, + "offset-y", priv->offset_y + axis.y * amount, + "offset-z", priv->offset_z + axis.z * amount, + NULL); + + priv->last_x = x; + priv->last_y = y; + } + + return TRUE; +} + +static gboolean +gimp_tool_transform_3d_grid_motion_rotate (GimpToolTransform3DGrid *grid, + gdouble x, + gdouble y) +{ + GimpToolTransform3DGridPrivate *priv = grid->priv; + GimpDisplayShell *shell; + GimpMatrix4 matrix; + GimpMatrix2 basis_inv; + GimpVector3 omega; + gdouble z_sign; + gboolean local_frame; + + shell = gimp_tool_widget_get_shell (GIMP_TOOL_WIDGET (grid)); + + local_frame = priv->local_frame && (priv->constrain_axis || priv->z_axis); + + if (! local_frame) + { + gimp_matrix2_identity (&basis_inv); + z_sign = 1.0; + } + else + { + GimpVector2 o, u, v; + + gimp_matrix4_identity (&matrix); + + gimp_transform_3d_matrix4_rotate_euler (&matrix, + priv->rotation_order, + priv->angle_x, + priv->angle_y, + priv->angle_z, + 0.0, 0.0, 0.0); + + z_sign = matrix.coeff[2][2] >= 0.0 ? +1.0 : -1.0; + + gimp_matrix3_transform_point (&priv->orig_transform, + priv->pivot_x, priv->pivot_y, + &o.x, &o.y); + gimp_matrix3_transform_point (&priv->orig_transform, + priv->pivot_x + 1.0, priv->pivot_y, + &u.x, &u.y); + gimp_matrix3_transform_point (&priv->orig_transform, + priv->pivot_x, priv->pivot_y + 1.0, + &v.x, &v.y); + + gimp_vector2_sub (&u, &u, &o); + gimp_vector2_sub (&v, &v, &o); + + gimp_vector2_normalize (&u); + gimp_vector2_normalize (&v); + + basis_inv.coeff[0][0] = u.x; + basis_inv.coeff[1][0] = u.y; + basis_inv.coeff[0][1] = v.x; + basis_inv.coeff[1][1] = v.y; + + gimp_matrix2_invert (&basis_inv); + } + + if (! priv->z_axis) + { + GimpVector2 scale; + gdouble norm; + + gimp_matrix2_transform_point (&basis_inv, + -(y - priv->last_y), + x - priv->last_x, + &omega.x, &omega.y); + + omega.z = 0.0; + + if (! gimp_tool_transform_3d_grid_constrain (grid, + x, y, + 0.0, 0.0, + &omega.x, &omega.y)) + { + return FALSE; + } + + norm = gimp_vector3_length (&omega); + + if (norm > 0.0) + { + scale.x = shell->scale_x * omega.y / norm; + scale.y = shell->scale_y * omega.x / norm; + + gimp_vector3_mul (&omega, gimp_vector2_length (&scale)); + gimp_vector3_mul (&omega, 2.0 * G_PI / PIXELS_PER_REVOLUTION); + } + } + else + { + GimpVector2 o; + GimpVector2 v1 = {priv->last_x, priv->last_y}; + GimpVector2 v2 = {x, y}; + + g_warn_if_fail (priv->pivot_z == 0.0); + + gimp_matrix3_transform_point (&priv->orig_transform, + priv->pivot_x, priv->pivot_y, + &o.x, &o.y); + + gimp_vector2_sub (&v1, &v1, &o); + gimp_vector2_sub (&v2, &v2, &o); + + gimp_vector2_normalize (&v1); + gimp_vector2_normalize (&v2); + + omega.x = 0.0; + omega.y = 0.0; + omega.z = atan2 (gimp_vector2_cross_product (&v1, &v2).y, + gimp_vector2_inner_product (&v1, &v2)); + + omega.z *= z_sign; + } + + gimp_matrix4_identity (&matrix); + + if (local_frame) + gimp_transform_3d_matrix4_rotate (&matrix, &omega); + + gimp_transform_3d_matrix4_rotate_euler (&matrix, + priv->rotation_order, + priv->angle_x, + priv->angle_y, + priv->angle_z, + 0.0, 0.0, 0.0); + + if (! local_frame) + gimp_transform_3d_matrix4_rotate (&matrix, &omega); + + gimp_transform_3d_matrix4_rotate_euler_decompose (&matrix, + priv->rotation_order, + &priv->angle_x, + &priv->angle_y, + &priv->angle_z); + + priv->last_x = x; + priv->last_y = y; + + g_object_set (grid, + "angle-x", priv->angle_x, + "angle-y", priv->angle_y, + "angle-z", priv->angle_z, + NULL); + + return TRUE; +} + + +/* public functions */ + +GimpToolWidget * +gimp_tool_transform_3d_grid_new (GimpDisplayShell *shell, + gdouble x1, + gdouble y1, + gdouble x2, + gdouble y2, + gdouble camera_x, + gdouble camera_y, + gdouble camera_z) +{ + g_return_val_if_fail (GIMP_IS_DISPLAY_SHELL (shell), NULL); + + return g_object_new (GIMP_TYPE_TOOL_TRANSFORM_3D_GRID, + "shell", shell, + "x1", x1, + "y1", y1, + "x2", x2, + "y2", y2, + "camera-x", camera_x, + "camera-y", camera_y, + "camera-z", camera_z, + "pivot-3d-x", (x1 + x2) / 2.0, + "pivot-3d-y", (y1 + y2) / 2.0, + "pivot-3d-z", 0.0, + NULL); +} diff --git a/app/display/gimptooltransform3dgrid.h b/app/display/gimptooltransform3dgrid.h new file mode 100644 index 0000000000..42ac3eaa49 --- /dev/null +++ b/app/display/gimptooltransform3dgrid.h @@ -0,0 +1,65 @@ +/* GIMP - The GNU Image Manipulation Program + * Copyright (C) 1995 Spencer Kimball and Peter Mattis + * + * gimptool3dtransformgrid.h + * Copyright (C) 2019 Ell + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef __GIMP_TOOL_TRANSFORM_3D_GRID_H__ +#define __GIMP_TOOL_TRANSFORM_3D_GRID_H__ + + +#include "gimptooltransformgrid.h" + + +#define GIMP_TYPE_TOOL_TRANSFORM_3D_GRID (gimp_tool_transform_3d_grid_get_type ()) +#define GIMP_TOOL_TRANSFORM_3D_GRID(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GIMP_TYPE_TOOL_TRANSFORM_3D_GRID, GimpToolTransform3DGrid)) +#define GIMP_TOOL_TRANSFORM_3D_GRID_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GIMP_TYPE_TOOL_TRANSFORM_3D_GRID, GimpToolTransform3DGridClass)) +#define GIMP_IS_TOOL_TRANSFORM_3D_GRID(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GIMP_TYPE_TOOL_TRANSFORM_3D_GRID)) +#define GIMP_IS_TOOL_TRANSFORM_3D_GRID_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GIMP_TYPE_TOOL_TRANSFORM_3D_GRID)) +#define GIMP_TOOL_TRANSFORM_3D_GRID_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GIMP_TYPE_TOOL_TRANSFORM_3D_GRID, GimpToolTransform3DGridClass)) + + +typedef struct _GimpToolTransform3DGrid GimpToolTransform3DGrid; +typedef struct _GimpToolTransform3DGridPrivate GimpToolTransform3DGridPrivate; +typedef struct _GimpToolTransform3DGridClass GimpToolTransform3DGridClass; + +struct _GimpToolTransform3DGrid +{ + GimpToolTransformGrid parent_instance; + + GimpToolTransform3DGridPrivate *priv; +}; + +struct _GimpToolTransform3DGridClass +{ + GimpToolTransformGridClass parent_class; +}; + + +GType gimp_tool_transform_3d_grid_get_type (void) G_GNUC_CONST; + +GimpToolWidget * gimp_tool_transform_3d_grid_new (GimpDisplayShell *shell, + gdouble x1, + gdouble y1, + gdouble x2, + gdouble y2, + gdouble camera_x, + gdouble camera_y, + gdouble camera_z); + + +#endif /* __GIMP_TOOL_TRANSFORM_3D_GRID_H__ */ diff --git a/app/widgets/gimpcursor.c b/app/widgets/gimpcursor.c index a48ce87c88..7509f48068 100644 --- a/app/widgets/gimpcursor.c +++ b/app/widgets/gimpcursor.c @@ -177,6 +177,7 @@ static GimpCursor gimp_tool_cursors[] = { "tool-rotate" }, { "tool-shear" }, { "tool-perspective" }, + { "tool-transform-3d-camera" }, { "tool-flip-horizontal" }, { "tool-flip-vertical" }, { "tool-text" }, diff --git a/app/widgets/widgets-enums.h b/app/widgets/widgets-enums.h index 26788ca142..59b7ed4928 100644 --- a/app/widgets/widgets-enums.h +++ b/app/widgets/widgets-enums.h @@ -228,6 +228,7 @@ typedef enum /*< skip >*/ GIMP_TOOL_CURSOR_ROTATE, GIMP_TOOL_CURSOR_SHEAR, GIMP_TOOL_CURSOR_PERSPECTIVE, + GIMP_TOOL_CURSOR_TRANSFORM_3D_CAMERA, GIMP_TOOL_CURSOR_FLIP_HORIZONTAL, GIMP_TOOL_CURSOR_FLIP_VERTICAL, GIMP_TOOL_CURSOR_TEXT, diff --git a/cursors/Makefile.am b/cursors/Makefile.am index 307800e301..4c2c679ed2 100644 --- a/cursors/Makefile.am +++ b/cursors/Makefile.am @@ -151,6 +151,8 @@ CURSOR_IMAGES = \ tool-smudge-x2.png \ tool-text.png \ tool-text-x2.png \ + tool-transform-3d-camera.png \ + tool-transform-3d-camera-x2.png \ tool-warp.png \ tool-warp-x2.png \ tool-zoom.png \ diff --git a/cursors/gimp-tool-cursors-x2.xcf b/cursors/gimp-tool-cursors-x2.xcf index ff0e27e383b4537c72fef339f299ddae123567c5..32e1da5c2405df7244d3c8b5099c9e7c6b4aab25 100644 GIT binary patch delta 10799 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 delta 5749 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 M^U7^CD@6bQ0Yo7!s{jB1 diff --git a/cursors/gimp-tool-cursors.xcf b/cursors/gimp-tool-cursors.xcf index 5092f680ad10ba098db829691445291daad88ff6..67aff4427e70f69cd05b8f63c8030fb2929c9451 100644 GIT binary patch delta 7434 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 Vb``H^vr)Bi`39YZyo_?oe*v>)CT#!! delta 4452 zcmZvf3sh9c8G!%)-d#{C2_lamD66gmmnFIotR%*28`PRe5FdqjS|}nfgP?*5R;$1< z8c&IcH)gSJ8a0s_dn;( z+?ji6zu~=3!^oVxWy?(^nc1edtkzgyVs0W% zz$@DTW|jcF#_rkcUjWR@1^C%rfH$%L(h%U6=KvPh04zxX$j%1HW&Jmc0Sf$KJuF`a zP`nS|SN8!*nZrA+0KfSRU<(UWRR!>VHGu6Tz|Jy&-7f*`-3m|}1#tKyfCe^+r{V$r zk_T{hKfw8XfPZ=e{Hq(_Vm-j+2!N|S0M{b{x;Fvb3;^g&0Jyyq;Qm5@M>gjCHZC7`C zO(6z$BQ-IzD{lt&_oo5mT4#n98sP1V#CTQY+}6m(uZh#&%ZsKDIc*?5Y*O4gxAoEa z``o6PzLzB8+u~{K%O=#_-?xvJ`aP7D;kQN3h9j?W9v;5H=i)J#@t!hBsx0$Gg%4$ zUgSK!Nfw!D1nX<%|7hG?12I>MYVKmzm1(B*Y(%f5MC|pnWs^T3&hjG8ur3SVLeE)3 zWyNw#RP*o_w$e3mx>zo=o|-(vOy^EKB`XsLncIoFxt+-8i5A4YAkoc}Y9tb;3(3>O zb!thT?q_j#>9Y7{nZ)xO%*C0W+s3yIAQy}OmactymVr!-6FIk?`csijO{MtbVp*9! zNZr$Qbx+soPM+~1((h+vOG?5AVk{m%fjzUDIO|I|%6jUS*=00%_D>{bR=KF=F0*uD zX0d$*)PlIlYU9)>wKj2$nv{5fCME8bEOU&W?sGy!Hb>$8A!@T_jCw42n`%!!NbSkd zk|pVcXvlLX{X=9)-?*GZ)zXD3a~|pXrKJl;NMt_S>614;R> z$hmFmvm#3kr^|nyERjVkMb2#(Z4}v}59pJ$r3NKk5;?a`=g&14^FT`{>{tEWk}_&p zLL`!Lp6i%6)!ZfH`T%mVjQg}Q`v8*6eIn4uV3PG4Kv zi6n=eA)2_=kh4}~ITdtS{!EGFeJyexH}5|p%X>g?6|9p;eu>DrZGMHw^6j*C#aj|7 zWXFXjZZ#A}imWi6W)#Ou$_m{$c-$4bDOthSF8%85Z&5w>_C0!Zbu5xqwPHN(w@SD6 zRyEV8H5M;r&0|m7wf-VoJC^5|yCDtLweR}S$K@R|DPu>hCT`;>W5=mGE32X-H_bMV z-*{2vyq=A{BHKiFReWw#D)`gN#Vu@wZfB_IV0t=6eHQ6HwFyb(mzsy*Q>i;vDzDK) z+Yd;}Hr=DnOWCH|F}7(t#&P>ZwS7m5TEAlht>4j#%JyBJX|^8{+4hsHuO7FrqciR0 z5~=39ITyFlRJRWx7puNWy*~P_OzfLQ&MUKjC^CC3?buUgBzvZdoZIeMAhJD6Xzizp zfz%9gLe=O_s2Zv^9ZFTxPkciC>ehRaI;$9q$FEBeS=|EKbZG7na->G&-1dlW7LWYZ z8UG1A*cjwR8ejDEX-pJZ<1e@mO*^&0o1C5^>UfURNg_L)PEAcekd-s+eN7WHyYj|t z&y;H9={%g-%KGZL=4e`du1h9OpNeYk(xf|io6gdPukA*qWvQoa%Q}&@Y~kULwW(@j zTNQ0=dtD-}gPa?!x^tsdp|5r{O3H<9F&r=FLchrV?aNbKy4pm8t~N_bd%dXUF6}2p z*3Ne{`sFuPq^Z|ONy-(zL31%X8}qsHl184gE3dM?x~032j_;l%DcA0ZYM!RkTV$QV z+@;~W_toRyRk8L+z1e%0bj65P+_P(j$hsEN@Y|DRkT9qe)sqtMs$H3gp$UsLdzaPB%G z6hF3ELvgo*T{cKjIQ*uNL95Xp1&03o6Tp@hjz=B!qcND(8mK$S`TYodBzgr6V|_>c zSWHI8qH$R^`yHU%c#W6xd3@ZoS7n0g#D#*z6vnq&)_69yD+D;v$J0z-2y@7pS3vCFnK7Oy$j zWgW{Dsr!!NnIiut`bM@n5+~t2IRc|S4+xAFvt#rEM@KxC8>e8*1tE?$NipKYVT?A? O^+vyotW`MMlm7uszSr6S diff --git a/cursors/tool-transform-3d-camera-x2.png b/cursors/tool-transform-3d-camera-x2.png new file mode 100644 index 0000000000000000000000000000000000000000..3a7765717e3579e9d6cd37f92bdbaeed4bc9840e GIT binary patch literal 396 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!oCO|{#S9GG!XV7ZFl&wk0|TR- zr;B4q#jUqDtosfbNH9E1yxuc`B_mvZ>0a(@xu*5I%?>QGH@ zs(`?e6K0H_lT^^r|HBWM*KPd!`2D<^d;PsO;WejPx4nJazBfjXVUC|VL*Maaqx@NF zGoL?BF{+GumA9Qa!EAP9zP(yY)6*PlH8sjsYp5Jsw^ifW$6u>fy?d8;`K3t1+p=uO z_^oWEQ-wcQ{QX+x`|Rg$w@k4s+nbWsR($<4xqqYe(I{cF*}M#|zglUYYWlUSdvEo> z%A!ef3}>Ey7E9O~RXOQjSFDa#XZ)^p*$jN`hws*Z|M}y^z6(hkGZ?3xUdp=nuyvR0 g{B*}jDqpYZdqmCrq^2z;2n=2ZPgg&ebxsLQ01Z8_N&o-= literal 0 HcmV?d00001 diff --git a/cursors/tool-transform-3d-camera.png b/cursors/tool-transform-3d-camera.png new file mode 100644 index 0000000000000000000000000000000000000000..69f2ca8ca58bc981a4fa07ed632c01de85a6c90b GIT binary patch literal 333 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbBI14-?iy0WWg+Z8+Vb&Z8px}Q` z7srr@!*8b?%sp(t!=OKPP3xw1J2$ZtOp?mpDXh-x|LmPC&zH&e>a^ReE%TYQqNcD5 z{PK9zxrn9ju>}PE?}}}9DOJb!tShY9E%Q5t%<^P!0d8t8OGbF$NmNmZptjoh%G4$`2`fCx>TIU$IA7&JM zShO=CEqDE!)&DB9UfumzaVIWmb9#PSi$Fl=vdl+iyK_CSSN!SrTOMpX|NQ>X$tC|} a-aibV