318 lines
12 KiB
Python
318 lines
12 KiB
Python
import json
|
|
|
|
import FreeCAD as App
|
|
import Part
|
|
import Sketcher
|
|
|
|
from freecad_mutation_evidence import capture_property_mutation
|
|
|
|
|
|
def version_text():
|
|
return ".".join(str(value) for value in App.Version()[:3])
|
|
|
|
|
|
def shape_result(case_id, document, feature):
|
|
document.recompute()
|
|
shape = feature.Shape
|
|
return {
|
|
"id": case_id,
|
|
"typeId": feature.TypeId,
|
|
"passed": not shape.isNull() and shape.isValid() and len(shape.Solids) == 1,
|
|
"status": feature.getStatusString(),
|
|
"solids": len(shape.Solids) if not shape.isNull() else 0,
|
|
"faces": len(shape.Faces) if not shape.isNull() else 0,
|
|
"edges": len(shape.Edges) if not shape.isNull() else 0,
|
|
"volume": float(shape.Volume) if not shape.isNull() else 0.0,
|
|
}
|
|
|
|
|
|
def add_rectangle(sketch, x_min, y_min, x_max, y_max):
|
|
points = [(x_min, y_min), (x_max, y_min), (x_max, y_max), (x_min, y_max)]
|
|
for index, start in enumerate(points):
|
|
end = points[(index + 1) % len(points)]
|
|
sketch.addGeometry(Part.LineSegment(App.Vector(start[0], start[1], 0), App.Vector(end[0], end[1], 0)), False)
|
|
|
|
|
|
def feature_linear_pattern():
|
|
document = App.newDocument("FeatureLinearPatternOracle")
|
|
body = document.addObject("PartDesign::Body", "Body")
|
|
base = body.newObject("PartDesign::AdditiveBox", "Base")
|
|
base.Length = 20
|
|
base.Width = 10
|
|
base.Height = 2
|
|
boss = body.newObject("PartDesign::AdditiveBox", "Boss")
|
|
boss.Length = 2
|
|
boss.Width = 2
|
|
boss.Height = 2
|
|
boss.Placement.Base = App.Vector(2, 4, 2)
|
|
document.recompute()
|
|
pattern = body.newObject("PartDesign::LinearPattern", "LinearPattern")
|
|
pattern.TransformMode = "Features"
|
|
pattern.Originals = [boss]
|
|
pattern.Direction = (document.X_Axis, [""])
|
|
pattern.Occurrences = 3
|
|
pattern.Length = 12
|
|
mutation = capture_property_mutation(document, pattern, "Occurrences", 4)
|
|
result = shape_result("linear-feature-list", document, pattern)
|
|
result["mutation"] = mutation
|
|
result.update({
|
|
"transformMode": pattern.TransformMode,
|
|
"originals": [item.Name for item in pattern.Originals],
|
|
"direction": pattern.Direction[0].Name,
|
|
"occurrences": int(pattern.Occurrences),
|
|
"length": float(pattern.Length),
|
|
})
|
|
App.closeDocument(document.Name)
|
|
return result
|
|
|
|
|
|
def feature_mirrored():
|
|
document = App.newDocument("FeatureMirroredOracle")
|
|
body = document.addObject("PartDesign::Body", "Body")
|
|
base = body.newObject("PartDesign::AdditiveBox", "Base")
|
|
base.Length = 20
|
|
base.Width = 10
|
|
base.Height = 2
|
|
boss = body.newObject("PartDesign::AdditiveBox", "Boss")
|
|
boss.Length = 2
|
|
boss.Width = 2
|
|
boss.Height = 2
|
|
boss.Placement.Base = App.Vector(0, 4, 2)
|
|
document.recompute()
|
|
mirrored = body.newObject("PartDesign::Mirrored", "Mirrored")
|
|
mirrored.TransformMode = "Features"
|
|
mirrored.Originals = [boss]
|
|
mirrored.MirrorPlane = (document.YZ_Plane, [""])
|
|
mutation = capture_property_mutation(document, mirrored, "Refine", False)
|
|
result = shape_result("mirrored-feature-list", document, mirrored)
|
|
result["mutation"] = mutation
|
|
result.update({
|
|
"transformMode": mirrored.TransformMode,
|
|
"originals": [item.Name for item in mirrored.Originals],
|
|
"plane": mirrored.MirrorPlane[0].Name,
|
|
})
|
|
App.closeDocument(document.Name)
|
|
return result
|
|
|
|
|
|
def feature_polar_pattern():
|
|
document = App.newDocument("FeaturePolarPatternOracle")
|
|
body = document.addObject("PartDesign::Body", "Body")
|
|
box = document.addObject("PartDesign::AdditiveBox", "Box")
|
|
body.addObject(box)
|
|
box.Length = 10
|
|
box.Width = 10
|
|
box.Height = 10
|
|
document.recompute()
|
|
pattern = document.addObject("PartDesign::PolarPattern", "PolarPattern")
|
|
pattern.Originals = [box]
|
|
pattern.Axis = (document.X_Axis, [""])
|
|
pattern.Angle = 360
|
|
pattern.Occurrences = 4
|
|
pattern.Refine = True
|
|
body.addObject(pattern)
|
|
mutation = capture_property_mutation(document, pattern, "Occurrences", 3)
|
|
result = shape_result("polar-feature-list", document, pattern)
|
|
result["mutation"] = mutation
|
|
result.update({
|
|
"transformMode": pattern.TransformMode,
|
|
"originals": [item.Name for item in pattern.Originals],
|
|
"axis": pattern.Axis[0].Name,
|
|
"angle": float(pattern.Angle),
|
|
"occurrences": int(pattern.Occurrences),
|
|
})
|
|
App.closeDocument(document.Name)
|
|
return result
|
|
|
|
|
|
def feature_multi_transform():
|
|
document = App.newDocument("FeatureMultiTransformOracle")
|
|
body = document.addObject("PartDesign::Body", "Body")
|
|
sketch = document.addObject("Sketcher::SketchObject", "SketchPad")
|
|
body.addObject(sketch)
|
|
add_rectangle(sketch, 0, 0, 10, 10)
|
|
document.recompute()
|
|
pad = document.addObject("PartDesign::Pad", "Pad")
|
|
body.addObject(pad)
|
|
pad.Profile = sketch
|
|
pad.Length = 10
|
|
document.recompute()
|
|
multi = document.addObject("PartDesign::MultiTransform", "MultiTransform")
|
|
document.recompute()
|
|
multi.Originals = [pad]
|
|
multi.Shape = pad.Shape
|
|
body.addObject(multi)
|
|
document.recompute()
|
|
mirrored = document.addObject("PartDesign::Mirrored", "Mirrored")
|
|
mirrored.MirrorPlane = (sketch, ["H_Axis"])
|
|
body.addObject(mirrored)
|
|
linear = document.addObject("PartDesign::LinearPattern", "LinearPattern")
|
|
linear.Direction = (sketch, ["H_Axis"])
|
|
linear.Length = 20
|
|
linear.Occurrences = 3
|
|
body.addObject(linear)
|
|
polar = document.addObject("PartDesign::PolarPattern", "PolarPattern")
|
|
polar.Axis = (sketch, ["N_Axis"])
|
|
polar.Angle = 360
|
|
polar.Occurrences = 4
|
|
body.addObject(polar)
|
|
multi.Transformations = [mirrored, linear, polar]
|
|
mutation = capture_property_mutation(document, multi, "Length", 15.0, target=linear, property_path="Transformations[1].Length")
|
|
result = shape_result("multi-transform-feature-list", document, multi)
|
|
result["mutation"] = mutation
|
|
result.update({
|
|
"transformMode": multi.TransformMode,
|
|
"originals": [item.Name for item in multi.Originals],
|
|
"transformations": [item.TypeId for item in multi.Transformations],
|
|
})
|
|
App.closeDocument(document.Name)
|
|
return result
|
|
|
|
|
|
def transform_failure(type_id):
|
|
suffix = type_id.split("::", 1)[1]
|
|
document = App.newDocument("Feature{}FailureOracle".format(suffix))
|
|
try:
|
|
body = document.addObject("PartDesign::Body", "Body")
|
|
base = body.newObject("PartDesign::AdditiveBox", "Base")
|
|
base.Length = 20
|
|
base.Width = 10
|
|
base.Height = 2
|
|
boss = body.newObject("PartDesign::AdditiveBox", "Boss")
|
|
boss.Length = 2
|
|
boss.Width = 2
|
|
boss.Height = 2
|
|
boss.Placement.Base = App.Vector(2, 4, 2)
|
|
document.recompute()
|
|
feature = document.addObject(type_id, suffix)
|
|
feature.Originals = [boss]
|
|
if type_id == "PartDesign::MultiTransform":
|
|
feature.Shape = boss.Shape
|
|
body.addObject(feature)
|
|
if type_id == "PartDesign::LinearPattern":
|
|
feature.Direction = (base, ["Edge999"])
|
|
if type_id == "PartDesign::MultiTransform":
|
|
child = document.addObject("PartDesign::LinearPattern", "InvalidLinearChild")
|
|
child.Length = 12
|
|
child.Occurrences = 3
|
|
child.Direction = (base, ["Edge999"])
|
|
body.addObject(child)
|
|
feature.Transformations = [child]
|
|
document.recompute()
|
|
shape = feature.Shape
|
|
shape_null = bool(shape.isNull())
|
|
state = [str(value) for value in feature.State]
|
|
status_string = str(feature.getStatusString())
|
|
invalid = "Invalid" in state or status_string != "Valid"
|
|
observed = "rejected" if invalid else "accepted-empty" if shape_null else "accepted"
|
|
return {
|
|
"id": "{}-missing-reference".format(suffix.lower()),
|
|
"typeId": type_id,
|
|
"inputClass": "valid-original-missing-transform-reference",
|
|
"expected": "rejected",
|
|
"observed": observed,
|
|
"passed": observed == "rejected",
|
|
"shapeNull": shape_null,
|
|
"staleShapePreserved": bool(invalid and not shape_null),
|
|
"state": state,
|
|
"status": status_string,
|
|
"originals": [item.Name for item in feature.Originals],
|
|
"transformations": [item.TypeId for item in feature.Transformations] if type_id == "PartDesign::MultiTransform" else [],
|
|
}
|
|
except Exception as error:
|
|
return {
|
|
"id": "{}-missing-reference".format(suffix.lower()),
|
|
"typeId": type_id,
|
|
"inputClass": "valid-original-missing-transform-reference",
|
|
"expected": "rejected",
|
|
"observed": "rejected",
|
|
"passed": True,
|
|
"errorType": type(error).__name__,
|
|
"error": str(error),
|
|
}
|
|
finally:
|
|
App.closeDocument(document.Name)
|
|
|
|
|
|
def hole_document(name):
|
|
document = App.newDocument(name)
|
|
body = document.addObject("PartDesign::Body", "Body")
|
|
box = body.newObject("PartDesign::AdditiveBox", "Box")
|
|
box.Length = 20
|
|
box.Width = 20
|
|
box.Height = 10
|
|
document.recompute()
|
|
top_face = max(range(len(box.Shape.Faces)), key=lambda index: box.Shape.Faces[index].CenterOfMass.z)
|
|
sketch = body.newObject("Sketcher::SketchObject", "HoleSketch")
|
|
sketch.AttachmentSupport = (box, ["Face{}".format(top_face + 1)])
|
|
sketch.MapMode = "FlatFace"
|
|
sketch.addGeometry(Part.Circle(App.Vector(10, 10, 0), App.Vector(0, 0, 1), 3), False)
|
|
hole = body.newObject("PartDesign::Hole", "Hole")
|
|
hole.Profile = sketch
|
|
hole.DepthType = "Dimension"
|
|
hole.Depth = 8
|
|
hole.DrillPoint = "Flat"
|
|
return document, body, hole
|
|
|
|
|
|
def iso_hole(modeled):
|
|
case_id = "iso-m6-modeled-thread" if modeled else "iso-m6-tap-drill"
|
|
document, body, hole = hole_document("ModeledThreadOracle" if modeled else "TapDrillOracle")
|
|
hole.ThreadType = "ISOMetricProfile"
|
|
hole.ThreadSize = "M6x1.0"
|
|
hole.Threaded = True
|
|
hole.ModelThread = modeled
|
|
hole.ThreadDirection = "Left" if modeled else "Right"
|
|
hole.ThreadDepthType = "Dimension"
|
|
hole.ThreadDepth = 6
|
|
mutation = capture_property_mutation(document, hole, "Diameter", 6.5)
|
|
result = shape_result(case_id, document, hole)
|
|
result["mutation"] = mutation
|
|
result.update({
|
|
"threaded": bool(hole.Threaded),
|
|
"modeled": bool(hole.ModelThread),
|
|
"threadType": hole.ThreadType,
|
|
"threadSize": hole.ThreadSize,
|
|
"threadClass": hole.ThreadClass,
|
|
"threadFit": hole.ThreadFit,
|
|
"threadDirection": hole.ThreadDirection,
|
|
"threadDepthType": hole.ThreadDepthType,
|
|
"threadDepth": float(hole.ThreadDepth),
|
|
"diameter": float(hole.Diameter),
|
|
"threadDiameter": float(hole.ThreadDiameter),
|
|
"threadPitch": float(hole.ThreadPitch) if "ThreadPitch" in hole.PropertiesList else 1.0,
|
|
"properties": list(hole.PropertiesList),
|
|
})
|
|
App.closeDocument(document.Name)
|
|
return result
|
|
|
|
|
|
success_cases = [feature_linear_pattern(), feature_mirrored(), feature_polar_pattern(), feature_multi_transform(), iso_hole(False), iso_hole(True)]
|
|
failure_cases = [transform_failure(type_id) for type_id in [
|
|
"PartDesign::Mirrored",
|
|
"PartDesign::MultiTransform",
|
|
"PartDesign::LinearPattern",
|
|
"PartDesign::PolarPattern",
|
|
]]
|
|
cases = success_cases + failure_cases
|
|
report = {
|
|
"schemaVersion": 1,
|
|
"baselineId": "freecad-1.1.1-partdesign-transform-oracle",
|
|
"freecadVersion": version_text(),
|
|
"gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d",
|
|
"status": "pass" if all(case["passed"] for case in cases) else "failed",
|
|
"tolerance": 1e-7,
|
|
"cases": cases,
|
|
"summary": {
|
|
"cases": len(cases),
|
|
"passed": sum(1 for case in cases if case["passed"]),
|
|
"successCases": len(success_cases),
|
|
"successPassed": sum(1 for case in success_cases if case["passed"]),
|
|
"failureCases": len(failure_cases),
|
|
"failurePassed": sum(1 for case in failure_cases if case["passed"]),
|
|
"rejected": sum(1 for case in failure_cases if case.get("observed") == "rejected"),
|
|
"acceptedEmpty": sum(1 for case in failure_cases if case.get("observed") == "accepted-empty"),
|
|
},
|
|
}
|
|
print("FREECAD_PARTDESIGN_TRANSFORM_RESULT=" + json.dumps(report, sort_keys=True, separators=(",", ":")))
|