Skip to content

Commit b0ae9b5

Browse files
chriswhongclaude
andcommitted
Add vertex snap support when drawing lines and polygons
- Fix line.ts snapping to exclude the feature being drawn (missing excludeFeatureId) - Add vertex-snap visual feedback (highlight circles) during drawing for both line and polygon modes - Alt+Shift during drawing now snaps to vertices only (matching edit mode behavior) instead of conflicting with direction locking - Update mode hints to document Option+Shift vertex snapping and only show snap hint for features with vertices Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent d5f3c58 commit b0ae9b5

3 files changed

Lines changed: 70 additions & 13 deletions

File tree

app/components/mode_hints.tsx

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import { useBreakpoint } from 'app/hooks/use_responsive';
44
import { getIsMac, localizeKeybinding } from 'app/lib/utils';
55
import clsx from 'clsx';
66
import { useAtom, useAtomValue } from 'jotai';
7-
import { hideHintsAtom, selectionAtom } from 'state/jotai';
7+
import {
8+
hideHintsAtom,
9+
selectedFeaturesAtom,
10+
selectionAtom
11+
} from 'state/jotai';
812
import { Mode, modeAtom } from 'state/mode';
913

1014
function ModeHint({
@@ -48,17 +52,26 @@ function ModeHint({
4852
export function ModeHints() {
4953
const mode = useAtomValue(modeAtom);
5054
const selection = useAtomValue(selectionAtom);
55+
const selectedFeatures = useAtomValue(selectedFeaturesAtom);
5156
const show = useBreakpoint('lg');
5257
const isMac = getIsMac();
5358

59+
const selectedGeometryType = selectedFeatures[0]?.feature.geometry?.type;
60+
const selectionHasVertices =
61+
selectedGeometryType === 'LineString' ||
62+
selectedGeometryType === 'MultiLineString' ||
63+
selectedGeometryType === 'Polygon' ||
64+
selectedGeometryType === 'MultiPolygon';
65+
5466
if (!show) {
5567
return null;
5668
}
5769

5870
const hold = (shift: boolean = false) => (
5971
<div className="text-xs">
60-
Hold {localizeKeybinding('Option', isMac)} to snap to existing features.
61-
{shift ? 'Hold Shift to snap to right angles.' : null}
72+
Hold {localizeKeybinding('Option', isMac)} to snap to existing features,{' '}
73+
{localizeKeybinding('Option', isMac)}+Shift to snap to vertices.
74+
{shift ? ' Hold Shift to snap to right angles.' : null}
6275
</div>
6376
);
6477

@@ -107,8 +120,12 @@ export function ModeHints() {
107120
<div>
108121
Hold space bar & drag to move entire features. Hold option &
109122
drag to rotate.
110-
<br />
111-
{hold()}
123+
{selectionHasVertices ? (
124+
<>
125+
<br />
126+
{hold()}
127+
</>
128+
) : null}
112129
</div>
113130
</ModeHint>
114131
);

app/lib/handlers/line.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,18 @@ import { captureException } from 'integrations/errors';
77
import { useSetAtom } from 'jotai';
88
import { useRef } from 'react';
99
import { USelection } from 'state';
10-
import { cursorStyleAtom, Mode, modeAtom, selectionAtom } from 'state/jotai';
10+
import {
11+
cursorStyleAtom,
12+
ephemeralStateAtom,
13+
Mode,
14+
modeAtom,
15+
selectionAtom
16+
} from 'state/jotai';
1117
import type { HandlerContext, IFeature, LineString, Position } from 'types';
1218
import {
1319
createOrUpdateFeature,
1420
getMapCoord,
21+
getNearbyVertices,
1522
getSnappingCoordinates
1623
} from './utils';
1724

@@ -28,6 +35,7 @@ export function useLineHandlers({
2835
const setSelection = useSetAtom(selectionAtom);
2936
const setMode = useSetAtom(modeAtom);
3037
const setCursor = useSetAtom(cursorStyleAtom);
38+
const setEphemeralState = useSetAtom(ephemeralStateAtom);
3139
const transact = rep.useTransact();
3240
const popMoment = usePopMoment();
3341
const usingTouchEvents = useRef<boolean>(false);
@@ -116,12 +124,26 @@ export function useLineHandlers({
116124

117125
let nextCoord = getMapCoord(e) as Position;
118126
const lastCoord = feature.geometry.coordinates.at(-2);
119-
if (shiftHeld.current && lastCoord) {
127+
const verticesOnly = altHeld.current && shiftHeld.current;
128+
if (shiftHeld.current && !altHeld.current && lastCoord) {
120129
nextCoord = lockDirection(lastCoord, nextCoord);
121130
}
122131

123132
if (altHeld.current && lastCoord) {
124-
nextCoord = getSnappingCoordinates(e, featureMap, pmap, idMap);
133+
nextCoord = getSnappingCoordinates(
134+
e,
135+
featureMap,
136+
pmap,
137+
idMap,
138+
selection.id,
139+
verticesOnly
140+
);
141+
setEphemeralState({
142+
type: 'vertex-snap',
143+
vertices: getNearbyVertices(e, featureMap, pmap, idMap, selection.id)
144+
});
145+
} else {
146+
setEphemeralState({ type: 'none' });
125147
}
126148

127149
void transact({

app/lib/handlers/polygon.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,18 @@ import { captureException } from 'integrations/errors';
1010
import { useSetAtom } from 'jotai';
1111
import { useRef } from 'react';
1212
import { USelection } from 'state';
13-
import { cursorStyleAtom, Mode, modeAtom, selectionAtom } from 'state/jotai';
13+
import {
14+
cursorStyleAtom,
15+
ephemeralStateAtom,
16+
Mode,
17+
modeAtom,
18+
selectionAtom
19+
} from 'state/jotai';
1420
import type { HandlerContext, IFeature, Polygon } from 'types';
1521
import {
1622
createOrUpdateFeature,
1723
getMapCoord,
24+
getNearbyVertices,
1825
getSnappingCoordinates
1926
} from './utils';
2027

@@ -31,6 +38,7 @@ export function usePolygonHandlers({
3138
const setSelection = useSetAtom(selectionAtom);
3239
const setMode = useSetAtom(modeAtom);
3340
const setCursor = useSetAtom(cursorStyleAtom);
41+
const setEphemeralState = useSetAtom(ephemeralStateAtom);
3442
const popMoment = usePopMoment();
3543
const transact = rep.useTransact();
3644
/**
@@ -116,7 +124,8 @@ export function usePolygonHandlers({
116124
}
117125

118126
const lastCoord = feature.geometry.coordinates[0].at(-3);
119-
if (shiftHeld.current && lastCoord) {
127+
const verticesOnly = altHeld.current && shiftHeld.current;
128+
if (shiftHeld.current && !altHeld.current && lastCoord) {
120129
nextCoord = lockDirection(lastCoord, nextCoord);
121130
}
122131

@@ -126,7 +135,8 @@ export function usePolygonHandlers({
126135
featureMap,
127136
pmap,
128137
idMap,
129-
selection.id
138+
selection.id,
139+
verticesOnly
130140
) as Pos2;
131141
}
132142

@@ -167,7 +177,8 @@ export function usePolygonHandlers({
167177
const feature = wrappedFeature.feature as IFeature<Polygon>;
168178

169179
const lastCoord = feature.geometry.coordinates[0].at(-3);
170-
if (shiftHeld.current && lastCoord) {
180+
const verticesOnly = altHeld.current && shiftHeld.current;
181+
if (shiftHeld.current && !altHeld.current && lastCoord) {
171182
nextCoord = lockDirection(lastCoord, nextCoord);
172183
}
173184

@@ -177,8 +188,15 @@ export function usePolygonHandlers({
177188
featureMap,
178189
pmap,
179190
idMap,
180-
selection.id
191+
selection.id,
192+
verticesOnly
181193
) as Pos2;
194+
setEphemeralState({
195+
type: 'vertex-snap',
196+
vertices: getNearbyVertices(e, featureMap, pmap, idMap, selection.id)
197+
});
198+
} else {
199+
setEphemeralState({ type: 'none' });
182200
}
183201

184202
const newRing = feature.geometry.coordinates[0].slice();

0 commit comments

Comments
 (0)