Skip to content

Commit d224b78

Browse files
authored
Merge pull request #8 from LIHPC-Computational-Geometry/chore/use-model-class
Chore/use model class
2 parents 90ae751 + 7394a93 commit d224b78

9 files changed

Lines changed: 121 additions & 52 deletions

File tree

bot/core/curve.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import nurbslib
1+
import ferrispline as ferr
22
import numpy as np
33

44

@@ -10,7 +10,7 @@ class BezierCurve:
1010
def __init__(self, tag: str, control_points: list[list[float]], degree: int):
1111
self.tag = tag
1212
cp_array = np.array(control_points, dtype=np.float64)
13-
self._engine = nurbslib.PyBezierCurve(degree, cp_array, None)
13+
self._engine = ferr.PyBezierCurve(degree, cp_array, None)
1414

1515
@staticmethod
1616
def _default_control_points(coords_a, coords_b, degree=3):
@@ -51,7 +51,7 @@ def get_degree(self):
5151
def set_control_points(self, control_points: list[list[float]]):
5252
"""Replace the internal curve engine with updated control points."""
5353
cp_array = np.array(control_points, dtype=np.float64)
54-
self._engine = nurbslib.PyBezierCurve(self.get_degree(), cp_array, None)
54+
self._engine = ferr.PyBezierCurve(self.get_degree(), cp_array, None)
5555

5656
def get_render_data(self) -> dict:
5757
curve_pts = self._engine.evaluate(100, False)

bot/core/spline.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from __future__ import annotations
2+
3+
import ferrispline
4+
import numpy as np
5+
6+
7+
class SplineModel:
8+
"""
9+
Public curve wrapper backed by ferrispline.PyModel.
10+
11+
The public methods intentionally mirror the old BezierCurve contract to keep
12+
existing model/viewer behavior unchanged during migration.
13+
"""
14+
15+
def __init__(self, tag: str, control_points: list[list[float]], degree: int):
16+
super().__init__()
17+
self.tag = str(tag)
18+
self._degree = int(degree)
19+
self._model = ferrispline.PyModel()
20+
self._curve_id: str | None = None
21+
self._control_points_cache: list[list[float]] = []
22+
self.set_control_points(control_points)
23+
24+
@staticmethod
25+
def _default_control_points(coords_a, coords_b, degree=3):
26+
points = []
27+
28+
if degree == 0:
29+
return [coords_a]
30+
31+
num_points = degree + 1
32+
for i in range(num_points):
33+
t = i / degree
34+
current_point = [a + (b - a) * t for a, b in zip(coords_a, coords_b)]
35+
points.append(current_point)
36+
37+
return points
38+
39+
def get_tag(self):
40+
return self.tag
41+
42+
def get_control_points(self):
43+
return [list(pt) for pt in self._control_points_cache]
44+
45+
def get_degree(self):
46+
return self._degree
47+
48+
def set_control_points(self, control_points: list[list[float]]):
49+
cp_array = np.array(control_points, dtype=np.float64)
50+
self._control_points_cache = cp_array.tolist()
51+
self._curve_id = self._model.create_bezier(self._degree, cp_array, None)
52+
53+
def _evaluate(self, sample: int):
54+
if self._curve_id is None:
55+
return np.empty((0, 3), dtype=np.float64)
56+
curve_pts = self._model.evaluate(self._curve_id, int(sample))
57+
if (
58+
len(curve_pts.shape) == 2
59+
and curve_pts.shape[0] in (2, 3)
60+
and curve_pts.shape[1] > 3
61+
):
62+
curve_pts = curve_pts.T
63+
return curve_pts
64+
65+
def get_render_data(self) -> dict:
66+
curve_pts = self._evaluate(100)
67+
return {
68+
"tag": self.tag,
69+
"control_points": self.get_control_points(),
70+
"degree": self.get_degree(),
71+
"curve": curve_pts.tolist(),
72+
}

bot/view/curve_app.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
LineSegs,
1414
NodePath,
1515
)
16-
import nurbslib
1716
import numpy as np
17+
from bot.core.spline import SplineModel
1818

1919
MASK_CURVE_PICK = BitMask32.bit(1)
2020
MASK_CP_PICK = BitMask32.bit(2)
@@ -251,10 +251,8 @@ def preview_control_point(self, cp_index: int, new_pos: List[float]):
251251
self.control_points[cp_index] = [new_pos[0], new_pos[1], new_pos[2]]
252252

253253
if self.type == "bezier" and self.degree is not None:
254-
cp_array = np.array(self.control_points, dtype=np.float64)
255-
engine = nurbslib.PyBezierCurve(int(self.degree), cp_array, None)
256-
257-
pts = engine.evaluate(100, False)
254+
engine = SplineModel(str(self.tag), self.control_points, int(self.degree))
255+
pts = np.array(engine.get_render_data()["curve"], dtype=np.float64)
258256
if len(pts.shape) == 2 and pts.shape[0] in (2, 3) and pts.shape[1] > 3:
259257
pts = pts.T
260258

bot/viewer/viewer.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import threading
1313
from typing import Any, Callable, Optional, TYPE_CHECKING
1414

