mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-08 04:52:10 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c18aaf7cc3 |
@@ -6,29 +6,48 @@ use std::collections::HashMap;
|
||||
use serde_json::{Value, json};
|
||||
use solidcore_record::Vec2;
|
||||
use solidcraft_io::{DxfEntity, DxfLayer, Geom2, MAX_DRAWING_BYTES};
|
||||
use solidcraft_sketch::{CurveKind, LinkKind, LinkSource, Sketch};
|
||||
use solidcraft_sketch::{Analysis, CurveKind, LinkKind, LinkSource, Sketch};
|
||||
|
||||
use super::CommandSpec;
|
||||
use super::sketch::{edit, ids_of};
|
||||
use super::sketch::{Left, edit_solved, ids_of};
|
||||
use super::sketch_store;
|
||||
use crate::params::{bad, num, str_, vec2};
|
||||
use crate::schema::sketch_import::{DrawingInserted, DxfExported, ExportDxfParams, ExportDxfResult, InsertDxfParams, InsertSvgParams, SketchSolve};
|
||||
use crate::schema::{TypedContract, output_schema_for};
|
||||
use crate::{EngineError, Result, Session};
|
||||
|
||||
const INSERT_DXF_CONTRACT: TypedContract<InsertDxfParams> = TypedContract::new();
|
||||
const INSERT_SVG_CONTRACT: TypedContract<InsertSvgParams> = TypedContract::new();
|
||||
const EXPORT_DXF_CONTRACT: TypedContract<ExportDxfParams> = TypedContract::new();
|
||||
|
||||
/// A command's reply as JSON, built from the type its output schema describes so the two can't
|
||||
/// drift apart.
|
||||
fn reply(cmd: &str, reply: impl serde::Serialize) -> Result<Value> {
|
||||
serde_json::to_value(reply).map_err(|e| bad(cmd, e.to_string()))
|
||||
}
|
||||
|
||||
pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("sketch.insert_dxf", "Insert DXF", insert_dxf)
|
||||
CommandSpec::new("sketch.insert_dxf", "Insert DXF", crate::checked!("sketch.insert_dxf", insert_dxf))
|
||||
.at("SKETCH", "INSERT")
|
||||
.icon("sketch_dxf")
|
||||
.contract(&INSERT_DXF_CONTRACT)
|
||||
.output(output_schema_for::<DrawingInserted>)
|
||||
.params(concat!("path | text: ASCII DXF (blocks expanded); at?: [x,y] offset, scale? | width? | height?: mm, the drawing's size before rotation, aspect kept (win over scale); angle?: degrees, about the drawing's origin (or its centre); align?: \"origin\" (default: `at` is where the file's origin goes) | \"center\" (`at` is where the drawing's middle goes and it turns and scales about it); tolerance?: mm within which ends join (default 0.0001); plane?: (when no sketch is active, a new sketch on it; default XY)", created_doc!())),
|
||||
CommandSpec::new("sketch.insert_svg", "Insert SVG", insert_svg)
|
||||
CommandSpec::new("sketch.insert_svg", "Insert SVG", crate::checked!("sketch.insert_svg", insert_svg))
|
||||
.at("SKETCH", "INSERT")
|
||||
.icon("svg")
|
||||
.contract(&INSERT_SVG_CONTRACT)
|
||||
.output(output_schema_for::<DrawingInserted>)
|
||||
.params(concat!("path | text: SVG; at?: [x,y] offset, scale? | width? | height?: mm, the drawing's size before rotation, aspect kept (win over scale); angle?: degrees, about the drawing's origin (or its centre); align?: \"origin\" (default: `at` is where the file's origin goes) | \"center\" (`at` is where the drawing's middle goes and it turns and scales about it); tolerance?: mm within which ends join (default 0.0001); plane?: (when no sketch is active, a new sketch on it; default XY)", created_doc!())),
|
||||
CommandSpec::new("sketch.drawing_bounds", "Drawing Size", drawing_bounds)
|
||||
.icon("svg")
|
||||
.noundo()
|
||||
.params("path | text: SVG or DXF (by `format`: \"svg\" | \"dxf\", default by the file name, else SVG); returns min, max, width and height in the file's own size"),
|
||||
CommandSpec::new("sketch.export_dxf", "Save As DXF", export_dxf).icon("sketch_dxf")
|
||||
CommandSpec::new("sketch.export_dxf", "Save As DXF", crate::checked!("sketch.export_dxf", export_dxf))
|
||||
.icon("sketch_dxf")
|
||||
.noundo()
|
||||
.contract(&EXPORT_DXF_CONTRACT)
|
||||
.output(output_schema_for::<ExportDxfResult>)
|
||||
.params("sketch?: id|name (default active); path?: file to write (else the DXF text is returned); units?: \"design\" | \"in\" | \"inch\" | \"mm\" | \"cm\" | \"m\" | \"ft\" (default mm); layer?: profile layer name (default \"0\"); layers?: \"single\" | \"roles\" (default \"single\": all on `layer`; \"roles\": outer on \"OUTLINE\", holes on \"CUT\"); join?: bool (default false: R12 LINE/ARC; true: closed loops as LWPOLYLINE with arc bulges)"),
|
||||
];
|
||||
|
||||
@@ -251,10 +270,13 @@ fn insert(s: &mut Session, p: &Value, cmd: &str, svg: bool) -> Result<Value> {
|
||||
if geom.len() > solidcraft_sketch::MAX_POINTS {
|
||||
return Err(solidcraft_sketch::SketchError::TooLarge.into());
|
||||
}
|
||||
let ((curves, texts), info) = edit(s, p, cmd, false, |sk, _| {
|
||||
// `sketch::edit`, but keeping the solve as a typed value for the reply.
|
||||
let target = sketch_store::target(s, p, cmd)?;
|
||||
let ((curves, texts), report) = edit_solved(s, target, false, Analysis::Freedom, |sk, _| {
|
||||
let (cs, texts) = add_geometry(sk, &geom, &f, k, tol)?;
|
||||
Ok((ids_of(sk, &cs), texts))
|
||||
Ok(((ids_of(sk, &cs), texts), Left::Unsolved))
|
||||
})?;
|
||||
let info = report.map(|rep| SketchSolve { solved: rep.keeps_orientation(), dof: rep.dof });
|
||||
// Text becomes sketch text.
|
||||
let id = sketch_store::active(s).ok_or_else(|| bad(cmd, "no sketch"))?;
|
||||
let mut n_text = 0;
|
||||
@@ -279,7 +301,7 @@ fn insert(s: &mut Session, p: &Value, cmd: &str, svg: bool) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(json!({"sketch": id, "created_sketch": created, "curves": curves.len(), "texts": n_text, "info": info}))
|
||||
reply(cmd, DrawingInserted { sketch: id, created_sketch: created, curves: curves.len(), texts: n_text, info, param_warnings: Vec::new() })
|
||||
}
|
||||
|
||||
fn insert_dxf(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
@@ -522,9 +544,10 @@ fn export_dxf(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
if let Some(path) = str_(p, "path").filter(|x| !x.trim().is_empty()) {
|
||||
solidcraft_io::vfs::write(path, text.as_bytes()).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
|
||||
return Ok(json!({"path": path, "entities": entity_count}));
|
||||
let exported = DxfExported::Written { path: path.to_string(), entities: entity_count };
|
||||
return reply(cmd, ExportDxfResult { exported, param_warnings: Vec::new() });
|
||||
}
|
||||
Ok(json!({"dxf": text, "entities": entity_count}))
|
||||
reply(cmd, ExportDxfResult { exported: DxfExported::Text { dxf: text, entities: entity_count }, param_warnings: Vec::new() })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -110,6 +110,7 @@ pub mod sketch_constraints;
|
||||
pub mod sketch_create;
|
||||
pub mod sketch_ends;
|
||||
pub mod sketch_equation;
|
||||
pub mod sketch_import;
|
||||
pub mod sketch_inspect;
|
||||
pub mod sketch_modify;
|
||||
pub mod sketch_project;
|
||||
|
||||
@@ -0,0 +1,448 @@
|
||||
//! Contracts and result types for the commands in `cmd/sketch_import.rs`: `sketch.insert_dxf`,
|
||||
//! `sketch.insert_svg` and `sketch.export_dxf`.
|
||||
//!
|
||||
//! The handlers read their parameters from the raw JSON, so each key here reads as its handler
|
||||
//! reads it and fails with the handler's message. Whether a file can be read or written, a sketch
|
||||
//! exists or a drawing has anything to import stays the handler's to check. A call with several
|
||||
//! problems may get a different one first in strict than with the check off (the check goes key
|
||||
//! by key, the handlers in their own order).
|
||||
//!
|
||||
//! Two insert keys are read only in some cases, and the contract reads them in the same cases:
|
||||
//! `path` only when `text` is not given, and of `width`, `height` and `scale` only the first one
|
||||
//! given, in that order (a `height` next to a `width`, or a `scale` next to either, is never read).
|
||||
//! A value of the wrong type that the handler reads as if the key were left out (`text: 5` next
|
||||
//! to a good `path`, a text for `scale`) is refused by the contract, as schemas.md has it: that
|
||||
//! difference is what warn and strict report.
|
||||
//! `plane` and `name` are read only when no sketch is being edited, which the check can't see
|
||||
//! (it has no session), so they take any value.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
|
||||
use serde::de::Error as _;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use serde_json::Value;
|
||||
use solidcraft_io::MAX_DRAWING_BYTES;
|
||||
|
||||
use super::check::{located, whole};
|
||||
use super::sketch_ends::SketchTarget;
|
||||
use super::types::{Point2, not_blank_pattern};
|
||||
|
||||
/// A key that, once given (`null` included), must parse as `T`, as the handler reads it.
|
||||
fn some<'de, D: Deserializer<'de>, T: Deserialize<'de>>(d: D) -> Result<Option<T>, D::Error> {
|
||||
T::deserialize(d).map(Some)
|
||||
}
|
||||
|
||||
/// The keys the two insert commands read. See [`InsertDxfParams`] and [`InsertSvgParams`], which
|
||||
/// leave `path`, `height` and `scale` unread in the cases the handler does.
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields, transform = source_required)]
|
||||
pub struct InsertDrawingKeys {
|
||||
/// The drawing itself, as text. Wins over `path`. At most 16 MiB (`MAX_DRAWING_BYTES`).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub text: Option<DrawingText>,
|
||||
/// The drawing file to read (not blank, fewer than 4096 UTF-8 bytes). Used when `text` is
|
||||
/// not given; ignored when it is.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(schema_with = "path_schema")]
|
||||
pub path: Option<DrawingPath>,
|
||||
/// An offset, in mm, [x, y]. Default: [0, 0]. With `align` "origin" it is where the file's
|
||||
/// origin goes; with "center", where the drawing's middle goes.
|
||||
#[serde(default)]
|
||||
pub at: Option<Point2>,
|
||||
/// A size factor, 1e-9 to 1e6 (a value outside is read as 1). Default: 1. Used only when
|
||||
/// neither `width` nor `height` is given.
|
||||
#[serde(default)]
|
||||
pub scale: Option<f64>,
|
||||
/// The drawing's width in mm after sizing, before rotation; the aspect ratio is kept. Wins
|
||||
/// over `height` and `scale`. The drawing must have a width.
|
||||
#[serde(default, deserialize_with = "some", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(with = "PositiveMm")]
|
||||
pub width: Option<PositiveMm>,
|
||||
/// The drawing's height in mm after sizing, before rotation; the aspect ratio is kept. Wins
|
||||
/// over `scale`; ignored when `width` is given. The drawing must have a height.
|
||||
#[serde(default, deserialize_with = "some", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(with = "PositiveMm")]
|
||||
pub height: Option<PositiveMm>,
|
||||
/// A turn in degrees, about the drawing's origin (with `align` "center": about its middle).
|
||||
/// Default: 0.
|
||||
#[serde(default, deserialize_with = "some", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(with = "AngleDegrees")]
|
||||
pub angle: Option<AngleDegrees>,
|
||||
/// What `at` places: the file's origin ("origin") or the drawing's middle ("center", which it
|
||||
/// then also turns and scales about). Default: origin.
|
||||
#[serde(default)]
|
||||
pub align: Option<DrawingAlign>,
|
||||
/// The distance in mm within which ends join into one point, 1e-9 to 10 (a value outside is
|
||||
/// brought inside; one that is not above 0 reads as the default). Default: 0.0001.
|
||||
#[serde(default)]
|
||||
pub tolerance: Option<f64>,
|
||||
/// Read only when no sketch is being edited: the plane of the new sketch, as `sketch.create`
|
||||
/// takes it. Default: XY. Ignored (and not checked) while a sketch is being edited.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub plane: Option<Value>,
|
||||
/// Read only when no sketch is being edited: the new sketch's name. Ignored (and not checked)
|
||||
/// while a sketch is being edited.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<Value>,
|
||||
/// The sketch to insert into: its feature id or its name. Default: the one being edited (a
|
||||
/// new one when none is).
|
||||
#[serde(default)]
|
||||
pub sketch: Option<SketchTarget>,
|
||||
}
|
||||
|
||||
/// Leaves out the keys the handler never reads for this call: `path` next to a `text` that is
|
||||
/// text, `height` next to a `width`, and `scale` next to either.
|
||||
fn leave_unread(value: &mut Value) {
|
||||
let Some(keys) = value.as_object_mut() else { return };
|
||||
if keys.get("text").is_some_and(Value::is_string) {
|
||||
keys.remove("path");
|
||||
}
|
||||
if keys.contains_key("width") {
|
||||
keys.remove("height");
|
||||
}
|
||||
if keys.contains_key("width") || keys.contains_key("height") {
|
||||
keys.remove("scale");
|
||||
}
|
||||
}
|
||||
|
||||
fn read_keys<'de, D: Deserializer<'de>>(d: D) -> Result<InsertDrawingKeys, D::Error> {
|
||||
let mut value = Value::deserialize(d)?;
|
||||
leave_unread(&mut value);
|
||||
serde_path_to_error::deserialize(&value).map_err(|e| D::Error::custom(located(&e)))
|
||||
}
|
||||
|
||||
/// Insert an ASCII DXF drawing into a sketch as curves: the active sketch, or a new one when none
|
||||
/// is being edited. Give the drawing as `path` or as `text`. Blocks are expanded. It can be
|
||||
/// sized, turned and placed. Text in the drawing becomes sketch text. One undo step.
|
||||
#[derive(Debug)]
|
||||
pub struct InsertDxfParams(pub InsertDrawingKeys);
|
||||
|
||||
impl<'de> Deserialize<'de> for InsertDxfParams {
|
||||
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||
read_keys(d).map(InsertDxfParams)
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for InsertDxfParams {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"InsertDxfParams".into()
|
||||
}
|
||||
fn json_schema(generator: &mut SchemaGenerator) -> Schema {
|
||||
let mut schema = InsertDrawingKeys::json_schema(generator);
|
||||
schema.insert("description".into(), INSERT_DXF.into());
|
||||
schema
|
||||
}
|
||||
}
|
||||
|
||||
const INSERT_DXF: &str = "Insert an ASCII DXF drawing into a sketch as curves: the active sketch, or a new one when none is being edited. Give the drawing as `path` or as `text`. Blocks are expanded. It can be sized, turned and placed. Text in the drawing becomes sketch text. One undo step.";
|
||||
|
||||
/// Insert an SVG drawing into a sketch as curves: the active sketch, or a new one when none is
|
||||
/// being edited. Give the drawing as `path` or as `text`. It can be sized, turned and placed.
|
||||
/// Text in the drawing becomes sketch text. One undo step.
|
||||
#[derive(Debug)]
|
||||
pub struct InsertSvgParams(pub InsertDrawingKeys);
|
||||
|
||||
impl<'de> Deserialize<'de> for InsertSvgParams {
|
||||
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||
read_keys(d).map(InsertSvgParams)
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for InsertSvgParams {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"InsertSvgParams".into()
|
||||
}
|
||||
fn json_schema(generator: &mut SchemaGenerator) -> Schema {
|
||||
let mut schema = InsertDrawingKeys::json_schema(generator);
|
||||
schema.insert("description".into(), INSERT_SVG.into());
|
||||
schema
|
||||
}
|
||||
}
|
||||
|
||||
const INSERT_SVG: &str = "Insert an SVG drawing into a sketch as curves: the active sketch, or a new one when none is being edited. Give the drawing as `path` or as `text`. It can be sized, turned and placed. Text in the drawing becomes sketch text. One undo step.";
|
||||
|
||||
/// The insert commands need `path` or `text`; the handler says so when both are left out. A
|
||||
/// `path` it reads (no `text`) is not blank and under 4096 bytes; `maxLength` counts
|
||||
/// characters, so 4095 is as close as the schema gets. `height` is read only without a `width`
|
||||
/// and `scale` only without either, so their shapes apply only then, as [`leave_unread`] has it.
|
||||
fn source_required(schema: &mut Schema) {
|
||||
let read_path = serde_json::json!({"type": "string", "maxLength": 4095, "pattern": not_blank_pattern()});
|
||||
schema.insert("anyOf".into(), serde_json::json!([{"required": ["text"]}, {"required": ["path"], "properties": {"path": read_path}}]));
|
||||
let Some(props) = schema.get_mut("properties").and_then(Value::as_object_mut) else { return };
|
||||
let mut read_only_without = |key: &str| {
|
||||
let shape = props.get_mut(key).map(Value::take).unwrap_or_default();
|
||||
props.insert(key.into(), serde_json::json!({"description": shape.get("description").cloned().unwrap_or_default()}));
|
||||
shape
|
||||
};
|
||||
let height = read_only_without("height");
|
||||
let scale = read_only_without("scale");
|
||||
schema.insert(
|
||||
"allOf".into(),
|
||||
serde_json::json!([
|
||||
{"anyOf": [{"required": ["width"]}, {"properties": {"height": height}}]},
|
||||
{"anyOf": [{"required": ["width"]}, {"required": ["height"]}, {"properties": {"scale": scale}}]},
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
/// `path` is checked by [`source_required`] only when it is read (no `text`).
|
||||
fn path_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
json_schema!({})
|
||||
}
|
||||
|
||||
/// A drawing's text, as `source_text` reads it: at most 16 MiB, or the handler's refusal.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct DrawingText(pub String);
|
||||
|
||||
impl<'de> Deserialize<'de> for DrawingText {
|
||||
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||
let text = String::deserialize(d)?;
|
||||
if text.len() > MAX_DRAWING_BYTES {
|
||||
return Err(D::Error::custom(whole("the drawing is too large")));
|
||||
}
|
||||
Ok(DrawingText(text))
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for DrawingText {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"DrawingText".into()
|
||||
}
|
||||
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
json_schema!({"type": "string"})
|
||||
}
|
||||
}
|
||||
|
||||
/// A drawing file name, as `source_text` reads `path`: not blank and under 4096 bytes, or the
|
||||
/// handler's "give `path` or `text`" (anything but text reads as no `path`).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct DrawingPath(pub String);
|
||||
|
||||
impl<'de> Deserialize<'de> for DrawingPath {
|
||||
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||
match Value::deserialize(d)? {
|
||||
Value::String(s) if !s.trim().is_empty() && s.len() < 4096 => Ok(DrawingPath(s)),
|
||||
_ => Err(D::Error::custom(whole("give `path` or `text`"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A size in mm above 0, as the insert commands' `width` and `height` are read.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct PositiveMm(pub f64);
|
||||
|
||||
/// The message the insert commands give for `width` or `height`: the check puts the key first.
|
||||
const POSITIVE_MM: &str = "must be a positive number (mm)";
|
||||
|
||||
impl<'de> Deserialize<'de> for PositiveMm {
|
||||
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||
Value::deserialize(d)?.as_f64().filter(|w| w.is_finite() && *w > 0.0).map(PositiveMm).ok_or_else(|| D::Error::custom(POSITIVE_MM))
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for PositiveMm {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"PositiveMm".into()
|
||||
}
|
||||
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
json_schema!({"description": "A size in mm, above 0.", "type": "number", "exclusiveMinimum": 0})
|
||||
}
|
||||
}
|
||||
|
||||
/// An angle in degrees as `angle` is read: a number, whatever its size.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct AngleDegrees(pub f64);
|
||||
|
||||
impl<'de> Deserialize<'de> for AngleDegrees {
|
||||
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||
Value::deserialize(d)?.as_f64().filter(|a| a.is_finite()).map(AngleDegrees).ok_or_else(|| D::Error::custom("must be a number (degrees)"))
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for AngleDegrees {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"AngleDegrees".into()
|
||||
}
|
||||
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
json_schema!({"description": "An angle in degrees.", "type": "number"})
|
||||
}
|
||||
}
|
||||
|
||||
/// What an insert's `at` places, as `align` is read (the words are case-sensitive).
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum DrawingAlign {
|
||||
/// `at` is where the file's origin goes.
|
||||
Origin,
|
||||
/// `at` is where the drawing's middle goes, and it turns and scales about it.
|
||||
Center,
|
||||
}
|
||||
|
||||
impl JsonSchema for DrawingAlign {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"DrawingAlign".into()
|
||||
}
|
||||
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
json_schema!({
|
||||
"description": "What `at` places: \"origin\" (the file's origin goes there) or \"center\" (the drawing's middle goes there, and it turns and scales about it). Lower case only.",
|
||||
"type": "string",
|
||||
"enum": ["origin", "center"]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for DrawingAlign {
|
||||
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||
match Value::deserialize(d)?.as_str() {
|
||||
Some("origin") => Ok(DrawingAlign::Origin),
|
||||
Some("center") => Ok(DrawingAlign::Center),
|
||||
_ => Err(D::Error::custom("must be \"origin\" or \"center\"")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What `sketch.insert_dxf` and `sketch.insert_svg` return.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct DrawingInserted {
|
||||
/// The sketch the drawing went into, by feature id.
|
||||
pub sketch: u64,
|
||||
/// The new sketch's id when no sketch was being edited and one was made; else null.
|
||||
pub created_sketch: Option<u64>,
|
||||
/// How many curves were added.
|
||||
pub curves: usize,
|
||||
/// How many sketch texts were added.
|
||||
pub texts: usize,
|
||||
/// The sketch's solve afterwards; null when it was not solved.
|
||||
pub info: Option<SketchSolve>,
|
||||
/// In the warn parameter-check mode: what the call's parameters got wrong.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub param_warnings: Vec<String>,
|
||||
}
|
||||
|
||||
/// A sketch's solve, as the sketch commands report it.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct SketchSolve {
|
||||
/// Whether the solve kept the sketch's orientation.
|
||||
pub solved: bool,
|
||||
/// The degrees of freedom left (0 = fully constrained).
|
||||
pub dof: usize,
|
||||
}
|
||||
|
||||
/// Save a sketch's curves as an ASCII DXF: to the file `path`, or, without it, as text in the
|
||||
/// reply. Construction curves are left out; splines and other curves are written as line
|
||||
/// segments.
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct ExportDxfParams {
|
||||
/// The sketch: its feature id or its name. Default: the one being edited.
|
||||
#[serde(default)]
|
||||
pub sketch: Option<SketchTarget>,
|
||||
/// The file to write. Default (also when blank): none, and the DXF text is returned.
|
||||
#[serde(default)]
|
||||
pub path: Option<String>,
|
||||
/// The drawing's units (any case): `design` (the design's length unit), `mm`, `in` or
|
||||
/// `inch`, `cm`, `m` or `ft`. Default: mm.
|
||||
#[serde(default, deserialize_with = "dxf_units")]
|
||||
#[schemars(schema_with = "dxf_units_schema")]
|
||||
pub units: Option<String>,
|
||||
/// The layer of the profiles (blank: the default). Default: `0`.
|
||||
#[serde(default, deserialize_with = "dxf_layer")]
|
||||
#[schemars(schema_with = "dxf_layer_schema")]
|
||||
pub layer: Option<String>,
|
||||
/// `single` (any case, the default): everything on `layer`; `roles`: outer loops on
|
||||
/// `OUTLINE` and holes on `CUT`.
|
||||
#[serde(default, deserialize_with = "dxf_layers")]
|
||||
#[schemars(schema_with = "dxf_layers_schema")]
|
||||
pub layers: Option<String>,
|
||||
/// `true` writes closed loops as LWPOLYLINE with arc bulges. Default: false (LINE and ARC).
|
||||
#[serde(default, deserialize_with = "dxf_join")]
|
||||
pub join: Option<bool>,
|
||||
}
|
||||
|
||||
/// The units `sketch.export_dxf` takes, in lower case (the handler reads them in any case).
|
||||
const DXF_UNITS: [&str; 7] = ["design", "mm", "in", "inch", "cm", "m", "ft"];
|
||||
|
||||
fn dxf_units<'de, D: Deserializer<'de>>(d: D) -> Result<Option<String>, D::Error> {
|
||||
match Option::<Value>::deserialize(d)? {
|
||||
None | Some(Value::Null) => Ok(None),
|
||||
Some(Value::String(units)) => {
|
||||
let lower = units.to_ascii_lowercase();
|
||||
if DXF_UNITS.contains(&lower.as_str()) {
|
||||
Ok(Some(lower))
|
||||
} else {
|
||||
Err(D::Error::custom(whole(format!("unsupported unit `{lower}` (use \"design\", \"mm\", \"in\", \"cm\", \"m\", or \"ft\")"))))
|
||||
}
|
||||
}
|
||||
Some(_) => Err(D::Error::custom(whole("`units` must be a string"))),
|
||||
}
|
||||
}
|
||||
|
||||
fn dxf_units_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
json_schema!({"type": ["string", "null"], "description": "design, mm, in, inch, cm, m or ft, in any case. Default: mm."})
|
||||
}
|
||||
|
||||
fn dxf_layer<'de, D: Deserializer<'de>>(d: D) -> Result<Option<String>, D::Error> {
|
||||
match Option::<Value>::deserialize(d)? {
|
||||
None | Some(Value::Null) => Ok(None),
|
||||
Some(Value::String(layer)) => Ok(Some(layer)),
|
||||
Some(_) => Err(D::Error::custom(whole("`layer` must be a string"))),
|
||||
}
|
||||
}
|
||||
|
||||
fn dxf_layer_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
json_schema!({"type": ["string", "null"], "description": "The profiles' layer (blank: the default). Default: 0."})
|
||||
}
|
||||
|
||||
fn dxf_layers<'de, D: Deserializer<'de>>(d: D) -> Result<Option<String>, D::Error> {
|
||||
match Option::<Value>::deserialize(d)? {
|
||||
None | Some(Value::Null) => Ok(None),
|
||||
Some(Value::String(layers)) if ["single", "roles"].contains(&layers.to_ascii_lowercase().as_str()) => Ok(Some(layers.to_ascii_lowercase())),
|
||||
Some(_) => Err(D::Error::custom(whole("`layers` must be \"single\" or \"roles\""))),
|
||||
}
|
||||
}
|
||||
|
||||
fn dxf_layers_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
json_schema!({"type": ["string", "null"], "description": "single or roles, in any case. Default: single."})
|
||||
}
|
||||
|
||||
fn dxf_join<'de, D: Deserializer<'de>>(d: D) -> Result<Option<bool>, D::Error> {
|
||||
match Option::<Value>::deserialize(d)? {
|
||||
None | Some(Value::Null) => Ok(None),
|
||||
Some(Value::Bool(join)) => Ok(Some(join)),
|
||||
Some(_) => Err(D::Error::custom(whole("`join` must be true or false"))),
|
||||
}
|
||||
}
|
||||
|
||||
/// What `sketch.export_dxf` returns.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct ExportDxfResult {
|
||||
#[serde(flatten)]
|
||||
pub exported: DxfExported,
|
||||
/// In the warn parameter-check mode: what the call's parameters got wrong.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub param_warnings: Vec<String>,
|
||||
}
|
||||
|
||||
/// Where the DXF went.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum DxfExported {
|
||||
/// With a `path`: the file was written.
|
||||
Written {
|
||||
/// The file written, as given.
|
||||
path: String,
|
||||
/// How many entities it holds.
|
||||
entities: usize,
|
||||
},
|
||||
/// Without one: the DXF as text.
|
||||
Text {
|
||||
/// The DXF file's text.
|
||||
dxf: String,
|
||||
/// How many entities it holds.
|
||||
entities: usize,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "sketch_import_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,313 @@
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_io::MAX_DRAWING_BYTES;
|
||||
|
||||
use super::{DrawingInserted, ExportDxfParams, ExportDxfResult, InsertDxfParams, InsertSvgParams};
|
||||
use crate::Session;
|
||||
use crate::schema::tests::{assert_keys, assert_strict_keeps_todays_errors, conforms};
|
||||
use crate::schema::{ParamCheck, input_schema_for, output_schema_for};
|
||||
|
||||
/// A 30 x 20 rectangle as DXF lines.
|
||||
const DXF: &str = "0\nSECTION\n2\nENTITIES\n0\nLINE\n10\n0\n20\n0\n11\n30\n21\n0\n0\nLINE\n10\n30\n20\n0\n11\n30\n21\n20\n0\nLINE\n10\n30\n20\n20\n11\n0\n21\n20\n0\nLINE\n10\n0\n20\n20\n11\n0\n21\n0\n0\nENDSEC\n0\nEOF\n";
|
||||
|
||||
const SVG: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" width="20mm" height="10mm" viewBox="0 0 20 10"><rect x="0" y="0" width="20" height="10"/><text x="2" y="5" font-size="3">Hi</text></svg>"#;
|
||||
|
||||
fn temp_dir(tag: &str) -> std::path::PathBuf {
|
||||
let dir = std::env::temp_dir().join(format!("solidcraft-12508-{tag}-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
dir
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schemas_and_the_key_check_agree() {
|
||||
let insert = ["align", "angle", "at", "height", "name", "path", "plane", "scale", "sketch", "text", "tolerance", "width"];
|
||||
assert_keys(input_schema_for::<InsertDxfParams>(), &insert);
|
||||
assert_keys(input_schema_for::<InsertSvgParams>(), &insert);
|
||||
assert_keys(input_schema_for::<ExportDxfParams>(), &["join", "layer", "layers", "path", "sketch", "units"]);
|
||||
}
|
||||
|
||||
/// Strict gives the handlers' messages for the calls they refuse for a key (a missing `path` and
|
||||
/// `text` included: the handler's words, not "is required"). The session has a sketch being edited.
|
||||
#[test]
|
||||
fn strict_keeps_todays_errors() {
|
||||
let too_big = "x".repeat(MAX_DRAWING_BYTES + 1);
|
||||
let mut cases: Vec<(&str, Value)> = vec![
|
||||
("sketch.insert_dxf", json!({})),
|
||||
("sketch.insert_dxf", json!({"path": ""})),
|
||||
("sketch.insert_dxf", json!({"path": " "})),
|
||||
("sketch.insert_dxf", json!({"path": 5})),
|
||||
("sketch.insert_dxf", json!({"path": null})),
|
||||
("sketch.insert_dxf", json!({"path": "x".repeat(4096)})),
|
||||
("sketch.insert_dxf", json!({"text": too_big})),
|
||||
("sketch.insert_svg", json!({})),
|
||||
("sketch.insert_svg", json!({"path": " "})),
|
||||
("sketch.insert_svg", json!({"text": null})),
|
||||
("sketch.export_dxf", json!({"sketch": true})),
|
||||
("sketch.export_dxf", json!({"sketch": [1]})),
|
||||
("sketch.export_dxf", json!({"sketch": -1})),
|
||||
("sketch.export_dxf", json!({"sketch": 1.5})),
|
||||
];
|
||||
for (id, text) in [("sketch.insert_dxf", DXF), ("sketch.insert_svg", SVG)] {
|
||||
for extra in [
|
||||
json!({"width": 0}),
|
||||
json!({"width": -2}),
|
||||
json!({"width": null}),
|
||||
json!({"width": "wide"}),
|
||||
json!({"width": [1]}),
|
||||
json!({"height": 0}),
|
||||
json!({"height": null}),
|
||||
json!({"height": true}),
|
||||
json!({"angle": "x"}),
|
||||
json!({"angle": null}),
|
||||
json!({"angle": {}}),
|
||||
json!({"align": "left"}),
|
||||
json!({"align": "Center"}),
|
||||
json!({"align": ""}),
|
||||
json!({"sketch": true}),
|
||||
json!({"sketch": null, "width": 0}),
|
||||
json!({"sketch": [1]}),
|
||||
json!({"sketch": 1.5}),
|
||||
] {
|
||||
let mut p = json!({"text": text});
|
||||
p.as_object_mut().unwrap().extend(extra.as_object().unwrap().clone());
|
||||
cases.push((id, p));
|
||||
}
|
||||
}
|
||||
assert_strict_keeps_todays_errors(&cases);
|
||||
}
|
||||
|
||||
/// The same refusals for a sketch that is named but doesn't exist, a width the drawing can't
|
||||
/// give (a drawing with no width to size is refused by the handler alone), and the export's
|
||||
/// options, which the handler reads only once a sketch is being edited.
|
||||
#[test]
|
||||
fn strict_keeps_the_handlers_own_refusals() {
|
||||
// A vertical line has no width to size.
|
||||
let vertical = "0\nSECTION\n2\nENTITIES\n0\nLINE\n10\n0\n20\n0\n11\n0\n21\n9\n0\nENDSEC\n0\nEOF\n";
|
||||
for (id, p) in [
|
||||
("sketch.insert_dxf", json!({"text": DXF, "sketch": "Nope"})),
|
||||
("sketch.insert_dxf", json!({"text": DXF, "sketch": 9999})),
|
||||
("sketch.insert_dxf", json!({"text": vertical, "width": 10})),
|
||||
("sketch.insert_dxf", json!({"text": "0\nEOF\n"})),
|
||||
("sketch.export_dxf", json!({"sketch": "Nope"})),
|
||||
("sketch.export_dxf", json!({"sketch": 9999})),
|
||||
("sketch.export_dxf", json!({"units": 5})),
|
||||
("sketch.export_dxf", json!({"units": "Yard"})),
|
||||
("sketch.export_dxf", json!({"layer": 1})),
|
||||
("sketch.export_dxf", json!({"layers": "all"})),
|
||||
("sketch.export_dxf", json!({"layers": true})),
|
||||
("sketch.export_dxf", json!({"join": "yes"})),
|
||||
] {
|
||||
let error = |level: ParamCheck| {
|
||||
let mut s = Session { param_check: level, ..Session::default() };
|
||||
s.execute("sketch.create", &json!({"plane": "XY"})).unwrap();
|
||||
s.execute(id, &p).err().map(|e| e.to_string())
|
||||
};
|
||||
let off = error(ParamCheck::Off);
|
||||
assert!(off.is_some(), "{id} {p} should fail today");
|
||||
assert_eq!(error(ParamCheck::Strict), off, "{id} {p}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The types read each key as the handler does: keys it never reads in a call are not read by the
|
||||
/// contract either, and the ones it reads fail with its words.
|
||||
#[test]
|
||||
fn the_types_read_like_the_handler() {
|
||||
let reads = |p: Value| serde_json::from_value::<InsertDxfParams>(p).is_ok();
|
||||
let issues = |p: Value| crate::schema::issues("sketch.insert_svg", &p).into_iter().map(|i| i.message).collect::<Vec<_>>();
|
||||
// `path` is not read when `text` is given; `height` not when `width` is; `scale` not when
|
||||
// either is.
|
||||
assert!(reads(json!({"text": "x", "path": 5})) && reads(json!({"text": "x", "path": ""})));
|
||||
assert!(reads(json!({"width": 5, "height": "x", "scale": "y"})) && reads(json!({"width": 5, "height": null})));
|
||||
assert!(reads(json!({"height": 5, "scale": "y"})) && reads(json!({"height": 5, "scale": null})));
|
||||
assert!(reads(json!({"path": "a.dxf", "scale": 2, "tolerance": 0.01, "at": [1, 2], "angle": 90, "align": "center"})));
|
||||
assert!(reads(json!({"text": "x", "at": {"x": 1, "y": 2}, "angle": 720.5, "width": 1e12, "scale": 0, "tolerance": -1})));
|
||||
// They are read when the handler reads them.
|
||||
// Stricter than the handler, which reads a `text` that isn't text as left out.
|
||||
assert!(!reads(json!({"text": 5, "path": "a.dxf"})) && !reads(json!({"path": "a.dxf", "scale": "y"})));
|
||||
assert!(!reads(json!({"path": ""})) && !reads(json!({"path": 5, "scale": 1})));
|
||||
assert!(!reads(json!({"text": "x", "height": 0})) && !reads(json!({"text": "x", "height": "x", "scale": 2})));
|
||||
assert!(!reads(json!({"text": "x", "width": null, "height": 5})) && !reads(json!({"text": "x", "scale": "y", "width": 0})));
|
||||
// Only while no sketch is being edited does `plane` or `name` matter, which the check can't
|
||||
// see: they take anything.
|
||||
assert!(reads(json!({"text": "x", "plane": 5, "name": [1]})) && reads(json!({"text": "x", "plane": null, "name": null})));
|
||||
assert_eq!(issues(json!({"text": "x", "width": 0})), ["`width` must be a positive number (mm)"]);
|
||||
assert_eq!(issues(json!({"text": "x", "height": -1})), ["`height` must be a positive number (mm)"]);
|
||||
assert_eq!(issues(json!({"text": "x", "angle": "1"})), ["`angle` must be a number (degrees)"]);
|
||||
assert_eq!(issues(json!({"text": "x", "align": "left"})), ["`align` must be \"origin\" or \"center\""]);
|
||||
assert_eq!(issues(json!({"path": " "})), ["give `path` or `text`"]);
|
||||
assert_eq!(issues(json!({"text": "x", "sketch": true})), ["`sketch` must be an id or a name"]);
|
||||
assert_eq!(issues(json!({"text": "x", "wdth": 5})), ["unknown key `wdth`; did you mean `width`?"]);
|
||||
assert!(issues(json!({"text": "x", "width": 5, "height": "x", "path": 5})).is_empty());
|
||||
let export = |p: Value| serde_json::from_value::<ExportDxfParams>(p).is_ok();
|
||||
assert!(export(json!({})) && export(json!({"sketch": null, "path": null})) && export(json!({"sketch": "S", "path": ""})));
|
||||
assert!(export(json!({"sketch": 3, "path": "a.dxf"})) && !export(json!({"sketch": true})) && !export(json!({"path": 5})));
|
||||
}
|
||||
|
||||
/// What the schemas take: a drawing from `path` or `text`, and the keys' own shapes.
|
||||
#[test]
|
||||
fn the_schemas_take_what_the_handlers_read() {
|
||||
let takes = |schema: schemars::Schema, cases: &[(Value, bool)]| {
|
||||
let schema = schema.as_value();
|
||||
for (p, takes) in cases {
|
||||
assert_eq!(conforms(schema, schema, p), *takes, "{p}");
|
||||
}
|
||||
};
|
||||
for schema in [input_schema_for::<InsertDxfParams>(), input_schema_for::<InsertSvgParams>()] {
|
||||
takes(
|
||||
schema,
|
||||
&[
|
||||
(json!({"text": "x"}), true),
|
||||
(json!({"path": "a.svg", "at": [1, 2], "scale": 2, "angle": 45, "align": "center", "tolerance": 0.01}), true),
|
||||
(json!({"text": "x", "width": 40, "plane": "XZ", "name": "Logo", "sketch": "Sketch1"}), true),
|
||||
(json!({"text": "x", "plane": {"origin": [0, 0, 1], "x_dir": [1, 0, 0], "y_dir": [0, 1, 0]}, "sketch": 4}), true),
|
||||
(json!({}), false),
|
||||
(json!({"width": 40}), false),
|
||||
(json!({"text": "x", "width": 0}), false),
|
||||
(json!({"text": "x", "height": -1}), false),
|
||||
(json!({"text": "x", "angle": "x"}), false),
|
||||
(json!({"text": "x", "align": "left"}), false),
|
||||
(json!({"text": "x", "at": [1]}), false),
|
||||
(json!({"text": "x", "wdth": 5}), false),
|
||||
(json!({"path": "x".repeat(4096)}), false),
|
||||
(json!({"text": "x", "path": " "}), true),
|
||||
// Unread keys take anything, as in the handler; read ones keep their shapes.
|
||||
(json!({"text": "x", "path": 5}), true),
|
||||
(json!({"text": "x", "width": 5, "height": -1}), true),
|
||||
(json!({"text": "x", "width": 5, "scale": "y"}), true),
|
||||
(json!({"text": "x", "height": 5, "scale": "y"}), true),
|
||||
(json!({"path": 5}), false),
|
||||
(json!({"text": "x", "height": "x", "scale": 2}), false),
|
||||
(json!({"text": "x", "scale": "y"}), false),
|
||||
],
|
||||
);
|
||||
}
|
||||
// A `path` read without `text` is not blank (`conforms` doesn't check patterns).
|
||||
for schema in [input_schema_for::<InsertDxfParams>(), input_schema_for::<InsertSvgParams>()] {
|
||||
let read_path = &schema.as_value()["anyOf"][1]["properties"]["path"];
|
||||
assert_eq!(read_path["pattern"], json!(crate::schema::types::not_blank_pattern()));
|
||||
}
|
||||
takes(
|
||||
input_schema_for::<ExportDxfParams>(),
|
||||
&[
|
||||
(json!({}), true),
|
||||
(json!({"sketch": "S", "path": "a.dxf"}), true),
|
||||
(json!({"sketch": 2}), true),
|
||||
(json!({"sketch": true}), false),
|
||||
(json!({"file": "a.dxf"}), false),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
fn matches(schema: schemars::Schema, r: &Value) {
|
||||
let schema = schema.as_value();
|
||||
assert!(conforms(schema, schema, r), "{r} doesn't match {schema}");
|
||||
}
|
||||
|
||||
/// Every form of the three commands replies as its output schema says, in Warn without warnings;
|
||||
/// the replies carry the keys they always had; a misspelt key gets its warning and the reply still
|
||||
/// matches, and strict refuses it.
|
||||
#[test]
|
||||
fn replies_match_the_output_schema() {
|
||||
let dir = temp_dir("replies");
|
||||
let svg_file = dir.join("plate.svg");
|
||||
let dxf_file = dir.join("plate.dxf");
|
||||
std::fs::write(&svg_file, SVG).unwrap();
|
||||
std::fs::write(&dxf_file, DXF).unwrap();
|
||||
let (svg_path, dxf_path) = (svg_file.to_string_lossy().into_owned(), dxf_file.to_string_lossy().into_owned());
|
||||
let mut s = Session { param_check: ParamCheck::Warn, ..Session::default() };
|
||||
let run = |s: &mut Session, id: &str, p: Value| {
|
||||
let r = s.execute(id, &p).unwrap_or_else(|e| panic!("{id} {p}: {e}"));
|
||||
assert!(r.get("param_warnings").is_none(), "{id} {p}: {r}");
|
||||
r
|
||||
};
|
||||
// The session adds `created` (the ids a sketch command made) to every `sketch.*` reply, outside
|
||||
// the handlers; it is not part of what they build.
|
||||
let keys = |r: &Value| {
|
||||
r.as_object().map(|o| o.keys().filter(|k| *k != "created").cloned().collect::<std::collections::BTreeSet<_>>()).unwrap_or_default()
|
||||
};
|
||||
let insert_keys: std::collections::BTreeSet<String> = ["created_sketch", "curves", "info", "sketch", "texts"].map(String::from).into();
|
||||
|
||||
// With no sketch being edited: a new one is made, on the plane and with the name given.
|
||||
let r = run(&mut s, "sketch.insert_dxf", json!({"text": DXF, "plane": "XZ", "name": "Plate", "at": [5, 5]}));
|
||||
matches(output_schema_for::<DrawingInserted>(), &r);
|
||||
assert_eq!(keys(&r), insert_keys, "{r}");
|
||||
assert_eq!((r["curves"].as_u64(), r["texts"].as_u64()), (Some(4), Some(0)), "{r}");
|
||||
assert_eq!(r["created_sketch"], r["sketch"], "{r}");
|
||||
// Exactly what the handler returned before it built its reply from the type.
|
||||
let mut handler_reply = r.clone();
|
||||
handler_reply.as_object_mut().unwrap().remove("created");
|
||||
assert_eq!(
|
||||
handler_reply,
|
||||
json!({"sketch": r["sketch"], "created_sketch": r["sketch"], "curves": 4, "texts": 0, "info": {"solved": true, "dof": 8}})
|
||||
);
|
||||
assert!(r["info"]["solved"].is_boolean() && r["info"]["dof"].is_u64(), "{r}");
|
||||
// With one: into it, and `plane` and `name` (of any kind) are not read.
|
||||
let id = s.active_sketch.unwrap();
|
||||
for p in [
|
||||
json!({"text": DXF, "path": 5, "plane": 7, "name": [1]}),
|
||||
json!({"text": DXF, "width": 60, "height": "x", "scale": "y", "angle": 90, "align": "center", "at": {"x": 1, "y": 2}}),
|
||||
json!({"text": DXF, "height": 10, "scale": "y", "tolerance": 0.01}),
|
||||
json!({"text": DXF, "scale": 2, "align": "origin", "sketch": id}),
|
||||
json!({"text": DXF, "sketch": s.doc.feature(id).map(|f| f.name.clone()).unwrap_or_default()}),
|
||||
json!({"path": dxf_path, "text": null, "sketch": null}),
|
||||
] {
|
||||
let r = run(&mut s, "sketch.insert_dxf", p);
|
||||
matches(output_schema_for::<DrawingInserted>(), &r);
|
||||
assert_eq!(keys(&r), insert_keys);
|
||||
assert_eq!((r["sketch"].as_u64(), r["created_sketch"].clone(), r["curves"].as_u64()), (Some(id), Value::Null, Some(4)), "{r}");
|
||||
}
|
||||
let r = run(&mut s, "sketch.insert_svg", json!({"text": SVG, "angle": 15}));
|
||||
matches(output_schema_for::<DrawingInserted>(), &r);
|
||||
assert_eq!((r["curves"].as_u64(), r["texts"].as_u64()), (Some(4), Some(1)), "{r}");
|
||||
let r = run(&mut s, "sketch.insert_svg", json!({"path": svg_path, "width": 40, "align": "center", "at": [10, 10]}));
|
||||
assert_eq!(r["texts"], 1, "{r}");
|
||||
s.execute("sketch.finish", &json!({})).unwrap();
|
||||
let r = run(&mut s, "sketch.insert_svg", json!({"path": svg_path, "plane": "YZ"}));
|
||||
matches(output_schema_for::<DrawingInserted>(), &r);
|
||||
assert_ne!(r["created_sketch"], Value::Null, "{r}");
|
||||
s.execute("sketch.finish", &json!({})).unwrap();
|
||||
|
||||
// Export: the text, or a file.
|
||||
let r = run(&mut s, "sketch.export_dxf", json!({"sketch": id}));
|
||||
matches(output_schema_for::<ExportDxfResult>(), &r);
|
||||
assert_eq!(keys(&r), ["dxf", "entities"].map(String::from).into(), "{r}");
|
||||
assert!(r["dxf"].as_str().is_some_and(|t| t.contains("ENTITIES")), "{r}");
|
||||
assert!(r["entities"].as_u64().is_some_and(|n| n >= 4), "{r}");
|
||||
let out = dir.join("out.dxf");
|
||||
let out_path = out.to_string_lossy().into_owned();
|
||||
let r = run(&mut s, "sketch.export_dxf", json!({"sketch": id, "path": out_path}));
|
||||
matches(output_schema_for::<ExportDxfResult>(), &r);
|
||||
assert_eq!(r["path"], json!(out_path), "{r}");
|
||||
assert_eq!(keys(&r), ["entities", "path"].map(String::from).into(), "{r}");
|
||||
assert!(std::fs::read_to_string(&out).unwrap().contains("ENTITIES"));
|
||||
// A blank `path` and a null `sketch` read as left out.
|
||||
s.execute("sketch.edit", &json!({"sketch": id})).unwrap();
|
||||
let r = run(&mut s, "sketch.export_dxf", json!({"sketch": null, "path": " "}));
|
||||
assert!(r.get("dxf").is_some(), "{r}");
|
||||
matches(output_schema_for::<ExportDxfResult>(), &r);
|
||||
// The units and layer options, in any case; null and a blank layer read as left out.
|
||||
for p in [
|
||||
json!({"units": "IN", "layer": "PART"}),
|
||||
json!({"units": "design", "layers": "Roles"}),
|
||||
json!({"join": true, "layer": " "}),
|
||||
json!({"units": null, "layer": null, "layers": null, "join": null}),
|
||||
] {
|
||||
let r = run(&mut s, "sketch.export_dxf", p);
|
||||
matches(output_schema_for::<ExportDxfResult>(), &r);
|
||||
assert_eq!(keys(&r), ["dxf", "entities"].map(String::from).into(), "{r}");
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
|
||||
// A misspelt key runs in warn and says so; the reply still matches. Strict refuses it.
|
||||
let p = json!({"text": DXF, "wdth": 5});
|
||||
let r = s.execute("sketch.insert_dxf", &p).unwrap();
|
||||
assert!(r["param_warnings"].as_array().is_some_and(|w| w.iter().any(|m| m.as_str().is_some_and(|m| m.contains("`wdth`")))), "{r}");
|
||||
matches(output_schema_for::<DrawingInserted>(), &r);
|
||||
let r = s.execute("sketch.export_dxf", &json!({"pth": "a.dxf"})).unwrap();
|
||||
assert!(r["param_warnings"].as_array().is_some_and(|w| !w.is_empty()), "{r}");
|
||||
matches(output_schema_for::<ExportDxfResult>(), &r);
|
||||
let r = s.execute("sketch.insert_svg", &json!({"text": SVG, "angel": 5})).unwrap();
|
||||
assert!(r["param_warnings"].as_array().is_some_and(|w| !w.is_empty()), "{r}");
|
||||
s.param_check = ParamCheck::Strict;
|
||||
let error = s.execute("sketch.insert_dxf", &p).err().map(|e| e.to_string()).unwrap_or_default();
|
||||
assert!(error.contains("`wdth`") && error.contains("did you mean `width`"), "{error}");
|
||||
}
|
||||
@@ -67,9 +67,6 @@ sketch.dimension_text
|
||||
sketch.drag
|
||||
sketch.drag_end
|
||||
sketch.draw.dimensioned
|
||||
sketch.export_dxf
|
||||
sketch.insert_dxf
|
||||
sketch.insert_svg
|
||||
sketch.move_point
|
||||
sketch.options
|
||||
sketch.polygon.circumscribed
|
||||
|
||||
Reference in New Issue
Block a user