Плавная задержка при одновременном перетаскивании двух элементов из двух разных компонентов

#svg #draggable #svelte #svelte-3

Вопрос:

Я пытался создать простой редактор svg, чтобы опробовать svelte. Все шло отлично, пока я не создал поле выбора для элемента, которое можно перетаскивать вместе с активным выбранным элементом. При перетаскивании выбранного элемента поле выбора отстает от самого элемента. Я не уверен, что что-то не так.

Я пробовал несколько вещей, таких как использование магазина для передачи данных о местоположении и размещение событий в родительском элементе, чтобы все вычислялось на одном и том же компоненте в случае, если это может быть проблемой, но все равно это не работает. Я не уверен, что делаю что-то не так. Я уже некоторое время пытаюсь это понять, но понятия не имею, в чем может быть проблема.

выберите поле отстающий элемент

Вы можете проверить мою упрощенную демонстрационную версию codesandbox здесь: 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) amp;amp;
            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 amp;amp; 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 amp;amp; "y1" in item amp;amp; "x2" in item amp;amp; "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 amp;amp; "y" in item amp;amp; "width" in item amp;amp; "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;
}
 

Комментарии:

1. У меня было что-то похожее с перетаскиванием, когда я преобразовал ванильный JavaScript ChessMeister v0 в стройный. Потратил неделю на попытки, затем продолжил работу с веб-компонентами Vanilla JS. Это, пожалуй, единственная причина, по которой мне не нравится стройность.. просто невозможно отследить клиентскую часть кода.. чем глубже вы погружаетесь; тем больше вы думаете: я должен был сделать это с Ванильным JS сам.

2. @Danny’365CSI ‘ Энгельман Я тоже встроил это в react, и у меня была та же самая проблема. Просто оказалось, что я неправильно использовал useState и useEffect. После того, как я исправил это, никакой задержки вообще не было. Так что я почти уверен, что просто делаю здесь что-то не так. Я предполагаю, что это просто проблема с тем, как работает стройность. Вероятно, было бы проще, если бы я выбрал ваниль, но мне потребовалось бы гораздо больше работы.

Ответ №1:

Я думаю, что это вызывает вашу проблему:

В 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;
 

и сразу после этого вы считываете позицию из DOM в convertGlobalToLocalPosition функции. 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. Вы, конечно, можете использовать неизменность в Стройности, но в этом нет необходимости. Лично я нахожу изменяемый код более читабельным.