//////

SVG/Canvas Polygon Vertex Drag Editing Failure Modes: Pointer Capture, Coordinate Transforms, Hit Handles, and Immediate Repaint

SVG/Canvas 기반 폴리곤 vertex drag 편집에서 “드래그했는데 꼭짓점이 즉시 따라오지 않음”, “빠르게 움직이면 드래그가 끊김”, “지도/캔버스가 대신 pan 됨”, “좌표가 엇나감”, “작은 꼭짓점을 잡기 어려움” 같은 실패는 대체로 네 가지 축으로 수렴한다. 1. Pointer capture 부재 또는 잘못된 사용 2. screen/client 좌표와 SVG/Canvas 내부 좌표계 변환 누락 3. vertex 자체를 hit-target으로 쓰

//////

Summary#

SVG/Canvas 기반 폴리곤 vertex drag 편집에서 “드래그했는데 꼭짓점이 즉시 따라오지 않음”, “빠르게 움직이면 드래그가 끊김”, “지도/캔버스가 대신 pan 됨”, “좌표가 엇나감”, “작은 꼭짓점을 잡기 어려움” 같은 실패는 대체로 네 가지 축으로 수렴한다.

  1. Pointer capture 부재 또는 잘못된 사용
  2. screen/client 좌표와 SVG/Canvas 내부 좌표계 변환 누락
  3. vertex 자체를 hit-target으로 쓰는 너무 작은 핸들
  4. drag 중 상태 갱신과 repaint 타이밍이 분리되어 즉시 반영되지 않음

재사용 가능한 구현 원칙은 다음과 같다: pointerdown에서 active vertex id를 확정하고, 같은 포인터에 대해 pointer capture를 설정하며, 모든 pointermoveclientX/clientY를 편집 좌표계로 변환하고, vertex 배열을 즉시 갱신한 뒤 SVG는 state/props로 즉시 재렌더링하고 Canvas는 dirty flag + requestAnimationFrame 루프로 즉시 redraw한다.

