release: bump version to 0.49.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
linkong
2026-05-08 17:42:27 +08:00
parent bb9183b8a4
commit e1984c7a35
86 changed files with 9165 additions and 412 deletions

View File

@@ -11,6 +11,26 @@ const DEFAULT_AVOIDANCE_PRECISION = 4;
const DEFAULT_AVOIDANCE_RADIUS = 1.1;
const DEFAULT_AVOIDANCE_STEP = 0.35;
const AVOIDANCE_RING_SLOT_COUNT = 8;
// Named avoidance profiles. Layers that should mutex with each other (e.g. fan
// out when sharing the same city center) must reference the SAME profile —
// markers are bucketed by the resulting key, and only equal keys collide.
//
// city — ~1.1km grid (precision 2). Use for site/observatory/POI markers
// that often share a city-center coordinate from geocoding.
// precise — ~11m grid (precision 4). Use for markers with building-level
// coordinates (default; preserves prior behavior).
//
// Layers that need a fully custom cluster identity (e.g. a city ID string)
// can pass `getKey: (item, position) => "..."` instead of using a profile.
export const SURFACE_AVOIDANCE_PROFILES = Object.freeze({
city: Object.freeze({ precision: 2, radius: 1.4, step: 0.5 }),
precise: Object.freeze({
precision: DEFAULT_AVOIDANCE_PRECISION,
radius: DEFAULT_AVOIDANCE_RADIUS,
step: DEFAULT_AVOIDANCE_STEP,
}),
});
const TANGENT_EPSILON_SQ = 1e-6;
const avoidanceNorthPole = new THREE.Vector3(0, 1, 0);
const avoidanceFallbackEast = new THREE.Vector3(1, 0, 0);
@@ -58,7 +78,16 @@ function createCanvas(width, height) {
return canvas;
}
function getAvoidanceKey(position, basePosition, precision = 4) {
function getAvoidanceKey(item, position, basePosition, config) {
if (typeof config?.getKey === "function") {
const key = config.getKey(item, position, basePosition);
if (key) return String(key);
}
const precision = Number.isFinite(config?.precision)
? config.precision
: DEFAULT_AVOIDANCE_PRECISION;
if (position instanceof THREE.Vector3) {
return [
"vec",
@@ -85,11 +114,14 @@ function recomputeAvoidanceBucket(key) {
if (!entries || entries.length === 0) return;
const affectedLayerIds = new Set(entries.map((entry) => entry.layerId));
const crossLayer = affectedLayerIds.size > 1;
if (entries.length === 1) {
const entry = entries[0];
entry.marker.position.copy(entry.marker.userData.icon_base_position);
entry.marker.userData.icon_avoidance_index = 0;
entry.marker.userData.icon_avoidance_count = 1;
entry.marker.userData.icon_avoidance_layer_count = 1;
entry.marker.userData.icon_avoidance_cross_layer = false;
notifyAvoidancePositionChanged(affectedLayerIds);
return;
}
@@ -127,6 +159,8 @@ function recomputeAvoidanceBucket(key) {
entry.marker.position.copy(avoidancePositionScratch);
entry.marker.userData.icon_avoidance_index = index;
entry.marker.userData.icon_avoidance_count = count;
entry.marker.userData.icon_avoidance_layer_count = affectedLayerIds.size;
entry.marker.userData.icon_avoidance_cross_layer = crossLayer;
});
notifyAvoidancePositionChanged(affectedLayerIds);
@@ -657,9 +691,10 @@ export function createInteractableLayer(options = {}) {
if (!position) return;
const kind = getKind(item);
const avoidanceKey = getAvoidanceKey(
item,
rawPosition,
position,
avoidanceConfig.precision,
avoidanceConfig,
);
const marker = new THREE.Object3D();
marker.position.copy(position);