mirror of
https://github.com/HKUDS/CLI-Anything.git
synced 2026-08-29 07:30:51 +08:00
feat(freecad): update harness for FreeCAD 1.1 (new datum system, tapping, simulation)
- PartDesign: LocalCoordinateSystem, datum attachment, Whitworth threads (BSW/BSF/BSP/NPT), tapered holes, toggle freeze - Assembly: inline part insertion, joint motion simulation - CAM: G84/G74 tapping, multi-pass profiles, tool library import/export - FEM: beam sections (box/elliptical), tie constraints, purge results, suppress objects, Netgen refinement, solver output format - Sketcher: projection/reference modes, plane intersection, face-based external geometry - Draft: edge-selective fillet, relative font paths - TechDraw: area annotations with hole accounting, shape validation - Measure: enhanced check-geometry, additive measurements - Updated registry.json to require FreeCAD >= 1.1
This commit is contained in:
@@ -5,6 +5,8 @@
|
||||
**FreeCAD** is an open-source parametric 3D CAD modeler built on OpenCASCADE (OCCT).
|
||||
It supports Part design, Sketcher, Assembly, TechDraw, Mesh, and many other workbenches.
|
||||
|
||||
**This harness targets FreeCAD 1.1** (released March 2026) with 258 commands across 18 workbench groups.
|
||||
|
||||
- **Backend engine**: OpenCASCADE Technology (OCCT)
|
||||
- **Native format**: `.FCStd` (ZIP containing `Document.xml` + BREP geometry files)
|
||||
- **Python API**: `FreeCAD` (`App`) module — full document/object manipulation
|
||||
@@ -63,7 +65,7 @@ The CLI maintains project state as a JSON document:
|
||||
"metadata": {
|
||||
"created": "2026-03-22T...",
|
||||
"modified": "2026-03-22T...",
|
||||
"software": "cli-anything-freecad 1.0.0"
|
||||
"software": "cli-anything-freecad 1.1.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -79,6 +81,36 @@ The CLI maintains project state as a JSON document:
|
||||
| `material` | create, assign, list, set |
|
||||
| `export` | render, info, presets |
|
||||
| `session` | undo, redo, status, history |
|
||||
| `draft` | wire, rectangle, circle, polygon, fillet-2d, shapestring, ... |
|
||||
| `assembly` | new, add-part, constrain, solve, insert-part, create-simulation, ... |
|
||||
| `techdraw` | new-page, add-view, add-annotation, export-pdf, ... |
|
||||
| `mesh` | import, from-shape, export, repair, decimate, ... |
|
||||
| `fem` | new-analysis, mesh-generate, solve, add-beam-section, add-tie, ... |
|
||||
| `cam` | new-job, add-profile, add-tapping, set-tool, generate-gcode, ... |
|
||||
| `measure` | distance, length, angle, area, volume, check-geometry, ... |
|
||||
| `import` | auto, step, iges, stl, obj, dxf, brep, 3mf, ... |
|
||||
| `surface` | filling, sections, extend, blend-curve, sew, cut |
|
||||
| `spread` | new, set-cell, get-cell, set-alias, import-csv, export-csv |
|
||||
|
||||
## FreeCAD 1.1 Changes
|
||||
|
||||
### Breaking: Datum/Origin Redesign
|
||||
FreeCAD 1.1 replaces the legacy `Origin` object with `LocalCoordinateSystem`.
|
||||
Use `body local-coordinate-system` to create configurable coordinate systems
|
||||
with cross-workbench attachment support. Datum planes, lines, and points now
|
||||
support `--attachment-mode` and `--attachment-refs` for flexible positioning.
|
||||
|
||||
**Note:** Files created with FreeCAD 1.1 are NOT backward-compatible with 1.0.
|
||||
|
||||
### New Features by Workbench
|
||||
- **PartDesign**: Whitworth threads (BSW/BSF/BSP/NPT), tapered holes, feature freeze toggle
|
||||
- **Assembly**: Inline part insertion, joint motion simulation
|
||||
- **CAM**: G84/G74 tapping, multi-pass profiles, new tool library system
|
||||
- **FEM**: Netgen refinement, beam sections (box/elliptical), tie constraints, result purging
|
||||
- **Sketcher**: Projection/reference modes, plane intersection, face-based external geometry
|
||||
- **Draft**: Edge-selective fillet, relative font paths
|
||||
- **TechDraw**: Area annotations with hole accounting, shape validation
|
||||
- **Measure**: Enhanced check-geometry with valid entries, additive measurements
|
||||
|
||||
## Rendering Pipeline
|
||||
|
||||
|
||||
@@ -416,3 +416,175 @@ def collapse_assembly(
|
||||
comp["transform"] = [0.0, 0.0, 0.0]
|
||||
|
||||
return {"collapsed": True, "components": len(assembly["components"])}
|
||||
|
||||
|
||||
def insert_new_part(
|
||||
project: Dict[str, Any],
|
||||
asm_index: int,
|
||||
part_type: str = "box",
|
||||
name: Optional[str] = None,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
transform: Optional[List[float]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a new part inline within an assembly.
|
||||
|
||||
Instead of referencing an existing part from ``project["parts"]``,
|
||||
this function embeds an inline part definition directly in the
|
||||
assembly's components list.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
project : dict
|
||||
The mutable project state dictionary.
|
||||
asm_index : int
|
||||
Index of the target assembly.
|
||||
part_type : str
|
||||
The type of part to create (e.g. ``"box"``, ``"cylinder"``).
|
||||
name : str or None
|
||||
Human-readable label. Auto-generated when *None*.
|
||||
params : dict or None
|
||||
Part-specific parameters (e.g. dimensions). Defaults to ``{}``.
|
||||
transform : list[float] or None
|
||||
Optional ``[x, y, z]`` placement offset. Defaults to ``[0, 0, 0]``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
The newly created component entry.
|
||||
|
||||
Raises
|
||||
------
|
||||
IndexError
|
||||
If *asm_index* is out of range.
|
||||
"""
|
||||
assembly = _get_assembly(project, asm_index)
|
||||
|
||||
if transform is not None:
|
||||
transform = _validate_vec3(transform, "transform")
|
||||
else:
|
||||
transform = [0.0, 0.0, 0.0]
|
||||
|
||||
if params is None:
|
||||
params = {}
|
||||
|
||||
if name is None:
|
||||
name = _unique_name(project, f"InlinePart_{part_type}")
|
||||
|
||||
component: Dict[str, Any] = {
|
||||
"id": _next_id(project),
|
||||
"name": name,
|
||||
"inline_part": {
|
||||
"type": part_type,
|
||||
"params": dict(params),
|
||||
},
|
||||
"transform": transform,
|
||||
}
|
||||
|
||||
assembly["components"].append(component)
|
||||
assembly["solved"] = False
|
||||
return component
|
||||
|
||||
|
||||
def create_simulation(
|
||||
project: Dict[str, Any],
|
||||
asm_index: int,
|
||||
name: Optional[str] = None,
|
||||
duration: float = 5.0,
|
||||
fps: int = 24,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a simulation entry on an assembly for joint motion/animation.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
project : dict
|
||||
The mutable project state dictionary.
|
||||
asm_index : int
|
||||
Index of the target assembly.
|
||||
name : str or None
|
||||
Human-readable label. Auto-generated when *None*.
|
||||
duration : float
|
||||
Total simulation duration in seconds.
|
||||
fps : int
|
||||
Frames per second for the simulation.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
The newly created simulation dictionary.
|
||||
|
||||
Raises
|
||||
------
|
||||
IndexError
|
||||
If *asm_index* is out of range.
|
||||
"""
|
||||
assembly = _get_assembly(project, asm_index)
|
||||
|
||||
if name is None:
|
||||
name = f"Simulation_{len(assembly.get('simulations', [])) + 1}"
|
||||
|
||||
simulation: Dict[str, Any] = {
|
||||
"name": name,
|
||||
"duration": float(duration),
|
||||
"fps": int(fps),
|
||||
"steps": [],
|
||||
"status": "configured",
|
||||
}
|
||||
|
||||
assembly.setdefault("simulations", []).append(simulation)
|
||||
return simulation
|
||||
|
||||
|
||||
def add_simulation_step(
|
||||
project: Dict[str, Any],
|
||||
asm_index: int,
|
||||
sim_index: int,
|
||||
joint_index: int,
|
||||
start_value: float = 0.0,
|
||||
end_value: float = 1.0,
|
||||
) -> Dict[str, Any]:
|
||||
"""Append a motion step to an existing simulation.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
project : dict
|
||||
The mutable project state dictionary.
|
||||
asm_index : int
|
||||
Index of the target assembly.
|
||||
sim_index : int
|
||||
Index of the simulation within the assembly's ``simulations`` list.
|
||||
joint_index : int
|
||||
Index of the joint/constraint this step drives.
|
||||
start_value : float
|
||||
Starting value for the joint parameter.
|
||||
end_value : float
|
||||
Ending value for the joint parameter.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
The newly created step dictionary.
|
||||
|
||||
Raises
|
||||
------
|
||||
IndexError
|
||||
If *asm_index* or *sim_index* is out of range.
|
||||
"""
|
||||
assembly = _get_assembly(project, asm_index)
|
||||
|
||||
simulations = assembly.get("simulations", [])
|
||||
if not isinstance(sim_index, int) or sim_index < 0 or sim_index >= len(simulations):
|
||||
raise IndexError(
|
||||
f"Simulation index {sim_index} out of range "
|
||||
f"(0..{len(simulations) - 1})"
|
||||
)
|
||||
|
||||
simulation = simulations[sim_index]
|
||||
|
||||
step: Dict[str, Any] = {
|
||||
"joint_index": int(joint_index),
|
||||
"start_value": float(start_value),
|
||||
"end_value": float(end_value),
|
||||
}
|
||||
|
||||
simulation["steps"].append(step)
|
||||
return step
|
||||
|
||||
@@ -23,9 +23,15 @@ VALID_FEATURE_TYPES = {
|
||||
"draft", "thickness",
|
||||
"linear_pattern", "polar_pattern", "mirrored", "multi_transform",
|
||||
"hole", "datum_plane", "datum_line", "datum_point", "shape_binder",
|
||||
"local_coordinate_system",
|
||||
}
|
||||
VALID_REVOLUTION_AXES = {"X", "Y", "Z"}
|
||||
VALID_PATTERN_PLANES = {"XY", "XZ", "YZ"}
|
||||
VALID_THREAD_STANDARDS = {"metric", "BSW", "BSF", "BSP", "NPT"}
|
||||
VALID_ATTACHMENT_MODES = {
|
||||
"flat_face", "normal_to_edge", "translate", "object_xyz",
|
||||
"concentric", "tangent_plane", "inertial_cs",
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1680,6 +1686,9 @@ def hole_feature(
|
||||
depth: float = 10.0,
|
||||
threaded: bool = False,
|
||||
thread_pitch: Optional[float] = None,
|
||||
thread_standard: str = "metric",
|
||||
tapered: bool = False,
|
||||
taper_angle: Optional[float] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Add a hole feature to a body based on a sketch with point positions.
|
||||
|
||||
@@ -1720,6 +1729,15 @@ def hole_feature(
|
||||
if depth <= 0:
|
||||
raise ValueError(f"Hole depth must be positive, got {depth}")
|
||||
|
||||
if thread_standard not in VALID_THREAD_STANDARDS:
|
||||
raise ValueError(f"Invalid thread_standard '{thread_standard}'. Valid: {sorted(VALID_THREAD_STANDARDS)}")
|
||||
|
||||
if tapered and taper_angle is None:
|
||||
if thread_standard == "NPT":
|
||||
taper_angle = 1.7899 # ASME B1.20.1
|
||||
elif thread_standard == "BSP":
|
||||
taper_angle = 1.7899 # ISO 7-1
|
||||
|
||||
feature: Dict[str, Any] = {
|
||||
"id": _next_feature_id(body),
|
||||
"type": "hole",
|
||||
@@ -1728,6 +1746,9 @@ def hole_feature(
|
||||
"diameter": diameter,
|
||||
"depth": depth,
|
||||
"threaded": bool(threaded),
|
||||
"thread_standard": thread_standard,
|
||||
"tapered": bool(tapered),
|
||||
"taper_angle": taper_angle,
|
||||
}
|
||||
|
||||
if threaded and thread_pitch is not None:
|
||||
@@ -1745,6 +1766,8 @@ def datum_plane(
|
||||
body_index: int,
|
||||
offset: float = 0.0,
|
||||
reference: str = "XY",
|
||||
attachment_mode: Optional[str] = None,
|
||||
attachment_refs: Optional[List[str]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Add a datum plane to a body.
|
||||
|
||||
@@ -1781,6 +1804,13 @@ def datum_plane(
|
||||
"reference": reference,
|
||||
}
|
||||
|
||||
if attachment_mode is not None:
|
||||
if attachment_mode not in VALID_ATTACHMENT_MODES:
|
||||
raise ValueError(f"Invalid attachment_mode '{attachment_mode}'. Valid: {sorted(VALID_ATTACHMENT_MODES)}")
|
||||
feature["attachment_mode"] = attachment_mode
|
||||
if attachment_refs is not None:
|
||||
feature["attachment_refs"] = attachment_refs
|
||||
|
||||
body["features"].append(feature)
|
||||
return feature
|
||||
|
||||
@@ -1790,6 +1820,8 @@ def datum_line(
|
||||
body_index: int,
|
||||
point: Optional[List[float]] = None,
|
||||
direction: Optional[List[float]] = None,
|
||||
attachment_mode: Optional[str] = None,
|
||||
attachment_refs: Optional[List[str]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Add a datum line to a body.
|
||||
|
||||
@@ -1831,6 +1863,13 @@ def datum_line(
|
||||
"direction": direction,
|
||||
}
|
||||
|
||||
if attachment_mode is not None:
|
||||
if attachment_mode not in VALID_ATTACHMENT_MODES:
|
||||
raise ValueError(f"Invalid attachment_mode '{attachment_mode}'. Valid: {sorted(VALID_ATTACHMENT_MODES)}")
|
||||
feature["attachment_mode"] = attachment_mode
|
||||
if attachment_refs is not None:
|
||||
feature["attachment_refs"] = attachment_refs
|
||||
|
||||
body["features"].append(feature)
|
||||
return feature
|
||||
|
||||
@@ -1839,6 +1878,8 @@ def datum_point(
|
||||
project: Dict[str, Any],
|
||||
body_index: int,
|
||||
position: Optional[List[float]] = None,
|
||||
attachment_mode: Optional[str] = None,
|
||||
attachment_refs: Optional[List[str]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Add a datum point to a body.
|
||||
|
||||
@@ -1871,10 +1912,45 @@ def datum_point(
|
||||
"position": position,
|
||||
}
|
||||
|
||||
if attachment_mode is not None:
|
||||
if attachment_mode not in VALID_ATTACHMENT_MODES:
|
||||
raise ValueError(f"Invalid attachment_mode '{attachment_mode}'. Valid: {sorted(VALID_ATTACHMENT_MODES)}")
|
||||
feature["attachment_mode"] = attachment_mode
|
||||
if attachment_refs is not None:
|
||||
feature["attachment_refs"] = attachment_refs
|
||||
|
||||
body["features"].append(feature)
|
||||
return feature
|
||||
|
||||
|
||||
def local_coordinate_system(
|
||||
project: Dict[str, Any],
|
||||
body_index: int,
|
||||
position: Optional[List[float]] = None,
|
||||
x_axis: Optional[List[float]] = None,
|
||||
y_axis: Optional[List[float]] = None,
|
||||
z_axis: Optional[List[float]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Add a local coordinate system to a body (FreeCAD 1.1).
|
||||
|
||||
Replaces the legacy Origin object with a fully configurable
|
||||
coordinate system that supports cross-workbench attachment.
|
||||
"""
|
||||
bodies = project.get("bodies", [])
|
||||
if body_index < 0 or body_index >= len(bodies):
|
||||
raise IndexError(f"Body index {body_index} out of range (0..{len(bodies) - 1}).")
|
||||
body = bodies[body_index]
|
||||
feature: Dict[str, Any] = {
|
||||
"type": "local_coordinate_system",
|
||||
"position": position or [0.0, 0.0, 0.0],
|
||||
"x_axis": x_axis or [1.0, 0.0, 0.0],
|
||||
"y_axis": y_axis or [0.0, 1.0, 0.0],
|
||||
"z_axis": z_axis or [0.0, 0.0, 1.0],
|
||||
}
|
||||
body.setdefault("features", []).append(feature)
|
||||
return feature
|
||||
|
||||
|
||||
def shape_binder(
|
||||
project: Dict[str, Any],
|
||||
body_index: int,
|
||||
@@ -1916,3 +1992,24 @@ def shape_binder(
|
||||
|
||||
body["features"].append(feature)
|
||||
return feature
|
||||
|
||||
|
||||
def toggle_freeze(
|
||||
project: Dict[str, Any],
|
||||
body_index: int,
|
||||
feature_index: int,
|
||||
) -> Dict[str, Any]:
|
||||
"""Toggle the frozen state of a feature in a body (FreeCAD 1.1).
|
||||
|
||||
Frozen features are excluded from recomputation.
|
||||
"""
|
||||
bodies = project.get("bodies", [])
|
||||
if body_index < 0 or body_index >= len(bodies):
|
||||
raise IndexError(f"Body index {body_index} out of range (0..{len(bodies) - 1}).")
|
||||
body = bodies[body_index]
|
||||
features = body.get("features", [])
|
||||
if feature_index < 0 or feature_index >= len(features):
|
||||
raise IndexError(f"Feature index {feature_index} out of range (0..{len(features) - 1}).")
|
||||
feat = features[feature_index]
|
||||
feat["frozen"] = not feat.get("frozen", False)
|
||||
return feat
|
||||
|
||||
@@ -16,7 +16,7 @@ from .document import ensure_collection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
VALID_STOCK_TYPES: Set[str] = {"box", "cylinder", "from_part"}
|
||||
VALID_TOOL_TYPES: Set[str] = {"endmill", "ballnose", "drill", "chamfer", "vbit", "facemill"}
|
||||
VALID_TOOL_TYPES: Set[str] = {"endmill", "ballnose", "drill", "chamfer", "vbit", "facemill", "tap", "threadmill", "reamer"}
|
||||
|
||||
_COLLECTION_KEY = "cam_jobs"
|
||||
|
||||
@@ -167,6 +167,8 @@ def add_profile_op(
|
||||
faces: str = "all",
|
||||
depth: Optional[float] = None,
|
||||
step_down: float = 1.0,
|
||||
passes: Optional[int] = None,
|
||||
finishing_pass: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""Add a profile (contour) machining operation.
|
||||
|
||||
@@ -182,6 +184,11 @@ def add_profile_op(
|
||||
Total cut depth. When *None*, derived from part geometry.
|
||||
step_down : float
|
||||
Depth of cut per pass.
|
||||
passes : int or None
|
||||
Explicit number of passes. When provided, overrides automatic
|
||||
calculation from *step_down*.
|
||||
finishing_pass : bool
|
||||
When *True*, adds a light finishing pass after roughing.
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -195,6 +202,8 @@ def add_profile_op(
|
||||
"faces": faces,
|
||||
"depth": float(depth) if depth is not None else None,
|
||||
"step_down": float(step_down),
|
||||
"passes": int(passes) if passes is not None else None,
|
||||
"finishing_pass": finishing_pass,
|
||||
}
|
||||
|
||||
job["operations"].append(op)
|
||||
@@ -321,6 +330,54 @@ def add_facing_op(
|
||||
return op
|
||||
|
||||
|
||||
def add_tapping_op(
|
||||
project: Dict[str, Any],
|
||||
job_index: int,
|
||||
holes: str = "all",
|
||||
depth: Optional[float] = None,
|
||||
thread_pitch: float = 1.5,
|
||||
right_hand: bool = True,
|
||||
) -> Dict[str, Any]:
|
||||
"""Add a tapping operation (G84 right-hand / G74 left-hand).
|
||||
|
||||
FreeCAD 1.1 introduces native tapping cycle support.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
project : dict
|
||||
The mutable project state dictionary.
|
||||
job_index : int
|
||||
Index of the target job.
|
||||
holes : str
|
||||
Hole selection (``"all"`` or specific hole references).
|
||||
depth : float or None
|
||||
Total tap depth. When *None*, derived from part geometry.
|
||||
thread_pitch : float
|
||||
Thread pitch in project units.
|
||||
right_hand : bool
|
||||
When *True*, uses G84 (right-hand thread). When *False*,
|
||||
uses G74 (left-hand thread).
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
The operation entry.
|
||||
"""
|
||||
job = _get_job(project, job_index)
|
||||
|
||||
op: Dict[str, Any] = {
|
||||
"type": "tapping",
|
||||
"holes": holes,
|
||||
"depth": float(depth) if depth is not None else None,
|
||||
"thread_pitch": float(thread_pitch),
|
||||
"right_hand": right_hand,
|
||||
"g_code": "G84" if right_hand else "G74",
|
||||
}
|
||||
|
||||
job["operations"].append(op)
|
||||
return op
|
||||
|
||||
|
||||
def set_tool(
|
||||
project: Dict[str, Any],
|
||||
job_index: int,
|
||||
@@ -328,6 +385,8 @@ def set_tool(
|
||||
diameter: float = 6.0,
|
||||
flutes: int = 2,
|
||||
type: str = "endmill",
|
||||
tool_material: Optional[str] = None,
|
||||
coating: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Define or replace a cutting tool in a CAM job.
|
||||
|
||||
@@ -345,6 +404,10 @@ def set_tool(
|
||||
Number of cutting flutes.
|
||||
type : str
|
||||
Tool type (``"endmill"``, ``"ballnose"``, ``"drill"``, etc.).
|
||||
tool_material : str or None
|
||||
Tool substrate material (e.g. ``"HSS"``, ``"carbide"``).
|
||||
coating : str or None
|
||||
Tool coating (e.g. ``"TiN"``, ``"AlTiN"``, ``"DLC"``).
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -369,6 +432,11 @@ def set_tool(
|
||||
"type": type,
|
||||
}
|
||||
|
||||
if tool_material is not None:
|
||||
tool["tool_material"] = str(tool_material)
|
||||
if coating is not None:
|
||||
tool["coating"] = str(coating)
|
||||
|
||||
# Replace existing tool with same number, or append
|
||||
for i, existing in enumerate(job["tools"]):
|
||||
if existing["tool_number"] == tool_number:
|
||||
@@ -444,6 +512,7 @@ def simulate_job(
|
||||
"pocket": 300.0,
|
||||
"drilling": 60.0,
|
||||
"facing": 180.0,
|
||||
"tapping": 90.0,
|
||||
}
|
||||
|
||||
total_time = 0.0
|
||||
@@ -499,3 +568,87 @@ def export_gcode(
|
||||
"path": path.strip(),
|
||||
"format": "gcode",
|
||||
}
|
||||
|
||||
|
||||
def import_tool_library(
|
||||
project: Dict[str, Any],
|
||||
job_index: int,
|
||||
library_path: str,
|
||||
) -> Dict[str, Any]:
|
||||
"""Import a FreeCAD 1.1 tool library file into a CAM job.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
project : dict
|
||||
The mutable project state dictionary.
|
||||
job_index : int
|
||||
Index of the target job.
|
||||
library_path : str
|
||||
Path to the tool library file to import.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
Import metadata.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If *library_path* is invalid.
|
||||
"""
|
||||
if not isinstance(library_path, str) or not library_path.strip():
|
||||
raise ValueError("library_path must be a non-empty string")
|
||||
|
||||
job = _get_job(project, job_index)
|
||||
|
||||
if "metadata" not in job:
|
||||
job["metadata"] = {}
|
||||
|
||||
job["metadata"]["tool_library_path"] = library_path.strip()
|
||||
|
||||
return {
|
||||
"action": "import_tool_library",
|
||||
"job_name": job["name"],
|
||||
"job_index": job_index,
|
||||
"library_path": library_path.strip(),
|
||||
}
|
||||
|
||||
|
||||
def export_tool_library(
|
||||
project: Dict[str, Any],
|
||||
job_index: int,
|
||||
path: str,
|
||||
) -> Dict[str, Any]:
|
||||
"""Export the tool library of a CAM job to a file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
project : dict
|
||||
The mutable project state dictionary.
|
||||
job_index : int
|
||||
Index of the target job.
|
||||
path : str
|
||||
Output file path for the tool library.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
Export metadata.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If *path* is invalid.
|
||||
"""
|
||||
if not isinstance(path, str) or not path.strip():
|
||||
raise ValueError("Path must be a non-empty string")
|
||||
|
||||
job = _get_job(project, job_index)
|
||||
|
||||
return {
|
||||
"action": "export_tool_library",
|
||||
"job_name": job["name"],
|
||||
"job_index": job_index,
|
||||
"path": path.strip(),
|
||||
"tools_count": len(job["tools"]),
|
||||
}
|
||||
|
||||
@@ -394,6 +394,7 @@ def draft_shapestring(
|
||||
text: str,
|
||||
font_file: str,
|
||||
size: float = 10.0,
|
||||
font_path_relative: bool = False,
|
||||
name: Optional[str] = None,
|
||||
position: Optional[List[float]] = None,
|
||||
rotation: Optional[List[float]] = None,
|
||||
@@ -410,6 +411,8 @@ def draft_shapestring(
|
||||
Path to the TrueType font file.
|
||||
size : float
|
||||
Font height (default ``10``).
|
||||
font_path_relative : bool
|
||||
Whether *font_file* is a relative path (default ``False``).
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -426,6 +429,7 @@ def draft_shapestring(
|
||||
"text": text.strip(),
|
||||
"font_file": font_file.strip(),
|
||||
"size": float(size),
|
||||
"font_path_relative": bool(font_path_relative),
|
||||
}, position, rotation)
|
||||
|
||||
|
||||
@@ -1116,6 +1120,7 @@ def draft_fillet_2d(
|
||||
project: Dict[str, Any],
|
||||
index: int,
|
||||
radius: float = 1.0,
|
||||
edges: Optional[List[int]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Apply a 2D fillet (rounding) to the vertices of a draft object.
|
||||
|
||||
@@ -1127,6 +1132,8 @@ def draft_fillet_2d(
|
||||
Index of the draft object.
|
||||
radius : float
|
||||
Fillet radius (default ``1``).
|
||||
edges : list[int] or None
|
||||
When provided, fillet only these edge indices instead of all vertices.
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -1137,6 +1144,8 @@ def draft_fillet_2d(
|
||||
raise ValueError("radius must be a positive number")
|
||||
obj = _get_draft(project, index)
|
||||
obj["properties"]["_fillet_radius"] = float(radius)
|
||||
if edges is not None:
|
||||
obj["properties"]["_fillet_edges"] = list(edges)
|
||||
return obj
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,9 @@ from .document import ensure_collection
|
||||
VALID_ELEMENT_TYPES: Set[str] = {"Tet4", "Tet10", "Hex8", "Hex20", "Tri3", "Tri6"}
|
||||
VALID_SOLVERS: Set[str] = {"calculix", "elmer", "z88"}
|
||||
VALID_EXPORT_FORMATS: Set[str] = {"vtk", "csv", "json"}
|
||||
VALID_BEAM_SECTIONS: Set[str] = {"rectangular", "circular", "box_beam", "elliptical", "pipe"}
|
||||
VALID_OUTPUT_FORMATS: Set[str] = {"vtu", "vtk", "result"}
|
||||
VALID_MESHERS: Set[str] = {"gmsh", "netgen"}
|
||||
|
||||
_COLLECTION_KEY = "fem_analyses"
|
||||
|
||||
@@ -384,6 +387,10 @@ def generate_fem_mesh(
|
||||
max_size: Optional[float] = None,
|
||||
min_size: Optional[float] = None,
|
||||
element_type: str = "Tet10",
|
||||
mesher: str = "gmsh",
|
||||
gmsh_verbosity: int = 1,
|
||||
second_order_linear: bool = False,
|
||||
local_refinement: Optional[Dict[str, float]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Configure mesh generation parameters for an analysis.
|
||||
|
||||
@@ -402,6 +409,14 @@ def generate_fem_mesh(
|
||||
Minimum element size. When *None*, FreeCAD uses automatic sizing.
|
||||
element_type : str
|
||||
Element type (e.g. ``"Tet4"``, ``"Tet10"``, ``"Hex8"``).
|
||||
mesher : str
|
||||
Meshing backend (``"gmsh"`` or ``"netgen"``).
|
||||
gmsh_verbosity : int
|
||||
Gmsh verbosity level (only relevant when *mesher* is ``"gmsh"``).
|
||||
second_order_linear : bool
|
||||
Enable Netgen Second Order Linear elements.
|
||||
local_refinement : dict or None
|
||||
Mapping of geometry references to local mesh sizes.
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -411,7 +426,7 @@ def generate_fem_mesh(
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If *element_type* is unknown.
|
||||
If *element_type* or *mesher* is unknown.
|
||||
"""
|
||||
if element_type not in VALID_ELEMENT_TYPES:
|
||||
valid = ", ".join(sorted(VALID_ELEMENT_TYPES))
|
||||
@@ -419,22 +434,193 @@ def generate_fem_mesh(
|
||||
f"Unknown element_type '{element_type}'. Valid: {valid}"
|
||||
)
|
||||
|
||||
if mesher not in VALID_MESHERS:
|
||||
valid = ", ".join(sorted(VALID_MESHERS))
|
||||
raise ValueError(
|
||||
f"Unknown mesher '{mesher}'. Valid: {valid}"
|
||||
)
|
||||
|
||||
analysis = _get_analysis(project, ai)
|
||||
|
||||
mesh_params: Dict[str, Any] = {
|
||||
"max_size": float(max_size) if max_size is not None else None,
|
||||
"min_size": float(min_size) if min_size is not None else None,
|
||||
"element_type": element_type,
|
||||
"mesher": mesher,
|
||||
"gmsh_verbosity": int(gmsh_verbosity),
|
||||
"second_order_linear": bool(second_order_linear),
|
||||
"local_refinement": dict(local_refinement) if local_refinement is not None else None,
|
||||
}
|
||||
|
||||
analysis["mesh_params"] = mesh_params
|
||||
return mesh_params
|
||||
|
||||
|
||||
def add_beam_section(
|
||||
project: Dict[str, Any],
|
||||
analysis_index: int,
|
||||
section_type: str = "rectangular",
|
||||
references: Optional[List[str]] = None,
|
||||
width: Optional[float] = None,
|
||||
height: Optional[float] = None,
|
||||
radius: Optional[float] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Add an ElementGeometry1D beam section (FreeCAD 1.1: box_beam, elliptical).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
project : dict
|
||||
The mutable project state dictionary.
|
||||
analysis_index : int
|
||||
Analysis index.
|
||||
section_type : str
|
||||
Beam cross-section type (``"rectangular"``, ``"circular"``,
|
||||
``"box_beam"``, ``"elliptical"``, ``"pipe"``).
|
||||
references : list[str] or None
|
||||
Geometry references (edges) where the section applies.
|
||||
width : float or None
|
||||
Section width (relevant for rectangular / box_beam / elliptical).
|
||||
height : float or None
|
||||
Section height (relevant for rectangular / box_beam / elliptical).
|
||||
radius : float or None
|
||||
Section radius (relevant for circular / pipe).
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
The constraint entry.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If *section_type* is unknown.
|
||||
"""
|
||||
if section_type not in VALID_BEAM_SECTIONS:
|
||||
valid = ", ".join(sorted(VALID_BEAM_SECTIONS))
|
||||
raise ValueError(
|
||||
f"Unknown section_type '{section_type}'. Valid: {valid}"
|
||||
)
|
||||
|
||||
analysis = _get_analysis(project, analysis_index)
|
||||
|
||||
constraint: Dict[str, Any] = {
|
||||
"type": "beam_section",
|
||||
"section_type": section_type,
|
||||
"references": list(references) if references is not None else [],
|
||||
"width": float(width) if width is not None else None,
|
||||
"height": float(height) if height is not None else None,
|
||||
"radius": float(radius) if radius is not None else None,
|
||||
}
|
||||
|
||||
analysis["constraints"].append(constraint)
|
||||
return constraint
|
||||
|
||||
|
||||
def add_tie_constraint(
|
||||
project: Dict[str, Any],
|
||||
analysis_index: int,
|
||||
master_refs: List[str],
|
||||
slave_refs: List[str],
|
||||
) -> Dict[str, Any]:
|
||||
"""Add a tie constraint between shell faces (FreeCAD 1.1).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
project : dict
|
||||
The mutable project state dictionary.
|
||||
analysis_index : int
|
||||
Analysis index.
|
||||
master_refs : list[str]
|
||||
Geometry references for the master surface.
|
||||
slave_refs : list[str]
|
||||
Geometry references for the slave surface.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
The constraint entry.
|
||||
"""
|
||||
analysis = _get_analysis(project, analysis_index)
|
||||
|
||||
constraint: Dict[str, Any] = {
|
||||
"type": "tie",
|
||||
"master_refs": list(master_refs),
|
||||
"slave_refs": list(slave_refs),
|
||||
}
|
||||
|
||||
analysis["constraints"].append(constraint)
|
||||
return constraint
|
||||
|
||||
|
||||
def purge_results(
|
||||
project: Dict[str, Any],
|
||||
analysis_index: int,
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete all result objects from an analysis (FreeCAD 1.1).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
project : dict
|
||||
The mutable project state dictionary.
|
||||
analysis_index : int
|
||||
Analysis index.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
The updated analysis dictionary.
|
||||
"""
|
||||
analysis = _get_analysis(project, analysis_index)
|
||||
analysis["results"] = None
|
||||
return analysis
|
||||
|
||||
|
||||
def suppress_object(
|
||||
project: Dict[str, Any],
|
||||
analysis_index: int,
|
||||
constraint_index: int,
|
||||
) -> Dict[str, Any]:
|
||||
"""Toggle suppressed state on a constraint (FreeCAD 1.1).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
project : dict
|
||||
The mutable project state dictionary.
|
||||
analysis_index : int
|
||||
Analysis index.
|
||||
constraint_index : int
|
||||
Index of the constraint to toggle.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
The updated constraint dictionary.
|
||||
|
||||
Raises
|
||||
------
|
||||
IndexError
|
||||
If *constraint_index* is out of range.
|
||||
"""
|
||||
analysis = _get_analysis(project, analysis_index)
|
||||
constraints = analysis["constraints"]
|
||||
|
||||
if not isinstance(constraint_index, int) or constraint_index < 0 or constraint_index >= len(constraints):
|
||||
raise IndexError(
|
||||
f"Constraint index {constraint_index} out of range "
|
||||
f"(0..{len(constraints) - 1})"
|
||||
)
|
||||
|
||||
constraint = constraints[constraint_index]
|
||||
constraint["suppressed"] = not constraint.get("suppressed", False)
|
||||
return constraint
|
||||
|
||||
|
||||
def solve_fem(
|
||||
project: Dict[str, Any],
|
||||
ai: int,
|
||||
solver: str = "calculix",
|
||||
output_format: Optional[str] = None,
|
||||
buckling_accuracy: Optional[float] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Configure the FEM solver for an analysis.
|
||||
|
||||
@@ -450,6 +636,11 @@ def solve_fem(
|
||||
Analysis index.
|
||||
solver : str
|
||||
Solver backend name (``"calculix"``, ``"elmer"``, ``"z88"``).
|
||||
output_format : str or None
|
||||
Result output format (``"vtu"``, ``"vtk"``, ``"result"``).
|
||||
When *None*, the solver default is used.
|
||||
buckling_accuracy : float or None
|
||||
Buckling accuracy parameter for CalculiX solver.
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -459,13 +650,19 @@ def solve_fem(
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If *solver* is unknown or the analysis is missing constraints
|
||||
or mesh parameters.
|
||||
If *solver* is unknown, *output_format* is invalid, or the
|
||||
analysis is missing constraints or mesh parameters.
|
||||
"""
|
||||
if solver not in VALID_SOLVERS:
|
||||
valid = ", ".join(sorted(VALID_SOLVERS))
|
||||
raise ValueError(f"Unknown solver '{solver}'. Valid: {valid}")
|
||||
|
||||
if output_format is not None and output_format not in VALID_OUTPUT_FORMATS:
|
||||
valid = ", ".join(sorted(VALID_OUTPUT_FORMATS))
|
||||
raise ValueError(
|
||||
f"Unknown output_format '{output_format}'. Valid: {valid}"
|
||||
)
|
||||
|
||||
analysis = _get_analysis(project, ai)
|
||||
|
||||
if not analysis["constraints"]:
|
||||
@@ -482,6 +679,8 @@ def solve_fem(
|
||||
"status": "pending",
|
||||
"solver": solver,
|
||||
"constraints_count": len(analysis["constraints"]),
|
||||
"output_format": output_format,
|
||||
"buckling_accuracy": float(buckling_accuracy) if buckling_accuracy is not None else None,
|
||||
}
|
||||
|
||||
return analysis["results"]
|
||||
|
||||
@@ -211,7 +211,8 @@ def _compute_inertia(part: Dict[str, Any]) -> Optional[Dict[str, float]]:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def measure_distance(
|
||||
project: Dict[str, Any], index1: int, index2: int
|
||||
project: Dict[str, Any], index1: int, index2: int,
|
||||
additive: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""Measure the Euclidean distance between two parts (bounding-box centres).
|
||||
|
||||
@@ -240,16 +241,20 @@ def measure_distance(
|
||||
dz = c2[2] - c1[2]
|
||||
dist = math.sqrt(dx ** 2 + dy ** 2 + dz ** 2)
|
||||
|
||||
return _store_measurement(project, "distance", {
|
||||
result: Dict[str, Any] = {
|
||||
"part1_index": index1,
|
||||
"part2_index": index2,
|
||||
"distance": round(dist, 6),
|
||||
"delta": [round(dx, 6), round(dy, 6), round(dz, 6)],
|
||||
})
|
||||
}
|
||||
if additive:
|
||||
result["additive"] = True
|
||||
return _store_measurement(project, "distance", result)
|
||||
|
||||
|
||||
def measure_length(
|
||||
project: Dict[str, Any], index: int, edge_ref: Optional[str] = None
|
||||
project: Dict[str, Any], index: int, edge_ref: Optional[str] = None,
|
||||
additive: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""Estimate the length of a part edge.
|
||||
|
||||
@@ -278,12 +283,15 @@ def measure_length(
|
||||
|
||||
if edge_ref is not None:
|
||||
# Deferred — requires macro execution
|
||||
return _store_measurement(project, "length", {
|
||||
result_deferred: Dict[str, Any] = {
|
||||
"part_index": index,
|
||||
"edge_ref": edge_ref,
|
||||
"length": None,
|
||||
"deferred": True,
|
||||
})
|
||||
}
|
||||
if additive:
|
||||
result_deferred["additive"] = True
|
||||
return _store_measurement(project, "length", result_deferred)
|
||||
|
||||
if t == "box":
|
||||
length = max(p["length"], p["width"], p["height"])
|
||||
@@ -302,16 +310,20 @@ def measure_length(
|
||||
p["zmax"] - p["zmin"],
|
||||
)
|
||||
|
||||
return _store_measurement(project, "length", {
|
||||
result_len: Dict[str, Any] = {
|
||||
"part_index": index,
|
||||
"edge_ref": edge_ref,
|
||||
"length": round(length, 6) if length is not None else None,
|
||||
"deferred": length is None,
|
||||
})
|
||||
}
|
||||
if additive:
|
||||
result_len["additive"] = True
|
||||
return _store_measurement(project, "length", result_len)
|
||||
|
||||
|
||||
def measure_angle(
|
||||
project: Dict[str, Any], index1: int, index2: int
|
||||
project: Dict[str, Any], index1: int, index2: int,
|
||||
additive: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""Measure the angle between two parts based on their centre vectors from the origin.
|
||||
|
||||
@@ -340,14 +352,17 @@ def measure_angle(
|
||||
cos_val = max(-1.0, min(1.0, dot / (mag1 * mag2)))
|
||||
angle_deg = math.degrees(math.acos(cos_val))
|
||||
|
||||
return _store_measurement(project, "angle", {
|
||||
result_angle: Dict[str, Any] = {
|
||||
"part1_index": index1,
|
||||
"part2_index": index2,
|
||||
"angle_deg": round(angle_deg, 6),
|
||||
})
|
||||
}
|
||||
if additive:
|
||||
result_angle["additive"] = True
|
||||
return _store_measurement(project, "angle", result_angle)
|
||||
|
||||
|
||||
def measure_area(project: Dict[str, Any], index: int) -> Dict[str, Any]:
|
||||
def measure_area(project: Dict[str, Any], index: int, additive: bool = False) -> Dict[str, Any]:
|
||||
"""Estimate the surface area of a part from its primitive parameters.
|
||||
|
||||
Returns
|
||||
@@ -358,14 +373,17 @@ def measure_area(project: Dict[str, Any], index: int) -> Dict[str, Any]:
|
||||
part = get_part(project, index)
|
||||
area = _compute_area(part)
|
||||
|
||||
return _store_measurement(project, "area", {
|
||||
result_area: Dict[str, Any] = {
|
||||
"part_index": index,
|
||||
"area": round(area, 6) if area is not None else None,
|
||||
"deferred": area is None,
|
||||
})
|
||||
}
|
||||
if additive:
|
||||
result_area["additive"] = True
|
||||
return _store_measurement(project, "area", result_area)
|
||||
|
||||
|
||||
def measure_volume(project: Dict[str, Any], index: int) -> Dict[str, Any]:
|
||||
def measure_volume(project: Dict[str, Any], index: int, additive: bool = False) -> Dict[str, Any]:
|
||||
"""Estimate the volume of a part from its primitive parameters.
|
||||
|
||||
Formulas used:
|
||||
@@ -383,14 +401,17 @@ def measure_volume(project: Dict[str, Any], index: int) -> Dict[str, Any]:
|
||||
part = get_part(project, index)
|
||||
volume = _compute_volume(part)
|
||||
|
||||
return _store_measurement(project, "volume", {
|
||||
result_vol: Dict[str, Any] = {
|
||||
"part_index": index,
|
||||
"volume": round(volume, 6) if volume is not None else None,
|
||||
"deferred": volume is None,
|
||||
})
|
||||
}
|
||||
if additive:
|
||||
result_vol["additive"] = True
|
||||
return _store_measurement(project, "volume", result_vol)
|
||||
|
||||
|
||||
def measure_radius(project: Dict[str, Any], index: int) -> Dict[str, Any]:
|
||||
def measure_radius(project: Dict[str, Any], index: int, additive: bool = False) -> Dict[str, Any]:
|
||||
"""Return the radius of a cylindrical, spherical, or toroidal part.
|
||||
|
||||
For cones, the larger of ``radius1`` / ``radius2`` is returned.
|
||||
@@ -423,13 +444,16 @@ def measure_radius(project: Dict[str, Any], index: int) -> Dict[str, Any]:
|
||||
f"Supported: cylinder, sphere, cone, torus"
|
||||
)
|
||||
|
||||
return _store_measurement(project, "radius", {
|
||||
result_rad: Dict[str, Any] = {
|
||||
"part_index": index,
|
||||
"radius": round(radius, 6),
|
||||
})
|
||||
}
|
||||
if additive:
|
||||
result_rad["additive"] = True
|
||||
return _store_measurement(project, "radius", result_rad)
|
||||
|
||||
|
||||
def measure_diameter(project: Dict[str, Any], index: int) -> Dict[str, Any]:
|
||||
def measure_diameter(project: Dict[str, Any], index: int, additive: bool = False) -> Dict[str, Any]:
|
||||
"""Return the diameter of a cylindrical, spherical, or toroidal part.
|
||||
|
||||
Returns
|
||||
@@ -460,13 +484,16 @@ def measure_diameter(project: Dict[str, Any], index: int) -> Dict[str, Any]:
|
||||
f"Supported: cylinder, sphere, cone, torus"
|
||||
)
|
||||
|
||||
return _store_measurement(project, "diameter", {
|
||||
result_dia: Dict[str, Any] = {
|
||||
"part_index": index,
|
||||
"diameter": round(diameter, 6),
|
||||
})
|
||||
}
|
||||
if additive:
|
||||
result_dia["additive"] = True
|
||||
return _store_measurement(project, "diameter", result_dia)
|
||||
|
||||
|
||||
def measure_position(project: Dict[str, Any], index: int) -> Dict[str, Any]:
|
||||
def measure_position(project: Dict[str, Any], index: int, additive: bool = False) -> Dict[str, Any]:
|
||||
"""Return the placement position of a part.
|
||||
|
||||
Returns
|
||||
@@ -477,14 +504,18 @@ def measure_position(project: Dict[str, Any], index: int) -> Dict[str, Any]:
|
||||
part = get_part(project, index)
|
||||
pos = _get_position(part)
|
||||
|
||||
return _store_measurement(project, "position", {
|
||||
result_pos: Dict[str, Any] = {
|
||||
"part_index": index,
|
||||
"position": pos,
|
||||
})
|
||||
}
|
||||
if additive:
|
||||
result_pos["additive"] = True
|
||||
return _store_measurement(project, "position", result_pos)
|
||||
|
||||
|
||||
def measure_center_of_mass(
|
||||
project: Dict[str, Any], index: int
|
||||
project: Dict[str, Any], index: int,
|
||||
additive: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""Estimate the centre of mass (geometric centre for simple shapes).
|
||||
|
||||
@@ -499,14 +530,18 @@ def measure_center_of_mass(
|
||||
part = get_part(project, index)
|
||||
com = _bbox_center(part)
|
||||
|
||||
return _store_measurement(project, "center_of_mass", {
|
||||
result_com: Dict[str, Any] = {
|
||||
"part_index": index,
|
||||
"center_of_mass": [round(v, 6) for v in com],
|
||||
})
|
||||
}
|
||||
if additive:
|
||||
result_com["additive"] = True
|
||||
return _store_measurement(project, "center_of_mass", result_com)
|
||||
|
||||
|
||||
def measure_bounding_box(
|
||||
project: Dict[str, Any], index: int
|
||||
project: Dict[str, Any], index: int,
|
||||
additive: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""Compute the axis-aligned bounding box of a part.
|
||||
|
||||
@@ -560,26 +595,32 @@ def measure_bounding_box(
|
||||
]
|
||||
else:
|
||||
# Unknown / boolean — deferred
|
||||
return _store_measurement(project, "bounding_box", {
|
||||
result_bb_def: Dict[str, Any] = {
|
||||
"part_index": index,
|
||||
"min": None,
|
||||
"max": None,
|
||||
"size": None,
|
||||
"deferred": True,
|
||||
})
|
||||
}
|
||||
if additive:
|
||||
result_bb_def["additive"] = True
|
||||
return _store_measurement(project, "bounding_box", result_bb_def)
|
||||
|
||||
size = [bb_max[i] - bb_min[i] for i in range(3)]
|
||||
|
||||
return _store_measurement(project, "bounding_box", {
|
||||
result_bb: Dict[str, Any] = {
|
||||
"part_index": index,
|
||||
"min": [round(v, 6) for v in bb_min],
|
||||
"max": [round(v, 6) for v in bb_max],
|
||||
"size": [round(v, 6) for v in size],
|
||||
"deferred": False,
|
||||
})
|
||||
}
|
||||
if additive:
|
||||
result_bb["additive"] = True
|
||||
return _store_measurement(project, "bounding_box", result_bb)
|
||||
|
||||
|
||||
def measure_inertia(project: Dict[str, Any], index: int) -> Dict[str, Any]:
|
||||
def measure_inertia(project: Dict[str, Any], index: int, additive: bool = False) -> Dict[str, Any]:
|
||||
"""Estimate the principal moments of inertia (unit density).
|
||||
|
||||
Returns
|
||||
@@ -593,26 +634,57 @@ def measure_inertia(project: Dict[str, Any], index: int) -> Dict[str, Any]:
|
||||
if inertia is not None:
|
||||
inertia = {k: round(v, 6) for k, v in inertia.items()}
|
||||
|
||||
return _store_measurement(project, "inertia", {
|
||||
result_inertia: Dict[str, Any] = {
|
||||
"part_index": index,
|
||||
"inertia": inertia,
|
||||
"deferred": inertia is None,
|
||||
})
|
||||
}
|
||||
if additive:
|
||||
result_inertia["additive"] = True
|
||||
return _store_measurement(project, "inertia", result_inertia)
|
||||
|
||||
|
||||
def check_geometry(project: Dict[str, Any], index: int) -> Dict[str, Any]:
|
||||
def check_geometry(
|
||||
project: Dict[str, Any],
|
||||
index: int,
|
||||
include_valid: bool = False,
|
||||
skip_objects: Optional[List[int]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Perform basic geometry validation on a part.
|
||||
|
||||
Checks that all numeric parameters are positive and that the part
|
||||
type is a known primitive.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
project : dict
|
||||
The mutable project state dictionary.
|
||||
index : int
|
||||
Index of the part in ``project["parts"]``.
|
||||
include_valid : bool
|
||||
When ``True``, also reports valid shape entries in ``valid_entries``
|
||||
(default ``False``).
|
||||
skip_objects : list[int] or None
|
||||
When provided, excludes these part indices from the check. If the
|
||||
requested *index* is in the skip list the result is returned
|
||||
immediately with ``"skipped": True``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
Record with ``valid`` boolean and list of ``issues``.
|
||||
"""
|
||||
if skip_objects is not None and index in skip_objects:
|
||||
return _store_measurement(project, "geometry_check", {
|
||||
"part_index": index,
|
||||
"valid": True,
|
||||
"issues": [],
|
||||
"skipped": True,
|
||||
})
|
||||
|
||||
part = get_part(project, index)
|
||||
issues: List[str] = []
|
||||
valid_entries: List[str] = []
|
||||
|
||||
t = part["type"]
|
||||
if t not in PRIMITIVES:
|
||||
@@ -626,6 +698,8 @@ def check_geometry(project: Dict[str, Any], index: int) -> Dict[str, Any]:
|
||||
# Angle parameters may be negative (e.g. angle1 on sphere/torus)
|
||||
if "angle" not in key and val <= 0:
|
||||
issues.append(f"Parameter '{key}' must be positive, got {val}")
|
||||
elif include_valid:
|
||||
valid_entries.append(f"Parameter '{key}' = {val}")
|
||||
else:
|
||||
issues.append(f"Missing expected parameter '{key}'")
|
||||
|
||||
@@ -636,11 +710,21 @@ def check_geometry(project: Dict[str, Any], index: int) -> Dict[str, Any]:
|
||||
else:
|
||||
if "position" not in placement:
|
||||
issues.append("Missing 'position' in placement")
|
||||
elif include_valid:
|
||||
valid_entries.append("Placement 'position' present")
|
||||
if "rotation" not in placement:
|
||||
issues.append("Missing 'rotation' in placement")
|
||||
elif include_valid:
|
||||
valid_entries.append("Placement 'rotation' present")
|
||||
|
||||
return _store_measurement(project, "geometry_check", {
|
||||
result: Dict[str, Any] = {
|
||||
"part_index": index,
|
||||
"valid": len(issues) == 0,
|
||||
"issues": issues,
|
||||
})
|
||||
}
|
||||
if include_valid:
|
||||
result["valid_entries"] = valid_entries
|
||||
if skip_objects is not None:
|
||||
result["skipped"] = False
|
||||
|
||||
return _store_measurement(project, "geometry_check", result)
|
||||
|
||||
@@ -1574,6 +1574,7 @@ def project_external(
|
||||
sketch_index: int,
|
||||
part_index: int,
|
||||
edge_ref: Optional[str] = None,
|
||||
mode: str = "projection",
|
||||
) -> Dict[str, Any]:
|
||||
"""Project external geometry into the sketch as a reference element.
|
||||
|
||||
@@ -1588,6 +1589,9 @@ def project_external(
|
||||
edge_ref:
|
||||
Optional edge reference identifier (e.g. ``"Edge1"``).
|
||||
If ``None``, the entire shape is projected.
|
||||
mode:
|
||||
External geometry mode: ``"projection"`` or ``"reference"``
|
||||
(FreeCAD 1.1).
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -1600,11 +1604,103 @@ def project_external(
|
||||
if sketch["closed"]:
|
||||
raise ValueError("Cannot add elements to a closed sketch")
|
||||
|
||||
valid_modes = {"projection", "reference"}
|
||||
if mode not in valid_modes:
|
||||
raise ValueError(
|
||||
f"Invalid mode '{mode}'. Must be one of: {', '.join(sorted(valid_modes))}"
|
||||
)
|
||||
|
||||
element: Dict[str, Any] = {
|
||||
"id": _next_element_id(sketch),
|
||||
"type": "external_reference",
|
||||
"part_index": int(part_index),
|
||||
"edge_ref": edge_ref,
|
||||
"mode": mode,
|
||||
"construction": True,
|
||||
}
|
||||
|
||||
sketch["elements"].append(element)
|
||||
return element
|
||||
|
||||
|
||||
def intersection_external(
|
||||
project: Dict[str, Any],
|
||||
sketch_index: int,
|
||||
body_index: int,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create external geometry from sketch-plane intersection with a body (FreeCAD 1.1).
|
||||
|
||||
Generates external geometry elements at the intersection of the sketch
|
||||
plane with the specified body geometry.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
project:
|
||||
The project dictionary.
|
||||
sketch_index:
|
||||
Index of the target sketch.
|
||||
body_index:
|
||||
Index of the body to intersect with the sketch plane.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Dict[str, Any]
|
||||
The newly created intersection reference element.
|
||||
"""
|
||||
_validate_project(project)
|
||||
sketch = _get_sketch(project, sketch_index)
|
||||
|
||||
if sketch["closed"]:
|
||||
raise ValueError("Cannot add elements to a closed sketch")
|
||||
|
||||
element: Dict[str, Any] = {
|
||||
"id": _next_element_id(sketch),
|
||||
"type": "intersection_reference",
|
||||
"body_index": int(body_index),
|
||||
"construction": True,
|
||||
}
|
||||
|
||||
sketch["elements"].append(element)
|
||||
return element
|
||||
|
||||
|
||||
def add_external_from_face(
|
||||
project: Dict[str, Any],
|
||||
sketch_index: int,
|
||||
part_index: int,
|
||||
face_ref: str,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create external geometry from a face selection (FreeCAD 1.1).
|
||||
|
||||
Projects the boundary of the referenced face onto the sketch plane.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
project:
|
||||
The project dictionary.
|
||||
sketch_index:
|
||||
Index of the target sketch.
|
||||
part_index:
|
||||
Index of the body/part containing the face.
|
||||
face_ref:
|
||||
Face reference identifier (e.g. ``"Face1"``).
|
||||
|
||||
Returns
|
||||
-------
|
||||
Dict[str, Any]
|
||||
The newly created face reference element.
|
||||
"""
|
||||
_validate_project(project)
|
||||
sketch = _get_sketch(project, sketch_index)
|
||||
|
||||
if sketch["closed"]:
|
||||
raise ValueError("Cannot add elements to a closed sketch")
|
||||
|
||||
element: Dict[str, Any] = {
|
||||
"id": _next_element_id(sketch),
|
||||
"type": "face_reference",
|
||||
"part_index": int(part_index),
|
||||
"face_ref": face_ref,
|
||||
"construction": True,
|
||||
}
|
||||
|
||||
|
||||
@@ -407,9 +407,26 @@ def add_annotation(
|
||||
page_index: int,
|
||||
text: str,
|
||||
position: Optional[List[float]] = None,
|
||||
area_mode: bool = False,
|
||||
shape_validation: bool = True,
|
||||
) -> Dict[str, Any]:
|
||||
"""Add a text annotation to a page.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
project : dict
|
||||
The mutable project state dictionary.
|
||||
page_index : int
|
||||
Index of the target page.
|
||||
text : str
|
||||
Annotation text content.
|
||||
position : list[float] or None
|
||||
Position on the page ``[x, y]``. Defaults to ``[0, 0]``.
|
||||
area_mode : bool
|
||||
When ``True``, computes area accounting for face holes (default ``False``).
|
||||
shape_validation : bool
|
||||
Enables shape validation (default ``True``).
|
||||
|
||||
Returns the annotation entry.
|
||||
"""
|
||||
page = _get_page(project, page_index)
|
||||
@@ -423,6 +440,8 @@ def add_annotation(
|
||||
"type": "annotation",
|
||||
"text": str(text),
|
||||
"position": position,
|
||||
"area_mode": bool(area_mode),
|
||||
"shape_validation": bool(shape_validation),
|
||||
}
|
||||
|
||||
page["annotations"].append(annotation)
|
||||
|
||||
@@ -1059,18 +1059,45 @@ def sketch_set_construction(sketch_index: int, elem_id: int, flag: bool) -> None
|
||||
@click.argument("sketch_index", type=int)
|
||||
@click.argument("part_index", type=int)
|
||||
@click.option("--edge-ref", help="Edge reference (e.g. Edge1).")
|
||||
@click.option("--mode", type=click.Choice(["projection", "reference"]),
|
||||
default="projection", help="Projection mode (FreeCAD 1.1).")
|
||||
@handle_error
|
||||
def sketch_project_external(sketch_index: int, part_index: int,
|
||||
edge_ref: Optional[str]) -> None:
|
||||
edge_ref: Optional[str], mode: str) -> None:
|
||||
"""Project external geometry into a sketch."""
|
||||
sess = get_session()
|
||||
sess.snapshot(f"Project external into sketch #{sketch_index}")
|
||||
proj = sess.get_project()
|
||||
result = sketch_mod.project_external(proj, sketch_index, part_index,
|
||||
edge_ref=edge_ref)
|
||||
edge_ref=edge_ref, mode=mode)
|
||||
output_fn(result, "Projected external geometry")
|
||||
|
||||
|
||||
@sketch_group.command("intersection")
|
||||
@click.argument("sketch_index", type=int)
|
||||
@click.argument("body_index", type=int)
|
||||
@handle_error
|
||||
def sketch_intersection(sketch_index, body_index):
|
||||
"""Create external geometry from sketch-plane intersection (FreeCAD 1.1)."""
|
||||
sess = get_session()
|
||||
proj = sess.get_project()
|
||||
result = sketch_mod.intersection_external(proj, sketch_index, body_index)
|
||||
output_fn(result, "Intersection reference created.")
|
||||
|
||||
|
||||
@sketch_group.command("add-external-face")
|
||||
@click.argument("sketch_index", type=int)
|
||||
@click.argument("part_index", type=int)
|
||||
@click.option("--face-ref", required=True, help="Face reference string")
|
||||
@handle_error
|
||||
def sketch_add_external_face(sketch_index, part_index, face_ref):
|
||||
"""Create external geometry from face selection (FreeCAD 1.1)."""
|
||||
sess = get_session()
|
||||
proj = sess.get_project()
|
||||
result = sketch_mod.add_external_from_face(proj, sketch_index, part_index, face_ref)
|
||||
output_fn(result, "Face reference created.")
|
||||
|
||||
|
||||
# ── Body commands ────────────────────────────────────────────────────
|
||||
|
||||
@cli.group("body")
|
||||
@@ -1553,16 +1580,24 @@ def body_thickness_feature(body_index: int, thickness_val: float,
|
||||
@click.option("--depth", default=10.0, type=float, help="Hole depth.")
|
||||
@click.option("--threaded", is_flag=True, help="Threaded hole.")
|
||||
@click.option("--thread-pitch", type=float, help="Thread pitch.")
|
||||
@click.option("--thread-standard", type=click.Choice(["metric", "BSW", "BSF", "BSP", "NPT"]),
|
||||
default="metric", help="Thread standard (FreeCAD 1.1).")
|
||||
@click.option("--tapered", is_flag=True, help="Tapered hole (FreeCAD 1.1).")
|
||||
@click.option("--taper-angle", type=float, default=None, help="Taper angle (FreeCAD 1.1).")
|
||||
@handle_error
|
||||
def body_hole(body_index: int, sketch_index: int, diameter: float,
|
||||
depth: float, threaded: bool, thread_pitch: Optional[float]) -> None:
|
||||
depth: float, threaded: bool, thread_pitch: Optional[float],
|
||||
thread_standard: str, tapered: bool,
|
||||
taper_angle: Optional[float]) -> None:
|
||||
"""Add a hole feature to a body."""
|
||||
sess = get_session()
|
||||
sess.snapshot(f"Hole body #{body_index}")
|
||||
proj = sess.get_project()
|
||||
result = body_mod.hole_feature(proj, body_index, sketch_index,
|
||||
diameter=diameter, depth=depth,
|
||||
threaded=threaded, thread_pitch=thread_pitch)
|
||||
threaded=threaded, thread_pitch=thread_pitch,
|
||||
thread_standard=thread_standard,
|
||||
tapered=tapered, taper_angle=taper_angle)
|
||||
output_fn(result, "Added hole feature")
|
||||
|
||||
|
||||
@@ -1636,13 +1671,21 @@ def body_multi_transform(body_index: int, transforms_json: str) -> None:
|
||||
@click.argument("body_index", type=int)
|
||||
@click.option("--offset", default=0.0, type=float, help="Offset from reference.")
|
||||
@click.option("--reference", default="XY", type=click.Choice(["XY", "XZ", "YZ"]))
|
||||
@click.option("--attachment-mode", type=str, default=None, help="Attachment mode (FreeCAD 1.1).")
|
||||
@click.option("--attachment-refs", type=str, default=None,
|
||||
help="Comma-separated attachment references (FreeCAD 1.1).")
|
||||
@handle_error
|
||||
def body_datum_plane(body_index: int, offset: float, reference: str) -> None:
|
||||
def body_datum_plane(body_index: int, offset: float, reference: str,
|
||||
attachment_mode: Optional[str],
|
||||
attachment_refs: Optional[str]) -> None:
|
||||
"""Add a datum plane to a body."""
|
||||
sess = get_session()
|
||||
sess.snapshot(f"Datum plane body #{body_index}")
|
||||
proj = sess.get_project()
|
||||
result = body_mod.datum_plane(proj, body_index, offset=offset, reference=reference)
|
||||
att_refs = [r.strip() for r in attachment_refs.split(",")] if attachment_refs else None
|
||||
result = body_mod.datum_plane(proj, body_index, offset=offset, reference=reference,
|
||||
attachment_mode=attachment_mode,
|
||||
attachment_refs=att_refs)
|
||||
output_fn(result, "Added datum plane")
|
||||
|
||||
|
||||
@@ -1650,29 +1693,45 @@ def body_datum_plane(body_index: int, offset: float, reference: str) -> None:
|
||||
@click.argument("body_index", type=int)
|
||||
@click.option("--point", default="0,0,0", help="Base point x,y,z.")
|
||||
@click.option("--direction", "-d", default="0,0,1", help="Direction x,y,z.")
|
||||
@click.option("--attachment-mode", type=str, default=None, help="Attachment mode (FreeCAD 1.1).")
|
||||
@click.option("--attachment-refs", type=str, default=None,
|
||||
help="Comma-separated attachment references (FreeCAD 1.1).")
|
||||
@handle_error
|
||||
def body_datum_line(body_index: int, point: str, direction: str) -> None:
|
||||
def body_datum_line(body_index: int, point: str, direction: str,
|
||||
attachment_mode: Optional[str],
|
||||
attachment_refs: Optional[str]) -> None:
|
||||
"""Add a datum line to a body."""
|
||||
sess = get_session()
|
||||
sess.snapshot(f"Datum line body #{body_index}")
|
||||
proj = sess.get_project()
|
||||
pt = _parse_vec3(point)
|
||||
d = _parse_vec3(direction)
|
||||
result = body_mod.datum_line(proj, body_index, point=pt, direction=d)
|
||||
att_refs = [r.strip() for r in attachment_refs.split(",")] if attachment_refs else None
|
||||
result = body_mod.datum_line(proj, body_index, point=pt, direction=d,
|
||||
attachment_mode=attachment_mode,
|
||||
attachment_refs=att_refs)
|
||||
output_fn(result, "Added datum line")
|
||||
|
||||
|
||||
@body_group.command("datum-point")
|
||||
@click.argument("body_index", type=int)
|
||||
@click.option("--position", "-p", default="0,0,0", help="Position x,y,z.")
|
||||
@click.option("--attachment-mode", type=str, default=None, help="Attachment mode (FreeCAD 1.1).")
|
||||
@click.option("--attachment-refs", type=str, default=None,
|
||||
help="Comma-separated attachment references (FreeCAD 1.1).")
|
||||
@handle_error
|
||||
def body_datum_point(body_index: int, position: str) -> None:
|
||||
def body_datum_point(body_index: int, position: str,
|
||||
attachment_mode: Optional[str],
|
||||
attachment_refs: Optional[str]) -> None:
|
||||
"""Add a datum point to a body."""
|
||||
sess = get_session()
|
||||
sess.snapshot(f"Datum point body #{body_index}")
|
||||
proj = sess.get_project()
|
||||
pos = _parse_vec3(position)
|
||||
result = body_mod.datum_point(proj, body_index, position=pos)
|
||||
att_refs = [r.strip() for r in attachment_refs.split(",")] if attachment_refs else None
|
||||
result = body_mod.datum_point(proj, body_index, position=pos,
|
||||
attachment_mode=attachment_mode,
|
||||
attachment_refs=att_refs)
|
||||
output_fn(result, "Added datum point")
|
||||
|
||||
|
||||
@@ -1692,6 +1751,38 @@ def body_shape_binder(body_index: int, source_body_index: int,
|
||||
output_fn(result, "Added shape binder")
|
||||
|
||||
|
||||
@body_group.command("local-coordinate-system")
|
||||
@click.argument("body_index", type=int)
|
||||
@click.option("--position", default=None, help="Position as x,y,z")
|
||||
@click.option("--x-axis", default=None, help="X axis direction as x,y,z")
|
||||
@click.option("--y-axis", default=None, help="Y axis direction as x,y,z")
|
||||
@click.option("--z-axis", default=None, help="Z axis direction as x,y,z")
|
||||
@handle_error
|
||||
def body_local_coordinate_system(body_index, position, x_axis, y_axis, z_axis):
|
||||
"""Add a local coordinate system to a body (FreeCAD 1.1)."""
|
||||
sess = get_session()
|
||||
proj = sess.get_project()
|
||||
pos = _parse_vec3(position) if position else None
|
||||
xa = _parse_vec3(x_axis) if x_axis else None
|
||||
ya = _parse_vec3(y_axis) if y_axis else None
|
||||
za = _parse_vec3(z_axis) if z_axis else None
|
||||
result = body_mod.local_coordinate_system(proj, body_index, pos, xa, ya, za)
|
||||
output_fn(result, "Local coordinate system added.")
|
||||
|
||||
|
||||
@body_group.command("toggle-freeze")
|
||||
@click.argument("body_index", type=int)
|
||||
@click.argument("feature_index", type=int)
|
||||
@handle_error
|
||||
def body_toggle_freeze(body_index, feature_index):
|
||||
"""Toggle frozen state of a feature (FreeCAD 1.1)."""
|
||||
sess = get_session()
|
||||
proj = sess.get_project()
|
||||
result = body_mod.toggle_freeze(proj, body_index, feature_index)
|
||||
state = "frozen" if result.get("frozen") else "unfrozen"
|
||||
output_fn(result, f"Feature {feature_index} is now {state}.")
|
||||
|
||||
|
||||
# ── Material commands ────────────────────────────────────────────────
|
||||
|
||||
@cli.group("material")
|
||||
@@ -2040,12 +2131,16 @@ def measure_inertia(index: int) -> None:
|
||||
|
||||
@measure_group.command("check-geometry")
|
||||
@click.argument("index", type=int)
|
||||
@click.option("--include-valid", is_flag=True, help="Include valid shape entries in report.")
|
||||
@click.option("--skip", default=None, type=str, help="Comma-separated part indices to skip.")
|
||||
@handle_error
|
||||
def measure_check_geometry(index: int) -> None:
|
||||
def measure_check_geometry(index: int, include_valid: bool, skip: Optional[str]) -> None:
|
||||
"""Perform geometry validation on a part."""
|
||||
sess = get_session()
|
||||
proj = sess.get_project()
|
||||
result = measure_mod.check_geometry(proj, index)
|
||||
skip_list = [int(x.strip()) for x in skip.split(",")] if skip else None
|
||||
result = measure_mod.check_geometry(proj, index, include_valid=include_valid,
|
||||
skip_objects=skip_list)
|
||||
output_fn(result, "Geometry check:")
|
||||
|
||||
|
||||
@@ -2527,15 +2622,17 @@ def draft_text(text_content: str, name: Optional[str],
|
||||
@click.argument("font_file", type=str)
|
||||
@click.option("--size", default=10.0, type=float, help="Font size.")
|
||||
@click.option("--name", "-n", help="Object name.")
|
||||
@click.option("--relative-font-path", is_flag=True, help="Use relative font path.")
|
||||
@handle_error
|
||||
def draft_shapestring(text_content: str, font_file: str, size: float,
|
||||
name: Optional[str]) -> None:
|
||||
name: Optional[str], relative_font_path: bool) -> None:
|
||||
"""Create a ShapeString."""
|
||||
sess = get_session()
|
||||
sess.snapshot("Draft shapestring")
|
||||
proj = sess.get_project()
|
||||
result = draft_mod.draft_shapestring(proj, text=text_content,
|
||||
font_file=font_file, size=size, name=name)
|
||||
font_file=font_file, size=size, name=name,
|
||||
font_path_relative=relative_font_path)
|
||||
output_fn(result, f"Created shapestring: {result.get('name', '')}")
|
||||
|
||||
|
||||
@@ -2831,13 +2928,15 @@ def draft_extrude(index: int, vector: Optional[str], name: Optional[str]) -> Non
|
||||
@draft_group.command("fillet-2d")
|
||||
@click.argument("index", type=int)
|
||||
@click.option("--radius", "-r", default=1.0, type=float, help="Fillet radius.")
|
||||
@click.option("--edges", default=None, type=str, help="Comma-separated edge indices to fillet.")
|
||||
@handle_error
|
||||
def draft_fillet_2d(index: int, radius: float) -> None:
|
||||
def draft_fillet_2d(index: int, radius: float, edges: Optional[str]) -> None:
|
||||
"""Apply a 2D fillet to a draft object."""
|
||||
sess = get_session()
|
||||
sess.snapshot(f"Draft fillet-2d #{index}")
|
||||
proj = sess.get_project()
|
||||
result = draft_mod.draft_fillet_2d(proj, index, radius=radius)
|
||||
edge_list = [int(e) for e in edges.split(",")] if edges else None
|
||||
result = draft_mod.draft_fillet_2d(proj, index, radius=radius, edges=edge_list)
|
||||
output_fn(result, f"Fillet-2D: {result.get('name', '')}")
|
||||
|
||||
|
||||
@@ -3320,6 +3419,52 @@ def assembly_collapse(asm_index: int) -> None:
|
||||
output_fn(result, "Assembly collapsed")
|
||||
|
||||
|
||||
@assembly_group.command("insert-part")
|
||||
@click.argument("asm_index", type=int)
|
||||
@click.option("--type", "part_type", default="box", help="Part type to insert")
|
||||
@click.option("--name", default=None, help="Part name")
|
||||
@click.option("-P", "--param", multiple=True, help="Parameters as key=value")
|
||||
@click.option("--transform", default=None, help="Transform as x,y,z")
|
||||
@handle_error
|
||||
def assembly_insert_part(asm_index, part_type, name, param, transform):
|
||||
"""Insert a new inline part into an assembly (FreeCAD 1.1)."""
|
||||
sess = get_session()
|
||||
proj = sess.get_project()
|
||||
params = _parse_params(param) if param else None
|
||||
t = _parse_vec3(transform) if transform else None
|
||||
result = asm_mod.insert_new_part(proj, asm_index, part_type, name, params, t)
|
||||
output_fn(result, "Part inserted into assembly.")
|
||||
|
||||
|
||||
@assembly_group.command("create-simulation")
|
||||
@click.argument("asm_index", type=int)
|
||||
@click.option("--name", default=None, help="Simulation name")
|
||||
@click.option("--duration", type=float, default=5.0, help="Duration in seconds")
|
||||
@click.option("--fps", type=int, default=24, help="Frames per second")
|
||||
@handle_error
|
||||
def assembly_create_simulation(asm_index, name, duration, fps):
|
||||
"""Create a joint motion simulation (FreeCAD 1.1)."""
|
||||
sess = get_session()
|
||||
proj = sess.get_project()
|
||||
result = asm_mod.create_simulation(proj, asm_index, name, duration, fps)
|
||||
output_fn(result, "Simulation created.")
|
||||
|
||||
|
||||
@assembly_group.command("add-sim-step")
|
||||
@click.argument("asm_index", type=int)
|
||||
@click.argument("sim_index", type=int)
|
||||
@click.option("--joint", type=int, required=True, help="Joint index")
|
||||
@click.option("--start", type=float, default=0.0, help="Start value")
|
||||
@click.option("--end", type=float, default=1.0, help="End value")
|
||||
@handle_error
|
||||
def assembly_add_sim_step(asm_index, sim_index, joint, start, end):
|
||||
"""Add a motion step to a simulation (FreeCAD 1.1)."""
|
||||
sess = get_session()
|
||||
proj = sess.get_project()
|
||||
result = asm_mod.add_simulation_step(proj, asm_index, sim_index, joint, start, end)
|
||||
output_fn(result, "Simulation step added.")
|
||||
|
||||
|
||||
# ── TechDraw commands ────────────────────────────────────────────────
|
||||
|
||||
@cli.group("techdraw")
|
||||
@@ -3454,15 +3599,19 @@ def techdraw_add_dimension(page_index: int, view_index: int, dim_type: str,
|
||||
@click.argument("page_index", type=int)
|
||||
@click.argument("text_content", type=str)
|
||||
@click.option("--position", help="Position x,y.")
|
||||
@click.option("--area", is_flag=True, help="Compute area accounting for face holes.")
|
||||
@click.option("--validate-shape", is_flag=True, default=False, help="Enable shape validation.")
|
||||
@handle_error
|
||||
def techdraw_add_annotation(page_index: int, text_content: str,
|
||||
position: Optional[str]) -> None:
|
||||
position: Optional[str], area: bool,
|
||||
validate_shape: bool) -> None:
|
||||
"""Add a text annotation to a page."""
|
||||
sess = get_session()
|
||||
sess.snapshot(f"Add annotation to page #{page_index}")
|
||||
proj = sess.get_project()
|
||||
p = _parse_vec2(position) if position else None
|
||||
result = td_mod.add_annotation(proj, page_index, text_content, position=p)
|
||||
result = td_mod.add_annotation(proj, page_index, text_content, position=p,
|
||||
area_mode=area, shape_validation=validate_shape)
|
||||
output_fn(result, "Added annotation")
|
||||
|
||||
|
||||
@@ -3694,16 +3843,32 @@ def fem_set_material(ai: int, material_index: int) -> None:
|
||||
@click.option("--max-size", type=float, help="Max element size.")
|
||||
@click.option("--min-size", type=float, help="Min element size.")
|
||||
@click.option("--element-type", default="Tet10", help="Element type.")
|
||||
@click.option("--mesher", type=click.Choice(["gmsh", "netgen"]), default="gmsh",
|
||||
help="Mesher backend (FreeCAD 1.1).")
|
||||
@click.option("--gmsh-verbosity", type=int, default=1,
|
||||
help="Gmsh verbosity level (FreeCAD 1.1).")
|
||||
@click.option("--second-order-linear", is_flag=True,
|
||||
help="Second order linear elements (FreeCAD 1.1).")
|
||||
@click.option("--local-refinement", type=str, default=None,
|
||||
help="Local refinement as JSON string (FreeCAD 1.1).")
|
||||
@handle_error
|
||||
def fem_mesh_generate(ai: int, max_size: Optional[float],
|
||||
min_size: Optional[float], element_type: str) -> None:
|
||||
min_size: Optional[float], element_type: str,
|
||||
mesher: str, gmsh_verbosity: int,
|
||||
second_order_linear: bool,
|
||||
local_refinement: Optional[str]) -> None:
|
||||
"""Configure mesh generation for an analysis."""
|
||||
sess = get_session()
|
||||
sess.snapshot(f"Generate FEM mesh for analysis #{ai}")
|
||||
proj = sess.get_project()
|
||||
lr = json.loads(local_refinement) if local_refinement else None
|
||||
result = fem_mod.generate_fem_mesh(proj, ai, max_size=max_size,
|
||||
min_size=min_size,
|
||||
element_type=element_type)
|
||||
element_type=element_type,
|
||||
mesher=mesher,
|
||||
gmsh_verbosity=gmsh_verbosity,
|
||||
second_order_linear=second_order_linear,
|
||||
local_refinement=lr)
|
||||
output_fn(result, "Mesh parameters set")
|
||||
|
||||
|
||||
@@ -3711,13 +3876,20 @@ def fem_mesh_generate(ai: int, max_size: Optional[float],
|
||||
@click.argument("ai", type=int)
|
||||
@click.option("--solver", default="calculix",
|
||||
type=click.Choice(["calculix", "elmer", "z88"]))
|
||||
@click.option("--output-format", type=click.Choice(["vtu", "vtk", "result"]),
|
||||
default=None, help="Output format (FreeCAD 1.1).")
|
||||
@click.option("--buckling-accuracy", type=float, default=None,
|
||||
help="Buckling accuracy (FreeCAD 1.1).")
|
||||
@handle_error
|
||||
def fem_solve(ai: int, solver: str) -> None:
|
||||
def fem_solve(ai: int, solver: str, output_format: Optional[str],
|
||||
buckling_accuracy: Optional[float]) -> None:
|
||||
"""Solve a FEM analysis."""
|
||||
sess = get_session()
|
||||
sess.snapshot(f"Solve analysis #{ai}")
|
||||
proj = sess.get_project()
|
||||
result = fem_mod.solve_fem(proj, ai, solver=solver)
|
||||
result = fem_mod.solve_fem(proj, ai, solver=solver,
|
||||
output_format=output_format,
|
||||
buckling_accuracy=buckling_accuracy)
|
||||
output_fn(result, "Analysis solver configured")
|
||||
|
||||
|
||||
@@ -3746,6 +3918,64 @@ def fem_export_results(ai: int, path: str, fmt: str) -> None:
|
||||
output_fn(result, f"Exported results: {path}")
|
||||
|
||||
|
||||
@fem_group.command("add-beam-section")
|
||||
@click.argument("analysis_index", type=int)
|
||||
@click.option("--section-type", type=click.Choice(["rectangular", "circular",
|
||||
"box_beam", "elliptical", "pipe"]), default="rectangular")
|
||||
@click.option("--references", default=None, help="Comma-separated geometry refs")
|
||||
@click.option("--width", type=float, default=None)
|
||||
@click.option("--height", type=float, default=None)
|
||||
@click.option("--radius", type=float, default=None)
|
||||
@handle_error
|
||||
def fem_add_beam_section(analysis_index, section_type, references, width, height, radius):
|
||||
"""Add an ElementGeometry1D beam section (FreeCAD 1.1)."""
|
||||
sess = get_session()
|
||||
proj = sess.get_project()
|
||||
refs = references.split(",") if references else None
|
||||
result = fem_mod.add_beam_section(proj, analysis_index, section_type, refs,
|
||||
width, height, radius)
|
||||
output_fn(result, "Beam section added.")
|
||||
|
||||
|
||||
@fem_group.command("add-tie")
|
||||
@click.argument("analysis_index", type=int)
|
||||
@click.option("--master-refs", required=True, help="Comma-separated master refs")
|
||||
@click.option("--slave-refs", required=True, help="Comma-separated slave refs")
|
||||
@handle_error
|
||||
def fem_add_tie(analysis_index, master_refs, slave_refs):
|
||||
"""Add a tie constraint between shell faces (FreeCAD 1.1)."""
|
||||
sess = get_session()
|
||||
proj = sess.get_project()
|
||||
result = fem_mod.add_tie_constraint(proj, analysis_index,
|
||||
master_refs.split(","),
|
||||
slave_refs.split(","))
|
||||
output_fn(result, "Tie constraint added.")
|
||||
|
||||
|
||||
@fem_group.command("purge-results")
|
||||
@click.argument("analysis_index", type=int)
|
||||
@handle_error
|
||||
def fem_purge_results(analysis_index):
|
||||
"""Delete all result objects from an analysis (FreeCAD 1.1)."""
|
||||
sess = get_session()
|
||||
proj = sess.get_project()
|
||||
result = fem_mod.purge_results(proj, analysis_index)
|
||||
output_fn(result, "Results purged.")
|
||||
|
||||
|
||||
@fem_group.command("suppress")
|
||||
@click.argument("analysis_index", type=int)
|
||||
@click.argument("constraint_index", type=int)
|
||||
@handle_error
|
||||
def fem_suppress(analysis_index, constraint_index):
|
||||
"""Toggle suppressed state on a constraint (FreeCAD 1.1)."""
|
||||
sess = get_session()
|
||||
proj = sess.get_project()
|
||||
result = fem_mod.suppress_object(proj, analysis_index, constraint_index)
|
||||
state = "suppressed" if result.get("suppressed") else "active"
|
||||
output_fn(result, f"Constraint is now {state}.")
|
||||
|
||||
|
||||
# ── CAM commands ─────────────────────────────────────────────────────
|
||||
|
||||
@cli.group("cam")
|
||||
@@ -3791,15 +4021,19 @@ def cam_set_stock(job_index: int, stock_type: str, extra_x: float,
|
||||
@click.option("--faces", default="all", help="Face selection.")
|
||||
@click.option("--depth", type=float, help="Cut depth.")
|
||||
@click.option("--step-down", default=1.0, type=float, help="Step-down per pass.")
|
||||
@click.option("--passes", type=int, default=None, help="Number of passes (FreeCAD 1.1).")
|
||||
@click.option("--finishing-pass", is_flag=True, help="Add finishing pass (FreeCAD 1.1).")
|
||||
@handle_error
|
||||
def cam_add_profile(job_index: int, faces: str, depth: Optional[float],
|
||||
step_down: float) -> None:
|
||||
step_down: float, passes: Optional[int],
|
||||
finishing_pass: bool) -> None:
|
||||
"""Add a profile (contour) operation."""
|
||||
sess = get_session()
|
||||
sess.snapshot(f"Add profile to job #{job_index}")
|
||||
proj = sess.get_project()
|
||||
result = cam_mod.add_profile_op(proj, job_index, faces=faces, depth=depth,
|
||||
step_down=step_down)
|
||||
step_down=step_down, passes=passes,
|
||||
finishing_pass=finishing_pass)
|
||||
output_fn(result, "Added profile operation")
|
||||
|
||||
|
||||
@@ -3861,15 +4095,19 @@ def cam_add_facing(job_index: int, depth: float, step_over: float) -> None:
|
||||
@click.option("--type", "tool_type", default="endmill",
|
||||
type=click.Choice(["endmill", "ballnose", "drill", "chamfer",
|
||||
"vbit", "facemill"]))
|
||||
@click.option("--material", type=str, default=None, help="Tool material (FreeCAD 1.1).")
|
||||
@click.option("--coating", type=str, default=None, help="Tool coating (FreeCAD 1.1).")
|
||||
@handle_error
|
||||
def cam_set_tool(job_index: int, tool_number: int, diameter: float,
|
||||
flutes: int, tool_type: str) -> None:
|
||||
flutes: int, tool_type: str, material: Optional[str],
|
||||
coating: Optional[str]) -> None:
|
||||
"""Define a cutting tool."""
|
||||
sess = get_session()
|
||||
sess.snapshot(f"Set tool on job #{job_index}")
|
||||
proj = sess.get_project()
|
||||
result = cam_mod.set_tool(proj, job_index, tool_number=tool_number,
|
||||
diameter=diameter, flutes=flutes, type=tool_type)
|
||||
diameter=diameter, flutes=flutes, type=tool_type,
|
||||
material=material, coating=coating)
|
||||
output_fn(result, f"Tool T{tool_number} defined")
|
||||
|
||||
|
||||
@@ -3908,6 +4146,45 @@ def cam_export_gcode(job_index: int, path: str) -> None:
|
||||
output_fn(result, f"Exported G-code: {path}")
|
||||
|
||||
|
||||
@cam_group.command("add-tapping")
|
||||
@click.argument("job_index", type=int)
|
||||
@click.option("--holes", default="all", help="Hole selection")
|
||||
@click.option("--depth", type=float, default=None, help="Tapping depth")
|
||||
@click.option("--thread-pitch", type=float, default=1.5, help="Thread pitch")
|
||||
@click.option("--left-hand", is_flag=True, help="Use G74 left-hand tapping")
|
||||
@handle_error
|
||||
def cam_add_tapping(job_index, holes, depth, thread_pitch, left_hand):
|
||||
"""Add a tapping operation G84/G74 (FreeCAD 1.1)."""
|
||||
sess = get_session()
|
||||
proj = sess.get_project()
|
||||
result = cam_mod.add_tapping_op(proj, job_index, holes, depth, thread_pitch, not left_hand)
|
||||
output_fn(result, "Tapping operation added.")
|
||||
|
||||
|
||||
@cam_group.command("import-tool-library")
|
||||
@click.argument("job_index", type=int)
|
||||
@click.argument("path", type=click.Path())
|
||||
@handle_error
|
||||
def cam_import_tool_library(job_index, path):
|
||||
"""Import a FreeCAD 1.1 tool library file."""
|
||||
sess = get_session()
|
||||
proj = sess.get_project()
|
||||
result = cam_mod.import_tool_library(proj, job_index, path)
|
||||
output_fn(result, "Tool library imported.")
|
||||
|
||||
|
||||
@cam_group.command("export-tool-library")
|
||||
@click.argument("job_index", type=int)
|
||||
@click.argument("path", type=click.Path())
|
||||
@handle_error
|
||||
def cam_export_tool_library(job_index, path):
|
||||
"""Export CAM job tool library."""
|
||||
sess = get_session()
|
||||
proj = sess.get_project()
|
||||
result = cam_mod.export_tool_library(proj, job_index, path)
|
||||
output_fn(result, "Tool library exported.")
|
||||
|
||||
|
||||
# ── REPL ─────────────────────────────────────────────────────────────
|
||||
|
||||
@cli.command("repl")
|
||||
@@ -3931,8 +4208,8 @@ def repl(project_path: Optional[str]) -> None:
|
||||
_repl_commands = {
|
||||
"document": "new|open|save|info|profiles",
|
||||
"part": "add|remove|list|get|transform|boolean|copy|mirror|scale|offset|thickness|compound|explode|fillet-3d|chamfer-3d|loft|sweep|revolve|extrude|section|slice|line-3d|wire|polygon-3d|info",
|
||||
"sketch": "new|add-line|add-circle|add-rect|add-arc|constrain|close|list|get|add-point|add-ellipse|add-polygon|add-bspline|add-slot|edit-element|remove-element|remove-constraint|edit-constraint|mirror|offset|trim|extend|validate|solve-status|set-construction|project-external",
|
||||
"body": "new|pad|pocket|fillet|chamfer|revolution|list|get|groove|additive-loft|additive-pipe|additive-helix|subtractive-loft|subtractive-pipe|subtractive-helix|additive-box|additive-cylinder|additive-sphere|additive-cone|additive-torus|additive-wedge|subtractive-box|subtractive-cylinder|subtractive-sphere|subtractive-cone|subtractive-torus|subtractive-wedge|draft-feature|thickness-feature|hole|linear-pattern|polar-pattern|mirrored|multi-transform|datum-plane|datum-line|datum-point|shape-binder",
|
||||
"sketch": "new|add-line|add-circle|add-rect|add-arc|constrain|close|list|get|add-point|add-ellipse|add-polygon|add-bspline|add-slot|edit-element|remove-element|remove-constraint|edit-constraint|mirror|offset|trim|extend|validate|solve-status|set-construction|project-external|intersection|add-external-face",
|
||||
"body": "new|pad|pocket|fillet|chamfer|revolution|list|get|groove|additive-loft|additive-pipe|additive-helix|subtractive-loft|subtractive-pipe|subtractive-helix|additive-box|additive-cylinder|additive-sphere|additive-cone|additive-torus|additive-wedge|subtractive-box|subtractive-cylinder|subtractive-sphere|subtractive-cone|subtractive-torus|subtractive-wedge|draft-feature|thickness-feature|hole|linear-pattern|polar-pattern|mirrored|multi-transform|datum-plane|datum-line|datum-point|shape-binder|local-coordinate-system|toggle-freeze",
|
||||
"material": "create|assign|list|get|set|presets|import-material|export-material",
|
||||
"export": "render|info|presets",
|
||||
"session": "undo|redo|status|history",
|
||||
@@ -3942,10 +4219,10 @@ def repl(project_path: Optional[str]) -> None:
|
||||
"draft": "wire|rectangle|circle|ellipse|polygon|bspline|bezier|point|text|shapestring|dimension|label|hatch|move|rotate|scale|mirror|offset|array-linear|array-polar|array-path|copy|clone|upgrade|downgrade|trim|join|extrude|fillet-2d|to-sketch|list|get|remove",
|
||||
"surface": "filling|sections|extend|blend-curve|sew|cut",
|
||||
"import": "auto|step|iges|stl|obj|dxf|svg|brep|3mf|ply|off|gltf|info",
|
||||
"assembly": "new|add-part|remove-part|list|get|constrain|solve|dof|bom|explode|collapse",
|
||||
"assembly": "new|add-part|remove-part|list|get|constrain|solve|dof|bom|explode|collapse|insert-part|create-simulation|add-sim-step",
|
||||
"techdraw": "new-page|set-template|add-view|add-projection-group|add-section-view|add-detail-view|add-dimension|add-annotation|add-leader|add-centerline|add-hatch|export-pdf|export-svg|list-views|get-view",
|
||||
"fem": "new-analysis|add-fixed|add-force|add-pressure|add-displacement|add-temperature|add-heatflux|set-material|mesh-generate|solve|results|export-results",
|
||||
"cam": "new-job|set-stock|add-profile|add-pocket|add-drilling|add-facing|set-tool|generate-gcode|simulate|export-gcode",
|
||||
"fem": "new-analysis|add-fixed|add-force|add-pressure|add-displacement|add-temperature|add-heatflux|set-material|mesh-generate|solve|results|export-results|add-beam-section|add-tie|purge-results|suppress",
|
||||
"cam": "new-job|set-stock|add-profile|add-pocket|add-drilling|add-facing|set-tool|generate-gcode|simulate|export-gcode|add-tapping|import-tool-library|export-tool-library",
|
||||
}
|
||||
|
||||
pt_session = skin.create_prompt_session()
|
||||
|
||||
@@ -42,12 +42,18 @@ from cli_anything.freecad.core.sketch import (
|
||||
from cli_anything.freecad.core.body import (
|
||||
chamfer,
|
||||
create_body,
|
||||
datum_plane,
|
||||
datum_line,
|
||||
datum_point,
|
||||
fillet,
|
||||
get_body,
|
||||
hole_feature,
|
||||
list_bodies,
|
||||
local_coordinate_system,
|
||||
pad,
|
||||
pocket,
|
||||
revolution,
|
||||
toggle_freeze,
|
||||
)
|
||||
from cli_anything.freecad.core.materials import (
|
||||
PRESETS,
|
||||
@@ -739,3 +745,127 @@ class TestSession:
|
||||
assert "index" in entry
|
||||
assert "timestamp" in entry
|
||||
assert "description" in entry
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# TestFreeCAD11Features — New features added for FreeCAD 1.1
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
class TestFreeCAD11Features:
|
||||
"""Tests for FreeCAD 1.1 new features across modules."""
|
||||
|
||||
# -- Body: LocalCoordinateSystem --
|
||||
|
||||
def test_local_coordinate_system_default(self):
|
||||
proj = _make_project()
|
||||
body = create_body(proj, name="LCSBody")
|
||||
feat = local_coordinate_system(proj, 0)
|
||||
assert feat["type"] == "local_coordinate_system"
|
||||
assert feat["position"] == [0.0, 0.0, 0.0]
|
||||
assert feat["x_axis"] == [1.0, 0.0, 0.0]
|
||||
assert feat["y_axis"] == [0.0, 1.0, 0.0]
|
||||
assert feat["z_axis"] == [0.0, 0.0, 1.0]
|
||||
|
||||
def test_local_coordinate_system_custom_axes(self):
|
||||
proj = _make_project()
|
||||
create_body(proj, name="LCSBody2")
|
||||
feat = local_coordinate_system(
|
||||
proj, 0,
|
||||
position=[10.0, 20.0, 30.0],
|
||||
x_axis=[0.0, 1.0, 0.0],
|
||||
z_axis=[1.0, 0.0, 0.0],
|
||||
)
|
||||
assert feat["position"] == [10.0, 20.0, 30.0]
|
||||
assert feat["x_axis"] == [0.0, 1.0, 0.0]
|
||||
|
||||
def test_local_coordinate_system_invalid_body(self):
|
||||
proj = _make_project()
|
||||
with pytest.raises(IndexError):
|
||||
local_coordinate_system(proj, 99)
|
||||
|
||||
# -- Body: Datum attachment --
|
||||
|
||||
def test_datum_plane_with_attachment(self):
|
||||
proj = _make_project()
|
||||
create_body(proj, name="DatumBody")
|
||||
feat = datum_plane(proj, 0, attachment_mode="flat_face",
|
||||
attachment_refs=["Body.Face1"])
|
||||
assert feat["attachment_mode"] == "flat_face"
|
||||
assert feat["attachment_refs"] == ["Body.Face1"]
|
||||
|
||||
def test_datum_line_with_attachment(self):
|
||||
proj = _make_project()
|
||||
create_body(proj, name="DatumBody2")
|
||||
feat = datum_line(proj, 0, attachment_mode="normal_to_edge",
|
||||
attachment_refs=["Body.Edge1"])
|
||||
assert feat["attachment_mode"] == "normal_to_edge"
|
||||
|
||||
def test_datum_point_with_attachment(self):
|
||||
proj = _make_project()
|
||||
create_body(proj, name="DatumBody3")
|
||||
feat = datum_point(proj, 0, attachment_mode="translate",
|
||||
attachment_refs=["Body.Vertex1"])
|
||||
assert feat["attachment_mode"] == "translate"
|
||||
|
||||
def test_datum_invalid_attachment_mode(self):
|
||||
proj = _make_project()
|
||||
create_body(proj, name="DatumBody4")
|
||||
with pytest.raises(ValueError, match="Invalid attachment_mode"):
|
||||
datum_plane(proj, 0, attachment_mode="nonexistent_mode")
|
||||
|
||||
# -- Body: Hole Whitworth threads --
|
||||
|
||||
def test_hole_whitworth_bsw(self):
|
||||
proj = _make_project()
|
||||
create_body(proj, name="HoleBody")
|
||||
sk = create_sketch(proj)
|
||||
add_line(proj, 0, [0, 0], [10, 0])
|
||||
close_sketch(proj, 0)
|
||||
pad(proj, 0, sketch_index=0, length=10.0)
|
||||
feat = hole_feature(proj, 0, sketch_index=0, diameter=6.0, depth=10.0,
|
||||
threaded=True, thread_standard="BSW")
|
||||
assert feat["thread_standard"] == "BSW"
|
||||
|
||||
def test_hole_npt_auto_taper(self):
|
||||
proj = _make_project()
|
||||
create_body(proj, name="HoleBody2")
|
||||
sk = create_sketch(proj)
|
||||
add_line(proj, 0, [0, 0], [10, 0])
|
||||
close_sketch(proj, 0)
|
||||
pad(proj, 0, sketch_index=0, length=10.0)
|
||||
feat = hole_feature(proj, 0, sketch_index=0, diameter=6.0, depth=10.0,
|
||||
threaded=True, thread_standard="NPT", tapered=True)
|
||||
assert feat["tapered"] is True
|
||||
assert abs(feat["taper_angle"] - 1.7899) < 0.001
|
||||
|
||||
def test_hole_invalid_thread_standard(self):
|
||||
proj = _make_project()
|
||||
create_body(proj, name="HoleBody3")
|
||||
sk = create_sketch(proj)
|
||||
add_line(proj, 0, [0, 0], [10, 0])
|
||||
close_sketch(proj, 0)
|
||||
pad(proj, 0, sketch_index=0, length=10.0)
|
||||
with pytest.raises(ValueError, match="Invalid thread_standard"):
|
||||
hole_feature(proj, 0, sketch_index=0, diameter=6.0, depth=10.0,
|
||||
thread_standard="INVALID")
|
||||
|
||||
# -- Body: Toggle freeze --
|
||||
|
||||
def test_toggle_freeze(self):
|
||||
proj = _make_project()
|
||||
create_body(proj, name="FreezeBody")
|
||||
create_sketch(proj)
|
||||
add_line(proj, 0, [0, 0], [10, 0])
|
||||
close_sketch(proj, 0)
|
||||
pad(proj, 0, sketch_index=0, length=5.0)
|
||||
feat = toggle_freeze(proj, 0, 0)
|
||||
assert feat["frozen"] is True
|
||||
feat2 = toggle_freeze(proj, 0, 0)
|
||||
assert feat2["frozen"] is False
|
||||
|
||||
def test_toggle_freeze_invalid_index(self):
|
||||
proj = _make_project()
|
||||
create_body(proj, name="FreezeBody2")
|
||||
with pytest.raises(IndexError):
|
||||
toggle_freeze(proj, 0, 99)
|
||||
|
||||
+2
-2
@@ -316,9 +316,9 @@
|
||||
{
|
||||
"name": "freecad",
|
||||
"display_name": "FreeCAD",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"description": "Parametric 3D CAD modeling via FreeCAD CLI (258 commands: Part, Sketcher, PartDesign, Assembly, Mesh, TechDraw, Draft, FEM, CAM, and more)",
|
||||
"requires": "FreeCAD >= 0.21 (freecad.org)",
|
||||
"requires": "FreeCAD >= 1.1 (freecad.org)",
|
||||
"homepage": "https://www.freecad.org",
|
||||
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=freecad/agent-harness",
|
||||
"entry_point": "cli-anything-freecad",
|
||||
|
||||
Reference in New Issue
Block a user