nortikin / sverchok

Sverchok
http://nortikin.github.io/sverchok/
GNU General Public License v3.0
2.26k stars 234 forks source link

rhino3dm testing #3382

Closed zeffii closed 4 years ago

zeffii commented 4 years ago

i'll dig around in the API tomorrow. related content: https://www.rhino3d.com/download/rhino/6/essentialmathematics

"""
in start_vector v d=[0,0,0] n=2
in radius s d=0.5 n=2
in angle_rads s d=0.3 n=2
out verts v
"""
import numpy as np
from rhino3dm import *

center = Point3d(*start_vector)

arc = Arc(center, radius, angle_rads)
nc = arc.ToNurbsCurve()

decompose = lambda point: (point.X, point.Y, point.Z)
verts.append([decompose(nc.PointAt(f)) for f in np.linspace(0, angle_rads, 10)])

image

portnov commented 4 years ago

Wow. How come I did not find this when I searched for nurbs-python?...

portnov commented 4 years ago

It seems it has a lot of functionality, parts of which we currently have implemented either with pure python or via Geomdl. So, to integrate this, we will have to think about two big questions

  1. How to build general API that could implement the same functionality either with rhino3dm or without it;
  2. What to do with nodes: should we have two sets of nodes (rhino3dm-based and geomdl-based and pure-python), or should there be one set of nodes that can switch between implementations (automatically? or give a choice to user?)
zeffii commented 4 years ago

i'm not sure, but I was thinking of writing a small wrapper to allow keyword args for rhino3dm constructors, the docs don't make it clear what's happening if we pass a few arguments to Arc(). there are i think 4 or more variants of Arc.

if going the wrapper route, that would make it possible to smoothly switch backends, either globally or local.

zeffii commented 4 years ago

it's an interesting approach, the Arc "class" can be instantiated differently based on the types of parameters received by the constructor.

    1. rhino3dm._rhino3dm.Arc(circle: BND_Circle, angleRadians: float)
    2. rhino3dm._rhino3dm.Arc(center: rhino3dm._rhino3dm.Point3d, radius: float, angleRadians: float)
    3. rhino3dm._rhino3dm.Arc(startPoint: rhino3dm._rhino3dm.Point3d, pointOnInterior: rhino3dm._rhino3dm.Point3d, endPoint: rhino3dm._rhino3dm.Point3d)
    4. rhino3dm._rhino3dm.Arc(pointA: rhino3dm._rhino3dm.Point3d, tangentA: rhino3dm._rhino3dm.Vector3d, pointB: rhino3dm._rhino3dm.Point3d)
portnov commented 4 years ago

C++-style :)

vicdoval commented 4 years ago

Just wanted to point the FreeCad api offers two kind of arcs (center, start, end) and 3pt and also NURBS... in case you want to explore...

zeffii commented 4 years ago

@vicdoval when you push freecad nodes to master, i think we can help explore the rest of the freecad API.

rhino api makes brep so that's interesting, i bet there is a way to make them work interchangably to some degree

portnov commented 4 years ago

It seems we are fastly moving to state of Grasshopper, where there are a lot of "node sets" (plugins), which are partially intersecting. Look at http://grasshopperdocs.com/ , there are very similar nodes in different sections. I assume they ended up in this state the same way we are: they had not some functions in core, then someone implemented it in plugin, then someone else implemented it in another plugin "the better way", then they implemented it in core... Probably having several implementations of the same functionality is not that bad (different implementations can be better in different corner cases, for example); but this brings the question of managing this amount of nodes — we may have "Solids (Rhino)" category near "Solids (FreeCAD)", and the same with several other categories... Maybe we should have some kind of super-categories?

portnov commented 4 years ago

Technically, what I think we should do is

  1. Implement SvCurve and SvSurface subclasses, which wrap functionality from libraries (this is what @vicdoval already doing);
  2. If necessary, add methods to SvCurve/SvSurface base classes to provide functionality which some implementation provides (for example, rhino3dm has curve.IsCircle, I think FreeCAD does not have it? or maybe there is a way to implement the same, maybe just checking isinstance). This will be necesssary only if we want to expose this functionality in a new node. Such node will raise an exception if passed with an object which does not support required method.
  3. It seems we will have to have SvSolid class, similar to SvCurve/SvSurface, which will generalize rhino3dm and FreeCAD implementations (and probably we will have some functions implemented in pure python too).

The question of how the user will select the required implementation (and how will he decide which implementation to use) is still open. Maybe we will add a popup menu into each node. Or maybe we will have several copies of each node, as Grasshopper does. Or?...

zeffii commented 4 years ago

I prefer we focus on getting it working nicely using freeCAD first, and put some requests on blender.org devs to add methods to the Nurbs object type to make it possible to pass it all the information it needs to represent the Surface (without using ops).

NurbData = bpy.data.curves.new(name, "NURBSURFACE<or whatever>", packed_data_tuple)
NurbsObject = bpy.data.objects.new(name, NurbData)

