|
@@ -0,0 +1,360 @@
|
|
|
|
+import { message } from "ant-design-vue";
|
|
|
|
+import { ref } from "vue";
|
|
|
|
+import type { SliceImage } from "@/types";
|
|
|
|
+import { useMarkStore } from "@/store";
|
|
|
|
+import {
|
|
|
|
+ getDataUrlForSliceConfig,
|
|
|
|
+ getDataUrlForSplitConfig,
|
|
|
|
+ loadImage,
|
|
|
|
+} from "@/utils/utils";
|
|
|
|
+import EventBus from "@/plugins/eventBus";
|
|
|
|
+
|
|
|
|
+// 计算裁切图和裁切图上的分数轨迹和特殊标记轨迹
|
|
|
|
+export default function useSliceTrack() {
|
|
|
|
+ const emit = defineEmits(["error"]);
|
|
|
|
+
|
|
|
|
+ const markStore = useMarkStore();
|
|
|
|
+
|
|
|
|
+ const rotateBoard = ref(0);
|
|
|
|
+ const sliceImagesWithTrackList = $ref<SliceImage[]>([]);
|
|
|
|
+ const maxSliceWidth = ref(0); // 最大的裁切块宽度,图片容器以此为准
|
|
|
|
+ const theFinalHeight = ref(0); // 最终宽度,用来定位轨迹在第几张图片,不包括image-seperator高度
|
|
|
|
+
|
|
|
|
+ async function processSliceConfig() {
|
|
|
|
+ if (!markStore.currentTask) return;
|
|
|
|
+ const markResult = markStore.currentTask.markResult;
|
|
|
|
+ if (hasMarkResultToRender) {
|
|
|
|
+ // check if have MarkResult for currentTask
|
|
|
|
+ if (!markResult) return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ const images = [];
|
|
|
|
+ // 必须要先加载一遍,把"选择整图"的宽高重置后,再算总高度
|
|
|
|
+ // 错误的搞法,张莹坚持要用
|
|
|
|
+ const sliceNum = markStore.currentTask.sliceUrls.length;
|
|
|
|
+ if (markStore.currentTask.sliceConfig.some((v) => v.i > sliceNum)) {
|
|
|
|
+ console.warn("裁切图设置的数量小于该学生的总图片数量");
|
|
|
|
+ }
|
|
|
|
+ markStore.currentTask.sliceConfig =
|
|
|
|
+ markStore.currentTask.sliceConfig.filter((v) => v.i <= sliceNum);
|
|
|
|
+ for (const sliceConfig of markStore.currentTask.sliceConfig) {
|
|
|
|
+ const url = markStore.currentTask.sliceUrls[sliceConfig.i - 1];
|
|
|
|
+ const image = await loadImage(url);
|
|
|
|
+ images[sliceConfig.i] = image;
|
|
|
|
+ const { x, y, w, h } = sliceConfig;
|
|
|
|
+ x < 0 && (sliceConfig.x = 0);
|
|
|
|
+ y < 0 && (sliceConfig.y = 0);
|
|
|
|
+ if (sliceConfig.w === 0 && sliceConfig.h === 0) {
|
|
|
|
+ // 选择整图时,w/h 为0
|
|
|
|
+ sliceConfig.w = image.naturalWidth;
|
|
|
|
+ sliceConfig.h = image.naturalHeight;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (x <= 1 && y <= 1 && sliceConfig.w <= 1 && sliceConfig.h <= 1) {
|
|
|
|
+ sliceConfig.x = image.naturalWidth * x;
|
|
|
|
+ sliceConfig.y = image.naturalHeight * y;
|
|
|
|
+ sliceConfig.w = image.naturalWidth * w;
|
|
|
|
+ sliceConfig.h = image.naturalHeight * h;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ theFinalHeight.value = markStore.currentTask.sliceConfig
|
|
|
|
+ .map((v) => v.h)
|
|
|
|
+ .reduce((acc, v) => (acc += v));
|
|
|
|
+ maxSliceWidth.value = Math.max(
|
|
|
|
+ ...markStore.currentTask.sliceConfig.map((v) => v.w)
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ // 用来保存sliceImage在整个图片容器中(不包括image-seperator)的高度范围
|
|
|
|
+ let accumTopHeight = 0;
|
|
|
|
+ let accumBottomHeight = 0;
|
|
|
|
+ const trackLists = hasMarkResultToRender
|
|
|
|
+ ? markResult.trackList
|
|
|
|
+ : markStore.currentTask.questionList.map((q) => q.trackList).flat();
|
|
|
|
+
|
|
|
|
+ const tagLists = hasMarkResultToRender
|
|
|
|
+ ? markResult.specialTagList ?? []
|
|
|
|
+ : markStore.currentTask.specialTagList ?? [];
|
|
|
|
+
|
|
|
|
+ const tempSliceImagesWithTrackList: Array<SliceImage> = [];
|
|
|
|
+ for (const sliceConfig of markStore.currentTask.sliceConfig) {
|
|
|
|
+ accumBottomHeight += sliceConfig.h;
|
|
|
|
+ const url = markStore.currentTask.sliceUrls[sliceConfig.i - 1];
|
|
|
|
+ const indexInSliceUrls = sliceConfig.i;
|
|
|
|
+ const image = images[sliceConfig.i];
|
|
|
|
+
|
|
|
|
+ const dataUrl = await getDataUrlForSliceConfig(
|
|
|
|
+ image,
|
|
|
|
+ sliceConfig,
|
|
|
|
+ maxSliceWidth.value,
|
|
|
|
+ url
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ const thisImageTrackList = trackLists.filter(
|
|
|
|
+ (t) => t.offsetIndex === indexInSliceUrls
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ const thisImageTagList = tagLists.filter(
|
|
|
|
+ (t) => t.offsetIndex === indexInSliceUrls
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ const sliceImageRendered = await loadImage(dataUrl);
|
|
|
|
+ tempSliceImagesWithTrackList.push({
|
|
|
|
+ url: dataUrl,
|
|
|
|
+ indexInSliceUrls: sliceConfig.i,
|
|
|
|
+ // 通过positionY来定位是第几张slice的还原,并过滤出相应的track
|
|
|
|
+ trackList: thisImageTrackList.filter(
|
|
|
|
+ (t) =>
|
|
|
|
+ t.positionY >= accumTopHeight / theFinalHeight.value &&
|
|
|
|
+ t.positionY < accumBottomHeight / theFinalHeight.value
|
|
|
|
+ ),
|
|
|
|
+ tagList: thisImageTagList.filter(
|
|
|
|
+ (t) =>
|
|
|
|
+ t.positionY >= accumTopHeight / theFinalHeight.value &&
|
|
|
|
+ t.positionY < accumBottomHeight / theFinalHeight.value
|
|
|
|
+ ),
|
|
|
|
+ // originalImageWidth: image.naturalWidth,
|
|
|
|
+ // originalImageHeight: image.naturalHeight,
|
|
|
|
+ sliceImageWidth: sliceImageRendered.naturalWidth,
|
|
|
|
+ sliceImageHeight: sliceImageRendered.naturalHeight,
|
|
|
|
+ dx: sliceConfig.x,
|
|
|
|
+ dy: sliceConfig.y,
|
|
|
|
+ accumTopHeight,
|
|
|
|
+ effectiveWidth: sliceConfig.w,
|
|
|
|
+ });
|
|
|
|
+ accumTopHeight = accumBottomHeight;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 测试是否所有的track和tag都在待渲染的tempSliceImagesWithTrackList中
|
|
|
|
+ const numOfTrackAndTagInData = trackLists.length + tagLists.length;
|
|
|
|
+ const numOfTrackAndTagInTempSlice = tempSliceImagesWithTrackList
|
|
|
|
+ .map((v) => v.trackList.length + v.tagList.length)
|
|
|
|
+ .reduce((p, c) => p + c);
|
|
|
|
+ if (numOfTrackAndTagInData !== numOfTrackAndTagInTempSlice) {
|
|
|
|
+ console.warn({ tagLists, trackLists, tempSliceImagesWithTrackList });
|
|
|
|
+ void message.warn("渲染轨迹数量与实际数量不一致");
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // console.log("render: ", store.currentTask.secretNumber);
|
|
|
|
+ if (sliceImagesWithTrackList.length === 0) {
|
|
|
|
+ // 初次渲染,不做动画
|
|
|
|
+ sliceImagesWithTrackList.push(...tempSliceImagesWithTrackList);
|
|
|
|
+ // 没抽象好,这里不好做校验
|
|
|
|
+ // const renderedTrackAndTagNumber = sliceImagesWithTrackList.map(s => s.trackList.length + s.tagList.length).reduce((p,c) => p+ c);
|
|
|
|
+ // if(renderedTrackAndTagNumber === thisIma)
|
|
|
|
+ } else {
|
|
|
|
+ rotateBoard.value = 1;
|
|
|
|
+ setTimeout(() => {
|
|
|
|
+ sliceImagesWithTrackList.splice(0);
|
|
|
|
+ sliceImagesWithTrackList.push(...tempSliceImagesWithTrackList);
|
|
|
|
+ setTimeout(() => {
|
|
|
|
+ rotateBoard.value = 0;
|
|
|
|
+ }, 300);
|
|
|
|
+ }, 300);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ async function processSplitConfig() {
|
|
|
|
+ if (!markStore.currentTask) return;
|
|
|
|
+ const markResult = markStore.currentTask.markResult;
|
|
|
|
+ if (hasMarkResultToRender) {
|
|
|
|
+ // check if have MarkResult for currentTask
|
|
|
|
+ if (!markResult) return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ const images = [];
|
|
|
|
+ for (const url of markStore.currentTask.sliceUrls) {
|
|
|
|
+ const image = await loadImage(url);
|
|
|
|
+ images.push(image);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 如果拒绝裁切,则保持整卷
|
|
|
|
+ if (!markStore.setting.enableSplit) {
|
|
|
|
+ markStore.setting.splitConfig = [0, 1];
|
|
|
|
+ }
|
|
|
|
+ // 裁切块,可能是一块,两块,三块... [start, width ...] => [0, 0.3] | [0, 0.55, 0.45, 0.55] | [0, 0.35, 0.33, 0.35, 0.66, 0.35]
|
|
|
|
+ // 要转变为 [[0, 0.3]] | [[0, 0.55], [0.45, 0.55]] | [[0, 0.35], [0.33, 0.35], [0.66, 0.35]]
|
|
|
|
+ const splitConfigPairs = markStore.setting.splitConfig.reduce<
|
|
|
|
+ [number, number][]
|
|
|
|
+ >((a, v, index) => {
|
|
|
|
+ // 偶数位组成数组的第一位,奇数位组成数组的第二位
|
|
|
|
+ index % 2 === 0 ? a.push([v, -1]) : (a.at(-1)![1] = v);
|
|
|
|
+ return a;
|
|
|
|
+ }, []);
|
|
|
|
+
|
|
|
|
+ // 最大的 splitConfig 的宽度
|
|
|
|
+ const maxSplitConfig = Math.max(
|
|
|
|
+ ...markStore.setting.splitConfig.filter((v, i) => i % 2)
|
|
|
|
+ );
|
|
|
|
+ maxSliceWidth.value =
|
|
|
|
+ Math.max(...images.map((v) => v.naturalWidth)) * maxSplitConfig;
|
|
|
|
+
|
|
|
|
+ theFinalHeight.value =
|
|
|
|
+ splitConfigPairs.length *
|
|
|
|
+ images.reduce((acc, v) => (acc += v.naturalHeight), 0);
|
|
|
|
+
|
|
|
|
+ // 高度比宽度大的图片不裁切
|
|
|
|
+ const imagesOfBiggerHeight = images.filter(
|
|
|
|
+ (v) => v.naturalHeight > v.naturalWidth
|
|
|
|
+ );
|
|
|
|
+ if (imagesOfBiggerHeight.length > 0) {
|
|
|
|
+ maxSliceWidth.value = Math.max(
|
|
|
|
+ maxSliceWidth.value,
|
|
|
|
+ ...imagesOfBiggerHeight.map((v) => v.naturalWidth)
|
|
|
|
+ );
|
|
|
|
+ // 不裁切的图剪切多加的高度
|
|
|
|
+ theFinalHeight.value -=
|
|
|
|
+ imagesOfBiggerHeight
|
|
|
|
+ .map((v) => v.naturalHeight)
|
|
|
|
+ .reduce((p, c) => p + c) *
|
|
|
|
+ (splitConfigPairs.length - 1);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ let accumTopHeight = 0;
|
|
|
|
+ let accumBottomHeight = 0;
|
|
|
|
+ const tempSliceImagesWithTrackList: SliceImage[] = [];
|
|
|
|
+ const trackLists = hasMarkResultToRender
|
|
|
|
+ ? markResult.trackList
|
|
|
|
+ : (markStore.currentTask.questionList || [])
|
|
|
|
+ .map((q) => q.trackList)
|
|
|
|
+ .flat();
|
|
|
|
+ const tagLists = hasMarkResultToRender
|
|
|
|
+ ? markResult.specialTagList ?? []
|
|
|
|
+ : markStore.currentTask.specialTagList ?? [];
|
|
|
|
+ for (const url of markStore.currentTask.sliceUrls) {
|
|
|
|
+ for (const config of splitConfigPairs) {
|
|
|
|
+ const indexInSliceUrls =
|
|
|
|
+ markStore.currentTask.sliceUrls.indexOf(url) + 1;
|
|
|
|
+ const image = images[indexInSliceUrls - 1];
|
|
|
|
+ let shouldBreak = false;
|
|
|
|
+ let [splitConfigStart, splitConfigEnd] = config;
|
|
|
|
+ if (image.naturalHeight > image.naturalWidth) {
|
|
|
|
+ splitConfigStart = 0;
|
|
|
|
+ splitConfigEnd = 1;
|
|
|
|
+ shouldBreak = true;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ accumBottomHeight += image.naturalHeight;
|
|
|
|
+
|
|
|
|
+ const dataUrl = await getDataUrlForSplitConfig(
|
|
|
|
+ image,
|
|
|
|
+ [splitConfigStart, splitConfigEnd],
|
|
|
|
+ maxSliceWidth.value,
|
|
|
|
+ url
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ const thisImageTrackList = trackLists.filter(
|
|
|
|
+ (t) => t.offsetIndex === indexInSliceUrls
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ const thisImageTagList = tagLists.filter(
|
|
|
|
+ (t) => t.offsetIndex === indexInSliceUrls
|
|
|
|
+ );
|
|
|
|
+ const sliceImageRendered = await loadImage(dataUrl);
|
|
|
|
+ tempSliceImagesWithTrackList.push({
|
|
|
|
+ url: dataUrl,
|
|
|
|
+ indexInSliceUrls: markStore.currentTask.sliceUrls.indexOf(url) + 1,
|
|
|
|
+ trackList: thisImageTrackList.filter(
|
|
|
|
+ (t) =>
|
|
|
|
+ t.positionY >= accumTopHeight / theFinalHeight.value &&
|
|
|
|
+ t.positionY < accumBottomHeight / theFinalHeight.value
|
|
|
|
+ ),
|
|
|
|
+ tagList: thisImageTagList.filter(
|
|
|
|
+ (t) =>
|
|
|
|
+ t.positionY >= accumTopHeight / theFinalHeight.value &&
|
|
|
|
+ t.positionY < accumBottomHeight / theFinalHeight.value
|
|
|
|
+ ),
|
|
|
|
+ // originalImageWidth: image.naturalWidth,
|
|
|
|
+ // originalImageHeight: image.naturalHeight,
|
|
|
|
+ sliceImageWidth: sliceImageRendered.naturalWidth,
|
|
|
|
+ sliceImageHeight: sliceImageRendered.naturalHeight,
|
|
|
|
+ dx: image.naturalWidth * splitConfigStart,
|
|
|
|
+ dy: 0,
|
|
|
|
+ accumTopHeight,
|
|
|
|
+ effectiveWidth: image.naturalWidth * splitConfigEnd,
|
|
|
|
+ });
|
|
|
|
+ accumTopHeight = accumBottomHeight;
|
|
|
|
+
|
|
|
|
+ // 如果本图高比宽大,不该裁切,则跳过多次裁切
|
|
|
|
+ if (shouldBreak) {
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 测试是否所有的track和tag都在待渲染的tempSliceImagesWithTrackList中
|
|
|
|
+ const numOfTrackAndTagInData = trackLists.length + tagLists.length;
|
|
|
|
+ const numOfTrackAndTagInTempSlice = tempSliceImagesWithTrackList
|
|
|
|
+ .map((v) => v.trackList.length + v.tagList.length)
|
|
|
|
+ .reduce((p, c) => p + c);
|
|
|
|
+ if (numOfTrackAndTagInData !== numOfTrackAndTagInTempSlice) {
|
|
|
|
+ console.warn({ tagLists, trackLists, tempSliceImagesWithTrackList });
|
|
|
|
+ void message.warn("渲染轨迹数量与实际数量不一致");
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ rotateBoard.value = 1;
|
|
|
|
+ addTimeout(() => {
|
|
|
|
+ sliceImagesWithTrackList.splice(0);
|
|
|
|
+ sliceImagesWithTrackList.push(...tempSliceImagesWithTrackList);
|
|
|
|
+ addTimeout(() => {
|
|
|
|
+ rotateBoard.value = 0;
|
|
|
|
+ }, 300);
|
|
|
|
+ }, 300);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // should not render twice at the same time
|
|
|
|
+ const renderPaperAndMark = async () => {
|
|
|
|
+ if (!markStore.currentTask) return;
|
|
|
|
+ if (!markStore.isScanImage) return;
|
|
|
|
+ if (markStore.renderLock) {
|
|
|
|
+ console.log("上个任务还未渲染完毕,稍等一秒再尝试渲染");
|
|
|
|
+ await new Promise((res) => setTimeout(res, 1000));
|
|
|
|
+ await renderPaperAndMark();
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+ // check if have MarkResult for currentTask
|
|
|
|
+ const markResult = markStore.currentTask.markResult;
|
|
|
|
+ if (hasMarkResultToRender && !markResult) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ markStore.renderLock = true;
|
|
|
|
+ try {
|
|
|
|
+ markStore.globalMask = true;
|
|
|
|
+
|
|
|
|
+ const hasSliceConfig = markStore.currentTask.sliceConfig?.length;
|
|
|
|
+
|
|
|
|
+ if (hasSliceConfig) {
|
|
|
|
+ await processSliceConfig();
|
|
|
|
+ } else {
|
|
|
|
+ await processSplitConfig();
|
|
|
|
+ }
|
|
|
|
+ } catch (error) {
|
|
|
|
+ sliceImagesWithTrackList.splice(0);
|
|
|
|
+ console.trace("render error ", error);
|
|
|
|
+ // 图片加载出错,自动加载下一个任务
|
|
|
|
+ emit("error");
|
|
|
|
+ } finally {
|
|
|
|
+ markStore.renderLock = false;
|
|
|
|
+ markStore.globalMask = false;
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ // 在阻止渲染的情况下,watchEffect收集不到 store.currentTask 的依赖,会导致本组件不再更新
|
|
|
|
+ watch(
|
|
|
|
+ () => markStore.currentTask,
|
|
|
|
+ () => {
|
|
|
|
+ setTimeout(renderPaperAndMark, 50);
|
|
|
|
+ }
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ watch(
|
|
|
|
+ () => sliceImagesWithTrackList,
|
|
|
|
+ () => {
|
|
|
|
+ EventBus.emit("draw-change", sliceImagesWithTrackList);
|
|
|
|
+ },
|
|
|
|
+ { deep: true }
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ return { maxSliceWidth, theFinalHeight, sliceImagesWithTrackList };
|
|
|
|
+}
|