diff --git a/crates/engine/tests/text_implicit_alignment.rs b/crates/engine/tests/text_implicit_alignment.rs
new file mode 100644
--- /dev/null
+++ b/crates/engine/tests/text_implicit_alignment.rs
@@ -0,0 +1,128 @@
+//! Making an implicit TEXT anchor explicit in DWG must preserve grip editing, property edits,
+//! and undo/redo. These exercise the engine API and interactive prompt path, not OS mouse input.
+
+use cadcraft_engine::Session;
+use cadcraft_engine::doc::{Common, Drawing, EntityKind, HAlign, Space, Text, VAlign};
+use cadcraft_engine::geom::{Vec2, Vec3};
+use cadcraft_engine::grips::GripMode;
+use cadcraft_io::{SaveOptions, Version};
+use serde_json::json;
+
+fn reopened(d: &Drawing, version: Version) -> Drawing {
+    let bytes = cadcraft_io::write_with(d, "text.dwg", &SaveOptions { version: Some(version), binary: false }).unwrap();
+    cadcraft_io::read(&bytes, "text.dwg").unwrap()
+}
+
+fn geometry(d: &Drawing) -> Vec<Vec2> {
+    let list = cadcraft_engine::render::build(d, &Space::Model, &Default::default());
+    assert!(!list.verts.is_empty());
+    list.verts
+}
+
+fn same_geometry(a: &Drawing, b: &Drawing) {
+    let a = geometry(a);
+    let b = geometry(b);
+    assert_eq!(a.len(), b.len());
+    assert!(a.iter().zip(b).all(|(a, b)| a.dist(b) < 1e-10));
+}
+
+#[test]
+fn rejected_dwg_save_keeps_existing_file_and_unsaved_drawing() {
+    use cadcraft_engine::cmd::file::{IoHooks, set_io};
+    use std::io::Write;
+
+    set_io(IoHooks {
+        read: |bytes, name| cadcraft_io::read(bytes, name).map_err(|e| e.to_string()),
+        write: |d, name| cadcraft_io::write(d, name).map_err(|e| e.to_string()),
+        plot: None,
+    });
+    let mut s = Session::new();
+    let saved = s.state().unwrap().saved.clone();
+    let previous_bytes = cadcraft_io::write(s.doc().unwrap(), "previous.dwg").unwrap();
+    let path = std::env::temp_dir().join(format!("cadcraft-text-rejected-save-{}.dwg", std::process::id()));
+    let mut file = std::fs::OpenOptions::new().write(true).create_new(true).open(&path).unwrap();
+    file.write_all(&previous_bytes).unwrap();
+    drop(file);
+    s.execute("text", &json!({"at": [12, 7], "text": "AB", "height": 3})).unwrap();
+    let handle = s.doc().unwrap().model.iter().next().unwrap().handle;
+    s.doc_mut()
+        .unwrap()
+        .modify_entity(handle, |e| {
+            let EntityKind::Text(t) = &mut e.kind else { panic!("TEXT") };
+            t.halign = HAlign::Fit;
+            t.align_pt = None;
+        })
+        .unwrap();
+    let before = s.doc().unwrap().clone();
+    let result = s.execute("saveas", &json!({"path": path.to_str().unwrap()}));
+    let actual_bytes = std::fs::read(&path).unwrap();
+    std::fs::remove_file(&path).unwrap();
+    let error = result.unwrap_err();
+    assert!(error.to_string().contains("second alignment point"), "{error}");
+    assert_eq!(actual_bytes, previous_bytes);
+    assert_eq!(s.doc().unwrap().model, before.model);
+    assert!(s.state().unwrap().path.is_none());
+    assert_eq!(s.state().unwrap().saved.model, saved.model);
+}
+
+#[test]
+fn reopened_implicit_text_keeps_grips_property_edits_and_undo() {
+    for (h, v) in
+        [(HAlign::Left, VAlign::Bottom), (HAlign::Center, VAlign::Baseline), (HAlign::Right, VAlign::Top), (HAlign::Middle, VAlign::Baseline)]
+    {
+        let mut source = Drawing::new_metric();
+        let handle = source
+            .add(
+                &Space::Model,
+                Common::default(),
+                EntityKind::Text(Text {
+                    insert: Vec3::new(12.0, 7.0, 5.0),
+                    align_pt: None,
+                    height: 3.0,
+                    value: "AB".into(),
+                    rotation: 0.7,
+                    width_factor: 1.25,
+                    oblique: -0.2,
+                    style: "Standard".into(),
+                    halign: h,
+                    valign: v,
+                }),
+            )
+            .unwrap();
+        for version in Version::ALL {
+            for interactive in [false, true] {
+                let mut edited = Vec::new();
+                for d in [source.clone(), reopened(&source, version)] {
+                    let before = d.clone();
+                    let mut s = Session::new();
+                    *s.doc_mut().unwrap() = d;
+                    assert_eq!(s.grips_of(handle).unwrap(), vec![Vec2::new(12.0, 7.0)]);
+                    if interactive {
+                        s.set_selection(vec![handle]);
+                        s.start_grip_edit(&[(handle, 0)]).unwrap();
+                        s.cmdline("16,5").unwrap();
+                        assert!(s.running.is_none(), "{}", s.prompt_text());
+                    } else {
+                        s.grip_edit(handle, 0, Vec2::new(16.0, 5.0), GripMode::Stretch).unwrap();
+                    }
+                    assert_eq!(s.grips_of(handle).unwrap(), vec![Vec2::new(16.0, 5.0)]);
+                    let after = s.doc().unwrap().clone();
+                    assert!(geometry(&before).iter().zip(geometry(&after)).all(|(a, b)| (*a + Vec2::new(4.0, -2.0)).dist(b) < 1e-10));
+                    s.execute("undo", &json!({})).unwrap();
+                    assert_eq!(s.doc().unwrap().model, before.model);
+                    s.execute("redo", &json!({})).unwrap();
+                    assert_eq!(s.doc().unwrap().model, after.model);
+                    s.execute("properties.set", &json!({"handles": [handle.hex()], "text": "AB AB AB", "height": 4.5})).unwrap();
+                    assert_eq!(s.grips_of(handle).unwrap(), vec![Vec2::new(16.0, 5.0)]);
+                    let EntityKind::Text(t) = &s.doc().unwrap().entity(handle).unwrap().kind else { panic!("TEXT") };
+                    assert_eq!(t.value, "AB AB AB");
+                    assert_eq!(t.height, 4.5);
+                    let back = reopened(s.doc().unwrap(), version);
+                    same_geometry(s.doc().unwrap(), &back);
+                    edited.push(back);
+                }
+                same_geometry(&edited[0], &edited[1]);
+            }
+        }
+    }
+}
diff --git a/crates/io/src/dxf_write.rs b/crates/io/src/dxf_write.rs
--- a/crates/io/src/dxf_write.rs
+++ b/crates/io/src/dxf_write.rs
@@ -31,7 +31,10 @@ use ctx::Ctx;
 use entity::entity;
 use sections::tags_for;
 
