diff --git a/.gitignore b/.gitignore index 442d997..a4ac77e 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,10 @@ build/ # Runtime data (SQLite + media) +maps/ +RBS.db +RBS.db-wal +RBS.db-shm data/RBS.db data/RBS.db-wal data/RBS.db-shm diff --git a/maps/6f761198ec8414ef/map.png b/maps/6f761198ec8414ef/map.png deleted file mode 100644 index 2893724..0000000 Binary files a/maps/6f761198ec8414ef/map.png and /dev/null differ diff --git a/maps/6f761198ec8414ef/map.yaml b/maps/6f761198ec8414ef/map.yaml deleted file mode 100644 index 7cdd3c3..0000000 --- a/maps/6f761198ec8414ef/map.yaml +++ /dev/null @@ -1,6 +0,0 @@ -image: Denso_1.png -resolution: 0.050000 -origin: [-12.238091, -13.200000, 0.0] -negate: 0 -occupied_thresh: 0.65 -free_thresh: 0.196 diff --git a/maps/75500f988b7f4f23/map.png b/maps/75500f988b7f4f23/map.png deleted file mode 100644 index 2893724..0000000 Binary files a/maps/75500f988b7f4f23/map.png and /dev/null differ diff --git a/maps/75500f988b7f4f23/map.yaml b/maps/75500f988b7f4f23/map.yaml deleted file mode 100644 index 7cdd3c3..0000000 --- a/maps/75500f988b7f4f23/map.yaml +++ /dev/null @@ -1,6 +0,0 @@ -image: Denso_1.png -resolution: 0.050000 -origin: [-12.238091, -13.200000, 0.0] -negate: 0 -occupied_thresh: 0.65 -free_thresh: 0.196 diff --git a/src/app/lidar_manager_app.cpp b/src/app/lidar_manager_app.cpp index d1f1e0e..83e7fe2 100644 --- a/src/app/lidar_manager_app.cpp +++ b/src/app/lidar_manager_app.cpp @@ -21,6 +21,24 @@ namespace lm { +namespace { + +bool isProjectRootDataPath(const std::filesystem::path& parent) +{ + return parent.empty() || parent == std::filesystem::path("."); +} + +std::filesystem::path resolveDataPath(std::filesystem::path data_path) +{ + if (!isProjectRootDataPath(data_path.parent_path())) + return data_path; + if (data_path.filename() == "state.json") + return std::filesystem::path("data") / "state.json"; + return std::filesystem::path("data") / "RBS.db"; +} + +} // namespace + LidarManagerApp::LidarManagerApp(int port, std::filesystem::path www_root, std::filesystem::path data_path) @@ -30,6 +48,7 @@ LidarManagerApp::LidarManagerApp(int port, int LidarManagerApp::run() { + data_path_ = resolveDataPath(data_path_); const std::filesystem::path data_dir = data_path_.parent_path(); Database database(data_dir); std::string db_err; diff --git a/src/storage/database.cpp b/src/storage/database.cpp index a7a37c7..3ab66b9 100644 --- a/src/storage/database.cpp +++ b/src/storage/database.cpp @@ -193,6 +193,48 @@ bool Database::ensureDataDirs(std::string& err) return true; } +bool Database::migrateLegacyMapsDir(std::string& err) +{ + if (data_dir_.filename() != "data") + return true; + + const std::filesystem::path legacy = "maps"; + const auto target = mapsDir(); + if (!std::filesystem::is_directory(legacy) || legacy == target) + return true; + + std::error_code ec; + std::filesystem::create_directories(target, ec); + if (ec) + { + err = "failed to create maps directory: " + target.string(); + return false; + } + + for (const auto& entry : std::filesystem::directory_iterator(legacy, ec)) + { + if (ec || !entry.is_directory()) + continue; + const auto dest = target / entry.path().filename(); + if (std::filesystem::exists(dest)) + continue; + std::filesystem::rename(entry.path(), dest, ec); + if (ec) + { + ec.clear(); + std::filesystem::copy(entry.path(), dest, std::filesystem::copy_options::recursive, ec); + if (ec) + { + err = "failed to migrate map directory: " + entry.path().string(); + return false; + } + std::filesystem::remove_all(entry.path(), ec); + ec.clear(); + } + } + return true; +} + bool Database::applySchema(std::string& err) { return execSql(db_, kSchemaSql, err); @@ -494,6 +536,8 @@ bool Database::init(std::string& err) return false; if (!ensureDataDirs(err)) return false; + if (!migrateLegacyMapsDir(err)) + return false; if (!migrateFromJsonIfNeeded(err)) return false; if (!getMeta("schema_version")) diff --git a/src/storage/database.hpp b/src/storage/database.hpp index 075552b..d3219f9 100644 --- a/src/storage/database.hpp +++ b/src/storage/database.hpp @@ -45,6 +45,7 @@ private: bool applySchemaMigrations(std::string& err); bool migrateFromJsonIfNeeded(std::string& err); bool ensureDataDirs(std::string& err); + bool migrateLegacyMapsDir(std::string& err); std::optional getMeta(const std::string& key) const; bool setMeta(const std::string& key, const std::string& value); }; diff --git a/www/i18n.js b/www/i18n.js index b9e073a..0b1883b 100644 --- a/www/i18n.js +++ b/www/i18n.js @@ -389,6 +389,7 @@ "maps.editor.tool.save": "Lưu map", "maps.editor.tool.pan": "Pan — di chuyển vùng nhìn", "maps.editor.tool.crosshair": "Crosshair", + "maps.editor.tool.origin": "Hiển thị gốc tọa độ", "maps.editor.tool.center": "Căn giữa vùng nhìn", "maps.editor.tool.lidar": "Hiển thị LiDAR", "maps.editor.tool.waypoints": "Vị trí / waypoint", @@ -882,7 +883,7 @@ "maps.uploadConfirm.title": "Overwrite floor plan?", "maps.uploadConfirm.text": "The current map image will be replaced. Continue?", "maps.uploadConfirm.yes": "Overwrite", - "maps.uploadMeta.title": "Map metadata (ROS)", + "maps.uploadMeta.title": "Map metadata", "maps.uploadMeta.hint": "Enter origin, resolution, and occupancy thresholds — or import a .yaml file.", "maps.uploadMeta.importYaml": "Import YAML file…", "maps.uploadMeta.negate": "Negate", @@ -908,6 +909,7 @@ "maps.editor.tool.save": "Save map", "maps.editor.tool.pan": "Pan — move view", "maps.editor.tool.crosshair": "Crosshair", + "maps.editor.tool.origin": "Show map origin", "maps.editor.tool.center": "Center view", "maps.editor.tool.lidar": "LiDAR overlay", "maps.editor.tool.waypoints": "Positions", diff --git a/www/index.html b/www/index.html index 18d4c28..fd54c09 100644 --- a/www/index.html +++ b/www/index.html @@ -1016,6 +1016,9 @@ + @@ -1045,12 +1048,14 @@
- + +
@@ -1723,6 +1728,7 @@ GET /api/v2.0.0/status + diff --git a/www/map-editor.js b/www/map-editor.js index b4417e2..2993c03 100644 --- a/www/map-editor.js +++ b/www/map-editor.js @@ -2,6 +2,7 @@ const el = (id) => document.getElementById(id); const t = (key, vars) => window.I18n?.t(key, vars) ?? key; const Geo = () => window.MapGeo; + const Occ = () => window.MapOccupancyCanvas; const state = { mapId: null, @@ -14,6 +15,9 @@ view: Geo()?.createView(1, 0, 0) || { scale: 1, panX: 0, panY: 0 }, panning: null, tipVisible: true, + showOrigin: true, + /** ROS yaml thresholds (occupied_thresh, free_thresh, negate) from map.yaml. */ + yamlMeta: null, /** Pending ROS metadata from upload dialog (set before PNG picker). */ uploadMeta: null, }; @@ -27,7 +31,10 @@ const sheetEl = el("mapEditorSheet"); const gridEl = el("mapEditorSheetGrid"); const imageEl = el("mapEditorImage"); + const occupancyCanvasEl = el("mapEditorOccupancyCanvas"); const originEl = el("mapEditorOrigin"); + const originHitEl = el("mapEditorOriginHit"); + const originLabelEl = el("mapEditorOriginLabel"); const emptyEl = el("mapEditorEmpty"); const tipEl = el("mapEditorCanvasTip"); const statusViewEl = el("mapEditorStatusView"); @@ -94,7 +101,58 @@ } function hasFloorPlan() { - return !!(state.map?.image_file && imageEl && !imageEl.hidden && imageEl.naturalWidth); + return !!( + state.map?.image_file && + imageEl && + !imageEl.hidden && + imageEl.naturalWidth && + occupancyCanvasEl && + !occupancyCanvasEl.hidden && + occupancyCanvasEl.width + ); + } + + /** ROS yaml thresholds for occupancy coloring. */ + function mapRenderMeta() { + const base = mapMetaForOriginDisplay() || state.map || {}; + const yaml = state.yamlMeta || {}; + return { + occupied_thresh: + base.occupied_thresh != null ? base.occupied_thresh : yaml.occupied_thresh, + free_thresh: base.free_thresh != null ? base.free_thresh : yaml.free_thresh, + negate: base.negate != null ? base.negate : yaml.negate, + }; + } + + async function loadYamlMeta() { + state.yamlMeta = await fetchExistingYamlMeta(); + } + + function setOccupancyCanvasVisible(visible) { + if (!occupancyCanvasEl) return; + occupancyCanvasEl.hidden = !visible; + occupancyCanvasEl.setAttribute("aria-hidden", visible ? "false" : "true"); + } + + /** Paint RViz-style occupancy colors from loaded PNG (hidden loader img). */ + function paintOccupancyFromImage() { + const occ = Occ(); + if (!occ || !occupancyCanvasEl || !imageEl?.naturalWidth) return false; + const ok = occ.renderFromImage(occupancyCanvasEl, imageEl, mapRenderMeta()); + setOccupancyCanvasVisible(ok); + return ok; + } + + /** + * Paint live occupancy grid (record/stream — roadmap step 2+). + * @param {{ width: number, height: number, data: number[]|Int8Array|string }} grid + */ + function paintOccupancyGrid(grid) { + const occ = Occ(); + if (!occ || !occupancyCanvasEl || !grid) return false; + const ok = occ.renderGrid(occupancyCanvasEl, grid); + if (ok) setOccupancyCanvasVisible(true); + return ok; } function setDirty(flag) { @@ -204,7 +262,6 @@ gridEl.hidden = true; } - if (originEl) originEl.hidden = !has; updateOriginMarker(); updateStatusBar(); } @@ -224,11 +281,25 @@ return base; } + function setShowOrigin(show) { + state.showOrigin = !!show; + const btn = el("mapEditorOriginBtn"); + btn?.classList.toggle("is-active", state.showOrigin); + btn?.setAttribute("aria-pressed", state.showOrigin ? "true" : "false"); + updateOriginMarker(); + } + + function setOriginLabelVisible(visible) { + if (!originEl) return; + originEl.classList.toggle("mapEditorOrigin--showLabel", !!visible); + if (originLabelEl) originLabelEl.setAttribute("aria-hidden", visible ? "false" : "true"); + } + function updateOriginMarker() { if (!originEl) return; const geo = Geo(); const { width, height } = floorPlanSize(); - if (!geo || !hasFloorPlan() || !width || !height) { + if (!state.showOrigin || !geo || !hasFloorPlan() || !width || !height) { originEl.hidden = true; originEl.setAttribute("aria-hidden", "true"); return; @@ -249,18 +320,23 @@ originEl.style.top = `${pt.y}px`; originEl.style.transform = `rotate(${yawDeg}deg)`; - const labelEl = el("mapEditorOriginLabel"); - if (labelEl) { - labelEl.textContent = t("maps.editor.originLabelShort", { + if (originLabelEl) { + originLabelEl.textContent = t("maps.editor.originLabelShort", { x: ox.toFixed(2), y: oy.toFixed(2), }); + originLabelEl.setAttribute("aria-hidden", "true"); } - originEl.title = t("maps.editor.originTooltip", { + const tooltip = t("maps.editor.originTooltip", { x: ox.toFixed(3), y: oy.toFixed(3), yaw: ((oyaw * 180) / Math.PI).toFixed(1), }); + if (originHitEl) { + originHitEl.title = tooltip; + originHitEl.setAttribute("aria-label", tooltip); + } + setOriginLabelVisible(false); } function updateStatusBar(pointerClient) { @@ -311,11 +387,13 @@ imageEl.src = url; imageEl.hidden = false; if (emptyEl) emptyEl.hidden = true; + setOccupancyCanvasVisible(false); } else { if (imageEl) { imageEl.hidden = true; imageEl.removeAttribute("src"); } + setOccupancyCanvasVisible(false); if (emptyEl) emptyEl.hidden = false; } updateMenuActionsUi(); @@ -323,6 +401,7 @@ imageEl?.addEventListener( "load", () => { + paintOccupancyFromImage(); updateImageLayer(); fitToView(); }, @@ -381,6 +460,7 @@ async function reloadMap() { if (!state.mapId) return; state.map = await api(`/api/maps/${encodeURIComponent(state.mapId)}`); + await loadYamlMeta(); updateHeader(); renderMapImage(); fillSettingsForm(); @@ -392,6 +472,7 @@ state.readOnly = !!callbacks.readOnly || !callbacks.canWrite; state.dirty = false; state.tipVisible = true; + state.showOrigin = true; state.activeTool = "pan"; state.view = Geo()?.createView(1, 0, 0) || { scale: 1, panX: 0, panY: 0 }; if (tipEl) { @@ -399,6 +480,7 @@ tipEl.textContent = t("maps.editor.canvasTip"); } setActiveTool("pan"); + setShowOrigin(true); setDirty(false); applyReadOnlyUi(); reloadMap().catch((e) => alert(e.message)); @@ -409,6 +491,7 @@ state.map = null; state.callbacks = {}; state.uploadMeta = null; + state.yamlMeta = null; menuDialogEl?.close(); settingsDialogEl?.close(); activateDialogEl?.close(); @@ -584,6 +667,7 @@ state.callbacks.onMapUpdated?.(updated); setDirty(false); fillSettingsForm(); + await loadYamlMeta(); renderMapImage(); menuDialogEl?.close(); uploadMetaDialogEl?.close(); @@ -689,6 +773,11 @@ } function bindEvents() { + originHitEl?.addEventListener("mouseenter", () => setOriginLabelVisible(true)); + originHitEl?.addEventListener("mouseleave", () => setOriginLabelVisible(false)); + originHitEl?.addEventListener("focus", () => setOriginLabelVisible(true)); + originHitEl?.addEventListener("blur", () => setOriginLabelVisible(false)); + el("mapEditorBackBtn")?.addEventListener("click", () => { if (state.dirty && !confirm(t("maps.editor.unsavedLeave"))) return; state.callbacks.onClose?.(); @@ -714,6 +803,7 @@ saveMap().catch((e) => alert(e.message)); }); el("mapEditorPanBtn")?.addEventListener("click", () => setActiveTool("pan")); + el("mapEditorOriginBtn")?.addEventListener("click", () => setShowOrigin(!state.showOrigin)); el("mapEditorFitBtn")?.addEventListener("click", fitToView); el("mapEditorCenterBtn")?.addEventListener("click", () => { dismissCanvasTip(); @@ -818,6 +908,9 @@ Object.values(uploadMetaFields).forEach((node) => { node?.addEventListener("input", () => { updateOriginMarker(); + if (uploadMetaDialogEl?.open && imageEl?.naturalWidth) { + paintOccupancyFromImage(); + } if (node === uploadMetaFields.resolution && uploadMetaDialogEl?.open) { updateImageLayer(); } @@ -835,5 +928,11 @@ bindCanvasPanZoom(); bindEvents(); - window.MapEditorApp = { open, close, reloadMap }; + window.MapEditorApp = { + open, + close, + reloadMap, + paintOccupancyGrid, + paintOccupancyFromImage, + }; })(); diff --git a/www/map-occupancy-canvas.js b/www/map-occupancy-canvas.js new file mode 100644 index 0000000..34d730d --- /dev/null +++ b/www/map-occupancy-canvas.js @@ -0,0 +1,178 @@ +(() => { + /** + * RViz-style occupancy grid renderer for map view (Canvas 2D). + * + * ROS map_server / nav_msgs/OccupancyGrid: + * 0 = free + * 100 = occupied + * -1 = unknown + * + * Grid data is row-major with index 0 at the bottom-left cell (world +Y up). + * Canvas pixels use top-left origin — Y is flipped when painting. + */ + + const DEFAULT_PALETTE = { + free: [254, 254, 254, 255], + occupied: [0, 0, 0, 255], + unknown: [180, 180, 180, 255], + }; + + const DEFAULT_THRESHOLDS = { + occupied_thresh: 0.65, + free_thresh: 0.196, + negate: 0, + }; + + function metaThresholds(meta) { + return { + occupied_thresh: Number(meta?.occupied_thresh ?? DEFAULT_THRESHOLDS.occupied_thresh), + free_thresh: Number(meta?.free_thresh ?? DEFAULT_THRESHOLDS.free_thresh), + negate: Number(meta?.negate ?? DEFAULT_THRESHOLDS.negate), + }; + } + + /** Trinary mode (Nav2/map_server): darkness or lightness vs thresholds in [0, 1]. */ + function grayToOccValue(gray, meta) { + const { occupied_thresh, free_thresh, negate } = metaThresholds(meta); + const lightness = gray / 255; + const probability = negate ? lightness : 1 - lightness; + if (probability > occupied_thresh) return 100; + if (probability < free_thresh) return 0; + return -1; + } + + function occToRgba(value, palette) { + if (value < 0) return palette.unknown; + if (value >= 100) return palette.occupied; + if (value === 0) return palette.free; + return value > 50 ? palette.occupied : palette.free; + } + + function paletteFrom(opts) { + const p = opts?.palette || {}; + return { + free: p.free || DEFAULT_PALETTE.free, + occupied: p.occupied || DEFAULT_PALETTE.occupied, + unknown: p.unknown || DEFAULT_PALETTE.unknown, + }; + } + + function ensureCanvasSize(canvas, width, height) { + if (canvas.width !== width || canvas.height !== height) { + canvas.width = width; + canvas.height = height; + } + } + + function decodeGridData(data) { + if (data instanceof Int8Array || data instanceof Uint8Array) return data; + if (Array.isArray(data)) return data; + if (typeof data === "string" && data.length) { + try { + const binary = atob(data); + const out = new Int8Array(binary.length); + for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i); + return out; + } catch { + return []; + } + } + return []; + } + + /** + * Paint ROS occupancy grid onto canvas. + * @param {HTMLCanvasElement} canvas + * @param {{ width: number, height: number, data: number[]|Int8Array|string }} grid + * @param {{ palette?: object }} [opts] + */ + function renderGrid(canvas, grid, opts = {}) { + if (!canvas || !grid) return false; + const width = Number(grid.width) | 0; + const height = Number(grid.height) | 0; + if (!width || !height) return false; + + const data = decodeGridData(grid.data); + if (!data.length) return false; + + ensureCanvasSize(canvas, width, height); + const ctx = canvas.getContext("2d"); + if (!ctx) return false; + + const palette = paletteFrom(opts); + const imageData = ctx.createImageData(width, height); + const px = imageData.data; + + for (let row = 0; row < height; row++) { + const srcRow = height - 1 - row; + for (let col = 0; col < width; col++) { + const idx = srcRow * width + col; + const value = idx < data.length ? data[idx] : -1; + const rgba = occToRgba(value, palette); + const dst = (row * width + col) * 4; + px[dst] = rgba[0]; + px[dst + 1] = rgba[1]; + px[dst + 2] = rgba[2]; + px[dst + 3] = rgba[3]; + } + } + + ctx.putImageData(imageData, 0, 0); + return true; + } + + /** + * Convert map_server grayscale image to RViz-style occupancy colors. + * @param {HTMLCanvasElement} canvas + * @param {CanvasImageSource} source + * @param {object} [meta] — negate, occupied_thresh, free_thresh + * @param {{ palette?: object }} [opts] + */ + function renderFromImage(canvas, source, meta = {}, opts = {}) { + if (!canvas || !source) return false; + const width = source.naturalWidth || source.videoWidth || source.width; + const height = source.naturalHeight || source.videoHeight || source.height; + if (!width || !height) return false; + + ensureCanvasSize(canvas, width, height); + const ctx = canvas.getContext("2d"); + if (!ctx) return false; + + const scratch = document.createElement("canvas"); + scratch.width = width; + scratch.height = height; + const sctx = scratch.getContext("2d"); + if (!sctx) return false; + + sctx.drawImage(source, 0, 0, width, height); + const sampled = sctx.getImageData(0, 0, width, height); + const src = sampled.data; + const palette = paletteFrom(opts); + const imageData = ctx.createImageData(width, height); + const dst = imageData.data; + const thresholds = metaThresholds(meta); + + for (let i = 0, p = 0; p < width * height; p++, i += 4) { + const gray = src[i]; + const occ = grayToOccValue(gray, thresholds); + const rgba = occToRgba(occ, palette); + dst[i] = rgba[0]; + dst[i + 1] = rgba[1]; + dst[i + 2] = rgba[2]; + dst[i + 3] = 255; + } + + ctx.putImageData(imageData, 0, 0); + return true; + } + + window.MapOccupancyCanvas = { + DEFAULT_PALETTE, + DEFAULT_THRESHOLDS, + grayToOccValue, + occToRgba, + decodeGridData, + renderGrid, + renderFromImage, + }; +})(); diff --git a/www/style.css b/www/style.css index f2853fd..fdaab13 100644 --- a/www/style.css +++ b/www/style.css @@ -2227,7 +2227,7 @@ body.dashboard-widget-dragging .dashboardWidgetHeader { height: 100%; overflow: hidden; background: #d8dde4; - border-radius: 2px; + border-radius: 0; } .dashboardWidget--operate.dashboardWidget--map .dashboardWidgetBody, .dashboardWidget--operate.dashboardWidget--map_locked .dashboardWidgetBody { @@ -3863,6 +3863,7 @@ body.auth-readonly-maps-page .mapsMirMapMenuCancelBtn { background: #b8b8b8; display: flex; flex-direction: column; + border-radius: 0; } .mapEditorCanvasWrap.is-panning { @@ -3898,6 +3899,7 @@ body.auth-readonly-maps-page .mapsMirMapMenuCancelBtn { flex: 1; min-height: 0; overflow: hidden; + border-radius: 0; } .mapEditorCanvasInner { @@ -3919,9 +3921,9 @@ body.auth-readonly-maps-page .mapsMirMapMenuCancelBtn { position: relative; min-width: 480px; min-height: 360px; - background: #fff; - box-shadow: 0 2px 16px rgba(0, 0, 0, 0.12); + background: transparent; user-select: none; + border-radius: 0; } .mapEditorSheet--blank { @@ -3929,9 +3931,10 @@ body.auth-readonly-maps-page .mapsMirMapMenuCancelBtn { } .mapEditorSheet--hasImage { - background: #fff; + background: transparent; min-width: 0; min-height: 0; + box-shadow: none; } .mapEditorSheetGrid { @@ -3967,18 +3970,23 @@ body.auth-readonly-maps-page .mapsMirMapMenuCancelBtn { left: 0; top: 0; transform-origin: 0 0; + pointer-events: none; } .mapEditorOriginAxis--x { width: 36px; height: 2px; + top: -1px; + left: 0; background: #e74c3c; box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.6); } -.mapEditorOriginAxis--y { +.mapEditorOriginAxis--z { width: 2px; height: 36px; + left: -1px; + top: -36px; background: #27ae60; box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.6); } @@ -3993,12 +4001,41 @@ body.auth-readonly-maps-page .mapsMirMapMenuCancelBtn { background: #e67e22; border: 2px solid #fff; box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25); + pointer-events: none; +} + +.mapEditorOriginHit { + position: absolute; + left: -18px; + top: -18px; + width: 36px; + height: 36px; + padding: 0; + margin: 0; + border: none; + border-radius: 0; + background: transparent; + pointer-events: auto; + cursor: help; + z-index: 5; + appearance: none; + font: inherit; + color: inherit; +} + +.mapEditorOriginHit:focus { + outline: none; +} + +.mapEditorOriginHit:focus-visible { + outline: 2px solid rgba(230, 126, 34, 0.85); + outline-offset: 2px; } .mapEditorOriginLabel { position: absolute; left: 8px; - top: -22px; + top: -48px; padding: 2px 6px; border-radius: 3px; background: rgba(255, 255, 255, 0.92); @@ -4009,6 +4046,17 @@ body.auth-readonly-maps-page .mapsMirMapMenuCancelBtn { white-space: nowrap; line-height: 1.3; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12); + pointer-events: none; + visibility: hidden; + opacity: 0; + transition: opacity 0.12s ease; +} + +.mapEditorOrigin:has(.mapEditorOriginHit:hover) .mapEditorOriginLabel, +.mapEditorOrigin:has(.mapEditorOriginHit:focus-visible) .mapEditorOriginLabel, +.mapEditorOrigin.mapEditorOrigin--showLabel .mapEditorOriginLabel { + visibility: visible; + opacity: 1; } .mapEditorOrigin--offMap .mapEditorOriginLabel { @@ -4016,17 +4064,27 @@ body.auth-readonly-maps-page .mapsMirMapMenuCancelBtn { border-color: #ccc; } -.mapEditorImage { +.mapEditorImageLoader { + position: absolute; + width: 0; + height: 0; + opacity: 0; + visibility: hidden; + pointer-events: none; +} + +.mapEditorOccupancyCanvas { display: block; position: relative; z-index: 1; width: 100%; height: 100%; max-width: none; + border-radius: 0; + background: transparent; image-rendering: pixelated; image-rendering: crisp-edges; user-select: none; - -webkit-user-drag: none; pointer-events: none; }