Skip to content

Commit cc2f703

Browse files
gsdaliclaude
andcommitted
feat(graph): graph-select verb + convexity-attributed face-adjacency in graph-ml
Closes #54, #55. #54 — new `graph-select` verb for local B-rep graph queries without dumping the whole graph: face-neighbors (+ convexity/shared-edge count via the kernel AAG), edge-faces (faces/vertices/boundary/manifold flags), vertex-edges, face-adjacency (full gAAG), and edges-class (boundary|non-manifold|seam|degenerate). The selection/pointer primitive for DSL selectors + GNN face-adjacency selection. #55 — graph-ml now emits a faceAdjacency block [{face1,face2,convexity,sharedEdgeCount}] from the AAG (the gAAG edge attribute; convexity is the top edge feature in B-rep GNNs). Face indices follow shape.faces() order (the face[N] scheme query-topology emits). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ba1013e commit cc2f703

3 files changed

Lines changed: 214 additions & 0 deletions

File tree

Sources/occtkit/Commands/GraphML.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,19 @@ enum GraphMLCommand: Subcommand {
1818
let edgeToVertex: COO
1919
let faces: [Face]
2020
let edges: [Edge]
21+
// Convexity-attributed face-adjacency (the gAAG edge attribute used by
22+
// B-rep GNNs). Face indices follow shape.faces() order — the `face[N]`
23+
// scheme query-topology emits. Added OCCTSwiftScripts#55.
24+
let faceAdjacency: [FaceAdjacency]
2125
let sampling: Sampling
2226

2327
struct COO: Codable { let sources: [Int]; let targets: [Int] }
28+
struct FaceAdjacency: Codable {
29+
let face1: Int
30+
let face2: Int
31+
let convexity: String // convex | concave | smooth
32+
let sharedEdgeCount: Int
33+
}
2434
struct Face: Codable {
2535
let index: Int
2636
let uSamples: Int
@@ -64,6 +74,17 @@ enum GraphMLCommand: Subcommand {
6474
return Payload.Edge(index: i, samples: pts.map { [$0.x, $0.y, $0.z] })
6575
}
6676

77+
// Attributed face-adjacency with per-adjacency convexity, from the AAG.
78+
let aag = AAG(shape: shape)
79+
let faceAdjacency: [Payload.FaceAdjacency] = aag.edges.map {
80+
Payload.FaceAdjacency(
81+
face1: $0.face1Index,
82+
face2: $0.face2Index,
83+
convexity: $0.convexity.label,
84+
sharedEdgeCount: $0.sharedEdgeCount
85+
)
86+
}
87+
6788
let payload = Payload(
6889
vertexPositions: g.vertexPositions,
6990
edgeBoundaryFlags: g.edgeBoundaryFlags,
@@ -74,6 +95,7 @@ enum GraphMLCommand: Subcommand {
7495
edgeToVertex: Payload.COO(sources: g.edgeToVertex.sources, targets: g.edgeToVertex.targets),
7596
faces: faces,
7697
edges: edges,
98+
faceAdjacency: faceAdjacency,
7799
sampling: Payload.Sampling(uvSamples: uvSamples, edgeSamples: edgeSamples)
78100
)
79101
try GraphIO.emitJSON(payload)
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
// GraphSelect — direct B-rep graph adjacency / selection queries.
2+
//
3+
// Closes OCCTSwiftScripts#54. Lets a consumer answer a *local* topology
4+
// question — "what is adjacent to face[N]?", "faces of edge[M]?", "edges of
5+
// vertex[K]?", "all boundary edges", "the convex/concave face adjacencies" —
6+
// without exporting and re-parsing the whole graph (graph-ml). This is the
7+
// selection / "pointer" primitive behind DSL-style selectors and the
8+
// face-adjacency-graph + GNN selection used in the generative-CAD literature
9+
// (UV-Net, Pointer-CAD, AAGNet).
10+
//
11+
// Index spaces: face queries run over the Attributed Adjacency Graph (AAG),
12+
// whose face indices follow `shape.faces()` order — the same `face[N]` scheme
13+
// `query-topology` emits. Edge/vertex queries run over the TopologyGraph
14+
// (`edge[M]` / `vertex[K]`). Convexity is a property of the dihedral between two
15+
// faces, so it is reported on face *adjacencies* (AAG edges), not on a lone edge.
16+
//
17+
// Usage:
18+
// graph-select <shape.brep> --query face-neighbors --face N
19+
// graph-select <shape.brep> --query edge-faces --edge M
20+
// graph-select <shape.brep> --query vertex-edges --vertex K
21+
// graph-select <shape.brep> --query face-adjacency
22+
// graph-select <shape.brep> --query edges-class --class boundary|non-manifold|seam|degenerate
23+
24+
import Foundation
25+
import OCCTSwift
26+
import ScriptHarness
27+
28+
extension EdgeConvexity {
29+
/// Stable lowercase label for JSON output.
30+
var label: String {
31+
switch self {
32+
case .concave: return "concave"
33+
case .smooth: return "smooth"
34+
case .convex: return "convex"
35+
}
36+
}
37+
}
38+
39+
enum GraphSelectCommand: Subcommand {
40+
static let name = "graph-select"
41+
static let summary = "Query B-rep graph adjacency / selection (face neighbours, edge faces, vertex edges, convexity)"
42+
static let usage = """
43+
Usage:
44+
graph-select <shape.brep> --query <type> [ids]
45+
46+
Queries:
47+
--query face-neighbors --face N faces adjacent to face N (+ convexity, shared-edge count)
48+
--query edge-faces --edge M faces / vertices / flags of edge M
49+
--query vertex-edges --vertex K edges incident to vertex K
50+
--query face-adjacency full attributed face-adjacency graph (gAAG)
51+
--query edges-class --class K edge indices matching: boundary | non-manifold | seam | degenerate
52+
53+
Face indices follow shape.faces() order (the `face[N]` scheme query-topology emits);
54+
edge/vertex indices are TopologyGraph indices (`edge[M]` / `vertex[K]`).
55+
"""
56+
57+
// MARK: responses
58+
59+
struct NeighbourOut: Encodable { let face: Int; let convexity: String; let sharedEdgeCount: Int }
60+
struct FaceNeighboursResponse: Encodable {
61+
let query = "face-neighbors"
62+
let face: Int
63+
let isPlanar: Bool
64+
let isVertical: Bool
65+
let isHorizontal: Bool
66+
let normal: [Double]?
67+
let neighbors: [NeighbourOut]
68+
}
69+
struct EdgeFacesResponse: Encodable {
70+
let query = "edge-faces"
71+
let edge: Int
72+
let faces: [Int]
73+
let startVertex: Int?
74+
let endVertex: Int?
75+
let boundary: Bool
76+
let manifold: Bool
77+
}
78+
struct VertexEdgesResponse: Encodable {
79+
let query = "vertex-edges"
80+
let vertex: Int
81+
let edges: [Int]
82+
}
83+
struct FaceAdj: Encodable { let face1: Int; let face2: Int; let convexity: String; let sharedEdgeCount: Int }
84+
struct FaceAdjacencyResponse: Encodable {
85+
let query = "face-adjacency"
86+
let faceCount: Int
87+
let adjacencies: [FaceAdj]
88+
}
89+
struct EdgesClassResponse: Encodable {
90+
let query = "edges-class"
91+
let `class`: String
92+
let edges: [Int]
93+
}
94+
95+
// MARK: run
96+
97+
static func run(args: [String]) throws -> Int32 {
98+
guard let path = args.first(where: { !$0.hasPrefix("--") }) else {
99+
throw ScriptError.message(usage)
100+
}
101+
let queryType = value("--query", in: args) ?? "face-adjacency"
102+
let shape = try GraphIO.loadBREP(at: path)
103+
104+
switch queryType {
105+
case "face-neighbors":
106+
let aag = AAG(shape: shape)
107+
let face = try intValue("--face", in: args)
108+
guard face >= 0 && face < aag.nodes.count else {
109+
throw ScriptError.message("face \(face) out of range (0..<\(aag.nodes.count))")
110+
}
111+
let node = aag.nodes[face]
112+
let neighbors = aag.neighbors(of: face).sorted().map { nb -> NeighbourOut in
113+
let e = aag.edge(between: face, and: nb)
114+
return NeighbourOut(face: nb,
115+
convexity: (e?.convexity ?? .smooth).label,
116+
sharedEdgeCount: e?.sharedEdgeCount ?? 0)
117+
}
118+
try GraphIO.emitJSON(FaceNeighboursResponse(
119+
face: face,
120+
isPlanar: node.isPlanar,
121+
isVertical: node.isVertical,
122+
isHorizontal: node.isHorizontal,
123+
normal: node.normal.map { [$0.x, $0.y, $0.z] },
124+
neighbors: neighbors))
125+
126+
case "edge-faces":
127+
let g = try GraphIO.buildGraph(from: shape)
128+
let edge = try intValue("--edge", in: args)
129+
guard edge >= 0 && edge < g.edgeCount else {
130+
throw ScriptError.message("edge \(edge) out of range (0..<\(g.edgeCount))")
131+
}
132+
try GraphIO.emitJSON(EdgeFacesResponse(
133+
edge: edge,
134+
faces: g.faces(of: edge),
135+
startVertex: g.edgeStartVertex(edge),
136+
endVertex: g.edgeEndVertex(edge),
137+
boundary: g.isBoundaryEdge(edge),
138+
manifold: g.isManifoldEdge(edge)))
139+
140+
case "vertex-edges":
141+
let g = try GraphIO.buildGraph(from: shape)
142+
let vertex = try intValue("--vertex", in: args)
143+
guard vertex >= 0 && vertex < g.vertexCount else {
144+
throw ScriptError.message("vertex \(vertex) out of range (0..<\(g.vertexCount))")
145+
}
146+
try GraphIO.emitJSON(VertexEdgesResponse(vertex: vertex, edges: g.edges(of: vertex)))
147+
148+
case "face-adjacency":
149+
let aag = AAG(shape: shape)
150+
let adjacencies = aag.edges.map {
151+
FaceAdj(face1: $0.face1Index, face2: $0.face2Index,
152+
convexity: $0.convexity.label, sharedEdgeCount: $0.sharedEdgeCount)
153+
}
154+
try GraphIO.emitJSON(FaceAdjacencyResponse(faceCount: aag.nodes.count, adjacencies: adjacencies))
155+
156+
case "edges-class":
157+
let g = try GraphIO.buildGraph(from: shape)
158+
let kind = value("--class", in: args) ?? "boundary"
159+
let matches: [Int] = (0..<g.edgeCount).filter { i in
160+
switch kind {
161+
case "boundary": return g.isBoundaryEdge(i)
162+
case "non-manifold": return !g.isManifoldEdge(i)
163+
case "seam": return g.edgeCoEdges(i).contains { g.coedgeSeamPair($0) != nil }
164+
case "degenerate": return g.isEdgeDegenerated(i)
165+
default: return false
166+
}
167+
}
168+
guard ["boundary", "non-manifold", "seam", "degenerate"].contains(kind) else {
169+
throw ScriptError.message("--class must be boundary | non-manifold | seam | degenerate")
170+
}
171+
try GraphIO.emitJSON(EdgesClassResponse(class: kind, edges: matches))
172+
173+
default:
174+
throw ScriptError.message("Unknown --query '\(queryType)'.\n\(usage)")
175+
}
176+
return 0
177+
}
178+
179+
// MARK: arg helpers
180+
181+
private static func value(_ name: String, in args: [String]) -> String? {
182+
guard let i = args.firstIndex(of: name), i + 1 < args.count else { return nil }
183+
return args[i + 1]
184+
}
185+
private static func intValue(_ name: String, in args: [String]) throws -> Int {
186+
guard let raw = value(name, in: args), let n = Int(raw) else {
187+
throw ScriptError.message("\(name) <Int> is required for this query")
188+
}
189+
return n
190+
}
191+
}

Sources/occtkit/Subcommand.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ enum Registry {
1919
GraphDedupCommand.self,
2020
GraphQueryCommand.self,
2121
GraphMLCommand.self,
22+
GraphSelectCommand.self,
2223
FeatureRecognizeCommand.self,
2324
DXFExportCommand.self,
2425
DrawingExportCommand.self,

0 commit comments

Comments
 (0)