release: bump version to 0.44.1
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "planet-frontend",
|
||||
"version": "0.44.0",
|
||||
"version": "0.44.1",
|
||||
"private": true,
|
||||
"packageManager": "bun@1",
|
||||
"dependencies": {
|
||||
|
||||
@@ -258,6 +258,7 @@ let lastBGPClickTime = 0;
|
||||
let lastBGPClickCollector = null;
|
||||
let lastBGPClickType = null;
|
||||
let lastBGPClickPos = { x: 0, y: 0 };
|
||||
let lastVesselHoverPickTime = 0;
|
||||
let earthTexture = null;
|
||||
let animationFrameId = null;
|
||||
let initialized = false;
|
||||
@@ -295,7 +296,9 @@ const scratchBGPWorldPosition = new THREE.Vector3();
|
||||
const scratchComputeCenterDirection = new THREE.Vector3();
|
||||
const scratchComputeCenterWorldPosition = new THREE.Vector3();
|
||||
const scratchVesselDirection = new THREE.Vector3();
|
||||
const scratchVesselCameraLocal = new THREE.Vector3();
|
||||
const scratchVesselWorldPosition = new THREE.Vector3();
|
||||
const scratchVesselScreenPosition = new THREE.Vector3();
|
||||
const scratchSatelliteWorldPosition = new THREE.Vector3();
|
||||
const scratchSatelliteScreenPosition = new THREE.Vector3();
|
||||
const scratchViewCenterWorld = new THREE.Vector3();
|
||||
@@ -310,6 +313,8 @@ const TOOLTIP_CURSOR_OFFSET = 14; // px offset from cursor for hover tooltips
|
||||
const TOOLTIP_COORDS_OFFSET = 10; // px offset for earth-coordinate tooltip
|
||||
const RELATED_SATELLITE_HIGHLIGHT_COLOR = "#7dd3fc";
|
||||
const DRAG_POINTER_THRESHOLD_PX = 8;
|
||||
const VESSEL_HOVER_PICK_INTERVAL_MS = 100;
|
||||
const VESSEL_POINTER_RADIUS_PX = 22;
|
||||
const GLOBE_DRAGGING_CLASS = "is-globe-dragging";
|
||||
const HUD_INTERACTIVE_SELECTORS = [
|
||||
".earth-left-column",
|
||||
@@ -364,6 +369,13 @@ function setGlobeDraggingUiState(active) {
|
||||
document.documentElement.classList.toggle(GLOBE_DRAGGING_CLASS, active);
|
||||
}
|
||||
|
||||
function hasActiveGlobeInertia() {
|
||||
return (
|
||||
Math.abs(inertialVelocity.x) > INERTIA_MIN_VELOCITY ||
|
||||
Math.abs(inertialVelocity.y) > INERTIA_MIN_VELOCITY
|
||||
);
|
||||
}
|
||||
|
||||
function getDragRotationFactor() {
|
||||
const zoom = Math.max(getZoomLevel(), 0.01);
|
||||
const scale = THREE.MathUtils.clamp(
|
||||
@@ -540,21 +552,92 @@ function getFrontFacingVesselMarkers(markers) {
|
||||
const earth = getEarth();
|
||||
if (!earth) return markers;
|
||||
|
||||
scratchCameraToEarth.subVectors(camera.position, earth.position).normalize();
|
||||
scratchVesselCameraLocal.copy(camera.position);
|
||||
earth.worldToLocal(scratchVesselCameraLocal);
|
||||
scratchVesselCameraLocal.normalize();
|
||||
|
||||
return markers.filter((marker) => {
|
||||
scratchVesselWorldPosition.copy(marker.position);
|
||||
marker.parent?.localToWorld(scratchVesselWorldPosition);
|
||||
scratchVesselDirection
|
||||
.subVectors(scratchVesselWorldPosition, earth.position)
|
||||
.normalize();
|
||||
scratchVesselDirection.copy(marker.position).normalize();
|
||||
return (
|
||||
scratchCameraToEarth.dot(scratchVesselDirection) >
|
||||
scratchVesselCameraLocal.dot(scratchVesselDirection) >
|
||||
SATELLITE_CONFIG.frontFacingDotThreshold
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function getVesselPointerIntersections() {
|
||||
const earth = getEarth();
|
||||
if (!earth) return [];
|
||||
scratchVesselCameraLocal.copy(camera.position);
|
||||
earth.worldToLocal(scratchVesselCameraLocal);
|
||||
scratchVesselCameraLocal.normalize();
|
||||
|
||||
const pointerX = ((interactionMouse.x + 1) / 2) * window.innerWidth;
|
||||
const pointerY = ((1 - interactionMouse.y) / 2) * window.innerHeight;
|
||||
const radiusSq = VESSEL_POINTER_RADIUS_PX * VESSEL_POINTER_RADIUS_PX;
|
||||
const intersections = [];
|
||||
|
||||
getVesselMarkers().forEach((marker) => {
|
||||
scratchVesselDirection.copy(marker.position).normalize();
|
||||
if (
|
||||
scratchVesselCameraLocal.dot(scratchVesselDirection) <=
|
||||
SATELLITE_CONFIG.frontFacingDotThreshold
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
scratchVesselWorldPosition.copy(marker.position);
|
||||
earth.localToWorld(scratchVesselWorldPosition);
|
||||
scratchVesselScreenPosition.copy(scratchVesselWorldPosition).project(camera);
|
||||
if (
|
||||
scratchVesselScreenPosition.z < -1 ||
|
||||
scratchVesselScreenPosition.z > 1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const screenX = (scratchVesselScreenPosition.x * 0.5 + 0.5) * window.innerWidth;
|
||||
const screenY = (-scratchVesselScreenPosition.y * 0.5 + 0.5) * window.innerHeight;
|
||||
const deltaX = screenX - pointerX;
|
||||
const deltaY = screenY - pointerY;
|
||||
const distancePxSq = deltaX * deltaX + deltaY * deltaY;
|
||||
if (distancePxSq > radiusSq) return;
|
||||
|
||||
intersections.push({
|
||||
object: marker,
|
||||
point: scratchVesselWorldPosition.clone(),
|
||||
distance: camera.position.distanceTo(scratchVesselWorldPosition),
|
||||
distancePxSq,
|
||||
});
|
||||
});
|
||||
|
||||
return intersections.sort((a, b) => a.distancePxSq - b.distancePxSq);
|
||||
}
|
||||
|
||||
function shouldSkipVesselHoverPicking() {
|
||||
return isDragging || hasActiveGlobeInertia();
|
||||
}
|
||||
|
||||
function getVesselHoverIntersections() {
|
||||
if (!getShowVessels()) {
|
||||
return { checked: true, intersects: [] };
|
||||
}
|
||||
if (shouldSkipVesselHoverPicking()) {
|
||||
return { checked: false, intersects: [] };
|
||||
}
|
||||
|
||||
const now = performance.now();
|
||||
if (now - lastVesselHoverPickTime < VESSEL_HOVER_PICK_INTERVAL_MS) {
|
||||
return { checked: false, intersects: [] };
|
||||
}
|
||||
lastVesselHoverPickTime = now;
|
||||
|
||||
return {
|
||||
checked: true,
|
||||
intersects: getVesselPointerIntersections(),
|
||||
};
|
||||
}
|
||||
|
||||
function applyBGPHoverState(marker) {
|
||||
resetTransientBGPStates();
|
||||
if (!marker) {
|
||||
@@ -590,14 +673,14 @@ function applyComputeCenterHoverState(marker) {
|
||||
}
|
||||
|
||||
function resetTransientVesselStates() {
|
||||
getVesselMarkers().forEach((marker) => {
|
||||
if (marker !== lockedObject) {
|
||||
setVesselMarkerState(marker, "normal");
|
||||
}
|
||||
});
|
||||
if (hoveredVessel && hoveredVessel !== lockedObject) {
|
||||
setVesselMarkerState(hoveredVessel, "normal");
|
||||
}
|
||||
hoveredVessel = null;
|
||||
}
|
||||
|
||||
function applyVesselHoverState(marker) {
|
||||
if (isSameVessel(hoveredVessel, marker)) return;
|
||||
resetTransientVesselStates();
|
||||
if (!marker) {
|
||||
hoveredVessel = null;
|
||||
@@ -3134,11 +3217,8 @@ function onMouseMove(event) {
|
||||
const computeCenterIntersects = getShowComputeCenters()
|
||||
? interactionRaycaster.intersectObjects(frontFacingComputeCenterMarkers)
|
||||
: [];
|
||||
const vesselIntersects = getShowVessels()
|
||||
? interactionRaycaster.intersectObjects(
|
||||
getFrontFacingVesselMarkers(getVesselMarkers()),
|
||||
)
|
||||
: [];
|
||||
const vesselPick = getVesselHoverIntersections();
|
||||
const vesselIntersects = vesselPick.intersects;
|
||||
|
||||
let hoveredSat = null;
|
||||
let hoveredSatIndexFromIntersect = null;
|
||||
@@ -3161,7 +3241,7 @@ function onMouseMove(event) {
|
||||
const hoveredComputeCenterMarker =
|
||||
computeCenterIntersects.length > 0 ? computeCenterIntersects[0].object : null;
|
||||
const hoveredVesselMarker =
|
||||
vesselIntersects.length > 0 ? vesselIntersects[0].object : null;
|
||||
vesselPick.checked && vesselIntersects.length > 0 ? vesselIntersects[0].object : null;
|
||||
|
||||
if (
|
||||
hoveredComputeCenter &&
|
||||
@@ -3169,7 +3249,11 @@ function onMouseMove(event) {
|
||||
) {
|
||||
clearTransientHoverState();
|
||||
}
|
||||
if (hoveredVessel && !isSameVessel(hoveredVessel, hoveredVesselMarker)) {
|
||||
if (
|
||||
vesselPick.checked &&
|
||||
hoveredVessel &&
|
||||
!isSameVessel(hoveredVessel, hoveredVesselMarker)
|
||||
) {
|
||||
clearTransientHoverState();
|
||||
}
|
||||
|
||||
@@ -3216,6 +3300,7 @@ function onMouseMove(event) {
|
||||
);
|
||||
objectTooltipShown = true;
|
||||
} else if (
|
||||
vesselPick.checked &&
|
||||
hoveredVesselMarker &&
|
||||
getShowVessels() &&
|
||||
lockedObjectType !== "vessel"
|
||||
@@ -3262,7 +3347,9 @@ function onMouseMove(event) {
|
||||
} else if (!lockedObjectType && !isCruisePresentationPinned()) {
|
||||
resetTransientBGPStates();
|
||||
resetTransientComputeCenterStates();
|
||||
resetTransientVesselStates();
|
||||
if (vesselPick.checked) {
|
||||
resetTransientVesselStates();
|
||||
}
|
||||
hideInfoCard();
|
||||
}
|
||||
|
||||
@@ -3483,9 +3570,7 @@ function onClick(event) {
|
||||
)
|
||||
: [];
|
||||
const vesselIntersects = getShowVessels()
|
||||
? interactionRaycaster.intersectObjects(
|
||||
getFrontFacingVesselMarkers(getVesselMarkers()),
|
||||
)
|
||||
? getVesselPointerIntersections()
|
||||
: [];
|
||||
const satIntersects = getSatellitePointerIntersections(event);
|
||||
|
||||
@@ -3681,9 +3766,7 @@ function animate() {
|
||||
|
||||
const earth = getEarth();
|
||||
const deltaTime = clock.getDelta() * 1000;
|
||||
const hasInertia =
|
||||
Math.abs(inertialVelocity.x) > INERTIA_MIN_VELOCITY ||
|
||||
Math.abs(inertialVelocity.y) > INERTIA_MIN_VELOCITY;
|
||||
const hasInertia = hasActiveGlobeInertia();
|
||||
|
||||
if (getAutoRotate() && getRotationMode() === ROTATION_MODE.ROTATE && earth) {
|
||||
earth.rotation.y += CONFIG.rotationSpeed * (deltaTime / 16);
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
import * as THREE from "three";
|
||||
|
||||
import { CONFIG, PATHS, VESSEL_CONFIG } from "./constants.js";
|
||||
import { getSurfaceMarkerCameraScale, latLonToVector3 } from "./utils.js";
|
||||
import { latLonToVector3 } from "./utils.js";
|
||||
|
||||
const vesselGroup = new THREE.Group();
|
||||
const vesselMarkers = [];
|
||||
const textureCache = new Map();
|
||||
let vesselPoints = null;
|
||||
let vesselPointObjects = [];
|
||||
let hoverOverlaySprite = null;
|
||||
let lockedOverlaySprite = null;
|
||||
let showVessels = false;
|
||||
let activeTrackLine = null;
|
||||
let lastVisualStateKey = "";
|
||||
let visualStateVersion = 0;
|
||||
|
||||
const VESSEL_RENDER_ORDER = 4.4;
|
||||
const VESSEL_POINT_SIZE = 34;
|
||||
const VESSEL_ATLAS_CELL_SIZE = 128;
|
||||
const VESSEL_COURSE_BINS = 32;
|
||||
|
||||
function invalidateVesselVisualState() {
|
||||
visualStateVersion += 1;
|
||||
lastVisualStateKey = "";
|
||||
}
|
||||
|
||||
function normalizeVesselType(value, code) {
|
||||
const type = String(value || "").trim().toLowerCase();
|
||||
@@ -22,36 +36,87 @@ function normalizeVesselType(value, code) {
|
||||
return "other";
|
||||
}
|
||||
|
||||
function createVesselTexture(type, anchored) {
|
||||
const textureKey = `${type}:${anchored ? "anchored" : "moving"}`;
|
||||
if (textureCache.has(textureKey)) return textureCache.get(textureKey);
|
||||
|
||||
const color = VESSEL_CONFIG.colors[type] || VESSEL_CONFIG.colors.other;
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = 96;
|
||||
canvas.height = 96;
|
||||
const context = canvas.getContext("2d");
|
||||
context.clearRect(0, 0, 96, 96);
|
||||
context.save();
|
||||
context.translate(48, 48);
|
||||
function drawVesselShape(context, anchored, glow, color = "#ffffff") {
|
||||
context.fillStyle = color;
|
||||
context.globalAlpha = anchored ? 0.55 : 0.96;
|
||||
context.shadowColor = color;
|
||||
context.shadowBlur = anchored ? 8 : 14;
|
||||
if (glow) {
|
||||
context.shadowColor = color;
|
||||
context.shadowBlur = anchored ? 8 : 14;
|
||||
}
|
||||
context.beginPath();
|
||||
if (anchored) {
|
||||
context.arc(0, 0, 18, 0, Math.PI * 2);
|
||||
context.arc(0, 0, 24, 0, Math.PI * 2);
|
||||
} else {
|
||||
context.moveTo(0, -28);
|
||||
context.lineTo(21, 24);
|
||||
context.lineTo(0, 13);
|
||||
context.lineTo(-21, 24);
|
||||
context.moveTo(0, -37);
|
||||
context.lineTo(28, 32);
|
||||
context.lineTo(0, 17);
|
||||
context.lineTo(-28, 32);
|
||||
context.closePath();
|
||||
}
|
||||
context.fill();
|
||||
}
|
||||
|
||||
function createVesselPointTexture(anchored, courseBin = 0) {
|
||||
const textureKey = `vessel-point:${anchored ? "anchored" : "moving"}:${courseBin}`;
|
||||
if (textureCache.has(textureKey)) return textureCache.get(textureKey);
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = VESSEL_ATLAS_CELL_SIZE;
|
||||
canvas.height = VESSEL_ATLAS_CELL_SIZE;
|
||||
const context = canvas.getContext("2d");
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
context.save();
|
||||
context.translate(canvas.width / 2, canvas.height / 2);
|
||||
if (!anchored) {
|
||||
context.rotate((courseBin / VESSEL_COURSE_BINS) * Math.PI * 2);
|
||||
}
|
||||
drawVesselShape(context, anchored, false);
|
||||
context.restore();
|
||||
|
||||
const texture = new THREE.CanvasTexture(canvas);
|
||||
texture.generateMipmaps = false;
|
||||
texture.minFilter = THREE.LinearFilter;
|
||||
texture.magFilter = THREE.LinearFilter;
|
||||
texture.needsUpdate = true;
|
||||
textureCache.set(textureKey, texture);
|
||||
return texture;
|
||||
}
|
||||
|
||||
function createVesselOverlayTexture(marker, glow = true) {
|
||||
const kind = marker?.userData?.vessel_kind || "other";
|
||||
const anchored = Boolean(marker?.userData?.anchored);
|
||||
const courseBin = marker ? getCourseBin(marker) : 0;
|
||||
const textureKey = [
|
||||
"vessel-overlay",
|
||||
kind,
|
||||
anchored ? "anchored" : "moving",
|
||||
courseBin,
|
||||
glow ? "glow" : "plain",
|
||||
].join(":");
|
||||
if (textureCache.has(textureKey)) return textureCache.get(textureKey);
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = VESSEL_ATLAS_CELL_SIZE;
|
||||
canvas.height = VESSEL_ATLAS_CELL_SIZE;
|
||||
const context = canvas.getContext("2d");
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
context.save();
|
||||
context.translate(canvas.width / 2, canvas.height / 2);
|
||||
if (!anchored) {
|
||||
context.rotate((courseBin / VESSEL_COURSE_BINS) * Math.PI * 2);
|
||||
}
|
||||
drawVesselShape(
|
||||
context,
|
||||
anchored,
|
||||
glow,
|
||||
VESSEL_CONFIG.colors[kind] || VESSEL_CONFIG.colors.other,
|
||||
);
|
||||
context.restore();
|
||||
|
||||
const texture = new THREE.CanvasTexture(canvas);
|
||||
texture.generateMipmaps = false;
|
||||
texture.minFilter = THREE.LinearFilter;
|
||||
texture.magFilter = THREE.LinearFilter;
|
||||
texture.needsUpdate = true;
|
||||
textureCache.set(textureKey, texture);
|
||||
return texture;
|
||||
@@ -80,16 +145,7 @@ function buildVesselMarkerData(feature) {
|
||||
}
|
||||
|
||||
function createVesselMarker(markerData) {
|
||||
const material = new THREE.SpriteMaterial({
|
||||
map: createVesselTexture(markerData.type, markerData.anchored),
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
opacity: VESSEL_CONFIG.marker.baseOpacity,
|
||||
rotation: markerData.anchored
|
||||
? 0
|
||||
: THREE.MathUtils.degToRad(-markerData.course),
|
||||
});
|
||||
const marker = new THREE.Sprite(material);
|
||||
const marker = new THREE.Object3D();
|
||||
marker.position.copy(
|
||||
latLonToVector3(
|
||||
markerData.latitude,
|
||||
@@ -97,9 +153,6 @@ function createVesselMarker(markerData) {
|
||||
CONFIG.earthRadius + VESSEL_CONFIG.altitudeOffset,
|
||||
),
|
||||
);
|
||||
marker.scale.setScalar(VESSEL_CONFIG.marker.baseScale);
|
||||
marker.renderOrder = VESSEL_RENDER_ORDER;
|
||||
marker.visible = showVessels;
|
||||
marker.userData = {
|
||||
...markerData,
|
||||
type: "vessel",
|
||||
@@ -107,10 +160,88 @@ function createVesselMarker(markerData) {
|
||||
baseScale: VESSEL_CONFIG.marker.baseScale,
|
||||
state: "normal",
|
||||
};
|
||||
vesselGroup.add(marker);
|
||||
vesselMarkers.push(marker);
|
||||
}
|
||||
|
||||
function colorToRgbArray(colorValue) {
|
||||
const color = new THREE.Color(colorValue || VESSEL_CONFIG.colors.other);
|
||||
return [color.r, color.g, color.b];
|
||||
}
|
||||
|
||||
function getCourseBin(marker) {
|
||||
if (marker.userData.anchored) return 0;
|
||||
const course = Number(marker.userData.course || 0);
|
||||
const normalized = ((course % 360) + 360) % 360;
|
||||
return Math.round((normalized / 360) * VESSEL_COURSE_BINS) % VESSEL_COURSE_BINS;
|
||||
}
|
||||
|
||||
function buildVesselPoints() {
|
||||
vesselPoints = new THREE.Group();
|
||||
vesselPoints.visible = showVessels;
|
||||
vesselPoints.renderOrder = VESSEL_RENDER_ORDER;
|
||||
vesselPoints.userData = { type: "vessel_points" };
|
||||
vesselPointObjects = [];
|
||||
|
||||
const groups = new Map();
|
||||
vesselMarkers.forEach((marker, index) => {
|
||||
const anchored = Boolean(marker.userData.anchored);
|
||||
const courseBin = getCourseBin(marker);
|
||||
const key = `${anchored ? "anchored" : "moving"}:${courseBin}`;
|
||||
if (!groups.has(key)) {
|
||||
groups.set(key, {
|
||||
anchored,
|
||||
courseBin,
|
||||
markers: [],
|
||||
markerIndexes: [],
|
||||
});
|
||||
}
|
||||
const group = groups.get(key);
|
||||
group.markers.push(marker);
|
||||
group.markerIndexes.push(index);
|
||||
});
|
||||
|
||||
groups.forEach((group) => {
|
||||
const count = group.markers.length;
|
||||
const positions = new Float32Array(count * 3);
|
||||
const colors = new Float32Array(count * 3);
|
||||
group.markers.forEach((marker, index) => {
|
||||
positions[index * 3] = marker.position.x;
|
||||
positions[index * 3 + 1] = marker.position.y;
|
||||
positions[index * 3 + 2] = marker.position.z;
|
||||
const [r, g, b] = colorToRgbArray(VESSEL_CONFIG.colors[marker.userData.vessel_kind]);
|
||||
colors[index * 3] = r;
|
||||
colors[index * 3 + 1] = g;
|
||||
colors[index * 3 + 2] = b;
|
||||
});
|
||||
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
|
||||
geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
|
||||
geometry.computeBoundingSphere();
|
||||
const material = new THREE.PointsMaterial({
|
||||
map: createVesselPointTexture(group.anchored, group.courseBin),
|
||||
size: VESSEL_POINT_SIZE,
|
||||
sizeAttenuation: false,
|
||||
vertexColors: true,
|
||||
transparent: true,
|
||||
opacity: VESSEL_CONFIG.marker.baseOpacity,
|
||||
depthWrite: false,
|
||||
depthTest: true,
|
||||
alphaTest: 0.01,
|
||||
});
|
||||
const points = new THREE.Points(geometry, material);
|
||||
points.renderOrder = VESSEL_RENDER_ORDER;
|
||||
points.frustumCulled = false;
|
||||
points.userData = {
|
||||
type: "vessel_points",
|
||||
};
|
||||
vesselPointObjects.push(points);
|
||||
vesselPoints.add(points);
|
||||
});
|
||||
|
||||
vesselGroup.add(vesselPoints);
|
||||
}
|
||||
|
||||
function clearGroup(group) {
|
||||
for (let index = group.children.length - 1; index >= 0; index -= 1) {
|
||||
const child = group.children[index];
|
||||
@@ -120,13 +251,24 @@ function clearGroup(group) {
|
||||
}
|
||||
}
|
||||
|
||||
function getDistanceScale(camera) {
|
||||
return getSurfaceMarkerCameraScale(camera, {
|
||||
altitudeOffset: VESSEL_CONFIG.altitudeOffset,
|
||||
referenceFov: 75,
|
||||
min: VESSEL_CONFIG.sizeStabilization.min,
|
||||
max: VESSEL_CONFIG.sizeStabilization.max,
|
||||
function disposeVesselRenderObjects() {
|
||||
if (vesselPoints?.parent) {
|
||||
vesselPoints.parent.remove(vesselPoints);
|
||||
}
|
||||
vesselPointObjects.forEach((points) => {
|
||||
points.geometry?.dispose?.();
|
||||
points.material?.dispose?.();
|
||||
});
|
||||
hoverOverlaySprite?.geometry?.dispose?.();
|
||||
hoverOverlaySprite?.material?.dispose?.();
|
||||
hoverOverlaySprite?.parent?.remove?.(hoverOverlaySprite);
|
||||
lockedOverlaySprite?.geometry?.dispose?.();
|
||||
lockedOverlaySprite?.material?.dispose?.();
|
||||
lockedOverlaySprite?.parent?.remove?.(lockedOverlaySprite);
|
||||
vesselPoints = null;
|
||||
vesselPointObjects = [];
|
||||
hoverOverlaySprite = null;
|
||||
lockedOverlaySprite = null;
|
||||
}
|
||||
|
||||
export function getVesselMarkers() {
|
||||
@@ -143,10 +285,11 @@ export function getShowVessels() {
|
||||
|
||||
export function toggleVessels(show) {
|
||||
showVessels = Boolean(show);
|
||||
invalidateVesselVisualState();
|
||||
vesselGroup.visible = showVessels;
|
||||
vesselMarkers.forEach((marker) => {
|
||||
marker.visible = showVessels;
|
||||
});
|
||||
if (vesselPoints) {
|
||||
vesselPoints.visible = showVessels;
|
||||
}
|
||||
if (activeTrackLine) {
|
||||
activeTrackLine.visible = showVessels;
|
||||
}
|
||||
@@ -168,12 +311,16 @@ function clearVesselTrack() {
|
||||
|
||||
export function setVesselMarkerState(marker, state = "normal") {
|
||||
if (!marker || marker.userData?.type !== "vessel") return;
|
||||
if (marker.userData.state === state) return;
|
||||
marker.userData.state = state;
|
||||
invalidateVesselVisualState();
|
||||
}
|
||||
|
||||
export function clearVesselData(earth) {
|
||||
vesselMarkers.length = 0;
|
||||
invalidateVesselVisualState();
|
||||
clearVesselSelection();
|
||||
vesselMarkers.length = 0;
|
||||
disposeVesselRenderObjects();
|
||||
clearGroup(vesselGroup);
|
||||
if (earth && vesselGroup.parent === earth) {
|
||||
earth.remove(vesselGroup);
|
||||
@@ -196,6 +343,7 @@ export async function loadVessels(_scene, earth, options = {}) {
|
||||
.filter(Boolean)
|
||||
.slice(0, VESSEL_CONFIG.maxRenderedMarkers)
|
||||
.forEach((markerData) => createVesselMarker(markerData));
|
||||
buildVesselPoints();
|
||||
|
||||
if (earth && !vesselGroup.parent) {
|
||||
earth.add(vesselGroup);
|
||||
@@ -256,26 +404,102 @@ export function getVesselLegendItems() {
|
||||
];
|
||||
}
|
||||
|
||||
export function updateVesselVisualState(lockedObjectType, lockedObject, camera) {
|
||||
const hasFocus = lockedObjectType === "vessel" && lockedObject;
|
||||
const distanceScale = getDistanceScale(camera);
|
||||
vesselMarkers.forEach((marker) => {
|
||||
const isLocked = lockedObjectType === "vessel" && lockedObject === marker;
|
||||
const state = marker.userData?.state || "normal";
|
||||
let opacity = VESSEL_CONFIG.marker.baseOpacity;
|
||||
let scaleMultiplier = 1;
|
||||
if (isLocked) {
|
||||
opacity = 1;
|
||||
scaleMultiplier = VESSEL_CONFIG.marker.lockedScale;
|
||||
} else if (state === "hover") {
|
||||
opacity = 0.98;
|
||||
scaleMultiplier = VESSEL_CONFIG.marker.hoverScale;
|
||||
} else if (hasFocus) {
|
||||
opacity = VESSEL_CONFIG.marker.dimmedOpacity;
|
||||
scaleMultiplier = VESSEL_CONFIG.marker.dimmedScale;
|
||||
}
|
||||
marker.material.opacity = showVessels ? opacity : 0;
|
||||
marker.scale.setScalar(marker.userData.baseScale * scaleMultiplier * distanceScale);
|
||||
marker.visible = showVessels;
|
||||
function ensureOverlaySprite(kind) {
|
||||
const existing = kind === "locked" ? lockedOverlaySprite : hoverOverlaySprite;
|
||||
if (existing) return existing;
|
||||
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
geometry.setAttribute(
|
||||
"position",
|
||||
new THREE.BufferAttribute(new Float32Array(3), 3),
|
||||
);
|
||||
const material = new THREE.PointsMaterial({
|
||||
size: VESSEL_POINT_SIZE,
|
||||
sizeAttenuation: false,
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
depthTest: true,
|
||||
opacity: 1,
|
||||
alphaTest: 0.01,
|
||||
});
|
||||
const overlay = new THREE.Points(geometry, material);
|
||||
overlay.renderOrder = VESSEL_RENDER_ORDER + (kind === "locked" ? 0.2 : 0.1);
|
||||
overlay.frustumCulled = false;
|
||||
overlay.visible = false;
|
||||
vesselGroup.add(overlay);
|
||||
if (kind === "locked") {
|
||||
lockedOverlaySprite = overlay;
|
||||
} else {
|
||||
hoverOverlaySprite = overlay;
|
||||
}
|
||||
return overlay;
|
||||
}
|
||||
|
||||
function updateOverlaySprite(overlay, marker, opacity) {
|
||||
if (!overlay) return;
|
||||
if (!marker) {
|
||||
overlay.visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const texture = createVesselOverlayTexture(marker, true);
|
||||
if (overlay.material.map !== texture) {
|
||||
overlay.material.map = texture;
|
||||
overlay.material.needsUpdate = true;
|
||||
}
|
||||
overlay.material.opacity = opacity;
|
||||
overlay.material.size = VESSEL_POINT_SIZE;
|
||||
const positionAttribute = overlay.geometry.getAttribute("position");
|
||||
positionAttribute.setXYZ(0, marker.position.x, marker.position.y, marker.position.z);
|
||||
positionAttribute.needsUpdate = true;
|
||||
overlay.visible = showVessels;
|
||||
}
|
||||
|
||||
export function updateVesselVisualState(lockedObjectType, lockedObject, camera) {
|
||||
if (!showVessels || vesselMarkers.length === 0 || !vesselPoints) {
|
||||
if (lastVisualStateKey !== "hidden") {
|
||||
if (vesselPoints) vesselPoints.visible = false;
|
||||
if (hoverOverlaySprite) hoverOverlaySprite.visible = false;
|
||||
if (lockedOverlaySprite) lockedOverlaySprite.visible = false;
|
||||
lastVisualStateKey = "hidden";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
vesselPoints.visible = true;
|
||||
const hasFocus = lockedObjectType === "vessel" && lockedObject;
|
||||
const lockedKey = hasFocus
|
||||
? lockedObject?.userData?.mmsi || lockedObject?.uuid || "locked"
|
||||
: "none";
|
||||
const stateKey = [
|
||||
"visible",
|
||||
lockedObjectType || "none",
|
||||
lockedKey,
|
||||
visualStateVersion,
|
||||
].join(":");
|
||||
|
||||
if (stateKey === lastVisualStateKey) return;
|
||||
lastVisualStateKey = stateKey;
|
||||
|
||||
vesselPointObjects.forEach((points) => {
|
||||
points.visible = showVessels;
|
||||
points.material.opacity = hasFocus
|
||||
? VESSEL_CONFIG.marker.dimmedOpacity
|
||||
: VESSEL_CONFIG.marker.baseOpacity;
|
||||
points.material.size = VESSEL_POINT_SIZE;
|
||||
});
|
||||
|
||||
const hoverMarker = vesselMarkers.find(
|
||||
(marker) => marker.userData?.state === "hover" && marker !== lockedObject,
|
||||
);
|
||||
updateOverlaySprite(
|
||||
ensureOverlaySprite("hover"),
|
||||
hoverMarker,
|
||||
0.98,
|
||||
);
|
||||
updateOverlaySprite(
|
||||
ensureOverlaySprite("locked"),
|
||||
hasFocus ? lockedObject : null,
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user