首页 > 解决方案 > 同时从两个不同的组件中拖动两个元素时出现苗条滞后

问题描述

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

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

选择框落后于元素

你可以在这里查看我的代码和框简化演示: 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, 0, 0, 1)',
        stroke: 'rgba(0, 0, 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,
                    },
                    active: true,
                };

                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,
                    y: e.clientY - bbox.top,
                };
                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,
                    type: app_data_value.tool.selected,
                };

                let pt = convertGlobalToLocalPosition(e.target);
                activeElement = {
                    ...activeElement,
                    bbox: {
                        x: pt.x,
                        y: pt.y,
                        width: bbox.width,
                        height: bbox.height,
                    },
                };
                // 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,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;
}

标签: svgdraggablesveltesvelte-3

解决方案


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

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 中使用不变性,但这不是必需的。就个人而言,我发现可变代码更具可读性。


推荐阅读