#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 <[email protected]>
#1415 made a dimensional Distance keep a negative value's sign in the
parameter while the geometry uses its magnitude (and a zero fold the
points together), but left the value-domain test asserting the old
refusal. Assert the new reading, and keep the radius refusal covered.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Symmetric held the connecting direction perpendicular to the axis with
the kernel's angle-based Perpendicular, which locks its turn side at
construction: a point starting far from its mirror image had to swing
the pair through the axis direction, the residual wrapped, and the
solver stalled with the point unmoved. The perpendicularity is now the
projection of the pair onto the axis direction being zero, a polynomial
residual the solver follows from anywhere. Circle centers and ellipse
centers use the same equation. tests/parametric_constraints_solve.rs
symmetric_constraint_mirrors_one_circles_center_across_the_axis_line
passes again.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
- .github/workflows/ci.yml runs `cargo test --workspace --locked` on
pushes to main and on pull requests, with the Linux libraries the
release build installs. No formatter check: the tree is not
rustfmt-clean and stays that way.
- flake.nix, flake.lock and the direnv hook are removed. Nothing used
them, and their pinned dependency hashes were already stale after the
codec and kernel bumps.
- Under cfg(test) the "system" language resolves to en-US. The unit
tests compare English text through one process-wide loader, and every
apply_config of a default config asked for the system language, so a
Turkish locale failed 22 tests and parallel tests raced each other
through a language switch (the thickness-row test found its section
under one language and looked it up under another).
- The arc grip test follows #1352: the start, end and midpoint grips
drive the whole arc.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
On Windows the temp directory has backslashes, which a Python string literal reads
as escape sequences (C:\Users is a unicode escape), so the RasterImage and OLE
lifecycle tests could not even create their entity. Windows accepts forward slashes.
Found by the first Windows run of the fork build workflow; Linux passed all 60.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
A saved user preference (on by default) toggled by the SCRIPTCOMMANDS command
gates HostApi::run_command; Cancel stays allowed so a waiting command is never
stranded. The runner refuses the SCRIPTCOMMANDS command itself, so a script
cannot change it. Documents why no helper-thread or stack-growing dependency was
added: native builds link with at least 8 MiB, the nesting depth is fixed, and the
deepest step found needs 2 to 4 MiB in a debug build.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Drop two unneeded mut bindings in tests and mark the generated dict/enum
converters allow(dead_code), since a kind can leave one unused.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
polyline_close/open/width/reverse/join (a line or arc is turned into a polyline
first), array_path and array_3d, written against observed prompts and checked
for exact geometry in the real-IPC audit. Polylines default their pick point to
the first vertex. The audit runs on a large-stack thread because PEDIT's
line-to-polyline step needs more than a test thread's 2 MiB in a debug build.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
The generated dict-to-struct conversions for nested records defaulted every
absent field and ignored unknown keys, so an LwPolyline built from
{'x': .., 'y': ..} vertices became zero-coordinate vertices. They now refuse an
unknown key (naming the keys the record takes) and a manifest list,
required_struct_fields, enforces the fields that define a nested record's
geometry (vertex locations, hatch edge ends and centres, section symbol points).
Regression test over real IPC; every existing lifecycle audit still passes.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Each is written against the prompt sequence observed in the exploration harness
and checked for exact geometry in the real-IPC audit. HATCH is left to
create_entity because its command needs an interactive boundary pick.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
HostApi::run_command drives the real OCS command through the same primitives
the automation channel uses, synchronously on the host thread: run a line,
start a command and answer point/text/token/entity/selection/enter prompts, or
cancel (closing anything the command opened). Refused while another command is
active, for a denylist (QUIT, NEW, OPEN, SAVE*, PY_* ...) and off the active tab.
Python: doc.command, doc.start_command and doc.modify (offset, trim, extend,
fillet, move, copy, rotate, scale, mirror, erase). Real-IPC audit checks exact
geometry for every wrapper plus guards and undo; IPC and Python unit tests; docs.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
TableOperation gains simple-linetype create/modify/rename/delete (standard names
taken, rename follows layers and entities, guarded delete) and paper-space
layout create/rename/delete/set-current/set-page (default page setup and sheet
viewport, the application's own layout switch, Model protected). CTAB now reports
the current layout. Real-IPC audit with DWG/DXF persistence and undo, IPC and
Python unit tests, docs.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
BlockEntityAdd adds a validated entity to a block definition (owner set by the
host, same validation as a model-space entity, self-nesting inserts refused),
reusing the scene's block-editor routing. Deleting a block member now also
drops its handle from the block record. Real-IPC audit with an attribute
definition inside a block, DWG/DXF persistence and undo; IPC and Python unit
tests; docs.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
TableOperation gains BlockCreate (copy entities into a definition shifted by the
base point, optionally erasing originals), BlockModify, BlockRename (inserts
follow) and BlockDelete (refused while referenced; removes members, markers and
orphaned draw-order tables). Real-IPC audit with DWG/DXF persistence and undo,
IPC and Python unit tests, docs; the DXF loss of the block description is pinned
by a canary.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Extends TableOperation with text style create/modify, dimension style
create/modify (DimStyle fields as a validated JSON object), rename, delete and
set-current. Reuses OCS's style in-use scan and reference-following rename
(four style_ops methods widened to pub(super)); a renamed text style also
updates dimension styles that name it. Real-IPC audit with DWG/DXF persistence
and undo, IPC and Python unit tests, docs. Four cadcodec findings are pinned by
canaries.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
printpdf 0.9's `PdfSaveOptions::optimize` is a no-op — its `doc.compress()`
call is commented out — so every page's content stream (megabytes of vector
operators for a CAD sheet) was written uncompressed: a 16-sheet drawing set
exported as 280 MB. Serialise through `printpdf::to_lopdf_doc` and run
lopdf's `compress()` before writing; the same export is now 49 MB and
readers see ordinary FlateDecode streams.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Two layout defects that together broke every numbered note in a Chinese
drawing:
- Words were split on whitespace only, so a Chinese sentence — which has no
spaces — was one unbreakable word. When it did not fit after "7.<tab>" the
whole sentence dropped to the next line and still ran past the column.
Ideographic text may break between any two characters except before a
closing mark or after an opening one (the basic kinsoku rule); the atom
splitter now emits those break opportunities.
- `\pxi…,l…,r…,t…;` paragraph indents and tab stops are multiples of the
text height, and the first-line indent is relative to the left indent
(a hanging list is `i-1.5,l1.5`). They were applied as raw drawing units,
i.e. a fraction of a unit, so wrapped lines snapped back to the margin
and the number/tab column collapsed. Scale them by the entity height and
resolve the first line from the left indent.
Adds a layout test: a 40-ideograph paragraph in a 30-unit column wraps into
several lines, none wider than the column.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Adds the additive v7 TableOperation request and HostApi::table_operation for
layer create/modify/rename/delete/set-current. Refusals are decided before the
undo step is recorded. Python wrapper, real-IPC audit with DWG/DXF persistence,
IPC round-trip test, Python unit test and docs.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Only the first line's ends are pinned while the second line is made
parallel, the way the Parallel constraint itself moves it; the distance
is then measured on the solved geometry.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Point & line and 2Lines take a text baseline or an ellipse axis as the
line; a zero value folds the points onto the first one; 2Lines turns the
second line about its start. The Properties Constraint section edits
Constraint Form (Dynamic on the *ADSK_CONSTRAINTS layer, Annotational on
the current layer), Reference (driven constraint in parentheses whose
parameter follows the measurement) and Description (stored with the
parameter and the native variable). Dynamic dimensions keep a 12 px text
and 8 px arrows at any zoom, drop trailing zeros and show formulas as
`fx: len=d1*2`; the hidden constraints layer stays out of the layer lists.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Glyph quads bake the atlas UV of their tile. `grow_height` rescales every
cached entry's V and bumps the atlas generation, and the tessellation memo
folds that generation in — but the caches that hold finished wires do not:
the resident wire set is keyed by geometry epoch, block definitions by
block epoch, and a full build can grow the atlas part-way through (a CJK
sheet set bakes thousands of tiles on first sight). Text tessellated before
the growth then samples the wrong tiles for as long as those caches live,
which on a big drawing is forever: whole tables of scrambled glyphs, and
the block contents stayed scrambled even after a REGEN.
- `resident_wires_for` rebuilds (dropping the block definition cache) until
the atlas generation is stable across the pass.
- `Scene::update` compares the live generation with the one the scene's
caches were built against and bumps geometry once when it moved, so a
later growth (opening a layout, editing text) re-lays-out everything.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
`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) <[email protected]>
DCFORM sets the annotational or dynamic form and continues into the
DIMCONSTRAINT option prompt, whose wording and last-used default match the
reference. -PARAMETERS manages parameters from the command line; a
double-clicked constraint dimension opens its value prompt. Dynamic
dimensions rescale on every camera change and their display overrides stay
out of the saved file. A driving dimension between an entity's own ends no
longer has its length retained, so a new value moves the second point along
the dimension axis; parameter edits hold the first constraint point.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
DCALIGNED opens with `[Object/Point & line/2Lines] <Object>`. Point & line
asks `Specify constraint point or [Line] <Line>:` then `Select line:` (or the
line first and `Select constraint point:`), measures perpendicular to the
line from the constraint point to the line end nearest to it, and stores a
directed distance with the line as direction reference. 2Lines asks
`Select first line:` and `Select second line to make parallel:`, adds a
Parallel constraint solved with the first line pinned, then measures on the
solved geometry between the nearest ends. Unsupported line picks report the
reference's message; the same measurement again reports `The constraint
already exists on the selected objects.` and ends the command; an invalid
expression reports both reference lines and asks again.
The offered default and seeded expression carry twelve significant digits;
a typed value moves the second point (or the other object) while the first
point and its measured line stay, negative values keep their sign in the
parameter and use their magnitude, zero folds the points together, and the
dynamic dimension's origins follow every solve. The lock mark sits after the
text, a selected dynamic dimension shows triangle grips at its points and the
text square, and its Properties show the Constraint section (Name and
Expression editable) plus Text rotation.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
A plain number shows as typed (d1=100), an expression under the fx: prefix
(fx: d2=d1*2); the text stays horizontal through DIMTIH/DIMTOH overrides
and the lock mark follows the text.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Every dynamic dimension gets a DIMSCALE override for 12 px text, refreshed
per zoom band, at creation and after load. A SCALE/ROTATE of an entity whose
own end points a driving dimension measures now pins its second constraint
point so the dimension moves the first end, as the reference does.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
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) <[email protected]>
`viewport_content_wires` projects model-space wires into paper coordinates
for plotting: the points and the linetype stations are multiplied by the
viewport scale, but `world_width` (the band width of a wide polyline) was
copied unchanged. The PDF / print exporter strokes that band at
`world_width` in paper millimetres, so a 15-unit-wide bus bar seen through
a 1:100 viewport came out as a ~21 mm black blob instead of 0.15 mm, and
every single-line diagram sheet plotted as solid black shapes.
Scale `world_width` by the viewport scale alongside the points, and keep
the per-point taper widths in step (or drop them when clipping changed the
vertex count, so they cannot misalign). Negative (fixed pixel) and zero
(lineweight) widths are left untouched.
Adds a regression test that plots a 15-unit-wide polyline through a 1:10
viewport and expects a 1.5-unit band in the projected plot wires.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The dynamic dimension of a dimensional constraint is now linked in the
drawing file (AcDbAssocDependency + AcDbAssocDimDependencyBody on the
constraint node), reread on load, hidden by DCHIDE/DYNCONSTRAINTDISPLAY
and never plotted; it carries a lock mark before its text. New
CONSTRAINTNAMEFORMAT and DYNCONSTRAINTDISPLAY variables and the
DIMCONSTRAINT option front end. A transformed entity whose own end points
a driving dimension measures keeps only the first constraint point, so a
scaled line returns to its dimension value.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
DCLINEAR, DCHORIZONTAL, DCVERTICAL and DCALIGNED follow the reference flow:
`Specify first constraint point or [Object] <Object>:`, `Specify second
constraint point:`, `Specify dimension line location:`, then `Dimension
text = …` and `Enter value or name and value <d1=100>:`. Linear reads
horizontal or vertical from the dimension line location the way a linear
dimension does; Object takes a line's, an arc's or a polyline segment's
ends. A miss reports `No valid constraint point found.` / `No object
found.` and asks again; another object type reports `Invalid selection for
Linear. Select a line, polyline segment or arc.`.
Each constraint creates a drawing parameter (`d1`, `d2`, …, or the typed
`name=expression`), drives the constraint through it, and draws a dynamic
dimension in constraint gray whose text follows CONSTRAINTNAMEFORMAT
(`d1=100.0000`); the dimension is associated with the constrained objects
and its text is refreshed after every solve. A typed value other than the
measured one moves the second point while the first stays, and so does a
later parameter edit. Deleting the constraint removes its dimension and,
when nothing else reads it, its parameter.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
A fresh reference session shows the "=" bar of every object made equal
through Multiple right away; the earlier hidden state was not the rule.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
A second object of the wrong kind (a circle after a line, a line after a
circle) or a pair that already carries Equal now reports and asks for the
second object again instead of ending the command. The mismatch message
names what the first object asks for — `Select a line segment, polyline
segment, text, MText, major or minor axis of ellipse or elliptical arc.`
after a length object, `Select a circle or arc.` after a radius object —
and the same-pick message carries the reference's double space.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
- An object that carries several Equal relations shows a single "="
badge, as the reference does, instead of one stacked badge per relation.
- A pick made in the Multiple flow leaves its new constraint bar unshown
until the object is selected or bars are shown, matching the reference;
the single flow still shows the bar at once.
- A typed coordinate at the Fix `Select object:` prompt and at the
Horizontal/Vertical `Select an object or [2Points]` prompt picks the
object under that point, like a click, instead of being read as a point.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Follow-up to the block-attribute grips picked from #1386.
- Movability no longer includes the invisible flag: an attribute ATTMODE 2
displays is grippable, so the grip the display-aware list shows can be
dragged. The document-less grips() still skips invisible attributes,
since it cannot consult ATTMODE.
- The grip sits on the displayed attribute, which an annotative insert
scales about its insertion point; Scene::apply_grip now unscales the
dragged target the same way before the attribute stores it, so the grip
no longer jumps to twice the drag at scale 2.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Selecting a block showed one grip, at its insertion point, so there was
no handle to pick an attribute up by: an attribute could only be moved
by moving the whole block.
Give the INSERT one grip per attribute, taken from the attribute's own
`Grippable` implementation, at ids `1 + index`. A drag on one moves that
attribute alone, through the same `apply_grip` the attribute already has
(which carries `alignment_point` along, so non-left alignments stay put
relative to the text). Grip 0 keeps translating the block and every
attribute with it (#255).
A constant attribute belongs to the block definition and an invisible one
is not drawn, so neither offers a grip; a locked-position attribute
declines on its own.
FixesHakanSeven12/OpenCADStudio#1259
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TQzYZsXBwPZ5HwBaGitEUV
(cherry picked from commit 7eb966f6a2)
An OTRACK crossing is the meeting of two tracking vectors, but
`OtrackHit` carried only the one a typed distance is measured along, so
the overlay had a single guide to draw. Snapping to the crossing of two
vectors worked and looked like an unexplained point on one line.
Carry the crossed ray out of `otrack_snap` as `OtrackHit::cross`, keep it
on the app beside `otrack_active`, and let the overlay take a list of
guides rather than one. `otrack_active` still drives typed-distance
entry on its own, so distance entry is unchanged.
FixesHakanSeven12/OpenCADStudio#1313
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TQzYZsXBwPZ5HwBaGitEUV
(cherry picked from commit 2f6caacece)
`sample_entity_xy` walks a polyline's exploded segments and drops the
vertex each segment shares with the previous one. A segment whose curve
plane is upright — any non-horizontal run of a 3D polyline — has no XY
curve, so `entity_curve_xy` declines it and the segment samples to
nothing. With nothing accumulated yet, the seam test compared the two
empty ends as equal and then sliced the empty sample from index 1:
panicked at src/modules/draw/modify/trim.rs:3982:42:
range start index 1 out of range for slice of length 0
On the web build that panic unwinds through winit's event-loop runner
and leaves its state borrowed, which is the "RefCell already borrowed"
follow-on in the same report.
Skip an empty sample instead, which leaves the seam handling to the
segments that do have a plan-view shape.
FixesHakanSeven12/OpenCADStudio#1318
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TQzYZsXBwPZ5HwBaGitEUV
(cherry picked from commit ac9a6ce91a)
- resolve_point, set_polyline_vertex and the solver's point_for_marker
treat the marker one past the last vertex of a closed polyline as
vertex 0, the way line_segment already did. Equal (#1366), the axis
pre-alignment (#1365) and the transform pin (#1361) address a picked
segment's end that way, so on the closing segment they either refused
the pick or pinned nothing.
- Page-setup dictionary keys compare case-insensitively: page_setup_save
replaces "site" when "Site" exists instead of adding a second entry
(PSETUPIN counted it as replaced, #1344), delete and rename match the
same way, and a rename that only changes case is allowed.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
- The acadrust DXF reader has converted ATTRIB/ATTDEF rotation to radians
since f226110 (in every codec rev OCS pinned from 2026-08-08 on), while
fix_dxf_dimension_rotations still applied to_radians a second time on
load. Only Shape needs the conversion here. Surfaced by #1391's hunk.
- Shape-file text styles are found by their is_shape_file flag instead of
an empty name (also from #1391).
- #1393's add_layer/modify_layer wrote an unresolved linetype name into
the LAYER record; a name the drawing (plus the standard set) cannot
supply is now refused, through one resolve_linetype helper.
Co-Authored-By: Claude Fable 5.1 <[email protected]>