577 lines
24 KiB
Python
577 lines
24 KiB
Python
import hashlib
|
|
import json
|
|
import os
|
|
import sys
|
|
import gc
|
|
|
|
import FreeCAD as App
|
|
import Path
|
|
|
|
|
|
MARKER = "FREECAD_CAM_PATH_RESULT="
|
|
|
|
|
|
def emit_marker(value, status=0):
|
|
result_file = os.environ.get("FREECAD_CAM_PATH_RESULT_FILE", "")
|
|
marker_value = value
|
|
if result_file:
|
|
parent = os.path.dirname(result_file)
|
|
if parent:
|
|
os.makedirs(parent, exist_ok=True)
|
|
with open(result_file, "w", encoding="utf-8") as stream:
|
|
json.dump(value, stream, sort_keys=True, separators=(",", ":"))
|
|
stream.write("\n")
|
|
os._exit(status)
|
|
marker_value = value
|
|
payload = (MARKER + json.dumps(marker_value, sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
|
|
offset = 0
|
|
while offset < len(payload):
|
|
offset += os.write(sys.stdout.fileno(), payload[offset:])
|
|
os._exit(status)
|
|
|
|
|
|
def command_record(command, precision=9):
|
|
threshold = 0.5 * (10 ** -precision)
|
|
parameters = {}
|
|
for name, value in sorted(dict(command.Parameters).items()):
|
|
rounded = round(float(value), precision)
|
|
parameters[str(name)] = 0.0 if abs(rounded) < threshold else rounded
|
|
parameter_text = " ".join(
|
|
"{}{:0.{}f}".format(name, value, precision)
|
|
for name, value in parameters.items()
|
|
)
|
|
return {
|
|
"name": str(command.Name),
|
|
"parameters": parameters,
|
|
"gcode": (str(command.Name) + (" " + parameter_text if parameter_text else "")).strip(),
|
|
}
|
|
|
|
|
|
def path_record(obj):
|
|
path = obj.Path
|
|
bounds = path.BoundBox
|
|
return {
|
|
"name": obj.Name,
|
|
"label": obj.Label,
|
|
"typeId": obj.TypeId,
|
|
"pathPropertyType": obj.getTypeIdOfProperty("Path"),
|
|
"commandCount": len(path.Commands),
|
|
"commands": [command_record(command) for command in path.Commands],
|
|
"length": round(float(path.Length), 9),
|
|
"boundingBox": {
|
|
"min": [round(float(bounds.XMin), 9), round(float(bounds.YMin), 9), round(float(bounds.ZMin), 9)],
|
|
"max": [round(float(bounds.XMax), 9), round(float(bounds.YMax), 9), round(float(bounds.ZMax), 9)],
|
|
},
|
|
"oracleTag": str(getattr(obj, "OracleTag", "")),
|
|
}
|
|
|
|
|
|
def archive_record(path):
|
|
with open(path, "rb") as stream:
|
|
payload = stream.read()
|
|
return {
|
|
"path": path,
|
|
"byteLength": len(payload),
|
|
"sha256": hashlib.sha256(payload).hexdigest(),
|
|
}
|
|
|
|
|
|
def load_path_objects(path):
|
|
document = App.openDocument(path)
|
|
try:
|
|
records = [path_record(document.getObject(name)) for name in ("NativePath", "PythonPath")]
|
|
return records
|
|
finally:
|
|
App.closeDocument(document.Name)
|
|
|
|
|
|
def verify_preserved_archive(path):
|
|
records = load_path_objects(path)
|
|
expected_native_count = int(os.environ.get("FREECAD_CAM_PATH_EXPECT_NATIVE_COUNT", "5"))
|
|
expected_python_count = int(os.environ.get("FREECAD_CAM_PATH_EXPECT_PYTHON_COUNT", "5"))
|
|
expected_native_last = os.environ.get("FREECAD_CAM_PATH_EXPECT_NATIVE_LAST", "")
|
|
expected_python_last = os.environ.get("FREECAD_CAM_PATH_EXPECT_PYTHON_LAST", "")
|
|
expected_counts = {"NativePath": expected_native_count, "PythonPath": expected_python_count}
|
|
result = {
|
|
"schemaVersion": 1,
|
|
"baselineId": "freecad-1.1.1",
|
|
"mode": "web-preserved-native-reopen",
|
|
"archive": archive_record(path),
|
|
"objects": records,
|
|
"verified": all(
|
|
record["pathPropertyType"] == "Path::PropertyPath"
|
|
and record["commandCount"] == expected_counts[record["name"]]
|
|
and (record["name"] != "NativePath" or not expected_native_last or record["commands"][-1]["name"] == expected_native_last)
|
|
and (record["name"] != "PythonPath" or not expected_python_last or record["commands"][-1]["name"] == expected_python_last)
|
|
for record in records
|
|
),
|
|
}
|
|
emit_marker(result, 0 if result["verified"] else 2)
|
|
|
|
|
|
def qt_command_oracle(document, selected_object):
|
|
import FreeCADGui as Gui
|
|
from PySide import QtCore, QtWidgets
|
|
|
|
workbenches = Gui.listWorkbenches()
|
|
workbench_name = "CAMWorkbench" if "CAMWorkbench" in workbenches else "PathWorkbench"
|
|
if workbench_name not in workbenches:
|
|
raise RuntimeError("FreeCAD CAM workbench is unavailable in the desktop oracle")
|
|
Gui.activateWorkbench(workbench_name)
|
|
Gui.updateGui()
|
|
QtWidgets.QApplication.processEvents()
|
|
|
|
all_commands = sorted(set(Gui.listCommands()))
|
|
cam_commands = [command_id for command_id in all_commands if command_id.startswith("CAM_")]
|
|
required = [
|
|
"CAM_Job",
|
|
"CAM_Profile",
|
|
"CAM_Pocket_Shape",
|
|
"CAM_Drilling",
|
|
"CAM_Adaptive",
|
|
"CAM_Pocket3D",
|
|
"CAM_Post",
|
|
"CAM_Simulator",
|
|
"CAM_Sanity",
|
|
"CAM_ToolBitDock",
|
|
]
|
|
|
|
def observe(command_id):
|
|
command = Gui.Command.get(command_id)
|
|
actions = [] if command is None else list(command.getAction())
|
|
if not actions:
|
|
return {"registered": command is not None, "actionCount": 0, "enabled": None}
|
|
action = actions[0]
|
|
return {
|
|
"registered": True,
|
|
"actionCount": len(actions),
|
|
"objectName": str(action.objectName()),
|
|
"text": str(action.text()).replace("&", ""),
|
|
"toolTip": str(action.toolTip()),
|
|
"shortcut": str(action.shortcut().toString()),
|
|
"enabled": bool(command.isActive()),
|
|
"visible": bool(action.isVisible()),
|
|
"iconPresent": not action.icon().isNull(),
|
|
}
|
|
|
|
Gui.Selection.clearSelection()
|
|
Gui.Command.update()
|
|
Gui.updateGui()
|
|
QtWidgets.QApplication.processEvents()
|
|
empty = {command_id: observe(command_id) for command_id in required}
|
|
Gui.Selection.addSelection(document.Name, selected_object.Name)
|
|
Gui.Command.update()
|
|
Gui.updateGui()
|
|
QtWidgets.QApplication.processEvents()
|
|
selected = {command_id: observe(command_id) for command_id in required}
|
|
Gui.Selection.clearSelection()
|
|
selected_job_object = document.getObject("OracleJob")
|
|
Gui.Selection.addSelection(document.Name, selected_job_object.Name)
|
|
Gui.Command.update()
|
|
Gui.updateGui()
|
|
QtWidgets.QApplication.processEvents()
|
|
selected_job = {command_id: observe(command_id) for command_id in required}
|
|
Gui.Selection.clearSelection()
|
|
selected_operation_object = document.getObject("OracleProfile")
|
|
Gui.Selection.addSelection(document.Name, selected_operation_object.Name)
|
|
Gui.Command.update()
|
|
Gui.updateGui()
|
|
QtWidgets.QApplication.processEvents()
|
|
selected_operation = {command_id: observe(command_id) for command_id in required}
|
|
Gui.Selection.clearSelection()
|
|
empty_document = App.newDocument("CamQtEmptyContext")
|
|
Gui.Command.update()
|
|
Gui.updateGui()
|
|
QtWidgets.QApplication.processEvents()
|
|
empty_document_state = {command_id: observe(command_id) for command_id in required}
|
|
App.closeDocument(empty_document.Name)
|
|
App.setActiveDocument(document.Name)
|
|
Gui.Command.update()
|
|
Gui.updateGui()
|
|
|
|
main_window = Gui.getMainWindow()
|
|
toolbars = []
|
|
for toolbar in main_window.findChildren(QtWidgets.QToolBar):
|
|
command_ids = [str(action.objectName()) for action in toolbar.actions() if str(action.objectName()).startswith("CAM_")]
|
|
if command_ids:
|
|
toolbars.append({
|
|
"objectName": str(toolbar.objectName()),
|
|
"title": str(toolbar.windowTitle()).replace("&", ""),
|
|
"visible": bool(toolbar.isVisible()),
|
|
"commands": command_ids,
|
|
})
|
|
|
|
menus = []
|
|
for menu_action in main_window.menuBar().actions():
|
|
menu = menu_action.menu()
|
|
if menu is None:
|
|
continue
|
|
command_ids = []
|
|
for action in menu.actions():
|
|
action_name = str(action.objectName())
|
|
if action_name.startswith("CAM_"):
|
|
command_ids.append(action_name)
|
|
submenu = action.menu()
|
|
if submenu is not None:
|
|
command_ids.extend(
|
|
str(child.objectName())
|
|
for child in submenu.actions()
|
|
if str(child.objectName()).startswith("CAM_")
|
|
)
|
|
if command_ids:
|
|
menus.append({
|
|
"title": str(menu_action.text()).replace("&", ""),
|
|
"commands": sorted(set(command_ids)),
|
|
})
|
|
|
|
def flush_qt():
|
|
Gui.updateGui()
|
|
QtWidgets.QApplication.processEvents()
|
|
|
|
def task_panel_state(phase):
|
|
focus = QtWidgets.QApplication.focusWidget()
|
|
button_boxes = [box for box in main_window.findChildren(QtWidgets.QDialogButtonBox) if box.isVisible()]
|
|
buttons = []
|
|
for box in button_boxes:
|
|
for button in box.buttons():
|
|
buttons.append({
|
|
"text": str(button.text()).replace("&", ""),
|
|
"objectName": str(button.objectName()),
|
|
"enabled": bool(button.isEnabled()),
|
|
"visible": bool(button.isVisible()),
|
|
"default": bool(button.isDefault()),
|
|
"role": str(box.buttonRole(button)),
|
|
})
|
|
fields = []
|
|
field_types = (QtWidgets.QLineEdit, QtWidgets.QComboBox, QtWidgets.QSpinBox, QtWidgets.QDoubleSpinBox, QtWidgets.QCheckBox)
|
|
widgets = []
|
|
seen_widgets = set()
|
|
for field_type in field_types:
|
|
for widget in main_window.findChildren(field_type):
|
|
identity = id(widget)
|
|
if identity not in seen_widgets:
|
|
widgets.append(widget)
|
|
seen_widgets.add(identity)
|
|
for widget in widgets:
|
|
if not widget.isVisible():
|
|
continue
|
|
fields.append({
|
|
"className": str(widget.metaObject().className()),
|
|
"objectName": str(widget.objectName()),
|
|
"enabled": bool(widget.isEnabled()),
|
|
"focus": widget is focus,
|
|
})
|
|
field_classes = sorted({field["className"] for field in fields})
|
|
in_edit = Gui.activeDocument().getInEdit() if Gui.activeDocument() else None
|
|
in_edit_object = getattr(in_edit, "Object", None) if in_edit else None
|
|
return {
|
|
"phase": phase,
|
|
"activeDialog": bool(Gui.Control.activeDialog()),
|
|
"inEdit": str(getattr(in_edit_object, "Name", "")) if in_edit else "",
|
|
"focus": None if focus is None else {"className": str(focus.metaObject().className()), "objectName": str(focus.objectName())},
|
|
"buttonBoxCount": len(button_boxes),
|
|
"buttonTexts": [{"text": button["text"], "enabled": button["enabled"], "default": button["default"], "role": button["role"]} for button in buttons],
|
|
"fieldCount": len(fields),
|
|
"fieldClasses": field_classes,
|
|
}
|
|
|
|
def close_task_panel(action):
|
|
candidates = []
|
|
for box in main_window.findChildren(QtWidgets.QDialogButtonBox):
|
|
if not box.isVisible():
|
|
continue
|
|
for button in box.buttons():
|
|
role = box.buttonRole(button)
|
|
if action == "accept" and role in (QtWidgets.QDialogButtonBox.AcceptRole, QtWidgets.QDialogButtonBox.ApplyRole):
|
|
candidates.append(button)
|
|
if action == "cancel" and role in (QtWidgets.QDialogButtonBox.RejectRole, QtWidgets.QDialogButtonBox.DestructiveRole):
|
|
candidates.append(button)
|
|
clicked = False
|
|
if candidates:
|
|
candidates[0].click()
|
|
clicked = True
|
|
flush_qt()
|
|
if Gui.Control.activeDialog():
|
|
Gui.activeDocument().resetEdit()
|
|
flush_qt()
|
|
return clicked
|
|
|
|
def edit_lifecycle(object_name, action):
|
|
before = task_panel_state("before")
|
|
entered = bool(Gui.activeDocument().setEdit(object_name))
|
|
flush_qt()
|
|
active = task_panel_state("active")
|
|
clicked = close_task_panel(action)
|
|
closed = task_panel_state("closed")
|
|
return {
|
|
"objectName": object_name,
|
|
"action": action,
|
|
"setEditAccepted": entered,
|
|
"buttonClicked": clicked,
|
|
"before": before,
|
|
"active": active,
|
|
"closed": closed,
|
|
"dialogOpened": active["activeDialog"] and bool(active["inEdit"]),
|
|
"dialogClosed": not closed["activeDialog"] and not closed["inEdit"],
|
|
}
|
|
|
|
task_lifecycles = [
|
|
edit_lifecycle(selected_job_object.Name, "cancel"),
|
|
edit_lifecycle(selected_operation_object.Name, "accept"),
|
|
edit_lifecycle(selected_operation_object.Name, "cancel"),
|
|
]
|
|
gc.collect()
|
|
QtWidgets.QApplication.processEvents()
|
|
|
|
observations = []
|
|
for command_id in required:
|
|
observations.append({
|
|
"id": command_id,
|
|
"emptySelection": empty[command_id],
|
|
"selectedModel": selected[command_id],
|
|
"selectedJob": selected_job[command_id],
|
|
"selectedOperation": selected_operation[command_id],
|
|
"emptyDocument": empty_document_state[command_id],
|
|
"selectionChangesEnabledState": len({
|
|
(empty[command_id].get("enabled"), empty[command_id].get("isActive")),
|
|
(selected[command_id].get("enabled"), selected[command_id].get("isActive")),
|
|
(selected_job[command_id].get("enabled"), selected_job[command_id].get("isActive")),
|
|
(selected_operation[command_id].get("enabled"), selected_operation[command_id].get("isActive")),
|
|
(empty_document_state[command_id].get("enabled"), empty_document_state[command_id].get("isActive")),
|
|
}) > 1,
|
|
})
|
|
return {
|
|
"guiUp": bool(App.GuiUp),
|
|
"qtVersion": str(QtCore.qVersion()),
|
|
"workbench": workbench_name,
|
|
"activeWorkbench": str(Gui.activeWorkbench().name()),
|
|
"registeredCommandCount": len(all_commands),
|
|
"camCommandCount": len(cam_commands),
|
|
"camCommands": cam_commands,
|
|
"requiredCommands": observations,
|
|
"toolbarCount": len(toolbars),
|
|
"toolbars": sorted(toolbars, key=lambda item: (item["title"], item["objectName"])),
|
|
"menuCount": len(menus),
|
|
"menus": menus,
|
|
"taskLifecycles": task_lifecycles,
|
|
"taskLifecycleVerified": all(item["dialogOpened"] and item["dialogClosed"] for item in task_lifecycles),
|
|
}
|
|
|
|
|
|
def profile_algorithm_oracle():
|
|
import Path.Main.Job as PathJob
|
|
import Path.Op.Profile as PathProfile
|
|
|
|
fixture = os.path.join(App.getHomePath(), "Mod", "CAM", "CAMTests", "test_profile.fcstd")
|
|
App.ConfigSet("SuppressRecomputeRequiredDialog", "True")
|
|
document = App.openDocument(fixture)
|
|
App.ConfigSet("SuppressRecomputeRequiredDialog", "")
|
|
job = PathJob.Create("OracleJob", [document.Body], None)
|
|
job.GeometryTolerance.Value = 0.001
|
|
profile = PathProfile.Create("OracleProfile", parentJob=job)
|
|
profile.Base = [(document.Body, ["Face18"])]
|
|
profile.processCircles = True
|
|
profile.processHoles = True
|
|
profile.UseComp = True
|
|
profile.Direction = "CW"
|
|
document.recompute()
|
|
|
|
def summarize(operation, scenario_id):
|
|
commands = [command_record(command, 2) for command in operation.Path.Commands]
|
|
cutting = [command for command in commands if command["name"] in ("G1", "G2", "G3")]
|
|
canonical = json.dumps(commands, sort_keys=True, separators=(",", ":"))
|
|
return {
|
|
"id": scenario_id,
|
|
"useComp": bool(operation.UseComp),
|
|
"direction": str(operation.Direction),
|
|
"commandCount": len(commands),
|
|
"cuttingCommandCount": len(cutting),
|
|
"commandSha256": hashlib.sha256(canonical.encode("utf-8")).hexdigest(),
|
|
"firstCuttingCommand": cutting[0] if cutting else None,
|
|
"lastCuttingCommand": cutting[-1] if cutting else None,
|
|
"pathLength": round(float(operation.Path.Length), 2),
|
|
"commands": commands,
|
|
}
|
|
|
|
compensated = summarize(profile, "outside-cw-tool-comp")
|
|
no_comp = PathProfile.Create("OracleProfileNoComp", parentJob=job)
|
|
no_comp.Base = [(document.Body, ["Face18"])]
|
|
no_comp.processCircles = True
|
|
no_comp.processHoles = True
|
|
no_comp.UseComp = False
|
|
no_comp.Direction = "CW"
|
|
document.recompute()
|
|
uncompensated = summarize(no_comp, "outside-cw-no-comp")
|
|
result = {
|
|
"fixture": "Mod/CAM/CAMTests/test_profile.fcstd",
|
|
"fixtureSha256": archive_record(fixture)["sha256"],
|
|
"modelTypeId": document.Body.TypeId,
|
|
"jobTypeId": job.TypeId,
|
|
"operationTypeId": profile.TypeId,
|
|
"pathPropertyType": profile.getTypeIdOfProperty("Path"),
|
|
"commandCount": compensated["commandCount"],
|
|
"cuttingCommandCount": compensated["cuttingCommandCount"],
|
|
"commandSha256": compensated["commandSha256"],
|
|
"normalizationToleranceMm": 0.01,
|
|
"firstCuttingCommand": compensated["firstCuttingCommand"],
|
|
"lastCuttingCommand": compensated["lastCuttingCommand"],
|
|
"pathLength": compensated["pathLength"],
|
|
"commands": compensated["commands"],
|
|
"variants": [compensated, uncompensated],
|
|
}
|
|
return document, result
|
|
|
|
|
|
def helix_algorithm_oracle():
|
|
import Path.Main.Job as PathJob
|
|
import Path.Op.Helix as PathHelix
|
|
|
|
fixture = os.path.join(App.getHomePath(), "Mod", "CAM", "CAMTests", "test_holes00.fcstd")
|
|
App.ConfigSet("SuppressRecomputeRequiredDialog", "True")
|
|
document = App.openDocument(fixture)
|
|
App.ConfigSet("SuppressRecomputeRequiredDialog", "")
|
|
try:
|
|
job = PathJob.Create("HelixOracleJob", [document.Body], None)
|
|
job.Tools.Group[0].Tool.Diameter = 0.9
|
|
operation = PathHelix.Create("OracleHelix", parentJob=job)
|
|
scenarios = []
|
|
expected = [
|
|
("inside-conventional", "Inside", "Conventional", "CW", "G2"),
|
|
("outside-climb", "Outside", "Climb", "CW", "G2"),
|
|
("inside-climb", "Inside", "Climb", "CCW", "G3"),
|
|
("outside-conventional", "Outside", "Conventional", "CCW", "G3"),
|
|
]
|
|
for scenario_id, start_side, cut_mode, expected_direction, expected_arc in expected:
|
|
operation.StartSide = start_side
|
|
operation.CutMode = cut_mode
|
|
operation.enforceRecompute()
|
|
document.recompute()
|
|
commands = [command_record(command, 2) for command in operation.Path.Commands]
|
|
arc_commands = [command for command in commands if command["name"] in ("G2", "G3")]
|
|
arc_names = sorted({command["name"] for command in arc_commands})
|
|
command_histogram = {
|
|
name: sum(1 for command in commands if command["name"] == name)
|
|
for name in sorted({command["name"] for command in commands})
|
|
}
|
|
canonical = json.dumps(commands, sort_keys=True, separators=(",", ":"))
|
|
scenarios.append({
|
|
"id": scenario_id,
|
|
"startSide": str(operation.StartSide),
|
|
"cutMode": str(operation.CutMode),
|
|
"direction": str(operation.Direction),
|
|
"expectedDirection": expected_direction,
|
|
"expectedArc": expected_arc,
|
|
"arcNames": arc_names,
|
|
"arcCommandCount": len(arc_commands),
|
|
"firstArcCommand": arc_commands[0] if arc_commands else None,
|
|
"lastArcCommand": arc_commands[-1] if arc_commands else None,
|
|
"commandCount": len(commands),
|
|
"commandHistogram": command_histogram,
|
|
"commandSha256": hashlib.sha256(canonical.encode("utf-8")).hexdigest(),
|
|
"pathLength": round(float(operation.Path.Length), 2),
|
|
})
|
|
base_sub_elements = sum(len(base[1]) for base in operation.Base)
|
|
return {
|
|
"fixture": "Mod/CAM/CAMTests/test_holes00.fcstd",
|
|
"fixtureSha256": archive_record(fixture)["sha256"],
|
|
"modelTypeId": document.Body.TypeId,
|
|
"jobTypeId": job.TypeId,
|
|
"operationTypeId": operation.TypeId,
|
|
"pathPropertyType": operation.getTypeIdOfProperty("Path"),
|
|
"toolDiameterMm": round(float(job.Tools.Group[0].Tool.Diameter), 3),
|
|
"baseSubElementCount": base_sub_elements,
|
|
"normalizationToleranceMm": 0.01,
|
|
"scenarioCount": len(scenarios),
|
|
"scenarios": scenarios,
|
|
}
|
|
finally:
|
|
App.closeDocument(document.Name)
|
|
|
|
|
|
def create_roundtrip_archives(source_path, mutated_path):
|
|
document = App.newDocument("NativeCamPathRoundTrip")
|
|
native = document.addObject("Path::Feature", "NativePath")
|
|
native.Label = "Native Path Original"
|
|
native.addProperty("App::PropertyString", "OracleTag", "Oracle")
|
|
native.OracleTag = "native-v1"
|
|
python_path = document.addObject("Path::FeaturePython", "PythonPath")
|
|
python_path.Label = "Python Path Original"
|
|
python_path.addProperty("App::PropertyString", "OracleTag", "Oracle")
|
|
python_path.OracleTag = "python-v1"
|
|
|
|
commands = [
|
|
Path.Command("G0", {"X": 0.0, "Y": 0.0, "Z": 5.0}),
|
|
Path.Command("G1", {"X": 0.0, "Y": 0.0, "Z": 0.0, "F": 120.0}),
|
|
Path.Command("G1", {"X": 20.0, "Y": 0.0, "Z": 0.0, "F": 300.0}),
|
|
Path.Command("G2", {"X": 20.0, "Y": 20.0, "I": 0.0, "J": 10.0, "F": 300.0}),
|
|
Path.Command("G0", {"X": 20.0, "Y": 20.0, "Z": 5.0}),
|
|
]
|
|
native.Path = Path.Path(commands)
|
|
python_path.Path = Path.Path([command for command in commands])
|
|
document.recompute()
|
|
document.saveAs(source_path)
|
|
source_before_close = [path_record(native), path_record(python_path)]
|
|
App.closeDocument(document.Name)
|
|
|
|
source_reopened = load_path_objects(source_path)
|
|
mutated = App.openDocument(source_path)
|
|
native_mutated = mutated.getObject("NativePath")
|
|
python_mutated = mutated.getObject("PythonPath")
|
|
native_mutated.Label = "Native Path Mutated"
|
|
native_mutated.OracleTag = "native-v2"
|
|
native_mutated.Path = Path.Path(native_mutated.Path.Commands + [Path.Command("M5")])
|
|
python_mutated.Label = "Python Path Mutated"
|
|
python_mutated.OracleTag = "python-v2"
|
|
python_mutated.Path = Path.Path(python_mutated.Path.Commands + [Path.Command("M2")])
|
|
mutated.recompute()
|
|
mutated.saveAs(mutated_path)
|
|
App.closeDocument(mutated.Name)
|
|
mutated_reopened = load_path_objects(mutated_path)
|
|
|
|
return {
|
|
"source": archive_record(source_path),
|
|
"mutated": archive_record(mutated_path),
|
|
"sourceBeforeClose": source_before_close,
|
|
"sourceReopened": source_reopened,
|
|
"mutatedReopened": mutated_reopened,
|
|
}
|
|
|
|
|
|
preserved_path = os.environ.get("FREECAD_CAM_PATH_VERIFY_ARCHIVE", "")
|
|
if os.environ.get("FREECAD_CAM_PATH_VERIFY_ONLY") == "1":
|
|
if not preserved_path:
|
|
raise RuntimeError("FREECAD_CAM_PATH_VERIFY_ARCHIVE is required in verify-only mode")
|
|
verify_preserved_archive(preserved_path)
|
|
|
|
source_path = os.environ.get("FREECAD_CAM_PATH_SOURCE", "")
|
|
mutated_path = os.environ.get("FREECAD_CAM_PATH_MUTATED", "")
|
|
if not source_path or not mutated_path:
|
|
raise RuntimeError("FREECAD_CAM_PATH_SOURCE and FREECAD_CAM_PATH_MUTATED are required")
|
|
|
|
profile_document, profile_result = profile_algorithm_oracle()
|
|
helix_result = helix_algorithm_oracle()
|
|
App.setActiveDocument(profile_document.Name)
|
|
try:
|
|
qt_result = qt_command_oracle(profile_document, profile_document.Body)
|
|
finally:
|
|
App.closeDocument(profile_document.Name)
|
|
|
|
fcstd_result = create_roundtrip_archives(source_path, mutated_path)
|
|
version = App.Version()
|
|
result = {
|
|
"schemaVersion": 1,
|
|
"baselineId": "freecad-1.1.1",
|
|
"freecadVersion": ".".join(str(value) for value in version[:3]),
|
|
"revision": str(version[3]),
|
|
"gitCommit": str(version[7]) if len(version) > 7 else "",
|
|
"nativeRuntime": {
|
|
"guiUp": bool(App.GuiUp),
|
|
"pathModule": str(getattr(Path, "__file__", "built-in")),
|
|
},
|
|
"profileAlgorithm": profile_result,
|
|
"helixAlgorithm": helix_result,
|
|
"qtDynamicOracle": qt_result,
|
|
"fcstdRoundTrip": fcstd_result,
|
|
}
|
|
emit_marker(result)
|