瀏覽代碼

Improve 2D editor zoom logic

- Add 1-5 shortcuts to zoom between 100% and 1600% quickly
  (similar to GIMP).
- When holding down Alt, go through integer zoom values if above 100%
  or fractional zoom values with integer denominators if below 100%
  (50%, ~33.3%, 25%, …).
Hugo Locurcio 4 年之前
父節點
當前提交
43f9699a26
共有 3 個文件被更改,包括 93 次插入29 次删除
  1. 62 24
      editor/editor_zoom_widget.cpp
  2. 1 1
      editor/editor_zoom_widget.h
  3. 30 4
      editor/plugins/canvas_item_editor_plugin.cpp

+ 62 - 24
editor/editor_zoom_widget.cpp

@@ -51,17 +51,17 @@ void EditorZoomWidget::_update_zoom_label() {
 }
 
 void EditorZoomWidget::_button_zoom_minus() {
-	set_zoom_by_increments(-6);
+	set_zoom_by_increments(-6, Input::get_singleton()->is_key_pressed(KEY_ALT));
 	emit_signal("zoom_changed", zoom);
 }
 
 void EditorZoomWidget::_button_zoom_reset() {
-	set_zoom(1.0);
+	set_zoom(1.0 * MAX(1, EDSCALE));
 	emit_signal("zoom_changed", zoom);
 }
 
 void EditorZoomWidget::_button_zoom_plus() {
-	set_zoom_by_increments(6);
+	set_zoom_by_increments(6, Input::get_singleton()->is_key_pressed(KEY_ALT));
 	emit_signal("zoom_changed", zoom);
 }
 
@@ -76,31 +76,69 @@ void EditorZoomWidget::set_zoom(float p_zoom) {
 	}
 }
 
