feat: add experimental basic hole feature
This commit is contained in:
@@ -17,7 +17,7 @@
|
|||||||
"Inspection": { "status": "ui-manifest", "level": "unsupported", "commands": ["measure-distance", "measure-angle", "measure-area", "section", "check-dependencies"] }
|
"Inspection": { "status": "ui-manifest", "level": "unsupported", "commands": ["measure-distance", "measure-angle", "measure-area", "section", "check-dependencies"] }
|
||||||
},
|
},
|
||||||
"facadeCapabilities": {
|
"facadeCapabilities": {
|
||||||
"geometry": { "level": "experimental", "provider": "BitBybit OCCT 1.1.1", "operations": ["box", "cylinder", "sphere", "cone", "placement", "union", "cut", "intersection", "part-primitive-recompute", "part-boolean-recompute", "shape-check", "pad", "pocket", "pocket-through-all", "revolution", "fillet", "chamfer", "linear-pattern-whole-shape", "polar-pattern-whole-shape", "feature-shape-cache", "face-edge-vertex-topology", "step-export", "stl-export"] },
|
"geometry": { "level": "experimental", "provider": "BitBybit OCCT 1.1.1", "operations": ["box", "cylinder", "sphere", "cone", "placement", "union", "cut", "intersection", "part-primitive-recompute", "part-boolean-recompute", "shape-check", "pad", "pocket", "pocket-through-all", "revolution", "fillet", "chamfer", "linear-pattern-whole-shape", "polar-pattern-whole-shape", "hole-basic-origin-normal", "feature-shape-cache", "face-edge-vertex-topology", "step-export", "stl-export"] },
|
||||||
"document": { "level": "experimental", "operations": ["typed-properties", "expressions", "units", "dependency-dag", "parallel-level-recompute", "sync-recompute", "async-generation-recompute", "root-cause-diagnostics", "repair-branch-recompute", "undo-redo"] },
|
"document": { "level": "experimental", "operations": ["typed-properties", "expressions", "units", "dependency-dag", "parallel-level-recompute", "sync-recompute", "async-generation-recompute", "root-cause-diagnostics", "repair-branch-recompute", "undo-redo"] },
|
||||||
"sketcher": { "level": "experimental", "operations": ["point-line-circle-arc-model", "basic-constraints", "diameter-symmetric-tangent", "basic-solver", "persistence"] },
|
"sketcher": { "level": "experimental", "operations": ["point-line-circle-arc-model", "basic-constraints", "diameter-symmetric-tangent", "basic-solver", "persistence"] },
|
||||||
"fcstd": { "level": "read-only", "operations": ["zip-preflight", "document-xml-metadata", "proxy-report", "script-isolation"] }
|
"fcstd": { "level": "read-only", "operations": ["zip-preflight", "document-xml-metadata", "proxy-report", "script-isolation"] }
|
||||||
|
|||||||
@@ -334,13 +334,14 @@ Geometry Facade 还接入了 Bitbybit IO 的 STEP 和 ASCII STL 导出,作为
|
|||||||
| P2-04 Project list/load | `project.list()` 暴露 SQLite/内存快照摘要,`app.document.load()` 将选定快照作为活动 Document 事务恢复,清理旧重算/Shape/Task 状态、保留 Undo 边界并按完整对象图异步重算;Projects 页面按 documentId 调用 Facade API | 列表摘要、保存→修改→加载后属性恢复、选择清空、任务清空和加载重算调用通过测试 | FCStd 导入仍是只读 metadata/proxy;跨文档 ShapeHandle 和 Shape 持久化仍待完成 |
|
| P2-04 Project list/load | `project.list()` 暴露 SQLite/内存快照摘要,`app.document.load()` 将选定快照作为活动 Document 事务恢复,清理旧重算/Shape/Task 状态、保留 Undo 边界并按完整对象图异步重算;Projects 页面按 documentId 调用 Facade API | 列表摘要、保存→修改→加载后属性恢复、选择清空、任务清空和加载重算调用通过测试 | FCStd 导入仍是只读 metadata/proxy;跨文档 ShapeHandle 和 Shape 持久化仍待完成 |
|
||||||
| P2-03 Resource/recovery fallback | SQLite Worker 在无 OPFS 的浏览器中使用内存资源表,保留 SHA-256 身份和引用计数;恢复报告区分数据库完整但目标文档缺失的 `unavailable`;销毁 Worker 时清空临时资源 | Worker 类型检查、生产构建通过 | 内存资源会随刷新丢失;配额回收、持久资源校验和大文件压力仍待实现 |
|
| P2-03 Resource/recovery fallback | SQLite Worker 在无 OPFS 的浏览器中使用内存资源表,保留 SHA-256 身份和引用计数;恢复报告区分数据库完整但目标文档缺失的 `unavailable`;销毁 Worker 时清空临时资源 | Worker 类型检查、生产构建通过 | 内存资源会随刷新丢失;配额回收、持久资源校验和大文件压力仍待实现 |
|
||||||
| PART-07 Shape check | `check-shape` 使用 Facade `geometry.topology()` 生成面/边/顶点数量和 `SHAPE_CHECK_PASSED` 信息诊断;空缓存、空拓扑和 Worker 错误分别返回结构化失败诊断 | 无 Shape、错误选择前置和拓扑调用边界通过测试/构建 | 当前是网格派生拓扑检查,不等价于 OCCT `BRepCheck_Analyzer`,完整修复建议和自交诊断仍待实现 |
|
| PART-07 Shape check | `check-shape` 使用 Facade `geometry.topology()` 生成面/边/顶点数量和 `SHAPE_CHECK_PASSED` 信息诊断;空缓存、空拓扑和 Worker 错误分别返回结构化失败诊断 | 无 Shape、错误选择前置和拓扑调用边界通过测试/构建 | 当前是网格派生拓扑检查,不等价于 OCCT `BRepCheck_Analyzer`,完整修复建议和自交诊断仍待实现 |
|
||||||
| P1-02 命令准确性 | manifest 仍可完整展示;Part `primitive/union/cut/intersection` 与 Part Design 核心命令已有 Facade executor;命令状态同时检查工作台和对象类型选择谓词;其余未接入 executor 的命令返回 disabled 与 `COMMAND_UNIMPLEMENTED`,不再静默完成 | Part 基本体/布尔 Shape 缓存、选择不兼容时的禁用原因、`hole` 未实现状态与诊断测试通过 | 其余工作台要逐项增加真实 executor、Task 参数和黄金回放后才能启用 |
|
| P1-02 命令准确性 | manifest 仍可完整展示;Part `primitive/union/cut/intersection` 与 Part Design 核心命令已有 Facade executor;命令状态同时检查工作台和对象类型选择谓词;其余未接入 executor 的命令返回 disabled 与 `COMMAND_UNIMPLEMENTED`,不再静默完成 | Part 基本体/布尔 Shape 缓存、选择不兼容时的禁用原因、`mirrored` 未实现状态与诊断测试通过 | 其余工作台要逐项增加真实 executor、Task 参数和黄金回放后才能启用 |
|
||||||
| P3-05 Pattern 实验切片 | `linear-pattern` 和 `polar-pattern` 通过 Part Design 选择谓词、Task 和类型化属性创建对应对象;Linear 按总 Length 等距平移,Polar 在小于 360° 时覆盖首末角、360° 时不复制重合端点;两者均由 OCCT union 合并并释放临时副本 | 线性间距/方向、圆周完整/部分角度分布、联合输入、句柄释放、非法参数诊断、Base 链接、Body.Tip、属性校验和任务提交回归测试通过 | 当前为 FreeCAD 1.0+ 的 Transform body 近似,只复制整个 Base Shape;未实现 Transform tool shapes、连接单实体过滤、稳定 TopoRef、历史映射、反向轴和引用轴,因此保持 `experimental`;Hole 仍待实现 |
|
| P3-05 Pattern 实验切片 | `linear-pattern` 和 `polar-pattern` 通过 Part Design 选择谓词、Task 和类型化属性创建对应对象;Linear 按总 Length 等距平移,Polar 在小于 360° 时覆盖首末角、360° 时不复制重合端点;两者均由 OCCT union 合并并释放临时副本 | 线性间距/方向、圆周完整/部分角度分布、联合输入、句柄释放、非法参数诊断、Base 链接、Body.Tip、属性校验和任务提交回归测试通过 | 当前为 FreeCAD 1.0+ 的 Transform body 近似,只复制整个 Base Shape;未实现 Transform tool shapes、连接单实体过滤、稳定 TopoRef、历史映射、反向轴和引用轴,因此保持 `experimental` |
|
||||||
|
| P3-05 Hole 基础切片 | `hole` 创建 `PartDesign::Hole`,用 32 边圆形 Profile 调用现有 OCCT Pocket;支持 Diameter、Depth 以及 Dimension/Through all,校验正尺寸并纳入 Base 链接和 Body.Tip | 圆孔 Profile、有限深度调用、非法直径诊断、Task 属性提交和 Tip 更新测试通过 | 仅为基体原点 XY 面、+Z 法向的直孔;草图圆/面定位、沉孔、锪孔、钻尖、螺纹、尺寸标准和稳定支撑面引用仍待实现,保持 `experimental` |
|
||||||
|
|
||||||
后续按以下顺序连续开发:
|
后续按以下顺序连续开发:
|
||||||
|
|
||||||
1. `P4-06/DAG-08`:同步/异步重算已共享抑制传播和根因诊断语义,并支持定位、解除错误后的最小闭包重算与合法特征抑制;继续补齐多配置抑制、Body Tip 重定向、最近有效 Shape 显式回滚和性能计数器。Shape 缓存已进入 Facade,但尚未持久化。
|
1. `P4-06/DAG-08`:同步/异步重算已共享抑制传播和根因诊断语义,并支持定位、解除错误后的最小闭包重算与合法特征抑制;继续补齐多配置抑制、Body Tip 重定向、最近有效 Shape 显式回滚和性能计数器。Shape 缓存已进入 Facade,但尚未持久化。
|
||||||
2. `P3-04/P3-05`:Pad/Pocket/Revolution/Fillet/Chamfer 已有 OCCT Worker 输入适配和 Shape 缓存回写,Pocket Through all 已接入;Linear/Polar Pattern 已完成整 Shape 实验切片;继续补齐 Up to face、局部特征/拓扑历史和 Hole,未完成能力保持 disabled 或 experimental。
|
2. `P3-04/P3-05`:Pad/Pocket/Revolution/Fillet/Chamfer 已有 OCCT Worker 输入适配和 Shape 缓存回写,Pocket Through all 已接入;Linear/Polar Pattern 与原点法向直孔已完成实验切片;继续补齐 Up to face、局部特征/拓扑历史及 Hole 高级形态,未完成能力保持 disabled 或 experimental。
|
||||||
3. `TSN-03/05/07/08`:补齐 edge/vertex 签名、OCCT Generated/Modified/Deleted 历史、跨特征迁移和歧义处理;任何 transient index 不得写入持久引用。
|
3. `TSN-03/05/07/08`:补齐 edge/vertex 签名、OCCT Generated/Modified/Deleted 历史、跨特征迁移和歧义处理;任何 transient index 不得写入持久引用。
|
||||||
4. `SK-03/SK-06/SK-07`:编译和验证 planegcs WASM,按基础/高级约束建立 FreeCAD 黄金回放;基础 solver 维持 experimental。
|
4. `SK-03/SK-06/SK-07`:编译和验证 planegcs WASM,按基础/高级约束建立 FreeCAD 黄金回放;基础 solver 维持 experimental。
|
||||||
5. `P2-04/P2-05/P2-06`:完成保存点恢复报告、迁移回滚、配额回收和 BroadcastChannel 单写者冲突。
|
5. `P2-04/P2-05/P2-06`:完成保存点恢复报告、迁移回滚、配额回收和 BroadcastChannel 单写者冲突。
|
||||||
|
|||||||
@@ -1603,7 +1603,7 @@ P0 基线/治理
|
|||||||
| P3-02 几何句柄与网格协议 | `IN PROGRESS` | Facade 已定义并实现受控 `ShapeHandle`、`MeshAsset`、`SubshapeRef` 类型;句柄携带 documentId/version,底层 OCCT hash 不出运行时,跨文档、字段篡改和释放后访问被拒绝;`geometry.topology()` 已从 BitBybit 网格生成面/边/点基础签名,OCCT 历史命名仍未实现 |
|
| P3-02 几何句柄与网格协议 | `IN PROGRESS` | Facade 已定义并实现受控 `ShapeHandle`、`MeshAsset`、`SubshapeRef` 类型;句柄携带 documentId/version,底层 OCCT hash 不出运行时,跨文档、字段篡改和释放后访问被拒绝;`geometry.topology()` 已从 BitBybit 网格生成面/边/点基础签名,OCCT 历史命名仍未实现 |
|
||||||
| P3-03 基本体和变换 | `IN PROGRESS` | 真实 OCCT Box/Cylinder/Sphere/Cone 和 FreeCAD 风格 Placement 已通过浏览器矩阵;0.05 mm 网格精度下曲面包围盒最大离散误差约 0.023 mm;自动化浏览器黄金测试尚未进入 CI |
|
| P3-03 基本体和变换 | `IN PROGRESS` | 真实 OCCT Box/Cylinder/Sphere/Cone 和 FreeCAD 风格 Placement 已通过浏览器矩阵;0.05 mm 网格精度下曲面包围盒最大离散误差约 0.023 mm;自动化浏览器黄金测试尚未进入 CI |
|
||||||
| P3-04 Boolean/Pad/Pocket/Revolution | `IN PROGRESS` | 文档作用域 Union/Cut/Intersection 与 PlanarProfile 驱动的 Pad/Pocket/Revolution 已通过真实 OCCT 浏览器矩阵;重算 executor 已在 Worker ready 时执行 Pad/Pocket/Revolution,Revolution Angle/Reversed 已接入,Fillet/Chamfer 也有 Shape 缓存回写;Up to face、FCStd Shape 持久化仍待完成 |
|
| P3-04 Boolean/Pad/Pocket/Revolution | `IN PROGRESS` | 文档作用域 Union/Cut/Intersection 与 PlanarProfile 驱动的 Pad/Pocket/Revolution 已通过真实 OCCT 浏览器矩阵;重算 executor 已在 Worker ready 时执行 Pad/Pocket/Revolution,Revolution Angle/Reversed 已接入,Fillet/Chamfer 也有 Shape 缓存回写;Up to face、FCStd Shape 持久化仍待完成 |
|
||||||
| P3-05 Fillet/Chamfer/Pattern/Hole | `IN PROGRESS` | Fillet/Chamfer 已接入 Shape 缓存;Linear/Polar Pattern 已贯通选择谓词、Task、`App::PropertyInteger`、Base/Body.Tip、Placement 副本、OCCT union、临时句柄释放和结构化输入诊断;Polar 的 360° 与部分角度间距分别遵循不重合端点和首末覆盖规则。当前为整 Shape 实验语义;局部特征历史、连接单实体过滤、稳定 TopoRef 与 Hole 仍待实现 |
|
| P3-05 Fillet/Chamfer/Pattern/Hole | `IN PROGRESS` | Fillet/Chamfer 已接入 Shape 缓存;Linear/Polar Pattern 已贯通选择谓词、Task、Placement/union 和清理;基础 Hole 用圆形 Profile 复用 Pocket,支持 Diameter、Depth、Dimension/Through all。Pattern 当前为整 Shape,Hole 当前固定原点法向;局部特征历史、连接单实体过滤、稳定 TopoRef、草图/面定位及沉孔/锪孔/螺纹仍待实现 |
|
||||||
| P5-02 网格增量接入 | `IN PROGRESS` | Three Adapter 可用 `BufferGeometry` 接收 Facade `MeshAsset`,替换时释放旧 GPU geometry;视口优先使用重计算缓存的对象 Shape,失败时保留最近有效结果,无缓存时才创建并释放 Pad/Pocket 临时预览链;对象级增量缓存与选择映射尚未实现 |
|
| P5-02 网格增量接入 | `IN PROGRESS` | Three Adapter 可用 `BufferGeometry` 接收 Facade `MeshAsset`,替换时释放旧 GPU geometry;视口优先使用重计算缓存的对象 Shape,失败时保留最近有效结果,无缓存时才创建并释放 Pad/Pocket 临时预览链;对象级增量缓存与选择映射尚未实现 |
|
||||||
| P8-01 第一批自动化场景 | `IN PROGRESS` | `tests/facade.test.ts` 已覆盖 47 个场景,包括 Part 命令、基本体/布尔重算、async generation 重算、DAG 分层并行、抑制传播/恢复、根因诊断/修复、OCCT 特征执行器、FCStd 安全检查、schema、串行/跨标签持久化、资源引用、事件、任务、禁用/未实现命令、历史和表达式;E2E/黄金几何待补齐 |
|
| P8-01 第一批自动化场景 | `IN PROGRESS` | `tests/facade.test.ts` 已覆盖 47 个场景,包括 Part 命令、基本体/布尔重算、async generation 重算、DAG 分层并行、抑制传播/恢复、根因诊断/修复、OCCT 特征执行器、FCStd 安全检查、schema、串行/跨标签持久化、资源引用、事件、任务、禁用/未实现命令、历史和表达式;E2E/黄金几何待补齐 |
|
||||||
|
|
||||||
@@ -1698,7 +1698,7 @@ DocumentSnapshot 现在区分模型树投影与 `DocumentObjectSnapshot` 真值
|
|||||||
| 类型控件 | `PASS` | View 标签由 metadata 生成 1 个 checkbox、2 个 enumeration、2 个 numeric、2 个 color 控件 |
|
| 类型控件 | `PASS` | View 标签由 metadata 生成 1 个 checkbox、2 个 enumeration、2 个 numeric、2 个 color 控件 |
|
||||||
| Part primitive Task | `PASS` | Part 工作台任务可选择 Box/Cylinder/Sphere/Cone,尺寸和角度草稿写入 `Part::*` 对象属性并进入 OCCT 重算 |
|
| Part primitive Task | `PASS` | Part 工作台任务可选择 Box/Cylinder/Sphere/Cone,尺寸和角度草稿写入 `Part::*` 对象属性并进入 OCCT 重算 |
|
||||||
| Part Boolean Task | `PASS` | Union/Cut/Intersection 任务提供 Base/Tool 对象选择槽,链接写入对象依赖图;确认后将新对象及其依赖闭包标记 dirty 并触发异步重算 |
|
| Part Boolean Task | `PASS` | Union/Cut/Intersection 任务提供 Base/Tool 对象选择槽,链接写入对象依赖图;确认后将新对象及其依赖闭包标记 dirty 并触发异步重算 |
|
||||||
| Part Design Task | `PASS` | Pad/Pocket/Revolution/Fillet/Chamfer 任务草稿写入 Length/Type/Angle/Reversed/Midplane/Radius/Distance;Linear/Polar Pattern 写入 Base、Occurrences 和各自 Length/Direction 或 Angle/Axis,确认后仍通过 Facade 历史边界提交并更新 Body.Tip |
|
| Part Design Task | `PASS` | Pad/Pocket/Revolution/Fillet/Chamfer 任务草稿写入核心参数;Linear/Polar Pattern 写入 Base、Occurrences、Length/Direction 或 Angle/Axis;Hole 写入 Base、Diameter、Depth、Type;确认后均通过 Facade 历史边界提交并更新 Body.Tip |
|
||||||
| 选择谓词 | `PASS` | 选择 Origin/文件夹后 Part Union/Check geometry 被禁用;选择非 Sketch 对象后 Sketcher Solve 被禁用,并返回可解释原因 |
|
| 选择谓词 | `PASS` | 选择 Origin/文件夹后 Part Union/Check geometry 被禁用;选择非 Sketch 对象后 Sketcher Solve 被禁用,并返回可解释原因 |
|
||||||
| 校验 | `PASS` | 负 Length、越界 Percent、未知 Enumeration、无效颜色、缺失 Link、只读 Property 在 Facade 拒绝 |
|
| 校验 | `PASS` | 负 Length、越界 Percent、未知 Enumeration、无效颜色、缺失 Link、只读 Property 在 Facade 拒绝 |
|
||||||
| Undo/Redo | `PASS` | Length 42→50 可撤销回 42、重做至 50,Property 和树投影同步 |
|
| Undo/Redo | `PASS` | Length 42→50 可撤销回 42、重做至 50,Property 和树投影同步 |
|
||||||
|
|||||||
@@ -364,7 +364,7 @@ function TaskPanel({ workbench, facade, showNotice }: { workbench: Workbench; fa
|
|||||||
const activeCommand = activeTask?.commandId
|
const activeCommand = activeTask?.commandId
|
||||||
const activeDocument = facade.app.document.getActive()
|
const activeDocument = facade.app.document.getActive()
|
||||||
const booleanCommand = activeCommand === 'union' || activeCommand === 'cut' || activeCommand === 'intersection'
|
const booleanCommand = activeCommand === 'union' || activeCommand === 'cut' || activeCommand === 'intersection'
|
||||||
const shapeObjects = activeDocument.objects.filter((object) => object.typeId.startsWith('Part::') || ['PartDesign::Feature', 'PartDesign::Pad', 'PartDesign::Pocket', 'PartDesign::Revolution', 'PartDesign::Fillet', 'PartDesign::Chamfer', 'PartDesign::LinearPattern', 'PartDesign::PolarPattern'].includes(object.typeId))
|
const shapeObjects = activeDocument.objects.filter((object) => object.typeId.startsWith('Part::') || ['PartDesign::Feature', 'PartDesign::Pad', 'PartDesign::Pocket', 'PartDesign::Revolution', 'PartDesign::Fillet', 'PartDesign::Chamfer', 'PartDesign::LinearPattern', 'PartDesign::PolarPattern', 'PartDesign::Hole'].includes(object.typeId))
|
||||||
const draftLink = (name: string) => typeof activeTask?.draft[name] === 'string' ? String(activeTask.draft[name]) : ''
|
const draftLink = (name: string) => typeof activeTask?.draft[name] === 'string' ? String(activeTask.draft[name]) : ''
|
||||||
const acceptTask = () => {
|
const acceptTask = () => {
|
||||||
if (!activeTask || activeTask.status !== 'preview') { showNotice('No active task'); return }
|
if (!activeTask || activeTask.status !== 'preview') { showNotice('No active task'); return }
|
||||||
@@ -403,6 +403,11 @@ function TaskPanel({ workbench, facade, showNotice }: { workbench: Workbench; fa
|
|||||||
<label className="field-label">Angle <span className="field-unit">deg</span><input className="field-input" type="number" min={0.001} max={360} step={1} value={typeof activeTask?.draft.angle === 'number' ? Number(activeTask.draft.angle) : 360} onChange={(event) => facade.task.update({ angle: Number(event.target.value) })} /></label>
|
<label className="field-label">Angle <span className="field-unit">deg</span><input className="field-input" type="number" min={0.001} max={360} step={1} value={typeof activeTask?.draft.angle === 'number' ? Number(activeTask.draft.angle) : 360} onChange={(event) => facade.task.update({ angle: Number(event.target.value) })} /></label>
|
||||||
<label className="field-label">Axis<select className="field-input" value={typeof activeTask?.draft.axis === 'string' ? activeTask.draft.axis : 'Normal'} onChange={(event) => facade.task.update({ axis: event.target.value })}><option>Normal</option><option>Horizontal</option><option>Vertical</option></select></label>
|
<label className="field-label">Axis<select className="field-input" value={typeof activeTask?.draft.axis === 'string' ? activeTask.draft.axis : 'Normal'} onChange={(event) => facade.task.update({ axis: event.target.value })}><option>Normal</option><option>Horizontal</option><option>Vertical</option></select></label>
|
||||||
</>}
|
</>}
|
||||||
|
{activeCommand === 'hole' && <>
|
||||||
|
<label className="field-label">Diameter <span className="field-unit">mm</span><input className="field-input" type="number" min={0.001} step={0.1} value={typeof activeTask?.draft.diameter === 'number' ? Number(activeTask.draft.diameter) : 5} onChange={(event) => facade.task.update({ diameter: Number(event.target.value) })} /></label>
|
||||||
|
<label className="field-label">Depth <span className="field-unit">mm</span><input className="field-input" type="number" min={0.001} step={0.1} value={typeof activeTask?.draft.depth === 'number' ? Number(activeTask.draft.depth) : 10} onChange={(event) => facade.task.update({ depth: Number(event.target.value) })} /></label>
|
||||||
|
<label className="field-label">Type<select className="field-input" value={typeof activeTask?.draft.type === 'string' ? activeTask.draft.type : 'Dimension'} onChange={(event) => facade.task.update({ type: event.target.value })}><option>Dimension</option><option>Through all</option></select></label>
|
||||||
|
</>}
|
||||||
{activeCommand === 'pocket' && <label className="field-label">Type<select className="field-input" value={typeof activeTask?.draft.type === 'string' ? activeTask.draft.type : 'Through all'} onChange={(event) => facade.task.update({ type: event.target.value })}><option>Dimension</option><option>Through all</option><option>Up to face</option></select></label>}
|
{activeCommand === 'pocket' && <label className="field-label">Type<select className="field-input" value={typeof activeTask?.draft.type === 'string' ? activeTask.draft.type : 'Through all'} onChange={(event) => facade.task.update({ type: event.target.value })}><option>Dimension</option><option>Through all</option><option>Up to face</option></select></label>}
|
||||||
{['pad', 'pocket', 'revolution'].includes(activeCommand || '') && <label className="check-row"><input type="checkbox" checked={activeTask?.draft.reversed === true} onChange={(event) => facade.task.update({ reversed: event.target.checked })} /><span>Reversed</span></label>}
|
{['pad', 'pocket', 'revolution'].includes(activeCommand || '') && <label className="check-row"><input type="checkbox" checked={activeTask?.draft.reversed === true} onChange={(event) => facade.task.update({ reversed: event.target.checked })} /><span>Reversed</span></label>}
|
||||||
{activeCommand === 'pad' && <label className="check-row"><input type="checkbox" checked={activeTask?.draft.midplane === true} onChange={(event) => facade.task.update({ midplane: event.target.checked })} /><span>Symmetric to plane</span></label>}
|
{activeCommand === 'pad' && <label className="check-row"><input type="checkbox" checked={activeTask?.draft.midplane === true} onChange={(event) => facade.task.update({ midplane: event.target.checked })} /><span>Symmetric to plane</span></label>}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const initialTree: ModelTreeItem[] = [
|
|||||||
{ id: 'reference', label: 'Reference geometry', type: 'folder', children: ['DatumPlane', 'DatumAxis'] },
|
{ id: 'reference', label: 'Reference geometry', type: 'folder', children: ['DatumPlane', 'DatumAxis'] },
|
||||||
]
|
]
|
||||||
|
|
||||||
const typeIdForItem = (item: ModelTreeItem) => item.type === 'body' ? 'PartDesign::Body' : item.type === 'sketch' ? 'Sketcher::SketchObject' : item.id.startsWith('box') ? 'Part::Box' : item.id.startsWith('cylinder') ? 'Part::Cylinder' : item.id.startsWith('sphere') ? 'Part::Sphere' : item.id.startsWith('cone') ? 'Part::Cone' : item.id.startsWith('union') ? 'Part::Fuse' : item.id.startsWith('cut') ? 'Part::Cut' : item.id.startsWith('intersection') ? 'Part::Common' : item.id.startsWith('pad') ? 'PartDesign::Pad' : item.id.startsWith('pocket') ? 'PartDesign::Pocket' : item.id.startsWith('revolution') ? 'PartDesign::Revolution' : item.id.startsWith('fillet') ? 'PartDesign::Fillet' : item.id.startsWith('chamfer') ? 'PartDesign::Chamfer' : item.id.startsWith('linear-pattern') ? 'PartDesign::LinearPattern' : item.id.startsWith('polar-pattern') ? 'PartDesign::PolarPattern' : item.type === 'feature' ? 'PartDesign::Feature' : 'App::DocumentObjectGroup'
|
const typeIdForItem = (item: ModelTreeItem) => item.type === 'body' ? 'PartDesign::Body' : item.type === 'sketch' ? 'Sketcher::SketchObject' : item.id.startsWith('box') ? 'Part::Box' : item.id.startsWith('cylinder') ? 'Part::Cylinder' : item.id.startsWith('sphere') ? 'Part::Sphere' : item.id.startsWith('cone') ? 'Part::Cone' : item.id.startsWith('union') ? 'Part::Fuse' : item.id.startsWith('cut') ? 'Part::Cut' : item.id.startsWith('intersection') ? 'Part::Common' : item.id.startsWith('pad') ? 'PartDesign::Pad' : item.id.startsWith('pocket') ? 'PartDesign::Pocket' : item.id.startsWith('revolution') ? 'PartDesign::Revolution' : item.id.startsWith('fillet') ? 'PartDesign::Fillet' : item.id.startsWith('chamfer') ? 'PartDesign::Chamfer' : item.id.startsWith('linear-pattern') ? 'PartDesign::LinearPattern' : item.id.startsWith('polar-pattern') ? 'PartDesign::PolarPattern' : item.id.startsWith('hole') ? 'PartDesign::Hole' : item.type === 'feature' ? 'PartDesign::Feature' : 'App::DocumentObjectGroup'
|
||||||
|
|
||||||
const commonProperties = (item: ModelTreeItem): ObjectPropertySnapshot[] => [
|
const commonProperties = (item: ModelTreeItem): ObjectPropertySnapshot[] => [
|
||||||
{ name: 'Label', label: 'Label', group: 'Identity', scope: 'data', type: 'App::PropertyString', value: item.label },
|
{ name: 'Label', label: 'Label', group: 'Identity', scope: 'data', type: 'App::PropertyString', value: item.label },
|
||||||
@@ -123,6 +123,12 @@ const featureProperties = (item: ModelTreeItem): ObjectPropertySnapshot[] => {
|
|||||||
{ name: 'Angle', label: 'Angle', group: 'Pattern', scope: 'data', type: 'App::PropertyAngle', value: 360, unit: 'deg', recompute: true },
|
{ name: 'Angle', label: 'Angle', group: 'Pattern', scope: 'data', type: 'App::PropertyAngle', value: 360, unit: 'deg', recompute: true },
|
||||||
{ name: 'Axis', label: 'Axis', group: 'Pattern', scope: 'data', type: 'App::PropertyEnumeration', value: 'Normal', options: ['Normal', 'Horizontal', 'Vertical'], recompute: true },
|
{ name: 'Axis', label: 'Axis', group: 'Pattern', scope: 'data', type: 'App::PropertyEnumeration', value: 'Normal', options: ['Normal', 'Horizontal', 'Vertical'], recompute: true },
|
||||||
]
|
]
|
||||||
|
if (item.id.startsWith('hole')) return [
|
||||||
|
{ name: 'Base', label: 'Base', group: 'Hole', scope: 'data', type: 'App::PropertyLink', value: null, recompute: true },
|
||||||
|
{ name: 'Diameter', label: 'Diameter', group: 'Hole', scope: 'data', type: 'App::PropertyLength', value: 5, unit: 'mm', recompute: true },
|
||||||
|
{ name: 'Depth', label: 'Depth', group: 'Hole', scope: 'data', type: 'App::PropertyLength', value: 10, unit: 'mm', recompute: true },
|
||||||
|
{ name: 'Type', label: 'Type', group: 'Hole', scope: 'data', type: 'App::PropertyEnumeration', value: 'Dimension', options: ['Dimension', 'Through all'], recompute: true },
|
||||||
|
]
|
||||||
if (item.type === 'sketch') return [
|
if (item.type === 'sketch') return [
|
||||||
{ name: 'Support', label: 'Support', group: 'Attachment', scope: 'data', type: 'App::PropertyLink', value: 'XY_Plane', recompute: true },
|
{ name: 'Support', label: 'Support', group: 'Attachment', scope: 'data', type: 'App::PropertyLink', value: 'XY_Plane', recompute: true },
|
||||||
{ name: 'ConstraintStatus', label: 'Solver state', group: 'Constraints', scope: 'data', type: 'App::PropertyString', value: 'Fully constrained', readOnly: true },
|
{ name: 'ConstraintStatus', label: 'Solver state', group: 'Constraints', scope: 'data', type: 'App::PropertyString', value: 'Fully constrained', readOnly: true },
|
||||||
@@ -181,10 +187,10 @@ const createDocument = (label = 'Pump Housing'): DocumentSnapshot => {
|
|||||||
|
|
||||||
const selectionRequired = new Set(['pad', 'pocket', 'revolution', 'fillet', 'chamfer', 'union', 'cut', 'intersection', 'check-shape', 'hole', 'linear-pattern', 'polar-pattern', 'measure-distance', 'measure-angle', 'measure-area', 'solve-sketch'])
|
const selectionRequired = new Set(['pad', 'pocket', 'revolution', 'fillet', 'chamfer', 'union', 'cut', 'intersection', 'check-shape', 'hole', 'linear-pattern', 'polar-pattern', 'measure-distance', 'measure-angle', 'measure-area', 'solve-sketch'])
|
||||||
const systemCommands = new Set(['new-document', 'save', 'select-object'])
|
const systemCommands = new Set(['new-document', 'save', 'select-object'])
|
||||||
const implementedCommandIds = new Set(['new-document', 'save', 'select-object', 'create-body', 'create-sketch', 'new-sketch', 'pad', 'pocket', 'revolution', 'fillet', 'chamfer', 'linear-pattern', 'polar-pattern', 'primitive', 'union', 'cut', 'intersection', 'check-shape', 'solve-sketch'])
|
const implementedCommandIds = new Set(['new-document', 'save', 'select-object', 'create-body', 'create-sketch', 'new-sketch', 'pad', 'pocket', 'revolution', 'fillet', 'chamfer', 'linear-pattern', 'polar-pattern', 'hole', 'primitive', 'union', 'cut', 'intersection', 'check-shape', 'solve-sketch'])
|
||||||
const partDesignCommands = new Set(['create-body', 'create-sketch', 'pad', 'pocket', 'revolution', 'fillet', 'chamfer', 'linear-pattern', 'polar-pattern'])
|
const partDesignCommands = new Set(['create-body', 'create-sketch', 'pad', 'pocket', 'revolution', 'fillet', 'chamfer', 'linear-pattern', 'polar-pattern', 'hole'])
|
||||||
const partCommands = new Set(['primitive', 'union', 'cut', 'intersection', 'check-shape'])
|
const partCommands = new Set(['primitive', 'union', 'cut', 'intersection', 'check-shape'])
|
||||||
const shapeSelectionCommands = new Set(['union', 'cut', 'intersection', 'check-shape', 'fillet', 'chamfer', 'linear-pattern', 'polar-pattern'])
|
const shapeSelectionCommands = new Set(['union', 'cut', 'intersection', 'check-shape', 'fillet', 'chamfer', 'linear-pattern', 'polar-pattern', 'hole'])
|
||||||
const featureSelectionCommands = new Set(['pad', 'pocket', 'revolution'])
|
const featureSelectionCommands = new Set(['pad', 'pocket', 'revolution'])
|
||||||
const shapeTypeIds = new Set([
|
const shapeTypeIds = new Set([
|
||||||
'Part::Box',
|
'Part::Box',
|
||||||
@@ -203,6 +209,7 @@ const shapeTypeIds = new Set([
|
|||||||
'PartDesign::Chamfer',
|
'PartDesign::Chamfer',
|
||||||
'PartDesign::LinearPattern',
|
'PartDesign::LinearPattern',
|
||||||
'PartDesign::PolarPattern',
|
'PartDesign::PolarPattern',
|
||||||
|
'PartDesign::Hole',
|
||||||
])
|
])
|
||||||
const featureCommands: Record<string, { label: string; detail: string }> = {
|
const featureCommands: Record<string, { label: string; detail: string }> = {
|
||||||
'create-body': { label: 'Body', detail: 'Part Design body' },
|
'create-body': { label: 'Body', detail: 'Part Design body' },
|
||||||
@@ -214,6 +221,7 @@ const featureCommands: Record<string, { label: string; detail: string }> = {
|
|||||||
chamfer: { label: 'Chamfer', detail: 'Length 2 mm' },
|
chamfer: { label: 'Chamfer', detail: 'Length 2 mm' },
|
||||||
'linear-pattern': { label: 'Linear Pattern', detail: '2 occurrences over 20 mm' },
|
'linear-pattern': { label: 'Linear Pattern', detail: '2 occurrences over 20 mm' },
|
||||||
'polar-pattern': { label: 'Polar Pattern', detail: '3 occurrences over 360 deg' },
|
'polar-pattern': { label: 'Polar Pattern', detail: '3 occurrences over 360 deg' },
|
||||||
|
hole: { label: 'Hole', detail: 'Simple 5 mm diameter hole' },
|
||||||
primitive: { label: 'Box', detail: '10 × 10 × 10 mm' },
|
primitive: { label: 'Box', detail: '10 × 10 × 10 mm' },
|
||||||
union: { label: 'Union', detail: 'Boolean fuse' },
|
union: { label: 'Union', detail: 'Boolean fuse' },
|
||||||
cut: { label: 'Cut', detail: 'Boolean difference' },
|
cut: { label: 'Cut', detail: 'Boolean difference' },
|
||||||
@@ -371,6 +379,10 @@ export function createMockFacade(): BitBybitWebCadFacade {
|
|||||||
for (const property of objectSnapshot.properties.filter((candidate) => candidate.recompute && !candidate.readOnly)) validatePropertyValue(document, property, property.value)
|
for (const property of objectSnapshot.properties.filter((candidate) => candidate.recompute && !candidate.readOnly)) validatePropertyValue(document, property, property.value)
|
||||||
if (commandId === 'linear-pattern' && Number(objectSnapshot.properties.find((property) => property.name === 'Length')?.value) <= 0) throw new RangeError('Linear pattern length must be greater than zero.')
|
if (commandId === 'linear-pattern' && Number(objectSnapshot.properties.find((property) => property.name === 'Length')?.value) <= 0) throw new RangeError('Linear pattern length must be greater than zero.')
|
||||||
if (commandId === 'polar-pattern' && Number(objectSnapshot.properties.find((property) => property.name === 'Angle')?.value) <= 0) throw new RangeError('Polar pattern angle must be greater than zero.')
|
if (commandId === 'polar-pattern' && Number(objectSnapshot.properties.find((property) => property.name === 'Angle')?.value) <= 0) throw new RangeError('Polar pattern angle must be greater than zero.')
|
||||||
|
if (commandId === 'hole') {
|
||||||
|
if (Number(objectSnapshot.properties.find((property) => property.name === 'Diameter')?.value) <= 0) throw new RangeError('Hole diameter must be greater than zero.')
|
||||||
|
if (Number(objectSnapshot.properties.find((property) => property.name === 'Depth')?.value) <= 0) throw new RangeError('Hole depth must be greater than zero.')
|
||||||
|
}
|
||||||
const objects = [...document.objects.map((object) => ({ ...object, properties: object.properties.map((property) => ({ ...property })), sketch: object.sketch ? cloneSketch(object.sketch) : undefined })), objectSnapshot]
|
const objects = [...document.objects.map((object) => ({ ...object, properties: object.properties.map((property) => ({ ...property })), sketch: object.sketch ? cloneSketch(object.sketch) : undefined })), objectSnapshot]
|
||||||
const tipObject = type === 'feature' && partDesignCommands.has(commandId) && commandId !== 'create-sketch' ? objects.find((object) => object.id === 'body') : undefined
|
const tipObject = type === 'feature' && partDesignCommands.has(commandId) && commandId !== 'create-sketch' ? objects.find((object) => object.id === 'body') : undefined
|
||||||
if (tipObject) {
|
if (tipObject) {
|
||||||
|
|||||||
@@ -302,7 +302,7 @@ export const createFacadeGeometryRecomputeExecutor = (
|
|||||||
return base
|
return base
|
||||||
}
|
}
|
||||||
if (base.status === 'failed' || object.sketch || geometry.capabilities().status !== 'ready') return base
|
if (base.status === 'failed' || object.sketch || geometry.capabilities().status !== 'ready') return base
|
||||||
if (!['Part::Box', 'Part::Cylinder', 'Part::Sphere', 'Part::Cone', 'Part::Fuse', 'Part::Cut', 'Part::Common', 'PartDesign::Pad', 'PartDesign::Pocket', 'PartDesign::Revolution', 'PartDesign::Fillet', 'PartDesign::Chamfer', 'PartDesign::LinearPattern', 'PartDesign::PolarPattern'].includes(object.typeId)) return base
|
if (!['Part::Box', 'Part::Cylinder', 'Part::Sphere', 'Part::Cone', 'Part::Fuse', 'Part::Cut', 'Part::Common', 'PartDesign::Pad', 'PartDesign::Pocket', 'PartDesign::Revolution', 'PartDesign::Fillet', 'PartDesign::Chamfer', 'PartDesign::LinearPattern', 'PartDesign::PolarPattern', 'PartDesign::Hole'].includes(object.typeId)) return base
|
||||||
|
|
||||||
const requiresProfile = object.typeId === 'PartDesign::Pad' || object.typeId === 'PartDesign::Pocket' || object.typeId === 'PartDesign::Revolution'
|
const requiresProfile = object.typeId === 'PartDesign::Pad' || object.typeId === 'PartDesign::Pocket' || object.typeId === 'PartDesign::Revolution'
|
||||||
const profileObject = requiresProfile ? linkedObject(object, 'Profile', document) : undefined
|
const profileObject = requiresProfile ? linkedObject(object, 'Profile', document) : undefined
|
||||||
@@ -396,6 +396,20 @@ export const createFacadeGeometryRecomputeExecutor = (
|
|||||||
} finally {
|
} finally {
|
||||||
await Promise.allSettled(copies.map((copy) => geometry.release(copy)))
|
await Promise.allSettled(copies.map((copy) => geometry.release(copy)))
|
||||||
}
|
}
|
||||||
|
} else if (object.typeId === 'PartDesign::Hole') {
|
||||||
|
const baseObject = linkedObject(object, 'Base', document)
|
||||||
|
const baseShape = baseObject ? shapes.get(baseObject.id) : undefined
|
||||||
|
if (!baseShape) return geometryFailure(object.id, 'BASE_SHAPE_MISSING', 'Hole base has no valid recomputed Shape.')
|
||||||
|
const diameter = numberProperty('Diameter', 5)
|
||||||
|
if (!(diameter > 0)) return geometryFailure(object.id, 'HOLE_DIAMETER_INVALID', 'Hole diameter must be greater than zero.')
|
||||||
|
const depth = numberProperty('Depth', 10)
|
||||||
|
if (!(depth > 0)) return geometryFailure(object.id, 'HOLE_DEPTH_INVALID', 'Hole depth must be greater than zero.')
|
||||||
|
const radius = diameter / 2
|
||||||
|
const outer = Array.from({ length: 32 }, (_, index): [number, number, number] => {
|
||||||
|
const angle = 2 * Math.PI * index / 32
|
||||||
|
return [radius * Math.cos(angle), radius * Math.sin(angle), 0]
|
||||||
|
})
|
||||||
|
result = await geometry.pocket({ ...documentContext, base: baseShape, profile: { outer }, length: depth, direction: [0, 0, 1], throughAll: propertyValue(object, 'Type') === 'Through all' })
|
||||||
} else {
|
} else {
|
||||||
const baseObject = linkedObject(object, 'Base', document)
|
const baseObject = linkedObject(object, 'Base', document)
|
||||||
const baseShape = baseObject ? shapes.get(baseObject.id) : undefined
|
const baseShape = baseObject ? shapes.get(baseObject.id) : undefined
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ test('OCCT feature executor builds a Sketch to Pad to Pocket chain and retains t
|
|||||||
cut: async () => shape('cut-shape'),
|
cut: async () => shape('cut-shape'),
|
||||||
intersection: async () => shape('intersection-shape'),
|
intersection: async () => shape('intersection-shape'),
|
||||||
pad: async (input) => { calls.push(`pad:${input.profile.outer.length}`); if (padShouldFail) throw new Error('OCCT pad failed'); return shape('pad-shape') },
|
pad: async (input) => { calls.push(`pad:${input.profile.outer.length}`); if (padShouldFail) throw new Error('OCCT pad failed'); return shape('pad-shape') },
|
||||||
pocket: async (input) => { calls.push(`pocket:${input.base.id}:${String(input.throughAll)}`); return shape('pocket-shape') },
|
pocket: async (input) => { calls.push(`pocket:${input.base.id}:${String(input.throughAll)}:${input.profile.outer.length}:${input.length}`); return shape('pocket-shape') },
|
||||||
revolution: async (input) => { calls.push(`revolution:${input.angle}:${input.axisDirection?.[1]}`); return shape('revolution-shape') },
|
revolution: async (input) => { calls.push(`revolution:${input.angle}:${input.axisDirection?.[1]}`); return shape('revolution-shape') },
|
||||||
fillet: async () => shape('fillet-shape'),
|
fillet: async () => shape('fillet-shape'),
|
||||||
chamfer: async () => shape('chamfer-shape'),
|
chamfer: async () => shape('chamfer-shape'),
|
||||||
@@ -414,7 +414,7 @@ test('OCCT feature executor builds a Sketch to Pad to Pocket chain and retains t
|
|||||||
const context = { documentId: document.id, documentVersion: document.version, generation: 1, signal: new AbortController().signal }
|
const context = { documentId: document.id, documentVersion: document.version, generation: 1, signal: new AbortController().signal }
|
||||||
const result = await new RecomputeCoordinator(executor, () => 2).run(document)
|
const result = await new RecomputeCoordinator(executor, () => 2).run(document)
|
||||||
assert.equal(result.status, 'completed')
|
assert.equal(result.status, 'completed')
|
||||||
assert.deepEqual(calls.slice(0, 2), ['pad:3', 'pocket:pad-shape:true'])
|
assert.deepEqual(calls.slice(0, 2), ['pad:3', 'pocket:pad-shape:true:3:5'])
|
||||||
assert.equal(shapes.get('pad')?.id, 'pad-shape')
|
assert.equal(shapes.get('pad')?.id, 'pad-shape')
|
||||||
assert.equal(shapes.get('pocket')?.id, 'pocket-shape')
|
assert.equal(shapes.get('pocket')?.id, 'pocket-shape')
|
||||||
|
|
||||||
@@ -476,6 +476,21 @@ test('OCCT feature executor builds a Sketch to Pad to Pocket chain and retains t
|
|||||||
assert.ok(calls.includes('placement:0,0,0:0,0,1:90'))
|
assert.ok(calls.includes('placement:0,0,0:0,0,1:90'))
|
||||||
assert.ok(calls.includes('placement:0,0,0:0,0,1:180'))
|
assert.ok(calls.includes('placement:0,0,0:0,0,1:180'))
|
||||||
|
|
||||||
|
const hole = { id: 'hole', typeId: 'PartDesign::Hole', properties: [
|
||||||
|
{ name: 'Base', label: 'Base', group: 'Hole', scope: 'data' as const, type: 'App::PropertyLink' as const, value: 'pad' },
|
||||||
|
{ name: 'Diameter', label: 'Diameter', group: 'Hole', scope: 'data' as const, type: 'App::PropertyLength' as const, value: 6, unit: 'mm' },
|
||||||
|
{ name: 'Depth', label: 'Depth', group: 'Hole', scope: 'data' as const, type: 'App::PropertyLength' as const, value: 8, unit: 'mm' },
|
||||||
|
{ name: 'Type', label: 'Type', group: 'Hole', scope: 'data' as const, type: 'App::PropertyEnumeration' as const, value: 'Dimension', options: ['Dimension', 'Through all'] },
|
||||||
|
] }
|
||||||
|
const holeResult = await executor(hole, document, context)
|
||||||
|
assert.equal(holeResult.status, 'success')
|
||||||
|
assert.equal(shapes.get('hole')?.id, 'pocket-shape')
|
||||||
|
assert.ok(calls.includes('pocket:pad-shape:false:32:8'))
|
||||||
|
const invalidHole = { ...hole, properties: hole.properties.map((property) => property.name === 'Diameter' ? { ...property, value: 0 } : property) }
|
||||||
|
const invalidHoleResult = await executor(invalidHole, document, context)
|
||||||
|
assert.equal(invalidHoleResult.status, 'failed')
|
||||||
|
assert.equal(invalidHoleResult.errors?.[0].code, 'HOLE_DIAMETER_INVALID')
|
||||||
|
|
||||||
const suppressedPad = { ...pad, properties: [...pad.properties, { name: 'Suppressed', label: 'Suppressed', group: 'Feature state', scope: 'data' as const, type: 'App::PropertyBool' as const, value: true }] }
|
const suppressedPad = { ...pad, properties: [...pad.properties, { name: 'Suppressed', label: 'Suppressed', group: 'Feature state', scope: 'data' as const, type: 'App::PropertyBool' as const, value: true }] }
|
||||||
const suppressedResult = await executor(suppressedPad, document, context)
|
const suppressedResult = await executor(suppressedPad, document, context)
|
||||||
assert.equal(suppressedResult.status, 'suppressed')
|
assert.equal(suppressedResult.status, 'suppressed')
|
||||||
@@ -660,10 +675,10 @@ test('disabled commands return a reason instead of mutating the document', () =>
|
|||||||
test('manifest commands without a Bitbybit executor are explicitly unsupported', () => {
|
test('manifest commands without a Bitbybit executor are explicitly unsupported', () => {
|
||||||
const facade = createMockFacade()
|
const facade = createMockFacade()
|
||||||
const before = facade.getState().document.version
|
const before = facade.getState().document.version
|
||||||
const state = facade.gui.command.getState('hole')
|
const state = facade.gui.command.getState('mirrored')
|
||||||
assert.equal(state.status, 'disabled')
|
assert.equal(state.status, 'disabled')
|
||||||
assert.match(state.reason || '', /business executor/)
|
assert.match(state.reason || '', /business executor/)
|
||||||
facade.gui.command.execute({ commandId: 'hole' })
|
facade.gui.command.execute({ commandId: 'mirrored' })
|
||||||
assert.equal(facade.getState().diagnostics.at(-1)?.code, 'COMMAND_UNIMPLEMENTED')
|
assert.equal(facade.getState().diagnostics.at(-1)?.code, 'COMMAND_UNIMPLEMENTED')
|
||||||
assert.equal(facade.getState().document.version, before)
|
assert.equal(facade.getState().document.version, before)
|
||||||
})
|
})
|
||||||
@@ -948,6 +963,17 @@ test('Part Design feature tasks commit a document object and remain undoable', (
|
|||||||
assert.equal(polarPattern?.properties.find((property) => property.name === 'Angle')?.value, 180)
|
assert.equal(polarPattern?.properties.find((property) => property.name === 'Angle')?.value, 180)
|
||||||
assert.equal(polarPattern?.properties.find((property) => property.name === 'Axis')?.value, 'Normal')
|
assert.equal(polarPattern?.properties.find((property) => property.name === 'Axis')?.value, 'Normal')
|
||||||
assert.equal(facade.app.document.getObject('body')?.properties.find((property) => property.name === 'Tip')?.value, 'polar-pattern')
|
assert.equal(facade.app.document.getObject('body')?.properties.find((property) => property.name === 'Tip')?.value, 'polar-pattern')
|
||||||
|
|
||||||
|
facade.gui.command.execute({ commandId: 'hole' })
|
||||||
|
facade.task.update({ diameter: 6, depth: 12, type: 'Through all' })
|
||||||
|
facade.task.apply()
|
||||||
|
const hole = facade.app.document.getObject('hole')
|
||||||
|
assert.equal(hole?.typeId, 'PartDesign::Hole')
|
||||||
|
assert.equal(hole?.properties.find((property) => property.name === 'Base')?.value, 'polar-pattern')
|
||||||
|
assert.equal(hole?.properties.find((property) => property.name === 'Diameter')?.value, 6)
|
||||||
|
assert.equal(hole?.properties.find((property) => property.name === 'Depth')?.value, 12)
|
||||||
|
assert.equal(hole?.properties.find((property) => property.name === 'Type')?.value, 'Through all')
|
||||||
|
assert.equal(facade.app.document.getObject('body')?.properties.find((property) => property.name === 'Tip')?.value, 'hole')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Part workbench commands create primitive and boolean document objects', () => {
|
test('Part workbench commands create primitive and boolean document objects', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user