add sim mode..
This commit is contained in:
@@ -239,7 +239,6 @@
|
||||
#downloadMap,
|
||||
#pathToggle,
|
||||
#sensorToggle,
|
||||
#splatToggle,
|
||||
#poseToggle,
|
||||
#goalToggle,
|
||||
#obstacleToggle,
|
||||
@@ -258,7 +257,6 @@
|
||||
#downloadMap,
|
||||
#pathToggle,
|
||||
#sensorToggle,
|
||||
#splatToggle,
|
||||
#poseToggle,
|
||||
#goalToggle,
|
||||
#obstacleToggle {
|
||||
@@ -269,25 +267,6 @@
|
||||
align-items:center;
|
||||
gap:6px;
|
||||
}
|
||||
#splatToggle {
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
position:absolute;
|
||||
top:16px;
|
||||
left:16px;
|
||||
z-index:10;
|
||||
width:14px;
|
||||
min-width:14px;
|
||||
min-height:14px;
|
||||
height:14px;
|
||||
padding:0;
|
||||
border-color:rgba(203,213,225,.3);
|
||||
border-radius:2px;
|
||||
background:rgba(148,163,184,.3);
|
||||
box-shadow:none;
|
||||
}
|
||||
#splatToggle:hover { background:rgba(148,163,184,.5); border-color:rgba(226,232,240,.45); }
|
||||
#cameraPresetSelect {
|
||||
min-width:86px;
|
||||
padding:0 24px 0 9px;
|
||||
@@ -301,7 +280,6 @@
|
||||
#downloadMap:hover,
|
||||
#pathToggle:hover,
|
||||
#sensorToggle:hover,
|
||||
#splatToggle:hover,
|
||||
#poseToggle:hover,
|
||||
#goalToggle:hover,
|
||||
#obstacleToggle:hover,
|
||||
@@ -332,10 +310,6 @@
|
||||
border-color:rgba(103,232,249,.55);
|
||||
color:#fff;
|
||||
}
|
||||
#splatToggle.active {
|
||||
background:rgba(148,163,184,.62);
|
||||
border-color:rgba(226,232,240,.58);
|
||||
}
|
||||
#goalToggle.active {
|
||||
background:rgba(22,163,74,.62);
|
||||
border-color:rgba(134,239,172,.55);
|
||||
@@ -388,13 +362,11 @@
|
||||
.editor-mode #rosTopicStatusWrap {
|
||||
display:none;
|
||||
}
|
||||
#splatToggle { display:none; }
|
||||
.nav2-slam #goalToggle, .nav2-slam #poseToggle { display:none; }
|
||||
.nav2-navigation #downloadWrap { display:none; }
|
||||
.nav2-navigation #topLeftControls { display:block; }
|
||||
.nav2-navigation #keyGuide { display:none; }
|
||||
.nav2-navigation #rosTopicStatusWrap { bottom:16px; }
|
||||
.nav2-navigation.real-runtime #splatToggle { display:inline-flex; }
|
||||
.real-runtime #topLeftControls { display:none; }
|
||||
.real-runtime #keyGuide { display:none; }
|
||||
</style>
|
||||
@@ -403,7 +375,6 @@
|
||||
<div id="topLeftControls">
|
||||
<button id="obstacleToggle" title="벽 배치" aria-label="벽 배치" aria-pressed="false">Wall</button>
|
||||
</div>
|
||||
<button id="splatToggle" title="3D 맵 표시" aria-label="3D 맵 표시" aria-pressed="false"></button>
|
||||
<div id="topRightControls">
|
||||
<div id="downloadWrap">
|
||||
<button id="downloadMap" title="맵 저장" aria-label="맵 저장" disabled><span>SAVE</span></button>
|
||||
@@ -452,8 +423,7 @@
|
||||
{
|
||||
"imports": {
|
||||
"three": "../vendor/three/build/three.module.js",
|
||||
"three/addons/": "../vendor/three/examples/jsm/",
|
||||
"@mkkellogg/gaussian-splats-3d": "./vendor/gaussian-splats-3d.module.js"
|
||||
"three/addons/": "../vendor/three/examples/jsm/"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -464,7 +434,6 @@ import { TransformControls } from 'three/addons/controls/TransformControls.js';
|
||||
import { ColladaLoader } from 'three/addons/loaders/ColladaLoader.js';
|
||||
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
||||
import { STLLoader } from 'three/addons/loaders/STLLoader.js';
|
||||
import * as GaussianSplats3D from '@mkkellogg/gaussian-splats-3d';
|
||||
|
||||
const viewerMapKey = new URLSearchParams(window.location.search).get('mapKey');
|
||||
const params = new URLSearchParams(location.search);
|
||||
@@ -479,6 +448,7 @@ const SENSOR_POINT_CLOUD_MAX_AGE_MS = 3_000;
|
||||
// regular visualization sessions never poll `/camera-frame`.
|
||||
const ENABLE_CAMERA_PREVIEW = false;
|
||||
const CUSTOM_WORLD = !REAL_RUNTIME && viewerMapKey === 'nav2-custom';
|
||||
const SAVED_GAZEBO_MAP_WORLD = !REAL_RUNTIME && viewerMapKey === 'nav2-saved-gazebo-map';
|
||||
const activeWorldModel = viewerMapKey === 'nav2-turtlebot3-house' ? 'turtlebot3_house' : 'turtlebot3_world';
|
||||
const MKMINI_WORLD_SINGLE_PILLAR = USE_MKMINI_MODEL && activeWorldModel === 'turtlebot3_world';
|
||||
const MKMINI_RENDER_LENGTH = 0.84;
|
||||
@@ -502,7 +472,6 @@ const mapSaveOverlay = document.getElementById('mapSaveOverlay');
|
||||
const downloadMapButton = document.getElementById('downloadMap');
|
||||
const pathToggleButton = document.getElementById('pathToggle');
|
||||
const sensorToggleButton = document.getElementById('sensorToggle');
|
||||
const splatToggleButton = document.getElementById('splatToggle');
|
||||
const poseToggleButton = document.getElementById('poseToggle');
|
||||
const goalToggleButton = document.getElementById('goalToggle');
|
||||
const obstacleToggleButton = document.getElementById('obstacleToggle');
|
||||
@@ -513,7 +482,6 @@ const rosTopicStatusToggle = document.getElementById('rosTopicStatusToggle');
|
||||
const rosTopicStatusList = document.getElementById('rosTopicStatusList');
|
||||
const EDITOR_MODE = params.get('editor') === '1';
|
||||
const NAV2_VIEW_MODE = params.get('mode') === 'navigation' ? 'navigation' : 'slam';
|
||||
const SHOW_REMOTE_NAVIGATION_ROOM = REAL_RUNTIME && NAV2_VIEW_MODE === 'navigation';
|
||||
// A navigation session must keep the selected saved map as its fixed
|
||||
// reference. The runtime /map topic may later emit a partial local update.
|
||||
const LOCK_NAVIGATION_MAP = REAL_RUNTIME && NAV2_VIEW_MODE === 'navigation';
|
||||
@@ -601,10 +569,10 @@ const camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 0.05, 1
|
||||
// Three.js OrbitControls 기본 조작이 자연스럽게 동작하도록 사선 시점에서 시작합니다.
|
||||
camera.position.set(4, 6, 5);
|
||||
camera.up.set(0, 1, 0);
|
||||
const renderer = new THREE.WebGLRenderer({ antialias:!SHOW_REMOTE_NAVIGATION_ROOM });
|
||||
const renderer = new THREE.WebGLRenderer({ antialias:true });
|
||||
renderer.setSize(innerWidth, innerHeight);
|
||||
renderer.setPixelRatio(SHOW_REMOTE_NAVIGATION_ROOM ? 1 : Math.min(devicePixelRatio, 1.25));
|
||||
renderer.shadowMap.enabled = !SHOW_REMOTE_NAVIGATION_ROOM;
|
||||
renderer.setPixelRatio(Math.min(devicePixelRatio, 1.25));
|
||||
renderer.shadowMap.enabled = true;
|
||||
renderer.domElement.tabIndex = 0;
|
||||
document.body.appendChild(renderer.domElement);
|
||||
|
||||
@@ -615,7 +583,7 @@ controls.mouseButtons = {
|
||||
RIGHT: THREE.MOUSE.DOLLY,
|
||||
};
|
||||
renderer.domElement.addEventListener('contextmenu', (e) => e.preventDefault());
|
||||
controls.enableDamping = !SHOW_REMOTE_NAVIGATION_ROOM;
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.08;
|
||||
controls.target.set(0, 0, 0);
|
||||
camera.lookAt(controls.target);
|
||||
@@ -658,15 +626,6 @@ gazeboWorldRoot.add(runtimeLayoutObjects);
|
||||
const navigationMapRoot = new THREE.Group();
|
||||
navigationMapRoot.name = 'navigation-occupancy-map';
|
||||
sdfRoot.add(navigationMapRoot);
|
||||
let remoteNavigationSplatViewer = null;
|
||||
let remoteNavigationSplatScene = null;
|
||||
let remoteNavigationSplatTransform = null;
|
||||
let remoteNavigationSplatEnabled = false;
|
||||
let remoteNavigationSplatLoading = null;
|
||||
// 화면의 빨간 X축과 평행하게 오른쪽으로 미세 보정합니다.
|
||||
const remoteNavigationSplatBasePosition = new THREE.Vector3(-1.6822845855746674, 1.3107954323633903, -0.8021416967574936);
|
||||
const remoteNavigationSplatBaseQuaternion = new THREE.Quaternion(-0.10999094822602469, 0.6984998148234114, 0.6984998148234114, 0.10999094822602469);
|
||||
const remoteNavigationSplatBaseScale = new THREE.Vector3(3.672624762730334, 3.672624762730334, 3.5309628207501587);
|
||||
const pointCloudGeometry = SHOW_POINT_CLOUD ? new THREE.BufferGeometry() : null;
|
||||
let pointCloudLayer = null;
|
||||
let hasGlobalMapPointCloud = false;
|
||||
@@ -894,6 +853,10 @@ let lastMkMiniSteeringUpdateMs = null;
|
||||
let lastMkMiniMotionSample = null;
|
||||
let hasSimulatorMkMiniSteering = false;
|
||||
let laserGroup = null;
|
||||
let laserFillMesh = null;
|
||||
let laserLineSegments = null;
|
||||
let laserOpenSpaceLineSegments = null;
|
||||
let laserFrontBeam = null;
|
||||
let slamSocket = null;
|
||||
let controlSocket = null;
|
||||
let latestSlamState = null;
|
||||
@@ -908,6 +871,10 @@ let navigationMapLocked = false;
|
||||
let navigationMapFocused = false;
|
||||
let latestScanMetadata = null;
|
||||
let latestScanRanges = null;
|
||||
let lastLaserRenderAtMs = 0;
|
||||
let lastMiniMapRenderAtMs = 0;
|
||||
let pendingMiniMapState = null;
|
||||
let miniMapRenderTimer = null;
|
||||
let pointCloudRequestInFlight = false;
|
||||
let pointCloudRefreshTimer = null;
|
||||
let savedMapPointCloudLoaded = false;
|
||||
@@ -1432,70 +1399,10 @@ function createMkMiniFrontWheelPivots(model) {
|
||||
return pivots;
|
||||
}
|
||||
|
||||
async function loadRemoteNavigationRoom() {
|
||||
const viewer = new GaussianSplats3D.Viewer({
|
||||
selfDrivenMode: false,
|
||||
threeScene: scene,
|
||||
renderer,
|
||||
camera,
|
||||
useBuiltInControls: false,
|
||||
ignoreDevicePixelRatio: true,
|
||||
gpuAcceleratedSort: false,
|
||||
sharedMemoryForWorkers: false,
|
||||
integerBasedSort: true,
|
||||
halfPrecisionCovariancesOnGPU: true,
|
||||
dynamicScene: true,
|
||||
renderMode: GaussianSplats3D.RenderMode.Always,
|
||||
sceneRevealMode: GaussianSplats3D.SceneRevealMode.Instant,
|
||||
antialiased: false,
|
||||
sphericalHarmonicsDegree: 0,
|
||||
enableOptionalEffects: false,
|
||||
inMemoryCompressionLevel: 2,
|
||||
});
|
||||
await viewer.addSplatScene('./assets/room_0731_navigation.ply', {
|
||||
position: remoteNavigationSplatBasePosition.toArray(),
|
||||
rotation: remoteNavigationSplatBaseQuaternion.toArray(),
|
||||
scale: remoteNavigationSplatBaseScale.toArray(),
|
||||
splatAlphaRemovalThreshold: 0,
|
||||
showLoadingUI: false,
|
||||
});
|
||||
remoteNavigationSplatViewer = viewer;
|
||||
remoteNavigationSplatScene = viewer.splatMesh.getScene(0);
|
||||
remoteNavigationSplatTransform = null;
|
||||
updateRemoteNavigationRoomTransform();
|
||||
}
|
||||
|
||||
async function setRemoteNavigationSplatVisible(visible) {
|
||||
if (!SHOW_REMOTE_NAVIGATION_ROOM) return;
|
||||
|
||||
splatToggleButton.disabled = true;
|
||||
try {
|
||||
if (visible && !remoteNavigationSplatViewer) {
|
||||
if (!remoteNavigationSplatLoading) {
|
||||
remoteNavigationSplatLoading = loadRemoteNavigationRoom();
|
||||
}
|
||||
await remoteNavigationSplatLoading;
|
||||
}
|
||||
remoteNavigationSplatEnabled = visible;
|
||||
if (remoteNavigationSplatScene) {
|
||||
remoteNavigationSplatScene.visible = visible;
|
||||
if (visible) {
|
||||
remoteNavigationSplatTransform = null;
|
||||
updateRemoteNavigationRoomTransform();
|
||||
}
|
||||
}
|
||||
remoteNavigationSplatViewer?.forceRenderNextFrame();
|
||||
} catch (error) {
|
||||
console.warn('remote navigation room render failed', error);
|
||||
remoteNavigationSplatEnabled = false;
|
||||
} finally {
|
||||
splatToggleButton.disabled = false;
|
||||
splatToggleButton.classList.toggle('active', remoteNavigationSplatEnabled);
|
||||
splatToggleButton.setAttribute('aria-pressed', String(remoteNavigationSplatEnabled));
|
||||
}
|
||||
}
|
||||
|
||||
const LASER_HOLE_R = 0.14;
|
||||
const LASER_RENDER_INTERVAL_MS = 100;
|
||||
const LASER_OPEN_SPACE_RAY_STRIDE = 4;
|
||||
const MINI_MAP_RENDER_INTERVAL_MS = 100;
|
||||
const LASER_OFFSET_X = USE_MKMINI_MODEL ? 0.15 : -0.032;
|
||||
const LASER_OFFSET_Y = 0;
|
||||
// Fallback height when an explicit ROS scan pose is unavailable.
|
||||
@@ -1507,9 +1414,12 @@ function setRobotPose(pose) {
|
||||
targetRobotPose = {
|
||||
x: Number(pose.x ?? targetRobotPose.x),
|
||||
y: Number(pose.y ?? targetRobotPose.y),
|
||||
z: Number(pose.z ?? targetRobotPose.z),
|
||||
roll: Number(pose.roll ?? targetRobotPose.roll),
|
||||
pitch: Number(pose.pitch ?? targetRobotPose.pitch),
|
||||
// Gazebo navigation is a ground-plane simulation. A saved 3D map can
|
||||
// include an elevated RTAB-Map pose, but that must not lift the rendered
|
||||
// vehicle after the learner supplies a 2D initial pose.
|
||||
z: REAL_RUNTIME ? Number(pose.z ?? targetRobotPose.z) : 0,
|
||||
roll: REAL_RUNTIME ? Number(pose.roll ?? targetRobotPose.roll) : 0,
|
||||
pitch: REAL_RUNTIME ? Number(pose.pitch ?? targetRobotPose.pitch) : 0,
|
||||
yaw: Number(pose.yaw ?? targetRobotPose.yaw),
|
||||
};
|
||||
updateMkMiniSteeringTarget(pose);
|
||||
@@ -1750,11 +1660,72 @@ function laserDirectionFromPose(pose, angle) {
|
||||
return new THREE.Vector3(Math.cos(angle), Math.sin(angle), 0).applyQuaternion(q).normalize();
|
||||
}
|
||||
|
||||
function ensureLaserScanLayer() {
|
||||
if (laserGroup) {
|
||||
laserGroup.visible = true;
|
||||
return;
|
||||
}
|
||||
|
||||
laserGroup = new THREE.Group();
|
||||
laserGroup.name = 'live-laser-scan';
|
||||
|
||||
const fillGeometry = new THREE.BufferGeometry();
|
||||
laserFillMesh = new THREE.Mesh(
|
||||
fillGeometry,
|
||||
new THREE.MeshBasicMaterial({
|
||||
color: 0x005cff,
|
||||
transparent: true,
|
||||
opacity: 0.6,
|
||||
side: THREE.DoubleSide,
|
||||
depthWrite: false,
|
||||
}),
|
||||
);
|
||||
laserFillMesh.frustumCulled = false;
|
||||
|
||||
const lineGeometry = new THREE.BufferGeometry();
|
||||
laserLineSegments = new THREE.LineSegments(
|
||||
lineGeometry,
|
||||
new THREE.LineBasicMaterial({ color: 0x3366ff, transparent: true, opacity: 0.5 }),
|
||||
);
|
||||
laserLineSegments.frustumCulled = false;
|
||||
|
||||
const openSpaceGeometry = new THREE.BufferGeometry();
|
||||
laserOpenSpaceLineSegments = new THREE.LineSegments(
|
||||
openSpaceGeometry,
|
||||
new THREE.LineBasicMaterial({ color: 0x6f9dff, transparent: true, opacity: 0.28 }),
|
||||
);
|
||||
laserOpenSpaceLineSegments.frustumCulled = false;
|
||||
|
||||
laserFrontBeam = new THREE.Mesh(
|
||||
new THREE.CylinderGeometry(0.006, 0.006, 1, 16),
|
||||
new THREE.MeshBasicMaterial({ color: 0x005cff }),
|
||||
);
|
||||
laserFrontBeam.frustumCulled = false;
|
||||
|
||||
laserGroup.add(laserFillMesh, laserLineSegments, laserOpenSpaceLineSegments, laserFrontBeam);
|
||||
sdfRoot.add(laserGroup);
|
||||
}
|
||||
|
||||
function updateLaserGeometry(geometry, values) {
|
||||
let position = geometry.getAttribute('position');
|
||||
if (!position || position.array.length < values.length) {
|
||||
const capacity = Math.max(1024, Math.ceil(values.length * 1.25));
|
||||
position = new THREE.BufferAttribute(new Float32Array(capacity), 3);
|
||||
position.setUsage(THREE.DynamicDrawUsage);
|
||||
geometry.setAttribute('position', position);
|
||||
}
|
||||
position.array.set(values);
|
||||
position.needsUpdate = true;
|
||||
geometry.setDrawRange(0, values.length / 3);
|
||||
}
|
||||
|
||||
function clearLaserScan() {
|
||||
if (!laserGroup) return;
|
||||
sdfRoot.remove(laserGroup);
|
||||
disposeObject(laserGroup);
|
||||
laserGroup = null;
|
||||
laserGroup.visible = false;
|
||||
laserFillMesh.visible = false;
|
||||
laserLineSegments.visible = false;
|
||||
laserOpenSpaceLineSegments.visible = false;
|
||||
laserFrontBeam.visible = false;
|
||||
}
|
||||
|
||||
function setNavigationSensorVisible(visible) {
|
||||
@@ -1767,6 +1738,7 @@ function setNavigationSensorVisible(visible) {
|
||||
window.clearInterval(sensorPointCloudRefreshTimer);
|
||||
sensorPointCloudRefreshTimer = null;
|
||||
}
|
||||
lastLaserRenderAtMs = 0;
|
||||
clearLaserScan();
|
||||
clearSensorPointCloud();
|
||||
drawMiniMap(latestSlamState);
|
||||
@@ -1775,7 +1747,7 @@ function setNavigationSensorVisible(visible) {
|
||||
if (SHOW_SENSOR_POINT_CLOUD) {
|
||||
void refreshSensorPointCloud();
|
||||
if (!sensorPointCloudRefreshTimer) {
|
||||
sensorPointCloudRefreshTimer = window.setInterval(() => void refreshSensorPointCloud(), 500);
|
||||
sensorPointCloudRefreshTimer = window.setInterval(() => void refreshSensorPointCloud(), 750);
|
||||
}
|
||||
}
|
||||
if (latestScanMetadata && latestScanRanges) {
|
||||
@@ -1796,6 +1768,11 @@ function drawLaserScan(scan, scanPose = (
|
||||
return;
|
||||
}
|
||||
if (!scan?.ranges?.length) return;
|
||||
// The state socket and scan binary socket can deliver the same scan nearly
|
||||
// together. Rebuilding transparent scan geometry twice stalls the viewer.
|
||||
const now = performance.now();
|
||||
if (now - lastLaserRenderAtMs < LASER_RENDER_INTERVAL_MS) return;
|
||||
lastLaserRenderAtMs = now;
|
||||
clearLaserScan();
|
||||
|
||||
const sourcePose = scanPose || currentRobotPose;
|
||||
@@ -1830,7 +1807,7 @@ function drawLaserScan(scan, scanPose = (
|
||||
endpoints.push({ start, point, hit: valid });
|
||||
if (valid) {
|
||||
linePoints.push(start.x, start.y, start.z, point.x, point.y, point.z);
|
||||
} else if (showOpenSpaceRays) {
|
||||
} else if (showOpenSpaceRays && i % LASER_OPEN_SPACE_RAY_STRIDE === 0) {
|
||||
// Match Gazebo's ray visualization: no-return rays remain visible, but
|
||||
// deliberately subdued so detected obstacles stay the primary signal.
|
||||
openSpaceLinePoints.push(start.x, start.y, start.z, point.x, point.y, point.z);
|
||||
@@ -1850,32 +1827,13 @@ function drawLaserScan(scan, scanPose = (
|
||||
fillPoints.push(next.start.x, next.start.y, next.start.z);
|
||||
}
|
||||
|
||||
const grp = new THREE.Group();
|
||||
const fillMat = new THREE.MeshBasicMaterial({
|
||||
color: 0x005cff,
|
||||
transparent: true,
|
||||
opacity: 0.6,
|
||||
side: THREE.DoubleSide,
|
||||
depthWrite: false,
|
||||
});
|
||||
const lineMat = new THREE.LineBasicMaterial({ color: 0x3366ff, transparent: true, opacity: 0.5 });
|
||||
|
||||
if (fillPoints.length) {
|
||||
const geo = new THREE.BufferGeometry();
|
||||
geo.setAttribute('position', new THREE.Float32BufferAttribute(fillPoints, 3));
|
||||
grp.add(new THREE.Mesh(geo, fillMat));
|
||||
}
|
||||
const lineGeo = new THREE.BufferGeometry();
|
||||
lineGeo.setAttribute('position', new THREE.Float32BufferAttribute(linePoints, 3));
|
||||
grp.add(new THREE.LineSegments(lineGeo, lineMat));
|
||||
if (openSpaceLinePoints.length) {
|
||||
const openSpaceGeo = new THREE.BufferGeometry();
|
||||
openSpaceGeo.setAttribute('position', new THREE.Float32BufferAttribute(openSpaceLinePoints, 3));
|
||||
grp.add(new THREE.LineSegments(
|
||||
openSpaceGeo,
|
||||
new THREE.LineBasicMaterial({ color: 0x6f9dff, transparent: true, opacity: 0.28 }),
|
||||
));
|
||||
}
|
||||
ensureLaserScanLayer();
|
||||
updateLaserGeometry(laserFillMesh.geometry, fillPoints);
|
||||
laserFillMesh.visible = fillPoints.length > 0;
|
||||
updateLaserGeometry(laserLineSegments.geometry, linePoints);
|
||||
laserLineSegments.visible = linePoints.length > 0;
|
||||
updateLaserGeometry(laserOpenSpaceLineSegments.geometry, openSpaceLinePoints);
|
||||
laserOpenSpaceLineSegments.visible = openSpaceLinePoints.length > 0;
|
||||
|
||||
const frontIndex = Math.max(0, Math.min(scan.ranges.length - 1, Math.round((0 - angleMin) / angleInc)));
|
||||
const front = endpoints[frontIndex];
|
||||
@@ -1889,22 +1847,20 @@ function drawLaserScan(scan, scanPose = (
|
||||
);
|
||||
const len = delta.length();
|
||||
if (len > 0.01) {
|
||||
const beam = new THREE.Mesh(
|
||||
new THREE.CylinderGeometry(0.006, 0.006, len, 16),
|
||||
new THREE.MeshBasicMaterial({ color: 0x005cff })
|
||||
);
|
||||
beam.position.set(
|
||||
laserFrontBeam.position.set(
|
||||
(front.start.x + front.point.x) / 2,
|
||||
(front.start.y + front.point.y) / 2,
|
||||
(front.start.z + front.point.z) / 2
|
||||
);
|
||||
beam.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), delta.normalize());
|
||||
grp.add(beam);
|
||||
laserFrontBeam.scale.set(1, len, 1);
|
||||
laserFrontBeam.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), delta.normalize());
|
||||
laserFrontBeam.visible = true;
|
||||
} else {
|
||||
laserFrontBeam.visible = false;
|
||||
}
|
||||
} else {
|
||||
laserFrontBeam.visible = false;
|
||||
}
|
||||
|
||||
laserGroup = grp;
|
||||
sdfRoot.add(grp);
|
||||
}
|
||||
|
||||
function updatePointGeometry(geometry, points) {
|
||||
@@ -1923,6 +1879,13 @@ function updatePointGeometry(geometry, points) {
|
||||
positions[count++] = z;
|
||||
}
|
||||
|
||||
if (count === 0) {
|
||||
geometry.deleteAttribute('position');
|
||||
geometry.boundingBox = null;
|
||||
geometry.boundingSphere = null;
|
||||
return;
|
||||
}
|
||||
|
||||
geometry.setAttribute('position', new THREE.BufferAttribute(positions.slice(0, count), 3));
|
||||
geometry.computeBoundingSphere();
|
||||
}
|
||||
@@ -2061,6 +2024,19 @@ async function refreshCameraFrame() {
|
||||
}
|
||||
|
||||
function drawMiniMap(state) {
|
||||
pendingMiniMapState = state;
|
||||
if (miniMapRenderTimer) return;
|
||||
|
||||
const elapsed = performance.now() - lastMiniMapRenderAtMs;
|
||||
const wait = Math.max(0, MINI_MAP_RENDER_INTERVAL_MS - elapsed);
|
||||
miniMapRenderTimer = window.setTimeout(() => {
|
||||
miniMapRenderTimer = null;
|
||||
lastMiniMapRenderAtMs = performance.now();
|
||||
drawMiniMapNow(pendingMiniMapState);
|
||||
}, wait);
|
||||
}
|
||||
|
||||
function drawMiniMapNow(state) {
|
||||
if (!miniCtx) return;
|
||||
const w = miniMap.width;
|
||||
const h = miniMap.height;
|
||||
@@ -2247,7 +2223,6 @@ function updateNavigationMapTransform() {
|
||||
// estimate and must not move the static world geometry.
|
||||
gazeboWorldRoot.position.set(0, 0, 0);
|
||||
gazeboWorldRoot.rotation.set(0, 0, 0);
|
||||
updateRemoteNavigationRoomTransform();
|
||||
return;
|
||||
}
|
||||
gazeboWorldRoot.position.set(0, 0, 0);
|
||||
@@ -2263,61 +2238,6 @@ function updateNavigationMapTransform() {
|
||||
0,
|
||||
);
|
||||
navigationMapRoot.rotation.set(0, 0, -yaw);
|
||||
updateRemoteNavigationRoomTransform();
|
||||
}
|
||||
|
||||
function updateRemoteNavigationRoomTransform() {
|
||||
if (!remoteNavigationSplatEnabled || !remoteNavigationSplatScene || NAV2_VIEW_MODE !== 'navigation') return;
|
||||
|
||||
if (navigationSceneUsesMapFrame()) {
|
||||
if (remoteNavigationSplatTransform?.frame === 'map') return;
|
||||
remoteNavigationSplatScene.position.copy(remoteNavigationSplatBasePosition);
|
||||
remoteNavigationSplatScene.quaternion.copy(remoteNavigationSplatBaseQuaternion);
|
||||
remoteNavigationSplatScene.scale.copy(remoteNavigationSplatBaseScale);
|
||||
remoteNavigationSplatViewer.splatMesh.updateTransforms();
|
||||
remoteNavigationSplatTransform = { frame: 'map' };
|
||||
return;
|
||||
}
|
||||
|
||||
const transform = navigationMapToOdom || { x:0, y:0, yaw:0 };
|
||||
if (remoteNavigationSplatTransform
|
||||
&& remoteNavigationSplatTransform.frame === 'odom'
|
||||
&& Math.abs(remoteNavigationSplatTransform.x - (transform.x || 0)) < 0.03
|
||||
&& Math.abs(remoteNavigationSplatTransform.y - (transform.y || 0)) < 0.03
|
||||
&& Math.abs(remoteNavigationSplatTransform.yaw - (transform.yaw || 0)) < 0.01) {
|
||||
return;
|
||||
}
|
||||
const yaw = transform.yaw || 0;
|
||||
const c = Math.cos(yaw);
|
||||
const s = Math.sin(yaw);
|
||||
// The Gaussian scene is authored in map coordinates. Compose the same
|
||||
// map <- odom inverse used by the occupancy-map root before each render.
|
||||
const mapToOdomMatrix = new THREE.Matrix4().compose(
|
||||
new THREE.Vector3(
|
||||
-c * (transform.x || 0) - s * (transform.y || 0),
|
||||
0,
|
||||
-s * (transform.x || 0) + c * (transform.y || 0),
|
||||
),
|
||||
new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), -yaw),
|
||||
new THREE.Vector3(1, 1, 1),
|
||||
);
|
||||
const sceneMatrix = mapToOdomMatrix.multiply(new THREE.Matrix4().compose(
|
||||
remoteNavigationSplatBasePosition,
|
||||
remoteNavigationSplatBaseQuaternion,
|
||||
remoteNavigationSplatBaseScale,
|
||||
));
|
||||
sceneMatrix.decompose(
|
||||
remoteNavigationSplatScene.position,
|
||||
remoteNavigationSplatScene.quaternion,
|
||||
remoteNavigationSplatScene.scale,
|
||||
);
|
||||
remoteNavigationSplatViewer.splatMesh.updateTransforms();
|
||||
remoteNavigationSplatTransform = {
|
||||
frame: 'odom',
|
||||
x: transform.x || 0,
|
||||
y: transform.y || 0,
|
||||
yaw,
|
||||
};
|
||||
}
|
||||
|
||||
function renderNavigationOccupancyMap() {
|
||||
@@ -2615,11 +2535,6 @@ sensorToggleButton.addEventListener('click', () => {
|
||||
focusViewer();
|
||||
});
|
||||
|
||||
splatToggleButton.addEventListener('click', () => {
|
||||
void setRemoteNavigationSplatVisible(!remoteNavigationSplatEnabled);
|
||||
focusViewer();
|
||||
});
|
||||
|
||||
cameraPreviewToggle?.addEventListener('click', () => {
|
||||
const collapsed = cameraPreviewWrap?.classList.toggle('is-collapsed') ?? false;
|
||||
cameraPreviewToggle.setAttribute('aria-expanded', String(!collapsed));
|
||||
@@ -3161,7 +3076,7 @@ function applySlamState(state) {
|
||||
updateNavigationPlan(state?.plan);
|
||||
// Remote navigation is rendered in the map frame, matching the saved map,
|
||||
// room model, AMCL pose, route and goal. Local Gazebo remains odom-based.
|
||||
const visualizationPose = (SHOW_POINT_CLOUD || (REAL_RUNTIME && NAV2_VIEW_MODE === 'navigation' && state?.pose_3d))
|
||||
const visualizationPose = (REAL_RUNTIME && (SHOW_POINT_CLOUD || (NAV2_VIEW_MODE === 'navigation' && state?.pose_3d)))
|
||||
? (state?.pose_3d || state?.pose)
|
||||
: (NAV2_VIEW_MODE === 'navigation'
|
||||
? (navigationSceneUsesMapFrame() ? (state?.pose_map || state?.pose) : state?.pose)
|
||||
@@ -3230,7 +3145,7 @@ if (SHOW_POINT_CLOUD) {
|
||||
}
|
||||
if (SHOW_SENSOR_POINT_CLOUD && navigationSensorVisible) {
|
||||
void refreshSensorPointCloud();
|
||||
sensorPointCloudRefreshTimer = window.setInterval(() => void refreshSensorPointCloud(), 500);
|
||||
sensorPointCloudRefreshTimer = window.setInterval(() => void refreshSensorPointCloud(), 750);
|
||||
}
|
||||
if (ENABLE_CAMERA_PREVIEW && REAL_RUNTIME) {
|
||||
void refreshCameraFrame();
|
||||
@@ -3240,6 +3155,7 @@ if (ENABLE_CAMERA_PREVIEW && REAL_RUNTIME) {
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (pointCloudRefreshTimer) window.clearInterval(pointCloudRefreshTimer);
|
||||
if (sensorPointCloudRefreshTimer) window.clearInterval(sensorPointCloudRefreshTimer);
|
||||
if (miniMapRenderTimer) window.clearTimeout(miniMapRenderTimer);
|
||||
if (cameraFrameRefreshTimer) window.clearInterval(cameraFrameRefreshTimer);
|
||||
if (cameraFrameObjectUrl) URL.revokeObjectURL(cameraFrameObjectUrl);
|
||||
});
|
||||
@@ -3459,7 +3375,7 @@ window.addEventListener('blur', () => {
|
||||
|
||||
// A static Gazebo model can contain assets that the browser cannot render.
|
||||
// Keep the ROS stream available even when the decorative world mesh fails.
|
||||
const worldSceneTasks = CUSTOM_WORLD || REAL_RUNTIME
|
||||
const worldSceneTasks = CUSTOM_WORLD || SAVED_GAZEBO_MAP_WORLD || REAL_RUNTIME
|
||||
? []
|
||||
: [buildFromSdf().catch((error) => console.warn('world model render failed', error))];
|
||||
const remoteNavigationSceneTasks = [];
|
||||
@@ -3488,12 +3404,7 @@ function animate(){
|
||||
updateSmoothedRobotPose();
|
||||
updateMkMiniFrontWheelSteering();
|
||||
controls.update();
|
||||
if (remoteNavigationSplatViewer && remoteNavigationSplatEnabled) {
|
||||
remoteNavigationSplatViewer.update();
|
||||
remoteNavigationSplatViewer.render();
|
||||
} else {
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
animate();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user