All examplesinteractionssnap-engine
§ examples · interactions

Snap Engine

Endpoint / midpoint / intersection snaps with visual guides. Works with any drawing tool.

slugsnap-engine
source154 lines
statuslive
tsexamples-src/snap-engine.ts
1/**
2 * Snap Engine — DrawPolylineTool / DrawPolygonTool / DrawPointTool wired
3 * to a configured SnapEngine. The snap engine keys off reference features
4 * already on the output layer; as you draw, guide glyphs light up when
5 * the cursor is within tolerance of an endpoint, midpoint, intersection
6 * or nearest-on-edge.
7 */
8 
9import { MapView } from "mapgpu";
10import { GraphicsLayer, RasterTileLayer } from "mapgpu/layers";
11import { RenderEngine } from "mapgpu/render";
12import {
13 DrawPointTool,
14 DrawPolygonTool,
15 DrawPolylineTool,
16 SnapEngine,
17 SnapType,
18} from "mapgpu/tools";
19 
20import type { RunResultObject } from "@/components/examples/ExampleCanvas";
21 
22type ToolKind = "point" | "polyline" | "polygon";
23 
24export async function run(container: HTMLElement): Promise<RunResultObject> {
25 const view = new MapView({
26 container,
27 renderEngine: new RenderEngine(),
28 mode: "2d",
29 center: [29.02, 41.01],
30 zoom: 14,
31 minZoom: 4,
32 maxZoom: 19,
33 backgroundColor: "transparent",
34 });
35 
36 view.map.add(
37 new RasterTileLayer({
38 id: "osm",
39 urlTemplate: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
40 maxZoom: 19,
41 attribution: "© OpenStreetMap contributors",
42 }),
43 );
44 
45 const output = new GraphicsLayer({ id: "drawings" });
46 const preview = new GraphicsLayer({ id: "__tool-preview__" });
47 view.map.add(output);
48 view.map.add(preview);
49 
50 // Seed reference features so there's something to snap to.
51 const cx = 29.02, cy = 41.01, d = 0.008;
52 output.add({
53 id: "triangle",
54 geometry: {
55 type: "Polygon" as const,
56 coordinates: [[
57 [cx - d, cy - d], [cx + d, cy - d], [cx, cy + d], [cx - d, cy - d],
58 ]],
59 },
60 attributes: {},
61 });
62 output.add({
63 id: "h-line",
64 geometry: {
65 type: "LineString" as const,
66 coordinates: [[cx - d * 1.5, cy], [cx + d * 1.5, cy]],
67 },
68 attributes: {},
69 });
70 output.add({
71 id: "v-line",
72 geometry: {
73 type: "LineString" as const,
74 coordinates: [[cx + d * 0.5, cy - d * 1.2], [cx + d * 0.5, cy + d * 1.2]],
75 },
76 attributes: {},
77 });
78 
79 const snap = new SnapEngine({
80 enabled: true,
81 tolerance: 14,
82 enabledTypes: new Set([
83 SnapType.EndPoint,
84 SnapType.MidPoint,
85 SnapType.Nearest,
86 SnapType.Intersection,
87 ]),
88 angleGuideIntervals: [0, 45, 90, 135],
89 });
90 snap.addSourceLayer(output);
91 
92 const tm = view.toolManager;
93 tm.setPreviewLayer(preview);
94 tm.registerTool(new DrawPointTool({ targetLayer: output, snapEngine: snap }));
95 tm.registerTool(new DrawPolylineTool({ targetLayer: output, snapEngine: snap }));
96 tm.registerTool(new DrawPolygonTool({ targetLayer: output, snapEngine: snap }));
97 
98 await view.when();
99 
100 const state: { kind: ToolKind; active: boolean } = { kind: "polyline", active: false };
101 const toolId = (k: ToolKind) =>
102 k === "point" ? "draw-point" : k === "polyline" ? "draw-polyline" : "draw-polygon";
103 const sync = () => {
104 if (state.active) tm.activateTool(toolId(state.kind));
105 else tm.deactivateTool();
106 };
107 
108 return {
109 dispose: () => view.destroy(),
110 controls: [
111 {
112 kind: "segmented",
113 id: "tool",
114 initial: "polyline",
115 options: [
116 { value: "point", label: "Point" },
117 { value: "polyline", label: "Polyline" },
118 { value: "polygon", label: "Polygon" },
119 ],
120 onChange: (value) => {
121 state.kind = value as ToolKind;
122 sync();
123 },
124 },
125 {
126 kind: "toggle",
127 id: "draw",
128 labels: ["idle", "draw + snap"],
129 initial: false,
130 onChange: (on) => {
131 state.active = on;
132 sync();
133 },
134 },
135 {
136 kind: "button",
137 id: "clear",
138 label: "Clear drawings",
139 onClick: () => {
140 // Keep the seeded reference features; only drop anything
141 // after them (the user-drawn shapes come last).
142 const all = output.getFeatures();
143 const seeds = new Set(["triangle", "h-line", "v-line"]);
144 const userDrawn = all.filter((f) => !seeds.has(String(f.id)));
145 for (const f of userDrawn) {
146 output.remove(f.id as string);
147 }
148 preview.clear();
149 },
150 },
151 ],
152 };
153}