release: bump version to 0.44.1

This commit is contained in:
linkong
2026-04-29 18:07:35 +08:00
parent 87594a95ff
commit a87537e903
8 changed files with 414 additions and 96 deletions

View File

@@ -1 +1 @@
0.44.0 0.44.1

View File

@@ -8,6 +8,16 @@ This project follows the repository versioning rule:
- `improvement` -> `+0.0.1`bugfix + 小功能混合) - `improvement` -> `+0.0.1`bugfix + 小功能混合)
- `bugfix` -> `+0.0.1` - `bugfix` -> `+0.0.1`
## [0.44.1] — 2026-04-29
### 🐛 Fixes
- 修复 Earth 船只图层拖动不跟手的问题,将普通船只从独立 Sprite 切换为按航向分桶的批量 Points 渲染。
- 修正船只 hover/click 拾取错位,改为屏幕空间命中检测并在拖拽/惯性期间跳过 hover 拾取。
- 统一 AIS 船只普通态与交互态方向,并让 hover/locked glow 与普通图标保持同尺寸覆盖。
- 恢复船只深度测试并收敛默认图标尺寸,避免北部岛屿/冰面附近出现明显压盖陆地的视觉问题。
---
## [0.44.0] — 2026-04-29 ## [0.44.0] — 2026-04-29
### ✨ Highlights ### ✨ Highlights

View File

@@ -16,12 +16,13 @@
## Current Version ## Current Version
- `main` 当前主线历史推导到:`0.16.5` - `main` 当前主线历史推导到:`0.16.5`
- `dev` 当前开发分支历史推导到:`0.44.0` - `dev` 当前开发分支历史推导到:`0.44.1`
## Timeline ## Timeline
| Version | Type | Branch | Commit | Summary | | Version | Type | Branch | Commit | Summary |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| `0.44.1` | bugfix | `dev` | `pending` | 优化 Earth 船只批量渲染性能,修复拖动卡顿、拾取错位、交互态方向/尺寸和地表压盖问题 |
| `0.44.0` | feature | `dev` | `pending` | 重构数据源目录与采集器设置,新增 BarentsWatch AIS 连接教程、Earth 船只/缩放体验优化和仪表盘前端重启 | | `0.44.0` | feature | `dev` | `pending` | 重构数据源目录与采集器设置,新增 BarentsWatch AIS 连接教程、Earth 船只/缩放体验优化和仪表盘前端重启 |
| `0.43.1` | bugfix | `dev` | `pending` | 修正全量 restart 后 AI Provider 启动提示语义,避免把预期未就绪描述成异常 | | `0.43.1` | bugfix | `dev` | `pending` | 修正全量 restart 后 AI Provider 启动提示语义,避免把预期未就绪描述成异常 |
| `0.43.0` | feature | `dev` | `pending` | 新增 Earth 船舶追踪、自定义数据源映射、外部集成配置中心、Markdown 渲染器增强,并整理规则/技能文档加载约束 | | `0.43.0` | feature | `dev` | `pending` | 新增 Earth 船舶追踪、自定义数据源映射、外部集成配置中心、Markdown 渲染器增强,并整理规则/技能文档加载约束 |

View File

