use acadrust::entities::Table; use glam::Vec3; use crate::command::EntityTransform; use crate::entities::common::{ro_prop as ro, square_grip}; use crate::entities::text_support::{ layout_mtext, MTextRenderOpts, MTextVAnchor, ResolvedTextStyle, }; use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible}; use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection, Property, PropValue}; use crate::scene::view::transform; use crate::scene::model::wire_model::SnapHint; fn v3(v: &acadrust::types::Vector3) -> Vec3 { Vec3::new(v.x as f32, v.y as f32, v.z as f32) } impl TruckConvertible for Table { fn to_truck(&self, document: &acadrust::CadDocument) -> Option { if self.rows.is_empty() || self.columns.is_empty() { return None; } // Lay the table out in a local frame with the origin at zero. The world // insertion point is added back as f64 at the widening step below, so // large coordinates (UTM etc.) keep full precision instead of snapping // onto the coarse f32 grid — which would collide cell-text baselines and // overflow the integer border-dedup keys. let base = [ self.insertion_point.x, self.insertion_point.y, self.insertion_point.z, ]; let origin = Vec3::ZERO; let h_raw = v3(&self.horizontal_direction); let h = if h_raw.length_squared() > 1e-10 { h_raw.normalize() } else { Vec3::X }; // Perpendicular "down" direction in the drawing plane (tables grow downward) let v_down = Vec3::new(h.y, -h.x, 0.0); let col_offsets: Vec = { let mut off = 0.0f32; let mut v = vec![0.0f32]; for col in &self.columns { off += col.width as f32; v.push(off); } v }; let total_w = *col_offsets.last().unwrap_or(&0.0); let row_offsets: Vec = { let mut off = 0.0f32; let mut v = vec![0.0f32]; for row in &self.rows { off += row.height as f32; v.push(off); } v }; let total_h = *row_offsets.last().unwrap_or(&0.0); let mut pts: Vec<[f32; 3]> = Vec::new(); let mut tris_pts: Vec<[f32; 3]> = Vec::new(); // Per-cell borders. When a cell carries a CellStyle, honour the // visibility / `invisible` flag of each of its four borders so // hidden borders disappear from the grid. Cells with no style still // emit the standard four borders. To avoid drawing each shared edge // twice we coalesce the segments by their (start, end) coordinates. use rustc_hash::FxHashSet as HashSet; let mut emitted: HashSet<(i32, i32, i32, i32)> = HashSet::default(); let try_add = |a: Vec3, b: Vec3, vis: bool, emitted: &mut HashSet<(i32, i32, i32, i32)>, pts: &mut Vec<[f32; 3]>| { if !vis { return; } let key = ( (a.x * 1_000.0) as i32, (a.y * 1_000.0) as i32, (b.x * 1_000.0) as i32, (b.y * 1_000.0) as i32, ); let key_rev = (key.2, key.3, key.0, key.1); if emitted.contains(&key) || emitted.contains(&key_rev) { return; } emitted.insert(key); if !pts.is_empty() { pts.push([f32::NAN; 3]); } pts.push([a.x, a.y, a.z]); pts.push([b.x, b.y, b.z]); }; for (ri, row) in self.rows.iter().enumerate() { let row_top = row_offsets[ri]; let row_bot = row_offsets .get(ri + 1) .copied() .unwrap_or(row_top + row.height as f32); for (ci, cell) in row.cells.iter().enumerate() { let col_left = col_offsets[ci]; let col_right = col_offsets.get(ci + 1).copied().unwrap_or( col_left + self.columns.get(ci).map(|c| c.width as f32).unwrap_or(1.0), ); // Default to visible when no style override is present. let (top_vis, right_vis, bottom_vis, left_vis) = cell .style .as_ref() .map(|s| { ( !s.top_border.invisible, !s.right_border.invisible, !s.bottom_border.invisible, !s.left_border.invisible, ) }) .unwrap_or((true, true, true, true)); let tl = origin + h * col_left + v_down * row_top; let tr = origin + h * col_right + v_down * row_top; let br_ = origin + h * col_right + v_down * row_bot; let bl = origin + h * col_left + v_down * row_bot; try_add(tl, tr, top_vis, &mut emitted, &mut pts); try_add(tr, br_, right_vis, &mut emitted, &mut pts); try_add(bl, br_, bottom_vis, &mut emitted, &mut pts); try_add(tl, bl, left_vis, &mut emitted, &mut pts); } } // Suppress unused-variable warnings now that the simple grid-pass // is gone — col/row offsets still feed cell drawing below. let _ = (total_w, total_h); // Cell text — resolve defaults via TableStyle, then layer per-cell // overrides on top. Resolution order (text height, text style, alignment): // 1. CellContent.* (per-content explicit override) // 2. CellStyle.* (per-cell explicit override) // 3. TableStyle._row_style.* (table-wide default for this row class) // 4. compiled-in fallback (0.18 / "txt" / MiddleCenter) // // Row classification: row 0 is Title (when not suppressed), row 1 is // Header (when not suppressed), everything else is Data. The two // suppressed flags shift the leading rows down to Data. let lookup_style = |h: acadrust::Handle| -> Option<&acadrust::tables::TextStyle> { document.text_styles.iter().find(|s| s.handle == h) }; let table_style: Option<&acadrust::objects::TableStyle> = self.table_style_handle.and_then(|h| { document.objects.get(&h).and_then(|obj| match obj { acadrust::objects::ObjectType::TableStyle(ts) => Some(ts), _ => None, }) }); let title_suppressed = table_style.map(|t| t.title_suppressed).unwrap_or(false); let header_suppressed = table_style.map(|t| t.header_suppressed).unwrap_or(false); let font_for_handle = |handle: Option| -> Option { handle.and_then(|h| lookup_style(h)).and_then(|s| { let mut font_name = if !s.true_type_font.trim().is_empty() { s.true_type_font.trim().to_string() } else { let file = s.font_file.trim(); if !file.is_empty() { let basename = file.rsplit(['/', '\\']).next().unwrap_or(file); let stem = basename.split('.').next().unwrap_or(basename).trim(); if !stem.is_empty() { stem.to_string() } else { return None; } } else { return None; } }; if !crate::scene::text::lff::is_builtin(&font_name) { if let Some(canonical) = crate::scene::text::sysfont::canonical_family_name(&font_name) { font_name = canonical; } } Some(font_name) }) }; // Build a ResolvedTextStyle for the cell — needed by the shared MText // pipeline so inline `\W`, `\Q`, etc. compose with the style baseline. let resolved_style_for_handle = |handle: Option, font_name: String| -> ResolvedTextStyle { let style = handle.and_then(|h| lookup_style(h)); ResolvedTextStyle { font_name, width_factor: style.map(|s| s.width_factor as f32).unwrap_or(1.0), oblique_angle: style.map(|s| s.oblique_angle as f32).unwrap_or(0.0), is_backward: style.map(|s| s.is_backward()).unwrap_or(false), is_upside_down: style.map(|s| s.is_upside_down()).unwrap_or(false), } }; for (ri, row) in self.rows.iter().enumerate() { let row_top = row_offsets[ri]; let row_bot = row_offsets .get(ri + 1) .copied() .unwrap_or(row_top + row.height as f32); let row_mid = (row_top + row_bot) * 0.5; // Pick the appropriate row_style from TableStyle for this row's role. let row_style: Option<&acadrust::objects::RowCellStyle> = table_style.map(|ts| { let kind = match (title_suppressed, header_suppressed, ri) { (false, _, 0) => 0, // title (false, false, 1) => 1, // header (true, false, 0) => 1, // header pulled up _ => 2, // data }; match kind { 0 => &ts.title_row_style, 1 => &ts.header_row_style, _ => &ts.data_row_style, } }); for (ci, cell) in row.cells.iter().enumerate() { let text = cell.text_value(); if text.is_empty() { continue; } let col_left = col_offsets[ci]; let col_width = self.columns.get(ci).map(|c| c.width as f32).unwrap_or(1.0); let col_right = col_left + col_width; // Resolve text height: content → cell-style → row-style → 0.18. let content = cell.contents.first(); let cell_h = content .map(|c| c.text_height) .filter(|h| *h > 1e-6) .or_else(|| { cell.style .as_ref() .map(|s| s.text_height) .filter(|h| *h > 1e-6) }) .or_else(|| row_style.map(|s| s.text_height).filter(|h| *h > 1e-6)) .map(|h| h as f32) .unwrap_or(0.18); let margin = cell_h * 0.5_f32; // Resolve text-style handle: content → cell-style → row-style. let style_handle = content .and_then(|c| c.text_style_handle) .or_else(|| cell.style.as_ref().and_then(|s| s.text_style_handle)) .or_else(|| row_style.and_then(|s| s.text_style_handle)); let font_owned = font_for_handle(style_handle).unwrap_or_else(|| "txt".to_string()); let resolved = resolved_style_for_handle(style_handle, font_owned); // Alignment resolution: cell.style.alignment (1-9) overrides; // otherwise fall back to row_style.alignment, then MiddleCenter. let align = cell .style .as_ref() .map(|s| s.alignment) .filter(|a| *a != 0) .or_else(|| row_style.map(|s| s.alignment as i32)) .unwrap_or(5); let horiz = ((align - 1).rem_euclid(3)) + 1; // 1=left, 2=center, 3=right let vert = ((align - 1) / 3) + 1; // 1=top, 2=middle, 3=bottom // Position the cell's MText block anchor at the requested // alignment corner / midpoint of the cell's content area. let (x_offset, attach_h_anchor) = match horiz { 1 => (col_left + margin, 0.0_f32), 3 => (col_right - margin, 1.0_f32), _ => (col_left + col_width * 0.5, 0.5_f32), }; let (y_offset, v_anchor) = match vert { 1 => (row_top + margin, MTextVAnchor::Top), 3 => (row_bot - margin, MTextVAnchor::Bottom), _ => (row_mid, MTextVAnchor::Middle), }; let text_origin = origin + h * x_offset + v_down * y_offset; // Content rotation (radians) on top of table cell rotation. let rot = content.map(|c| c.rotation as f32).unwrap_or(0.0) + cell.rotation as f32; let layout = layout_mtext(&MTextRenderOpts { value: text, insertion: [text_origin.x as f64, text_origin.y as f64, origin.z as f64], height: cell_h, rect_w: 0.0, rotation: rot, style: &resolved, attach_h_anchor, v_anchor, line_spacing_factor: 1.0, vertical_text: false, want_glyph_boxes: false, }); // Flatten TextStroke groups into the table's Lines buffer. // Per-run inline `\C` / `\c` colour is dropped here because the // table emits a single TruckObject::Lines for borders + text; // tracking it would require splitting the table into multiple // WireModels per cell colour. Borders + uniform-coloured runs // honour the entity's outer colour. for ts in &layout.strokes { let ox = ts.origin[0] as f32; let oy = ts.origin[1] as f32; for stroke in &ts.strokes { if stroke.len() < 2 { continue; } if !pts.is_empty() { pts.push([f32::NAN; 3]); } for &[x, y] in stroke { pts.push([x + ox, y + oy, origin.z]); } } for &[x, y] in &ts.fill_tris { tris_pts.push([x + ox, y + oy, origin.z]); } } } } // The layout above is in a local f32 frame (small magnitudes). Widen to // f64 and add the world insertion so the absolute position carries full // precision; tessellate.rs then applies world_offset. let pts_f64: Vec<[f64; 3]> = pts .into_iter() .map(|[x, y, z]| { if x.is_nan() { [f64::NAN, f64::NAN, f64::NAN] } else { [x as f64 + base[0], y as f64 + base[1], z as f64 + base[2]] } }) .collect(); let fill_tris_f64: Vec<[f64; 3]> = tris_pts .into_iter() .map(|[x, y, z]| { [x as f64 + base[0], y as f64 + base[1], z as f64 + base[2]] }) .collect(); Some(TruckEntity { object: TruckObject::Lines(pts_f64), snap_pts: vec![(glam::DVec3::new(self.insertion_point.x, self.insertion_point.y, self.insertion_point.z), SnapHint::Insertion)], tangent_geoms: vec![], key_vertices: vec![], fill_tris: fill_tris_f64, }) } } /// Coloured synthesis render for tables WITHOUT a baked block (e.g. tables /// created in-app). Emits one `WireModel` per distinct colour for cell fills, /// per-cell text, and grid borders, honouring every `TableStyle` / /// `RowCellStyle` / per-cell `CellStyle` field: fill colour + enable, text /// colour, border type/weight/colour/visibility (incl. inside borders), /// margins, and flow direction. Imported tables keep using AutoCAD's baked /// block (see scene/mod.rs). pub fn tessellate_table( tab: &Table, document: &acadrust::CadDocument, selected: bool, entity_color: [f32; 4], line_weight_px: f32, ) -> Vec { use crate::scene::convert::tess_util::aci_to_rgba; use crate::scene::model::wire_model::WireModel; use acadrust::types::Color; use rustc_hash::FxHashMap as HashMap; if tab.rows.is_empty() || tab.columns.is_empty() { return Vec::new(); } let rel = |p: Vec3| -> [f32; 3] { [ (p.x as f64) as f32, (p.y as f64) as f32, (p.z as f64) as f32, ] }; let resolve_col = |c: &Color, fallback: [f32; 4]| -> [f32; 4] { match c { Color::ByLayer | Color::ByBlock => fallback, _ => aci_to_rgba(c), } }; let key4 = |c: [f32; 4]| -> [u8; 4] { [ (c[0] * 255.0) as u8, (c[1] * 255.0) as u8, (c[2] * 255.0) as u8, (c[3] * 255.0) as u8, ] }; let lw_px = |w: &acadrust::types::LineWeight| -> f32 { match w { acadrust::types::LineWeight::Value(v) if *v >= 0 => (*v as f32 / 100.0) * (96.0 / 25.4), _ => line_weight_px, } }; let origin = v3(&tab.insertion_point); let h_raw = v3(&tab.horizontal_direction); let h = if h_raw.length_squared() > 1e-10 { h_raw.normalize() } else { Vec3::X }; let v_down = Vec3::new(h.y, -h.x, 0.0); // Flow direction: `Up` stacks rows upward instead of downward. let table_style: Option<&acadrust::objects::TableStyle> = tab.table_style_handle.and_then(|h| { document.objects.get(&h).and_then(|obj| match obj { acadrust::objects::ObjectType::TableStyle(ts) => Some(ts), _ => None, }) }); let flow_up = matches!( table_style.map(|t| t.flow_direction), Some(acadrust::objects::TableFlowDirection::Up) ); let v_flow = if flow_up { -v_down } else { v_down }; let col_offsets: Vec = { let mut off = 0.0f32; let mut v = vec![0.0f32]; for col in &tab.columns { off += col.width as f32; v.push(off); } v }; let row_offsets: Vec = { let mut off = 0.0f32; let mut v = vec![0.0f32]; for row in &tab.rows { off += row.height as f32; v.push(off); } v }; let title_suppressed = table_style.map(|t| t.title_suppressed).unwrap_or(false); let header_suppressed = table_style.map(|t| t.header_suppressed).unwrap_or(false); let h_margin = table_style .map(|t| t.horizontal_margin as f32) .unwrap_or(0.0); let v_margin = table_style.map(|t| t.vertical_margin as f32).unwrap_or(0.0); let lookup_style = |hh: acadrust::Handle| -> Option<&acadrust::tables::TextStyle> { document.text_styles.iter().find(|s| s.handle == hh) }; let font_for_handle = |handle: Option| -> Option { handle.and_then(lookup_style).and_then(|s| { let mut font_name = if !s.true_type_font.trim().is_empty() { s.true_type_font.trim().to_string() } else { let file = s.font_file.trim(); let basename = file.rsplit(['/', '\\']).next().unwrap_or(file); let stem = basename.split('.').next().unwrap_or(basename).trim(); if !stem.is_empty() { stem.to_string() } else { return None; } }; if !crate::scene::text::lff::is_builtin(&font_name) { if let Some(canonical) = crate::scene::text::sysfont::canonical_family_name(&font_name) { font_name = canonical; } } Some(font_name) }) }; let resolved_style_for_handle = |handle: Option, font_name: String| -> ResolvedTextStyle { let style = handle.and_then(lookup_style); ResolvedTextStyle { font_name, width_factor: style.map(|s| s.width_factor as f32).unwrap_or(1.0), oblique_angle: style.map(|s| s.oblique_angle as f32).unwrap_or(0.0), is_backward: style.map(|s| s.is_backward()).unwrap_or(false), is_upside_down: style.map(|s| s.is_upside_down()).unwrap_or(false), } }; // Accumulators keyed by quantised colour (+ weight for borders). let mut fills: HashMap<[u8; 4], ([f32; 4], Vec<[f32; 3]>)> = HashMap::default(); let mut texts: HashMap<[u8; 4], ([f32; 4], Vec<[f32; 3]>, Vec<[f32; 3]>)> = HashMap::default(); let mut borders: HashMap<([u8; 4], u32), ([f32; 4], f32, Vec<[f32; 3]>)> = HashMap::default(); let mut emitted: rustc_hash::FxHashSet<(i32, i32, i32, i32)> = rustc_hash::FxHashSet::default(); let sel_col = WireModel::SELECTED; let mut add_edge = |a: Vec3, b: Vec3, col: [f32; 4], lw: f32| { let k = ( (a.x * 1000.0) as i32, (a.y * 1000.0) as i32, (b.x * 1000.0) as i32, (b.y * 1000.0) as i32, ); let kr = (k.2, k.3, k.0, k.1); if emitted.contains(&k) || emitted.contains(&kr) { return; } emitted.insert(k); let entry = borders .entry((key4(col), (lw * 100.0) as u32)) .or_insert_with(|| (col, lw, Vec::new())); if !entry.2.is_empty() { entry.2.push([f32::NAN; 3]); } entry.2.push(rel(a)); entry.2.push(rel(b)); }; for (ri, row) in tab.rows.iter().enumerate() { let row_top = row_offsets[ri]; let row_bot = row_offsets .get(ri + 1) .copied() .unwrap_or(row_top + row.height as f32); let row_mid = (row_top + row_bot) * 0.5; let row_style: Option<&acadrust::objects::RowCellStyle> = table_style.map(|ts| { let kind = match (title_suppressed, header_suppressed, ri) { (false, _, 0) => 0, (false, false, 1) => 1, (true, false, 0) => 1, _ => 2, }; match kind { 0 => &ts.title_row_style, 1 => &ts.header_row_style, _ => &ts.data_row_style, } }); for (ci, cell) in row.cells.iter().enumerate() { let col_left = col_offsets[ci]; let col_width = tab.columns.get(ci).map(|c| c.width as f32).unwrap_or(1.0); let col_right = col_left + col_width; let tl = origin + h * col_left + v_flow * row_top; let tr = origin + h * col_right + v_flow * row_top; let br_ = origin + h * col_right + v_flow * row_bot; let bl = origin + h * col_left + v_flow * row_bot; // ── Fill ────────────────────────────────────────────────────── let (fill_on, fill_color) = if let Some(cs) = &cell.style { (cs.fill_enabled, cs.background_color) } else if let Some(rs) = row_style { (rs.fill_enabled, rs.fill_color) } else { (false, Color::ByLayer) }; if fill_on { let col = resolve_col(&fill_color, entity_color); let buf = &mut fills .entry(key4(col)) .or_insert_with(|| (col, Vec::new())) .1; for v in [bl, br_, tr, bl, tr, tl] { buf.push(rel(v)); } } // ── Borders (per edge: cell override → row style → default) ─── // (top, right, bottom, left) let cell_b = cell.style.as_ref(); let edge = |which: u8| -> (bool, [f32; 4], f32) { if let Some(cs) = cell_b { let b = match which { 0 => &cs.top_border, 1 => &cs.right_border, 2 => &cs.bottom_border, _ => &cs.left_border, }; ( !b.invisible, if selected { sel_col } else { resolve_col(&b.color, entity_color) }, lw_px(&b.line_weight), ) } else if let Some(rs) = row_style { let b = match which { 0 => &rs.top_border, 1 => &rs.right_border, 2 => &rs.bottom_border, _ => &rs.left_border, }; ( !b.is_invisible, if selected { sel_col } else { resolve_col(&b.color, entity_color) }, lw_px(&b.line_weight), ) } else { ( true, if selected { sel_col } else { entity_color }, line_weight_px, ) } }; let (tv, tc, tw) = edge(0); if tv { add_edge(tl, tr, tc, tw); } let (rv, rc, rw) = edge(1); if rv { add_edge(tr, br_, rc, rw); } let (bv, bc, bw) = edge(2); if bv { add_edge(bl, br_, bc, bw); } let (lv, lc, lw) = edge(3); if lv { add_edge(tl, bl, lc, lw); } // ── Text ────────────────────────────────────────────────────── let text = cell.text_value(); if text.is_empty() { continue; } let content = cell.contents.first(); let cell_h = content .map(|c| c.text_height) .filter(|h| *h > 1e-6) .or_else(|| { cell.style .as_ref() .map(|s| s.text_height) .filter(|h| *h > 1e-6) }) .or_else(|| row_style.map(|s| s.text_height).filter(|h| *h > 1e-6)) .map(|h| h as f32) .unwrap_or(0.18); let m_h = if h_margin > 1e-6 { h_margin } else { cell_h * 0.5 }; let m_v = if v_margin > 1e-6 { v_margin } else { cell_h * 0.5 }; let style_handle = content .and_then(|c| c.text_style_handle) .or_else(|| cell.style.as_ref().and_then(|s| s.text_style_handle)) .or_else(|| row_style.and_then(|s| s.text_style_handle)); let font_owned = font_for_handle(style_handle).unwrap_or_else(|| "txt".to_string()); let resolved = resolved_style_for_handle(style_handle, font_owned); let align = cell .style .as_ref() .map(|s| s.alignment) .filter(|a| *a != 0) .or_else(|| row_style.map(|s| s.alignment as i32)) .unwrap_or(5); let horiz = ((align - 1).rem_euclid(3)) + 1; let vert = ((align - 1) / 3) + 1; let (x_offset, attach_h_anchor) = match horiz { 1 => (col_left + m_h, 0.0_f32), 3 => (col_right - m_h, 1.0_f32), _ => (col_left + col_width * 0.5, 0.5_f32), }; let (y_offset, v_anchor) = match vert { 1 => (row_top + m_v, MTextVAnchor::Top), 3 => (row_bot - m_v, MTextVAnchor::Bottom), _ => (row_mid, MTextVAnchor::Middle), }; let to = origin + h * x_offset + v_flow * y_offset; let rot = content.map(|c| c.rotation as f32).unwrap_or(0.0) + cell.rotation as f32; let layout = layout_mtext(&MTextRenderOpts { value: text, insertion: [(to.x as f64), (to.y as f64), (to.z as f64)], height: cell_h, rect_w: 0.0, rotation: rot, style: &resolved, attach_h_anchor, v_anchor, line_spacing_factor: 1.0, vertical_text: false, want_glyph_boxes: false, }); let tcol = if selected { sel_col } else if let Some(cs) = &cell.style { resolve_col(&cs.content_color, entity_color) } else if let Some(rs) = row_style { resolve_col(&rs.text_color, entity_color) } else { entity_color }; let entry = texts .entry(key4(tcol)) .or_insert_with(|| (tcol, Vec::new(), Vec::new())); let buf = &mut entry.1; let tris_buf = &mut entry.2; for ts in &layout.strokes { let sx = ts.origin[0] as f32; let sy = ts.origin[1] as f32; for stroke in &ts.strokes { if stroke.len() < 2 { continue; } if !buf.is_empty() { buf.push([f32::NAN; 3]); } for &[x, y] in stroke { buf.push([x + sx, y + sy, (to.z as f64) as f32]); } } for &[x, y] in &ts.fill_tris { tris_buf.push([x + sx, y + sy, (to.z as f64) as f32]); } } } } let name = tab.common.handle.value().to_string(); let mk = |color: [f32; 4], points: Vec<[f32; 3]>, fill_tris: Vec<[f32; 3]>, lw: f32| -> WireModel { WireModel { name: name.clone(), points, points_low: Vec::new(), color, selected, pattern_length: 0.0, pattern: [0.0; 8], line_weight_px: lw, aci: 0, snap_pts: vec![], tangent_geoms: vec![], key_vertices: vec![], aabb: WireModel::UNBOUNDED_AABB, plinegen: true, vp_scissor: None, fill_tris, // fill_tris_low intentionally empty: this fill renders on the // top-level path, where consumers (face3d_gpu, xclip) treat a // short low half as all-zero, so it draws at f32 precision // (sub-metre error at UTM scale) — not a crash. Follow-up: // double-single-split via points_to_ds to match emit_wire. fill_tris_low: Vec::new(), } }; let mut out: Vec = Vec::new(); // Fills first (drawn under borders/text). for (_, (color, tris)) in fills { if !tris.is_empty() { out.push(mk(color, vec![], tris, 1.0)); } } for (_, (color, lw, pts)) in borders { if !pts.is_empty() { out.push(mk(color, pts, vec![], lw)); } } for (_, (color, pts, tris)) in texts { if !pts.is_empty() { out.push(mk(color, pts, vec![], line_weight_px)); } if !tris.is_empty() { out.push(mk(color, vec![], tris, line_weight_px)); } } out } impl Grippable for Table { fn grips(&self) -> Vec { vec![square_grip( 0, glam::DVec3::new( self.insertion_point.x, self.insertion_point.y, self.insertion_point.z, ), )] } fn apply_grip(&mut self, grip_id: usize, apply: GripApply) { if grip_id == 0 { match apply { GripApply::Translate(d) => { self.insertion_point.x += d.x as f64; self.insertion_point.y += d.y as f64; self.insertion_point.z += d.z as f64; } GripApply::Absolute(p) => { self.insertion_point.x = p.x as f64; self.insertion_point.y = p.y as f64; self.insertion_point.z = p.z as f64; } } } } } impl PropertyEditable for Table { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { use crate::entities::common::edit_prop as edit; use acadrust::entities::table::BreakOptionFlags; let fmt_h = |oh: &Option| -> String { match oh { Some(h) if !h.is_null() => format!("{:X}", h.value()), _ => "(none)".to_string(), } }; let toggle = |label: &'static str, field: &'static str, b: bool| -> Property { Property { label: label.into(), field, value: PropValue::BoolToggle { field, value: b }, } }; // Direction = angle of the horizontal direction vector in the XY plane. let direction_deg = (self.horizontal_direction.y.atan2(self.horizontal_direction.x)).to_degrees(); vec![ PropSection { title: "Table".into(), props: vec![ ro( "Table style", "tbl_style_handle", fmt_h(&self.table_style_handle), ), ro("Rows", "tbl_rows", self.rows.len().to_string()), ro("Columns", "tbl_cols", self.columns.len().to_string()), ro("Direction", "tbl_direction", format!("{:.4}", direction_deg)), ro( "Table width", "tbl_width", format!("{:.4}", self.total_width()), ), ro( "Table height", "tbl_height", format!("{:.4}", self.total_height()), ), ], }, PropSection { title: "Table Breaks".into(), props: vec![ toggle( "Enabled", "tbl_break_enabled", self.break_options.contains(BreakOptionFlags::ENABLE_BREAKS), ), ro( "Direction", "tbl_break_direction", format!("{:?}", self.break_flow_direction), ), toggle( "Repeat top labels", "tbl_break_repeat_top", self.break_options .contains(BreakOptionFlags::REPEAT_TOP_LABELS), ), toggle( "Repeat bottom labels", "tbl_break_repeat_bottom", self.break_options .contains(BreakOptionFlags::REPEAT_BOTTOM_LABELS), ), toggle( "Manual positions", "tbl_break_manual_positions", self.break_options .contains(BreakOptionFlags::ALLOW_MANUAL_POSITIONS), ), toggle( "Manual heights", "tbl_break_manual_heights", self.break_options .contains(BreakOptionFlags::ALLOW_MANUAL_HEIGHTS), ), ro("Break height", "tbl_break_height", String::new()), edit("Spacing", "tbl_break_spacing", self.break_spacing), ], }, ] } fn apply_geom_prop(&mut self, field: &str, value: &str) { use crate::entities::common::parse_f64; use acadrust::entities::table::BreakOptionFlags; let flag = match field { "tbl_break_enabled" => Some(BreakOptionFlags::ENABLE_BREAKS), "tbl_break_repeat_top" => Some(BreakOptionFlags::REPEAT_TOP_LABELS), "tbl_break_repeat_bottom" => Some(BreakOptionFlags::REPEAT_BOTTOM_LABELS), "tbl_break_manual_positions" => Some(BreakOptionFlags::ALLOW_MANUAL_POSITIONS), "tbl_break_manual_heights" => Some(BreakOptionFlags::ALLOW_MANUAL_HEIGHTS), _ => None, }; if let Some(flag) = flag { let on = if value == "toggle" { !self.break_options.contains(flag) } else { value == "true" }; self.break_options.set(flag, on); return; } if field == "tbl_break_spacing" { if let Some(v) = parse_f64(value) { self.break_spacing = v; } } } } impl Transformable for Table { fn apply_transform(&mut self, t: &EntityTransform) { transform::apply_standard_entity_transform(self, t, |entity, p1, p2| { transform::reflect_xy_point( &mut entity.insertion_point.x, &mut entity.insertion_point.y, p1, p2, ); // Reflect the horizontal direction by reflecting a tip point let mut tip_x = entity.insertion_point.x + entity.horizontal_direction.x; let mut tip_y = entity.insertion_point.y + entity.horizontal_direction.y; transform::reflect_xy_point(&mut tip_x, &mut tip_y, p1, p2); entity.horizontal_direction.x = tip_x - entity.insertion_point.x; entity.horizontal_direction.y = tip_y - entity.insertion_point.y; }); } }