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:
0HyperCube
2021-07-13 07:50:10 +01:00
committed by GitHub
parent 8ff545f01c
commit 39e6893630
22 changed files with 442 additions and 73 deletions

View File

@@ -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);
},

View File

@@ -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);
},
},
});