mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-12-23 10:11:54 +00:00
Viewport canvas navigation with modifier keys and zoom widget (#229)
* Add rotation around the center * Document transform centred * Fix drawing hexagon on rotated document * Format * Fix translation on rotated document * Remove logging * Rotate around centre of viewport * Rotate with shift + MMB drag * Zoom with +/- keys * Rotation input field * Implement frontend zoom buttons * Zoom with ctrl + MMB * Format * Update number inputs * Require Ctrl + Plus / Minus key * Ctrl scroll * Update zoom -> Multiply Zoom * Fix typo * More fixing typo * Remove :v-on * Add mouse scroll X * Scrolling on document * Refactor * Format * Fix ctrl + plus/minus to zoom * Reduce zoom sensitivity * Ctrl + shift + mmb drag = snap rotate * Further reduce zoom speed * Add ctrl + number key to change zoom * Switch Ctrl and Shift for zoom and rotate * Fix compile errors * Format JS * Add increment to snap angle * Edit getting layerdata functions * Pass viewport size directily into create_document_transform_from_layerdata * Add to_dvec2() * Refactor get_transform * Get -> Calculate * Add consts * Use to_radians * Remove get from function names * Use .entry when getting layerdata that does not exist * Fix distance scroll calculations * Fix zooming. * Remove 'Violation' in chrome * Fix compile errors
This commit is contained in:
parent
8ff545f01c
commit
39e6893630
22 changed files with 442 additions and 73 deletions
|
|
@ -88,13 +88,17 @@
|
|||
|
||||
<Separator :type="SeparatorType.Section" />
|
||||
|
||||
<IconButton :icon="'ZoomIn'" :size="24" title="Zoom In" />
|
||||
<IconButton :icon="'ZoomOut'" :size="24" title="Zoom Out" />
|
||||
<IconButton :icon="'ZoomReset'" :size="24" title="Zoom to 100%" />
|
||||
<NumberInput :callback="setRotation" :initial_value="0" :step="15" :unit="`°`" :update_on_callback="false" ref="rotation" />
|
||||
|
||||
<Separator :type="SeparatorType.Section" />
|
||||
|
||||
<IconButton :icon="'ZoomIn'" :size="24" title="Zoom In" @click="this.$refs.zoom.onIncrement(1)" />
|
||||
<IconButton :icon="'ZoomOut'" :size="24" title="Zoom Out" @click="this.$refs.zoom.onIncrement(-1)" />
|
||||
<IconButton :icon="'ZoomReset'" :size="24" title="Zoom to 100%" @click="this.$refs.zoom.updateValue(100)" />
|
||||
|
||||
<Separator :type="SeparatorType.Related" />
|
||||
|
||||
<NumberInput :value="25" :unit="`%`" />
|
||||
<NumberInput :callback="setZoom" :initial_value="100" :min="0.001" :increaseMultiplier="1.25" :decreaseMultiplier="0.8" :unit="`%`" :update_on_callback="false" ref="zoom" />
|
||||
</div>
|
||||
</LayoutRow>
|
||||
<LayoutRow :class="'shelf-and-viewport'">
|
||||
|
|
@ -135,7 +139,7 @@
|
|||
<WorkingColors />
|
||||
</LayoutCol>
|
||||
<LayoutCol :class="'viewport'">
|
||||
<div class="canvas" @mousedown="canvasMouseDown" @mouseup="canvasMouseUp" @mousemove="canvasMouseMove">
|
||||
<div class="canvas" @mousedown="canvasMouseDown" @mouseup="canvasMouseUp" @mousemove="canvasMouseMove" ref="canvas">
|
||||
<svg v-html="viewportSvg"></svg>
|
||||
</div>
|
||||
</LayoutCol>
|
||||
|
|
@ -188,7 +192,7 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { ResponseType, registerResponseHandler, Response, UpdateCanvas, SetActiveTool, ExportDocument } from "../../response-handler";
|
||||
import { ResponseType, registerResponseHandler, Response, UpdateCanvas, SetActiveTool, ExportDocument, SetZoom, SetRotation } from "../../response-handler";
|
||||
import LayoutRow from "../layout/LayoutRow.vue";
|
||||
import LayoutCol from "../layout/LayoutCol.vue";
|
||||
import WorkingColors from "../widgets/WorkingColors.vue";
|
||||
|
|
@ -235,6 +239,11 @@ function makeModifiersBitfield(control: boolean, shift: boolean, alt: boolean):
|
|||
|
||||
export default defineComponent({
|
||||
methods: {
|
||||
async viewportResize() {
|
||||
const { on_viewport_resize } = await wasm;
|
||||
const canvas = this.$refs.canvas as HTMLDivElement;
|
||||
on_viewport_resize(canvas.clientWidth, canvas.clientHeight);
|
||||
},
|
||||
async canvasMouseDown(e: MouseEvent) {
|
||||
const { on_mouse_down } = await wasm;
|
||||
const modifiers = makeModifiersBitfield(e.ctrlKey, e.shiftKey, e.altKey);
|
||||
|
|
@ -250,6 +259,20 @@ export default defineComponent({
|
|||
const modifiers = makeModifiersBitfield(e.ctrlKey, e.shiftKey, e.altKey);
|
||||
on_mouse_move(e.offsetX, e.offsetY, modifiers);
|
||||
},
|
||||
async canvasMouseScroll(e: WheelEvent) {
|
||||
e.preventDefault();
|
||||
const { on_mouse_scroll } = await wasm;
|
||||
const modifiers = makeModifiersBitfield(e.ctrlKey, e.shiftKey, e.altKey);
|
||||
on_mouse_scroll(e.deltaX, e.deltaY, e.deltaZ, modifiers);
|
||||
},
|
||||
async setZoom(newZoom: number) {
|
||||
const { on_set_zoom } = await wasm;
|
||||
on_set_zoom(newZoom / 100);
|
||||
},
|
||||
async setRotation(newRotation: number) {
|
||||
const { on_set_rotation } = await wasm;
|
||||
on_set_rotation(newRotation * (Math.PI / 180));
|
||||
},
|
||||
async keyDown(e: KeyboardEvent) {
|
||||
if (redirectKeyboardEventToBackend(e)) {
|
||||
e.preventDefault();
|
||||
|
|
@ -302,9 +325,28 @@ export default defineComponent({
|
|||
const toolData = responseData as SetActiveTool;
|
||||
if (toolData) this.activeTool = toolData.tool_name;
|
||||
});
|
||||
registerResponseHandler(ResponseType.SetZoom, (responseData: Response) => {
|
||||
const updateData = responseData as SetZoom;
|
||||
if (updateData) {
|
||||
const zoomWidget = this.$refs.zoom as typeof NumberInput;
|
||||
zoomWidget.setValue(updateData.new_zoom * 100);
|
||||
}
|
||||
});
|
||||
registerResponseHandler(ResponseType.SetRotation, (responseData: Response) => {
|
||||
const updateData = responseData as SetRotation;
|
||||
if (updateData) {
|
||||
const rotationWidget = this.$refs.rotation as typeof NumberInput;
|
||||
const newRotation = updateData.new_radians * (180 / Math.PI);
|
||||
rotationWidget.setValue((360 + (newRotation % 360)) % 360);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("keyup", (e: KeyboardEvent) => this.keyUp(e));
|
||||
const canvas = this.$refs.canvas as HTMLDivElement;
|
||||
canvas.addEventListener("wheel", this.canvasMouseScroll, { passive: false });
|
||||
window.addEventListener("keydown", (e: KeyboardEvent) => this.keyDown(e));
|
||||
window.addEventListener("resize", () => this.viewportResize());
|
||||
window.addEventListener("DOMContentLoaded", () => this.viewportResize());
|
||||
|
||||
this.$watch("viewModeIndex", this.viewModeChanged);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@
|
|||
<div class="number-input">
|
||||
<button class="arrow left" @click="onIncrement(-1)"></button>
|
||||
<button class="arrow right" @click="onIncrement(1)"></button>
|
||||
<input type="text" spellcheck="false" :value="displayValue" />
|
||||
<input type="text" spellcheck="false" v-model="text" @change="updateText($event.target.value)" /> />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.number-input {
|
||||
width: 64px;
|
||||
width: 80px;
|
||||
height: 24px;
|
||||
position: relative;
|
||||
border-radius: 2px;
|
||||
|
|
@ -98,37 +98,57 @@ import { defineComponent } from "vue";
|
|||
export default defineComponent({
|
||||
components: {},
|
||||
props: {
|
||||
value: { type: Number, required: true },
|
||||
initial_value: { type: Number, default: 0, required: false },
|
||||
unit: { type: String, default: "", required: false },
|
||||
step: { type: Number, default: 1, required: false },
|
||||
increaseMultiplier: { type: Number, default: null, required: false },
|
||||
decreaseMultiplier: { type: Number, default: null, required: false },
|
||||
min: { type: Number, required: false },
|
||||
max: { type: Number, required: false },
|
||||
callback: { type: Function, required: false },
|
||||
update_on_callback: { type: Boolean, default: true, required: false },
|
||||
},
|
||||
computed: {
|
||||
displayValue(): string {
|
||||
if (!this.unit) return this.value.toString();
|
||||
return `${this.value}${this.unit}`;
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
value: this.initial_value,
|
||||
text: this.initial_value.toString() + this.unit,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onIncrement(direction: number) {
|
||||
const step = this.step * direction;
|
||||
const newValue = this.value + step;
|
||||
this.updateValue(newValue);
|
||||
if (direction === 1 && this.increaseMultiplier) this.updateValue(this.value * this.increaseMultiplier, true);
|
||||
else if (direction === -1 && this.decreaseMultiplier) this.updateValue(this.value * this.decreaseMultiplier, true);
|
||||
else this.updateValue(this.value + this.step * direction, true);
|
||||
},
|
||||
|
||||
updateValue(newValue: number) {
|
||||
let value = newValue;
|
||||
updateText(newText: string) {
|
||||
const newValue = parseInt(newText, 10);
|
||||
this.updateValue(newValue, true);
|
||||
},
|
||||
|
||||
clampValue(newValue: number, resetOnClamp: boolean) {
|
||||
if (!Number.isFinite(newValue)) return this.value;
|
||||
let result = newValue;
|
||||
if (Number.isFinite(this.min) && typeof this.min === "number") {
|
||||
value = Math.max(value, this.min);
|
||||
if (resetOnClamp && newValue < this.min) return this.value;
|
||||
result = Math.max(result, this.min);
|
||||
}
|
||||
|
||||
if (Number.isFinite(this.max) && typeof this.max === "number") {
|
||||
value = Math.min(value, this.max);
|
||||
if (resetOnClamp && newValue > this.max) return this.value;
|
||||
result = Math.min(result, this.max);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
setValue(newValue: number) {
|
||||
this.value = newValue;
|
||||
this.text = `${Math.round(this.value)}${this.unit}`;
|
||||
},
|
||||
updateValue(inValue: number, resetOnClamp: boolean) {
|
||||
const newValue = this.clampValue(inValue, resetOnClamp);
|
||||
|
||||
this.$emit("update:value", value);
|
||||
if (this.callback) this.callback(newValue);
|
||||
|
||||
if (this.update_on_callback) this.setValue(newValue);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ export enum ResponseType {
|
|||
CloseDocument = "CloseDocument",
|
||||
UpdateWorkingColors = "UpdateWorkingColors",
|
||||
PromptCloseConfirmationModal = "PromptCloseConfirmationModal",
|
||||
SetZoom = "SetZoom",
|
||||
SetRotation = "SetRotation",
|
||||
}
|
||||
|
||||
export function attachResponseHandlerToPage() {
|
||||
|
|
@ -64,6 +66,10 @@ function parseResponse(responseType: string, data: any): Response {
|
|||
return newCloseDocument(data.CloseDocument);
|
||||
case "UpdateCanvas":
|
||||
return newUpdateCanvas(data.UpdateCanvas);
|
||||
case "SetZoom":
|
||||
return newSetZoom(data.SetZoom);
|
||||
case "SetRotation":
|
||||
return newSetRotation(data.SetRotation);
|
||||
case "ExportDocument":
|
||||
return newExportDocument(data.ExportDocument);
|
||||
case "UpdateWorkingColors":
|
||||
|
|
@ -71,11 +77,11 @@ function parseResponse(responseType: string, data: any): Response {
|
|||
case "PromptCloseConfirmationModal":
|
||||
return {};
|
||||
default:
|
||||
throw new Error(`Unrecognized origin/responseType pair: ${origin}, ${responseType}`);
|
||||
throw new Error(`Unrecognized origin/responseType pair: ${origin}, '${responseType}'`);
|
||||
}
|
||||
}
|
||||
|
||||
export type Response = SetActiveTool | UpdateCanvas | DocumentChanged | CollapseFolder | ExpandFolder | UpdateWorkingColors;
|
||||
export type Response = SetActiveTool | UpdateCanvas | DocumentChanged | CollapseFolder | ExpandFolder | UpdateWorkingColors | SetZoom | SetRotation;
|
||||
|
||||
export interface CloseDocument {
|
||||
document_index: number;
|
||||
|
|
@ -175,6 +181,24 @@ function newExpandFolder(input: any): ExpandFolder {
|
|||
};
|
||||
}
|
||||
|
||||
export interface SetZoom {
|
||||
new_zoom: number;
|
||||
}
|
||||
function newSetZoom(input: any): SetZoom {
|
||||
return {
|
||||
new_zoom: input.new_zoom,
|
||||
};
|
||||
}
|
||||
|
||||
export interface SetRotation {
|
||||
new_radians: number;
|
||||
}
|
||||
function newSetRotation(input: any): SetRotation {
|
||||
return {
|
||||
new_radians: input.new_radians,
|
||||
};
|
||||
}
|
||||
|
||||
export interface LayerPanelEntry {
|
||||
name: string;
|
||||
visible: boolean;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use crate::shims::Error;
|
|||
use crate::wrappers::{translate_key, translate_tool, Color};
|
||||
use crate::EDITOR_STATE;
|
||||
use editor_core::input::input_preprocessor::ModifierKeys;
|
||||
use editor_core::input::mouse::ScrollDelta;
|
||||
use editor_core::message_prelude::*;
|
||||
use editor_core::{
|
||||
input::mouse::{MouseState, ViewportPosition},
|
||||
|
|
@ -37,6 +38,14 @@ pub fn new_document() -> Result<(), JsValue> {
|
|||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::NewDocument).map_err(convert_error))
|
||||
}
|
||||
|
||||
// TODO: Call event when the panels are resized
|
||||
/// Viewport resized
|
||||
#[wasm_bindgen]
|
||||
pub fn on_viewport_resize(new_width: u32, new_height: u32) -> Result<(), JsValue> {
|
||||
let ev = InputPreprocessorMessage::ViewportResize(ViewportPosition { x: new_width, y: new_height });
|
||||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error)
|
||||
}
|
||||
|
||||
// TODO: When a mouse button is down that started in the viewport, this should trigger even when the mouse is outside the viewport (or even the browser window if the browser supports it)
|
||||
/// Mouse movement within the screenspace bounds of the viewport
|
||||
#[wasm_bindgen]
|
||||
|
|
@ -47,6 +56,15 @@ pub fn on_mouse_move(x: u32, y: u32, modifiers: u8) -> Result<(), JsValue> {
|
|||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error)
|
||||
}
|
||||
|
||||
/// Mouse scrolling within the screenspace bounds of the viewport
|
||||
#[wasm_bindgen]
|
||||
pub fn on_mouse_scroll(delta_x: i32, delta_y: i32, delta_z: i32, modifiers: u8) -> Result<(), JsValue> {
|
||||
// TODO: Convert these screenspace viewport coordinates to canvas coordinates based on the current zoom and pan
|
||||
let mods = ModifierKeys::from_bits(modifiers).expect("invalid modifier keys");
|
||||
let ev = InputPreprocessorMessage::MouseScroll(ScrollDelta::new(delta_x, delta_y, delta_z), mods);
|
||||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error)
|
||||
}
|
||||
|
||||
/// A mouse button depressed within screenspace the bounds of the viewport
|
||||
#[wasm_bindgen]
|
||||
pub fn on_mouse_down(x: u32, y: u32, mouse_keys: u8, modifiers: u8) -> Result<(), JsValue> {
|
||||
|
|
@ -139,6 +157,20 @@ pub fn export_document() -> Result<(), JsValue> {
|
|||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::ExportDocument)).map_err(convert_error)
|
||||
}
|
||||
|
||||
/// Sets the zoom to the value
|
||||
#[wasm_bindgen]
|
||||
pub fn on_set_zoom(new_zoom: f64) -> Result<(), JsValue> {
|
||||
let ev = DocumentMessage::SetZoom(new_zoom);
|
||||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error)
|
||||
}
|
||||
|
||||
/// Sets the rotation to the new value (in radians)
|
||||
#[wasm_bindgen]
|
||||
pub fn on_set_rotation(new_radians: f64) -> Result<(), JsValue> {
|
||||
let ev = DocumentMessage::SetRotation(new_radians);
|
||||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error)
|
||||
}
|
||||
|
||||
/// Update the list of selected layers. The layer paths have to be stored in one array and are separated by LayerId::MAX
|
||||
#[wasm_bindgen]
|
||||
pub fn select_layers(paths: Vec<LayerId>) -> Result<(), JsValue> {
|
||||
|
|
|
|||
|
|
@ -115,6 +115,9 @@ pub fn translate_key(name: &str) -> Key {
|
|||
"8" => Key8,
|
||||
"9" => Key9,
|
||||
"enter" => KeyEnter,
|
||||
"=" => KeyEquals,
|
||||
"+" => KeyPlus,
|
||||
"-" => KeyMinus,
|
||||
"shift" => KeyShift,
|
||||
// When using linux + chrome + the neo keyboard layout, the shift key is recognized as caps
|
||||
"capslock" => KeyShift,
|
||||
|
|
|
|||
|
|
@ -316,6 +316,13 @@ impl Document {
|
|||
self.root.cache_dirty = true;
|
||||
Some(vec![DocumentResponse::DocumentChanged])
|
||||
}
|
||||
Operation::SetLayerTransform { path, transform } => {
|
||||
let transform = DAffine2::from_cols_array(&transform);
|
||||
let layer = self.document_layer_mut(path).unwrap();
|
||||
layer.transform = transform;
|
||||
layer.cache_dirty = true;
|
||||
Some(vec![DocumentResponse::DocumentChanged])
|
||||
}
|
||||
Operation::DiscardWorkingFolder => {
|
||||
self.work_operations.clear();
|
||||
self.work_mount_path = vec![];
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ use glam::DVec2;
|
|||
use crate::intersection::intersect_quad_bez_path;
|
||||
use crate::LayerId;
|
||||
use kurbo::BezPath;
|
||||
use kurbo::Vec2;
|
||||
|
||||
use super::style;
|
||||
use super::LayerData;
|
||||
|
|
@ -26,11 +25,9 @@ impl Shape {
|
|||
|
||||
impl LayerData for Shape {
|
||||
fn to_kurbo_path(&self, transform: glam::DAffine2, _style: style::PathStyle) -> BezPath {
|
||||
fn unit_rotation(theta: f64) -> Vec2 {
|
||||
Vec2::new(-theta.sin(), theta.cos())
|
||||
fn unit_rotation(theta: f64) -> DVec2 {
|
||||
DVec2::new(-theta.sin(), theta.cos())
|
||||
}
|
||||
let extent = Vec2::new((transform.x_axis.x + transform.x_axis.y) / 2., (transform.y_axis.x + transform.y_axis.y) / 2.);
|
||||
let translation = transform.translation;
|
||||
let mut path = kurbo::BezPath::new();
|
||||
let apothem_offset_angle = std::f64::consts::PI / (self.sides as f64);
|
||||
|
||||
|
|
@ -51,11 +48,12 @@ impl LayerData for Shape {
|
|||
if self.equal_sides {
|
||||
p
|
||||
} else {
|
||||
Vec2::new((p.x - min_x) / (max_x - min_x) * 2. - 1., (p.y - min_y) / (max_y - min_y) * 2. - 1.)
|
||||
DVec2::new((p.x - min_x) / (max_x - min_x) * 2. - 1., (p.y - min_y) / (max_y - min_y) * 2. - 1.)
|
||||
}
|
||||
})
|
||||
.map(|unit| Vec2::new(-unit.x * extent.x + translation.x + extent.x, -unit.y * extent.y + translation.y + extent.y))
|
||||
.map(|pos| (pos).to_point())
|
||||
.map(|p| DVec2::new(p.x / 2. + 0.5, p.y / 2. + 0.5))
|
||||
.map(|unit| transform.transform_point2(unit.into()))
|
||||
.map(|pos| kurbo::Point::new(pos.x, pos.y))
|
||||
.enumerate()
|
||||
.for_each(|(i, p)| {
|
||||
if i == 0 {
|
||||
|
|
|
|||
|
|
@ -61,6 +61,10 @@ pub enum Operation {
|
|||
path: Vec<LayerId>,
|
||||
transform: [f64; 6],
|
||||
},
|
||||
SetLayerTransform {
|
||||
path: Vec<LayerId>,
|
||||
transform: [f64; 6],
|
||||
},
|
||||
DiscardWorkingFolder,
|
||||
ClearWorkingFolder,
|
||||
CommitTransaction,
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ log = "0.4"
|
|||
bitflags = "1.2.1"
|
||||
thiserror = "1.0.24"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
graphite-proc-macros = {path = "../proc-macro"}
|
||||
glam = "0.16"
|
||||
graphite-proc-macros = { path = "../proc-macro" }
|
||||
glam = { version="0.16", features = ["serde"] }
|
||||
|
||||
[dependencies.document-core]
|
||||
path = "../document"
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ impl Dispatcher {
|
|||
| Message::InputMapper(_)
|
||||
| Message::Document(DocumentMessage::RenderDocument)
|
||||
| Message::Frontend(FrontendMessage::UpdateCanvas { .. })
|
||||
| Message::Frontend(FrontendMessage::SetZoom { .. })
|
||||
| Message::Frontend(FrontendMessage::SetRotation { .. })
|
||||
| Message::Document(DocumentMessage::DispatchOperation { .. })
|
||||
) || MessageDiscriminant::from(&message).local_name().ends_with("MouseMove"))
|
||||
{
|
||||
|
|
|
|||
7
core/editor/src/consts.rs
Normal file
7
core/editor/src/consts.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
pub const PLUS_KEY_MULTIPLIER: f64 = 1.25;
|
||||
pub const MINUS_KEY_MULTIPLIER: f64 = 0.8;
|
||||
|
||||
pub const WHEEL_ZOOM_DIVISOR: f64 = 500.;
|
||||
pub const MOUSE_ZOOM_DIVISOR: f64 = 400.;
|
||||
|
||||
pub const ROTATE_SNAP_INTERVAL: f64 = 15.;
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
use crate::{frontend::layer_panel::*, EditorError};
|
||||
use crate::{consts::ROTATE_SNAP_INTERVAL, frontend::layer_panel::*, EditorError};
|
||||
use document_core::{document::Document as InteralDocument, layers::Layer, LayerId};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
|
@ -15,7 +16,7 @@ impl Default for Document {
|
|||
Self {
|
||||
document: InteralDocument::default(),
|
||||
name: String::from("Untitled Document"),
|
||||
layer_data: vec![(vec![], LayerData { selected: false, expanded: true })].into_iter().collect(),
|
||||
layer_data: vec![(vec![], LayerData::new(true))].into_iter().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -25,14 +26,14 @@ impl Document {
|
|||
Self {
|
||||
document: InteralDocument::default(),
|
||||
name,
|
||||
layer_data: vec![(vec![], LayerData { selected: false, expanded: true })].into_iter().collect(),
|
||||
layer_data: vec![(vec![], LayerData::new(true))].into_iter().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn layer_data<'a>(layer_data: &'a mut HashMap<Vec<LayerId>, LayerData>, path: &[LayerId]) -> &'a mut LayerData {
|
||||
if !layer_data.contains_key(path) {
|
||||
layer_data.insert(path.to_vec(), LayerData::default());
|
||||
layer_data.insert(path.to_vec(), LayerData::new(false));
|
||||
}
|
||||
layer_data.get_mut(path).unwrap()
|
||||
}
|
||||
|
|
@ -74,8 +75,43 @@ impl Document {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Copy, Default)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Copy)]
|
||||
pub struct LayerData {
|
||||
pub selected: bool,
|
||||
pub expanded: bool,
|
||||
pub translation: DVec2,
|
||||
pub rotation: f64,
|
||||
pub snap_rotate: bool,
|
||||
pub scale: f64,
|
||||
}
|
||||
|
||||
impl LayerData {
|
||||
pub fn new(expanded: bool) -> LayerData {
|
||||
LayerData {
|
||||
selected: false,
|
||||
expanded,
|
||||
translation: DVec2::ZERO,
|
||||
rotation: 0.,
|
||||
snap_rotate: false,
|
||||
scale: 1.,
|
||||
}
|
||||
}
|
||||
pub fn snapped_angle(&self) -> f64 {
|
||||
let increment_radians: f64 = ROTATE_SNAP_INTERVAL.to_radians();
|
||||
if self.snap_rotate {
|
||||
(self.rotation / increment_radians).round() * increment_radians
|
||||
} else {
|
||||
self.rotation
|
||||
}
|
||||
}
|
||||
pub fn calculate_offset_transform(&self, offset: DVec2) -> DAffine2 {
|
||||
let offset_transform = DAffine2::from_translation(offset);
|
||||
let scale_transform = DAffine2::from_scale(DVec2::new(self.scale, self.scale));
|
||||
let angle_transform = DAffine2::from_angle(self.snapped_angle());
|
||||
let translation_transform = DAffine2::from_translation(self.translation.into());
|
||||
scale_transform * offset_transform * angle_transform * scale_transform * translation_transform
|
||||
}
|
||||
pub fn calculate_transform(&self) -> DAffine2 {
|
||||
self.calculate_offset_transform(DVec2::ZERO)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
|
||||
use crate::message_prelude::*;
|
||||
use crate::{
|
||||
consts::{MOUSE_ZOOM_DIVISOR, WHEEL_ZOOM_DIVISOR},
|
||||
input::{mouse::ViewportPosition, InputPreprocessor},
|
||||
};
|
||||
use document_core::layers::Layer;
|
||||
use document_core::{DocumentResponse, LayerId, Operation as DocumentOperation};
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
|
@ -8,6 +11,8 @@ use log::warn;
|
|||
use crate::document::Document;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use super::LayerData;
|
||||
|
||||
#[impl_message(Message, Document)]
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
pub enum DocumentMessage {
|
||||
|
|
@ -36,6 +41,14 @@ pub enum DocumentMessage {
|
|||
MouseMove,
|
||||
TranslateDown,
|
||||
TranslateUp,
|
||||
WheelTranslate,
|
||||
RotateDown { snap: bool },
|
||||
ZoomDown,
|
||||
TransformUp,
|
||||
SetZoom(f64),
|
||||
MultiplyZoom(f64),
|
||||
WheelZoom,
|
||||
SetRotation(f64),
|
||||
NudgeSelectedLayers(f64, f64),
|
||||
}
|
||||
|
||||
|
|
@ -54,7 +67,9 @@ impl From<DocumentOperation> for Message {
|
|||
pub struct DocumentMessageHandler {
|
||||
documents: Vec<Document>,
|
||||
active_document: usize,
|
||||
mmb_down: bool,
|
||||
translating: bool,
|
||||
rotating: bool,
|
||||
zooming: bool,
|
||||
mouse_pos: ViewportPosition,
|
||||
copy_buffer: Vec<Layer>,
|
||||
}
|
||||
|
|
@ -86,6 +101,35 @@ impl DocumentMessageHandler {
|
|||
// TODO: Add deduplication
|
||||
(!path.is_empty()).then(|| self.handle_folder_changed(path[..path.len() - 1].to_vec())).flatten()
|
||||
}
|
||||
fn layerdata(&self, path: &[LayerId]) -> &LayerData {
|
||||
self.active_document().layer_data.get(path).expect("Layerdata does not exist")
|
||||
}
|
||||
fn layerdata_mut(&mut self, path: &[LayerId]) -> &mut LayerData {
|
||||
self.active_document_mut().layer_data.entry(path.to_vec()).or_insert(LayerData::new(true))
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
fn create_transform_from_layerdata(&self, path: Vec<u64>, responses: &mut VecDeque<Message>) {
|
||||
let layerdata = self.layerdata(&path);
|
||||
responses.push_back(
|
||||
DocumentOperation::SetLayerTransform {
|
||||
path: path,
|
||||
transform: layerdata.calculate_transform().to_cols_array(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
fn create_document_transform_from_layerdata(&self, viewport_size: &ViewportPosition, responses: &mut VecDeque<Message>) {
|
||||
let half_viewport = viewport_size.to_dvec2() / 2.;
|
||||
let layerdata = self.layerdata(&vec![]);
|
||||
let scaled_half_viewport = half_viewport / layerdata.scale;
|
||||
responses.push_back(
|
||||
DocumentOperation::SetLayerTransform {
|
||||
path: vec![],
|
||||
transform: layerdata.calculate_offset_transform(scaled_half_viewport).to_cols_array(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the paths to the selected layers in order
|
||||
fn selected_layers_sorted(&self) -> Vec<Vec<LayerId>> {
|
||||
|
|
@ -119,7 +163,9 @@ impl Default for DocumentMessageHandler {
|
|||
Self {
|
||||
documents: vec![Document::default()],
|
||||
active_document: 0,
|
||||
mmb_down: false,
|
||||
translating: false,
|
||||
rotating: false,
|
||||
zooming: false,
|
||||
mouse_pos: ViewportPosition::default(),
|
||||
copy_buffer: vec![],
|
||||
}
|
||||
|
|
@ -335,22 +381,100 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
.into(),
|
||||
),
|
||||
TranslateDown => {
|
||||
self.mmb_down = true;
|
||||
self.translating = true;
|
||||
self.mouse_pos = ipp.mouse.position;
|
||||
}
|
||||
TranslateUp => {
|
||||
self.mmb_down = false;
|
||||
RotateDown { snap } => {
|
||||
self.rotating = true;
|
||||
let layerdata = self.layerdata_mut(&vec![]);
|
||||
layerdata.snap_rotate = snap;
|
||||
self.mouse_pos = ipp.mouse.position;
|
||||
}
|
||||
ZoomDown => {
|
||||
self.zooming = true;
|
||||
self.mouse_pos = ipp.mouse.position;
|
||||
}
|
||||
TransformUp => {
|
||||
let layerdata = self.layerdata_mut(&vec![]);
|
||||
layerdata.rotation = layerdata.snapped_angle();
|
||||
layerdata.snap_rotate = false;
|
||||
self.translating = false;
|
||||
self.rotating = false;
|
||||
self.zooming = false;
|
||||
}
|
||||
MouseMove => {
|
||||
if self.mmb_down {
|
||||
let delta = DVec2::new(ipp.mouse.position.x as f64 - self.mouse_pos.x as f64, ipp.mouse.position.y as f64 - self.mouse_pos.y as f64);
|
||||
let operation = DocumentOperation::TransformLayer {
|
||||
path: vec![],
|
||||
transform: DAffine2::from_translation(delta).to_cols_array(),
|
||||
};
|
||||
responses.push_back(operation.into());
|
||||
self.mouse_pos = ipp.mouse.position;
|
||||
if self.translating {
|
||||
let delta = ipp.mouse.position.to_dvec2() - self.mouse_pos.to_dvec2();
|
||||
let transformed_delta = self.active_document().document.root.transform.inverse().transform_vector2(delta);
|
||||
|
||||
let layerdata = self.layerdata_mut(&vec![]);
|
||||
layerdata.translation = layerdata.translation + transformed_delta;
|
||||
self.create_document_transform_from_layerdata(&ipp.viewport_size, responses);
|
||||
}
|
||||
if self.rotating {
|
||||
let half_viewport = ipp.viewport_size.to_dvec2() / 2.;
|
||||
let rotation = {
|
||||
let start_vec = self.mouse_pos.to_dvec2() - half_viewport;
|
||||
let end_vec = ipp.mouse.position.to_dvec2() - half_viewport;
|
||||
start_vec.angle_between(end_vec)
|
||||
};
|
||||
|
||||
let layerdata = self.layerdata_mut(&vec![]);
|
||||
layerdata.rotation += rotation;
|
||||
responses.push_back(
|
||||
FrontendMessage::SetRotation {
|
||||
new_radians: layerdata.snapped_angle(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
self.create_document_transform_from_layerdata(&ipp.viewport_size, responses);
|
||||
}
|
||||
if self.zooming {
|
||||
let difference = self.mouse_pos.y as f64 - ipp.mouse.position.y as f64;
|
||||
let amount = 1. + difference / MOUSE_ZOOM_DIVISOR;
|
||||
let layerdata = self.layerdata_mut(&vec![]);
|
||||
layerdata.scale *= amount;
|
||||
responses.push_back(FrontendMessage::SetZoom { new_zoom: layerdata.scale }.into());
|
||||
self.create_document_transform_from_layerdata(&ipp.viewport_size, responses);
|
||||
}
|
||||
self.mouse_pos = ipp.mouse.position;
|
||||
}
|
||||
SetZoom(new) => {
|
||||
let layerdata = self.layerdata_mut(&vec![]);
|
||||
layerdata.scale = new;
|
||||
responses.push_back(FrontendMessage::SetZoom { new_zoom: layerdata.scale }.into());
|
||||
self.create_document_transform_from_layerdata(&ipp.viewport_size, responses);
|
||||
}
|
||||
MultiplyZoom(mult) => {
|
||||
let layerdata = self.layerdata_mut(&vec![]);
|
||||
layerdata.scale *= mult;
|
||||
responses.push_back(FrontendMessage::SetZoom { new_zoom: layerdata.scale }.into());
|
||||
self.create_document_transform_from_layerdata(&ipp.viewport_size, responses);
|
||||
}
|
||||
WheelZoom => {
|
||||
let scroll = ipp.mouse.scroll_delta.y as f64;
|
||||
let amount = if ipp.mouse.scroll_delta.y > 0 {
|
||||
1. + scroll / -WHEEL_ZOOM_DIVISOR
|
||||
} else {
|
||||
1. / (1. + scroll / WHEEL_ZOOM_DIVISOR)
|
||||
};
|
||||
let layerdata = self.layerdata_mut(&vec![]);
|
||||
layerdata.scale *= amount;
|
||||
responses.push_back(FrontendMessage::SetZoom { new_zoom: layerdata.scale }.into());
|
||||
self.create_document_transform_from_layerdata(&ipp.viewport_size, responses);
|
||||
}
|
||||
WheelTranslate => {
|
||||
let delta = -ipp.mouse.scroll_delta.to_dvec2();
|
||||
let transformed_delta = self.active_document().document.root.transform.inverse().transform_vector2(delta);
|
||||
let layerdata = self.layerdata_mut(&vec![]);
|
||||
layerdata.translation += transformed_delta;
|
||||
self.create_document_transform_from_layerdata(&ipp.viewport_size, responses);
|
||||
}
|
||||
SetRotation(new) => {
|
||||
let layerdata = self.layerdata_mut(&vec![]);
|
||||
layerdata.rotation = new;
|
||||
self.create_document_transform_from_layerdata(&ipp.viewport_size, responses);
|
||||
responses.push_back(FrontendMessage::SetRotation { new_radians: new }.into());
|
||||
}
|
||||
NudgeSelectedLayers(x, y) => {
|
||||
let paths: Vec<Vec<LayerId>> = self.selected_layers_sorted();
|
||||
|
|
@ -367,9 +491,9 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
}
|
||||
fn actions(&self) -> ActionList {
|
||||
if self.active_document().layer_data.values().any(|data| data.selected) {
|
||||
actions!(DocumentMessageDiscriminant; Undo, SelectAllLayers, DeselectAllLayers, DeleteSelectedLayers, DuplicateSelectedLayers, RenderDocument, ExportDocument, NewDocument, CloseActiveDocument, NextDocument, PrevDocument, MouseMove, TranslateUp, TranslateDown, CopySelectedLayers, PasteLayers, NudgeSelectedLayers)
|
||||
actions!(DocumentMessageDiscriminant; Undo, SelectAllLayers, DeselectAllLayers, DeleteSelectedLayers, DuplicateSelectedLayers, RenderDocument, ExportDocument, NewDocument, CloseActiveDocument, NextDocument, PrevDocument, MouseMove, TransformUp, TranslateDown, CopySelectedLayers, PasteLayers, NudgeSelectedLayers, RotateDown, ZoomDown, SetZoom, MultiplyZoom, SetRotation, WheelZoom, WheelTranslate)
|
||||
} else {
|
||||
actions!(DocumentMessageDiscriminant; Undo, SelectAllLayers, DeselectAllLayers, RenderDocument, ExportDocument, NewDocument, CloseActiveDocument, NextDocument, PrevDocument, MouseMove, TranslateUp, TranslateDown, PasteLayers)
|
||||
actions!(DocumentMessageDiscriminant; Undo, SelectAllLayers, DeselectAllLayers, RenderDocument, ExportDocument, NewDocument, CloseActiveDocument, NextDocument, PrevDocument, MouseMove, TransformUp, TranslateDown, PasteLayers, RotateDown, ZoomDown, SetZoom, MultiplyZoom, SetRotation, WheelZoom, WheelTranslate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ pub enum FrontendMessage {
|
|||
DisableTextInput,
|
||||
UpdateWorkingColors { primary: Color, secondary: Color },
|
||||
PromptCloseConfirmationModal,
|
||||
SetZoom { new_zoom: f64 },
|
||||
SetRotation { new_radians: f64 },
|
||||
}
|
||||
|
||||
pub struct FrontendMessageHandler {
|
||||
|
|
@ -46,5 +48,7 @@ impl MessageHandler<FrontendMessage, ()> for FrontendMessageHandler {
|
|||
UpdateCanvas,
|
||||
EnableTextInput,
|
||||
DisableTextInput,
|
||||
SetZoom,
|
||||
SetRotation,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use document_core::{layers::LayerDataTypes, LayerId};
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct LayerPanelEntry {
|
||||
pub name: String,
|
||||
pub visible: bool,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use crate::consts::{MINUS_KEY_MULTIPLIER, PLUS_KEY_MULTIPLIER};
|
||||
use crate::message_prelude::*;
|
||||
use crate::tool::ToolType;
|
||||
|
||||
|
|
@ -13,6 +14,7 @@ const SHIFT_NUDGE_AMOUNT: f64 = 10.;
|
|||
#[derive(PartialEq, Clone, Debug)]
|
||||
pub enum InputMapperMessage {
|
||||
PointerMove,
|
||||
MouseScroll,
|
||||
KeyUp(Key),
|
||||
KeyDown(Key),
|
||||
}
|
||||
|
|
@ -62,6 +64,7 @@ struct Mapping {
|
|||
up: [KeyMappingEntries; NUMBER_OF_KEYS],
|
||||
down: [KeyMappingEntries; NUMBER_OF_KEYS],
|
||||
pointer_move: KeyMappingEntries,
|
||||
mouse_scroll: KeyMappingEntries,
|
||||
}
|
||||
|
||||
macro_rules! modifiers {
|
||||
|
|
@ -91,21 +94,23 @@ macro_rules! mapping {
|
|||
let mut up = KeyMappingEntries::key_array();
|
||||
let mut down = KeyMappingEntries::key_array();
|
||||
let mut pointer_move: KeyMappingEntries = Default::default();
|
||||
let mut mouse_scroll: KeyMappingEntries = Default::default();
|
||||
$(
|
||||
let arr = match $entry.trigger {
|
||||
InputMapperMessage::KeyDown(key) => &mut down[key as usize],
|
||||
InputMapperMessage::KeyUp(key) => &mut up[key as usize],
|
||||
InputMapperMessage::PointerMove => &mut pointer_move,
|
||||
InputMapperMessage::MouseScroll => &mut mouse_scroll,
|
||||
};
|
||||
arr.push($entry);
|
||||
)*
|
||||
(up, down, pointer_move)
|
||||
(up, down, pointer_move, mouse_scroll)
|
||||
}};
|
||||
}
|
||||
|
||||
impl Default for Mapping {
|
||||
fn default() -> Self {
|
||||
let (up, down, pointer_move) = mapping![
|
||||
let (up, down, pointer_move, mouse_scroll) = mapping![
|
||||
entry! {action=DocumentMessage::PasteLayers, key_down=KeyV, modifiers=[KeyControl]},
|
||||
// Select
|
||||
entry! {action=SelectMessage::MouseMove, message=InputMapperMessage::PointerMove},
|
||||
|
|
@ -181,8 +186,18 @@ impl Default for Mapping {
|
|||
entry! {action=DocumentMessage::ExportDocument, key_down=KeyS, modifiers=[KeyControl, KeyShift]},
|
||||
entry! {action=DocumentMessage::ExportDocument, key_down=KeyE, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentMessage::MouseMove, message=InputMapperMessage::PointerMove},
|
||||
entry! {action=DocumentMessage::RotateDown{snap:true}, key_down=Mmb, modifiers=[KeyControl, KeyShift]},
|
||||
entry! {action=DocumentMessage::RotateDown{snap:false}, key_down=Mmb, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentMessage::ZoomDown, key_down=Mmb, modifiers=[KeyShift]},
|
||||
entry! {action=DocumentMessage::TranslateDown, key_down=Mmb},
|
||||
entry! {action=DocumentMessage::TranslateUp, key_up=Mmb},
|
||||
entry! {action=DocumentMessage::TransformUp, key_up=Mmb},
|
||||
entry! {action=DocumentMessage::MultiplyZoom(PLUS_KEY_MULTIPLIER), key_down=KeyPlus, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentMessage::MultiplyZoom(PLUS_KEY_MULTIPLIER), key_down=KeyEquals, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentMessage::MultiplyZoom(MINUS_KEY_MULTIPLIER), key_down=KeyMinus, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentMessage::SetZoom(1.), key_down=Key1, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentMessage::SetZoom(2.), key_down=Key2, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentMessage::WheelZoom, message=InputMapperMessage::MouseScroll, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentMessage::WheelTranslate, message=InputMapperMessage::MouseScroll},
|
||||
entry! {action=DocumentMessage::NewDocument, key_down=KeyN, modifiers=[KeyShift]},
|
||||
entry! {action=DocumentMessage::NextDocument, key_down=KeyTab, modifiers=[KeyShift]},
|
||||
entry! {action=DocumentMessage::CloseActiveDocument, key_down=KeyW, modifiers=[KeyShift]},
|
||||
|
|
@ -217,7 +232,7 @@ impl Default for Mapping {
|
|||
entry! {action=GlobalMessage::LogDebug, key_down=Key2},
|
||||
entry! {action=GlobalMessage::LogTrace, key_down=Key3},
|
||||
];
|
||||
Self { up, down, pointer_move }
|
||||
Self { up, down, pointer_move, mouse_scroll }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -228,6 +243,7 @@ impl Mapping {
|
|||
KeyDown(key) => &self.down[key as usize],
|
||||
KeyUp(key) => &self.up[key as usize],
|
||||
PointerMove => &self.pointer_move,
|
||||
MouseScroll => &self.mouse_scroll,
|
||||
};
|
||||
list.match_mapping(keys, actions)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
use std::usize;
|
||||
|
||||
use super::keyboard::{Key, KeyStates};
|
||||
use super::mouse::{MouseKeys, MouseState, ViewportPosition};
|
||||
use super::mouse::{MouseKeys, MouseState, ScrollDelta, ViewportPosition};
|
||||
use crate::message_prelude::*;
|
||||
use bitflags::bitflags;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use document_core::DocumentResponse;
|
||||
use glam::DVec2;
|
||||
|
||||
#[impl_message(Message, InputPreprocessor)]
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
|
|
@ -14,8 +15,10 @@ pub enum InputPreprocessorMessage {
|
|||
MouseDown(MouseState, ModifierKeys),
|
||||
MouseUp(MouseState, ModifierKeys),
|
||||
MouseMove(ViewportPosition, ModifierKeys),
|
||||
MouseScroll(ScrollDelta, ModifierKeys),
|
||||
KeyUp(Key, ModifierKeys),
|
||||
KeyDown(Key, ModifierKeys),
|
||||
ViewportResize(ViewportPosition),
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
|
|
@ -32,6 +35,7 @@ bitflags! {
|
|||
pub struct InputPreprocessor {
|
||||
pub keyboard: KeyStates,
|
||||
pub mouse: MouseState,
|
||||
pub viewport_size: ViewportPosition,
|
||||
}
|
||||
|
||||
enum KeyPosition {
|
||||
|
|
@ -41,32 +45,46 @@ enum KeyPosition {
|
|||
|
||||
impl MessageHandler<InputPreprocessorMessage, ()> for InputPreprocessor {
|
||||
fn process_action(&mut self, message: InputPreprocessorMessage, _data: (), responses: &mut VecDeque<Message>) {
|
||||
let response = match message {
|
||||
match message {
|
||||
InputPreprocessorMessage::MouseMove(pos, modifier_keys) => {
|
||||
self.handle_modifier_keys(modifier_keys, responses);
|
||||
self.mouse.position = pos;
|
||||
InputMapperMessage::PointerMove.into()
|
||||
responses.push_back(InputMapperMessage::PointerMove.into());
|
||||
}
|
||||
InputPreprocessorMessage::MouseScroll(delta, modifier_keys) => {
|
||||
self.handle_modifier_keys(modifier_keys, responses);
|
||||
self.mouse.scroll_delta = delta;
|
||||
responses.push_back(InputMapperMessage::MouseScroll.into());
|
||||
}
|
||||
InputPreprocessorMessage::MouseDown(state, modifier_keys) => {
|
||||
self.handle_modifier_keys(modifier_keys, responses);
|
||||
self.translate_mouse_event(state, KeyPosition::Pressed)
|
||||
responses.push_back(self.translate_mouse_event(state, KeyPosition::Pressed));
|
||||
}
|
||||
InputPreprocessorMessage::MouseUp(state, modifier_keys) => {
|
||||
self.handle_modifier_keys(modifier_keys, responses);
|
||||
self.translate_mouse_event(state, KeyPosition::Released)
|
||||
responses.push_back(self.translate_mouse_event(state, KeyPosition::Released));
|
||||
}
|
||||
InputPreprocessorMessage::KeyDown(key, modifier_keys) => {
|
||||
self.handle_modifier_keys(modifier_keys, responses);
|
||||
self.keyboard.set(key as usize);
|
||||
InputMapperMessage::KeyDown(key).into()
|
||||
responses.push_back(InputMapperMessage::KeyDown(key).into());
|
||||
}
|
||||
InputPreprocessorMessage::KeyUp(key, modifier_keys) => {
|
||||
self.handle_modifier_keys(modifier_keys, responses);
|
||||
self.keyboard.unset(key as usize);
|
||||
InputMapperMessage::KeyUp(key).into()
|
||||
responses.push_back(InputMapperMessage::KeyUp(key).into());
|
||||
}
|
||||
InputPreprocessorMessage::ViewportResize(size) => {
|
||||
responses.push_back(
|
||||
document_core::Operation::TransformLayer {
|
||||
path: vec![],
|
||||
transform: glam::DAffine2::from_translation(DVec2::new((size.x as f64 - self.viewport_size.x as f64) / 2., (size.y as f64 - self.viewport_size.y as f64) / 2.)).to_cols_array(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
self.viewport_size = size;
|
||||
}
|
||||
};
|
||||
responses.push_back(response)
|
||||
}
|
||||
// clean user input and if possible reconstruct it
|
||||
// store the changes in the keyboard if it is a key event
|
||||
|
|
|
|||
|
|
@ -55,6 +55,9 @@ pub enum Key {
|
|||
Key8,
|
||||
Key9,
|
||||
KeyEnter,
|
||||
KeyEquals,
|
||||
KeyMinus,
|
||||
KeyPlus,
|
||||
KeyShift,
|
||||
KeyControl,
|
||||
KeyDelete,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use bitflags::bitflags;
|
||||
use glam::DVec2;
|
||||
|
||||
// origin is top left
|
||||
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq)]
|
||||
|
|
@ -13,12 +14,31 @@ impl ViewportPosition {
|
|||
let y_diff = other.y as i64 - self.y as i64;
|
||||
f64::sqrt((x_diff * x_diff + y_diff * y_diff) as f64)
|
||||
}
|
||||
pub fn to_dvec2(&self) -> DVec2 {
|
||||
DVec2::new(self.x as f64, self.y as f64)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq)]
|
||||
pub struct ScrollDelta {
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
pub z: i32,
|
||||
}
|
||||
impl ScrollDelta {
|
||||
pub fn new(x: i32, y: i32, z: i32) -> ScrollDelta {
|
||||
ScrollDelta { x, y, z }
|
||||
}
|
||||
pub fn to_dvec2(&self) -> DVec2 {
|
||||
DVec2::new(self.x as f64, self.y as f64)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq)]
|
||||
pub struct MouseState {
|
||||
pub position: ViewportPosition,
|
||||
pub mouse_keys: MouseKeys,
|
||||
pub scroll_delta: ScrollDelta,
|
||||
}
|
||||
|
||||
impl MouseState {
|
||||
|
|
@ -30,11 +50,16 @@ impl MouseState {
|
|||
MouseState {
|
||||
position: ViewportPosition { x, y },
|
||||
mouse_keys: MouseKeys::default(),
|
||||
scroll_delta: ScrollDelta::default(),
|
||||
}
|
||||
}
|
||||
pub fn from_u8_pos(keys: u8, position: ViewportPosition) -> Self {
|
||||
let mouse_keys = MouseKeys::from_bits(keys).expect("invalid modifier keys");
|
||||
Self { position, mouse_keys }
|
||||
Self {
|
||||
position,
|
||||
mouse_keys,
|
||||
scroll_delta: ScrollDelta::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
bitflags! {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ mod global;
|
|||
pub mod input;
|
||||
pub mod tool;
|
||||
|
||||
pub mod consts;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use misc::EditorError;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
input::{
|
||||
mouse::{MouseKeys, MouseState, ViewportPosition},
|
||||
mouse::{MouseKeys, MouseState, ScrollDelta, ViewportPosition},
|
||||
InputPreprocessorMessage, ModifierKeys,
|
||||
},
|
||||
message_prelude::{Message, ToolMessage},
|
||||
|
|
@ -47,6 +47,7 @@ impl EditorTestUtils for Editor {
|
|||
self.mouseup(MouseState {
|
||||
position: ViewportPosition { x: x2, y: y2 },
|
||||
mouse_keys: MouseKeys::empty(),
|
||||
scroll_delta: ScrollDelta::default(),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -66,6 +67,7 @@ impl EditorTestUtils for Editor {
|
|||
self.mousedown(MouseState {
|
||||
position: ViewportPosition { x, y },
|
||||
mouse_keys: MouseKeys::LEFT,
|
||||
scroll_delta: ScrollDelta::default(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use crate::input::InputPreprocessor;
|
|||
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
|
||||
use crate::{message_prelude::*, SvgDocument};
|
||||
use document_core::{layers::style, Operation};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use glam::DAffine2;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Pen {
|
||||
|
|
@ -56,7 +56,7 @@ impl Fsm for PenToolFsmState {
|
|||
|
||||
fn transition(self, event: ToolMessage, document: &SvgDocument, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, responses: &mut VecDeque<Message>) -> Self {
|
||||
let transform = document.root.transform;
|
||||
let pos = transform.inverse() * DAffine2::from_translation(DVec2::new(input.mouse.position.x as f64, input.mouse.position.y as f64));
|
||||
let pos = transform.inverse() * DAffine2::from_translation(input.mouse.position.to_dvec2());
|
||||
|
||||
use PenMessage::*;
|
||||
use PenToolFsmState::*;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue