feat: 清理失效源对象布线载体

dev
Zhaowenlong 3 weeks ago
parent 57fd53dec1
commit 4dd33c0365

@ -143,6 +143,16 @@ QetProjectUuid = <project_uuid>
Points = [Vector, Vector, ...]
```
由线槽、安装板/柜面、过线孔等源对象自动生成的 carrier 还会记录来源:
```text
QetRouteSourceName = <FreeCAD source object name>
QetRouteSourceLabel = <FreeCAD source object label>
QetRouteSourceKind = "WireDuct" | "RoutingRange" | "WiringCutOut"
```
这些属性只用于 FreeCAD 文档内部刷新和清理,不写入数据库,也不要求 QET 提供。
carrier 统一放在:
```text
@ -324,6 +334,8 @@ src/Mod/FreeCADExchange/InitGui.py
生成线槽 carrier 时,系统除了 `WireDuct` 中心路径,还会在线槽两端生成 `WireDuctOpenEnd` 横向路径;对象名或标签包含 `Wiring Cut-Out`、`wire cutout`、`穿线孔`、`过线孔` 等语义时,会生成 `WiringCutOut` 穿线路径载体。
自动生成的 carrier 会随源对象生命周期刷新:源对象仍有效时更新几何;安装板尺寸变化时同步增删 `RoutingRange` 网格线;源对象被删除或不再满足线槽/支撑面规则时,下一次生成布线路径网络会删除对应自动 carrier并撤销该源对象的穿越/支撑面障碍模式。用户手工创建、没有源对象元数据的 carrier 不会被这一步自动删除。
### 5.3 布线连接功能
已完成:
@ -395,6 +407,7 @@ tests/python/freecad_exchange_auto_routing_test.py
21. 相邻线槽端点在容差内会被网络自动连通;端子接入会连接到最近的网络线段点,而不是只连接到已有端点。
22. 线槽端部会生成 `WireDuctOpenEnd` 横向路径,穿线孔/过线孔会生成 `WiringCutOut` carrier。
23. 导线会保存 routing track网络检查会生成 `RoutingPathNetwork` 诊断对象。
24. 自动生成的线槽、过线孔和支撑面 carrier 会在源对象移动、缩放、删除或失效后刷新/清理。
已完成 FreeCAD smoke

