This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -15,6 +15,10 @@
|
|||||||
build/
|
build/
|
||||||
|
|
||||||
# Runtime data (SQLite + media)
|
# Runtime data (SQLite + media)
|
||||||
|
maps/
|
||||||
|
RBS.db
|
||||||
|
RBS.db-wal
|
||||||
|
RBS.db-shm
|
||||||
data/RBS.db
|
data/RBS.db
|
||||||
data/RBS.db-wal
|
data/RBS.db-wal
|
||||||
data/RBS.db-shm
|
data/RBS.db-shm
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 48 KiB |
@@ -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
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 48 KiB |
@@ -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
|
|
||||||
@@ -21,6 +21,24 @@
|
|||||||
|
|
||||||
namespace lm {
|
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,
|
LidarManagerApp::LidarManagerApp(int port,
|
||||||
std::filesystem::path www_root,
|
std::filesystem::path www_root,
|
||||||
std::filesystem::path data_path)
|
std::filesystem::path data_path)
|
||||||
@@ -30,6 +48,7 @@ LidarManagerApp::LidarManagerApp(int port,
|
|||||||
|
|
||||||
int LidarManagerApp::run()
|
int LidarManagerApp::run()
|
||||||
{
|
{
|
||||||
|
data_path_ = resolveDataPath(data_path_);
|
||||||
const std::filesystem::path data_dir = data_path_.parent_path();
|
const std::filesystem::path data_dir = data_path_.parent_path();
|
||||||
Database database(data_dir);
|
Database database(data_dir);
|
||||||
std::string db_err;
|
std::string db_err;
|
||||||
|
|||||||
@@ -193,6 +193,48 @@ bool Database::ensureDataDirs(std::string& err)
|
|||||||
return true;
|
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)
|
bool Database::applySchema(std::string& err)
|
||||||
{
|
{
|
||||||
return execSql(db_, kSchemaSql, err);
|
return execSql(db_, kSchemaSql, err);
|
||||||
@@ -494,6 +536,8 @@ bool Database::init(std::string& err)
|
|||||||
return false;
|
return false;
|
||||||
if (!ensureDataDirs(err))
|
if (!ensureDataDirs(err))
|
||||||
return false;
|
return false;
|
||||||
|
if (!migrateLegacyMapsDir(err))
|
||||||
|
return false;
|
||||||
if (!migrateFromJsonIfNeeded(err))
|
if (!migrateFromJsonIfNeeded(err))
|
||||||
return false;
|
return false;
|
||||||
if (!getMeta("schema_version"))
|
if (!getMeta("schema_version"))
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ private:
|
|||||||
bool applySchemaMigrations(std::string& err);
|
bool applySchemaMigrations(std::string& err);
|
||||||
bool migrateFromJsonIfNeeded(std::string& err);
|
bool migrateFromJsonIfNeeded(std::string& err);
|
||||||
bool ensureDataDirs(std::string& err);
|
bool ensureDataDirs(std::string& err);
|
||||||
|
bool migrateLegacyMapsDir(std::string& err);
|
||||||
std::optional<std::string> getMeta(const std::string& key) const;
|
std::optional<std::string> getMeta(const std::string& key) const;
|
||||||
bool setMeta(const std::string& key, const std::string& value);
|
bool setMeta(const std::string& key, const std::string& value);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -389,6 +389,7 @@
|
|||||||
"maps.editor.tool.save": "Lưu map",
|
"maps.editor.tool.save": "Lưu map",
|
||||||
"maps.editor.tool.pan": "Pan — di chuyển vùng nhìn",
|
"maps.editor.tool.pan": "Pan — di chuyển vùng nhìn",
|
||||||
"maps.editor.tool.crosshair": "Crosshair",
|
"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.center": "Căn giữa vùng nhìn",
|
||||||
"maps.editor.tool.lidar": "Hiển thị LiDAR",
|
"maps.editor.tool.lidar": "Hiển thị LiDAR",
|
||||||
"maps.editor.tool.waypoints": "Vị trí / waypoint",
|
"maps.editor.tool.waypoints": "Vị trí / waypoint",
|
||||||
@@ -882,7 +883,7 @@
|
|||||||
"maps.uploadConfirm.title": "Overwrite floor plan?",
|
"maps.uploadConfirm.title": "Overwrite floor plan?",
|
||||||
"maps.uploadConfirm.text": "The current map image will be replaced. Continue?",
|
"maps.uploadConfirm.text": "The current map image will be replaced. Continue?",
|
||||||
"maps.uploadConfirm.yes": "Overwrite",
|
"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.hint": "Enter origin, resolution, and occupancy thresholds — or import a .yaml file.",
|
||||||
"maps.uploadMeta.importYaml": "Import YAML file…",
|
"maps.uploadMeta.importYaml": "Import YAML file…",
|
||||||
"maps.uploadMeta.negate": "Negate",
|
"maps.uploadMeta.negate": "Negate",
|
||||||
@@ -908,6 +909,7 @@
|
|||||||
"maps.editor.tool.save": "Save map",
|
"maps.editor.tool.save": "Save map",
|
||||||
"maps.editor.tool.pan": "Pan — move view",
|
"maps.editor.tool.pan": "Pan — move view",
|
||||||
"maps.editor.tool.crosshair": "Crosshair",
|
"maps.editor.tool.crosshair": "Crosshair",
|
||||||
|
"maps.editor.tool.origin": "Show map origin",
|
||||||
"maps.editor.tool.center": "Center view",
|
"maps.editor.tool.center": "Center view",
|
||||||
"maps.editor.tool.lidar": "LiDAR overlay",
|
"maps.editor.tool.lidar": "LiDAR overlay",
|
||||||
"maps.editor.tool.waypoints": "Positions",
|
"maps.editor.tool.waypoints": "Positions",
|
||||||
|
|||||||
@@ -1016,6 +1016,9 @@
|
|||||||
<button type="button" class="mapEditorMapTool" id="mapEditorCrosshairBtn" disabled data-i18n-title="maps.editor.tool.crosshair" title="Crosshair">
|
<button type="button" class="mapEditorMapTool" id="mapEditorCrosshairBtn" disabled data-i18n-title="maps.editor.tool.crosshair" title="Crosshair">
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="6" fill="none" stroke="currentColor" stroke-width="1.4"/><path d="M10 4v12M4 10h12" stroke="currentColor" stroke-width="1.3"/></svg>
|
<svg width="20" height="20" viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="6" fill="none" stroke="currentColor" stroke-width="1.4"/><path d="M10 4v12M4 10h12" stroke="currentColor" stroke-width="1.3"/></svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="mapEditorMapTool is-active" id="mapEditorOriginBtn" data-i18n-title="maps.editor.tool.origin" title="Show origin" aria-pressed="true">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="2.2" fill="currentColor"/><path d="M10 10h7.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M10 10V2.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||||
|
</button>
|
||||||
<button type="button" class="mapEditorMapTool" id="mapEditorFitBtn" data-i18n-title="maps.editor.fit" title="Fit">
|
<button type="button" class="mapEditorMapTool" id="mapEditorFitBtn" data-i18n-title="maps.editor.fit" title="Fit">
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" aria-hidden="true"><path d="M3 7V3h4M13 3h4v4M17 13h-4v4M7 17H3v-4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
<svg width="20" height="20" viewBox="0 0 20 20" aria-hidden="true"><path d="M3 7V3h4M13 3h4v4M17 13h-4v4M7 17H3v-4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||||
</button>
|
</button>
|
||||||
@@ -1045,12 +1048,14 @@
|
|||||||
<div class="mapEditorImageLayer" id="mapEditorImageLayer">
|
<div class="mapEditorImageLayer" id="mapEditorImageLayer">
|
||||||
<div class="mapEditorSheet" id="mapEditorSheet">
|
<div class="mapEditorSheet" id="mapEditorSheet">
|
||||||
<div class="mapEditorSheetGrid" id="mapEditorSheetGrid" aria-hidden="true"></div>
|
<div class="mapEditorSheetGrid" id="mapEditorSheetGrid" aria-hidden="true"></div>
|
||||||
<img id="mapEditorImage" class="mapEditorImage" alt="" draggable="false" hidden />
|
<img id="mapEditorImage" class="mapEditorImageLoader" alt="" draggable="false" hidden />
|
||||||
|
<canvas id="mapEditorOccupancyCanvas" class="mapEditorOccupancyCanvas" hidden aria-hidden="true"></canvas>
|
||||||
<div id="mapEditorOrigin" class="mapEditorOrigin" hidden aria-hidden="true">
|
<div id="mapEditorOrigin" class="mapEditorOrigin" hidden aria-hidden="true">
|
||||||
<span class="mapEditorOriginAxis mapEditorOriginAxis--x" aria-hidden="true"></span>
|
<span class="mapEditorOriginAxis mapEditorOriginAxis--x" aria-hidden="true"></span>
|
||||||
<span class="mapEditorOriginAxis mapEditorOriginAxis--y" aria-hidden="true"></span>
|
<span class="mapEditorOriginAxis mapEditorOriginAxis--z" aria-hidden="true"></span>
|
||||||
<span class="mapEditorOriginDot" aria-hidden="true"></span>
|
<span class="mapEditorOriginDot" aria-hidden="true"></span>
|
||||||
<span class="mapEditorOriginLabel" id="mapEditorOriginLabel"></span>
|
<button type="button" class="mapEditorOriginHit" id="mapEditorOriginHit" tabindex="0" aria-describedby="mapEditorOriginLabel"></button>
|
||||||
|
<span class="mapEditorOriginLabel" id="mapEditorOriginLabel" aria-hidden="true"></span>
|
||||||
</div>
|
</div>
|
||||||
<div id="mapEditorEmpty" class="mapEditorEmpty" hidden data-i18n="maps.editor.noData">No map data — open ⋮ menu to upload a PNG.</div>
|
<div id="mapEditorEmpty" class="mapEditorEmpty" hidden data-i18n="maps.editor.noData">No map data — open ⋮ menu to upload a PNG.</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1723,6 +1728,7 @@ GET /api/v2.0.0/status</pre>
|
|||||||
<script src="/nav.js"></script>
|
<script src="/nav.js"></script>
|
||||||
<script src="/missions.js"></script>
|
<script src="/missions.js"></script>
|
||||||
<script src="/map-geo.js"></script>
|
<script src="/map-geo.js"></script>
|
||||||
|
<script src="/map-occupancy-canvas.js"></script>
|
||||||
<script src="/map-yaml.js"></script>
|
<script src="/map-yaml.js"></script>
|
||||||
<script src="/maps.js"></script>
|
<script src="/maps.js"></script>
|
||||||
<script src="/map-editor.js"></script>
|
<script src="/map-editor.js"></script>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
const el = (id) => document.getElementById(id);
|
const el = (id) => document.getElementById(id);
|
||||||
const t = (key, vars) => window.I18n?.t(key, vars) ?? key;
|
const t = (key, vars) => window.I18n?.t(key, vars) ?? key;
|
||||||
const Geo = () => window.MapGeo;
|
const Geo = () => window.MapGeo;
|
||||||
|
const Occ = () => window.MapOccupancyCanvas;
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
mapId: null,
|
mapId: null,
|
||||||
@@ -14,6 +15,9 @@
|
|||||||
view: Geo()?.createView(1, 0, 0) || { scale: 1, panX: 0, panY: 0 },
|
view: Geo()?.createView(1, 0, 0) || { scale: 1, panX: 0, panY: 0 },
|
||||||
panning: null,
|
panning: null,
|
||||||
tipVisible: true,
|
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). */
|
/** Pending ROS metadata from upload dialog (set before PNG picker). */
|
||||||
uploadMeta: null,
|
uploadMeta: null,
|
||||||
};
|
};
|
||||||
@@ -27,7 +31,10 @@
|
|||||||
const sheetEl = el("mapEditorSheet");
|
const sheetEl = el("mapEditorSheet");
|
||||||
const gridEl = el("mapEditorSheetGrid");
|
const gridEl = el("mapEditorSheetGrid");
|
||||||
const imageEl = el("mapEditorImage");
|
const imageEl = el("mapEditorImage");
|
||||||
|
const occupancyCanvasEl = el("mapEditorOccupancyCanvas");
|
||||||
const originEl = el("mapEditorOrigin");
|
const originEl = el("mapEditorOrigin");
|
||||||
|
const originHitEl = el("mapEditorOriginHit");
|
||||||
|
const originLabelEl = el("mapEditorOriginLabel");
|
||||||
const emptyEl = el("mapEditorEmpty");
|
const emptyEl = el("mapEditorEmpty");
|
||||||
const tipEl = el("mapEditorCanvasTip");
|
const tipEl = el("mapEditorCanvasTip");
|
||||||
const statusViewEl = el("mapEditorStatusView");
|
const statusViewEl = el("mapEditorStatusView");
|
||||||
@@ -94,7 +101,58 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function hasFloorPlan() {
|
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) {
|
function setDirty(flag) {
|
||||||
@@ -204,7 +262,6 @@
|
|||||||
gridEl.hidden = true;
|
gridEl.hidden = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (originEl) originEl.hidden = !has;
|
|
||||||
updateOriginMarker();
|
updateOriginMarker();
|
||||||
updateStatusBar();
|
updateStatusBar();
|
||||||
}
|
}
|
||||||
@@ -224,11 +281,25 @@
|
|||||||
return base;
|
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() {
|
function updateOriginMarker() {
|
||||||
if (!originEl) return;
|
if (!originEl) return;
|
||||||
const geo = Geo();
|
const geo = Geo();
|
||||||
const { width, height } = floorPlanSize();
|
const { width, height } = floorPlanSize();
|
||||||
if (!geo || !hasFloorPlan() || !width || !height) {
|
if (!state.showOrigin || !geo || !hasFloorPlan() || !width || !height) {
|
||||||
originEl.hidden = true;
|
originEl.hidden = true;
|
||||||
originEl.setAttribute("aria-hidden", "true");
|
originEl.setAttribute("aria-hidden", "true");
|
||||||
return;
|
return;
|
||||||
@@ -249,18 +320,23 @@
|
|||||||
originEl.style.top = `${pt.y}px`;
|
originEl.style.top = `${pt.y}px`;
|
||||||
originEl.style.transform = `rotate(${yawDeg}deg)`;
|
originEl.style.transform = `rotate(${yawDeg}deg)`;
|
||||||
|
|
||||||
const labelEl = el("mapEditorOriginLabel");
|
if (originLabelEl) {
|
||||||
if (labelEl) {
|
originLabelEl.textContent = t("maps.editor.originLabelShort", {
|
||||||
labelEl.textContent = t("maps.editor.originLabelShort", {
|
|
||||||
x: ox.toFixed(2),
|
x: ox.toFixed(2),
|
||||||
y: oy.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),
|
x: ox.toFixed(3),
|
||||||
y: oy.toFixed(3),
|
y: oy.toFixed(3),
|
||||||
yaw: ((oyaw * 180) / Math.PI).toFixed(1),
|
yaw: ((oyaw * 180) / Math.PI).toFixed(1),
|
||||||
});
|
});
|
||||||
|
if (originHitEl) {
|
||||||
|
originHitEl.title = tooltip;
|
||||||
|
originHitEl.setAttribute("aria-label", tooltip);
|
||||||
|
}
|
||||||
|
setOriginLabelVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateStatusBar(pointerClient) {
|
function updateStatusBar(pointerClient) {
|
||||||
@@ -311,11 +387,13 @@
|
|||||||
imageEl.src = url;
|
imageEl.src = url;
|
||||||
imageEl.hidden = false;
|
imageEl.hidden = false;
|
||||||
if (emptyEl) emptyEl.hidden = true;
|
if (emptyEl) emptyEl.hidden = true;
|
||||||
|
setOccupancyCanvasVisible(false);
|
||||||
} else {
|
} else {
|
||||||
if (imageEl) {
|
if (imageEl) {
|
||||||
imageEl.hidden = true;
|
imageEl.hidden = true;
|
||||||
imageEl.removeAttribute("src");
|
imageEl.removeAttribute("src");
|
||||||
}
|
}
|
||||||
|
setOccupancyCanvasVisible(false);
|
||||||
if (emptyEl) emptyEl.hidden = false;
|
if (emptyEl) emptyEl.hidden = false;
|
||||||
}
|
}
|
||||||
updateMenuActionsUi();
|
updateMenuActionsUi();
|
||||||
@@ -323,6 +401,7 @@
|
|||||||
imageEl?.addEventListener(
|
imageEl?.addEventListener(
|
||||||
"load",
|
"load",
|
||||||
() => {
|
() => {
|
||||||
|
paintOccupancyFromImage();
|
||||||
updateImageLayer();
|
updateImageLayer();
|
||||||
fitToView();
|
fitToView();
|
||||||
},
|
},
|
||||||
@@ -381,6 +460,7 @@
|
|||||||
async function reloadMap() {
|
async function reloadMap() {
|
||||||
if (!state.mapId) return;
|
if (!state.mapId) return;
|
||||||
state.map = await api(`/api/maps/${encodeURIComponent(state.mapId)}`);
|
state.map = await api(`/api/maps/${encodeURIComponent(state.mapId)}`);
|
||||||
|
await loadYamlMeta();
|
||||||
updateHeader();
|
updateHeader();
|
||||||
renderMapImage();
|
renderMapImage();
|
||||||
fillSettingsForm();
|
fillSettingsForm();
|
||||||
@@ -392,6 +472,7 @@
|
|||||||
state.readOnly = !!callbacks.readOnly || !callbacks.canWrite;
|
state.readOnly = !!callbacks.readOnly || !callbacks.canWrite;
|
||||||
state.dirty = false;
|
state.dirty = false;
|
||||||
state.tipVisible = true;
|
state.tipVisible = true;
|
||||||
|
state.showOrigin = true;
|
||||||
state.activeTool = "pan";
|
state.activeTool = "pan";
|
||||||
state.view = Geo()?.createView(1, 0, 0) || { scale: 1, panX: 0, panY: 0 };
|
state.view = Geo()?.createView(1, 0, 0) || { scale: 1, panX: 0, panY: 0 };
|
||||||
if (tipEl) {
|
if (tipEl) {
|
||||||
@@ -399,6 +480,7 @@
|
|||||||
tipEl.textContent = t("maps.editor.canvasTip");
|
tipEl.textContent = t("maps.editor.canvasTip");
|
||||||
}
|
}
|
||||||
setActiveTool("pan");
|
setActiveTool("pan");
|
||||||
|
setShowOrigin(true);
|
||||||
setDirty(false);
|
setDirty(false);
|
||||||
applyReadOnlyUi();
|
applyReadOnlyUi();
|
||||||
reloadMap().catch((e) => alert(e.message));
|
reloadMap().catch((e) => alert(e.message));
|
||||||
@@ -409,6 +491,7 @@
|
|||||||
state.map = null;
|
state.map = null;
|
||||||
state.callbacks = {};
|
state.callbacks = {};
|
||||||
state.uploadMeta = null;
|
state.uploadMeta = null;
|
||||||
|
state.yamlMeta = null;
|
||||||
menuDialogEl?.close();
|
menuDialogEl?.close();
|
||||||
settingsDialogEl?.close();
|
settingsDialogEl?.close();
|
||||||
activateDialogEl?.close();
|
activateDialogEl?.close();
|
||||||
@@ -584,6 +667,7 @@
|
|||||||
state.callbacks.onMapUpdated?.(updated);
|
state.callbacks.onMapUpdated?.(updated);
|
||||||
setDirty(false);
|
setDirty(false);
|
||||||
fillSettingsForm();
|
fillSettingsForm();
|
||||||
|
await loadYamlMeta();
|
||||||
renderMapImage();
|
renderMapImage();
|
||||||
menuDialogEl?.close();
|
menuDialogEl?.close();
|
||||||
uploadMetaDialogEl?.close();
|
uploadMetaDialogEl?.close();
|
||||||
@@ -689,6 +773,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function bindEvents() {
|
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", () => {
|
el("mapEditorBackBtn")?.addEventListener("click", () => {
|
||||||
if (state.dirty && !confirm(t("maps.editor.unsavedLeave"))) return;
|
if (state.dirty && !confirm(t("maps.editor.unsavedLeave"))) return;
|
||||||
state.callbacks.onClose?.();
|
state.callbacks.onClose?.();
|
||||||
@@ -714,6 +803,7 @@
|
|||||||
saveMap().catch((e) => alert(e.message));
|
saveMap().catch((e) => alert(e.message));
|
||||||
});
|
});
|
||||||
el("mapEditorPanBtn")?.addEventListener("click", () => setActiveTool("pan"));
|
el("mapEditorPanBtn")?.addEventListener("click", () => setActiveTool("pan"));
|
||||||
|
el("mapEditorOriginBtn")?.addEventListener("click", () => setShowOrigin(!state.showOrigin));
|
||||||
el("mapEditorFitBtn")?.addEventListener("click", fitToView);
|
el("mapEditorFitBtn")?.addEventListener("click", fitToView);
|
||||||
el("mapEditorCenterBtn")?.addEventListener("click", () => {
|
el("mapEditorCenterBtn")?.addEventListener("click", () => {
|
||||||
dismissCanvasTip();
|
dismissCanvasTip();
|
||||||
@@ -818,6 +908,9 @@
|
|||||||
Object.values(uploadMetaFields).forEach((node) => {
|
Object.values(uploadMetaFields).forEach((node) => {
|
||||||
node?.addEventListener("input", () => {
|
node?.addEventListener("input", () => {
|
||||||
updateOriginMarker();
|
updateOriginMarker();
|
||||||
|
if (uploadMetaDialogEl?.open && imageEl?.naturalWidth) {
|
||||||
|
paintOccupancyFromImage();
|
||||||
|
}
|
||||||
if (node === uploadMetaFields.resolution && uploadMetaDialogEl?.open) {
|
if (node === uploadMetaFields.resolution && uploadMetaDialogEl?.open) {
|
||||||
updateImageLayer();
|
updateImageLayer();
|
||||||
}
|
}
|
||||||
@@ -835,5 +928,11 @@
|
|||||||
bindCanvasPanZoom();
|
bindCanvasPanZoom();
|
||||||
bindEvents();
|
bindEvents();
|
||||||
|
|
||||||
window.MapEditorApp = { open, close, reloadMap };
|
window.MapEditorApp = {
|
||||||
|
open,
|
||||||
|
close,
|
||||||
|
reloadMap,
|
||||||
|
paintOccupancyGrid,
|
||||||
|
paintOccupancyFromImage,
|
||||||
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
178
www/map-occupancy-canvas.js
Normal file
178
www/map-occupancy-canvas.js
Normal file
@@ -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,
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -2227,7 +2227,7 @@ body.dashboard-widget-dragging .dashboardWidgetHeader {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #d8dde4;
|
background: #d8dde4;
|
||||||
border-radius: 2px;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
.dashboardWidget--operate.dashboardWidget--map .dashboardWidgetBody,
|
.dashboardWidget--operate.dashboardWidget--map .dashboardWidgetBody,
|
||||||
.dashboardWidget--operate.dashboardWidget--map_locked .dashboardWidgetBody {
|
.dashboardWidget--operate.dashboardWidget--map_locked .dashboardWidgetBody {
|
||||||
@@ -3863,6 +3863,7 @@ body.auth-readonly-maps-page .mapsMirMapMenuCancelBtn {
|
|||||||
background: #b8b8b8;
|
background: #b8b8b8;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mapEditorCanvasWrap.is-panning {
|
.mapEditorCanvasWrap.is-panning {
|
||||||
@@ -3898,6 +3899,7 @@ body.auth-readonly-maps-page .mapsMirMapMenuCancelBtn {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mapEditorCanvasInner {
|
.mapEditorCanvasInner {
|
||||||
@@ -3919,9 +3921,9 @@ body.auth-readonly-maps-page .mapsMirMapMenuCancelBtn {
|
|||||||
position: relative;
|
position: relative;
|
||||||
min-width: 480px;
|
min-width: 480px;
|
||||||
min-height: 360px;
|
min-height: 360px;
|
||||||
background: #fff;
|
background: transparent;
|
||||||
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.12);
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mapEditorSheet--blank {
|
.mapEditorSheet--blank {
|
||||||
@@ -3929,9 +3931,10 @@ body.auth-readonly-maps-page .mapsMirMapMenuCancelBtn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mapEditorSheet--hasImage {
|
.mapEditorSheet--hasImage {
|
||||||
background: #fff;
|
background: transparent;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mapEditorSheetGrid {
|
.mapEditorSheetGrid {
|
||||||
@@ -3967,18 +3970,23 @@ body.auth-readonly-maps-page .mapsMirMapMenuCancelBtn {
|
|||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
transform-origin: 0 0;
|
transform-origin: 0 0;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mapEditorOriginAxis--x {
|
.mapEditorOriginAxis--x {
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
|
top: -1px;
|
||||||
|
left: 0;
|
||||||
background: #e74c3c;
|
background: #e74c3c;
|
||||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.6);
|
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mapEditorOriginAxis--y {
|
.mapEditorOriginAxis--z {
|
||||||
width: 2px;
|
width: 2px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
|
left: -1px;
|
||||||
|
top: -36px;
|
||||||
background: #27ae60;
|
background: #27ae60;
|
||||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.6);
|
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.6);
|
||||||
}
|
}
|
||||||
@@ -3993,12 +4001,41 @@ body.auth-readonly-maps-page .mapsMirMapMenuCancelBtn {
|
|||||||
background: #e67e22;
|
background: #e67e22;
|
||||||
border: 2px solid #fff;
|
border: 2px solid #fff;
|
||||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
|
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 {
|
.mapEditorOriginLabel {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 8px;
|
left: 8px;
|
||||||
top: -22px;
|
top: -48px;
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
background: rgba(255, 255, 255, 0.92);
|
background: rgba(255, 255, 255, 0.92);
|
||||||
@@ -4009,6 +4046,17 @@ body.auth-readonly-maps-page .mapsMirMapMenuCancelBtn {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
|
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 {
|
.mapEditorOrigin--offMap .mapEditorOriginLabel {
|
||||||
@@ -4016,17 +4064,27 @@ body.auth-readonly-maps-page .mapsMirMapMenuCancelBtn {
|
|||||||
border-color: #ccc;
|
border-color: #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mapEditorImage {
|
.mapEditorImageLoader {
|
||||||
|
position: absolute;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapEditorOccupancyCanvas {
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
|
border-radius: 0;
|
||||||
|
background: transparent;
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
image-rendering: crisp-edges;
|
image-rendering: crisp-edges;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-user-drag: none;
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user