donalffons / opencascade.js

Port of the OpenCascade CAD library to JavaScript and WebAssembly via Emscripten.
https://ocjs.org/
GNU Lesser General Public License v2.1
631 stars 92 forks source link

Pass by reference APIs #55

Closed sgenoud closed 3 years ago

sgenoud commented 3 years ago

I was trying to retries the UV bounds of a face using this API. From what I understand in C++ you need to pass a reference to a real, and the function update the value pointed.

Is there a way to do this with opencascade.js? I have tried something like this:

  let u0 = 0;
  let u1 = 0;
  let v0 = 0;
  let v1 = 0;
  new oc.BRepTools.UVBounds_1(face, u0, u1, v0, v1);
  console.log(u0, u1, v0, v1);

but it fails miserably with a RuntimeError: function signature mismatch.

Am I missing a way to retrieve the values?

donalffons commented 3 years ago

Hi,

unfortunately, it is currently not possible to accept "return values" from an OpenCascade function that is using references (or pointers) to built-in datatypes.

I outlined some ideas on how to implement this in this post. However, I'm currently not sure how the actual C++ syntax might look like to make this work in the context of class methods. The generated C++ binding code (in Emscripten's Embind convention) for this class looks like this:

  class_<BRepTools>("BRepTools")
    .constructor<>()
    .class_function("UVBounds_1", reinterpret_cast<void (*) (const TopoDS_Face &, Standard_Real , Standard_Real , Standard_Real , Standard_Real ) >((void (*)(const TopoDS_Face & F, Standard_Real & UMin, Standard_Real & UMax, Standard_Real & VMin, Standard_Real & VMax) ) &BRepTools::UVBounds), allow_raw_pointers())
    .class_function("UVBounds_2", reinterpret_cast<void (*) (const TopoDS_Face &, const TopoDS_Wire &, Standard_Real , Standard_Real , Standard_Real , Standard_Real ) >((void (*)(const TopoDS_Face & F, const TopoDS_Wire & W, Standard_Real & UMin, Standard_Real & UMax, Standard_Real & VMin, Standard_Real & VMax) ) &BRepTools::UVBounds), allow_raw_pointers())
    .class_function("UVBounds_3", reinterpret_cast<void (*) (const TopoDS_Face &, const TopoDS_Edge &, Standard_Real , Standard_Real , Standard_Real , Standard_Real ) >((void (*)(const TopoDS_Face & F, const TopoDS_Edge & E, Standard_Real & UMin, Standard_Real & UMax, Standard_Real & VMin, Standard_Real & VMax) ) &BRepTools::UVBounds), allow_raw_pointers())
    .class_function("AddUVBounds_1", reinterpret_cast<void (*) (const TopoDS_Face &, const Bnd_Box2d &) >((void (*)(const TopoDS_Face & F, Bnd_Box2d & B) ) &BRepTools::AddUVBounds), allow_raw_pointers())
    .class_function("AddUVBounds_2", reinterpret_cast<void (*) (const TopoDS_Face &, const TopoDS_Wire &, const Bnd_Box2d &) >((void (*)(const TopoDS_Face & F, const TopoDS_Wire & W, Bnd_Box2d & B) ) &BRepTools::AddUVBounds), allow_raw_pointers())
    .class_function("AddUVBounds_3", reinterpret_cast<void (*) (const TopoDS_Face &, const TopoDS_Edge &, const Bnd_Box2d &) >((void (*)(const TopoDS_Face & F, const TopoDS_Edge & E, Bnd_Box2d & B) ) &BRepTools::AddUVBounds), allow_raw_pointers())
    .class_function("Update_1", static_cast<void (*) (const TopoDS_Vertex &) >((void (*)(const TopoDS_Vertex & V) ) &BRepTools::Update), allow_raw_pointers())
    .class_function("Update_2", static_cast<void (*) (const TopoDS_Edge &) >((void (*)(const TopoDS_Edge & E) ) &BRepTools::Update), allow_raw_pointers())
    .class_function("Update_3", static_cast<void (*) (const TopoDS_Wire &) >((void (*)(const TopoDS_Wire & W) ) &BRepTools::Update), allow_raw_pointers())
    .class_function("Update_4", static_cast<void (*) (const TopoDS_Face &) >((void (*)(const TopoDS_Face & F) ) &BRepTools::Update), allow_raw_pointers())
    .class_function("Update_5", static_cast<void (*) (const TopoDS_Shell &) >((void (*)(const TopoDS_Shell & S) ) &BRepTools::Update), allow_raw_pointers())
    .class_function("Update_6", static_cast<void (*) (const TopoDS_Solid &) >((void (*)(const TopoDS_Solid & S) ) &BRepTools::Update), allow_raw_pointers())
    .class_function("Update_7", static_cast<void (*) (const TopoDS_CompSolid &) >((void (*)(const TopoDS_CompSolid & C) ) &BRepTools::Update), allow_raw_pointers())
    .class_function("Update_8", static_cast<void (*) (const TopoDS_Compound &) >((void (*)(const TopoDS_Compound & C) ) &BRepTools::Update), allow_raw_pointers())
    .class_function("Update_9", static_cast<void (*) (const TopoDS_Shape &) >((void (*)(const TopoDS_Shape & S) ) &BRepTools::Update), allow_raw_pointers())
    .class_function("UpdateFaceUVPoints", static_cast<void (*) (const TopoDS_Face &) >(&BRepTools::UpdateFaceUVPoints), allow_raw_pointers())
    .class_function("Clean", static_cast<void (*) (const TopoDS_Shape &) >(&BRepTools::Clean), allow_raw_pointers())
    .class_function("CleanGeometry", static_cast<void (*) (const TopoDS_Shape &) >(&BRepTools::CleanGeometry), allow_raw_pointers())
    .class_function("RemoveUnusedPCurves", static_cast<void (*) (const TopoDS_Shape &) >(&BRepTools::RemoveUnusedPCurves), allow_raw_pointers())
    .class_function("Triangulation", static_cast<Standard_Boolean (*) (const TopoDS_Shape &, const Standard_Real, const Standard_Boolean) >(&BRepTools::Triangulation), allow_raw_pointers())
    .class_function("Compare_1", static_cast<Standard_Boolean (*) (const TopoDS_Vertex &, const TopoDS_Vertex &) >((Standard_Boolean (*)(const TopoDS_Vertex & V1, const TopoDS_Vertex & V2) ) &BRepTools::Compare), allow_raw_pointers())
    .class_function("Compare_2", static_cast<Standard_Boolean (*) (const TopoDS_Edge &, const TopoDS_Edge &) >((Standard_Boolean (*)(const TopoDS_Edge & E1, const TopoDS_Edge & E2) ) &BRepTools::Compare), allow_raw_pointers())
    .class_function("OuterWire", static_cast<TopoDS_Wire (*) (const TopoDS_Face &) >(&BRepTools::OuterWire), allow_raw_pointers())
    .class_function("Map3DEdges", reinterpret_cast<void (*) (const TopoDS_Shape &, const TopTools_IndexedMapOfShape &) >(&BRepTools::Map3DEdges), allow_raw_pointers())
    .class_function("IsReallyClosed", static_cast<Standard_Boolean (*) (const TopoDS_Edge &, const TopoDS_Face &) >(&BRepTools::IsReallyClosed), allow_raw_pointers())
    .class_function("DetectClosedness", reinterpret_cast<void (*) (const TopoDS_Face &, Standard_Boolean , Standard_Boolean ) >(&BRepTools::DetectClosedness), allow_raw_pointers())
    .class_function("Dump", reinterpret_cast<void (*) (const TopoDS_Shape &, const Standard_OStream &) >(&BRepTools::Dump), allow_raw_pointers())
    .class_function("Write_1", reinterpret_cast<void (*) (const TopoDS_Shape &, const Standard_OStream &, const Message_ProgressRange &) >((void (*)(const TopoDS_Shape & Sh, Standard_OStream & S, const Message_ProgressRange & theProgress) ) &BRepTools::Write), allow_raw_pointers())
    .class_function("Read_1", reinterpret_cast<void (*) (const TopoDS_Shape &, const Standard_IStream &, const BRep_Builder &, const Message_ProgressRange &) >((void (*)(TopoDS_Shape & Sh, Standard_IStream & S, const BRep_Builder & B, const Message_ProgressRange & theProgress) ) &BRepTools::Read), allow_raw_pointers())
    .class_function("Write_2", reinterpret_cast<Standard_Boolean (*) (const TopoDS_Shape &, std::string, const Message_ProgressRange &) >((Standard_Boolean (*)(const TopoDS_Shape & Sh, const Standard_CString File, const Message_ProgressRange & theProgress) ) &BRepTools::Write), allow_raw_pointers())
    .class_function("Read_2", reinterpret_cast<Standard_Boolean (*) (const TopoDS_Shape &, std::string, const BRep_Builder &, const Message_ProgressRange &) >((Standard_Boolean (*)(TopoDS_Shape & Sh, const Standard_CString File, const BRep_Builder & B, const Message_ProgressRange & theProgress) ) &BRepTools::Read), allow_raw_pointers())
    .class_function("EvalAndUpdateTol", static_cast<Standard_Real (*) (const TopoDS_Edge &, const opencascade::handle<Geom_Curve> &, const opencascade::handle<Geom2d_Curve>, const opencascade::handle<Geom_Surface> &, const Standard_Real, const Standard_Real) >(&BRepTools::EvalAndUpdateTol), allow_raw_pointers())
    .class_function("OriEdgeInFace", static_cast<TopAbs_Orientation (*) (const TopoDS_Edge &, const TopoDS_Face &) >(&BRepTools::OriEdgeInFace), allow_raw_pointers())
    .class_function("RemoveInternals", reinterpret_cast<void (*) (const TopoDS_Shape &, const Standard_Boolean) >(&BRepTools::RemoveInternals), allow_raw_pointers())
  ;

