From e44a9dc2f140ce391250dd08f7e6507bb2b958d8 Mon Sep 17 00:00:00 2001 From: aiongg Date: Tue, 13 Oct 2026 22:40:35 +0000 Subject: [PATCH] Integration: #846 stroke edges of a partly selected merged cell are classified against the selection grown to whole cells, in table.setCell and table.getCellStroke --- crates/engine/src/cmd/table.rs | 57 +++++++++++++++++--------------- crates/engine/src/tests_table.rs | 30 +++++++++-------- 2 files changed, 48 insertions(+), 39 deletions(-) diff --git a/crates/engine/src/cmd/table.rs b/crates/engine/src/cmd/table.rs index 4b215faa0..6c9092050 100644 --- a/crates/engine/src/cmd/table.rs +++ b/crates/engine/src/cmd/table.rs @@ -940,19 +940,23 @@ fn set_cell(s: &mut Session, p: &Value) -> Result { // Local edits outrank previously imported edges, including explicit zero/None. let priority = if stroke.is_some() { next_cell_stroke_priority(t) } else { 0 }; let rg = g.range; + // Edges are inner or outer within the selection grown to whole merged cells, so a + // merged cell selected through one of its grid positions keeps its own outline. + let outline = t.expand_range(rg); // Each chosen edge starts from the stroke drawn there, which both copies of a shared // edge then take, so the edit changes only the attributes it names. let mut bases = std::collections::HashMap::new(); if let Some((_, edges)) = &stroke { for (r, c) in t.selected_owners(rg) { let Some(cell) = t.cell(r, c) else { continue }; - for (i, on) in edges.sides(rg, (r, c), (cell.row_span, cell.col_span), rtl).into_iter().enumerate() { + for (i, on) in edges.sides(outline, (r, c), (cell.row_span, cell.col_span), rtl).into_iter().enumerate() { if let Some(base) = drawn_side(t, &owners, (r, c), i).filter(|_| on) { bases.insert((r, c, i), base); } } } } + let mut edited = std::collections::HashSet::new(); for (r, c) in t.selected_owners(rg) { let Some(cell) = t.cell_mut(r, c) else { continue }; if let Some(f) = &fill { @@ -974,6 +978,7 @@ fn set_cell(s: &mut Session, p: &Value) -> Result { if let Some((edit, _)) = &stroke { for i in 0..4 { if let Some(base) = bases.remove(&(r, c, i)) { + edited.insert((r, c, i)); cell.strokes[i] = edit.apply(base); cell.stroke_defined[i] = true; cell.stroke_priorities[i] = priority; @@ -982,31 +987,36 @@ fn set_cell(s: &mut Session, p: &Value) -> Result { } } } - // Keep both copies of unmerged shared boundaries consistent. Left/right name - // physical sides, also in RTL tables. + // Keep both copies of unmerged shared boundaries consistent: each edited edge is + // copied to its neighbour. Left/right name physical sides, also in RTL tables. + let mirror = |t: &mut Table, from: (usize, usize), edge: usize, to: (usize, usize), opposite: usize| { + if edited.contains(&(from.0, from.1, edge)) { + mirror_cell_edge(t, &owners, from, edge, to, opposite); + } + }; if let Some((_, edges)) = &stroke { if edges.top && let Some(row) = rg.r0.checked_sub(1) { for c in rg.c0..=rg.c1 { - mirror_cell_edge(t, &owners, (rg.r0, c), 0, (row, c), 2); + mirror(t, (rg.r0, c), 0, (row, c), 2); } } if edges.bottom && rg.r1.saturating_add(1) < nr { for c in rg.c0..=rg.c1 { - mirror_cell_edge(t, &owners, (rg.r1, c), 2, (rg.r1 + 1, c), 0); + mirror(t, (rg.r1, c), 2, (rg.r1 + 1, c), 0); } } if (if rtl { edges.right } else { edges.left }) && let Some(col) = rg.c0.checked_sub(1) { for r in rg.r0..=rg.r1 { - mirror_cell_edge(t, &owners, (r, rg.c0), if rtl { 3 } else { 1 }, (r, col), if rtl { 1 } else { 3 }); + mirror(t, (r, rg.c0), if rtl { 3 } else { 1 }, (r, col), if rtl { 1 } else { 3 }); } } if (if rtl { edges.left } else { edges.right }) && rg.c1.saturating_add(1) < nc { for r in rg.r0..=rg.r1 { - mirror_cell_edge(t, &owners, (r, rg.c1), if rtl { 1 } else { 3 }, (r, rg.c1 + 1), if rtl { 3 } else { 1 }); + mirror(t, (r, rg.c1), if rtl { 1 } else { 3 }, (r, rg.c1 + 1), if rtl { 3 } else { 1 }); } } } @@ -1023,7 +1033,7 @@ fn get_cell_stroke(s: &mut Session, p: &Value) -> Result { let st = s.doc()?; let t = st.doc.story(g.story).and_then(|x| x.tables.get(&g.table)).ok_or_else(|| bad(ID, "no table"))?; let owners = t.owners(); - let nc = t.ncols(); + let outline = t.expand_range(g.range); let rtl = t.options.direction == designcraft_doc::TextDirection::RightToLeft; // One value per key, or null once two edges (cells) disagree. fn merge(slot: &mut Option, v: Value) { @@ -1038,28 +1048,23 @@ fn get_cell_stroke(s: &mut Session, p: &Value) -> Result { let (mut fill, mut fill_tint) = (None, None); // Each drawn edge once: a shared edge is a side of both its cells. let mut seen = std::collections::HashSet::new(); - for r in g.range.r0..=g.range.r1 { - for c in g.range.c0..=g.range.c1 { - if owners.get(r * nc + c) != Some(&(r, c)) { + for (r, c) in t.selected_owners(g.range) { + let Some(cell) = t.cell(r, c) else { continue }; + merge(&mut fill, json!(cell.fill)); + merge(&mut fill_tint, json!(cell.fill_tint)); + for (i, on) in edges.sides(outline, (r, c), (cell.row_span, cell.col_span), rtl).into_iter().enumerate() { + if !on { continue; } - let Some(cell) = t.cell(r, c) else { continue }; - merge(&mut fill, json!(cell.fill)); - merge(&mut fill_tint, json!(cell.fill_tint)); - for (i, on) in edges.sides(g.range, (r, c), (cell.row_span, cell.col_span), rtl).into_iter().enumerate() { - if !on { + for side in compose::table::cell_side_strokes(t, &owners, (r, c), i) { + let (x, y) = (Some((r, c)), side.neighbor); + if !seen.insert((i % 2, side.boundary, x.min(y), x.max(y))) { continue; } - for side in compose::table::cell_side_strokes(t, &owners, (r, c), i) { - let (x, y) = (Some((r, c)), side.neighbor); - if !seen.insert((i % 2, side.boundary, x.min(y), x.max(y))) { - continue; - } - let e = side.stroke; - let values = [json!(e.weight), json!(e.color), json!(e.tint), json!(e.kind), json!(e.gap_color), json!(e.gap_tint)]; - for (slot, v) in stroke.iter_mut().zip(values) { - merge(slot, v); - } + let e = side.stroke; + let values = [json!(e.weight), json!(e.color), json!(e.tint), json!(e.kind), json!(e.gap_color), json!(e.gap_tint)]; + for (slot, v) in stroke.iter_mut().zip(values) { + merge(slot, v); } } } diff --git a/crates/engine/src/tests_table.rs b/crates/engine/src/tests_table.rs index c89ce9818..c63c67ecd 100644 --- a/crates/engine/src/tests_table.rs +++ b/crates/engine/src/tests_table.rs @@ -209,7 +209,9 @@ fn selected_merged_owners_receive_cell_options_and_unique_style_counts() { } #[test] -fn covered_owner_strokes_keep_literal_edge_masks_and_neighbors() { +fn selecting_part_of_a_merged_cell_strokes_its_whole_outline() { + // Row 0 is one merged cell; selecting its second grid position selects the whole cell, so + // all four of its edges are outer edges. for rtl in [false, true] { for outer in [false, true] { let (mut s, sid, tid, _) = selected_merged_table(true); @@ -218,24 +220,26 @@ fn covered_owner_strokes_keep_literal_edge_masks_and_neighbors() { } s.execute("table.select", &json!({"rows": [0, 0], "cols": [1, 1]})).unwrap(); let before = table_for(&s, sid, tid).clone(); - s.execute("table.setCell", &json!({"stroke": {"weight": 3, "edges": if outer { "outer" } else { "inner" }}})).unwrap(); + let edges = if outer { "outer" } else { "inner" }; + s.execute("table.setCell", &json!({"stroke": {"weight": 3, "edges": edges}})).unwrap(); let after = table_for(&s, sid, tid); - let mask = if rtl { [true, true, true, false] } else { [true, false, true, true] }; - let expected = mask.map(|edge| if outer { edge } else { !edge }); let owner = after.cell(0, 0).unwrap(); - assert_eq!(owner.border_overrides, expected); - assert_eq!(owner.stroke_defined, expected); - for (i, on) in expected.into_iter().enumerate() { - if on { - assert_eq!(owner.strokes[i].weight, 3.0); - assert_eq!(owner.stroke_priorities[i], 1); - } else { - assert_eq!(owner.strokes[i], before.cell(0, 0).unwrap().strokes[i]); - } + if outer { + assert_eq!(owner.border_overrides, [true; 4]); + assert_eq!(owner.stroke_defined, [true; 4]); + assert!(owner.strokes.iter().all(|e| e.weight == 3.0)); + assert_eq!(owner.stroke_priorities, [1; 4]); + } else { + assert_eq!(Some(owner), before.cell(0, 0), "a single merged cell has no inner edges"); } for (r, c) in [(0, 1), (1, 0), (1, 1)] { assert_eq!(after.cell(r, c), before.cell(r, c), "strict merged/covered mirror guard preserves {r},{c}"); } + let shown = s.execute("table.getCellStroke", &json!({"edges": "outer"})).unwrap(); + assert!(shown["edges"].as_u64().unwrap() >= 4, "the merged cell's outline is read: {shown}"); + if outer { + assert_eq!(shown["weight"], 3.0); + } assert_eq!(s.doc().unwrap().selection.cells.unwrap().range, doc::CellRange::cell(0, 1)); check(&s); }