fix: enable added week deletion and preserve learning concepts when cloning courses
This commit is contained in:
@@ -48,11 +48,11 @@
|
||||
}
|
||||
#cameraPreviewWrap {
|
||||
position:absolute;
|
||||
right:16px;
|
||||
bottom:256px;
|
||||
left:16px;
|
||||
top:48px;
|
||||
z-index:10;
|
||||
display:none;
|
||||
width:260px;
|
||||
width:min(260px, calc(100vw - 32px));
|
||||
border:1px solid rgba(0,0,0,.45);
|
||||
background:#111520;
|
||||
box-shadow:0 10px 26px rgba(0,0,0,.16);
|
||||
@@ -61,16 +61,16 @@
|
||||
#cameraPreviewWrap.is-collapsed { background:transparent; border-color:transparent; box-shadow:none; }
|
||||
#cameraPreview {
|
||||
display:block;
|
||||
width:260px;
|
||||
width:100%;
|
||||
aspect-ratio:16 / 9;
|
||||
object-fit:cover;
|
||||
object-fit:contain;
|
||||
background:#111520;
|
||||
}
|
||||
#cameraPreviewWrap.is-collapsed #cameraPreview { display:none; }
|
||||
#cameraPreviewToggle {
|
||||
position:absolute;
|
||||
right:0;
|
||||
top:-28px;
|
||||
top:100%;
|
||||
display:grid;
|
||||
width:30px;
|
||||
height:28px;
|
||||
@@ -86,8 +86,8 @@
|
||||
box-shadow:0 10px 28px rgba(0,0,0,.16);
|
||||
}
|
||||
#cameraPreviewToggle:hover { color:#fff; background:rgba(255,255,255,.06); }
|
||||
#cameraPreviewChevron { font-size:13px; line-height:1; }
|
||||
#cameraPreviewWrap.is-collapsed #cameraPreviewChevron { transform:rotate(180deg); }
|
||||
#cameraPreviewChevron { font-size:13px; line-height:1; transform:rotate(180deg); }
|
||||
#cameraPreviewWrap.is-collapsed #cameraPreviewChevron { transform:rotate(0deg); }
|
||||
#topRightControls {
|
||||
position:absolute;
|
||||
right:16px;
|
||||
@@ -369,6 +369,8 @@
|
||||
.nav2-navigation #rosTopicStatusWrap { bottom:16px; }
|
||||
.real-runtime #topLeftControls { display:none; }
|
||||
.real-runtime #keyGuide { display:none; }
|
||||
.real-yahboom-teleop:not(.editor-mode) #keyGuide { display:block; }
|
||||
.real-yahboom-teleop #rosTopicStatusWrap { bottom:72px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -441,12 +443,12 @@ const REAL_RUNTIME = params.get('runtime') === 'real';
|
||||
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 USE_YAHBOOM_MODEL = ROBOT_PLATFORM === 'yahboom';
|
||||
const REAL_YAHBOOM_TELEOP = REAL_RUNTIME && USE_YAHBOOM_MODEL;
|
||||
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`.
|
||||
const ENABLE_CAMERA_PREVIEW = false;
|
||||
const ENABLE_CAMERA_PREVIEW = params.get('editor') !== '1';
|
||||
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';
|
||||
@@ -493,6 +495,12 @@ const rosTopicStatusList = document.getElementById('rosTopicStatusList');
|
||||
if (EDITOR_MODE) document.body.classList.add('editor-mode');
|
||||
if (!EDITOR_MODE) document.body.classList.add(`nav2-${NAV2_VIEW_MODE}`);
|
||||
if (REAL_RUNTIME) document.body.classList.add('real-runtime');
|
||||
if (REAL_YAHBOOM_TELEOP && !EDITOR_MODE) {
|
||||
document.body.classList.add('real-yahboom-teleop');
|
||||
const hint = document.createElement('div');
|
||||
hint.textContent = '키를 누르는 동안 이동 · 놓으면 정지';
|
||||
document.getElementById('keyGuide')?.appendChild(hint);
|
||||
}
|
||||
if (EDITOR_MODE) responseWaitOverlay?.classList.add('is-hidden');
|
||||
|
||||
let visualizationResponseReceived = EDITOR_MODE;
|
||||
@@ -868,6 +876,7 @@ let laserFrontBeam = null;
|
||||
let slamSocket = null;
|
||||
let controlSocket = null;
|
||||
let latestSlamState = null;
|
||||
let latestSlamStateReceivedAt = 0;
|
||||
let currentRobotPose = { x:-2.0, y:-0.5, z:0.01, roll:0, pitch:0, yaw:0 };
|
||||
let targetRobotPose = { ...currentRobotPose };
|
||||
let hasRobotPose = false;
|
||||
@@ -1385,6 +1394,42 @@ async function loadMkMiniModel(sdfPose = { x:0, y:0, z:0, roll:0, pitch:0, yaw:0
|
||||
sdfRoot.add(posePreviewModel);
|
||||
}
|
||||
|
||||
async function loadYahboomModel(sdfPose) {
|
||||
const gltf = await new GLTFLoader().loadAsync('./assets/yahboom-pi5.glb');
|
||||
const robotGroup = new THREE.Group();
|
||||
robotGroup.name = 'yahboom';
|
||||
applySdfPose(robotGroup, sdfPose);
|
||||
// GLB is meter-scaled Y-up; the viewer's SDF root expects ROS Z-up.
|
||||
// The exported origin is already base_footprint, so do not recenter it.
|
||||
gltf.scene.rotation.x = Math.PI / 2;
|
||||
gltf.scene.traverse((child) => {
|
||||
if (!child.isMesh) return;
|
||||
child.castShadow = true;
|
||||
// The world-scale shadow map causes self-shadow acne on small CAD parts.
|
||||
child.receiveShadow = false;
|
||||
});
|
||||
robotGroup.add(gltf.scene);
|
||||
sdfRoot.add(robotGroup);
|
||||
robotModel = robotGroup;
|
||||
robotModel.visible = shouldRenderRobot();
|
||||
posePreviewModel = robotGroup.clone(true);
|
||||
posePreviewModel.name = 'yahboom_pose_preview';
|
||||
posePreviewModel.traverse((child) => {
|
||||
if (!child.isMesh) return;
|
||||
child.castShadow = false;
|
||||
child.material = Array.isArray(child.material)
|
||||
? child.material.map((material) => material.clone()) : child.material.clone();
|
||||
const materials = Array.isArray(child.material) ? child.material : [child.material];
|
||||
for (const material of materials) {
|
||||
material.transparent = true;
|
||||
material.opacity = 0.38;
|
||||
material.depthWrite = false;
|
||||
}
|
||||
});
|
||||
posePreviewModel.visible = false;
|
||||
sdfRoot.add(posePreviewModel);
|
||||
}
|
||||
|
||||
function createMkMiniFrontWheelPivots(model) {
|
||||
const pivots = [];
|
||||
model.updateMatrixWorld(true);
|
||||
@@ -1411,10 +1456,10 @@ 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_X = USE_MKMINI_MODEL ? 0.15 : USE_YAHBOOM_MODEL ? 0 : -0.032;
|
||||
const LASER_OFFSET_Y = 0;
|
||||
// Fallback height when an explicit ROS scan pose is unavailable.
|
||||
const LASER_Z = USE_MKMINI_MODEL ? 0.5 : 0.181;
|
||||
const LASER_Z = USE_MKMINI_MODEL ? 0.5 : USE_YAHBOOM_MODEL ? 0.124183557 : 0.181;
|
||||
const MKMINI_LASER_RENDER_BACKWARD_OFFSET = 0.03;
|
||||
|
||||
function setRobotPose(pose) {
|
||||
@@ -2010,10 +2055,10 @@ function setCameraPreviewAvailable(available) {
|
||||
}
|
||||
|
||||
async function refreshCameraFrame() {
|
||||
if (!ENABLE_CAMERA_PREVIEW || !REAL_RUNTIME || cameraFrameRequestInFlight) return;
|
||||
if (!ENABLE_CAMERA_PREVIEW || cameraFrameRequestInFlight || document.hidden) return;
|
||||
cameraFrameRequestInFlight = true;
|
||||
try {
|
||||
const response = await fetch(`${BACKEND_URL}/camera-frame`, { cache: 'no-store' });
|
||||
const response = await fetch(`${BACKEND_URL}/camera-frame`, { cache: 'no-store', signal: AbortSignal.timeout(2000) });
|
||||
if (!response.ok) {
|
||||
setCameraPreviewAvailable(false);
|
||||
return;
|
||||
@@ -2023,7 +2068,6 @@ async function refreshCameraFrame() {
|
||||
cameraFrameObjectUrl = nextUrl;
|
||||
if (cameraPreview) cameraPreview.src = nextUrl;
|
||||
if (previousUrl) URL.revokeObjectURL(previousUrl);
|
||||
setCameraPreviewAvailable(true);
|
||||
} catch (_) {
|
||||
setCameraPreviewAvailable(false);
|
||||
} finally {
|
||||
@@ -2031,6 +2075,9 @@ async function refreshCameraFrame() {
|
||||
}
|
||||
}
|
||||
|
||||
cameraPreview?.addEventListener('load', () => setCameraPreviewAvailable(true));
|
||||
cameraPreview?.addEventListener('error', () => setCameraPreviewAvailable(false));
|
||||
|
||||
function drawMiniMap(state) {
|
||||
pendingMiniMapState = state;
|
||||
if (miniMapRenderTimer) return;
|
||||
@@ -3063,6 +3110,7 @@ renderer.domElement.addEventListener('click', async (event) => {
|
||||
|
||||
function applySlamState(state) {
|
||||
latestSlamState = state;
|
||||
latestSlamStateReceivedAt = Date.now();
|
||||
resetNavigationPoseForNewSession(state);
|
||||
restoreNavigationPoseFromState(state);
|
||||
updateNavigationLocalizationState(state);
|
||||
@@ -3155,7 +3203,7 @@ if (SHOW_SENSOR_POINT_CLOUD && navigationSensorVisible) {
|
||||
void refreshSensorPointCloud();
|
||||
sensorPointCloudRefreshTimer = window.setInterval(() => void refreshSensorPointCloud(), 750);
|
||||
}
|
||||
if (ENABLE_CAMERA_PREVIEW && REAL_RUNTIME) {
|
||||
if (ENABLE_CAMERA_PREVIEW) {
|
||||
void refreshCameraFrame();
|
||||
cameraFrameRefreshTimer = window.setInterval(() => void refreshCameraFrame(), 200);
|
||||
}
|
||||
@@ -3172,6 +3220,10 @@ function connectControlSocket() {
|
||||
if (!CONTROL_WS) return;
|
||||
controlSocket = new WebSocket(CONTROL_WS);
|
||||
controlSocket.addEventListener('close', () => {
|
||||
if (REAL_YAHBOOM_TELEOP) {
|
||||
activeMoveKey = null;
|
||||
stopRobot();
|
||||
}
|
||||
setTimeout(connectControlSocket, 1000);
|
||||
});
|
||||
controlSocket.addEventListener('error', () => {
|
||||
@@ -3179,8 +3231,8 @@ function connectControlSocket() {
|
||||
});
|
||||
}
|
||||
|
||||
let maxLinear = 0.22;
|
||||
let maxAngular = 2.84;
|
||||
let maxLinear = REAL_YAHBOOM_TELEOP ? 0.1 : 0.22;
|
||||
let maxAngular = REAL_YAHBOOM_TELEOP ? 0.5 : 2.84;
|
||||
let activeMoveKey = null;
|
||||
let teleopInterval = null;
|
||||
let singleMoveStopTimer = null;
|
||||
@@ -3204,12 +3256,19 @@ const speedBindings = {
|
||||
};
|
||||
|
||||
function canUseTeleop(event) {
|
||||
if (REAL_RUNTIME) return false;
|
||||
if (REAL_RUNTIME && (!REAL_YAHBOOM_TELEOP || !realTeleopReady())) return false;
|
||||
if (!document.hasFocus()) return false;
|
||||
if (document.hidden || event.ctrlKey || event.altKey || event.metaKey || event.target?.isContentEditable) return false;
|
||||
const tag = event.target?.tagName?.toLowerCase();
|
||||
return !['input', 'textarea', 'select', 'button'].includes(tag);
|
||||
}
|
||||
|
||||
function realTeleopReady() {
|
||||
return latestSlamState?.teleop_deadman === true
|
||||
&& Date.now() - latestSlamStateReceivedAt < 1000
|
||||
&& controlSocket?.readyState === WebSocket.OPEN;
|
||||
}
|
||||
|
||||
function flushQueuedCmdVel() {
|
||||
if (cmdVelRequestInFlight || !queuedCmdVel) return;
|
||||
const command = queuedCmdVel;
|
||||
@@ -3236,6 +3295,20 @@ function flushQueuedCmdVel() {
|
||||
|
||||
function sendCmdVel(linear, angular) {
|
||||
const payload = { type:'cmd_vel', linear, angular };
|
||||
if (REAL_YAHBOOM_TELEOP) {
|
||||
// Never queue physical motion for replay after a connection recovers.
|
||||
if (!controlSocket || controlSocket.readyState !== WebSocket.OPEN) return;
|
||||
if ((linear || angular) && !realTeleopReady()) return;
|
||||
payload.deadman = true;
|
||||
payload.linear = Math.max(-0.1, Math.min(0.1, linear));
|
||||
payload.angular = Math.max(-0.5, Math.min(0.5, angular));
|
||||
if (controlSocket.bufferedAmount > 0) {
|
||||
controlSocket.close();
|
||||
return;
|
||||
}
|
||||
controlSocket.send(JSON.stringify(payload));
|
||||
return;
|
||||
}
|
||||
if (controlSocket?.readyState === WebSocket.OPEN) {
|
||||
controlSocket.send(JSON.stringify(payload));
|
||||
return;
|
||||
@@ -3289,6 +3362,11 @@ function stopRobot() {
|
||||
|
||||
function sendActiveMoveCommand() {
|
||||
if (!activeMoveKey) return;
|
||||
if (REAL_YAHBOOM_TELEOP && (!realTeleopReady() || document.hidden || !document.hasFocus())) {
|
||||
activeMoveKey = null;
|
||||
stopRobot();
|
||||
return;
|
||||
}
|
||||
const [linearSign, angularSign] = moveBindings[activeMoveKey];
|
||||
if (linearSign === 0 && angularSign === 0) return;
|
||||
|
||||
@@ -3340,6 +3418,7 @@ window.addEventListener('keydown', (event) => {
|
||||
|
||||
if (speedBindings[key]) {
|
||||
event.preventDefault();
|
||||
if (REAL_YAHBOOM_TELEOP) return;
|
||||
const [linearScale, angularScale] = speedBindings[key];
|
||||
maxLinear = Math.max(0.01, maxLinear * linearScale);
|
||||
maxAngular = Math.max(0.1, maxAngular * angularScale);
|
||||
@@ -3355,6 +3434,11 @@ window.addEventListener('keydown', (event) => {
|
||||
stopRobot();
|
||||
return;
|
||||
}
|
||||
if (REAL_YAHBOOM_TELEOP) {
|
||||
activeMoveKey = key;
|
||||
startActiveMoveCommand();
|
||||
return;
|
||||
}
|
||||
if (USE_MKMINI_MODEL && !REAL_RUNTIME && (key === 'j' || key === 'l')) {
|
||||
activeMoveKey = key;
|
||||
sendActiveMoveCommand();
|
||||
@@ -3370,6 +3454,11 @@ window.addEventListener('keydown', (event) => {
|
||||
});
|
||||
|
||||
window.addEventListener('keyup', (event) => {
|
||||
if (REAL_YAHBOOM_TELEOP && event.key.toLowerCase() === activeMoveKey) {
|
||||
activeMoveKey = null;
|
||||
stopRobot();
|
||||
return;
|
||||
}
|
||||
if (!canUseTeleop(event)) return;
|
||||
const key = event.key.toLowerCase();
|
||||
if (moveBindings[key]) event.preventDefault();
|
||||
@@ -3381,6 +3470,14 @@ window.addEventListener('blur', () => {
|
||||
stopRobot();
|
||||
});
|
||||
|
||||
function releaseRealTeleop() {
|
||||
if (!REAL_YAHBOOM_TELEOP || !activeMoveKey) return;
|
||||
activeMoveKey = null;
|
||||
stopRobot();
|
||||
}
|
||||
document.addEventListener('visibilitychange', () => { if (document.hidden) releaseRealTeleop(); });
|
||||
window.addEventListener('pagehide', releaseRealTeleop);
|
||||
|
||||
// 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 || SAVED_GAZEBO_MAP_WORLD || REAL_RUNTIME
|
||||
@@ -3389,7 +3486,7 @@ const worldSceneTasks = CUSTOM_WORLD || SAVED_GAZEBO_MAP_WORLD || REAL_RUNTIME
|
||||
const remoteNavigationSceneTasks = [];
|
||||
Promise.all(EDITOR_MODE
|
||||
? worldSceneTasks
|
||||
: [...worldSceneTasks, ...remoteNavigationSceneTasks, (USE_MKMINI_MODEL ? loadMkMiniModel : loadBurgerModel)({ ...DEFAULT_SIMULATION_SPAWN, z:0.01, roll:0, pitch:0, yaw:0 })]
|
||||
: [...worldSceneTasks, ...remoteNavigationSceneTasks, (USE_YAHBOOM_MODEL ? loadYahboomModel : USE_MKMINI_MODEL ? loadMkMiniModel : loadBurgerModel)({ ...DEFAULT_SIMULATION_SPAWN, z:0.01, roll:0, pitch:0, yaw:0 })]
|
||||
).then(() => {
|
||||
if (EDITOR_MODE) {
|
||||
window.parent.postMessage({ source:'autonomy-world-editor', type:'ready' }, '*');
|
||||
|
||||
Reference in New Issue
Block a user