linear bug and osm new features

This commit is contained in:
2026-05-08 18:28:07 +05:30
parent 6eb854444d
commit 2660ef12b9
4 changed files with 781 additions and 23 deletions

View File

@@ -400,7 +400,15 @@ function AppShell({
snap_ignored: number;
};
type OsmUndoEntry =
| { kind: "restore-roads"; snapshots: OsmRoadSnap[]; label: string }
| {
kind: "restore-roads";
snapshots: OsmRoadSnap[];
// Rows that were *created* by the operation (e.g. the right-half row
// from a section split). Undo deletes them before restoring snapshots,
// so the original geometry is the only survivor.
createdIds?: number[];
label: string;
}
| { kind: "unhide"; ids: number[]; label: string }
| { kind: "rehide"; ids: number[]; label: string };
const [osmUndoStack, setOsmUndoStack] = useState<OsmUndoEntry[]>([]);
@@ -423,12 +431,41 @@ function AppShell({
console.warn("osm_road_snapshot failed", e);
}
}
// For ops that mutate AND create new rows (delete_road_section split), the
// caller should snapshot first, run the op, then call this with the ids
// that were freshly inserted so undo can drop them on the way back.
function attachCreatedIdsToLastUndo(createdIds: number[]) {
if (createdIds.length === 0) return;
setOsmUndoStack((prev) => {
if (prev.length === 0) return prev;
const last = prev[prev.length - 1];
if (last.kind !== "restore-roads") return prev;
const next = prev.slice();
next[next.length - 1] = {
...last,
createdIds: [...(last.createdIds ?? []), ...createdIds],
};
return next;
});
}
async function performOsmUndo() {
if (osmUndoStack.length === 0) return;
const last = osmUndoStack[osmUndoStack.length - 1];
setOsmUndoStack((prev) => prev.slice(0, -1));
try {
if (last.kind === "restore-roads") {
// Delete any rows that the operation created before restoring the
// original geometry — otherwise a split-section undo leaves a stale
// duplicate alongside the resurrected original.
if (last.createdIds && last.createdIds.length > 0) {
for (const cid of last.createdIds) {
try {
await invoke("delete_osm_road", { id: cid });
} catch (e) {
console.warn(`undo: delete_osm_road(${cid}) failed`, e);
}
}
}
const n = await invoke<number>("restore_road_state", {
snapshots: last.snapshots,
});
@@ -2069,6 +2106,82 @@ function AppShell({
Math.sin(dLng / 2) ** 2;
return 2 * R * Math.asin(Math.sqrt(s));
};
// OSM-edit-mode lasso: convert shape → polygon and bulk-delete the road
// sections that fall inside it. Skips the asset-bulk-action flow below.
if (osmEditMode) {
const m_per_lat = 111_320;
let polygon: Array<[number, number]> = [];
if (shape === "circle") {
const c = points[0];
const edge = points[1];
const radius_m = haversine(c, edge);
if (radius_m < 1) {
setStatus("Lasso too small.");
return;
}
const m_per_lng = 111_320 * Math.cos(toRad(c[1]));
const N = 64;
for (let i = 0; i < N; i++) {
const a = (i / N) * Math.PI * 2;
polygon.push([
c[0] + (radius_m * Math.cos(a)) / m_per_lng,
c[1] + (radius_m * Math.sin(a)) / m_per_lat,
]);
}
} else if (shape === "rect") {
const [a, b] = points;
const minLng = Math.min(a[0], b[0]);
const maxLng = Math.max(a[0], b[0]);
const minLat = Math.min(a[1], b[1]);
const maxLat = Math.max(a[1], b[1]);
polygon = [
[minLng, minLat],
[maxLng, minLat],
[maxLng, maxLat],
[minLng, maxLat],
];
} else {
if (points.length < 3) {
setStatus("Need at least 3 polygon vertices.");
return;
}
polygon = points.map((p) => [p[0], p[1]] as [number, number]);
}
const ok = window.confirm(
`Bulk-delete OSM road sections inside this lasso?\n\n` +
`Roads fully inside are removed; roads partially inside are clipped at the lasso boundary — the inside portion is dropped, the outside portion(s) survive as separate roads. ` +
`This is NOT covered by the OSM Undo (the ring buffer doesn't recreate deleted rows).`,
);
if (!ok) {
setLassoMode(false);
return;
}
setBusy(true);
invoke<{
roads_examined: number;
roads_deleted: number;
roads_split: number;
new_road_ids: number[];
}>("delete_roads_in_lasso", { polygon })
.then(async (r) => {
setStatus(
`Lasso roads: examined ${r.roads_examined}, fully deleted ${r.roads_deleted}, split ${r.roads_split} (→ ${r.new_road_ids.length} new sub-road(s)).`,
);
// The currently-edited / selected / marked road(s) may have been
// deleted or split — drop those UI references so the next click
// doesn't act on a stale id.
setEditingRoadId(null);
setMarkedVertices(new Set());
setSelectedRoadIds(new Set());
await refreshOsmRoads();
})
.catch((e) => setStatus(`Bulk road delete failed: ${e}`))
.finally(() => {
setBusy(false);
setLassoMode(false);
});
return;
}
const ids: number[] = [];
let label = "";
if (shape === "circle") {
@@ -4667,6 +4780,56 @@ function AppShell({
>
Simplify this road
</button>
<button
className="primary-btn"
style={{
marginTop: 4,
width: "100%",
background: "#c0392b",
fontSize: 11,
}}
disabled={busy || markedVertices.size < 2}
onClick={async () => {
if (editingRoadId === null) return;
const idx = Array.from(markedVertices).sort((a, b) => a - b);
if (idx.length < 2) return;
const ok = window.confirm(
`Delete the road section between vertex #${idx[0]} and vertex #${idx[idx.length - 1]}?\n\n` +
"If both halves keep ≥2 vertices the road splits in two; otherwise the original road is shrunk.",
);
if (!ok) return;
setBusy(true);
try {
await snapshotRoadsForUndo([editingRoadId], "delete section");
const origId = editingRoadId;
const ids = await invoke<number[]>("delete_road_section", {
id: origId,
indices: idx,
});
// Anything other than the original id was freshly inserted
// by the split — attach those to the snapshot so undo
// deletes them before restoring the original geometry.
const createdIds = ids.filter((i) => i !== origId);
if (createdIds.length > 0) {
attachCreatedIdsToLastUndo(createdIds);
}
setMarkedVertices(new Set());
setStatus(
ids.length === 1
? `Section deleted; road #${ids[0]} kept.`
: `Section deleted; road split into #${ids[0]} and #${ids[1]}.`,
);
await refreshOsmRoads();
} catch (e) {
setStatus(`Delete section failed: ${e}`);
} finally {
setBusy(false);
}
}}
title="Drop the marked vertex range from this road. With 2 marks: deletes everything strictly between them. Splits the road into two if both halves still have ≥2 vertices."
>
Delete section between marked
</button>
</div>
{focusedVideo === null && (
<button
@@ -4852,12 +5015,17 @@ function AppShell({
setLassoSelected(null);
setLassoPolygonPoints(null);
setLassoMode((v) => !v);
const turningOn = !lassoMode;
const targetHint = osmEditMode
? "OSM edit mode is on — lasso will bulk-delete road sections, NOT assets."
: "";
setStatus(
lassoMode
!turningOn
? "Lasso off."
: lassoShape === "polygon"
? "Lasso on: click to add polygon vertices, double-click to finish."
: "Lasso on: drag to select.",
: (lassoShape === "polygon"
? "Lasso on: click to add polygon vertices, double-click to finish."
: "Lasso on: drag to select.") +
(targetHint ? ` ${targetHint}` : ""),
);
}}
disabled={busy}