DXF: a CNC / CAM preset sets the export options for cutting software

Squashed from #5500 onto main.
This commit is contained in:
dotCooCoo
2026-11-09 00:25:53 +07:00
committed by bflatastic
parent d61a7abc96
commit cd8e98c6cd
11 changed files with 309 additions and 32 deletions
+16 -3
View File
@@ -6,7 +6,7 @@ use std::sync::Arc;
use serde::Deserialize;
use serde_json::{Value, json};
use vectorcraft_cad::{ColorDepth, DxfOptions, DxfVersion, Preserve, RasterFormat};
use vectorcraft_cad::{ColorDepth, DxfOptions, DxfPreset, DxfVersion, Preserve, RasterFormat};
use vectorcraft_doc::{Document, Unit};
use vectorcraft_geom::Rect;
@@ -21,7 +21,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Export DXF",
[],
None,
"{path?, version?: R12|R13|R14|2000|2004|2007|2010|2013|2018 (default), unit?: mm (default; any unit name: pt, in, cm…), scale?: 1 (drawing units per unit), scaleLineweights?: false, colors?: 8|16|256|true (default; true colour needs 2004+, else 256), rasterFormat?: png (default)|jpeg (placed images, written next to the file), preserve?: appearance (default: type as outlines; strokes a CAD line can't draw as filled outlines; brush art)|editability (type as text; every stroke a line with its lineweight and dashes), alterPaths?: false (strokes a CAD line can't draw as their filled outlines, with editability too; also strokes wider than 2.11 mm, and every stroke in R13 and R14), outlineText?: false, selectedOnly?: false (the selected objects, in their layers), artboard?: 0 (the origin is its bottom-left corner), useArtboards?: true (one drawing per chosen artboard, artboards?/range?, holding the art over it: {stem}-{artboard}.dxf) | false (the origin at the art's bounds)} → {path, bytes, warnings, files?, linked?: [image files]}; no path → {dataBase64, …, linked?: [{name, dataBase64}]}. Layers become DXF layers (hidden: off; locked; non-printing: not plotted); paths become polylines and splines, fills their outlines (which laser and cutter software reads) and solid hatches. Same as document.export {format: dxf}",
"{path?, dxfPreset?: custom (default)|cnc (CNC / CAM: version 2000, the oldest version that has both lightweight polylines and lineweights; unit mm, scale 1; colors 256, all 2000 holds; preserve editability and alterPaths false, so strokes stay center lines; outlineText true, so type is paths; rasterFormat png; the options given override it), version?: R12|R13|R14|2000|2004|2007|2010|2013|2018 (default), unit?: mm (default; any unit name: pt, in, cm…), scale?: 1 (drawing units per unit), scaleLineweights?: false, colors?: 8|16|256|true (default; true colour needs 2004+, else 256), rasterFormat?: png (default)|jpeg (placed images, written next to the file), preserve?: appearance (default: type as outlines; strokes a CAD line can't draw as filled outlines; brush art)|editability (type as text; every stroke a line with its lineweight and dashes), alterPaths?: false (strokes a CAD line can't draw as their filled outlines, with editability too; also strokes wider than 2.11 mm, and every stroke in R13 and R14), outlineText?: false, selectedOnly?: false (the selected objects, in their layers), artboard?: 0 (the origin is its bottom-left corner), useArtboards?: true (one drawing per chosen artboard, artboards?/range?, holding the art over it: {stem}-{artboard}.dxf) | false (the origin at the art's bounds)} → {path, bytes, warnings, files?, linked?: [image files]}; no path → {dataBase64, …, linked?: [{name, dataBase64}]}. Layers become DXF layers (hidden: off; locked; non-printing: not plotted); paths become polylines and splines, fills their outlines (which laser and cutter software reads) and solid hatches. Same as document.export {format: dxf}",
has_doc,
export_dxf
)]
@@ -33,6 +33,12 @@ pub(super) const OPTIONS: &[FormatOption] = &[
super::ARTBOARDS,
super::RANGE,
super::USE_ARTBOARDS,
FormatOption {
name: "dxfPreset",
ty: "string",
default: "\"custom\"",
description: "custom, or cnc (CNC / CAM: version 2000, mm, 256 colors, editability, type as outlines, PNG images); the options given override it",
},
FormatOption { name: "version", ty: "string", default: "\"2018\"", description: "R12, R13, R14, 2000, 2004, 2007, 2010, 2013 or 2018" },
FormatOption {
name: "unit",
@@ -120,6 +126,7 @@ impl Unsupported {
struct Params {
#[serde(flatten)]
boards: ArtboardPick,
dxf_preset: Option<String>,
version: Option<Value>,
unit: Option<String>,
scale: Option<f64>,
@@ -150,7 +157,8 @@ fn choice<T>(field: &str, v: Option<String>, read: impl Fn(&str) -> Option<T>, d
/// The DXF options in `p` (the region and crop are set per drawing) and the artboard choice.
fn parse(p: &Value) -> Result<(DxfOptions, ArtboardPick)> {
let q: Params = if p.is_object() { Params::deserialize(p).map_err(|e| bad(C, format!("DXF options: {e}")))? } else { Params::default() };
let d = DxfOptions::default();
// The options a preset sets are the defaults of those `p` leaves out.
let d = choice("dxfPreset", q.dxf_preset, DxfPreset::from_id, DxfPreset::Custom, "custom or cnc")?.options();
let scale = q.scale.unwrap_or(d.scale);
if !(scale.is_finite() && vectorcraft_cad::SCALE_RANGE.contains(&scale)) {
return Err(bad(C, format!("DXF scale must be a positive number up to {}, not {scale}", vectorcraft_cad::SCALE_RANGE.end())));
@@ -181,6 +189,11 @@ pub fn check(p: &Value) -> std::result::Result<(), String> {
parse(p).map(|_| ()).map_err(|e| e.to_string())
}
/// The DXF options in `p` (the region and crop left at their defaults).
pub fn options(p: &Value) -> std::result::Result<DxfOptions, String> {
parse(p).map(|(o, _)| o).map_err(|e| e.to_string())
}
/// Encode `doc` as DXF: one drawing whose origin is the chosen artboard's bottom-left corner
/// (default the first), or with `use_artboards` one drawing per chosen artboard (default all)
/// holding the art over it.
+26
View File
@@ -63,6 +63,32 @@ fn export_writes_dxf_of_the_chosen_version() {
assert_eq!(header(&text(&ser), "$INSUNITS").as_deref(), Some("1"));
}
#[test]
fn the_cnc_preset_sets_the_options_that_are_not_given() {
let mut s = two_boards();
let cnc = text(&s.execute("document.exportDxf", &json!({"dxfPreset": "cnc"})).unwrap());
assert_eq!(header(&cnc, "$ACADVER").as_deref(), Some("AC1015"));
assert_eq!(header(&cnc, "$INSUNITS").as_deref(), Some("4"));
// document.export takes it too, beside a PDF preset's name.
let r = s.execute("document.export", &json!({"format": "dxf", "dxfPreset": "cnc"})).unwrap();
assert_eq!(header(&text(&r), "$ACADVER").as_deref(), Some("AC1015"));
let o = dxf::options(&json!({"dxfPreset": "CNC", "unit": "in"})).unwrap();
assert_eq!(
(o.version, o.unit, o.colors, o.preserve, o.alter_paths, o.outline_text),
(
vectorcraft_cad::DxfVersion::R2000,
vectorcraft_doc::Unit::Inches,
vectorcraft_cad::ColorDepth::Aci256,
vectorcraft_cad::Preserve::Editability,
false,
true
)
);
assert_eq!(dxf::options(&json!({})).unwrap().version, vectorcraft_cad::DxfVersion::R2018, "custom by default");
let e = s.execute("document.exportDxf", &json!({"dxfPreset": "laser"})).unwrap_err().to_string();
assert!(e.contains("custom or cnc"), "{e}");
}
#[test]
fn use_artboards_writes_one_drawing_per_artboard() {
let mut s = two_boards();