Surface Sweep carries open curves and closed profile boundaries along a sketch path into surface bodies, with smooth walls along curved point paths. Adds the SurfaceSweep feature (format 18 note), the surface.sweep command, a dialog with Edit Feature support, and kernel and engine tests. Part of #757.
506 lines
20 KiB
Rust
506 lines
20 KiB
Rust
//! Components and occurrences: the design as a tree.
|
||
//!
|
||
//! The root component (id 0) is the design. Every other component lives in a parent component
|
||
//! through one or more occurrences, each with a rigid transform (instances share the component's
|
||
//! contents; a "Paste New" copy is a separate component). A component's sketches, construction
|
||
//! geometry and features are authored in its own frame (its origin); the model is evaluated in
|
||
//! those frames and placed in the world through the occurrence transforms when bodies are
|
||
//! gathered for display, measuring and export (`world_bodies`). Commands take world coordinates
|
||
//! and the engine maps a new feature's geometry into the active component's frame.
|
||
|
||
use serde::{Deserialize, Serialize};
|
||
use solidcraft_geom::{Plane, Vec3};
|
||
|
||
use crate::{AxisRef, DocError, Document, FeatureKind, PatternKind, PlaneRef, Result};
|
||
|
||
/// Column-major 4×4 affine transform (`m[col][row]`).
|
||
pub type Mat = [[f64; 4]; 4];
|
||
|
||
pub const IDENTITY: Mat = [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0]];
|
||
|
||
pub fn mat_mul(a: &Mat, b: &Mat) -> Mat {
|
||
let mut r = [[0.0; 4]; 4];
|
||
for (c, col) in r.iter_mut().enumerate() {
|
||
for (row, cell) in col.iter_mut().enumerate() {
|
||
*cell = (0..4).map(|k| a[k][row] * b[c][k]).sum();
|
||
}
|
||
}
|
||
r
|
||
}
|
||
|
||
pub fn apply_point(m: &Mat, p: Vec3) -> Vec3 {
|
||
Vec3::new(
|
||
m[0][0] * p.x + m[1][0] * p.y + m[2][0] * p.z + m[3][0],
|
||
m[0][1] * p.x + m[1][1] * p.y + m[2][1] * p.z + m[3][1],
|
||
m[0][2] * p.x + m[1][2] * p.y + m[2][2] * p.z + m[3][2],
|
||
)
|
||
}
|
||
|
||
pub fn apply_vector(m: &Mat, v: Vec3) -> Vec3 {
|
||
Vec3::new(
|
||
m[0][0] * v.x + m[1][0] * v.y + m[2][0] * v.z,
|
||
m[0][1] * v.x + m[1][1] * v.y + m[2][1] * v.z,
|
||
m[0][2] * v.x + m[1][2] * v.y + m[2][2] * v.z,
|
||
)
|
||
}
|
||
|
||
pub fn is_identity(m: &Mat) -> bool {
|
||
m.iter().flatten().zip(IDENTITY.iter().flatten()).all(|(a, b)| (a - b).abs() < 1e-12)
|
||
}
|
||
|
||
/// Inverse of an affine transform.
|
||
pub fn mat_inverse(m: &Mat) -> Option<Mat> {
|
||
let a = |r: usize, c: usize| m[c][r];
|
||
let det = a(0, 0) * (a(1, 1) * a(2, 2) - a(1, 2) * a(2, 1)) - a(0, 1) * (a(1, 0) * a(2, 2) - a(1, 2) * a(2, 0))
|
||
+ a(0, 2) * (a(1, 0) * a(2, 1) - a(1, 1) * a(2, 0));
|
||
if det.abs() < 1e-14 || !det.is_finite() {
|
||
return None;
|
||
}
|
||
let inv = |r: usize, c: usize| -> f64 {
|
||
// Cofactor of (c, r) / det.
|
||
let (r0, r1) = match c {
|
||
0 => (1, 2),
|
||
1 => (0, 2),
|
||
_ => (0, 1),
|
||
};
|
||
let (c0, c1) = match r {
|
||
0 => (1, 2),
|
||
1 => (0, 2),
|
||
_ => (0, 1),
|
||
};
|
||
let minor = a(r0, c0) * a(r1, c1) - a(r0, c1) * a(r1, c0);
|
||
let sign = if (r + c).is_multiple_of(2) { 1.0 } else { -1.0 };
|
||
sign * minor / det
|
||
};
|
||
let mut out = IDENTITY;
|
||
for (c, col) in out.iter_mut().enumerate().take(3) {
|
||
for (r, cell) in col.iter_mut().enumerate().take(3) {
|
||
*cell = inv(r, c);
|
||
}
|
||
}
|
||
let t = Vec3::new(m[3][0], m[3][1], m[3][2]);
|
||
let it = apply_vector(&out, t);
|
||
out[3] = [-it.x, -it.y, -it.z, 1.0];
|
||
Some(out)
|
||
}
|
||
|
||
/// A rigid transform: rotation by `angle` about `axis` through `origin`, then a translation.
|
||
pub fn rigid(translate: Vec3, origin: Vec3, axis: Vec3, angle: f64) -> Mat {
|
||
let k = axis.normalized().unwrap_or(Vec3::Z);
|
||
let (s, c) = angle.sin_cos();
|
||
let t = 1.0 - c;
|
||
let r = [
|
||
[t * k.x * k.x + c, t * k.x * k.y + s * k.z, t * k.x * k.z - s * k.y],
|
||
[t * k.x * k.y - s * k.z, t * k.y * k.y + c, t * k.y * k.z + s * k.x],
|
||
[t * k.x * k.z + s * k.y, t * k.y * k.z - s * k.x, t * k.z * k.z + c],
|
||
];
|
||
let mut m = IDENTITY;
|
||
for (mc, rc) in m.iter_mut().zip(r) {
|
||
mc[..3].copy_from_slice(&rc);
|
||
}
|
||
let ro = apply_vector(&m, origin);
|
||
let tr = origin - ro + translate;
|
||
m[3] = [tr.x, tr.y, tr.z, 1.0];
|
||
m
|
||
}
|
||
|
||
/// A placement of a component inside a parent component.
|
||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||
pub struct Occurrence {
|
||
pub id: u64,
|
||
pub component: u64,
|
||
/// The component it sits in (0 = the root).
|
||
#[serde(default)]
|
||
pub parent: u64,
|
||
pub name: String,
|
||
#[serde(default = "identity")]
|
||
pub transform: Mat,
|
||
/// Grounded occurrences don't move.
|
||
#[serde(default)]
|
||
pub grounded: bool,
|
||
}
|
||
|
||
fn identity() -> Mat {
|
||
IDENTITY
|
||
}
|
||
|
||
fn map_plane(m: &Mat, p: &Plane) -> Plane {
|
||
let (o, x, y) = (apply_point(m, p.origin), apply_vector(m, p.x), apply_vector(m, p.y));
|
||
Plane::new(o, x, y).unwrap_or(*p)
|
||
}
|
||
|
||
fn map_plane_ref(m: &Mat, p: &mut PlaneRef) {
|
||
match p {
|
||
PlaneRef::Custom { plane } => *plane = map_plane(m, plane),
|
||
PlaneRef::Face { plane, at, .. } => {
|
||
*plane = map_plane(m, plane);
|
||
*at = apply_point(m, *at);
|
||
}
|
||
PlaneRef::Offset { base, .. } => map_plane_ref(m, base),
|
||
PlaneRef::AtAngle { base, axis_origin, axis_dir, .. } => {
|
||
map_plane_ref(m, base);
|
||
*axis_origin = apply_point(m, *axis_origin);
|
||
*axis_dir = apply_vector(m, *axis_dir);
|
||
}
|
||
// Named origin planes and construction planes are the component's own.
|
||
PlaneRef::Origin { .. } | PlaneRef::Construction { .. } => {}
|
||
PlaneRef::Tangent { face, at } => {
|
||
*at = apply_point(m, *at);
|
||
map_geo_ref(m, face);
|
||
}
|
||
PlaneRef::Midplane { .. }
|
||
| PlaneRef::TwoEdges { .. }
|
||
| PlaneRef::ThreePoints { .. }
|
||
| PlaneRef::AlongPath { .. }
|
||
| PlaneRef::Perpendicular { .. } => {
|
||
for r in p.refs_mut() {
|
||
map_geo_ref(m, r);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn map_geo_ref(m: &Mat, g: &mut crate::construct::GeoRef) {
|
||
g.map_points(&mut |p: &mut Vec3| *p = apply_point(m, *p), &mut |pl: &mut PlaneRef| map_plane_ref(m, pl));
|
||
}
|
||
|
||
/// Map a point given as expressions by `m`: numbers stay numbers; a coordinate that is an
|
||
/// expression stays one (`m · (x, y, z) + t`, written out), so it still follows its parameters.
|
||
pub fn map_point_expr(m: &Mat, p: &mut crate::PointExpr) {
|
||
let num = |e: &String| e.trim().parse::<f64>().ok().filter(|v| v.is_finite());
|
||
if let [Some(x), Some(y), Some(z)] = [num(&p[0]), num(&p[1]), num(&p[2])] {
|
||
*p = crate::point_expr(apply_point(m, Vec3::new(x, y, z)));
|
||
return;
|
||
}
|
||
let old = p.clone();
|
||
for (row, out) in p.iter_mut().enumerate() {
|
||
let mut constant = m[3][row];
|
||
let mut terms: Vec<String> = Vec::new();
|
||
for (col, e) in old.iter().enumerate() {
|
||
let c = m[col][row];
|
||
if c.abs() < 1e-12 {
|
||
continue;
|
||
}
|
||
match num(e) {
|
||
Some(v) => constant += c * v,
|
||
None if (c - 1.0).abs() < 1e-12 => terms.push(format!("({e})")),
|
||
None => terms.push(format!("{c} * ({e})")),
|
||
}
|
||
}
|
||
let mut e = terms.join(" + ");
|
||
if terms.is_empty() {
|
||
e = format!("{constant} mm");
|
||
} else if constant.abs() >= 1e-12 {
|
||
e = format!("{e} {} {} mm", if constant < 0.0 { '-' } else { '+' }, constant.abs());
|
||
}
|
||
*out = e;
|
||
}
|
||
}
|
||
|
||
impl FeatureKind {
|
||
/// Map the feature's world geometry (points, directions, explicit planes) by `m`.
|
||
pub fn transform_geometry(&mut self, m: &Mat) {
|
||
let pt = |p: &mut Vec3| *p = apply_point(m, *p);
|
||
let dir = |v: &mut Vec3| *v = apply_vector(m, *v);
|
||
match self {
|
||
FeatureKind::Sketch { plane, .. } | FeatureKind::ConstructionPlane { plane } | FeatureKind::Mirror { plane, .. } => {
|
||
map_plane_ref(m, plane)
|
||
}
|
||
FeatureKind::ConstructionAxis { def } => def.refs_mut().into_iter().for_each(|r| map_geo_ref(m, r)),
|
||
FeatureKind::ConstructionPoint { def } => def.refs_mut().into_iter().for_each(|r| map_geo_ref(m, r)),
|
||
FeatureKind::Split { plane, tool, .. } => {
|
||
map_plane_ref(m, plane);
|
||
if let Some(t) = tool {
|
||
pt(&mut t.point);
|
||
}
|
||
}
|
||
FeatureKind::SplitFace { faces, plane, tool, .. } => {
|
||
faces.iter_mut().for_each(pt);
|
||
map_plane_ref(m, plane);
|
||
if let Some(t) = tool {
|
||
pt(&mut t.point);
|
||
}
|
||
}
|
||
FeatureKind::Revolve { axis: AxisRef::Line { origin, dir: d }, .. }
|
||
| FeatureKind::SurfaceRevolve { axis: AxisRef::Line { origin, dir: d }, .. } => {
|
||
pt(origin);
|
||
dir(d);
|
||
}
|
||
FeatureKind::Fillet { edges, style, .. } => {
|
||
edges.iter_mut().for_each(pt);
|
||
if let crate::FilletStyle::Variable { start, .. } | crate::FilletStyle::Spline { start, .. } = style {
|
||
pt(start);
|
||
}
|
||
}
|
||
FeatureKind::Chamfer { edges, .. } => edges.iter_mut().for_each(pt),
|
||
FeatureKind::Shell { faces, .. } | FeatureKind::OffsetFace { faces, .. } | FeatureKind::DeleteFace { faces, .. } => {
|
||
faces.iter_mut().for_each(pt)
|
||
}
|
||
FeatureKind::Draft { faces, neutral, pull, .. } => {
|
||
faces.iter_mut().for_each(pt);
|
||
map_plane_ref(m, neutral);
|
||
dir(pull);
|
||
}
|
||
FeatureKind::Box { corner, .. } => map_point_expr(m, corner),
|
||
FeatureKind::Cylinder { base, axis, .. } => {
|
||
map_point_expr(m, base);
|
||
dir(axis);
|
||
}
|
||
FeatureKind::Sphere { center, .. } | FeatureKind::Torus { center, .. } => map_point_expr(m, center),
|
||
FeatureKind::Pattern { pattern: PatternKind::Rectangular { dir1, dir2, .. }, .. } => {
|
||
dir(dir1);
|
||
if let Some(d) = dir2 {
|
||
dir(d);
|
||
}
|
||
}
|
||
FeatureKind::Pattern { pattern: PatternKind::Circular { origin, axis, .. }, .. } => {
|
||
pt(origin);
|
||
dir(axis);
|
||
}
|
||
// Follows its sketch.
|
||
FeatureKind::Pattern { pattern: PatternKind::Path { .. }, .. } => {}
|
||
FeatureKind::Hole { position, direction, .. } => {
|
||
pt(position);
|
||
dir(direction);
|
||
}
|
||
FeatureKind::Thread { face, .. } => pt(face),
|
||
FeatureKind::SheetFlange { edges, .. } => edges.iter_mut().for_each(|e| pt(&mut e.point)),
|
||
FeatureKind::SheetHem { edges, .. } => edges.iter_mut().for_each(pt),
|
||
FeatureKind::SheetConvert { face, bends, .. } => {
|
||
pt(face);
|
||
bends.iter_mut().for_each(pt);
|
||
}
|
||
FeatureKind::SheetFold { a, b, fixed, .. } => {
|
||
pt(a);
|
||
pt(b);
|
||
if let Some(f) = fixed {
|
||
pt(f);
|
||
}
|
||
}
|
||
FeatureKind::Boss { position, direction, .. } => {
|
||
pt(position);
|
||
dir(direction);
|
||
}
|
||
FeatureKind::Lip { face, .. } => pt(face),
|
||
FeatureKind::BoundaryFill { cells, .. } => cells.iter_mut().for_each(pt),
|
||
FeatureKind::Rest { position, direction, along, .. } => {
|
||
pt(position);
|
||
dir(direction);
|
||
if let Some(a) = along {
|
||
dir(a);
|
||
}
|
||
}
|
||
FeatureKind::SnapFit { position, direction, hook, .. } => {
|
||
pt(position);
|
||
dir(direction);
|
||
dir(hook);
|
||
}
|
||
FeatureKind::SheetBase { .. } | FeatureKind::SheetContour { .. } | FeatureKind::SheetUnfold { .. } => {}
|
||
FeatureKind::Coil { base, axis, .. } => {
|
||
map_point_expr(m, base);
|
||
dir(axis);
|
||
}
|
||
FeatureKind::ReplaceFace { faces, target, .. } => {
|
||
faces.iter_mut().for_each(pt);
|
||
map_plane_ref(m, target);
|
||
}
|
||
FeatureKind::Align { from, to, from_normal, to_normal, .. } => {
|
||
pt(from);
|
||
pt(to);
|
||
if let Some(n) = from_normal {
|
||
dir(n);
|
||
}
|
||
if let Some(n) = to_normal {
|
||
dir(n);
|
||
}
|
||
}
|
||
FeatureKind::Scale { origin, .. } => pt(origin),
|
||
FeatureKind::Move { rotate_axis, .. } => {
|
||
if let Some(a) = rotate_axis {
|
||
dir(a);
|
||
}
|
||
}
|
||
FeatureKind::Revolve { .. }
|
||
| FeatureKind::Extrude { .. }
|
||
| FeatureKind::SurfaceExtrude { .. }
|
||
| FeatureKind::SurfaceRevolve { .. }
|
||
| FeatureKind::SurfaceSweep { .. }
|
||
| FeatureKind::Combine { .. }
|
||
| FeatureKind::Loft { .. }
|
||
| FeatureKind::Sweep { .. }
|
||
| FeatureKind::BoundingSolid { .. }
|
||
| FeatureKind::Pipe { .. }
|
||
| FeatureKind::Emboss { .. }
|
||
| FeatureKind::Rib { .. }
|
||
| FeatureKind::Remove { .. }
|
||
| FeatureKind::Stitch { .. }
|
||
| FeatureKind::Thicken { .. }
|
||
| FeatureKind::Import { .. }
|
||
| FeatureKind::MeshImport { .. }
|
||
| FeatureKind::MeshConvert { .. }
|
||
| FeatureKind::BodyTransfer { .. } => {}
|
||
FeatureKind::Patch { edges, .. } | FeatureKind::SurfaceExtend { edges, .. } => edges.iter_mut().for_each(pt),
|
||
FeatureKind::Unstitch { faces, .. } | FeatureKind::ReverseNormal { faces, .. } => faces.iter_mut().for_each(pt),
|
||
FeatureKind::SurfaceTrim { plane, tool, keep, .. } => {
|
||
map_plane_ref(m, plane);
|
||
if let Some(t) = tool {
|
||
pt(&mut t.point);
|
||
}
|
||
pt(keep);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Document {
|
||
/// The first occurrence of a component (its placement for authoring).
|
||
pub fn occurrence_of(&self, component: u64) -> Option<&crate::Occurrence> {
|
||
self.occurrences.iter().find(|o| o.component == component)
|
||
}
|
||
|
||
/// World transform of a component's frame (through its first occurrence and its parents').
|
||
pub fn component_transform(&self, component: u64) -> Mat {
|
||
let mut m = IDENTITY;
|
||
let mut c = component;
|
||
for _ in 0..1000 {
|
||
if c == 0 {
|
||
break;
|
||
}
|
||
let Some(o) = self.occurrence_of(c) else { break };
|
||
m = mat_mul(&o.transform, &m);
|
||
c = o.parent;
|
||
}
|
||
m
|
||
}
|
||
|
||
/// Every placement of each component: (component, occurrence path ids, world transform).
|
||
pub fn placements(&self) -> Vec<(u64, Vec<u64>, Mat)> {
|
||
let mut out = vec![(0, Vec::new(), IDENTITY)];
|
||
let mut stack: Vec<(u64, Vec<u64>, Mat)> = vec![(0, Vec::new(), IDENTITY)];
|
||
while let Some((comp, path, m)) = stack.pop() {
|
||
if path.len() > 64 || out.len() > 100_000 {
|
||
break;
|
||
}
|
||
for o in self.occurrences.iter().filter(|o| o.parent == comp) {
|
||
let mut p = path.clone();
|
||
p.push(o.id);
|
||
let w = mat_mul(&m, &o.transform);
|
||
out.push((o.component, p.clone(), w));
|
||
stack.push((o.component, p, w));
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Every body as the world shows it: its world name ("Body1", or "Body1 (Occ:2)" for a
|
||
/// further instance), its component, the occurrence path, and the transform from the frame
|
||
/// its geometry is in to the world.
|
||
pub fn body_frames(&self, st: &crate::ModelState) -> Vec<(String, u64, Vec<u64>, Mat)> {
|
||
self.body_frames_with_source(st).into_iter().map(|(world, _, component, path, matrix)| (world, component, path, matrix)).collect()
|
||
}
|
||
|
||
/// Body frames with explicit definition names; callers must not infer them from labels.
|
||
pub fn body_frames_with_source(&self, st: &crate::ModelState) -> Vec<(String, String, u64, Vec<u64>, Mat)> {
|
||
let mut out = Vec::new();
|
||
for (comp, path, m) in self.placements() {
|
||
for b in st.bodies.iter().filter(|b| self.model_body_component(b) == comp) {
|
||
let name = self.world_body_name(st, &b.name, comp, &path);
|
||
let w = match self.model_body_offset(b) {
|
||
Some(o) => mat_mul(&m, o),
|
||
None => m,
|
||
};
|
||
out.push((name, b.definition_name().to_string(), comp, path.clone(), w));
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Computed placement name. Source names retain priority; ambiguous labels use full IDs.
|
||
/// Resolves label ambiguity with bounded dynamic counts instead of expanding all placements.
|
||
pub fn world_body_name(&self, st: &crate::ModelState, source: &str, component: u64, path: &[u64]) -> String {
|
||
if path == self.first_path(component) {
|
||
return source.to_string();
|
||
}
|
||
let names: Vec<&str> = path.iter().filter_map(|id| self.occurrences.iter().find(|o| o.id == *id)).map(|o| o.name.as_str()).collect();
|
||
let label = names.join("/");
|
||
let candidate = format!("{source} ({label})");
|
||
let mut counts = std::collections::BTreeMap::from([(0, 1usize)]);
|
||
for name in &names {
|
||
let mut next = std::collections::BTreeMap::<u64, usize>::new();
|
||
for o in self.occurrences.iter().filter(|o| o.name == *name) {
|
||
if let Some(count) = counts.get(&o.parent) {
|
||
let n = next.entry(o.component).or_default();
|
||
*n = (*n + count).min(2);
|
||
}
|
||
}
|
||
counts = next;
|
||
}
|
||
let ambiguous = counts.get(&component).copied().unwrap_or(0) > 1;
|
||
// Slash and reserved-marker names could alias concatenated labels or fallback keys.
|
||
let reserved =
|
||
self.occurrences.iter().any(|o| o.name.contains('/') || o.name.contains("@occ[")) || st.bodies.iter().any(|b| b.name.contains("@occ["));
|
||
if !ambiguous && !reserved && st.body(&candidate).is_none() {
|
||
return candidate;
|
||
}
|
||
let ids = path.iter().map(u64::to_string).collect::<Vec<_>>().join("/");
|
||
let encoded = serde_json::to_string(source).unwrap_or_else(|_| source.to_string());
|
||
let mut key = format!("{encoded}@occ[{ids}]");
|
||
while st.body(&key).is_some() {
|
||
key.push('#');
|
||
}
|
||
key
|
||
}
|
||
|
||
/// The occurrence path of a component's first placement.
|
||
pub fn first_path(&self, component: u64) -> Vec<u64> {
|
||
let mut path = Vec::new();
|
||
let mut c = component;
|
||
for _ in 0..1000 {
|
||
if c == 0 {
|
||
break;
|
||
}
|
||
let Some(o) = self.occurrence_of(c) else { break };
|
||
path.insert(0, o.id);
|
||
c = o.parent;
|
||
}
|
||
path
|
||
}
|
||
|
||
/// Is `a` inside `b` (or `b` itself)?
|
||
pub fn component_within(&self, a: u64, b: u64) -> bool {
|
||
let mut c = a;
|
||
for _ in 0..1000 {
|
||
if c == b {
|
||
return true;
|
||
}
|
||
if c == 0 {
|
||
return false;
|
||
}
|
||
match self.components.iter().find(|x| x.id == c) {
|
||
Some(x) => c = x.parent,
|
||
None => return false,
|
||
}
|
||
}
|
||
false
|
||
}
|
||
|
||
/// Add an occurrence of an existing component into `parent`.
|
||
pub fn add_occurrence(&mut self, component: u64, parent: u64, transform: Mat) -> Result<u64> {
|
||
if component == 0 || !self.components.iter().any(|c| c.id == component) {
|
||
return Err(DocError::Unknown(format!("component {component}")));
|
||
}
|
||
if self.component_within(parent, component) {
|
||
return Err(DocError::Invalid("a component can't contain itself".into()));
|
||
}
|
||
if self.occurrences.len() >= 100_000 {
|
||
return Err(DocError::Invalid("too many occurrences".into()));
|
||
}
|
||
let id = self.next_id.max(1);
|
||
self.next_id = id + 1;
|
||
let cname = self.components.iter().find(|c| c.id == component).map(|c| c.name.clone()).unwrap_or_default();
|
||
let k = self.occurrences.iter().filter(|o| o.component == component).count() + 1;
|
||
self.occurrences.push(crate::Occurrence { id, component, parent, name: format!("{cname}:{k}"), transform, grounded: false });
|
||
Ok(id)
|
||
}
|
||
}
|