Skip to content

Commit 3a9581d

Browse files
committed
feat(editor): drag polygon-collision vertices to move them
The per-tile collision polygon editor could only add and delete vertices, so fine-tuning a shape meant deleting a point and re-clicking a new one. Vertices now drag to move (live preview, one undo step on release); a click that doesn't move still deletes, as before. Pointer capture is best-effort (wrapped) so the gesture is robust. 887 desktop tests pass; tsc clean.
1 parent 1c6b0d9 commit 3a9581d

3 files changed

Lines changed: 41 additions & 9 deletions

File tree

desktop/src/i18n/messages/tile.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,8 @@ export const tileMessages = defineMessages({
157157
'tile.pe.title': { en: 'Collision polygon · #{id}', zh: '碰撞多边形 · #{id}' },
158158
'tile.pe.clear': { en: 'Clear', zh: '清除' },
159159
'tile.pe.hint': {
160-
en: 'Click to add a vertex · click a vertex to delete · takes effect at ≥3 points',
161-
zh: '点击添加顶点 · 点击顶点以删除 · ≥3 个顶点时生效',
160+
en: 'Click to add a vertex · drag a vertex to move · click it to delete · takes effect at ≥3 points',
161+
zh: '点击添加顶点 · 拖动顶点移动 · 点击顶点删除 · ≥3 个顶点时生效',
162162
},
163163

164164
// — Tileset editor: terrain authoring —

desktop/src/panels/TilesetEditor.tsx

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,16 +80,25 @@ function PolygonEditor(props: {
8080
const [pts, setPts] = useState<[number, number][]>(
8181
existing?.type === 'polygon' ? existing.points.map((p) => [p[0], p[1]] as [number, number]) : [],
8282
);
83+
const ptsRef = useRef(pts);
84+
ptsRef.current = pts;
85+
const stageRef = useRef<HTMLDivElement>(null);
86+
// A vertex drag: which point, and whether it actually moved (so a plain click still
87+
// deletes it). Live-updates local state during the drag, commits ONE undo on release.
88+
const vdrag = useRef<{ i: number; moved: boolean } | null>(null);
8389

8490
const commit = (next: [number, number][]) => { setPts(next); TilesetCommands.setTilePolygon(tileId, next); };
8591

86-
const addPoint = (e: ReactMouseEvent) => {
87-
const r = (e.currentTarget as HTMLElement).getBoundingClientRect();
88-
const px = Math.round(((e.clientX - r.left) / r.width) * tw);
89-
const py = Math.round(((e.clientY - r.top) / r.height) * th);
90-
commit([...pts, [Math.max(0, Math.min(tw, px)), Math.max(0, Math.min(th, py))]]);
92+
/** Tile-space (0..tw, 0..th) point under the pointer, clamped to the tile. */
93+
const pointAt = (clientX: number, clientY: number): [number, number] => {
94+
const r = stageRef.current!.getBoundingClientRect();
95+
const px = Math.round(((clientX - r.left) / r.width) * tw);
96+
const py = Math.round(((clientY - r.top) / r.height) * th);
97+
return [Math.max(0, Math.min(tw, px)), Math.max(0, Math.min(th, py))];
9198
};
9299

100+
const addPoint = (e: ReactMouseEvent) => { commit([...pts, pointAt(e.clientX, e.clientY)]); };
101+
93102
// Escape closes, like every other transient surface.
94103
useEffect(() => {
95104
const onKey = (e: KeyboardEvent) => {
@@ -109,6 +118,7 @@ function PolygonEditor(props: {
109118
<button type="button" onClick={onClose}>{t('tile.done')}</button>
110119
</div>
111120
<div
121+
ref={stageRef}
112122
className="ts-pe-stage"
113123
style={{
114124
width: SIZE, height: SIZE,
@@ -125,7 +135,28 @@ function PolygonEditor(props: {
125135
{pts.map((p, i) => (
126136
<circle
127137
key={i} cx={p[0]} cy={p[1]} r={Math.max(1.2, tw * 0.06)} className="ts-pe-pt"
128-
onClick={(e) => { e.stopPropagation(); commit(pts.filter((_, j) => j !== i)); }}
138+
// Drag to move the vertex (live), click (no move) to remove it.
139+
onPointerDown={(e) => {
140+
e.stopPropagation();
141+
vdrag.current = { i, moved: false };
142+
try { (e.target as Element).setPointerCapture(e.pointerId); } catch { /* capture is optional */ }
143+
}}
144+
onPointerMove={(e) => {
145+
const d = vdrag.current;
146+
if (!d || d.i !== i) return;
147+
d.moved = true;
148+
const np = pointAt(e.clientX, e.clientY);
149+
setPts((cur) => cur.map((q, j) => (j === i ? np : q)));
150+
}}
151+
onPointerUp={(e) => {
152+
const d = vdrag.current;
153+
vdrag.current = null;
154+
try { (e.target as Element).releasePointerCapture(e.pointerId); } catch { /* not captured */ }
155+
if (!d) return;
156+
if (d.moved) TilesetCommands.setTilePolygon(tileId, ptsRef.current);
157+
else commit(ptsRef.current.filter((_, j) => j !== i));
158+
}}
159+
onClick={(e) => e.stopPropagation()}
129160
/>
130161
))}
131162
</svg>

desktop/src/theme/tileset.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,8 @@
416416
}
417417
.ts-pe-svg { position: absolute; left: 0; top: 0; pointer-events: none; }
418418
.ts-pe-poly { fill: color-mix(in srgb, var(--gizmo-tile) 30%, transparent); stroke: var(--gizmo-tile); stroke-width: 0.5; }
419-
.ts-pe-pt { fill: #fff; stroke: var(--gizmo-tile); stroke-width: 0.5; pointer-events: auto; cursor: pointer; }
419+
.ts-pe-pt { fill: #fff; stroke: var(--gizmo-tile); stroke-width: 0.5; pointer-events: auto; cursor: grab; touch-action: none; }
420+
.ts-pe-pt:active { cursor: grabbing; }
420421
.ts-pe-pt:hover { fill: var(--error-hi); }
421422
.ts-pe-hint { color: var(--text-faint); font-size: var(--fs-xs); text-align: center; }
422423

0 commit comments

Comments
 (0)