Bläddra i källkod

feat: 修复textarea文字对应不上textShape

bill 6 månader sedan
förälder
incheckning
2457f2886c

+ 1 - 0
package.json

@@ -14,6 +14,7 @@
     "@types/node": "^22.9.0",
     "@types/three": "^0.169.0",
     "element-plus": "^2.8.6",
+    "html2canvas": "^1.4.1",
     "konva": "^9.3.16",
     "localforage": "^1.10.0",
     "mitt": "^3.0.1",

+ 33 - 0
pnpm-lock.yaml

@@ -7,6 +7,7 @@ specifiers:
   '@types/three': ^0.169.0
   '@vitejs/plugin-vue': ^5.1.4
   element-plus: ^2.8.6
+  html2canvas: ^1.4.1
   konva: ^9.3.16
   localforage: ^1.10.0
   mitt: ^3.0.1
@@ -29,6 +30,7 @@ dependencies:
   '@types/node': 22.9.0
   '@types/three': 0.169.0
   element-plus: 2.8.6_vue@3.5.13
+  html2canvas: 1.4.1
   konva: 9.3.16
   localforage: 1.10.0
   mitt: 3.0.1
@@ -832,6 +834,11 @@ packages:
   /balanced-match/1.0.2:
     resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
 
+  /base64-arraybuffer/1.0.2:
+    resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
+    engines: {node: '>= 0.6.0'}
+    dev: false
+
   /boolbase/1.0.0:
     resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
     dev: false
@@ -929,6 +936,12 @@ packages:
     resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==}
     dev: false
 
+  /css-line-break/2.1.0:
+    resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==}
+    dependencies:
+      utrie: 1.0.2
+    dev: false
+
   /css-select/4.3.0:
     resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==}
     dependencies:
@@ -1164,6 +1177,14 @@ packages:
       terser: 5.36.0
     dev: false
 
+  /html2canvas/1.4.1:
+    resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==}
+    engines: {node: '>=8.0.0'}
+    dependencies:
+      css-line-break: 2.1.0
+      text-segmentation: 1.0.3
+    dev: false
+
   /immediate/3.0.6:
     resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
     dev: false
@@ -1704,6 +1725,12 @@ packages:
       source-map-support: 0.5.21
     dev: false
 
+  /text-segmentation/1.0.3:
+    resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==}
+    dependencies:
+      utrie: 1.0.2
+    dev: false
+
   /three/0.169.0:
     resolution: {integrity: sha512-Ed906MA3dR4TS5riErd4QBsRGPcx+HBDX2O5yYE5GqJeFQTPU+M56Va/f/Oph9X7uZo3W3o4l2ZhBZ6f6qUv0w==}
     dev: false
@@ -1730,6 +1757,12 @@ packages:
     engines: {node: '>= 10.0.0'}
     dev: false
 
+  /utrie/1.0.2:
+    resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
+    dependencies:
+      base64-arraybuffer: 1.0.2
+    dev: false
+
   /uuid/11.0.2:
     resolution: {integrity: sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ==}
     hasBin: true

+ 46 - 0
src/core/components/share/shape-dom.vue

@@ -0,0 +1,46 @@
+<template>
+  <Teleport :to="`#${DomMountId}`">
+    <div class="rep-dom" ref="dom">
+      <slot name="dom" />
+    </div>
+  </Teleport>
+  <v-rect></v-rect>
+</template>
+<script lang="ts" setup>
+import { DomMountId } from "@/constant";
+import { useOnComponentBoundChange } from "@/core/hook/use-component";
+import { DC, EntityShape } from "@/deconstruction";
+import { ref, watch } from "vue";
+
+const props = withDefaults(
+  defineProps<{
+    shape: DC<EntityShape> | undefined;
+    show?: boolean;
+  }>(),
+  { show: true }
+);
+const onChange = useOnComponentBoundChange();
+const dom = ref<HTMLDivElement>();
+const update = () => {
+  const $shape = props.shape!.getNode();
+  const mat = $shape.getAbsoluteTransform();
+  dom.value!.style.transform = `matrix(${mat.m.join(",")})`;
+};
+
+watch(
+  () => ({ shape: props.shape?.getNode(), show: props.show }),
+  ({ shape, show }, _, onCleanup) => {
+    if (shape && show) {
+      onCleanup(onChange.on(shape, update));
+    }
+  }
+);
+</script>
+
+<style lang="scss" scoped>
+.rep-dom {
+  position: absolute;
+  transform-origin: left top;
+  pointer-events: none;
+}
+</style>

+ 1 - 0
src/core/components/text/index.ts

@@ -26,6 +26,7 @@ export type TextData = Partial<typeof defaultStyle> & BaseItem & {
   mat: number[]
   content: string
   width?: number
+  heihgt?: number
 };
 
 

+ 3 - 2
src/core/components/text/temp-text.vue

@@ -1,12 +1,13 @@
 <template>
