forked from 1b-agent/fieldcraft-upstream
geometry: defeature a design by suppressing its features, virtually when they cannot be
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
9c1c8f75c2
commit
e9ef7f9d4d
4 files changed
+422
No files matched your search
@@ -0,0 +1,417 @@
|
||||
//! Defeaturing for meshing: holes, fillets, chamfers and small bosses taken out of a design so they
|
||||
//! do not drive the mesh.
|
||||
//!
|
||||
//! A design opened from its timeline knows its features ([`Design::features`]), so a hole is
|
||||
//! removed by rebuilding the design with the hole's feature suppressed, exactly as SolidCraft
|
||||
//! would build it without it: the hole is filled, a fillet or chamfer goes back to its sharp edge,
|
||||
//! a boss is gone. The faces the other features made keep their persistent names, so sets, seeds
|
||||
//! and loads on them still resolve.
|
||||
//!
|
||||
//! A feature that cannot be suppressed on its own (a later feature needs it: a fillet along the
|
||||
//! hole's edge, a pattern of it), or whose suppression would take a body away, is kept in the
|
||||
//! B-rep and defeatured for meshing only instead, as virtual topology
|
||||
//! ([`crate::virtual_faces`]): its faces are merged into the neighbour they share the longest
|
||||
//! edges with, so the meshers see no edges around it (the material stays). Such a feature is
|
||||
//! listed with `virtual` in [`Design::defeat_report`].
|
||||
//!
|
||||
//! Defeatures live in the FieldCraft model by feature id (the `F<id>` of persistent names, stable
|
||||
//! across edits of the design) and are applied whenever the design is opened, before the
|
||||
//! partitions. [`Design::defeatured`] keeps the design as opened, so switching a feature back on
|
||||
//! rebuilds from it.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::Serialize;
|
||||
use solidcraft_doc::FeatureKind;
|
||||
|
||||
use crate::features::FeatureNode;
|
||||
use crate::virtual_faces::Merge;
|
||||
use crate::{Design, Error, FeatureError, Result};
|
||||
|
||||
/// Most features all of a design's defeatures name together.
|
||||
pub const MAX_DEFEATURED: usize = 1024;
|
||||
/// Most rebuilds spent finding which features suppress on their own when suppressing them all
|
||||
/// at once breaks the design; the features past it are defeatured virtually.
|
||||
const MAX_TRIALS: usize = 64;
|
||||
|
||||
/// Features of the design's timeline suppressed for meshing (`geometry.defeature`).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Defeature {
|
||||
pub name: String,
|
||||
/// Feature ids (`F<id>`).
|
||||
pub features: Vec<u64>,
|
||||
}
|
||||
|
||||
/// How a defeatured feature was taken out.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct Defeated {
|
||||
/// The defeature that names it.
|
||||
pub defeature: String,
|
||||
pub feature: u64,
|
||||
/// `suppressed` (the design rebuilt without it) or `virtual` (its faces merged into a
|
||||
/// neighbour for meshing; the B-rep keeps it).
|
||||
pub method: &'static str,
|
||||
/// Why it could not be suppressed, for a virtual one.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
/// The defeatures applied to a design, and the design as opened.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Applied {
|
||||
/// The design as opened, before any defeature.
|
||||
original: Design,
|
||||
defeatures: Vec<Defeature>,
|
||||
report: Vec<Defeated>,
|
||||
/// The virtual defeatures, as merges applied with the model's own.
|
||||
merges: Vec<Merge>,
|
||||
}
|
||||
|
||||
/// A feature a mesh-prep pass may take out, and its size.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||
pub struct Candidate {
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
/// The feature's definition type (`hole`, `extrude`, `fillet`, …).
|
||||
#[serde(rename = "type")]
|
||||
pub kind: String,
|
||||
/// `hole`, `fillet`, `chamfer` or `boss`.
|
||||
pub category: &'static str,
|
||||
/// The size compared: a hole's diameter, a fillet's radius, a chamfer's distance, a boss's
|
||||
/// height (mm).
|
||||
pub size: f64,
|
||||
/// The body's faces it made.
|
||||
pub faces: Vec<String>,
|
||||
}
|
||||
|
||||
/// Whether the feature `node` is a hole, fillet, chamfer or boss, and its size.
|
||||
pub fn candidate(node: &FeatureNode) -> Option<Candidate> {
|
||||
let v = |k: &str| node.values.get(k).copied();
|
||||
let op = node.operation.as_deref();
|
||||
// A circular profile's diameter (an extrude of a circle).
|
||||
let circle = || {
|
||||
let p = node.sketch.as_ref()?.profiles.first()?;
|
||||
match p.outer.as_slice() {
|
||||
[crate::Curve::Circle { radius, .. }] if p.holes.is_empty() => Some(2.0 * radius),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
let (category, size) = match (node.kind.as_str(), op) {
|
||||
("hole", _) => ("hole", v("diameter")?),
|
||||
("fillet", _) => ("fillet", v("radius")?),
|
||||
("chamfer", _) => ("chamfer", v("distance")?),
|
||||
("cylinder", Some("cut")) => ("hole", 2.0 * v("radius")?),
|
||||
("extrude", Some("cut")) => ("hole", circle()?),
|
||||
("extrude", Some("join")) => ("boss", v("distance")?.abs()),
|
||||
("cylinder" | "box", Some("join")) => ("boss", v("height")?.abs()),
|
||||
_ => return None,
|
||||
};
|
||||
(size.is_finite() && size > 0.0).then(|| Candidate {
|
||||
id: node.id,
|
||||
name: node.name.clone(),
|
||||
kind: node.kind.clone(),
|
||||
category,
|
||||
size,
|
||||
faces: node.faces.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
impl Design {
|
||||
/// The design as opened, before its defeatures.
|
||||
pub fn undefeatured(&self) -> &Design {
|
||||
self.defeat.as_deref().map_or(self, |a| &a.original)
|
||||
}
|
||||
|
||||
/// How each defeatured feature was taken out, in the defeatures' order.
|
||||
pub fn defeat_report(&self) -> &[Defeated] {
|
||||
self.defeat.as_deref().map_or(&[], |a| a.report.as_slice())
|
||||
}
|
||||
|
||||
/// The design with `defeatures` applied, from the design as opened, and the merges that
|
||||
/// defeature its features virtually (apply them with the model's own, [`Design::merged`]).
|
||||
/// A defeature naming a feature the timeline lacks, or one that builds a body or a sketch, is
|
||||
/// listed in [`Design::errors`] (`defeature <name>`) and the rest still apply. A design
|
||||
/// without a timeline defeatures nothing (each is an error).
|
||||
pub fn defeatured(&self, defeatures: &[Defeature]) -> Result<(Design, Vec<Merge>)> {
|
||||
let base = self.undefeatured();
|
||||
if defeatures.iter().all(|d| d.features.is_empty()) {
|
||||
return Ok((base.clone(), Vec::new()));
|
||||
}
|
||||
if let Some(a) = self.defeat.as_deref().filter(|a| a.defeatures == defeatures) {
|
||||
return Ok((self.clone(), a.merges.clone()));
|
||||
}
|
||||
if defeatures.iter().map(|d| d.features.len()).sum::<usize>() > MAX_DEFEATURED {
|
||||
return Err(Error::Invalid(format!("a design takes at most {MAX_DEFEATURED} defeatured features")));
|
||||
}
|
||||
let mut errors = Vec::new();
|
||||
let fail = |d: &Defeature, message: String| FeatureError { feature: format!("defeature {}", d.name), message, bodies: Vec::new() };
|
||||
// The features to take out, each with the first defeature naming it.
|
||||
let mut wanted: BTreeMap<u64, &str> = BTreeMap::new();
|
||||
let mut order = Vec::new();
|
||||
for d in defeatures {
|
||||
for &id in &d.features {
|
||||
let refused = match base.history.as_deref().map(|h| h.doc.feature(id)) {
|
||||
None => Some("the design has no feature timeline".to_string()),
|
||||
Some(None) => Some(format!("the design has no feature F{id}")),
|
||||
Some(Some(f)) if !suppressible(&f.kind) => {
|
||||
Some(format!("F{id} ({}) builds a body or a sketch; it is not a feature to defeature", f.name))
|
||||
}
|
||||
Some(Some(_)) => None,
|
||||
};
|
||||
match refused {
|
||||
Some(m) => errors.push(fail(d, m)),
|
||||
None if !wanted.contains_key(&id) => {
|
||||
wanted.insert(id, &d.name);
|
||||
order.push(id);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Suppress them all at once; when that breaks the design, one at a time, keeping those
|
||||
// that rebuild cleanly.
|
||||
let mut kept: BTreeSet<u64> = order.iter().copied().collect();
|
||||
let mut rebuilt = rebuild_without(base, &kept)?.ok();
|
||||
let mut reasons: BTreeMap<u64, String> = BTreeMap::new();
|
||||
if rebuilt.is_none() {
|
||||
kept.clear();
|
||||
for (k, &id) in order.iter().enumerate() {
|
||||
if k >= MAX_TRIALS {
|
||||
reasons.insert(id, format!("more than {MAX_TRIALS} features to try one at a time"));
|
||||
continue;
|
||||
}
|
||||
let mut next = kept.clone();
|
||||
next.insert(id);
|
||||
match rebuild_without(base, &next)? {
|
||||
Ok(d) => {
|
||||
kept = next;
|
||||
rebuilt = Some(d);
|
||||
}
|
||||
Err(why) => {
|
||||
reasons.insert(id, why);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut design = match rebuilt {
|
||||
Some(d) if !kept.is_empty() => d,
|
||||
_ => base.clone(),
|
||||
};
|
||||
// The rest: their faces merged into a neighbour, body by body.
|
||||
let mut merges = Vec::new();
|
||||
let mut report = Vec::new();
|
||||
let graph = if reasons.is_empty() { Vec::new() } else { base.features(None)? };
|
||||
for &id in &order {
|
||||
let defeature = wanted.get(&id).map_or_else(String::new, |n| n.to_string());
|
||||
if kept.contains(&id) {
|
||||
report.push(Defeated { defeature, feature: id, method: "suppressed", reason: None });
|
||||
continue;
|
||||
}
|
||||
for b in &graph {
|
||||
let Some(node) = b.features.iter().find(|f| f.id == id).filter(|f| !f.faces.is_empty()) else { continue };
|
||||
match absorb(base, &b.body, &node.faces) {
|
||||
Ok(Some(into)) => {
|
||||
let mut faces = node.faces.clone();
|
||||
faces.push(into.clone());
|
||||
merges.push(Merge { name: format!("{defeature}-F{id}"), body: b.body.clone(), faces, into: Some(into) });
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(e) => errors.push(FeatureError {
|
||||
feature: format!("defeature {defeature}"),
|
||||
message: format!("F{id} on `{}`: {e}", b.body),
|
||||
bodies: vec![b.body.clone()],
|
||||
}),
|
||||
}
|
||||
}
|
||||
report.push(Defeated { defeature, feature: id, method: "virtual", reason: reasons.get(&id).cloned() });
|
||||
}
|
||||
design.errors.extend(errors);
|
||||
design.defeat = Some(Arc::new(Applied { original: base.clone(), defeatures: defeatures.to_vec(), report, merges: merges.clone() }));
|
||||
Ok((design, merges))
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a feature of this kind may be taken out without taking a body or a sketch with it.
|
||||
fn suppressible(kind: &FeatureKind) -> bool {
|
||||
use solidcraft_doc::Operation::NewBody;
|
||||
match kind {
|
||||
FeatureKind::Sketch { .. }
|
||||
| FeatureKind::ConstructionPlane { .. }
|
||||
| FeatureKind::ConstructionAxis { .. }
|
||||
| FeatureKind::ConstructionPoint { .. }
|
||||
| FeatureKind::Combine { .. }
|
||||
| FeatureKind::Split { .. } => false,
|
||||
FeatureKind::Extrude { operation, .. }
|
||||
| FeatureKind::Revolve { operation, .. }
|
||||
| FeatureKind::Sweep { operation, .. }
|
||||
| FeatureKind::Loft { operation, .. }
|
||||
| FeatureKind::Box { operation, .. }
|
||||
| FeatureKind::Cylinder { operation, .. }
|
||||
| FeatureKind::Sphere { operation, .. }
|
||||
| FeatureKind::Torus { operation, .. } => *operation != NewBody,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// `base` rebuilt with `ids` suppressed: `Err(why)` when that fails a feature that built in
|
||||
/// `base` or loses one of its bodies.
|
||||
fn rebuild_without(base: &Design, ids: &BTreeSet<u64>) -> Result<std::result::Result<Design, String>> {
|
||||
let history = base.history.as_deref().ok_or_else(|| Error::Invalid("the design has no feature timeline".into()))?;
|
||||
let mut doc = history.doc.clone();
|
||||
for &id in ids {
|
||||
if let Some(f) = doc.feature_mut(id) {
|
||||
f.suppressed = true;
|
||||
}
|
||||
}
|
||||
let design = crate::design::rebuild(&doc)?;
|
||||
let failed: Vec<String> = design
|
||||
.errors
|
||||
.iter()
|
||||
.filter(|e| !base.errors.iter().any(|b| b.feature == e.feature))
|
||||
.map(|e| format!("{} no longer builds: {}", e.feature, e.message))
|
||||
.collect();
|
||||
if !failed.is_empty() {
|
||||
return Ok(Err(failed.join("; ")));
|
||||
}
|
||||
let lost: Vec<&str> = base.source.iter().map(|b| b.name.as_str()).filter(|n| design.source.iter().all(|b| b.name != *n)).collect();
|
||||
if !lost.is_empty() {
|
||||
return Ok(Err(format!("body {} would be gone", lost.join(", "))));
|
||||
}
|
||||
Ok(Ok(design))
|
||||
}
|
||||
|
||||
/// The face of `body` (in `base` as opened) outside `faces` that `faces` share the longest edges
|
||||
/// with; `None` when the body has none of them.
|
||||
fn absorb(base: &Design, body: &str, faces: &[String]) -> Result<Option<String>> {
|
||||
let Some(shape) = base.source_body(body) else { return Ok(None) };
|
||||
for f in faces {
|
||||
if shape.faces_named(f).is_empty() {
|
||||
continue;
|
||||
}
|
||||
// A face of the feature may border only the feature's own faces (the middle of a
|
||||
// fillet chain): the next one finds the outside.
|
||||
let mut probe = shape.clone();
|
||||
let others: Vec<String> = faces.iter().filter(|g| *g != f && !shape.faces_named(g).is_empty()).cloned().collect();
|
||||
if !others.is_empty() {
|
||||
let mut all = vec![f.clone()];
|
||||
all.extend(others);
|
||||
match shape.merge_faces(f, &all) {
|
||||
Ok(m) => probe = m,
|
||||
Err(_) => continue,
|
||||
}
|
||||
}
|
||||
if let Ok(n) = probe.absorbing_neighbour(f) {
|
||||
return Ok(Some(n));
|
||||
}
|
||||
}
|
||||
Err(Error::Invalid("its faces have no neighbour to merge into".into()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::sample::{Plate, plate_design};
|
||||
|
||||
fn plate() -> Design {
|
||||
crate::open_design(&plate_design(&Plate::default()).unwrap()).unwrap()
|
||||
}
|
||||
|
||||
fn hole(d: &Design) -> u64 {
|
||||
d.features(None).unwrap()[0].features.iter().filter_map(candidate).find(|c| c.category == "hole").unwrap().id
|
||||
}
|
||||
|
||||
/// Suppressing the plate's hole fills it (the body is a solid block, its faces keep their
|
||||
/// names); an empty list brings it back.
|
||||
#[test]
|
||||
fn the_plate_without_its_hole_is_a_block() {
|
||||
let d = plate();
|
||||
let id = hole(&d);
|
||||
let c = d.features(None).unwrap()[0].features.iter().filter_map(candidate).next().unwrap();
|
||||
assert_eq!((c.category, c.size), ("hole", Plate::default().hole_diameter));
|
||||
let def = [Defeature { name: "Holes".into(), features: vec![id] }];
|
||||
let (filled, merges) = d.defeatured(&def).unwrap();
|
||||
assert!(merges.is_empty());
|
||||
let p = Plate::default();
|
||||
let v = filled.bodies[0].measure().unwrap().volume;
|
||||
assert!((v - p.width * p.depth * p.thickness).abs() < 1e-6 * v, "{v}");
|
||||
assert_eq!(filled.bodies[0].faces().len(), 6);
|
||||
assert!(filled.bodies[0].faces().iter().all(|f| d.bodies[0].faces().iter().any(|g| g.name == f.name)));
|
||||
assert_eq!(filled.defeat_report(), [Defeated { defeature: "Holes".into(), feature: id, method: "suppressed", reason: None }]);
|
||||
// The feature graph of the defeatured design leaves the hole out; the design as opened keeps it.
|
||||
assert_eq!(filled.features(None).unwrap()[0].features.len(), 1);
|
||||
assert_eq!(filled.undefeatured().features(None).unwrap()[0].features.len(), 2);
|
||||
// Applied again with the same list: the same design, no rebuild; with none: as opened.
|
||||
let (again, _) = filled.defeatured(&def).unwrap();
|
||||
assert_eq!(again.bodies[0].faces().len(), 6);
|
||||
let (back, _) = filled.defeatured(&[]).unwrap();
|
||||
assert_eq!(back.bodies[0].faces().len(), d.bodies[0].faces().len());
|
||||
assert!(back.defeat_report().is_empty());
|
||||
}
|
||||
|
||||
/// A feature the timeline lacks, or the base extrude, is an error of the defeature, not of
|
||||
/// the call.
|
||||
#[test]
|
||||
fn unknown_and_body_features_are_listed_errors() {
|
||||
let d = plate();
|
||||
let base = d.features(None).unwrap()[0].features[0].id;
|
||||
let (out, _) = d.defeatured(&[Defeature { name: "Bad".into(), features: vec![base, 999] }]).unwrap();
|
||||
let errors: Vec<&str> = out.errors.iter().filter(|e| e.feature == "defeature Bad").map(|e| e.message.as_str()).collect();
|
||||
assert_eq!(errors.len(), 2, "{errors:?}");
|
||||
assert!(errors[0].contains("builds a body") && errors[1].contains("no feature F999"), "{errors:?}");
|
||||
assert_eq!(out.bodies[0].faces().len(), d.bodies[0].faces().len());
|
||||
}
|
||||
|
||||
/// The bracket's fillet is suppressed (back to a sharp corner) and listed as one.
|
||||
#[cfg(feature = "scripts")]
|
||||
#[test]
|
||||
fn a_bracket_fillet_goes_back_to_its_sharp_edge() {
|
||||
let script = r#"[
|
||||
{"command": "sketch.create", "params": {"plane": "XY", "name": "Profile"}},
|
||||
{"command": "sketch.line", "params": {"points": [[0, 0], [60, 0], [60, 8], [8, 8], [8, 50], [0, 50]], "closed": true}},
|
||||
{"command": "sketch.finish", "params": {}},
|
||||
{"command": "solid.extrude", "params": {"distance": 40, "body_name": "Bracket"}},
|
||||
{"command": "solid.fillet", "params": {"edges": [[8, 8, 20]], "radius": 6}},
|
||||
{"command": "solid.hole", "params": {"position": [40, 8, 20], "diameter": 8}}
|
||||
]"#;
|
||||
let d = crate::open_design(&crate::script_design(script).unwrap()).unwrap();
|
||||
let cands: Vec<Candidate> = d.features(None).unwrap()[0].features.iter().filter_map(candidate).collect();
|
||||
assert_eq!(cands.iter().map(|c| (c.category, c.size)).collect::<Vec<_>>(), [("fillet", 6.0), ("hole", 8.0)]);
|
||||
let fillet = cands[0].id;
|
||||
let (sharp, merges) = d.defeatured(&[Defeature { name: "Small".into(), features: vec![fillet] }]).unwrap();
|
||||
assert!(merges.is_empty());
|
||||
let (before, after) = (d.bodies[0].measure().unwrap().volume, sharp.bodies[0].measure().unwrap().volume);
|
||||
// The fillet's material comes out: (1 − π/4) r² × 40 in the inside corner.
|
||||
let fill = (1.0 - std::f64::consts::FRAC_PI_4) * 36.0 * 40.0;
|
||||
assert!((before - after - fill).abs() < 1e-3 * fill, "{before} {after}");
|
||||
assert!(sharp.bodies[0].faces().iter().all(|f| !f.name.starts_with(&format!("F{fillet}:"))));
|
||||
}
|
||||
|
||||
/// A hole whose rim a later fillet rounds cannot be suppressed alone (the fillet would lose
|
||||
/// its edge): it is defeatured virtually, its faces merged into a neighbour, the B-rep kept.
|
||||
#[cfg(feature = "scripts")]
|
||||
#[test]
|
||||
fn a_hole_a_fillet_needs_is_defeatured_virtually() {
|
||||
let script = r#"[
|
||||
{"command": "solid.box", "params": {"length": 40, "width": 40, "height": 10, "body_name": "Block"}},
|
||||
{"command": "solid.hole", "params": {"position": [20, 20, 10], "diameter": 10}},
|
||||
{"command": "solid.fillet", "params": {"edges": [[25, 20, 10]], "radius": 1}}
|
||||
]"#;
|
||||
let d = crate::open_design(&crate::script_design(script).unwrap()).unwrap();
|
||||
assert!(d.errors.is_empty(), "{:?}", d.errors);
|
||||
let graph = d.features(None).unwrap();
|
||||
let hole = graph[0].features.iter().find(|f| f.kind == "hole").unwrap();
|
||||
let (out, merges) = d.defeatured(&[Defeature { name: "H".into(), features: vec![hole.id] }]).unwrap();
|
||||
let report = out.defeat_report();
|
||||
assert_eq!((report.len(), report[0].method), (1, "virtual"), "{report:?}");
|
||||
assert!(report[0].reason.as_deref().is_some_and(|r| r.contains("no longer builds")), "{report:?}");
|
||||
assert_eq!(merges.len(), 1, "{merges:?}");
|
||||
let m = &merges[0];
|
||||
assert!(m.into.as_ref().is_some_and(|i| !hole.faces.contains(i)) && m.faces.len() == hole.faces.len() + 1, "{m:?}");
|
||||
// The B-rep is the design as opened; the merge applies.
|
||||
assert_eq!(out.bodies[0].faces().len(), d.bodies[0].faces().len());
|
||||
let merged = out.merged(&merges).unwrap();
|
||||
assert!(merged.errors.is_empty(), "{:?}", merged.errors);
|
||||
assert_eq!(merged.bodies[0].faces().len(), d.bodies[0].faces().len() - hole.faces.len());
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,8 @@ pub struct Design {
|
||||
pub(crate) opened: std::sync::Arc<Vec<Shape>>,
|
||||
/// The timeline the design was rebuilt from ([`Design::features`]).
|
||||
pub(crate) history: Option<std::sync::Arc<crate::features::History>>,
|
||||
/// The defeatures applied ([`Design::defeatured`]) and the design as opened before them.
|
||||
pub(crate) defeat: Option<std::sync::Arc<crate::defeature::Applied>>,
|
||||
}
|
||||
|
||||
/// A feature of the design that failed to rebuild.
|
||||
@@ -164,6 +166,7 @@ pub(crate) fn rebuild(doc: &Document) -> Result<Design> {
|
||||
errors,
|
||||
cuts: Vec::new(),
|
||||
history: Some(std::sync::Arc::new(history)),
|
||||
defeat: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
pub mod check;
|
||||
pub mod defeature;
|
||||
mod design;
|
||||
mod edges;
|
||||
pub mod face_split;
|
||||
|
||||
@@ -178,6 +178,7 @@ impl Design {
|
||||
source: std::sync::Arc::clone(&self.source),
|
||||
opened: std::sync::Arc::clone(&self.source),
|
||||
history: self.history.clone(),
|
||||
defeat: self.defeat.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user