|
|
|
|
@ -57,9 +57,15 @@ def _install_fake_freecad():
|
|
|
|
|
)
|
|
|
|
|
sys.modules["FreeCAD"] = fake_freecad
|
|
|
|
|
|
|
|
|
|
selection_state = {"selection": [], "selection_ex": []}
|
|
|
|
|
selection_state = {
|
|
|
|
|
"selection": [],
|
|
|
|
|
"selection_ex": [],
|
|
|
|
|
"commands": [],
|
|
|
|
|
"control_events": [],
|
|
|
|
|
}
|
|
|
|
|
fake_freecadgui = types.ModuleType("FreeCADGui")
|
|
|
|
|
fake_freecadgui.addCommand = lambda *args, **kwargs: None
|
|
|
|
|
fake_freecadgui.runCommand = lambda command: selection_state["commands"].append(command)
|
|
|
|
|
fake_freecadgui.SendMsgToActiveView = lambda *args, **kwargs: None
|
|
|
|
|
def clear_selection():
|
|
|
|
|
selection_state["selection"] = []
|
|
|
|
|
@ -76,7 +82,7 @@ def _install_fake_freecad():
|
|
|
|
|
fake_freecadgui.Control = types.SimpleNamespace(
|
|
|
|
|
activeDialog=lambda: False,
|
|
|
|
|
showDialog=lambda panel: panel,
|
|
|
|
|
closeDialog=lambda: None,
|
|
|
|
|
closeDialog=lambda: selection_state["control_events"].append("closeDialog"),
|
|
|
|
|
)
|
|
|
|
|
sys.modules["FreeCADGui"] = fake_freecadgui
|
|
|
|
|
|
|
|
|
|
@ -643,6 +649,58 @@ class ManualWiringPanelTest(unittest.TestCase):
|
|
|
|
|
self.assertEqual(400.0, getattr(carrier, "QetCarrierLength", None))
|
|
|
|
|
self.assertEqual(2.0, getattr(carrier, "QetCarrierScaleX", None))
|
|
|
|
|
|
|
|
|
|
def test_controller_refreshes_face_contact_mount_when_changing_carrier_length(self):
|
|
|
|
|
selection_state = _install_fake_freecad()
|
|
|
|
|
terminal_objects, panel = _reload_modules()
|
|
|
|
|
app = sys.modules["FreeCAD"]
|
|
|
|
|
|
|
|
|
|
doc = FakeDocument()
|
|
|
|
|
app.ActiveDocument = doc
|
|
|
|
|
terminal_objects.ensure_root_group(doc, "project-1")
|
|
|
|
|
cabinet = doc.addObject("Part::Feature", "CabinetPanel")
|
|
|
|
|
carrier = doc.addObject("App::DocumentObjectGroup", "WireDuctCarrier")
|
|
|
|
|
cabinet.Placement = app.Placement(app.Vector(100, 0, 0), app.Rotation())
|
|
|
|
|
carrier.Placement = app.Placement(app.Vector(120, 0, 5), app.Rotation())
|
|
|
|
|
terminal_objects.ensure_string_property(
|
|
|
|
|
carrier,
|
|
|
|
|
"QetCarrierKind",
|
|
|
|
|
"QET Wiring",
|
|
|
|
|
"Carrier kind",
|
|
|
|
|
"wire_duct",
|
|
|
|
|
)
|
|
|
|
|
carrier.addProperty("App::PropertyFloat", "QetCarrierBaseLength", "QET Wiring", "Base length")
|
|
|
|
|
carrier.QetCarrierBaseLength = 200.0
|
|
|
|
|
terminal_objects.ensure_string_property(
|
|
|
|
|
carrier,
|
|
|
|
|
"QetMountMode",
|
|
|
|
|
"QET Assembly",
|
|
|
|
|
"QET cabinet assembly mount metadata",
|
|
|
|
|
"face_contact",
|
|
|
|
|
)
|
|
|
|
|
terminal_objects.ensure_string_property(
|
|
|
|
|
carrier,
|
|
|
|
|
"QetMountHostName",
|
|
|
|
|
"QET Assembly",
|
|
|
|
|
"QET cabinet assembly mount metadata",
|
|
|
|
|
"CabinetPanel",
|
|
|
|
|
)
|
|
|
|
|
terminal_objects.ensure_string_property(
|
|
|
|
|
carrier,
|
|
|
|
|
"QetMountLocalBaseJson",
|
|
|
|
|
"QET Assembly",
|
|
|
|
|
"QET cabinet assembly local base offset",
|
|
|
|
|
json.dumps({"x": 20.0, "y": 0.0, "z": 5.0}, ensure_ascii=False),
|
|
|
|
|
)
|
|
|
|
|
cabinet.Placement = app.Placement(app.Vector(130, 0, 0), app.Rotation())
|
|
|
|
|
selection_state["selection"] = [carrier]
|
|
|
|
|
|
|
|
|
|
updated = panel.ManualWiringController().apply_length_to_selected_carriers(500.0)
|
|
|
|
|
|
|
|
|
|
self.assertEqual([carrier], updated)
|
|
|
|
|
self.assertEqual(500.0, carrier.QetCarrierLength)
|
|
|
|
|
self.assertEqual((150.0, 0.0, 5.0), (carrier.Placement.Base.x, carrier.Placement.Base.y, carrier.Placement.Base.z))
|
|
|
|
|
self.assertEqual({"x": 130.0, "y": 0.0, "z": 0.0}, json.loads(carrier.QetMountHostBaseJson))
|
|
|
|
|
|
|
|
|
|
def test_controller_auto_marks_selected_wire_duct_by_name_before_length_change(self):
|
|
|
|
|
selection_state = _install_fake_freecad()
|
|
|
|
|
terminal_objects, panel = _reload_modules()
|
|
|
|
|
@ -797,6 +855,160 @@ class ManualWiringPanelTest(unittest.TestCase):
|
|
|
|
|
self.assertEqual("Face2", rail.QetMountContactSubElement)
|
|
|
|
|
self.assertEqual({"x": 0.0, "y": 0.0, "z": 0.0}, json.loads(rail.QetMountHostBaseJson))
|
|
|
|
|
self.assertEqual({"x": 0.0, "y": 0.0, "z": 1.0}, json.loads(rail.QetMountLocalBaseJson))
|
|
|
|
|
self.assertEqual({"x": 0.0, "y": 0.0, "z": 1.0}, json.loads(rail.QetMountHostNormalJson))
|
|
|
|
|
|
|
|
|
|
def test_controller_aligns_contact_faces_with_configured_normal_offset(self):
|
|
|
|
|
selection_state = _install_fake_freecad()
|
|
|
|
|
terminal_objects, panel = _reload_modules()
|
|
|
|
|
app = sys.modules["FreeCAD"]
|
|
|
|
|
|
|
|
|
|
doc = FakeDocument()
|
|
|
|
|
app.ActiveDocument = doc
|
|
|
|
|
terminal_objects.ensure_root_group(doc, "project-1")
|
|
|
|
|
cabinet = doc.addObject("Part::Feature", "CabinetPanel")
|
|
|
|
|
rail = doc.addObject("Part::Feature", "DINRail")
|
|
|
|
|
rail.Placement = app.Placement(app.Vector(0, 0, 10), app.Rotation())
|
|
|
|
|
target_face = types.SimpleNamespace(
|
|
|
|
|
ShapeType="Face",
|
|
|
|
|
normalAt=lambda u, v: app.Vector(0, 0, 1),
|
|
|
|
|
)
|
|
|
|
|
moving_face = types.SimpleNamespace(
|
|
|
|
|
ShapeType="Face",
|
|
|
|
|
normalAt=lambda u, v: app.Vector(0, 0, -1),
|
|
|
|
|
)
|
|
|
|
|
selection_state["selection_ex"] = [
|
|
|
|
|
types.SimpleNamespace(
|
|
|
|
|
PickedPoints=[app.Vector(100, 20, 0)],
|
|
|
|
|
SubObjects=[target_face],
|
|
|
|
|
SubElementNames=["Face1"],
|
|
|
|
|
Object=cabinet,
|
|
|
|
|
),
|
|
|
|
|
types.SimpleNamespace(
|
|
|
|
|
PickedPoints=[app.Vector(5, 6, 9)],
|
|
|
|
|
SubObjects=[moving_face],
|
|
|
|
|
SubElementNames=["Face2"],
|
|
|
|
|
Object=rail,
|
|
|
|
|
),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
result = panel.ManualWiringController(contact_offset_mm=2.0).align_selected_contact_faces()
|
|
|
|
|
|
|
|
|
|
self.assertIs(rail, result["moving_object"])
|
|
|
|
|
self.assertEqual((0.0, 0.0, 3.0), (rail.Placement.Base.x, rail.Placement.Base.y, rail.Placement.Base.z))
|
|
|
|
|
self.assertEqual((-0.0, -0.0, -7.0), (result["translation"].x, result["translation"].y, result["translation"].z))
|
|
|
|
|
self.assertEqual(2.0, result["contact_offset_mm"])
|
|
|
|
|
self.assertEqual(2.0, rail.QetMountOffsetMm)
|
|
|
|
|
|
|
|
|
|
def test_controller_applies_contact_offset_to_selected_mounted_object(self):
|
|
|
|
|
selection_state = _install_fake_freecad()
|
|
|
|
|
terminal_objects, panel = _reload_modules()
|
|
|
|
|
app = sys.modules["FreeCAD"]
|
|
|
|
|
|
|
|
|
|
doc = FakeDocument()
|
|
|
|
|
app.ActiveDocument = doc
|
|
|
|
|
terminal_objects.ensure_root_group(doc, "project-1")
|
|
|
|
|
cabinet = doc.addObject("Part::Feature", "CabinetPanel")
|
|
|
|
|
rail = doc.addObject("Part::Feature", "DINRail")
|
|
|
|
|
rail.Placement = app.Placement(app.Vector(0, 0, 10), app.Rotation())
|
|
|
|
|
target_face = types.SimpleNamespace(
|
|
|
|
|
ShapeType="Face",
|
|
|
|
|
normalAt=lambda u, v: app.Vector(0, 0, 1),
|
|
|
|
|
)
|
|
|
|
|
moving_face = types.SimpleNamespace(
|
|
|
|
|
ShapeType="Face",
|
|
|
|
|
normalAt=lambda u, v: app.Vector(0, 0, -1),
|
|
|
|
|
)
|
|
|
|
|
selection_state["selection_ex"] = [
|
|
|
|
|
types.SimpleNamespace(
|
|
|
|
|
PickedPoints=[app.Vector(100, 20, 0)],
|
|
|
|
|
SubObjects=[target_face],
|
|
|
|
|
SubElementNames=["Face1"],
|
|
|
|
|
Object=cabinet,
|
|
|
|
|
),
|
|
|
|
|
types.SimpleNamespace(
|
|
|
|
|
PickedPoints=[app.Vector(5, 6, 9)],
|
|
|
|
|
SubObjects=[moving_face],
|
|
|
|
|
SubElementNames=["Face2"],
|
|
|
|
|
Object=rail,
|
|
|
|
|
),
|
|
|
|
|
]
|
|
|
|
|
controller = panel.ManualWiringController()
|
|
|
|
|
controller.align_selected_contact_faces()
|
|
|
|
|
selection_state["selection"] = [rail]
|
|
|
|
|
|
|
|
|
|
updated = controller.apply_contact_offset_to_selected_mounts(5.0)
|
|
|
|
|
|
|
|
|
|
self.assertEqual([rail], updated)
|
|
|
|
|
self.assertEqual((0.0, 0.0, 6.0), (rail.Placement.Base.x, rail.Placement.Base.y, rail.Placement.Base.z))
|
|
|
|
|
self.assertEqual(5.0, rail.QetMountOffsetMm)
|
|
|
|
|
self.assertEqual({"x": 0.0, "y": 0.0, "z": 6.0}, json.loads(rail.QetMountLocalBaseJson))
|
|
|
|
|
|
|
|
|
|
def test_controller_reverses_contact_normal_for_selected_mounted_object(self):
|
|
|
|
|
selection_state = _install_fake_freecad()
|
|
|
|
|
terminal_objects, panel = _reload_modules()
|
|
|
|
|
app = sys.modules["FreeCAD"]
|
|
|
|
|
|
|
|
|
|
doc = FakeDocument()
|
|
|
|
|
app.ActiveDocument = doc
|
|
|
|
|
terminal_objects.ensure_root_group(doc, "project-1")
|
|
|
|
|
cabinet = doc.addObject("Part::Feature", "CabinetPanel")
|
|
|
|
|
rail = doc.addObject("Part::Feature", "DINRail")
|
|
|
|
|
rail.Placement = app.Placement(app.Vector(0, 0, 10), app.Rotation())
|
|
|
|
|
target_face = types.SimpleNamespace(
|
|
|
|
|
ShapeType="Face",
|
|
|
|
|
normalAt=lambda u, v: app.Vector(0, 0, 1),
|
|
|
|
|
)
|
|
|
|
|
moving_face = types.SimpleNamespace(
|
|
|
|
|
ShapeType="Face",
|
|
|
|
|
normalAt=lambda u, v: app.Vector(0, 0, -1),
|
|
|
|
|
)
|
|
|
|
|
selection_state["selection_ex"] = [
|
|
|
|
|
types.SimpleNamespace(
|
|
|
|
|
PickedPoints=[app.Vector(100, 20, 0)],
|
|
|
|
|
SubObjects=[target_face],
|
|
|
|
|
SubElementNames=["Face1"],
|
|
|
|
|
Object=cabinet,
|
|
|
|
|
),
|
|
|
|
|
types.SimpleNamespace(
|
|
|
|
|
PickedPoints=[app.Vector(5, 6, 9)],
|
|
|
|
|
SubObjects=[moving_face],
|
|
|
|
|
SubElementNames=["Face2"],
|
|
|
|
|
Object=rail,
|
|
|
|
|
),
|
|
|
|
|
]
|
|
|
|
|
controller = panel.ManualWiringController()
|
|
|
|
|
controller.align_selected_contact_faces()
|
|
|
|
|
selection_state["selection"] = [rail]
|
|
|
|
|
|
|
|
|
|
reversed_objects = controller.reverse_contact_normal_for_selected_mounts()
|
|
|
|
|
updated = controller.apply_contact_offset_to_selected_mounts(5.0)
|
|
|
|
|
|
|
|
|
|
self.assertEqual([rail], reversed_objects)
|
|
|
|
|
self.assertEqual([rail], updated)
|
|
|
|
|
self.assertEqual({"x": -0.0, "y": -0.0, "z": -1.0}, json.loads(rail.QetMountHostNormalJson))
|
|
|
|
|
self.assertEqual((0.0, 0.0, -4.0), (rail.Placement.Base.x, rail.Placement.Base.y, rail.Placement.Base.z))
|
|
|
|
|
|
|
|
|
|
def test_controller_requires_saved_contact_normal_before_applying_offset(self):
|
|
|
|
|
selection_state = _install_fake_freecad()
|
|
|
|
|
terminal_objects, panel = _reload_modules()
|
|
|
|
|
app = sys.modules["FreeCAD"]
|
|
|
|
|
|
|
|
|
|
doc = FakeDocument()
|
|
|
|
|
app.ActiveDocument = doc
|
|
|
|
|
terminal_objects.ensure_root_group(doc, "project-1")
|
|
|
|
|
rail = doc.addObject("Part::Feature", "DINRail")
|
|
|
|
|
rail.Placement = app.Placement(app.Vector(0, 0, 1), app.Rotation())
|
|
|
|
|
terminal_objects.ensure_string_property(
|
|
|
|
|
rail,
|
|
|
|
|
"QetMountMode",
|
|
|
|
|
"QET Assembly",
|
|
|
|
|
"QET cabinet assembly mount metadata",
|
|
|
|
|
"face_contact",
|
|
|
|
|
)
|
|
|
|
|
selection_state["selection"] = [rail]
|
|
|
|
|
|
|
|
|
|
with self.assertRaisesRegex(panel.ManualWiringPanelError, "贴合法向"):
|
|
|
|
|
panel.ManualWiringController().apply_contact_offset_to_selected_mounts(3.0)
|
|
|
|
|
|
|
|
|
|
def test_refresh_mount_hosted_objects_moves_child_by_host_delta(self):
|
|
|
|
|
_install_fake_freecad()
|
|
|
|
|
@ -999,6 +1211,97 @@ class ManualWiringPanelTest(unittest.TestCase):
|
|
|
|
|
self.assertEqual((0.0, 0.0, 0.0), (child.Placement.Base.x, child.Placement.Base.y, child.Placement.Base.z))
|
|
|
|
|
self.assertEqual([device], selection_state["selection"])
|
|
|
|
|
|
|
|
|
|
def test_controller_rejects_multiple_moving_faces_after_target_face_is_set(self):
|
|
|
|
|
selection_state = _install_fake_freecad()
|
|
|
|
|
terminal_objects, panel = _reload_modules()
|
|
|
|
|
app = sys.modules["FreeCAD"]
|
|
|
|
|
|
|
|
|
|
doc = FakeDocument()
|
|
|
|
|
app.ActiveDocument = doc
|
|
|
|
|
terminal_objects.ensure_root_group(doc, "project-1")
|
|
|
|
|
cabinet = doc.addObject("Part::Feature", "CabinetPanel")
|
|
|
|
|
rail = doc.addObject("Part::Feature", "DINRail")
|
|
|
|
|
|
|
|
|
|
target_face = types.SimpleNamespace(
|
|
|
|
|
ShapeType="Face",
|
|
|
|
|
normalAt=lambda u, v: app.Vector(0, 0, 1),
|
|
|
|
|
)
|
|
|
|
|
moving_face_a = types.SimpleNamespace(
|
|
|
|
|
ShapeType="Face",
|
|
|
|
|
normalAt=lambda u, v: app.Vector(0, 0, -1),
|
|
|
|
|
)
|
|
|
|
|
moving_face_b = types.SimpleNamespace(
|
|
|
|
|
ShapeType="Face",
|
|
|
|
|
normalAt=lambda u, v: app.Vector(0, 0, -1),
|
|
|
|
|
)
|
|
|
|
|
controller = panel.ManualWiringController()
|
|
|
|
|
selection_state["selection_ex"] = [
|
|
|
|
|
types.SimpleNamespace(
|
|
|
|
|
PickedPoints=[app.Vector(0, 0, 0)],
|
|
|
|
|
SubObjects=[target_face],
|
|
|
|
|
SubElementNames=["Face1"],
|
|
|
|
|
Object=cabinet,
|
|
|
|
|
)
|
|
|
|
|
]
|
|
|
|
|
controller.set_contact_target_from_selection()
|
|
|
|
|
|
|
|
|
|
selection_state["selection_ex"] = [
|
|
|
|
|
types.SimpleNamespace(
|
|
|
|
|
PickedPoints=[app.Vector(0, 0, 9), app.Vector(0, 1, 9)],
|
|
|
|
|
SubObjects=[moving_face_a, moving_face_b],
|
|
|
|
|
SubElementNames=["Face2", "Face3"],
|
|
|
|
|
Object=rail,
|
|
|
|
|
)
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
with self.assertRaisesRegex(panel.ManualWiringPanelError, "只选择一个"):
|
|
|
|
|
controller.align_selected_contact_faces()
|
|
|
|
|
|
|
|
|
|
def test_controller_moves_plain_app_part_parent_when_selected_face_belongs_to_child_shape(self):
|
|
|
|
|
selection_state = _install_fake_freecad()
|
|
|
|
|
terminal_objects, panel = _reload_modules()
|
|
|
|
|
app = sys.modules["FreeCAD"]
|
|
|
|
|
|
|
|
|
|
doc = FakeDocument()
|
|
|
|
|
app.ActiveDocument = doc
|
|
|
|
|
terminal_objects.ensure_root_group(doc, "project-1")
|
|
|
|
|
cabinet = doc.addObject("Part::Feature", "CabinetPanel")
|
|
|
|
|
assembly = doc.addObject("App::Part", "ImportedFcstdDevice")
|
|
|
|
|
child = doc.addObject("Part::Feature", "ImportedFcstdBody")
|
|
|
|
|
assembly.Placement = app.Placement(app.Vector(0, 0, 10), app.Rotation())
|
|
|
|
|
child.Placement = app.Placement(app.Vector(0, 0, 0), app.Rotation())
|
|
|
|
|
assembly.addObject(child)
|
|
|
|
|
|
|
|
|
|
target_face = types.SimpleNamespace(
|
|
|
|
|
ShapeType="Face",
|
|
|
|
|
normalAt=lambda u, v: app.Vector(0, 0, 1),
|
|
|
|
|
)
|
|
|
|
|
moving_face = types.SimpleNamespace(
|
|
|
|
|
ShapeType="Face",
|
|
|
|
|
normalAt=lambda u, v: app.Vector(0, 0, -1),
|
|
|
|
|
)
|
|
|
|
|
selection_state["selection_ex"] = [
|
|
|
|
|
types.SimpleNamespace(
|
|
|
|
|
PickedPoints=[app.Vector(0, 0, 0)],
|
|
|
|
|
SubObjects=[target_face],
|
|
|
|
|
SubElementNames=["Face1"],
|
|
|
|
|
Object=cabinet,
|
|
|
|
|
),
|
|
|
|
|
types.SimpleNamespace(
|
|
|
|
|
PickedPoints=[app.Vector(0, 0, 9)],
|
|
|
|
|
SubObjects=[moving_face],
|
|
|
|
|
SubElementNames=["Face2"],
|
|
|
|
|
Object=child,
|
|
|
|
|
),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
result = panel.ManualWiringController().align_selected_contact_faces()
|
|
|
|
|
|
|
|
|
|
self.assertIs(assembly, result["moving_object"])
|
|
|
|
|
self.assertEqual((0.0, 0.0, 1.0), (assembly.Placement.Base.x, assembly.Placement.Base.y, assembly.Placement.Base.z))
|
|
|
|
|
self.assertEqual((0.0, 0.0, 0.0), (child.Placement.Base.x, child.Placement.Base.y, child.Placement.Base.z))
|
|
|
|
|
self.assertEqual([assembly], selection_state["selection"])
|
|
|
|
|
|
|
|
|
|
def test_controller_requires_two_faces_for_contact_alignment(self):
|
|
|
|
|
selection_state = _install_fake_freecad()
|
|
|
|
|
terminal_objects, panel = _reload_modules()
|
|
|
|
|
@ -1558,6 +1861,39 @@ class ManualWiringPanelTest(unittest.TestCase):
|
|
|
|
|
|
|
|
|
|
self.assertEqual(("undo", ""), doc.transactions[-1])
|
|
|
|
|
|
|
|
|
|
def test_controller_closes_panel_and_launches_native_transform(self):
|
|
|
|
|
selection_state = _install_fake_freecad()
|
|
|
|
|
terminal_objects, panel = _reload_modules()
|
|
|
|
|
app = sys.modules["FreeCAD"]
|
|
|
|
|
|
|
|
|
|
doc = FakeDocument()
|
|
|
|
|
app.ActiveDocument = doc
|
|
|
|
|
terminal_objects.ensure_root_group(doc, "project-1")
|
|
|
|
|
device = doc.addObject("Part::Feature", "Device")
|
|
|
|
|
selection_state["selection"] = [device]
|
|
|
|
|
|
|
|
|
|
result = panel.ManualWiringController().launch_native_transform_for_selection()
|
|
|
|
|
|
|
|
|
|
self.assertTrue(result)
|
|
|
|
|
self.assertEqual(["closeDialog"], selection_state["control_events"])
|
|
|
|
|
self.assertEqual(["Std_TransformManip"], selection_state["commands"])
|
|
|
|
|
|
|
|
|
|
def test_controller_requires_selection_before_native_transform(self):
|
|
|
|
|
selection_state = _install_fake_freecad()
|
|
|
|
|
terminal_objects, panel = _reload_modules()
|
|
|
|
|
app = sys.modules["FreeCAD"]
|
|
|
|
|
|
|
|
|
|
doc = FakeDocument()
|
|
|
|
|
app.ActiveDocument = doc
|
|
|
|
|
terminal_objects.ensure_root_group(doc, "project-1")
|
|
|
|
|
selection_state["selection"] = []
|
|
|
|
|
|
|
|
|
|
with self.assertRaisesRegex(panel.ManualWiringPanelError, "请选择要变换的对象"):
|
|
|
|
|
panel.ManualWiringController().launch_native_transform_for_selection()
|
|
|
|
|
|
|
|
|
|
self.assertEqual([], selection_state["control_events"])
|
|
|
|
|
self.assertEqual([], selection_state["commands"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
unittest.main()
|
|
|
|
|
|