- Added save file foramt
- Split main.py refactor
This commit is contained in:
Generated
+10
-2
@@ -6,7 +6,7 @@
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="8f0bafd6-58a0-4b20-aa2b-ddc3ba278873" name="Changes" comment="- Added save file foramt - Split main.py refactor">
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/fluency/ui/dialogs.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/ui/dialogs.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/fluency/rendering/occ_renderer.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/rendering/occ_renderer.py" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -359,7 +359,15 @@
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1783456842297</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="32" />
|
||||
<task id="LOCAL-00032" summary="- Added save file foramt - Split main.py refactor">
|
||||
<option name="closed" value="true" />
|
||||
<created>1783755278516</created>
|
||||
<option name="number" value="00032" />
|
||||
<option name="presentableId" value="LOCAL-00032" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1783755278516</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="33" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
|
||||
@@ -1047,27 +1047,44 @@ class OCCRenderer(Renderer):
|
||||
shape = self._context.DetectedShape()
|
||||
if shape is None:
|
||||
return None
|
||||
return self._classify_detected_shape(shape)
|
||||
results = self._classify_detected_shape(shape)
|
||||
if not results:
|
||||
return None
|
||||
# For cylinders with two ends, pick the one closest to the camera.
|
||||
if len(results) > 1:
|
||||
eye = None
|
||||
try:
|
||||
if self._view is not None:
|
||||
e = self._view.Eye()
|
||||
eye = np.array([e.X(), e.Y(), e.Z()], dtype=float)
|
||||
except Exception:
|
||||
pass
|
||||
if eye is not None:
|
||||
results.sort(key=lambda c: float(np.linalg.norm(
|
||||
np.array(c["position"]) - eye)))
|
||||
return results[0]
|
||||
|
||||
def _classify_detected_shape(
|
||||
self, shape: Any, owner_obj_id: Optional[str] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Classify a detected OCC sub-shape into a snap-candidate dict.
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Classify a detected OCC sub-shape into snap-candidate dicts.
|
||||
|
||||
Shared by ``pick_entity`` (single-pixel) and ``probe_snap_candidates``
|
||||
(multi-pixel grid probing). Determines whether *shape* is a planar
|
||||
face, cylindrical face (hole), edge, or vertex and returns the snap
|
||||
info dict with ``position`` / ``normal`` / ``x_dir`` / ``type`` /
|
||||
``owner_obj_id`` (+ ``radius`` for holes). When *owner_obj_id* is
|
||||
omitted it is looked up from the context's currently-detected AIS.
|
||||
face, cylindrical face (hole), edge, or vertex and returns a list of
|
||||
snap info dicts with ``position`` / ``normal`` / ``x_dir`` / ``type`` /
|
||||
``owner_obj_id`` (+ ``radius`` for holes). Cylindrical faces yield
|
||||
two candidates (one per circular end) so the user can snap to either
|
||||
opening. When *owner_obj_id* is omitted it is looked up from the
|
||||
context's currently-detected AIS.
|
||||
"""
|
||||
if shape is None:
|
||||
return None
|
||||
return []
|
||||
|
||||
from OCP.TopoDS import TopoDS_Face, TopoDS_Edge, TopoDS_Vertex, TopoDS
|
||||
from OCP.TopAbs import TopAbs_FACE, TopAbs_EDGE, TopAbs_VERTEX
|
||||
from OCP.BRepAdaptor import BRepAdaptor_Surface, BRepAdaptor_Curve
|
||||
from OCP.GeomAbs import GeomAbs_Plane, GeomAbs_Cylinder
|
||||
from OCP.GeomAbs import GeomAbs_Plane, GeomAbs_Cylinder, GeomAbs_Circle
|
||||
from OCP.BRep import BRep_Tool
|
||||
from OCP.TopExp import TopExp_Explorer
|
||||
from OCP.TopAbs import TopAbs_EDGE as TopAbs_EDGE_TYPE
|
||||
@@ -1139,14 +1156,14 @@ class OCCRenderer(Renderer):
|
||||
px = pln.XAxis().Direction()
|
||||
x_dir = (px.X(), px.Y(), px.Z())
|
||||
|
||||
return {
|
||||
return [{
|
||||
"type": "planar_face",
|
||||
"position": origin,
|
||||
"normal": (nx, ny, nz),
|
||||
"x_dir": x_dir,
|
||||
"face": face,
|
||||
"owner_obj_id": owner_obj_id,
|
||||
}
|
||||
}]
|
||||
|
||||
elif stype == GeomAbs_Cylinder:
|
||||
cyl = adaptor.Cylinder()
|
||||
@@ -1155,52 +1172,67 @@ class OCCRenderer(Renderer):
|
||||
ax_pos = axis.Location()
|
||||
radius = cyl.Radius()
|
||||
|
||||
# Parameter extents along the cylinder axis (v = height).
|
||||
# BRepAdaptor_Surface exposes these via First/Last V
|
||||
# *Parameter() — NOT a Bounds() method (that quirk crashed
|
||||
# cylindrical-face picking).
|
||||
vmin = adaptor.FirstVParameter()
|
||||
vmax = adaptor.LastVParameter()
|
||||
# Find the actual circular edge loops at each end of the
|
||||
# cylinder face. This is more reliable than computing from
|
||||
# vmin/vmax parameters, which can be offset depending on how
|
||||
# the BRep was constructed (e.g. a hole drilled into a block).
|
||||
|
||||
# Two candidate snap points: the centers of the cylinder's
|
||||
# two end circles (the hole openings). A bolt enters a
|
||||
# hole from the camera-facing opening, so pick the END of
|
||||
# the axis closest to the camera as the primary snap point.
|
||||
# The axis location (ax_pos) is already on the cylinder axis
|
||||
# at v=0; the other end is at v=vmax.
|
||||
p0 = np.array([
|
||||
ax_pos.X(), ax_pos.Y(), ax_pos.Z(),
|
||||
], dtype=float)
|
||||
p1 = np.array([
|
||||
ax_pos.X() + ax_dir.X() * (vmax - vmin),
|
||||
ax_pos.Y() + ax_dir.Y() * (vmax - vmin),
|
||||
ax_pos.Z() + ax_dir.Z() * (vmax - vmin),
|
||||
], dtype=float)
|
||||
# Collect all edges of the face.
|
||||
edge_explorer = TopExp_Explorer(face, TopAbs_EDGE_TYPE)
|
||||
circle_centers: List[np.ndarray] = []
|
||||
while edge_explorer.More():
|
||||
edge = TopoDS.Edge_s(edge_explorer.Current())
|
||||
try:
|
||||
curve_adaptor = BRepAdaptor_Curve(edge)
|
||||
if curve_adaptor.GetType() == GeomAbs_Circle:
|
||||
circ = curve_adaptor.Circle()
|
||||
center_pnt = circ.Location()
|
||||
circle_centers.append(np.array([
|
||||
center_pnt.X(), center_pnt.Y(), center_pnt.Z()
|
||||
], dtype=float))
|
||||
except Exception:
|
||||
pass
|
||||
edge_explorer.Next()
|
||||
|
||||
# Choose the camera-facing end. The camera looks FROM its
|
||||
# eye TOWARD its target, so the camera direction is
|
||||
# (target - eye). The end whose vector-from-camera is MOST
|
||||
# OPPOSITE to (i.e. faces) the camera is the near opening.
|
||||
cam_from = None
|
||||
try:
|
||||
if self._view is not None:
|
||||
eye = self._view.Eye()
|
||||
at = self._view.At()
|
||||
cam_from = np.array([eye.X(), eye.Y(), eye.Z()], dtype=float)
|
||||
cam_to = np.array([at.X(), at.Y(), at.Z()], dtype=float)
|
||||
except Exception:
|
||||
cam_from = None
|
||||
|
||||
if cam_from is not None:
|
||||
# End closest to the camera eye is the visible opening.
|
||||
d0 = float(np.linalg.norm(p0 - cam_from))
|
||||
d1 = float(np.linalg.norm(p1 - cam_from))
|
||||
near_end = p0 if d0 <= d1 else p1
|
||||
# Group circle centers by their position along the axis.
|
||||
# Two distinct groups = two end openings of the cylinder.
|
||||
if len(circle_centers) >= 2:
|
||||
# Project each center onto the axis direction to get a
|
||||
# scalar "height" value. Cluster into two groups.
|
||||
ax_dir_np = np.array([
|
||||
ax_dir.X(), ax_dir.Y(), ax_dir.Z()
|
||||
], dtype=float)
|
||||
heights = [np.dot(c, ax_dir_np) for c in circle_centers]
|
||||
# Sort by height (scalar) and split roughly in half.
|
||||
indexed = list(enumerate(heights))
|
||||
indexed.sort(key=lambda x: x[1])
|
||||
mid = len(indexed) // 2
|
||||
idx0 = [i for i, _ in indexed[:mid]] if mid > 0 else [indexed[0][0]]
|
||||
idx1 = [i for i, _ in indexed[mid:]] if mid < len(indexed) else [indexed[-1][0]]
|
||||
group0 = [circle_centers[i] for i in idx0]
|
||||
group1 = [circle_centers[i] for i in idx1]
|
||||
# Average each group to get the center of each end circle.
|
||||
c0 = np.mean(group0, axis=0)
|
||||
c1 = np.mean(group1, axis=0)
|
||||
elif len(circle_centers) == 1:
|
||||
# Only one circular edge found (e.g. open-ended cylinder).
|
||||
c0 = circle_centers[0]
|
||||
c1 = c0
|
||||
else:
|
||||
# Fallback: axial midpoint.
|
||||
near_end = 0.5 * (p0 + p1)
|
||||
# No circular edges found — fall back to parameter-based.
|
||||
vmin = adaptor.FirstVParameter()
|
||||
vmax = adaptor.LastVParameter()
|
||||
c0 = np.array([
|
||||
ax_pos.X() + ax_dir.X() * vmin,
|
||||
ax_pos.Y() + ax_dir.Y() * vmin,
|
||||
ax_pos.Z() + ax_dir.Z() * vmin,
|
||||
], dtype=float)
|
||||
c1 = np.array([
|
||||
ax_pos.X() + ax_dir.X() * vmax,
|
||||
ax_pos.Y() + ax_dir.Y() * vmax,
|
||||
ax_pos.Z() + ax_dir.Z() * vmax,
|
||||
], dtype=float)
|
||||
|
||||
origin = (float(near_end[0]), float(near_end[1]), float(near_end[2]))
|
||||
# Normal = the cylinder axis direction. This is the "bolt
|
||||
# axis": the direction a bolt would travel INTO the hole.
|
||||
# (Sign is the cylinder's own axis direction; a later flip can
|
||||
@@ -1216,15 +1248,22 @@ class OCCRenderer(Renderer):
|
||||
except Exception:
|
||||
x_dir = (1.0, 0.0, 0.0)
|
||||
|
||||
return {
|
||||
"type": "cylindrical_face",
|
||||
"position": origin,
|
||||
"normal": normal,
|
||||
"x_dir": x_dir,
|
||||
"face": face,
|
||||
"owner_obj_id": owner_obj_id,
|
||||
"radius": radius,
|
||||
}
|
||||
# Return BOTH circular ends as snap candidates so the user can
|
||||
# snap to either opening of a cylinder/hole (e.g. bolt-to-bore
|
||||
# on the far side of a part).
|
||||
results: List[Dict[str, Any]] = []
|
||||
for end_center in [c0, c1]:
|
||||
origin = (float(end_center[0]), float(end_center[1]), float(end_center[2]))
|
||||
results.append({
|
||||
"type": "cylindrical_face",
|
||||
"position": origin,
|
||||
"normal": normal,
|
||||
"x_dir": x_dir,
|
||||
"face": face,
|
||||
"owner_obj_id": owner_obj_id,
|
||||
"radius": radius,
|
||||
})
|
||||
return results
|
||||
|
||||
# Try edge.
|
||||
edge = None
|
||||
@@ -1267,14 +1306,14 @@ class OCCRenderer(Renderer):
|
||||
x = x / xlen
|
||||
x_dir = (float(x[0]), float(x[1]), float(x[2]))
|
||||
|
||||
return {
|
||||
return [{
|
||||
"type": "edge",
|
||||
"position": position,
|
||||
"normal": tangent,
|
||||
"x_dir": x_dir,
|
||||
"edge": edge,
|
||||
"owner_obj_id": owner_obj_id,
|
||||
}
|
||||
}]
|
||||
|
||||
# Try vertex.
|
||||
vertex = None
|
||||
@@ -1282,18 +1321,18 @@ class OCCRenderer(Renderer):
|
||||
vertex = TopoDS.Vertex_s(shape)
|
||||
p = BRep_Tool.Pnt_s(vertex)
|
||||
position = (p.X(), p.Y(), p.Z())
|
||||
return {
|
||||
return [{
|
||||
"type": "vertex",
|
||||
"position": position,
|
||||
"normal": None,
|
||||
"x_dir": None,
|
||||
"vertex": vertex,
|
||||
"owner_obj_id": owner_obj_id,
|
||||
}
|
||||
}]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None
|
||||
return []
|
||||
|
||||
def _project_to_screen(self, p3d: Tuple[float, float, float]) -> Optional[Tuple[int, int]]:
|
||||
"""Project a 3D world point to (x, y) screen pixel.
|
||||
@@ -1315,19 +1354,19 @@ class OCCRenderer(Renderer):
|
||||
return None
|
||||
|
||||
def probe_snap_candidates(
|
||||
self, x: int, y: int, radius: int = 18,
|
||||
self, x: int, y: int, radius: int = 30,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Probe a pixel grid around (x, y) and return visible snap candidates.
|
||||
|
||||
Samples a small ring + centre around the cursor, runs OCC's
|
||||
Samples a dense ring + centre around the cursor, runs OCC's
|
||||
``MoveTo`` at each pixel, and classifies every distinct detected
|
||||
sub-shape via :meth:`_classify_detected_shape`. Results are
|
||||
deduplicated by (owner_obj_id, type, rounded position) and sorted by
|
||||
screen-space distance to the cursor, nearest first.
|
||||
|
||||
This is the general hover snap indicator: it surfaces nearby
|
||||
vertices, edge midpoints, hole centres, and face centres so the
|
||||
user can see the snap targets in the cursor neighbourhood — not
|
||||
vertices, edge midpoints, hole centres, and face centres so that
|
||||
the user can see the snap targets in the cursor neighbourhood — not
|
||||
just the single entity directly under the crosshair.
|
||||
|
||||
Each entry is the same dict shape returned by ``pick_entity`` plus an
|
||||
@@ -1337,14 +1376,21 @@ class OCCRenderer(Renderer):
|
||||
if self._view is None or self._context is None:
|
||||
return []
|
||||
|
||||
# Sample pattern: the exact cursor pixel plus a small ring of
|
||||
# offsets. The ring catches nearby vertices/edges/holes that sit a
|
||||
# few pixels away from where the user is pointing.
|
||||
# Dense sample pattern: centre + multiple rings at different radii
|
||||
# to catch small features like hole openings that might be missed by
|
||||
# a single sparse ring. Uses quarter, half, and full radius offsets.
|
||||
q = radius // 4
|
||||
h = radius // 2
|
||||
ring_offsets = [
|
||||
(0, 0),
|
||||
# Full radius ring (cardinal + diagonal)
|
||||
(-radius, 0), (radius, 0), (0, -radius), (0, radius),
|
||||
(-radius, -radius), (radius, radius), (-radius, radius), (radius, -radius),
|
||||
(-radius // 2, 0), (radius // 2, 0), (0, -radius // 2), (0, radius // 2),
|
||||
# Half-radius ring
|
||||
(-h, 0), (h, 0), (0, -h), (0, h),
|
||||
(-h, -h), (h, h), (-h, h), (h, -h),
|
||||
# Quarter-radius ring for small features
|
||||
(-q, 0), (q, 0), (0, -q), (0, q),
|
||||
]
|
||||
|
||||
candidates: Dict[Tuple[str, str, Tuple[int, int, int]], Dict[str, Any]] = {}
|
||||
@@ -1359,22 +1405,24 @@ class OCCRenderer(Renderer):
|
||||
shape = self._context.DetectedShape()
|
||||
if shape is None:
|
||||
continue
|
||||
info = self._classify_detected_shape(shape)
|
||||
if info is None:
|
||||
infos = self._classify_detected_shape(shape)
|
||||
if not infos:
|
||||
continue
|
||||
# Skip non-trackable hits (no owner — e.g. the workplane plane).
|
||||
if not info.get("owner_obj_id"):
|
||||
continue
|
||||
pos = info.get("position") or (0.0, 0.0, 0.0)
|
||||
# Dedupe key: owner + type + position rounded to 0.1 mm.
|
||||
key = (
|
||||
info.get("owner_obj_id", ""),
|
||||
info.get("type", ""),
|
||||
(round(pos[0], 1), round(pos[1], 1), round(pos[2], 1)),
|
||||
)
|
||||
if key not in candidates:
|
||||
info["screen"] = (sx, sy)
|
||||
candidates[key] = info
|
||||
# infos is a list; for cylinders it contains two ends.
|
||||
for info in infos:
|
||||
# Skip non-trackable hits (no owner — e.g. the workplane plane).
|
||||
if not info.get("owner_obj_id"):
|
||||
continue
|
||||
pos = info.get("position") or (0.0, 0.0, 0.0)
|
||||
# Dedupe key: owner + type + position rounded to 0.1 mm.
|
||||
key = (
|
||||
info.get("owner_obj_id", ""),
|
||||
info.get("type", ""),
|
||||
(round(pos[0], 1), round(pos[1], 1), round(pos[2], 1)),
|
||||
)
|
||||
if key not in candidates:
|
||||
info["screen"] = (sx, sy)
|
||||
candidates[key] = info
|
||||
|
||||
# Sort by screen-space distance to the cursor, nearest first.
|
||||
results = list(candidates.values())
|
||||
|
||||
Reference in New Issue
Block a user