@@ -1,6 +1,6 @@
{ {
"name": "planet-frontend", "name": "planet-frontend",
"version": "0.44.0", "version": "0.44.1",
"private": true, "private": true,
"packageManager": "bun@1", "packageManager": "bun@1",
"dependencies": { "dependencies": {

View File

@@ -258,6 +258,7 @@ let lastBGPClickTime = 0;
let lastBGPClickCollector = null; let lastBGPClickCollector = null;
let lastBGPClickType = null; let lastBGPClickType = null;
let lastBGPClickPos = { x: 0, y: 0 }; let lastBGPClickPos = { x: 0, y: 0 };
let lastVesselHoverPickTime = 0;
let earthTexture = null; let earthTexture = null;
let animationFrameId = null; let animationFrameId = null;
let initialized = false; let initialized = false;
@@ -295,7 +296,9 @@ const scratchBGPWorldPosition = new THREE.Vector3();
const scratchComputeCenterDirection = new THREE.Vector3(); const scratchComputeCenterDirection = new THREE.Vector3();
const scratchComputeCenterWorldPosition = new THREE.Vector3(); const scratchComputeCenterWorldPosition = new THREE.Vector3();
const scratchVesselDirection = new THREE.Vector3(); const scratchVesselDirection = new THREE.Vector3();
const scratchVesselCameraLocal = new THREE.Vector3();
const scratchVesselWorldPosition = new THREE.Vector3(); const scratchVesselWorldPosition = new THREE.Vector3();
const scratchVesselScreenPosition = new THREE.Vector3();
const scratchSatelliteWorldPosition = new THREE.Vector3(); const scratchSatelliteWorldPosition = new THREE.Vector3();
const scratchSatelliteScreenPosition = new THREE.Vector3(); const scratchSatelliteScreenPosition = new THREE.Vector3();
const scratchViewCenterWorld = 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 TOOLTIP_COORDS_OFFSET = 10; // px offset for earth-coordinate tooltip
const RELATED_SATELLITE_HIGHLIGHT_COLOR = "#7dd3fc"; const RELATED_SATELLITE_HIGHLIGHT_COLOR = "#7dd3fc";
const DRAG_POINTER_THRESHOLD_PX = 8; 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 GLOBE_DRAGGING_CLASS = "is-globe-dragging";
const HUD_INTERACTIVE_SELECTORS = [ const HUD_INTERACTIVE_SELECTORS = [
".earth-left-column", ".earth-left-column",
@@ -364,6 +369,13 @@ function setGlobeDraggingUiState(active) {
document.documentElement.classList.toggle(GLOBE_DRAGGING_CLASS, 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() { function getDragRotationFactor() {
const zoom = Math.max(getZoomLevel(), 0.01); const zoom = Math.max(getZoomLevel(), 0.01);
const scale = THREE.MathUtils.clamp( const scale = THREE.MathUtils.clamp(
@@ -540,21 +552,92 @@ function getFrontFacingVesselMarkers(markers) {
const earth = getEarth(); const earth = getEarth();
if (!earth) return markers; if (!earth) return markers;
scratchCameraToEarth.subVectors(camera.position, earth.position).normalize(); scratchVesselCameraLocal.copy(camera.position);
earth.worldToLocal(scratchVesselCameraLocal);
scratchVesselCameraLocal.normalize();
return markers.filter((marker) => { return markers.filter((marker) => {
scratchVesselWorldPosition.copy(marker.position); scratchVesselDirection.copy(marker.position).normalize();
marker.parent?.localToWorld(scratchVesselWorldPosition);
scratchVesselDirection
.subVectors(scratchVesselWorldPosition, earth.position)
.normalize();
return ( return (
scratchCameraToEarth.dot(scratchVesselDirection) > scratchVesselCameraLocal.dot(scratchVesselDirection) >
SATELLITE_CONFIG.frontFacingDotThreshold 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) { function applyBGPHoverState(marker) {
resetTransientBGPStates(); resetTransientBGPStates();
if (!marker) { if (!marker) {
@@ -590,14 +673,14 @@ function applyComputeCenterHoverState(marker) {
} }
function resetTransientVesselStates() { function resetTransientVesselStates() {
getVesselMarkers().forEach((marker) => { if (hoveredVessel && hoveredVessel !== lockedObject) {
if (marker !== lockedObject) { setVesselMarkerState(hoveredVessel, "normal");
setVesselMarkerState(marker, "normal"); }
} hoveredVessel = null;
});
} }
function applyVesselHoverState(marker) { function applyVesselHoverState(marker) {
if (isSameVessel(hoveredVessel, marker)) return;
resetTransientVesselStates(); resetTransientVesselStates();
if (!marker) { if (!marker) {
hoveredVessel = null; hoveredVessel = null;
@@ -3134,11 +3217,8 @@ function onMouseMove(event) {
const computeCenterIntersects = getShowComputeCenters() const computeCenterIntersects = getShowComputeCenters()
? interactionRaycaster.intersectObjects(frontFacingComputeCenterMarkers) ? interactionRaycaster.intersectObjects(frontFacingComputeCenterMarkers)
: []; : [];
const vesselIntersects = getShowVessels() const vesselPick = getVesselHoverIntersections();
? interactionRaycaster.intersectObjects( const vesselIntersects = vesselPick.intersects;
getFrontFacingVesselMarkers(getVesselMarkers()),
)
: [];
let hoveredSat = null; let hoveredSat = null;
let hoveredSatIndexFromIntersect = null; let hoveredSatIndexFromIntersect = null;
@@ -3161,7 +3241,7 @@ function onMouseMove(event) {
const hoveredComputeCenterMarker = const hoveredComputeCenterMarker =
computeCenterIntersects.length > 0 ? computeCenterIntersects[0].object : null; computeCenterIntersects.length > 0 ? computeCenterIntersects[0].object : null;
const hoveredVesselMarker = const hoveredVesselMarker =
vesselIntersects.length > 0 ? vesselIntersects[0].object : null; vesselPick.checked && vesselIntersects.length > 0 ? vesselIntersects[0].object : null;
if ( if (
hoveredComputeCenter && hoveredComputeCenter &&
@@ -3169,7 +3249,11 @@ function onMouseMove(event) {
) { ) {
clearTransientHoverState(); clearTransientHoverState();
} }
if (hoveredVessel && !isSameVessel(hoveredVessel, hoveredVesselMarker)) { if (
vesselPick.checked &&
hoveredVessel &&
!isSameVessel(hoveredVessel, hoveredVesselMarker)
) {
clearTransientHoverState(); clearTransientHoverState();
} }
@@ -3216,6 +3300,7 @@ function onMouseMove(event) {
); );
objectTooltipShown = true; objectTooltipShown = true;
} else if ( } else if (
vesselPick.checked &&
hoveredVesselMarker && hoveredVesselMarker &&
getShowVessels() && getShowVessels() &&
lockedObjectType !== "vessel" lockedObjectType !== "vessel"
@@ -3262,7 +3347,9 @@ function onMouseMove(event) {
} else if (!lockedObjectType && !isCruisePresentationPinned()) { } else if (!lockedObjectType && !isCruisePresentationPinned()) {
resetTransientBGPStates(); resetTransientBGPStates();
resetTransientComputeCenterStates(); resetTransientComputeCenterStates();
resetTransientVesselStates(); if (vesselPick.checked) {
resetTransientVesselStates();
}
hideInfoCard(); hideInfoCard();
} }
@@ -3483,9 +3570,7 @@ function onClick(event) {
) )
: []; : [];
const vesselIntersects = getShowVessels() const vesselIntersects = getShowVessels()
? interactionRaycaster.intersectObjects( ? getVesselPointerIntersections()
getFrontFacingVesselMarkers(getVesselMarkers()),
)
: []; : [];
const satIntersects = getSatellitePointerIntersections(event); const satIntersects = getSatellitePointerIntersections(event);
@@ -3681,9 +3766,7 @@ function animate() {
const earth = getEarth(); const earth = getEarth();
const deltaTime = clock.getDelta() * 1000; const deltaTime = clock.getDelta() * 1000;
const hasInertia = const hasInertia = hasActiveGlobeInertia();
Math.abs(inertialVelocity.x) > INERTIA_MIN_VELOCITY ||
Math.abs(inertialVelocity.y) > INERTIA_MIN_VELOCITY;
if (getAutoRotate() && getRotationMode() === ROTATION_MODE.ROTATE && earth) { if (getAutoRotate() && getRotationMode() === ROTATION_MODE.ROTATE && earth) {
earth.rotation.y += CONFIG.rotationSpeed * (deltaTime / 16); earth.rotation.y += CONFIG.rotationSpeed * (deltaTime / 16);

View File

@@ -1,15 +1,29 @@
import * as THREE from "three"; import * as THREE from "three";
import { CONFIG, PATHS, VESSEL_CONFIG } from "./constants.js"; 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 vesselGroup = new THREE.Group();
const vesselMarkers = []; const vesselMarkers = [];
const textureCache = new Map(); const textureCache = new Map();
let vesselPoints = null;
let vesselPointObjects = [];
let hoverOverlaySprite = null;
let lockedOverlaySprite = null;
let showVessels = false; let showVessels = false;
let activeTrackLine = null; let activeTrackLine = null;
let lastVisualStateKey = "";
let visualStateVersion = 0;
const VESSEL_RENDER_ORDER = 4.4; 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) { function normalizeVesselType(value, code) {
const type = String(value || "").trim().toLowerCase(); const type = String(value || "").trim().toLowerCase();
@@ -22,36 +36,87 @@ function normalizeVesselType(value, code) {
return "other"; return "other";
} }
function createVesselTexture(type, anchored) { function drawVesselShape(context, anchored, glow, color = "#ffffff") {
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);
context.fillStyle = color; context.fillStyle = color;
context.globalAlpha = anchored ? 0.55 : 0.96; context.globalAlpha = anchored ? 0.55 : 0.96;
context.shadowColor = color; if (glow) {
context.shadowBlur = anchored ? 8 : 14; context.shadowColor = color;
context.shadowBlur = anchored ? 8 : 14;
}
context.beginPath(); context.beginPath();
if (anchored) { if (anchored) {
context.arc(0, 0, 18, 0, Math.PI * 2); context.arc(0, 0, 24, 0, Math.PI * 2);
} else { } else {
context.moveTo(0, -28); context.moveTo(0, -37);
context.lineTo(21, 24); context.lineTo(28, 32);
context.lineTo(0, 13); context.lineTo(0, 17);
context.lineTo(-21, 24); context.lineTo(-28, 32);
context.closePath(); context.closePath();
} }
context.fill(); 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(); context.restore();
const texture = new THREE.CanvasTexture(canvas); 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; texture.needsUpdate = true;
textureCache.set(textureKey, texture); textureCache.set(textureKey, texture);
return texture; return texture;
@@ -80,16 +145,7 @@ function buildVesselMarkerData(feature) {
} }
function createVesselMarker(markerData) { function createVesselMarker(markerData) {
const material = new THREE.SpriteMaterial({ const marker = new THREE.Object3D();
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);
marker.position.copy( marker.position.copy(
latLonToVector3( latLonToVector3(
markerData.latitude, markerData.latitude,
@@ -97,9 +153,6 @@ function createVesselMarker(markerData) {
CONFIG.earthRadius + VESSEL_CONFIG.altitudeOffset, CONFIG.earthRadius + VESSEL_CONFIG.altitudeOffset,
), ),
); );
marker.scale.setScalar(VESSEL_CONFIG.marker.baseScale);
marker.renderOrder = VESSEL_RENDER_ORDER;
marker.visible = showVessels;
marker.userData = { marker.userData = {
...markerData, ...markerData,
type: "vessel", type: "vessel",
@@ -107,10 +160,88 @@ function createVesselMarker(markerData) {
baseScale: VESSEL_CONFIG.marker.baseScale, baseScale: VESSEL_CONFIG.marker.baseScale,
state: "normal", state: "normal",
}; };
vesselGroup.add(marker);
vesselMarkers.push(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) { function clearGroup(group) {
for (let index = group.children.length - 1; index >= 0; index -= 1) { for (let index = group.children.length - 1; index >= 0; index -= 1) {
const child = group.children[index]; const child = group.children[index];
@@ -120,13 +251,24 @@ function clearGroup(group) {
} }
} }
function getDistanceScale(camera) { function disposeVesselRenderObjects() {
return getSurfaceMarkerCameraScale(camera, { if (vesselPoints?.parent) {
altitudeOffset: VESSEL_CONFIG.altitudeOffset, vesselPoints.parent.remove(vesselPoints);
referenceFov: 75, }
min: VESSEL_CONFIG.sizeStabilization.min, vesselPointObjects.forEach((points) => {
max: VESSEL_CONFIG.sizeStabilization.max, 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() { export function getVesselMarkers() {
@@ -143,10 +285,11 @@ export function getShowVessels() {
export function toggleVessels(show) { export function toggleVessels(show) {
showVessels = Boolean(show); showVessels = Boolean(show);
invalidateVesselVisualState();
vesselGroup.visible = showVessels; vesselGroup.visible = showVessels;
vesselMarkers.forEach((marker) => { if (vesselPoints) {
marker.visible = showVessels; vesselPoints.visible = showVessels;
}); }
if (activeTrackLine) { if (activeTrackLine) {
activeTrackLine.visible = showVessels; activeTrackLine.visible = showVessels;
} }
@@ -168,12 +311,16 @@ function clearVesselTrack() {
export function setVesselMarkerState(marker, state = "normal") { export function setVesselMarkerState(marker, state = "normal") {
if (!marker || marker.userData?.type !== "vessel") return; if (!marker || marker.userData?.type !== "vessel") return;
if (marker.userData.state === state) return;
marker.userData.state = state; marker.userData.state = state;
invalidateVesselVisualState();
} }
export function clearVesselData(earth) { export function clearVesselData(earth) {
vesselMarkers.length = 0; invalidateVesselVisualState();
clearVesselSelection(); clearVesselSelection();
vesselMarkers.length = 0;
disposeVesselRenderObjects();
clearGroup(vesselGroup); clearGroup(vesselGroup);
if (earth && vesselGroup.parent === earth) { if (earth && vesselGroup.parent === earth) {
earth.remove(vesselGroup); earth.remove(vesselGroup);
@@ -196,6 +343,7 @@ export async function loadVessels(_scene, earth, options = {}) {
.filter(Boolean) .filter(Boolean)
.slice(0, VESSEL_CONFIG.maxRenderedMarkers) .slice(0, VESSEL_CONFIG.maxRenderedMarkers)
.forEach((markerData) => createVesselMarker(markerData)); .forEach((markerData) => createVesselMarker(markerData));
buildVesselPoints();
if (earth && !vesselGroup.parent) { if (earth && !vesselGroup.parent) {
earth.add(vesselGroup); earth.add(vesselGroup);
@@ -256,26 +404,102 @@ export function getVesselLegendItems() {
]; ];
} }
export function updateVesselVisualState(lockedObjectType, lockedObject, camera) { function ensureOverlaySprite(kind) {
const hasFocus = lockedObjectType === "vessel" && lockedObject; const existing = kind === "locked" ? lockedOverlaySprite : hoverOverlaySprite;
const distanceScale = getDistanceScale(camera); if (existing) return existing;
vesselMarkers.forEach((marker) => {
const isLocked = lockedObjectType === "vessel" && lockedObject === marker; const geometry = new THREE.BufferGeometry();
const state = marker.userData?.state || "normal"; geometry.setAttribute(
let opacity = VESSEL_CONFIG.marker.baseOpacity; "position",
let scaleMultiplier = 1; new THREE.BufferAttribute(new Float32Array(3), 3),
if (isLocked) { );
opacity = 1; const material = new THREE.PointsMaterial({
scaleMultiplier = VESSEL_CONFIG.marker.lockedScale; size: VESSEL_POINT_SIZE,
} else if (state === "hover") { sizeAttenuation: false,
opacity = 0.98; transparent: true,
scaleMultiplier = VESSEL_CONFIG.marker.hoverScale; depthWrite: false,
} else if (hasFocus) { depthTest: true,
opacity = VESSEL_CONFIG.marker.dimmedOpacity; opacity: 1,
scaleMultiplier = VESSEL_CONFIG.marker.dimmedScale; alphaTest: 0.01,
}
marker.material.opacity = showVessels ? opacity : 0;
marker.scale.setScalar(marker.userData.baseScale * scaleMultiplier * distanceScale);
marker.visible = showVessels;
}); });
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,
);
} }

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "planet" name = "planet"
version = "0.44.0" version = "0.44.1"
description = "智能星球计划 - 态势感知系统" description = "智能星球计划 - 态势感知系统"
requires-python = ">=3.14" requires-python = ">=3.14"
dependencies = [ dependencies = [

2
uv.lock generated
View File

@@ -475,7 +475,7 @@ wheels = [
[[package]] [[package]]
name = "planet" name = "planet"
version = "0.44.0" version = "0.44.1"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "aiofiles" }, { name = "aiofiles" },