重构 改为标准化输出
This commit is contained in:
@@ -12,6 +12,7 @@ from .rebuild import compare_with_gold, compile_cdsl_to_pack, run_cdsl_only, run
|
||||
from .semantic_validation import validate_semantic_cdsl
|
||||
from .sketch_solver import CORE_SHAPE_GENERATORS, resolve_all_sketches, resolve_required_sketches
|
||||
from .runtime import ALL_ATOMIC_IDS, EXECUTORS, RuntimeExecutionError, analyze_cdsl, rebuild_cdsl
|
||||
from .operation_contracts import materialized_feature_contracts
|
||||
|
||||
# The package-level runtime contract is the session executor registry. The
|
||||
# older llm_engine dispatcher remains available only for legacy engine packs.
|
||||
@@ -37,6 +38,7 @@ __all__ = [
|
||||
"rebuild_cdsl",
|
||||
"analyze_cdsl",
|
||||
"RuntimeExecutionError",
|
||||
"materialized_feature_contracts",
|
||||
]
|
||||
|
||||
__version__ = "1.0.0"
|
||||
|
||||
@@ -343,6 +343,10 @@ def batch_analyze(
|
||||
if not files:
|
||||
raise ValueError(f"No *.cdsl.json files found in {cdsl_dir}")
|
||||
if part_ids is not None:
|
||||
# ``part_ids`` are exact CDSL filename stems. This is intentionally
|
||||
# independent from document ``part_id``: imported provenance may use
|
||||
# different case or normalisation than its collision-free artifact
|
||||
# filename. Phase pools return these same stems.
|
||||
requested = {str(part_id).strip() for part_id in part_ids if str(part_id).strip()}
|
||||
available = {path.name.removesuffix(".cdsl.json") for path in files}
|
||||
unknown = sorted(requested - available)
|
||||
|
||||
@@ -451,6 +451,11 @@ class Build123dGeometryAdapter:
|
||||
|
||||
# 3. 导出面记录:含包围盒、中心、法向、面积、曲面类型与邻接签名;
|
||||
# 平面面额外写入规范化法向与平面偏移,便于后续按平面匹配。
|
||||
# 圆柱面还保存轴、半径和共享边关联的平面面。这使 verifier 能从
|
||||
# 实际 B-rep 证明孔是否连接两个方向相反的外部平面,而不是根据
|
||||
# author 传入的 blind-depth 文字猜测“贯穿”。
|
||||
face_edge_indexes: list[set[int]] = []
|
||||
face_geometries: list[dict[str, Any]] = []
|
||||
for index, face in enumerate(faces):
|
||||
bbox = face.bounding_box()
|
||||
center = face.center()
|
||||
@@ -472,10 +477,162 @@ class Build123dGeometryAdapter:
|
||||
)
|
||||
geometry["plane_normal"] = list(plane_normal)
|
||||
geometry["plane_offset_mm"] = plane_offset
|
||||
boundary_loops: list[list[list[float]]] = []
|
||||
for wire in face.wires():
|
||||
samples: list[list[float]] = []
|
||||
for edge in wire.edges():
|
||||
curve_type = str(edge.geom_type).split(".")[-1].lower()
|
||||
fractions = [step / 16 for step in range(16)] if curve_type in {"circle", "ellipse"} else [0.0]
|
||||
for fraction in fractions:
|
||||
point = edge.position_at(fraction)
|
||||
value = [float(point.X), float(point.Y), float(point.Z)]
|
||||
if not samples or sum((value[axis] - samples[-1][axis]) ** 2 for axis in range(3)) > 1e-12:
|
||||
samples.append(value)
|
||||
if len(samples) >= 3:
|
||||
boundary_loops.append(samples)
|
||||
if boundary_loops:
|
||||
geometry["boundary_loops_mm"] = boundary_loops
|
||||
elif geometry["surface_type"] in {"cylinder", "cone"}:
|
||||
axis = face.axis_of_rotation
|
||||
if axis is None:
|
||||
raise ValueError("rotational face is missing an axis")
|
||||
direction = axis.direction
|
||||
origin = axis.position
|
||||
geometry["axis_origin_mm"] = [origin.X, origin.Y, origin.Z]
|
||||
geometry["axis_direction"] = [direction.X, direction.Y, direction.Z]
|
||||
raw_cylinder_radius = face.radius if geometry["surface_type"] == "cylinder" else None
|
||||
if geometry["surface_type"] == "cone":
|
||||
boundary_radii: list[float] = []
|
||||
for edge in face.edges():
|
||||
if str(edge.geom_type).split(".")[-1].lower() != "circle":
|
||||
continue
|
||||
try:
|
||||
boundary_radii.append(float(edge.radius))
|
||||
except ValueError:
|
||||
continue
|
||||
geometry["boundary_radii_mm"] = sorted(boundary_radii)
|
||||
geometry["semi_angle_deg"] = float(face.semi_angle) if face.semi_angle is not None else None
|
||||
# ``through`` alone describes a cylinder spanning two opposed
|
||||
# planar faces. That applies to both a through bore and the
|
||||
# outside wall of a cylindrical extrusion. Classify the B-rep
|
||||
# face by its oriented normal so downstream acceptance claims
|
||||
# can prove holes without mistaking an exterior wall for one.
|
||||
unit_axis = (direction.X, direction.Y, direction.Z)
|
||||
radial = (center.X - origin.X, center.Y - origin.Y, center.Z - origin.Z)
|
||||
axial_projection = sum(radial[component] * unit_axis[component] for component in range(3))
|
||||
radial = tuple(radial[component] - axial_projection * unit_axis[component] for component in range(3))
|
||||
radial_length = sum(component * component for component in radial) ** 0.5
|
||||
if geometry["surface_type"] == "cylinder":
|
||||
# OCC can report a cylinder surface with ``radius=None``
|
||||
# after a non-planar-side Boolean cut. The face centre is
|
||||
# still on that cylinder, so its perpendicular distance to
|
||||
# the rotation axis is an equivalent measured radius. Do
|
||||
# not fail an otherwise valid build merely because that
|
||||
# optional OCC convenience property is absent.
|
||||
if isinstance(raw_cylinder_radius, (int, float)) and math.isfinite(float(raw_cylinder_radius)):
|
||||
geometry["radius_mm"] = float(raw_cylinder_radius)
|
||||
elif radial_length > 1e-9:
|
||||
geometry["radius_mm"] = radial_length
|
||||
if radial_length > 1e-9:
|
||||
normal_components = (normal.X, normal.Y, normal.Z)
|
||||
alignment = sum(float(normal_components[component]) * radial[component] for component in range(3)) / radial_length
|
||||
geometry["radial_normal_alignment"] = alignment
|
||||
geometry["cylinder_role"] = "outer" if alignment > 0.5 else "inner" if alignment < -0.5 else "unknown"
|
||||
else:
|
||||
geometry["cylinder_role"] = "unknown"
|
||||
face_edge_indexes.append(set(boundary_edge_indexes))
|
||||
face_geometries.append(geometry)
|
||||
records.append(TopologyRecord(
|
||||
record_id=f"{body_id}:face:{index}", kind="face", feature_id=feature_id, body_id=body_id, value=face,
|
||||
geometry=geometry,
|
||||
))
|
||||
plane_indexes = [index for index, geometry in enumerate(face_geometries) if geometry["surface_type"] == "plane"]
|
||||
|
||||
def directly_linked_planes(face_index: int) -> list[int]:
|
||||
return [
|
||||
plane_index
|
||||
for plane_index in plane_indexes
|
||||
if face_edge_indexes[face_index].intersection(face_edge_indexes[plane_index])
|
||||
]
|
||||
|
||||
def same_inner_rotational_channel(first: int, second: int) -> bool:
|
||||
"""Whether two inner rotational faces share one B-rep bore channel."""
|
||||
if not face_edge_indexes[first].intersection(face_edge_indexes[second]):
|
||||
return False
|
||||
left, right = face_geometries[first], face_geometries[second]
|
||||
if left.get("cylinder_role") != "inner" or right.get("cylinder_role") != "inner":
|
||||
return False
|
||||
left_axis, right_axis = left.get("axis_direction"), right.get("axis_direction")
|
||||
left_origin, right_origin = left.get("axis_origin_mm"), right.get("axis_origin_mm")
|
||||
if not all(isinstance(value, list) and len(value) == 3 for value in (left_axis, right_axis, left_origin, right_origin)):
|
||||
return False
|
||||
try:
|
||||
left_direction = tuple(float(value) for value in left_axis)
|
||||
right_direction = tuple(float(value) for value in right_axis)
|
||||
offset = tuple(float(left_origin[index]) - float(right_origin[index]) for index in range(3))
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
alignment = sum(left_direction[index] * right_direction[index] for index in range(3))
|
||||
if abs(alignment) < 1.0 - 1e-6:
|
||||
return False
|
||||
axial_offset = sum(offset[index] * left_direction[index] for index in range(3))
|
||||
radial_offset = tuple(offset[index] - axial_offset * left_direction[index] for index in range(3))
|
||||
return sum(value * value for value in radial_offset) ** 0.5 <= 1e-5
|
||||
|
||||
inner_rotational_indexes = [
|
||||
index
|
||||
for index, geometry in enumerate(face_geometries)
|
||||
if geometry["surface_type"] in {"cylinder", "cone"} and geometry.get("cylinder_role") == "inner"
|
||||
]
|
||||
|
||||
def channel_plane_indexes(start: int) -> list[int]:
|
||||
"""Collect endpoint planes through joined, co-axial inner faces.
|
||||
|
||||
A countersink or counterbore splits a physical bore into a cone and
|
||||
a cylinder. The cylinder has only one direct planar neighbour, so
|
||||
direct adjacency alone cannot prove that the complete channel exits
|
||||
the part. Traverse shared B-rep edges only across co-axial inner
|
||||
rotational faces, then inspect the channel's actual plane ends.
|
||||
"""
|
||||
pending = [start]
|
||||
visited: set[int] = set()
|
||||
endpoints: set[int] = set()
|
||||
while pending:
|
||||
index = pending.pop()
|
||||
if index in visited:
|
||||
continue
|
||||
visited.add(index)
|
||||
endpoints.update(directly_linked_planes(index))
|
||||
pending.extend(
|
||||
candidate
|
||||
for candidate in inner_rotational_indexes
|
||||
if candidate not in visited and same_inner_rotational_channel(index, candidate)
|
||||
)
|
||||
return sorted(endpoints)
|
||||
|
||||
def spans_opposed_planes(linked: list[int], axis: Any) -> bool:
|
||||
if not isinstance(axis, list) or len(axis) != 3:
|
||||
return False
|
||||
try:
|
||||
direction = tuple(float(value) for value in axis)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
return any(
|
||||
sum(float(face_geometries[first]["normal"][component]) * float(face_geometries[second]["normal"][component]) for component in range(3)) <= -0.99
|
||||
and all(abs(sum(float(face_geometries[position]["normal"][component]) * direction[component] for component in range(3))) >= 0.99 for position in (first, second))
|
||||
for first in linked
|
||||
for second in linked
|
||||
if first < second
|
||||
)
|
||||
|
||||
for index, geometry in enumerate(face_geometries):
|
||||
if geometry["surface_type"] != "cylinder":
|
||||
continue
|
||||
linked = directly_linked_planes(index)
|
||||
geometry["connected_plane_ids"] = [records[plane_index].record_id for plane_index in linked]
|
||||
channel_linked = channel_plane_indexes(index) if geometry.get("cylinder_role") == "inner" else linked
|
||||
geometry["channel_connected_plane_ids"] = [records[plane_index].record_id for plane_index in channel_linked]
|
||||
geometry["through"] = spans_opposed_planes(channel_linked, geometry.get("axis_direction"))
|
||||
# 4. 导出边记录:含包围盒、中心、长度、曲线类型与相邻面数;端点坐标可用时附加。
|
||||
for index, edge in enumerate(edges):
|
||||
bbox = edge.bounding_box()
|
||||
|
||||
@@ -13,6 +13,7 @@ from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
|
||||
from .runtime_types import CapabilityResult, FeaturePlanNode, HoleSpec, RuntimeDiagnostic
|
||||
from .operation_contracts import materialized_feature_contracts
|
||||
|
||||
|
||||
_SELECTOR_REQUIRED = frozenset({"fillet", "chamfer"})
|
||||
@@ -123,7 +124,7 @@ def pattern_transform_blocker(source: FeaturePlanNode) -> str | None:
|
||||
|
||||
def _schema_contract() -> dict[str, dict[str, Any]]:
|
||||
path = Path(__file__).with_name("profile_schema.json")
|
||||
return json.loads(path.read_text(encoding="utf-8"))["feature_atomic_ids"]
|
||||
return materialized_feature_contracts(json.loads(path.read_text(encoding="utf-8")))
|
||||
|
||||
|
||||
def sketch_ids_required_by_contract(cdsl: dict[str, Any]) -> frozenset[str]:
|
||||
|
||||
@@ -279,6 +279,25 @@
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"constructionBsplineSegment": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {"const": "bspline"},
|
||||
"degree": {"type": "integer", "minimum": 1},
|
||||
"control_points": {"type": "array", "minItems": 2, "items": {"$ref": "#/$defs/point2"}},
|
||||
"knots": {"type": "array", "minItems": 2, "items": {"type": "number"}},
|
||||
"periodic": {"type": "boolean"},
|
||||
"weights": {"type": "array", "minItems": 2, "items": {"type": "number", "exclusiveMinimum": 0}}
|
||||
},
|
||||
"required": ["type", "degree", "control_points", "knots", "periodic"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"constructionSegment": {
|
||||
"oneOf": [
|
||||
{"$ref": "#/$defs/analyticSegment"},
|
||||
{"$ref": "#/$defs/constructionBsplineSegment"}
|
||||
]
|
||||
},
|
||||
"analyticContour": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -294,7 +313,7 @@
|
||||
"properties": {
|
||||
"type": {"const": "analytic_contours"},
|
||||
"contours": {"type": "array", "items": {"$ref": "#/$defs/analyticContour"}},
|
||||
"construction": {"type": "array", "items": {"$ref": "#/$defs/analyticSegment"}}
|
||||
"construction": {"type": "array", "items": {"$ref": "#/$defs/constructionSegment"}}
|
||||
},
|
||||
"required": ["type", "contours"],
|
||||
"additionalProperties": false
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Derive engine-facing feature metadata from the v3 operation registry.
|
||||
|
||||
``profile_schema.json.operation_contracts`` is the only feature-operation
|
||||
registry. The engine needs a compact view while validating materialized CDSL,
|
||||
but that view is intentionally derived at load time instead of maintained as a
|
||||
second hand-written ``feature_atomic_ids`` table.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def materialized_feature_contracts(profile: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
||||
"""Return required engine metadata derived from each v3 operation contract.
|
||||
|
||||
Author-owned params are supplied by the tool schema. Server-injected
|
||||
``params.*`` paths become required only after materialization, while
|
||||
injected feature selectors are represented outside the params object.
|
||||
"""
|
||||
raw_contracts = profile.get("operation_contracts")
|
||||
if not isinstance(raw_contracts, dict) or not raw_contracts:
|
||||
raise ValueError("profile schema has no operation_contracts registry")
|
||||
derived: dict[str, dict[str, Any]] = {}
|
||||
for atomic_id, raw in raw_contracts.items():
|
||||
if not isinstance(atomic_id, str) or not atomic_id or not isinstance(raw, dict):
|
||||
raise ValueError("operation contract registry has an invalid entry")
|
||||
if raw.get("atomic_id") != atomic_id:
|
||||
raise ValueError(f"operation contract key and atomic_id disagree for {atomic_id}")
|
||||
shape = raw.get("fragment_shape")
|
||||
params_schema = raw.get("author_params_schema")
|
||||
injected_paths = raw.get("server_injected_paths")
|
||||
if not isinstance(shape, dict) or not isinstance(params_schema, dict) or not isinstance(injected_paths, list):
|
||||
raise ValueError(f"operation contract is incomplete for {atomic_id}")
|
||||
properties = params_schema.get("properties")
|
||||
required = params_schema.get("required")
|
||||
if not isinstance(properties, dict) or not isinstance(required, list):
|
||||
raise ValueError(f"operation author params are invalid for {atomic_id}")
|
||||
if not all(isinstance(name, str) and name in properties for name in required):
|
||||
raise ValueError(f"operation required params are invalid for {atomic_id}")
|
||||
materialized_required = list(dict.fromkeys(required))
|
||||
for path in injected_paths:
|
||||
if not isinstance(path, str):
|
||||
raise ValueError(f"operation injection path is invalid for {atomic_id}")
|
||||
if path.startswith("params.") and path.count(".") == 1:
|
||||
materialized_required.append(path.removeprefix("params."))
|
||||
elif path != "feature.selectors":
|
||||
raise ValueError(f"operation injection path is unsupported for {atomic_id}")
|
||||
materialized_required = list(dict.fromkeys(materialized_required))
|
||||
derived[atomic_id] = {
|
||||
"required_params": materialized_required,
|
||||
"optional_params": [name for name in properties if name not in materialized_required],
|
||||
"requires_sketch": shape.get("sketch") == "required",
|
||||
}
|
||||
return derived
|
||||
@@ -82,13 +82,19 @@ def is_static_phase_ready(cdsl: dict[str, Any], phase: str) -> bool:
|
||||
|
||||
|
||||
def select_static_phase_pool(cdsl_dir: Path, phase: str) -> list[str]:
|
||||
"""Return sorted part ids in one documented static phase input pool."""
|
||||
part_ids: list[str] = []
|
||||
"""Return sorted CDSL file-stem selectors for one static phase pool.
|
||||
|
||||
Batch rebuild selection is keyed by the immutable filename rather than the
|
||||
document's display ``part_id``. Imports retain the latter as source
|
||||
provenance and it may differ only by case or normalisation, which makes it
|
||||
unsuitable as a unique filesystem selector.
|
||||
"""
|
||||
selectors: list[str] = []
|
||||
for path in sorted(cdsl_dir.glob("*.cdsl.json")):
|
||||
document: dict[str, Any] = json.loads(path.read_text(encoding="utf-8"))
|
||||
if is_static_phase_ready(document, phase):
|
||||
part_ids.append(str(document.get("part_id") or path.name.removesuffix(".cdsl.json")))
|
||||
return part_ids
|
||||
selectors.append(path.name.removesuffix(".cdsl.json"))
|
||||
return selectors
|
||||
|
||||
|
||||
def is_p3_static_ready(cdsl: dict[str, Any]) -> bool:
|
||||
|
||||
@@ -1,28 +1,56 @@
|
||||
{
|
||||
"schema": "cdsl.engine.schema.v1",
|
||||
"schema_version": "1.3.0",
|
||||
"schema_version": "1.3.1",
|
||||
"cdsl_json_schema_file": "cdsl_schema.json",
|
||||
"maintenance_rule": "The CDSL-only runtime contract is limited to direct generic profiles and runtime.py EXECUTORS. Legacy macro profiles are importer compatibility syntax and must be lowered by cdsl_importer.legacy_profile_adapter before generic runtime validation.",
|
||||
"coordinate_convention": "All profile dimensions use millimetres. Two-dimensional points are [u, v] in the sketch workplane.",
|
||||
"runtime_supported_atomic_ids": ["extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind", "revolve_add", "revolve_cut", "hole_blind", "hole_countersink", "hole_counterbore", "sphere_add", "reference_plane", "reference_axis", "hole_wizard", "fillet", "chamfer", "pattern_linear", "pattern_mirror"],
|
||||
"runtime_supported_profiles": ["circle", "polygon", "analytic_contours"],
|
||||
"feature_atomic_ids": {
|
||||
"extrude_add_blind": {"summary": "Add the closed profile by one signed extrusion distance.", "required_params": ["distance_mm"], "optional_params": ["reverse"], "requires_sketch": true, "produces_body": true},
|
||||
"extrude_add_two_sided": {"summary": "Add the closed profile with independently captured forward and reverse terminations.", "required_params": ["distance_mm", "reverse_distance_mm"], "optional_params": ["reverse", "end_condition", "reverse_end_condition"], "requires_sketch": true, "produces_body": true},
|
||||
"extrude_cut_blind": {"summary": "Remove the closed profile by one signed extrusion distance.", "required_params": ["distance_mm"], "optional_params": ["reverse"], "requires_sketch": true},
|
||||
"revolve_add": {"summary": "Add the closed profile by revolving it around an axis.", "required_params": ["angle_deg", "axis"], "optional_params": ["reverse"], "requires_sketch": true, "produces_body": true},
|
||||
"revolve_cut": {"summary": "Remove the closed profile by revolving it around an axis.", "required_params": ["angle_deg", "axis"], "optional_params": ["reverse"], "requires_sketch": true},
|
||||
"hole_blind": {"summary": "Cut one or more blind cylindrical holes in the current body.", "required_params": ["diameter_mm", "depth_mm", "positions", "host_face"], "optional_params": ["drill_angle_rad"], "position_format": "positions is a non-empty array of objects: [{\"mm\":[u_mm,v_mm,w_mm]}]. A bare coordinate array is invalid. host_face is injected from exactly one face selector token.", "requires_sketch": false},
|
||||
"hole_countersink": {"summary": "Cut one or more blind holes with countersink dimensions.", "required_params": ["diameter_mm", "depth_mm", "positions", "countersink_diameter_mm", "countersink_angle_rad", "host_face"], "optional_params": ["drill_angle_rad"], "position_format": "positions is a non-empty array of objects: [{\"mm\":[u_mm,v_mm,w_mm]}]. A bare coordinate array is invalid. host_face is injected from exactly one face selector token.", "requires_sketch": false},
|
||||
"hole_counterbore": {"summary": "Cut one or more blind holes with counterbore dimensions.", "required_params": ["diameter_mm", "depth_mm", "positions", "counterbore_diameter_mm", "counterbore_depth_mm", "host_face"], "optional_params": ["drill_angle_rad"], "position_format": "positions is a non-empty array of objects: [{\"mm\":[u_mm,v_mm,w_mm]}]. A bare coordinate array is invalid. host_face is injected from exactly one face selector token.", "requires_sketch": false},
|
||||
"sphere_add": {"summary": "Add one spherical solid at an explicit model-space center.", "required_params": ["radius_mm", "center_mm"], "optional_params": [], "requires_sketch": false, "produces_body": true},
|
||||
"fillet": {"summary": "Apply a radius to selected edges or faces.", "required_params": ["radius_mm"], "optional_params": ["tangent_propagation"], "requires_sketch": false, "selector_slot": {"path": "feature.selectors", "min_items": 1, "max_items": 64}},
|
||||
"chamfer": {"summary": "Apply an equal-distance or angle-distance chamfer to selected edges or faces.", "required_params": ["distance_mm"], "optional_params": ["distance_2_mm", "angle_rad"], "requires_sketch": false, "selector_slot": {"path": "feature.selectors", "min_items": 1, "max_items": 64}},
|
||||
"pattern_linear": {"summary": "Repeat source features along one or two directions.", "required_params": ["source_feature_ids", "direction_1", "spacing_1_mm", "pattern_count_1"], "optional_params": ["direction_2", "spacing_2_mm", "pattern_count_2"], "requires_sketch": false},
|
||||
"pattern_mirror": {"summary": "Mirror source features about a selected plane.", "required_params": ["source_feature_ids", "mirror_plane"], "optional_params": [], "requires_sketch": false, "selector_slot": {"path": "params.mirror_plane", "min_items": 1, "max_items": 1}},
|
||||
"reference_plane": {"summary": "A named reference plane used by sketches or patterns.", "required_params": ["plane"], "optional_params": [], "requires_sketch": false},
|
||||
"reference_axis": {"summary": "A named reference axis used by revolve or pattern features.", "required_params": ["axis"], "optional_params": [], "requires_sketch": false},
|
||||
"hole_wizard": {"summary": "A SolidWorks Hole Wizard feature including its typed dimensional contract and placement selectors.", "required_params": ["hole_type", "diameter_mm", "depth_mm"], "optional_params": ["positions", "host_face", "thread", "countersink", "counterbore"], "requires_sketch": false, "selector_slot": {"path": "params.host_face", "min_items": 1, "max_items": 1}}
|
||||
"operation_contracts": {
|
||||
"extrude_add_blind": {"atomic_id":"extrude_add_blind","contract_version":"3.0","fragment_shape":{"sketch":"required","params":"required_object","selector_tokens":"forbidden"},"author_params_schema":{"type":"object","properties":{"distance_mm":{"type":"number","exclusiveMinimum":0},"reverse":{"type":"boolean"}},"required":["distance_mm"],"additionalProperties":false},"selector_policy":{"slot":null,"token_kind":null,"min_items":0,"max_items":0,"snapshot_bound":false},"server_injected_paths":[],"reference_policy":{"mode":"none"},"semantic_preflight":["sketch_workplane","profile_non_self_intersecting"],"candidate_verifiers":["single_connected_body"]},
|
||||
"extrude_add_two_sided": {"atomic_id":"extrude_add_two_sided","contract_version":"3.0","fragment_shape":{"sketch":"required","params":"required_object","selector_tokens":"forbidden"},"author_params_schema":{"type":"object","properties":{"distance_mm":{"type":"number","exclusiveMinimum":0},"reverse_distance_mm":{"type":"number","exclusiveMinimum":0},"reverse":{"type":"boolean"},"end_condition":{"type":"object","properties":{"type":{"type":"string","minLength":1,"maxLength":48},"solidworks_code":{"type":"integer"}},"required":["type","solidworks_code"],"additionalProperties":false},"reverse_end_condition":{"type":"object","properties":{"type":{"type":"string","minLength":1,"maxLength":48},"solidworks_code":{"type":"integer"}},"required":["type","solidworks_code"],"additionalProperties":false}},"required":["distance_mm","reverse_distance_mm"],"additionalProperties":false},"selector_policy":{"slot":null,"token_kind":null,"min_items":0,"max_items":0,"snapshot_bound":false},"server_injected_paths":[],"reference_policy":{"mode":"none"},"semantic_preflight":["sketch_workplane","profile_non_self_intersecting"],"candidate_verifiers":["single_connected_body"]},
|
||||
"extrude_cut_blind": {"atomic_id":"extrude_cut_blind","contract_version":"3.0","fragment_shape":{"sketch":"required","params":"required_object","selector_tokens":"forbidden"},"author_params_schema":{"type":"object","properties":{"distance_mm":{"type":"number","exclusiveMinimum":0},"reverse":{"type":"boolean"}},"required":["distance_mm"],"additionalProperties":false},"selector_policy":{"slot":null,"token_kind":null,"min_items":0,"max_items":0,"snapshot_bound":false},"server_injected_paths":[],"reference_policy":{"mode":"none"},"semantic_preflight":["requires_active_solid","sketch_workplane","profile_non_self_intersecting","cut_exit_distance"],"candidate_verifiers":["single_connected_body","volume_decreased"]},
|
||||
"revolve_add": {"atomic_id":"revolve_add","contract_version":"3.0","fragment_shape":{"sketch":"required","params":"required_object","selector_tokens":"forbidden"},"author_params_schema":{"type":"object","properties":{"angle_deg":{"type":"number","exclusiveMinimum":0,"maximum":360},"axis":{"type":"object","properties":{"origin_mm":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3},"direction":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3}},"required":["origin_mm","direction"],"additionalProperties":false},"reverse":{"type":"boolean"}},"required":["angle_deg","axis"],"additionalProperties":false},"selector_policy":{"slot":null,"token_kind":null,"min_items":0,"max_items":0,"snapshot_bound":false},"server_injected_paths":[],"reference_policy":{"mode":"none"},"semantic_preflight":["sketch_workplane","revolve_axis_on_sketch"],"candidate_verifiers":["single_connected_body"]},
|
||||
"revolve_cut": {"atomic_id":"revolve_cut","contract_version":"3.0","fragment_shape":{"sketch":"required","params":"required_object","selector_tokens":"forbidden"},"author_params_schema":{"type":"object","properties":{"angle_deg":{"type":"number","exclusiveMinimum":0,"maximum":360},"axis":{"type":"object","properties":{"origin_mm":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3},"direction":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3}},"required":["origin_mm","direction"],"additionalProperties":false},"reverse":{"type":"boolean"}},"required":["angle_deg","axis"],"additionalProperties":false},"selector_policy":{"slot":null,"token_kind":null,"min_items":0,"max_items":0,"snapshot_bound":false},"server_injected_paths":[],"reference_policy":{"mode":"none"},"semantic_preflight":["requires_active_solid","sketch_workplane","revolve_axis_on_sketch"],"candidate_verifiers":["single_connected_body","volume_decreased"]},
|
||||
"hole_blind": {"atomic_id":"hole_blind","contract_version":"3.0","fragment_shape":{"sketch":"forbidden","params":"required_object","selector_tokens":"required"},"author_params_schema":{"type":"object","properties":{"diameter_mm":{"type":"number","exclusiveMinimum":0},"depth_mm":{"type":"number","exclusiveMinimum":0},"positions":{"type":"array","minItems":1,"maxItems":64,"items":{"type":"object","properties":{"mm":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3}},"required":["mm"],"additionalProperties":false}},"drill_angle_rad":{"type":"number","exclusiveMinimum":0,"maximum":3.141592653589793}},"required":["diameter_mm","depth_mm","positions"],"additionalProperties":false},"selector_policy":{"slot":"params.host_face","token_kind":"face","min_items":1,"max_items":1,"snapshot_bound":true},"server_injected_paths":["params.host_face"],"reference_policy":{"mode":"none"},"semantic_preflight":["host_face_exists","hole_positions_on_host_plane","cut_exit_distance"],"candidate_verifiers":["cylindrical_bore","through_cylindrical_bore"]},
|
||||
"hole_countersink": {"atomic_id":"hole_countersink","contract_version":"3.0","fragment_shape":{"sketch":"forbidden","params":"required_object","selector_tokens":"required"},"author_params_schema":{"type":"object","properties":{"diameter_mm":{"type":"number","exclusiveMinimum":0},"depth_mm":{"type":"number","exclusiveMinimum":0},"positions":{"type":"array","minItems":1,"maxItems":64,"items":{"type":"object","properties":{"mm":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3}},"required":["mm"],"additionalProperties":false}},"countersink_diameter_mm":{"type":"number","exclusiveMinimum":0},"countersink_angle_rad":{"type":"number","exclusiveMinimum":0,"maximum":3.141592653589793},"drill_angle_rad":{"type":"number","exclusiveMinimum":0,"maximum":3.141592653589793}},"required":["diameter_mm","depth_mm","positions","countersink_diameter_mm","countersink_angle_rad"],"additionalProperties":false},"selector_policy":{"slot":"params.host_face","token_kind":"face","min_items":1,"max_items":1,"snapshot_bound":true},"server_injected_paths":["params.host_face"],"reference_policy":{"mode":"none"},"semantic_preflight":["host_face_exists","hole_positions_on_host_plane","cut_exit_distance"],"candidate_verifiers":["cylindrical_bore"]},
|
||||
"hole_counterbore": {"atomic_id":"hole_counterbore","contract_version":"3.0","fragment_shape":{"sketch":"forbidden","params":"required_object","selector_tokens":"required"},"author_params_schema":{"type":"object","properties":{"diameter_mm":{"type":"number","exclusiveMinimum":0},"depth_mm":{"type":"number","exclusiveMinimum":0},"positions":{"type":"array","minItems":1,"maxItems":64,"items":{"type":"object","properties":{"mm":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3}},"required":["mm"],"additionalProperties":false}},"counterbore_diameter_mm":{"type":"number","exclusiveMinimum":0},"counterbore_depth_mm":{"type":"number","exclusiveMinimum":0},"drill_angle_rad":{"type":"number","exclusiveMinimum":0,"maximum":3.141592653589793}},"required":["diameter_mm","depth_mm","positions","counterbore_diameter_mm","counterbore_depth_mm"],"additionalProperties":false},"selector_policy":{"slot":"params.host_face","token_kind":"face","min_items":1,"max_items":1,"snapshot_bound":true},"server_injected_paths":["params.host_face"],"reference_policy":{"mode":"none"},"semantic_preflight":["host_face_exists","hole_positions_on_host_plane","cut_exit_distance"],"candidate_verifiers":["cylindrical_bore"]},
|
||||
"sphere_add": {"atomic_id":"sphere_add","contract_version":"3.0","fragment_shape":{"sketch":"forbidden","params":"required_object","selector_tokens":"forbidden"},"author_params_schema":{"type":"object","properties":{"radius_mm":{"type":"number","exclusiveMinimum":0},"center_mm":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3}},"required":["radius_mm","center_mm"],"additionalProperties":false},"selector_policy":{"slot":null,"token_kind":null,"min_items":0,"max_items":0,"snapshot_bound":false},"server_injected_paths":[],"reference_policy":{"mode":"none"},"semantic_preflight":[],"candidate_verifiers":["single_connected_body"]},
|
||||
"reference_plane": {"atomic_id":"reference_plane","contract_version":"3.0","fragment_shape":{"sketch":"forbidden","params":"required_object","selector_tokens":"forbidden"},"author_params_schema":{"type":"object","properties":{"plane":{"type":"object","properties":{"origin_mm":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3},"x_dir":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3},"normal":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3}},"required":["origin_mm","x_dir","normal"],"additionalProperties":false}},"required":["plane"],"additionalProperties":false},"selector_policy":{"slot":null,"token_kind":null,"min_items":0,"max_items":0,"snapshot_bound":false},"server_injected_paths":[],"reference_policy":{"mode":"none"},"semantic_preflight":["requires_active_solid","reference_plane_nonzero_normal"],"candidate_verifiers":[]},
|
||||
"reference_axis": {"atomic_id":"reference_axis","contract_version":"3.0","fragment_shape":{"sketch":"forbidden","params":"required_object","selector_tokens":"forbidden"},"author_params_schema":{"type":"object","properties":{"axis":{"type":"object","properties":{"origin_mm":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3},"direction":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3}},"required":["origin_mm","direction"],"additionalProperties":false}},"required":["axis"],"additionalProperties":false},"selector_policy":{"slot":null,"token_kind":null,"min_items":0,"max_items":0,"snapshot_bound":false},"server_injected_paths":[],"reference_policy":{"mode":"none"},"semantic_preflight":["requires_active_solid","reference_axis_nonzero_direction"],"candidate_verifiers":[]},
|
||||
"hole_wizard": {
|
||||
"atomic_id": "hole_wizard",
|
||||
"contract_version": "3.0",
|
||||
"fragment_shape": {"sketch": "forbidden", "params": "required_object", "selector_tokens": "required"},
|
||||
"author_params_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"hole_type": {"enum": ["simple", "threaded", "countersink", "counterbore"]},
|
||||
"diameter_mm": {"type": "number", "exclusiveMinimum": 0},
|
||||
"depth_mm": {"type": "number", "exclusiveMinimum": 0},
|
||||
"end_condition": {"type": "object", "properties": {"type": {"enum": ["blind", "through_all", "through_all_both"]}, "solidworks_code": {"type": "integer"}}, "required": ["type", "solidworks_code"], "additionalProperties": false},
|
||||
"positions": {"type": "array", "minItems": 1, "maxItems": 64, "items": {"type": "object", "properties": {"mm": {"type": "array", "items": {"type": "number"}, "minItems": 3, "maxItems": 3}}, "required": ["mm"], "additionalProperties": false}},
|
||||
"thread": {"type": "object", "properties": {"designation": {"type": "string", "minLength": 1, "maxLength": 80}, "pitch_mm": {"type": "number", "exclusiveMinimum": 0}}, "required": ["designation", "pitch_mm"], "additionalProperties": false},
|
||||
"countersink": {"type": "object", "properties": {"diameter_mm": {"type": "number", "exclusiveMinimum": 0}, "angle_rad": {"type": "number", "exclusiveMinimum": 0, "maximum": 3.141592653589793}}, "required": ["diameter_mm", "angle_rad"], "additionalProperties": false},
|
||||
"counterbore": {"type": "object", "properties": {"diameter_mm": {"type": "number", "exclusiveMinimum": 0}, "depth_mm": {"type": "number", "exclusiveMinimum": 0}}, "required": ["diameter_mm", "depth_mm"], "additionalProperties": false}
|
||||
},
|
||||
"required": ["hole_type", "diameter_mm", "depth_mm", "end_condition", "positions"],
|
||||
"allOf": [
|
||||
{"if": {"properties": {"hole_type": {"const": "threaded"}}, "required": ["hole_type"]}, "then": {"required": ["thread"]}},
|
||||
{"if": {"properties": {"hole_type": {"const": "countersink"}}, "required": ["hole_type"]}, "then": {"required": ["countersink"]}},
|
||||
{"if": {"properties": {"hole_type": {"const": "counterbore"}}, "required": ["hole_type"]}, "then": {"required": ["counterbore"]}}
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"selector_policy": {"slot": "params.host_face", "token_kind": "face", "min_items": 1, "max_items": 1, "snapshot_bound": true},
|
||||
"server_injected_paths": ["params.host_face"],
|
||||
"reference_policy": {"mode": "none"},
|
||||
"semantic_preflight": ["host_face_exists", "hole_positions_on_host_plane"],
|
||||
"candidate_verifiers": ["cylindrical_bore"]
|
||||
},
|
||||
"fillet": {"atomic_id":"fillet","contract_version":"3.0","fragment_shape":{"sketch":"forbidden","params":"required_object","selector_tokens":"required"},"author_params_schema":{"type":"object","properties":{"radius_mm":{"type":"number","exclusiveMinimum":0},"tangent_propagation":{"type":"boolean"}},"required":["radius_mm"],"additionalProperties":false},"selector_policy":{"slot":"feature.selectors","token_kind":"edge","min_items":1,"max_items":64,"snapshot_bound":true},"server_injected_paths":["feature.selectors"],"reference_policy":{"mode":"none"},"semantic_preflight":["selected_edges_exist"],"candidate_verifiers":["single_connected_body"]},
|
||||
"chamfer": {"atomic_id":"chamfer","contract_version":"3.0","fragment_shape":{"sketch":"forbidden","params":"required_object","selector_tokens":"required"},"author_params_schema":{"type":"object","properties":{"distance_mm":{"type":"number","exclusiveMinimum":0},"distance_2_mm":{"type":"number","exclusiveMinimum":0},"angle_rad":{"type":"number","exclusiveMinimum":0,"maximum":3.141592653589793}},"required":["distance_mm"],"additionalProperties":false},"selector_policy":{"slot":"feature.selectors","token_kind":"edge","min_items":1,"max_items":64,"snapshot_bound":true},"server_injected_paths":["feature.selectors"],"reference_policy":{"mode":"none"},"semantic_preflight":["selected_edges_exist"],"candidate_verifiers":["single_connected_body"]},
|
||||
"pattern_linear": {"atomic_id":"pattern_linear","contract_version":"3.0","fragment_shape":{"sketch":"forbidden","params":"required_object","selector_tokens":"forbidden"},"author_params_schema":{"type":"object","properties":{"source_feature_ids":{"type":"array","items":{"type":"string","pattern":"^[a-z][a-z0-9_:-]{0,95}$"},"minItems":1,"maxItems":16,"uniqueItems":true},"direction_1":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3},"spacing_1_mm":{"type":"number","exclusiveMinimum":0},"pattern_count_1":{"type":"integer","minimum":1,"maximum":128},"direction_2":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3},"spacing_2_mm":{"type":"number","exclusiveMinimum":0},"pattern_count_2":{"type":"integer","minimum":1,"maximum":128}},"required":["source_feature_ids","direction_1","spacing_1_mm","pattern_count_1"],"additionalProperties":false},"selector_policy":{"slot":null,"token_kind":null,"min_items":0,"max_items":0,"snapshot_bound":false},"server_injected_paths":[],"reference_policy":{"mode":"snapshot_bound","slot":"params.source_feature_ids","token_kind":"feature","min_items":1,"max_items":16,"snapshot_bound":true},"semantic_preflight":["source_features_exist"],"candidate_verifiers":[]},
|
||||
"pattern_mirror": {"atomic_id":"pattern_mirror","contract_version":"3.0","fragment_shape":{"sketch":"forbidden","params":"required_object","selector_tokens":"required"},"author_params_schema":{"type":"object","properties":{"source_feature_ids":{"type":"array","items":{"type":"string","pattern":"^[a-z][a-z0-9_:-]{0,95}$"},"minItems":1,"maxItems":16,"uniqueItems":true}},"required":["source_feature_ids"],"additionalProperties":false},"selector_policy":{"slot":"params.mirror_plane","token_kind":"plane","min_items":1,"max_items":1,"snapshot_bound":true},"server_injected_paths":["params.mirror_plane"],"reference_policy":{"mode":"snapshot_bound","slot":"params.source_feature_ids","token_kind":"feature","min_items":1,"max_items":16,"snapshot_bound":true},"semantic_preflight":["source_features_exist","mirror_plane_exists"],"candidate_verifiers":[]}
|
||||
},
|
||||
"profiles": {
|
||||
"circle": {
|
||||
@@ -37,7 +65,7 @@
|
||||
"constraints": ["vertices contains at least three [u, v] points"]
|
||||
},
|
||||
"analytic_contours": {
|
||||
"summary": "Closed direct line, arc and circle contours. B-splines are unsupported."
|
||||
"summary": "Closed executable line, arc and circle contours. B-splines are unsupported in contours; imported construction B-splines are retained as non-executable audit geometry."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user