698 lines
19 KiB
JavaScript
698 lines
19 KiB
JavaScript
// cables.js - Cable loading and rendering module
|
|
|
|
import * as THREE from "three";
|
|
|
|
import {
|
|
CONFIG,
|
|
CABLE_COLORS,
|
|
PATHS,
|
|
CABLE_STATE,
|
|
CABLE_CONFIG,
|
|
} from "./constants.js";
|
|
import { getSurfaceMarkerCameraScale, latLonToVector3 } from "./utils.js";
|
|
import { setEarthStatValue, updateEarthStats, showStatusMessage } from "./ui.js";
|
|
import { showInfoCard } from "./info-card.js";
|
|
import { setLegendItems, setLegendMode } from "./legend.js";
|
|
|
|
export let cableLines = [];
|
|
export let landingPoints = [];
|
|
export let lockedCable = null;
|
|
let cableIdMap = new Map();
|
|
let cableStates = new Map();
|
|
let cablesVisible = true;
|
|
let landingPointTexture = null;
|
|
const _lpWorldPos = new THREE.Vector3();
|
|
|
|
function createLandingPointTexture() {
|
|
const size = CABLE_CONFIG.landingPoint.textureSize;
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = size;
|
|
canvas.height = size;
|
|
const ctx = canvas.getContext("2d");
|
|
const iconPath = new Path2D(
|
|
[
|
|
"M400 704",
|
|
"C386 704 375 697 367 684",
|
|
"L173 378",
|
|
"C117 290 144 173 229 111",
|
|
"C278 75 337 57 400 57",
|
|
"C463 57 522 75 571 111",
|
|
"C656 173 683 290 627 378",
|
|
"L433 684",
|
|
"C425 697 414 704 400 704",
|
|
"Z",
|
|
].join(" "),
|
|
);
|
|
|
|
ctx.clearRect(0, 0, size, size);
|
|
ctx.save();
|
|
ctx.translate(size * 0.12, size * 0.02);
|
|
ctx.scale(size / 1000, size / 1000);
|
|
|
|
ctx.fillStyle = "#ffffff";
|
|
ctx.fill(iconPath);
|
|
|
|
ctx.globalCompositeOperation = "destination-out";
|
|
ctx.beginPath();
|
|
ctx.arc(400, 320, 86, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.restore();
|
|
|
|
const texture = new THREE.CanvasTexture(canvas);
|
|
texture.colorSpace = THREE.SRGBColorSpace;
|
|
texture.needsUpdate = true;
|
|
return texture;
|
|
}
|
|
|
|
function getLandingPointTexture() {
|
|
if (!landingPointTexture) {
|
|
landingPointTexture = createLandingPointTexture();
|
|
}
|
|
return landingPointTexture;
|
|
}
|
|
|
|
function clamp(value, min, max) {
|
|
return Math.min(max, Math.max(min, value));
|
|
}
|
|
|
|
function getLandingPointDistanceScale(point, camera) {
|
|
if (
|
|
!point ||
|
|
!camera ||
|
|
CABLE_CONFIG.landingPointSizeStabilization?.enabled === false
|
|
) return 1;
|
|
|
|
return getSurfaceMarkerCameraScale(camera, {
|
|
altitudeOffset: CABLE_CONFIG.landingPoint.altitudeOffset,
|
|
referenceFov: CABLE_CONFIG.landingPointSizeStabilization?.referenceFov || 75,
|
|
min: CABLE_CONFIG.landingPointSizeStabilization?.min ?? 0.12,
|
|
max: CABLE_CONFIG.landingPointSizeStabilization?.max ?? 3.0,
|
|
});
|
|
}
|
|
|
|
function disposeMaterial(material) {
|
|
if (!material) return;
|
|
|
|
if (Array.isArray(material)) {
|
|
material.forEach(disposeMaterial);
|
|
return;
|
|
}
|
|
|
|
if (material.map && !material.userData?.sharedMap) {
|
|
material.map.dispose();
|
|
}
|
|
material.dispose();
|
|
}
|
|
|
|
function disposeObject(object, parent) {
|
|
if (!object) return;
|
|
const owner = parent || object.parent;
|
|
if (owner) {
|
|
owner.remove(object);
|
|
}
|
|
if (object.geometry && !object.userData?.sharedGeometry) {
|
|
object.geometry.dispose();
|
|
}
|
|
if (object.material) {
|
|
disposeMaterial(object.material);
|
|
}
|
|
}
|
|
|
|
function setLandingPointMaterialState(point, { color, opacity, emissive, emissiveIntensity }) {
|
|
point.material.color.set(color);
|
|
point.material.opacity = opacity;
|
|
if (point.material.emissive && emissive !== undefined) {
|
|
point.material.emissive.setHex(emissive);
|
|
}
|
|
if ("emissiveIntensity" in point.material && emissiveIntensity !== undefined) {
|
|
point.material.emissiveIntensity = emissiveIntensity;
|
|
}
|
|
}
|
|
|
|
function setLandingPointScale(point, heightScale) {
|
|
const aspect = CABLE_CONFIG.landingPoint.iconAspectRatio;
|
|
point.scale.set(heightScale * aspect, heightScale, 1);
|
|
}
|
|
|
|
function getCableColor(properties) {
|
|
if (properties.color) {
|
|
if (
|
|
typeof properties.color === "string" &&
|
|
properties.color.startsWith("#")
|
|
) {
|
|
return parseInt(properties.color.substring(1), 16);
|
|
}
|
|
if (typeof properties.color === "number") {
|
|
return properties.color;
|
|
}
|
|
}
|
|
|
|
const cableName =
|
|
properties.Name ||
|
|
properties.name ||
|
|
properties.cableName ||
|
|
properties.shortname ||
|
|
"";
|
|
if (cableName.includes("Americas II")) {
|
|
return CABLE_COLORS["Americas II"];
|
|
}
|
|
if (cableName.includes("AU Aleutian A")) {
|
|
return CABLE_COLORS["AU Aleutian A"];
|
|
}
|
|
if (cableName.includes("AU Aleutian B")) {
|
|
return CABLE_COLORS["AU Aleutian B"];
|
|
}
|
|
|
|
return CABLE_COLORS.default;
|
|
}
|
|
|
|
function createCableLine(points, color, properties) {
|
|
if (points.length < 2) return null;
|
|
|
|
const lineGeometry = new THREE.BufferGeometry().setFromPoints(points);
|
|
lineGeometry.computeBoundingSphere();
|
|
|
|
const lineMaterial = new THREE.LineBasicMaterial({
|
|
color,
|
|
linewidth: CABLE_CONFIG.line.lineWidth,
|
|
transparent: true,
|
|
opacity: CABLE_CONFIG.line.opacity,
|
|
depthTest: true,
|
|
depthWrite: true,
|
|
});
|
|
|
|
const cableLine = new THREE.Line(lineGeometry, lineMaterial);
|
|
const cableId =
|
|
properties.cable_id ||
|
|
properties.id ||
|
|
properties.Name ||
|
|
properties.name ||
|
|
Math.random().toString(36);
|
|
cableLine.userData = {
|
|
type: "cable",
|
|
cableId,
|
|
name:
|
|
properties.Name ||
|
|
properties.name ||
|
|
properties.cableName ||
|
|
properties.shortname ||
|
|
"Unknown",
|
|
owner: properties.owner || properties.owners || "-",
|
|
status: properties.status || "-",
|
|
length: properties.length || "-",
|
|
coords: "-",
|
|
rfs: properties.rfs || "-",
|
|
originalColor: color,
|
|
localCenter:
|
|
lineGeometry.boundingSphere?.center?.clone() || new THREE.Vector3(),
|
|
};
|
|
cableLine.renderOrder = CABLE_CONFIG.line.renderOrder;
|
|
|
|
if (!cableIdMap.has(cableId)) {
|
|
cableIdMap.set(cableId, []);
|
|
}
|
|
cableIdMap.get(cableId).push(cableLine);
|
|
|
|
return cableLine;
|
|
}
|
|
|
|
function calculateGreatCirclePoints(
|
|
lat1,
|
|
lon1,
|
|
lat2,
|
|
lon2,
|
|
radius,
|
|
segments = 50,
|
|
) {
|
|
const points = [];
|
|
const phi1 = (lat1 * Math.PI) / 180;
|
|
const lambda1 = (lon1 * Math.PI) / 180;
|
|
const phi2 = (lat2 * Math.PI) / 180;
|
|
const lambda2 = (lon2 * Math.PI) / 180;
|
|
|
|
const dLambda = Math.min(
|
|
Math.abs(lambda2 - lambda1),
|
|
2 * Math.PI - Math.abs(lambda2 - lambda1),
|
|
);
|
|
const cosDelta =
|
|
Math.sin(phi1) * Math.sin(phi2) +
|
|
Math.cos(phi1) * Math.cos(phi2) * Math.cos(dLambda);
|
|
|
|
let delta = Math.acos(Math.max(-1, Math.min(1, cosDelta)));
|
|
|
|
if (delta < CABLE_CONFIG.line.nearPointThreshold) {
|
|
const p1 = latLonToVector3(lat1, lon1, radius);
|
|
const p2 = latLonToVector3(lat2, lon2, radius);
|
|
return [p1, p2];
|
|
}
|
|
|
|
for (let i = 0; i <= segments; i++) {
|
|
const t = i / segments;
|
|
const sinDelta = Math.sin(delta);
|
|
const A = Math.sin((1 - t) * delta) / sinDelta;
|
|
const B = Math.sin(t * delta) / sinDelta;
|
|
|
|
const x1 = Math.cos(phi1) * Math.cos(lambda1);
|
|
const y1 = Math.cos(phi1) * Math.sin(lambda1);
|
|
const z1 = Math.sin(phi1);
|
|
|
|
const x2 = Math.cos(phi2) * Math.cos(lambda2);
|
|
const y2 = Math.cos(phi2) * Math.sin(lambda2);
|
|
const z2 = Math.sin(phi2);
|
|
|
|
let x = A * x1 + B * x2;
|
|
let y = A * y1 + B * y2;
|
|
let z = A * z1 + B * z2;
|
|
|
|
const norm = Math.sqrt(x * x + y * y + z * z);
|
|
x = (x / norm) * radius;
|
|
y = (y / norm) * radius;
|
|
z = (z / norm) * radius;
|
|
|
|
const lat = (Math.asin(z / radius) * 180) / Math.PI;
|
|
let lon = (Math.atan2(y, x) * 180) / Math.PI;
|
|
|
|
if (lon > 180) lon -= 360;
|
|
if (lon < -180) lon += 360;
|
|
|
|
points.push(latLonToVector3(lat, lon, radius));
|
|
}
|
|
|
|
return points;
|
|
}
|
|
|
|
export function clearCableLines(earthObj = null) {
|
|
cableLines.forEach((line) => disposeObject(line, earthObj));
|
|
cableLines = [];
|
|
cableIdMap = new Map();
|
|
cableStates.clear();
|
|
}
|
|
|
|
export function clearLandingPoints(earthObj = null) {
|
|
landingPoints.forEach((point) => disposeObject(point, earthObj));
|
|
landingPoints = [];
|
|
}
|
|
|
|
export function clearCableData(earthObj = null) {
|
|
clearCableSelection();
|
|
clearCableLines(earthObj);
|
|
clearLandingPoints(earthObj);
|
|
}
|
|
|
|
export async function loadGeoJSONFromPath(scene, earthObj, options = {}) {
|
|
const { silent = false } = options;
|
|
console.log("正在加载电缆数据...");
|
|
if (!silent) {
|
|
showStatusMessage("正在加载电缆数据...", "warning");
|
|
}
|
|
|
|
const response = await fetch(PATHS.cablesApi);
|
|
if (!response.ok) {
|
|
throw new Error(`电缆接口返回 HTTP ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
if (!data.features || !Array.isArray(data.features)) {
|
|
throw new Error("无效的电缆 GeoJSON 格式");
|
|
}
|
|
|
|
clearCableLines(earthObj);
|
|
|
|
for (const feature of data.features) {
|
|
const geometry = feature.geometry;
|
|
const properties = feature.properties || {};
|
|
|
|
if (!geometry || !geometry.coordinates) continue;
|
|
|
|
const color = getCableColor(properties);
|
|
|
|
if (geometry.type === "MultiLineString") {
|
|
for (const lineCoords of geometry.coordinates) {
|
|
if (!lineCoords || lineCoords.length < 2) continue;
|
|
|
|
const points = [];
|
|
for (let i = 0; i < lineCoords.length - 1; i++) {
|
|
const lon1 = lineCoords[i][0];
|
|
const lat1 = lineCoords[i][1];
|
|
const lon2 = lineCoords[i + 1][0];
|
|
const lat2 = lineCoords[i + 1][1];
|
|
|
|
const segment = calculateGreatCirclePoints(
|
|
lat1,
|
|
lon1,
|
|
lat2,
|
|
lon2,
|
|
CONFIG.earthRadius + CABLE_CONFIG.line.altitudeOffset,
|
|
CABLE_CONFIG.line.greatCircleSegments,
|
|
);
|
|
points.push(...(i === 0 ? segment : segment.slice(1)));
|
|
}
|
|
|
|
const line = createCableLine(points, color, properties);
|
|
if (line) {
|
|
cableLines.push(line);
|
|
earthObj.add(line);
|
|
}
|
|
}
|
|
} else if (geometry.type === "LineString") {
|
|
const points = [];
|
|
for (let i = 0; i < geometry.coordinates.length - 1; i++) {
|
|
const lon1 = geometry.coordinates[i][0];
|
|
const lat1 = geometry.coordinates[i][1];
|
|
const lon2 = geometry.coordinates[i + 1][0];
|
|
const lat2 = geometry.coordinates[i + 1][1];
|
|
|
|
const segment = calculateGreatCirclePoints(
|
|
lat1,
|
|
lon1,
|
|
lat2,
|
|
lon2,
|
|
CONFIG.earthRadius + CABLE_CONFIG.line.altitudeOffset,
|
|
CABLE_CONFIG.line.greatCircleSegments,
|
|
);
|
|
points.push(...(i === 0 ? segment : segment.slice(1)));
|
|
}
|
|
|
|
const line = createCableLine(points, color, properties);
|
|
if (line) {
|
|
cableLines.push(line);
|
|
earthObj.add(line);
|
|
}
|
|
}
|
|
}
|
|
|
|
const cableCount = data.features.length;
|
|
const inServiceCount = data.features.filter(
|
|
(feature) =>
|
|
feature.properties &&
|
|
(feature.properties.status === "active" ||
|
|
feature.properties.status === "In Service"),
|
|
).length;
|
|
|
|
const statusEl = document.getElementById("cable-status-summary");
|
|
setEarthStatValue("cable-count", `${cableCount}个`);
|
|
if (statusEl) statusEl.textContent = `${inServiceCount}/${cableCount} 运行中`;
|
|
|
|
updateEarthStats({
|
|
cableCount: cableLines.length,
|
|
landingPointCount: landingPoints.length,
|
|
terrainOn: false,
|
|
textureQuality: "8K 卫星图",
|
|
});
|
|
|
|
if (!silent) {
|
|
showStatusMessage(`成功加载 ${cableLines.length} 条电缆`, "success");
|
|
}
|
|
return cableLines.length;
|
|
}
|
|
|
|
export async function loadLandingPoints(scene, earthObj, options = {}) {
|
|
const { silent = false } = options;
|
|
console.log("正在加载登陆点数据...");
|
|
|
|
const response = await fetch(PATHS.landingPointsApi);
|
|
if (!response.ok) {
|
|
throw new Error(`登陆点接口返回 HTTP ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
if (!data.features || !Array.isArray(data.features)) {
|
|
throw new Error("无效的登陆点 GeoJSON 格式");
|
|
}
|
|
|
|
clearLandingPoints(earthObj);
|
|
|
|
let validCount = 0;
|
|
|
|
for (const feature of data.features) {
|
|
if (!feature.geometry || !feature.geometry.coordinates) continue;
|
|
|
|
const [lon, lat] = feature.geometry.coordinates;
|
|
const properties = feature.properties || {};
|
|
|
|
if (
|
|
typeof lon !== "number" ||
|
|
typeof lat !== "number" ||
|
|
Number.isNaN(lon) ||
|
|
Number.isNaN(lat) ||
|
|
Math.abs(lat) > 90 ||
|
|
Math.abs(lon) > 180
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
const position = latLonToVector3(
|
|
lat,
|
|
lon,
|
|
CONFIG.earthRadius + CABLE_CONFIG.landingPoint.altitudeOffset,
|
|
);
|
|
if (
|
|
Number.isNaN(position.x) ||
|
|
Number.isNaN(position.y) ||
|
|
Number.isNaN(position.z)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
const marker = new THREE.Sprite(
|
|
new THREE.SpriteMaterial({
|
|
map: getLandingPointTexture(),
|
|
color: CABLE_CONFIG.landingPoint.color,
|
|
transparent: true,
|
|
opacity: CABLE_CONFIG.landingPoint.opacity,
|
|
depthTest: true,
|
|
depthWrite: false,
|
|
}),
|
|
);
|
|
marker.material.userData.sharedMap = true;
|
|
marker.renderOrder = CABLE_CONFIG.landingPoint.renderOrder;
|
|
marker.center.set(
|
|
CABLE_CONFIG.landingPoint.anchorX,
|
|
CABLE_CONFIG.landingPoint.anchorY,
|
|
);
|
|
marker.position.copy(position);
|
|
marker.userData = {
|
|
type: "landingPoint",
|
|
name: properties.name || "未知登陆站",
|
|
cableNames: properties.cable_names || [],
|
|
country: properties.country || "未知国家",
|
|
status: properties.status || "Unknown",
|
|
baseScale: CABLE_CONFIG.landingPoint.baseScale,
|
|
};
|
|
setLandingPointScale(marker, CABLE_CONFIG.landingPoint.baseScale);
|
|
|
|
earthObj.add(marker);
|
|
landingPoints.push(marker);
|
|
validCount++;
|
|
}
|
|
|
|
setEarthStatValue("landing-point-count", `${validCount}个`);
|
|
|
|
if (!silent) {
|
|
showStatusMessage(`成功加载 ${validCount} 个登陆点`, "success");
|
|
}
|
|
return validCount;
|
|
}
|
|
|
|
export function handleCableClick(cable) {
|
|
lockedCable = cable;
|
|
setLegendItems("cables", getCableLegendItems());
|
|
|
|
const data = cable.userData;
|
|
setLegendMode("cables");
|
|
showInfoCard("cable", {
|
|
name: data.name,
|
|
owner: data.owner,
|
|
status: data.status,
|
|
length: data.length,
|
|
coords: data.coords,
|
|
rfs: data.rfs,
|
|
});
|
|
|
|
showStatusMessage(`已锁定: ${data.name}`, "info");
|
|
}
|
|
|
|
export function clearCableSelection() {
|
|
lockedCable = null;
|
|
setLegendItems("cables", getCableLegendItems());
|
|
}
|
|
|
|
export function getCableLines() {
|
|
return cableLines;
|
|
}
|
|
|
|
export function getCableLegendItems() {
|
|
const legendMap = new Map();
|
|
|
|
cableLines.forEach((cable) => {
|
|
const color = cable.userData?.originalColor;
|
|
const label = cable.userData?.name || "未知线缆";
|
|
|
|
if (typeof color === "number" && !legendMap.has(label)) {
|
|
legendMap.set(label, {
|
|
label,
|
|
color: `#${color.toString(16).padStart(6, "0")}`,
|
|
});
|
|
}
|
|
});
|
|
|
|
if (legendMap.size === 0) {
|
|
return [{ label: "其他电缆", color: "#ffff44" }];
|
|
}
|
|
|
|
const items = Array.from(legendMap.values()).sort((a, b) =>
|
|
a.label.localeCompare(b.label, "zh-CN"),
|
|
);
|
|
|
|
const selectedName = lockedCable?.userData?.name;
|
|
if (!selectedName) {
|
|
return items;
|
|
}
|
|
|
|
const selectedIndex = items.findIndex((item) => item.label === selectedName);
|
|
if (selectedIndex <= 0) {
|
|
return items;
|
|
}
|
|
|
|
const [selectedItem] = items.splice(selectedIndex, 1);
|
|
items.unshift(selectedItem);
|
|
return items;
|
|
}
|
|
|
|
export function getCablesById(cableId) {
|
|
return cableIdMap.get(cableId) || [];
|
|
}
|
|
|
|
export function getLandingPoints() {
|
|
return landingPoints;
|
|
}
|
|
|
|
export function getCableState(cableId) {
|
|
return cableStates.get(cableId) || CABLE_STATE.NORMAL;
|
|
}
|
|
|
|
export function setCableState(cableId, state) {
|
|
cableStates.set(cableId, state);
|
|
}
|
|
|
|
export function clearAllCableStates() {
|
|
cableStates.clear();
|
|
}
|
|
|
|
export function getCableStateInfo() {
|
|
const states = {};
|
|
cableStates.forEach((state, cableId) => {
|
|
states[cableId] = state;
|
|
});
|
|
return states;
|
|
}
|
|
|
|
export function getLandingPointsByCableName(cableName) {
|
|
return landingPoints.filter((lp) =>
|
|
lp.userData.cableNames?.includes(cableName),
|
|
);
|
|
}
|
|
|
|
export function getAllLandingPoints() {
|
|
return landingPoints;
|
|
}
|
|
|
|
// Hide pins within ~3° of the limb (normalised dot < 0.05) to prevent the
|
|
// "half-clipped-into-globe" look caused by depthTest clipping the billboard
|
|
// against closer earth-surface geometry near the horizon.
|
|
const _FACING_DOT_MIN_SQ = 0.05 * 0.05;
|
|
|
|
function isFacingCamera(lp, camera) {
|
|
lp.getWorldPosition(_lpWorldPos);
|
|
const d = _lpWorldPos.dot(camera.position);
|
|
if (d <= 0) return false;
|
|
return d * d > _FACING_DOT_MIN_SQ * _lpWorldPos.lengthSq() * camera.position.lengthSq();
|
|
}
|
|
|
|
export function applyLandingPointVisualState(lockedCableName, dimAll = false, camera = null) {
|
|
const pulse =
|
|
(Math.sin(Date.now() * CABLE_CONFIG.landingPointVisual.pulseSpeed) + 1) * 0.5;
|
|
const brightness = CABLE_CONFIG.landingPointVisual.dimBrightness;
|
|
const relatedNames = Array.isArray(lockedCableName)
|
|
? lockedCableName.filter(Boolean)
|
|
: lockedCableName
|
|
? [lockedCableName]
|
|
: [];
|
|
|
|
landingPoints.forEach((lp) => {
|
|
lp.visible = cablesVisible && (camera ? isFacingCamera(lp, camera) : true);
|
|
const isRelated =
|
|
!dimAll &&
|
|
Array.isArray(lp.userData.cableNames) &&
|
|
lp.userData.cableNames.some((name) => relatedNames.includes(name));
|
|
|
|
if (isRelated) {
|
|
setLandingPointMaterialState(lp, {
|
|
color: 0xffd27a,
|
|
emissive: 0x7a4a00,
|
|
emissiveIntensity:
|
|
CABLE_CONFIG.landingPointVisual.related.emissiveIntensityBase +
|
|
0.2 +
|
|
pulse * (CABLE_CONFIG.landingPointVisual.related.emissiveIntensityPulse + 0.2),
|
|
opacity: Math.max(
|
|
0.92,
|
|
CABLE_CONFIG.landingPointVisual.related.opacityBase +
|
|
pulse * CABLE_CONFIG.landingPointVisual.related.opacityPulse,
|
|
),
|
|
});
|
|
const distanceScale = getLandingPointDistanceScale(lp, camera);
|
|
const baseScale = lp.userData?.baseScale || CABLE_CONFIG.landingPoint.baseScale;
|
|
setLandingPointScale(
|
|
lp,
|
|
(CABLE_CONFIG.landingPointVisual.related.scaleBase +
|
|
pulse * CABLE_CONFIG.landingPointVisual.related.scalePulse) *
|
|
baseScale *
|
|
distanceScale,
|
|
);
|
|
} else {
|
|
const dimColor = CABLE_CONFIG.landingPointVisual.dimmed.colorRGB;
|
|
const r = dimColor.r * brightness;
|
|
const g = dimColor.g * brightness;
|
|
const b = dimColor.b * brightness;
|
|
setLandingPointMaterialState(lp, {
|
|
color: new THREE.Color(r / 255, g / 255, b / 255),
|
|
emissive: CABLE_CONFIG.landingPointVisual.dimmed.emissive,
|
|
emissiveIntensity: CABLE_CONFIG.landingPointVisual.dimmed.emissiveIntensity,
|
|
opacity: CABLE_CONFIG.landingPointVisual.dimmed.opacity,
|
|
});
|
|
const distanceScale = getLandingPointDistanceScale(lp, camera);
|
|
const baseScale = lp.userData?.baseScale || CABLE_CONFIG.landingPoint.baseScale;
|
|
setLandingPointScale(lp, baseScale * distanceScale);
|
|
}
|
|
});
|
|
}
|
|
|
|
export function resetLandingPointVisualState(camera = null) {
|
|
landingPoints.forEach((lp) => {
|
|
lp.visible = cablesVisible && (camera ? isFacingCamera(lp, camera) : true);
|
|
setLandingPointMaterialState(lp, {
|
|
color: CABLE_CONFIG.landingPoint.color,
|
|
emissive: CABLE_CONFIG.landingPoint.emissive,
|
|
emissiveIntensity: CABLE_CONFIG.landingPoint.emissiveIntensity,
|
|
opacity: CABLE_CONFIG.landingPoint.opacity,
|
|
});
|
|
const distanceScale = getLandingPointDistanceScale(lp, camera);
|
|
const baseScale = lp.userData?.baseScale || CABLE_CONFIG.landingPoint.baseScale;
|
|
setLandingPointScale(lp, baseScale * distanceScale);
|
|
});
|
|
}
|
|
|
|
export function toggleCables(show) {
|
|
cablesVisible = show;
|
|
cableLines.forEach((cable) => {
|
|
cable.visible = cablesVisible;
|
|
});
|
|
landingPoints.forEach((lp) => {
|
|
lp.visible = cablesVisible;
|
|
});
|
|
}
|
|
|
|
export function getShowCables() {
|
|
return cablesVisible;
|
|
}
|