You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
541 lines
16 KiB
Python
541 lines
16 KiB
Python
# FreeCADExchange manual wiring helpers.
|
|
|
|
import json
|
|
|
|
import FreeCAD as App
|
|
|
|
try:
|
|
import FreeCADGui as Gui
|
|
except ImportError:
|
|
Gui = None
|
|
|
|
import TerminalObjects as TerminalObjects
|
|
import WiringObjects
|
|
|
|
try:
|
|
import DeviceImport
|
|
except Exception:
|
|
DeviceImport = None
|
|
|
|
|
|
class ManualWiringError(RuntimeError):
|
|
pass
|
|
|
|
|
|
def _append_debug_log(message):
|
|
if DeviceImport is None:
|
|
return
|
|
try:
|
|
DeviceImport._append_debug_log(message)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def _vector_from_point(point):
|
|
if isinstance(point, App.Vector):
|
|
return point
|
|
if isinstance(point, (list, tuple)) and len(point) >= 3:
|
|
return App.Vector(float(point[0]), float(point[1]), float(point[2]))
|
|
return None
|
|
|
|
|
|
def _vector_length(vector):
|
|
return (
|
|
float(getattr(vector, "x", 0.0)) ** 2
|
|
+ float(getattr(vector, "y", 0.0)) ** 2
|
|
+ float(getattr(vector, "z", 0.0)) ** 2
|
|
) ** 0.5
|
|
|
|
|
|
def _normalize_vector(vector, fallback=None):
|
|
length = _vector_length(vector)
|
|
if length <= 0.000001:
|
|
return fallback or App.Vector(0, 0, 1)
|
|
return App.Vector(
|
|
float(getattr(vector, "x", 0.0)) / length,
|
|
float(getattr(vector, "y", 0.0)) / length,
|
|
float(getattr(vector, "z", 0.0)) / length,
|
|
)
|
|
|
|
|
|
def _offset_point(point, direction, distance):
|
|
return App.Vector(
|
|
float(getattr(point, "x", 0.0)) + float(getattr(direction, "x", 0.0)) * distance,
|
|
float(getattr(point, "y", 0.0)) + float(getattr(direction, "y", 0.0)) * distance,
|
|
float(getattr(point, "z", 0.0)) + float(getattr(direction, "z", 0.0)) * distance,
|
|
)
|
|
|
|
|
|
def _vector_close(left, right, tolerance=0.000001):
|
|
return (
|
|
abs(float(getattr(left, "x", 0.0)) - float(getattr(right, "x", 0.0))) <= tolerance
|
|
and abs(float(getattr(left, "y", 0.0)) - float(getattr(right, "y", 0.0))) <= tolerance
|
|
and abs(float(getattr(left, "z", 0.0)) - float(getattr(right, "z", 0.0))) <= tolerance
|
|
)
|
|
|
|
|
|
def _axis_value(vector, axis):
|
|
return float(getattr(vector, axis, 0.0))
|
|
|
|
|
|
def _vector_with_axis(vector, axis, value):
|
|
return App.Vector(
|
|
float(value) if axis == "x" else float(getattr(vector, "x", 0.0)),
|
|
float(value) if axis == "y" else float(getattr(vector, "y", 0.0)),
|
|
float(value) if axis == "z" else float(getattr(vector, "z", 0.0)),
|
|
)
|
|
|
|
|
|
def _dominant_axis(vector):
|
|
if vector is None:
|
|
return None
|
|
components = {
|
|
"x": abs(float(getattr(vector, "x", 0.0))),
|
|
"y": abs(float(getattr(vector, "y", 0.0))),
|
|
"z": abs(float(getattr(vector, "z", 0.0))),
|
|
}
|
|
axis = max(components, key=components.get)
|
|
if components[axis] <= 0.000001:
|
|
return None
|
|
return axis
|
|
|
|
|
|
def _coerce_waypoint(point_like):
|
|
if isinstance(point_like, dict):
|
|
point = _vector_from_point(
|
|
point_like.get("point")
|
|
or point_like.get("base")
|
|
or point_like.get("position")
|
|
or point_like.get("origin")
|
|
)
|
|
if point is None and {"x", "y", "z"}.issubset(set(point_like.keys())):
|
|
point = App.Vector(
|
|
float(point_like.get("x", 0.0)),
|
|
float(point_like.get("y", 0.0)),
|
|
float(point_like.get("z", 0.0)),
|
|
)
|
|
if point is None:
|
|
return None
|
|
|
|
support_axis = (point_like.get("support_axis", "") or "").strip().lower()
|
|
if support_axis not in {"x", "y", "z"}:
|
|
support_axis = None
|
|
|
|
support_normal = _vector_from_point(point_like.get("support_normal"))
|
|
if support_axis is None:
|
|
support_axis = _dominant_axis(support_normal)
|
|
|
|
return {
|
|
"point": point,
|
|
"support_axis": support_axis,
|
|
"anchor_kind": (point_like.get("anchor_kind", "") or "").strip(),
|
|
"source_label": (point_like.get("source_label", "") or "").strip(),
|
|
"subelement_name": (point_like.get("subelement_name", "") or "").strip(),
|
|
}
|
|
|
|
point = _vector_from_point(point_like)
|
|
if point is None:
|
|
return None
|
|
return {
|
|
"point": point,
|
|
"support_axis": None,
|
|
"anchor_kind": "",
|
|
"source_label": "",
|
|
"subelement_name": "",
|
|
}
|
|
|
|
|
|
def _orthogonal_segment_points(start_point, end_point, preferred_axis=None):
|
|
if _vector_close(start_point, end_point):
|
|
return [start_point]
|
|
|
|
axis_order = []
|
|
if preferred_axis in {"x", "y", "z"}:
|
|
axis_order.append(preferred_axis)
|
|
|
|
remaining = sorted(
|
|
("x", "y", "z"),
|
|
key=lambda axis: abs(_axis_value(end_point, axis) - _axis_value(start_point, axis)),
|
|
reverse=True,
|
|
)
|
|
for axis in remaining:
|
|
if axis not in axis_order:
|
|
axis_order.append(axis)
|
|
|
|
points = [start_point]
|
|
current = start_point
|
|
for axis in axis_order:
|
|
target = _axis_value(end_point, axis)
|
|
if abs(_axis_value(current, axis) - target) <= 0.000001:
|
|
continue
|
|
current = _vector_with_axis(current, axis, target)
|
|
if not _vector_close(current, points[-1]):
|
|
points.append(current)
|
|
|
|
if not _vector_close(points[-1], end_point):
|
|
points.append(end_point)
|
|
return points
|
|
|
|
|
|
def _terminal_exit_direction(terminal):
|
|
placement = None
|
|
try:
|
|
if hasattr(terminal, "getGlobalPlacement"):
|
|
placement = terminal.getGlobalPlacement()
|
|
except Exception:
|
|
placement = None
|
|
|
|
if placement is None:
|
|
placement = getattr(terminal, "Placement", None)
|
|
|
|
rotation = getattr(placement, "Rotation", None)
|
|
if rotation is not None:
|
|
try:
|
|
return _normalize_vector(rotation.multVec(App.Vector(0, 0, 1)))
|
|
except Exception:
|
|
pass
|
|
|
|
return App.Vector(0, 0, 1)
|
|
|
|
|
|
def _manual_waypoints_payload(waypoints):
|
|
payload = []
|
|
for waypoint in waypoints or []:
|
|
point = waypoint.get("point")
|
|
if point is None:
|
|
continue
|
|
payload.append(
|
|
{
|
|
"point": {
|
|
"x": float(getattr(point, "x", 0.0)),
|
|
"y": float(getattr(point, "y", 0.0)),
|
|
"z": float(getattr(point, "z", 0.0)),
|
|
},
|
|
"support_axis": waypoint.get("support_axis", ""),
|
|
"anchor_kind": waypoint.get("anchor_kind", ""),
|
|
"source_label": waypoint.get("source_label", ""),
|
|
"subelement_name": waypoint.get("subelement_name", ""),
|
|
}
|
|
)
|
|
return payload
|
|
|
|
|
|
def _terminal_points(start_terminal, end_terminal, waypoints=None, terminal_exit_length=0.0):
|
|
start_origin = TerminalObjects.terminal_origin(start_terminal)
|
|
end_origin = TerminalObjects.terminal_origin(end_terminal)
|
|
exit_length = max(float(terminal_exit_length or 0.0), 0.0)
|
|
|
|
points = [start_origin]
|
|
normalized_waypoints = []
|
|
if exit_length > 0:
|
|
points.append(
|
|
_offset_point(
|
|
start_origin,
|
|
_terminal_exit_direction(start_terminal),
|
|
exit_length,
|
|
)
|
|
)
|
|
|
|
for point_like in waypoints or []:
|
|
waypoint = _coerce_waypoint(point_like)
|
|
if waypoint is None:
|
|
continue
|
|
normalized_waypoints.append(waypoint)
|
|
if not _vector_close(points[-1], waypoint["point"]):
|
|
points.append(waypoint["point"])
|
|
|
|
if exit_length > 0:
|
|
end_exit = _offset_point(
|
|
end_origin,
|
|
_terminal_exit_direction(end_terminal),
|
|
exit_length,
|
|
)
|
|
if not _vector_close(points[-1], end_exit):
|
|
points.append(end_exit)
|
|
if len(points) < 2 or not _vector_close(points[-1], end_origin):
|
|
points.append(end_origin)
|
|
return points, normalized_waypoints
|
|
|
|
|
|
def _wire_object_name(start_terminal, end_terminal):
|
|
start_uuid = TerminalObjects.safe_token(getattr(start_terminal, "QetTerminalUuid", ""))
|
|
end_uuid = TerminalObjects.safe_token(getattr(end_terminal, "QetTerminalUuid", ""))
|
|
return "QETWire_{0}_{1}".format(start_uuid, end_uuid)
|
|
|
|
|
|
def _set_wire_properties(
|
|
obj,
|
|
project_uuid,
|
|
start_terminal,
|
|
end_terminal,
|
|
wire_uuid="",
|
|
wire_label="",
|
|
net_uuid="",
|
|
group_uuid="",
|
|
wire_mark="",
|
|
wire_mark_is_manual=False,
|
|
manual_waypoints=None,
|
|
):
|
|
WiringObjects.set_routed_wire_semantics(
|
|
obj,
|
|
project_uuid,
|
|
wire_uuid,
|
|
wire_label,
|
|
getattr(start_terminal, "QetTerminalUuid", "").strip(),
|
|
getattr(end_terminal, "QetTerminalUuid", "").strip(),
|
|
getattr(start_terminal, "QetInstanceId", "").strip(),
|
|
getattr(end_terminal, "QetInstanceId", "").strip(),
|
|
route_type="Manual",
|
|
route_status="Routed",
|
|
route_mode="Manual",
|
|
net_uuid=net_uuid,
|
|
group_uuid=group_uuid,
|
|
wire_mark=wire_mark,
|
|
wire_mark_is_manual=wire_mark_is_manual,
|
|
)
|
|
try:
|
|
if "QetManualWaypointsJson" not in getattr(obj, "PropertiesList", []):
|
|
obj.addProperty(
|
|
"App::PropertyString",
|
|
"QetManualWaypointsJson",
|
|
"QET Wiring",
|
|
"Manual waypoint metadata",
|
|
)
|
|
obj.QetManualWaypointsJson = json.dumps(
|
|
_manual_waypoints_payload(manual_waypoints or []),
|
|
ensure_ascii=False,
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def _set_wire_points(obj, points):
|
|
try:
|
|
if "Points" not in getattr(obj, "PropertiesList", []):
|
|
obj.addProperty(
|
|
"App::PropertyVectorList",
|
|
"Points",
|
|
"QET Wiring",
|
|
"Manual route points",
|
|
)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
obj.Points = list(points)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def _create_wire_geometry(doc, wire_name, points):
|
|
if getattr(App, "ActiveDocument", None) is doc:
|
|
try:
|
|
import Draft
|
|
|
|
wire_obj = Draft.make_wire(
|
|
points,
|
|
closed=False,
|
|
placement=None,
|
|
face=None,
|
|
support=None,
|
|
bs2wire=False,
|
|
)
|
|
if wire_obj is not None:
|
|
_set_wire_points(wire_obj, points)
|
|
return wire_obj
|
|
except Exception as exc:
|
|
_append_debug_log("Draft wire creation failed, falling back to Part polygon: {0}".format(exc))
|
|
|
|
import Part
|
|
|
|
wire_obj = doc.addObject("Part::Feature", wire_name)
|
|
wire_obj.Shape = Part.makePolygon(points)
|
|
_set_wire_points(wire_obj, points)
|
|
return wire_obj
|
|
|
|
|
|
def _wire_parent_group(doc, project_uuid, start_terminal, end_terminal, fallback_group=None):
|
|
for terminal in (start_terminal, end_terminal):
|
|
element_uuid = getattr(terminal, "QetElementUuid", "").strip()
|
|
if not element_uuid:
|
|
continue
|
|
|
|
device_group = TerminalObjects.find_device_group(doc, element_uuid)
|
|
if device_group is None:
|
|
continue
|
|
|
|
device_instance_id = getattr(device_group, "QetInstanceId", "").strip()
|
|
return TerminalObjects.ensure_wire_group(
|
|
doc,
|
|
device_group,
|
|
project_uuid=project_uuid,
|
|
instance_id=device_instance_id,
|
|
)
|
|
|
|
return fallback_group
|
|
|
|
|
|
def create_manual_wire(
|
|
doc,
|
|
start_terminal,
|
|
end_terminal,
|
|
waypoints=None,
|
|
parent_group=None,
|
|
terminal_exit_length=0.0,
|
|
wire_uuid="",
|
|
wire_label="",
|
|
net_uuid="",
|
|
group_uuid="",
|
|
wire_mark="",
|
|
wire_mark_is_manual=False,
|
|
):
|
|
if not TerminalObjects.is_terminal_object(start_terminal):
|
|
raise ManualWiringError("The start selection is not a valid terminal.")
|
|
if not TerminalObjects.is_terminal_object(end_terminal):
|
|
raise ManualWiringError("The end selection is not a valid terminal.")
|
|
if start_terminal == end_terminal:
|
|
raise ManualWiringError("The start and end terminal must be different.")
|
|
|
|
project_uuid = (
|
|
getattr(start_terminal, "QetProjectUuid", "").strip()
|
|
or getattr(end_terminal, "QetProjectUuid", "").strip()
|
|
or getattr(TerminalObjects.ensure_root_group(doc), "QetProjectUuid", "").strip()
|
|
)
|
|
if not project_uuid:
|
|
raise ManualWiringError("A project UUID is required to create a wire.")
|
|
|
|
wire_base_name = TerminalObjects.safe_token(
|
|
_wire_object_name(start_terminal, end_terminal)
|
|
)
|
|
wire_name = wire_base_name
|
|
suffix = 1
|
|
while doc.getObject(wire_name) is not None:
|
|
wire_name = "{0}_{1}".format(wire_base_name, suffix)
|
|
suffix += 1
|
|
|
|
points, normalized_waypoints = _terminal_points(
|
|
start_terminal,
|
|
end_terminal,
|
|
waypoints=waypoints,
|
|
terminal_exit_length=terminal_exit_length,
|
|
)
|
|
if len(points) < 2:
|
|
raise ManualWiringError("A wire requires at least two points.")
|
|
|
|
wire_obj = _create_wire_geometry(doc, wire_name, points)
|
|
wire_obj.Label = wire_label or "QET Manual Wire"
|
|
_set_wire_properties(
|
|
wire_obj,
|
|
project_uuid,
|
|
start_terminal,
|
|
end_terminal,
|
|
wire_uuid=wire_uuid,
|
|
wire_label=wire_label,
|
|
net_uuid=net_uuid,
|
|
group_uuid=group_uuid,
|
|
wire_mark=wire_mark,
|
|
wire_mark_is_manual=wire_mark_is_manual,
|
|
manual_waypoints=normalized_waypoints,
|
|
)
|
|
|
|
routed_group = WiringObjects.ensure_routed_group(doc, project_uuid)
|
|
if wire_obj not in getattr(routed_group, "Group", []):
|
|
routed_group.addObject(wire_obj)
|
|
|
|
if parent_group is None:
|
|
try:
|
|
parent_group = _wire_parent_group(
|
|
doc,
|
|
project_uuid,
|
|
start_terminal,
|
|
end_terminal,
|
|
fallback_group=TerminalObjects.ensure_root_group(doc, project_uuid),
|
|
)
|
|
except Exception:
|
|
parent_group = None
|
|
if parent_group is not None and wire_obj not in getattr(parent_group, "Group", []):
|
|
parent_group.addObject(wire_obj)
|
|
if parent_group is not None:
|
|
try:
|
|
if (
|
|
getattr(parent_group, "Name", "").startswith(TerminalObjects.WIRE_GROUP_PREFIX)
|
|
or (getattr(parent_group, "QetGroupKind", "") or "").strip()
|
|
== TerminalObjects.WIRE_GROUP_KIND
|
|
):
|
|
parent_group.ViewObject.Visibility = False
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
wire_obj.ViewObject.LineWidth = 2.0
|
|
except Exception:
|
|
pass
|
|
try:
|
|
wire_obj.ViewObject.LineColor = (0.0, 0.6, 1.0)
|
|
except Exception:
|
|
pass
|
|
|
|
doc.recompute()
|
|
return wire_obj
|
|
|
|
|
|
class CommandCreateManualWire:
|
|
def GetResources(self):
|
|
return {
|
|
"MenuText": "连接选中端子",
|
|
"ToolTip": "在两个选中的工程端子之间创建手动导线",
|
|
}
|
|
|
|
def IsActive(self):
|
|
return App.ActiveDocument is not None and Gui is not None
|
|
|
|
def Activated(self):
|
|
if Gui is None:
|
|
return
|
|
|
|
selection = [
|
|
obj
|
|
for obj in Gui.Selection.getSelection()
|
|
if TerminalObjects.is_terminal_object(obj)
|
|
]
|
|
if len(selection) != 2:
|
|
try:
|
|
App.Console.PrintWarning(
|
|
"Select exactly two valid terminals before creating a wire.\n"
|
|
)
|
|
except Exception:
|
|
pass
|
|
return
|
|
|
|
try:
|
|
create_manual_wire(App.ActiveDocument, selection[0], selection[1])
|
|
try:
|
|
Gui.SendMsgToActiveView("ViewFit")
|
|
except Exception:
|
|
pass
|
|
except Exception as exc:
|
|
try:
|
|
App.Console.PrintError(
|
|
"[FreeCADExchange] manual wire creation failed: {0}\n".format(exc)
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
_COMMANDS_REGISTERED = False
|
|
|
|
|
|
def register_commands():
|
|
global _COMMANDS_REGISTERED
|
|
if _COMMANDS_REGISTERED:
|
|
return
|
|
if Gui is None:
|
|
return
|
|
try:
|
|
Gui.addCommand("QET_Exchange_CreateManualWire", CommandCreateManualWire())
|
|
_COMMANDS_REGISTERED = True
|
|
except Exception as exc:
|
|
_append_debug_log("failed to register manual wiring command: {0}".format(exc))
|
|
|
|
|
|
register_commands()
|