Complete Table host model coverage

Maps rows, columns, merged ranges, style and placement through the generic
generator (now with tuple and usize support), adds grid/merge/reference
validation, and keeps DXF cell merge dimensions in step with merged_ranges.
staged_python_table_lifecycle_over_real_ipc passes the full gate including
DWG and DXF reopen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Felix RiestraandClaude Sonnet 5 committed 2026-09-20 14:36:12 +02:00
1 parent b3f5388208
commit 0206c7c30b
12 files changed
+428 -12

No files matched your search

+1 -1
View File
@@ -94,7 +94,7 @@ The host-owned catalog classifies all 48 entity variants and tracks every
traced field, including fields marked `unmapped`. Query it with
`doc.coverage()` or `doc.coverage("Line")`; each row includes source field,
type, access, and current validation status. Geometry edits remain limited to
the 26 mapped kinds.
the 27 mapped kinds.
For any entity, `entity.snapshot` returns a detached dictionary of its raw
CAD fields, including `snapshot["common"]` and kind-specific nested values.
It can inspect an unmapped kind; editing the dictionary does
+52
View File
@@ -201,6 +201,10 @@ fn helper_closure(manifest: &Manifest, registry: &TypeRegistry) -> Vec<String> {
}
while let Some(name) = queue.pop() {
if let Some(parts) = tuple_parts(&name) {
queue.extend(parts.into_iter().map(str::to_owned));
continue;
}
if BUILTIN.contains(&name.as_str())
|| SPECIAL.contains(&name.as_str())
|| f64_array_len(&name).is_some()
@@ -300,6 +304,26 @@ fn is_native(type_id: &str) -> bool {
BUILTIN.contains(&type_id)
}
/// Split a tuple type id such as `(u32,CellBorder)` into its element types.
fn tuple_parts(type_id: &str) -> Option<Vec<&str>> {
let inner = type_id.strip_prefix('(')?.strip_suffix(')')?;
let mut parts = Vec::new();
let (mut depth, mut start) = (0i32, 0usize);
for (i, ch) in inner.char_indices() {
match ch {
'(' | '[' | '<' => depth += 1,
')' | ']' | '>' => depth -= 1,
',' if depth == 0 => {
parts.push(inner[start..i].trim());
start = i + 1;
}
_ => {}
}
}
parts.push(inner[start..].trim());
(parts.len() >= 2).then_some(parts)
}
/// `[f64; N]` fixed arrays (e.g. a 4x4 transform) convert as flat float lists.
fn f64_array_len(type_id: &str) -> Option<usize> {
type_id
@@ -313,6 +337,10 @@ fn default_expr(type_id: &str) -> String {
if let Some(n) = f64_array_len(type_id) {
return format!("[0.0f64; {n}]");
}
if let Some(parts) = tuple_parts(type_id) {
let items: Vec<String> = parts.iter().map(|part| default_expr(part)).collect();
return format!("({})", items.join(", "));
}
match type_id {
"f64" | "f32" => "0.0".to_string(),
"i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" => "0".to_string(),
@@ -339,6 +367,14 @@ fn leaf_to_py(registry: &TypeRegistry, type_id: &str, item_expr: &str) -> String
"Ok(vm.ctx.new_list(({item_expr}).iter().map(|item| vm.new_pyobj(*item)).collect()).into())"
);
}
if let Some(parts) = tuple_parts(type_id) {
let items: Vec<String> = parts
.iter()
.enumerate()
.map(|(i, part)| format!("{}?", leaf_to_py(registry, part, &format!("&({item_expr}).{i}"))))
.collect();
return format!("Ok(vm.ctx.new_list(vec![{}]).into())", items.join(", "));
}
match type_id {
"f64" | "f32" | "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "bool" => {
format!("Ok(vm.new_pyobj(*{parenthesized}))")
@@ -365,12 +401,28 @@ fn leaf_to_py(registry: &TypeRegistry, type_id: &str, item_expr: &str) -> String
/// `value_expr` must already be an owned `PyObjectRef` expression. Result: a `PyResult<T>` expression.
fn leaf_from_py(registry: &TypeRegistry, type_id: &str, value_expr: &str) -> String {
if type_id == "u64" {
// serde reports Rust `usize` as `u64`; the inferred cast serves both.
return format!("{value_expr}.try_into_value::<u64>(vm).map(|n| n as _)");
}
if is_native(type_id) {
return format!("{value_expr}.try_into_value::<{}>(vm)", native_rust_type(type_id));
}
if let Some(n) = f64_array_len(type_id) {
return format!("py_to_f64_array::<{n}>({value_expr}, vm)");
}
if let Some(parts) = tuple_parts(type_id) {
let n = parts.len();
let items: Vec<String> = parts
.iter()
.enumerate()
.map(|(i, part)| format!("{}?", leaf_from_py(registry, part, &format!("items.remove(0 * {i})"))))
.collect();
return format!(
"(|| -> PyResult<_> {{ let mut items: Vec<PyObjectRef> = ({value_expr}).try_into_value(vm)?; if items.len() != {n} {{ return Err(vm.new_value_error(\"expected a {n}-item list\".to_owned())); }} Ok(({})) }})()",
items.join(", ")
);
}
match type_id {
"Vector3" => format!("py_to_vector3_dict({value_expr}, vm)"),
"Vector2" => format!("py_to_vector2({value_expr}, vm)"),
+31 -1
View File
@@ -25,7 +25,8 @@
"Leader",
"MLine",
"Dimension",
"MultiLeader"
"MultiLeader",
"Table"
],
"manual_kinds": ["Dimension"],
"base_fields": ["handle", "layer", "owner_handle"],
@@ -280,6 +281,35 @@
"rust_setter": "if let Some(v) = dict.get_item_opt(\"context\", vm)? { VAR.context = merge_multileader_context(&VAR.context, v, vm)?; }"
}
}
},
"Table": {
"fields": {
"insertion_point": { "required": true },
"block_name": { "read_only": true },
"block_record_handle": { "read_only": true },
"data_version": { "read_only": true },
"value_flags": { "read_only": true },
"override_flag": { "read_only": true },
"override_border_color": { "read_only": true },
"override_border_line_weight": { "read_only": true },
"override_border_visibility": { "read_only": true },
"legacy_style_override": { "read_only": true },
"name": { "read_only": true },
"description": { "read_only": true },
"field_handles": { "read_only": true },
"base_style": { "read_only": true },
"break_data": { "read_only": true },
"break_ranges": { "read_only": true },
"dwg_unknown_byte": { "exclude": true },
"dwg_unknown_handle": { "exclude": true },
"dwg_unknown_long1": { "exclude": true },
"dwg_r2010_unknown_bit": { "exclude": true },
"dwg_unknown_long2": { "exclude": true },
"dwg_unknown_short": { "exclude": true },
"legacy_border_colors": { "exclude": true, "_comment": "Generic LegacyBorderOverrides<T> is not expressible by the generator; legacy R13-R2000 border overrides are preserved untouched." },
"legacy_border_line_weights": { "exclude": true },
"legacy_border_visibility": { "exclude": true }
}
}
}
}
+1 -1
View File
@@ -4756,7 +4756,7 @@ mod tests {
);
}
}
assert_eq!(emitted.len(), 26);
assert_eq!(emitted.len(), 27);
}
#[cfg(feature = "experimental-host-model")]