Commit Graph
63 Commits
Author SHA1 Message Date
dotCooCoo 1246c62095 DXF: paths of lines and circular arcs export as polylines with bulges
A path made of straight segments and circular arcs (a slot, a D shape,
a rounded rectangle, a pie) was written as one SPLINE, or as a flattened
polyline in R12. It is now one polyline whose vertices carry a bulge
(group 42) for each arc: an LWPOLYLINE in R14 and later, a POLYLINE with
VERTEX entities in R12 and R13, closed when the path is closed. Each run
of cubics on one circle becomes one arc. A path with any other curve
stays a SPLINE, and whole circles, arcs and ellipses keep their CIRCLE,
ARC and ELLIPSE entities.

- The circle test's tolerance is 5e-4 of the smaller of the radius and
  the curve's extent, so a shallow curve that is not circular is no
  longer written as an ARC.
- The drawing extents take each ARC's and each bulge's furthest points.

Part of #1299
2026-11-04 08:12:52 +07:00
Dylan M. Taylor ac391b0f85 DXF import: charge every dash copy and symbol name, name the block-work limit
Follow-up to #5288. A dashed compound path keeps its dash pattern on the compound node and on each loop, so the dash budget is charged for every copy. A symbol's name is charged against the text budget before the symbol is kept. Running out of block work now says so instead of reporting more than 2,000,000 objects.
2026-11-03 22:52:28 +07:00
bflatastic a9ce928f06 Format with rustfmt 2026-11-03 16:03:38 +07:00
Dylan M. Taylor cd1afaf1cf DXF import: budget dash lengths, copied text and names, and block items looked at
A drawing could make the importer keep far more than its own size: every dashed path held its
own copy of its linetype, every text object and symbol instance of an array copied its text,
font family or block name, and each placement of a block looked at all of its items, hidden or
not, without charging the work budget.

- Dash lengths kept across the import are capped; past the cap lines are drawn solid.
- Text, font family and block-name bytes copied into objects share one 64 MB budget, and a
  font family longer than 256 bytes falls back to the default.
- Each block placement charges one visit per item it looks at, and stops once the budget is spent.
2026-11-03 16:03:38 +07:00
Dylan M. Taylor ed4a663986 DXF export: charge symbol art as it is placed unclipped, test the segment budget
Count nine-slice symbol art with clips off, as place_symbol_art_unclipped
places it, so art crossing the guides is not over-charged; add a test that
the flattening segment budget stops a path independently of the point
budget; format.
2026-11-02 22:33:39 -05:00
Dylan M. Taylor f1d4969864 DXF export: bound curve flattening work and expanded symbol art
Flattening a path for DXF capped the points it kept but kept flattening every remaining curve
after the cap, each curve at a fixed tolerance however large it was. Symbol instances placed
inside symbols were cloned and walked once per instance with only the nesting depth bounded, so
a small symbol graph of empty symbols could fan out into billions of visits that write nothing.

- Flatten through vectorcraft_geom::flatten (a bounded number of segments per curve) and stop
  at the first curve past the point or segment budget.
- Charge each symbol instance's art to MAX_SYMBOL_NODES with charge_symbol_art before placing
  it, as the document does elsewhere; once spent, no further instances are expanded.
2026-11-02 20:43:17 -05:00
dotCooCoo 83f2bfe9d6 Symbols: a scaled instance of a symbol with 9-slice guides keeps its corners and edges unstretched
Squashed from #5079 onto main.
2026-11-02 15:22:07 +07:00
bflatastic 87979644cd DXF: unicode_escape gets its own doc comment, above escapes' 2026-11-02 14:36:14 +07:00
Max MittelstädtandGPT-5.6-Luna 9b93c609b9 Decode DXF surrogate pairs
Found while testing crates/cad/src/import/text.rs:80

Co-Authored-By: GPT-5.6-Luna <noreply@openai.com>
Co-Authored-By: GPT-6-Luna <noreply@openai.com>
2026-11-02 14:36:14 +07:00
dotCooCoo 427bce12b0 Character: Font Size can measure the cap height, x-height or ICF box
Squashed from #5026 onto main.
2026-11-01 11:08:55 +07:00
bflatastic f22caa6108 Fix #4938: Dynamic symbols keep per-instance appearance linked to the symbol 2026-11-01 04:53:38 +07:00
Don Omondi 22b1c6bc2e Fix #3085: DXF LAYOUT objects write only the group codes the DXF Reference defines
The PLOTSETTINGS part of each LAYOUT wrote the paper image origin as a point
(148 and 158), but the origin is two values at 148 (x) and 149 (y), and 158 is
not a PLOTSETTINGS code. Strict readers such as Autodesk Fusion can reject the
file. The ezdxf re-save the reporter found Fusion accepts differs from our export in
these objects, not in the geometry.

