Files
OneCAD-Draft/src/modules/annotate/table_cmd.rs
T
PanosandClaude Opus 5 391a74a615 perf: let one tessellation serve every background
Switching to a Layout re-tessellated the whole model — `memo_miss=186455`,
2034 ms — to arrive at geometry identical to what was already in the memo.
The only thing that differed was colour.

`local_wires_for` already knows this: it stores the raw colour and drops
its background argument with `let _ = bg_color;`, noting that
`Batches::finalize` applies `adapt_to_bg` last, "so the cache no longer
has to rebuild on BACKGROUND / layout-switch". The tessellation memo's
guard had not been told, and still mixed the background in, so every
entry was discarded on a switch.

`finalize` now records what its colour resolution consumed — the raw
colour, the raw glyph colours, and the three flags it branches on — as
`WireModel::bg_adapt`, and `resolve_colors_for_bg` replays it for another
background. The guard drops the background, so a memo entry serves any
layout.

The resolution recomputes from the recorded raw colour rather than from
the resolved one, which makes it idempotent and therefore safe over a set
that mixes memo hits with freshly tessellated misses. Re-adapting the
resolved colour would not be safe: `adapt_to_bg` snaps a near-white colour
to pure black on a light background, and pure black comes back pure
*white* on a dark one, losing the tint. A test pins that asymmetry so the
raw colour is not optimised away later.

The contract is tested against the real `finalize` rather than a copy of
its logic: for every background pair, building under one and resolving for
the other lands exactly where building under the other would have.

`bg_adapt` is `None` for wires built anywhere else, and the resolution
skips them, so nothing outside the block cache changes colour. It is also
`None` when the resolution would be a no-op — a preserved colour with no
canvas fill looks the same under every background — which keeps the common
case at no cost.

Ordering is unchanged: the pass runs inside the builder, before
`append_scene_markers` and `apply_refedit_fade`, both of which have their
own separate background dependency.

`wires-build` gains `colors=` so the pass has a number of its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 18:53:16 +03:00

