geop_ops_parts/operation/schema.rs
1//! A description of every operation and its arguments, for whatever edits
2//! programs: a UI builds the form for an operation from its
3//! [`OperationSchema`] rather than knowing the operation by hand, so a new
4//! operation shows up in every editor without touching any of them.
5//!
6//! Written by `#[derive(OperationArgs)]` / `#[derive(Operations)]` from
7//! each argument's `#[arg(...)]` and doc comment.
8
9use geop_core_part::DatumKind;
10use serde::Serialize;
11
12use super::entity::Role;
13
14/// What kind of value an argument holds — and so how an editor lets a user
15/// enter it.
16#[derive(Clone, Debug, PartialEq, Serialize)]
17#[serde(tag = "type", rename_all = "snake_case")]
18pub enum ArgKind {
19 /// A real number: a length, a distance. `min`/`max` bound what a slider
20 /// offers, not what is valid.
21 Number {
22 default: f64,
23 min: f64,
24 max: f64,
25 },
26 Bool {
27 default: bool,
28 },
29 /// The name of a solid of the part — picked in the viewport.
30 Solid,
31 /// The name of a face of the part — picked in the viewport.
32 Face,
33 /// The name of a sketch of the part.
34 Sketch,
35 /// The id of a line of the sketch named by the argument `sketch`.
36 SketchLine {
37 sketch: &'static str,
38 },
39 /// One of a fixed set of values.
40 Choice {
41 options: &'static [&'static str],
42 default: &'static str,
43 },
44 /// A plane to sketch on: a base plane, a planar face or a datum plane,
45 /// picked in the viewport (see [`crate::EntityRef`]).
46 Plane,
47 /// Entities to build on — vertices, edges, faces, datums, the origin,
48 /// world axes and base planes — picked in the viewport, in order (see
49 /// [`crate::EntityRef`]).
50 Selection,
51 /// How to build a datum from the entities of the argument `selection`:
52 /// one of `options`, each of which fits only some selections (see
53 /// [`crate::operation::inspect_selection`]).
54 Construction {
55 selection: &'static str,
56 options: &'static [ConstructionSchema],
57 },
58 /// Sketch geometry, drawn on the plane given by the argument `plane`.
59 Drawing {
60 plane: &'static str,
61 },
62 /// A new body, or a boolean with a target solid (see
63 /// [`crate::operation::Combine`]): a choice of mode, and the target
64 /// picked in the viewport. `sign` names the number argument whose sign
65 /// picks the mode until the user chooses one: join when it is
66 /// positive, cut when it is negative — an extrude up out of a face
67 /// adds material, one down into it removes some.
68 Combine {
69 sign: Option<&'static str>,
70 },
71}
72
73/// One argument of an operation.
74#[derive(Clone, Debug, PartialEq, Serialize)]
75pub struct ArgSchema {
76 /// The field name, as the argument serializes.
77 pub name: &'static str,
78 pub doc: &'static str,
79 pub kind: ArgKind,
80}
81
82/// One way to build a datum (see [`crate::Construction`]): what it builds,
83/// what it needs selected — one entity per input, in any order — and the
84/// values it takes besides.
85#[derive(Clone, Debug, PartialEq, Serialize)]
86pub struct ConstructionSchema {
87 /// How the construction is spelled: `offset`.
88 pub method: &'static str,
89 pub label: &'static str,
90 pub doc: &'static str,
91 pub result: DatumKind,
92 pub inputs: &'static [Role],
93 pub params: &'static [ArgSchema],
94}
95
96/// One operation: its kind (as a program step spells it), a short label,
97/// what it does, and its arguments.
98#[derive(Clone, Debug, PartialEq, Serialize)]
99pub struct OperationSchema {
100 pub kind: &'static str,
101 pub label: &'static str,
102 pub doc: &'static str,
103 pub args: Vec<ArgSchema>,
104}
105
106/// An operation's arguments, described field by field — implemented by
107/// `#[derive(OperationArgs)]`.
108pub trait OperationArgs {
109 fn schema() -> Vec<ArgSchema>;
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use crate::PartOperation;
116
117 /// Every operation is described, with its arguments in declaration
118 /// order and their doc comments.
119 #[test]
120 fn schemas_describe_every_operation() {
121 let schemas = PartOperation::schemas();
122 let kinds: Vec<&str> = schemas.iter().map(|s| s.kind).collect();
123 assert_eq!(
124 kinds,
125 ["add_sketch", "extrude", "revolve", "boolean", "add_datum"]
126 );
127 let extrude = &schemas[1];
128 assert_eq!(extrude.label, "Extrude");
129 let args: Vec<&str> = extrude.args.iter().map(|a| a.name).collect();
130 assert_eq!(args, ["sketch", "distance", "symmetric", "combine"]);
131 assert_eq!(
132 extrude.args[3].kind,
133 ArgKind::Combine {
134 sign: Some("distance")
135 }
136 );
137 assert_eq!(extrude.args[0].kind, ArgKind::Sketch);
138 assert!(extrude.args[1].doc.starts_with("How far"));
139 assert_eq!(schemas[0].label, "Sketch");
140 serde_json::to_string(&schemas).unwrap();
141 }
142
143 /// The kind a schema gives an operation is the tag it serializes under,
144 /// so an editor can build steps from schemas alone.
145 #[test]
146 fn schema_kinds_are_the_serialized_tags() {
147 let kinds: Vec<&str> = PartOperation::schemas().iter().map(|s| s.kind).collect();
148 for (_, program) in crate::examples::all() {
149 for step in &program.steps {
150 let json = serde_json::to_value(&step.operation).unwrap();
151 assert_eq!(json["operation"], step.operation.kind());
152 assert!(kinds.contains(&step.operation.kind()));
153 }
154 }
155 }
156}