Files
cadSet/designir-pipeline/scripts/surfaceir_pipeline.py
2026-07-28 14:41:26 +08:00

4517 lines
166 KiB
Python

#!/usr/bin/env python3
"""Extract an independent, editable DesignIR 3.0 surface/topology program."""
from __future__ import annotations
import argparse
from collections import Counter
from concurrent.futures import ThreadPoolExecutor, as_completed
import copy
import hashlib
import json
import math
from pathlib import Path
import re
import subprocess
import sys
import tempfile
from typing import Any
from build123d import Shape, import_step
from OCP.BRep import BRep_Builder, BRep_Tool
from OCP.BRepBndLib import BRepBndLib
from OCP.BRepCheck import BRepCheck_Analyzer
from OCP.BRepClass import BRepClass_FaceClassifier
from OCP.BRepExtrema import BRepExtrema_DistShapeShape
from OCP.BRepAdaptor import BRepAdaptor_Curve, BRepAdaptor_Surface
from OCP.BRepAlgoAPI import BRepAlgoAPI_Cut
from OCP.BRepLib import BRepLib
from OCP.BRepGProp import BRepGProp
from OCP.BRepBuilderAPI import (
BRepBuilderAPI_GTransform,
BRepBuilderAPI_MakeEdge,
BRepBuilderAPI_MakeFace,
BRepBuilderAPI_MakeSolid,
BRepBuilderAPI_MakeVertex,
BRepBuilderAPI_MakeWire,
BRepBuilderAPI_Sewing,
BRepBuilderAPI_Transform,
)
from OCP.BRepTools import BRepTools, BRepTools_WireExplorer
from OCP.BRepPrimAPI import BRepPrimAPI_MakeCylinder
from OCP.Bnd import Bnd_Box
from OCP.Geom import (
Geom_BSplineCurve,
Geom_BSplineSurface,
Geom_Circle,
Geom_ConicalSurface,
Geom_CylindricalSurface,
Geom_Ellipse,
Geom_Line,
Geom_Plane,
Geom_SphericalSurface,
Geom_ToroidalSurface,
)
from OCP.Geom2d import (
Geom2d_BSplineCurve,
Geom2d_Circle,
Geom2d_Ellipse,
Geom2d_Line,
)
from OCP.Geom2dAdaptor import Geom2dAdaptor_Curve
from OCP.GeomAbs import (
GeomAbs_BSplineCurve,
GeomAbs_BSplineSurface,
GeomAbs_Circle,
GeomAbs_Cone,
GeomAbs_Cylinder,
GeomAbs_Ellipse,
GeomAbs_Line,
GeomAbs_Plane,
GeomAbs_Sphere,
GeomAbs_Torus,
)
from OCP.TColgp import TColgp_Array1OfPnt, TColgp_Array1OfPnt2d, TColgp_Array2OfPnt
from OCP.TColStd import (
TColStd_Array1OfInteger,
TColStd_Array1OfReal,
TColStd_Array2OfReal,
)
from OCP.IFSelect import IFSelect_RetDone
from OCP.GProp import GProp_GProps
from OCP.STEPControl import STEPControl_AsIs, STEPControl_Reader, STEPControl_Writer
from OCP.ShapeFix import ShapeFix_Shape
from OCP.TopAbs import (
TopAbs_EDGE,
TopAbs_FACE,
TopAbs_IN,
TopAbs_ON,
TopAbs_SHELL,
TopAbs_SOLID,
TopAbs_VERTEX,
TopAbs_WIRE,
)
from OCP.TopExp import TopExp_Explorer
from OCP.TopExp import TopExp
from OCP.TopLoc import TopLoc_Location
from OCP.TopTools import TopTools_IndexedMapOfShape
from OCP.TopoDS import TopoDS, TopoDS_Compound, TopoDS_Shell, TopoDS_Vertex
from OCP.gp import (
gp_Ax2,
gp_Ax22d,
gp_Ax3,
gp_Circ,
gp_Circ2d,
gp_Dir,
gp_Dir2d,
gp_Elips,
gp_Elips2d,
gp_GTrsf,
gp_Lin,
gp_Lin2d,
gp_Mat,
gp_Pnt,
gp_Pnt2d,
gp_Trsf,
gp_XYZ,
)
def _number(value: float) -> float:
result = float(value)
if not math.isfinite(result):
raise ValueError("DesignIR cannot contain non-finite geometry")
return result
def _point(value: Any) -> list[float]:
return [_number(value.X()), _number(value.Y()), _number(value.Z())]
def _direction(value: Any) -> list[float]:
return _point(value)
def _axis3(position: Any) -> dict[str, Any]:
direct = (
bool(position.Direct())
if hasattr(position, "Direct")
else bool(
position.XDirection()
.Crossed(position.YDirection())
.Dot(position.Direction())
> 0
)
)
return {
"location": _point(position.Location()),
"axis": _direction(position.Direction()),
"x_direction": _direction(position.XDirection()),
"y_direction": _direction(position.YDirection()),
"direct": direct,
}
def _bspline_curve(curve: Any) -> dict[str, Any]:
return {
"type": "bspline",
"degree": int(curve.Degree()),
"periodic": bool(curve.IsPeriodic()),
"rational": bool(curve.IsRational()),
"poles": [_point(curve.Pole(index)) for index in range(1, curve.NbPoles() + 1)],
"weights": [
_number(curve.Weight(index)) for index in range(1, curve.NbPoles() + 1)
],
"knots": [
_number(curve.Knot(index)) for index in range(1, curve.NbKnots() + 1)
],
"multiplicities": [
int(curve.Multiplicity(index))
for index in range(1, curve.NbKnots() + 1)
],
}
def _curve_record(edge: Any) -> dict[str, Any]:
if bool(BRep_Tool.Degenerated_s(edge)):
return {"type": "degenerate"}
adaptor = BRepAdaptor_Curve(edge)
first, last = map(_number, BRep_Tool.Range_s(edge))
kind = adaptor.GetType()
if kind == GeomAbs_Line:
line = adaptor.Line()
payload = {
"type": "line",
"location": _point(line.Location()),
"direction": _direction(line.Direction()),
}
elif kind == GeomAbs_Circle:
circle = adaptor.Circle()
payload = {
"type": "circle",
"position": _axis3(circle.Position()),
"radius": _number(circle.Radius()),
}
elif kind == GeomAbs_Ellipse:
ellipse = adaptor.Ellipse()
payload = {
"type": "ellipse",
"position": _axis3(ellipse.Position()),
"major_radius": _number(ellipse.MajorRadius()),
"minor_radius": _number(ellipse.MinorRadius()),
}
elif kind == GeomAbs_BSplineCurve:
payload = _bspline_curve(adaptor.BSpline())
else:
raise ValueError(f"Unsupported curve type: {int(kind)}")
payload["parameter_range"] = [first, last]
return payload
def _point2d(value: Any) -> list[float]:
return [_number(value.X()), _number(value.Y())]
def _pcurve_record(edge: Any, face: Any) -> dict[str, Any] | None:
curve = BRep_Tool.CurveOnSurface_s(edge, face, 0.0, 0.0)
if curve is None:
return None
adaptor = Geom2dAdaptor_Curve(curve)
first, last = map(_number, BRep_Tool.Range_s(edge, face))
kind = adaptor.GetType()
if kind == GeomAbs_Line:
line = adaptor.Line()
payload = {
"type": "line",
"location": _point2d(line.Location()),
"direction": _point2d(line.Direction()),
}
elif kind == GeomAbs_Circle:
circle = adaptor.Circle()
position = circle.Position()
payload = {
"type": "circle",
"location": _point2d(position.Location()),
"x_direction": _point2d(position.XDirection()),
"y_direction": _point2d(position.YDirection()),
"radius": _number(circle.Radius()),
}
elif kind == GeomAbs_Ellipse:
ellipse = adaptor.Ellipse()
position = ellipse.Axis()
payload = {
"type": "ellipse",
"location": _point2d(position.Location()),
"x_direction": _point2d(position.XDirection()),
"y_direction": _point2d(position.YDirection()),
"major_radius": _number(ellipse.MajorRadius()),
"minor_radius": _number(ellipse.MinorRadius()),
}
elif kind == GeomAbs_BSplineCurve:
spline = adaptor.BSpline()
payload = {
"type": "bspline",
"degree": int(spline.Degree()),
"periodic": bool(spline.IsPeriodic()),
"rational": bool(spline.IsRational()),
"poles": [
_point2d(spline.Pole(index))
for index in range(1, spline.NbPoles() + 1)
],
"weights": [
_number(spline.Weight(index))
for index in range(1, spline.NbPoles() + 1)
],
"knots": [
_number(spline.Knot(index))
for index in range(1, spline.NbKnots() + 1)
],
"multiplicities": [
int(spline.Multiplicity(index))
for index in range(1, spline.NbKnots() + 1)
],
}
else:
raise ValueError(f"Unsupported pcurve type: {int(kind)}")
payload["parameter_range"] = [first, last]
return payload
def _bspline_surface(surface: Any) -> dict[str, Any]:
return {
"type": "bspline",
"u_degree": int(surface.UDegree()),
"v_degree": int(surface.VDegree()),
"u_periodic": bool(surface.IsUPeriodic()),
"v_periodic": bool(surface.IsVPeriodic()),
"u_rational": bool(surface.IsURational()),
"v_rational": bool(surface.IsVRational()),
"poles": [
[
_point(surface.Pole(u_index, v_index))
for v_index in range(1, surface.NbVPoles() + 1)
]
for u_index in range(1, surface.NbUPoles() + 1)
],
"weights": [
[
_number(surface.Weight(u_index, v_index))
for v_index in range(1, surface.NbVPoles() + 1)
]
for u_index in range(1, surface.NbUPoles() + 1)
],
"u_knots": [
_number(surface.UKnot(index))
for index in range(1, surface.NbUKnots() + 1)
],
"v_knots": [
_number(surface.VKnot(index))
for index in range(1, surface.NbVKnots() + 1)
],
"u_multiplicities": [
int(surface.UMultiplicity(index))
for index in range(1, surface.NbUKnots() + 1)
],
"v_multiplicities": [
int(surface.VMultiplicity(index))
for index in range(1, surface.NbVKnots() + 1)
],
}
def _surface_record(face: Any) -> dict[str, Any]:
adaptor = BRepAdaptor_Surface(face)
kind = adaptor.GetType()
if kind == GeomAbs_Plane:
plane = adaptor.Plane()
return {"type": "plane", "position": _axis3(plane.Position())}
if kind == GeomAbs_Cylinder:
cylinder = adaptor.Cylinder()
return {
"type": "cylinder",
"position": _axis3(cylinder.Position()),
"radius": _number(cylinder.Radius()),
}
if kind == GeomAbs_Cone:
cone = adaptor.Cone()
return {
"type": "cone",
"position": _axis3(cone.Position()),
"reference_radius": _number(cone.RefRadius()),
"semi_angle_radians": _number(cone.SemiAngle()),
}
if kind == GeomAbs_Sphere:
sphere = adaptor.Sphere()
return {
"type": "sphere",
"position": _axis3(sphere.Position()),
"radius": _number(sphere.Radius()),
}
if kind == GeomAbs_Torus:
torus = adaptor.Torus()
return {
"type": "torus",
"position": _axis3(torus.Position()),
"major_radius": _number(torus.MajorRadius()),
"minor_radius": _number(torus.MinorRadius()),
}
if kind == GeomAbs_BSplineSurface:
return _bspline_surface(adaptor.BSpline())
raise ValueError(f"Unsupported surface type: {int(kind)}")
def _edge_records(
wire: Any,
face: Any,
prefix: str,
edge_map: TopTools_IndexedMapOfShape,
vertex_map: TopTools_IndexedMapOfShape,
) -> list[dict[str, Any]]:
records = []
explorer = BRepTools_WireExplorer(wire)
index = 0
while explorer.More():
index += 1
edge = explorer.Current()
topology_index = int(edge_map.FindIndex(edge))
if topology_index <= 0:
raise ValueError("Cannot resolve shared topology edge")
first_vertex = TopoDS_Vertex()
last_vertex = TopoDS_Vertex()
TopExp.Vertices_s(edge, first_vertex, last_vertex)
vertex_ids = []
for vertex in (first_vertex, last_vertex):
if vertex.IsNull():
continue
vertex_index = int(vertex_map.FindIndex(vertex))
if vertex_index <= 0:
raise ValueError("Cannot resolve shared topology vertex")
vertex_ids.append(f"vertex_{vertex_index}")
records.append(
{
"id": f"{prefix}.edge_{index}",
"topology_edge_id": f"edge_{topology_index}",
"vertex_ids": vertex_ids,
"orientation": int(explorer.Orientation()),
"tolerance": _number(BRep_Tool.Tolerance_s(edge)),
"curve": _curve_record(edge),
"pcurve": _pcurve_record(edge, face),
}
)
explorer.Next()
return records
def _wire_records(
face: Any,
prefix: str,
edge_map: TopTools_IndexedMapOfShape,
vertex_map: TopTools_IndexedMapOfShape,
) -> list[dict[str, Any]]:
records = []
outer = BRepTools.OuterWire_s(face)
explorer = TopExp_Explorer(face, TopAbs_WIRE)
index = 0
while explorer.More():
index += 1
wire = TopoDS.Wire_s(explorer.Current())
wire_id = f"{prefix}.wire_{index}"
records.append(
{
"id": wire_id,
"orientation": int(wire.Orientation()),
"outer": bool(wire.IsSame(outer)),
"edges": _edge_records(
wire, face, wire_id, edge_map, vertex_map
),
}
)
explorer.Next()
return records
def _face_records(
shell: Any,
prefix: str,
edge_map: TopTools_IndexedMapOfShape,
vertex_map: TopTools_IndexedMapOfShape,
) -> list[dict[str, Any]]:
records = []
explorer = TopExp_Explorer(shell, TopAbs_FACE)
index = 0
while explorer.More():
index += 1
face = TopoDS.Face_s(explorer.Current())
face_id = f"{prefix}.face_{index}"
records.append(
{
"id": face_id,
"orientation": int(face.Orientation()),
"surface": _surface_record(face),
"uv_bounds": [_number(value) for value in BRepTools.UVBounds_s(face)],
"wires": _wire_records(face, face_id, edge_map, vertex_map),
}
)
explorer.Next()
return records
def _vertex_records(vertex_map: TopTools_IndexedMapOfShape) -> list[dict[str, Any]]:
records = []
for index in range(1, vertex_map.Extent() + 1):
vertex = TopoDS.Vertex_s(vertex_map.FindKey(index))
records.append(
{
"id": f"vertex_{index}",
"point": _point(BRep_Tool.Pnt_s(vertex)),
"tolerance": _number(BRep_Tool.Tolerance_s(vertex)),
}
)
return records
def _topology_records(shape: Any) -> dict[str, Any]:
edge_map = TopTools_IndexedMapOfShape()
TopExp.MapShapes_s(shape, TopAbs_EDGE, edge_map)
vertex_map = TopTools_IndexedMapOfShape()
TopExp.MapShapes_s(shape, TopAbs_VERTEX, vertex_map)
solids = []
solid_shell_shapes = []
solid_explorer = TopExp_Explorer(shape, TopAbs_SOLID)
solid_index = 0
while solid_explorer.More():
solid_index += 1
solid = TopoDS.Solid_s(solid_explorer.Current())
shells = []
shell_explorer = TopExp_Explorer(solid, TopAbs_SHELL)
shell_index = 0
while shell_explorer.More():
shell_index += 1
shell = TopoDS.Shell_s(shell_explorer.Current())
solid_shell_shapes.append(shell)
shell_id = f"solid_{solid_index}.shell_{shell_index}"
shells.append(
{
"id": shell_id,
"orientation": int(shell.Orientation()),
"faces": _face_records(
shell, shell_id, edge_map, vertex_map
),
}
)
shell_explorer.Next()
solids.append({"id": f"solid_{solid_index}", "shells": shells})
solid_explorer.Next()
free_shells = []
shell_explorer = TopExp_Explorer(shape, TopAbs_SHELL)
free_shell_index = 0
while shell_explorer.More():
shell = TopoDS.Shell_s(shell_explorer.Current())
if not any(shell.IsSame(candidate) for candidate in solid_shell_shapes):
free_shell_index += 1
shell_id = f"free_shell_{free_shell_index}"
free_shells.append(
{
"id": shell_id,
"orientation": int(shell.Orientation()),
"faces": _face_records(
shell, shell_id, edge_map, vertex_map
),
}
)
shell_explorer.Next()
return {
"vertices": _vertex_records(vertex_map),
"solids": solids,
"free_shells": free_shells,
}
def _normalized_axis(position: dict[str, Any]) -> tuple[list[float], list[float]]:
axis = [float(value) for value in position["axis"]]
magnitude = math.sqrt(sum(value * value for value in axis))
axis = [value / magnitude for value in axis]
for value in axis:
if abs(value) > 1e-9:
if value < 0:
axis = [-item for item in axis]
break
location = [float(value) for value in position["location"]]
distance = sum(location[index] * axis[index] for index in range(3))
anchor = [
location[index] - distance * axis[index] for index in range(3)
]
return axis, anchor
def _parallel(first: list[float], second: list[float], tolerance: float = 1e-6) -> bool:
return abs(abs(sum(a * b for a, b in zip(first, second))) - 1.0) <= tolerance
def _distance(first: list[float], second: list[float]) -> float:
return math.sqrt(sum((a - b) ** 2 for a, b in zip(first, second)))
def _dot(first: list[float], second: list[float]) -> float:
return sum(a * b for a, b in zip(first, second))
def _cross(first: list[float], second: list[float]) -> list[float]:
return [
first[1] * second[2] - first[2] * second[1],
first[2] * second[0] - first[0] * second[2],
first[0] * second[1] - first[1] * second[0],
]
def _surfaceir_faces(topology: dict[str, Any]) -> list[dict[str, Any]]:
solid_faces = [
face
for solid in topology["solids"]
for shell in solid["shells"]
for face in shell["faces"]
]
free_faces = [
face
for shell in topology.get("free_shells", [])
for face in shell["faces"]
]
return solid_faces + free_faces
def _observed_internal_cylinders(topology: dict[str, Any]) -> list[dict[str, Any]]:
observations = []
for face in _surfaceir_faces(topology):
surface = face["surface"]
if surface["type"] != "cylinder" or int(face["orientation"]) != 1:
continue
axis, anchor = _normalized_axis(surface["position"])
observations.append(
{
"face_id": face["id"],
"axis": axis,
"anchor": anchor,
"radius": float(surface["radius"]),
"axial_span": abs(
float(face["uv_bounds"][3]) - float(face["uv_bounds"][2])
),
}
)
return observations
def _cylinder_semantics(topology: dict[str, Any]) -> dict[str, Any]:
"""Infer conservative paper-style names without claiming source history."""
cylinders = []
for face in _surfaceir_faces(topology):
surface = face["surface"]
if surface["type"] != "cylinder":
continue
axis, anchor = _normalized_axis(surface["position"])
cylinders.append(
{
"face_id": face["id"],
"internal": int(face["orientation"]) == 1,
"axis": axis,
"anchor": anchor,
"radius": float(surface["radius"]),
"diameter": 2.0 * float(surface["radius"]),
"axial_span": abs(
float(face["uv_bounds"][3]) - float(face["uv_bounds"][2])
),
"boundary_edge_ids": sorted(
{
edge["topology_edge_id"]
for wire in face["wires"]
for edge in wire["edges"]
}
),
}
)
parameters: dict[str, Any] = {}
datums: dict[str, Any] = {}
features: list[dict[str, Any]] = []
constraints: list[dict[str, Any]] = []
internal = [item for item in cylinders if item["internal"]]
external = [item for item in cylinders if not item["internal"]]
# Coaxial cylinder segments represent one canonical hole axis. This avoids
# treating counterbores and stepped bores as unrelated holes.
hole_axes: list[dict[str, Any]] = []
for item in sorted(internal, key=lambda value: value["face_id"]):
match = next(
(
candidate
for candidate in hole_axes
if _parallel(candidate["axis"], item["axis"])
and _distance(candidate["anchor"], item["anchor"]) <= 1e-5
),
None,
)
if match is None:
match = {
"axis": item["axis"],
"anchor": item["anchor"],
"segments": [],
}
hole_axes.append(match)
match["segments"].append(item)
hole_axes.sort(
key=lambda hole: (
tuple(round(value, 9) for value in hole["axis"]),
tuple(round(value, 9) for value in hole["anchor"]),
)
)
for index, hole in enumerate(hole_axes, 1):
hole["id"] = f"hole_axis_{index}"
for hole in hole_axes:
datums[hole["id"]] = {
"kind": "axis",
"origin": [_number(value) for value in hole["anchor"]],
"direction": [_number(value) for value in hole["axis"]],
"inference": "canonical_from_final_brep",
}
# Equal-diameter parallel axes are candidates for a repeated hole feature.
unused = set(range(len(hole_axes)))
pattern_index = 0
while unused:
seed_index = min(unused)
seed = hole_axes[seed_index]
seed_diameter = min(segment["diameter"] for segment in seed["segments"])
group = [
index
for index in sorted(unused)
if _parallel(seed["axis"], hole_axes[index]["axis"])
and abs(
min(
segment["diameter"]
for segment in hole_axes[index]["segments"]
)
- seed_diameter
)
<= max(1e-5, seed_diameter * 1e-6)
]
for index in group:
unused.discard(index)
if len(group) < 2:
continue
points = [hole_axes[index]["anchor"] for index in group]
centroid = [
sum(point[dimension] for point in points) / len(points)
for dimension in range(3)
]
radial_distances = [_distance(point, centroid) for point in points]
radius_mean = sum(radial_distances) / len(radial_distances)
radial_equal = (
len(group) >= 3
and radius_mean > 1e-7
and max(abs(value - radius_mean) for value in radial_distances)
<= max(1e-5, radius_mean * 1e-5)
)
angular_equal = False
if radial_equal:
reference = [
(points[0][index] - centroid[index]) / radius_mean
for index in range(3)
]
transverse = _cross(seed["axis"], reference)
transverse_length = math.sqrt(_dot(transverse, transverse))
if transverse_length > 1e-7:
transverse = [
value / transverse_length for value in transverse
]
angles = sorted(
math.atan2(
_dot(
[
point[index] - centroid[index]
for index in range(3)
],
transverse,
),
_dot(
[
point[index] - centroid[index]
for index in range(3)
],
reference,
),
)
% (2.0 * math.pi)
for point in points
)
angle_gaps = [
(
angles[(index + 1) % len(angles)]
- angles[index]
)
% (2.0 * math.pi)
for index in range(len(angles))
]
expected_gap = 2.0 * math.pi / len(angles)
angular_equal = max(
abs(gap - expected_gap) for gap in angle_gaps
) <= 1e-5
polar = radial_equal and angular_equal
# A covariance-free collinearity test is sufficient for exact CAD axes.
direction = [
points[-1][dimension] - points[0][dimension]
for dimension in range(3)
]
direction_length = math.sqrt(sum(value * value for value in direction))
linear = direction_length > 1e-7
if linear:
direction = [value / direction_length for value in direction]
linear = all(
math.sqrt(
sum(
(
(point[dimension] - points[0][dimension])
- sum(
(point[index] - points[0][index])
* direction[index]
for index in range(3)
)
* direction[dimension]
)
** 2
for dimension in range(3)
)
)
<= 1e-5
for point in points
)
pattern_index += 1
feature_id = f"hole_pattern_{pattern_index}"
diameter_name = (
"hole_diameter" if pattern_index == 1 else f"hole_diameter_{pattern_index}"
)
count_name = (
"hole_count" if pattern_index == 1 else f"hole_count_{pattern_index}"
)
parameters[diameter_name] = {
"value": _number(seed_diameter),
"unit": "mm",
"editable": False,
"semantic_role": "hole_diameter",
"inference": "cylindrical_cut_surface",
"confidence": 0.98,
"affected_face_ids": [
segment["face_id"]
for index in group
for segment in hole_axes[index]["segments"]
if abs(segment["diameter"] - seed_diameter)
<= max(1e-5, seed_diameter * 1e-6)
],
"edit_state": "executable_enlarge_cut_binding",
}
parameters[count_name] = {
"value": len(group),
"unit": "count",
"editable": False,
"semantic_role": "hole_count",
"inference": "equal_diameter_parallel_axes",
"confidence": 0.95,
"edit_state": "awaiting_executable_binding",
}
pattern_kind = "polar" if polar else "linear" if linear else "set"
feature = {
"id": feature_id,
"operation": f"{pattern_kind}_hole_pattern",
"member_axis_ids": [hole_axes[index]["id"] for index in group],
"diameter_parameter": diameter_name,
"count_parameter": count_name,
"inference": "canonical_from_final_brep",
"original_history_recovered": False,
"confidence": 0.95 if pattern_kind != "set" else 0.75,
}
if polar:
parameter_name = (
"bolt_circle_diameter"
if pattern_index == 1
else f"bolt_circle_diameter_{pattern_index}"
)
parameters[parameter_name] = {
"value": _number(2.0 * radius_mean),
"unit": "mm",
"editable": False,
"semantic_role": "bolt_circle_diameter",
"inference": "equal_radius_axis_distribution",
"confidence": 0.98,
"edit_state": "awaiting_executable_binding",
}
feature["pitch_diameter_parameter"] = parameter_name
constraints.append(
{
"id": f"{feature_id}_equal_angular_spacing",
"kind": "equal_angular_spacing",
"participants": [feature_id],
"confidence": 0.9,
}
)
elif linear:
projections = sorted(
sum(
(point[index] - points[0][index]) * direction[index]
for index in range(3)
)
for point in points
)
gaps = [
projections[index + 1] - projections[index]
for index in range(len(projections) - 1)
]
if gaps and max(gaps) - min(gaps) <= max(1e-5, max(gaps) * 1e-5):
parameter_name = (
"hole_spacing"
if pattern_index == 1
else f"hole_spacing_{pattern_index}"
)
parameters[parameter_name] = {
"value": _number(sum(gaps) / len(gaps)),
"unit": "mm",
"editable": False,
"semantic_role": "hole_spacing",
"inference": "equal_linear_axis_spacing",
"confidence": 0.98,
"edit_state": "awaiting_executable_binding",
}
feature["spacing_parameter"] = parameter_name
constraints.append(
{
"id": f"{feature_id}_equal_spacing",
"kind": "equal_linear_spacing",
"participants": [feature_id],
"confidence": 0.98,
}
)
features.append(feature)
patterned_axes = {
axis_id
for feature in features
for axis_id in feature.get("member_axis_ids", [])
}
vertex_points = [
[float(value) for value in vertex["point"]]
for vertex in topology.get("vertices", [])
]
bounds_center = (
[
(
min(point[index] for point in vertex_points)
+ max(point[index] for point in vertex_points)
)
/ 2.0
for index in range(3)
]
if vertex_points
else [0.0, 0.0, 0.0]
)
bounds_diagonal = (
_distance(
[min(point[index] for point in vertex_points) for index in range(3)],
[max(point[index] for point in vertex_points) for index in range(3)],
)
if vertex_points
else 0.0
)
primary_external = (
max(external, key=lambda item: item["radius"]) if external else None
)
unpatterned_index = 0
for hole in hole_axes:
if hole["id"] in patterned_axes:
continue
unpatterned_index += 1
diameter = min(segment["diameter"] for segment in hole["segments"])
center_delta = [
bounds_center[index] - hole["anchor"][index]
for index in range(3)
]
axial_component = _dot(center_delta, hole["axis"])
axis_distance_from_center = math.sqrt(
sum(
(
center_delta[index]
- axial_component * hole["axis"][index]
)
** 2
for index in range(3)
)
)
central = (
primary_external is not None
and _parallel(hole["axis"], primary_external["axis"])
and _distance(hole["anchor"], primary_external["anchor"])
<= max(1e-5, primary_external["diameter"] * 1e-6)
) or axis_distance_from_center <= max(
1e-5,
bounds_diagonal * 1e-6,
)
parameter_name = (
"bore_diameter"
if central
else f"hole_diameter_single_{unpatterned_index}"
)
parameters[parameter_name] = {
"value": _number(diameter),
"unit": "mm",
"editable": False,
"semantic_role": (
"bore_diameter" if central else "hole_diameter"
),
"inference": "cylindrical_cut_surface",
"confidence": 0.9 if len(hole_axes) == 1 else 0.8,
"affected_face_ids": [
segment["face_id"]
for segment in hole["segments"]
if abs(segment["diameter"] - diameter)
<= max(1e-5, diameter * 1e-6)
],
"edit_state": "executable_enlarge_cut_binding",
}
features.append(
{
"id": f"canonical_hole_{unpatterned_index}",
"operation": (
"central_bore" if central else "cylindrical_cut"
),
"axis_id": hole["id"],
"diameter_parameter": parameter_name,
"segment_face_ids": [
segment["face_id"] for segment in hole["segments"]
],
"inference": "canonical_from_final_brep",
"original_history_recovered": False,
"confidence": 0.9 if len(hole_axes) == 1 else 0.8,
}
)
if external:
has_feature_specific_cut_binding = any(
parameter.get("edit_state")
== "executable_enlarge_cut_binding"
for parameter in parameters.values()
)
primary_axis_edit_state = (
"awaiting_executable_binding"
if has_feature_specific_cut_binding
else "executable_axis_affine_binding"
)
largest = max(external, key=lambda item: item["radius"])
parameters["outer_diameter"] = {
"value": _number(largest["diameter"]),
"unit": "mm",
"editable": False,
"semantic_role": "outer_diameter",
"inference": "largest_external_cylinder",
"confidence": 0.9,
"affected_face_ids": [largest["face_id"]],
"edit_state": primary_axis_edit_state,
}
feature = {
"id": "primary_cylindrical_body",
"operation": "cylindrical_body",
"face_id": largest["face_id"],
"diameter_parameter": "outer_diameter",
"inference": "canonical_from_final_brep",
"original_history_recovered": False,
"confidence": 0.9,
}
if largest["axial_span"] > 1e-7:
semantic_role = (
"plate_thickness"
if largest["axial_span"] < largest["diameter"] * 0.35
else "body_length"
)
parameters[semantic_role] = {
"value": _number(largest["axial_span"]),
"unit": "mm",
"editable": False,
"semantic_role": semantic_role,
"inference": "external_cylinder_axial_span",
"confidence": 0.85,
"affected_face_ids": [largest["face_id"]],
"edit_state": primary_axis_edit_state,
}
feature["length_parameter"] = semantic_role
features.append(feature)
return {
"parameters": parameters,
"datums": datums,
"features": features,
"constraints": constraints,
"inference_policy": {
"kind": "canonical_design_interpretation",
"claims_original_feature_history": False,
"editable_only_after_perturbation_validation": True,
},
}
def apply_semantic_parameter(
payload: dict[str, Any], parameter_name: str, value: float
) -> dict[str, Any]:
"""Apply one explicitly bound semantic edit to an independent IR copy."""
result = copy.deepcopy(payload)
parameters = result.get("semantic_layer", {}).get("parameters", {})
if parameter_name not in parameters:
raise ValueError(f"Unknown semantic parameter: {parameter_name}")
parameter = parameters[parameter_name]
if parameter.get("edit_state") not in {
"executable_enlarge_cut_binding",
"executable_axis_affine_binding",
"executable_uniform_scale_binding",
"validated_executable_binding",
}:
raise ValueError(
f"Semantic parameter {parameter_name} has no executable binding"
)
role = parameter.get("semantic_role")
if role not in {
"hole_diameter",
"bore_diameter",
"outer_diameter",
"body_length",
"plate_thickness",
"overall_size_x",
"overall_size_y",
"overall_size_z",
"overall_scale",
}:
raise ValueError(f"Unsupported semantic edit role: {role}")
new_value = float(value)
if not math.isfinite(new_value) or new_value <= 0:
raise ValueError("Semantic edits require a finite positive value")
old_value = float(parameter["value"])
if role == "overall_scale":
if math.isclose(new_value, old_value, rel_tol=0.0, abs_tol=1e-12):
raise ValueError("Overall scale edit must change the value")
pivot_name = parameter.get("pivot_datum")
pivot = (
result.get("semantic_layer", {})
.get("datums", {})
.get(pivot_name, {})
.get("point")
)
if not isinstance(pivot, list) or len(pivot) != 3:
raise ValueError("Overall scale edit has no valid pivot datum")
scale_factor = new_value / old_value
parameter["value"] = _number(new_value)
parameter["editable"] = True
parameter["edit_state"] = "edited_pending_acceptance"
result["edit_operations"] = [
{
"operation": "uniform_scale",
"factor": _number(scale_factor),
"pivot": [_number(float(component)) for component in pivot],
}
]
preferred_boundary_strategy = result.get(
"reconstruction_strategy", {}
).get("boundary_strategy", "analytic_uv_rect")
result["reconstruction_strategy"] = {
"boundary_strategy": preferred_boundary_strategy,
"selection_status": "semantic_edit_candidate",
}
result["active_edit"] = {
"parameter": parameter_name,
"semantic_role": role,
"old_value": _number(old_value),
"new_value": _number(new_value),
"operation_count": 1,
"range_status": (
"within_validated_range"
if parameter.get("validated_range")
and float(
parameter["validated_range"]["minimum_tested_inclusive"]
)
<= new_value
<= float(
parameter["validated_range"]["maximum_tested_inclusive"]
)
else "requires_revalidation"
),
"acceptance_status": "pending",
}
return result
if role in {"overall_size_x", "overall_size_y", "overall_size_z"}:
if math.isclose(new_value, old_value, rel_tol=0.0, abs_tol=1e-12):
raise ValueError(f"{role} edit must change the value")
axis_index = {"overall_size_x": 0, "overall_size_y": 1, "overall_size_z": 2}[
role
]
axis = [0.0, 0.0, 0.0]
axis[axis_index] = 1.0
pivot_name = parameter.get("pivot_datum")
pivot = (
result.get("semantic_layer", {})
.get("datums", {})
.get(pivot_name, {})
.get("point")
)
if not isinstance(pivot, list) or len(pivot) != 3:
raise ValueError(f"{role} has no valid pivot datum")
factor = new_value / old_value
parameter["value"] = _number(new_value)
parameter["editable"] = True
parameter["edit_state"] = "edited_pending_acceptance"
result["edit_operations"] = [
{
"operation": "axis_affine_scale",
"axis": axis,
"pivot": [_number(float(component)) for component in pivot],
"axial_factor": _number(factor),
"radial_factor": 1.0,
"semantic_role": role,
"target_measure": "bounds_extent",
}
]
preferred_boundary_strategy = result.get(
"reconstruction_strategy", {}
).get("boundary_strategy", "analytic_uv_rect")
result["reconstruction_strategy"] = {
"boundary_strategy": preferred_boundary_strategy,
"selection_status": "semantic_edit_candidate",
}
result["active_edit"] = {
"parameter": parameter_name,
"semantic_role": role,
"old_value": _number(old_value),
"new_value": _number(new_value),
"operation_count": 1,
"range_status": (
"within_validated_range"
if parameter.get("validated_range")
and float(
parameter["validated_range"]["minimum_tested_inclusive"]
)
<= new_value
<= float(
parameter["validated_range"]["maximum_tested_inclusive"]
)
else "requires_revalidation"
),
"acceptance_status": "pending",
}
return result
if role in {"outer_diameter", "body_length", "plate_thickness"}:
if math.isclose(new_value, old_value, rel_tol=0.0, abs_tol=1e-12):
raise ValueError(f"{role} edit must change the value")
affected_face_ids = set(parameter.get("affected_face_ids", []))
faces = _surfaceir_faces(result["surface_layer"])
target_face = next(
(
face
for face in faces
if face["id"] in affected_face_ids
and face.get("surface", {}).get("type") == "cylinder"
),
None,
)
if target_face is None:
raise ValueError(f"{role} binding matched no primary cylinder")
position = target_face["surface"]["position"]
raw_axis = [float(component) for component in position["axis"]]
axis_length = math.sqrt(sum(component**2 for component in raw_axis))
axis = [component / axis_length for component in raw_axis]
location = [
float(component) for component in position["location"]
]
v_min, v_max = sorted(
[
float(target_face["uv_bounds"][2]),
float(target_face["uv_bounds"][3]),
]
)
midpoint = (v_min + v_max) / 2.0
pivot = [
_number(location[index] + midpoint * axis[index])
for index in range(3)
]
factor = new_value / old_value
axial_factor = (
factor
if role in {"body_length", "plate_thickness"}
else 1.0
)
radial_factor = factor if role == "outer_diameter" else 1.0
parameter["value"] = _number(new_value)
parameter["editable"] = True
parameter["edit_state"] = "edited_pending_acceptance"
result["edit_operations"] = [
{
"operation": "axis_affine_scale",
"axis": [_number(component) for component in axis],
"pivot": pivot,
"axial_factor": _number(axial_factor),
"radial_factor": _number(radial_factor),
"target_face_id": target_face["id"],
"semantic_role": role,
}
]
preferred_boundary_strategy = result.get(
"reconstruction_strategy", {}
).get("boundary_strategy", "analytic_uv_rect")
result["reconstruction_strategy"] = {
"boundary_strategy": preferred_boundary_strategy,
"selection_status": "semantic_edit_candidate",
}
result["active_edit"] = {
"parameter": parameter_name,
"semantic_role": role,
"old_value": _number(old_value),
"new_value": _number(new_value),
"edited_face_ids": [target_face["id"]],
"operation_count": 1,
"range_status": (
"within_validated_range"
if parameter.get("validated_range")
and float(
parameter["validated_range"]["minimum_tested_inclusive"]
)
<= new_value
<= float(
parameter["validated_range"]["maximum_tested_inclusive"]
)
else "requires_revalidation"
),
"acceptance_status": "pending",
}
return result
if new_value <= old_value:
raise ValueError(
f"Semantic parameter {parameter_name} currently supports "
"diameter increases only"
)
old_radius = old_value / 2.0
new_radius = new_value / 2.0
affected_face_ids = set(parameter.get("affected_face_ids", []))
if not affected_face_ids:
raise ValueError(f"Semantic parameter {parameter_name} has no target faces")
faces = _surfaceir_faces(result["surface_layer"])
edited_face_ids: list[str] = []
edit_operations: list[dict[str, Any]] = []
seen_operations: set[tuple[Any, ...]] = set()
radius_tolerance = max(1e-6, abs(old_radius) * 1e-6)
for face in faces:
if face["id"] not in affected_face_ids:
continue
surface = face["surface"]
if (
surface.get("type") != "cylinder"
or abs(float(surface["radius"]) - old_radius) > radius_tolerance
):
continue
edited_face_ids.append(face["id"])
operation_axis, operation_anchor = _normalized_axis(surface["position"])
operation_v_range = sorted(
[_number(face["uv_bounds"][2]), _number(face["uv_bounds"][3])]
)
operation_key = (
tuple(round(value, 8) for value in operation_axis),
tuple(round(value, 8) for value in operation_anchor),
round(operation_v_range[0], 8),
round(operation_v_range[1], 8),
)
if operation_key in seen_operations:
continue
seen_operations.add(operation_key)
edit_operations.append(
{
"operation": "enlarge_cylindrical_cut",
"face_id": face["id"],
"position": copy.deepcopy(surface["position"]),
"v_range": operation_v_range,
"old_radius": _number(old_radius),
"new_radius": _number(new_radius),
}
)
if not edited_face_ids:
raise ValueError(
f"Semantic parameter {parameter_name} binding matched no cylinders"
)
merged_operations: list[dict[str, Any]] = []
for operation in edit_operations:
operation_axis, operation_anchor = _normalized_axis(
operation["position"]
)
location = [
float(value) for value in operation["position"]["location"]
]
raw_axis = [
float(value) for value in operation["position"]["axis"]
]
axis_sign = sum(
first * second
for first, second in zip(raw_axis, operation_axis)
)
location_scalar = sum(
first * second
for first, second in zip(location, operation_axis)
)
absolute_interval = sorted(
location_scalar + axis_sign * float(value)
for value in operation["v_range"]
)
duplicate = None
for candidate in merged_operations:
candidate_axis = candidate["_canonical_axis"]
candidate_anchor = candidate["_canonical_anchor"]
candidate_interval = candidate["_absolute_interval"]
span = max(
absolute_interval[1] - absolute_interval[0],
candidate_interval[1] - candidate_interval[0],
1e-9,
)
interval_tolerance = max(1e-4, span * 1e-5)
if (
_parallel(operation_axis, candidate_axis)
and _distance(operation_anchor, candidate_anchor) <= 1e-5
and abs(absolute_interval[0] - candidate_interval[0])
<= interval_tolerance
and abs(absolute_interval[1] - candidate_interval[1])
<= interval_tolerance
):
duplicate = candidate
break
if duplicate is None:
candidate = copy.deepcopy(operation)
candidate["source_face_ids"] = [operation["face_id"]]
candidate["_canonical_axis"] = operation_axis
candidate["_canonical_anchor"] = operation_anchor
candidate["_absolute_interval"] = absolute_interval
merged_operations.append(candidate)
else:
duplicate["source_face_ids"].append(operation["face_id"])
duplicate["_absolute_interval"] = [
min(duplicate["_absolute_interval"][0], absolute_interval[0]),
max(duplicate["_absolute_interval"][1], absolute_interval[1]),
]
duplicate_location = [
float(value)
for value in duplicate["position"]["location"]
]
duplicate_axis = [
float(value) for value in duplicate["position"]["axis"]
]
duplicate_location_scalar = sum(
first * second
for first, second in zip(
duplicate_location, duplicate["_canonical_axis"]
)
)
duplicate_sign = sum(
first * second
for first, second in zip(
duplicate_axis, duplicate["_canonical_axis"]
)
)
duplicate["v_range"] = sorted(
(
value - duplicate_location_scalar
)
/ duplicate_sign
for value in duplicate["_absolute_interval"]
)
for operation in merged_operations:
operation.pop("_canonical_axis", None)
operation.pop("_canonical_anchor", None)
operation.pop("_absolute_interval", None)
operation["source_face_ids"] = sorted(
set(operation["source_face_ids"])
)
parameter["value"] = _number(new_value)
parameter["editable"] = True
parameter["edit_state"] = "edited_pending_acceptance"
result["edit_operations"] = merged_operations
preferred_boundary_strategy = result.get(
"reconstruction_strategy", {}
).get("boundary_strategy", "analytic_uv_rect")
result["reconstruction_strategy"] = {
"boundary_strategy": preferred_boundary_strategy,
"selection_status": "semantic_edit_candidate",
}
result["active_edit"] = {
"parameter": parameter_name,
"semantic_role": role,
"old_value": _number(old_value),
"new_value": _number(new_value),
"edited_face_ids": sorted(edited_face_ids),
"operation_count": len(merged_operations),
"range_status": (
"within_validated_range"
if parameter.get("validated_range")
and new_value
<= float(
parameter["validated_range"]["maximum_tested_inclusive"]
)
else "requires_revalidation"
),
"acceptance_status": "pending",
}
return result
def _point_value(values: list[float]) -> gp_Pnt:
return gp_Pnt(*map(float, values))
def _direction_value(values: list[float]) -> gp_Dir:
return gp_Dir(*map(float, values))
def _ax3_value(payload: dict[str, Any]) -> gp_Ax3:
result = gp_Ax3(
_point_value(payload["location"]),
_direction_value(payload["axis"]),
_direction_value(payload["x_direction"]),
)
if bool(result.Direct()) != bool(payload.get("direct", True)):
result.YReverse()
return result
def _ax2_value(payload: dict[str, Any]) -> gp_Ax2:
return gp_Ax2(
_point_value(payload["location"]),
_direction_value(payload["axis"]),
_direction_value(payload["x_direction"]),
)
def _real_array(values: list[float]) -> TColStd_Array1OfReal:
result = TColStd_Array1OfReal(1, len(values))
for index, value in enumerate(values, 1):
result.SetValue(index, float(value))
return result
def _integer_array(values: list[int]) -> TColStd_Array1OfInteger:
result = TColStd_Array1OfInteger(1, len(values))
for index, value in enumerate(values, 1):
result.SetValue(index, int(value))
return result
def _point_array(values: list[list[float]]) -> TColgp_Array1OfPnt:
result = TColgp_Array1OfPnt(1, len(values))
for index, value in enumerate(values, 1):
result.SetValue(index, _point_value(value))
return result
def _point2d_array(values: list[list[float]]) -> TColgp_Array1OfPnt2d:
result = TColgp_Array1OfPnt2d(1, len(values))
for index, value in enumerate(values, 1):
result.SetValue(index, gp_Pnt2d(*map(float, value)))
return result
def _pcurve_from_record(payload: dict[str, Any]) -> Any:
kind = payload["type"]
if kind == "line":
return Geom2d_Line(
gp_Lin2d(
gp_Pnt2d(*map(float, payload["location"])),
gp_Dir2d(*map(float, payload["direction"])),
)
)
if kind == "circle":
axis = gp_Ax22d(
gp_Pnt2d(*map(float, payload["location"])),
gp_Dir2d(*map(float, payload["x_direction"])),
gp_Dir2d(*map(float, payload["y_direction"])),
)
return Geom2d_Circle(gp_Circ2d(axis, float(payload["radius"])))
if kind == "ellipse":
axis = gp_Ax22d(
gp_Pnt2d(*map(float, payload["location"])),
gp_Dir2d(*map(float, payload["x_direction"])),
gp_Dir2d(*map(float, payload["y_direction"])),
)
return Geom2d_Ellipse(
gp_Elips2d(
axis,
float(payload["major_radius"]),
float(payload["minor_radius"]),
)
)
if kind == "bspline":
poles = _point2d_array(payload["poles"])
knots = _real_array(payload["knots"])
mults = _integer_array(payload["multiplicities"])
if payload.get("rational"):
return Geom2d_BSplineCurve(
poles,
_real_array(payload["weights"]),
knots,
mults,
int(payload["degree"]),
bool(payload["periodic"]),
)
return Geom2d_BSplineCurve(
poles,
knots,
mults,
int(payload["degree"]),
bool(payload["periodic"]),
)
raise ValueError(f"Unsupported pcurve record: {kind}")
def _curve_from_record(
payload: dict[str, Any],
surface: Any | None = None,
pcurve_payload: dict[str, Any] | None = None,
vertices: list[Any] | None = None,
) -> Any:
vertices = vertices or []
kind = payload["type"]
if surface is not None and pcurve_payload is not None:
first, last = map(float, pcurve_payload["parameter_range"])
arguments: list[Any] = [
_pcurve_from_record(pcurve_payload),
surface,
]
if len(vertices) == 2:
arguments.extend(vertices)
arguments.extend([first, last])
edge = BRepBuilderAPI_MakeEdge(*arguments).Edge()
if kind == "degenerate":
BRep_Builder().Degenerated(edge, True)
return edge
first, last = map(float, payload.get("parameter_range", [0.0, 1.0]))
if kind == "line":
curve = Geom_Line(
gp_Lin(
_point_value(payload["location"]),
_direction_value(payload["direction"]),
)
)
arguments = [curve]
if len(vertices) == 2:
arguments.extend(vertices)
arguments.extend([first, last])
return BRepBuilderAPI_MakeEdge(*arguments).Edge()
if kind == "circle":
curve = Geom_Circle(
gp_Circ(_ax2_value(payload["position"]), float(payload["radius"]))
)
arguments = [curve]
if len(vertices) == 2:
arguments.extend(vertices)
arguments.extend([first, last])
return BRepBuilderAPI_MakeEdge(*arguments).Edge()
if kind == "ellipse":
curve = Geom_Ellipse(
gp_Elips(
_ax2_value(payload["position"]),
float(payload["major_radius"]),
float(payload["minor_radius"]),
)
)
arguments = [curve]
if len(vertices) == 2:
arguments.extend(vertices)
arguments.extend([first, last])
return BRepBuilderAPI_MakeEdge(*arguments).Edge()
if kind == "bspline":
poles = _point_array(payload["poles"])
knots = _real_array(payload["knots"])
mults = _integer_array(payload["multiplicities"])
if payload.get("rational"):
curve = Geom_BSplineCurve(
poles,
_real_array(payload["weights"]),
knots,
mults,
int(payload["degree"]),
bool(payload["periodic"]),
)
else:
curve = Geom_BSplineCurve(
poles,
knots,
mults,
int(payload["degree"]),
bool(payload["periodic"]),
)
arguments = [curve]
if len(vertices) == 2:
arguments.extend(vertices)
arguments.extend([first, last])
return BRepBuilderAPI_MakeEdge(*arguments).Edge()
if kind == "degenerate":
return None
raise ValueError(f"Unsupported curve record: {kind}")
def _surface_from_record(payload: dict[str, Any]) -> Any:
kind = payload["type"]
if kind == "plane":
return Geom_Plane(_ax3_value(payload["position"]))
if kind == "cylinder":
return Geom_CylindricalSurface(
_ax3_value(payload["position"]), float(payload["radius"])
)
if kind == "cone":
return Geom_ConicalSurface(
_ax3_value(payload["position"]),
float(payload["semi_angle_radians"]),
float(payload["reference_radius"]),
)
if kind == "sphere":
return Geom_SphericalSurface(
_ax3_value(payload["position"]), float(payload["radius"])
)
if kind == "torus":
return Geom_ToroidalSurface(
_ax3_value(payload["position"]),
float(payload["major_radius"]),
float(payload["minor_radius"]),
)
if kind == "bspline":
poles_data = payload["poles"]
weights_data = payload["weights"]
poles = TColgp_Array2OfPnt(
1, len(poles_data), 1, len(poles_data[0])
)
weights = TColStd_Array2OfReal(
1, len(weights_data), 1, len(weights_data[0])
)
for u_index, row in enumerate(poles_data, 1):
for v_index, value in enumerate(row, 1):
poles.SetValue(u_index, v_index, _point_value(value))
weights.SetValue(
u_index, v_index, float(weights_data[u_index - 1][v_index - 1])
)
arguments = [
poles,
_real_array(payload["u_knots"]),
_real_array(payload["v_knots"]),
_integer_array(payload["u_multiplicities"]),
_integer_array(payload["v_multiplicities"]),
int(payload["u_degree"]),
int(payload["v_degree"]),
bool(payload["u_periodic"]),
bool(payload["v_periodic"]),
]
if payload.get("u_rational") or payload.get("v_rational"):
arguments.insert(1, weights)
return Geom_BSplineSurface(*arguments)
raise ValueError(f"Unsupported surface record: {kind}")
def _wire_from_record(
payload: dict[str, Any],
surface: Any,
boundary_strategy: str,
edge_cache: dict[str, Any],
vertex_cache: dict[str, Any],
) -> Any:
builder = BRepBuilderAPI_MakeWire()
resolved_edges: list[tuple[dict[str, Any], Any]] = []
for edge_payload in payload["edges"]:
topology_edge_id = str(
edge_payload.get("topology_edge_id", edge_payload["id"])
)
use_shared_edge = boundary_strategy != "first_pcurve"
edge = edge_cache.get(topology_edge_id) if use_shared_edge else None
if edge is None:
vertices = [
vertex_cache[vertex_id]
for vertex_id in edge_payload.get("vertex_ids", [])
if vertex_id in vertex_cache
]
use_parametric_curve = (
boundary_strategy == "first_pcurve"
or edge_payload["curve"]["type"] == "degenerate"
)
try:
edge = _curve_from_record(
edge_payload["curve"],
surface if use_parametric_curve else None,
edge_payload.get("pcurve")
if use_parametric_curve
else None,
vertices,
)
except Exception as exc:
raise ValueError(
f"Cannot rebuild edge {edge_payload['id']}: "
f"{type(exc).__name__}: {exc}"
) from exc
if edge is not None and use_shared_edge:
edge_cache[topology_edge_id] = edge
if edge is None:
continue
tolerance = max(float(edge_payload.get("tolerance", 1e-7)), 1e-7)
BRep_Builder().UpdateEdge(edge, tolerance)
resolved_edges.append((edge_payload, edge))
if boundary_strategy == "exact_3d_pcurve":
edge_occurrences: dict[
str, tuple[Any, list[dict[str, Any]]]
] = {}
for edge_payload, edge in resolved_edges:
pcurve = edge_payload.get("pcurve")
if pcurve is None:
continue
topology_edge_id = str(
edge_payload.get("topology_edge_id", edge_payload["id"])
)
if topology_edge_id not in edge_occurrences:
edge_occurrences[topology_edge_id] = (edge, [])
edge_occurrences[topology_edge_id][1].append(edge_payload)
edge_builder = BRep_Builder()
location = TopLoc_Location()
for topology_edge_id, (edge, occurrences) in edge_occurrences.items():
if len(occurrences) > 2:
raise ValueError(
"Cannot bind more than two pcurves for shared edge "
f"{topology_edge_id} on one face"
)
tolerance = max(
max(
float(occurrence.get("tolerance", 1e-7))
for occurrence in occurrences
),
1e-7,
)
pcurves = [
_pcurve_from_record(occurrence["pcurve"])
for occurrence in occurrences
]
try:
if len(pcurves) == 2:
edge_builder.UpdateEdge(
edge,
pcurves[0],
pcurves[1],
surface,
location,
tolerance,
)
else:
edge_builder.UpdateEdge(
edge, pcurves[0], surface, location, tolerance
)
first, last = map(
float, occurrences[0]["pcurve"]["parameter_range"]
)
edge_builder.Range(edge, surface, location, first, last)
except Exception as exc:
raise ValueError(
f"Cannot bind pcurve for edge {topology_edge_id}: "
f"{type(exc).__name__}: {exc}"
) from exc
for edge_payload, edge in resolved_edges:
if int(edge_payload["orientation"]) == 1:
edge = TopoDS.Edge_s(edge.Reversed())
builder.Add(edge)
if not builder.IsDone():
raise ValueError(f"Cannot rebuild wire {payload['id']}")
wire = builder.Wire()
if int(payload.get("orientation", 0)) == 1:
wire = TopoDS.Wire_s(wire.Reversed())
return wire
def _face_from_record(
payload: dict[str, Any],
boundary_strategy: str,
edge_cache: dict[str, Any],
vertex_cache: dict[str, Any],
) -> Any:
surface = _surface_from_record(payload["surface"])
wire_payloads = sorted(payload["wires"], key=lambda item: not item.get("outer"))
rectangular_parametric_patch = (
(
payload.get("edit_rebuild_strategy") == "uv_rect"
or payload["surface"]["type"] == "torus"
or (
boundary_strategy in {"uv_rect_all", "analytic_uv_rect"}
and payload["surface"]["type"] != "plane"
and (
boundary_strategy == "uv_rect_all"
or payload["surface"]["type"] != "bspline"
)
)
)
and len(wire_payloads) == 1
and all(
edge.get("pcurve", {}).get("type") == "line"
and (
abs(float(edge["pcurve"]["direction"][0])) <= 1e-9
or abs(float(edge["pcurve"]["direction"][1])) <= 1e-9
)
for edge in wire_payloads[0]["edges"]
)
)
if not wire_payloads or rectangular_parametric_patch:
u_min, u_max, v_min, v_max = map(float, payload["uv_bounds"])
face = BRepBuilderAPI_MakeFace(
surface, u_min, u_max, v_min, v_max, 1e-7
).Face()
else:
try:
outer = _wire_from_record(
wire_payloads[0],
surface,
boundary_strategy,
edge_cache,
vertex_cache,
)
except Exception as exc:
raise ValueError(
f"Cannot rebuild outer wire {wire_payloads[0]['id']}: "
f"{type(exc).__name__}: {exc}"
) from exc
inner_wires = []
for inner_payload in wire_payloads[1:]:
try:
inner_wires.append(
_wire_from_record(
inner_payload,
surface,
boundary_strategy,
edge_cache,
vertex_cache,
)
)
except Exception as exc:
raise ValueError(
f"Cannot rebuild inner wire {inner_payload['id']}: "
f"{type(exc).__name__}: {exc}"
) from exc
def make_trimmed_face(outer_wire: Any, inner_wire_list: list[Any]) -> Any:
face_builder = BRepBuilderAPI_MakeFace(surface, outer_wire, True)
for inner_wire in inner_wire_list:
face_builder.Add(inner_wire)
if not face_builder.IsDone():
raise ValueError(f"Cannot rebuild face {payload['id']}")
return face_builder.Face()
face = make_trimmed_face(outer, inner_wires)
BRepLib.BuildCurves3d_s(face)
if int(payload["orientation"]) == 1:
face = TopoDS.Face_s(face.Reversed())
return face
def _axis_affine_transform(
axis: list[float],
pivot: list[float],
axial_factor: float,
radial_factor: float,
) -> gp_GTrsf:
axis_length = math.sqrt(sum(float(value) ** 2 for value in axis))
if axis_length <= 1e-12:
raise ValueError("Axis-affine edit requires a non-zero axis")
unit = [float(value) / axis_length for value in axis]
axial = float(axial_factor)
radial = float(radial_factor)
if (
not math.isfinite(axial)
or not math.isfinite(radial)
or axial <= 0
or radial <= 0
):
raise ValueError("Axis-affine factors must be finite and positive")
values = [
radial * (1.0 if row == column else 0.0)
+ (axial - radial) * unit[row] * unit[column]
for row in range(3)
for column in range(3)
]
matrix = gp_Mat(*values)
pivot_values = [float(value) for value in pivot]
transformed_pivot = [
sum(
values[row * 3 + column] * pivot_values[column]
for column in range(3)
)
for row in range(3)
]
translation = [
pivot_values[index] - transformed_pivot[index]
for index in range(3)
]
transform = gp_GTrsf()
transform.SetVectorialPart(matrix)
transform.SetTranslationPart(gp_XYZ(*translation))
return transform
def _apply_edit_operations(shape: Any, payload: dict[str, Any]) -> Any:
operations = payload.get("edit_operations", [])
if not operations:
return shape
fixer = ShapeFix_Shape(shape)
fixer.SetPrecision(1e-7)
fixer.SetMaxTolerance(1e-5)
fixer.Perform()
result = fixer.Shape()
for operation in operations:
operation_name = operation.get("operation")
if operation_name == "axis_affine_scale":
transform = _axis_affine_transform(
operation["axis"],
operation["pivot"],
float(operation["axial_factor"]),
float(operation["radial_factor"]),
)
builder = BRepBuilderAPI_GTransform(result, transform, True)
if not builder.IsDone():
raise ValueError("Failed axis-affine scale operation")
result = builder.Shape()
continue
if operation_name == "uniform_scale":
factor = float(operation["factor"])
if not math.isfinite(factor) or factor <= 0:
raise ValueError("Uniform scale factor must be finite and positive")
transform = gp_Trsf()
transform.SetScale(
_point_value(operation["pivot"]),
factor,
)
builder = BRepBuilderAPI_Transform(result, transform, True)
if not builder.IsDone():
raise ValueError("Failed uniform scale operation")
result = builder.Shape()
continue
if operation_name != "enlarge_cylindrical_cut":
raise ValueError(
f"Unsupported SurfaceIR edit operation: {operation_name}"
)
v_min, v_max = sorted(map(float, operation["v_range"]))
height = v_max - v_min
if height <= 1e-9:
raise ValueError("Cylindrical cut edit has zero axial span")
axial_margin = max(1e-6, height * 1e-7)
v_min -= axial_margin
height += 2.0 * axial_margin
position = operation["position"]
axis = _direction_value(position["axis"])
location = _point_value(position["location"])
base = gp_Pnt(
location.X() + v_min * axis.X(),
location.Y() + v_min * axis.Y(),
location.Z() + v_min * axis.Z(),
)
cutter_axis = gp_Ax2(
base,
axis,
_direction_value(position["x_direction"]),
)
cutter = BRepPrimAPI_MakeCylinder(
cutter_axis, float(operation["new_radius"]), height
).Shape()
algorithm = BRepAlgoAPI_Cut(result, cutter)
algorithm.Build()
if not algorithm.IsDone():
raise ValueError(
f"Failed semantic cut for {operation.get('face_id')}"
)
result = algorithm.Shape()
return result
def build_surfaceir(
payload: dict[str, Any], boundary_strategy: str | None = None
) -> Any:
boundary_strategy = boundary_strategy or payload.get(
"reconstruction_strategy", {}
).get("boundary_strategy", "exact_3d")
if boundary_strategy not in {
"exact_3d",
"exact_3d_pcurve",
"first_pcurve",
"analytic_uv_rect",
"uv_rect_all",
}:
raise ValueError(f"Unknown boundary strategy: {boundary_strategy}")
rebuilt_solids = []
rebuilt_free_shells = []
edge_cache: dict[str, Any] = {}
vertex_cache: dict[str, Any] = {}
vertex_builder = BRep_Builder()
for vertex_payload in payload["surface_layer"].get("vertices", []):
vertex = BRepBuilderAPI_MakeVertex(
_point_value(vertex_payload["point"])
).Vertex()
vertex_builder.UpdateVertex(
vertex, max(float(vertex_payload.get("tolerance", 1e-7)), 1e-7)
)
vertex_cache[str(vertex_payload["id"])] = vertex
for solid_payload in payload["surface_layer"]["solids"]:
rebuilt_shells = []
for shell_payload in solid_payload["shells"]:
shell_tolerance = max(
[
float(edge.get("tolerance", 1e-7))
for face in shell_payload["faces"]
for wire in face["wires"]
for edge in wire["edges"]
]
or [1e-7]
)
sewing = BRepBuilderAPI_Sewing(
max(shell_tolerance, 1e-7), True, True, True, False
)
for face_payload in shell_payload["faces"]:
try:
face = _face_from_record(
face_payload,
boundary_strategy,
edge_cache,
vertex_cache,
)
except Exception as exc:
raise ValueError(
f"Cannot rebuild face {face_payload['id']}: "
f"{type(exc).__name__}: {exc}"
) from exc
sewing.Add(face)
sewing.Perform()
sewed = sewing.SewedShape()
shell_explorer = TopExp_Explorer(sewed, TopAbs_SHELL)
if not shell_explorer.More():
raise ValueError(f"Cannot sew shell {shell_payload['id']}")
shell = TopoDS.Shell_s(shell_explorer.Current())
if boundary_strategy == "exact_3d_pcurve":
rebuilt_shells.append(shell)
else:
solid_builder = BRepBuilderAPI_MakeSolid(shell)
if not solid_builder.IsDone():
raise ValueError(
f"Cannot rebuild solid {solid_payload['id']}"
)
rebuilt_solids.append(solid_builder.Solid())
if boundary_strategy == "exact_3d_pcurve":
solid_builder = BRepBuilderAPI_MakeSolid()
for shell in rebuilt_shells:
solid_builder.Add(shell)
if not solid_builder.IsDone():
raise ValueError(f"Cannot rebuild solid {solid_payload['id']}")
rebuilt_solids.append(solid_builder.Solid())
for shell_payload in payload["surface_layer"].get("free_shells", []):
shell_builder = BRep_Builder()
shell = TopoDS_Shell()
shell_builder.MakeShell(shell)
for face_payload in shell_payload["faces"]:
try:
shell_builder.Add(
shell,
_face_from_record(
face_payload,
boundary_strategy,
edge_cache,
vertex_cache,
)
)
except Exception as exc:
raise ValueError(
f"Cannot rebuild free-shell face {face_payload['id']}: "
f"{type(exc).__name__}: {exc}"
) from exc
rebuilt_free_shells.append(shell)
components = rebuilt_solids + rebuilt_free_shells
if len(components) == 1:
result = components[0]
else:
builder = BRep_Builder()
compound = TopoDS_Compound()
builder.MakeCompound(compound)
for component in components:
builder.Add(compound, component)
result = compound
return _apply_edit_operations(result, payload)
def extract_surfaceir(step_path: Path) -> dict[str, Any]:
source_bytes = step_path.read_bytes()
reader = STEPControl_Reader()
if reader.ReadFile(str(step_path)) != IFSelect_RetDone:
raise ValueError("OCCT failed to read STEP")
reader.TransferRoots()
shape = reader.OneShape()
topology = (
{"vertices": [], "solids": [], "free_shells": []}
if shape.IsNull()
else _topology_records(shape)
)
solids = topology["solids"]
free_shells = topology.get("free_shells", [])
faces = _surfaceir_faces(topology)
surface_types = sorted(
{face["surface"]["type"] for face in faces}
)
curve_types = sorted(
{
edge["curve"]["type"]
for face in faces
for wire in face["wires"]
for edge in wire["edges"]
}
)
digest = hashlib.sha256(source_bytes).hexdigest()
modification_levels = ["analytic_surface"]
if "bspline" in surface_types or "bspline" in curve_types:
modification_levels.append("spline_control")
semantic_layer = _cylinder_semantics(topology)
if (solids or free_shells) and topology["vertices"]:
points = [
[float(value) for value in vertex["point"]]
for vertex in topology["vertices"]
]
scale_pivot = [
_number(
(
min(point[axis_index] for point in points)
+ max(point[axis_index] for point in points)
)
/ 2.0
)
for axis_index in range(3)
]
semantic_layer["datums"]["overall_scale_pivot"] = {
"point": scale_pivot,
"inference": "surfaceir_vertex_bounds_center",
"confidence": 1.0,
}
semantic_layer["parameters"]["overall_scale"] = {
"value": 1.0,
"unit": "ratio",
"editable": False,
"semantic_role": "overall_scale",
"inference": "universal_source_independent_transform",
"confidence": 1.0,
"pivot_datum": "overall_scale_pivot",
"edit_state": "executable_uniform_scale_binding",
}
semantic_layer["features"].append(
{
"id": "overall_scale_control",
"operation": "uniform_scale",
"parameter": "overall_scale",
"pivot_datum": "overall_scale_pivot",
"inference": "canonical_edit_fallback",
"original_history_recovered": False,
"confidence": 1.0,
}
)
if len(semantic_layer["parameters"]) == 1:
for axis_name, axis_index in zip(("x", "y", "z"), range(3)):
extent = (
max(point[axis_index] for point in points)
- min(point[axis_index] for point in points)
)
if extent <= 1e-9:
continue
parameter_name = f"overall_size_{axis_name}"
semantic_layer["parameters"][parameter_name] = {
"value": _number(extent),
"unit": "mm",
"editable": False,
"semantic_role": parameter_name,
"inference": "canonical_global_axis_bounds_extent",
"confidence": 1.0,
"pivot_datum": "overall_scale_pivot",
"axis": axis_name,
"edit_state": "executable_axis_affine_binding",
}
semantic_layer["features"].append(
{
"id": f"{parameter_name}_control",
"operation": "axis_affine_scale",
"parameter": parameter_name,
"pivot_datum": "overall_scale_pivot",
"axis": axis_name,
"inference": "canonical_envelope_dimension",
"original_history_recovered": False,
"confidence": 1.0,
}
)
executable_semantic_parameters = sorted(
name
for name, parameter in semantic_layer["parameters"].items()
if parameter.get("edit_state")
in {
"executable_enlarge_cut_binding",
"executable_axis_affine_binding",
"executable_uniform_scale_binding",
}
)
return {
"schema_version": "3.0",
"designir_kind": "independent_parametric_cad",
"model_id": f"surfaceir_{digest[:16]}",
"units": "mm",
"document_status": (
"geometry_present"
if solids or free_shells
else "empty_geometry"
),
"reconstruction_mode": "surface_parametric",
"semantic_layer": semantic_layer,
"surface_layer": {
"vertices": topology["vertices"],
"solids": solids,
"free_shells": free_shells,
"surface_vocabulary": surface_types,
"curve_vocabulary": curve_types,
},
"edit_interface": {
"semantic_parameters": executable_semantic_parameters,
"surface_parameter_groups": [
"surface_layer.solids.*.shells.*.faces.*.surface",
"surface_layer.solids.*.shells.*.faces.*.wires.*.edges.*.curve",
],
"modification_levels": modification_levels,
},
"validation_contract": {
"source_independence": True,
"geometry_checks": [
"solid_count",
"volume",
"center_of_mass",
"bounding_box",
"symmetric_difference",
"surface_distance",
],
"edit_checks": [
"edited_parameter_changes_geometry",
"unselected_parameter_groups_remain_stable",
"result_is_valid_manifold",
],
},
}
def write_json(path: Path, payload: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
json.dumps(payload, ensure_ascii=False, separators=(",", ":")) + "\n",
encoding="utf-8",
)
def extract_folder(input_dir: Path, output_dir: Path) -> dict[str, Any]:
sources = sorted(
[
path
for path in input_dir.iterdir()
if path.is_file() and path.suffix.lower() in {".step", ".stp"}
]
)
rows = []
surface_counts: Counter[str] = Counter()
curve_counts: Counter[str] = Counter()
for source in sources:
output = output_dir / f"{source.stem}.designir.json"
try:
payload = extract_surfaceir(source)
write_json(output, payload)
surface_counts.update(payload["surface_layer"]["surface_vocabulary"])
curve_counts.update(payload["surface_layer"]["curve_vocabulary"])
rows.append(
{
"source_name": source.name,
"model_id": payload["model_id"],
"output_name": output.name,
"state": "extracted",
"solid_count": len(payload["surface_layer"]["solids"]),
"free_shell_count": len(
payload["surface_layer"].get("free_shells", [])
),
"document_status": payload["document_status"],
"surface_vocabulary": payload["surface_layer"][
"surface_vocabulary"
],
"curve_vocabulary": payload["surface_layer"][
"curve_vocabulary"
],
"byte_count": output.stat().st_size,
}
)
except Exception as exc:
rows.append(
{
"source_name": source.name,
"state": "failed",
"error": f"{type(exc).__name__}: {exc}",
}
)
report = {
"schema_version": "1.0",
"report_kind": "designir_3_surface_extraction",
"input_count": len(sources),
"extracted_count": sum(row["state"] == "extracted" for row in rows),
"failed_count": sum(row["state"] == "failed" for row in rows),
"case_presence_by_surface_type": dict(sorted(surface_counts.items())),
"case_presence_by_curve_type": dict(sorted(curve_counts.items())),
"total_json_bytes": sum(row.get("byte_count", 0) for row in rows),
"rows": rows,
}
write_json(output_dir / "manifest.json", report)
return report
def augment_overall_scale(designir_dir: Path, output: Path) -> dict[str, Any]:
"""Add a source-independent uniform-scale fallback to existing DesignIR."""
manifest = json.loads(
(designir_dir / "manifest.json").read_text(encoding="utf-8")
)
rows = []
for manifest_row in manifest["rows"]:
if manifest_row["state"] != "extracted":
continue
designir_path = designir_dir / manifest_row["output_name"]
payload = json.loads(designir_path.read_text(encoding="utf-8"))
vertices = payload.get("surface_layer", {}).get("vertices", [])
if (
payload.get("document_status") != "geometry_present"
or not vertices
):
rows.append(
{
"source_name": manifest_row["source_name"],
"state": "empty_geometry_skipped",
}
)
continue
points = [
[float(value) for value in vertex["point"]]
for vertex in vertices
]
pivot = [
_number(
(
min(point[index] for point in points)
+ max(point[index] for point in points)
)
/ 2.0
)
for index in range(3)
]
semantic_layer = payload.setdefault("semantic_layer", {})
datums = semantic_layer.setdefault("datums", {})
parameters = semantic_layer.setdefault("parameters", {})
features = semantic_layer.setdefault("features", [])
datums["overall_scale_pivot"] = {
"point": pivot,
"inference": "surfaceir_vertex_bounds_center",
"confidence": 1.0,
}
parameters["overall_scale"] = {
"value": 1.0,
"unit": "ratio",
"editable": False,
"semantic_role": "overall_scale",
"inference": "universal_source_independent_transform",
"confidence": 1.0,
"pivot_datum": "overall_scale_pivot",
"edit_state": "executable_uniform_scale_binding",
}
features[:] = [
feature
for feature in features
if feature.get("id") != "overall_scale_control"
]
features.append(
{
"id": "overall_scale_control",
"operation": "uniform_scale",
"parameter": "overall_scale",
"pivot_datum": "overall_scale_pivot",
"inference": "canonical_edit_fallback",
"original_history_recovered": False,
"confidence": 1.0,
}
)
edit_parameters = payload.setdefault("edit_interface", {}).setdefault(
"semantic_parameters", []
)
if "overall_scale" not in edit_parameters:
edit_parameters.append("overall_scale")
edit_parameters.sort()
write_json(designir_path, payload)
rows.append(
{
"source_name": manifest_row["source_name"],
"state": "overall_scale_added",
"pivot": pivot,
}
)
report = {
"schema_version": "1.0",
"report_kind": "designir_3_overall_scale_augmentation",
"case_count": len(rows),
"augmented_count": sum(
row["state"] == "overall_scale_added" for row in rows
),
"skipped_count": sum(
row["state"] != "overall_scale_added" for row in rows
),
"rows": rows,
}
write_json(output, report)
return report
def augment_axis_affine_bindings(
designir_dir: Path, output: Path
) -> dict[str, Any]:
"""Bind primary-axis dimensions only where no feature edit is validated."""
manifest = json.loads(
(designir_dir / "manifest.json").read_text(encoding="utf-8")
)
rows = []
for manifest_row in manifest["rows"]:
if manifest_row["state"] != "extracted":
continue
designir_path = designir_dir / manifest_row["output_name"]
payload = json.loads(designir_path.read_text(encoding="utf-8"))
if payload.get("document_status") != "geometry_present":
continue
parameters = (
payload.get("semantic_layer", {}).get("parameters", {})
)
has_validated_feature_edit = any(
name != "overall_scale"
and parameter.get("edit_state")
== "validated_executable_binding"
for name, parameter in parameters.items()
)
bound = []
if not has_validated_feature_edit:
for name, parameter in parameters.items():
if parameter.get("semantic_role") not in {
"outer_diameter",
"body_length",
"plate_thickness",
}:
continue
if not parameter.get("affected_face_ids"):
continue
parameter["editable"] = False
parameter["edit_state"] = "executable_axis_affine_binding"
bound.append(name)
edit_parameters = payload.setdefault("edit_interface", {}).setdefault(
"semantic_parameters", []
)
for name in bound:
if name not in edit_parameters:
edit_parameters.append(name)
edit_parameters.sort()
if bound:
write_json(designir_path, payload)
rows.append(
{
"source_name": manifest_row["source_name"],
"state": (
"axis_affine_bound"
if bound
else "not_applicable"
),
"parameters": sorted(bound),
}
)
report = {
"schema_version": "1.0",
"report_kind": "designir_3_axis_affine_binding_augmentation",
"case_count": len(rows),
"applicable_case_count": sum(
row["state"] == "axis_affine_bound" for row in rows
),
"parameter_count": sum(len(row["parameters"]) for row in rows),
"rows": rows,
}
write_json(output, report)
return report
def augment_directional_size_bindings(
designir_dir: Path, output: Path
) -> dict[str, Any]:
"""Add canonical global-axis dimensions to remaining scale-only models."""
manifest = json.loads(
(designir_dir / "manifest.json").read_text(encoding="utf-8")
)
rows = []
for manifest_row in manifest["rows"]:
if manifest_row["state"] != "extracted":
continue
designir_path = designir_dir / manifest_row["output_name"]
payload = json.loads(designir_path.read_text(encoding="utf-8"))
if payload.get("document_status") != "geometry_present":
continue
semantic_layer = payload.get("semantic_layer", {})
parameters = semantic_layer.get("parameters", {})
has_validated_named_edit = any(
name != "overall_scale"
and parameter.get("edit_state")
== "validated_executable_binding"
for name, parameter in parameters.items()
)
bound = []
if not has_validated_named_edit:
vertices = payload["surface_layer"]["vertices"]
points = [
[float(value) for value in vertex["point"]]
for vertex in vertices
]
pivot = [
_number(
(
min(point[index] for point in points)
+ max(point[index] for point in points)
)
/ 2.0
)
for index in range(3)
]
semantic_layer.setdefault("datums", {})[
"directional_size_pivot"
] = {
"point": pivot,
"inference": "surfaceir_vertex_bounds_center",
"confidence": 1.0,
}
features = semantic_layer.setdefault("features", [])
for axis_name, axis_index in zip(("x", "y", "z"), range(3)):
extent = (
max(point[axis_index] for point in points)
- min(point[axis_index] for point in points)
)
if extent <= 1e-9:
continue
parameter_name = f"overall_size_{axis_name}"
parameters[parameter_name] = {
"value": _number(extent),
"unit": "mm",
"editable": False,
"semantic_role": parameter_name,
"inference": "canonical_global_axis_bounds_extent",
"confidence": 1.0,
"pivot_datum": "directional_size_pivot",
"axis": axis_name,
"edit_state": "executable_axis_affine_binding",
}
features[:] = [
feature
for feature in features
if feature.get("id")
!= f"{parameter_name}_control"
]
features.append(
{
"id": f"{parameter_name}_control",
"operation": "axis_affine_scale",
"parameter": parameter_name,
"pivot_datum": "directional_size_pivot",
"axis": axis_name,
"inference": "canonical_envelope_dimension",
"original_history_recovered": False,
"confidence": 1.0,
}
)
bound.append(parameter_name)
edit_parameters = payload.setdefault("edit_interface", {}).setdefault(
"semantic_parameters", []
)
for name in bound:
if name not in edit_parameters:
edit_parameters.append(name)
edit_parameters.sort()
if bound:
write_json(designir_path, payload)
rows.append(
{
"source_name": manifest_row["source_name"],
"state": (
"directional_sizes_bound"
if bound
else "not_applicable"
),
"parameters": sorted(bound),
}
)
report = {
"schema_version": "1.0",
"report_kind": "designir_3_directional_size_binding_augmentation",
"case_count": len(rows),
"applicable_case_count": sum(
row["state"] == "directional_sizes_bound" for row in rows
),
"parameter_count": sum(len(row["parameters"]) for row in rows),
"rows": rows,
}
write_json(output, report)
return report
def _facts_from_shape(shape: Any) -> dict[str, Any]:
wrapped = shape.wrapped if hasattr(shape, "wrapped") else shape
bounds = Bnd_Box()
BRepBndLib.Add_s(wrapped, bounds)
if bounds.IsVoid():
x_min = y_min = z_min = x_max = y_max = z_max = 0.0
else:
(
x_min,
y_min,
z_min,
x_max,
y_max,
z_max,
) = bounds.Get()
properties = GProp_GProps()
BRepGProp.VolumeProperties_s(wrapped, properties)
volume_value = float(properties.Mass())
surface_properties = GProp_GProps()
BRepGProp.SurfaceProperties_s(wrapped, surface_properties)
surface_area = float(surface_properties.Mass())
if abs(float(properties.Mass())) <= 1e-12:
properties = surface_properties
center = properties.CentreOfMass()
counts = {}
for name, shape_type in {
"solid_count": TopAbs_SOLID,
"face_count": TopAbs_FACE,
"edge_count": TopAbs_EDGE,
"vertex_count": TopAbs_VERTEX,
}.items():
shape_map = TopTools_IndexedMapOfShape()
TopExp.MapShapes_s(wrapped, shape_type, shape_map)
counts[name] = shape_map.Extent()
return {
**counts,
"volume": float(volume_value),
"surface_area": surface_area,
"center_of_mass": [float(center.X()), float(center.Y()), float(center.Z())],
"bounds_min": [float(x_min), float(y_min), float(z_min)],
"bounds_max": [float(x_max), float(y_max), float(z_max)],
"valid": bool(BRepCheck_Analyzer(wrapped).IsValid()),
}
def _shape_facts(path: Path) -> tuple[Any, dict[str, Any]]:
try:
shape = import_step(str(path))
return shape, _facts_from_shape(shape)
except AssertionError:
reader = STEPControl_Reader()
if reader.ReadFile(str(path)) != IFSelect_RetDone:
raise ValueError(f"OCCT failed to read STEP facts: {path}")
reader.TransferRoots()
wrapped = reader.OneShape()
if wrapped.IsNull():
builder = BRep_Builder()
compound = TopoDS_Compound()
builder.MakeCompound(compound)
wrapped = compound
shape = wrapped
return shape, _facts_from_shape(shape)
def _max_error(first: list[float], second: list[float]) -> float:
return max(abs(a - b) for a, b in zip(first, second))
def _sample_shape_points(shape: Any) -> list[gp_Pnt]:
wrapped = shape.wrapped if hasattr(shape, "wrapped") else shape
points: list[gp_Pnt] = []
vertex_map = TopTools_IndexedMapOfShape()
TopExp.MapShapes_s(wrapped, TopAbs_VERTEX, vertex_map)
for index in range(1, vertex_map.Extent() + 1):
points.append(
BRep_Tool.Pnt_s(TopoDS.Vertex_s(vertex_map.FindKey(index)))
)
edge_map = TopTools_IndexedMapOfShape()
TopExp.MapShapes_s(wrapped, TopAbs_EDGE, edge_map)
for index in range(1, edge_map.Extent() + 1):
edge = TopoDS.Edge_s(edge_map.FindKey(index))
adaptor = BRepAdaptor_Curve(edge)
first = float(adaptor.FirstParameter())
last = float(adaptor.LastParameter())
if not math.isfinite(first) or not math.isfinite(last):
continue
for fraction in (0.25, 0.5, 0.75):
points.append(adaptor.Value(first + fraction * (last - first)))
face_map = TopTools_IndexedMapOfShape()
TopExp.MapShapes_s(wrapped, TopAbs_FACE, face_map)
for index in range(1, face_map.Extent() + 1):
face = TopoDS.Face_s(face_map.FindKey(index))
u_min, u_max, v_min, v_max = map(
float, BRepTools.UVBounds_s(face)
)
if not all(
math.isfinite(value)
for value in (u_min, u_max, v_min, v_max)
):
continue
adaptor = BRepAdaptor_Surface(face)
for u_fraction in (0.25, 0.5, 0.75):
for v_fraction in (0.25, 0.5, 0.75):
u_value = u_min + u_fraction * (u_max - u_min)
v_value = v_min + v_fraction * (v_max - v_min)
classifier = BRepClass_FaceClassifier(
face, gp_Pnt2d(u_value, v_value), 1e-7
)
if classifier.State() in {TopAbs_IN, TopAbs_ON}:
points.append(adaptor.Value(u_value, v_value))
return points
def _directed_sample_distance(
source_shape: Any, target_shape: Any
) -> tuple[float, int]:
target = (
target_shape.wrapped
if hasattr(target_shape, "wrapped")
else target_shape
)
distance = BRepExtrema_DistShapeShape()
distance.LoadS2(target)
maximum = 0.0
points = _sample_shape_points(source_shape)
for point in points:
vertex = BRepBuilderAPI_MakeVertex(point).Vertex()
distance.LoadS1(vertex)
distance.Perform()
if not distance.IsDone():
raise ValueError("Surface sample distance evaluation failed")
maximum = max(maximum, float(distance.Value()))
return maximum, len(points)
def validate_folder(
input_dir: Path,
designir_dir: Path,
rebuilt_dir: Path,
limit: int | None,
offset: int,
boolean_check: bool,
boundary_strategy: str,
) -> dict[str, Any]:
manifest = json.loads(
(designir_dir / "manifest.json").read_text(encoding="utf-8")
)
rows = [row for row in manifest["rows"] if row["state"] == "extracted"]
rows = rows[offset:]
if limit is not None:
rows = rows[:limit]
results = []
for row in rows:
source = input_dir / row["source_name"]
designir_path = designir_dir / row["output_name"]
rebuilt = rebuilt_dir / f"{Path(row['source_name']).stem}.step"
payload = json.loads(designir_path.read_text(encoding="utf-8"))
try:
shape = build_surfaceir(payload, boundary_strategy)
export_step_shape(shape, rebuilt)
teacher_shape, teacher = _shape_facts(source)
rebuilt_shape, rebuilt_facts = _shape_facts(rebuilt)
volume_base = max(abs(teacher["volume"]), 1e-12)
relative_volume_error = (
abs(teacher["volume"] - rebuilt_facts["volume"]) / volume_base
)
area_base = max(abs(teacher["surface_area"]), 1e-12)
relative_surface_area_error = abs(
teacher["surface_area"] - rebuilt_facts["surface_area"]
) / area_base
center_error = math.dist(
teacher["center_of_mass"], rebuilt_facts["center_of_mass"]
)
bounds_error = max(
_max_error(teacher["bounds_min"], rebuilt_facts["bounds_min"]),
_max_error(teacher["bounds_max"], rebuilt_facts["bounds_max"]),
)
topology_match = all(
teacher[name] == rebuilt_facts[name]
for name in ["solid_count", "face_count", "edge_count"]
)
vertex_count_delta = (
rebuilt_facts["vertex_count"] - teacher["vertex_count"]
)
symmetric_difference_ratio = None
bidirectional_sample_distance = None
sample_point_count = None
if boolean_check and teacher["solid_count"] > 0:
try:
symmetric_difference_ratio = float(
(teacher_shape - rebuilt_shape).volume
+ (rebuilt_shape - teacher_shape).volume
) / volume_base
except Exception:
symmetric_difference_ratio = math.inf
if symmetric_difference_ratio > 1e-8:
forward_distance, forward_count = (
_directed_sample_distance(
teacher_shape, rebuilt_shape
)
)
reverse_distance, reverse_count = (
_directed_sample_distance(
rebuilt_shape, teacher_shape
)
)
bidirectional_sample_distance = max(
forward_distance, reverse_distance
)
sample_point_count = forward_count + reverse_count
geometry_pass = (
rebuilt_facts["valid"]
and topology_match
and (
relative_volume_error <= 1e-7
if teacher["solid_count"] > 0
else relative_surface_area_error <= 1e-7
)
and center_error <= 1e-6
and bounds_error <= 1e-6
and (
symmetric_difference_ratio is None
or symmetric_difference_ratio <= 1e-8
or (
bidirectional_sample_distance is not None
and bidirectional_sample_distance <= 1e-4
)
)
)
results.append(
{
"source_name": row["source_name"],
"state": "validated" if geometry_pass else "geometry_mismatch",
"geometry_pass": geometry_pass,
"surface_vocabulary": payload["surface_layer"][
"surface_vocabulary"
],
"curve_vocabulary": payload["surface_layer"]["curve_vocabulary"],
"teacher": teacher,
"rebuilt": rebuilt_facts,
"topology_match": topology_match,
"vertex_count_delta": vertex_count_delta,
"relative_volume_error": relative_volume_error,
"relative_surface_area_error": relative_surface_area_error,
"center_of_mass_error": center_error,
"bounds_error": bounds_error,
"symmetric_difference_ratio": symmetric_difference_ratio,
"bidirectional_sample_distance": (
bidirectional_sample_distance
),
"surface_sample_point_count": sample_point_count,
}
)
except Exception as exc:
results.append(
{
"source_name": row["source_name"],
"state": "rebuild_failed",
"geometry_pass": False,
"surface_vocabulary": payload["surface_layer"][
"surface_vocabulary"
],
"curve_vocabulary": payload["surface_layer"]["curve_vocabulary"],
"error": f"{type(exc).__name__}: {exc}",
}
)
state_counts = Counter(row["state"] for row in results)
report = {
"schema_version": "1.0",
"report_kind": "designir_3_independent_rebuild_validation",
"case_count": len(results),
"offset": offset,
"boundary_strategy": boundary_strategy,
"geometry_pass_count": sum(row["geometry_pass"] for row in results),
"geometry_fail_count": sum(not row["geometry_pass"] for row in results),
"state_counts": dict(sorted(state_counts.items())),
"thresholds": {
"relative_volume_error_max": 1e-7,
"relative_surface_area_error_max": 1e-7,
"center_of_mass_error_mm_max": 1e-6,
"bounds_error_mm_max": 1e-6,
"symmetric_difference_ratio_max": 1e-8 if boolean_check else None,
"bidirectional_sample_distance_mm_max": (
1e-4 if boolean_check else None
),
"topology_must_match": True,
"vertex_count_normalization_is_diagnostic": True,
},
"rows": results,
}
write_json(rebuilt_dir / "validation-report.json", report)
return report
def reclassify_validation_report(
report_path: Path, output: Path
) -> dict[str, Any]:
"""Reapply the current deterministic thresholds to measured geometry."""
report = json.loads(report_path.read_text(encoding="utf-8"))
for row in report["rows"]:
if "teacher" not in row or "rebuilt" not in row:
row["geometry_pass"] = False
continue
teacher = row["teacher"]
rebuilt = row["rebuilt"]
row["topology_match"] = all(
int(teacher[name]) == int(rebuilt[name])
for name in ("solid_count", "face_count", "edge_count")
)
row["vertex_count_delta"] = (
int(rebuilt["vertex_count"]) - int(teacher["vertex_count"])
)
metric_pass = (
float(row["relative_volume_error"]) <= 1e-7
if int(teacher["solid_count"]) > 0
else float(row["relative_surface_area_error"]) <= 1e-7
)
symmetric_difference = row.get("symmetric_difference_ratio")
sample_distance = row.get("bidirectional_sample_distance")
distance_pass = (
symmetric_difference is None
or float(symmetric_difference) <= 1e-8
or (
sample_distance is not None
and float(sample_distance) <= 1e-4
)
)
row["geometry_pass"] = bool(
rebuilt["valid"]
and row["topology_match"]
and metric_pass
and float(row["center_of_mass_error"]) <= 1e-6
and float(row["bounds_error"]) <= 1e-6
and distance_pass
)
row["state"] = (
"validated" if row["geometry_pass"] else "geometry_mismatch"
)
state_counts = Counter(row["state"] for row in report["rows"])
report["geometry_pass_count"] = sum(
bool(row["geometry_pass"]) for row in report["rows"]
)
report["geometry_fail_count"] = sum(
not bool(row["geometry_pass"]) for row in report["rows"]
)
report["state_counts"] = dict(sorted(state_counts.items()))
report["thresholds"] = {
"relative_volume_error_max": 1e-7,
"relative_surface_area_error_max": 1e-7,
"center_of_mass_error_mm_max": 1e-6,
"bounds_error_mm_max": 1e-6,
"symmetric_difference_ratio_max": 1e-8,
"bidirectional_sample_distance_mm_max": 1e-4,
"topology_must_match": True,
"vertex_count_normalization_is_diagnostic": True,
}
report["report_kind"] = (
"designir_3_authoritative_reclassified_rebuild_validation"
)
report["measurement_source_report"] = str(report_path)
write_json(output, report)
return report
def select_validated_strategies(
designir_dir: Path,
primary_report_path: Path,
fallback_report_paths: list[Path],
output: Path,
) -> dict[str, Any]:
primary = json.loads(primary_report_path.read_text(encoding="utf-8"))
fallbacks = [
json.loads(path.read_text(encoding="utf-8"))
for path in fallback_report_paths
]
if any(
primary["case_count"] != fallback["case_count"]
for fallback in fallbacks
):
raise ValueError("Strategy reports must contain the same number of cases")
rows = []
for index, primary_row in enumerate(primary["rows"]):
fallback_rows = [fallback["rows"][index] for fallback in fallbacks]
if any(
primary_row["source_name"] != fallback_row["source_name"]
for fallback_row in fallback_rows
):
raise ValueError("Strategy reports are not paired by source")
if primary_row["geometry_pass"]:
selected = primary["boundary_strategy"]
selected_row = primary_row
status = "geometry_validated"
elif any(row["geometry_pass"] for row in fallback_rows):
selected_index = next(
position
for position, row in enumerate(fallback_rows)
if row["geometry_pass"]
)
selected = fallbacks[selected_index]["boundary_strategy"]
selected_row = fallback_rows[selected_index]
status = "geometry_validated"
else:
selected = primary["boundary_strategy"]
selected_row = primary_row
status = "unresolved"
designir_path = (
designir_dir / f"{Path(primary_row['source_name']).stem}.designir.json"
)
payload = json.loads(designir_path.read_text(encoding="utf-8"))
payload["reconstruction_strategy"] = {
"boundary_strategy": selected,
"selection_status": status,
}
write_json(designir_path, payload)
rows.append(
{
"source_name": primary_row["source_name"],
"selected_boundary_strategy": selected,
"selection_status": status,
"geometry_pass": bool(selected_row["geometry_pass"]),
"selected_state": selected_row["state"],
"primary_geometry_pass": primary_row["geometry_pass"],
"fallback_geometry_passes": [
row["geometry_pass"] for row in fallback_rows
],
}
)
report = {
"schema_version": "1.0",
"report_kind": "designir_3_validated_strategy_selection",
"case_count": len(rows),
"geometry_validated_count": sum(
row["selection_status"] == "geometry_validated" for row in rows
),
"geometry_pass_count": sum(row["geometry_pass"] for row in rows),
"geometry_fail_count": sum(not row["geometry_pass"] for row in rows),
"unresolved_count": sum(
row["selection_status"] == "unresolved" for row in rows
),
"selected_strategy_counts": dict(
sorted(Counter(row["selected_boundary_strategy"] for row in rows).items())
),
"rows": rows,
}
write_json(output, report)
return report
def export_step_shape(shape: Any, output: Path) -> None:
writer = STEPControl_Writer()
transfer_status = writer.Transfer(shape, STEPControl_AsIs)
if transfer_status != IFSelect_RetDone:
raise ValueError("OCCT failed to transfer rebuilt SurfaceIR shape")
output.parent.mkdir(parents=True, exist_ok=True)
write_status = writer.Write(str(output))
if write_status != IFSelect_RetDone:
raise ValueError("OCCT failed to write rebuilt SurfaceIR STEP")
def _observe_internal_cylinders_from_shape(shape: Any) -> list[dict[str, Any]]:
wrapped = shape.wrapped if hasattr(shape, "wrapped") else shape
observations = []
explorer = TopExp_Explorer(wrapped, TopAbs_FACE)
index = 0
while explorer.More():
index += 1
face = TopoDS.Face_s(explorer.Current())
adaptor = BRepAdaptor_Surface(face)
if adaptor.GetType() == GeomAbs_Cylinder and int(face.Orientation()) == 1:
cylinder = adaptor.Cylinder()
axis, anchor = _normalized_axis(
{
"axis": _direction(cylinder.Position().Direction()),
"location": _point(cylinder.Position().Location()),
}
)
observations.append(
{
"face_id": f"observed_face_{index}",
"axis": axis,
"anchor": anchor,
"radius": float(cylinder.Radius()),
}
)
explorer.Next()
return observations
def _match_edit_cylinders(
edited: dict[str, Any], observed_cylinders: list[dict[str, Any]]
) -> list[dict[str, Any]]:
target_rows = []
unmatched = list(observed_cylinders)
for operation in edited.get("edit_operations", []):
expected_axis, expected_anchor = _normalized_axis(operation["position"])
expected_radius = float(operation["new_radius"])
radius_tolerance = max(1e-5, expected_radius * 1e-5)
axis_tolerance = 1e-5
match_index = next(
(
index
for index, cylinder in enumerate(unmatched)
if _parallel(expected_axis, cylinder["axis"])
and _distance(expected_anchor, cylinder["anchor"])
<= axis_tolerance
and abs(cylinder["radius"] - expected_radius)
<= radius_tolerance
),
None,
)
observed_radius = None
observed_face_id = None
if match_index is not None:
match = unmatched.pop(match_index)
observed_radius = match["radius"]
observed_face_id = match["face_id"]
target_rows.append(
{
"source_face_id": operation["face_id"],
"expected_radius": expected_radius,
"observed_radius": observed_radius,
"observed_face_id": observed_face_id,
"pass": match_index is not None,
}
)
return target_rows
def _observe_axis_affine_target(
payload: dict[str, Any],
operation: dict[str, Any],
expected_value: float,
) -> dict[str, Any]:
vertex_points = {
vertex["id"]: [float(value) for value in vertex["point"]]
for vertex in payload["surface_layer"]["vertices"]
}
axis = [float(value) for value in operation["axis"]]
pivot = [float(value) for value in operation["pivot"]]
role = operation["semantic_role"]
if role in {"overall_size_x", "overall_size_y", "overall_size_z"}:
coordinates = [
_dot(
[
point[index] - pivot[index]
for index in range(3)
],
axis,
)
for point in vertex_points.values()
]
observed_value = max(coordinates) - min(coordinates)
return {
"semantic_role": role,
"observed_value": _number(observed_value),
"target_measure": "bounds_extent",
"observed_vertex_count": len(coordinates),
}
candidates = []
for face in _surfaceir_faces(payload["surface_layer"]):
vertex_ids = {
vertex_id
for wire in face["wires"]
for edge in wire["edges"]
for vertex_id in edge.get("vertex_ids", [])
}
points = [
vertex_points[vertex_id]
for vertex_id in vertex_ids
if vertex_id in vertex_points
]
if not points:
continue
axial_coordinates = []
radial_distances = []
for point in points:
delta = [
point[index] - pivot[index] for index in range(3)
]
axial = _dot(delta, axis)
axial_coordinates.append(axial)
radial_distances.append(
math.sqrt(
sum(
(
delta[index] - axial * axis[index]
)
** 2
for index in range(3)
)
)
)
radial_mean = sum(radial_distances) / len(radial_distances)
radial_spread = max(
abs(value - radial_mean) for value in radial_distances
)
radial_tolerance = max(1e-5, radial_mean * 1e-5)
if radial_spread > radial_tolerance:
continue
observed_value = (
2.0 * radial_mean
if role == "outer_diameter"
else max(axial_coordinates) - min(axial_coordinates)
)
if observed_value <= 1e-9:
continue
candidates.append(
{
"face_id": face["id"],
"observed_value": observed_value,
"boundary_vertex_count": len(points),
"absolute_error": abs(observed_value - expected_value),
}
)
if not candidates:
return {
"semantic_role": role,
"observed_value": None,
"reason": "no_axis_aligned_face_measurement",
}
match = min(candidates, key=lambda candidate: candidate["absolute_error"])
return {
"semantic_role": role,
"observed_value": _number(match["observed_value"]),
"observed_face_id": match["face_id"],
"source_target_face_id": operation["target_face_id"],
"boundary_vertex_count": match["boundary_vertex_count"],
"candidate_count": len(candidates),
}
def validate_semantic_edit(
teacher_step: Path,
designir_path: Path,
parameter_name: str,
value: float,
output_dir: Path,
) -> dict[str, Any]:
"""Acceptance-Agent check for one isolated semantic perturbation."""
payload = json.loads(designir_path.read_text(encoding="utf-8"))
edited = apply_semantic_parameter(payload, parameter_name, value)
output_dir.mkdir(parents=True, exist_ok=True)
edited_ir_path = output_dir / f"{designir_path.stem}.edited.json"
edited_step_path = output_dir / f"{designir_path.stem}.edited.step"
checks: dict[str, Any] = {}
try:
teacher_shape, teacher_facts = _shape_facts(teacher_step)
operation_names = [
operation.get("operation")
for operation in edited.get("edit_operations", [])
]
is_uniform_scale = operation_names == ["uniform_scale"]
is_axis_affine = operation_names == ["axis_affine_scale"]
is_scale_transform = is_uniform_scale or is_axis_affine
scale_operation = (
edited["edit_operations"][0] if is_scale_transform else None
)
preferred = edited.get("reconstruction_strategy", {}).get(
"boundary_strategy", "analytic_uv_rect"
)
strategies = list(
dict.fromkeys(
[
preferred,
"exact_3d_pcurve",
"exact_3d",
"analytic_uv_rect",
"first_pcurve",
"uv_rect_all",
]
)
)
strategy_attempts = []
selected_result = None
expected_removed_volume = (
0.0
if is_scale_transform
else sum(
math.pi
* (
float(operation["new_radius"]) ** 2
- float(operation["old_radius"]) ** 2
)
* abs(
float(operation["v_range"][1])
- float(operation["v_range"][0])
)
for operation in edited.get("edit_operations", [])
)
)
removal_tolerance = max(
1e-6,
expected_removed_volume * 1e-4,
abs(teacher_facts["volume"]) * 1e-8,
)
for strategy in strategies:
trial = copy.deepcopy(edited)
trial["reconstruction_strategy"] = {
"boundary_strategy": strategy,
"selection_status": "semantic_edit_candidate",
}
try:
rebuilt = build_surfaceir(trial)
export_step_shape(rebuilt, edited_step_path)
trial_shape, trial_facts = _shape_facts(edited_step_path)
volume_delta = (
trial_facts["volume"] - teacher_facts["volume"]
)
if is_scale_transform:
if is_uniform_scale:
factor = float(scale_operation["factor"])
pivot = [
float(component)
for component in scale_operation["pivot"]
]
expected_volume = (
teacher_facts["volume"] * factor**3
)
expected_center = [
pivot[index]
+ factor
* (
teacher_facts["center_of_mass"][index]
- pivot[index]
)
for index in range(3)
]
expected_bounds_min = [
pivot[index]
+ factor
* (
teacher_facts["bounds_min"][index]
- pivot[index]
)
for index in range(3)
]
expected_bounds_max = [
pivot[index]
+ factor
* (
teacher_facts["bounds_max"][index]
- pivot[index]
)
for index in range(3)
]
target_observation = {
"semantic_role": "overall_scale",
"factor": factor,
}
else:
baseline_topology = build_surfaceir(
payload, strategy
)
expected_topology = _apply_edit_operations(
baseline_topology,
{"edit_operations": [scale_operation]},
)
expected_step_path = (
output_dir
/ "expected-axis-affine-normalized.step"
)
export_step_shape(
expected_topology, expected_step_path
)
_, expected_facts = _shape_facts(
expected_step_path
)
expected_volume = expected_facts["volume"]
expected_center = expected_facts["center_of_mass"]
expected_bounds_min = expected_facts["bounds_min"]
expected_bounds_max = expected_facts["bounds_max"]
factor = (
float(scale_operation["radial_factor"])
if scale_operation["semantic_role"]
== "outer_diameter"
else float(scale_operation["axial_factor"])
)
observed_ir = extract_surfaceir(edited_step_path)
expected_parameter_value = float(value)
target_observation = _observe_axis_affine_target(
observed_ir,
scale_operation,
expected_parameter_value,
)
observed_value = target_observation[
"observed_value"
]
semantic_tolerance = max(
1e-5, abs(expected_parameter_value) * 1e-5
)
target_observation.update(
{
"expected_value": expected_parameter_value,
"pass": observed_value is not None
and abs(
observed_value - expected_parameter_value
)
<= semantic_tolerance,
}
)
volume_error = abs(
trial_facts["volume"] - expected_volume
) / max(abs(expected_volume), 1e-12)
center_error = math.dist(
trial_facts["center_of_mass"], expected_center
)
bounds_error = max(
_max_error(
trial_facts["bounds_min"], expected_bounds_min
),
_max_error(
trial_facts["bounds_max"], expected_bounds_max
),
)
topology_match = all(
trial_facts[name] == teacher_facts[name]
for name in (
"solid_count",
"face_count",
"edge_count",
)
)
vertex_count_delta = (
trial_facts["vertex_count"]
- teacher_facts["vertex_count"]
)
basic_pass = (
bool(trial_facts["valid"])
and topology_match
and volume_error <= 1e-7
and center_error <= 1e-6
and bounds_error <= 1e-6
and target_observation.get("pass", True)
)
trial_targets = [
{
**target_observation,
"expected_volume": expected_volume,
"observed_volume": trial_facts["volume"],
"relative_volume_error": volume_error,
"center_error": center_error,
"bounds_error": bounds_error,
"topology_match": topology_match,
"vertex_count_delta": vertex_count_delta,
"pass": basic_pass,
}
]
strategy_attempts.append(
{
"boundary_strategy": strategy,
"pass": basic_pass,
"solid_count": trial_facts["solid_count"],
"valid": trial_facts["valid"],
"volume_delta": volume_delta,
"scale_factor": factor,
"operation": operation_names[0],
"relative_volume_error": volume_error,
"center_error": center_error,
"bounds_error": bounds_error,
"topology_match": topology_match,
"vertex_count_delta": vertex_count_delta,
"target_observation": target_observation,
}
)
else:
trial_observed = _observe_internal_cylinders_from_shape(
trial_shape
)
trial_targets = _match_edit_cylinders(
trial, trial_observed
)
trial_removed_volume = -volume_delta
basic_pass = (
bool(trial_facts["valid"])
and trial_facts["solid_count"]
== teacher_facts["solid_count"]
and bool(trial_targets)
and all(row["pass"] for row in trial_targets)
and volume_delta
< -max(
1e-8, abs(teacher_facts["volume"]) * 1e-10
)
)
strategy_attempts.append(
{
"boundary_strategy": strategy,
"pass": basic_pass,
"solid_count": trial_facts["solid_count"],
"valid": trial_facts["valid"],
"target_axis_match_count": sum(
row["pass"] for row in trial_targets
),
"target_axis_count": len(trial_targets),
"volume_delta": volume_delta,
"removed_volume": trial_removed_volume,
"expected_removed_volume": expected_removed_volume,
"removal_tolerance": removal_tolerance,
}
)
if basic_pass:
selected_result = (
trial,
trial_shape,
trial_facts,
trial_targets,
)
break
except Exception as strategy_exc:
strategy_attempts.append(
{
"boundary_strategy": strategy,
"pass": False,
"error": (
f"{type(strategy_exc).__name__}: {strategy_exc}"
),
}
)
edited["active_edit"]["strategy_attempts"] = strategy_attempts
if selected_result is None:
raise ValueError("No semantic edit reconstruction strategy passed")
(
edited,
edited_shape,
edited_facts,
target_rows,
) = selected_result
edited["active_edit"]["selected_boundary_strategy"] = edited[
"reconstruction_strategy"
]["boundary_strategy"]
write_json(edited_ir_path, edited)
expected_parameters = payload["semantic_layer"]["parameters"]
expected_value = float(value)
checks["source_independence"] = {
"pass": not any(
token in edited_ir_path.read_text(encoding="utf-8").lower()
for token in (
str(teacher_step).lower(),
teacher_step.name.lower(),
"import_step",
"stepcontrol_reader",
"step_text",
"\"brep\"",
"\"mesh\"",
)
)
}
checks["target_parameter"] = {
"pass": bool(target_rows) and all(row["pass"] for row in target_rows),
"expected": expected_value,
"observed_semantic_value": (
expected_value if all(row["pass"] for row in target_rows) else None
),
"axis_radius_matches": target_rows,
}
checks["valid_solid"] = {
"pass": bool(edited_facts["valid"])
and edited_facts["solid_count"] == teacher_facts["solid_count"],
"teacher_solid_count": teacher_facts["solid_count"],
"edited_solid_count": edited_facts["solid_count"],
"edited_valid": edited_facts["valid"],
}
volume_delta = edited_facts["volume"] - teacher_facts["volume"]
surface_area_delta = (
edited_facts["surface_area"] - teacher_facts["surface_area"]
)
minimum_change = max(
1e-8, abs(teacher_facts["volume"]) * 1e-10
)
minimum_surface_change = max(
1e-8, abs(teacher_facts["surface_area"]) * 1e-10
)
checks["geometry_changed_as_expected"] = {
"pass": (
(
abs(volume_delta) > minimum_change
if teacher_facts["solid_count"] > 0
else abs(surface_area_delta) > minimum_surface_change
)
if is_scale_transform
else volume_delta < -minimum_change
),
"teacher_volume": teacher_facts["volume"],
"edited_volume": edited_facts["volume"],
"volume_delta": volume_delta,
"teacher_surface_area": teacher_facts["surface_area"],
"edited_surface_area": edited_facts["surface_area"],
"surface_area_delta": surface_area_delta,
"expected_direction": (
"increase"
if is_scale_transform
and (
float(scale_operation.get("factor", 1.0)) > 1.0
or float(scale_operation.get("axial_factor", 1.0))
* float(scale_operation.get("radial_factor", 1.0))
** 2
> 1.0
)
else "decrease"
),
}
invariant_rows = []
for name, expected in expected_parameters.items():
if name == parameter_name:
continue
expected_number = float(expected["value"])
edited_number = float(
edited["semantic_layer"]["parameters"][name]["value"]
)
invariant_rows.append(
{
"parameter": name,
"expected": expected_number,
"edited_designir": edited_number,
"pass": edited_number == expected_number,
}
)
removed_volume = teacher_facts["volume"] - edited_facts["volume"]
checks["non_target_parameters"] = {
"pass": all(row["pass"] for row in invariant_rows)
and (
operation_names
in (["uniform_scale"], ["axis_affine_scale"])
if is_scale_transform
else all(
operation
== "enlarge_cylindrical_cut"
for operation in operation_names
)
and removed_volume > minimum_change
),
"rows": invariant_rows,
"geometry_guard": {
"removed_volume": removed_volume,
"expected_removed_volume": expected_removed_volume,
"tolerance": removal_tolerance,
"operation_whitelist": operation_names,
"expected": (
"only one declared scale transform may change geometry"
if is_scale_transform
else "only declared cylindrical Cut operations may remove material; no additive operation is allowed"
),
},
}
accepted = all(check["pass"] for check in checks.values())
state = "accepted" if accepted else "rejected"
except Exception as exc:
accepted = False
state = "execution_failed"
checks["execution"] = {
"pass": False,
"error": f"{type(exc).__name__}: {exc}",
}
edited["active_edit"]["acceptance_status"] = state
write_json(edited_ir_path, edited)
report = {
"schema_version": "1.0",
"report_kind": "designir_3_semantic_edit_acceptance",
"source_name": teacher_step.name,
"model_id": payload["model_id"],
"parameter": parameter_name,
"requested_value": float(value),
"state": state,
"accepted": accepted,
"checks": checks,
"artifacts": {
"edited_designir": edited_ir_path.name,
"edited_step": (
edited_step_path.name if edited_step_path.is_file() else None
),
},
}
write_json(output_dir / "acceptance-report.json", report)
return report
def validate_edits_folder(
input_dir: Path,
designir_dir: Path,
output_dir: Path,
geometry_report_path: Path | None,
limit: int | None,
offset: int,
workers: int,
max_parameters_per_case: int,
timeout_seconds: int,
parameter_filter: str | None = None,
) -> dict[str, Any]:
"""Run isolated Acceptance-Agent workers over a STEP corpus."""
manifest = json.loads(
(designir_dir / "manifest.json").read_text(encoding="utf-8")
)
rows = [row for row in manifest["rows"] if row["state"] == "extracted"]
rows = rows[offset:]
if limit is not None:
rows = rows[:limit]
geometry_pass: dict[str, bool] = {}
if geometry_report_path is not None:
geometry_report = json.loads(
geometry_report_path.read_text(encoding="utf-8")
)
geometry_pass = {
row["source_name"]: bool(row["geometry_pass"])
for row in geometry_report["rows"]
}
attempts = []
case_rows = []
for row in rows:
source_name = row["source_name"]
designir_path = designir_dir / row["output_name"]
payload = json.loads(designir_path.read_text(encoding="utf-8"))
if geometry_pass and not geometry_pass.get(source_name, False):
case_rows.append(
{
"source_name": source_name,
"state": "baseline_geometry_not_validated",
"attempt_count": 0,
}
)
continue
parameters = payload.get("semantic_layer", {}).get("parameters", {})
candidates = [
name
for name in payload.get("edit_interface", {}).get(
"semantic_parameters", []
)
if name in parameters
and (
parameter_filter is None
or name == parameter_filter
)
and parameters[name].get("edit_state")
in {
"executable_enlarge_cut_binding",
"executable_axis_affine_binding",
"executable_uniform_scale_binding",
"validated_executable_binding",
}
][:max_parameters_per_case]
if not candidates:
case_rows.append(
{
"source_name": source_name,
"state": "no_executable_semantic_parameter",
"attempt_count": 0,
}
)
continue
case_rows.append(
{
"source_name": source_name,
"state": "scheduled",
"attempt_count": len(candidates),
}
)
for parameter_name in candidates:
old_value = float(parameters[parameter_name]["value"])
requested_value = old_value * 1.1
safe_parameter = re.sub(r"[^a-zA-Z0-9_.-]+", "_", parameter_name)
attempt_dir = (
output_dir / Path(source_name).stem / safe_parameter
)
attempts.append(
{
"source_name": source_name,
"teacher": input_dir / source_name,
"designir": designir_path,
"parameter": parameter_name,
"old_value": old_value,
"requested_value": requested_value,
"output_dir": attempt_dir,
}
)
def run_attempt(attempt: dict[str, Any]) -> dict[str, Any]:
report_path = attempt["output_dir"] / "acceptance-report.json"
if report_path.is_file():
report = json.loads(report_path.read_text(encoding="utf-8"))
if (
report.get("parameter") == attempt["parameter"]
and abs(
float(report.get("requested_value", math.nan))
- attempt["requested_value"]
)
<= 1e-12
and bool(report.get("accepted"))
):
return {
**attempt,
"state": report["state"],
"accepted": bool(report["accepted"]),
"resumed": True,
"report": str(report_path),
}
command = [
sys.executable,
str(Path(__file__).resolve()),
"validate-edit",
str(attempt["teacher"]),
str(attempt["designir"]),
"--parameter",
attempt["parameter"],
"--value",
str(attempt["requested_value"]),
"--output-dir",
str(attempt["output_dir"]),
]
try:
completed = subprocess.run(
command,
capture_output=True,
text=True,
timeout=timeout_seconds,
check=False,
)
if completed.returncode == 0 and report_path.is_file():
report = json.loads(report_path.read_text(encoding="utf-8"))
return {
**attempt,
"state": report["state"],
"accepted": bool(report["accepted"]),
"resumed": False,
"report": str(report_path),
}
return {
**attempt,
"state": "worker_failed",
"accepted": False,
"returncode": completed.returncode,
"stderr_tail": completed.stderr[-1000:],
}
except subprocess.TimeoutExpired:
return {
**attempt,
"state": "worker_timeout",
"accepted": False,
}
attempt_rows = []
output_dir.mkdir(parents=True, exist_ok=True)
with ThreadPoolExecutor(max_workers=max(1, workers)) as executor:
futures = [executor.submit(run_attempt, attempt) for attempt in attempts]
for future in as_completed(futures):
attempt_rows.append(future.result())
attempt_rows.sort(key=lambda row: (row["source_name"], row["parameter"]))
accepted_by_source = {
row["source_name"] for row in attempt_rows if row["accepted"]
}
attempted_by_source = {row["source_name"] for row in attempt_rows}
for row in case_rows:
if row["source_name"] in accepted_by_source:
row["state"] = "semantic_edit_accepted"
elif row["source_name"] in attempted_by_source:
row["state"] = "semantic_edit_rejected"
state_counts = Counter(row["state"] for row in case_rows)
attempt_state_counts = Counter(row["state"] for row in attempt_rows)
report = {
"schema_version": "1.0",
"report_kind": "designir_3_semantic_edit_batch_acceptance",
"case_count": len(case_rows),
"applicable_case_count": len(attempted_by_source),
"accepted_case_count": len(accepted_by_source),
"attempt_count": len(attempt_rows),
"accepted_attempt_count": sum(row["accepted"] for row in attempt_rows),
"case_state_counts": dict(sorted(state_counts.items())),
"attempt_state_counts": dict(sorted(attempt_state_counts.items())),
"perturbation": {
"kind": "multiply",
"factor": 1.1,
"maximum_parameters_per_case": max_parameters_per_case,
},
"worker_policy": {
"process_isolation": True,
"workers": workers,
"timeout_seconds": timeout_seconds,
},
"cases": case_rows,
"attempts": [
{
key: (
str(value)
if isinstance(value, Path)
else value
)
for key, value in row.items()
}
for row in attempt_rows
],
}
write_json(output_dir / "batch-acceptance-report.json", report)
return report
def merge_acceptance_reports(
baseline_path: Path, retry_paths: list[Path], output: Path
) -> dict[str, Any]:
baseline = json.loads(baseline_path.read_text(encoding="utf-8"))
attempts = {
(row["source_name"], row["parameter"]): row
for row in baseline["attempts"]
}
baseline_factor = float(
baseline.get("perturbation", {}).get("factor", 0.0)
)
if baseline_factor > 0.0:
for row in attempts.values():
if "old_value" not in row and "requested_value" in row:
row["old_value"] = (
float(row["requested_value"]) / baseline_factor
)
lineage = [str(baseline_path)]
for retry_path in retry_paths:
lineage.append(str(retry_path))
if retry_path.is_dir():
retry_rows = []
for report_path in sorted(
retry_path.rglob("acceptance-report.json")
):
retry_report = json.loads(
report_path.read_text(encoding="utf-8")
)
retry_rows.append(
{
"source_name": retry_report["source_name"],
"parameter": retry_report["parameter"],
"requested_value": retry_report["requested_value"],
"state": retry_report["state"],
"accepted": bool(retry_report["accepted"]),
"resumed": False,
"report": str(report_path),
}
)
else:
retry = json.loads(retry_path.read_text(encoding="utf-8"))
retry_rows = retry["attempts"]
for row in retry_rows:
key = (row["source_name"], row["parameter"])
previous = attempts.get(key, {})
merged_row = {**previous, **row}
if "old_value" in previous and "old_value" not in row:
merged_row["old_value"] = previous["old_value"]
attempts[key] = merged_row
attempt_rows = sorted(
attempts.values(), key=lambda row: (row["source_name"], row["parameter"])
)
accepted_by_source = {
row["source_name"] for row in attempt_rows if row["accepted"]
}
attempted_by_source = {row["source_name"] for row in attempt_rows}
case_rows = copy.deepcopy(baseline["cases"])
for row in case_rows:
if row["source_name"] in accepted_by_source:
row["state"] = "semantic_edit_accepted"
elif row["source_name"] in attempted_by_source:
row["state"] = "semantic_edit_rejected"
report = copy.deepcopy(baseline)
report["report_kind"] = "designir_3_semantic_edit_merged_acceptance"
report["lineage"] = lineage
report["attempts"] = attempt_rows
report["cases"] = case_rows
report["applicable_case_count"] = len(attempted_by_source)
report["accepted_case_count"] = len(accepted_by_source)
report["attempt_count"] = len(attempt_rows)
report["accepted_attempt_count"] = sum(
row["accepted"] for row in attempt_rows
)
report["case_state_counts"] = dict(
sorted(Counter(row["state"] for row in case_rows).items())
)
report["attempt_state_counts"] = dict(
sorted(Counter(row["state"] for row in attempt_rows).items())
)
factors = sorted(
{
round(
float(row["requested_value"]) / float(row["old_value"]),
8,
)
for row in attempt_rows
if float(row.get("old_value", 0.0)) != 0.0
}
)
if len(factors) > 1:
report["perturbation"] = {
"kind": "adaptive_multiply",
"validated_factors": factors,
"selection_policy": "largest attempted factor with a passing edit contract",
}
write_json(output, report)
return report
def apply_edit_validation_to_designir(
designir_dir: Path, acceptance_report_path: Path, output: Path
) -> dict[str, Any]:
"""Persist measured edit bounds and rejections in private DesignIR files."""
report = json.loads(
acceptance_report_path.read_text(encoding="utf-8")
)
rows = []
for attempt in report["attempts"]:
source_name = attempt["source_name"]
parameter_name = attempt["parameter"]
designir_path = (
designir_dir / f"{Path(source_name).stem}.designir.json"
)
payload = json.loads(designir_path.read_text(encoding="utf-8"))
parameters = payload.get("semantic_layer", {}).get("parameters", {})
if parameter_name not in parameters:
raise ValueError(
f"{source_name} is missing semantic parameter {parameter_name}"
)
parameter = parameters[parameter_name]
old_value = float(
attempt.get("old_value", parameter["value"])
)
requested_value = float(attempt["requested_value"])
if attempt["accepted"]:
parameter["editable"] = True
parameter["edit_state"] = "validated_executable_binding"
parameter["validated_range"] = {
"direction": "increase",
"minimum_tested_inclusive": _number(old_value),
"maximum_tested_inclusive": _number(requested_value),
"acceptance_contract": "designir_3_semantic_edit_acceptance",
}
state = "validated_edit_range"
else:
parameter["editable"] = False
parameter["edit_state"] = "rejected_by_edit_contract"
parameter["validation_failure"] = {
"requested_value": _number(requested_value),
"attempt_state": attempt["state"],
}
parameter.pop("validated_range", None)
state = "rejected_edit_binding"
write_json(designir_path, payload)
rows.append(
{
"source_name": source_name,
"parameter": parameter_name,
"state": state,
"old_value": _number(old_value),
"requested_value": _number(requested_value),
}
)
summary = {
"schema_version": "1.0",
"report_kind": "designir_3_persisted_edit_validation",
"case_count": len({row["source_name"] for row in rows}),
"parameter_count": len(rows),
"validated_parameter_count": sum(
row["state"] == "validated_edit_range" for row in rows
),
"rejected_parameter_count": sum(
row["state"] == "rejected_edit_binding" for row in rows
),
"acceptance_report": str(acceptance_report_path),
"rows": rows,
}
write_json(output, summary)
return summary
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
commands = parser.add_subparsers(dest="command", required=True)
extract = commands.add_parser("extract")
extract.add_argument("step", type=Path)
extract.add_argument("--output", type=Path, required=True)
rebuild = commands.add_parser("rebuild")
rebuild.add_argument("designir", type=Path)
rebuild.add_argument("--output", type=Path, required=True)
rebuild.add_argument(
"--boundary-strategy",
choices=[
"exact_3d",
"exact_3d_pcurve",
"first_pcurve",
"analytic_uv_rect",
"uv_rect_all",
],
)
edit = commands.add_parser("edit")
edit.add_argument("designir", type=Path)
edit.add_argument("--parameter", required=True)
edit.add_argument("--value", type=float, required=True)
edit.add_argument("--output-designir", type=Path, required=True)
edit.add_argument("--output-step", type=Path)
validate_edit = commands.add_parser("validate-edit")
validate_edit.add_argument("teacher", type=Path)
validate_edit.add_argument("designir", type=Path)
validate_edit.add_argument("--parameter", required=True)
validate_edit.add_argument("--value", type=float, required=True)
validate_edit.add_argument("--output-dir", type=Path, required=True)
validate_edits = commands.add_parser("validate-edits-folder")
validate_edits.add_argument("input", type=Path)
validate_edits.add_argument("--designir-dir", type=Path, required=True)
validate_edits.add_argument("--output-dir", type=Path, required=True)
validate_edits.add_argument("--geometry-report", type=Path)
validate_edits.add_argument("--limit", type=int)
validate_edits.add_argument("--offset", type=int, default=0)
validate_edits.add_argument("--workers", type=int, default=4)
validate_edits.add_argument("--max-parameters-per-case", type=int, default=1)
validate_edits.add_argument("--timeout-seconds", type=int, default=60)
validate_edits.add_argument("--parameter")
merge_acceptance = commands.add_parser("merge-acceptance-reports")
merge_acceptance.add_argument("--baseline", type=Path, required=True)
merge_acceptance.add_argument(
"--retry", type=Path, action="append", required=True
)
merge_acceptance.add_argument("--output", type=Path, required=True)
persist_edit_validation = commands.add_parser("persist-edit-validation")
persist_edit_validation.add_argument(
"--designir-dir", type=Path, required=True
)
persist_edit_validation.add_argument(
"--acceptance-report", type=Path, required=True
)
persist_edit_validation.add_argument("--output", type=Path, required=True)
augment_scale = commands.add_parser("augment-overall-scale")
augment_scale.add_argument("designir_dir", type=Path)
augment_scale.add_argument("--output", type=Path, required=True)
augment_axis = commands.add_parser("augment-axis-affine-bindings")
augment_axis.add_argument("designir_dir", type=Path)
augment_axis.add_argument("--output", type=Path, required=True)
augment_directional = commands.add_parser(
"augment-directional-size-bindings"
)
augment_directional.add_argument("designir_dir", type=Path)
augment_directional.add_argument("--output", type=Path, required=True)
batch = commands.add_parser("extract-folder")
batch.add_argument("input", type=Path)
batch.add_argument("--output-dir", type=Path, required=True)
validate = commands.add_parser("validate-folder")
validate.add_argument("input", type=Path)
validate.add_argument("--designir-dir", type=Path, required=True)
validate.add_argument("--rebuilt-dir", type=Path, required=True)
validate.add_argument("--limit", type=int)
validate.add_argument("--offset", type=int, default=0)
validate.add_argument("--boolean", action="store_true")
validate.add_argument(
"--boundary-strategy",
choices=[
"exact_3d",
"exact_3d_pcurve",
"first_pcurve",
"analytic_uv_rect",
"uv_rect_all",
],
default="exact_3d",
)
reclassify = commands.add_parser("reclassify-validation-report")
reclassify.add_argument("report", type=Path)
reclassify.add_argument("--output", type=Path, required=True)
select = commands.add_parser("select-strategies")
select.add_argument("--designir-dir", type=Path, required=True)
select.add_argument("--primary-report", type=Path, required=True)
select.add_argument(
"--fallback-report", type=Path, action="append", required=True
)
select.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
if args.command == "extract":
payload = extract_surfaceir(args.step.resolve())
write_json(args.output.resolve(), payload)
print(
json.dumps(
{
"output": str(args.output.resolve()),
"solid_count": len(payload["surface_layer"]["solids"]),
"surface_vocabulary": payload["surface_layer"][
"surface_vocabulary"
],
"curve_vocabulary": payload["surface_layer"][
"curve_vocabulary"
],
},
ensure_ascii=False,
)
)
elif args.command == "rebuild":
payload = json.loads(args.designir.read_text(encoding="utf-8"))
shape = build_surfaceir(payload, args.boundary_strategy)
export_step_shape(shape, args.output)
print(json.dumps({"output": str(args.output.resolve())}))
elif args.command == "edit":
payload = json.loads(args.designir.read_text(encoding="utf-8"))
edited = apply_semantic_parameter(
payload, args.parameter, args.value
)
write_json(args.output_designir.resolve(), edited)
output = {
"output_designir": str(args.output_designir.resolve()),
"parameter": args.parameter,
"value": args.value,
}
if args.output_step is not None:
shape = build_surfaceir(edited)
export_step_shape(shape, args.output_step.resolve())
output["output_step"] = str(args.output_step.resolve())
print(json.dumps(output, ensure_ascii=False))
elif args.command == "validate-edit":
report = validate_semantic_edit(
args.teacher.resolve(),
args.designir.resolve(),
args.parameter,
args.value,
args.output_dir.resolve(),
)
print(
json.dumps(
{
"report": str(
(args.output_dir.resolve() / "acceptance-report.json")
),
"state": report["state"],
"accepted": report["accepted"],
},
ensure_ascii=False,
)
)
elif args.command == "validate-edits-folder":
report = validate_edits_folder(
args.input.resolve(),
args.designir_dir.resolve(),
args.output_dir.resolve(),
args.geometry_report.resolve()
if args.geometry_report is not None
else None,
args.limit,
args.offset,
args.workers,
args.max_parameters_per_case,
args.timeout_seconds,
args.parameter,
)
print(
json.dumps(
{
"report": str(
(
args.output_dir.resolve()
/ "batch-acceptance-report.json"
)
),
"case_count": report["case_count"],
"applicable_case_count": report["applicable_case_count"],
"accepted_case_count": report["accepted_case_count"],
"attempt_count": report["attempt_count"],
"accepted_attempt_count": report[
"accepted_attempt_count"
],
"case_state_counts": report["case_state_counts"],
"attempt_state_counts": report["attempt_state_counts"],
},
ensure_ascii=False,
)
)
elif args.command == "merge-acceptance-reports":
report = merge_acceptance_reports(
args.baseline.resolve(),
[path.resolve() for path in args.retry],
args.output.resolve(),
)
print(
json.dumps(
{
"output": str(args.output.resolve()),
"applicable_case_count": report["applicable_case_count"],
"accepted_case_count": report["accepted_case_count"],
"attempt_state_counts": report["attempt_state_counts"],
},
ensure_ascii=False,
)
)
elif args.command == "persist-edit-validation":
report = apply_edit_validation_to_designir(
args.designir_dir.resolve(),
args.acceptance_report.resolve(),
args.output.resolve(),
)
print(
json.dumps(
{
"output": str(args.output.resolve()),
"parameter_count": report["parameter_count"],
"validated_parameter_count": report[
"validated_parameter_count"
],
"rejected_parameter_count": report[
"rejected_parameter_count"
],
},
ensure_ascii=False,
)
)
elif args.command == "augment-overall-scale":
report = augment_overall_scale(
args.designir_dir.resolve(),
args.output.resolve(),
)
print(
json.dumps(
{
"output": str(args.output.resolve()),
"case_count": report["case_count"],
"augmented_count": report["augmented_count"],
"skipped_count": report["skipped_count"],
},
ensure_ascii=False,
)
)
elif args.command == "augment-axis-affine-bindings":
report = augment_axis_affine_bindings(
args.designir_dir.resolve(),
args.output.resolve(),
)
print(
json.dumps(
{
"output": str(args.output.resolve()),
"case_count": report["case_count"],
"applicable_case_count": report[
"applicable_case_count"
],
"parameter_count": report["parameter_count"],
},
ensure_ascii=False,
)
)
elif args.command == "augment-directional-size-bindings":
report = augment_directional_size_bindings(
args.designir_dir.resolve(),
args.output.resolve(),
)
print(
json.dumps(
{
"output": str(args.output.resolve()),
"case_count": report["case_count"],
"applicable_case_count": report[
"applicable_case_count"
],
"parameter_count": report["parameter_count"],
},
ensure_ascii=False,
)
)
elif args.command == "extract-folder":
report = extract_folder(args.input.resolve(), args.output_dir.resolve())
print(
json.dumps(
{
"manifest": str(
(args.output_dir.resolve() / "manifest.json").resolve()
),
"input_count": report["input_count"],
"extracted_count": report["extracted_count"],
"failed_count": report["failed_count"],
"total_json_bytes": report["total_json_bytes"],
},
ensure_ascii=False,
)
)
elif args.command == "validate-folder":
report = validate_folder(
args.input.resolve(),
args.designir_dir.resolve(),
args.rebuilt_dir.resolve(),
args.limit,
args.offset,
args.boolean,
args.boundary_strategy,
)
print(
json.dumps(
{
"report": str(
(args.rebuilt_dir.resolve() / "validation-report.json")
),
"case_count": report["case_count"],
"geometry_pass_count": report["geometry_pass_count"],
"geometry_fail_count": report["geometry_fail_count"],
"state_counts": report["state_counts"],
},
ensure_ascii=False,
)
)
elif args.command == "reclassify-validation-report":
report = reclassify_validation_report(
args.report.resolve(),
args.output.resolve(),
)
print(
json.dumps(
{
"output": str(args.output.resolve()),
"case_count": report["case_count"],
"geometry_pass_count": report[
"geometry_pass_count"
],
"geometry_fail_count": report[
"geometry_fail_count"
],
"state_counts": report["state_counts"],
},
ensure_ascii=False,
)
)
elif args.command == "select-strategies":
report = select_validated_strategies(
args.designir_dir.resolve(),
args.primary_report.resolve(),
[path.resolve() for path in args.fallback_report],
args.output.resolve(),
)
print(
json.dumps(
{
"output": str(args.output.resolve()),
"case_count": report["case_count"],
"geometry_validated_count": report["geometry_validated_count"],
"unresolved_count": report["unresolved_count"],
"selected_strategy_counts": report["selected_strategy_counts"],
},
ensure_ascii=False,
)
)
if __name__ == "__main__":
main()