Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,35 @@ jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
submodules: true

# for gmsh and panda3D
- name: Install system graphics dependencies
run: |
sudo apt-get update
sudo apt-get install -y libglu1-mesa libgl1-mesa-dev libosmesa6

- name: Install Rust
uses: dtolnay/rust-toolchain@stable

- name: Cache Rust
uses: Swatinem/rust-cache@v2
with:
workspaces: "ferrispline"

- name: Install uv
uses: astral-sh/setup-uv@v3
with:
enable-cache: true
cache-dependency-glob: "uv.lock"

- name: Set up Python
run: uv python install

- name: Install dependencies
run: uv sync --all-extras --dev --find-links wheel/
run: uv sync --all-extras --dev

- name: Run Ruff Check
# Vérifie les erreurs de code et de logique
Expand Down
30 changes: 22 additions & 8 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,44 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
submodules: true

# for gmsh and panda3D
- name: Install system graphics dependencies
run: |
sudo apt-get update
sudo apt-get install -y libglu1-mesa libgl1-mesa-dev libosmesa6

- name: Install Rust
uses: dtolnay/rust-toolchain@stable

- name: Cache Rust
uses: Swatinem/rust-cache@v2
with:
workspaces: "ferrispline"

- name: Install uv
uses: astral-sh/setup-uv@v3
with:
enable-cache: true
cache-dependency-glob: "uv.lock"

- name: Set up Python
run: uv python install

- name: Install dependencies
run: uv sync --all-extras --dev --find-links wheel/
run: uv sync --all-extras --dev

- name: Run tests with coverage
# On génère un rapport XML pour Codecov
run: uv run pytest --cov=. --cov-report=xml

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.xml
fail_ci_if_error: true
# FIXME: need access to the organization

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get what is the problem here. How can we fix it?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codecov requires a secret token named CODECOV_TOKEN. This token is generated by CODECOV and requires access permissions to the repository or something similar. Franck had to set this up for his personal project before forking it.

