mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 02:25:55 +00:00
* claim: #5641 * Cone primitive: solid.cone with base and top radius (#5641) A cone or truncated cone placed like the cylinder: base centre, axis, base radius (or diameter), top radius (0, the default, is a full cone) and height, with the new body / join / cut / intersect choice. It is built from what the kernel already exposes, a revolve of its half section about +Z through the origin, moved into place. Toolbar entry after Torus, a Cone dialog that also places it on a picked face, Edit Feature, a timeline icon, pending UI strings and attribution, and one pending design-format change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -63,6 +63,7 @@ combine false
|
||||
compare false
|
||||
component false
|
||||
compute false
|
||||
cone false
|
||||
configuration_table false
|
||||
conic false
|
||||
constrainer false
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Cone</title>
|
||||
<path d="M32 5L12.7 45.97A20 11.55 0 1 0 51.3 45.97Z" fill="#328bd3"/>
|
||||
<path d="M32 5L39 59.82A20 11.55 0 0 0 51.3 45.97Z" fill="#236ead"/>
|
||||
<path d="M32 5L12.7 45.97A20 11.55 0 1 0 51.3 45.97Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 388 B |
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"en": {
|
||||
"ui.cone": "Cone",
|
||||
"ui.heading_cone": "CONE"
|
||||
},
|
||||
"ja": {
|
||||
"ui.cone": "円錐",
|
||||
"ui.heading_cone": "円錐"
|
||||
},
|
||||
"zh": {
|
||||
"ui.cone": "圆锥体",
|
||||
"ui.heading_cone": "圆锥体"
|
||||
},
|
||||
"cs": {
|
||||
"ui.cone": "Kužel",
|
||||
"ui.heading_cone": "KUŽEL"
|
||||
},
|
||||
"da": {
|
||||
"ui.cone": "Kegle",
|
||||
"ui.heading_cone": "KEGLE"
|
||||
},
|
||||
"de": {
|
||||
"ui.cone": "Kegel",
|
||||
"ui.heading_cone": "KEGEL"
|
||||
},
|
||||
"es": {
|
||||
"ui.cone": "Cono",
|
||||
"ui.heading_cone": "CONO"
|
||||
},
|
||||
"fr": {
|
||||
"ui.cone": "Cône",
|
||||
"ui.heading_cone": "CÔNE"
|
||||
},
|
||||
"gl": {
|
||||
"ui.cone": "Cono",
|
||||
"ui.heading_cone": "CONO"
|
||||
},
|
||||
"hu": {
|
||||
"ui.cone": "Kúp",
|
||||
"ui.heading_cone": "KÚP"
|
||||
},
|
||||
"id": {
|
||||
"ui.cone": "Kerucut",
|
||||
"ui.heading_cone": "KERUCUT"
|
||||
},
|
||||
"it": {
|
||||
"ui.cone": "Cono",
|
||||
"ui.heading_cone": "CONO"
|
||||
},
|
||||
"ko": {
|
||||
"ui.cone": "원뿔",
|
||||
"ui.heading_cone": "원뿔"
|
||||
},
|
||||
"ms": {
|
||||
"ui.cone": "Kon",
|
||||
"ui.heading_cone": "KON"
|
||||
},
|
||||
"nl": {
|
||||
"ui.cone": "Kegel",
|
||||
"ui.heading_cone": "KEGEL"
|
||||
},
|
||||
"pl": {
|
||||
"ui.cone": "Stożek",
|
||||
"ui.heading_cone": "STOŻEK"
|
||||
},
|
||||
"pt-BR": {
|
||||
"ui.cone": "Cone",
|
||||
"ui.heading_cone": "CONE"
|
||||
},
|
||||
"ro": {
|
||||
"ui.cone": "Con",
|
||||
"ui.heading_cone": "CON"
|
||||
},
|
||||
"ru": {
|
||||
"ui.cone": "Конус",
|
||||
"ui.heading_cone": "КОНУС"
|
||||
},
|
||||
"sv": {
|
||||
"ui.cone": "Kon",
|
||||
"ui.heading_cone": "KON"
|
||||
},
|
||||
"tr": {
|
||||
"ui.cone": "Koni",
|
||||
"ui.heading_cone": "KONİ"
|
||||
},
|
||||
"uk": {
|
||||
"ui.cone": "Конус",
|
||||
"ui.heading_cone": "КОНУС"
|
||||
},
|
||||
"vi": {
|
||||
"ui.cone": "Hình nón",
|
||||
"ui.heading_cone": "HÌNH NÓN"
|
||||
},
|
||||
"zh-TW": {
|
||||
"ui.cone": "圓錐體",
|
||||
"ui.heading_cone": "圓錐體"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
| File | Author | Source | Licence |
|
||||
| --- | --- | --- | --- |
|
||||
| assets/icons/ui/design/svg/model/cone.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
@@ -714,6 +714,23 @@ pub enum FeatureKind {
|
||||
#[serde(default)]
|
||||
operation: Operation,
|
||||
},
|
||||
/// A cone or a truncated cone (#5641): the base circle's centre `base`, its `radius`, the
|
||||
/// radius of the top circle `height` along `axis` (`top_radius`, 0 for a full cone).
|
||||
Cone {
|
||||
#[serde(with = "point_expr")]
|
||||
#[walk(placement)]
|
||||
base: PointExpr,
|
||||
#[serde(default = "z_axis")]
|
||||
axis: Dir,
|
||||
#[walk(length = "Radius")]
|
||||
radius: Expr,
|
||||
#[walk(length = "Top radius")]
|
||||
top_radius: Expr,
|
||||
#[walk(length = "Height")]
|
||||
height: Expr,
|
||||
#[serde(default)]
|
||||
operation: Operation,
|
||||
},
|
||||
Combine {
|
||||
target: BodyName,
|
||||
tools: Vec<BodyName>,
|
||||
@@ -2315,6 +2332,7 @@ impl FeatureKind {
|
||||
FeatureKind::Cylinder { .. } => "CylinderFeature",
|
||||
FeatureKind::Sphere { .. } => "SphereFeature",
|
||||
FeatureKind::Torus { .. } => "TorusFeature",
|
||||
FeatureKind::Cone { .. } => "ConeFeature",
|
||||
FeatureKind::Combine { .. } => "CombineFeature",
|
||||
FeatureKind::Pattern { pattern: PatternKind::Rectangular { .. }, .. } => "RectangularPatternFeature",
|
||||
FeatureKind::Pattern { pattern: PatternKind::Path { .. }, .. } => "PathPatternFeature",
|
||||
@@ -2397,6 +2415,7 @@ impl FeatureKind {
|
||||
FeatureKind::Cylinder { .. } => "Cylinder",
|
||||
FeatureKind::Sphere { .. } => "Sphere",
|
||||
FeatureKind::Torus { .. } => "Torus",
|
||||
FeatureKind::Cone { .. } => "Cone",
|
||||
FeatureKind::Combine { .. } => "Combine",
|
||||
FeatureKind::Pattern { pattern: PatternKind::Rectangular { .. }, .. } => "RectangularPattern",
|
||||
FeatureKind::Pattern { pattern: PatternKind::Path { .. }, .. } => "PathPattern",
|
||||
|
||||
+17
-1
@@ -2672,6 +2672,7 @@ fn note_join_apart(before: &ModelState, after: &ModelState, f: &Feature, warning
|
||||
| FeatureKind::Cylinder { operation, .. }
|
||||
| FeatureKind::Sphere { operation, .. }
|
||||
| FeatureKind::Torus { operation, .. }
|
||||
| FeatureKind::Cone { operation, .. }
|
||||
| FeatureKind::Loft { operation, .. }
|
||||
| FeatureKind::Sweep { operation, .. }
|
||||
| FeatureKind::Pipe { operation, .. }
|
||||
@@ -5727,6 +5728,13 @@ fn feature_tools_with_constructor(
|
||||
FeatureKind::Torus { center, major, minor, .. } => {
|
||||
Ok(unnamed(vec![kernel::torus(point(vals, center)?, val(vals, major, Kind::Length)?, val(vals, minor, Kind::Length)?)?]))
|
||||
}
|
||||
FeatureKind::Cone { base, axis, radius, top_radius, height, .. } => Ok(unnamed(vec![more::cone(
|
||||
point(vals, base)?,
|
||||
axis.0,
|
||||
val(vals, radius, Kind::Length)?,
|
||||
val(vals, top_radius, Kind::Length)?,
|
||||
val(vals, height, Kind::Length)?,
|
||||
)?])),
|
||||
FeatureKind::Hole { .. } => Ok(hole_feature_tools(vals, st, f)?.into_iter().map(|(tool, _)| tool).collect()),
|
||||
_ => Err(DocError::Invalid(format!("{} cannot be patterned or mirrored yet", f.name))),
|
||||
}
|
||||
@@ -6021,7 +6029,8 @@ fn feature_op(f: &Feature) -> (Operation, Vec<String>) {
|
||||
FeatureKind::Box { operation, .. }
|
||||
| FeatureKind::Cylinder { operation, .. }
|
||||
| FeatureKind::Sphere { operation, .. }
|
||||
| FeatureKind::Torus { operation, .. } => (*operation, Vec::new()),
|
||||
| FeatureKind::Torus { operation, .. }
|
||||
| FeatureKind::Cone { operation, .. } => (*operation, Vec::new()),
|
||||
FeatureKind::Hole { .. } => (Operation::Cut, Vec::new()),
|
||||
FeatureKind::Pipe { operation, targets, .. } => (*operation, crate::plain(targets)),
|
||||
FeatureKind::Loft { operation, targets, .. } | FeatureKind::Sweep { operation, targets, .. } => (*operation, crate::plain(targets)),
|
||||
@@ -7605,6 +7614,12 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
let b = kernel::torus(point(vals, center)?, val(vals, major, Kind::Length)?, val(vals, minor, Kind::Length)?)?;
|
||||
apply_op(doc, st, f, vec![b], *operation, &[])
|
||||
}
|
||||
FeatureKind::Cone { base, axis, radius, top_radius, height, operation } => {
|
||||
let (radius, top_radius, height) =
|
||||
(val(vals, radius, Kind::Length)?, val(vals, top_radius, Kind::Length)?, val(vals, height, Kind::Length)?);
|
||||
let b = more::cone(point(vals, base)?, axis.0, radius, top_radius, height)?;
|
||||
apply_op(doc, st, f, vec![b], *operation, &[])
|
||||
}
|
||||
FeatureKind::Combine { target, tools, operation, keep_tools } => {
|
||||
if tools.iter().any(|t| t == target) {
|
||||
return Err(DocError::Invalid("a body cannot be combined with itself".into()));
|
||||
@@ -7940,6 +7955,7 @@ fn drawn_tools_change_bodies(kind: &FeatureKind) -> bool {
|
||||
| FeatureKind::Cylinder { operation, .. }
|
||||
| FeatureKind::Sphere { operation, .. }
|
||||
| FeatureKind::Torus { operation, .. }
|
||||
| FeatureKind::Cone { operation, .. }
|
||||
| FeatureKind::Extrude { operation, .. }
|
||||
| FeatureKind::Revolve { operation, .. }
|
||||
| FeatureKind::Loft { operation, .. }
|
||||
|
||||
@@ -1168,6 +1168,57 @@ pub(super) fn sheet_drill_cuts(
|
||||
Ok(Some(handled))
|
||||
}
|
||||
|
||||
/// The largest size a cone takes (mm): beyond it the tessellation and booleans lose precision.
|
||||
const MAX_CONE_SIZE: f64 = 1e7;
|
||||
|
||||
/// A cone or truncated cone (#5641): the base circle of `radius` centred on `base`, the top circle
|
||||
/// of `top_radius` `height` along `axis` (a negative height goes the other way). A top radius of
|
||||
/// 0 (or below a micrometre) makes a full cone ending in an apex. Built, as the torus is, by
|
||||
/// revolving its half section, a trapezoid (a triangle for a full cone) with an edge on the axis.
|
||||
pub(super) fn cone(base: Vec3, axis: Vec3, radius: f64, top_radius: f64, height: f64) -> Result<Body> {
|
||||
let in_range = |v: f64| v.is_finite() && v.abs() <= MAX_CONE_SIZE;
|
||||
if !(in_range(base.x) && in_range(base.y) && in_range(base.z)) {
|
||||
return Err(DocError::Invalid("the cone's base is out of range".into()));
|
||||
}
|
||||
if !(in_range(radius) && in_range(top_radius) && in_range(height)) {
|
||||
return Err(DocError::Invalid(format!("cone sizes must be finite and within {MAX_CONE_SIZE} mm")));
|
||||
}
|
||||
if !(radius > 1e-6) {
|
||||
return Err(DocError::Invalid("the cone's radius must be positive".into()));
|
||||
}
|
||||
if top_radius < 0.0 {
|
||||
return Err(DocError::Invalid("the cone's top radius must be 0 or more".into()));
|
||||
}
|
||||
if !(height.abs() > 1e-6) {
|
||||
return Err(DocError::Invalid("the cone's height must not be zero".into()));
|
||||
}
|
||||
let dir = axis.normalized().ok_or_else(|| DocError::Invalid("the cone's axis has no direction".into()))?;
|
||||
let (dir, h) = if height < 0.0 { (-dir, -height) } else { (dir, height) };
|
||||
// The half section in the XZ plane, the axis along +Z through the origin, revolved there
|
||||
// (where an apex on the axis comes out exact; built in a slanted frame directly, its side
|
||||
// surface measured too large) and then moved into place.
|
||||
let section = if top_radius > 1e-6 {
|
||||
vec![Vec2::ZERO, Vec2::new(radius, 0.0), Vec2::new(top_radius, h), Vec2::new(0.0, h)]
|
||||
} else {
|
||||
vec![Vec2::ZERO, Vec2::new(radius, 0.0), Vec2::new(0.0, h)]
|
||||
};
|
||||
let plane = Plane { origin: Vec3::ZERO, x: Vec3::X, y: Vec3::Z };
|
||||
let region = Region2 { outer: Loop2::polygon(§ion), holes: vec![] };
|
||||
let mut bodies = kernel::revolve(&plane, &[region], Vec2::ZERO, Vec2::Y, std::f64::consts::TAU)?;
|
||||
let body = match (bodies.pop(), bodies.is_empty()) {
|
||||
(Some(b), true) => b,
|
||||
_ => return Err(DocError::Invalid("the cone could not be built".into())),
|
||||
};
|
||||
if (dir - Vec3::Z).len() < 1e-12 && base.len() == 0.0 {
|
||||
return Ok(body);
|
||||
}
|
||||
// A rotation taking +Z onto the axis (x and y square to it, right-handed), then the base.
|
||||
let x = Plane::from_normal(base, dir).ok_or_else(|| DocError::Invalid("the cone's axis has no direction".into()))?.x;
|
||||
let y = dir.cross(x);
|
||||
let m = [[x.x, x.y, x.z, 0.0], [y.x, y.y, y.z, 0.0], [dir.x, dir.y, dir.z, 0.0], [base.x, base.y, base.z, 1.0]];
|
||||
Ok(kernel::transform_matrix(&body, m)?)
|
||||
}
|
||||
|
||||
/// A primitive tool's exact geometry, for [`sheet_primitive_cut`].
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(super) enum PrimitivePrism {
|
||||
|
||||
@@ -58,6 +58,10 @@ pub(super) fn fixtures() -> Vec<(&'static str, Value)> {
|
||||
("cylinder", json!({"type": "cylinder", "base": ["cyl.x", "cyl.y", "cyl.z"], "radius": "cyl.radius", "height": "cyl.height"})),
|
||||
("sphere", json!({"type": "sphere", "center": ["sph.x", "sph.y", "sph.z"], "radius": "sph.radius"})),
|
||||
("torus", json!({"type": "torus", "center": ["tor.x", "tor.y", "tor.z"], "major": "tor.major", "minor": "tor.minor"})),
|
||||
(
|
||||
"cone",
|
||||
json!({"type": "cone", "base": ["cone.x", "cone.y", "cone.z"], "radius": "cone.radius", "top_radius": "cone.top_radius", "height": "cone.height"}),
|
||||
),
|
||||
("combine", json!({"type": "combine", "target": "A", "tools": ["B"], "operation": "join"})),
|
||||
(
|
||||
"pattern_rectangular",
|
||||
|
||||
@@ -82,6 +82,12 @@ torus
|
||||
Major radius = tor.major (Length)
|
||||
Minor radius = tor.minor (Length)
|
||||
expressions: tor.major, tor.minor, tor.x, tor.y, tor.z
|
||||
cone
|
||||
inputs:
|
||||
Radius = cone.radius (Length)
|
||||
Top radius = cone.top_radius (Length)
|
||||
Height = cone.height (Length)
|
||||
expressions: cone.radius, cone.top_radius, cone.height, cone.x, cone.y, cone.z
|
||||
combine
|
||||
pattern_rectangular
|
||||
inputs:
|
||||
|
||||
@@ -36,6 +36,9 @@ sphere
|
||||
torus
|
||||
moves: center[0], center[1], center[2]
|
||||
placement: tor.x, tor.y, tor.z
|
||||
cone
|
||||
moves: axis[0], axis[2], base[0], base[1], base[2]
|
||||
placement: cone.x, cone.y, cone.z
|
||||
combine
|
||||
pattern_rectangular
|
||||
moves: pattern.dir1[0], pattern.dir1[1], pattern.dir2[1], pattern.dir2[2]
|
||||
|
||||
@@ -31,6 +31,7 @@ box
|
||||
cylinder
|
||||
sphere
|
||||
torus
|
||||
cone
|
||||
combine
|
||||
remap_bodies: target, tools[0]
|
||||
pattern_rectangular
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
//! A design can hold cones (#5641): a `cone` feature with the centre of its base circle (`base`,
|
||||
//! numbers in mm or length expressions), its `axis` (default +Z), the base `radius`, the
|
||||
//! `top_radius` (0: a full cone ending in an apex) and the `height` along the axis, and its
|
||||
//! `operation` (new body, join, cut or intersect) like the other primitives. Older designs need no
|
||||
//! transformation: they have no such feature. The last release refuses a design saved with this
|
||||
//! (the newer format number).
|
||||
@@ -402,6 +402,7 @@ fn geometric_from(b: &ModelBody, o: &NamingOrigin, nf: usize, names_of: fn(&Mode
|
||||
| FeatureKind::Cylinder { operation: crate::Operation::NewBody, .. }
|
||||
| FeatureKind::Sphere { operation: crate::Operation::NewBody, .. }
|
||||
| FeatureKind::Torus { operation: crate::Operation::NewBody, .. }
|
||||
| FeatureKind::Cone { operation: crate::Operation::NewBody, .. }
|
||||
| FeatureKind::Extrude { operation: crate::Operation::NewBody, .. }
|
||||
| FeatureKind::Revolve { operation: crate::Operation::NewBody, .. }
|
||||
);
|
||||
|
||||
@@ -44,6 +44,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("solid.cylinder", "Cylinder", prim_cylinder).at("SOLID", "CREATE").icon("cylinder").params("radius | diameter, height: expr; base?: [x,y,z] (centre of the bottom: numbers in mm or length expressions; default: the origin); axis?: X|Y|Z|[x,y,z] (default Z); operation?, targets?: [body], name?, body_name?"),
|
||||
CommandSpec::new("solid.sphere", "Sphere", prim_sphere).at("SOLID", "CREATE").icon("sphere").params("radius | diameter: expr; center?: [x,y,z] (numbers in mm or length expressions; default: the origin); operation?, targets?: [body], name?, body_name?"),
|
||||
CommandSpec::new("solid.torus", "Torus", prim_torus).at("SOLID", "CREATE").icon("torus").params("major, minor: expr (radii); center?: [x,y,z] (numbers in mm or length expressions; default: the origin); operation?, targets?: [body], name?, body_name?"),
|
||||
CommandSpec::new("solid.cone", "Cone", prim_cone).at("SOLID", "CREATE").icon("cone").params("radius | diameter, height: expr (the base circle, and the top's distance along the axis; a negative height goes the other way); top_radius? | top_diameter?: expr (the top circle; default 0: a full cone ending in an apex); base?: [x,y,z] (centre of the base circle: numbers in mm or length expressions; default: the origin); axis?: X|Y|Z|[x,y,z] (default Z); operation?, targets?: [body], name?, body_name?"),
|
||||
CommandSpec::new("solid.fillet", "Fillet", fillet)
|
||||
.at("SOLID", "MODIFY")
|
||||
.icon("fillet")
|
||||
@@ -666,7 +667,10 @@ fn add_feature_in(s: &mut Session, p: &Value, kind: FeatureKind, world: bool) ->
|
||||
}
|
||||
};
|
||||
// Primitives have no `targets` of their own: their `targets` are the bodies taking part (#259).
|
||||
let primitive = matches!(kind, FeatureKind::Box { .. } | FeatureKind::Cylinder { .. } | FeatureKind::Sphere { .. } | FeatureKind::Torus { .. });
|
||||
let primitive = matches!(
|
||||
kind,
|
||||
FeatureKind::Box { .. } | FeatureKind::Cylinder { .. } | FeatureKind::Sphere { .. } | FeatureKind::Torus { .. } | FeatureKind::Cone { .. }
|
||||
);
|
||||
let (participants, key) = match string_list(p, "participants") {
|
||||
v if v.is_empty() && primitive => (string_list(p, "targets"), "targets"),
|
||||
v => (v, "participants"),
|
||||
@@ -1339,6 +1343,32 @@ fn prim_torus(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
add_feature(s, p, FeatureKind::Torus { center, major, minor, operation: operation(p, cmd)? })
|
||||
}
|
||||
|
||||
/// A cone or truncated cone (#5641), placed and sized like a cylinder, with the top circle's
|
||||
/// radius (or diameter) besides; none is a full cone.
|
||||
fn prim_cone(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "solid.cone";
|
||||
let radius = radius_expr(s, p, cmd)?;
|
||||
let top_radius = match (expr(p, "top_radius"), expr(p, "top_diameter")) {
|
||||
(Some(r), _) => r,
|
||||
(None, Some(d)) => {
|
||||
let [d] = length_operands(s, [(&d, &typed_length_unit(s))]);
|
||||
format!("({d}) / 2").into()
|
||||
}
|
||||
// Given but unreadable (a huge number, a list): refused, never a full cone instead.
|
||||
(None, None) if p.get("top_radius").is_some() || p.get("top_diameter").is_some() => {
|
||||
return Err(bad(cmd, "`top_radius` (or `top_diameter`) must be a length expression"));
|
||||
}
|
||||
(None, None) => "0".into(),
|
||||
};
|
||||
let height = req_expr(cmd, p, "height")?;
|
||||
for (e, n) in [(&radius, "radius"), (&top_radius, "top_radius"), (&height, "height")] {
|
||||
check_expr(s, e, Kind::Length, cmd, n)?;
|
||||
}
|
||||
let base = point_param(s, p, "base", cmd)?.unwrap_or_else(|| point_expr(Vec3::ZERO));
|
||||
let axis = axis_dir_param(p, cmd)?;
|
||||
add_feature(s, p, FeatureKind::Cone { base, axis: Dir(axis), radius, top_radius, height, operation: operation(p, cmd)? })
|
||||
}
|
||||
|
||||
/// Edge references → points on the edges.
|
||||
fn edge_points(s: &Session, p: &Value, cmd: &str) -> Result<Vec<Vec3>> {
|
||||
let a = p.get("edges").and_then(Value::as_array).ok_or_else(|| bad(cmd, "`edges` must be a list"))?;
|
||||
|
||||
@@ -54,7 +54,7 @@ fn table(id: &str) -> &'static [(&'static str, K)] {
|
||||
}
|
||||
"solid.revolve" => &[("face", Pick), ("axis", Axis), ("to", Make), ("targets", Body), ("participants", Body)],
|
||||
"solid.box" | "solid.sphere" => &[("corner", Make), ("center", Make)],
|
||||
"solid.cylinder" | "solid.coil" | "solid.torus" => &[("base", Make), ("center", Make), ("axis", Dir)],
|
||||
"solid.cylinder" | "solid.coil" | "solid.torus" | "solid.cone" => &[("base", Make), ("center", Make), ("axis", Dir)],
|
||||
"solid.fillet" | "solid.chamfer" => &[("edges", Picks), ("body", Body)],
|
||||
"solid.hole" => &[("position", Pick), ("direction", Dir), ("body", Body)],
|
||||
"solid.scale" => &[("origin", Make), ("bodies", Body)],
|
||||
|
||||
@@ -774,6 +774,8 @@ fn hostile_params_never_panic() {
|
||||
"base",
|
||||
"major",
|
||||
"minor",
|
||||
"top_radius",
|
||||
"top_diameter",
|
||||
"edges",
|
||||
"body",
|
||||
"target",
|
||||
@@ -3784,7 +3786,7 @@ fn feature_params_docs_list_shared_keys() {
|
||||
let params =
|
||||
|id: &str| cmds.as_array().unwrap().iter().find(|c| c["id"] == id).and_then(|c| c["params"].as_str()).unwrap_or_default().to_string();
|
||||
let with_targets = ["solid.extrude", "solid.revolve", "solid.sweep", "solid.loft", "solid.pipe", "solid.coil"];
|
||||
let without_targets = ["solid.box", "solid.cylinder", "solid.sphere", "solid.torus", "sheet.flange"];
|
||||
let without_targets = ["solid.box", "solid.cylinder", "solid.sphere", "solid.torus", "solid.cone", "sheet.flange"];
|
||||
for id in with_targets.iter().chain(&without_targets) {
|
||||
let doc = params(id);
|
||||
assert!(doc.contains("name?") && doc.contains("body_name?"), "{id}: {doc}");
|
||||
|
||||
@@ -119,3 +119,189 @@ fn point_expressions_in_a_moved_component() {
|
||||
let lo = wlo(&s);
|
||||
assert!((lo.x - 19.0).abs() < 1e-3 && (lo.y - 59.0).abs() < 1e-3, "{lo:?}");
|
||||
}
|
||||
|
||||
// The cone primitive (#5641): sizes from the analytic formulas, placement, operations, hostile
|
||||
// sizes, saving and editing.
|
||||
|
||||
/// V = πh(R² + Rr + r²)/3.
|
||||
fn cone_volume(r: f64, top: f64, h: f64) -> f64 {
|
||||
std::f64::consts::PI * h * (r * r + r * top + top * top) / 3.0
|
||||
}
|
||||
|
||||
/// The base, the top and the side: πR² + πr² + π(R + r)·√(h² + (R − r)²).
|
||||
fn cone_area(r: f64, top: f64, h: f64) -> f64 {
|
||||
std::f64::consts::PI * (r * r + top * top + (r + top) * (h * h + (r - top) * (r - top)).sqrt())
|
||||
}
|
||||
|
||||
fn measured(s: &Session, body: &str) -> solidcraft_kernel::BodyMeasure {
|
||||
solidcraft_kernel::measure(&s.model.state().body(body).map(|b| b.body.clone()).unwrap()).unwrap()
|
||||
}
|
||||
|
||||
fn rel(a: f64, b: f64) -> f64 {
|
||||
(a - b).abs() / b.abs()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cone_and_frustum_match_the_analytic_volume_and_area() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.cone", json!({"radius": 10, "top_radius": 4, "height": 15, "body_name": "Frustum"}));
|
||||
run(&mut s, "solid.cone", json!({"diameter": 12, "height": 9, "base": [40, 0, 0], "body_name": "Cone"}));
|
||||
run(&mut s, "solid.cone", json!({"radius": 5, "top_diameter": 16, "height": 6, "base": [0, 40, 0], "body_name": "Cup"}));
|
||||
for (body, r, top, h) in [("Frustum", 10.0, 4.0, 15.0), ("Cone", 6.0, 0.0, 9.0), ("Cup", 5.0, 8.0, 6.0)] {
|
||||
let m = measured(&s, body);
|
||||
assert!(rel(m.volume, cone_volume(r, top, h)) < 2e-3, "{body}: volume {} vs {}", m.volume, cone_volume(r, top, h));
|
||||
assert!(rel(m.area, cone_area(r, top, h)) < 2e-3, "{body}: area {} vs {}", m.area, cone_area(r, top, h));
|
||||
}
|
||||
// A full cone has its base and its side, a frustum its top too (each one merged surface).
|
||||
let faces = |body: &str| measured(&s, body).merged.faces;
|
||||
assert_eq!((faces("Cone"), faces("Frustum")), (2, 3));
|
||||
let (lo, hi) = bbox(&s, "Cone");
|
||||
assert!(near(lo, [34.0, -6.0, 0.0]) && near(hi, [46.0, 6.0, 9.0]), "{lo:?} {hi:?}");
|
||||
let (lo, hi) = bbox(&s, "Cup");
|
||||
assert!(near(lo, [-8.0, 32.0, 0.0]) && near(hi, [8.0, 48.0, 6.0]), "{lo:?} {hi:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cone_takes_any_axis_base_and_a_negative_height() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.cone", json!({"radius": 10, "height": 20, "base": [5, -3, 2], "axis": "X", "body_name": "AlongX"}));
|
||||
let (lo, hi) = bbox(&s, "AlongX");
|
||||
assert!(near(lo, [5.0, -13.0, -8.0]) && near(hi, [25.0, 7.0, 12.0]), "{lo:?} {hi:?}");
|
||||
// A negative height goes down the axis from the base.
|
||||
run(&mut s, "solid.cone", json!({"radius": 3, "top_radius": 1, "height": -8, "base": [0, 50, 0], "body_name": "Down"}));
|
||||
let (lo, hi) = bbox(&s, "Down");
|
||||
assert!(near(lo, [-3.0, 47.0, -8.0]) && near(hi, [3.0, 53.0, 0.0]), "{lo:?} {hi:?}");
|
||||
// A slanted axis: the apex is the height along it; the volume does not depend on it.
|
||||
let d = [1.0 / 3f64.sqrt(); 3];
|
||||
run(&mut s, "solid.cone", json!({"radius": 4, "height": 12, "base": [100, 0, 0], "axis": [1, 1, 1], "body_name": "Slanted"}));
|
||||
// (Revolved in that frame directly, the apex came out wrong: 3 % too little volume, 11 %
|
||||
// too much area.)
|
||||
let m = measured(&s, "Slanted");
|
||||
assert!(rel(m.volume, cone_volume(4.0, 0.0, 12.0)) < 2e-3, "{}", m.volume);
|
||||
assert!(rel(m.area, cone_area(4.0, 0.0, 12.0)) < 2e-3, "{}", m.area);
|
||||
run(
|
||||
&mut s,
|
||||
"solid.cone",
|
||||
json!({"radius": 4, "top_radius": 2, "height": 12, "base": [0, 0, 0], "axis": [1, 1, 1], "body_name": "SlantedFrustum"}),
|
||||
);
|
||||
let m = measured(&s, "SlantedFrustum");
|
||||
assert!(rel(m.volume, cone_volume(4.0, 2.0, 12.0)) < 2e-3 && rel(m.area, cone_area(4.0, 2.0, 12.0)) < 2e-3, "{m:?}");
|
||||
let (_, hi) = bbox(&s, "Slanted");
|
||||
assert!(hi.iter().zip([100.0, 0.0, 0.0]).zip(d).all(|((h, b), d)| *h >= b + 12.0 * d - 1e-3), "the apex lies inside: {hi:?}");
|
||||
// The axis is followed as the parameters of an expression base move it.
|
||||
run(&mut s, "parameters.change", json!({"name": "lift", "expression": "7 mm"}));
|
||||
run(&mut s, "solid.cone", json!({"radius": 2, "height": 4, "base": [0, 0, "lift"], "axis": "Y", "body_name": "Lifted"}));
|
||||
assert!(near(bbox(&s, "Lifted").0, [-2.0, 0.0, 5.0]), "{:?}", bbox(&s, "Lifted"));
|
||||
run(&mut s, "parameters.change", json!({"name": "lift", "expression": "17 mm"}));
|
||||
assert!(near(bbox(&s, "Lifted").0, [-2.0, 0.0, 15.0]), "{:?}", bbox(&s, "Lifted"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cone_joins_cuts_and_intersects_like_the_other_primitives() {
|
||||
// A 5 mm block with the cone standing on its bottom: the lower half of the cone (radius 6 to
|
||||
// 4) is in it, the upper half (4 to 2) above it.
|
||||
let block = 20.0 * 20.0 * 5.0;
|
||||
let (inside, above) = (cone_volume(6.0, 4.0, 5.0), cone_volume(4.0, 2.0, 5.0));
|
||||
for (operation, want) in [("join", block + above), ("cut", block - inside), ("intersect", inside)] {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 20, "width": 20, "height": 5, "corner": [-10, -10, 0], "body_name": "Block"}));
|
||||
run(&mut s, "solid.cone", json!({"radius": 6, "top_radius": 2, "height": 10, "operation": operation, "targets": ["Block"]}));
|
||||
let state = s.model.state();
|
||||
assert_eq!(state.bodies.len(), 1, "{operation}: {:?}", state.bodies.iter().map(|b| &b.name).collect::<Vec<_>>());
|
||||
let got = solidcraft_kernel::measure(&state.bodies[0].body).unwrap().volume;
|
||||
assert!(rel(got, want) < 2e-3, "{operation}: {got} vs {want}");
|
||||
}
|
||||
// A new body leaves the block alone.
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 20, "width": 20, "height": 5, "corner": [-10, -10, 0], "body_name": "Block"}));
|
||||
run(&mut s, "solid.cone", json!({"radius": 6, "top_radius": 2, "height": 10, "operation": "new_body", "body_name": "Spike"}));
|
||||
assert!(rel(measured(&s, "Block").volume, block) < 1e-9);
|
||||
assert!(rel(measured(&s, "Spike").volume, cone_volume(6.0, 2.0, 10.0)) < 2e-3);
|
||||
}
|
||||
|
||||
/// Sizes a cone cannot have are refused with an error: no panic, no feature added.
|
||||
#[test]
|
||||
fn hostile_cone_sizes_are_refused() {
|
||||
let mut s = Session::default();
|
||||
let cases: Vec<Value> = vec![
|
||||
json!({"radius": 0, "height": 10}),
|
||||
json!({"radius": -5, "height": 10}),
|
||||
json!({"radius": 5, "top_radius": -1, "height": 10}),
|
||||
json!({"radius": 5, "height": 0}),
|
||||
json!({"radius": 5}),
|
||||
json!({"height": 5}),
|
||||
json!({"radius": 1e300, "height": 10}),
|
||||
json!({"radius": 5, "top_radius": 1e300, "height": 10}),
|
||||
json!({"radius": 5, "height": 1e300}),
|
||||
json!({"radius": 5, "height": -1e300}),
|
||||
json!({"radius": "0/0", "height": 10}),
|
||||
json!({"radius": "sqrt(-1)", "height": 10}),
|
||||
json!({"radius": 5, "top_radius": "nope", "height": 10}),
|
||||
json!({"radius": 5, "height": "5 deg"}),
|
||||
json!({"radius": 5, "height": 10, "axis": [0, 0, 0]}),
|
||||
json!({"radius": 5, "height": 10, "axis": [1e-320, 0, 0]}),
|
||||
json!({"radius": 5, "height": 10, "axis": "W"}),
|
||||
json!({"radius": 5, "height": 10, "base": [1e300, 0, 0]}),
|
||||
json!({"radius": 5, "height": 10, "base": [0, "x +", 0]}),
|
||||
json!({"radius": 5, "height": 10, "operation": "explode"}),
|
||||
json!({"radius": 5, "height": 10, "operation": "cut", "targets": ["Nowhere"]}),
|
||||
json!({"radius": [5], "height": {"a": 1}}),
|
||||
json!(null),
|
||||
json!("cone"),
|
||||
];
|
||||
for p in cases {
|
||||
let before = s.doc.features.len();
|
||||
let r = s.execute("solid.cone", &p);
|
||||
assert!(r.is_err(), "{p} should be refused: {r:?} {:?}", s.doc.features.last().map(|f| &f.kind));
|
||||
assert_eq!(s.doc.features.len(), before, "{p} added a feature");
|
||||
}
|
||||
// A parameter that later makes the cone impossible fails the feature, never the program,
|
||||
// and a good value brings it back.
|
||||
run(&mut s, "parameters.change", json!({"name": "rr", "expression": "5 mm"}));
|
||||
let made = run(&mut s, "solid.cone", json!({"radius": "rr", "height": 10, "body_name": "Flex"}));
|
||||
let id = made["feature"].as_u64().unwrap();
|
||||
for bad in ["-5 mm", "0 mm", "1e300 mm"] {
|
||||
let _ = s.execute("parameters.change", &json!({"name": "rr", "expression": bad}));
|
||||
assert!(s.model.result(id).is_none_or(|r| r.error.is_some()) || s.model.state().body("Flex").is_none(), "{bad}");
|
||||
}
|
||||
run(&mut s, "parameters.change", json!({"name": "rr", "expression": "3 mm"}));
|
||||
assert!(rel(measured(&s, "Flex").volume, cone_volume(3.0, 0.0, 10.0)) < 2e-3);
|
||||
}
|
||||
|
||||
/// A cone is saved with its sizes as expressions and opens again as it was; Edit Feature changes
|
||||
/// it in place.
|
||||
#[test]
|
||||
fn cone_saves_reopens_and_edits() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "parameters.change", json!({"name": "tip", "expression": "3 mm"}));
|
||||
run(
|
||||
&mut s,
|
||||
"solid.cone",
|
||||
json!({"radius": 8, "top_radius": "tip", "height": 12, "base": [1, 2, 3], "axis": "Y", "name": "Nozzle", "body_name": "N"}),
|
||||
);
|
||||
let text = s.doc.to_json();
|
||||
let v: Value = serde_json::from_str(&text).unwrap();
|
||||
let f = &v["features"][0];
|
||||
assert_eq!((&f["type"], &f["top_radius"], &f["axis"]), (&json!("cone"), &json!("tip"), &json!([0.0, 1.0, 0.0])), "{f}");
|
||||
let mut s2 = Session::new(solidcraft_doc::Document::from_json(&text).unwrap());
|
||||
assert!(rel(measured(&s2, "N").volume, cone_volume(8.0, 3.0, 12.0)) < 2e-3);
|
||||
assert_eq!(bbox(&s2, "N"), bbox(&s, "N"));
|
||||
// Edit Feature: a full cone, taller, cutting nothing (it is the only body: a new body).
|
||||
run(&mut s2, "timeline.edit", json!({"feature": "Nozzle", "set": {"top_radius": "0", "height": "20"}}));
|
||||
assert!(rel(measured(&s2, "N").volume, cone_volume(8.0, 0.0, 20.0)) < 2e-3);
|
||||
let (lo, hi) = bbox(&s2, "N");
|
||||
assert!(near(lo, [-7.0, 2.0, -5.0]) && near(hi, [9.0, 22.0, 11.0]), "{lo:?} {hi:?}");
|
||||
// Rebuilding it from the command form works too, and undo brings the edit back.
|
||||
run(
|
||||
&mut s2,
|
||||
"timeline.redefine",
|
||||
json!({"feature": "Nozzle", "command": "solid.cone", "params": {"diameter": 10, "top_diameter": 4, "height": 6}}),
|
||||
);
|
||||
assert!(rel(measured(&s2, "N").volume, cone_volume(5.0, 2.0, 6.0)) < 2e-3);
|
||||
run(&mut s2, "edit.undo", json!({}));
|
||||
assert!(rel(measured(&s2, "N").volume, cone_volume(8.0, 0.0, 20.0)) < 2e-3);
|
||||
// The parameter still drives it after the round trip.
|
||||
let mut s3 = Session::new(solidcraft_doc::Document::from_json(&text).unwrap());
|
||||
run(&mut s3, "parameters.change", json!({"name": "tip", "expression": "5 mm"}));
|
||||
assert!(rel(measured(&s3, "N").volume, cone_volume(8.0, 5.0, 12.0)) < 2e-3);
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ pub const MODIFIERS: &[ToolModifiers] = &[
|
||||
ToolModifiers { tool: "solid.cylinder", scopes: SELECT_ONLY, bindings: &[], audit: Audit::VerifiedNoOp },
|
||||
ToolModifiers { tool: "solid.sphere", scopes: SELECT_ONLY, bindings: &[], audit: Audit::VerifiedNoOp },
|
||||
ToolModifiers { tool: "solid.torus", scopes: SELECT_ONLY, bindings: &[], audit: Audit::VerifiedNoOp },
|
||||
ToolModifiers { tool: "solid.cone", scopes: SELECT_ONLY, bindings: &[], audit: Audit::VerifiedNoOp },
|
||||
ToolModifiers { tool: "solid.coil", scopes: SELECT_ONLY, bindings: &[], audit: Audit::VerifiedNoOp },
|
||||
ToolModifiers { tool: "solid.combine", scopes: SELECT_ONLY, bindings: &[], audit: Audit::VerifiedNoOp },
|
||||
];
|
||||
@@ -919,6 +920,19 @@ impl Dialog {
|
||||
},
|
||||
vec![],
|
||||
),
|
||||
"solid.cone" => Dialog::new(
|
||||
Kind::Primitive {
|
||||
cmd: "solid.cone",
|
||||
fields: vec![
|
||||
("diameter", length_default(s, "20")),
|
||||
("top_diameter", length_default(s, "0")),
|
||||
("height", length_default(s, "20")),
|
||||
],
|
||||
operation: 0,
|
||||
at: None,
|
||||
},
|
||||
vec![],
|
||||
),
|
||||
"solid.coil" => Dialog::new(
|
||||
Kind::Primitive {
|
||||
cmd: "solid.coil",
|
||||
@@ -1996,6 +2010,7 @@ fn title(k: &Kind) -> &'static str {
|
||||
"solid.cylinder" => "CYLINDER",
|
||||
"solid.sphere" => "SPHERE",
|
||||
"solid.coil" => "COIL",
|
||||
"solid.cone" => "CONE",
|
||||
_ => "TORUS",
|
||||
},
|
||||
Kind::Combine { .. } => "COMBINE",
|
||||
@@ -4378,7 +4393,7 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
|
||||
"solid.box" => {
|
||||
p.insert("corner".into(), box_corner(s, fields, point, normal));
|
||||
}
|
||||
"solid.cylinder" | "solid.coil" => {
|
||||
"solid.cylinder" | "solid.coil" | "solid.cone" => {
|
||||
p.insert("base".into(), pt(point));
|
||||
if let Some(n) = normal {
|
||||
p.insert("axis".into(), pt(n));
|
||||
@@ -5439,6 +5454,22 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
|
||||
}
|
||||
d
|
||||
}
|
||||
FeatureKind::Cone { base, axis, radius, top_radius, height, operation } => {
|
||||
let mut d = start("solid.cone")?;
|
||||
d.kind = Kind::Primitive {
|
||||
cmd: "solid.cone",
|
||||
fields: vec![
|
||||
("radius", stored_length(s, radius)),
|
||||
("top_radius", stored_length(s, top_radius)),
|
||||
("height", stored_length(s, height)),
|
||||
],
|
||||
operation: op_index(operation),
|
||||
at: None,
|
||||
};
|
||||
d.extra.insert("base".into(), pt_expr(base));
|
||||
d.extra.insert("axis".into(), pt3(axis.0));
|
||||
d
|
||||
}
|
||||
FeatureKind::BoundaryFill { tools, cells, operation, remove_tools } => {
|
||||
let mut d = start("solid.boundary_fill")?;
|
||||
// The cells again, ticked where a stored cell point lies in the same cell.
|
||||
|
||||
@@ -367,6 +367,7 @@ form_case!(primitive_box, "solid.box", ["length", "width", "height", "Operation"
|
||||
form_case!(cylinder, "solid.cylinder", ["diameter", "height", "Operation"]);
|
||||
form_case!(sphere, "solid.sphere", ["diameter", "Operation"]);
|
||||
form_case!(torus, "solid.torus", ["major", "minor", "Operation"]);
|
||||
form_case!(cone, "solid.cone", ["diameter", "top_diameter", "height", "Operation"]);
|
||||
form_case!(coil, "solid.coil", ["diameter", "revolutions", "pitch", "section_size", "Operation"]);
|
||||
form_case!(combine, "solid.combine", ["Operation"]);
|
||||
form_case!(surface_extrude, "surface.extrude", ["Distance", "Direction"]);
|
||||
|
||||
@@ -2192,6 +2192,21 @@ fn a_box_stands_out_of_a_face_that_faces_down_an_axis() {
|
||||
assert!(near(c, Vec3::new(-10.0, 12.0, 8.0)), "{c:?}");
|
||||
}
|
||||
|
||||
/// A cone (#5641) stands on the picked face like the cylinder: its base on the face, its axis
|
||||
/// out of it. The default is a full cone, whose centroid is a quarter of its height up.
|
||||
#[test]
|
||||
fn a_cone_stands_on_the_picked_face() {
|
||||
let mut app = boxed();
|
||||
let top = picked(&app, Vec3::new(20.0, 15.0, 20.0), Vec3::new(8.0, 10.0, 20.0));
|
||||
let (p, c) = primitive_on(&mut app, vec![top], "solid.cone", |_| {});
|
||||
assert_eq!((&p["base"], &p["axis"]), (&json!([8.0, 10.0, 20.0]), &json!([0.0, 0.0, 1.0])), "{p}");
|
||||
assert!(near(c, Vec3::new(8.0, 10.0, 25.0)), "{c:?}");
|
||||
let side = picked(&app, Vec3::new(0.0, 15.0, 10.0), Vec3::new(0.0, 12.0, 8.0));
|
||||
let (p, c) = primitive_on(&mut app, vec![side], "solid.cone", |_| {});
|
||||
assert_eq!(p["axis"], json!([-1.0, 0.0, 0.0]), "{p}");
|
||||
assert!(near(c, Vec3::new(-5.0, 12.0, 8.0)), "{c:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_primitive_without_a_pick_or_with_its_position_cleared_stays_at_the_origin() {
|
||||
let mut app = boxed();
|
||||
|
||||
@@ -393,3 +393,48 @@ fn inch_extrude_secondary_lengths_and_unchanged_edit_keep_their_physical_sizes()
|
||||
let reopened = Session::new(solidcraft_engine::doc::Document::from_json(&h.app.session.doc.to_json()).unwrap());
|
||||
assert_eq!(reopened.doc.length_inputs(), h.app.session.doc.length_inputs());
|
||||
}
|
||||
|
||||
/// The Cone dialog (#5641) shows its three sizes in the design's units with a full cone by
|
||||
/// default, makes what the bare numbers make, and its Edit shows the kept radii; a top radius
|
||||
/// typed there makes a frustum, and the operation is sent as chosen.
|
||||
#[test]
|
||||
fn cone_dialog_makes_a_cone_and_edits_it_into_a_frustum() {
|
||||
let sizes: &[(&str, &str)] = &[("diameter", "20"), ("top_diameter", "0"), ("height", "20")];
|
||||
let mut h = design("mm");
|
||||
h.app.start("solid.cone");
|
||||
h.frames(3);
|
||||
let shown = fields(&h);
|
||||
let want: Vec<(&str, String)> = sizes.iter().map(|(k, v)| (*k, format!("{v} mm"))).collect();
|
||||
assert_eq!(shown, want);
|
||||
assert_shown(&h, &shown);
|
||||
ok(&mut h);
|
||||
let made = volume(&mut h.app.session);
|
||||
let before = bare_volume("mm", "solid.cone", sizes);
|
||||
assert!((made - before).abs() < 1e-6 * before, "{made} mm³, the bare numbers made {before} mm³");
|
||||
let full = std::f64::consts::PI * 100.0 * 20.0 / 3.0;
|
||||
assert!((made - full).abs() < 2e-3 * full, "{made} vs {full}");
|
||||
// (The round base is measured on its facets: within a micrometre or so of 20 mm across.)
|
||||
let got = size(&mut h.app.session);
|
||||
assert!(got.iter().all(|g| (g - 20.0).abs() < 1e-3), "{got:?}");
|
||||
// Edit shows the radii it keeps; a top radius makes a frustum.
|
||||
let id = h.app.session.doc.features.first().map(|f| f.id).unwrap();
|
||||
h.app.edit_feature(id);
|
||||
h.frames(3);
|
||||
let shown = fields(&h);
|
||||
assert_eq!(shown, vec![("radius", "(20 mm) / 2".to_string()), ("top_radius", "(0 mm) / 2".to_string()), ("height", "20 mm".to_string())]);
|
||||
type_into(&mut h, "top_radius", "4");
|
||||
h.frames(2);
|
||||
ok(&mut h);
|
||||
let frustum = std::f64::consts::PI * 20.0 * (100.0 + 40.0 + 16.0) / 3.0;
|
||||
let got = volume(&mut h.app.session);
|
||||
assert!((got - frustum).abs() < 2e-3 * frustum, "{got} vs {frustum}");
|
||||
assert_eq!(h.app.session.doc.features.len(), 1, "the edit changed the cone in place");
|
||||
// The operation choice reaches the command.
|
||||
h.app.start("solid.cone");
|
||||
h.frames(3);
|
||||
if let Some(Kind::Primitive { operation, .. }) = h.app.dialog.as_mut().map(|d| &mut d.kind) {
|
||||
*operation = 2;
|
||||
}
|
||||
let (_, params) = apply_commands(&h.app, h.app.dialog.as_ref().unwrap()).unwrap().remove(0);
|
||||
assert_eq!(params["operation"], json!("cut"));
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ pub(crate) fn icon_of(k: &FeatureKind) -> &'static str {
|
||||
FeatureKind::Cylinder { .. } => "cylinder",
|
||||
FeatureKind::Sphere { .. } => "sphere",
|
||||
FeatureKind::Torus { .. } => "torus",
|
||||
FeatureKind::Cone { .. } => "cone",
|
||||
FeatureKind::Combine { .. } => "combine",
|
||||
FeatureKind::Pattern { pattern: solidcraft_engine::doc::PatternKind::Rectangular { .. }, .. } => "pattern_rect",
|
||||
FeatureKind::Pattern { pattern: solidcraft_engine::doc::PatternKind::Path { .. }, .. } => "pattern_path",
|
||||
|
||||
@@ -9,7 +9,7 @@ Always shown, in order: MODEL, SURFACE, ASSEMBLE, INSERT, INSPECT, MANAGE, VIEW,
|
||||
| Tab | Section | Command ids, in menu order |
|
||||
|---|---|---|
|
||||
| MODEL | SKETCH | **`sketch.create`**, `sketch.edit`, `sketch.line3d`, `sketch.spline3d`, `sketch.point3d`, `sketch.move3d` |
|
||||
| MODEL | CREATE | **`solid.extrude`**, **`solid.revolve`**, **`solid.hole`**, `solid.sweep`, `solid.loft`, `solid.box`, `solid.cylinder`, `solid.sphere`, `solid.torus`, `solid.pipe`, `solid.bounding_solid`, `solid.thread`, `solid.boundary_fill`, `solid.coil`, `solid.rib`, `solid.web`, `solid.emboss`, `plastic.boss`, `plastic.lip`, `plastic.snap_fit`, `plastic.rest` |
|
||||
| MODEL | CREATE | **`solid.extrude`**, **`solid.revolve`**, **`solid.hole`**, `solid.sweep`, `solid.loft`, `solid.box`, `solid.cylinder`, `solid.sphere`, `solid.torus`, `solid.cone`, `solid.pipe`, `solid.bounding_solid`, `solid.thread`, `solid.boundary_fill`, `solid.coil`, `solid.rib`, `solid.web`, `solid.emboss`, `plastic.boss`, `plastic.lip`, `plastic.snap_fit`, `plastic.rest` |
|
||||
| MODEL | MODIFY | **`solid.fillet`**, **`solid.chamfer`**, **`solid.shell`**, **`solid.press_pull`**, `sheet.convert`, `solid.combine`, `solid.scale`, `solid.offset_face`, `solid.draft`, `solid.split_face`, `solid.split_body`, `solid.replace_face`, `solid.remove`, `timeline.delete`, `face.delete` |
|
||||
| MODEL | ARRANGE | **`solid.move`**, **`solid.mirror`**, **`solid.pattern.rectangular`**, `solid.align`, `solid.pattern.circular`, `solid.pattern.path` |
|
||||
| MODEL | CONFIGURE | **`parameters.change`**, **`config.table`**, `config.configure` |
|
||||
@@ -96,7 +96,7 @@ Explicit command-id lists override the default placement mapping. Shared command
|
||||
| SOLID / CONSTRUCT | MODEL / CONSTRUCT | `construct.plane.offset`, `construct.plane.at_angle`, `construct.axis.through_cylinder`, `construct.axis.normal_to_face`, `construct.axis.two_planes`, `construct.axis.two_points`, `construct.axis.edge`, `construct.point.vertex`, `construct.point.two_edges`, `construct.point.three_planes`, `construct.point.center`, `construct.point.edge_plane`, `construct.point.along_path`, `construct.plane.midplane`, `construct.plane.two_edges`, `construct.plane.three_points`, `construct.plane.tangent`, `construct.plane.along_path`, `construct.plane.perpendicular` |
|
||||
| SOLID / CREATE | ASSEMBLE / JOINTS | `joint.origin` |
|
||||
| SOLID / CREATE | MODEL / ARRANGE | `solid.pattern.rectangular`, `solid.pattern.circular`, `solid.pattern.path`, `solid.mirror` |
|
||||
| SOLID / CREATE | MODEL / CREATE | `solid.extrude`, `solid.revolve`, `solid.sweep`, `solid.loft`, `solid.box`, `solid.cylinder`, `solid.sphere`, `solid.torus`, `solid.hole`, `solid.pipe`, `solid.bounding_solid`, `solid.thread`, `solid.boundary_fill`, `solid.coil`, `solid.rib`, `solid.web`, `solid.emboss` |
|
||||
| SOLID / CREATE | MODEL / CREATE | `solid.extrude`, `solid.revolve`, `solid.sweep`, `solid.loft`, `solid.box`, `solid.cylinder`, `solid.sphere`, `solid.torus`, `solid.cone`, `solid.hole`, `solid.pipe`, `solid.bounding_solid`, `solid.thread`, `solid.boundary_fill`, `solid.coil`, `solid.rib`, `solid.web`, `solid.emboss` |
|
||||
| SOLID / CREATE | MODEL / SKETCH | `sketch.create` |
|
||||
| SOLID / INSERT | INSERT / FILES | `file.insert_step`, `file.insert_mesh` |
|
||||
| SOLID / INSERT | INSERT / LIBRARY | `parts.fastener` |
|
||||
|
||||
Reference in New Issue
Block a user