SDF is the only text renderer now — the stroke path and its LOD ladder are
gone, and the remaining text kinds render as SDF glyph quads too.
- ATTDEF / ATTRIB: carry a GlyphRun so single-line attribute text renders as
SDF (multi-line already did via layout_mtext). Tolerance / complex-linetype
text already SDF.
- Remove `sdf_text_enabled()` (the OCS_TEXT_SDF flag / kill switch) and take
the SDF path unconditionally everywhere: tessellate Text arm (per-group by
GlyphRun presence), hit_test fallbacks, render gather, complex_lt, table,
mleader.
- Delete the text LOD ladder: tess.rs baseline/greek block and the
block_cache greek/baseline path (emit_greeked_text / emit_text_baseline),
plus the now-dead helpers text_baseline_points / text_greek_obb_tris /
text_obb_corners_native / mtext_line_count and the LocalWire
text_height_local / text_obb_local fields. All text is crisp at every zoom;
text entities are exempt from the sub-pixel stub cull.
- Fix a latent bug: block-internal SDF text was culled because its LocalWire
aabb_local was [0,0,0,0] (no points) — fold the glyph-quad extent into
aabb_local and exempt text_verts-carrying wires from the sub-pixel cull.
- Clear the pre-existing dead-code warnings (TextPlacement.elevation, TabKind,
MTextRunKind::Tab).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Two related steps for the text pipeline:
- TEXT parsing now goes through acadrust's parse_plain_text (like MTEXT
uses parse_mtext). text::acad_text_encode parses the value's %% codes —
including %%u/%%o underline/overline and %%nnn — and re-encodes to the
stroke tessellator's \L/\O + resolved-Unicode grammar, so OCS no longer
tokenizes TEXT %% codes itself. (ATTRIB/TOLERANCE/DIMENSION still use the
local tokenizer for now.) Bumps the acadrust pin to the commit that
completes the TEXT/MTEXT control-code set.
- SDF text (OCS_TEXT_SDF) now covers MTEXT and every text-producing
entity, not just top-level TEXT. TextStroke carries a GlyphRun (layout
params); the collector walks convert()'s TruckObject::Text runs and
applies annotation scale exactly as tessellate does for strokes.
layout_glyph_quads shares lff::tokenize_run so %% specials resolve and
\L/\O decorations are skipped (decoration LINES in SDF are still TODO).
Verified: stroke rendering unchanged with the flag off; SDF glyphs render
correctly for TEXT and MTEXT with OCS_TEXT_SDF=1. 154 lib tests green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
OCS maintained a second MTEXT inline-code parser that duplicated
acadrust's `mtext_format::parse_mtext`. Replace it with an adapter
(`adapt_mtext_paragraphs`) that maps acadrust's structured MTextDocument
onto the existing layout types (`MTextLine`/`RunState`); the layout
engine (`layout_mtext`, `mtext_line_count`, and their callers) is
unchanged. Removed the hand-rolled `parse_mtext_paragraphs*` and its
helpers.
Validated during migration by a differential test (adapter vs the old
parser matched byte-for-byte over a 20-case corpus) and confirmed
rendering identically in the app; the differential test is replaced with
adapter-only unit tests. Relies on acadrust's new `MTextScalar` to tell
absolute `\H` from relative `\Hx` (bumped the acadrust pin).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
resolve_font previously returned &str, passing raw stems like "arial" verbatim to Face::resolve. System font lookup requires the canonical family name ("Arial"), so inline \f/\F overrides from block-nested MTEXT were silently unresolvable and fell through to no geometry.
Change resolve_font to return Cow<'a, str>:
- no override -> Borrowed(base), zero allocation
- LFF built-in -> Borrowed(font), zero allocation
- system font resolved -> Owned(canonical), one allocation
- unresolvable -> Borrowed(base), falls back to style font
All 4 call sites updated to &font_name (&Cow<str> derefs to &str).
Adds two unit tests covering the fallback and TTF-stem-to-family-name paths. Adds integration test verifying block-nested MTEXT produces geometry and correctly separates outline/fill wires.
Follow-up cleanups on the merged TTF-fill work:
- Drop the unconditional resolve_text_style log line — it fired for every
text style and spammed ~7k stderr lines opening a single real drawing.
- Drop the per-glyph tessellation-error eprintln; a malformed font would
otherwise log once per character. The glyph still falls back silently.
- Tessellate glyph fills with the nonzero winding rule (lyon defaults to
even-odd, which mis-fills TrueType outlines whose contours overlap).
- Make the sysfont prefix fallback deterministic (sorted) and require ≥3
chars so a 1–2 letter request can't grab an arbitrary family.
- Remove the unrelated windows-gnu --exclude-all-symbols linker flag that
slipped into .cargo/config.toml.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Text elements previously rendered as hollow wireframe outlines and sometimes
fell back to the wrong system font due to case-sensitive exact matching. Also,
the viewport background grid drew on top of solid entities.
Case-insensitive and partial-name fallback matching (e.g., mapping "arialn"
to "Arial Narrow") is now added to `sysfont::canonical_family_name`, ensuring
text resolves to the correct installed system font.
TrueType font contours are now triangulated into solid fills (`fill_tris`)
using `lyon_tessellation` in `ttf_glyph.rs`. To prevent the GPU from misclassifying
this 2D text as a 3D mesh (which broke flat shading), text tessellation splits
the output into separate outline and fill `WireModel`s.
The background grid is extracted from `SelectionCanvas` into `GridCanvas` and
moved beneath the 3D viewport to ensure proper layering. Finally, `DepthBiasState`
is enabled on all face/fill pipelines to apply a polygon offset, perfectly
resolving Z-fighting between coplanar wireframe lines and solid faces.
Two issues in a file whose dimensions carry baked *D### blocks.
Missing diameter text: the baked MText values are "\U+220520" /
"\U+220510" — the \U+2205 (⌀) escape immediately followed by the literal
digits "20"/"10". The MText parser read up to six hex digits for a \U+
escape, so it greedily consumed "220520" = 0x220520, which is past the
maximum code point, char::from_u32 returned None, and the whole text
dropped. A \U+ escape is exactly four hex digits; read four so the symbol
decodes and the trailing digits stay literal. (Angular "90\U+00B0" only
worked because its escape sits at the end of the string.)
Node artifacts: PDMODE=0 points were drawn as a fixed 0.1-world-unit "+",
which in a small drawing turned the 31 dimension def-points on the
Defpoints layer into visible crosses. Size the dot from world-per-pixel
(~1 px, zoom-adaptive via the tessellation cache key) to match the near-
invisible dot other CAD apps draw.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
In the files where ox/oy/oz came solely from the now-zero offset
destructure (tessellate, block_cache, dimension, leader, text_support),
remove the `let [ox,oy,oz] = [0,0,0]` binding and the `- ox` arithmetic so
the coordinate math reads as the plain absolute value it now is.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Now that geometry reaches the GPU/CPU as absolute coordinates via the
double-single relative-to-eye path, world_offset is always [0,0,0]. Strip
the parameter that was threaded through the whole tessellation / block-
expansion / fallback / camera-decode chain and the ExpandCtx field that
carried it — ~340 references across 20 files, all subtracting zero.
- truck_tess: to_local / to_local_low / tessellate_* drop the offset arg;
to_local is now a pure double-single split.
- tessellate: offset_to_ds → points_to_ds (pure DS split); tessellate(),
tessellate_entity(), fallback_geometry(), solid_wire_fallback(),
entity_aabb(), expand_insert(), expand_block_meshes(), the dimension
helpers (vec3_local, dimension_snap_pts, …), text_support, leader,
multileader, image_model, xclip and camera_from_view all lose the param.
- block_cache: ExpandCtx loses its world_offset field.
- Remove the now-dead offset_snap_pts and the unused set_grid_snap.
Behaviour is unchanged (the offset was zero everywhere); the compiler
verifies every call site.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Single-line TEXT and ATTRIB/ATTDEF cast the insertion/alignment point to
f32 before computing the stroke origin. At large coordinates (UTM ~4.5M)
the f32 ulp is ~0.5, snapping text baselines onto a coarse grid so
adjacent rows collapse onto each other and overlap. Keep the anchor in
f64 throughout.
Horizontal alignment now uses the pen advance box [0, advance] instead of
the inked min_x/max_x, so leading/trailing spaces keep their width and
space-aligned columns no longer shift. text_local_bounds returns
TextLocalBounds { ink_min, ink_max, advance }.
TABLE laid out its whole grid in an f32 world frame; switch to a local
f32 frame plus an f64 base added at the widening step, fixing cell-text
baseline snap and the i32 overflow in the border-dedup keys.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Move the 30 flat modules under src/scene/ into six folders by role:
convert (entity→geometry), text (fonts/shaping), model (render models),
pick (hit-test/selection/grips/xclip), view (camera/transform/render),
cache (block + property caches). pipeline/ is unchanged.
Full path migration — all `scene::X` references become
`scene::<group>::X`, no compatibility re-exports. render.rs items that
were `pub(super)` (parent was scene) become `pub(in crate::scene)` to
keep their original scene-wide visibility.
No behavior change; build and tests are green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add a TrueType/OpenType render path alongside the LFF stroke engine so
users can pick fonts installed on their machine for text and MText.
- Discover system families via fontdb (sysfont) and list them in the
text-style picker.
- Extract glyph outlines with ttf-parser, flattened into wire strokes
normalized to the same 9-unit cap-height space as LFF glyphs.
- Shape runs with cosmic-text (ligatures, Arabic joining, kerning, bidi)
plus automatic font fallback: a glyph missing from the chosen family
is taken from another installed font.
- Unify both sources behind a single Face type; a shared run tokenizer
drives the stroke (per-glyph) and shaped (TTF) renderers identically.
- Stroke fonts now fall back to a system TTF (cosmic-text picks it) for
characters no LFF font covers, replacing the unicode/iso3098 chain.
Also trim the embedded LFF set from 47 to 26, keeping the AutoCAD-SHX
equivalents and dropping LibreCAD-only fonts now covered by TrueType.
LFF builtins keep priority over same-named system fonts so DXF text
renders identically across machines.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Type directly into the rendered MText preview: click to position the
caret, insert/delete, Enter for a new line, Space for a literal space,
and arrow keys to move. The caret blinks when idle.
Keep the trailing empty paragraph in the editor so the caret appears on a
fresh line immediately after Enter, before any typing. parse_mtext_paragraphs
split into a _ex variant; layout_mtext skips the blank-edge trim when
emitting glyph boxes.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Replace the small QCAD CXF stroke-font set with the full LibreCAD LFF font
library (46 fonts) and a new LFF parser/engine in src/scene/lff.rs. The
parser reads the `[hex] char` glyph blocks, tessellates `,A<bulge>` arcs, and
resolves `C<hex>` glyph references. Per LibreCAD's RS_Polyline convention a
vertex's bulge curves the segment ending at that vertex — getting this wrong
turned round letters like O into sideways capsules.
Font name resolution mirrors LibreCAD: match by file stem, map common
AutoCAD/DXF SHX names (txt, isocp, complex, italic, gothic*, gost, …) to the
nearest LFF family, and fall back to `standard`. A missing glyph in the chosen
family falls back to the broad `unicode` font so accented/Cyrillic/Greek text
still renders. Cap height stays 9 units, so the text scale is unchanged.
All text consumers move from cxf:: to lff::; complex linetypes still render
embedded text but no longer draw embedded shapes (LibreCAD ships no shape
file). The CXF module and every *.cxf asset are removed, and the MText / text
style font pickers list the LFF families.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The rendered MText preview is now interactive: drag across glyphs to select
a character range and apply toolbar formatting (bold, italic, underline,
overline, strike, case, alignment) to exactly those characters.
The text layout gains an opt-in pass that emits one world-space box per
visible character carrying its reading-order index; the editor maps those
boxes into the preview frame, hit-tests pointer drags against them, and
highlights the selection. Formatting splices the matching inline codes into
the raw value at the selected byte range (mapped via a code-aware visible-
character scanner). With no preview selection the Edit-box path is unchanged.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Five chunks moved out of the 5965-line scene/mod.rs to live with the
entities they belong to:
- wide_lwpolyline_fills → entities/lwpolyline.rs::wide_fills
- wide_polyline2d_fills → entities/polyline.rs::wide_fills
- polyline_segment_fill → entities/common.rs (shared by both above)
- text_obb_corners_native + text_baseline_points + text_greek_obb_tris
→ entities/text_support.rs (handles Text/MText/Attribute alike)
- append_insert_attribute_wires → entities/insert.rs
Net: scene/mod.rs 5965 → 5404 lines (~560 lines extracted). Three
helpers in scene/render.rs / scene/block_cache.rs widened from
pub(super) to pub(crate) so the relocated functions can still reach
them. scene::entity_aabb also widened for the same reason.
scene::tessellate_entity_dim_text stays put — it's a thin wrapper
around the private tessellate_entity dispatcher and moving it would
require exposing tessellate_entity's full signature across the crate.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Lift the MText parser + renderer into a reusable `layout_mtext` helper in
`text_support` and re-point the remaining text-bearing entities at it, so
every inline format code (`\f`, `\C`/`\c`, `\H`, `\W`, `\Q`, `\T`, `\A`,
`\p…`, decorations, stacked fractions, paragraph indents, tab stops, …)
reaches the stroke output uniformly.
Per-entity changes:
- TABLE cell ([entities/table.rs]) — anchor + alignment math now feeds
`layout_mtext`; `\C7;Red`-style codes no longer render as literal
glyph noise.
- MLEADER MText content (both entity path in [entities/multileader.rs]
and the LOD-aware path in [scene/tessellate.rs]) — drops the
`strip_mtext_codes` + per-line `tessellate_text_ex` chain; the LOD 0
baseline / LOD 1 greek rect / LOD 2 stroke ladder and the text-frame /
bg-fill rectangle now read line widths from the layout.
- ATTRIB / ATTDEF — when `mtext_flag` is MultiLine / ConstantMultiLine
the value goes through the MText pipeline (SingleLine stays on the
Text-style anchor maths because the DXF spec disallows codes there).
- DIMENSION text_override — `dimension_text_entity` emits a synthetic
MText (instead of Text) whenever the value contains MText escapes;
the LOD ladder via `tessellate_entity_dim_text` handles both kinds.
Infrastructure:
- `MTextVAnchor` gains `MiddleOfTopLine`, `MiddleOfBottomLine`,
`BottomOfTopLine` so MLEADER's 9 attachment variants all map cleanly.
- `MTextLayout` returns `line_widths` / `line_height` / `v_offset` so
callers that draw chrome (frames, fills, baselines, greek rects)
share the same numbers as the rendered strokes.
- `mtext_line_count` re-routed through the new parser so LOD row counts
and rendered row counts can never disagree.
- Legacy helpers (`strip_mtext_codes`, `split_mtext_lines`,
`measure_mtext_chars`, `word_wrap`, `cxf::measure_text`, the
`v_offset_for_attachment` MLEADER offset table, `MTextLine::plain_text`)
deleted — all callers migrated.
Audit (TEXT_FORMAT_CODE_INTEGRATION.md) removed; its plan is implemented
here.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
`text_obb_corners_native` derived MText line count from explicit `\n`
matches in `m.value`, so any MText that relied on `\P`/`\N` breaks or
word-wrap (rectangle_width-driven) reported a single line. The greek
emitter then split the OBB into one row and the user saw a single box
on top of a 5-line wrapped paragraph.
- text_support: new `mtext_line_count` — strip codes, split on
\n/\P/\N, word-wrap each paragraph against `rectangle_width`,
sum. Same line splitting the MText/MultiLeader renderers already
perform; centralized for reuse.
- text_obb_corners_native: takes `mtext_lines_override: Option<usize>`.
When supplied, OBB height = h_world × n × line_spacing_factor; this
also overrides a possibly stale `rectangle_height` on resaved DWGs.
- Top-level greek paths (scene/mod.rs) compute the line count via
`mtext_line_count` and pass it through to baseline / rect emission.
- block_cache: tessellate_sub_local computes the count at defn build
so the stored `text_obb_local` is wrap-aware; emit_text_baseline
and emit_greeked_text recover n_lines from `ulen / h_local` as
before and now produce the correct per-row split inside Inserts.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Previously \\U+XXXX was in the "strip until ;" list, silently dropping
all Unicode character references (°, ±, ⌀, Ø, €, …).
Changes:
- \\U+XXXX / \\u+XXXX decode to the actual Unicode character (was stripped)
- \\S upper/lower; stacked fractions/tolerances render as "upper/lower"
or "upper^lower" instead of leaking as literal text
- \\t tab → four spaces (was leaking as literal 't')
- Unknown escapes silently consume the code char (was leaking it)
- \\M+...; DBCS multibyte — stripped (added to semicolon-arg list)
- \\X paragraph-align end marker — stripped cleanly
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Line spacing: DXF code 44 (line_spacing_factor) is a multiplier on
AutoCAD's default baseline-to-baseline distance of 5/3 × text_height.
The renderer was using factor × 1.0 instead of factor × 1.667, making
all MTEXT appear cramped.
Blank paragraphs: split_mtext_lines was filtering out empty strings, so
\\P\\P (double paragraph break used to separate sections) was silently
collapsed. Empty lines in the middle of the block are now preserved;
only leading/trailing blank lines are stripped.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Spline entities now expose fit_points (or control_points when no fit
points are defined) as key_vertices, enabling Endpoint and Midpoint
snap to reach spline construction points.
TextStyle is_backward flag is applied by negating the width_factor,
producing horizontally mirrored text. is_upside_down rotates the text
180°. Both tessellate_text_ex and text_local_bounds now handle negative
width_factor without clamping it to zero.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Text/MText values now correctly handle DXF special-character sequences:
- %%d → °, %%p → ±, %%c → ⌀, %%nnn → Unicode scalar (resolved in tessellation)
- %%u / %%o toggle underline and overline strokes for TEXT entities
MText decoration codes (\L/\l, \O/\o, \K/\k) were previously consumed
as argument-bearing codes, silently eating the decorated text. Fixed
strip_mtext_codes to keep them as zero-width markers; tessellate_text_ex
now emits actual line strokes (underline, overline, strikethrough).
measure_mtext_chars and word_wrap skip markers so line widths are correct.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>