Compare commits

..

1 Commits

Author SHA1 Message Date
4d13313e42 fix(origin): return absolute icon path from SiloOrigin.icon()
The icon() method returned bare 'silo' which BitmapFactory could not
resolve. Return the absolute path to silo.svg so the C++ side can load
the icon directly.
2026-02-12 13:40:56 -06:00
12 changed files with 217 additions and 1641 deletions

View File

@@ -45,7 +45,6 @@ class SiloWorkbench(FreeCADGui.Workbench):
"Separator",
"Silo_Info",
"Silo_BOM",
"Silo_Jobs",
]
self.appendToolbar("Silo Origin", self.silo_toolbar_commands, "Unavailable")
@@ -53,14 +52,12 @@ class SiloWorkbench(FreeCADGui.Workbench):
self.menu_commands = [
"Silo_Info",
"Silo_BOM",
"Silo_Jobs",
"Silo_TagProjects",
"Silo_SetStatus",
"Silo_Rollback",
"Separator",
"Silo_Settings",
"Silo_Auth",
"Silo_Runners",
"Silo_StartPanel",
"Silo_Diag",
]
@@ -106,9 +103,7 @@ def _register_silo_overlay():
return False
try:
from kindred_sdk import register_overlay
register_overlay(
FreeCADGui.registerEditingOverlay(
"silo", # overlay id
["Silo Origin"], # toolbar names to append
_silo_overlay_match, # match function

View File

@@ -1,463 +0,0 @@
"""DAG extraction engine for FreeCAD documents.
Extracts the feature tree from a FreeCAD document as nodes and edges
for syncing to the Silo server. No GUI dependencies -- usable in both
desktop and headless (``--console``) mode.
Public API
----------
classify_type(type_id) -> Optional[str]
compute_properties_hash(obj) -> str
extract_dag(doc) -> (nodes, edges)
"""
import hashlib
import json
import math
from typing import Any, Dict, List, Optional, Set, Tuple
# ---------------------------------------------------------------------------
# TypeId -> DAG node type mapping
# ---------------------------------------------------------------------------
_TYPE_MAP: Dict[str, str] = {
# Sketch
"Sketcher::SketchObject": "sketch",
# Sketch-based additive
"PartDesign::Pad": "pad",
"PartDesign::Revolution": "pad",
"PartDesign::AdditivePipe": "pad",
"PartDesign::AdditiveLoft": "pad",
"PartDesign::AdditiveHelix": "pad",
# Sketch-based subtractive
"PartDesign::Pocket": "pocket",
"PartDesign::Groove": "pocket",
"PartDesign::Hole": "pocket",
"PartDesign::SubtractivePipe": "pocket",
"PartDesign::SubtractiveLoft": "pocket",
"PartDesign::SubtractiveHelix": "pocket",
# Dress-up
"PartDesign::Fillet": "fillet",
"PartDesign::Chamfer": "chamfer",
"PartDesign::Draft": "chamfer",
"PartDesign::Thickness": "chamfer",
# Transformations
"PartDesign::Mirrored": "pad",
"PartDesign::LinearPattern": "pad",
"PartDesign::PolarPattern": "pad",
"PartDesign::Scaled": "pad",
"PartDesign::MultiTransform": "pad",
# Boolean
"PartDesign::Boolean": "pad",
# Additive primitives
"PartDesign::AdditiveBox": "pad",
"PartDesign::AdditiveCylinder": "pad",
"PartDesign::AdditiveSphere": "pad",
"PartDesign::AdditiveCone": "pad",
"PartDesign::AdditiveEllipsoid": "pad",
"PartDesign::AdditiveTorus": "pad",
"PartDesign::AdditivePrism": "pad",
"PartDesign::AdditiveWedge": "pad",
# Subtractive primitives
"PartDesign::SubtractiveBox": "pocket",
"PartDesign::SubtractiveCylinder": "pocket",
"PartDesign::SubtractiveSphere": "pocket",
"PartDesign::SubtractiveCone": "pocket",
"PartDesign::SubtractiveEllipsoid": "pocket",
"PartDesign::SubtractiveTorus": "pocket",
"PartDesign::SubtractivePrism": "pocket",
"PartDesign::SubtractiveWedge": "pocket",
# Containers
"PartDesign::Body": "body",
"App::Part": "part",
"Part::Feature": "part",
# Datum / reference
"PartDesign::Point": "datum",
"PartDesign::Line": "datum",
"PartDesign::Plane": "datum",
"PartDesign::CoordinateSystem": "datum",
"PartDesign::ShapeBinder": "datum",
"PartDesign::SubShapeBinder": "datum",
}
def classify_type(type_id: str) -> Optional[str]:
"""Map a FreeCAD TypeId string to a DAG node type.
Returns one of ``sketch``, ``pad``, ``pocket``, ``fillet``,
``chamfer``, ``body``, ``part``, ``datum``, or ``None`` if the
TypeId is not a recognized feature.
"""
return _TYPE_MAP.get(type_id)
# ---------------------------------------------------------------------------
# Properties hash
# ---------------------------------------------------------------------------
def _safe_float(val: Any) -> Any:
"""Convert a float to a JSON-safe value, replacing NaN/Infinity with 0."""
if isinstance(val, float) and (math.isnan(val) or math.isinf(val)):
return 0.0
return val
def _prop_value(obj: Any, name: str) -> Any:
"""Safely read ``obj.<name>.Value``, returning *None* on failure."""
try:
return _safe_float(getattr(obj, name).Value)
except Exception:
return None
def _prop_raw(obj: Any, name: str) -> Any:
"""Safely read ``obj.<name>``, returning *None* on failure."""
try:
return getattr(obj, name)
except Exception:
return None
def _link_name(obj: Any, name: str) -> Optional[str]:
"""Return the ``.Name`` of a linked object property, or *None*."""
try:
link = getattr(obj, name)
if isinstance(link, (list, tuple)):
link = link[0]
return link.Name if link is not None else None
except Exception:
return None
def _collect_inputs(obj: Any) -> Dict[str, Any]:
"""Extract the parametric inputs that affect *obj*'s geometry.
The returned dict is JSON-serialized and hashed to produce the
``properties_hash`` for the DAG node.
"""
tid = obj.TypeId
inputs: Dict[str, Any] = {"_type": tid}
# --- Sketch ---
if tid == "Sketcher::SketchObject":
inputs["geometry_count"] = _prop_raw(obj, "GeometryCount")
inputs["constraint_count"] = _prop_raw(obj, "ConstraintCount")
try:
inputs["geometry"] = obj.Shape.exportBrepToString()
except Exception:
pass
return inputs
# --- Extrude (Pad / Pocket) ---
if tid in ("PartDesign::Pad", "PartDesign::Pocket"):
inputs["length"] = _prop_value(obj, "Length")
inputs["type"] = str(_prop_raw(obj, "Type") or "")
inputs["reversed"] = _prop_raw(obj, "Reversed")
inputs["sketch"] = _link_name(obj, "Profile")
return inputs
# --- Revolution / Groove ---
if tid in ("PartDesign::Revolution", "PartDesign::Groove"):
inputs["angle"] = _prop_value(obj, "Angle")
inputs["type"] = str(_prop_raw(obj, "Type") or "")
inputs["reversed"] = _prop_raw(obj, "Reversed")
inputs["sketch"] = _link_name(obj, "Profile")
return inputs
# --- Hole ---
if tid == "PartDesign::Hole":
inputs["diameter"] = _prop_value(obj, "Diameter")
inputs["depth"] = _prop_value(obj, "Depth")
inputs["threaded"] = _prop_raw(obj, "Threaded")
inputs["thread_type"] = str(_prop_raw(obj, "ThreadType") or "")
inputs["depth_type"] = str(_prop_raw(obj, "DepthType") or "")
inputs["sketch"] = _link_name(obj, "Profile")
return inputs
# --- Pipe / Loft / Helix (additive + subtractive) ---
if tid in (
"PartDesign::AdditivePipe",
"PartDesign::SubtractivePipe",
"PartDesign::AdditiveLoft",
"PartDesign::SubtractiveLoft",
"PartDesign::AdditiveHelix",
"PartDesign::SubtractiveHelix",
):
inputs["sketch"] = _link_name(obj, "Profile")
inputs["spine"] = _link_name(obj, "Spine")
return inputs
# --- Fillet ---
if tid == "PartDesign::Fillet":
inputs["radius"] = _prop_value(obj, "Radius")
return inputs
# --- Chamfer ---
if tid == "PartDesign::Chamfer":
inputs["chamfer_type"] = str(_prop_raw(obj, "ChamferType") or "")
inputs["size"] = _prop_value(obj, "Size")
inputs["size2"] = _prop_value(obj, "Size2")
inputs["angle"] = _prop_value(obj, "Angle")
return inputs
# --- Draft ---
if tid == "PartDesign::Draft":
inputs["angle"] = _prop_value(obj, "Angle")
inputs["reversed"] = _prop_raw(obj, "Reversed")
return inputs
# --- Thickness ---
if tid == "PartDesign::Thickness":
inputs["value"] = _prop_value(obj, "Value")
inputs["reversed"] = _prop_raw(obj, "Reversed")
inputs["mode"] = str(_prop_raw(obj, "Mode") or "")
inputs["join"] = str(_prop_raw(obj, "Join") or "")
return inputs
# --- Mirrored ---
if tid == "PartDesign::Mirrored":
inputs["mirror_plane"] = _link_name(obj, "MirrorPlane")
return inputs
# --- LinearPattern ---
if tid == "PartDesign::LinearPattern":
inputs["direction"] = _link_name(obj, "Direction")
inputs["reversed"] = _prop_raw(obj, "Reversed")
inputs["length"] = _prop_value(obj, "Length")
inputs["occurrences"] = _prop_value(obj, "Occurrences")
return inputs
# --- PolarPattern ---
if tid == "PartDesign::PolarPattern":
inputs["axis"] = _link_name(obj, "Axis")
inputs["reversed"] = _prop_raw(obj, "Reversed")
inputs["angle"] = _prop_value(obj, "Angle")
inputs["occurrences"] = _prop_value(obj, "Occurrences")
return inputs
# --- Scaled ---
if tid == "PartDesign::Scaled":
inputs["factor"] = _prop_value(obj, "Factor")
inputs["occurrences"] = _prop_value(obj, "Occurrences")
return inputs
# --- MultiTransform ---
if tid == "PartDesign::MultiTransform":
try:
inputs["transform_count"] = len(obj.Transformations)
except Exception:
pass
return inputs
# --- Boolean ---
if tid == "PartDesign::Boolean":
inputs["type"] = str(_prop_raw(obj, "Type") or "")
return inputs
# --- Primitives (additive) ---
if tid in (
"PartDesign::AdditiveBox",
"PartDesign::SubtractiveBox",
):
inputs["length"] = _prop_value(obj, "Length")
inputs["width"] = _prop_value(obj, "Width")
inputs["height"] = _prop_value(obj, "Height")
return inputs
if tid in (
"PartDesign::AdditiveCylinder",
"PartDesign::SubtractiveCylinder",
):
inputs["radius"] = _prop_value(obj, "Radius")
inputs["height"] = _prop_value(obj, "Height")
inputs["angle"] = _prop_value(obj, "Angle")
return inputs
if tid in (
"PartDesign::AdditiveSphere",
"PartDesign::SubtractiveSphere",
):
inputs["radius"] = _prop_value(obj, "Radius")
return inputs
if tid in (
"PartDesign::AdditiveCone",
"PartDesign::SubtractiveCone",
):
inputs["radius1"] = _prop_value(obj, "Radius1")
inputs["radius2"] = _prop_value(obj, "Radius2")
inputs["height"] = _prop_value(obj, "Height")
return inputs
if tid in (
"PartDesign::AdditiveEllipsoid",
"PartDesign::SubtractiveEllipsoid",
):
inputs["radius1"] = _prop_value(obj, "Radius1")
inputs["radius2"] = _prop_value(obj, "Radius2")
inputs["radius3"] = _prop_value(obj, "Radius3")
return inputs
if tid in (
"PartDesign::AdditiveTorus",
"PartDesign::SubtractiveTorus",
):
inputs["radius1"] = _prop_value(obj, "Radius1")
inputs["radius2"] = _prop_value(obj, "Radius2")
return inputs
if tid in (
"PartDesign::AdditivePrism",
"PartDesign::SubtractivePrism",
):
inputs["polygon"] = _prop_raw(obj, "Polygon")
inputs["circumradius"] = _prop_value(obj, "Circumradius")
inputs["height"] = _prop_value(obj, "Height")
return inputs
if tid in (
"PartDesign::AdditiveWedge",
"PartDesign::SubtractiveWedge",
):
for dim in (
"Xmin",
"Ymin",
"Zmin",
"X2min",
"Z2min",
"Xmax",
"Ymax",
"Zmax",
"X2max",
"Z2max",
):
inputs[dim.lower()] = _prop_value(obj, dim)
return inputs
# --- Datum / ShapeBinder ---
if tid in (
"PartDesign::Point",
"PartDesign::Line",
"PartDesign::Plane",
"PartDesign::CoordinateSystem",
"PartDesign::ShapeBinder",
"PartDesign::SubShapeBinder",
):
try:
p = obj.Placement
inputs["position"] = {
"x": _safe_float(p.Base.x),
"y": _safe_float(p.Base.y),
"z": _safe_float(p.Base.z),
}
inputs["rotation"] = {
"axis_x": _safe_float(p.Rotation.Axis.x),
"axis_y": _safe_float(p.Rotation.Axis.y),
"axis_z": _safe_float(p.Rotation.Axis.z),
"angle": _safe_float(p.Rotation.Angle),
}
except Exception:
pass
return inputs
# --- Body / Part (containers) ---
if tid in ("PartDesign::Body", "App::Part", "Part::Feature"):
try:
inputs["child_count"] = len(obj.Group)
except Exception:
inputs["child_count"] = 0
return inputs
# --- Fallback ---
inputs["label"] = obj.Label
return inputs
def compute_properties_hash(obj: Any) -> str:
"""Return a SHA-256 hex digest of *obj*'s parametric inputs.
The hash is used for memoization -- if a node's inputs haven't
changed since the last validation run, re-validation can be skipped.
"""
inputs = _collect_inputs(obj)
canonical = json.dumps(inputs, sort_keys=True, default=str)
return hashlib.sha256(canonical.encode()).hexdigest()
# ---------------------------------------------------------------------------
# DAG extraction
# ---------------------------------------------------------------------------
def extract_dag(
doc: Any,
) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
"""Walk a FreeCAD document and return ``(nodes, edges)``.
*nodes* is a list of dicts matching the Silo ``PUT /dag`` payload
schema. *edges* connects dependencies (source) to dependents
(target).
Only objects whose TypeId is recognized by :func:`classify_type`
are included. Edges are limited to pairs where **both** endpoints
are included, preventing dangling references to internal objects
such as ``App::Origin``.
"""
# Pass 1 -- identify included objects
included: Set[str] = set()
classified: Dict[str, str] = {} # obj.Name -> node_type
for obj in doc.Objects:
if not hasattr(obj, "TypeId"):
continue
node_type = classify_type(obj.TypeId)
if node_type is not None:
included.add(obj.Name)
classified[obj.Name] = node_type
# Pass 2 -- build nodes and edges
nodes: List[Dict[str, Any]] = []
edges: List[Dict[str, Any]] = []
seen_edges: Set[Tuple[str, str]] = set()
for obj in doc.Objects:
if obj.Name not in included:
continue
nodes.append(
{
"node_key": obj.Name,
"node_type": classified[obj.Name],
"properties_hash": compute_properties_hash(obj),
"metadata": {
"label": obj.Label,
"type_id": obj.TypeId,
},
}
)
# Walk dependencies: OutList contains objects this one depends on
try:
out_list = obj.OutList
except Exception:
continue
for dep in out_list:
if not hasattr(dep, "Name"):
continue
if dep.Name not in included:
continue
edge_key = (dep.Name, obj.Name)
if edge_key in seen_edges:
continue
seen_edges.add(edge_key)
edges.append(
{
"source_key": dep.Name,
"target_key": obj.Name,
"edge_type": "depends_on",
}
)
return nodes, edges

View File

@@ -12,17 +12,4 @@
<subdirectory>./</subdirectory>
</workbench>
</content>
<!-- Kindred Create extensions -->
<kindred>
<min_create_version>0.1.0</min_create_version>
<load_priority>60</load_priority>
<pure_python>true</pure_python>
<dependencies>
<dependency>sdk</dependency>
</dependencies>
<contexts>
<context id="*" action="overlay"/>
</contexts>
</kindred>
</package>

View File

@@ -1,156 +0,0 @@
"""Headless runner entry points for silorunner compute jobs.
These functions are invoked via ``create --console -e`` by the
silorunner binary. They must work without a display server.
Entry Points
------------
dag_extract(input_path, output_path)
Extract feature DAG and write JSON.
validate(input_path, output_path)
Rebuild all features and report pass/fail per node.
export(input_path, output_path, format='step')
Export geometry to STEP, IGES, STL, or OBJ.
"""
import json
import FreeCAD
def dag_extract(input_path, output_path):
"""Extract the feature DAG from a Create file.
Parameters
----------
input_path : str
Path to the ``.kc`` or ``.FCStd`` file.
output_path : str
Path to write the JSON output.
Output JSON::
{"nodes": [...], "edges": [...]}
"""
from dag import extract_dag
doc = FreeCAD.openDocument(input_path)
try:
nodes, edges = extract_dag(doc)
with open(output_path, "w") as f:
json.dump({"nodes": nodes, "edges": edges}, f)
FreeCAD.Console.PrintMessage(
f"DAG extracted: {len(nodes)} nodes, {len(edges)} edges -> {output_path}\n"
)
finally:
FreeCAD.closeDocument(doc.Name)
def validate(input_path, output_path):
"""Validate a Create file by rebuilding all features.
Parameters
----------
input_path : str
Path to the ``.kc`` or ``.FCStd`` file.
output_path : str
Path to write the JSON output.
Output JSON::
{
"valid": true/false,
"nodes": [
{"node_key": "Pad001", "state": "clean", "message": null, "properties_hash": "..."},
...
]
}
"""
from dag import classify_type, compute_properties_hash
doc = FreeCAD.openDocument(input_path)
try:
doc.recompute()
results = []
all_valid = True
for obj in doc.Objects:
if not hasattr(obj, "TypeId"):
continue
node_type = classify_type(obj.TypeId)
if node_type is None:
continue
state = "clean"
message = None
if hasattr(obj, "isValid") and not obj.isValid():
state = "failed"
message = f"Feature {obj.Label} failed to recompute"
all_valid = False
results.append(
{
"node_key": obj.Name,
"state": state,
"message": message,
"properties_hash": compute_properties_hash(obj),
}
)
with open(output_path, "w") as f:
json.dump({"valid": all_valid, "nodes": results}, f)
status = "PASS" if all_valid else "FAIL"
FreeCAD.Console.PrintMessage(
f"Validation {status}: {len(results)} nodes -> {output_path}\n"
)
finally:
FreeCAD.closeDocument(doc.Name)
def export(input_path, output_path, format="step"):
"""Export a Create file to an external geometry format.
Parameters
----------
input_path : str
Path to the ``.kc`` or ``.FCStd`` file.
output_path : str
Path to write the exported file.
format : str
One of ``step``, ``iges``, ``stl``, ``obj``.
"""
import Part
doc = FreeCAD.openDocument(input_path)
try:
shapes = [
obj.Shape for obj in doc.Objects if hasattr(obj, "Shape") and obj.Shape
]
if not shapes:
raise ValueError("No geometry found in document")
compound = Part.makeCompound(shapes)
format_lower = format.lower()
if format_lower == "step":
compound.exportStep(output_path)
elif format_lower == "iges":
compound.exportIges(output_path)
elif format_lower == "stl":
import Mesh
Mesh.export([compound], output_path)
elif format_lower == "obj":
import Mesh
Mesh.export([compound], output_path)
else:
raise ValueError(f"Unsupported format: {format}")
FreeCAD.Console.PrintMessage(
f"Exported {format_lower.upper()} -> {output_path}\n"
)
finally:
FreeCAD.closeDocument(doc.Name)

View File

@@ -10,6 +10,9 @@ backward-compatible :class:`SchemaFormDialog` modal.
"""
import json
import urllib.error
import urllib.parse
import urllib.request
import FreeCAD
from PySide import QtCore, QtGui, QtWidgets
@@ -264,8 +267,17 @@ class SchemaFormWidget(QtWidgets.QWidget):
def _fetch_properties(self, category: str) -> dict:
"""Fetch merged property definitions for a category."""
from silo_commands import _get_api_url, _get_auth_headers, _get_ssl_context
api_url = _get_api_url().rstrip("/")
url = f"{api_url}/schemas/kindred-rd/properties?category={urllib.parse.quote(category)}"
req = urllib.request.Request(url, method="GET")
req.add_header("Accept", "application/json")
for k, v in _get_auth_headers().items():
req.add_header(k, v)
try:
data = self._client.get_property_schema(category=category)
resp = urllib.request.urlopen(req, context=_get_ssl_context(), timeout=5)
data = json.loads(resp.read().decode("utf-8"))
return data.get("properties", data)
except Exception as e:
FreeCAD.Console.PrintWarning(
@@ -275,10 +287,19 @@ class SchemaFormWidget(QtWidgets.QWidget):
def _generate_pn_preview(self, category: str) -> str:
"""Call the server to preview the next part number."""
from silo_commands import _get_schema_name
from silo_commands import _get_api_url, _get_auth_headers, _get_ssl_context
api_url = _get_api_url().rstrip("/")
url = f"{api_url}/generate-part-number"
payload = json.dumps({"schema": "kindred-rd", "category": category}).encode()
req = urllib.request.Request(url, data=payload, method="POST")
req.add_header("Content-Type", "application/json")
req.add_header("Accept", "application/json")
for k, v in _get_auth_headers().items():
req.add_header(k, v)
try:
data = self._client.generate_part_number(_get_schema_name(), category)
resp = urllib.request.urlopen(req, context=_get_ssl_context(), timeout=5)
data = json.loads(resp.read().decode("utf-8"))
return data.get("part_number", "")
except Exception:
return ""
@@ -553,10 +574,8 @@ class SchemaFormWidget(QtWidgets.QWidget):
return
try:
from silo_commands import _get_schema_name
result = self._client.create_item(
_get_schema_name(),
"kindred-rd",
data["category"],
data["description"],
projects=data["projects"],

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,8 @@ delegating operations to the established Silo infrastructure while
providing the standardized origin interface.
"""
import os
import FreeCAD
import FreeCADGui
from silo_commands import (
@@ -30,7 +32,7 @@ class SiloOrigin:
Key behaviors:
- Documents are always stored locally (hybrid local-remote model)
- Database tracks metadata, part numbers, and revision history
- Server stores revision files for sync/backup
- MinIO stores revision snapshots for sync/backup
- Identity is tracked by UUID (SiloItemId), displayed as part number
"""
@@ -61,8 +63,10 @@ class SiloOrigin:
return self._nickname
def icon(self) -> str:
"""Return icon name for BitmapFactory."""
return "silo"
"""Return icon path for BitmapFactory."""
return os.path.join(
os.path.dirname(os.path.abspath(__file__)), "resources", "icons", "silo.svg"
)
def type(self) -> int:
"""Return origin type (OriginType.PLM = 1)."""
@@ -565,9 +569,12 @@ def register_silo_origin():
This should be called during workbench initialization to make
Silo available as a file origin.
"""
from kindred_sdk import register_origin
register_origin(get_silo_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():
@@ -577,7 +584,9 @@ def unregister_silo_origin():
"""
global _silo_origin
if _silo_origin:
from kindred_sdk import unregister_origin
unregister_origin(_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

View File

@@ -19,10 +19,23 @@ from PySide import QtCore, QtGui, QtWidgets
# ---------------------------------------------------------------------------
# Catppuccin Mocha palette
# ---------------------------------------------------------------------------
# Catppuccin Mocha palette — sourced from kindred-addon-sdk
from kindred_sdk.theme import get_theme_tokens
_MOCHA = get_theme_tokens()
_MOCHA = {
"base": "#1e1e2e",
"mantle": "#181825",
"crust": "#11111b",
"surface0": "#313244",
"surface1": "#45475a",
"surface2": "#585b70",
"text": "#cdd6f4",
"subtext0": "#a6adc8",
"subtext1": "#bac2de",
"blue": "#89b4fa",
"green": "#a6e3a1",
"red": "#f38ba8",
"peach": "#fab387",
"lavender": "#b4befe",
"overlay0": "#6c7086",
}
_PREF_GROUP = "User parameter:BaseApp/Preferences/Mod/KindredSilo"