Add CSG operations to the artifact graph (#6104)

* Add CSG operations to the artifact graph

* Add tool IDs for subtract

* Fix names to match modeling-cmds now that it's done

* Update output since adding CSG ops

* Update formatting

* Add extra solid ids to the graph

* Fix to not add duplicates to the graph
This commit is contained in:
Jonathan Tran
2025-04-11 01:16:29 -04:00
committed by GitHub
parent 6f2e6d14b6
commit 5832890dbb
8 changed files with 152 additions and 1 deletions

View File

@ -115,6 +115,30 @@ impl CodeRef {
} }
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Artifact.ts")]
#[serde(rename_all = "camelCase")]
pub struct CompositeSolid {
pub id: ArtifactId,
pub sub_type: CompositeSolidSubType,
/// Constituent solids of the composite solid.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub solid_ids: Vec<ArtifactId>,
/// Tool solids used for asymmetric operations like subtract.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tool_ids: Vec<ArtifactId>,
pub code_ref: CodeRef,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS)]
#[ts(export_to = "Artifact.ts")]
#[serde(rename_all = "camelCase")]
pub enum CompositeSolidSubType {
Intersect,
Subtract,
Union,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Artifact.ts")] #[ts(export_to = "Artifact.ts")]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@ -318,6 +342,7 @@ pub struct Helix {
#[ts(export_to = "Artifact.ts")] #[ts(export_to = "Artifact.ts")]
#[serde(tag = "type", rename_all = "camelCase")] #[serde(tag = "type", rename_all = "camelCase")]
pub enum Artifact { pub enum Artifact {
CompositeSolid(CompositeSolid),
Plane(Plane), Plane(Plane),
Path(Path), Path(Path),
Segment(Segment), Segment(Segment),
@ -336,6 +361,7 @@ pub enum Artifact {
impl Artifact { impl Artifact {
pub(crate) fn id(&self) -> ArtifactId { pub(crate) fn id(&self) -> ArtifactId {
match self { match self {
Artifact::CompositeSolid(a) => a.id,
Artifact::Plane(a) => a.id, Artifact::Plane(a) => a.id,
Artifact::Path(a) => a.id, Artifact::Path(a) => a.id,
Artifact::Segment(a) => a.id, Artifact::Segment(a) => a.id,
@ -355,6 +381,7 @@ impl Artifact {
#[expect(dead_code)] #[expect(dead_code)]
pub(crate) fn code_ref(&self) -> Option<&CodeRef> { pub(crate) fn code_ref(&self) -> Option<&CodeRef> {
match self { match self {
Artifact::CompositeSolid(a) => Some(&a.code_ref),
Artifact::Plane(a) => Some(&a.code_ref), Artifact::Plane(a) => Some(&a.code_ref),
Artifact::Path(a) => Some(&a.code_ref), Artifact::Path(a) => Some(&a.code_ref),
Artifact::Segment(a) => Some(&a.code_ref), Artifact::Segment(a) => Some(&a.code_ref),
@ -375,6 +402,7 @@ impl Artifact {
/// type, return the new artifact which should be used as a replacement. /// type, return the new artifact which should be used as a replacement.
fn merge(&mut self, new: Artifact) -> Option<Artifact> { fn merge(&mut self, new: Artifact) -> Option<Artifact> {
match self { match self {
Artifact::CompositeSolid(a) => a.merge(new),
Artifact::Plane(a) => a.merge(new), Artifact::Plane(a) => a.merge(new),
Artifact::Path(a) => a.merge(new), Artifact::Path(a) => a.merge(new),
Artifact::Segment(a) => a.merge(new), Artifact::Segment(a) => a.merge(new),
@ -392,6 +420,18 @@ impl Artifact {
} }
} }
impl CompositeSolid {
fn merge(&mut self, new: Artifact) -> Option<Artifact> {
let Artifact::CompositeSolid(new) = new else {
return Some(new);
};
merge_ids(&mut self.solid_ids, new.solid_ids);
merge_ids(&mut self.tool_ids, new.tool_ids);
None
}
}
impl Plane { impl Plane {
fn merge(&mut self, new: Artifact) -> Option<Artifact> { fn merge(&mut self, new: Artifact) -> Option<Artifact> {
let Artifact::Plane(new) = new else { let Artifact::Plane(new) = new else {
@ -1047,6 +1087,85 @@ fn artifacts_to_update(
// the helix here, but it's not useful right now. // the helix here, but it's not useful right now.
return Ok(return_arr); return Ok(return_arr);
} }
ModelingCmd::BooleanIntersection(_) | ModelingCmd::BooleanSubtract(_) | ModelingCmd::BooleanUnion(_) => {
let (sub_type, solid_ids, tool_ids) = match cmd {
ModelingCmd::BooleanIntersection(intersection) => {
let solid_ids = intersection
.solid_ids
.iter()
.copied()
.map(ArtifactId::new)
.collect::<Vec<_>>();
(CompositeSolidSubType::Intersect, solid_ids, Vec::new())
}
ModelingCmd::BooleanSubtract(subtract) => {
let solid_ids = subtract
.target_ids
.iter()
.copied()
.map(ArtifactId::new)
.collect::<Vec<_>>();
let tool_ids = subtract
.tool_ids
.iter()
.copied()
.map(ArtifactId::new)
.collect::<Vec<_>>();
(CompositeSolidSubType::Subtract, solid_ids, tool_ids)
}
ModelingCmd::BooleanUnion(union) => {
let solid_ids = union.solid_ids.iter().copied().map(ArtifactId::new).collect::<Vec<_>>();
(CompositeSolidSubType::Union, solid_ids, Vec::new())
}
_ => unreachable!(),
};
let mut new_solid_ids = vec![id];
match response {
OkModelingCmdResponse::BooleanIntersection(intersection) => intersection
.extra_solid_ids
.iter()
.copied()
.map(ArtifactId::new)
.for_each(|id| new_solid_ids.push(id)),
OkModelingCmdResponse::BooleanSubtract(subtract) => subtract
.extra_solid_ids
.iter()
.copied()
.map(ArtifactId::new)
.for_each(|id| new_solid_ids.push(id)),
OkModelingCmdResponse::BooleanUnion(union) => union
.extra_solid_ids
.iter()
.copied()
.map(ArtifactId::new)
.for_each(|id| new_solid_ids.push(id)),
_ => {}
}
let return_arr = new_solid_ids
.into_iter()
// Extra solid IDs may include the command's ID. Make sure we
// don't create a duplicate.
.filter(|solid_id| *solid_id != id)
.map(|solid_id| {
Artifact::CompositeSolid(CompositeSolid {
id: solid_id,
sub_type,
solid_ids: solid_ids.clone(),
tool_ids: tool_ids.clone(),
code_ref: CodeRef {
range,
path_to_node: path_to_node.clone(),
},
})
})
.collect::<Vec<_>>();
// TODO: Should we add the reverse graph edges?
return Ok(return_arr);
}
_ => {} _ => {}
} }

View File

@ -67,6 +67,11 @@ impl Artifact {
/// the graph. This should be disjoint with `child_ids`. /// the graph. This should be disjoint with `child_ids`.
pub(crate) fn back_edges(&self) -> Vec<ArtifactId> { pub(crate) fn back_edges(&self) -> Vec<ArtifactId> {
match self { match self {
Artifact::CompositeSolid(a) => {
let mut ids = a.solid_ids.clone();
ids.extend(a.tool_ids.iter());
ids
}
Artifact::Plane(_) => Vec::new(), Artifact::Plane(_) => Vec::new(),
Artifact::Path(a) => vec![a.plane_id], Artifact::Path(a) => vec![a.plane_id],
Artifact::Segment(a) => vec![a.path_id], Artifact::Segment(a) => vec![a.path_id],
@ -87,6 +92,11 @@ impl Artifact {
/// the graph. /// the graph.
pub(crate) fn child_ids(&self) -> Vec<ArtifactId> { pub(crate) fn child_ids(&self) -> Vec<ArtifactId> {
match self { match self {
Artifact::CompositeSolid(_) => {
// Note: Don't include these since they're parents: solid_ids,
// tool_ids.
Vec::new()
}
Artifact::Plane(a) => a.path_ids.clone(), Artifact::Plane(a) => a.path_ids.clone(),
Artifact::Path(a) => { Artifact::Path(a) => {
// Note: Don't include these since they're parents: plane_id. // Note: Don't include these since they're parents: plane_id.
@ -213,6 +223,7 @@ impl ArtifactGraph {
let id = artifact.id(); let id = artifact.id();
let grouped = match artifact { let grouped = match artifact {
Artifact::CompositeSolid(_) => false,
Artifact::Plane(_) => false, Artifact::Plane(_) => false,
Artifact::Path(_) => { Artifact::Path(_) => {
groups.entry(id).or_insert_with(Vec::new).push(id); groups.entry(id).or_insert_with(Vec::new).push(id);
@ -278,6 +289,15 @@ impl ArtifactGraph {
} }
match artifact { match artifact {
Artifact::CompositeSolid(composite_solid) => {
writeln!(
output,
"{prefix}{}[\"CompositeSolid {:?}<br>{:?}\"]",
id,
composite_solid.sub_type,
code_ref_display(&composite_solid.code_ref)
)?;
}
Artifact::Plane(plane) => { Artifact::Plane(plane) => {
writeln!( writeln!(
output, output,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 57 KiB

View File

@ -48,6 +48,7 @@ flowchart LR
42["SweepEdge Adjacent"] 42["SweepEdge Adjacent"]
43["SweepEdge Opposite"] 43["SweepEdge Opposite"]
44["SweepEdge Adjacent"] 44["SweepEdge Adjacent"]
45["CompositeSolid Intersect<br>[448, 477, 0]"]
1 --- 2 1 --- 2
2 --- 3 2 --- 3
2 --- 4 2 --- 4
@ -114,4 +115,6 @@ flowchart LR
30 --- 42 30 --- 42
30 --- 43 30 --- 43
30 --- 44 30 --- 44
2 <--x 45
24 <--x 45
``` ```

View File

@ -36,6 +36,7 @@ flowchart LR
30["Cap End"] 30["Cap End"]
31["SweepEdge Opposite"] 31["SweepEdge Opposite"]
32["SweepEdge Adjacent"] 32["SweepEdge Adjacent"]
33["CompositeSolid Subtract<br>[461, 497, 0]"]
1 --- 2 1 --- 2
2 --- 3 2 --- 3
2 --- 4 2 --- 4
@ -81,4 +82,6 @@ flowchart LR
27 --- 30 27 --- 30
27 --- 31 27 --- 31
27 --- 32 27 --- 32
2 <--x 33
24 <--x 33
``` ```

View File

@ -48,6 +48,7 @@ flowchart LR
42["SweepEdge Adjacent"] 42["SweepEdge Adjacent"]
43["SweepEdge Opposite"] 43["SweepEdge Opposite"]
44["SweepEdge Adjacent"] 44["SweepEdge Adjacent"]
45["CompositeSolid Union<br>[448, 473, 0]"]
1 --- 2 1 --- 2
2 --- 3 2 --- 3
2 --- 4 2 --- 4
@ -114,4 +115,6 @@ flowchart LR
30 --- 42 30 --- 42
30 --- 43 30 --- 43
30 --- 44 30 --- 44
2 <--x 45
24 <--x 45
``` ```

View File

@ -514,7 +514,9 @@ export function getCodeRefsByArtifactId(
artifactGraph: ArtifactGraph artifactGraph: ArtifactGraph
): Array<CodeRef> | null { ): Array<CodeRef> | null {
const artifact = artifactGraph.get(id) const artifact = artifactGraph.get(id)
if (artifact?.type === 'solid2d') { if (artifact?.type === 'compositeSolid') {
return [artifact.codeRef]
} else if (artifact?.type === 'solid2d') {
const codeRef = getSolid2dCodeRef(artifact, artifactGraph) const codeRef = getSolid2dCodeRef(artifact, artifactGraph)
if (err(codeRef)) return null if (err(codeRef)) return null
return [codeRef] return [codeRef]

View File

@ -71,6 +71,7 @@ export type {
ArtifactId, ArtifactId,
Cap as CapArtifact, Cap as CapArtifact,
CodeRef, CodeRef,
CompositeSolid as CompositeSolidArtifact,
EdgeCut, EdgeCut,
Path as PathArtifact, Path as PathArtifact,
Plane as PlaneArtifact, Plane as PlaneArtifact,