FreeCAD CAM 工作台 Asset Manager 资产管理模块:架构、序列化协议与实战指南
2026/9/10 22:41:17 网站建设 项目流程

FreeCAD CAM 工作台 Asset Manager 资产管理模块:架构、序列化协议与实战指南

【免费下载链接】FreeCADOfficial source code of FreeCAD, a free and opensource multiplatform 3D parametric modeler.项目地址: https://gitcode.com/GitHub_Trending/fr/FreeCAD

导读

本文基于 FreeCAD 官方仓库中src/Mod/CAM/Path/Tool/assets/目录下的模块文档与源码,系统讲解 CAM 工作台的资产管理框架(Asset Manager)。你将理解 CAM 中刀具库、刀具、刀具形状、机床等资源如何被统一建模为"资产"(Asset),掌握AssetUri资源寻址、Asset/AssetSerializer序列化协议、FileStore/MemoryStore存储后端、依赖解析与缓存等核心机制,并通过可运行的 API 示例学会在 FreeCAD 的 Python 环境中注册、存储、读取与复制资产。

模块定位:为 CAM 工作台打造的资产管理框架

src/Mod/CAM/Path/Tool/assets/README.md开宗明义:该模块实现了一个资产管理器(AssetManager),为 FreeCAD CAM 工作台提供资源的**存储(store)、更新(update)、删除(delete)与获取(receive)**能力。与 FreeCAD 传统的"刀具库文件结构"不同,这套框架将资源抽象为可寻址、可版本化、可依赖的"资产",为后续统一资源 UI 奠定基础。

从源码目录结构看,模块组织非常清晰(assets/):

  • asset.pyAsset抽象基类,所有可存储资源的公共接口;
  • serializer.pyAssetSerializer抽象基类及DummyAssetSerializer占位实现;
  • uri.pyAssetUri,资产的统一资源标识符;
  • manager.pyAssetManager核心管理器(约 1184 行);
  • cache.pyAssetCache带 LRU 与依赖传播失效的缓存;
  • store/:存储后端,含base.pyAssetStore抽象基类)、filestore.pyFileStore本地文件系统实现)、memory.pyMemoryStore内存实现);
  • ui/:UI 辅助模块(文件对话框、工具函数、偏好设置);
  • docs/blender-assets.jpg:Blender 资源管理器的参考界面截图。

所有公共类型都在 assets/init.py 中导出:AssetAssetUriAssetManagerAssetSerializerDummyAssetSerializerAssetStoreMemoryStoreFileStore,使用时可统一从Path.Tool.assets导入。

设计目标与最终愿景

从 Blender 借鉴的统一资源界面

模块文档明确指出,当前AssetManager尚无 UI,但规划中会添加。终极愿景是提供一个统一 UI,能够:

  • 从任意来源下载资产:在线数据库、Git 仓库、本地存储;
  • 在不同存储之间复制资产,从而事实上支持"发布"(publishing)资产——即把资源从一个存储推送到另一个存储(例如从本地推送到在线仓库)。

文档以 Blender 的资源管理器作为类比目标,仓库内保留了参考截图 blender-assets.jpg,其典型形态是"左侧分类导航 + 右侧网格式资源缩略图列表":

值得注意的是,这一愿景在仓库中已经迈出了第一步:CamAssetManager(见下文"实战:CAM 中的落地实现")已经将本地用户目录与内置资源目录组织为两个FileStore,并实现了"本地缺失时自动回退内置"的查找语义,相当于多存储源读取的雏形。

CAM 语境下的"资产"是什么

文档给出的定义是:资产是任意数据,例如 FreeCAD 模型、刀具(Tools)等。具体到 CAM 工作台,资产包括:

  • 刀具位库(Tool bit libraries)
  • 刀具位(Tool bits)
  • 刀具位形状文件(Tool bit shape files)
  • 刀具位形状图标(Tool bit shape icons)
  • 机床(Machines)
  • 夹具(Fixtures)
  • 后处理器(Post processors)
  • ……(省略号表示可继续扩展)

资产之间存在依赖关系,这是整个框架设计的核心约束之一:

例如,一个 ToolBitLibrary 依赖 ToolBits,而一个 ToolBit 依赖 ToolBitShape(一个 FreeCAD 模型)。

