Compare commits
7 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
646209ba15 | 7 days ago |
|
|
38f9df431f | 7 days ago |
|
|
03cc72b0c4 | 7 days ago |
|
|
4549c908c7 | 7 days ago |
|
|
baf323b0c1 | 1 week ago |
|
|
45fdeba1d1 | 1 week ago |
|
|
d315f500e3 | 1 week ago |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,10 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
set "FC_LIBPACK=E:\fc\LibPack-1.1.0-v3.1.1.3-Release"
|
||||||
|
set "FC_RUN=E:\fc\run-FreeCAD-1.1.1"
|
||||||
|
set "FC_PYSIDE=%FC_LIBPACK%\bin\Lib\site-packages\PySide6"
|
||||||
|
set "FC_SHIBOKEN=%FC_LIBPACK%\bin\Lib\site-packages\shiboken6"
|
||||||
|
set "QT_PLUGIN_PATH=%FC_LIBPACK%\plugins"
|
||||||
|
set "QML2_IMPORT_PATH=%FC_LIBPACK%\qml"
|
||||||
|
set "PATH=%FC_LIBPACK%\bin;%FC_LIBPACK%\lib;%FC_PYSIDE%;%FC_SHIBOKEN%;%FC_RUN%\bin;%PATH%"
|
||||||
|
start "" /D "%FC_RUN%\bin" "%FC_RUN%\bin\FreeCAD.exe"
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
set(FreeCADExchange_Scripts
|
||||||
|
__init__.py
|
||||||
|
Init.py
|
||||||
|
InitGui.py
|
||||||
|
ExchangeBootstrap.py
|
||||||
|
DeviceImport.py
|
||||||
|
DevicePreview.py
|
||||||
|
)
|
||||||
|
|
||||||
|
add_custom_target(FreeCADExchangeScripts ALL
|
||||||
|
SOURCES ${FreeCADExchange_Scripts}
|
||||||
|
)
|
||||||
|
|
||||||
|
fc_target_copy_resource(FreeCADExchangeScripts
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
|
${CMAKE_BINARY_DIR}/Mod/FreeCADExchange
|
||||||
|
${FreeCADExchange_Scripts}
|
||||||
|
)
|
||||||
|
|
||||||
|
install(
|
||||||
|
FILES
|
||||||
|
${FreeCADExchange_Scripts}
|
||||||
|
DESTINATION
|
||||||
|
Mod/FreeCADExchange
|
||||||
|
)
|
||||||
@ -0,0 +1,749 @@
|
|||||||
|
import json
|
||||||
|
import traceback
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import FreeCAD as App
|
||||||
|
import FreeCADGui as Gui
|
||||||
|
import DeviceImport
|
||||||
|
import DevicePreview
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PySide6 import QtCore, QtWidgets
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
from PySide2 import QtCore, QtWidgets
|
||||||
|
except ImportError:
|
||||||
|
from PySide import QtCore
|
||||||
|
from PySide import QtGui as QtWidgets
|
||||||
|
|
||||||
|
|
||||||
|
ENV_JSON_PATH = "QET_2D_TO_3D_JSON"
|
||||||
|
ENV_SCENE_PATH = "QET_FREECAD_SCENE_FILE"
|
||||||
|
STATE_FLAG = "_qet_exchange_bootstrapped"
|
||||||
|
STATE_PAYLOAD = "_qet_exchange_payload"
|
||||||
|
STATE_SUMMARY = "_qet_exchange_summary"
|
||||||
|
STATE_IMPORT_REPORT = "_qet_exchange_import_report"
|
||||||
|
STATE_IMPORT_SCHEDULED = "_qet_exchange_import_scheduled"
|
||||||
|
STATE_TREE_FILTER = "_qet_exchange_tree_filter"
|
||||||
|
STATE_TREE_SIGNAL_CONNECTIONS = "_qet_exchange_tree_signal_connections"
|
||||||
|
TREE_FILTER_MARKER = "_qet_exchange_tree_filter_installed"
|
||||||
|
TREE_SIGNAL_MARKER = "_qet_exchange_tree_signal_installed"
|
||||||
|
IMPORT_READY_DELAY_MS = 1500
|
||||||
|
IMPORT_READY_RETRY_DELAY_MS = 1000
|
||||||
|
IMPORT_READY_MAX_RETRIES = 10
|
||||||
|
|
||||||
|
|
||||||
|
class ExchangeValidationError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _debug_log_path():
|
||||||
|
local_app_data = os.environ.get("LOCALAPPDATA", "").strip()
|
||||||
|
if local_app_data:
|
||||||
|
return os.path.join(local_app_data, "QETDeps", "freecad_exchange_bootstrap.log")
|
||||||
|
return os.path.join(str(Path.home()), "AppData", "Local", "QETDeps", "freecad_exchange_bootstrap.log")
|
||||||
|
|
||||||
|
|
||||||
|
def _append_debug_log(message):
|
||||||
|
try:
|
||||||
|
log_path = _debug_log_path()
|
||||||
|
os.makedirs(os.path.dirname(log_path), exist_ok=True)
|
||||||
|
with open(log_path, "a", encoding="utf-8") as handle:
|
||||||
|
handle.write(message + "\n")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _reset_debug_log():
|
||||||
|
try:
|
||||||
|
log_path = _debug_log_path()
|
||||||
|
os.makedirs(os.path.dirname(log_path), exist_ok=True)
|
||||||
|
with open(log_path, "w", encoding="utf-8") as handle:
|
||||||
|
handle.write("[QET Exchange] new debug session\n")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _get_main_window():
|
||||||
|
try:
|
||||||
|
return Gui.getMainWindow()
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _show_info(title, message):
|
||||||
|
QtWidgets.QMessageBox.information(_get_main_window(), title, message)
|
||||||
|
|
||||||
|
|
||||||
|
def _show_error(title, message):
|
||||||
|
QtWidgets.QMessageBox.critical(_get_main_window(), title, message)
|
||||||
|
|
||||||
|
|
||||||
|
def _qt_class_name(widget):
|
||||||
|
try:
|
||||||
|
return widget.metaObject().className()
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _has_tree_widget_parent(widget):
|
||||||
|
current = widget
|
||||||
|
while current is not None:
|
||||||
|
class_name = _qt_class_name(current)
|
||||||
|
if "TreeWidget" in class_name or class_name.endswith("QTreeWidget") or class_name.endswith("QTreeView"):
|
||||||
|
return True
|
||||||
|
current = current.parent()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class _DeviceTreeDoubleClickFilter(QtCore.QObject):
|
||||||
|
def eventFilter(self, watched, event):
|
||||||
|
try:
|
||||||
|
if event.type() != QtCore.QEvent.MouseButtonDblClick:
|
||||||
|
return False
|
||||||
|
_append_debug_log(
|
||||||
|
"tree double click captured: watched_class={0}".format(
|
||||||
|
_qt_class_name(watched)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
QtCore.QTimer.singleShot(0, _open_selected_device_preview_if_needed)
|
||||||
|
except Exception as exc:
|
||||||
|
_append_debug_log(
|
||||||
|
"tree double click filter failed: {0}".format(exc)
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class _DeviceTreeSignalBridge(QtCore.QObject):
|
||||||
|
def on_item_double_clicked(self, *args):
|
||||||
|
_append_debug_log("tree double click signal received: itemDoubleClicked")
|
||||||
|
QtCore.QTimer.singleShot(0, _open_selected_device_preview_if_needed)
|
||||||
|
|
||||||
|
def on_index_double_clicked(self, *args):
|
||||||
|
_append_debug_log("tree double click signal received: doubleClicked")
|
||||||
|
QtCore.QTimer.singleShot(0, _open_selected_device_preview_if_needed)
|
||||||
|
|
||||||
|
|
||||||
|
def _install_tree_double_click_filter():
|
||||||
|
main_window = _get_main_window()
|
||||||
|
if main_window is None:
|
||||||
|
return
|
||||||
|
if getattr(Gui, STATE_TREE_FILTER, None) is not None:
|
||||||
|
tree_filter = getattr(Gui, STATE_TREE_FILTER)
|
||||||
|
else:
|
||||||
|
tree_filter = _DeviceTreeDoubleClickFilter(main_window)
|
||||||
|
setattr(Gui, STATE_TREE_FILTER, tree_filter)
|
||||||
|
|
||||||
|
if getattr(Gui, STATE_TREE_SIGNAL_CONNECTIONS, None) is not None:
|
||||||
|
signal_bridge = getattr(Gui, STATE_TREE_SIGNAL_CONNECTIONS)
|
||||||
|
else:
|
||||||
|
signal_bridge = _DeviceTreeSignalBridge(main_window)
|
||||||
|
setattr(Gui, STATE_TREE_SIGNAL_CONNECTIONS, signal_bridge)
|
||||||
|
|
||||||
|
installed_count = 0
|
||||||
|
signal_count = 0
|
||||||
|
for widget in main_window.findChildren(QtWidgets.QWidget):
|
||||||
|
class_name = _qt_class_name(widget)
|
||||||
|
if "TreeWidget" not in class_name and not class_name.endswith("QTreeWidget") and not class_name.endswith("QTreeView"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
targets = [widget]
|
||||||
|
viewport = getattr(widget, "viewport", lambda: None)()
|
||||||
|
if viewport is not None:
|
||||||
|
targets.append(viewport)
|
||||||
|
|
||||||
|
for target in targets:
|
||||||
|
if target is None:
|
||||||
|
continue
|
||||||
|
if bool(target.property(TREE_FILTER_MARKER)):
|
||||||
|
continue
|
||||||
|
target.installEventFilter(tree_filter)
|
||||||
|
target.setProperty(TREE_FILTER_MARKER, True)
|
||||||
|
installed_count += 1
|
||||||
|
_append_debug_log(
|
||||||
|
"tree double click filter attached: class={0}".format(
|
||||||
|
_qt_class_name(target)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not bool(widget.property(TREE_SIGNAL_MARKER)):
|
||||||
|
connected = False
|
||||||
|
item_double_clicked = getattr(widget, "itemDoubleClicked", None)
|
||||||
|
if item_double_clicked is not None:
|
||||||
|
try:
|
||||||
|
item_double_clicked.connect(signal_bridge.on_item_double_clicked)
|
||||||
|
connected = True
|
||||||
|
signal_count += 1
|
||||||
|
_append_debug_log(
|
||||||
|
"tree double click signal connected: class={0}, signal=itemDoubleClicked".format(
|
||||||
|
class_name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
_append_debug_log(
|
||||||
|
"tree double click signal connect failed: class={0}, signal=itemDoubleClicked, error={1}".format(
|
||||||
|
class_name, exc
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
view_double_clicked = getattr(widget, "doubleClicked", None)
|
||||||
|
if view_double_clicked is not None:
|
||||||
|
try:
|
||||||
|
view_double_clicked.connect(signal_bridge.on_index_double_clicked)
|
||||||
|
connected = True
|
||||||
|
signal_count += 1
|
||||||
|
_append_debug_log(
|
||||||
|
"tree double click signal connected: class={0}, signal=doubleClicked".format(
|
||||||
|
class_name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
_append_debug_log(
|
||||||
|
"tree double click signal connect failed: class={0}, signal=doubleClicked, error={1}".format(
|
||||||
|
class_name, exc
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if connected:
|
||||||
|
widget.setProperty(TREE_SIGNAL_MARKER, True)
|
||||||
|
|
||||||
|
_append_debug_log(
|
||||||
|
"tree double click filter install pass complete: attached={0}, signal_connections={1}".format(
|
||||||
|
installed_count,
|
||||||
|
signal_count,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _open_selected_device_preview_if_needed():
|
||||||
|
try:
|
||||||
|
selection = Gui.Selection.getSelection()
|
||||||
|
_append_debug_log(
|
||||||
|
"tree double click selection count={0}".format(len(selection))
|
||||||
|
)
|
||||||
|
if len(selection) != 1:
|
||||||
|
return
|
||||||
|
obj = selection[0]
|
||||||
|
_append_debug_log(
|
||||||
|
"tree double click selected object: name={0}, label={1}, type={2}, doc={3}".format(
|
||||||
|
getattr(obj, "Name", ""),
|
||||||
|
getattr(obj, "Label", ""),
|
||||||
|
getattr(obj, "TypeId", ""),
|
||||||
|
getattr(getattr(obj, "Document", None), "Name", ""),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
device_obj = DevicePreview.find_parent_qet_device_object(obj)
|
||||||
|
_append_debug_log(
|
||||||
|
"tree double click resolved device object: name={0}, label={1}, doc={2}".format(
|
||||||
|
getattr(device_obj, "Name", "") if device_obj else "",
|
||||||
|
getattr(device_obj, "Label", "") if device_obj else "",
|
||||||
|
getattr(getattr(device_obj, "Document", None), "Name", "") if device_obj else "",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if device_obj is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if DevicePreview.is_preview_document_name(
|
||||||
|
getattr(getattr(device_obj, "Document", None), "Name", "")
|
||||||
|
):
|
||||||
|
_append_debug_log("tree double click ignored inside preview document")
|
||||||
|
return
|
||||||
|
|
||||||
|
DevicePreview.open_preview_for_device_object(device_obj)
|
||||||
|
_append_debug_log("tree double click preview open requested")
|
||||||
|
except Exception as exc:
|
||||||
|
_append_debug_log(
|
||||||
|
"open selected device preview failed: {0}".format(exc)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_gui_ready():
|
||||||
|
main_window = _get_main_window()
|
||||||
|
if main_window is None:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
return bool(main_window.isVisible())
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _require_string(payload, field_name):
|
||||||
|
value = payload.get(field_name)
|
||||||
|
if not isinstance(value, str) or not value.strip():
|
||||||
|
raise ExchangeValidationError(
|
||||||
|
"Field '{0}' must be a non-empty string.".format(field_name)
|
||||||
|
)
|
||||||
|
return value.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_instance_id(item):
|
||||||
|
value = item.get("instance_id", "")
|
||||||
|
if value is None:
|
||||||
|
return ""
|
||||||
|
if not isinstance(value, str):
|
||||||
|
raise ExchangeValidationError(
|
||||||
|
"Field 'instance_id' must be a string when present."
|
||||||
|
)
|
||||||
|
return value.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_devices(payload):
|
||||||
|
devices = payload.get("devices", [])
|
||||||
|
if not isinstance(devices, list):
|
||||||
|
raise ExchangeValidationError("Field 'devices' must be a list.")
|
||||||
|
|
||||||
|
normalized = []
|
||||||
|
for index, item in enumerate(devices):
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
raise ExchangeValidationError(
|
||||||
|
"Device entry #{0} must be an object.".format(index)
|
||||||
|
)
|
||||||
|
element_uuid = _require_string(item, "element_uuid")
|
||||||
|
display_tag = item.get("display_tag", "")
|
||||||
|
if display_tag and not isinstance(display_tag, str):
|
||||||
|
raise ExchangeValidationError(
|
||||||
|
"Field 'display_tag' in device entry #{0} must be a string.".format(
|
||||||
|
index
|
||||||
|
)
|
||||||
|
)
|
||||||
|
normalized.append(
|
||||||
|
{
|
||||||
|
"element_uuid": element_uuid,
|
||||||
|
"instance_id": _normalize_instance_id(item),
|
||||||
|
"display_tag": display_tag.strip() if isinstance(display_tag, str) else "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_terminals(payload):
|
||||||
|
terminals = payload.get("terminals", [])
|
||||||
|
if not isinstance(terminals, list):
|
||||||
|
raise ExchangeValidationError("Field 'terminals' must be a list.")
|
||||||
|
|
||||||
|
normalized = []
|
||||||
|
for index, item in enumerate(terminals):
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
raise ExchangeValidationError(
|
||||||
|
"Terminal entry #{0} must be an object.".format(index)
|
||||||
|
)
|
||||||
|
terminal_uuid = _require_string(item, "terminal_uuid")
|
||||||
|
element_uuid = item.get("element_uuid", "")
|
||||||
|
if element_uuid and not isinstance(element_uuid, str):
|
||||||
|
raise ExchangeValidationError(
|
||||||
|
"Field 'element_uuid' in terminal entry #{0} must be a string.".format(
|
||||||
|
index
|
||||||
|
)
|
||||||
|
)
|
||||||
|
normalized.append(
|
||||||
|
{
|
||||||
|
"terminal_uuid": terminal_uuid,
|
||||||
|
"instance_id": _normalize_instance_id(item),
|
||||||
|
"element_uuid": element_uuid.strip() if isinstance(element_uuid, str) else "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_device_models(payload):
|
||||||
|
models = payload.get("device_models", [])
|
||||||
|
if not isinstance(models, list):
|
||||||
|
raise ExchangeValidationError("Field 'device_models' must be a list.")
|
||||||
|
|
||||||
|
normalized = []
|
||||||
|
for index, item in enumerate(models):
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
raise ExchangeValidationError(
|
||||||
|
"Device model entry #{0} must be an object.".format(index)
|
||||||
|
)
|
||||||
|
element_uuid = _require_string(item, "element_uuid")
|
||||||
|
parts_3d = item.get("parts_3d", "")
|
||||||
|
if parts_3d and not isinstance(parts_3d, str):
|
||||||
|
raise ExchangeValidationError(
|
||||||
|
"Field 'parts_3d' in device model entry #{0} must be a string.".format(
|
||||||
|
index
|
||||||
|
)
|
||||||
|
)
|
||||||
|
resolved_model_path = item.get("resolved_model_path", "")
|
||||||
|
if resolved_model_path and not isinstance(resolved_model_path, str):
|
||||||
|
raise ExchangeValidationError(
|
||||||
|
"Field 'resolved_model_path' in device model entry #{0} must be a string.".format(
|
||||||
|
index
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
device_id = item.get("device_id")
|
||||||
|
if device_id is not None and not isinstance(device_id, int):
|
||||||
|
raise ExchangeValidationError(
|
||||||
|
"Field 'device_id' in device model entry #{0} must be an integer.".format(
|
||||||
|
index
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
normalized.append(
|
||||||
|
{
|
||||||
|
"element_uuid": element_uuid,
|
||||||
|
"device_id": device_id,
|
||||||
|
"parts_3d": parts_3d.strip() if isinstance(parts_3d, str) else "",
|
||||||
|
"resolved_model_path": (
|
||||||
|
resolved_model_path.strip()
|
||||||
|
if isinstance(resolved_model_path, str)
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_cabinet(payload):
|
||||||
|
cabinet = payload.get("cabinet")
|
||||||
|
if cabinet is None:
|
||||||
|
return None
|
||||||
|
if not isinstance(cabinet, dict):
|
||||||
|
raise ExchangeValidationError("Field 'cabinet' must be an object when present.")
|
||||||
|
|
||||||
|
location_id = cabinet.get("location_id")
|
||||||
|
if location_id is not None and not isinstance(location_id, int):
|
||||||
|
raise ExchangeValidationError("Field 'location_id' in cabinet must be an integer.")
|
||||||
|
|
||||||
|
normalized = {
|
||||||
|
"location_id": location_id,
|
||||||
|
"label": "",
|
||||||
|
"name": "",
|
||||||
|
"display_text": "",
|
||||||
|
"associated_fileset": "",
|
||||||
|
"three_d_relative_path": "",
|
||||||
|
"resolved_scene_path": "",
|
||||||
|
}
|
||||||
|
for field_name in (
|
||||||
|
"label",
|
||||||
|
"name",
|
||||||
|
"display_text",
|
||||||
|
"associated_fileset",
|
||||||
|
"three_d_relative_path",
|
||||||
|
"resolved_scene_path",
|
||||||
|
):
|
||||||
|
value = cabinet.get(field_name, "")
|
||||||
|
if value and not isinstance(value, str):
|
||||||
|
raise ExchangeValidationError(
|
||||||
|
"Field '{0}' in cabinet must be a string.".format(field_name)
|
||||||
|
)
|
||||||
|
normalized[field_name] = value.strip() if isinstance(value, str) else ""
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def load_exchange_payload(json_path):
|
||||||
|
try:
|
||||||
|
raw_text = Path(json_path).read_text(encoding="utf-8")
|
||||||
|
except OSError as exc:
|
||||||
|
raise ExchangeValidationError(
|
||||||
|
"Failed to read exchange file:\n{0}".format(exc)
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = json.loads(raw_text)
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
raise ExchangeValidationError(
|
||||||
|
"Exchange JSON is invalid:\n{0}".format(exc)
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
raise ExchangeValidationError("Exchange JSON root must be an object.")
|
||||||
|
|
||||||
|
project_uuid = _require_string(payload, "project_uuid")
|
||||||
|
schema_version = payload.get("schema_version", "1.0")
|
||||||
|
if not isinstance(schema_version, str) or not schema_version.strip():
|
||||||
|
raise ExchangeValidationError("Field 'schema_version' must be a string.")
|
||||||
|
|
||||||
|
normalized = {
|
||||||
|
"schema_version": schema_version.strip(),
|
||||||
|
"project_uuid": project_uuid,
|
||||||
|
"generated_at": payload.get("generated_at", ""),
|
||||||
|
"source": payload.get("source", {}),
|
||||||
|
"cabinet": _normalize_cabinet(payload),
|
||||||
|
"devices": _normalize_devices(payload),
|
||||||
|
"terminals": _normalize_terminals(payload),
|
||||||
|
"device_models": _normalize_device_models(payload),
|
||||||
|
}
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def _build_summary(payload, json_path):
|
||||||
|
devices = payload["devices"]
|
||||||
|
terminals = payload["terminals"]
|
||||||
|
device_models = payload["device_models"]
|
||||||
|
cabinet = payload.get("cabinet")
|
||||||
|
missing_device_instances = sum(1 for item in devices if not item["instance_id"])
|
||||||
|
missing_terminal_instances = sum(
|
||||||
|
1 for item in terminals if not item["instance_id"]
|
||||||
|
)
|
||||||
|
with_model_paths = sum(
|
||||||
|
1 for item in device_models if item["resolved_model_path"] or item["parts_3d"]
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"json_path": json_path,
|
||||||
|
"project_uuid": payload["project_uuid"],
|
||||||
|
"device_count": len(devices),
|
||||||
|
"terminal_count": len(terminals),
|
||||||
|
"device_model_count": len(device_models),
|
||||||
|
"device_models_with_parts": with_model_paths,
|
||||||
|
"missing_device_instances": missing_device_instances,
|
||||||
|
"missing_terminal_instances": missing_terminal_instances,
|
||||||
|
"cabinet": cabinet,
|
||||||
|
"scene_path": os.environ.get(ENV_SCENE_PATH, "").strip(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _summary_message(summary, import_report=None):
|
||||||
|
lines = [
|
||||||
|
"QET exchange file loaded successfully.",
|
||||||
|
"",
|
||||||
|
"Project UUID: {0}".format(summary["project_uuid"]),
|
||||||
|
"Exchange file: {0}".format(summary["json_path"]),
|
||||||
|
"Devices: {0}".format(summary["device_count"]),
|
||||||
|
"Terminals: {0}".format(summary["terminal_count"]),
|
||||||
|
"Device models: {0}".format(summary["device_model_count"]),
|
||||||
|
"Resolved model paths: {0}".format(summary["device_models_with_parts"]),
|
||||||
|
]
|
||||||
|
|
||||||
|
cabinet = summary.get("cabinet")
|
||||||
|
if isinstance(cabinet, dict):
|
||||||
|
cabinet_name = cabinet.get("display_text") or cabinet.get("label") or cabinet.get("name")
|
||||||
|
if cabinet_name:
|
||||||
|
lines.append("Cabinet: {0}".format(cabinet_name))
|
||||||
|
if cabinet.get("associated_fileset"):
|
||||||
|
lines.append("Cabinet fileset: {0}".format(cabinet["associated_fileset"]))
|
||||||
|
if cabinet.get("three_d_relative_path"):
|
||||||
|
lines.append(
|
||||||
|
"Cabinet 3D relative path: {0}".format(
|
||||||
|
cabinet["three_d_relative_path"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if cabinet.get("resolved_scene_path"):
|
||||||
|
lines.append(
|
||||||
|
"Cabinet resolved scene path: {0}".format(
|
||||||
|
cabinet["resolved_scene_path"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if summary["missing_device_instances"]:
|
||||||
|
lines.append(
|
||||||
|
"Devices without instance_id yet: {0}".format(
|
||||||
|
summary["missing_device_instances"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if summary["missing_terminal_instances"]:
|
||||||
|
lines.append(
|
||||||
|
"Terminals without instance_id yet: {0}".format(
|
||||||
|
summary["missing_terminal_instances"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if summary["scene_path"]:
|
||||||
|
lines.append("Scene file: {0}".format(summary["scene_path"]))
|
||||||
|
|
||||||
|
if import_report:
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"3D device import summary:",
|
||||||
|
"Target document: {0}".format(import_report["document_name"]),
|
||||||
|
"Imported cabinet models: {0}".format(import_report["cabinet_imported"]),
|
||||||
|
"Imported devices: {0}".format(import_report["imported_devices"]),
|
||||||
|
"Updated devices: {0}".format(import_report["updated_devices"]),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if import_report["imported_without_instance_id"]:
|
||||||
|
lines.append(
|
||||||
|
"Imported without instance_id yet: {0}".format(
|
||||||
|
import_report["imported_without_instance_id"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if import_report["skipped_missing_model"]:
|
||||||
|
lines.append(
|
||||||
|
"Skipped without resolved model path: {0}".format(
|
||||||
|
import_report["skipped_missing_model"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if import_report["skipped_missing_file"]:
|
||||||
|
lines.append(
|
||||||
|
"Skipped missing model file: {0}".format(
|
||||||
|
import_report["skipped_missing_file"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if import_report["skipped_unsupported_format"]:
|
||||||
|
lines.append(
|
||||||
|
"Skipped unsupported model format: {0}".format(
|
||||||
|
import_report["skipped_unsupported_format"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if import_report["skipped_import_error"]:
|
||||||
|
lines.append(
|
||||||
|
"Skipped after import errors: {0}".format(
|
||||||
|
import_report["skipped_import_error"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
warnings = import_report.get("warnings", [])
|
||||||
|
if warnings:
|
||||||
|
lines.append("")
|
||||||
|
lines.append("Warnings:")
|
||||||
|
lines.extend("- {0}".format(item) for item in warnings[:10])
|
||||||
|
if len(warnings) > 10:
|
||||||
|
lines.append("- ... ({0} more)".format(len(warnings) - 10))
|
||||||
|
|
||||||
|
lines.append("")
|
||||||
|
lines.append("This step validates the exchange payload and imports devices with valid resolved model paths.")
|
||||||
|
lines.append("3D terminal creation is not running yet.")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _run_scheduled_device_import(attempt=0):
|
||||||
|
_append_debug_log(
|
||||||
|
"scheduled device import invoked: attempt={0}".format(attempt)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not _is_gui_ready():
|
||||||
|
if attempt < IMPORT_READY_MAX_RETRIES:
|
||||||
|
_append_debug_log(
|
||||||
|
"scheduled device import postponed: gui not ready, retrying"
|
||||||
|
)
|
||||||
|
QtCore.QTimer.singleShot(
|
||||||
|
IMPORT_READY_RETRY_DELAY_MS,
|
||||||
|
lambda: _run_scheduled_device_import(attempt + 1),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
_append_debug_log("scheduled device import aborted: gui never became ready")
|
||||||
|
_show_error(
|
||||||
|
"QET Exchange",
|
||||||
|
"FreeCAD main window did not finish initializing before device import.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
payload = getattr(App, STATE_PAYLOAD, None)
|
||||||
|
summary = getattr(App, STATE_SUMMARY, None)
|
||||||
|
if not isinstance(payload, dict) or not isinstance(summary, dict):
|
||||||
|
_append_debug_log(
|
||||||
|
"scheduled device import aborted: cached payload/summary missing"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
scene_path = os.environ.get(ENV_SCENE_PATH, "").strip()
|
||||||
|
_append_debug_log(
|
||||||
|
"scheduled device import starting with scene_path={0}".format(scene_path)
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
import_report = DeviceImport.import_devices_from_payload(payload, scene_path)
|
||||||
|
except DeviceImport.DeviceImportError as exc:
|
||||||
|
_append_debug_log("device import failed: {0}".format(exc))
|
||||||
|
_show_error("QET Exchange", str(exc))
|
||||||
|
App.Console.PrintError("[FreeCADExchange] {0}\n".format(exc))
|
||||||
|
return
|
||||||
|
except Exception as exc:
|
||||||
|
_append_debug_log("unexpected device import exception: {0}".format(exc))
|
||||||
|
_append_debug_log(traceback.format_exc())
|
||||||
|
_show_error("QET Exchange", "Failed to import 3D devices:\n{0}".format(exc))
|
||||||
|
App.Console.PrintError(
|
||||||
|
"[FreeCADExchange] Failed to import devices: {0}\n".format(exc)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
setattr(App, STATE_IMPORT_REPORT, import_report)
|
||||||
|
_append_debug_log(
|
||||||
|
"device import summary: imported={0}, updated={1}, skipped_missing_model={2}, skipped_missing_file={3}, skipped_import_error={4}".format(
|
||||||
|
import_report["imported_devices"],
|
||||||
|
import_report["updated_devices"],
|
||||||
|
import_report["skipped_missing_model"],
|
||||||
|
import_report["skipped_missing_file"],
|
||||||
|
import_report["skipped_import_error"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
App.Console.PrintMessage(
|
||||||
|
"[FreeCADExchange] Loaded exchange payload from {0}\n".format(
|
||||||
|
summary["json_path"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
App.Console.PrintMessage(
|
||||||
|
"[FreeCADExchange] Devices: {0}, Terminals: {1}, Device models: {2}\n".format(
|
||||||
|
summary["device_count"],
|
||||||
|
summary["terminal_count"],
|
||||||
|
summary["device_model_count"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
App.Console.PrintMessage(
|
||||||
|
"[FreeCADExchange] Imported devices: {0}, updated: {1}, skipped without model: {2}\n".format(
|
||||||
|
import_report["imported_devices"],
|
||||||
|
import_report["updated_devices"],
|
||||||
|
import_report["skipped_missing_model"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
_show_info("QET Exchange", _summary_message(summary, import_report))
|
||||||
|
_append_debug_log("summary dialog shown")
|
||||||
|
|
||||||
|
|
||||||
|
def bootstrap_if_requested():
|
||||||
|
if not getattr(App, STATE_FLAG, False):
|
||||||
|
_reset_debug_log()
|
||||||
|
_append_debug_log("bootstrap_if_requested entered")
|
||||||
|
_install_tree_double_click_filter()
|
||||||
|
if getattr(App, STATE_FLAG, False):
|
||||||
|
_append_debug_log("bootstrap_if_requested skipped: already bootstrapped")
|
||||||
|
return
|
||||||
|
|
||||||
|
json_path = os.environ.get(ENV_JSON_PATH, "").strip()
|
||||||
|
_append_debug_log("ENV QET_2D_TO_3D_JSON={0}".format(json_path))
|
||||||
|
_append_debug_log("ENV QET_FREECAD_SCENE_FILE={0}".format(os.environ.get(ENV_SCENE_PATH, "").strip()))
|
||||||
|
if not json_path:
|
||||||
|
_append_debug_log("bootstrap_if_requested skipped: env missing")
|
||||||
|
return
|
||||||
|
|
||||||
|
setattr(App, STATE_FLAG, True)
|
||||||
|
_append_debug_log("STATE_FLAG set")
|
||||||
|
|
||||||
|
if not os.path.isfile(json_path):
|
||||||
|
_append_debug_log("exchange file missing: {0}".format(json_path))
|
||||||
|
_show_error(
|
||||||
|
"QET Exchange",
|
||||||
|
"Environment variable {0} points to a missing file:\n{1}".format(
|
||||||
|
ENV_JSON_PATH, json_path
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = load_exchange_payload(json_path)
|
||||||
|
except ExchangeValidationError as exc:
|
||||||
|
_append_debug_log("payload validation failed: {0}".format(exc))
|
||||||
|
_show_error("QET Exchange", str(exc))
|
||||||
|
App.Console.PrintError("[FreeCADExchange] {0}\n".format(exc))
|
||||||
|
return
|
||||||
|
except Exception as exc:
|
||||||
|
_append_debug_log("unexpected payload exception: {0}".format(exc))
|
||||||
|
_append_debug_log(traceback.format_exc())
|
||||||
|
raise
|
||||||
|
|
||||||
|
summary = _build_summary(payload, json_path)
|
||||||
|
_append_debug_log(
|
||||||
|
"payload loaded: devices={0}, terminals={1}, models={2}, cabinet={3}".format(
|
||||||
|
summary["device_count"],
|
||||||
|
summary["terminal_count"],
|
||||||
|
summary["device_model_count"],
|
||||||
|
"yes" if summary.get("cabinet") else "no",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setattr(App, STATE_PAYLOAD, payload)
|
||||||
|
setattr(App, STATE_SUMMARY, summary)
|
||||||
|
if not getattr(App, STATE_IMPORT_SCHEDULED, False):
|
||||||
|
setattr(App, STATE_IMPORT_SCHEDULED, True)
|
||||||
|
_append_debug_log(
|
||||||
|
"device import scheduled after startup delay: {0} ms".format(
|
||||||
|
IMPORT_READY_DELAY_MS
|
||||||
|
)
|
||||||
|
)
|
||||||
|
QtCore.QTimer.singleShot(
|
||||||
|
IMPORT_READY_DELAY_MS, lambda: _run_scheduled_device_import(0)
|
||||||
|
)
|
||||||
@ -0,0 +1 @@
|
|||||||
|
# FreeCADExchange init module.
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
# FreeCADExchange gui init module.
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PySide6 import QtCore
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
from PySide2 import QtCore
|
||||||
|
except ImportError:
|
||||||
|
from PySide import QtCore
|
||||||
|
|
||||||
|
import ExchangeBootstrap
|
||||||
|
|
||||||
|
|
||||||
|
def _append_init_log(message):
|
||||||
|
try:
|
||||||
|
local_app_data = os.environ.get("LOCALAPPDATA", "").strip()
|
||||||
|
if local_app_data:
|
||||||
|
log_path = os.path.join(local_app_data, "QETDeps", "freecad_exchange_bootstrap.log")
|
||||||
|
else:
|
||||||
|
log_path = os.path.join(str(Path.home()), "AppData", "Local", "QETDeps", "freecad_exchange_bootstrap.log")
|
||||||
|
os.makedirs(os.path.dirname(log_path), exist_ok=True)
|
||||||
|
with open(log_path, "a", encoding="utf-8") as handle:
|
||||||
|
handle.write(message + "\n")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
_append_init_log("InitGui imported")
|
||||||
|
|
||||||
|
|
||||||
|
QtCore.QTimer.singleShot(0, ExchangeBootstrap.bootstrap_if_requested)
|
||||||
@ -0,0 +1 @@
|
|||||||
|
# FreeCADExchange Python module.
|
||||||
@ -0,0 +1,250 @@
|
|||||||
|
param(
|
||||||
|
[string]$RunRoot = "",
|
||||||
|
[string]$LibPackRoot = "",
|
||||||
|
[string]$OcctRoot = "",
|
||||||
|
[string]$RuntimeRoot = "",
|
||||||
|
[switch]$SkipRuntimeJson
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
function Resolve-NormalizedPath {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Path
|
||||||
|
)
|
||||||
|
|
||||||
|
return [System.IO.Path]::GetFullPath($Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-ConfiguredPath {
|
||||||
|
param(
|
||||||
|
[string]$ConfiguredPath,
|
||||||
|
[string[]]$EnvironmentVariableNames
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($ConfiguredPath)) {
|
||||||
|
return Resolve-NormalizedPath -Path $ConfiguredPath
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($variableName in $EnvironmentVariableNames) {
|
||||||
|
$value = [Environment]::GetEnvironmentVariable($variableName)
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($value)) {
|
||||||
|
return Resolve-NormalizedPath -Path $value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function Ensure-Directory {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Path
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $Path)) {
|
||||||
|
New-Item -ItemType Directory -Path $Path | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Copy-MatchingFiles {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$SourceDir,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string[]]$Patterns,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$DestinationDir
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $SourceDir)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($pattern in $Patterns) {
|
||||||
|
Get-ChildItem -LiteralPath $SourceDir -Filter $pattern -File -ErrorAction SilentlyContinue | ForEach-Object {
|
||||||
|
Copy-Item -LiteralPath $_.FullName -Destination (Join-Path $DestinationDir $_.Name) -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Copy-PluginDirectory {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$SourceDir,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$DestinationDir
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $SourceDir)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Ensure-Directory -Path $DestinationDir
|
||||||
|
Get-ChildItem -LiteralPath $SourceDir -Force | ForEach-Object {
|
||||||
|
Copy-Item -LiteralPath $_.FullName -Destination $DestinationDir -Recurse -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-RuntimeRoot {
|
||||||
|
param([string]$ConfiguredRoot)
|
||||||
|
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($ConfiguredRoot)) {
|
||||||
|
return Resolve-NormalizedPath -Path $ConfiguredRoot
|
||||||
|
}
|
||||||
|
|
||||||
|
$localAppData = [Environment]::GetEnvironmentVariable("LOCALAPPDATA")
|
||||||
|
if ([string]::IsNullOrWhiteSpace($localAppData)) {
|
||||||
|
throw "LOCALAPPDATA is not available."
|
||||||
|
}
|
||||||
|
|
||||||
|
return Resolve-NormalizedPath -Path (Join-Path $localAppData "QETDeps")
|
||||||
|
}
|
||||||
|
|
||||||
|
function Read-ExistingRuntimeJson {
|
||||||
|
param([string]$RuntimeConfigPath)
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $RuntimeConfigPath)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
return Get-Content -LiteralPath $RuntimeConfigPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-OcctRoot {
|
||||||
|
param(
|
||||||
|
[string]$ConfiguredRoot,
|
||||||
|
$ExistingRuntime
|
||||||
|
)
|
||||||
|
|
||||||
|
$resolvedConfiguredRoot = Resolve-ConfiguredPath -ConfiguredPath $ConfiguredRoot -EnvironmentVariableNames @(
|
||||||
|
"FREECAD_OCCT_ROOT",
|
||||||
|
"QET_OCCT_ROOT"
|
||||||
|
)
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($resolvedConfiguredRoot)) {
|
||||||
|
return $resolvedConfiguredRoot
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -ne $ExistingRuntime -and $ExistingRuntime.PSObject.Properties.Name -contains "occt_root") {
|
||||||
|
$existingOcctRoot = [string]$ExistingRuntime.occt_root
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($existingOcctRoot) -and (Test-Path -LiteralPath $existingOcctRoot)) {
|
||||||
|
return Resolve-NormalizedPath -Path $existingOcctRoot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolvedRunRoot = Resolve-ConfiguredPath -ConfiguredPath $RunRoot -EnvironmentVariableNames @(
|
||||||
|
"FREECAD_RUN_ROOT",
|
||||||
|
"QET_FREECAD_RUN_ROOT"
|
||||||
|
)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($resolvedRunRoot)) {
|
||||||
|
throw "RunRoot is required. Pass -RunRoot or set FREECAD_RUN_ROOT / QET_FREECAD_RUN_ROOT."
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolvedLibPackRoot = Resolve-ConfiguredPath -ConfiguredPath $LibPackRoot -EnvironmentVariableNames @(
|
||||||
|
"FREECAD_LIBPACK_ROOT",
|
||||||
|
"QET_FREECAD_LIBPACK_ROOT"
|
||||||
|
)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($resolvedLibPackRoot)) {
|
||||||
|
throw "LibPackRoot is required. Pass -LibPackRoot or set FREECAD_LIBPACK_ROOT / QET_FREECAD_LIBPACK_ROOT."
|
||||||
|
}
|
||||||
|
|
||||||
|
$runBinDir = Join-Path $resolvedRunRoot "bin"
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $runBinDir)) {
|
||||||
|
throw "Run directory bin folder was not found: $runBinDir"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $resolvedLibPackRoot)) {
|
||||||
|
throw "LibPack root was not found: $resolvedLibPackRoot"
|
||||||
|
}
|
||||||
|
|
||||||
|
$copySpecs = @(
|
||||||
|
@{ Source = (Join-Path $resolvedLibPackRoot "bin"); Patterns = @("*.dll") },
|
||||||
|
@{ Source = (Join-Path $resolvedLibPackRoot "lib"); Patterns = @("*.dll") },
|
||||||
|
@{ Source = (Join-Path $resolvedLibPackRoot "bin"); Patterns = @("python.exe", "pythonw.exe", "py.exe", "python*.zip") },
|
||||||
|
@{ Source = (Join-Path $resolvedLibPackRoot "bin\Lib\site-packages\shiboken6"); Patterns = @("*.dll", "*.pyd") },
|
||||||
|
@{ Source = (Join-Path $resolvedLibPackRoot "bin\Lib\site-packages\PySide6"); Patterns = @("*.dll", "*.pyd") }
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($copySpec in $copySpecs) {
|
||||||
|
Copy-MatchingFiles -SourceDir $copySpec.Source -Patterns $copySpec.Patterns -DestinationDir $runBinDir
|
||||||
|
}
|
||||||
|
|
||||||
|
$pluginRoot = Join-Path $resolvedLibPackRoot "bin\Lib\site-packages\PySide6\plugins"
|
||||||
|
$pluginDirs = @(
|
||||||
|
"platforms",
|
||||||
|
"imageformats",
|
||||||
|
"iconengines",
|
||||||
|
"platformthemes",
|
||||||
|
"styles"
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($pluginDir in $pluginDirs) {
|
||||||
|
Copy-PluginDirectory `
|
||||||
|
-SourceDir (Join-Path $pluginRoot $pluginDir) `
|
||||||
|
-DestinationDir (Join-Path $runBinDir $pluginDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
Copy-PluginDirectory `
|
||||||
|
-SourceDir (Join-Path $resolvedLibPackRoot "bin\Lib") `
|
||||||
|
-DestinationDir (Join-Path $runBinDir "Lib")
|
||||||
|
|
||||||
|
Copy-PluginDirectory `
|
||||||
|
-SourceDir (Join-Path $resolvedLibPackRoot "bin\DLLs") `
|
||||||
|
-DestinationDir (Join-Path $runBinDir "DLLs")
|
||||||
|
|
||||||
|
if (-not $SkipRuntimeJson) {
|
||||||
|
$resolvedRuntimeRoot = Resolve-RuntimeRoot -ConfiguredRoot $RuntimeRoot
|
||||||
|
Ensure-Directory -Path $resolvedRuntimeRoot
|
||||||
|
|
||||||
|
$runtimeConfigPath = Join-Path $resolvedRuntimeRoot "runtime.json"
|
||||||
|
$diagnosticLogPath = Join-Path $resolvedRuntimeRoot "bootstrap.log"
|
||||||
|
$existingRuntime = Read-ExistingRuntimeJson -RuntimeConfigPath $runtimeConfigPath
|
||||||
|
$resolvedOcctRoot = Resolve-OcctRoot -ConfiguredRoot $OcctRoot -ExistingRuntime $existingRuntime
|
||||||
|
|
||||||
|
$resolvedFreeCadPython = ""
|
||||||
|
$pythonCandidate = Join-Path $runBinDir "python.exe"
|
||||||
|
if (Test-Path -LiteralPath $pythonCandidate) {
|
||||||
|
$resolvedFreeCadPython = $pythonCandidate
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolvedFreeCadCmd = ""
|
||||||
|
$cmdCandidate = Join-Path $runBinDir "FreeCADCmd.exe"
|
||||||
|
if (Test-Path -LiteralPath $cmdCandidate) {
|
||||||
|
$resolvedFreeCadCmd = $cmdCandidate
|
||||||
|
}
|
||||||
|
|
||||||
|
$qet3dPython = if (-not [string]::IsNullOrWhiteSpace($resolvedFreeCadPython)) {
|
||||||
|
$resolvedFreeCadPython
|
||||||
|
} else {
|
||||||
|
$resolvedFreeCadCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
$runtimeObject = [ordered]@{
|
||||||
|
schema_version = 1
|
||||||
|
runtime_root = $resolvedRuntimeRoot
|
||||||
|
runtime_config = $runtimeConfigPath
|
||||||
|
diagnostic_log = $diagnosticLogPath
|
||||||
|
occt_root = $resolvedOcctRoot
|
||||||
|
freecad_root = $resolvedRunRoot
|
||||||
|
freecad_lib = $runBinDir
|
||||||
|
freecad_python = $resolvedFreeCadPython
|
||||||
|
freecad_cmd = $resolvedFreeCadCmd
|
||||||
|
qet_3d_python = $qet3dPython
|
||||||
|
path_prefix = @($runBinDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
($runtimeObject | ConvertTo-Json -Depth 5) | Set-Content -LiteralPath $runtimeConfigPath -Encoding UTF8
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "FreeCAD runtime deployment completed."
|
||||||
|
Write-Host (" Run root: {0}" -f $resolvedRunRoot)
|
||||||
|
Write-Host (" LibPack root: {0}" -f $resolvedLibPackRoot)
|
||||||
|
if (-not $SkipRuntimeJson) {
|
||||||
|
Write-Host (" runtime.json: {0}" -f (Join-Path (Resolve-RuntimeRoot -ConfiguredRoot $RuntimeRoot) "runtime.json"))
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue