<jupyter_start><jupyter_text>Voxel painter in pythreejsBased on: [voxel painter](http://threejs.org/examples/webgl_interactive_voxelpainter.html) ([source](https://github.com/mrdoob/three.js/blob/master/examples/webgl_interactive_voxelpainter.html))TODO:     - Have rollOver helper snap to other voxel face    - Delete voxel when shift-clicked<jupyter_code>from __future__ import division, print_function
from pythreejs import *
from IPython.display import display
from ipywidgets import HTML
from traitlets import link
import random
import math
import numpy as np<jupyter_output><empty_output><jupyter_text>Geometry definitions<jupyter_code>csize = 50 # size of voxel
nx = ny = 10
stepx, stepy = csize, csize
sizex, sizey = nx * csize, ny * csize<jupyter_output><empty_output><jupyter_text>Helper functions<jupyter_code>def normalize(list):
    """ 
    Normalize vector list 
    """
    return [x/sum(list) for x in list]

def rotation_matrix(angle, axis='x'):
    """ 
    Return rotation matrix as list of rows used in the
    Object3d.quaternion_from_rotation() class method
    """
    if axis in ['x','y','z']:
        sin = math.sin(angle)
        cos = math.cos(angle)
        # counter-clockwise rotation in yz-plane
        if axis is 'x':
            return [1, 0, 0, 0, cos, -sin, 0, sin, cos]
        # counter-clockwise rotation in xz-plane
        elif axis is 'y':
            return [cos, 0, sin, 0, 1, 0, -sin, 0, cos]
        # counter-clockwise rotation in xy-plane
        elif axis is 'z':
            return [cos, -sin, 0, sin, cos, 0, 0, 0, 1]
    else:
        raise ValueError('Cannot rotate about %s axis' % axis)<jupyter_output><empty_output><jupyter_text>Scene objects:  Voxels<jupyter_code>cube_tex = ImageTexture(
    imageUri = 'textures/square-outline-textured.png'
)

cube_geo = BoxGeometry(
    width = csize,
    height = csize,
    depth = csize
)

cube_mat = MeshLambertMaterial(
    color = '#feb74c',
    shading = 'FlatShading',
    map = cube_tex
)<jupyter_output><empty_output><jupyter_text>Roll-over helper<jupyter_code>rollOver_geo = cube_geo

rollOver_mat = MeshBasicMaterial(
    color = '#ff0000',
    opacity = 0.5,
    transparent = True
)

rollOver_point = Mesh(
    geometry = rollOver_geo,
    material = rollOver_mat,
    visible = False,
)<jupyter_output><empty_output><jupyter_text>Surface<jupyter_code>surf_geo = SurfaceGeometry(
    z = [0] * (nx + 1) * (ny + 1),
    width = sizex,
    height = sizey,
    width_segments = nx,
    height_segments = ny,
)
surf_grid = SurfaceGrid(
    geometry = surf_geo,
    material = LineBasicMaterial(
        color = '#000000',
        opacity = 0.2,
        transparent = True,
    ),
)

surface = Mesh(
    geometry = surf_geo,
    material = MeshBasicMaterial(
        color = '#00ff00',
        opacity = 0.3,
        transparent = True,
        side = 'DoubleSide',
        
    ),
    visible = True
)

m = rotation_matrix(-math.pi/2)
surface.setRotationFromMatrix(m)
surf_grid.setRotationFromMatrix(m)<jupyter_output><empty_output><jupyter_text>Pickers (raycasting)<jupyter_code>pickable_objects = Group()
pickable_objects.add(surface)
click_picker = Picker(
    controlling = pickable_objects,
    event = 'dblclick'
)
mousemove_picker = Picker(
    controlling = pickable_objects,
    event = 'mousemove'
)
def map_to_grid(value):
    """
    Convert continous to discrete coordinates
    """
    # limit position to positive y-axis
    if value[1] < 0: 
        value[1] = float(0)
        
    # limit to discrete steps based on cube size
    pos = [int(x//csize*csize+csize/2) for x in value]

    # if block already exist at this position, shift up
    while tuple(pos) in objects.keys():
        pos[1] += csize
    return pos

def map_to_voxel_side(voxel, face):
    """
    Convert continous to discrete coordinates
    """
    # limit to discrete steps based on cube size
    pos = list(voxel.position)
    print(face)
    if face in (0, 1):
        pos[0] += csize
    elif face in (2, 3):
        pos[0] -= csize
    elif face in (4, 5):
        pos[1] += csize
    elif face in (6, 7):
        pos[1] -= csize
    elif face in (8, 9):
        pos[2] += csize
    elif face in (10, 11):
        pos[2] -= csize

    # if block already exist at this position, mark as invalid
    while tuple(pos) in objects.keys():
        return None
    return pos
objects = {} # contains all voxels added to the scene

def on_surf_click(change):
    """
    Create new object when mouse is clicked on surface
    """
    # convert position to discrete coordinates
    pos = map_to_grid(change.owner.point)
        
    # create new object
    voxel = Mesh(
        geometry = cube_geo,
        material = cube_mat,
        position = pos
    )
    
    # add new object to scene and object list
    pickable_objects.add(voxel)
    objects[tuple(map(int, voxel.position))] = voxel
    
def on_voxel_click(change):
    """
    Create new object when mouse is clicked on voxel
    """
    old_voxel = change.owner.object
    if change.owner.modifiers[0] is True:
        pickable_objects.remove(old_voxel)
        del objects[tuple(map(int, old_voxel.position))]
        return
    # convert position to discrete coordinates
    pos = map_to_voxel_side(old_voxel, change.owner.faceIndex)
    if pos is None:
        return
        
    # create new object
    voxel = Mesh(
        geometry = cube_geo,
        material = cube_mat,
        position = pos
    )
    
    # add new object to scene and object list
    pickable_objects.add(voxel)
    objects[tuple(map(int, voxel.position))] = voxel
coord_html = HTML("Coords: ()")
def on_surf_mousemove(change):
    """
    Show rollOver helper on mousemove
    """
    rollOver_point.visible = True
    
    # convert to discrete coordinates
    pos = map_to_grid(change.owner.point)
    
    # update rollOver helper position
    rollOver_point.position = pos
    
    # write coordinates to html container
    coord_html.value = "Coords: (%d, %d, %d)" % tuple(pos)

def on_voxel_mousemove(change):
    """
    Show rollOver helper on mousemove on voxel
    """
    # convert to discrete coordinates
    voxel = change.owner.object
    pos = map_to_voxel_side(voxel, change.owner.faceIndex)
    if pos is None:
        rollOver_point.visible = False
        return
    rollOver_point.visible = True
    
    # update rollOver helper position
    rollOver_point.position = pos

def on_click(change):
    if change.owner.object is surface:
        on_surf_click(change)
    else:
        on_voxel_click(change)
        
def on_mousemove(change):
    if change.owner.object is None:
        # Hide rollOver if outside
        rollOver_point.visible = False
    elif change.owner.object is surface:
        on_surf_mousemove(change)
    else:
        on_voxel_mousemove(change)
click_picker.observe(on_click, names=['point'])
mousemove_picker.observe(on_mousemove, names=['faceIndex'])<jupyter_output><empty_output><jupyter_text>Camera and scene<jupyter_code>width = 800
height = 600
camera = PerspectiveCamera(
    position = [500, 800, 1300],
    aspect = width/height,
    fov = 35, 
    near = 1,
    far = 10000
)
camera.lookAt([0,0,0])
scene = Scene(
    children = [
        pickable_objects,
        surf_grid,
        rollOver_point,
        AmbientLight(
            color = '#606060'
        ),
        DirectionalLight(
            color = '#ffffff',
            position = normalize([1, 0.75, 0.5]),
            intensity = 0.5
        )
    ]
)<jupyter_output><empty_output><jupyter_text>Render the scene<jupyter_code>renderer = Renderer(
    camera = camera,
    scene = scene,
    controls = [
        OrbitControls(
            controlling = camera
        ),
        click_picker,
        mousemove_picker,
    ],
    background = '#f0f0f0',
    antialias = True,
    width=width,
    height=height,
)
display(coord_html, renderer)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.10232147363108755, 'fraction_numerical': 0.014761544284632853, 'mean_word_length': 2.794638583054093, 'pattern_counts': {'":': 0, '<': 38, '<?xml version=': 0, '>': 37, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '9', 'dir': 'jupyter-structured-clean-dedup', 'id': '389307', 'license': 'BSD-3-Clause', 'n_tokens_mistral': 2722, 'n_tokens_neox': 2598, 'n_words': 658, 'path': 'examples/threejs/voxel_painter.ipynb', 'repo_name': 'mariobuikhuizen/pythreejs', 'size': '7926'}