-void EditorZoomWidget::set_zoom_by_increments(int p_increment_count) {
-	// Base increment factor defined as the twelveth root of two.
-	// This allow a smooth geometric evolution of the zoom, with the advantage of
-	// visiting all integer power of two scale factors.
-	// note: this is analogous to the 'semitones' interval in the music world
-	// In order to avoid numerical imprecisions, we compute and edit a zoom index
-	// with the following relation: zoom = 2 ^ (index / 12)
-
-	if (zoom < CMP_EPSILON || p_increment_count == 0) {
-		return;
-	}
+void EditorZoomWidget::set_zoom_by_increments(int p_increment_count, bool p_integer_only) {
+	// Remove editor scale from the index computation.
+	const float zoom_noscale = zoom / MAX(1, EDSCALE);
+
+	if (p_integer_only) {
+		// Only visit integer scaling factors above 100%, and fractions with an integer denominator below 100%
+		// (1/2 = 50%, 1/3 = 33.33%, 1/4 = 25%, …).
+		// This is useful when working on pixel art projects to avoid distortion.
+		// This algorithm is designed to handle fractional start zoom values correctly
+		// (e.g. 190% will zoom up to 200% and down to 100%).
+		if (zoom_noscale + p_increment_count * 0.001 >= 1.0 - CMP_EPSILON) {
+			// New zoom is certain to be above 100%.
+			if (p_increment_count >= 1) {
+				// Zooming.
+				set_zoom(Math::floor(zoom_noscale + p_increment_count) * MAX(1, EDSCALE));
+			} else {
+				// Dezooming.
+				set_zoom(Math::ceil(zoom_noscale + p_increment_count) * MAX(1, EDSCALE));
+			}
+		} else {
+			if (p_increment_count >= 1) {
+				// Zooming. Convert the current zoom into a denominator.
+				float new_zoom = 1.0 / Math::ceil(1.0 / zoom_noscale - p_increment_count);
+				if (Math::is_equal_approx(zoom_noscale, new_zoom)) {
+					// New zoom is identical to the old zoom, so try again.
+					// This can happen due to floating-point precision issues.
+					new_zoom = 1.0 / Math::ceil(1.0 / zoom_noscale - p_increment_count - 1);
+				}
+				set_zoom(new_zoom * MAX(1, EDSCALE));
+			} else {
+				// Dezooming. Convert the current zoom into a denominator.
+				float new_zoom = 1.0 / Math::floor(1.0 / zoom_noscale - p_increment_count);
+				if (Math::is_equal_approx(zoom_noscale, new_zoom)) {
+					// New zoom is identical to the old zoom, so try again.
+					// This can happen due to floating-point precision issues.
+					new_zoom = 1.0 / Math::floor(1.0 / zoom_noscale - p_increment_count + 1);
+				}
+				set_zoom(new_zoom * MAX(1, EDSCALE));
+			}
+		}
+	} else {
+		// Base increment factor defined as the twelveth root of two.
+		// This allow a smooth geometric evolution of the zoom, with the advantage of
+		// visiting all integer power of two scale factors.
+		// note: this is analogous to the 'semitones' interval in the music world
+		// In order to avoid numerical imprecisions, we compute and edit a zoom index
+		// with the following relation: zoom = 2 ^ (index / 12)
 
-	// Remove Editor scale from the index computation
-	float zoom_noscale = zoom / MAX(1, EDSCALE);
+		if (zoom < CMP_EPSILON || p_increment_count == 0) {
+			return;
+		}
 
-	// zoom = 2**(index/12) => log2(zoom) = index/12
-	float closest_zoom_index = Math::round(Math::log(zoom_noscale) * 12.f / Math::log(2.f));
+		// zoom = 2**(index/12) => log2(zoom) = index/12
+		float closest_zoom_index = Math::round(Math::log(zoom_noscale) * 12.f / Math::log(2.f));
 
-	float new_zoom_index = closest_zoom_index + p_increment_count;
-	float new_zoom = Math::pow(2.f, new_zoom_index / 12.f);
+		float new_zoom_index = closest_zoom_index + p_increment_count;
+		float new_zoom = Math::pow(2.f, new_zoom_index / 12.f);
 
-	// Restore Editor scale transformation
-	new_zoom *= MAX(1, EDSCALE);
+		// Restore Editor scale transformation
+		new_zoom *= MAX(1, EDSCALE);
 
-	set_zoom(new_zoom);
+		set_zoom(new_zoom);
+	}
 }
 
 void EditorZoomWidget::_notification(int p_what) {
@@ -118,7 +156,7 @@ void EditorZoomWidget::_notification(int p_what) {
 void EditorZoomWidget::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("set_zoom", "zoom"), &EditorZoomWidget::set_zoom);
 	ClassDB::bind_method(D_METHOD("get_zoom"), &EditorZoomWidget::get_zoom);
-	ClassDB::bind_method(D_METHOD("set_zoom_by_increments", "increment"), &EditorZoomWidget::set_zoom_by_increments);
+	ClassDB::bind_method(D_METHOD("set_zoom_by_increments", "increment", "integer_only"), &EditorZoomWidget::set_zoom_by_increments);
 
 	ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "zoom"), "set_zoom", "get_zoom");
 

+ 1 - 1
editor/editor_zoom_widget.h

@@ -56,7 +56,7 @@ public:
 
 	float get_zoom();
 	void set_zoom(float p_zoom);
-	void set_zoom_by_increments(int p_increment_count);
+	void set_zoom_by_increments(int p_increment_count, bool p_integer_only = false);
 };
 
 #endif // EDITOR_ZOOM_WIDGET_H

+ 30 - 4
editor/plugins/canvas_item_editor_plugin.cpp

