Reference-only entities (plugin API v9): a scene-scoped, in-memory
designation for plugin-owned display geometry, plus HostNotification
dispatch to native plugins so they can re-mark on DocumentOpened.
Resolves the new DeltaSnapshot site in block_editor.rs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The end angle subtracted a whole octant less the end offset, so the arc
in the ISOCP 9 stopped about 45 degrees short and the glyph shrank to
0.68 of the cap height; clockwise arcs and n = 0 (a full circle) drew
nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ISOCPEUR, ISOCTEUR and the other aliased names are real fonts; with one
installed, its face draws the text instead of the stroke substitute,
which has no Cyrillic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The web build has no font folder, so a text style whose TrueType file is
not a bundled Noto subset falls back to it. font_register takes the file
from the host page:
{"op":"font_register","file_name":"Brand-Regular.ttf","data_base64":"…"}
The faces join the font database, file_name resolves to the family of
the first face (case-insensitive, without directory, like installed
fonts), and open drawings lay their text out again (fallback glyph
cache cleared, text geometry invalidated per tab). The reply names the
family and the number of faces. Bytes without a face fail with
font_invalid. SysFonts::by_file becomes updatable for this.
Proposed in #1771.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every further reference to an already expanded block (a translated copy
of the same definition, at the top level or nested) was materialised as
a full copy of every wire, although the GPU already draws those copies
from one shared source with a per-copy offset. Such a copy is now a
single stub wire holding the shared prototype and its offset (stubs nest
inside prototypes, so nested repeats collapse too).
- The block renderer expands stubs into instance references while it
groups by source, so drawing is unchanged.
- Interaction queries expand only the stubs near the query, so snapping,
picking, window and crossing selection see each copy's geometry.
- Plot, boundary detection, viewport projection and entity_wires() get
fully expanded wires; count and reference-edit recolouring expand the
set while they are active.
- OCS_NO_BLOCK_STUBS turns it off.
A drawing with 480 references to a block of 100 nested 50-line blocks
(2.4M lines) now opens with 11k wires instead of 2.4M: peak memory
5.4 GB -> 0.37 GB, wire assembly 2.4 s -> 36 ms, interaction index
3.7 s -> 23 ms. The viewport, a plotted PDF, snaps and selections are
identical with and without stubs, and on five real drawings the view
renders the same.
Refs #1698, #1542
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Host side of HostApi::set_reference_only: plugin-owned display geometry that
renders and snaps normally but must not be edited or picked by the person at
the screen, while the owning plugin keeps full write access through the host.
- Storage: a human-readable marker value in an XDATA record under the
host-owned OCS APPID (registered on first use; foreign values survive
verbatim; stale verbatim DWG EED is dropped on rewrite), so the flag
round-trips DWG and DXF with the entity and cannot collide with plugin
APPID namespaces.
- Grips: refresh_selected_grips yields none for a reference-only entity and
an active grip edit on a newly designated target is cancelled like a
locked-layer one.
- Picking: a reference-only wire flag extends the snap_only skips in every
hit test, so a click passes through to the editable entity underneath, and
box, window, lasso, fence and select-all gathers skip the designation;
fills, solids and instanced meshes are excluded through the shared
candidate handle set.
- Write policy: built-in user paths treat a reference-only entity like a
locked-layer one (Scene::update_entity / update_entities refuse,
erase_entities skips), while the plugin-owned variants
(update_entity_plugin / erase_entities_plugin, mirroring the
remove_entities editor split) and the whole HostSession surface write
through. The MCP entities read exposes a top-level reference_only boolean
and set_properties refuses with the reference_only failure code.
- Undo: the entity-write contract - a change joins a plugin-opened push_undo
scope, a scope-less change is one undo step of its own, refusals and
idempotent re-marks record nothing.
- Known limit: entities nested inside block instances (the block cache
rebuilds instance wires without the flag).
Evidence: marking_flags_and_unmarking_clears,
foreign_values_under_the_host_app_id_survive, other_apps_are_untouched,
reference_only_refusals_record_no_undo_step,
reference_only_designation_composes_undo_like_entity_writes,
reference_only_survives_dxf_and_dwg_round_trips,
user_paths_refuse_and_plugin_paths_write_reference_only_entities,
select_all_visible_skips_reference_only_entities,
clicks_pass_through_reference_only_wires,
box_selections_skip_reference_only_wires,
entities_records_expose_the_reference_only_designation.
Review of the image wave (with empirical reproduction against the
vendored image 0.25) found the budget contract did not hold across
formats: the decoder's Limits reserve checks NATIVE bytes-per-pixel,
so a grayscale/RGB JPEG/PNG/BMP over the RGBA budget decoded anyway
and into_rgba8() then ballooned to up to 4x it — the tab death the
budgets exist to prevent. The probed dimensions already prove an
OverBudget image cannot fit, so it is now refused without decoding,
removing all dependence on per-decoder limit fidelity.
Conversely, image's TIFF codec stacks ~3x scratch buffers on top of
the reserve, so an in-budget TIFF (the flagship GIS scenario) was
refused with a self-contradictory note ('400 MB, over the 512 MB
limit'); the decoder allowance for TIFF is now 3x while tier math
stays on the real budget.
Also: the refusal message saturates instead of overflowing on crafted
headers, the image pick only matches the refusal note naming ITS file
(peek, not drain — other references' notes survive for their own
drains), and the Georgian Supplement range ends at U+2D2F (U+2D30.. is
Tifinagh, which ships no font).
(cherry picked from commit 58d3789bcf)
Ported onto main (#1717 review): the build-web.sh wrapper and its
pages/README/Trunk wiring stay out — it works around a local Windows
trunk dist lock; the Linux Pages build is unaffected.
Giant rasters (500 MB aerial/GIS TIFFs) decoded whole: multiple GB of
RGBA on a wasm heap is tab death, and even desktop silently failed
past the image crate's 512 MiB default allocation cap. Decoding now
probes the header for dimensions first and runs in tiers — full decode
under a per-target budget (512 MB wasm / 4 GB native, enforced through
the decoder's allocation limits so oversize fails fast instead of
ballooning), Lanczos3 downsample to a display cap (4096² wasm /
8192² native) when the decode fits but the buffer is oversized, and a
clear localized refusal naming the dimensions and the limit when it
cannot. The probed source dimensions stay the entity's sizing truth,
so a downsampled buffer never shrinks the on-paper footprint; PDF
export self-corrects (effective DPI derives from placement × stored
pixels). Refusals surface once per image through the pick, open, and
drop/re-resolve paths.
Decode coverage gains webp and gif (pure Rust in image 0.25 — no C,
no lockfile churn), with the attach picker and drop filters matching.
Remote http(s) raster references now fetch on web: CORS-classified
failures name CORS, HTTP errors name the status, bodies over 64 MB and
8 s deadlines are bounded, and fetched bytes join the session source
registry (so ARCHIVE can package them and re-resolution reads cache).
Georgian and Armenian Noto subsets ship (generation blocks included),
so those scripts render instead of skipping; scripts still without
subsets remain silently skipped, unchanged.
(cherry picked from commit ca0d7b96)
Ported onto main (#1717 review): Armenian and Georgian take the script
indices after Lao, which main added meanwhile.
A fresh web-vs-desktop audit found the remaining gaps; this closes the
top ten.
Raster images: IMAGE attach read the picked file via image::open on a
name-only path (dead on wasm), and raster references inside opened
drawings decoded the same way. Picked bytes are now registered in a
name-keyed store (the PDF-underlay pattern), decoding runs from memory,
and dropped .png/.jpg/.bmp/.tif files register by name so unresolved
references in opened drawings re-resolve and render (and export to PDF).
Command gaps fixed with the bytes-at-pick / download_bytes pattern:
- WBLOCK now downloads the extracted block instead of failing on fs.
- SAVEALL saves every dirty tab instead of printing a no-op message.
- SCRIPT picks and reads the .scr file through the browser picker.
- CENTERLTYPEFILE picks the .lin/.dnl file and populates from text.
- Custom .ctb tables picked on web now parse, register for the
session, appear in the plot dialog list, and apply (load_named
previously returned builtin-only on wasm).
- LAYTRANS 'Load standards file' parses the picked drawing on the
existing web worker.
- INSERT 'from file' in the block palette parses picked bytes and
feeds the shared import path.
Text fidelity: TTF-named styles rendered as thin stroke glyphs on web
because the font database was empty. Fetched Noto faces are now
registered into it with a family alias table (Arial, Times New Roman,
Cambria, ... -> Noto Sans/Serif), so those styles render filled, shape,
export to PDF as vector fills, and appear in the style picker.
ViewCube labels request semibold again on web.
docs/native-vs-web.md: the hatch and font rows were stale (both work on
web now), the GPU row claimed WebGPU is used where available (the
backend is pinned to OpenGL), and the parallelism row missed the parse
worker — all corrected.
(cherry picked from commit c643a10e)
Ported onto main (#1717 review): conflicts resolved toward main — the
font-file lookup, PDF searchable text, purge service, block palette and
plot-style copy stay as they are on main; the web pieces (font registry
and aliases, session plot styles, SCRIPT/SAVEALL/CENTERLTYPEFILE picks,
block-from-file on the web) are layered on top.
- Language gains lo-LA, labelled ລາວ in every catalog, so the new
translation can be chosen.
- Two Lao keys under stale names now use the catalog's names, and the
touchpad option added since gets its Lao label.
- The web build gets a Lao font (Noto Sans Lao, generated like Thai) and
routes Lao and Thai drawing text to their fonts; Thai had a font but
was never routed to it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- One visible-named LRM constant (ttf_glyph) instead of two invisible
literals. Stroke and SDF text skip it, so a right-to-left script the
shaper does not handle no longer gets a 6-unit gap per mark; vertical
MTEXT does not insert it at all.
- Shaped cells keep a cluster the shaper merged into the leading mark.
- The always-false RTL paragraph flag, its indent/anchor branches and the
unused reorder_line_atoms are gone; its test now covers reorder_rtl_runs.
- Underline, overline and strike follow each drawn run of a mixed word.
- Justify/distribute count only the gaps the measured width holds.
- The two RTL alignment tests assert the new left-to-right behaviour;
glyph_boxes keeps its own doc comment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The ROMANS advance list was pasted twice; once is enough.
- The stroke-font golden checksum follows TXT's new advances (segment and
vertex counts unchanged).
- The tolerance compartment test checks ink against pen only for symbol
cells: text fonts now advance by the reference's pen, not their ink.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
txt, simplex and romans keep the reference fonts' printable ASCII pen
advances, so lines measure, wrap and align like the reference.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SHX names the embedded LFF fonts stand in for (txt, monotxt,
simplex, romans, romand, romanc, complex, italic, gdt, isocp) use the
space advance of the reference's fonts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MTEXT paragraphs keep left-to-right order and their attachment's
alignment even when they start with Hebrew or Arabic: a run of
right-to-left words (with the spaces between and after them) reads
backwards in place, and inside a word each right-to-left letter run is
reversed on its own while digits, punctuation and Latin keep their
order. Shaped text gets a left-to-right paragraph direction, so TEXT
follows the bidirectional rules with a left-to-right base, and a TEXT
line that starts with Hebrew or Arabic is no longer right-aligned. A
paragraph's leading spaces keep their width and trailing spaces are left
out of the width used for centring and right alignment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Layout glyph cells carry their unrotated cell, so a field box in rotated
MTEXT is placed exactly; multi-line attributes box each line of a field
from their own layout cells and single-line attributes split on \P per
line; shaped words (TrueType fonts, right-to-left and complex scripts)
take the shaper's glyph cells for their characters instead of evenly
divided slots. The field background is also drawn for text rendered as
shaped strokes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1580 built a GlyphSource for every fallback glyph, so drawing a CJK
character on screen copied the whole fallback font (tens of MB) into a
process-wide cache whether or not a PDF was ever exported. The source is
now built only when fallback_source asks for it, through the same search.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts with 02c470aa (invisible searchable text layer): main already
depends on allsorts 0.16 with the same features, so its line stays. Both
layers would put the text into the PDF twice, so a run drawn wholly in
faces the PDF can embed now goes out as embedded-font text and skips the
invisible layer; any other run (stroke fonts, mixed scripts) keeps its
outlines and the invisible layer. Faces only probed for such a run are
not installed. The PR's test follows layout_glyph_quads' new tuple.
Real BT/Tj text objects ride alongside the vector outlines (Tr 3,
zero visual change): runs preserved at layout time, TTF styles embedded
as allsorts subsets with ToUnicode, width-matched Tm, clip-culled.
WireModel literals moved to ..Default::default() tails.
PDF export filled every TrueType glyph's tessellated outline, one triangle
per triple. A stroke-heavy CJK face such as DFKai-SB tessellates into
hundreds of triangles per character, so a 28-layout schematic set
exported at 320.9 MB and viewers crawled through it. The atlas export
table was also rebuilt for every text wire, which made the same export
take 566 s.
- ttf_glyph: GlyphSource records the face, glyph id and font-unit scale a
filled outline came from; glyph_source / fallback_source resolve the
same face as glyph / fallback_glyph (fallback picks both in one search).
- Face::glyph_source mirrors Face::glyph; stroke glyphs (LFF, SHX, big
font) return None and keep their polyline export.
- GlyphExport carries the char and the source (filled glyphs only, so
TEXTFILL 0 still exports hollow outlines).
- pdf_export draws a sourced glyph as text (Tf 1 + a Tm that maps its em
onto the SDF quad), subsets each face with allsorts after all pages
(keeping cvt/fpgm/prep for tricky fonts), renumbers glyph ids, and
snapshots the glyph table once per export.
Same set: 566 s / 320.9 MB -> 27 s / 1.42 MB; text is searchable and
lands on the same pixels as before.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Since 2026.39 reads `unifont 1.0` files (isocp.shx and its family among
them) instead of falling back to a substitute, a glyph built from a
subshape went wrong: code 7 is followed by a two-byte code point in a
unifont, but only big fonts read two bytes, so the low byte ran on as a
drawing opcode. ISOCP's `9` came out at x-height as a small bowl.
Fixes#1556
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Frames: follow the drawing state, DWF/DGN frames, image frame profile, frame-only picking
The codec [patch] to ramox81/cadcodec@ea5edaa is dropped: that commit landed as opencadcodec#63 and main already pins ad16215, which contains it.
#1506 paired each comment with a Chinese line; the codebase comments in English. Code and test data are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The Frames list shows the drawing's frame state (hide / display and plot /
display but not plot / frames vary), follows commands, undo and document
switches, and lists the mixed state as a disabled item.
- DWFFRAME and DGNFRAME are frame variables of their own, kept in the header,
and FRAME sets them with the others.
- Out-of-range frame values say "Requires an integer between 0 and 2." and
ask again; a set value reports "Regenerating model.".
- IMAGEFRAME is stored in the image variables as bits (2 is written as 3) and
a drawing without them uses the profile value, which setting IMAGEFRAME
updates and the settings file keeps.
- Raster images, PDF underlays and wipeouts are picked on their frame only;
underlay snap geometry is never picked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DFKai-SB (kaiu.ttf), MingLiU and other early DynaLab fonts assemble each
ideograph from stroke components that only the TrueType instructions move
into place. ttf-parser's `outline_glyph` returns the raw, unhinted `glyf`
outline, so these fonts rendered with missing or displaced strokes
('自' drawn as '目'; 範, 直, 啟, 動 broken). FreeType calls them "tricky"
and always runs the interpreter for them.
Every non-test `outline_glyph` call site in ttf_glyph.rs (`glyph`,
`outline_char` - shared by both native fallback paths -, the web
`build_fallback`, and both `build_shaped` variants) now goes through
`outline_glyph_into`; `outline_char` takes the font data and face index
for that. When skrifa's `require_interpreter()` says the face
is hint-reliant, the glyph is hinted with the TrueType interpreter
(Target::Mono) at 512 ppem and scaled back to font units (x upem / 512);
every other font keeps the exact ttf-parser outline. The decision and the
HintingInstance are cached once per face, keyed by (byte length, face
index, head.checkSumAdjustment) because fontdb may hand out a new buffer
per call. The hinted path is recorded first and replayed only on success,
so a failed draw falls back to ttf-parser without a half-drawn glyph.
skrifa 0.42.1 is already in the lockfile (via hayro / glifo).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every glyph advanced by the face's letter spacing, including CJK glyphs a
style's big font or the TrueType fallback supplies. Their advances already
carry the gap (big font) or side bearings (TrueType), so with an LFF primary
such as iso each ideograph took 1.33 heights instead of one.
Face::spacing_after(ch) gives the tracking only to glyphs the stroke font
draws itself; every layout path uses it.
The 1.8-height widths in the report were the TEXT bounds estimate counting
UTF-8 bytes; opencadcodec 1109d52 counts characters.
Pins opencadcodec 1109d52, opencadkernel 8a246b0, opencadgraph 3c4af1c.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The codec and kernel repositories and crates are renamed opencadcodec and
opencadkernel. Inside OCS they are depended on under short keys, so code
reads `codec::`, `kernel::` and the new `graph::` (opencadgraph, the node
graph engine). File-format and protocol spellings stay as they were: the
ACIS product id `acadrust`, the ACADRUST_* switches and the plugin
version keys `acadrust_version` / `acadrust_source`.
A button left of MCP on the command line opens a translucent node graph
over the viewport, with a palette of object, input, math, logic, list,
text and geometry nodes added by click or drag. An object node owns
drawing entities and its rows are the Properties panel's own rows for
them: the single-object page is extracted from refresh_properties as
entity_property_sections, values use the control protocol's
property_json, and writes go through set_property_value with a property
target override, so the panel, MCP and the graph share one read/write
path. A list reaching an object row makes one entity per element. Each
graph action is one undo step; the canvas pans and zooms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Direct distance entry (#1429) turned every bare number at a point prompt
into a distance, so ROTATE's angle and the values automation types were
read as coordinates. The step reads the token first now — `feed_command`
reports whether it claimed it — and only an unclaimed one becomes a
distance. That also puts the token back on the unified step driver, which
owns the P / L selection keywords and the dimension-snap bookkeeping, and
stops `on_text_input` running twice for the same token. A tracking ray
carries its own base, so it no longer demands an anchor that the first
point of a command does not have yet.
Plugin command execution (#1425) re-implemented the command line inside
the plugin host: it dispatched families directly, skipping the teardown
that starts a command (grip edits, selection gestures, `last_point`, the
accepted snaps), and made every `CmdResult::Dispatch` in the application
skip plugin dispatch to avoid its own re-entrancy. A scripted line now
runs through `run_command_line`, with the trailing Enter left off while
the plugin streams the command, and the re-entrancy is handled by a flag
set for the length of the plugin's call. A pause parks the line's Enter
with the rest of its tokens instead of dropping it.
A font reference (#1432) tried the family lookup before the face-file
index, and that lookup ends in a prefix match a longer file name loses to
(`ISOCPEUI.TTF` would take `ISOCP`). The exact file answers first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
cosmic-text's fallback search can land on a face that claims the character and
outlines to nothing: macOS's system font does that for every glyph, because
ttf-parser reads no contours out of it. The code took the first non-.notdef
glyph id and stopped, so the character drew nothing while believing it drew
something — invisible text, and no warning either.
An empty outline is now treated as no answer: the search moves on to the next
face, and when cosmic-text's own list has nothing that draws the character, the
installed families are walked for one that does. That list is the platform's own
handful of names (on macOS `.SF NS`, `Menlo`, `Apple Color Emoji`, `Geneva`,
`Arial Unicode MS`) and covers neither Cyrillic nor Greek, however many fonts the
machine has that do.
`cargo test --lib a_fallback_glyph_is_never_empty`, and the Turkish letters in
`lff::tests::fonts_parse_and_resolve` render on macOS now instead of failing.
A DWG text style stores its TrueType font as a *file* name — `GOST2304_TypeA_italic.ttf`,
sometimes with a directory — which is what AutoCAD resolves. The family lives
inside the file and is usually a different string (`GOST2304 Type A`), so looking
the reference up as a family missed an installed font and fell through to the
embedded stroke fonts. Those carry no Cyrillic at all (`simplex`, `standard`,
`romans`, `iso3098` hold no U+04xx glyph), so a Russian drawing came out with
every word missing: the letters went on to the system-font fallback, which
returns nothing on macOS.
Installed faces are now indexed by file name, and a reference — a style's font
file, or an inline `\f` code, which carries either form — resolves by family
first and by file name second. An embedded stroke font still wins, so
`romans.shx` keeps rendering as it did.
`cargo test --lib face_files_resolve_to_their_family`
#1418 added both as direct dependencies although each was already in the
tree: printpdf's to_lopdf_doc returns the lopdf document whose compress
and save_to are plain methods, and the codec's encoding_from_code_page
hands back the Big5 / GBK / Shift_JIS / EUC-KR encodings by their DXF
code-page names. Cargo.toml keeps its dependency list.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`shx.rs` only accepted the classic `AutoCAD-86 shapes 1.x` container, so
every modern Autodesk font (`txt.shx`, `simplex.shx`, `romans.shx`, … are
`unifont 1.0`) and every Asian big font (`chineset.shx`, `hztxt.shx`,
`bigfont.shx` — `bigfont 1.0`) was rejected and text fell back to the LFF
substitutes and TrueType fallback glyphs. Drawings authored with SHX big
fonts (the norm for Chinese / Japanese / Korean sheets) never showed their
real glyphs.
- Parse all three containers. Unifont glyphs are keyed by Unicode; big-font
glyphs by their double-byte code from a sparse hash table of
(code, length, offset) entries. Big fonts reference subshapes with a
two-byte `7, 0, hi, lo`.
- `bigfont_glyph` encodes a character with encoding_rs (Big5 / GBK /
Shift_JIS / EUC-KR, most likely first for the file name) and takes the
first code the font has.
- `Face::WithBig` pairs a style's big font with its primary face: full-width
characters come from the big font, everything else from the primary —
how a STYLE with a "Big Font" behaves in AutoCAD. `resolve_text_style`
resolves `big_font_file` like the primary (next to the drawing, then the
user fonts folder) and rides it along in the font name.
- The missing-fonts dialog lists unresolvable big fonts too.
Unit tests cover a synthetic big font (Big5 lookup, cap-height scaling, no
aliasing of unknown codes), a synthetic unifont, and the pairing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
When a text style's SHX / stroke font lacks a character, the glyph comes
from a system TrueType font through `ttf_glyph::fallback_glyph`. That
substitute was normalised to the Latin cap height (9 units = text height),
which is right for Latin, Greek or Cyrillic stand-ins but wrong for CJK:
an ideograph fills the whole em square, and 1 em is 1.3–1.5 cap heights in
every common CJK font (DFKai-SB has no OS/2 cap height at all, so it fell
to the 0.7 em guess). Every substituted Chinese character therefore came
out ~40 % taller and wider than the big font (`chineset.shx`, `hztxt.shx`,
…) the drawing was authored with, and whole lines of notes ran past their
frames.
A big font draws an ideograph one text height tall and one text height
wide, so size full-width / ideographic fallback glyphs by their em box
instead. Other scripts keep the cap-height normalisation. Real TrueType
styles (`Face::Ttf`, shaped runs) are unchanged.
Adds a unit test that a fallback `中` advances by about one text height and
its ink fits the text height (skipped on machines without a CJK font).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Enable fallback font metrics for HarfBuzz shaping in TTF glyph resolver
- Implement Unicode Bidirectional Algorithm (UBA) paragraph base direction detection
- Anchor right-to-left paragraphs to right container boundaries by default
- Reorder line atoms with base direction level to preserve neutral punctuation attachment
- Right-align RTL single-line TEXT and handle editor caret placement on RTL glyphs
Root cause (found by instrumenting build_fallback and reading
ttf-parser's own source, not guessed): cosmic-text's fallback search
picks macOS's system font (San Francisco / SFNS.ttf) for any codepoint
no stroke font or explicitly-requested family covers. It's a variable
TrueType font, and ttf-parser's outline_glyph() resolves a variable
glyph's contours through its gvar table's deltas relative to whatever
variation coordinates are currently set on the Face — an unset Face
has none, so outline_glyph silently returns None (not an error) for
such a glyph. The caller then still returned Some(Glyph { strokes:
vec![], .. }): a "resolved" glyph with nothing to render, no error
anywhere.
Two-part fix:
1. pin_default_variations() sets every variation axis to its own
default value before outlining, applied at all TTF outline-
extraction sites (both build_fallback variants, both build_shaped
variants, and the plain by-family glyph() lookup). This alone
wasn't sufficient for San Francisco specifically — even pinned, its
outline still didn't resolve for the glyphs this bug was found on,
apparently because ttf-parser can't retrieve base glyf data gvar
needs for at least some of that font's accented Latin glyphs.
2. So build_fallback() now treats "resolved a glyph ID but couldn't
extract its outline" as a real failure (outline_from_face returns
None, not an empty glyph) and falls through to a direct fontdb scan
for any other installed font that also covers the character and
can actually be outlined, rather than silently giving up.
Full workspace test suite: 813 passed, 0 failed (previously 812
passed, 1 failed — this was the one pre-existing failure carried
through the entire session).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Switching to a Layout re-tessellated the whole model — `memo_miss=186455`,
2034 ms — to arrive at geometry identical to what was already in the memo.
The only thing that differed was colour.
`local_wires_for` already knows this: it stores the raw colour and drops
its background argument with `let _ = bg_color;`, noting that
`Batches::finalize` applies `adapt_to_bg` last, "so the cache no longer
has to rebuild on BACKGROUND / layout-switch". The tessellation memo's
guard had not been told, and still mixed the background in, so every
entry was discarded on a switch.
`finalize` now records what its colour resolution consumed — the raw
colour, the raw glyph colours, and the three flags it branches on — as
`WireModel::bg_adapt`, and `resolve_colors_for_bg` replays it for another
background. The guard drops the background, so a memo entry serves any
layout.
The resolution recomputes from the recorded raw colour rather than from
the resolved one, which makes it idempotent and therefore safe over a set
that mixes memo hits with freshly tessellated misses. Re-adapting the
resolved colour would not be safe: `adapt_to_bg` snaps a near-white colour
to pure black on a light background, and pure black comes back pure
*white* on a dark one, losing the tint. A test pins that asymmetry so the
raw colour is not optimised away later.
The contract is tested against the real `finalize` rather than a copy of
its logic: for every background pair, building under one and resolving for
the other lands exactly where building under the other would have.
`bg_adapt` is `None` for wires built anywhere else, and the resolution
skips them, so nothing outside the block cache changes colour. It is also
`None` when the resolution would be a no-op — a preserved colour with no
canvas fill looks the same under every background — which keeps the common
case at no cost.
Ordering is unchanged: the pass runs inside the builder, before
`append_scene_markers` and `apply_refedit_fade`, both of which have their
own separate background dependency.
`wires-build` gains `colors=` so the pass has a number of its own.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Share source buffers across block placements and render nested content with GPU instance transforms.\n\nChunk silhouettes and compact mesh material data so large drawings stay under device buffer limits and remain responsive.
Preload one language-aware font resource across the interface, drawing text, and navigation labels.\n\nRender the navigation cube, ring, outlines, and labels through a 4x MSAA target.