826 lines
26 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// TABLE command — create a styled empty table.
//
// Title/header rows come from the selected table style and are not counted as
// data rows. The command remembers its previous settings and supports either a
// fixed insertion point or a two-corner sizing window.
use acadrust::entities::TableBuilder;
use acadrust::types::Vector3;
use acadrust::EntityType;
use glam::DVec3;
use std::sync::{Mutex, OnceLock};
use crate::command::{CadCommand, CmdResult, InputKind, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
use crate::t;
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/table.svg"));
pub fn tool() -> ToolDef {
ToolDef {
id: "TABLE",
label: "Table",
icon: ICON,
event: ModuleEvent::Command("TABLE".to_string()),
}
}
const DEFAULT_COLS: usize = 3;
const DEFAULT_DATA_ROWS: usize = 4;
const DEFAULT_COL_WIDTH: f64 = 2.0;
const DEFAULT_ROW_HEIGHT: f64 = 0.5;
#[derive(Clone, Copy)]
enum InsertionMode {
Point,
Window,
}
#[derive(Clone, Copy)]
struct TableDefaults {
columns: usize,
data_rows: usize,
column_width: f64,
row_height: f64,
insertion_mode: InsertionMode,
}
impl Default for TableDefaults {
fn default() -> Self {
Self {
columns: DEFAULT_COLS,
data_rows: DEFAULT_DATA_ROWS,
column_width: DEFAULT_COL_WIDTH,
row_height: DEFAULT_ROW_HEIGHT,
insertion_mode: InsertionMode::Point,
}
}
}
fn saved_defaults() -> &'static Mutex<TableDefaults> {
static DEFAULTS: OnceLock<Mutex<TableDefaults>> = OnceLock::new();
DEFAULTS.get_or_init(|| Mutex::new(TableDefaults::default()))
}
#[derive(Clone, Copy)]
enum Step {
Columns,
DataRows { columns: usize },
ColumnWidth { columns: usize, data_rows: usize },
RowHeight {
columns: usize,
data_rows: usize,
column_width: f64,
},
InsertionMode {
columns: usize,
data_rows: usize,
column_width: f64,
row_height: f64,
},
Insertion {
columns: usize,
data_rows: usize,
column_width: f64,
row_height: f64,
},
WindowFirst { columns: usize, data_rows: usize },
WindowSecond {
columns: usize,
data_rows: usize,
first: DVec3,
},
}
pub struct TableCommand {
step: Step,
style_handle: Option<acadrust::Handle>,
suggested_column_width: f64,
suggested_row_height: f64,
preview_scale: f64,
title_row: bool,
header_row: bool,
plane: WorkingPlane,
}
impl TableCommand {
pub fn new() -> Self {
Self {
step: Step::Columns,
style_handle: None,
suggested_column_width: DEFAULT_COL_WIDTH,
suggested_row_height: DEFAULT_ROW_HEIGHT,
preview_scale: 1.0,
title_row: true,
header_row: true,
plane: WorkingPlane::default(),
}
}
pub fn with_style(
style_handle: acadrust::Handle,
style: &acadrust::objects::TableStyle,
annotation_multiplier: f64,
) -> Self {
let text_height = style
.data_row_style
.text_height
.max(style.header_row_style.text_height)
.max(style.title_row_style.text_height)
.max(1.0e-6);
Self {
step: Step::Columns,
style_handle: Some(style_handle),
suggested_column_width: text_height * 8.0 + style.horizontal_margin * 2.0,
suggested_row_height: text_height * 1.5 + style.vertical_margin * 2.0,
preview_scale: if style.annotative {
annotation_multiplier
} else {
1.0
},
title_row: !style.title_suppressed,
header_row: !style.header_suppressed,
plane: WorkingPlane::default(),
}
}
fn defaults(&self) -> TableDefaults {
let mut defaults = saved_defaults().lock().map(|value| *value).unwrap_or_default();
if (defaults.column_width - DEFAULT_COL_WIDTH).abs() < 1.0e-9 {
defaults.column_width = self.suggested_column_width;
}
if (defaults.row_height - DEFAULT_ROW_HEIGHT).abs() < 1.0e-9 {
defaults.row_height = self.suggested_row_height;
}
defaults
}
fn total_rows(&self, data_rows: usize) -> usize {
data_rows + usize::from(self.title_row) + usize::from(self.header_row)
}
fn build_table(
&self,
point: DVec3,
rows: usize,
columns: usize,
column_width: f64,
row_height: f64,
) -> EntityType {
let point = self.plane.to_local(point);
let mut table = TableBuilder::new(rows, columns)
.at(Vector3::new(point.x, point.y, point.z))
.row_height(row_height)
.column_width(column_width)
.build();
table.table_style_handle = self.style_handle;
self.plane.place_entity(EntityType::Table(table))
}
fn preview_grid(
&self,
point: DVec3,
rows: usize,
columns: usize,
column_width: f64,
row_height: f64,
scale: f64,
) -> WireModel {
let point = self.plane.to_local(point);
let width = columns as f64 * column_width * scale;
let height = rows as f64 * row_height * scale;
let mut points = Vec::with_capacity((rows + columns + 2) * 2);
for column in 0..=columns {
let x = column as f64 * column_width * scale;
points.push(self.plane.to_world(point + DVec3::X * x).as_vec3().to_array());
points.push(
self.plane
.to_world(point + DVec3::new(x, -height, 0.0))
.as_vec3()
.to_array(),
);
}
for row in 0..=rows {
let y = -(row as f64 * row_height * scale);
points.push(self.plane.to_world(point + DVec3::Y * y).as_vec3().to_array());
points.push(
self.plane
.to_world(point + DVec3::new(width, y, 0.0))
.as_vec3()
.to_array(),
);
}
WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
dash_align_end: None,
text_verts: Vec::new(),
name: "table_preview".into(),
points,
points_low: Vec::new(),
color: WireModel::CYAN,
selected: false,
pattern_length: 0.0,
pattern: [0.0; 8],
line_weight_px: 1.0,
snap_pts: Vec::new(),
tangent_geoms: Vec::new(),
aci: 0,
key_vertices: Vec::new(),
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
fill_tris: Vec::new(),
fill_tris_low: Vec::new(),
}
}
}
impl CadCommand for TableCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str {
"TABLE"
}
fn prompt(&self) -> String {
let defaults = self.defaults();
match &self.step {
Step::Columns => t!(
"TABLE Enter number of columns [%{cols}]:",
cols = defaults.columns
)
.into_owned(),
Step::DataRows { columns } => t!(
"TABLE Enter number of data rows [%{rows}] (%{cols} cols):",
rows = defaults.data_rows,
cols = columns
)
.into_owned(),
Step::ColumnWidth { .. } => t!(
"TABLE Enter column width [%{width}]:",
width = format!("{:.4}", defaults.column_width)
)
.into_owned(),
Step::RowHeight { .. } => t!(
"TABLE Enter row height [%{height}]:",
height = format!("{:.4}", defaults.row_height)
)
.into_owned(),
Step::InsertionMode { .. } => t!(
"TABLE Insertion behavior [Point/Window] <%{mode}>:",
mode = match defaults.insertion_mode {
InsertionMode::Point => t!("Point"),
InsertionMode::Window => t!("Window"),
}
)
.into_owned(),
Step::Insertion { columns, data_rows, .. } => t!(
"TABLE Specify insertion point [%{cols}×%{rows}]:",
cols = columns,
rows = self.total_rows(*data_rows)
)
.into_owned(),
Step::WindowFirst { .. } => {
t!("%{n} Specify first corner:", n = "TABLE").into_owned()
}
Step::WindowSecond { .. } => {
t!("%{n} Specify opposite corner:", n = "TABLE").into_owned()
}
}
}
fn input_kind(&self) -> InputKind {
if matches!(
self.step,
Step::Columns
| Step::DataRows { .. }
| Step::ColumnWidth { .. }
| Step::RowHeight { .. }
| Step::InsertionMode { .. }
) {
InputKind::SingleToken
} else {
InputKind::Point
}
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let input = text.trim();
let defaults = self.defaults();
match self.step {
Step::Columns => {
let columns = if input.is_empty() {
defaults.columns
} else {
input.parse::<usize>().ok().filter(|value| *value > 0)?
};
self.step = Step::DataRows { columns };
}
Step::DataRows { columns } => {
let data_rows = if input.is_empty() {
defaults.data_rows
} else {
input.parse::<usize>().ok().filter(|value| *value > 0)?
};
self.step = Step::ColumnWidth { columns, data_rows };
}
Step::ColumnWidth { columns, data_rows } => {
let column_width = if input.is_empty() {
defaults.column_width
} else {
input.parse::<f64>().ok().filter(|value| *value > 0.0)?
};
self.step = Step::RowHeight {
columns,
data_rows,
column_width,
};
}
Step::RowHeight { columns, data_rows, column_width } => {
let row_height = if input.is_empty() {
defaults.row_height
} else {
input.parse::<f64>().ok().filter(|value| *value > 0.0)?
};
self.step = Step::InsertionMode {
columns,
data_rows,
column_width,
row_height,
};
}
Step::InsertionMode { columns, data_rows, column_width, row_height } => {
let mode = if input.is_empty() {
defaults.insertion_mode
} else if "POINT".starts_with(&input.to_ascii_uppercase()) {
InsertionMode::Point
} else if "WINDOW".starts_with(&input.to_ascii_uppercase()) {
InsertionMode::Window
} else {
return None;
};
if let Ok(mut saved) = saved_defaults().lock() {
*saved = TableDefaults {
columns,
data_rows,
column_width,
row_height,
insertion_mode: mode,
};
}
self.step = match mode {
InsertionMode::Point => Step::Insertion {
columns,
data_rows,
column_width,
row_height,
},
InsertionMode::Window => Step::WindowFirst { columns, data_rows },
};
}
Step::Insertion { .. } | Step::WindowFirst { .. } | Step::WindowSecond { .. } => {
return None;
}
}
Some(CmdResult::NeedPoint)
}
fn on_enter(&mut self) -> CmdResult {
self.on_text_input("")
.map_or(CmdResult::Cancel, |_| CmdResult::NeedPoint)
}
fn on_point(&mut self, point: DVec3) -> CmdResult {
match self.step {
Step::Insertion {
columns,
data_rows,
column_width,
row_height,
} => CmdResult::CommitAndExit(self.build_table(
point,
self.total_rows(data_rows),
columns,
column_width,
row_height,
)),
Step::WindowFirst { columns, data_rows } => {
self.step = Step::WindowSecond {
columns,
data_rows,
first: point,
};
CmdResult::NeedPoint
}
Step::WindowSecond { columns, data_rows, first } => {
let first = self.plane.to_local(first);
let second = self.plane.to_local(point);
let rows = self.total_rows(data_rows);
let width = (second.x - first.x).abs().max(1.0e-6);
let height = (second.y - first.y).abs().max(1.0e-6);
let top_left = DVec3::new(first.x.min(second.x), first.y.max(second.y), first.z);
CmdResult::CommitAndExit(self.build_table(
self.plane.to_world(top_left),
rows,
columns,
width / columns as f64,
height / rows as f64,
))
}
_ => CmdResult::NeedPoint,
}
}
fn on_mouse_move(&mut self, point: DVec3) -> Option<WireModel> {
match self.step {
Step::Insertion {
columns,
data_rows,
column_width,
row_height,
} => Some(self.preview_grid(
point,
self.total_rows(data_rows),
columns,
column_width,
row_height,
self.preview_scale,
)),
Step::WindowSecond { columns, data_rows, first } => {
let first = self.plane.to_local(first);
let second = self.plane.to_local(point);
let rows = self.total_rows(data_rows);
let width = (second.x - first.x).abs().max(1.0e-6);
let height = (second.y - first.y).abs().max(1.0e-6);
let top_left = DVec3::new(first.x.min(second.x), first.y.max(second.y), first.z);
Some(self.preview_grid(
self.plane.to_world(top_left),
rows,
columns,
width / columns as f64,
height / rows as f64,
1.0,
))
}
_ => None,
}
}
}
inventory::submit!(crate::command::CommandRegistration { names: &["TABLE"] });
/// A table cell resolved from a pick: grid coordinates plus whether its
/// content is locked against editing.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct TableCellHit {
pub row: usize,
pub column: usize,
pub locked: bool,
}
/// Outcome of launching the table cell editor for a pick (double-click or
/// TABLEDIT). Distinguishes "cell found but locked" — the caller should
/// stop, the indicator is armed — from "nothing to edit" — the caller may
/// fall through to other handlers or re-prompt.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum TableCellEditStart {
/// Cell editor launched.
Started,
/// Cell resolved but content-locked/read-only: indicator armed, no
/// editor.
LockedCell,
/// Not a table, or the pick missed the grid.
NoCell,
}
impl TableCellHit {
/// Flat cell index, matching the Properties palette ordering.
pub fn index(&self, table: &acadrust::entities::Table) -> usize {
self.row * table.column_count() + self.column
}
}
/// Resolve the table cell under `click_world`. Shared by the double-click
/// shortcut and the TABLEDIT command so both agree on grid coordinates,
/// flow direction and locked-cell detection. `table_style` is the style
/// object the table references, when the caller has the document at hand;
/// the table's embedded base style covers the common fallbacks.
pub(crate) fn table_cell_at(
table: &acadrust::entities::Table,
table_style: Option<&acadrust::objects::TableStyle>,
click_world: DVec3,
) -> Option<TableCellHit> {
let horizontal = glam::DVec3::new(
table.horizontal_direction.x,
table.horizontal_direction.y,
table.horizontal_direction.z,
)
.normalize_or(glam::DVec3::X);
let normal = glam::DVec3::new(table.normal.x, table.normal.y, table.normal.z)
.normalize_or(glam::DVec3::Z);
let mut down = horizontal.cross(normal).normalize_or(glam::DVec3::NEG_Y);
if crate::entities::table::resolved_flow_up(table, table_style) {
down = -down;
}
let origin = glam::DVec3::new(
table.insertion_point.x,
table.insertion_point.y,
table.insertion_point.z,
);
let relative = click_world - origin;
let x = relative.dot(horizontal);
let y = relative.dot(down);
if x < 0.0 || y < 0.0 {
return None;
}
let column = table
.columns
.iter()
.scan(0.0, |offset, column| {
*offset += column.width;
Some(*offset)
})
.position(|end| x <= end)?;
let row = table
.rows
.iter()
.scan(0.0, |offset, row| {
*offset += row.height;
Some(*offset)
})
.position(|end| y <= end)?;
Some(TableCellHit {
row,
column,
locked: cell_locked(table, row, column),
})
}
/// True when the cell's content may not be edited (locked or read-only).
pub(crate) fn cell_locked(table: &acadrust::entities::Table, row: usize, column: usize) -> bool {
use acadrust::entities::table::CellStateFlags;
table.cell(row, column).is_some_and(|cell| {
cell.state
.intersects(CellStateFlags::CONTENT_LOCKED | CellStateFlags::CONTENT_READ_ONLY)
})
}
/// TABLEDIT — edit a table cell's text by picking it. The pick only
/// collects (table, point); the host resolves the cell and launches
/// `TableCellEditCommand`, so the command-first flow and the double-click
/// shortcut share one resolver and one editor setup.
pub struct TableditCommand;
impl TableditCommand {
pub fn new() -> Self {
Self
}
}
impl Default for TableditCommand {
fn default() -> Self {
Self::new()
}
}
impl CadCommand for TableditCommand {
fn name(&self) -> &'static str {
"TABLEDIT"
}
fn prompt(&self) -> String {
t!("TABLEDIT Select a table cell:").into_owned()
}
fn on_point(&mut self, point: DVec3) -> CmdResult {
CmdResult::EditTableCell {
handle: acadrust::Handle::NULL,
point,
}
}
// A bare Enter at the pick prompt ends the command (AutoCAD leaves the
// select-until-valid loop on Enter).
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
inventory::submit!(crate::command::CommandRegistration {
names: &["TABLEDIT"]
});
pub struct TableCellEditCommand {
handle: acadrust::Handle,
table: acadrust::entities::Table,
row: usize,
column: usize,
}
impl TableCellEditCommand {
pub fn new(
handle: acadrust::Handle,
table: &acadrust::entities::Table,
row: usize,
column: usize,
) -> Self {
Self {
handle,
table: table.clone(),
row,
column,
}
}
}
impl CadCommand for TableCellEditCommand {
fn name(&self) -> &'static str {
"TABLE CELL"
}
fn prompt(&self) -> String {
let current = self
.table
.cell_text(self.row, self.column)
.unwrap_or("")
.replace("\\P", "/n");
t!(
"TABLE CELL Enter text for [%{row},%{column}] <%{current}>:",
row = self.row,
column = self.column,
current = current
)
.into_owned()
}
// Cell content is free-form prose ("Foo Bar"): Space must type a
// literal space instead of submitting, Enter finishes, and
// /n or \n inserts a line break.
fn input_kind(&self) -> InputKind {
InputKind::FreeText
}
fn on_point(&mut self, _point: DVec3) -> CmdResult {
CmdResult::NeedPoint
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
use acadrust::entities::table::CellStateFlags;
let cell = self.table.cell_mut(self.row, self.column)?;
if cell
.state
.intersects(CellStateFlags::CONTENT_LOCKED | CellStateFlags::CONTENT_READ_ONLY)
{
return Some(CmdResult::Cancel);
}
let formatted = text
.replace("/n", "\\P")
.replace("\\n", "\\P")
.replace('\n', "\\P");
cell.set_text(&formatted);
self.table.block_record_handle = None;
Some(CmdResult::ReplaceMany(
vec![(self.handle, vec![EntityType::Table(self.table.clone())])],
Vec::new(),
))
}
// Bare Enter (empty buffer) ends the edit *without* writing — the
// host routes an empty submit here instead of on_text_input(""), so
// pressing Enter on an untouched cell keeps its current content.
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
#[cfg(test)]
mod tabledit_tests {
//! `table_cell_at` is the one resolver shared by the double-click
//! shortcut and the TABLEDIT pick, so its grid math is pinned here.
//! `Table::new` seeds 2.5-unit columns and 0.25-unit rows with the
//! default orientation (horizontal +X, flow down).
use super::{table_cell_at, TableCellEditCommand, TableCellHit, TableditCommand};
use crate::command::{CadCommand, CmdResult};
use acadrust::entities::Table;
use acadrust::entities::table::CellStateFlags;
use acadrust::types::Vector3;
use glam::DVec3;
fn table_2x2() -> Table {
Table::new(Vector3::new(0.0, 0.0, 0.0), 2, 2)
}
#[test]
fn resolves_row_and_column_from_point() {
let table = table_2x2();
// Centers of the four cells.
assert_eq!(
table_cell_at(&table, None, DVec3::new(1.25, -0.125, 0.0)),
Some(TableCellHit {
row: 0,
column: 0,
locked: false
})
);
assert_eq!(
table_cell_at(&table, None, DVec3::new(3.75, -0.125, 0.0)),
Some(TableCellHit {
row: 0,
column: 1,
locked: false
})
);
assert_eq!(
table_cell_at(&table, None, DVec3::new(1.25, -0.375, 0.0)),
Some(TableCellHit {
row: 1,
column: 0,
locked: false
})
);
}
#[test]
fn misses_outside_the_grid() {
let table = table_2x2();
// Before the insertion point, beyond the last column/row, and a
// point in the plane's empty space all miss.
assert_eq!(
table_cell_at(&table, None, DVec3::new(-0.1, -0.1, 0.0)),
None
);
assert_eq!(table_cell_at(&table, None, DVec3::new(6.0, -0.1, 0.0)), None);
assert_eq!(table_cell_at(&table, None, DVec3::new(1.0, -9.0, 0.0)), None);
assert_eq!(table_cell_at(&table, None, DVec3::new(1.0, 1.0, 0.0)), None);
}
#[test]
fn reports_locked_cells() {
let mut table = table_2x2();
if let Some(cell) = table.cell_mut(0, 1) {
cell.state |= CellStateFlags::CONTENT_LOCKED;
}
let hit = table_cell_at(&table, None, DVec3::new(3.75, -0.125, 0.0))
.expect("locked cell still resolves");
assert!(hit.locked);
// Its neighbour stays editable through the same pick path.
assert!(!table_cell_at(&table, None, DVec3::new(1.25, -0.125, 0.0))
.expect("neighbour resolves")
.locked);
}
#[test]
fn tabledit_point_hands_point_to_host() {
let mut cmd = TableditCommand::new();
assert!(!cmd.needs_entity_pick());
match cmd.on_point(DVec3::new(1.0, 2.0, 0.0)) {
CmdResult::EditTableCell {
handle,
point,
} => {
assert_eq!(handle, acadrust::Handle::NULL);
assert_eq!(point, DVec3::new(1.0, 2.0, 0.0));
}
_ => panic!("expected EditTableCell"),
}
// A bare Enter at the pick prompt ends the command.
assert!(matches!(cmd.on_enter(), CmdResult::Cancel));
}
#[test]
fn table_cell_edit_formats_newlines() {
let table = table_2x2();
let handle = acadrust::Handle::new(42);
let mut edit_cmd = TableCellEditCommand::new(handle, &table, 0, 0);
let result = edit_cmd
.on_text_input("Line1/nLine2\\nLine3\nLine4")
.expect("cell edit commits");
match result {
CmdResult::ReplaceMany(repl, _) => {
assert_eq!(repl.len(), 1);
if let acadrust::EntityType::Table(t) = &repl[0].1[0] {
assert_eq!(t.cell_text(0, 0).unwrap(), "Line1\\PLine2\\PLine3\\PLine4");
} else {
panic!("expected Table entity");
}
}
_ => panic!("expected ReplaceMany"),
}
}
}