initial: FreeCAD Silo workbench (extracted from silo monorepo)
FreeCAD workbench for Silo PLM integration. Uses shared silo-client package (submodule) for API communication. Changes from monorepo version: - SiloClient class removed, imported from silo_client package - FreeCADSiloSettings adapter wraps FreeCAD.ParamGet() preferences - Init.py adds silo-client to sys.path at startup - All command classes and UI unchanged
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "silo-client"]
|
||||||
|
path = silo-client
|
||||||
|
url = https://git.kindred-systems.com/kindred/silo-client.git
|
||||||
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Kindred Systems LLC
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
57
Makefile
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
.PHONY: install-freecad install-freecad-flatpak install-freecad-native uninstall-freecad help
|
||||||
|
|
||||||
|
# Detect FreeCAD Mod directory (Flatpak or native)
|
||||||
|
FREECAD_MOD_DIR_FLATPAK := $(HOME)/.var/app/org.freecad.FreeCAD/data/FreeCAD/Mod
|
||||||
|
FREECAD_MOD_DIR_NATIVE := $(HOME)/.local/share/FreeCAD/Mod
|
||||||
|
FREECAD_MOD_DIR_LEGACY := $(HOME)/.FreeCAD/Mod
|
||||||
|
|
||||||
|
# Install FreeCAD workbench (auto-detect Flatpak or native)
|
||||||
|
install-freecad:
|
||||||
|
@if [ -d "$(HOME)/.var/app/org.freecad.FreeCAD" ]; then \
|
||||||
|
echo "Detected Flatpak FreeCAD (org.freecad.FreeCAD)"; \
|
||||||
|
mkdir -p $(FREECAD_MOD_DIR_FLATPAK); \
|
||||||
|
rm -f $(FREECAD_MOD_DIR_FLATPAK)/Silo; \
|
||||||
|
ln -sf $(PWD)/freecad $(FREECAD_MOD_DIR_FLATPAK)/Silo; \
|
||||||
|
echo "Installed to $(FREECAD_MOD_DIR_FLATPAK)/Silo"; \
|
||||||
|
else \
|
||||||
|
echo "Using native FreeCAD installation"; \
|
||||||
|
mkdir -p $(FREECAD_MOD_DIR_NATIVE); \
|
||||||
|
mkdir -p $(FREECAD_MOD_DIR_LEGACY); \
|
||||||
|
rm -f $(FREECAD_MOD_DIR_NATIVE)/Silo; \
|
||||||
|
rm -f $(FREECAD_MOD_DIR_LEGACY)/Silo; \
|
||||||
|
ln -sf $(PWD)/freecad $(FREECAD_MOD_DIR_NATIVE)/Silo; \
|
||||||
|
ln -sf $(PWD)/freecad $(FREECAD_MOD_DIR_LEGACY)/Silo; \
|
||||||
|
echo "Installed to $(FREECAD_MOD_DIR_NATIVE)/Silo"; \
|
||||||
|
fi
|
||||||
|
@echo ""
|
||||||
|
@echo "Restart FreeCAD to load the Silo workbench"
|
||||||
|
|
||||||
|
install-freecad-flatpak:
|
||||||
|
mkdir -p $(FREECAD_MOD_DIR_FLATPAK)
|
||||||
|
rm -f $(FREECAD_MOD_DIR_FLATPAK)/Silo
|
||||||
|
ln -sf $(PWD)/freecad $(FREECAD_MOD_DIR_FLATPAK)/Silo
|
||||||
|
@echo "Installed to $(FREECAD_MOD_DIR_FLATPAK)/Silo"
|
||||||
|
@echo "Restart FreeCAD to load the Silo workbench"
|
||||||
|
|
||||||
|
install-freecad-native:
|
||||||
|
mkdir -p $(FREECAD_MOD_DIR_NATIVE)
|
||||||
|
mkdir -p $(FREECAD_MOD_DIR_LEGACY)
|
||||||
|
rm -f $(FREECAD_MOD_DIR_NATIVE)/Silo
|
||||||
|
rm -f $(FREECAD_MOD_DIR_LEGACY)/Silo
|
||||||
|
ln -sf $(PWD)/freecad $(FREECAD_MOD_DIR_NATIVE)/Silo
|
||||||
|
ln -sf $(PWD)/freecad $(FREECAD_MOD_DIR_LEGACY)/Silo
|
||||||
|
@echo "Installed to $(FREECAD_MOD_DIR_NATIVE)/Silo"
|
||||||
|
|
||||||
|
uninstall-freecad:
|
||||||
|
rm -f $(FREECAD_MOD_DIR_FLATPAK)/Silo
|
||||||
|
rm -f $(FREECAD_MOD_DIR_NATIVE)/Silo
|
||||||
|
rm -f $(FREECAD_MOD_DIR_LEGACY)/Silo
|
||||||
|
@echo "Uninstalled Silo workbench"
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "Silo FreeCAD Workbench Makefile"
|
||||||
|
@echo ""
|
||||||
|
@echo " install-freecad Install workbench (auto-detect Flatpak/native)"
|
||||||
|
@echo " install-freecad-flatpak Install for Flatpak FreeCAD"
|
||||||
|
@echo " install-freecad-native Install for native FreeCAD"
|
||||||
|
@echo " uninstall-freecad Remove workbench symlinks"
|
||||||
35
README.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# silo-mod
|
||||||
|
|
||||||
|
FreeCAD workbench for the Silo parts database. Provides item management, revision control, BOM editing, and file synchronization within Kindred Create.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
silo-mod/
|
||||||
|
├── silo-client/ [submodule] shared Python API client
|
||||||
|
├── freecad/ FreeCAD workbench package
|
||||||
|
│ ├── Init.py Console initialization (adds silo-client to sys.path)
|
||||||
|
│ ├── InitGui.py Workbench registration
|
||||||
|
│ ├── silo_commands.py 14 commands + SiloSync + auth dock widget
|
||||||
|
│ ├── silo_origin.py FileOrigin adapter for unified origin system
|
||||||
|
│ ├── package.xml Workbench metadata
|
||||||
|
│ └── resources/icons/ SVG icons (Catppuccin Mocha palette)
|
||||||
|
├── Makefile Install/uninstall targets
|
||||||
|
└── LICENSE
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
For standalone use (outside Kindred Create):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone --recurse-submodules https://git.kindred-systems.com/kindred/silo-mod.git
|
||||||
|
cd silo-mod
|
||||||
|
make install-freecad
|
||||||
|
```
|
||||||
|
|
||||||
|
Within Kindred Create, this repo is included as a submodule at `mods/silo/` and loaded automatically by `src/Mod/Create/Init.py`.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
15
freecad/Init.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
"""Silo FreeCAD Workbench - Console initialization.
|
||||||
|
|
||||||
|
This file is loaded when FreeCAD starts (even in console mode).
|
||||||
|
The GUI-specific initialization is in InitGui.py.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Add the shared silo-client package to sys.path so that
|
||||||
|
# ``import silo_client`` works from silo_commands.py.
|
||||||
|
_mod_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
_client_dir = os.path.join(_mod_dir, "silo-client")
|
||||||
|
if os.path.isdir(_client_dir) and _client_dir not in sys.path:
|
||||||
|
sys.path.insert(0, _client_dir)
|
||||||
102
freecad/InitGui.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
"""Kindred Silo Workbench - Item database integration for Kindred Create."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
import FreeCAD
|
||||||
|
import FreeCADGui
|
||||||
|
|
||||||
|
FreeCAD.Console.PrintMessage("Kindred Silo InitGui.py loading...\n")
|
||||||
|
|
||||||
|
|
||||||
|
class SiloWorkbench(FreeCADGui.Workbench):
|
||||||
|
"""Kindred Silo workbench for item database integration."""
|
||||||
|
|
||||||
|
MenuText = "Kindred Silo"
|
||||||
|
ToolTip = "Item database and part management for Kindred Create"
|
||||||
|
Icon = ""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# Resolve icon relative to this file so it works regardless of install location
|
||||||
|
icon_path = os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)), "resources", "icons", "silo.svg"
|
||||||
|
)
|
||||||
|
if os.path.exists(icon_path):
|
||||||
|
self.__class__.Icon = icon_path
|
||||||
|
|
||||||
|
def Initialize(self):
|
||||||
|
"""Called when workbench is first activated."""
|
||||||
|
import silo_commands
|
||||||
|
|
||||||
|
# Register Silo as a file origin in the unified origin system
|
||||||
|
try:
|
||||||
|
import silo_origin
|
||||||
|
|
||||||
|
silo_origin.register_silo_origin()
|
||||||
|
except Exception as e:
|
||||||
|
FreeCAD.Console.PrintWarning(f"Could not register Silo origin: {e}\n")
|
||||||
|
|
||||||
|
self.toolbar_commands = [
|
||||||
|
"Silo_ToggleMode",
|
||||||
|
"Separator",
|
||||||
|
"Silo_Open",
|
||||||
|
"Silo_New",
|
||||||
|
"Silo_Save",
|
||||||
|
"Silo_Commit",
|
||||||
|
"Silo_Pull",
|
||||||
|
"Silo_Push",
|
||||||
|
"Silo_Info",
|
||||||
|
"Silo_BOM",
|
||||||
|
"Silo_Settings",
|
||||||
|
"Silo_Auth",
|
||||||
|
]
|
||||||
|
|
||||||
|
self.appendToolbar("Silo", self.toolbar_commands)
|
||||||
|
self.appendMenu("Silo", self.toolbar_commands)
|
||||||
|
|
||||||
|
def Activated(self):
|
||||||
|
"""Called when workbench is activated."""
|
||||||
|
FreeCAD.Console.PrintMessage("Kindred Silo workbench activated\n")
|
||||||
|
self._show_shortcut_recommendations()
|
||||||
|
|
||||||
|
def Deactivated(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def GetClassName(self):
|
||||||
|
return "Gui::PythonWorkbench"
|
||||||
|
|
||||||
|
def _show_shortcut_recommendations(self):
|
||||||
|
"""Show keyboard shortcut recommendations dialog on first activation."""
|
||||||
|
try:
|
||||||
|
param_group = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/KindredSilo")
|
||||||
|
if param_group.GetBool("ShortcutsShown", False):
|
||||||
|
return
|
||||||
|
param_group.SetBool("ShortcutsShown", True)
|
||||||
|
|
||||||
|
from PySide import QtGui
|
||||||
|
|
||||||
|
msg = """<h3>Welcome to Kindred Silo!</h3>
|
||||||
|
<p>For the best experience, set up these keyboard shortcuts:</p>
|
||||||
|
<table style="margin: 10px 0;">
|
||||||
|
<tr><td><b>Ctrl+O</b></td><td> - </td><td>Silo_Open (Search & Open)</td></tr>
|
||||||
|
<tr><td><b>Ctrl+N</b></td><td> - </td><td>Silo_New (Register new item)</td></tr>
|
||||||
|
<tr><td><b>Ctrl+S</b></td><td> - </td><td>Silo_Save (Save & upload)</td></tr>
|
||||||
|
<tr><td><b>Ctrl+Shift+S</b></td><td> - </td><td>Silo_Commit (Save with comment)</td></tr>
|
||||||
|
</table>
|
||||||
|
<p><b>To set shortcuts:</b> Tools > Customize > Keyboard</p>
|
||||||
|
<p style="color: #888;">This message appears once.</p>"""
|
||||||
|
|
||||||
|
dialog = QtGui.QMessageBox()
|
||||||
|
dialog.setWindowTitle("Silo Keyboard Shortcuts")
|
||||||
|
dialog.setTextFormat(QtGui.Qt.RichText)
|
||||||
|
dialog.setText(msg)
|
||||||
|
dialog.setIcon(QtGui.QMessageBox.Information)
|
||||||
|
dialog.addButton("Set Up Now", QtGui.QMessageBox.AcceptRole)
|
||||||
|
dialog.addButton("Later", QtGui.QMessageBox.RejectRole)
|
||||||
|
if dialog.exec_() == 0:
|
||||||
|
FreeCADGui.runCommand("Std_DlgCustomize", 0)
|
||||||
|
except Exception as e:
|
||||||
|
FreeCAD.Console.PrintWarning("Silo shortcuts dialog: " + str(e) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
FreeCADGui.addWorkbench(SiloWorkbench())
|
||||||
|
FreeCAD.Console.PrintMessage("Silo workbench registered\n")
|
||||||
15
freecad/package.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<package format="1" xmlns="https://wiki.freecad.org/Package_Metadata">
|
||||||
|
<name>Kindred Silo</name>
|
||||||
|
<description>Item database and part management workbench for Kindred Create</description>
|
||||||
|
<version>0.1.0</version>
|
||||||
|
<maintainer email="info@kindredsystems.io">Kindred Systems</maintainer>
|
||||||
|
<license file="LICENSE">MIT</license>
|
||||||
|
<url type="repository">https://github.com/kindredsystems/silo</url>
|
||||||
|
<content>
|
||||||
|
<workbench>
|
||||||
|
<classname>SiloWorkbench</classname>
|
||||||
|
<subdirectory>./</subdirectory>
|
||||||
|
</workbench>
|
||||||
|
</content>
|
||||||
|
</package>
|
||||||
8
freecad/resources/icons/silo-auth.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cba6f7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<!-- Padlock body -->
|
||||||
|
<rect x="5" y="11" width="14" height="10" rx="2" fill="#313244" stroke="#cba6f7"/>
|
||||||
|
<!-- Padlock shackle -->
|
||||||
|
<path d="M8 11V7a4 4 0 0 1 8 0v4" fill="none" stroke="#89dceb"/>
|
||||||
|
<!-- Keyhole -->
|
||||||
|
<circle cx="12" cy="16" r="1.5" fill="#89dceb" stroke="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 448 B |
12
freecad/resources/icons/silo-bom.svg
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cba6f7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<!-- Outer box -->
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2" fill="#313244"/>
|
||||||
|
<!-- List lines (BOM rows) -->
|
||||||
|
<line x1="8" y1="8" x2="18" y2="8" stroke="#89dceb" stroke-width="1.5"/>
|
||||||
|
<line x1="8" y1="12" x2="18" y2="12" stroke="#89dceb" stroke-width="1.5"/>
|
||||||
|
<line x1="8" y1="16" x2="18" y2="16" stroke="#89dceb" stroke-width="1.5"/>
|
||||||
|
<!-- Hierarchy dots -->
|
||||||
|
<circle cx="6" cy="8" r="1" fill="#cba6f7"/>
|
||||||
|
<circle cx="6" cy="12" r="1" fill="#cba6f7"/>
|
||||||
|
<circle cx="6" cy="16" r="1" fill="#cba6f7"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 680 B |
8
freecad/resources/icons/silo-commit.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cba6f7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<!-- Git commit style -->
|
||||||
|
<circle cx="12" cy="12" r="4" fill="#313244" stroke="#a6e3a1"/>
|
||||||
|
<line x1="12" y1="2" x2="12" y2="8" stroke="#cba6f7"/>
|
||||||
|
<line x1="12" y1="16" x2="12" y2="22" stroke="#cba6f7"/>
|
||||||
|
<!-- Checkmark inside -->
|
||||||
|
<polyline points="9.5 12 11 13.5 14.5 10" stroke="#a6e3a1" stroke-width="1.5" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 493 B |
6
freecad/resources/icons/silo-info.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cba6f7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<!-- Info circle -->
|
||||||
|
<circle cx="12" cy="12" r="10" fill="#313244"/>
|
||||||
|
<line x1="12" y1="16" x2="12" y2="12" stroke="#89dceb" stroke-width="2"/>
|
||||||
|
<circle cx="12" cy="8" r="0.5" fill="#89dceb" stroke="#89dceb"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 377 B |
8
freecad/resources/icons/silo-new.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cba6f7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<!-- Document with plus -->
|
||||||
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" fill="#313244"/>
|
||||||
|
<polyline points="14 2 14 8 20 8" fill="#45475a" stroke="#cba6f7"/>
|
||||||
|
<!-- Plus sign -->
|
||||||
|
<line x1="12" y1="11" x2="12" y2="17" stroke="#a6e3a1" stroke-width="2"/>
|
||||||
|
<line x1="9" y1="14" x2="15" y2="14" stroke="#a6e3a1" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 521 B |
8
freecad/resources/icons/silo-open.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cba6f7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<!-- Folder open icon -->
|
||||||
|
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" fill="#313244"/>
|
||||||
|
<path d="M2 10h20" stroke="#6c7086"/>
|
||||||
|
<!-- Search magnifier -->
|
||||||
|
<circle cx="17" cy="15" r="3" fill="#1e1e2e" stroke="#a6e3a1" stroke-width="1.5"/>
|
||||||
|
<line x1="19.5" y1="17.5" x2="22" y2="20" stroke="#a6e3a1" stroke-width="1.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 529 B |
7
freecad/resources/icons/silo-pull.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cba6f7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<!-- Cloud -->
|
||||||
|
<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z" fill="#313244"/>
|
||||||
|
<!-- Download arrow -->
|
||||||
|
<path d="M12 13v5m0 0l-2-2m2 2l2-2" stroke="#89b4fa" stroke-width="2"/>
|
||||||
|
<line x1="12" y1="9" x2="12" y2="13" stroke="#89b4fa" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 428 B |
7
freecad/resources/icons/silo-push.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cba6f7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<!-- Cloud -->
|
||||||
|
<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z" fill="#313244"/>
|
||||||
|
<!-- Upload arrow -->
|
||||||
|
<path d="M12 18v-5m0 0l-2 2m2-2l2 2" stroke="#a6e3a1" stroke-width="2"/>
|
||||||
|
<line x1="12" y1="13" x2="12" y2="9" stroke="#a6e3a1" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 427 B |
8
freecad/resources/icons/silo-save.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cba6f7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<!-- Floppy disk -->
|
||||||
|
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" fill="#313244"/>
|
||||||
|
<polyline points="17 21 17 13 7 13 7 21" fill="#45475a" stroke="#cba6f7"/>
|
||||||
|
<polyline points="7 3 7 8 15 8" fill="#45475a" stroke="#6c7086"/>
|
||||||
|
<!-- Upload arrow -->
|
||||||
|
<path d="M12 17v-4m0 0l-2 2m2-2l2 2" stroke="#a6e3a1" stroke-width="1.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 523 B |
28
freecad/resources/icons/silo.svg
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||||
|
<!-- Silo icon - grain silo with database/sync symbolism -->
|
||||||
|
<!-- Uses Catppuccin Mocha colors -->
|
||||||
|
|
||||||
|
<!-- Silo body (cylindrical tower) -->
|
||||||
|
<path d="M16 20 L16 52 Q16 56 32 56 Q48 56 48 52 L48 20"
|
||||||
|
fill="#313244" stroke="#cba6f7" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Silo dome/roof -->
|
||||||
|
<ellipse cx="32" cy="20" rx="16" ry="6" fill="#45475a" stroke="#cba6f7" stroke-width="2"/>
|
||||||
|
<path d="M24 14 Q32 4 40 14" fill="none" stroke="#cba6f7" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<line x1="32" y1="6" x2="32" y2="14" stroke="#cba6f7" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<!-- Horizontal bands (like database rows / silo rings) -->
|
||||||
|
<ellipse cx="32" cy="28" rx="15" ry="4" fill="none" stroke="#6c7086" stroke-width="1.5"/>
|
||||||
|
<ellipse cx="32" cy="36" rx="15" ry="4" fill="none" stroke="#6c7086" stroke-width="1.5"/>
|
||||||
|
<ellipse cx="32" cy="44" rx="15" ry="4" fill="none" stroke="#6c7086" stroke-width="1.5"/>
|
||||||
|
|
||||||
|
<!-- Base ellipse -->
|
||||||
|
<ellipse cx="32" cy="52" rx="16" ry="4" fill="none" stroke="#cba6f7" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Sync arrows (circular) - represents upload/download -->
|
||||||
|
<g transform="translate(44, 8)">
|
||||||
|
<circle cx="8" cy="8" r="7" fill="#1e1e2e" stroke="#a6e3a1" stroke-width="1.5"/>
|
||||||
|
<path d="M5 6 L8 3 L11 6 M8 3 L8 10" stroke="#a6e3a1" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M11 10 L8 13 L5 10 M8 13 L8 10" stroke="#a6e3a1" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.6"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
2976
freecad/silo_commands.py
Normal file
584
freecad/silo_origin.py
Normal file
@@ -0,0 +1,584 @@
|
|||||||
|
"""Silo origin adapter for FreeCAD Origin system.
|
||||||
|
|
||||||
|
This module provides the SiloOrigin class that implements the FileOrigin
|
||||||
|
interface, allowing Silo to be used as a document origin in the unified
|
||||||
|
origin system introduced in Issue #9.
|
||||||
|
|
||||||
|
The SiloOrigin wraps existing Silo commands and SiloSync functionality,
|
||||||
|
delegating operations to the established Silo infrastructure while
|
||||||
|
providing the standardized origin interface.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import FreeCAD
|
||||||
|
import FreeCADGui
|
||||||
|
|
||||||
|
from .silo_commands import (
|
||||||
|
_client,
|
||||||
|
_sync,
|
||||||
|
get_tracked_object,
|
||||||
|
set_silo_properties,
|
||||||
|
find_file_by_part_number,
|
||||||
|
collect_document_properties,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SiloOrigin:
|
||||||
|
"""FileOrigin implementation for Silo PLM.
|
||||||
|
|
||||||
|
This class adapts Silo functionality to the FileOrigin interface,
|
||||||
|
enabling Silo to be used as a document origin in the unified system.
|
||||||
|
|
||||||
|
Key behaviors:
|
||||||
|
- Documents are always stored locally (hybrid local-remote model)
|
||||||
|
- Database tracks metadata, part numbers, and revision history
|
||||||
|
- MinIO stores revision snapshots for sync/backup
|
||||||
|
- Identity is tracked by UUID (SiloItemId), displayed as part number
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, origin_id="silo", nickname="Silo"):
|
||||||
|
"""Initialize SiloOrigin.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
origin_id: Unique identifier for this origin instance
|
||||||
|
nickname: Short display name for UI elements
|
||||||
|
"""
|
||||||
|
self._id = origin_id
|
||||||
|
self._nickname = nickname
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Identity Methods
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
def id(self) -> str:
|
||||||
|
"""Return unique identifier for this origin."""
|
||||||
|
return self._id
|
||||||
|
|
||||||
|
def name(self) -> str:
|
||||||
|
"""Return display name for UI."""
|
||||||
|
return "Kindred Silo"
|
||||||
|
|
||||||
|
def nickname(self) -> str:
|
||||||
|
"""Return short nickname for compact UI elements."""
|
||||||
|
return self._nickname
|
||||||
|
|
||||||
|
def icon(self) -> str:
|
||||||
|
"""Return icon name for BitmapFactory."""
|
||||||
|
return "silo"
|
||||||
|
|
||||||
|
def type(self) -> int:
|
||||||
|
"""Return origin type (OriginType.PLM = 1)."""
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Workflow Characteristics
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
def tracksExternally(self) -> bool:
|
||||||
|
"""Return True - Silo tracks documents in database."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def requiresAuthentication(self) -> bool:
|
||||||
|
"""Return True - Silo requires user authentication."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Capabilities
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
def supportsRevisions(self) -> bool:
|
||||||
|
"""Return True - Silo supports revision history."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def supportsBOM(self) -> bool:
|
||||||
|
"""Return True - Silo supports Bill of Materials."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def supportsPartNumbers(self) -> bool:
|
||||||
|
"""Return True - Silo assigns part numbers from schema."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def supportsAssemblies(self) -> bool:
|
||||||
|
"""Return True - Silo supports assembly documents."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Connection State
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
def connectionState(self) -> int:
|
||||||
|
"""Return connection state enum value.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
0 = Disconnected
|
||||||
|
1 = Connecting
|
||||||
|
2 = Connected
|
||||||
|
3 = Error
|
||||||
|
"""
|
||||||
|
if not _client.is_authenticated():
|
||||||
|
return 0 # Disconnected
|
||||||
|
|
||||||
|
try:
|
||||||
|
ok, _ = _client.check_connection()
|
||||||
|
return 2 if ok else 3 # Connected or Error
|
||||||
|
except Exception:
|
||||||
|
return 3 # Error
|
||||||
|
|
||||||
|
def connect(self) -> bool:
|
||||||
|
"""Trigger authentication if needed.
|
||||||
|
|
||||||
|
Shows the Silo authentication dialog if not already authenticated.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if authenticated after this call
|
||||||
|
"""
|
||||||
|
if _client.is_authenticated():
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Show auth dialog via existing Silo_Auth command
|
||||||
|
try:
|
||||||
|
cmd = FreeCADGui.Command.get("Silo_Auth")
|
||||||
|
if cmd:
|
||||||
|
cmd.Activated()
|
||||||
|
return _client.is_authenticated()
|
||||||
|
except Exception as e:
|
||||||
|
FreeCAD.Console.PrintError(f"Silo connect failed: {e}\n")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
"""Log out of Silo."""
|
||||||
|
_client.logout()
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Document Identity
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
def documentIdentity(self, doc) -> str:
|
||||||
|
"""Return UUID (SiloItemId) as primary identity.
|
||||||
|
|
||||||
|
The UUID is the immutable tracking key for the document in the
|
||||||
|
database. Falls back to part number if UUID not yet assigned.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
doc: FreeCAD App.Document
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
UUID string, or part number as fallback, or empty string
|
||||||
|
"""
|
||||||
|
if not doc:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
obj = get_tracked_object(doc)
|
||||||
|
if not obj:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Prefer UUID (SiloItemId)
|
||||||
|
if hasattr(obj, "SiloItemId") and obj.SiloItemId:
|
||||||
|
return obj.SiloItemId
|
||||||
|
|
||||||
|
# Fallback to part number
|
||||||
|
if hasattr(obj, "SiloPartNumber") and obj.SiloPartNumber:
|
||||||
|
return obj.SiloPartNumber
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def documentDisplayId(self, doc) -> str:
|
||||||
|
"""Return part number for display.
|
||||||
|
|
||||||
|
The part number is the human-readable identifier shown in the UI.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
doc: FreeCAD App.Document
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Part number string or empty string
|
||||||
|
"""
|
||||||
|
if not doc:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
obj = get_tracked_object(doc)
|
||||||
|
if obj and hasattr(obj, "SiloPartNumber") and obj.SiloPartNumber:
|
||||||
|
return obj.SiloPartNumber
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def ownsDocument(self, doc) -> bool:
|
||||||
|
"""Check if document is tracked by Silo.
|
||||||
|
|
||||||
|
A document is owned by Silo if it has a tracked object with
|
||||||
|
SiloItemId or SiloPartNumber property set.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
doc: FreeCAD App.Document
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if Silo owns this document
|
||||||
|
"""
|
||||||
|
if not doc:
|
||||||
|
return False
|
||||||
|
|
||||||
|
obj = get_tracked_object(doc)
|
||||||
|
if not obj:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check for SiloItemId (preferred) or SiloPartNumber
|
||||||
|
if hasattr(obj, "SiloItemId") and obj.SiloItemId:
|
||||||
|
return True
|
||||||
|
if hasattr(obj, "SiloPartNumber") and obj.SiloPartNumber:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Property Sync
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
def syncProperties(self, doc) -> bool:
|
||||||
|
"""Sync document properties to database.
|
||||||
|
|
||||||
|
Pushes syncable properties from the FreeCAD document to the
|
||||||
|
Silo database.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
doc: FreeCAD App.Document
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if sync succeeded
|
||||||
|
"""
|
||||||
|
if not doc:
|
||||||
|
return False
|
||||||
|
|
||||||
|
obj = get_tracked_object(doc)
|
||||||
|
if not obj or not hasattr(obj, "SiloPartNumber"):
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Collect syncable properties
|
||||||
|
updates = {}
|
||||||
|
if hasattr(obj, "SiloDescription") and obj.SiloDescription:
|
||||||
|
updates["description"] = obj.SiloDescription
|
||||||
|
|
||||||
|
if updates:
|
||||||
|
_client.update_item(obj.SiloPartNumber, **updates)
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
FreeCAD.Console.PrintError(f"Silo property sync failed: {e}\n")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Core Operations
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
def newDocument(self, name: str = ""):
|
||||||
|
"""Create new document via Silo part creation form.
|
||||||
|
|
||||||
|
Delegates to the existing Silo_New command which:
|
||||||
|
1. Shows part creation dialog with category selection
|
||||||
|
2. Generates part number from schema
|
||||||
|
3. Creates document with Silo properties
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Optional document name (not used, Silo assigns name)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Created App.Document or None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
cmd = FreeCADGui.Command.get("Silo_New")
|
||||||
|
if cmd:
|
||||||
|
cmd.Activated()
|
||||||
|
return FreeCAD.ActiveDocument
|
||||||
|
except Exception as e:
|
||||||
|
FreeCAD.Console.PrintError(f"Silo new document failed: {e}\n")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def openDocument(self, identity: str):
|
||||||
|
"""Open document by UUID or part number.
|
||||||
|
|
||||||
|
If identity is empty, shows the Silo search dialog.
|
||||||
|
Otherwise, finds the local file or downloads from Silo.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
identity: UUID or part number, or empty for search dialog
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Opened App.Document or None
|
||||||
|
"""
|
||||||
|
if not identity:
|
||||||
|
# No identity - show search dialog
|
||||||
|
try:
|
||||||
|
cmd = FreeCADGui.Command.get("Silo_Open")
|
||||||
|
if cmd:
|
||||||
|
cmd.Activated()
|
||||||
|
return FreeCAD.ActiveDocument
|
||||||
|
except Exception as e:
|
||||||
|
FreeCAD.Console.PrintError(f"Silo open failed: {e}\n")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Try to find existing local file by part number
|
||||||
|
# (UUID lookup would require API enhancement)
|
||||||
|
local_path = find_file_by_part_number(identity)
|
||||||
|
if local_path and local_path.exists():
|
||||||
|
return FreeCAD.openDocument(str(local_path))
|
||||||
|
|
||||||
|
# Download from Silo
|
||||||
|
try:
|
||||||
|
doc = _sync.open_item(identity)
|
||||||
|
return doc
|
||||||
|
except Exception as e:
|
||||||
|
FreeCAD.Console.PrintError(f"Silo open item failed: {e}\n")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def openDocumentInteractive(self):
|
||||||
|
"""Open document interactively via Silo search dialog.
|
||||||
|
|
||||||
|
Shows the Silo_Open dialog for searching and selecting
|
||||||
|
a document to open.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Opened App.Document or None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
cmd = FreeCADGui.Command.get("Silo_Open")
|
||||||
|
if cmd:
|
||||||
|
cmd.Activated()
|
||||||
|
return FreeCAD.ActiveDocument
|
||||||
|
except Exception as e:
|
||||||
|
FreeCAD.Console.PrintError(f"Silo open failed: {e}\n")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def saveDocument(self, doc) -> bool:
|
||||||
|
"""Save document and sync to Silo.
|
||||||
|
|
||||||
|
Saves the document locally to the canonical path and uploads
|
||||||
|
to Silo for sync.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
doc: FreeCAD App.Document
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if save succeeded
|
||||||
|
"""
|
||||||
|
if not doc:
|
||||||
|
return False
|
||||||
|
|
||||||
|
obj = get_tracked_object(doc)
|
||||||
|
if not obj:
|
||||||
|
# Not a Silo document - just save locally
|
||||||
|
if doc.FileName:
|
||||||
|
doc.save()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Save to canonical path
|
||||||
|
file_path = _sync.save_to_canonical_path(doc)
|
||||||
|
if not file_path:
|
||||||
|
FreeCAD.Console.PrintError("Failed to save to canonical path\n")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Upload to Silo
|
||||||
|
properties = collect_document_properties(doc)
|
||||||
|
_client._upload_file(obj.SiloPartNumber, str(file_path), properties, comment="")
|
||||||
|
|
||||||
|
# Clear modified flag
|
||||||
|
doc.Modified = False
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
FreeCAD.Console.PrintError(f"Silo save failed: {e}\n")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def saveDocumentAs(self, doc, newIdentity: str) -> bool:
|
||||||
|
"""Save with new identity - triggers migration or copy workflow.
|
||||||
|
|
||||||
|
For local documents: Triggers migration to Silo (new item creation)
|
||||||
|
For Silo documents: Would trigger copy workflow (not yet implemented)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
doc: FreeCAD App.Document
|
||||||
|
newIdentity: New identity (currently unused)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if operation succeeded
|
||||||
|
"""
|
||||||
|
if not doc:
|
||||||
|
return False
|
||||||
|
|
||||||
|
obj = get_tracked_object(doc)
|
||||||
|
|
||||||
|
if not obj:
|
||||||
|
# Local document being migrated to Silo
|
||||||
|
# Trigger new item creation form
|
||||||
|
result = self.newDocument()
|
||||||
|
return result is not None
|
||||||
|
|
||||||
|
# Already a Silo document - copy workflow
|
||||||
|
# TODO: Issue #17 will implement copy workflow
|
||||||
|
FreeCAD.Console.PrintWarning(
|
||||||
|
"Silo copy workflow not yet implemented. Use Silo_New to create a new item.\n"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def saveDocumentAsInteractive(self, doc) -> bool:
|
||||||
|
"""Save document interactively with new identity.
|
||||||
|
|
||||||
|
For Silo, this triggers the new item creation form which allows
|
||||||
|
the user to select category and create a new part number.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
doc: FreeCAD App.Document
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if operation succeeded
|
||||||
|
"""
|
||||||
|
if not doc:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# For Silo, "Save As" means creating a new item
|
||||||
|
# Trigger the new item creation form
|
||||||
|
result = self.newDocument()
|
||||||
|
return result is not None
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Extended Operations
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
def commitDocument(self, doc) -> bool:
|
||||||
|
"""Commit with revision comment.
|
||||||
|
|
||||||
|
Delegates to Silo_Commit command.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
doc: FreeCAD App.Document
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if command was executed
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
cmd = FreeCADGui.Command.get("Silo_Commit")
|
||||||
|
if cmd:
|
||||||
|
cmd.Activated()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
FreeCAD.Console.PrintError(f"Silo commit failed: {e}\n")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def pullDocument(self, doc) -> bool:
|
||||||
|
"""Pull latest from Silo.
|
||||||
|
|
||||||
|
Delegates to Silo_Pull command.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
doc: FreeCAD App.Document
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if command was executed
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
cmd = FreeCADGui.Command.get("Silo_Pull")
|
||||||
|
if cmd:
|
||||||
|
cmd.Activated()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
FreeCAD.Console.PrintError(f"Silo pull failed: {e}\n")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def pushDocument(self, doc) -> bool:
|
||||||
|
"""Push changes to Silo.
|
||||||
|
|
||||||
|
Delegates to Silo_Push command.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
doc: FreeCAD App.Document
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if command was executed
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
cmd = FreeCADGui.Command.get("Silo_Push")
|
||||||
|
if cmd:
|
||||||
|
cmd.Activated()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
FreeCAD.Console.PrintError(f"Silo push failed: {e}\n")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def showInfo(self, doc):
|
||||||
|
"""Show document info dialog.
|
||||||
|
|
||||||
|
Delegates to Silo_Info command.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
doc: FreeCAD App.Document
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
cmd = FreeCADGui.Command.get("Silo_Info")
|
||||||
|
if cmd:
|
||||||
|
cmd.Activated()
|
||||||
|
except Exception as e:
|
||||||
|
FreeCAD.Console.PrintError(f"Silo info failed: {e}\n")
|
||||||
|
|
||||||
|
def showBOM(self, doc):
|
||||||
|
"""Show BOM dialog.
|
||||||
|
|
||||||
|
Delegates to Silo_BOM command.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
doc: FreeCAD App.Document
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
cmd = FreeCADGui.Command.get("Silo_BOM")
|
||||||
|
if cmd:
|
||||||
|
cmd.Activated()
|
||||||
|
except Exception as e:
|
||||||
|
FreeCAD.Console.PrintError(f"Silo BOM failed: {e}\n")
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Module-level functions
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Global instance
|
||||||
|
_silo_origin = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_silo_origin():
|
||||||
|
"""Get or create the global SiloOrigin instance.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
SiloOrigin instance
|
||||||
|
"""
|
||||||
|
global _silo_origin
|
||||||
|
if _silo_origin is None:
|
||||||
|
_silo_origin = SiloOrigin()
|
||||||
|
return _silo_origin
|
||||||
|
|
||||||
|
|
||||||
|
def register_silo_origin():
|
||||||
|
"""Register SiloOrigin with FreeCADGui.
|
||||||
|
|
||||||
|
This should be called during workbench initialization to make
|
||||||
|
Silo available as a file origin.
|
||||||
|
"""
|
||||||
|
origin = get_silo_origin()
|
||||||
|
try:
|
||||||
|
FreeCADGui.addOrigin(origin)
|
||||||
|
FreeCAD.Console.PrintLog("Registered Silo origin\n")
|
||||||
|
except Exception as e:
|
||||||
|
FreeCAD.Console.PrintWarning(f"Could not register Silo origin: {e}\n")
|
||||||
|
|
||||||
|
|
||||||
|
def unregister_silo_origin():
|
||||||
|
"""Unregister SiloOrigin from FreeCADGui.
|
||||||
|
|
||||||
|
This should be called during workbench cleanup if needed.
|
||||||
|
"""
|
||||||
|
global _silo_origin
|
||||||
|
if _silo_origin:
|
||||||
|
try:
|
||||||
|
FreeCADGui.removeOrigin(_silo_origin)
|
||||||
|
FreeCAD.Console.PrintLog("Unregistered Silo origin\n")
|
||||||
|
except Exception as e:
|
||||||
|
FreeCAD.Console.PrintWarning(f"Could not unregister Silo origin: {e}\n")
|
||||||
|
_silo_origin = None
|
||||||