- arc improvements, fillets, operations, bodys

This commit is contained in:
bklronin
2026-08-05 09:44:17 +02:00
parent 48042659fc
commit a7e7a9f2c3
4 changed files with 209 additions and 11 deletions
+13 -9
View File
@@ -4,15 +4,11 @@
<option name="autoReloadType" value="SELECTIVE" /> <option name="autoReloadType" value="SELECTIVE" />
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="8f0bafd6-58a0-4b20-aa2b-ddc3ba278873" name="Changes" comment="- added &quot;measurement lines&quot;"> <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$/.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/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/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> </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" />
@@ -459,7 +455,15 @@
<option name="project" value="LOCAL" /> <option name="project" value="LOCAL" />
<updated>1785697123545</updated> <updated>1785697123545</updated>
</task> </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 /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
@@ -480,7 +484,6 @@
<ignored-roots> <ignored-roots>
<path value="$PROJECT_DIR$/pythonProject" /> <path value="$PROJECT_DIR$/pythonProject" />
</ignored-roots> </ignored-roots>
<MESSAGE value="- Added new componnt controls" />
<MESSAGE value="- changing compos for sketches works" /> <MESSAGE value="- changing compos for sketches works" />
<MESSAGE value="- changing compos including sketches and bodies" /> <MESSAGE value="- changing compos including sketches and bodies" />
<MESSAGE value="- Drawing bodys depending on the selected compo&#10;- Cut working&#10;- Edit sketch working" /> <MESSAGE value="- Drawing bodys depending on the selected compo&#10;- Cut working&#10;- Edit sketch working" />
@@ -505,6 +508,7 @@
<MESSAGE value="- added renderer&#10;- Added undo" /> <MESSAGE value="- added renderer&#10;- Added undo" />
<MESSAGE value="- Render improvements, camera plane, update" /> <MESSAGE value="- Render improvements, camera plane, update" />
<MESSAGE value="- added &quot;measurement lines&quot;" /> <MESSAGE value="- added &quot;measurement lines&quot;" />
<option name="LAST_COMMIT_MESSAGE" value="- added &quot;measurement lines&quot;" /> <MESSAGE value="- arc improvements, fillets, operations, bodys" />
<option name="LAST_COMMIT_MESSAGE" value="- arc improvements, fillets, operations, bodys" />
</component> </component>
</project> </project>
+1 -1
View File
@@ -75,7 +75,7 @@ class OCGeometryKernel(GeometryKernel):
return OCCGeometryObject(edge, {"type": "circle"}) return OCCGeometryObject(edge, {"type": "circle"})
def create_arc( def create_arc(
self, center: Point2D, radius: float, start_angle: float, end_angle: float self, center: Point2D, radius: float , start_angle: float, end_angle: float
) -> GeometryObject: ) -> GeometryObject:
"""Create a 2D arc.""" """Create a 2D arc."""
import math import math
+14
View File
@@ -1528,6 +1528,20 @@ class MainWindow(QMainWindow):
self._btn_con_horiz.setChecked(True) self._btn_con_horiz.setChecked(True)
elif mode == "constrain_vertical": elif mode == "constrain_vertical":
self._btn_con_vert.setChecked(True) 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): def _on_construct_change(self, checked):
"""Handle the "Cstrct" toolbar button. """Handle the "Cstrct" toolbar button.
+181 -1
View File
@@ -98,6 +98,7 @@ class Sketch2DWidget(QWidget):
constrain_done = Signal() constrain_done = Signal()
sketch_updated = Signal() sketch_updated = Signal()
# Emitted when the SolveSpace solver returns a non-OKAY result # 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 # (INCONSISTENT, DIDNT_CONVERGE, or an exception). The payload is
# a short human-readable explanation; the main window hooks this up # a short human-readable explanation; the main window hooks this up
# to the status bar so the user sees the failure instead of the # 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._snap_distance: int = 10
self._angle_steps: int = 15 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._zoom: float = 1.0
self._offset: QPoint = QPoint(0, 0) self._offset: QPoint = QPoint(0, 0)
self._panning: bool = False self._panning: bool = False
@@ -1588,6 +1596,47 @@ class Sketch2DWidget(QWidget):
new_arcs.append((centre_ent, new_radius, start_ent, end_ent, new_sweep)) new_arcs.append((centre_ent, new_radius, start_ent, end_ent, new_sweep))
self._arcs = new_arcs 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: def _solve_and_sync(self) -> bool:
"""Solve constraints, sync positions, update UI. Returns True if solved OK. """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 :attr:`OCCSketch.last_solve_status`. The main window hooks this
to the status bar so the user sees the failure instead of the to the status bar so the user sees the failure instead of the
geometry silently staying put. 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: if not self._sketch:
return True return True
ok = self._sketch.solve() ok = self._sketch.solve()
failure_reason = getattr(self._sketch, "last_solve_status", "Unknown error")
if not ok: if not ok:
# Surface the failure to the user. Status text comes from # Surface the failure to the user. Status text comes from
# OCCSketch.last_solve_status (e.g. "inconsistent: the new # OCCSketch.last_solve_status (e.g. "inconsistent: the new
# constraint conflicts with existing constraints"). # 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._sync_solved_positions()
self.update() self.update()
return ok return ok
@@ -2313,6 +2372,80 @@ class Sketch2DWidget(QWidget):
self._solve_and_sync() self._solve_and_sync()
self.update() 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): def _delete_hovered_constraint(self):
"""Delete the hovered constraint (by log index) and recompute the rest.""" """Delete the hovered constraint (by log index) and recompute the rest."""
idx = self._hovered_constraint_idx idx = self._hovered_constraint_idx
@@ -3407,6 +3540,53 @@ class Sketch2DWidget(QWidget):
painter.setRenderHint(QPainter.Antialiasing) painter.setRenderHint(QPainter.Antialiasing)
painter.fillRect(self.rect(), QColor("#1e1e2e")) 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) ── # ── Grid (fixed 10mm world-units spacing) ──
# Minor grid: 10 world-unit (10mm) spacing. # Minor grid: 10 world-unit (10mm) spacing.
# Major grid: 100 world-unit (100mm) spacing drawn bolder. # Major grid: 100 world-unit (100mm) spacing drawn bolder.