|
|
# FreeCADExchange wire task import helpers.
|
|
|
|
|
|
import json
|
|
|
|
|
|
import FreeCAD as App
|
|
|
|
|
|
import TerminalObjects
|
|
|
import WiringObjects
|
|
|
|
|
|
|
|
|
class WiringImportError(RuntimeError):
|
|
|
pass
|
|
|
|
|
|
|
|
|
def _string_value(item, field_name):
|
|
|
value = item.get(field_name, "")
|
|
|
if value is None:
|
|
|
return ""
|
|
|
if not isinstance(value, str):
|
|
|
return str(value).strip()
|
|
|
return value.strip()
|
|
|
|
|
|
|
|
|
def _bool_value(item, field_name):
|
|
|
return bool(item.get(field_name, False))
|
|
|
|
|
|
|
|
|
def _int_text_value(item, field_name):
|
|
|
value = item.get(field_name, "")
|
|
|
if value is None:
|
|
|
return ""
|
|
|
try:
|
|
|
return str(int(value)).strip()
|
|
|
except Exception:
|
|
|
return str(value).strip()
|
|
|
|
|
|
|
|
|
def _conductor_uuids(item):
|
|
|
values = item.get("conductor_uuids", [])
|
|
|
if not isinstance(values, list):
|
|
|
return []
|
|
|
return [str(value).strip() for value in values if str(value).strip()]
|
|
|
|
|
|
|
|
|
def _device_display_map(payload):
|
|
|
labels = {}
|
|
|
for item in payload.get("devices", []) or []:
|
|
|
if not isinstance(item, dict):
|
|
|
continue
|
|
|
display_tag = _string_value(item, "display_tag")
|
|
|
if not display_tag:
|
|
|
continue
|
|
|
|
|
|
# 新交换协议中,一个 3D 设备实例可能合并多个 2D 符号;
|
|
|
# 导线端点仍用 element_uuid,所以这里要把组内所有 2D 符号都映射到同一设备标注。
|
|
|
candidate_element_uuids = []
|
|
|
for terminal in item.get("terminals", []) or []:
|
|
|
if not isinstance(terminal, dict):
|
|
|
continue
|
|
|
element_uuid = _string_value(terminal, "element_uuid")
|
|
|
if element_uuid:
|
|
|
candidate_element_uuids.append(element_uuid)
|
|
|
|
|
|
for element_uuid in candidate_element_uuids:
|
|
|
labels[element_uuid] = display_tag
|
|
|
return labels
|
|
|
|
|
|
|
|
|
def _endpoint_instance_map(payload):
|
|
|
by_terminal = {}
|
|
|
by_element = {}
|
|
|
for device in payload.get("devices", []) or []:
|
|
|
if not isinstance(device, dict):
|
|
|
continue
|
|
|
device_instance_id = _string_value(device, "device_instance_id")
|
|
|
if not device_instance_id:
|
|
|
continue
|
|
|
for terminal in device.get("terminals", []) or []:
|
|
|
if not isinstance(terminal, dict):
|
|
|
continue
|
|
|
element_uuid = _string_value(terminal, "element_uuid")
|
|
|
terminal_uuid = _string_value(terminal, "terminal_uuid")
|
|
|
if element_uuid:
|
|
|
by_element.setdefault(element_uuid, device_instance_id)
|
|
|
if element_uuid and terminal_uuid:
|
|
|
by_terminal[(element_uuid, terminal_uuid)] = device_instance_id
|
|
|
return by_terminal, by_element
|
|
|
|
|
|
|
|
|
def _endpoint_text(device_label, terminal_display, terminal_uuid):
|
|
|
terminal = terminal_display or terminal_uuid
|
|
|
if device_label and terminal:
|
|
|
return "{0}:{1}".format(device_label, terminal)
|
|
|
return device_label or terminal or "未命名端子"
|
|
|
|
|
|
|
|
|
def _normalize_wire_entry(item, index, device_labels=None, endpoint_instances=None):
|
|
|
if not isinstance(item, dict):
|
|
|
raise WiringImportError("Wire entry #{0} must be an object.".format(index))
|
|
|
|
|
|
device_labels = device_labels or {}
|
|
|
endpoint_by_terminal, endpoint_by_element = endpoint_instances or ({}, {})
|
|
|
wire_uuid = (
|
|
|
_string_value(item, "wire_id")
|
|
|
or _string_value(item, "wire_uuid")
|
|
|
or _string_value(item, "id")
|
|
|
)
|
|
|
start_terminal_uuid = _string_value(item, "start_terminal_uuid")
|
|
|
end_terminal_uuid = _string_value(item, "end_terminal_uuid")
|
|
|
|
|
|
if not wire_uuid:
|
|
|
raise WiringImportError("Wire entry #{0} is missing wire_id.".format(index))
|
|
|
if not start_terminal_uuid or not end_terminal_uuid:
|
|
|
raise WiringImportError(
|
|
|
"Wire {0} is missing start_terminal_uuid or end_terminal_uuid.".format(
|
|
|
wire_uuid
|
|
|
)
|
|
|
)
|
|
|
|
|
|
wire_mark = _string_value(item, "wire_mark")
|
|
|
wire_label = _string_value(item, "wire_label") or wire_mark or wire_uuid
|
|
|
start_element_uuid = _string_value(item, "start_element_uuid")
|
|
|
end_element_uuid = _string_value(item, "end_element_uuid")
|
|
|
start_terminal_display = _string_value(item, "start_terminal_display")
|
|
|
end_terminal_display = _string_value(item, "end_terminal_display")
|
|
|
start_instance_id = endpoint_by_terminal.get(
|
|
|
(start_element_uuid, start_terminal_uuid),
|
|
|
endpoint_by_element.get(start_element_uuid, ""),
|
|
|
)
|
|
|
end_instance_id = endpoint_by_terminal.get(
|
|
|
(end_element_uuid, end_terminal_uuid),
|
|
|
endpoint_by_element.get(end_element_uuid, ""),
|
|
|
)
|
|
|
start_device_label = _string_value(item, "start_device_label") or device_labels.get(
|
|
|
start_element_uuid, ""
|
|
|
)
|
|
|
end_device_label = _string_value(item, "end_device_label") or device_labels.get(
|
|
|
end_element_uuid, ""
|
|
|
)
|
|
|
endpoint_label = "{0} -> {1}".format(
|
|
|
_endpoint_text(start_device_label, start_terminal_display, start_terminal_uuid),
|
|
|
_endpoint_text(end_device_label, end_terminal_display, end_terminal_uuid),
|
|
|
)
|
|
|
return {
|
|
|
"wire_uuid": wire_uuid,
|
|
|
"wire_label": wire_label,
|
|
|
"net_uuid": _string_value(item, "net_uuid"),
|
|
|
"group_uuid": _string_value(item, "group_uuid"),
|
|
|
"wire_mark": wire_mark,
|
|
|
"wire_mark_is_manual": _bool_value(item, "wire_mark_is_manual"),
|
|
|
"wire_style_id": _int_text_value(item, "wire_style_id"),
|
|
|
"start_element_uuid": start_element_uuid,
|
|
|
"start_terminal_uuid": start_terminal_uuid,
|
|
|
"start_instance_id": start_instance_id,
|
|
|
"start_terminal_display": start_terminal_display,
|
|
|
"start_device_label": start_device_label,
|
|
|
"end_element_uuid": end_element_uuid,
|
|
|
"end_terminal_uuid": end_terminal_uuid,
|
|
|
"end_instance_id": end_instance_id,
|
|
|
"end_terminal_display": end_terminal_display,
|
|
|
"end_device_label": end_device_label,
|
|
|
"endpoint_label": endpoint_label,
|
|
|
"conductor_uuids": _conductor_uuids(item),
|
|
|
}
|
|
|
|
|
|
|
|
|
def _unique_object_name(doc, base_name):
|
|
|
name = TerminalObjects.safe_token(base_name)
|
|
|
if doc.getObject(name) is None:
|
|
|
return name
|
|
|
suffix = 1
|
|
|
while doc.getObject("{0}_{1}".format(name, suffix)) is not None:
|
|
|
suffix += 1
|
|
|
return "{0}_{1}".format(name, suffix)
|
|
|
|
|
|
|
|
|
def _task_label(entry):
|
|
|
mark = entry["wire_mark"] or entry["wire_label"] or entry["wire_uuid"]
|
|
|
return "{0} {1}".format(mark, entry["endpoint_label"])
|
|
|
|
|
|
|
|
|
def _find_task_by_wire_uuid(task_group, wire_uuid):
|
|
|
target = (wire_uuid or "").strip()
|
|
|
if not target:
|
|
|
return None
|
|
|
for candidate in list(getattr(task_group, "Group", []) or []):
|
|
|
if getattr(candidate, "QetWireUuid", "").strip() == target:
|
|
|
return candidate
|
|
|
return None
|
|
|
|
|
|
|
|
|
def _ensure_string_property(obj, prop_name, value, description="QET wire task property"):
|
|
|
TerminalObjects.ensure_string_property(
|
|
|
obj,
|
|
|
prop_name,
|
|
|
"QET Wiring",
|
|
|
description,
|
|
|
value,
|
|
|
)
|
|
|
|
|
|
|
|
|
def _set_task_extra_properties(task, entry):
|
|
|
_ensure_string_property(task, "QetStartElementUuid", entry["start_element_uuid"])
|
|
|
_ensure_string_property(task, "QetEndElementUuid", entry["end_element_uuid"])
|
|
|
_ensure_string_property(task, "QetWireStyleId", entry["wire_style_id"])
|
|
|
_ensure_string_property(task, "QetStartTerminalDisplay", entry["start_terminal_display"])
|
|
|
_ensure_string_property(task, "QetEndTerminalDisplay", entry["end_terminal_display"])
|
|
|
_ensure_string_property(task, "QetStartDeviceLabel", entry["start_device_label"])
|
|
|
_ensure_string_property(task, "QetEndDeviceLabel", entry["end_device_label"])
|
|
|
_ensure_string_property(task, "QetEndpointLabel", entry["endpoint_label"])
|
|
|
_ensure_string_property(
|
|
|
task,
|
|
|
"QetConductorUuidsJson",
|
|
|
json.dumps(entry["conductor_uuids"], ensure_ascii=False),
|
|
|
)
|
|
|
|
|
|
|
|
|
def _upsert_wire_task(doc, task_group, project_uuid, entry):
|
|
|
task = _find_task_by_wire_uuid(task_group, entry["wire_uuid"])
|
|
|
created = task is None
|
|
|
previous_status = ""
|
|
|
if task is None:
|
|
|
task = doc.addObject(
|
|
|
"App::DocumentObjectGroup",
|
|
|
_unique_object_name(doc, "QETWireTask_{0}".format(entry["wire_uuid"])),
|
|
|
)
|
|
|
task_group.addObject(task)
|
|
|
else:
|
|
|
previous_status = (getattr(task, "RouteStatus", "") or "").strip()
|
|
|
|
|
|
WiringObjects.set_wire_task_semantics(
|
|
|
task,
|
|
|
project_uuid,
|
|
|
entry["wire_uuid"],
|
|
|
entry["wire_label"],
|
|
|
entry["start_terminal_uuid"],
|
|
|
entry["end_terminal_uuid"],
|
|
|
entry["start_instance_id"],
|
|
|
entry["end_instance_id"],
|
|
|
net_uuid=entry["net_uuid"],
|
|
|
group_uuid=entry["group_uuid"],
|
|
|
wire_mark=entry["wire_mark"],
|
|
|
wire_mark_is_manual=entry["wire_mark_is_manual"],
|
|
|
)
|
|
|
_set_task_extra_properties(task, entry)
|
|
|
if previous_status and previous_status != "Task":
|
|
|
TerminalObjects.ensure_string_property(
|
|
|
task,
|
|
|
"RouteStatus",
|
|
|
"QET Wiring",
|
|
|
"Wire task route status",
|
|
|
previous_status,
|
|
|
)
|
|
|
task.Label = _task_label(entry)
|
|
|
return task, created
|
|
|
|
|
|
|
|
|
def import_wire_tasks_from_payload(payload, doc=None):
|
|
|
if doc is None:
|
|
|
doc = getattr(App, "ActiveDocument", None)
|
|
|
if doc is None:
|
|
|
raise WiringImportError("No active FreeCAD document is available.")
|
|
|
if not isinstance(payload, dict):
|
|
|
raise WiringImportError("Exchange payload must be an object.")
|
|
|
|
|
|
project_uuid = _string_value(payload, "project_uuid")
|
|
|
if not project_uuid:
|
|
|
raise WiringImportError("Field 'project_uuid' is required for wire task import.")
|
|
|
|
|
|
wires = payload.get("wires", [])
|
|
|
if wires is None:
|
|
|
wires = []
|
|
|
if not isinstance(wires, list):
|
|
|
raise WiringImportError("Field 'wires' must be a list.")
|
|
|
|
|
|
task_group = WiringObjects.ensure_task_group(doc, project_uuid)
|
|
|
report = {
|
|
|
"project_uuid": project_uuid,
|
|
|
"total_wires": len(wires),
|
|
|
"imported_tasks": 0,
|
|
|
"updated_tasks": 0,
|
|
|
"removed_stale_tasks": 0,
|
|
|
"skipped_invalid": 0,
|
|
|
"warnings": [],
|
|
|
}
|
|
|
|
|
|
device_labels = _device_display_map(payload)
|
|
|
endpoint_instances = _endpoint_instance_map(payload)
|
|
|
for index, item in enumerate(wires):
|
|
|
try:
|
|
|
entry = _normalize_wire_entry(
|
|
|
item,
|
|
|
index,
|
|
|
device_labels=device_labels,
|
|
|
endpoint_instances=endpoint_instances,
|
|
|
)
|
|
|
except WiringImportError as exc:
|
|
|
report["skipped_invalid"] += 1
|
|
|
report["warnings"].append(str(exc))
|
|
|
continue
|
|
|
|
|
|
_task, created = _upsert_wire_task(doc, task_group, project_uuid, entry)
|
|
|
if created:
|
|
|
report["imported_tasks"] += 1
|
|
|
else:
|
|
|
report["updated_tasks"] += 1
|
|
|
|
|
|
return report
|