从 camassets.py 中可以印证实际资产类型及其落盘格式:

asset_mapping = { "toolbitlibrary": "Tools/Library/{asset_id}.fctl", "toolbit": "Tools/Bit/{asset_id}.fctb", "toolbitshape": "Tools/Shape/{asset_id}.fcstd", "toolbitshapesvg": "Tools/Shape/{asset_id}", # Asset ID has ".svg" included "toolbitshapepng": "Tools/Shape/{asset_id}", # Asset ID has ".png" included "machine": "Machine/{asset_id}.fcm", }

可以看到:刀具库对应.fctl文件、刀具对应.fctb文件、形状对应.fcstd模型文件,而形状图标分为.svg.png两种asset_type。这正是"依赖链"的具体体现——toolbit资产通过其序列化数据引用toolbitshape资产的 URI。

核心概念一:AssetUri —— 资产寻址协议

AssetUri(uri.py)是资产的统一资源标识符,其结构为:

<asset_type>://<asset_id>[/<version>][?params]

文档类图中的注释给出了三个真实示例:

URI含义
material://1234567/1材料类型,ID 为1234567,版本 1
toolbitshape://endmill/1立铣刀形状,版本 1
material://aluminium-6012/2铝合金 6012,版本 2

从实现看,AssetUri支持:

  • 手工解析__init__通过split("://")拆分类型与其余部分,再按/拆分asset_id与可选的version,最后用urllib.parse.parse_qs解析?key=value查询参数;非法结构(多余路径段、空类型或空 ID)会抛出ValueError
  • 构造AssetUri.build(asset_type, asset_id, version=None, params=None)静态方法可直接从组件构造;
  • 判定AssetUri.is_uri(uri)可判断一个字符串是否为合法 URI;
  • 等值与哈希:实现了__eq__/__hash__,因此AssetUri可以安全地用作字典键或集合元素——这一点对后续依赖去重、循环检测至关重要。

另外Asset基类还提供了resolve_name(identifier)类方法,它先判断输入是否为合法 URI 字符串,是则解析为AssetUri,否则按当前资产类型构造asset_type://identifier,从而允许调用方直接用 ID 或名称代替完整 URI。

核心概念二:Asset 接口与 AssetSerializer 序列化协议

Asset:可存储对象的公共接口

Asset(asset.py)是一个抽象基类,所有希望"可存储"的类都必须实现它。其关键成员:

  • asset_type: str:类属性,声明资产类型(如"toolbit""material")。__init__会校验子类必须定义该属性,否则抛ValueError
  • get_id() -> str:抽象方法,返回资产唯一 ID;
  • get_uri() -> AssetUri:由asset_type+get_id()便捷构造 URI;
  • label属性:默认返回类名,用于 UI 展示;
  • to_bytes(serializer)/from_bytes(data, id, dependencies, serializer)把实际的序列化/反序列化工作委托给AssetSerializer
  • extract_dependencies(data, serializer):使用序列化器从原始字节中提取依赖 URI 列表。

AssetSerializer:序列化格式的抽象基类

AssetSerializer(serializer.py)定义了把资产对象转换为字节、以及从字节还原的接口,其类属性包括:

  • for_class: Type[Asset]:该序列化器服务的资产类;
  • extensions: Tuple[str, ...]:关联的文件扩展名(如(".fctb",)),供文件对话框生成过滤器;
  • mime_type: str:MIME 类型;
  • can_import/can_export:是否支持导入/导出,默认均为True

抽象方法:

  • extract_dependencies(data) -> List[AssetUri]:从序列化字节中提取依赖 URI;
  • serialize(asset) -> bytes:对象 → 字节;
  • deserialize(data, id, dependencies) -> Asset:字节 + 已解析依赖 → 对象。dependenciesNone表示浅加载(shallow load),即未解析依赖
  • deep_deserialize(data) -> Asset:与deserialize类似,但自行从数据中构建依赖(数据中可能内嵌部分依赖),用于导入/导出场景。

此外还提供了一个DummyAssetSerializer占位实现:extract_dependencies返回空列表、serialize返回空字节、deserialize直接抛RuntimeError。它服务于那些"不需要非原生序列化"的简单资产——这类资产可以自己实现to_bytes/from_bytes而忽略传入的序列化器。

为什么这样设计?文档强调:这种关注点分离(separation of concerns)使得资产的存储与检索独立于其具体序列化格式AssetManager只需为每种资产类型注册对应的序列化器,即可同时支持多种格式(例如同一把刀具既可以是.fctb,也可以是 LinuxCNC 或 Camotics 的文本格式——仓库中 library/serializers/ 与 toolbit/serializers/ 正是fctlfctblinuxcnccamotics等多个序列化器的实际落地)。

核心概念三:AssetStore —— 存储后端抽象

AssetStore(store/base.py)是抽象基类,负责"以原始字节存储/检索资产",是对具体存储后端(本地文件系统、HTTP 服务器等)的低层封装。接口全部为异步方法:

方法作用
get(uri) -> bytes按 URI 读取原始字节,不存在抛FileNotFoundError
exists(uri) -> bool判断资产是否存在(基类默认通过 tryget实现)
delete(uri)删除资产
create(asset_type, asset_id, data) -> AssetUri新建资产,返回最终 URI
update(uri, data) -> AssetUri更新资产,创建新版本
list_assets(asset_type, limit, offset) -> List[AssetUri]列出资产(可筛选类型、分页;版本化存储只列每个资产的最新版本)
count_assets(asset_type) -> int统计资产数量
list_versions(uri) -> List[AssetUri]列出某资产的所有可用版本
is_empty(asset_type) -> bool是否为空

内置两个实现:

  • FileStore(store/filestore.py):本地文件系统实现,支持版本化
  • MemoryStore(store/memory.py):纯内存实现,主要用于测试与演示,dump(print)方法可打印内容。

FileStore 的路径映射与版本化

FileStoreasset_type://asset_id[/version]映射到基础目录下的文件路径,映射规则可用字典配置。文档给出的示例:

mapping = { "*": "{asset_type}/{asset_id}/{version}.dat", "model": "models_dir/{asset_id}-{version}.ml", "dataset": "data/{asset_id}.csv", # 无版本(概念版本恒为 "1") }

默认映射为"*": "{asset_type}/{asset_id}/{version}"。支持的占位符有asset_typeasset_ididversion,其中{version}按贪心.*匹配,但版本号约定为数字字符串__init__时会做严格校验(filestore.py):

  • 每个模式必须是字符串;
  • 只允许已知占位符;
  • 模式必须包含{asset_id}{id}
  • 通配符*键的模式必须包含{asset_type}

版本管理语义(均有源码支撑):

  • 创建:新资产概念上恒为版本"1";目标文件已存在则抛FileExistsError
  • 更新:先列出已有版本,取最大版本号 +1 写入新文件(版本化模式);非版本化模式则直接覆盖;
  • 读取uri.version == "latest"时会先list_versions取最新版本;非版本化模式只接受版本"1"或不带版本;
  • 删除:不带版本删除该资产全部版本,并递归清理空的父目录(不越过base_dir);
  • 列出list_assets遍历base_dir下所有文件,通过路径反解析出 URI,对每个(asset_type, asset_id)只保留最大版本,支持limit/offset分页。

此外FileStore还做了大小写不敏感的路径解析兼容(_resolve_case_insensitive),以兼容 Windows/macOS 文件系统行为差异。

AssetManager:管理器功能全景

AssetManager(manager.py)是整个模块的中枢。文档归纳的四大能力在源码中一一对应:

1. 存储管理(保持既有刀具库文件结构)

register_store(store, cacheable=False)按名称注册AssetStorecacheable=True的存储会启用对象缓存。stores字典将协议(存储名)映射到存储实例。文档特别强调:框架管理存储的同时保留现有 FreeCAD 刀具库文件结构——这正是通过FileStore的可配置路径映射实现的,CAM 中沿用Tools/Library/Tools/Bit/等传统目录而不破坏兼容性。

2. 依赖管理

_fetch_asset_construction_data_recursive_async()(manager.py)递归获取资产及其依赖的原始数据:

  • 循环依赖检测:维护visited_uris集合,再次遇到已访问 URI 时抛RuntimeError("Cyclic dependency ...")
  • 深度控制depth参数控制递归层级——depth=None表示无限深,depth=0表示只取该资产本身、不解析任何依赖dependencies_dataNone作为"未尝试获取"的标记),depth=n每递归一层减一;
  • 多存储回退:按传入的存储名序列逐个尝试,FileNotFoundError时跳到下一个存储;
  • 内部使用_AssetConstructionData数据类暂存"存储名 + URI + 原始字节 + 资产类 + 依赖数据",为后续对象组装与缓存做准备。

3. 线程管理(异步存储 + 主线程组装)

这是该模块最值得注意的设计决策。AssetManager采用"异步 IO 获取数据,同步构建对象"的混合模型:

  • 读/写存储是异步的get_asyncget_raw_asyncadd_asyncdelete_async等均为async方法,底层AssetStore接口全部异步,可通过asyncio并发调度(如get_bulkasyncio.gather并发拉取多个资产);
  • 对象组装是同步的get()内部用asyncio.run()完成数据拉取后,在当前线程(文档与代码注释明确假定为主 UI 线程)同步调用_build_asset_tree_from_data_sync()递归构建资产树并调用from_bytes。原因在于 FreeCAD 对象(如刀具、形状)的装配可能涉及 UI 操作,必须在主线程进行;
  • 代码甚至包含防御性检查:若AssetManager.get()在非主线程被调用且from_bytes可能做 UI 操作,会记录logger.warning

get()的完整流程(manager.py)可概括为:

get(uri, store, depth) └─ asyncio.run(_fetch_asset_construction_data_recursive_async(uri, stores, visited=set(), depth)) ├─ 循环检测 → 依赖提取 → 递归获取依赖数据(可并行) └─ 返回 _AssetConstructionData 树 └─ _build_asset_tree_from_data_sync(construction_data) # 主线程同步执行 ├─ 缓存查询(cacheable 存储) ├─ 递归解析依赖对象 └─ asset_class.from_bytes(data, id, dependencies, serializer) → Asset

4. 统一序列化协议

register_asset(asset_class, serializer)注册资产类与序列化器;get_serializer_for_class()issubclass匹配(因此支持继承层次)。Asset.to_bytes/from_bytes委托序列化器完成格式转换,使所有资产共享统一的导入/导出机制。

完整 API 速查(来自 README 类图与源码)

AssetManager公开方法(同步包装 + 对应_async版本):

方法作用
register_store(store, cacheable=False)注册存储
register_asset(asset_class, serializer)注册资产类型与序列化器
get(uri, store="local", depth=None)获取资产对象(depth=None无限深,0仅自身)
get_or_none(...)同上,找不到返回None而非抛异常
get_raw(uri, store="local")获取原始字节
add(obj, store="local")添加资产对象(存在则更新为新版本)
add_raw(asset_type, asset_id, data, store="local")添加原始字节资产
add_file(asset_type, path, store="local", asset_id=None)便捷封装:读取文件字节添加
delete(uri, store="local")删除资产
exists(uri, store="local")是否存在
is_empty(asset_type=None, store="local")是否为空
list_assets(asset_type=None, limit=None, offset=None, store="local")列出资产 URI(分页)
list_versions(uri, store="local")列出版本
count_assets(asset_type=None, store="local")统计数量
get_bulk(uris, store="local", depth=None)批量获取(并发拉取,异常会重抛)
fetch(asset_type=None, limit=None, offset=None, store="local", depth=None)列出并批量实例化资产
copy(src, dest_store, store="local", dest=None)浅拷贝:仅复制顶层资产原始字节(get_raw+add_raw
deepcopy(src, dest_store, store="local", dest=None)深拷贝:连同全部依赖一起复制,依赖已存在则跳过、顶层覆盖
get_registered_asset_types()已注册资产类型列表

API 实战示例:自定义 Material 资产

README 提供了一个完整的可运行示例,这里完整保留并补充注释。它演示了"定义资产类 → 注册存储与资产 → 存储 → 读取"的最小闭环:

import pathlib from typing import Any, Mapping, List, Type, Optional from Path.Tool.assets import AssetManager, FileStore, AssetUri, Asset # 定义一个实现 Asset 接口的简单 Material 类 class Material(Asset): asset_type: str = "material" def __init__(self, name: str): self.name = name def get_id(self) -> str: return self.name.lower().replace(" ", "-") @classmethod def dependencies(cls, data: bytes) -> List[AssetUri]: return [] @classmethod def from_bytes( cls, data: bytes, id: str, dependencies: Optional[Mapping[AssetUri, Asset]] ) -> Material: return cls(data.decode("utf-8")) def to_bytes(self) -> bytes: return self.name.encode("utf-8") manager = AssetManager() # 注册 FileStore 与资产类 manager.register_store(FileStore("local", pathlib.Path("/tmp/assets"))) manager.register_asset(Material) # 创建并获取资产 asset_uri = manager.add(Material("Copper")) print(f"Stored with URI: {asset_uri}") retrieved_asset = manager.get(asset_uri) print(f"Retrieved: {retrieved_asset}")

结合源码可以指出几个实际运行要点:

  1. manager.add(obj)内部调用add_async(manager.py):先obj.get_uri()取 URI,再通过get_serializer_for_class取序列化器、obj.to_bytes(serializer)序列化,最后走add_raw_async——它会先尝试update(即版本 +1),捕获FileNotFoundError后再create(版本 1)。因此同名资产再次add不会覆盖,而是产生新版本
  2. 未注册资产类型的对象会被警告"unregistered type",但不会阻断写入;
  3. get(uri)若在所有存储中都找不到,会抛FileNotFoundError;需要容错时可改用get_or_none

注意 README 示例中get_id的写法(def get_id() -> str)缺少self参数,与 asset.py 中get_id(self)的抽象签名不一致,实际实现必须带self;上例已修正。另外 README 示例未显式传序列化器,此时AssetManager.register_asset仍要求第二个参数,可传入DummyAssetSerializer(配合资产自身实现to_bytes/from_bytes),这与 serializer.py 中DummyAssetSerializer的设计意图完全吻合。

实战:CAM 中的落地实现 CamAssetManager

框架在 CAM 工作台中的实际装配位于 camassets.py:

user_asset_store = FileStore( name="local", base_dir=Preferences.getAssetPath(), mapping=asset_mapping, # "Tools/Library/{asset_id}.fctl" 等 ) builtin_asset_store = FileStore( name="builtin", base_dir=Preferences.getBuiltinAssetPath(), mapping=builtin_asset_mapping, # "Library/{asset_id}.fctl" 等 ) class CamAssetManager(AssetManager): def __init__(self): super().__init__() self.register_store(user_asset_store) self.register_store(builtin_asset_store) def get(self, uri, store=("local", "builtin"), depth=None): # 本地缺失时自动回退到内置存储 return super().get(uri, store=store, depth=depth) cam_assets = CamAssetManager()

关键细节:

  • 两个存储local(用户可写目录,沿用Tools/Library/Tools/Bit/Tools/Shape/Machine/结构)与builtin(内置资源,沿用Library/Bit/Shape/结构);
  • 多存储回退get默认按("local", "builtin")顺序查找,这正是AssetManager._fetch_asset_construction_data_recursive_async中"逐个存储尝试、FileNotFoundError则跳过"机制的实际应用;
  • 自动初始化ensure_assets_initialized()在本地存储为空时,从内置路径批量导入*.fctl刀具库、*.fctb刀具、*.fcstd/*.svg/*.png形状资产(见ensure_library_assets_initializedensure_toolbit_assets_initializedensure_toolbitshape_assets_initialized);
  • 偏好联动addToolPreferenceObserver(_on_asset_path_changed)监听资源目录偏好变更,变化时调用user_asset_store.set_dir(value)并重新初始化。

缓存机制:AssetCache

AssetCache(cache.py)为可缓存存储提供对象级缓存:

  • 容量上限AssetManager(cache_max_size_bytes=100*1024*1024),默认 100 MB;
  • 缓存键CacheKey(store_name, asset_uri_str, raw_data_hash, dependency_signature)——raw_data_hash为原始数据 SHA-256,dependency_signature为直接依赖 URI 的有序元组(浅加载时用特殊标记("shallow_children",)),因此同一 URI 不同依赖深度/不同内容不会互相污染
  • LRU 淘汰:超出容量按最久未使用顺序淘汰;
  • 依赖感知失效invalidate_for_uri()不仅删除该 URI 的缓存项,还会沿_cache_dependents_map(依赖 → 被依赖方)递归删除所有依赖它的上级资产缓存,保证刀具形状更新后引用它的刀具库缓存同步失效。add_raw_async/delete对可缓存存储都会触发失效。

UI 辅助模块

ui/目录包含资产管理器 UI 的辅助代码:

  • filedialog.py:AssetOpenDialog(继承QFileDialog)提供资产的导入/导出文件对话框。它根据资产类型设置默认目录,用序列化器的extensions生成文件过滤器(make_import_filters/make_export_filters),按扩展名反查序列化器(get_serializer_from_extension);导入时会先用extract_dependencies检查依赖是否在存储中(local/builtin),若缺失则尝试在库文件同级目录查找刀具文件(如Library/旁的Bit/目录),找不到则弹窗报错;
  • util.py:通用工具函数(过滤器构造、序列化器按扩展名匹配等);
  • preferences.py:偏好设置相关辅助。

仓库中 library/ui/(浏览器、停靠面板、编辑器)与 toolbit/ui/ 等目录已基于这些基础构建了实际可用的刀具库/刀具浏览界面。

类图速览(Mermaid)

README 附带了完整的 Mermaid 类图,核心关系整理如下(原文保留在 README.md 中):

  • AssetManager聚合多个AssetStorestores: Mapping[str, AssetStore]);
  • AssetStore为抽象基类,FileStoreMemoryStore是其实现(is关系);
  • AssetManager聚合多个AssetSerializerAsset通过to_bytes/from_bytes使用AssetSerializer
  • AssetManager创建Assetget/add的产物);
  • CAM 模块示例:ToolBitShapeToolBit均实现Asset接口,ToolBit依赖(has)ToolBitShape
  • 材料模块示例:Material实现Asset接口。

测试验证

仓库在 src/Mod/CAM/CAMTests/ 下提供了系统化的测试套件,是理解模块行为的最佳"活文档":

  • TestPathToolAssetManager.py:管理器测试(含MockAssetMockAssetWithDeps两个模拟资产类,覆盖注册、增删查、深浅拷贝、循环依赖等场景);
  • TestPathToolAssetUri.py:URI 解析与构造测试;
  • TestPathToolAssetStore.py:存储后端(FileStore/MemoryStore)测试;
  • TestPathToolAssetCache.py:缓存与失效传播测试;
  • TestPathToolBitSerializer.py、TestPathToolShapeClasses.py 等:具体资产类型与序列化器测试。

潜在未来扩展

README 明确列出三条扩展方向(README.md 原文):

  1. AssetManager UI:提供浏览与搜索 UI,覆盖各类资产(机床、夹具、库、刀具、形状、后处理器……),支持任意来源(在线数据库、Git 仓库等);
  2. GitStore:连接 FreeCAD 官方维护的 FreeCAD-library 资源库,将远程 Git 仓库作为存储后端;
  3. HttpStore:提供与在线数据库的连接能力。

这些扩展之所以可行,正是得益于存储层抽象:AssetStore接口全部为异步 IO 方法,新后端只需实现同一套接口即可无缝接入AssetManager的多存储回退、依赖解析与复制(copy/deepcopy)机制——deepcopy的"依赖先于顶层写入、已存在则跳过"逻辑(manager.py)已经为"从远程拉取并发布到本地"这样的跨存储同步场景铺好了路。

小结

FreeCAD CAM 工作台的 Asset Manager 模块是一个以"资产"为中心的通用资源管理框架:AssetUri统一寻址与版本化,Asset+AssetSerializer将对象模型与序列化格式解耦,AssetStore抽象出可插拔的存储后端,AssetManager则在其上提供依赖解析(含循环检测与深浅控制)、异步存储与主线程组装、LRU 依赖感知缓存以及跨存储复制发布能力。它在 CAM 工作台中的落地(CamAssetManager的 local/builtin 双存储回退与自动初始化)证明了框架的实用性,也为未来统一资源 UI、GitStore、HttpStore 等扩展保留了清晰的演进路径。

【免费下载链接】FreeCADOfficial source code of FreeCAD, a free and opensource multiplatform 3D parametric modeler.项目地址: https://gitcode.com/GitHub_Trending/fr/FreeCAD

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询