@ -22,6 +22,11 @@ ROUTE_CARRIER_KIND_WIRING_CUT_OUT = "WiringCutOut"
ROUTE_CARRIER_KIND_AUXILIARY_PATH = "AuxiliaryPath"
ROUTE_CARRIER_KIND_ROUTING_RANGE = "RoutingRange"
ROUTE_CARRIER_KIND_TERMINAL_ACCESS = "TerminalAccess"
MANAGED_ROUTE_SOURCE_KINDS = {
ROUTE_CARRIER_KIND_WIRE_DUCT,
ROUTE_CARRIER_KIND_WIRING_CUT_OUT,
ROUTE_CARRIER_KIND_ROUTING_RANGE,
}
PROPERTY_GROUP = "QET Routing"
DEFAULT_NODE_TOLERANCE = 0.001
DEFAULT_SURFACE_LANE_SPACING = 100.0
@ -872,10 +877,11 @@ def _detach_from_groups(doc, obj):
pass
def clear_route_carriers(doc):
"""Delete generated route carriers while keeping terminals and routed wires."""
def _remove_route_carriers(doc, carriers):
removed = 0
for carrier in list(collect_route_carriers(doc)):
for carrier in list(carriers or []):
if carrier is None or not is_route_carrier(carrier):
continue
_detach_from_groups(doc, carrier)
try:
if doc.getObject(getattr(carrier, "Name", "")) is not None:
@ -883,6 +889,12 @@ def clear_route_carriers(doc):
removed += 1
except Exception:
pass
return removed
def clear_route_carriers(doc):
"""Delete generated route carriers while keeping terminals and routed wires."""
removed = _remove_route_carriers(doc, collect_route_carriers(doc))
try:
doc.recompute()
except Exception:
@ -1528,6 +1540,40 @@ def _live_source_carriers(doc, source):
return carriers
def _source_kind_value(source):
return (getattr(source, "QetRoutingSourceKind", "") or "").strip()
def _set_route_carrier_source_metadata(carrier, source, source_kind=""):
if carrier is None or source is None:
return
source_name = (getattr(source, "Name", "") or "").strip()
if not source_name:
return
kind = (source_kind or _source_kind_value(source)).strip()
TerminalObjects.ensure_string_property(
carrier,
"QetRouteSourceName",
PROPERTY_GROUP,
"FreeCAD source object name that generated this route carrier",
source_name,
)
TerminalObjects.ensure_string_property(
carrier,
"QetRouteSourceLabel",
PROPERTY_GROUP,
"FreeCAD source object label that generated this route carrier",
getattr(source, "Label", "") or source_name,
)
TerminalObjects.ensure_string_property(
carrier,
"QetRouteSourceKind",
PROPERTY_GROUP,
"Routing source kind that generated this route carrier",
kind,
)
def _remember_source_carriers(source, carriers):
live_names = [
getattr(carrier, "Name", "")
@ -1535,6 +1581,9 @@ def _remember_source_carriers(source, carriers):
if carrier is not None and getattr(carrier, "Name", "")
]
if live_names:
source_kind = _source_kind_value(source)
for carrier in carriers or []:
_set_route_carrier_source_metadata(carrier, source, source_kind=source_kind)
TerminalObjects.ensure_string_property(
source,
"QetRouteCarrierNamesJson",
@ -1591,6 +1640,7 @@ def _mark_wiring_cut_out_source(source, carrier):
"Generated route carrier for this source",
getattr(carrier, "Name", ""),
)
_remember_source_carriers(source, [carrier])
except Exception:
pass
@ -1613,6 +1663,7 @@ def _mark_terminal_access_source(source, carrier):
"Generated route carrier for this source",
getattr(carrier, "Name", ""),
)
_remember_source_carriers(source, [carrier])
except Exception:
pass
@ -1622,6 +1673,77 @@ def _live_source_carrier(doc, source):
return carriers[0] if carriers else None
def _source_is_valid_for_kind(source, source_kind):
if source_kind == ROUTE_CARRIER_KIND_WIRE_DUCT:
return _is_wire_duct_candidate(source)
if source_kind == ROUTE_CARRIER_KIND_ROUTING_RANGE:
return _is_support_surface_candidate(source)
if source_kind == ROUTE_CARRIER_KIND_WIRING_CUT_OUT:
return _is_wiring_cut_out_candidate(source)
return True
def _clear_invalid_source_route_metadata(source):
for property_name in (
"QetRouteCarrierName",
"QetRouteCarrierNamesJson",
"QetRoutingObstacleMode",
):
if property_name not in getattr(source, "PropertiesList", []) and not getattr(source, property_name, ""):
continue
TerminalObjects.ensure_string_property(
source,
property_name,
PROPERTY_GROUP,
"Cleared invalid routing source metadata",
"",
)
def _document_object_by_name(doc, name):
if doc is None or not name:
return None
try:
return doc.getObject(name)
except Exception:
return None
def cleanup_invalid_source_carriers(doc):
"""Remove generated carriers whose FreeCAD source object is missing or invalid."""
if doc is None:
return 0
removed = 0
for carrier in list(collect_route_carriers(doc)):
source_name = (getattr(carrier, "QetRouteSourceName", "") or "").strip()
source_kind = (getattr(carrier, "QetRouteSourceKind", "") or "").strip()
if source_kind not in MANAGED_ROUTE_SOURCE_KINDS or not source_name:
continue
if _document_object_by_name(doc, source_name) is None:
removed += _remove_route_carriers(doc, [carrier])
for source in list(getattr(doc, "Objects", []) or []):
if source is None or is_route_carrier(source):
continue
source_kind = _source_kind_value(source)
if source_kind not in MANAGED_ROUTE_SOURCE_KINDS:
continue
if not _route_source_carrier_names(source):
continue
if _source_is_valid_for_kind(source, source_kind):
continue
removed += _remove_route_carriers(doc, _live_source_carriers(doc, source))
_clear_invalid_source_route_metadata(source)
if removed:
try:
doc.recompute()
except Exception:
pass
return removed
def detect_wire_duct_sources(doc, min_aspect=DEFAULT_AUTO_WIRE_DUCT_MIN_ASPECT):
"""Return document objects that look like wire ducts based on semantics/name and shape."""
sources = []
@ -1672,6 +1794,7 @@ def prepare_layout_space_sources_from_document(doc, project_uuid=""):
raise RoutingNetworkError("No FreeCAD document is available.")
WiringObjects.ensure_wiring_root_group(doc, project_uuid)
cleanup_invalid_source_carriers(doc)
wire_duct_sources = detect_wire_duct_sources(doc)
support_surface_sources = detect_support_surface_sources(doc)
@ -1713,6 +1836,7 @@ def create_wire_duct_carriers_from_document(
min_aspect=DEFAULT_AUTO_WIRE_DUCT_MIN_ASPECT,
):
"""Auto-detect wire duct objects in the document and create WireDuct centerlines."""
cleanup_invalid_source_carriers(doc)
created = []
for index, source in enumerate(detect_wire_duct_sources(doc, min_aspect=min_aspect), start=1):
bbox = _bound_box_from_object(source)
@ -1765,6 +1889,7 @@ def create_wire_duct_carriers_from_document(
def create_wiring_cut_out_carriers_from_document(doc, project_uuid=""):
"""Create pass-through route carriers for wiring cut-out objects."""
cleanup_invalid_source_carriers(doc)
created = []
for source in detect_wiring_cut_out_sources(doc):
bbox = _bound_box_from_object(source)
@ -1808,6 +1933,7 @@ def create_surface_carriers_from_document(
margin=DEFAULT_SURFACE_MARGIN,
):
"""Auto-detect thin support panels and create low-priority RoutingRange grids."""
cleanup_invalid_source_carriers(doc)
created = []
for source in detect_support_surface_sources(doc):
bbox = _bound_box_from_object(source)
@ -2076,6 +2202,7 @@ def create_wire_duct_carriers_from_selection(
min_aspect=1.5,
):
"""Create WireDuct centerline carriers from selected duct-like solids."""
cleanup_invalid_source_carriers(doc)
created = []
for index, source in enumerate(_wire_duct_sources_from_selection(selection_ex), start=1):
bbox = _bound_box_from_object(source)

@ -965,6 +965,37 @@ class AutoRoutingTest(unittest.TestCase):
self.assertEqual(60.0, max(x_values))
self.assertEqual(60.0, max(z_values))
def test_auto_detect_support_surface_removes_carriers_and_obstacle_mode_when_source_invalid(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules()
doc = FakeDocument()
terminal_objects.ensure_root_group(doc, "project-1")
panel = doc.addObject("Part::Feature", "MountingPlateA")
panel.Label = "安装板A"
panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100))
created = routing_network.create_surface_carriers_from_document(
doc,
project_uuid="project-1",
spacing=60.0,
offset=5.0,
margin=0.0,
)
panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 120, 0, 120))
created_again = routing_network.create_surface_carriers_from_document(
doc,
project_uuid="project-1",
spacing=60.0,
offset=5.0,
margin=0.0,
)
self.assertEqual(6, len(created))
self.assertEqual(0, len(created_again))
self.assertEqual([], routing_network.collect_route_carriers(doc))
self.assertEqual("", getattr(panel, "QetRoutingObstacleMode", ""))
self.assertEqual("", getattr(panel, "QetRouteCarrierNamesJson", ""))
def test_eplan_connection_route_can_use_auto_detected_support_surface(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules()
@ -1093,6 +1124,30 @@ class AutoRoutingTest(unittest.TestCase):
self.assertEqual([(20.0, 0.0, 10.0), (200.0, 0.0, 10.0)], [(p.x, p.y, p.z) for p in main.Points])
self.assertEqual([20.0, 20.0, 200.0, 200.0], open_end_x_values)
def test_generate_routing_paths_removes_generated_wire_duct_carriers_after_source_deleted(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules()
auto_routing_panel = importlib.import_module("AutoRoutingPanel")
app = sys.modules["FreeCAD"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
duct = doc.addObject("Part::Feature", "WireDuctA")
duct.Label = "Wire Duct A"
duct.Shape = FakeShape(FakeBoundBox(0, 160, -10, 10, 0, 20))
auto_routing_panel.AutoRoutingController().generate_routing_paths()
generated = [
item
for item in routing_network.collect_route_carriers(doc)
if getattr(item, "QetRouteSourceName", "") == "WireDuctA"
]
doc.removeObject("WireDuctA")
auto_routing_panel.AutoRoutingController().generate_routing_paths()
self.assertEqual(3, len(generated))
self.assertEqual([], routing_network.collect_route_carriers(doc))
def test_prepare_layout_space_uses_whole_document_not_selected_face_workflow(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules()

Loading…
Cancel
Save