Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ OCCTSwiftScripts is a script harness for rapid OCCTSwift geometry iteration —

See `docs/SCRIPT_WORKFLOW.md` for the script iteration workflow.

**Knowledge management**: durable, cross-cutting project knowledge lives in `docs/knowledge/`
— an **OKF** bundle (Open Knowledge Format: categorized markdown + YAML frontmatter, from
Google's Knowledge Catalog). Start at `docs/knowledge/index.md`. `CLAUDE.md` remains the
detailed quick reference; the bundle holds policies, durable decisions, and the relationship
to downstream consumers (e.g. the commercial OCCTStudio app). Record durable decisions/policies
as OKF entries (+ a `log.md` line), not only in chat or commit messages.

## Build & Run

```bash
Expand Down
22 changes: 22 additions & 0 deletions Sources/occtkit/Commands/GraphML.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,19 @@ enum GraphMLCommand: Subcommand {
let edgeToVertex: COO
let faces: [Face]
let edges: [Edge]
// Convexity-attributed face-adjacency (the gAAG edge attribute used by
// B-rep GNNs). Face indices follow shape.faces() order — the `face[N]`
// scheme query-topology emits. Added OCCTSwiftScripts#55.
let faceAdjacency: [FaceAdjacency]
let sampling: Sampling

struct COO: Codable { let sources: [Int]; let targets: [Int] }
struct FaceAdjacency: Codable {
let face1: Int
let face2: Int
let convexity: String // convex | concave | smooth
let sharedEdgeCount: Int
}
struct Face: Codable {
let index: Int
let uSamples: Int
Expand Down Expand Up @@ -64,6 +74,17 @@ enum GraphMLCommand: Subcommand {
return Payload.Edge(index: i, samples: pts.map { [$0.x, $0.y, $0.z] })
}

// Attributed face-adjacency with per-adjacency convexity, from the AAG.
let aag = AAG(shape: shape)
let faceAdjacency: [Payload.FaceAdjacency] = aag.edges.map {
Payload.FaceAdjacency(
face1: $0.face1Index,
face2: $0.face2Index,
convexity: $0.convexity.label,
sharedEdgeCount: $0.sharedEdgeCount
)
}

let payload = Payload(
vertexPositions: g.vertexPositions,
edgeBoundaryFlags: g.edgeBoundaryFlags,
Expand All @@ -74,6 +95,7 @@ enum GraphMLCommand: Subcommand {
edgeToVertex: Payload.COO(sources: g.edgeToVertex.sources, targets: g.edgeToVertex.targets),
faces: faces,
edges: edges,
faceAdjacency: faceAdjacency,
sampling: Payload.Sampling(uvSamples: uvSamples, edgeSamples: edgeSamples)
)
try GraphIO.emitJSON(payload)
Expand Down
191 changes: 191 additions & 0 deletions Sources/occtkit/Commands/GraphSelect.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// GraphSelect — direct B-rep graph adjacency / selection queries.
//
// Closes OCCTSwiftScripts#54. Lets a consumer answer a *local* topology
// question — "what is adjacent to face[N]?", "faces of edge[M]?", "edges of
// vertex[K]?", "all boundary edges", "the convex/concave face adjacencies" —
// without exporting and re-parsing the whole graph (graph-ml). This is the
// selection / "pointer" primitive behind DSL-style selectors and the
// face-adjacency-graph + GNN selection used in the generative-CAD literature
// (UV-Net, Pointer-CAD, AAGNet).
//
// Index spaces: face queries run over the Attributed Adjacency Graph (AAG),
// whose face indices follow `shape.faces()` order — the same `face[N]` scheme
// `query-topology` emits. Edge/vertex queries run over the TopologyGraph
// (`edge[M]` / `vertex[K]`). Convexity is a property of the dihedral between two
// faces, so it is reported on face *adjacencies* (AAG edges), not on a lone edge.
//
// Usage:
// graph-select <shape.brep> --query face-neighbors --face N
// graph-select <shape.brep> --query edge-faces --edge M
// graph-select <shape.brep> --query vertex-edges --vertex K
// graph-select <shape.brep> --query face-adjacency
// graph-select <shape.brep> --query edges-class --class boundary|non-manifold|seam|degenerate

import Foundation
import OCCTSwift
import ScriptHarness

extension EdgeConvexity {
/// Stable lowercase label for JSON output.
var label: String {
switch self {
case .concave: return "concave"
case .smooth: return "smooth"
case .convex: return "convex"
}
}
}

enum GraphSelectCommand: Subcommand {
static let name = "graph-select"
static let summary = "Query B-rep graph adjacency / selection (face neighbours, edge faces, vertex edges, convexity)"
static let usage = """
Usage:
graph-select <shape.brep> --query <type> [ids]

Queries:
--query face-neighbors --face N faces adjacent to face N (+ convexity, shared-edge count)
--query edge-faces --edge M faces / vertices / flags of edge M
--query vertex-edges --vertex K edges incident to vertex K
--query face-adjacency full attributed face-adjacency graph (gAAG)
--query edges-class --class K edge indices matching: boundary | non-manifold | seam | degenerate

Face indices follow shape.faces() order (the `face[N]` scheme query-topology emits);
edge/vertex indices are TopologyGraph indices (`edge[M]` / `vertex[K]`).
"""

// MARK: responses

struct NeighbourOut: Encodable { let face: Int; let convexity: String; let sharedEdgeCount: Int }
struct FaceNeighboursResponse: Encodable {
let query = "face-neighbors"
let face: Int
let isPlanar: Bool
let isVertical: Bool
let isHorizontal: Bool
let normal: [Double]?
let neighbors: [NeighbourOut]
}
struct EdgeFacesResponse: Encodable {
let query = "edge-faces"
let edge: Int
let faces: [Int]
let startVertex: Int?
let endVertex: Int?
let boundary: Bool
let manifold: Bool
}
struct VertexEdgesResponse: Encodable {
let query = "vertex-edges"
let vertex: Int
let edges: [Int]
}
struct FaceAdj: Encodable { let face1: Int; let face2: Int; let convexity: String; let sharedEdgeCount: Int }
struct FaceAdjacencyResponse: Encodable {
let query = "face-adjacency"
let faceCount: Int
let adjacencies: [FaceAdj]
}
struct EdgesClassResponse: Encodable {
let query = "edges-class"
let `class`: String
let edges: [Int]
}

// MARK: run

static func run(args: [String]) throws -> Int32 {
guard let path = args.first(where: { !$0.hasPrefix("--") }) else {
throw ScriptError.message(usage)
}
let queryType = value("--query", in: args) ?? "face-adjacency"
let shape = try GraphIO.loadBREP(at: path)

switch queryType {
case "face-neighbors":
let aag = AAG(shape: shape)
let face = try intValue("--face", in: args)
guard face >= 0 && face < aag.nodes.count else {
throw ScriptError.message("face \(face) out of range (0..<\(aag.nodes.count))")
}
let node = aag.nodes[face]
let neighbors = aag.neighbors(of: face).sorted().map { nb -> NeighbourOut in
let e = aag.edge(between: face, and: nb)
return NeighbourOut(face: nb,
convexity: (e?.convexity ?? .smooth).label,
sharedEdgeCount: e?.sharedEdgeCount ?? 0)
}
try GraphIO.emitJSON(FaceNeighboursResponse(
face: face,
isPlanar: node.isPlanar,
isVertical: node.isVertical,
isHorizontal: node.isHorizontal,
normal: node.normal.map { [$0.x, $0.y, $0.z] },
neighbors: neighbors))

case "edge-faces":
let g = try GraphIO.buildGraph(from: shape)
let edge = try intValue("--edge", in: args)
guard edge >= 0 && edge < g.edgeCount else {
throw ScriptError.message("edge \(edge) out of range (0..<\(g.edgeCount))")
}
try GraphIO.emitJSON(EdgeFacesResponse(
edge: edge,
faces: g.faces(of: edge),
startVertex: g.edgeStartVertex(edge),
endVertex: g.edgeEndVertex(edge),
boundary: g.isBoundaryEdge(edge),
manifold: g.isManifoldEdge(edge)))

case "vertex-edges":
let g = try GraphIO.buildGraph(from: shape)
let vertex = try intValue("--vertex", in: args)
guard vertex >= 0 && vertex < g.vertexCount else {
throw ScriptError.message("vertex \(vertex) out of range (0..<\(g.vertexCount))")
}
try GraphIO.emitJSON(VertexEdgesResponse(vertex: vertex, edges: g.edges(of: vertex)))

case "face-adjacency":
let aag = AAG(shape: shape)
let adjacencies = aag.edges.map {
FaceAdj(face1: $0.face1Index, face2: $0.face2Index,
convexity: $0.convexity.label, sharedEdgeCount: $0.sharedEdgeCount)
}
try GraphIO.emitJSON(FaceAdjacencyResponse(faceCount: aag.nodes.count, adjacencies: adjacencies))

case "edges-class":
let g = try GraphIO.buildGraph(from: shape)
let kind = value("--class", in: args) ?? "boundary"
let matches: [Int] = (0..<g.edgeCount).filter { i in
switch kind {
case "boundary": return g.isBoundaryEdge(i)
case "non-manifold": return !g.isManifoldEdge(i)
case "seam": return g.edgeCoEdges(i).contains { g.coedgeSeamPair($0) != nil }
case "degenerate": return g.isEdgeDegenerated(i)
default: return false
}
}
guard ["boundary", "non-manifold", "seam", "degenerate"].contains(kind) else {
throw ScriptError.message("--class must be boundary | non-manifold | seam | degenerate")
}
try GraphIO.emitJSON(EdgesClassResponse(class: kind, edges: matches))

default:
throw ScriptError.message("Unknown --query '\(queryType)'.\n\(usage)")
}
return 0
}

// MARK: arg helpers

private static func value(_ name: String, in args: [String]) -> String? {
guard let i = args.firstIndex(of: name), i + 1 < args.count else { return nil }
return args[i + 1]
}
private static func intValue(_ name: String, in args: [String]) throws -> Int {
guard let raw = value(name, in: args), let n = Int(raw) else {
throw ScriptError.message("\(name) <Int> is required for this query")
}
return n
}
}
1 change: 1 addition & 0 deletions Sources/occtkit/Subcommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ enum Registry {
GraphDedupCommand.self,
GraphQueryCommand.self,
GraphMLCommand.self,
GraphSelectCommand.self,
FeatureRecognizeCommand.self,
DXFExportCommand.self,
DrawingExportCommand.self,
Expand Down
6 changes: 6 additions & 0 deletions docs/knowledge/architecture/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Architecture

The detailed architecture (targets, the `occtkit` umbrella + verb registry, the `--serve`
envelope protocol, the output pipeline, the dependency cohort) is documented in **`CLAUDE.md`**
at the repo root — that is the source of truth. Add OKF entries here only for durable
architectural decisions that need rationale captured beyond the reference in `CLAUDE.md`.
4 changes: 4 additions & 0 deletions docs/knowledge/decisions/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Decisions

Recorded engineering decisions and their rationale. (Seeded as durable decisions arise; the
historical decisions to date are captured in `CLAUDE.md` and the git log.)
19 changes: 19 additions & 0 deletions docs/knowledge/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# OCCTSwiftScripts — Knowledge Bundle

OKF-conformant durable knowledge for OCCTSwiftScripts (the OSS script harness + `occtkit` CLI
for OCCTSwift geometry iteration). See [What is OKF](references/okf.md) for the format; start
at the [overview](overview.md).

This complements `CLAUDE.md` (the always-loaded, detailed quick reference — the per-verb
detail lives there). The bundle holds durable, cross-cutting knowledge: policies, key
decisions, and the relationship to downstream consumers.

## Sections

* [Overview](overview.md) — what this repo is and its boundary.
* [Architecture](architecture/index.md) — pointer to the `CLAUDE.md` architecture.
* [Strategies](strategies/index.md) — how-we-do-it notes.
* [Policies](policies/index.md) — standing rules (the open-source boundary).
* [Decisions](decisions/index.md) — recorded decisions.
* [References](references/index.md) — OKF, the commercial-app relationship.
* [Operations](operations/index.md) — build / run playbooks.
7 changes: 7 additions & 0 deletions docs/knowledge/log.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Knowledge Log

## 2026-06-18

* **Creation**: Initialised the OKF knowledge bundle.
* **Creation**: Recorded the open-source-boundary policy and the relationship to the
downstream commercial app (OCCTStudio), which consumes the `reconstruct` feature-graph IR.
5 changes: 5 additions & 0 deletions docs/knowledge/operations/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Operations

Build / test / run playbooks. The canonical commands (`swift build`, `swift run Script`,
`swift run occtkit <verb>`, `make install`) and conventions are in `CLAUDE.md`; add playbooks
here for multi-step operational procedures that don't fit there.
26 changes: 26 additions & 0 deletions docs/knowledge/overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
type: Project
title: OCCTSwiftScripts overview
description: OSS script harness for rapid OCCTSwift geometry iteration plus the headless occtkit CLI of reusable verbs for downstream consumers.
resource: /
tags: [project, overview, occtkit, oss]
timestamp: 2026-06-18T00:00:00Z
---

# What this is

A script harness for rapid OCCTSwift geometry iteration — the OCCTSwift equivalent of CadQuery
or OpenSCAD — plus a headless CLI (`occtkit`) bundling reusable verbs (graph-validate,
reconstruct, drawing-export, render-preview, …) for downstream consumers (OCCTMCP, the
OCCTStudio app, Python pipelines).

The full architecture, the verb inventory, the `--serve` protocol, and the dependency cohort
are documented in detail in **`CLAUDE.md`** — that remains the source of truth for
implementation detail. This bundle holds the durable, cross-cutting knowledge.

# Boundary

LGPL-2.1, OSS, depends only on open-source Swift packages. See
[policies/open-source-boundary](policies/open-source-boundary.md). The commercial **OCCTStudio**
app consumes this repo's `reconstruct` verb but lives in a separate private repo — see
[references/commercial-app-relationship](references/commercial-app-relationship.md).
3 changes: 3 additions & 0 deletions docs/knowledge/policies/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Policies

* [Open-source boundary](open-source-boundary.md) - LGPL-only deps; never depend on closed-source projects.
22 changes: 22 additions & 0 deletions docs/knowledge/policies/open-source-boundary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
type: Policy
title: Open-source boundary
description: This repo is LGPL-2.1 and depends only on open-source Swift packages. Never propose anything that makes it depend on a closed-source project.
resource: /
tags: [policy, oss, licensing, boundary]
timestamp: 2026-06-18T00:00:00Z
---

# Policy

OCCTSwiftScripts is **LGPL-2.1** and depends only on open-source Swift packages (the OCCTSwift
family). **Never propose a verb, dependency, or change that would make this repo depend on a
closed-source project.** Downstream closed-source consumers (e.g. the OCCTStudio app) wire
their own proprietary pieces — constraint-solving (the former `solve-sketch`) was removed when
the swiftGCS dep was dropped for exactly this reason.

# Direction

Dependencies flow OSS-internal only. Downstream commercial consumers depend on *this* repo;
this repo never depends on *them* (see
[references/commercial-app-relationship](/docs/knowledge/references/commercial-app-relationship.md)).
28 changes: 28 additions & 0 deletions docs/knowledge/references/commercial-app-relationship.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
type: Reference
title: Relationship to the OCCTStudio commercial app
description: OCCTStudio (private, commercial) is built on this OSS repo and consumes its reconstruct feature-graph IR; this repo stays OSS and must not depend on the app.
resource: https://github.com/gsdali/OCCTStudio
tags: [reference, commercial, occtstudio, boundary]
timestamp: 2026-06-18T00:00:00Z
---

# Relationship

**OCCTStudio** (`gsdali/OCCTStudio`, private/commercial) is a freemium parametric-CAD app — a
B-Rep alternative to OpenSCAD/CadQuery/ManifoldCAD — built on the OCCTSwift stack. It consumes
this repo:

- OCCTStudio's portable DSL compiles to an IR that its **native adapter** translates to the
JSON consumed by this repo's `reconstruct` verb (`FeatureReconstructor.buildJSON`). Changes
to `reconstruct`'s schema affect the app's native adapter.
- The cookbook recipes (`recipes/01`–`07`) are the app's v0 acceptance set for the DSL/IR.

# Direction

Dependencies point **app → OSS, never the reverse.** This repo must never depend on OCCTStudio
(see [policies/open-source-boundary](/docs/knowledge/policies/open-source-boundary.md)). If a
capability seems shared, it lands here under LGPL and the app consumes it.

The app's own design (DSL grammar, IR schema, AI architecture, freemium/feature-flags) lives in
the OCCTStudio repo's `docs/DSL_DESIGN.md` and `docs/knowledge/` bundle, not here.
4 changes: 4 additions & 0 deletions docs/knowledge/references/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# References

* [What is OKF](okf.md) - the markdown+frontmatter knowledge format this bundle uses.
* [Commercial-app relationship](commercial-app-relationship.md) - how OCCTStudio (private, commercial) consumes this OSS repo.
Loading
Loading