feat(cadfs): expand reconstruction capability coverage
This commit is contained in:
@@ -82,6 +82,12 @@ def _transform_contours(contours: list[_Ctx], workplane: _Ctx) -> list[_Ctx]:
|
||||
normal = workplane.get("normal") or [0, 0, 1]
|
||||
for edge in contours:
|
||||
output = deepcopy(edge)
|
||||
if edge["type"] == "circle":
|
||||
output["center_mm"] = _to_3d(workplane, edge["center_mm"][0], edge["center_mm"][1])
|
||||
output["x_dir_mm"] = _to_3d_vector(workplane, 1.0, 0.0)
|
||||
output["normal"] = list(normal)
|
||||
transformed.append(output)
|
||||
continue
|
||||
output["start_mm"] = _to_3d(workplane, edge["start_mm"][0], edge["start_mm"][1])
|
||||
output["end_mm"] = _to_3d(workplane, edge["end_mm"][0], edge["end_mm"][1])
|
||||
if edge["type"] == "arc":
|
||||
@@ -245,10 +251,19 @@ def _segment_edges(segment: _Ctx) -> list[_Ctx]:
|
||||
return _ellipse_edges(segment)
|
||||
if kind == "bspline":
|
||||
points = segment.get("points") or []
|
||||
if len(points) < 3:
|
||||
raise ValueError("analytic_contours: bspline needs at least 3 interpolation points")
|
||||
if len(points) < 2:
|
||||
raise ValueError("analytic_contours: bspline needs at least 2 interpolation points")
|
||||
converted = [_point(point) for point in points]
|
||||
periodic = bool(segment.get("periodic"))
|
||||
start_tangent = segment.get("start_tangent")
|
||||
end_tangent = segment.get("end_tangent")
|
||||
if len(converted) == 2:
|
||||
if periodic:
|
||||
raise ValueError("analytic_contours: two-point bspline cannot be periodic")
|
||||
if _distance(converted[0], converted[1]) <= _TOLERANCE_MM:
|
||||
raise ValueError("analytic_contours: two-point bspline endpoints must be distinct")
|
||||
if start_tangent is None or end_tangent is None:
|
||||
raise ValueError("analytic_contours: two-point bspline requires both endpoint tangents")
|
||||
if periodic:
|
||||
if _distance(converted[0], converted[-1]) > _TOLERANCE_MM:
|
||||
raise ValueError("analytic_contours: periodic bspline endpoints do not meet")
|
||||
@@ -271,6 +286,8 @@ def _segment_edges(segment: _Ctx) -> list[_Ctx]:
|
||||
raise ValueError("analytic_contours: bspline parameters must be finite")
|
||||
if any(right - left <= _TOLERANCE_MM for left, right in zip(parameters, parameters[1:])):
|
||||
raise ValueError("analytic_contours: bspline parameters must be strictly increasing")
|
||||
if len(converted) == 2 and parameters is None:
|
||||
raise ValueError("analytic_contours: two-point bspline requires explicit parameters")
|
||||
output: _Ctx = {
|
||||
"type": "bspline",
|
||||
"start_mm": converted[0],
|
||||
@@ -280,8 +297,6 @@ def _segment_edges(segment: _Ctx) -> list[_Ctx]:
|
||||
**({"parameters": parameters} if parameters is not None else {}),
|
||||
**({"parameters": _centripetal_parameters(interpolation_points, periodic)} if parameters is None and parameterization == "centripetal" else {}),
|
||||
}
|
||||
start_tangent = segment.get("start_tangent")
|
||||
end_tangent = segment.get("end_tangent")
|
||||
if (start_tangent is None) != (end_tangent is None):
|
||||
raise ValueError("analytic_contours: bspline requires both endpoint tangents")
|
||||
if start_tangent is not None:
|
||||
@@ -293,13 +308,68 @@ def _segment_edges(segment: _Ctx) -> list[_Ctx]:
|
||||
raise ValueError(f"analytic_contours: unsupported segment type {kind!r}")
|
||||
|
||||
|
||||
def _imprint_segment_edges(segment: _Ctx) -> list[_Ctx]:
|
||||
"""Convert an IMPRINT source while retaining its FeatureScript edge identity.
|
||||
|
||||
Closed contour assembly intentionally divides circles into four arcs so
|
||||
that its loops have explicit vertices. An IMPRINT source id, however,
|
||||
denotes one logical FeatureScript edge. Splitting that circle before the
|
||||
OCC arrangement loses the one-to-one source/history mapping and makes a
|
||||
valid fragment appear to be an ambiguous multi-edge source.
|
||||
"""
|
||||
if segment.get("type") != "circle":
|
||||
return _segment_edges(segment)
|
||||
center = segment.get("center") or [0.0, 0.0]
|
||||
radius = float(segment.get("radius_mm") or 0.0)
|
||||
if radius <= 0:
|
||||
raise ValueError("planar_imprint: circle radius_mm must be > 0")
|
||||
output: _Ctx = {
|
||||
"type": "circle",
|
||||
"center_mm": [float(center[0]), float(center[1]), 0.0],
|
||||
"radius_mm": radius,
|
||||
}
|
||||
if "clockwise" in segment:
|
||||
output["clockwise"] = bool(segment["clockwise"])
|
||||
return [output]
|
||||
|
||||
|
||||
def _sample_loop(edges: list[_Ctx]) -> list[tuple[float, float]]:
|
||||
points: list[tuple[float, float]] = []
|
||||
for edge in edges:
|
||||
start = edge["start_mm"]
|
||||
points.append((float(start[0]), float(start[1])))
|
||||
if edge.get("type") == "bspline":
|
||||
points.extend((float(point[0]), float(point[1])) for point in edge["points_mm"][1:-1])
|
||||
spline_points = edge["points_mm"]
|
||||
if len(spline_points) == 2:
|
||||
# GeomAPI_Interpolate with exactly two endpoint derivatives is
|
||||
# the cubic Hermite curve over the explicit parameter span.
|
||||
# There are no intermediate interpolation points to sample, so
|
||||
# use its analytical points rather than incorrectly treating
|
||||
# an otherwise valid curved loop as zero-area.
|
||||
parameters = edge.get("parameters") or []
|
||||
start_tangent = edge.get("start_tangent_mm")
|
||||
end_tangent = edge.get("end_tangent_mm")
|
||||
if len(parameters) != 2 or start_tangent is None or end_tangent is None:
|
||||
raise ValueError("analytic_contours: two-point bspline sampling is unresolved")
|
||||
parameter_span = float(parameters[1]) - float(parameters[0])
|
||||
if parameter_span <= _TOLERANCE_MM:
|
||||
raise ValueError("analytic_contours: two-point bspline parameter span is degenerate")
|
||||
start_point, end_point = spline_points
|
||||
for fraction in (0.25, 0.5, 0.75):
|
||||
squared = fraction * fraction
|
||||
cubed = squared * fraction
|
||||
h00 = 2.0 * cubed - 3.0 * squared + 1.0
|
||||
h10 = cubed - 2.0 * squared + fraction
|
||||
h01 = -2.0 * cubed + 3.0 * squared
|
||||
h11 = cubed - squared
|
||||
points.append((
|
||||
h00 * float(start_point[0]) + h10 * parameter_span * float(start_tangent[0])
|
||||
+ h01 * float(end_point[0]) + h11 * parameter_span * float(end_tangent[0]),
|
||||
h00 * float(start_point[1]) + h10 * parameter_span * float(start_tangent[1])
|
||||
+ h01 * float(end_point[1]) + h11 * parameter_span * float(end_tangent[1]),
|
||||
))
|
||||
continue
|
||||
points.extend((float(point[0]), float(point[1])) for point in spline_points[1:-1])
|
||||
continue
|
||||
if edge.get("type") == "ellipse":
|
||||
center, axis = edge["center_mm"], edge["major_axis_mm"]
|
||||
@@ -439,7 +509,38 @@ def _gen_analytic_contours(profile: _Ctx, meta: _Ctx) -> tuple[list[_Ctx], list[
|
||||
return entities, []
|
||||
|
||||
|
||||
CORE_SHAPE_GENERATORS: dict[str, Any] = {"circle": _gen_circle, "polygon": _gen_polygon, "analytic_contours": _gen_analytic_contours}
|
||||
def _gen_planar_imprint(profile: _Ctx, meta: _Ctx) -> tuple[list[_Ctx], list[_Ctx]]:
|
||||
"""Prepare source curves for an exact OCC planar-arrangement split.
|
||||
|
||||
Unlike ``analytic_contours``, these curves are intentionally not joined
|
||||
into a guessed outer wire. FeatureScript's IMPRINT query identifies
|
||||
regions in the arrangement of all source curves, including open curves
|
||||
and split fragments, and the geometry adapter chooses those actual B-rep
|
||||
regions after the split.
|
||||
"""
|
||||
source_entities: list[_Ctx] = []
|
||||
for source in profile.get("source_entities") or []:
|
||||
source_id = str(source.get("id") or "")
|
||||
curve = source.get("curve") or {}
|
||||
if not source_id:
|
||||
raise ValueError("planar_imprint: source entity id is missing")
|
||||
edges = _imprint_segment_edges(curve)
|
||||
if not edges:
|
||||
raise ValueError(f"planar_imprint: source entity {source_id!r} has no curve")
|
||||
source_entities.append({"id": source_id, "edges": edges})
|
||||
if len(source_entities) < 2:
|
||||
raise ValueError("planar_imprint: at least two source entities are required")
|
||||
meta["_imprint_entities"] = source_entities
|
||||
meta["_imprint_selections"] = deepcopy(profile.get("selections") or [])
|
||||
return [], []
|
||||
|
||||
|
||||
CORE_SHAPE_GENERATORS: dict[str, Any] = {
|
||||
"circle": _gen_circle,
|
||||
"polygon": _gen_polygon,
|
||||
"analytic_contours": _gen_analytic_contours,
|
||||
"planar_imprint": _gen_planar_imprint,
|
||||
}
|
||||
SHAPE_GENERATORS = CORE_SHAPE_GENERATORS
|
||||
SHAPE_CAPABILITIES: dict[str, _Ctx] = {
|
||||
"circle": {"detectable": True, "arity": "circle", "description": "single circular contour"},
|
||||
@@ -463,7 +564,10 @@ def resolve_profile(sketch: _Ctx) -> _Ctx:
|
||||
generator = SHAPE_GENERATORS.get(profile.get("type"))
|
||||
if generator is None:
|
||||
raise ValueError(f"sketch {sketch.get('id')}: unsupported profile type {profile.get('type')!r}")
|
||||
meta: _Ctx = {"id": sketch.get("id"), "_entities": sketch.get("entities"), "_regions": [], "_has_open_contour": False}
|
||||
meta: _Ctx = {
|
||||
"id": sketch.get("id"), "_entities": sketch.get("entities"), "_regions": [], "_has_open_contour": False,
|
||||
"_imprint_entities": [], "_imprint_selections": [],
|
||||
}
|
||||
entities, contour = generator(profile, meta)
|
||||
output = deepcopy(sketch)
|
||||
original_circles = [entity for entity in sketch.get("entities") or [] if entity.get("type") == "circle" and not entity.get("construction")]
|
||||
@@ -480,6 +584,15 @@ def resolve_profile(sketch: _Ctx) -> _Ctx:
|
||||
]
|
||||
if meta["_has_open_contour"]:
|
||||
output["_open_contour"] = True
|
||||
if meta["_imprint_entities"]:
|
||||
output["imprint_entities_mm"] = [
|
||||
{
|
||||
"id": entity["id"],
|
||||
"edges": _transform_contours(entity["edges"], workplane) if workplane else entity["edges"],
|
||||
}
|
||||
for entity in meta["_imprint_entities"]
|
||||
]
|
||||
output["imprint_selections"] = meta["_imprint_selections"]
|
||||
return output
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user