Draft Pattern 繪製中的物件放獨立 state

正在繪製、尚未完成的物件(draft)跟已完成的物件(items)分開存。繪製過程中只更新 draft,鬆手時才併入 items;過短/無效的 draft 直接丟掉。

做法

const [items, setItems] = useState<Item[]>([])
const [draftStroke, setDraftStroke] = useState<StrokeItem | null>(null)
const [draftRect,   setDraftRect]   = useState<RectItem | null>(null)
 
// 繪製中:只動 draft,不碰 items
const onPointerMove = (e) => {
  setDraftStroke(s => s ? { ...s, points: [...s.points, world] } : s)
}
 
// 鬆手:有效才併入 items
const onPointerUp = () => {
  if (draftStroke && draftStroke.points.length > 1) {
    setItems(prev => [...prev, draftStroke])
  }
  if (draftRect && draftRect.width > 4 && draftRect.height > 4) {
    setItems(prev => [...prev, draftRect])
  }
  setDraftStroke(null)
  setDraftRect(null)
}
 
// 渲染:把兩邊合起來畫
const renderItems = [...items, ...(draftRect ? [draftRect] : []), ...(draftStroke ? [draftStroke] : [])]

為什麼分開

如果繪製中直接 setItems(prev => [...prev.slice(0, -1), { ...prev.at(-1)!, points: [...] }])

  • 每次 move 都複製整個 items 陣列 — 已完成物件多時 O(n) 拷貝很貴
  • items 陣列頻繁變更會讓所有 memo / 子元件 invalidate
  • 中斷繪製(如 pointercancel)要回滾 items,很容易留下髒資料

把 draft 拆出來:

  • 繪製中只動 draft 那一筆,items 完全不動
  • 中斷只要 setDraft(null),items 自然乾淨
  • 鬆手才一次 setItems(prev => [...prev, draft]),全程只 push 一次

「有效才併入」過濾誤觸

if (draftStroke && draftStroke.points.length > 1) { ... }
if (draftRect && draftRect.width > 4 && draftRect.height > 4) { ... }

使用者按一下沒拖就放開(誤觸)會產生:

  • 0 個點的筆畫
  • 0×0 的矩形

直接丟掉,避免清單塞滿透明的空物件。

適用場景

  • 畫筆、筆畫
  • 拖曳建立矩形、橢圓、連線
  • 選取框(marquee)— 鬆手後 draft 變成「選取結果」而非新物件
  • 任何「按下 → 拖動預覽 → 放開確認」的互動

相關