feat: add rect select but have some problems

This commit is contained in:
SikongJueluo 2025-06-11 21:25:15 +08:00
parent b6fb7e05fa
commit f340c86a41
No known key found for this signature in database
6 changed files with 298 additions and 64 deletions

View File

@ -1,7 +1,7 @@
import { ref, reactive, watchPostEffect } from 'vue'
import { defineStore } from 'pinia'
import { isString, toNumber } from 'lodash';
import { Common } from '@/Common';
import { Common } from '@/utils/Common';
import z from "zod"
import { isNumber } from 'mathjs';
import { JtagClient, MatrixKeyClient, PowerClient } from "@/APIClient";

View File

@ -1,4 +1,4 @@
import { type FileParameter } from "./APIClient";
import { type FileParameter } from "@/APIClient";
import { isNull, isUndefined } from "lodash";
export namespace Common {

30
src/utils/VueKonvaType.ts Normal file
View File

@ -0,0 +1,30 @@
import Konva from "konva";
import type { VueElement } from "vue";
interface VNode extends VueElement {
getNode(): Konva.Node
}
interface VLayer extends VueElement {
getNode(): Konva.Layer
}
interface VGroup extends VueElement {
getNode(): Konva.Group
}
interface VStage extends VueElement {
getStage(): Konva.Stage
}
interface VTransformer extends VueElement {
getNode(): Konva.Transformer
}
export type {
VNode,
VLayer,
VGroup,
VStage,
VTransformer,
}

View File

@ -115,7 +115,7 @@ import { isNull, isUndefined } from "lodash";
import { ref } from "vue";
import { RemoteUpdateClient } from "@/APIClient";
import { useDialogStore } from "@/stores/dialog";
import { Common } from "@/Common";
import { Common } from "@/utils/Common";
const dialog = useDialogStore();

View File

@ -1,13 +1,14 @@
<template>
<div class="h-screen w-screen">
<v-stage class="h-full w-full" ref="stage" :config="stageSize">
<v-stage class="h-full w-full" ref="stage" :config="stageSize" @mousedown="handleMouseDown"
@mousemove="handleMouseMove" @mouseup="handleMouseUp" @click="handleStageClick">
<v-layer ref="layer">
<v-group ref="group">
<v-star v-for="item in list" :key="item.id" :config="{
x: item.x,
y: item.y,
rotation: item.rotation,
id: item.id,
draggable: true,
numPoints: 5,
innerRadius: 30,
outerRadius: 50,
@ -18,8 +19,15 @@
shadowOpacity: 0.6,
scaleX: item.scale,
scaleY: item.scale,
}" @dragstart="handleDragStart" @dragend="handleDragEnd" />
<v-transformer ref="transformer" />
<v-rect ref="selectRect" v-if="selectionRectangle.visible" :config="{
x: Math.min(selectionRectangle.x1, selectionRectangle.x2),
y: Math.min(selectionRectangle.y1, selectionRectangle.y2),
width: Math.abs(selectionRectangle.x2 - selectionRectangle.x1),
height: Math.abs(selectionRectangle.y2 - selectionRectangle.y1),
fill: '#0069FF88',
}" />
</v-group>
</v-layer>
</v-stage>
<div class="absolute top-20 left-10">
@ -31,54 +39,24 @@
<script setup lang="ts">
import { isNull, isUndefined } from "mathjs";
import { ref, onMounted, useTemplateRef, type HTMLAttributes } from "vue";
type CanvasObject = {
id: string;
x: number;
y: number;
rotation: number;
scale: number;
};
import Konva from "konva";
import type {
VGroup,
VLayer,
VNode,
VStage,
VTransformer,
} from "@/utils/VueKonvaType";
import { ref, reactive, watch, onMounted, useTemplateRef } from "vue";
const stageSize = {
width: window.innerWidth,
height: window.innerHeight,
};
const list = ref<CanvasObject[]>([]);
const group = useTemplateRef<HTMLAttributes>("group");
const dragItemId = ref(null);
function handleCacheChange(e: Event) {
const target = e.target as HTMLInputElement;
const shouldCache = isNull(target) ? false : target.checked;
if (shouldCache) {
group.getNode().cache();
} else {
group.getNode().clearCache();
}
}
function handleDragstart(e: Event) {
// save drag element:
const target = e.target as HTMLInputElement;
dragItemId.value = e.target?.id();
// move current element to the top:
const item = list.value.find((i) => i.id === dragItemId.value);
if (isUndefined(item)) return;
const index = list.value.indexOf(item);
list.value.splice(index, 1);
list.value.push(item);
}
function handleDragend() {
dragItemId.value = null;
}
const list = ref<Konva.NodeConfig[]>([]);
onMounted(() => {
for (let n = 0; n < 30; n++) {
for (let n = 0; n < 100; n++) {
list.value.push({
id: Math.round(Math.random() * 10000).toString(),
x: Math.random() * stageSize.width,
@ -88,6 +66,232 @@ onMounted(() => {
});
}
});
const layer = useTemplateRef<VLayer>("layer");
const transformer = useTemplateRef<VTransformer>("transformer");
const selectRect = useTemplateRef<VNode>("selectRect");
const stage = useTemplateRef<VStage>("stage");
const isDragging = ref(false);
const dragItemId = ref<string | null>(null);
const isSelecting = ref(false);
const selectedIds = ref<string[]>([]);
const selectionRectangle = reactive({
visible: false,
x1: 0,
y1: 0,
x2: 0,
y2: 0,
});
function degToRad(angle: number) {
return (angle / 180) * Math.PI;
}
function getCorner(
pivotX: number,
pivotY: number,
diffX: number,
diffY: number,
angle: number,
) {
const distance = Math.sqrt(diffX * diffX + diffY * diffY);
angle += Math.atan2(diffY, diffX);
const x = pivotX + distance * Math.cos(angle);
const y = pivotY + distance * Math.sin(angle);
return { x, y };
}
function getClientRect(element: Konva.Node) {
const { x, y, width, height, rotation } = element;
const rad = degToRad(rotation() ?? 0);
const p1 = getCorner(x(), y(), 0, 0, rad);
const p2 = getCorner(x(), y(), width(), 0, rad);
const p3 = getCorner(x(), y(), width(), height(), rad);
const p4 = getCorner(x(), y(), 0, height(), rad);
const minX = Math.min(p1.x, p2.x, p3.x, p4.x);
const minY = Math.min(p1.y, p2.y, p3.y, p4.y);
const maxX = Math.max(p1.x, p2.x, p3.x, p4.x);
const maxY = Math.max(p1.y, p2.y, p3.y, p4.y);
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
};
}
function handleCacheChange(e: Event) {
const target = e.target as HTMLInputElement;
const shouldCache = isNull(target) ? false : target.checked;
if (isNull(layer.value)) return;
if (shouldCache) {
layer.value.getNode().cache();
} else {
layer.value.getNode().clearCache();
}
}
// Drag event handlers
function handleDragStart(e: Event) {
isDragging.value = true;
// save drag element:
const target = e.target as unknown as Konva.Node;
dragItemId.value = target.id();
// move current element to the top:
const item = list.value.find((i) => i.id === dragItemId.value);
if (isUndefined(item)) return;
const index = list.value.indexOf(item);
list.value.splice(index, 1);
list.value.push(item);
}
function handleDragEnd() {
isDragging.value = false;
dragItemId.value = null;
}
// Update transformer nodes when selection changes
watch(selectedIds, () => {
if (!transformer.value) return;
const nodes = selectedIds.value
.map((id) => {
return layer.value
?.getNode()
.find((ref: Konva.Node) => ref.attrs.id === id);
})
.flat()
.filter(Boolean) as Konva.Node[];
if (!isUndefined(nodes)) transformer.value.getNode().nodes(nodes);
});
// Mouse event handlers
function handleStageClick(e: Event) {
if (isNull(e.target)) return;
const target = e.target as unknown as Konva.Container;
// if we are selecting with rect, do nothing
if (selectionRectangle.visible) {
return;
}
// if click on empty area - remove all selections
if ((e.target as unknown) === target.getStage()) {
selectedIds.value = [];
return;
}
// do nothing if clicked NOT on our rectangles
if (!target.hasName("rect")) {
return;
}
const clickedId = target.attrs.id;
// do we pressed shift or ctrl?
const metaPressed =
(e as any).evt?.shiftKey ||
(e as any).evt?.ctrlKey ||
(e as any).evt?.metaKey;
const isSelected = selectedIds.value.includes(clickedId);
if (!metaPressed && !isSelected) {
// if no key pressed and the node is not selected
// select just one
selectedIds.value = [clickedId];
} else if (metaPressed && isSelected) {
// if we pressed keys and node was selected
// we need to remove it from selection:
selectedIds.value = selectedIds.value.filter((id) => id !== clickedId);
} else if (metaPressed && !isSelected) {
// add the node into selection
selectedIds.value = [...selectedIds.value, clickedId];
}
}
function handleMouseDown(e: Event) {
if (isNull(e.target)) return;
const target = e.target as unknown as Konva.Container;
// do nothing if we mousedown on any shape
if ((e.target as unknown) !== target.getStage()) {
return;
}
// start selection rectangle
isSelecting.value = true;
const pos = target.getStage()?.getPointerPosition();
if (!isNull(pos) && !isUndefined(pos)) {
selectionRectangle.visible = true;
selectionRectangle.x1 = pos.x;
selectionRectangle.y1 = pos.y;
selectionRectangle.x2 = pos.x;
selectionRectangle.y2 = pos.y;
}
}
function handleMouseMove(e: Event) {
if (isNull(e.target)) return;
const target = e.target as unknown as Konva.Container;
// do nothing if we didn't start selection
if (!isSelecting.value) {
return;
}
const pos = target.getStage()?.getPointerPosition();
if (!isNull(pos) && !isUndefined(pos)) {
selectionRectangle.x2 = pos.x;
selectionRectangle.y2 = pos.y;
}
}
function handleMouseUp() {
// do nothing if we didn't start selection
if (!isSelecting.value) {
return;
}
isSelecting.value = false;
// update visibility in timeout, so we can check it in click event
setTimeout(() => {
selectionRectangle.visible = false;
});
const selBox = {
x: Math.min(selectionRectangle.x1, selectionRectangle.x2),
y: Math.min(selectionRectangle.y1, selectionRectangle.y2),
width: Math.abs(selectionRectangle.x2 - selectionRectangle.x1),
height: Math.abs(selectionRectangle.y2 - selectionRectangle.y1),
};
if (isNull(layer.value)) return;
const selected = layer.value.getNode().children.filter((node: Konva.Node) => {
// Check if rectangle intersects with selection box
if (
node === transformer.value?.getNode() ||
node === selectRect.value?.getNode()
)
return false;
return Konva.Util.haveIntersection(selBox, node.getClientRect());
});
selectedIds.value = selected.map((node: Konva.Node) => node.id());
}
</script>
<style>

View File

@ -44,7 +44,7 @@
import { JtagClient, type FileParameter } from "@/APIClient";
import UploadCard from "@/components/UploadCard.vue";
import { useDialogStore } from "@/stores/dialog";
import { Common } from "@/Common";
import { Common } from "@/utils/Common";
import { toNumber, isUndefined } from "lodash";
import { ref } from "vue";