# Soya 3D
# Copyright (C) 2001-2002 Jean-Baptiste LAMY
#
# This program 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 2 of the License, or
# (at your option) any later version.
#
# This program 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 this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA


from editobj.observe import *
import editobj.custom as custom
import Tkinter, time, weakref
import soya, soya.opengl, soya.sdlconst, soya.cube as cube, soya.cursor as cursor

STEP    = 0.125
EPSILON = 0.001

# Additionnal key binding. (A dict mapping key ID to a lambda taking 2 args, the root world and the edited object).
KEY_BINDINGS = {}

class WorldEditor:
  def __init__(self, world, dialog):
    self.active              = 0
    self.dialog              = dialog
    self.handles             = []
    self.vertex_handles      = []
    self.children_visibility = { world : 1 }
    
    self.world = self.current = world
    
    self.scene  = soya.World()
    self.scene.name = "__scene__"
    self.scene.atmosphere = soya.Atmosphere()
    self.scene.atmosphere.ambient = (0.5, 0.5, 0.5, 1.0)
    
    self.camera = soya.Camera(self.scene)
    self.camera.set_xyz(0.0, 0.0, 5.0)
    self.camera.rotate_lateral(45)
    soya.set_root_widget(self.camera)
    
    self.light = soya.Light(self.scene)
    self.light.set_xyz(0.2, 2.0, 2.2)
    self.light.top_level = 1
    self.light.cast_shadow = 0
    
    self.cursor = Cursor(self.scene, self.camera, self.handles, self.light)
    self.cursor.name = "cursor"
    
    self.content = soya.World(self.scene)
    self.content.name = "__content__"
    self.content.append(world)
    
    self.add_handles_for(world)
    
    observe_tree(world, self.on_changed)
    
    self.last_click  = 0
    self.camera_mode = 0
    
  def add_handles_for(self, item):
    if   isinstance(item, soya.Sprite):
      self.handles.append(PositionHandle(self.content, self, item))
      
    elif isinstance(item, soya.Face):
      for vertex in item.vertices: self.add_handles_for_vertex(vertex)
      
    elif isinstance(item, soya.CoordSyst):
      self.handles.append(PositionHandle   (self.content, self, item))
      self.handles.append(OrientationHandle(self.content, self, item))
      
    if   isinstance(item, soya.World) and self.children_visibility.get(item, 0):
      for subitem in item.children: self.add_handles_for(subitem)
      
  def add_handles_for_vertex(self, vertex):
    if getattr(vertex, "immature", 0): return
    
    for handle in self.vertex_handles:
      if handle.is_for(vertex): return
    
    for handle in self.vertex_handles:
      v = handle.vertices[0]
      if (v.parent is vertex.parent) and (abs(v.x - vertex.x) < EPSILON) and (abs(v.y - vertex.y) < EPSILON) and (abs(v.z - vertex.z) < EPSILON):
        handle.add_vertex(vertex)
        return handle
    else:
      handle = VertexHandle(self.content, self, vertex)
      self.handles.append(handle)
      self.vertex_handles.append(handle)
      
  def remove_handles_of(self, item):
    for handle in self.handles[:]: handle.del_for(item)
    
    if   isinstance(item, soya.Face):
      for handle in self.vertex_handles[:]:
        for vertex in item.vertices: handle.del_for(vertex)
        
    elif isinstance(item, soya.World):
      for subitem in item: self.remove_handles_of(subitem)
      
  def select_handles_for(self, item):
    self.current = item
    for handle in self.handles:
      if handle.is_for(item): handle.select()
      
  def children_edited(self, world, visible):
    self.children_visibility[world] = visible
    if visible:
      for item in world.children:
        for handle in self.handles:
          if handle.is_for(item): continue
        self.add_handles_for(item)
    else:
      for item in world.children: self.remove_handles_of(item)
      
  def on_event(self, event):
    #print event
    if   event[0] == soya.sdlconst.MOUSEMOTION:
      if soya.get_mod() & soya.sdlconst.MOD_CTRL:
        self.cursor.grid_step = STEP
      else: self.cursor.grid_step = 0.0
      
      if self.camera_mode:
        self.camera.turn_lateral (float(event[3]))
        self.camera.turn_vertical(float(event[4]))
        
      x, y = event[1], event[2]
      if   x == 0:                                   self.camera += soya.Vector(self.camera, -0.1,  0.0, 0.0)
      elif x == self.camera.get_screen_width()  - 1: self.camera += soya.Vector(self.camera,  0.1,  0.0, 0.0)
      if   y == 0:                                   self.camera += soya.Vector(self.camera,  0.0,  0.1, 0.0)
      elif y == self.camera.get_screen_height() - 1: self.camera += soya.Vector(self.camera,  0.0, -0.1, 0.0)
      
      self.cursor.mouse_moved(x, y)
      
    elif event[0] == soya.sdlconst.MOUSEBUTTONDOWN:
      if event[1] < 3: # Left or middle button
        current = time.time()
        if current - self.last_click < 0.2:
          self.cursor.button_pressed(2, soya.get_mod() & soya.sdlconst.MOD_SHIFT) # Double-click is the same that middle click
        else:
          self.cursor.button_pressed(event[1], soya.get_mod() & soya.sdlconst.MOD_SHIFT)
        self.last_click = current
      elif event[1] == 3: self.camera_mode = 1
      
    elif event[0] == soya.sdlconst.MOUSEBUTTONUP:
      if self.camera_mode:
        if   event[1] == 3: self.camera_mode = 0
        elif event[1] == 4: self._zoom(-1.0)
        elif event[1] == 5: self._zoom( 1.0)
      else:
        if   event[1] <  3: self.cursor.button_released(event[1])
        elif event[1] == 4: self.cursor.mouse_rolled(-0.25)
        elif event[1] == 5: self.cursor.mouse_rolled( 0.25)
        
    elif event[0] == soya.sdlconst.KEYDOWN:
      # Page up, page down
      if   event[1] == 280: self.cursor.mouse_rolled(-1 - 9 * (soya.get_mod() & soya.sdlconst.MOD_SHIFT))
      elif event[1] == 281: self.cursor.mouse_rolled( 1 + 9 * (soya.get_mod() & soya.sdlconst.MOD_SHIFT))
      
      # Left, right, up, down
      elif event[1] == 276: self.camera += soya.Vector(self.camera, -1.0 - 9.0 * (soya.get_mod() & soya.sdlconst.MOD_SHIFT),  0.0, 0.0)
      elif event[1] == 275: self.camera += soya.Vector(self.camera,  1.0 + 9.0 * (soya.get_mod() & soya.sdlconst.MOD_SHIFT),  0.0, 0.0)
      elif event[1] == 273: self.camera += soya.Vector(self.camera,  0.0,  1.0 + 9.0 * (soya.get_mod() & soya.sdlconst.MOD_SHIFT), 0.0)
      elif event[1] == 274: self.camera += soya.Vector(self.camera,  0.0, -1.0 - 9.0 * (soya.get_mod() & soya.sdlconst.MOD_SHIFT), 0.0)
      
      # o : toggle ortho camera
      elif event[1] == 111:
        self.camera.ortho = not self.camera.ortho
        self.camera.fov = 60.0
        
      # +, - : zoom
      elif event[1] == 270: self._zoom(-1.0 - 9 * (soya.get_mod() & soya.sdlconst.MOD_SHIFT))
      elif event[1] == 269: self._zoom( 1.0 + 9 * (soya.get_mod() & soya.sdlconst.MOD_SHIFT))
      
      # 4, 6, 8, 2, 5, 0 : predefined views
      elif event[1] == 260:
        a, b = self.world.get_box()
        cameray, cameraz = self.camera.y, self.camera.z
        self.camera.set_identity()
        self.camera.set_xyz(min((a % self.scene).x, (b % self.scene).x) - 3.0, cameray, cameraz)
        self.camera.look_at(soya.Vector(None,  1.0,  0.0,  0.0))
      elif event[1] == 262:
        a, b = self.world.get_box()
        cameray, cameraz = self.camera.y, self.camera.z
        self.camera.set_identity()
        self.camera.set_xyz(max((a % self.scene).x, (b % self.scene).x) + 3.0, cameray, cameraz)
        self.camera.look_at(soya.Vector(None, -1.0,  0.0,  0.0))
      elif event[1] == 264:
        a, b = self.world.get_box()
        camerax, cameraz = self.camera.x, self.camera.z
        self.camera.set_identity()
        self.camera.set_xyz(camerax, max((a % self.scene).y, (b % self.scene).y) + 3.0, cameraz)
        self.camera.look_at(soya.Vector(None,  0.0, -1.0,  0.0))
      elif event[1] == 258:
        a, b = self.world.get_box()
        camerax, cameraz = self.camera.x, self.camera.z
        self.camera.set_identity()
        self.camera.set_xyz(camerax, min((a % self.scene).y, (b % self.scene).y) - 3.0, cameraz)
        self.camera.look_at(soya.Vector(None,  0.0,  1.0,  0.0))
      elif event[1] == 261:
        a, b = self.world.get_box()
        camerax, cameray = self.camera.x, self.camera.y
        self.camera.set_identity()
        self.camera.set_xyz(camerax, cameray, min((a % self.scene).z, (b % self.scene).z) - 3.0)
        self.camera.look_at(soya.Vector(None,  0.0,  0.0,  1.0))
      elif event[1] == 256:
        a, b = self.world.get_box()
        camerax, cameray = self.camera.x, self.camera.y
        self.camera.set_identity()
        self.camera.set_xyz(camerax, cameray, max((a % self.scene).z, (b % self.scene).z) + 3.0)
        self.camera.look_at(soya.Vector(None,  0.0,  0.0, -1.0))
        
      # q, t : new quad, new triangle
      elif event[1] == 113:
        if hasattr(self.current, "children"): into = self.current
        else:                                 into = self.current.parent
        into.append(soya.Quad())
      elif event[1] == 116:
        if hasattr(self.current, "children"): into = self.current
        else:                                 into = self.current.parent
        into.append(soya.Triangle())
      else:
        if KEY_BINDINGS.has_key(event[1]):
          KEY_BINDINGS[event[1]](self.world, self.current)
          
  def _zoom(self, z):
    if self.camera.ortho: self.camera.fov = self.camera.fov * (1.0 + z / 10.0)
    else:                 self.camera += soya.Vector(self.camera, 0.0, 0.0, z)
      
  def render(self):
    if self.active:
      self.scene.begin_round()
      self.scene.advance_time(1.0)
      self.scene.begin_round()
      self.scene.advance_time(1.0)
      self.scene.begin_round()
      self.scene.advance_time(1.0)
      self.scene.begin_round()
      self.scene.advance_time(1.0)
      self.scene.begin_round()
      self.scene.advance_time(1.0)
      soya.render()
      
  def activate(self, event = None):
    if not self.active:
      self.active = 1
      soya.set_root_widget(self.camera)
      soya.cursor_set_visible(0)
      self.auto_render()
      
  def auto_render(self):
    for event in soya.coalesce_motion_event(soya.process_event()): self.on_event(event)
    self.render()
    self.cancel = self.dialog.after(150, self.auto_render)
    
  def deactivate(self, event = None):
    if self.active:
      self.active = 0
      soya.cursor_set_visible(1)
      self.dialog.after_cancel(self.cancel)
      
  def on_changed(self, obj, type, new, old):
    if type is list:
      for item in new:
        if not item in old:
          self.add_handles_for(item)
          if isinstance(item, soya.Face):
            for vertex in item.vertices:
              if getattr(vertex, "immature", 0):
                self.cursor.manage_click(FaceClickManager(self, item))
                break
          elif hasattr(item.__class__, "__clickmanager__"):
            self.cursor.manage_click(item.__class__.__clickmanager__(self, item))
            
      for item in old:
        if not item in new: self.remove_handles_of(item)
        
    else:
      if (not hasattr(obj, "children")) and (not hasattr(obj, "items")):
        unobserve_tree(obj, self.on_changed)
        
