add commits..
This commit is contained in:
@@ -389,7 +389,7 @@
|
||||
display:none;
|
||||
}
|
||||
#splatToggle { display:none; }
|
||||
.nav2-slam #goalToggle, .nav2-slam #poseToggle, .nav2-slam #sensorToggle { 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; }
|
||||
@@ -473,6 +473,7 @@ const ROBOT_PLATFORM = (params.get('platform') || '').trim().toLowerCase();
|
||||
// Keep the prior real-platform default when an older LMS URL has no platform.
|
||||
const USE_MKMINI_MODEL = ROBOT_PLATFORM === 'mkmini' || (!ROBOT_PLATFORM && REAL_RUNTIME);
|
||||
const SHOW_POINT_CLOUD = params.get('pointCloud') === '1';
|
||||
const SHOW_SENSOR_POINT_CLOUD = REAL_RUNTIME && SHOW_POINT_CLOUD;
|
||||
const SENSOR_POINT_CLOUD_MAX_AGE_MS = 3_000;
|
||||
// Camera preview is kept for the RGB test module, but is paused for now so
|
||||
// regular visualization sessions never poll `/camera-frame`.
|
||||
@@ -480,8 +481,10 @@ const ENABLE_CAMERA_PREVIEW = false;
|
||||
const CUSTOM_WORLD = !REAL_RUNTIME && viewerMapKey === 'nav2-custom';
|
||||
const activeWorldModel = viewerMapKey === 'nav2-turtlebot3-house' ? 'turtlebot3_house' : 'turtlebot3_world';
|
||||
const MKMINI_WORLD_SPACING_SCALE = USE_MKMINI_MODEL && activeWorldModel === 'turtlebot3_world' ? 1.6 : 1;
|
||||
const MKMINI_RENDER_LENGTH = 0.855;
|
||||
const MKMINI_WHEELBASE_METERS = 0.325;
|
||||
const MKMINI_RENDER_LENGTH = 0.84;
|
||||
const MKMINI_RENDER_WIDTH = 0.61;
|
||||
const MKMINI_RENDER_HEIGHT = 0.6575;
|
||||
const MKMINI_WHEELBASE_METERS = 0.6;
|
||||
const MKMINI_MAX_STEERING_RADIANS = 0.52;
|
||||
const DEFAULT_SIMULATION_SPAWN = USE_MKMINI_MODEL
|
||||
? { x: -1.4, y: -0.5 }
|
||||
@@ -648,6 +651,8 @@ gazeboWorldRoot.name = 'gazebo-world';
|
||||
sdfRoot.add(gazeboWorldRoot);
|
||||
const editorObjects = new THREE.Group();
|
||||
sdfRoot.add(editorObjects);
|
||||
const editorBoundaryObjects = new THREE.Group();
|
||||
sdfRoot.add(editorBoundaryObjects);
|
||||
const runtimeLayoutObjects = new THREE.Group();
|
||||
gazeboWorldRoot.add(runtimeLayoutObjects);
|
||||
const navigationMapRoot = new THREE.Group();
|
||||
@@ -665,9 +670,9 @@ const remoteNavigationSplatBaseScale = new THREE.Vector3(3.672624762730334, 3.67
|
||||
const pointCloudGeometry = SHOW_POINT_CLOUD ? new THREE.BufferGeometry() : null;
|
||||
let pointCloudLayer = null;
|
||||
let hasGlobalMapPointCloud = false;
|
||||
// A real platform can provide a 3D LiDAR stream even when this viewer was
|
||||
// opened through a legacy NAV2 stack URL without pointCloud=1.
|
||||
const sensorPointCloudGeometry = REAL_RUNTIME ? new THREE.BufferGeometry() : null;
|
||||
// The live sensor point cloud belongs to the 3D view. 2D SLAM renders only
|
||||
// LaserScan and must never poll the point-cloud endpoint.
|
||||
const sensorPointCloudGeometry = SHOW_SENSOR_POINT_CLOUD ? new THREE.BufferGeometry() : null;
|
||||
let sensorPointCloudLayer = null;
|
||||
if (pointCloudGeometry) {
|
||||
pointCloudLayer = new THREE.Points(
|
||||
@@ -833,8 +838,9 @@ function createLayoutMesh(item, editor = false) {
|
||||
|
||||
function renderEditorLayout(layout) {
|
||||
if (!EDITOR_MODE) return;
|
||||
editorLayout = layout && Array.isArray(layout.objects) ? layout : { version:1, objects:[] };
|
||||
editorLayout = layout && Array.isArray(layout.objects) ? layout : { version:1, bounds:{ width:10, depth:10 }, objects:[] };
|
||||
clearLayoutObjects(editorObjects);
|
||||
clearLayoutObjects(editorBoundaryObjects);
|
||||
for (const item of editorLayout.objects) {
|
||||
editorObjects.add(createLayoutMesh(item, true));
|
||||
}
|
||||
@@ -846,7 +852,7 @@ async function refreshRuntimeLayout() {
|
||||
if (EDITOR_MODE || REAL_RUNTIME || !CUSTOM_WORLD) return;
|
||||
try {
|
||||
const layout = await fetch(`${BACKEND_URL}/world-layout`).then(response => response.ok ? response.json() : { objects: [] });
|
||||
const signature = JSON.stringify(layout?.objects || []);
|
||||
const signature = JSON.stringify({ bounds: layout?.bounds || null, objects: layout?.objects || [] });
|
||||
if (signature === runtimeLayoutSignature) return;
|
||||
runtimeLayoutSignature = signature;
|
||||
clearLayoutObjects(runtimeLayoutObjects);
|
||||
@@ -884,6 +890,7 @@ let targetMkMiniSteeringAngle = 0;
|
||||
let currentMkMiniSteeringAngle = 0;
|
||||
let targetMkMiniSteeringAngles = { left: 0, right: 0 };
|
||||
let currentMkMiniSteeringAngles = { left: 0, right: 0 };
|
||||
let lastMkMiniSteeringUpdateMs = null;
|
||||
let lastMkMiniMotionSample = null;
|
||||
let hasSimulatorMkMiniSteering = false;
|
||||
let laserGroup = null;
|
||||
@@ -910,10 +917,12 @@ let cameraFrameRequestInFlight = false;
|
||||
let cameraFrameRefreshTimer = null;
|
||||
let cameraFrameObjectUrl = null;
|
||||
let pathVisible = false;
|
||||
// Sensor data is opt-in in every visualization mode. A saved 3D map is
|
||||
// rendered independently, so it must not implicitly enable the live lidar.
|
||||
let navigationSensorVisible = false;
|
||||
if (sensorPointCloudLayer) sensorPointCloudLayer.visible = false;
|
||||
// SLAM always renders its live scan, including the forward beam. Navigation
|
||||
// keeps the sensor opt-in so a saved-map view starts cleanly.
|
||||
let navigationSensorVisible = NAV2_VIEW_MODE !== 'navigation';
|
||||
if (sensorPointCloudLayer) sensorPointCloudLayer.visible = navigationSensorVisible;
|
||||
sensorToggleButton.classList.toggle('active', navigationSensorVisible);
|
||||
sensorToggleButton.setAttribute('aria-pressed', String(navigationSensorVisible));
|
||||
const robotPathPoints = [];
|
||||
let robotPathLine = null;
|
||||
let navigationPlanLine = null;
|
||||
@@ -1183,7 +1192,7 @@ function addCylinder(parent, visual, geometry) {
|
||||
const cyl = directChild(geometry, 'cylinder');
|
||||
const radius = Number(getDirectText(cyl, 'radius', '0.1')) || .1;
|
||||
const sourceLength = Number(getDirectText(cyl, 'length', '1')) || 1;
|
||||
const length = isMkMiniWorldPillar(visual) && sourceLength === .5 ? .8 : sourceLength;
|
||||
const length = isMkMiniWorldPillar(visual) && sourceLength === .5 ? 1.2 : sourceLength;
|
||||
const mesh = new THREE.Mesh(new THREE.CylinderGeometry(radius, radius, length, 32), materialFromVisual(visual));
|
||||
// Three cylinder 기본 축은 Y, SDF cylinder 기본 축은 Z입니다.
|
||||
mesh.rotation.x = Math.PI / 2;
|
||||
@@ -1231,7 +1240,7 @@ async function buildFromSdf() {
|
||||
const visualGroup = new THREE.Group();
|
||||
visualGroup.name = visual.getAttribute('name') || 'visual';
|
||||
applyWorldSdfPose(visualGroup, parsePose(getDirectText(visual, 'pose', '0 0 0 0 0 0')));
|
||||
if (isMkMiniWorldPillar(visual)) visualGroup.position.z += 0.15;
|
||||
if (isMkMiniWorldPillar(visual)) visualGroup.position.z = 0.6;
|
||||
linkGroup.add(visualGroup);
|
||||
|
||||
const geometry = directChild(visual, 'geometry');
|
||||
@@ -1366,15 +1375,18 @@ async function loadMkMiniModel(sdfPose = { x:0, y:0, z:0, roll:0, pitch:0, yaw:0
|
||||
model.updateMatrixWorld(true);
|
||||
const initialBounds = new THREE.Box3().setFromObject(model);
|
||||
const initialSize = initialBounds.getSize(new THREE.Vector3());
|
||||
const largestDimension = Math.max(initialSize.x, initialSize.y, initialSize.z, 0.001);
|
||||
// Match the Gazebo MKMini collision body's 1.05 m longest dimension.
|
||||
model.scale.setScalar(MKMINI_RENDER_LENGTH / largestDimension);
|
||||
// The GLB is rotated into the SDF coordinate system above. Its local axes
|
||||
// then map to SDF as X=length, Z=width, Y=height. Normalize every axis to
|
||||
// the same physical envelope used by the Gazebo collision model.
|
||||
const scaleX = MKMINI_RENDER_LENGTH / Math.max(initialSize.x, 0.001);
|
||||
const scaleY = MKMINI_RENDER_HEIGHT / Math.max(initialSize.z, 0.001);
|
||||
const scaleZ = MKMINI_RENDER_WIDTH / Math.max(initialSize.y, 0.001);
|
||||
model.scale.set(scaleX, scaleY, scaleZ);
|
||||
model.updateMatrixWorld(true);
|
||||
|
||||
const normalizedBounds = new THREE.Box3().setFromObject(model);
|
||||
const center = normalizedBounds.getCenter(new THREE.Vector3());
|
||||
// This model lives under sdfRoot (Gazebo Z-up). Center it on base_link in
|
||||
// the horizontal plane and place its base on SDF Z=0.
|
||||
// Keep the wheels on SDF Z=0 after scaling the vehicle height.
|
||||
model.position.set(-center.x, -center.y, -normalizedBounds.min.z);
|
||||
model.traverse((child) => {
|
||||
if (!child.isMesh) return;
|
||||
@@ -1491,15 +1503,11 @@ async function setRemoteNavigationSplatVisible(visible) {
|
||||
}
|
||||
|
||||
const LASER_HOLE_R = 0.14;
|
||||
const LASER_OFFSET_X = USE_MKMINI_MODEL ? 0.1 : -0.032;
|
||||
const LASER_OFFSET_X = USE_MKMINI_MODEL ? 0.15 : -0.032;
|
||||
const LASER_OFFSET_Y = 0;
|
||||
// Calibrated to the MKMini GLB at the current display scale.
|
||||
// Fallback height when an explicit ROS scan pose is unavailable.
|
||||
const LASER_Z = USE_MKMINI_MODEL ? 0.5 : 0.181;
|
||||
// The MKMini ROS frame reports os_lidar at Z=0.6 m while the bundled GLB
|
||||
// places the visible LiDAR head at approximately Z=0.5 m. Keep ROS/map
|
||||
// coordinates untouched and correct only the rendered scan origin.
|
||||
const MKMINI_LASER_RENDER_Z_OFFSET = 0.1;
|
||||
const MKMINI_LASER_RENDER_FORWARD_OFFSET = 0.12;
|
||||
const MKMINI_LASER_RENDER_BACKWARD_OFFSET = 0.03;
|
||||
|
||||
function setRobotPose(pose) {
|
||||
if (!pose) return;
|
||||
@@ -1556,7 +1564,6 @@ function applyCurrentRobotPose() {
|
||||
robotModel.visible = shouldRenderRobot();
|
||||
robotModel.position.set(currentRobotPose.x, currentRobotPose.y, currentRobotPose.z);
|
||||
robotModel.rotation.set(currentRobotPose.roll, currentRobotPose.pitch, currentRobotPose.yaw, 'XYZ');
|
||||
updateMkMiniFrontWheelSteering();
|
||||
}
|
||||
|
||||
function updateMkMiniSteeringTarget(pose) {
|
||||
@@ -1647,7 +1654,7 @@ function mkMiniAckermannAngles(steeringAngle) {
|
||||
if (Math.abs(steeringAngle) < 0.0001) return { left: 0, right: 0 };
|
||||
const direction = Math.sign(steeringAngle);
|
||||
const radius = MKMINI_GAZEBO_WHEELBASE_METERS / Math.tan(Math.abs(steeringAngle));
|
||||
const halfTrack = 0.293;
|
||||
const halfTrack = 0.255;
|
||||
const inner = Math.atan(MKMINI_GAZEBO_WHEELBASE_METERS / Math.max(0.01, radius - halfTrack));
|
||||
const outer = Math.atan(MKMINI_GAZEBO_WHEELBASE_METERS / (radius + halfTrack));
|
||||
return direction > 0 ? { left: inner, right: outer } : { left: -outer, right: -inner };
|
||||
@@ -1655,13 +1662,17 @@ function mkMiniAckermannAngles(steeringAngle) {
|
||||
|
||||
function updateMkMiniFrontWheelSteering() {
|
||||
if (!mkMiniFrontWheelPivots.length) return;
|
||||
currentMkMiniSteeringAngle += (targetMkMiniSteeringAngle - currentMkMiniSteeringAngle)
|
||||
* (hasSimulatorMkMiniSteering ? 1 : 0.28);
|
||||
const now = performance.now();
|
||||
const elapsedSeconds = lastMkMiniSteeringUpdateMs === null
|
||||
? 0
|
||||
: Math.min((now - lastMkMiniSteeringUpdateMs) / 1000, 0.1);
|
||||
lastMkMiniSteeringUpdateMs = now;
|
||||
const alpha = 1 - Math.exp(-8 * elapsedSeconds);
|
||||
currentMkMiniSteeringAngle += (targetMkMiniSteeringAngle - currentMkMiniSteeringAngle) * alpha;
|
||||
for (const { pivot, side } of mkMiniFrontWheelPivots) {
|
||||
// The asset is Z-up before its model-level X rotation, so its local Y axis
|
||||
// becomes the viewer's vertical steering axis.
|
||||
currentMkMiniSteeringAngles[side] += (targetMkMiniSteeringAngles[side] - currentMkMiniSteeringAngles[side])
|
||||
* (hasSimulatorMkMiniSteering ? 1 : 0.28);
|
||||
currentMkMiniSteeringAngles[side] += (targetMkMiniSteeringAngles[side] - currentMkMiniSteeringAngles[side]) * alpha;
|
||||
pivot.rotation.y = currentMkMiniSteeringAngles[side];
|
||||
}
|
||||
}
|
||||
@@ -1768,7 +1779,7 @@ function setNavigationSensorVisible(visible) {
|
||||
drawMiniMap(latestSlamState);
|
||||
return;
|
||||
}
|
||||
if (REAL_RUNTIME) {
|
||||
if (SHOW_SENSOR_POINT_CLOUD) {
|
||||
void refreshSensorPointCloud();
|
||||
if (!sensorPointCloudRefreshTimer) {
|
||||
sensorPointCloudRefreshTimer = window.setInterval(() => void refreshSensorPointCloud(), 500);
|
||||
@@ -1798,9 +1809,8 @@ function drawLaserScan(scan, scanPose = (
|
||||
const renderPose = USE_MKMINI_MODEL && scanPose
|
||||
? {
|
||||
...sourcePose,
|
||||
x: sourcePose.x + Math.cos(sourcePose.yaw || 0) * MKMINI_LASER_RENDER_FORWARD_OFFSET,
|
||||
y: sourcePose.y + Math.sin(sourcePose.yaw || 0) * MKMINI_LASER_RENDER_FORWARD_OFFSET,
|
||||
z: (sourcePose.z || 0) - MKMINI_LASER_RENDER_Z_OFFSET,
|
||||
x: sourcePose.x - Math.cos(sourcePose.yaw || 0) * MKMINI_LASER_RENDER_BACKWARD_OFFSET,
|
||||
y: sourcePose.y - Math.sin(sourcePose.yaw || 0) * MKMINI_LASER_RENDER_BACKWARD_OFFSET,
|
||||
}
|
||||
: sourcePose;
|
||||
const origin = scanPose
|
||||
@@ -1813,18 +1823,25 @@ function drawLaserScan(scan, scanPose = (
|
||||
|
||||
const endpoints = [];
|
||||
const linePoints = [];
|
||||
const openSpaceLinePoints = [];
|
||||
const showOpenSpaceRays = NAV2_VIEW_MODE === 'slam';
|
||||
for (let i = 0; i < scan.ranges.length; i++) {
|
||||
const raw = scan.ranges[i];
|
||||
const valid = Number.isFinite(raw) && raw >= rangeMin && raw < rangeMax;
|
||||
const d = valid ? Math.min(raw, rangeMax) : rangeMax;
|
||||
const a = angleMin + i * angleInc;
|
||||
const dir = laserDirectionFromPose(sourcePose, a);
|
||||
const dir = laserDirectionFromPose(renderPose, a);
|
||||
const startD = Math.min(LASER_HOLE_R, d);
|
||||
const start = origin.clone().addScaledVector(dir, startD);
|
||||
const point = origin.clone().addScaledVector(dir, d);
|
||||
endpoints.push({ start, point, hit: valid });
|
||||
// A range-max value represents open space, not an actual reflected beam.
|
||||
if (valid) linePoints.push(start.x, start.y, start.z, point.x, point.y, point.z);
|
||||
if (valid) {
|
||||
linePoints.push(start.x, start.y, start.z, point.x, point.y, point.z);
|
||||
} else if (showOpenSpaceRays) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
const fillPoints = [];
|
||||
@@ -1858,10 +1875,20 @@ function drawLaserScan(scan, scanPose = (
|
||||
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 }),
|
||||
));
|
||||
}
|
||||
|
||||
const frontIndex = Math.max(0, Math.min(scan.ranges.length - 1, Math.round((0 - angleMin) / angleInc)));
|
||||
const front = endpoints[frontIndex];
|
||||
if (front?.hit) {
|
||||
// Keep the forward direction beam even in open space. It is the visual
|
||||
// heading reference used in the TurtleBot SLAM view, not only a hit marker.
|
||||
if (front) {
|
||||
const delta = new THREE.Vector3(
|
||||
front.point.x - front.start.x,
|
||||
front.point.y - front.start.y,
|
||||
@@ -1980,7 +2007,7 @@ async function refreshPointCloud() {
|
||||
}
|
||||
|
||||
async function refreshSensorPointCloud() {
|
||||
if (!REAL_RUNTIME || !navigationSensorVisible || sensorPointCloudRequestInFlight) return;
|
||||
if (!SHOW_SENSOR_POINT_CLOUD || !navigationSensorVisible || sensorPointCloudRequestInFlight) return;
|
||||
sensorPointCloudRequestInFlight = true;
|
||||
try {
|
||||
const response = await fetch(`${BACKEND_URL}/sensor-point-cloud`);
|
||||
@@ -3188,7 +3215,7 @@ if (SHOW_POINT_CLOUD) {
|
||||
void refreshPointCloud();
|
||||
pointCloudRefreshTimer = window.setInterval(() => void refreshPointCloud(), 1000);
|
||||
}
|
||||
if (REAL_RUNTIME && navigationSensorVisible) {
|
||||
if (SHOW_SENSOR_POINT_CLOUD && navigationSensorVisible) {
|
||||
void refreshSensorPointCloud();
|
||||
sensorPointCloudRefreshTimer = window.setInterval(() => void refreshSensorPointCloud(), 500);
|
||||
}
|
||||
@@ -3223,7 +3250,7 @@ let singleMoveStopTimer = null;
|
||||
const TELEOP_COMMAND_INTERVAL_MS = 120;
|
||||
const SINGLE_MOVE_DURATION_MS = 120;
|
||||
const LATCHED_ROTATION_SPEED_SCALE = 0.25;
|
||||
const MKMINI_GAZEBO_WHEELBASE_METERS = 0.489;
|
||||
const MKMINI_GAZEBO_WHEELBASE_METERS = 0.6;
|
||||
const MKMINI_GAZEBO_MAX_STEERING_RADIANS = 0.5;
|
||||
let cmdVelRequestInFlight = false;
|
||||
let queuedCmdVel = null;
|
||||
@@ -3446,6 +3473,7 @@ window.addEventListener('resize', () => {
|
||||
function animate(){
|
||||
requestAnimationFrame(animate);
|
||||
updateSmoothedRobotPose();
|
||||
updateMkMiniFrontWheelSteering();
|
||||
controls.update();
|
||||
if (remoteNavigationSplatViewer && remoteNavigationSplatEnabled) {
|
||||
remoteNavigationSplatViewer.update();
|
||||
|
||||
Reference in New Issue
Block a user