Issue when porting an example sketch from the Hemesh library to Python mode

Hi,

For some reason I get a ClassCastException error when porting this sketch (code below) to Python mode.

processing.app.SketchException: java.lang.ClassCastException: wblut.geom.WB_Triangle cannot be cast to wblut.geom.WB_Coord

For comparison, the original code:

import wblut.math.*;
import wblut.processing.*;
import wblut.core.*;
import wblut.hemesh.*;
import wblut.geom.*;


HE_Mesh mesh;
WB_Render render;

void setup() {
  size(1000,1000,P3D);
  smooth(8);

// Create an isosurface from an explicit grid of values.
// Potentially uses a lot of memory.

  float[][][] values=new float[51][51][51];
  for (int i = 0; i < 51; i++) {
    for (int j = 0; j < 51; j++) {
      for (int k = 0; k < 51; k++) {
        values[i][j][k]=noise(0.07*i, 0.07*j, 0.07*k);
      }
    }
  }

  HEC_IsoSurface creator=new HEC_IsoSurface();
  creator.setResolution(50, 50,50);// number of cells in x,y,z direction
  creator.setSize(8, 8, 8);// cell size
  creator.setValues(values);// values corresponding to the grid points
  // values can also be double[][][]
  creator.setIsolevel(.6);// isolevel to mesh
  creator.setInvert(false);// invert mesh
  creator.setBoundary(-200);// value of isoFunction outside grid
  // use creator.clearBoundary() to rest boundary values to "no value".
  // A boundary value of "no value" results in an open mesh
  
  //Gamma controls level of grid snap, 0.0-0.5. Can improve the 
  //quality of the triangles, but can give small changes in topology.
  creator.setGamma(0.3); 
  

  mesh=new HE_Mesh(creator);

  render=new WB_Render(this);
}

void draw() {
  background(55);
  lights();
  translate(width/2, height/2);
  rotateY(mouseX*1.0f/width*TWO_PI);
  rotateX(mouseY*1.0f/height*TWO_PI);
  noStroke();
  render.drawFaces(mesh);
  stroke(0);
  render.drawEdges(mesh);
}

In Python:

add_library('hemesh')

def setup():
    size(1000, 600, P3D)
    smooth(8)
    
    locations = [[[noise(0.07 * x, 0.07*y, 0.07*z) for z in range(51)] for y in range(51)] for x in range(51)]
    
    creator =  WB_IsoSurface()
    creator.setSize(8, 8, 8)
    creator.setValues(locations)
    creator.setIsolevel(.6)
    creator.setInvert(False)
    creator.setBoundary(-200)
    creator.setGamma(.3)
    
    global triangles, render
    triangles = creator.getTriangles()
    render = WB_Render(this)
    
def draw():
    background(12)
    
    render.drawTriangle(triangles)

Do you have an idea of what could possibly be the issue here ?

1 Like

Your port is using WB_IsoSurface instead of HEC_IsoSurface used by the original sketch.

I still haven’t dug why your WB_IsoSurface fails.
But I did a Python Mode port of the original using HEC_IsoSurface.

Actually I did 2 versions: A java-ish and a pythonic.

The java-ish 1 uses an actual double[][][] array w/ a triple loop.
While the pythonic 1 uses a 3d list comprehension as you did too.

“Ref_HEC_IsoSurface_Javaish.pyde”:

"""
 Ref_HEC_IsoSurface (Java-ish version) (v1.0.1)
 ported by GoToLoop (2019-Feb-21)

 https://Discourse.Processing.org/t/
 issue-when-porting-an-example-sketch-from-the-
 hemesh-library-to-python-mode/8570/2

 https://GitHub.com/wblut/HE_Mesh/blob/master/examples/
 hemesh/create/Ref_HEC_IsoSurface/Ref_HEC_IsoSurface.pde
"""

add_library('hemesh')

from java.lang.reflect import Array
from java.lang import Double

DIM, N = 51, .07
DRANGE = tuple(range(DIM))

BG, FG = 070, 0
FPS = 'FPS: '

