|
|
|
|
@ -57,6 +57,7 @@ DEFAULT_TERMINAL_ACCESS_COMPONENT_SEGMENT_PENALTY = 25.0
|
|
|
|
|
DEFAULT_TERMINAL_ACCESS_FALLBACK_ONLY_COMPONENT_PENALTY = 1000.0
|
|
|
|
|
DEFAULT_TERMINAL_ACCESS_FALLBACK_CARRIER_PENALTY = 5000.0
|
|
|
|
|
DEFAULT_TERMINAL_ACCESS_ENTRY_CANDIDATE_PENALTY = 2000.0
|
|
|
|
|
DEFAULT_TERMINAL_DEVICE_EXIT_CLEARANCE = 10.0
|
|
|
|
|
DEFAULT_ADJOINING_DUCT_TOLERANCE = 5.0
|
|
|
|
|
DEFAULT_WIRING_CUT_OUT_BRIDGE_EXTENSION = 20.0
|
|
|
|
|
WIRE_DUCT_OBSTACLE_MODE = "PassThrough"
|
|
|
|
|
@ -700,6 +701,70 @@ def _set_user_path_source_semantics(source):
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _set_user_path_sketch_semantics(sketch, project_uuid="", support=None, sub_element_name="", offset=DEFAULT_ROUTE_PATH_FACE_OFFSET):
|
|
|
|
|
_set_user_path_source_semantics(sketch)
|
|
|
|
|
TerminalObjects.ensure_string_property(
|
|
|
|
|
sketch,
|
|
|
|
|
"QetProjectUuid",
|
|
|
|
|
PROPERTY_GROUP,
|
|
|
|
|
"Project UUID for this route sketch",
|
|
|
|
|
project_uuid,
|
|
|
|
|
)
|
|
|
|
|
TerminalObjects.ensure_string_property(
|
|
|
|
|
sketch,
|
|
|
|
|
"QetRouteSketchMode",
|
|
|
|
|
PROPERTY_GROUP,
|
|
|
|
|
"Manual route sketch mode",
|
|
|
|
|
"ManualUserPathSketch",
|
|
|
|
|
)
|
|
|
|
|
TerminalObjects.ensure_string_property(
|
|
|
|
|
sketch,
|
|
|
|
|
"QetRouteSketchSupportName",
|
|
|
|
|
PROPERTY_GROUP,
|
|
|
|
|
"One-shot support object used when creating this manual route sketch",
|
|
|
|
|
getattr(support, "Name", "") if support is not None else "",
|
|
|
|
|
)
|
|
|
|
|
TerminalObjects.ensure_string_property(
|
|
|
|
|
sketch,
|
|
|
|
|
"QetRouteSketchSupportLabel",
|
|
|
|
|
PROPERTY_GROUP,
|
|
|
|
|
"One-shot support label used when creating this manual route sketch",
|
|
|
|
|
getattr(support, "Label", "") if support is not None else "",
|
|
|
|
|
)
|
|
|
|
|
TerminalObjects.ensure_string_property(
|
|
|
|
|
sketch,
|
|
|
|
|
"QetRouteSketchSupportSubElement",
|
|
|
|
|
PROPERTY_GROUP,
|
|
|
|
|
"One-shot support sub-element used when creating this manual route sketch",
|
|
|
|
|
sub_element_name or "",
|
|
|
|
|
)
|
|
|
|
|
_ensure_float_property(
|
|
|
|
|
sketch,
|
|
|
|
|
"QetRouteSketchFaceOffsetMm",
|
|
|
|
|
"Offset from selected support face when creating this manual route sketch",
|
|
|
|
|
offset,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _style_user_path_sketch(sketch):
|
|
|
|
|
view = getattr(sketch, "ViewObject", None)
|
|
|
|
|
if view is None:
|
|
|
|
|
return
|
|
|
|
|
for attr_name, value in (
|
|
|
|
|
("LineColor", (1.0, 0.85, 0.0)),
|
|
|
|
|
("ShapeColor", (1.0, 0.85, 0.0)),
|
|
|
|
|
("PointColor", (1.0, 0.85, 0.0)),
|
|
|
|
|
):
|
|
|
|
|
try:
|
|
|
|
|
setattr(view, attr_name, value)
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
try:
|
|
|
|
|
view.LineWidth = 3.0
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _object_has_bbox(obj):
|
|
|
|
|
shape = getattr(obj, "Shape", None)
|
|
|
|
|
return getattr(shape, "BoundBox", None) is not None
|
|
|
|
|
@ -1222,6 +1287,14 @@ def _object_global_placement(obj):
|
|
|
|
|
return getattr(obj, "Placement", None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _route_source_geometry_placement(obj):
|
|
|
|
|
type_id = (getattr(obj, "TypeId", "") or "").lower()
|
|
|
|
|
if "sketch" in type_id:
|
|
|
|
|
# 真实 Sketcher::SketchObject 的 Shape 点已经带有 Attachment/Placement,不能再平移一次。
|
|
|
|
|
return None
|
|
|
|
|
return _object_global_placement(obj)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _points_with_placement(points, placement):
|
|
|
|
|
return [_placement_mult_vec(placement, _vector(point)) for point in points]
|
|
|
|
|
|
|
|
|
|
@ -1283,6 +1356,13 @@ def _routing_source_text(obj):
|
|
|
|
|
).lower()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _is_explicit_user_path_source(obj):
|
|
|
|
|
return (
|
|
|
|
|
(getattr(obj, "QetRoutingSourceKind", "") or "").strip() == ROUTE_CARRIER_KIND_USER_PATH
|
|
|
|
|
or (getattr(obj, "QetRouteSketchMode", "") or "").strip() == "ManualUserPathSketch"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _bbox_aspect_ratio(bbox):
|
|
|
|
|
extents = sorted(
|
|
|
|
|
(_bbox_extent(bbox, axis) for axis in ("x", "y", "z")),
|
|
|
|
|
@ -1300,6 +1380,8 @@ def _is_wire_duct_candidate(obj, min_aspect=DEFAULT_AUTO_WIRE_DUCT_MIN_ASPECT):
|
|
|
|
|
return False
|
|
|
|
|
if is_route_carrier(obj) or is_routing_boundary(obj) or TerminalObjects.is_terminal_object(obj):
|
|
|
|
|
return False
|
|
|
|
|
if _is_explicit_user_path_source(obj):
|
|
|
|
|
return False
|
|
|
|
|
if (getattr(obj, "RouteType", "") or "").strip():
|
|
|
|
|
return False
|
|
|
|
|
text = _routing_source_text(obj)
|
|
|
|
|
@ -1345,6 +1427,8 @@ def _is_support_surface_candidate(obj):
|
|
|
|
|
return False
|
|
|
|
|
if is_route_carrier(obj) or is_routing_boundary(obj) or TerminalObjects.is_terminal_object(obj):
|
|
|
|
|
return False
|
|
|
|
|
if _is_explicit_user_path_source(obj):
|
|
|
|
|
return False
|
|
|
|
|
if (getattr(obj, "RouteType", "") or "").strip():
|
|
|
|
|
return False
|
|
|
|
|
text = _routing_source_text(obj)
|
|
|
|
|
@ -1372,6 +1456,8 @@ def _is_wiring_cut_out_candidate(obj):
|
|
|
|
|
return False
|
|
|
|
|
if is_route_carrier(obj) or is_routing_boundary(obj) or TerminalObjects.is_terminal_object(obj):
|
|
|
|
|
return False
|
|
|
|
|
if _is_explicit_user_path_source(obj):
|
|
|
|
|
return False
|
|
|
|
|
if (getattr(obj, "RouteType", "") or "").strip():
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
@ -1437,6 +1523,7 @@ def _point_runs_from_selection_item(selection_item):
|
|
|
|
|
points = []
|
|
|
|
|
obj = getattr(selection_item, "Object", None)
|
|
|
|
|
placement = _object_global_placement(obj)
|
|
|
|
|
geometry_placement = _route_source_geometry_placement(obj)
|
|
|
|
|
|
|
|
|
|
for point in list(getattr(selection_item, "PickedPoints", []) or []):
|
|
|
|
|
points.append(_vector(point))
|
|
|
|
|
@ -1447,19 +1534,19 @@ def _point_runs_from_selection_item(selection_item):
|
|
|
|
|
if points:
|
|
|
|
|
runs.append(points)
|
|
|
|
|
points = []
|
|
|
|
|
runs.append(_points_with_placement(_wire_points(sub_object), placement))
|
|
|
|
|
runs.append(_points_with_placement(_wire_points(sub_object), geometry_placement))
|
|
|
|
|
continue
|
|
|
|
|
if shape_type == "edge":
|
|
|
|
|
points.extend(_points_with_placement(_edge_points(sub_object), placement))
|
|
|
|
|
points.extend(_points_with_placement(_edge_points(sub_object), geometry_placement))
|
|
|
|
|
continue
|
|
|
|
|
if shape_type == "vertex":
|
|
|
|
|
point = getattr(sub_object, "Point", None)
|
|
|
|
|
if point is not None:
|
|
|
|
|
points.append(_placement_mult_vec(placement, _vector(point)))
|
|
|
|
|
points.append(_placement_mult_vec(geometry_placement, _vector(point)))
|
|
|
|
|
continue
|
|
|
|
|
center = _shape_center(sub_object)
|
|
|
|
|
if center is not None:
|
|
|
|
|
points.append(_placement_mult_vec(placement, center))
|
|
|
|
|
points.append(_placement_mult_vec(geometry_placement, center))
|
|
|
|
|
|
|
|
|
|
if obj is not None and _is_route_path_source_object(obj):
|
|
|
|
|
for point in list(getattr(obj, "Points", []) or []):
|
|
|
|
|
@ -1473,14 +1560,14 @@ def _point_runs_from_selection_item(selection_item):
|
|
|
|
|
runs.append(points)
|
|
|
|
|
points = []
|
|
|
|
|
for wire in wires:
|
|
|
|
|
runs.append(_points_with_placement(_wire_points(wire), placement))
|
|
|
|
|
runs.append(_points_with_placement(_wire_points(wire), geometry_placement))
|
|
|
|
|
else:
|
|
|
|
|
for edge in list(getattr(shape, "Edges", []) or []):
|
|
|
|
|
points.extend(_points_with_placement(_edge_points(edge), placement))
|
|
|
|
|
points.extend(_points_with_placement(_edge_points(edge), geometry_placement))
|
|
|
|
|
if not runs and not points:
|
|
|
|
|
center = _shape_center(shape)
|
|
|
|
|
if center is not None:
|
|
|
|
|
points.append(_placement_mult_vec(placement, center))
|
|
|
|
|
points.append(_placement_mult_vec(geometry_placement, center))
|
|
|
|
|
|
|
|
|
|
if points:
|
|
|
|
|
runs.append(points)
|
|
|
|
|
@ -1508,6 +1595,21 @@ def _support_face_from_selection(selection_ex):
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _support_face_selection_from_selection(selection_ex):
|
|
|
|
|
for item in selection_ex or []:
|
|
|
|
|
source = getattr(item, "Object", None)
|
|
|
|
|
sub_names = list(getattr(item, "SubElementNames", []) or [])
|
|
|
|
|
for index, sub_object in enumerate(list(getattr(item, "SubObjects", []) or [])):
|
|
|
|
|
if (getattr(sub_object, "ShapeType", "") or "").lower() != "face":
|
|
|
|
|
continue
|
|
|
|
|
return {
|
|
|
|
|
"face": sub_object,
|
|
|
|
|
"source": source,
|
|
|
|
|
"sub_element_name": sub_names[index] if index < len(sub_names) else "",
|
|
|
|
|
}
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _selection_item_is_only_support_face(selection_item):
|
|
|
|
|
sub_objects = list(getattr(selection_item, "SubObjects", []) or [])
|
|
|
|
|
if not sub_objects:
|
|
|
|
|
@ -1703,6 +1805,216 @@ def _project_points_to_face(points, face, offset=DEFAULT_ROUTE_PATH_FACE_OFFSET)
|
|
|
|
|
return projected
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _rotation_from_face_normal(normal):
|
|
|
|
|
try:
|
|
|
|
|
rotation = App.Rotation(App.Vector(0, 0, 1), normal)
|
|
|
|
|
if rotation is not None:
|
|
|
|
|
return rotation
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
try:
|
|
|
|
|
return App.Rotation()
|
|
|
|
|
except Exception:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _attach_sketch_to_support_face(sketch, support, sub_element_name, offset, fallback_base, normal):
|
|
|
|
|
if sketch is None:
|
|
|
|
|
return
|
|
|
|
|
attached = False
|
|
|
|
|
if support is not None and sub_element_name:
|
|
|
|
|
try:
|
|
|
|
|
sketch.AttachmentSupport = [(support, sub_element_name)]
|
|
|
|
|
sketch.MapMode = "FlatFace"
|
|
|
|
|
sketch.AttachmentOffset = App.Placement(
|
|
|
|
|
App.Vector(0, 0, abs(float(offset or 0.0))),
|
|
|
|
|
App.Rotation(),
|
|
|
|
|
)
|
|
|
|
|
attached = True
|
|
|
|
|
except Exception:
|
|
|
|
|
attached = False
|
|
|
|
|
try:
|
|
|
|
|
sketch.Placement = App.Placement(fallback_base, _rotation_from_face_normal(normal))
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
if attached:
|
|
|
|
|
# Attachment 是真实 FreeCAD 里的编辑语义;Placement 仍保留为测试和旧对象兜底。
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_user_path_sketch_from_selection(
|
|
|
|
|
doc,
|
|
|
|
|
selection_ex,
|
|
|
|
|
project_uuid="",
|
|
|
|
|
offset=DEFAULT_ROUTE_PATH_FACE_OFFSET,
|
|
|
|
|
):
|
|
|
|
|
"""Create a one-shot Sketcher sketch on the selected cabinet/support face for manual UserPath drawing."""
|
|
|
|
|
if doc is None:
|
|
|
|
|
raise RoutingNetworkError("没有可用的 FreeCAD 文档。")
|
|
|
|
|
|
|
|
|
|
support = _support_face_selection_from_selection(selection_ex)
|
|
|
|
|
if support is None:
|
|
|
|
|
raise RoutingNetworkError("请先选中机柜面板、安装板、门板或线槽上的一个面 Face,再创建布线路径草图。")
|
|
|
|
|
|
|
|
|
|
face = support["face"]
|
|
|
|
|
normal = _normalize(_face_normal(face))
|
|
|
|
|
if normal is None:
|
|
|
|
|
raise RoutingNetworkError("选中的 Face 无法确定法向,不能创建布线路径草图。")
|
|
|
|
|
|
|
|
|
|
face_points = _face_points(face)
|
|
|
|
|
origin = _face_origin(face, face_points)
|
|
|
|
|
base = _add(origin, _scale(normal, abs(float(offset or 0.0))))
|
|
|
|
|
name = _unique_name(doc, "QETUserPathSketch")
|
|
|
|
|
sketch = doc.addObject("Sketcher::SketchObject", name)
|
|
|
|
|
sketch.Label = "布线路径草图"
|
|
|
|
|
_attach_sketch_to_support_face(
|
|
|
|
|
sketch,
|
|
|
|
|
support.get("source"),
|
|
|
|
|
support.get("sub_element_name", ""),
|
|
|
|
|
abs(float(offset or 0.0)),
|
|
|
|
|
base,
|
|
|
|
|
normal,
|
|
|
|
|
)
|
|
|
|
|
_set_user_path_sketch_semantics(
|
|
|
|
|
sketch,
|
|
|
|
|
project_uuid=project_uuid,
|
|
|
|
|
support=support.get("source"),
|
|
|
|
|
sub_element_name=support.get("sub_element_name", ""),
|
|
|
|
|
offset=abs(float(offset or 0.0)),
|
|
|
|
|
)
|
|
|
|
|
_style_user_path_sketch(sketch)
|
|
|
|
|
try:
|
|
|
|
|
doc.recompute()
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
return {
|
|
|
|
|
"sketch": sketch,
|
|
|
|
|
"support": support.get("source"),
|
|
|
|
|
"sub_element_name": support.get("sub_element_name", ""),
|
|
|
|
|
"offset": abs(float(offset or 0.0)),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _selected_points_from_selection(selection_ex):
|
|
|
|
|
points = []
|
|
|
|
|
for item in selection_ex or []:
|
|
|
|
|
obj = getattr(item, "Object", None)
|
|
|
|
|
placement = _object_global_placement(obj)
|
|
|
|
|
geometry_placement = _route_source_geometry_placement(obj)
|
|
|
|
|
for point in list(getattr(item, "PickedPoints", []) or []):
|
|
|
|
|
points.append(_vector(point))
|
|
|
|
|
for sub_object in list(getattr(item, "SubObjects", []) or []):
|
|
|
|
|
shape_type = (getattr(sub_object, "ShapeType", "") or "").lower()
|
|
|
|
|
if shape_type == "wire":
|
|
|
|
|
points.extend(_points_with_placement(_wire_points(sub_object), geometry_placement))
|
|
|
|
|
continue
|
|
|
|
|
if shape_type == "edge":
|
|
|
|
|
points.extend(_points_with_placement(_edge_points(sub_object), geometry_placement))
|
|
|
|
|
continue
|
|
|
|
|
if shape_type == "vertex":
|
|
|
|
|
point = getattr(sub_object, "Point", None)
|
|
|
|
|
if point is not None:
|
|
|
|
|
points.append(_placement_mult_vec(geometry_placement, _vector(point)))
|
|
|
|
|
continue
|
|
|
|
|
center = _shape_center(sub_object)
|
|
|
|
|
if center is not None:
|
|
|
|
|
points.append(_placement_mult_vec(geometry_placement, center))
|
|
|
|
|
if obj is not None and _is_route_path_source_object(obj):
|
|
|
|
|
for point in list(getattr(obj, "Points", []) or []):
|
|
|
|
|
points.append(_placement_mult_vec(placement, _vector(point)))
|
|
|
|
|
return _normalize_point_run(points)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_user_path_carrier_from_selected_points(doc, selection_ex, project_uuid="", label="QET 3D User Route Path"):
|
|
|
|
|
"""Create one 3D UserPath from selected vertices, picked points, or point-like sub-objects in selection order."""
|
|
|
|
|
if doc is None:
|
|
|
|
|
raise RoutingNetworkError("没有可用的 FreeCAD 文档。")
|
|
|
|
|
points = _selected_points_from_selection(selection_ex)
|
|
|
|
|
if len(points) < 2:
|
|
|
|
|
raise RoutingNetworkError("请至少按顺序选择两个 3D 点、顶点、边端点或带 Points 的路径对象。")
|
|
|
|
|
carrier = create_route_carrier(
|
|
|
|
|
doc,
|
|
|
|
|
points,
|
|
|
|
|
label=label,
|
|
|
|
|
project_uuid=project_uuid,
|
|
|
|
|
kind=ROUTE_CARRIER_KIND_USER_PATH,
|
|
|
|
|
)
|
|
|
|
|
try:
|
|
|
|
|
doc.recompute()
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
return {
|
|
|
|
|
"carrier": carrier,
|
|
|
|
|
"points": points,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _orthogonal_points_between(start, end, axis_order=("x", "y", "z")):
|
|
|
|
|
current = _vector(start)
|
|
|
|
|
end = _vector(end)
|
|
|
|
|
points = [current]
|
|
|
|
|
for axis in axis_order or ("x", "y", "z"):
|
|
|
|
|
if abs(_axis_value(current, axis) - _axis_value(end, axis)) <= DEFAULT_NODE_TOLERANCE:
|
|
|
|
|
continue
|
|
|
|
|
current = _set_axis(current, axis, _axis_value(end, axis))
|
|
|
|
|
if _distance(points[-1], current) > DEFAULT_NODE_TOLERANCE:
|
|
|
|
|
points.append(current)
|
|
|
|
|
if _distance(points[-1], end) > DEFAULT_NODE_TOLERANCE:
|
|
|
|
|
points.append(end)
|
|
|
|
|
return points
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _orthogonalize_points(points, axis_order=("x", "y", "z")):
|
|
|
|
|
source_points = _normalize_point_run([_vector(point) for point in points or []])
|
|
|
|
|
if len(source_points) < 2:
|
|
|
|
|
return source_points
|
|
|
|
|
result = [source_points[0]]
|
|
|
|
|
for end in source_points[1:]:
|
|
|
|
|
segment_points = _orthogonal_points_between(result[-1], end, axis_order=axis_order)
|
|
|
|
|
for point in segment_points[1:]:
|
|
|
|
|
if _distance(result[-1], point) > DEFAULT_NODE_TOLERANCE:
|
|
|
|
|
result.append(point)
|
|
|
|
|
return _normalize_point_run(result)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_orthogonal_user_path_carrier_from_selected_points(
|
|
|
|
|
doc,
|
|
|
|
|
selection_ex,
|
|
|
|
|
project_uuid="",
|
|
|
|
|
label="QET Orthogonal 3D User Route Path",
|
|
|
|
|
axis_order=("x", "y", "z"),
|
|
|
|
|
):
|
|
|
|
|
"""Create one X/Y/Z orthogonal 3D UserPath from selected points in selection order."""
|
|
|
|
|
if doc is None:
|
|
|
|
|
raise RoutingNetworkError("没有可用的 FreeCAD 文档。")
|
|
|
|
|
points = _selected_points_from_selection(selection_ex)
|
|
|
|
|
if len(points) < 2:
|
|
|
|
|
raise RoutingNetworkError("请至少按顺序选择两个 3D 点、顶点、边端点或带 Points 的路径对象。")
|
|
|
|
|
orthogonal_points = _orthogonalize_points(points, axis_order=axis_order)
|
|
|
|
|
carrier = create_route_carrier(
|
|
|
|
|
doc,
|
|
|
|
|
orthogonal_points,
|
|
|
|
|
label=label,
|
|
|
|
|
project_uuid=project_uuid,
|
|
|
|
|
kind=ROUTE_CARRIER_KIND_USER_PATH,
|
|
|
|
|
)
|
|
|
|
|
TerminalObjects.ensure_string_property(
|
|
|
|
|
carrier,
|
|
|
|
|
"QetRoutePathMode",
|
|
|
|
|
PROPERTY_GROUP,
|
|
|
|
|
"Manual 3D route path mode",
|
|
|
|
|
"Orthogonal3D",
|
|
|
|
|
)
|
|
|
|
|
try:
|
|
|
|
|
doc.recompute()
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
return {
|
|
|
|
|
"carrier": carrier,
|
|
|
|
|
"points": orthogonal_points,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_carriers_from_selection(doc, selection_ex, project_uuid="", kind=ROUTE_CARRIER_KIND):
|
|
|
|
|
created = []
|
|
|
|
|
support_face = _support_face_from_selection(selection_ex)
|
|
|
|
|
@ -1762,6 +2074,11 @@ def create_user_path_carriers_from_selection(doc, selection_ex, project_uuid="")
|
|
|
|
|
point_runs = [_normalize_point_run(points) for points in point_runs]
|
|
|
|
|
point_runs = [points for points in point_runs if len(points) >= 2]
|
|
|
|
|
if not point_runs:
|
|
|
|
|
if source is not None:
|
|
|
|
|
live_carriers = _live_source_carriers(doc, source)
|
|
|
|
|
if live_carriers:
|
|
|
|
|
_remove_route_carriers(doc, live_carriers)
|
|
|
|
|
_mark_user_path_source_carriers(source, [])
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
label = "QET User Route Path {0}".format(index)
|
|
|
|
|
@ -1821,6 +2138,70 @@ def create_user_path_carriers_from_selection(doc, selection_ex, project_uuid="")
|
|
|
|
|
return created
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _create_or_refresh_user_path_source(doc, source, project_uuid="", label_prefix="QET User Route Path"):
|
|
|
|
|
if source is None or is_route_carrier(source) or is_routing_boundary(source):
|
|
|
|
|
return []
|
|
|
|
|
point_runs = _point_runs_from_selection_item(type("_Selection", (), {"Object": source, "SubObjects": []})())
|
|
|
|
|
point_runs = [_normalize_point_run(points) for points in point_runs]
|
|
|
|
|
point_runs = [points for points in point_runs if len(points) >= 2]
|
|
|
|
|
if not point_runs:
|
|
|
|
|
live_carriers = _live_source_carriers(doc, source)
|
|
|
|
|
if live_carriers:
|
|
|
|
|
_remove_route_carriers(doc, live_carriers)
|
|
|
|
|
_mark_user_path_source_carriers(source, [])
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
label = "{0} {1}".format(
|
|
|
|
|
label_prefix,
|
|
|
|
|
getattr(source, "Label", "") or getattr(source, "Name", "") or "",
|
|
|
|
|
).strip()
|
|
|
|
|
capacity = _route_carrier_capacity_value(source, default=1)
|
|
|
|
|
live_carriers = _live_source_carriers(doc, source)
|
|
|
|
|
refreshed = []
|
|
|
|
|
for run_index, points in enumerate(point_runs, start=1):
|
|
|
|
|
run_label = label if len(point_runs) == 1 else "{0} {1}".format(label, run_index)
|
|
|
|
|
if run_index <= len(live_carriers):
|
|
|
|
|
carrier = live_carriers[run_index - 1]
|
|
|
|
|
if _update_route_carrier(
|
|
|
|
|
carrier,
|
|
|
|
|
points,
|
|
|
|
|
project_uuid=project_uuid,
|
|
|
|
|
kind=ROUTE_CARRIER_KIND_USER_PATH,
|
|
|
|
|
capacity=capacity,
|
|
|
|
|
):
|
|
|
|
|
refreshed.append(carrier)
|
|
|
|
|
continue
|
|
|
|
|
refreshed.append(
|
|
|
|
|
create_route_carrier(
|
|
|
|
|
doc,
|
|
|
|
|
points,
|
|
|
|
|
label=run_label,
|
|
|
|
|
project_uuid=project_uuid,
|
|
|
|
|
kind=ROUTE_CARRIER_KIND_USER_PATH,
|
|
|
|
|
capacity=capacity,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
if len(live_carriers) > len(point_runs):
|
|
|
|
|
_remove_route_carriers(doc, live_carriers[len(point_runs) :])
|
|
|
|
|
_mark_user_path_source_carriers(source, refreshed)
|
|
|
|
|
return refreshed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_user_path_carriers_from_document(doc, project_uuid=""):
|
|
|
|
|
"""Create or refresh UserPath carriers from all sketch/Draft-like path sources in the document."""
|
|
|
|
|
cleanup_invalid_source_carriers(doc)
|
|
|
|
|
created = []
|
|
|
|
|
for source in detect_document_user_path_sources(doc):
|
|
|
|
|
if (
|
|
|
|
|
_is_wire_duct_candidate(source)
|
|
|
|
|
or _is_support_surface_candidate(source)
|
|
|
|
|
or _is_wiring_cut_out_candidate(source)
|
|
|
|
|
):
|
|
|
|
|
continue
|
|
|
|
|
created.extend(_create_or_refresh_user_path_source(doc, source, project_uuid=project_uuid))
|
|
|
|
|
return created
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _nearest_points_between_route_point_runs(left_points, right_points):
|
|
|
|
|
left_points = _normalized_route_points(left_points)
|
|
|
|
|
right_points = _normalized_route_points(right_points)
|
|
|
|
|
@ -2357,6 +2738,8 @@ def _set_route_carrier_source_metadata(carrier, source, source_kind="", source_p
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _remember_source_carriers(source, carriers):
|
|
|
|
|
if source is None:
|
|
|
|
|
return
|
|
|
|
|
live_names = [
|
|
|
|
|
getattr(carrier, "Name", "")
|
|
|
|
|
for carrier in (carriers or [])
|
|
|
|
|
@ -2377,13 +2760,13 @@ def _remember_source_carriers(source, carriers):
|
|
|
|
|
source_kind=source_kind,
|
|
|
|
|
source_path_index=source_path_index,
|
|
|
|
|
)
|
|
|
|
|
TerminalObjects.ensure_string_property(
|
|
|
|
|
source,
|
|
|
|
|
"QetRouteCarrierNamesJson",
|
|
|
|
|
PROPERTY_GROUP,
|
|
|
|
|
"Generated route carriers for this source",
|
|
|
|
|
json.dumps(live_names, ensure_ascii=False),
|
|
|
|
|
)
|
|
|
|
|
TerminalObjects.ensure_string_property(
|
|
|
|
|
source,
|
|
|
|
|
"QetRouteCarrierNamesJson",
|
|
|
|
|
PROPERTY_GROUP,
|
|
|
|
|
"Generated route carriers for this source",
|
|
|
|
|
json.dumps(live_names, ensure_ascii=False),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _mark_wire_duct_source(source, carrier, carriers=None, end_margin=DEFAULT_WIRE_DUCT_MARGIN):
|
|
|
|
|
@ -2457,7 +2840,7 @@ def _mark_user_path_source(source, carrier):
|
|
|
|
|
|
|
|
|
|
def _mark_user_path_source_carriers(source, carriers):
|
|
|
|
|
carriers = [carrier for carrier in (carriers or []) if carrier is not None]
|
|
|
|
|
if source is None or not carriers:
|
|
|
|
|
if source is None:
|
|
|
|
|
return
|
|
|
|
|
try:
|
|
|
|
|
_set_user_path_source_semantics(source)
|
|
|
|
|
@ -2466,7 +2849,7 @@ def _mark_user_path_source_carriers(source, carriers):
|
|
|
|
|
"QetRouteCarrierName",
|
|
|
|
|
PROPERTY_GROUP,
|
|
|
|
|
"Generated route carrier for this source",
|
|
|
|
|
getattr(carriers[0], "Name", ""),
|
|
|
|
|
getattr(carriers[0], "Name", "") if carriers else "",
|
|
|
|
|
)
|
|
|
|
|
_remember_source_carriers(source, carriers)
|
|
|
|
|
except Exception:
|
|
|
|
|
@ -2626,6 +3009,47 @@ def detect_user_path_sources(doc):
|
|
|
|
|
return sources
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _is_document_user_path_source(obj):
|
|
|
|
|
if not _is_route_path_source_object(obj):
|
|
|
|
|
return False
|
|
|
|
|
if _is_explicit_user_path_source(obj):
|
|
|
|
|
return True
|
|
|
|
|
text = " ".join(
|
|
|
|
|
str(value or "")
|
|
|
|
|
for value in (
|
|
|
|
|
getattr(obj, "Name", ""),
|
|
|
|
|
getattr(obj, "Label", ""),
|
|
|
|
|
)
|
|
|
|
|
).lower()
|
|
|
|
|
return any(
|
|
|
|
|
keyword in text
|
|
|
|
|
for keyword in (
|
|
|
|
|
"userpath",
|
|
|
|
|
"user path",
|
|
|
|
|
"route path",
|
|
|
|
|
"routing path",
|
|
|
|
|
"wire path",
|
|
|
|
|
"wiring path",
|
|
|
|
|
"布线路径",
|
|
|
|
|
"走线路径",
|
|
|
|
|
"用户路径",
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def detect_document_user_path_sources(doc):
|
|
|
|
|
"""Return path sources that are safe to auto-convert during full network generation."""
|
|
|
|
|
sources = []
|
|
|
|
|
seen = set()
|
|
|
|
|
for obj in list(getattr(doc, "Objects", []) or []):
|
|
|
|
|
if id(obj) in seen:
|
|
|
|
|
continue
|
|
|
|
|
seen.add(id(obj))
|
|
|
|
|
if _is_document_user_path_source(obj):
|
|
|
|
|
sources.append(obj)
|
|
|
|
|
return sources
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _source_sample(obj):
|
|
|
|
|
return {
|
|
|
|
|
"name": getattr(obj, "Name", ""),
|
|
|
|
|
@ -3060,6 +3484,68 @@ def _terminal_parent_chain(terminal):
|
|
|
|
|
return chain
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _bbox_volume(bbox):
|
|
|
|
|
try:
|
|
|
|
|
return (
|
|
|
|
|
max(float(bbox.XMax) - float(bbox.XMin), 0.0)
|
|
|
|
|
* max(float(bbox.YMax) - float(bbox.YMin), 0.0)
|
|
|
|
|
* max(float(bbox.ZMax) - float(bbox.ZMin), 0.0)
|
|
|
|
|
)
|
|
|
|
|
except Exception:
|
|
|
|
|
return float("inf")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _terminal_parent_device_bbox(terminal, origin):
|
|
|
|
|
candidates = []
|
|
|
|
|
seen = set()
|
|
|
|
|
pending = list(getattr(terminal, "InList", []) or [])
|
|
|
|
|
while pending:
|
|
|
|
|
parent = pending.pop(0)
|
|
|
|
|
if parent is None or id(parent) in seen:
|
|
|
|
|
continue
|
|
|
|
|
seen.add(id(parent))
|
|
|
|
|
bbox = _bound_box_from_object(parent)
|
|
|
|
|
if bbox is not None and _point_inside_bbox(origin, bbox, tolerance=DEFAULT_NODE_TOLERANCE):
|
|
|
|
|
candidates.append(bbox)
|
|
|
|
|
pending.extend(list(getattr(parent, "InList", []) or []))
|
|
|
|
|
if not candidates:
|
|
|
|
|
return None
|
|
|
|
|
return min(candidates, key=_bbox_volume)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ray_exit_distance_from_bbox(origin, direction, bbox):
|
|
|
|
|
if not _point_inside_bbox(origin, bbox, tolerance=DEFAULT_NODE_TOLERANCE):
|
|
|
|
|
return None
|
|
|
|
|
distances = []
|
|
|
|
|
for axis in ("x", "y", "z"):
|
|
|
|
|
component = _axis_value(direction, axis)
|
|
|
|
|
if abs(component) <= DEFAULT_NODE_TOLERANCE:
|
|
|
|
|
continue
|
|
|
|
|
low, high = _bbox_axis_range(bbox, axis)
|
|
|
|
|
boundary = high if component > 0 else low
|
|
|
|
|
distance = (boundary - _axis_value(origin, axis)) / component
|
|
|
|
|
if distance >= -DEFAULT_NODE_TOLERANCE:
|
|
|
|
|
distances.append(max(float(distance), 0.0))
|
|
|
|
|
if not distances:
|
|
|
|
|
return None
|
|
|
|
|
return min(distances)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _terminal_device_aware_exit_point(terminal, exit_length):
|
|
|
|
|
origin = _vector(TerminalObjects.terminal_origin(terminal))
|
|
|
|
|
direction = _normalize(_vector(TerminalObjects.terminal_direction(terminal)))
|
|
|
|
|
if direction is None:
|
|
|
|
|
direction = App.Vector(0, 0, 1)
|
|
|
|
|
|
|
|
|
|
length = max(float(exit_length or 0.0), 0.0)
|
|
|
|
|
bbox = _terminal_parent_device_bbox(terminal, origin)
|
|
|
|
|
exit_distance = _ray_exit_distance_from_bbox(origin, direction, bbox)
|
|
|
|
|
if exit_distance is not None:
|
|
|
|
|
# 没有人工局部路径时,默认出线至少先离开所属设备外轮廓,避免导线贴在模型内部。
|
|
|
|
|
length = max(length, exit_distance + DEFAULT_TERMINAL_DEVICE_EXIT_CLEARANCE)
|
|
|
|
|
return _add(origin, _scale(direction, length))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _terminal_local_point_to_global(terminal, local_point):
|
|
|
|
|
try:
|
|
|
|
|
if hasattr(terminal, "getGlobalPlacement"):
|
|
|
|
|
@ -3176,7 +3662,7 @@ def terminal_access_path_points(terminal, exit_length=20.0):
|
|
|
|
|
normalized = _normalized_route_points(points)
|
|
|
|
|
if len(normalized) >= 2:
|
|
|
|
|
return normalized
|
|
|
|
|
return _normalized_route_points([origin, _terminal_exit_point(terminal, exit_length)])
|
|
|
|
|
return _normalized_route_points([origin, _terminal_device_aware_exit_point(terminal, exit_length)])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _orthogonal_access_points(start, end):
|
|
|
|
|
@ -3491,6 +3977,10 @@ def create_routing_path_network_from_document(
|
|
|
|
|
selection_ex,
|
|
|
|
|
project_uuid=project_uuid,
|
|
|
|
|
)
|
|
|
|
|
document_user_paths = create_user_path_carriers_from_document(
|
|
|
|
|
doc,
|
|
|
|
|
project_uuid=project_uuid,
|
|
|
|
|
)
|
|
|
|
|
wire_ducts = create_wire_duct_carriers_from_document(
|
|
|
|
|
doc,
|
|
|
|
|
project_uuid=project_uuid,
|
|
|
|
|
@ -3520,6 +4010,13 @@ def create_routing_path_network_from_document(
|
|
|
|
|
for carrier in selected_wire_ducts
|
|
|
|
|
if (getattr(carrier, "QetRouteCarrierKind", "") or "").strip() == ROUTE_CARRIER_KIND_WIRE_DUCT
|
|
|
|
|
)
|
|
|
|
|
all_user_paths = []
|
|
|
|
|
seen_user_paths = set()
|
|
|
|
|
for carrier in list(selected_user_paths) + list(document_user_paths):
|
|
|
|
|
if carrier is None or id(carrier) in seen_user_paths:
|
|
|
|
|
continue
|
|
|
|
|
seen_user_paths.add(id(carrier))
|
|
|
|
|
all_user_paths.append(carrier)
|
|
|
|
|
open_end_count = sum(
|
|
|
|
|
1
|
|
|
|
|
for carrier in all_wire_duct_created
|
|
|
|
|
@ -3529,7 +4026,9 @@ def create_routing_path_network_from_document(
|
|
|
|
|
return {
|
|
|
|
|
"wire_duct_carriers": wire_duct_main_count,
|
|
|
|
|
"selected_wire_duct_carriers": selected_wire_duct_main_count,
|
|
|
|
|
"user_path_carriers": len(selected_user_paths),
|
|
|
|
|
"user_path_carriers": len(all_user_paths),
|
|
|
|
|
"selected_user_path_carriers": len(selected_user_paths),
|
|
|
|
|
"document_user_path_carriers": len(document_user_paths),
|
|
|
|
|
"wire_duct_open_end_carriers": open_end_count,
|
|
|
|
|
"wiring_cut_out_carriers": len(cut_outs),
|
|
|
|
|
"surface_carriers": len(surfaces),
|
|
|
|
|
|