+  <!-- wrap: 'char',
+   -->
   <v-text
     :config="{
       ...data,
       ...matConfig,
-      wrap: 'char',
-      verticalAlign: 'center',
       text: data.content,
       zIndex: undefined,
+      verticalAlign: 'center',
       opacity: addMode ? 0.3 : data.opacity,
     }"
     ref="shape"

+ 6 - 4
src/core/components/text/text-dom.vue

@@ -61,14 +61,14 @@ const refreshMat = () => {
 
 const refreshSize = () => {
   const dom = textarea.value!;
-  let newWidth = props.shape.width();
+  let newWidth = props.shape.width() - props.shape.padding() * 2;
   if (isSafari || isFirefox) {
     newWidth = Math.ceil(newWidth);
   } else if (isEdge) {
     newWidth += 1;
   }
   dom.style.width = newWidth + "px";
-  dom.style.height = props.shape.height() + props.shape.fontSize() / 2 + "px";
+  dom.style.height = props.shape.height() - props.shape.padding() * 2 + 5 + "px";
 };
 
 const getPointerTextNdx = useGetPointerTextNdx();
@@ -85,6 +85,7 @@ const styles = computed(() => {
     fontSize: shape.fontSize() + "px",
     lineHeight: shape.lineHeight(),
     fontFamily: shape.fontFamily(),
+    letterSpacing: shape.letterSpacing() + "px",
     textAlign: shape.align() as CanvasTextAlign,
     color: shape.fill().toString(),
   };
@@ -163,9 +164,10 @@ textarea {
   resize: none;
   pointer-events: all;
   outline: none;
-  word-break: break-all;
-  overflow-wrap: break-word;
+  // word-break: break-all;
+  // overflow-wrap: break-word;
   transform-origin: left top;
+  color: rgba(255, 0, 0, 0.8) !important;
 }
 
 div {

+ 0 - 2
src/core/components/text/text.vue

@@ -163,5 +163,3 @@ const quitHandler = (val: string) => {
   }
 };
 </script>
-
-<style scoped lang="scss"></style>

+ 12 - 32
src/core/helper/active-boxs.vue

@@ -14,15 +14,15 @@
 </template>
 
 <script lang="ts" setup>
-import { nextTick, reactive, ref, watch, watchEffect } from "vue";
+import { reactive, ref, watchEffect } from "vue";
 import { useMouseShapesStatus } from "../hook/use-mouse-status";
 import { IRect } from "konva/lib/types";
-import { useViewer } from "../hook/use-viewer";
 import { DC, EntityShape } from "@/deconstruction";
 import { themeColor } from "@/constant/help-style";
 import { Rect } from "konva/lib/shapes/Rect";
 import { useDashAnimation } from "../hook/use-animation";
-import { useGetComponentData } from "../hook/use-component";
+import { useOnComponentBoundChange } from "../hook/use-component";
+import { mergeFuns } from "@/utils/shared";
 
 const status = useMouseShapesStatus();
 const boxs = reactive(new WeakMap<EntityShape, IRect>());
@@ -38,40 +38,20 @@ const updateBox = ($shape: EntityShape) => {
   });
 };
 
-type OnCleanup = (cleanupFn: () => void) => void;
-
-const getComponentData = useGetComponentData();
-const shapeListener = (shape: EntityShape, onCleanup: OnCleanup) => {
-  const update = async () => {
-    await nextTick();
-    updateBox(shape);
-  };
-
-  const repShape = shape.repShape || shape;
-  repShape.on("transform", update);
-  shape.on("bound-change", update);
-
-  watch(() => getComponentData(shape).value, update);
-  update();
-  onCleanup(() => {
-    repShape.off("transform", update);
-    shape.off("bound-change", update);
-  });
-};
-
+const { on } = useOnComponentBoundChange();
 watchEffect(
   (onCleanup) => {
-    status.actives.forEach((shape) => shapeListener(shape, onCleanup));
+    onCleanup(
+      mergeFuns(
+        status.actives.map((shape) => {
+          updateBox(shape);
+          return on(shape, () => updateBox(shape));
+        })
+      )
+    );
   },
   { flush: "pre" }
 );
-
-watch(useViewer().transform, () => {
-  for (const $shape of status.actives) {
-    updateBox($shape);
-  }
-});
-
 // animation
 const rects = ref<DC<Rect>[]>([]);
 useDashAnimation(rects);

+ 54 - 0
src/core/hook/use-component.ts

@@ -4,11 +4,14 @@ import {
   EmitFn,
   isRef,
   markRaw,
+  nextTick,
+  onUnmounted,
   reactive,
   Ref,
   ref,
   shallowReactive,
   shallowRef,
+  watch,
   watchEffect,
 } from "vue";
 import { useAutomaticData } from "./use-automatic-data";
@@ -38,6 +41,8 @@ import { mergeDescribes, PropertyKeys } from "../propertys";
 import { useStore } from "../store";
 import { globalWatch, useStage } from "./use-global-vars";
 import { useAlignmentShape } from "./use-alignment";
