This commit is contained in:
234
www/map-occupancy-edit.js
Normal file
234
www/map-occupancy-edit.js
Normal file
@@ -0,0 +1,234 @@
|
||||
(() => {
|
||||
/**
|
||||
* map_server grayscale editing — bake walls/floors and brush-erase on floor-plan raster.
|
||||
* Values match ROS map_server convention (see map-occupancy-canvas.js).
|
||||
*/
|
||||
const GRAY = {
|
||||
free: 254,
|
||||
occupied: 0,
|
||||
unknown: 205,
|
||||
};
|
||||
|
||||
const DEFAULT_LINE_WIDTH = 3;
|
||||
const DEFAULT_BRUSH_RADIUS = 10;
|
||||
|
||||
function ensureCanvas(canvas, width, height) {
|
||||
if (!canvas) return null;
|
||||
if (canvas.width !== width || canvas.height !== height) {
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
}
|
||||
return canvas.getContext("2d");
|
||||
}
|
||||
|
||||
function copyCanvas(src, dst) {
|
||||
if (!src?.width || !dst) return false;
|
||||
const ctx = ensureCanvas(dst, src.width, src.height);
|
||||
if (!ctx) return false;
|
||||
ctx.drawImage(src, 0, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Composite = base scan + floor polygons (free) + wall polylines (occupied).
|
||||
* @param {HTMLCanvasElement} baseCanvas
|
||||
* @param {HTMLCanvasElement} outCanvas
|
||||
* @param {object[]} zones
|
||||
*/
|
||||
function rebakeComposite(baseCanvas, outCanvas, zones) {
|
||||
if (!baseCanvas?.width || !outCanvas) return false;
|
||||
copyCanvas(baseCanvas, outCanvas);
|
||||
const list = Array.isArray(zones) ? zones : [];
|
||||
list.filter((z) => z?.type === "floor").forEach((z) => bakeFloor(outCanvas, z.points));
|
||||
list.filter((z) => z?.type === "wall").forEach((z) => bakeWall(outCanvas, z.points));
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Load PNG pixels into editable grayscale source canvas. */
|
||||
function initSourceFromImage(sourceCanvas, imageEl) {
|
||||
if (!sourceCanvas || !imageEl?.naturalWidth) return false;
|
||||
const w = imageEl.naturalWidth;
|
||||
const h = imageEl.naturalHeight;
|
||||
const ctx = ensureCanvas(sourceCanvas, w, h);
|
||||
if (!ctx) return false;
|
||||
ctx.drawImage(imageEl, 0, 0, w, h);
|
||||
return true;
|
||||
}
|
||||
|
||||
function cloneRaster(sourceCanvas) {
|
||||
if (!sourceCanvas?.width) return null;
|
||||
const ctx = sourceCanvas.getContext("2d");
|
||||
if (!ctx) return null;
|
||||
return ctx.getImageData(0, 0, sourceCanvas.width, sourceCanvas.height);
|
||||
}
|
||||
|
||||
function restoreRaster(sourceCanvas, imageData) {
|
||||
if (!sourceCanvas || !imageData) return false;
|
||||
const ctx = ensureCanvas(sourceCanvas, imageData.width, imageData.height);
|
||||
if (!ctx) return false;
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
function grayRgb(gray) {
|
||||
const g = Math.max(0, Math.min(255, gray | 0));
|
||||
return `rgb(${g},${g},${g})`;
|
||||
}
|
||||
|
||||
/** Paint display canvas from grayscale source using map_server thresholds. */
|
||||
function renderDisplayFromSource(displayCanvas, sourceCanvas, meta, occModule) {
|
||||
const occ = occModule || window.MapOccupancyCanvas;
|
||||
if (!occ?.renderFromImage || !displayCanvas || !sourceCanvas?.width) return false;
|
||||
return occ.renderFromImage(displayCanvas, sourceCanvas, meta || {});
|
||||
}
|
||||
|
||||
function strokePolyline(ctx, points, gray, lineWidth = DEFAULT_LINE_WIDTH) {
|
||||
if (!ctx || !points || points.length < 2) return;
|
||||
ctx.save();
|
||||
ctx.strokeStyle = grayRgb(gray);
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.lineCap = "round";
|
||||
ctx.lineJoin = "round";
|
||||
ctx.beginPath();
|
||||
points.forEach((p, i) => {
|
||||
if (i === 0) ctx.moveTo(p.x, p.y);
|
||||
else ctx.lineTo(p.x, p.y);
|
||||
});
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function fillPolygon(ctx, points, gray) {
|
||||
if (!ctx || !points || points.length < 3) return;
|
||||
ctx.save();
|
||||
ctx.fillStyle = grayRgb(gray);
|
||||
ctx.beginPath();
|
||||
points.forEach((p, i) => {
|
||||
if (i === 0) ctx.moveTo(p.x, p.y);
|
||||
else ctx.lineTo(p.x, p.y);
|
||||
});
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
/** Bake wall polyline as occupied pixels. */
|
||||
function bakeWall(sourceCanvas, points, lineWidth = DEFAULT_LINE_WIDTH) {
|
||||
const ctx = sourceCanvas?.getContext("2d");
|
||||
if (!ctx) return false;
|
||||
strokePolyline(ctx, points, GRAY.occupied, lineWidth);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Bake floor polygon as free walkable pixels. */
|
||||
function bakeFloor(sourceCanvas, points) {
|
||||
const ctx = sourceCanvas?.getContext("2d");
|
||||
if (!ctx) return false;
|
||||
fillPolygon(ctx, points, GRAY.free);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Remove baked wall by painting free along its path. */
|
||||
function unbakeWall(sourceCanvas, points, lineWidth = DEFAULT_LINE_WIDTH + 2) {
|
||||
const ctx = sourceCanvas?.getContext("2d");
|
||||
if (!ctx) return false;
|
||||
strokePolyline(ctx, points, GRAY.free, lineWidth);
|
||||
return true;
|
||||
}
|
||||
|
||||
function bakeZone(sourceCanvas, zone) {
|
||||
if (!zone?.points?.length) return false;
|
||||
if (zone.type === "wall") return bakeWall(sourceCanvas, zone.points);
|
||||
if (zone.type === "floor") return bakeFloor(sourceCanvas, zone.points);
|
||||
return false;
|
||||
}
|
||||
|
||||
function unbakeZone(sourceCanvas, zone) {
|
||||
if (!zone?.points?.length) return false;
|
||||
if (zone.type === "wall") return unbakeWall(sourceCanvas, zone.points);
|
||||
if (zone.type === "floor") {
|
||||
const ctx = sourceCanvas?.getContext("2d");
|
||||
if (!ctx) return false;
|
||||
fillPolygon(ctx, zone.points, GRAY.unknown);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function paintBrush(sourceCanvas, x, y, layer, radius = DEFAULT_BRUSH_RADIUS) {
|
||||
const ctx = sourceCanvas?.getContext("2d");
|
||||
if (!ctx) return false;
|
||||
const gray = layer === "floor" ? GRAY.unknown : GRAY.free;
|
||||
ctx.save();
|
||||
ctx.fillStyle = grayRgb(gray);
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Erase pixels inside axis-aligned rectangle on base layer. */
|
||||
function eraseRect(sourceCanvas, x0, y0, x1, y1, layer) {
|
||||
const ctx = sourceCanvas?.getContext("2d");
|
||||
if (!ctx) return false;
|
||||
const gray = layer === "floor" ? GRAY.unknown : GRAY.free;
|
||||
const x = Math.min(x0, x1);
|
||||
const y = Math.min(y0, y1);
|
||||
const w = Math.abs(x1 - x0);
|
||||
const h = Math.abs(y1 - y0);
|
||||
if (w < 1 || h < 1) return false;
|
||||
ctx.save();
|
||||
ctx.fillStyle = grayRgb(gray);
|
||||
ctx.fillRect(x, y, w, h);
|
||||
ctx.restore();
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Interpolated brush stroke between two image-space points. */
|
||||
function paintBrushStroke(sourceCanvas, x0, y0, x1, y1, layer, radius = DEFAULT_BRUSH_RADIUS) {
|
||||
const dist = Math.hypot(x1 - x0, y1 - y0);
|
||||
const step = Math.max(1, radius * 0.5);
|
||||
const n = Math.max(1, Math.ceil(dist / step));
|
||||
for (let i = 0; i <= n; i++) {
|
||||
const t = i / n;
|
||||
paintBrush(sourceCanvas, x0 + (x1 - x0) * t, y0 + (y1 - y0) * t, layer, radius);
|
||||
}
|
||||
}
|
||||
|
||||
function exportPngBlob(sourceCanvas) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!sourceCanvas?.width) {
|
||||
reject(new Error("no source canvas"));
|
||||
return;
|
||||
}
|
||||
sourceCanvas.toBlob(
|
||||
(blob) => {
|
||||
if (blob) resolve(blob);
|
||||
else reject(new Error("export failed"));
|
||||
},
|
||||
"image/png",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
window.MapOccupancyEdit = {
|
||||
GRAY,
|
||||
DEFAULT_LINE_WIDTH,
|
||||
DEFAULT_BRUSH_RADIUS,
|
||||
initSourceFromImage,
|
||||
cloneRaster,
|
||||
restoreRaster,
|
||||
copyCanvas,
|
||||
rebakeComposite,
|
||||
renderDisplayFromSource,
|
||||
bakeWall,
|
||||
bakeFloor,
|
||||
bakeZone,
|
||||
unbakeZone,
|
||||
paintBrush,
|
||||
paintBrushStroke,
|
||||
eraseRect,
|
||||
exportPngBlob,
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user