RED    = soya.Material(); RED   .diffuse = (1.0, 0.0, 0.0, 1.0)
GREEN  = soya.Material(); GREEN .diffuse = (0.0, 1.0, 0.0, 1.0)
BLUE   = soya.Material(); BLUE  .diffuse = (0.0, 0.0, 1.0, 1.0)
YELLOW = soya.Material(); YELLOW.diffuse = (1.0, 1.0, 0.0, 1.0)

RED_HANDLE = GREEN_HANDLE = BLUE_HANDLE = YELLOW_HANDLE = None

def build_handles():
  global RED_HANDLE, GREEN_HANDLE, BLUE_HANDLE, YELLOW_HANDLE
  
  red_cube    = cube.Cube(None, RED);    red_cube   .scale(STEP, STEP, STEP)
  green_cube  = cube.Cube(None, GREEN);  green_cube .scale(STEP, STEP, STEP)
  blue_cube   = cube.Cube(None, BLUE);   blue_cube  .scale(STEP, STEP, STEP)
  yellow_cube = cube.Cube(None, YELLOW); yellow_cube.scale(STEP, STEP, STEP)
  
  RED_HANDLE    = red_cube   .shapify()
  GREEN_HANDLE  = green_cube .shapify()
  BLUE_HANDLE   = blue_cube  .shapify()
  YELLOW_HANDLE = yellow_cube.shapify()
  