# - name: Upload coverage to Codecov
# uses: codecov/codecov-action@v4
# with:
# token: ${{ secrets.CODECOV_TOKEN }}
# file: ./coverage.xml
# fail_ci_if_error: true
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "ferrispline"]
path = ferrispline
url = https://github.com/LIHPC-Computational-Geometry/ferrispline
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ We use [uv](https://docs.astral.sh/uv/) for all dependency and environment manag
### 1. Clone and set up

```bash
git clone https://github.com/franck-ledoux/bot.git
git clone --recurse-submodules https://github.com/franck-ledoux/bot.git
cd bot
uv sync # creates .venv and installs all production + dev dependencies
```
Expand Down Expand Up @@ -193,6 +193,7 @@ bot/
│ └── viewer/
│ ├── viewer.py # Viewer — public API, manages the subprocess
│ └── app.py # ViewerApp — Panda3D ShowBase (runs in subprocess)
|── ferrispline/ # Submodule library for generating, manipulating and computing hexahedral meshes
├── tests/
│ ├── unit/ # Isolated class tests (no display required)
│ └── system/ # End-to-end workflow tests
Expand Down
2 changes: 1 addition & 1 deletion bot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@
CADModel = None
Model = CADModel

__all__ = ['core', 'control', 'view', 'viewer', 'Model', 'Viewer']
__all__ = ["core", "control", "view", "viewer", "Model", "Viewer"]
4 changes: 1 addition & 3 deletions bot/control/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
"""
Control module: This submodule of bot gathers classes and functions for controling the view module and make the link
Control module: This submodule of bot gathers classes and functions for controling the view module and make the link
between the view and the model
"""

from . import *
80 changes: 43 additions & 37 deletions bot/control/camera.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from panda3d.core import LPoint3, LVector3, OrthographicLens, LineSegs, NodePath
from direct.interval.IntervalGlobal import Parallel, LerpFunc, Sequence, Func


class CameraController:
"""
Orthographic camera controller for the 3D view.
Expand All @@ -11,7 +12,7 @@ class CameraController:
``cmd_zoom``, ``cmd_center`` and ``cmd_align_plane``.
"""

#TODO: check settings maybe a configuration error
# TODO: check settings maybe a configuration error
def __init__(self, base, scene, settings):
"""
Set up the orthographic camera and its node hierarchy.
Expand Down Expand Up @@ -51,11 +52,10 @@ def __init__(self, base, scene, settings):
self.key_pan_speed = 2.0

# Task pour maintenir le marqueur et le gizmo
taskMgr.add(self.update_task, "CameraUpdateTask")
self.taskMgr.add(self.update_task, "CameraUpdateTask")
# Configure la camera par rapport au contenu de la scene
self.refresh_scene()


def refresh_scene(self):
"""
Recompute camera parameters from the current scene geometry.
Expand All @@ -67,7 +67,8 @@ def refresh_scene(self):
bounds = self.scene.geom_node.getBounds()
center = bounds.getCenter()
radius = bounds.getRadius()
if radius <= 0: radius = 1 # Sécurité si modèle vide
if radius <= 0:
radius = 1 # Sécurité si modèle vide

# 2. Positionner le pivot au centre de l'objet
self.focal_node.setPos(center)
Expand Down Expand Up @@ -100,10 +101,12 @@ def create_marker(self):
"""
ls = LineSegs()
ls.setThickness(2)
for i, col in enumerate([(1,0,0,1), (0,1,0,1), (0,0,1,1)]):
for i, col in enumerate([(1, 0, 0, 1), (0, 1, 0, 1), (0, 0, 1, 1)]):
ls.setColor(col)
v = LVector3(0,0,0); v[i] = 0.5
ls.moveTo(0,0,0); ls.drawTo(v)
v = LVector3(0, 0, 0)
v[i] = 0.5
ls.moveTo(0, 0, 0)
ls.drawTo(v)
return NodePath(ls.create())

def handle_rotate(self, dx, dy):
Expand All @@ -114,7 +117,8 @@ def handle_rotate(self, dx, dy):
dx: Normalised horizontal delta (screen space, -1..1).
dy: Normalised vertical delta (screen space, -1..1).
"""
if self.is_animating: return
if self.is_animating:
return
sens = 100.0
new_h = self.focal_node.getH() - dx * sens
new_p = self.focal_node.getP() + dy * sens
Expand All @@ -131,7 +135,8 @@ def handle_pan(self, dx, dy):
dx: Normalised horizontal delta (screen space, -1..1).
dy: Normalised vertical delta (screen space, -1..1).
"""
if self.is_animating: return
if self.is_animating:
return

# Largeur de la vue actuelle
fs = self.lens.getFilmSize().getX()
Expand All @@ -155,7 +160,8 @@ def handle_zoom(self, factor):
factor: Multiplier applied to the current film size
(< 1 zooms in, > 1 zooms out).
"""
if self.is_animating: return
if self.is_animating:
return

# 1. Calculer la nouvelle taille de vue
current_size = self.lens.getFilmSize().getX()
Expand All @@ -180,21 +186,20 @@ def recenter(self):
Only the pivot position is animated; rotation and zoom are left
unchanged so the user keeps their current viewing angle.
"""
if self.is_animating: return
if self.is_animating:
return
self.is_animating = True

# On récupère le centre réel de l'objet calculé dans analyze_model
# target_pos est un LPoint3 (le centre géométrique du modèle)
target_pos = self.model_center

duration = 0.4 # Un peu plus rapide pour un recadrage fluide
duration = 0.4 # Un peu plus rapide pour un recadrage fluide

# On ne crée qu'UN SEUL intervalle : la position.
# On ne touche NI au HPR (rotation) NI au FilmSize (zoom).
self.cam_anim = self.focal_node.posInterval(
duration,
target_pos,
blendType='easeInOut'
duration, target_pos, blendType="easeInOut"
)
# Séquence pour déverrouiller à la fin
Sequence(self.cam_anim, Func(self._unlock)).start()
Expand All @@ -214,43 +219,46 @@ def align_to_plane(self, axis):
axis: One of ``"x"`` (right view), ``"y"`` (front view) or
``"z"`` (top view).
"""
if self.is_animating: return
if self.is_animating:
return

# 1. Définir les rotations cibles (Heading, Pitch, Roll)
if axis == "z": # Vue de dessus (Top)
if axis == "z": # Vue de dessus (Top)
target_hpr = LPoint3(0, -90, 0)
elif axis == "y": # Vue de face (Front)
elif axis == "y": # Vue de face (Front)
target_hpr = LPoint3(0, 0, 0)
elif axis == "x": # Vue de côté (Right)
elif axis == "x": # Vue de côté (Right)
target_hpr = LPoint3(90, 0, 0)
else:
return

self.is_animating = True

duration = 0.5
# 2. Animation fluide de la rotation du pivot
# 2. Animation fluide de la rotation du pivot
# On peut aussi combiner cela avec un recentrage automatique
center = LPoint3(*self.scene.bounds['center'])
max_dim = max(self.scene.bounds['size']) if max(self.scene.bounds['size']) > 0 else 1.0
center = LPoint3(*self.scene.bounds["center"])
max_dim = (
max(self.scene.bounds["size"])
if max(self.scene.bounds["size"]) > 0
else 1.0
)
self.transition = Parallel(
# 1. Aligne la rotation sur l'axe demandé
self.focal_node.hprInterval(duration, target_hpr, blendType='easeInOut'),

self.focal_node.hprInterval(duration, target_hpr, blendType="easeInOut"),
# 2. Déplace le pivot vers le centre réel du modèle
self.focal_node.posInterval(duration, center, blendType='easeInOut'),
LerpFunc(lambda s: self.lens.setFilmSize(s),
fromData=self.lens.getFilmSize().getX(),
toData=max_dim * 1.5,
duration=duration,
blendType='easeInOut'),
self.focal_node.posInterval(duration, center, blendType="easeInOut"),
LerpFunc(
lambda s: self.lens.setFilmSize(s),
fromData=self.lens.getFilmSize().getX(),
toData=max_dim * 1.5,
duration=duration,
blendType="easeInOut",
),
)

# On lance et on déverrouille à la fin
Sequence(
self.transition,
Func(self._unlock)
).start()
Sequence(self.transition, Func(self._unlock)).start()

# Parallel(
# self.focal_node.hprInterval(duration, target_hpr, blendType='easeInOut'),
Expand All @@ -265,7 +273,6 @@ def align_to_plane(self, axis):

# taskMgr.doMethodLater(duration, self._unlock, "UnlockTask")


def update_task(self, task):
"""
Per-frame task: keep the pivot marker and gizmo in sync with the camera.
Expand All @@ -276,7 +283,6 @@ def update_task(self, task):
# Le marqueur suit le pivot
self.marker.setPos(self.focal_node.getPos())
# Mise à jour du Gizmo (orientation de la caméra vers le monde)
if hasattr(self.scene, 'gizmo'):
if hasattr(self.scene, "gizmo"):
self.scene.gizmo.update(self.base.camera.getQuat(self.base.render))
return task.cont

Loading
Loading