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