Files
pythonocc-step-editor/scripts/verify_hole_slot_isolated_edit.py
T

1107 lines
47 KiB
Python

from __future__ import annotations
import json
from pathlib import Path
import subprocess
import sys
import tempfile
PROJECT_ROOT = Path(__file__).resolve().parent.parent
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
SCRIPTS_ROOT = PROJECT_ROOT / "scripts"
if str(SCRIPTS_ROOT) not in sys.path:
sys.path.insert(0, str(SCRIPTS_ROOT))
from step_editor.model import StepModel
from verify_hole_resize import (
_axis_center,
_depth,
_first_hole_face,
_hole_face_ids,
_nearest_blind_depth,
_nearest_hole_by_diameter,
_volume,
_write_blind_hole_model,
_write_through_hole_model,
)
from verify_slot_resize import (
_first_slot_face,
_nearest_axis_center,
_nearest_metric,
_nearest_obround_axis_pair,
_nearest_obround_total_length,
_slot_axis_center,
_slot_metric,
_write_half_round_slot_model,
_write_obround_slot_model,
)
def _run_worker(request_path: Path) -> dict[str, object]:
completed = subprocess.run(
[sys.executable, "-m", "step_editor.isolated_edit_worker", str(request_path)],
cwd=PROJECT_ROOT,
text=True,
capture_output=True,
check=False,
)
response_path = request_path.with_suffix(".response.json")
if completed.returncode != 0:
stderr = completed.stderr.strip()
response = response_path.read_text(encoding="utf-8-sig") if response_path.exists() else ""
raise SystemExit(
f"isolated worker failed: returncode={completed.returncode}, stderr={stderr}, response={response}"
)
if not response_path.exists():
raise SystemExit("isolated worker did not write a response JSON file")
response = json.loads(response_path.read_text(encoding="utf-8-sig"))
if not response.get("ok"):
raise SystemExit(f"isolated worker returned an error response: {response}")
return response
def _run_worker_allow_failure(request_path: Path) -> tuple[int, dict[str, object]]:
completed = subprocess.run(
[sys.executable, "-m", "step_editor.isolated_edit_worker", str(request_path)],
cwd=PROJECT_ROOT,
text=True,
capture_output=True,
check=False,
)
response_path = request_path.with_suffix(".response.json")
if not response_path.exists():
raise SystemExit(
f"isolated worker did not write a response JSON file; "
f"returncode={completed.returncode}, stderr={completed.stderr.strip()}"
)
response = json.loads(response_path.read_text(encoding="utf-8-sig"))
return completed.returncode, response
def _write_request(
request_path: Path,
input_path: Path,
output_path: Path,
operation: str,
args: list[object],
) -> None:
request_path.write_text(
json.dumps(
{
"input_path": str(input_path),
"output_path": str(output_path),
"operation": operation,
"args": args,
},
ensure_ascii=False,
indent=2,
),
encoding="utf-8",
)
def _assert_one_solid(model: StepModel, label: str) -> None:
if model.stats().solids != 1:
raise SystemExit(f"{label} should keep one Solid: {model.stats()}")
def _logical_region_has_diameter(model: StepModel, logical_id: int, target_diameter: float) -> bool:
tolerance = max(abs(target_diameter) * 1e-4, 1e-5)
for face_id in model.face_ids_for_logical_id(logical_id):
try:
diameter = float(model.face_info(face_id).get("diameter"))
except (TypeError, ValueError):
continue
if abs(diameter - target_diameter) <= tolerance:
return True
return False
def _logical_region_has_slot_metric(model: StepModel, logical_id: int, key: str, target_value: float) -> bool:
tolerance = max(abs(target_value) * 1e-4, 1e-5)
for face_id in model.face_ids_for_logical_id(logical_id):
try:
feature = model.feature_info(face_id)
info = model.face_info(face_id)
value = feature.get(key)
if value is None:
value = info.get(key)
value = float(value)
except BaseException:
continue
if abs(value - target_value) <= tolerance:
return True
return False
def _logical_region_has_blind_depth(model: StepModel, logical_id: int, target_depth: float) -> bool:
tolerance = max(abs(target_depth) * 1e-4, 1e-5)
for face_id in model.face_ids_for_logical_id(logical_id):
try:
depth = _depth(model, face_id)
except BaseException:
continue
if abs(depth - target_depth) <= tolerance:
return True
return False
def _logical_region_has_hole_axis(
model: StepModel,
logical_id: int,
target_center: tuple[float, float, float],
target_diameter: float,
) -> bool:
for face_id in model.face_ids_for_logical_id(logical_id):
try:
center = _axis_center(model, face_id)
diameter = float(model.face_info(face_id).get("diameter"))
except BaseException:
continue
center_error = sum((center[index] - target_center[index]) ** 2 for index in range(3)) ** 0.5
diameter_error = abs(diameter - target_diameter)
if center_error <= 1e-4 and diameter_error <= max(abs(target_diameter) * 1e-4, 1e-5):
return True
return False
def _logical_region_has_slot_axis(
model: StepModel,
logical_id: int,
target_center: tuple[float, float, float],
) -> bool:
for face_id in model.face_ids_for_logical_id(logical_id):
try:
center = _slot_axis_center(model, face_id)
except BaseException:
continue
center_error = sum((center[index] - target_center[index]) ** 2 for index in range(3)) ** 0.5
if center_error <= 1e-4:
return True
return False
def _logical_region_contains_any(model: StepModel, logical_id: int, face_ids: tuple[int, ...]) -> bool:
logical_faces = set(model.face_ids_for_logical_id(logical_id))
return any(face_id in logical_faces for face_id in face_ids)
def _window_probe_for(input_path: Path):
from step_editor.window_actions import WindowActionMixin
class _WindowProbe(WindowActionMixin):
def __init__(self) -> None:
self.model = StepModel.load(input_path)
self.step_path = input_path
self.current_info_values = {}
self.selected_pick_position = None
self.isolated_edit_cancel_requested = False
self.active_isolated_edit_process = None
return _WindowProbe()
def _verify_hole_diameter_isolated(temp_dir: Path) -> None:
input_path = temp_dir / "through_hole.step"
output_path = temp_dir / "through_hole_out.step"
request_path = temp_dir / "hole_request.json"
_write_through_hole_model(input_path)
model = StepModel.load(input_path)
face_id = _first_hole_face(model, blind=False)
target_center = _axis_center(model, face_id)
target_diameter = 8.0
_write_request(request_path, input_path, output_path, "resize_cylindrical_hole", [face_id, target_diameter])
response = _run_worker(request_path)
result_model = StepModel.load(output_path)
verified_face, diameter, center, error = _nearest_hole_by_diameter(
result_model,
target_diameter,
target_center,
blind=False,
)
if error > 1e-4 or abs(diameter - target_diameter) > 1e-4:
raise SystemExit(
f"isolated hole resize verification failed: face={verified_face}, "
f"diameter={diameter:g}, center={center}, error={error:g}, response={response}"
)
_assert_one_solid(result_model, "isolated hole resize")
print(f"isolated hole resize ok: face={verified_face}, diameter={diameter:g}")
def _verify_hole_owning_scale_isolated(temp_dir: Path) -> None:
input_path = temp_dir / "through_hole_scale.step"
output_path = temp_dir / "through_hole_scale_out.step"
request_path = temp_dir / "hole_scale_request.json"
_write_through_hole_model(input_path)
model = StepModel.load(input_path)
face_id = _first_hole_face(model, blind=False)
target_center = _axis_center(model, face_id)
target_diameter = 8.0
_write_request(request_path, input_path, output_path, "resize_cylindrical_owning_scale", [face_id, target_diameter])
response = _run_worker(request_path)
result_model = StepModel.load(output_path)
verified_face, diameter, center, error = _nearest_hole_by_diameter(
result_model,
target_diameter,
target_center,
blind=False,
)
if error > 1e-4 or abs(diameter - target_diameter) > 1e-4:
raise SystemExit(
f"isolated hole owning-scale verification failed: face={verified_face}, "
f"diameter={diameter:g}, center={center}, error={error:g}, response={response}"
)
_assert_one_solid(result_model, "isolated hole owning-scale")
print(f"isolated hole owning-scale ok: face={verified_face}, diameter={diameter:g}")
def _verify_hole_axis_isolated(temp_dir: Path) -> None:
input_path = temp_dir / "through_hole_axis.step"
output_path = temp_dir / "through_hole_axis_out.step"
request_path = temp_dir / "hole_axis_request.json"
_write_through_hole_model(input_path)
model = StepModel.load(input_path)
face_id = _first_hole_face(model, blind=False)
current_center = _axis_center(model, face_id)
target_center = (current_center[0] + 2.0, current_center[1], current_center[2])
current_diameter = float(model.face_info(face_id)["diameter"])
_write_request(request_path, input_path, output_path, "move_cylindrical_hole_axis", [face_id, target_center])
response = _run_worker(request_path)
result_model = StepModel.load(output_path)
verified_face, diameter, center, error = _nearest_hole_by_diameter(
result_model,
current_diameter,
target_center,
blind=False,
)
if error > 1e-4 or abs(diameter - current_diameter) > 1e-4:
raise SystemExit(
f"isolated hole axis move verification failed: face={verified_face}, "
f"diameter={diameter:g}, center={center}, error={error:g}, response={response}"
)
_assert_one_solid(result_model, "isolated hole axis move")
print(f"isolated hole axis move ok: face={verified_face}, center={center}")
def _verify_hole_suppress_isolated(temp_dir: Path) -> None:
input_path = temp_dir / "through_hole_suppress.step"
output_path = temp_dir / "through_hole_suppress_out.step"
request_path = temp_dir / "hole_suppress_request.json"
_write_through_hole_model(input_path)
model = StepModel.load(input_path)
face_id = _first_hole_face(model, blind=False)
before_volume = _volume(model)
_write_request(request_path, input_path, output_path, "suppress_cylindrical_hole", [face_id])
response = _run_worker(request_path)
result_model = StepModel.load(output_path)
after_volume = _volume(result_model)
remaining_holes = _hole_face_ids(result_model)
full_box_volume = 30.0 * 20.0 * 8.0
if remaining_holes:
raise SystemExit(f"isolated hole suppress left hole faces: {remaining_holes}, response={response}")
if abs(after_volume - full_box_volume) > 1e-3:
raise SystemExit(
f"isolated hole suppress volume failed: before={before_volume:g}, "
f"after={after_volume:g}, target={full_box_volume:g}, response={response}"
)
_assert_one_solid(result_model, "isolated hole suppress")
print(f"isolated hole suppress ok: volume={after_volume:g}")
def _verify_blind_depth_isolated(temp_dir: Path) -> None:
input_path = temp_dir / "blind_hole.step"
output_path = temp_dir / "blind_hole_out.step"
request_path = temp_dir / "blind_depth_request.json"
_write_blind_hole_model(input_path)
model = StepModel.load(input_path)
face_id = _first_hole_face(model, blind=True)
feature = model.feature_info(face_id)
bottom_face_ids = tuple(feature.get("feature_bottom_face_ids", ()))
bottom_face_id = int(bottom_face_ids[0]) if bottom_face_ids else None
target_depth = 8.0
_write_request(request_path, input_path, output_path, "resize_cylindrical_depth", [face_id, target_depth, bottom_face_id])
response = _run_worker(request_path)
result_model = StepModel.load(output_path)
verified_face, depth, error = _nearest_blind_depth(result_model, target_depth)
if error > 1e-4:
raise SystemExit(
f"isolated blind depth verification failed: face={verified_face}, "
f"depth={depth:g}, error={error:g}, response={response}"
)
_assert_one_solid(result_model, "isolated blind depth")
print(f"isolated blind depth ok: face={verified_face}, depth={depth:g}")
def _verify_blind_depth_owning_scale_isolated(temp_dir: Path) -> None:
input_path = temp_dir / "blind_hole_scale.step"
output_path = temp_dir / "blind_hole_scale_out.step"
request_path = temp_dir / "blind_depth_scale_request.json"
_write_blind_hole_model(input_path)
model = StepModel.load(input_path)
face_id = _first_hole_face(model, blind=True)
feature = model.feature_info(face_id)
bottom_face_ids = tuple(feature.get("feature_bottom_face_ids", ()))
bottom_face_id = int(bottom_face_ids[0]) if bottom_face_ids else None
target_depth = 8.0
_write_request(
request_path,
input_path,
output_path,
"resize_cylindrical_depth_owning_scale",
[face_id, target_depth, bottom_face_id],
)
returncode, response = _run_worker_allow_failure(request_path)
if returncode != 0:
error = str(response.get("error", ""))
if "rolled back" not in error and "回滚" not in error:
raise SystemExit(f"isolated blind depth owning-scale failed without rollback message: {response}")
print("isolated blind depth owning-scale guarded ok: result rejected and rolled back")
return
if not response.get("ok"):
raise SystemExit(f"isolated blind depth owning-scale returned an error response: {response}")
result_model = StepModel.load(output_path)
verified_face, depth, error = _nearest_blind_depth(result_model, target_depth)
if error > 1e-4:
raise SystemExit(
f"isolated blind depth owning-scale verification failed: face={verified_face}, "
f"depth={depth:g}, error={error:g}, response={response}"
)
_assert_one_solid(result_model, "isolated blind depth owning-scale")
print(f"isolated blind depth owning-scale ok: face={verified_face}, depth={depth:g}")
def _verify_slot_width_isolated(temp_dir: Path) -> None:
input_path = temp_dir / "half_round_slot.step"
output_path = temp_dir / "half_round_slot_out.step"
request_path = temp_dir / "slot_request.json"
_write_half_round_slot_model(input_path)
model = StepModel.load(input_path)
face_id = _first_slot_face(model)
target_width = 8.0
_write_request(request_path, input_path, output_path, "resize_cylindrical_slot_width", [face_id, target_width, None])
response = _run_worker(request_path)
result_model = StepModel.load(output_path)
verified_face, width, error = _nearest_metric(result_model, "slot_chord_width_estimate", target_width)
if error > 1e-4:
raise SystemExit(
f"isolated slot width verification failed: face={verified_face}, "
f"width={width:g}, error={error:g}, response={response}"
)
_assert_one_solid(result_model, "isolated slot width resize")
print(f"isolated slot width resize ok: face={verified_face}, width={width:g}")
def _verify_slot_owning_scale_isolated(temp_dir: Path) -> None:
input_path = temp_dir / "half_round_slot_scale.step"
output_path = temp_dir / "half_round_slot_scale_out.step"
request_path = temp_dir / "slot_scale_request.json"
_write_half_round_slot_model(input_path)
model = StepModel.load(input_path)
face_id = _first_slot_face(model)
target_diameter = 8.0
target_width = 8.0
_write_request(request_path, input_path, output_path, "resize_cylindrical_owning_scale", [face_id, target_diameter])
response = _run_worker(request_path)
result_model = StepModel.load(output_path)
verified_face, width, error = _nearest_metric(result_model, "slot_chord_width_estimate", target_width)
if error > 1e-4:
raise SystemExit(
f"isolated slot owning-scale verification failed: face={verified_face}, "
f"width={width:g}, error={error:g}, response={response}"
)
_assert_one_solid(result_model, "isolated slot owning-scale")
print(f"isolated slot owning-scale ok: face={verified_face}, width={width:g}")
def _verify_slot_metric_isolated(
temp_dir: Path,
stem: str,
operation: str,
target_value: float,
metric_key: str,
) -> None:
input_path = temp_dir / f"{stem}.step"
output_path = temp_dir / f"{stem}_out.step"
request_path = temp_dir / f"{stem}_request.json"
_write_half_round_slot_model(input_path)
model = StepModel.load(input_path)
face_id = _first_slot_face(model)
args: list[object] = [face_id, target_value]
if operation in {
"resize_cylindrical_slot_width",
"resize_cylindrical_slot_depth",
"resize_cylindrical_slot_arc_length",
}:
args.append(None)
_write_request(request_path, input_path, output_path, operation, args)
response = _run_worker(request_path)
result_model = StepModel.load(output_path)
verified_face, value, error = _nearest_metric(result_model, metric_key, target_value)
if error > 1e-4:
raise SystemExit(
f"isolated {stem} verification failed: face={verified_face}, "
f"value={value:g}, error={error:g}, response={response}"
)
_assert_one_solid(result_model, f"isolated {stem}")
print(f"isolated {stem} ok: face={verified_face}, value={value:g}")
def _verify_slot_axis_isolated(temp_dir: Path) -> None:
input_path = temp_dir / "half_round_slot_axis.step"
output_path = temp_dir / "half_round_slot_axis_out.step"
request_path = temp_dir / "slot_axis_request.json"
_write_half_round_slot_model(input_path)
model = StepModel.load(input_path)
face_id = _first_slot_face(model)
current_center = _slot_axis_center(model, face_id)
target_center = (current_center[0], current_center[1] + 1.0, current_center[2])
_write_request(request_path, input_path, output_path, "move_cylindrical_slot_axis", [face_id, target_center])
response = _run_worker(request_path)
result_model = StepModel.load(output_path)
verified_face, center, error = _nearest_axis_center(result_model, target_center)
if error > 1e-4:
raise SystemExit(
f"isolated slot axis move verification failed: face={verified_face}, "
f"center={center}, error={error:g}, response={response}"
)
_assert_one_solid(result_model, "isolated slot axis move")
print(f"isolated slot axis move ok: face={verified_face}, center={center}")
def _verify_obround_slot_total_length_isolated(temp_dir: Path) -> None:
input_path = temp_dir / "obround_total_length.step"
output_path = temp_dir / "obround_total_length_out.step"
request_path = temp_dir / "obround_total_length_request.json"
_write_obround_slot_model(input_path)
model = StepModel.load(input_path)
face_id, pair_face_id, _current_total_length, _current_center_distance, _ = _nearest_obround_total_length(model, 16.0)
target_total_length = 20.0
_write_request(
request_path,
input_path,
output_path,
"resize_cylindrical_slot_total_length",
[face_id, target_total_length, pair_face_id],
)
response = _run_worker(request_path)
result_model = StepModel.load(output_path)
verified_face, verified_pair, total_length, center_distance, error = _nearest_obround_total_length(
result_model,
target_total_length,
)
if error > 1e-4:
raise SystemExit(
f"isolated obround total length verification failed: faces=({verified_face}, {verified_pair}), "
f"total_length={total_length:g}, center_distance={center_distance:g}, error={error:g}, response={response}"
)
_assert_one_solid(result_model, "isolated obround total length")
print(f"isolated obround total length ok: faces=({verified_face}, {verified_pair}), total_length={total_length:g}")
def _verify_obround_slot_center_distance_isolated(temp_dir: Path) -> None:
input_path = temp_dir / "obround_center_distance.step"
output_path = temp_dir / "obround_center_distance_out.step"
request_path = temp_dir / "obround_center_distance_request.json"
_write_obround_slot_model(input_path)
model = StepModel.load(input_path)
face_id, pair_face_id, current_total_length, current_center_distance, _ = _nearest_obround_total_length(model, 16.0)
slot_diameter = current_total_length - current_center_distance
target_center_distance = 14.0
target_total_length = target_center_distance + slot_diameter
_write_request(
request_path,
input_path,
output_path,
"resize_cylindrical_slot_center_distance",
[face_id, target_center_distance, pair_face_id],
)
response = _run_worker(request_path)
result_model = StepModel.load(output_path)
verified_face, verified_pair, total_length, center_distance, total_error = _nearest_obround_total_length(
result_model,
target_total_length,
)
center_error = abs(center_distance - target_center_distance)
if center_error > 1e-4 or total_error > 1e-4:
raise SystemExit(
f"isolated obround center distance verification failed: faces=({verified_face}, {verified_pair}), "
f"total_length={total_length:g}, center_distance={center_distance:g}, "
f"center_error={center_error:g}, total_error={total_error:g}, response={response}"
)
_assert_one_solid(result_model, "isolated obround center distance")
print(
"isolated obround center distance ok: "
f"faces=({verified_face}, {verified_pair}), center_distance={center_distance:g}"
)
def _verify_obround_slot_axis_isolated(temp_dir: Path) -> None:
input_path = temp_dir / "obround_axis.step"
output_path = temp_dir / "obround_axis_out.step"
request_path = temp_dir / "obround_axis_request.json"
_write_obround_slot_model(input_path)
model = StepModel.load(input_path)
face_id, pair_face_id, _current_total_length, _current_center_distance, _ = _nearest_obround_total_length(model, 16.0)
current_center = _slot_axis_center(model, face_id)
pair_center = _slot_axis_center(model, pair_face_id)
target_center = (current_center[0], current_center[1] + 1.0, current_center[2])
target_pair_center = (pair_center[0], pair_center[1] + 1.0, pair_center[2])
_write_request(request_path, input_path, output_path, "move_cylindrical_slot_axis", [face_id, target_center])
response = _run_worker(request_path)
result_model = StepModel.load(output_path)
verified_face, verified_pair, center_1, center_2, error = _nearest_obround_axis_pair(
result_model,
target_center,
target_pair_center,
)
if error > 1e-4:
raise SystemExit(
f"isolated obround axis move verification failed: faces=({verified_face}, {verified_pair}), "
f"centers=({center_1}, {center_2}), error={error:g}, response={response}"
)
_assert_one_solid(result_model, "isolated obround axis move")
print(f"isolated obround axis move ok: faces=({verified_face}, {verified_pair}), error={error:g}")
def _verify_hole_window_isolation_keeps_logical_id(temp_dir: Path) -> None:
input_path = temp_dir / "through_hole_window.step"
_write_through_hole_model(input_path)
probe = _window_probe_for(input_path)
face_id = _first_hole_face(probe.model, blind=False)
logical_id = probe.model.face_logical_id(face_id)
target_diameter = 8.0
target_center = _axis_center(probe.model, face_id)
plan = probe.model.cylindrical_resize_plan(face_id, target_diameter)
context = {
"operation_name": "调整圆柱孔径",
"target": f"Face {face_id}",
"parameters": {
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"face_id": face_id,
"new_diameter": target_diameter,
"old_diameter": plan.get("current_diameter"),
"resize_strategy": plan.get("resize_strategy"),
},
"target_kind": "face",
"target_id": face_id,
"target_logical_id": logical_id,
"pick_position": None,
"show_same_domain_internal_edges": False,
"edit_result_deflection": 1.6,
}
if probe._context_is_face_parameter_edit(context):
raise SystemExit("hole resize should not be treated as a generic Face parameter edit")
if not probe._context_should_preserve_face_logical_id(context):
raise SystemExit("hole resize should still preserve the selected logical Face ID")
result = probe._run_isolated_edit_job(
context=context,
isolation={
"operation": "resize_cylindrical_hole",
"args": [face_id, target_diameter],
"timeout_seconds": 120.0,
},
snapshot=probe.model.snapshot(),
before_stats=probe.model.stats(),
before_part_stats=probe.model.part_topology_stats(int(plan.get("part_id") or 1)),
before_quality=probe._edit_quality_info_or_none(probe.model, context, int(plan.get("part_id") or 1)),
before_geometry={},
)
after_model = result.get("after_model")
if not isinstance(after_model, StepModel):
raise SystemExit(f"window hole isolation should return after_model: {result}")
verified_face, diameter, center, error = _nearest_hole_by_diameter(
after_model,
target_diameter,
target_center,
blind=False,
)
if error > 1e-4 or abs(diameter - target_diameter) > 1e-4:
raise SystemExit(
f"window hole isolation verification failed: face={verified_face}, "
f"diameter={diameter:g}, center={center}, error={error:g}, result={result}"
)
if not _logical_region_has_diameter(after_model, logical_id, target_diameter):
raise SystemExit(
f"window hole isolation did not preserve logical Face {logical_id} on the resized hole"
)
_assert_one_solid(after_model, "window hole isolation")
print(f"window hole isolation ok: logical={logical_id}, face={verified_face}, diameter={diameter:g}")
def _verify_hole_axis_window_isolation_keeps_logical_id(temp_dir: Path) -> None:
input_path = temp_dir / "through_hole_axis_window.step"
_write_through_hole_model(input_path)
probe = _window_probe_for(input_path)
face_id = _first_hole_face(probe.model, blind=False)
logical_id = probe.model.face_logical_id(face_id)
current_center = _axis_center(probe.model, face_id)
current_diameter = float(probe.model.face_info(face_id)["diameter"])
target_center = (current_center[0] + 2.0, current_center[1], current_center[2])
plan = probe.model.cylindrical_axis_move_plan(face_id, target_center)
context = {
"operation_name": "move hole axis",
"target": f"Face {face_id}",
"parameters": {
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"face_id": face_id,
"current_center": current_center,
"target_center": target_center,
"current_diameter": current_diameter,
"resize_strategy": plan.get("resize_strategy"),
},
"target_kind": "face",
"target_id": face_id,
"target_logical_id": logical_id,
"pick_position": None,
"show_same_domain_internal_edges": False,
"edit_result_deflection": 1.6,
}
if probe._context_is_face_parameter_edit(context):
raise SystemExit("hole axis move should not be treated as a generic Face parameter edit")
if not probe._context_should_preserve_face_logical_id(context):
raise SystemExit("hole axis move should still preserve the selected logical Face ID")
result = probe._run_isolated_edit_job(
context=context,
isolation={
"operation": "move_cylindrical_hole_axis",
"args": [face_id, target_center],
"timeout_seconds": 120.0,
},
snapshot=probe.model.snapshot(),
before_stats=probe.model.stats(),
before_part_stats=probe.model.part_topology_stats(int(plan.get("part_id") or 1)),
before_quality=probe._edit_quality_info_or_none(probe.model, context, int(plan.get("part_id") or 1)),
before_geometry={},
)
after_model = result.get("after_model")
if not isinstance(after_model, StepModel):
raise SystemExit(f"window hole-axis isolation should return after_model: {result}")
verified_face, diameter, center, error = _nearest_hole_by_diameter(
after_model,
current_diameter,
target_center,
blind=False,
)
if error > 1e-4 or abs(diameter - current_diameter) > 1e-4:
raise SystemExit(
f"window hole-axis isolation verification failed: face={verified_face}, "
f"diameter={diameter:g}, center={center}, error={error:g}, result={result}"
)
if not _logical_region_has_hole_axis(after_model, logical_id, target_center, current_diameter):
raise SystemExit(
f"window hole-axis isolation did not preserve logical Face {logical_id} on the moved hole"
)
_assert_one_solid(after_model, "window hole-axis isolation")
print(f"window hole-axis isolation ok: logical={logical_id}, face={verified_face}, center={center}")
def _verify_slot_window_isolation_keeps_logical_id(temp_dir: Path) -> None:
input_path = temp_dir / "slot_window.step"
_write_half_round_slot_model(input_path)
probe = _window_probe_for(input_path)
face_id = _first_slot_face(probe.model)
logical_id = probe.model.face_logical_id(face_id)
target_width = 8.0
plan = probe.model.cylindrical_slot_resize_plan(face_id, target_width, "width")
context = {
"operation_name": "resize slot width",
"target": f"Face {face_id}",
"parameters": {
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"face_id": face_id,
"target_width": target_width,
"current_width": plan.get("slot_current_width"),
"resize_strategy": plan.get("resize_strategy"),
"slot_pair_face_id": plan.get("slot_pair_face_id"),
},
"target_kind": "face",
"target_id": face_id,
"target_logical_id": logical_id,
"pick_position": None,
"show_same_domain_internal_edges": False,
"edit_result_deflection": 1.6,
}
if probe._context_is_face_parameter_edit(context):
raise SystemExit("slot width should not be treated as a generic Face parameter edit")
if not probe._context_should_preserve_face_logical_id(context):
raise SystemExit("slot width should still preserve the selected logical Face ID")
result = probe._run_isolated_edit_job(
context=context,
isolation={
"operation": "resize_cylindrical_slot_width",
"args": [face_id, target_width, None],
"timeout_seconds": 120.0,
},
snapshot=probe.model.snapshot(),
before_stats=probe.model.stats(),
before_part_stats=probe.model.part_topology_stats(int(plan.get("part_id") or 1)),
before_quality=probe._edit_quality_info_or_none(probe.model, context, int(plan.get("part_id") or 1)),
before_geometry={},
)
after_model = result.get("after_model")
if not isinstance(after_model, StepModel):
raise SystemExit(f"window slot isolation should return after_model: {result}")
verified_face, width, error = _nearest_metric(after_model, "slot_chord_width_estimate", target_width)
if error > 1e-4:
raise SystemExit(
f"window slot isolation verification failed: face={verified_face}, "
f"width={width:g}, error={error:g}, result={result}"
)
if not _logical_region_has_slot_metric(after_model, logical_id, "slot_chord_width_estimate", target_width):
raise SystemExit(
f"window slot isolation did not preserve logical Face {logical_id} on the resized slot"
)
_assert_one_solid(after_model, "window slot isolation")
print(f"window slot isolation ok: logical={logical_id}, face={verified_face}, width={width:g}")
def _verify_slot_axis_window_isolation_keeps_logical_id(temp_dir: Path) -> None:
input_path = temp_dir / "slot_axis_window.step"
_write_half_round_slot_model(input_path)
probe = _window_probe_for(input_path)
face_id = _first_slot_face(probe.model)
logical_id = probe.model.face_logical_id(face_id)
current_center = _slot_axis_center(probe.model, face_id)
target_center = (current_center[0], current_center[1] + 1.0, current_center[2])
plan = probe.model.cylindrical_slot_axis_move_plan(face_id, target_center)
context = {
"operation_name": "move slot axis",
"target": f"Face {face_id}",
"parameters": {
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"face_id": face_id,
"current_center": current_center,
"target_center": target_center,
"resize_strategy": plan.get("resize_strategy"),
},
"target_kind": "face",
"target_id": face_id,
"target_logical_id": logical_id,
"pick_position": None,
"show_same_domain_internal_edges": False,
"edit_result_deflection": 1.6,
}
if probe._context_is_face_parameter_edit(context):
raise SystemExit("slot axis move should not be treated as a generic Face parameter edit")
if not probe._context_should_preserve_face_logical_id(context):
raise SystemExit("slot axis move should still preserve the selected logical Face ID")
result = probe._run_isolated_edit_job(
context=context,
isolation={
"operation": "move_cylindrical_slot_axis",
"args": [face_id, target_center],
"timeout_seconds": 120.0,
},
snapshot=probe.model.snapshot(),
before_stats=probe.model.stats(),
before_part_stats=probe.model.part_topology_stats(int(plan.get("part_id") or 1)),
before_quality=probe._edit_quality_info_or_none(probe.model, context, int(plan.get("part_id") or 1)),
before_geometry={},
)
after_model = result.get("after_model")
if not isinstance(after_model, StepModel):
raise SystemExit(f"window slot-axis isolation should return after_model: {result}")
verified_face, center, error = _nearest_axis_center(after_model, target_center)
if error > 1e-4:
raise SystemExit(
f"window slot-axis isolation verification failed: face={verified_face}, "
f"center={center}, error={error:g}, result={result}"
)
if not _logical_region_has_slot_axis(after_model, logical_id, target_center):
raise SystemExit(
f"window slot-axis isolation did not preserve logical Face {logical_id} on the moved slot"
)
_assert_one_solid(after_model, "window slot-axis isolation")
print(f"window slot-axis isolation ok: logical={logical_id}, face={verified_face}, center={center}")
def _verify_blind_depth_window_isolation_keeps_logical_id(temp_dir: Path) -> None:
input_path = temp_dir / "blind_depth_window.step"
_write_blind_hole_model(input_path)
probe = _window_probe_for(input_path)
face_id = _first_hole_face(probe.model, blind=True)
logical_id = probe.model.face_logical_id(face_id)
feature = probe.model.feature_info(face_id)
bottom_face_ids = tuple(feature.get("feature_bottom_face_ids", ()))
bottom_face_id = int(bottom_face_ids[0]) if bottom_face_ids else None
target_depth = 8.0
plan = probe.model.cylindrical_depth_plan(face_id, target_depth, bottom_face_id=bottom_face_id)
context = {
"operation_name": "resize blind hole depth",
"target": f"Face {face_id}",
"parameters": {
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"face_id": face_id,
"target_depth": target_depth,
"current_depth": plan.get("current_depth"),
"resize_strategy": plan.get("resize_strategy"),
"manual_bottom_face_id": bottom_face_id,
},
"target_kind": "face",
"target_id": face_id,
"target_logical_id": logical_id,
"pick_position": None,
"show_same_domain_internal_edges": False,
"edit_result_deflection": 1.6,
}
if probe._context_is_face_parameter_edit(context):
raise SystemExit("blind depth should not be treated as a generic Face parameter edit")
if not probe._context_should_preserve_face_logical_id(context):
raise SystemExit("blind depth should still preserve the selected logical Face ID")
result = probe._run_isolated_edit_job(
context=context,
isolation={
"operation": "resize_cylindrical_depth",
"args": [face_id, target_depth, bottom_face_id],
"timeout_seconds": 120.0,
},
snapshot=probe.model.snapshot(),
before_stats=probe.model.stats(),
before_part_stats=probe.model.part_topology_stats(int(plan.get("part_id") or 1)),
before_quality=probe._edit_quality_info_or_none(probe.model, context, int(plan.get("part_id") or 1)),
before_geometry={},
)
after_model = result.get("after_model")
if not isinstance(after_model, StepModel):
raise SystemExit(f"window blind-depth isolation should return after_model: {result}")
verified_face, depth, error = _nearest_blind_depth(after_model, target_depth)
if error > 1e-4:
raise SystemExit(
f"window blind-depth isolation verification failed: face={verified_face}, "
f"depth={depth:g}, error={error:g}, result={result}"
)
if not _logical_region_has_blind_depth(after_model, logical_id, target_depth):
raise SystemExit(
f"window blind-depth isolation did not preserve logical Face {logical_id} on the resized feature"
)
_assert_one_solid(after_model, "window blind-depth isolation")
print(f"window blind-depth isolation ok: logical={logical_id}, face={verified_face}, depth={depth:g}")
def _verify_obround_total_length_window_isolation_keeps_logical_id(temp_dir: Path) -> None:
input_path = temp_dir / "obround_total_length_window.step"
_write_obround_slot_model(input_path)
probe = _window_probe_for(input_path)
face_id, pair_face_id, _current_total_length, _current_center_distance, _ = _nearest_obround_total_length(
probe.model,
16.0,
)
logical_id = probe.model.face_logical_id(face_id)
target_total_length = 20.0
plan = probe.model.cylindrical_slot_total_length_plan(face_id, target_total_length, pair_face_id=pair_face_id)
context = {
"operation_name": "resize obround slot total length",
"target": f"Face {face_id}",
"parameters": {
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"face_id": face_id,
"target_total_length": target_total_length,
"slot_pair_face_id": pair_face_id,
"resize_strategy": plan.get("resize_strategy"),
},
"target_kind": "face",
"target_id": face_id,
"target_logical_id": logical_id,
"pick_position": None,
"show_same_domain_internal_edges": False,
"edit_result_deflection": 1.6,
}
if probe._context_is_face_parameter_edit(context):
raise SystemExit("obround total length should not be treated as a generic Face parameter edit")
if not probe._context_should_preserve_face_logical_id(context):
raise SystemExit("obround total length should still preserve the selected logical Face ID")
result = probe._run_isolated_edit_job(
context=context,
isolation={
"operation": "resize_cylindrical_slot_total_length",
"args": [face_id, target_total_length, pair_face_id],
"timeout_seconds": 120.0,
},
snapshot=probe.model.snapshot(),
before_stats=probe.model.stats(),
before_part_stats=probe.model.part_topology_stats(int(plan.get("part_id") or 1)),
before_quality=probe._edit_quality_info_or_none(probe.model, context, int(plan.get("part_id") or 1)),
before_geometry={},
)
after_model = result.get("after_model")
if not isinstance(after_model, StepModel):
raise SystemExit(f"window obround-total-length isolation should return after_model: {result}")
verified_face, verified_pair, total_length, center_distance, error = _nearest_obround_total_length(
after_model,
target_total_length,
)
if error > 1e-4:
raise SystemExit(
f"window obround-total-length isolation verification failed: "
f"faces=({verified_face}, {verified_pair}), total_length={total_length:g}, "
f"center_distance={center_distance:g}, error={error:g}, result={result}"
)
if not _logical_region_contains_any(after_model, logical_id, (verified_face, verified_pair)):
raise SystemExit(
f"window obround-total-length isolation did not preserve logical Face {logical_id} "
"on the resized slot end"
)
_assert_one_solid(after_model, "window obround-total-length isolation")
print(
"window obround-total-length isolation ok: "
f"logical={logical_id}, faces=({verified_face}, {verified_pair}), total_length={total_length:g}"
)
def _verify_obround_axis_window_isolation_keeps_logical_id(temp_dir: Path) -> None:
input_path = temp_dir / "obround_axis_window.step"
_write_obround_slot_model(input_path)
probe = _window_probe_for(input_path)
face_id, pair_face_id, _current_total_length, _current_center_distance, _ = _nearest_obround_total_length(
probe.model,
16.0,
)
logical_id = probe.model.face_logical_id(face_id)
current_center = _slot_axis_center(probe.model, face_id)
pair_center = _slot_axis_center(probe.model, pair_face_id)
target_center = (current_center[0], current_center[1] + 1.0, current_center[2])
target_pair_center = (pair_center[0], pair_center[1] + 1.0, pair_center[2])
plan = probe.model.cylindrical_slot_axis_move_plan(face_id, target_center)
context = {
"operation_name": "move obround slot axis",
"target": f"Face {face_id}",
"parameters": {
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"face_id": face_id,
"slot_pair_face_id": pair_face_id,
"target_center": target_center,
"target_pair_center": target_pair_center,
"resize_strategy": plan.get("resize_strategy"),
},
"target_kind": "face",
"target_id": face_id,
"target_logical_id": logical_id,
"pick_position": None,
"show_same_domain_internal_edges": False,
"edit_result_deflection": 1.6,
}
if probe._context_is_face_parameter_edit(context):
raise SystemExit("obround axis move should not be treated as a generic Face parameter edit")
if not probe._context_should_preserve_face_logical_id(context):
raise SystemExit("obround axis move should still preserve the selected logical Face ID")
result = probe._run_isolated_edit_job(
context=context,
isolation={
"operation": "move_cylindrical_slot_axis",
"args": [face_id, target_center],
"timeout_seconds": 120.0,
},
snapshot=probe.model.snapshot(),
before_stats=probe.model.stats(),
before_part_stats=probe.model.part_topology_stats(int(plan.get("part_id") or 1)),
before_quality=probe._edit_quality_info_or_none(probe.model, context, int(plan.get("part_id") or 1)),
before_geometry={},
)
after_model = result.get("after_model")
if not isinstance(after_model, StepModel):
raise SystemExit(f"window obround-axis isolation should return after_model: {result}")
verified_face, verified_pair, center_1, center_2, error = _nearest_obround_axis_pair(
after_model,
target_center,
target_pair_center,
)
if error > 1e-4:
raise SystemExit(
f"window obround-axis isolation verification failed: faces=({verified_face}, {verified_pair}), "
f"centers=({center_1}, {center_2}), error={error:g}, result={result}"
)
if not _logical_region_contains_any(after_model, logical_id, (verified_face, verified_pair)):
raise SystemExit(
f"window obround-axis isolation did not preserve logical Face {logical_id} on the moved slot end"
)
_assert_one_solid(after_model, "window obround-axis isolation")
print(f"window obround-axis isolation ok: logical={logical_id}, faces=({verified_face}, {verified_pair})")
def main() -> int:
with tempfile.TemporaryDirectory(prefix="geom_param_hole_slot_isolated_") as temp_name:
temp_dir = Path(temp_name)
_verify_hole_diameter_isolated(temp_dir)
_verify_hole_owning_scale_isolated(temp_dir)
_verify_hole_axis_isolated(temp_dir)
_verify_hole_suppress_isolated(temp_dir)
_verify_blind_depth_isolated(temp_dir)
_verify_blind_depth_owning_scale_isolated(temp_dir)
_verify_slot_width_isolated(temp_dir)
_verify_slot_owning_scale_isolated(temp_dir)
_verify_slot_metric_isolated(
temp_dir,
"slot_depth",
"resize_cylindrical_slot_depth",
4.0,
"slot_sagitta_depth_estimate",
)
_verify_slot_metric_isolated(
temp_dir,
"slot_arc_length",
"resize_cylindrical_slot_arc_length",
12.0,
"slot_arc_length_estimate",
)
_verify_slot_metric_isolated(
temp_dir,
"slot_angular_span",
"resize_cylindrical_slot_angular_span",
2.2,
"slot_angular_span",
)
_verify_slot_axis_isolated(temp_dir)
_verify_obround_slot_total_length_isolated(temp_dir)
_verify_obround_slot_center_distance_isolated(temp_dir)
_verify_obround_slot_axis_isolated(temp_dir)
_verify_hole_window_isolation_keeps_logical_id(temp_dir)
_verify_hole_axis_window_isolation_keeps_logical_id(temp_dir)
_verify_slot_window_isolation_keeps_logical_id(temp_dir)
_verify_slot_axis_window_isolation_keeps_logical_id(temp_dir)
_verify_blind_depth_window_isolation_keeps_logical_id(temp_dir)
_verify_obround_total_length_window_isolation_keeps_logical_id(temp_dir)
_verify_obround_axis_window_isolation_keeps_logical_id(temp_dir)
print("hole/slot isolated edit worker ok")
return 0
if __name__ == "__main__":
raise SystemExit(main())