- arc improvements, fillets, operations, bodys
This commit is contained in:
Generated
+13
-9
@@ -4,15 +4,11 @@
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="8f0bafd6-58a0-4b20-aa2b-ddc3ba278873" name="Changes" comment="- added "measurement lines"">
|
||||
<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$/src/fluency/geometry_occ/kernel.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/geometry_occ/kernel.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/rendering/occ_renderer.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/rendering/occ_renderer.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/ui/main_window.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/ui/main_window.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" />
|
||||
<change beforePath="$PROJECT_DIR$/src/fluency/ui/sketch_widget.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/ui/sketch_widget.py" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -459,7 +455,15 @@
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1785697123545</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="43" />
|
||||
<task id="LOCAL-00043" summary="- arc improvements, fillets, operations, bodys">
|
||||
<option name="closed" value="true" />
|
||||
<created>1785701099363</created>
|
||||
<option name="number" value="00043" />
|
||||
<option name="presentableId" value="LOCAL-00043" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1785701099363</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="44" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
@@ -480,7 +484,6 @@
|
||||
<ignored-roots>
|
||||
<path value="$PROJECT_DIR$/pythonProject" />
|
||||
</ignored-roots>
|
||||
<MESSAGE value="- Added new componnt controls" />
|
||||
<MESSAGE value="- changing compos for sketches works" />
|
||||
<MESSAGE value="- changing compos including sketches and bodies" />
|
||||
<MESSAGE value="- Drawing bodys depending on the selected compo - Cut working - Edit sketch working" />
|
||||
@@ -505,6 +508,7 @@
|
||||
<MESSAGE value="- added renderer - Added undo" />
|
||||
<MESSAGE value="- Render improvements, camera plane, update" />
|
||||
<MESSAGE value="- added "measurement lines"" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="- added "measurement lines"" />
|
||||
<MESSAGE value="- arc improvements, fillets, operations, bodys" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="- arc improvements, fillets, operations, bodys" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1528,6 +1528,20 @@ class MainWindow(QMainWindow):
|
||||
self._btn_con_horiz.setChecked(True)
|
||||
elif mode == "constrain_vertical":
|
||||
self._btn_con_vert.setChecked(True)
|
||||
elif mode == "constrain_perpendicular":
|
||||
self._btn_con_perp.setChecked(True)
|
||||
elif mode == "constrain_parallel":
|
||||
self._btn_con_par.setChecked(True)
|
||||
elif mode == "constrain_distance":
|
||||
self._btn_con_dist.setChecked(True)
|
||||
elif mode == "constrain_midpoint":
|
||||
self._btn_con_mid.setChecked(True)
|
||||
elif mode == "constrain_ptline":
|
||||
self._btn_con_ptline.setChecked(True)
|
||||
elif mode == "constrain_symmetric":
|
||||
self._btn_con_sym.setChecked(True)
|
||||
elif mode == "constrain_diameter":
|
||||
self._btn_con_diameter.setChecked(True)
|
||||
|
||||
def _on_construct_change(self, checked):
|
||||
"""Handle the "Cstrct" toolbar button.
|
||||
|
||||
@@ -98,6 +98,7 @@ class Sketch2DWidget(QWidget):
|
||||
constrain_done = Signal()
|
||||
sketch_updated = Signal()
|
||||
# Emitted when the SolveSpace solver returns a non-OKAY result
|
||||
constraint_failed = Signal(int, str) # (constraint_log_index, failure_reason)
|
||||
# (INCONSISTENT, DIDNT_CONVERGE, or an exception). The payload is
|
||||
# a short human-readable explanation; the main window hooks this up
|
||||
# to the status bar so the user sees the failure instead of the
|
||||
@@ -163,6 +164,13 @@ class Sketch2DWidget(QWidget):
|
||||
self._snap_distance: int = 10
|
||||
self._angle_steps: int = 15
|
||||
|
||||
# Constraint status indicators
|
||||
self._constraint_status: Dict[int, Tuple[str, float]] = {}
|
||||
self._inconsistent_constraints: set[int] = set()
|
||||
self._failed_constraint_idx: Optional[int] = None
|
||||
self._failed_constraint_reason: str = ""
|
||||
self._global_inconsistent: bool = False
|
||||
|
||||
self._zoom: float = 1.0
|
||||
self._offset: QPoint = QPoint(0, 0)
|
||||
self._panning: bool = False
|
||||
@@ -1588,6 +1596,47 @@ class Sketch2DWidget(QWidget):
|
||||
new_arcs.append((centre_ent, new_radius, start_ent, end_ent, new_sweep))
|
||||
self._arcs = new_arcs
|
||||
|
||||
def _update_constraint_status(self, status: str, message: str, log_index: int = -1) -> None:
|
||||
"""Update the visual status indicator for constraint solving results."""
|
||||
import time
|
||||
now = time.time()
|
||||
# Keep only recent statuses (last 5 seconds)
|
||||
self._constraint_status = {
|
||||
k: (v[0], now)
|
||||
for k, v in self._constraint_status.items()
|
||||
if now - v[1] < 5.0
|
||||
}
|
||||
if log_index >= 0:
|
||||
self._constraint_status[log_index] = (status, now)
|
||||
# Track inconsistent/failed constraints separately for highlighting
|
||||
# Only track if log_index is valid (>= 0) to avoid corrupting state
|
||||
if "inconsistent" in status.lower() or status == "FAILED":
|
||||
if log_index >= 0:
|
||||
self._inconsistent_constraints.add(log_index)
|
||||
# Also track the most recent failed constraint index for highlighting
|
||||
self._failed_constraint_idx = log_index
|
||||
else:
|
||||
# Global inconsistent mode - no specific constraint, but solver is in bad state
|
||||
self._global_inconsistent = True
|
||||
elif status == "OKAY":
|
||||
if log_index >= 0:
|
||||
self._inconsistent_constraints.discard(log_index)
|
||||
# Clear failed constraint when resolved (only if log_index was valid)
|
||||
if log_index == self._failed_constraint_idx:
|
||||
self._failed_constraint_idx = None
|
||||
else:
|
||||
# Global OKAY - clear global inconsistent mode
|
||||
self._global_inconsistent = False
|
||||
|
||||
def mark_constraint_failed(self, log_index: int, reason: str) -> None:
|
||||
"""Mark a specific constraint as failed so it can be highlighted."""
|
||||
self._failed_constraint_idx = log_index
|
||||
self._failed_constraint_reason = reason
|
||||
# Also add to inconsistent set for orange indicator
|
||||
self._inconsistent_constraints.add(log_index)
|
||||
# Emit signal so the user can remove this constraint via context menu
|
||||
self.constraint_failed.emit(log_index, reason)
|
||||
|
||||
def _solve_and_sync(self) -> bool:
|
||||
"""Solve constraints, sync positions, update UI. Returns True if solved OK.
|
||||
|
||||
@@ -1597,15 +1646,25 @@ class Sketch2DWidget(QWidget):
|
||||
:attr:`OCCSketch.last_solve_status`. The main window hooks this
|
||||
to the status bar so the user sees the failure instead of the
|
||||
geometry silently staying put.
|
||||
|
||||
Also updates the visual constraint status indicator in the top-left
|
||||
corner and marks any failed constraints for highlighting.
|
||||
"""
|
||||
if not self._sketch:
|
||||
return True
|
||||
ok = self._sketch.solve()
|
||||
failure_reason = getattr(self._sketch, "last_solve_status", "Unknown error")
|
||||
if not ok:
|
||||
# Surface the failure to the user. Status text comes from
|
||||
# OCCSketch.last_solve_status (e.g. "inconsistent: the new
|
||||
# constraint conflicts with existing constraints").
|
||||
self.solver_warning.emit(f"Solver failed — {self._sketch.last_solve_status}")
|
||||
self.solver_warning.emit(f"Solver failed — {failure_reason}")
|
||||
# Update visual indicator for inconsistent/failed constraints
|
||||
# Pass -1 to indicate this is a global status, not tied to a specific constraint log entry
|
||||
self._update_constraint_status("INCONSISTENT", failure_reason, log_index=-1)
|
||||
else:
|
||||
# OKAY - clear any previous inconsistent status
|
||||
self._update_constraint_status("OKAY", "Constraints satisfied")
|
||||
self._sync_solved_positions()
|
||||
self.update()
|
||||
return ok
|
||||
@@ -2313,6 +2372,80 @@ class Sketch2DWidget(QWidget):
|
||||
self._solve_and_sync()
|
||||
self.update()
|
||||
|
||||
# Add option to delete the failed constraint (right-click anywhere when constraint failed)
|
||||
if self._failed_constraint_idx is not None:
|
||||
delete_failed = menu.addAction("Delete Failed Constraint")
|
||||
if action == delete_failed:
|
||||
self._delete_failed_constraint()
|
||||
return
|
||||
|
||||
def _get_failed_constraint_entities(self) -> List[Any]:
|
||||
"""Get the entities involved in the failed constraint for highlighting."""
|
||||
if self._failed_constraint_idx is None or self._sketch is None:
|
||||
return []
|
||||
try:
|
||||
entry = self._sketch._constraint_log[self._failed_constraint_idx]
|
||||
ids = entry.get("ids", ())
|
||||
entities = []
|
||||
for eid in ids:
|
||||
entity = self._sketch._entities.get(eid)
|
||||
if entity is not None:
|
||||
entities.append(entity)
|
||||
return entities
|
||||
except (IndexError, KeyError):
|
||||
return []
|
||||
|
||||
def _highlight_failed_constraint(self, painter: QPainter) -> None:
|
||||
"""Draw a visual highlight around the failed constraint using the given painter."""
|
||||
# Safety check: only highlight if we have a valid, non-negative index
|
||||
if self._failed_constraint_idx is None or self._failed_constraint_idx < 0:
|
||||
return
|
||||
entities = self._get_failed_constraint_entities()
|
||||
if not entities:
|
||||
return
|
||||
# Bright red glow effect
|
||||
pen = QPen(QColor("#ef4444"), 3, Qt.DashDotLine)
|
||||
brush = QBrush(QColor("rgba(255,0,0,100)") if hasattr(QColor, "fromRgb") else QColor("#ef4444"))
|
||||
painter.setPen(pen)
|
||||
painter.setBrush(brush)
|
||||
for entity in entities:
|
||||
xy = self._flat_xy(entity.geometry)
|
||||
if xy is None:
|
||||
continue
|
||||
x, y = xy
|
||||
screen_pos = self._world_to_screen(QPoint(int(round(x)), int(round(y))))
|
||||
# Draw a pulsing circle around each entity
|
||||
radius = 10
|
||||
painter.drawEllipse(screen_pos, radius, radius)
|
||||
|
||||
def _delete_failed_constraint(self):
|
||||
"""Delete the currently failed constraint and recompute."""
|
||||
idx = self._failed_constraint_idx
|
||||
if idx is None or idx < 0:
|
||||
# Clear global inconsistent mode if no specific constraint to delete
|
||||
self._global_inconsistent = False
|
||||
return
|
||||
# If no sketch exists, just clear the tracking state (user can right-click to delete)
|
||||
if self._sketch is None:
|
||||
self._failed_constraint_idx = None
|
||||
self._failed_constraint_reason = ""
|
||||
self._inconsistent_constraints.discard(idx)
|
||||
self._global_inconsistent = False
|
||||
return
|
||||
# Save state before deleting
|
||||
if self._undo_manager:
|
||||
self._undo_manager.save_state()
|
||||
ok = self._sketch.remove_constraint_at(idx)
|
||||
logger.info(f"Deleted failed constraint #{idx}; recompute solved={ok}")
|
||||
self._failed_constraint_idx = None
|
||||
self._failed_constraint_reason = ""
|
||||
self._inconsistent_constraints.discard(idx)
|
||||
self._hovered_constraint_idx = -1
|
||||
self._rebuild_from_sketch()
|
||||
self._solve_and_sync()
|
||||
self.sketch_updated.emit()
|
||||
self.update()
|
||||
|
||||
def _delete_hovered_constraint(self):
|
||||
"""Delete the hovered constraint (by log index) and recompute the rest."""
|
||||
idx = self._hovered_constraint_idx
|
||||
@@ -3407,6 +3540,53 @@ class Sketch2DWidget(QWidget):
|
||||
painter.setRenderHint(QPainter.Antialiasing)
|
||||
painter.fillRect(self.rect(), QColor("#1e1e2e"))
|
||||
|
||||
# ── Constraint status indicator (top-left corner) ──
|
||||
# Shows orange text for inconsistent constraints, bright red when a constraint failed.
|
||||
has_status = self._inconsistent_constraints or self._failed_constraint_idx is not None or self._global_inconsistent
|
||||
|
||||
if has_status:
|
||||
colors: List[QColor] = []
|
||||
status_text: List[str] = []
|
||||
|
||||
# Orange indicator for inconsistent constraints
|
||||
if self._inconsistent_constraints:
|
||||
count = len(self._inconsistent_constraints)
|
||||
status_text.append(f"⚠ {count} inconsistent")
|
||||
colors.append(QColor("#fb923c")) # orange-400
|
||||
|
||||
# Bright red indicator for failed constraint
|
||||
if self._failed_constraint_idx is not None:
|
||||
reason = self._failed_constraint_reason[:50] + "..." if len(self._failed_constraint_reason) > 50 else self._failed_constraint_reason
|
||||
status_text.append(f"❌ Failed: {reason}")
|
||||
colors.append(QColor("#ef4444")) # red-500
|
||||
|
||||
# Red indicator for global solver inconsistency (no specific constraint)
|
||||
if self._global_inconsistent:
|
||||
status_text.append("⚠ Solver inconsistent")
|
||||
colors.append(QColor("#ef4444")) # red-500
|
||||
|
||||
if status_text:
|
||||
# Draw status text in top-left corner with colored background bar
|
||||
font_metrics = QFontMetrics(self.font())
|
||||
y_pos = 12
|
||||
|
||||
for i, line in enumerate(status_text):
|
||||
text_width = font_metrics.horizontalAdvance(line)
|
||||
# Background bar with color gradient effect
|
||||
bg_rect = QRect(8, y_pos - 4, max(text_width + 30, 150), font_metrics.height() + 6)
|
||||
painter.setPen(Qt.NoPen)
|
||||
brush = QBrush(colors[i] if i < len(colors) else QColor("#fb923c"))
|
||||
painter.setBrush(brush)
|
||||
painter.drawRoundedRect(bg_rect, 8, 8)
|
||||
|
||||
# Text
|
||||
painter.setPen(QColor("#1e1e2e"))
|
||||
painter.drawText(bg_rect, Qt.AlignLeft | Qt.AlignVCenter, line)
|
||||
y_pos += font_metrics.height() + 4
|
||||
|
||||
# ── Highlight failed constraint (drawn after all geometry) ──
|
||||
self._highlight_failed_constraint(painter)
|
||||
|
||||
# ── Grid (fixed 10mm world-units spacing) ──
|
||||
# Minor grid: 10 world-unit (10mm) spacing.
|
||||
# Major grid: 100 world-unit (100mm) spacing drawn bolder.
|
||||
|
||||
Reference in New Issue
Block a user