build_handles()

class Cursor(cursor.Cursor):
  def __init__(self, parent = None, camera = None, handles = None, light = None):
    cursor.Cursor.__init__(self, parent, camera, GREEN_HANDLE)
    
    self.handles        = handles
    self.draging        = 0
    self.light          = light
    self.click_managers = []
    
  def manage_click(self, click_manager):
    self.click_managers.append(click_manager)
    
  def move(self, pos): self.add_vector(self >> pos)
    
  def add_vector(self, dep):
    cursor.Cursor.add_vector(self, dep)
    for handle in self.handles: handle.cursor_moved(self, self.is_near, dep)
    self.light.move(self)
    
    if self.click_managers:
      if not self.click_managers[-1].on_motion(self): del self.click_managers[-1]
      
  __iadd__ = add_vector
  
  def is_near(self, position): return position.distance_to(self) < STEP
  def near(self, a, b): return ((self.distance_to(a) < self.distance_to(b)) and a) or b
  
  def button_pressed(self, button, multiselect = 0):
    if   button == 1:
      pred = self.is_near
      print "%s, %s, %s" % (self.x, self.y, self.z)
    else:             pred = self.is_under_tester(STEP)
    
    if self.click_managers:
      if not self.click_managers[-1].on_click(self): del self.click_managers[-1]
    else:
      handles = filter(pred, self.handles)
      if handles:
        handle = min(map(lambda handle: (self.distance_to(handle), handle), handles))[1]
        # move the cursor at the handle's Z (in the camera coordinate system)
        mouse = self % self.camera
        self.mouse_rolled((handle % self.camera).z - mouse.z)
        handle.select()
        
        if not multiselect:
          for h in self.handles:
            if not h is handle: h.highlight(0)
      else:
        for h in self.handles: h.highlight(0)
        
      self.draging = 1
        
  def button_released(self, button):
    for handle in self.selected_handles():
      handle.cursor_endmove(self)
      
    if self.click_managers:
      if not self.click_managers[-1].on_release(self): del self.click_managers[-1]
    else: self.draging = 0
    
  def selected_handles        (self): return filter(lambda handle: handle.selected   , self.handles)
  def highlighted_handles     (self): return filter(lambda handle: handle.highlighted, self.handles)
  def highlighted_only_handles(self): return filter(lambda handle: handle.highlighted and not handle.selected, self.handles)
  def nearest_selected_handle(self):
    selected_handles = self.selected_handles()
    return selected_handles and min(map(lambda handle: (self.distance_to(handle), handle), selected_handles))[1]
  def over_handle(self):
    handles = self.highlighted_only_handles()
    return handles and min(map(lambda handle: (self.distance_to(handle), handle), handles))[1]
    