NurbsObject.data.update_from_pydata(packed_data_tuple)
NurbsObject.data.update()
NurbsObject.data.validate()

NurbsObject.data.update_from_pydata(packed_data_tuple, validate=True, update=True)

multiple implementations is always possible, but now we are almost spoiled for choice, it would be useful to get better backend support for displaying this data (and let Blender do that heavy lifting for a change...it's practically implemented already... we are just missing one or two functions to use it)

portnov commented 4 years ago

Creating Blender's nurbs objects having control points and weights is the easiest part. Mathematics / algorithms required to

  1. calculate required control points from other conditions, for example by interpolating points or converting from mesh etc,
  2. evaluate - calculate points having U/V coordinates,
  3. perform "interesting things" such as bending along surface, or offsetting — is much heavier. And that is what rhino3dm or Geomdl (or FreeCAD? can FreeCAD give us NURBS @vicdoval ?) can help us do.
zeffii commented 4 years ago

yes, the longer we can keep the objects being shuttled around our nodetree as 'symbolic' entities, the better.

portnov commented 4 years ago

Hmm. Rhino3dm's Curve doesn't seem to provide much differential geometry information:

So, such things we still will have to calculate numerically... Note: Geomdl does provide derivatives.

vicdoval commented 4 years ago

As far as i have tested FreeCad supplies NURBS (and many things I don't understand) image

Also the Curve Object (at least the solid edges) can give Tangents, curvature, derivatives 2nd and 3rd and intersections i think i saw binormals somewhere too... and other things i dont understand) image

portnov commented 4 years ago

rhino3dm's Nurbs curve:

"""
in control_points_in v
in weights_in s
in degree_in s
out curve_out C
"""

import numpy as np

from sverchok.data_structure import zip_long_repeat
from sverchok.utils.curve import SvCurve

from rhino3dm._rhino3dm import NurbsCurve, Point3d

class RhinoNurbsCurve(SvCurve):
    def __init__(self, degree, points, weights=None, is_cyclic=False):
        pts = [Point3d(*t) for t in points]
        self.curve = NurbsCurve.Create(is_cyclic, degree, pts)
        if weights is not None:
            for pt, weight in zip_long_repeat(self.curve.Points, weights):
                pt.W = weight

    def is_closed(self, eps=None):
        return self.curve.IsPeriodic

    def evaluate(self, t):
        pt = self.curve.PointAt(t)
        return np.array([pt.X, pt.Y, pt.Z])

    def evaluate_array(self, ts):
        return np.vectorize(self.evaluate)(ts)

    def get_u_bounds(self):
        return (0, 1)

curve_out = []
for control_points, weights, degree in zip_long_repeat(control_points_in, weights_in, degree_in):
    if isinstance(degree, (list, tuple)):
        degree = degree[0]

    curve = RhinoNurbsCurve(degree, control_points, weights)
    curve_out.append(curve)
zeffii commented 4 years ago

So, such things we still will have to calculate numerically... Note: Geomdl does provide derivatives.

tragic. for rhino3dm :)

vicdoval commented 4 years ago

This would be it? Not sure what "Mults" is... image

portnov commented 4 years ago

Mults probably are weights ("multipliers") ?

rendetto commented 4 years ago

Mults probably are weights ("multipliers") ?

I think those refer to knot multiplicities

zeffii commented 4 years ago

@portnov does curve.PointAt(t) accept multiple inputs or is that function called for every t , the numpy kind of makes it difficult to be sure :) , the docs suggest a single input. oh i see, you are (truely) vectorizing that.

would passing an array not be a better / faster implementation for that API, than calling that function repeatedly ?

portnov commented 4 years ago

rhino3dm's PointAt() method must be called for each t, as far as I see...

 |  PointAt(...)
 |      PointAt(self: rhino3dm._rhino3dm.Curve, t: float) -> rhino3dm._rhino3dm.Point3d

Off course if the API allows to pass an array, such feature must be used (and called from evaluate_array()).

zeffii commented 4 years ago

https://github.com/mcneel/rhino3dm/issues

i wonder if they accept pullrequests for features like that.

PointAt(t)
PointsAt([t0, t1, t2, t3. ....tn])
zeffii commented 4 years ago

https://github.com/mcneel/rhino3dm/blob/3b5034d076e45c84480f72dfe52f9d95c3c3da25/src/bindings/bnd_bezier.cpp#L11

    .def("PointAt", &BND_BezierCurve::PointAt, py::arg("t"))

interesting to see pybind11 in the wild.

portnov commented 4 years ago

Well, obviously C++-part should support such features first. If it uses Eigen3 or boost::ublas, it should be relatively easy. Otherwise...

portnov commented 4 years ago

C++ part of rhino3dm is OpenNurbs https://developer.rhino3d.com/guides/opennurbs/what-is-opennurbs/ . Which is, as far as I understand, a small opensourced part of Rhino itself.

