同时从两个不同的组件中拖动两个元素时出现苗条滞后

问题描述

我一直在尝试构建一个简单的 svg 编辑器来试用 svelte。它一直很好,直到我为元素构建了一个选择框,以便与活动的选定元素一起拖动。 拖动所选元素时,选择框落后于元素本身。我不确定出了什么问题。

我尝试了一些方法,例如使用 store 传递位置数据并将事件放在父元素上,以便所有内容都在同一个组件上进行计算,以防万一这可能是问题,但仍然无法正常工作。我不确定我做错了什么。 我一直在尝试解决这个问题,但不知道可能是什么问题。

select box lagging behind element

您可以在此处查看我的代码和框简化演示: codesandbox.io

<script lang="ts">
    import ImageViewer from "../ImageViewer/ImageViewer.svelte";
    import EditorControls from "../EditorControls/EditorControls.svelte";

    import { app_data,app_state } from "../../stores";
    import {
        getBoundingBox,convertGlobalToLocalPosition,} from "../../helpers/svg";
    import { elementData,elementDataRect } from "../../helpers/variables";

    import { mousePointerLocation } from "../../helpers/mouse";

    let activeElement = {
        i: 0,bBox: {
            x: 0,y: 0,width: 0,height: 0,},active: false,};
    let elements = [{
        type: 'rect',x: 100,y: 100,width: 400,height: 280,active: true,fill: 'rgba(0,1)',stroke: 'rgba(0,1)'
    }];
    let app_data_value;
    const unsub_app_data = app_data.subscribe((value) => {
        app_data_value = value;
    });

    let pos = elementData;
    let posRect = elementDataRect;

    let strokeWidth = 15;

    let app_state_value;
    const unsub_app_state = app_state.subscribe((value) => {
        app_state_value = { ...value };
    });

    let moving = app_state_value.action === "move" ? true : false;
    let movePos;
    let active = false;

    const elementMoveDownHandler = (e) => {
        if (
            (e.button === 0 || e.button === -1) &&
            app_data_value.tool.selected === "select"
        ) {
            active = true;
            let i = (e.target as SVGElement).getAttribute("data-obj-id");
            if (!moving) {
                e.target.setPointerCapture(e.pointerId);
                let cursorpt: any = mousePointerLocation(e);
                let bBox: any;
                bBox = getBoundingBox(e.target);
                const offset = {
                    x: e.clientX - bBox.left,y: e.clientY - bBox.top,};

                movePos = {
                    ...movePos,init_x: cursorpt.x,init_y: cursorpt.y,offset: {
                        x: offset.x,y: offset.y,type: app_data_value.tool.selected,};

                let pt = convertGlobalToLocalPosition(e.target);
                activeElement = {
                    ...activeElement,i: parseInt(i),bBox: {
                        x: pt.x,y: pt.y,width: bBox.width,height: bBox.height,};

                moving = true;
            }
        }
    };
    const elementMoveMoveHandler = (e) => {
        if (
            e.button === 0 ||
            (e.button === -1 && app_data_value.tool.selected === "select")
        ) {
            active = true;
            let i = (e.target as SVGElement).getAttribute("data-obj-id");
            if (moving) {
                let cursorpt: any;
                let bBox: any;
                bBox = getBoundingBox(e.target);
                cursorpt = mousePointerLocation(e);
                const offset = {
                    x: e.clientX - bBox.left,};
                let j;
                switch (e.target.nodeName) {
                    case "rect":
                        j = [...elements]

                        j[i]["x"] =
                            elements[i]["x"] - (movePos.offset.x - offset.x);
                        j[i]["y"] =
                            elements[i]["y"] - (movePos.offset.y - offset.y);
                        elements = j;
                        break;
                    default:
                        break;
                }
                // elements = elements;
                movePos = {
                    ...movePos,move_x: cursorpt.x,move_y: cursorpt.y,};
                // activeElement = activeElement;
                app_state.update((j) => {
                    j.action = "move";
                    return j;
                });
            }
        }
    };
    const elementMoveUpHandler = (e) => {
        moving = false;
        app_state.update((j) => {
            j.action = "standby";
            return j;
        });
        e.target.releasePointerCapture(e.pointerId);
    };
</script>

<div
    on:pointerdown={(e) => {
        elementMoveDownHandler(e);
    }}
    on:pointerup={(e) => {
        if (active) {
            elementMoveUpHandler(e);
        }
    }}
    on:pointermove={(e) => {
        if (active) {
            elementMoveMoveHandler(e);
        }
    }}
>
    <ImageViewer {strokeWidth} {elements} />
    <EditorControls {pos} {posRect} {activeElement} />
</div>

<style lang="scss">
    @import "./SVGEditor.scss";
</style>
<script lang="">
    import { app_data } from "../../stores";

    export let strokeWidth;
    export let elements;

    let app_data_value;
    const unsub_app_data = app_data.subscribe((value) => {
        app_data_value = value;
    });

</script>

<svg
    id="image-viewer"
    width={app_data_value.doc_size.width}
    height={app_data_value.doc_size.height}
>
    {#each elements as item,i}
        {#if typeof item === "object"}
            {#if "type" in item}
                {#if item.type === "line"}
                    {#if "x1" in item && "y1" in item && "x2" in item && "y2" in item}
                        <line
                            x1={item.x1}
                            y1={item.y1}
                            x2={item.x2}
                            y2={item.y2}
                            stroke="black"
                            stroke-width={strokeWidth}
                            data-obj-id={i}
                        />
                    {/if}
                {/if}
                {#if item.type === "rect"}
                    {#if "x" in item && "y" in item && "width" in item && "height" in item}
                        <rect
                            x={item.x}
                            y={item.y}
                            width={item.width}
                            height={item.height}
                            stroke="black"
                            stroke-width={strokeWidth}
                            data-obj-id={i}
                        />
                    {/if}
                {/if}
            {/if}
        {/if}
    {/each}
</svg>

<style lang="scss">
    @import "./ImageViewer.scss";
</style>
<script lang="ts">
    import { app_data } from "../../stores";
    import SelectCtrl from "../SelectCtrl/SelectCtrl.svelte";

    export let pos;
    export let posRect;
    export let activeElement;

    let app_data_value;
    const unsub_app_data = app_data.subscribe((value) => {
        app_data_value = value;
    });
    let active;
    $: active = activeElement.active;
    let strokeWidth = 2;
</script>

<svg
    id="editor-controls"
    width={app_data_value.doc_size.width}
    height={app_data_value.doc_size.height}
>
    {#if active}
        <SelectCtrl activeElement={activeElement} strokeWidth={2}/>
    {/if}
</svg>

<style lang="scss">
    @import "./EditorControls.scss";
</style>
<script lang="typescript">
    export let activeElement;
    export let strokeWidth;

    let x = 0;
    let y = 0;
    let width = 0;
    let height = 0;

    $: x = activeElement.bBox.x;
    $: y = activeElement.bBox.y;
    $: width = activeElement.bBox.width;
    $: height = activeElement.bBox.height;
    
    let fill = 'rgba(0,0)';
    let stroke = '#246bf0';

    
    let strokeWidthMain;
    $: strokeWidthMain = strokeWidth*2;

</script>
<g
    class="selector-parent-group"
>
    <g
        class="selection-Box"
    >
        <rect
            class="bounding-Box"
            x={x}
            y={y}
            width={width}
            height={height}
            fill={fill}
            stroke={stroke}
            stroke-width={strokeWidthMain}
        />
        <rect
            class="bounding-Box-light"
            x={x-strokeWidthMain}
            y={y-strokeWidthMain}
            width={width+strokeWidthMain*2}
            height={height+strokeWidthMain*2}
            fill={fill}
            stroke={'#B9B9B9'}
            stroke-width={strokeWidthMain}
        />
    </g>
</g>

编辑:我没想到要为 convertGlobalToLocalPosition 和 getBoundingBox 函数添加代码,但由于解决了我的问题的答案,如果我将该代码添加为,它将更好地说明我遇到的问题嗯。

export function convertGlobalToLocalPosition(element: any) {
    if (!element) return { x: 0,y: 0 };
    if (typeof element.ownerSVGElement === 'undefined') return { x: 0,y: 0 };
    var svg = element.ownerSVGElement;

    // Get the cx and cy coordinates
    var pt = svg.createSVGPoint();

    let BoxParent = getBoundingBox(svg);
    let Box = getBoundingBox(element);
    pt.x = Box.x - BoxParent.x;
    pt.y = Box.y - BoxParent.y;
    while (true) {
        // Get this elementents transform
        var transform = element.transform.baseVal.consolidate();
        // If it has a transform,then apply it to our point
        if (transform) {
            var matrix = element.transform.baseVal.consolidate().matrix;
            pt = pt.matrixTransform(matrix);
        }
        // If this elementent's parent is the root SVG elementent,then stop
        if (element.parentNode == svg)
            break;
        // Otherwise step up to the parent elementent and repeat the process
        element = element.parentNode;
    }
    return pt;
}
export function getBoundingBox(el: any) {
    let computed: any = window.getComputedStyle(el);
    let strokeWidthCalc: string = computed['stroke-width'];
    let strokeWidth: number = 0;
    if (strokeWidthCalc.includes('px')) {
        strokeWidth = parseFloat(strokeWidthCalc.substring(0,strokeWidthCalc.length - 2));
    } else {
        // examine value further
    }
    let boundingClientRect = el.getBoundingClientRect();
    let bBox = el.getBBox();
    if (boundingClientRect.width === bBox.width) {
        boundingClientRect.x -= strokeWidth / 2;
        boundingClientRect.y -= strokeWidth / 2;
        boundingClientRect.width += strokeWidth;
        boundingClientRect.height += strokeWidth;
    }
    return boundingClientRect;
}

解决方法

我认为这是导致您出现问题的原因:

elementMoveMoveHandler 中,您正在此处更新元素的位置:

j = [...elements];

j[i]["x"] = elements[i]["x"] - (movePos.offset.x - offset.x);
j[i]["y"] = elements[i]["y"] - (movePos.offset.y - offset.y);
elements = j;

之后,您将在 convertGlobalToLocalPosition 函数中从 DOM 中读取位置。 Svelte 将批量 DOM 更新,它没有时间更新 DOM 元素。因此 convertGlobalToLocalPosition 会给你一个旧值。最简单的解决方法是在 await tick(); 之前添加 let pt = convertGlobalToLocalPosition(e.target); 并使 elementMoveMoveHandler 异步。 您可以在此处阅读有关刻度函数的更多信息:https://svelte.dev/tutorial/tick

还有一些建议:

  1. 您无需手动订阅商店或调用更新功能。您可以使用 $ 符号,Svelte 会为您处理订阅、取消订阅和通知订阅者。 https://svelte.dev/tutorial/auto-subscriptions

  2. 您当然可以在 Svelte 中使用不变性,但这不是必需的。就我个人而言,我发现可变代码更具可读性。