fix(arch, table): what the 1.6.0 README renders showed

- The exterior dimension chains follow the plot scale. DIMSCALE is the
  plan's scale while they are drawn and is put back afterwards; a 1:50
  chain's 2.5 text had been 2.5 drawing units, 0.05 mm on the sheet.
- A headless TABLE merges its title row, as AutoCAD's TABLE does; a long
  title no longer wraps inside the first column over the header row.
- A headless TABLE's cell margin scales with the lettering. A flat 1.0 was
  0.02 mm on a 1:50 schedule; tables at the default 2.5 text are unchanged.
- drawing_new writes the Standard text style in arial.ttf, as AutoCAD
  2026's acadiso.dwt ships it: txt has no superscript two, so 'm²' read
  'm?' live and rendered as a box headlessly. The five bundled templates
  are rebuilt and build_templates --check reproduces them.

Each fix has a test that fails on the old code.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
Umut CAN
2026-09-25 00:17:10 +03:00
co-authored by Claude Opus 5.5
parent 554daff8d0
commit 7e7fe11a71
13 changed files with 182 additions and 370 deletions
+29 -4
View File
@@ -510,6 +510,17 @@ def _normalise_dimstyle_rounding(doc) -> None:
continue
def _arial_standard_style(doc) -> None:
"""Standard in Arial, as AutoCAD 2026's acadiso.dwt ships it.
ezdxf's own default is txt.shx, which has no superscript two: an
architectural area label 'm²' read 'm?' when AutoCAD opened the DXF, and
rendered as a box headlessly. A new live drawing already starts in Arial,
so the two engines now begin from the same Standard.
"""
doc.styles.get("Standard").dxf.font = "arial.ttf"
def _apply_iso_dimstyle(doc) -> None:
"""Store ISO defaults on the document's `Standard` dimension style."""
try:
@@ -2276,6 +2287,7 @@ class EzdxfBackend(AutoCADBackend):
else:
doc = ezdxf.new(dxfversion="R2010")
_apply_iso_dimstyle(doc)
_arial_standard_style(doc)
# R32: registering a fresh Drawing is the release — the runaway keeps
# the object it captured. The quarantined entry is evicted first so
# it can never be re-activated. Clear before _reset_history_baseline,
@@ -4861,21 +4873,34 @@ class EzdxfBackend(AutoCADBackend):
line = msp.add_line((x0, yy), (x0 + layout.width, yy))
self._apply_attrs(line, layer, None)
children.append(line)
for xx in x_positions:
line = msp.add_line((xx, y0), (xx, y0 - layout.height))
last = len(x_positions) - 1
for index, xx in enumerate(x_positions):
# a title row is merged, as AutoCAD's TABLE draws it: only the
# outer edges cross it
inner = 0 < index < last
top = y0 - layout.row_height if layout.title_row and inner else y0
line = msp.add_line((xx, top), (xx, y0 - layout.height))
self._apply_attrs(line, layer, None)
children.append(line)
padding = min(1.0, layout.row_height * 0.15)
# the cell margin scales with the lettering: 1.0 at the default 2.5
# text, 50 on a 1:50 schedule (a flat 1.0 there is 0.02 mm on the
# sheet and the text sat on the cell line)
padding = min(0.4 * layout.text_height, 0.15 * layout.row_height)
for row_index, row in enumerate(layout.cells):
for column_index, value in enumerate(row):
if not value:
continue
span = (
layout.width
if layout.title_row and row_index == 0
else layout.column_widths[column_index]
)
text_entity = msp.add_mtext(
value,
dxfattribs={
"char_height": layout.text_height,
"width": max(1.0, layout.column_widths[column_index] - 2 * padding),
"width": max(1.0, span - 2 * padding),
},
)
text_entity.dxf.insert = (
@@ -1,302 +0,0 @@
{
"summary": {
"baseline_ref": "v1.5.1",
"total": 48,
"old_pass": 26,
"new_pass": 48,
"old_pct": 54.2,
"new_pct": 100.0,
"fixed": 22,
"regressed": 0
},
"rows": [
{
"check": "core_line_length",
"category": "Core",
"old": "pass",
"new": "pass"
},
{
"check": "core_circle_radius",
"category": "Core",
"old": "pass",
"new": "pass"
},
{
"check": "core_layer_create",
"category": "Core",
"old": "pass",
"new": "pass"
},
{
"check": "core_polyline_closed",
"category": "Core",
"old": "pass",
"new": "pass"
},
{
"check": "core_linear_dim",
"category": "Core",
"old": "pass",
"new": "pass"
},
{
"check": "core_save_dxf_roundtrip",
"category": "Core",
"old": "pass",
"new": "pass"
},
{
"check": "dim_aligned_no_error",
"category": "Dimensions",
"old": "pass",
"new": "pass"
},
{
"check": "dim_angular_no_error",
"category": "Dimensions",
"old": "pass",
"new": "pass"
},
{
"check": "array_polar_360_distinct",
"category": "Modify",
"old": "pass",
"new": "pass"
},
{
"check": "point_intersection_line_line",
"category": "Geometry",
"old": "pass",
"new": "pass"
},
{
"check": "point_tangent_external",
"category": "Geometry",
"old": "pass",
"new": "pass"
},
{
"check": "arc_has_length",
"category": "Query",
"old": "pass",
"new": "pass"
},
{
"check": "arc_select_by_length",
"category": "Query",
"old": "pass",
"new": "pass"
},
{
"check": "ezdxf_bounding_box",
"category": "Query",
"old": "pass",
"new": "pass"
},
{
"check": "mtext_rotation_roundtrip",
"category": "Entities",
"old": "pass",
"new": "pass"
},
{
"check": "screenshot_png",
"category": "Render",
"old": "pass",
"new": "pass"
},
{
"check": "dim_overlap_critique_fires",
"category": "Quality gate",
"old": "pass",
"new": "pass"
},
{
"check": "iso13567_dim_layer",
"category": "Quality gate",
"old": "pass",
"new": "pass"
},
{
"check": "construction_left_iso_caught",
"category": "Quality gate",
"old": "pass",
"new": "pass"
},
{
"check": "gear_no_self_overlap",
"category": "Engineering",
"old": "pass",
"new": "pass"
},
{
"check": "center_linetype_applied",
"category": "Entities",
"old": "pass",
"new": "pass"
},
{
"check": "hatch_area_subtracts_its_island",
"category": "Measurement",
"old": "pass",
"new": "pass"
},
{
"check": "boundary_area_agrees_with_measure",
"category": "Measurement",
"old": "pass",
"new": "pass"
},
{
"check": "two_vertex_circle_has_area",
"category": "Measurement",
"old": "pass",
"new": "pass"
},
{
"check": "diameter_dim_measures_the_diameter",
"category": "Dimensions",
"old": "pass",
"new": "pass"
},
{
"check": "radius_dim_ignores_the_leader_length",
"category": "Dimensions",
"old": "pass",
"new": "pass"
},
{
"check": "pid_block_define_attdef_roundtrip",
"category": "P&ID",
"old": "miss",
"new": "pass"
},
{
"check": "pid_tag_parse_fic",
"category": "P&ID",
"old": "miss",
"new": "pass"
},
{
"check": "pid_graph_edge_count",
"category": "P&ID",
"old": "miss",
"new": "pass"
},
{
"check": "settings_dimstyle_iso25_values",
"category": "Settings",
"old": "miss",
"new": "pass"
},
{
"check": "settings_layer_state_roundtrip",
"category": "Settings",
"old": "miss",
"new": "pass"
},
{
"check": "settings_pdf_mediabox_a3",
"category": "Settings",
"old": "miss",
"new": "pass"
},
{
"check": "mech_part_roundtrip",
"category": "Mechanical",
"old": "miss",
"new": "pass"
},
{
"check": "mech_section_hatch_area",
"category": "Mechanical",
"old": "miss",
"new": "pass"
},
{
"check": "mech_iso286_on_dimension",
"category": "Mechanical",
"old": "miss",
"new": "pass"
},
{
"check": "mech_thread_unrepresented_is_caught",
"category": "Mechanical",
"old": "miss",
"new": "pass"
},
{
"check": "std_part_iso4014_m12",
"category": "Mechanical",
"old": "miss",
"new": "pass"
},
{
"check": "sheet_frame_iso5457_a3",
"category": "Sheet",
"old": "miss",
"new": "pass"
},
{
"check": "bom_balloon_link",
"category": "Sheet",
"old": "miss",
"new": "pass"
},
{
"check": "arch_junction_l_t_x",
"category": "Architecture",
"old": "miss",
"new": "pass"
},
{
"check": "arch_room_area_net",
"category": "Architecture",
"old": "miss",
"new": "pass"
},
{
"check": "arch_opening_cuts_wall",
"category": "Architecture",
"old": "miss",
"new": "pass"
},
{
"check": "arch_rooms_detect_foreign",
"category": "Architecture",
"old": "miss",
"new": "pass"
},
{
"check": "scale_check_detects_schematic",
"category": "Understanding",
"old": "miss",
"new": "pass"
},
{
"check": "pipe_takeoff_rmst_exact",
"category": "Understanding",
"old": "miss",
"new": "pass"
},
{
"check": "cable_takeoff_roundup",
"category": "Understanding",
"old": "miss",
"new": "pass"
},
{
"check": "diff_detects_known_edits",
"category": "Understanding",
"old": "miss",
"new": "pass"
},
{
"check": "topology_known_defects",
"category": "Understanding",
"old": "miss",
"new": "pass"
}
]
}
+4 -1
View File
@@ -11,6 +11,9 @@ class TableLayout:
column_widths: list[float]
row_height: float
text_height: float
#: The first row is a title, merged across every column as AutoCAD's
#: TABLE draws it.
title_row: bool = False
@property
def row_count(self) -> int:
@@ -71,7 +74,7 @@ def prepare_table_layout(
if any(width <= 0 for width in widths):
raise RuntimeError("entity_create_table: column widths must be positive")
return TableLayout(data, widths, float(row_height), float(text_height))
return TableLayout(data, widths, float(row_height), float(text_height), title_row=bool(title))
def validate_mleader(points: list[list[float]], text: str) -> list[tuple[float, float]]:
+39 -26
View File
@@ -375,42 +375,55 @@ async def add_stair(backend, stair: dict, *, lang: str = "en", scale=50) -> dict
async def draw_chains(
backend, walls, openings, *, sides=("bottom", "left"), scale=50, first_offset=None, step=None
) -> dict:
"""The exterior chains as real linear DIMENSIONs on the dimension layer."""
"""The exterior chains as real linear DIMENSIONs on the dimension layer.
Their text, arrows and gaps follow the plot scale like every other plan
annotation: DIMSCALE is ``scale`` while the chains are drawn, so each chain
dimension carries it, and the drawing's own DIMSCALE is put back afterwards.
Drawn at the style's DIMSCALE 1, a 1:50 chain's 2.5 text was 2.5 drawing
units - 0.05 mm on the sheet, which is no text at all.
"""
intents = exterior_chains(
walls, openings, sides=sides, first_offset=first_offset, step=step, scale=scale
)
layer = ARCH_ROLE_LAYER["dim"]
await ensure_arch_layers(backend, [layer])
factor = float(scale)
previous = await backend.system_get_variable("DIMSCALE")
previous = 1.0 if previous is None else float(previous)
handles: list[str] = []
rows: list[dict] = []
for intent in intents:
horizontal = abs(intent.p2[1] - intent.p1[1]) <= abs(intent.p2[0] - intent.p1[0])
info = await backend.dimension_linear(
intent.p1[0],
intent.p1[1],
intent.p2[0],
intent.p2[1],
intent.text_at[0],
intent.text_at[1],
0.0 if horizontal else 90.0,
layer,
)
handles.append(info.handle)
rows.append(
{
"row": intent.feature,
"from": list(intent.p1),
"to": list(intent.p2),
"value": math.dist(intent.p1, intent.p2),
}
)
if abs(previous - factor) > 1e-9:
await backend.system_set_variable("DIMSCALE", factor)
try:
for intent in intents:
horizontal = abs(intent.p2[1] - intent.p1[1]) <= abs(intent.p2[0] - intent.p1[0])
info = await backend.dimension_linear(
intent.p1[0],
intent.p1[1],
intent.p2[0],
intent.p2[1],
intent.text_at[0],
intent.text_at[1],
0.0 if horizontal else 90.0,
layer,
)
handles.append(info.handle)
rows.append(
{
"row": intent.feature,
"from": list(intent.p1),
"to": list(intent.p2),
"value": math.dist(intent.p1, intent.p2),
}
)
finally:
if abs(previous - factor) > 1e-9:
await backend.system_set_variable("DIMSCALE", previous)
return {
"count": len(handles),
"handles": handles,
"dimensions": rows,
"text_height": (
"set by the current dimension style; at 1:{0:g} set DIMSCALE={0:g} with "
"dimstyle_modify so the text plots at its paper height".format(float(scale))
),
"dimscale": factor,
"backend": backend.name,
}
+3 -2
View File
@@ -10090,8 +10090,9 @@ async def arch_dimension_chains(
own faces, read back from the plan on the drawing. A row that would only
restate a farther one is not drawn (ISO 129-1: each measurement once).
The row spacing defaults (10 mm, then 8 mm on paper) are this server's
drafting choices, not standard values. Text height follows the current
dimension style: at 1:50 set DIMSCALE=50 with `dimstyle_modify`.
drafting choices, not standard values. Text, arrows and gaps follow the
plot scale: each chain is drawn at DIMSCALE = `scale` (reported as
`dimscale`), and the drawing's own DIMSCALE is put back afterwards.
Refused by name: a drawing with no walls, an unknown or repeated side, a
non-positive scale, offset or step.
+7 -7
View File
@@ -489,7 +489,7 @@ $SKPOLY
9
$TDCREATE
40
2461306.528125
2461308.9858101853
9
$TDUCREATE
40
@@ -497,7 +497,7 @@ $TDUCREATE
9
$TDUPDATE
40
2461306.528125
2461308.9858101853
9
$TDUUPDATE
40
@@ -929,11 +929,11 @@ $PSTYLEMODE
9
$FINGERPRINTGUID
2
{4D9D8563-5BFB-473B-8F1E-D1D0F3D0DAFA}
{FB139631-7B1A-47EB-AF87-B1687B365FDD}
9
$VERSIONGUID
2
{8D04634D-773A-4616-8A64-559AF6DC2617}
{6BE2B494-ED83-4913-8849-67761A7D0FDA}
9
$EXTNAMES
290
@@ -2037,7 +2037,7 @@ Standard
42
2.5
3
txt
arial.ttf
4
0
@@ -3591,7 +3591,7 @@ DictionaryVariables
280
0
1
1.4.4 @ 2026-09-22T09:40:30.938108+00:00
1.4.4 @ 2026-09-24T20:39:34.891596+00:00
0
DICTIONARYVAR
5
@@ -3603,7 +3603,7 @@ DictionaryVariables
280
0
1
1.4.4 @ 2026-09-22T09:40:30.946903+00:00
1.4.4 @ 2026-09-24T20:39:34.900113+00:00
0
ENDSEC
0
+7 -7
View File
@@ -489,7 +489,7 @@ $SKPOLY
9
$TDCREATE
40
2461306.528125
2461308.9858101853
9
$TDUCREATE
40
@@ -497,7 +497,7 @@ $TDUCREATE
9
$TDUPDATE
40
2461306.528125
2461308.9858101853
9
$TDUUPDATE
40
@@ -929,11 +929,11 @@ $PSTYLEMODE
9
$FINGERPRINTGUID
2
{EFF5DFA9-C336-4985-B6C0-43235A21CA22}
{3E50F756-652E-48B0-AE41-6BC3AFA2655A}
9
$VERSIONGUID
2
{FAECDBB5-1575-421E-8ABB-3C85F40DB897}
{D664D6B0-1825-4401-AB82-8B8A64CDCC28}
9
$EXTNAMES
290
@@ -2037,7 +2037,7 @@ Standard
42
2.5
3
txt
arial.ttf
4
0
@@ -3591,7 +3591,7 @@ DictionaryVariables
280
0
1
1.4.4 @ 2026-09-22T09:40:30.949904+00:00
1.4.4 @ 2026-09-24T20:39:34.904120+00:00
0
DICTIONARYVAR
5
@@ -3603,7 +3603,7 @@ DictionaryVariables
280
0
1
1.4.4 @ 2026-09-22T09:40:30.956981+00:00
1.4.4 @ 2026-09-24T20:39:34.910627+00:00
0
ENDSEC
0
+7 -7
View File
@@ -489,7 +489,7 @@ $SKPOLY
9
$TDCREATE
40
2461306.528125
2461308.9858101853
9
$TDUCREATE
40
@@ -497,7 +497,7 @@ $TDUCREATE
9
$TDUPDATE
40
2461306.528125
2461308.9858101853
9
$TDUUPDATE
40
@@ -929,11 +929,11 @@ $PSTYLEMODE
9
$FINGERPRINTGUID
2
{6F1F6CA9-24DC-4EFD-8119-161D0C1B13F5}
{D6D14E50-062D-4B6D-94BD-7BBE205F2BDD}
9
$VERSIONGUID
2
{BB862168-CC9E-4423-9C5D-4DD3A99B384D}
{D46D597C-787A-4F32-8632-E6227A415146}
9
$EXTNAMES
290
@@ -2037,7 +2037,7 @@ Standard
42
2.5
3
txt
arial.ttf
4
0
@@ -3663,7 +3663,7 @@ DictionaryVariables
280
0
1
1.4.4 @ 2026-09-22T09:40:30.902394+00:00
1.4.4 @ 2026-09-24T20:39:34.835350+00:00
0
SCALE
5
@@ -4007,7 +4007,7 @@ DictionaryVariables
280
0
1
1.4.4 @ 2026-09-22T09:40:30.911407+00:00
1.4.4 @ 2026-09-24T20:39:34.863902+00:00
0
ENDSEC
0
+7 -7
View File
@@ -489,7 +489,7 @@ $SKPOLY
9
$TDCREATE
40
2461306.528125
2461308.9858101853
9
$TDUCREATE
40
@@ -497,7 +497,7 @@ $TDUCREATE
9
$TDUPDATE
40
2461306.528125
2461308.9858101853
9
$TDUUPDATE
40
@@ -929,11 +929,11 @@ $PSTYLEMODE
9
$FINGERPRINTGUID
2
{522C5F29-75D3-427F-A718-E45B971550C2}
{68192C9F-2E1B-4491-B3B5-24934A927E62}
9
$VERSIONGUID
2
{CF5A954B-3734-4AD2-B15E-F6411516FB5E}
{B09ECF76-6F17-4166-8AA8-1673B93563F5}
9
$EXTNAMES
290
@@ -2037,7 +2037,7 @@ Standard
42
2.5
3
txt
arial.ttf
4
0
@@ -4541,7 +4541,7 @@ DictionaryVariables
280
0
1
1.4.4 @ 2026-09-22T09:40:30.742891+00:00
1.4.4 @ 2026-09-24T20:39:34.680466+00:00
0
DICTIONARYVAR
5
@@ -4553,7 +4553,7 @@ DictionaryVariables
280
0
1
1.4.4 @ 2026-09-22T09:40:30.898227+00:00
1.4.4 @ 2026-09-24T20:39:34.829339+00:00
0
ENDSEC
0
+7 -7
View File
@@ -489,7 +489,7 @@ $SKPOLY
9
$TDCREATE
40
2461306.528125
2461308.9858101853
9
$TDUCREATE
40
@@ -497,7 +497,7 @@ $TDUCREATE
9
$TDUPDATE
40
2461306.528125
2461308.9858101853
9
$TDUUPDATE
40
@@ -929,11 +929,11 @@ $PSTYLEMODE
9
$FINGERPRINTGUID
2
{629792F3-C911-49E6-B9A0-300331D1DFBB}
{F0BBB091-B6E3-4896-AD21-EB815B809E89}
9
$VERSIONGUID
2
{ACABCD08-CEC8-4984-94D2-99D2ED56844B}
{CA4E384E-EE93-48E0-A671-949D2BCF158F}
9
$EXTNAMES
290
@@ -2205,7 +2205,7 @@ Standard
42
2.5
3
txt
arial.ttf
4
0
@@ -4709,7 +4709,7 @@ DictionaryVariables
280
0
1
1.4.4 @ 2026-09-22T09:40:30.915301+00:00
1.4.4 @ 2026-09-24T20:39:34.868575+00:00
0
DICTIONARYVAR
5
@@ -4721,7 +4721,7 @@ DictionaryVariables
280
0
1
1.4.4 @ 2026-09-22T09:40:30.933095+00:00
1.4.4 @ 2026-09-24T20:39:34.887094+00:00
0
ENDSEC
0
+21
View File
@@ -197,6 +197,27 @@ async def test_draw_chains_puts_real_dimensions_on_the_dimension_layer(backend):
}
async def test_draw_chains_text_follows_the_plot_scale_and_dimscale_is_put_back(backend):
# At the style's DIMSCALE 1 a 1:50 chain's 2.5 text was 2.5 drawing units -
# 0.05 mm on the sheet - and the README plan render showed empty boxes where
# the numbers belong. The chain carries the scale; the drawing keeps its own.
shell = Wall(
id="shell",
axis=((0.0, 0.0), (8200.0, 0.0), (8200.0, 6200.0), (0.0, 6200.0)),
thickness=200.0,
closed=True,
)
before = await backend.system_get_variable("DIMSCALE")
result = await draw_chains(backend, [shell], [], sides=("bottom",), scale=50)
assert result["dimscale"] == 50.0
heights = set()
for handle in result["handles"]:
block = backend._doc.entitydb[handle].get_geometry_block()
heights |= {round(e.dxf.char_height, 6) for e in block if e.dxftype() == "MTEXT"}
assert heights == {125.0} # 2.5 mm on paper x 50
assert await backend.system_get_variable("DIMSCALE") == before
async def test_assign_tags_numbers_each_kind_in_the_chosen_language():
ops = [
Opening(id="1", wall="a", kind="door", offset=0.0, width=900.0),
+8
View File
@@ -46,6 +46,14 @@ def _rendered(backend, handle):
# ── headless engine ─────────────────────────────────────────────────────────
async def test_a_new_drawing_writes_standard_in_arial_like_acadiso(backend):
# AutoCAD 2026's acadiso.dwt ships Standard as Arial. ezdxf's own default is
# txt.shx, which has no superscript two: an architectural area label 'm²'
# read 'm?' in AutoCAD and rendered as a box headlessly.
rows = {row["name"]: row["font"] for row in await backend.textstyle_list()}
assert rows["Standard"] == "arial.ttf"
async def test_dimstyle_list_reports_standard_current_with_the_seventeen_values(backend):
rows = await backend.dimstyle_list()
assert [r["name"] for r in rows] == ["Standard"]
+43
View File
@@ -28,6 +28,49 @@ async def test_ezdxf_table_returns_composite_contract(backend):
assert result.properties["bounds"] == {"min": [10.0, 72.0], "max": [30.0, 100.0]}
async def test_ezdxf_table_title_row_is_merged_across_the_table(backend):
# AutoCAD's TABLE merges its title row. Drawn as one column's cell, a title
# such as 'DOOR SCHEDULE' wrapped inside the first column and ran over the
# header row below it. The title spans the table, and only the table's outer
# edges cross its row.
result = await backend.entity_create_table(
0,
100,
rows=[["D1", "900"]],
headers=["TAG", "WIDTH"],
column_widths=[20, 30],
row_height=10,
text_height=2.5,
title="DOOR SCHEDULE",
)
children = [backend._doc.entitydb[h] for h in result.properties["child_handles"]]
(title,) = [e for e in children if e.dxftype() == "MTEXT" and e.text == "DOOR SCHEDULE"]
assert title.dxf.width == pytest.approx(50.0 - 2 * 1.0) # padding min(1.0, 10 x 0.15)
verticals = [e for e in children if e.dxftype() == "LINE" and e.dxf.start.x == e.dxf.end.x]
inner = [v for v in verticals if 0.0 < v.dxf.start.x < 50.0]
assert inner and all(max(v.dxf.start.y, v.dxf.end.y) == pytest.approx(90.0) for v in inner)
outer = [v for v in verticals if v.dxf.start.x in (0.0, 50.0)]
assert len(outer) == 2 and all(max(v.dxf.start.y, v.dxf.end.y) == 100.0 for v in outer)
async def test_ezdxf_table_cell_margin_scales_with_the_lettering(backend):
# A 1:50 schedule letters at 125 in rows of 350. A flat 1.0 margin is 0.02 mm
# on that sheet: the text sat on the cell line, and 'LEFT' read as 'FT'.
result = await backend.entity_create_table(
0,
0,
rows=[["D1", "LEFT"]],
column_widths=[750, 1000],
row_height=350,
text_height=125,
)
children = [backend._doc.entitydb[h] for h in result.properties["child_handles"]]
(left,) = [e for e in children if e.dxftype() == "MTEXT" and e.text == "LEFT"]
assert left.dxf.insert.x == pytest.approx(750 + 50.0) # 0.4 x 125
assert left.dxf.insert.y == pytest.approx(-50.0)
assert left.dxf.width == pytest.approx(1000 - 2 * 50.0)
async def test_ezdxf_table_survives_save_and_reopen(backend, tmp_path):
result = await backend.entity_create_table(0, 30, rows=[["A", "1"]], headers=["Name", "Qty"])
path = tmp_path / "table.dxf"