@@ -1149,8 +1149,9 @@ bool CanvasItemEditor::_gui_input_zoom_or_pan(const Ref<InputEvent> &p_event, bo
 				view_offset.y += int(EditorSettings::get_singleton()->get("editors/2d/pan_speed")) / zoom * b->get_factor();
 				update_viewport();
 			} else {
-				zoom_widget->set_zoom_by_increments(-1);
-				if (b->get_factor() != 1.f) {
+				zoom_widget->set_zoom_by_increments(-1, Input::get_singleton()->is_key_pressed(KEY_ALT));
+				if (!Math::is_equal_approx(b->get_factor(), 1.0f)) {
+					// Handle high-precision (analog) scrolling.
 					zoom_widget->set_zoom(zoom * ((zoom_widget->get_zoom() / zoom - 1.f) * b->get_factor() + 1.f));
 				}
 				_zoom_on_position(zoom_widget->get_zoom(), b->get_position());
@@ -1164,8 +1165,9 @@ bool CanvasItemEditor::_gui_input_zoom_or_pan(const Ref<InputEvent> &p_event, bo
 				view_offset.y -= int(EditorSettings::get_singleton()->get("editors/2d/pan_speed")) / zoom * b->get_factor();
 				update_viewport();
 			} else {
-				zoom_widget->set_zoom_by_increments(1);
-				if (b->get_factor() != 1.f) {
+				zoom_widget->set_zoom_by_increments(1, Input::get_singleton()->is_key_pressed(KEY_ALT));
+				if (!Math::is_equal_approx(b->get_factor(), 1.0f)) {
+					// Handle high-precision (analog) scrolling.
 					zoom_widget->set_zoom(zoom * ((zoom_widget->get_zoom() / zoom - 1.f) * b->get_factor() + 1.f));
 				}
 				_zoom_on_position(zoom_widget->get_zoom(), b->get_position());
@@ -1194,6 +1196,20 @@ bool CanvasItemEditor::_gui_input_zoom_or_pan(const Ref<InputEvent> &p_event, bo
 
 	Ref<InputEventKey> k = p_event;
 	if (k.is_valid()) {
+		if (k->is_pressed()) {
+			if (ED_GET_SHORTCUT("canvas_item_editor/zoom_100_percent")->is_shortcut(p_event)) {
+				_update_zoom(1.0 * MAX(1, EDSCALE));
+			} else if (ED_GET_SHORTCUT("canvas_item_editor/zoom_200_percent")->is_shortcut(p_event)) {
+				_update_zoom(2.0 * MAX(1, EDSCALE));
+			} else if (ED_GET_SHORTCUT("canvas_item_editor/zoom_400_percent")->is_shortcut(p_event)) {
+				_update_zoom(4.0 * MAX(1, EDSCALE));
+			} else if (ED_GET_SHORTCUT("canvas_item_editor/zoom_800_percent")->is_shortcut(p_event)) {
+				_update_zoom(8.0 * MAX(1, EDSCALE));
+			} else if (ED_GET_SHORTCUT("canvas_item_editor/zoom_1600_percent")->is_shortcut(p_event)) {
+				_update_zoom(16.0 * MAX(1, EDSCALE));
+			}
+		}
+
 		bool is_pan_key = pan_view_shortcut.is_valid() && pan_view_shortcut->is_shortcut(p_event);
 
 		if (is_pan_key && (EditorSettings::get_singleton()->get("editors/2d/simple_panning") || drag_type != DRAG_NONE)) {
@@ -5610,6 +5626,16 @@ CanvasItemEditor::CanvasItemEditor(EditorNode *p_editor) {
 	skeleton_menu->get_popup()->set_item_checked(skeleton_menu->get_popup()->get_item_index(SKELETON_SHOW_BONES), true);
 	singleton = this;
 
+	// To ensure that scripts can parse the list of shortcuts correctly, we have to define
+	// those shortcuts one by one.
+	// Resetting zoom to 100% is a duplicate shortcut of `canvas_item_editor/reset_zoom`,
+	// but it ensures both 1 and Ctrl + 0 can be used to reset zoom.
+	ED_SHORTCUT("canvas_item_editor/zoom_100_percent", TTR("Zoom To 100%"), KEY_1);
+	ED_SHORTCUT("canvas_item_editor/zoom_200_percent", TTR("Zoom To 200%"), KEY_2);
+	ED_SHORTCUT("canvas_item_editor/zoom_400_percent", TTR("Zoom To 400%"), KEY_3);
+	ED_SHORTCUT("canvas_item_editor/zoom_800_percent", TTR("Zoom To 800%"), KEY_4);
+	ED_SHORTCUT("canvas_item_editor/zoom_1600_percent", TTR("Zoom To 1600%"), KEY_5);
+
 	set_process_unhandled_key_input(true);
 
 	// Update the menus' checkboxes