@particle-academy/agent-integrations 0.21.1 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bridges/map.d.cts +62 -0
- package/dist/bridges/map.d.ts +62 -0
- package/dist/bridges-map.cjs +454 -0
- package/dist/bridges-map.cjs.map +1 -0
- package/dist/bridges-map.js +7 -0
- package/dist/bridges-map.js.map +1 -0
- package/dist/chunk-3VRQGDKV.js +334 -0
- package/dist/chunk-3VRQGDKV.js.map +1 -0
- package/dist/index.cjs +385 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +16 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { T as ToolHost } from '../tool-host-BQuUygLF.cjs';
|
|
2
|
+
import { B as Bridge } from '../types-CCSBGW9T.cjs';
|
|
3
|
+
import '../types-aOQLTW0E.cjs';
|
|
4
|
+
|
|
5
|
+
type LatLng = {
|
|
6
|
+
lat: number;
|
|
7
|
+
lng: number;
|
|
8
|
+
};
|
|
9
|
+
type MapView = {
|
|
10
|
+
center: LatLng;
|
|
11
|
+
zoom: number;
|
|
12
|
+
bearing?: number;
|
|
13
|
+
pitch?: number;
|
|
14
|
+
};
|
|
15
|
+
type MapMarker = {
|
|
16
|
+
id: string;
|
|
17
|
+
position: LatLng;
|
|
18
|
+
label?: string;
|
|
19
|
+
color?: string;
|
|
20
|
+
icon?: string;
|
|
21
|
+
draggable?: boolean;
|
|
22
|
+
data?: Record<string, unknown>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Adapter the host provides — getters/setters over the SAME controlled state it
|
|
27
|
+
* passes to fancy-map's `<Map>` (view, markers, selection), plus optional
|
|
28
|
+
* `fitBounds` (from the map's onReady handle) and `setFollow` (drives the map's
|
|
29
|
+
* `follow` prop so an agent can make the camera track a moving marker).
|
|
30
|
+
*/
|
|
31
|
+
type MapBridgeAdapter = {
|
|
32
|
+
getView: () => MapView;
|
|
33
|
+
setView: (next: MapView) => void;
|
|
34
|
+
getMarkers: () => MapMarker[];
|
|
35
|
+
setMarkers: (next: MapMarker[] | ((prev: MapMarker[]) => MapMarker[])) => void;
|
|
36
|
+
getSelected: () => string | null;
|
|
37
|
+
setSelected: (id: string | null) => void;
|
|
38
|
+
/** Fit the camera to points (wire to the `MapHandle.fitBounds` from `<Map onReady>`). */
|
|
39
|
+
fitBounds?: (points: LatLng[], padding?: number) => void;
|
|
40
|
+
/** Make the camera follow a marker as it moves (drives the map's `follow` prop). */
|
|
41
|
+
setFollow?: (id: string | null) => void;
|
|
42
|
+
};
|
|
43
|
+
type MapBridgeOptions = {
|
|
44
|
+
adapter: MapBridgeAdapter;
|
|
45
|
+
/** Identity tagged onto agent-authored markers. */
|
|
46
|
+
agent?: {
|
|
47
|
+
id: string;
|
|
48
|
+
name?: string;
|
|
49
|
+
color?: string;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* registerMapBridge — an MCP tool set over a fancy-map `<Map>`'s controlled
|
|
54
|
+
* state, so a human and an agent cohabit one map. The agent moves the camera,
|
|
55
|
+
* drops/updates/removes markers, selects, fits bounds, and follows a live
|
|
56
|
+
* track; every mutation broadcasts AgentActivity (for presence) and pushes an
|
|
57
|
+
* undo entry (so the human can step the agent back). Mirrors the whiteboard /
|
|
58
|
+
* flow bridges in shape.
|
|
59
|
+
*/
|
|
60
|
+
declare function registerMapBridge(host: ToolHost, options: MapBridgeOptions): Bridge;
|
|
61
|
+
|
|
62
|
+
export { type MapBridgeAdapter, type MapBridgeOptions, registerMapBridge };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { T as ToolHost } from '../tool-host-C8JMMGYq.js';
|
|
2
|
+
import { B as Bridge } from '../types-DIVNcIQO.js';
|
|
3
|
+
import '../types-aOQLTW0E.js';
|
|
4
|
+
|
|
5
|
+
type LatLng = {
|
|
6
|
+
lat: number;
|
|
7
|
+
lng: number;
|
|
8
|
+
};
|
|
9
|
+
type MapView = {
|
|
10
|
+
center: LatLng;
|
|
11
|
+
zoom: number;
|
|
12
|
+
bearing?: number;
|
|
13
|
+
pitch?: number;
|
|
14
|
+
};
|
|
15
|
+
type MapMarker = {
|
|
16
|
+
id: string;
|
|
17
|
+
position: LatLng;
|
|
18
|
+
label?: string;
|
|
19
|
+
color?: string;
|
|
20
|
+
icon?: string;
|
|
21
|
+
draggable?: boolean;
|
|
22
|
+
data?: Record<string, unknown>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Adapter the host provides — getters/setters over the SAME controlled state it
|
|
27
|
+
* passes to fancy-map's `<Map>` (view, markers, selection), plus optional
|
|
28
|
+
* `fitBounds` (from the map's onReady handle) and `setFollow` (drives the map's
|
|
29
|
+
* `follow` prop so an agent can make the camera track a moving marker).
|
|
30
|
+
*/
|
|
31
|
+
type MapBridgeAdapter = {
|
|
32
|
+
getView: () => MapView;
|
|
33
|
+
setView: (next: MapView) => void;
|
|
34
|
+
getMarkers: () => MapMarker[];
|
|
35
|
+
setMarkers: (next: MapMarker[] | ((prev: MapMarker[]) => MapMarker[])) => void;
|
|
36
|
+
getSelected: () => string | null;
|
|
37
|
+
setSelected: (id: string | null) => void;
|
|
38
|
+
/** Fit the camera to points (wire to the `MapHandle.fitBounds` from `<Map onReady>`). */
|
|
39
|
+
fitBounds?: (points: LatLng[], padding?: number) => void;
|
|
40
|
+
/** Make the camera follow a marker as it moves (drives the map's `follow` prop). */
|
|
41
|
+
setFollow?: (id: string | null) => void;
|
|
42
|
+
};
|
|
43
|
+
type MapBridgeOptions = {
|
|
44
|
+
adapter: MapBridgeAdapter;
|
|
45
|
+
/** Identity tagged onto agent-authored markers. */
|
|
46
|
+
agent?: {
|
|
47
|
+
id: string;
|
|
48
|
+
name?: string;
|
|
49
|
+
color?: string;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* registerMapBridge — an MCP tool set over a fancy-map `<Map>`'s controlled
|
|
54
|
+
* state, so a human and an agent cohabit one map. The agent moves the camera,
|
|
55
|
+
* drops/updates/removes markers, selects, fits bounds, and follows a live
|
|
56
|
+
* track; every mutation broadcasts AgentActivity (for presence) and pushes an
|
|
57
|
+
* undo entry (so the human can step the agent back). Mirrors the whiteboard /
|
|
58
|
+
* flow bridges in shape.
|
|
59
|
+
*/
|
|
60
|
+
declare function registerMapBridge(host: ToolHost, options: MapBridgeOptions): Bridge;
|
|
61
|
+
|
|
62
|
+
export { type MapBridgeAdapter, type MapBridgeOptions, registerMapBridge };
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var fancyAutoCommon = require('@particle-academy/fancy-auto-common');
|
|
4
|
+
|
|
5
|
+
// src/mcp/server.ts
|
|
6
|
+
function textResult(text, structured) {
|
|
7
|
+
return {
|
|
8
|
+
content: [{ type: "text", text }],
|
|
9
|
+
...structured !== void 0 ? { structuredContent: structured } : {}
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function errorResult(text) {
|
|
13
|
+
return { content: [{ type: "text", text }], isError: true };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// src/presence/wrap-tool-with-activity.ts
|
|
17
|
+
function wrapToolWithActivity(handler, options) {
|
|
18
|
+
return async (args) => {
|
|
19
|
+
const result = await handler(args);
|
|
20
|
+
if (result.isError) return result;
|
|
21
|
+
let target;
|
|
22
|
+
if (options.resolveTarget) {
|
|
23
|
+
target = options.resolveTarget({ toolName: options.toolName, args, result });
|
|
24
|
+
} else {
|
|
25
|
+
target = { kind: options.kind, screenId: options.screenId };
|
|
26
|
+
}
|
|
27
|
+
if (!target) return result;
|
|
28
|
+
fancyAutoCommon.emitActivity({
|
|
29
|
+
agentId: options.agent.id,
|
|
30
|
+
agentName: options.agent.name,
|
|
31
|
+
agentColor: options.agent.color,
|
|
32
|
+
target: { ...target, kind: target.kind ?? options.kind, screenId: target.screenId ?? options.screenId },
|
|
33
|
+
action: options.toolName,
|
|
34
|
+
timestamp: Date.now(),
|
|
35
|
+
meta: extractMeta(result),
|
|
36
|
+
ttlMs: options.ttlMs
|
|
37
|
+
});
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function extractMeta(result) {
|
|
42
|
+
const sc = result.structuredContent;
|
|
43
|
+
if (sc && typeof sc === "object" && !Array.isArray(sc)) {
|
|
44
|
+
return sc;
|
|
45
|
+
}
|
|
46
|
+
return void 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/undo/undo-tools.ts
|
|
50
|
+
var installedHosts = /* @__PURE__ */ new WeakSet();
|
|
51
|
+
function ensureUndoToolsRegistered(host, options = {}) {
|
|
52
|
+
if (installedHosts.has(host)) return;
|
|
53
|
+
installedHosts.add(host);
|
|
54
|
+
registerUndoTools(host, options);
|
|
55
|
+
}
|
|
56
|
+
function registerUndoTools(host, options = {}) {
|
|
57
|
+
const defaultAgent = options.defaultAgentId ?? "agent";
|
|
58
|
+
const disposers = [];
|
|
59
|
+
const agentOf = (args) => typeof args?.agentId === "string" ? args.agentId : defaultAgent;
|
|
60
|
+
disposers.push(
|
|
61
|
+
host.registerTool(
|
|
62
|
+
{
|
|
63
|
+
name: "agent_undo",
|
|
64
|
+
description: "Undo the most recent action on the agent's stack. Optional agentId targets a specific agent.",
|
|
65
|
+
inputSchema: {
|
|
66
|
+
type: "object",
|
|
67
|
+
properties: { agentId: { type: "string" } },
|
|
68
|
+
additionalProperties: false
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
async (args) => {
|
|
72
|
+
const entry = await fancyAutoCommon.undoOne(agentOf(args));
|
|
73
|
+
if (!entry) return errorResult("Nothing to undo.");
|
|
74
|
+
return textResult(`Undid: ${entry.label}`, { entry: serialize(entry) });
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
);
|
|
78
|
+
disposers.push(
|
|
79
|
+
host.registerTool(
|
|
80
|
+
{
|
|
81
|
+
name: "agent_redo",
|
|
82
|
+
description: "Redo the most recently undone action.",
|
|
83
|
+
inputSchema: {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: { agentId: { type: "string" } },
|
|
86
|
+
additionalProperties: false
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
async (args) => {
|
|
90
|
+
const entry = await fancyAutoCommon.redoOne(agentOf(args));
|
|
91
|
+
if (!entry) return errorResult("Nothing to redo.");
|
|
92
|
+
return textResult(`Redid: ${entry.label}`, { entry: serialize(entry) });
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
);
|
|
96
|
+
disposers.push(
|
|
97
|
+
host.registerTool(
|
|
98
|
+
{
|
|
99
|
+
name: "agent_history",
|
|
100
|
+
description: "List the agent's undo stack (oldest first). Useful for understanding what's reversible.",
|
|
101
|
+
inputSchema: {
|
|
102
|
+
type: "object",
|
|
103
|
+
properties: { agentId: { type: "string" } },
|
|
104
|
+
additionalProperties: false
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
async (args) => {
|
|
108
|
+
const history = fancyAutoCommon.readHistory(agentOf(args)).map(serialize);
|
|
109
|
+
const text = history.map((e) => `${new Date(e.timestamp).toISOString()} ${e.bridgeId} ${e.action}: ${e.label}`).join("\n");
|
|
110
|
+
return textResult(text || "(empty)", history);
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
);
|
|
114
|
+
return () => disposers.forEach((d) => d());
|
|
115
|
+
}
|
|
116
|
+
function serialize(entry) {
|
|
117
|
+
return {
|
|
118
|
+
timestamp: entry.timestamp,
|
|
119
|
+
bridgeId: entry.bridgeId,
|
|
120
|
+
action: entry.action,
|
|
121
|
+
label: entry.label
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/bridges/map.ts
|
|
126
|
+
var DEFAULT_AGENT = { id: "agent", name: "Agent", color: "#7c3aed" };
|
|
127
|
+
var num = (v, fallback) => typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
128
|
+
var str = (v, fallback = "") => typeof v === "string" ? v : fallback;
|
|
129
|
+
var newId = (prefix) => `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
|
|
130
|
+
function registerMapBridge(host, options) {
|
|
131
|
+
const { adapter } = options;
|
|
132
|
+
const agent = { ...DEFAULT_AGENT, ...options.agent ?? {} };
|
|
133
|
+
const disposers = [];
|
|
134
|
+
ensureUndoToolsRegistered(host, { defaultAgentId: agent.id });
|
|
135
|
+
const mTarget = (args, result) => ({
|
|
136
|
+
kind: "map",
|
|
137
|
+
elementId: result?.structuredContent?.id ?? args?.id
|
|
138
|
+
});
|
|
139
|
+
const reg = (name, description, properties, required, handler, resolveTarget) => {
|
|
140
|
+
const wrapped = async (args) => {
|
|
141
|
+
try {
|
|
142
|
+
return await handler(args);
|
|
143
|
+
} catch (e) {
|
|
144
|
+
return errorResult(e instanceof Error ? e.message : String(e));
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
const final = resolveTarget ? wrapToolWithActivity(wrapped, {
|
|
148
|
+
toolName: name,
|
|
149
|
+
agent: { id: agent.id, name: agent.name, color: agent.color },
|
|
150
|
+
kind: "map",
|
|
151
|
+
resolveTarget: ({ args, result }) => resolveTarget(args, result)
|
|
152
|
+
}) : wrapped;
|
|
153
|
+
disposers.push(
|
|
154
|
+
host.registerTool(
|
|
155
|
+
{
|
|
156
|
+
name,
|
|
157
|
+
description,
|
|
158
|
+
inputSchema: {
|
|
159
|
+
type: "object",
|
|
160
|
+
properties,
|
|
161
|
+
required,
|
|
162
|
+
additionalProperties: false
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
final
|
|
166
|
+
)
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
reg("map_get_state", "Get the map's camera, markers, and current selection.", {}, [], () => {
|
|
170
|
+
const state = {
|
|
171
|
+
view: adapter.getView(),
|
|
172
|
+
markers: adapter.getMarkers(),
|
|
173
|
+
selectedId: adapter.getSelected()
|
|
174
|
+
};
|
|
175
|
+
return textResult(JSON.stringify(state, null, 2), state);
|
|
176
|
+
});
|
|
177
|
+
reg(
|
|
178
|
+
"map_set_view",
|
|
179
|
+
"Move the camera to an absolute position. Any omitted field is left unchanged.",
|
|
180
|
+
{
|
|
181
|
+
lat: { type: "number", description: "New center latitude." },
|
|
182
|
+
lng: { type: "number", description: "New center longitude." },
|
|
183
|
+
zoom: { type: "number", description: "New zoom level (higher = closer)." }
|
|
184
|
+
},
|
|
185
|
+
[],
|
|
186
|
+
(args) => {
|
|
187
|
+
const prev = adapter.getView();
|
|
188
|
+
const next = {
|
|
189
|
+
center: {
|
|
190
|
+
lat: args.lat !== void 0 ? num(args.lat) : prev.center.lat,
|
|
191
|
+
lng: args.lng !== void 0 ? num(args.lng) : prev.center.lng
|
|
192
|
+
},
|
|
193
|
+
zoom: args.zoom !== void 0 ? num(args.zoom) : prev.zoom
|
|
194
|
+
};
|
|
195
|
+
adapter.setView(next);
|
|
196
|
+
fancyAutoCommon.pushUndoEntry(agent.id, {
|
|
197
|
+
timestamp: Date.now(),
|
|
198
|
+
bridgeId: "map",
|
|
199
|
+
action: "map_set_view",
|
|
200
|
+
label: `Moved camera to ${next.center.lat.toFixed(4)},${next.center.lng.toFixed(4)} z${next.zoom}`,
|
|
201
|
+
undo: () => adapter.setView(prev),
|
|
202
|
+
redo: () => adapter.setView(next)
|
|
203
|
+
});
|
|
204
|
+
return textResult(`Camera \u2192 ${next.center.lat.toFixed(4)},${next.center.lng.toFixed(4)} z${next.zoom}`, next);
|
|
205
|
+
},
|
|
206
|
+
mTarget
|
|
207
|
+
);
|
|
208
|
+
reg(
|
|
209
|
+
"map_pan",
|
|
210
|
+
"Pan the camera by a relative offset in degrees (positive dLat = north, positive dLng = east).",
|
|
211
|
+
{ dLat: { type: "number" }, dLng: { type: "number" } },
|
|
212
|
+
[],
|
|
213
|
+
(args) => {
|
|
214
|
+
const prev = adapter.getView();
|
|
215
|
+
const next = {
|
|
216
|
+
...prev,
|
|
217
|
+
center: { lat: prev.center.lat + num(args.dLat), lng: prev.center.lng + num(args.dLng) }
|
|
218
|
+
};
|
|
219
|
+
adapter.setView(next);
|
|
220
|
+
fancyAutoCommon.pushUndoEntry(agent.id, {
|
|
221
|
+
timestamp: Date.now(),
|
|
222
|
+
bridgeId: "map",
|
|
223
|
+
action: "map_pan",
|
|
224
|
+
label: "Panned camera",
|
|
225
|
+
undo: () => adapter.setView(prev),
|
|
226
|
+
redo: () => adapter.setView(next)
|
|
227
|
+
});
|
|
228
|
+
return textResult(`Panned to ${next.center.lat.toFixed(4)},${next.center.lng.toFixed(4)}`, next);
|
|
229
|
+
},
|
|
230
|
+
mTarget
|
|
231
|
+
);
|
|
232
|
+
reg(
|
|
233
|
+
"map_zoom",
|
|
234
|
+
"Zoom the camera by a relative delta (e.g. +1 zooms in one level, -2 out two).",
|
|
235
|
+
{ delta: { type: "number" } },
|
|
236
|
+
["delta"],
|
|
237
|
+
(args) => {
|
|
238
|
+
const prev = adapter.getView();
|
|
239
|
+
const next = { ...prev, zoom: prev.zoom + num(args.delta) };
|
|
240
|
+
adapter.setView(next);
|
|
241
|
+
fancyAutoCommon.pushUndoEntry(agent.id, {
|
|
242
|
+
timestamp: Date.now(),
|
|
243
|
+
bridgeId: "map",
|
|
244
|
+
action: "map_zoom",
|
|
245
|
+
label: `Zoom \u2192 ${next.zoom}`,
|
|
246
|
+
undo: () => adapter.setView(prev),
|
|
247
|
+
redo: () => adapter.setView(next)
|
|
248
|
+
});
|
|
249
|
+
return textResult(`Zoom \u2192 ${next.zoom}`, next);
|
|
250
|
+
}
|
|
251
|
+
);
|
|
252
|
+
reg(
|
|
253
|
+
"map_fit_bounds",
|
|
254
|
+
"Fit the camera to enclose points. Pass `points`, or omit to fit all current markers.",
|
|
255
|
+
{
|
|
256
|
+
points: {
|
|
257
|
+
type: "array",
|
|
258
|
+
description: "Points to enclose, e.g. [{lat,lng},\u2026].",
|
|
259
|
+
items: {
|
|
260
|
+
type: "object",
|
|
261
|
+
properties: { lat: { type: "number" }, lng: { type: "number" } },
|
|
262
|
+
required: ["lat", "lng"]
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
padding: { type: "number", description: "Padding in pixels (default 40)." }
|
|
266
|
+
},
|
|
267
|
+
[],
|
|
268
|
+
(args) => {
|
|
269
|
+
if (!adapter.fitBounds) {
|
|
270
|
+
return errorResult("Host did not wire fitBounds (pass the <Map onReady> handle's fitBounds).");
|
|
271
|
+
}
|
|
272
|
+
const points = Array.isArray(args.points) ? args.points.map((p) => ({ lat: num(p?.lat), lng: num(p?.lng) })) : adapter.getMarkers().map((m) => m.position);
|
|
273
|
+
if (!points.length) {
|
|
274
|
+
return errorResult("No points to fit (map has no markers).");
|
|
275
|
+
}
|
|
276
|
+
adapter.fitBounds(points, args.padding !== void 0 ? num(args.padding) : void 0);
|
|
277
|
+
return textResult(`Fit camera to ${points.length} point(s).`);
|
|
278
|
+
}
|
|
279
|
+
);
|
|
280
|
+
reg(
|
|
281
|
+
"map_add_marker",
|
|
282
|
+
"Drop a marker. `id` is optional (auto-generated); `icon` is an emoji or 1\u20132 chars.",
|
|
283
|
+
{
|
|
284
|
+
id: { type: "string" },
|
|
285
|
+
lat: { type: "number" },
|
|
286
|
+
lng: { type: "number" },
|
|
287
|
+
label: { type: "string" },
|
|
288
|
+
color: { type: "string", description: "CSS color, e.g. #2563eb." },
|
|
289
|
+
icon: { type: "string", description: "Emoji / short text in the pin." },
|
|
290
|
+
draggable: { type: "boolean" },
|
|
291
|
+
data: { type: "object", description: "Arbitrary JSON payload carried with the marker." }
|
|
292
|
+
},
|
|
293
|
+
["lat", "lng"],
|
|
294
|
+
(args) => {
|
|
295
|
+
const id = args.id ? str(args.id) : newId("m");
|
|
296
|
+
if (adapter.getMarkers().some((m) => m.id === id)) {
|
|
297
|
+
return errorResult(`A marker with id ${id} already exists \u2014 use map_update_marker.`);
|
|
298
|
+
}
|
|
299
|
+
const marker = {
|
|
300
|
+
id,
|
|
301
|
+
position: { lat: num(args.lat), lng: num(args.lng) },
|
|
302
|
+
...args.label !== void 0 ? { label: str(args.label) } : {},
|
|
303
|
+
...args.color !== void 0 ? { color: str(args.color) } : {},
|
|
304
|
+
...args.icon !== void 0 ? { icon: str(args.icon) } : {},
|
|
305
|
+
...args.draggable !== void 0 ? { draggable: !!args.draggable } : {},
|
|
306
|
+
...args.data && typeof args.data === "object" ? { data: args.data } : {}
|
|
307
|
+
};
|
|
308
|
+
adapter.setMarkers((all) => [...all, marker]);
|
|
309
|
+
fancyAutoCommon.pushUndoEntry(agent.id, {
|
|
310
|
+
timestamp: Date.now(),
|
|
311
|
+
bridgeId: "map",
|
|
312
|
+
action: "map_add_marker",
|
|
313
|
+
label: `Added marker ${id}`,
|
|
314
|
+
undo: () => adapter.setMarkers((all) => all.filter((m) => m.id !== id)),
|
|
315
|
+
redo: () => adapter.setMarkers((all) => [...all, marker])
|
|
316
|
+
});
|
|
317
|
+
return textResult(`Added marker ${id} at ${marker.position.lat.toFixed(4)},${marker.position.lng.toFixed(4)}`, marker);
|
|
318
|
+
},
|
|
319
|
+
mTarget
|
|
320
|
+
);
|
|
321
|
+
reg(
|
|
322
|
+
"map_update_marker",
|
|
323
|
+
"Update a marker. Only provided fields change \u2014 update lat/lng repeatedly to drive a live track.",
|
|
324
|
+
{
|
|
325
|
+
id: { type: "string" },
|
|
326
|
+
lat: { type: "number" },
|
|
327
|
+
lng: { type: "number" },
|
|
328
|
+
label: { type: "string" },
|
|
329
|
+
color: { type: "string" },
|
|
330
|
+
icon: { type: "string" },
|
|
331
|
+
draggable: { type: "boolean" }
|
|
332
|
+
},
|
|
333
|
+
["id"],
|
|
334
|
+
(args) => {
|
|
335
|
+
const id = str(args.id);
|
|
336
|
+
const prev = adapter.getMarkers().find((m) => m.id === id);
|
|
337
|
+
if (!prev) {
|
|
338
|
+
return errorResult(`No marker with id ${id}`);
|
|
339
|
+
}
|
|
340
|
+
const next = {
|
|
341
|
+
...prev,
|
|
342
|
+
position: {
|
|
343
|
+
lat: args.lat !== void 0 ? num(args.lat) : prev.position.lat,
|
|
344
|
+
lng: args.lng !== void 0 ? num(args.lng) : prev.position.lng
|
|
345
|
+
},
|
|
346
|
+
...args.label !== void 0 ? { label: str(args.label) } : {},
|
|
347
|
+
...args.color !== void 0 ? { color: str(args.color) } : {},
|
|
348
|
+
...args.icon !== void 0 ? { icon: str(args.icon) } : {},
|
|
349
|
+
...args.draggable !== void 0 ? { draggable: !!args.draggable } : {}
|
|
350
|
+
};
|
|
351
|
+
adapter.setMarkers((all) => all.map((m) => m.id === id ? next : m));
|
|
352
|
+
fancyAutoCommon.pushUndoEntry(agent.id, {
|
|
353
|
+
timestamp: Date.now(),
|
|
354
|
+
bridgeId: "map",
|
|
355
|
+
action: "map_update_marker",
|
|
356
|
+
label: `Updated marker ${id}`,
|
|
357
|
+
undo: () => adapter.setMarkers((all) => all.map((m) => m.id === id ? prev : m)),
|
|
358
|
+
redo: () => adapter.setMarkers((all) => all.map((m) => m.id === id ? next : m))
|
|
359
|
+
});
|
|
360
|
+
return textResult(`Updated marker ${id}`, next);
|
|
361
|
+
},
|
|
362
|
+
mTarget
|
|
363
|
+
);
|
|
364
|
+
reg(
|
|
365
|
+
"map_remove_marker",
|
|
366
|
+
"Remove a marker by id.",
|
|
367
|
+
{ id: { type: "string" } },
|
|
368
|
+
["id"],
|
|
369
|
+
(args) => {
|
|
370
|
+
const id = str(args.id);
|
|
371
|
+
const prev = adapter.getMarkers().find((m) => m.id === id);
|
|
372
|
+
if (!prev) {
|
|
373
|
+
return errorResult(`No marker with id ${id}`);
|
|
374
|
+
}
|
|
375
|
+
adapter.setMarkers((all) => all.filter((m) => m.id !== id));
|
|
376
|
+
fancyAutoCommon.pushUndoEntry(agent.id, {
|
|
377
|
+
timestamp: Date.now(),
|
|
378
|
+
bridgeId: "map",
|
|
379
|
+
action: "map_remove_marker",
|
|
380
|
+
label: `Removed marker ${id}`,
|
|
381
|
+
undo: () => adapter.setMarkers((all) => [...all, prev]),
|
|
382
|
+
redo: () => adapter.setMarkers((all) => all.filter((m) => m.id !== id))
|
|
383
|
+
});
|
|
384
|
+
return textResult(`Removed marker ${id}`);
|
|
385
|
+
},
|
|
386
|
+
mTarget
|
|
387
|
+
);
|
|
388
|
+
reg(
|
|
389
|
+
"map_select",
|
|
390
|
+
"Select a marker (or pass no id / null to clear the selection).",
|
|
391
|
+
{ id: { type: "string" } },
|
|
392
|
+
[],
|
|
393
|
+
(args) => {
|
|
394
|
+
const prev = adapter.getSelected();
|
|
395
|
+
const id = args.id === void 0 || args.id === null ? null : str(args.id);
|
|
396
|
+
if (id !== null && !adapter.getMarkers().some((m) => m.id === id)) {
|
|
397
|
+
return errorResult(`No marker with id ${id}`);
|
|
398
|
+
}
|
|
399
|
+
adapter.setSelected(id);
|
|
400
|
+
fancyAutoCommon.pushUndoEntry(agent.id, {
|
|
401
|
+
timestamp: Date.now(),
|
|
402
|
+
bridgeId: "map",
|
|
403
|
+
action: "map_select",
|
|
404
|
+
label: id ? `Selected ${id}` : "Cleared selection",
|
|
405
|
+
undo: () => adapter.setSelected(prev),
|
|
406
|
+
redo: () => adapter.setSelected(id)
|
|
407
|
+
});
|
|
408
|
+
return textResult(id ? `Selected marker ${id}` : "Cleared selection", { selectedId: id });
|
|
409
|
+
},
|
|
410
|
+
mTarget
|
|
411
|
+
);
|
|
412
|
+
reg(
|
|
413
|
+
"map_start_track",
|
|
414
|
+
"Make the camera follow a marker as its position updates (live tracking). Requires the host to wire setFollow.",
|
|
415
|
+
{ id: { type: "string" } },
|
|
416
|
+
["id"],
|
|
417
|
+
(args) => {
|
|
418
|
+
if (!adapter.setFollow) {
|
|
419
|
+
return errorResult("Host did not wire setFollow (pass the map's `follow` setter).");
|
|
420
|
+
}
|
|
421
|
+
const id = str(args.id);
|
|
422
|
+
if (!adapter.getMarkers().some((m) => m.id === id)) {
|
|
423
|
+
return errorResult(`No marker with id ${id}`);
|
|
424
|
+
}
|
|
425
|
+
adapter.setFollow(id);
|
|
426
|
+
return textResult(`Camera now following ${id}`, { follow: id });
|
|
427
|
+
},
|
|
428
|
+
mTarget
|
|
429
|
+
);
|
|
430
|
+
reg(
|
|
431
|
+
"map_stop_track",
|
|
432
|
+
"Stop following any marker (releases the camera). Requires the host to wire setFollow.",
|
|
433
|
+
{},
|
|
434
|
+
[],
|
|
435
|
+
() => {
|
|
436
|
+
if (!adapter.setFollow) {
|
|
437
|
+
return errorResult("Host did not wire setFollow.");
|
|
438
|
+
}
|
|
439
|
+
adapter.setFollow(null);
|
|
440
|
+
return textResult("Camera released (no longer following).", { follow: null });
|
|
441
|
+
}
|
|
442
|
+
);
|
|
443
|
+
return {
|
|
444
|
+
id: "map",
|
|
445
|
+
title: "Map",
|
|
446
|
+
dispose: () => {
|
|
447
|
+
for (const d of disposers) d();
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
exports.registerMapBridge = registerMapBridge;
|
|
453
|
+
//# sourceMappingURL=bridges-map.cjs.map
|
|
454
|
+
//# sourceMappingURL=bridges-map.cjs.map
|