forked from 1b-agent/opencadstudio-upstream
feat(table): coloured synthesis render honouring all TableStyle fields
Tables without a baked block (e.g. created in-app) were drawn as a single monochrome wire, ignoring cell colours/fills/border styling. Add a coloured synthesis path that emits per-colour WireModels for: - cell background fill (fill_color + fill_enabled), - per-cell text in its content/text colour, - grid borders with per-edge type/weight/colour/visibility (cell-style override → row-style → default), - horizontal/vertical margins and flow direction (down/up). Imported tables keep using AutoCAD's baked block. Resolution layers the per-cell CellStyle over the TableStyle row style over defaults. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
90c9ac62c8
commit
1b20d7d781
2 files changed
+379
No files matched your search
@@ -316,6 +316,371 @@ impl TruckConvertible for Table {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
world_offset: [f64; 3],
|
||||||
|
) -> Vec<crate::scene::wire_model::WireModel> {
|
||||||
|
use crate::scene::tess_util::aci_to_rgba;
|
||||||
|
use crate::scene::wire_model::WireModel;
|
||||||
|
use acadrust::types::Color;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
if tab.rows.is_empty() || tab.columns.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let [ox, oy, oz] = world_offset;
|
||||||
|
let rel = |p: Vec3| -> [f32; 3] {
|
||||||
|
[
|
||||||
|
(p.x as f64 - ox) as f32,
|
||||||
|
(p.y as f64 - oy) as f32,
|
||||||
|
(p.z as f64 - oz) 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<f32> = {
|
||||||
|
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<f32> = {
|
||||||
|
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<acadrust::Handle>| -> Option<String> {
|
||||||
|
handle.and_then(lookup_style).and_then(|s| {
|
||||||
|
let file = s.font_file.trim();
|
||||||
|
let basename = file.rsplit(['/', '\\']).next().unwrap_or(file);
|
||||||
|
let stem = basename.split('.').next().unwrap_or(basename).trim();
|
||||||
|
(!stem.is_empty()).then(|| stem.to_string())
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let resolved_style_for_handle =
|
||||||
|
|handle: Option<acadrust::Handle>, 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::new();
|
||||||
|
let mut texts: HashMap<[u8; 4], ([f32; 4], Vec<[f32; 3]>)> = HashMap::new();
|
||||||
|
let mut borders: HashMap<([u8; 4], u32), ([f32; 4], f32, Vec<[f32; 3]>)> = HashMap::new();
|
||||||
|
let mut emitted: std::collections::HashSet<(i32, i32, i32, i32)> = std::collections::HashSet::new();
|
||||||
|
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 - ox), (to.y as f64 - oy), (to.z as f64 - oz)],
|
||||||
|
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 buf = &mut texts.entry(key4(tcol)).or_insert_with(|| (tcol, Vec::new())).1;
|
||||||
|
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 - oz) 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,
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut out: Vec<WireModel> = 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)) in texts {
|
||||||
|
if !pts.is_empty() {
|
||||||
|
out.push(mk(color, pts, vec![], line_weight_px));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
impl Grippable for Table {
|
impl Grippable for Table {
|
||||||
fn grips(&self) -> Vec<GripDef> {
|
fn grips(&self) -> Vec<GripDef> {
|
||||||
vec![square_grip(0, v3(&self.insertion_point))]
|
vec![square_grip(0, v3(&self.insertion_point))]
|
||||||
|
|||||||
@@ -6734,6 +6734,20 @@ fn tessellate_entity(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// No baked block (e.g. a table created in-app) — synthesise coloured
|
||||||
|
// geometry from the rows + TableStyle so fills/colours/borders/margins
|
||||||
|
// are honoured instead of the monochrome fallback.
|
||||||
|
let mut wires = crate::entities::table::tessellate_table(
|
||||||
|
tab, document, sel, entity_color, line_weight_px, world_offset,
|
||||||
|
);
|
||||||
|
if !wires.is_empty() {
|
||||||
|
let aabb = entity_aabb(e, world_offset);
|
||||||
|
for w in &mut wires {
|
||||||
|
w.aci = aci;
|
||||||
|
w.aabb = aabb;
|
||||||
|
}
|
||||||
|
return wires;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let EntityType::Insert(ins) = e {
|
if let EntityType::Insert(ins) = e {
|
||||||
|
|||||||
Reference in new issue
Block a user