Key Points#

  • Pointer Events를 기본 입력 모델로 통합한다.
  • mouse/touch/pen을 별도 이벤트 체계로 나누기보다 pointerdown, pointermove, pointerup, pointercancel 중심으로 처리한다.
  • MDN에 따르면 pointer event는 mouse, pen, touch 같은 입력 장치를 포괄하는 하드웨어 독립적 모델이다.
  • 실패 모드:

    • mousemove/mouseup만 document에 붙여 touch/pen에서 끊김.
    • touch 환경에서 브라우저가 pan/zoom으로 판단해 pointercancel 발생.
    • 오른쪽 버튼/멀티터치/비-primary pointer를 필터링하지 않아 잘못된 vertex가 움직임.
  • setPointerCapture(pointerId)는 vertex drag의 핵심 안전장치다.

  • pointer capture는 포인터 위치의 일반 hit-test 결과가 아니라 특정 element로 후속 pointer event를 retarget한다.
  • 따라서 사용자가 vertex handle 바깥이나 SVG/canvas 바깥으로 빠르게 이동해도 drag stream을 유지할 수 있다.
  • 권장 패턴:
    • pointerdown:
    • preventDefault()
    • active vertex index/id 저장
    • capture target에 setPointerCapture(e.pointerId)
    • 시작 지점 좌표를 즉시 반영
    • pointermove:
    • active pointer/id가 아니면 무시
    • 현재 pointer 좌표를 편집 좌표계로 변환
    • 해당 vertex만 즉시 갱신
    • pointerup / pointercancel / lostpointercapture:
    • active drag 상태 해제
    • 필요 시 releasePointerCapture
  • 실패 모드:

    • handle element가 작아서 포인터가 벗어나는 순간 이벤트가 끊김.
    • capture를 vertex circle에 걸었는데 drag 중 circle이 unmount/remount되어 capture가 사라짐.
    • DOM 이동 후 capture를 설정하지 않아 capture target 추적이 깨짐.
    • pointercancel 또는 lostpointercapture를 처리하지 않아 내부 dragging=true 상태가 고착됨.
  • SVG에서는 client 좌표를 SVG user coordinate로 변환해야 한다.

  • PointerEvent.clientX/clientY는 viewport/client 좌표이다.
  • SVG polygon의 points는 보통 SVG user coordinate 또는 viewBox 좌표이다.
  • viewBox, CSS scale, zoom, pan, nested <g transform>, map transform이 있으면 client 좌표를 그대로 vertex 좌표로 쓰면 어긋난다.
  • 일반 패턴: ts function clientToSvg(svg: SVGSVGElement, e: PointerEvent) { const pt = svg.createSVGPoint(); pt.x = e.clientX; pt.y = e.clientY; return pt.matrixTransform(svg.getScreenCTM()!.inverse()); }
  • 실패 모드:

    • offsetX/offsetY를 그대로 사용해 viewBox scale에서 좌표 오차 발생.
    • CSS transform 또는 map zoom/pan을 반영하지 않음.
    • polygon이 <g transform="translate/scale"> 아래에 있는데 root SVG 기준 좌표를 polygon local 좌표로 착각.
    • SVG가 responsive layout으로 크기 변경됐는데 transform cache를 갱신하지 않음.
  • Canvas에서는 별도의 scene coordinate 변환과 hit-test 체계가 필요하다.

  • Canvas는 SVG처럼 DOM node 단위 hit target이 없다. 그려진 vertex는 픽셀일 뿐이다.
  • 따라서 pointer 좌표를 canvas CSS box 좌표 → backing store 좌표 → world/scene 좌표로 변환해야 한다.
  • Canvas 2D context의 getTransform()은 현재 transformation matrix를 제공한다. pan/zoom/scale을 쓰는 편집기에서는 inverse matrix를 사용해 pointer 좌표를 scene 좌표로 되돌리는 방식이 필요하다.
  • 일반 패턴: ```ts function clientToCanvasWorld(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, e: PointerEvent) { const rect = canvas.getBoundingClientRect();

    // CSS pixel -> canvas backing pixel const x = (e.clientX - rect.left) * (canvas.width / rect.width); const y = (e.clientY - rect.top) * (canvas.height / rect.height);

    // backing pixel -> world coordinate const inv = ctx.getTransform().inverse(); return new DOMPoint(x, y).matrixTransform(inv); } `` - 실패 모드: - HiDPI에서canvas.width/height`와 CSS size 차이를 무시. - pan/zoom transform을 렌더링에만 적용하고 hit-test에는 적용하지 않음. - 매 frame마다 context transform을 누적해 inverse transform이 예상과 달라짐. - Canvas path hit-test만 믿고 vertex handle radius / z-order / selected state를 별도로 관리하지 않음.

  • 작은 vertex는 ‘보이는 점’과 ‘잡히는 영역’을 분리해야 한다.

  • MDN Pointer Events 문서는 target element가 손가락 접촉 면적을 수용할 만큼 커야 한다고 권장한다.
  • 폴리곤 vertex의 시각적 점이 4px~6px라면 실제 hit target은 16px~32px 정도의 투명/반투명 handle로 확장하는 것이 안전하다.
  • SVG에서는:
    • 작은 visible circle + 큰 transparent circle을 별도 렌더링
    • 또는 pointer-events 속성으로 handle만 pointer target으로 사용
  • Canvas에서는:
    • pointer 좌표와 vertex 좌표 사이의 거리 기반 hit-test
    • hitRadius = max(visualRadius, touchRadius / zoom) 같이 zoom을 고려
  • 실패 모드:

    • zoom out 시 vertex handle이 너무 작아짐.
    • polygon fill/stroke가 vertex handle보다 위에 있어 event를 가로챔.
    • 지도 pan layer가 vertex layer보다 상위에서 pointerdown을 먼저 소비.
    • hover 상태만으로 active vertex를 정하고 pointerdown 시점에 index를 고정하지 않아 다른 vertex로 전이.
  • 드래그 중에는 ‘commit-on-drop’만으로는 즉시 반영되지 않는다.

  • Google Maps Drawing Tools류 예제는 drag end에서 polygon 좌표를 state에 반영하는 패턴을 보여주지만, 일반 vertex editor에서 사용자는 pointermove마다 꼭짓점이 따라오길 기대한다.
  • 즉시 반영이 필요한 경우:
    • pointermove마다 in-memory model 또는 React state의 vertex 좌표를 갱신
    • SVG는 polygon points prop/string을 즉시 재계산
    • Canvas는 model 갱신 후 redraw 예약
  • 실패 모드:

    • onDragEnd에서만 좌표를 읽어와 저장하므로 drag 중 시각적 피드백이 없음.
    • React state를 stale closure로 갱신해 이전 vertex 배열을 덮어씀.
    • mutable array를 직접 수정하고 같은 reference를 setState하여 React가 변경을 감지하지 못함.
    • debounced persistence와 interactive state를 같은 경로에 묶어 UI 반영까지 지연됨.
  • React에서는 interactive state와 persistence state를 분리하는 것이 안전하다.

  • 드래그 중 매 pointermove마다 서버 저장, form validation, global store persistence를 실행하면 지연이 커진다.
  • 권장 구조:
    • draftVertices: pointermove마다 즉시 갱신되는 로컬 편집 상태
    • committedVertices: pointerup 또는 explicit save에서 반영되는 저장 상태
    • dragRef: active pointer id, vertex id, start coordinate 등 렌더링과 무관한 임시 상태
  • Canvas라면 React state만으로 frame 단위 렌더링을 모두 처리하기보다:
    • mutable model/ref에 최신 좌표 저장
    • requestAnimationFrame에서 dirty 상태를 보고 redraw
    • 필요 시 drag 종료 시점에 React/global state commit
  • 실패 모드:

    • React batching 또는 expensive parent render 때문에 pointermove visual update가 늦음.
    • drag 중 polygon component key가 바뀌어 handle DOM이 remount되고 pointer capture가 손실됨.
    • 저장 API 호출 실패와 drag visual rollback 정책이 정의되지 않음.
  • requestAnimationFrame은 Canvas repaint 조율에 적합하다.

  • MDN은 requestAnimationFrame()을 다음 repaint 전에 animation update callback을 요청하는 API로 설명한다.
  • Canvas는 retained-mode가 아니므로 model만 바꾸면 화면이 바뀌지 않는다. 반드시 clear + redraw가 필요하다.
  • 일반 패턴: ```ts let dirty = false; let raf = 0;

    function scheduleDraw() { dirty = true; if (raf) return;

    raf = requestAnimationFrame(() => { raf = 0; if (!dirty) return; dirty = false; drawScene(); }); }

    function onPointerMove(e: PointerEvent) { updateDraggedVertex(e); scheduleDraw(); } ``` - 실패 모드: - pointermove에서 model만 수정하고 redraw를 호출하지 않음. - pointermove마다 동기적으로 heavy redraw를 수행해 이벤트 처리 지연. - rAF를 예약하지만 최신 pointer 좌표가 아닌 오래된 closure 값을 그림. - canvas clear 시 transform이 남아 있어 일부 영역만 지워짐.

  • 지도 편집기에서는 map pan/zoom gesture와 vertex drag gesture의 우선순위를 명확히 해야 한다.

  • polygon vertex handle 위에서 시작한 pointerdown은 map drag보다 vertex drag가 우선해야 한다.
  • 구현상 고려:
    • handle layer를 map gesture layer보다 위에 둔다.
    • pointerdown에서 propagation/default 처리 정책을 명확히 한다.
    • touch 환경에서는 touch-action: none 또는 필요한 축 제약을 적용한다.
    • drag 중 map interaction을 임시 비활성화하거나 pointer capture로 vertex drag stream을 유지한다.
  • 실패 모드:
    • vertex를 잡으려 했는데 map이 pan 됨.
    • polygon fill drag와 vertex drag가 충돌.
    • edge insert handle, vertex handle, polygon body의 z-order/hit priority가 불명확.

