Repository navigation
feat(edit): move coincident polygon vertices together #929
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
db2e79e
4d2da00
6600106
977e982
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| //! Moves every polygon vertex stored at the source coordinate, within one layer. | ||
| //! Exact coordinate equality includes point-only contacts; no edge remapping or tolerance. | ||
|
|
||
| use rustgis_data::{Geometry, VertexId}; | ||
| use rustgis_geom::Coord; | ||
| use serde_json::{Value, json}; | ||
|
|
||
| use super::{Change, apply}; | ||
| use crate::{CmdResult, Session}; | ||
|
|
||
| /// Moves coincident polygon nodes through one design-rule batch and the caller's transaction. | ||
| /// Source addresses match `edit.vertex`; closed rings follow `Geometry::move_vertex`. | ||
| /// Same-position calls still run design rules and retain ordinary command history behaviour. | ||
| pub(super) fn run(s: &mut Session, p: &Value) -> CmdResult { | ||
| let id = s.layer_id(p)?; | ||
| let row = index(p, "row", None)?; | ||
| let vertex = VertexId::new(index(p, "part", Some(0))?, index(p, "ring", Some(0))?, index(p, "index", None)?); | ||
| let lon = super::f(p, "lon").ok_or("lon is required")?; | ||
| let lat = super::f(p, "lat").ok_or("lat is required")?; | ||
| if !(lon.is_finite() && lat.is_finite()) || lon.abs() > 360.0 || lat.abs() > 90.0 { | ||
| return Err("lon and lat are degrees".into()); | ||
| } | ||
| let at = Coord::new(lon, lat); | ||
| let layer = s.map().layer(id).ok_or("no such layer")?; | ||
| let source = layer.dataset.features.get(row).ok_or("no such row")?.geometry.as_ref().ok_or("the feature has no shape")?; | ||
| if !matches!(source, Geometry::Polygon(_)) { | ||
| return Err("shared vertices can only be moved in polygons".into()); | ||
| } | ||
| let original = source.vertex(vertex).ok_or("no such vertex")?; | ||
| // This clone retains the Arc, not a second row table. The first write can still copy the | ||
| // full table, which the undo snapshot also shares; keep these rows for post-rule counts. | ||
| let before = layer.dataset.features.clone(); | ||
| let mut replacements = Vec::new(); | ||
| for (row, feature) in before.iter().enumerate() { | ||
| let Some(geometry) = &feature.geometry else { continue }; | ||
| let Some(moved) = move_matching(geometry, original, at) else { continue }; | ||
| super::super::edit::validate(&moved)?; | ||
| replacements.push((row, moved)); | ||
| } | ||
| let layer = s.map_mut().layer_mut(id).ok_or("no such layer")?; | ||
| let mut changes = Vec::new(); | ||
| for (row, geometry) in replacements { | ||
| let feature = layer.dataset.features.get_mut(row).ok_or("no such row")?; | ||
| changes.push(Change { before: Some(feature.clone()), row: Some(row) }); | ||
| feature.geometry = Some(geometry); | ||
| } | ||
| layer.touch_geometry(); | ||
| let rows: Vec<usize> = changes.iter().filter_map(|c| c.row).collect(); | ||
| apply(s, id, changes)?; | ||
| let layer = s.map().layer(id).ok_or("no such layer")?; | ||
| let modified = rows.iter().filter(|&&row| before.get(row) != layer.dataset.features.get(row)).count(); | ||
| Ok(json!({"part": vertex.part, "ring": vertex.ring, "index": vertex.index, "modified": modified})) | ||
| } | ||
|
|
||
| /// Copies a matching polygon once and rewrites its original-coordinate matches in linear time. | ||
| /// Stored closing duplicates match their first node and move with it, as with `move_vertex`; | ||
| /// open rings stay unnormalized. Non-polygons and polygons without a match need no clone. | ||
| fn move_matching(geometry: &Geometry, original: Coord, at: Coord) -> Option<Geometry> { | ||
| let Geometry::Polygon(parts) = geometry else { return None }; | ||
| if !parts.iter().flatten().flatten().any(|c| *c == original) { | ||
| return None; | ||
| } | ||
| let mut moved = parts.clone(); | ||
| for c in moved.iter_mut().flatten().flatten() { | ||
| if *c == original { | ||
| *c = at; | ||
| } | ||
| } | ||
| Some(Geometry::Polygon(moved)) | ||
| } | ||
|
|
||
| /// Reads a platform-sized address; optional null parts/rings use the same defaults as `edit.vertex`. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit (low confidence): the bounds |
||
| fn index(p: &Value, key: &str, default: Option<usize>) -> Result<usize, String> { | ||
| match p.get(key) { | ||
| None | Some(Value::Null) if default.is_some() => default.ok_or_else(|| format!("{key} is required")), | ||
| Some(v) => v.as_u64().and_then(|n| usize::try_from(n).ok()).ok_or_else(|| format!("{key} must be a whole number")), | ||
| None => Err(format!("{key} is required")), | ||
| } | ||
|
yongyin-leon marked this conversation as resolved.
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment says retaining the shared rows "adds no table copy", but this clone makes the
Arcshared. The firstget_mutbelow goes throughFeatures::deref_mut→Arc::make_mut, which deep-copies the wholeVec<Feature>, geometries included, even when only a few rows change. On a large layer each shared move therefore costs O(table), not O(matches). The session undo snapshot may already force that copy, so I'm not certain it's extra. Either way the comment is misleading. Consider keeping only the changed rows' prior state (you already buildChange.before) and comparing against those formodified. Confidence: medium-low.