Skip to content

Commit fc59ca8

Browse files
ochafikclaude
andcommitted
fix: separate point and label entities, fix marker rendering
- Split marker into two Cesium entities (point + billboard label) to avoid point/billboard conflict that suppressed labels entirely - Remove heightReference: CLAMP_TO_GROUND (no terrain loaded, caused 3D offset on tilted views) - Use canvas roundRect() for cleaner rounded corners - Use actualBoundingBoxAscent/Descent for precise text centering - Add hint in show-map description: skip markers for single location - Update/remove now correctly handles both entities Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0fcdfd2 commit fc59ca8

2 files changed

Lines changed: 61 additions & 47 deletions

File tree

examples/map-server/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ export function createServer(): McpServer {
234234
{
235235
title: "Show Map",
236236
description:
237-
"Display an interactive world map. Specify the view with either a bounding box (`west`/`south`/`east`/`north`) or a center point (`latitude`/`longitude`) with optional `radiusKm` (default 50). Optionally pass initial `markers`.",
237+
"Display an interactive world map. Specify the view with either a bounding box (`west`/`south`/`east`/`north`) or a center point (`latitude`/`longitude`) with optional `radiusKm` (default 50). Optionally pass initial `markers` (useful when showing multiple points; skip for a single location as the map already centers there).",
238238
inputSchema: {
239239
west: z
240240
.number()

examples/map-server/src/mcp-app.ts

Lines changed: 60 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -822,7 +822,8 @@ interface MarkerDef {
822822

823823
interface TrackedMarker extends MarkerDef {
824824
id: string;
825-
entity: any; // Cesium.Entity
825+
pointEntity: any; // Cesium.Entity for the dot
826+
labelEntity: any; // Cesium.Entity for the billboard label (or null)
826827
}
827828

828829
type MapCommand =
@@ -943,18 +944,20 @@ function flyToBoundingBox(
943944
*/
944945
function renderLabelImage(text: string): string {
945946
const dpr = window.devicePixelRatio || 1;
946-
const fontSize = 13 * dpr;
947+
const fontSize = Math.round(13 * dpr);
947948
const font = `bold ${fontSize}px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`;
948-
const padX = 8 * dpr;
949-
const padY = 5 * dpr;
950-
const radius = 5 * dpr;
949+
const padX = Math.round(8 * dpr);
950+
const padY = Math.round(5 * dpr);
951+
const radius = Math.round(5 * dpr);
951952

952953
// Measure text
953954
const measure = document.createElement("canvas").getContext("2d")!;
954955
measure.font = font;
955956
const metrics = measure.measureText(text);
956957
const textW = Math.ceil(metrics.width);
957-
const textH = fontSize; // approximate em height
958+
const ascent = Math.ceil(metrics.actualBoundingBoxAscent || fontSize * 0.8);
959+
const descent = Math.ceil(metrics.actualBoundingBoxDescent || fontSize * 0.2);
960+
const textH = ascent + descent;
958961

959962
const w = textW + padX * 2;
960963
const h = textH + padY * 2;
@@ -966,25 +969,16 @@ function renderLabelImage(text: string): string {
966969

967970
// Rounded rect background
968971
ctx.beginPath();
969-
ctx.moveTo(radius, 0);
970-
ctx.lineTo(w - radius, 0);
971-
ctx.quadraticCurveTo(w, 0, w, radius);
972-
ctx.lineTo(w, h - radius);
973-
ctx.quadraticCurveTo(w, h, w - radius, h);
974-
ctx.lineTo(radius, h);
975-
ctx.quadraticCurveTo(0, h, 0, h - radius);
976-
ctx.lineTo(0, radius);
977-
ctx.quadraticCurveTo(0, 0, radius, 0);
978-
ctx.closePath();
972+
ctx.roundRect(0, 0, w, h, radius);
979973
ctx.fillStyle = "rgba(30, 30, 30, 0.78)";
980974
ctx.fill();
981975

982-
// Text
976+
// Text — use alphabetic baseline with computed ascent for precise centering
983977
ctx.font = font;
984-
ctx.textBaseline = "middle";
978+
ctx.textBaseline = "alphabetic";
985979
ctx.textAlign = "left";
986980
ctx.fillStyle = "#fff";
987-
ctx.fillText(text, padX, h / 2);
981+
ctx.fillText(text, padX, padY + ascent);
988982

989983
return canvas.toDataURL("image/png");
990984
}
@@ -1015,36 +1009,42 @@ function addMarker(
10151009
const cesiumColor = parseCesiumColor(color || "red");
10161010

10171011
const dpr = window.devicePixelRatio || 1;
1018-
const entityOptions: any = {
1012+
1013+
// Point entity (the colored dot)
1014+
const pointEntity = cesiumViewer.entities.add({
10191015
position,
10201016
point: {
10211017
pixelSize: 12,
10221018
color: cesiumColor,
10231019
outlineColor: Cesium.Color.WHITE,
10241020
outlineWidth: 2,
1025-
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
1021+
disableDepthTestDistance: Number.POSITIVE_INFINITY,
10261022
},
1027-
};
1023+
});
10281024

1025+
// Label entity (separate so it doesn't conflict with point rendering)
1026+
let labelEntity: any = null;
10291027
if (label) {
1030-
entityOptions.billboard = {
1031-
image: renderLabelImage(label),
1032-
scale: 1 / dpr,
1033-
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
1034-
pixelOffset: new Cesium.Cartesian2(0, -14),
1035-
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
1036-
disableDepthTestDistance: Number.POSITIVE_INFINITY,
1037-
};
1028+
labelEntity = cesiumViewer.entities.add({
1029+
position,
1030+
billboard: {
1031+
image: renderLabelImage(label),
1032+
scale: 1 / dpr,
1033+
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
1034+
pixelOffset: new Cesium.Cartesian2(0, -12),
1035+
disableDepthTestDistance: Number.POSITIVE_INFINITY,
1036+
},
1037+
});
10381038
}
10391039

1040-
const entity = cesiumViewer.entities.add(entityOptions);
10411040
markerMap.set(id, {
10421041
id,
10431042
latitude: lat,
10441043
longitude: lon,
10451044
label,
10461045
color,
1047-
entity,
1046+
pointEntity,
1047+
labelEntity,
10481048
});
10491049
updateCopyButton();
10501050
persistMarkers();
@@ -1068,34 +1068,47 @@ function updateMarker(
10681068
log.warn("updateMarker: unknown id", id);
10691069
return;
10701070
}
1071-
const entity = tracked.entity;
10721071

10731072
if (updates.latitude != null || updates.longitude != null) {
10741073
const lat = updates.latitude ?? tracked.latitude;
10751074
const lon = updates.longitude ?? tracked.longitude;
1076-
entity.position = Cesium.Cartesian3.fromDegrees(lon, lat);
1075+
const pos = Cesium.Cartesian3.fromDegrees(lon, lat);
1076+
tracked.pointEntity.position = pos;
1077+
if (tracked.labelEntity) tracked.labelEntity.position = pos;
10771078
tracked.latitude = lat;
10781079
tracked.longitude = lon;
10791080
}
10801081

10811082
if (updates.color != null) {
1082-
entity.point.color = parseCesiumColor(updates.color);
1083+
tracked.pointEntity.point.color = parseCesiumColor(updates.color);
10831084
tracked.color = updates.color;
10841085
}
10851086

10861087
if (updates.label !== undefined) {
10871088
const dpr = window.devicePixelRatio || 1;
10881089
if (updates.label) {
1089-
entity.billboard = {
1090-
image: renderLabelImage(updates.label),
1091-
scale: 1 / dpr,
1092-
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
1093-
pixelOffset: new Cesium.Cartesian2(0, -14),
1094-
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
1095-
disableDepthTestDistance: Number.POSITIVE_INFINITY,
1096-
};
1097-
} else {
1098-
entity.billboard = undefined;
1090+
if (tracked.labelEntity) {
1091+
// Update existing label billboard
1092+
tracked.labelEntity.billboard.image = renderLabelImage(updates.label);
1093+
tracked.labelEntity.billboard.scale = 1 / dpr;
1094+
} else {
1095+
// Create new label entity
1096+
tracked.labelEntity = viewer!.entities.add({
1097+
position: tracked.pointEntity.position.getValue(
1098+
Cesium.JulianDate.now(),
1099+
),
1100+
billboard: {
1101+
image: renderLabelImage(updates.label),
1102+
scale: 1 / dpr,
1103+
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
1104+
pixelOffset: new Cesium.Cartesian2(0, -12),
1105+
disableDepthTestDistance: Number.POSITIVE_INFINITY,
1106+
},
1107+
});
1108+
}
1109+
} else if (tracked.labelEntity) {
1110+
viewer!.entities.remove(tracked.labelEntity);
1111+
tracked.labelEntity = null;
10991112
}
11001113
tracked.label = updates.label || undefined;
11011114
}
@@ -1114,7 +1127,8 @@ function removeMarker(cesiumViewer: any, id: string): void {
11141127
log.warn("removeMarker: unknown id", id);
11151128
return;
11161129
}
1117-
cesiumViewer.entities.remove(tracked.entity);
1130+
cesiumViewer.entities.remove(tracked.pointEntity);
1131+
if (tracked.labelEntity) cesiumViewer.entities.remove(tracked.labelEntity);
11181132
markerMap.delete(id);
11191133
updateCopyButton();
11201134
persistMarkers();

0 commit comments

Comments
 (0)