If you happen to have an idea how the syntax for a modified UVBounds_1 binding definition might look like, please let me know.

However, the runtime error that you mentioned is unexpected... I was able to reproduce it and got:

Uncaught (in promise) RuntimeError: function signature mismatch
    at _ZN10emscripten8internal7InvokerIvJRK19BRepAdaptor_SurfaceddddEE6invokeEPFvS4_ddddEPS2_dddd (<anonymous>:wasm-function[2460]:0xa69d36)
    at new BRepTools$UVBounds_1 (eval at new_ (webpack://opencascade.js-examples/./node_modules/opencascade.js/dist/opencascade.js?), <anonymous>:13:1)
    at makePolygon (webpack://opencascade.js-examples/./src/demos/polygon/library.js?:17:3)
    at eval (webpack://opencascade.js-examples/./src/demos/polygon/index.js?:22:140)
_ZN10emscripten8internal7InvokerIvJRK19BRepAdaptor_SurfaceddddEE6invokeEPFvS4_ddddEPS2_dddd @ 04410a92:0xa69d36
BRepTools$UVBounds_1 @ VM126223:13
makePolygon @ library.js:17
eval @ index.js:22

The demangled error is

emscripten::internal::Invoker<void, BRepAdaptor_Surface const&, double, double, double, double>::invoke(void (*)(BRepAdaptor_Surface const&, double, double, double, double), BRepAdaptor_Surface*, double, double, double, double)

The presence of BRepAdaptor_Surface in the error message is confusing... I need to investigate this further.

sgenoud commented 3 years ago

Sorry I missed the part on passing by reference in your doc!

I would not be surprised if the BRepAdaptor_Surface was actually used by the BRepTool API. I have looked into different ways to access the UV info, but so far all seem to use this type of interface 😢.

So if I understand it well, you would need to manually define all the interfaces that work with the passing by reference. For this API in particular, I would rewrite the signature, such that it returns an array with the four values. It would not follow exactly the documentation - but be a bit easier to use in JS / TS land.

donalffons commented 3 years ago

So if I understand it well, you would need to manually define all the interfaces that work with the passing by reference. For this API in particular, I would rewrite the signature, such that it returns an array with the four values. It would not follow exactly the documentation - but be a bit easier to use in JS / TS land.

Hmm, so this project is automatically generating the C++ code for the bindings. Therefore, I would like to find a way of handling these references to built-in's in a way that works across all (or most) of OpenCascade's methods. Rewriting the signature to return the references to built-in datatypes might work, but my instinct is that "wrapping" the references to built-ins like this

const intRef = {
  current: 123,
};
new oc.Something(intRef); // intRef is passed as a shallow copy, therefore intRef.current can be mutated by this method.

might be easier and more consistent, as it keeps the structure of the function signatures very similar to the original ones. Here is another issue over at the Emscripten repo with a few more examples of the idea... This approach is pretty straightforward to do with "normal" functions. With class methods, I'm currently not sure how to implement that in C++, i.e.:

  1. If I'm using the shown approach using C++ lambda functions, how would I call the class method on the current object i.e. object->someClassMethod()? Maybe there is a way to achieve this using templates? Capturing lambdas? Clever casting? Or probably a combination of those :slightly_smiling_face:.
  2. Alternatively, I might create a derived class with the modified method signatures. However, I feel this approach would lead to quite a bit of overhead and potential clashes in signatures?! I feel option 1 would be preferred, if there is a "proper" way to do this.
donalffons commented 3 years ago

...if you want a quick workaround, you might want to look into creating a custom build (using the beta version), like this (untested):

MyBRepTools.wasm:
  inputs: []
  bindings:
    - symbol: MyBRepTools
  additionalCppCode: |
    #include <emscripten/val.h>
    class MyBRepTools: public BRepTools {
    public:
      static emscripten::val MyUVBounds(const TopoDS_Face &f) {
        Standard_Real uMin;
        Standard_Real uMax;
        Standard_Real vMin;
        Standard_Real vMax;
        BRepTools::UVBounds(f, uMin, uMax, vMin, vMax);
        emscripten::val ret(emscripten::val::object());
        ret.set("uMin", emscripten::val(uMin));
        ret.set("uMax", emscripten::val(uMax));
        ret.set("vMin", emscripten::val(vMin));
        ret.set("vMax", emscripten::val(vMax));
        return ret;
      }
    };
  emccFlags:
    - -sSIDE_MODULE=1
    - -O3

Have a look at the docs for Emscripten::val. You can then load the library like this:

import MyBRepTools from "MyBRepTools.wasm";
import {
  initOpenCascade,
  ocCore,
  ocModelingAlgorithms,
  ocVisualApplication,
  ocDataExchangeBase,
  ocDataExchangeExtra,
} from 'opencascade.js/dist';

initOpenCascade({
  libs: [
    ocCore,
    ocModelingAlgorithms,
    ocVisualApplication,
    ocDataExchangeBase,
    ocDataExchangeExtra,
    MyBRepTools,
]
}).then(oc => {
    const path = [[-50, 0, 0], [50, 0, 0], [50, 100, 0]].map(([x, y, z]) => new oc.gp_Pnt_3(x, y, z));
    const makePolygon = new oc.BRepBuilderAPI_MakePolygon_3(path[0], path[1], path[2], true);
    const wire = makePolygon.Wire();
    const makeFace = new oc.BRepBuilderAPI_MakeFace_15(wire, false);
    const uvs = oc.MyBRepTools.MyUVBounds(makeFace.Face());
    console.log(uvs);
});

Output:

{
  uMax: 35.35533905932738,
  uMin: -64.64466094067264,
  vMax: 64.64466094067262,
  vMin: -35.35533905932739
}
sgenoud commented 3 years ago

Thanks, I will look into this. I will keep you posted - but I will not have time in the next couple of days!

donalffons commented 3 years ago

I updated the code in my previous post with a tested working example. Keep in mind though that the API of the beta builds is not yet stable and might change...

sgenoud commented 3 years ago

Thanks a lot, it works! It also makes clearer to be how awsome your docker based build system is!

donalffons commented 3 years ago

Nice! Happy to hear that it works for you!

donalffons commented 3 years ago

There is now "native" support for references to built-in types, as described here.

If you experience any issues with that, please let me know. Cheers.