Arrays are associative by default since #455, so arrayrect makes one
block reference; the test wants many separate objects.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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>