-struct W {
+struct W<'a> {
+    // DXF tag emission is infallible. The DWG bridge checks this result before encoding;
+    // recording the first error here validates only records that are actually emitted.
+    dwg_text: Option<&'a mut crate::Result<()>>,
     t: Vec<Tag>,
     next: u64,
     /// Upper-case plot style name → its `ACDBPLACEHOLDER` handle (group 390 of layers and objects).
@@ -43,7 +46,7 @@ struct W {
     reactors: HashMap<Handle, Vec<String>>,
 }
 
-impl W {
+impl W<'_> {
     fn s(&mut self, c: i32, v: impl Into<String>) {
         self.t.push(Tag::s(c, v));
     }
@@ -107,15 +110,17 @@ pub fn write(d: &Drawing) -> String {
 
 /// The drawing's DXF tags, declaring `version` in `$ACADVER`.
 pub fn tags(d: &Drawing, version: Version) -> Vec<Tag> {
-    tags_for(d, version, version)
+    tags_for(d, version, version, None)
 }
 
 /// The R2000 DXF the DWG bridge converts to a `dwg` version DWG: its entity types are the DWG
 /// version's (MULTILEADER from 2007).
-pub(crate) fn dwg_bridge(d: &Drawing, dwg: Version) -> String {
-    let mut tags = tags_for(d, Version::R2000, dwg);
+pub(crate) fn dwg_bridge(d: &Drawing, dwg: Version) -> crate::Result<String> {
+    let mut text_result = Ok(());
+    let mut tags = tags_for(d, Version::R2000, dwg, Some(&mut text_result));
+    text_result?;
     bridge_markers(&mut tags);
-    cadcraft_dxf::write_ascii(&tags)
+    Ok(cadcraft_dxf::write_ascii(&tags))
 }
 
 /// Subclass markers the DWG bridge knows by another spelling (it matches them exactly):
@@ -136,6 +141,7 @@ fn bridge_markers(tags: &mut [Tag]) {
 /// out. AutoLISP's `entget` is built on it.
 pub fn entity_groups(d: &Drawing, e: &Entity) -> (String, Vec<Tag>) {
     let mut w = W {
+        dwg_text: None,
         t: Vec::new(),
         next: d.handseed.max(0x100).saturating_add(0x1000),
         pstyles: HashMap::new(),
diff --git a/crates/io/src/dxf_write/common.rs b/crates/io/src/dxf_write/common.rs
--- a/crates/io/src/dxf_write/common.rs
+++ b/crates/io/src/dxf_write/common.rs
@@ -13,7 +13,7 @@ pub(super) fn annotative_xdata(w: &mut W) {
 
 /// Override (`ACAD` DSTYLE) and associativity (`CADCRAFT` ASSOC) xdata of a dimension.
 pub(super) fn dim_xdata(w: &mut W, dm: &Dimension, cx: &Ctx) {
-    let mut ov = W { t: Vec::new(), next: 0, pstyles: HashMap::new(), layers: HashMap::new(), reactors: HashMap::new() };
+    let mut ov = W { dwg_text: None, t: Vec::new(), next: 0, pstyles: HashMap::new(), layers: HashMap::new(), reactors: HashMap::new() };
     let mut sah = false;
     for (k, v) in &dm.overrides {
         let Some(field) = DimStyle::field_name(k) else { continue };
@@ -187,7 +187,7 @@ fn attrib_flags(a: &Attrib) -> i64 {
 /// The insertion point (group 10) to write for `t`. A justified text is placed by its alignment
 /// point (group 11), and its insertion point is the start of its baseline, which AutoCAD reads
 /// from DWG as is (it recomputes it only for DXF): writing the alignment point there drew centred
-/// and right-aligned text from the wrong end in AutoCAD (#1050).
+/// and right-aligned text from the wrong end in AutoCAD.
 ///
 /// The stored point is kept when it is close to where CADCraft's font puts the baseline start:
 /// then it is AutoCAD's own (exact for its fonts). It is replaced when it is clearly stale: the
@@ -197,9 +197,9 @@ fn attrib_flags(a: &Attrib) -> i64 {
 /// across it within a fifth of its height: Left Middle and Left Bottom text made here, whose
 /// alignment point is only half or a third of a height from the baseline start, must not pass
 /// for AutoCAD's.
-fn text_insert(d: &Drawing, t: &Text) -> Vec3 {
-    let computed = computed_text_insert(d, t);
-    if t.align_pt.is_some_and(|a| a.xy() == t.insert.xy()) {
+fn text_insert(d: &Drawing, t: &Text, align_pt: Option<Vec3>) -> Vec3 {
+    let computed = computed_text_insert(d, t, align_pt);
+    if align_pt.is_some_and(|a| a.xy() == t.insert.xy()) {
         return computed;
     }
     let h = t.height.abs();
@@ -207,7 +207,7 @@ fn text_insert(d: &Drawing, t: &Text) -> Vec3 {
     let close = if t.value.contains('\n') {
         // A multiline attribute's lines are placed by its embedded MTEXT: a one-line baseline
         // start says little about them, so only the distance counts.
-        let width = t.align_pt.map_or(0.0, |a| 2.0 * a.xy().dist(computed.xy()));
+        let width = align_pt.map_or(0.0, |a| 2.0 * a.xy().dist(computed.xy()));
         off.len() <= 0.1 * width + 0.5 * h
     } else {
         let width = cadcraft_fonts::line_width(&t.value, h, t.width_factor.abs());
@@ -216,9 +216,9 @@ fn text_insert(d: &Drawing, t: &Text) -> Vec3 {
     if close { t.insert } else { computed }
 }
 
-fn computed_text_insert(d: &Drawing, t: &Text) -> Vec3 {
+fn computed_text_insert(d: &Drawing, t: &Text, align_pt: Option<Vec3>) -> Vec3 {
     let justified = !matches!((t.halign, t.valign), (HAlign::Left, VAlign::Baseline) | (HAlign::Aligned | HAlign::Fit, _));
-    let Some(a) = t.align_pt.filter(|_| justified) else { return t.insert };
+    let Some(a) = align_pt.filter(|_| justified) else { return t.insert };
     let ts = d.text_style(&t.style).cloned().unwrap_or_default();
     let (h, v) = cadcraft_render::text_align(t.halign, t.valign);
     let p = cadcraft_fonts::TextParams {
@@ -237,8 +237,37 @@ fn computed_text_insert(d: &Drawing, t: &Text) -> Vec3 {
     if q.is_finite() { Vec3::new(q.x, q.y, a.z) } else { t.insert }
 }
 
+/// Resolve only an emitted record's DWG anchor; do not mutate the drawing or DXF's optional
+/// point. Aligned/Fit need an authored endpoint: inventing one changes subsequent value edits.
+fn dwg_alignment_point(t: &Text) -> crate::Result<Option<Vec3>> {
+    if t.align_pt.is_some() || (t.halign == HAlign::Left && t.valign == VAlign::Baseline) {
+        return Ok(t.align_pt);
+    }
+    if matches!(t.halign, HAlign::Aligned | HAlign::Fit) {
+        return Err(crate::IoError::Format(format!("DWG export: {:?} text requires a second alignment point", t.halign)));
+    }
+    if !t.insert.x.is_finite() || !t.insert.y.is_finite() || !t.insert.z.is_finite() {
+        return Err(crate::IoError::Format("DWG export: implicit text alignment point must be finite".into()));
+    }
+    Ok(Some(t.insert))
+}
+
 pub(super) fn text_tags(w: &mut W, d: &Drawing, t: &Text, attrib: Option<(&Attrib, Version)>, attdef_prompt: Option<&str>) {
-    w.p(10, text_insert(d, t));
+    let align_pt = if let Some(result) = w.dwg_text.as_deref_mut() {
+        if result.is_err() {
+            return;
+        }
+        match dwg_alignment_point(t) {
+            Ok(point) => point,
+            Err(error) => {
+                *result = Err(error);
+                return;
+            }
+        }
+    } else {
+        t.align_pt
+    };
+    w.p(10, text_insert(d, t, align_pt));
     w.f(40, t.height);
     w.s(1, &t.value);
     if t.rotation != 0.0 {
@@ -268,7 +297,7 @@ pub(super) fn text_tags(w: &mut W, d: &Drawing, t: &Text, attrib: Option<(&Attri
     if h != 0 {
         w.i(72, h);
     }
-    if let Some(a) = t.align_pt {
+    if let Some(a) = align_pt {
         w.p(11, a);
     }
     match attrib {
diff --git a/crates/io/src/dxf_write/sections.rs b/crates/io/src/dxf_write/sections.rs
--- a/crates/io/src/dxf_write/sections.rs
+++ b/crates/io/src/dxf_write/sections.rs
@@ -10,12 +10,13 @@ use super::*;
 
 /// The drawing's DXF tags, declaring `version` in `$ACADVER`, with what `target` (the version the
 /// file ends up in: `version`, or the DWG version the bridge writes) has.
-pub(super) fn tags_for(d: &Drawing, version: Version, target: Version) -> Vec<Tag> {
+pub(super) fn tags_for(d: &Drawing, version: Version, target: Version, dwg_text: Option<&mut crate::Result<()>>) -> Vec<Tag> {
     // An in-place block edit in progress is not written: the drawing as before it.
     let committed = d.committed();
     let d: &Drawing = &committed;
     // Structural handles start above every entity handle.
     let mut w = W {
+        dwg_text,
         t: Vec::new(),
         next: d.handseed.max(0x100).saturating_add(0x1000),
         pstyles: HashMap::new(),
@@ -487,7 +488,7 @@ pub(super) fn tags_for(d: &Drawing, version: Version, target: Version) -> Vec<Ta
         w.i(70, 0);
         let v = serde_json::to_value(s).unwrap_or_default();
         let defaults = serde_json::to_value(DimStyle::default()).unwrap_or_default();
-        let mut newer = W { t: Vec::new(), next: 0, pstyles: HashMap::new(), layers: HashMap::new(), reactors: HashMap::new() };
+        let mut newer = W { dwg_text: None, t: Vec::new(), next: 0, pstyles: HashMap::new(), layers: HashMap::new(), reactors: HashMap::new() };
         for (field, code, kind) in dxf_ext::DIM_CODES {
             let Some(enc) = v.get(*field).and_then(|x| cx.encode(*kind, x)) else { continue };
             // DIMPOST is written even when empty: a reader that defaults a missing one to "<>"
diff --git a/crates/io/src/lib.rs b/crates/io/src/lib.rs
--- a/crates/io/src/lib.rs
+++ b/crates/io/src/lib.rs
@@ -99,7 +99,7 @@ pub fn write_with(d: &Drawing, name: &str, opts: &SaveOptions) -> Result<Vec<u8>
         // gradients, so a drawing from an R2000 DXF doesn't lose them by default.
         "dwg" if !opts.binary => {
             let version = opts.version.unwrap_or_else(|| version.max(Version::R2004));
-            cadcraft_dwg::dxf_to_dwg_as(dxf_write::dwg_bridge(d, version).as_bytes(), version.code()).map_err(IoError::Format)
+            cadcraft_dwg::dxf_to_dwg_as(dxf_write::dwg_bridge(d, version)?.as_bytes(), version.code()).map_err(IoError::Format)
         }
         e if opts.binary => Err(IoError::Format(format!("binary output is for DXF files, not `{e}`"))),
         _ => write_framed(d, name, None),
diff --git a/crates/io/tests/it/main.rs b/crates/io/tests/it/main.rs
--- a/crates/io/tests/it/main.rs
+++ b/crates/io/tests/it/main.rs
@@ -62,6 +62,7 @@ mod svg_arc_budget;
 mod svg_tiny_extents;
 mod table_cellstyle_roundtrip;
 mod tables_objects_roundtrip;
+mod text_implicit_alignment;
 mod text_left_justify;
 mod tolerance_roundtrip;
 mod user_lin_pat_roundtrip;
diff --git a/crates/io/tests/it/text_implicit_alignment.rs b/crates/io/tests/it/text_implicit_alignment.rs
new file mode 100644
--- /dev/null
+++ b/crates/io/tests/it/text_implicit_alignment.rs
@@ -0,0 +1,370 @@
+//! DWG must preserve the placement and edits of TEXT whose ordinary alignment anchor is
+//! implicit. Aligned/Fit without a second endpoint must fail explicitly, not gain a new
+//! length constraint. DXF keeps its existing optional-point representation.
+
+use cadcraft_doc::{Attrib, Block, Common, Drawing, Entity, EntityKind, HAlign, Insert, Space, Text, VAlign};
+use cadcraft_geom::{Mat3, Vec2, Vec3};
+use cadcraft_io::{SaveOptions, Version};
+use std::sync::Arc;
+
+fn text(h: HAlign, v: VAlign) -> Text {
+    Text {
+        insert: Vec3::new(12.0, 7.0, 5.0),
+        align_pt: None,
+        height: 3.0,
+        value: "AB".into(),
+        rotation: 0.7,
+        width_factor: 1.25,
+        oblique: -0.2,
+        style: "Standard".into(),
+        halign: h,
+        valign: v,
+    }
+}
+
+fn drawing(t: Text) -> Drawing {
+    let mut d = Drawing::new_metric();
+    d.add(&Space::Model, Common::default(), EntityKind::Text(t)).unwrap();
+    d
+}
+
+fn first_text(d: &Drawing) -> &Text {
+    let EntityKind::Text(t) = &d.model.iter().next().unwrap().kind else { panic!("TEXT") };
+    t
+}
+
+fn reopen(d: &Drawing, name: &str, version: Version, binary: bool) -> Drawing {
+    let bytes = cadcraft_io::write_with(d, name, &SaveOptions { version: Some(version), binary }).unwrap();
+    let back = cadcraft_io::read(&bytes, name).unwrap();
+    assert_eq!(cadcraft_io::save_version(&back), version);
+    assert!(back.read_notes.is_empty(), "{:?}", back.read_notes);
+    back
+}
+
+fn same_geometry(a: &Drawing, b: &Drawing) {
+    let a = cadcraft_render::build(a, &Space::Model, &Default::default());
+    let b = cadcraft_render::build(b, &Space::Model, &Default::default());
+    assert!(!a.verts.is_empty());
+    assert_eq!(a.prims.len(), b.prims.len());
+    for (pa, pb) in a.prims.iter().zip(&b.prims) {
+        assert_eq!(pa.kind, pb.kind);
+        assert_eq!(a.points(pa).len(), b.points(pb).len());
+        for (a, b) in a.points(pa).iter().zip(b.points(pb)) {
+            // Wire angle conversions round slightly; the fixed fixture is only tens of units.
+            assert!(a.dist(*b) < 1e-10, "vertex moved: {a:?} -> {b:?}");
+        }
+    }
+}
+
+fn edit(d: &mut Drawing) {
+    let handle = d.model.iter().next().unwrap().handle;
+    d.model.modify(handle, |e| {
+        e.kind.transform(&Mat3::translate(Vec2::new(4.0, -2.0)));
+        let EntityKind::Text(t) = &mut e.kind else { panic!("TEXT") };
+        t.value = "AB AB AB".into();
+    });
+}
+
+#[test]
+fn dwg_keeps_implicit_anchors_geometry_and_value_edits() {
+    for h in [HAlign::Left, HAlign::Center, HAlign::Right, HAlign::Middle] {
+        for v in [VAlign::Baseline, VAlign::Bottom, VAlign::Middle, VAlign::Top] {
+            if h == HAlign::Middle && v != VAlign::Baseline {
+                continue;
+            }
+            for flags in 0..4 {
+                let mut source = drawing(text(h, v));
+                source.text_styles[0].backwards = flags & 1 != 0;
+                source.text_styles[0].upside_down = flags & 2 != 0;
+                let mut edited_source = source.clone();
+                edit(&mut edited_source);
+                for version in Version::ALL {
+                    let mut current = source.clone();
+                    for _ in 0..2 {
+                        current = reopen(&current, "text.dwg", version, false);
+                        let back = first_text(&current);
+                        assert_eq!((back.halign, back.valign), (h, v));
+                        assert_eq!(back.justify_point(), first_text(&source).justify_point());
+                        assert_eq!(back.align_pt, if h == HAlign::Left && v == VAlign::Baseline { None } else { Some(first_text(&source).insert) });
+                        same_geometry(&source, &current);
+                        let mut edited = current.clone();
+                        edit(&mut edited);
+                        same_geometry(&edited_source, &edited);
+                        let edited_back = reopen(&edited, "edited.dwg", version, false);
+                        assert_eq!(first_text(&edited_back).value, "AB AB AB");
+                        assert_eq!(first_text(&edited_back).justify_point(), first_text(&edited_source).justify_point());
+                        same_geometry(&edited_source, &edited_back);
+                    }
+                }
+                assert_eq!(first_text(&source).align_pt, None, "saving does not mutate the drawing");
+            }
+        }
+    }
+}
+
+#[test]
+fn dwg_refuses_missing_two_point_alignment_but_keeps_explicit_endpoints() {
+    for h in [HAlign::Aligned, HAlign::Fit] {
+        let source = drawing(text(h, VAlign::Baseline));
+        for version in Version::ALL {
+            let Err(error) = cadcraft_io::write_with(&source, "text.dwg", &SaveOptions { version: Some(version), binary: false }) else {
+                panic!("DWG must refuse {h:?} without its second endpoint");
+            };
+            assert!(error.to_string().contains("second alignment point"), "{error}");
+            let mut explicit = text(h, VAlign::Baseline);
+            explicit.align_pt = Some(Vec3::new(18.0, 7.0, 5.0));
+            let explicit = drawing(explicit);
+            let back = reopen(&explicit, "text.dwg", version, false);
+            assert_eq!(first_text(&back).align_pt, first_text(&explicit).align_pt);
+            same_geometry(&explicit, &back);
+        }
+    }
+}
+
+#[test]
+fn dwg_keeps_authored_ordinary_alignment_points() {
+    for (h, v) in
+        [(HAlign::Left, VAlign::Bottom), (HAlign::Center, VAlign::Baseline), (HAlign::Right, VAlign::Top), (HAlign::Middle, VAlign::Baseline)]
+    {
+        let mut t = text(h, v);
+        t.align_pt = Some(Vec3::new(20.0, 8.0, 5.0));
+        let source = drawing(t);
+        for version in Version::ALL {
+            let back = reopen(&source, "explicit.dwg", version, false);
+            assert_eq!(first_text(&back).align_pt, first_text(&source).align_pt);
+            same_geometry(&source, &back);
+        }
+    }
+}
+
+#[test]
+fn dxf_keeps_absent_alignment_points() {
+    for h in [HAlign::Left, HAlign::Center, HAlign::Right, HAlign::Middle, HAlign::Aligned, HAlign::Fit] {
+        let source = drawing(text(h, if h == HAlign::Left { VAlign::Bottom } else { VAlign::Baseline }));
+        for version in Version::ALL {
+            for binary in [false, true] {
+                let back = reopen(&source, "text.dxf", version, binary);
+                let actual = first_text(&back);
+                let mut expected = first_text(&source).clone();
+                // Only degree/radian conversion roundoff is permitted, not point synthesis.
+                assert!((actual.rotation - expected.rotation).abs() <= 8.0 * f64::EPSILON);
+                assert!((actual.oblique - expected.oblique).abs() <= 8.0 * f64::EPSILON);
+                expected.rotation = actual.rotation;
+                expected.oblique = actual.oblique;
+                assert_eq!(actual, &expected);
+                same_geometry(&source, &back);
+            }
+        }
+    }
+}
+
+#[test]
+fn entity_inspection_keeps_the_authored_optional_point() {
+    let source = drawing(text(HAlign::Right, VAlign::Top));
+    let entity = source.model.iter().next().unwrap();
+    let (kind, tags) = cadcraft_io::entity_groups(&source, entity);
+    assert_eq!(kind, "TEXT");
+    assert!(!tags.iter().any(|tag| matches!(tag.code, 11 | 21 | 31)));
+    assert_eq!(tags.iter().find(|tag| tag.code == 10).unwrap().f64(), 12.0);
+    assert_eq!(tags.iter().find(|tag| tag.code == 20).unwrap().f64(), 7.0);
+    let back = reopen(&source, "text.dwg", Version::R2018, false);
+    assert_eq!(first_text(&back).align_pt, Some(first_text(&source).insert));
+    assert_eq!(cadcraft_io::entity_groups(&source, entity), (kind, tags));
+}
+
+fn attribute() -> Attrib {
+    Attrib {
+        tag: "ID".into(),
+        // Nonbaseline ATTRIB reading currently drops vertical justification; isolate anchor export here.
+        text: text(HAlign::Center, VAlign::Baseline),
+        invisible: false,
+        constant: false,
+        prompt: String::new(),
+        props: Default::default(),
+        multiline: None,
+        modes: Default::default(),
+    }
+}
+
+fn texts(d: &Drawing) -> Vec<&Text> {
+    d.model
+        .iter()
+        .chain(d.layouts.iter().flat_map(|l| l.entities.iter()))
+        .chain(d.blocks.values().flat_map(|b| b.entities.iter()))
+        .flat_map(|e| match &e.kind {
+            EntityKind::Text(t) => vec![t],
+            EntityKind::AttDef(a) => vec![&a.text],
+            EntityKind::Insert(i) => i.attribs.iter().map(|a| &a.text).collect(),
+            _ => Vec::new(),
+        })
+        .collect()
+}
+
+#[test]
+fn invented_endpoint_changes_future_value_edits() {
+    for h in [HAlign::Aligned, HAlign::Fit] {
+        let mut t = text(h, VAlign::Baseline);
+        let mut implicit = drawing(t.clone());
+        let width = cadcraft_fonts::text_width(&cadcraft_fonts::TextFont::Stroke, &t.value, t.height, t.width_factor);
+        t.align_pt = Some(t.insert + Vec3::new(width * t.rotation.cos(), width * t.rotation.sin(), 0.0));
+        let mut invented = reopen(&drawing(t), "invented.dwg", Version::R2018, false);
+        same_geometry(&implicit, &invented);
+        edit(&mut implicit);
+        edit(&mut invented);
+        let a = cadcraft_render::build(&implicit, &Space::Model, &Default::default());
+        let b = cadcraft_render::build(&invented, &Space::Model, &Default::default());
+        assert_eq!(a.verts.len(), b.verts.len());
+        let max_shift = a.verts.iter().zip(&b.verts).map(|(a, b)| a.dist(*b)).fold(0.0_f64, f64::max);
+        assert!(max_shift > 1.0, "an invented endpoint silently introduces a length constraint");
+    }
+}
+
+fn insert(block: &str) -> Insert {
+    Insert {
+        block: block.into(),
+        insert: Vec3::ZERO,
+        scale: Vec3::new(1.0, 1.0, 1.0),
+        rotation: 0.0,
+        attribs: vec![],
+        cols: 1,
+        rows: 1,
+        col_spacing: 0.0,
+        row_spacing: 0.0,
+    }
+}
+
+#[test]
+fn xref_contents_do_not_block_host_export() {
+    let mut d = drawing(text(HAlign::Left, VAlign::Baseline));
+    let mut external = Block::new("External");
+    external.xref_path = Some("external.dxf".into());
+    external.entities.push(Entity::new(d.new_handle(), EntityKind::Text(text(HAlign::Fit, VAlign::Baseline))));
+    d.blocks.insert(external.name.clone(), Arc::new(external));
+    let mut dependent = Block::new("External|Child");
+    dependent.entities.push(Entity::new(d.new_handle(), EntityKind::Text(text(HAlign::Aligned, VAlign::Baseline))));
+    d.blocks.insert(dependent.name.clone(), Arc::new(dependent));
+    d.add(&Space::Model, Common::default(), EntityKind::Insert(insert("External"))).unwrap();
+    for version in Version::ALL {
+        let back = reopen(&d, "host.dwg", version, false);
+        assert!(back.block("External").unwrap().entities.is_empty());
+        assert!(back.block("External|Child").is_none());
+        // The same contents are rejected if they belong to a stored local block.
+        let mut local = d.clone();
+        Arc::make_mut(local.blocks.get_mut("External").unwrap()).xref_path = None;
+        assert!(cadcraft_io::write_with(&local, "local.dwg", &SaveOptions { version: Some(version), binary: false }).is_err());
+    }
+    assert_eq!(d.block("External").unwrap().entities.len(), 1);
+    assert!(texts(&d).iter().all(|t| t.align_pt.is_none()));
+}
+
+#[test]
+fn uncommitted_refedit_objects_do_not_block_export() {
+    let mut d = Drawing::new_metric();
+    let mut block = Block::new("Tag");
+    let original = d.new_handle();
+    block.entities.push(Entity::new(original, EntityKind::Text(text(HAlign::Left, VAlign::Bottom))));
+    d.blocks.insert(block.name.clone(), Arc::new(block));
+    let handle = d.add(&Space::Model, Common { visible: false, ..Default::default() }, EntityKind::Insert(insert("Tag"))).unwrap();
+    let copy = d.add(&Space::Model, Common::default(), EntityKind::Text(text(HAlign::Fit, VAlign::Baseline))).unwrap();
+    d.refedit = Some(cadcraft_doc::RefEdit {
+        block: "Tag".into(),
+        insert: handle,
+        insert_was_visible: true,
+        space: Space::Model,
+        to_world: Mat3::IDENTITY,
+        items: vec![cadcraft_doc::WorkItem { copy, source: Some(original) }],
+        ghosts: vec![],
+        released: vec![],
+    });
+    for version in Version::ALL {
+        let back = reopen(&d, "during-edit.dwg", version, false);
+        assert!(back.model.get(copy).is_none());
+        assert!(back.model.get(handle).unwrap().common.visible);
+        same_geometry(&d.committed(), &back);
+        // Objects added from the host (source=None) survive discard and must be validated.
+        let mut retained = d.clone();
+        retained.refedit.as_mut().unwrap().items[0].source = None;
+        assert!(cadcraft_io::write_with(&retained, "retained.dwg", &SaveOptions { version: Some(version), binary: false }).is_err());
+    }
+    assert!(d.refedit.is_some());
+    assert!(d.model.get(copy).is_some());
+    assert!(!d.model.get(handle).unwrap().common.visible);
+    assert!(texts(&d).iter().all(|t| t.align_pt.is_none()));
+}
+
+#[test]
+fn emitted_text_records_reject_missing_endpoints_and_nonfinite_implicit_anchors() {
+    let mut cases = vec![text(HAlign::Aligned, VAlign::Baseline), text(HAlign::Fit, VAlign::Baseline)];
+    for coordinate in 0..3 {
+        for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
+            let mut t = text(HAlign::Right, VAlign::Top);
+            match coordinate {
+                0 => t.insert.x = value,
+                1 => t.insert.y = value,
+                _ => t.insert.z = value,
+            }
+            cases.push(t);
+        }
+    }
+    for t in cases {
+        for location in 0..5 {
+            let mut d = Drawing::new_metric();
+            let mut a = attribute();
+            a.text = t.clone();
+            match location {
+                0 => {
+                    d.add(&Space::Model, Common::default(), EntityKind::Text(t.clone())).unwrap();
+                }
+                1 => {
+                    d.add(&Space::Paper("Layout1".into()), Common::default(), EntityKind::Text(t.clone())).unwrap();
+                }
+                2 | 3 => {
+                    let mut block = Block::new("Tag");
+                    let kind = if location == 2 { EntityKind::Text(t.clone()) } else { EntityKind::AttDef(a) };
+                    block.entities.push(Entity::new(d.new_handle(), kind));
+                    d.blocks.insert(block.name.clone(), Arc::new(block));
+                }
+                _ => {
+                    d.blocks.insert("Tag".into(), Arc::new(Block::new("Tag")));
+                    let mut i = insert("Tag");
+                    i.attribs.push(a);
+                    d.add(&Space::Model, Common::default(), EntityKind::Insert(i)).unwrap();
+                }
+            }
+            let before = d.clone();
+            for version in Version::ALL {
+                let Err(error) = cadcraft_io::write_with(&d, "invalid.dwg", &SaveOptions { version: Some(version), binary: false }) else {
+                    panic!("location {location} must reject {t:?}");
+                };
+                let reason = if matches!(t.halign, HAlign::Aligned | HAlign::Fit) { "second alignment point" } else { "must be finite" };
+                assert!(error.to_string().contains(reason), "{error}");
+                assert!(d.model.same_as(&before.model), "failed export must not mutate the source");
+                assert!(texts(&d).iter().all(|t| t.align_pt.is_none()));
+            }
+        }
+    }
+}
+
+#[test]
+fn dwg_resolves_text_records_in_blocks_layouts_and_attributes() {
+    let mut d = drawing(text(HAlign::Left, VAlign::Bottom));
+    d.add(&Space::Paper("Layout1".into()), Common::default(), EntityKind::Text(text(HAlign::Left, VAlign::Bottom))).unwrap();
+    let mut block = Block::new("Anchors");
+    block.entities.push(Entity::new(d.new_handle(), EntityKind::Text(text(HAlign::Left, VAlign::Bottom))));
+    block.entities.push(Entity::new(d.new_handle(), EntityKind::AttDef(attribute())));
+    let insert = insert("Anchors");
+    // Stored records, not expanded references: even a cycle must terminate.
+    block.entities.push(Entity::new(d.new_handle(), EntityKind::Insert(insert.clone())));
+    d.blocks.insert(block.name.clone(), Arc::new(block));
+    d.add(&Space::Model, Common::default(), EntityKind::Insert(Insert { attribs: vec![attribute()], ..insert })).unwrap();
+    for version in Version::ALL {
+        let back = reopen(&d, "locations.dwg", version, false);
+        let records = texts(&back);
+        assert_eq!(records.len(), 5);
+        for t in records {
+            assert_eq!(t.align_pt, Some(Vec3::new(12.0, 7.0, 5.0)));
+            assert_eq!(t.justify_point(), Vec2::new(12.0, 7.0));
+        }
+    }
+    assert!(texts(&d).iter().all(|t| t.align_pt.is_none()));
+}