portnov commented 4 years ago

It does not seem to be vectorized: https://github.com/mcneel/opennurbs/blob/7.x/opennurbs_bezier.cpp#L173 .

portnov commented 4 years ago

On the other hand, my pure-python implementation of Bezier curves is vectorized with numpy: https://github.com/nortikin/sverchok/blob/master/utils/curve.py#L1640 . So, I would not be very surprised if it appeared that python implementation is actually faster than rhino3dm's, at least for some applications.

I'm already thinking about implementing nurbs evaluation in python/numpy similarly; it will be more complex than Bezier, but not so that very much. But at the moment I'm too lazy :)

zeffii commented 4 years ago

So, I would not be very surprised if it appeared that python implementation is actually faster than rhino3dm's, at least for some applications.

i thought it might be.

But at the moment I'm too lazy :)

sure, until you need it ;)

portnov commented 4 years ago

FreeCAD's nurbs implementation is not, in general, vectorized either; For one single operation - evaluation - there is a function that can return several points at one time, and in some particular cases it can be useful:

     |  discretize(...)
     |      Discretizes the curve and returns a list of points.
     |      The function accepts keywords as argument:
     |      discretize(Number=n) => gives a list of 'n' equidistant points
     |      discretize(QuasiNumber=n) => gives a list of 'n' quasi equidistant points (is faster than the method above)
     |      discretize(Distance=d) => gives a list of equidistant points with distance 'd'
     |      discretize(Deflection=d) => gives a list of points with a maximum deflection 'd' to the curve
     |      discretize(QuasiDeflection=d) => gives a list of points with a maximum deflection 'd' to the curve (faster)
     |      discretize(Angular=a,Curvature=c,[Minimum=m]) => gives a list of points with an angular deflection of 'a'
     |                                          and a curvature deflection of 'c'. Optionally a minimum number of points
     |                                          can be set which by default is set to 2.
     |      
     |      Optionally you can set the keywords 'First' and 'Last' to define a sub-range of the parameter range
     |      of the curve.
     |      
     |      If no keyword is given then it depends on whether the argument is an int or float.
     |      If it's an int then the behaviour is as if using the keyword 'Number', if it's float
     |      then the behaviour is as if using the keyword 'Distance'.
     |      
     |      Example:
     |      
     |      import Part
     |      c=Part.Circle()
     |      c.Radius=5
     |      p=c.discretize(Number=50,First=3.14)
     |      s=Part.Compound([Part.Vertex(i) for i in p])
     |      Part.show(s)
     |      
     |      
     |      p=c.discretize(Angular=0.09,Curvature=0.01,Last=3.14,Minimum=100)
     |      s=Part.Compound([Part.Vertex(i) for i in p])
     |      Part.show(s)
zeffii commented 4 years ago

totally this.

discretize(Number=n) => gives a list of 'n' equidistant points
portnov commented 4 years ago

FreeCAD's Nurbs curve

"""
in control_points_in v
in weights_in s
in degree_in s
out curve_out C
"""

import numpy as np

from sverchok.data_structure import zip_long_repeat, repeat_last_for_length
from sverchok.utils.curve import SvCurve

from FreeCAD import Base
import Part

class FreeCadNurbsCurve(SvCurve):
    def __init__(self, degree, points, weights=None):
        pts = [Base.Vector(*t) for t in points]
        print(pts)
        pts_count = len(points)
        knots_count = pts_count + degree
        order = degree+1
        self.curve = Part.BSplineCurve()
        knots = np.linspace(0, 1, num=pts_count-2).tolist()
        mults = [degree+1] + [1]*(len(knots)-degree-1) + [degree+1]
        print(knots)
        print(mults)
        self.curve.buildFromPolesMultsKnots(pts, mults, knots, False, degree, weights)

    def is_closed(self, eps=None):
        return self.curve.isClosed()

    def evaluate(self, t):
        pt = self.curve.value(t)
        r = np.array([pt.x, pt.y, pt.z])
        #print(r)
        return r

    def evaluate_array(self, ts):
        return np.vectorize(self.evaluate, signature='()->(3)')(ts)

    def tangent(self, t):
        v = self.curve.tangent(t)
        return np.array([v.x, v.y, v.z])

    def tangent_array(self, ts):
        return np.vectorize(self.tangent, signature='()->(3)')(ts)    

    def get_u_bounds(self):
        return (0, 1)

curve_out = []
for control_points, weights, degree in zip_long_repeat(control_points_in, weights_in, degree_in):
    if isinstance(degree, (list, tuple)):
        degree = degree[0]

    weights = repeat_last_for_length(weights, len(control_points))
    curve = FreeCadNurbsCurve(degree, control_points, weights)
    curve_out.append(curve)
zeffii commented 4 years ago

clearly FreeCAD is looking like it will get preferential treatment :)

anything we can ransack from rhino3dm is just bonus :) (i thought the export functions look particularly useful )