refactor: remove FreeCAD workbench from server repo

Closes #2 — FreeCAD client code belongs in silo-mod, not the server.

Remove pkg/freecad/ (16 files, ~4,500 lines):
- InitGui.py, Init.py, silo_commands.py, silo_origin.py
- package.xml manifest, 10 SVG icons
- HolePattern.FCMacro

The FreeCAD workbench now lives at:
https://git.kindred-systems.com/kindred/silo-mod
This commit is contained in:
Forbes
2026-02-06 16:19:07 -06:00
parent 004dc9aef0
commit 503c3d1831
16 changed files with 0 additions and 4469 deletions

View File

@@ -1,68 +0,0 @@
import FreeCAD as App
import Sketcher
import Part
def generate_hole_pattern():
"""Generate parametric hole grid from spreadsheet values."""
doc = App.ActiveDocument
if not doc:
App.Console.PrintError("No active document\n")
return
# Create/get spreadsheet
sheet = doc.getObject('Spreadsheet')
if not sheet:
sheet = doc.addObject('Spreadsheet::Sheet', 'Spreadsheet')
sheet.set('A1', 'hole_spacing'); sheet.set('B1', '10 mm'); sheet.setAlias('B1', 'hole_spacing')
sheet.set('A2', 'hole_radius'); sheet.set('B2', '2.5 mm'); sheet.setAlias('B2', 'hole_radius')
sheet.set('A3', 'grid_offset_x'); sheet.set('B3', '10 mm'); sheet.setAlias('B3', 'grid_offset_x')
sheet.set('A4', 'grid_offset_y'); sheet.set('B4', '10 mm'); sheet.setAlias('B4', 'grid_offset_y')
sheet.set('A5', 'grid_cols'); sheet.set('B5', '5'); sheet.setAlias('B5', 'grid_cols')
sheet.set('A6', 'grid_rows'); sheet.set('B6', '5'); sheet.setAlias('B6', 'grid_rows')
doc.recompute()
App.Console.PrintMessage("Created Spreadsheet with default parameters\n")
# Read grid size
cols = int(sheet.grid_cols)
rows = int(sheet.grid_rows)
# Get/create sketch
sketch = doc.getObject('HolePatternSketch')
if not sketch:
body = doc.getObject('Body')
if body:
sketch = body.newObject('Sketcher::SketchObject', 'HolePatternSketch')
sketch.AttachmentSupport = [(body.Origin.XY_Plane, '')]
sketch.MapMode = 'FlatFace'
else:
sketch = doc.addObject('Sketcher::SketchObject', 'HolePatternSketch')
App.Console.PrintMessage("Created HolePatternSketch\n")
# Clear existing geometry
for i in range(sketch.GeometryCount - 1, -1, -1):
sketch.delGeometry(i)
# Generate pattern
for i in range(cols):
for j in range(rows):
circle_idx = sketch.addGeometry(
Part.Circle(App.Vector(0, 0, 0), App.Vector(0, 0, 1), 1),
False
)
cx = sketch.addConstraint(Sketcher.Constraint('DistanceX', -1, 1, circle_idx, 3, 10))
sketch.setExpression(f'Constraints[{cx}]', f'Spreadsheet.grid_offset_x + {i} * Spreadsheet.hole_spacing')
cy = sketch.addConstraint(Sketcher.Constraint('DistanceY', -1, 1, circle_idx, 3, 10))
sketch.setExpression(f'Constraints[{cy}]', f'Spreadsheet.grid_offset_y + {j} * Spreadsheet.hole_spacing')
r = sketch.addConstraint(Sketcher.Constraint('Radius', circle_idx, 1))
sketch.setExpression(f'Constraints[{r}]', 'Spreadsheet.hole_radius')
doc.recompute()
App.Console.PrintMessage(f"Generated {cols}x{rows} hole pattern ({cols*rows} holes)\n")
# Run when macro is executed
if __name__ == '__main__':
generate_hole_pattern()

View File

@@ -1,8 +0,0 @@
"""Silo FreeCAD Workbench - Console initialization.
This file is loaded when FreeCAD starts (even in console mode).
The GUI-specific initialization is in InitGui.py.
"""
# No console-only initialization needed for Silo workbench
# All functionality requires the GUI

View File

@@ -1,102 +0,0 @@
"""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")

View File

@@ -1,15 +0,0 @@
<?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>

View File

@@ -1,8 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 448 B

View File

@@ -1,12 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 680 B

View File

@@ -1,8 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 493 B

View File

@@ -1,6 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 377 B

View File

@@ -1,8 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 521 B

View File

@@ -1,8 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 529 B

View File

@@ -1,7 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 428 B

View File

@@ -1,7 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 427 B

View File

@@ -1,8 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 523 B

View File

@@ -1,28 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -1,584 +0,0 @@
"""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