- 148/149 for the paper image origin
- shade plot mode, resolution level and DPI (76/77/78) from DXF 2004
- plot flag 1024 (model type) on the Model layout
- the plot style ACDBPLACEHOLDER without a subclass marker

Test r2000_and_later_files_have_the_structure_cad_apps_require checks the
required sections and table entries, handles below $HANDSEED, references
that resolve, and LAYOUT group codes.

# Conflicts:
#	docs/file-format-parity.md
2026-10-31 19:43:17 +07:00
dotCooCoo 958566468a DXF export: dashed polylines continue their linetype around vertices
A polyline with a linetype now sets group 70 bit 128, so the dash pattern runs on through its vertices instead of restarting at each corner. This applies to LWPOLYLINE and the R12 POLYLINE.
2026-10-31 19:43:15 +07:00
dotCooCoo d318a77560 DXF export: Alter Paths for Appearance outlines only strokes a CAD line can't draw
Alter Paths for Appearance wrote every stroke as its filled outline, so a
1 pt circle became two circles and a hatch, and an open arc became a
closed outline and a hatch: two cut lines where a CAM program expects
one. The option now outlines only the strokes a line can't draw: the ones
Preserve Appearance outlines, strokes wider than the widest lineweight
(2.11 mm, to the hundredth), and every stroke in R13 and R14, which have
no lineweights. Other strokes stay CIRCLE, ARC, SPLINE and LWPOLYLINE
entities with their lineweights; R12 keeps them as POLYLINEs with their
width.

A dashed stroke whose offset starts part way into its pattern is now
outlined with Preserve Appearance and with Alter Paths: a linetype always
starts at the start of its pattern, so the dashes were written in the
wrong place.
2026-10-31 19:43:15 +07:00
dotCooCoo d0a903d5d7 DXF export: ellipses and unevenly scaled circles are written as ELLIPSE
A closed path of four cubics that is the four-cubic circle under an
affine map (an ellipse, or a circle scaled out of proportion) is written
as a native ELLIPSE in R13 and later, with its center, major axis and
minor to major ratio. The check maps the path back onto the unit circle
and runs the existing circle recognition on it. R12 keeps the existing
fallback, and ellipses with a ratio under 1e-6 stay splines. The ratio
and end parameter are written in full so that a thin ellipse keeps its
size and a whole turn reads back closed.

Part of #1299.
2026-10-31 19:43:15 +07:00
dotCooCoo 06e8d711ae DXF export: an open path of one straight segment is written as a LINE
An open path with a single straight segment was written as a two-vertex
LWPOLYLINE (POLYLINE before R14). It is now a LINE entity from its start
to its end point. A stroke with a weight in R12, R13 and R14 keeps its
polyline, since a LINE has no width. Closed paths and paths with more
than one segment are unchanged.