+import { useViewer, useViewerTransform } from "./use-viewer";
+import { usePause } from "./use-pause";
 
 type Emit<T> = EmitFn<{
   updateShape: (value: T) => void;
@@ -303,3 +308,52 @@ export const useComponentsAttach = <T>(
     cleanup: mergeFuns(cleanups),
   };
 };
+
+export const useOnComponentBoundChange = () => {
+  const getComponentData = useGetComponentData();
+  const transform = useViewerTransform()
+  const quitHooks = [] as Array<() => void>;
+  const destory = () => mergeFuns(quitHooks)();
+
+  const on = <T extends EntityShape>(
+    shape: Ref<T | undefined> | T | undefined,
+    callback: (shape: T, type: 'transform' | 'data') => void
+  ) => {
+    const $shape = computed(() => (shape = isRef(shape) ? shape.value : shape));
+    let repShape: T | undefined;
+    const item = getComponentData($shape);
+    const update = (type?: 'transform' | 'data') => {
+      $shape.value && !api.isPause && callback(repShape || $shape.value, type || 'data');
+    };
+    const sync = () => update()
+    const shapeListener = (shape: T) => {
+      repShape = shape.repShape as T || shape;
+      repShape.on("transform", sync);
+      shape.on("bound-change", sync);
+      return () => {
+        repShape!.off("transform", sync);
+        shape.off("bound-change", sync);
+      };
+    };
+
+    const onDestroy = mergeFuns([
+      watch(item, () => nextTick(() => update('data')), { deep: true }),
+      watch(transform, () => update('transform')),
+      watch($shape, (shape, _, onCleanup) => {
+        if (!shape) return;
+        onCleanup(shapeListener(shape));
+      }, {immediate: true}),
+    ])
+    quitHooks.push(onDestroy);
+
+    return () => {
+      const ndx = quitHooks.indexOf(onDestroy) 
+      ~ndx && quitHooks.splice(ndx, 1)
+      onDestroy
+    }
+  };
+
+  const api = usePause({ destory, on });
+  onUnmounted(destory)
+  return api
+};

+ 2 - 1
src/core/hook/use-mouse-status.ts

@@ -143,6 +143,7 @@ export const useMouseShapesStatus = installGlobalVar(() => {
 
     stage.on("pointerdown.mouse-status", (ev) => {
       if (prevent.value) return;
+      console.log(ev.evt.button)
       downTime = Date.now();
       const target = shapeTreeContain(listeners.value, ev.target);
       if (target && !press.value.includes(target)) {
@@ -157,7 +158,7 @@ export const useMouseShapesStatus = installGlobalVar(() => {
       listener(
         stage.container().parentElement as HTMLDivElement,
         "pointerup",
-        () => {
+        (ev) => {
           if (prevent.value) return;
           press.value = [];
           if (Date.now() - downTime >= 300) return;

+ 2 - 1
src/core/hook/use-transformer.ts

@@ -396,7 +396,8 @@ export const useShapeTransformer = <T extends EntityShape>(
           }
           transformer.off("pointerdown.shapemer", downHandler);
         });
-      }
+      },
+      {immediate: true}
     );
 
     return () => {

+ 4 - 3
src/core/renderer/renderer.vue

@@ -1,8 +1,8 @@
 <template>
   <div class="draw-layout" @contextmenu.prevent :style="{ cursor: cursorStyle }">
-    <div class="mount-mask" :id="DomMountId" />
+    <div class="mount-mask" :id="DomMountId" ref="maskMount" />
 
-    <v-stage ref="stage" :config="size">
+    <v-stage ref="stage" :config="size" v-if="maskMount">
       <v-layer :config="viewerConfig" id="formal">
         <!--	不可去除,去除后移动端拖拽会有溢出	-->
         <BackGrid v-if="expose.config.showGrid" />
@@ -44,7 +44,7 @@ import { useAutoService, useExpose } from "../hook/use-expose.ts";
 import { DomMountId } from "../../constant";
 import { useStore } from "../store/index.ts";
 import { Mode } from "@/constant/mode.ts";
-import { computed, getCurrentInstance } from "vue";
+import { computed, getCurrentInstance, ref } from "vue";
 import { install } from "../../install-lib.ts";
 
 const instance = getCurrentInstance();
@@ -58,6 +58,7 @@ useAutoService();
 
 const stage = useStage();
 const size = useResize();
+const maskMount = ref<HTMLDivElement>();
 const viewerConfig = useViewerTransformConfig();
 const types = Object.keys(components) as ShapeType[];
 const mode = useMode();

+ 1 - 1
src/example/fuse/views/slide/menu.ts

@@ -118,6 +118,6 @@ export const menus: MenuItem[] = [
   },
   {
     icon: "",
-    ...genItem("text"),
+    ...genItem("text", { width: 200, height: 100, content: "文本" }),
   },
 ];