diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d448594..ca4d6ef 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,7 +6,9 @@ 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 @@ -14,14 +16,25 @@ jobs: 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 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b6bf54a..47dec96 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,7 +6,9 @@ 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 @@ -14,22 +16,34 @@ jobs: 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 + # - name: Upload coverage to Codecov + # uses: codecov/codecov-action@v4 + # with: + # token: ${{ secrets.CODECOV_TOKEN }} + # file: ./coverage.xml + # fail_ci_if_error: true diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..f0c7960 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "ferrispline"] + path = ferrispline + url = https://github.com/LIHPC-Computational-Geometry/ferrispline diff --git a/README.md b/README.md index 95986e2..89688f1 100644 --- a/README.md +++ b/README.md @@ -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 ``` @@ -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 diff --git a/bot/__init__.py b/bot/__init__.py index 8de0e48..adae8b6 100644 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -24,4 +24,4 @@ CADModel = None Model = CADModel -__all__ = ['core', 'control', 'view', 'viewer', 'Model', 'Viewer'] +__all__ = ["core", "control", "view", "viewer", "Model", "Viewer"] diff --git a/bot/control/__init__.py b/bot/control/__init__.py index 52ee808..d327a7b 100644 --- a/bot/control/__init__.py +++ b/bot/control/__init__.py @@ -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 * diff --git a/bot/control/camera.py b/bot/control/camera.py index de44bde..4d40827 100644 --- a/bot/control/camera.py +++ b/bot/control/camera.py @@ -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. @@ -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. @@ -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. @@ -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) @@ -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): @@ -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 @@ -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() @@ -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() @@ -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() @@ -214,14 +219,15 @@ 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 @@ -229,28 +235,30 @@ def align_to_plane(self, axis): 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'), @@ -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. @@ -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 - diff --git a/bot/control/keyboard.py b/bot/control/keyboard.py index 0e6c1ad..9df7b7e 100644 --- a/bot/control/keyboard.py +++ b/bot/control/keyboard.py @@ -1,6 +1,7 @@ from direct.showbase.InputStateGlobal import inputState import sys + class KeyboardHandler: """ Handles keyboard input and dispatches camera commands via the Panda3D messenger. @@ -20,44 +21,54 @@ def __init__(self, base): # Dictionnaire pour stocker l'état des touches self.keys = {"arrow_left": 0, "arrow_right": 0, "arrow_up": 0, "arrow_down": 0} - + # On écoute l'appui et le relâchement for key in self.keys: self.base.accept(key, self.set_key, [key, 1]) self.base.accept(key + "-up", self.set_key, [key, 0]) # Actions directes (Events) - self.base.accept('escape', sys.exit) - self.base.accept('f5', lambda: base.messenger.send("cmd_hot_reload")) - self.base.accept('c', lambda: base.messenger.send("cmd_center")) - self.base.accept('alt-x', lambda: base.messenger.send("cmd_align_plane", ["x"])) - self.base.accept('alt-y', lambda: base.messenger.send("cmd_align_plane", ["y"])) - self.base.accept('alt-z', lambda: base.messenger.send("cmd_align_plane", ["z"])) - - self.base.accept('x', lambda: base.messenger.send("cmd_axis_constraint", [1])) - self.base.accept('y', lambda: base.messenger.send("cmd_axis_constraint", [2])) - self.base.accept('z', lambda: base.messenger.send("cmd_axis_constraint", [4])) - self.base.accept('shift-x', lambda: base.messenger.send("cmd_axis_constraint", [6])) - self.base.accept('shift-y', lambda: base.messenger.send("cmd_axis_constraint", [5])) - self.base.accept('shift-z', lambda: base.messenger.send("cmd_axis_constraint", [3])) + self.base.accept("escape", sys.exit) + self.base.accept("f5", lambda: base.messenger.send("cmd_hot_reload")) + self.base.accept("c", lambda: base.messenger.send("cmd_center")) + self.base.accept("alt-x", lambda: base.messenger.send("cmd_align_plane", ["x"])) + self.base.accept("alt-y", lambda: base.messenger.send("cmd_align_plane", ["y"])) + self.base.accept("alt-z", lambda: base.messenger.send("cmd_align_plane", ["z"])) + + self.base.accept("x", lambda: base.messenger.send("cmd_axis_constraint", [1])) + self.base.accept("y", lambda: base.messenger.send("cmd_axis_constraint", [2])) + self.base.accept("z", lambda: base.messenger.send("cmd_axis_constraint", [4])) + self.base.accept( + "shift-x", lambda: base.messenger.send("cmd_axis_constraint", [6]) + ) + self.base.accept( + "shift-y", lambda: base.messenger.send("cmd_axis_constraint", [5]) + ) + self.base.accept( + "shift-z", lambda: base.messenger.send("cmd_axis_constraint", [3]) + ) # Axis-constraint shortcuts (0..7 mask). # x=1, y=2, z=4 for mask in range(8): - self.base.accept(str(mask), lambda m=mask: base.messenger.send("cmd_axis_constraint", [m])) + self.base.accept( + str(mask), + lambda m=mask: base.messenger.send("cmd_axis_constraint", [m]), + ) # Flèches directionnelles - #self.base.accept('arrow_left', lambda: base.messenger.send("cmd_pan", [-1, 0])) - #self.base.accept('arrow_right', lambda: base.messenger.send("cmd_pan", [1, 0])) - #self.base.accept('arrow_up', lambda: base.messenger.send("cmd_pan", [0, 1])) - #self.base.accept('arrow_down', lambda: base.messenger.send("cmd_pan", [0, -1])) - - self.base.accept('p', lambda: base.messenger.send("cmd_toggle_marker")) - inputState.watchWithModifiers('up', 'arrow_up') - inputState.watchWithModifiers('down', 'arrow_down') - inputState.watchWithModifiers('left', 'arrow_left') - inputState.watchWithModifiers('right', 'arrow_right') + # self.base.accept('arrow_left', lambda: base.messenger.send("cmd_pan", [-1, 0])) + # self.base.accept('arrow_right', lambda: base.messenger.send("cmd_pan", [1, 0])) + # self.base.accept('arrow_up', lambda: base.messenger.send("cmd_pan", [0, 1])) + # self.base.accept('arrow_down', lambda: base.messenger.send("cmd_pan", [0, -1])) + + self.base.accept("p", lambda: base.messenger.send("cmd_toggle_marker")) + inputState.watchWithModifiers("up", "arrow_up") + inputState.watchWithModifiers("down", "arrow_down") + inputState.watchWithModifiers("left", "arrow_left") + inputState.watchWithModifiers("right", "arrow_right") # On ajoute une tâche pour traiter le mouvement fluide self.base.taskMgr.add(self.move_task, "KeyboardMoveTask") + def set_key(self, key, value): """Update the pressed state of *key* (1 = down, 0 = up).""" self.keys[key] = value @@ -74,13 +85,13 @@ def move_task(self, task): # On calcule le vecteur de direction selon les touches pressées dx = self.keys["arrow_right"] - self.keys["arrow_left"] dy = self.keys["arrow_up"] - self.keys["arrow_down"] - + if dx != 0 or dy != 0: # On envoie une petite valeur de déplacement constante # On multiplie par globalClock.getDt() pour que la vitesse soit # la même peu importe la puissance du PC (Frame Rate Independent) dt = self.base.clock.getDt() - speed = 0.5 # Ajustez cette valeur pour la sensibilité clavier + speed = 0.5 # Ajustez cette valeur pour la sensibilité clavier self.base.messenger.send("cmd_pan", [dx * speed * dt, dy * speed * dt]) - - return task.cont \ No newline at end of file + + return task.cont diff --git a/bot/control/mouse.py b/bot/control/mouse.py index 8403eda..91bee30 100644 --- a/bot/control/mouse.py +++ b/bot/control/mouse.py @@ -1,6 +1,12 @@ from direct.showbase.InputStateGlobal import inputState -from panda3d.core import BitMask32, CollisionHandlerQueue, CollisionNode, CollisionRay, CollisionTraverser -from panda3d.core import GeomNode, MouseButton, Plane, Point2, Point3, Vec3 +from panda3d.core import ( + BitMask32, + CollisionHandlerQueue, + CollisionNode, + CollisionRay, + CollisionTraverser, +) +from panda3d.core import MouseButton, Plane, Point2, Point3, Vec3 class MouseHandler: @@ -25,7 +31,7 @@ def __init__(self, base): self.picker = CollisionTraverser() self.pq = CollisionHandlerQueue() - self.pickerNode = CollisionNode('mouseRay') + self.pickerNode = CollisionNode("mouseRay") self.pickerNP = self.base.camera.attachNewNode(self.pickerNode) self.pickerNode.setFromCollideMask(BitMask32.bit(1) | BitMask32.bit(2)) self.pickerNode.setIntoCollideMask(BitMask32.allOff()) @@ -46,8 +52,12 @@ def __init__(self, base): self.axis_constraint_mask = 7 self._last_drag_emit = 0.0 - self.base.accept("wheel_up", lambda: self.base.messenger.send("cmd_zoom", [0.9])) - self.base.accept("wheel_down", lambda: self.base.messenger.send("cmd_zoom", [1.1])) + self.base.accept( + "wheel_up", lambda: self.base.messenger.send("cmd_zoom", [0.9]) + ) + self.base.accept( + "wheel_down", lambda: self.base.messenger.send("cmd_zoom", [1.1]) + ) self.base.taskMgr.add(self.update, "MouseTask") @@ -86,13 +96,18 @@ def _reset_drag_state(self): def _finalize_drag(self, world_pos): if self.drag_curve_tag is not None and self.drag_cp_index is not None: if getattr(self.base, "_scene", None) is not None: - self.base._scene.set_cp_color(self.drag_curve_tag, self.drag_cp_index, [0.5, 0.5, 0.5, 1]) + self.base._scene.set_cp_color( + self.drag_curve_tag, self.drag_cp_index, [0.5, 0.5, 0.5, 1] + ) self.base._scene.hide_axis_guide() - self.base._on_event_cb('cp_pick_end', { - 'tag': self.drag_curve_tag, - 'cp_index': self.drag_cp_index, - 'world_pos': world_pos, - }) + self.base._on_event_cb( + "cp_pick_end", + { + "tag": self.drag_curve_tag, + "cp_index": self.drag_cp_index, + "world_pos": world_pos, + }, + ) self._reset_drag_state() def _pick_entry(self, m_pos, mask: BitMask32): @@ -107,10 +122,14 @@ def _pick_entry(self, m_pos, mask: BitMask32): def _entry_metadata(self, entry): np = entry.getIntoNodePath() return { - 'curve_tag': np.getNetTag('curve_tag') if np.hasNetTag('curve_tag') else None, - 'cp_index': np.getNetTag('cp_index') if np.hasNetTag('cp_index') else None, - 'pick_kind': np.getNetTag('pick_kind') if np.hasNetTag('pick_kind') else None, - 'point': entry.getSurfacePoint(self.base.render), + "curve_tag": np.getNetTag("curve_tag") + if np.hasNetTag("curve_tag") + else None, + "cp_index": np.getNetTag("cp_index") if np.hasNetTag("cp_index") else None, + "pick_kind": np.getNetTag("pick_kind") + if np.hasNetTag("pick_kind") + else None, + "point": entry.getSurfacePoint(self.base.render), } def _handle_hover(self, m_pos): @@ -119,13 +138,13 @@ def _handle_hover(self, m_pos): hover_entry = self._pick_entry(m_pos, BitMask32.bit(1)) if hover_entry is not None: metadata = self._entry_metadata(hover_entry) - hovered_tag = metadata['curve_tag'] + hovered_tag = metadata["curve_tag"] # Si on survole une nouvelle courbe (ou si on ne survole plus rien) if hovered_tag != self.last_hovered_tag: self.last_hovered_tag = hovered_tag # On envoie l'info au parent via le callback - self.base._on_event_cb('hover', hovered_tag) + self.base._on_event_cb("hover", hovered_tag) def _build_drag_plane(self, start_point: Point3): normal = self.base.render.getRelativeVector(self.base.cam, Vec3(0, 1, 0)) @@ -207,7 +226,9 @@ def _mouse_to_constrained_axis(self, m_pos): } axis_origin = Point3(start[0], start[1], start[2]) axis_dir = axis_map[mask] - result = self._closest_point_on_axis_to_ray(ray_origin, ray_dir, axis_origin, axis_dir) + result = self._closest_point_on_axis_to_ray( + ray_origin, ray_dir, axis_origin, axis_dir + ) if result is not None: return result fallback = self._mouse_to_plane(m_pos) @@ -227,7 +248,9 @@ def _mouse_to_constrained_axis(self, m_pos): return self._apply_axis_constraint(start, fallback) def _handle_cp_interaction(self, m_pos, left_down): - if not getattr(self, "edit_mode_enabled", False) and not getattr(self, "dragging_cp", False): + if not getattr(self, "edit_mode_enabled", False) and not getattr( + self, "dragging_cp", False + ): return # NOTE: Check si une interaction pick un cp vient de commencer @@ -237,26 +260,44 @@ def _handle_cp_interaction(self, m_pos, left_down): if entry is None: return metadata = self._entry_metadata(entry) - if metadata['pick_kind'] != 'cp' or metadata['curve_tag'] is None or metadata['cp_index'] is None: + if ( + metadata["pick_kind"] != "cp" + or metadata["curve_tag"] is None + or metadata["cp_index"] is None + ): return - if self.active_curve_tag is not None and metadata['curve_tag'] != self.active_curve_tag: + if ( + self.active_curve_tag is not None + and metadata["curve_tag"] != self.active_curve_tag + ): return self.dragging_cp = True - self.drag_curve_tag = metadata['curve_tag'] - self.drag_cp_index = int(metadata['cp_index']) - self.drag_plane = self._build_drag_plane(metadata['point']) - self.drag_start_world_pos = [metadata['point'][0], metadata['point'][1], metadata['point'][2]] + self.drag_curve_tag = metadata["curve_tag"] + self.drag_cp_index = int(metadata["cp_index"]) + self.drag_plane = self._build_drag_plane(metadata["point"]) + self.drag_start_world_pos = [ + metadata["point"][0], + metadata["point"][1], + metadata["point"][2], + ] self.drag_last_valid_world_pos = list(self.drag_start_world_pos) self.drag_active_mask = int(self.axis_constraint_mask) - self.base._scene.set_cp_color(self.drag_curve_tag, self.drag_cp_index, [1, 0.5, 0, 1]) + self.base._scene.set_cp_color( + self.drag_curve_tag, self.drag_cp_index, [1, 0.5, 0, 1] + ) if getattr(self.base, "_scene", None) is not None: - self.base._scene.show_axis_guide(self.drag_start_world_pos, self.drag_active_mask) - self.base._on_event_cb('cp_pick_start', { - 'tag': self.drag_curve_tag, - 'cp_index': self.drag_cp_index, - 'world_pos': self.drag_start_world_pos, - }) + self.base._scene.show_axis_guide( + self.drag_start_world_pos, self.drag_active_mask + ) + self.base._on_event_cb( + "cp_pick_start", + { + "tag": self.drag_curve_tag, + "cp_index": self.drag_cp_index, + "world_pos": self.drag_start_world_pos, + }, + ) return # NOTE: le drag du cp est en cours, un envoie régulier de la nouvelle position du cp est envoyé au processus parent @@ -267,13 +308,18 @@ def _handle_cp_interaction(self, m_pos, left_down): return self.drag_last_valid_world_pos = list(world_pos) if getattr(self.base, "_scene", None) is not None: - self.base._scene.preview_control_point(int(self.drag_curve_tag), self.drag_cp_index, world_pos) + self.base._scene.preview_control_point( + int(self.drag_curve_tag), self.drag_cp_index, world_pos + ) self.base._scene.update_axis_guide(world_pos, self.drag_active_mask) - self.base._on_event_cb('cp_drag', { - 'tag': self.drag_curve_tag, - 'cp_index': self.drag_cp_index, - 'world_pos': world_pos, - }) + self.base._on_event_cb( + "cp_drag", + { + "tag": self.drag_curve_tag, + "cp_index": self.drag_cp_index, + "world_pos": world_pos, + }, + ) return # NOTE: Fin de déplacement du cp, envoie de la posistion final du cp @@ -293,8 +339,8 @@ def _handle_curve_click(self, m_pos, left_down): if entry is None: return metadata = self._entry_metadata(entry) - if metadata['pick_kind'] == 'curve' and metadata['curve_tag'] is not None: - self.base._on_event_cb('curve_selected', metadata['curve_tag']) + if metadata["pick_kind"] == "curve" and metadata["curve_tag"] is not None: + self.base._on_event_cb("curve_selected", metadata["curve_tag"]) def _handle_drag(self, curr_pos): """Handles mouse drag for rotating and panning.""" @@ -310,9 +356,13 @@ def _handle_drag(self, curr_pos): # On n'envoie le message QUE si la souris a réellement bougé if delta.lengthSquared() > 0: if self.base.mouseWatcherNode.isButtonDown("shift"): - self.base.messenger.send("cmd_pan", [delta.getX(), delta.getY()]) + self.base.messenger.send( + "cmd_pan", [delta.getX(), delta.getY()] + ) else: - self.base.messenger.send("cmd_rotate", [delta.getX(), delta.getY()]) + self.base.messenger.send( + "cmd_rotate", [delta.getX(), delta.getY()] + ) # CRITIQUE : On met à jour prev_mouse_pos À CHAQUE FRAME # pour que le delta reste minuscule entre deux frames. @@ -346,4 +396,4 @@ def update(self, task): def is_shift_down(self): """Return True if the Shift modifier is currently held.""" - return inputState.isSet('shift') + return inputState.isSet("shift") diff --git a/bot/core/__init__.py b/bot/core/__init__.py index d46eca0..546e02a 100644 --- a/bot/core/__init__.py +++ b/bot/core/__init__.py @@ -1,6 +1,3 @@ """ Kernel module: The model module gathers the business object of our application """ - -from . import * - diff --git a/bot/core/cad.py b/bot/core/cad.py index f775267..d9551c4 100644 --- a/bot/core/cad.py +++ b/bot/core/cad.py @@ -5,15 +5,17 @@ """"global value to indicate how to round floating numbers""" nb_digit_rounding = 4 + class Model: """ The geometric model provides simple and basic function to load geometric files and to query a geometric model based on the OpenCascade technology. To do so, we totally rely on the gmsh library. """ + def __init__(self): self.initialize() self._observers = [] - self.bounds = {'min': [0,0,0], 'max': [0,0,0]} + self.bounds = {"min": [0, 0, 0], "max": [0, 0, 0]} self.curves = {} def set_curve(self, tag: int, curve_object): @@ -58,7 +60,7 @@ def finalize(self): """ gmsh.finalize() - def open(self,filename): + def open(self, filename): """ core.cad.Model.open(filename) @@ -69,7 +71,7 @@ def open(self,filename): - `filename`: string """ if Path(filename).suffix == ".geo": - gmsh.open(filename) + gmsh.open(filename) else: gmsh.model.occ.importShapes(filename) @@ -81,16 +83,22 @@ def _recompute_bounds(self): node_tags, coords, _ = gmsh.model.mesh.getNodes() # The map will be used later to store some geom info # node_map = {tag: i for i, tag in enumerate(node_tags)} - self.points = [(coords[i], coords[i+1], coords[i+2]) for i in range(0, len(coords), 3)] + self.points = [ + (coords[i], coords[i + 1], coords[i + 2]) for i in range(0, len(coords), 3) + ] if not self.points: return - #Compute now the bounds for automatic rescaling + # Compute now the bounds for automatic rescaling xs, ys, zs = zip(*self.points) self.bounds = { - 'min': [min(xs), min(ys), min(zs)], - 'max': [max(xs), max(ys), max(zs)], - 'center': [(min(xs)+max(xs))/2, (min(ys)+max(ys))/2, (min(zs)+max(zs))/2], - 'size': [max(xs)-min(xs), max(ys)-min(ys), max(zs)-min(zs)], + "min": [min(xs), min(ys), min(zs)], + "max": [max(xs), max(ys), max(zs)], + "center": [ + (min(xs) + max(xs)) / 2, + (min(ys) + max(ys)) / 2, + (min(zs) + max(zs)) / 2, + ], + "size": [max(xs) - min(xs), max(ys) - min(ys), max(zs) - min(zs)], } def get_render_data(self) -> dict: @@ -101,7 +109,7 @@ def get_render_data(self) -> dict: node_tags, coords, _ = gmsh.model.mesh.getNodes() node_id_to_coords = { - tag: (coords[i*3], coords[i*3+1], coords[i*3+2]) + tag: (coords[i * 3], coords[i * 3 + 1], coords[i * 3 + 2]) for i, tag in enumerate(node_tags) } @@ -120,44 +128,48 @@ def get_render_data(self) -> dict: local_edges = [] for i in range(0, len(connectivity), 2): - for node_tag in (connectivity[i], connectivity[i+1]): + for node_tag in (connectivity[i], connectivity[i + 1]): if node_tag not in seen: seen[node_tag] = len(local_points) local_points.append(node_id_to_coords[node_tag]) - local_edges.append((seen[connectivity[i]], seen[connectivity[i+1]])) + local_edges.append((seen[connectivity[i]], seen[connectivity[i + 1]])) curve_entry = { - 'points': local_points, - 'edges': local_edges, - 'type': 'linear', + "points": local_points, + "edges": local_edges, + "type": "linear", } # Surcharge si courbe paramétrique connue if tag in self.curves: custom = self.curves[tag] render = custom.get_render_data() - curve_entry.update({ - 'type': 'bezier', - 'degree': render['degree'], - 'control_points': render['control_points'], - 'cp_edges': [(i, i+1) for i in range(len(render['control_points'])-1)], - 'points': render['curve'], # écrase les pts gmsh - 'edges': [(i, i+1) for i in range(len(render['curve'])-1)], - }) + curve_entry.update( + { + "type": "bezier", + "degree": render["degree"], + "control_points": render["control_points"], + "cp_edges": [ + (i, i + 1) for i in range(len(render["control_points"]) - 1) + ], + "points": render["curve"], # écrase les pts gmsh + "edges": [(i, i + 1) for i in range(len(render["curve"]) - 1)], + } + ) curves_data[str(tag)] = curve_entry # Backward-compatible flattened payload expected by existing tests/viewers. offset = len(flat_points) - flat_points.extend(curve_entry['points']) - for idx_a, idx_b in curve_entry['edges']: + flat_points.extend(curve_entry["points"]) + for idx_a, idx_b in curve_entry["edges"]: flat_edges.append((offset + idx_a, offset + idx_b, int(tag))) return { - 'points': flat_points, - 'edges': flat_edges, - 'curves': curves_data, - 'bounds': dict(self.bounds), + "points": flat_points, + "edges": flat_edges, + "curves": curves_data, + "bounds": dict(self.bounds), } def add_point(self, coords: list, mesh_size: float = 1.0) -> int: @@ -176,14 +188,13 @@ def add_point(self, coords: list, mesh_size: float = 1.0) -> int: def __synchronize(self): """ - core.cad.Model.synchronize() + core.cad.Model.synchronize() - This operation must be invocated after applying geometric operations to be sure to have a consistent state. - We advise to call this method before starting the meshing stage. + This operation must be invocated after applying geometric operations to be sure to have a consistent state. + We advise to call this method before starting the meshing stage. """ gmsh.model.occ.synchronize() - def __get_cell_tags(self, dim): """ Private method that returns the tags, i.e. the ids, of the cells of dimension dim. @@ -208,7 +219,6 @@ def __get_cell_tags(self, dim): tags.append(j) return tags - def get_point_tags(self): """ core.cad.Model.get_point_tags(dim) @@ -233,8 +243,7 @@ def get_surface_tags(self): """ return self.__get_cell_tags(2) - - def getClosestPoint(self,dim, tag, coord): + def getClosestPoint(self, dim, tag, coord): """ core.cad.Model.getClosestPoint(dim, tag, coord) @@ -256,15 +265,22 @@ def getClosestPoint(self,dim, tag, coord): raise TypeError("the dimension parameter (dim) must be an int.") if not isinstance(tag, int): raise TypeError("the tag parameter (tag) must be an int.") - if not isinstance(coord, (list, tuple)) or len(coord)%3 != 0 or not all( - isinstance(x, numbers.Real) for x in coord): - raise TypeError("the coord parameter must be a list or tuple of 3 real numbers.") + if ( + not isinstance(coord, (list, tuple)) + or len(coord) % 3 != 0 + or not all(isinstance(x, numbers.Real) for x in coord) + ): + raise TypeError( + "the coord parameter must be a list or tuple of 3 real numbers." + ) if dim < 1 or dim > 2: - raise ValueError("the dim parameter must be an int comprised between 1 and 2.") + raise ValueError( + "the dim parameter must be an int comprised between 1 and 2." + ) return gmsh.model.getClosestPoint(dim, tag, coord)[0] - def get_end_points(self,curve_tag): + def get_end_points(self, curve_tag): """ core.cad.Model.get_end_points(curve_tag) @@ -277,7 +293,7 @@ def get_end_points(self,curve_tag): def get_adjacent_curves_of_point(self, point_tag): # We check if the tag is an existing point tag - if not point_tag in self.get_point_tags() : + if point_tag not in self.get_point_tags(): raise ValueError("Invalid point tag") curves = [] @@ -285,9 +301,9 @@ def get_adjacent_curves_of_point(self, point_tag): for curve_tag in self.get_curve_tags(): # get curve end points end_points = self.get_end_points(curve_tag) - if end_points[0] == point_tag : + if end_points[0] == point_tag: curves.append(curve_tag) - elif end_points[1] == point_tag : + elif end_points[1] == point_tag: curves.append(curve_tag) return curves @@ -380,13 +396,10 @@ def get_curve_discretization(self): # Get all the entities of dim 1 elem_types, elem_tags, elem_node_tags = gmsh.model.mesh.getElements(dim=1) - # On transforme la liste plate [x,y,z,x,y,z] en liste de tuples [(x,y,z), ...] points_3d = [] for i in range(0, len(coords), 3): - points_3d.append((coords[i], coords[i+1], coords[i+2])) - - + points_3d.append((coords[i], coords[i + 1], coords[i + 2])) # 2. Liste finale des données # Structure : (index_sommet_A, index_sommet_B, tag_courbe_origine) @@ -396,7 +409,7 @@ def get_curve_discretization(self): entities = gmsh.model.getEntities(1) for entity in entities: - curve_tag = entity[1] # C'est le Tag de la courbe CAO + curve_tag = entity[1] # C'est le Tag de la courbe CAO # Récupérer les éléments (segments) appartenant UNIQUEMENT à cette courbe _, _, node_tags_per_elem = gmsh.model.mesh.getElements(1, curve_tag) @@ -407,7 +420,7 @@ def get_curve_discretization(self): # Parcourir les nœuds par paires pour former les arêtes for i in range(0, len(connectivity), 2): tag_a = connectivity[i] - tag_b = connectivity[i+1] + tag_b = connectivity[i + 1] # Conversion en indices (0, 1, 2...) pour Panda3D idx_a = node_id_to_index[tag_a] @@ -427,29 +440,31 @@ def __discretize_surfaces(self): gmsh.model.mesh.generate(2) # Get mesh nodes node_tags, node_coords, _ = gmsh.model.mesh.getNodes() - num_nodes = len(node_tags) node_coords_3d = list(zip(*(iter(node_coords),) * 3)) # (x, y, z) tuples - # Build a node_id -> (x, y, z) map for later use - node_map = dict(zip(node_tags, node_coords_3d)) + # NOTE: Build a node_id -> (x, y, z) map for later use + node_map = dict(zip(node_tags, node_coords_3d)) # noqa: F841 # === 2. Get all face elements (triangles & quads) === surfaces = [] # type 1 = edge, type 2 = triangle, type 15 = point surf_tags = self.get_surface_tags() for i in surf_tags: - element_types, element_tags, node_tags_list = gmsh.model.mesh.getElements(2, i) - faces=[] + element_types, element_tags, node_tags_list = gmsh.model.mesh.getElements( + 2, i + ) + faces = [] for elem_tags, elem_node_tags in zip(element_tags, node_tags_list): for i in range(len(elem_tags)): - elem_id = elem_tags[i] - node_ids = [nid - 1 for nid in elem_node_tags[i * 3: (i + 1) * 3]] + node_ids = [nid - 1 for nid in elem_node_tags[i * 3 : (i + 1) * 3]] faces.append(node_ids) surfaces.append(faces) return node_coords_3d, surfaces - def update_control_point(self, tag: int, cp_index: int, new_pt: list(float), notify: bool = True): + def update_control_point( + self, tag: int, cp_index: int, new_pt: list(float), notify: bool = True + ): """Met à jour un point de contrôle d'une courbe et rafraîchit l'affichage.""" if tag in self.curves: curve = self.curves[tag] @@ -469,6 +484,8 @@ def update_control_point(self, tag: int, cp_index: int, new_pt: list(float), not if notify: self._notify_observers() else: - print(f"Erreur : L'index {cp_index} n'existe pas. La courbe a {len(cps)} points de contrôle.") + print( + f"Erreur : L'index {cp_index} n'existe pas. La courbe a {len(cps)} points de contrôle." + ) else: - print(f"Erreur : La courbe {tag} n'est pas une courbe personnalisée.") \ No newline at end of file + print(f"Erreur : La courbe {tag} n'est pas une courbe personnalisée.") diff --git a/bot/core/curve.py b/bot/core/curve.py index bab118f..b494104 100644 --- a/bot/core/curve.py +++ b/bot/core/curve.py @@ -1,9 +1,11 @@ import nurbslib + class BezierCurve: """ This class acts as a bridge between your current geometry and the Rust lib. """ + def __init__(self, tag: str, control_points: list[list[float]], degree: int): self.tag = tag self._engine = nurbslib.PyBezierCurve(degree, control_points, None) @@ -26,10 +28,7 @@ def _default_control_points(coords_a, coords_b, degree=3): # Linear interpolation (lerp) for each axis (x, y, z) # Mathematical formula: point = A + (B - A) * t - current_point = [ - a + (b - a) * t - for a, b in zip(coords_a, coords_b) - ] + current_point = [a + (b - a) * t for a, b in zip(coords_a, coords_b)] points.append(current_point) @@ -50,8 +49,8 @@ def set_control_points(self, control_points: list[list[float]]): def get_render_data(self) -> dict: return { - 'tag': self.tag, - 'control_points': self.get_control_points(), - 'degree': self.get_degree(), - 'curve': self._engine.evaluate(100, False) - } \ No newline at end of file + "tag": self.tag, + "control_points": self.get_control_points(), + "degree": self.get_degree(), + "curve": self._engine.evaluate(100, False), + } diff --git a/bot/view/__init__.py b/bot/view/__init__.py index be39b7a..a0836a4 100644 --- a/bot/view/__init__.py +++ b/bot/view/__init__.py @@ -1,5 +1,3 @@ """ View module: This submodule of bot gathers classes and functions for 2D/3D rendering """ - -from . import * diff --git a/bot/view/curve_app.py b/bot/view/curve_app.py index 6f3349c..f2a3021 100644 --- a/bot/view/curve_app.py +++ b/bot/view/curve_app.py @@ -1,5 +1,12 @@ from panda3d.core import BitMask32, CollisionNode, CollisionSphere, CollisionTube -from panda3d.core import Geom, GeomNode, GeomPoints, GeomVertexData, GeomVertexFormat, GeomVertexWriter +from panda3d.core import ( + Geom, + GeomNode, + GeomPoints, + GeomVertexData, + GeomVertexFormat, + GeomVertexWriter, +) from panda3d.core import LineSegs, NodePath import nurbslib @@ -8,12 +15,13 @@ MASK_CURVE_PICK = BitMask32.bit(1) MASK_CP_PICK = BitMask32.bit(2) + class CurveApp: def __init__(self, tag: str, curve_data: dict): self.tag: int = int(tag) - self.edges: list = curve_data['edges'] - self.points: list = curve_data['points'] - self.type: str = curve_data['type'] + self.edges: list = curve_data["edges"] + self.points: list = curve_data["points"] + self.type: str = curve_data["type"] self.node_path: NodePath | None = None self.curve_render_node: NodePath | None = None @@ -31,23 +39,24 @@ def __init__(self, tag: str, curve_data: dict): self.line_thickness = 2 self.cp_color: list | None = None - if self.type == 'bezier' or self.type == 'bspline': - self.control_points = curve_data['control_points'] - self.cp_color = [[0.5, 0.5, 0.5, 1] for _ in range(len(self.control_points))] - self.degree = curve_data['degree'] + if self.type == "bezier" or self.type == "bspline": + self.control_points = curve_data["control_points"] + self.cp_color = [ + [0.5, 0.5, 0.5, 1] for _ in range(len(self.control_points)) + ] + self.degree = curve_data["degree"] # TODO: bspline / nurbs not already implemented - if self.type == 'bspline': - self.knots = curve_data['knots'] - + if self.type == "bspline": + self.knots = curve_data["knots"] def _draw_control_points(self, node_path: NodePath): format = GeomVertexFormat.getV3cp() # NOTE: correspond à l'espace mémoire utilisé pour stocker les points 3D - vdata = GeomVertexData('anchors', format, Geom.UHDynamic) + vdata = GeomVertexData("anchors", format, Geom.UHDynamic) # NOTE: permet d'écrire dans l'espace mémoire vdata dans la 'colonne' nommé 'vertex' - vertex = GeomVertexWriter(vdata, 'vertex') - color_writer = GeomVertexWriter(vdata, 'color') + vertex = GeomVertexWriter(vdata, "vertex") + color_writer = GeomVertexWriter(vdata, "color") # NOTE: Permet de dire au GPU de traiter les coordonnée en tant que point flottant (on pourrait très bien utiliser GeomTriangles(Geom.UHDynamic) pour dessiner des surffaces) prim = GeomPoints(Geom.UHDynamic) @@ -62,7 +71,7 @@ def _draw_control_points(self, node_path: NodePath): geom = Geom(vdata) geom.addPrimitive(prim) # NOTE: Emballe le Geom pour être lisible par le moteur 3D - gnode = GeomNode(f'anchors_{self.tag}') + gnode = GeomNode(f"anchors_{self.tag}") gnode.addGeom(geom) if self._cp_geom_node is not None: @@ -82,7 +91,6 @@ def _draw_control_points(self, node_path: NodePath): self._cp_line_node.removeNode() self._cp_line_node = node_path.attachNewNode(lines.create()) - def _attachCPNode(self): self.cp_render_node = self.node_path.attachNewNode("cp_render") self.cp_collision_node = self.node_path.attachNewNode("cp_collision") @@ -106,10 +114,9 @@ def _rebuild_cp_collision(self): cnode.setFromCollideMask(BitMask32.allOff()) cnode.addSolid(CollisionSphere(pt[0], pt[1], pt[2], 1.2)) cnp = self.cp_collision_node.attachNewNode(cnode) - cnp.setTag('curve_tag', str(self.tag)) - cnp.setTag('cp_index', str(i)) - cnp.setTag('pick_kind', 'cp') - + cnp.setTag("curve_tag", str(self.tag)) + cnp.setTag("cp_index", str(i)) + cnp.setTag("pick_kind", "cp") def _create_collision(self, cnode: NodePath): for idxA, idxB in self.edges: @@ -119,7 +126,6 @@ def _create_collision(self, cnode: NodePath): tube = CollisionTube(ptA[0], ptA[1], ptA[2], ptB[0], ptB[1], ptB[2], radius) cnode.addSolid(tube) - def _attachColissionNode(self): cnode = CollisionNode(f"col_{self.tag}") cnode.setFromCollideMask(BitMask32.allOff()) @@ -129,8 +135,8 @@ def _attachColissionNode(self): self.curve_collision_node.removeNode() self.curve_collision_node = self.node_path.attachNewNode("curve_collision") cnp = self.curve_collision_node.attachNewNode(cnode) - cnp.setTag('curve_tag', str(self.tag)) - cnp.setTag('pick_kind', 'curve') + cnp.setTag("curve_tag", str(self.tag)) + cnp.setTag("pick_kind", "curve") return cnp def _draw_curve(self): @@ -141,10 +147,9 @@ def _draw_curve(self): self._curve_geom_node.removeNode() self._curve_geom_node = self.curve_render_node.attachNewNode(lines.create()) - def attachCuveNode(self, node_path: NodePath): self.node_path = node_path - self.node_path.setTag('curve_tag', str(self.tag)) + self.node_path.setTag("curve_tag", str(self.tag)) self.curve_render_node = self.node_path.attachNewNode("curve_render") self._draw_curve() self._attachColissionNode() @@ -152,7 +157,6 @@ def attachCuveNode(self, node_path: NodePath): self._attachCPNode() return self.node_path - def set_cp_color(self, cp_index: int, color: list): self.cp_color[cp_index] = color if self.cp_node is not None: @@ -167,7 +171,6 @@ def set_color(self, color: list): else: self.cp_node.hide() - def draw_curve(self, lines: LineSegs): for idxA, idxB in self.edges: ptA = self.points[idxA] @@ -196,7 +199,7 @@ def preview_control_point(self, cp_index: int, new_pos: list[float]): self.control_points[cp_index] = [new_pos[0], new_pos[1], new_pos[2]] - if self.type == 'bezier' and self.degree is not None: + if self.type == "bezier" and self.degree is not None: engine = nurbslib.PyBezierCurve(int(self.degree), self.control_points, None) self.points = engine.evaluate(100, False) self.edges = [(i, i + 1) for i in range(len(self.points) - 1)] @@ -207,6 +210,3 @@ def preview_control_point(self, cp_index: int, new_pos: list[float]): self._draw_control_points(self.cp_render_node) self._attachColissionNode() self._rebuild_cp_collision() - - - diff --git a/bot/view/scene.py b/bot/view/scene.py index 8e5e772..c100e24 100644 --- a/bot/view/scene.py +++ b/bot/view/scene.py @@ -1,12 +1,21 @@ -from panda3d.core import LineSegs, NodePath, LColor, Vec4, Vec3, DirectionalLight, AmbientLight +from panda3d.core import ( + LineSegs, + NodePath, + LColor, + Vec4, + Vec3, + DirectionalLight, + AmbientLight, +) from panda3d.core import CollisionNode, CollisionTube -from panda3d.core import GeomVertexFormat, GeomVertexData, Geom, GeomPoints, GeomNode, GeomVertexWriter from bot.view.curve_app import CurveApp _DEFAULT_BOUNDS = { - 'min': [0, 0, 0], 'max': [0, 0, 0], - 'center': [0, 0, 0], 'size': [1, 1, 1], + "min": [0, 0, 0], + "max": [0, 0, 0], + "center": [0, 0, 0], + "size": [1, 1, 1], } @@ -38,7 +47,8 @@ def _create_axes(self): for i, col in enumerate([(1, 0, 0), (0, 1, 0), (0, 0, 1)]): ls.setColor(LColor(*col, 1)) ls.moveTo(0, 0, 0) - target = [0, 0, 0]; target[i] = 0.1 + target = [0, 0, 0] + target[i] = 0.1 ls.drawTo(*target) self.root.attachNewNode(ls.create()) @@ -75,9 +85,9 @@ def __init__(self, base, geom_data: dict, settings: dict): """ self.base = base self._geom_data = geom_data - self.background_color = settings.get('background_color', [0.1, 0.1, 0.12]) + self.background_color = settings.get("background_color", [0.1, 0.1, 0.12]) self.base.set_background_color(self.background_color) - self.line_thickness = settings.get('line_thickness', 2) + self.line_thickness = settings.get("line_thickness", 2) self.curves = {} self.active_curve_tag = None self.edit_mode_enabled = False @@ -89,10 +99,14 @@ def __init__(self, base, geom_data: dict, settings: dict): self._constraint_guide_visible = False self.geom_node = self._build_from_data(geom_data) - self._constraint_guide_np = self.base.render.attachNewNode("constraint_guide_root") + self._constraint_guide_np = self.base.render.attachNewNode( + "constraint_guide_root" + ) self._constraint_guide_np.hide() self._world_axes_np = self.base.render.attachNewNode("world_axes_root") - self._transform_gizmo_np = self.base.render.attachNewNode("transform_gizmo_root") + self._transform_gizmo_np = self.base.render.attachNewNode( + "transform_gizmo_root" + ) self._transform_gizmo_np.hide() self.gizmo = HUDGizmo(self.base.pixel2d) self.add_lighting() @@ -100,12 +114,11 @@ def __init__(self, base, geom_data: dict, settings: dict): @property def bounds(self) -> dict: """Bounding-box data of the current geometry (center, size, min, max).""" - return self._geom_data.get('bounds', _DEFAULT_BOUNDS) + return self._geom_data.get("bounds", _DEFAULT_BOUNDS) def _group_edges_by_tag(self, edges: list) -> dict: # NOTE: A curve is a liste of small edges. This function group all these edges for set the same color - """Groups a list of edges by their curve tag. - """ + """Groups a list of edges by their curve tag.""" edges_by_tag = {} for e in edges: idxA, idxB = e[0], e[1] @@ -118,7 +131,7 @@ def _group_edges_by_tag(self, edges: list) -> dict: def _create_curve_geometry(self, tag: str, tag_edges: list, points: list): """Creates the visible lines and invisible collision nodes for a set of edges.""" lines = LineSegs() - is_control_polygon = tag.endswith('_cp') + is_control_polygon = tag.endswith("_cp") if is_control_polygon: lines.setThickness(1.0) @@ -138,7 +151,9 @@ def _create_curve_geometry(self, tag: str, tag_edges: list, points: list): if not is_control_polygon: radius = 1.0 - tube = CollisionTube(ptA[0], ptA[1], ptA[2], ptB[0], ptB[1], ptB[2], radius) + tube = CollisionTube( + ptA[0], ptA[1], ptA[2], ptB[0], ptB[1], ptB[2], radius + ) cnode.addSolid(tube) if is_control_polygon: @@ -166,7 +181,7 @@ def _build_from_data(self, geom_data: dict): NodePath: The root node containing all visible and collision geometry, attached to `render`. Returns `None` if there are no edges. """ - curves = geom_data.get('curves', []) + curves = geom_data.get("curves", []) self.curves = {} @@ -191,7 +206,7 @@ def set_curve_color(self, tag: str, color: list): else: try: curve = self.curves.get(int(tag)) - except (TypeError, ValueError): + except TypeError, ValueError: curve = None if curve is not None: @@ -204,7 +219,7 @@ def set_cp_color(self, tag: str, cp_index: int, color: list): else: try: curve = self.curves.get(int(tag)) - except (TypeError, ValueError): + except TypeError, ValueError: curve = None if curve is not None: @@ -220,7 +235,7 @@ def set_edit_mode(self, enabled: bool): def set_active_curve(self, tag): try: normalized = int(tag) if tag is not None else None - except (TypeError, ValueError): + except TypeError, ValueError: normalized = None self.active_curve_tag = normalized for curve_tag, curve in self.curves.items(): @@ -236,43 +251,50 @@ def preview_control_point(self, tag: int, cp_index: int, new_pos: list[float]): def set_axis_constraint(self, mask: int): self.axis_constraint_mask = max(0, min(7, int(mask))) if self._constraint_guide_visible and self._constraint_guide_origin is not None: - self.update_axis_guide(self._constraint_guide_origin, self.axis_constraint_mask) + self.update_axis_guide( + self._constraint_guide_origin, self.axis_constraint_mask + ) def _guide_length(self) -> float: - size = self.bounds.get('size', [1, 1, 1]) + size = self.bounds.get("size", [1, 1, 1]) max_size = max(size) if size else 1 return max(2.0, float(max_size) * 0.15) def _world_axes_length(self) -> float: - size = self.bounds.get('size', [1, 1, 1]) + size = self.bounds.get("size", [1, 1, 1]) max_size = max(size) if size else 1 return max(1000.0, float(max_size) * 100.0) - def _draw_axis_line(self, root: NodePath, origin: list[float], axis: str, length: float): - colors = {'x': (1, 0, 0, 0.2), 'y': (0, 1, 0, 0.2), 'z': (0, 0, 1, 0.2)} - vectors = {'x': (1, 0, 0), 'y': (0, 1, 0), 'z': (0, 0, 1)} + def _draw_axis_line( + self, root: NodePath, origin: list[float], axis: str, length: float + ): + colors = {"x": (1, 0, 0, 0.2), "y": (0, 1, 0, 0.2), "z": (0, 0, 1, 0.2)} + vectors = {"x": (1, 0, 0), "y": (0, 1, 0), "z": (0, 0, 1)} color = colors[axis] vx, vy, vz = vectors[axis] ls = LineSegs() ls.setThickness(2) ls.setColor(*color) - ls.moveTo(origin[0] - vx * length, origin[1] - vy * length, origin[2] - vz * length) - ls.drawTo(origin[0] + vx * length, origin[1] + vy * length, origin[2] + vz * length) + ls.moveTo( + origin[0] - vx * length, origin[1] - vy * length, origin[2] - vz * length + ) + ls.drawTo( + origin[0] + vx * length, origin[1] + vy * length, origin[2] + vz * length + ) root.attachNewNode(ls.create()) - def _update_transform_gizmo(self, origin: list[float], mask: int): if self._transform_gizmo_np is None: return self._transform_gizmo_np.getChildren().detach() length = self._guide_length() if mask & 1: - self._draw_axis_line(self._transform_gizmo_np, origin, 'x', length) + self._draw_axis_line(self._transform_gizmo_np, origin, "x", length) if mask & 2: - self._draw_axis_line(self._transform_gizmo_np, origin, 'y', length) + self._draw_axis_line(self._transform_gizmo_np, origin, "y", length) if mask & 4: - self._draw_axis_line(self._transform_gizmo_np, origin, 'z', length) + self._draw_axis_line(self._transform_gizmo_np, origin, "z", length) def show_axis_guide(self, origin: list[float], mask: int): self._constraint_guide_visible = True @@ -289,11 +311,11 @@ def update_axis_guide(self, origin: list[float], mask: int): length = self._guide_length() if mask & 1: - self._draw_axis_line(self._constraint_guide_np, origin, 'x', length) + self._draw_axis_line(self._constraint_guide_np, origin, "x", length) if mask & 2: - self._draw_axis_line(self._constraint_guide_np, origin, 'y', length) + self._draw_axis_line(self._constraint_guide_np, origin, "y", length) if mask & 4: - self._draw_axis_line(self._constraint_guide_np, origin, 'z', length) + self._draw_axis_line(self._constraint_guide_np, origin, "z", length) self._update_transform_gizmo(origin, mask) def hide_axis_guide(self): @@ -306,7 +328,6 @@ def hide_axis_guide(self): self._transform_gizmo_np.getChildren().detach() self._transform_gizmo_np.hide() - def rebuild(self, geom_data: dict): """Replace displayed geometry with new render data.""" self._geom_data = geom_data @@ -328,7 +349,11 @@ def clear(self): if self._transform_gizmo_np is not None: self._transform_gizmo_np.removeNode() self._transform_gizmo_np = None - if hasattr(self, 'gizmo') and self.gizmo is not None and hasattr(self.gizmo, 'root'): + if ( + hasattr(self, "gizmo") + and self.gizmo is not None + and hasattr(self.gizmo, "root") + ): self.gizmo.root.removeNode() def apply_settings(self, settings: dict): @@ -340,11 +365,11 @@ def apply_settings(self, settings: dict): Args: settings: Partial or full scene configuration dict. """ - if 'background_color' in settings: - self.background_color = settings['background_color'] + if "background_color" in settings: + self.background_color = settings["background_color"] self.base.set_background_color(self.background_color) - if 'line_thickness' in settings: - self.line_thickness = settings['line_thickness'] + if "line_thickness" in settings: + self.line_thickness = settings["line_thickness"] if self.geom_node is not None: self.geom_node.removeNode() self.geom_node = self._build_from_data(self._geom_data) diff --git a/bot/viewer/__init__.py b/bot/viewer/__init__.py index 5670c3b..961657e 100644 --- a/bot/viewer/__init__.py +++ b/bot/viewer/__init__.py @@ -4,4 +4,4 @@ from .viewer import Viewer -__all__ = ['Viewer'] +__all__ = ["Viewer"] diff --git a/bot/viewer/app.py b/bot/viewer/app.py index 7339170..4893e53 100644 --- a/bot/viewer/app.py +++ b/bot/viewer/app.py @@ -13,14 +13,14 @@ from bot.control.keyboard import KeyboardHandler _DEFAULT_SCENE = { - 'background_color': [0.1, 0.1, 0.12], - 'line_thickness': 2, + "background_color": [0.1, 0.1, 0.12], + "line_thickness": 2, } _DEFAULT_CAMERA = { - 'pan_speed': 10.0, - 'rotate_speed': 100.0, - 'animation_duration': 0.5, - 'show_marker_at_start': False, + "pan_speed": 10.0, + "rotate_speed": 100.0, + "animation_duration": 0.5, + "show_marker_at_start": False, } @@ -60,18 +60,16 @@ def __init__(self, config_filename: str, cmd_queue: queue.Queue, on_event_cb): self.accept("cmd_axis_constraint", self._on_axis_constraint_cmd) self.hud = OnscreenText( - text="", - pos=(-1.3, -0.5), - scale=0.06, - fg=(1, 1, 1, 1), - align=TextNode.ALeft + text="", pos=(-1.3, -0.5), scale=0.06, fg=(1, 1, 1, 1), align=TextNode.ALeft ) - taskMgr.add(self._process_commands, "ViewerProcessCommands") + self.taskMgr.add(self._process_commands, "ViewerProcessCommands") def _load_config(self, config_filename: str) -> dict: """Load and parse the TOML config file. Returns an empty dict if not found.""" - base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + base_dir = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + ) path = os.path.join(base_dir, config_filename) if os.path.exists(path): with open(path, "rb") as f: @@ -80,12 +78,12 @@ def _load_config(self, config_filename: str) -> dict: def _scene_cfg(self) -> dict: """Return the ``[view.scene]`` section of the config, or built-in defaults.""" - return self._config.get('view', {}).get('scene', _DEFAULT_SCENE) + return self._config.get("view", {}).get("scene", _DEFAULT_SCENE) def _camera_cfg(self) -> dict: """Return the ``[view.camera]`` section merged with built-in defaults.""" cfg = _DEFAULT_CAMERA.copy() - cfg.update(self._config.get('view', {}).get('camera', {})) + cfg.update(self._config.get("view", {}).get("camera", {})) return cfg def _process_commands(self, task): @@ -100,36 +98,38 @@ def _process_commands(self, task): while not self._cmd_queue.empty(): try: cmd, data = self._cmd_queue.get_nowait() - if cmd == 'load': + if cmd == "load": self.load_scene(data) - elif cmd == 'update': + elif cmd == "update": self.update_scene(data) - elif cmd == 'reload_config': + elif cmd == "reload_config": self._config = data if self._scene: self._scene.apply_settings(self._scene_cfg()) if self._camera_controller: self._camera_controller.apply_settings(self._camera_cfg()) - elif cmd == 'highlight_curve': + elif cmd == "highlight_curve": if self._scene: - self._scene.set_curve_color(data['tag'], data['color']) - elif cmd == 'update_hud': - self.hud.setText(data['text']) - elif cmd == 'set_edit_mode': - enabled = bool(data.get('enabled', False)) - curve_tag = data.get('curve_tag') + self._scene.set_curve_color(data["tag"], data["color"]) + elif cmd == "update_hud": + self.hud.setText(data["text"]) + elif cmd == "set_edit_mode": + enabled = bool(data.get("enabled", False)) + curve_tag = data.get("curve_tag") self.mouse_handler.set_edit_mode(enabled, curve_tag) if self._scene: self._scene.set_edit_mode(enabled) if curve_tag is not None: self._scene.set_active_curve(curve_tag) - elif cmd == 'set_active_curve': - curve_tag = data.get('curve_tag') - self.mouse_handler.set_edit_mode(self.mouse_handler.edit_mode_enabled, curve_tag) + elif cmd == "set_active_curve": + curve_tag = data.get("curve_tag") + self.mouse_handler.set_edit_mode( + self.mouse_handler.edit_mode_enabled, curve_tag + ) if self._scene: self._scene.set_active_curve(curve_tag) - elif cmd == 'set_axis_constraint': - self._set_axis_constraint(data.get('mask', 7)) + elif cmd == "set_axis_constraint": + self._set_axis_constraint(data.get("mask", 7)) except queue.Empty: break return task.cont @@ -137,7 +137,7 @@ def _process_commands(self, task): def _set_axis_constraint(self, mask: int): try: raw_mask = int(mask) - except (TypeError, ValueError): + except TypeError, ValueError: raw_mask = 7 self.hud.setText("Axis constraint invalid, fallback to xyz (7).") self.axis_constraint_mask = max(0, min(7, raw_mask)) @@ -160,7 +160,9 @@ def load_scene(self, geom_data: dict): self._scene.set_axis_constraint(self.axis_constraint_mask) if self._camera_controller is None: - self._camera_controller = CameraController(self, self._scene, self._camera_cfg()) + self._camera_controller = CameraController( + self, self._scene, self._camera_cfg() + ) else: self._camera_controller.scene = self._scene diff --git a/bot/viewer/viewer.py b/bot/viewer/viewer.py index 0c8aeb8..6ef14d8 100644 --- a/bot/viewer/viewer.py +++ b/bot/viewer/viewer.py @@ -8,7 +8,6 @@ in the form of serializable dicts. """ -import math import multiprocessing as mp import threading from typing import Any, Callable, Optional, TYPE_CHECKING @@ -25,12 +24,14 @@ # Subprocess entry function (must be at module level for pickle) # --------------------------------------------------------------------------- + def _viewer_subprocess(conn, config_filename: str): """ Panda3D subprocess entry point. Runs on the subprocess main thread → macOS safe. """ import queue as _queue + cmd_queue = _queue.Queue() # Thread that reads the parent pipe → puts into the internal queue @@ -39,7 +40,7 @@ def _pipe_reader(): try: msg = conn.recv() cmd_queue.put(msg) - if msg[0] == 'exit': # Exit command + if msg[0] == "exit": # Exit command break except EOFError: break @@ -53,14 +54,16 @@ def on_event(event_type, data): pass from bot.viewer.app import ViewerApp + app = ViewerApp(config_filename, cmd_queue, on_event) - app.run() # blocking — intentional, it's the main thread of the subprocess + app.run() # blocking — intentional, it's the main thread of the subprocess # --------------------------------------------------------------------------- # Public API # --------------------------------------------------------------------------- + class Viewer: """ 3D Viewer connected to a Model core. @@ -82,9 +85,9 @@ class Viewer: def __init__(self, config_filename: str = "bot_config.toml"): self._config_filename = config_filename self.model: Optional[Model] = None - self._conn = None # parent end of the Pipe - self._process = None # Panda3D subprocess - self._event_thread = None # event listening thread + self._conn = None # parent end of the Pipe + self._process = None # Panda3D subprocess + self._event_thread = None # event listening thread self._running = False self._default_last_hovered = None @@ -96,37 +99,35 @@ def __init__(self, config_filename: str = "bot_config.toml"): self.on_cp_drag: Optional[Callable] = None self.on_cp_pick_end: Optional[Callable] = self._default_on_cp_pick_end - - # ------------------------------------------------------------------ # Public API # ------------------------------------------------------------------ def highlight_curve(self, tag: str, color: list) -> "Viewer": """Colors the geometry associated with a tag.""" - self._send('highlight_curve', {'tag': tag, 'color': color}) + self._send("highlight_curve", {"tag": tag, "color": color}) return self def set_hud_text(self, text: str) -> "Viewer": """Updates the text displayed in an overlay on the screen.""" - self._send('update_hud', {'text': text}) + self._send("update_hud", {"text": text}) return self def set_edit_mode(self, enabled: bool, curve_tag: Optional[int] = None) -> "Viewer": - self._send('set_edit_mode', {'enabled': enabled, 'curve_tag': curve_tag}) + self._send("set_edit_mode", {"enabled": enabled, "curve_tag": curve_tag}) return self def set_active_curve(self, curve_tag: Optional[int]) -> "Viewer": - self._send('set_active_curve', {'curve_tag': curve_tag}) + self._send("set_active_curve", {"curve_tag": curve_tag}) return self def set_axis_constraint(self, mask: int) -> "Viewer": try: normalized = int(mask) - except (TypeError, ValueError): + except TypeError, ValueError: normalized = 7 normalized = max(0, min(7, normalized)) - self._send('set_axis_constraint', {'mask': normalized}) + self._send("set_axis_constraint", {"mask": normalized}) return self def connect(self, model: Model) -> "Viewer": @@ -140,7 +141,7 @@ def connect(self, model: Model) -> "Viewer": self.model = model model.add_observer(self) if self._conn is not None: - self._send('load', model.get_render_data()) + self._send("load", model.get_render_data()) return self def disconnect(self) -> "Viewer": @@ -157,7 +158,7 @@ def run(self) -> "Viewer": """ self._running = True - ctx = mp.get_context('spawn') + ctx = mp.get_context("spawn") parent_conn, child_conn = ctx.Pipe() self._conn = parent_conn @@ -170,7 +171,7 @@ def run(self) -> "Viewer": child_conn.close() # useless in the parent process if self.model is not None: - self._send('load', self.model.get_render_data()) + self._send("load", self.model.get_render_data()) self._start_event_listener() return self @@ -187,15 +188,15 @@ def stop(self): # 2. Send the stop signal to the subprocess if self._conn is not None: try: - self._send('exit', None) - except: + self._send("exit", None) + except: # noqa: E722 pass # 3. Wait for the process to finish if self._process is not None: - self._process.join(timeout=2.0) # We allow 2 seconds to close + self._process.join(timeout=2.0) # We allow 2 seconds to close if self._process.is_alive(): - self._process.terminate() # Brute force if still alive + self._process.terminate() # Brute force if still alive self._process = None # 4. Close communication @@ -205,13 +206,14 @@ def stop(self): self._event_thread = None print("Stopped Viewer") + # ------------------------------------------------------------------ # Observer callback (called by Model when state changes) # ------------------------------------------------------------------ def update(self, model: Model): """Called by the Model through the observer pattern when geometry changes.""" - self._send('update', model.get_render_data()) + self._send("update", model.get_render_data()) # ------------------------------------------------------------------ # Internal helpers @@ -227,24 +229,34 @@ def _send(self, cmd: str, data): def _start_event_listener(self): """Lightweight thread that receives events from the subprocess (e.g., picking).""" + def _listen(): while self._running: try: if self._conn.poll(0.1): event_type, data = self._conn.recv() - if event_type == 'pick' and self.on_pick is not None: + if event_type == "pick" and self.on_pick is not None: self.on_pick(data) - elif event_type == 'hover' and self.on_hover is not None: + elif event_type == "hover" and self.on_hover is not None: self.on_hover(data) - elif event_type == 'curve_selected' and self.on_curve_selected is not None: + elif ( + event_type == "curve_selected" + and self.on_curve_selected is not None + ): self.on_curve_selected(data) - elif event_type == 'cp_pick_start' and self.on_cp_pick_start is not None: + elif ( + event_type == "cp_pick_start" + and self.on_cp_pick_start is not None + ): self.on_cp_pick_start(data) - elif event_type == 'cp_drag' and self.on_cp_drag is not None: + elif event_type == "cp_drag" and self.on_cp_drag is not None: self.on_cp_drag(data) - elif event_type == 'cp_pick_end' and self.on_cp_pick_end is not None: + elif ( + event_type == "cp_pick_end" + and self.on_cp_pick_end is not None + ): self.on_cp_pick_end(data) - except (EOFError, BrokenPipeError, AttributeError): + except EOFError, BrokenPipeError, AttributeError: break except Exception: pass @@ -273,7 +285,7 @@ def _default_on_hover(self, tag): pt_a = f"({coords_a[0]:.2f}, {coords_a[1]:.2f}, {coords_a[2]:.2f})" pt_b = f"({coords_b[0]:.2f}, {coords_b[1]:.2f}, {coords_b[2]:.2f})" - info_text += f"Type: Segment linéaire\n" + info_text += "Type: Segment linéaire\n" info_text += f"Extrémité A: {pt_a}\n" info_text += f"Extrémité B: {pt_b}" @@ -297,7 +309,7 @@ def _default_on_curve_selected(self, tag): return try: normalized = int(tag) - except (TypeError, ValueError): + except TypeError, ValueError: return self.set_edit_mode(True, normalized) self.set_active_curve(normalized) @@ -307,9 +319,9 @@ def _default_on_cp_pick_end(self, data): if self.model is None or data is None: return try: - tag = int(data['tag']) - cp_index = int(data['cp_index']) - world_pos = data.get('world_pos') + tag = int(data["tag"]) + cp_index = int(data["cp_index"]) + world_pos = data.get("world_pos") if world_pos is None: return self.model.update_control_point(tag, cp_index, world_pos) @@ -321,7 +333,9 @@ def bezier_conversion(self, degree: int): tag = int(self._default_last_hovered) if self.model is not None: coords_a, coords_b = self.model.get_end_points_coords(int(tag)) - control_points = BezierCurve._default_control_points(coords_a, coords_b, degree) + control_points = BezierCurve._default_control_points( + coords_a, coords_b, degree + ) curve = BezierCurve(tag, control_points, degree) self.model.set_curve(tag, curve) else: @@ -332,6 +346,8 @@ def bezier_conversion(self, degree: int): def move_control_point(self, tag: int, cp_index: int, new_pos: list[float]): if self.model is not None: self.model.update_control_point(tag, cp_index, new_pos) - self.set_hud_text(f"Point de contrôle {cp_index} de la courbe {tag} déplacé.") + self.set_hud_text( + f"Point de contrôle {cp_index} de la courbe {tag} déplacé." + ) else: self.set_hud_text("Aucun modèle chargé.") diff --git a/ferrispline b/ferrispline new file mode 160000 index 0000000..d414f24 --- /dev/null +++ b/ferrispline @@ -0,0 +1 @@ +Subproject commit d414f24f11d5b001e36813eb176a3e6a84d6dc7c diff --git a/pyproject.toml b/pyproject.toml index 1ef49c3..10306ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,10 +15,8 @@ dependencies = [ [tool.uv] package = true -# FIXME: will be change, using a path like this isn't good -# NOTE For find nurbslib in the folder wheel, you can run "uv sync --find-links wheel" -# [tool.uv.sources] -# nurbslib = { path = "../vtk_converter/nurbslib", editable = true } +[tool.uv.sources] +nurbslib = { path = "ferrispline/nurbslib" } [build-system] requires = ["hatchling"] @@ -26,9 +24,15 @@ build-backend = "hatchling.build" [dependency-groups] dev = [ + "matplotlib>=3.10.9", + "numpy>=2.4.4", "pdoc>=16.0.0", + "pyqt5>=5.15.11", "pytest>=9.0.2", "pytest-cov>=7.0.0", + "pyvista>=0.47.3", + "ruff", + "scipy>=1.17.1", ] [tool.pytest.ini_options] diff --git a/tests/system/test_cad_workflow.py b/tests/system/test_cad_workflow.py index d4c22c6..3a6e0ed 100644 --- a/tests/system/test_cad_workflow.py +++ b/tests/system/test_cad_workflow.py @@ -4,6 +4,7 @@ These tests exercise the Model end-to-end: file loading, topology queries, point addition and render-data consistency. No Panda3D required. """ + import unittest from bot.core.cad import Model as CADModel @@ -59,42 +60,39 @@ def tearDown(self): def test_render_data_has_all_keys(self): data = self.cad.get_render_data() - self.assertIn('points', data) - self.assertIn('edges', data) - self.assertIn('bounds', data) + self.assertIn("points", data) + self.assertIn("edges", data) + self.assertIn("bounds", data) def test_edges_reference_valid_point_indices(self): data = self.cad.get_render_data() - n = len(data['points']) - for idx_a, idx_b, _curve_tag in data['edges']: + n = len(data["points"]) + for idx_a, idx_b, _curve_tag in data["edges"]: self.assertGreaterEqual(idx_a, 0) self.assertLess(idx_a, n) self.assertGreaterEqual(idx_b, 0) self.assertLess(idx_b, n) def test_bounds_center_is_inside_min_max(self): - bounds = self.cad.get_render_data()['bounds'] + bounds = self.cad.get_render_data()["bounds"] for axis in range(3): - self.assertGreaterEqual( - bounds['center'][axis], bounds['min'][axis] - 1e-9 - ) - self.assertLessEqual( - bounds['center'][axis], bounds['max'][axis] + 1e-9 - ) + self.assertGreaterEqual(bounds["center"][axis], bounds["min"][axis] - 1e-9) + self.assertLessEqual(bounds["center"][axis], bounds["max"][axis] + 1e-9) def test_bounds_size_matches_min_max(self): - bounds = self.cad.get_render_data()['bounds'] + bounds = self.cad.get_render_data()["bounds"] for axis in range(3): - expected = bounds['max'][axis] - bounds['min'][axis] - self.assertAlmostEqual(bounds['size'][axis], expected, places=9) + expected = bounds["max"][axis] - bounds["min"][axis] + self.assertAlmostEqual(bounds["size"][axis], expected, places=9) def test_render_data_is_picklable(self): import pickle + data = self.cad.get_render_data() serialised = pickle.dumps(data) restored = pickle.loads(serialised) - self.assertEqual(data['bounds'], restored['bounds']) - self.assertEqual(len(data['points']), len(restored['points'])) + self.assertEqual(data["bounds"], restored["bounds"]) + self.assertEqual(len(data["points"]), len(restored["points"])) class TestAddPointWorkflow(unittest.TestCase): @@ -104,7 +102,7 @@ def setUp(self): self.cad = CADModel() self.cad.open(GEO_FILE) self.initial_point_count = len(self.cad.get_point_tags()) - self.initial_render_pts = len(self.cad.get_render_data()['points']) + self.initial_render_pts = len(self.cad.get_render_data()["points"]) def tearDown(self): self.cad.finalize() @@ -122,16 +120,16 @@ def test_add_point_returns_valid_tag(self): def test_add_point_updates_render_data(self): self.cad.add_point([100.0, 100.0, 0.0]) - new_count = len(self.cad.get_render_data()['points']) + new_count = len(self.cad.get_render_data()["points"]) # Mesh is regenerated: number of discretisation nodes changes self.assertGreater(new_count, 0) def test_bounds_extend_when_point_is_outside(self): - old_bounds = self.cad.get_render_data()['bounds'] - far_x = old_bounds['max'][0] + 1000.0 + old_bounds = self.cad.get_render_data()["bounds"] + far_x = old_bounds["max"][0] + 1000.0 self.cad.add_point([far_x, 0.0, 0.0]) - new_bounds = self.cad.get_render_data()['bounds'] - self.assertGreaterEqual(new_bounds['max'][0], far_x - 1e-6) + new_bounds = self.cad.get_render_data()["bounds"] + self.assertGreaterEqual(new_bounds["max"][0], far_x - 1e-6) def test_cumulative_additions(self): coords = [[10.0, 0.0, 0.0], [20.0, 0.0, 0.0], [30.0, 0.0, 0.0]] @@ -167,5 +165,5 @@ def test_closest_point_result_length_matches_input(self): self.assertEqual(len(result), len(coords)) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/system/test_model_viewer_pipeline.py b/tests/system/test_model_viewer_pipeline.py index 08d6010..901bacc 100644 --- a/tests/system/test_model_viewer_pipeline.py +++ b/tests/system/test_model_viewer_pipeline.py @@ -8,8 +8,9 @@ endpoint is replaced by a spy so we can assert on what would have been transmitted to the rendering process. """ + import unittest -from unittest.mock import MagicMock, call +from unittest.mock import MagicMock from bot.core.cad import Model as CADModel from bot.viewer.viewer import Viewer @@ -40,22 +41,22 @@ def test_connect_sends_load_command(self): viewer.connect(self.model) spy.send.assert_called_once() cmd, data = spy.send.call_args[0][0] - self.assertEqual(cmd, 'load') + self.assertEqual(cmd, "load") def test_load_payload_contains_points_and_edges(self): viewer, spy = _make_spied_viewer() viewer.connect(self.model) _, data = spy.send.call_args[0][0] - self.assertIn('points', data) - self.assertIn('edges', data) - self.assertGreater(len(data['points']), 0) - self.assertGreater(len(data['edges']), 0) + self.assertIn("points", data) + self.assertIn("edges", data) + self.assertGreater(len(data["points"]), 0) + self.assertGreater(len(data["edges"]), 0) def test_load_payload_contains_bounds(self): viewer, spy = _make_spied_viewer() viewer.connect(self.model) _, data = spy.send.call_args[0][0] - self.assertIn('bounds', data) + self.assertIn("bounds", data) class TestViewerReceivesUpdate(unittest.TestCase): @@ -66,7 +67,7 @@ def setUp(self): self.model.open(GEO_FILE) self.viewer, self.spy = _make_spied_viewer() self.viewer.connect(self.model) - self.spy.reset_mock() # ignore the initial 'load' call + self.spy.reset_mock() # ignore the initial 'load' call def tearDown(self): self.model.finalize() @@ -75,14 +76,14 @@ def test_add_point_triggers_update(self): self.model.add_point([50.0, 50.0, 0.0]) self.spy.send.assert_called_once() cmd, _ = self.spy.send.call_args[0][0] - self.assertEqual(cmd, 'update') + self.assertEqual(cmd, "update") def test_update_payload_is_consistent_render_data(self): self.model.add_point([50.0, 50.0, 0.0]) _, data = self.spy.send.call_args[0][0] - self.assertIn('points', data) - self.assertIn('edges', data) - self.assertIn('bounds', data) + self.assertIn("points", data) + self.assertIn("edges", data) + self.assertIn("bounds", data) def test_multiple_mutations_each_trigger_update(self): for x in [10.0, 20.0, 30.0]: @@ -90,7 +91,7 @@ def test_multiple_mutations_each_trigger_update(self): self.assertEqual(3, self.spy.send.call_count) for c in self.spy.send.call_args_list: cmd, _ = c[0][0] - self.assertEqual(cmd, 'update') + self.assertEqual(cmd, "update") class TestMultipleViewers(unittest.TestCase): @@ -168,5 +169,5 @@ def test_after_stop_model_mutation_does_not_reach_viewer(self): spy.send.assert_not_called() -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/system/test_viewer_subprocess.py b/tests/system/test_viewer_subprocess.py index 28ebc7b..77f444e 100644 --- a/tests/system/test_viewer_subprocess.py +++ b/tests/system/test_viewer_subprocess.py @@ -8,6 +8,7 @@ Note: these tests will briefly open a Panda3D window. """ + import sys import time import unittest @@ -20,6 +21,7 @@ def _has_display() -> bool: if sys.platform == "darwin": return True # macOS always has a display import os + return bool(os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY")) @@ -29,6 +31,7 @@ class TestViewerSubprocessLifecycle(unittest.TestCase): def _start_viewer(self, model=None): from bot.viewer.viewer import Viewer + v = Viewer() if model: v.connect(model) @@ -60,6 +63,7 @@ def test_stop_closes_the_connection(self): def test_stop_deregisters_observer(self): from bot.core.cad import Model as CADModel + model = CADModel() model.open("data/profil_1.geo") try: @@ -72,6 +76,7 @@ def test_stop_deregisters_observer(self): def test_run_returns_self_for_chaining(self): from bot.viewer.viewer import Viewer + v = Viewer() result = v.run() try: @@ -90,6 +95,7 @@ class TestViewerIPCWithModel(unittest.TestCase): def setUp(self): from bot.core.cad import Model as CADModel from bot.viewer.viewer import Viewer + self.model = CADModel() self.model.open("data/profil_1.geo") self.viewer = Viewer() @@ -116,5 +122,5 @@ def test_multiple_mutations_keep_subprocess_alive(self): self.assertTrue(self.viewer._process.is_alive()) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/unit/test_cad/test_model.py b/tests/unit/test_cad/test_model.py index 55e7a98..c0ae050 100644 --- a/tests/unit/test_cad/test_model.py +++ b/tests/unit/test_cad/test_model.py @@ -5,12 +5,14 @@ observer pattern, get_render_data structure, add_point side-effects and input-validation errors on getClosestPoint / get_adjacent_curves_of_point. """ + import unittest from bot.core.cad import Model as CADModel class _MockObserver: """Minimal observer that records every update() call.""" + def __init__(self): self.calls = [] @@ -19,7 +21,6 @@ def update(self, model): class TestObserverPattern(unittest.TestCase): - def setUp(self): self.model = CADModel() @@ -64,7 +65,6 @@ def test_multiple_observers_notified_independently(self): class TestGetRenderData(unittest.TestCase): - def setUp(self): self.model = CADModel() @@ -73,30 +73,29 @@ def tearDown(self): def test_returns_dict_with_required_keys(self): data = self.model.get_render_data() - self.assertIn('points', data) - self.assertIn('edges', data) - self.assertIn('bounds', data) + self.assertIn("points", data) + self.assertIn("edges", data) + self.assertIn("bounds", data) def test_points_and_edges_are_lists(self): data = self.model.get_render_data() - self.assertIsInstance(data['points'], list) - self.assertIsInstance(data['edges'], list) + self.assertIsInstance(data["points"], list) + self.assertIsInstance(data["edges"], list) def test_bounds_has_expected_keys(self): self.model.open("data/profil_1.geo") - bounds = self.model.get_render_data()['bounds'] - for key in ('min', 'max', 'center', 'size'): + bounds = self.model.get_render_data()["bounds"] + for key in ("min", "max", "center", "size"): self.assertIn(key, bounds) def test_bounds_are_lists_of_three_floats(self): self.model.open("data/profil_1.geo") - bounds = self.model.get_render_data()['bounds'] - for key in ('min', 'max', 'center', 'size'): + bounds = self.model.get_render_data()["bounds"] + for key in ("min", "max", "center", "size"): self.assertEqual(len(bounds[key]), 3) class TestAddPoint(unittest.TestCase): - def setUp(self): self.model = CADModel() @@ -122,12 +121,11 @@ def test_add_point_notifies_observers(self): def test_add_point_updates_bounds(self): self.model.add_point([10.0, 20.0, 30.0]) bounds = self.model.bounds - self.assertIn('min', bounds) - self.assertIn('max', bounds) + self.assertIn("min", bounds) + self.assertIn("max", bounds) class TestGetClosestPointValidation(unittest.TestCase): - def setUp(self): self.model = CADModel() self.model.open("data/profil_1.geo") @@ -160,5 +158,5 @@ def test_raises_value_error_for_dim_3(self): self.model.getClosestPoint(3, 1, [0, 0, 0]) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/unit/test_control/test_mouse_axis_constraint.py b/tests/unit/test_control/test_mouse_axis_constraint.py index 1c071b4..218c19c 100644 --- a/tests/unit/test_control/test_mouse_axis_constraint.py +++ b/tests/unit/test_control/test_mouse_axis_constraint.py @@ -5,6 +5,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[3])) try: from bot.control.mouse import MouseHandler + _HAS_PANDA_DEPS = True except ModuleNotFoundError: MouseHandler = None @@ -28,13 +29,17 @@ def test_mask_four_keeps_only_z(self): handler = self._make_handler(4) start = [1.0, 2.0, 3.0] candidate = [9.0, 8.0, 7.0] - self.assertEqual(handler._apply_axis_constraint(start, candidate), [1.0, 2.0, 7.0]) + self.assertEqual( + handler._apply_axis_constraint(start, candidate), [1.0, 2.0, 7.0] + ) def test_mask_three_keeps_xy(self): handler = self._make_handler(3) start = [1.0, 2.0, 3.0] candidate = [9.0, 8.0, 7.0] - self.assertEqual(handler._apply_axis_constraint(start, candidate), [9.0, 8.0, 3.0]) + self.assertEqual( + handler._apply_axis_constraint(start, candidate), [9.0, 8.0, 3.0] + ) def test_mask_seven_keeps_all_axes(self): handler = self._make_handler(7) diff --git a/tests/unit/test_control/test_mouse_drag_session.py b/tests/unit/test_control/test_mouse_drag_session.py index 6e0e405..430dd80 100644 --- a/tests/unit/test_control/test_mouse_drag_session.py +++ b/tests/unit/test_control/test_mouse_drag_session.py @@ -5,6 +5,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[3])) try: from bot.control.mouse import MouseHandler + _HAS_PANDA_DEPS = True except ModuleNotFoundError: MouseHandler = None diff --git a/tests/unit/test_curve/test_bezier-curve.py b/tests/unit/test_curve/test_bezier-curve.py index 527c26d..d586677 100644 --- a/tests/unit/test_curve/test_bezier-curve.py +++ b/tests/unit/test_curve/test_bezier-curve.py @@ -4,6 +4,7 @@ The external Rust dependency (nurbslib) is mocked to allow testing the Python bridge and logic without requiring the compiled engine. """ + import unittest from unittest.mock import MagicMock, patch @@ -11,8 +12,7 @@ class TestBezierCurve(unittest.TestCase): - - @patch('bot.core.curve.nurbslib') + @patch("bot.core.curve.nurbslib") def test_initialization_and_attributes(self, mock_nurbslib): """Tests the initialization of the curve and access to its basic attributes.""" # 1. Data preparation @@ -35,7 +35,9 @@ def test_initialization_and_attributes(self, mock_nurbslib): self.assertEqual(curve.get_degree(), degree) # Ensure the Rust engine was called with the correct arguments - mock_nurbslib.PyBezierCurve.assert_called_once_with(degree, control_points, None) + mock_nurbslib.PyBezierCurve.assert_called_once_with( + degree, control_points, None + ) def test_default_control_points_default_degree(self): """Tests that the method uses default degree 3 if not specified.""" @@ -50,7 +52,7 @@ def test_default_control_points_default_degree(self): [0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [20.0, 0.0, 0.0], - [30.0, 0.0, 0.0] + [30.0, 0.0, 0.0], ] self.assertEqual(len(pts), 4) self.assertEqual(pts, expected_pts) @@ -65,11 +67,7 @@ def test_default_control_points_distribution(self): pts = BezierCurve._default_control_points(coords_a, coords_b, degree) # We expect 3 points: point A, middle point, and point B - expected_pts = [ - [0.0, 0.0, 0.0], - [5.0, 0.0, 0.0], - [10.0, 0.0, 0.0] - ] + expected_pts = [[0.0, 0.0, 0.0], [5.0, 0.0, 0.0], [10.0, 0.0, 0.0]] self.assertEqual(pts, expected_pts) def test_default_control_points_count(self): @@ -93,7 +91,7 @@ def test_default_control_points_degree_zero(self): self.assertEqual(len(pts), 1) self.assertEqual(pts[0], coords_a) - @patch('bot.core.curve.nurbslib') + @patch("bot.core.curve.nurbslib") def test_get_render_data(self, mock_nurbslib): """Tests the structure and content of the render dictionary (used by the viewer).""" tag = "42" @@ -115,20 +113,20 @@ def test_get_render_data(self, mock_nurbslib): data = curve.get_render_data() # Verification of the presence of all required keys - self.assertIn('tag', data) - self.assertIn('control_points', data) - self.assertIn('degree', data) - self.assertIn('curve', data) + self.assertIn("tag", data) + self.assertIn("control_points", data) + self.assertIn("degree", data) + self.assertIn("curve", data) # Verifications of values - self.assertEqual(data['tag'], "42") - self.assertEqual(data['control_points'], control_points) - self.assertEqual(data['degree'], degree) - self.assertEqual(data['curve'], mock_curve_eval) + self.assertEqual(data["tag"], "42") + self.assertEqual(data["control_points"], control_points) + self.assertEqual(data["degree"], degree) + self.assertEqual(data["curve"], mock_curve_eval) # Ensure the engine was called to generate 100 points mock_engine_instance.evaluate.assert_called_once_with(100, False) -if __name__ == '__main__': - unittest.main() \ No newline at end of file +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/test_geom/test_geom.py b/tests/unit/test_geom/test_geom.py index e7f8845..35cfc49 100644 --- a/tests/unit/test_geom/test_geom.py +++ b/tests/unit/test_geom/test_geom.py @@ -1,8 +1,8 @@ import unittest from bot.core.cad import Model as CADModel -class GeomTest(unittest.TestCase): +class GeomTest(unittest.TestCase): def test_topology_query_2D(self): cad = CADModel() cad.open("data/profil_1.geo") @@ -48,14 +48,15 @@ def test_closest_points(self): coords = [2, 0, 0, 4, 2, 0] # We get the closest points on the 2nd curve which is a straight line # that connects (0,1,0) to (5,1,0) - outputs = cad.getClosestPoint(1,2,coords) + outputs = cad.getClosestPoint(1, 2, coords) # We know what should be the closest points oracle = [2, 1, 0, 4, 1, 0] - #and we compare + # and we compare epsilon = 1e-9 - for v1, v2 in zip(oracle,outputs): + for v1, v2 in zip(oracle, outputs): self.assertAlmostEqual(v1, v2, delta=epsilon) -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() diff --git a/tests/unit/test_utils/test_color_generator.py b/tests/unit/test_utils/test_color_generator.py index aae7190..5b918c5 100644 --- a/tests/unit/test_utils/test_color_generator.py +++ b/tests/unit/test_utils/test_color_generator.py @@ -3,7 +3,6 @@ class TestColorGenerator(unittest.TestCase): - def test_returns_correct_number_of_colors(self): for n in [1, 3, 10]: colors = ColorGenerator.generate_distinct_colors(n) @@ -35,5 +34,5 @@ def test_empty(self): self.assertEqual(colors, []) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/unit/test_view/test_scene_axis_guide.py b/tests/unit/test_view/test_scene_axis_guide.py index f60f520..f027724 100644 --- a/tests/unit/test_view/test_scene_axis_guide.py +++ b/tests/unit/test_view/test_scene_axis_guide.py @@ -5,6 +5,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[3])) try: from bot.view.scene import Scene + _HAS_PANDA_DEPS = True except ModuleNotFoundError: Scene = None diff --git a/tests/unit/test_viewer/test_viewer.py b/tests/unit/test_viewer/test_viewer.py index d1a4892..f4f5b7d 100644 --- a/tests/unit/test_viewer/test_viewer.py +++ b/tests/unit/test_viewer/test_viewer.py @@ -4,12 +4,14 @@ The subprocess and multiprocessing pipe are fully mocked so these tests run without Panda3D or a display. """ + import unittest -from unittest.mock import MagicMock, patch, PropertyMock +from unittest.mock import MagicMock, patch class _FakeModel: """Minimal Model stand-in for observer-registration tests.""" + def __init__(self): self._observers = [] @@ -20,13 +22,13 @@ def remove_observer(self, obs): self._observers.remove(obs) def get_render_data(self): - return {'points': [], 'edges': [], 'bounds': {}} + return {"points": [], "edges": [], "bounds": {}} class TestViewerConnect(unittest.TestCase): - def _make_viewer(self): from bot.viewer.viewer import Viewer + return Viewer() def test_connect_registers_viewer_as_observer(self): @@ -59,9 +61,9 @@ def test_connect_replaces_previous_model(self): class TestViewerDisconnect(unittest.TestCase): - def _make_viewer(self): from bot.viewer.viewer import Viewer + return Viewer() def test_disconnect_removes_observer(self): @@ -92,9 +94,9 @@ def test_disconnect_without_model_is_safe(self): class TestViewerUpdate(unittest.TestCase): - def _make_viewer(self): from bot.viewer.viewer import Viewer + return Viewer() def test_update_sends_render_data_over_pipe(self): @@ -106,9 +108,7 @@ def test_update_sends_render_data_over_pipe(self): viewer.update(model) - mock_conn.send.assert_called_once_with( - ('update', model.get_render_data()) - ) + mock_conn.send.assert_called_once_with(("update", model.get_render_data())) def test_update_ignores_broken_pipe(self): viewer = self._make_viewer() @@ -129,17 +129,17 @@ def test_update_does_nothing_without_connection(self): class TestViewerSend(unittest.TestCase): - def _make_viewer(self): from bot.viewer.viewer import Viewer + return Viewer() def test_send_transmits_command(self): viewer = self._make_viewer() mock_conn = MagicMock() viewer._conn = mock_conn - viewer._send('load', {'points': []}) - mock_conn.send.assert_called_once_with(('load', {'points': []})) + viewer._send("load", {"points": []}) + mock_conn.send.assert_called_once_with(("load", {"points": []})) def test_send_silences_broken_pipe(self): viewer = self._make_viewer() @@ -147,12 +147,13 @@ def test_send_silences_broken_pipe(self): mock_conn.send.side_effect = BrokenPipeError viewer._conn = mock_conn # Should not raise - viewer._send('load', {}) + viewer._send("load", {}) def test_send_noop_when_no_connection(self): viewer = self._make_viewer() # No exception expected - viewer._send('load', {}) + viewer._send("load", {}) + class TestViewerDefaultOnHover(unittest.TestCase): """Tests pour le comportement par défaut au survol (HUD et surbrillance).""" @@ -160,6 +161,7 @@ class TestViewerDefaultOnHover(unittest.TestCase): def _make_viewer_with_mocks(self): """Crée un Viewer avec tous les mocks nécessaires pour tester l'interface.""" from bot.viewer.viewer import Viewer + viewer = Viewer() viewer._conn = MagicMock() @@ -179,7 +181,9 @@ def test_default_on_hover_empty_space(self): viewer._default_on_hover(None) viewer.highlight_curve.assert_called_once_with("2", [1, 1, 1, 1]) - viewer.set_hud_text.assert_called_once_with("Prêt. Survolez ou cliquez sur les courbes.") + viewer.set_hud_text.assert_called_once_with( + "Prêt. Survolez ou cliquez sur les courbes." + ) self.assertIsNone(viewer._default_last_hovered) def test_default_on_hover_invalid_tag(self): @@ -193,7 +197,10 @@ def test_default_on_hover_invalid_tag(self): def test_default_on_hover_valid_tag(self): viewer = self._make_viewer_with_mocks() - viewer.model.get_end_points_coords.return_value = [[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]] + viewer.model.get_end_points_coords.return_value = [ + [0.0, 0.0, 0.0], + [1.0, 1.0, 1.0], + ] viewer._default_on_hover("1") @@ -212,7 +219,10 @@ def test_default_on_hover_change_curve(self): viewer = self._make_viewer_with_mocks() viewer._default_last_hovered = "1" - viewer.model.get_end_points_coords.return_value = [[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]] + viewer.model.get_end_points_coords.return_value = [ + [0.0, 0.0, 0.0], + [1.0, 1.0, 1.0], + ] viewer._default_on_hover("3") @@ -220,12 +230,14 @@ def test_default_on_hover_change_curve(self): viewer.highlight_curve.assert_any_call("3", [1, 0.5, 0, 1]) self.assertEqual(viewer._default_last_hovered, "3") + class TestViewerBezierInteractions(unittest.TestCase): """Tests for Bezier curve modification interactions via the Viewer.""" def _make_viewer_with_mocks(self): """Creates a Viewer with all necessary mocks.""" from bot.viewer.viewer import Viewer + viewer = Viewer() # Mock connection and HUD text display @@ -237,8 +249,7 @@ def _make_viewer_with_mocks(self): return viewer - - @patch('bot.viewer.viewer.BezierCurve') + @patch("bot.viewer.viewer.BezierCurve") def test_bezier_conversion_success(self, MockBezierCurve): """Verifies the conversion of a classic curve into a Bezier curve.""" viewer = self._make_viewer_with_mocks() @@ -251,7 +262,12 @@ def test_bezier_conversion_success(self, MockBezierCurve): viewer.model.get_end_points_coords.return_value = [coords_a, coords_b] # Mock configuration for the static method _default_control_points - MockBezierCurve._default_control_points.return_value = [coords_a, [3.3, 0, 0], [6.6, 0, 0], coords_b] + MockBezierCurve._default_control_points.return_value = [ + coords_a, + [3.3, 0, 0], + [6.6, 0, 0], + coords_b, + ] # Configuration of the mocked instance returned by BezierCurve(...) mock_curve_instance = MagicMock() @@ -262,9 +278,13 @@ def test_bezier_conversion_success(self, MockBezierCurve): # Assertions viewer.model.get_end_points_coords.assert_called_once_with(42) - MockBezierCurve._default_control_points.assert_called_once_with(coords_a, coords_b, degree) - MockBezierCurve.assert_called_once_with("42", MockBezierCurve._default_control_points.return_value, degree) - viewer.model.set_curve.assert_called_once_with("42", mock_curve_instance) + MockBezierCurve._default_control_points.assert_called_once_with( + coords_a, coords_b, degree + ) + MockBezierCurve.assert_called_once_with( + 42, MockBezierCurve._default_control_points.return_value, degree + ) + viewer.model.set_curve.assert_called_once_with(42, mock_curve_instance) def test_bezier_conversion_no_selection(self): """Verifies behavior if no curve is selected/hovered.""" @@ -274,7 +294,9 @@ def test_bezier_conversion_no_selection(self): viewer.bezier_conversion(3) # Verification of the HUD error - viewer.set_hud_text.assert_called_once_with("Impossible to convert: no curve selected") + viewer.set_hud_text.assert_called_once_with( + "Impossible to convert: no curve selected" + ) # Verifies the model was not called viewer.model.get_end_points_coords.assert_not_called() @@ -287,8 +309,9 @@ def test_bezier_conversion_no_model(self): viewer.bezier_conversion(3) # Verification of the HUD error - viewer.set_hud_text.assert_called_once_with("Impossible to convert: no model loaded") - + viewer.set_hud_text.assert_called_once_with( + "Impossible to convert: no model loaded" + ) def test_move_control_point_success(self): """Verifies that the modification of a control point is correctly transmitted to the model.""" @@ -301,9 +324,13 @@ def test_move_control_point_success(self): viewer.move_control_point(tag, cp_index, new_pos) # Verifies the model is updated - viewer.model.update_control_point.assert_called_once_with(tag, cp_index, new_pos) + viewer.model.update_control_point.assert_called_once_with( + tag, cp_index, new_pos + ) # Verifies the success message in the HUD - viewer.set_hud_text.assert_called_once_with(f"Point de contrôle {cp_index} de la courbe {tag} déplacé.") + viewer.set_hud_text.assert_called_once_with( + f"Point de contrôle {cp_index} de la courbe {tag} déplacé." + ) def test_move_control_point_no_model(self): """Verifies behavior if attempting to move a point without a connected model.""" @@ -317,9 +344,9 @@ def test_move_control_point_no_model(self): class TestViewerCurveEditMode(unittest.TestCase): - def _make_viewer(self): from bot.viewer.viewer import Viewer + viewer = Viewer() viewer._conn = MagicMock() return viewer @@ -327,22 +354,24 @@ def _make_viewer(self): def test_set_edit_mode_sends_command(self): viewer = self._make_viewer() viewer.set_edit_mode(True, 7) - viewer._conn.send.assert_called_with(('set_edit_mode', {'enabled': True, 'curve_tag': 7})) + viewer._conn.send.assert_called_with( + ("set_edit_mode", {"enabled": True, "curve_tag": 7}) + ) def test_set_active_curve_sends_command(self): viewer = self._make_viewer() viewer.set_active_curve(9) - viewer._conn.send.assert_called_with(('set_active_curve', {'curve_tag': 9})) + viewer._conn.send.assert_called_with(("set_active_curve", {"curve_tag": 9})) def test_set_axis_constraint_sends_command(self): viewer = self._make_viewer() viewer.set_axis_constraint(6) - viewer._conn.send.assert_called_with(('set_axis_constraint', {'mask': 6})) + viewer._conn.send.assert_called_with(("set_axis_constraint", {"mask": 6})) def test_set_axis_constraint_clamps_invalid_value(self): viewer = self._make_viewer() viewer.set_axis_constraint("oops") - viewer._conn.send.assert_called_with(('set_axis_constraint', {'mask': 7})) + viewer._conn.send.assert_called_with(("set_axis_constraint", {"mask": 7})) def test_default_on_curve_selected_enables_edit_mode(self): viewer = self._make_viewer() @@ -360,9 +389,12 @@ def test_default_on_cp_pick_end_commits_to_model(self): viewer = self._make_viewer() viewer.model = MagicMock() - viewer._default_on_cp_pick_end({'tag': '4', 'cp_index': 2, 'world_pos': [1.0, 2.0, 3.0]}) + viewer._default_on_cp_pick_end( + {"tag": "4", "cp_index": 2, "world_pos": [1.0, 2.0, 3.0]} + ) viewer.model.update_control_point.assert_called_once_with(4, 2, [1.0, 2.0, 3.0]) -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() diff --git a/uv.lock b/uv.lock index 9974f65..f45c4d0 100644 --- a/uv.lock +++ b/uv.lock @@ -11,6 +11,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, ] +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + [[package]] name = "bot" version = "0.1.0" @@ -24,24 +33,86 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "matplotlib" }, + { name = "numpy" }, { name = "pdoc" }, + { name = "pyqt5" }, { name = "pytest" }, { name = "pytest-cov" }, + { name = "pyvista" }, + { name = "ruff" }, + { name = "scipy" }, ] [package.metadata] requires-dist = [ { name = "gmsh", specifier = ">=4.15.0" }, { name = "ipython", specifier = ">=9.10.0" }, - { name = "nurbslib" }, + { name = "nurbslib", directory = "ferrispline/nurbslib" }, { name = "panda3d", specifier = ">=1.10.16" }, ] [package.metadata.requires-dev] dev = [ + { name = "matplotlib", specifier = ">=3.10.9" }, + { name = "numpy", specifier = ">=2.4.4" }, { name = "pdoc", specifier = ">=16.0.0" }, + { name = "pyqt5", specifier = ">=5.15.11" }, { name = "pytest", specifier = ">=9.0.2" }, { name = "pytest-cov", specifier = ">=7.0.0" }, + { name = "pyvista", specifier = ">=0.47.3" }, + { name = "ruff" }, + { name = "scipy", specifier = ">=1.17.1" }, +] + +[[package]] +name = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, ] [[package]] @@ -53,6 +124,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, +] + [[package]] name = "coverage" version = "7.13.4" @@ -92,6 +196,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, ] +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "cyclopts" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "docstring-parser" }, + { name = "rich" }, + { name = "rich-rst" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/fa/eff8f1abae783bade9b5e9bafafd0040d4dbf51988f9384bfdc0326ba1fc/cyclopts-4.11.0.tar.gz", hash = "sha256:1ffcb9990dbd56b90da19980d31596de9e99019980a215a5d76cf88fe452e94d", size = 170690, upload-time = "2026-04-23T00:23:36.858Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/37/197db187c260d24d4be1f09d427f59f3fb9a89bcf1354e23865c7bff7607/cyclopts-4.11.0-py3-none-any.whl", hash = "sha256:34318e3823b44b5baa754a5e37ec70a5c17dc81c65e4295ed70e17bc1aeae50d", size = 208494, upload-time = "2026-04-23T00:23:34.948Z" }, +] + [[package]] name = "decorator" version = "5.2.1" @@ -101,6 +229,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, ] +[[package]] +name = "docstring-parser" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, +] + +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + [[package]] name = "executing" version = "2.2.1" @@ -110,6 +256,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] +[[package]] +name = "fonttools" +version = "4.62.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/08/7012b00a9a5874311b639c3920270c36ee0c445b69d9989a85e5c92ebcb0/fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d", size = 3580737, upload-time = "2026-03-13T13:54:25.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca", size = 2864442, upload-time = "2026-03-13T13:53:37.509Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b2/e521803081f8dc35990816b82da6360fa668a21b44da4b53fc9e77efcd62/fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca", size = 2410901, upload-time = "2026-03-13T13:53:40.55Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/8c3511ff06e53110039358dbbdc1a65d72157a054638387aa2ada300a8b8/fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782", size = 4999608, upload-time = "2026-03-13T13:53:42.798Z" }, + { url = "https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae", size = 4912726, upload-time = "2026-03-13T13:53:45.405Z" }, + { url = "https://files.pythonhosted.org/packages/70/b9/ac677cb07c24c685cf34f64e140617d58789d67a3dd524164b63648c6114/fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7", size = 4951422, upload-time = "2026-03-13T13:53:48.326Z" }, + { url = "https://files.pythonhosted.org/packages/e6/10/11c08419a14b85b7ca9a9faca321accccc8842dd9e0b1c8a72908de05945/fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a", size = 5060979, upload-time = "2026-03-13T13:53:51.366Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3c/12eea4a4cf054e7ab058ed5ceada43b46809fce2bf319017c4d63ae55bb4/fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800", size = 2283733, upload-time = "2026-03-13T13:53:53.606Z" }, + { url = "https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e", size = 2335663, upload-time = "2026-03-13T13:53:56.23Z" }, + { url = "https://files.pythonhosted.org/packages/42/c5/4d2ed3ca6e33617fc5624467da353337f06e7f637707478903c785bd8e20/fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82", size = 2947288, upload-time = "2026-03-13T13:53:59.397Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e9/7ab11ddfda48ed0f89b13380e5595ba572619c27077be0b2c447a63ff351/fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260", size = 2449023, upload-time = "2026-03-13T13:54:01.642Z" }, + { url = "https://files.pythonhosted.org/packages/b2/10/a800fa090b5e8819942e54e19b55fc7c21fe14a08757c3aa3ca8db358939/fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4", size = 5137599, upload-time = "2026-03-13T13:54:04.495Z" }, + { url = "https://files.pythonhosted.org/packages/37/dc/8ccd45033fffd74deb6912fa1ca524643f584b94c87a16036855b498a1ed/fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b", size = 4920933, upload-time = "2026-03-13T13:54:07.557Z" }, + { url = "https://files.pythonhosted.org/packages/99/eb/e618adefb839598d25ac8136cd577925d6c513dc0d931d93b8af956210f0/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87", size = 5016232, upload-time = "2026-03-13T13:54:10.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5f/9b5c9bfaa8ec82def8d8168c4f13615990d6ce5996fe52bd49bfb5e05134/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c", size = 5042987, upload-time = "2026-03-13T13:54:13.569Z" }, + { url = "https://files.pythonhosted.org/packages/90/aa/dfbbe24c6a6afc5c203d90cc0343e24bcbb09e76d67c4d6eef8c2558d7ba/fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a", size = 2348021, upload-time = "2026-03-13T13:54:16.98Z" }, + { url = "https://files.pythonhosted.org/packages/13/6f/ae9c4e4dd417948407b680855c2c7790efb52add6009aaecff1e3bc50e8e/fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e", size = 2414147, upload-time = "2026-03-13T13:54:19.416Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" }, +] + [[package]] name = "gmsh" version = "4.15.0" @@ -121,6 +292,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/23/51/21175eb9c6a592431a5c87f288ce447af1cea68a6ad4169765dc7ab7a9e7/gmsh-4.15.0-py2.py3-none-win_amd64.whl", hash = "sha256:a5b9867f88af59aff269a93abbd009551fbc263eb94a165a25a0c8072bae13db", size = 42233991, upload-time = "2025-10-26T20:30:32.722Z" }, ] +[[package]] +name = "idna" +version = "3.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -187,6 +367,56 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "kiwisolver" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231, upload-time = "2026-03-09T13:14:41.323Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489, upload-time = "2026-03-09T13:14:42.534Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063, upload-time = "2026-03-09T13:14:44.759Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" }, + { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" }, + { url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086, upload-time = "2026-03-09T13:15:07.775Z" }, + { url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577, upload-time = "2026-03-09T13:15:09.139Z" }, + { url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794, upload-time = "2026-03-09T13:15:10.525Z" }, + { url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646, upload-time = "2026-03-09T13:15:12.016Z" }, + { url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511, upload-time = "2026-03-09T13:15:13.311Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" }, + { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" }, + { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687, upload-time = "2026-03-09T13:15:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032, upload-time = "2026-03-09T13:15:34.411Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + [[package]] name = "markdown2" version = "2.5.4" @@ -226,6 +456,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] +[[package]] +name = "matplotlib" +version = "3.10.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233, upload-time = "2026-04-24T00:14:13.539Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/e6/3bd8afd04949f02eabc1c17115ea5255e19cacd4d06fc5abdde4eeb0052c/matplotlib-3.10.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:172db52c9e683f5d12eaf57f0f54834190e12581fe1cc2a19595a8f5acb4e77d", size = 8321276, upload-time = "2026-04-24T00:13:18.318Z" }, + { url = "https://files.pythonhosted.org/packages/41/86/86231232fff41c9f8e4a1a7d7a597d349a02527109c3af7d618366122139/matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f", size = 8218218, upload-time = "2026-04-24T00:13:20.974Z" }, + { url = "https://files.pythonhosted.org/packages/85/8f/becc9722cafc64f5d2eb0b7c1bf5f585271c618a45dbd8fabeb021f898b6/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aba1615dabe83188e19d4f75a253c6a08423e04c1425e64039f800050a69de6b", size = 9608145, upload-time = "2026-04-24T00:13:23.228Z" }, + { url = "https://files.pythonhosted.org/packages/32/5d/f7e914f7d9325abff4057cee62c0fa70263683189f774473cbfb534cd13b/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2", size = 9885085, upload-time = "2026-04-24T00:13:25.849Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fd/fa69f2221534e80cc5772ac2b7d222011a2acafc2ec7216d5dd174c864ae/matplotlib-3.10.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59476c6d29d612b8e9bb6ce8c5b631be6ba8f9e3a2421f22a02b192c7dd28716", size = 9672358, upload-time = "2026-04-24T00:13:28.906Z" }, + { url = "https://files.pythonhosted.org/packages/ab/1a/5a4f747a8b271cbb024946d2dd3c913ab5032ba430626f8c3528ada96b4b/matplotlib-3.10.9-cp314-cp314-win_amd64.whl", hash = "sha256:336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f", size = 8349970, upload-time = "2026-04-24T00:13:31.904Z" }, + { url = "https://files.pythonhosted.org/packages/64/dc/95d60ecaefe30680a154b52ea96ab4b0dab547f1fd6aa12f5fb655e89cae/matplotlib-3.10.9-cp314-cp314-win_arm64.whl", hash = "sha256:2dc9477819ffd78ad12a20df1d9d6a6bd4fec6aaa9072681465fddca052f1456", size = 8272785, upload-time = "2026-04-24T00:13:34.511Z" }, + { url = "https://files.pythonhosted.org/packages/70/a0/005d68bc8b8418300ce6591f18586910a8526806e2ab663933d9f20a41e9/matplotlib-3.10.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:da4e09638420548f31c354032a6250e473c68e5a4e96899b4844cf39ddea23fe", size = 8367999, upload-time = "2026-04-24T00:13:36.962Z" }, + { url = "https://files.pythonhosted.org/packages/22/05/1236cc9290be70b2498af20ca348add76e3fffe7f67b477db5133a84f3ea/matplotlib-3.10.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:345f6f68ecc8da0ca56fad2ea08fde1a115eda530079eca185d50a7bc3e146c6", size = 8264543, upload-time = "2026-04-24T00:13:39.851Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c2/071f5a5ff6c5bd63aaaf2f45c811d9bf2ced94bde188d9e1a519e21d0cba/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4edcfbd8565339aa62f1cd4012f7180926fdbe71850f7b0d3c379c175cd6b66c", size = 9622800, upload-time = "2026-04-24T00:13:42.296Z" }, + { url = "https://files.pythonhosted.org/packages/95/57/da7d1f10a85624b9e7db68e069dd94e58dc41dbf9463c5921632ecbe3661/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be157fe17fc37cb95ac1d7374cf717ce9259616edec911a78d9d26dae8522d4", size = 9888561, upload-time = "2026-04-24T00:13:45.026Z" }, + { url = "https://files.pythonhosted.org/packages/67/b2/ef8d6bb59b0edb6c16c968b70f548aa13b54348972def5aa6ac85df67145/matplotlib-3.10.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4e42042d54db34fda4e95a7bd3e5789c2a995d2dad3eb8850232ee534092fbbf", size = 9680884, upload-time = "2026-04-24T00:13:48.066Z" }, + { url = "https://files.pythonhosted.org/packages/61/1c/d21bfeb9931881ebe96bcfcff27c7ae4b160ae0ec291a714c42641a56d75/matplotlib-3.10.9-cp314-cp314t-win_amd64.whl", hash = "sha256:c27df8b3848f32a83d1767566595e43cfaa4460380974da06f4279a7ec143c39", size = 8432333, upload-time = "2026-04-24T00:13:51.008Z" }, + { url = "https://files.pythonhosted.org/packages/78/23/92493c3e6e1b635ccfff146f7b99e674808787915420373ac399283764c2/matplotlib-3.10.9-cp314-cp314t-win_arm64.whl", hash = "sha256:a49f1eadc84ca85fd72fa4e89e70e61bf86452df6f971af04b12c60761a0772c", size = 8324785, upload-time = "2026-04-24T00:13:53.633Z" }, +] + [[package]] name = "matplotlib-inline" version = "0.2.1" @@ -259,16 +522,53 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c5/2a/afe0193b673a79ffd2e01ad999511b7e9e6b49af02bb3759d82a78c3043d/maturin-1.13.1-py3-none-win_arm64.whl", hash = "sha256:2839024dcd65776abb4759e5bca29941971e095574162a4d335191da4be9ff24", size = 8905575, upload-time = "2026-04-09T15:14:03.891Z" }, ] +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, + { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, + { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, + { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, + { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, +] + [[package]] name = "nurbslib" -version = "0.1.0" -source = { registry = "wheel" } +source = { directory = "ferrispline/nurbslib" } dependencies = [ { name = "maturin" }, ] -wheels = [ - { path = "nurbslib-0.1.0-cp39-abi3-manylinux_2_34_x86_64.whl" }, -] + +[package.metadata] +requires-dist = [{ name = "maturin" }] [[package]] name = "packaging" @@ -332,6 +632,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, ] +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -341,6 +683,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "pooch" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "platformdirs" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/43/85ef45e8b36c6a48546af7b266592dc32d7f67837a6514d111bced6d7d75/pooch-1.9.0.tar.gz", hash = "sha256:de46729579b9857ffd3e741987a2f6d5e0e03219892c167c6578c0091fb511ed", size = 61788, upload-time = "2026-01-30T19:15:09.649Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl", hash = "sha256:f265597baa9f760d25ceb29d0beb8186c243d6607b0f60b83ecf14078dbc703b", size = 67175, upload-time = "2026-01-30T19:15:08.36Z" }, +] + [[package]] name = "prompt-toolkit" version = "3.0.52" @@ -380,6 +736,55 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pyqt5" +version = "5.15.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyqt5-qt5" }, + { name = "pyqt5-sip" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/07/c9ed0bd428df6f87183fca565a79fee19fa7c88c7f00a7f011ab4379e77a/PyQt5-5.15.11.tar.gz", hash = "sha256:fda45743ebb4a27b4b1a51c6d8ef455c4c1b5d610c90d2934c7802b5c1557c52", size = 3216775, upload-time = "2024-07-19T08:39:57.756Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/64/42ec1b0bd72d87f87bde6ceb6869f444d91a2d601f2e67cd05febc0346a1/PyQt5-5.15.11-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:c8b03dd9380bb13c804f0bdb0f4956067f281785b5e12303d529f0462f9afdc2", size = 6579776, upload-time = "2024-07-19T08:39:19.775Z" }, + { url = "https://files.pythonhosted.org/packages/49/f5/3fb696f4683ea45d68b7e77302eff173493ac81e43d63adb60fa760b9f91/PyQt5-5.15.11-cp38-abi3-macosx_11_0_x86_64.whl", hash = "sha256:6cd75628f6e732b1ffcfe709ab833a0716c0445d7aec8046a48d5843352becb6", size = 7016415, upload-time = "2024-07-19T08:39:32.977Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8c/4065950f9d013c4b2e588fe33cf04e564c2322842d84dbcbce5ba1dc28b0/PyQt5-5.15.11-cp38-abi3-manylinux_2_17_x86_64.whl", hash = "sha256:cd672a6738d1ae33ef7d9efa8e6cb0a1525ecf53ec86da80a9e1b6ec38c8d0f1", size = 8188103, upload-time = "2024-07-19T08:39:40.561Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/ae5a5b4f9b826b29ea4be841b2f2d951bcf5ae1d802f3732b145b57c5355/PyQt5-5.15.11-cp38-abi3-win32.whl", hash = "sha256:76be0322ceda5deecd1708a8d628e698089a1cea80d1a49d242a6d579a40babd", size = 5433308, upload-time = "2024-07-19T08:39:46.932Z" }, + { url = "https://files.pythonhosted.org/packages/56/d5/68eb9f3d19ce65df01b6c7b7a577ad3bbc9ab3a5dd3491a4756e71838ec9/PyQt5-5.15.11-cp38-abi3-win_amd64.whl", hash = "sha256:bdde598a3bb95022131a5c9ea62e0a96bd6fb28932cc1619fd7ba211531b7517", size = 6865864, upload-time = "2024-07-19T08:39:53.572Z" }, +] + +[[package]] +name = "pyqt5-qt5" +version = "5.15.18" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/90/bf01ac2132400997a3474051dd680a583381ebf98b2f5d64d4e54138dc42/pyqt5_qt5-5.15.18-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:8bb997eb903afa9da3221a0c9e6eaa00413bbeb4394d5706118ad05375684767", size = 39715743, upload-time = "2025-11-09T12:56:42.936Z" }, + { url = "https://files.pythonhosted.org/packages/24/8e/76366484d9f9dbe28e3bdfc688183433a7b82e314216e9b14c89e5fab690/pyqt5_qt5-5.15.18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c656af9c1e6aaa7f59bf3d8995f2fa09adbf6762b470ed284c31dca80d686a26", size = 36798484, upload-time = "2025-11-09T12:56:59.998Z" }, + { url = "https://files.pythonhosted.org/packages/9a/46/ffe177f99f897a59dc237a20059020427bd2d3853d713992b8081933ddfe/pyqt5_qt5-5.15.18-py3-none-manylinux2014_x86_64.whl", hash = "sha256:bf2457e6371969736b4f660a0c153258fa03dbc6a181348218e6f05421682af7", size = 60864590, upload-time = "2025-11-09T12:57:26.724Z" }, +] + +[[package]] +name = "pyqt5-sip" +version = "12.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/31/5ef342de9faee0f3801088946ae103db9b9eaeba3d6a64fefd5ce74df244/pyqt5_sip-12.18.0.tar.gz", hash = "sha256:71c37db75a0664325de149f43e2a712ec5fa1f90429a21dafbca005cb6767f94", size = 104143, upload-time = "2026-01-13T15:53:19.576Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/3a/b46a0116b1aacbb6156b2957eb5cb928c94b49f4626eb2540ca8d16ee757/pyqt5_sip-12.18.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8372ec8704bfd5e09942d0d055a1657eb4f702f4b30847a5e59df0496f99d67f", size = 124594, upload-time = "2026-01-13T15:53:13.159Z" }, + { url = "https://files.pythonhosted.org/packages/58/63/df3037f11391c25c5b0ab233d22e58b8f056cb1ce16d7ecadb844421ce75/pyqt5_sip-12.18.0-cp314-cp314-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fdb45c7cd2af7eccd7370b994d432bfc7965079f845392760724f26771bb59dc", size = 339056, upload-time = "2026-01-13T15:53:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e7/4f96b84520b8f8b7502682fd43f68f63ca6572b5858f56e5f61c76a54fe2/pyqt5_sip-12.18.0-cp314-cp314-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:92abe984becbde768954d6d0951f56d80a9868d2fd9e738e61fc944f0ff83dd6", size = 282439, upload-time = "2026-01-13T15:53:14.856Z" }, + { url = "https://files.pythonhosted.org/packages/79/8e/ccdf20d373ceba83e1d1b7f818505c375208ffde4a96376dc7dbe592406c/pyqt5_sip-12.18.0-cp314-cp314-win32.whl", hash = "sha256:bd9e3c6f81346f1b08d6db02305cdee20c009b43aa083d44ee2de47a7da0e123", size = 50713, upload-time = "2026-01-13T15:53:18.634Z" }, + { url = "https://files.pythonhosted.org/packages/7f/21/8486ed45977be615ec5371b24b47298b1cb0e1a455b419eddd0215078dba/pyqt5_sip-12.18.0-cp314-cp314-win_amd64.whl", hash = "sha256:6d948f1be619c645cd3bda54952bfdc1aef7c79242dccea6a6858748e61114b9", size = 59622, upload-time = "2026-01-13T15:53:17.714Z" }, +] + [[package]] name = "pytest" version = "9.0.2" @@ -410,6 +815,152 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pyvista" +version = "0.47.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cyclopts" }, + { name = "matplotlib" }, + { name = "numpy" }, + { name = "pillow" }, + { name = "pooch" }, + { name = "scooby" }, + { name = "typing-extensions" }, + { name = "vtk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/36/512c05b1cd431150d47479d1397013915eaf5c6d3f728eada53a601871b8/pyvista-0.47.3.tar.gz", hash = "sha256:03ce3923b42053cf8c9c151ea385431474f11b286d31fe9513cf5b7bf29fe848", size = 2463344, upload-time = "2026-04-10T17:47:07.39Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/33/f2775b3c7ee908bdd96c665cb1d6658a02d123dea33b9ee861e7d56ad2ae/pyvista-0.47.3-py3-none-any.whl", hash = "sha256:8db0dd77c744d2673a1b34333694cb4e8828a9193bbe2c0a8b3ceb9bfc12dd0f", size = 2508590, upload-time = "2026-04-10T17:47:05.532Z" }, +] + +[[package]] +name = "requests" +version = "2.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "rich-rst" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/6d/a506aaa4a9eaa945ed8ab2b7347859f53593864289853c5d6d62b77246e0/rich_rst-1.3.2.tar.gz", hash = "sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4", size = 14936, upload-time = "2025-10-14T16:49:45.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl", hash = "sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a", size = 12567, upload-time = "2025-10-14T16:49:42.953Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/99/43/3291f1cc9106f4c63bdce7a8d0df5047fe8422a75b091c16b5e9355e0b11/ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6", size = 4643852, upload-time = "2026-04-24T18:17:14.305Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/6e/e78ffb61d4686f3d96ba3df2c801161843746dcbcbb17a1e927d4829312b/ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c", size = 10640713, upload-time = "2026-04-24T18:17:22.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/08/a317bc231fb9e7b93e4ef3089501e51922ff88d6936ce5cf870c4fe55419/ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c", size = 11069267, upload-time = "2026-04-24T18:17:30.105Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5", size = 10397182, upload-time = "2026-04-24T18:17:07.177Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/3310fc6d1b5e1fdea22bf3b1b807c7e187b581021b0d7d4514cccdb5fb71/ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002", size = 10758012, upload-time = "2026-04-24T18:16:55.759Z" }, + { url = "https://files.pythonhosted.org/packages/11/c1/a606911aee04c324ddaa883ae418f3569792fd3c4a10c50e0dd0a2311e1e/ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5", size = 10447479, upload-time = "2026-04-24T18:16:51.677Z" }, + { url = "https://files.pythonhosted.org/packages/9d/68/4201e8444f0894f21ab4aeeaee68aa4f10b51613514a20d80bd628d57e88/ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6", size = 11234040, upload-time = "2026-04-24T18:17:16.529Z" }, + { url = "https://files.pythonhosted.org/packages/34/ff/8a6d6cf4ccc23fd67060874e832c18919d1557a0611ebef03fdb01fff11e/ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33", size = 12087377, upload-time = "2026-04-24T18:17:04.944Z" }, + { url = "https://files.pythonhosted.org/packages/85/f6/c669cf73f5152f623d34e69866a46d5e6185816b19fcd5b6dd8a2d299922/ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847", size = 11367784, upload-time = "2026-04-24T18:17:25.409Z" }, + { url = "https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0", size = 11344088, upload-time = "2026-04-24T18:17:12.258Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8d/49afab3645e31e12c590acb6d3b5b69d7aab5b81926dbaf7461f9441f37a/ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339", size = 11271770, upload-time = "2026-04-24T18:17:02.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/06/33f41fe94403e2b755481cdfb9b7ef3e4e0ed031c4581124658d935d52b4/ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5", size = 10719355, upload-time = "2026-04-24T18:17:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/0d/59/18aa4e014debbf559670e4048e39260a85c7fcee84acfd761ac01e7b8d35/ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd", size = 10462758, upload-time = "2026-04-24T18:17:32.347Z" }, + { url = "https://files.pythonhosted.org/packages/25/e7/cc9f16fd0f3b5fddcbd7ec3d6ae30c8f3fde1047f32a4093a98d633c6570/ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b", size = 10953498, upload-time = "2026-04-24T18:17:20.674Z" }, + { url = "https://files.pythonhosted.org/packages/72/7a/a9ba7f98c7a575978698f4230c5e8cc54bbc761af34f560818f933dafa0c/ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e", size = 11447765, upload-time = "2026-04-24T18:17:09.755Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f9/0ae446942c846b8266059ad8a30702a35afae55f5cdc54c5adf8d7afdc27/ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20", size = 10657277, upload-time = "2026-04-24T18:17:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d", size = 11837758, upload-time = "2026-04-24T18:17:00.113Z" }, + { url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + +[[package]] +name = "scooby" +version = "0.11.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/06/9a8600207fd72a29ee965e9a4c61b750cc3fa106768f14a7b3ee3e36cb61/scooby-0.11.2.tar.gz", hash = "sha256:0575c73636ec4c2587bea1f8a038798ddcb249e02067fae897dac3bf4f4e444d", size = 242928, upload-time = "2026-04-22T23:13:12.307Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/bc/1173f502f1870e3bae81c148326c5cbcc19ec77df79a9aaf17a59911355c/scooby-0.11.2-py3-none-any.whl", hash = "sha256:f34c36bbee749b2c55816a080521f216d88304e635017e911c12249607d38c49", size = 20142, upload-time = "2026-04-22T23:13:10.705Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + [[package]] name = "stack-data" version = "0.6.3" @@ -433,6 +984,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, ] +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "vtk" +version = "9.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/de/ad1ccb188681d51b5e5621f383afa0a1dd8711ff8f3dcba4ff950f758cbb/vtk-9.6.1-cp314-cp314-macosx_10_10_x86_64.whl", hash = "sha256:91257894723dfced8be264915d81ba418d08e5bbdb4873da0f12b02c1e21f244", size = 114382745, upload-time = "2026-03-27T13:58:30.592Z" }, + { url = "https://files.pythonhosted.org/packages/59/21/7bb2bd61b4ae05db79e2f40df452a6dbcd3bb66c259fd2043aaf60bbe5b7/vtk-9.6.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e119721774418fba34e95852efaefe5ac4156a4a270362fef06894cdc1377b6e", size = 106826433, upload-time = "2026-03-27T13:59:22.109Z" }, + { url = "https://files.pythonhosted.org/packages/fc/e4/5317afdeaa4a66fe037e1fedcb6bde86b888b8227c89aba6c8ad2946e380/vtk-9.6.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98df48bcd630a4ffa71ac09d6aebb69c628925902920419e3db838dc7f7ce0ed", size = 145936133, upload-time = "2026-03-27T14:01:19.29Z" }, + { url = "https://files.pythonhosted.org/packages/67/7f/f1375aa3ef1835e39b4bea978da06d4985bc1407e3e91384dfaabf5e09d0/vtk-9.6.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:3ea3c3e466a4a9cd8fa7e5b64595215467a7936c9f0fd61ec3275954c122a79c", size = 135710278, upload-time = "2026-03-27T14:00:19.358Z" }, + { url = "https://files.pythonhosted.org/packages/91/25/7ff877a0d4f3e848d994d3a774d5a8e4495681ed26c32eb9dbb2a86b50e5/vtk-9.6.1-cp314-cp314-win_amd64.whl", hash = "sha256:a05e12ab8c82e81b225feee5ca08fda5fff814520a11c2941cc866335d990e03", size = 83197720, upload-time = "2026-03-27T14:01:53.313Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ae/f621aaed0a36c99ba3c1b2f2593386094222132a8396f21c616adffacaaf/vtk-9.6.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:043fb013a2669180180bd0ab667f318f2e1f14da69ae943192a7443f5ce3721a", size = 145557875, upload-time = "2026-03-27T14:03:45.531Z" }, + { url = "https://files.pythonhosted.org/packages/e1/d5/41edd3f8b38ad45b8fb30d12ef35be3d62e1221e455722a5d7d103ca2f0d/vtk-9.6.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d08b3760cbd8bffdbb4551033c4c9d87afe30e9e9d4c0e5cad29cbe7107c9af7", size = 135524312, upload-time = "2026-03-27T14:02:46.035Z" }, +] + [[package]] name = "wcwidth" version = "0.6.0" diff --git a/wheel/nurbslib-0.1.0-cp39-abi3-manylinux_2_34_x86_64.whl b/wheel/nurbslib-0.1.0-cp39-abi3-manylinux_2_34_x86_64.whl deleted file mode 100644 index c3c2ad6..0000000 Binary files a/wheel/nurbslib-0.1.0-cp39-abi3-manylinux_2_34_x86_64.whl and /dev/null differ