Part of #1299.
2026-10-31 19:43:15 +07:00
el-j c0d7d22d42 Fix #4151: verify continuous angular direction for cubic arc conversion (#4151) 2026-10-29 09:57:34 +07:00
el-j a24326e046 fix(cad): verify circular subpaths complete a full turn without reversing (#4151) 2026-10-29 09:57:34 +07:00
el-j 7f48664fed test(cad): add regression test for closed sliver along a circle in DXF (#4151) 2026-10-29 09:57:34 +07:00
Max MittelstädtandGPT-6-Luna c714797e99 Reject fractional DXF integer values
Found while testing crates/cad/src/import/reader.rs:69

Co-Authored-By: GPT-6-Luna <noreply@openai.com>
2026-10-29 08:45:52 +07:00
NetterSilver 472cd812f9 Keep escaped backslash literal before U+XXXX in DXF MTEXT
Squashed from #4000 onto main.
2026-10-24 20:54:42 +07:00
NetterSilverandClaude Haiku 5.5 41c26c6997 Keep the denominator of DXF MTEXT stacked fractions written with ^
Found while testing crates/cad/src/import/text.rs:14-27 (mtext calls escapes at :14, which treats `^2` as a caret escape and drops it; the `\S` branch at :25 expects the `^`)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 20:45:46 +07:00
NetterSilverandClaude Haiku 5.5 2a4aafe346 Require four hex digits in DXF text \U+XXXX escape
Found while testing crates/cad/src/import/text.rs:73-74 (escapes)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 19:02:09 +07:00
dotCooCoo bc041e15ef Type: the Numbered List button numbers paragraphs 1, 2, 3
Squashed from #3960 onto main.
2026-10-24 19:02:08 +07:00
Dylan M. TaylorandClaude Sonnet 5.5 a7910edd64 Review fixes: keep later CAD layouts, ordered-stop gradient sampling, skip full-stroke previews, cache shift line
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 17:19:59 +07:00
Dylan M. TaylorandClaude Opus 5.5 20bba9ebae Security: linear work for shortcut review, gradients, recovery names, include cycles, batch references, DOCX style ids, CAD layouts and long strokes
CodeRabbit findings #2, #16, #31, #103, #114, #124, #162, #179, #182, #183, #185:

- Shortcut import review: example names for each code in one pass over the rows.
- Gradients: a midpoint stop is mixed from its two stops; freeform recolor samples its
  increasing positions with one advancing search (`Gradient::sample_increasing_with`).
- Recovery: image entry names are looked up by address, and dead ones swept when the map
  doubles, not on every image.
- Script checker: include cycles from strongly connected components (iterative Tarjan), not
  a reachability row per file.
- Batch step references copy at most 256 MB of values for one step.
- DOCX: style ids over 256 bytes are ignored where defined and where used.
- DXF: layouts keep at most 4 MB of names; a hatch's loops count against the node limit.
- Freehand and Liquify: past 1000 samples a drag previews every n/1000 samples, strokes keep
  at most 100,000 samples, and the stroke kept is previewed in full before it is committed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 17:19:59 +07:00
bflatastic a82bcfe7a5 DXF export: format the far-coordinate check 2026-10-24 04:49:07 +07:00
bflatastic 62333a2128 DXF export: the far-coordinate check compares with partial_cmp (also refuses NaN; clippy) 2026-10-24 04:49:07 +07:00
bflatastic 711b10ca59 Security: DXF export bounds (layer and style names, flattening, entity count, image size) and import dash patterns
Squashed from #3886 onto main.
2026-10-24 04:49:07 +07:00
bflatastic c2ad9f8bbb Type: emoji go with type that effects reshape and Expand Appearance
Squashed from #3879 onto main.
2026-10-24 04:49:06 +07:00
dotCooCoo 2a2e63856c Type: the Paragraph panel's Bullets button turns paragraphs into a bulleted list
VectorCraft had no lists. A bullet could only be typed as a character,
and the tab after it did not stop at the left indent. The reference app
turns paragraphs into a bulleted list from the Paragraph panel, the
context menu and Type › Bullets and Numbering › Apply Bullets.

text.setFormat takes list ("none" or "bullet") and bullet ("none",
"dot", "dash", "circle" or "square"). The layout draws the bullet before
the first line of each list paragraph that has text, in the font, size
and color of its first character, and starts the text at the next tab
stop, with the left indent counted as a stop. Wrapped lines start at
the left indent, and type on a path draws the bullet too. In a
horizontal right-to-left list paragraph the indents are mirrored: the
bullet hangs at the right, and the wrapped lines start at the right
where the first line's text starts after the bullet. The bullet is not
a character of the text. A paragraph whose left and first-line indents
are both 0 gets 18 pt and -18 pt when it joins a list. Return continues
the list, paragraph styles carry it, Reset Panel turns it off, and the
native file saves it only when it is set.

The Paragraph panel has a Bullets button with a list of the five
presets, and Type › Bullets and Numbering › Apply Bullets and the
context menu run type.applyBullets. Live SVG text writes the bullet's
character, and SVG writes type on a path with bullets as outlines. DXF
writes the bullet as a TEXT entity of its own, and legacy .ai export
warns that bulleted lists are left out. The caret, selections, Touch
Type and Show Hidden Characters skip the bullet.

Live SVG text of horizontally scaled type now writes each tab in a
tspan of its own. Before, the characters before a tab shared its tspan,
and its textLength made a viewer stretch them across the tab's gap.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-24 03:09:11 +07:00
bflatastic 07762956d2 Cap path elements in DXF import across the whole file
Squashed from #3800 onto main.
2026-10-24 01:43:58 +07:00
NetterSilverandClaude Haiku 5.5 609132987b Bound DXF import work for nested inserts that produce no nodes
Fixes #3572

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 01:43:58 +07:00
bflatastic 5455e9be07 Cap the total array insert copies in a DXF import
Squashed from #3789 onto main.
2026-10-24 01:43:57 +07:00
NetterSilverandClaude Haiku 5.5 23bc1c0027 Fix quadratic paper layout de-duplication in DXF import
Fixes #3595

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 01:43:56 +07:00
NetterSilverandClaude Opus 5.5 fb53e53eaf Stop tiny DXF spline weights from splitting every span to the cap
Fixes #3588

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 11:59:46 +07:00
NetterSilverandClaude Haiku 5.5 77d1f8901f Stop DXF hatch source-handle loop at first missing handle
Fixes #3590

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 11:38:06 +07:00
NetterSilverandClaude Haiku 5.5 b16f44d87f Make DXF import symbol-name uniquing linear
Fixes #3589

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 11:20:15 +07:00
dotCooCoo 4e4723699d Export: EPS, legacy .ai, EMF/WMF and DXF give one note for images that can't be decoded
EPS, EMF, WMF and DXF exports gave each image they left out because it
couldn't be decoded its own warning, "image '<key>' could not be
decoded and was left out", which names the internal key the document
stores the image under. A version 7 .ai said "an image that can't be
decoded or is too large was left out".

The four writers now give the note raster exports, PDF and the legacy
.ai flattening give, "an image couldn't be decoded and is missing from
the picture", once per export. In a version 7 .ai it is one line with
the note for flattened art. The other image notes in the same
functions no longer name the key: EPS's note for an image too large to
write, and DXF's notes for an image with no pixels (its rows in 23
catalogs are reworded) and one that could not be written. Legacy .ai
gives a separate note for an image that is empty or too large.

Docs: docs/mcp.md (EPS, DXF export, EMF and WMF, aiVersion "7").
2026-10-22 06:13:32 +07:00
dotCooCoo 68d8a9c755 Export: area type that Shrink Text to Fit scales down is written at the size it draws at
Shrink Text to Fit scales area type down at layout time, and the canvas,
PDF export and the exports that write type as outlines draw it at that
size. SVG export wrote the live text at its stored font size, letter
spacing and baseline shift, so a viewer drew it larger than the canvas
does. Legacy .ai saves wrote the stored size, leading and baseline shift
into the text objects, DXF export took the TEXT heights from the stored
size, and the CSS Properties panel gave the stored font size, letter
spacing and line height.

The text crate's scaled, which the layout scales shrunk text with, is
now public. SVG export, legacy .ai saves and the CSS Properties panel
take the type's styles from it at the layout's fit scale, and DXF
export multiplies the TEXT heights by that scale. Positions and
textLength in SVG still come from the layout.

Docs: docs/target-app-parity.md.
2026-10-21 16:55:16 +07:00
Mirko Kerezovic af48dda322 M4: address DXF circle export review (#1299) 2026-10-19 23:34:20 +07:00
Mirko Kerezovic b87cb157ed M4: export DXF circles and arcs as native entities (#1299) 2026-10-19 23:34:19 +07:00
FredApps 0e97cf1d4b test(cad): round-trip closed DXF SPLINE flags through export and import (#1299)
#1320 (write) and #1318 (read) landed on main; this keeps only the missing
regression: explicitly closed control-point splines export with the closed
bit and re-import closed, while open paths (even with coincident endpoints)
stay open, across every SPLINE-capable DXF version.
2026-10-16 15:27:55 +07:00
Gert Pellin 74c391c7ba G13: Object › Intertwine: Make, Release and arranging overlap areas (#1763)
An Intertwine group keeps its objects as they are, in their own stacking
order; each member can be drawn over the others in an area of its own.
The group draws, hits, saves and exports through its live expansion, so the
members stay editable and Release gives them back unchanged.

object.intertwine.arrange picks an overlap area by a click point or by a
closed path drawn around overlaps and toggles which member is on top
there, or brings a member to the front or sends it to the back where three
or more objects overlap.
2026-10-16 04:44:52 +07:00
bflatastic 0efcaeb558 Importers: a brush that draws itself by name no longer crashes
A scatter brush whose art is an instance of a symbol stroked with that brush, or any brush whose
art is painted with a pattern whose tile is stroked with it, drew itself without end: a stack
overflow in a hand-written native file. Brush pieces already strip the brushes of their own
strokes; now what they draw by name that would draw a brushed stroke (a symbol instance, or a
pattern paint, also through the symbols and patterns those use) is left out too, in one place
(`stroke_pieces`, which every renderer and exporter calls with the document).

Probed the rest of item 2 of the depth follow-up with hand-written hostile inputs, kept as tests:
graph designs of deep art and of a symbol holding itself, Affinity groups nested to and past
both of its reader's caps, DXF blocks inserting themselves, EMF/WMF saving states without end,
and plug-in manifests and output nested 30,000 to 100,000 deep. None of them overflowed.
2026-10-14 10:13:45 +07:00
bflatastic c10da25ce4 Importers: nesting is capped, so a deeply nested file can't overflow the stack 2026-10-14 07:37:52 +07:00
bflatastic a57aea8e94 DXF export: leave out the centerline option and the 3ds Max preset for now
The VPORT view direction fix and its test stay. The Export Strokes as
Centerlines option and the preset button are not in the documented DXF/DWG
export options, so they wait for the owner's decision.
2026-10-14 05:16:15 +07:00
Osama Amin 4a68f84282 DXF export: strokes as zero-width centerlines, a 3ds Max R12 linework preset, and one view direction in VPORT 2026-10-14 05:16:09 +07:00
dotCooCoo d9bd9b134d Symbols: instances of imported symbols are measured by their art
Every symbol instance was measured as its transform applied to the
20 × 20 square around the origin, which symbols made in the app keep
their art in. The SVG and DXF importers keep symbol art as drawn, so
the box of an instance of an imported symbol did not match its art.
The SVG Star instance at x 20, y 30 draws from (20, 30) to (40, 50)
and was measured from (10, 20) to (30, 40). Align moved such an
instance by the wrong amount, clicks and marquees beside its art
selected it, and the canvas, exports and prints left it out when its
art reached the view or the page and the square did not.

An instance is now measured by its symbol's art, which
Document::symbol_boxes looks up in the document each time
(Node::geometric_bounds_with, visual_bounds_with and
reach_bounds_with). With Use Preview Bounds on, the strokes in the art
count at their own weight, as the canvas draws them, and
object.setBounds counts them at that weight when it sizes an instance
or an object that holds one. When strokes do not scale and a width or
height of 0 or more is smaller than what a path's stroke adds,
object.setBounds now gives the path no width or height, as it does
for an instance whose art has strokes; before, it mirrored the path.
With Use Preview Bounds on, object.setBounds now measures the margins
that strokes add object by object, the members of groups and the
objects in symbol instances' art included, and solves for the width,
height and reference point asked for when the width and height are 0
or more. Before, it measured the margins of the whole selection and
could miss the size or the reference point asked for when strokes did
not scale and the objects' strokes added different margins, and when
strokes scaled and the width and height were out of proportion, even
for one path. A clip group, a Shaper Group, a compound path, type, and
a group or instance with effects or an opacity mask of its own are each
measured as one object, and so is an instance in perspective or one
turned or slanted relative to the bounding box, other than by quarter
turns. When the selection holds more than 4,096 objects to measure this
way, each selected object is measured as one. Instances inside symbol
art are measured by their own symbol's art. An instance is measured as
the square when its symbol is missing, when its symbol or a symbol
nested in its art holds an instance of itself, and when symbols nest 32
deep in its art.

Align and Distribute, the Transform panel, the bounding box and the
selection outline, clicks, the marquee, document.inspect and
document.node, Fit to Artwork Bounds and Fit to Selected Art, canvas
culling, thumbnails, the SVG filter region of an instance with a
raster effect, raster effect images, Export Selection, the SVG of an
artboard, printing, and the PDF, EPS, EMF/WMF and DXF writers use the
new box. The renderer also culls an instance by the effects and
brushed strokes in its art, and it culls a blend, envelope or repeat
by the brush art of what it holds and the effects in the art of the
instances in it. It culls an instance with geometry effects of its own
by the effects in its art after those geometry effects reshape the art.
It measures the effects in a symbol's art once for each symbol and
linear part of the transform that places it. For symbols whose art
holds instances, it measures at most 4,096 placements a frame, and at
most 4,096 more for each pattern tile it rasterizes and each placed
document it draws. Past that, it estimates the reach from the symbol's
reach with no transform, which it measures once. When the drawn
document's symbols change, the renderer drops the cached bounds of the
instances and of the objects that hold them.

The PDF and EPS writers, the SVG of an artboard and Skip Blank
Artboards grow the box of an instance whose art holds brushed strokes
by 16 times the widest stroke (at least 1 pt) of the objects in that
art that have a brushed stroke, plus 8 pt, as the renderer grows the
cull bounds of a brushed path. They keep an instance whose brush art
reaches the page. The SVG of an artboard also keeps an object that
holds an instance when what the canvas paints for it, the effects in
the symbol's art and the instance's own included, reaches the artboard.

Not yet: other commands that measure objects, such as Select › All on
Active Artboard, Transform Each and Convert to Artboards, still measure
an instance by the square.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-14 04:56:30 +07:00
Ikki Nishihara 0d244440a9 fix(cad): a closed control-point DXF SPLINE is read as closed (#1299) 2026-10-12 15:16:43 +07:00