def setup():
    size(800, 700, P3D)
    smooth(8)

    creator = HEC_IsoSurface()

    creator.setResolution(50, 50, 50)
    creator.setSize(8, 8, 8)
    creator.setIsolevel(.6)
    creator.setBoundary(-200)
    creator.setGamma(.3)
    creator.setInvert(False)

    arr3d = Array.newInstance(Double.TYPE, DIM, DIM, DIM)
    creator.setValues(arr3d)

    for z, arr2d in enumerate(arr3d):
        for y, arr1d in enumerate(arr2d):
            for x in DRANGE: arr1d[x] = noise(N*x, N*y, N*z)

    global render, mesh
    render, mesh = WB_Render(this), HE_Mesh(creator)


def draw():
    background(BG)
    lights()

    translate(width>>1, height>>1)
    rotateY(TAU * mouseX / width)
    rotateX(TAU * mouseY / height)

    noStroke()
    render.drawFaces(mesh)

    stroke(FG)
    render.drawEdges(mesh)

    this.surface.title = FPS + `this.round(frameRate)`

“Ref_HEC_IsoSurface_Pythonic.pyde”:

"""
 Ref_HEC_IsoSurface (Pythonic version) (v1.0.1)
 ported by GoToLoop (2019-Feb-21)

 https://Discourse.Processing.org/t/
 issue-when-porting-an-example-sketch-from-the-
 hemesh-library-to-python-mode/8570/2

 https://GitHub.com/wblut/HE_Mesh/blob/master/examples/
 hemesh/create/Ref_HEC_IsoSurface/Ref_HEC_IsoSurface.pde
"""

add_library('hemesh')

DIM, N = 51, .07
DRANGE = tuple(range(DIM))

BG, FG = 070, 0
FPS = 'FPS: '

def setup():
    size(800, 700, P3D)
    smooth(8)

    creator = HEC_IsoSurface()

    creator.setResolution(50, 50, 50)
    creator.setSize(8, 8, 8)
    creator.setIsolevel(.6)
    creator.setBoundary(-200)
    creator.setGamma(.3)
    creator.setInvert(False)

    creator.setValues(
        [[[noise(N*x, N*y, N*z) for z in DRANGE]
        for y in DRANGE] for x in DRANGE]
    )

    global render, mesh
    render, mesh = WB_Render(this), HE_Mesh(creator)


def draw():
    background(BG)
    lights()

    translate(width>>1, height>>1)
    rotateY(TAU * mouseX / width)
    rotateX(TAU * mouseY / height)

    noStroke()
    render.drawFaces(mesh)

    stroke(FG)
    render.drawEdges(mesh)

    this.surface.title = FPS + `this.round(frameRate)`
3 Likes

Sweet, clever workaround ! (Thanks)

The ClassCastException error from the getTriangles() function is still a mystery though.

Do you have a Java Mode example using WB_IsoSurface? :grinning:

1 Like

I just realized the Java example sketch I posted is different from the one in the Hemesh examples folder on my computer (the one with WB_IsoSurface that I was trying to port to Python mode !). Here it is:

List<WB_Triangle> triangles;
WB_Render render;

void setup() {
  size(1000,1000,P3D);
  smooth(8);

  float[][][] values=new float[51][51][51];
  for (int i = 0; i < 51; i++) {
    for (int j = 0; j < 51; j++) {
      for (int k = 0; k < 51; k++) {
        values[i][j][k]=noise(0.07*i, 0.07*j, 0.07*k);
      }
    }
  }

  WB_IsoSurface creator=new WB_IsoSurface();
  creator.setSize(8, 8, 8);
  creator.setValues(values);

  creator.setIsolevel(.6);
  creator.setInvert(false);
  creator.setBoundary(-200);// value outside grid
  // use creator.clearBoundary() to rest boundary values to "no value".
  // A boundary value of "no value" results in an open mesh
  
  //Gamma controls level of grid snap, 0.0-0.5. Can improve the 
  //quality of the triangles, but can give small changes in topology.
  //For 3D, gamma=0.3 is a good value.
  creator.setGamma(0.3); 
  
  triangles=creator.getTriangles();
  render=new WB_Render(this);
}

