Layers and objects get a PlotStyle property (ByLayer/ByBlock/name) applied
through named tables (built-in default.stb and monochrome.stb, our own
definitions). DXF keeps the names in the ACAD_PLOTSTYLENAME dictionary of
ACDBPLACEHOLDER objects with group 390 on layers and objects; tables kept in
the drawing travel in a CADCRAFT_PLOTSTYLES XRECORD. PLOTSTYLE sets an object's
or the current (CPLOTSTYLE) style; STYLESMANAGER lists, creates, edits, deletes,
loads and saves tables in our own JSON format (docs/plot-styles.md). A Plot
Style Manager dialog edits a table's styles and the layout's table; the
Properties palette shows the current plot style and table.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
GROUP makes named or unnamed (*A1, *A2…) groups from the selection, typed
("Select objects or [Name/Description]") or as JSON. UNGROUP removes a group
picked by one of its objects or by name. GROUPEDIT adds and removes objects and
renames a group; removing the last object deletes it. groups.list reports them.
After every command, members that no longer exist leave their groups and empty
groups are deleted (undo restores both). The drawing model and the ACAD_GROUP
DXF round trip already existed.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
MLEADER's Options answered "not available yet" for Leader type sPline and
None and for Content type Block. Spline leaders now draw as smooth curves
through their points (DXF LEADER path type 72 = 1); None places only the
content at one picked point; Block asks for a block name and how it
attaches (Center extents or Insertion point) and inserts it at the end of
the landing. MLeader gains `spline` and `block` (serde defaults); DXF stays
LEADER + MTEXT/INSERT. PURGE, RENAME, copy/paste, FLATTEN, grips and
extents follow the block content. The choices are remembered like the
other MLEADER options. docs/gaps.md: top-10 row 5, F1 and F10 narrowed.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
SPLINE's Knots, Object, start/end Tangency and toLerance options printed
"not available yet". Fit-point splines now take a knot parametrisation
(chord, square root, uniform), end tangents (clamped cubic with derivative
constraints) and a fit tolerance (least-squares approximation with the
fewest control points that stay within it). The spline keeps these options
so grip and SPLINEDIT edits refit the same way; DXF writes the tolerance
(44) and tangents (12/13) and recovers the parametrisation from the knots.
Object turns 2D/3D polylines with straight segments (PEDIT Spline output)
into splines through their vertices.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The options at MTEXT's "Specify opposite corner" prompt printed "not
available yet". Each now asks its question and returns to the corner
prompt: Height (also sets TEXTSIZE), Justify (TL..BR, the insertion point
is the box's attachment point), Line spacing (At least/Exactly, then a
factor such as 1.5x or a distance), Rotation (picked, or typed through the
drawing's angle settings; the box turns with it), Style (? lists them),
Width (replaces the opposite corner) and Columns (No; Dynamic and Static
say columns are not available yet). MText stores the line spacing style
(DXF group 73) so Exactly round-trips.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A PlotStyleTable model in doc with our own built-in tables (default.ctb keeps
object properties, monochrome.ctb prints black, grayscale.ctb prints the grey of
each colour's luminance). The render builder resolves each object's style by its
index colour (true colours by the nearest index) and applies colour, screening,
linetype and lineweight. PLOT/EXPORTPDF use the page setup's table (or the
plotStyleTable option); layouts with Display plot styles (PAGESETUP
displayPlotStyles, DXF plot flag 2) show it on screen and in PNG/SVG exports.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
AUDIT checks for missing layers, linetypes, styles and block definitions,
current-style variables naming missing entries, invalid geometry (NaN or
infinite numbers, zero-length lines, zero-radius circles and arcs),
block definitions that contain themselves, duplicate handles, and
references to missing objects (dimension associations, groups,
constraints, viewport layer lists). It asks "Fix any errors detected?
[Yes/No] <N>:"; the JSON form `audit {fix?}` returns a structured
report. RECOVER opens a file through the read hook and applies AUDIT's
fixes. A question prompt now shows its answers without "or".
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Typed END, MID, CEN, GCEN, NOD, QUA, INT, APP, EXT, INS, PER, TAN, NEA,
PAR (any start of the word from three letters, with `_`/`'` prefixes)
and NON push a one-pick snap frame on the point-modifier layer: the
prompt reads "Endpoint of:", a typed point is snapped with that mode
alone within the aperture (or rejected with "No Endpoint found for
specified point."), and the canvas snaps the cursor with it
(Session::snap_override). A command's own keyword still wins (ARC/ZOOM
`cen`); `_cen` always snaps. Overrides nest with FROM and the filters.
Shift+right-click or Ctrl+right-click on the canvas opens the object
snap menu (From, Mid Between 2 Points, Point Filters, the snap modes,
None, Osnap Settings...), whose items type the same overrides. Tab over
a snap marker with nothing typed cycles the candidate snaps
(pointmod::snap_candidates: the running snap first, then the closest of
each mode).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
PLINE arc mode answered "not available yet" for Angle, CEnter, Direction,
Radius and Second pt, and line mode for Length. Each now asks its questions
(included angles and directions read the drawing's angle settings) and adds
the segment. XLINE's options were already in draw2's XlineM2; the dead
keyword stub in draw.rs's RAY machine goes, and Offset/Reference use the
polyline segment picked rather than the first one.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Resting the cursor on a snap point acquires it for tracking (seven at
most; resting again releases it). Acquired points send horizontal and
vertical alignment paths, or every polar angle with POLARMODE 2; the
cursor snaps where two paths cross, where a path crosses an object, or
onto the nearest path. EXT runs a line or arc on from an acquired end,
PAR gives a path parallel to a rested-on line through the base point,
APP snaps where objects would meet if extended. Direct distance entry
follows the path the cursor is on. POLARMODE is now AutoCAD's bitcode;
POLARADDANG and AUTOSNAP are new.
The engine owns all of it (Session::snap_cursor, snap/tracking.rs); the
canvas draws the + markers, dotted paths and tooltip.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The options TRIM and EXTEND list at their prompt all printed "not available
yet". Quick and Standard modes follow TRIMEXTENDMODE, Edge extends lines and
arcs virtually (EDGEMODE), Project is kept in PROJMODE. The JSON forms take
fence, crossing, edgeMode and (TRIM) mode.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A point modifier typed at any point prompt pushes a frame on the session
(Session::point_mods) that asks for what it needs ("Base point:",
"<Offset>:", "First point of mid:", ".X of", "(need YZ):") and hands one
point to the command, so every command gets them from the command line
and in scripts, and they nest (FROM then M2P). `*x,y`, `*@` and `@*`
parse as world coordinates (the same as the plain forms: no UCS yet).
`<a` locks the next point to that angle from the base point: a typed
distance follows it, a pick is projected onto it, typed coordinates win.
TK is left to object snap tracking (#379).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
With DIMTOL on, the DIMPOST suffix was added to the measurement only.
As in AutoCAD, the suffix (what follows `<>`, or the whole value when
there is no `<>`) now also follows each tolerance value: symmetric
(10.0mm±0.1mm) and stacked deviations alike. The prefix stays on the
measurement; limits are unchanged.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Freezing a viewport's own layer hid only its border; the model space
seen through it still showed on screen and in plots. As in AutoCAD, a
frozen viewport layer now hides the contents too, while turning the
layer off still hides only the border. The check is in the shared
layout path, so display, PDF/SVG/PNG export and plotting agree.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Center, Scale and Dynamic all zoomed to the extents, so the point or
factor typed after them ran as a command. Center now asks for the centre
point and a magnification or height (a height, nX or nXP), Scale for a
factor, and Dynamic, which needs a drag, asks for a window. A plain n
(at the first prompt and at Scale) is relative to the limits, as in
AutoCAD: 1 fits them. ZOOM All and Extents with nothing drawn used the
model limits in paper space too; Session::zoom_limits gives the sheet
there.
Builds on #372 (ZOOM inside a viewport).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
`Attrib` gains optional per-attribute properties (`props`), each overriding
the attribute definition's. DXF reading takes them from the ATTRIB's groups
8/6/62/420/370; writing gives each ATTRIB the properties it is drawn with.
The renderer prefers them over the definition's. -ATTEDIT's Layer and Color
options set them; ATTSYNC resets them to the definitions, as in AutoCAD.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Typed VPLAYER ran its JSON form with no parameters and ended, so its
options ran as commands (F started FILLET). It now loops on
?/Color/Freeze/Thaw/Reset/Newfrz/Vpvisdflt until Enter, asks for layer
names (wildcards, comma lists) and then which viewports to change:
All/Select/Current/Except current (Current by default inside a viewport,
Select by point on the sheet in paper space). Changes go through
viewport.set, layer.new and layer.set; the session is one undo step.
The Model tab refuses it.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Typed LAYOUT ran its JSON default and ended, so the option and the
layout names typed after it ran as commands. It now asks for an option
(Copy/Delete/New/Template/Rename/SAveas/Set/?, Enter = Set) and the
layout name(s) the option needs, with the current layout as the default.
The options run the layout.* command bodies, so a refused name re-prompts
and the command is one undo step. Template and SAveas report that they
are not available yet and end.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
In MSPACE, Extents/All/Window/Object fitted the drawing window's aspect and
paper-view height, In/Out/nX zoomed about the screen centre, and nXP was read
as nX. Now ZOOM works on the active (unlocked) viewport's model view: fits use
the viewport's aspect, zooms keep its centre, and nXP sets the viewport scale
to n paper units per model unit. Locked viewports and the Model tab are
unchanged.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
When Enter was refused (Enter repeats a command that isn't available),
headless failed the cmdline.key call while the app dropped the error,
and pressing Enter in the app's command line echoes it. Both now echo
the error to the history and reply with the command-line state, the
`output` lines the key added and `error` only when Enter was refused,
the shape #351 gives cmdline.input.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The next MLEADER started again with "arrowhead first", Maxpoints 2 and
a landing line. As in AutoCAD, the order (arrowhead, landing or content
first) and the Options values (Maxpoints, leader landing, content type,
first and second angle) are now kept in Session::last_used for the
rest of the session and are where the next MLEADER starts. JSON calls
keep their documented defaults.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
EXPLODE and the DXF *D block took the text height from the bare style's
DIMTXT × DIMSCALE, ignoring the dimension's overrides (SETVAR DIM*, DIMOVERRIDE),
a fixed-height text style and the DIMSCALE 0 fallback (text ~1e-10 high), and
EXPLODE always wrote rotation 0, so aligned text turned horizontal. Both now
use the geometry the renderer draws (dimension_in): its text height, angle and
the overridden text style; EXPLODE adds no MTEXT for suppressed text.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Typed at the command line or run from a script, DIMTEDIT ran the JSON
form at once and failed with "select dimensions". It now asks "Select
dimension", then "Specify new location for dimension text or
[Left/Right/Center/Home/Angle]": a point moves the text there (the text
follows the cursor in the preview), Left/Right/Center justify it,
Home restores the default position and Angle asks for the text angle.
A single preselected dimension is taken at once. Each answer runs the
existing JSON form on the picked dimension. DIMTED is its alias.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Only the direction of the picked dimension line location was used, so
the text always went outside beyond the arrowhead. A location inside
the circle (typed, picked or JSON `at`) now places the text there: the
dimension stores it as its text position, and the dimension line runs
from the text out to the arrowhead; a diameter's spans the circle,
broken around the text. DIMTIH (not DIMTOH) decides whether inside
text is horizontal. Outside locations keep the default placement.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Tolerances and alternate units copied the primary number format, so they were
rounded by DIMRND and took DIMZIN's zero suppression: DIMRND 0.5 turned a
±0.1 tolerance into ±0.0000, and 10.3 in showed as [261.50] mm. Tolerances are
no longer rounded and use DIMTZIN. Alternate units use the new DIMALTRND
(rounding), DIMALTU (unit format) and DIMALTZ (zero suppression). The new
variables can be set with SETVAR, DIMSTYLE and overrides. A DXF keeps them as
DIMSTYLE group codes 148, 273, 284 and 285.
DIMPOST is a prefix/suffix for linear dimensions, so angular dimensions no
longer get it ("90°mm"). A text override still wraps the angle.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
When the arc location lies outside the sweep from the first leg to the second,
the arc runs from leg 2 to leg 1. The extension lines, DIMSE1/DIMSE2 and
DIMBLK1/DIMBLK2 still took the arc start as leg 1, so each extension line was
sized from the other leg. A short leg got no extension line, a leg already
crossing the arc got one drawn along it, and DIMSE1/DIMSE2 suppressed the
wrong line. The arc's ends are now paired with their legs.
Radius and diameter dimensions drew the DIMCEN mark even when the dimension
line is inside the circle. As in AutoCAD, the mark is now drawn only when the
line is outside: a radius led out to its text. A diameter line runs through
the centre, so it gets no mark.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Baseline dimensions were spaced by the bare style's DIMDLI × DIMSCALE, so
SETVAR DIMSCALE/DIMDLI (stored as overrides since #129) had no effect and a
style DIMSCALE of 0 collapsed the spacing to ~0. Use the continued
dimension's style with its overrides and the effective scale (the drawing's
DIMSCALE when the style's is 0), as drawing does; DIMSPACE's default spacing
uses the same scale.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Undo and redo swapped the drawing but left the session on the current
layout's name. After undoing a rename or the creation of the current layout,
that layout no longer existed: nothing was drawn and commands failed with
"no such space". Now the session follows a renamed layout to its other name
(same tab order), keeping its paper view, and returns to the Model tab when
the layout is gone.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#344 made JSON LINE refuse coincident points; files can still hold them, so
the test that checks PURGE Zero-length adds one to the drawing directly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
STRETCH gets its own prompt machine instead of a special case inside the
shared MOVE/ROTATE/SCALE one. "Select objects" now repeats until Enter:
implied windows (crossing right to left), `C` and `W` windows, single
picks and All/Last/Previous (moved whole), Remove/Add. Vertices inside any
of the windows move. Enter or `Displacement` at the base point asks for a
displacement, and Enter at the second point uses the base point as the
displacement, as in AutoCAD. Polygon and fence selection say they are not
available yet instead of being misread.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Copying in one drawing and pasting in another lost what the objects
refer to: a line on a layer the target lacked landed on a new default
layer (colour and linetype gone), and a block reference pointed at a
block the target didn't have. Cmd/Ctrl+C, X and V did nothing on the
desktop, because egui delivers them as clipboard events, not key
presses. PASTEORIG had no checks, and the canvas had no right-click
menu.
- Engine (cmd/clipboard.rs): the session clipboard keeps, with the
copied objects, the layers, linetypes, text/dimension/multileader/
table styles and blocks (nested too) they use. Pasting adds the ones
the target lacks; names it has keep the target's definitions, as in
AutoCAD; generated (*U, *D) blocks whose name is taken get a fresh
one; objects get new handles.
- PASTEORIG pastes at the original coordinates, only into a drawing
other than the one copied from (greyed and refused there, as in
AutoCAD). PASTECLIP and PASTEBLOCK are greyed with an empty clipboard.
- PASTEBLOCK (Edit > Paste as Block, Cmd+Shift+V): the clipboard
becomes an A$C… block inserted at a point. AI_DESELECT.
- UI: Cut/Copy/Paste events run the clipboard commands (Shift: Copy
with Base Point / Paste as Block). Each copy is also put on the system
clipboard as DXF text, and pasted DXF text that isn't our own last
copy becomes the clipboard, so another CADCraft window can paste it.
- Right-click on the canvas with no command running opens a shortcut
menu: Repeat <last>, Clipboard (Cut, Copy, Copy with Base Point,
Paste, Paste as Block, Paste to Original Coordinates), the
selection's editing commands and Deselect All, Undo/Redo, Pan/Zoom,
Quick Select, Find. During a command it is still Enter.
- Ukrainian labels for the new items.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
At the block name prompt, *NAME inserts the block's objects: after the
insertion point INSERT asks one scale factor and the rotation, and no
attribute values. A path (C:\parts\bolt.dxf), NAME=path or NAME= reads
the drawing through the io hooks and defines a block named after the
file (or NAME) from its model space, with the file's $INSBASE as the
base point, adding the layers, linetypes, styles and blocks it needs and
keeping this drawing's definitions. A plain path whose block already
exists asks whether to redefine it; NAME=path redefines without asking.
A file that can't be read is reported at the name prompt. The JSON form
takes `file` (optional `name`) and "*NAME".
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Both backends now return the same command-line state (prompt, running,
keywords, accept, buffer, history, historyExpanded). cmdline.input echoes
a refused line to the history and `output` and also reports it in `error`
on both sides. Headless app.open without a path answers "missing path"
like the app instead of reaching OPEN. Documented in control-protocol.md.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The control server gave up after 60 s with "timeout" but left the request
queued, so the app ran it later and a client retry applied it twice.
Requests now carry a shared state: on timeout the server cancels a request
the app has not started (reply state "not-run", safe to retry) and the app
skips it; one already started replies state "may-have-run". The MCP bridge
passes that on in its error.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
ROTATE's angle and the hatch pattern angle are rotations (AUNITS, clockwise
when ANGDIR is 1). The dimension line (Rotated) and dimension text angles are
directions from ANGBASE. CHAMFER's angle and the MLEADER angle constraints are
amounts in AUNITS. The ROTATE and HATCH defaults are offered as they would be
typed.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
acadrust's DXF writer leaves ANGBASE, ANGDIR, FILLETRAD, CHAMFERA/B,
ELEVATION and THICKNESS out of the HEADER although its DWG reader fills
them in; the DWG bridge now adds them to the DXF it hands our reader.
SNAPBASE/SNAPANG are written to and read from the *ACTIVE VPORT record
(groups 13/23 and 50), where R13+ files keep them.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
CIRCLE/ARC no longer make a negative radius positive; POLYGON refuses a
non-integer sides count and a zero/negative radius or zero-length edge;
DONUT, RECTANG, PLINE, HATCH and TABLE refuse negative or zero sizes
instead of clamping them; LINE drops repeated points like the interactive
command; SPLINE needs two different fit points; DIMRADIUS/DIMDIAMETER/
DIMANGULAR refuse coincident points; DDPTYPE refuses an invalid PDMODE;
DIMSTYLE/DIMOVERRIDE list a non-positive DIMTXT/DIMTFAC or a negative
DIMASZ/DIMSCALE/DIMTSZ as ignored.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
For every justification but baseline-left (and aligned/fit) the DXF
Reference makes group 11 the point the text is placed by; group 10 is a
computed point on the baseline. Left-justified text with a vertical
alignment was drawn, measured and picked from group 10 and then shifted
by the alignment again: TL and ML too low, BL too high. The renderer
(fonts::place) and text extents (doc text_box, used for picking) now
place it from the alignment point; a mirrored (extrusion 0,0,-1) TL/ML/BL
text keeps its alignment point when it turns right-justified; and the
Properties position moves justified text by its alignment point.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
With DIMASSOC on, every dimension QDIM added looked up the objects under
its definition points by scanning the whole space, and each dimension
adds an entity, so QDIM over many objects took quadratic time (20,000
lines: 28 s). QDIM now builds the SnapIndex DIMCONTINUE/DIMBASELINE use
(#281) once per call; dimensions are no snap targets, so it stays valid
while QDIM adds them. A call naming more than 100,000 objects, or whose
objects give more than 100,000 distinct points, is refused before
anything is created.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Pick box at object-selection prompts; prompts remember the last option
Part of #34.
The crosshair showed the pick box only at "Select objects" prompts. The
commands that pick one object themselves (FILLET, CHAMFER, OFFSET, TRIM,
EXTEND, BREAK, the dimension and constraint picks...) ask with a plain
point prompt, so the cursor stayed a bare crosshair there and hitting an
object was guesswork. The idle crosshair also showed the box with
PICKFIRST off.
- Engine: Prompt::picks_objects() (a selection prompt, or a point prompt
that starts with "Select") and Session::picking_objects() (that, or no
command with PICKFIRST on). The canvas uses it for the pick box and the
rollover highlight.
- The box is sized from PICKBOX as the pick aperture the click uses
(PICKBOX x 1.5 px half side), hidden at PICKBOX 0, and drawn with a new
`crosshair` theme token.
- HATCH/GRADIENT: "Select objects" mode is a real mode now ("Select
objects or [picK internal point/...]", Enter creates the hatch) and the
next HATCH starts in the mode used last. Pattern, scale and angle
sub-prompts show the current value as <default>. At select prompts P, L
and ALL stay selection modes even when a keyword shares the letter.
- POLYGON offers the last Inscribed/Circumscribed choice, ROTATE the last
angle, SCALE the last factor (Session::last_used, session only).
- FILLET shows the radius as <default>; CHAMFER asks for the second
distance too, defaulting to the first (FILLETRAD, CHAMFERA/B).
JSON calls are unchanged: explicit params and documented defaults win.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* FILLET/CHAMFER Trim, Multiple, Undo; CHAMFER Angle, mEthod, Polyline (fixes#248)
Trim asks for the trim mode (TRIMMODE); with No trim the fillet arc or
chamfer line is added and both objects are kept. Multiple repeats until
Enter and Undo takes back the last fillet/chamfer of the command. CHAMFER
Angle asks for a length on the first line and an angle from it (CHAMFERC,
CHAMFERD, CHAMMODE = 1), mEthod picks Distance or Angle, and Polyline
chamfers every straight corner of a 2D polyline. The JSON forms take
`trim`, and CHAMFER `length`/`angle` and `polyline`.
Keyword matching now prefers a whole keyword or its capital-letter
shortcut over a prefix of another keyword, so `M` is Multiple rather than
the start of mEthod.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
DIMORDINATE Xdatum/Ydatum/Mtext/Text/Angle, DIMRADIUS/DIMDIAMETER
Mtext/Text/Angle, DIMANGULAR Mtext/Text/Angle/Quadrant and DIMARC
Mtext/Text/Angle/Partial were listed but ignored (any DIMORDINATE
keyword other than Xdatum forced a Y datum). DIMLINEAR/DIMALIGNED
Angle rotated the dimension line instead of the text.
A shared DimText handles Mtext/Text (override text, <> = measurement)
and Angle (text rotation) for every placement prompt. Quadrant locks
the measured angle to a picked point's side; Partial dimensions the
part of the arc between two picked points. After Horizontal/Vertical/
Rotated only the text options remain listed.
MLEADER: leader Landing first / Content first switch the placement
order, Enter or Options opens the options loop (Leader type, leader
lAnding, Content type, Maxpoints, First/Second angle). Spline leaders,
leaderless and block content say "not available yet". Angle prompts
take numbers so scripts keep splitting the following tokens.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- Island detection style (group 75) picks the loops that take part: outer keeps the
outermost loops and their first islands, ignore only the outermost loops.
- A pattern hatch's background colour is filled under its pattern lines.
- Gradients blend colour 1 to colour 2 (LINEAR, CYLINDER, CURVED, SPHERICAL,
HEMISPHERICAL and the INV forms) along their angle, shifted when not centred, drawn as
bounded colour steps; ByLayer / ByBlock gradient colours resolve to the hatch's layer /
block colour instead of colour 7.
- DXF HATCH pattern definition lines (78, 53, 43-46, 79, 49) of a pattern the library
doesn't define are kept on the hatch (new `patternLines` field), drawn and written back;
hostile counts are capped.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Typed -LAYER (and LAYER in scripts and headless sessions) now starts the
option loop instead of listing the layers and ending: ?, Make, Set, New,
Rename, ON, OFF, Color (with Truecolor), Ltype, LWeight, TRansparency,
Plot, Freeze, Thaw, LOck, Unlock and Description, until Enter. MATerial,
stAte and rEconcile report "not available yet" without taking the next
token. Every option runs the layer.* command bodies, so the session is
one undo step and refusals match the JSON form. Turning the current
layer off asks first; freezing it is refused.
layer.new and layer.set now reject a linetype that is not loaded (or
ByLayer/ByBlock), and layer.set refuses to rename a layer onto the name
of another existing layer.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Typed -PURGE (PURGE has no dialog yet) now asks for the type of unused
objects, the names to purge <*> and whether to verify each name, instead
of purging everything at once and leaving the rest of the line to run as
commands. Verify Yes confirms each name; No purges every match. Types
CADCraft does not have read their answers and report nothing to purge;
Zero-length geometry and Empty text objects purge at once.
The JSON form takes {type, names}: LA purges only layers. All covers
every named type, so unused text, dimension, multileader and table
styles and empty groups are purged as the params doc promised. Layers and
text styles go through layer.delete and style.delete. Linetypes used
only inside block definitions, or set as CELTYPE, are kept.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
ATTEDIT / EATTEDIT typed at the command line ran the JSON form and failed
with "`handle` is required". It now asks "Select block reference:" and then
"Enter value for <TAG> <current>:" for each attribute (Enter keeps the
value); the answers are applied together as one undo step.
-ATTEDIT is a command of its own (Modify > Object > Attribute > Global):
one at a time (Value: Change/Replace, Next) or global string replacement,
over the attributes chosen by block name, tag and value specifications.
Session::start now prefers an id that keeps its leading dash, so -ATTEDIT
is not taken for ATTEDIT; dashed aliases (-INSERT...) resolve as before.
INSERT `attribs` and ATTEDIT `values` match attribute tags in any case and
report the keys no attribute has in `unknownTags`.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
ATTDISP asks "Enter attribute visibility setting [Normal/ON/OFF]" (default:
the current setting) and sets ATTMODE to 1 / 2 / 0. The renderer now reads
ATTMODE: OFF hides every attribute, ON also draws invisible ones, Normal
draws the visible ones as before.
ATTSYNC ("Enter an option [?/Name/Select] <Select>") brings every reference
of a block, in model space, layouts and other block definitions, in line
with the block's attribute definitions: missing attributes are added with
their defaults, ones whose definition is gone are removed, values are kept
for tags still defined (any case), and position, height, style and
visibility come from the definition.
Both live in the new cmd/blocks/attribs.rs.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
LAYMRG moves every object on the source layers (model space, layouts
and block definitions) to the target layer, drops the sources from
viewport freeze lists and colour overrides, and deletes the source
layers. LAYDEL deletes the layers and every object on them the same
way. Typed, both ask "Select object on layer to merge/delete or
[Name]:" until Enter (Undo drops the last one), LAYMRG then asks for
the target layer, and both warn and ask [Yes/No] <No> before changing
anything. Scripts answer Y. The JSON forms laymrg {from, to} and
laydel {names} never prompt.
Layer 0, Defpoints and xref-dependent layers are refused, and so is
merging a layer into itself. LAYDEL refuses the current layer; LAYMRG
merges it and makes the target current. Each run is one undo step.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Midpoint snaps halfway along the length of an elliptical arc or open spline.
Perpendicular and Tangent from a base point find every foot point and
tangent point on ellipses, elliptical arcs and splines (the one nearest the
cursor wins, as for circles): tangents to an ellipse exactly through the unit
circle, the rest by sampling the parameter range and bisecting each sign
change, with fixed iteration bounds. Quadrant on an elliptical arc offers only
the quadrant points the arc passes through.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- A dimension snaps to what it draws: the objects of its block (read from a
file), expanded like block references, or else the lines generated from it
(Endpoint on their ends, Midpoint, Intersection, Nearest); Node on its
definition points.
- TEXT justified other than left-baseline (as read from DXF, group 11)
snaps (Insertion) and grips at its alignment point; attributes too.
- The snap grid starts at SNAPBASE and turns by SNAPANG (drawing variables,
defaults 0,0 and 0 degrees). SETVAR takes `x,y` for point variables.
- Hatches are left out of object snaps unless OSNAPHATCH is 1 (new session
variable, default 0 as in AutoCAD).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Polar points and angle prompts read bare numbers in AUNITS (explicit
d/r/g, deg-min-sec and bearings still work) and measure directions from
ANGBASE in the ANGDIR sense; `d<<a` (degrees) and `d<<<a` (AUNITS)
measure from +X counterclockwise. XLINE Reference turns per ANGDIR;
included angles and sweeps take the units only. ANGBASE is held in
radians like $ANGBASE in DXF; SETVAR reads and reports it in degrees.
JSON params keep their meaning.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- A dimension on a layer with a viewport colour override now takes the
override inside that viewport (dimension and extension lines, arrows,
text), like every other object.
- A LEADER's arrowhead is DIMASZ x DIMSCALE of the leader's dimension
style; the drawing's DIMSCALE only applies when the style's is 0 (as
for dimensions), and both 0 no longer gives a zero-size arrow.
- Attributes are drawn with the properties of their attribute
definition, which AutoCAD copies to the attribute on insertion:
ByLayer = the definition's layer (layer 0 = the block reference's
layer), ByBlock = the block reference's colour and lineweight,
explicit colours and lineweights as set. An attribute on an off or
frozen layer is hidden. Attributes without a definition keep the block
reference's colour and now its lineweight too, instead of 0.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
tools/call checks the arguments against the tool's input schema: an
unknown tool, non-object arguments, a missing required argument or one
of the wrong type is a JSON-RPC -32602 error, as the MCP spec says, so
`command_line {}` no longer presses Enter and `script {}` no longer
succeeds. An empty batch `[]` gets one Invalid Request (-32600).
The `entities` command behind query_entities rejects a malformed window,
non-string type/layer and bad limit/offset instead of ignoring them.
`count` is every match, `returned` the page (`offset` is now in the tool
schema), and a window matches objects whose geometry is inside or
crosses it (the window/crossing selection test), with `crossing: false`
for entirely inside.
list_commands' filter matches command ids, labels and aliases, not any
text in the command's JSON.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Inputs to MOVE, SCALE, ROTATE, ARRAY, OFFSET and the rest are checked for
being finite, but finite inputs can still overflow: moving by 1e308 twice
put a circle at infinity, after which inspect showed null coordinates,
the extents were null and a DXF save wrote it at 0,0.
After a command changes the drawing (JSON `execute`, grip edits, and an
interactive command when it ends), the objects it changed are checked:
every number in their fields and their extents must be finite. If not,
the drawing and selection are put back, no undo step is recorded and the
command fails with "the result is too large: object … would get infinite
or undefined coordinates. Nothing was changed".
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Stretching a grip now drags the grips of the other selected objects that sit
on the same point, so connected lines stay connected, and the Move, Rotate,
Scale and Mirror grip modes transform every selected object about the hot
grip, as in AutoCAD. grip.move/grip.stretch take the other objects in
`handles`; with only `handle` they edit that object alone, as before.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A circle in the aperture replaced the best Nearest candidate without
comparing distances, so a closer line drawn earlier lost to it.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
SAVEAS / QSAVE with a .svg, .png or .pdf path wrote the export and then
marked the drawing saved under that file: the title became the image name,
the unsaved changes were cleared (no "Save changes?" on close) and a later
Save wrote the image again. Exports now only store the file; DXF/DWG saves
are unchanged.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
targets() fell back to the current selection when `handle` wasn't a hex
handle or `handles` wasn't an array, and dropped unreadable entries from
`handles`. A typo such as {"handle": "#101"} made ERASE, MOVE and the other
~70 commands that take objects act on whatever was selected (in a bridged
MCP session: the user's selection in the window). Such values are now an
error naming the bad value; the selection is used only when neither key is
given (or it is null).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Follow-up to #318: a huge array of an empty or tiny block could spin up to
1e8 cells in the snap path without touching the entity budget.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OFFSET's Erase and Layer keywords open their Yes/No and Current/Source
prompts (remembered in OFFSETERASE/OFFSETLAYER and shown in the current
settings line); Multiple keeps offsetting the newest copy until Enter and
Undo takes back the last offset. The JSON form takes `erase` and `layer`.
MOVE and COPY ask for a displacement after the Displacement keyword or
Enter at the base point prompt.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
After the insertion point, the command-line INSERT now asks "Enter X
scale factor, specify opposite corner, or [Corner] <1>", then "Enter Y
scale factor <use X scale factor>", then the rotation, so scripts like
`-INSERT B1 20,20 1 1 0` place the block as written instead of rotating
it by the Y scale and running the rotation as a command. A point (or
Corner) at the X scale prompt gives both factors from the opposite
corner.
The Basepoint, Scale and Rotate options at the insertion point prompt
now work: Scale and Rotate preset the value and skip that prompt later,
Basepoint picks a new base point on the block at its definition
position.
The JSON form takes `scale: [x, y]` and `basePoint` too.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Typed RENAME / -RENAME (and REN) asks for the object type
[Block/Dimstyle/LAyer/LType/Material/multileadeRstyle/Plotstyle/
textStyle/Tablestyle/Ucs/VIew/VPort], the old name and the new name,
then runs the rename command body, so the checks and the single undo
step match the JSON form. An unknown old name or a refused new name
re-prompts. Material, Plotstyle and VPort read both names and report
"not available yet".
The JSON form now renames blocks, as its params doc promised. Every
INSERT follows the new name, in model space, layouts and nested block
definitions, including inserts that carry attributes. INSNAME and
dimension-style arrowheads follow too. Duplicate names (any case),
invalid names and anonymous blocks are refused. Multileader styles,
table styles, named views and UCSs can be renamed as well. A layer
rename no longer records two undo steps.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Running snaps only saw a block reference's insertion point. Now the objects
inside it snap where they are drawn: nested references and MINSERT arrays are
expanded, placed exactly by the reference's transform (non-uniform scale
turns circles and arcs into ellipses), and hidden objects or objects on layers
that are off are left out. Insertion also snaps to visible attributes.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Linetype text elements (FENCELINE1, GAS-style linetypes from DXF files)
were drawn as part of the gap. Each text is now placed where the element
that carries it ends, at every repeat of the pattern: in its text style's
font, at the element's scale times the style's fixed height and the
linetype scale, shifted by its X/Y offsets along and across the line, and
rotated relative to the line direction or absolutely. Runs drawn
continuous (pattern smaller than a pixel) draw no text, and coarse
previews without text skip it. Shape elements need SHX shape files,
which CADCraft doesn't read, so they still draw nothing.
FENCELINE1 carries its circle on the first gap, centred between the two
gaps, so it no longer sits on the start of the long dash.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Save As suggested `.dxf` for every untitled drawing, so saving new work as
DWG meant typing the extension each time. AutoCAD keeps this choice in
Options > Open and Save > "Save as".
- File > Default Save Format > DXF / DWG sets the format Save As suggests
for drawings that have no file yet. Drawings opened from a file keep that
file's format, as before. DXF stays the default.
- The choice is saved across restarts with the other preferences
(`saveFormat`), survives Reset Palettes, and is scriptable as
`ui.saveformat {format}` or the `saveFormat` UiState field.
- Ukrainian labels for the new menu entries.
On macOS the suggested `.dwg` name needs DWG in the Save As filter list
(#198) to survive the dialog.
Claude-Session: https://claude.ai/code/session_019uMCkRAB8DMd8MEmoY9Vyr
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Extents: ignore rays and references to missing or empty blocks, as ZOOM Extents does
Opening real drawings in CADCraft often showed a tiny plan in a huge empty
canvas, because the extents included things that draw nothing:
- the base point of a RAY (Autodesk's own reference says infinite
construction lines and rays do not affect ZOOM Extents; an office plan in
our corpus carries two rays 1.98 million units from the building);
- the insertion point of an INSERT whose block is missing (a purged
definition, a bound xref that never came along) or empty (an unloaded
xref): a floor plan had such references 125 000 and 3.4 million units away.
XLINEs were already ignored; rays now are too, and an insert contributes only
what its block draws (attributes still count). Two regression tests use the
measured coordinates.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Extents: skip invisible block entities, attdefs and blank texts
- Entities in a block definition count for an insert's (and a dimension
block's) extents only if they are not AttDefs, have common.visible set,
and sit on layer "0" or on a layer that is on and thawed. Dynamic blocks
keep hidden constraint geometry far from the visible part.
- Text/MText that is empty, whitespace-only or only "\~" has empty bounds.
- Tests cover each rule separately.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
A wipeout was drawn only as its outline, so objects behind it stayed
visible on the canvas and in PNG, SVG and PDF output. The display list
gets a new primitive kind, Kind::Mask: the wipeout polygon as triangles
that every backend fills with its background colour (paper white for
SVG/PDF and plots, the raster background for PNG, the canvas colour or
the white sheet on screen), in draw order, so it hides what came before
it (DRAWORDER decides). The frame follows WIPEOUTFRAME: 0 none, 1 shown
and plotted (default), 2 shown but not plotted.
The GPU canvas drew all fills before all lines; it now splits the mesh
into draw-order groups at each mask (MeshData::breaks) and draws each
group's fills, lines and quads in turn. Infinite lines stay on top as
before.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A polyline with width was always filled as one continuous band. Now,
with a linetype, each dash is a filled piece of the band (tapers and arc
segments included) and the gaps stay empty; dots are drawn as a line
across the band. As for thin polylines, PLINEGEN runs the pattern along
the whole polyline, otherwise it restarts at every vertex, and a pattern
too small to see (min_dash) fills the whole band.
linetype::ranges lays the pattern out along a length; wide::dashed cuts
the band at those distances.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
display_rgb flipped every near-white colour to black on paper and every
near-black colour to white on the dark canvas, so ACI 51/255 and
true-colour white printed black and dark index colours showed white.
Display-list primitives now carry an aci7 flag set when the colour
resolves to 7 (directly, by layer or by block); only those flip.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
-BLOCK was an alias of BLOCK, so the selected objects were always
converted to a block reference; a script doing -BLOCK then INSERT ended
up with an extra reference. -BLOCK is now its own command: the command
line erases the selection, and its JSON form defaults to keep "delete".
BLOCK (menu Make..., typed BLOCK) and the "block" JSON form still
convert by default.
Both forms ask 'Block "<name>" already exists. Redefine it? [Yes/No]
<N>:' when the name is in use; No asks for the name again.
Session::start resolves a dashed name to a command with that id (the
same change as #280's -ATTEDIT).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The offset copy of a spline was always rebuilt with the open fit-point
interpolation, so a closed spline came out open with a corner at its start
point. Use Spline::from_fit with the source's closed flag, which gives a
periodic spline for closed input.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
wblock {name, base} ignored base and always measured from the block's
own base point. base (in block coordinates) now overrides it, the way
it already worked for selected objects. In both modes the base point
becomes the written drawing's origin, and $INSBASE is 0,0,0.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The linetype library is defined in inches, and LINETYPE loaded it
unchanged into metric drawings: DASHED was 0.5 mm on, 0.25 mm off at
LTSCALE 1, so dashed lines looked solid. CAD programs load ISO-scaled
definitions when MEASUREMENT is 1 (the same patterns with lengths times
25.4). library::standard_linetypes_for(d) does the equivalent with our
own definitions: in a metric drawing every length (dashes, gaps, text and
shape size, offsets) is scaled by 25.4 as it is loaded. LINETYPE and the
CENTER linetype that CENTERMARK/CENTERLINE load use it. Definitions
already in a drawing are not changed.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Objects whose lineweight resolves to Default were always drawn and
plotted at 0.25 mm. They now use LWDEFAULT (hundredths of a mm, 0..211),
falling back to 0.25 mm when it is unset or out of range. All backends
(canvas, PNG, SVG, PDF) read the width from the display list.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Session::execute and interactive input already ran under catch_unwind;
the machine factory, begin (the first step of Session::start), prompt and
preview (called by the UI every frame) did not, so a panic there took
down the app. Session::start now wraps each machine in guard::Guarded,
which runs all of its steps under catch_unwind. A panic in the factory,
begin or input ends the command with an internal error and the session
restores the drawing, as for input. prompt and preview can't end the
command, so they fall back to an "Internal error" prompt and no preview,
and the next input ends the command the same way. AGENTS.md/CLAUDE.md
name the wider guard.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Array commands: ARRAY, and the edit-array option loops for ARRAYRECT/ARRAYPOLAR (#230)
ARRAY (alias AR) selects objects, asks for the array type and continues as
ARRAYRECT, ARRAYPATH or ARRAYPOLAR. ARRAYRECT no longer creates a fixed 4x3
grid right after selection: it previews the array and loops on
ASsociative/Base point/COUnt/Spacing/COLumns/Rows/Levels/eXit. ARRAYPOLAR
asks for the center (or Base point), then loops on Items/Angle between/
Fill angle/ROTate items/eXit. The copies are created on eXit or Enter.
Associative arrays, levels and polar rows report "not available yet" and
return to the loop without consuming the next input.
The array commands move to cmd/array.rs. Arrays stay non-associative
copies, and the JSON forms are unchanged. A full clockwise polar fill
(-360) no longer puts the last item on top of the source.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs/gaps.md: narrow F5 (array option loops, #230)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Arrays: refuse arrays over MAXARRAY instead of hanging or shrinking them (#230)
`arrayrect {"rows":1000000,"cols":1000000}` was clamped to 1000x1000 and
then created a million copies of every selected object, which hangs the
app. ARRAYRECT, ARRAYPOLAR and ARRAYPATH (JSON and interactive, also via
ARRAY) now count items x selected objects and refuse an array over the
new MAXARRAY session variable (default 100000, settable 100..10000000)
with a message naming the limit, instead of clamping the counts and
silently creating fewer copies. The interactive count prompts accept at
most what still fits under MAXARRAY.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
SPLINE Close now builds a smooth closed (periodic) curve through the fit points
instead of repeating the first point. The geometry gets Spline::from_fit_points_closed:
chord-length parameters including the closing chord, a periodic knot vector, and the
first three control points repeated at the end. The fit data no longer duplicates the
first point. DXF writes group 70 bit 2 (periodic) for it, and a file whose closed spline
needs refitting is refitted closed. SPLINEDIT Close/refit/move and fit-point grips keep
a closed spline periodic.
SPLINE Method asks Fit/CV, and CV continues as SPLINE by control vertices. Knots, Object,
start/end Tangency and toLerance report "not available yet" and stay at the prompt.
Garbage text reports "Point or option keyword required.".
POLYGON with a typed radius puts the bottom edge horizontal for Inscribed and for
Circumscribed. The JSON form uses the same orientation when no angle is given.
CIRCLE reports "Point or option keyword required." for an invalid token at the centre
prompt, and "Invalid point." at the 3P/2P point prompts, then asks again.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The Save As dialog listed DXF, SVG, PNG and PDF, though CADCraft writes
DWG. On macOS the dialog accepts only listed extensions, so a name typed as
`part.dwg` was changed to .dxf and the drawing was saved as DXF.
Save As now lists "Drawing (DWG)" too. The existing suggested-type-first
ordering does the rest: a drawing opened from DWG defaults to DWG, while new
drawings and DXF files keep DXF first. The list moves into
`filters_for_save` with a unit test, and the label gets its Ukrainian
translation following "Drawing (DXF)".
Related to #14 (the Open half was fixed in #21).
Claude-Session: https://claude.ai/code/session_019uMCkRAB8DMd8MEmoY9Vyr
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
AutoCAD and BricsCAD store a mirrored block reference, and many 2D
entities drawn in a mirrored UCS, with extrusion (0,0,-1). Their points
and angles are then in an OCS whose x axis runs the other way. The reader
only honoured that for CIRCLE, ARC, LWPOLYLINE and SOLID/TRACE, so
mirrored blocks, hatches and texts landed on the wrong side of the
y axis (e.g. profile blocks at x -57618 instead of +57618).
- INSERT: insertion point through the OCS, negative x scale, opposite
rotation and negative column spacing. Only for extrusions parallel to
-Z (x/y noise up to 1e-3); a tilted block plane needs a 3D transform
that Insert cannot hold and is left as read.
- HATCH and R12 2D POLYLINE: boundary x and bulges mirrored like
LWPOLYLINE; the hatch pattern angle mirrors too.
- TEXT/ATTDEF/ATTRIB and MTEXT: points through the OCS; drawn readable
(MIRRTEXT = 0) with the mirrored footprint, so the direction turns and
left/right justification or attachment swap.
- ELLIPSE: centre and major axis are WCS, but the minor axis is
extrusion x major, so the parameter range is negated.
- DIMENSION: the text midpoint (11) is the one OCS point.
Checked against an independent DXF library on private drawings: every
INSERT transform (plain, mirrored, tilted) matches, as do the mirrored
arcs, ellipses, polylines and hatches. The remaining extents outliers in
those drawings come from references to empty blocks, which still add
their insertion point to the extents.
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
With DIMASSOC on, every chained dimension looked up the objects under
its definition points by scanning the whole space, and each point adds
an entity, so a long `points` list took quadratic time (16,000 points:
2.7 s continue, 10 s baseline). The chain now builds one x-sorted
SnapIndex up front. Dimensions are no snap targets, so the index stays
valid while the chain adds them, and each lookup is a binary search. The
index picks the same snap as find_snap: lowest rank first, then the first
object in space order. A call with more than 100,000 points is refused.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Redefining a block with a selection that holds a reference to that block,
directly or through nested blocks, is now refused with "Block "B"
references itself." and changes nothing. Before, the block ended up
containing itself and lost its original geometry. At the command line
the message ends -BLOCK, as for any definition that can't be made.
Redefining with a name that differs only in case (Door, then DOOR)
replaces the existing block instead of adding a second one.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
dxf_ext::is_paper_view (#225) matched any 1000 string in the CADCraft xdata, but later strings
are layer names (VPFROZEN, VPCOLORS). A real viewport with a frozen or recoloured layer named
PAPERVIEW was therefore dropped on read. Only the first string, where the writer puts the
marker, counts now. Regression test in tests/paperview_marker.rs.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
With PSLTSCALE on (the default), dashes of model-space objects seen through a
viewport keep their paper-space length whatever the viewport scale; before,
a 1:100 viewport drew them 100 times too short, so dashed lines looked solid
in layouts and PDF plots. PSLTSCALE 0 keeps the old behaviour.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
POINT objects were always a one-pixel dot. They now draw the figure PDMODE
picks (nothing, plus, cross, tick, with an optional circle and/or square)
at the size PDSIZE gives: drawing units when positive, a percentage of the
view height when negative, 5 % when 0. The canvas passes its view height;
exports and plots use the plotted window or the extents.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Bound block expansion in drawing and extents (fixes#255)
MAX_BLOCK_DEPTH bounds only the depth of nested block references, so a
small DXF with a self-referencing block, blocks that each insert the next
several times, or one large MINSERT expanded to billions of copies and
hung the app (drawing, PNG/SVG/PDF export, extents, saving).
- render: a per-display-list budget (MAX_BLOCK_EXPANSION, 2 million
block copies and entities drawn inside blocks); past it the remaining
block contents are not drawn.
- doc: entity_bounds computes each block's content bounds once per call
and reuses them for every further reference.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Move Builder::expand below the drawing primitives
Pure code motion so this branch merges cleanly with fix/272-aci7-only-flips
(#272), which changes the Builder::polyline signature next to it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
spline {control, degree} took any degree up to the control-point count
minus one; tessellation and extents grow with its square, so a few
thousand control points with a high degree hung the app on every redraw.
The Degree prompt, spline.cv and DXF input already limit it to 1..10.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
tiny-skia's anti-aliased fill_rect hits a debug_assert when a rect under
two pixels wide has both sides mid-pixel, so PNG export panicked in debug
builds depending on where a point landed. Fill the same 1.5 px square
through the path filler instead.
Fixes#270.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- entity_props_roundtrip: look for the Standard text style inside the STYLE
table; #216 now also writes Standard multileader and table styles.
- uk.tsv: translate the Accessibility menu and Show System Cursor (#187).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DWG saves convert our R2000 DXF with acadrust, which wrote an R2000 DWG:
no true colours, no entity transparency, no table styles, and its R2000
reader puts every layout's paper-space entities into the first layout.
The bridge now writes at least AutoCAD 2004 (AC1018) DWG; the DXF itself
stays R2000. acadrust goes to 0.6.3 (explicit transparency encoding).
DWG doesn't store viewport ids: readers number a layout's viewports in
order, and id 1 is the paper-space viewport (the sheet). CADCraft keeps
that one implicit, so its first viewport came back as id 1 and wasn't
drawn. The DXF writer now puts a paper-space viewport (id 1, marked with
CADCraft xdata) ahead of a layout's viewports when it has none, and the
reader drops it again. Viewports also get status 68 and the always-set
bit 32768 in group 90, so other readers don't take them as "off".
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
On wasm, saving skipped the file write but still marked the drawing saved,
renamed its tab and reported success, so nothing reached the user. Saving
now hands the bytes to a host delivery hook (`file::set_deliver`); the web
app installs a small web-sys download helper (Blob + object URL + temporary
<a download>), reusable for other files such as plots. Without the hook, or
when the download can't be started, save errors and the drawing stays dirty.
A started download counts as saved and keeps the name for later QSAVEs.
The web app's Save/Save As (menu, close prompt) download under the drawing's
name, as there is no file picker in the browser.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The web app ran CadApp with Services::default(), so OPEN found no file
picker and returned without doing anything: no picker, no message.
The web app now installs an open picker (apps/cadcraft-web/src/upload.rs,
web-sys only): a hidden <input type="file" accept=".dxf,.dwg"> is
clicked, the chosen file is read in the background (Blob.arrayBuffer)
and handed to the app on its next frame, which runs OPEN with
{data, name} through the new CadApp::open_bytes. Cancelling the picker
does nothing. Files dropped on the page, which eframe hands over unread
on the web and the UI ignored there, take the same route.
Files over 256 MB (CadApp::MAX_OPEN_BYTES; the browser holds the drawing
several times while decoding it) are refused before they are read.
Unreadable or rejected files report on the command line and in the
status bar; nothing panics.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The canvas hides the OS cursor (CursorIcon::None, which winit turns into
ShowCursor(FALSE) on Windows) and draws its own crosshair, so Windows
Magnifier's "follow the mouse pointer", screen readers, recorders and
remote desktops lose the pointer there. View > Accessibility > Show System
Cursor (ui.toggle.systemcursor, ui.set {"systemCursor": true}, or
CADCRAFT_SYSTEM_CURSOR=1 at startup) keeps a system crosshair visible at
the centre of the drawn one.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
AppKit draws the traffic lights centred in its standard 28 pt title-bar
band, and the integrated title row is that band, but the title text sat
1 pt below the centre line. It is now centred on the lights. Neither
winit nor eframe can move the lights without AppKit calls (unsafe), so
the row fits them instead, as in WordCraft (storytold/wordcraft#255).
The geometry lives in chrome.rs. A long drawing name in a narrow window
no longer slides under the lights: the title starts after them, with the
same gap the lights keep from the top of the window.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
UNDO now prompts "Enter the number of operations to undo or
[Auto/Control/BEgin/End/Mark/Back] <1>" and undoes that many; Enter undoes
one. Its other options say they are not available yet (Auto and Control
still take their value), so nothing that follows is run as a command.
The JSON form (`undo {count?}`) is unchanged.
U is its own command, as in AutoCAD: it undoes one operation without
asking and is what Edit > Undo, Cmd+Z and the toolbar button run.
An interactive command outside undo that changed the drawing (UNDO)
records no undo step when it finishes, matching `execute`.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
TEXTEDIT's prompt no longer reads "or or". Undo reverts the last edit made
in this TEXTEDIT, and Mode switches between Multiple (keep selecting) and
Single (end after one edit). At the command line and in scripts, MTEXT
takes one paragraph per input line until an empty line, instead of ending
after the first line and running the next one as a command. The Text
Editor window sends its contents and then Enter, so it still ends MTEXT.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
AREA's Object option measures picked objects; Add area / Subtract area keep
a running "Total area" over areas measured by points or by object, and
Enter at the first-corner prompt ends the command. Length continues the
last segment by a typed length in AREA and DIST point sequences. DIST's
Multiple points measures a path and reports the running and total
distance. Arc now says it is not available yet instead of being ignored
(or ending AREA as Total).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
"Specify the reference angle <0>" takes a typed angle or two points;
"Specify the new angle or [Points] <0>" takes a typed angle, a point
measured from the base point, or two points after `P`. The objects turn by
the new angle minus the reference angle. Previously Reference only said
"not available yet" and the values that followed were misread or run as
commands.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
ELLIPSE: after Rotation the next value is the rotation around the major
axis (ratio = cos angle, 0 to 89.4 degrees; a picked point gives the
angle from the centre), instead of the other-axis distance. At the
elliptical arc's start/end prompts, Parameter reads the values as
ellipse parameters and Angle switches back to true angles.
BREAK: First point asks for a new first break point; the selection pick
then only selects the object.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Raster images: the reader now follows an IMAGE's group 340 to its IMAGEDEF
and keeps the file path and the definition's name (its ACAD_IMAGE_DICT
key), pixel size and resolution unit. It also keeps the clip boundary (in
pixel coordinates), the clipping state, the display flags and
brightness/contrast/fade. The writer emits the IMAGE entity, plus one
IMAGEDEF per path and name, an IMAGEDEF_REACTOR per image,
RASTERVARIABLES, the ACAD_IMAGE_DICT and ACAD_IMAGE_VARS entries in the
named object dictionary, and the four CLASS records, as the DXF Reference
describes. An image without a clip boundary gets the full-image rectangle.
DWG save keeps the same data, except the resolution unit, which the DWG
bridge drops. The pixels are still not loaded.
Entity types CADCraft doesn't model (MLINE, MULTILEADER, 3DSOLID, REGION,
MESH, ...) are still not written. Writing their raw groups back into the
R2000 file would leave references to objects that aren't kept
(MLINESTYLE, MLEADERSTYLE, ACIS data in later versions), and some of the
types don't exist in R2000 at all. Another CAD program could then reject
the whole file. Instead, qsave, saveas and document.bytes now say on the
command line which ones weren't saved ("Not saved (CADCraft can't write
these object types yet): 1 3DSOLID, 2 MLINE") and list them by type in the
result's `notSaved`. `cadcraft-cli run --save` and `convert` print the
same line.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Saving to DXF and reopening lost several table and object-section items.
They are now written and read as the DXF Reference describes:
- LTYPE: embedded text and shape elements (74 type flags, 75 shape number,
340 STYLE, 46 scale, 50 rotation, 44/45 offset, 9 text). Shapes from a
shape file get an unnamed shape STYLE record (70 bit 1). DashElement
gains an `absolute` flag for absolute rotations (74 bit 1). The built-in
FENCELINE1 names its text style (Standard).
- MLEADERSTYLE objects in ACAD_MLEADERSTYLE with arrow size, text height,
landing gap, dogleg length and text style, plus $CMLEADERSTYLE, so the
current multileader style survives reopening.
- LAYOUT plot settings: device (2), plot style table (7), plot area (74),
custom scale (142/143, 147), scale to fit (70 bit 16 with 75 = 0),
centred plot and lineweights (70 bits 4 and 128). The layout's saved
paper-space view goes in CADCraft xdata on the LAYOUT object.
- VIEW and UCS table records, and GROUP objects in ACAD_GROUP (members
that were written to the file). A named view's layer state is CADCraft
xdata on the VIEW record.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Saving to DXF and reopening lost several entity, block and style
properties:
- Thickness (group 39) is written for LINE, POINT, CIRCLE, ARC, TEXT,
SOLID, TRACE, LWPOLYLINE and POLYLINE when non-zero.
- POINT writes its X-axis angle (group 50).
- HATCH: the origin goes into the pattern lines' base points (43/44), so
other programs see the shift, and travels with the background colour as
CADCRAFT xdata (1000 HATCH, 1011 origin, 1000 colour name); the DXF
Reference has no groups for either. It is appended to the gradient
xdata when there is one. The reader takes the elevation from the z of
the first group 10.
- Xref blocks get BLOCK 70 bit 4 and their path in group 1, and the
reader reads the path.
- The built-in "CADCraft Stroke" font is still written as "txt" for other
readers; its name travels as CADCRAFT xdata (1000 FONT) and is used
while group 3 is still "txt".
- Polyface (70 bit 64) and polygon mesh (bit 16) POLYLINEs from other
programs are read as 3D faces instead of a 3D polyline through their
vertices; face records no longer become vertices at the origin. Bad
indices and counts are skipped.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The ATTDEF writer put only the invisible bit into group 70, so constant
attribute definitions came back as variable ones: INSERT then created an
attribute for them, and other programs stopped drawing their text. The
constant bit (2) is now written too.
BLOCK's description was never written (BLOCK group 4) nor read, and the
insertion units (BLOCK_RECORD 70) and explodability (280) of blocks were
neither read nor written. All three now round-trip.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
LAYERSTATE.SAVE stored its states in Drawing::layer_states, but the DXF
writer never wrote them and the reader never read any, so every saved
layer state was gone after saving and reopening (DWG too, as it is
written from the same DXF).
They are now written like the parametric constraints: as JSON in a
CADCRAFT_LAYERSTATES XRECORD of the named object dictionary, read back
with the same payload and size limits. The XRECORD chunking and reading
are shared by both records.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Rebase onto main: the interface language is saved with main's other
preferences (cadcraft.prefs, beside the theme) instead of its own storage
key, on desktop and web; strings main added or changed in the touched
places (Dynamic Input settings, Properties header and current-property
rows, block picker, theme and command-history menu items, open/save
filters) are wrapped and translated; main's native-menu poll order is kept.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Closing the window (title bar, File > Exit, Cmd+Q) with unsaved drawings now
cancels the close and asks Save / Don't Save / Cancel for each one, through
the same in-app prompt as File > Close All (#172). The app quits once every
drawing has been saved or discarded; Cancel keeps it open. The control
channel's app.quit never asks.
Rebuilt on top of #172 from LloydNicholson's original PR.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Dynamic Input: pointer input boxes, Tab separator, relative entry
While a command asks for a point and Dynamic Input is on, the cursor
tooltip shows a pair of value boxes, following AutoCAD's pointer input:
- The first point of a command is X and Y, absolute.
- Second and next points default to polar (distance and angle) measured
from the last point, so no `@` is needed.
- Typing `,` after a value locks it as X and moves to Y; `<` locks it as
the distance and moves to the angle. A locked value holds the cursor and
shows a lock.
- Tab types the separator for the format on show, so `40` Tab `90` is 40
units at 90 degrees, and with the Cartesian setting `40` Tab `40` is 40
over and 40 up.
- Enter or Space sends the entry with `@` in front, filling an empty box
from the cursor. A plain number and Enter is still direct distance entry;
`@`/`#` entries, keywords and command names go through untouched.
The command-line text is the single source of truth: the boxes are a view
of it, so the command line always shows exactly what will be sent
(`40,`, `40<90`) and Backspace simply edits it.
Tab is taken from egui in the raw input hook (desktop and web) while no
widget has focus. Otherwise egui moves keyboard focus to the next button on
Tab and the command line stops receiving keystrokes.
Two new settings mirror AutoCAD's: DYNPIFORMAT (1 = second points start
Cartesian) and DYNPICOORDS (1 = absolute), through SETVAR and in Drafting
Settings under Dynamic Input. The tooltip uses the theme tokens, matching
the command line.
Scripts, the control channel and MCP never pass through the boxes and keep
the plain command-line rules. The parsing lives in a small pure module
(dyninput.rs) with unit tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019uMCkRAB8DMd8MEmoY9Vyr
* Dynamic Input tooltip: border colour from theme tokens
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Typing UNITS ran the JSON form with no parameters, which changed nothing
and printed nothing. Typed or menu-invoked UNITS (no parameters) now opens
a Drawing Units dialog like QSELECT does: length type and precision, angle
type and precision, insertion units and a sample output. OK applies the
values through the `units` command; JSON calls run the command as before.
SETVAR had no prompts either, so `SETVAR LUNITS 2` ran with no name and
the remaining inputs were dropped. Space acting as Enter was not the
cause. SETVAR now asks "Enter variable name or [?]" and then "Enter new
value for NAME <current>"; `?` lists the variables, unknown names end the
command, and a value of the wrong type re-prompts.
sysvars::set now refuses variables that `get` derives from state
(DWGNAME, DBMOD, EXTMIN, ...) and keeps integer header variables integer,
so a typo can no longer store text in LUNITS.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* DIMANGULAR: accept a circle
The first prompt offers "arc, circle, line" but picking a circle was
rejected. Picking a circle now uses its center as the angle vertex and
the picked point as the first endpoint, then asks for the second
endpoint.
* cargo fmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
`SETVAR DIMSCALE 24` was stored in the header but never reached new
dimensions: they were created with no overrides, and the header DIMSCALE
was only a fallback for styles whose scale is zero.
As in AutoCAD, the header DIM* variables that differ from the current
dimension style are now its overrides, and new dimensions take them on
(`Drawing::dim_overrides`). Existing dimensions keep their size. Making a
style current, or editing the current style, sets the variables back to
the style's values (`Drawing::sync_dim_vars`), so
`dimstyle {"name": "Standard", "DIMSCALE": 24}` still works.
GETVAR/SETVAR read DIM* variables the header doesn't carry from the
current style.
Dimension extents now use the same scale as the geometry (the style with
the dimension's overrides) instead of multiplying by the header DIMSCALE.
A DIMSCALE of zero (AutoCAD: scale to the layout viewport) falls back to
the drawing's DIMSCALE and then 1 instead of 1e-9.
Reading DXF, $DIMDSEP becomes the separator character, as the DIMSTYLE
table already is, so AutoCAD's header variables don't turn into spurious
overrides.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Selection windows only worked click-then-click: the canvas reacts to clicks,
and a press that moves before release is a drag, not a click, so a drag was
ignored entirely.
Now, while objects are being selected (no command running, or a command
asking for objects), a drag opens the window where the button went down and
letting go closes it, as with AutoCAD's default PICKDRAG = 2. Dragging right
is a window, dragging left a crossing window, Shift still removes, and
click-click works as before. Drags that start on a grip or while a command
asks for a point are left alone.
Session::begin_window opens the window at the drag's start, even over an
object (a click there would pick the object instead), with an engine test.
Claude-Session: https://claude.ai/code/session_019uMCkRAB8DMd8MEmoY9Vyr
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Write Wipeout entities to DXF so saving keeps them (#79)
* Wipeout: clip vertices are in image pixel space (origin top-left, y down)
Write and read the boundary with y flipped, so other programs show the same
shape and wipeouts from other programs are no longer mirrored vertically.
Tested with an asymmetric boundary.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Wipeout: collapse nested if (clippy)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* DXF/DWG: keep hatch gradients through save and reopen (fixes#80)
The HATCH writer never emitted a gradient and the reader always set
`gradient: None`, so a GRADIENT fill reopened as a plain SOLID hatch after
any DXF or DWG save.
- Writer: native HATCH groups per the DXF Reference (450 gradient flag,
451, 452 two-colour, 453 colour count, 460 angle in radians, 461 shift
(0 = centered), 462 tint, 463 per-colour records with 63 ACI and 421 true
colour, 470 name), plus a `CADCRAFT` `GRADIENT` xdata copy (name cut to
255 bytes, angle, centered, colour names).
- Reader: native groups win when present (another program may have edited
them); otherwise the xdata copy is used. The xdata is what survives DWG
saves: the file is R2000 (AC1015), whose DWG HATCH has no gradient fields,
so the DWG bridge drops the native groups.
- Hostile values (NaN angles, out-of-range colours, extra colour records,
missing groups) fall back to the GRADIENT command's defaults.
Tests: gradient_hatch_roundtrips_through_dxf,
gradient_hatch_roundtrips_through_dwg,
gradient_falls_back_to_xdata_and_native_groups_win,
long_gradient_name_is_cut_only_in_xdata, hostile_gradient_groups_never_panic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* contributors: add vangel76
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gradient test: expect \U+ escapes now that non-ASCII text is escaped (#169)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The squash for #159 picked up an earlier revision of the #140 test, which
called as_bytes() on a Vec<u8>. Also apply rustfmt to code from #120 and #155.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* DIMANGULAR: accept polyline segments (fixes#10)
Picking a side of a rectangle or a PLINE segment was rejected with
"Select a line or an arc." because only LINE and ARC entities were
accepted. The picked object is now resolved to the segment under the
cursor: straight polyline segments work like lines and arc segments
like arcs.
* Redraw edits made while a command is still running (fixes#4)
LINE and PLINE add geometry on every point, but the canvas only rebuilt
its display list when the drawing revision or the document pointer
changed. The revision was bumped when a command finished, and the
pointer changed only on the first copy-on-write edit, so only the first
segment appeared until the command ended. Session::feed now bumps the
revision after each input once the running command has changed the
drawing.
* Fix#14: include DWG in the Open drawing filter
* Blocks window: click a block to insert it (#8)
Each block name in the Blocks window is now a button. Clicking it starts INSERT with that block already chosen, so the command line asks for the insertion point.
* SCALE: Reference option (#17)
SCALE > Reference printed "not available yet". It now asks for the reference length (typed, or two points) and the new length (typed, or a point measured from the base point), and scales by new / reference. Zero, negative or non-finite lengths are rejected and asked again.
* Print and Ctrl+P ask where to save the PDF (#5)
PLOT/EXPORTPDF started from the menu, the toolbar, Ctrl+P or the command line had no path,
so the PDF was built in memory and dropped. They now open the save dialog (PDF filter first)
and write the file there. JSON calls from scripts, the control channel and MCP still never
open a dialog. Ctrl+P was shown in the menu but not bound; it is now.
* Scripts: split a line at keyword prompts so POLYGON 6 200,20 I 15 works (#43)
* Layer Properties Manager: stop the window growing every frame (fixes#29)
The vertical separator between the filter tree and the table takes all the
height it is offered, and a resizable window offers its previous height, so
the window grew 30px per frame until it filled the screen. Cap the row at the
table's fixed height. Regression test runs the dialog headless for 30 frames
and checks the height stays put.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ui.render: fit the drawing and return pngBase64 like the headless render (#49, #50)
* Linux: default to XWayland for file drag-and-drop on Wayland desktops
* Fonts: per-character fallback, FONTALT and .ttc for CJK text in SHX styles (#130)
CJK text in a style that uses an SHX font (romans.shx + chineset.shx big font)
drew every ideograph as an empty box: the SHX name resolves to our stroke font,
which has no CJK glyphs, and the font scan skipped .ttc collections, where most
CJK system fonts live.
- Per-character fallback: a character the text's font lacks (stroke font or
TrueType) is drawn from an installed font that has it, checked by cmap.
Wide-coverage fonts go first (Noto Sans CJK, Source Han, WenQuanYi, Droid
Sans Fallback, PingFang, Microsoft JhengHei/YaHei, MS Gothic, ...), regular
weights before others. Fallback glyphs are sized like TrueType text (cap
height = text height). Lookups are cached; the search is bounded (24 files,
160 MiB, 256 faces per character, files over 96 MiB never read) and skips
spaces, controls and private-use code points. No fallback: the box as before.
- FONTALT (sysvar, seeded from CADCRAFT_FONTALT): font used for a style font
that can't be found (an SHX file, a missing TTF). Empty = our stroke font.
- FONTFALLBACK (sysvar, seeded from CADCRAFT_FONTFALLBACK): fonts tried first
for missing characters, e.g. "Noto Sans CJK TC".
- .ttc/.otc are scanned; TextFont::Outline carries a ttf::Face (bytes + face
index). Fonts are also found by family name among files with a similar
stem, so "Noto Sans CJK TC" picks face 3 of NotoSansCJK-Regular.ttc.
- Cap height falls back to the height of "H" when OS/2 has none (DejaVu
Sans), which fixes ttf_tests::system_font_outlines_when_available and
render's truetype_text_is_filled_or_outlined on machines without Arial.
- README: a Fonts section for the two settings; ROADMAP M4 note.
Native SHX / big-font rendering stays on the roadmap (M4).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fonts: cap the total bytes of system font files loaded from disk
A drawing naming many text styles could otherwise load every installed font
into memory through family-name lookups.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Pan with a two-finger trackpad swipe; keep wheel and pinch zoom
Trackpad two-finger swipes arrive as scroll events, and the canvas turned
every scroll into a zoom, so trackpad users had no realtime pan (middle-drag
needs a middle button).
Scroll now follows the device: precise point deltas (trackpad, Magic Mouse)
pan the view by x and y; notched line deltas (mouse wheel) zoom about the
cursor as before. Pinch and Cmd+scroll still zoom through zoom_delta, and
middle-drag still pans.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019uMCkRAB8DMd8MEmoY9Vyr
* Add Tru10 to contributors/people.toml
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019uMCkRAB8DMd8MEmoY9Vyr
* Trackpad pan: only on macOS, where point deltas mean a trackpad
Browsers and Wayland smooth-scroll mice report ordinary wheels in points too,
so the wheel keeps zooming there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep arc-length dimensions as arc-length through DXF save and reopen (#100)
* PLINE: Halfwidth is centre-to-edge, and Width answers taper the next segment (fixes#83, #104)
Interactive PLINE kept one width value: Halfwidth answers were stored as
the total width, and the ending-width answer overwrote the starting one,
so a taper became a constant width. The next segment's starting and
ending widths are now tracked separately, Halfwidth answers are doubled,
and each new segment (including the closing one) gets its widths on its
first vertex; later segments are uniform at the ending width, as in
AutoCAD. Uniform polylines are still stored as const_width, so their DXF
output is unchanged.
Fixes#83. Fixes#104.
* Arc-length dimensions: write a standard aligned record plus CADCraft xdata
DXF has no arc-length DIMENSION type, so write type 1 and mark the kind and
arc centre in CADCRAFT xdata (like ASSOC). Other programs read an aligned
dimension; CADCraft restores the arc length, through DXF and DWG.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Desktop: write log records to standard error and a rotating log file
The desktop app installed no logger, so every log::warn!/log::error!/
log::info! in the workspace was discarded: a broken app icon, the GPU
canvas set-up, the engine's command-line echo at debug level. A launch
from a desktop menu has no terminal either, so there was nothing to
attach to a bug report.
apps/cadcraft/src/logging.rs (no new dependency):
- standard error plus <settings dir>/logs/cadcraft.log; each launch
rotates the previous log to cadcraft.1.log and .2.log, so the log of a
run that crashed survives the next start; the file is capped at 16 MiB
- records logged before the settings directory is known are buffered
(bounded) and written once the file is attached
- default levels: info for cadcraft*, warn for everything else;
RUST_LOG replaces them with env_logger-style directives
- never panics: an unusable directory or a failed write leaves standard
error as the only sink; a poisoned lock is tolerated
CADCraft had no per-user directory yet. config_dir in main.rs adds one:
CADCRAFT_CONFIG_DIR, else $XDG_CONFIG_HOME/cadcraft or ~/.config/cadcraft
(Linux, BSDs), ~/Library/Application Support/CADCraft (macOS),
%APPDATA%\CADCraft (Windows).
The logger is installed first in main and the file is opened after
argument parsing (--version leaves no file). The control server's
eprintln!s now go through the logger. A --control or
CADCRAFT_CONTROL_PORT value that is not a port is now logged instead of
silently leaving the control channel off. Documented in README.md
(Quick start > Logs).
---------
Co-authored-by: SBaksa <sabaksa11@gmail.com>
Co-authored-by: DWAA1660 <dwatnip123@gmail.com>
Co-authored-by: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com>
Co-authored-by: lloyd <34131158+LloydNicholson@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Tru10 <133776756+Tru10@users.noreply.github.com>
Co-authored-by: Gert Pellin <gert@pellin.be>
The vertical separator between the filter tree and the table takes all the
height it is offered, and a resizable window offers its previous height, so
the window grew 30px per frame until it filled the screen. Cap the row at the
table's fixed height. Regression test runs the dialog headless for 30 frames
and checks the height stays put.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Each block name in the Blocks window is now a button. Clicking it starts INSERT with that block already chosen, so the command line asks for the insertion point.
SCALE > Reference printed "not available yet". It now asks for the reference length (typed, or two points) and the new length (typed, or a point measured from the base point), and scales by new / reference. Zero, negative or non-finite lengths are rejected and asked again.
PLOT/EXPORTPDF started from the menu, the toolbar, Ctrl+P or the command line had no path,
so the PDF was built in memory and dropped. They now open the save dialog (PDF filter first)
and write the file there. JSON calls from scripts, the control channel and MCP still never
open a dialog. Ctrl+P was shown in the menu but not bound; it is now.
Picking a side of a rectangle or a PLINE segment was rejected with
"Select a line or an arc." because only LINE and ARC entities were
accepted. The picked object is now resolved to the segment under the
cursor: straight polyline segments work like lines and arc segments
like arcs.
LINE and PLINE add geometry on every point, but the canvas only rebuilt
its display list when the drawing revision or the document pointer
changed. The revision was bumped when a command finished, and the
pointer changed only on the first copy-on-write edit, so only the first
segment appeared until the command ended. Session::feed now bumps the
revision after each input once the running command has changed the
drawing.
The key labelled "delete" on a Mac sends Backspace, and only forward Delete
(fn+delete) erased the selection, so the obvious key did nothing.
On macOS (native, and the web build via egui's user-agent detection),
Backspace now erases selected objects, but only when it can't be meant for
the command line:
- a fresh press, never an auto-repeat, so holding delete to clear typing
can't run on into the drawing;
- a plain press: Option/Cmd+delete delete a word or line of text on macOS;
- no command running and the command line empty;
- nothing typed at the command line since the selection was made (typing,
then clearing it, leaves delete editing text until the selection changes).
Windows and Linux are unchanged: Backspace only edits the command line
there. The decision is a small pure function with unit tests.
Claude-Session: https://claude.ai/code/session_019uMCkRAB8DMd8MEmoY9Vyr
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Polyline extents were the centre line padded by const_width / 2, so
per-segment start/end widths never counted: a band built from vertex
widths, a taper, or segments of different widths reported the
zero-height bounds of the centre line. The bounds now enclose the
filled band of every segment as it is drawn: the exact corners of
straight segments (tapers included), and both edges of arc segments,
sampled along the arc with the width interpolated and widened by the
sampling tolerance so they are never too small. The constant width
still wins over vertex widths, the end vertex of an open polyline
starts no segment, and non-finite or negative widths count as zero.
Thin polylines take the same path as before.
Fixes#90.
The `arc` command checked its three-point form for collinear points but
added the arcs built by its start/center/end and center/radius forms
unchecked, so radius 0 (or a start point on the center) added a
degenerate Arc, dirtied the drawing and recorded an undo step. Every
form now goes through the existing finite_arc check and fails with the
same "these values do not define an arc" error the arc.* variants and
the interactive ARC already use, leaving the drawing unchanged.
Fixes#85.
* Render tests: drop the always-true assert in degenerate_input
`assert!(... || true)` trips clippy's overly_complex_bool_expr on current
stable (1.96), which fails `cargo xtask ci` at the clippy step. The check
only proves that NaN input does not panic, so call the function and
discard the result instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Menu commands open their dialogs; JSON calls never open pickers (#65, #101)
Tools > Drafting Settings... (DSETTINGS, DS, SE) ran the JSON form with no
parameters, which changed nothing, so the menu item looked dead; only the
status-bar gear opened the dialog. Typed or menu-invoked DSETTINGS now
opens the Drafting Settings dialog, and PROPERTIES (PR, CH) shows the
Properties palette.
The engine's OPEN, SAVEAS and QSAVE opened a native file picker whenever
their parameters had no path, so `engine.execute` and the bridged MCP
`execute` tool could block on a desktop dialog. They now ask for a file
only when typed or chosen from a menu; JSON calls get the command's own
missing-path error. The ui.open / ui.saveas commands (Cmd+O,
Cmd+Shift+S) still open the pickers.
Typing the `cmd {json}` form at the command line (e.g. `layer {}`) no
longer opens the command's dialog or picker; the engine runs it with
those parameters.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
File > Close, a file tab's x and File > Close All ran the engine's
close/closeall directly, which drop the drawing. Nothing checked for
unsaved changes on any platform; the web report just made it visible.
- New ui-egui `closing` module: close requests from the UI go through a
queue of drawings (by DocState uid). A dirty drawing is shown and asked
about in an in-app egui modal ("Save changes to X?" Save / Don't Save /
Cancel, Escape = Cancel), the same on desktop and web.
- Save routes to the existing qsave path (picker for untitled drawings)
and closes only if the drawing is really clean afterwards; otherwise it
stays open with its edits and a status message.
- Close All asks per dirty drawing in tab order; Cancel stops it.
- Menu, typed CLOSE/CLOSEALL (null params) and the tab x use the prompt;
JSON calls (engine.execute, control, MCP, scripts) keep discarding
without asking.
- Shortcuts are paused while the prompt is open; ui.inspect reports
`closePrompt`.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Linux: present without a vsync queue so the crosshair follows the mouse (#25)
The canvas hides the OS pointer and draws its own crosshair, so every frame
the swapchain holds back shows as cursor lag. Under vsync (FIFO) wgpu asks for
one queued frame, but Vulkan clamps it to the driver's minimum swapchain size:
three images on X11 (Linux Mint's Cinnamon session), i.e. two frames queued
behind the one on screen. Windows DX12 honours one. The canvas also requested
a repaint on every frame while hovered, which kept that queue full.
- gpu::surface_config: AutoNoVsync (Immediate, then Mailbox, then FIFO; valid
on every surface) on Linux/BSD, frame latency 1. CADCRAFT_VSYNC=1 restores
vsync, CADCRAFT_VSYNC=0 selects low latency elsewhere.
- gpu::FrameCap (240 fps) when presenting without vsync: nothing else paces
frames on X11, and a 1000 Hz input stream rendered ~900 frames per second.
- No continuous repaint while hovering the canvas; input repaints by itself.
The status-bar message schedules its own repaint to clear on time.
- Hover/selection highlight looks up primitives in a per-list sorted index
instead of scanning the whole display list every frame (~2.5 ms per frame
on a 270k-entity drawing).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Move frame-cap and status-repaint hunks away from other PRs' to avoid merge conflicts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Clicks on the command line's history, the UCS icon and the ViewCube ring fell
through to the drawing (picking objects or starting a selection window, or
entering a point during a command), the chevron did nothing, and the wheel over
the overlays zoomed the drawing. The ViewCube's Top face was a click-only
widget, so the canvas still counted as hovered underneath it (crosshair, wheel).
- Every overlay now registers a widget that senses clicks and drags, so egui
hands the pointer to the overlay instead of the canvas.
- Command line: a click on the history lines or the chevron (now pointing up)
opens a scrollable history window with the whole log; F2 and TEXTSCR open it
too (`ui.toggle.history`, Window > Command History). Dragging the history's
top edge sets how many lines show (`ui.cmdline.lines`, 0-12).
- ViewCube (new viewcube.rs): Top face and a Home button run `zoom.extents`;
hover highlights; right-click menu (Home, Hide ViewCube). The compass ring
says that the 2D plan view can't rotate (no view twist yet) instead of
doing nothing.
- UCS icon and the WCS pill open the coordinate-system menu (World, Hide UCS
Icon).
- Control channel: `ui.drag` (real press, move, release) and
`cmdline.state` -> `historyExpanded`.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Light and System interface themes (System follows the OS appearance live)
CADCraft had one hard-coded dark interface: Tokens::get() always returned
Tokens::DARK and Visuals::dark() was installed once at startup.
- theme.rs: Tokens::LIGHT (light grey chrome, dark text) beside the unchanged
Tokens::DARK; Tokens::get() returns the palette of the theme in use (a
UI-thread thread-local set once per frame). Both egui styles are built from
the tokens and egui is switched to the shown theme, so standard widgets,
menus and popups follow too. The drawing area (canvas, grid, axes,
selection, grips, ViewCube, UCS icon) keeps its model-space colours in both
themes: drawing colours are not part of the interface theme.
- ThemePref { System, Light, Dark } in UiState (`theme`), resolved every
frame against ctx.system_theme(), so System follows the OS (winit, or the
browser's prefers-color-scheme) live without touching the saved choice.
No reported appearance: System shows Dark (SYSTEM_FALLBACK). Default stays
Dark (today's look).
- View > Interface Theme > Use System Setting / Light / Dark (ui.theme.system,
ui.theme.light, ui.theme.dark) and ui.theme {theme} for scripts and the
control channel; ui.inspect reports the theme shown.
- Hard-coded colours on the command line (history, border, keywords,
AutoComplete list), the status-bar message and the layer status icon go
through tokens; overlays drawn on the canvas use a new canvas_ink token.
- Desktop app: the choice persists via eframe storage (`persistence`
feature, key cadcraft.prefs); window geometry and egui memory are not
persisted, as before.
Fixes#59.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Document the theme field away from other PRs' edits to avoid merge conflicts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The scripted TEXT ended with a single newline, so TEXT was still asking for
the next line when the test ran undo. Undo then reverted to before the centre
line (taking the uncommitted text with it), and the only post-undo check,
"no Text", passed anyway.
The script now ends TEXT with a blank line (Enter), as an AutoCAD script
would, and the test asserts no command is running afterwards. After undo it
checks that exactly the text was undone and that the four circles, the
outline and the Center-layer line are still there.
Fixes#51.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
`convert` and `run --export` always fitted the model-space extents, so
`zoom`, `pan` or `view.set` in a run had no effect on the image and there
was no way to export a region.
- io: `write_framed(d, name, window)` frames PNG, SVG and PDF on a
model-space rectangle (fitted without margin, centred, no distortion);
`write` is unchanged and still fits the extents. `check_window` rejects
non-finite, inverted, zero-size and out-of-range (±1e12) windows.
- CLI: `run --export` shows the current view when the commands changed
it, otherwise the extents as before. `--window x0,y0,x1,y1` and
`--view extents|current` on `convert` and `run` choose the framing
explicitly. The usage text and README say how images are framed.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Menu items open their missing dialogs; COUNT shows its result; New Layout asks for a name (#126)
Eight menu items ran their JSON form with no parameters and changed nothing.
Typed or chosen from a menu they now do what the menu promises; JSON calls
still run the command and never open a dialog.
- New crates/ui-egui/src/managers.rs: Linetype Manager (load from the
library, set current, global scale, pattern preview), Point Style
(PDMODE tiles drawn in code, size relative/absolute), Table Style and
Multileader Style (list, set current, new, edit properties), Page Setup
Manager (per layout: paper, orientation, plot area, fit/scale, center,
lineweights) and Constraint Settings (infer, bars, transparency, dynamic
constraints, AutoConstrain types and tolerances). All changes go through
their commands (app.run), so they are undoable and scriptable.
- menus::run_ui_command routes LINETYPE/LT, DDPTYPE, TABLESTYLE/TS,
MLEADERSTYLE/MLS, PAGESETUP and CONSTRAINTSETTINGS/CSETTINGS without
parameters to the dialogs; dialogs::show dispatches them.
- COUNT returns a `message` (per-block counts and total) that the command
line shows; Session::start echoes multi-line messages line by line.
- LAYOUT.NEW from the menu or command line prompts
"Enter name of new layout <LayoutN>:"; Enter takes the default, a taken
or invalid name is reported and asked again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Move COUNT test away from other PRs' hunks to avoid merge conflicts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Properties palette: every control acts or is visibly unavailable (#6)
Several Properties palette controls looked clickable but did nothing: most
current-property rows with nothing selected (linetype, lineweight, thickness,
text/dimension/multileader/table style), the selection-type drop-down, a
circle's circumference and area, a line's length and angle, a dimension's
style, and the All/My switch.
- Nothing selected: Linetype runs `linetype {current}`, Lineweight `lweight`,
Thickness `setvar THICKNESS`, the style rows `style.current`,
`dimstyle.current`, `mleaderstyle` and `tablestyle`. Transparency,
Annotation scale and Plot style(s) have no command yet: greyed with a
"Not available yet" tooltip.
- Header: the type drop-down narrows the selection to one object type
(`select {handles}`), plus Toggle PICKADD (`setvar PICKADD`), Select Objects
(`select`) and Quick Select buttons, with icons drawn in code.
- Geometry: circle circumference/area, line length/angle and dimension style
are editable through `properties.set` (new keys `circumference`, `area`,
`dimStyle`); computed values (line deltas, polyline length/area, block
name) are greyed read-only.
- PICKADD now changes clicking with no command running: off, each pick
replaces the selection and Shift adds.
- Palette text fields keep typed text while the window itself is unfocused
(they read focus from egui memory, not `Response::has_focus`).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Move palette tests away from other PRs' hunks to avoid merge conflicts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Edit > Cut / Copy / Copy with Base Point, View > Zoom > Object, Dimension >
Update, Tools > Draw Order > Bring to Front / Send to Back and the Format >
Layer Tools LAYCUR, LAYFRZ, LAYOFF, LAYLCK and LAYULK did nothing when started
from a menu, toolbar or the command line with nothing selected.
- machines::SelectRun: "Select objects:", then run the command's JSON body on
the picked handles. With a pickfirst selection it runs at once, as before.
It replaces the DIMREASSOCIATE/DIMDISASSOCIATE SelectThen in annotate.rs and
calls the command body directly, so the command records one undo step.
- LAYFRZ/LAYOFF/LAYLCK/LAYULK pick one object ("Select an object on the layer
to be frozen" etc.).
- COPYBASE asks for the base point first, then for objects (skipped with a
pickfirst selection).
- Objects picked at the prompt are deselected afterwards.
- JSON/programmatic calls are unchanged and never prompt.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
TAN and PER snaps need the other end of the line, so the first point of a
LINE (no base point yet) could never snap tangent: a belt line tangent to
two circles was impossible to draw.
Now, as in AutoCAD, a point prompt can accept *deferred* snaps:
- snap: with no base and the new `deferred` flag, osnap offers "Deferred
Tangent" (circles, arcs) and "Deferred Perpendicular" (circles, arcs,
lines, xlines, rays) where no other snap wins. The hit carries the curve
(`SnapHit::deferred`, `Deferred`, `SnapCurve`).
- `resolve_deferred` solves the line once the other end is known: tangent
or perpendicular from a point, tangent-to-tangent (common tangents of two
circles, new `Circle::common_tangents` in geom) and the perpendicular
mixes. Among several solutions the one nearest the picks wins. No
solution (point inside the circle, concentric or nested circles,
non-finite input) is `None`, never a panic.
- prompt: `Prompt::deferred()` opts a prompt in; `Input::Deferred` reaches
only such prompts, everywhere else the session turns it into the point it
was picked at, so other commands snap as before.
- LINE opts in for its first point (and the next one while the first is
still deferred). No solution reports and re-prompts; Undo drops the
deferred point. The rubber band shows the resolved line, also the
tangent-to-tangent one while hovering the second curve
(`Session::cursor_deferred`, set by the canvas).
- canvas: passes the prompt's flag to osnap and sends a deferred pick as
`Input::Deferred`.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Point picks found the outer boundary by tracing the planar graph of all
curves, but islands only came from single closed objects (circles, closed
polylines, full ellipses, closed splines). A square drawn as four LINEs
inside a rectangle was ignored: HATCH drew through it, GRADIENT filled it
and BOUNDARY created only the outer polyline.
- geom::region::boundary_at returns the outer loop plus islands: every
connected group of curves inside the face that doesn't touch its
boundary and doesn't surround the pick point. Each island is the
outline of the group's outside, traced with the same sharpest-turn
walk from its leftmost vertex after dangling ends are pruned. Islands
report which input curves they came from. Traces are bounded by the
group's edge count and islands are capped at MAX_ISLANDS.
- loops_at (shared by HATCH, GRADIENT and BOUNDARY) adds those islands
when every curve in the group is an open LINE, ARC, elliptical arc,
open polyline or open spline. Groups containing a closed object keep
the existing per-object islands, so closed-polyline and circle islands
are unchanged (bulges kept).
- Tests: geom region tests for separate-line islands, dangling ends,
open and attached curves, nesting and hostile input; engine tests for
HATCH (rendered pattern stays out of the hole), GRADIENT (375 filled,
hole clear) and BOUNDARY (two polylines) with four-line and
closed-polyline islands.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* DWG open: stop runaway conversions early, name the limit (#57)
"file too large" was the DXF reader's 50,000,000 group-code cap (MAX_TAGS),
not a file-size limit. For a DWG it was only checked at the very end: after
acadrust had read the whole file, written all of its DXF text into one Vec,
the text had been copied again for decoding, and 50 million tags had been
parsed. A real 10 MB DWG is nowhere near the cap (a synthetic 10 MB AC1024
file is 3.7 million group codes and opens in about 2 s). A damaged file, or
objects the DWG reader misreads, can be: acadrust caps every count at 100,000
and reads zeros past the end of the data, so one desynchronised object becomes
a polyline with 100,000 vertices. A fuzzed 0.25 MB AC1024 file made it write
11 GB of DXF.
- cadcraft-dwg: Limits { max_dwg_bytes: 512 MB, max_dxf_bytes: 1 GB }. The DWG
size is checked before the reader runs; the DXF is streamed into a writer
that fails as soon as it passes the limit, so the conversion stops there
instead of filling memory. Both errors give actual and allowed sizes. The
input is no longer copied, and the DXF writer also runs under catch_unwind.
- cadcraft-dxf: TooLarge { tags, limit } says "drawing too large: N DXF group
codes, more than the 50000000 CADCraft reads". For ASCII the count comes
from the line count before any decoding or tag allocation. Valid UTF-8 text
is borrowed instead of copied. MAX_TAGS is public and unchanged.
- cadcraft-io: errors from the DXF converted from a DWG say so.
On the fuzzed file: before, 76 s and 13.4 GB, then the process aborted copying
the 11 GB text (under a 16 GB memory limit). After, 15 s and 5.3 GB, with
"DWG: converting this 0.2 MB file produced more than 1024 MB of drawing data
(the limit); ...". What's left is acadrust's reader itself (8 s, 5.3 GB on that
file), which this bridge can't bound without patching the crate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Move tests away from other PRs' hunks to avoid merge conflicts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Programmatic undo/redo ends a running command first (#177)
`undo` or `redo` run through engine.execute, MCP, the control channel or a
script while an interactive command was still running (e.g. TEXT waiting for
another line) swapped the drawing under that command. Interactive commands
push their undo step only when they end, so `undo` popped the previous
command's step instead: it removed the earlier geometry, dropped the running
command's entities, and left the command running from a stale state.
Both commands now first end the running command the way Esc does, exactly as
Session::start already does for Cmd+Z, the menu and the command line. The
command's work so far becomes one undo step, and `undo` then removes that step.
Regression test: programmatic_undo_during_a_command_ends_it_first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Move test away from other PRs' hunks to avoid merge conflicts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* DXF: write non-ASCII text as \U+ escapes so AC1015 files stay valid (#164)
The writer declares $ACADVER AC1015 (R2000) and $DWGCODEPAGE ANSI_1252, where
text is in the code page, but wrote strings as raw UTF-8. Every other reader
decoded those bytes as CP1252 and turned non-ASCII text into mojibake; only
CADCraft (which tries UTF-8 first) read its own files back correctly.
- cadcraft_dxf::write_ascii now writes pure ASCII: every non-ASCII character
in a string group becomes the pre-R2007 \U+XXXX escape (UTF-16 code units,
so characters above U+FFFF are a surrogate pair of escapes). This covers
TEXT, MTEXT, ATTRIB/ATTDEF, table, block and layer names, xdata and
XRECORD strings alike. The header and the R2000 structure are unchanged,
so the DWG bridge (acadrust, which decodes the escapes) behaves as before.
- The reader combines surrogate-pair escapes, requires four hex digits, keeps
malformed escapes and unpaired surrogates as literal text, and now also
unescapes strings in binary DXF.
- Tests: R2000 save/reopen of CJK, Latin-1 and astral text in TEXT, MTEXT, a
layer name, a block name and ATTRIB/ATTDEF; pure-ASCII output with the
AC1015/ANSI_1252 header; UTF-8 (AC1032) and code-page (AC1015) input still
read; DWG round trip; hostile escapes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Move code and tests away from other PRs' hunks to avoid merge conflicts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
properties.set documented `transparency` but never read it, nothing reported it,
and DXF dropped group 440. Fixes#132.
- properties.set takes "ByLayer", "ByBlock" or a percentage 0..90 (a number or
"50"/"50%"); anything else (out of range, NaN, junk) is an error and leaves
the selection unchanged.
- properties and drawing.inspect / drawing.entities report it in the same form.
- Properties palette: the Transparency row is a menu (ByLayer, ByBlock, a
0..90 percentage) that acts through properties.set.
- DXF: entities write 440 unless ByLayer (ByBlock 0x01000000, a percentage its
alpha | 0x02000000, the alpha mapping layers use); the reader parses it back,
any other value reads as ByLayer.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Render tests: drop the always-true assert in degenerate_input
`assert!(... || true)` trips clippy's overly_complex_bool_expr on current
stable (1.96), which fails `cargo xtask ci` at the clippy step. The check
only proves that NaN input does not panic, so call the function and
discard the result instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* DXF: keep layer transparency and description through save and reopen (#96, #97)
The LAYER writer omitted both properties and the reader left them at
their defaults, so a drawing reopened with every layer opaque and every
description empty. Both are xdata on the LAYER record, as AutoCAD writes
them:
- transparency: AcCmTransparency, 1071 = 0x02000000 | alpha, with
alpha = (100 - percent) * 255 / 100 (40 % is alpha 153); values that
aren't "by alpha" read as opaque
- description: AcAecLayerStandard, two 1000 strings (the layer
standard's name, unused, then the description), cut to the 255 bytes
a 1000 group may hold
Both applications are registered in the APPID table.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
`assert!(... || true)` trips clippy's overly_complex_bool_expr on current
stable (1.96), which fails `cargo xtask ci` at the clippy step. The check
only proves that NaN input does not panic, so call the function and
discard the result instead.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* About: contributors compiled in from contributors.json (grab bag + table, sortable, name toggle)
Replaces the TSV files and `cargo xtask contributors` with the craftrules standard
(standards/contributors.md): contributors/contributors.json is generated by
craftrules/scripts/contributors.py and baked into the binary by build.rs as static tables.
Each contributor line: @username, opt-in display/real name, PRs, commits, lines +/−/Δ,
binary assets +/−, first/last commit. Sortable by every column; alphabetical ignores
case and the leading @.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors docs: craftrules is two levels up (../../craftrules)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors: self-submitted names in contributors/people.toml, read by build.rs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors: regenerate (people_entry verified)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* AGENTS.md: contributor credits section (agents offer to credit their own human only); docs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- Layer Properties Manager: filter tree (All / All Used Layers) with search, sortable table
(Status, Name, On, Freeze, Lock, Plot, Color, Linetype, Lineweight, Transparency,
New VP Freeze, Description; VP Freeze / VP Color inside an MSPACE viewport), inline
rename and description, ACI + true-colour picker, linetype picker with Load from the
library, lineweight picker, layer states save/restore/delete.
- Engine: qselect rewritten ({type, property, operator =,!=,>,<,*, value, mode new|append|
exclude, applyTo}) + qselect.info; layerstate.list/delete/rename; layer list reports
used layers and states; Layer.vp_freeze_new (DXF flag 2, applied by MVIEW) and
layer.set newVpFreeze; per-viewport layer colours (Viewport.layer_colors, vplayer
colors, honoured by the renderer); vplayer defaults to the active viewport; QPMODE
setting/sysvar/toggle; properties.set length/angle for lines.
- Canvas: constraint bars with code-drawn glyphs and coincident markers, dynamic
dimensional constraints (aligned/horizontal/vertical/radius/diameter/angular) with
name=value labels and lock icon, honouring CONSTRAINTBAR/DCDISPLAY; cached per
drawing revision; double-click a label to edit its expression (parameters).
- Quick Properties palette near the selection (status-bar toggle), Quick Select dialog,
Parameters Manager (edit, add user parameter, delete).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- DIMSTYLE: every DimStyle field with its standard group code (DIMBLK/1/2 as
BLOCK_RECORD handles 342/343/344 with DIMSAH, DIMTXSTY 340, DIMDSEP, DIMCLRx, ...);
arrowhead blocks generated from our own geometry when no user block exists, and
not re-imported as user blocks on read.
- Per-dimension overrides as ACAD "DSTYLE" xdata (1070 code + typed value).
- Associativity: standard ACAD_DIMASSOC extension dictionary + DIMASSOC objects
(AcDbOsnapPointRef) for linear/aligned/ordinate extension origins, plus exact
links in CADCRAFT xdata; reader infers links from DIMASSOC for foreign files.
- Constraints/parameters: JSON in an XRECORD under the named-object-dictionary
key CADCRAFT_CONSTRAINTS (250-char chunks, backslashes escaped so no \U+).
- ACAD_TABLE write/read (sizes, cell text, merges) with *T display block,
TABLESTYLE objects and CADCRAFT xdata for title/header/text height/style.
- STYLE: vertical (70), backwards/upside-down (71), AcadAnnotative xdata.
- Reader: owner 330 taken outside 102 groups; hostile-input caps and tests.
- Validated: ezdxf strict readfile + audit (0 errors/0 fixes) and resave;
DXF->DWG->DXF via acadrust keeps all of it except TABLESTYLE objects.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A pick after one edit at 200k entities drops from ~110 ms (full rebuild) to ~4 ms;
steady-state picks stay at ~1 us. Adds a parity test against fresh builds and a
perf row for the incremental path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Annotation (M4):
- TrueType text from system fonts (filled/outlined), shared placement for all text
- MTEXT formatting codes: fonts, heights, colours, under/overline, stacks, wrapping
- Full DIMSTYLE variables, per-dimension overrides, all arrowheads and ticks,
unit formats, tolerances, limits, alternate units, colours by role
- Associative dimensions that follow geometry; DIMREASSOCIATE/DIMDISASSOCIATE
- STYLE/DIMSTYLE management commands, DIMTEDIT, DIMSPACE, TABLE create/edit/merge
Parametric (M9):
- New cadcraft-constraints crate: weighted damped Gauss-Newton solver, conflict
and redundancy detection, expressions, AutoConstrain inference
- All GC*/DC* commands, GEOMCONSTRAINT/DIMCONSTRAINT, PARAMETERS, DELCONSTRAINT,
constraint bar / display settings; re-solve after every edit
Also: MODEL/PAPER status bar toggle, cargo xtask alias, roadmap update
(233/491 menu items, 290 commands).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Double-click a viewport (or MS) to edit model space inside it; the screen view maps
consistently so picks, snaps, grips and previews work in model units, and zoom/pan
change the viewport's view (or the sheet when the viewport is locked). PS or a
double-click outside returns to paper space.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Workflows and packaging adapted from the craftrules playbook (release environment with the twelve
signing secrets, new WiX upgrade code, .dxf/.dwg file associations), app icon set (hicolor, icns,
ico), docs/releasing.md. MTEXT now opens a text editor window for its contents.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DIMLINEAR/ALIGNED/RADIUS/DIAMETER/ANGULAR/ARC/ORDINATE/CONTINUE/BASELINE, QDIM, MLEADER, LEADER;
HATCH/GRADIENT/BOUNDARY/REGION with pick-point boundary tracing (planar face walk with islands);
BLOCK/INSERT/ATTDEF/ATTEDIT/WBLOCK/PURGE/BATTMAN; dimension text gaps and horizontal text;
DXF header writer whitelist (correct group codes).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>