diff --git a/crates/cad/src/scene.rs b/crates/cad/src/scene.rs index 300407eed6..7c5d8b6755 100644 --- a/crates/cad/src/scene.rs +++ b/crates/cad/src/scene.rs @@ -342,6 +342,32 @@ fn subpaths(bp: &BezPath) -> Vec { out } +/// `bp` with each open subpath that can enclose an area closed, as a fill closes it: two or more +/// segments, or one curve. A single straight segment stays open. +fn closed_fill(bp: &BezPath) -> BezPath { + let mut out = BezPath::new(); + let (mut segs, mut curved) = (0usize, false); + let close = |out: &mut BezPath, segs: usize, curved: bool| { + if segs >= 2 || (segs == 1 && curved) { + out.close_path(); + } + }; + for el in bp.elements() { + match *el { + PathEl::MoveTo(_) => { + close(&mut out, segs, curved); + (segs, curved) = (0, false); + } + PathEl::ClosePath => (segs, curved) = (0, false), + PathEl::LineTo(_) => segs += 1, + PathEl::QuadTo(..) | PathEl::CurveTo(..) => (segs, curved) = (segs + 1, true), + } + out.push(*el); + } + close(&mut out, segs, curved); + out +} + /// The most pixels of an image written (it is held decoded, as RGBA and encoded at once). const MAX_IMAGE_PIXELS: u64 = 64 << 20; @@ -887,7 +913,7 @@ impl<'a> Scene<'a> { fn area(&mut self, bp: &BezPath, rule: FillRule, style: &EntityStyle, clean: bool) { if !self.o.version.handles() { self.warn(NO_FILLS); - self.lines(bp, style, 0.0); + self.lines(&closed_fill(bp), style, 0.0); return; } let several = bp.elements().iter().filter(|e| matches!(e, PathEl::MoveTo(_))).count() > 1; @@ -901,7 +927,8 @@ impl<'a> Scene<'a> { if loops.is_empty() { return; } - self.lines(outline, style, 0.0); + // The outlines close as the fill does, so an open path's fill keeps its closing edge. + self.lines(&closed_fill(outline), style, 0.0); if !self.o.include_hatches { self.warn(HATCHES_OFF); return; diff --git a/crates/cad/src/tests.rs b/crates/cad/src/tests.rs index 2ffa594f44..9d2a519e1d 100644 --- a/crates/cad/src/tests.rs +++ b/crates/cad/src/tests.rs @@ -925,6 +925,30 @@ fn fills_without_hatches_keep_their_outlines() { assert!(!off.warnings.iter().any(|w| w == HATCHES_OFF)); } +#[test] +fn an_open_path_s_fill_outline_keeps_its_closing_edge() { + let mut bp = vectorcraft_geom::BezPath::new(); + bp.move_to((10.0, 10.0)); + bp.line_to((90.0, 10.0)); + bp.line_to((50.0, 80.0)); + let open = PathData::from_bezpath(&bp); + let d = doc_with(|d| vec![path(d, open.clone(), Paint::solid(Color::BLACK), Paint::solid(Color::BLACK), 1.0)]); + let closed = |e: &Pairs| get(e, 70).and_then(|v| v.trim().parse::().ok()).is_some_and(|v| v & 1 == 1); + for include_hatches in [false, true] { + let out = export(&d, &DxfOptions { include_hatches, ..opts(&d) }).unwrap(); + let e: Vec = entities(&out).into_iter().filter(|x| kind(x) == "LWPOLYLINE").collect(); + assert_eq!(e.len(), 2, "the fill's outline and the stroke: {:?}", kinds(&out)); + assert!(closed(&e[0]), "the fill's outline closes as the fill does (hatches {include_hatches})"); + assert!(!closed(&e[1]), "the stroke stays open (hatches {include_hatches})"); + } + // R12 writes the outline alone, closed too. + let fill_only = doc_with(|d| vec![path(d, open.clone(), Paint::solid(Color::BLACK), Paint::None, 0.0)]); + let r12 = export(&fill_only, &DxfOptions { version: DxfVersion::R12, ..opts(&fill_only) }).unwrap(); + let e = entities(&r12); + assert_eq!(kind(&e[0]), "POLYLINE", "{:?}", kinds(&r12)); + assert!(closed(&e[0]), "R12's outline closes as the fill does"); +} + #[test] fn images_can_be_left_out_with_their_files() { let d = image_and_fill_doc(Paint::None); diff --git a/docs/mcp.md b/docs/mcp.md index ac3e1feec1..381c177075 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -5862,7 +5862,7 @@ DXF layer (hidden layers switched off, locked ones locked, non-printing ones not straight paths become polylines (an open path of one straight segment becomes a line, except for a stroke weight before 2000), curved ones cubic splines through every anchor, fills their outlines followed by solid hatches (laser and cutter software reads the outlines and skips hatches; R12 has no hatches: the outlines -alone), strokes lines with their lineweight and a linetype per dash pattern, and placed images image entities linked +alone; an open path's fill outline is closed, as the fill is), strokes lines with their lineweight and a linetype per dash pattern, and placed images image entities linked to PNG or JPEG files (`rasterFormat`) written next to the drawing (`linked` in the result). `includeImages: false` leaves placed images out, with no image entity, no linked file and no decoding of their pixels, and `includeHatches: false` writes each filled area as its outlines alone, without its solid hatch (both default to `true`; these two diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 4903c82f91..0b3c7b03f8 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1501,7 +1501,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - EPS and PostScript import reads image data through `CCITTFaxDecode`: Group 4 (ITU-T T.6) and Group 3 one-dimensional and mixed (ITU-T T.4) data, with the filter's `K`, `Columns`, `Rows`, `EndOfBlock`, `EndOfLine`, `EncodedByteAlign` and `BlackIs1` parameters, decodes to 1-bit rows for `image` and `imagemask`. Damaged data keeps the rows decoded before it (damage in the first row is a PostScript `ioerror`), and parameters out of range or past the 256 MiB limit on decoded data are refused (`crates/eps/src/import/tests_images.rs`). Reading such a file stopped at an error naming the filter, and the file opened from a fallback, such as its preview or the art drawn before the image, or did not open. - EPS and PostScript `.ai` import undoes the TIFF predictor (`/Predictor 2`) of `FlateDecode` and `LZWDecode` data at 1, 2, 4 and 16 bits a component as well as 8: each component adds the same component of the pixel to its left within its bit width, 16-bit components add as big-endian numbers, and each row starts again (ISO 32000-1 §7.4.4.4; `crates/eps/src/import/tests_images.rs`, `crates/engine/src/cmd/fileio/tests_epsimport.rs`). At those depths the image drew the stored differences instead of its samples. - DXF control-point SPLINE import honors its closed flag (#1299 follow-up after #1320 fixed export), including rational curves; open curves whose endpoints meet stay open. Own-fixture regressions cover controls/knots, fit-point fallback and all export versions (`crates/cad/src/import/tests.rs`, `crates/cad/src/tests.rs`); native save/read checked. Circular cubic paths export as native CIRCLE/ARC where stroke semantics permit; R12 weighted strokes remain POLYLINE, and a non-circular ellipse keeps the existing fallback (`crates/cad/src/tests.rs`); an open path of one straight segment exports as a LINE (R12 to R14 strokes with a weight stay polylines, which carry the width) and reads back as the same segment (`crates/cad/src/tests.rs`, `crates/cad/src/import/tests.rs`); DXF Options' Preset menu and `document.exportDxf`'s `dxfPreset: "cnc"` set the CNC / CAM options (version 2000, 1 mm = 1 unit, 256 colors, Maximize Editability with Alter Paths off, Outline Text, placed images and fill hatches left out), and the menu shows Custom once a field changes (`the_cnc_preset_writes_version_2000_in_millimeters_with_type_as_paths_and_strokes_as_lines`, `the_cnc_preset_sets_the_options_that_are_not_given`, `the_cnc_cam_preset_sets_the_fields_and_a_change_shows_custom`); VCarve QA remains pending. - - DXF export's Include Placed Images and Include Fill Hatches (`includeImages`, `includeHatches`; VectorCraft's own options, on by default and off in CNC / CAM) leave out placed images with their linked files, without decoding them, and the solid hatch after each filled area's outlines, each on its own, with one note in `warnings` when something was left out; R12 keeps its own notes (`fills_without_hatches_keep_their_outlines`, `images_can_be_left_out_with_their_files`, `images_and_hatches_are_left_out_independently_and_the_cnc_preset_leaves_out_both`, `placed_images_and_fill_hatches_can_be_left_out`, `include_placed_images_and_fill_hatches_follow_the_preset_and_a_change_shows_custom`). + - DXF export's Include Placed Images and Include Fill Hatches (`includeImages`, `includeHatches`; VectorCraft's own options, on by default and off in CNC / CAM) leave out placed images with their linked files, without decoding them, and the solid hatch after each filled area's outlines, each on its own, with one note in `warnings` when something was left out; R12 keeps its own notes. An open path's fill outline is closed, as the fill is, in every version (`an_open_path_s_fill_outline_keeps_its_closing_edge`, `fills_without_hatches_keep_their_outlines`, `images_can_be_left_out_with_their_files`, `images_and_hatches_are_left_out_independently_and_the_cnc_preset_leaves_out_both`, `placed_images_and_fill_hatches_can_be_left_out`, `include_placed_images_and_fill_hatches_follow_the_preset_and_a_change_shows_custom`). - DXF export writes a path of straight segments and circular arcs (a slot, a D shape, a rounded rectangle, a pie) as 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, and it reads back as the same outline (#1299, `crates/cad/src/tests.rs`). Such a path was written as one SPLINE (a flattened polyline in R12); a path with any other curve still is. A shallow curve becomes an ARC or a bulge only when it lies on the circle within 5e-4 of its own extent as well as of the radius, and the drawing extents (`$EXTMIN`, `$EXTMAX`) reach the furthest points of every ARC and bulge. - Affinity documents in (AF.1–AF.4): `.af` (Affinity 3) and `.afdesign`/`.afphoto`/`.afpub` (Affinity 1 and 2, container versions 8–12) open and place as editable art: layers and sublayers, groups (pass-through or isolated), artboards (with their names and backgrounds) and Publisher pages side by side, curves with live corners, parametric shapes (rectangles with corner radii, ellipses, polygons, stars, pies, triangles, trapezoids), compound shapes, solid/linear/elliptical gradient fills and strokes (weight, dashes, alignment, scale-with-object), clipping by nesting, vector and pixel masks, artistic and frame text with fonts, sizes, tracking, leading and alignment, placed images (their original JPEG/PNG) and pixel layers (8/16-bit RGB, CMYK), opacity, visibility, locking and blend modes, at the document's resolution in points. Effects, adjustments, brushes and pressure profiles, transparency gradients, master pages, symbol instances (imported as their cached picture), special shapes (cloud, heart, cog…, imported as ellipses) and Affinity-only blend modes are named in the import warning. A file whose native data can't be read opens as its embedded preview with the reason, and that preview can't be placed. Save never writes back over the Affinity file and there is no Affinity export. AF.5 (from DesignCraft's import, storytold/designcraft#291) adds master pages (on a locked layer behind the pages that show them), tables (area type per cell and a line per edge), each paragraph's alignment, indents, spacing and leading, horizontal text scale, outlined text, frame text sized by its text scale, drop shadows and outer glows as live effects (inner shadows as inner glows), outline effects as strokes, colour and gradient overlays baked into the paint, embedded Affinity documents as editable content (their artboards don't become artboards), pixel layers over 64 MP at a reduced resolution and HEIC photos from their stored pixels; automatic leading (size plus the font's line gap) fitted on thumbnails; underlines and strikethrough; strokes whose style is None (every shape stores one) left out, which also brings 6 public files closer to their thumbnails; line breaks inside a paragraph; table cell text centred as Affinity centres it, also where it is taller than its cell; CMYK pixel layers converted through the document's embedded CMYK profile (relative colorimetric with black-point compensation, as each layer's colour settings name), or kept as CMYK images when there is none; outline effects on stroked shapes around their stroke; artistic text whose paragraphs align differently aligned within its longest line, a frame's first baseline at cap height, and frames whose last lines' descenders hang out made tall enough to show them (all measured on a local document's own PDF export: baselines match to 0.1 pt); a local 56-artboard document's mean thumbnail difference went from 16.2 to 11.4, and its pages' mean difference from the owner's PDF export from 12.9 to 9.3. AF.4 adds Affinity 3 artboard recognition and actual-outline clipping, source-backed JPEG/PNG raster tiles, Unicode text-run recovery, stored fallback/exact installed fonts, OpenType feature flags and compound-geometry limits. 51 public CC0/MIT/Apache-2.0 files (33 current `.af`), pinned and sha256-verified (`cargo xtask corpus --affinity`), test native retention and every-board native/SVG/PDF/PSD output for current files; selected files compare their saved thumbnails. No Affinity reopen or independent paired PSD oracle was available; [source audit and complex-font gaps](affinity-validation.md); [scope, provenance and limits](../crates/affinity/README.md). - Placed documents: File › Place of a `.vectorcraft` file links to it by default (`file.place`'s `link`): one locked object showing an artboard of the document (or its art's bounds), read the way placing it without Link reads it, its symbols, patterns and images kept apart from the document's; without Link (or from bytes, not a file) it is an editable copy as before. It stays vectors in every output (SVG, PDF, EPS, EMF, DXF), and so do placed documents inside it, read from their own files up to 8 levels deep (documents that place each other stop there). On screen they draw from bitmaps cached per file and zoom step, shared by every copy and made in the background (a placeholder until ready), so a sheet of them redraws in about a millisecond; exports and prints draw them exactly. Like linked images, a save keeps only a preview of each (JPEG when opaque) unless Include Linked Files; output, and zooming past the preview, read the file again, falling back to the preview with a warning when it is gone or changed. Saves for older versions write them as plain art. Links check, update, relink, Update Links on open, Link Info (the artboard's size), Placement Options, Break Link (also Embed, Object › Expand: the editable copy placing without Link gives, its resources joining the document), Edit Original (the document in a new tab) and the Links panel cover them; saving a document updates the open documents that place it. The native format (v4) keeps data of 8 KB or more after the JSON instead of base64 in it.