diff --git a/.vscode/settings.json b/.vscode/settings.json
index 84455a3..1e30f8e 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -17,11 +17,13 @@
"freecad",
"fretboard",
"Fretwire",
+ "gordon",
"inkex",
"inkscape",
"ISVGDocumentElement",
"linexy",
"Luthiery",
+ "marz",
"Marz",
"marzguitars",
"OCCT",
@@ -30,6 +32,7 @@
"Pixmap",
"Preselection",
"qobject",
+ "reparametrize",
"rgetattr",
"rsetattr",
"SETIN",
@@ -39,5 +42,10 @@
"unpolish"
],
"python.defaultInterpreterPath": "/usr/bin/python3",
- "editor.fontLigatures": true
+ "editor.fontLigatures": true,
+ "licenser.license": "GPLv3",
+ "licenser.projectName": "Marz",
+ "licenser.author": "Frank David Martinez M (mnesarco)",
+ "ruff.format.args": ["--config", "line-length=120"]
+
}
\ No newline at end of file
diff --git a/README.md b/README.md
index a46bb2e..0e014c4 100644
--- a/README.md
+++ b/README.md
@@ -90,7 +90,6 @@ The Wiki contains some useful documents: [Wiki](https://github.com/mnesarco/Marz
## Requirements
* FreeCAD v0.21+ ([releases](https://github.com/FreeCAD/FreeCAD/releases/))
-* Curves Workbench 0.6.31+ (Install using AddonManager)
## Install
diff --git a/Resources/intro.html b/Resources/intro.html
index c8c17df..41f7fe9 100644
--- a/Resources/intro.html
+++ b/Resources/intro.html
@@ -1,22 +1,22 @@
+
Examples
@@ -52,7 +52,7 @@ Examples
Black Machine B6
-
+
@@ -92,7 +92,7 @@ Inkscape Extension
Download here
Install it in Inkscape: Extensions/Manage Extensions.../Install packages/Install package
Once installed, access it in Extensions/FreeCAD/Marz Guitars Selector...
-
+
@@ -118,15 +118,10 @@ Local Environment
| {{freecad_version}} |
{{freecad_supported}} |
-
- Curves Workbench |
- {{curves_version}} |
- {{curves_supported}} |
-
-
-
+
+
Help
FreeCAD Forum
@@ -140,7 +135,7 @@ FreeCAD Forum
Youtube channel
@@ -155,18 +150,18 @@ Wiki
Bug tracking
-
+
Operating system support
- Any OS supported by the current version of FreeCAD should work,
- but in case of issues related to the OS, Linux has first
- class support and issues will be resolved there first. Issues
- specifically related to Mac or Windows has second
+ Any OS supported by the current version of FreeCAD should work,
+ but in case of issues related to the OS, Linux has first
+ class support and issues will be resolved there first. Issues
+ specifically related to Mac or Windows has second
class support and has lower priority.
diff --git a/changelog.md b/changelog.md
index aacaf2a..61f9b76 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,9 @@
# Marz Workbench Changelog
+## 0.1.9 (Nov 1st 2024)
+
+- Removed CurvesWB dependency
+
## 0.1.8 (Aug 22th 2024)
- Improve neck-headstock transition geometry (Issue #40)
@@ -59,7 +63,7 @@
- GUI
- FreeCAD's Property editor is now in readonly mode
- - New custom Property editor
+ - New custom Property editor
- New import svg ui with preview and validation
- Imported files are now embedded into the document to keep everything together and can be exported back
- Grouping all parts in the three
diff --git a/freecad/marz/__init__.py b/freecad/marz/__init__.py
index 71baa57..b19094e 100644
--- a/freecad/marz/__init__.py
+++ b/freecad/marz/__init__.py
@@ -18,11 +18,10 @@
# | along with Marz Workbench. If not, see . |
# +---------------------------------------------------------------------------+
-__version__ = "0.1.8"
+__version__ = "0.1.9"
__author__ = "Frank David Martinez M "
__copyright__ = "Copyright (c) 2020, Frank David Martinez M."
__license__ = "GPLv3"
__maintainer__ = "https://github.com/mnesarco/"
-__dep_min_curves__ = '0.6.31'
__dep_min_freecad__ = '0.21.1'
diff --git a/freecad/marz/curves/__init__.py b/freecad/marz/curves/__init__.py
new file mode 100644
index 0000000..1963c75
--- /dev/null
+++ b/freecad/marz/curves/__init__.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# +---------------------------------------------------------------------------+
+# | Copyright (c) 2020 Frank Martinez |
+# | |
+# | This file is part of Marz Workbench. |
+# | |
+# | Marz Workbench is free software: you can redistribute it and/or modify |
+# | it under the terms of the GNU General Public License as published by |
+# | the Free Software Foundation, either version 3 of the License, or |
+# | (at your option) any later version. |
+# | |
+# | Marz Workbench is distributed in the hope that it will be useful, |
+# | but WITHOUT ANY WARRANTY; without even the implied warranty of |
+# | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
+# | GNU General Public License for more details. |
+# | |
+# | You should have received a copy of the GNU General Public License |
+# | along with Marz Workbench. If not, see . |
+# +---------------------------------------------------------------------------+
diff --git a/freecad/marz/curves/gordon.py b/freecad/marz/curves/gordon.py
new file mode 100644
index 0000000..b952ef5
--- /dev/null
+++ b/freecad/marz/curves/gordon.py
@@ -0,0 +1,2118 @@
+# -*- coding: utf-8 -*-
+# +---------------------------------------------------------------------------+
+# | Copyright (c) 2020 Frank Martinez |
+# | |
+# | This file is part of Marz Workbench. |
+# | |
+# | Marz Workbench is free software: you can redistribute it and/or modify |
+# | it under the terms of the GNU General Public License as published by |
+# | the Free Software Foundation, either version 3 of the License, or |
+# | (at your option) any later version. |
+# | |
+# | Marz Workbench is distributed in the hope that it will be useful, |
+# | but WITHOUT ANY WARRANTY; without even the implied warranty of |
+# | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
+# | GNU General Public License for more details. |
+# | |
+# | You should have received a copy of the GNU General Public License |
+# | along with Marz Workbench. If not, see . |
+# +---------------------------------------------------------------------------+
+
+# !NOTICE
+#
+# This file is a modified version of files:
+# - freecad/Curves/gordon.py
+# - freecad/Curves/BSplineAlgorithms.py
+# - freecad/Curves/BSplineApproxInterp.py
+# - freecad/Curves/curve_network_sorter.py
+# - freecad/Curves/nurbs_tools.py
+#
+# From the CurvesWB library: https://github.com/tomate44/CurvesWB
+# under LGPL2.1 license
+# author: Christophe Grellier (Chris_G)
+#
+# Original code in C++ comes from: https://github.com/DLR-SC/tigl
+# under Apache-2 license
+
+from __future__ import annotations
+
+from collections.abc import Sequence
+from math import pi
+
+import FreeCAD # type: ignore
+import numpy as np
+import Part # type: ignore
+from FreeCAD import Vector, Base # type: ignore
+from numpy.typing import NDArray
+
+from freecad.marz.extension.fcui import ProgressIndicator
+
+warn = FreeCAD.Console.PrintWarning
+Vector2d = Base.Vector2d
+
+class GordonSurfaceBuilderError(Exception):
+ pass
+
+
+def find(val: int | float, array: Sequence[int | float], tol: float = 1e-5) -> int:
+ """
+ Return index of val in array, within given tolerance
+ Else return -1
+ """
+ for i, x in enumerate(array):
+ if abs(val - x) < tol:
+ return i
+ return -1
+
+
+class GordonSurfaceBuilder:
+ """Build a Gordon surface from a network of curves"""
+
+ tensorProdSurf: Part.BSplineSurface
+ skinningSurfProfiles: Part.BSplineSurface
+ skinningSurfProfiles: Part.BSplineSurface
+ gordonSurf: Part.BSplineSurface
+
+ def __init__(
+ self,
+ profiles: list[Part.Curve],
+ guides: list[Part.Curve],
+ params_u: list[float],
+ params_v: list[float],
+ tol: float = 1e-7,
+ par_tol: float = 1e-12,
+ ):
+ if len(profiles) < 2:
+ raise GordonSurfaceBuilderError("Not enough profiles")
+
+ if len(guides) < 2:
+ raise GordonSurfaceBuilderError("Not enough guides")
+
+ if tol <= 0:
+ raise GordonSurfaceBuilderError("tolerance (tol) must be a positive number")
+
+ if par_tol <= 0:
+ raise GordonSurfaceBuilderError("tolerance (par_tol) must be a positive number")
+
+ self.profiles = profiles
+ self.guides = guides
+ self.intersectionParamsU = params_u
+ self.intersectionParamsV = params_v
+ self.has_performed = False
+ self.tolerance = tol
+ self.par_tol = par_tol
+ self.gordonSurf = None
+ self.skinningSurfProfiles = None
+ self.tensorProdSurf = None
+
+ def perform(self):
+ if self.has_performed:
+ return
+ self.create_gordon_surface()
+ self.has_performed = True
+
+ def surface_gordon(self) -> Part.BSplineSurface:
+ self.perform()
+ return self.gordonSurf
+
+ def surface_profiles(self) -> Part.BSplineSurface:
+ self.perform()
+ return self.skinningSurfProfiles
+
+ def surface_guides(self) -> Part.BSplineSurface:
+ self.perform()
+ return self.skinningSurfGuides
+
+ def surface_intersections(self) -> Part.BSplineSurface:
+ self.perform()
+ return self.tensorProdSurf
+
+ def curve_network(self) -> Part.Compound:
+ self.perform()
+ profiles = Part.Compound([c.toShape() for c in self.profiles])
+ guides = Part.Compound([c.toShape() for c in self.guides])
+ return Part.Compound([profiles, guides])
+
+ def create_gordon_surface(self) -> None:
+ if len(self.profiles) < 2:
+ raise GordonSurfaceBuilderError("There must be at least two profiles")
+
+ if len(self.guides) < 2:
+ raise GordonSurfaceBuilderError("There must be at least two guides")
+
+ # check B-spline parametrization is equal among all curves
+ # umin = self.profiles[0].FirstParameter
+ # umax = self.profiles[0].LastParameter
+ # TODO
+ # for (CurveArray::const_iterator it = m_self.profiles.begin(); it != m_self.profiles.end(); ++it) {
+ # assertRange(*it, umin, umax, 1e-5);
+
+ # vmin = self.guides[0].FirstParameter
+ # vmax = self.guides[0].LastParameter
+ # TODO
+ # for (CurveArray::const_iterator it = m_self.guides.begin(); it != m_self.guides.end(); ++it) {
+ # assertRange(*it, vmin, vmax, 1e-5);
+
+ # TODO: Do we really need to check compatibility?
+ # We don't need to do this, if the curves were re-parametrized before
+ # In this case, they might be even incompatible, as the curves have been approximated
+ self.check_curve_network_compatibility()
+
+ # setting everything up for creating Tensor Product Surface by interpolating intersection
+ # points of self.profiles and self.guides with B-Spline surface
+ # find the intersection points:
+ num_intersectionParamsV = len(self.intersectionParamsV)
+ num_intersectionParamsU = len(self.intersectionParamsU)
+ intersection_pnts = [[0] * num_intersectionParamsV for i in range(num_intersectionParamsU)]
+
+ # TColgp_Array2OfPnt intersection_pnts(1, static_cast(self.intersectionParamsU.size()),
+ # 1, static_cast(self.intersectionParamsV.size()));
+
+ # use splines in u-direction to get intersection points
+ for spline_idx in range(len(self.profiles)):
+ for intersection_idx in range(num_intersectionParamsU):
+ spline_u = self.profiles[spline_idx]
+ parameter = self.intersectionParamsU[intersection_idx]
+ intersection_pnts[intersection_idx][spline_idx] = spline_u.value(parameter)
+
+ # check, whether to build a closed continuous surface
+ bsa = BSplineAlgorithms(self.par_tol)
+ # curve_u_tolerance = bsa.REL_TOL_CLOSED * bsa.scale(self.guides)
+ # curve_v_tolerance = bsa.REL_TOL_CLOSED * bsa.scale(self.profiles)
+ tp_tolerance = bsa.REL_TOL_CLOSED * bsa.scale_pt_array(intersection_pnts)
+ # TODO No IsEqual in FreeCAD
+ makeUClosed = bsa.isUDirClosed(intersection_pnts, tp_tolerance)
+ # and self.guides[0].toShape().isPartner(self.guides[-1].toShape()) # .isEqual(self.guides[-1], curve_u_tolerance);
+ makeVClosed = bsa.isVDirClosed(intersection_pnts, tp_tolerance)
+ # and self.profiles[0].toShape().IsPartner(self.profiles[-1].toShape())
+
+ # Skinning in v-direction with u directional B-Splines
+ surfProfiles = bsa.curvesToSurface(self.profiles, self.intersectionParamsV, makeVClosed)
+ # therefore re-parametrization before this method
+
+ # Skinning in u-direction with v directional B-Splines
+ surfGuides = bsa.curvesToSurface(self.guides, self.intersectionParamsU, makeUClosed)
+
+ # flipping of the surface in v-direction
+ surfGuides = bsa.flipSurface(surfGuides)
+
+ # if there are too little points for UDegree = 3 and VDegree = 3
+ # creating an interpolation B-spline surface isn't possible in OCC
+
+ # Open CASCADE doesn't have a B-spline surface interpolation method
+ # where one can give the u- and v-directional parameters as arguments
+ tensorProdSurf = bsa.pointsToSurface(
+ intersection_pnts,
+ self.intersectionParamsU,
+ self.intersectionParamsV,
+ makeUClosed,
+ makeVClosed,
+ )
+
+ # match degree of all three surfaces
+ degreeU = max(max(surfGuides.UDegree, surfProfiles.UDegree), tensorProdSurf.UDegree)
+ degreeV = max(max(surfGuides.VDegree, surfProfiles.VDegree), tensorProdSurf.VDegree)
+
+ # check whether degree elevation is necessary
+ # and if yes, elevate degree
+ surfGuides.increaseDegree(degreeU, degreeV)
+ surfProfiles.increaseDegree(degreeU, degreeV)
+ tensorProdSurf.increaseDegree(degreeU, degreeV)
+ surfaces_vector_unmod = [surfGuides, surfProfiles, tensorProdSurf]
+
+ # create common knot vector for all three surfaces
+ surfaces_vector = bsa.createCommonKnotsVectorSurface(surfaces_vector_unmod, self.par_tol)
+
+ assert len(surfaces_vector) == 3
+
+ self.skinningSurfGuides = surfaces_vector[0]
+ self.skinningSurfProfiles = surfaces_vector[1]
+ self.tensorProdSurf = surfaces_vector[2]
+
+ assert (
+ self.skinningSurfGuides.NbUPoles == self.skinningSurfProfiles.NbUPoles
+ and self.skinningSurfProfiles.NbUPoles == self.tensorProdSurf.NbUPoles
+ )
+ assert (
+ self.skinningSurfGuides.NbVPoles == self.skinningSurfProfiles.NbVPoles
+ and self.skinningSurfProfiles.NbVPoles == self.tensorProdSurf.NbVPoles
+ )
+
+ self.gordonSurf = self.skinningSurfProfiles.copy()
+
+ # creating the Gordon Surface = s_u + s_v - tps by adding the control points
+ for cp_u_idx in range(1, self.gordonSurf.NbUPoles + 1):
+ for cp_v_idx in range(1, self.gordonSurf.NbVPoles + 1):
+ cp_surf_u = self.skinningSurfProfiles.getPole(cp_u_idx, cp_v_idx)
+ cp_surf_v = self.skinningSurfGuides.getPole(cp_u_idx, cp_v_idx)
+ cp_tensor = self.tensorProdSurf.getPole(cp_u_idx, cp_v_idx)
+ self.gordonSurf.setPole(cp_u_idx, cp_v_idx, cp_surf_u + cp_surf_v - cp_tensor)
+
+ def check_curve_network_compatibility(self) -> None:
+ # self.profiles, self.guides, self.intersectionParamsU, self.intersectionParamsV, tol):
+ # find out the 'average' scale of the B-splines in order to being able to handle a more
+ # approximate dataset and find its intersections
+ bsa = BSplineAlgorithms(self.par_tol)
+ splines_scale = 0.5 * (bsa.scale(self.profiles) + bsa.scale(self.guides))
+ scale_tol = splines_scale * self.tolerance
+
+ if abs(self.intersectionParamsU[0]) > scale_tol or abs(self.intersectionParamsU[-1] - 1.0) > scale_tol:
+ warn("B-splines in u-direction must not stick out, spline network must be 'closed'!")
+
+ if abs(self.intersectionParamsV[0]) > scale_tol or abs(self.intersectionParamsV[-1] - 1.0) > scale_tol:
+ warn("B-splines in v-direction mustn't stick out, spline network must be 'closed'!")
+
+ # check compatibility of network
+ num_intersectionParamsV = len(self.intersectionParamsV)
+ for u_param_idx in range(len(self.intersectionParamsU)):
+ spline_u_param = self.intersectionParamsU[u_param_idx]
+ spline_v = self.guides[u_param_idx]
+ for v_param_idx in range(num_intersectionParamsV):
+ spline_u = self.profiles[v_param_idx]
+ spline_v_param = self.intersectionParamsV[v_param_idx]
+ p_prof = spline_u.value(spline_u_param)
+ p_guid = spline_v.value(spline_v_param)
+ distance = p_prof.distanceToPoint(p_guid)
+ if distance > scale_tol:
+ raise GordonSurfaceBuilderError(f"""
+ B-spline network is incompatible (e.g. wrong parametrization)
+ or intersection parameters are in a wrong order!
+ profile {u_param_idx} - guide {v_param_idx}
+ """)
+
+
+class InterpolateCurveNetworkError(Exception):
+ pass
+
+
+class InterpolateCurveNetwork(object):
+ """Bspline surface interpolating a network of curves"""
+
+ profiles: list[Part.BSplineCurve]
+ guides: list[Part.BSplineCurve]
+
+ def __init__(
+ self,
+ profiles: Sequence[Part.BSplineCurve],
+ guides: Sequence[Part.BSplineCurve],
+ tol: float = 1e-5,
+ par_tolerance: float = 1e-10,
+ ):
+ if len(profiles) < 2:
+ raise InterpolateCurveNetworkError("Not enough profiles")
+
+ if len(guides) < 2:
+ raise InterpolateCurveNetworkError("Not enough guides")
+
+ if tol <= 0:
+ raise InterpolateCurveNetworkError("tolerance (tol) must be a positive number")
+
+ if par_tolerance <= 0:
+ raise InterpolateCurveNetworkError("tolerance (par_tol) must be a positive number")
+
+ self.tolerance = tol
+ self.par_tolerance = par_tolerance
+ self.max_ctrl_pts = 80
+ self.has_performed = False
+ self.profiles = [p.copy() for p in profiles]
+ self.guides = [g.copy() for g in guides]
+
+ def perform(self):
+ if self.has_performed:
+ return
+ self.make_curves_compatible()
+ builder = GordonSurfaceBuilder(
+ self.profiles,
+ self.guides,
+ self.intersectionParamsU,
+ self.intersectionParamsV,
+ self.tolerance,
+ self.par_tolerance,
+ )
+ self.gordon_surf = builder.surface_gordon()
+ self.skinning_surf_profiles = builder.surface_profiles()
+ self.skinning_surf_guides = builder.surface_guides()
+ self.tensor_prod_surf = builder.surface_intersections()
+ self.curve_network_compound = builder.curve_network()
+ self.has_performed = True
+
+ def surface_profiles(self):
+ self.perform()
+ return self.skinning_surf_profiles
+
+ def surface_guides(self):
+ self.perform()
+ return self.skinning_surf_guides
+
+ def surface_intersections(self):
+ self.perform()
+ return self.tensor_prod_surf
+
+ def parameters_profiles(self):
+ self.perform()
+ return self.intersection_params_v
+
+ def parameters_guides(self):
+ self.perform()
+ return self.intersection_params_u
+
+ def surface(self):
+ self.perform()
+ return self.gordon_surf
+
+ def curve_network(self):
+ self.perform()
+ return self.curve_network_compound
+
+ def compute_intersections(
+ self,
+ intersection_params_u: list[list[float]],
+ intersection_params_v: list[list[float]],
+ ):
+ for spline_u_idx in range(len(self.profiles)):
+ for spline_v_idx in range(len(self.guides)):
+ currentIntersections = BSplineAlgorithms(self.par_tolerance).intersections(
+ self.profiles[spline_u_idx],
+ self.guides[spline_v_idx],
+ self.par_tolerance,
+ )
+ if len(currentIntersections) < 1:
+ raise InterpolateCurveNetworkError(
+ """U-directional B-spline and v-directional B-spline don't intersect each other!
+ profile {spline_u_idx} / guide {spline_v_idx}
+ """
+ )
+ elif len(currentIntersections) == 1:
+ intersection_params_u[spline_u_idx][spline_v_idx] = currentIntersections[0][0]
+ intersection_params_v[spline_u_idx][spline_v_idx] = currentIntersections[0][1]
+ # for closed curves
+ elif len(currentIntersections) == 2:
+ # only the u-directional B-spline curves are closed
+ if self.profiles[0].isClosed():
+ if spline_v_idx == 0:
+ intersection_params_u[spline_u_idx][spline_v_idx] = min(
+ currentIntersections[0][0], currentIntersections[1][0]
+ )
+ elif spline_v_idx == len(self.guides) - 1:
+ intersection_params_u[spline_u_idx][spline_v_idx] = max(
+ currentIntersections[0][0], currentIntersections[1][0]
+ )
+ # intersection_params_vector[0].second == intersection_params_vector[1].second
+ intersection_params_v[spline_u_idx][spline_v_idx] = currentIntersections[0][1]
+
+ # only the v-directional B-spline curves are closed
+ if self.guides[0].isClosed():
+ if spline_u_idx == 0:
+ intersection_params_v[spline_u_idx][spline_v_idx] = min(
+ currentIntersections[0][1], currentIntersections[1][1]
+ )
+ elif spline_u_idx == len(self.profiles) - 1:
+ intersection_params_v[spline_u_idx][spline_v_idx] = max(
+ currentIntersections[0][1], currentIntersections[1][1]
+ )
+ # intersection_params_vector[0].first == intersection_params_vector[1].first
+ intersection_params_u[spline_u_idx][spline_v_idx] = currentIntersections[0][0]
+ # TODO: both u-directional splines and v-directional splines are closed
+ # elif len(currentIntersections) == 4:
+ else:
+ raise InterpolateCurveNetworkError(
+ "U-directional B-spline and v-directional B-spline have more than two intersections with each other!"
+ )
+
+ def sort_curves(
+ self,
+ intersection_params_u: list[list[float]],
+ intersection_params_v: list[list[float]],
+ ):
+ sorterObj = CurveNetworkSorter(
+ self.profiles,
+ self.guides,
+ intersection_params_u,
+ intersection_params_v,
+ )
+ sorterObj.Perform()
+
+ # get the sorted matrices and vectors
+ intersection_params_u = sorterObj.parmsIntersProfiles
+ intersection_params_v = sorterObj.parmsIntersGuides
+
+ self.profiles = sorterObj.profiles
+ self.guides = sorterObj.guides
+ return intersection_params_u, intersection_params_v
+
+ def make_curves_compatible(self):
+ # re-parametrize into [0,1]
+ bsa = BSplineAlgorithms()
+ for c in self.profiles:
+ bsa.reparametrize_bspline(c, 0.0, 1.0, self.par_tolerance)
+ for c in self.guides:
+ bsa.reparametrize_bspline(c, 0.0, 1.0, self.par_tolerance)
+ # now the parameter range of all profiles and guides is [0, 1]
+
+ nGuides = len(self.guides)
+ nProfiles = len(self.profiles)
+ # now find all intersections of all B-splines with each other
+ intersection_params_u = [[0] * nGuides for k in range(nProfiles)] # (0, nProfiles - 1, 0, nGuides - 1);
+ intersection_params_v = [[0] * nGuides for k in range(nProfiles)] # (0, nProfiles - 1, 0, nGuides - 1);
+ self.compute_intersections(intersection_params_u, intersection_params_v)
+ # sort intersection_params_u and intersection_params_v and u-directional and v-directional B-spline curves
+ intersection_params_u, intersection_params_v = self.sort_curves(intersection_params_u, intersection_params_v)
+
+ # eliminate small inaccuracies of the intersection parameters:
+ self.eliminate_inaccuracies_network_intersections(
+ self.profiles,
+ self.guides,
+ intersection_params_u,
+ intersection_params_v,
+ )
+
+ newParametersProfiles = list()
+ for spline_v_idx in range(1, nGuides + 1): # (int spline_v_idx = 1; spline_v_idx <= nGuides; ++spline_v_idx) {
+ summ = 0
+ for spline_u_idx in range(1, nProfiles + 1):
+ summ += intersection_params_u[spline_u_idx - 1][spline_v_idx - 1]
+ newParametersProfiles.append(1.0 * summ / nProfiles)
+
+ newParametersGuides = list()
+ for spline_u_idx in range(1, nProfiles + 1):
+ summ = 0
+ for spline_v_idx in range(1, nGuides + 1):
+ summ += intersection_params_v[spline_u_idx - 1][spline_v_idx - 1]
+ newParametersGuides.append(1.0 * summ / nGuides)
+
+ if newParametersProfiles[0] > self.tolerance or newParametersGuides[0] > self.tolerance:
+ raise InterpolateCurveNetworkError("At least one B-splines has no intersection at the beginning.")
+
+ # Get maximum number of control points to figure out detail of spline
+ max_cp_u = 0
+ max_cp_v = 0
+ for c in self.profiles:
+ max_cp_u = max(max_cp_u, c.NbPoles)
+ for c in self.guides:
+ max_cp_v = max(max_cp_v, c.NbPoles)
+
+ # we want to use at least 10 and max "self.max_ctrl_pts" control
+ # points to be able to re-parametrize the geometry properly
+ mincp = 10
+ maxcp = self.max_ctrl_pts
+
+ # since we interpolate the intersections, we cannot use fewer control points than curves
+ # We need to add two since we want c2 continuity, which adds two equations
+ min_u = max(nGuides + 2, mincp)
+ min_v = max(nProfiles + 2, mincp)
+
+ max_u = max(min_u, maxcp)
+ max_v = max(min_v, maxcp)
+
+ # Clamp(val, min, max) : return std::max(min, std::min(val, max));
+ max_cp_u = max(min_u, min(max_cp_u + 10, max_u))
+ max_cp_v = max(min_v, min(max_cp_v + 10, max_v))
+
+ progress_bar = ProgressIndicator()
+ progress_bar.start("Computing Gordon surface ...", nProfiles + nGuides)
+
+ # re-parametrize u-directional B-splines
+ for spline_u_idx in range(nProfiles):
+ # (int spline_u_idx = 0; spline_u_idx < nProfiles; ++spline_u_idx) {
+ oldParametersProfile = list()
+ for spline_v_idx in range(nGuides):
+ oldParametersProfile.append(intersection_params_u[spline_u_idx][spline_v_idx])
+ # eliminate small inaccuracies at the first knot
+ if abs(oldParametersProfile[0]) < self.tolerance:
+ oldParametersProfile[0] = 0.0
+ if abs(newParametersProfiles[0]) < self.tolerance:
+ newParametersProfiles[0] = 0.0
+ # eliminate small inaccuracies at the last knot
+ if abs(oldParametersProfile[-1] - 1.0) < self.tolerance:
+ oldParametersProfile[-1] = 1.0
+ if abs(newParametersProfiles[-1] - 1.0) < self.tolerance:
+ newParametersProfiles[-1] = 1.0
+
+ profile = self.profiles[spline_u_idx]
+ self.profiles[spline_u_idx] = bsa.reparametrizeBSplineContinuouslyApprox(
+ profile, oldParametersProfile, newParametersProfiles, max_cp_u
+ )
+ progress_bar.next()
+
+ # re-parametrize v-directional B-splines
+ for spline_v_idx in range(nGuides):
+ oldParameterGuide = list()
+ for spline_u_idx in range(nProfiles):
+ oldParameterGuide.append(intersection_params_v[spline_u_idx][spline_v_idx])
+ # eliminate small inaccuracies at the first knot
+ if abs(oldParameterGuide[0]) < self.tolerance:
+ oldParameterGuide[0] = 0.0
+ if abs(newParametersGuides[0]) < self.tolerance:
+ newParametersGuides[0] = 0.0
+ # eliminate small inaccuracies at the last knot
+ if abs(oldParameterGuide[-1] - 1.0) < self.tolerance:
+ oldParameterGuide[-1] = 1.0
+ if abs(newParametersGuides[-1] - 1.0) < self.tolerance:
+ newParametersGuides[-1] = 1.0
+
+ guide = self.guides[spline_v_idx]
+ self.guides[spline_v_idx] = bsa.reparametrizeBSplineContinuouslyApprox(
+ guide, oldParameterGuide, newParametersGuides, max_cp_v
+ )
+ progress_bar.next()
+
+ progress_bar.stop()
+
+ self.intersectionParamsU = newParametersProfiles
+ self.intersectionParamsV = newParametersGuides
+
+ def eliminate_inaccuracies_network_intersections(
+ self,
+ sortedProfiles: list[Part.BSplineCurve],
+ sortedGuides: list[Part.BSplineCurve],
+ intersection_params_u: list[list[float]],
+ intersection_params_v: list[list[float]],
+ ):
+ nProfiles = len(sortedProfiles)
+ nGuides = len(sortedGuides)
+ # tol = 0.001
+ # eliminate small inaccuracies of the intersection parameters:
+
+ # first intersection
+ for spline_u_idx in range(nProfiles):
+ if abs(intersection_params_u[spline_u_idx][0] - sortedProfiles[0].getKnot(1)) < self.tolerance:
+ if abs(sortedProfiles[0].getKnot(1)) < self.par_tolerance:
+ intersection_params_u[spline_u_idx][0] = 0
+ else:
+ intersection_params_u[spline_u_idx][0] = sortedProfiles[0].getKnot(1)
+
+ for spline_v_idx in range(nGuides):
+ if abs(intersection_params_v[0][spline_v_idx] - sortedGuides[0].getKnot(1)) < self.tolerance:
+ if abs(sortedGuides[0].getKnot(1)) < self.par_tolerance:
+ intersection_params_v[0][spline_v_idx] = 0
+ else:
+ intersection_params_v[0][spline_v_idx] = sortedGuides[0].getKnot(1)
+
+ # last intersection
+ first_profile_last_knot = sortedProfiles[0].getKnot(sortedProfiles[0].NbKnots)
+ for spline_u_idx in range(nProfiles):
+ if abs(intersection_params_u[spline_u_idx][nGuides - 1] - first_profile_last_knot) < self.tolerance:
+ intersection_params_u[spline_u_idx][nGuides - 1] = first_profile_last_knot
+
+ first_guide_last_knot = sortedGuides[0].getKnot(sortedGuides[0].NbKnots)
+ for spline_v_idx in range(nGuides):
+ if abs(intersection_params_v[nProfiles - 1][spline_v_idx] - first_guide_last_knot) < self.tolerance:
+ intersection_params_v[nProfiles - 1][spline_v_idx] = first_guide_last_knot
+
+
+# -/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/
+# BSplineApproxInterp
+# -/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/
+
+
+def square_distance(v1: float, v2: float) -> float:
+ return pow(v2.x - v1.x, 2) + pow(v2.y - v1.y, 2)
+
+
+def insert_knot(
+ knot: float,
+ count: int,
+ degree: int,
+ knots: list[float],
+ mults: list[int],
+ tol: float = 1e-5,
+):
+ """Insert knot in knots, with multiplicity count in mults"""
+ if knot < knots[0] or knot > knots[-1]:
+ raise RuntimeError("knot out of range")
+
+ pos = find(knot, knots, tol)
+ if pos == -1: # knot not found, insert new one
+ pos = 0
+ while knots[pos] < knot:
+ pos += 1
+ knots.insert(pos, knot)
+ mults.insert(pos, min(count, degree))
+ else: # knot found, increase multiplicity
+ mults[pos] = min(mults[pos] + count, degree)
+
+
+def bsplineBasisMat(
+ degree: int,
+ knots: list[float],
+ params: list[float],
+ derivOrder: int,
+) -> NDArray:
+ """Return a matrix of values of BSpline Basis functions(or derivatives)"""
+ ncp = len(knots) - degree - 1
+ mx = np.array([[0.0] * ncp for i in range(len(params))])
+ # math_Matrix mx(1, params.Length(), 1, ncp);
+ # mx.Init(0.);
+ bspl_basis = np.array([[0.0] * (ncp) for i in range(derivOrder + 1)])
+ # math_Matrix bspl_basis(1, derivOrder + 1, 1, degree + 1);
+ # bspl_basis.Init(0.);
+ # debug("params %s"%str(params))
+ for iparm in range(len(params)):
+ basis_start_index = 0
+ bb = BsplineBasis()
+ bb.knots = knots
+ bb.degree = degree
+
+ for irow in range(derivOrder + 1):
+ bspl_basis[irow] = bb.evaluate(params[iparm], d=irow)
+
+ for i in range(len(bspl_basis[derivOrder])):
+ mx[iparm][basis_start_index + i] = bspl_basis[derivOrder][i]
+
+ return mx
+
+
+class BSplineApproxInterp:
+ """
+ BSpline curve approximating a list of points
+ Some points can be interpolated, or be set as C0 kinks
+ """
+
+ # used in BSplineAlgorithms.reparametrizeBSplineContinuouslyApprox
+
+ def __init__(
+ self,
+ points: list[Vector],
+ nControlPoints: int,
+ degree: int,
+ continuous_if_closed: bool,
+ ):
+ self.pnts = points
+ self.indexOfApproximated = list(range(len(points)))
+ self.degree = degree
+ self.ncp = nControlPoints
+ self.C2Continuous = continuous_if_closed
+ self.indexOfInterpolated = list()
+ self.indexOfKinks = list()
+
+ def InterpolatePoint(self, pointIndex: int, withKink: bool) -> None:
+ """Switch point from approximation to interpolation
+ If withKink, also set it as Kink"""
+ if pointIndex not in self.indexOfApproximated:
+ warn("""Invalid index in BSplineApproxInterp.InterpolatePoint
+ {pointIndex} is not in {self.indexOfApproximated}
+ """)
+ else:
+ self.indexOfApproximated.remove(pointIndex)
+ self.indexOfInterpolated.append(int(pointIndex))
+ if withKink:
+ self.indexOfKinks.append(pointIndex)
+
+ def FitCurveOptimal(self, initialParms: list[float], maxIter: int) -> tuple[Part.BSplineCurve, float]:
+ """Iterative fitting of a BSpline curve on the points"""
+ # compute initial parameters, if initialParms empty
+ if len(initialParms) == 0:
+ parms = self.computeParameters(0.5)
+ else:
+ parms = initialParms
+
+ if not len(parms) == len(self.pnts):
+ raise RuntimeError("Number of parameters don't match number of points")
+
+ # Compute knots from parameters
+ knots, mults = self.computeKnots(self.ncp, parms)
+
+ # solve system
+ iteration = 0
+ result, error = self.python_solve(parms, knots, mults) # TODO occKnots, occMults ???? See above
+ old_error = error * 2
+
+ while (error > 0) and ((old_error - error) / max(error, 1e-6) > 1e-6) and (iteration < maxIter):
+ old_error = error
+ self.optimizeParameters(result, parms)
+ result, error = self.python_solve(parms, knots, mults)
+ iteration += 1
+
+ return result, error
+
+ def computeParameters(self, alpha: float):
+ """Computes parameters for the points self.pnts
+ alpha is a parametrization factor
+ alpha = 0.0 -> Uniform
+ alpha = 0.5 -> Centripetal
+ alpha = 1.0 -> ChordLength"""
+ sum = 0.0
+ nPoints = len(self.pnts)
+ t = [0.0]
+ # calc total arc length: dt^2 = dx^2 + dy^2
+ for i in range(1, nPoints):
+ len2 = square_distance(self.pnts[i - 1], self.pnts[i])
+ sum += pow(len2, alpha) # / 2.)
+ t.append(sum)
+ # normalize parameter with maximum
+ tmax = t[-1]
+ for i in range(1, nPoints):
+ t[i] /= tmax
+ # reset end value to achieve a better accuracy
+ t[-1] = 1.0
+ return t
+
+ def computeKnots(self, ncp: int, parms: list[float]):
+ """Computes knots and mults from parameters"""
+ order = self.degree + 1
+ if ncp < order:
+ raise RuntimeError("Number of control points to small!")
+
+ umin = min(parms)
+ umax = max(parms)
+
+ knots = [0] * (ncp - self.degree + 1)
+ mults = [0] * (ncp - self.degree + 1)
+ # debug("computeKnots(ncp, params, knots, mults):\n%s\n%s\n%s\n%s"%(ncp, parms, knots, mults))
+
+ # fill multiplicity at start
+ knots[0] = umin
+ mults[0] = order
+
+ # number of knots between the multiplicities
+ N = ncp - order
+ # set uniform knot distribution
+ for i in range(1, N + 1):
+ knots[i] = umin + (umax - umin) * float(i) / float(N + 1)
+ mults[i] = 1
+
+ # fill multiplicity at end
+ knots[N + 1] = umax
+ mults[N + 1] = order
+
+ for i in self.indexOfKinks:
+ insert_knot(parms[i], self.degree, self.degree, knots, mults, 1e-4)
+
+ # debug("computeKnots(ncp, params, knots, mults):\n%s\n%s\n%s\n%s"%(ncp, parms, knots, mults))
+ return knots, mults
+
+ def maxDistanceOfBoundingBox(self, points: list[Vector]):
+ """return maximum distance of a group of points"""
+ maxDistance = 0.0
+ for i in range(len(points)):
+ for j in range(len(points)):
+ distance = points[i].distanceToPoint(points[j])
+ if maxDistance < distance:
+ maxDistance = distance
+ return maxDistance
+
+ def isClosed(self) -> bool:
+ """Returns True if first and last points are close enough"""
+ if not self.C2Continuous:
+ return False
+ maxDistance = self.maxDistanceOfBoundingBox(self.pnts)
+ error = 1e-12 * maxDistance
+ return self.pnts[0].distanceToPoint(self.pnts[-1]) < error
+
+ def firstAndLastInterpolated(self) -> bool:
+ """Returns True if first and last points must be interpolated"""
+ first = 0 in self.indexOfInterpolated
+ last = (len(self.pnts) - 1) in self.indexOfInterpolated
+ return first and last
+
+ def matrix(self, nrow: int, ncol: int, val: float = 0.0) -> NDArray:
+ """nrow x ncol matrix filled with val"""
+ return np.array([[val] * ncol for i in range(nrow)])
+
+ def getContinuityMatrix(self, nCtrPnts, contin_cons, params, flatKnots):
+ """Additional matrix for continuity conditions on closed curves"""
+ continuity_entries = self.matrix(contin_cons, nCtrPnts)
+ continuity_params1 = [params[0]] # TColStd_Array1OfReal continuity_params1(params[0], 1, 1);
+ continuity_params2 = [params[-1]] # TColStd_Array1OfReal continuity_params2(params[params.size() - 1], 1, 1);
+
+ # bsa = BSplineAlgorithms(1e-6)
+ diff1_1 = bsplineBasisMat(self.degree, flatKnots, continuity_params1, 1)
+ diff1_2 = bsplineBasisMat(self.degree, flatKnots, continuity_params2, 1)
+ # math_Matrix diff1_1 = CTiglBSplineAlgorithms::bsplineBasisMat(m_degree, flatKnots, continuity_params1, 1);
+ # math_Matrix diff1_2 = CTiglBSplineAlgorithms::bsplineBasisMat(m_degree, flatKnots, continuity_params2, 1);
+ diff2_1 = bsplineBasisMat(self.degree, flatKnots, continuity_params1, 2)
+ diff2_2 = bsplineBasisMat(self.degree, flatKnots, continuity_params2, 2)
+ # math_Matrix diff2_1 = CTiglBSplineAlgorithms::bsplineBasisMat(m_degree, flatKnots, continuity_params1, 2);
+ # math_Matrix diff2_2 = CTiglBSplineAlgorithms::bsplineBasisMat(m_degree, flatKnots, continuity_params2, 2);
+
+ # Set C1 condition
+ continuity_entries[0] = diff1_1 - diff1_2
+ # Set C2 condition
+ continuity_entries[1] = diff2_1 - diff2_2
+
+ if not self.firstAndLastInterpolated():
+ diff0_1 = bsplineBasisMat(self.degree, flatKnots, continuity_params1, 0)
+ diff0_2 = bsplineBasisMat(self.degree, flatKnots, continuity_params2, 0)
+ # math_Matrix diff0_1 = CTiglBSplineAlgorithms::bsplineBasisMat(m_degree, flatKnots, continuity_params1, 0);
+ # math_Matrix diff0_2 = CTiglBSplineAlgorithms::bsplineBasisMat(m_degree, flatKnots, continuity_params2, 0);
+ continuity_entries[2] = diff0_1 - diff0_2
+
+ return continuity_entries
+
+ def python_solve(self, params, knots, mults):
+ """Compute the BSpline curve that fits the points
+ Returns the curve, and the max error between points and curve
+ This method is used by iterative function FitCurveOptimal"""
+
+ # debug("python_solve(params, knots, mults):\n%s\n%s\n%s"%(params, knots, mults))
+
+ # TODO knots and mults are OCC arrays (1-based)
+ # TODO I replaced the following OCC objects with numpy arrays:
+ # math_Matrix (Init, Set, Transposed, Multiplied, )
+ # math_Gauss (Solve, IsDone)
+ # math_Vector (Set)
+ # compute flat knots to solve system
+
+ # TODO check code below !!!
+ # nFlatKnots = BSplCLib::KnotSequenceLength(mults, self.degree, False)
+ # TColStd_Array1OfReal flatKnots(1, nFlatKnots)
+ # BSplCLib::KnotSequence(knots, mults, flatKnots)
+ flatKnots = []
+ for i in range(len(knots)):
+ flatKnots += [knots[i]] * mults[i]
+
+ n_apprxmated = len(self.indexOfApproximated)
+ n_intpolated = len(self.indexOfInterpolated)
+ n_continuityConditions = 0
+ if self.isClosed():
+ # C0, C1, C2
+ n_continuityConditions = 3
+ if self.firstAndLastInterpolated():
+ # Remove C0 as they are already equal by design
+ n_continuityConditions -= 1
+ # Number of control points required
+ nCtrPnts = len(flatKnots) - self.degree - 1
+
+ if nCtrPnts < (n_intpolated + n_continuityConditions) or nCtrPnts < (self.degree + 1 + n_continuityConditions):
+ raise RuntimeError("Too few control points for curve interpolation!")
+
+ if n_apprxmated == 0 and not nCtrPnts == (n_intpolated + n_continuityConditions):
+ raise RuntimeError("Wrong number of control points for curve interpolation!")
+
+ # Build left hand side of the equation
+ n_vars = nCtrPnts + n_intpolated + n_continuityConditions
+ lhs = np.array([[0.0] * (n_vars) for i in range(n_vars)])
+ # math_Matrix lhs(1, n_vars, 1, n_vars)
+ # lhs.Init(0.)
+
+ # Allocate right hand side
+ rhsx = np.array([0.0] * n_vars)
+ rhsy = np.array([0.0] * n_vars)
+ rhsz = np.array([0.0] * n_vars)
+ # debug(n_apprxmated)
+ if n_apprxmated > 0:
+ # Write b vector. These are the points to be approximated
+ appParams = np.array([0.0] * n_apprxmated)
+ bx = np.array([0.0] * n_apprxmated)
+ by = np.array([0.0] * n_apprxmated)
+ bz = np.array([0.0] * n_apprxmated)
+ # appIndex = 0
+ for idx in range(len(self.indexOfApproximated)):
+ ioa = self.indexOfApproximated[idx] # + 1
+ p = self.pnts[ioa]
+ bx[idx] = p.x
+ by[idx] = p.y
+ bz[idx] = p.z
+ appParams[idx] = params[ioa]
+ # debug(appParams[idx])
+ # appIndex += 1
+
+ # Solve constrained linear least squares
+ # min(Ax - b) s.t. Cx = d
+ # Create left hand side block matrix
+ # A.T*A C.T
+ # C 0
+ # math_Matrix A = CTiglBSplineAlgorithms::bsplineBasisMat(m_degree, flatKnots, appParams)
+ # math_Matrix At = A.Transposed()
+
+ # bsa = BSplineAlgorithms(1e-6)
+ A = bsplineBasisMat(self.degree, flatKnots, appParams, 0)
+ # debug("self.degree %s"%str(self.degree))
+ # debug("flatKnots %s"%str(flatKnots))
+ # debug("appParams %s"%str(appParams))
+ At = A.T
+ mul = np.matmul(At, A)
+ # debug(lhs)
+ # debug(mul)
+ for i in range(len(mul)):
+ for j in range(len(mul)):
+ lhs[i][j] = mul[i][j]
+ # debug("mul : %s"%str(mul.shape))
+ # debug("rhsx : %s"%(rhsx.shape))
+ # debug("np.matmul(At,bx) : %s"%(np.matmul(At,bx).shape))
+ le = len(np.matmul(At, bx))
+ rhsx[0:le] = np.matmul(At, bx)
+ rhsy[0:le] = np.matmul(At, by)
+ rhsz[0:le] = np.matmul(At, bz)
+ # debug("lhs : %s"%str(lhs))
+ if n_intpolated + n_continuityConditions > 0:
+ # Write d vector. These are the points that should be interpolated as well as the continuity constraints for closed curve
+ # math_Vector dx(1, n_intpolated + n_continuityConditions, 0.)
+ dx = np.array([0.0] * (n_intpolated + n_continuityConditions))
+ dy = np.array([0.0] * (n_intpolated + n_continuityConditions))
+ dz = np.array([0.0] * (n_intpolated + n_continuityConditions))
+ if n_intpolated > 0:
+ interpParams = [0] * n_intpolated
+ # intpIndex = 0
+ # for (std::vector::const_iterator it_idx = m_indexOfInterpolated.begin() it_idx != m_indexOfInterpolated.end() ++it_idx) {
+ # Standard_Integer ipnt = static_cast(*it_idx + 1)
+ for idx in range(len(self.indexOfInterpolated)):
+ ioi = self.indexOfInterpolated[idx] # + 1
+ p = self.pnts[ioi]
+ dx[idx] = p.x
+ dy[idx] = p.y
+ dz[idx] = p.z
+ try:
+ interpParams[idx] = params[ioi]
+ except IndexError:
+ warn(f"IndexError: {ioi}")
+ # intpIndex += 1
+
+ C = bsplineBasisMat(self.degree, flatKnots, interpParams, 0)
+ Ct = C.T
+ # debug("C : %s"%str(C.shape))
+ # debug("Ct : %s"%str(Ct.shape))
+ # debug("lhs : %s"%str(lhs.shape))
+ for i in range(nCtrPnts):
+ for j in range(n_intpolated):
+ lhs[i][j + nCtrPnts] = Ct[i][j]
+ lhs[j + nCtrPnts][i] = C[j][i]
+ # lhs.Set(1, nCtrPnts, nCtrPnts + 1, nCtrPnts + n_intpolated, Ct)
+ # lhs.Set(nCtrPnts + 1, nCtrPnts + n_intpolated, 1, nCtrPnts, C)
+ # debug("lhs : %s"%str(lhs.shape))
+
+ # sets the C2 continuity constraints for closed curves on the left hand side if requested
+ if self.isClosed():
+ continuity_entries = self.getContinuityMatrix(nCtrPnts, n_continuityConditions, params, flatKnots)
+ continuity_entriest = continuity_entries.T
+ for i in range(n_continuityConditions):
+ for j in range(nCtrPnts):
+ lhs[nCtrPnts + n_intpolated + i][j] = continuity_entries[i][j]
+ lhs[j][nCtrPnts + n_intpolated + i] = continuity_entriest[j][i]
+ # lhs.Set(nCtrPnts + n_intpolated + 1, nCtrPnts + n_intpolated + n_continuityConditions, 1, nCtrPnts, continuity_entries)
+ # lhs.Set(1, nCtrPnts, nCtrPnts + n_intpolated + 1, nCtrPnts + n_intpolated + n_continuityConditions, continuity_entries.Transposed())
+
+ rhsx[nCtrPnts : n_vars + 1] = dx
+ rhsy[nCtrPnts : n_vars + 1] = dy
+ rhsz[nCtrPnts : n_vars + 1] = dz
+ # rhsy.Set(nCtrPnts + 1, n_vars, dy)
+ # rhsz.Set(nCtrPnts + 1, n_vars, dz)
+
+ # math_Gauss solver(lhs)
+
+ # math_Vector cp_x(1, n_vars)
+ # math_Vector cp_y(1, n_vars)
+ # math_Vector cp_z(1, n_vars)
+
+ # solver.Solve(rhsx, cp_x)
+ # if (!solver.IsDone()) {
+ # raise RuntimeError("Singular Matrix")
+ # }
+ # debug("lhs : %s"%str(lhs))
+ # debug("rhsx : %s"%str(rhsx))
+ try:
+ cp_x = np.linalg.solve(lhs, rhsx)
+ cp_y = np.linalg.solve(lhs, rhsy)
+ cp_z = np.linalg.solve(lhs, rhsz)
+ except np.linalg.LinAlgError:
+ return None, None
+ poles = [Vector(cp_x[i], cp_y[i], cp_z[i]) for i in range(nCtrPnts)]
+
+ result = Part.BSplineCurve()
+ result.buildFromPolesMultsKnots(poles, mults, knots, False, self.degree)
+
+ # compute error
+ max_error = 0.0
+ for idx in range(len(self.indexOfApproximated)):
+ ioa = self.indexOfApproximated[idx]
+ p = self.pnts[ioa]
+ par = params[ioa]
+ error = result.value(par).distanceToPoint(p)
+ max_error = max(max_error, error)
+ return result, max_error
+
+ def optimizeParameters(self, curve: Part.BSplineCurve, params: list[float]):
+ # /**
+ # * @brief Recalculates the curve parameters t_k after the
+ # * control points are fitted to achieve an even better fit.
+ # */
+ # optimize each parameter by finding it's position on the curve
+ # for (std::vector::const_iterator it_idx = m_indexOfApproximated.begin(); it_idx != m_indexOfApproximated.end(); ++it_idx) {
+ for i in self.indexOfApproximated: # range(len(self.indexOfApproximated)):
+ # parameter, error = self.projectOnCurve(self.pnts[i], curve, params[i])
+ # debug("optimize Parameter %d from %0.4f to %0.4f"%(i,params[i],parameter))
+ # store optimised parameter
+ params[i] = curve.parameter(self.pnts[i]) # parameter
+
+
+# -/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/
+# nurbs_tools
+# -/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/
+
+
+class BsplineBasis:
+ """Computes basis functions of a bspline curve, and its derivatives"""
+
+ def __init__(self):
+ self.knots = [0.0, 0.0, 1.0, 1.0]
+ self.degree = 1
+
+ def find_span(self, u: float) -> int:
+ """Determine the knot span index.
+ - input: parameter u (float)
+ - output: the knot span index (int)
+ Nurbs Book Algo A2.1 p.68
+ """
+ n = len(self.knots) - self.degree - 1
+ if u == self.knots[n + 1]:
+ return n - 1
+ low = self.degree
+ high = n + 1
+ mid = int((low + high) / 2)
+ while u < self.knots[mid] or u >= self.knots[mid + 1]:
+ if u < self.knots[mid]:
+ high = mid
+ else:
+ low = mid
+ mid = int((low + high) / 2)
+ return mid
+
+ def ders_basis_funs(self, i: int, u: float, n: int) -> list[list[float]]:
+ """Compute nonzero basis functions and their derivatives.
+ First section is A2.2 modified to store functions and knot differences.
+ - input: start index i (int), parameter u (float), number of derivatives n (int)
+ - output: basis functions and derivatives ders (array2d of floats)
+ Nurbs Book Algo A2.3 p.72
+ """
+ ders = [[0.0 for x in range(self.degree + 1)] for y in range(n + 1)]
+ ndu = [[1.0 for x in range(self.degree + 1)] for y in range(self.degree + 1)]
+ ndu[0][0] = 1.0
+ left = [0.0]
+ right = [0.0]
+ for j in range(1, self.degree + 1):
+ left.append(u - self.knots[i + 1 - j])
+ right.append(self.knots[i + j] - u)
+ saved = 0.0
+ for r in range(j):
+ ndu[j][r] = right[r + 1] + left[j - r]
+ temp = ndu[r][j - 1] / ndu[j][r]
+ ndu[r][j] = saved + right[r + 1] * temp
+ saved = left[j - r] * temp
+ ndu[j][j] = saved
+
+ for j in range(0, self.degree + 1):
+ ders[0][j] = ndu[j][self.degree]
+ for r in range(0, self.degree + 1):
+ s1 = 0
+ s2 = 1
+ a = [[0.0 for x in range(self.degree + 1)] for y in range(2)]
+ a[0][0] = 1.0
+ for k in range(1, n + 1):
+ d = 0.0
+ rk = r - k
+ pk = self.degree - k
+ if r >= k:
+ a[s2][0] = a[s1][0] / ndu[pk + 1][rk]
+ d = a[s2][0] * ndu[rk][pk]
+ if rk >= -1:
+ j1 = 1
+ else:
+ j1 = -rk
+ if (r - 1) <= pk:
+ j2 = k - 1
+ else:
+ j2 = self.degree - r
+ for j in range(j1, j2 + 1):
+ a[s2][j] = (a[s1][j] - a[s1][j - 1]) / ndu[pk + 1][rk + j]
+ d += a[s2][j] * ndu[rk + j][pk]
+ if r <= pk:
+ a[s2][k] = -a[s1][k - 1] / ndu[pk + 1][r]
+ d += a[s2][k] * ndu[r][pk]
+ ders[k][r] = d
+ j = s1
+ s1 = s2
+ s2 = j
+ r = self.degree
+ for k in range(1, n + 1):
+ for j in range(0, self.degree + 1):
+ ders[k][j] *= r
+ r *= self.degree - k
+ return ders
+
+ def evaluate(self, u: float, d: int) -> list[float]:
+ """Compute the derivative d of the basis functions.
+ - input: parameter u (float), derivative d (int)
+ - output: derivative d of the basis functions (list of floats)
+ """
+ n = len(self.knots) - self.degree - 1
+ f = [0.0 for x in range(n)]
+ span = self.find_span(u)
+ ders = self.ders_basis_funs(span, u, d)
+ for i, val in enumerate(ders[d]):
+ f[span - self.degree + i] = val
+ return f
+
+
+class KnotVector:
+ """Knot vector object to use in Bsplines"""
+
+ def __init__(self, v: Part.BSplineCurve | list[float] = None):
+ if isinstance(v, Part.BSplineCurve):
+ self.vector = v.getKnots()
+ elif v is None:
+ self.vector = [0.0, 1.0]
+ else:
+ self.vector = v
+
+ def __repr__(self):
+ return "KnotVector({})".format(str(self._vector))
+
+ @property
+ def vector(self):
+ return self._vector
+
+ @vector.setter
+ def vector(self, v):
+ self._vector = v
+ self._vector.sort()
+ self._min_max()
+
+ @property
+ def knots(self):
+ """Get the list of unique knots, without duplicates"""
+ return list(set(self._vector))
+
+ @property
+ def mults(self):
+ """Get the list of multiplicities of the knot vector"""
+ no_duplicates = self.knots
+ return [self._vector.count(k) for k in no_duplicates]
+
+ def _min_max(self):
+ """Compute the min and max values of the knot vector"""
+ self.maxi = max(self._vector)
+ self.mini = min(self._vector)
+
+ def reverse(self) -> list[float]:
+ """Reverse the knot vector"""
+ newknots = [(self.maxi + self.mini - k) for k in self._vector]
+ newknots.reverse()
+ self._vector = newknots
+ return self._vector
+
+ def scale(self, length=1.0) -> list[float]:
+ """Scales the knot vector to a [0.0, length]"""
+ if length <= 0.0:
+ raise ValueError(f"scale error : bad value = {length}")
+
+ ran = self.maxi - self.mini
+ newknots = [length * (k - self.mini) / ran for k in self._vector]
+ self._vector = newknots
+ self._min_max()
+ return self._vector
+
+
+# ---------------------------------------------------
+
+
+def bspline_copy(bs: Part.BSplineCurve, reverse: bool = False, scale: float = 1.0) -> Part.BSplineCurve:
+ """Copy a BSplineCurve, with knotvector optionally reversed and scaled
+ newbspline = bspline_copy(bspline, reverse = False, scale = 1.0)"""
+ mults = bs.getMultiplicities()
+ weights = bs.getWeights()
+ poles = bs.getPoles()
+ knots = KnotVector(bs)
+ perio = bs.isPeriodic()
+ ratio = bs.isRational()
+ if scale:
+ knots.scale(scale)
+ if reverse:
+ mults.reverse()
+ weights.reverse()
+ poles.reverse()
+ knots.reverse()
+ bspline = Part.BSplineCurve()
+ bspline.buildFromPolesMultsKnots(poles, mults, knots.vector, perio, bs.Degree, weights, ratio)
+ return bspline
+
+
+# -/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/
+# curve_network_sorter
+# -/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/
+
+
+def maxRowIndex(m, irow):
+ """returns the column index of the maximum of i-th row"""
+ maxi = -1e50
+ jmax = 0
+ for jcol in range(len(m[0])):
+ if m[irow][jcol] > maxi:
+ maxi = m[irow][jcol]
+ jmax = jcol
+ return jmax
+
+
+def maxColIndex(m, jcol):
+ """returns the row index of the maximum of i-th col"""
+ maxi = -1e50
+ imax = 0
+ for irow in range(len(m)):
+ if m[irow][jcol] > maxi:
+ maxi = m[irow][jcol]
+ imax = irow
+ return imax
+
+
+def minRowIndex(m, irow):
+ """returns the column index of the minimum of i-th row"""
+ mini = 1e50
+ jmin = 0
+ for jcol in range(len(m[0])):
+ if m[irow][jcol] < mini:
+ mini = m[irow][jcol]
+ jmin = jcol
+ return jmin
+
+
+def minColIndex(m, jcol):
+ """returns the row index of the minimum of i-th col"""
+ mini = 1e50
+ imin = 0
+ for irow in range(len(m)):
+ if m[irow][jcol] < mini:
+ mini = m[irow][jcol]
+ imin = irow
+ return imin
+
+
+def swap(o, i, j):
+ """swap o[i] and o[j]"""
+ o[i], o[j] = o[j], o[i]
+
+
+def swap_row(o, i, j):
+ """swap rows i and j of 2d array o"""
+ o[i], o[j] = o[j], o[i]
+
+
+def swap_col(o, i, j):
+ """swap cols i and j of 2d array o"""
+ for row in o:
+ row[i], row[j] = row[j], row[i]
+
+
+class CurveNetworkSorter(object):
+ def __init__(
+ self,
+ profiles: list[Part.BSplineCurve],
+ guides: list[Part.BSplineCurve],
+ parmsIntersProfiles: list[list[float]],
+ parmsIntersGuides: list[list[float]],
+ ):
+ self.has_performed = False
+ if (len(profiles) < 2) or (len(guides) < 2):
+ raise ValueError("Not enough guides or profiles")
+ else:
+ self.profiles = profiles
+ self.guides = guides
+ self.n_profiles = len(profiles)
+ self.n_guides = len(guides)
+ self.parmsIntersProfiles = parmsIntersProfiles
+ self.parmsIntersGuides = parmsIntersGuides
+ if not self.n_profiles == len(self.parmsIntersProfiles):
+ raise ValueError("Invalid row size of parmsIntersProfiles matrix.")
+ if not self.n_profiles == len(self.parmsIntersGuides):
+ raise ValueError("Invalid row size of parmsIntersGuides matrix.")
+ if not self.n_guides == len(self.parmsIntersProfiles[0]):
+ raise ValueError("Invalid col size of parmsIntersProfiles matrix.")
+ if not self.n_guides == len(self.parmsIntersGuides[0]):
+ raise ValueError("Invalid col size of parmsIntersGuides matrix.")
+ # ????
+ # assert(m_parmsIntersGuides.UpperRow() == n_profiles - 1);
+ # assert(m_parmsIntersProfiles.UpperRow() == n_profiles - 1);
+ # assert(m_parmsIntersGuides.UpperCol() == n_guides - 1);
+ # assert(m_parmsIntersProfiles.UpperCol() == n_guides - 1);
+ self.profIdx = [str(i) for i in range(self.n_profiles)]
+ self.guidIdx = [str(i) for i in range(self.n_guides)]
+
+ def swapProfiles(self, idx1, idx2):
+ if idx1 == idx2:
+ return
+ swap(self.profiles, idx1, idx2)
+ swap(self.profIdx, idx1, idx2)
+ swap_row(self.parmsIntersGuides, idx1, idx2)
+ swap_row(self.parmsIntersProfiles, idx1, idx2)
+
+ def swapGuides(self, idx1, idx2):
+ if idx1 == idx2:
+ return
+ swap(self.guides, idx1, idx2)
+ swap(self.guidIdx, idx1, idx2)
+ swap_col(self.parmsIntersGuides, idx1, idx2)
+ swap_col(self.parmsIntersProfiles, idx1, idx2)
+
+ def GetStartCurveIndices(self): # prof_idx, guid_idx, guideMustBeReversed):
+ """find curves, that begin at the same point (have the smallest parameter at their intersection)"""
+ for irow in range(len(self.profiles)):
+ jmin = minRowIndex(self.parmsIntersProfiles, irow)
+ imin = minColIndex(self.parmsIntersGuides, jmin)
+ if imin == irow:
+ # we found the start curves
+ # prof_idx = imin
+ # guid_idx = jmin
+ # guideMustBeReversed = False
+ return imin, jmin, False
+ # there are situation (a loop) when the previous situation does not exist
+ # find curves were the start of a profile hits the end of a guide
+ for irow in range(len(self.profiles)):
+ jmin = minRowIndex(self.parmsIntersProfiles, irow)
+ imax = maxColIndex(self.parmsIntersGuides, jmin)
+ if imax == irow:
+ # we found the start curves
+ # prof_idx = imax
+ # guid_idx = jmin
+ # guideMustBeReversed = True
+ return imax, jmin, True
+ # we have not found the starting curve. The network seems invalid
+ raise RuntimeError("Cannot find starting curves of curve network.")
+
+ def Perform(self):
+ if self.has_performed:
+ return
+
+ prof_start = 0
+ guide_start = 0
+ nGuid = len(self.guides)
+ nProf = len(self.profiles)
+
+ guideMustBeReversed = False
+ prof_start, guide_start, guideMustBeReversed = self.GetStartCurveIndices()
+
+ # put start curves first in array
+ self.swapProfiles(0, prof_start)
+ self.swapGuides(0, guide_start)
+
+ if guideMustBeReversed:
+ self.reverseGuide(0)
+
+ # perform a bubble sort for the guides,
+ # such that the guides intersection of the first profile are ascending
+ r = list(range(2, nGuid + 1))
+ r.reverse()
+ for n in r: # (int n = nGuid; n > 1; n = n - 1) {
+ for j in range(1, n - 1): # (int j = 1; j < n - 1; ++j) {
+ if self.parmsIntersProfiles[0][j] > self.parmsIntersProfiles[0][j + 1]:
+ self.swapGuides(j, j + 1)
+ # perform a bubble sort of the profiles,
+ # such that the profiles are in ascending order of the first guide
+ r = list(range(2, nProf + 1))
+ r.reverse()
+ for n in r: # (int n = nProf; n > 1; n = n - 1) {
+ for i in range(1, n - 1): # (int i = 1; i < n - 1; ++i) {
+ if self.parmsIntersGuides[i][0] > self.parmsIntersGuides[i + 1][0]:
+ self.swapProfiles(i, i + 1)
+
+ # reverse profiles, if necessary
+ for iProf in range(1, nProf): # (Standard_Integer iProf = 1; iProf < nProf; ++iProf) {
+ if self.parmsIntersProfiles[iProf][0] > self.parmsIntersProfiles[iProf][nGuid - 1]:
+ self.reverseProfile(iProf)
+ # reverse guide, if necessary
+ for iGuid in range(1, nGuid): # (Standard_Integer iGuid = 1; iGuid < nGuid; ++iGuid) {
+ if self.parmsIntersGuides[0][iGuid] > self.parmsIntersGuides[nProf - 1][iGuid]:
+ self.reverseGuide(iGuid)
+ self.has_performed = True
+
+ def reverseProfile(self, profileIdx):
+ pIdx = int(profileIdx)
+ profile = self.profiles[profileIdx]
+ if profile is not None: # .IsNull()
+ firstParm = profile.FirstParameter
+ lastParm = profile.LastParameter
+ else:
+ firstParm = self.parmsIntersProfiles[pIdx][int(minRowIndex(self.parmsIntersProfiles, pIdx))]
+ lastParm = self.parmsIntersProfiles[pIdx][int(maxRowIndex(self.parmsIntersProfiles, pIdx))]
+ # compute new parameters
+ for icol in range(len(self.guides)): # (int icol = 0; icol < static_cast(NGuides()); ++icol) {
+ self.parmsIntersProfiles[pIdx][icol] = -self.parmsIntersProfiles[pIdx][icol] + firstParm + lastParm
+ if profile is not None: # .IsNull()
+ profile = bspline_copy(profile, reverse=True, scale=1.0)
+ self.profiles[profileIdx] = profile
+ self.profIdx[profileIdx] = "-" + self.profIdx[profileIdx]
+
+ def reverseGuide(self, guideIdx):
+ gIdx = int(guideIdx)
+ guide = self.guides[guideIdx]
+ if guide is not None: # .IsNull()
+ firstParm = guide.FirstParameter
+ lastParm = guide.LastParameter
+ else:
+ firstParm = self.parmsIntersGuides[int(minColIndex(self.parmsIntersGuides, gIdx))][gIdx]
+ lastParm = self.parmsIntersGuides[int(maxColIndex(self.parmsIntersGuides, gIdx))][gIdx]
+ # compute new parameters
+ for irow in range(len(self.profiles)):
+ self.parmsIntersGuides[irow][gIdx] = -self.parmsIntersGuides[irow][gIdx] + firstParm + lastParm
+ if guide is not None: # .IsNull()
+ guide = bspline_copy(guide, reverse=True, scale=1.0)
+ self.guides[guideIdx] = guide
+ self.guidIdx[guideIdx] = "-" + self.guidIdx[guideIdx]
+
+
+# -/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/
+# BSplineAlgorithms
+# -/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/
+
+
+def IsInsideTolerance(array, value, tolerance=1e-15):
+ """Return index of value in array, within given tolerance
+ Else return -1"""
+ for i in range(len(array)):
+ if abs(array[i] - value) <= tolerance:
+ return i
+ return -1
+
+
+def LinspaceWithBreaks(umin, umax, n_values, breaks):
+ """Returns a knot sequence of n_values between umin and umax
+ that will also contain the breaks"""
+ du = float(umax - umin) / (n_values - 1)
+ result = list() # size = n_values
+ for i in range(n_values):
+ result.append(i * du + umin)
+ # now insert the break
+
+ eps = 0.3
+ # remove points, that are closer to each break point than du*eps
+ for break_point in breaks:
+ pos = IsInsideTolerance(
+ result, break_point, du * eps
+ ) # std::find_if(result.begin(), result.end(), IsInsideTolerance(breakpoint, du*eps));
+ if pos >= 0:
+ # point found, replace it
+ result[pos] = break_point
+ else:
+ # find closest element
+ pos = IsInsideTolerance(result, break_point, (0.5 + 1e-8) * du)
+ if result[pos] > break_point:
+ result.insert(pos, break_point)
+ else:
+ result.insert(pos + 1, break_point)
+ return result
+
+
+class SurfAdapterViewError(Exception):
+ pass
+
+
+class SurfAdapterView:
+ def __init__(self, surf: Part.BSplineSurface, direc: int):
+ self.s = surf
+ self.d = direc
+
+ @property
+ def NbKnots(self):
+ return self.getNKnots()
+
+ @property
+ def NbPoles(self):
+ return self.getNPoles()
+
+ @property
+ def Degree(self):
+ return self.getDegree()
+
+ def insertKnot(self, knot, mult, tolerance=1e-15):
+ try:
+ if self.d == 0:
+ self.s.insertUKnot(knot, mult, tolerance)
+ else:
+ self.s.insertVKnot(knot, mult, tolerance)
+ except Part.OCCError as err:
+ raise SurfAdapterViewError(f"Failed to insert knot : {knot} - {mult} - {tolerance}") from err
+
+ def getKnot(self, idx):
+ if self.d == 0:
+ return self.s.getUKnot(idx)
+ else:
+ return self.s.getVKnot(idx)
+
+ def getKnots(self):
+ if self.d == 0:
+ return self.s.getUKnots()
+ else:
+ return self.s.getVKnots()
+
+ def getMultiplicities(self):
+ if self.d == 0:
+ return self.s.getUMultiplicities()
+ else:
+ return self.s.getVMultiplicities()
+
+ def increaseMultiplicity(self, idx, mult):
+ if self.d == 0:
+ return self.s.increaseUMultiplicity(idx, mult)
+ else:
+ return self.s.increaseVMultiplicity(idx, mult)
+
+ def getMult(self, idx):
+ if self.d == 0:
+ return self.s.getUMultiplicity(idx)
+ else:
+ return self.s.getVMultiplicity(idx)
+
+ def getMultiplicity(self, idx):
+ return self.getMult(idx)
+
+ def getNKnots(self):
+ if self.d == 0:
+ return self.s.NbUKnots
+ else:
+ return self.s.NbVKnots
+
+ def getNPoles(self):
+ if self.d == 0:
+ return self.s.NbUPoles
+ else:
+ return self.s.NbVPoles
+
+ def getDegree(self):
+ if self.d == 0:
+ return int(self.s.UDegree)
+ else:
+ return int(self.s.VDegree)
+
+ def isPeriodic(self):
+ if self.d == 0:
+ return self.s.isUPeriodic()
+ else:
+ return self.s.isVPeriodic()
+
+
+class BSplineAlgorithms(object):
+ """Various BSpline algorithms"""
+
+ def __init__(self, tol: float = 1e-8):
+ self.REL_TOL_CLOSED = tol
+ if tol > 0.0:
+ self.tol = tol # parametric tolerance
+
+ def scale(self, c):
+ """Returns the max size of a curve (or list of curves) poles"""
+ res = 0
+ if isinstance(c, (tuple, list)):
+ for cu in c:
+ res = max(res, self.scale(cu))
+ elif isinstance(c, (Part.BSplineCurve, Part.BezierCurve)):
+ pts = c.getPoles()
+ for p in pts[1:]:
+ res = max(res, p.distanceToPoint(pts[0]))
+ return res
+
+ def scale_pt_array(self, points: list[Vector]):
+ """Returns the max distance of a 2D array of points"""
+ theScale = 0.0
+ for uidx in range(len(points)):
+ pFirst = points[uidx][0]
+ for vidx in range(1, len(points[0])):
+ dist = pFirst.distanceToPoint(points[uidx][vidx])
+ theScale = max(theScale, dist)
+ return theScale
+
+ def isUDirClosed(self, points, tolerance):
+ """check that first row and last row of a 2D array of points are the same"""
+ uDirClosed = True
+ for v_idx in range(len(points[0])):
+ uDirClosed = uDirClosed and (points[0][v_idx].distanceToPoint(points[-1][v_idx]) < tolerance)
+ return uDirClosed
+
+ def isVDirClosed(self, points, tolerance):
+ """check that first column and last column of a 2D array of points are the same"""
+ vDirClosed = True
+ for u_idx in range(len(points)):
+ vDirClosed = vDirClosed and (points[u_idx][0].distanceToPoint(points[u_idx][-1]) < tolerance)
+ return vDirClosed
+
+ def matchDegree(self, curves):
+ """Match degree of all curves by increasing degree where needed"""
+ maxDegree = 0
+ for bs in curves:
+ curDegree = bs.Degree
+ if curDegree > maxDegree:
+ maxDegree = curDegree
+ for bs in curves:
+ curDegree = bs.Degree
+ if curDegree < maxDegree:
+ bs.increaseDegree(maxDegree)
+
+ def flipSurface(self, surf: Part.BSplineSurface) -> Part.BSplineSurface:
+ """Flip U/V parameters of a surface"""
+ result = surf.copy()
+ result.exchangeUV()
+ return result
+
+ def haveSameRange(self, splines_vector, par_tolerance):
+ """Check that all curves have the same parameter range"""
+ begin_param_dir = splines_vector[0].getKnot(1)
+ end_param_dir = splines_vector[0].getKnot(splines_vector[0].NbKnots)
+ for spline_idx in range(
+ 1, len(splines_vector)
+ ): # (unsigned int spline_idx = 1; spline_idx < splines_vector.size(); ++spline_idx) {
+ curSpline = splines_vector[spline_idx]
+ begin_param_dir_surface = curSpline.getKnot(1)
+ end_param_dir_surface = curSpline.getKnot(curSpline.NbKnots)
+ if (
+ abs(begin_param_dir_surface - begin_param_dir) > par_tolerance
+ or abs(end_param_dir_surface - end_param_dir) > par_tolerance
+ ):
+ return False
+ return True
+
+ def haveSameDegree(self, splines):
+ """Check that all curves have the same degree"""
+ degree = splines[0].Degree
+ for splineIdx in range(
+ 1, len(splines)
+ ): # (unsigned int splineIdx = 1; splineIdx < splines.size(); ++splineIdx) {
+ if not splines[splineIdx].Degree == degree:
+ return False
+ return True
+
+ def findKnot(self, spline, knot, tolerance=1e-15):
+ """Return index of knot in spline, within given tolerance
+ Else return -1"""
+ for curSplineKnotIdx in range(
+ spline.NbKnots
+ ): # (int curSplineKnotIdx = 1; curSplineKnotIdx <= spline.getNKnots(); ++curSplineKnotIdx) {
+ if abs(spline.getKnot(curSplineKnotIdx + 1) - knot) < tolerance:
+ return curSplineKnotIdx
+ return -1
+
+ def clampBSpline(self, curve):
+ """If curve is periodic, it is trimmed to First / Last Parameters"""
+ if not curve.isPeriodic():
+ return
+ # curve.setNotPeriodic()
+ curve.trim(curve.FirstParameter, curve.LastParameter)
+ # TODO is the code below really needed in FreCAD ?
+ # Handle(Geom_Curve) c = new Geom_TrimmedCurve(curve, curve->FirstParameter(), curve->LastParameter());
+ # curve = GeomConvert::CurveToBSplineCurve(c);
+
+ def makeGeometryCompatibleImpl(self, splines_vector, par_tolerance):
+ """Modify all the splines, so that they have the same knots / mults"""
+ # all B-spline splines must have the same parameter range in the chosen direction
+ if not self.haveSameRange(splines_vector, par_tolerance):
+ self.error(
+ "B-splines don't have the same parameter range at least in one direction (u / v) in method createCommonKnotsVectorImpl!"
+ )
+ # all B-spline splines must have the same degree in the chosen direction
+ if not self.haveSameDegree(splines_vector):
+ self.error(
+ "B-splines don't have the same degree at least in one direction (u / v) in method createCommonKnotsVectorImpl!"
+ )
+
+ # # The parametric tolerance must be smaller than half of the minimum knot distance
+ # for spline in splines_vector:
+ # for idx in range(spline.NbKnots - 1):
+ # knot_dist = spline.getKnot(idx + 2) - spline.getKnot(idx + 1)
+ # par_tolerance = min(par_tolerance, knot_dist / 2.0)
+ #
+ # # insert all knots in first spline
+ # firstSpline = splines_vector[0]
+ # for spline in splines_vector[1:]:
+ # for knot_idx in range(1, spline.NbKnots + 1):
+ # knot = spline.getKnot(knot_idx)
+ # mult = spline.getMultiplicity(knot_idx)
+ # firstSpline.insertKnot(knot, mult, par_tolerance)
+ #
+ # # now insert knots from first into all others
+ # for spline in splines_vector[1:]:
+ # for knot_idx in range(1, firstSpline.NbKnots + 1):
+ # knot = firstSpline.getKnot(knot_idx)
+ # mult = firstSpline.getMultiplicity(knot_idx)
+ # spline.insertKnot(knot, mult, par_tolerance)
+ # if not (spline.NbKnots == firstSpline.NbKnots):
+ # self.error("Unexpected error in Algorithm makeGeometryCompatibleImpl.\nPlease contact the developers.")
+
+ # create a vector of all knots in chosen direction (u or v) of all splines
+ resultKnots = list()
+ for spline in splines_vector:
+ for k in spline.getKnots():
+ resultKnots.append(k)
+
+ # sort vector of all knots in given direction of all splines
+ resultKnots.sort()
+ prev = resultKnots[0]
+ unique = [prev]
+ for i in range(1, len(resultKnots)):
+ if abs(resultKnots[i] - prev) > par_tolerance:
+ unique.append(resultKnots[i])
+ prev = resultKnots[i]
+ resultKnots = unique
+
+ # find highest multiplicities
+ resultMults = [0] * len(resultKnots)
+ for spline in splines_vector:
+ for knotIdx in range(len(resultKnots)):
+ # get multiplicity of current knot in surface
+ splKnotIdx = self.findKnot(spline, resultKnots[knotIdx], par_tolerance)
+ if splKnotIdx > -1:
+ resultMults[knotIdx] = max(resultMults[knotIdx], spline.getMultiplicity(splKnotIdx + 1))
+
+ for spline in splines_vector:
+ # debug("\n%d - %d poles\n%s\n%s"%(spline.Degree, spline.NbPoles, spline.getKnots(), spline.getMultiplicities()))
+ for knotIdx in range(len(resultKnots)):
+ # get multiplicity of current knot in surface
+ splKnotIdx = self.findKnot(spline, resultKnots[knotIdx], par_tolerance)
+ if splKnotIdx > -1:
+ # print("getting mult {} / {}, periodic = {}".format(splKnotIdx + 1, len(spline.getMultiplicities()), spline.isPeriodic()))
+ if int(spline.getMultiplicity(splKnotIdx + 1)) < resultMults[knotIdx]:
+ # debug("increasing mult %f / %d"%(resultKnots[knotIdx], resultMults[knotIdx]))
+ spline.increaseMultiplicity(splKnotIdx + 1, resultMults[knotIdx])
+ else:
+ # debug("inserting knot %f / %d"%(resultKnots[knotIdx], resultMults[knotIdx]))
+ spline.insertKnot(resultKnots[knotIdx], resultMults[knotIdx], par_tolerance)
+
+ def createCommonKnotsVectorCurve(self, curves, tol):
+ """Modify all the splines, so that they have the same knots / mults"""
+ # TODO: Match parameter range
+ # Create a copy that we can modify
+ splines_adapter = [c.copy() for c in curves]
+ self.makeGeometryCompatibleImpl(splines_adapter, tol)
+ return splines_adapter
+
+ def createCommonKnotsVectorSurface(
+ self,
+ old_surfaces_vector: list[Part.BSplineSurface],
+ tol: float,
+ ) -> list[Part.BSplineSurface]:
+ """Make all the surfaces have the same knots / mults"""
+ # all B-spline surfaces must have the same parameter range in u- and v-direction
+ # TODO: Match parameter range
+
+ # Create a copy that we can modify
+ adapterSplines = list()
+ for i in range(len(old_surfaces_vector)):
+ # debug(old_surfaces_vector[i])
+ adapterSplines.append(SurfAdapterView(old_surfaces_vector[i].copy(), 0))
+ # first in u direction
+ self.makeGeometryCompatibleImpl(adapterSplines, tol)
+
+ for i in range(len(old_surfaces_vector)):
+ adapterSplines[i].d = 1
+
+ # now in v direction
+ self.makeGeometryCompatibleImpl(adapterSplines, tol)
+
+ return [ads.s for ads in adapterSplines]
+
+ def reparametrize_bspline(
+ self,
+ spline: Part.BSplineCurve,
+ umin: float,
+ umax: float,
+ tol: float,
+ ) -> None:
+ """reparametrize BSpline to range [umin, umax]"""
+ knots = spline.getKnots()
+ ma = knots[-1]
+ mi = knots[0]
+ if abs(mi - umin) > tol or abs(ma - umax) > tol:
+ ran = ma - mi
+ # fix from edwilliams16
+ # https://forum.freecadweb.org/viewtopic.php?f=22&t=75293&p=653658#p653658
+ fracknots = [(k - mi) / ran for k in knots]
+ newknots = [umin * (1 - f) + umax * f for f in fracknots]
+ spline.setKnots(newknots)
+
+ def getKinkParameters(self, curve):
+ """Returns a list of knots of sharp points in curve"""
+ if not curve:
+ raise ValueError("Null Pointer curve")
+
+ eps = self.tol
+
+ kinks = list()
+ for knotIndex in range(2, curve.NbKnots):
+ if curve.getMultiplicity(knotIndex) == curve.Degree:
+ knot = curve.getKnot(knotIndex)
+ # check if really a kink
+ angle = curve.tangent(knot + eps)[0].getAngle(curve.tangent(knot - eps)[0])
+ if angle > 6.0 / 180.0 * pi:
+ kinks.append(knot)
+ return kinks
+
+ # Below are the most important methods of BSplineAlgorithms
+
+ def intersections(self, spline1, spline2, tol3d):
+ """Returns a list of tuples (param1, param2) that are intersection parameters of spline1 with spline2"""
+ # light weight simple minimizer
+ # check parametrization of B-splines beforehand
+ # find out the average scale of the two B-splines in order to being able to handle a more approximate curves and find its intersections
+ splines_scale = (self.scale(spline1) + self.scale(spline2)) / 2.0
+ intersection_params_vector = []
+ inters = spline1.intersectCC(spline2)
+ # GeomAPI_ExtremaCurveCurve intersectionObj(spline1, spline2);
+ # debug("intersectCC results")
+ if len(inters) >= 2:
+ p1 = Vector(inters[0].X, inters[0].Y, inters[0].Z)
+ p2 = Vector(inters[1].X, inters[1].Y, inters[1].Z)
+ if p1.distanceToPoint(p2) < tol3d * splines_scale:
+ inters = [p1]
+ for intpt in inters:
+ if isinstance(intpt, Part.Point):
+ inter = Vector(intpt.X, intpt.Y, intpt.Z)
+ else:
+ inter = intpt
+ # debug(intpt)
+ param1 = spline1.parameter(inter)
+ param2 = spline2.parameter(inter)
+ # intersectionObj.Parameters(intersect_idx, param1, param2);
+ # filter out real intersections
+ point1 = spline1.value(param1)
+ point2 = spline2.value(param2)
+ if point1.distanceToPoint(point2) < tol3d * splines_scale:
+ intersection_params_vector.append([param1, param2])
+
+ # for closed B-splines:
+ if len(inters) == 1:
+ if spline1.isClosed():
+ if abs(param1 - spline1.getKnot(1)) < self.tol:
+ # GeomAPI_ExtremaCurveCurve doesn't find second intersection point at the end of the closed curve, so add it by hand
+ intersection_params_vector.append([spline1.getKnot(spline1.NbKnots), param2])
+ if abs(param1 - spline1.getKnot(spline1.NbKnots)) < self.tol:
+ # GeomAPI_ExtremaCurveCurve doesn't find second intersection point at the beginning of the closed curve, so add it by hand
+ intersection_params_vector.append([spline1.getKnot(1), param2])
+ elif spline2.isClosed():
+ if abs(param2 - spline2.getKnot(1)) < self.tol:
+ # GeomAPI_ExtremaCurveCurve doesn't find second intersection point at the end of the closed curve, so add it by hand
+ intersection_params_vector.append([param1, spline2.getKnot(spline2.NbKnots)])
+ if abs(param2 - spline2.getKnot(spline2.NbKnots)) < self.tol:
+ # GeomAPI_ExtremaCurveCurve doesn't find second intersection point at the beginning of the closed curve, so add it by hand
+ intersection_params_vector.append([param1, spline2.getKnot(1)])
+
+ if len(inters) == 0:
+ e1 = spline1.toShape()
+ e2 = spline2.toShape()
+ d, pts, info = e1.distToShape(e2)
+ if d > tol3d * splines_scale:
+ warn("distToShape over tolerance ! %f > %f" % (d, tol3d * splines_scale))
+ p1, p2 = pts[0]
+ intersection_params_vector.append([spline1.parameter(p1), spline2.parameter(p2)])
+ return intersection_params_vector
+
+ def curvesToSurface(self, curves, vParameters, continuousIfClosed) -> Part.BSplineSurface:
+ """Returns a surface that skins the list of curves"""
+ # check amount of given parameters
+ if not len(vParameters) == len(curves):
+ raise ValueError("The amount of given parameters has to be equal to the amount of given B-splines!")
+
+ # check if all curves are closed
+ tolerance = self.scale(curves) * self.REL_TOL_CLOSED
+ makeClosed = continuousIfClosed # and curves[0].toShape().isPartner(curves[-1].toShape())
+
+ self.matchDegree(curves)
+ nCurves = len(curves)
+
+ # create a common knot vector for all splines
+ compatSplines = self.createCommonKnotsVectorCurve(curves, tolerance)
+
+ firstCurve = compatSplines[0]
+ numControlPointsU = firstCurve.NbPoles
+
+ degreeV = 0
+ degreeU = firstCurve.Degree
+ knotsV = list()
+ multsV = list()
+
+ if makeClosed:
+ nPointsAdapt = nCurves - 1
+ else:
+ nPointsAdapt = nCurves
+
+ # create matrix of new control points with size which is possibly DIFFERENT from the size of controlPoints
+ cpSurf = list()
+ interpPointsVDir = [0] * nPointsAdapt
+
+ # now continue to create new control points by interpolating the remaining columns of controlPoints
+ # in Skinning direction (here v-direction) by B-splines
+ for cpUIdx in range(numControlPointsU): # (int cpUIdx = 1; cpUIdx <= numControlPointsU; ++cpUIdx) {
+ for cpVIdx in range(nPointsAdapt): # (int cpVIdx = 1; cpVIdx <= nPointsAdapt; ++cpVIdx) {
+ # print("%dx%d - %d"%(cpUIdx, cpVIdx, compatSplines[cpVIdx].NbPoles))
+ interpPointsVDir[cpVIdx] = compatSplines[cpVIdx].getPole(cpUIdx + 1)
+ interpSpline = Part.BSplineCurve()
+ try:
+ interpSpline.interpolate(
+ Points=interpPointsVDir,
+ Parameters=vParameters,
+ PeriodicFlag=makeClosed,
+ Tolerance=tolerance,
+ )
+ except Part.OCCError:
+ print("interpSpline creation failed")
+ print("%d points" % len(interpPointsVDir))
+ for p in interpPointsVDir:
+ print("%0.4f %0.4f %0.4f" % (p.x, p.y, p.z))
+ print("%d parameters" % len(vParameters))
+ for p in vParameters:
+ print("%0.4f" % p)
+ # print(vParameters)
+ print("Closed : %s" % makeClosed)
+
+ # debug(interpSpline)
+ if makeClosed:
+ self.clampBSpline(interpSpline)
+ # debug(interpSpline)
+
+ if cpUIdx == 0:
+ degreeV = interpSpline.Degree
+ knotsV = interpSpline.getKnots()
+ multsV = interpSpline.getMultiplicities()
+ cpSurf = [[0] * interpSpline.NbPoles for i in range(numControlPointsU)]
+ # new TColgp_HArray2OfPnt(1, static_cast(numControlPointsU), 1, interpSpline->NbPoles());
+
+ # the final surface control points are the control points resulting from
+ # the interpolation
+ for i in range(interpSpline.NbPoles): # for (int i = cpSurf->LowerCol(); i <= cpSurf->UpperCol(); ++i) {
+ cpSurf[cpUIdx][i] = interpSpline.getPole(i + 1)
+
+ # check degree always the same
+ assert degreeV == interpSpline.Degree
+
+ knotsU = firstCurve.getKnots()
+ multsU = firstCurve.getMultiplicities()
+
+ # makeClosed = False
+
+ skinnedSurface = Part.BSplineSurface()
+ skinnedSurface.buildFromPolesMultsKnots(
+ cpSurf,
+ multsU,
+ multsV,
+ knotsU,
+ knotsV,
+ firstCurve.isPeriodic(),
+ makeClosed,
+ degreeU,
+ degreeV,
+ )
+
+ return skinnedSurface
+
+ def pointsToSurface(
+ self,
+ points,
+ uParams,
+ vParams,
+ uContinuousIfClosed,
+ vContinuousIfClosed,
+ ) -> Part.BSplineSurface:
+ """Returns a surface that skins the 2D array of points"""
+ # debug("- pointsToSurface")
+ tolerance = self.REL_TOL_CLOSED * self.scale_pt_array(points)
+ makeVDirClosed = vContinuousIfClosed and self.isVDirClosed(points, tolerance)
+ makeUDirClosed = uContinuousIfClosed and self.isUDirClosed(points, tolerance)
+
+ # GeomAPI_Interpolate does not want to have the last point,
+ # if the curve should be closed. It internally uses the first point
+ # as the last point
+ if makeUDirClosed:
+ nPointsUpper = len(points) - 1
+ else:
+ nPointsUpper = len(points) # points.UpperRow()
+
+ # first interpolate all points by B-splines in u-direction
+ uSplines = list()
+ for cpVIdx in range(
+ len(points[0])
+ ): # for (int cpVIdx = points.LowerCol(); cpVIdx <= points.UpperCol(); ++cpVIdx) {
+ points_u = [0] * nPointsUpper
+ for iPointU in range(
+ nPointsUpper
+ ): # for (int iPointU = points_u->Lower(); iPointU <= points_u->Upper(); ++iPointU) {
+ points_u[iPointU] = points[iPointU][cpVIdx]
+ curve = Part.BSplineCurve()
+ curve.interpolate(
+ Points=points_u,
+ Parameters=uParams,
+ PeriodicFlag=makeUDirClosed,
+ Tolerance=tolerance,
+ )
+
+ if makeUDirClosed:
+ self.clampBSpline(curve)
+ uSplines.append(curve)
+
+ # now create a skinned surface with these B-splines which represents the interpolating surface
+ interpolatingSurf = self.curvesToSurface(uSplines, vParams, makeVDirClosed)
+ return interpolatingSurf
+
+ def reparametrizeBSplineContinuouslyApprox(
+ self, spline: Part.BSplineCurve, old_parameters, new_parameters, n_control_pnts
+ ):
+ """Approximate spline while moving old_parameters to new_parameters"""
+ if not len(old_parameters) == len(new_parameters):
+ self.error("parameter sizes don't match")
+
+ # create a B-spline as a function for reparametrization
+ old_parameters_pnts = [Vector2d(old_parameters[i], 0) for i in range(len(old_parameters))]
+
+ reparametrizing_spline = Part.Geom2d.BSplineCurve2d()
+ try:
+ reparametrizing_spline.interpolate(
+ Points=old_parameters_pnts,
+ Parameters=new_parameters,
+ PeriodicFlag=False,
+ Tolerance=self.tol,
+ )
+ except Exception:
+ self.error("reparametrizing_spline failed")
+ self.error("nb_pts = %d" % (len(old_parameters_pnts)))
+ self.error("nb_par = %d" % (len(new_parameters)))
+ self.error("pts = %s" % old_parameters_pnts)
+ self.error("pars = %s" % new_parameters)
+
+ # Create a vector of parameters including the intersection parameters
+ breaks = new_parameters[1:-1]
+ par_tol = 1e-10
+ # kinks is the list of C0 knots of input spline without tangency
+ kinks = self.getKinkParameters(spline)
+ # convert kink parameters into reparametrized parameter using the
+ # inverse reparametrization function
+ for ikink in range(len(kinks)):
+ kinks[ikink] = reparametrizing_spline.parameter(Vector2d(kinks[ikink], 0.0))
+
+ for kink in kinks:
+ pos = IsInsideTolerance(breaks, kink, par_tol)
+ if pos >= 0:
+ breaks.pop(pos)
+
+ # create equidistance array of parameters, including the breaks
+ parameters = LinspaceWithBreaks(new_parameters[0], new_parameters[-1], max(101, n_control_pnts * 2), breaks)
+
+ # insert kinks into parameters array at the correct position
+ for kink in kinks:
+ parameters.append(kink)
+ parameters.sort()
+
+ # Compute points on spline at the new parameters
+ # Those will be approximated later on
+ points = list()
+ for i in range(len(parameters)): # (size_t i = 1; i <= parameters.size(); ++i) {
+ oldParameter = reparametrizing_spline.value(parameters[i]).x
+ points.append(spline.value(oldParameter))
+
+ makeContinuous = spline.isClosed() and (
+ spline.tangent(spline.FirstParameter)[0].getAngle(spline.tangent(spline.LastParameter)[0])
+ < 6.0 / 180.0 * pi
+ )
+
+ # # Create the new spline as a interpolation of the old one
+ approximationObj = BSplineApproxInterp(points, n_control_pnts, 3, makeContinuous)
+
+ breaks.insert(0, new_parameters[0])
+ breaks.append(new_parameters[-1])
+ # # Interpolate points at breaking parameters (required for gordon surface)
+ # for (size_t ibreak = 0; ibreak < breaks.size(); ++ibreak) {
+ for thebreak in breaks:
+ pos = IsInsideTolerance(parameters, thebreak, par_tol)
+ if pos >= 0:
+ approximationObj.InterpolatePoint(pos, False)
+
+ for kink in kinks:
+ pos = IsInsideTolerance(parameters, kink, par_tol)
+ if pos >= 0:
+ approximationObj.InterpolatePoint(pos, True)
+
+ result, error = approximationObj.FitCurveOptimal(parameters, 10)
+ if not isinstance(result, Part.BSplineCurve):
+ raise ValueError("FitCurveOptimal failed to compute a valid curve")
+ return result
diff --git a/freecad/marz/extension/version.py b/freecad/marz/extension/version.py
index e53682b..7c65c1d 100644
--- a/freecad/marz/extension/version.py
+++ b/freecad/marz/extension/version.py
@@ -38,7 +38,7 @@ def __init__(self, version_str: str = None) -> None:
def __str__(self) -> str:
return self.raw
-
+
def __repr__(self) -> str:
return str(self.parts)
@@ -47,7 +47,7 @@ def __lt__(self, other: object) -> bool:
return self.value < other.value
except:
raise NotImplementedError()
-
+
def __eq__(self, other: object) -> bool:
try:
return self.value == other.value
@@ -56,15 +56,6 @@ def __eq__(self, other: object) -> bool:
def __bool__(self):
return all(self.value)
-
-
-def _get_curves_wb_version():
- try:
- from freecad.Curves import gordon # type: ignore
- metadata = App.Metadata(str(Path(Path(gordon.__file__).parent.parent.parent, 'package.xml')))
- return Version(metadata.Version)
- except:
- return Version()
-CurvesVersion = _get_curves_wb_version()
+
FreecadVersion = Version(".".join(App.Version()[:4]))
diff --git a/freecad/marz/feature/edit_form_layout.py b/freecad/marz/feature/edit_form_layout.py
index 7cb2bc8..0628154 100644
--- a/freecad/marz/feature/edit_form_layout.py
+++ b/freecad/marz/feature/edit_form_layout.py
@@ -27,13 +27,13 @@
from freecad.marz.model.instrument import NeckJoint, NutPosition, NutSpacing
from freecad.marz.model.neck_profile import NeckProfile
from freecad.marz.extension.paths import graphicsPath, resourcePath
-from freecad.marz import __version__, __author__, __license__, __copyright__
+from freecad.marz import __version__, __license__, __copyright__
from freecad.marz.feature.style import (
- STYLESHEET, SectionHeader, FlatIcon, intro_style, banner_style,
+ STYLESHEET, SectionHeader, FlatIcon, intro_style, banner_style,
log_styles)
from freecad.marz.feature.preferences import pref_current_tab
-from freecad.marz.extension.version import FreecadVersion, CurvesVersion, Version
-from freecad.marz import __dep_min_curves__, __dep_min_freecad__
+from freecad.marz.extension.version import FreecadVersion, Version
+from freecad.marz import __dep_min_freecad__
from freecad.marz.feature.document import File_Svg_Body, File_Svg_Headstock, File_Svg_Fret_Inlays
@@ -42,7 +42,6 @@
MarzVersion = Version(__version__)
MinFreecadVersion = Version(__dep_min_freecad__)
-MinCurvesVersion = Version(__dep_min_curves__)
# InputFloat with defaults for millimeters
def InputFloat(*args, suffix=' mm', decimals=4, step=0.0001, **kwargs):
@@ -75,24 +74,23 @@ def version_support_message(installed_version: Version, min_version: Version):
def tab_general(form):
variables = dict(
- version = MarzVersion,
+ version = MarzVersion,
freecad_version = FreecadVersion,
freecad_supported = version_support_message(FreecadVersion, MinFreecadVersion),
- curves_version = CurvesVersion,
- curves_supported = version_support_message(CurvesVersion, MinCurvesVersion))
+ )
with ui.Tab(tr('About')):
with ui.Col(contentsMargins=(0,0,0,0), spacing=0):
content = ui.TextLabel(f"{__copyright__}, License {__license__}", styleSheet=banner_style())
with ui.Scroll(widgetResizable=True):
- intro = ui.Html(file=resourcePath('intro.html'),
+ intro = ui.Html(file=resourcePath('intro.html'),
variables=variables,
styleSheet=intro_style(),
textInteractionFlags=Qt.TextBrowserInteraction | Qt.LinksAccessibleByMouse,
openExternalLinks=False)
intro.setAlignment(Qt.AlignTop)
ui.Stretch()
-
+
@ui.on_event(intro.linkActivated)
def navigate(url: str):
form.open_link(url)
@@ -103,11 +101,11 @@ def form_nut(form):
spacings = dict((
(tr('Equal Center'), NutSpacing.EQ_CENTER),
(tr('Equal Gap'), NutSpacing.EQ_GAP)))
-
+
positions = dict((
(tr('Parallel to fret zero'), NutPosition.PARALLEL),
(tr('Perpendicular to mid-line'), NutPosition.PERPENDICULAR)))
-
+
spacing_preview = {
NutSpacing.EQ_CENTER: graphicsPath('nut_eq_center.svg'),
NutSpacing.EQ_GAP: graphicsPath('nut_eq_gap.svg'),
@@ -133,12 +131,12 @@ def form_nut(form):
# minimumHeight=100, maximumHeight=100)
with ui.Section(SectionHeader(tr("Strings"), level=1)):
- form.nut_spacing = ui.InputOptions(label=tr('Gaps/Spacing'), options=spacings)
+ form.nut_spacing = ui.InputOptions(label=tr('Gaps/Spacing'), options=spacings)
# gap_preview_img = ui.SvgImageView(
# label="Preview",
# uri=spacing_preview[NutSpacing.EQ_CENTER],
# minimumHeight=100, maximumHeight=100)
-
+
# @ui.on_event(form.nut_position.currentIndexChanged)
# def on_pos_change(idx):
@@ -157,21 +155,21 @@ def tab_nut_and_bridge(form):
with ui.Section(SectionHeader(tr("Strings"))):
form.stringSet_gauges_f = ui.InputFloatList(
label_fn=lambda i: tr("String {}", i+1),
- values=[0.0]*6,
- label=tr('String gauges'),
- decimals=3,
- resizable=True,
+ values=[0.0]*6,
+ label=tr('String gauges'),
+ decimals=3,
+ resizable=True,
suffix=' in',
min_count=2)
with ScrollContainer():
- form_nut(form)
+ form_nut(form)
# ────────────────────────────────────────────────────────────────────────────
def neck_truss_rod(form):
with ui.Section(SectionHeader(tr('Truss rod channel'))):
- form.trussRod_start = InputFloat(label=tr('Start'), min=-1000)
+ form.trussRod_start = InputFloat(label=tr('Start'), min=-1000)
with ui.Section(SectionHeader(tr('Rod'), level=1), indent=15):
form.trussRod_length = InputFloat(label=tr('Length'))
form.trussRod_width = InputFloat(label=tr('Width'))
@@ -196,32 +194,32 @@ def tab_fretboard(form):
with ui.Section(SectionHeader(tr("Scale"))):
form.scale_treble = InputFloat(label=tr('Treble'))
form.scale_bass = InputFloat(label=tr('Bass'))
-
+
with ui.Section(SectionHeader(tr('Frets'))):
form.fretboard_frets = ui.InputInt(label=tr('Number of frets'))
form.fretboard_perpendicularFret = ui.InputInt(label=tr('Perpendicular fret'))
form.fretboard_fretNipping = InputFloat(label=tr('Hidden fret nipping'))
-
+
with ui.Section(SectionHeader(tr('Radius'))):
form.fretboard_startRadius = InputFloat(label=tr('At fret 0'))
form.fretboard_endRadius = InputFloat(label=tr('At fret 12'))
-
+
with ui.Section(SectionHeader(tr('Board'))):
form.fretboard_thickness = InputFloat(label=tr('Board thickness'))
form.fretboard_filletRadius = InputFloat(label=tr('Fillet radius'))
form.fretboard_inlayDepth = InputFloat(label=tr('Inlay depth'))
-
+
with ui.Section(SectionHeader(tr('Margins'))):
form.fretboard_startMargin = InputFloat(label=tr('At Nut'))
form.fretboard_endMargin = InputFloat(label=tr('At Heel'))
form.fretboard_sideMargin = InputFloat(label=tr('At Sides'))
-
+
with ui.Section(SectionHeader(tr('Fret Wire'))):
form.fretWire_tangDepth = InputFloat(label=tr('Tang Depth'))
form.fretWire_tangWidth = InputFloat(label=tr('Tang Width (Thickness)'))
form.fretWire_crownHeight = InputFloat(label=tr('Crown Height'))
form.fretWire_crownWidth = InputFloat(label=tr('Crown Width'))
-
+
with ui.GroupBox():
with ui.Col(contentsMargins=(0,0,0,0)):
form.inlays_svg = ImportSvgWidget(
@@ -259,13 +257,13 @@ def neck_joint(form):
# ────────────────────────────────────────────────────────────────────────────
def neck_profile(form):
- profiles = {p.name:p.name for p in NeckProfile.LIST.values()}
+ profiles = {p.name:p.name for p in NeckProfile.LIST.values()}
with ui.Section(SectionHeader(tr('Profile'))):
form.neck_profile = ui.InputOptions(
- label=tr('Profile'),
- options=profiles,
+ label=tr('Profile'),
+ options=profiles,
value=NeckProfile.DEFAULT)
-
+
with ui.Section(SectionHeader(tr('Section at fret 0'))):
form.neck_profile_preview = NeckProfileWidget(form, width=300, height=170)
@@ -310,15 +308,15 @@ def tab_body(form):
form.body_length = InputFloat(label=tr('Length'))
with ui.Section(SectionHeader(tr('Neck pocket'))):
- form.body_neckPocketCarve = ui.InputOptions(label=tr('Pocket'), options=neck_pocket_options)
- form.body_neckPocketDepth = InputFloat(label=tr('Depth'))
- form.body_neckPocketLength = InputFloat(label=tr('Manual position'))
-
+ form.body_neckPocketCarve = ui.InputOptions(label=tr('Pocket'), options=neck_pocket_options)
+ form.body_neckPocketDepth = InputFloat(label=tr('Depth'))
+ form.body_neckPocketLength = InputFloat(label=tr('Manual position'))
+
with ui.GroupBox():
with ui.Col(contentsMargins=(0,0,0,0)):
form.body_svg = ImportSvgWidget(
form,
- tr('Body custom shape (imported)'),
+ tr('Body custom shape (imported)'),
file=File_Svg_Body,
import_action=form.import_body,
export_action=form.export_body)
@@ -327,12 +325,12 @@ def tab_body(form):
def form_bridge(form):
with ui.Section(SectionHeader(tr("Bridge"))):
with ui.Section(SectionHeader(('Geometry'), level=1)):
- form.bridge_height = InputFloat(label=tr('Height'))
- form.bridge_stringDistanceProj = InputFloat(label=tr('String distance'))
+ form.bridge_height = InputFloat(label=tr('Height'))
+ form.bridge_stringDistanceProj = InputFloat(label=tr('String distance'))
with ui.Section(SectionHeader(('Compensation'), level=1)):
- form.bridge_trebleCompensation = InputFloat(label=tr('Treble'))
- form.bridge_bassCompensation = InputFloat(label=tr('Bass'))
+ form.bridge_trebleCompensation = InputFloat(label=tr('Treble'))
+ form.bridge_bassCompensation = InputFloat(label=tr('Bass'))
# ────────────────────────────────────────────────────────────────────────────
@@ -342,28 +340,28 @@ def tab_headstock(form):
with ScrollContainer():
with ui.Section(SectionHeader(tr('Angled Peghead'))):
form.headStock_angle = InputAngle(label=tr('Angle'), max=20)
-
+
with ui.Section(SectionHeader(tr('Flat Peghead'))):
form.headStock_depth = InputFloat(label=tr('Depth'))
form.headStock_topTransitionLength = InputFloat(label=tr('Top transition length'))
-
+
with ui.Section(SectionHeader(tr('Wood blank'))):
form.headStock_width = InputFloat(label=tr('Width'))
form.headStock_length = InputFloat(label=tr('Length'))
- form.headStock_thickness = InputFloat(label=tr('Thickness'))
-
+ form.headStock_thickness = InputFloat(label=tr('Thickness'))
+
with ui.Section(SectionHeader(tr('Volute'))):
form.headStock_voluteRadius = InputFloat(label=tr('Radius'))
-
+
with ui.Section(SectionHeader(tr('Transition'))):
form.headStock_transitionParamHorizontal = InputFloat(label=tr('Length'))
form.headStock_transitionHeight = InputFloat(label=tr('Tension'))
-
+
with ui.GroupBox():
with ui.Col(contentsMargins=(0,0,0,0)):
form.headstock_svg = ImportSvgWidget(
- form,
- title='Headstock custom shape (imported)',
+ form,
+ title='Headstock custom shape (imported)',
file=File_Svg_Headstock,
import_action=form.import_headstock,
export_action=form.export_headstock)
@@ -388,25 +386,25 @@ def build(form: InstrumentFormBase):
tab_headstock(form)
tab_nut_and_bridge(form)
tabs.setCurrentIndex(current_tab)
-
+
with ui.GroupBox(title=tr("Log"), visible=current_tab != 0) as log:
with ui.Col(contentsMargins=(0,0,0,0)):
form.log = ui.LogView(**log_styles())
- with ui.Container(visible=current_tab != 0) as buttons:
+ with ui.Container(visible=current_tab != 0) as buttons:
with ui.Row(contentsMargins=(0,0,0,0)):
form.status_line = ui.TextLabel(stretch=100, wordWrap=True)
update_2d = ui.button(
- label=tr(" Update Drafts"),
+ label=tr(" Update Drafts"),
icon=FlatIcon('2d.svg'),
autoDefault=True,
default=True)
-
+
update_3d = ui.button(
- label=tr(" Update Parts"),
+ label=tr(" Update Parts"),
icon=FlatIcon('3d.svg'))
-
+
update_2d(form.update_2d)
update_3d(form.update_3d)
@@ -415,7 +413,7 @@ def on_tab_changed(index):
log.setVisible(index != 0)
buttons.setVisible(index != 0)
pref_current_tab.write(index)
-
+
@ui.on_event((
form.trussRod_headDepth.valueChanged,
@@ -428,7 +426,7 @@ def on_tab_changed(index):
def neck_profile_changed(*args, **kwargs):
form.neck_profile_preview.update()
- supported = CurvesVersion >= Version(__dep_min_curves__) and FreecadVersion >= Version(__dep_min_freecad__)
+ supported = FreecadVersion >= Version(__dep_min_freecad__)
if not supported:
for tab in range(1, tabs.count()):
tabs.setTabEnabled(tab, False)
diff --git a/freecad/marz/init_gui.py b/freecad/marz/init_gui.py
index eb1ebc8..d8297b3 100644
--- a/freecad/marz/init_gui.py
+++ b/freecad/marz/init_gui.py
@@ -69,12 +69,5 @@ def Deactivated(self):
def import_dependencies():
MarzLogger.info(tr("Preloading dependencies..."))
import Part # type: ignore
- try:
- from freecad.Curves import ( # type: ignore
- gordon as tigl,
- BSplineApproxInterp,
- BSplineAlgorithms)
- except:
- MarzLogger.error(tr("Error importing Curves Workbench"))
Gui.addWorkbench(MarzWorkbench)
diff --git a/freecad/marz/model/gordon_neck.py b/freecad/marz/model/gordon_neck.py
index 3454678..02fb609 100644
--- a/freecad/marz/model/gordon_neck.py
+++ b/freecad/marz/model/gordon_neck.py
@@ -17,6 +17,7 @@
# | You should have received a copy of the GNU General Public License |
# | along with Marz Workbench. If not, see . |
# +---------------------------------------------------------------------------+
+from __future__ import annotations
from freecad.marz.model.instrument import Instrument
from freecad.marz.model.neck_data import NeckData
@@ -26,21 +27,22 @@
from freecad.marz.utils import traced, geom, traceTime
from freecad.marz.extension.threading import Task, task
from freecad.marz.extension.fc import App, Vector, Rotation, Placement
+import freecad.marz.curves.gordon as tigl
from typing import List, Optional
import math
-import contextlib
+
from dataclasses import dataclass
import Part # type: ignore
from Part import ( # type: ignore
- Edge,
- Face,
- Wire,
- Solid,
- Shell,
- Vertex,
- BSplineCurve,
+ Edge,
+ Face,
+ Wire,
+ Solid,
+ Shell,
+ Vertex,
+ BSplineCurve,
LineSegment)
import BOPTools.SplitAPI as split_api # type: ignore
@@ -123,7 +125,7 @@ def heel_profiles(inst: Instrument, fbd: FretboardData, neckd: NeckData) -> Heel
"""
height = min(max(20, inst.neck.transitionTension), 50)
-
+
bridge = fbd.neckFrame.bridge.edge()
mid = edge_mid_point(bridge)
@@ -131,7 +133,7 @@ def heel_profiles(inst: Instrument, fbd: FretboardData, neckd: NeckData) -> Heel
joint_fret_x = neckd.lineToFret(inst.neck.jointFret).end.x
delta = abs(mid.x - joint_fret_x) / 5
rot = Rotation(Vector(0,0,1), 0)
-
+
edge1 = heel_profile(bridge, inst, height)
edges = [edge1]
p1 = edge1.Placement.Base
@@ -149,7 +151,7 @@ def heel_profiles(inst: Instrument, fbd: FretboardData, neckd: NeckData) -> Heel
def neck_profiles(inst: Instrument, fbd: FretboardData, neckd: NeckData) -> NeckProfiles:
"""
Generate neck profiles except Heel profiles and Headstock profile
- """
+ """
steps = 9
edge = neckd.lineToFret(inst.neck.jointFret).edge()
curve = edge.Curve
@@ -171,7 +173,8 @@ def profile_edge(i, l=None, point=None, h_delta=0.0):
point = Vector(points[i].x, points[i].y, points[i].z)
w = widthAt(l)
p = profile(w, h, wire=False)
- if p: p.Placement = Placement(point, rot)
+ if p:
+ p.Placement = Placement(point, rot)
return p
# Edges between nut and transition start
@@ -185,7 +188,7 @@ def profile_edge(i, l=None, point=None, h_delta=0.0):
control_edge = profile_edge(0, offset, points[0] + direction * offset)
edges.append(control_edge)
- space = 1.5
+ # space = 1.5
# Edge for headstock transition start
# edges.insert(1, profile_edge(0, transition_offset*space, points[0] + direction * transition_offset*space))
edges.insert(1, profile_edge(0, transition_offset, points[0] + direction * transition_offset))
@@ -193,7 +196,7 @@ def profile_edge(i, l=None, point=None, h_delta=0.0):
# Edges before nut to force the guides pass for nut point
control = -1.2 # Issue #40
edges.insert(0, profile_edge(0, control, points[0] + direction * control))
-
+
return NeckProfiles(edges, nut_profile)
@@ -264,7 +267,7 @@ def base_headstock_profile_bspline(support_edge: Wire, height: float) -> Edge:
-
i--h--g--f
"""
- add = 5.0
+ # add = 5.0
a = support_edge.Vertexes[0].Point
b = a + Vector(0,0,height/2)
c = b + Vector(0,0,height/2)
@@ -310,19 +313,20 @@ def gordon_neck(inst: Instrument, fbd: FretboardData, neckd: NeckData):
with traceTime('Gordon Neck: Base + Headstock'):
# High tolerance to avoid possible self intersecting artifacts
- pre_assemble = t_blank().solid.fuse([t_headstock().solid], 1e-3)
+ pre_assemble = t_blank().solid.fuse([t_headstock().solid], 1e-3)
with traceTime('Gordon Neck: Collect Top, Volute, Pockets'):
tools = [t_volute_cut(), t_top_cut()]
tool_cut_pockets = t_pockets()
- if tool_cut_pockets: tools.append(tool_cut_pockets)
+ if tool_cut_pockets:
+ tools.append(tool_cut_pockets)
with traceTime('Gordon Neck: Assembly - Top - Bottom - Pockets'):
- pre_assemble = pre_assemble.cut(tools, 1e-3)
+ pre_assemble = pre_assemble.cut(tools, 1e-3)
with traceTime('Gordon Neck: Refine'):
assemble = pre_assemble.removeSplitter()
-
+
return assemble
@@ -382,7 +386,7 @@ def apply_headstock_angle(edge: Edge, angle_deg: float, fbd: FretboardData) -> E
# Reconstruct a BSpline edge
bspline = Wire(segments)
-
+
# Re-parameterize the curve to make it homogeneous
points = bspline.discretize(Number=100)
bspline = BSplineCurve(points).toShape()
@@ -420,28 +424,28 @@ def neck_blank(inst: Instrument, fbd: FretboardData, neckd: NeckData) -> Task[Ne
# Exclude some redundant or invalid profiles from the surface
exclude = 1, 2, 12, 13, 14
- profiles_edges = [e for i,e in enumerate(all_profiles) if i not in exclude]
+ profiles_edges: list[Edge] = [e for i,e in enumerate(all_profiles) if i not in exclude]
# Neck gordon surface
- with traceTime("Gordon Neck (CurvesWB): InterpolateCurveNetwork"):
+ with traceTime("Gordon Neck: InterpolateCurveNetwork"):
tol_3d = 1e-5
tol_2d = 1e-5
guide_curves = [e.Curve.toBSpline(e.FirstParameter, e.LastParameter) for e in guides]
prof_curves = [e.Curve.toBSpline(e.FirstParameter, e.LastParameter) for e in profiles_edges]
- with curves_wb(GORDON_DEBUG) as tigl:
- gordon = tigl.InterpolateCurveNetwork(prof_curves, guide_curves, tol_3d, tol_2d)
- gordon.max_ctrl_pts = 80
- face_gordon = gordon.surface().toShape()
- del(gordon)
+ gordon = tigl.InterpolateCurveNetwork(prof_curves, guide_curves, tol_3d, tol_2d)
+ gordon.max_ctrl_pts = 80
+ face_gordon = gordon.surface().toShape()
+ del(gordon)
+
# Extract terminal edges to create faces for the shell
- heel_edge, headstock_edge = geom.query(face_gordon.Edges,
+ heel_edge, headstock_edge = geom.query(face_gordon.Edges,
where=lambda x: not geom.is_planar(x, normal=Vector(0,0,1)),
order_by=lambda f: f.CenterOfGravity.x,
limit=2)
# Extract border edges to create faces for the shell
- sides = geom.query(face_gordon.Edges,
+ sides = geom.query(face_gordon.Edges,
where=lambda x: geom.is_planar(x, normal=Vector(0,0,1)),
limit=2)
@@ -531,26 +535,6 @@ def extrude_headstock_plate(contour: Wire, thickness: float, transition_edge: Wi
return HeadstockPlate(selected.extrude(normal * thickness), normal)
-@contextlib.contextmanager
-def curves_wb(debug: bool = False):
- from freecad.Curves import gordon as tigl, BSplineApproxInterp, BSplineAlgorithms
- packages = (
- tigl,
- BSplineAlgorithms,
- BSplineApproxInterp,
- tigl.curve_network_sorter)
-
- saved = tuple(getattr(pkg, 'DEBUG', False) for pkg in packages)
-
- for pkg in packages:
- pkg.DEBUG = debug
-
- yield tigl
-
- for i, pkg in enumerate(packages):
- pkg.DEBUG = saved[i]
-
-
@traced("Gordon Neck: Top Cut")
def top_cut_flat(pos: Vector, depth: float, transition_length: float, volute_offset: float) -> Solid:
"""Create solid to cleanup the top surface"""
diff --git a/manifest.ini b/manifest.ini
index 9fb6236..b541f71 100644
--- a/manifest.ini
+++ b/manifest.ini
@@ -3,7 +3,7 @@
;* General identification metadata *
;***************************************************************************
[general]
-version=0.1.8
+version=0.1.9
name=Marz
title=Marz Guitar Design
description=Parametric Guitar Design
diff --git a/package.xml b/package.xml
index 4564ca9..0d98d85 100644
--- a/package.xml
+++ b/package.xml
@@ -2,7 +2,7 @@
Marz Workbench
Parametric Guitar design workbench
- 0.1.8
+ 0.1.9
Frank Martinez
GPL-3.0
https://github.com/mnesarco/MarzWorkbench
@@ -13,7 +13,7 @@
Marz Workbench
MarzWorkbench
Parametric Guitar design workbench.
- 0.1.8
+ 0.1.9
./
Resources/icons/Marz.svg
0.21.0