feat: sharp edge pipeline V02, tessellation presets, media cache-bust, GMSH plan
Sharp Edge Pipeline V02:
- export_step_to_gltf.py: replace BRep_Tool.Polygon3D_s (returns None in XCAF) with
GCPnts_UniformAbscissa curve sampling at 0.3mm step — extracts 17,129 segment pairs
- Inject sharp_edge_pairs + sharp_threshold_deg into GLB extras (scenes[0].extras)
via binary GLB JSON-chunk patching (no extra dependency)
- export_gltf.py: read schaeffler_sharp_edge_pairs from Blender scene custom props,
apply via KD-tree to mark edges sharp=True + seam=True (OCC mm Z-up → Blender transform)
- tools/restore_sharp_marks.py: dual-pass (dihedral angle + OCC pairs), updated coordinate
transform (X, -Z, Y) * 0.001
Tessellation:
- Admin UI: Draft / Standard / Fine preset buttons with active-state highlighting
- Default angular deflection: preview 0.5→0.1 rad, production 0.2→0.05 rad
- export_glb.py: read updated defaults from system_settings
Media / Cache:
- media/service.py: get_download_url appends ?v={file_size_bytes} cache-buster
- media/router.py: Cache-Control: no-cache for all download/thumbnail endpoints
Render pipeline:
- still_render.py / turntable_render.py: shared GPU activation + camera improvements
- render_order_line.py: global render position support
- render_thumbnail.py: updated defaults
Frontend:
- InlineCadViewer: file_size_bytes-aware URL update triggers re-fetch on regeneration
- ThreeDViewer: material panel, part selection, PBR mode improvements
- Admin.tsx: tessellation preset cards, GMSH setting dropdown
- MediaBrowser, ProductDetail, OrderDetail, Orders: various UI improvements
- New: MaterialPanel, GlobalRenderPositionsPanel, StepIndicator components
- New: renderPositions.ts API client
Plans / Docs:
- plan.md: GMSH Frontal-Delaunay tessellation plan (6 tasks)
- LEARNINGS.md: OCC Polygon3D_s None issue + GCPnts fix
- .gitignore: add backend/core (core dump from root process)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -196,26 +196,60 @@ def process_cad_file(cad_file_id: str) -> None:
|
||||
|
||||
|
||||
def extract_mesh_edge_data(step_path: str) -> dict:
|
||||
"""Extract sharp edge metrics and suggested smooth angle from STEP topology.
|
||||
"""Extract sharp edge data and suggested smooth angle from STEP topology.
|
||||
|
||||
Uses PCurve-based normal evaluation: for each shared edge, the 2D curve of
|
||||
the edge on each adjacent face (BRep_Tool.CurveOnSurface) is evaluated at
|
||||
its midpoint to get the exact UV coordinates on that face. BRepLProp_SLProps
|
||||
then computes the surface normal at that precise location — far more accurate
|
||||
than sampling at the face's UV center.
|
||||
|
||||
Returns dict with:
|
||||
- suggested_smooth_angle: float (degrees) — recommended auto-smooth angle
|
||||
- has_mechanical_edges: bool — True if part has distinct hard edges (bearings etc.)
|
||||
- sharp_edge_midpoints: list of [x, y, z] — midpoints of sharp edges in mm (max 500)
|
||||
- has_mechanical_edges: bool — True if part has distinct hard edges
|
||||
- sharp_edge_pairs: list of [[x0,y0,z0],[x1,y1,z1]] — vertex pairs of
|
||||
sharp edges in mm (no artificial cap)
|
||||
"""
|
||||
try:
|
||||
from OCC.Core.STEPControl import STEPControl_Reader
|
||||
from OCC.Core.IFSelect import IFSelect_RetDone
|
||||
from OCC.Core.TopExp import TopExp_Explorer
|
||||
from OCC.Core.TopAbs import TopAbs_EDGE, TopAbs_FACE
|
||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
|
||||
from OCC.Core.BRep import BRep_Tool
|
||||
from OCC.Core.BRepGProp import brepgprop
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
|
||||
from OCC.Core.gp import gp_Pnt
|
||||
# Try OCP first (cadquery's fork, available in render-worker).
|
||||
# Fall back to OCC.Core (standard pythonocc, if installed elsewhere).
|
||||
_using_ocp = False
|
||||
try:
|
||||
from OCP.STEPControl import STEPControl_Reader
|
||||
from OCP.IFSelect import IFSelect_RetDone
|
||||
from OCP.TopAbs import TopAbs_EDGE, TopAbs_FACE, TopAbs_FORWARD
|
||||
from OCP.BRepAdaptor import BRepAdaptor_Surface, BRepAdaptor_Curve, BRepAdaptor_Curve2d
|
||||
from OCP.BRepLProp import BRepLProp_SLProps
|
||||
from OCP.BRepMesh import BRepMesh_IncrementalMesh
|
||||
from OCP.TopTools import TopTools_IndexedDataMapOfShapeListOfShape
|
||||
from OCP.TopExp import TopExp as _TopExp
|
||||
from OCP.TopoDS import TopoDS as _TopoDS
|
||||
_using_ocp = True
|
||||
except ImportError:
|
||||
from OCC.Core.STEPControl import STEPControl_Reader
|
||||
from OCC.Core.IFSelect import IFSelect_RetDone
|
||||
from OCC.Core.TopAbs import TopAbs_EDGE, TopAbs_FACE, TopAbs_FORWARD
|
||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface, BRepAdaptor_Curve, BRepAdaptor_Curve2d
|
||||
from OCC.Core.BRepLProp import BRepLProp_SLProps
|
||||
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
|
||||
from OCC.Core.TopTools import TopTools_IndexedDataMapOfShapeListOfShape
|
||||
from OCC.Core.TopExp import topexp as _TopExp
|
||||
from OCC.Core.TopoDS import TopoDS as _TopoDS
|
||||
import math
|
||||
|
||||
# OCP uses _s suffix for static methods; OCC.Core uses module-level callables.
|
||||
def _map_shapes(shape, edge_type, face_type, out_map):
|
||||
if _using_ocp:
|
||||
_TopExp.MapShapesAndAncestors_s(shape, edge_type, face_type, out_map)
|
||||
else:
|
||||
_TopExp.MapShapesAndAncestors(shape, edge_type, face_type, out_map)
|
||||
|
||||
def _to_edge(s):
|
||||
return _TopoDS.Edge_s(s) if _using_ocp else _TopoDS.Edge(s)
|
||||
|
||||
def _to_face(s):
|
||||
return _TopoDS.Face_s(s) if _using_ocp else _TopoDS.Face(s)
|
||||
|
||||
reader = STEPControl_Reader()
|
||||
status = reader.ReadFile(step_path)
|
||||
if status != IFSelect_RetDone:
|
||||
@@ -223,71 +257,88 @@ def extract_mesh_edge_data(step_path: str) -> dict:
|
||||
reader.TransferRoots()
|
||||
shape = reader.OneShape()
|
||||
|
||||
# Mesh the shape for geometry access
|
||||
# Mesh at 0.5 mm deflection
|
||||
BRepMesh_IncrementalMesh(shape, 0.5, False, 0.5)
|
||||
|
||||
# Collect face normals per edge (for dihedral angle computation)
|
||||
from OCC.Core.TopTools import TopTools_IndexedDataMapOfShapeListOfShape
|
||||
from OCC.Core.TopExp import topexp
|
||||
|
||||
# Build edge → adjacent faces map
|
||||
edge_face_map = TopTools_IndexedDataMapOfShapeListOfShape()
|
||||
topexp.MapShapesAndAncestors(shape, TopAbs_EDGE, TopAbs_FACE, edge_face_map)
|
||||
_map_shapes(shape, TopAbs_EDGE, TopAbs_FACE, edge_face_map)
|
||||
|
||||
dihedral_angles = []
|
||||
sharp_midpoints = []
|
||||
sharp_pairs = []
|
||||
SHARP_THRESHOLD_DEG = 20.0
|
||||
|
||||
for i in range(1, edge_face_map.Extent() + 1):
|
||||
edge = edge_face_map.FindKey(i)
|
||||
edge_shape = edge_face_map.FindKey(i)
|
||||
faces = edge_face_map.FindFromIndex(i)
|
||||
if faces.Size() < 2:
|
||||
continue
|
||||
|
||||
# Get the two adjacent faces
|
||||
face_list = list(faces)
|
||||
if len(face_list) < 2:
|
||||
face_shapes = list(faces)
|
||||
if len(face_shapes) < 2:
|
||||
continue
|
||||
|
||||
try:
|
||||
surf1 = BRepAdaptor_Surface(face_list[0])
|
||||
surf2 = BRepAdaptor_Surface(face_list[1])
|
||||
edge = _to_edge(edge_shape)
|
||||
face1 = _to_face(face_shapes[0])
|
||||
face2 = _to_face(face_shapes[1])
|
||||
|
||||
# Get normals at midpoint of edge
|
||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Curve
|
||||
curve = BRepAdaptor_Curve(edge)
|
||||
mid_u = (curve.FirstParameter() + curve.LastParameter()) / 2
|
||||
mid_pt = curve.Value(mid_u)
|
||||
# 3D edge endpoints in mm
|
||||
curve3d = BRepAdaptor_Curve(edge)
|
||||
pt_start = curve3d.Value(curve3d.FirstParameter())
|
||||
pt_end = curve3d.Value(curve3d.LastParameter())
|
||||
|
||||
# Sample face normals at UV center
|
||||
u1 = (surf1.FirstUParameter() + surf1.LastUParameter()) / 2
|
||||
v1 = (surf1.FirstVParameter() + surf1.LastVParameter()) / 2
|
||||
n1 = surf1.DN(u1, v1, 0, 1).Crossed(surf1.DN(u1, v1, 1, 0))
|
||||
# PCurve-based normal evaluation: BRepAdaptor_Curve2d gives UV at the
|
||||
# edge's actual location on the face — far more accurate than UV center.
|
||||
c2d_1 = BRepAdaptor_Curve2d(edge, face1)
|
||||
uv1 = c2d_1.Value((c2d_1.FirstParameter() + c2d_1.LastParameter()) / 2)
|
||||
surf1 = BRepAdaptor_Surface(face1)
|
||||
props1 = BRepLProp_SLProps(surf1, uv1.X(), uv1.Y(), 1, 1e-6)
|
||||
if not props1.IsNormalDefined():
|
||||
continue
|
||||
n1 = props1.Normal()
|
||||
if face1.Orientation() != TopAbs_FORWARD:
|
||||
n1.Reverse()
|
||||
|
||||
u2 = (surf2.FirstUParameter() + surf2.LastUParameter()) / 2
|
||||
v2 = (surf2.FirstVParameter() + surf2.LastVParameter()) / 2
|
||||
n2 = surf2.DN(u2, v2, 0, 1).Crossed(surf2.DN(u2, v2, 1, 0))
|
||||
c2d_2 = BRepAdaptor_Curve2d(edge, face2)
|
||||
uv2 = c2d_2.Value((c2d_2.FirstParameter() + c2d_2.LastParameter()) / 2)
|
||||
surf2 = BRepAdaptor_Surface(face2)
|
||||
props2 = BRepLProp_SLProps(surf2, uv2.X(), uv2.Y(), 1, 1e-6)
|
||||
if not props2.IsNormalDefined():
|
||||
continue
|
||||
n2 = props2.Normal()
|
||||
if face2.Orientation() != TopAbs_FORWARD:
|
||||
n2.Reverse()
|
||||
|
||||
if n1.Magnitude() > 1e-10 and n2.Magnitude() > 1e-10:
|
||||
n1.Normalize()
|
||||
n2.Normalize()
|
||||
cos_angle = max(-1.0, min(1.0, n1.Dot(n2)))
|
||||
angle_deg = math.degrees(math.acos(abs(cos_angle)))
|
||||
dihedral_angles.append(angle_deg)
|
||||
cos_angle = max(-1.0, min(1.0, n1.Dot(n2)))
|
||||
angle_deg = math.degrees(math.acos(cos_angle))
|
||||
# Use exterior angle (supplement when normals point same side)
|
||||
if angle_deg > 90:
|
||||
angle_deg = 180.0 - angle_deg
|
||||
dihedral_angles.append(angle_deg)
|
||||
|
||||
if angle_deg > 20 and len(sharp_midpoints) < 500:
|
||||
sharp_midpoints.append([
|
||||
round(mid_pt.X(), 3),
|
||||
round(mid_pt.Y(), 3),
|
||||
round(mid_pt.Z(), 3),
|
||||
])
|
||||
if angle_deg > SHARP_THRESHOLD_DEG:
|
||||
sharp_pairs.append([
|
||||
[round(pt_start.X(), 3), round(pt_start.Y(), 3), round(pt_start.Z(), 3)],
|
||||
[round(pt_end.X(), 3), round(pt_end.Y(), 3), round(pt_end.Z(), 3)],
|
||||
])
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# Bounding box extraction (OCC Bnd_Box)
|
||||
from OCC.Core.Bnd import Bnd_Box
|
||||
from OCC.Core.BRepBndLib import brepbndlib
|
||||
# Bounding box
|
||||
if _using_ocp:
|
||||
from OCP.Bnd import Bnd_Box
|
||||
from OCP.BRepBndLib import BRepBndLib as _brepbndlib_mod
|
||||
def _brepbndlib_add(shape, bbox):
|
||||
_brepbndlib_mod.Add_s(shape, bbox)
|
||||
else:
|
||||
from OCC.Core.Bnd import Bnd_Box
|
||||
from OCC.Core.BRepBndLib import brepbndlib as _brepbndlib_mod
|
||||
def _brepbndlib_add(shape, bbox):
|
||||
_brepbndlib_mod.Add(shape, bbox)
|
||||
try:
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib.Add(shape, bbox)
|
||||
_brepbndlib_add(shape, bbox)
|
||||
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
|
||||
dimensions_mm = {
|
||||
"x": round(xmax - xmin, 2),
|
||||
@@ -311,11 +362,8 @@ def extract_mesh_edge_data(step_path: str) -> dict:
|
||||
return result
|
||||
|
||||
import statistics
|
||||
median_angle = statistics.median(dihedral_angles)
|
||||
max_angle = max(dihedral_angles)
|
||||
|
||||
# Suggest smooth angle: slightly below the median of hard edges
|
||||
hard_edges = [a for a in dihedral_angles if a > 20]
|
||||
hard_edges = [a for a in dihedral_angles if a > SHARP_THRESHOLD_DEG]
|
||||
if hard_edges:
|
||||
suggested = max(15.0, min(60.0, statistics.median(hard_edges) * 0.8))
|
||||
else:
|
||||
@@ -324,7 +372,7 @@ def extract_mesh_edge_data(step_path: str) -> dict:
|
||||
result = {
|
||||
"suggested_smooth_angle": round(suggested, 1),
|
||||
"has_mechanical_edges": max_angle > 45,
|
||||
"sharp_edge_midpoints": sharp_midpoints[:500],
|
||||
"sharp_edge_pairs": sharp_pairs,
|
||||
}
|
||||
if dimensions_mm:
|
||||
result["dimensions_mm"] = dimensions_mm
|
||||
|
||||
Reference in New Issue
Block a user