15-
from bot.core.curve import BezierCurve
15+
from bot.core.spline import SplineModel
1616

1717
if TYPE_CHECKING:
1818
from bot.core.cad import Model
@@ -333,10 +333,10 @@ def bezier_conversion(self, degree: int):
333333
tag = int(self._default_last_hovered)
334334
if self.model is not None:
335335
coords_a, coords_b = self.model.get_end_points_coords(int(tag))
336-
control_points = BezierCurve._default_control_points(
336+
control_points = SplineModel._default_control_points(
337337
coords_a, coords_b, degree
338338
)
339-
curve = BezierCurve(tag, control_points, degree)
339+
curve = SplineModel(tag, control_points, degree)
340340
self.model.set_curve(tag, curve)
341341
else:
342342
self.set_hud_text("Impossible to convert: no model loaded")

ferrispline

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ requires-python = ">=3.14"
88
dependencies = [
99
"gmsh>=4.15.0",
1010
"ipython>=9.10.0",
11-
"nurbslib",
11+
"ferrispline",
1212
"panda3d>=1.10.16",
1313
]
1414

1515
[tool.uv]
1616
package = true
1717

1818
[tool.uv.sources]
19-
nurbslib = { path = "ferrispline/nurbslib" }
19+
ferrispline = { path = "ferrispline/python" }
2020

2121
[build-system]
2222
requires = ["hatchling"]

tests/unit/test_curve/test_bezier-curve.py

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,43 @@
11
"""
2-
Unit tests for bot.core.curve.BezierCurve.
2+
Unit tests for bot.core.curve.SplineModel.
33
4-
The external Rust dependency (nurbslib) is mocked to allow testing the
4+
The external Rust dependency (ferrispline) is mocked to allow testing the
55
Python bridge and logic without requiring the compiled engine.
66
"""
77

88
import unittest
99
from unittest.mock import MagicMock, patch
1010
import numpy as np
1111

12-
from bot.core.curve import BezierCurve
12+
from bot.core.spline import SplineModel
1313

1414

15-
class TestBezierCurve(unittest.TestCase):
16-
@patch("bot.core.curve.nurbslib")
15+
class TestSplineModel(unittest.TestCase):
16+
@patch("bot.core.spline.ferrispline")
1717
def test_initialization_and_attributes(self, mock_nurbslib):
1818
"""Tests the initialization of the curve and access to its basic attributes."""
1919
# 1. Data preparation
2020
tag = "curve_1"
2121
control_points = [[0.0, 0.0, 0.0], [5.0, 5.0, 0.0], [10.0, 0.0, 0.0]]
2222
degree = 2
2323

24-
# 2. Mock configuration to simulate the nurbslib engine
25-
mock_engine_instance = MagicMock()
26-
mock_engine_instance.get_control_points.return_value = np.array(control_points)
27-
mock_engine_instance.get_degree.return_value = degree
28-
mock_nurbslib.PyBezierCurve.return_value = mock_engine_instance
24+
# 2. Mock configuration to simulate the ferrispline engine
25+
mock_model_instance = MagicMock()
26+
mock_model_instance.create_bezier.return_value = "curve-1"
27+
mock_model_instance.evaluate.return_value = np.array(control_points)
28+
mock_nurbslib.PyModel.return_value = mock_model_instance
2929

3030
# 3. Object creation
31-
curve = BezierCurve(tag, control_points, degree)
31+
curve = SplineModel(tag, control_points, degree)
3232

3333
# 4. Verifications (Assertions)
3434
self.assertEqual(curve.get_tag(), "curve_1")
3535
self.assertEqual(curve.get_control_points(), control_points)
3636
self.assertEqual(curve.get_degree(), degree)
3737

38-
# Ensure the Rust engine was called with the correct arguments
39-
mock_nurbslib.PyBezierCurve.assert_called_once()
40-
args, kwargs = mock_nurbslib.PyBezierCurve.call_args
38+
# Ensure the Rust model was called with the correct arguments
39+
mock_nurbslib.PyModel.assert_called_once()
40+
args, kwargs = mock_model_instance.create_bezier.call_args
4141
self.assertEqual(args[0], degree)
4242
np.testing.assert_array_equal(args[1], np.array(control_points))
4343
self.assertIsNone(args[2])
@@ -48,7 +48,7 @@ def test_default_control_points_default_degree(self):
4848
coords_b = [30.0, 0.0, 0.0]
4949

5050
# Call WITHOUT specifying the degree
51-
pts = BezierCurve._default_control_points(coords_a, coords_b)
51+
pts = SplineModel._default_control_points(coords_a, coords_b)
5252

5353
# Since the default degree is 3, we expect 3 + 1 = 4 points
5454
expected_pts = [
@@ -67,7 +67,7 @@ def test_default_control_points_distribution(self):
6767
degree = 2
6868

6969
# Execute static method
70-
pts = BezierCurve._default_control_points(coords_a, coords_b, degree)
70+
pts = SplineModel._default_control_points(coords_a, coords_b, degree)
7171

7272
# We expect 3 points: point A, middle point, and point B
7373
expected_pts = [[0.0, 0.0, 0.0], [5.0, 0.0, 0.0], [10.0, 0.0, 0.0]]
@@ -80,21 +80,21 @@ def test_default_control_points_count(self):
8080

8181
# We check for several different degrees
8282
for degree in [1, 3, 5, 10]:
83-
pts = BezierCurve._default_control_points(coords_a, coords_b, degree)
83+
pts = SplineModel._default_control_points(coords_a, coords_b, degree)
8484
self.assertEqual(len(pts), degree + 1)
8585

8686
def test_default_control_points_degree_zero(self):
8787
"""Tests the edge case where the curve degree is 0."""
8888
coords_a = [1.0, 2.0, 3.0]
8989
coords_b = [4.0, 5.0, 6.0]
9090

91-
pts = BezierCurve._default_control_points(coords_a, coords_b, degree=0)
91+
pts = SplineModel._default_control_points(coords_a, coords_b, degree=0)
9292

9393
# For degree 0, there must be only one point (point A)
9494
self.assertEqual(len(pts), 1)
9595
self.assertEqual(pts[0], coords_a)
9696

97-
@patch("bot.core.curve.nurbslib")
97+
@patch("bot.core.spline.ferrispline")
9898
def test_get_render_data(self, mock_nurbslib):
9999
"""Tests the structure and content of the render dictionary (used by the viewer)."""
100100
tag = "42"
@@ -105,14 +105,13 @@ def test_get_render_data(self, mock_nurbslib):
105105
mock_curve_eval = [[0.0, 0.0, 0.0], [0.5, 0.5, 0.5], [1.0, 1.0, 1.0]]
106106

107107
# Mock configuration
108-
mock_engine_instance = MagicMock()
109-
mock_engine_instance.get_control_points.return_value = np.array(control_points)
110-
mock_engine_instance.get_degree.return_value = degree
111-
mock_engine_instance.evaluate.return_value = np.array(mock_curve_eval)
112-
mock_nurbslib.PyBezierCurve.return_value = mock_engine_instance
108+
mock_model_instance = MagicMock()
109+
mock_model_instance.create_bezier.return_value = "curve-42"
110+
mock_model_instance.evaluate.return_value = np.array(mock_curve_eval)
111+
mock_nurbslib.PyModel.return_value = mock_model_instance
113112

114113
# Object creation and data retrieval
115-
curve = BezierCurve(tag, control_points, degree)
114+
curve = SplineModel(tag, control_points, degree)
116115
data = curve.get_render_data()
117116

118117
# Verification of the presence of all required keys
@@ -128,7 +127,7 @@ def test_get_render_data(self, mock_nurbslib):
128127
self.assertEqual(data["curve"], mock_curve_eval)
129128

130129
# Ensure the engine was called to generate 100 points
131-
mock_engine_instance.evaluate.assert_called_once_with(100, False)
130+
mock_model_instance.evaluate.assert_called_once_with("curve-42", 100)
132131

133132

134133
if __name__ == "__main__":

tests/unit/test_viewer/test_viewer.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -249,8 +249,8 @@ def _make_viewer_with_mocks(self):
249249

250250
return viewer
251251

252-
@patch("bot.viewer.viewer.BezierCurve")
253-
def test_bezier_conversion_success(self, MockBezierCurve):
252+
@patch("bot.viewer.viewer.SplineModel")
253+
def test_bezier_conversion_success(self, MockSplineModel):
254254
"""Verifies the conversion of a classic curve into a Bezier curve."""
255255
viewer = self._make_viewer_with_mocks()
256256

@@ -262,27 +262,27 @@ def test_bezier_conversion_success(self, MockBezierCurve):
262262
viewer.model.get_end_points_coords.return_value = [coords_a, coords_b]
263263

264264
# Mock configuration for the static method _default_control_points
265-
MockBezierCurve._default_control_points.return_value = [
265+
MockSplineModel._default_control_points.return_value = [
266266
coords_a,
267267
[3.3, 0, 0],
268268
[6.6, 0, 0],
269269
coords_b,
270270
]
271271

272-
# Configuration of the mocked instance returned by BezierCurve(...)
272+
# Configuration of the mocked instance returned by SplineModel(...)
273273
mock_curve_instance = MagicMock()
274-
MockBezierCurve.return_value = mock_curve_instance
274+
MockSplineModel.return_value = mock_curve_instance
275275

276276
# Method call
277277
viewer.bezier_conversion(degree)
278278

279279
# Assertions
280280
viewer.model.get_end_points_coords.assert_called_once_with(42)
281-
MockBezierCurve._default_control_points.assert_called_once_with(
281+
MockSplineModel._default_control_points.assert_called_once_with(
282282
coords_a, coords_b, degree
283283
)
284-
MockBezierCurve.assert_called_once_with(
285-
42, MockBezierCurve._default_control_points.return_value, degree
284+
MockSplineModel.assert_called_once_with(
285+
42, MockSplineModel._default_control_points.return_value, degree
286286
)
287287
viewer.model.set_curve.assert_called_once_with(42, mock_curve_instance)
288288

uv.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)