diff --git a/backends/ezdxf_backend.py b/backends/ezdxf_backend.py index a7efa4e..985ba0a 100644 --- a/backends/ezdxf_backend.py +++ b/backends/ezdxf_backend.py @@ -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 = ( diff --git a/benchmarks/results/published/ab-v1.5.1-vs-v1.6.0-dev.json b/benchmarks/results/published/ab-v1.5.1-vs-v1.6.0-dev.json deleted file mode 100644 index a22e996..0000000 --- a/benchmarks/results/published/ab-v1.5.1-vs-v1.6.0-dev.json +++ /dev/null @@ -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" - } - ] -} \ No newline at end of file diff --git a/engineering/annotation.py b/engineering/annotation.py index 5dac767..03510b2 100644 --- a/engineering/annotation.py +++ b/engineering/annotation.py @@ -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]]: diff --git a/engineering/arch/draw.py b/engineering/arch/draw.py index cbf273a..08f0e5b 100644 --- a/engineering/arch/draw.py +++ b/engineering/arch/draw.py @@ -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, } diff --git a/server.py b/server.py index de4f771..d8288b1 100644 --- a/server.py +++ b/server.py @@ -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. diff --git a/templates/ansi_b_mech.dxf b/templates/ansi_b_mech.dxf index 780babb..0ae7adb 100644 --- a/templates/ansi_b_mech.dxf +++ b/templates/ansi_b_mech.dxf @@ -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 diff --git a/templates/ansi_d_arch.dxf b/templates/ansi_d_arch.dxf index cde86c9..fa59292 100644 --- a/templates/ansi_d_arch.dxf +++ b/templates/ansi_d_arch.dxf @@ -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 diff --git a/templates/iso_a1_arch.dxf b/templates/iso_a1_arch.dxf index 4f9b378..08f614d 100644 --- a/templates/iso_a1_arch.dxf +++ b/templates/iso_a1_arch.dxf @@ -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 diff --git a/templates/iso_a3_mech.dxf b/templates/iso_a3_mech.dxf index db4b488..af565f4 100644 --- a/templates/iso_a3_mech.dxf +++ b/templates/iso_a3_mech.dxf @@ -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 diff --git a/templates/iso_a3_pid.dxf b/templates/iso_a3_pid.dxf index 55f1c27..564093c 100644 --- a/templates/iso_a3_pid.dxf +++ b/templates/iso_a3_pid.dxf @@ -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 diff --git a/tests/test_arch_draw.py b/tests/test_arch_draw.py index c687135..d6c5306 100644 --- a/tests/test_arch_draw.py +++ b/tests/test_arch_draw.py @@ -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), diff --git a/tests/test_styles.py b/tests/test_styles.py index a0a9b41..302c61f 100644 --- a/tests/test_styles.py +++ b/tests/test_styles.py @@ -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"] diff --git a/tests/test_table_leader.py b/tests/test_table_leader.py index 687eb68..4ae3de3 100644 --- a/tests/test_table_leader.py +++ b/tests/test_table_leader.py @@ -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"