feat: establish reproducible FreeCAD web compatibility baseline
This commit is contained in:
576
scripts/freecad-cam-path-oracle.py
Normal file
576
scripts/freecad-cam-path-oracle.py
Normal file
@@ -0,0 +1,576 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user