class ClickManager:
  def on_motion(self, cursor):
    """Called when the cursor is moved. Must returns true if this manager is still usefull, or false if this manager's task is achieved."""
    return 1
  def on_click(self, cursor):
    """Called when the cursor is clicked. Must returns true if this manager is still usefull, or false if this manager's task is achieved."""
    return 1
  def on_release(self, cursor):
    """Called when the cursor is released. Must returns true if this manager is still usefull, or false if this manager's task is achieved."""
    return 1
  
class FaceClickManager(ClickManager):
  def __init__(self, editor, face):
    self.editor = editor
    self.face   = face
    
  def on_motion(self, cursor):
    i = 0
    while not getattr(self.face.vertices[i], "immature", 0): i = i + 1
    
    while i < len(self.face.vertices):
      self.face.vertices[i].parent = self.face.parent
      self.face.vertices[i].move(cursor)
      i = i + 1
      
    return 1
  
  def on_click(self, cursor):
    i = 0
    while not getattr(self.face.vertices[i], "immature", 0): i = i + 1
    
    vertex = self.face.vertices[i]
    del vertex.immature
    
    handle = cursor.over_handle()
    if handle:
      if isinstance(handle, VertexHandle):
        handle.add_vertex(self.face.vertices[i])
      else:
        self.face.vertices[i].parent = self.face.parent
        self.face.vertices[i].move(handle)
        
        self.editor.add_handles_for_vertex(vertex)
    else:
      self.face.vertices[i].parent = self.face.parent
      self.face.vertices[i].move(cursor)
      
      self.editor.add_handles_for_vertex(vertex)
      
    return i < len(self.face.vertices) - 1
  