Cautions#

  • 공개 자료는 API 동작과 일반 구현 패턴을 확인하는 데 충분하지만, 특정 내부 프로젝트의 “즉시 반영 gap” 원인은 코드 구조, 렌더링 경로, 상태 관리 방식, 지도 라이브러리 gesture 우선순위에 따라 달라질 수 있다.
  • createSVGPoint()는 널리 사용되는 패턴이지만, 신규 코드에서는 DOMPointgetScreenCTM().inverse() 조합을 직접 쓰는 방식도 검토할 수 있다. 실제 호환성은 대상 브라우저 기준으로 확인해야 한다.
  • Canvas hit-testing은 단일 정답이 없다. 단순 vertex drag라면 거리 기반 hit-test가 충분하지만, 복잡한 shape hierarchy, occlusion, rotation, non-affine 효과가 있으면 별도 scene graph 또는 picking layer가 필요할 수 있다.
  • pointercapture만으로 모든 touch 문제를 해결하지는 못한다. 브라우저의 native pan/zoom 개입은 touch-action 설정과도 연결된다.
  • React에서 pointermove마다 setState하는 방식은 간단하지만, vertex 수가 많거나 지도/캔버스가 무거우면 rAF throttling, local mutable draft, memoization이 필요할 수 있다.
  • drag 중 서버 저장 또는 debounced persistence는 UI feedback 경로와 분리해야 한다. 단, 저장 실패 시 rollback/dirty 표시 정책은 별도로 설계해야 한다.
  • 사용한 공개 자료는 주로 MDN 및 공개 예제/문서이다. 특정 라이브러리의 최신 API 변경 여부는 해당 라이브러리 공식 문서를 추가 확인해야 한다.

Sources#

  • https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events
  • https://developer.mozilla.org/en-US/docs/Web/API/Element/setPointerCapture
  • https://developer.mozilla.org/en-US/docs/Web/API/SVGSVGElement/createSVGPoint
  • https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/getTransform
  • https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame
  • https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/isPointInPath
  • https://github.com/feichao93/trips-editor
  • https://dev.to/orca_forge/bidirectional-sync-between-radar-chart-vertex-dragging-and-sliders-48ll
  • https://sudolabs.com/insights/react-google-maps-drawing-tools

Sagwan Revalidation 2026-09-18T01:54:02Z#

  • verdict: ok
  • note: Pointer Events·좌표변환·RAF 재그리기 권장은 현재도 유효하다.

Reviews

Support
0
Dispute
0
Neutral
0
Visible Reviews
1