- Operation highlighting, body highlighting
This commit is contained in:
Generated
+59
-47
@@ -6,13 +6,9 @@
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="8f0bafd6-58a0-4b20-aa2b-ddc3ba278873" name="Changes" comment="- arc improvements, fillets, operations, bodys">
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/gui.ui" beforeDir="false" afterPath="$PROJECT_DIR$/gui.ui" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/gui_ui.py" beforeDir="false" afterPath="$PROJECT_DIR$/gui_ui.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/fluency/io/project_io.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/io/project_io.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/fluency/models/data_model.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/models/data_model.py" 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" />
|
||||
<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/ui/sketch_widget.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/ui/sketch_widget.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/fluency/ui/viewer_widget.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/ui/viewer_widget.py" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -49,47 +45,47 @@
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"keyToString": {
|
||||
"Python.2dtest.executor": "Run",
|
||||
"Python.3d_windows.executor": "Run",
|
||||
"Python.Unnamed.executor": "Run",
|
||||
"Python.base.executor": "Run",
|
||||
"Python.data_model.executor": "Run",
|
||||
"Python.debug_dragging.executor": "Run",
|
||||
"Python.draw_widget2d.executor": "Run",
|
||||
"Python.draw_widget_solve.executor": "Run",
|
||||
"Python.fluency.executor": "Run",
|
||||
"Python.fluencyb.executor": "Run",
|
||||
"Python.gl_widget.executor": "Run",
|
||||
"Python.gui_ui.executor": "Run",
|
||||
"Python.kernel.executor": "Run",
|
||||
"Python.main.executor": "Run",
|
||||
"Python.main_window.executor": "Run",
|
||||
"Python.meshtest.executor": "Run",
|
||||
"Python.occ_renderer.executor": "Run",
|
||||
"Python.occ_to_mesh.executor": "Run",
|
||||
"Python.render_backend.executor": "Run",
|
||||
"Python.side_fluency.executor": "Run",
|
||||
"Python.simple_mesh.executor": "Run",
|
||||
"Python.sketch.executor": "Run",
|
||||
"Python.vtk_widget.executor": "Run",
|
||||
"Python.vulkan.executor": "Run",
|
||||
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||
"codeWithMe.voiceChat.enabledByDefault": "false",
|
||||
"git-widget-placeholder": "feature/occ-migration",
|
||||
"last_opened_file_path": "/Volumes/Data_drive/Programming/fluency/src/fluency/Tesfiles",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"settings.editor.selected.configurable": "project.propVCSSupport.DirectoryMappings"
|
||||
<component name="PropertiesComponent">{
|
||||
"keyToString": {
|
||||
"Python.2dtest.executor": "Run",
|
||||
"Python.3d_windows.executor": "Run",
|
||||
"Python.Unnamed.executor": "Run",
|
||||
"Python.base.executor": "Run",
|
||||
"Python.data_model.executor": "Run",
|
||||
"Python.debug_dragging.executor": "Run",
|
||||
"Python.draw_widget2d.executor": "Run",
|
||||
"Python.draw_widget_solve.executor": "Run",
|
||||
"Python.fluency.executor": "Run",
|
||||
"Python.fluencyb.executor": "Run",
|
||||
"Python.gl_widget.executor": "Run",
|
||||
"Python.gui_ui.executor": "Run",
|
||||
"Python.kernel.executor": "Run",
|
||||
"Python.main.executor": "Run",
|
||||
"Python.main_window.executor": "Run",
|
||||
"Python.meshtest.executor": "Run",
|
||||
"Python.occ_renderer.executor": "Run",
|
||||
"Python.occ_to_mesh.executor": "Run",
|
||||
"Python.render_backend.executor": "Run",
|
||||
"Python.side_fluency.executor": "Run",
|
||||
"Python.simple_mesh.executor": "Run",
|
||||
"Python.sketch.executor": "Run",
|
||||
"Python.vtk_widget.executor": "Run",
|
||||
"Python.vulkan.executor": "Run",
|
||||
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||
"codeWithMe.voiceChat.enabledByDefault": "false",
|
||||
"git-widget-placeholder": "feature/occ-migration",
|
||||
"last_opened_file_path": "/Volumes/Data_drive/Programming/fluency/src/fluency/Tesfiles",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"settings.editor.selected.configurable": "project.propVCSSupport.DirectoryMappings"
|
||||
}
|
||||
}]]></component>
|
||||
}</component>
|
||||
<component name="QodanaReportsService">
|
||||
<option name="descriptions">
|
||||
<ReportDescription localRun="true" path="/private/var/folders/kg/zm48w_r96yb68mlbzvb9gtq40000gn/T/qodana_output/qodana.sarif.json" reportGuid="5f5b823c-c594-48c5-ae1f-062e30303918" reportId="fluency/qodana/2024-02-04" />
|
||||
@@ -491,7 +487,23 @@
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1785948828454</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="47" />
|
||||
<task id="LOCAL-00047" summary="- arc improvements, fillets, operations, bodys">
|
||||
<option name="closed" value="true" />
|
||||
<created>1786110295218</created>
|
||||
<option name="number" value="00047" />
|
||||
<option name="presentableId" value="LOCAL-00047" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1786110295218</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00048" summary="- arc improvements, fillets, operations, bodys">
|
||||
<option name="closed" value="true" />
|
||||
<created>1786125317688</created>
|
||||
<option name="number" value="00048" />
|
||||
<option name="presentableId" value="LOCAL-00048" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1786125317688</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="49" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
|
||||
@@ -157,6 +157,8 @@ class OCCRenderer(Renderer):
|
||||
self._highlight_ais: Any = None
|
||||
# Overlays for the fillet tool's two picked faces (one AIS per face).
|
||||
self._faces_highlight_ais: List[Any] = []
|
||||
# Hot-pink translucent overlay for operation-history highlighting.
|
||||
self._op_highlight_ais: Any = None
|
||||
# Temporary transparent preview AIS for the live extrude/cut dialog.
|
||||
self._preview_ais: Any = None
|
||||
# Smart entity picker gizmo objects (snap markers, axis lines, rings).
|
||||
@@ -543,6 +545,16 @@ class OCCRenderer(Renderer):
|
||||
return False
|
||||
return True
|
||||
|
||||
def set_color(
|
||||
self, obj_id: str, color: Tuple[float, float, float]
|
||||
) -> bool:
|
||||
"""Set the colour of an object by ID. Returns True on success."""
|
||||
obj = self._objects.get(obj_id)
|
||||
if obj is None:
|
||||
return False
|
||||
self.set_object_color(obj, color)
|
||||
return True
|
||||
|
||||
# ─── Live preview (extrude/cut preview) ──────────────────────────────
|
||||
|
||||
_PREVIEW_ID = "__extrude_preview__"
|
||||
@@ -608,6 +620,7 @@ class OCCRenderer(Renderer):
|
||||
return
|
||||
self.clear_preview()
|
||||
self.clear_face_highlight()
|
||||
self.clear_operation_highlight()
|
||||
self.clear_entity_gizmo()
|
||||
# The sketch reference gizmo is scene-anchored — drop it with the rest.
|
||||
self.remove_sketch_gizmo()
|
||||
@@ -1385,6 +1398,56 @@ class OCCRenderer(Renderer):
|
||||
logger.debug("clear_faces_highlight remove failed", exc_info=True)
|
||||
self._faces_highlight_ais = []
|
||||
|
||||
|
||||
# ─── Operation history highlight ─────────────────────────────────────
|
||||
|
||||
def highlight_operation_shape(
|
||||
self, shape: Any, color: Tuple[float, float, float] = (1.0, 0.08, 0.58)
|
||||
) -> None:
|
||||
"""Overlay a translucent hot-pink *shape* on the 3D view.
|
||||
|
||||
Used to show the intermediate geometry at a selected operation in
|
||||
the feature-history list. The overlay is an independent
|
||||
``AIS_Shape`` with polygon offset so it draws on top of the
|
||||
coincident body surface without z-fighting. Replaces any previous
|
||||
operation highlight.
|
||||
"""
|
||||
if self._context is None:
|
||||
return
|
||||
self.clear_operation_highlight()
|
||||
from OCP.AIS import AIS_Shape
|
||||
from OCP.Quantity import Quantity_Color, Quantity_TOC_RGB
|
||||
|
||||
ais = AIS_Shape(shape)
|
||||
try:
|
||||
ais.SetMaterial(self._default_material())
|
||||
except Exception:
|
||||
logger.debug("op highlight material set failed", exc_info=True)
|
||||
ais.SetColor(Quantity_Color(*color, Quantity_TOC_RGB))
|
||||
ais.SetDisplayMode(1) # shaded
|
||||
try:
|
||||
ais.SetTransparency(0.65)
|
||||
except Exception:
|
||||
logger.debug("op highlight transparency set failed", exc_info=True)
|
||||
try:
|
||||
ais.SetPolygonOffsets(3, 1.0, -0.5)
|
||||
except Exception:
|
||||
logger.debug("op highlight polygon offset failed", exc_info=True)
|
||||
self._context.Display(ais, True)
|
||||
self._op_highlight_ais = ais
|
||||
if self._view is not None:
|
||||
self._view.Redraw()
|
||||
|
||||
def clear_operation_highlight(self) -> None:
|
||||
"""Remove the operation-history overlay, if any."""
|
||||
if self._context is None or self._op_highlight_ais is None:
|
||||
return
|
||||
try:
|
||||
self._context.Remove(self._op_highlight_ais, True)
|
||||
except Exception:
|
||||
logger.debug("clear_operation_highlight remove failed", exc_info=True)
|
||||
self._op_highlight_ais = None
|
||||
|
||||
# ─── General entity picking (for assembly connectors / snaps) ───────────
|
||||
|
||||
def pick_entity(self, x: int, y: int) -> Optional[Dict[str, Any]]:
|
||||
|
||||
@@ -1423,6 +1423,13 @@ class MainWindow(QMainWindow):
|
||||
# selectable axis / centre / plane from the 3D viewport.
|
||||
self._sketch_gizmo_selection: Optional[dict] = None
|
||||
self._selected_body: Optional[Body] = None
|
||||
# Track which body we're hiding while an operation-history highlight
|
||||
# is active, so we can restore it when the selection changes.
|
||||
self._op_highlight_body_id: Optional[str] = None
|
||||
# Body-highlight state: when no operation is selected we tint the
|
||||
# selected body light-blue; save its original colour to restore later.
|
||||
self._body_highlight_id: Optional[str] = None
|
||||
self._body_highlight_original_color: Optional[Tuple[float, float, float]] = None
|
||||
|
||||
# Fillet tool: two-face pick flow (face 1 → face 2 → options dialog).
|
||||
self._fillet_pick_active: bool = False
|
||||
@@ -2345,19 +2352,154 @@ class MainWindow(QMainWindow):
|
||||
item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsSelectable)
|
||||
self._operations_list.addItem(item)
|
||||
self._operations_list.setCurrentRow(min(1, len(features) - 1))
|
||||
# setCurrentRow doesn't fire for non-selectable items (the base);
|
||||
# ensure buttons reflect the empty selection in that edge case.
|
||||
if self._operations_list.currentItem() is None:
|
||||
self._on_operations_selection_changed()
|
||||
|
||||
def _compute_operation_highlight_shape(
|
||||
self, body: Body, features: List[Feature], index: int, feat: Feature
|
||||
) -> Optional[Any]:
|
||||
"""Return the shape to highlight for *feat* at *index* in the history.
|
||||
|
||||
For subtractive operations (cut), returns the tool shape — the
|
||||
volume that was removed — so the user sees what was cut away.
|
||||
For additive operations (union), returns the tool shape that was
|
||||
added. For everything else, returns the intermediate body shape
|
||||
after replaying features up to and including *index*.
|
||||
"""
|
||||
if feat.operation in ("cut", "union"):
|
||||
return self._compute_tool_shape(body, features, index, feat)
|
||||
# Default: show the intermediate body after this operation.
|
||||
geom = _replay_body_features(
|
||||
self._kernel, body, features[: index + 1],
|
||||
self._through_all_length_for_geometry,
|
||||
)
|
||||
if geom is None:
|
||||
return None
|
||||
return self._kernel._get_shape(geom)
|
||||
|
||||
def _compute_tool_shape(
|
||||
self, body: Body, features: List[Feature], index: int, feat: Feature
|
||||
) -> Optional[Any]:
|
||||
"""Compute the extruded tool shape for a cut or union feature.
|
||||
|
||||
Replays the body up to (but not including) *index* so the
|
||||
pre-operation geometry is available for through-all length
|
||||
calculation, then extrudes the feature's sketch profile to
|
||||
produce the tool volume.
|
||||
"""
|
||||
sketch = feat.sketch
|
||||
if sketch is None or sketch.occ_sketch is None:
|
||||
return None
|
||||
|
||||
sketch.apply_workplane()
|
||||
sketch.solve()
|
||||
|
||||
face_geom = _feature_face_geometry(body, feat, sketch.occ_sketch)
|
||||
if face_geom is None:
|
||||
return None
|
||||
|
||||
# For through-all cuts we need the pre-op body to size the tool.
|
||||
if feat.through_all:
|
||||
pre_geom = _replay_body_features(
|
||||
self._kernel, body, features[:index],
|
||||
self._through_all_length_for_geometry,
|
||||
)
|
||||
if pre_geom is not None:
|
||||
length = self._through_all_length_for_geometry(pre_geom, sketch)
|
||||
symmetric = True
|
||||
invert = False
|
||||
else:
|
||||
length = feat.length if feat.length is not None else 10.0
|
||||
symmetric = feat.symmetric
|
||||
invert = True # cut: go inward
|
||||
elif feat.operation == "cut":
|
||||
# Cut tool must go INTO the solid — the face normal points away,
|
||||
# so force the extrude direction inward.
|
||||
length = feat.length if feat.length is not None else 10.0
|
||||
symmetric = feat.symmetric
|
||||
invert = True
|
||||
else:
|
||||
length = feat.length if feat.length is not None else 10.0
|
||||
symmetric = feat.symmetric
|
||||
invert = feat.invert
|
||||
|
||||
tool_geom = self._kernel.extrude(
|
||||
face_geom, -length if invert else length, symmetric=symmetric
|
||||
)
|
||||
if tool_geom is None:
|
||||
return None
|
||||
return self._kernel._get_shape(tool_geom)
|
||||
|
||||
|
||||
# ── Body highlight helpers ─────────────────────────────────────────
|
||||
|
||||
def _restore_body_highlight_color(self) -> None:
|
||||
"""Restore the original colour of a previously highlighted body."""
|
||||
if self._body_highlight_id is not None:
|
||||
if self._body_highlight_original_color is not None:
|
||||
self._viewer_3d.set_body_color(
|
||||
self._body_highlight_id, self._body_highlight_original_color
|
||||
)
|
||||
self._body_highlight_id = None
|
||||
self._body_highlight_original_color = None
|
||||
|
||||
def _highlight_selected_body_light_blue(self) -> None:
|
||||
"""Tint the currently selected body light blue in the 3D view."""
|
||||
body = self._selected_body
|
||||
if body is None or body.render_object is None or not body.visible:
|
||||
return
|
||||
# Save the original colour so we can restore it later.
|
||||
self._body_highlight_id = body.render_object
|
||||
self._body_highlight_original_color = body.color
|
||||
self._viewer_3d.set_body_color(
|
||||
body.render_object, (0.45, 0.75, 1.0)
|
||||
)
|
||||
|
||||
def _on_operations_selection_changed(self) -> None:
|
||||
"""Enable 'Del Op' / 'Mirror Op' when a non-base op is selected."""
|
||||
"""Enable 'Del Op' / 'Mirror Op', highlight body or selected op in 3D."""
|
||||
item = self._operations_list.currentItem()
|
||||
|
||||
# Restore the body we hid for the previous operation highlight.
|
||||
if self._op_highlight_body_id is not None:
|
||||
self._viewer_3d.set_visibility(self._op_highlight_body_id, True)
|
||||
self._op_highlight_body_id = None
|
||||
self._viewer_3d.clear_operation_highlight()
|
||||
|
||||
# Restore the previous body colour highlight.
|
||||
self._restore_body_highlight_color()
|
||||
|
||||
if item is None:
|
||||
self._btn_del_op.setEnabled(False)
|
||||
self._btn_mirror_op.setEnabled(False)
|
||||
self._highlight_selected_body_light_blue()
|
||||
return
|
||||
index = item.data(Qt.ItemDataRole.UserRole)
|
||||
deletable = index is not None and index > 0
|
||||
self._btn_del_op.setEnabled(deletable)
|
||||
self._btn_mirror_op.setEnabled(index is not None and index >= 0)
|
||||
if index is not None and index > 0 and self._selected_body is not None:
|
||||
body = self._selected_body
|
||||
features = _ensure_feature_history(body)
|
||||
if 0 <= index < len(features):
|
||||
feat = features[index]
|
||||
try:
|
||||
shape = self._compute_operation_highlight_shape(
|
||||
body, features, index, feat
|
||||
)
|
||||
if shape is not None:
|
||||
if body.render_object is not None and body.visible:
|
||||
self._viewer_3d.set_visibility(
|
||||
body.render_object, False
|
||||
)
|
||||
self._op_highlight_body_id = body.render_object
|
||||
self._viewer_3d.highlight_operation(shape)
|
||||
except Exception:
|
||||
logger.debug("op highlight replay failed", exc_info=True)
|
||||
else:
|
||||
# Base operation (or non-selectable) — highlight body light blue.
|
||||
self._highlight_selected_body_light_blue()
|
||||
|
||||
def _on_mirror_operation(self) -> None:
|
||||
"""Mirror the body at the selected operation's point in the feature history.
|
||||
@@ -6747,6 +6889,9 @@ class MainWindow(QMainWindow):
|
||||
self._project = Project()
|
||||
self._current_component = None
|
||||
self._current_sketch = None
|
||||
self._op_highlight_body_id = None
|
||||
self._body_highlight_id = None
|
||||
self._body_highlight_original_color = None
|
||||
self._selected_body = None
|
||||
self._selected_assembly_component_id = None
|
||||
self._assembly_view_active = False
|
||||
@@ -6994,6 +7139,9 @@ class MainWindow(QMainWindow):
|
||||
self._current_sketch = None
|
||||
self._selected_body = None
|
||||
self._selected_assembly_component_id = None
|
||||
self._op_highlight_body_id = None
|
||||
self._body_highlight_id = None
|
||||
self._body_highlight_original_color = None
|
||||
self._assembly_view_active = False
|
||||
self._sketch_widget.clear_source_face()
|
||||
self._sketch_widget.set_sketch(None)
|
||||
|
||||
@@ -279,6 +279,19 @@ class Viewer3DWidget(QWidget):
|
||||
self._renderer.render()
|
||||
return ok
|
||||
|
||||
def set_body_color(
|
||||
self, mesh_id: str, color: Tuple[float, float, float]
|
||||
) -> bool:
|
||||
"""Change the colour of a body in the 3D view. Returns True on success."""
|
||||
self._ensure_initialized()
|
||||
fn = getattr(self._renderer, "set_color", None)
|
||||
if fn is None:
|
||||
return False
|
||||
ok = fn(mesh_id, color)
|
||||
if ok:
|
||||
self._renderer.render()
|
||||
return ok
|
||||
|
||||
def set_transparency(self, mesh_id: str, transparency: float) -> bool:
|
||||
"""Set a previously-added mesh's transparency (0..1).
|
||||
|
||||
@@ -862,6 +875,22 @@ class Viewer3DWidget(QWidget):
|
||||
fn()
|
||||
self._renderer.render()
|
||||
|
||||
def highlight_operation(self, shape: Any) -> None:
|
||||
"""Overlay a hot-pink translucent *shape* to show operation history."""
|
||||
self._ensure_initialized()
|
||||
fn = getattr(self._renderer, "highlight_operation_shape", None)
|
||||
if fn is not None:
|
||||
fn(shape)
|
||||
self._renderer.render()
|
||||
|
||||
def clear_operation_highlight(self) -> None:
|
||||
"""Remove the operation-history overlay, if any."""
|
||||
self._ensure_initialized()
|
||||
fn = getattr(self._renderer, "clear_operation_highlight", None)
|
||||
if fn is not None:
|
||||
fn()
|
||||
self._renderer.render()
|
||||
|
||||
# ─── Connector pick mode (assembly) ────────────────────────────────────
|
||||
|
||||
def set_connector_pick_mode(self, enabled: bool, clear_gizmo: bool = True) -> None:
|
||||
|
||||
Reference in New Issue
Block a user