class MoveClickManager(ClickManager):
  def __init__(self, editor, item):
    self.editor = editor
    self.item   = item
    
  def on_motion(self, cursor):
    self.item.move(cursor)
    return 1
  
  def on_click(self, cursor):
    handle = cursor.over_handle()
    if handle:
      self.item.move(handle)
    else:
      self.item.move(cursor)
      
    return 0

# List of 3D items' classes that are positionned at the next mouse click's position.

class Handle(soya.Volume):
  def __init__(self, parent, editor = None):
    soya.Volume.__init__(self, parent, self.NATURAL)
    self.editor      = editor
    self.selected    = 0
    self.highlighted = 0
    self.ideal       = soya.Point(parent)
    
  def del_for(self, item):
    if self.is_for(item):
      self.parent.remove(self)
      self.editor.handles.remove(self)
      return 1
    
  def highlight(self, value = 1):
    self.highlighted = value
    if value:
      self.set_shape(GREEN_HANDLE)
    else:
      if self.selected: self.select(0)
      self.set_shape(self.NATURAL)
      
  def select(self, value = 1):
    self.selected = value
    if value:
      if not self.highlighted: self.highlight(1)
      
  def cursor_moved(self, cursor, pred, dep):
    if self.selected:
      if cursor.draging:
        self.ideal += dep
        if cursor.grid_step:
          pos   = self.ideal.copy()
          step  = cursor.grid_step
          d     = step / 2.0
          pos.x = ((pos.x + d) // step) * step
          pos.y = ((pos.y + d) // step) * step
          pos.z = ((pos.z + d) // step) * step
          self.move(pos)
        else: self.move(self.ideal)
    else:
      if self.highlighted:
        if not pred(self): self.highlight(0)
      elif pred(self): self.highlight()
      
  def cursor_endmove(self, cursor): pass
  

class PositionHandle(Handle):
  NATURAL = BLUE_HANDLE
  def __init__(self, parent, editor, position):
    Handle.__init__(self, parent, editor)
    
    self.position = position
    self.ideal.move(position)
    Handle.move(self, position)
    
    observe(position, self.on_changed)
    world = position.parent
    while world:
      observe(world, self.on_parent_changed)
      world = world.parent
      
  def is_for (self, item): return item is self.position
  
  def move(self, position):
    Handle.move(self, position)
    self.position.move(position)
    
  def add_vector(self, vector):
    Handle.add_vector(self, vector)
    self.position.add_vector(vector)
  __iadd__ = add_vector
  
  def on_changed(self, obj, type, new, old):
    if type is object:
      if (new["x"] != old["x"]) or (new["y"] != old["y"]) or (new["z"] != old["z"]):
        Handle.move(self, obj)
        self.ideal.move(obj)
      if new["parent"] != old["parent"]:
        self.update_hierarchy(new["parent"], old["parent"])
        Handle.move(self, self.position)
        self.ideal.move(obj)
        
  def on_parent_changed(self, obj, type, new, old):
    if type is object:
      if (new["x"] != old["x"]) or (new["y"] != old["y"]) or (new["z"] != old["z"]):
        Handle.move(self, self.position)
        self.ideal.move(self.position)
      if new["parent"] != old["parent"]:
        self.update_hierarchy(new["parent"], old["parent"])
        Handle.move(self, self.position)
        self.ideal.move(self.position)
        
  def update_hierarchy(self, newparent, oldparent):
    world = oldparent
    while world:
      unobserve(world, self.on_parent_changed)
      world = world.parent
      
    world = newparent
    while world:
      observe(world, self.on_parent_changed)
      world = world.parent
      
  def __repr__(self): return "<PositionHandle for %s>" % (self.position,)
  
class VertexHandle(Handle):
  NATURAL = RED_HANDLE
  def __init__(self, parent, editor, vertex):
    Handle.__init__(self, parent, editor)
    self.vertices = [vertex]
    Handle.move(self, vertex)
    self.ideal.move(vertex)
    
    observe(vertex, self.on_changed)
    world = vertex.parent
    while world:
      observe(world, self.on_parent_changed)
      world = world.parent
      
  def is_for(self, item):
    for vertex in self.vertices:
      if vertex is item: return 1
    return 0
  
  def del_for(self, item):
    for vertex in self.vertices:
      if vertex is item:
        self.vertices.remove(vertex)
        unobserve(vertex, self.on_changed)
        if not self.vertices:
          self.parent.remove(self)
          self.editor.handles.remove(self)
          self.editor.vertex_handles.remove(self)
          
  def del_for_all(self):
    for vertex in self.vertices:
      unobserve(vertex, self.on_changed)
      
    self.parent.remove(self)
    self.editor.handles.remove(self)
    
  def move(self, position):
    Handle.move(self, position)
    for vertex in self.vertices: vertex.move(position)
    
  def add_vector(self, vector):
    Handle.add_vector(self, vector)
    for vertex in self.vertices:
      vertex.add_vector(vector)
  __iadd__ = add_vector
  
  def on_changed(self, obj, type, new, old):
    if type is object:
      if (new["x"] != old["x"]) or (new["y"] != old["y"]) or (new["z"] != old["z"]):
        if len(self.vertices) == 1:
          Handle.move(self, obj)
          self.ideal.move(obj)
        else:
          self.give_up_vertex(obj)
          
      if new["parent"] != old["parent"]: self.give_up_vertex(obj)
      
  def on_parent_changed(self, obj, type, old, new):
    if type is object:
      if (new["x"] != old["x"]) or (new["y"] != old["y"]) or (new["z"] != old["z"]):
        Handle.move(self, self.vertices[0])
        self.ideal.move(self.vertices[0])
      elif new["parent"] != old["parent"]:
        self.update_hierarchy(new["parent"], old["parent"])
        Handle.move(self, self.vertices[0])
        self.ideal.move(self.vertices[0])
      
  def update_hierarchy(self, newparent, oldparent):
    world = oldparent
    while world:
      unobserve(world, self.on_parent_changed)
      world = world.parent
      
    world = newparent
    while world:
      observe(world, self.on_parent_changed)
      world = world.parent
      
  def add_vertex(self, vertex):
    vertex.parent = self.vertices[0].parent
    vertex.move(self)
    self.vertices.append(vertex)
    observe(vertex, self.on_changed)
    
  def give_up_vertex(self, vertex):
    self.del_for(vertex)
    self.editor.add_handles_for_vertex(vertex)
    
  def cursor_endmove(self, cursor):
    handle = cursor.over_handle()
    if handle:
      if isinstance(handle, VertexHandle):
        self.del_for_all()
        for vertex in self.vertices: handle.add_vertex(vertex)
      else:
        self.move(handle)
        
  def __repr__(self): return "<VertexHandle for %s>" % (self.vertices,)

class OrientationHandle(Handle):
  NATURAL = YELLOW_HANDLE
  def __init__(self, parent, editor, orientation):
    Handle.__init__(self, parent, editor)
    
    self.orientation = orientation
    self.place()
    self.ideal.move(soya.Point(orientation, 0.0, 0.0, -1.0))
    
    observe(orientation, self.on_changed)
    world = orientation.parent
    while world:
      observe(world, self.on_parent_changed)
      world = world.parent
      
  def is_for (self, item): return item is self.orientation
  
  def place(self): Handle.move(self, soya.Point(self.orientation, 0.0, 0.0, -1.0))
    
  def move(self, position):
    if soya.get_mod() & soya.sdlconst.MOD_SHIFT:
      position = soya.Point(position.parent, position.x, position.y, position.z)
      position.convert_to(self.orientation.parent)
      position.y = self.orientation.y
    self.orientation.look_at(position)
    self.place()
    
  def add_vector(self, vector):
    self.move(self.position() + vector)
  __iadd__ = add_vector
  
  def on_changed(self, obj, type, new, old):
    if type is object:
      if (new["x"] != old["x"]) or (new["y"] != old["y"]) or (new["z"] != old["z"]):
        self.place()
        self.ideal.move(self)
      if new["parent"] != old["parent"]:
        self.update_hierarchy(new["parent"], old["parent"])
        self.ideal.move(self)
        self.place()
      
  def on_parent_changed(self, obj, type, new, old):
    if type is object:
      if (new["x"] != old["x"]) or (new["y"] != old["y"]) or (new["z"] != old["z"]):
        self.place()
        self.ideal.move(self)
      if new["parent"] != old["parent"]:
        self.update_hierarchy(new["parent"], old["parent"])
        self.place()
        self.ideal.move(self)
        
  def update_hierarchy(self, newparent, oldparent):
    world = oldparent
    while world:
      unobserve(world, self.on_parent_changed)
      world = world.parent
      
    world = newparent
    while world:
      observe(world, self.on_parent_changed)
      world = world.parent
      
  def __repr__(self): return "<OrientationHandle for %s>" % (self.orientation,)
