Files
planet/frontend/public/earth/js/earth-interactables.js
linkong 899e3bce43
Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
release / images (push) Has been cancelled
ci / delivery (push) Has been cancelled
release: bump version to 0.71.0
2026-06-11 16:47:24 +08:00

186 lines
5.5 KiB
JavaScript

import { PATHS } from "./constants.js";
import { createInteractableLayer, SURFACE_AVOIDANCE_PROFILES } from "./interactable.js";
const interactableRevisions = new Map();
function featureToInteractableItem(feature) {
const props = feature?.properties || {};
const coordinates = feature?.geometry?.coordinates || [];
const longitude = Number(props.longitude ?? coordinates[0]);
const latitude = Number(props.latitude ?? coordinates[1]);
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return null;
return {
...props,
id: String(props.id ?? feature.id ?? ""),
latitude,
longitude,
kind: props.kind || "default",
label: props.label || "",
revision: Number(props.revision || 0),
};
}
function normalizeInteractableItem(item) {
const latitude = Number(item?.latitude ?? item?.lat);
const longitude = Number(item?.longitude ?? item?.lon ?? item?.lng);
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return null;
const id = String(item?.id || "").trim();
if (!id) return null;
return {
...item,
id,
latitude,
longitude,
kind: item.kind || "default",
label: item.label || "",
revision: Number(item.revision || 0),
};
}
function drawInteractableIcon(context, { glow = false, color = "#7dd3fc" }) {
if (glow) {
context.shadowColor = color;
context.shadowBlur = 16;
}
context.fillStyle = color;
context.strokeStyle = "rgba(8, 15, 27, 0.92)";
context.lineWidth = 7;
context.beginPath();
context.arc(0, 0, 27, 0, Math.PI * 2);
context.fill();
context.stroke();
context.fillStyle = "rgba(255,255,255,0.95)";
context.beginPath();
context.arc(0, 0, 9, 0, Math.PI * 2);
context.fill();
}
const earthInteractableLayer = createInteractableLayer({
id: "earthInteractables",
objectType: "earth_interactable",
renderOrder: 4.55,
altitudeOffset: 0.28,
pointSize: 30,
colors: {
normal: "#7dd3fc",
default: "#7dd3fc",
note: "#facc15",
alert: "#fb7185",
place: "#86efac",
},
opacity: {
normal: 0.9,
dimmed: 0.3,
hover: 1,
locked: 1,
},
stateScale: {
hover: 1.18,
locked: 1.34,
dimmed: 0.82,
},
avoidance: SURFACE_AVOIDANCE_PROFILES.city,
icon: {
draw: drawInteractableIcon,
},
getPosition: (item) => ({
latitude: item.latitude,
longitude: item.longitude,
}),
getKind: (item) => item.kind || "default",
getUserData: (item) => ({
...item,
type: "earth_interactable",
}),
cluster: {
strategy: "stable-spherical",
},
});
export async function loadEarthInteractables(earth, { silent = false } = {}) {
if (!earth) return { totalCount: 0 };
const response = await fetch(PATHS.interactablesApi, { cache: "no-store" });
if (!response.ok) {
throw new Error(`Failed to load Earth interactables: ${response.status}`);
}
const payload = await response.json();
const items = (payload.features || [])
.map(featureToInteractableItem)
.filter(Boolean);
interactableRevisions.clear();
items.forEach((item) => interactableRevisions.set(item.id, Number(item.revision || 0)));
earthInteractableLayer.setData(items);
earthInteractableLayer.attach(earth);
earthInteractableLayer.setVisible(true);
if (!silent) {
console.info("Earth interactables loaded", { count: items.length });
}
return { totalCount: earthInteractableLayer.getCount() };
}
export function clearEarthInteractables(earth) {
interactableRevisions.clear();
earthInteractableLayer.setData([]);
if (earth) earthInteractableLayer.attach(earth);
}
export function refreshEarthInteractables(earth) {
return loadEarthInteractables(earth, { silent: true });
}
export function applyEarthInteractableEvent(earth, payload = {}) {
if (payload.entity !== "interactable") return false;
const action = payload.action;
const item = normalizeInteractableItem(payload.item);
const ids = Array.isArray(payload.ids) ? payload.ids.map(String) : [];
const id = item?.id || ids[0];
if (!id) return false;
const nextRevision = Number(payload.revision ?? item?.revision ?? 0);
const currentRevision = Number(interactableRevisions.get(id) || 0);
if (nextRevision && currentRevision && nextRevision < currentRevision) {
return false;
}
if (action === "deleted") {
const changed = earthInteractableLayer.removeItem(id);
interactableRevisions.set(id, nextRevision || currentRevision);
return changed;
}
if (!item) {
refreshEarthInteractables(earth).catch((error) => {
console.warn("刷新 Earth interactables 失败:", error);
});
return false;
}
const changed = earthInteractableLayer.upsertItem(item);
interactableRevisions.set(id, nextRevision || Number(item.revision || 0));
earthInteractableLayer.attach(earth);
earthInteractableLayer.setVisible(true);
return changed;
}
export function getEarthInteractableMarkers() {
return earthInteractableLayer.getMarkers();
}
export function getEarthInteractablePointerIntersections(options = {}) {
return earthInteractableLayer.getPointerIntersections(options);
}
export function setEarthInteractableMarkerState(marker, state = "normal") {
earthInteractableLayer.setMarkerState(marker, state);
}
export function clearEarthInteractableSelection() {
earthInteractableLayer.getMarkers().forEach((marker) => {
earthInteractableLayer.setMarkerState(marker, "normal");
});
}
export function updateEarthInteractableVisualState(focusType, focusObject, camera) {
earthInteractableLayer.updateVisualState(focusType, focusObject, camera);
}