From a6e8ba31bdb55d166675b7f38e16afde139f01fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B0=D1=88=D0=B5=20=D0=98=D0=BC=D1=8F?= Date: Fri, 24 Jul 2026 13:12:17 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B4=D0=BE=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B0?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D0=B0=20=D0=B4=D0=B0=D1=82=D0=B0=D1=81=D0=B5?= =?UTF-8?q?=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../web/src/composables/useGboSimulator.js | 14 +- .../web/src/composables/useMleSimulator.js | 152 ++++++------------ frontend/web/src/views/GboView.vue | 4 +- frontend/web/src/views/MleView.vue | 4 +- 4 files changed, 67 insertions(+), 107 deletions(-) diff --git a/frontend/web/src/composables/useGboSimulator.js b/frontend/web/src/composables/useGboSimulator.js index 2913101..bacef82 100644 --- a/frontend/web/src/composables/useGboSimulator.js +++ b/frontend/web/src/composables/useGboSimulator.js @@ -63,6 +63,14 @@ function sampleHeight(meshInfo, x, y) { return (h00 * (1 - tx) + h10 * tx) * (1 - ty) + (h01 * (1 - tx) + h11 * tx) * ty; } +/** Flat seafloor datum (without corridor relief). AUV altitude is measured from this. */ +function referenceSeafloorZ(meshInfo) { + if (!meshInfo) return -5; + const base = Number(meshInfo.baseZ); + if (Number.isFinite(base)) return base; + return sampleHeight(meshInfo, 0, 0); +} + /** * @param {import('vue').Ref} containerRef main 3D view * @param {import('vue').Ref} surveyRef bottom-right survey surface panel @@ -959,7 +967,7 @@ export function useGboSimulator(containerRef, surveyRef) { function placeAuv(x, y, depth, headingDeg) { if (!auvPivot) return; - const floorZ = sampleHeight(meshInfo, x, y); + const floorZ = referenceSeafloorZ(meshInfo); auvPivot.position.set(x, y, floorZ + Math.max(0.3, Number(depth) || 2.5)); headingRad = degToRad(headingDeg); auvPivot.rotation.set(0, 0, headingRad); @@ -1016,8 +1024,8 @@ export function useGboSimulator(containerRef, surveyRef) { traveled += step; const nx = auvPivot.position.x + Math.cos(headingRad) * step; const ny = auvPivot.position.y + Math.sin(headingRad) * step; - const floorZ = sampleHeight(meshInfo, nx, ny); - auvPivot.position.set(nx, ny, floorZ + auvDepth); + // Constant altitude vs flat datum — do not follow seafloor relief. + auvPivot.position.set(nx, ny, referenceSeafloorZ(meshInfo) + auvDepth); updateRays(true); updateTrail(); onStatus({ diff --git a/frontend/web/src/composables/useMleSimulator.js b/frontend/web/src/composables/useMleSimulator.js index 00e5568..bdf784d 100644 --- a/frontend/web/src/composables/useMleSimulator.js +++ b/frontend/web/src/composables/useMleSimulator.js @@ -63,6 +63,14 @@ function sampleHeight(meshInfo, x, y) { return (h00 * (1 - tx) + h10 * tx) * (1 - ty) + (h01 * (1 - tx) + h11 * tx) * ty; } +/** Flat seafloor datum (without corridor relief). AUV altitude is measured from this. */ +function referenceSeafloorZ(meshInfo) { + if (!meshInfo) return -5; + const base = Number(meshInfo.baseZ); + if (Number.isFinite(base)) return base; + return sampleHeight(meshInfo, 0, 0); +} + /** * @param {import('vue').Ref} containerRef main 3D view * @param {import('vue').Ref} surveyRef bottom-right survey surface panel @@ -257,8 +265,8 @@ export function useMleSimulator(containerRef, surveyRef) { } /** - * Preview of seafloor/object contacts the AUV will meet along the survey track. - * Starts at current echosounder footprint and extends forward by surveyLength. + * Preview of the future survey swath on the seafloor only (relief / unevenness). + * Does not wrap or outline the bottom object — object contacts appear only during motion. */ function buildPathReliefPreview() { clearPathRelief(); @@ -277,122 +285,65 @@ export function useMleSimulator(containerRef, surveyRef) { const startY = auvPivot.position.y; const depth = Math.max(0.3, Number(auvDepth) || 2.5); - // Station 0: real beam hits (where rays touch seafloor/object now). - const currentHits = castBeamHits(); const positions = new Float32Array(nAlong * nAcross * 3); const colors = new Float32Array(nAlong * nAcross * 3); const flat = new THREE.Color(0x2f6b52); const mid = new THREE.Color(0xe6a820); const hot = new THREE.Color(0xfff176); - const objTint = new THREE.Color(0xff6b2d); - const writeVertex = (row, col, x, y, z, intensity, isObject) => { + const writeVertex = (row, col, x, y, z, intensity) => { const o = (row * nAcross + col) * 3; positions[o] = x; positions[o + 1] = y; positions[o + 2] = z + 0.08; // slightly above so it reads over the wireframe - let c; - if (isObject) { - c = objTint; - } else if (intensity < 0.35) { - c = flat.clone().lerp(mid, intensity / 0.35); - } else { - c = mid.clone().lerp(hot, Math.min(1, (intensity - 0.35) / 0.65)); - } - // Keep the whole swath visible; boost alpha via brightness on relief. + const c = + intensity < 0.35 + ? flat.clone().lerp(mid, intensity / 0.35) + : mid.clone().lerp(hot, Math.min(1, (intensity - 0.35) / 0.65)); colors[o] = c.r; colors[o + 1] = c.g; colors[o + 2] = c.b; }; - for (let col = 0; col < nAcross; col += 1) { - const u = nAcross === 1 ? 0.5 : col / (nAcross - 1); - if (currentHits.length) { - const src = currentHits[Math.min(currentHits.length - 1, Math.round(u * (currentHits.length - 1)))]; - const intensity = reliefIntensity(src.x, src.y); - writeVertex(0, col, src.x, src.y, src.z, Math.max(intensity, src.isObject ? 1 : 0.2), !!src.isObject); - } else { - const angle = -swath * 0.5 + swath * u; - const dirX = Math.sin(angle) * acrossX; - const dirY = Math.sin(angle) * acrossY; - const dirZ = -Math.cos(angle); - // fallback probe from AUV - let hitX = startX; - let hitY = startY; - let hitZ = sampleHeight(meshInfo, startX, startY); - const originZ = hitZ + depth; - const step = Math.max(0.3, maxRange / 180); - let px = startX; - let py = startY; - let pz = originZ; - for (let s = 0; s < 220; s += 1) { - px += dirX * step; - py += dirY * step; - pz += dirZ * step; - const floorZ = sampleHeight(meshInfo, px, py); - if (pz <= floorZ) { - hitX = px; - hitY = py; - hitZ = floorZ; - break; - } + const probeSeafloorHit = (ax, ay, originZ, dirX, dirY, dirZ) => { + let hitX = ax; + let hitY = ay; + let hitZ = sampleHeight(meshInfo, ax, ay); + const step = Math.max(0.3, maxRange / 180); + let px = ax; + let py = ay; + let pz = originZ; + for (let s = 0; s < 220; s += 1) { + px += dirX * step; + py += dirY * step; + pz += dirZ * step; + if (Math.hypot(px - ax, py - ay) + Math.abs(pz - originZ) > maxRange * 1.15) break; + const floorZ = sampleHeight(meshInfo, px, py); + if (pz <= floorZ) { + hitX = px; + hitY = py; + hitZ = floorZ; + break; } - writeVertex(0, col, hitX, hitY, hitZ, Math.max(0.15, reliefIntensity(hitX, hitY)), false); } - } + return { hitX, hitY, hitZ }; + }; - // Forward stations: predicted beam footprint along the future track. - for (let row = 1; row < nAlong; row += 1) { - const along = (row / (nAlong - 1)) * length; + // Stations along track: seafloor footprint only (ignore object mesh until simulation). + // Beam origins stay at constant altitude above the flat datum (not terrain-following). + const originZ = referenceSeafloorZ(meshInfo) + depth; + for (let row = 0; row < nAlong; row += 1) { + const along = nAlong === 1 ? 0 : (row / (nAlong - 1)) * length; const ax = startX + hx * along; const ay = startY + hy * along; - const floorHere = sampleHeight(meshInfo, ax, ay); - const originZ = floorHere + depth; for (let col = 0; col < nAcross; col += 1) { const u = nAcross === 1 ? 0.5 : col / (nAcross - 1); const angle = -swath * 0.5 + swath * u; const dirX = Math.sin(angle) * acrossX; const dirY = Math.sin(angle) * acrossY; const dirZ = -Math.cos(angle); - let hitX = ax; - let hitY = ay; - let hitZ = floorHere; - let isObject = false; - const step = Math.max(0.3, maxRange / 180); - let px = ax; - let py = ay; - let pz = originZ; - for (let s = 0; s < 220; s += 1) { - px += dirX * step; - py += dirY * step; - pz += dirZ * step; - if (Math.hypot(px - ax, py - ay) + Math.abs(pz - originZ) > maxRange * 1.15) break; - const floorZ = sampleHeight(meshInfo, px, py); - if (pz <= floorZ) { - hitX = px; - hitY = py; - hitZ = floorZ; - break; - } - } - // Object occlusion preview: if object mesh is above floor along beam, tint as object. - if (objectPivot) { - const origin = new THREE.Vector3(ax, ay, originZ); - const dir = new THREE.Vector3(dirX, dirY, dirZ).normalize(); - raycaster.set(origin, dir); - raycaster.far = maxRange; - const intersects = raycaster.intersectObject(objectPivot, true); - if (intersects.length) { - const dFloor = Math.hypot(hitX - ax, hitY - ay, hitZ - originZ); - if (intersects[0].distance < dFloor) { - hitX = intersects[0].point.x; - hitY = intersects[0].point.y; - hitZ = intersects[0].point.z; - isObject = true; - } - } - } - writeVertex(row, col, hitX, hitY, hitZ, Math.max(0.12, reliefIntensity(hitX, hitY)), isObject); + const { hitX, hitY, hitZ } = probeSeafloorHit(ax, ay, originZ, dirX, dirY, dirZ); + writeVertex(row, col, hitX, hitY, hitZ, Math.max(0.12, reliefIntensity(hitX, hitY))); } } @@ -893,7 +844,7 @@ export function useMleSimulator(containerRef, surveyRef) { function placeAuv(x, y, depth, headingDeg) { if (!auvPivot) return; - const floorZ = sampleHeight(meshInfo, x, y); + const floorZ = referenceSeafloorZ(meshInfo); auvPivot.position.set(x, y, floorZ + Math.max(0.3, Number(depth) || 2.5)); headingRad = degToRad(headingDeg); auvPivot.rotation.set(0, 0, headingRad); @@ -915,7 +866,7 @@ export function useMleSimulator(containerRef, surveyRef) { ); } - function castBeamHits() { + function castBeamHits({ includeObject = true } = {}) { if (!auvPivot || !meshInfo) return []; const origin = auvPivot.position.clone(); const count = Math.max(1, Math.min(256, Number(beamCount) || 45)); @@ -923,7 +874,7 @@ export function useMleSimulator(containerRef, surveyRef) { const maxRange = Math.max(1, Number(detectionRangeM) || DETECTION_RANGE_DEFAULT); raycaster.far = maxRange; const across = new THREE.Vector3(-Math.sin(headingRad), Math.cos(headingRad), 0); - if (objectPivot) objectPivot.updateMatrixWorld(true); + if (includeObject && objectPivot) objectPivot.updateMatrixWorld(true); const hits = []; for (let i = 0; i < count; i += 1) { @@ -952,10 +903,10 @@ export function useMleSimulator(containerRef, surveyRef) { } } - // Object first-hit (mesh raycast) — echosounder return if closer than seafloor + // Object first-hit (mesh raycast) — only during simulation / survey accumulation let objPoint = null; let objDist = Infinity; - if (objectPivot) { + if (includeObject && objectPivot) { raycaster.set(origin, beamDir); const intersects = raycaster.intersectObject(objectPivot, true); if (intersects.length && intersects[0].distance <= maxRange) { @@ -996,7 +947,8 @@ export function useMleSimulator(containerRef, surveyRef) { } if (!auvPivot || !meshInfo) return; const origin = auvPivot.position.clone(); - const hits = castBeamHits(); + // Before motion starts, rays and path preview ignore the object (seafloor only). + const hits = castBeamHits({ includeObject: !!accumulateSurvey || running }); const positions = new Float32Array(hits.length * 2 * 3); for (let i = 0; i < hits.length; i += 1) { const o = i * 6; @@ -1060,8 +1012,8 @@ export function useMleSimulator(containerRef, surveyRef) { traveled += step; const nx = auvPivot.position.x + Math.cos(headingRad) * step; const ny = auvPivot.position.y + Math.sin(headingRad) * step; - const floorZ = sampleHeight(meshInfo, nx, ny); - auvPivot.position.set(nx, ny, floorZ + auvDepth); + // Constant altitude vs flat datum — do not follow seafloor relief. + auvPivot.position.set(nx, ny, referenceSeafloorZ(meshInfo) + auvDepth); updateRays(true); updateTrail(); onStatus({ diff --git a/frontend/web/src/views/GboView.vue b/frontend/web/src/views/GboView.vue index 755a093..e63c01b 100644 --- a/frontend/web/src/views/GboView.vue +++ b/frontend/web/src/views/GboView.vue @@ -221,11 +221,11 @@ function onShotSurvey() {