brep_kernel/feature_pipeline/features/
nurbs_solid.rs1use crate::feature_pipeline::features::common;
20use crate::feature_pipeline::{Env, FeatureContext, FeatureResult};
21use crate::{CagePrimitive, ControlCage, MAX_RINGS, MAX_SEGMENTS, MIN_RINGS, MIN_SEGMENTS};
22use serde_json::Value;
23
24pub const PERSISTENT_KEY: &str = "cage";
26pub const DEFAULT_PRIMITIVE: &str = "BOX";
28pub const DEFAULT_SIZE: f64 = 10.0;
29pub const DEFAULT_SEGMENTS: usize = 8;
30pub const DEFAULT_RINGS: usize = 3;
31
32pub fn execute(ctx: &FeatureContext) -> FeatureResult {
33 match build(ctx) {
34 Ok(result) => result,
35 Err(error) => ctx.fail(error),
36 }
37}
38
39fn build(ctx: &FeatureContext) -> Result<FeatureResult, String> {
40 let cage = resolve_cage(ctx.env, ctx.params, ctx.persistent)?;
41 let solid = cage.solid(Some(ctx.id.as_str()))?;
42 Ok(common::finalize_solid(ctx, solid, &ctx.id))
43}
44
45pub fn has_persisted_cage(persistent: &Value) -> bool {
48 persistent.get(PERSISTENT_KEY).is_some_and(|cage| !cage.is_null())
49}
50
51pub fn resolve_cage(env: &Env, params: &Value, persistent: &Value) -> Result<ControlCage, String> {
55 if let Some(stored) = persistent.get(PERSISTENT_KEY).filter(|cage| !cage.is_null()) {
56 return ControlCage::from_json(stored)
57 .map_err(|error| format!("nurbs solid: the stored cage is unusable: {error}"));
58 }
59 primitive_cage(env, params)
60}
61
62pub fn primitive_cage(env: &Env, params: &Value) -> Result<ControlCage, String> {
65 let kind_name = params
66 .get("primitive")
67 .and_then(Value::as_str)
68 .unwrap_or(DEFAULT_PRIMITIVE);
69 let kind = CagePrimitive::parse(kind_name)
70 .ok_or_else(|| format!("nurbs solid: unknown primitive `{kind_name}` (BOX, CYLINDER or SPHERE)"))?;
71 let size = [
72 number(env, params, "sizeX", DEFAULT_SIZE)?,
73 number(env, params, "sizeY", DEFAULT_SIZE)?,
74 number(env, params, "sizeZ", DEFAULT_SIZE)?,
75 ];
76 let count = |key: &str, default: usize, low: usize, high: usize| -> Result<usize, String> {
77 let value = number(env, params, key, default as f64)?;
78 let rounded = value.round();
79 if rounded < low as f64 || rounded > high as f64 {
80 return Err(format!("nurbs solid: `{key}` must be between {low} and {high}, got {value}"));
81 }
82 Ok(rounded as usize)
83 };
84 let segments = count("segments", DEFAULT_SEGMENTS, MIN_SEGMENTS, MAX_SEGMENTS)?;
85 let rings = count("rings", DEFAULT_RINGS, MIN_RINGS, MAX_RINGS)?;
86 ControlCage::primitive(kind, size, segments, rings).map_err(|error| format!("nurbs solid: {error}"))
87}
88
89fn number(env: &Env, params: &Value, key: &str, default: f64) -> Result<f64, String> {
92 let value = match params.get(key) {
93 None | Some(Value::Null) => return Ok(default),
94 Some(Value::Number(number)) => number
95 .as_f64()
96 .ok_or_else(|| format!("nurbs solid: param `{key}` is not a finite number"))?,
97 Some(Value::String(source)) => env
98 .eval(source)
99 .map_err(|error| format!("nurbs solid: param `{key}`: {error}"))?,
100 Some(other) => {
101 return Err(format!(
102 "nurbs solid: param `{key}` must be a number or expression string, found {other}"
103 ))
104 }
105 };
106 if !value.is_finite() {
107 return Err(format!("nurbs solid: param `{key}` evaluated to {value}"));
108 }
109 Ok(value)
110}
111
112pub fn context_applicable(_probe: &crate::feature_pipeline::SelectionProbe) -> bool {
115 false
116}
117
118pub fn schema() -> serde_json::Value {
119 serde_json::json!({
120 "type": "NS",
121 "shortName": "NS",
122 "longName": "NURBS Solid",
123 "ribbonPath": "Home/Primitives/NURBS Solid",
124 "commandSize": "Compact",
125 "displayBuilder": false,
126 "inputParamsSchema": {
127 "id": {
128 "type": "string",
129 "default_value": null,
130 "hint": "Unique identifier for the feature"
131 },
132 "primitive": {
133 "type": "options",
134 "label": "Primitive",
135 "options": CagePrimitive::OPTIONS,
136 "default_value": DEFAULT_PRIMITIVE,
137 "hint": "The primitive the control cage starts as; the smooth body lies inside the cage"
138 },
139 "sizeX": {
140 "type": "number",
141 "default_value": DEFAULT_SIZE,
142 "hint": "Cage extent along X (centred on the origin)"
143 },
144 "sizeY": {
145 "type": "number",
146 "default_value": DEFAULT_SIZE,
147 "hint": "Cage extent along Y, the pole-to-pole axis"
148 },
149 "sizeZ": {
150 "type": "number",
151 "default_value": DEFAULT_SIZE,
152 "hint": "Cage extent along Z"
153 },
154 "segments": {
155 "type": "number",
156 "default_value": DEFAULT_SEGMENTS,
157 "min": MIN_SEGMENTS,
158 "step": 1,
159 "hint": "Control points around each ring of the initial cage (at least 4)"
160 },
161 "rings": {
162 "type": "number",
163 "default_value": DEFAULT_RINGS,
164 "min": MIN_RINGS,
165 "step": 1,
166 "hint": "Rings of control points between the two poles of the initial cage (at least 2)"
167 },
168 "boolean": common::optional_boolean_schema()
169 }
170})
171}
172