void draw() {
  background(55);
  lights();
  translate(width/2, height/2);
  rotateY(mouseX*1.0f/width*TWO_PI);
  rotateX(mouseY*1.0f/height*TWO_PI);
  fill(255);
  stroke(0);
  render.drawTriangle(triangles);
}

WB_Render3D::drawTriangle() got 6 overloaded versions of it:

And outta those 6, there are 2 w/ the 1 parameter signature that is a container:

And 1 w/ 1 parameter that is a single WB_Triangle:

Method WB_IsoSurface::getTriangles() returns a List<WB_Triangle>:

But if we call print type(triangles), we’ll find out its actual datatype is:
<type 'org.eclipse.collections.impl.list.mutable.FastList'>

Eclipse.org/collections/javadoc/9.0.0/org/eclipse/collections/impl/list/mutable/FastList.html

Now it seems like when we invoke render.drawTriangle(triangles), the overloaded signature chosen is:

Which causes processing.app.SketchException: java.lang.ClassCastException: wblut.geom.WB_Triangle cannot be cast to wblut.geom.WB_Coord

But what we’re looking for is this 1:

I’ve tried searching for some Jython builtin way to force a method invocation to pick a specific signature.

However, if there’s 1, it’s unpublished or it’s almost impossible to find!
As a workaround, I’ve attempted Java’s reflection capabilities.

  1. Class::getMethod():
    Docs.Oracle.com/en/java/javase/11/docs/api/java.base/java/lang/Class.html#getMethod(java.lang.String,java.lang.Class...)
  2. Method::invoke():
    Docs.Oracle.com/en/java/javase/11/docs/api/java.base/java/lang/reflect/Method.html#invoke(java.lang.Object,java.lang.Object...)

There’s a subtle diff. between the container parameter type signatures.
The 1 for WB_Coord is a List, but the 1 for WB_Triangle is a Collection.

So in order to grab the overloaded method signature we need we do like this:
method = WB_Render.getMethod('drawTriangle', Collection)

Now, inside draw(), we replace render.drawTriangle(triangles) w/ method.invoke(render, triangles).

Here’s the full sketch solution: :angel:

"""
 Ref_WB_IsoSurface (v1.0.1)
 ported by GoToLoop (2019-Feb-21)

 https://Discourse.Processing.org/t/
 issue-when-porting-an-example-sketch-from-the-
 hemesh-library-to-python-mode/8570/6
 
 https://GitHub.com/wblut/HE_Mesh/blob/master/examples/
 geom/isosurface/Ref_WB_IsoSurface/Ref_WB_IsoSurface.pde
"""

add_library('hemesh')

from java.util import Collection
METHOD = WB_Render.getMethod('drawTriangle', Collection)

DIM, N = 51, .07
DRANGE = tuple(range(DIM))

BG, FG, FILL = 070, 0, -1
FPS = 'FPS: '

def setup():
    size(800, 700, P3D)
    smooth(8)

    fill(FILL)
    stroke(FG)

    creator = WB_IsoSurface()

    creator.setSize(8, 8, 8)
    creator.isolevel = .6
    creator.boundary = -200
    creator.gamma = .3
    creator.invert = False

    creator.setValues(
        [[[noise(N*x, N*y, N*z) for z in DRANGE]
        for y in DRANGE] for x in DRANGE]
    )

    global render, triangles
    render, triangles = WB_Render(this), creator.triangles


def draw():
    background(BG)
    lights()

    translate(width>>1, height>>1)
    rotateY(TAU * mouseX / width)
    rotateX(TAU * mouseY / height)

    # render.drawTriangle(triangles) # invokes wrong sig!
    METHOD.invoke(render, triangles)

    this.surface.title = FPS + `this.round(frameRate)`
3 Likes

Thanks a ton for the comprehensive explanation.

bravo :clap: :clap: :clap: