#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
Еще несколько предложений:
- Вам не нужно вручную подписываться на магазины или вызывать функцию обновления. Вы можете использовать
$
символ -, и Svelte позаботится о подписке, отмене подписки и уведомлении подписчиков за вас. https://svelte.dev/tutorial/auto-subscriptions - Вы, конечно, можете использовать неизменность в Стройности, но в этом нет необходимости. Лично я нахожу изменяемый код более читабельным.