This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
(() => {
|
||||
const el = (id) => document.getElementById(id);
|
||||
const t = (key, vars) => window.I18n?.t(key, vars) ?? key;
|
||||
const Geo = () => window.MapGeo;
|
||||
|
||||
const state = {
|
||||
mapId: null,
|
||||
@@ -9,23 +10,46 @@
|
||||
readOnly: false,
|
||||
dirty: false,
|
||||
activeTool: "pan",
|
||||
view: { scale: 1, panX: 0, panY: 0 },
|
||||
/** Layer 1 — view space (screen pan/zoom only). */
|
||||
view: Geo()?.createView(1, 0, 0) || { scale: 1, panX: 0, panY: 0 },
|
||||
panning: null,
|
||||
tipVisible: true,
|
||||
/** Pending ROS metadata from upload dialog (set before PNG picker). */
|
||||
uploadMeta: null,
|
||||
};
|
||||
|
||||
const titleEl = el("mapEditorTitle");
|
||||
const dirtyEl = el("mapEditorDirty");
|
||||
const canvasWrapEl = el("mapEditorCanvasWrap");
|
||||
const viewportEl = el("mapEditorViewport");
|
||||
const canvasInnerEl = el("mapEditorCanvasInner");
|
||||
const imageLayerEl = el("mapEditorImageLayer");
|
||||
const sheetEl = el("mapEditorSheet");
|
||||
const gridEl = el("mapEditorSheetGrid");
|
||||
const imageEl = el("mapEditorImage");
|
||||
const originEl = el("mapEditorOrigin");
|
||||
const emptyEl = el("mapEditorEmpty");
|
||||
const tipEl = el("mapEditorCanvasTip");
|
||||
const statusViewEl = el("mapEditorStatusView");
|
||||
const statusImageEl = el("mapEditorStatusImage");
|
||||
const statusWorldEl = el("mapEditorStatusWorld");
|
||||
const uploadInputEl = el("mapEditorUploadInput");
|
||||
const menuDialogEl = el("mapEditorMenuDialog");
|
||||
const settingsDialogEl = el("mapEditorSettingsDialog");
|
||||
const activateDialogEl = el("mapActivateDialog");
|
||||
const uploadConfirmDialogEl = el("mapUploadConfirmDialog");
|
||||
const uploadMetaDialogEl = el("mapUploadMetaDialog");
|
||||
const uploadYamlInputEl = el("mapUploadYamlInput");
|
||||
|
||||
const uploadMetaFields = {
|
||||
resolution: el("mapUploadResolution"),
|
||||
originX: el("mapUploadOriginX"),
|
||||
originY: el("mapUploadOriginY"),
|
||||
originYaw: el("mapUploadOriginYaw"),
|
||||
negate: el("mapUploadNegate"),
|
||||
occupiedThresh: el("mapUploadOccupiedThresh"),
|
||||
freeThresh: el("mapUploadFreeThresh"),
|
||||
};
|
||||
|
||||
const toolBtnEls = () => document.querySelectorAll(".mapEditorMapTool[data-tool]");
|
||||
|
||||
@@ -59,6 +83,20 @@
|
||||
return `/api/maps/${encodeURIComponent(map.id)}/image?t=${encodeURIComponent(map.updated_at || "")}`;
|
||||
}
|
||||
|
||||
function viewportSize() {
|
||||
const rect = viewportEl?.getBoundingClientRect();
|
||||
return { width: rect?.width || 1, height: rect?.height || 1 };
|
||||
}
|
||||
|
||||
/** Layer 2 — image dimensions in floor-plan pixels. */
|
||||
function floorPlanSize() {
|
||||
return Geo()?.imageSize(state.map, imageEl) || { width: 0, height: 0 };
|
||||
}
|
||||
|
||||
function hasFloorPlan() {
|
||||
return !!(state.map?.image_file && imageEl && !imageEl.hidden && imageEl.naturalWidth);
|
||||
}
|
||||
|
||||
function setDirty(flag) {
|
||||
state.dirty = !!flag;
|
||||
if (dirtyEl) dirtyEl.hidden = !state.dirty;
|
||||
@@ -86,51 +124,184 @@
|
||||
updateCanvasCursor();
|
||||
}
|
||||
|
||||
function centerSheetInView() {
|
||||
if (!canvasWrapEl || !sheetEl) return;
|
||||
const wrap = canvasWrapEl.getBoundingClientRect();
|
||||
const sw = sheetEl.offsetWidth || 480;
|
||||
const sh = sheetEl.offsetHeight || 360;
|
||||
state.view.panX = Math.max(40, (wrap.width - sw * state.view.scale) / 2);
|
||||
state.view.panY = Math.max(40, (wrap.height - sh * state.view.scale) / 2);
|
||||
}
|
||||
|
||||
/** Layer 1 — apply view transform to inner canvas only. */
|
||||
function applyViewTransform() {
|
||||
if (!canvasInnerEl) return;
|
||||
const { scale, panX, panY } = state.view;
|
||||
canvasInnerEl.style.transform = `translate(${panX}px, ${panY}px) scale(${scale})`;
|
||||
Geo()?.applyViewTransform(canvasInnerEl, state.view);
|
||||
updateStatusBar();
|
||||
}
|
||||
|
||||
function fitToView() {
|
||||
dismissCanvasTip();
|
||||
if (!canvasWrapEl || !sheetEl) return;
|
||||
const wrap = canvasWrapEl.getBoundingClientRect();
|
||||
const sw = imageEl && !imageEl.hidden ? imageEl.naturalWidth || sheetEl.offsetWidth : sheetEl.offsetWidth;
|
||||
const sh = imageEl && !imageEl.hidden ? imageEl.naturalHeight || sheetEl.offsetHeight : sheetEl.offsetHeight;
|
||||
const pad = 48;
|
||||
const scale = Math.min((wrap.width - pad) / sw, (wrap.height - pad) / sh, 4);
|
||||
state.view.scale = Math.max(0.1, scale);
|
||||
state.view.panX = (wrap.width - sw * state.view.scale) / 2;
|
||||
state.view.panY = (wrap.height - sh * state.view.scale) / 2;
|
||||
const vp = viewportSize();
|
||||
const { width, height } = floorPlanSize();
|
||||
const blankW = sheetEl?.offsetWidth || 480;
|
||||
const blankH = sheetEl?.offsetHeight || 360;
|
||||
state.view = Geo()?.fitViewToImage(
|
||||
vp.width,
|
||||
vp.height,
|
||||
hasFloorPlan() ? width : blankW,
|
||||
hasFloorPlan() ? height : blankH,
|
||||
) || state.view;
|
||||
applyViewTransform();
|
||||
updateCanvasCursor();
|
||||
}
|
||||
|
||||
function zoomBy(factor) {
|
||||
dismissCanvasTip();
|
||||
state.view.scale = Math.min(8, Math.max(0.1, state.view.scale * factor));
|
||||
function centerSheetInView() {
|
||||
const vp = viewportSize();
|
||||
const { width, height } = floorPlanSize();
|
||||
const blankW = sheetEl?.offsetWidth || 480;
|
||||
const blankH = sheetEl?.offsetHeight || 360;
|
||||
state.view = Geo()?.centerViewOnImage(
|
||||
vp.width,
|
||||
vp.height,
|
||||
hasFloorPlan() ? width : blankW,
|
||||
hasFloorPlan() ? height : blankH,
|
||||
state.view,
|
||||
) || state.view;
|
||||
applyViewTransform();
|
||||
}
|
||||
|
||||
function updateSheetSize() {
|
||||
if (!sheetEl || !imageEl) return;
|
||||
if (!imageEl.hidden && imageEl.naturalWidth) {
|
||||
sheetEl.style.width = `${imageEl.naturalWidth}px`;
|
||||
sheetEl.style.minHeight = `${imageEl.naturalHeight}px`;
|
||||
function zoomBy(factor, anchorClientX, anchorClientY) {
|
||||
dismissCanvasTip();
|
||||
const vpRect = viewportEl?.getBoundingClientRect();
|
||||
if (!vpRect) return;
|
||||
const anchorVx = anchorClientX != null ? anchorClientX - vpRect.left : vpRect.width / 2;
|
||||
const anchorVy = anchorClientY != null ? anchorClientY - vpRect.top : vpRect.height / 2;
|
||||
state.view = Geo()?.zoomViewAt(state.view, anchorVx, anchorVy, factor) || state.view;
|
||||
applyViewTransform();
|
||||
}
|
||||
|
||||
/** Layer 2 — sheet sized 1:1 with PNG pixels. */
|
||||
function updateImageLayer() {
|
||||
if (!sheetEl || !imageLayerEl) return;
|
||||
const has = hasFloorPlan();
|
||||
const { width, height } = floorPlanSize();
|
||||
|
||||
sheetEl.classList.toggle("mapEditorSheet--hasImage", has);
|
||||
sheetEl.classList.toggle("mapEditorSheet--blank", !has);
|
||||
|
||||
if (has && width && height) {
|
||||
sheetEl.style.width = `${width}px`;
|
||||
sheetEl.style.height = `${height}px`;
|
||||
imageLayerEl.style.width = `${width}px`;
|
||||
imageLayerEl.style.height = `${height}px`;
|
||||
} else {
|
||||
sheetEl.style.width = "";
|
||||
sheetEl.style.height = "";
|
||||
sheetEl.style.minWidth = "480px";
|
||||
sheetEl.style.minHeight = "360px";
|
||||
imageLayerEl.style.width = "";
|
||||
imageLayerEl.style.height = "";
|
||||
}
|
||||
|
||||
if (gridEl && has) {
|
||||
const mapForLayer = mapMetaForOriginDisplay() || state.map;
|
||||
const steps = Geo()?.gridSteps(mapForLayer) || { minor: 20, major: 100 };
|
||||
gridEl.style.setProperty("--map-grid-minor", `${steps.minor}px`);
|
||||
gridEl.style.setProperty("--map-grid-major", `${steps.major}px`);
|
||||
gridEl.hidden = false;
|
||||
} else if (gridEl) {
|
||||
gridEl.hidden = true;
|
||||
}
|
||||
|
||||
if (originEl) originEl.hidden = !has;
|
||||
updateOriginMarker();
|
||||
updateStatusBar();
|
||||
}
|
||||
|
||||
/** Origin fields → world (0,0) on the floor plan (ROS map_server). */
|
||||
function mapMetaForOriginDisplay() {
|
||||
if (!state.map) return null;
|
||||
const base = { ...state.map };
|
||||
if (uploadMetaDialogEl?.open) {
|
||||
const m = readUploadMetaPayload();
|
||||
return { ...base, ...m };
|
||||
}
|
||||
if (settingsDialogEl?.open || state.dirty) {
|
||||
const s = readSettingsPayload();
|
||||
return { ...base, ...s };
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
function updateOriginMarker() {
|
||||
if (!originEl) return;
|
||||
const geo = Geo();
|
||||
const { width, height } = floorPlanSize();
|
||||
if (!geo || !hasFloorPlan() || !width || !height) {
|
||||
originEl.hidden = true;
|
||||
originEl.setAttribute("aria-hidden", "true");
|
||||
return;
|
||||
}
|
||||
|
||||
const mapMeta = mapMetaForOriginDisplay();
|
||||
const pt = geo.worldToPixel(mapMeta, width, height, 0, 0);
|
||||
const ox = Number(mapMeta?.origin_x) || 0;
|
||||
const oy = Number(mapMeta?.origin_y) || 0;
|
||||
const oyaw = Number(mapMeta?.origin_yaw) || 0;
|
||||
const yawDeg = (-oyaw * 180) / Math.PI;
|
||||
const onMap = pt.x >= -2 && pt.y >= -2 && pt.x <= width + 2 && pt.y <= height + 2;
|
||||
|
||||
originEl.hidden = false;
|
||||
originEl.setAttribute("aria-hidden", "false");
|
||||
originEl.classList.toggle("mapEditorOrigin--offMap", !onMap);
|
||||
originEl.style.left = `${pt.x}px`;
|
||||
originEl.style.top = `${pt.y}px`;
|
||||
originEl.style.transform = `rotate(${yawDeg}deg)`;
|
||||
|
||||
const labelEl = el("mapEditorOriginLabel");
|
||||
if (labelEl) {
|
||||
labelEl.textContent = t("maps.editor.originLabelShort", {
|
||||
x: ox.toFixed(2),
|
||||
y: oy.toFixed(2),
|
||||
});
|
||||
}
|
||||
originEl.title = t("maps.editor.originTooltip", {
|
||||
x: ox.toFixed(3),
|
||||
y: oy.toFixed(3),
|
||||
yaw: ((oyaw * 180) / Math.PI).toFixed(1),
|
||||
});
|
||||
}
|
||||
|
||||
function updateStatusBar(pointerClient) {
|
||||
const geo = Geo();
|
||||
const { width, height } = floorPlanSize();
|
||||
const pct = Math.round((state.view.scale || 1) * 100);
|
||||
|
||||
if (statusViewEl) {
|
||||
statusViewEl.textContent = t("maps.editor.statusView", {
|
||||
zoom: pct,
|
||||
panX: Math.round(state.view.panX),
|
||||
panY: Math.round(state.view.panY),
|
||||
});
|
||||
}
|
||||
|
||||
if (!geo || !hasFloorPlan() || !pointerClient) {
|
||||
if (statusImageEl) statusImageEl.textContent = t("maps.editor.statusImageIdle");
|
||||
if (statusWorldEl) statusWorldEl.textContent = t("maps.editor.statusWorldIdle");
|
||||
return;
|
||||
}
|
||||
|
||||
const sheetRect = sheetEl?.getBoundingClientRect();
|
||||
const imgPt = geo.clientToImage(pointerClient.x, pointerClient.y, sheetRect, width, height);
|
||||
if (!imgPt) {
|
||||
if (statusImageEl) statusImageEl.textContent = t("maps.editor.statusImageIdle");
|
||||
if (statusWorldEl) statusWorldEl.textContent = t("maps.editor.statusWorldIdle");
|
||||
return;
|
||||
}
|
||||
|
||||
const world = geo.pixelToWorld(state.map, width, height, imgPt.x, imgPt.y);
|
||||
if (statusImageEl) {
|
||||
statusImageEl.textContent = t("maps.editor.statusImage", {
|
||||
px: Math.round(imgPt.x),
|
||||
py: Math.round(imgPt.y),
|
||||
});
|
||||
}
|
||||
if (statusWorldEl) {
|
||||
statusWorldEl.textContent = t("maps.editor.statusWorld", {
|
||||
x: world.x.toFixed(2),
|
||||
y: world.y.toFixed(2),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,11 +319,11 @@
|
||||
if (emptyEl) emptyEl.hidden = false;
|
||||
}
|
||||
updateMenuActionsUi();
|
||||
updateSheetSize();
|
||||
updateImageLayer();
|
||||
imageEl?.addEventListener(
|
||||
"load",
|
||||
() => {
|
||||
updateSheetSize();
|
||||
updateImageLayer();
|
||||
fitToView();
|
||||
},
|
||||
{ once: true },
|
||||
@@ -222,6 +393,7 @@
|
||||
state.dirty = false;
|
||||
state.tipVisible = true;
|
||||
state.activeTool = "pan";
|
||||
state.view = Geo()?.createView(1, 0, 0) || { scale: 1, panX: 0, panY: 0 };
|
||||
if (tipEl) {
|
||||
tipEl.hidden = false;
|
||||
tipEl.textContent = t("maps.editor.canvasTip");
|
||||
@@ -236,9 +408,12 @@
|
||||
state.mapId = null;
|
||||
state.map = null;
|
||||
state.callbacks = {};
|
||||
state.uploadMeta = null;
|
||||
menuDialogEl?.close();
|
||||
settingsDialogEl?.close();
|
||||
activateDialogEl?.close();
|
||||
uploadConfirmDialogEl?.close();
|
||||
uploadMetaDialogEl?.close();
|
||||
}
|
||||
|
||||
function loadImageDimensions(file) {
|
||||
@@ -257,15 +432,119 @@
|
||||
});
|
||||
}
|
||||
|
||||
function readUploadMetaPayload() {
|
||||
return {
|
||||
resolution: Number(uploadMetaFields.resolution?.value) || 0.05,
|
||||
origin_x: Number(uploadMetaFields.originX?.value) || 0,
|
||||
origin_y: Number(uploadMetaFields.originY?.value) || 0,
|
||||
origin_yaw: Number(uploadMetaFields.originYaw?.value) || 0,
|
||||
negate: Number(uploadMetaFields.negate?.value) || 0,
|
||||
occupied_thresh: Number(uploadMetaFields.occupiedThresh?.value) || 0.65,
|
||||
free_thresh: Number(uploadMetaFields.freeThresh?.value) || 0.196,
|
||||
};
|
||||
}
|
||||
|
||||
function fillUploadMetaForm(meta) {
|
||||
const m = meta || {};
|
||||
if (uploadMetaFields.resolution) {
|
||||
uploadMetaFields.resolution.value = m.resolution != null ? m.resolution : 0.05;
|
||||
}
|
||||
if (uploadMetaFields.originX) uploadMetaFields.originX.value = m.origin_x != null ? m.origin_x : 0;
|
||||
if (uploadMetaFields.originY) uploadMetaFields.originY.value = m.origin_y != null ? m.origin_y : 0;
|
||||
if (uploadMetaFields.originYaw) uploadMetaFields.originYaw.value = m.origin_yaw != null ? m.origin_yaw : 0;
|
||||
if (uploadMetaFields.negate) uploadMetaFields.negate.value = m.negate != null ? m.negate : 0;
|
||||
if (uploadMetaFields.occupiedThresh) {
|
||||
uploadMetaFields.occupiedThresh.value = m.occupied_thresh != null ? m.occupied_thresh : 0.65;
|
||||
}
|
||||
if (uploadMetaFields.freeThresh) {
|
||||
uploadMetaFields.freeThresh.value = m.free_thresh != null ? m.free_thresh : 0.196;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchExistingYamlMeta() {
|
||||
if (!state.map?.yaml_file) return null;
|
||||
try {
|
||||
const res = await fetch(`/api/maps/${encodeURIComponent(state.map.id)}/yaml`, {
|
||||
credentials: "include",
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const text = await res.text();
|
||||
const parsed = window.MapYaml?.parse(text);
|
||||
if (!parsed || parsed.error) return null;
|
||||
return parsed;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function openUploadMetaDialog() {
|
||||
const map = state.map;
|
||||
const defaults = {
|
||||
resolution: map?.resolution != null ? map.resolution : 0.05,
|
||||
origin_x: map?.origin_x != null ? map.origin_x : 0,
|
||||
origin_y: map?.origin_y != null ? map.origin_y : 0,
|
||||
origin_yaw: map?.origin_yaw != null ? map.origin_yaw : 0,
|
||||
negate: 0,
|
||||
occupied_thresh: 0.65,
|
||||
free_thresh: 0.196,
|
||||
};
|
||||
const fromYaml = await fetchExistingYamlMeta();
|
||||
fillUploadMetaForm(fromYaml || defaults);
|
||||
menuDialogEl?.close();
|
||||
uploadMetaDialogEl?.showModal();
|
||||
updateOriginMarker();
|
||||
}
|
||||
|
||||
function beginUploadOverwrite() {
|
||||
if (!state.map || state.readOnly) return;
|
||||
if (state.map.image_file) {
|
||||
const textEl = el("mapUploadConfirmText");
|
||||
if (textEl) textEl.textContent = t("maps.uploadConfirm.text");
|
||||
menuDialogEl?.close();
|
||||
uploadConfirmDialogEl?.showModal();
|
||||
return;
|
||||
}
|
||||
openUploadMetaDialog().catch((e) => alert(e.message));
|
||||
}
|
||||
|
||||
function applyYamlToUploadForm(text) {
|
||||
const parsed = window.MapYaml?.parse(text);
|
||||
if (!parsed || parsed.error) {
|
||||
alert(t("maps.uploadMeta.invalidYaml"));
|
||||
return;
|
||||
}
|
||||
fillUploadMetaForm(parsed);
|
||||
}
|
||||
|
||||
async function saveYamlForMap(imageFilename) {
|
||||
const meta = state.uploadMeta || readUploadMetaPayload();
|
||||
const yamlText = window.MapYaml?.serialize({
|
||||
...meta,
|
||||
image: imageFilename || "map.png",
|
||||
});
|
||||
if (!yamlText) return;
|
||||
await api(`/api/maps/${encodeURIComponent(state.map.id)}/yaml`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "text/yaml; charset=utf-8" },
|
||||
body: yamlText,
|
||||
});
|
||||
}
|
||||
|
||||
async function uploadImage(file) {
|
||||
if (!state.map || !file || state.readOnly) return;
|
||||
if (!/\.png$/i.test(file.name)) {
|
||||
alert(t("maps.error.pngOnly"));
|
||||
return;
|
||||
}
|
||||
const meta = state.uploadMeta || readUploadMetaPayload();
|
||||
if (!meta.resolution || meta.resolution <= 0) {
|
||||
alert(t("maps.uploadMeta.invalidResolution"));
|
||||
return;
|
||||
}
|
||||
const dims = await loadImageDimensions(file);
|
||||
const pngName = file.name.endsWith(".png") ? file.name : `${file.name}.png`;
|
||||
const form = new FormData();
|
||||
form.append("file", file, file.name.endsWith(".png") ? file.name : `${file.name}.png`);
|
||||
form.append("file", file, pngName);
|
||||
const res = await fetch(`/api/maps/${encodeURIComponent(state.map.id)}/image`, {
|
||||
credentials: "include",
|
||||
method: "POST",
|
||||
@@ -286,16 +565,28 @@
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
...readSettingsPayload(),
|
||||
resolution: meta.resolution,
|
||||
origin_x: meta.origin_x,
|
||||
origin_y: meta.origin_y,
|
||||
origin_yaw: meta.origin_yaw,
|
||||
width: dims.width,
|
||||
height: dims.height,
|
||||
}),
|
||||
});
|
||||
try {
|
||||
await saveYamlForMap(pngName);
|
||||
updated = (await api(`/api/maps/${encodeURIComponent(state.map.id)}`)) || updated;
|
||||
} catch {
|
||||
/* yaml save is best-effort */
|
||||
}
|
||||
state.uploadMeta = null;
|
||||
state.map = updated;
|
||||
state.callbacks.onMapUpdated?.(updated);
|
||||
setDirty(false);
|
||||
fillSettingsForm();
|
||||
renderMapImage();
|
||||
menuDialogEl?.close();
|
||||
uploadMetaDialogEl?.close();
|
||||
promptActivate();
|
||||
}
|
||||
|
||||
@@ -315,6 +606,7 @@
|
||||
state.callbacks.onMapUpdated?.(updated);
|
||||
setDirty(false);
|
||||
updateHeader();
|
||||
updateImageLayer();
|
||||
menuDialogEl?.close();
|
||||
promptActivate();
|
||||
}
|
||||
@@ -341,15 +633,32 @@
|
||||
}
|
||||
|
||||
function bindCanvasPanZoom() {
|
||||
canvasWrapEl?.addEventListener("wheel", (evt) => {
|
||||
const blockNativeDrag = (evt) => {
|
||||
evt.preventDefault();
|
||||
};
|
||||
|
||||
imageEl?.addEventListener("dragstart", blockNativeDrag);
|
||||
sheetEl?.addEventListener("dragstart", blockNativeDrag);
|
||||
imageLayerEl?.addEventListener("dragstart", blockNativeDrag);
|
||||
|
||||
viewportEl?.addEventListener("wheel", (evt) => {
|
||||
evt.preventDefault();
|
||||
dismissCanvasTip();
|
||||
const factor = evt.deltaY < 0 ? 1.1 : 0.9;
|
||||
zoomBy(factor);
|
||||
zoomBy(factor, evt.clientX, evt.clientY);
|
||||
}, { passive: false });
|
||||
|
||||
canvasWrapEl?.addEventListener("mousedown", (evt) => {
|
||||
viewportEl?.addEventListener("mousemove", (evt) => {
|
||||
updateStatusBar({ x: evt.clientX, y: evt.clientY });
|
||||
});
|
||||
|
||||
viewportEl?.addEventListener("mouseleave", () => {
|
||||
updateStatusBar();
|
||||
});
|
||||
|
||||
viewportEl?.addEventListener("mousedown", (evt) => {
|
||||
if (evt.button !== 0 || state.activeTool !== "pan") return;
|
||||
evt.preventDefault();
|
||||
dismissCanvasTip();
|
||||
state.panning = {
|
||||
startX: evt.clientX,
|
||||
@@ -372,6 +681,11 @@
|
||||
state.panning = null;
|
||||
updateCanvasCursor();
|
||||
});
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
if (!state.mapId) return;
|
||||
applyViewTransform();
|
||||
});
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
@@ -394,6 +708,7 @@
|
||||
el("mapEditorSettingsBtn")?.addEventListener("click", () => {
|
||||
fillSettingsForm();
|
||||
settingsDialogEl?.showModal();
|
||||
updateOriginMarker();
|
||||
});
|
||||
el("mapEditorSaveBtn")?.addEventListener("click", () => {
|
||||
saveMap().catch((e) => alert(e.message));
|
||||
@@ -403,12 +718,17 @@
|
||||
el("mapEditorCenterBtn")?.addEventListener("click", () => {
|
||||
dismissCanvasTip();
|
||||
centerSheetInView();
|
||||
applyViewTransform();
|
||||
});
|
||||
el("mapEditorZoomInBtn")?.addEventListener("click", () => zoomBy(1.2));
|
||||
el("mapEditorZoomOutBtn")?.addEventListener("click", () => zoomBy(1 / 1.2));
|
||||
el("mapEditorZoomInBtn")?.addEventListener("click", () => {
|
||||
const rect = viewportEl?.getBoundingClientRect();
|
||||
zoomBy(1.2, rect ? rect.left + rect.width / 2 : undefined, rect ? rect.top + rect.height / 2 : undefined);
|
||||
});
|
||||
el("mapEditorZoomOutBtn")?.addEventListener("click", () => {
|
||||
const rect = viewportEl?.getBoundingClientRect();
|
||||
zoomBy(1 / 1.2, rect ? rect.left + rect.width / 2 : undefined, rect ? rect.top + rect.height / 2 : undefined);
|
||||
});
|
||||
|
||||
el("mapMenuUploadOverwrite")?.addEventListener("click", () => uploadInputEl?.click());
|
||||
el("mapMenuUploadOverwrite")?.addEventListener("click", () => beginUploadOverwrite());
|
||||
el("mapMenuDownload")?.addEventListener("click", () => {
|
||||
const url = mapImageUrl(state.map);
|
||||
if (!url) return;
|
||||
@@ -426,11 +746,59 @@
|
||||
uploadImage(file).catch((e) => alert(e.message));
|
||||
});
|
||||
|
||||
el("mapUploadConfirmYesBtn")?.addEventListener("click", () => {
|
||||
uploadConfirmDialogEl?.close();
|
||||
openUploadMetaDialog().catch((e) => alert(e.message));
|
||||
});
|
||||
el("mapUploadConfirmNoBtn")?.addEventListener("click", () => uploadConfirmDialogEl?.close());
|
||||
uploadConfirmDialogEl?.addEventListener("cancel", (evt) => {
|
||||
evt.preventDefault();
|
||||
uploadConfirmDialogEl?.close();
|
||||
});
|
||||
|
||||
el("mapUploadImportYamlBtn")?.addEventListener("click", () => uploadYamlInputEl?.click());
|
||||
uploadYamlInputEl?.addEventListener("change", () => {
|
||||
const file = uploadYamlInputEl.files?.[0];
|
||||
uploadYamlInputEl.value = "";
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
applyYamlToUploadForm(String(reader.result || ""));
|
||||
updateOriginMarker();
|
||||
};
|
||||
reader.onerror = () => alert(t("maps.uploadMeta.invalidYaml"));
|
||||
reader.readAsText(file);
|
||||
});
|
||||
|
||||
el("mapUploadMetaCancelBtn")?.addEventListener("click", () => {
|
||||
state.uploadMeta = null;
|
||||
uploadMetaDialogEl?.close();
|
||||
updateOriginMarker();
|
||||
});
|
||||
uploadMetaDialogEl?.addEventListener("cancel", (evt) => {
|
||||
evt.preventDefault();
|
||||
state.uploadMeta = null;
|
||||
uploadMetaDialogEl?.close();
|
||||
updateOriginMarker();
|
||||
});
|
||||
el("mapUploadMetaForm")?.addEventListener("submit", (evt) => {
|
||||
evt.preventDefault();
|
||||
const meta = readUploadMetaPayload();
|
||||
if (!meta.resolution || meta.resolution <= 0) {
|
||||
alert(t("maps.uploadMeta.invalidResolution"));
|
||||
return;
|
||||
}
|
||||
state.uploadMeta = meta;
|
||||
uploadMetaDialogEl?.close();
|
||||
uploadInputEl?.click();
|
||||
});
|
||||
|
||||
el("mapEditorSettingsForm")?.addEventListener("submit", (evt) => {
|
||||
evt.preventDefault();
|
||||
if (!state.map) return;
|
||||
Object.assign(state.map, readSettingsPayload());
|
||||
setDirty(true);
|
||||
updateImageLayer();
|
||||
updateHeader();
|
||||
settingsDialogEl?.close();
|
||||
});
|
||||
@@ -441,12 +809,26 @@
|
||||
el("mapActivateNoBtn")?.addEventListener("click", () => activateDialogEl?.close());
|
||||
|
||||
Object.values(settingsFields).forEach((node) => {
|
||||
node?.addEventListener("input", () => setDirty(true));
|
||||
node?.addEventListener("input", () => {
|
||||
setDirty(true);
|
||||
updateOriginMarker();
|
||||
});
|
||||
});
|
||||
|
||||
Object.values(uploadMetaFields).forEach((node) => {
|
||||
node?.addEventListener("input", () => {
|
||||
updateOriginMarker();
|
||||
if (node === uploadMetaFields.resolution && uploadMetaDialogEl?.open) {
|
||||
updateImageLayer();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener("lm:locale-change", () => {
|
||||
if (state.tipVisible && tipEl) tipEl.textContent = t("maps.editor.canvasTip");
|
||||
updateHeader();
|
||||
updateStatusBar();
|
||||
updateOriginMarker();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user