Spaces:
Sleeping
Sleeping
File size: 7,561 Bytes
e32c964 2330e12 e32c964 e660ef7 e32c964 e660ef7 e32c964 e660ef7 2330e12 e660ef7 e32c964 2330e12 e32c964 2330e12 e32c964 e660ef7 e32c964 e660ef7 ae6029b e660ef7 ae6029b e32c964 ae6029b e32c964 e660ef7 e32c964 e660ef7 e32c964 e660ef7 e32c964 ae6029b e660ef7 ae6029b e32c964 ae6029b e32c964 e660ef7 e32c964 e660ef7 e32c964 e660ef7 e32c964 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 | """
CNC Manufacturability Validator.
Checks a CadQuery solid for common CNC machining issues:
- Thin walls
- Sharp internal corners (no fillet / too small for tool)
- Deep narrow pockets (aspect ratio)
- Overall size feasibility
- Undercut detection (basic heuristic)
"""
from typing import Optional
import cadquery as cq
from pydantic import BaseModel, Field, computed_field
class CNCIssue(BaseModel):
severity: str # "error", "warning", "info"
category: str
message: str
def __init__(self, severity: str = "", category: str = "", message: str = "", **data):
super().__init__(severity=severity, category=category, message=message, **data)
class CNCValidationResult(BaseModel):
part_name: str = Field(exclude=True)
issues: list[CNCIssue] = Field(default_factory=list)
machinable: bool = True
axis_recommendation: str = "3-axis"
@computed_field
@property
def error_count(self) -> int:
return sum(1 for i in self.issues if i.severity == "error")
@computed_field
@property
def warning_count(self) -> int:
return sum(1 for i in self.issues if i.severity == "warning")
def summary(self) -> str:
status = "PASS" if self.machinable else "FAIL"
lines = [
f"CNC Validation [{status}] — {self.part_name}",
f" Recommended: {self.axis_recommendation} milling",
f" Errors: {self.error_count} | Warnings: {self.warning_count}",
]
for issue in self.issues:
icon = {"error": "✗", "warning": "⚠", "info": "ℹ"}[issue.severity]
lines.append(f" {icon} [{issue.category}] {issue.message}")
return "\n".join(lines)
# --- Configurable thresholds ---
DEFAULT_CONFIG = {
"min_wall_thickness_mm": 1.5,
"min_fillet_radius_mm": 1.0, # Typical smallest endmill radius
"max_pocket_depth_ratio": 4.0, # depth / width ratio
"max_part_size_mm": 500.0, # Typical CNC work envelope
"min_part_size_mm": 1.0,
"min_hole_diameter_mm": 1.0,
}
def _get_validation_config(overrides: dict | None = None) -> "ValidationConfig":
"""Get validation config, optionally with overrides."""
from config.settings import settings, ValidationConfig
if overrides:
data = settings.validation.model_dump()
data.update(overrides)
return ValidationConfig(**data)
return settings.validation
def validate_for_cnc(
workplane: cq.Workplane,
part_name: str = "Part",
config: Optional[dict] = None,
) -> CNCValidationResult:
"""
Run manufacturability checks on a CadQuery solid.
Returns a CNCValidationResult with issues found.
"""
cfg = _get_validation_config(config)
result = CNCValidationResult(part_name=part_name)
shape = workplane.val()
bb = shape.BoundingBox()
# --- 1. Bounding box / size checks ---
dims = sorted([bb.xlen, bb.ylen, bb.zlen])
max_dim = dims[-1]
min_dim = dims[0]
if max_dim > cfg.max_part_size_mm:
result.issues.append(
CNCIssue(
"error",
"Size",
f"Part too large: {max_dim:.1f}mm exceeds {cfg.max_part_size_mm}mm work envelope",
)
)
result.machinable = False
if min_dim < cfg.min_part_size_mm:
result.issues.append(
CNCIssue(
"warning",
"Size",
f"Very small dimension: {min_dim:.2f}mm — may be difficult to fixture",
)
)
# --- 2. Volume sanity check ---
volume = shape.Volume()
bb_volume = bb.xlen * bb.ylen * bb.zlen
if bb_volume > 0:
fill_ratio = volume / bb_volume
if fill_ratio < 0.05:
result.issues.append(
CNCIssue(
"warning",
"Geometry",
f"Very low fill ratio ({fill_ratio:.1%}) — complex geometry, high machining time",
)
)
result.issues.append(
CNCIssue(
"info",
"Geometry",
f"Fill ratio: {fill_ratio:.1%} (volume/bounding box)",
)
)
# --- 3. Face and edge complexity ---
faces = workplane.faces().vals()
edges = workplane.edges().vals()
n_faces = len(faces)
n_edges = len(edges)
from config.settings import settings
thresholds = settings.validation.complexity_thresholds
five_axis_faces = thresholds.five_axis_faces
three_plus_two_faces = thresholds.three_plus_two_faces
if n_faces > five_axis_faces:
result.issues.append(
CNCIssue(
"warning",
"Complexity",
f"{n_faces} faces detected — may require multi-setup or 5-axis",
)
)
result.axis_recommendation = "5-axis"
elif n_faces > three_plus_two_faces:
result.issues.append(
CNCIssue(
"info",
"Complexity",
f"{n_faces} faces — consider 4-axis or indexed 5-axis",
)
)
result.axis_recommendation = "3+2 axis"
# --- 4. Edge length analysis (thin feature proxy) ---
edge_lengths = []
for edge in edges:
try:
edge_lengths.append(edge.Length())
except Exception:
pass
if edge_lengths:
min_edge = min(edge_lengths)
if min_edge < cfg.min_wall_thickness_mm:
result.issues.append(
CNCIssue(
"warning",
"Thin Feature",
f"Shortest edge: {min_edge:.2f}mm — below min wall thickness "
f"({cfg.min_wall_thickness_mm}mm)",
)
)
# --- 5. Aspect ratio check (deep pocket heuristic) ---
# Only flag if the narrowest dimension is small enough to be a pocket/slot
if dims[0] > 0 and dims[0] < 20:
aspect = dims[2] / dims[0] # tallest / narrowest
if aspect > cfg.max_pocket_depth_ratio:
result.issues.append(
CNCIssue(
"warning",
"Deep Feature",
f"Aspect ratio {aspect:.1f}:1 — may require long-reach tooling or "
f"special fixturing",
)
)
# --- 6. Surface type analysis ---
has_freeform = False
planar_count = 0
cylindrical_count = 0
for face in faces:
try:
geom_type = face.geomType()
if geom_type == "PLANE":
planar_count += 1
elif geom_type == "CYLINDER":
cylindrical_count += 1
elif geom_type in ("BSPLINE", "BEZIER", "OTHER"):
has_freeform = True
except Exception:
pass
if has_freeform:
result.issues.append(
CNCIssue(
"warning",
"Surface",
"Freeform/spline surfaces detected — requires 3D contouring toolpaths",
)
)
if result.axis_recommendation == "3-axis":
result.axis_recommendation = "3-axis (with 3D finishing)"
result.issues.append(
CNCIssue(
"info",
"Surface",
f"Faces: {planar_count} planar, {cylindrical_count} cylindrical, "
f"{n_faces - planar_count - cylindrical_count} other",
)
)
# --- 7. Set final machinable flag ---
if result.error_count > 0:
result.machinable = False
return result
|